AgentBlueprintProtocol.Digest (Agent Blueprint Protocol v0.1.1)

Copy Markdown View Source

Tagged content digests over RFC 8785 canonical bytes.

The wire form is the self-identifying tagged string "sha-256:<43-char unpadded base64url>" — a consumer never guesses the algorithm or encoding from length. Preimages are domain-separated — SHA-256(separator || <<0>> || jcs_bytes) — under the registered separator for the hashing domain (base §8.2), so a digest transplanted across domains never verifies.

Canonicality is the caller's contract. hash/2 and verify_content/3 treat the covered bytes as opaque: callers MUST pass canonical JCS bytes. The enforcement point is where received bytes enter the package — the artifact decode path verifies canonicality first (digests are computed only over exact received bytes). This module does not re-verify, so hashing never double-parses a document nor leaks the JSON reason set into this vocabulary.

The algorithm set is closed (:sha256); algorithm succession is a data change plus a protocol revision, never a format change — an unknown tag denies :digest_algorithm_unsupported and any malformed body denies :digest_encoding_invalid, uniformly. SHA-256 conformance (FIPS 180-4 known-answer vectors) is pinned in the conformance suite. A digest is an identity fact about bytes; it never authorizes anything. A digest is an identity fact about bytes; it never authorizes anything.

Summary

Functions

Constant-time equality over the digest bytes: the comparison accumulates the XOR of every byte pair and tests once at the end, so no early exit leaks the position of a difference. Length and algorithm equality are checked first and may short-circuit — both are public shape, carried in the tagged wire form itself. This is defense-in-depth: a digest is public once tagged; the guarantee matters where one side is not yet public.

Parse a tagged digest string. A string with no colon is not a tagged digest (:digest_encoding_invalid). A tag segment that does not exactly match a registered algorithm tag denies :digest_algorithm_unsupported — case and hyphenation variants and the empty tag included. Every body defect denies :digest_encoding_invalid uniformly: wrong length (a sha-256 body decodes to exactly 32 bytes), non-alphabet characters, padding, non-canonical pad bits, extra colons.

Domain-separated digest over canonical JCS bytes: the base §8.2 preimage separator || <<0>> || jcs_bytes under the registered separator for domain. An unknown domain is an internal invariant and raises loudly.

SHA-256 of data as a tagged digest value.

The wire form "sha-256:<43-char unpadded base64url>". The clause matches the 256-bit t() byte shape, so a hand-built struct with wrong-size bytes fails loud (a FunctionClauseError invariant) rather than emitting a non-conforming wire string.

Verify that tagged is the honest hash(domain, jcs_bytes): parse first (a malformed tagged string denies with its from_tagged/1 reason, before any comparison), then constant-time equality — a well-formed but divergent digest denies :digest_mismatch. jcs_bytes are the caller's canonical covered bytes (see the moduledoc contract).

Types

algorithm()

@type algorithm() :: :sha256

domain()

@type domain() ::
  :blueprint_content
  | :deployment_content
  | :federation_envelope
  | :signature
  | :extension_schema
  | :extension_registry
  | :conformance_report
  | :corpus_index

reason()

@type reason() ::
  :digest_algorithm_unsupported | :digest_encoding_invalid | :digest_mismatch

t()

@type t() :: %AgentBlueprintProtocol.Digest{algorithm: algorithm(), bytes: <<_::256>>}

Functions

equal?(digest1, digest2)

@spec equal?(t(), t()) :: boolean()

Constant-time equality over the digest bytes: the comparison accumulates the XOR of every byte pair and tests once at the end, so no early exit leaks the position of a difference. Length and algorithm equality are checked first and may short-circuit — both are public shape, carried in the tagged wire form itself. This is defense-in-depth: a digest is public once tagged; the guarantee matters where one side is not yet public.

from_tagged(input)

@spec from_tagged(binary()) :: {:ok, t()} | {:error, reason()}

Parse a tagged digest string. A string with no colon is not a tagged digest (:digest_encoding_invalid). A tag segment that does not exactly match a registered algorithm tag denies :digest_algorithm_unsupported — case and hyphenation variants and the empty tag included. Every body defect denies :digest_encoding_invalid uniformly: wrong length (a sha-256 body decodes to exactly 32 bytes), non-alphabet characters, padding, non-canonical pad bits, extra colons.

hash(domain, data)

@spec hash(domain(), iodata()) :: t()

Domain-separated digest over canonical JCS bytes: the base §8.2 preimage separator || <<0>> || jcs_bytes under the registered separator for domain. An unknown domain is an internal invariant and raises loudly.

of(data)

@spec of(iodata()) :: t()

SHA-256 of data as a tagged digest value.

to_tagged(digest)

@spec to_tagged(t()) :: binary()

The wire form "sha-256:<43-char unpadded base64url>". The clause matches the 256-bit t() byte shape, so a hand-built struct with wrong-size bytes fails loud (a FunctionClauseError invariant) rather than emitting a non-conforming wire string.

verify_content(domain, jcs_bytes, tagged)

@spec verify_content(domain(), binary(), binary()) :: :ok | {:error, reason()}

Verify that tagged is the honest hash(domain, jcs_bytes): parse first (a malformed tagged string denies with its from_tagged/1 reason, before any comparison), then constant-time equality — a well-formed but divergent digest denies :digest_mismatch. jcs_bytes are the caller's canonical covered bytes (see the moduledoc contract).