Atex.Repo.Commit (atex v0.9.1)

View Source

The signed commit object at the top of an AT Protocol repository.

A commit binds together:

  • The account DID that owns the repository.
  • A CID link (data) to the root of the MST that holds all records.
  • A monotonically-increasing revision (rev) in TID string format, used as a logical clock.
  • A prev link to the previous commit (virtually always nil in v3 repos, but the field must be present in the CBOR object).
  • A cryptographic sig over the DRISL CBOR encoding of the unsigned commit.

Signing a commit

The signing convention follows the AT Protocol repository spec:

  1. Build an unsigned commit (all fields except sig).
  2. Encode it with encode_unsigned/1 to get the DRISL CBOR bytes.
  3. SHA-256 hash the bytes, then ECDSA-sign the hash with the account's signing key.
  4. Store the raw (DER-encoded) signature bytes in sig.

sign/2 performs steps 2–4 in one call. Verification with verify/2 reverses the process using a public key.

CID computation

The CID for a commit is computed from the DRISL CBOR encoding of the signed commit object (with sig present), using the :drisl codec.

Wire format

Map keys follow the AT Protocol specification field names:

  • "did" - account DID string
  • "version" - integer 3
  • "data" - CID link to MST root
  • "rev" - TID string
  • "prev" - CID link or nil
  • "sig" - raw ECDSA signature bytes (absent from the unsigned map)

ATProto spec: https://atproto.com/specs/repository#commit-objects

Summary

Types

t()

A v3 AT Protocol repository commit.

Functions

Computes the CID of a signed commit.

Decodes a DRISL CBOR binary into a %Atex.Repo.Commit{}.

Serializes a signed commit (including sig) as DRISL CBOR.

Serializes the commit without the sig field as DRISL CBOR.

Builds an unsigned commit struct from the given fields.

Signs an unsigned commit with the given private key.

Verifies the signature of a signed commit against the given public key.

Types

t()

@type t() :: %Atex.Repo.Commit{
  data: DASL.CID.t(),
  did: String.t(),
  prev: DASL.CID.t() | nil,
  rev: String.t(),
  sig: binary() | nil,
  version: pos_integer()
}

A v3 AT Protocol repository commit.

Functions

cid(commit)

@spec cid(t()) :: {:ok, DASL.CID.t()} | {:error, :unsigned | atom()}

Computes the CID of a signed commit.

The CID is derived from the DRISL CBOR encoding of the signed commit object, using the :drisl codec (blessed CID format).

Returns {:error, :unsigned} if sig is nil.

Examples

iex> data_cid = DASL.CID.compute("data", :drisl)
iex> commit = Atex.Repo.Commit.new(did: "did:plc:e", data: data_cid, rev: "3jzfcijpj2z2a")
iex> jwk = JOSE.JWK.generate_key({:ec, "P-256"})
iex> {:ok, signed} = Atex.Repo.Commit.sign(commit, jwk)
iex> {:ok, cid} = Atex.Repo.Commit.cid(signed)
iex> cid.codec
:drisl

decode(binary)

@spec decode(binary()) :: {:ok, t(), binary()} | {:error, atom()}

Decodes a DRISL CBOR binary into a %Atex.Repo.Commit{}.

Accepts both signed (with "sig") and unsigned (without "sig") payloads.

Examples

iex> data_cid = DASL.CID.compute("data", :drisl)
iex> commit = Atex.Repo.Commit.new(did: "did:plc:e", data: data_cid, rev: "3jzfcijpj2z2a")
iex> {:ok, bin} = Atex.Repo.Commit.encode_unsigned(commit)
iex> {:ok, decoded, ""} = Atex.Repo.Commit.decode(bin)
iex> decoded.did
"did:plc:e"

encode(commit)

@spec encode(t()) :: {:ok, binary()} | {:error, :unsigned | atom()}

Serializes a signed commit (including sig) as DRISL CBOR.

Returns {:error, :unsigned} if sig is nil.

Examples

iex> data_cid = DASL.CID.compute("data", :drisl)
iex> commit = Atex.Repo.Commit.new(did: "did:plc:e", data: data_cid, rev: "3jzfcijpj2z2a")
iex> jwk = JOSE.JWK.generate_key({:ec, "P-256"})
iex> {:ok, signed} = Atex.Repo.Commit.sign(commit, jwk)
iex> {:ok, bin} = Atex.Repo.Commit.encode(signed)
iex> is_binary(bin)
true

encode_unsigned(commit)

@spec encode_unsigned(t()) :: {:ok, binary()} | {:error, atom()}

Serializes the commit without the sig field as DRISL CBOR.

This is the payload that is hashed and signed. The sig field is omitted entirely from the map, as required by the spec.

Examples

iex> data_cid = DASL.CID.compute("data", :drisl)
iex> commit = Atex.Repo.Commit.new(did: "did:plc:e", data: data_cid, rev: "3jzfcijpj2z2a")
iex> {:ok, bin} = Atex.Repo.Commit.encode_unsigned(commit)
iex> is_binary(bin)
true

new(fields)

@spec new(keyword()) :: t()

Builds an unsigned commit struct from the given fields.

sig is set to nil.

Options

  • :did (required) - the account DID string
  • :data (required) - DASL.CID pointing to the MST root
  • :rev (required) - TID string used as the logical clock
  • :prev - DASL.CID pointing to the previous commit, or nil (default)

Examples

iex> data_cid = DASL.CID.compute("data", :drisl)
iex> commit = Atex.Repo.Commit.new(
...>   did: "did:plc:example",
...>   data: data_cid,
...>   rev: "3jzfcijpj2z2a"
...> )
iex> commit.version
3
iex> commit.sig
nil

sign(commit, jwk)

@spec sign(t(), JOSE.JWK.t()) :: {:ok, t()} | {:error, :already_signed | atom()}

Signs an unsigned commit with the given private key.

Encodes the unsigned commit as DRISL CBOR and signs the bytes using Atex.Crypto.sign/2 (SHA-256 ECDSA, low-S normalized DER output).

Returns {:error, :already_signed} if sig is already present.

Examples

iex> data_cid = DASL.CID.compute("data", :drisl)
iex> commit = Atex.Repo.Commit.new(did: "did:plc:e", data: data_cid, rev: "3jzfcijpj2z2a")
iex> jwk = JOSE.JWK.generate_key({:ec, "P-256"})
iex> {:ok, signed} = Atex.Repo.Commit.sign(commit, jwk)
iex> is_binary(signed.sig)
true

verify(commit, jwk)

@spec verify(t(), JOSE.JWK.t()) :: :ok | {:error, :unsigned | atom()}

Verifies the signature of a signed commit against the given public key.

Returns :ok or {:error, reason}.

Examples

iex> data_cid = DASL.CID.compute("data", :drisl)
iex> commit = Atex.Repo.Commit.new(did: "did:plc:e", data: data_cid, rev: "3jzfcijpj2z2a")
iex> jwk = JOSE.JWK.generate_key({:ec, "P-256"})
iex> {:ok, signed} = Atex.Repo.Commit.sign(commit, jwk)
iex> Atex.Repo.Commit.verify(signed, JOSE.JWK.to_public(jwk))
:ok