| … | … | @@ -1,9 +1,9 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "favela-peer", |
| 3 | "version": "0.0.35", | |
| 3 | "version": "0.0.36", | |
| 4 | 4 | "runtime": "bare", |
| 5 | "rootTreeHash": "24f7243a6a113102f8b6d7ccfbf76d383b73386b83d012252eb7622468f4f35c", | |
| 6 | "publishedAt": "2026-09-07T20:22:21.564Z", | |
| 5 | "rootTreeHash": "23b06cbe3a6df55b474321e938d032a04b6eea4ab383602bc2981871b178d47e", | |
| 6 | "publishedAt": "2026-09-08T01:03:14.653Z", | |
| 7 | 7 | "deps": [ |
| 8 | 8 | { |
| 9 | 9 | "name": "@hyperswarm/secret-stream", |
| … | … | @@ -3009,8 +3009,8 @@ |
| 3009 | 3009 | "tree": "c5f18b3620514f54714aeb351f074adface216a335242e323e33d28d261376fe" |
| 3010 | 3010 | } |
| 3011 | 3011 | ], |
| 3012 | "reason": "0.0.35 authorship edge", | |
| 3013 | "acceptedAt": "2026-09-07T20:22:21.578Z" | |
| 3012 | "reason": "0.0.36 reach", | |
| 3013 | "acceptedAt": "2026-09-08T01:03:14.667Z" | |
| 3014 | 3014 | }, |
| 3015 | 3015 | { |
| 3016 | 3016 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3052,10 +3052,15 @@ |
| 3052 | 3052 | "appName": "pgh-sidecar", |
| 3053 | 3053 | "version": "3.6.1", |
| 3054 | 3054 | "tree": "6bb5233025d1c30e4f181e29b6ab5af549d8909c7070c7b4438b30f1a4e76ce2" |
| 3055 | }, | |
| 3056 | { | |
| 3057 | "appName": "favela-bridge", | |
| 3058 | "version": "3.6.1", | |
| 3059 | "tree": "6bb5233025d1c30e4f181e29b6ab5af549d8909c7070c7b4438b30f1a4e76ce2" | |
| 3055 | 3060 | } |
| 3056 | 3061 | ], |
| 3057 | "reason": "0.0.35 authorship edge", | |
| 3058 | "acceptedAt": "2026-09-07T20:22:21.578Z" | |
| 3062 | "reason": "0.0.36 reach", | |
| 3063 | "acceptedAt": "2026-09-08T01:03:14.667Z" | |
| 3059 | 3064 | }, |
| 3060 | 3065 | { |
| 3061 | 3066 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3069,8 +3074,8 @@ |
| 3069 | 3074 | "tree": "58c0bf8698af19464d0ee8354a77ed1ac96d3a5fa18aa4e794686cfbbf080e5d" |
| 3070 | 3075 | } |
| 3071 | 3076 | ], |
| 3072 | "reason": "0.0.35 authorship edge", | |
| 3073 | "acceptedAt": "2026-09-07T20:22:21.578Z" | |
| 3077 | "reason": "0.0.36 reach", | |
| 3078 | "acceptedAt": "2026-09-08T01:03:14.667Z" | |
| 3074 | 3079 | }, |
| 3075 | 3080 | { |
| 3076 | 3081 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3094,8 +3099,8 @@ |
| 3094 | 3099 | "tree": "bdebba5a532ce4846b49f1e5ffad774becfc44182d0c3737f7e441ab42f4c43d" |
| 3095 | 3100 | } |
| 3096 | 3101 | ], |
| 3097 | "reason": "0.0.35 authorship edge", | |
| 3098 | "acceptedAt": "2026-09-07T20:22:21.578Z" | |
| 3102 | "reason": "0.0.36 reach", | |
| 3103 | "acceptedAt": "2026-09-08T01:03:14.667Z" | |
| 3099 | 3104 | }, |
| 3100 | 3105 | { |
| 3101 | 3106 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3122,10 +3127,15 @@ |
| 3122 | 3127 | "appName": "hyper-proto-peer", |
| 3123 | 3128 | "version": "0.1.4", |
| 3124 | 3129 | "tree": "b799e6ba8726aa951b45f12cc7ecffe5f4e9ee1e8adf09a34d026d9d5658828a" |
| 3130 | }, | |
| 3131 | { | |
| 3132 | "appName": "favela-bridge", | |
| 3133 | "version": "0.1.4", | |
| 3134 | "tree": "b799e6ba8726aa951b45f12cc7ecffe5f4e9ee1e8adf09a34d026d9d5658828a" | |
| 3125 | 3135 | } |
| 3126 | 3136 | ], |
| 3127 | "reason": "0.0.35 authorship edge", | |
| 3128 | "acceptedAt": "2026-09-07T20:22:21.578Z" | |
| 3137 | "reason": "0.0.36 reach", | |
| 3138 | "acceptedAt": "2026-09-08T01:03:14.667Z" | |
| 3129 | 3139 | }, |
| 3130 | 3140 | { |
| 3131 | 3141 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3139,8 +3149,8 @@ |
| 3139 | 3149 | "tree": "e92100a4f821a638ac99c1407e4c7e784f8eb9661e6d9119ecbec4faae77cc7e" |
| 3140 | 3150 | } |
| 3141 | 3151 | ], |
| 3142 | "reason": "0.0.35 authorship edge", | |
| 3143 | "acceptedAt": "2026-09-07T20:22:21.578Z" | |
| 3152 | "reason": "0.0.36 reach", | |
| 3153 | "acceptedAt": "2026-09-08T01:03:14.667Z" | |
| 3144 | 3154 | }, |
| 3145 | 3155 | { |
| 3146 | 3156 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3154,8 +3164,8 @@ |
| 3154 | 3164 | "tree": "1f5de2daa08643a5cb8bce2153ca4d55d4504fb366f1f1ef80bca1700ec085f7" |
| 3155 | 3165 | } |
| 3156 | 3166 | ], |
| 3157 | "reason": "0.0.35 authorship edge", | |
| 3158 | "acceptedAt": "2026-09-07T20:22:21.578Z" | |
| 3167 | "reason": "0.0.36 reach", | |
| 3168 | "acceptedAt": "2026-09-08T01:03:14.667Z" | |
| 3159 | 3169 | }, |
| 3160 | 3170 | { |
| 3161 | 3171 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3169,8 +3179,8 @@ |
| 3169 | 3179 | "tree": "579bdc1eaabbca726d3916f1fc74cfd4986653f6748632db586817a2a8caabea" |
| 3170 | 3180 | } |
| 3171 | 3181 | ], |
| 3172 | "reason": "0.0.35 authorship edge", | |
| 3173 | "acceptedAt": "2026-09-07T20:22:21.578Z" | |
| 3182 | "reason": "0.0.36 reach", | |
| 3183 | "acceptedAt": "2026-09-08T01:03:14.668Z" | |
| 3174 | 3184 | }, |
| 3175 | 3185 | { |
| 3176 | 3186 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3184,8 +3194,8 @@ |
| 3184 | 3194 | "tree": "967cf214778dbacd7cc7cc3850ffdff6a6f145b4bd73de741d0938d7d84d33d5" |
| 3185 | 3195 | } |
| 3186 | 3196 | ], |
| 3187 | "reason": "0.0.35 authorship edge", | |
| 3188 | "acceptedAt": "2026-09-07T20:22:21.578Z" | |
| 3197 | "reason": "0.0.36 reach", | |
| 3198 | "acceptedAt": "2026-09-08T01:03:14.668Z" | |
| 3189 | 3199 | }, |
| 3190 | 3200 | { |
| 3191 | 3201 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3207,10 +3217,15 @@ |
| 3207 | 3217 | "appName": "atproto-pds", |
| 3208 | 3218 | "version": "7.11.0", |
| 3209 | 3219 | "tree": "35b214bfbf85f8bbcb9f1926470e77f4e240d9b5045b07f81ce83fec24c1380c" |
| 3220 | }, | |
| 3221 | { | |
| 3222 | "appName": "favela-bridge", | |
| 3223 | "version": "7.11.0", | |
| 3224 | "tree": "35b214bfbf85f8bbcb9f1926470e77f4e240d9b5045b07f81ce83fec24c1380c" | |
| 3210 | 3225 | } |
| 3211 | 3226 | ], |
| 3212 | "reason": "0.0.35 authorship edge", | |
| 3213 | "acceptedAt": "2026-09-07T20:22:21.578Z" | |
| 3227 | "reason": "0.0.36 reach", | |
| 3228 | "acceptedAt": "2026-09-08T01:03:14.668Z" | |
| 3214 | 3229 | }, |
| 3215 | 3230 | { |
| 3216 | 3231 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3224,8 +3239,8 @@ |
| 3224 | 3239 | "tree": "67c4c05f69061f65a2548595c43282be2da8e0934e6ce347afa034b29cc40c81" |
| 3225 | 3240 | } |
| 3226 | 3241 | ], |
| 3227 | "reason": "0.0.35 authorship edge", | |
| 3228 | "acceptedAt": "2026-09-07T20:22:21.578Z" | |
| 3242 | "reason": "0.0.36 reach", | |
| 3243 | "acceptedAt": "2026-09-08T01:03:14.668Z" | |
| 3229 | 3244 | }, |
| 3230 | 3245 | { |
| 3231 | 3246 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3247,10 +3262,15 @@ |
| 3247 | 3262 | "appName": "atproto-pds", |
| 3248 | 3263 | "version": "11.33.5", |
| 3249 | 3264 | "tree": "4ef8a03ba96b1488cb7ee35dc582b14d096ae4b3fab5481186c94516d434fe18" |
| 3265 | }, | |
| 3266 | { | |
| 3267 | "appName": "favela-bridge", | |
| 3268 | "version": "11.33.5", | |
| 3269 | "tree": "4ef8a03ba96b1488cb7ee35dc582b14d096ae4b3fab5481186c94516d434fe18" | |
| 3250 | 3270 | } |
| 3251 | 3271 | ], |
| 3252 | "reason": "0.0.35 authorship edge", | |
| 3253 | "acceptedAt": "2026-09-07T20:22:21.578Z" | |
| 3272 | "reason": "0.0.36 reach", | |
| 3273 | "acceptedAt": "2026-09-08T01:03:14.668Z" | |
| 3254 | 3274 | }, |
| 3255 | 3275 | { |
| 3256 | 3276 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3272,10 +3292,15 @@ |
| 3272 | 3292 | "appName": "atproto-pds", |
| 3273 | 3293 | "version": "3.1.2", |
| 3274 | 3294 | "tree": "b7491052487798618f5759cb50e0b98c89db49c6f1b475ca1e5f8ad7893ddb06" |
| 3295 | }, | |
| 3296 | { | |
| 3297 | "appName": "favela-bridge", | |
| 3298 | "version": "3.2.0", | |
| 3299 | "tree": "5fc49d698f6ddfd653b3cdaa72ec572d6804293e2e9c3e98303f69417115e903" | |
| 3275 | 3300 | } |
| 3276 | 3301 | ], |
| 3277 | "reason": "0.0.35 authorship edge", | |
| 3278 | "acceptedAt": "2026-09-07T20:22:21.578Z" | |
| 3302 | "reason": "0.0.36 reach", | |
| 3303 | "acceptedAt": "2026-09-08T01:03:14.668Z" | |
| 3279 | 3304 | }, |
| 3280 | 3305 | { |
| 3281 | 3306 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3289,8 +3314,8 @@ |
| 3289 | 3314 | "tree": "d0099e1743480f28a08c7374d20ad5048088475180ab606626b63acd59e365a5" |
| 3290 | 3315 | } |
| 3291 | 3316 | ], |
| 3292 | "reason": "0.0.35 authorship edge", | |
| 3293 | "acceptedAt": "2026-09-07T20:22:21.578Z" | |
| 3317 | "reason": "0.0.36 reach", | |
| 3318 | "acceptedAt": "2026-09-08T01:03:14.668Z" | |
| 3294 | 3319 | }, |
| 3295 | 3320 | { |
| 3296 | 3321 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3304,8 +3329,8 @@ |
| 3304 | 3329 | "tree": "60429c7756f039ec2b3e5240f702dee450c1da19ad6de06900a3877c6c72bb52" |
| 3305 | 3330 | } |
| 3306 | 3331 | ], |
| 3307 | "reason": "0.0.35 authorship edge", | |
| 3308 | "acceptedAt": "2026-09-07T20:22:21.578Z" | |
| 3332 | "reason": "0.0.36 reach", | |
| 3333 | "acceptedAt": "2026-09-08T01:03:14.668Z" | |
| 3309 | 3334 | }, |
| 3310 | 3335 | { |
| 3311 | 3336 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3327,10 +3352,15 @@ |
| 3327 | 3352 | "appName": "atproto-pds", |
| 3328 | 3353 | "version": "3.17.1", |
| 3329 | 3354 | "tree": "5ad1a6959a786417767537ed91321b27eb23a8388fb53cbf868c15f901e0d3d9" |
| 3355 | }, | |
| 3356 | { | |
| 3357 | "appName": "favela-bridge", | |
| 3358 | "version": "3.17.2", | |
| 3359 | "tree": "6fc80ad1ccbfb046613f36299032f21a4752e2614ff8d88a7a5bd7ce66fc1293" | |
| 3330 | 3360 | } |
| 3331 | 3361 | ], |
| 3332 | "reason": "0.0.35 authorship edge", | |
| 3333 | "acceptedAt": "2026-09-07T20:22:21.578Z" | |
| 3362 | "reason": "0.0.36 reach", | |
| 3363 | "acceptedAt": "2026-09-08T01:03:14.668Z" | |
| 3334 | 3364 | }, |
| 3335 | 3365 | { |
| 3336 | 3366 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3349,8 +3379,8 @@ |
| 3349 | 3379 | "tree": "92424acff1a50468df203528e41739f44f602904abc4beb34500ce172c6d0ad2" |
| 3350 | 3380 | } |
| 3351 | 3381 | ], |
| 3352 | "reason": "0.0.35 authorship edge", | |
| 3353 | "acceptedAt": "2026-09-07T20:22:21.578Z" | |
| 3382 | "reason": "0.0.36 reach", | |
| 3383 | "acceptedAt": "2026-09-08T01:03:14.668Z" | |
| 3354 | 3384 | }, |
| 3355 | 3385 | { |
| 3356 | 3386 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3367,10 +3397,15 @@ |
| 3367 | 3397 | "appName": "hyperident", |
| 3368 | 3398 | "version": "6.30.0", |
| 3369 | 3399 | "tree": "fc36977bda7dd2c7467653aa2ef520890b3a0aae16870a9269f0f71691673dce" |
| 3400 | }, | |
| 3401 | { | |
| 3402 | "appName": "favela-bridge", | |
| 3403 | "version": "6.33.0", | |
| 3404 | "tree": "36d01631ddee74ac2ce62971abd7c0ac30562da8dfd6495480da56c534eb80b7" | |
| 3370 | 3405 | } |
| 3371 | 3406 | ], |
| 3372 | "reason": "0.0.35 authorship edge", | |
| 3373 | "acceptedAt": "2026-09-07T20:22:21.578Z" | |
| 3407 | "reason": "0.0.36 reach", | |
| 3408 | "acceptedAt": "2026-09-08T01:03:14.668Z" | |
| 3374 | 3409 | }, |
| 3375 | 3410 | { |
| 3376 | 3411 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3384,8 +3419,8 @@ |
| 3384 | 3419 | "tree": "bba869f5c85b11bce5671d57a87cd2793b490588867cd07fbe94cad796114970" |
| 3385 | 3420 | } |
| 3386 | 3421 | ], |
| 3387 | "reason": "0.0.35 authorship edge", | |
| 3388 | "acceptedAt": "2026-09-07T20:22:21.578Z" | |
| 3422 | "reason": "0.0.36 reach", | |
| 3423 | "acceptedAt": "2026-09-08T01:03:14.668Z" | |
| 3389 | 3424 | }, |
| 3390 | 3425 | { |
| 3391 | 3426 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3399,8 +3434,8 @@ |
| 3399 | 3434 | "tree": "2ecc91a8c3595dda5dfc86ace62a70875520adc4e588a2ac14945dbc4313c450" |
| 3400 | 3435 | } |
| 3401 | 3436 | ], |
| 3402 | "reason": "0.0.35 authorship edge", | |
| 3403 | "acceptedAt": "2026-09-07T20:22:21.578Z" | |
| 3437 | "reason": "0.0.36 reach", | |
| 3438 | "acceptedAt": "2026-09-08T01:03:14.668Z" | |
| 3404 | 3439 | } |
| 3405 | 3440 | ] |
| 3406 | 3441 | } |
| … | … | @@ -1,142 +1,17 @@ |
| 1 | // Attestation — the one place that knows keet-identity-key. | |
| 1 | // Attestation — re-exported from `favela-identity`, which is where it lives now. | |
| 2 | 2 | // |
| 3 | // A device proves it belongs to an identity by carrying a CHAIN PROOF: a signed path from the | |
| 4 | // identity root down to the device's own public key. Verifying one is a pure function of the proof's | |
| 5 | // bytes — no disk, no network, no clock — which is the property that lets it run inside `apply`. | |
| 3 | // ⛔ ONE COPY. This file used to hold the implementation, and its own header explained why the | |
| 4 | // keet-identity-key `ProofEncoding` coupling "lives HERE so there is one place to fix if it moves". | |
| 5 | // Other components need the same verifier — proving who someone is from a chain proof alone — and a | |
| 6 | // second copy would put that coupling in two places. That is the failure | |
| 7 | // `favela-markdown/lib/reach.js` names: "written three times… and the third one would eventually be | |
| 8 | // written wrong". | |
| 6 | 9 | // |
| 7 | // ── Why this file exists rather than calling IdentityKey directly ───────────────────────────── | |
| 10 | // ⚠ Kept as a re-export rather than deleted. Eight modules import it by this path, and relocating an | |
| 11 | // implementation is not a reason to touch eight call sites in the same change. | |
| 8 | 12 | // |
| 9 | // 1. ⛔ `IdentityKey.verify` THROWS on malformed input. Measured, not assumed: an empty buffer and | |
| 10 | // a truncated one both raise "Out of bounds", and random bytes raise "uint is greater than the | |
| 11 | // maximum safe integer". apply must be TOTAL — a throw mid-replay wedges the base and the op | |
| 12 | // that caused it is already durable, so there is no version of "fix it and retry". Every call | |
| 13 | // is wrapped here, once, instead of at each site. | |
| 14 | // | |
| 15 | // 2. It returns the CHAIN, which `verify` does not. `verify` gives back | |
| 16 | // `{ receipt, identityPublicKey, devicePublicKey }` and drops the ancestry — but the ancestry is | |
| 17 | // exactly what revocation needs, because `attestDevice(publicKey, parent, proof)` lets any | |
| 18 | // device mint further devices using only its own keypair and its own proof. No mnemonic. So a | |
| 19 | // compromised device can have descendants, and revoking it has to reach them. | |
| 20 | // ⚠ That means decoding `ProofEncoding` from the package's `lib/`, which is internal. It is | |
| 21 | // reachable (keet-identity-key declares no `exports` map), and 3.1↔3.2 are wire-identical, but | |
| 22 | // it is a coupling and it lives HERE so there is one place to fix if it moves. | |
| 23 | // | |
| 24 | // 3. Everything comes back as hex STRINGS. The view stores strings, apply compares strings, and a | |
| 25 | // Buffer that compared by identity instead of by value would be a silent authorisation bug. | |
| 26 | ||
| 27 | import IdentityKey from 'keet-identity-key' | |
| 28 | import { ProofEncoding } from 'keet-identity-key/lib/encoding' | |
| 29 | import c from 'compact-encoding' | |
| 30 | import b4a from 'b4a' | |
| 31 | ||
| 32 | const hex = (k) => (typeof k === 'string' ? k : b4a.toString(k, 'hex')) | |
| 33 | const isKeyHex = (s) => typeof s === 'string' && /^[0-9a-f]{64}$/i.test(s) | |
| 34 | ||
| 35 | /** | |
| 36 | * readAttested(proof, attestedData, expectedRootHex) -> { root, device, chain } | null | |
| 37 | * | |
| 38 | * root hex identity public key the chain resolves to — the ANCHOR | |
| 39 | * device hex DEVICE key this proof attests — a real Ed25519 keypair, one that can sign | |
| 40 | * chain hex device keys from the root's first child down to `device`. The ANCESTRY. | |
| 41 | * | |
| 42 | * The general form: verify that some identity attested `attestedData`, optionally pinning WHICH | |
| 43 | * identity. One `verify` call settles both halves — it walks root → … → device checking each | |
| 44 | * link's signature, then checks `proof.data.signature` against the LAST device in that chain. So a | |
| 45 | * non-null return means exactly: *this root attested this device, and that device signed these | |
| 46 | * bytes.* Neither half is trusted separately, and there is no second primitive to keep in step. | |
| 47 | * | |
| 48 | * ⛔ `expectedRootHex` is pinned INSIDE verify (`opts.expectedIdentity`) rather than string-compared | |
| 49 | * afterwards. keet does the same at `keet-core/lib/core/invitations/index.js:122`. A comparison the | |
| 50 | * caller has to remember to write is a comparison someone eventually forgets. | |
| 51 | * | |
| 52 | * ⚠ `attestedData` is not optional for a proof that carries `data`: `verify` on one, called without | |
| 53 | * it, throws inside blake2b ("input must be a typed array") rather than returning null. Guarded | |
| 54 | * here, because the caller is apply and apply may not throw for any reason at all. | |
| 55 | * | |
| 56 | * TOTAL: returns null for anything it cannot verify, and never throws. `proof` may be a Buffer or | |
| 57 | * base64 — a daemon stores it as text and hands it back over a socket, so both arrive in practice. | |
| 58 | */ | |
| 59 | export function readAttested (proof, attestedData, expectedRootHex) { | |
| 60 | if (!proof) return null | |
| 61 | ||
| 62 | let buf | |
| 63 | try { | |
| 64 | buf = typeof proof === 'string' ? b4a.from(proof, 'base64') : proof | |
| 65 | } catch { | |
| 66 | return null | |
| 67 | } | |
| 68 | if (!buf || buf.length === 0) return null | |
| 69 | ||
| 70 | if (attestedData !== undefined && (!attestedData || attestedData.length === 0)) return null | |
| 71 | ||
| 72 | const opts = {} | |
| 73 | if (expectedRootHex !== undefined) { | |
| 74 | if (!isKeyHex(expectedRootHex)) return null | |
| 75 | opts.expectedIdentity = b4a.from(expectedRootHex, 'hex') | |
| 76 | } | |
| 77 | ||
| 78 | // Two independent throwers: verify() on malformed bytes, and decode() on bytes that verify | |
| 79 | // happened to accept. Both are inside the same guard. | |
| 80 | try { | |
| 81 | const verified = attestedData === undefined | |
| 82 | ? IdentityKey.verify(buf, null, opts) | |
| 83 | : IdentityKey.verify(buf, attestedData, opts) | |
| 84 | if (!verified) return null | |
| 85 | ||
| 86 | const decoded = c.decode(ProofEncoding, buf) | |
| 87 | if (!decoded || !Array.isArray(decoded.chain)) return null | |
| 88 | ||
| 89 | return { | |
| 90 | root: hex(verified.identityPublicKey), | |
| 91 | device: hex(verified.devicePublicKey), | |
| 92 | chain: decoded.chain.map((link) => hex(link.publicKey)) | |
| 93 | } | |
| 94 | } catch { | |
| 95 | return null | |
| 96 | } | |
| 97 | } | |
| 98 | ||
| 99 | /** | |
| 100 | * readProof(proof, writerKeyHex) -> { root, device, chain } | null | |
| 101 | * | |
| 102 | * The admission case: `writerKeyHex` is the hypercore writer key being admitted, verified as | |
| 103 | * ATTESTED DATA. That indirection is the whole design: | |
| 104 | * | |
| 105 | * - A writer core key is MANIFEST-derived. Nobody holds its secret half, so it can never sign, | |
| 106 | * so a chain naming it can never be extended — measured: attesting one and then trying to | |
| 107 | * attest from it is REFUSED. | |
| 108 | * - A device key is an ordinary keypair. It CAN sign, so an already-linked device can attest the | |
| 109 | * next one with no mnemonic — which is exactly what pear-git-v3's DeviceLinker does over | |
| 110 | * blind-pairing, shipping { deviceSecretKey, deviceProof } and never the root. | |
| 111 | * | |
| 112 | * So the device key carries the identity, and the writer key is bound to it as data. | |
| 113 | */ | |
| 114 | export function readProof (proof, writerKeyHex) { | |
| 115 | if (writerKeyHex === undefined) return readAttested(proof, undefined) | |
| 116 | if (typeof writerKeyHex !== 'string' || writerKeyHex.length === 0) return null | |
| 117 | ||
| 118 | let attested | |
| 119 | try { attested = b4a.from(writerKeyHex, 'hex') } catch { return null } | |
| 120 | if (attested.length === 0) return null | |
| 121 | ||
| 122 | return readAttested(proof, attested) | |
| 123 | } | |
| 124 | ||
| 125 | /** | |
| 126 | * Sign arbitrary bytes as this device, carrying the chain that proves who it is. | |
| 127 | * | |
| 128 | * The inverse of `readAttested`. `attestData` re-encodes the credential's proof with a `data` | |
| 129 | * signature over `hash(bytes)` made by the device's own keypair — so the result is a self-contained | |
| 130 | * "identity R, via device D, signed these bytes", verifiable with no lookup and no state. | |
| 131 | * | |
| 132 | * ⚠ It OVERWRITES `proof.data`, so one credential proof yields one signature per call and each call | |
| 133 | * must start from the stored credential rather than from a previous result. Decoding from the | |
| 134 | * stored base64 each time is what keeps that true. | |
| 135 | */ | |
| 136 | export function signAttested (bytes, cred) { | |
| 137 | if (!cred || !cred.keyPair || !cred.proof) throw new Error('signing needs a device credential { keyPair, proof }') | |
| 138 | const proof = typeof cred.proof === 'string' ? b4a.from(cred.proof, 'base64') : cred.proof | |
| 139 | return b4a.toString(IdentityKey.attestData(bytes, cred.keyPair, proof), 'base64') | |
| 140 | } | |
| 141 | ||
| 142 | export default { readProof, readAttested, signAttested } | |
| 13 | // ⚠ And the wording here is load-bearing: `test/boundary.test.js` refuses any source file in this | |
| 14 | // package that names another network's stack, and the first draft of this comment named two. The | |
| 15 | // daemon does not know what is downstream of it, including in its prose. | |
| 16 | export { readAttested, readProof, signAttested } from 'favela-identity/lib/attestation.js' | |
| 17 | export { default } from 'favela-identity/lib/attestation.js' |
| … | … | @@ -47,7 +47,8 @@ |
| 47 | 47 | "anproto": "file:../anproto", |
| 48 | 48 | "blind-pairing": "^2.3.1", |
| 49 | 49 | "sodium-hmac": "^2.0.1", |
| 50 | "bip39-mnemonic": "^2.2.0" | |
| 50 | "bip39-mnemonic": "^2.2.0", | |
| 51 | "favela-identity": "file:../favela-identity" | |
| 51 | 52 | }, |
| 52 | 53 | "devDependencies": { |
| 53 | 54 | "brittle": "^4.0.2" |