Organization: PQSafe Inc.
Status: Informational
Published: 2026-05-05
License: Apache-2.0
Submission: FIDO Agentic Auth TWG (pending)
Contact: [email protected]

AP2-PQ Profile v1: Post-Quantum Extension to AP2 for Agentic Payment Authorization

draft-chau-ap2-pq-spend-envelope-00  ·  https://pqsafe.xyz/spec/ap2-pq-v1/

This document is the first published implementation profile of AP2 for post-quantum digital signatures. It specifies the PQ-SignedSpendEnvelope wire format, the ML-DSA-65 signing and verification protocol, and NIST ACVP-aligned conformance test vectors for autonomous AI agent payment authorization. It is not a claim of ownership over the AP2 specification itself; AP2 is housed within the FIDO Alliance.

Abstract

The Agent Protocol v2 (AP2) currently mandates ECDSA P-256 for SpendEnvelope signing. Shor's algorithm renders ECDSA vulnerable to a cryptographically relevant quantum computer (CRQC). This document specifies the AP2-PQ profile: an extension that registers ML-DSA-65 (NIST FIPS 204, Category 3) as the required post-quantum signature algorithm for AP2 spend envelopes.

The profile defines: the PQ-SignedSpendEnvelope JSON wire format and JSON Schema (Draft 2020-12); JCS canonicalization per RFC 8785; the SHA-256 fingerprint construction; ML-DSA-65 signing and 7-check verification procedures; a three-phase ECDSA-to-PQ migration path; and six NIST ACVP-aligned conformance test vectors (five positive, one negative). Implementations following this specification achieve at least 128 bits of post-quantum security for all AP2 agent-to-agent payment authorization flows.

This specification has been proposed to the FIDO Alliance Agentic Authentication TWG (formed 2026-04-28) for consideration as an Informational extension to the AP2 protocol. Formal submission pending paid membership.

1. Status of this Memo

This document is an Informational specification published by PQSafe Inc. It is not an IETF Standard, nor is it a FIDO Alliance Recommendation at this time. It has been proposed to the FIDO Alliance Agentic Authentication Technical Working Group for consideration via open letter (May 2026).

