Skip to content

About

Draft profile + CC0 test vectors for independent work-acceptance records

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

31 Commits

Folders and files

Repository files navigation

Work-acceptance record (draft profile 1.4.2, schema 1.2)

A work-acceptance record is a checking party's signed findings about a delivered file, against criteria that were fixed before the check. It is distinct from a counterparty's acceptance decision (accepted / rejected / disputed). A counterparty acceptance can reference a work-acceptance record by digest, so "the checker found X" and "the counterparty accepted it" remain two signatures from two parties.

Properties

  1. Bound to the exact file: SHA-256 of the delivered file (and of the original, when there is one).
  2. Criteria committed first: the criteria document has its own SHA-256 and an RFC 3161 timestamp (or a binding to terms the counterparty signed). A verifier reports whether the commitment predates the check. The token over criteria.sha256 is authoritative for when; criteria.committed_at is the checker's own claim.
  3. Checker independence is checkable: parties.checker and parties.producer are named separately. The checker key must not equal the producer key; a self-check is labelled independence: "self-checked" and verifiers warn on it.
  4. Verdict vocabulary: each criterion result is pass | fail | not-evaluated | error. The overall verdict is pass only if every required criterion is pass. Any required fail makes it fail, and fail takes precedence over incomplete; otherwise any required not-evaluated or error makes it incomplete, never pass. Verifiers recompute the verdict from the results; a stated verdict that differs is VERDICT_MISMATCH.
  5. Verifiable from bytes: canonical form JCS (RFC 8785). Ed25519 signature over the canonical receipt object. An RFC 3161 token over the SHA-256 of the canonical receipt. Suites are declared in the record (canonicalization, suite). No hosted service or account is needed.

