// Attestation — the one place that knows keet-identity-key.
//
// A device proves it belongs to an identity by carrying a CHAIN PROOF: a signed path from the
// identity root down to the device's own public key. Verifying one is a pure function of the proof's
// bytes — no disk, no network, no clock — which is the property that lets it run inside `apply`.
//
// ── Why this file exists rather than calling IdentityKey directly ─────────────────────────────
//
// 1. ⛔ `IdentityKey.verify` THROWS on malformed input. Measured, not assumed: an empty buffer and
// a truncated one both raise "Out of bounds", and random bytes raise "uint is greater than the
// maximum safe integer". apply must be TOTAL — a throw mid-replay wedges the base and the op
// that caused it is already durable, so there is no version of "fix it and retry". Every call
// is wrapped here, once, instead of at each site.
//
// 2. It returns the CHAIN, which `verify` does not. `verify` gives back
// `{ receipt, identityPublicKey, devicePublicKey }` and drops the ancestry — but the ancestry is
// exactly what revocation needs, because `attestDevice(publicKey, parent, proof)` lets any
// device mint further devices using only its own keypair and its own proof. No mnemonic. So a
// compromised device can have descendants, and revoking it has to reach them.
// ⚠ That means decoding `ProofEncoding` from the package's `lib/`, which is internal. It is
// reachable (keet-identity-key declares no `exports` map), and 3.1↔3.2 are wire-identical, but
// it is a coupling and it lives HERE so there is one place to fix if it moves.
//
// 3. Everything comes back as hex STRINGS. The view stores strings, apply compares strings, and a
// Buffer that compared by identity instead of by value would be a silent authorisation bug.
import IdentityKey from 'keet-identity-key'
import { ProofEncoding } from 'keet-identity-key/lib/encoding'
import c from 'compact-encoding'
import b4a from 'b4a'
const hex = (k) => (typeof k === 'string' ? k : b4a.toString(k, 'hex'))
const isKeyHex = (s) => typeof s === 'string' && /^[0-9a-f]{64}$/i.test(s)
/**
* readAttested(proof, attestedData, expectedRootHex) -> { root, device, chain } | null
*
* root hex identity public key the chain resolves to — the ANCHOR
* device hex DEVICE key this proof attests — a real Ed25519 keypair, one that can sign
* chain hex device keys from the root's first child down to `device`. The ANCESTRY.
*
* The general form: verify that some identity attested `attestedData`, optionally pinning WHICH
* identity. One `verify` call settles both halves — it walks root → … → device checking each
* link's signature, then checks `proof.data.signature` against the LAST device in that chain. So a
* non-null return means exactly: *this root attested this device, and that device signed these
* bytes.* Neither half is trusted separately, and there is no second primitive to keep in step.
*
* ⛔ `expectedRootHex` is pinned INSIDE verify (`opts.expectedIdentity`) rather than string-compared
* afterwards. keet does the same at `keet-core/lib/core/invitations/index.js:122`. A comparison the
* caller has to remember to write is a comparison someone eventually forgets.
*
* ⚠ `attestedData` is not optional for a proof that carries `data`: `verify` on one, called without
* it, throws inside blake2b ("input must be a typed array") rather than returning null. Guarded
* here, because the caller is apply and apply may not throw for any reason at all.
*
* TOTAL: returns null for anything it cannot verify, and never throws. `proof` may be a Buffer or
* base64 — a daemon stores it as text and hands it back over a socket, so both arrive in practice.
*/
export function readAttested (proof, attestedData, expectedRootHex) {
if (!proof) return null
let buf
try {
buf = typeof proof === 'string' ? b4a.from(proof, 'base64') : proof
} catch {
return null
}
if (!buf || buf.length === 0) return null
if (attestedData !== undefined && (!attestedData || attestedData.length === 0)) return null
const opts = {}
if (expectedRootHex !== undefined) {
if (!isKeyHex(expectedRootHex)) return null
opts.expectedIdentity = b4a.from(expectedRootHex, 'hex')
}
// Two independent throwers: verify() on malformed bytes, and decode() on bytes that verify
// happened to accept. Both are inside the same guard.
try {
const verified = attestedData === undefined
? IdentityKey.verify(buf, null, opts)
: IdentityKey.verify(buf, attestedData, opts)
if (!verified) return null
const decoded = c.decode(ProofEncoding, buf)
if (!decoded || !Array.isArray(decoded.chain)) return null
return {
root: hex(verified.identityPublicKey),
device: hex(verified.devicePublicKey),
chain: decoded.chain.map((link) => hex(link.publicKey))
}
} catch {
return null
}
}
/**
* readProof(proof, writerKeyHex) -> { root, device, chain } | null
*
* The admission case: `writerKeyHex` is the hypercore writer key being admitted, verified as
* ATTESTED DATA. That indirection is the whole design:
*
* - A writer core key is MANIFEST-derived. Nobody holds its secret half, so it can never sign,
* so a chain naming it can never be extended — measured: attesting one and then trying to
* attest from it is REFUSED.
* - A device key is an ordinary keypair. It CAN sign, so an already-linked device can attest the
* next one with no mnemonic — which is exactly what pear-git-v3's DeviceLinker does over
* blind-pairing, shipping { deviceSecretKey, deviceProof } and never the root.
*
* So the device key carries the identity, and the writer key is bound to it as data.
*/
export function readProof (proof, writerKeyHex) {
if (writerKeyHex === undefined) return readAttested(proof, undefined)
if (typeof writerKeyHex !== 'string' || writerKeyHex.length === 0) return null
let attested
try { attested = b4a.from(writerKeyHex, 'hex') } catch { return null }
if (attested.length === 0) return null
return readAttested(proof, attested)
}
/**
* Sign arbitrary bytes as this device, carrying the chain that proves who it is.
*
* The inverse of `readAttested`. `attestData` re-encodes the credential's proof with a `data`
* signature over `hash(bytes)` made by the device's own keypair — so the result is a self-contained
* "identity R, via device D, signed these bytes", verifiable with no lookup and no state.
*
* ⚠ It OVERWRITES `proof.data`, so one credential proof yields one signature per call and each call
* must start from the stored credential rather than from a previous result. Decoding from the
* stored base64 each time is what keeps that true.
*/
export function signAttested (bytes, cred) {
if (!cred || !cred.keyPair || !cred.proof) throw new Error('signing needs a device credential { keyPair, proof }')
const proof = typeof cred.proof === 'string' ? b4a.from(cred.proof, 'base64') : cred.proof
return b4a.toString(IdentityKey.attestData(bytes, cred.keyPair, proof), 'base64')
}
export default { readProof, readAttested, signAttested }