Skip to main content

Algorithm whitelist and alg=none rejection

The rule

Every JWT/JWS verifier in the system (authorization server, MCP SDK, merchant SDK, wallet portal, storefront) MUST refuse alg=none (RFC 7515 §10.6) and enforce an explicit allow-list:

ContextAllow-listReject
Tokens the authorization server issues (JWT-AT, ID Token, mandate SD-JWT VC, software_statement)EdDSA (Ed25519)everything else for new tokens
Agent / Merchant DPoP proofs and client_assertionEdDSA, ES256RS256 (unless legacy use case), RSA1_5, none
RFC 9421 signatures (Offer, Catalog)ed25519, ecdsa-p256-sha256hmac-sha256, rsa-pss-sha512
Externally introduced JWTs (federation partners)EdDSA, ES256, RS256 (≥2048)none, RSA1_5, RS256 < 2048

Mandatory tests: test_jwt_alg_none_rejected, test_jwt_rs256_rejected_outside_legacy, test_dpop_proof_hmac_rejected.

Why alg=none rejection matters

A verifier that respects the JWT header's alg claim without an allow-list will accept a forged token whose header declares alg=none and whose body is whatever the attacker wants. This is the canonical JWT pitfall (RFC 7515 §10.6). Every OID4Pay verifier enforces an explicit allow-list before signature verification.

Implementation pattern (Node)

import { jwtVerify } from "jose";

const { payload } = await jwtVerify(token, jwks, {
  algorithms: ["EdDSA"], // explicit allow-list
  issuer: "https://as.oid4pay.com",
  audience: "https://shop.alpacanica.com",
  typ: "at+jwt",
});

Implementation pattern (Python)

from joserfc import jwt
from joserfc.jwk import KeySet

claims = jwt.decode(
    token,
    KeySet.import_key_set(as_jwks),
    algorithms=["EdDSA"],  # explicit allow-list
)
claims.validate(now=time.time())

Implementation pattern (Go)

import "github.com/oid4pay/oid4ac-go/jwtverify"

claims, err := jwtverify.Verify(token, jwks,
    jwtverify.WithAlgorithms("EdDSA"),
    jwtverify.WithIssuer("https://as.oid4pay.com"),
    jwtverify.WithAudience("https://shop.alpacanica.com"),
    jwtverify.WithTyp("at+jwt"),
)

SOPS key-generator default

The OID4Pay infra script infra/scripts/generate_oid4ac_keys.sh defaults to EdDSA; it emits ES256 only on explicit --alg=ES256 flag. Generating an RSA key requires --alg=RS256 --i-know-what-i-am-doing.

Mandatory test cases

Every SDK and every service ships these three tests in CI: