diff --git a/internal/mldsa/helpers.go b/internal/mldsa/helpers.go new file mode 100644 index 00000000..9371f8c7 --- /dev/null +++ b/internal/mldsa/helpers.go @@ -0,0 +1,39 @@ +package mldsa + +import ( + "crypto/sha3" + "errors" +) + +var ( + errContextTooLong = errors.New("mldsa: context too long") +) + +func PublicKeyHash(pub *PublicKey) [64]byte { + H := sha3.NewSHAKE256() + H.Write(pub.Bytes()) + var tr [64]byte + H.Read(tr[:]) + return tr +} + +func MessageHash(pub *PublicKey, msg []byte, opts *Options) ([64]byte, error) { + if opts == nil { + opts = &Options{} + } + + tr := PublicKeyHash(pub) + if len(opts.Context) > 255 { + return [64]byte{}, errContextTooLong + } + + H := sha3.NewSHAKE256() + H.Write(tr[:]) + H.Write([]byte{0}) // ML-DSA / HashML-DSA domain separator + H.Write([]byte{byte(len(opts.Context))}) + H.Write([]byte(opts.Context)) + H.Write(msg) + var μ [64]byte + H.Read(μ[:]) + return μ, nil +} diff --git a/internal/mldsa/mldsa_go127.go b/internal/mldsa/mldsa_go127.go new file mode 100644 index 00000000..a1e8a9a6 --- /dev/null +++ b/internal/mldsa/mldsa_go127.go @@ -0,0 +1,27 @@ +//go:build go1.27 + +package mldsa + +import ( + "crypto/mldsa" +) + +// Supported reports whether ML-DSA is available in the current build. It is +// true when compiled with Go 1.27 or later. +const Supported = true + +type Options = mldsa.Options + +type Parameters = mldsa.Parameters + +type PrivateKey = mldsa.PrivateKey + +type PublicKey = mldsa.PublicKey + +var ( + GenerateKey = mldsa.GenerateKey + Verify = mldsa.Verify + MLDSA44 = mldsa.MLDSA44 + MLDSA65 = mldsa.MLDSA65 + MLDSA87 = mldsa.MLDSA87 +) diff --git a/internal/mldsa/mldsa_other.go b/internal/mldsa/mldsa_other.go new file mode 100644 index 00000000..0ca764ec --- /dev/null +++ b/internal/mldsa/mldsa_other.go @@ -0,0 +1,81 @@ +//go:build !go1.27 + +package mldsa + +import ( + "crypto" + "errors" + "io" +) + +var errNotSupported = errors.New("mldsa is not supported") + +// Supported reports whether ML-DSA is available in the current build. It is +// false when compiled with a Go toolchain older than 1.27. +const Supported = false + +type Parameters struct{} + +func MLDSA44() Parameters { + return Parameters{} +} + +func MLDSA65() Parameters { + return Parameters{} +} + +func MLDSA87() Parameters { + return Parameters{} +} + +type Options struct { + Context string +} + +func (o *Options) HashFunc() crypto.Hash { + return 0 +} + +type PrivateKey struct{} + +func (sk *PrivateKey) Bytes() []byte { + return nil +} + +func (sk *PrivateKey) Equal(x crypto.PrivateKey) bool { + return false +} + +func (sk *PrivateKey) Public() crypto.PublicKey { + return (*PublicKey)(nil) +} + +func (sk *PrivateKey) PublicKey() *PublicKey { + return (*PublicKey)(nil) +} + +func (sk *PrivateKey) Sign(_ io.Reader, message []byte, opts crypto.SignerOpts) (signature []byte, err error) { + return nil, errNotSupported +} + +type PublicKey struct{} + +func (pk *PublicKey) Bytes() []byte { + return nil +} + +func (pk *PublicKey) Equal(x crypto.PublicKey) bool { + return false +} + +func (pk *PublicKey) Parameters() Parameters { + return Parameters{} +} + +func GenerateKey(params Parameters) (*PrivateKey, error) { + return nil, errNotSupported +} + +func Verify(pk *PublicKey, message, signature []byte, opts *Options) error { + return errNotSupported +} diff --git a/keyutil/key.go b/keyutil/key.go index a8ec53d8..c41f967f 100644 --- a/keyutil/key.go +++ b/keyutil/key.go @@ -10,12 +10,14 @@ import ( "crypto/rand" "crypto/rsa" "crypto/x509" + "fmt" "math/big" "sync/atomic" "github.com/pkg/errors" "golang.org/x/crypto/ssh" + "go.step.sm/crypto/internal/mldsa" "go.step.sm/crypto/x25519" ) @@ -26,6 +28,8 @@ var ( DefaultKeySize = 2048 // DefaultKeyCurve is the default curve of a private key. DefaultKeyCurve = "P-256" + // DefaultKeyAlgorithm is the default algorithm for AKP (ML-DSA) keys. + DefaultKeyAlgorithm = mldsa.MLDSA44 // DefaultSignatureAlgorithm is the default signature algorithm used on a // certificate with the default key type. DefaultSignatureAlgorithm = x509.ECDSAWithSHA256 @@ -55,15 +59,7 @@ func Insecure() (revert func()) { // PublicKey extracts a public key from a private key. func PublicKey(priv interface{}) (crypto.PublicKey, error) { switch k := priv.(type) { - case *rsa.PrivateKey: - return &k.PublicKey, nil - case *ecdsa.PrivateKey: - return &k.PublicKey, nil - case ed25519.PrivateKey: - return k.Public(), nil - case x25519.PrivateKey: - return k.Public(), nil - case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey, x25519.PublicKey: + case *rsa.PublicKey, *ecdsa.PublicKey, *mldsa.PublicKey, ed25519.PublicKey, x25519.PublicKey: return k, nil case crypto.Signer: return k.Public(), nil @@ -87,7 +83,7 @@ func GenerateDefaultKeyPair() (crypto.PublicKey, crypto.PrivateKey, error) { // GenerateKey generates a key of the given type (kty). func GenerateKey(kty, crv string, size int) (crypto.PrivateKey, error) { switch kty { - case "EC", "RSA", "OKP": + case "EC", "RSA", "OKP", "AKP": return GenerateSigner(kty, crv, size) case "oct": return generateOctKey(size) @@ -113,7 +109,8 @@ func GenerateDefaultSigner() (crypto.Signer, error) { } // GenerateSigner creates an asymmetric crypto key that implements -// crypto.Signer. +// crypto.Signer. For ML-DSA keys, the crv parameters indicates the algorithm to +// use. func GenerateSigner(kty, crv string, size int) (crypto.Signer, error) { switch kty { case "EC": @@ -122,6 +119,8 @@ func GenerateSigner(kty, crv string, size int) (crypto.Signer, error) { return generateRSAKey(size) case "OKP": return generateOKPKey(crv) + case "AKP": + return generateAKPKey(crv) default: return nil, errors.Errorf("unrecognized key type: %s", kty) } @@ -133,6 +132,7 @@ func ExtractKey(in interface{}) (interface{}, error) { switch k := in.(type) { case *rsa.PublicKey, *rsa.PrivateKey, *ecdsa.PublicKey, *ecdsa.PrivateKey, + *mldsa.PublicKey, *mldsa.PrivateKey, ed25519.PublicKey, ed25519.PrivateKey, x25519.PublicKey, x25519.PrivateKey: return in, nil @@ -147,7 +147,7 @@ func ExtractKey(in interface{}) (interface{}, error) { case *ssh.Certificate: return ExtractKey(k.Key) default: - return nil, errors.Errorf("cannot extract the key from type '%T'", k) + return nil, errors.Errorf("cannot extract the key from type '%T'", in) } } @@ -165,31 +165,14 @@ func VerifyPair(pub crypto.PublicKey, priv crypto.PrivateKey) error { // Equal reports if x and y are the same key. func Equal(x, y any) bool { + if eq, ok := x.(interface{ Equal(crypto.PublicKey) bool }); ok { + return eq.Equal(y) + } + if eq, ok := x.(interface{ Equal(crypto.PrivateKey) bool }); ok { + return eq.Equal(y) + } + switch xx := x.(type) { - case *ecdsa.PublicKey: - yy, ok := y.(*ecdsa.PublicKey) - return ok && xx.Equal(yy) - case *ecdsa.PrivateKey: - yy, ok := y.(*ecdsa.PrivateKey) - return ok && xx.Equal(yy) - case *rsa.PublicKey: - yy, ok := y.(*rsa.PublicKey) - return ok && xx.Equal(yy) - case *rsa.PrivateKey: - yy, ok := y.(*rsa.PrivateKey) - return ok && xx.Equal(yy) - case ed25519.PublicKey: - yy, ok := y.(ed25519.PublicKey) - return ok && xx.Equal(yy) - case ed25519.PrivateKey: - yy, ok := y.(ed25519.PrivateKey) - return ok && xx.Equal(yy) - case x25519.PublicKey: - yy, ok := y.(x25519.PublicKey) - return ok && xx.Equal(yy) - case x25519.PrivateKey: - yy, ok := y.(x25519.PrivateKey) - return ok && xx.Equal(yy) case []byte: // special case for symmetric keys yy, ok := y.([]byte) return ok && bytes.Equal(xx, yy) @@ -252,6 +235,38 @@ func generateOKPKey(crv string) (crypto.Signer, error) { } } +func generateAKPKey(alg string) (crypto.Signer, error) { + switch alg { + case "": + key, err := mldsa.GenerateKey(DefaultKeyAlgorithm()) + if err != nil { + return nil, fmt.Errorf("error generating ML-DSA key: %w", err) + } + return key, nil + case "ML-DSA-44": + key, err := mldsa.GenerateKey(mldsa.MLDSA44()) + if err != nil { + return nil, fmt.Errorf("error generating ML-DSA-44 key: %w", err) + } + return key, nil + case "ML-DSA-65": + key, err := mldsa.GenerateKey(mldsa.MLDSA65()) + if err != nil { + return nil, fmt.Errorf("error generating ML-DSA-65 key: %w", err) + } + return key, nil + case "ML-DSA-87": + key, err := mldsa.GenerateKey(mldsa.MLDSA87()) + if err != nil { + return nil, fmt.Errorf("error generating ML-DSA-87 key: %w", err) + } + return key, nil + default: + return nil, errors.Errorf("missing or invalid value for argument 'alg'. "+ + "expected 'ML-DSA-44', 'ML-DSA-65', or 'ML-DSA-87', but got '%s'", alg) + } +} + func generateOctKey(size int) (interface{}, error) { const chars = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" result := make([]byte, size) diff --git a/keyutil/key_test.go b/keyutil/key_test.go index d9f1d629..1f1fea31 100644 --- a/keyutil/key_test.go +++ b/keyutil/key_test.go @@ -13,9 +13,11 @@ import ( "reflect" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/crypto/ssh" + "go.step.sm/crypto/internal/mldsa" "go.step.sm/crypto/x25519" ) @@ -62,6 +64,17 @@ func must(args ...interface{}) interface{} { return args[0] } +func shouldMLDSA(t *testing.T, p mldsa.Parameters) *mldsa.PrivateKey { + t.Helper() + k, err := mldsa.GenerateKey(p) + if mldsa.Supported { + require.NoError(t, err) + } else { + require.Error(t, err) + } + return k +} + var randReader = rand.Reader //nolint:gocritic // ignore sloppy func name due to function signature @@ -118,6 +131,10 @@ func verifyKeyPair(h crypto.Hash, priv, pub any) error { if err := rsa.VerifyPKCS1v15(p, h, sig, sum); err != nil { return fmt.Errorf("rsa.VerifyPKCS1v15 failed") } + case *mldsa.PublicKey: + if err := mldsa.Verify(p, sum, sig, nil); err != nil { + return fmt.Errorf("mldsa.Verify failed") + } case ed25519.PublicKey: if !ed25519.Verify(p, sum, sig) { return fmt.Errorf("ed25519.Verify failed") @@ -152,6 +169,7 @@ func TestPublicKey(t *testing.T) { ed25519Key := must(generateOKPKey("Ed25519")).(ed25519.PrivateKey) x25519Pub, x25519Priv, err := x25519.GenerateKey(rand.Reader) require.NoError(t, err) + mldsaKey := shouldMLDSA(t, mldsa.MLDSA44()) type args struct { priv interface{} @@ -170,6 +188,8 @@ func TestPublicKey(t *testing.T) { {"ed25519Public", args{ed25519.PublicKey(ed25519Key[32:])}, ed25519Key.Public(), false}, {"x25519", args{x25519Priv}, x25519Pub, false}, {"x25519Public", args{x25519Pub}, x25519Pub, false}, + {"mldsa", args{mldsaKey}, mldsaKey.Public(), false}, + {"mldsaPublic", args{mldsaKey.PublicKey()}, mldsaKey.Public(), false}, {"ecdsaSigner", args{ecdsaSigner}, ecdsaKey.Public(), false}, {"fail", args{[]byte("octkey")}, nil, true}, } @@ -306,6 +326,9 @@ func TestGenerateKey(t *testing.T) { {"P-521", randReader, args{"EC", "P-521", 0}, assertKey, crypto.SHA512, false}, {"Ed25519", randReader, args{"OKP", "Ed25519", 0}, assertKey, crypto.Hash(0), false}, {"X25519", randReader, args{"OKP", "X25519", 0}, assertKey, crypto.Hash(0), false}, + {"ML-DSA-44", randReader, args{"AKP", "ML-DSA-44", 0}, assertKey, crypto.Hash(0), !mldsa.Supported}, + {"ML-DSA-65", randReader, args{"AKP", "ML-DSA-65", 0}, assertKey, crypto.Hash(0), !mldsa.Supported}, + {"ML-DSA-87", randReader, args{"AKP", "ML-DSA-87", 0}, assertKey, crypto.Hash(0), !mldsa.Supported}, {"OCT", zeroReader{}, args{"oct", "", 32}, assertOCT, crypto.Hash(0), false}, {"eof EC", eofReader{}, args{"EC", "P-256", 0}, nil, 0, true}, {"eof RSA", eofReader{}, args{"RSA", "", 1024}, nil, 0, true}, @@ -399,6 +422,28 @@ func TestGenerateKeyPair(t *testing.T) { } } + mldsaEnabled := mldsa.Supported + assertMLDSA := func(p mldsa.Parameters) func(t *testing.T, got, got1 any) { + if !mldsaEnabled { + return assertNil() + } + return func(t *testing.T, got, got1 any) { + t.Helper() + require.NotNil(t, got) + require.NotNil(t, got1) + + pub, ok := got.(*mldsa.PublicKey) + require.True(t, ok) + assert.Equal(t, p, pub.Parameters()) + + priv := got1.(*mldsa.PrivateKey) + require.True(t, ok) + assert.Equal(t, p, priv.PublicKey().Parameters()) + + assert.True(t, pub.Equal(priv.Public())) + } + } + type args struct { kty string crv string @@ -415,6 +460,9 @@ func TestGenerateKeyPair(t *testing.T) { {"P-384", randReader, args{"EC", "P-384", 0}, assertKey(crypto.SHA384), false}, {"P-521", randReader, args{"EC", "P-521", 0}, assertKey(crypto.SHA512), false}, {"Ed25519", randReader, args{"OKP", "Ed25519", 0}, assertKey(crypto.Hash(0)), false}, + {"ML-DSA-44", randReader, args{"AKP", "ML-DSA-44", 0}, assertMLDSA(mldsa.MLDSA44()), !mldsaEnabled}, + {"ML-DSA-65", randReader, args{"AKP", "ML-DSA-65", 0}, assertMLDSA(mldsa.MLDSA65()), !mldsaEnabled}, + {"ML-DSA-87", randReader, args{"AKP", "ML-DSA-87", 0}, assertMLDSA(mldsa.MLDSA87()), !mldsaEnabled}, {"OCT", zeroReader{}, args{"oct", "", 32}, assertNil(), true}, {"eof", eofReader{}, args{"EC", "P-256", 0}, assertNil(), true}, {"unknown", randReader, args{"EC", "P-128", 0}, assertNil(), true}, @@ -532,6 +580,14 @@ func TestGenerateSigner(t *testing.T) { } } + mldsaEnabled := mldsa.Supported + assertSignerMLDSA := func() func(t *testing.T, got crypto.Signer) { + if !mldsaEnabled { + return assertNil() + } + return assertSigner(crypto.Hash(0)) + } + type args struct { kty string crv string @@ -547,6 +603,9 @@ func TestGenerateSigner(t *testing.T) { {"P-384", args{"EC", "P-384", 0}, assertSigner(crypto.SHA384), false}, {"P-521", args{"EC", "P-521", 0}, assertSigner(crypto.SHA512), false}, {"Ed25519", args{"OKP", "Ed25519", 0}, assertSigner(crypto.Hash(0)), false}, + {"ML-DSA-44", args{"AKP", "ML-DSA-44", 0}, assertSignerMLDSA(), !mldsaEnabled}, + {"ML-DSA-65", args{"AKP", "ML-DSA-65", 0}, assertSignerMLDSA(), !mldsaEnabled}, + {"ML-DSA-87", args{"AKP", "ML-DSA-87", 0}, assertSignerMLDSA(), !mldsaEnabled}, {"OCT", args{"oct", "", 32}, assertNil(), true}, {"unknown", args{"EC", "P-128", 0}, assertNil(), true}, {"unknown", args{"FOO", "", 1024}, assertNil(), true}, @@ -568,6 +627,7 @@ func TestExtractKey(t *testing.T) { ecKey := must(generateECKey("P-256")).(*ecdsa.PrivateKey) edKey := must(generateOKPKey("Ed25519")).(ed25519.PrivateKey) octKey := must(generateOctKey(64)).([]byte) + mldsaKey := shouldMLDSA(t, mldsa.MLDSA44()) b, _ := pem.Decode([]byte(testCRT)) cert, err := x509.ParseCertificate(b.Bytes) @@ -602,6 +662,8 @@ func TestExtractKey(t *testing.T) { {"EC public key", args{ecKey.Public()}, ecKey.Public(), false}, {"OKP private key", args{edKey}, edKey, false}, {"OKP public key", args{edKey.Public()}, edKey.Public(), false}, + {"ML-DSA private key", args{mldsaKey}, mldsaKey, false}, + {"ML-DSA public key", args{mldsaKey.Public()}, mldsaKey.Public(), false}, {"oct key", args{octKey}, octKey, false}, {"certificate", args{cert}, cert.PublicKey, false}, {"csr", args{csr}, csr.PublicKey, false}, @@ -629,10 +691,12 @@ func TestVerifyPair(t *testing.T) { ecdsaKey := must(generateECKey("P-256")).(*ecdsa.PrivateKey) rsaKey := must(generateRSAKey(2048)).(*rsa.PrivateKey) ed25519Key := must(generateOKPKey("Ed25519")).(ed25519.PrivateKey) + mldsaKey := shouldMLDSA(t, mldsa.MLDSA65()) ecdsaKey1 := must(generateECKey("P-256")).(*ecdsa.PrivateKey) rsaKey1 := must(generateRSAKey(2048)).(*rsa.PrivateKey) ed25519Key1 := must(generateOKPKey("Ed25519")).(ed25519.PrivateKey) + mldsaKey1 := shouldMLDSA(t, mldsa.MLDSA65()) type args struct { pubkey interface{} @@ -646,14 +710,17 @@ func TestVerifyPair(t *testing.T) { {"ecdsa", args{ecdsaKey.Public(), ecdsaKey}, false}, {"rsa", args{rsaKey.Public(), rsaKey}, false}, {"ed25519", args{ed25519Key.Public(), ed25519Key}, false}, + {"ml-dsa", args{mldsaKey.Public(), mldsaKey}, !mldsa.Supported}, // wrong private type {"fail ecdsa", args{ecdsaKey.Public(), ecdsaKey.Public()}, true}, {"fail rsa", args{rsaKey.Public(), rsaKey.Public()}, true}, {"fail ed25519", args{ed25519Key.Public(), ed25519Key.Public()}, true}, + {"fail ml-dsa", args{mldsaKey.Public(), mldsaKey.Public()}, true}, // wrong private key {"fail ecdsa key", args{ecdsaKey.Public(), ecdsaKey1}, true}, {"fail rsa key", args{rsaKey.Public(), rsaKey1}, true}, {"fail ed25519 key", args{ed25519Key.Public(), ed25519Key1}, true}, + {"fail ml-dsa key", args{mldsaKey.Public(), mldsaKey1}, true}, // wrong public type {"fail type", args{[]byte("foo"), []byte("foo")}, true}, } @@ -716,6 +783,9 @@ func TestEqual(t *testing.T) { if x, ok := key.(x25519.PrivateKey); ok { return x25519.PrivateKey([]byte(x)) } + if x, ok := key.(*mldsa.PrivateKey); ok && !mldsa.Supported { + return x + } b, err := x509.MarshalPKCS8PrivateKey(key) if err != nil { @@ -736,6 +806,7 @@ func TestEqual(t *testing.T) { rsaKey := mustSigner("RSA", "", 2048) ed25519Key := mustSigner("OKP", "Ed25519", 0) x25519Key := mustSigner("OKP", "X25519", 0) + mldsaKey := shouldMLDSA(t, mldsa.MLDSA87()) type args struct { x any @@ -750,19 +821,23 @@ func TestEqual(t *testing.T) { {"ok rsaKey", args{rsaKey, mustCopy(rsaKey)}, true}, {"ok ed25519Key", args{ed25519Key, mustCopy(ed25519Key)}, true}, {"ok x25519Key", args{x25519Key, mustCopy(x25519Key)}, true}, + {"ok mldsaKey", args{mldsaKey, mustCopy(mldsaKey)}, mldsa.Supported}, {"ok ecdsaKey pub", args{ecdsaKey.Public(), mustCopy(ecdsaKey).Public()}, true}, {"ok rsaKey pub", args{rsaKey.Public(), mustCopy(rsaKey).Public()}, true}, {"ok ed25519Key pub", args{ed25519Key.Public(), mustCopy(ed25519Key).Public()}, true}, {"ok x25519Key pub", args{x25519Key.Public(), mustCopy(x25519Key).Public()}, true}, + {"ok mldsaKey pub", args{mldsaKey.Public(), mustCopy(mldsaKey).Public()}, mldsa.Supported}, {"ok []byte", args{[]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}}, true}, {"fail ecdsaKey", args{ecdsaKey, mustCopy(ecdsaKey).Public()}, false}, {"fail rsaKey", args{rsaKey, mustCopy(rsaKey).Public()}, false}, {"fail ed25519Key", args{ed25519Key, mustCopy(ed25519Key).Public()}, false}, {"fail x25519Key", args{x25519Key, mustCopy(x25519Key).Public()}, false}, + {"fail mldsaKey", args{mldsaKey, mustCopy(mldsaKey).Public()}, false}, {"fail ecdsaKey pub", args{ecdsaKey.Public(), mustCopy(ecdsaKey)}, false}, {"fail rsaKey pub", args{rsaKey.Public(), mustCopy(rsaKey)}, false}, {"fail ed25519Key pub", args{ed25519Key.Public(), mustCopy(ed25519Key)}, false}, {"fail x25519Key pub", args{x25519Key.Public(), mustCopy(x25519Key)}, false}, + {"fail mldsaKey pub", args{mldsaKey.Public(), mustCopy(mldsaKey)}, false}, {"fail []byte", args{[]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}}, false}, {"fail int", args{1, 2}, false}, {"fail string", args{"foo", "foo"}, false}, diff --git a/kms/apiv1/requests.go b/kms/apiv1/requests.go index b2a16352..53a679b4 100644 --- a/kms/apiv1/requests.go +++ b/kms/apiv1/requests.go @@ -90,6 +90,12 @@ const ( ECDSAWithSHA512 // EdDSA on Curve25519 with a SHA512 digest. PureEd25519 + // ML-DSA-44 PQ algorithm defined in FIPS 204. + MLDSA44 + // ML-DSA-65 PQ algorithm defined in FIPS 204. + MLDSA65 + // ML-DSA-87 PQ algorithm defined in FIPS 204. + MLDSA87 ) // String returns a string representation of s. diff --git a/kms/awskms/awskms.go b/kms/awskms/awskms.go index 782dea3b..fbf594b8 100644 --- a/kms/awskms/awskms.go +++ b/kms/awskms/awskms.go @@ -39,7 +39,7 @@ type KeyManagementClient interface { // customerMasterKeySpecMapping is a mapping between the step signature algorithm, // and bits for RSA keys, with awskms CustomerMasterKeySpec. -var customerMasterKeySpecMapping = map[apiv1.SignatureAlgorithm]interface{}{ +var customerMasterKeySpecMapping = patchSignatureAlgorithmMapping(map[apiv1.SignatureAlgorithm]interface{}{ apiv1.UnspecifiedSignAlgorithm: types.KeySpecEccNistP256, apiv1.SHA256WithRSA: map[int]types.KeySpec{ 0: types.KeySpecRsa3072, @@ -80,7 +80,8 @@ var customerMasterKeySpecMapping = map[apiv1.SignatureAlgorithm]interface{}{ apiv1.ECDSAWithSHA256: types.KeySpecEccNistP256, apiv1.ECDSAWithSHA384: types.KeySpecEccNistP384, apiv1.ECDSAWithSHA512: types.KeySpecEccNistP521, -} + apiv1.PureEd25519: types.KeySpecEccNistEdwards25519, +}) // New creates a new AWSKMS. By default, clients will be created using the // credentials in `~/.aws/credentials`, but this can be overridden using the diff --git a/kms/awskms/awskms_go127.go b/kms/awskms/awskms_go127.go new file mode 100644 index 00000000..c39fff97 --- /dev/null +++ b/kms/awskms/awskms_go127.go @@ -0,0 +1,15 @@ +//go:build go1.27 + +package awskms + +import ( + "github.com/aws/aws-sdk-go-v2/service/kms/types" + "go.step.sm/crypto/kms/apiv1" +) + +func patchSignatureAlgorithmMapping(m map[apiv1.SignatureAlgorithm]interface{}) map[apiv1.SignatureAlgorithm]interface{} { + m[apiv1.MLDSA44] = types.KeySpecMlDsa44 + m[apiv1.MLDSA65] = types.KeySpecMlDsa65 + m[apiv1.MLDSA87] = types.KeySpecMlDsa87 + return m +} diff --git a/kms/awskms/awskms_other.go b/kms/awskms/awskms_other.go new file mode 100644 index 00000000..6cffa251 --- /dev/null +++ b/kms/awskms/awskms_other.go @@ -0,0 +1,9 @@ +//go:build !go1.27 + +package awskms + +import "go.step.sm/crypto/kms/apiv1" + +func patchSignatureAlgorithmMapping(m map[apiv1.SignatureAlgorithm]interface{}) map[apiv1.SignatureAlgorithm]interface{} { + return m +} diff --git a/kms/awskms/awskms_test.go b/kms/awskms/awskms_test.go index fc411a98..90339e7d 100644 --- a/kms/awskms/awskms_test.go +++ b/kms/awskms/awskms_test.go @@ -181,10 +181,20 @@ func TestKMS_CreateKey(t *testing.T) { SigningKey: "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936", }, }, false}, + {"ok ed25519", fields{okClient}, args{&apiv1.CreateKeyRequest{ + Name: "awskms:name=root", + SignatureAlgorithm: apiv1.PureEd25519, + }}, &apiv1.CreateKeyResponse{ + Name: "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936", + PublicKey: key, + CreateSignerRequest: apiv1.CreateSignerRequest{ + SigningKey: "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936", + }, + }, false}, {"fail empty", fields{okClient}, args{&apiv1.CreateKeyRequest{}}, nil, true}, {"fail unsupported alg", fields{okClient}, args{&apiv1.CreateKeyRequest{ Name: "root", - SignatureAlgorithm: apiv1.PureEd25519, + SignatureAlgorithm: apiv1.SignatureAlgorithm(100), }}, nil, true}, {"fail unsupported bits", fields{okClient}, args{&apiv1.CreateKeyRequest{ Name: "root", @@ -395,7 +405,7 @@ func Test_getCustomerMasterKeySpecMapping(t *testing.T) { {"ECDSAWithSHA256", args{apiv1.ECDSAWithSHA256, 0}, types.KeySpecEccNistP256, assert.NoError}, {"ECDSAWithSHA384", args{apiv1.ECDSAWithSHA384, 0}, types.KeySpecEccNistP384, assert.NoError}, {"ECDSAWithSHA512", args{apiv1.ECDSAWithSHA512, 0}, types.KeySpecEccNistP521, assert.NoError}, - {"fail Ed25519", args{apiv1.PureEd25519, 0}, "", assert.Error}, + {"Ed25519", args{apiv1.PureEd25519, 0}, types.KeySpecEccNistEdwards25519, assert.NoError}, {"fail type switch", args{apiv1.SignatureAlgorithm(100), 0}, "", assert.Error}, } for _, tt := range tests { diff --git a/kms/awskms/signer.go b/kms/awskms/signer.go index 3ec8935f..ffcba955 100644 --- a/kms/awskms/signer.go +++ b/kms/awskms/signer.go @@ -5,13 +5,16 @@ package awskms import ( "crypto" "crypto/ecdsa" + "crypto/ed25519" "crypto/rsa" + "fmt" "io" "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/aws/aws-sdk-go-v2/service/kms/types" "github.com/pkg/errors" + "go.step.sm/crypto/internal/mldsa" "go.step.sm/crypto/pemutil" ) @@ -68,11 +71,33 @@ func (s *Signer) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byt return nil, err } + var messageType types.MessageType + switch alg { + case types.SigningAlgorithmSpecEd25519Sha512: + messageType = types.MessageTypeRaw + // AWS does not support Ed25519 (ED25519_SHA_512) with messages larger than 4096 bytes + if len(digest) > 4096 { + return nil, fmt.Errorf("awskms Sign failed: message must have length less than or equal to 4096") + } + case types.SigningAlgorithmSpecMlDsaShake256: + if len(digest) > 4096 { + messageType = types.MessageTypeExternalMu + digest, err = mldsaMessageHash(s.publicKey, digest, opts) + if err != nil { + return nil, fmt.Errorf("awskms Sign failed: %w", err) + } + } else { + messageType = types.MessageTypeRaw + } + default: + messageType = types.MessageTypeDigest + } + req := &kms.SignInput{ KeyId: pointer(s.keyID), SigningAlgorithm: alg, Message: digest, - MessageType: types.MessageTypeDigest, + MessageType: messageType, } ctx, cancel := defaultContext() @@ -120,7 +145,25 @@ func getSigningAlgorithm(key crypto.PublicKey, opts crypto.SignerOpts) (types.Si default: return "", errors.Errorf("unsupported hash function %v", h) } + case *mldsa.PublicKey: + return types.SigningAlgorithmSpecMlDsaShake256, nil + case ed25519.PublicKey: + return types.SigningAlgorithmSpecEd25519Sha512, nil default: return "", errors.Errorf("unsupported key type %T", key) } } + +func mldsaMessageHash(pub crypto.PublicKey, msg []byte, o crypto.SignerOpts) ([]byte, error) { + pk, ok := pub.(*mldsa.PublicKey) + if !ok { + return nil, fmt.Errorf("unexpected type %T", pub) + } + + opts, _ := o.(*mldsa.Options) + h, err := mldsa.MessageHash(pk, msg, opts) + if err != nil { + return nil, err + } + return h[:], nil +} diff --git a/kms/awskms/signer_test.go b/kms/awskms/signer_test.go index bc002762..2baefa18 100644 --- a/kms/awskms/signer_test.go +++ b/kms/awskms/signer_test.go @@ -4,6 +4,7 @@ import ( "context" "crypto" "crypto/ecdsa" + "crypto/ed25519" "crypto/rand" "crypto/rsa" "fmt" @@ -14,6 +15,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/aws/aws-sdk-go-v2/service/kms/types" + "go.step.sm/crypto/internal/mldsa" "go.step.sm/crypto/pemutil" ) @@ -173,6 +175,8 @@ func Test_getSigningAlgorithm(t *testing.T) { {"P256", args{&ecdsa.PublicKey{}, crypto.SHA256}, "ECDSA_SHA_256", false}, {"P384", args{&ecdsa.PublicKey{}, crypto.SHA384}, "ECDSA_SHA_384", false}, {"P521", args{&ecdsa.PublicKey{}, crypto.SHA512}, "ECDSA_SHA_512", false}, + {"Ed25519", args{ed25519.PublicKey{}, crypto.Hash(0)}, "ED25519_SHA_512", false}, + {"ML-DSA", args{&mldsa.PublicKey{}, crypto.Hash(0)}, "ML_DSA_SHAKE_256", false}, {"fail type", args{[]byte("key"), crypto.SHA256}, "", true}, {"fail rsa alg", args{&rsa.PublicKey{}, crypto.MD5}, "", true}, {"fail ecdsa alg", args{&ecdsa.PublicKey{}, crypto.MD5}, "", true}, diff --git a/kms/cloudkms/attestation_test.go b/kms/cloudkms/attestation_test.go index 28f95ff7..dd1ea205 100644 --- a/kms/cloudkms/attestation_test.go +++ b/kms/cloudkms/attestation_test.go @@ -757,7 +757,7 @@ func TestValidateCaviumRoot(t *testing.T) { req.Header.Set("Cache-Control", "no-cache") req.Header.Set("Referer", "https://www.marvell.com/products/security-solutions/nitrox-hs-adapters/software-key-attestation.html") req.Header.Set("Accept-Language", "en-US") - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36") + req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36") resp, err := http.DefaultClient.Do(req) require.NoError(t, err) diff --git a/kms/cloudkms/cloudkms.go b/kms/cloudkms/cloudkms.go index adae96f5..38547cd5 100644 --- a/kms/cloudkms/cloudkms.go +++ b/kms/cloudkms/cloudkms.go @@ -42,7 +42,7 @@ var protectionLevelMapping = map[apiv1.ProtectionLevel]kmspb.ProtectionLevel{ // // Cloud KMS does not support SHA384WithRSA, SHA384WithRSAPSS, SHA384WithRSAPSS, // ECDSAWithSHA512, and PureEd25519. -var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]interface{}{ +var signatureAlgorithmMapping = patchSignatureAlgorithmMapping(map[apiv1.SignatureAlgorithm]interface{}{ apiv1.UnspecifiedSignAlgorithm: kmspb.CryptoKeyVersion_CRYPTO_KEY_VERSION_ALGORITHM_UNSPECIFIED, apiv1.SHA256WithRSA: map[int]kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm{ 0: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_3072_SHA256, @@ -66,9 +66,9 @@ var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]interface{}{ }, apiv1.ECDSAWithSHA256: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, apiv1.ECDSAWithSHA384: kmspb.CryptoKeyVersion_EC_SIGN_P384_SHA384, -} +}) -var cryptoKeyVersionMapping = map[kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm]x509.SignatureAlgorithm{ +var cryptoKeyVersionMapping = patchCryptoKeyVersionMapping(map[kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm]x509.SignatureAlgorithm{ kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256: x509.ECDSAWithSHA256, kmspb.CryptoKeyVersion_EC_SIGN_P384_SHA384: x509.ECDSAWithSHA384, kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256: x509.SHA256WithRSA, @@ -79,7 +79,7 @@ var cryptoKeyVersionMapping = map[kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorit kmspb.CryptoKeyVersion_RSA_SIGN_PSS_3072_SHA256: x509.SHA256WithRSAPSS, kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA256: x509.SHA256WithRSAPSS, kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA512: x509.SHA512WithRSAPSS, -} +}) // KeyManagementClient defines the methods on KeyManagementClient that this // package will use. This interface will be used for unit testing. diff --git a/kms/cloudkms/cloudkms_go127.go b/kms/cloudkms/cloudkms_go127.go new file mode 100644 index 00000000..bf3b4713 --- /dev/null +++ b/kms/cloudkms/cloudkms_go127.go @@ -0,0 +1,24 @@ +//go:build go1.27 && !nocloudkms + +package cloudkms + +import ( + "crypto/x509" + + "cloud.google.com/go/kms/apiv1/kmspb" + "go.step.sm/crypto/kms/apiv1" +) + +func patchSignatureAlgorithmMapping(m map[apiv1.SignatureAlgorithm]interface{}) map[apiv1.SignatureAlgorithm]interface{} { + m[apiv1.MLDSA44] = kmspb.CryptoKeyVersion_PQ_SIGN_ML_DSA_44 + m[apiv1.MLDSA65] = kmspb.CryptoKeyVersion_PQ_SIGN_ML_DSA_65 + m[apiv1.MLDSA87] = kmspb.CryptoKeyVersion_PQ_SIGN_ML_DSA_87 + return m +} + +func patchCryptoKeyVersionMapping(m map[kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm]x509.SignatureAlgorithm) map[kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm]x509.SignatureAlgorithm { + m[kmspb.CryptoKeyVersion_PQ_SIGN_ML_DSA_44] = x509.MLDSA44 + m[kmspb.CryptoKeyVersion_PQ_SIGN_ML_DSA_65] = x509.MLDSA65 + m[kmspb.CryptoKeyVersion_PQ_SIGN_ML_DSA_87] = x509.MLDSA87 + return m +} diff --git a/kms/cloudkms/cloudkms_other.go b/kms/cloudkms/cloudkms_other.go new file mode 100644 index 00000000..5654c899 --- /dev/null +++ b/kms/cloudkms/cloudkms_other.go @@ -0,0 +1,18 @@ +//go:build !go1.27 && !nocloudkms + +package cloudkms + +import ( + "crypto/x509" + + "cloud.google.com/go/kms/apiv1/kmspb" + "go.step.sm/crypto/kms/apiv1" +) + +func patchSignatureAlgorithmMapping(m map[apiv1.SignatureAlgorithm]interface{}) map[apiv1.SignatureAlgorithm]interface{} { + return m +} + +func patchCryptoKeyVersionMapping(m map[kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm]x509.SignatureAlgorithm) map[kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm]x509.SignatureAlgorithm { + return m +} diff --git a/kms/cloudkms/signer.go b/kms/cloudkms/signer.go index 16fb712d..41ad265c 100644 --- a/kms/cloudkms/signer.go +++ b/kms/cloudkms/signer.go @@ -75,6 +75,9 @@ func (s *Signer) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byt req.Digest.Digest = &kmspb.Digest_Sha512{ Sha512: digest, } + case crypto.Hash(0): + req.Digest = nil + req.Data = digest default: return nil, errors.Errorf("unsupported hash function %v", h) } diff --git a/kms/softkms/softkms.go b/kms/softkms/softkms.go index 1e37c364..720f27ef 100644 --- a/kms/softkms/softkms.go +++ b/kms/softkms/softkms.go @@ -13,6 +13,7 @@ import ( "github.com/pkg/errors" + "go.step.sm/crypto/internal/mldsa" "go.step.sm/crypto/keyutil" "go.step.sm/crypto/kms/apiv1" "go.step.sm/crypto/kms/uri" @@ -43,6 +44,9 @@ var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]algorithmAttributes apiv1.ECDSAWithSHA384: {"EC", "P-384"}, apiv1.ECDSAWithSHA512: {"EC", "P-521"}, apiv1.PureEd25519: {"OKP", "Ed25519"}, + apiv1.MLDSA44: {"AKP", "ML-DSA-44"}, + apiv1.MLDSA65: {"AKP", "ML-DSA-65"}, + apiv1.MLDSA87: {"AKP", "ML-DSA-87"}, } // generateKey is used for testing purposes. @@ -148,7 +152,7 @@ func (k *SoftKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey switch vv := v.(type) { case *x509.Certificate: return vv.PublicKey, nil - case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey, x25519.PublicKey: + case *rsa.PublicKey, *ecdsa.PublicKey, *mldsa.PublicKey, ed25519.PublicKey, x25519.PublicKey: return vv, nil case crypto.Signer: return vv.Public(), nil diff --git a/pemutil/pem.go b/pemutil/pem.go index b7b41a25..1f5d3845 100644 --- a/pemutil/pem.go +++ b/pemutil/pem.go @@ -22,6 +22,7 @@ import ( "github.com/pkg/errors" "golang.org/x/crypto/ssh" + "go.step.sm/crypto/internal/mldsa" fileutils "go.step.sm/crypto/internal/utils/file" "go.step.sm/crypto/keyutil" "go.step.sm/crypto/x25519" @@ -558,7 +559,7 @@ func Serialize(in interface{}, opts ...Options) (*pem.Block, error) { var p *pem.Block var isPrivateKey bool switch k := in.(type) { - case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey: + case *rsa.PublicKey, *ecdsa.PublicKey, *mldsa.PublicKey, ed25519.PublicKey: b, err := x509.MarshalPKIXPublicKey(k) if err != nil { return nil, errors.WithStack(err) @@ -611,12 +612,12 @@ func Serialize(in interface{}, opts ...Options) (*pem.Block, error) { Bytes: b, } } - case ed25519.PrivateKey: + case ed25519.PrivateKey, *mldsa.PrivateKey: isPrivateKey = true switch { case !ctx.pkcs8 && ctx.openSSH: return SerializeOpenSSHPrivateKey(k, withContext(ctx)) - default: // Ed25519 keys will use pkcs8 by default + default: // Ed25519 and ML-DSA keys will use pkcs8 by default ctx.pkcs8 = true b, err := x509.MarshalPKCS8PrivateKey(k) if err != nil { @@ -638,7 +639,7 @@ func Serialize(in interface{}, opts ...Options) (*pem.Block, error) { Bytes: k.Raw, } default: - return nil, errors.Errorf("cannot serialize type '%T', value '%v'", k, k) + return nil, fmt.Errorf("cannot serialize type '%T', value '%v'", in, in) } if isPrivateKey {