Deed Link
Multi-device continuity for a KeyAuth identity: enroll a
second, third, … device without ever handing over the original private key. Not to be confused
with cross-slice calling (drift slice link, an unrelated feature for one of your
own slices calling another); this Link enrolls a device for one identity.
- Called by
- the joining device
- What it does
- Opens a session and returns its id.
metadatais an opaque string the attester can read back.
- Called by
- either
- What it does
- Renders a short string, in practice the session id, as SVG markup, server-side.
- Called by
- the attesting device
- What it does
- Returns the session's
new_pubkeyandmetadata, and needsJWT.SliceIDbeside it for the identity value the signed message also carries. Read-only and repeatable.
- Called by
- an active device
- What it does
- Vouches for the session's device.
sealedis an opaque payload relayed to the joiner.
- Called by
- the joining device
- What it does
- Polls the session:
pending, thenattestedwith the identity and anysealedpayload.
- Called by
- an active device
- What it does
- Deactivates a device, including itself.
Enrolling a second device
SessionInfo is not optional in practice. The attesting device only ever learns the session
id, from a scanned or typed code, and the message it has to sign is over the joiner's
new_pubkey plus this slice's own identity. The slice verifies both against values it
holds itself and never trusts the request body, so the attester has to fetch them and rebuild the
message exactly.
// on the NEW device: fresh keypair, open a session, show the code
sessionID, _ := drift.Deed.Link.Begin(newDevicePubkey, boxPubkeyHex) // metadata: a key to seal for
svg, _ := drift.Deed.Link.QR(sessionID)
// on an ALREADY-ACTIVE device: read the session, sign client-side, attest
info, _ := drift.Deed.Link.SessionInfo(sessionID) // info.NewPubkey, info.Metadata
slice := drift.Deed.JWT.SliceID() // the same value the joiner must sign
drift.Deed.Link.Attest(identity, sessionID, activePubkey, sig, sealedForJoiner)
// back on the NEW device, polled until an active device has vouched
status, _ := drift.Deed.Link.Complete(sessionID)
// status.Status == "attested" → status.Identity, status.Sealed
sealed is how the joining device receives key material. Deed relays it and never
opens it: seal a payload for the ephemeral key the joiner published as metadata, and
Complete hands it back once the attestation lands. A session lives 5 minutes.
Link's two signatures are made client-side over a fixed domain of Link's own, not your app's, in
the same canonical form as KeyAuth's: sorted keys, no whitespace, and this slice's own identity
folded in as slice, filled in by the slice itself rather than trusted from the request.
Every slice on the platform runs the identical binary, so without that field an attestation, or a
revocation, signed for one slice would verify on every other one too.
{"domain":"drift-deed-link-attest-v1","identity":"<identity>","new_pubkey":"<joiner>","slice":"<this-slice>"}
{"domain":"drift-deed-link-revoke-v1","identity":"<identity>","slice":"<this-slice>","target_pubkey":"<device>"}Revoking a device
drift.Deed.Link.Revoke(identity, targetPubkey, revokingPubkey, sig)Any currently-active device can revoke another, or itself. The revoked key still produces cryptographically valid signatures; KeyAuth refuses it anyway, which is what the registry is for. Revoking the identity's last active device is refused instead, even when that device is revoking itself: honouring it would leave the registry with nothing able to ever sign another Link operation for this identity again, and there is no self-service way back in.
Drift verifies, it never decides.
Attest succeeds. Revoking that implicit device by itself
hits the same last-active-device refusal above, so it never writes
a registry either. Snapshot restore is the one path that writes a registry with no attestation at
all, and it is operator-only.