0.5.2 - 2026-10-16

Changed

  • Architectural decision records moved out of the public repository and package into the maintainer's private records; the published documentation no longer references them. The architecture and threat model describe the accepted decisions in place (source-bound profiles, the extension surface, the atomic replay envelope), and the public document list shrank accordingly. No library behavior changed.
  • RequestSeal.Plug.Verify's {:halt, status} rejection now sets the halted flag before sending the response instead of after. The wire behavior is identical (Plug reads the flag only after the pipeline returns); register_before_send callbacks now observe the final halted state. The Plug transport test harness reports its observed connection from a before_send hook, so the report strictly precedes the response bytes and can no longer be lost when a client closes the connection immediately after receiving a rejection.

0.5.1 - 2026-10-16

Fixed

  • A JWKS key whose alg names an algorithm RequestSeal does not verify with — an encryption key ("RSA-OAEP", "A128GCM"), or a signing name outside both the HTTP and JOSE registries ("ES512") — is now skipped, and the rest of the key set still fetches and resolves. Previously any out-of-registry alg rejected the whole set with :invalid_key_set, so a publisher's JWKS that mixed signing keys with an encryption key was unusable. A malformed non-string alg still rejects the whole set. The :max_keys bound still counts every listed key.
  • The Plug target-fidelity test suite no longer flakes on saturated CI runners: each real-Bandit-transport assertion drains the helper's per-request reports so the mailbox never backs up between iterations, and the observed-result window is 15 seconds (a starved handler process can trail the client's received response by more than the previous 5-second window; the identical 0.5.0 tree passed one lane and failed a cold-cache rerun of another purely on this timing). No library code changed.

0.5.0 - 2026-10-16

Breaking changes

  • RequestSeal.AshHooks.Http.request/5 now requires a nonce on every signed attempt: a signing spec with nonce: nil returns {:error, {:terminal, :request_seal_signing_failed}} before any request is sent. It previously signed such attempts without a nonce. Remove nonce: nil from ash_hooks signing specs.
  • RequestSeal.Plug.Verify on an already verified connection: a repeat with the same :policy and :label now reuses the first result; a repeat with any other policy or label fails closed. RequestSeal.Plug.verification/1 then returns :error, private state holds {:error, %RequestSeal.Adapter.Error{reason: :already_verified}}, every Verify assign is removed, and on_reject applies. Previously any repeat replaced the result with that error. Migration: install Verify once per pipeline; if a route needs stricter checks, verify once with the stricter policy instead of stacking a second Verify.

Added

  • RequestSeal.Replay.Postgres.ddl_index/1 returns a CREATE INDEX statement on retain_until for the replay table, so caller-scheduled sweeps delete through an index instead of a scan that grows with the table. Execute it once beside ddl/1; it accepts table names of at most 46 bytes so the derived index identifier (17 more bytes) fits PostgreSQL's 63-byte name limit.
  • RequestSeal.verify_quorum/3 accepts a :deadline option: an absolute System.monotonic_time(:millisecond) value bounding the slot-assignment search's wall-clock time, closing the capacity gap where a crafted message could hold a verifier at the full node budget (~0.5–1 s CPU). Expiry keeps the best-so-far acceptance semantics (a policy-satisfying assignment still succeeds) and otherwise returns the new closed reason :deadline at :quorum — the first retryable RequestSeal.Error (the same call may succeed with a later deadline or under less load). Without the option, the deterministic 131,072-node budget stays the only bound, so conformance behavior is unchanged.
  • A versioned telemetry contract (ADR 0006): RequestSeal.Telemetry documents the v1 events — [:request_seal, :verify, :stop], [:request_seal, :verify_quorum, :stop], [:request_seal, :web_bot_auth, :verify, :stop], [:request_seal, :replay, :claim], and [:request_seal, :custody, :sign, :stop] — each with a native-unit :duration measurement and low-cardinality outcome/reason/layer/detail metadata only; no label, key ID, URL, byte, or exception text ever appears. The :telemetry package (~> 1.0, no transitive dependencies) becomes the library's single required dependency. Both signing forms emit the custody event: generated-spec function signers now run through the shared custody signing path, which also applies its documented 1–1,024-byte signature bound to them (previously only the explicit form enforced it for function signers).
  • RequestSeal.Plug.Verify accepts a :name option (atom, or 1..256-byte binary): a later Verify on the same connection reuses the first result when its :name and :label are equal, even when the :policy value is a rebuilt-equivalent policy whose function fields make == fail — the name is the caller's explicit assertion that the policy is the same. A repeat with a different name, no name, or a different label still fails closed :already_verified.
  • Discovery caches revalidate conditionally: when a :jwks_uri or :directory source serves an ETag, refetches send If-None-Match (RFC 9111) and an unchanged source's 304 keeps the stored key set while extending freshness under the 304's own cache directives (the stored representation's lifetime persists when the 304 omits them, per RFC 9111 Section 3.2; a 304 naming a different validator rejects :invalid_response, per Section 4.3.4; a kept key still expires at its own exp or the directory proof's bound, whichever it reached first). The stored validator is visible as RequestSeal.Discovery.KeySet's new etag field. Validators never cross a redirect (they identify their own request target) and CIMD sources always revalidate unconditionally — a CIMD document's 304 says nothing about the nested JWKS it names. An unsolicited 304 (no conditional was sent) rejects :unexpected_status as before.

