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)

The Go SDK enforces the allow-list internally: VerifyMandate accepts only EdDSA on the mandate issuer JWT and returns an offer_alg_rejected or invalid_mandate_signature error for anything else. The caller does not pass an algorithm option; the whitelist is not configurable.

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

verified, err := oid4ac.VerifyMandate(
    presentation,                  // compact SD-JWT VC + KB-JWT
    "https://shop.alpacanica.com", // merchant audience
    oid4ac.VerifyMandateOptions{
        JWKSURL:        "https://as.oid4pay.com/.well-known/jwks.json",
        ExpectedIssuer: "https://as.oid4pay.com",
    },
)
if err != nil {
    var verr *oid4ac.VerifyError
    if errors.As(err, &verr) {
        // verr.Code is the stable reason, eg invalid_mandate_signature
    }
    return err
}

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: