Bounded Authority Report Adapter

Holder-side companion signer for the Bounded Authority Protocol. Current release: 0.8.0 — the role-attestation release (RA11: the optional BA-attested role gate on sign_grant/3, over BAP 0.6.0). Registry checksum read back from the registry API and pinned in the post-publish docs-currency commit; the 0.7.0 checksum was 39ec21ffabe981059b9940d17f86a782e12a9148fddefbf14cc7f4a2c96bfc0d (identical to its tagged-tree build and publish output). (GitHub). The protocol package produces the deterministic signing input for each protocol object (holder proof, boundary anchor, grant, key transition) and refuses to sign; this library takes a local key handle and a signing input and produces the signed compact form. The private key never enters the library — callers supply a {module(), term()} handle whose module implements the signing callbacks against their own custody (an HSM, a KMS, or an in-process key in test). The protocol package's README describes this adapter as its holder-side companion; the dependency is one-directional (this adapter depends on the protocol package, never the reverse).

Verifiers depend only on the protocol package, never on this adapter. Consuming an envelope (the verifier's side of the contract) is documented in docs/consumer-integration.md.

Installation

def deps do
[
{:bounded_authority_report_adapter, "~> 0.6.0"}
]
end

What it is

An edge agent proves a request is authorized — not merely transport-authenticated — by presenting a grant + proof envelope: an issuer-signed capability grant plus a holder proof signed by the agent's own key. This adapter is what the agent calls to produce that envelope. It signs the proof; the grant arrives issuer-signed and passes through untouched. The receiver verifies the envelope with the protocol package's check_envelope/2 and gets back cryptographic facts.

The signer is universal across the four protocol objects, each through one shared signing tail:

Function Object Role
sign_report/3 holder proof (the grant passes through) holder
sign_local_loopback_report/3 local-loopback application proof (ba+loopback-proof) holder
sign_anchor/3 boundary anchor role-agnostic
sign_key_transition/3 key transition role-agnostic
sign_grant/3 grant issuer-only, structurally gated

The role gate is load-bearing: a holder handle cannot sign a grant. Only a handle that resolves the issuer role may, so an agent can never mint its own capability.

The local-loopback profile (development listeners)

sign_local_loopback_report/3 is the explicit holder-side signer for BAP's byte-distinct bap-application-proof/local-loopback-http/1 profile — plain HTTP on the literal loopback interface (http://127.0.0.1 / http://[::1] only, exactly spelled). It exists for development listeners where TLS is impossible; the proof it produces carries typ: ba+loopback-proof and is rejected by the standard verifier, just as a standard dpop+jwt proof is rejected by the profile's verifier — the two families never mix.

Three things this profile is NOT:

The nonce is mandatory (a non-empty binary — on the verify side it is the listener's own single-use challenge), and only canonical literal-loopback targets sign — localhost, 127.0.0.2, 0x7f.1, [::ffff:127.0.0.1], uppercase schemes, queries, fragments, HTTPS, and every other spelling fail closed. See the recipe; the examples/edge_agent app runs the flow over real IPv4 and IPv6 sockets.

See it run, self-contained (no database, no Docker): the repository's examples/ directory carries a Livebook demo that plays issuer → holder → verifier in one notebook, and an edge_agent app that runs the full loop over real HTTP (agent signs and POSTs; receiver verifies via check_envelope). Both prove a tampered or wrong-key proof is rejected.

Key custody

The library never holds a key. A caller passes a {module, ref} handle; the module implements sign/2, public_key/1, and thumbprint/1 (plus optional identity callbacks) against its own key store. Every sign path ends in a verify-against-the-public-key guard, so a misconfigured signer fails loudly rather than emitting an unverifiable signature. A production holder points the handle at an HSM or KMS; the in-memory reference handle used in tests compiles only in the test environment and never ships.

Development

mix deps.get
mix ci

mix ci reproduces the CI pipeline locally: dependency resolution plus the latest-first currency gate (ADR-0020), format, warnings-as-errors compilation, Credo, and the full test suite (including the conformance round-trip against the protocol package's published oracle vectors and the dependency-direction wall), the coverage floor, dialyzer, doc warnings, both advisory audits, the package-boundary and reproducibility gates — for both the library and the example app, and a transport advisory fails the local and GitHub entry points alike. GitHub CI runs one lane per supported OTP major on Linux plus a windows-latest lane on the pinned versions: clone → build → test holds on macOS, Linux, and Windows.

Requires Elixir ~> 1.18 — supported minors 1.18/1.19/1.20 — on Erlang/OTP 27 through 29 (the majors on which the stack compiles — the protocol package's codecs decode through OTP 27's :json module, so 25/26 are out; enforced at compile time by the repository's own config/config.exs, never shipped to consumers — ADR-0019). Developed on 1.20 / OTP 29. The runnable examples/edge_agent app is a separate mix project with its own deps and CI job — develop it from inside that directory.

Telemetry

The four signing entry points emit a closed, value-free telemetry surface (two events, atoms-only metadata — never key material, message bytes, or report content):

No handler is attached by default. The event/class tables, alerting guidance (:signing_failed rate = custody misconfiguration), and an attach example live in docs/telemetry.md.

Documentation

Security

See SECURITY.md for the vulnerability-reporting process.

License

Apache-2.0. See LICENSE and NOTICE.