| Internet-Draft | Attenuation Chains Analysis | November 2026 |
| Davliatshin | Expires 6 May 2027 | [Page] |
This document reports an independent implementation and formal analysis of two individual Internet-Drafts that propose offline-verifiable, attenuating delegation chains for AI agents: Attenuating Authorization Tokens (AATs), in revisions -01 and -02, and the Agent Delegation Chain of draft-asor-wimse-agent-delegation-chain-02. It describes a minimal verifier written from each draft's text, with profiles for AAT -01, AAT -02 and asor-02, and a ProVerif model of the chain structures of AAT -01 and asor-02. The AAT -02 profile, written from the text and then run against the author's 229-vector test suite, agrees with it on all 229 vectors. Of 25 observations recorded on AAT -01, the -02 text resolves 19 (16 closed, 3 closed as intended design); one is partly resolved, one no longer arises, and four remain open. The report lists ten points where -02 leaves an implementer more than one conforming behaviour. Two properties raised in public review are checked on both chain structures. The author of this report is not an author of either draft. The report specifies nothing; it follows the shape of RFC 7942.¶
This note is to be removed before publishing as an RFC.¶
Status information for this document may be found at https://datatracker.ietf.org/doc/draft-davliatshin-oauth-attenuation-chains-report/.¶
Discussion of this document takes place on the Web Authorization Protocol Working Group mailing list (mailto:oauth@ietf.org), which is archived at https://mailarchive.ietf.org/arch/browse/oauth/. Subscribe at https://www.ietf.org/mailman/listinfo/oauth/.¶
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 6 May 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.¶
Several individual Internet-Drafts propose chains of signed tokens in which each link can only narrow the authority of the one before it, so that a verifier can check an agent's authority offline. This report covers two of them, [AAT-01] with its revision [AAT-02], and [ASOR-02]. Other individual drafts propose related mechanisms (a delegation header over HTTP message signatures; an issuer-per-hop chain anchored in a trust registry); they are outside this report.¶
The work began against AAT-01. AAT-02 appeared on 26 October 2026, with changes that its Appendix F describes as breaking -01 implementations, and with a byte-exact test-vector suite and a procedure for comparing results. The verifier gained a separate profile for -02. This report covers both versions and says which of its observations on -01 the -02 text resolves (Section 6).¶
What was done:¶
A minimal TypeScript verifier for the chain checks of both drafts, written as an executable specification (see Section 4).¶
A separate verifier profile for AAT-02, written from the -02 text and then run against the author's 229-vector suite: 229 of 229 verdicts agree, with the expected -02 Section 8 step on every denial.¶
A ProVerif model [PROVERIF] of the shared chain structure (parent-hash linkage, depth, lifetime and capability monotonicity, holder binding and proof of possession), instantiated for both drafts (see Section 5).¶
This report.¶
What was not done: no minting or issuing service, no CBOR encoding, no revocation or status-list handling. The AAT-02 profile was run against the author's published vectors; it has not exchanged tokens with another running implementation.¶
Who did it: an individual, not an author of either draft. The shape and tone of this report follow [AGNIHOTRI-02]. The drafts are the object of study; the mechanisms described here are theirs.¶
Source code, model and vectors: https://github.com/azatdavliatshin/aat (verifier: https://github.com/azatdavliatshin/aat/tree/main/verifier; model: https://github.com/azatdavliatshin/aat/tree/main/model).¶
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.¶
This document uses the following terms, informally, across both drafts.¶
An ordered list of tokens, the first a root minted by a trust anchor or issuer and each later one a link attenuating its predecessor.¶
A token in a chain other than the root.¶
The party named by a token's confirmation claim ([RFC7800]) as able to present it; in AAT-01 and AAT-02 also the only party able to derive from it (I1).¶
The party whose key signs a token. In asor-02 this is the iss of an
access token ([RFC9068]); in AAT-01 and AAT-02 the root issuer signs
the root, and a derived token is signed by its parent's holder, whose
key thumbprint is its iss.¶
Producing a link whose authority (capabilities, depth, lifetime) is no greater than its parent's.¶
Proof of possession of the key a token is bound to, for example a DPoP proof [RFC9449].¶
"AAT-01" is [AAT-01]; "AAT-02" is [AAT-02]; "asor-02" is [ASOR-02].¶
Related documents used below: [RFC8785] (JSON Canonicalization Scheme:
asor-02 requires the JWS header and payload of every token to be its
output, Section 3, and compares a constraint it cannot evaluate by its
bytes, Section 4.3; AAT-01 requires it for the PoP JWT payload and for
comparing hta with the invocation arguments, Sections 5.2 and 7; -02
drops the payload requirement and compares constraint values and
arguments by it, -02 Section 3.4), [RFC9278] (JWK thumbprint URI, the
iss of a derived AAT; asor-02 binds the holder by a [RFC7638]
thumbprint in cnf instead) and [RFC9396] (rich authorization
requests; both drafts carry authority as authorization_details).¶
Finding labels A1 to A7 (asor-02), B1 to B25 (AAT-01), C1 to C10 (AAT-02) and M1, M2 (the two properties of Section 5.1) are this document's own numbering. They are not used by the drafts or by the reviews cited here.¶
[AAT-01] (Niyikiza) defines Attenuating Authorization Tokens: JWTs whose
authorization_details ([RFC9396]) list the tools a holder may invoke and
typed constraints on each argument. A root token is issued by a root issuer
and verified under a configured trust anchor key. Each later token is
derived offline by the holder of its parent: it is signed by the key the
parent names in cnf.jwk, its iss is that key's JWK thumbprint URI
([RFC9278]), and its par_hash is the SHA-256 hash of the parent's JWS
signing input (Section 4.6). The invariants I1 to I6 (Section 4) let a
derived token only narrow tools, argument constraints (by a per-type
subsumption relation, Section 4.5), delegation depth and lifetime. At each
tool invocation the leaf holder presents a proof-of-possession (PoP) JWT
over the tool and its arguments (Section 5); an enforcement point checks
the chain and the proof in one algorithm (Section 7).¶
[AAT-02] (26 October 2026) keeps this structure and makes normative
changes that, by its Appendix F, break -01 implementations. The JWS
algorithm is the fully-specified Ed25519 ([RFC9864]) in place of
EdDSA, and typ is required and explicit (aat+jwt for tokens,
aat-pop+jwt for the PoP JWT). The PoP JWT carries aat_hash, the hash of
the leaf's signing input. Trust anchors are (issuer, public key) pairs, and
the root's iss must equal the issuer of the anchor whose key verified it.
Parsing rules are strict: duplicate member names are rejected ([RFC8259]),
NumericDates and depths have no fraction or exponent part, crit is
processed, and numbers in arguments and constraints must be admissible
(-02 Section 3.4). Constraint maps gain optional arguments and a "*"
entry for arguments they do not name (-02 Section 3.3). The subsumption
relation is written as a table of valid (parent, derived) pairs, every
other pair invalid (-02 Section 4.5). MAX_DELEGATION_DEPTH is at least 8.
A presentation object with members aat_chain and aat_pop is defined
(-02 Section 7.4). The verification algorithm is -02 Section 8; this report
names its steps by number and letter (for example, step 3k). -02 publishes
a byte-exact JWS test-vector suite and lists implementations in its
Appendix E. Both versions are implemented; the model is of -01 (see
Section 5).¶
[ASOR-02] (Asor) defines the Agent Delegation Chain, a profile of OAuth
2.0 JWT access tokens ([RFC9068]). Each Delegation Token carries its
authority as authorization_details ([RFC9396]), the principal in sub
(the same value in every token of the chain), the acting agent in
client_id, a del_depth that grows by one per hop and is bounded by
del_max_depth, and, below the root, a par_hash: the SHA-256 hash of
the parent's JWS signing input. Tokens are verified under keys configured
at the verifier (Section 6, step 1). The leaf is bound to its holder by
cnf ([RFC7800]) and a DPoP proof ([RFC9449]). The offline
verification algorithm (Section 6) enforces monotonic attenuation, bounded
depth, monotonic expiry and the same principal throughout. Modelled and
implemented.¶
The verifier is a minimal TypeScript implementation, not a product. It
verifies a chain under one of three profiles, aat-01, aat-02 and
asor-02, with a shared chain walk and a profile that supplies the named,
section-tagged checks of the draft. Layout, inputs, failure codes and
options are documented in verifier/README.md
(https://github.com/azatdavliatshin/aat/blob/main/verifier/README.md).¶
The aat-02 profile is a separate set of files that follows -02 Section 8
step by step; every failure carries the step at which it occurs (for
example, 3a or 4p2). The aat-01 profile is kept unchanged next to it,
as the implementation the AAT-01 findings of Section 6 refer to.¶
It checks, for asor-02, the steps of its verification procedure up to the
action check; for AAT-01, the root, link and leaf checks of its verification
algorithm and the proof of possession; for AAT-02, every step of -02
Section 8, including the PoP JWT, with PoP jti tracking and nonces taken
from state the caller supplies. It does not implement:¶
asor-02: status lists (a token carrying a status claim is denied with
the distinct code not_implemented), holder binding for x5t#S256
(mTLS) confirmation and for ES256 proofs, or a DPoP jti replay cache;
cumulative running totals are taken from the caller.¶
AAT-01: keys other than Ed25519 (OKP), HS256 (never accepted), a PoP
jti replay cache, or extension constraint types (denied); and it parses
every token before verifying any signature, which is not the order of
Section 7.¶
AAT-02: algorithms other than Ed25519 (EdDSA is denied, -02
Section 9.11), cnf.jwk keys other than OKP Ed25519 (denied at steps 3l
and 4b2), any crit parameter (denied whenever present, since no
extension parameter is understood), or extension constraint types
(denied).¶
The complete list is in verifier/README.md.¶
Vectors:¶
asor-02: the 35 offline-verification vectors published with the
attenu-guard 0.20.0 source distribution, Apache License 2.0, copied
unchanged; all 35 produce the outcome they name.¶
AAT-01: 41 vectors of this work's own, written from its reading of -01, which publishes none. They are not the author's vectors.¶
AAT-02: the author's JWS test-vector suite, 229 vectors (54 permit,
175 deny), copied byte for byte from https://github.com/tenuo-ai/tenuo,
commit 99fb00994d8d66b213cd12967d6e8eef45025672, file
tests/vectors/aat-jws-vectors.json (SHA-256
b7a42e9dfdce7fbc1f623670bf060f90e6d6b25f64384cbab1bd79363e82209b),
Apache License 2.0. Result: 229 of 229 verdicts agree on the first run,
0 skipped, and the expected -02 Section 8 step on all 175 denials. No
check was adjusted to a vector. Results were compared with the suite's
own compare_results.py, following its INTEROP.md.¶
The model is a symbolic (Dolev-Yao) model in ProVerif's typed applied
pi-calculus. Its roles, abstractions, bound (chains of at most four tokens,
depth and expiry bounded by 3, tool sets over a three-tool alphabet) and
attacker (controls the network, may compromise any holder key, never the
trust anchor's key) are described in model/README.md
(https://github.com/azatdavliatshin/aat/blob/main/model/README.md).¶
For both profiles:¶
Every root of a chain that passes the link checks was minted by the anchor.¶
Every link whose parent names an honest holder key was derived from that parent by that holder, unless the key was compromised. For asor-02, the variant P1b' states the same with "some uncompromised configured key" in place of the parent's holder (see A7).¶
The invoked tool was granted at the root.¶
Leaf depth, maximum depth and expiry are bounded by the root's.¶
A leaf naming an honest key is used only by that key's holder (non-injective).¶
Parent validity after derivation: a parent is still accepted after its holder has derived a child from it, with no compromise at all.¶
Intermediate links carry no proof of possession of their own. Variants (a) an attacker derives a child of a token it merely observed, (b) a request is accepted under such a link, (c) a token is usable without its holder's key.¶
M1 and M2 are the two points of [MADDEN-REVIEW].¶
| Model | Seconds | Query | Result |
|---|---|---|---|
| aat01 | 42 | P1a | true |
| aat01 | 42 | P1b | true |
| aat01 | 42 | P2 | true |
| aat01 | 42 | P3 | true |
| aat01 | 42 | P4 | true |
| aat01-no-i5 | 16 | P1a | true |
| aat01-no-i5 | 16 | P1b | false |
| aat01-no-i6 | 12 | P4 | false |
| aat01-p4-inj | 23 | P4 injective | false |
| aat01-p4-inj | 23 | P4 | true |
| aat01-m1 | 1 | M1 | reached |
| aat01-m2 | 39 | M2 (a) | true |
| aat01-m2 | 39 | M2 (b) | true |
| aat01-m2 | 39 | M2 (c) = P4 | true |
| asor02 | 197 | P1a | true |
| asor02 | 197 | P1b' | true |
| asor02 | 197 | P2 | true |
| asor02 | 197 | P3 (depth, expiry) | true |
| asor02 | 197 | P3 (maximum depth) | true |
| asor02 | 197 | P4 | true |
| asor02-p1b | 37 | P1b | false |
| asor02-nodpop | 113 | P1a | true |
| asor02-nodpop | 113 | P1b | false |
| asor02-nodpop | 113 | P1b' | true |
| asor02-nodpop | 113 | P2 | true |
| asor02-nodpop | 113 | P3 (depth, expiry) | true |
| asor02-nodpop | 113 | P4 | false |
| asor02-m1 | 139 | M1 | reached |
| asor02-m2 (DPoP on) | 475 | M2 (a) | false |
| asor02-m2 (DPoP on) | 475 | M2 (b) | false |
| asor02-m2 (DPoP on) | 475 | M2 (c) = P4 | true |
| asor02-nodpop-m2 | 120 | M2 (a) | false |
| asor02-nodpop-m2 | 120 | M2 (b) | false |
| asor02-nodpop-m2 | 120 | M2 (c) = P4 | false |
asor02-holder-signs-m2 (DPoP on, hs) |
350 | M2 (a) | true |
| asor02-holder-signs-m2 | 350 | M2 (b) | true |
| asor02-holder-signs-m2 | 350 | M2 (c) = P4 | true |
asor02-nodpop-holder-signs-m2 (hs) |
143 | M2 (a) | true |
| asor02-nodpop-holder-signs-m2 | 143 | M2 (b) | true |
| asor02-nodpop-holder-signs-m2 | 143 | M2 (c) = P4 | false |
Each row is one query of one model file. A file without a suffix is a
profile with every check in place, and every query of it is expected
true. A .expectfalse file is a variant with one check removed
(no-i5, no-i6, nodpop) or asks for a property the profile does not
claim (p4-inj, asor02-p1b, the asor-02 M2 files), and at least one of
its queries is expected false; a .reachable file asks whether an event
occurs in a run of honest roles only, and "reached" is the expected
answer. A "false" in a variant is therefore the expected outcome of that
variant, not a failed proof; each is accompanied by a ProVerif derivation
and trace in model/out/, summarized in model/README.md. hs is the
holder-signs flag of the asor-02 model (Section 8). A "true" is a
ProVerif proof for an unbounded number of sessions of the bounded model
(Section 5.3), not a statement about the draft text. Seconds are
wall-clock per model file on one machine under make -j6, from
model/out/SUMMARY.md; they are indicative.¶
Nothing about constraint semantics below the tool level, time (freshness, skew, lifetimes; only expiry order), revocation, the trust anchor's key, size and algorithm checks, or chains longer than four tokens. A "true" is a ProVerif proof for unbounded sessions of the bounded model, not for the draft text.¶
Section numbers in this section are those of AAT-01 unless marked "-02 Section". Each finding B1 to B25 has a status line, starting "-02:", that gives how the -02 text relates to it:¶
The -02 text decides the point.¶
-02 keeps the behaviour and says it is intended.¶
-02 decides part of the point.¶
The question no longer arises in -02.¶
Unchanged in -02.¶
Where a vector of the author's suite pins the point, the status line names it by its identifier (J.x.y). Step numbers such as 3k are those of -02 Section 8.¶
B1 (Section 4.5): cross-type subsumption is an allow-list. Section 4.5
of -01 defines subsumption as a fixed list of per-type rules over
(derived, parent) constraint-type pairs and declares every other pair
invalid (-02 writes the same list as a table). The list leaves out some
pairs whose narrowing is decidable from the values alone.
For a composite parent the list loses nothing of its own: an exact
under an any can be written as a one-clause any (see B2). Two pairs
are lost outright: a one_of whose members all fall inside a parent
range, and an exact under a not_one_of, have no equivalent in the
parent's type.
This implementation follows the list and rejects all of them. -02 keeps
the pairs and names the choice: the verifier compares types, not values,
so that every verifier gives the same verdict.¶
-02: closed as design -- -02 Section 4.5 lists every valid (parent, derived) pair and says "Every other pair is invalid"; J.19.1, J.19.4 to J.19.7 and J.8.4 deny semantically narrower pairs, and their notes call this conservative by design.¶
B2 (Section 4.5): non-composite child under a composite parent. Section
4.5 of -01 gives rules for all and any only against a parent of the
same type, so a plain constraint under a composite parent falls under the
closing sentence and is invalid. This implementation rejects it. Nothing
is lost beyond B1, because the child can be written in the parent's
type. Under an any, a one-clause any is valid when the child narrows
one of the parent's clauses. Under an all, the child has to narrow
every parent clause: -01 matches each parent clause to a distinct
derived clause, so the child is repeated once per clause, while -02 lets
one derived clause satisfy several. Each step goes through the pairs, so
the rewrite fails exactly where a clause meets one of the two pairs of
B1, and an all with more clauses meets them more often.¶
-02: closed as design -- J.8.4 (range under all) and J.19.1 (exact under any) are denied; J.18.1 denies all under any.¶
B3 (Section 4.5): empty composite constraints. Section 4.5 of -01 says
only that a derived any must contain at least one clause. An empty
all is satisfied by every value and an empty any by none, so an empty
parent all grants the argument without limit and an empty parent any
forbids it; -01 addresses neither. This implementation treats an empty
clause list as malformed for both types, in either position.¶
-02: closed -- -02 Section 4.5 makes an empty all or any invalid in both the parent and the derived position; it is denied at steps 3n and 4o (J.21.7, J.16.3).¶
B4 (Section 4.5): range corner cases. Section 4.5 of -01 compares the
bounds of a derived range with the parent's, one bound at a time. It
does not say what a range that no number satisfies means (min greater
than max, or equal bounds with an exclusive side), nor what an
inclusivity flag without its bound means. This implementation applies
the bound rules literally, which never widens authority. An empty
derived range whose bounds lie inside the parent's is accepted and
denies every value; one whose bounds lie outside is rejected, although
an empty set narrows anything. Under an empty parent every derived range
that passes the bound rules is itself empty. A flag without its bound
has no effect. A verifier that rejects empty ranges as malformed
therefore denies more than this one, never less, and neither grants
anything the parent does not.¶
-02: partly -- -02 Section 3.4 well-formedness requires min not greater than max and inclusivity flags only with their bound (J.8.6, J.8.21, J.8.24). An empty range with equal bounds and an exclusive side (min equal to max, min_inclusive false) is still well-formed.¶
B5 (Section 3.4): value model and equality. -01 says only that
exact.value is "any scalar" and must be equal "exactly"; it does not
say whether null is a scalar, whether 1 equals "1" or 1.0, or what
happens to a number a parser cannot hold exactly. This implementation
takes string, number and boolean, compares by JCS, and refuses integers
beyond 2^53-1 so that a constraint and the tool see the same value. That
refusal covers integers only: a fraction such as 1.00000000000000001 is
still read as 1.¶
-02: closed -- exact.value may be a string, number, boolean or null,
equality is by JCS, and the admissibility rule of -02 Section 3.4
replaces this implementation's refusal of integers above 2^53-1 (J.8.8
permit; J.8.7, J.8.10, J.8.14 deny).¶
B6 (Section 3.4): arrays as sets. -01 does not say whether order or
duplicates matter in the arrays of contains and subset, so two
verifiers can disagree on the same argument. This implementation treats
both arrays as sets.¶
-02: closed -- -02 Section 3.4: contains and subset "treat their arrays as sets" (J.26.11, J.26.12).¶
B7 (Section 3.4): unknown members of a core constraint type. -01
specifies what to do with an unknown constraint type and says that
unknown top-level claims are ignored, but not what to do with an unknown
member of a known type. Ignoring it would be unsafe: a misspelt bound
such as maximum for max would leave the argument unbounded. This
implementation rejects such a constraint as malformed.¶
-02: closed -- well-formedness allows "no members other than" those of
the type (J.21.12, maximum). The strict reading of -01 taken by this
implementation is now the text.¶
B8 (Sections 3.6.2, 4.5): key-set rule and tool omission. Under a non-empty constraint map -01 fixes the shape of an invocation: every named argument must be present and no other may appear. A derived token may drop a whole tool (Section 3.6.2) but must keep exactly the parent's argument keys, because adding or dropping a key yields invocations the parent would reject. The rules are consistent; the cost is rigidity. A holder cannot forbid one argument while keeping the tool, and an argument the issuer wants to leave optional cannot be constrained at all; only a parent with an empty map lets a child choose keys. This implementation follows the rules as written.¶
-02: moot -- optional and "*" (-02 Section 3.3, the key rules of -02
Section 4.5, steps 4p2 and 4p3) give the means the closed-world key rule
lacked (J.22.*).¶
B9 (Section 4, Section 7): redundant checks (I2e, step 5). Some checks
of -01 Section 7 follow from others. child.del_depth <=
parent.del_max_depth (4e) follows from 4g and 4m, child.del_depth <=
MAX_DELEGATION_DEPTH (4f) from 3i, 4g and 4m, and the "defense in depth"
step 5 from 3c and 4d. No input fails one of them alone; this
implementation keeps all of them, and its property tests reach 4e only
together with 4g or 4m. Redundant checks are harmless, but a test suite
cannot cover them as single faults.¶
-02: open -- unchanged in -02 Section 8. The suite's README names "steps 4e, 4f and 5" among the checks that can never be the only fault. J.10.c reaches 4e only as the first of two faults (4m also fails).¶
B10 (Section 4.4): MAX_TOKEN_LIFETIME is redundant by construction. A child cannot outlive its parent, so a lifetime bound checked on the root holds for every link; the note to step 4l of -01 says as much. This implementation checks it on every token anyway.¶
-02: closed -- the notes to -02 Section 8 say the bound holds by induction (steps 3h, 4h and 4j); -02 has no per-child lifetime step.¶
B11 (Section 7): which anchor key verifies the root and what iss
means. In -01 the trust anchors are "a set of public keys" without
issuer names; the root verifies under any of them (step 3b), and step 3k
requires only that root.iss be a URI. Section 3.2 says that iss
identifies the root issuer, but nothing ties it to the key that
verified the signature. When an enforcement point trusts more than one
issuer, any of them can sign a root that names another as iss, and the
chain verifies. Within -01 this does not change a verdict, since nothing
else reads root.iss; it matters as soon as audit or local policy relies
on the issuer. The aat-01 profile follows -01 and accepts the root
under any anchor; the aat-02 profile implements the new step 3k.¶
-02: closed -- trust anchors are (issuer, key) pairs; step 3k compares
the root's iss with the issuer of the anchor that verified the
signature (J.21.6, J.21.22, J.21.23).¶
B12 (Section 4.2): I1 comparison of a URN as a string. I1 requires
child.iss to equal jwk_thumbprint_uri(parent.cnf.jwk), a URN.
[RFC8141] makes parts of a URN case-insensitive, so "equals" can be
read as a string or as a URN comparison. This implementation compares
strings exactly; it computes the thumbprint for OKP keys only, with the
member order of [RFC7638]. An exact comparison can only deny more, never
less.¶
-02: open -- step 4c still says "equals"; -02 says nothing about case-insensitive comparison of the URN ([RFC8141]). J.27.6 pins only the thumbprint members.¶
B13 (Sections 3.2, 7, 8.13): algorithms and thumbprints. -01 Section 3.2
requires support for Ed25519, while Section 8.13 admits the DPoP
algorithm list, including ES256 and RS256, and the closing note of
Section 7 mentions Ed448; I1 would
then need thumbprints for those key types as well. This implementation
accepts only EdDSA with an OKP Ed25519 key and always refuses
symmetric algorithms (Section 8.13).¶
-02: closed -- the fully-specified Ed25519 ([RFC9864]); -02 forbids
accepting EdDSA and requires alg to be consistent with the key's
kty and crv (J.7.5, J.7.8, J.7.10, J.14.b).¶
B14 (Section 7): parse before verify. -01 Section 7 forbids reading
claims before the signature is verified, but step 4b needs the parent's
parsed cnf.jwk, and step 2c lets jti be extracted first. This
implementation checks the size limits (2a, 2b), parses every token, and
then verifies. The verdict is the same either way; only the reported
code for a forged malformed token differs (malformed rather than
signature_invalid).¶
-02: closed -- steps 3b, 4b and 7b fix the order per token. With pairs
walked in order, the parent's cnf.jwk is parsed before the child's
signature is needed. The aat-02 profile follows the order literally;
aat-01 keeps parse-first.¶
B15 (Sections 3.2, 3.4): sub. -01 Section 3.2 "intentionally omits"
sub, and Section 3.4 says a token must not be rejected solely for
claims outside those defined. Rejecting a token with sub is therefore
not required, and arguably not allowed. This implementation rejects it
by default and has an option to accept it.¶
-02: open -- -02 still "intentionally omits the sub claim" and requires
unrecognized top-level claims to be ignored. The aat-02 profile does
not reject sub by default (rejectSub false). No vector carries sub
(J.27.1 covers other unrecognized claims).¶
B16 (Section 3.2): jti format. -01 says jti SHOULD be a UUIDv7 and,
when a UUID is used, MUST be lowercase and hyphenated, but steps 3j and
4b1 check only for a non-empty string. This implementation accepts any
non-empty string and rejects a UUID-shaped value with upper-case
letters.¶
-02: closed -- the notes to -02 Section 8 say the lowercase UUID form
"binds producers and has no verification step". The aat-02 profile
checks only for a non-empty string (aat-01 still rejects upper-case
UUIDs).¶
B17 (Section 7): limits without defaults. MAX_DELEGATION_DEPTH has no recommended value in -01; the other limits (MAX_IAT_SKEW 30 s, MAX_TOKEN_LIFETIME 90 days, MAX_CONSTRAINT_DEPTH 32, MAX_TOKEN_SIZE 64 KiB, MAX_STACK_SIZE 256 KiB) are RECOMMENDED. Whether a chain is valid then depends on local configuration, and a test vector has to state the limits it assumes. This implementation uses 16 for the depth.¶
-02: closed -- MAX_DELEGATION_DEPTH is at least 8 (J.10.f, J.21.15, J.21.19); the suite states every parameter.¶
B18 (Section 4.6): par_hash covers the signing input, not the
signature. Two JWS with the same signing input and different signature
bytes are therefore the same parent for I5. With Ed25519 and strict
verification this cannot be exploited; the choice is still worth stating
in the draft, since a profile with a malleable signature algorithm would
inherit it.¶
-02: open -- unchanged design; aat_hash is computed the same way.¶
B19 (Sections 4.2, 4.7, 8.9): the review's points as AAT-01 answers them. [MADDEN-REVIEW], posted on the OAuth list the day -01 appeared, makes two technical points about the chain, apart from the comparison with HMAC-chained Macaroons and SPKI/SDSI: a parent token stays valid after a child has been derived from it, so the design lacks the re-keying of Macaroons; and only the leaf proves possession, so an agent that sees the bytes of an intermediate or root token could use them. The author's reply on the list called additive derivation intended and every token holder-bound. This report names the points M1 and M2 (Section 5.1) and checks them in the model rather than in the reply. The second point -01 answers in its text. Section 4.2 (I1) requires the derived token to be signed by the holder of the parent, Section 4.6 (I5) commits it to that parent's bytes, and Section 4.7 (I6) requires the presenter to prove possession of the leaf key. A token observed in transit can therefore neither be extended, since derivation needs its holder's key, nor presented as a leaf, nor moved under another parent. The model confirms this for -01: against an attacker who sees every token and may hold any holder key other than that of the token in question, no link is derived from it (M2 a, P1b), no request is accepted through such a link (M2 b), and no request is accepted with it (M2 c, P4). The first point -01 does not state. Nothing in the text says that a parent remains valid after derivation; Section 8.9 puts revocation out of scope and recommends short lifetimes, and Sections 8.1.2 and 8.4 give the consequence, that a compromised holder key exercises the full authority of every token bound to it until expiry, without the property behind it. The model shows M1 as a run of honest roles alone: a holder derives a child from its token and is then accepted with that same token, with no key compromised. It is how the design works, not an attack on it. This implementation checks I1, I5 and I6 and keeps no state that would retire a parent after derivation, since -01 defines none.¶
-02: closed as design -- -02 Section 9.1.1 says that derivation is additive, that every token in a chain carries a holder key, and that a sub-agent holding its parent's bytes cannot invoke with them; -02 Section 9.4 says that AATs do not re-key, that a compromised holder key exposes every token bound to it, and gives deriving to a fresh key and destroying the parent's key as the approximation (-02 Section 9.4, with the fresh holder key of -02 Section 9.10). M1 and M2 (Section 5.1) are this report's independent check of it; the vectors do not bear on it.¶
B20 (Section 5.2): PoP header and typ. -01 specifies no typ, kid
or jwk header parameter for the PoP JWT, so nothing distinguishes it
from another JWT signed by the same key. This implementation looks only
at alg.¶
-02: closed -- typ is aat-pop+jwt (J.7.1, J.7.2); a jwk header
parameter is ignored as a key source (J.7.14).¶
B21 (Sections 5.2, 7): JCS of the whole PoP payload. -01 Section 5.2
requires the holder to serialize the whole PoP payload with JCS, but
step 7f compares only hta, and nothing says whether an enforcement
point must reject a payload that is not canonical. This implementation
rejects it: the payload bytes must equal their JCS re-serialization.¶
-02: closed (requirement dropped) -- -02 Appendix F: "PoP payloads no longer
need to be JCS-canonical". Only hta is compared (step 7f).¶
B22 (Section 7): clock tolerance. -01 recommends a PoP window of 30 seconds either side, and separately a one-sided MAX_IAT_SKEW of 30 seconds for tokens; it does not say whether the two are the same parameter. This implementation keeps them apart: the PoP window is its own option, symmetric, with inclusive bounds.¶
-02: closed -- -02 Section 9.9 separates the one-sided MAX_IAT_SKEW from the bilateral PoP window (J.20.4, J.20.15, J.20.16).¶
B23 (Section 5): PoP binds the leaf only. In -01 the PoP JWT names the
leaf by aat_id, which must equal the leaf's jti (Section 5.3), a
value chosen by whoever derived the leaf; neither the leaf's bytes nor
the chain enter the PoP signature. Which chain a PoP belongs to
therefore rests on jti being unique across derivers, which nothing
checks (steps 3j and 4b1 accept any non-empty string), and aat_aud is
optional. The model shows the consequence: one PoP signed by an honest
holder is accepted under two different chains that end in leaves with
that holder's key and that jti (trace aat01-p4-inj,
Section 5.2). This implementation compares aat_id with the leaf
jti, as -01 says, and nothing more.¶
-02: closed -- aat_hash binds the PoP JWT to the exact leaf instance
(J.20.7), and through par_hash to the whole chain above it. A present
aat_aud always applies (J.6.4). -02 Section 9.5 covers replay across
enforcement points.¶
B24 (Section 3.3): empty constraint map and absent arguments. A tool
with an empty constraint map accepts any arguments, while a tool with
even one constraint rejects an invocation that leaves out a constrained
argument, even one under wildcard. -01 Section 3.3 states both, but
the step from "no constraints" to "one constraint" changes the meaning
of every other argument, which is easy to misread. This implementation
follows the text.¶
-02: closed -- {} is equivalent to {"*": {"constraint_type":
"wildcard"}}, normalized at steps 4p and 6b (J.8.1, J.8.2, J.19.15,
J.22.17).¶
B25: no published test vectors; the 41 vectors are ours. -01 came with
no test vectors. The 41 vectors in verifier/vectors/aat/ were written
for this report from its own reading of -01. They show that the
implementation matches that reading, not that the reading matches the
author's.¶
-02: closed -- 229 author vectors; 229 of 229 agree here.¶
These are points where the -02 text, as read by this implementer, admits more than one conforming behaviour. None is a claim of a defect. Each ends with what this implementation does.¶
C1 (-02 Section 3.5; steps 3a, 4a, 7a): duplicate members in the
protected header. Steps 3b, 4b and 7b reject duplicate member names in
claims, and the notes to -02 Section 8 give parser divergence
([RFC8259], Section 4) as the reason. The JOSE header is not covered.
[RFC7515], Section 4 lets a parser either reject such a header or keep
the last member. With {"typ":"aat-pop+jwt",...,"typ":"aat+jwt"},
explicit typing then depends on the parser. This implementation: rejects
at steps 3a, 4a and 7a.¶
C2 (-02 Sections 3.4, 4.3.1): JSON nesting needed for
MAX_CONSTRAINT_DEPTH. A constraint tree of the depth -02 recommends (32)
nests about 2 x 32 JSON levels inside the payload. J.8.15 (permit) is 68
levels deep and J.8.16 is 70. A verifier whose JSON parser has a common
default nesting limit of 64 denies J.8.15 at step 3b. -02 says nothing
about parser nesting limits or how they relate to MAX_CONSTRAINT_DEPTH.
This implementation: the aat-02 parser allows
max(64, 2 * MAX_CONSTRAINT_DEPTH + 32) levels.¶
C3 (step 2c): "minimal JSON parsing". -02 does not say which jti is
extracted when the payload repeats jti with different values, nor
whether a payload that is not valid JSON fails at step 2c. J.23.4 uses
equal values and so avoids the question. This implementation: a tolerant
parse that takes the first occurrence; the duplicate is then rejected at
step 3b or 4b.¶
C4 (-02 Section 3.5; step 3a): typ comparison. The text says typ
"equals" aat+jwt. [RFC7515], Section 4.1.9 makes typ values
case-insensitive and allows the application/ prefix to be omitted, so
application/aat+jwt names the same type. -02 does not say whether
application/aat+jwt or AAT+JWT is to be accepted. This
implementation: exact string comparison, so both are denied.¶
C5 (-02 Section 7.2; step 7d): aat_aud type and semantics. Table 5 of
-02 types aat_aud as a string, but gives it "the semantics of the JWT
aud claim" ([RFC7519], Section 4.1.3), which allows an array. Whether
an array aat_aud naming the enforcement point is accepted is not
stated: -02 Section 3.2 has aat_aud identify an enforcement point "by
exact string comparison", which leans towards a string, while step 6c
explicitly allows aud to be an array. This implementation: anything
other than a matching string is denied.¶
C6 (steps 3b, 3k): "the trust anchor whose key verified the signature".
When two anchors share one key under different issuers (rotation,
aliasing), "the" anchor is ambiguous: the text can be read as any of
them or as exactly one. This implementation: accepts when the issuer of
any anchor whose key verifies the signature equals iss. The suite's
anchors have distinct keys, so no vector decides it.¶
C7 (-02 Section 3.2): magnitude of NumericDates and depths. Integer
syntax is required, but admissibility (-02 Section 3.4) applies only to
arguments and constraint values. An exp such as 9007199254740993 is
well-formed; comparing it as a double or exactly gives the same verdict
only because of the other bounds (lifetime, skew). Minor. This
implementation: compares as a double.¶
C8 (steps 3l, 4b2): what makes cnf.jwk invalid. "Encodes a public
key" is followed only by examples of private members. The suite expects
a denial at 3l or 4b2 for an RSA kty carrying Ed25519 members
(J.21.21), an X25519 curve (J.24.10) and a 31-byte x (J.24.11); the
description of J.24.10 itself allows the denial to come at step 7a
instead. -02 does not refer to the member validation of [RFC7517] or
[RFC8037]. This implementation: requires kty OKP, crv Ed25519, a
32-byte x and no private members (d, p, q, dp, dq, qi,
oth, k).¶
C9 (step 6b): args "as written" when the binding has already parsed
them. The duplicate-member check is possible only on the invocation's
JSON text. -02 Section 3.4 obliges a binding to preserve exact numbers
but says nothing about duplicates. This implementation: accepts args
as JSON text (checked) or as an already-parsed object (documented as
unable to carry either fault).¶
C10 (step 3k): the root's iss "is a URI". No grammar is referenced
(for example, absolute-URI of [RFC3986]). This implementation:
scheme ":" followed by non-space characters. Minor; no vector tests a
borderline form.¶
A1 (Section 6, step 1): which key verifies a link below the root. Step
1 of asor-02 Section 6 verifies the signature of every DT_i, takes the
algorithm from the verifier's "own configuration for the key", and
requires DT_0 to verify under a trusted root key. Which key verifies
DT_i for i > 0 it does not say, and no other rule relates the signer of
a link to its parent: not to the parent's cnf, as I1 of AAT-01 does,
nor to its iss, nor to the root key. Section 7 puts cnf in every
token, but cnf binds the party that presents the leaf (step 6), not
the party that signs the next link. Table 1 of Section 6.1 is
consistent with this: below the root it claims only that each child
commits to the parent instance, nothing about who signed it. A
verifier can therefore only apply a configured key set, selected by
kid, at every position. The author's implementations run "under a
single-signer trust model" (Section 11) and the 35 vectors sign every
hop with one key, so neither decides the point. With one configured key
the question does not arise. With several, the link checks accept a
child signed by any of them: the model states this as P1b false and
P1b' true (asor02-p1b, Section 5.2), and M2 (a) and (b) follow
from it (Section 8): one compromised configured key extends any chain,
and with the attacker's own key in the leaf cnf the request is
accepted even with DPoP. This implementation selects the key by kid
from one configured set for every DT_i, which is all the text allows.¶
A2 (Section 3, Section 4.3): del_max_depth inheritance. Section 3
requires del_max_depth in DT_0 only, and rule 5 of Section 4.3
compares C.del_max_depth <= P.del_max_depth for every link. When the
parent carries none, the rule has nothing to compare against; when the
child carries none, rule 3's "absence means unbounded" is written for
constraints, not for this claim. The vectors' children normally omit
it, and reject_child_raises_del_max_depth raises it directly under
the root. This implementation compares a child's value with the
nearest ancestor that states one, and lets a child without the claim
pass rule 5.¶
A3 (Section 3, Section 6): root without principal, malformed or step¶
Section 3 says that a chain whose DT_0 carries no sub, or an empty
one, is malformed; step 3 of Section 6 checks the same condition again
("DT_0.sub is a non-empty string") and says deny, and Table 2 has no
step-3 name for it (principal_altered is for a sub that differs
from DT_0's). The vector reject_root_without_principal expects
malformed. This implementation follows the vector and reports it at
parse time.¶
A4 (Section 6, step 3): chain length bound by convention. Step 3
requires n < DT_0.del_max_depth, where Section 6 writes the chain as
DT_0 ... DT_n, so n is the index of the last token and the chain has
n + 1 tokens; Section 3 calls del_max_depth "the maximum permitted
chain length". The two agree only if n is read as that index: a chain
of exactly del_max_depth tokens is then accepted and one token more
is denied. Read as a count, the same test denies the chain of
del_max_depth tokens. No vector reaches the boundary (A5). This
implementation reads n as the index.¶
A5: coverage of the 35 vectors. The vectors are signed with one public
HS256 test key, so step 1 is exercised only for an algorithm off the
accepted list, not for a wrong key or an unknown kid; cnf carries a
stand-in thumbprint with no key, so step 6 is not exercised; now is
0, so step 5 is exercised for monotonic expiry only; no vector reaches
depth_invalid (step 3), the JCS parse names (non_canonical,
duplicate_member, non_finite), step 7, or step 8 beyond
audience_mismatch (scope_not_granted, ceiling_exceeded,
unknown_constraint, cumulative totals); rule 5 is exercised only
directly under the root (A2). The verifier covers these in its own
tests, with generated Ed25519 keys for step 6; those tests are this
work's reading, not the author's.¶
A6 (Sections 4.3, 6): a child's del_max_depth has no effect. Step 3
bounds the chain by DT_0.del_max_depth alone, and rule 5 only
forbids a child from raising the value. A DT_1 carrying del_max_depth
1 therefore still admits DT_2 and DT_3 below it: the claim is checked
for monotonicity and never used as a limit. The model's asor02-p1b
trace shows exactly this chain (Section 5.2). In AAT-01, I2
requires derived.del_depth <= parent.del_max_depth at every link,
so there the value binds. This implementation follows the asor-02
text.¶
A7 (Sections 6.1, 9.4): who mints a link below the root. asor-02 does
not name the signer of a child, and its verbs differ: Section 6.1 has
"an issuer validating a parent before minting a child", Section 9.4 a
parent that "can mint any number of children" and "two issuers
allocating offline" from one budget, Section 11 "a single-signer trust
model". A Delegation Token is an OAuth JWT access token ([RFC9068])
with its own iss (Sections 2 and 3), and nothing requires the holder
of DT_{i-1} to take part in signing DT_i. This report therefore reads
asor-02 as issuer-minted attenuation verified offline: the holder of a
parent is bound by cnf and named by client_id, and a link is signed
by a key the verifier is configured to accept. Under that reading P1b,
a link made by the holder of its parent, is not a property asor-02
claims, and its failure (A1) is a different trust model rather than a
defect: what a chain establishes is P1b', and its security is that of
the weakest configured key. In the model the asor-02 holder signs the
child with its own key, so holder keys and configured keys are one
population, each agent its own issuer; this is one admissible
deployment, and the queries and verdicts are the same under either
reading. Where a deployment keeps issuer keys apart from agent keys,
the M2 (a) and (b) results need a compromised issuer key, not an agent
key.¶
Both drafts build the same chain: a root signed by a trusted key, each
link committing to its parent's signing input by par_hash, depth,
expiry and capabilities narrowing at every hop, a holder key in every
token and a proof of possession for the leaf. The model treats them as
two instances of one structure, and the shared properties come out the
same: the invoked tool was granted at the root (P2), the leaf's depth,
maximum depth and expiry are bounded by the root's (P3), a leaf naming an
honest key is used only by that key's holder (P4; for asor-02 with DPoP
on), and a parent remains accepted after its holder has derived from it
(M1), in both.¶
They differ in one place: who signs a link below the root. AAT-01 is
holder-derived: I1 requires the link to be signed by the key the parent
names in cnf, so the holder of a token is the only party that can
extend the chain below it. asor-02 is issuer-minted (A7): a link is
signed by a key the verifier is configured to accept, the holder of the
parent is bound by cnf and named by client_id, and nothing requires
it to take part in the signing (A1). Table 2 shows what follows
for M2. In the AAT-01 row an attacker who sees every token and may hold
any key but that of the token in question can neither derive below it
(a), nor have a request accepted through a link its holder did not make
(b), nor use it (c). In the asor-02 rows the same attacker, holding any
one configured key, which need not belong to the chain, signs a link
below an honest token and the link checks accept it (a); naming its own
key in the forged leaf's cnf, it is accepted at step 6 with its own
DPoP proof (b), since DPoP binds the presenter of the leaf and not the
signer of each link. DPoP decides (c) only. The security of an asor-02
chain is therefore that of the weakest key its verifier is configured to
accept; where issuer keys and agent keys are kept apart, (a) and (b)
need a compromised issuer key.¶
The last row isolates the cause. With one change to the asor-02 model,
the flag hs of asor02.pvl, a link verifies under its parent's cnf
instead of the configured key, and every M2 result for asor-02 with DPoP
equals the AAT-01 result (asor02-holder-signs-m2, Table 1);
with DPoP off, (a) and (b) become no and (c) stays yes. The M2 difference
between the drafts is caused by A1 alone. This is a reading asor-02 does
not state, not a rule of it; adopting it would make the signer of a link
the holder of its parent, which is the other draft's design.¶
| Profile | M1 | M2 (a) | M2 (b) | M2 (c) |
|---|---|---|---|---|
| AAT-01 | parent accepted after derivation: yes | no | no | no |
| asor-02, DPoP on | yes | yes: one configured key extends any chain | yes: attacker's own key in the forged leaf's cnf
|
no: DPoP gives P4 |
| asor-02, DPoP off | yes | yes | yes | yes: an observed chain is accepted from anyone |
asor-02, DPoP on, holder-signs reading (hs) |
not run | no | no | no |
Note to the RFC Editor: please remove this section and the reference to [RFC7942] before publication.¶
This section records the status of known implementations at the time of posting, following [RFC7942]. The inclusion of an implementation here does not imply endorsement by the IETF.¶
[AAT-02] Appendix E lists further implementations of AATs, and the
INTEROP.md file of the author's vector suite keeps a table of results
against it; this report does not restate them.¶
None (individual).¶
aat (verifier and model).¶
A minimal TypeScript verifier for AAT-01, AAT-02 and asor-02 chains, and a ProVerif model of AAT-01 and asor-02; see Section 4 and Section 5.¶
Research prototype; an executable specification, not a product.¶
Chain verification only; no minting service, no CBOR, no revocation. asor-02: the token format (Section 3), authority and subsumption (Section 4), chain linkage (Section 5) and the offline verification algorithm (Section 6) through the action check (step 8), with DPoP holder binding (Section 7) for EdDSA proofs; not status lists (step 7, Section 8), mTLS confirmation or ES256 proofs. AAT-01: the root, link and leaf checks of Section 7 (invariants I1 to I6 of Section 4), the nine constraint types and their subsumption (Sections 3.4 and 4.5) and the PoP JWT (Section 5); not root issuance (Section 3.7) or derivation (Section 6) beyond test-only minting, keys other than Ed25519, or a PoP replay cache. AAT-02: every step of -02 Section 8, with the constraint map, well-formedness and subsumption rules of -02 Sections 3.3, 3.4 and 4.5 and the PoP JWT of -02 Section 7; not issuance or derivation (-02 Section 6), algorithms other than Ed25519, or extension constraint types (-02 Section 5).¶
draft-niyikiza-oauth-attenuating-agent-tokens-01, draft-niyikiza-oauth-attenuating-agent-tokens-02 and draft-asor-wimse-agent-delegation-chain-02.¶
Apache License 2.0.¶
The AAT-02 profile was written from the -02 text and then run against the author's vector suite; it agreed on all 229 vectors on the first run. Of the 25 observations made on AAT-01 while implementing it (Section 6), the -02 text decides 19 (16 closed, one of them by dropping the requirement, and 3 closed as design), decides one in part (B4), makes one moot (B8) and leaves four unchanged (B9, B12, B15, B18); ten points of the -02 text are underdetermined for an implementer (C1 to C10), and no vector decides any of them against this implementation. On asor-02 (Section 7) the main point is A1 with A7: Section 6 names no key for a link below the root, and the model states what the chain then guarantees; A2 to A6 are smaller gaps in the text and in the vectors' coverage. Where a draft was decidable the implementation followed it; where it was not, the choice is recorded in this document, not only in the code.¶
Azat Davliatshin, ardavlyatshin@gmail.com¶
2026-11-02¶
This document specifies no protocol and no change to one. The model's attacker controls the network, observes every token, and may compromise any holder key at any time but not the trust anchor's key; results are only as strong as that attacker and the bounds of Section 5.3.¶
This document has no IANA actions.¶
From the root of https://github.com/azatdavliatshin/aat:¶
npm --prefix verifier install¶
npm --prefix verifier test: property and unit tests, the 35 asor-02
vectors, the 41 AAT-01 vectors and the 229-vector AAT-02 suite.¶
npm --prefix verifier run suite:aat02: runs the AAT-02 suite alone and
writes verifier/results/aat02-results.json in the format of the
suite's compare_results.py.¶
In a checkout of the suite's repository:
python3 ietf/vectors/compare_results.py results.json, with
results.json the file written by the previous command. It prints the
suite's SHA-256 and the count of agreements; --steps also lists the DENY
vectors whose failing step differs. The script is not part of this
repository, and outside the suite's directory layout it needs
--suite path/to/aat-jws-vectors.json.¶
make -C model summary: every ProVerif model, and model/out/SUMMARY.md.
The proverif executable must be on PATH (or passed as
PROVERIF=/path/to/proverif); it can be installed with opam install
proverif.¶
make -C report and make -C report idnits: build this document and
check it. They need kramdown-rfc, xml2rfc and idnits on PATH.¶
Versions: Node 22 or later, Python 3 and ProVerif 2.05 are required. The verifier run reported here used Node v26.12.0, npm 11.20.0 and Python 3.14.4 (146 tests, 146 passed). The model results of Table 1 are from ProVerif 2.05. The report was built with kramdown-rfc 1.7.43, xml2rfc 3.34.1 and idnits 3.2.0.¶
The author of the AAT drafts published the -02 test-vector suite and its comparison script, which made the AAT-02 results in this report reproducible by anyone; the author of asor-02 published the test vectors used for the asor-02 profile. The M1 and M2 properties of the model follow the two points of Neil Madden's review of AAT on the OAuth working group list. The author of this report thanks all three. Any error in reading the drafts is the author's own.¶