From 15fe069c319481b4047c7b5a0237548343bc8ec9 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 4 Aug 2026 19:06:37 -0700 Subject: [PATCH 1/8] Add support for ML-DSA algorithms on cloudkms This commit adds support for creating keys and signing using ML-DSA-44, ML-DSA-65, and ML-DSA=87 algorithms on GCP cloud KMS. --- keyutil/key.go | 2 +- keyutil/key_go127.go | 18 +++++++++++++++++ keyutil/key_other.go | 9 +++++++++ kms/apiv1/requests.go | 6 ++++++ kms/cloudkms/cloudkms.go | 8 ++++---- kms/cloudkms/cloudkms_go127.go | 24 +++++++++++++++++++++++ kms/cloudkms/cloudkms_other.go | 18 +++++++++++++++++ kms/cloudkms/signer.go | 3 +++ pemutil/pem.go | 5 ++++- pemutil/pem_go127.go | 35 ++++++++++++++++++++++++++++++++++ pemutil/pem_other.go | 12 ++++++++++++ 11 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 keyutil/key_go127.go create mode 100644 keyutil/key_other.go create mode 100644 kms/cloudkms/cloudkms_go127.go create mode 100644 kms/cloudkms/cloudkms_other.go create mode 100644 pemutil/pem_go127.go create mode 100644 pemutil/pem_other.go diff --git a/keyutil/key.go b/keyutil/key.go index a8ec53d8..f6dd1583 100644 --- a/keyutil/key.go +++ b/keyutil/key.go @@ -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 extractKey(in) } } diff --git a/keyutil/key_go127.go b/keyutil/key_go127.go new file mode 100644 index 00000000..0c839d78 --- /dev/null +++ b/keyutil/key_go127.go @@ -0,0 +1,18 @@ +//go:build go1.27 + +package keyutil + +import ( + "crypto/mldsa" + + "github.com/pkg/errors" +) + +func extractKey(in any) (any, error) { + switch in.(type) { + case *mldsa.PublicKey, *mldsa.PrivateKey: + return in, nil + default: + return nil, errors.Errorf("cannot extract the key from type '%T'", in) + } +} diff --git a/keyutil/key_other.go b/keyutil/key_other.go new file mode 100644 index 00000000..1acfe46b --- /dev/null +++ b/keyutil/key_other.go @@ -0,0 +1,9 @@ +//go:build !go1.27 + +package keyutil + +import "github.com/pkg/errors" + +func extractKey(in any) (any, error) { + return nil, errors.Errorf("cannot extract the key from type '%T'", in) +} 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/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/pemutil/pem.go b/pemutil/pem.go index b7b41a25..88f0f41f 100644 --- a/pemutil/pem.go +++ b/pemutil/pem.go @@ -638,7 +638,10 @@ 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) + var err error + if p, isPrivateKey, err = serialize(in); err != nil { + return nil, err + } } if isPrivateKey { diff --git a/pemutil/pem_go127.go b/pemutil/pem_go127.go new file mode 100644 index 00000000..b60e271d --- /dev/null +++ b/pemutil/pem_go127.go @@ -0,0 +1,35 @@ +//go:build go1.27 + +package pemutil + +import ( + "crypto/mldsa" + "crypto/x509" + "encoding/pem" + "fmt" +) + +func serialize(in any) (*pem.Block, bool, error) { + switch in.(type) { + case *mldsa.PublicKey: + b, err := x509.MarshalPKIXPublicKey(in) + if err != nil { + return nil, false, err + } + return &pem.Block{ + Type: "PUBLIC KEY", + Bytes: b, + }, false, nil + case *mldsa.PrivateKey: + b, err := x509.MarshalPKCS8PrivateKey(in) + if err != nil { + return nil, false, err + } + return &pem.Block{ + Type: "PRIVATE KEY", + Bytes: b, + }, true, nil + default: + return nil, false, fmt.Errorf("cannot serialize type '%T', value '%v'", in, in) + } +} diff --git a/pemutil/pem_other.go b/pemutil/pem_other.go new file mode 100644 index 00000000..5116a271 --- /dev/null +++ b/pemutil/pem_other.go @@ -0,0 +1,12 @@ +//go:build !go1.27 + +package pemutil + +import ( + "encoding/pem" + "fmt" +) + +func serialize(in any) (*pem.Block, bool, error) { + return nil, false, fmt.Errorf("cannot serialize type '%T', value '%v'", in, in) +} From a31136cd380997f5456a552875082a3f5c90c06a Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 5 Aug 2026 11:24:18 -0700 Subject: [PATCH 2/8] Use an internal package to alias mldsa on Go 1.27 --- internal/mldsa/mldsa_go127.go | 11 +++++++++++ internal/mldsa/mldsa_other.go | 7 +++++++ keyutil/key.go | 4 +++- keyutil/key_go127.go | 18 ------------------ keyutil/key_other.go | 9 --------- pemutil/pem.go | 12 +++++------- pemutil/pem_go127.go | 35 ----------------------------------- pemutil/pem_other.go | 12 ------------ 8 files changed, 26 insertions(+), 82 deletions(-) create mode 100644 internal/mldsa/mldsa_go127.go create mode 100644 internal/mldsa/mldsa_other.go delete mode 100644 keyutil/key_go127.go delete mode 100644 keyutil/key_other.go delete mode 100644 pemutil/pem_go127.go delete mode 100644 pemutil/pem_other.go diff --git a/internal/mldsa/mldsa_go127.go b/internal/mldsa/mldsa_go127.go new file mode 100644 index 00000000..02904c40 --- /dev/null +++ b/internal/mldsa/mldsa_go127.go @@ -0,0 +1,11 @@ +//go:build go1.27 + +package mldsa + +import ( + "crypto/mldsa" +) + +type PublicKey = mldsa.PublicKey + +type PrivateKey = mldsa.PrivateKey diff --git a/internal/mldsa/mldsa_other.go b/internal/mldsa/mldsa_other.go new file mode 100644 index 00000000..c421f3f4 --- /dev/null +++ b/internal/mldsa/mldsa_other.go @@ -0,0 +1,7 @@ +//go:build !go1.27 + +package mldsa + +type PublicKey struct{} + +type PrivateKey struct{} diff --git a/keyutil/key.go b/keyutil/key.go index f6dd1583..14fb22e8 100644 --- a/keyutil/key.go +++ b/keyutil/key.go @@ -16,6 +16,7 @@ import ( "github.com/pkg/errors" "golang.org/x/crypto/ssh" + "go.step.sm/crypto/internal/mldsa" "go.step.sm/crypto/x25519" ) @@ -133,6 +134,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 +149,7 @@ func ExtractKey(in interface{}) (interface{}, error) { case *ssh.Certificate: return ExtractKey(k.Key) default: - return extractKey(in) + return nil, errors.Errorf("cannot extract the key from type '%T'", in) } } diff --git a/keyutil/key_go127.go b/keyutil/key_go127.go deleted file mode 100644 index 0c839d78..00000000 --- a/keyutil/key_go127.go +++ /dev/null @@ -1,18 +0,0 @@ -//go:build go1.27 - -package keyutil - -import ( - "crypto/mldsa" - - "github.com/pkg/errors" -) - -func extractKey(in any) (any, error) { - switch in.(type) { - case *mldsa.PublicKey, *mldsa.PrivateKey: - return in, nil - default: - return nil, errors.Errorf("cannot extract the key from type '%T'", in) - } -} diff --git a/keyutil/key_other.go b/keyutil/key_other.go deleted file mode 100644 index 1acfe46b..00000000 --- a/keyutil/key_other.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !go1.27 - -package keyutil - -import "github.com/pkg/errors" - -func extractKey(in any) (any, error) { - return nil, errors.Errorf("cannot extract the key from type '%T'", in) -} diff --git a/pemutil/pem.go b/pemutil/pem.go index 88f0f41f..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,10 +639,7 @@ func Serialize(in interface{}, opts ...Options) (*pem.Block, error) { Bytes: k.Raw, } default: - var err error - if p, isPrivateKey, err = serialize(in); err != nil { - return nil, err - } + return nil, fmt.Errorf("cannot serialize type '%T', value '%v'", in, in) } if isPrivateKey { diff --git a/pemutil/pem_go127.go b/pemutil/pem_go127.go deleted file mode 100644 index b60e271d..00000000 --- a/pemutil/pem_go127.go +++ /dev/null @@ -1,35 +0,0 @@ -//go:build go1.27 - -package pemutil - -import ( - "crypto/mldsa" - "crypto/x509" - "encoding/pem" - "fmt" -) - -func serialize(in any) (*pem.Block, bool, error) { - switch in.(type) { - case *mldsa.PublicKey: - b, err := x509.MarshalPKIXPublicKey(in) - if err != nil { - return nil, false, err - } - return &pem.Block{ - Type: "PUBLIC KEY", - Bytes: b, - }, false, nil - case *mldsa.PrivateKey: - b, err := x509.MarshalPKCS8PrivateKey(in) - if err != nil { - return nil, false, err - } - return &pem.Block{ - Type: "PRIVATE KEY", - Bytes: b, - }, true, nil - default: - return nil, false, fmt.Errorf("cannot serialize type '%T', value '%v'", in, in) - } -} diff --git a/pemutil/pem_other.go b/pemutil/pem_other.go deleted file mode 100644 index 5116a271..00000000 --- a/pemutil/pem_other.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build !go1.27 - -package pemutil - -import ( - "encoding/pem" - "fmt" -) - -func serialize(in any) (*pem.Block, bool, error) { - return nil, false, fmt.Errorf("cannot serialize type '%T', value '%v'", in, in) -} From aadd66070bbc9e7f4cf93be9955f91c4e0532cbe Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 5 Aug 2026 12:09:48 -0700 Subject: [PATCH 3/8] Use kty AKP, Algorithm Key Pair for ML-DSA keys This commit uses the key type (kty) AKP for ML-DSA keys matching RFC 9964, ML-DSA for JSON Object Signing and Encryption (JOSE) and CBOR Object Signing and Encryption (COSE) It also adds support for softkms. --- internal/mldsa/mldsa_go127.go | 7 ++++++ internal/mldsa/mldsa_other.go | 34 ++++++++++++++++++++++++++++ keyutil/key.go | 42 +++++++++++++++++++++++++++++++++-- kms/softkms/softkms.go | 6 ++++- 4 files changed, 86 insertions(+), 3 deletions(-) diff --git a/internal/mldsa/mldsa_go127.go b/internal/mldsa/mldsa_go127.go index 02904c40..9980cb39 100644 --- a/internal/mldsa/mldsa_go127.go +++ b/internal/mldsa/mldsa_go127.go @@ -9,3 +9,10 @@ import ( type PublicKey = mldsa.PublicKey type PrivateKey = mldsa.PrivateKey + +var ( + GenerateKey = mldsa.GenerateKey + MLDSA44 = mldsa.MLDSA44 + MLDSA65 = mldsa.MLDSA65 + MLDSA87 = mldsa.MLDSA87 +) diff --git a/internal/mldsa/mldsa_other.go b/internal/mldsa/mldsa_other.go index c421f3f4..4b27c628 100644 --- a/internal/mldsa/mldsa_other.go +++ b/internal/mldsa/mldsa_other.go @@ -2,6 +2,40 @@ package mldsa +import ( + "crypto" + "errors" + "io" +) + +var errNotSupported = errors.New("mldsa is not supported") + type PublicKey struct{} type PrivateKey struct{} + +func (sk *PrivateKey) Public() crypto.PublicKey { + return nil +} + +func (sk *PrivateKey) Sign(_ io.Reader, message []byte, opts crypto.SignerOpts) (signature []byte, err error) { + return nil, errNotSupported +} + +type Parameters struct{} + +func MLDSA44() Parameters { + return Parameters{} +} + +func MLDSA65() Parameters { + return Parameters{} +} + +func MLDSA87() Parameters { + return Parameters{} +} + +func GenerateKey(params Parameters) (*PrivateKey, error) { + return nil, errNotSupported +} diff --git a/keyutil/key.go b/keyutil/key.go index 14fb22e8..39386db3 100644 --- a/keyutil/key.go +++ b/keyutil/key.go @@ -10,6 +10,7 @@ import ( "crypto/rand" "crypto/rsa" "crypto/x509" + "fmt" "math/big" "sync/atomic" @@ -27,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.MLDSA65 // DefaultSignatureAlgorithm is the default signature algorithm used on a // certificate with the default key type. DefaultSignatureAlgorithm = x509.ECDSAWithSHA256 @@ -88,7 +91,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) @@ -114,7 +117,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": @@ -123,6 +127,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) } @@ -254,6 +260,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/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 From 69ba5736d55906cf51980065a51a6a81b3e0bd1d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 6 Aug 2026 15:27:12 -0700 Subject: [PATCH 4/8] Add full support of ML-DSA in keyutil package --- internal/mldsa/mldsa_go127.go | 13 +++++- internal/mldsa/mldsa_other.go | 54 +++++++++++++++++++++---- keyutil/key.go | 41 ++++--------------- keyutil/key_test.go | 75 +++++++++++++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 42 deletions(-) diff --git a/internal/mldsa/mldsa_go127.go b/internal/mldsa/mldsa_go127.go index 9980cb39..0959762c 100644 --- a/internal/mldsa/mldsa_go127.go +++ b/internal/mldsa/mldsa_go127.go @@ -6,12 +6,23 @@ import ( "crypto/mldsa" ) -type PublicKey = mldsa.PublicKey +// Enabled returns if mdlsa package is implemented. It will return true in Go +// 1.27+ and false on lower versions. +func Enabled() bool { + return 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 index 4b27c628..42e5e923 100644 --- a/internal/mldsa/mldsa_other.go +++ b/internal/mldsa/mldsa_other.go @@ -10,32 +10,70 @@ import ( var errNotSupported = errors.New("mldsa is not supported") -type PublicKey struct{} +// Enabled returns if mdlsa package is implemented. It will return true in Go +// 1.27+ and false on lower versions. +func Enabled() bool { + return false +} + +type Parameters struct{} + +func MLDSA44() Parameters { + return Parameters{} +} + +func MLDSA65() Parameters { + return Parameters{} +} + +func MLDSA87() Parameters { + return Parameters{} +} + +type Options struct { + Context string +} type PrivateKey struct{} -func (sk *PrivateKey) Public() crypto.PublicKey { +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 Parameters struct{} +type PublicKey struct{} -func MLDSA44() Parameters { - return Parameters{} +func (pk *PublicKey) Bytes() []byte { + return nil } -func MLDSA65() Parameters { - return Parameters{} +func (pk *PublicKey) Equal(x crypto.PublicKey) bool { + return false } -func MLDSA87() Parameters { +func (pk *PublicKey) Parameters() Parameters { return Parameters{} } func GenerateKey(params Parameters) (*PrivateKey, error) { return nil, errNotSupported } + +func Verify(pk *PublicKey, message []byte, signature []byte, opts *Options) error { + return errNotSupported +} diff --git a/keyutil/key.go b/keyutil/key.go index 39386db3..d42fe4d9 100644 --- a/keyutil/key.go +++ b/keyutil/key.go @@ -59,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 @@ -173,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) diff --git a/keyutil/key_test.go b/keyutil/key_test.go index d9f1d629..a2121be6 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.Enabled() { + 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.Enabled()}, + {"ML-DSA-65", randReader, args{"AKP", "ML-DSA-65", 0}, assertKey, crypto.Hash(0), !mldsa.Enabled()}, + {"ML-DSA-87", randReader, args{"AKP", "ML-DSA-87", 0}, assertKey, crypto.Hash(0), !mldsa.Enabled()}, {"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.Enabled() + 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.Enabled() + 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.Enabled()}, // 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.Enabled() { + 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.Enabled()}, {"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.Enabled()}, {"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}, From d6b2674ff4f8da3e45a3df4df40e9f422d1e2ae5 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 6 Aug 2026 16:47:14 -0700 Subject: [PATCH 5/8] Fix linter issue --- internal/mldsa/mldsa_other.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/mldsa/mldsa_other.go b/internal/mldsa/mldsa_other.go index 42e5e923..6e8eaae6 100644 --- a/internal/mldsa/mldsa_other.go +++ b/internal/mldsa/mldsa_other.go @@ -74,6 +74,6 @@ func GenerateKey(params Parameters) (*PrivateKey, error) { return nil, errNotSupported } -func Verify(pk *PublicKey, message []byte, signature []byte, opts *Options) error { +func Verify(pk *PublicKey, message, signature []byte, opts *Options) error { return errNotSupported } From c418c7802848d0482f96b6f265f0f768a95bf179 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 11 Aug 2026 14:52:01 -0700 Subject: [PATCH 6/8] Add ML-DSA and Ed25519 in AWSKMS --- internal/mldsa/helpers.go | 39 ++++++++++++++++++++++++++++++ internal/mldsa/mldsa_other.go | 4 ++++ keyutil/key.go | 2 +- kms/awskms/awskms.go | 5 ++-- kms/awskms/awskms_go127.go | 15 ++++++++++++ kms/awskms/awskms_other.go | 9 +++++++ kms/awskms/awskms_test.go | 14 +++++++++-- kms/awskms/signer.go | 45 ++++++++++++++++++++++++++++++++++- kms/awskms/signer_test.go | 4 ++++ 9 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 internal/mldsa/helpers.go create mode 100644 kms/awskms/awskms_go127.go create mode 100644 kms/awskms/awskms_other.go 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_other.go b/internal/mldsa/mldsa_other.go index 6e8eaae6..6d0e4804 100644 --- a/internal/mldsa/mldsa_other.go +++ b/internal/mldsa/mldsa_other.go @@ -34,6 +34,10 @@ type Options struct { Context string } +func (o *Options) HashFunc() crypto.Hash { + return 0 +} + type PrivateKey struct{} func (sk *PrivateKey) Bytes() []byte { diff --git a/keyutil/key.go b/keyutil/key.go index d42fe4d9..c41f967f 100644 --- a/keyutil/key.go +++ b/keyutil/key.go @@ -29,7 +29,7 @@ var ( // DefaultKeyCurve is the default curve of a private key. DefaultKeyCurve = "P-256" // DefaultKeyAlgorithm is the default algorithm for AKP (ML-DSA) keys. - DefaultKeyAlgorithm = mldsa.MLDSA65 + DefaultKeyAlgorithm = mldsa.MLDSA44 // DefaultSignatureAlgorithm is the default signature algorithm used on a // certificate with the default key type. DefaultSignatureAlgorithm = x509.ECDSAWithSHA256 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}, From 6ae40ccbc7d909690bf79375c66925afeb3bf431 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 18 Aug 2026 18:29:58 -0700 Subject: [PATCH 7/8] Use mldsa.Required constant instead of a method --- internal/mldsa/mldsa_go127.go | 8 +++----- internal/mldsa/mldsa_other.go | 8 +++----- keyutil/key_test.go | 20 ++++++++++---------- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/internal/mldsa/mldsa_go127.go b/internal/mldsa/mldsa_go127.go index 0959762c..a1e8a9a6 100644 --- a/internal/mldsa/mldsa_go127.go +++ b/internal/mldsa/mldsa_go127.go @@ -6,11 +6,9 @@ import ( "crypto/mldsa" ) -// Enabled returns if mdlsa package is implemented. It will return true in Go -// 1.27+ and false on lower versions. -func Enabled() bool { - return true -} +// 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 diff --git a/internal/mldsa/mldsa_other.go b/internal/mldsa/mldsa_other.go index 6d0e4804..0ca764ec 100644 --- a/internal/mldsa/mldsa_other.go +++ b/internal/mldsa/mldsa_other.go @@ -10,11 +10,9 @@ import ( var errNotSupported = errors.New("mldsa is not supported") -// Enabled returns if mdlsa package is implemented. It will return true in Go -// 1.27+ and false on lower versions. -func Enabled() bool { - return false -} +// 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{} diff --git a/keyutil/key_test.go b/keyutil/key_test.go index a2121be6..1f1fea31 100644 --- a/keyutil/key_test.go +++ b/keyutil/key_test.go @@ -67,7 +67,7 @@ func must(args ...interface{}) interface{} { func shouldMLDSA(t *testing.T, p mldsa.Parameters) *mldsa.PrivateKey { t.Helper() k, err := mldsa.GenerateKey(p) - if mldsa.Enabled() { + if mldsa.Supported { require.NoError(t, err) } else { require.Error(t, err) @@ -326,9 +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.Enabled()}, - {"ML-DSA-65", randReader, args{"AKP", "ML-DSA-65", 0}, assertKey, crypto.Hash(0), !mldsa.Enabled()}, - {"ML-DSA-87", randReader, args{"AKP", "ML-DSA-87", 0}, assertKey, crypto.Hash(0), !mldsa.Enabled()}, + {"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}, @@ -422,7 +422,7 @@ func TestGenerateKeyPair(t *testing.T) { } } - mldsaEnabled := mldsa.Enabled() + mldsaEnabled := mldsa.Supported assertMLDSA := func(p mldsa.Parameters) func(t *testing.T, got, got1 any) { if !mldsaEnabled { return assertNil() @@ -580,7 +580,7 @@ func TestGenerateSigner(t *testing.T) { } } - mldsaEnabled := mldsa.Enabled() + mldsaEnabled := mldsa.Supported assertSignerMLDSA := func() func(t *testing.T, got crypto.Signer) { if !mldsaEnabled { return assertNil() @@ -710,7 +710,7 @@ 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.Enabled()}, + {"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}, @@ -783,7 +783,7 @@ 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.Enabled() { + if x, ok := key.(*mldsa.PrivateKey); ok && !mldsa.Supported { return x } @@ -821,12 +821,12 @@ 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.Enabled()}, + {"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.Enabled()}, + {"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}, From 70f5c0cff853ca4549a97346545cf612db244e0d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 18 Aug 2026 18:30:27 -0700 Subject: [PATCH 8/8] Fix TestValidateCaviumRoot on Go 1.27 For some reason Go 1.27 gets an error when downloading the Cavium root certificate. Changing the user agent fixes the issue. --- kms/cloudkms/attestation_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)