wso2 / wso2/api-platform

PQC: RS256 JWT login-token signing has no ML-DSA/hybrid migration path

Open
#3,450 1 comment 0 reactions 1 assignee View on GitHub

@tharindu1st is already working on this.

Since Sep 15, 2026.

Dominant language
Go
Stars
71
Forks
111
Avg merge
1d 14h
Merged PRs (30d)
110

Description

Summary

platform-api's local JWT auth (auth.mode = "internal_token" / "file") signs and verifies tokens with RS256 only, with no PQC/hybrid migration path — per .claude/rules/post-quantum-cryptography.md directive 1, the only prior acknowledgement of this was an inline // TODO(pqc): migrate comment, which the rule explicitly treats as equivalent to shipping the violation unfixed. This issue is the tracked remediation directive 1 requires in place of that comment.

Code:

  • platform-api/config/config.go — the JWT config struct (now documents the gap properly instead of a bare TODO; see the PR removing the comment) and LoadPublicKey/LoadPrivateKey, which only parse PEM-encoded RSA keys.
  • platform-api/internal/handler/auth_login.goLogin signs with jwt.SigningMethodRS256 unconditionally.
  • platform-api/internal/middleware/auth.govalidateLocalJWT only accepts RS256/RS384/RS512 (*jwt.SigningMethodRSA).
  • platform-api/internal/server/server.go — wires the RSA public key into both the internal_token and file-mode middleware.

Key generation/provisioning is equally RSA-only, so a real fix isn't just the Go code — every path that provisions the keypair mounted at auth.jwt.public_key_file/private_key_file also hardcodes openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 with no alternative:

  • platform-api/scripts/setup-local-dev.sh
  • distribution/all-in-one/docker-compose.yaml (platform-api-jwtkeygen init-container)
  • tests/integration-e2e/*.yaml, tests/ai-workspace-cli-e2e/docker-compose.yaml
  • kubernetes/helm/api-portal-helm-chart/generate-secrets.sh, kubernetes/helm/ai-workspace-helm-chart/generate-secrets.sh
  • portals/scripts/setup.sh, samples/mcp-registry-catalog/setup.sh, portals/api-portal/it/Makefile, portals/api-portal/docs/administer/manage-organizations.md

Why this isn't fixed in the same PR that removes the TODO comment

Implementing real ML-DSA-65 (FIPS 204) signing requires a Go library — the standard library's crypto/internal/fips140/mldsa is unexported/unusable from application code. The only viable option found (github.com/cloudflare/circl) is not currently an approved dependency, and adding it needs to go through the standard vetting path this repo requires for any new Go dependency before it can be used in a security-critical auth path — see .claude/rules/dependency-management.md (latest-version check, govulncheck, go-licenses check, transitive-dependency diff, all before opening the PR that adds it) and the wso2/engineering-governance approved-dependency-registry gate in CI. That is real, separate work and shouldn't be bundled silently into the fix that removes the TODO.

Scope of the full fix (tracked here)

  1. Get an ML-DSA-65 (FIPS 204) Go library through the dependency-management.md vetting process and the engineering-governance registry. Leading candidate: github.com/cloudflare/circl's sign/mldsa/mldsa65 package (the FIPS-204-final API — not sign/dilithium/mode3, which is the older, differently-encoded pre-standardization round-3 submission).
  2. Add Auth.JWT.Algorithm config ("RS256" default / "ML-DSA-65"), implement a jwt.SigningMethod for ML-DSA-65, and wire it through config.go (LoadPublicKey/LoadPrivateKey), auth_login.go (signing), internal/middleware/auth.go (validateLocalJWT's explicit algorithm allowlist — GO-AUTH-002/directive 6, never inferred from the token header), and internal/server/server.go.
  3. Update every key-generation script listed above to be able to mint an ML-DSA-65 keypair as an alternative to RSA-2048, and document the new auth.jwt.algorithm option.
  4. Account for ML-DSA-65's larger key/signature sizes (~1952B public key, ~3309B signature) in any relevant size budgets (post-quantum-cryptography.md directive 4).
  5. Once tooling and ecosystem support are in place, evaluate flipping the default from RS256 to ML-DSA-65.

RS256 remains the correct, documented, config-gated fallback in the meantime per directive 1 — this is a phased migration, not a regression.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.