Conformance contract

Copy Markdown View Source

Normative artifacts

Each released protocol major ships language-neutral JSON fixtures containing:

  • exact protected/payload JSON bytes, canonical base64url segments, standard JWS signing inputs, public keys, signatures, JWK thumbprints, request preimages, ath, and ba_req;
  • explicit trusted issuer, expected grant/request context, and exact redacted successful facts;
  • valid, boundary-near, exact-bound, and maximum-plus-one cases;
  • invalid duplicate, encoding, algorithm, key, claim, time, nonce, URI, request, selector, limit, and meaningful-byte tamper cases.

Fixtures contain public keys only, no private key/seed, and no production values. Their schema and ordering are versioned with the protocol major.

Every Draft 2020-12 schema validates against the canonical meta-schema with an independent validator. Schemas remain structural companions: JSON Schema string length counts code points, while annotated x-bap-maximum-utf8-bytes limits count bytes. Duplicate names, raw numeric lexemes, decoded-size projection, depth, nodes, canonical encodings, and every byte limit remain decoder-and-corpus checks.

Key census integrity

The manifest partitions its canonical public-key fingerprints into exact sorted sets for each independent verifier. Each set equals—not merely contains—the keys observed at that verifier's standard-library public-key import boundary, and the exact union equals the canonical manifest set across every tracked vector. Repository-wide discovery remains a supplemental private-material and unlisted-key gate; it cannot satisfy import reach. Every fixture key occupies a schema-declared key-bearing field, and the import boundary computes RFC 7638 OKP thumbprints instead of trusting claimed fingerprints.

The gate fails in both directions: when a listed fingerprint is removed while its key remains reachable, when a reached key is assigned to the wrong verifier, and when an unreferenced fingerprint is added. Manifest membership is never a substitute for signature, holder, or issuer verification.

Independent implementation rule

A vector is normative only after a second implementation that imports no project code independently recomputes canonical base64url, SHA-256/JWK thumbprints, standard JWS messages, Ed25519 validation, ath, request JCS/preimage, ba_req, selector identity, URI normalization, and exact verdicts. A self-round-trip proves only internal consistency. Byte drift or a manifest census mismatch exits nonzero.

BAP-03's focused vectors and independent verifier are public repository acceptance evidence. The Hex package ships the protocol schemas, Elixir runtime, and the portable v1 conformance corpus (283 cases across 28 surfaces with a total surface × class applicability matrix, .raw sidecars for oversize wire inputs). BAP-05's corpus is normative: a second Node implementation (conformance/corpus_independent.mjs, node:* only) independently recomputes every corpus verdict from scratch and agrees. The corpus index declares its full public-key set (public_key_fingerprints); the independent runner's published-mode census is hard two-way (observed import-boundary keys == declared, both directions, always). The manifest partitions grow to three (grant_proof + chain_archive + corpus = 19 = canonical set); the corpus partition equals the index list. The deterministic verifier CLI (escript bounded_authority_conformance, --corpus DIR required, exits 0/1/2) runs the pure core against any corpus directory; the report is deterministic JCS bytes binding the index SHA-256. The n_a applicability criterion is falsifiable: a cell is not-applicable only when the input algebra cannot express that class for that surface; required cells must be ≥1, n_a cells must be 0, both directions enforced. Every invalid vector on the cryptographic verifying surfaces (algorithm-confusion, meaningful-byte tamper, key/claim/time/request/nonce binding, archive and chain integrity) is a one-defect skip-would-accept construction — it differs from a passing valid case in exactly the dimension its class names, so a verifier that skips that check would accept it — confirmed rejected by BOTH implementations before it is normative (a wrong invalid vector silently certifies nonconforming verifiers, so it is worse than a missing one). BAP-06 owns immutable candidate-archive proof.

BAP-04's public-only chain/archive corpus includes genesis, continuation, same-key boundaries, one-step rollover, multi-step rollover, same-ID/equal-time rollover, and separately valid shortened, relinked-omission, signed cross-chain, signed reverse-time, and signed invalid-genesis artifacts. Its independent Node verifier imports no project code and recomputes strict base64url, JCS, RFC 7638 fingerprints, standard JWS/Ed25519, row-domain hashes, binary framing and digest, historical intervals, complete EOF, caller boundaries, and result verdicts. Decoded-byte commitment/link/anchor/transition/signature/key/frame/prefix drift and transition chronology/path mutations must exit nonzero at their intended verification stage.

Public verifier API constraints

Conforming APIs:

  • expose bounded untrusted_key_locator/2, returning only a closed untrusted kid hint;
  • accept raw compact credentials, exact named structs, already-trusted public keys, expected context, evaluation time, and tightening-only bounds;
  • revalidate every struct field at every public entry and return only closed verified facts or exactly {:error, :invalid};
  • permit standalone raw-grant verification to return GrantFacts, but never accept decoded/facts intermediates as credentials; combined verification re-verifies the raw grant;
  • perform no I/O, trust discovery, private-key work, clock read, randomness, replay/state check, or effect;
  • reject unknown extensions and ambiguous encodings and expose the protocol major in all results.
  • require raw canonical rows and raw archived-object chunks, mandatory caller boundaries, exact out-of-band object version, exact historical public keys, and authenticated ordered rollover;
  • never infer archive completeness from a self-consistent chain or treat current key status as evidence for a historical signature.