Implementers are advised that this document is a working specification and may be updated. Any party implementing the AP2-PQ profile should reference this URL (https://pqsafe.xyz/spec/ap2-pq-v1/) to ensure they are using the canonical version.

The machine-readable conformance test vectors are published separately at https://pqsafe.xyz/spec/ap2-pq-test-vectors-v1.json. Implementations must test against the live JSON file, which is the normative source for all six vectors.

2. Conventions

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.

All byte sequences in this document are encoded as lowercase hexadecimal unless otherwise noted. Integer byte lengths are exact. Base64url refers to the URL-safe encoding of RFC 4648 §5 without padding.

3. Terminology

AP2
Agent Protocol v2. The FIDO Alliance standard for autonomous AI agent payment authorization, originally contributed by Google in April 2026. Uses JCS-canonicalized SpendEnvelopes with cryptographic signatures.
AP2-PQ profile
This specification. The extension to AP2 that adds ML-DSA-65 as the mandatory post-quantum signature algorithm for SpendEnvelopes.
Mandate
An authorization token issued by a human (the Issuer) to an AI agent, granting permission to execute payments within defined bounds. Encoded as a PQ-SignedSpendEnvelope under this profile.
SpendEnvelope
The AP2 top-level JSON object representing a payment instruction and its authorization constraints. Contains the agent identity, amount, currency, allowed recipients, validity window, nonce, and signature.
PQ-SignedSpendEnvelope
A SpendEnvelope in which the signature object uses the ap2-mldsa65 algorithm identifier as defined in this document, with an ML-DSA-65 signature over the AP2 Fingerprint.
Issuer
The human or system that creates and signs a SpendEnvelope using their ML-DSA-65 secret key, granting a bounded payment mandate to an Agent.
Agent
The autonomous AI system that carries the signed SpendEnvelope and presents it to a Verifier when initiating a payment on the Issuer's behalf.
Verifier
The payment rail gateway or authorization service that receives the SpendEnvelope, performs ML-DSA-65 signature verification, enforces the 7 policy checks, and executes or rejects the payment.
AP2 Fingerprint
The 32-byte SHA-256 digest of the JCS-canonical form of the SpendEnvelope with the signature member absent. This is the exact byte sequence that ML-DSA-65 signs and verifies.
Audit Trail
An append-only, hash-chained log of verified SpendEnvelope events. Each entry references the fingerprint and execution outcome of a preceding entry to form an immutable chain.
Nonce
A 32-byte cryptographically random value, encoded as 64 lowercase hex characters, included in every SpendEnvelope. Used for replay protection: Verifiers must reject envelopes with a previously seen nonce.
ML-DSA-65
The Module-Lattice-Based Digital Signature Algorithm at NIST Category 3, specified in NIST FIPS 204. Formerly CRYSTALS-Dilithium3. Public key: 1,952 bytes. Signature: 3,309 bytes. OID: 2.16.840.1.101.3.4.3.18.
CRQC
Cryptographically Relevant Quantum Computer. A quantum machine with sufficient qubit count and fault-correction overhead to execute Shor's algorithm against deployed 256-bit elliptic curve keys within a practical timeframe.
JCS
JSON Canonicalization Scheme. RFC 8785. Deterministic UTF-8 serialization of JSON objects via recursive Unicode code-point key sorting and ECMAScript number formatting.

4. Protocol Overview

The AP2-PQ profile introduces a post-quantum cryptographic layer to the AP2 spend envelope lifecycle. The Issuer signs the envelope offline using their ML-DSA-65 key pair. The Agent carries the signed envelope and presents it at the payment rail gateway (Verifier). The Verifier performs signature verification and policy enforcement before executing the payment.

  ┌─────────────────────────────────────────────────────────────────────┐
  │                     AP2-PQ PROTOCOL FLOW                           │
  └─────────────────────────────────────────────────────────────────────┘

   ┌──────────┐     ┌──────────────────────────────────────────────────┐
   │          │     │  SpendEnvelope                                   │
   │  ISSUER  │────▶│  { issuer, agent, max_amount, currency,         │
   │ (Human)  │sign │    allowed_recipients, valid_from, valid_until,  │
   │          │     │    nonce, rail,                                  │
   └──────────┘     │    signature: { alg: "ap2-mldsa65",             │
        │           │                 value: <ML-DSA-65 sig, 3309B>,  │
        │           │                 dsaPublicKey: <pk, 1952B> }     │
        │           └─────────────────────────────────────────────────┘
        │                              │
        │           ┌──────────────────▼─────────────────────────────┐
        │           │               AGENT                            │
        │           │  Carries signed envelope; presents to Verifier │
        │           └──────────────────┬─────────────────────────────┘
        │                              │
        │           ┌──────────────────▼─────────────────────────────┐
        │           │            VERIFIER                            │
        │           │  before_tool_call hook                         │
        │           │                                                │
        │           │  1. Recompute fingerprint (JCS → SHA-256)      │
        │           │  2. ml_dsa65.verify(sig, fingerprint, pubkey)  │
        │           │  3–7. Policy checks (see §7)                   │
        │           └──────────────────┬─────────────────────────────┘
        │                              │
        │           ┌──────────────────▼─────────────────────────────┐
        │           │               RAIL                             │
        │           │  Airwallex · Wise · Stripe · USDC-Base · x402  │
        │           └────────────────────────────────────────────────┘

4.1 Why ML-DSA-65

ML-DSA-65 (NIST Category 3) is selected as the mandatory algorithm for the following reasons:

4.2 Key Sizes — Quick Reference

Component Raw bytes Hex chars Base64url chars
ML-DSA-65 public key (dsaPublicKey)1,9523,904~2,603
ML-DSA-65 secret key4,0328,064~5,376
ML-DSA-65 signature (value)3,3096,618~4,412
ECDSA P-256 public key (legacy)64128~86
ECDSA P-256 signature (legacy)64128~86

5. Wire Format — JSON Schema (Draft 2020-12)

A PQ-SignedSpendEnvelope is a JSON object. The following JSON Schema (Draft 2020-12) is the normative definition. Conformant implementations must produce and accept objects that validate against this schema.

Note on encoding: in this profile, signature.value and dsaPublicKey are encoded as lowercase hex strings (base16, RFC 4648), not Base64url. This matches the live test vectors at ap2-pq-test-vectors-v1.json. The signing procedure in §6 uses hex encoding throughout.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://pqsafe.xyz/schemas/pq-signed-spend-envelope-v1.json",
  "title": "PQ-SignedSpendEnvelope",
  "description": "AP2 SpendEnvelope with ML-DSA-65 post-quantum signature (AP2-PQ Profile v1)",
  "type": "object",
  "required": [
    "issuer", "agent", "max_amount", "currency",
    "allowed_recipients", "valid_from", "valid_until",
    "nonce", "rail", "signature"
  ],
  "additionalProperties": false,
  "properties": {
    "issuer": {
      "type": "string",
      "pattern": "^pq1[0-9a-f]{64}$",
      "description": "Issuer identity string. Format: 'pq1' prefix + 64 lowercase hex chars
                      (Keccak-256 of the raw ML-DSA-65 public key)."
    },
    "agent": {
      "type": "string",
      "description": "Agent identity string. Identifies the authorized agent."
    },
    "max_amount": {
      "type": "number",
      "exclusiveMinimum": 0,
      "description": "Maximum authorized transaction amount."
    },
    "currency": {
      "type": "string",
      "pattern": "^[A-Z]{3}$",
      "description": "ISO 4217 three-letter currency code (e.g., 'USD', 'HKD')."
    },
    "allowed_recipients": {
      "type": "array",
      "items": { "type": "string" },
      "minItems": 1,
      "description": "Allowlist of permitted payment recipient identifiers.
                      The actual recipient MUST match one entry exactly."
    },
    "valid_from": {
      "type": "integer",
      "description": "Unix epoch timestamp (seconds) from which the envelope is valid."
    },
    "valid_until": {
      "type": "integer",
      "description": "Unix epoch timestamp (seconds) at which the envelope expires.
                      Verifiers MUST reject envelopes where now > valid_until."
    },
    "nonce": {
      "type": "string",
      "pattern": "^[0-9a-f]{64}$",
      "description": "32 bytes of cryptographically random data, hex-encoded (64 lowercase
                      hex chars). MUST be unique per envelope. Used for replay protection."
    },
    "rail": {
      "type": "string",
      "enum": ["airwallex", "wise", "stripe", "usdc-base", "x402"],
      "description": "Payment rail identifier. Only the specified rail may execute this envelope."
    },
    "signature": {
      "type": "object",
      "required": ["alg", "value", "dsaPublicKey"],
      "additionalProperties": false,
      "properties": {
        "alg": {
          "type": "string",
          "const": "ML-DSA-65",
          "description": "Algorithm identifier. MUST be 'ML-DSA-65' for this profile."
        },
        "value": {
          "type": "string",
          "pattern": "^[0-9a-f]{6618}$",
          "description": "ML-DSA-65 signature over the AP2 Fingerprint, hex-encoded.
                          Raw length: exactly 3,309 bytes / 6,618 hex chars."
        },
        "dsaPublicKey": {
          "type": "string",
          "pattern": "^[0-9a-f]{3904}$",
          "description": "ML-DSA-65 public key, hex-encoded.
                          Raw length: exactly 1,952 bytes / 3,904 hex chars."
        }
      }
    }
  }
}

