Skip to content

bounded_authority_protocol

Deterministic, dependency-free verification for cryptographically bounded, argument-level proof-of-possession authority — the open wire profile, verifier, and conformance suite for the Bounded Authority Protocol (BAP v1).

An issuer signs a capability grant that names exactly which operations a holder may invoke and with exactly which arguments. On each call the holder presents a proof-of-possession bound to that grant, the operation, and a digest of the typed arguments. A verifier checks the exact bytes and returns cryptographic facts — never an authorization decision. The wire profile is closed: a conforming verifier rejects every unlisted member, value, encoding, or extension with a single value-free error, which structurally forecloses the alg:"none" and permissive-parsing failure class.

This package is the standard any party can implement: the normative profile, the verifier, and the conformance corpus. A stateful authority service (issuance, key custody, live revocation, replay, evidence) and holder-side signer SDKs build on top of it; this package deliberately contains neither, holds no keys, performs no I/O, and runs no service.

Built on IETF primitives: compact JWS (RFC 7515), DPoP proof-of-possession (RFC 9449), JCS canonicalization (RFC 8785), JWK thumbprints (RFC 7638), and EdDSA over Ed25519 (RFC 8032). The current cryptographic suite is BAP1-Ed25519-SHA256.

Installation

After the separately authorized Hex 0.3.0 archive is published and its registry read-back succeeds, registry consumers can use the patch-bounded requirement:

def deps do
  [
    {:bounded_authority_protocol, "~> 0.3.0"}
  ]
end

The package has zero production dependencies, no application callback, and no supervision tree. v0.3.0 identifies the reviewable source release for the application-profile addition, but a Git tag is not the immutable package identity. Hex archive publication is a separate action and the dependency above does not resolve until that archive is present in the registry.

What it provides

Verification (all results are redacted and non-authorizing — they carry authorization: :not_evaluated):

  • verify_grant/3 — verifies a raw compact grant against an exact public key, issuer, audience, time, and bounds; returns GrantFacts.
  • check_envelope/2 — re-verifies the raw grant and binds the holder signature, method, normalized URI, invocation id, operation, argument digest, time, nonce, and every selector; returns EnvelopeFacts, or exactly {:error, :invalid}.
  • untrusted_key_locator/2 — a bounded protected-header preparse returning only the kid as an explicitly untrusted lookup hint; it decodes no payload or signature and never marks a key trusted.
  • check_chain/2, verify_historical_anchor/3, verify_key_transition/4, verify_anchored_export/3 — evidence verification: canonical consumption chains, signed boundary anchors, authenticated historical-key rollover, and anchored archives checked to exact EOF, digest, and out-of-band object version.

Production (the package emits deterministic signing inputs and assembles compact forms from a caller-supplied signature; it never accepts private key material or a signer):

  • grant_signing_input/2, proof_signing_input/2, boundary_anchor_signing_input/2, key_transition_signing_input/2, assemble_compact/2,3, encode_consumption_entry/2, encode_anchored_export/2, request_digest/3.

Decoding:

  • BoundedAuthorityProtocol.V1.Json.decode/2 — the closed tagged-JSON algebra with recursive duplicate rejection and no input-name atomization.
  • BoundedAuthorityProtocol.V1.Base64Url.decode/2 — strict canonical unpadded base64url.

Application proof profiles are explicit sibling namespaces. The local-development profile BoundedAuthorityProtocol.ApplicationProfile.LocalLoopbackHttp.V1 uses signed typ: "ba+loopback-proof", mandatory nonces, and only canonical literal-loopback HTTP targets (127.0.0.1 or [::1]). It exposes URI normalization, proof signing-input production, compact assembly, proof decode, and envelope verification. Standard dpop+jwt functions reject these bytes and the loopback functions reject dpop+jwt; callers must select one profile and never retry another after failure. Loopback HTTP is not TLS and is not process isolation.

All verification inputs are explicit: the already-trusted public key, expected audience and instance, server-derived method, normalized URI, invocation id, operation, cast arguments, evaluation time, and limits. A successful result means only that the supplied bytes satisfy those supplied inputs. Resource limits are tightening-only positive integers; the Ed25519 key/signature and SHA-256 digest widths are immutable protocol constants. Unknown, non-integer, zero, negative, widening, or width-changing values fail with the fixed {:error, :invalid}.

What it does not do

This package does not discover trust, issue grants, hold keys, read a database, reserve replay, check live revocation, claim an execution, authorize a business effect, append outcomes, remove archived evidence, submit witnesses, or run a service. Those responsibilities belong to a stateful authority runtime and the consuming host. Verification returns facts; the authorization decision is a separate step the host owns.

Conformance

The package ships a language-neutral v1 conformance corpus and a deterministic offline verifier CLI. The corpus (priv/conformance/v1/corpus) is the normative evidence: 283 cases across 28 surfaces with a full surface × class applicability matrix, independently re-verified by a second implementation that recomputes every verdict from scratch — a value that only round-trips the reference implementation is not normative until the independent runner agrees. Every invalid case is constructed one defect away from a passing case, so a verifier that skips the named check accepts it.

mix escript.build
./bounded_authority_conformance --corpus priv/conformance/v1/corpus

