diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index e8effc22c..47ce324e7 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -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: @@ -46,7 +44,6 @@ jobs: env: GOFLAGS: "-mod=vendor" - GODEBUG: "x509sha1=1" BUILD_TAGS: "postgresql" PGHOST: localhost MYSQL_HOST: 127.0.0.1 @@ -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 ./bundler -run MLDSA + go test ./initca -run '^(TestInitCA|TestRenewMLDSA)$' - uses: codecov/codecov-action@v4 golangci: @@ -77,12 +81,8 @@ 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 + uses: golangci/golangci-lint-action@v9 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 + version: v2.13.2 diff --git a/.golangci.yaml b/.golangci.yaml index 15aa43248..dd9822f7d 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,3 +1,5 @@ +version: "2" + linters: disable: - staticcheck diff --git a/bundler/bundle.go b/bundler/bundle.go index f13e51db5..0ba808405 100644 --- a/bundler/bundle.go +++ b/bundler/bundle.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/ecdsa" "crypto/ed25519" + "crypto/mldsa" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" @@ -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" } @@ -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}) case fmt.Stringer: keyString = key.String() } diff --git a/bundler/bundler.go b/bundler/bundler.go index 733e60cfc..da247ef83 100644 --- a/bundler/bundler.go +++ b/bundler/bundler.go @@ -7,6 +7,7 @@ import ( "crypto" "crypto/ecdsa" "crypto/ed25519" + "crypto/mldsa" "crypto/rsa" "crypto/tls" "crypto/x509" @@ -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) } @@ -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) } diff --git a/bundler/mldsa_test.go b/bundler/mldsa_test.go new file mode 100644 index 000000000..72b456cf2 --- /dev/null +++ b/bundler/mldsa_test.go @@ -0,0 +1,141 @@ +package bundler + +import ( + "bytes" + "crypto/mldsa" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/json" + "encoding/pem" + "math/big" + "testing" + "time" +) + +func TestMLDSABundle(t *testing.T) { + tests := []struct { + name string + params mldsa.Parameters + sigAlgo x509.SignatureAlgorithm + keyType string + }{ + {name: "MLDSA44", params: mldsa.MLDSA44(), sigAlgo: x509.MLDSA44, keyType: "ML-DSA-44"}, + {name: "MLDSA65", params: mldsa.MLDSA65(), sigAlgo: x509.MLDSA65, keyType: "ML-DSA-65"}, + {name: "MLDSA87", params: mldsa.MLDSA87(), sigAlgo: x509.MLDSA87, keyType: "ML-DSA-87"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + key, cert := newMLDSASelfSignedCertificate(t, tt.params, tt.sigAlgo) + bundler := new(Bundler) + + bundle, err := bundler.Bundle([]*x509.Certificate{cert}, key, Force) + if err != nil { + t.Fatalf("bundling with private key: %v", err) + } + + if _, err := bundler.Bundle([]*x509.Certificate{cert}, nil, Force); err != nil { + t.Fatalf("bundling without private key: %v", err) + } + + mismatchedKey, err := mldsa.GenerateKey(tt.params) + if err != nil { + t.Fatalf("generating mismatched key: %v", err) + } + if _, err := bundler.Bundle([]*x509.Certificate{cert}, mismatchedKey, Force); err == nil { + t.Fatal("bundling accepted a mismatched ML-DSA key") + } + + encoded, err := json.Marshal(bundle) + if err != nil { + t.Fatalf("marshaling bundle: %v", err) + } + var metadata struct { + Key string `json:"key"` + KeyType string `json:"key_type"` + KeySize int `json:"key_size"` + Signature string `json:"signature"` + } + if err := json.Unmarshal(encoded, &metadata); err != nil { + t.Fatalf("unmarshaling bundle metadata: %v", err) + } + if metadata.KeyType != tt.keyType { + t.Fatalf("key type = %q, want %q", metadata.KeyType, tt.keyType) + } + if metadata.KeySize != tt.params.PublicKeySize() { + t.Fatalf("key size = %d, want %d", metadata.KeySize, tt.params.PublicKeySize()) + } + if metadata.Signature != tt.name { + t.Fatalf("signature = %q, want %q", metadata.Signature, tt.name) + } + + block, _ := pem.Decode([]byte(metadata.Key)) + if block == nil { + t.Fatal("bundle key is not PEM encoded") + } + if block.Type != "PRIVATE KEY" { + t.Fatalf("PEM block type = %q, want PRIVATE KEY", block.Type) + } + + var privateKeyInfo struct { + Version int + Algorithm pkix.AlgorithmIdentifier + PrivateKey []byte + } + rest, err := asn1.Unmarshal(block.Bytes, &privateKeyInfo) + if err != nil { + t.Fatalf("parsing PKCS#8 structure: %v", err) + } + if len(rest) != 0 { + t.Fatalf("parsing PKCS#8 structure left %d trailing bytes", len(rest)) + } + wantSeed := append([]byte{0x80, byte(mldsa.PrivateKeySize)}, key.Bytes()...) + if !bytes.Equal(privateKeyInfo.PrivateKey, wantSeed) { + t.Fatal("PKCS#8 key is not the RFC 9881 seed-only representation") + } + + parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + t.Fatalf("parsing PKCS#8 key: %v", err) + } + parsedKey, ok := parsed.(*mldsa.PrivateKey) + if !ok { + t.Fatalf("parsed key type = %T, want *mldsa.PrivateKey", parsed) + } + if !parsedKey.PublicKey().Equal(key.PublicKey()) { + t.Fatal("public key changed after PKCS#8 round-trip") + } + }) + } +} + +func newMLDSASelfSignedCertificate(t *testing.T, params mldsa.Parameters, sigAlgo x509.SignatureAlgorithm) (*mldsa.PrivateKey, *x509.Certificate) { + t.Helper() + + key, err := mldsa.GenerateKey(params) + if err != nil { + t.Fatalf("generating ML-DSA key: %v", err) + } + now := time.Now() + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: params.String()}, + NotBefore: now.Add(-time.Minute), + NotAfter: now.Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + IsCA: true, + SignatureAlgorithm: sigAlgo, + } + der, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key) + if err != nil { + t.Fatalf("creating ML-DSA certificate: %v", err) + } + cert, err := x509.ParseCertificate(der) + if err != nil { + t.Fatalf("parsing ML-DSA certificate: %v", err) + } + return key, cert +} diff --git a/cmd/multirootca/ca.go b/cmd/multirootca/ca.go index 93f5cc937..84ec4730a 100644 --- a/cmd/multirootca/ca.go +++ b/cmd/multirootca/ca.go @@ -6,6 +6,7 @@ import ( "crypto/rsa" "errors" "flag" + "fmt" "net" "net/http" @@ -42,6 +43,21 @@ func parseSigner(root *config.Root) (signer.Signer, error) { } } +func loadSigners(roots config.RootList, destination map[string]signer.Signer, destinationWhitelists map[string]whitelist.NetACL) error { + for label, root := range roots { + s, err := parseSigner(root) + if err != nil { + return fmt.Errorf("load signer %q: %w", label, err) + } + destination[label] = s + if root.ACL != nil { + destinationWhitelists[label] = root.ACL + } + log.Info("loaded signer ", label) + } + return nil +} + var ( defaultLabel string signers = map[string]signer.Signer{} @@ -66,16 +82,8 @@ func main() { log.Fatalf("%v", err) } - for label, root := range roots { - s, err := parseSigner(root) - if err != nil { - log.Criticalf("%v", err) - } - signers[label] = s - if root.ACL != nil { - whitelists[label] = root.ACL - } - log.Info("loaded signer ", label) + if err := loadSigners(roots, signers, whitelists); err != nil { + log.Fatalf("%v", err) } defaultLabel = *flagDefaultLabel diff --git a/cmd/multirootca/ca_test.go b/cmd/multirootca/ca_test.go new file mode 100644 index 000000000..b0c7e699c --- /dev/null +++ b/cmd/multirootca/ca_test.go @@ -0,0 +1,36 @@ +package main + +import ( + "crypto/mldsa" + "strings" + "testing" + + "github.com/cloudflare/cfssl/multiroot/config" + "github.com/cloudflare/cfssl/signer" + "github.com/cloudflare/cfssl/whitelist" +) + +func TestLoadSignersRejectsMLDSAWithoutRegisteringSigner(t *testing.T) { + key, err := mldsa.GenerateKey(mldsa.MLDSA44()) + if err != nil { + t.Fatalf("generating ML-DSA key: %v", err) + } + + loadedSigners := map[string]signer.Signer{} + loadedWhitelists := map[string]whitelist.NetACL{} + err = loadSigners(config.RootList{ + "unsupported": {PrivateKey: key}, + }, loadedSigners, loadedWhitelists) + if err == nil { + t.Fatal("loadSigners accepted an unsupported ML-DSA root") + } + if !strings.Contains(err.Error(), `load signer "unsupported": unsupported private key type`) { + t.Fatalf("loadSigners error = %q, want label and parseSigner error", err) + } + if len(loadedSigners) != 0 { + t.Fatalf("loadSigners registered %d signer(s) after rejecting the root", len(loadedSigners)) + } + if len(loadedWhitelists) != 0 { + t.Fatalf("loadSigners registered %d whitelist(s) after rejecting the root", len(loadedWhitelists)) + } +} diff --git a/crl/crl_test.go b/crl/crl_test.go index c1fdbf1fa..0a49dc6c4 100644 --- a/crl/crl_test.go +++ b/crl/crl_test.go @@ -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 ( @@ -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) + } + }) + } +} diff --git a/csr/csr.go b/csr/csr.go index 52039326b..5bc5046ae 100644 --- a/csr/csr.go +++ b/csr/csr.go @@ -6,6 +6,7 @@ import ( "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" + "crypto/mldsa" "crypto/rand" "crypto/rsa" "crypto/x509" @@ -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() { @@ -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") } @@ -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 } @@ -271,8 +284,21 @@ func ParseRequest(req *CertificateRequest) (csr, key []byte, err error) { Bytes: key, } key = pem.EncodeToMemory(&block) + case *mldsa.PrivateKey: + 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) default: - panic("Generate should have failed to produce a valid key.") + err = cferr.Wrap(cferr.PrivateKeyError, cferr.Unknown, + fmt.Errorf("unsupported generated private key type %T", priv)) + return } csr, err = Generate(priv.(crypto.Signer), req) diff --git a/csr/csr_test.go b/csr/csr_test.go index ebe2552a9..d17b3723b 100644 --- a/csr/csr_test.go +++ b/csr/csr_test.go @@ -5,6 +5,7 @@ import ( "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" + "crypto/mldsa" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" @@ -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()) + } } } @@ -331,6 +336,117 @@ 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 TestMLDSAUnknownVariant(t *testing.T) { + request := &KeyRequest{A: "mldsa99"} + if _, err := request.Generate(); err == nil { + t.Fatal("Generate() accepted an unknown ML-DSA variant") + } + if got := request.SigAlgo(); got != x509.UnknownSignatureAlgorithm { + t.Fatalf("SigAlgo() = %v, want UnknownSignatureAlgorithm", got) + } +} + +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 diff --git a/go.mod b/go.mod index a7c8e3441..6dacb28e4 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 40b8a5a3c..9a6b1f64a 100644 --- a/go.sum +++ b/go.sum @@ -160,6 +160,7 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -221,9 +222,11 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/go-gypsy v1.0.0 h1:7/wQ7A3UL1bnqRMnZ6T8cwCOArfZCxFmb1iTxaOOo1s= github.com/kylelemons/go-gypsy v1.0.0/go.mod h1:chkXM0zjdpXOiqkCW1XcCHDfjfk14PH2KKkQWxfJUcU= github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= @@ -300,6 +303,7 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= @@ -535,6 +539,7 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -682,6 +687,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= @@ -698,6 +704,7 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/helpers/derhelpers/derhelpers.go b/helpers/derhelpers/derhelpers.go index 8fe25aad5..23ba3984e 100644 --- a/helpers/derhelpers/derhelpers.go +++ b/helpers/derhelpers/derhelpers.go @@ -6,13 +6,14 @@ import ( "crypto" "crypto/ecdsa" "crypto/ed25519" + "crypto/mldsa" "crypto/rsa" "crypto/x509" cferr "github.com/cloudflare/cfssl/errors" ) -// ParsePrivateKeyDER parses a PKCS #1, PKCS #8, ECDSA, or Ed25519 DER-encoded +// ParsePrivateKeyDER parses a PKCS #1, PKCS #8, ECDSA, Ed25519, or ML-DSA DER-encoded // private key. The key must not be in PEM format. func ParsePrivateKeyDER(keyDER []byte) (key crypto.Signer, err error) { generalKey, err := x509.ParsePKCS8PrivateKey(keyDER) @@ -41,6 +42,8 @@ func ParsePrivateKeyDER(keyDER []byte) (key crypto.Signer, err error) { return generalKey, nil case ed25519.PrivateKey: return generalKey, nil + case *mldsa.PrivateKey: + return generalKey, nil } // should never reach here diff --git a/helpers/derhelpers/ed25519_test.go b/helpers/derhelpers/ed25519_test.go index 4a639de59..de0b512a9 100644 --- a/helpers/derhelpers/ed25519_test.go +++ b/helpers/derhelpers/ed25519_test.go @@ -3,6 +3,8 @@ package derhelpers import ( "bytes" "crypto/ed25519" + "crypto/mldsa" + "crypto/x509" "encoding/pem" "testing" ) @@ -96,3 +98,85 @@ func TestKeyPair(t *testing.T) { len(pk), pk, len(pk2), pk2) } } + +// TestParsePrivateKeyDERMLDSARFC9881 verifies round-trip parsing of the +// ML-DSA-44 example private key from RFC 9881. +func TestParsePrivateKeyDERMLDSARFC9881(t *testing.T) { + const rfc9881PEM = `-----BEGIN PRIVATE KEY----- +MDQCAQAwCwYJYIZIAWUDBAMRBCKAIAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZ +GhscHR4f +-----END PRIVATE KEY-----` + + block, _ := pem.Decode([]byte(rfc9881PEM)) + if block == nil { + t.Fatal("failed to decode RFC 9881 PEM") + } + + parsed, err := ParsePrivateKeyDER(block.Bytes) + if err != nil { + t.Fatalf("ParsePrivateKeyDER failed: %v", err) + } + + mldsaKey, ok := parsed.(*mldsa.PrivateKey) + if !ok { + t.Fatalf("expected *mldsa.PrivateKey, got %T", parsed) + } + + if mldsaKey.PublicKey().Parameters() != mldsa.MLDSA44() { + t.Fatalf("expected MLDSA44 parameters, got %v", mldsaKey.PublicKey().Parameters()) + } + + // Verify round-trip: marshal back to PKCS#8 and re-parse + der, err := x509.MarshalPKCS8PrivateKey(mldsaKey) + if err != nil { + t.Fatalf("MarshalPKCS8PrivateKey failed: %v", err) + } + + reparsed, err := ParsePrivateKeyDER(der) + if err != nil { + t.Fatalf("ParsePrivateKeyDER round-trip failed: %v", err) + } + + if !reparsed.(*mldsa.PrivateKey).PublicKey().Equal(mldsaKey.PublicKey()) { + t.Fatal("public keys differ after round-trip") + } +} + +func TestParsePrivateKeyDERMLDSA(t *testing.T) { + tests := []struct { + name string + params mldsa.Parameters + }{ + {"MLDSA44", mldsa.MLDSA44()}, + {"MLDSA65", mldsa.MLDSA65()}, + {"MLDSA87", mldsa.MLDSA87()}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + priv, err := mldsa.GenerateKey(tt.params) + if err != nil { + t.Fatalf("GenerateKey failed: %v", err) + } + + der, err := x509.MarshalPKCS8PrivateKey(priv) + if err != nil { + t.Fatalf("MarshalPKCS8PrivateKey failed: %v", err) + } + + parsed, err := ParsePrivateKeyDER(der) + if err != nil { + t.Fatalf("ParsePrivateKeyDER failed: %v", err) + } + + mldsaKey, ok := parsed.(*mldsa.PrivateKey) + if !ok { + t.Fatalf("expected *mldsa.PrivateKey, got %T", parsed) + } + + if mldsaKey.PublicKey().Parameters() != tt.params { + t.Fatalf("parameters mismatch after round-trip") + } + }) + } +} diff --git a/helpers/helpers.go b/helpers/helpers.go index e721e352c..aa951432e 100644 --- a/helpers/helpers.go +++ b/helpers/helpers.go @@ -8,6 +8,7 @@ import ( "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" + "crypto/mldsa" "crypto/rsa" "crypto/tls" "crypto/x509" @@ -73,6 +74,8 @@ func KeyLength(key interface{}) int { return rsaKey.N.BitLen() } else if _, ok := key.(ed25519.PublicKey); ok { return ed25519.PublicKeySize + } else if mldsaKey, ok := key.(*mldsa.PublicKey); ok { + return mldsaKey.Parameters().PublicKeySize() } return 0 @@ -156,6 +159,12 @@ func SignatureString(alg x509.SignatureAlgorithm) string { return "ECDSAWithSHA512" case x509.PureEd25519: return "Ed25519" + case x509.MLDSA44: + return "MLDSA44" + case x509.MLDSA65: + return "MLDSA65" + case x509.MLDSA87: + return "MLDSA87" default: return "Unknown Signature" } @@ -191,6 +200,12 @@ func HashAlgoString(alg x509.SignatureAlgorithm) string { return "SHA512" case x509.PureEd25519: return "Ed25519" + case x509.MLDSA44: + return "MLDSA44" + case x509.MLDSA65: + return "MLDSA65" + case x509.MLDSA87: + return "MLDSA87" default: return "Unknown Hash Algorithm" } @@ -502,6 +517,17 @@ func SignerAlgo(priv crypto.Signer) x509.SignatureAlgorithm { } case ed25519.PublicKey: return x509.PureEd25519 + case *mldsa.PublicKey: + switch { + case pub.Parameters() == mldsa.MLDSA44(): + return x509.MLDSA44 + case pub.Parameters() == mldsa.MLDSA65(): + return x509.MLDSA65 + case pub.Parameters() == mldsa.MLDSA87(): + return x509.MLDSA87 + default: + return x509.UnknownSignatureAlgorithm + } default: return x509.UnknownSignatureAlgorithm } diff --git a/helpers/helpers_test.go b/helpers/helpers_test.go index bbc74d38e..2a0135c1b 100644 --- a/helpers/helpers_test.go +++ b/helpers/helpers_test.go @@ -5,6 +5,7 @@ import ( "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" + "crypto/mldsa" "crypto/rand" "crypto/rsa" "crypto/x509" @@ -99,7 +100,7 @@ func TestKeyLength(t *testing.T) { } // test the rsa branch - rsaPriv, _ := rsa.GenerateKey(rand.Reader, 256) + rsaPriv, _ := rsa.GenerateKey(rand.Reader, 2048) rsaIn, _ := rsaPriv.Public().(*rsa.PublicKey) expRsa := rsaIn.N.BitLen() outRsa := KeyLength(rsaIn) @@ -283,6 +284,28 @@ func TestSignatureString(t *testing.T) { } } +func TestMLDSAAlgorithmStrings(t *testing.T) { + tests := []struct { + name string + algo x509.SignatureAlgorithm + }{ + {name: "MLDSA44", algo: x509.MLDSA44}, + {name: "MLDSA65", algo: x509.MLDSA65}, + {name: "MLDSA87", algo: x509.MLDSA87}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := SignatureString(tt.algo); got != tt.name { + t.Errorf("SignatureString(%v) = %q, want %q", tt.algo, got, tt.name) + } + if got := HashAlgoString(tt.algo); got != tt.name { + t.Errorf("HashAlgoString(%v) = %q, want %q", tt.algo, got, tt.name) + } + }) + } +} + func TestParseCertificatePEM(t *testing.T) { for _, testFile := range []string{testCertFile, testExtraWSCertFile, testSinglePKCS7} { certPEM, err := os.ReadFile(testFile) @@ -677,3 +700,28 @@ func TestSCTListFromOCSPResponse(t *testing.T) { t.Fatal("SCTs don't match") } } + +func TestSignerAlgoMLDSA(t *testing.T) { + tests := []struct { + name string + params mldsa.Parameters + want x509.SignatureAlgorithm + }{ + {"MLDSA44", mldsa.MLDSA44(), x509.MLDSA44}, + {"MLDSA65", mldsa.MLDSA65(), x509.MLDSA65}, + {"MLDSA87", mldsa.MLDSA87(), x509.MLDSA87}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + priv, err := mldsa.GenerateKey(tt.params) + if err != nil { + t.Fatalf("GenerateKey failed: %v", err) + } + got := SignerAlgo(priv) + if got != tt.want { + t.Errorf("SignerAlgo() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/initca/initca.go b/initca/initca.go index 54e051260..3e7b902bd 100644 --- a/initca/initca.go +++ b/initca/initca.go @@ -7,6 +7,7 @@ import ( "crypto" "crypto/ecdsa" "crypto/ed25519" + "crypto/mldsa" "crypto/rand" "crypto/rsa" "crypto/x509" @@ -220,6 +221,15 @@ func RenewFromSigner(ca *x509.Certificate, priv crypto.Signer) ([]byte, error) { if !(bytes.Equal(ca.PublicKey.(ed25519.PublicKey), ed25519PublicKey)) { return nil, cferr.New(cferr.PrivateKeyError, cferr.KeyMismatch) } + case ca.PublicKeyAlgorithm == x509.MLDSA: + var mldsaPublicKey *mldsa.PublicKey + var ok bool + if mldsaPublicKey, ok = priv.Public().(*mldsa.PublicKey); !ok { + return nil, cferr.New(cferr.PrivateKeyError, cferr.KeyMismatch) + } + if !ca.PublicKey.(*mldsa.PublicKey).Equal(mldsaPublicKey) { + return nil, cferr.New(cferr.PrivateKeyError, cferr.KeyMismatch) + } default: return nil, cferr.New(cferr.PrivateKeyError, cferr.NotRSAOrECCOrEd25519) } diff --git a/initca/initca_test.go b/initca/initca_test.go index c46c208b9..15c08d059 100644 --- a/initca/initca_test.go +++ b/initca/initca_test.go @@ -3,7 +3,9 @@ package initca import ( "bytes" "crypto/ecdsa" + "crypto/mldsa" "crypto/rsa" + "crypto/x509" "os" "strings" "testing" @@ -24,6 +26,9 @@ var validKeyParams = []csr.KeyRequest{ {A: "ecdsa", S: 384}, {A: "ecdsa", S: 521}, {A: "ed25519"}, + {A: "mldsa44"}, + {A: "mldsa65"}, + {A: "mldsa87"}, } var validCAConfigs = []csr.CAConfig{ @@ -63,6 +68,8 @@ var invalidCryptoParams = []csr.KeyRequest{ // Bad param {A: "rsaCrypto", S: 2048}, {A: "ecdsa", S: 2000}, + // Invalid ML-DSA variant + {A: "mldsa99"}, } func TestInitCA(t *testing.T) { @@ -396,6 +403,67 @@ func TestRenewMismatch(t *testing.T) { } } +func TestRenewMLDSA(t *testing.T) { + tests := []struct { + name string + algo string + params mldsa.Parameters + sigAlgo x509.SignatureAlgorithm + }{ + {name: "MLDSA44", algo: "mldsa44", params: mldsa.MLDSA44(), sigAlgo: x509.MLDSA44}, + {name: "MLDSA65", algo: "mldsa65", params: mldsa.MLDSA65(), sigAlgo: x509.MLDSA65}, + {name: "MLDSA87", algo: "mldsa87", params: mldsa.MLDSA87(), sigAlgo: x509.MLDSA87}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + certPEM, _, keyPEM, err := New(&csr.CertificateRequest{ + CN: "ML-DSA test CA", + KeyRequest: &csr.KeyRequest{A: tt.algo}, + }) + if err != nil { + t.Fatalf("creating CA: %v", err) + } + cert, err := helpers.ParseCertificatePEM(certPEM) + if err != nil { + t.Fatalf("parsing CA certificate: %v", err) + } + key, err := helpers.ParsePrivateKeyPEM(keyPEM) + if err != nil { + t.Fatalf("parsing CA key: %v", err) + } + + renewedPEM, err := RenewFromSigner(cert, key) + if err != nil { + t.Fatalf("renewing CA: %v", err) + } + renewed, err := helpers.ParseCertificatePEM(renewedPEM) + if err != nil { + t.Fatalf("parsing renewed certificate: %v", err) + } + if err := renewed.CheckSignatureFrom(renewed); err != nil { + t.Fatalf("checking renewed certificate signature: %v", err) + } + if renewed.SignatureAlgorithm != tt.sigAlgo { + t.Fatalf("signature algorithm = %v, want %v", renewed.SignatureAlgorithm, tt.sigAlgo) + } + if renewed.PublicKeyAlgorithm != x509.MLDSA { + t.Fatalf("public key algorithm = %v, want ML-DSA", renewed.PublicKeyAlgorithm) + } + renewedPublicKey, ok := renewed.PublicKey.(*mldsa.PublicKey) + if !ok { + t.Fatalf("public key type = %T, want *mldsa.PublicKey", renewed.PublicKey) + } + if renewedPublicKey.Parameters() != tt.params { + t.Fatalf("parameters = %v, want %v", renewedPublicKey.Parameters(), tt.params) + } + if !renewedPublicKey.Equal(cert.PublicKey) { + t.Fatal("renewal changed the public key") + } + }) + } +} + func TestRenew(t *testing.T) { in, err := os.ReadFile(testECDSACAFile) if err != nil { diff --git a/revoke/revoke.go b/revoke/revoke.go index 6371ec065..e11022401 100644 --- a/revoke/revoke.go +++ b/revoke/revoke.go @@ -192,11 +192,11 @@ func VerifyCertificateError(cert *x509.Certificate) (revoked, ok bool, err error if !time.Now().Before(cert.NotAfter) { msg := fmt.Sprintf("Certificate expired %s\n", cert.NotAfter) log.Info(msg) - return true, true, fmt.Errorf(msg) + return true, true, errors.New(msg) } else if !time.Now().After(cert.NotBefore) { msg := fmt.Sprintf("Certificate isn't valid until %s\n", cert.NotBefore) log.Info(msg) - return true, true, fmt.Errorf(msg) + return true, true, errors.New(msg) } return revCheck(cert) } diff --git a/scan/crypto/rsa/pss_test.go b/scan/crypto/rsa/pss_test.go index cae24e58c..40654ffc6 100644 --- a/scan/crypto/rsa/pss_test.go +++ b/scan/crypto/rsa/pss_test.go @@ -160,7 +160,7 @@ func TestPSSGolden(t *testing.T) { t.Error(err) } default: - t.Fatalf("unknown marker: " + marker) + t.Fatalf("unknown marker: %s", marker) } } } diff --git a/signer/local/local_test.go b/signer/local/local_test.go index 914763689..8c061bc10 100644 --- a/signer/local/local_test.go +++ b/signer/local/local_test.go @@ -1463,20 +1463,32 @@ func TestSignFromPrecert(t *testing.T) { t.Fatal("Certificate without SCT list extension was returned") } + // Find CT poison extension index by OID + poisonIdx := -1 + for i, ext := range precert.Extensions { + if ext.Id.Equal(signer.CTPoisonOID) { + poisonIdx = i + break + } + } + if poisonIdx == -1 { + t.Fatal("precert does not contain CT poison extension") + } + // Break poison extension - precert.Extensions[7].Value = []byte{1, 3, 3, 7} + precert.Extensions[poisonIdx].Value = []byte{1, 3, 3, 7} _, err = testSigner.SignFromPrecert(precert, scts) if err == nil { t.Fatal("SignFromPrecert didn't fail with invalid poison extension") } - precert.Extensions[7].Critical = false + precert.Extensions[poisonIdx].Critical = false _, err = testSigner.SignFromPrecert(precert, scts) if err == nil { t.Fatal("SignFromPrecert didn't fail with non-critical poison extension") } - precert.Extensions = append(precert.Extensions[:7], precert.Extensions[8:]...) + precert.Extensions = append(precert.Extensions[:poisonIdx], precert.Extensions[poisonIdx+1:]...) _, err = testSigner.SignFromPrecert(precert, scts) if err == nil { t.Fatal("SignFromPrecert didn't fail with missing poison extension") @@ -1492,6 +1504,10 @@ func TestSignFromPrecert(t *testing.T) { func TestLint(t *testing.T) { k, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) serial := big.NewInt(1337) + policyOID, err := x509.OIDFromInts([]uint64{1, 2, 3}) + if err != nil { + t.Fatalf("failed to construct certificate policy OID: %v", err) + } // jankyTemplate is an x509 cert template that mostly passes through zlint // without errors/warnings. It is used as the basis of both the signer's issuing @@ -1500,14 +1516,12 @@ func TestLint(t *testing.T) { Subject: pkix.Name{ CommonName: "janky.cert", }, - SerialNumber: serial, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(0, 0, 90), - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, - PolicyIdentifiers: []asn1.ObjectIdentifier{ - {1, 2, 3}, - }, + SerialNumber: serial, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(0, 0, 90), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + Policies: []x509.OID{policyOID}, BasicConstraintsValid: true, IsCA: true, IssuingCertificateURL: []string{"http://ca.cpu"}, diff --git a/signer/signer.go b/signer/signer.go index cfffba1b0..a11c2b619 100644 --- a/signer/signer.go +++ b/signer/signer.go @@ -6,6 +6,7 @@ import ( "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" + "crypto/mldsa" "crypto/rsa" "crypto/sha1" "crypto/x509" @@ -172,6 +173,17 @@ func DefaultSigAlgo(priv crypto.Signer) x509.SignatureAlgorithm { } case ed25519.PublicKey: return x509.PureEd25519 + case *mldsa.PublicKey: + switch { + case pub.Parameters() == mldsa.MLDSA44(): + return x509.MLDSA44 + case pub.Parameters() == mldsa.MLDSA65(): + return x509.MLDSA65 + case pub.Parameters() == mldsa.MLDSA87(): + return x509.MLDSA87 + default: + return x509.UnknownSignatureAlgorithm + } default: return x509.UnknownSignatureAlgorithm } diff --git a/signer/signer_test.go b/signer/signer_test.go index acdea1ee2..ee9f02c8f 100644 --- a/signer/signer_test.go +++ b/signer/signer_test.go @@ -2,6 +2,7 @@ package signer import ( "bytes" + "crypto/mldsa" "crypto/x509" "encoding/asn1" "encoding/hex" @@ -180,3 +181,28 @@ func TestName(t *testing.T) { } } + +func TestDefaultSigAlgoMLDSA(t *testing.T) { + tests := []struct { + name string + params mldsa.Parameters + want x509.SignatureAlgorithm + }{ + {"MLDSA44", mldsa.MLDSA44(), x509.MLDSA44}, + {"MLDSA65", mldsa.MLDSA65(), x509.MLDSA65}, + {"MLDSA87", mldsa.MLDSA87(), x509.MLDSA87}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + priv, err := mldsa.GenerateKey(tt.params) + if err != nil { + t.Fatalf("GenerateKey failed: %v", err) + } + got := DefaultSigAlgo(priv) + if got != tt.want { + t.Errorf("DefaultSigAlgo() = %v, want %v", got, tt.want) + } + }) + } +}