AgentBlueprintProtocol.Signature (Agent Blueprint Protocol v0.1.0)

Copy Markdown View Source

Detached JWS signature envelope: RFC 7515 compact serialization with the RFC 7797 b64=false unencoded, detached payload, Ed25519 signatures verified through :crypto. Verify-only — the package never signs, never accepts a private key on any function, and performs no key discovery or trust selection; hosts supply the trusted keys.

What is signed is the RFC 7797 §3 input:

ASCII(BASE64URL(JCS(protected_header))) || "." || JCS(signed_attributes)

so a standard, b64=false-aware JOSE tooling stack can verify the same bytes (the ratified preimage form, byte-disjoint from the design's earlier sketch). Producer contract: the protected header segment must be the JCS serialization of the exact four-member header; a producer using any other member order produces a legitimately-signed JWS this verifier denies :signature_not_verified. This package is deliberately narrower than generic JOSE for determinism.

The envelope shape (closed world, value-free denials):

{"protected":        {"alg":"EdDSA","b64":false,"crit":["b64"],"kid":"…"},
 "signed_attributes":{"algorithm":"Ed25519","content_digest":"sha-256:…",
                      "created_at":"2026-08-20T00:00:00Z","key_id":"…",
                      "purpose":"blueprint"},
 "signature":        "<86-char unpadded base64url>"}

kid must equal the signed key_id (key substitution prevention); key_id is dot-free so the unencoded payload can never contain . (RFC 7797 §5.2 forbids it in compact serializations); created_at is Z-form whole seconds. Attestations use the identical envelope with purpose/content_digest replaced by a registered kind and a statement_digest; the kind registry is empty until kinds become data, so every attestation denies :attestation_malformed today (fail-closed).

Because content_digest and purpose are inside the signed bytes, a signature cannot be lifted onto a different artifact, digest, or purpose. Errors are value-free. Signature verification produces evidence only; it never authorizes an operation.

Summary

Functions

The signed attributes of a signature entry as an Attributes value. Shape-validated only — no signature has been checked; only verify/2 establishes the signature fact. Do not consume attributes from an envelope that has not verified.

The RFC 7797 b64=false signing input of a signature entry (the tagged algebra as decoded by Json): BASE64URL(JCS(protected)) || "." || JCS(signed_attributes). Envelope violations deny with their verify/2 reason.

The RFC 7515 detached compact serialization (BASE64URL(JCS(protected)) .. BASE64URL(signature), empty payload segment per Appendix F) for standard-tooling cross-checks.

Verify a signature entry against host-supplied trusted keys. Returns facts, never authorization: {:ok, :verified} when any key supplied for the entry's key_id verifies the envelope; {:error, :signature_key_unsupported} when no supplied key carries that id; {:error, :signature_algorithm_unsupported} when the header names a non-EdDSA algorithm or no matched key is usable Ed25519; {:error, :signature_malformed} for envelope violations; {:error, :signature_not_verified} when the bytes do not verify.

Verify an attestation entry (identical envelope; kind and statement_digest replace purpose and content_digest). The kind registry is empty, so a well-formed attestation denies {:error, :attestation_malformed} today — the fail-closed posture, not a defect. Shape errors use :attestation_malformed; algorithm denials (alg/algorithm not EdDSA/Ed25519) share the signature vocabulary by design — the envelope's algorithm posture is one concern. When the first kind registers, the single-clause match below fails loudly on the now-possible {:ok, _} and the identical check/3 wiring used by verify/2 is added deliberately; the envelope machinery (parts/2) is already shared.

Types

reason()

@type reason() ::
  :signature_algorithm_unsupported
  | :signature_key_unsupported
  | :signature_malformed
  | :signature_not_verified
  | :attestation_malformed

Functions

attributes(entry)

The signed attributes of a signature entry as an Attributes value. Shape-validated only — no signature has been checked; only verify/2 establishes the signature fact. Do not consume attributes from an envelope that has not verified.

signing_input(entry)

@spec signing_input(AgentBlueprintProtocol.Json.value()) ::
  {:ok, binary()} | {:error, reason()}

The RFC 7797 b64=false signing input of a signature entry (the tagged algebra as decoded by Json): BASE64URL(JCS(protected)) || "." || JCS(signed_attributes). Envelope violations deny with their verify/2 reason.

to_compact(entry)

@spec to_compact(AgentBlueprintProtocol.Json.value()) ::
  {:ok, binary()} | {:error, reason()}

The RFC 7515 detached compact serialization (BASE64URL(JCS(protected)) .. BASE64URL(signature), empty payload segment per Appendix F) for standard-tooling cross-checks.

verify(entry, keys)

@spec verify(AgentBlueprintProtocol.Json.value(), [
  AgentBlueprintProtocol.Signature.PublicKey.t()
]) ::
  {:ok, :verified} | {:error, reason()}

Verify a signature entry against host-supplied trusted keys. Returns facts, never authorization: {:ok, :verified} when any key supplied for the entry's key_id verifies the envelope; {:error, :signature_key_unsupported} when no supplied key carries that id; {:error, :signature_algorithm_unsupported} when the header names a non-EdDSA algorithm or no matched key is usable Ed25519; {:error, :signature_malformed} for envelope violations; {:error, :signature_not_verified} when the bytes do not verify.

verify_attestation(entry, keys)

@spec verify_attestation(AgentBlueprintProtocol.Json.value(), [
  AgentBlueprintProtocol.Signature.PublicKey.t()
]) :: {:ok, :verified} | {:error, reason()}

Verify an attestation entry (identical envelope; kind and statement_digest replace purpose and content_digest). The kind registry is empty, so a well-formed attestation denies {:error, :attestation_malformed} today — the fail-closed posture, not a defect. Shape errors use :attestation_malformed; algorithm denials (alg/algorithm not EdDSA/Ed25519) share the signature vocabulary by design — the envelope's algorithm posture is one concern. When the first kind registers, the single-clause match below fails loudly on the now-possible {:ok, _} and the identical check/3 wiring used by verify/2 is added deliberately; the envelope machinery (parts/2) is already shared.