5.1 Field Notes

6. Signing Protocol

The normative signing procedure for a PQ-SignedSpendEnvelope is:

  1. Construct the envelope. Build the JSON object with all required fields populated except the signature member. All string values must be valid UTF-8.
  2. JCS canonicalize (RFC 8785). Serialize the envelope using the JSON Canonicalization Scheme. JCS recursively sorts all JSON object keys in ascending Unicode code point order and uses ECMAScript Number::toString for numeric values. The result is a deterministic UTF-8 byte sequence. The signature key must be absent from the canonical form.
  3. Compute the AP2 Fingerprint.
    fingerprint = SHA-256( JCS( envelope_without_signature ) )
                  // 32 bytes
    This is the exact byte sequence that ML-DSA-65 signs.
  4. Sign with ML-DSA-65.
    sigma = ML-DSA-65.Sign(secretKey, fingerprint, ctx="")
            // Output: 3,309 bytes raw
    The context string (ctx) must be the empty string. Implementations should use the hedged variant (random rnd from a CSPRNG) for defense against fault attacks. The deterministic variant (rnd = 0x00 × 32) is also conformant.
  5. Hex-encode the signature.
    signature_hex = hex(sigma)    // 6,618 lowercase hex chars
    pubkey_hex    = hex(publicKey) // 3,904 lowercase hex chars
  6. Attach the signature object.
    "signature": {
      "alg":         "ML-DSA-65",
      "value":       "<6618 lowercase hex chars>",
      "dsaPublicKey":"<3904 lowercase hex chars>"
    }
