Exosphere.ATProto.Identity.DID.PLC.Signer (Exosphere v0.6.0)

Copy Markdown View Source

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

cid(op)

@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_signature(sig)

@spec decode_signature(term()) :: {:ok, binary()} | {:error, term()}

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.

derive_did(op)

@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_signature(signature)

@spec encode_signature(binary()) :: String.t()

Encode a raw 64-byte signature as base64url without padding.

sign(op, private_key, curve)

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

verify(op, did_key)

@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.

verify_with_keys(op, keys)

@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.