Changed

Closed error reasons that callers may match on changed as follows (old → new).

  • RequestSeal.Discovery.Cache.resolve/4 and refresh/2 on a cache that is not running: :invalid_response (not retryable) → :source_unavailable (retryable), as remove/3 and invalidate/2 already returned.
  • RequestSeal.Replay.Postgres: a lost connection or a shutting-down server, :store_failed → :store_unavailable; a server-side statement cancellation or lock timeout, :store_failed → :store_timeout. Other database errors stay :store_failed.
  • RequestSeal.Replay.Postgres and RequestSeal.Replay.AshOnetime: a pool queue timeout, or a connection failure or rolled-back transaction observed after the claim's deadline (a client timeout), :store_unavailable or :store_failed → :store_timeout.
  • RequestSeal.sign/4 with a generated signing spec and a key handle bound to another algorithm: :invalid_options → :signer_algorithm_mismatch (layer :input), like the explicit form. Req, Finch, and Plug keep :invalid_options and carry the core error in source.
  • RequestSeal.Profile.dictionaries/2 with an invalid message or a max outside 1..64: :invalid_signature_input → :invalid_message or :invalid_options, checked before parsing. RequestSeal.Profile.preflight/4 with an extra other than :allow or :reject, or invalid freshness values such as a negative skew: used as given (for example :unexpected_component or :expired) → :invalid_policy.
  • RequestSeal.Req.attach/2 with a verify map whose max_stream_bytes is nil: :unsupported_delivery → :invalid_options, as for any other non-integer or negative value.
  • RequestSeal.Plug.Verify.init/1 with a Boolean :assign: accepted → raises RequestSeal.Adapter.Error with :invalid_options.
  • RequestSeal.JOSE.JWS.sign/4 and RequestSeal.JOSE.JWE.encrypt/4 with a header that is not a list of {name, value} pairs: :limit → :invalid_header. More than 64 pairs stays :limit.
  • RequestSeal.WebBotAuth.sign/4 with an invalid caller-supplied :agent location, such as a non-HTTPS URL: :invalid_signature_agent at :fields → :invalid_options at :input. An invalid existing Signature-Agent field keeps :invalid_signature_agent.
  • Verify and sign entry points are total: an unexpected internal fault returns the new closed reason :internal at the new layer :internal, with the exception class (the raised exception's module, or :exit/:throw) in detail; previously such a fault escaped RequestSeal.verify/3, verify_quorum/3, Web Bot Auth verification, or sign/4 as an uncaught exception. A defective label inside a quorum becomes that label's nonqualifying fact instead of failing the whole call. The crypto layer's blanket rescue is narrowed to OTP's argument/runtime error classes, so logic-defect classes surface as :internal instead of masking as input reasons; the [:request_seal, :verify, :stop] telemetry event makes them observable. RequestSeal.Error's retryable field widens from the literal false to boolean() (:deadline is the only retryable reason).
  • DNS resolution failures split: an authoritative NXDOMAIN on every queried family returns the new nonretryable :domain_not_found (a configuration error — the configured host does not exist), while every other :resolution_failed becomes retryable (possibly-transient resolver trouble). RequestSeal.Discovery.Error's retryable set gains :resolution_failed.
  • JWKS keys may assert alg with standard RFC 7517 JOSE names ("EdDSA", "RS256", ...) as well as HTTP registry identifiers; either spelling normalizes once at import to its standard JOSE name, and {:jws, name} tuples in algorithm allowlists match either asserted spelling in RequestSeal.Discovery.KeySet.lookup/3 and directory signature verification — including PS256 and PS384, which exist only as JOSE names. An alg outside both registries still rejects the whole set :invalid_key_set.
  • RequestSeal.JOSE.Nested.verify/4 accepts an inner JWS carrying a non-JOSE cty (for example application/json); only values naming another JOSE envelope reject :nesting_depth — application/jose, application/jose+json, the per-structure application/jws and application/jwe, and RFC 7519's application/jwt (abbreviated JWT), case-insensitively, with RFC 7515 Section 4.1.10's abbreviation rule and media-type parameters ignored for the classification. Previously any inner cty rejected.
  • Signature-base hot path: field-value OWS trimming uses binary parts instead of integer-list round-trips (the 1 MiB / 256-component base measures ~2.5× faster at p50, ~5× at p99), message builder counting is single-pass, and a verification validates the message once instead of ~5× (the parse/build/check internals gained validated fast paths used by the verification pipeline; the public entry points validate exactly as before).
  • Development and test dependency refresh within pins: ash 3.34.6 → 3.36.1, ash_hooks 2.0.3 → 2.0.4, ash_onetime 1.5.0 → 1.6.0 (lockfile only; no package requirement changed). Development-only additions: dialyxir in the declared gate (static analysis with an exact-entry .dialyzer_ignore.exs; the CI latest lane caches the PLT) and a grouped Dependabot configuration for the two Mix environments and GitHub Actions.

Fixed

  • RequestSeal.sign/4 (explicit form) no longer raises an uncaught MatchError when the one-entry Signature-Input dictionary would serialize past the 65,536-byte Structured Fields ceiling (for example, a signature input at the ceiling with any label, or a 256-byte label); it returns {:error, reason: :limit, layer: :input}. The Signature and field-occurrence serialization sites in the same pipeline received the same total mapping.
  • RequestSeal.Discovery cache freshness now honors RFC 9111's qualified no-cache and no-store spellings, such as no-cache="content-type" and the RFC 7234 #field-name form with commas inside the quoted value. Previously only the bare tokens forced a zero freshness lifetime, so a publisher demanding revalidation could have its keys served from the caller cache up to max_ttl (default 86,400 seconds). The directive name before = is now matched case-insensitively in every form.
  • RequestSeal.Discovery's HTTP/1.1 client skips up to four interim 1xx responses (for example 103 Early Hints from a CDN) before the final status, per RFC 9110 Section 15.2. A 1xx used to be read as the final status and rejected as non-retryable :unexpected_status, making an Early Hints origin permanently unfetchable.
  • A CIMD document's nested jwks_uri is now checked against the origin that actually served the document instead of the originally configured origin. Under redirect_scope: :any_https with a cross-origin redirect, the previous check rejected a same-origin-with-the-document JWKS and, when satisfied, labeled key provenance with the wrong origin.
  • RequestSeal.JOSE resolver entries without an :algorithm key now reject with :key_resolver_failed, as documented; :algorithm_mismatch stays reserved for a present, different algorithm. Oversized caller-supplied JOSE header strings (over 16,384 bytes per value or key) reject with :limit before serialization allocates, instead of building the full JSON first.
  • RequestSeal.Plug's request-local state reader tolerates a foreign conn.private[:request_seal] value by treating it as absent (no capture), instead of raising inside Plug.Parsers; the value is server-controlled and inside the documented trust boundary, so this hardens the parser path only.
  • The Plug delivery adapter's replay-digest comparison hashes the retained bytes directly instead of finalizing the shared incremental hash state, so post-verification body reads keep updating a live state instead of relying on undocumented OTP crypto reuse semantics.
  • RequestSeal.AcceptSignature.fulfill/5 reports the underlying signing failure's bounded reason in the error's detail when it returns :negotiation_unfulfillable.
  • RequestSeal.AshHooks.Http.request/5 returns {:error, {:terminal, :request_seal_signing_failed}} for a malformed options container instead of raising out of the ash_hooks callback.
  • corpus/index.json's elixir_release now follows the package version, and the declared gate fails whenever it differs from v plus the mix.exs version. At the v0.4.1 tag the corpus was identical to v0.4.0's, elixir_release: "v0.4.0" included, so the corpus release workflow refused the tag and v0.4.1 has no corpus release. The field follows the package version from this change on; this release publishes its corpus once the gate passes.
  • Development and test dependency Bandit moved to ~> 1.13.1, pulling thousand_island to 1.6.0, clearing mix hex.audit advisories EEF-CVE-2026-105659 (HIGH) and EEF-CVE-2026-105660 (MEDIUM) against Bandit 1.12.5. Bandit is a test-only dependency and ships in no published package; no package requirement changed. The notebook toolchain's Bandit override moved with it.
  • The internal signature-fields component parser's inner/1 (used by policies, adapters, and Web Bot Auth) returns one error shape — {:error, %RequestSeal.StructuredFields.Error{}} — instead of also returning a bare :error; every in-tree caller is updated in the same pass and the Plug target check no longer relies on an internal match that could raise.
  • RequestSeal.Digest.check_stream/3 rejects an invalid message with {:error, %RequestSeal.Digest.Error{reason: :invalid_message}}; it previously surfaced the same reason as a RequestSeal.StructuredFields.Error through the field parser. check/3 is unchanged.
  • RequestSeal.Custody's identity/1 callback specification admits the documented {:error, reason} return that function-signer and JOSE custodian bridges produce (a callback_type_mismatch found by the new static analysis).

Documentation

  • RequestSeal.sign/4 now states that its explicit form preserves caller metadata beyond what verify/3 accepts — a nonce above verification's 1,024-byte bound still signs and is rejected by the verifier with :limit — and that callers signing for RequestSeal verifiers should keep nonces within that bound. RequestSeal.verify_quorum/3 documents that a quorum rejection is returned before Accept-Signature negotiation is evaluated. The Req adapter documents that cross-origin credential stripping is monotonic across the redirect chain. Message.request/5 documents that a generated content-digest occurrence counts toward the combined 1,024 field bound. The key-custody guide states that CEKs cannot be zeroized on the BEAM and names the process-level mitigation; the key-discovery guide covers qualified no-cache spellings and interim 1xx responses.

  • Every public function now documents its parameters, options with defaults and bounds, return shape, the error reasons it can return, and executed doctest examples. RequestSeal.Req.attach/2, RequestSeal.Req.verification/1, the RequestSeal.Finch functions, RequestSeal.JOSE.JWS.sign/4, RequestSeal.JOSE.KeyManagement.wrap/4 and unwrap/4, and RequestSeal.JOSE.Nested.verify/4 had no documentation before. Replay store callbacks are documented on each adapter.

  • RequestSeal.Ash.Scope's type now admits replay: :claimed, which RequestSeal.Ash.scope/2 already produced.

  • New guides: diagnose and handle errors, rotate signing keys, and test your integration. New Livebooks: core signing and verification, a Plug webhook receiver, Web Bot Auth, and JOSE; the RFC 9421 notebook now verifies the published vector through RequestSeal.

  • Corrected documentation: size limits of Body and Plug capture, the spec-form nonce option, Web Bot Auth max_lifetime, the Req redirect default, the TypeScript package name (@request-seal/core), SECURITY.md's description of the library and its private vulnerability reporting route (GitHub's "Report a vulnerability"), and the release runbook.