Implementation note: Use a fully conformant JCS library. Ad-hoc key sorting or custom serialization must not be used — subtle deviations from RFC 8785 produce different byte sequences and cause verification failures. Reference implementations: json-canonicalize (npm, RFC 8785 compliant), jcs (PyPI), github.com/cyberphone/json-canonicalization (Go).

7. Verification Protocol (7 Checks)

Verifiers must perform all seven checks in the order listed before executing any payment. A failure on any check must cause the envelope to be rejected. Verifiers must not apply spend policy or business logic to an unverified envelope.

  1. Signature valid. Recompute the AP2 Fingerprint from the received envelope (with signature removed), then call:
    result = ml_dsa65.verify(
      sig       = hex_decode(envelope.signature.value),
      message   = fingerprint,  // 32-byte SHA-256
      publicKey = hex_decode(envelope.signature.dsaPublicKey)
    )
    // result must be true
    If result is false or an exception is raised, reject with AP2ERR_SIGNATURE_INVALID.
  2. Not expired. Check now < envelope.valid_until. Reject with AP2ERR_EXPIRED if the envelope has passed its expiry timestamp.
  3. Not yet valid. Check now ≥ envelope.valid_from. Reject with AP2ERR_NOT_YET_VALID if the envelope is presented before its validity window opens.
  4. Recipient in allowlist. The payment's intended recipient must appear exactly in envelope.allowed_recipients. Reject with AP2ERR_RECIPIENT_NOT_ALLOWED otherwise.
  5. Amount within mandate. The requested payment amount must not exceed envelope.max_amount. Reject with AP2ERR_AMOUNT_EXCEEDED otherwise.
  6. Nonce not replayed. Check the nonce field against a persistent seen-set scoped to the issuer. Reject with AP2ERR_NONCE_REPLAY if the nonce has been seen before. On successful verification, add the nonce to the seen-set with TTL set to valid_until.
  7. Key ID valid. Optionally, verify that dsaPublicKey is registered in the issuer's key registry and has not been revoked. If a registry is configured, reject with AP2ERR_KEY_NOT_REGISTERED or AP2ERR_KEY_REVOKED on failure. In self-contained mode (no registry), skip this check but log a warning.

7.1 Error Codes Summary

CodeCheckHTTP status suggestion
AP2ERR_SIGNATURE_INVALIDCheck 1401
AP2ERR_EXPIREDCheck 2403
AP2ERR_NOT_YET_VALIDCheck 3403
AP2ERR_RECIPIENT_NOT_ALLOWEDCheck 4403
AP2ERR_AMOUNT_EXCEEDEDCheck 5403
AP2ERR_NONCE_REPLAYCheck 6409
AP2ERR_KEY_NOT_REGISTEREDCheck 7401
AP2ERR_KEY_REVOKEDCheck 7401

8. Conformance Test Vectors

The live, machine-readable test vector file is published at:

https://pqsafe.xyz/spec/ap2-pq-test-vectors-v1.json

The JSON file is the normative source. Implementations must verify against the live file, not only the summaries below. The file contains six vectors across two test suites (sign_verify and negative).

8.1 Positive Test Cases (TC-1 through TC-5)

Five positive vectors each provide a complete PQ-SignedSpendEnvelope with known issuer key material and expected verification result true. A conformant implementation must:

  1. Recompute the AP2 Fingerprint (JCS → SHA-256) from the envelope fields excluding signature.
  2. Hex-decode signature.value (6,618 hex chars → 3,309 bytes).
  3. Hex-decode signature.dsaPublicKey (3,904 hex chars → 1,952 bytes).
  4. Call ml_dsa65.verify(sig, fingerprint, pubkey) and assert true.

TC-1 — Baseline positive PASS

Standard envelope: USD 100, Airwallex rail, 24-hour validity window. Tests the complete sign→fingerprint→verify pipeline.

TC-2 — Different amount + currency PASS

HKD 500 envelope on the Wise rail. Verifies currency field handling in JCS canonicalization.

TC-3 — Multiple allowed recipients PASS

Allowlist with three recipient identifiers. Verifies array serialization ordering in JCS canonical form.

TC-4 — Stripe rail PASS

EUR 250 on Stripe. Validates rail enum handling and cross-currency correctness.

TC-5 — x402 rail PASS

USDC 50 on x402 HTTP-native micropayment rail. Tests non-fiat currency envelope signing.

8.2 Negative Test Case (TC-N1)

TC-N1 — Modified signature byte MUST REJECT

A valid envelope with one byte of signature.value flipped (XOR 0x01 at byte index 100). A conformant implementation must return false from ml_dsa65.verify(). This vector exposes a critical defect in pqcrypto v0.4.0, which silently returned true for structurally invalid ML-DSA-65 signatures due to incomplete polynomial coefficient validation. Implementations using pqcrypto must upgrade to v0.4.1+ before claiming AP2-PQ conformance.

TC-N1 CVE context: The pqcrypto 0.4.0 silent-accept bug was discovered during development of this test suite. The bug causes certain structurally malformed ML-DSA-65 signatures to pass verification. Any implementation that passes TC-N1 is vulnerable to signature forgery attacks. See the test vector file for the exact tampered signature bytes.

9. Security Considerations

9.1 Replay Protection (Nonce + Revocation)

Each SpendEnvelope contains a 64-hex-char nonce (32 bytes of CSPRNG output). Verifiers must maintain a persistent seen-set of nonces, keyed by issuer identity, with entries retained until valid_until. An envelope submitted a second time with the same nonce must be rejected with AP2ERR_NONCE_REPLAY, even if all other checks pass. Nonce storage should be durable across Verifier restarts.

Key revocation complements nonce protection for longer-term replay scenarios. Issuers should provide a revocation endpoint or publish a revocation list. Verifiers should check revocation status on each verification request rather than caching revocation state for more than 60 seconds.

9.2 Expiry Enforcement

Verifiers must reject envelopes where the current time exceeds valid_until. Clock skew between Issuer and Verifier should be addressed by issuing envelopes with a short validity window (e.g., 5–60 minutes for single-transaction use, up to 24 hours for multi-transaction mandates) rather than by applying a tolerance window at the Verifier.

9.3 Audit Log Integrity (Hash Chain)

Verifiers should maintain an append-only audit log of verified SpendEnvelope events. Each log entry should include the AP2 Fingerprint of the envelope and the SHA-256 hash of the previous entry, forming a hash chain that makes retroactive modification detectable. For higher assurance, the chain head may be anchored to an external immutable store (e.g., a public blockchain commitment).

9.4 Key Rotation

ML-DSA-65 key pairs should be rotated at least annually for long-lived agent identities. During rotation:

9.5 Post-Quantum Migration Timeline

PhaseStart dateSender requirementReceiver requirement
Phase 1: Dual-Sign Optional2025-01MAY dual-sign (ECDSA + ML-DSA-65)SHOULD accept PQ
Phase 2: Dual-Sign Mandatory2026-01MUST include ML-DSA-65 signatureMUST accept PQ
Phase 3: PQ-Only2027-01MUST use ML-DSA-65 onlyMAY reject ECDSA-only

Operators should treat 2030 as a hard deadline for completing migration to post-quantum signing across all production AP2 deployments — the lower bound of credible CRQC estimates from published intelligence and academic assessments.

9.6 Side-Channel Resistance

Implementations must use a constant-time ML-DSA-65 library. Timing variations in signing or verification can leak secret key material. Recommended libraries with constant-time properties: @noble/post-quantum v0.2.x+ (TypeScript), Cloudflare CIRCL mldsa65 (Go), RustCrypto ml-dsa crate (Rust), pyca/cryptography ML-DSA via OpenSSL (Python — verify constant-time guarantee for the target platform).

9.7 Key Reuse

AP2 agents must not reuse their ML-DSA-65 key pair for any purpose other than AP2 SpendEnvelope signing as defined in this document. The same key pair must not be used for encryption, key encapsulation, TLS certificates, JWT issuance, or document signing without explicit cross-protocol security analysis.

9.8 DoS via Large Payloads

ML-DSA-65 signatures (3,309 bytes) and public keys (1,952 bytes) are substantially larger than ECDSA P-256 equivalents (~64 bytes each). Verifiers must enforce input length limits. The recommended maximum is 8,192 bytes per HTTP header field. Rate limiting on the authorization endpoint is recommended; ML-DSA-65 verification takes approximately 0.3 ms on modern hardware and unthrottled requests can exhaust CPU.

9.9 Harvest-Now-Decrypt-Later

Adversaries at the nation-state level are documented to be capturing signed traffic today with intent to forge signatures once a CRQC is available. AP2 SpendEnvelope records may remain legally and financially significant for years; the signature algorithm must therefore remain unforgeable over that same timescale. Adopting ML-DSA-65 now — before CRQCs exist — is the correct engineering response.

10. On-chain Verification (Audit Registry)

10.1 Architecture — Pattern A (Hash-Witness)

Full ML-DSA-65 signature verification on an EVM chain is gas-prohibitive (estimated 10–50 M gas per call — impractical at any current L2 gas price). PQSafe therefore uses Pattern A — Hash-Witness:

The contract is an audit ledger, not a cryptographic verifier. Any external party can independently verify:

  1. That a specific envelope was committed (by querying isCommitted(envelopeId)).
  2. That it has not been revoked (isRevoked(envelopeId)).
  3. That the full ML-DSA-65 signature matches the on-chain fingerprint (by re-deriving sigFingerprint = bytes32(sig[0:32]) locally and comparing against getRecord(envelopeId).sigFingerprint).
  4. That the spend amount, currency, and expiry match the original envelope payload via the indexed EnvelopeCommitted event.

10.2 Contract — SpendEnvelopeRegistryV2_1

The canonical production contract is SpendEnvelopeRegistryV2_1 (Apache-2.0). Source: pqsafe/evm/src/SpendEnvelopeRegistryV2_1.sol. It extends SpendEnvelopeRegistryV2 (AccessControl + ReentrancyGuard) with an OpenZeppelin Pausable circuit-breaker (OZ v5).

Key roles:

10.3 Deployed Addresses

Canonical address registry: spec/contracts.json (updated post-deploy). CREATE2 salt keccak256("PQSafe.SpendEnvelopeRegistryV2_1.v2.1.0.mainnet") produces the same deterministic address on Base, Arbitrum One, and Optimism.

NetworkChain IDAddressExplorer
Base Mainnet 8453 TBD — mainnet deploy pending
Arbitrum One 42161 TBD — mainnet deploy pending
Optimism 10 TBD — mainnet deploy pending
Arbitrum Sepolia (testnet) 421614 0x142bA5626bf8B032EB0B59052421C42595417F5d Arbiscan Sepolia

10.4 Gas Estimates

Deployment gas for SpendEnvelopeRegistryV2_1: approximately 1.3–1.5 M gas. Per-call gas for the hot path:

Expected deploy cost at typical L2 gas prices: ~$3–15 per chain (0.001–0.005 ETH, assuming ETH ≈ $2,500–$3,000 and L2 base fee 0.01–0.1 gwei). Raymond should ensure 0.01 ETH on each target chain before deploying.

11. IANA Considerations

This document has no IANA considerations. It is an Informational specification published by PQSafe Inc. The algorithm identifier ML-DSA-65 used in the signature.alg field is defined by this document for use within the AP2-PQ profile. No new HTTP header fields are introduced; the x-ap2-alg, x-ap2-signature, and x-ap2-pubkey HTTP headers referenced in this profile are existing AP2 headers.

12. Normative References

13. Informative References

14. Authors' Addresses

PQSafe Inc.
Hong Kong, China
Email: [email protected]
URI: https://pqsafe.xyz

Raymond Chau Yik-Chun
PQSafe Inc.
Email: [email protected]