| Internet-Draft | OASNT-ENFORCE | August 2026 |
| Thallapelly | Expires 16 February 2027 | [Page] |
This document profiles the enforcement of OASNT tokens at the point of
execution. It defines the OASNT-Token HTTP field, the rules by which an
enforcement point derives the observed request from the octets it will
itself forward, a verification procedure for relying parties that hold no
request-to-action mapping, uniform refusal behavior, and the set of
refusals a conforming enforcement point is required to produce. It further
defines an optional grp claim and an exclusivity ledger, by which a set
of tokens issued from one human confirmation is made spendable only once
between them, and fixes which relying party in a deployment performs that
consumption. An enforcement point conforming to this profile makes a human
approval a precondition of execution for the requests it fronts, without
any change to the protected service.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 16 February 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.¶
[I-D.thallapelly-oasnt] (hereafter "the core document") defines a token
in which a hardware-bound device key attests that a specific human
authorized one specific action, optionally bound to one concrete HTTP
request through the rqf claim. The core document also states where such
a token counts: at the party that verifies it against the request it is
about to perform.¶
This document specifies that party. Without it, the token gates only a cooperating caller: an agent may obtain a token for one request and issue another, or issue a request with no token at all, and nothing positioned at the transport observes the difference. An enforcement point conforming to this profile closes that gap for every request it fronts, and does so without modifying the protected service, which continues to receive ordinary HTTP.¶
The profile deliberately specifies behavior, not placement. A conforming enforcement point is typically a reverse proxy in front of an unmodified origin, but the same rules apply to an in-process interceptor inside the service itself.¶
This profile is written from a running implementation, whose adversarial corpus exercises every refusal Section 9 lists.¶
This revision adds Section 6, which specifies exclusivity groups: the
optional grp claim, the ledger that enforces it, and the rule fixing
which relying party consumes. That material is confined to what runs in
the implementation described in Appendix A, and Appendix A states
plainly which half of the deployment that is.¶
Three adjacent problems are deliberately not specified here, and an implementer should not read Section 6 as covering them. They are named in Section 6.7 so that their absence is visible rather than inferred.¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
"The core document" refers to [I-D.thallapelly-oasnt]. The claim names
adg, dsp, rqf, and jti, and the terms "canonical action" and
"request fingerprint", have the meanings the core document gives them.¶
Enforcement point: the party that verifies a token against the request it is itself about to perform or forward.¶
Approval point: a relying party that verifies a token in order to record whether a confirmation was valid, and that will not itself execute or forward the authorized request.¶
Presenter: the party transmitting the request and token to the enforcement point. The presenter is untrusted.¶
Observed request: the request as the enforcement point will actually execute it, derived under Section 4. Never anything the presenter asserts about the request.¶
Upstream: the protected service the enforcement point fronts.¶
Issuing party: the party that decides a set of actions will be offered
under one human confirmation, and that assigns the grp value binding
them (Section 6.1).¶
The token travels in a dedicated HTTP field:¶
OASNT-Token = b64part "." b64part "." b64part b64part = 1*base64url-char base64url-char = ALPHA / DIGIT / "-" / "_"¶
The value is the compact JWS serialization the core document defines, unmodified; the segment alphabet is the URL-safe alphabet of [RFC4648] without padding.¶
A dedicated field is used rather than the Authorization field of [RFC9110] because the upstream commonly consumes Authorization for its own credential, and this profile's promise is that the upstream changes nothing. The enforcement point removes the OASNT-Token field before forwarding (Section 8), so the two never collide.¶
A request MUST carry at most one OASNT-Token field with a single value. A
request carrying more than one, or a value that does not match the syntax
above, MUST be refused. A request carrying none MUST be refused
(no-token); there is no anonymous path through an enforcement point.¶
The observed request is derived exclusively from what the enforcement point will itself execute:¶
Method and target: the method and request target the enforcement point will use upstream, as octets. These are the same octets it received, but their authority comes from the enforcement point's intent to forward them, not from the presenter having sent them.¶
Tenant and privilege: the orgId and scope inputs to the request
fingerprint are not inferable from HTTP. Configuration MUST bind them to
request targets. A target no configuration entry names MUST be refused
(no-route): a target nobody declared is not an open path.
Misconfiguration is fail-closed by construction, because a wrong tenant
or privilege recomputes to a different fingerprint and refuses as a
mismatch.¶
Body: the enforcement point MUST buffer the body it will forward, up to a configured cap, and compute the body digest over exactly those raw octets. A body exceeding the cap MUST be refused and MUST NOT be forwarded unverified. There is no compliant configuration in which oversized requests bypass verification.¶
The request fingerprint is then recomputed from these observed values as the core document specifies. Nothing the presenter declares participates in the derivation at any point.¶
An enforcement point MUST apply the core document's verification
procedure to the presented token, with the observed request of
Section 4 as the expected request. Because an enforcement point
always authorizes a concrete request, the core document's request-binding
rule applies in full: a token without rqf MUST be refused, and absence
is never a downgrade.¶
The core verification procedure recomputes the action and display digests from the relying party's own representation of the action. An enforcement point positioned at the transport typically holds no request-to-action mapping and cannot form that representation. This profile therefore defines the following modification, and only this modification:¶
In place of the action-binding and intent-binding recomputation steps, an
enforcement point that holds no request-to-action mapping MUST require
adg and dsp to be present as non-empty strings covered by the
verified signature. Every other step of the core procedure applies
unchanged.¶
The result is transport-bound verification: the token is proven to originate from the enrolled key, to be fresh, unconsumed, and bound to exactly the observed request, while the action and display digests are carried at signature strength rather than recomputed. A deployment that requires action recomputation places it at a party that holds the mapping, such as an executor-side processing model in the style of [I-D.schrock-action-evidence-boundary] performing identifier matching under [I-D.thallapelly-oasnt-caid]; matching there restores recomputation strength to the carried digests. An enforcement point MUST NOT represent transport-bound verification as full verification.¶
The consumed-jti set is scoped to the enforcement deployment that
maintains it, consistent with the core document's scoping of nonce
consumption. Coordinating consumption across deployments or trust domains
is out of scope here, as it is there.¶
The core document makes a token single-use through jti, and this profile
binds it to one request through rqf. Neither constrains two different
tokens. A deployment that offers a human a choice with a fallback, of the
form "transfer by ACH; if ACH is unavailable, by card", issues more than
one token from a single confirmation: each has its own action, its own
request binding, and its own nonce. Every one of them verifies. Nothing
specified so far prevents a presenter from executing two.¶
An exclusivity group is the mechanism that does. Tokens the issuing
party intends as mutually exclusive carry a common grp value, and a
relying party maintains a ledger recording which groups have been spent.
The first token of a group to be accepted for execution spends the group;
every later presentation of any token in that group is refused.¶
This section specifies the claim, the ledger, and the rule that fixes which relying party spends. It does not specify how the issuing party composes what the human sees, nor how a relying party could prove that an executed action was among those confirmed; see Section 6.7.¶
grp claim
| Claim | Type | Presence | Binds |
|---|---|---|---|
grp
|
string | OPTIONAL | the set of tokens among which at most one may execute |
A token that belongs to no group omits grp, and omitting it has precise
consequences given in Section 6.5. When the claim is present it MUST be
a string of non-zero length; a relying party that finds it present and of
any other type or empty MUST refuse (grp-malformed) rather than proceed
as though the token were ungrouped. Treating a malformed value as absent
would convert a corrupt grouped token into an unconstrained one, which
inverts the direction every other check in this profile fails in.¶
A grp value carries no information a relying party is entitled to read.
Its only two uses are equality comparison and construction of a ledger key
(Section 6.4). A relying party MUST NOT parse it, enumerate over it,
infer the size or membership of a group from it, or treat two values as
related on any basis short of octet equality.¶
The issuing party is the only party that assigns a value, and it MUST generate one that the presenter can neither predict nor derive. Two properties are required and they are separate. The value MUST be unpredictable, drawn from a cryptographically secure generator; the running implementation draws sixteen random bytes per group. And it MUST NOT be computed from the contents of the confirmation it groups: a value derived from the actions, their number, or their order would publish the shape of the confirmation to the presenter, and the presenter is the party the group exists to constrain.¶
Because the value reaches a relying party only inside a signed token, an attacker cannot mint a group of its own. What unpredictability buys is narrower and worth stating exactly: it stops a presenter that has seen one group's value from recognising, or constructing a key for, any other.¶
A relying party verifying a token that carries grp acts in exactly one
of two modes:¶
consume: the relying party is authorizing execution. It maintains a group ledger, consults it, and spends the group on full acceptance (Section 6.3).¶
verify-without-consuming: the relying party is an approval point or an auditor. It checks the group constraints that do not require spending, and MUST NOT write to any ledger.¶
The mode is a property of the deployment's configuration, not of the token. A relying party MUST fix its mode independently of anything the presenter transmits. It MUST NOT derive the mode from any claim, header, or parameter carried with the request, and MUST NOT fall back from one mode to the other on any error.¶
A relying party MUST NOT express verify-without-consuming by omitting the ledger. Omitting the ledger is itself a refusal (Section 6.6), and a deployment in which "no ledger" and "do not consume" are the same configuration cannot distinguish an auditor from a broken enforcement point. The mode MUST be stated.¶
A relying party that verifies a grp-bearing token without a stated mode
MUST refuse (group-mode-missing). There is deliberately no default:
defaulting to consume would silently destroy the fallback the group exists
to offer, and defaulting to verify-without-consuming would silently
destroy exclusivity. Both readings are unsafe, so neither is taken.¶
The enforcement point consumes. The approval point does not.¶
A deployment MUST designate exactly one consuming relying party per group. Every other party that inspects a token of that group in transit, for any purpose other than executing or forwarding the authorized request, MUST operate in verify-without-consuming.¶
The reason is structural rather than a matter of preference. A choice and
its fallbacks share one grp by construction, and a confirmation is
recorded only once every token of the ceremony has verified. An approval
point that consumed would spend the group while verifying the first token,
so every sibling would then refuse as already consumed and the
confirmation could never complete. Even were it to complete, the group
would be spent before any request executed, and the fallback could never
be taken. Consuming at the approval point does not weaken exclusivity; it
removes the capability the claim exists to provide.¶
What is spent is the pair (sub, grp), never grp on its own. The core
document's procedure checks sub against the enrollment record before
this point is reached, so the subject in the pair is one an enrolled key
has vouched for. Keying on the pair therefore confines a group to the
subject it was issued to: naming another subject's group spends nothing,
and naming a subject one is not enrolled as fails earlier.¶
Spending is the last thing a relying party does, not the first. It MUST NOT record a group as spent until every other check on the token has succeeded, and a token that is refused for any reason MUST leave its group exactly as it found it. The cause of the refusal is irrelevant to this rule: an expired token, a body that does not match the fingerprint, and a malformed group all leave the group unspent alike.¶
Getting this backwards is not a small ordering defect. If a refusal spent
the group, a presenter would need no valid token at all to suppress a
genuine one; it could submit anything bearing the right grp and the
fallback would be dead before the human's real choice was ever exercised.
The rule converts that from an attack into a no-op.¶
A relying party that reserves state before verifying, rather than after, MUST still produce this outcome. Two obligations follow for such an implementation, and the running one meets both. It MUST release a group reservation whenever the verification it was taken for does not end in acceptance. And it MUST NOT take the group reservation at all once the token's nonce is already known to be spent, because that request cannot execute, and reserving a group for a request that cannot execute is the same denial of service arriving by a different route.¶
A group ledger answers one question, whether this (sub, grp) pair has
been spent, and accepts one instruction, to mark it spent. A relying party in
consume mode requires both operations; one in verify-without-consuming
requires only the first.¶
A ledger key MUST be constructed so that no pair can be confused with a different pair. Where a key is formed by joining the subject and the group value, both components MUST be encoded such that a value containing the join delimiter cannot shift the boundary between them. Omitting that encoding lets a crafted subject or group value produce the key of a different pair, which is a way to spend a group that was never issued to you, or to appear unspent when you are not.¶
The failure this most often meets in practice is not a restart. It is two enforcement points behind one load balancer: nothing is misconfigured, both are healthy, and a process-local ledger at each means the first instance's record is simply invisible to the second. A token executes at one, its sibling arrives at the other, and both are accepted. Every request is individually valid, exclusivity is absent throughout, and no error is raised anywhere to say so.¶
A deployment therefore MUST ensure that every token capable of carrying a
given grp is verified against the same ledger. Two relying parties that
do not share one provide no exclusivity between them, and a deployment
that spreads a group across them has none regardless of how correctly each
behaves in isolation. Restarts are the same defect on a longer timescale:
a ledger that does not survive the process forgets every group still
inside its tokens' lifetimes.¶
An entry MUST remain until no token carrying that grp can still be
accepted anywhere the ledger serves. That bound is set by the slowest
clock among the relying parties sharing it, not by the issuing party's
clock: a verifier running behind will still accept a token some interval
past its expiry, and the entry has to outlast that acceptance or the
sibling becomes spendable again exactly when the record disappears. A
deployment MUST derive retention from the token's expiry extended by the
clock skew it tolerates.¶
The bound also runs the other way, and the direction that fails closed is
worth naming. Where a deployment caps how long it will retain a record, a
token whose expiry exceeds that cap MUST be refused
(exp-beyond-replay-retention, Section 9). It MUST NOT be
accepted against a record that will lapse first, and the cap MUST NOT be
silently applied to shorten the record instead: a record trimmed to fit a
cap expires while its token is still live, which is precisely the replay
the record existed to stop.¶
This one refusal is not confined to grouped tokens. The retention bound is
a property of the replay record, which every token has, so the refusal
fires whether or not grp is present. It is stated here because this is
where retention is specified, and listed in Section 9 rather
than Section 6.6 because that is where its scope actually places
it, a distinction Section 6.5 depends on being exact.¶
grp
A token carrying no grp claim MUST be verified exactly as a
deployment that does not implement this section would verify it. No
group ledger is consulted or written, no mode is required or read, and no
refusal of Section 6.6.1 can be produced.¶
One refusal named in Section 6.6 sits outside that guarantee, and
it is named here rather than left to be discovered.
group-mode-not-caller-selectable (Section 6.6.2) can be
returned against a token carrying no grp at all. It does not contradict
the sentence above, because it is not a verification outcome for the
token: the situation it is defined over is a caller supplying a mode to an
enforcement point, which is a property of the call's arguments and not of
the presented token's claims. Nothing in that row is conditioned on grp,
so the claim's absence does not withhold it. A deployment whose caller
threads a mode argument alongside a grp-less token is refused under that
label, and a reader who took the guarantee above to mean that no label
from Section 6.6 can ever accompany a grp-less token would be
wrong. The guarantee is exact over the five token-conditioned causes,
which are the causes a token can reach.¶
This guarantee is normative in both directions and is what makes the
mechanism safe to deploy incrementally. A relying party may adopt group
handling without re-qualifying every single-token caller in front of it,
and a grp-bearing token that reaches a relying party unaware of this
section fails closed, because such a party consults no ledger and this
section's refusals are unavailable to it, so the token is either refused
by the core procedure or accepted with exclusivity unenforced. A
deployment MUST NOT rely on group exclusivity across a relying party that
does not implement this section.¶
A conforming relying party MUST refuse in each of the situations named in this section. There are six, and they are of two kinds, tabulated separately because they answer different questions: five are reached because of what the presented token carries, and one is reached because of what the caller passed. Only the first kind is a statement about a token, and conflating them is what would make Section 6.5 approximate. As everywhere in this profile, the labels are descriptive and for the operator alone; nothing on the wire distinguishes them (Section 7).¶
Each of these is reached only because the presented token carries grp.
Every one of them requires the claim to be present, which is what makes
the guarantee of Section 6.5 exact rather than approximate.¶
| Situation | Label |
|---|---|
grp present but not a non-empty string |
grp-malformed
|
grp present and no group ledger available to this relying party |
group-ledger-missing
|
grp present and no group mode stated, or a mode outside the two of Section 6.2
|
group-mode-missing
|
grp present and the available ledger cannot be consulted, or cannot be written by a relying party in consume mode |
group-ledger-unusable
|
| a token of this group has already been consumed under this subject |
group-consumed
|
The order of these checks is normative where it is observable: a relying party MUST determine that a ledger is available before it evaluates the stated mode. Stating a mode MUST NOT substitute for holding a ledger, because "I could not check the group" is not answered by declaring what would have been done if it could.¶
The retention-cap refusal discussed in Section 6.4 is deliberately not listed here: it applies to any token, grouped or not, and so belongs with Section 9, where it is listed.¶
The sixth refusal is a requirement on an enforcement point's own interface rather than on the token, and it is tabulated apart from the five above for exactly that reason.¶
| Situation | Label |
|---|---|
| a mode supplied by the caller at an enforcement point |
group-mode-not-caller-selectable
|
Because an enforcement point consumes unconditionally (Section 6.3), its mode is not a parameter, and a caller that supplies one has misunderstood the deployment. Such a call MUST be refused rather than served with the supplied value ignored: a silently ignored parameter leaves the caller believing a behavior is available that is not.¶
The situation is stated over the call, not over the token, and this row
therefore does not require grp to be present. A caller that supplies
a mode is refused whatever token accompanies it, including one carrying no
grp at all; Section 6.5 states that consequence. The row binds an
enforcement point only. A relying party that does not consume
unconditionally, such as an approval point verifying without consuming
(Section 6.2), is told its mode by its caller in the ordinary way,
and this refusal does not reach it.¶
Three problems adjacent to Section 6 are deliberately out of scope of this revision, as Section 1.1 states. Each is named here so that an implementer sees the boundary rather than inferring coverage that does not exist.¶
Composed multi-action disclosure. How an issuing party renders several
actions into one artifact for a single human confirmation, and what dsp
is then computed over, is not specified here. This revision makes no
statement about dsp beyond the core document and Section 5.1.¶
Containment. Where the disclosure covers several actions, a relying party accepting a token cannot, from anything in this revision, establish that the action it is about to execute was among those confirmed. This profile does not define a mechanism for that proof, and an enforcement point conforming to this revision MUST NOT represent its acceptance of a grouped token as evidence that the executed action was one the human confirmed. Deployments requiring that property MUST establish it at a party holding the mapping between the confirmation and its actions.¶
Device carriage. The device-side production of grp is not specified
here. This revision places no requirement on any device, SDK, or minting
runtime. Every requirement above falls on a relying party, with one
exception that is deliberately not a device requirement: Section 6.1
constrains how the issuing party generates a grp value. The issuing
party is whichever party decides that a set of actions will be offered
under one confirmation. In the running deployment that is a server-side
broker, never the device, which receives the value already formed and
carries it verbatim. See Appendix A for what this means about the implementation
this revision was written from.¶
On the wire, every refusal MUST be indistinguishable from every other: the same status code and an identical body, carrying no indication of which check failed. The distinct refusal cause MUST be available to the operator, and MUST NOT be available to the presenter. Refusal causes reveal enrollment state and nonce state, which is reconnaissance an untrusted presenter is not owed.¶
This applies to the causes of Section 6.6 without exception. Group
state is enrollment-adjacent: disclosing that a token was refused as
group-consumed, rather than for any other cause, would tell a presenter
that a sibling of its token had already executed.¶
A failure of the upstream after an allow decision is an availability outcome, not an authorization outcome. It MUST be distinguishable from a refusal (for example, 502 rather than 403) and MUST NOT be folded into the uniform refusal, because masking availability as refusal corrupts the operator's signal in both directions.¶
Only a request whose token passed verification in full is forwarded, and what is forwarded is exactly the observed octets the fingerprint was computed over: same method, same target, same body. The OASNT-Token field MUST be removed before forwarding; every other field is passed through unmodified.¶
Because the fingerprinted octets and the forwarded octets are the same buffer, there is no window at this hop between what was verified and what executes. What the upstream does beyond those octets, such as dereferencing an identifier the body names, is outside the binding and belongs to the action layer.¶
A conforming enforcement point MUST refuse, without forwarding, in each of the following situations. The labels are descriptive, for operator logs; nothing on the wire distinguishes them (Section 7).¶
| Situation | Label |
|---|---|
| body altered relative to the fingerprinted request |
rqf-mismatch
|
| target altered relative to the fingerprinted request |
rqf-mismatch
|
| token already consumed by a previously executed request |
replay
|
| no token presented |
no-token
|
| tenant or privilege configuration disagrees with the binding |
rqf-mismatch
|
| body exceeding the configured cap |
body-too-large
|
| target named by no configuration entry |
no-route
|
| more than one OASNT-Token field, or a syntactically invalid one |
no-token
|
The reference implementation exercises each of these against a live enforcement point over HTTP, together with a check that all refusal responses are octet-identical on the wire; see Appendix A.¶
One further refusal belongs to this table by subject but not to the coverage claim just made, and is therefore listed apart from it:¶
| Situation | Label |
|---|---|
| a token whose expiry runs past the deployment's replay-retention bound (Section 6.4) |
exp-beyond-replay-retention
|
This refusal is a property of the replay record rather than of the group:
it applies to every token an enforcement point admits, whether or not
grp is present, which is why it does not appear in Section 6.6.
A deployment MUST NOT satisfy it by shortening the retention entry to fit
its bound; the token is refused instead. Its implementation coverage is
stated in Appendix A and is not the live-HTTP coverage described
above.¶
The refusals of Section 6.6 are additionally required of a conforming relying party, and are listed separately because their implementation coverage differs; Appendix A states how each is exercised and does not claim the coverage described in the paragraph above for all of them.¶
An enforcement point protects exactly the requests that pass through it. A service reachable around it is not protected, and no property of this profile survives such a path. Deployments MUST ensure the upstream accepts requests only from its enforcement point, by network isolation, mutual authentication, or equivalent means. With that condition in place, skipping the authorization ceremony is not a bypass; it is the refused path.¶
Section 5.1 trades action recomputation for deployability at the
transport. The carried adg and dsp are exactly as strong as the
signature over them: they prove what the device signed, not what the
upstream will do. The composition intended by this profile is that
recomputation happens where the mapping lives, at the approval broker
that derives the display from the request, and at the executor that
matches identifiers per [I-D.thallapelly-oasnt-caid]. An enforcement point is one layer
of that composition, not the whole of it.¶
This is the attack Section 6 exists to defeat. Both tokens are
individually valid: each carries its own action digest, its own request
fingerprint, and its own nonce, and nothing in the core document refuses
the second. Only the ledger does, on (sub, grp), at the single consuming
party Section 6.3 designates.¶
Two residual conditions are load-bearing and are stated rather than assumed. Exclusivity holds only within one ledger (Section 6.4), so a deployment that spreads a group across enforcement points not sharing one has no exclusivity at all. And it holds only if the consuming party actually reaches its ledger, which is why an unusable ledger is a refusal rather than a pass.¶
Placing the spend at an approval point does not make a deployment more restrictive than this profile requires. It makes the deployment stop working, in a way that resists diagnosis.¶
Consider what an operator observes. Every token is genuine and every signature verifies. The logs fill with a refusal cause that is defined, documented, and correct for what the code did. No component reports an error, because none has encountered one. What has actually happened is that the first sibling verified at the approval point spent the group, so its siblings were refused as already spent, so the confirmation never reached an approved state and the human's fallback was consumed before any request was ever authorized. Every individual step behaved as specified.¶
The reason this belongs in security considerations rather than in an operations note is the shape of the failure, not its severity. It is reached by leaving something out rather than by configuring something wrong, and it presents as correct behaviour. Requiring the mode to be declared (Section 6.2) is what removes the omission path: a relying party that has not said which mode it is in does not silently pick one.¶
The three controls answer three different questions, and a deployment that reads any of them as covering another has a gap it cannot see. The nonce answers whether this token has been used before. The request fingerprint answers whether this token is being used against the request it was bound to. The group answers whether a different token, issued alongside this one, has already been spent.¶
Nothing about the presence of a group weakens the first two. A grouped token is still single-use in its own right and still bound to one request, and a deployment MUST NOT relax nonce handling or fingerprint checking on the grounds that a ledger is present. The converse is the more tempting error and it is equally wrong: a correct nonce check says nothing at all about a sibling, because a sibling is a different token with a different nonce.¶
In a correct implementation the nonce check also comes first in time, and that ordering carries a consequence worth stating: a request already doomed by its nonce MUST NOT reach the point of spending a group (Section 6.3).¶
Section 7 applies to group causes in full. Beyond the reconnaissance
argument that applies to every cause, group-consumed is a positive
statement that a sibling token has already executed, which is
transaction-level information about another party's activity.¶
The core document's limits pass through unchanged. A compromised device may still assert a clean integrity verdict. A valid display digest proves which octets were shown, not that they were understood. Nothing in this profile strengthens either claim.¶
Where a single confirmation authorizes several actions, this profile makes no statement at all about whether the human distinguished them; it constrains only how many of the resulting tokens may be spent. Containment is not specified here (Section 6.7), so a deployment MUST NOT infer from a grouped token's acceptance that the executed action was among those confirmed.¶
Refusing oversized bodies is the fail-closed arm of a real trade: it makes the enforcement point a hard dependency for large uploads. The alternative, forwarding unverified above a threshold, silently exempts the largest requests from the strongest control, which is the wrong shape for a security boundary. Deployments with large-body endpoints SHOULD raise the cap for those targets explicitly rather than exempt them.¶
Refusal causes are operator-only (Section 7) precisely because they reveal enrollment and nonce state. The log that carries them SHOULD be protected as security telemetry, and log entries need carry no token material beyond the cause label.¶
IANA is requested to register the following entry in the "Hypertext Transfer Protocol (HTTP) Field Name Registry" defined by [RFC9110]:¶
IANA is requested to register the following in the "JSON Web Token Claims" registry established by [RFC7519]:¶
| Claim Name | Description | Change Controller | Reference |
|---|---|---|---|
grp
|
Exclusivity group | IETF | Section 6.1 of this document |
This name does not appear in the registry at the time of writing.¶
This section records the implementation this profile was written from; it is not a conformance statement. It distinguishes what runs from what does not, because the coverage is not uniform across this document and a reader who assumed it was would be misled.¶
A reverse-proxy enforcement point and its verification core exist and run in front of an unmodified HTTP origin. The verification core delegates every cryptographic decision to the same verifier the core document's test vectors were generated from. An adversarial corpus drives a live enforcement point over real HTTP through every situation Section 9 lists, asserting each is refused, that nothing refused reaches the upstream, and that all refusal responses are octet-identical on the wire. The corpus and the list are cross-checked mechanically, so a refusal cannot be documented without being exercised or exercised without being documented.¶
The relying-party half of Section 6 runs, split across two layers, and
the split matters to anyone checking the claim. A shared verification
routine recognizes grp as an optional claim, keys its ledger on
(sub, grp), requires a stated mode, and spends a group only under
reserve-and-commit; it produces the malformed-value, absent-ledger,
absent-mode, unusable-ledger and already-consumed causes, which are
exactly Section 6.6.1. The remaining cause
(Section 6.6.2), raised when a caller supplies a mode, is
not produced by that routine and could not be: it is enforced one
layer out, at the enforcement point's own entry, which is the only place a
caller-supplied argument exists to be rejected. That placement is also why
it is not conditioned on grp: it is decided before the presented token
is examined, and it is returned for a call carrying no token at all. Two relying
parties drive the routine in production configuration: an approval broker
in verify-without-consuming mode, and the enforcement point in consume
mode, where the mode is fixed in the enforcement point itself and a
supplied mode is refused rather than ignored. The ledger backing the
enforcement point is durable
and shared across instances; a multi-instance test asserts that a group
spent at one instance is refused at another behind the same load balancer,
and that test requires a durable store to be present in order to run.¶
Coverage is not uniform, and this document does not claim that it is.
The corpus rows and unit suites exercise every cause of
Section 6.6.1 against the verifier in process. Of those causes,
only group-consumed is additionally exercised against a live enforcement
point over real HTTP, in a multi-instance test that requires a durable
store to be present; the remainder are not, and none of the group causes
participates in the mechanical corpus cross-check described above for
Section 9. The sentence in Section 9 about live
HTTP coverage therefore applies to that table's first list and not to
Section 6.6.¶
group-mode-not-caller-selectable (Section 6.6.2) is
covered differently, as follows from where it is enforced: it cannot be
exercised against the verification routine at all, which is why the
sentence above is stated over Section 6.6.1 alone and not over
all six causes. It is exercised in process at the enforcement point's own
entry, by a unit test that passes each of the two defined modes and an
explicit undefined one, asserts the refusal in every case, and asserts
that a refused call spends neither a nonce nor a group. The equivalent
guard on the durable wrapper has its own test; that test requires a
durable store to be present, and in the run recorded for this document it
did not execute for want of one. Neither is a live-HTTP exercise.¶
exp-beyond-replay-retention is covered differently again, and is
recorded here rather than folded into any statement above. It is
produced by the replay store's reservation gate before verification, not
by the verification routine, and it is exercised by a unit test asserting
that a token outrunning the bound is refused and that nothing is written.
That test requires a durable store to be present; in the run recorded for
this document it did not execute for want of one. The same branch exists
in the in-memory store and was exercised directly, refusing a claims
object carrying no grp at all, which is the observation that places
this refusal outside Section 6.6.¶
Two adversarial rows carry the reserve-and-commit property specifically. A grouped token presented against the wrong request is refused on the fingerprint alone and is asserted not to have spent its group; a following row then verifies the sibling successfully, which is possible only if the refusal did not spend it, and confirms that the sibling's genuine use does spend it. Together they pin the rule that a refused token never burns its group.¶
No device produces grp. In the native core, the claim does not exist
in the claims structure or in the signing input, and no shipping Android
or iOS surface carries it. The only software that emits a token bearing
grp is a reference minter used to generate test and corpus material; it
is not the SDK and it establishes nothing about device behavior.¶
This is why Section 6 places no requirement on a device and why device
carriage is deferred in Section 6.7. Every normative statement in
Section 6 binds either a relying party or the issuing party, and both of
those are server-side in the implementation described here. The broker
that composes the confirmation generates each grp from a cryptographically
secure source, and the relying parties are the approval point and the
enforcement point. That is precisely the part that runs.¶
Composed multi-action disclosure and containment are likewise not implemented at the enforcement point and are not specified in this revision.¶
Added Section 6: the grp claim, group modes, the consumption rule
keyed on (sub, grp), ledger scope, the absent-grp guarantee, and six
named refusals. The six are tabulated as two kinds: the five causes of
Section 6.6.1, each of which requires grp to be present and
over which the absent-grp guarantee is stated, and the single cause of
Section 6.6.2, which is stated over an enforcement point's
own interface and does not require grp.¶
Added one refusal to Section 9, exp-beyond-replay-retention,
for a token whose expiry outruns the deployment's replay-retention bound.
It is listed there rather than with the group refusals because it applies
to every token, grouped or not.¶
Added Section 6.7, naming composed disclosure, containment and device carriage as out of scope of this revision.¶
Added a JSON Web Token Claims registration request for grp.¶
Added Security Considerations covering the choice-and-fallback attack,
consuming at the wrong party, the non-relaxation of jti and rqf, and
the operator-only treatment of group state.¶
Appendix A now separates transport-profile coverage from group coverage rather than describing one implementation status for the whole document.¶
No change to the OASNT-Token field, observation, transport-bound verification, forwarding, or Section 9.¶
The executor-side framing that this profile composes with, in particular the discussion of admissibility conditions on the WIMSE mailing list, sharpened the boundary between transport-bound and action-recomputed verification.¶