Skip to content

SECENG-13957: feat: add ML-DSA-44/65/87 post-quantum key support - #1445

Merged
mitch292 merged 1 commit into
cloudflare:masterfrom
ang-cloudflare:ang/SECENG-13957
Sep 28, 2026
Merged

mitch292 merged 1 commit into
cloudflare:masterfrom
ang-cloudflare:ang/SECENG-13957

Conversation

@ang-cloudflare

@ang-cloudflare ang-cloudflare commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Add ML-DSA (FIPS 204) key generation and signature algorithm support using Go 1.27 crypto/mldsa. This enables the explicit mldsa44, mldsa65, and mldsa87 KeyRequest algorithm values for post-quantum certificate authority generation.

ML-DSA-44 is the downstream default when an ML-DSA variant is omitted. cfssl continues to accept only explicit parameter-set names; the omitted-selection default belongs in the calling service.

Changes

Core ML-DSA support

  • Generate ML-DSA-44/65/87 keys and select the matching X.509 signature algorithm.
  • Marshal private keys as seed-only PKCS#8 and parse them through the existing DER helpers.
  • Support ML-DSA certificates in bundling, CA renewal, key metadata, and display helpers. Bundles that contain an ML-DSA key get an ML-DSA ubiquity warning instead of the ECDSA one.
  • Preserve errors for unsupported algorithms without falling back to RSA or ECDSA.
  • Drop key usages that RFC 9881 forbids for ML-DSA keys (keyEncipherment, dataEncipherment, keyAgreement, encipherOnly, decipherOnly) when a signing profile is applied, so the default profile gives ML-DSA certificates digitalSignature only. If none of a profile's key usages are valid for the key, signing fails with NoKeyUsages even when the profile also lists extended key usages. Without that check, the certificate would be issued with no keyUsage extension.
  • multirootca continues to reject ML-DSA roots, now at startup. Before this change, derhelpers could not parse ML-DSA keys, so config parsing already failed. Now that derhelpers accepts them, loadSigners returns an error instead of registering a nil signer.

Verification

  • Cover key generation, CSR creation, PKCS#8 round trips, signature selection, self-signed CA creation, renewal, and bundling for all three parameter sets.
  • Round-trip the RFC 9881 ML-DSA-44 example private key.
  • Generate, parse, and verify ML-DSA-issued CRLs for all three parameter sets.
  • Run focused ML-DSA tests with GOFIPS140=latest and GODEBUG=fips140=on.

Go 1.27 migration

  • Raise the module minimum from Go 1.20 to Go 1.27. crypto/mldsa and the x509.MLDSA* constants only exist in Go 1.27. A //go:build go1.27 split with stub files could keep an older floor, but it needs paired files across seven packages. This PR takes the simpler 1.27 floor. Maintainers, please say if you would prefer the split.
  • Pin godebug x509negativeserial=1 in go.mod. Raising the go line flips this default, and the EC-ACC root in cloudflare/cfssl_trust has a negative serial. Without the pin, cfssl bundle fails with "Failed to parse root certificate" and cfssl serve disables its sign, newcert, and bundle endpoints. The pin applies only to cfssl's own binaries and tests; library importers follow their own go line.
  • Build the Docker images with golang:1.27 and update the README minimum.
  • Update CI to Go 1.27 and migrate the golangci-lint config to v2 (govet only, which excludes the scan/crypto fork and one pre-existing struct tag issue). Remove the deleted x509sha1 GODEBUG override and fix Go 1.27 compatibility and vet failures.
  • Change the -min-tls-version help text to the Go 1.27 default (1.2). Map "1.3" to TLS 1.3 in helpers.StringTLSVersion; it used to fall through to TLS 1.0.

Release notes

  • Go 1.27 is the new minimum.
  • Other defaults that change with the go line for cfssl binaries: X25519MLKEM768 is enabled by default, SHA-1 TLS signatures are disabled, RSA keys under 1024 bits are rejected, and custom crypto rand readers are ignored.
  • Go 1.27 removed the tls10server, tlsrsakex, and tls3des settings. cfssl serve and multirootca now require TLS 1.2 by default and no longer offer RSA key exchange or 3DES. cfssl serve -min-tls-version 1.0 still allows TLS 1.0 and 1.1, but nothing restores RSA key exchange or 3DES. Outbound TLS (cfssl bundle -domain, cfssl certinfo -domain) changes the same way.
  • -min-tls-version 1.3 now sets TLS 1.3. It used to set TLS 1.0.
  • multirootca rejects ML-DSA roots at startup.
  • ML-DSA certificates never carry keyEncipherment, dataEncipherment, keyAgreement, encipherOnly, or decipherOnly, even when the profile lists them. A request fails with NoKeyUsages if none of the profile's key usages are valid for ML-DSA.
  • Suggested version: v1.7.0. No exported API is removed or changed. signer.KeyUsageForPublicKey is new, and helpers.StringTLSVersion now maps "1.3" to TLS 1.3.