Verification procedure (summary)

  1. Parse; refuse an unknown schema, canonicalization or suite.
  2. JCS-canonicalize receipt; verify the Ed25519 signature with the key named by the signed receipt.key_id. The envelope's signature.key_id must equal it, else SIGNATURE_INVALID (the envelope is unsigned, so it cannot choose the key); whether that key is parties.checker.key_id is a separate check (CHECKER_NOT_SIGNER). Key rule: the public key must be a canonical encoding (y < p) of a point of prime order; any other key (neutral point, small or mixed order, non-canonical) is KEY_REJECTED and no signature check is run. That is a finding about the key list, not evidence of tampering. The rule applies to the key named by the signed receipt.key_id only; other entries in the key file do not change the outcome fields, and a verifier may report them separately (vector 28). On a signature failure report SIGNATURE_INVALID; the remaining checks may still be reported as diagnostics, but the contents are not presented as results.
  3. If the delivered file is supplied, compare its SHA-256 with files.remediated.sha256.
  4. If the criteria document is supplied, compare its SHA-256 with criteria.sha256 (CRITERIA_HASH_MISMATCH); when it matches, check the results correspond to the criteria (CRITERIA_RESULTS_MISMATCH).
  5. Criteria commitment. If a timestamp token is supplied, the imprint of its signed TSTInfo (never other bytes of the response, such as PKIStatusInfo.statusString; genTime and accuracy come from the same TSTInfo) must be SHA-256 (OID 2.16.840.1.101.3.4.2.1; a same-length imprint under another algorithm is not a match) and equal criteria.sha256 (CRITERIA_TIMESTAMP_MISMATCH otherwise), and its genTime is authoritative; a criteria.committed_at that disagrees is CRITERIA_TIME_MISMATCH. Without a usable token (none, or one over other bytes), fall back to committed_at and warn CRITERIA_COMMITMENT_UNVERIFIED. The criteria were committed before the check iff the commitment time is strictly earlier than the reference time: the earlier of issued_utc and the genTime of the record's own token when a token covering the record is supplied, else issued_utc (a covering token bounds the check from above; it never moves the signed issue time later). A token's stated accuracy widens its time against "committed before"; a token stating none is taken at its genTime; ordering is ignored. If not, criteria_committed_before_check is no and CRITERIA_COMMITTED_AFTER_CHECK is raised. That is a problem, not a warning.
  6. Check the record's timestamp token imprint (SHA-256, as in step 5) against SHA-256 of the canonical receipt; the unsigned timestamp.digest_sha256 field is informative and never decides the outcome; report receipt_timestamp as none (no timestamp or no token supplied), imprint_match or imprint_mismatch. A mismatching token is never used as evidence of time.
  7. Recompute the verdict from the per-criterion results (property 4) and compare it with the stated verdict (VERDICT_MISMATCH).
  8. Emit problem codes (SIGNATURE_INVALID, KEY_REJECTED, CHECKER_NOT_SIGNER, KEY_COLLISION, INDEPENDENCE_INCONSISTENT, RESULT_VOCABULARY, VERDICT_MISMATCH, CRITERIA_HASH_MISMATCH, CRITERIA_RESULTS_MISMATCH, CRITERIA_TIMESTAMP_MISMATCH, CRITERIA_TIME_MISMATCH, CRITERIA_COMMITTED_AFTER_CHECK, RECEIPT_TIME_UNREADABLE) and warning codes (SELF_CHECKED, PRODUCER_UNDECLARED, CRITERIA_COMMITMENT_UNVERIFIED). The record is accepted as stated when the signature is valid and there are no problem codes. Code lists are sets: compare without regard to order. PRODUCER_UNDECLARED follows the parties block (no producer named), whatever the record states; a stated independence that differs from the derived one is also INDEPENDENCE_INCONSISTENT. A record refused at step 1 has the outcome {"refused": true, "reason": "unknown schema, canonicalization or suite"}.
  9. Time-stamp authority. The outcome fields cover imprint and time only. A verifier that presents a time as evidence to a third party should also validate the token's CMS signature and certificate chain against authorities the relying party trusts, and report that separately. Profile 1.4: the outcome may carry tsa_signature, an object with keys criteria and/or receipt (one per token present). Without relying-party anchors (none supplied, or an empty list) it is not_evaluated, and an absent field reads the same. With anchors it is verified (CMS signature, the contentType signed attribute equal to an eContentType of id-ct-TSTInfo (RFC 5652 §5.3), ESS signing-certificate binding, id-kp-timeStamping, and a chain of CA issuers reaching a supplied anchor (matched by subject name and public key, RFC 5280 §6.1.1, not by certificate bytes; an anchor with the issuer's name but another key is not that issuer, so if only such anchors fail the value is untrusted), with the signer and every issuer other than the anchor valid at genTime; a root inside the token is not an anchor), failed, untrusted, unsupported (e.g. a curve other than P-256/P-384; RSA PKCS#1 v1.5 and ECDSA P-256/P-384 are required, RSASSA-PSS is optional) or no_signer. It never changes any other outcome member. When a token has more than one defect, the value is failed if any check that can run fails; otherwise no_signer, then unsupported, then untrusted. An issuer in the token that fails a check is failed whether or not the chain reaches a supplied anchor.
  10. Profile 1.4.2 rules (vectors 60-90; vectors 01-57 and their outcomes are unchanged).
    • Canonical base64. Every base64 value read is canonical: the standard alphabet, = padding to a multiple of four, no whitespace, and re-encoding the decoded bytes gives the input string. A non-canonical signature value is SIGNATURE_INVALID; a non-canonical public key is KEY_REJECTED; a token or criteria file that is not canonical base64 matches nothing: it is reported as the corresponding mismatch (CRITERIA_HASH_MISMATCH, CRITERIA_TIMESTAMP_MISMATCH, or imprint_mismatch for the record's token), never as a refusal (60-63, 89, 90).
    • Lowercase digests. A SHA-256 digest in a record is exactly 64 lowercase hex digits; upper case matches no digest (67).
    • Repeated key identifier. A key_id listed more than once in the key list is KEY_REJECTED, whichever entry comes first. If it is the signing key's, no signature check is run; if it is the producer's or checker's, independence is derived as self-checked (75, 79).
    • Form of genTime. DER GeneralizedTime in UTC: YYYYMMDDHHMMSS, optionally . and digits whose last is not 0, then Z; real calendar fields. Any other form is no usable time and the token is treated as giving none: warning CRITERIA_COMMITMENT_UNVERIFIED and fallback to committed_at (70-74, 78). A fraction is kept to the millisecond; the criteria time is rounded up and the reference time down, and "before" is strict < (66, 68).
    • Stated times. committed_at and issued_utc are RFC 3339 UTC, YYYY-MM-DDTHH:MM:SS[.fff]Z. A stated time that step 5 uses and that is in another form gives RECEIPT_TIME_UNREADABLE and criteria_committed_before_check: no, not CRITERIA_COMMITTED_AFTER_CHECK (76). Step 5 always uses issued_utc; it uses committed_at only when no usable criteria token supplies the time. When one does, a committed_at in another form is CRITERIA_TIME_MISMATCH (88).
    • Independence by key. Keys are compared as decoded bytes: two identifiers whose entries decode to one public key are one key (KEY_COLLISION, self-checked; 65, 87). A key entry for a stated producer or checker identifier that is not canonical base64 is KEY_REJECTED and independence is self-checked (69). When a party's key_id is stated and independence: independent is claimed, that key_id must be non-empty and listed once, else INDEPENDENCE_INCONSISTENT (80, 81); a producer with no key_id rests on the party names (82).
    • Checker is the signer. CHECKER_NOT_SIGNER compares the checker's key_id with the one the signature was checked under (the signed key_id), never the unsigned envelope's. An envelope naming another key is SIGNATURE_INVALID only (64, 86).
    • Unsigned attributes. Unsigned attributes of a token's SignerInfo are not read and are no ground for refusal (83).
    • Types. A record or input whose objects, arrays or strings have other types than the specification gives is refused as an unsupported schema, and never makes a verifier fail (77).

Vectors (vectors/, CC0)

file case
01-valid.json valid, independent, criteria committed first
02-criteria-committed-after-issue.json criteria commitment later than the check
03-self-checked.json checker == producer organisation
04-required-not-evaluated.json a required criterion not-evaluated → incomplete
05-checker-key-equals-producer-key.json key collision
06-receipt-edited-after-signing.json a finding edited after signing (contributed by babyblueviper1, CC0)
07-criteria-file-swapped.json another criteria document supplied (contributed by babyblueviper1, CC0)
08-receipt-token-over-another-receipt.json the record's token covers another record (contributed by babyblueviper1, CC0)
09-stated-pass-recomputes-incomplete.json signed pass with a required criterion not-evaluated (contributed by babyblueviper1, CC0)
10-committed-at-contradicts-token.json committed_at before the check, token genTime after it (contributed by babyblueviper1, CC0)
11-required-criterion-fails.json a required fail (contributed by babyblueviper1, CC0)
12-signer-is-not-the-checker.json signed receipt.key_id and envelope name test-2, parties.checker.key_id names test-1 (rebuilt 12b; contributed by Tetsurohhori, CC0)
13-independent-but-same-party.json stated independent, same party (contributed by Tetsurohhori, CC0)
14-results-not-in-criteria.json results do not correspond to the criteria (contributed by Tetsurohhori, CC0)
15-criteria-token-over-other-bytes.json criteria token over other bytes; fallback with warning (contributed by Tetsurohhori, CC0)
16-no-criteria-token.json no criteria token (contributed by Tetsurohhori, CC0)
17-committed-at-equals-reference.json commitment time equals reference time: strictly earlier required (contributed by Tetsurohhori, CC0)
18-mismatching-record-token-later-time.json record token over other bytes, later time: not used (contributed by Tetsurohhori, CC0)
19-result-outside-vocabulary.json result passed (contributed by Tetsurohhori, CC0)
20-producer-undeclared.json no producer, stated independent (contributed by Tetsurohhori, CC0; expectation amended, the contributor's is stated in the file)
21-unknown-schema.json unknown schema: refused (contributed by Tetsurohhori, CC0)
22-matching-record-token-later-than-issued.json covering record token later than issued_utc: reference is the earlier (contributed by babyblueviper1, CC0)
23-criteria-token-imprint-oid-sha384.json criteria token imprint algorithm SHA-384, same bytes (contributed by babyblueviper1, CC0)
24-criteria-token-imprint-oid-sha3-256.json as 23 with SHA3-256 (case from robertolocatelli81-dev)
25-criteria-token-imprint-oid-sha512-256.json as 23 with SHA-512/256 (case from robertolocatelli81-dev)
26-key-small-order.json neutral-point key; R = neutral, S = 0 verifies for any message under the bare equation: KEY_REJECTED
27-key-non-canonical-encoding.json non-canonical key encoding (y = p + 1): KEY_REJECTED
28-key-file-with-unrelated-bad-entry.json an unrelated small-order entry listed before the valid signing key: same outcome as 01 (case from robertolocatelli81-dev)
29-criteria-token-accuracy-flips-commitment.json the criteria token is 30 s before the check but states an accuracy of 60 s: CRITERIA_COMMITTED_AFTER_CHECK (case from robertolocatelli81-dev)
30-criteria-token-accuracy-millis-still-before.json control for 29: the same times with an accuracy of 500 ms (the millis field): no problems
31-tsa-freetsa-verified.json live FreeTSA tokens over the criteria and the record, FreeTSA root as anchor: tsa_signature both verified
32-tsa-digicert-verified.json live DigiCert tokens, DigiCert Trusted Root G4 as anchor: both verified
33-tsa-unrelated-root-untrusted.json 31's tokens with an unrelated anchor: both untrusted
34-tsa-tampered-signature-failed.json 31 with one bit flipped in the criteria token's signature: criteria failed, receipt verified
35-tsa-token-without-signer-certificate.json a live FreeTSA criteria token requested with certReq=false: criteria no_signer
36-tsa-no-anchors-not-evaluated.json 31's tokens with no anchors: both not_evaluated (an omitted field reads the same)
37-tsa-synthetic-control-verified.json a criteria token from a test-only PKI (root, intermediate, TSA), test root as anchor: verified (control for 38 and 39)
38-tsa-intermediate-expired-failed.json as 37, but the intermediate CA expired before genTime while the TSA certificate is still valid: failed
39-tsa-content-type-mismatch-failed.json as 37, but the signed contentType attribute is id-ct-authData; the signature itself is valid: failed
40-tsa-rsassa-pss-verified.json as 37, signed with RSASSA-PSS (SHA-256, MGF1-SHA-256, salt 32): verified; unsupported also conforms (PSS is optional, listed in the vector's tsa_signature_accepted)
41-tsa-ecdsa-p256-verified.json as 37, with an ECDSA P-256 TSA key: verified
42-tsa-ecdsa-p521-unsupported.json as 37, with an ECDSA P-521 TSA key: unsupported
43-tsa-empty-anchor-list-not-evaluated.json 31's tokens with tsa_roots_base64: []: both not_evaluated, as 36
44-tsa-unsupported-curve-and-bad-digest-failed.json as 42 (P-521), with a messageDigest that does not match: failed
45-tsa-no-certificates-and-bad-digest-failed.json a test-PKI token without certificates, with a messageDigest that does not match: failed
46-tsa-unsupported-curve-and-ess-mismatch-failed.json as 42 (P-521), with an ESS signing-certificate hash that does not match: failed
47-receipt-payment-x402-exact.json a passing receipt whose signed body carries the optional payment member (x402 exact on Base, synthetic values): no problems, the same outcome as without it
48-tsa-signer-without-timestamping-eku-failed.json a test-PKI token (root B) whose signer certificate lacks id-kp-timeStamping; everything else holds: failed
49-tsa-issuer-not-a-ca-failed.json a test-PKI token (root B) whose TSA certificate is issued by a certificate without the CA flag; everything else holds: failed
50-criteria-token-imprint-only-in-status-string.json the criteria token's signed TSTInfo covers other bytes; the criteria imprint and time sit only in the unsigned statusString: CRITERIA_TIMESTAMP_MISMATCH, warning CRITERIA_COMMITMENT_UNVERIFIED (contributed by Tetsurohhori, CC0)
51-record-token-imprint-only-in-status-string.json the record-token side of 50: receipt_timestamp imprint_mismatch, no problems
52-receipt-non-integer-numbers.json a passing receipt whose signed body holds non-integer numbers, canonicalized as RFC 8785 (ECMAScript) numbers
53-tsa-anchor-same-name-other-key-untrusted.json 37's token; the only anchor is root B, with root A's name and another key: untrusted
54-tsa-expired-intermediate-no-anchor-failed.json 38's token (intermediate expired) with an unrelated anchor: failed
55-receipt-payment-outside-the-form.json as 47, with payment.amount a JSON number and an extra member: no problems, the same outcome as without it
56-third-party-checker-live-receipt.json a live receipt issued by another checker under its own key (a self-test, not a real job), kept as received: no problems (contributed by babyblueviper1, CC0)
57-receipt-integer-valued-doubles.json a passing receipt whose signed body holds 1.0, 100.0, -0.0 and 300.0, which RFC 8785 writes as 1, 100, 0 and 300
58-criteria-token-imprint-only-in-unsigned-attribute.json as 50, with the criteria imprint and time in an unsigned CMS attribute of the token's SignerInfo instead of the statusString (a well-formed response): CRITERIA_TIMESTAMP_MISMATCH, fallback with CRITERIA_COMMITMENT_UNVERIFIED (contributed by Tetsurohhori, CC0)
59-record-token-imprint-only-in-unsigned-attribute.json the record-token side of 58, as 51: receipt_timestamp imprint_mismatch, no problems (contributed by Tetsurohhori, CC0)
60-signature-base64-invalid-character.json A ! inside signature.value; a lax decoder discards it and the signature verifies: signature_valid false; SIGNATURE_INVALID. (case proposed by actaseal)
61-signature-base64-internal-whitespace.json A space inside signature.value: signature_valid false; SIGNATURE_INVALID.
62-signature-base64-noncanonical-trailing-bits.json The last base64 character of signature.value carries non-zero trailing bits: the same 64 bytes under a different string: signature_valid false; SIGNATURE_INVALID.
63-signature-base64-missing-padding.json signature.value with its = padding removed: signature_valid false; SIGNATURE_INVALID.
64-envelope-key-id-differs-from-signed-key-id.json The envelope signature.key_id names another key from the one inside the signed receipt; the key list holds both, with the same public key: signature_valid false; SIGNATURE_INVALID only. (case proposed by actaseal)
65-one-public-key-under-two-key-ids.json The producer's key identifier differs from the checker's, but both entries in the key list hold the same public key: KEY_COLLISION; warning SELF_CHECKED; independence self-checked. (case proposed by actaseal)
66-criteria-gentime-fraction-makes-it-after-issue.json The criteria token's genTime is 12:00:00.5; issued_utc is 12:00:00.250. Read to the second the criteria are earlier; with the fraction they are later: CRITERIA_COMMITTED_AFTER_CHECK; criteria_committed_before_check no. (case proposed by actaseal)
67-criteria-sha256-upper-case.json criteria.sha256 written in upper-case hex (re-signed): CRITERIA_HASH_MISMATCH, CRITERIA_TIMESTAMP_MISMATCH. (case proposed by actaseal)
68-criteria-time-equals-issue-time-at-millisecond.json The criteria genTime 12:00:00.2495 rounds up to .250, which equals issued_utc 12:00:00.250: CRITERIA_COMMITTED_AFTER_CHECK; no (strict <). (case proposed by actaseal)
69-key-alias-noncanonical-base64-dodges-same-key.json The producer's key entry is the checker's key with its last character changed (non-canonical base64): KEY_REJECTED; independence self-checked.
70-criteria-gentime-with-offset.json The criteria token's genTime is 20260301010000+0100: No usable time: warning CRITERIA_COMMITMENT_UNVERIFIED; no problems.
71-criteria-gentime-month-13.json The criteria token's genTime has month 13: No usable time: warning CRITERIA_COMMITMENT_UNVERIFIED; no problems.
72-criteria-gentime-feb-30.json The criteria token's genTime is 30 February: No usable time: warning CRITERIA_COMMITMENT_UNVERIFIED; no problems.
73-criteria-gentime-comma-fraction.json The criteria token's genTime uses a comma as the fraction separator: No usable time: warning CRITERIA_COMMITMENT_UNVERIFIED; no problems.
74-criteria-gentime-trailing-zero-fraction.json The criteria token's genTime fraction ends in a zero (.5000): No usable time: warning CRITERIA_COMMITMENT_UNVERIFIED; no problems.
75-duplicate-key-id-in-keys-file.json The checker's key identifier is listed twice in the key list: signature_valid false; KEY_REJECTED.
76-committed-at-malformed.json committed_at is 2026-13-01T00:00:00Z and no criteria token is supplied: RECEIPT_TIME_UNREADABLE; criteria_committed_before_check no; warning CRITERIA_COMMITMENT_UNVERIFIED.
77-result-id-is-an-object.json A result's id is an object, not a string: Refused, as for an unsupported schema.
78-token-imprint-matches-gentime-unusable.json The criteria token covers criteria.sha256 but its genTime is month 13: As with no token: warning CRITERIA_COMMITMENT_UNVERIFIED; the time comes from committed_at; no problems.
79-unrelated-duplicate-key-id.json A key identifier that is neither the producer's nor the checker's is listed twice in the key list: KEY_REJECTED; the signature is still valid.
80-independent-claim-producer-key-id-empty.json Independence is claimed; the producer's key_id is the empty string: INDEPENDENCE_INCONSISTENT (derived self-checked).
81-independent-claim-producer-key-id-unknown.json Independence is claimed; the producer's key_id is not in the key list: INDEPENDENCE_INCONSISTENT (derived self-checked).
82-independent-claim-producer-key-id-absent.json Independence is claimed; the producer states no key_id. The control for 80 and 81: independence rests on the names: No problems, no warnings; independence independent.
83-tsa-signer-info-unsigned-attributes-ignored.json Vector 37's criteria token with an unsignedAttrs field holding one arbitrary attribute appended to its SignerInfo (DER lengths corrected; not covered by the signature): Identical to vector 37: tsa_signature.criteria verified.
84-receipt-exponent-numbers.json a passing receipt whose signed body holds numbers that RFC 8785 writes in exponent form with more than one significant digit (1.5e-7, 1.25e+22, 1.234e-10, 9.87654321e-8, 2.5e+21): no problems (contributed by actaseal, CC0)
85-receipt-key-outside-bmp.json a passing receipt whose signed body has an object with keys z, U+1F600 and U+FF61. Sorting by UTF-16 code units puts U+1F600 before U+FF61; sorting by code point reverses them and breaks the signature: no problems (contributed by actaseal, CC0)
86-envelope-key-id-differs-from-signed.json signed with key A; the signed key_id and parties.checker.key_id name A; the unsigned envelope signature.key_id names B, which is also in the key list: signature_valid false; SIGNATURE_INVALID only. (contributed by actaseal, CC0)
87-same-key-under-two-ids.json as 05, but the producer names a different key_id whose entry in the key list holds the same public key as the checker's: KEY_COLLISION; warning SELF_CHECKED; independence self-checked. (contributed by actaseal, CC0)
88-committed-at-malformed-with-token.json committed_at is 2026-13-01T00:00:00Z and a valid criteria token is supplied. Step 5 takes the time from the token, so the time is readable: CRITERIA_TIME_MISMATCH, not RECEIPT_TIME_UNREADABLE; criteria_committed_before_check yes. (case proposed by Tetsurohhori)
89-criteria-file-base64-noncanonical.json The criteria file's base64 has a space inside: it matches nothing: CRITERIA_HASH_MISMATCH; criteria_hash mismatch; not refused.
90-criteria-token-base64-noncanonical.json The criteria token's base64 has a space inside: it matches nothing: CRITERIA_TIMESTAMP_MISMATCH; warning CRITERIA_COMMITMENT_UNVERIFIED; not refused.

Each file contains its inputs and an expected object with the outcome an independent verifier should reproduce. The keys in the vectors are test keys: never trust them. The timestamp tokens in the vectors are imprint-and-time fixtures, not valid RFC 3161 tokens: their SignedData has no SignerInfo, so OpenSSL and other CMS parsers that require a signer refuse them. They test imprint and time handling only. Vectors 31-36 instead carry real RFC 3161 tokens (FreeTSA and DigiCert, fetched 2026-10-20), and 37-46 (except 43) tokens from a test-only PKI (openssl 3.5, re-signed at a fixed genTime; the test root is not a real authority), and relying-party anchors in inputs.tsa_roots_base64.

Composition (composition/)

A profile (war/1.2) for composing a work-acceptance record with a payment-and-delivery record, with sixteen vectors and a checker: composition/COMPOSITION.md.

Reference verifier (verifier/)

verifier/war_verify.py runs the procedure above using only the Python standard library (Ed25519, JCS with RFC 8785 numbers, reading the token's signed TSTInfo, and the profile 1.4 authority-signature checks are implemented in the file), with no network access:

python -I verifier/war_verify.py vectors/*.json

verifier/check_payment.py (standard library, network) checks a receipt's optional payment member against the chain: EIP-3009 authorizationState(payer, nonce) on asset, and, given settlement_tx, the AuthorizationUsed and Transfer(payer, pay_to, amount) logs and a block no later than issued_utc. It reports verified, authorization_used, not_found, mismatch or unsupported (also for a member outside the form: all members strings, network eip155: plus a decimal chain id, amount a decimal string, 0x-hex addresses and nonce, no other members), and changes no outcome:

python -I verifier/check_payment.py receipt.json

It prints AGREE or DIFF per vector and exits non-zero on any disagreement; --outcome FILE prints the computed outcome object. It checks token imprints, times and accuracy; the authority signature (step 9) is evaluated when anchors are supplied, from a vector's inputs.tsa_roots_base64 or --tsa-roots FILE. It is offered as a reference for comparison; clean-room verifiers that disagree with it, or with a vector's expected object, are the useful result.

The file verifier/war_verify.py implements profile 1.4.2 and agrees with all 90 vectors. With WAR_VERIFY_STRICT=1 an internal error is raised instead of being reported as a refusal.

Implementations

Verifiers written from this text, each run by its implementer on the published vectors. Every row links that implementer's own run file at a fixed commit; the result and the disclosure are theirs, as posted.

Implementer Code Vectors Result Run file As disclosed by the implementer
Tetsurohhori Python, cryptography 01-57 at bc5a138 57/57 ADDENDUM-bc5a138.md written from the text; reference verifiers not opened; expected objects visible while the inputs were read
Tetsurohhori Python, cryptography 01-87 at 85a310d 87/87 ADDENDUM-profile142.md written from the 1.4.2 rule text; reference verifier not opened; answer-exposed (the 1.4.1 verifier was scored on the 87 first); 66, 68 and 77 corrected after seeing them
Tetsurohhori Python, cryptography 01-90 at cb7e8f1 90/90 comment in x402-foundation/tsc#4; CORRECTION-profile142-reading3.md v142.1 unchanged since it was frozen; 88-90 as pinned
babyblueviper1 Python, cryptography 01-57 at c63208d 57/57 comment in x402-foundation/tsc#4; earlier run, 01-55 at 6b2f730 (55/55): upstream_vectors_1_4_1_55_run.txt as in the earlier row: written from the text; reference not opened; expected objects visible while the inputs were read
babyblueviper1 Python, cryptography 01-90 at cb7e8f1 90/90 run (comment); the checker frozen for 1.4.1 (c63208d) gives 68/90 on the same set: run updated from the README step 10 text; reference verifier not opened; two readings marked ASSUMED in the code agree with the reference verifier
robertolocatelli81-dev Python 01-47 at 06d3b94 47/47 (441/441 fields) results/score_default.txt written from the text; reference verifiers not opened
robertolocatelli81-dev Python 01-90 at cb7e8f1 90/90 (821/821 fields) comment in x402-foundation/tsc#4; results/score_default.txt (01-87), 88-90 in tests/test_profile142.py war-verify 0.6.0; written from the 1.4.2 rule text; reference verifier not opened for 0.6.0; answer-exposed (0.5.0 scored on the 87 first; expected of 58-90 read, and the inputs of 84-85 for receipt.tools); three results changed after the clarification; two exceptions carried from 0.5.0: the receipt.payment form taken from the reference verifier/check_payment.py at bc5a138, and the reference verifier/war_verify.py at bc5a138 searched once with grep and run as a black box
actaseal Python 01-90 at cb7e8f1 90/90 run-cb7e8f1-90.txt (comment); the checker frozen for 1.4.1 (written from the text and vectors at bc5a138) gives 72/90 on the same set, each disagreement on a rule 1.4.2 introduces updated from the README step 10 text; reference verifier not opened; answer-exposed (the frozen run came first; the descriptions of 66, 68, 70, 76 and 88 read); one reading fixed after the first run: a key_id of null is not a stated key, an empty string is
actaseal Python, standard-library Ed25519 01-57 at bc5a138; 01-59 with PR #1 57/57; 59/59 comment in x402-foundation/tsc#4; run and per-mutant results written from the text; reference verifiers not opened
reference (this repository) Python standard library 01-90 90/90 python -I verifier/war_verify.py vectors/*.json written with the vectors

The other direction: receipts issued by another checker under its own key (babyblueviper1's invinoveritas checker) pass the reference verifier unchanged, with --tsa-roots set to the FreeTSA and DigiCert roots of vectors 31 and 32 (run); one of them is vector 56.

To be listed, open an issue here that links your code and a run file at a published commit.

Changelog

Profile 1.4.2 (2026-10-27; schema identifier unchanged). Closes readings that let a malformed or ambiguous input pass. Vectors 01-57 and their outcomes are unchanged; vectors 58-59 (contributed by Tetsurohhori, CC0) and 60-87 are added. Canonical base64 only; a key identifier listed more than once is KEY_REJECTED (60-63, 75, 79). A SHA-256 digest in a record is 64 lowercase hex digits (67). The form of genTime, millisecond precision, rounding and a strict comparison (66, 68, 70-74, 78). Stated times are RFC 3339 UTC; new problem code RECEIPT_TIME_UNREADABLE (76). Independence is checked by key when a key identifier is stated, and keys are compared as decoded bytes (65, 69, 80-82, 87). A record or input of the wrong type is refused (77). CHECKER_NOT_SIGNER compares with the signed key_id, never the envelope's (64, 86). Unsigned attributes of a token's SignerInfo are not read (83). Cases in 60 and 64-68 were proposed by actaseal; 84-87 were contributed by actaseal. A profile for composing a record with a payment-and-delivery record is under composition/. Clarification (2026-10-27; no change to the reference verifier or to any earlier outcome): vectors 88-90 pin two readings of the 1.4.2 text, stated by Tetsurohhori: a non-canonical token or criteria file is a mismatch, never a refusal, and committed_at is read for RECEIPT_TIME_UNREADABLE only when step 5 uses it. Profile 1.4.1 (2026-10-21; schema identifier unchanged). Settles readings raised by clean-room verifiers of 1.4 (Tetsurohhori; robertolocatelli81-dev) in x402-foundation/tsc#4; vectors 01-36 and their outcomes are unchanged. The contentType signed attribute must equal eContentType (39). The signer and every issuer other than the anchor must be valid at genTime, and a chain that reaches the anchor but fails a check is failed (38, control 37). RSA PKCS#1 v1.5 and ECDSA P-256/P-384 are required, RSASSA-PSS is optional (40, via the new vector field tsa_signature_accepted), P-256 (41), other curves unsupported (42). An empty anchor list is no anchors (43). Several defects: failed if any check that can run fails, else no_signer, unsupported, untrusted (44-46). Not exercised by a live authority: RSASSA-PSS and ECDSA P-256. Revocation is not checked. No further outcome changes to 1.4.1 before 2026-11-21; reports of disagreement are collected and answered together. Added 2026-10-21, outcome-neutral: the optional body member payment = {protocol: "x402", scheme: "exact", network: "eip155:<chain id>", asset, payer, pay_to, amount, nonce} (optionally settlement_tx), inside the signed and timestamped body; (network, asset, payer, nonce) names one settlement (vector 47, verifier/check_payment.py; asked for by babyblueviper1 from a live record paid on Base). Clarified 2026-10-21, outcome-neutral (from babyblueviper1's run): the anchor is matched by subject name and public key, not by certificate bytes (vector 32 separates the readings); vectors 48 and 49 exercise the id-kp-timeStamping and CA-flag rules. Clarified 2026-10-22, outcome-neutral for every earlier vector (from Tetsurohhori and robertolocatelli81-dev): imprint, genTime and accuracy are read from the token's signed TSTInfo only, never from other bytes of a response (50, 51); numbers follow RFC 8785's ECMAScript form, including non-integers (52). The reference verifier follows both from this version. Clarified 2026-10-22, outcome-neutral for every earlier vector (from robertolocatelli81-dev): an anchor with the issuer's name but another key does not match (53); an issuer that fails a check is failed even when no chain reaches an anchor (54); the payment form is stated and a member outside it changes no outcome (55). An anchor is not checked for validity at genTime; keys outside RSA PKCS#1 v1.5, RSASSA-PSS and ECDSA P-256/P-384 (for example Ed25519) are unsupported. Added 2026-10-23, outcome-neutral: vector 56 (contributed by babyblueviper1), a receipt issued by another checker under its own key; vector 57, integer-valued doubles and negative zero (from stillmarcus24). Edited 2026-10-23: the descriptions of vectors 37-42, 44-46, 48, 49, 53 and 54 now point to the provenance on the spec site; inputs and expected outcomes are unchanged. Profile 1.4 (2026-10-20; schema identifier unchanged). Adds the authority-signature outcome tsa_signature (step 9), additive and not_evaluated without anchors, so vectors 01-30 and existing verifiers are unchanged; vectors 31-36 from live FreeTSA (ECDSA P-384) and DigiCert (RSA) tokens. Profile 1.3 (2026-10-18; schema identifier unchanged). Resolves the questions from clean-room verifiers by Tetsurohhori and robertolocatelli81-dev and a second run by babyblueviper1 (x402-foundation/tsc#4): step 2 names the signed receipt.key_id and adds the canonical, prime-order key rule (KEY_REJECTED); step 5's reference time is the earlier of issued_utc and a covering token's genTime; imprints must be SHA-256 by OID; accuracy widens comparisons, ordering ignored; a token over other bytes falls back with CRITERIA_COMMITMENT_UNVERIFIED; PRODUCER_UNDECLARED follows the parties block; step 1 refusal shape; authority signature reported separately; vectors 12-27 added (12b replaces 12; Tetsurohhori's SHA-1 vector superseded by 23). Clarified 2026-10-18: the key rule applies to the named signing key only; vector 28 added; outcomes for 01-27 unchanged. Added 2026-10-19: vectors 29-30, the first whose tokens state an accuracy; the reference verifier in verifier/. Profile 1.2.1 (2026-10-17; schema identifier unchanged). Resolves ambiguities found by an independent clean-room review (babyblueviper1): CRITERIA_COMMITTED_AFTER_CHECK is a problem; the token over criteria.sha256 is authoritative for commitment time (CRITERIA_TIME_MISMATCH, fallback with CRITERIA_COMMITMENT_UNVERIFIED); verdict rule completed (required fail wins, VERDICT_MISMATCH); reference time uses the record's token only when it covers the record; vectors 06-11 added. Profile 1.2. First draft, vectors 01-05.

Status

Draft, offered for the evidence-record discussion in x402-foundation/tsc#4 and referenced by ACP SEP #303 (Section 7.4a). Independent clean-room implementations are welcome; please report any disagreement with a vector's expected outcome as an issue.

Licence: text CC BY 4.0; vectors and verifier code CC0 1.0.

About

Draft profile + CC0 test vectors for independent work-acceptance records

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages