The open wire profile, verifier, and conformance suite of the Bounded Authority Protocol: an issuer signs a capability grant that names exactly which operations a holder may invoke, with which arguments; the holder signs a proof of possession for each request; a verifier checks the exact bytes and returns redacted facts. It never returns an authorization decision.
The package is pure and deterministic: no keys, no clock, no I/O, no runtime dependencies. Every
failure is the single value-free {:error, :invalid}, and the wire profile is closed, with no
permissive mode. It builds on compact JWS, DPoP, JCS, and JWK thumbprints
(RFC 7515, 9449,
8785, 7638).
Versions
"Package 1.x" is a Hex release line; "contract-major N" is a closed wire profile. One package release carries several profiles.
| What | Version | Namespace | Suite | Specification |
|---|---|---|---|---|
| Hex package | 1.0.0 | BoundedAuthorityProtocol | Changelog | |
| Contract-major 1 | frozen | BoundedAuthorityProtocol.V1 | BAP1-Ed25519-SHA256 | spec |
| Contract-major 2 | frozen | BoundedAuthorityProtocol.V2 | BAP2-Ed25519-SHA256 | spec |
| Contract-major 3 | frozen | BoundedAuthorityProtocol.V3 | BAP3-ES256-SHA256 | spec |
| Local-loopback HTTP proofs | profile 1 | ...ApplicationProfile.LocalLoopbackHttp.V1 | Ed25519 | spec |
| Role attestations | profile 1 | ...RoleAttestation.V1 | Ed25519 | spec |
| Content assertions | profile 1 | ...ContentAssertion.V1 | Ed25519 | spec |
See Choosing a profile and API stability and versioning.
Installation
def deps do
[{:bounded_authority_protocol, "~> 1.0"}]
endElixir 1.18 or later (Mix enforces it) and Erlang/OTP 27 or later (for :json; nothing checks
it). CI tests Elixir 1.18, 1.19, and 1.20 on OTP 27, 28, and 29.
Quickstart
An issuer signs a grant, a holder signs a proof for one request, and the service verifies both
against what it derived from the request. Paste it into iex -S mix in a project that depends on
the package; the repository's test suite runs this exact block.
alias BoundedAuthorityProtocol.V1
alias BoundedAuthorityProtocol.V1.{
Credentials,
ExpectedRequest,
Grant,
Operation,
Proof,
TrustedIssuer
}
# Demo keys. In production the issuer's private key stays in its KMS or HSM, the holder's in its
# own key store, and the verifier only ever receives public keys.
{issuer_public, issuer_private} = :crypto.generate_key(:eddsa, :ed25519)
{holder_public, holder_private} = :crypto.generate_key(:eddsa, :ed25519)
{:ok, holder_thumbprint} = V1.Jwk.public_key_thumbprint_raw(holder_public, %{})
# The issuer grants one operation, restricted to one argument value.
grant = %Grant{
key_id: "issuer-1",
issuer: "https://issuer.example",
grant_id: "grant-001",
audiences: ["https://docs.example"],
issued_at: 1_800_000_000,
not_before: 1_800_000_000,
expires_at: 1_800_003_600,
holder_thumbprint: holder_thumbprint,
operations: [
%Operation{name: "publish_article", selectors: [{:equals, ["section"], {:string, "science"}}]}
]
}
{:ok, grant_input} = V1.grant_signing_input(grant, %{})
grant_signature = :crypto.sign(:eddsa, :none, grant_input.message, [issuer_private, :ed25519])
{:ok, grant_compact} = V1.assemble_compact(grant_input, grant_signature)
# The holder binds one request to the grant with a proof of possession.
now = 1_800_000_100
arguments = {:object, [{"section", {:string, "science"}}, {"title", {:string, "Tides"}}]}
proof = %Proof{
holder_public_key: holder_public,
proof_id: "proof-001",
method: "POST",
target_uri: "https://docs.example/articles",
issued_at: now,
nonce: nil,
invocation_id: "9b2f1c3e-7a51-4d0e-8f6a-2c4b5d6e7f80",
operation: "publish_article",
grant_compact: grant_compact,
cast_arguments: arguments
}
{:ok, proof_input} = V1.proof_signing_input(proof, %{})
proof_signature = :crypto.sign(:eddsa, :none, proof_input.message, [holder_private, :ed25519])
{:ok, proof_compact} = V1.assemble_compact(proof_input, proof_signature)
# The service verifies the raw bytes against what IT derived from the request.
expected = %ExpectedRequest{
trusted_issuer: %TrustedIssuer{key_id: "issuer-1", public_key: issuer_public},
issuer: "https://issuer.example",
audience: "https://docs.example",
method: "POST",
target_uri: "https://docs.example/articles",
invocation_id: "9b2f1c3e-7a51-4d0e-8f6a-2c4b5d6e7f80",
operation: "publish_article",
cast_arguments: arguments,
evaluation_time: now,
clock_skew: 60,
proof_max_age: 300,
nonce: :not_required,
bounds: %{}
}
credentials = %Credentials{grant: grant_compact, proof: proof_compact}
{:ok, facts} = V1.check_envelope(credentials, expected)
# The same proof replayed against other arguments fails closed, with no detail.
other_section = {:object, [{"section", {:string, "sports"}}, {"title", {:string, "Tides"}}]}
{:error, :invalid} = V1.check_envelope(credentials, %{expected | cast_arguments: other_section})
{facts.operation, facts.authorization}The final value is {"publish_article", :not_evaluated}: the bytes verified, and the
authorization decision is still yours to make.
What it leaves to you
The package verifies grants, proofs, and the evidence formats, and computes the exact bytes to
sign for every protocol object. It does not select trusted keys, hold private keys, reserve
replay, check revocation, or decide whether an effect is authorized; the
usage rules list what your service owns. To sign with keys in an HSM, a KMS, or
a key store, use bounded_authority_report_adapter.
Conformance
Every certified corpus ships under priv/conformance, with a CLI that exits 0 only on
complete agreement. Contract-major 1's corpus alone has 283 cases across 28 surfaces:
mix escript.install hex bounded_authority_protocol 1.0.0
bounded_authority_conformance --corpus deps/bounded_authority_protocol/priv/conformance/v1/corpus
TypeScript, Python, Rust, and Go verifiers pass the same corpora (Other languages).
Documentation
Getting started, the cheatsheet, and three Livebooks: the walkthrough, service integration, and chains and archives. Then the guides: Recipes, Evidence, Troubleshooting, Upgrading, the glossary, the FAQ, the threat model, and governance.
Security
Report a vulnerability privately through the
Security tab; see
SECURITY.md.