Skip to content

Historical document. This was the original cryptographic spec used as vetting input. It has been superseded by docs/architecture/identity-ops.md, docs/architecture/value-auth.md, and docs/design/aid-model.md. It contains known inconsistencies with the current design.

What ships, and the accepted design

Historical vetting input. Nothing here ships. The current machine is the Lean checkpoint and the simulations on the home page.

AID Operations — Cryptographic Specification

This document describes the three on-chain AID operations and their cryptographic bindings. It is the primary input for correctness vetting.

Primitives available on-chain (Aiken / PlutusV3)

Primitive Function Cost
Ed25519 signature verify verify_ed25519_signature(vk, msg, sig) ~1700 mem / ~900 cpu units
Blake2b-256 hash blake2b_256(bytes) ~1 mem unit per byte
Equality == on ByteArray cheap

NOT available on-chain (unbounded / missing): SHA-3, secp256k1 schnorr (available but not used here), CESR parsing, list recursion over unbounded KEL.

Core types

AID        : ByteArray[32]   -- self-certifying identifier
PubKey     : ByteArray[32]   -- Ed25519 public key
KeyDigest  : ByteArray[32]   -- blake2b_256(PubKey)
Seq        : Int              -- monotonic rotation counter (starts at 0)

KeyState {
  cur_digest  : KeyDigest    -- blake2b_256(current pubkey)
  next_digest : KeyDigest    -- blake2b_256(NEXT pubkey, pre-committed)
  seq         : Seq
}

InceptionEvent {
  cur_key     : PubKey       -- initial public key
  next_digest : KeyDigest    -- pre-committed hash of next key
}

AID self-certification

AID = blake2b_256( cbor(InceptionEvent{ cur_key, next_digest }) )

The AID value IS the hash of the inception event. Anyone can verify the binding offline without trusting any authority.

Operation 1 — Inception

Purpose: Register a new AID and its initial key-state.

Redeemer input:

IncRedeemer {
  inception : InceptionEvent { cur_key, next_digest }
}

On-chain checks (script): 1. blake2b_256( cbor(inception) ) == aid_from_datum — self-cert binding 2. MPF absence proof: aid not yet in the identity trie (no double-registration) 3. Resulting key-state: KeyState{ cur_digest = blake2b_256(cur_key), next_digest, seq = 0 }

No signature required: the AID value itself is the proof of pre-image knowledge.

Security claim: An adversary who does not know cur_key cannot produce a valid InceptionEvent whose hash matches a chosen AID. Pre-image resistance of blake2b_256.

Operation 2 — Rotation

Purpose: Rotate to the pre-committed next key.

Redeemer input:

RotRedeemer {
  reveal_key  : PubKey       -- the key whose digest was committed as next_digest
  new_next    : KeyDigest    -- digest of the key AFTER this rotation
  sig         : Signature    -- Ed25519(reveal_key, rot_msg)
}

rot_msg (the signed message):

rot_msg = cbor({ aid, seq: current_seq + 1, new_next })

On-chain checks (script): 1. blake2b_256(reveal_key) == current_state.next_digest — pre-rotation binding 2. verify_ed25519_signature(reveal_key, rot_msg, sig) — key possession 3. MPF inclusion proof: current KeyState is in the identity trie at aid 4. MPF update: replace KeyState with KeyState{ cur_digest = blake2b_256(reveal_key), next_digest = new_next, seq = seq + 1 }

Security claim: An adversary who stole cur_key cannot rotate forward because reveal_key (the next key) was never revealed and its digest was committed at inception. Stealing cur_key only lets the adversary transact with the current key-state; it cannot advance the sequence.

Operation 3 — Value-write (MPFS cage op)

Purpose: Authorize an MPFS operation (insert/delete/update a leaf) using the AID key-state as authorization.

This is NOT an identity-registry operation. It happens in the MPFS value cage script, which reads the identity UTxO as a CIP-31 reference input.

Redeemer input (MPFS cage redeemer, augmented):

AuthRedeemer {
  aid        : AID
  op         : MpfsOp         -- Insert | Delete | Update
  sig        : Signature      -- Ed25519(cur_key, auth_msg)
  id_proof   : MpfInclusionProof  -- proves aid→keyState in identity trie
}

auth_msg:

auth_msg = cbor({ aid, op, identity_root })
where identity_root is the MPF root of the identity UTxO (taken from the reference input datum).

On-chain checks (MPFS value cage script): 1. Reference input has identity UTxO thread token (fixed policyId + assetName) 2. MPF inclusion proof: id_proof proves aid → keyState against identity_root 3. blake2b_256(vk_from_tx_signatories) == keyState.cur_digest — resolve signer 4. verify_ed25519_signature(vk_from_tx_signatories, auth_msg, sig) — key possession 5. auth_msg binds to current identity_root — anti-replay binding

Security claim: The identity_root in auth_msg ties the authorization to the exact key-state snapshot used. A replayed signature from a previous rotation cannot be re-used because identity_root would have changed.

Questions for cryptographic vetting

  1. Inception replay: Can an adversary register the same AID twice (before the first registration is confirmed on-chain, e.g. front-running)?

  2. Rotation replay: Is rot_msg = cbor({ aid, seq: seq+1, new_next }) a sufficient anti-replay binding? What if new_next collides?

  3. Value-write replay: Does binding auth_msg to identity_root fully prevent replaying a signature across rotations? What if an MPF root collides or is predictable?

  4. Rotation after key compromise: If cur_key is stolen, the attacker can sign with it but cannot advance the sequence. Can the attacker do anything else harmful (e.g. spam value-write ops, block legitimate rotation)?

  5. Inception event malleability: Is cbor(InceptionEvent) deterministic across CBOR encoders? Should canonical CBOR be mandated?

  6. next_digest grinding: Can an adversary grind new_next in a rotation redeemer to claim a useful AID as the "next" AID? (Is the AID namespace polluted by this?)

  7. Missing: inception self-auth: Inception requires no signature. Is absence proof + hash pre-image sufficient, or should the registrant also sign the inception event to prove liveness?

  8. On-chain vs off-chain boundary: What properties are guaranteed purely by on-chain checks, and what requires off-chain KEL replay? Is the boundary drawn correctly for the MPFS use case?