0.4.1 - 2026-10-10

  • RequestSeal.Custody.sign/3, verify/4, RequestSeal.Crypto.sign/4 and verify/5 accept max_bytes: (1–16,777,216, default 1,048,576). The default and the existing :invalid_data rejection are unchanged. Out-of-range or non-integer values reject with :invalid_options before handle, capability, or algorithm validation. unwrap/3 does not accept it. Crypto.max_bytes_ceiling/0 exposes the shared ceiling.
  • Direct Custody.Local.sign/4, Local.verify/5, Custody.SSHAgent.sign/4, and SSHAgent.verify/5 callback calls (outside RequestSeal.Custody) accept up to the 16,777,216-byte ceiling; Local previously stopped at 1 MiB. They reject larger or nonbinary inputs with the bare reason {:error, :invalid_data} before reading the key reference, so such inputs no longer report :key_mismatch or a RequestSeal.Crypto.Error struct first. The public RequestSeal.Custody and RequestSeal.Crypto functions are unchanged for callers that pass no max_bytes:.
  • Third-party custodians that sign with Crypto.sign/4 or verify with Crypto.verify/5 must pass max_bytes: RequestSeal.Crypto.max_bytes_ceiling(); custody has already enforced the caller's bound.
  • The SSH-agent custodian forwards larger requests unchanged. OpenSSH caps the whole agent message at 256 KiB, including protocol overhead, so the practical payload limit is slightly under 262,144 bytes. Observed with OpenSSH 9.9 and Ed25519: 262,000 bytes signed successfully; 262,200 returned :custodian_protocol.
  • Message.request/5 and Message.response/4 (1 MiB body default), message field totals, and compact JWS keep their 1 MiB limits; Body.new/1 and RequestSeal.Plug.Capture keep their caller-selected bounds. This release does not propagate the new custody/crypto opt-in to those APIs. RequestSeal.SignatureBase retains its separate 1 MiB ceiling.

0.4.0 - 2026-10-10

  • Accept an algorithm-bound RequestSeal.KeyHandle in the explicit RequestSeal.sign/4 form. signing_timeout accepts 1–300,000 ms, default 5,000. The existing arity-two function signer stays synchronous and preserves its existing errors and wire bytes.
  • A handle/spec algorithm mismatch in that form returns reason :signer_algorithm_mismatch, layer :input, and the exact message string "signer algorithm does not match signature specification".
  • Preserve custody signing failures as RequestSeal.Error with reason :signing_failed, layer :crypto, and a bounded RequestSeal.Custody.Error in source. For the existing signing-spec path with a KeyHandle, the reason was :signer_failed, now :signing_failed with the custody error as source. Core errors add message and source fields, both nil for existing non-custody failures. Malformed signing output uses custody :invalid_signing_output; malformed custody errors use :custodian_failure on both HTTP signing paths.
  • Add caller-supervised RequestSeal.Custody.Local.Owner for Ed25519 seeds from tagged environment, file, and file-from-environment sources. Encodings are :base64url, :base64, :hex, and :raw; source inputs are bounded to 4,096 bytes and decoded seeds to 32 bytes. Files require mode 0600 or stricter and must be regular, not symbolic links. Bad keys report bounded statuses and fail closed without preventing startup.
  • Owner restart rereads sources and retires cached handles. Fetch per operation or fetch again and retry once on :key_not_found; prefer direct file sources in production. Environment variables are never deleted by the Owner.
  • Owner queries distinguish :owner_unavailable from an unconfigured key. Empty environment values and empty file paths from environment variables report :unconfigured. Files removed or replaced after initial checks report :insecure_file. Encoded seeds require canonical trailing bits and trim only ASCII space, tab, CR, and LF. Base64url padding remains optional.
  • Owner terminate and crash reports discard private reason terms while retaining configured key names, bounded statuses, and an atom failure reason.

0.3.1 - 2026-10-09

  • Tighten corpus validators to skip .DS_Store only for empty regular files or files starting with Finder metadata magic; reject other contents and directories. Refuse release packaging if any .DS_Store exists and exclude the name from tar as a second guard.

0.3.0 - 2026-10-09

  • Reject Structured Fields byte sequences with nonzero unused base64 pad bits under RFC 4648 Section 3.5; retain missing-padding recovery under RFC 8941 Section 4.2.7. Pin the affected HTTP WG cases to rejection.
  • Enforce RFC 3986 Section 3.2.2 IP-literal grammar for message authorities: accept IPv6 and IPvFuture, reject bracketed IPv4 forms and zone identifiers.
  • Add a canonical conformance corpus, source provenance, digest inventory, full batch execution, rejection rule mapping, and mutation checks.
  • Execute all twelve RFC 9421 signature bases and five Appendix B.4 transformations independently; correct rejection inputs to isolate one defect.
  • Publish deterministic checksummed corpus assets from release tags.
  • Bound caller clocks and replay sweeps at 253,402,300,799 Unix seconds; retain the Structured Fields wire integer range. ETS and Postgres sweeps now reject negative inputs. Generic verification rejects negative caller clocks.
  • Reject replay claims whose retention end exceeds 253,402,300,799; no accepted sweep clock can evict them. Stores return their existing invalid-claim failure.
  • Reject generic and Web Bot Auth verification whose derived replay retention end exceeds 253,402,300,799 with :retention_exceeded at :replay before commitment or storage. Bound policy max_age at the same maximum; larger values reject with :invalid_policy. Skew retains its existing 0..86,400-second range.
  • Upgrade note: Postgres replay rows written before 0.3.0 with retain_until above 253,402,300,799 are never swept by sweep/3. Operators can remove them manually (replace replay_claims with the caller-selected table name): DELETE FROM "replay_claims" WHERE retain_until > 253402300799;
  • Reserve hyphenated core profile package names.
  • Document the separate TypeScript package in progress and corpus vendoring.

0.2.0 - 2026-10-09

  • Document RequestSeal.Custody.run/2 as a public deadline and cancellation boundary, with sensitive workers, caller and context-owner monitoring, nested cancellation, bounded failures, and revocable replies with cleanup.
  • Change the formerly undocumented Custody.run/2 return contract: bare :ok returns {:ok, :ok}; other bare returns become :custodian_failure; non-Context arguments return :invalid_options instead of raising; context-owner death returns a nonretryable :custodian_failure error instead of silence. An owner already dead at entry prevents callback execution.
  • Document the dirty-NIF cancellation limit: cleanup waits for the runner's exit, so a long native call can overshoot the deadline.
  • Add RequestSeal.Profile.valid_signature_input?/1 for parsed RFC 9421 Signature-Input Inner List semantics, delegating to the shared validator.
  • Document RequestSeal.Replay.Claim.valid?/1 without changing its validation.
  • Document JWS verification and JWE encryption/decryption arguments, exact policy shapes, error results, and executable cryptographic examples.

0.1.0 - 2026-10-08

First public release: RFC 9421 HTTP Message Signatures, source-selected Web Bot Auth, compact and nested JOSE, caller-owned key custody and discovery, replay protection, and optional Plug/Phoenix, Req, Finch, Ash, ash_hooks, and ash_onetime integrations. Includes executable guides, published-vector checks, and Livebooks.

  • Cover webhook URL queries, generate a new nonce per attempt, reject duplicate signed fields, and preserve transport exceptions with distinct signing errors.

  • Bind inbound webhook policy resolution to tenant and key reference.

  • Validate durable replay claims before encoding and anchor retention to the database transaction clock; retain spent nonces across indeterminate timeouts.

  • Preserve portable replay guide coverage and require optional integration metadata and Elixir 1.20 or newer for publishing.

  • Added optional RequestSeal.Replay.AshOnetime for independently committed durable nonce claims, deadline propagation, and ash_onetime-managed retention and cleanup. It is the recommended durable store; Replay.Postgres remains available.

  • Added RequestSeal.AshHooks.Http for final-byte RFC 9421 signing through ash_hooks' bounded transport, and RequestSeal.AshHooks.verify_signature/4 for Provider delegation. Added executable webhook and durable replay examples.

  • Optional ash_onetime ~> 1.5 and ash_hooks ~> 2.0 run on the latest Elixir lane; Elixir 1.18/1.19 omit them. Raised the Ash dependency floor to 3.34.3.

  • Req and Finch accept the same defaulted signing specs as RequestSeal.sign/4, with fresh nonces per attempt and unchanged full-spec wire bytes.

  • Plug verification accepts zero-arity policy functions and policy MFAs, validates their results per request, and contains callback failures.

  • Req and Finch reject the :nonce option; selected nonces regenerate per attempt or invocation.

  • Spec signing preserves :unsupported_component and :invalid_request errors and bounds callback faults without hiding internal exceptions.

  • Core spec signing defaults metadata and requires a positive integer expiry; negative clocks reject.

  • sign/4 documents separate types for explicit-input function signing and spec function or key-handle signing.

  • The request builder lowercases scheme/host, omits default ports, and rejects fragments and invalid UTF-8 before URL parsing.

  • Added lossless request/response builders and framework-free spec signing shared with the Req and Finch adapters. The quick start uses exact body bytes and an explicit verification policy.

  • Custody runners and middle processes now set sensitivity before every operation. JWE handle-binding failures follow the same random-CEK authentication path and complete error as OAEP/GCM failures; direct unwrap rejects empty OAEP plaintext with :decryption_failed. Added recipient-handle guides and startup health checks.

  • Added algorithm-bound local RSA-OAEP and RSA-OAEP-256 decryption custody, private PEM/PKCS #8 DER/JWK import, and RequestSeal.Custody.unwrap/3; JWE resolvers accept unwrap handles while retaining random-CEK fallback and identical OAEP/GCM failure errors.

  • Added executable installation and integration examples, task-oriented guides, and grouped API documentation. Documentation metadata is retained in hidden HTML comments; the documentation check requires byte-identical tested Elixir examples.

  • The TypeScript counterpart and npm package remain deferred; this release targets the Elixir library.

  • JOSE protected headers now use compact JSON with top-level members in caller order.

  • Feature-adaptive public-key encoding supports Elixir 1.18 or newer on OTP 27 or newer, tested in CI on Elixir/OTP pairs 1.18.4/27, 1.19.5/28, and 1.20.4/29. PKCS #8 v2 (OneAsymmetricKey) containers reject. CI validates all three pairs and optional client floors on the floor toolchain.

  • Added fixed-seed parser, signature, and replay properties with 300 runs per property, real cryptography, and caller-provisioned PostgreSQL storm checks.

  • Compact and nested JOSE parsing stops at the first excess segment instead of allocating all attacker-supplied segments.

  • Discovery rejects trailing non-JSON whitespace according to RFC 8259 Section 2.

  • Added reproducible input-ceiling and RSA-OAEP failure-timing measurements; the threat model records the observed distributions.

  • Added a package-namespaced extension surface for named application profiles.

  • Added explicit directory-assigned discovery key IDs while retaining computed thumbprints for identity, revocation, and removal.

  • Added RequestSeal.JOSE.JWS.sign_protected/4 for caller-serialized protected JSON bytes with the same bounds and header rejections as Header.json/1.

  • Added explicit Web Bot Auth protocol-00 signing and source-bound verification with nested coverage and whole-envelope replay; deployed-verifier acceptance remains open.

  • RequestSeal.Verification inspection no longer shows principal for any profile; access authenticated principals explicitly.

  • Plug verification requires the original replay adapter and, after any replay read, full draining with a digest matching the complete captured body. Partial reads, unread-suffix tampering, and replaced adapters reject before key resolution. The configured RequestSeal body reader records invocation; custom readers that bypass both it and the adapter remain a documented caller-configuration boundary.

  • Invalid replay lengths return bounded errors without rewinding. Malformed response callback state produces an empty unsigned failure; absent callback state remains supported. Parameterized set-cookie coverage also rejects pending cookies.

  • Plug verification permits unread raw bodies after pass-through parsing, including requests without a content type. Trusted IPv6 ranges containing IPv4-mapped addresses also match native IPv4 peers; unrelated transition forms do not acquire IPv4 trust.

  • Plug Capture omits connection headers on HTTP/2 rejects. Retained-body replay runs at the adapter for multipart and default readers, honors read lengths, and checks a handed-out byte digest before verification. Wrapper readers must preserve returned bytes.

  • Plug response signing runs after registered callbacks and rejects covered set-cookie fields when response cookies await Plug's later merge, including cookies added by session callbacks. Explicit final cookie headers remain signable.

  • Plug verification rejects required or covered @request-target, @target-uri, and @query with :unsupported_component when exact transport evidence is unavailable. Response signing refuses the same related-request components; path, method, authority, fields, and content coverage remain usable.

  • Added optional Plug/Phoenix request capture, explicit trusted-origin reconstruction, private verification results, parser body replay, and final-response signing bound to the captured request.

  • Added bounded compact JWS/JWE and one-level nesting with pinned algorithms, protected bytes, sensitive results, published RFC/Wycheproof vectors, real entropy failure, and reciprocal Node WebCrypto/OpenSSL checks.

  • Req signing rejects transport overrides outside an explicit option allowlist; allowed cross-origin redirects strip standard and caller-declared credential headers. Verified responses use bounded collection and verification before retry or redirect, including intermediate responses. Function signers share custody deadlines and cancellation.

  • Local custody now keeps key material in sensitive per-handle holder processes with random 32-byte tokens and constant-time token checks; holders live until creator exit or explicit RequestSeal.Custody.Local.release/1, so long-lived owners must release unused handles or reuse handles created at startup.

  • Added explicit Ash actor/tenant/context bindings, authorization-enabled scopes, and real ETS policy denial and tenant-isolation checks.

  • Added optional Req/Finch adapters with final-attempt signing, fresh retry/redirect parameters, explicit origin policy, exact response association, and bounded verified streaming delivery.

  • Added RFC public-key thumbprints and explicit HTTPS directory/JWKS/CIMD discovery with signed possession proofs, address and byte limits, cancellation, and a caller-started cache for freshness, rotation, removal, and revocation.

  • Discovery cache freshness honors Cache-Control before Expires with Date/Age accounting; fallback freshness defaults to 300 seconds and negative entries are capped at 300 seconds. CIMD failures are not cached, expired snapshots are not served after refresh failure, and 5xx status failures are retryable while 4xx failures are not.

  • Added post-validation nonce replay commitments, bounded deadlines and retention, atomic caller-owned ETS and optional Postgrex stores, and real concurrency checks.

  • verify/3 intentionally fails closed with :duplicate_label for repeated labels within or across Signature-Input or Signature field occurrences, per RFC 9421 Section 3.2.

  • Signing and single-label/quorum verification reject repeated parameter names in either signature dictionary with :duplicate_parameter.

  • Key resolvers accept an optional :identity member carrying trusted RequestSeal.KeyIdentity for quorum key-equivalence counting; identities remain internal and establish no principal attribution.

  • Added explicit RFC 9421 quorum verification, trusted key/principal/role counting, nested bindings, and Accept-Signature challenge negotiation.

  • Quorum results use a plural label-keyed signatures map for assigned signatures; satisfied includes optional slots, and negotiation can be fulfilled by a verified eligible label outside the counted assignment.

  • Quorum assignment maximizes distinct units before filled slots, enforces unique labels and merged identity classes, and searches binding alternatives under a 131,072-node budget. New identity-bridging evidence can reduce an otherwise feasible count; budget exhaustion succeeds only with a policy-satisfying assignment.

  • Quorum verification rejects required replay before callbacks and stores run. Slot policies accept retained-body Content-Digest checks, rejecting representation digests and caller-fed digest state.

  • Added opaque caller-owned key handles, local OTP/private PEM/JWK custody, trusted symmetric equivalence, monitored deadlines and cancellation, and verified non-exporting OpenSSH agent signing.

  • Added explicit generic RFC 9421 policy, single-label layered verification, caller-owned signing functions, bounded safe errors, and published signed-message checks.

  • Added six HTTP signature primitives, explicit JWS algorithm selection, public key formats and binding, published vectors, and reciprocal OpenSSL checks.

  • Added RFC 9530 content/representation digests, bounded incremental SHA-256/SHA-512 hashing, explicit trailer checks, and digest preference fields.

  • Added RFC 9421 signature-base construction with all derived components, ordered field parameters, bounded safe errors, and independent Sections 2, 4.3 and Appendix B vectors.

  • Added bounded RFC 8941/9651 Structured Fields parsing, canonical serialization, explicit field type schemas, and ordered Message occurrence processing with independent HTTP WG vectors.

  • Digest check/3 and check_stream/3 reject repeated supported checksum algorithms with :conflicting_digest, including identical checksums.

  • Streaming init/3 accepts an explicit max_bytes above 16 MiB; retained-body compute/3 and check/3 keep that hard limit.

  • Digest preferences/3 rejects sections other than :headers and :trailers.

  • Added lossless HTTP message constructors and validators for raw ordered fields, request-target forms, response linkage, explicit trailers, caller-owned body availability, and declared transport facts, with bounded errors and redacted inspection.

  • Added executable API documentation, published RFC inputs, and a captured HTTP exchange for message preservation and rejection tests.

  • Established the local Elixir library scaffold and exact development toolchain.

  • Defined HTTP signature and Web Bot Auth scope, architecture decisions, public technical explanations, and interoperability principles.

  • Added official-Livebook execution tooling and a published RFC signature learning notebook.