GrantFacts and EnvelopeFacts are value-bearing/redacted, contain only their exact documented fields, have fixed redacted inspection and no generic encoder, and carry authorization: :not_evaluated.

ChainFacts, AnchorFacts, KeyTransitionFacts, and AnchoredExportFacts follow the same closed redacted rule. They contain only bounded identifiers, times, counts, hashes/fingerprints, and performed-verification labels; no row, commitment, compact JWS, signature, public-key container, archive bytes, or object version is returned.

Nonconforming claims

Passing public vectors does not certify live revocation, replay prevention, key custody, issuance, database correctness, evidence durability, archive deletion, witnesses, transport authentication, host policy, or business-effect safety. Those require private-runtime and operational tests.

Successful decode is verification: :not_evaluated. Successful verification is non-authorizing facts. The only production invalid result is {:error, :invalid}; offline tooling may classify failures only if those classes cannot widen the runtime surface.

Cross-vendor findings → corpus/spec mapping

The cross-language SDKs (BAP-09) were hardened by a multi-vendor review that surfaced byte-level divergences from the Elixir reference. To keep the ADR 0014 D5 bar — a new SDK is authored from docs/protocol-v1.md + ADRs + the corpus alone, with no code-level derivation from the Elixir reference or a sibling SDK — each behavior the review pinned is reachable from the spec or carried as a corpus case. The table maps every byte-level finding to its pinning artifact; (d)-class host closures stay as per-SDK mutation-gates (the corpus cannot express them).

FindingSurfaceClassSpec / ADR pinWhere pinned
astral codepoint → 4-byte UTF-8 rawjcs.encodevalidRFC 8785 §3.2.2.2jcs-encode-astral-raw
DEL U+007F raw-emit (not \u007f)jcs.encodevalidRFC 8785 §3.2.2.2 + protocol-v1.md JCS subsectionjcs-encode-del-raw
ECMAScript float thresholds (e<-6,e>=21)jcs.encodevalidRFC 8785 §3.2.2.3 + ECMA §7.1.12.1 / TC39 §6.1.6.1.20jcs-encode-float-* (5 cases, both sides)
float cast_arguments digestrequest_digestvalidREQ1-DIGEST-typed-projectionrequest-digest-float-cast-arguments
malformed UTF-8 member namejson.decodeinvalid_encodingREQ1-JSON-no-normalizationjson-decode-malformed-utf8-member-name
float magnitude over boundjson.decodeinvalid_encodingREQ1-JSON-raw-lexemejson-decode-float-magnitude
malformed IPv6 literalsuri.normalizeinvalid_uriREQ1-URI-reject-listuri-normalize-ipv6-* (3 cases)
locator decodes protected onlyuntrusted_key_locatorvalidREQ1-LOCATOR-opaque-payloaduntrusted-key-locator-empty-payload-signature
canonical re-encode of raw rowscheck_chaininvalid_encodingREQ1-CHAIN-raw-rowscheck-chain-canonical-reencode
rejects sequence 0check_chaininvalid_claimADR 0004check-chain-sequence-zero-row
genesis previous_hash bindingcheck_chaininvalid_claimREQ1-CHAIN-raw-rowscheck-chain-genesis-previous-hash-forge
seq-1 + nonzero previousencode_consumption_entryinvalid_encodingADR 0004 (genesis row)encode-consumption-entry-invalid-seq1-nonzero-previous
seq-0 + nonzero chain_hashboundary_anchor_signing_inputinvalid_keyADR 0004 (genesis anchor)boundary-anchor-signing-input-invalid-seq0-nonzero-chain-hash
start-anchor binding (seq == first_sequence-1)encode_anchored_exportinvalid_encodingADR 0004encode-anchored-export-invalid-start-anchor-binding
empty chunkverify_anchored_exportinvalid_encodingREQ1-EXPORT-complete-scanverify-anchored-export-invalid-empty-chunk
non-monotone effective_atverify_anchored_exportinvalid_timeADR 0004 "strictly increase"verify-anchored-export-invalid-non-monotone-transitions
fingerprint cycle A→B→Averify_anchored_exportinvalid_keyADR 0004 "cannot cycle"verify-anchored-export-invalid-fingerprint-cycle
one-key / zero-transition validverify_anchored_exportvalidADR 0004 (no min key count)verify-anchored-export-valid-one-key
per-node encode boundsjcs.encode(d)bounds threading (host closure)per-SDK mutation-gate (ADR 0014 D6/D7)
transition-count bound (257)encode_anchored_exportn_aREQ1-BOUNDS at bounds.newindex.json n_a reason (representation-blocked)
chunk-count bound (65797)verify_anchored_exportn_abounds.new archive_chunksindex.json n_a reason (representation-blocked)

The two maximum_plus_one n_a reasons name the consuming-surface representation constraint: the over-bound structured array (65797 chunks or 257 transitions) exceeds the loader's case-file json_bytes ceiling (65536) and the .raw sidecar is single-blob only, so the input cannot be carried as a corpus case; the bound itself stays pinned at bounds.new and is proven red-capable by the source-isolated mutation battery. The upper JCS float threshold (>= 21) is unreachable from the bounded algebra (the largest representable float has decimal exponent ≈ 15.95), so no corpus case pins it — the spec subsection documents both thresholds and the reference implements both.