Context

  • Jira: SECENG-13957
  • Current spec: PQC ML-DSA managed-CA generation in COMS
  • Regenerate testdata and adapt tests for golang 1.24+ #1434 (merged) regenerated the SHA-1 test fixtures, so the full suite runs on Go 1.27.
  • Production qualification uses the Cloudflare Go 1.27.1-1 or newer package with GOFIPS140=latest; this public workflow verifies compatibility with the corresponding upstream FIPS mode.
  • Downstream scope: COMS selection/defaulting, KDL/GKA wrapping, and the wrapped-key /gencrl endpoint remain integration responsibilities outside this repository.
  • Not in scope: hybrid/composite mldsa44p256 certificates or CSR-signed PQ leaf certificates.

@ang-cloudflare
ang-cloudflare marked this pull request as draft August 26, 2026 23:08

@ang-cloudflare ang-cloudflare left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scout Review

Comment — 3 findings worth addressing, none blocking.

The core ML-DSA support (key generation, sig algo mapping, PKCS#8 serialization, DER parsing) is correctly implemented. These are cross-boundary gaps in subsystems the PR doesn't touch yet — fixing them here keeps the feature internally consistent.

See inline comments for details.

Comment thread helpers/helpers.go
Comment thread initca/initca_test.go
Comment thread helpers/helpers.go
Comment thread bundler/bundle.go Outdated
Comment thread bundler/bundle.go
Comment thread helpers/helpers.go
Comment thread cmd/multirootca/ca.go
Comment thread csr/csr.go
@rphillips

Copy link
Copy Markdown

Could we get a release with this feature? It would be super helpful for the Kubernetes project.

Comment thread csr/csr.go

@mitch292 mitch292 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, but can you please squash the commits

@mitch292

mitch292 commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Also can we update the readme now that latest requires go1.27 - https://github.com/cloudflare/cfssl/blob/master/README.md - there are a few references to go 1.20

Add ML-DSA (FIPS 204) key generation and X.509 signature support using
Go 1.27 crypto/mldsa for the explicit mldsa44, mldsa65, and mldsa87
KeyRequest algorithms.

- Marshal ML-DSA private keys as seed-only PKCS#8 and parse them
  through derhelpers.
- Support ML-DSA in bundling, CA renewal, key metadata, and display
  helpers. Bundles with an ML-DSA key get an ML-DSA ubiquity warning
  instead of the ECDSA one.
- Drop the key usages RFC 9881 forbids for ML-DSA keys, and fail with
  NoKeyUsages when none of the profile's key usages remain.
- Return an error instead of panicking on unexpected CSR key types.
- multirootca rejects ML-DSA roots at startup instead of registering a
  nil signer.

Go 1.27 migration:
- Raise the module minimum to Go 1.27 and pin
  godebug x509negativeserial=1 so the negative-serial root in
  cfssl_trust still parses.
- Build the Docker images and CI with Go 1.27, migrate the
  golangci-lint config to v2, add a FIPS 140 ML-DSA test step, and
  update the README minimum.
- Replace the README's `go get` install for mkbundle, which fails
  outside a module, with `go install ...@latest`, and drop the go.rice
  static-build steps now that the web assets use go:embed.
- Map -min-tls-version 1.3 to TLS 1.3 and document the Go 1.27 TLS 1.2
  default.
@ang-cloudflare

Copy link
Copy Markdown
Contributor Author

Squashed into one commit (05f2d4f).

The README's Go 1.20 references now say 1.27. I also fixed the mkbundle install command (go get → go install …@latest) and replaced the old go.rice section, since the assets use go:embed. No other 1.20 references are left in the repo.

@mitch292
mitch292 merged commit 7d5fdbb into cloudflare:master Sep 28, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants