Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 12 additions & 14 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
# starting with go 1.24 the GODEBUG=x509sha1=1 flag has been removed.
# many tests rely on sha1 certificates. After resolving #1413 we can
# run these on stable and oldstable again. Min version (1.20) can
# always be run.
go: ['1.23', '1.22', '1.20']
# ML-DSA requires Go 1.27. The SHA-1 test fixtures must be updated by
# #1434 because Go 1.24 removed the x509sha1 GODEBUG override.
go: ['1.27']
services:
# Label used to access the service container
postgres:
Expand Down Expand Up @@ -46,7 +44,6 @@ jobs:

env:
GOFLAGS: "-mod=vendor"
GODEBUG: "x509sha1=1"
BUILD_TAGS: "postgresql"
PGHOST: localhost
MYSQL_HOST: 127.0.0.1
Expand All @@ -68,6 +65,13 @@ jobs:
- run: ./bin/goose -path certdb/mysql up;
- name: Test
run: ./test.sh
- name: Test ML-DSA in FIPS mode
env:
GOFIPS140: latest
GODEBUG: "fips140=on"
run: |
go test ./csr ./signer ./helpers ./helpers/derhelpers ./crl -run MLDSA
go test ./initca -run '^TestInitCA$'
- uses: codecov/codecov-action@v4

