From 37d2c9cc64330bbe688624ff47e31bb14a4b3505 Mon Sep 17 00:00:00 2001 From: Rachel Culpepper <84159930+rculpepper@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:58:46 -0600 Subject: [PATCH] add ce changes for ecdsa hybrid (#29123) --- builtin/logical/transit/path_keys.go | 59 ++++- builtin/logical/transit/path_keys_test.go | 36 +++ sdk/helper/keysutil/lock_manager.go | 15 ++ sdk/helper/keysutil/policy.go | 296 ++++++++++++---------- sdk/helper/keysutil/policy_test.go | 1 + 5 files changed, 273 insertions(+), 134 deletions(-) diff --git a/builtin/logical/transit/path_keys.go b/builtin/logical/transit/path_keys.go index e00ba9c51f95..3c7d16f74ed6 100644 --- a/builtin/logical/transit/path_keys.go +++ b/builtin/logical/transit/path_keys.go @@ -135,6 +135,16 @@ key.`, Description: `The parameter set to use. Applies to ML-DSA and SLH-DSA key types. For ML-DSA key types, valid values are 44, 65, or 87.`, }, + "hybrid_key_type_pqc": { + Type: framework.TypeString, + Description: `The key type of the post-quantum key to use for hybrid signature schemes. +Supported types are: ML-DSA.`, + }, + "hybrid_key_type_ec": { + Type: framework.TypeString, + Description: `The key type of the elliptic curve key to use for hybrid signature schemes. +Supported types are: ecdsa-p256, ecdsa-p384, ecdsa-p521.`, + }, }, Operations: map[logical.Operation]framework.OperationHandler{ @@ -184,6 +194,8 @@ func (b *backend) pathPolicyWrite(ctx context.Context, req *logical.Request, d * managedKeyName := d.Get("managed_key_name").(string) managedKeyId := d.Get("managed_key_id").(string) parameterSet := d.Get("parameter_set").(string) + pqcKeyType := d.Get("hybrid_key_type_pqc").(string) + ecKeyType := d.Get("hybrid_key_type_ec").(string) if autoRotatePeriod != 0 && autoRotatePeriod < time.Hour { return logical.ErrorResponse("auto rotate period must be 0 to disable or at least an hour"), nil @@ -241,6 +253,16 @@ func (b *backend) pathPolicyWrite(ctx context.Context, req *logical.Request, d * return logical.ErrorResponse(fmt.Sprintf("invalid parameter set %s for key type %s", parameterSet, keyType)), logical.ErrInvalidRequest } + polReq.ParameterSet = parameterSet + case "hybrid": + polReq.KeyType = keysutil.KeyType_HYBRID + + var err error + polReq.HybridConfig, err = getHybridKeyConfig(pqcKeyType, parameterSet, ecKeyType) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("invalid config for hybrid key: %s", err)), logical.ErrInvalidRequest + } + polReq.ParameterSet = parameterSet default: return logical.ErrorResponse(fmt.Sprintf("unknown key type %v", keyType)), logical.ErrInvalidRequest @@ -393,6 +415,11 @@ func (b *backend) formatKeyPolicy(p *keysutil.Policy, context []byte) (*logical. resp.Data["parameter_set"] = p.ParameterSet } + if p.Type == keysutil.KeyType_HYBRID { + resp.Data["hybrid_key_type_pqc"] = p.HybridConfig.PQCKeyType.String() + resp.Data["hybrid_key_type_ec"] = p.HybridConfig.ECKeyType.String() + } + switch p.Type { case keysutil.KeyType_AES128_GCM96, keysutil.KeyType_AES256_GCM96, keysutil.KeyType_ChaCha20_Poly1305: retKeys := map[string]int64{} @@ -401,7 +428,7 @@ func (b *backend) formatKeyPolicy(p *keysutil.Policy, context []byte) (*logical. } resp.Data["keys"] = retKeys - case keysutil.KeyType_ECDSA_P256, keysutil.KeyType_ECDSA_P384, keysutil.KeyType_ECDSA_P521, keysutil.KeyType_ED25519, keysutil.KeyType_RSA2048, keysutil.KeyType_RSA3072, keysutil.KeyType_RSA4096, keysutil.KeyType_ML_DSA: + case keysutil.KeyType_ECDSA_P256, keysutil.KeyType_ECDSA_P384, keysutil.KeyType_ECDSA_P521, keysutil.KeyType_ED25519, keysutil.KeyType_RSA2048, keysutil.KeyType_RSA3072, keysutil.KeyType_RSA4096, keysutil.KeyType_ML_DSA, keysutil.KeyType_HYBRID: retKeys := map[string]map[string]interface{}{} for k, v := range p.Keys { key := asymKey{ @@ -488,6 +515,36 @@ func (b *backend) pathPolicyDelete(ctx context.Context, req *logical.Request, d return nil, nil } +func getHybridKeyConfig(pqcKeyType, parameterSet, ecKeyType string) (keysutil.HybridKeyConfig, error) { + config := keysutil.HybridKeyConfig{} + + switch pqcKeyType { + case "ml-dsa": + config.PQCKeyType = keysutil.KeyType_ML_DSA + + if parameterSet != keysutil.ParameterSet_ML_DSA_44 && + parameterSet != keysutil.ParameterSet_ML_DSA_65 && + parameterSet != keysutil.ParameterSet_ML_DSA_87 { + return keysutil.HybridKeyConfig{}, fmt.Errorf("invalid parameter set %s for key type %s", parameterSet, pqcKeyType) + } + default: + return keysutil.HybridKeyConfig{}, fmt.Errorf("invalid PQC key type: %s", pqcKeyType) + } + + switch ecKeyType { + case "ecdsa-p256": + config.ECKeyType = keysutil.KeyType_ECDSA_P256 + case "ecdsa-p384": + config.ECKeyType = keysutil.KeyType_ECDSA_P384 + case "ecdsa-p521": + config.ECKeyType = keysutil.KeyType_ECDSA_P521 + default: + return keysutil.HybridKeyConfig{}, fmt.Errorf("invalid key type for hybrid key: %s", ecKeyType) + } + + return config, nil +} + const pathPolicyHelpSyn = `Managed named encryption keys` const pathPolicyHelpDesc = ` diff --git a/builtin/logical/transit/path_keys_test.go b/builtin/logical/transit/path_keys_test.go index 8219dcbfbff5..ed6e84583469 100644 --- a/builtin/logical/transit/path_keys_test.go +++ b/builtin/logical/transit/path_keys_test.go @@ -254,6 +254,42 @@ func TestTransit_CreateKey(t *testing.T) { creationParams: map[string]interface{}{"type": "ml-dsa", "parameter_set": "87"}, entOnly: true, }, + "Hybrid ML-DSA-44-ECDSA-P256": { + creationParams: map[string]interface{}{"type": "hybrid", "parameter_set": "44", "hybrid_key_type_ec": "ecdsa-p256", "hybrid_key_type_pqc": "ml-dsa"}, + entOnly: true, + }, + "Hybrid ML-DSA-44-ECDSA-P384": { + creationParams: map[string]interface{}{"type": "hybrid", "parameter_set": "44", "hybrid_key_type_ec": "ecdsa-p384", "hybrid_key_type_pqc": "ml-dsa"}, + entOnly: true, + }, + "Hybrid ML-DSA-44-ECDSA-P521": { + creationParams: map[string]interface{}{"type": "hybrid", "parameter_set": "44", "hybrid_key_type_ec": "ecdsa-p521", "hybrid_key_type_pqc": "ml-dsa"}, + entOnly: true, + }, + "Hybrid ML-DSA-65-ECDSA-P256": { + creationParams: map[string]interface{}{"type": "ml-dsa", "parameter_set": "65", "hybrid_key_type_ec": "ecdsa-p256", "hybrid_key_type_pqc": "ml-dsa"}, + entOnly: true, + }, + "Hybrid ML-DSA-65-ECDSA-P384": { + creationParams: map[string]interface{}{"type": "ml-dsa", "parameter_set": "65", "hybrid_key_type_ec": "ecdsa-p384", "hybrid_key_type_pqc": "ml-dsa"}, + entOnly: true, + }, + "Hybrid ML-DSA-65-ECDSA-P521": { + creationParams: map[string]interface{}{"type": "ml-dsa", "parameter_set": "65", "hybrid_key_type_ec": "ecdsa-p521", "hybrid_key_type_pqc": "ml-dsa"}, + entOnly: true, + }, + "Hybrid ML-DSA-87-ECDSA-P256": { + creationParams: map[string]interface{}{"type": "ml-dsa", "parameter_set": "87", "hybrid_key_type_ec": "ecdsa-p256", "hybrid_key_type_pqc": "ml-dsa"}, + entOnly: true, + }, + "Hybrid ML-DSA-87-ECDSA-P384": { + creationParams: map[string]interface{}{"type": "ml-dsa", "parameter_set": "87", "hybrid_key_type_ec": "ecdsa-p384", "hybrid_key_type_pqc": "ml-dsa"}, + entOnly: true, + }, + "Hybrid ML-DSA-87-ECDSA-P521": { + creationParams: map[string]interface{}{"type": "ml-dsa", "parameter_set": "87", "hybrid_key_type_ec": "ecdsa-p521", "hybrid_key_type_pqc": "ml-dsa"}, + entOnly: true, + }, "bad key type": { creationParams: map[string]interface{}{"type": "fake-key-type"}, shouldError: true, diff --git a/sdk/helper/keysutil/lock_manager.go b/sdk/helper/keysutil/lock_manager.go index 435c361df48b..6b601227790d 100644 --- a/sdk/helper/keysutil/lock_manager.go +++ b/sdk/helper/keysutil/lock_manager.go @@ -71,6 +71,14 @@ type PolicyRequest struct { // ParameterSet indicates the parameter set to use with ML-DSA and SLH-DSA keys ParameterSet string + + // HybridConfig contains the key types and parameters for hybrid keys + HybridConfig HybridKeyConfig +} + +type HybridKeyConfig struct { + PQCKeyType KeyType + ECKeyType KeyType } type LockManager struct { @@ -412,6 +420,12 @@ func (lm *LockManager) GetPolicy(ctx context.Context, req PolicyRequest, rand io return nil, false, fmt.Errorf("key derivation and convergent encryption not supported for keys of type %v", req.KeyType) } + case KeyType_HYBRID: + if req.Derived || req.Convergent { + cleanup() + return nil, false, fmt.Errorf("key derivation and convergent encryption not supported for keys of type %v", req.KeyType) + } + default: cleanup() return nil, false, fmt.Errorf("unsupported key type %v", req.KeyType) @@ -427,6 +441,7 @@ func (lm *LockManager) GetPolicy(ctx context.Context, req PolicyRequest, rand io AutoRotatePeriod: req.AutoRotatePeriod, KeySize: req.KeySize, ParameterSet: req.ParameterSet, + HybridConfig: req.HybridConfig, } if req.Derived { diff --git a/sdk/helper/keysutil/policy.go b/sdk/helper/keysutil/policy.go index 3e4be8b1cd71..534827acdd88 100644 --- a/sdk/helper/keysutil/policy.go +++ b/sdk/helper/keysutil/policy.go @@ -73,6 +73,7 @@ const ( KeyType_AES128_CMAC KeyType_AES256_CMAC KeyType_ML_DSA + KeyType_HYBRID // If adding to this list please update allTestKeyTypes in policy_test.go ) @@ -189,7 +190,7 @@ func (kt KeyType) DecryptionSupported() bool { func (kt KeyType) SigningSupported() bool { switch kt { - case KeyType_ECDSA_P256, KeyType_ECDSA_P384, KeyType_ECDSA_P521, KeyType_ED25519, KeyType_RSA2048, KeyType_RSA3072, KeyType_RSA4096, KeyType_MANAGED_KEY, KeyType_ML_DSA: + case KeyType_ECDSA_P256, KeyType_ECDSA_P384, KeyType_ECDSA_P521, KeyType_ED25519, KeyType_RSA2048, KeyType_RSA3072, KeyType_RSA4096, KeyType_MANAGED_KEY, KeyType_ML_DSA, KeyType_HYBRID: return true } return false @@ -241,7 +242,7 @@ func (kt KeyType) HMACSupported() bool { func (kt KeyType) IsPQC() bool { switch kt { - case KeyType_ML_DSA: + case KeyType_ML_DSA, KeyType_HYBRID: return true default: return false @@ -297,6 +298,8 @@ func (kt KeyType) String() string { return "aes256-cmac" case KeyType_ML_DSA: return "ml-dsa" + case KeyType_HYBRID: + return "hybrid" } return "[unknown]" @@ -570,6 +573,9 @@ type Policy struct { // ParameterSet indicates the parameter set to use with ML-DSA and SLH-DSA keys ParameterSet string + + // HybridConfig contains the key types and parameters for hybrid keys + HybridConfig HybridKeyConfig } func (p *Policy) Lock(exclusive bool) { @@ -1266,69 +1272,7 @@ func (p *Policy) SignWithOptions(ver int, context, input []byte, options *Signin switch p.Type { case KeyType_ECDSA_P256, KeyType_ECDSA_P384, KeyType_ECDSA_P521: - var curveBits int - var curve elliptic.Curve - switch p.Type { - case KeyType_ECDSA_P384: - curveBits = 384 - curve = elliptic.P384() - case KeyType_ECDSA_P521: - curveBits = 521 - curve = elliptic.P521() - default: - curveBits = 256 - curve = elliptic.P256() - } - - key := &ecdsa.PrivateKey{ - PublicKey: ecdsa.PublicKey{ - Curve: curve, - X: keyParams.EC_X, - Y: keyParams.EC_Y, - }, - D: keyParams.EC_D, - } - - r, s, err := ecdsa.Sign(rand.Reader, key, input) - if err != nil { - return nil, err - } - - switch marshaling { - case MarshalingTypeASN1: - // This is used by openssl and X.509 - sig, err = asn1.Marshal(ecdsaSignature{ - R: r, - S: s, - }) - if err != nil { - return nil, err - } - - case MarshalingTypeJWS: - // This is used by JWS - - // First we have to get the length of the curve in bytes. Although - // we only support 256 now, we'll do this in an agnostic way so we - // can reuse this marshaling if we support e.g. 521. Getting the - // number of bytes without rounding up would be 65.125 so we need - // to add one in that case. - keyLen := curveBits / 8 - if curveBits%8 > 0 { - keyLen++ - } - - // Now create the output array - sig = make([]byte, keyLen*2) - rb := r.Bytes() - sb := s.Bytes() - copy(sig[keyLen-len(rb):], rb) - copy(sig[2*keyLen-len(sb):], sb) - - default: - return nil, errutil.UserError{Err: "requested marshaling type is invalid"} - } - + sig, err = signWithECDSA(p.Type, keyParams, input, marshaling) case KeyType_ED25519: var key ed25519.PrivateKey @@ -1403,6 +1347,76 @@ func (p *Policy) SignWithOptions(ver int, context, input []byte, options *Signin return res, nil } +func signWithECDSA(keyType KeyType, keyParams KeyEntry, input []byte, marshaling MarshalingType) ([]byte, error) { + var curveBits int + var curve elliptic.Curve + switch keyType { + case KeyType_ECDSA_P256: + curveBits = 256 + curve = elliptic.P256() + case KeyType_ECDSA_P384: + curveBits = 384 + curve = elliptic.P384() + case KeyType_ECDSA_P521: + curveBits = 521 + curve = elliptic.P521() + default: + return nil, fmt.Errorf("invalid key type %s for ECDSA", keyType) + } + + key := &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{ + Curve: curve, + X: keyParams.EC_X, + Y: keyParams.EC_Y, + }, + D: keyParams.EC_D, + } + + r, s, err := ecdsa.Sign(rand.Reader, key, input) + if err != nil { + return nil, err + } + + var sig []byte + switch marshaling { + case MarshalingTypeASN1: + // This is used by openssl and X.509 + sig, err = asn1.Marshal(ecdsaSignature{ + R: r, + S: s, + }) + if err != nil { + return nil, err + } + + case MarshalingTypeJWS: + // This is used by JWS + + // First we have to get the length of the curve in bytes. Although + // we only support 256 now, we'll do this in an agnostic way so we + // can reuse this marshaling if we support e.g. 521. Getting the + // number of bytes without rounding up would be 65.125 so we need + // to add one in that case. + keyLen := curveBits / 8 + if curveBits%8 > 0 { + keyLen++ + } + + // Now create the output array + sig = make([]byte, keyLen*2) + rb := r.Bytes() + sb := s.Bytes() + copy(sig[keyLen-len(rb):], rb) + copy(sig[2*keyLen-len(sb):], sb) + + default: + return nil, errutil.UserError{Err: "requested marshaling type is invalid"} + } + + return sig, nil +} + func (p *Policy) VerifySignature(context, input []byte, hashAlgorithm HashType, sigAlgorithm string, marshaling MarshalingType, sig string) (bool, error) { return p.VerifySignatureWithOptions(context, input, sig, &SigningOptions{ HashAlgorithm: hashAlgorithm, @@ -1465,49 +1479,11 @@ func (p *Policy) VerifySignatureWithOptions(context, input []byte, sig string, o switch p.Type { case KeyType_ECDSA_P256, KeyType_ECDSA_P384, KeyType_ECDSA_P521: - var curve elliptic.Curve - switch p.Type { - case KeyType_ECDSA_P384: - curve = elliptic.P384() - case KeyType_ECDSA_P521: - curve = elliptic.P521() - default: - curve = elliptic.P256() - } - - var ecdsaSig ecdsaSignature - - switch marshaling { - case MarshalingTypeASN1: - rest, err := asn1.Unmarshal(sigBytes, &ecdsaSig) - if err != nil { - return false, errutil.UserError{Err: "supplied signature is invalid"} - } - if rest != nil && len(rest) != 0 { - return false, errutil.UserError{Err: "supplied signature contains extra data"} - } - - case MarshalingTypeJWS: - paramLen := len(sigBytes) / 2 - rb := sigBytes[:paramLen] - sb := sigBytes[paramLen:] - ecdsaSig.R = new(big.Int) - ecdsaSig.R.SetBytes(rb) - ecdsaSig.S = new(big.Int) - ecdsaSig.S.SetBytes(sb) - } - - keyParams, err := p.safeGetKeyEntry(ver) + key, err := p.safeGetKeyEntry(ver) if err != nil { return false, err } - key := &ecdsa.PublicKey{ - Curve: curve, - X: keyParams.EC_X, - Y: keyParams.EC_Y, - } - - return ecdsa.Verify(key, input, ecdsaSig.R, ecdsaSig.S), nil + return verifyWithECDSA(p.Type, key, input, sigBytes, marshaling) case KeyType_ED25519: var pub ed25519.PublicKey @@ -1586,6 +1562,50 @@ func (p *Policy) VerifySignatureWithOptions(context, input []byte, sig string, o } } +func verifyWithECDSA(keyType KeyType, keyParams KeyEntry, input, sigBytes []byte, marshaling MarshalingType) (bool, error) { + var curve elliptic.Curve + switch keyType { + case KeyType_ECDSA_P256: + curve = elliptic.P256() + case KeyType_ECDSA_P384: + curve = elliptic.P384() + case KeyType_ECDSA_P521: + curve = elliptic.P521() + default: + return false, fmt.Errorf("invalid key type %s for ECDSA", keyType) + } + + var ecdsaSig ecdsaSignature + + switch marshaling { + case MarshalingTypeASN1: + rest, err := asn1.Unmarshal(sigBytes, &ecdsaSig) + if err != nil { + return false, errutil.UserError{Err: "supplied signature is invalid"} + } + if rest != nil && len(rest) != 0 { + return false, errutil.UserError{Err: "supplied signature contains extra data"} + } + + case MarshalingTypeJWS: + paramLen := len(sigBytes) / 2 + rb := sigBytes[:paramLen] + sb := sigBytes[paramLen:] + ecdsaSig.R = new(big.Int) + ecdsaSig.R.SetBytes(rb) + ecdsaSig.S = new(big.Int) + ecdsaSig.S.SetBytes(sb) + } + + key := &ecdsa.PublicKey{ + Curve: curve, + X: keyParams.EC_X, + Y: keyParams.EC_Y, + } + + return ecdsa.Verify(key, input, ecdsaSig.R, ecdsaSig.S), nil +} + func (p *Policy) Import(ctx context.Context, storage logical.Storage, key []byte, randReader io.Reader) error { return p.ImportPublicOrPrivate(ctx, storage, key, true, randReader) } @@ -1772,36 +1792,9 @@ func (p *Policy) RotateInMemory(randReader io.Reader) (retErr error) { } case KeyType_ECDSA_P256, KeyType_ECDSA_P384, KeyType_ECDSA_P521: - var curve elliptic.Curve - switch p.Type { - case KeyType_ECDSA_P384: - curve = elliptic.P384() - case KeyType_ECDSA_P521: - curve = elliptic.P521() - default: - curve = elliptic.P256() - } - - privKey, err := ecdsa.GenerateKey(curve, rand.Reader) - if err != nil { + if err = generateECDSAKey(p.Type, &entry); err != nil { return err } - entry.EC_D = privKey.D - entry.EC_X = privKey.X - entry.EC_Y = privKey.Y - derBytes, err := x509.MarshalPKIXPublicKey(privKey.Public()) - if err != nil { - return errwrap.Wrapf("error marshaling public key: {{err}}", err) - } - pemBlock := &pem.Block{ - Type: "PUBLIC KEY", - Bytes: derBytes, - } - pemBytes := pem.EncodeToMemory(pemBlock) - if pemBytes == nil || len(pemBytes) == 0 { - return fmt.Errorf("error PEM-encoding public key") - } - entry.FormattedPublicKey = string(pemBytes) case KeyType_ED25519: // Go uses a 64-byte private key for Ed25519 keys (private+public, each @@ -2758,3 +2751,40 @@ func (p *Policy) ValidateAndPersistCertificateChain(ctx context.Context, keyVers p.Keys[strconv.Itoa(keyVersion)] = keyEntry return p.Persist(ctx, storage) } + +func generateECDSAKey(keyType KeyType, entry *KeyEntry) error { + var curve elliptic.Curve + switch keyType { + case KeyType_ECDSA_P256: + curve = elliptic.P256() + case KeyType_ECDSA_P384: + curve = elliptic.P384() + case KeyType_ECDSA_P521: + curve = elliptic.P521() + default: + return fmt.Errorf("invalid key type %s for ECDSA", keyType) + } + + privKey, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + return err + } + entry.EC_D = privKey.D + entry.EC_X = privKey.X + entry.EC_Y = privKey.Y + derBytes, err := x509.MarshalPKIXPublicKey(privKey.Public()) + if err != nil { + return errwrap.Wrapf("error marshaling public key: {{err}}", err) + } + pemBlock := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: derBytes, + } + pemBytes := pem.EncodeToMemory(pemBlock) + if pemBytes == nil || len(pemBytes) == 0 { + return fmt.Errorf("error PEM-encoding public key") + } + entry.FormattedPublicKey = string(pemBytes) + + return nil +} diff --git a/sdk/helper/keysutil/policy_test.go b/sdk/helper/keysutil/policy_test.go index dd125e0b88c1..7dfeec3ae58d 100644 --- a/sdk/helper/keysutil/policy_test.go +++ b/sdk/helper/keysutil/policy_test.go @@ -36,6 +36,7 @@ var allTestKeyTypes = []KeyType{ KeyType_AES256_GCM96, KeyType_ECDSA_P256, KeyType_ED25519, KeyType_RSA2048, KeyType_RSA4096, KeyType_ChaCha20_Poly1305, KeyType_ECDSA_P384, KeyType_ECDSA_P521, KeyType_AES128_GCM96, KeyType_RSA3072, KeyType_MANAGED_KEY, KeyType_HMAC, KeyType_AES128_CMAC, KeyType_AES256_CMAC, KeyType_ML_DSA, + KeyType_HYBRID, } func TestPolicy_KeyTypes(t *testing.T) {