- Treat
sign_report/3output as a cryptographic artifact, not an authorization decision. The envelope proves the holder signed THIS report under THAT grant — whether the action is allowed is the verifier's authority decision (consumer integration §8). - The private key never enters this library. You hold it behind a
{module, term}key handle implementing theBoundedAuthorityReportAdapterbehaviour (sign/2,public_key/1,thumbprint/1,key_identity/1,signing_identity/1). If you find yourself passing key bytes INTO the adapter, the integration is wrong — see Getting started. sign_grant/3's role gate is declaration-rejection, NOT cryptographic role separation. A handle whosesigning_identity/1does not resolve{:issuer, _, _}is rejected with:invalid_key_handleBEFOREsign/2is called — but a handle that consistently lies (returns:issuerwhile holding a holder key) passes the gate. Key-role separation is a custody property, not something this gate adds to. (ADR-0006/0007.)- Never trust caller-supplied
key_idorpublic_keycontent.sign_anchor/3andsign_key_transition/3resolve BOTH key identifiers from ONE atomickey_identity/1snapshot on the handle — a caller-supplied:current_key_idis ignored. Keep it that way: a caller-named kid over a differently-signed anchor is the forgery that shape enables. - Pin
:issued_at(and:anchored_at) when the verifier's evaluation time is far from your wall clock. The default isSystem.system_time(:second)— replayed tests and offline flows against a pinnedevaluation_timeneed the explicit option, or the proof falls outsideproof_max_age. cast_argumentsmust be BAP's taggedJson.value()form, produced byBoundedAuthorityProtocol.V1.Json.decode/2of the SAME raw bytes on BOTH sides. A raw map is rejected; feeding the two sides DIFFERENT bytes (or a re-encoding, instead of the original bytes) is the divergence this rule prevents — same bytes + same deterministic decode is byte-agreement by construction (consumer integration §3–§4).- Consumer-side identity binding and nonce-ledger replay protection are obligations, not options. This library signs; the consuming verifier must bind the holder thumbprint to its own identity source and dedupe nonces (consumer integration §8/§9).
- Errors are closed atoms — there is no value-echoing.
{:producer_error, :invalid}is exactly that tuple; key ids, message bytes, and report content never appear in an error. Do not wrap losses into logs by inspecting inputs on failure — the atoms are the whole story (Errors). - A signature that does not verify against the resolved public key is
:signing_failed— the wrong-key guard runs on EVERY object. If your handle signs with a different key thanpublic_key/1reports, you get a red, never a false success. Fix the handle, never the guard. - Attach telemetry BEFORE the first production sign if you want the custody alarm:
[:bounded_authority_report_adapter, :sign, :stop]withresult_class: :signing_failedis the custody-misconfiguration signal. Metadata is value-free; never extend it with key material (Telemetry). - Pin the protocol dependency and treat a version bump as a reviewed change — the
dependency-direction wall pins the locked version, and a silent
mix deps.updatecrosses an unreviewed protocol span (ADR-0010). - Production handles never come from this library. The
{pub, priv}reference handle in the source repo'stest/support/is TEST-ONLY and deliberately not shipped in the package — shipping it would pave the road to exactly the custody failure the separate key-handle contract exists to prevent (design C5, ADR-0014).
See Getting started, Errors, Telemetry, and Consumer integration for the long forms.