Signing, signature verification, and DID derivation for did:plc operations.
did:plc is self-certifying: the identifier is derived from the signed genesis operation itself, so nothing pre-exists the key that controls it.
did:plc:<first 24 chars of lowercase base32 of sha256(dagcbor(signed genesis))>What is signed
The DAG-CBOR encoding of the operation with sig omitted entirely —
see Operation.unsigned_bytes/1. Signatures are ECDSA-SHA256, low-S
normalized, emitted as raw 32+32-byte big-endian R||S and encoded
base64url without padding.
Low-S handling is Exosphere.ATProto.Crypto's job, not this module's:
Crypto.sign/3 normalizes and Crypto.verify/4 rejects high-S. This
module adds the base64url layer and the strictness the directory applies
to it — a signature carrying padding characters, non-canonical padding
bits, or whitespace is rejected before it ever reaches the curve.
Summary
Functions
The CID of a signed operation, as the directory reports it in an audit log.
Decode an operation signature, strictly.
Derive the DID from a signed genesis operation.
Encode a raw 64-byte signature as base64url without padding.
Sign an unsigned operation, returning it with sig set.
Verify an operation's signature against a single did:key.
Verify an operation against a list of candidate rotation keys, returning the index of the key that signed it.
Functions
@spec cid(Exosphere.ATProto.Identity.DID.PLC.Operation.t()) :: {:ok, String.t()} | {:error, term()}
The CID of a signed operation, as the directory reports it in an audit log.
Decode an operation signature, strictly.
Rejects padding characters, whitespace/newlines, and non-canonical padding bits in the final sextet — all of which the directory's conformance fixtures exercise as invalid.
@spec derive_did(Exosphere.ATProto.Identity.DID.PLC.Operation.t()) :: {:ok, String.t()} | {:error, term()}
Derive the DID from a signed genesis operation.
The operation must already carry its sig; the DID covers the signed
bytes.
Examples
iex> Signer.derive_did(signed_genesis)
{:ok, "did:plc:6adr3q2labdllanslzhqkqd3"}
Encode a raw 64-byte signature as base64url without padding.
@spec sign( Exosphere.ATProto.Identity.DID.PLC.Operation.t(), binary(), Exosphere.ATProto.Crypto.curve() ) :: {:ok, Exosphere.ATProto.Identity.DID.PLC.Operation.t()} | {:error, term()}
Sign an unsigned operation, returning it with sig set.
Examples
iex> {:ok, signed} = Signer.sign(op, private_key, :secp256k1)
iex> is_binary(signed["sig"])
true
@spec verify(Exosphere.ATProto.Identity.DID.PLC.Operation.t(), String.t()) :: :ok | {:error, term()}
Verify an operation's signature against a single did:key.
Returns :ok, or {:error, :invalid_signature} for a bad signature —
including a malformed base64url encoding, which the directory treats as
invalid rather than as a decoding accident.
@spec verify_with_keys(Exosphere.ATProto.Identity.DID.PLC.Operation.t(), [String.t()]) :: {:ok, non_neg_integer()} | {:error, term()}
Verify an operation against a list of candidate rotation keys, returning the index of the key that signed it.
The index is the authority level — rotation keys are ordered by descending authority, so a lower index means a stronger key, which is what the nullification rules compare.