golangci:
Expand All @@ -77,12 +81,6 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.20"
go-version: "1.27"
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
# There is a breaking change in 1.58 that causes the linter not to recognize
# internal imports or standard library imports and results in linting errors
# that cannot be ignored.
# e.g certdb/certdb.go:5:2: could not import encoding/json (Config.Importer.Import(encoding/json) returned nil but no error) (typecheck)
version: v1.57
uses: golangci/golangci-lint-action@v9
19 changes: 19 additions & 0 deletions bundler/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/mldsa"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
Expand Down Expand Up @@ -112,6 +113,21 @@ func (b *Bundle) MarshalJSON() ([]byte, error) {
keyType = "DSA"
case x509.Ed25519:
keyType = "Ed25519"
case x509.MLDSA:
if pub, ok := b.Cert.PublicKey.(*mldsa.PublicKey); ok {
switch {
case pub.Parameters() == mldsa.MLDSA44():
keyType = "ML-DSA-44"
case pub.Parameters() == mldsa.MLDSA65():
keyType = "ML-DSA-65"
case pub.Parameters() == mldsa.MLDSA87():
keyType = "ML-DSA-87"
default:
keyType = "ML-DSA"
}
} else {
keyType = "ML-DSA"
}
default:
keyType = "Unknown"
}
Expand All @@ -126,6 +142,9 @@ func (b *Bundle) MarshalJSON() ([]byte, error) {
case ed25519.PrivateKey:
keyBytes, _ = derhelpers.MarshalEd25519PrivateKey(key)
keyString = PemBlockToString(&pem.Block{Type: "Ed25519 PRIVATE KEY", Bytes: keyBytes})
case *mldsa.PrivateKey:
keyBytes, _ = x509.MarshalPKCS8PrivateKey(key)
keyString = PemBlockToString(&pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's add test that it correctly roundtrips this example private key from RFC 9881:

-----BEGIN PRIVATE KEY-----
MDQCAQAwCwYJYIZIAWUDBAMRBCKAIAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZ
GhscHR4f
-----END PRIVATE KEY-----

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added TestParsePrivateKeyDERMLDSARFC9881 — parses the RFC 9881 ML-DSA-44 seed-only PKCS#8, verifies it produces MLDSA44 parameters, marshals back to PKCS#8, and confirms the public key survives the round-trip.

case fmt.Stringer:
keyString = key.String()
}
Expand Down
10 changes: 10 additions & 0 deletions bundler/bundler.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/mldsa"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
Expand Down Expand Up @@ -597,6 +598,14 @@ func (b *Bundler) Bundle(certs []*x509.Certificate, key crypto.Signer, flavor Bu
if !(bytes.Equal(cert.PublicKey.(ed25519.PublicKey), ed25519PublicKey)) {
return nil, errors.New(errors.PrivateKeyError, errors.KeyMismatch)
}
case cert.PublicKeyAlgorithm == x509.MLDSA:
var mldsaPublicKey *mldsa.PublicKey
if mldsaPublicKey, ok = key.Public().(*mldsa.PublicKey); !ok {
return nil, errors.New(errors.PrivateKeyError, errors.KeyMismatch)
}
if !cert.PublicKey.(*mldsa.PublicKey).Equal(mldsaPublicKey) {
return nil, errors.New(errors.PrivateKeyError, errors.KeyMismatch)
}
default:
return nil, errors.New(errors.PrivateKeyError, errors.NotRSAOrECCOrEd25519)
}
Expand All @@ -605,6 +614,7 @@ func (b *Bundler) Bundle(certs []*x509.Certificate, key crypto.Signer, flavor Bu
case cert.PublicKeyAlgorithm == x509.RSA:
case cert.PublicKeyAlgorithm == x509.ECDSA:
case cert.PublicKeyAlgorithm == x509.Ed25519:
case cert.PublicKeyAlgorithm == x509.MLDSA:
default:
return nil, errors.New(errors.PrivateKeyError, errors.NotRSAOrECCOrEd25519)
}
Expand Down
52 changes: 52 additions & 0 deletions crl/crl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import (
"crypto/x509"
"os"
"testing"

"github.com/cloudflare/cfssl/csr"
"github.com/cloudflare/cfssl/helpers"
"github.com/cloudflare/cfssl/initca"
)

const (
Expand Down Expand Up @@ -75,3 +79,51 @@ func TestNewCRLFromFileWithoutRevocations(t *testing.T) {
t.Fatal("Wrong number of expired certificates")
}
}

func TestNewCRLFromFileMLDSA(t *testing.T) {
tests := []struct {
algo string
sigAlg x509.SignatureAlgorithm
}{
{algo: "mldsa44", sigAlg: x509.MLDSA44},
{algo: "mldsa65", sigAlg: x509.MLDSA65},
{algo: "mldsa87", sigAlg: x509.MLDSA87},
}

for _, tt := range tests {
t.Run(tt.algo, func(t *testing.T) {
req := &csr.CertificateRequest{
CN: "ML-DSA test CA",
KeyRequest: &csr.KeyRequest{A: tt.algo},
}
certPEM, _, keyPEM, err := initca.New(req)
if err != nil {
t.Fatalf("creating CA: %v", err)
}

crlDER, err := NewCRLFromFile([]byte("1\n"), certPEM, keyPEM, "60")
if err != nil {
t.Fatalf("creating CRL: %v", err)
}

parsedCRL, err := x509.ParseRevocationList(crlDER)
if err != nil {
t.Fatalf("parsing CRL: %v", err)
}
if parsedCRL.SignatureAlgorithm != tt.sigAlg {
t.Fatalf("signature algorithm = %v, want %v", parsedCRL.SignatureAlgorithm, tt.sigAlg)
}
if len(parsedCRL.RevokedCertificateEntries) != 1 {
t.Fatalf("revoked certificate count = %d, want 1", len(parsedCRL.RevokedCertificateEntries))
}

issuer, err := helpers.ParseCertificatePEM(certPEM)
if err != nil {
t.Fatalf("parsing CA: %v", err)
}
if err := parsedCRL.CheckSignatureFrom(issuer); err != nil {
t.Fatalf("checking CRL signature: %v", err)
}
})
}
}
26 changes: 24 additions & 2 deletions csr/csr.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/mldsa"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
Expand Down Expand Up @@ -67,7 +68,7 @@ func (kr *KeyRequest) Size() int {
}

// Generate generates a key as specified in the request. Currently,
// only ECDSA, RSA and ed25519 algorithms are supported.
// ECDSA, RSA, Ed25519, and ML-DSA (mldsa44, mldsa65, mldsa87) algorithms are supported.
func (kr *KeyRequest) Generate() (crypto.PrivateKey, error) {
log.Debugf("generate key from request: algo=%s, size=%d", kr.Algo(), kr.Size())
switch kr.Algo() {
Expand Down Expand Up @@ -98,6 +99,12 @@ func (kr *KeyRequest) Generate() (crypto.PrivateKey, error) {
return nil, err
}
return ed25519.NewKeyFromSeed(seed), nil
case "mldsa44":
return mldsa.GenerateKey(mldsa.MLDSA44())
case "mldsa65":
return mldsa.GenerateKey(mldsa.MLDSA65())
case "mldsa87":
return mldsa.GenerateKey(mldsa.MLDSA87())
default:
return nil, errors.New("invalid algorithm")
}
Expand Down Expand Up @@ -131,6 +138,12 @@ func (kr *KeyRequest) SigAlgo() x509.SignatureAlgorithm {
}
case "ed25519":
return x509.PureEd25519
case "mldsa44":
return x509.MLDSA44
case "mldsa65":
return x509.MLDSA65
case "mldsa87":
return x509.MLDSA87
default:
return x509.UnknownSignatureAlgorithm
}
Expand Down Expand Up @@ -272,7 +285,16 @@ func ParseRequest(req *CertificateRequest) (csr, key []byte, err error) {
}
key = pem.EncodeToMemory(&block)
default:
panic("Generate should have failed to produce a valid key.")
key, err = x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
err = cferr.Wrap(cferr.PrivateKeyError, cferr.Unknown, err)
return
}
block := pem.Block{
Type: "PRIVATE KEY",
Bytes: key,
}
key = pem.EncodeToMemory(&block)
}

csr, err = Generate(priv.(crypto.Signer), req)
Expand Down
106 changes: 106 additions & 0 deletions csr/csr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/mldsa"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
Expand Down Expand Up @@ -47,6 +48,10 @@ func TestKeyRequest(t *testing.T) {
if kr.Algo() != "ed25519" {
t.Fatal("Ed25519 key generated, but expected", kr.Algo())
}
case *mldsa.PrivateKey:
if kr.Algo() != "mldsa44" && kr.Algo() != "mldsa65" && kr.Algo() != "mldsa87" {
t.Fatal("ML-DSA key generated, but expected", kr.Algo())
}
}
}

Expand Down Expand Up @@ -331,6 +336,107 @@ func TestED25519Generation(t *testing.T) {
}
}

func TestMLDSAGeneration(t *testing.T) {
tests := []struct {
algo string
params mldsa.Parameters
sigAlg x509.SignatureAlgorithm
}{
{"mldsa44", mldsa.MLDSA44(), x509.MLDSA44},
{"mldsa65", mldsa.MLDSA65(), x509.MLDSA65},
{"mldsa87", mldsa.MLDSA87(), x509.MLDSA87},
}

for _, tt := range tests {
t.Run(tt.algo, func(t *testing.T) {
kr := &KeyRequest{A: tt.algo}
priv, err := kr.Generate()
if err != nil {
t.Fatalf("Generate() failed: %v", err)
}
mldsaKey, ok := priv.(*mldsa.PrivateKey)
if !ok {
t.Fatalf("expected *mldsa.PrivateKey, got %T", priv)
}
pub := mldsaKey.PublicKey()
if pub.Parameters() != tt.params {
t.Fatalf("expected parameters %v, got %v", tt.params, pub.Parameters())
}
if sa := kr.SigAlgo(); sa != tt.sigAlg {
t.Fatalf("expected SigAlgo %v, got %v", tt.sigAlg, sa)
}
})
}
}

func TestMLDSAGenerationIgnoresSize(t *testing.T) {
kr := &KeyRequest{A: "mldsa65", S: 999}
priv, err := kr.Generate()
if err != nil {
t.Fatalf("Generate() should succeed ignoring Size: %v", err)
}
if _, ok := priv.(*mldsa.PrivateKey); !ok {
t.Fatalf("expected *mldsa.PrivateKey, got %T", priv)
}
}

func TestMLDSACertRequest(t *testing.T) {
tests := []string{"mldsa44", "mldsa65", "mldsa87"}
for _, algo := range tests {
t.Run(algo, func(t *testing.T) {
req := &CertificateRequest{
Names: []Name{
{
C: "US",
ST: "California",
L: "San Francisco",
O: "CloudFlare",
OU: "Systems Engineering",
},
},
CN: "cloudflare.com",
Hosts: []string{"cloudflare.com", "www.cloudflare.com"},
KeyRequest: &KeyRequest{A: algo},
}
csrBytes, keyBytes, err := ParseRequest(req)
if err != nil {
t.Fatalf("ParseRequest() failed: %v", err)
}

// Verify key PEM block type
block, _ := pem.Decode(keyBytes)
if block == nil {
t.Fatal("failed to decode private key PEM")
}
if block.Type != "PRIVATE KEY" {
t.Fatalf("expected PEM type 'PRIVATE KEY', got %q", block.Type)
}

// Verify round-trip via PKCS#8
parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
t.Fatalf("ParsePKCS8PrivateKey() failed: %v", err)
}
if _, ok := parsed.(*mldsa.PrivateKey); !ok {
t.Fatalf("expected *mldsa.PrivateKey, got %T", parsed)
}

// Verify CSR
csrBlock, _ := pem.Decode(csrBytes)
if csrBlock == nil {
t.Fatal("failed to decode CSR PEM")
}
csr, err := x509.ParseCertificateRequest(csrBlock.Bytes)
if err != nil {
t.Fatalf("ParseCertificateRequest() failed: %v", err)
}
if csr.PublicKeyAlgorithm != x509.MLDSA {
t.Fatalf("expected PublicKeyAlgorithm MLDSA, got %v", csr.PublicKeyAlgorithm)
}
})
}
}

func TestRSAKeyGeneration(t *testing.T) {
var rsakey *rsa.PrivateKey

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/cloudflare/cfssl

go 1.20
go 1.27

require (
bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c
Expand Down
Loading