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_sendcallbacks now observe the final halted state. The Plug transport test harness reports its observed connection from abefore_sendhook, 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
algnames 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-registryalgrejected 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-stringalgstill rejects the whole set. The:max_keysbound 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/5now requires a nonce on every signed attempt: a signing spec withnonce: nilreturns{:error, {:terminal, :request_seal_signing_failed}}before any request is sent. It previously signed such attempts without a nonce. Removenonce: nilfrom ash_hooks signing specs.RequestSeal.Plug.Verifyon an already verified connection: a repeat with the same:policyand:labelnow reuses the first result; a repeat with any other policy or label fails closed.RequestSeal.Plug.verification/1then returns:error, private state holds{:error, %RequestSeal.Adapter.Error{reason: :already_verified}}, every Verify assign is removed, andon_rejectapplies. 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/1returns aCREATE INDEXstatement onretain_untilfor the replay table, so caller-scheduled sweeps delete through an index instead of a scan that grows with the table. Execute it once besideddl/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/3accepts a:deadlineoption: an absoluteSystem.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:deadlineat:quorum— the first retryableRequestSeal.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.Telemetrydocuments 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:durationmeasurement and low-cardinalityoutcome/reason/layer/detailmetadata only; no label, key ID, URL, byte, or exception text ever appears. The:telemetrypackage (~> 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.Verifyaccepts a:nameoption (atom, or 1..256-byte binary): a later Verify on the same connection reuses the first result when its:nameand:labelare equal, even when the:policyvalue 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_urior:directorysource serves anETag, refetches sendIf-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 ownexpor the directory proof's bound, whichever it reached first). The stored validator is visible asRequestSeal.Discovery.KeySet's newetagfield. 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_statusas before.
Changed
Closed error reasons that callers may match on changed as follows (old → new).
RequestSeal.Discovery.Cache.resolve/4andrefresh/2on a cache that is not running::invalid_response(not retryable) →:source_unavailable(retryable), asremove/3andinvalidate/2already 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.PostgresandRequestSeal.Replay.AshOnetime: a pool queue timeout, or a connection failure or rolled-back transaction observed after the claim's deadline (a client timeout),:store_unavailableor:store_failed→:store_timeout.RequestSeal.sign/4with 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_optionsand carry the core error insource.RequestSeal.Profile.dictionaries/2with an invalid message or amaxoutside 1..64::invalid_signature_input→:invalid_messageor:invalid_options, checked before parsing.RequestSeal.Profile.preflight/4with anextraother than:allowor:reject, or invalid freshness values such as a negative skew: used as given (for example:unexpected_componentor:expired) →:invalid_policy.RequestSeal.Req.attach/2with averifymap whosemax_stream_bytesis nil::unsupported_delivery→:invalid_options, as for any other non-integer or negative value.RequestSeal.Plug.Verify.init/1with a Boolean:assign: accepted → raisesRequestSeal.Adapter.Errorwith:invalid_options.RequestSeal.JOSE.JWS.sign/4andRequestSeal.JOSE.JWE.encrypt/4with a header that is not a list of{name, value}pairs::limit→:invalid_header. More than 64 pairs stays:limit.RequestSeal.WebBotAuth.sign/4with an invalid caller-supplied:agentlocation, such as a non-HTTPS URL::invalid_signature_agentat:fields→:invalid_optionsat:input. An invalid existingSignature-Agentfield keeps:invalid_signature_agent.- Verify and sign entry points are total: an unexpected internal fault returns the new closed reason
:internalat the new layer:internal, with the exception class (the raised exception's module, or:exit/:throw) indetail; previously such a fault escapedRequestSeal.verify/3,verify_quorum/3, Web Bot Auth verification, orsign/4as 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:internalinstead of masking as input reasons; the[:request_seal, :verify, :stop]telemetry event makes them observable.RequestSeal.Error'sretryablefield widens from the literalfalsetoboolean()(:deadlineis 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_failedbecomes retryable (possibly-transient resolver trouble).RequestSeal.Discovery.Error's retryable set gains:resolution_failed. - JWKS keys may assert
algwith 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 inRequestSeal.Discovery.KeySet.lookup/3and directory signature verification — includingPS256andPS384, which exist only as JOSE names. Analgoutside both registries still rejects the whole set:invalid_key_set. RequestSeal.JOSE.Nested.verify/4accepts an inner JWS carrying a non-JOSEcty(for exampleapplication/json); only values naming another JOSE envelope reject:nesting_depth—application/jose,application/jose+json, the per-structureapplication/jwsandapplication/jwe, and RFC 7519'sapplication/jwt(abbreviatedJWT), case-insensitively, with RFC 7515 Section 4.1.10's abbreviation rule and media-type parameters ignored for the classification. Previously any innerctyrejected.- 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 uncaughtMatchErrorwhen 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.Discoverycache freshness now honors RFC 9111's qualifiedno-cacheandno-storespellings, such asno-cache="content-type"and the RFC 7234#field-nameform 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 tomax_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_uriis now checked against the origin that actually served the document instead of the originally configured origin. Underredirect_scope: :any_httpswith 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.JOSEresolver entries without an:algorithmkey now reject with:key_resolver_failed, as documented;:algorithm_mismatchstays reserved for a present, different algorithm. Oversized caller-supplied JOSE header strings (over 16,384 bytes per value or key) reject with:limitbefore serialization allocates, instead of building the full JSON first.RequestSeal.Plug's request-local state reader tolerates a foreignconn.private[:request_seal]value by treating it as absent (no capture), instead of raising insidePlug.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/5reports the underlying signing failure's bounded reason in the error'sdetailwhen it returns:negotiation_unfulfillable.RequestSeal.AshHooks.Http.request/5returns{:error, {:terminal, :request_seal_signing_failed}}for a malformed options container instead of raising out of the ash_hooks callback.corpus/index.json'selixir_releasenow follows the package version, and the declared gate fails whenever it differs fromvplus themix.exsversion. 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, clearingmix hex.auditadvisories 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/3rejects an invalid message with{:error, %RequestSeal.Digest.Error{reason: :invalid_message}}; it previously surfaced the same reason as aRequestSeal.StructuredFields.Errorthrough the field parser.check/3is unchanged.RequestSeal.Custody'sidentity/1callback 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/4now states that its explicit form preserves caller metadata beyond whatverify/3accepts — anonceabove 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/3documents that a quorum rejection is returned beforeAccept-Signaturenegotiation is evaluated. The Req adapter documents that cross-origin credential stripping is monotonic across the redirect chain.Message.request/5documents that a generatedcontent-digestoccurrence 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 qualifiedno-cachespellings 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, theRequestSeal.Finchfunctions,RequestSeal.JOSE.JWS.sign/4,RequestSeal.JOSE.KeyManagement.wrap/4andunwrap/4, andRequestSeal.JOSE.Nested.verify/4had no documentation before. Replay store callbacks are documented on each adapter.RequestSeal.Ash.Scope's type now admitsreplay: :claimed, whichRequestSeal.Ash.scope/2already 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
Bodyand Plug capture, the spec-form nonce option, Web Bot Authmax_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/4andverify/5acceptmax_bytes:(1–16,777,216, default 1,048,576). The default and the existing:invalid_datarejection are unchanged. Out-of-range or non-integer values reject with:invalid_optionsbefore handle, capability, or algorithm validation.unwrap/3does not accept it.Crypto.max_bytes_ceiling/0exposes the shared ceiling.- Direct
Custody.Local.sign/4,Local.verify/5,Custody.SSHAgent.sign/4, andSSHAgent.verify/5callback calls (outsideRequestSeal.Custody) accept up to the 16,777,216-byte ceiling;Localpreviously 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_mismatchor aRequestSeal.Crypto.Errorstruct first. The publicRequestSeal.CustodyandRequestSeal.Cryptofunctions are unchanged for callers that pass nomax_bytes:. - Third-party custodians that sign with
Crypto.sign/4or verify withCrypto.verify/5must passmax_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/5andMessage.response/4(1 MiB body default), message field totals, and compact JWS keep their 1 MiB limits;Body.new/1andRequestSeal.Plug.Capturekeep their caller-selected bounds. This release does not propagate the new custody/crypto opt-in to those APIs.RequestSeal.SignatureBaseretains its separate 1 MiB ceiling.
0.4.0 - 2026-10-10
- Accept an algorithm-bound
RequestSeal.KeyHandlein the explicitRequestSeal.sign/4form.signing_timeoutaccepts 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.Errorwith reason:signing_failed, layer:crypto, and a boundedRequestSeal.Custody.Errorinsource. For the existing signing-spec path with aKeyHandle, the reason was:signer_failed, now:signing_failedwith the custody error assource. Core errors addmessageandsourcefields, both nil for existing non-custody failures. Malformed signing output uses custody:invalid_signing_output; malformed custody errors use:custodian_failureon both HTTP signing paths. - Add caller-supervised
RequestSeal.Custody.Local.Ownerfor 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_unavailablefrom 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_Storeonly for empty regular files or files starting with Finder metadata magic; reject other contents and directories. Refuse release packaging if any.DS_Storeexists 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_exceededat:replaybefore commitment or storage. Bound policymax_ageat 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_untilabove 253,402,300,799 are never swept bysweep/3. Operators can remove them manually (replacereplay_claimswith 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/2as 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/2return contract: bare:okreturns{:ok, :ok}; other bare returns become:custodian_failure; non-Context arguments return:invalid_optionsinstead of raising; context-owner death returns a nonretryable:custodian_failureerror 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?/1for parsed RFC 9421 Signature-Input Inner List semantics, delegating to the shared validator. - Document
RequestSeal.Replay.Claim.valid?/1without 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.AshOnetimefor independently committed durable nonce claims, deadline propagation, and ash_onetime-managed retention and cleanup. It is the recommended durable store;Replay.Postgresremains available.Added
RequestSeal.AshHooks.Httpfor final-byte RFC 9421 signing through ash_hooks' bounded transport, andRequestSeal.AshHooks.verify_signature/4for Provider delegation. Added executable webhook and durable replay examples.Optional ash_onetime
~> 1.5and ash_hooks~> 2.0run 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
:nonceoption; selected nonces regenerate per attempt or invocation.Spec signing preserves
:unsupported_componentand:invalid_requesterrors and bounds callback faults without hiding internal exceptions.Core spec signing defaults metadata and requires a positive integer expiry; negative clocks reject.
sign/4documents 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/4for caller-serialized protected JSON bytes with the same bounds and header rejections asHeader.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.Verificationinspection no longer showsprincipalfor 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-cookiecoverage 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-cookiefields 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@querywith:unsupported_componentwhen 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/3intentionally fails closed with:duplicate_labelfor 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
:identitymember carrying trustedRequestSeal.KeyIdentityfor 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
signaturesmap for assigned signatures;satisfiedincludes 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/3andcheck_stream/3reject repeated supported checksum algorithms with:conflicting_digest, including identical checksums.Streaming
init/3accepts an explicitmax_bytesabove 16 MiB; retained-bodycompute/3andcheck/3keep that hard limit.Digest
preferences/3rejects sections other than:headersand: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.