OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-identity commits / 029fa19

0.0.3: attestDeviceViaDevice — an attested device can admit the next one
Admitting a new device to a room needs a proof anchored at the room's ROOT identity, and attestDeviceChain needs the root MNEMONIC. That mnemonic is deliberately not stored on any node (config.js: a device "must never hold" it), and none exists anywhere in the fleet — ashatron has no vault at all, tabertron's holds devicePrivateKey/rootDidKey/rotationKeys and no mnemonic. Read from our wrapper alone, adding a co-owner therefore looked impossible without recovering a secret nobody kept. It isn't. keet's attestDevice(publicKey, parent, proof) signs { epoch, identity, device } with the PARENT's keypair and pushes onto the parent's chain, so an already-attested device has everything needed to vouch for the next one. The result resolves to the same root identity and the same epoch, so it passes the identical admission gate and the identical minimum-epoch receipt check. Only the root's PUBLIC key is needed to verify, and every node already carries it as rootIdentityKey. The proof is passed as BYTES on purpose: keet decodes a fresh copy, so the caller's proof is never mutated by the chain.push inside attestDevice — a wrapper passing the decoded object would grow the parent's own proof on every call and eventually break it. atproto-room's membership suite asserts the parent still verifies after vouching, precisely to pin that. Proven through the REAL admission crypto (makeAttest/makeVerifyAttest, what StateBaseV3 injects), not just the library call: the chained device is admitted and resolves to the same root; it cannot claim to be its parent; it is still bound to its signData; and chaining does not launder a foreign root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
029fa190b3208d27385b4c8ca7c05829f12dc50e
by plankatron <fleeky@gmail.com> · 2026-07-27T20:39:08.000Z

Changed files (2) +23 -1

lib/device-chain.js +22 -0
……@@ -32,6 +32,28 @@ export async function attestDeviceChain ({ rootMnemonic, rootSeed, devicePublicK
3232 return IdentityKey.bootstrap({ mnemonic: rootMnemonic, seed: rootSeed }, devicePublicKey)
3333}
3434
35// An ALREADY-ATTESTED device attests ANOTHER device, extending the chain: root → parent → child.
36// keet's attestDevice signs { epoch, identity, device } with the PARENT's keypair and pushes
37// { publicKey, signature } onto the parent's chain, so the result still resolves to the same root
38// identity and the same epoch — a chained device passes the identical admission gate and the identical
39// minimum-epoch receipt check as a root-attested one.
40//
41// WHY THIS MATTERS OPERATIONALLY: admitting a new device to a room needs a proof anchored at the room's
42// root identity, and attestDeviceChain above needs the ROOT MNEMONIC — which is deliberately not stored
43// on any node (config.js: a device "must never hold" it). Read alone, that makes adding a co-owner
44// impossible without recovering a secret nobody kept. It is not: an existing attested device already has
45// everything required to vouch for the next one. Only the root's PUBLIC key is needed to verify.
46//
47// parentKeyPair: the attesting device's Ed25519 identity keypair (deviceKeetKeyPair of its mnemonic).
48// parentProof: that device's own proof (root-attested, or itself chained — chains nest).
49export function attestDeviceViaDevice ({ parentKeyPair, parentProof, devicePublicKey }) {
50 if (!parentKeyPair || !parentProof || !devicePublicKey) throw new Error('attestDeviceViaDevice: parentKeyPair, parentProof, devicePublicKey required')
51 // Pass the proof as BYTES: keet decodes a fresh copy, so the caller's proof is never mutated by the
52 // chain.push inside attestDevice (it would otherwise grow the parent's own proof on every call).
53 const proof = b4a.isBuffer(parentProof) ? parentProof : b4a.from(parentProof)
54 return IdentityKey.attestDevice(b4a.from(devicePublicKey), parentKeyPair, proof)
55}
56
3557// The attested (leaf) device vouches for the atproto DID by signing it as attested data,
3658// extending the proof with `proof.data`. Result: root → device → vouches-for(did).
3759export function attestAtprotoBinding ({ deviceKeyPair, did, proof }) {
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "atproto-identity",
3 "version": "0.0.2",
3 "version": "0.0.3",
44 "description": "Seed-anchored atproto identity: one BIP39 mnemonic -> Ed25519 (Hypercore) + secp256k1 (did:plc). Offline did:plc genesis + verification.",
55 "type": "module",
66 "main": "lib/atproto-identity.js",