--corpus DIR is required (no default — a wrong-corpus run that exits 0 would be a quiet misverification path in the tool built to eliminate quiet misverification). It exits 0 only on complete agreement, 1 on any integrity or verdict failure, 2 on usage error. From a consumer dependency, point --corpus at the packaged path under deps/bounded_authority_protocol/. The oracle vectors used by holder-side consumers to verify their own production live under priv/conformance/v1/vectors.

The local-loopback application profile has a separate certified corpus under priv/conformance/application-profiles/local-loopback-http/v1. Its Elixir, TypeScript, Python, Rust, and Go consumers assert the same file hashes and verdicts. The non-mock transport drill opens real IPv4 and IPv6 listeners, uses fresh in-memory keys, and prints a secret-free JSON receipt:

mix local_loopback_http.verify

Cross-language verifier SDKs

Alongside the Elixir package, the repository authors typed verifier SDKs of the frozen v1 profile — TypeScript (Node, zero runtime dependencies), Python (single dependency), Rust (#![forbid(unsafe_code)]), and Go (stdlib-only) — each written from the specification and corpus alone, with no code-level derivation from the reference implementation. Each passes all 283 conformance vectors recomputed from scratch, asserts the corpus digest at startup, and proves every parser-layer closure red-capable via a per-language mutation gate. Each SDK graduates to its own repository on first publication.

Standards posture

Each wire profile is closed permanently. Evolution happens through parallel contract-majors or an explicitly identified byte-distinct application profile with its own protected typ, public APIs, normative specification, and certified corpus; no sibling profile changes standard dpop+jwt bytes or verdicts, and implementations never infer or fall back between profiles. Contract-majors never downgrade, with a minimum twelve-month deprecation window and published change-control, errata, and security-release policy (see docs/governance.md). Cryptographic agility is a named-suite succession, with a post-quantum path (ML-DSA) and cross-suite evidence attestation designed in. A pre-submission MCP authorization extension draft targeting the MCP extensions track lives under docs/extensions/.

Documentation

Guides, in curated reading order:

  1. Getting started — zero to a verified envelope, and the three rules that surprise newcomers.
  2. The implementer's guide — building a conforming verifier in any language.
  3. Upgrading — the published compatibility contract.
  4. Runnable Livebook walkthrough — standard HTTPS and literal-loopback HTTP proof production, verification, and fail-closed rejection with ephemeral keys.

Reference set:

  • spec/bap-v1.md — the normative v1 wire profile (docs/protocol-v1.md is its generated view).
  • spec/bap-local-loopback-http-v1.md — the normative literal-loopback HTTP application profile.
  • docs/design/protocol-charter.md — what the verifier does and the verification chain.
  • docs/design/conformance-contract.md — how conformance is proven.
  • docs/design/interoperability-report.md — exact standard and application-profile cross-validation identities and results.
  • docs/design/standards-track.md — evolution, suite succession, governance, and venue strategy.
  • docs/governance.md — change classes, errata, deprecation, and security-release policy.

When NOT to use this

  • You need an authorization decision. This package verifies bytes; it never decides. An operational decision requires a stateful authority runtime that owns trust selection, replay reservation, and revocation. Treating a verification result as execution authority moves all of those checks to nothing.
  • You need transport security, confidentiality, or storage. Every wire object is plaintext JSON by design; the profile provides authenticity and integrity over TLS-transported, runtime-stored artifacts.
  • You want permissive parsing. There is no compatibility mode, no tolerance for unknown members, and no alternate-encoding acceptance — the closed rejection IS the product.
  • You need online revocation or replay defense. Those are runtime responsibilities; the verifier is stateless by contract.

Named misuses

Misuse: treating GrantFacts or EnvelopeFacts as a decision or credential. Facts carry authorization: :not_evaluated and are redacted; accepting them as authority is the central anti-pattern this package exists to prevent.

Misuse: passing a caller-provided facts struct to the runtime boundary. The runtime accepts RAW credentials at its public boundary and performs its own verification; a facts struct is an output, never an input.

Misuse: re-deriving trusted keys from the credential (e.g., using the untrusted kid as a lookup the attacker controls). kid is a case-sensitive hint; key selection belongs to the caller's trusted-key set.

Misuse: loosening bounds because a "legitimate" payload exceeded them. The bounds are part of the wire contract; an oversized payload is non-conforming, not a bug.

Misuse: accepting the proof without the server-derived request context. The request digest is computed over the SERVER's derived operation and typed arguments — never over anything the presenting client asserts.

Development

Supported Elixir/OTP: 1.18/27, 1.19/28, 1.20/29.

mix deps.get
mix quality

mix quality runs formatting, warnings-as-errors compilation, the purity architecture gate, Credo, tests with coverage, Dialyzer, documentation, dependency and license audits, CycloneDX SBOM generation, the conformance corpus and mutation gates, and an exact packed/unpacked consumer test.

Security

See SECURITY.md for the vulnerability-reporting process.

License

Apache License 2.0. See LICENSE and NOTICE.

About

Public Apache-2.0 deterministic verifier and conformance suite for bounded proof-of-possession authority

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages