From ce256df91646e67ed13d71731750ac72cb263662 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Mon, 1 Jun 2026 13:33:20 -0400 Subject: [PATCH 01/25] feat(policy,crypto): add ML-KEM 768/1024 algorithm support Add pure ML-KEM (Module-Lattice-Based Key-Encapsulation Mechanism) support alongside existing hybrid post-quantum algorithms. Changes: - Add ALGORITHM_MLKEM_768 and ALGORITHM_MLKEM_1024 to proto enums - Add ML-KEM key types (mlkem:768, mlkem:1024) to ocrypto library - Implement MLKEMKeyPair and MLKEM1024KeyPair with key generation - Update proto validation to accept ML-KEM algorithms - Regenerate all protocol buffers and OpenAPI docs This is a partial implementation. Remaining work includes: - Add ML-KEM encryption/decryption to asym_encryption.go - Add ML-KEM support to SDK and service layers - Add corresponding tests Co-Authored-By: Claude Sonnet 4.5 --- docs/grpc/index.html | 24 + .../authorization/authorization.openapi.yaml | 4 + .../v2/authorization.openapi.yaml | 4 + .../policy/actions/actions.openapi.yaml | 4 + .../policy/attributes/attributes.openapi.yaml | 4 + .../key_access_server_registry.openapi.yaml | 8 +- .../policy/namespaces/namespaces.openapi.yaml | 4 + docs/openapi/policy/objects.openapi.yaml | 4 + .../obligations/obligations.openapi.yaml | 4 + .../registered_resources.openapi.yaml | 4 + .../resource_mapping.openapi.yaml | 4 + .../subject_mapping.openapi.yaml | 4 + .../openapi/policy/unsafe/unsafe.openapi.yaml | 4 + lib/ocrypto/ec_key_pair.go | 108 ++++- .../key_access_server_registry.pb.go | 415 +++++++++--------- protocol/go/policy/objects.pb.go | 122 ++--- .../key_access_server_registry.proto | 4 +- service/policy/objects.proto | 4 + 18 files changed, 462 insertions(+), 267 deletions(-) diff --git a/docs/grpc/index.html b/docs/grpc/index.html index c3c252a786..af3b2301d5 100644 --- a/docs/grpc/index.html +++ b/docs/grpc/index.html @@ -4147,6 +4147,18 @@

Algorithm

+ + ALGORITHM_MLKEM_768 + 20 +

+ + + + ALGORITHM_MLKEM_1024 + 21 +

+ + @@ -4276,6 +4288,18 @@

KasPublicKeyAlgEnum

+ + KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768 + 20 +

+ + + + KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_1024 + 21 +

+ + diff --git a/docs/openapi/authorization/authorization.openapi.yaml b/docs/openapi/authorization/authorization.openapi.yaml index 5a112b61b6..cb0927c76c 100644 --- a/docs/openapi/authorization/authorization.openapi.yaml +++ b/docs/openapi/authorization/authorization.openapi.yaml @@ -143,6 +143,8 @@ components: - ALGORITHM_HPQT_XWING - ALGORITHM_HPQT_SECP256R1_MLKEM768 - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + - ALGORITHM_MLKEM_768 + - ALGORITHM_MLKEM_1024 description: Supported key algorithms. policy.KasPublicKeyAlgEnum: type: string @@ -157,6 +159,8 @@ components: - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_1024 policy.SourceType: type: string title: SourceType diff --git a/docs/openapi/authorization/v2/authorization.openapi.yaml b/docs/openapi/authorization/v2/authorization.openapi.yaml index 0e03a9e182..e6914f49ba 100644 --- a/docs/openapi/authorization/v2/authorization.openapi.yaml +++ b/docs/openapi/authorization/v2/authorization.openapi.yaml @@ -178,6 +178,8 @@ components: - ALGORITHM_HPQT_XWING - ALGORITHM_HPQT_SECP256R1_MLKEM768 - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + - ALGORITHM_MLKEM_768 + - ALGORITHM_MLKEM_1024 description: Supported key algorithms. policy.KasPublicKeyAlgEnum: type: string @@ -192,6 +194,8 @@ components: - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_1024 policy.SourceType: type: string title: SourceType diff --git a/docs/openapi/policy/actions/actions.openapi.yaml b/docs/openapi/policy/actions/actions.openapi.yaml index da57657292..69a29f3504 100644 --- a/docs/openapi/policy/actions/actions.openapi.yaml +++ b/docs/openapi/policy/actions/actions.openapi.yaml @@ -206,6 +206,8 @@ components: - ALGORITHM_HPQT_XWING - ALGORITHM_HPQT_SECP256R1_MLKEM768 - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + - ALGORITHM_MLKEM_768 + - ALGORITHM_MLKEM_1024 description: Supported key algorithms. policy.AttributeRuleTypeEnum: type: string @@ -235,6 +237,8 @@ components: - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_1024 policy.SourceType: type: string title: SourceType diff --git a/docs/openapi/policy/attributes/attributes.openapi.yaml b/docs/openapi/policy/attributes/attributes.openapi.yaml index cbdde67a8c..104c4a4702 100644 --- a/docs/openapi/policy/attributes/attributes.openapi.yaml +++ b/docs/openapi/policy/attributes/attributes.openapi.yaml @@ -725,6 +725,8 @@ components: - ALGORITHM_HPQT_XWING - ALGORITHM_HPQT_SECP256R1_MLKEM768 - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + - ALGORITHM_MLKEM_768 + - ALGORITHM_MLKEM_1024 description: Supported key algorithms. policy.AttributeRuleTypeEnum: type: string @@ -754,6 +756,8 @@ components: - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_1024 policy.SortDirection: type: string title: SortDirection diff --git a/docs/openapi/policy/kasregistry/key_access_server_registry.openapi.yaml b/docs/openapi/policy/kasregistry/key_access_server_registry.openapi.yaml index 2d803524bb..2229b977e4 100644 --- a/docs/openapi/policy/kasregistry/key_access_server_registry.openapi.yaml +++ b/docs/openapi/policy/kasregistry/key_access_server_registry.openapi.yaml @@ -526,6 +526,8 @@ components: - ALGORITHM_HPQT_XWING - ALGORITHM_HPQT_SECP256R1_MLKEM768 - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + - ALGORITHM_MLKEM_768 + - ALGORITHM_MLKEM_1024 description: Supported key algorithms. policy.KasPublicKeyAlgEnum: type: string @@ -540,6 +542,8 @@ components: - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_1024 policy.KeyMode: type: string title: KeyMode @@ -1181,7 +1185,7 @@ components: Required The algorithm to be used for the key The key_algorithm must be one of the defined values.: ``` - this in [1, 2, 3, 4, 5, 6, 7, 8] + this in [1, 2, 3, 4, 5, 6, 7, 8, 20, 21] ``` $ref: '#/components/schemas/policy.Algorithm' @@ -2020,7 +2024,7 @@ components: Required The key_algorithm must be one of the defined values.: ``` - this in [1, 2, 3, 4, 5, 6, 7, 8] + this in [1, 2, 3, 4, 5, 6, 7, 8, 20, 21] ``` $ref: '#/components/schemas/policy.Algorithm' diff --git a/docs/openapi/policy/namespaces/namespaces.openapi.yaml b/docs/openapi/policy/namespaces/namespaces.openapi.yaml index d6f72e3e69..c813ecd93c 100644 --- a/docs/openapi/policy/namespaces/namespaces.openapi.yaml +++ b/docs/openapi/policy/namespaces/namespaces.openapi.yaml @@ -356,6 +356,8 @@ components: - ALGORITHM_HPQT_XWING - ALGORITHM_HPQT_SECP256R1_MLKEM768 - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + - ALGORITHM_MLKEM_768 + - ALGORITHM_MLKEM_1024 description: Supported key algorithms. policy.KasPublicKeyAlgEnum: type: string @@ -370,6 +372,8 @@ components: - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_1024 policy.SortDirection: type: string title: SortDirection diff --git a/docs/openapi/policy/objects.openapi.yaml b/docs/openapi/policy/objects.openapi.yaml index 00630c5163..819df15b55 100644 --- a/docs/openapi/policy/objects.openapi.yaml +++ b/docs/openapi/policy/objects.openapi.yaml @@ -24,6 +24,8 @@ components: - ALGORITHM_HPQT_XWING - ALGORITHM_HPQT_SECP256R1_MLKEM768 - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + - ALGORITHM_MLKEM_768 + - ALGORITHM_MLKEM_1024 description: Supported key algorithms. policy.AttributeRuleTypeEnum: type: string @@ -53,6 +55,8 @@ components: - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_1024 policy.KeyMode: type: string title: KeyMode diff --git a/docs/openapi/policy/obligations/obligations.openapi.yaml b/docs/openapi/policy/obligations/obligations.openapi.yaml index 562045f797..fe4758bf1a 100644 --- a/docs/openapi/policy/obligations/obligations.openapi.yaml +++ b/docs/openapi/policy/obligations/obligations.openapi.yaml @@ -556,6 +556,8 @@ components: - ALGORITHM_HPQT_XWING - ALGORITHM_HPQT_SECP256R1_MLKEM768 - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + - ALGORITHM_MLKEM_768 + - ALGORITHM_MLKEM_1024 description: Supported key algorithms. policy.AttributeRuleTypeEnum: type: string @@ -585,6 +587,8 @@ components: - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_1024 policy.SortDirection: type: string title: SortDirection diff --git a/docs/openapi/policy/registeredresources/registered_resources.openapi.yaml b/docs/openapi/policy/registeredresources/registered_resources.openapi.yaml index ca41d4841e..84ae4a909b 100644 --- a/docs/openapi/policy/registeredresources/registered_resources.openapi.yaml +++ b/docs/openapi/policy/registeredresources/registered_resources.openapi.yaml @@ -416,6 +416,8 @@ components: - ALGORITHM_HPQT_XWING - ALGORITHM_HPQT_SECP256R1_MLKEM768 - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + - ALGORITHM_MLKEM_768 + - ALGORITHM_MLKEM_1024 description: Supported key algorithms. policy.AttributeRuleTypeEnum: type: string @@ -445,6 +447,8 @@ components: - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_1024 policy.SortDirection: type: string title: SortDirection diff --git a/docs/openapi/policy/resourcemapping/resource_mapping.openapi.yaml b/docs/openapi/policy/resourcemapping/resource_mapping.openapi.yaml index a11306bc24..f393b21637 100644 --- a/docs/openapi/policy/resourcemapping/resource_mapping.openapi.yaml +++ b/docs/openapi/policy/resourcemapping/resource_mapping.openapi.yaml @@ -416,6 +416,8 @@ components: - ALGORITHM_HPQT_XWING - ALGORITHM_HPQT_SECP256R1_MLKEM768 - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + - ALGORITHM_MLKEM_768 + - ALGORITHM_MLKEM_1024 description: Supported key algorithms. policy.AttributeRuleTypeEnum: type: string @@ -445,6 +447,8 @@ components: - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_1024 policy.SourceType: type: string title: SourceType diff --git a/docs/openapi/policy/subjectmapping/subject_mapping.openapi.yaml b/docs/openapi/policy/subjectmapping/subject_mapping.openapi.yaml index 4c78ec9f8d..2762d4cffa 100644 --- a/docs/openapi/policy/subjectmapping/subject_mapping.openapi.yaml +++ b/docs/openapi/policy/subjectmapping/subject_mapping.openapi.yaml @@ -452,6 +452,8 @@ components: - ALGORITHM_HPQT_XWING - ALGORITHM_HPQT_SECP256R1_MLKEM768 - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + - ALGORITHM_MLKEM_768 + - ALGORITHM_MLKEM_1024 description: Supported key algorithms. policy.AttributeRuleTypeEnum: type: string @@ -481,6 +483,8 @@ components: - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_1024 policy.SortDirection: type: string title: SortDirection diff --git a/docs/openapi/policy/unsafe/unsafe.openapi.yaml b/docs/openapi/policy/unsafe/unsafe.openapi.yaml index 09ebdf2e85..0fd9822b2f 100644 --- a/docs/openapi/policy/unsafe/unsafe.openapi.yaml +++ b/docs/openapi/policy/unsafe/unsafe.openapi.yaml @@ -390,6 +390,8 @@ components: - ALGORITHM_HPQT_XWING - ALGORITHM_HPQT_SECP256R1_MLKEM768 - ALGORITHM_HPQT_SECP384R1_MLKEM1024 + - ALGORITHM_MLKEM_768 + - ALGORITHM_MLKEM_1024 description: Supported key algorithms. policy.AttributeRuleTypeEnum: type: string @@ -419,6 +421,8 @@ components: - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768 + - KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_1024 policy.KeyMode: type: string title: KeyMode diff --git a/lib/ocrypto/ec_key_pair.go b/lib/ocrypto/ec_key_pair.go index 70e30cc8df..e0b9aefecd 100644 --- a/lib/ocrypto/ec_key_pair.go +++ b/lib/ocrypto/ec_key_pair.go @@ -4,6 +4,7 @@ import ( "crypto/ecdh" "crypto/ecdsa" "crypto/elliptic" + "crypto/mlkem" "crypto/rand" "crypto/sha256" "crypto/x509" @@ -22,11 +23,13 @@ type ECCMode uint8 type KeyType string const ( - RSA2048Key KeyType = "rsa:2048" - RSA4096Key KeyType = "rsa:4096" - EC256Key KeyType = "ec:secp256r1" - EC384Key KeyType = "ec:secp384r1" - EC521Key KeyType = "ec:secp521r1" + RSA2048Key KeyType = "rsa:2048" + RSA4096Key KeyType = "rsa:4096" + EC256Key KeyType = "ec:secp256r1" + EC384Key KeyType = "ec:secp384r1" + EC521Key KeyType = "ec:secp521r1" + MLKEM768Key KeyType = "mlkem:768" + MLKEM1024Key KeyType = "mlkem:1024" ) // ParseKeyType validates a string as a known KeyType, returning an error for @@ -35,6 +38,7 @@ func ParseKeyType(alg string) (KeyType, error) { switch KeyType(alg) { case RSA2048Key, RSA4096Key, EC256Key, EC384Key, EC521Key, + MLKEM768Key, MLKEM1024Key, HybridXWingKey, HybridSecp256r1MLKEM768Key, HybridSecp384r1MLKEM1024Key: return KeyType(alg), nil default: @@ -79,6 +83,10 @@ func NewKeyPair(kt KeyType) (KeyPair, error) { return NewECKeyPair(mode) case HybridSecp256r1MLKEM768Key, HybridSecp384r1MLKEM1024Key, HybridXWingKey: return NewHybridKeyPair(kt) + case MLKEM768Key: + return NewMLKEMKeyPair() + case MLKEM1024Key: + return NewMLKEM1024KeyPair() default: return nil, fmt.Errorf("unsupported key type: %v", kt) } @@ -88,6 +96,14 @@ type ECKeyPair struct { PrivateKey *ecdsa.PrivateKey } +type MLKEMKeyPair struct { + PrivateKey *mlkem.DecapsulationKey768 +} + +type MLKEM1024KeyPair struct { + PrivateKey *mlkem.DecapsulationKey1024 +} + func IsECKeyType(kt KeyType) bool { switch kt { //nolint:exhaustive // only handle ec types case EC256Key, EC384Key, EC521Key: @@ -509,3 +525,85 @@ func GetECKeySize(pemData []byte) (int, error) { func (keyPair ECKeyPair) GetKeyType() KeyType { return EC256Key } + +func NewMLKEMKeyPair() (MLKEMKeyPair, error) { + privateKey, err := mlkem.GenerateKey768() + if err != nil { + return MLKEMKeyPair{}, fmt.Errorf("mlkem.GenerateKey768 failed: %w", err) + } + + return MLKEMKeyPair{PrivateKey: privateKey}, nil +} + +func NewMLKEM1024KeyPair() (MLKEM1024KeyPair, error) { + privateKey, err := mlkem.GenerateKey1024() + if err != nil { + return MLKEM1024KeyPair{}, fmt.Errorf("mlkem.GenerateKey1024 failed: %w", err) + } + + return MLKEM1024KeyPair{PrivateKey: privateKey}, nil +} + +func (keyPair MLKEMKeyPair) PrivateKeyInPemFormat() (string, error) { + if keyPair.PrivateKey == nil { + return "", errors.New("failed to generate PEM formatted private key") + } + + privateKeyPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "MLKEM DECAPSULATION KEY", + Bytes: keyPair.PrivateKey.Bytes(), + }, + ) + return string(privateKeyPEM), nil +} + +func (keyPair MLKEMKeyPair) PublicKeyInPemFormat() (string, error) { + if keyPair.PrivateKey == nil { + return "", errors.New("failed to generate PEM formatted public key") + } + + publicKeyPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "MLKEM ENCAPSULATOR", + Bytes: keyPair.PrivateKey.EncapsulationKey().Bytes(), + }, + ) + return string(publicKeyPEM), nil +} + +func (keyPair MLKEMKeyPair) GetKeyType() KeyType { + return MLKEM768Key +} + +func (keyPair MLKEM1024KeyPair) PrivateKeyInPemFormat() (string, error) { + if keyPair.PrivateKey == nil { + return "", errors.New("failed to generate PEM formatted private key") + } + + privateKeyPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "MLKEM DECAPSULATION KEY", + Bytes: keyPair.PrivateKey.Bytes(), + }, + ) + return string(privateKeyPEM), nil +} + +func (keyPair MLKEM1024KeyPair) PublicKeyInPemFormat() (string, error) { + if keyPair.PrivateKey == nil { + return "", errors.New("failed to generate PEM formatted public key") + } + + publicKeyPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "MLKEM ENCAPSULATOR", + Bytes: keyPair.PrivateKey.EncapsulationKey().Bytes(), + }, + ) + return string(publicKeyPEM), nil +} + +func (keyPair MLKEM1024KeyPair) GetKeyType() KeyType { + return MLKEM1024Key +} diff --git a/protocol/go/policy/kasregistry/key_access_server_registry.pb.go b/protocol/go/policy/kasregistry/key_access_server_registry.pb.go index c3cd4c7dc2..c3ca23ec9e 100644 --- a/protocol/go/policy/kasregistry/key_access_server_registry.pb.go +++ b/protocol/go/policy/kasregistry/key_access_server_registry.pb.go @@ -4297,226 +4297,227 @@ var file_policy_kasregistry_key_access_server_registry_proto_rawDesc = []byte{ 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x3a, 0x02, 0x18, 0x01, 0x22, 0xd5, 0x0c, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, + 0x69, 0x6f, 0x6e, 0x3a, 0x02, 0x18, 0x01, 0x22, 0xdd, 0x0c, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, - 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0xad, 0x01, + 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0xb5, 0x01, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, - 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x42, 0x75, 0xba, 0x48, 0x72, 0xba, 0x01, 0x6f, + 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x42, 0x7d, 0xba, 0x48, 0x7a, 0xba, 0x01, 0x77, 0x0a, 0x15, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x5f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x12, 0x34, 0x54, 0x68, 0x65, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x65, - 0x66, 0x69, 0x6e, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2e, 0x1a, 0x20, 0x74, + 0x66, 0x69, 0x6e, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2e, 0x1a, 0x28, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x5b, 0x31, 0x2c, 0x20, 0x32, 0x2c, 0x20, 0x33, 0x2c, - 0x20, 0x34, 0x2c, 0x20, 0x35, 0x2c, 0x20, 0x36, 0x2c, 0x20, 0x37, 0x2c, 0x20, 0x38, 0x5d, 0x52, - 0x0c, 0x6b, 0x65, 0x79, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x93, 0x01, - 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x0f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x4d, 0x6f, 0x64, - 0x65, 0x42, 0x67, 0xba, 0x48, 0x64, 0xba, 0x01, 0x61, 0x0a, 0x10, 0x6b, 0x65, 0x79, 0x5f, 0x6d, - 0x6f, 0x64, 0x65, 0x5f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x12, 0x35, 0x54, 0x68, 0x65, - 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, - 0x65, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x65, 0x66, - 0x69, 0x6e, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x20, 0x28, 0x31, 0x2d, 0x34, - 0x29, 0x2e, 0x1a, 0x16, 0x74, 0x68, 0x69, 0x73, 0x20, 0x3e, 0x3d, 0x20, 0x31, 0x20, 0x26, 0x26, - 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x3c, 0x3d, 0x20, 0x34, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x4d, - 0x6f, 0x64, 0x65, 0x12, 0x42, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, - 0x79, 0x5f, 0x63, 0x74, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x43, 0x74, - 0x78, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0c, 0x70, 0x75, 0x62, 0x6c, 0x69, - 0x63, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x12, 0x3d, 0x0a, 0x0f, 0x70, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x15, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, - 0x65, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x52, 0x0d, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, - 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x18, 0x08, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x12, 0x33, 0x0a, 0x08, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, - 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x3a, 0xbb, 0x07, 0xba, 0x48, 0xb7, 0x07, 0x1a, 0x97, 0x03, 0x0a, 0x23, 0x70, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x5f, 0x6f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x6c, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, - 0x12, 0xbc, 0x01, 0x54, 0x68, 0x65, 0x20, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, - 0x65, 0x79, 0x20, 0x69, 0x73, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x20, 0x69, - 0x66, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, - 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x52, 0x4f, - 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x20, 0x6f, 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, - 0x44, 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x52, 0x4f, 0x4f, 0x54, - 0x5f, 0x4b, 0x45, 0x59, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, - 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x65, 0x6d, - 0x70, 0x74, 0x79, 0x20, 0x69, 0x66, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, - 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x52, 0x45, 0x4d, 0x4f, - 0x54, 0x45, 0x20, 0x6f, 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, - 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x2e, 0x1a, - 0xb0, 0x01, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, - 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, - 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x29, 0x20, 0x26, 0x26, - 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, - 0x79, 0x5f, 0x63, 0x74, 0x78, 0x2e, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, - 0x79, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x28, 0x28, 0x74, 0x68, - 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x33, - 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, - 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x34, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, - 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x2e, - 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x3d, 0x3d, 0x20, 0x27, - 0x27, 0x29, 0x1a, 0xf4, 0x02, 0x0a, 0x26, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x61, 0x6c, 0x6c, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0xa8, 0x01, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x20, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x20, - 0x69, 0x64, 0x20, 0x69, 0x73, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x20, 0x69, - 0x66, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, - 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, - 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x20, 0x6f, 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, - 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x2e, 0x20, 0x49, 0x74, 0x20, - 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x20, 0x66, 0x6f, - 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, - 0x47, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x4b, - 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, - 0x45, 0x59, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x2e, 0x1a, 0x9e, 0x01, 0x28, 0x28, 0x74, 0x68, 0x69, - 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x20, - 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, - 0x20, 0x3d, 0x3d, 0x20, 0x34, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x70, + 0x20, 0x34, 0x2c, 0x20, 0x35, 0x2c, 0x20, 0x36, 0x2c, 0x20, 0x37, 0x2c, 0x20, 0x38, 0x2c, 0x20, + 0x32, 0x30, 0x2c, 0x20, 0x32, 0x31, 0x5d, 0x52, 0x0c, 0x6b, 0x65, 0x79, 0x41, 0x6c, 0x67, 0x6f, + 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x93, 0x01, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, + 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x42, 0x67, 0xba, 0x48, 0x64, 0xba, 0x01, + 0x61, 0x0a, 0x10, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x5f, 0x64, 0x65, 0x66, 0x69, + 0x6e, 0x65, 0x64, 0x12, 0x35, 0x54, 0x68, 0x65, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, + 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, + 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x73, 0x20, 0x28, 0x31, 0x2d, 0x34, 0x29, 0x2e, 0x1a, 0x16, 0x74, 0x68, 0x69, 0x73, + 0x20, 0x3e, 0x3d, 0x20, 0x31, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x3c, 0x3d, + 0x20, 0x34, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x42, 0x0a, 0x0e, 0x70, + 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x75, 0x62, + 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, + 0x01, 0x52, 0x0c, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x12, + 0x3d, 0x0a, 0x0f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, + 0x74, 0x78, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x52, + 0x0d, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x12, 0x2c, + 0x0a, 0x12, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, + 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6c, 0x65, + 0x67, 0x61, 0x63, 0x79, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, + 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x3a, 0xbb, 0x07, 0xba, 0x48, 0xb7, 0x07, + 0x1a, 0x97, 0x03, 0x0a, 0x23, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, + 0x5f, 0x63, 0x74, 0x78, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x6c, 0x79, 0x5f, + 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0xbc, 0x01, 0x54, 0x68, 0x65, 0x20, 0x77, + 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x69, 0x73, 0x20, 0x72, 0x65, + 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x20, 0x69, 0x66, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, + 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x43, + 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x20, 0x6f, + 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, + 0x44, 0x45, 0x52, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x2e, 0x20, 0x54, 0x68, + 0x65, 0x20, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x6d, 0x75, + 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x20, 0x69, 0x66, 0x20, 0x6b, + 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, + 0x4f, 0x44, 0x45, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x20, 0x6f, 0x72, 0x20, 0x4b, 0x45, + 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, + 0x59, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x2e, 0x1a, 0xb0, 0x01, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, + 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x20, 0x7c, + 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, + 0x3d, 0x3d, 0x20, 0x32, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x70, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x2e, 0x77, 0x72, + 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, 0x29, + 0x20, 0x7c, 0x7c, 0x20, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, + 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x33, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, + 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x34, 0x29, 0x20, + 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, + 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x2e, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, + 0x6b, 0x65, 0x79, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x1a, 0xf4, 0x02, 0x0a, 0x26, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, - 0x64, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x28, 0x28, 0x74, 0x68, - 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, - 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, - 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x33, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, - 0x69, 0x64, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x1a, 0xa3, 0x01, 0x0a, 0x23, 0x70, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x5f, 0x66, 0x6f, - 0x72, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x6f, 0x6e, 0x6c, - 0x79, 0x12, 0x48, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, - 0x74, 0x78, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x73, - 0x65, 0x74, 0x20, 0x69, 0x66, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, - 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, - 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x2e, 0x1a, 0x32, 0x21, 0x28, 0x74, - 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, - 0x34, 0x20, 0x26, 0x26, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x70, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x29, 0x29, 0x22, - 0x3c, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, - 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x22, 0x7a, 0x0a, - 0x0d, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, - 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, - 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4b, 0x61, 0x73, - 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x48, 0x00, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x42, 0x13, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, - 0x65, 0x72, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x22, 0x39, 0x0a, 0x0e, 0x47, 0x65, 0x74, - 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6b, - 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6b, 0x61, - 0x73, 0x4b, 0x65, 0x79, 0x22, 0x86, 0x04, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0xb0, 0x01, 0x0a, 0x0d, 0x6b, 0x65, 0x79, - 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, - 0x74, 0x68, 0x6d, 0x42, 0x78, 0xba, 0x48, 0x75, 0xba, 0x01, 0x72, 0x0a, 0x15, 0x6b, 0x65, 0x79, - 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x5f, 0x64, 0x65, 0x66, 0x69, 0x6e, - 0x65, 0x64, 0x12, 0x34, 0x54, 0x68, 0x65, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, - 0x72, 0x69, 0x74, 0x68, 0x6d, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x6f, 0x6e, - 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, - 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2e, 0x1a, 0x23, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, - 0x6e, 0x20, 0x5b, 0x30, 0x2c, 0x20, 0x31, 0x2c, 0x20, 0x32, 0x2c, 0x20, 0x33, 0x2c, 0x20, 0x34, - 0x2c, 0x20, 0x35, 0x2c, 0x20, 0x36, 0x2c, 0x20, 0x37, 0x2c, 0x20, 0x38, 0x5d, 0x52, 0x0c, 0x6b, - 0x65, 0x79, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x21, 0x0a, 0x06, 0x6b, - 0x61, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, - 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, 0x12, 0x24, - 0x0a, 0x08, 0x6b, 0x61, 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x48, 0x00, 0x52, 0x07, 0x6b, 0x61, 0x73, - 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x75, 0x72, 0x69, 0x18, + 0x64, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x6c, 0x79, 0x5f, 0x72, 0x65, 0x71, + 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0xa8, 0x01, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x20, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x20, 0x69, 0x64, 0x20, 0x69, 0x73, 0x20, 0x72, 0x65, + 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x20, 0x69, 0x66, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, + 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, + 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, + 0x20, 0x6f, 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x52, 0x45, 0x4d, + 0x4f, 0x54, 0x45, 0x2e, 0x20, 0x49, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, + 0x65, 0x6d, 0x70, 0x74, 0x79, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, + 0x44, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, + 0x45, 0x59, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, + 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x2e, + 0x1a, 0x9e, 0x01, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, + 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, + 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x34, 0x29, 0x20, 0x26, + 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x29, + 0x20, 0x7c, 0x7c, 0x20, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, + 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, + 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x33, 0x29, 0x20, + 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, + 0x29, 0x1a, 0xa3, 0x01, 0x0a, 0x23, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, + 0x79, 0x5f, 0x63, 0x74, 0x78, 0x5f, 0x66, 0x6f, 0x72, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x12, 0x48, 0x70, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, + 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x74, 0x20, 0x69, 0x66, 0x20, 0x6b, 0x65, + 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, + 0x44, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x4f, 0x4e, + 0x4c, 0x59, 0x2e, 0x1a, 0x32, 0x21, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6b, 0x65, 0x79, 0x5f, + 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x34, 0x20, 0x26, 0x26, 0x20, 0x68, 0x61, 0x73, + 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, + 0x79, 0x5f, 0x63, 0x74, 0x78, 0x29, 0x29, 0x22, 0x3c, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x07, + 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6b, + 0x61, 0x73, 0x4b, 0x65, 0x79, 0x22, 0x7a, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, + 0x69, 0x64, 0x12, 0x38, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x66, 0x69, 0x65, 0x72, 0x48, 0x00, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x42, 0x13, 0x0a, 0x0a, + 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, + 0x01, 0x22, 0x39, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, + 0x73, 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x22, 0x86, 0x04, 0x0a, + 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0xb0, 0x01, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, + 0x68, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x42, 0x78, 0xba, 0x48, 0x75, + 0xba, 0x01, 0x72, 0x0a, 0x15, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, + 0x68, 0x6d, 0x5f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x12, 0x34, 0x54, 0x68, 0x65, 0x20, + 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x20, 0x6d, 0x75, + 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, + 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2e, + 0x1a, 0x23, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x5b, 0x30, 0x2c, 0x20, 0x31, 0x2c, + 0x20, 0x32, 0x2c, 0x20, 0x33, 0x2c, 0x20, 0x34, 0x2c, 0x20, 0x35, 0x2c, 0x20, 0x36, 0x2c, 0x20, + 0x37, 0x2c, 0x20, 0x38, 0x5d, 0x52, 0x0c, 0x6b, 0x65, 0x79, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, + 0x74, 0x68, 0x6d, 0x12, 0x21, 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, + 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x08, 0x6b, 0x61, 0x73, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, + 0x01, 0x48, 0x00, 0x52, 0x07, 0x6b, 0x61, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x07, + 0x6b, 0x61, 0x73, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, + 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x48, 0x00, 0x52, 0x06, 0x6b, 0x61, 0x73, + 0x55, 0x72, 0x69, 0x12, 0x1b, 0x0a, 0x06, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x08, 0x48, 0x01, 0x52, 0x06, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x88, 0x01, 0x01, + 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, + 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x0b, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, + 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x73, + 0x53, 0x6f, 0x72, 0x74, 0x42, 0x08, 0xba, 0x48, 0x05, 0x92, 0x01, 0x02, 0x10, 0x01, 0x52, 0x04, + 0x73, 0x6f, 0x72, 0x74, 0x12, 0x26, 0x0a, 0x06, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x18, 0x0c, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x52, 0x06, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x42, 0x0c, 0x0a, 0x0a, + 0x6b, 0x61, 0x73, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x6c, + 0x65, 0x67, 0x61, 0x63, 0x79, 0x22, 0x73, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, 0x08, 0x6b, 0x61, 0x73, + 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x07, 0x6b, 0x61, 0x73, + 0x4b, 0x65, 0x79, 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, + 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x86, 0x03, 0x0a, 0x10, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, + 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, + 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x54, + 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, 0x6d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, 0x68, 0x61, + 0x76, 0x69, 0x6f, 0x72, 0x3a, 0xcc, 0x01, 0xba, 0x48, 0xc8, 0x01, 0x1a, 0xc5, 0x01, 0x0a, 0x18, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, + 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x12, 0x52, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x20, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x20, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, + 0x6f, 0x72, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x65, 0x69, 0x74, 0x68, 0x65, + 0x72, 0x20, 0x41, 0x50, 0x50, 0x45, 0x4e, 0x44, 0x20, 0x6f, 0x72, 0x20, 0x52, 0x45, 0x50, 0x4c, + 0x41, 0x43, 0x45, 0x2c, 0x20, 0x77, 0x68, 0x65, 0x6e, 0x20, 0x75, 0x70, 0x64, 0x61, 0x74, 0x69, + 0x6e, 0x67, 0x20, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x1a, 0x55, 0x28, 0x28, + 0x21, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x29, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x28, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, + 0x73, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, + 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x20, 0x21, 0x3d, 0x20, + 0x30, 0x29, 0x29, 0x22, 0x3c, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x4b, 0x65, + 0x79, 0x22, 0xa4, 0x01, 0x0a, 0x10, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, + 0x48, 0x00, 0x52, 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, + 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, - 0x01, 0x48, 0x00, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x55, 0x72, 0x69, 0x12, 0x1b, 0x0a, 0x06, 0x6c, - 0x65, 0x67, 0x61, 0x63, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x48, 0x01, 0x52, 0x06, 0x6c, - 0x65, 0x67, 0x61, 0x63, 0x79, 0x88, 0x01, 0x01, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, - 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, - 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, - 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x73, 0x53, 0x6f, 0x72, 0x74, 0x42, 0x08, 0xba, 0x48, - 0x05, 0x92, 0x01, 0x02, 0x10, 0x01, 0x52, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x12, 0x26, 0x0a, 0x06, - 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x06, 0x73, 0x65, - 0x61, 0x72, 0x63, 0x68, 0x42, 0x0c, 0x0a, 0x0a, 0x6b, 0x61, 0x73, 0x5f, 0x66, 0x69, 0x6c, 0x74, - 0x65, 0x72, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x22, 0x73, 0x0a, - 0x10, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x29, 0x0a, 0x08, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, - 0x4b, 0x65, 0x79, 0x52, 0x07, 0x6b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x34, 0x0a, 0x0a, - 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x22, 0x86, 0x03, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, - 0x64, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x54, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, - 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, - 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x3a, 0xcc, 0x01, 0xba, - 0x48, 0xc8, 0x01, 0x1a, 0xc5, 0x01, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, - 0x12, 0x52, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x20, 0x75, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x20, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, - 0x62, 0x65, 0x20, 0x65, 0x69, 0x74, 0x68, 0x65, 0x72, 0x20, 0x41, 0x50, 0x50, 0x45, 0x4e, 0x44, - 0x20, 0x6f, 0x72, 0x20, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x2c, 0x20, 0x77, 0x68, 0x65, - 0x6e, 0x20, 0x75, 0x70, 0x64, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x2e, 0x1a, 0x55, 0x28, 0x28, 0x21, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, - 0x73, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x29, 0x29, 0x20, 0x7c, 0x7c, 0x20, - 0x28, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, - 0x76, 0x69, 0x6f, 0x72, 0x20, 0x21, 0x3d, 0x20, 0x30, 0x29, 0x29, 0x22, 0x3c, 0x0a, 0x11, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x27, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, - 0x79, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x22, 0xa4, 0x01, 0x0a, 0x10, 0x4b, 0x61, - 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x21, - 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, - 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x05, 0x6b, 0x61, 0x73, 0x49, - 0x64, 0x12, 0x1d, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, - 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x12, 0x1e, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, - 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x48, 0x00, 0x52, 0x03, 0x75, 0x72, 0x69, - 0x12, 0x19, 0x0a, 0x03, 0x6b, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, - 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x03, 0x6b, 0x69, 0x64, 0x42, 0x13, 0x0a, 0x0a, 0x69, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, - 0x22, 0xee, 0x0e, 0x0a, 0x10, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, - 0x64, 0x12, 0x38, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x66, 0x69, 0x65, 0x72, 0x48, 0x00, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x44, 0x0a, 0x07, 0x6e, - 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, - 0x79, 0x2e, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x2e, 0x4e, 0x65, 0x77, 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6e, 0x65, 0x77, 0x4b, 0x65, - 0x79, 0x1a, 0xd8, 0x04, 0x0a, 0x06, 0x4e, 0x65, 0x77, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x06, - 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, - 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0xa6, 0x01, 0x0a, - 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, - 0x74, 0x68, 0x6d, 0x42, 0x75, 0xba, 0x48, 0x72, 0xba, 0x01, 0x6f, 0x0a, 0x15, 0x6b, 0x65, 0x79, - 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x5f, 0x64, 0x65, 0x66, 0x69, 0x6e, - 0x65, 0x64, 0x12, 0x34, 0x54, 0x68, 0x65, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, - 0x72, 0x69, 0x74, 0x68, 0x6d, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x6f, 0x6e, - 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, - 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2e, 0x1a, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, - 0x6e, 0x20, 0x5b, 0x31, 0x2c, 0x20, 0x32, 0x2c, 0x20, 0x33, 0x2c, 0x20, 0x34, 0x2c, 0x20, 0x35, - 0x2c, 0x20, 0x36, 0x2c, 0x20, 0x37, 0x2c, 0x20, 0x38, 0x5d, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, + 0x01, 0x48, 0x00, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x19, 0x0a, 0x03, 0x6b, 0x69, 0x64, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x03, + 0x6b, 0x69, 0x64, 0x42, 0x13, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, + 0x72, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x22, 0xf6, 0x0e, 0x0a, 0x10, 0x52, 0x6f, 0x74, + 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, + 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, + 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x48, 0x00, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x44, 0x0a, 0x07, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, + 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, + 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4e, 0x65, 0x77, 0x4b, 0x65, + 0x79, 0x52, 0x06, 0x6e, 0x65, 0x77, 0x4b, 0x65, 0x79, 0x1a, 0xe0, 0x04, 0x0a, 0x06, 0x4e, 0x65, + 0x77, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x05, 0x6b, + 0x65, 0x79, 0x49, 0x64, 0x12, 0xae, 0x01, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, + 0x68, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x42, 0x7d, 0xba, 0x48, 0x7a, + 0xba, 0x01, 0x77, 0x0a, 0x15, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, + 0x68, 0x6d, 0x5f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x12, 0x34, 0x54, 0x68, 0x65, 0x20, + 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x20, 0x6d, 0x75, + 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, + 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2e, + 0x1a, 0x28, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x5b, 0x31, 0x2c, 0x20, 0x32, 0x2c, + 0x20, 0x33, 0x2c, 0x20, 0x34, 0x2c, 0x20, 0x35, 0x2c, 0x20, 0x36, 0x2c, 0x20, 0x37, 0x2c, 0x20, + 0x38, 0x2c, 0x20, 0x32, 0x30, 0x2c, 0x20, 0x32, 0x31, 0x5d, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x9e, 0x01, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x42, 0x72, 0xba, 0x48, 0x6f, 0xba, 0x01, diff --git a/protocol/go/policy/objects.pb.go b/protocol/go/policy/objects.pb.go index 1e386a7531..92175c8323 100644 --- a/protocol/go/policy/objects.pb.go +++ b/protocol/go/policy/objects.pb.go @@ -246,6 +246,8 @@ const ( KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING KasPublicKeyAlgEnum = 10 KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 KasPublicKeyAlgEnum = 11 KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 KasPublicKeyAlgEnum = 12 + KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768 KasPublicKeyAlgEnum = 20 + KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_1024 KasPublicKeyAlgEnum = 21 ) // Enum value maps for KasPublicKeyAlgEnum. @@ -260,6 +262,8 @@ var ( 10: "KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING", 11: "KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768", 12: "KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024", + 20: "KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768", + 21: "KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_1024", } KasPublicKeyAlgEnum_value = map[string]int32{ "KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED": 0, @@ -271,6 +275,8 @@ var ( "KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING": 10, "KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768": 11, "KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024": 12, + "KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768": 20, + "KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_1024": 21, } ) @@ -314,20 +320,24 @@ const ( Algorithm_ALGORITHM_HPQT_XWING Algorithm = 6 Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768 Algorithm = 7 Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024 Algorithm = 8 + Algorithm_ALGORITHM_MLKEM_768 Algorithm = 20 + Algorithm_ALGORITHM_MLKEM_1024 Algorithm = 21 ) // Enum value maps for Algorithm. var ( Algorithm_name = map[int32]string{ - 0: "ALGORITHM_UNSPECIFIED", - 1: "ALGORITHM_RSA_2048", - 2: "ALGORITHM_RSA_4096", - 3: "ALGORITHM_EC_P256", - 4: "ALGORITHM_EC_P384", - 5: "ALGORITHM_EC_P521", - 6: "ALGORITHM_HPQT_XWING", - 7: "ALGORITHM_HPQT_SECP256R1_MLKEM768", - 8: "ALGORITHM_HPQT_SECP384R1_MLKEM1024", + 0: "ALGORITHM_UNSPECIFIED", + 1: "ALGORITHM_RSA_2048", + 2: "ALGORITHM_RSA_4096", + 3: "ALGORITHM_EC_P256", + 4: "ALGORITHM_EC_P384", + 5: "ALGORITHM_EC_P521", + 6: "ALGORITHM_HPQT_XWING", + 7: "ALGORITHM_HPQT_SECP256R1_MLKEM768", + 8: "ALGORITHM_HPQT_SECP384R1_MLKEM1024", + 20: "ALGORITHM_MLKEM_768", + 21: "ALGORITHM_MLKEM_1024", } Algorithm_value = map[string]int32{ "ALGORITHM_UNSPECIFIED": 0, @@ -339,6 +349,8 @@ var ( "ALGORITHM_HPQT_XWING": 6, "ALGORITHM_HPQT_SECP256R1_MLKEM768": 7, "ALGORITHM_HPQT_SECP384R1_MLKEM1024": 8, + "ALGORITHM_MLKEM_768": 20, + "ALGORITHM_MLKEM_1024": 21, } ) @@ -3760,7 +3772,7 @@ var file_policy_objects_proto_rawDesc = []byte{ 0x10, 0x00, 0x12, 0x18, 0x0a, 0x14, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x45, 0x58, 0x54, 0x45, - 0x52, 0x4e, 0x41, 0x4c, 0x10, 0x02, 0x2a, 0x9b, 0x03, 0x0a, 0x13, 0x4b, 0x61, 0x73, 0x50, 0x75, + 0x52, 0x4e, 0x41, 0x4c, 0x10, 0x02, 0x2a, 0xea, 0x03, 0x0a, 0x13, 0x4b, 0x61, 0x73, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x41, 0x6c, 0x67, 0x45, 0x6e, 0x75, 0x6d, 0x12, 0x27, 0x0a, 0x23, 0x4b, 0x41, 0x53, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x4c, 0x47, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, @@ -3786,47 +3798,55 @@ var file_policy_objects_proto_rawDesc = []byte{ 0x0a, 0x30, 0x4b, 0x41, 0x53, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x4c, 0x47, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x48, 0x50, 0x51, 0x54, 0x5f, 0x53, 0x45, 0x43, 0x50, 0x33, 0x38, 0x34, 0x52, 0x31, 0x5f, 0x4d, 0x4c, 0x4b, 0x45, 0x4d, 0x31, 0x30, - 0x32, 0x34, 0x10, 0x0c, 0x2a, 0x84, 0x02, 0x0a, 0x09, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, - 0x68, 0x6d, 0x12, 0x19, 0x0a, 0x15, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, - 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x16, 0x0a, - 0x12, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, 0x52, 0x53, 0x41, 0x5f, 0x32, - 0x30, 0x34, 0x38, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, - 0x48, 0x4d, 0x5f, 0x52, 0x53, 0x41, 0x5f, 0x34, 0x30, 0x39, 0x36, 0x10, 0x02, 0x12, 0x15, 0x0a, - 0x11, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, 0x45, 0x43, 0x5f, 0x50, 0x32, - 0x35, 0x36, 0x10, 0x03, 0x12, 0x15, 0x0a, 0x11, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, - 0x4d, 0x5f, 0x45, 0x43, 0x5f, 0x50, 0x33, 0x38, 0x34, 0x10, 0x04, 0x12, 0x15, 0x0a, 0x11, 0x41, - 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, 0x45, 0x43, 0x5f, 0x50, 0x35, 0x32, 0x31, - 0x10, 0x05, 0x12, 0x18, 0x0a, 0x14, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, - 0x48, 0x50, 0x51, 0x54, 0x5f, 0x58, 0x57, 0x49, 0x4e, 0x47, 0x10, 0x06, 0x12, 0x25, 0x0a, 0x21, - 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, 0x48, 0x50, 0x51, 0x54, 0x5f, 0x53, - 0x45, 0x43, 0x50, 0x32, 0x35, 0x36, 0x52, 0x31, 0x5f, 0x4d, 0x4c, 0x4b, 0x45, 0x4d, 0x37, 0x36, - 0x38, 0x10, 0x07, 0x12, 0x26, 0x0a, 0x22, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, - 0x5f, 0x48, 0x50, 0x51, 0x54, 0x5f, 0x53, 0x45, 0x43, 0x50, 0x33, 0x38, 0x34, 0x52, 0x31, 0x5f, - 0x4d, 0x4c, 0x4b, 0x45, 0x4d, 0x31, 0x30, 0x32, 0x34, 0x10, 0x08, 0x2a, 0x56, 0x0a, 0x09, 0x4b, - 0x65, 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1a, 0x0a, 0x16, 0x4b, 0x45, 0x59, 0x5f, - 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, - 0x45, 0x44, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, 0x4b, 0x45, 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, - 0x55, 0x53, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x4b, - 0x45, 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x52, 0x4f, 0x54, 0x41, 0x54, 0x45, - 0x44, 0x10, 0x02, 0x2a, 0x94, 0x01, 0x0a, 0x07, 0x4b, 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x12, - 0x18, 0x0a, 0x14, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, - 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x4b, 0x45, 0x59, - 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x52, 0x4f, 0x4f, - 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x01, 0x12, 0x1e, 0x0a, 0x1a, 0x4b, 0x45, 0x59, 0x5f, 0x4d, - 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x52, 0x4f, 0x4f, - 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x4b, 0x45, 0x59, 0x5f, 0x4d, - 0x4f, 0x44, 0x45, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x10, 0x03, 0x12, 0x1c, 0x0a, 0x18, - 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, - 0x4b, 0x45, 0x59, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x10, 0x04, 0x42, 0x82, 0x01, 0x0a, 0x0a, 0x63, - 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x42, 0x0c, 0x4f, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, - 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, - 0x67, 0x6f, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0xa2, 0x02, 0x03, 0x50, 0x58, 0x58, 0xaa, - 0x02, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0xca, 0x02, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0xe2, 0x02, 0x12, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x32, 0x34, 0x10, 0x0c, 0x12, 0x25, 0x0a, 0x21, 0x4b, 0x41, 0x53, 0x5f, 0x50, 0x55, 0x42, 0x4c, + 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x4c, 0x47, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, + 0x4d, 0x4c, 0x4b, 0x45, 0x4d, 0x5f, 0x37, 0x36, 0x38, 0x10, 0x14, 0x12, 0x26, 0x0a, 0x22, 0x4b, + 0x41, 0x53, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x4c, + 0x47, 0x5f, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x4d, 0x4c, 0x4b, 0x45, 0x4d, 0x5f, 0x31, 0x30, 0x32, + 0x34, 0x10, 0x15, 0x2a, 0xb7, 0x02, 0x0a, 0x09, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, + 0x6d, 0x12, 0x19, 0x0a, 0x15, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, + 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, 0x52, 0x53, 0x41, 0x5f, 0x32, 0x30, + 0x34, 0x38, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, + 0x4d, 0x5f, 0x52, 0x53, 0x41, 0x5f, 0x34, 0x30, 0x39, 0x36, 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, + 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, 0x45, 0x43, 0x5f, 0x50, 0x32, 0x35, + 0x36, 0x10, 0x03, 0x12, 0x15, 0x0a, 0x11, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, + 0x5f, 0x45, 0x43, 0x5f, 0x50, 0x33, 0x38, 0x34, 0x10, 0x04, 0x12, 0x15, 0x0a, 0x11, 0x41, 0x4c, + 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, 0x45, 0x43, 0x5f, 0x50, 0x35, 0x32, 0x31, 0x10, + 0x05, 0x12, 0x18, 0x0a, 0x14, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, 0x48, + 0x50, 0x51, 0x54, 0x5f, 0x58, 0x57, 0x49, 0x4e, 0x47, 0x10, 0x06, 0x12, 0x25, 0x0a, 0x21, 0x41, + 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, 0x48, 0x50, 0x51, 0x54, 0x5f, 0x53, 0x45, + 0x43, 0x50, 0x32, 0x35, 0x36, 0x52, 0x31, 0x5f, 0x4d, 0x4c, 0x4b, 0x45, 0x4d, 0x37, 0x36, 0x38, + 0x10, 0x07, 0x12, 0x26, 0x0a, 0x22, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, + 0x48, 0x50, 0x51, 0x54, 0x5f, 0x53, 0x45, 0x43, 0x50, 0x33, 0x38, 0x34, 0x52, 0x31, 0x5f, 0x4d, + 0x4c, 0x4b, 0x45, 0x4d, 0x31, 0x30, 0x32, 0x34, 0x10, 0x08, 0x12, 0x17, 0x0a, 0x13, 0x41, 0x4c, + 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, 0x5f, 0x4d, 0x4c, 0x4b, 0x45, 0x4d, 0x5f, 0x37, 0x36, + 0x38, 0x10, 0x14, 0x12, 0x18, 0x0a, 0x14, 0x41, 0x4c, 0x47, 0x4f, 0x52, 0x49, 0x54, 0x48, 0x4d, + 0x5f, 0x4d, 0x4c, 0x4b, 0x45, 0x4d, 0x5f, 0x31, 0x30, 0x32, 0x34, 0x10, 0x15, 0x2a, 0x56, 0x0a, + 0x09, 0x4b, 0x65, 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1a, 0x0a, 0x16, 0x4b, 0x45, + 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, + 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, 0x4b, 0x45, 0x59, 0x5f, 0x53, 0x54, + 0x41, 0x54, 0x55, 0x53, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, 0x01, 0x12, 0x16, 0x0a, + 0x12, 0x4b, 0x45, 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x52, 0x4f, 0x54, 0x41, + 0x54, 0x45, 0x44, 0x10, 0x02, 0x2a, 0x94, 0x01, 0x0a, 0x07, 0x4b, 0x65, 0x79, 0x4d, 0x6f, 0x64, + 0x65, 0x12, 0x18, 0x0a, 0x14, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x55, 0x4e, + 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x4b, + 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x52, + 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x01, 0x12, 0x1e, 0x0a, 0x1a, 0x4b, 0x45, 0x59, + 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x52, + 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x4b, 0x45, 0x59, + 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x10, 0x03, 0x12, 0x1c, + 0x0a, 0x18, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, + 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x10, 0x04, 0x42, 0x82, 0x01, 0x0a, + 0x0a, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x42, 0x0c, 0x4f, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2e, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, + 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0xa2, 0x02, 0x03, 0x50, 0x58, + 0x58, 0xaa, 0x02, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0xca, 0x02, 0x06, 0x50, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0xe2, 0x02, 0x12, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x47, 0x50, 0x42, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/service/policy/kasregistry/key_access_server_registry.proto b/service/policy/kasregistry/key_access_server_registry.proto index 86e9fbc9a6..d2f4679980 100644 --- a/service/policy/kasregistry/key_access_server_registry.proto +++ b/service/policy/kasregistry/key_access_server_registry.proto @@ -436,7 +436,7 @@ message CreateKeyRequest { Algorithm key_algorithm = 3 [(buf.validate.field).cel = { id: "key_algorithm_defined" message: "The key_algorithm must be one of the defined values." - expression: "this in [1, 2, 3, 4, 5, 6, 7, 8]" // Allow ALGORITHM_RSA_2048, ALGORITHM_RSA_4096, ALGORITHM_EC_P256, ALGORITHM_EC_P384, ALGORITHM_EC_P521, ALGORITHM_HPQT_XWING, ALGORITHM_HPQT_SECP256R1_MLKEM768, ALGORITHM_HPQT_SECP384R1_MLKEM1024 + expression: "this in [1, 2, 3, 4, 5, 6, 7, 8, 20, 21]" // Allow ALGORITHM_RSA_2048, ALGORITHM_RSA_4096, ALGORITHM_EC_P256, ALGORITHM_EC_P384, ALGORITHM_EC_P521, ALGORITHM_HPQT_XWING, ALGORITHM_HPQT_SECP256R1_MLKEM768, ALGORITHM_HPQT_SECP384R1_MLKEM1024, ALGORITHM_MLKEM_768, ALGORITHM_MLKEM_1024 }]; // The algorithm to be used for the key // Required KeyMode key_mode = 4 [(buf.validate.field).cel = { @@ -593,7 +593,7 @@ message RotateKeyRequest { Algorithm algorithm = 2 [(buf.validate.field).cel = { id: "key_algorithm_defined" message: "The key_algorithm must be one of the defined values." - expression: "this in [1, 2, 3, 4, 5, 6, 7, 8]" // Allow ALGORITHM_RSA_2048, ALGORITHM_RSA_4096, ALGORITHM_EC_P256, ALGORITHM_EC_P384, ALGORITHM_EC_P521, ALGORITHM_HPQT_XWING, ALGORITHM_HPQT_SECP256R1_MLKEM768, ALGORITHM_HPQT_SECP384R1_MLKEM1024 + expression: "this in [1, 2, 3, 4, 5, 6, 7, 8, 20, 21]" // Allow ALGORITHM_RSA_2048, ALGORITHM_RSA_4096, ALGORITHM_EC_P256, ALGORITHM_EC_P384, ALGORITHM_EC_P521, ALGORITHM_HPQT_XWING, ALGORITHM_HPQT_SECP256R1_MLKEM768, ALGORITHM_HPQT_SECP384R1_MLKEM1024, ALGORITHM_MLKEM_768, ALGORITHM_MLKEM_1024 }]; // Required KeyMode key_mode = 3 [ diff --git a/service/policy/objects.proto b/service/policy/objects.proto index 2bf66902ae..3ad41ad7db 100644 --- a/service/policy/objects.proto +++ b/service/policy/objects.proto @@ -399,6 +399,8 @@ enum KasPublicKeyAlgEnum { KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING = 10; KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 = 11; KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 = 12; + KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768 = 20; + KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_1024 = 21; } // Deprecated @@ -572,6 +574,8 @@ enum Algorithm { ALGORITHM_HPQT_XWING = 6; ALGORITHM_HPQT_SECP256R1_MLKEM768 = 7; ALGORITHM_HPQT_SECP384R1_MLKEM1024 = 8; + ALGORITHM_MLKEM_768 = 20; + ALGORITHM_MLKEM_1024 = 21; } // The status of the key From 7b997b21856eee82dca2b8abacfa02321c395f3d Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Wed, 27 May 2026 15:04:12 -0400 Subject: [PATCH 02/25] feat(crypto): add ML-KEM encryption and decryption Implement complete ML-KEM 768/1024 encryption and decryption support in the ocrypto library. Encryption changes (asym_encryption.go): - Add MLKEM SchemeType constant - Add MLKEMEncryptor768 and MLKEMEncryptor1024 types - Implement newMLKEM768/newMLKEM1024 constructors - Handle mlkem.EncapsulationKey in FromPublicPEMWithSalt - Implement all PublicKeyEncryptor interface methods - Use AES-GCM with shared secret from KEM encapsulation Decryption changes (asym_decryption.go): - Add DecryptWithEphemeralKey method to interface - Add MLKEMDecryptor768 and MLKEMDecryptor1024 types - Handle "MLKEM DECAPSULATION KEY" PEM blocks - Implement Decrypt and DecryptWithEphemeralKey methods - Use AES-GCM with shared secret from KEM decapsulation Also updated existing decryptors: - Add DecryptWithEphemeralKey to AsymDecryption (RSA) - Add DecryptWithEphemeralKey to ECDecryptor (ECDH) - Add DecryptWithEphemeralKey to XWingDecryptor (hybrid) - Add DecryptWithEphemeralKey to HybridNISTDecryptor (hybrid) All lib/ocrypto tests pass. Co-Authored-By: Claude Sonnet 4.5 --- lib/ocrypto/asym_decryption.go | 113 ++++++++++++++++++++++++++++++++ lib/ocrypto/asym_encryption.go | 114 +++++++++++++++++++++++++++++++++ lib/ocrypto/hybrid_nist.go | 8 +++ lib/ocrypto/xwing.go | 8 +++ 4 files changed, 243 insertions(+) diff --git a/lib/ocrypto/asym_decryption.go b/lib/ocrypto/asym_decryption.go index 1cbfbfc943..e241ed391c 100644 --- a/lib/ocrypto/asym_decryption.go +++ b/lib/ocrypto/asym_decryption.go @@ -7,6 +7,7 @@ import ( "crypto/ecdh" "crypto/ecdsa" "crypto/elliptic" + "crypto/mlkem" "crypto/rsa" "crypto/sha256" "crypto/x509" @@ -26,6 +27,17 @@ type AsymDecryption struct { type PrivateKeyDecryptor interface { // Decrypt decrypts ciphertext with private key. Decrypt(data []byte) ([]byte, error) + + // DecryptWithEphemeralKey decrypts ciphertext using additional sender material. + DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) +} + +type MLKEMDecryptor768 struct { + decap *mlkem.DecapsulationKey768 +} + +type MLKEMDecryptor1024 struct { + decap *mlkem.DecapsulationKey1024 } // FromPrivatePEM creates and returns a new AsymDecryption. @@ -52,6 +64,18 @@ func FromPrivatePEMWithSalt(privateKeyInPem string, salt, info []byte) (PrivateK return NewSaltedP384MLKEM1024Decryptor(block.Bytes, salt, info) } + if block.Type == "MLKEM DECAPSULATION KEY" { + decap768, err := mlkem.NewDecapsulationKey768(block.Bytes) + if err == nil { + return &MLKEMDecryptor768{decap: decap768}, nil + } + decap1024, err1024 := mlkem.NewDecapsulationKey1024(block.Bytes) + if err1024 != nil { + return nil, fmt.Errorf("mlkem.NewDecapsulationKey1024 failed after mlkem.NewDecapsulationKey768 failed: %w / %w", err, err1024) + } + return &MLKEMDecryptor1024{decap: decap1024}, nil + } + priv, err := x509.ParsePKCS8PrivateKey(block.Bytes) switch { case err == nil: @@ -117,6 +141,13 @@ func (asymDecryption AsymDecryption) Decrypt(data []byte) ([]byte, error) { return bytes, nil } +func (asymDecryption AsymDecryption) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) { + if len(ephemeral) > 0 { + return nil, errors.New("ephemeral key is not supported for RSA decryption") + } + return asymDecryption.Decrypt(data) +} + type ECDecryptor struct { sk *ecdh.PrivateKey salt []byte @@ -214,3 +245,85 @@ func convCurve(c ecdh.Curve) elliptic.Curve { return nil } } + +func (d MLKEMDecryptor768) Decrypt(_ []byte) ([]byte, error) { + return nil, errors.New("ciphertext encapsulation is required for ML-KEM decryption") +} + +func (d MLKEMDecryptor1024) Decrypt(_ []byte) ([]byte, error) { + return nil, errors.New("ciphertext encapsulation is required for ML-KEM decryption") +} + +func (d MLKEMDecryptor768) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) { + if d.decap == nil { + return nil, errors.New("mlkem decapsulation key is nil") + } + if len(ephemeral) == 0 { + return nil, errors.New("ciphertext encapsulation is required for ML-KEM decryption") + } + + sharedSecret, err := d.decap.Decapsulate(ephemeral) + if err != nil { + return nil, fmt.Errorf("mlkem.Decapsulate failed: %w", err) + } + + block, err := aes.NewCipher(sharedSecret) + if err != nil { + return nil, fmt.Errorf("aes.NewCipher failure: %w", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("cipher.NewGCM failure: %w", err) + } + + nonceSize := gcm.NonceSize() + if len(data) < nonceSize { + return nil, errors.New("ciphertext too short") + } + + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, fmt.Errorf("gcm.Open failure: %w", err) + } + + return plaintext, nil +} + +func (d MLKEMDecryptor1024) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) { + if d.decap == nil { + return nil, errors.New("mlkem decapsulation key is nil") + } + if len(ephemeral) == 0 { + return nil, errors.New("ciphertext encapsulation is required for ML-KEM decryption") + } + + sharedSecret, err := d.decap.Decapsulate(ephemeral) + if err != nil { + return nil, fmt.Errorf("mlkem.Decapsulate failed: %w", err) + } + + block, err := aes.NewCipher(sharedSecret) + if err != nil { + return nil, fmt.Errorf("aes.NewCipher failure: %w", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("cipher.NewGCM failure: %w", err) + } + + nonceSize := gcm.NonceSize() + if len(data) < nonceSize { + return nil, errors.New("ciphertext too short") + } + + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, fmt.Errorf("gcm.Open failure: %w", err) + } + + return plaintext, nil +} diff --git a/lib/ocrypto/asym_encryption.go b/lib/ocrypto/asym_encryption.go index 2a030c3f32..919913348b 100644 --- a/lib/ocrypto/asym_encryption.go +++ b/lib/ocrypto/asym_encryption.go @@ -5,6 +5,7 @@ import ( "crypto/cipher" "crypto/ecdh" "crypto/ecdsa" + "crypto/mlkem" "crypto/rand" "crypto/rsa" "crypto/sha1" //nolint:gosec // used for padding which is safe @@ -26,6 +27,7 @@ const ( RSA SchemeType = "wrapped" EC SchemeType = "ec-wrapped" Hybrid SchemeType = "hybrid-wrapped" + MLKEM SchemeType = "mlkem-wrapped" ) type PublicKeyEncryptor interface { @@ -60,6 +62,18 @@ type ECEncryptor struct { info []byte } +type MLKEMEncryptor768 struct { + pub *mlkem.EncapsulationKey768 + cipherText []byte + sharedSecret []byte +} + +type MLKEMEncryptor1024 struct { + pub *mlkem.EncapsulationKey1024 + cipherText []byte + sharedSecret []byte +} + func FromPublicPEM(publicKeyInPem string) (PublicKeyEncryptor, error) { // TK Move salt and info out of library, into API option functions digest := sha256.New() @@ -99,6 +113,10 @@ func FromPublicPEMWithSalt(publicKeyInPem string, salt, info []byte) (PublicKeyE return newECIES(e, salt, info) case *ecdh.PublicKey: return newECIES(pub, salt, info) + case *mlkem.EncapsulationKey768: + return newMLKEM768(pub), nil + case *mlkem.EncapsulationKey1024: + return newMLKEM1024(pub), nil default: break } @@ -111,6 +129,16 @@ func newECIES(pub *ecdh.PublicKey, salt, info []byte) (ECEncryptor, error) { return ECEncryptor{pub, ek, salt, info}, err } +func newMLKEM768(pub *mlkem.EncapsulationKey768) *MLKEMEncryptor768 { + sharedSecret, cipherText := pub.Encapsulate() + return &MLKEMEncryptor768{pub: pub, cipherText: cipherText, sharedSecret: sharedSecret} +} + +func newMLKEM1024(pub *mlkem.EncapsulationKey1024) *MLKEMEncryptor1024 { + sharedSecret, cipherText := pub.Encapsulate() + return &MLKEMEncryptor1024{pub: pub, cipherText: cipherText, sharedSecret: sharedSecret} +} + // NewAsymEncryption creates and returns a new AsymEncryption. // // Deprecated: Use FromPublicPEM instead. @@ -282,3 +310,89 @@ func (e ECEncryptor) Encrypt(data []byte) ([]byte, error) { func (e ECEncryptor) PublicKeyInPemFormat() (string, error) { return publicKeyInPemFormat(e.ek.Public()) } + +// MLKEMEncryptor768 methods + +func (e MLKEMEncryptor768) Type() SchemeType { + return MLKEM +} + +func (e MLKEMEncryptor768) KeyType() KeyType { + return MLKEM768Key +} + +func (e MLKEMEncryptor768) EphemeralKey() []byte { + return e.cipherText +} + +func (e MLKEMEncryptor768) Metadata() (map[string]string, error) { + return map[string]string{ + "ephemeralKey": string(e.cipherText), + }, nil +} + +func (e MLKEMEncryptor768) Encrypt(data []byte) ([]byte, error) { + block, err := aes.NewCipher(e.sharedSecret) + if err != nil { + return nil, fmt.Errorf("aes.NewCipher failed: %w", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("cipher.NewGCM failed: %w", err) + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, fmt.Errorf("nonce generation failed: %w", err) + } + + return gcm.Seal(nonce, nonce, data, nil), nil +} + +func (e MLKEMEncryptor768) PublicKeyInPemFormat() (string, error) { + return "", errors.New("public key PEM not available for ML-KEM encryptor") +} + +// MLKEMEncryptor1024 methods + +func (e MLKEMEncryptor1024) Type() SchemeType { + return MLKEM +} + +func (e MLKEMEncryptor1024) KeyType() KeyType { + return MLKEM1024Key +} + +func (e MLKEMEncryptor1024) EphemeralKey() []byte { + return e.cipherText +} + +func (e MLKEMEncryptor1024) Metadata() (map[string]string, error) { + return map[string]string{ + "ephemeralKey": string(e.cipherText), + }, nil +} + +func (e MLKEMEncryptor1024) Encrypt(data []byte) ([]byte, error) { + block, err := aes.NewCipher(e.sharedSecret) + if err != nil { + return nil, fmt.Errorf("aes.NewCipher failed: %w", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("cipher.NewGCM failed: %w", err) + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, fmt.Errorf("nonce generation failed: %w", err) + } + + return gcm.Seal(nonce, nonce, data, nil), nil +} + +func (e MLKEMEncryptor1024) PublicKeyInPemFormat() (string, error) { + return "", errors.New("public key PEM not available for ML-KEM encryptor") +} diff --git a/lib/ocrypto/hybrid_nist.go b/lib/ocrypto/hybrid_nist.go index ee0a575ac5..223100de70 100644 --- a/lib/ocrypto/hybrid_nist.go +++ b/lib/ocrypto/hybrid_nist.go @@ -6,6 +6,7 @@ import ( "crypto/rand" "crypto/sha256" "encoding/asn1" + "errors" "fmt" "io" @@ -517,3 +518,10 @@ func deriveHybridNISTWrapKey(combinedSecret, salt, info []byte) ([]byte, error) return derivedKey, nil } + +func (d *HybridNISTDecryptor) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) { + if len(ephemeral) > 0 { + return nil, errors.New("ephemeral key should not be provided for hybrid NIST decryption") + } + return d.Decrypt(data) +} diff --git a/lib/ocrypto/xwing.go b/lib/ocrypto/xwing.go index 1ea9d5ab25..1c4a57a89c 100644 --- a/lib/ocrypto/xwing.go +++ b/lib/ocrypto/xwing.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/asn1" "encoding/pem" + "errors" "fmt" "io" @@ -258,3 +259,10 @@ func decodeSizedPEMBlock(data []byte, blockType string, expectedSize int) ([]byt return append([]byte(nil), block.Bytes...), nil } + +func (d *XWingDecryptor) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) { + if len(ephemeral) > 0 { + return nil, errors.New("ephemeral key should not be provided for X-Wing decryption") + } + return d.Decrypt(data) +} From 81909bf3c66f7c27f48431152e6f0b52f493559c Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Wed, 27 May 2026 15:48:19 -0400 Subject: [PATCH 03/25] feat(kas): add ml-kem support to service layer Add ML-KEM-768 and ML-KEM-1024 support to the KAS service layer: - Add AlgorithmMLKEM768 and AlgorithmMLKEM1024 constants - Add ML-KEM decryption support to BasicManager - Add StandardMLKEMCrypto type for ML-KEM key management - Add MLKEMPublicKey method for public key export - Add ML-KEM case to StandardCrypto.Decrypt method Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Dave Mihalcik --- service/internal/security/basic_manager.go | 10 +++++ service/internal/security/crypto_provider.go | 4 ++ service/internal/security/standard_crypto.go | 42 ++++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/service/internal/security/basic_manager.go b/service/internal/security/basic_manager.go index a33ce486f7..4e5f303fd3 100644 --- a/service/internal/security/basic_manager.go +++ b/service/internal/security/basic_manager.go @@ -162,6 +162,16 @@ func (b *BasicManager) Decrypt(ctx context.Context, keyDetails trust.KeyDetails, return nil, fmt.Errorf("failed to create protected key: %w", err) } return protectedKey, nil + case ocrypto.MLKEM768Key, ocrypto.MLKEM1024Key: + plaintext, err := decrypter.DecryptWithEphemeralKey(ciphertext, ephemeralPublicKey) + if err != nil { + return nil, fmt.Errorf("failed to decrypt with ML-KEM: %w", err) + } + protectedKey, err := ocrypto.NewAESProtectedKey(plaintext) + if err != nil { + return nil, fmt.Errorf("failed to create protected key: %w", err) + } + return protectedKey, nil } return nil, fmt.Errorf("unsupported algorithm: %s", keyDetails.Algorithm()) diff --git a/service/internal/security/crypto_provider.go b/service/internal/security/crypto_provider.go index dcbe1ae5a1..4a0b9a28ec 100644 --- a/service/internal/security/crypto_provider.go +++ b/service/internal/security/crypto_provider.go @@ -18,4 +18,8 @@ const ( // Used for hybrid NIST EC + ML-KEM wrapping of the KAO AlgorithmHPQTSecp256r1MLKEM768 = "hpqt:secp256r1-mlkem768" AlgorithmHPQTSecp384r1MLKEM1024 = "hpqt:secp384r1-mlkem1024" + + // Used for encryption with ML-KEM of the KAO + AlgorithmMLKEM768 = "mlkem:768" + AlgorithmMLKEM1024 = "mlkem:1024" ) diff --git a/service/internal/security/standard_crypto.go b/service/internal/security/standard_crypto.go index 59953a8523..5752331fd0 100644 --- a/service/internal/security/standard_crypto.go +++ b/service/internal/security/standard_crypto.go @@ -79,6 +79,12 @@ type StandardHybridCrypto struct { hybridPublicKeyPem string } +type StandardMLKEMCrypto struct { + KeyPairInfo + mlkemPrivateKeyPem string + mlkemPublicKeyPem string +} + // List of keys by identifier type keylist map[string]any @@ -174,6 +180,12 @@ func loadKey(k KeyPairInfo) (any, error) { hybridPrivateKeyPem: string(privatePEM), hybridPublicKeyPem: string(certPEM), }, nil + case AlgorithmMLKEM768, AlgorithmMLKEM1024: + return StandardMLKEMCrypto{ + KeyPairInfo: k, + mlkemPrivateKeyPem: string(privatePEM), + mlkemPublicKeyPem: string(certPEM), + }, nil case AlgorithmRSA2048, AlgorithmRSA4096: asymDecryption, err := ocrypto.NewAsymDecryption(string(privatePEM)) if err != nil { @@ -389,6 +401,21 @@ func (s StandardCrypto) HybridPublicKey(kid string) (string, error) { } } +func (s StandardCrypto) MLKEMPublicKey(kid string) (string, error) { + k, ok := s.keysByID[kid] + if !ok { + return "", fmt.Errorf("no mlkem key with id [%s]: %w", kid, ErrCertNotFound) + } + mlkem, ok := k.(StandardMLKEMCrypto) + if !ok { + return "", fmt.Errorf("key with id [%s] is not an ML-KEM key: %w", kid, ErrCertNotFound) + } + if mlkem.mlkemPublicKeyPem == "" { + return "", fmt.Errorf("no ML-KEM public key with id [%s]: %w", kid, ErrCertNotFound) + } + return mlkem.mlkemPublicKeyPem, nil +} + func (s StandardCrypto) RSADecrypt(_ crypto.Hash, kid string, _ string, ciphertext []byte) ([]byte, error) { k, ok := s.keysByID[kid] if !ok { @@ -542,6 +569,21 @@ func (s *StandardCrypto) Decrypt(_ context.Context, keyID trust.KeyIdentifier, c return nil, fmt.Errorf("unsupported hybrid algorithm [%s]", key.Algorithm) } + case StandardMLKEMCrypto: + if len(ephemeralPublicKey) == 0 { + return nil, errors.New("ephemeral public key (ciphertext) is required for ML-KEM decryption") + } + + decryptor, err := ocrypto.FromPrivatePEM(key.mlkemPrivateKeyPem) + if err != nil { + return nil, fmt.Errorf("failed to create ML-KEM decryptor from PEM: %w", err) + } + + rawKey, err = decryptor.DecryptWithEphemeralKey(ciphertext, ephemeralPublicKey) + if err != nil { + return nil, fmt.Errorf("failed to decrypt with ML-KEM: %w", err) + } + default: return nil, fmt.Errorf("unsupported key type for key ID [%s]", kid) } From 956496b45843fb38332702ce4c784072a9592fb9 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Wed, 27 May 2026 15:52:21 -0400 Subject: [PATCH 04/25] feat(kas): extend ml-kem support across service layer Complete ML-KEM integration across the service layer: - Add ML-KEM algorithms to InProcessProvider key listing and decryption - Add ML-KEM support to public key export in KAS access layer - Add ML-KEM algorithm mappings in policy grant_mappings Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Dave Mihalcik --- service/internal/security/in_process_provider.go | 8 ++++++++ service/kas/access/publicKey.go | 7 +++++-- service/policy/db/grant_mappings.go | 4 ++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/service/internal/security/in_process_provider.go b/service/internal/security/in_process_provider.go index 7d4b624e22..43cb2c3a2c 100644 --- a/service/internal/security/in_process_provider.go +++ b/service/internal/security/in_process_provider.go @@ -27,6 +27,8 @@ var InProcessSupportedAlgorithms = []ocrypto.KeyType{ ocrypto.HybridXWingKey, ocrypto.HybridSecp256r1MLKEM768Key, ocrypto.HybridSecp384r1MLKEM1024Key, + ocrypto.MLKEM768Key, + ocrypto.MLKEM1024Key, } func convertPEMToJWK(_ string) (string, error) { @@ -274,6 +276,12 @@ func (a *InProcessProvider) Decrypt(ctx context.Context, keyDetails trust.KeyDet } return a.cryptoProvider.Decrypt(ctx, trust.KeyIdentifier(kid), ciphertext, nil) + case AlgorithmMLKEM768, AlgorithmMLKEM1024: + if len(ephemeralPublicKey) == 0 { + return nil, errors.New("ephemeral public key (ciphertext) is required for ML-KEM decryption") + } + return a.cryptoProvider.Decrypt(ctx, trust.KeyIdentifier(kid), ciphertext, ephemeralPublicKey) + default: return nil, errors.New("unsupported key algorithm") } diff --git a/service/kas/access/publicKey.go b/service/kas/access/publicKey.go index d7f381c307..14c6dd8a05 100644 --- a/service/kas/access/publicKey.go +++ b/service/kas/access/publicKey.go @@ -77,7 +77,8 @@ func (p *Provider) LegacyPublicKey(ctx context.Context, req *connect.Request[kas return nil, connect.NewError(connect.CodeInternal, errors.Join(ErrConfig, errors.New("configuration error"))) } case security.AlgorithmRSA2048, security.AlgorithmHPQTXWing, - security.AlgorithmHPQTSecp256r1MLKEM768, security.AlgorithmHPQTSecp384r1MLKEM1024, "": + security.AlgorithmHPQTSecp256r1MLKEM768, security.AlgorithmHPQTSecp384r1MLKEM1024, + security.AlgorithmMLKEM768, security.AlgorithmMLKEM1024, "": // For RSA keys, return the public key in PKCS8 format pem, err = keyDetails.ExportPublicKey(ctx, trust.KeyTypePKCS8) if err != nil { @@ -154,7 +155,9 @@ func (p *Provider) PublicKey(ctx context.Context, req *connect.Request[kaspb.Pub return r(ecPublicKeyPem, kid, err) case security.AlgorithmHPQTXWing, security.AlgorithmHPQTSecp256r1MLKEM768, - security.AlgorithmHPQTSecp384r1MLKEM1024: + security.AlgorithmHPQTSecp384r1MLKEM1024, + security.AlgorithmMLKEM768, + security.AlgorithmMLKEM1024: switch fmt { case "pkcs8", "": publicKeyPEM, err := keyDetails.ExportPublicKey(ctx, trust.KeyTypePKCS8) diff --git a/service/policy/db/grant_mappings.go b/service/policy/db/grant_mappings.go index 7cd8830f92..2d1683c56e 100644 --- a/service/policy/db/grant_mappings.go +++ b/service/policy/db/grant_mappings.go @@ -28,6 +28,10 @@ func mapAlgorithmToKasPublicKeyAlg(alg policy.Algorithm) policy.KasPublicKeyAlgE return policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768 case policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024: return policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024 + case policy.Algorithm_ALGORITHM_MLKEM_768: + return policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768 + case policy.Algorithm_ALGORITHM_MLKEM_1024: + return policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_1024 case policy.Algorithm_ALGORITHM_UNSPECIFIED: return policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED default: From c74c1acc57ba66d99a28ef0ace5d8b4ef2cbe015 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Wed, 27 May 2026 15:59:15 -0400 Subject: [PATCH 05/25] feat(sdk): add ml-kem algorithm support Add ML-KEM-768 and ML-KEM-1024 algorithm support to SDK: - Add ML-KEM cases to KeyTypeToPolicyAlgorithm conversion - Add ML-KEM cases to PolicyAlgorithmToKeyType conversion - Add ML-KEM enum mappings in convertAlgEnum2Simple - Add ML-KEM string mappings in algProto2String Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Dave Mihalcik --- sdk/basekey.go | 8 ++++++++ sdk/granter.go | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/sdk/basekey.go b/sdk/basekey.go index 96f43f927c..a445729acd 100644 --- a/sdk/basekey.go +++ b/sdk/basekey.go @@ -46,6 +46,10 @@ func KeyTypeToPolicyAlgorithm(kt ocrypto.KeyType) (policy.Algorithm, error) { return policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768, nil case ocrypto.HybridSecp384r1MLKEM1024Key: return policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024, nil + case ocrypto.MLKEM768Key: + return policy.Algorithm_ALGORITHM_MLKEM_768, nil + case ocrypto.MLKEM1024Key: + return policy.Algorithm_ALGORITHM_MLKEM_1024, nil default: return policy.Algorithm_ALGORITHM_UNSPECIFIED, fmt.Errorf("unknown key type: %s", kt) } @@ -69,6 +73,10 @@ func PolicyAlgorithmToKeyType(alg policy.Algorithm) (ocrypto.KeyType, error) { return ocrypto.HybridSecp256r1MLKEM768Key, nil case policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024: return ocrypto.HybridSecp384r1MLKEM1024Key, nil + case policy.Algorithm_ALGORITHM_MLKEM_768: + return ocrypto.MLKEM768Key, nil + case policy.Algorithm_ALGORITHM_MLKEM_1024: + return ocrypto.MLKEM1024Key, nil case policy.Algorithm_ALGORITHM_UNSPECIFIED: fallthrough default: diff --git a/sdk/granter.go b/sdk/granter.go index ede578cc17..571616efa0 100644 --- a/sdk/granter.go +++ b/sdk/granter.go @@ -292,6 +292,10 @@ func convertAlgEnum2Simple(a policy.KasPublicKeyAlgEnum) policy.Algorithm { return policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768 case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024: return policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024 + case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768: + return policy.Algorithm_ALGORITHM_MLKEM_768 + case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_1024: + return policy.Algorithm_ALGORITHM_MLKEM_1024 case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED: return policy.Algorithm_ALGORITHM_UNSPECIFIED default: @@ -484,6 +488,10 @@ func algProto2String(e policy.KasPublicKeyAlgEnum) string { return string(ocrypto.HybridSecp256r1MLKEM768Key) case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024: return string(ocrypto.HybridSecp384r1MLKEM1024Key) + case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768: + return string(ocrypto.MLKEM768Key) + case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_1024: + return string(ocrypto.MLKEM1024Key) case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED: return "" } From 8c3b7841aa0dbecd429ddd2b84e3b9c4604ee841 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Wed, 27 May 2026 16:04:50 -0400 Subject: [PATCH 06/25] feat(otdfctl,sdk): add ml-kem support to CLI and experimental SDK Add ML-KEM-768 and ML-KEM-1024 support to otdfctl and experimental SDK: - Add ML-KEM key generation in kasKeys command - Add "mlkem:768" and "mlkem:1024" string mappings in CLI helpers - Add ML-KEM validation in PEM validator - Add ML-KEM enum conversions in experimental keysplit SDK Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Dave Mihalcik --- otdfctl/cmd/policy/kasKeys.go | 4 ++++ otdfctl/pkg/cli/sdkHelpers.go | 8 ++++++++ otdfctl/pkg/utils/pemvalidate.go | 8 ++++++++ sdk/experimental/tdf/keysplit/attributes.go | 4 ++++ 4 files changed, 24 insertions(+) diff --git a/otdfctl/cmd/policy/kasKeys.go b/otdfctl/cmd/policy/kasKeys.go index e811486284..fab10c03e6 100644 --- a/otdfctl/cmd/policy/kasKeys.go +++ b/otdfctl/cmd/policy/kasKeys.go @@ -89,6 +89,10 @@ func generateKeyPair(alg policy.Algorithm) (ocrypto.KeyPair, error) { key, err = ocrypto.NewKeyPair(ocrypto.HybridSecp256r1MLKEM768Key) case policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024: key, err = ocrypto.NewKeyPair(ocrypto.HybridSecp384r1MLKEM1024Key) + case policy.Algorithm_ALGORITHM_MLKEM_768: + key, err = ocrypto.NewKeyPair(ocrypto.MLKEM768Key) + case policy.Algorithm_ALGORITHM_MLKEM_1024: + key, err = ocrypto.NewKeyPair(ocrypto.MLKEM1024Key) case policy.Algorithm_ALGORITHM_UNSPECIFIED: fallthrough default: diff --git a/otdfctl/pkg/cli/sdkHelpers.go b/otdfctl/pkg/cli/sdkHelpers.go index 943fbbc71e..941e406b7e 100644 --- a/otdfctl/pkg/cli/sdkHelpers.go +++ b/otdfctl/pkg/cli/sdkHelpers.go @@ -131,6 +131,10 @@ func KeyAlgToEnum(alg string) (policy.Algorithm, error) { return policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768, nil case "hpqt:secp384r1-mlkem1024": return policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024, nil + case "mlkem:768": + return policy.Algorithm_ALGORITHM_MLKEM_768, nil + case "mlkem:1024": + return policy.Algorithm_ALGORITHM_MLKEM_1024, nil default: return policy.Algorithm_ALGORITHM_UNSPECIFIED, errors.New("invalid algorithm") } @@ -154,6 +158,10 @@ func KeyEnumToAlg(enum policy.Algorithm) (string, error) { return "hpqt:secp256r1-mlkem768", nil case policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024: return "hpqt:secp384r1-mlkem1024", nil + case policy.Algorithm_ALGORITHM_MLKEM_768: + return "mlkem:768", nil + case policy.Algorithm_ALGORITHM_MLKEM_1024: + return "mlkem:1024", nil default: return "", errors.New("invalid enum algorithm") } diff --git a/otdfctl/pkg/utils/pemvalidate.go b/otdfctl/pkg/utils/pemvalidate.go index 29fc17f8ef..558af3cd55 100644 --- a/otdfctl/pkg/utils/pemvalidate.go +++ b/otdfctl/pkg/utils/pemvalidate.go @@ -53,6 +53,14 @@ func ValidatePublicKeyPEM(pemBytes []byte, expected policy.Algorithm) error { if enc.KeyType() != ocrypto.HybridSecp384r1MLKEM1024Key { return errors.New("algorithm mismatch: expected hybrid NIST P-384 + ML-KEM-1024") } + case policy.Algorithm_ALGORITHM_MLKEM_768: + if enc.KeyType() != ocrypto.MLKEM768Key { + return errors.New("algorithm mismatch: expected ML-KEM-768") + } + case policy.Algorithm_ALGORITHM_MLKEM_1024: + if enc.KeyType() != ocrypto.MLKEM1024Key { + return errors.New("algorithm mismatch: expected ML-KEM-1024") + } case policy.Algorithm_ALGORITHM_UNSPECIFIED: fallthrough default: diff --git a/sdk/experimental/tdf/keysplit/attributes.go b/sdk/experimental/tdf/keysplit/attributes.go index 27cc1e386f..f21fab3824 100644 --- a/sdk/experimental/tdf/keysplit/attributes.go +++ b/sdk/experimental/tdf/keysplit/attributes.go @@ -213,6 +213,10 @@ func convertAlgEnum2Simple(a policy.KasPublicKeyAlgEnum) policy.Algorithm { return policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768 case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024: return policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024 + case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_768: + return policy.Algorithm_ALGORITHM_MLKEM_768 + case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_MLKEM_1024: + return policy.Algorithm_ALGORITHM_MLKEM_1024 case policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED: return policy.Algorithm_ALGORITHM_UNSPECIFIED default: From 4cdc7c936a099a16b9dcbb04cdf866b3a52cc87f Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Wed, 27 May 2026 16:09:21 -0400 Subject: [PATCH 07/25] feat(keygen,tests): add ml-kem key generation support Add ML-KEM-768 and ML-KEM-1024 key generation: - Add ML-KEM key generation to service keygen command - Add ML-KEM key generation to BDD test utilities - Generate kas-mlkem768-private.pem and kas-mlkem768-public.pem - Generate kas-mlkem1024-private.pem and kas-mlkem1024-public.pem Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Dave Mihalcik --- service/cmd/keygen/main.go | 46 +++++++++++++++++++++++++- tests-bdd/cukes/utils/utils_genKeys.go | 36 +++++++++++++++++++- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/service/cmd/keygen/main.go b/service/cmd/keygen/main.go index 721191e286..f182339e6e 100644 --- a/service/cmd/keygen/main.go +++ b/service/cmd/keygen/main.go @@ -1,4 +1,4 @@ -// Package main generates hybrid post-quantum KAS key pairs (X-Wing, P256+ML-KEM-768, P384+ML-KEM-1024) +// Package main generates post-quantum KAS key pairs (X-Wing, P256+ML-KEM-768, P384+ML-KEM-1024, ML-KEM-768, ML-KEM-1024) // as PEM files for use with the OpenTDF platform. package main @@ -45,6 +45,18 @@ func main() { privateOut: "kas-p384mlkem1024-private.pem", publicOut: "kas-p384mlkem1024-public.pem", }, + { + name: "ML-KEM-768", + newKeyPair: generateMLKEM768, + privateOut: "kas-mlkem768-private.pem", + publicOut: "kas-mlkem768-public.pem", + }, + { + name: "ML-KEM-1024", + newKeyPair: generateMLKEM1024, + privateOut: "kas-mlkem1024-private.pem", + publicOut: "kas-mlkem1024-public.pem", + }, } for _, s := range specs { @@ -114,3 +126,35 @@ func generateP384MLKEM1024() (string, string, error) { } return priv, pub, nil } + +func generateMLKEM768() (string, string, error) { + kp, err := ocrypto.NewMLKEMKeyPair() + if err != nil { + return "", "", err + } + priv, err := kp.PrivateKeyInPemFormat() + if err != nil { + return "", "", err + } + pub, err := kp.PublicKeyInPemFormat() + if err != nil { + return "", "", err + } + return priv, pub, nil +} + +func generateMLKEM1024() (string, string, error) { + kp, err := ocrypto.NewMLKEM1024KeyPair() + if err != nil { + return "", "", err + } + priv, err := kp.PrivateKeyInPemFormat() + if err != nil { + return "", "", err + } + pub, err := kp.PublicKeyInPemFormat() + if err != nil { + return "", "", err + } + return priv, pub, nil +} diff --git a/tests-bdd/cukes/utils/utils_genKeys.go b/tests-bdd/cukes/utils/utils_genKeys.go index 3ad0b57599..5a3d344b95 100644 --- a/tests-bdd/cukes/utils/utils_genKeys.go +++ b/tests-bdd/cukes/utils/utils_genKeys.go @@ -207,7 +207,7 @@ func createJavaKeystore(ctx context.Context, certPath, keystorePath string) { log.Printf("Java keystore generated successfully: %s", keystorePath) } -// generateHybridKeys creates X-Wing, P256+ML-KEM-768, and P384+ML-KEM-1024 key pairs. +// generateHybridKeys creates post-quantum key pairs: X-Wing, P256+ML-KEM-768, P384+ML-KEM-1024, ML-KEM-768, and ML-KEM-1024. func generateHybridKeys(outputPath string) { specs := []struct { name string @@ -218,6 +218,8 @@ func generateHybridKeys(outputPath string) { {"X-Wing", generateXWingKeyPair, "kas-xwing-private.pem", "kas-xwing-public.pem"}, {"P256+ML-KEM-768", generateP256MLKEM768KeyPair, "kas-p256mlkem768-private.pem", "kas-p256mlkem768-public.pem"}, {"P384+ML-KEM-1024", generateP384MLKEM1024KeyPair, "kas-p384mlkem1024-private.pem", "kas-p384mlkem1024-public.pem"}, + {"ML-KEM-768", generateMLKEM768KeyPair, "kas-mlkem768-private.pem", "kas-mlkem768-public.pem"}, + {"ML-KEM-1024", generateMLKEM1024KeyPair, "kas-mlkem1024-private.pem", "kas-mlkem1024-public.pem"}, } for _, s := range specs { @@ -289,3 +291,35 @@ func generateP384MLKEM1024KeyPair() (string, string, error) { } return priv, pub, nil } + +func generateMLKEM768KeyPair() (string, string, error) { + kp, err := ocrypto.NewMLKEMKeyPair() + if err != nil { + return "", "", err + } + priv, err := kp.PrivateKeyInPemFormat() + if err != nil { + return "", "", err + } + pub, err := kp.PublicKeyInPemFormat() + if err != nil { + return "", "", err + } + return priv, pub, nil +} + +func generateMLKEM1024KeyPair() (string, string, error) { + kp, err := ocrypto.NewMLKEM1024KeyPair() + if err != nil { + return "", "", err + } + priv, err := kp.PrivateKeyInPemFormat() + if err != nil { + return "", "", err + } + pub, err := kp.PublicKeyInPemFormat() + if err != nil { + return "", "", err + } + return priv, pub, nil +} From 0f89e33c7325edb9c33c074ef3b233b0f3c33c15 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Wed, 27 May 2026 16:23:15 -0400 Subject: [PATCH 08/25] test(tdf-roundtrips): add ML-KEM-768 and ML-KEM-1024 roundtrip tests Add pure post-quantum encryption tests for ML-KEM-768 and ML-KEM-1024 algorithms. Tests validate KAO type (mlkem-wrapped), KID assignment (m1, m2), and successful encrypt/decrypt roundtrips. Also updates KAS config to include ML-KEM keys. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Dave Mihalcik --- test/tdf-roundtrips.bats | 54 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/test/tdf-roundtrips.bats b/test/tdf-roundtrips.bats index e6f63f4fca..cd16a6c84c 100755 --- a/test/tdf-roundtrips.bats +++ b/test/tdf-roundtrips.bats @@ -89,6 +89,48 @@ printf '%s\n' "$output" | grep "Hello P384+ML-KEM-1024 wrappers!" } +@test "examples: roundtrip Z-TDF with ML-KEM-768 wrapped KAO" { + echo "[INFO] create a tdf3 format file" + run go run ./examples encrypt -o sensitive-with-mlkem768.txt.tdf --autoconfigure=false -A "mlkem:768" "Hello ML-KEM-768 wrappers!" + echo "[INFO] echoing output; if successful, this is just the manifest" + echo "$output" + + echo "[INFO] Validate the manifest lists the expected type in its KAO" + kaotype=$(jq -r '.encryptionInformation.keyAccess[0].type' <<<"${output}") + echo "$kaotype" + [ "$kaotype" = mlkem-wrapped ] + + kid=$(jq -r '.encryptionInformation.keyAccess[0].kid' <<<"${output}") + echo "kao.kid=$kid" + [ "$kid" = m1 ] + + echo "[INFO] decrypting..." + run go run ./examples decrypt sensitive-with-mlkem768.txt.tdf + echo "$output" + printf '%s\n' "$output" | grep "Hello ML-KEM-768 wrappers!" +} + +@test "examples: roundtrip Z-TDF with ML-KEM-1024 wrapped KAO" { + echo "[INFO] create a tdf3 format file" + run go run ./examples encrypt -o sensitive-with-mlkem1024.txt.tdf --autoconfigure=false -A "mlkem:1024" "Hello ML-KEM-1024 wrappers!" + echo "[INFO] echoing output; if successful, this is just the manifest" + echo "$output" + + echo "[INFO] Validate the manifest lists the expected type in its KAO" + kaotype=$(jq -r '.encryptionInformation.keyAccess[0].type' <<<"${output}") + echo "$kaotype" + [ "$kaotype" = mlkem-wrapped ] + + kid=$(jq -r '.encryptionInformation.keyAccess[0].kid' <<<"${output}") + echo "kao.kid=$kid" + [ "$kid" = m2 ] + + echo "[INFO] decrypting..." + run go run ./examples decrypt sensitive-with-mlkem1024.txt.tdf + echo "$output" + printf '%s\n' "$output" | grep "Hello ML-KEM-1024 wrappers!" +} + @test "examples: legacy key support Z-TDF" { echo "[INFO] validating default key is r1" echo "[INFO] default key result: $(grpcurl "localhost:8080" "kas.AccessService/PublicKey")" @@ -272,6 +314,10 @@ services: alg: hpqt:secp256r1-mlkem768 - kid: h2 alg: hpqt:secp384r1-mlkem1024 + - kid: m1 + alg: mlkem:768 + - kid: m2 + alg: mlkem:1024 policy: enabled: true authorization: @@ -331,6 +377,14 @@ server: alg: hpqt:secp384r1-mlkem1024 private: kas-p384mlkem1024-private.pem cert: kas-p384mlkem1024-public.pem + - kid: m1 + alg: mlkem:768 + private: kas-mlkem768-private.pem + cert: kas-mlkem768-public.pem + - kid: m2 + alg: mlkem:1024 + private: kas-mlkem1024-private.pem + cert: kas-mlkem1024-public.pem port: 8080 opa: embedded: true From e6901417bcb2ddc61492dd4f5ed7a060a3f72e90 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Thu, 28 May 2026 08:16:43 -0400 Subject: [PATCH 09/25] refactor(ocrypto): adopt X-Wing pattern for ML-KEM implementation Refactors plain ML-KEM (768/1024) to use ASN.1 DER encoding and HKDF key derivation, matching the X-Wing and Hybrid NIST patterns. Stores ML-KEM ciphertext concatenated with wrapped DEK in ASN.1 structure instead of overloading ephemeralKey metadata. - Add lib/ocrypto/mlkem.go with X-Wing-style wrap/unwrap functions - Add comprehensive test coverage in mlkem_test.go - Preserve backwards compatibility by renaming old types to Legacy variants - Update otdfctl documentation to include mlkem:768 and mlkem:1024 algorithms Co-Authored-By: Claude Sonnet 4.5 --- lib/ocrypto/asym_decryption.go | 16 +- lib/ocrypto/asym_encryption.go | 36 +- lib/ocrypto/mlkem.go | 405 ++++++++++++++++++ lib/ocrypto/mlkem_test.go | 222 ++++++++++ .../man/policy/kas-registry/key/create.md | 2 + .../man/policy/kas-registry/key/import.md | 2 + .../man/policy/kas-registry/key/rotate.md | 2 + 7 files changed, 659 insertions(+), 26 deletions(-) create mode 100644 lib/ocrypto/mlkem.go create mode 100644 lib/ocrypto/mlkem_test.go diff --git a/lib/ocrypto/asym_decryption.go b/lib/ocrypto/asym_decryption.go index e241ed391c..3f10f520cb 100644 --- a/lib/ocrypto/asym_decryption.go +++ b/lib/ocrypto/asym_decryption.go @@ -32,11 +32,11 @@ type PrivateKeyDecryptor interface { DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) } -type MLKEMDecryptor768 struct { +type MLKEMDecryptor768Legacy struct { decap *mlkem.DecapsulationKey768 } -type MLKEMDecryptor1024 struct { +type MLKEMDecryptor1024Legacy struct { decap *mlkem.DecapsulationKey1024 } @@ -67,13 +67,13 @@ func FromPrivatePEMWithSalt(privateKeyInPem string, salt, info []byte) (PrivateK if block.Type == "MLKEM DECAPSULATION KEY" { decap768, err := mlkem.NewDecapsulationKey768(block.Bytes) if err == nil { - return &MLKEMDecryptor768{decap: decap768}, nil + return &MLKEMDecryptor768Legacy{decap: decap768}, nil } decap1024, err1024 := mlkem.NewDecapsulationKey1024(block.Bytes) if err1024 != nil { return nil, fmt.Errorf("mlkem.NewDecapsulationKey1024 failed after mlkem.NewDecapsulationKey768 failed: %w / %w", err, err1024) } - return &MLKEMDecryptor1024{decap: decap1024}, nil + return &MLKEMDecryptor1024Legacy{decap: decap1024}, nil } priv, err := x509.ParsePKCS8PrivateKey(block.Bytes) @@ -246,15 +246,15 @@ func convCurve(c ecdh.Curve) elliptic.Curve { } } -func (d MLKEMDecryptor768) Decrypt(_ []byte) ([]byte, error) { +func (d MLKEMDecryptor768Legacy) Decrypt(_ []byte) ([]byte, error) { return nil, errors.New("ciphertext encapsulation is required for ML-KEM decryption") } -func (d MLKEMDecryptor1024) Decrypt(_ []byte) ([]byte, error) { +func (d MLKEMDecryptor1024Legacy) Decrypt(_ []byte) ([]byte, error) { return nil, errors.New("ciphertext encapsulation is required for ML-KEM decryption") } -func (d MLKEMDecryptor768) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) { +func (d MLKEMDecryptor768Legacy) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) { if d.decap == nil { return nil, errors.New("mlkem decapsulation key is nil") } @@ -291,7 +291,7 @@ func (d MLKEMDecryptor768) DecryptWithEphemeralKey(data, ephemeral []byte) ([]by return plaintext, nil } -func (d MLKEMDecryptor1024) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) { +func (d MLKEMDecryptor1024Legacy) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) { if d.decap == nil { return nil, errors.New("mlkem decapsulation key is nil") } diff --git a/lib/ocrypto/asym_encryption.go b/lib/ocrypto/asym_encryption.go index 919913348b..417dc41b62 100644 --- a/lib/ocrypto/asym_encryption.go +++ b/lib/ocrypto/asym_encryption.go @@ -62,13 +62,13 @@ type ECEncryptor struct { info []byte } -type MLKEMEncryptor768 struct { +type MLKEMEncryptor768Legacy struct { pub *mlkem.EncapsulationKey768 cipherText []byte sharedSecret []byte } -type MLKEMEncryptor1024 struct { +type MLKEMEncryptor1024Legacy struct { pub *mlkem.EncapsulationKey1024 cipherText []byte sharedSecret []byte @@ -129,14 +129,14 @@ func newECIES(pub *ecdh.PublicKey, salt, info []byte) (ECEncryptor, error) { return ECEncryptor{pub, ek, salt, info}, err } -func newMLKEM768(pub *mlkem.EncapsulationKey768) *MLKEMEncryptor768 { +func newMLKEM768(pub *mlkem.EncapsulationKey768) *MLKEMEncryptor768Legacy { sharedSecret, cipherText := pub.Encapsulate() - return &MLKEMEncryptor768{pub: pub, cipherText: cipherText, sharedSecret: sharedSecret} + return &MLKEMEncryptor768Legacy{pub: pub, cipherText: cipherText, sharedSecret: sharedSecret} } -func newMLKEM1024(pub *mlkem.EncapsulationKey1024) *MLKEMEncryptor1024 { +func newMLKEM1024(pub *mlkem.EncapsulationKey1024) *MLKEMEncryptor1024Legacy { sharedSecret, cipherText := pub.Encapsulate() - return &MLKEMEncryptor1024{pub: pub, cipherText: cipherText, sharedSecret: sharedSecret} + return &MLKEMEncryptor1024Legacy{pub: pub, cipherText: cipherText, sharedSecret: sharedSecret} } // NewAsymEncryption creates and returns a new AsymEncryption. @@ -313,25 +313,25 @@ func (e ECEncryptor) PublicKeyInPemFormat() (string, error) { // MLKEMEncryptor768 methods -func (e MLKEMEncryptor768) Type() SchemeType { +func (e MLKEMEncryptor768Legacy) Type() SchemeType { return MLKEM } -func (e MLKEMEncryptor768) KeyType() KeyType { +func (e MLKEMEncryptor768Legacy) KeyType() KeyType { return MLKEM768Key } -func (e MLKEMEncryptor768) EphemeralKey() []byte { +func (e MLKEMEncryptor768Legacy) EphemeralKey() []byte { return e.cipherText } -func (e MLKEMEncryptor768) Metadata() (map[string]string, error) { +func (e MLKEMEncryptor768Legacy) Metadata() (map[string]string, error) { return map[string]string{ "ephemeralKey": string(e.cipherText), }, nil } -func (e MLKEMEncryptor768) Encrypt(data []byte) ([]byte, error) { +func (e MLKEMEncryptor768Legacy) Encrypt(data []byte) ([]byte, error) { block, err := aes.NewCipher(e.sharedSecret) if err != nil { return nil, fmt.Errorf("aes.NewCipher failed: %w", err) @@ -350,31 +350,31 @@ func (e MLKEMEncryptor768) Encrypt(data []byte) ([]byte, error) { return gcm.Seal(nonce, nonce, data, nil), nil } -func (e MLKEMEncryptor768) PublicKeyInPemFormat() (string, error) { +func (e MLKEMEncryptor768Legacy) PublicKeyInPemFormat() (string, error) { return "", errors.New("public key PEM not available for ML-KEM encryptor") } // MLKEMEncryptor1024 methods -func (e MLKEMEncryptor1024) Type() SchemeType { +func (e MLKEMEncryptor1024Legacy) Type() SchemeType { return MLKEM } -func (e MLKEMEncryptor1024) KeyType() KeyType { +func (e MLKEMEncryptor1024Legacy) KeyType() KeyType { return MLKEM1024Key } -func (e MLKEMEncryptor1024) EphemeralKey() []byte { +func (e MLKEMEncryptor1024Legacy) EphemeralKey() []byte { return e.cipherText } -func (e MLKEMEncryptor1024) Metadata() (map[string]string, error) { +func (e MLKEMEncryptor1024Legacy) Metadata() (map[string]string, error) { return map[string]string{ "ephemeralKey": string(e.cipherText), }, nil } -func (e MLKEMEncryptor1024) Encrypt(data []byte) ([]byte, error) { +func (e MLKEMEncryptor1024Legacy) Encrypt(data []byte) ([]byte, error) { block, err := aes.NewCipher(e.sharedSecret) if err != nil { return nil, fmt.Errorf("aes.NewCipher failed: %w", err) @@ -393,6 +393,6 @@ func (e MLKEMEncryptor1024) Encrypt(data []byte) ([]byte, error) { return gcm.Seal(nonce, nonce, data, nil), nil } -func (e MLKEMEncryptor1024) PublicKeyInPemFormat() (string, error) { +func (e MLKEMEncryptor1024Legacy) PublicKeyInPemFormat() (string, error) { return "", errors.New("public key PEM not available for ML-KEM encryptor") } diff --git a/lib/ocrypto/mlkem.go b/lib/ocrypto/mlkem.go new file mode 100644 index 0000000000..e64288dfa3 --- /dev/null +++ b/lib/ocrypto/mlkem.go @@ -0,0 +1,405 @@ +package ocrypto + +import ( + "crypto/mlkem" + "crypto/sha256" + "encoding/asn1" + "encoding/pem" + "errors" + "fmt" + "io" + + "golang.org/x/crypto/hkdf" +) + +const ( + MLKEM768PublicKeySize = 1184 // mlkem768 encapsulation key + MLKEM768PrivateKeySize = 64 // mlkem768 seed (d || z) + MLKEM768CiphertextSize = 1088 // mlkem768 ciphertext + MLKEM1024PublicKeySize = 1568 // mlkem1024 encapsulation key + MLKEM1024PrivateKeySize = 64 // mlkem1024 seed (d || z) + MLKEM1024CiphertextSize = 1568 // mlkem1024 ciphertext + + mlkemWrapKeySize = 32 // AES-256 key size for wrap key derivation +) + +type MLKEMWrappedKey struct { + MLKEMCiphertext []byte `asn1:"tag:0"` + EncryptedDEK []byte `asn1:"tag:1"` +} + +type MLKEMEncryptor768 struct { + publicKey []byte + salt []byte + info []byte +} + +type MLKEMDecryptor768 struct { + privateKey []byte + salt []byte + info []byte +} + +type MLKEMEncryptor1024 struct { + publicKey []byte + salt []byte + info []byte +} + +type MLKEMDecryptor1024 struct { + privateKey []byte + salt []byte + info []byte +} + +func NewMLKEM768Encryptor(publicKey, salt, info []byte) (*MLKEMEncryptor768, error) { + if len(publicKey) != MLKEM768PublicKeySize { + return nil, fmt.Errorf("invalid ML-KEM-768 public key size: got %d want %d", len(publicKey), MLKEM768PublicKeySize) + } + + return &MLKEMEncryptor768{ + publicKey: append([]byte(nil), publicKey...), + salt: cloneOrNil(salt), + info: cloneOrNil(info), + }, nil +} + +func (e *MLKEMEncryptor768) Encrypt(data []byte) ([]byte, error) { + return mlkem768WrapDEK(e.publicKey, data, e.salt, e.info) +} + +func (e *MLKEMEncryptor768) PublicKeyInPemFormat() (string, error) { + pemBlock := &pem.Block{ + Type: "MLKEM ENCAPSULATOR", + Bytes: e.publicKey, + } + return string(pem.EncodeToMemory(pemBlock)), nil +} + +func (e *MLKEMEncryptor768) Type() SchemeType { + return MLKEM +} + +func (e *MLKEMEncryptor768) KeyType() KeyType { + return MLKEM768Key +} + +func (e *MLKEMEncryptor768) EphemeralKey() []byte { + return nil +} + +func (e *MLKEMEncryptor768) Metadata() (map[string]string, error) { + return make(map[string]string), nil +} + +func NewMLKEM768Decryptor(privateKey []byte) (*MLKEMDecryptor768, error) { + return NewSaltedMLKEM768Decryptor(privateKey, defaultTDFSalt(), nil) +} + +func NewSaltedMLKEM768Decryptor(privateKey, salt, info []byte) (*MLKEMDecryptor768, error) { + if len(privateKey) != MLKEM768PrivateKeySize { + return nil, fmt.Errorf("invalid ML-KEM-768 private key size: got %d want %d", len(privateKey), MLKEM768PrivateKeySize) + } + + return &MLKEMDecryptor768{ + privateKey: append([]byte(nil), privateKey...), + salt: cloneOrNil(salt), + info: cloneOrNil(info), + }, nil +} + +func (d *MLKEMDecryptor768) Decrypt(data []byte) ([]byte, error) { + return mlkem768UnwrapDEK(d.privateKey, data, d.salt, d.info) +} + +func (d *MLKEMDecryptor768) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) { + if len(ephemeral) > 0 { + return nil, errors.New("ephemeral key should not be provided for ML-KEM-768 decryption") + } + return d.Decrypt(data) +} + +func NewMLKEM1024Encryptor(publicKey, salt, info []byte) (*MLKEMEncryptor1024, error) { + if len(publicKey) != MLKEM1024PublicKeySize { + return nil, fmt.Errorf("invalid ML-KEM-1024 public key size: got %d want %d", len(publicKey), MLKEM1024PublicKeySize) + } + + return &MLKEMEncryptor1024{ + publicKey: append([]byte(nil), publicKey...), + salt: cloneOrNil(salt), + info: cloneOrNil(info), + }, nil +} + +func (e *MLKEMEncryptor1024) Encrypt(data []byte) ([]byte, error) { + return mlkem1024WrapDEK(e.publicKey, data, e.salt, e.info) +} + +func (e *MLKEMEncryptor1024) PublicKeyInPemFormat() (string, error) { + pemBlock := &pem.Block{ + Type: "MLKEM ENCAPSULATOR", + Bytes: e.publicKey, + } + return string(pem.EncodeToMemory(pemBlock)), nil +} + +func (e *MLKEMEncryptor1024) Type() SchemeType { + return MLKEM +} + +func (e *MLKEMEncryptor1024) KeyType() KeyType { + return MLKEM1024Key +} + +func (e *MLKEMEncryptor1024) EphemeralKey() []byte { + return nil +} + +func (e *MLKEMEncryptor1024) Metadata() (map[string]string, error) { + return make(map[string]string), nil +} + +func NewMLKEM1024Decryptor(privateKey []byte) (*MLKEMDecryptor1024, error) { + return NewSaltedMLKEM1024Decryptor(privateKey, defaultTDFSalt(), nil) +} + +func NewSaltedMLKEM1024Decryptor(privateKey, salt, info []byte) (*MLKEMDecryptor1024, error) { + if len(privateKey) != MLKEM1024PrivateKeySize { + return nil, fmt.Errorf("invalid ML-KEM-1024 private key size: got %d want %d", len(privateKey), MLKEM1024PrivateKeySize) + } + + return &MLKEMDecryptor1024{ + privateKey: append([]byte(nil), privateKey...), + salt: cloneOrNil(salt), + info: cloneOrNil(info), + }, nil +} + +func (d *MLKEMDecryptor1024) Decrypt(data []byte) ([]byte, error) { + return mlkem1024UnwrapDEK(d.privateKey, data, d.salt, d.info) +} + +func (d *MLKEMDecryptor1024) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) { + if len(ephemeral) > 0 { + return nil, errors.New("ephemeral key should not be provided for ML-KEM-1024 decryption") + } + return d.Decrypt(data) +} + +func MLKEM768WrapDEK(publicKeyRaw, dek []byte) ([]byte, error) { + return mlkem768WrapDEK(publicKeyRaw, dek, defaultTDFSalt(), nil) +} + +func MLKEM768UnwrapDEK(privateKeyRaw, wrappedDER []byte) ([]byte, error) { + return mlkem768UnwrapDEK(privateKeyRaw, wrappedDER, defaultTDFSalt(), nil) +} + +func MLKEM1024WrapDEK(publicKeyRaw, dek []byte) ([]byte, error) { + return mlkem1024WrapDEK(publicKeyRaw, dek, defaultTDFSalt(), nil) +} + +func MLKEM1024UnwrapDEK(privateKeyRaw, wrappedDER []byte) ([]byte, error) { + return mlkem1024UnwrapDEK(privateKeyRaw, wrappedDER, defaultTDFSalt(), nil) +} + +// MLKEM768Encapsulate performs ML-KEM-768 encapsulation, returning the shared +// secret and ciphertext without applying KDF or encryption. +func MLKEM768Encapsulate(publicKeyRaw []byte) ([]byte, []byte, error) { + if len(publicKeyRaw) != MLKEM768PublicKeySize { + return nil, nil, fmt.Errorf("invalid ML-KEM-768 public key size: got %d want %d", len(publicKeyRaw), MLKEM768PublicKeySize) + } + + ek, err := mlkem.NewEncapsulationKey768(publicKeyRaw) + if err != nil { + return nil, nil, fmt.Errorf("mlkem.NewEncapsulationKey768 failed: %w", err) + } + + sharedSecret, ciphertext := ek.Encapsulate() + + return sharedSecret, ciphertext, nil +} + +// MLKEM1024Encapsulate performs ML-KEM-1024 encapsulation, returning the shared +// secret and ciphertext without applying KDF or encryption. +func MLKEM1024Encapsulate(publicKeyRaw []byte) ([]byte, []byte, error) { + if len(publicKeyRaw) != MLKEM1024PublicKeySize { + return nil, nil, fmt.Errorf("invalid ML-KEM-1024 public key size: got %d want %d", len(publicKeyRaw), MLKEM1024PublicKeySize) + } + + ek, err := mlkem.NewEncapsulationKey1024(publicKeyRaw) + if err != nil { + return nil, nil, fmt.Errorf("mlkem.NewEncapsulationKey1024 failed: %w", err) + } + + sharedSecret, ciphertext := ek.Encapsulate() + + return sharedSecret, ciphertext, nil +} + +func mlkem768WrapDEK(publicKeyRaw, dek, salt, info []byte) ([]byte, error) { + sharedSecret, ciphertext, err := MLKEM768Encapsulate(publicKeyRaw) + if err != nil { + return nil, err + } + + wrapKey, err := deriveMLKEMWrapKey(sharedSecret, salt, info) + if err != nil { + return nil, err + } + + gcm, err := NewAESGcm(wrapKey) + if err != nil { + return nil, fmt.Errorf("NewAESGcm failed: %w", err) + } + + encryptedDEK, err := gcm.Encrypt(dek) + if err != nil { + return nil, fmt.Errorf("AES-GCM encrypt failed: %w", err) + } + + wrappedDER, err := asn1.Marshal(MLKEMWrappedKey{ + MLKEMCiphertext: ciphertext, + EncryptedDEK: encryptedDEK, + }) + if err != nil { + return nil, fmt.Errorf("asn1.Marshal failed: %w", err) + } + + return wrappedDER, nil +} + +func mlkem768UnwrapDEK(privateKeyRaw, wrappedDER, salt, info []byte) ([]byte, error) { + if len(privateKeyRaw) != MLKEM768PrivateKeySize { + return nil, fmt.Errorf("invalid ML-KEM-768 private key size: got %d want %d", len(privateKeyRaw), MLKEM768PrivateKeySize) + } + + var wrappedKey MLKEMWrappedKey + rest, err := asn1.Unmarshal(wrappedDER, &wrappedKey) + if err != nil { + return nil, fmt.Errorf("asn1.Unmarshal failed: %w", err) + } + if len(rest) != 0 { + return nil, fmt.Errorf("asn1.Unmarshal left %d trailing bytes", len(rest)) + } + if len(wrappedKey.MLKEMCiphertext) != MLKEM768CiphertextSize { + return nil, fmt.Errorf("invalid ML-KEM-768 ciphertext size: got %d want %d", len(wrappedKey.MLKEMCiphertext), MLKEM768CiphertextSize) + } + + dk, err := mlkem.NewDecapsulationKey768(privateKeyRaw) + if err != nil { + return nil, fmt.Errorf("mlkem.NewDecapsulationKey768 failed: %w", err) + } + + sharedSecret, err := dk.Decapsulate(wrappedKey.MLKEMCiphertext) + if err != nil { + return nil, fmt.Errorf("mlkem768 decapsulate failed: %w", err) + } + + wrapKey, err := deriveMLKEMWrapKey(sharedSecret, salt, info) + if err != nil { + return nil, err + } + + gcm, err := NewAESGcm(wrapKey) + if err != nil { + return nil, fmt.Errorf("NewAESGcm failed: %w", err) + } + + plaintext, err := gcm.Decrypt(wrappedKey.EncryptedDEK) + if err != nil { + return nil, fmt.Errorf("AES-GCM decrypt failed: %w", err) + } + + return plaintext, nil +} + +func mlkem1024WrapDEK(publicKeyRaw, dek, salt, info []byte) ([]byte, error) { + sharedSecret, ciphertext, err := MLKEM1024Encapsulate(publicKeyRaw) + if err != nil { + return nil, err + } + + wrapKey, err := deriveMLKEMWrapKey(sharedSecret, salt, info) + if err != nil { + return nil, err + } + + gcm, err := NewAESGcm(wrapKey) + if err != nil { + return nil, fmt.Errorf("NewAESGcm failed: %w", err) + } + + encryptedDEK, err := gcm.Encrypt(dek) + if err != nil { + return nil, fmt.Errorf("AES-GCM encrypt failed: %w", err) + } + + wrappedDER, err := asn1.Marshal(MLKEMWrappedKey{ + MLKEMCiphertext: ciphertext, + EncryptedDEK: encryptedDEK, + }) + if err != nil { + return nil, fmt.Errorf("asn1.Marshal failed: %w", err) + } + + return wrappedDER, nil +} + +func mlkem1024UnwrapDEK(privateKeyRaw, wrappedDER, salt, info []byte) ([]byte, error) { + if len(privateKeyRaw) != MLKEM1024PrivateKeySize { + return nil, fmt.Errorf("invalid ML-KEM-1024 private key size: got %d want %d", len(privateKeyRaw), MLKEM1024PrivateKeySize) + } + + var wrappedKey MLKEMWrappedKey + rest, err := asn1.Unmarshal(wrappedDER, &wrappedKey) + if err != nil { + return nil, fmt.Errorf("asn1.Unmarshal failed: %w", err) + } + if len(rest) != 0 { + return nil, fmt.Errorf("asn1.Unmarshal left %d trailing bytes", len(rest)) + } + if len(wrappedKey.MLKEMCiphertext) != MLKEM1024CiphertextSize { + return nil, fmt.Errorf("invalid ML-KEM-1024 ciphertext size: got %d want %d", len(wrappedKey.MLKEMCiphertext), MLKEM1024CiphertextSize) + } + + dk, err := mlkem.NewDecapsulationKey1024(privateKeyRaw) + if err != nil { + return nil, fmt.Errorf("mlkem.NewDecapsulationKey1024 failed: %w", err) + } + + sharedSecret, err := dk.Decapsulate(wrappedKey.MLKEMCiphertext) + if err != nil { + return nil, fmt.Errorf("mlkem1024 decapsulate failed: %w", err) + } + + wrapKey, err := deriveMLKEMWrapKey(sharedSecret, salt, info) + if err != nil { + return nil, err + } + + gcm, err := NewAESGcm(wrapKey) + if err != nil { + return nil, fmt.Errorf("NewAESGcm failed: %w", err) + } + + plaintext, err := gcm.Decrypt(wrappedKey.EncryptedDEK) + if err != nil { + return nil, fmt.Errorf("AES-GCM decrypt failed: %w", err) + } + + return plaintext, nil +} + +func deriveMLKEMWrapKey(sharedSecret, salt, info []byte) ([]byte, error) { + if len(salt) == 0 { + salt = defaultTDFSalt() + } + + hkdfObj := hkdf.New(sha256.New, sharedSecret, salt, info) + derivedKey := make([]byte, mlkemWrapKeySize) + if _, err := io.ReadFull(hkdfObj, derivedKey); err != nil { + return nil, fmt.Errorf("hkdf failure: %w", err) + } + + return derivedKey, nil +} diff --git a/lib/ocrypto/mlkem_test.go b/lib/ocrypto/mlkem_test.go new file mode 100644 index 0000000000..5450d920e7 --- /dev/null +++ b/lib/ocrypto/mlkem_test.go @@ -0,0 +1,222 @@ +package ocrypto + +import ( + "encoding/asn1" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMLKEM768WrapUnwrapRoundTrip(t *testing.T) { + keyPair, err := NewMLKEMKeyPair() + require.NoError(t, err) + + publicKeyBytes := keyPair.PrivateKey.EncapsulationKey().Bytes() + privateKeyBytes := keyPair.PrivateKey.Bytes() + + dek := []byte("0123456789abcdef0123456789abcdef") + wrapped, err := MLKEM768WrapDEK(publicKeyBytes, dek) + require.NoError(t, err) + + plaintext, err := MLKEM768UnwrapDEK(privateKeyBytes, wrapped) + require.NoError(t, err) + assert.Equal(t, dek, plaintext) +} + +func TestMLKEM1024WrapUnwrapRoundTrip(t *testing.T) { + keyPair, err := NewMLKEM1024KeyPair() + require.NoError(t, err) + + publicKeyBytes := keyPair.PrivateKey.EncapsulationKey().Bytes() + privateKeyBytes := keyPair.PrivateKey.Bytes() + + dek := []byte("0123456789abcdef0123456789abcdef") + wrapped, err := MLKEM1024WrapDEK(publicKeyBytes, dek) + require.NoError(t, err) + + plaintext, err := MLKEM1024UnwrapDEK(privateKeyBytes, wrapped) + require.NoError(t, err) + assert.Equal(t, dek, plaintext) +} + +func TestMLKEM768WrapUnwrapWrongKeyFails(t *testing.T) { + keyPair1, err := NewMLKEMKeyPair() + require.NoError(t, err) + keyPair2, err := NewMLKEMKeyPair() + require.NoError(t, err) + + publicKey1 := keyPair1.PrivateKey.EncapsulationKey().Bytes() + privateKey2 := keyPair2.PrivateKey.Bytes() + + wrapped, err := MLKEM768WrapDEK(publicKey1, []byte("top secret dek")) + require.NoError(t, err) + + _, err = MLKEM768UnwrapDEK(privateKey2, wrapped) + require.Error(t, err) + assert.Contains(t, err.Error(), "AES-GCM decrypt failed") +} + +func TestMLKEM1024WrapUnwrapWrongKeyFails(t *testing.T) { + keyPair1, err := NewMLKEM1024KeyPair() + require.NoError(t, err) + keyPair2, err := NewMLKEM1024KeyPair() + require.NoError(t, err) + + publicKey1 := keyPair1.PrivateKey.EncapsulationKey().Bytes() + privateKey2 := keyPair2.PrivateKey.Bytes() + + wrapped, err := MLKEM1024WrapDEK(publicKey1, []byte("top secret dek")) + require.NoError(t, err) + + _, err = MLKEM1024UnwrapDEK(privateKey2, wrapped) + require.Error(t, err) + assert.Contains(t, err.Error(), "AES-GCM decrypt failed") +} + +func TestMLKEMWrappedKeyASN1RoundTrip(t *testing.T) { + original := MLKEMWrappedKey{ + MLKEMCiphertext: []byte("ciphertext"), + EncryptedDEK: []byte("encrypted-dek"), + } + + der, err := asn1.Marshal(original) + require.NoError(t, err) + + var decoded MLKEMWrappedKey + rest, err := asn1.Unmarshal(der, &decoded) + require.NoError(t, err) + assert.Empty(t, rest) + assert.Equal(t, original, decoded) +} + +func TestMLKEM768CiphertextSizeValidation(t *testing.T) { + keyPair, err := NewMLKEMKeyPair() + require.NoError(t, err) + + privateKeyBytes := keyPair.PrivateKey.Bytes() + + invalidWrapped := MLKEMWrappedKey{ + MLKEMCiphertext: []byte("too-short"), + EncryptedDEK: []byte("encrypted-dek"), + } + + der, err := asn1.Marshal(invalidWrapped) + require.NoError(t, err) + + _, err = MLKEM768UnwrapDEK(privateKeyBytes, der) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid ML-KEM-768 ciphertext size") +} + +func TestMLKEM1024CiphertextSizeValidation(t *testing.T) { + keyPair, err := NewMLKEM1024KeyPair() + require.NoError(t, err) + + privateKeyBytes := keyPair.PrivateKey.Bytes() + + invalidWrapped := MLKEMWrappedKey{ + MLKEMCiphertext: []byte("too-short"), + EncryptedDEK: []byte("encrypted-dek"), + } + + der, err := asn1.Marshal(invalidWrapped) + require.NoError(t, err) + + _, err = MLKEM1024UnwrapDEK(privateKeyBytes, der) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid ML-KEM-1024 ciphertext size") +} + +func TestMLKEMCustomSaltInfo(t *testing.T) { + keyPair, err := NewMLKEMKeyPair() + require.NoError(t, err) + + publicKeyBytes := keyPair.PrivateKey.EncapsulationKey().Bytes() + privateKeyBytes := keyPair.PrivateKey.Bytes() + + customSalt := []byte("custom-salt-value") + customInfo := []byte("custom-info-value") + + encryptor, err := NewMLKEM768Encryptor(publicKeyBytes, customSalt, customInfo) + require.NoError(t, err) + + decryptor, err := NewSaltedMLKEM768Decryptor(privateKeyBytes, customSalt, customInfo) + require.NoError(t, err) + + dek := []byte("test-dek-value-123456") + wrapped, err := encryptor.Encrypt(dek) + require.NoError(t, err) + + plaintext, err := decryptor.Decrypt(wrapped) + require.NoError(t, err) + assert.Equal(t, dek, plaintext) +} + +func TestMLKEMEncryptorImplementsInterface(t *testing.T) { + keyPair, err := NewMLKEMKeyPair() + require.NoError(t, err) + + publicKeyBytes := keyPair.PrivateKey.EncapsulationKey().Bytes() + + encryptor, err := NewMLKEM768Encryptor(publicKeyBytes, nil, nil) + require.NoError(t, err) + + assert.Equal(t, MLKEM, encryptor.Type()) + assert.Equal(t, MLKEM768Key, encryptor.KeyType()) + assert.Nil(t, encryptor.EphemeralKey()) + + metadata, err := encryptor.Metadata() + require.NoError(t, err) + assert.Empty(t, metadata) +} + +func TestMLKEMDecryptorImplementsInterface(t *testing.T) { + keyPair, err := NewMLKEMKeyPair() + require.NoError(t, err) + + privateKeyBytes := keyPair.PrivateKey.Bytes() + + decryptor, err := NewMLKEM768Decryptor(privateKeyBytes) + require.NoError(t, err) + + _, err = decryptor.DecryptWithEphemeralKey([]byte("data"), []byte("ephemeral")) + require.Error(t, err) + assert.Contains(t, err.Error(), "ephemeral key should not be provided") +} + +func TestMLKEM768Encapsulate(t *testing.T) { + keyPair, err := NewMLKEMKeyPair() + require.NoError(t, err) + + publicKeyBytes := keyPair.PrivateKey.EncapsulationKey().Bytes() + + sharedSecret, ciphertext, err := MLKEM768Encapsulate(publicKeyBytes) + require.NoError(t, err) + assert.Len(t, sharedSecret, 32) + assert.Len(t, ciphertext, MLKEM768CiphertextSize) +} + +func TestMLKEM1024Encapsulate(t *testing.T) { + keyPair, err := NewMLKEM1024KeyPair() + require.NoError(t, err) + + publicKeyBytes := keyPair.PrivateKey.EncapsulationKey().Bytes() + + sharedSecret, ciphertext, err := MLKEM1024Encapsulate(publicKeyBytes) + require.NoError(t, err) + assert.Len(t, sharedSecret, 32) + assert.Len(t, ciphertext, MLKEM1024CiphertextSize) +} + +func TestMLKEM768EncapsulateInvalidKeySize(t *testing.T) { + _, _, err := MLKEM768Encapsulate([]byte("too-short")) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid ML-KEM-768 public key size") +} + +func TestMLKEM1024EncapsulateInvalidKeySize(t *testing.T) { + _, _, err := MLKEM1024Encapsulate([]byte("too-short")) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid ML-KEM-1024 public key size") +} diff --git a/otdfctl/docs/man/policy/kas-registry/key/create.md b/otdfctl/docs/man/policy/kas-registry/key/create.md index 76f5a85af9..166f14dd93 100644 --- a/otdfctl/docs/man/policy/kas-registry/key/create.md +++ b/otdfctl/docs/man/policy/kas-registry/key/create.md @@ -75,6 +75,8 @@ otdfctl policy kas-registry key create --key-id "aws-key" --algorithm "rsa:2048" | `ec:secp256r1` | | `ec:secp384r1` | | `ec:secp521r1` | + | `mlkem:768` | + | `mlkem:1024` | | `hpqt:xwing` | | `hpqt:secp256r1-mlkem768` | | `hpqt:secp384r1-mlkem1024` | diff --git a/otdfctl/docs/man/policy/kas-registry/key/import.md b/otdfctl/docs/man/policy/kas-registry/key/import.md index b08a2ea843..1c209271f6 100644 --- a/otdfctl/docs/man/policy/kas-registry/key/import.md +++ b/otdfctl/docs/man/policy/kas-registry/key/import.md @@ -79,6 +79,8 @@ otdfctl policy kas-registry key import --key-id "imported-key" --algorithm "rsa: | `ec:secp256r1` | | `ec:secp384r1` | | `ec:secp521r1` | + | `mlkem:768` | + | `mlkem:1024` | | `hpqt:xwing` | | `hpqt:secp256r1-mlkem768` | | `hpqt:secp384r1-mlkem1024` | diff --git a/otdfctl/docs/man/policy/kas-registry/key/rotate.md b/otdfctl/docs/man/policy/kas-registry/key/rotate.md index 726d85f74b..29a94bf53e 100644 --- a/otdfctl/docs/man/policy/kas-registry/key/rotate.md +++ b/otdfctl/docs/man/policy/kas-registry/key/rotate.md @@ -88,6 +88,8 @@ otdfctl policy kas-registry key rotate --key "public-key-old" --kas "Secondary K | `ec:secp256r1` | | `ec:secp384r1` | | `ec:secp521r1` | + | `mlkem:768` | + | `mlkem:1024` | | `hpqt:xwing` | | `hpqt:secp256r1-mlkem768` | | `hpqt:secp384r1-mlkem1024` | From 2ef41a8547f8b3e6ab747a1487cfaf2cd6f6bf7d Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Thu, 28 May 2026 08:35:01 -0400 Subject: [PATCH 10/25] refactor(ocrypto): remove DecryptWithEphemeralKey from interface and Legacy ML-KEM types Remove DecryptWithEphemeralKey method from PrivateKeyDecryptor interface, making it EC-specific. Only ECDecryptor needs ephemeral keys for ECDH. Remove Legacy ML-KEM types and implementations: - MLKEMEncryptor768Legacy, MLKEMEncryptor1024Legacy - MLKEMDecryptor768Legacy, MLKEMDecryptor1024Legacy - Old "MLKEM DECAPSULATION KEY" PEM handling - Helper functions newMLKEM768(), newMLKEM1024() Add PEM block constants for new ML-KEM implementation: - PEMBlockMLKEM768PublicKey, PEMBlockMLKEM768PrivateKey - PEMBlockMLKEM1024PublicKey, PEMBlockMLKEM1024PrivateKey Remove DecryptWithEphemeralKey from all non-EC decryptors: - MLKEMDecryptor768, MLKEMDecryptor1024 - XWingDecryptor, HybridNISTDecryptor - AsymDecryption (RSA) Update service layer to use Decrypt() for ML-KEM instead of DecryptWithEphemeralKey. EC decryption continues to use the EC-specific DecryptWithEphemeralKey method. Breaking changes: - Old ML-KEM PEM format ("MLKEM DECAPSULATION KEY") no longer supported - Callers must use concrete ECDecryptor type for DecryptWithEphemeralKey Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Dave Mihalcik --- lib/ocrypto/asym_decryption.go | 117 +------------------ lib/ocrypto/asym_encryption.go | 117 +------------------ lib/ocrypto/hybrid_nist.go | 8 -- lib/ocrypto/mlkem.go | 20 +--- lib/ocrypto/mlkem_test.go | 14 --- lib/ocrypto/xwing.go | 8 -- service/internal/security/basic_manager.go | 5 +- service/internal/security/standard_crypto.go | 6 +- 8 files changed, 20 insertions(+), 275 deletions(-) diff --git a/lib/ocrypto/asym_decryption.go b/lib/ocrypto/asym_decryption.go index 3f10f520cb..6656c92be9 100644 --- a/lib/ocrypto/asym_decryption.go +++ b/lib/ocrypto/asym_decryption.go @@ -7,7 +7,6 @@ import ( "crypto/ecdh" "crypto/ecdsa" "crypto/elliptic" - "crypto/mlkem" "crypto/rsa" "crypto/sha256" "crypto/x509" @@ -27,17 +26,6 @@ type AsymDecryption struct { type PrivateKeyDecryptor interface { // Decrypt decrypts ciphertext with private key. Decrypt(data []byte) ([]byte, error) - - // DecryptWithEphemeralKey decrypts ciphertext using additional sender material. - DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) -} - -type MLKEMDecryptor768Legacy struct { - decap *mlkem.DecapsulationKey768 -} - -type MLKEMDecryptor1024Legacy struct { - decap *mlkem.DecapsulationKey1024 } // FromPrivatePEM creates and returns a new AsymDecryption. @@ -62,18 +50,10 @@ func FromPrivatePEMWithSalt(privateKeyInPem string, salt, info []byte) (PrivateK return NewSaltedP256MLKEM768Decryptor(block.Bytes, salt, info) case PEMBlockP384MLKEM1024PrivateKey: return NewSaltedP384MLKEM1024Decryptor(block.Bytes, salt, info) - } - - if block.Type == "MLKEM DECAPSULATION KEY" { - decap768, err := mlkem.NewDecapsulationKey768(block.Bytes) - if err == nil { - return &MLKEMDecryptor768Legacy{decap: decap768}, nil - } - decap1024, err1024 := mlkem.NewDecapsulationKey1024(block.Bytes) - if err1024 != nil { - return nil, fmt.Errorf("mlkem.NewDecapsulationKey1024 failed after mlkem.NewDecapsulationKey768 failed: %w / %w", err, err1024) - } - return &MLKEMDecryptor1024Legacy{decap: decap1024}, nil + case PEMBlockMLKEM768PrivateKey: + return NewSaltedMLKEM768Decryptor(block.Bytes, salt, info) + case PEMBlockMLKEM1024PrivateKey: + return NewSaltedMLKEM1024Decryptor(block.Bytes, salt, info) } priv, err := x509.ParsePKCS8PrivateKey(block.Bytes) @@ -141,13 +121,6 @@ func (asymDecryption AsymDecryption) Decrypt(data []byte) ([]byte, error) { return bytes, nil } -func (asymDecryption AsymDecryption) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) { - if len(ephemeral) > 0 { - return nil, errors.New("ephemeral key is not supported for RSA decryption") - } - return asymDecryption.Decrypt(data) -} - type ECDecryptor struct { sk *ecdh.PrivateKey salt []byte @@ -245,85 +218,3 @@ func convCurve(c ecdh.Curve) elliptic.Curve { return nil } } - -func (d MLKEMDecryptor768Legacy) Decrypt(_ []byte) ([]byte, error) { - return nil, errors.New("ciphertext encapsulation is required for ML-KEM decryption") -} - -func (d MLKEMDecryptor1024Legacy) Decrypt(_ []byte) ([]byte, error) { - return nil, errors.New("ciphertext encapsulation is required for ML-KEM decryption") -} - -func (d MLKEMDecryptor768Legacy) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) { - if d.decap == nil { - return nil, errors.New("mlkem decapsulation key is nil") - } - if len(ephemeral) == 0 { - return nil, errors.New("ciphertext encapsulation is required for ML-KEM decryption") - } - - sharedSecret, err := d.decap.Decapsulate(ephemeral) - if err != nil { - return nil, fmt.Errorf("mlkem.Decapsulate failed: %w", err) - } - - block, err := aes.NewCipher(sharedSecret) - if err != nil { - return nil, fmt.Errorf("aes.NewCipher failure: %w", err) - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, fmt.Errorf("cipher.NewGCM failure: %w", err) - } - - nonceSize := gcm.NonceSize() - if len(data) < nonceSize { - return nil, errors.New("ciphertext too short") - } - - nonce, ciphertext := data[:nonceSize], data[nonceSize:] - plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) - if err != nil { - return nil, fmt.Errorf("gcm.Open failure: %w", err) - } - - return plaintext, nil -} - -func (d MLKEMDecryptor1024Legacy) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) { - if d.decap == nil { - return nil, errors.New("mlkem decapsulation key is nil") - } - if len(ephemeral) == 0 { - return nil, errors.New("ciphertext encapsulation is required for ML-KEM decryption") - } - - sharedSecret, err := d.decap.Decapsulate(ephemeral) - if err != nil { - return nil, fmt.Errorf("mlkem.Decapsulate failed: %w", err) - } - - block, err := aes.NewCipher(sharedSecret) - if err != nil { - return nil, fmt.Errorf("aes.NewCipher failure: %w", err) - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, fmt.Errorf("cipher.NewGCM failure: %w", err) - } - - nonceSize := gcm.NonceSize() - if len(data) < nonceSize { - return nil, errors.New("ciphertext too short") - } - - nonce, ciphertext := data[:nonceSize], data[nonceSize:] - plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) - if err != nil { - return nil, fmt.Errorf("gcm.Open failure: %w", err) - } - - return plaintext, nil -} diff --git a/lib/ocrypto/asym_encryption.go b/lib/ocrypto/asym_encryption.go index 417dc41b62..e3529084b3 100644 --- a/lib/ocrypto/asym_encryption.go +++ b/lib/ocrypto/asym_encryption.go @@ -5,7 +5,6 @@ import ( "crypto/cipher" "crypto/ecdh" "crypto/ecdsa" - "crypto/mlkem" "crypto/rand" "crypto/rsa" "crypto/sha1" //nolint:gosec // used for padding which is safe @@ -62,18 +61,6 @@ type ECEncryptor struct { info []byte } -type MLKEMEncryptor768Legacy struct { - pub *mlkem.EncapsulationKey768 - cipherText []byte - sharedSecret []byte -} - -type MLKEMEncryptor1024Legacy struct { - pub *mlkem.EncapsulationKey1024 - cipherText []byte - sharedSecret []byte -} - func FromPublicPEM(publicKeyInPem string) (PublicKeyEncryptor, error) { // TK Move salt and info out of library, into API option functions digest := sha256.New() @@ -95,6 +82,10 @@ func FromPublicPEMWithSalt(publicKeyInPem string, salt, info []byte) (PublicKeyE return NewP256MLKEM768Encryptor(block.Bytes, salt, info) case PEMBlockP384MLKEM1024PublicKey: return NewP384MLKEM1024Encryptor(block.Bytes, salt, info) + case PEMBlockMLKEM768PublicKey: + return NewMLKEM768Encryptor(block.Bytes, salt, info) + case PEMBlockMLKEM1024PublicKey: + return NewMLKEM1024Encryptor(block.Bytes, salt, info) } pub, err := getPublicPart(publicKeyInPem) @@ -113,10 +104,6 @@ func FromPublicPEMWithSalt(publicKeyInPem string, salt, info []byte) (PublicKeyE return newECIES(e, salt, info) case *ecdh.PublicKey: return newECIES(pub, salt, info) - case *mlkem.EncapsulationKey768: - return newMLKEM768(pub), nil - case *mlkem.EncapsulationKey1024: - return newMLKEM1024(pub), nil default: break } @@ -129,16 +116,6 @@ func newECIES(pub *ecdh.PublicKey, salt, info []byte) (ECEncryptor, error) { return ECEncryptor{pub, ek, salt, info}, err } -func newMLKEM768(pub *mlkem.EncapsulationKey768) *MLKEMEncryptor768Legacy { - sharedSecret, cipherText := pub.Encapsulate() - return &MLKEMEncryptor768Legacy{pub: pub, cipherText: cipherText, sharedSecret: sharedSecret} -} - -func newMLKEM1024(pub *mlkem.EncapsulationKey1024) *MLKEMEncryptor1024Legacy { - sharedSecret, cipherText := pub.Encapsulate() - return &MLKEMEncryptor1024Legacy{pub: pub, cipherText: cipherText, sharedSecret: sharedSecret} -} - // NewAsymEncryption creates and returns a new AsymEncryption. // // Deprecated: Use FromPublicPEM instead. @@ -310,89 +287,3 @@ func (e ECEncryptor) Encrypt(data []byte) ([]byte, error) { func (e ECEncryptor) PublicKeyInPemFormat() (string, error) { return publicKeyInPemFormat(e.ek.Public()) } - -// MLKEMEncryptor768 methods - -func (e MLKEMEncryptor768Legacy) Type() SchemeType { - return MLKEM -} - -func (e MLKEMEncryptor768Legacy) KeyType() KeyType { - return MLKEM768Key -} - -func (e MLKEMEncryptor768Legacy) EphemeralKey() []byte { - return e.cipherText -} - -func (e MLKEMEncryptor768Legacy) Metadata() (map[string]string, error) { - return map[string]string{ - "ephemeralKey": string(e.cipherText), - }, nil -} - -func (e MLKEMEncryptor768Legacy) Encrypt(data []byte) ([]byte, error) { - block, err := aes.NewCipher(e.sharedSecret) - if err != nil { - return nil, fmt.Errorf("aes.NewCipher failed: %w", err) - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, fmt.Errorf("cipher.NewGCM failed: %w", err) - } - - nonce := make([]byte, gcm.NonceSize()) - if _, err := io.ReadFull(rand.Reader, nonce); err != nil { - return nil, fmt.Errorf("nonce generation failed: %w", err) - } - - return gcm.Seal(nonce, nonce, data, nil), nil -} - -func (e MLKEMEncryptor768Legacy) PublicKeyInPemFormat() (string, error) { - return "", errors.New("public key PEM not available for ML-KEM encryptor") -} - -// MLKEMEncryptor1024 methods - -func (e MLKEMEncryptor1024Legacy) Type() SchemeType { - return MLKEM -} - -func (e MLKEMEncryptor1024Legacy) KeyType() KeyType { - return MLKEM1024Key -} - -func (e MLKEMEncryptor1024Legacy) EphemeralKey() []byte { - return e.cipherText -} - -func (e MLKEMEncryptor1024Legacy) Metadata() (map[string]string, error) { - return map[string]string{ - "ephemeralKey": string(e.cipherText), - }, nil -} - -func (e MLKEMEncryptor1024Legacy) Encrypt(data []byte) ([]byte, error) { - block, err := aes.NewCipher(e.sharedSecret) - if err != nil { - return nil, fmt.Errorf("aes.NewCipher failed: %w", err) - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, fmt.Errorf("cipher.NewGCM failed: %w", err) - } - - nonce := make([]byte, gcm.NonceSize()) - if _, err := io.ReadFull(rand.Reader, nonce); err != nil { - return nil, fmt.Errorf("nonce generation failed: %w", err) - } - - return gcm.Seal(nonce, nonce, data, nil), nil -} - -func (e MLKEMEncryptor1024Legacy) PublicKeyInPemFormat() (string, error) { - return "", errors.New("public key PEM not available for ML-KEM encryptor") -} diff --git a/lib/ocrypto/hybrid_nist.go b/lib/ocrypto/hybrid_nist.go index 223100de70..ee0a575ac5 100644 --- a/lib/ocrypto/hybrid_nist.go +++ b/lib/ocrypto/hybrid_nist.go @@ -6,7 +6,6 @@ import ( "crypto/rand" "crypto/sha256" "encoding/asn1" - "errors" "fmt" "io" @@ -518,10 +517,3 @@ func deriveHybridNISTWrapKey(combinedSecret, salt, info []byte) ([]byte, error) return derivedKey, nil } - -func (d *HybridNISTDecryptor) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) { - if len(ephemeral) > 0 { - return nil, errors.New("ephemeral key should not be provided for hybrid NIST decryption") - } - return d.Decrypt(data) -} diff --git a/lib/ocrypto/mlkem.go b/lib/ocrypto/mlkem.go index e64288dfa3..3df647aaa1 100644 --- a/lib/ocrypto/mlkem.go +++ b/lib/ocrypto/mlkem.go @@ -5,7 +5,6 @@ import ( "crypto/sha256" "encoding/asn1" "encoding/pem" - "errors" "fmt" "io" @@ -21,6 +20,11 @@ const ( MLKEM1024CiphertextSize = 1568 // mlkem1024 ciphertext mlkemWrapKeySize = 32 // AES-256 key size for wrap key derivation + + PEMBlockMLKEM768PublicKey = "MLKEM768 PUBLIC KEY" + PEMBlockMLKEM768PrivateKey = "MLKEM768 PRIVATE KEY" + PEMBlockMLKEM1024PublicKey = "MLKEM1024 PUBLIC KEY" + PEMBlockMLKEM1024PrivateKey = "MLKEM1024 PRIVATE KEY" ) type MLKEMWrappedKey struct { @@ -112,13 +116,6 @@ func (d *MLKEMDecryptor768) Decrypt(data []byte) ([]byte, error) { return mlkem768UnwrapDEK(d.privateKey, data, d.salt, d.info) } -func (d *MLKEMDecryptor768) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) { - if len(ephemeral) > 0 { - return nil, errors.New("ephemeral key should not be provided for ML-KEM-768 decryption") - } - return d.Decrypt(data) -} - func NewMLKEM1024Encryptor(publicKey, salt, info []byte) (*MLKEMEncryptor1024, error) { if len(publicKey) != MLKEM1024PublicKeySize { return nil, fmt.Errorf("invalid ML-KEM-1024 public key size: got %d want %d", len(publicKey), MLKEM1024PublicKeySize) @@ -179,13 +176,6 @@ func (d *MLKEMDecryptor1024) Decrypt(data []byte) ([]byte, error) { return mlkem1024UnwrapDEK(d.privateKey, data, d.salt, d.info) } -func (d *MLKEMDecryptor1024) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) { - if len(ephemeral) > 0 { - return nil, errors.New("ephemeral key should not be provided for ML-KEM-1024 decryption") - } - return d.Decrypt(data) -} - func MLKEM768WrapDEK(publicKeyRaw, dek []byte) ([]byte, error) { return mlkem768WrapDEK(publicKeyRaw, dek, defaultTDFSalt(), nil) } diff --git a/lib/ocrypto/mlkem_test.go b/lib/ocrypto/mlkem_test.go index 5450d920e7..bc85f72dad 100644 --- a/lib/ocrypto/mlkem_test.go +++ b/lib/ocrypto/mlkem_test.go @@ -171,20 +171,6 @@ func TestMLKEMEncryptorImplementsInterface(t *testing.T) { assert.Empty(t, metadata) } -func TestMLKEMDecryptorImplementsInterface(t *testing.T) { - keyPair, err := NewMLKEMKeyPair() - require.NoError(t, err) - - privateKeyBytes := keyPair.PrivateKey.Bytes() - - decryptor, err := NewMLKEM768Decryptor(privateKeyBytes) - require.NoError(t, err) - - _, err = decryptor.DecryptWithEphemeralKey([]byte("data"), []byte("ephemeral")) - require.Error(t, err) - assert.Contains(t, err.Error(), "ephemeral key should not be provided") -} - func TestMLKEM768Encapsulate(t *testing.T) { keyPair, err := NewMLKEMKeyPair() require.NoError(t, err) diff --git a/lib/ocrypto/xwing.go b/lib/ocrypto/xwing.go index 1c4a57a89c..1ea9d5ab25 100644 --- a/lib/ocrypto/xwing.go +++ b/lib/ocrypto/xwing.go @@ -5,7 +5,6 @@ import ( "crypto/sha256" "encoding/asn1" "encoding/pem" - "errors" "fmt" "io" @@ -259,10 +258,3 @@ func decodeSizedPEMBlock(data []byte, blockType string, expectedSize int) ([]byt return append([]byte(nil), block.Bytes...), nil } - -func (d *XWingDecryptor) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) { - if len(ephemeral) > 0 { - return nil, errors.New("ephemeral key should not be provided for X-Wing decryption") - } - return d.Decrypt(data) -} diff --git a/service/internal/security/basic_manager.go b/service/internal/security/basic_manager.go index 4e5f303fd3..9f6e6caaff 100644 --- a/service/internal/security/basic_manager.go +++ b/service/internal/security/basic_manager.go @@ -163,7 +163,10 @@ func (b *BasicManager) Decrypt(ctx context.Context, keyDetails trust.KeyDetails, } return protectedKey, nil case ocrypto.MLKEM768Key, ocrypto.MLKEM1024Key: - plaintext, err := decrypter.DecryptWithEphemeralKey(ciphertext, ephemeralPublicKey) + if len(ephemeralPublicKey) > 0 { + return nil, errors.New("ephemeral public key should not be provided for ML-KEM decryption") + } + plaintext, err := decrypter.Decrypt(ciphertext) if err != nil { return nil, fmt.Errorf("failed to decrypt with ML-KEM: %w", err) } diff --git a/service/internal/security/standard_crypto.go b/service/internal/security/standard_crypto.go index 5752331fd0..74cdaf47c9 100644 --- a/service/internal/security/standard_crypto.go +++ b/service/internal/security/standard_crypto.go @@ -570,8 +570,8 @@ func (s *StandardCrypto) Decrypt(_ context.Context, keyID trust.KeyIdentifier, c } case StandardMLKEMCrypto: - if len(ephemeralPublicKey) == 0 { - return nil, errors.New("ephemeral public key (ciphertext) is required for ML-KEM decryption") + if len(ephemeralPublicKey) > 0 { + return nil, errors.New("ephemeral public key should not be provided for ML-KEM decryption") } decryptor, err := ocrypto.FromPrivatePEM(key.mlkemPrivateKeyPem) @@ -579,7 +579,7 @@ func (s *StandardCrypto) Decrypt(_ context.Context, keyID trust.KeyIdentifier, c return nil, fmt.Errorf("failed to create ML-KEM decryptor from PEM: %w", err) } - rawKey, err = decryptor.DecryptWithEphemeralKey(ciphertext, ephemeralPublicKey) + rawKey, err = decryptor.Decrypt(ciphertext) if err != nil { return nil, fmt.Errorf("failed to decrypt with ML-KEM: %w", err) } From a04cadbbd9ecf1f87094b90aea221707117419bb Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Thu, 28 May 2026 08:46:55 -0400 Subject: [PATCH 11/25] fix(security): handle StandardMLKEMCrypto in determineKeyType ML-KEM keys loaded through InProcessProvider failed FindKeyByID and Decrypt with "could not determine key type" because the type switch in determineKeyType was missing the StandardMLKEMCrypto case. Add the case and a regression test exercising both mlkem:768 and mlkem:1024 through determineKeyType and FindKeyByID. Co-Authored-By: Claude Opus 4.7 Signed-off-by: Dave Mihalcik --- .../internal/security/in_process_provider.go | 2 + .../security/in_process_provider_test.go | 23 ++++++++ .../internal/security/test_helpers_test.go | 57 +++++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/service/internal/security/in_process_provider.go b/service/internal/security/in_process_provider.go index 43cb2c3a2c..0706038330 100644 --- a/service/internal/security/in_process_provider.go +++ b/service/internal/security/in_process_provider.go @@ -372,6 +372,8 @@ func (a *InProcessProvider) determineKeyType(kid string) (string, error) { return key.Algorithm, nil case StandardHybridCrypto: return key.Algorithm, nil + case StandardMLKEMCrypto: + return key.Algorithm, nil } return "", errors.New("could not determine key type") diff --git a/service/internal/security/in_process_provider_test.go b/service/internal/security/in_process_provider_test.go index 9870ad53bb..277a4348ea 100644 --- a/service/internal/security/in_process_provider_test.go +++ b/service/internal/security/in_process_provider_test.go @@ -229,3 +229,26 @@ func TestInProcessProviderDetermineKeyType(t *testing.T) { _, err = provider.determineKeyType("missing") require.Error(t, err) } + +func TestInProcessProviderDetermineKeyTypeMLKEM(t *testing.T) { + cryptoProvider, material := newStandardCryptoWithMLKEMForTest(t) + providerIface := NewSecurityProviderAdapter(cryptoProvider, nil, nil) + provider, ok := providerIface.(*InProcessProvider) + require.True(t, ok) + + keyType, err := provider.determineKeyType(material.mlkem768Kid) + require.NoError(t, err) + assert.Equal(t, AlgorithmMLKEM768, keyType) + + keyType, err = provider.determineKeyType(material.mlkem1024Kid) + require.NoError(t, err) + assert.Equal(t, AlgorithmMLKEM1024, keyType) + + details, err := provider.FindKeyByID(t.Context(), trust.KeyIdentifier(material.mlkem768Kid)) + require.NoError(t, err) + assert.Equal(t, ocrypto.KeyType(AlgorithmMLKEM768), details.Algorithm()) + + details, err = provider.FindKeyByID(t.Context(), trust.KeyIdentifier(material.mlkem1024Kid)) + require.NoError(t, err) + assert.Equal(t, ocrypto.KeyType(AlgorithmMLKEM1024), details.Algorithm()) +} diff --git a/service/internal/security/test_helpers_test.go b/service/internal/security/test_helpers_test.go index f3736529b7..f15144a1f1 100644 --- a/service/internal/security/test_helpers_test.go +++ b/service/internal/security/test_helpers_test.go @@ -17,6 +17,14 @@ type testKeyMaterial struct { ecKid string ecPrivatePEM string ecPublicPEM string + + mlkem768Kid string + mlkem768PrivatePEM string + mlkem768PublicPEM string + + mlkem1024Kid string + mlkem1024PrivatePEM string + mlkem1024PublicPEM string } func writeTempFile(t *testing.T, dir, name, contents string) string { @@ -83,6 +91,55 @@ func newStandardCryptoForTest(t *testing.T, includeRSA, includeEC bool) (*Standa return crypto, material } +func newStandardCryptoWithMLKEMForTest(t *testing.T) (*StandardCrypto, testKeyMaterial) { + t.Helper() + + dir := t.TempDir() + var keys []KeyPairInfo + var material testKeyMaterial + + kp768, err := ocrypto.NewMLKEMKeyPair() + require.NoError(t, err) + mlkem768Private, err := kp768.PrivateKeyInPemFormat() + require.NoError(t, err) + mlkem768Public, err := kp768.PublicKeyInPemFormat() + require.NoError(t, err) + + material.mlkem768Kid = "mlkem768-test-key" + material.mlkem768PrivatePEM = mlkem768Private + material.mlkem768PublicPEM = mlkem768Public + + keys = append(keys, KeyPairInfo{ + Algorithm: AlgorithmMLKEM768, + KID: material.mlkem768Kid, + Private: writeTempFile(t, dir, "mlkem768-private.pem", mlkem768Private), + Certificate: writeTempFile(t, dir, "mlkem768-public.pem", mlkem768Public), + }) + + kp1024, err := ocrypto.NewMLKEM1024KeyPair() + require.NoError(t, err) + mlkem1024Private, err := kp1024.PrivateKeyInPemFormat() + require.NoError(t, err) + mlkem1024Public, err := kp1024.PublicKeyInPemFormat() + require.NoError(t, err) + + material.mlkem1024Kid = "mlkem1024-test-key" + material.mlkem1024PrivatePEM = mlkem1024Private + material.mlkem1024PublicPEM = mlkem1024Public + + keys = append(keys, KeyPairInfo{ + Algorithm: AlgorithmMLKEM1024, + KID: material.mlkem1024Kid, + Private: writeTempFile(t, dir, "mlkem1024-private.pem", mlkem1024Private), + Certificate: writeTempFile(t, dir, "mlkem1024-public.pem", mlkem1024Public), + }) + + crypto, err := NewStandardCrypto(StandardConfig{Keys: keys}) + require.NoError(t, err) + + return crypto, material +} + func exportProtectedKey(t *testing.T, key ocrypto.ProtectedKey) []byte { t.Helper() raw, err := (&noOpEncapsulator{}).Encapsulate(key) From 90532847416952c98f59b0ed78ee2ba8e0352c39 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Thu, 28 May 2026 11:47:02 -0400 Subject: [PATCH 12/25] perf(security): pre-parse ML-KEM private key in loadKey Cache the ML-KEM PrivateKeyDecryptor on StandardMLKEMCrypto during loadKey instead of re-parsing the PEM on every Decrypt call. Mirrors the existing RSA pattern. Also fixes a latent bug in ocrypto.MLKEMKeyPair PEM writers: they emitted "MLKEM DECAPSULATION KEY" / "MLKEM ENCAPSULATOR" block types, but commit 40d10ce3 removed parser support for those headers. Updated the writers to use the PEMBlockMLKEM{768,1024}{Private,Public}Key constants the parser now recognizes. Signed-off-by: Dave Mihalcik --- lib/ocrypto/ec_key_pair.go | 8 ++++---- service/internal/security/standard_crypto.go | 19 +++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/ocrypto/ec_key_pair.go b/lib/ocrypto/ec_key_pair.go index e0b9aefecd..ccac3d7dff 100644 --- a/lib/ocrypto/ec_key_pair.go +++ b/lib/ocrypto/ec_key_pair.go @@ -551,7 +551,7 @@ func (keyPair MLKEMKeyPair) PrivateKeyInPemFormat() (string, error) { privateKeyPEM := pem.EncodeToMemory( &pem.Block{ - Type: "MLKEM DECAPSULATION KEY", + Type: PEMBlockMLKEM768PrivateKey, Bytes: keyPair.PrivateKey.Bytes(), }, ) @@ -565,7 +565,7 @@ func (keyPair MLKEMKeyPair) PublicKeyInPemFormat() (string, error) { publicKeyPEM := pem.EncodeToMemory( &pem.Block{ - Type: "MLKEM ENCAPSULATOR", + Type: PEMBlockMLKEM768PublicKey, Bytes: keyPair.PrivateKey.EncapsulationKey().Bytes(), }, ) @@ -583,7 +583,7 @@ func (keyPair MLKEM1024KeyPair) PrivateKeyInPemFormat() (string, error) { privateKeyPEM := pem.EncodeToMemory( &pem.Block{ - Type: "MLKEM DECAPSULATION KEY", + Type: PEMBlockMLKEM1024PrivateKey, Bytes: keyPair.PrivateKey.Bytes(), }, ) @@ -597,7 +597,7 @@ func (keyPair MLKEM1024KeyPair) PublicKeyInPemFormat() (string, error) { publicKeyPEM := pem.EncodeToMemory( &pem.Block{ - Type: "MLKEM ENCAPSULATOR", + Type: PEMBlockMLKEM1024PublicKey, Bytes: keyPair.PrivateKey.EncapsulationKey().Bytes(), }, ) diff --git a/service/internal/security/standard_crypto.go b/service/internal/security/standard_crypto.go index 74cdaf47c9..6a574eac69 100644 --- a/service/internal/security/standard_crypto.go +++ b/service/internal/security/standard_crypto.go @@ -83,6 +83,7 @@ type StandardMLKEMCrypto struct { KeyPairInfo mlkemPrivateKeyPem string mlkemPublicKeyPem string + decryptor ocrypto.PrivateKeyDecryptor } // List of keys by identifier @@ -126,7 +127,8 @@ func loadKeys(ks []KeyPairInfo) (*StandardCrypto, error) { keysByAlg := make(map[string]keylist) keysByID := make(keylist) for _, k := range ks { - slog.Info("crypto cfg loading", + slog.Info( + "crypto cfg loading", slog.Any("id", k.KID), slog.Any("alg", k.Algorithm), ) @@ -181,10 +183,15 @@ func loadKey(k KeyPairInfo) (any, error) { hybridPublicKeyPem: string(certPEM), }, nil case AlgorithmMLKEM768, AlgorithmMLKEM1024: + decryptor, err := ocrypto.FromPrivatePEM(string(privatePEM)) + if err != nil { + return nil, fmt.Errorf("ocrypto.FromPrivatePEM (ML-KEM) failed: %w", err) + } return StandardMLKEMCrypto{ KeyPairInfo: k, mlkemPrivateKeyPem: string(privatePEM), mlkemPublicKeyPem: string(certPEM), + decryptor: decryptor, }, nil case AlgorithmRSA2048, AlgorithmRSA4096: asymDecryption, err := ocrypto.NewAsymDecryption(string(privatePEM)) @@ -259,7 +266,8 @@ func loadDeprecatedKeys(rsaKeys map[string]StandardKeyInfo, ecKeys map[string]St keysByID[id] = k } for id, kasInfo := range ecKeys { - slog.Info("cfg.ECKeys", + slog.Info( + "cfg.ECKeys", slog.String("id", id), slog.Any("kasInfo", kasInfo), ) @@ -574,12 +582,7 @@ func (s *StandardCrypto) Decrypt(_ context.Context, keyID trust.KeyIdentifier, c return nil, errors.New("ephemeral public key should not be provided for ML-KEM decryption") } - decryptor, err := ocrypto.FromPrivatePEM(key.mlkemPrivateKeyPem) - if err != nil { - return nil, fmt.Errorf("failed to create ML-KEM decryptor from PEM: %w", err) - } - - rawKey, err = decryptor.Decrypt(ciphertext) + rawKey, err = key.decryptor.Decrypt(ciphertext) if err != nil { return nil, fmt.Errorf("failed to decrypt with ML-KEM: %w", err) } From 168636b059a8be8f363dcaaef2d2dd04a037da8c Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Thu, 28 May 2026 11:48:15 -0400 Subject: [PATCH 13/25] feat(kasregistry): allow ML-KEM algorithms in ListKeysRequest filter The key_algorithm CEL validation on ListKeysRequest only accepted algorithm values 0-8, which excluded the post-quantum ML-KEM-768 (20) and ML-KEM-1024 (21) values. Filtering by these algorithms returned a validation error even though the server could store and serve them. RotateKeyRequest.NewKey already includes 20 and 21 in its allowed set. Signed-off-by: Dave Mihalcik --- .../key_access_server_registry.openapi.yaml | 2 +- .../key_access_server_registry.pb.go | 847 +++++++++--------- .../key_access_server_registry.proto | 2 +- 3 files changed, 426 insertions(+), 425 deletions(-) diff --git a/docs/openapi/policy/kasregistry/key_access_server_registry.openapi.yaml b/docs/openapi/policy/kasregistry/key_access_server_registry.openapi.yaml index 2229b977e4..b8c7b84cad 100644 --- a/docs/openapi/policy/kasregistry/key_access_server_registry.openapi.yaml +++ b/docs/openapi/policy/kasregistry/key_access_server_registry.openapi.yaml @@ -1746,7 +1746,7 @@ components: Filter keys by algorithm The key_algorithm must be one of the defined values.: ``` - this in [0, 1, 2, 3, 4, 5, 6, 7, 8] + this in [0, 1, 2, 3, 4, 5, 6, 7, 8, 20, 21] ``` $ref: '#/components/schemas/policy.Algorithm' diff --git a/protocol/go/policy/kasregistry/key_access_server_registry.pb.go b/protocol/go/policy/kasregistry/key_access_server_registry.pb.go index c3ca23ec9e..61fe847fae 100644 --- a/protocol/go/policy/kasregistry/key_access_server_registry.pb.go +++ b/protocol/go/policy/kasregistry/key_access_server_registry.pb.go @@ -4414,444 +4414,445 @@ var file_policy_kasregistry_key_access_server_registry_proto_rawDesc = []byte{ 0x01, 0x22, 0x39, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, - 0x73, 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x22, 0x86, 0x04, 0x0a, + 0x73, 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x22, 0x8f, 0x04, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0xb0, 0x01, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, + 0x12, 0xb9, 0x01, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x42, 0x78, 0xba, 0x48, 0x75, - 0xba, 0x01, 0x72, 0x0a, 0x15, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, - 0x68, 0x6d, 0x5f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x12, 0x34, 0x54, 0x68, 0x65, 0x20, - 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x20, 0x6d, 0x75, - 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, - 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2e, - 0x1a, 0x23, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x5b, 0x30, 0x2c, 0x20, 0x31, 0x2c, - 0x20, 0x32, 0x2c, 0x20, 0x33, 0x2c, 0x20, 0x34, 0x2c, 0x20, 0x35, 0x2c, 0x20, 0x36, 0x2c, 0x20, - 0x37, 0x2c, 0x20, 0x38, 0x5d, 0x52, 0x0c, 0x6b, 0x65, 0x79, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, - 0x74, 0x68, 0x6d, 0x12, 0x21, 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, - 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x08, 0x6b, 0x61, 0x73, 0x5f, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, - 0x01, 0x48, 0x00, 0x52, 0x07, 0x6b, 0x61, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x07, - 0x6b, 0x61, 0x73, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, - 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x48, 0x00, 0x52, 0x06, 0x6b, 0x61, 0x73, - 0x55, 0x72, 0x69, 0x12, 0x1b, 0x0a, 0x06, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x08, 0x48, 0x01, 0x52, 0x06, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x88, 0x01, 0x01, - 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, - 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x0b, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, - 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x73, - 0x53, 0x6f, 0x72, 0x74, 0x42, 0x08, 0xba, 0x48, 0x05, 0x92, 0x01, 0x02, 0x10, 0x01, 0x52, 0x04, - 0x73, 0x6f, 0x72, 0x74, 0x12, 0x26, 0x0a, 0x06, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x18, 0x0c, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x65, - 0x61, 0x72, 0x63, 0x68, 0x52, 0x06, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x42, 0x0c, 0x0a, 0x0a, - 0x6b, 0x61, 0x73, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x6c, - 0x65, 0x67, 0x61, 0x63, 0x79, 0x22, 0x73, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, 0x08, 0x6b, 0x61, 0x73, - 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x07, 0x6b, 0x61, 0x73, - 0x4b, 0x65, 0x79, 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, - 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x86, 0x03, 0x0a, 0x10, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, - 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, - 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x54, - 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, 0x6d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, 0x68, 0x61, - 0x76, 0x69, 0x6f, 0x72, 0x3a, 0xcc, 0x01, 0xba, 0x48, 0xc8, 0x01, 0x1a, 0xc5, 0x01, 0x0a, 0x18, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, - 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x12, 0x52, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x20, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x20, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, - 0x6f, 0x72, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x65, 0x69, 0x74, 0x68, 0x65, - 0x72, 0x20, 0x41, 0x50, 0x50, 0x45, 0x4e, 0x44, 0x20, 0x6f, 0x72, 0x20, 0x52, 0x45, 0x50, 0x4c, - 0x41, 0x43, 0x45, 0x2c, 0x20, 0x77, 0x68, 0x65, 0x6e, 0x20, 0x75, 0x70, 0x64, 0x61, 0x74, 0x69, - 0x6e, 0x67, 0x20, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x1a, 0x55, 0x28, 0x28, - 0x21, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x29, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x28, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, - 0x73, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, - 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x20, 0x21, 0x3d, 0x20, - 0x30, 0x29, 0x29, 0x22, 0x3c, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x4b, 0x65, - 0x79, 0x22, 0xa4, 0x01, 0x0a, 0x10, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, - 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, - 0x48, 0x00, 0x52, 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, - 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, - 0x01, 0x48, 0x00, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x19, 0x0a, 0x03, 0x6b, 0x69, 0x64, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x03, - 0x6b, 0x69, 0x64, 0x42, 0x13, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, - 0x72, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x22, 0xf6, 0x0e, 0x0a, 0x10, 0x52, 0x6f, 0x74, - 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, - 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, - 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, - 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x48, 0x00, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x12, 0x44, 0x0a, 0x07, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, - 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, - 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4e, 0x65, 0x77, 0x4b, 0x65, - 0x79, 0x52, 0x06, 0x6e, 0x65, 0x77, 0x4b, 0x65, 0x79, 0x1a, 0xe0, 0x04, 0x0a, 0x06, 0x4e, 0x65, - 0x77, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x05, 0x6b, - 0x65, 0x79, 0x49, 0x64, 0x12, 0xae, 0x01, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, - 0x68, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x42, 0x7d, 0xba, 0x48, 0x7a, - 0xba, 0x01, 0x77, 0x0a, 0x15, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, - 0x68, 0x6d, 0x5f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x12, 0x34, 0x54, 0x68, 0x65, 0x20, - 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x20, 0x6d, 0x75, - 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, - 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2e, - 0x1a, 0x28, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x5b, 0x31, 0x2c, 0x20, 0x32, 0x2c, - 0x20, 0x33, 0x2c, 0x20, 0x34, 0x2c, 0x20, 0x35, 0x2c, 0x20, 0x36, 0x2c, 0x20, 0x37, 0x2c, 0x20, - 0x38, 0x2c, 0x20, 0x32, 0x30, 0x2c, 0x20, 0x32, 0x31, 0x5d, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, - 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x9e, 0x01, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, - 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x42, 0x72, 0xba, 0x48, 0x6f, 0xba, 0x01, - 0x67, 0x0a, 0x14, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x5f, - 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x12, 0x39, 0x54, 0x68, 0x65, 0x20, 0x6e, 0x65, 0x77, - 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, - 0x65, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x65, 0x66, - 0x69, 0x6e, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x20, 0x28, 0x31, 0x2d, 0x34, - 0x29, 0x2e, 0x1a, 0x14, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x5b, 0x31, 0x2c, 0x20, - 0x32, 0x2c, 0x20, 0x33, 0x2c, 0x20, 0x34, 0x5d, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x07, 0x6b, - 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x42, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, - 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, - 0x79, 0x43, 0x74, 0x78, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0c, 0x70, 0x75, - 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x12, 0x3d, 0x0a, 0x0f, 0x70, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x52, 0x0d, 0x70, 0x72, 0x69, 0x76, - 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x49, 0x64, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, - 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, - 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x3a, 0xcd, 0x08, 0xba, - 0x48, 0xc9, 0x08, 0x1a, 0xd8, 0x03, 0x0a, 0x23, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, - 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, - 0x6c, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0xcd, 0x01, 0x46, 0x6f, - 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6e, 0x65, 0x77, 0x20, 0x6b, 0x65, 0x79, 0x2c, 0x20, 0x74, - 0x68, 0x65, 0x20, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x69, - 0x73, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x20, 0x69, 0x66, 0x20, 0x6b, 0x65, - 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, - 0x44, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, - 0x45, 0x59, 0x20, 0x6f, 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, - 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, - 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, - 0x79, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x20, - 0x69, 0x66, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, - 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x20, 0x6f, - 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, - 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x2e, 0x1a, 0xe0, 0x01, 0x28, 0x28, - 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, 0x79, - 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, - 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, - 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, - 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, - 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x2e, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, - 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x20, 0x7c, 0x7c, 0x20, - 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, - 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x33, 0x20, 0x7c, 0x7c, 0x20, - 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, 0x79, - 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x34, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, - 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x69, 0x76, - 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x2e, 0x77, 0x72, 0x61, 0x70, - 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x1a, 0xb5, - 0x03, 0x0a, 0x26, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x5f, 0x69, 0x64, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x6c, 0x79, - 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0xb9, 0x01, 0x46, 0x6f, 0x72, 0x20, - 0x74, 0x68, 0x65, 0x20, 0x6e, 0x65, 0x77, 0x20, 0x6b, 0x65, 0x79, 0x2c, 0x20, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x20, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x20, 0x69, 0x64, 0x20, - 0x69, 0x73, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x20, 0x69, 0x66, 0x20, 0x6b, - 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, - 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x52, 0x4f, 0x4f, - 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x20, 0x6f, 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, - 0x45, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x2e, 0x20, 0x49, 0x74, 0x20, 0x6d, 0x75, 0x73, - 0x74, 0x20, 0x62, 0x65, 0x20, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x4b, - 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x52, - 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x4b, 0x45, 0x59, 0x5f, - 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, - 0x4f, 0x4e, 0x4c, 0x59, 0x2e, 0x1a, 0xce, 0x01, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, - 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, - 0x3d, 0x3d, 0x20, 0x31, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, - 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, - 0x20, 0x34, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, - 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x20, 0x7c, 0x7c, - 0x20, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, - 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x20, 0x7c, 0x7c, + 0x79, 0x2e, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x42, 0x80, 0x01, 0xba, 0x48, + 0x7d, 0xba, 0x01, 0x7a, 0x0a, 0x15, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, + 0x74, 0x68, 0x6d, 0x5f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x12, 0x34, 0x54, 0x68, 0x65, + 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x20, 0x6d, + 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, + 0x65, 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x2e, 0x1a, 0x2b, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x5b, 0x30, 0x2c, 0x20, 0x31, + 0x2c, 0x20, 0x32, 0x2c, 0x20, 0x33, 0x2c, 0x20, 0x34, 0x2c, 0x20, 0x35, 0x2c, 0x20, 0x36, 0x2c, + 0x20, 0x37, 0x2c, 0x20, 0x38, 0x2c, 0x20, 0x32, 0x30, 0x2c, 0x20, 0x32, 0x31, 0x5d, 0x52, 0x0c, + 0x6b, 0x65, 0x79, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x21, 0x0a, 0x06, + 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, + 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x05, 0x6b, 0x61, 0x73, 0x49, 0x64, 0x12, + 0x24, 0x0a, 0x08, 0x6b, 0x61, 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x48, 0x00, 0x52, 0x07, 0x6b, 0x61, + 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x75, 0x72, 0x69, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, + 0x01, 0x01, 0x48, 0x00, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x55, 0x72, 0x69, 0x12, 0x1b, 0x0a, 0x06, + 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x48, 0x01, 0x52, 0x06, + 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x88, 0x01, 0x01, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, + 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3d, + 0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x73, 0x53, 0x6f, 0x72, 0x74, 0x42, 0x08, 0xba, + 0x48, 0x05, 0x92, 0x01, 0x02, 0x10, 0x01, 0x52, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x12, 0x26, 0x0a, + 0x06, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x06, 0x73, + 0x65, 0x61, 0x72, 0x63, 0x68, 0x42, 0x0c, 0x0a, 0x0a, 0x6b, 0x61, 0x73, 0x5f, 0x66, 0x69, 0x6c, + 0x74, 0x65, 0x72, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x22, 0x73, + 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x29, 0x0a, 0x08, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, + 0x73, 0x4b, 0x65, 0x79, 0x52, 0x07, 0x6b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x34, 0x0a, + 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x22, 0x86, 0x03, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, + 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, + 0x69, 0x64, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x54, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, + 0x69, 0x6f, 0x72, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x16, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x3a, 0xcc, 0x01, + 0xba, 0x48, 0xc8, 0x01, 0x1a, 0xc5, 0x01, 0x0a, 0x18, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, + 0x72, 0x12, 0x52, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x20, 0x75, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x20, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x20, 0x6d, 0x75, 0x73, 0x74, + 0x20, 0x62, 0x65, 0x20, 0x65, 0x69, 0x74, 0x68, 0x65, 0x72, 0x20, 0x41, 0x50, 0x50, 0x45, 0x4e, + 0x44, 0x20, 0x6f, 0x72, 0x20, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x2c, 0x20, 0x77, 0x68, + 0x65, 0x6e, 0x20, 0x75, 0x70, 0x64, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x2e, 0x1a, 0x55, 0x28, 0x28, 0x21, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, + 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x29, 0x29, 0x20, 0x7c, 0x7c, + 0x20, 0x28, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, 0x68, + 0x61, 0x76, 0x69, 0x6f, 0x72, 0x20, 0x21, 0x3d, 0x20, 0x30, 0x29, 0x29, 0x22, 0x3c, 0x0a, 0x11, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, + 0x65, 0x79, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x22, 0xa4, 0x01, 0x0a, 0x10, 0x4b, + 0x61, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, + 0x21, 0x0a, 0x06, 0x6b, 0x61, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x05, 0x6b, 0x61, 0x73, + 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x1e, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, + 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x88, 0x01, 0x01, 0x48, 0x00, 0x52, 0x03, 0x75, 0x72, + 0x69, 0x12, 0x19, 0x0a, 0x03, 0x6b, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, + 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x03, 0x6b, 0x69, 0x64, 0x42, 0x13, 0x0a, 0x0a, + 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, + 0x01, 0x22, 0xf6, 0x0e, 0x0a, 0x10, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, + 0x69, 0x64, 0x12, 0x38, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x66, 0x69, 0x65, 0x72, 0x48, 0x00, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x44, 0x0a, 0x07, + 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x79, 0x2e, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x2e, 0x4e, 0x65, 0x77, 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6e, 0x65, 0x77, 0x4b, + 0x65, 0x79, 0x1a, 0xe0, 0x04, 0x0a, 0x06, 0x4e, 0x65, 0x77, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, + 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, + 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0xae, 0x01, + 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x11, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x41, 0x6c, 0x67, 0x6f, 0x72, + 0x69, 0x74, 0x68, 0x6d, 0x42, 0x7d, 0xba, 0x48, 0x7a, 0xba, 0x01, 0x77, 0x0a, 0x15, 0x6b, 0x65, + 0x79, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x5f, 0x64, 0x65, 0x66, 0x69, + 0x6e, 0x65, 0x64, 0x12, 0x34, 0x54, 0x68, 0x65, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x6c, 0x67, + 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x6f, + 0x6e, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, + 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2e, 0x1a, 0x28, 0x74, 0x68, 0x69, 0x73, 0x20, + 0x69, 0x6e, 0x20, 0x5b, 0x31, 0x2c, 0x20, 0x32, 0x2c, 0x20, 0x33, 0x2c, 0x20, 0x34, 0x2c, 0x20, + 0x35, 0x2c, 0x20, 0x36, 0x2c, 0x20, 0x37, 0x2c, 0x20, 0x38, 0x2c, 0x20, 0x32, 0x30, 0x2c, 0x20, + 0x32, 0x31, 0x5d, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x9e, + 0x01, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x0f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x4d, 0x6f, + 0x64, 0x65, 0x42, 0x72, 0xba, 0x48, 0x6f, 0xba, 0x01, 0x67, 0x0a, 0x14, 0x6e, 0x65, 0x77, 0x5f, + 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x5f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, + 0x12, 0x39, 0x54, 0x68, 0x65, 0x20, 0x6e, 0x65, 0x77, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, + 0x64, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x6f, + 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x20, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x20, 0x28, 0x31, 0x2d, 0x34, 0x29, 0x2e, 0x1a, 0x14, 0x74, 0x68, 0x69, + 0x73, 0x20, 0x69, 0x6e, 0x20, 0x5b, 0x31, 0x2c, 0x20, 0x32, 0x2c, 0x20, 0x33, 0x2c, 0x20, 0x34, + 0x5d, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x12, + 0x42, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, + 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x43, 0x74, 0x78, 0x42, 0x06, 0xba, + 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0c, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, + 0x43, 0x74, 0x78, 0x12, 0x3d, 0x0a, 0x0f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, + 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, + 0x43, 0x74, 0x78, 0x52, 0x0d, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x43, + 0x74, 0x78, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x49, 0x64, + 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x4d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x3a, 0xcd, 0x08, 0xba, 0x48, 0xc9, 0x08, 0x1a, 0xd8, 0x03, 0x0a, + 0x23, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, + 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x6c, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, + 0x69, 0x72, 0x65, 0x64, 0x12, 0xcd, 0x01, 0x46, 0x6f, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6e, + 0x65, 0x77, 0x20, 0x6b, 0x65, 0x79, 0x2c, 0x20, 0x74, 0x68, 0x65, 0x20, 0x77, 0x72, 0x61, 0x70, + 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x69, 0x73, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, + 0x72, 0x65, 0x64, 0x20, 0x69, 0x66, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, + 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, + 0x49, 0x47, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x20, 0x6f, 0x72, 0x20, 0x4b, + 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, + 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x77, + 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, + 0x62, 0x65, 0x20, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x20, 0x69, 0x66, 0x20, 0x6b, 0x65, 0x79, 0x5f, + 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, + 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x20, 0x6f, 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, + 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x4f, + 0x4e, 0x4c, 0x59, 0x2e, 0x1a, 0xe0, 0x01, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, + 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, + 0x3d, 0x20, 0x31, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, + 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, + 0x32, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, + 0x65, 0x79, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, + 0x74, 0x78, 0x2e, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x20, 0x21, + 0x3d, 0x20, 0x27, 0x27, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, + 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, + 0x20, 0x3d, 0x3d, 0x20, 0x33, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, + 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, + 0x3d, 0x20, 0x34, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, + 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, + 0x5f, 0x63, 0x74, 0x78, 0x2e, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, + 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x1a, 0xb5, 0x03, 0x0a, 0x26, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, 0x5f, 0x6f, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x6c, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, + 0x65, 0x64, 0x12, 0xb9, 0x01, 0x46, 0x6f, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6e, 0x65, 0x77, + 0x20, 0x6b, 0x65, 0x79, 0x2c, 0x20, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x20, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x20, 0x69, 0x64, 0x20, 0x69, 0x73, 0x20, 0x72, 0x65, 0x71, 0x75, + 0x69, 0x72, 0x65, 0x64, 0x20, 0x69, 0x66, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, + 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x52, 0x4f, + 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x20, 0x6f, + 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, + 0x45, 0x2e, 0x20, 0x49, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x65, 0x6d, + 0x70, 0x74, 0x79, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, + 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x4b, 0x45, 0x59, + 0x20, 0x61, 0x6e, 0x64, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x55, + 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x2e, 0x1a, 0xce, + 0x01, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, + 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, - 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x33, 0x29, 0x20, 0x26, 0x26, 0x20, + 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x34, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, 0x20, - 0x21, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x1a, 0xb3, 0x01, 0x0a, 0x23, 0x70, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x5f, 0x66, 0x6f, 0x72, 0x5f, 0x70, - 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x12, 0x48, - 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x20, - 0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x74, 0x20, - 0x69, 0x66, 0x20, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, - 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, - 0x45, 0x59, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x2e, 0x1a, 0x42, 0x21, 0x28, 0x74, 0x68, 0x69, 0x73, + 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x20, 0x7c, 0x7c, 0x20, 0x28, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, - 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x34, 0x20, 0x26, 0x26, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, - 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x29, 0x29, 0x42, 0x13, 0x0a, 0x0a, - 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, - 0x01, 0x22, 0x32, 0x0a, 0x0e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, - 0x6e, 0x67, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x66, 0x71, 0x6e, 0x22, 0xe3, 0x02, 0x0a, 0x10, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, - 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x36, 0x0a, 0x0f, 0x72, 0x6f, - 0x74, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6f, 0x75, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, - 0x4b, 0x65, 0x79, 0x52, 0x0d, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x64, 0x4f, 0x75, 0x74, 0x4b, - 0x65, 0x79, 0x12, 0x66, 0x0a, 0x1d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, - 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, - 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x43, - 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x1b, 0x61, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x5c, 0x0a, 0x18, 0x61, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x6d, 0x61, - 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, - 0x79, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, - 0x52, 0x16, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x51, 0x0a, 0x12, 0x6e, 0x61, 0x6d, 0x65, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x04, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, - 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, - 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x11, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x8f, 0x01, 0x0a, 0x11, - 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, - 0x65, 0x79, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x51, 0x0a, 0x11, 0x72, 0x6f, - 0x74, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, - 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x6f, 0x74, 0x61, 0x74, - 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x10, 0x72, 0x6f, 0x74, - 0x61, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x22, 0x7e, 0x0a, - 0x11, 0x53, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, - 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, - 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, - 0x72, 0x48, 0x00, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x42, 0x13, 0x0a, 0x0a, 0x61, 0x63, 0x74, 0x69, - 0x76, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x22, 0x13, 0x0a, - 0x11, 0x47, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x22, 0x45, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x08, 0x62, 0x61, 0x73, 0x65, - 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, - 0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x22, 0x8e, 0x01, 0x0a, 0x12, 0x53, 0x65, - 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x36, 0x0a, 0x0c, 0x6e, 0x65, 0x77, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x0a, 0x6e, 0x65, - 0x77, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x40, 0x0a, 0x11, 0x70, 0x72, 0x65, 0x76, - 0x69, 0x6f, 0x75, 0x73, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, + 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, + 0x65, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, + 0x3d, 0x3d, 0x20, 0x33, 0x29, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, + 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, 0x29, 0x1a, + 0xb3, 0x01, 0x0a, 0x23, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, + 0x63, 0x74, 0x78, 0x5f, 0x66, 0x6f, 0x72, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, + 0x65, 0x79, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x12, 0x48, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x74, 0x78, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, + 0x74, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x74, 0x20, 0x69, 0x66, 0x20, 0x6b, 0x65, 0x79, 0x5f, + 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x69, 0x73, 0x20, 0x4b, 0x45, 0x59, 0x5f, 0x4d, 0x4f, 0x44, 0x45, + 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, + 0x2e, 0x1a, 0x42, 0x21, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, 0x6b, 0x65, + 0x79, 0x2e, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x34, 0x20, + 0x26, 0x26, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x5f, + 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, + 0x63, 0x74, 0x78, 0x29, 0x29, 0x42, 0x13, 0x0a, 0x0a, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, + 0x6b, 0x65, 0x79, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x22, 0x32, 0x0a, 0x0e, 0x43, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, + 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x22, 0xe3, + 0x02, 0x0a, 0x10, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x12, 0x36, 0x0a, 0x0f, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6f, + 0x75, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x0d, 0x72, 0x6f, + 0x74, 0x61, 0x74, 0x65, 0x64, 0x4f, 0x75, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x66, 0x0a, 0x1d, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x61, + 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x1b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x70, 0x70, 0x69, + 0x6e, 0x67, 0x73, 0x12, 0x5c, 0x0a, 0x18, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, + 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x16, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, + 0x73, 0x12, 0x51, 0x0a, 0x12, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6d, + 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x79, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, + 0x73, 0x52, 0x11, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4d, 0x61, 0x70, 0x70, + 0x69, 0x6e, 0x67, 0x73, 0x22, 0x8f, 0x01, 0x0a, 0x11, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, + 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x07, 0x6b, 0x61, + 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x06, 0x6b, 0x61, 0x73, + 0x4b, 0x65, 0x79, 0x12, 0x51, 0x0a, 0x11, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x72, 0x79, 0x2e, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x73, 0x52, 0x10, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x22, 0x7e, 0x0a, 0x11, 0x53, 0x65, 0x74, 0x42, 0x61, 0x73, + 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, + 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, + 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x48, 0x00, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x42, 0x13, 0x0a, 0x0a, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x12, + 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x22, 0x13, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x42, 0x61, 0x73, + 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x45, 0x0a, 0x12, 0x47, + 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x2f, 0x0a, 0x08, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x69, 0x6d, - 0x70, 0x6c, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x0f, 0x70, 0x72, 0x65, 0x76, 0x69, - 0x6f, 0x75, 0x73, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x22, 0x36, 0x0a, 0x12, 0x4d, 0x61, - 0x70, 0x70, 0x65, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, - 0x12, 0x10, 0x0a, 0x03, 0x66, 0x71, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x66, - 0x71, 0x6e, 0x22, 0xb4, 0x02, 0x0a, 0x0a, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, - 0x67, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x6b, 0x69, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x6b, 0x61, 0x73, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6b, 0x61, 0x73, 0x55, 0x72, 0x69, 0x12, 0x55, 0x0a, 0x12, - 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, - 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x70, 0x6c, 0x65, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x4b, + 0x65, 0x79, 0x22, 0x8e, 0x01, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, + 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x0c, 0x6e, 0x65, 0x77, + 0x5f, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x4b, + 0x61, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x0a, 0x6e, 0x65, 0x77, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, + 0x79, 0x12, 0x40, 0x0a, 0x11, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x5f, 0x62, 0x61, + 0x73, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x4b, 0x61, 0x73, 0x4b, + 0x65, 0x79, 0x52, 0x0f, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x42, 0x61, 0x73, 0x65, + 0x4b, 0x65, 0x79, 0x22, 0x36, 0x0a, 0x12, 0x4d, 0x61, 0x70, 0x70, 0x65, 0x64, 0x50, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x66, 0x71, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x66, 0x71, 0x6e, 0x22, 0xb4, 0x02, 0x0a, 0x0a, + 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x69, 0x64, 0x12, 0x17, 0x0a, 0x07, + 0x6b, 0x61, 0x73, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6b, + 0x61, 0x73, 0x55, 0x72, 0x69, 0x12, 0x55, 0x0a, 0x12, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x26, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x61, 0x70, 0x70, 0x65, 0x64, 0x50, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x11, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x55, 0x0a, 0x12, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, + 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x61, 0x70, 0x70, 0x65, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x52, 0x11, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, - 0x6e, 0x67, 0x73, 0x12, 0x55, 0x0a, 0x12, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x26, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x61, 0x70, 0x70, 0x65, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x11, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x4d, 0x0a, 0x0e, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x05, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x61, 0x70, 0x70, 0x65, 0x64, 0x50, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x0d, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x22, 0xb8, 0x01, 0x0a, 0x16, 0x4c, 0x69, - 0x73, 0x74, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, - 0x12, 0x38, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x72, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, - 0x69, 0x65, 0x72, 0x48, 0x00, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, - 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, - 0x13, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x05, 0xba, - 0x48, 0x02, 0x08, 0x00, 0x22, 0x92, 0x01, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, - 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x41, 0x0a, 0x0c, 0x6b, 0x65, 0x79, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, - 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x4d, - 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x0b, 0x6b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, - 0x6e, 0x67, 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, - 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2a, 0xef, 0x01, 0x0a, 0x18, 0x53, 0x6f, - 0x72, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2c, 0x0a, 0x28, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4b, - 0x45, 0x59, 0x5f, 0x41, 0x43, 0x43, 0x45, 0x53, 0x53, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, - 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, - 0x45, 0x44, 0x10, 0x00, 0x12, 0x25, 0x0a, 0x21, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x45, 0x59, + 0x52, 0x11, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, + 0x6e, 0x67, 0x73, 0x12, 0x4d, 0x0a, 0x0e, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x6d, 0x61, 0x70, + 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, + 0x2e, 0x4d, 0x61, 0x70, 0x70, 0x65, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4f, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x52, 0x0d, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, + 0x67, 0x73, 0x22, 0xb8, 0x01, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x4d, 0x61, + 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, + 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x4b, + 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x48, 0x00, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x33, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x70, 0x61, + 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x13, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x00, 0x22, 0x92, 0x01, + 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x6b, 0x65, 0x79, + 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, + 0x0b, 0x6b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x34, 0x0a, 0x0a, + 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x14, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x61, 0x67, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x2a, 0xef, 0x01, 0x0a, 0x18, 0x53, 0x6f, 0x72, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x2c, 0x0a, 0x28, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x43, 0x43, 0x45, + 0x53, 0x53, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x25, 0x0a, + 0x21, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x43, 0x43, 0x45, 0x53, 0x53, + 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4e, 0x41, + 0x4d, 0x45, 0x10, 0x01, 0x12, 0x24, 0x0a, 0x20, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x43, 0x43, 0x45, 0x53, 0x53, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x53, 0x5f, - 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4e, 0x41, 0x4d, 0x45, 0x10, 0x01, 0x12, 0x24, 0x0a, 0x20, 0x53, - 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x43, 0x43, 0x45, 0x53, 0x53, 0x5f, 0x53, - 0x45, 0x52, 0x56, 0x45, 0x52, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x52, 0x49, 0x10, - 0x02, 0x12, 0x2b, 0x0a, 0x27, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x43, - 0x43, 0x45, 0x53, 0x53, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x53, 0x5f, 0x54, 0x59, 0x50, - 0x45, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x5f, 0x41, 0x54, 0x10, 0x03, 0x12, 0x2b, - 0x0a, 0x27, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x43, 0x43, 0x45, 0x53, - 0x53, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, - 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x5f, 0x41, 0x54, 0x10, 0x04, 0x2a, 0x9a, 0x01, 0x0a, 0x0f, - 0x53, 0x6f, 0x72, 0x74, 0x4b, 0x61, 0x73, 0x4b, 0x65, 0x79, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x22, 0x0a, 0x1e, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x41, 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x53, - 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, - 0x44, 0x10, 0x00, 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x41, 0x53, 0x5f, - 0x4b, 0x45, 0x59, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x49, 0x44, - 0x10, 0x01, 0x12, 0x21, 0x0a, 0x1d, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x41, 0x53, 0x5f, 0x4b, - 0x45, 0x59, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, - 0x5f, 0x41, 0x54, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x41, - 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x50, 0x44, 0x41, - 0x54, 0x45, 0x44, 0x5f, 0x41, 0x54, 0x10, 0x03, 0x32, 0x99, 0x0c, 0x0a, 0x1e, 0x4b, 0x65, 0x79, - 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x72, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7e, 0x0a, 0x14, 0x4c, - 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x73, 0x12, 0x2f, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, - 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, - 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, - 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, - 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x78, 0x0a, 0x12, 0x47, + 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x52, 0x49, 0x10, 0x02, 0x12, 0x2b, 0x0a, 0x27, 0x53, 0x4f, + 0x52, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x43, 0x43, 0x45, 0x53, 0x53, 0x5f, 0x53, 0x45, + 0x52, 0x56, 0x45, 0x52, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, + 0x45, 0x44, 0x5f, 0x41, 0x54, 0x10, 0x03, 0x12, 0x2b, 0x0a, 0x27, 0x53, 0x4f, 0x52, 0x54, 0x5f, + 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x43, 0x43, 0x45, 0x53, 0x53, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, + 0x52, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x5f, + 0x41, 0x54, 0x10, 0x04, 0x2a, 0x9a, 0x01, 0x0a, 0x0f, 0x53, 0x6f, 0x72, 0x74, 0x4b, 0x61, 0x73, + 0x4b, 0x65, 0x79, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x22, 0x0a, 0x1e, 0x53, 0x4f, 0x52, 0x54, + 0x5f, 0x4b, 0x41, 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x53, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1d, 0x0a, 0x19, + 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x41, 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x53, 0x5f, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x49, 0x44, 0x10, 0x01, 0x12, 0x21, 0x0a, 0x1d, 0x53, + 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x41, 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x53, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x5f, 0x41, 0x54, 0x10, 0x02, 0x12, 0x21, + 0x0a, 0x1d, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x4b, 0x41, 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x53, 0x5f, + 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x5f, 0x41, 0x54, 0x10, + 0x03, 0x32, 0x99, 0x0c, 0x0a, 0x1e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x12, 0x7e, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x41, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x2f, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x03, 0x90, 0x02, 0x01, 0x12, 0x78, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x2d, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, + 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x12, 0x2d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x2e, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, - 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x7e, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, - 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x30, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x31, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, - 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x7e, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, - 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x30, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x31, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, - 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x7e, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, - 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x30, - 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x31, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, - 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x90, 0x01, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, - 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x61, - 0x6e, 0x74, 0x73, 0x12, 0x34, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, - 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, - 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x61, 0x6e, - 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, - 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x06, 0x88, 0x02, 0x01, 0x90, 0x02, 0x01, 0x12, 0x5a, 0x0a, 0x09, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, - 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, - 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x06, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x21, + 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x7e, + 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x7e, + 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x7e, + 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x30, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x90, + 0x01, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x12, 0x34, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x41, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x61, 0x6e, 0x74, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0x88, 0x02, 0x01, 0x90, 0x02, + 0x01, 0x12, 0x5a, 0x0a, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x22, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x57, 0x0a, 0x08, 0x4c, 0x69, 0x73, 0x74, 0x4b, - 0x65, 0x79, 0x73, 0x12, 0x23, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, - 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x5a, 0x0a, 0x09, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x72, 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, - 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, - 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x09, - 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x52, - 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x74, 0x72, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, + 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, + 0x06, 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, + 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, + 0x47, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x57, 0x0a, 0x08, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x23, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x09, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x70, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x79, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x09, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, + 0x65, 0x79, 0x12, 0x24, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, + 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x6f, + 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x12, 0x5d, 0x0a, 0x0a, 0x53, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x25, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5d, 0x0a, 0x0a, 0x53, 0x65, 0x74, 0x42, - 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x25, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, + 0x73, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x74, 0x42, - 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, - 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x72, 0x79, 0x2e, 0x53, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5d, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x42, 0x61, - 0x73, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x25, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, + 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x5d, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x25, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, - 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x70, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, - 0x79, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6c, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, - 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x2a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, - 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, - 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, - 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x42, 0xdb, 0x01, 0x0a, 0x16, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x42, - 0x1c, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, - 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, - 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, - 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0xa2, 0x02, 0x03, 0x50, 0x4b, - 0x58, 0xaa, 0x02, 0x12, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x72, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0xca, 0x02, 0x12, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, - 0x4b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0xe2, 0x02, 0x1e, 0x50, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x4b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, - 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x13, 0x50, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x3a, 0x3a, 0x4b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x72, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x6c, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, + 0x67, 0x73, 0x12, 0x2a, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x4d, + 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, + 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, + 0x6e, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0xdb, 0x01, + 0x0a, 0x16, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x6b, 0x61, 0x73, + 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x42, 0x1c, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x70, 0x6c, 0x61, + 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x67, + 0x6f, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2f, 0x6b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x79, 0xa2, 0x02, 0x03, 0x50, 0x4b, 0x58, 0xaa, 0x02, 0x12, 0x50, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x2e, 0x4b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0xca, + 0x02, 0x12, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x4b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x79, 0xe2, 0x02, 0x1e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5c, 0x4b, 0x61, + 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x13, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x3a, 0x3a, + 0x4b, 0x61, 0x73, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( diff --git a/service/policy/kasregistry/key_access_server_registry.proto b/service/policy/kasregistry/key_access_server_registry.proto index d2f4679980..4b40df67cf 100644 --- a/service/policy/kasregistry/key_access_server_registry.proto +++ b/service/policy/kasregistry/key_access_server_registry.proto @@ -480,7 +480,7 @@ message ListKeysRequest { Algorithm key_algorithm = 1 [(buf.validate.field).cel = { id: "key_algorithm_defined" message: "The key_algorithm must be one of the defined values." - expression: "this in [0, 1, 2, 3, 4, 5, 6, 7, 8]" // Allow unspecified and all supported algorithm values + expression: "this in [0, 1, 2, 3, 4, 5, 6, 7, 8, 20, 21]" // Allow unspecified and all supported algorithm values, including ALGORITHM_MLKEM_768 and ALGORITHM_MLKEM_1024 }]; // Filter keys by algorithm oneof kas_filter { From 7cfef9b6f73a6bdbda1fa6bbe1fc2aae7f950554 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Thu, 28 May 2026 12:21:45 -0400 Subject: [PATCH 14/25] fix(security): export ML-KEM public keys from InProcessProvider ExportPublicKey fell through RSA/Hybrid/XWing to ECPublicKey, so pure ML-KEM keys returned ErrCertNotFound and KAS PublicKey requests for mlkem:768/mlkem:1024 failed with not_found. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Dave Mihalcik --- service/internal/security/in_process_provider.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/service/internal/security/in_process_provider.go b/service/internal/security/in_process_provider.go index 0706038330..9d23930310 100644 --- a/service/internal/security/in_process_provider.go +++ b/service/internal/security/in_process_provider.go @@ -104,6 +104,9 @@ func (k *KeyDetailsAdapter) ExportPublicKey(_ context.Context, format trust.KeyT if xwingKey, err := k.cryptoProvider.XWingPublicKey(kid); err == nil { return xwingKey, nil } + if mlkemKey, err := k.cryptoProvider.MLKEMPublicKey(kid); err == nil { + return mlkemKey, nil + } return k.cryptoProvider.ECPublicKey(kid) default: return "", ErrCertNotFound From 303ffc666722af6deb9845ef087c93ff2a629c1f Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Thu, 28 May 2026 15:51:59 -0400 Subject: [PATCH 15/25] fix(security): allow pure ML-KEM rewrap through InProcessProvider InProcessProvider.Decrypt rejected empty ephemeralPublicKey for ML-KEM, but StandardCrypto.Decrypt and BasicManager.Decrypt both reject non-empty values. The KEM ciphertext is the encapsulation; there is no separate ephemeral key. Invert the check to match the HPQT case above and pass nil downstream. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Dave Mihalcik --- service/internal/security/in_process_provider.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/service/internal/security/in_process_provider.go b/service/internal/security/in_process_provider.go index 9d23930310..463f69c6d3 100644 --- a/service/internal/security/in_process_provider.go +++ b/service/internal/security/in_process_provider.go @@ -280,10 +280,10 @@ func (a *InProcessProvider) Decrypt(ctx context.Context, keyDetails trust.KeyDet return a.cryptoProvider.Decrypt(ctx, trust.KeyIdentifier(kid), ciphertext, nil) case AlgorithmMLKEM768, AlgorithmMLKEM1024: - if len(ephemeralPublicKey) == 0 { - return nil, errors.New("ephemeral public key (ciphertext) is required for ML-KEM decryption") + if len(ephemeralPublicKey) > 0 { + return nil, errors.New("ephemeral public key should not be provided for ML-KEM decryption") } - return a.cryptoProvider.Decrypt(ctx, trust.KeyIdentifier(kid), ciphertext, ephemeralPublicKey) + return a.cryptoProvider.Decrypt(ctx, trust.KeyIdentifier(kid), ciphertext, nil) default: return nil, errors.New("unsupported key algorithm") From 0410a9ddfffeb5b6f899877b1e1aac740bade895 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Thu, 28 May 2026 15:52:51 -0400 Subject: [PATCH 16/25] fix(ocrypto): emit canonical PEM block types from ML-KEM encryptors MLKEMEncryptor768/1024.PublicKeyInPemFormat emitted "MLKEM ENCAPSULATOR", but FromPublicPEMWithSalt dispatches on PEMBlockMLKEM768PublicKey / PEMBlockMLKEM1024PublicKey, breaking PEM round-trip. Use the canonical constants to match the X-Wing pattern and the existing MLKEMKeyPair serialization in ec_key_pair.go. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Dave Mihalcik --- lib/ocrypto/mlkem.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ocrypto/mlkem.go b/lib/ocrypto/mlkem.go index 3df647aaa1..e87150aebf 100644 --- a/lib/ocrypto/mlkem.go +++ b/lib/ocrypto/mlkem.go @@ -74,7 +74,7 @@ func (e *MLKEMEncryptor768) Encrypt(data []byte) ([]byte, error) { func (e *MLKEMEncryptor768) PublicKeyInPemFormat() (string, error) { pemBlock := &pem.Block{ - Type: "MLKEM ENCAPSULATOR", + Type: PEMBlockMLKEM768PublicKey, Bytes: e.publicKey, } return string(pem.EncodeToMemory(pemBlock)), nil @@ -134,7 +134,7 @@ func (e *MLKEMEncryptor1024) Encrypt(data []byte) ([]byte, error) { func (e *MLKEMEncryptor1024) PublicKeyInPemFormat() (string, error) { pemBlock := &pem.Block{ - Type: "MLKEM ENCAPSULATOR", + Type: PEMBlockMLKEM1024PublicKey, Bytes: e.publicKey, } return string(pem.EncodeToMemory(pemBlock)), nil From 0ded9ea3ea89c0f7ca178753c7e5407a73fa5633 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Thu, 28 May 2026 15:53:42 -0400 Subject: [PATCH 17/25] test(security): cover missing-kid case in ML-KEM determineKeyType Mirrors the negative assertion already in TestInProcessProviderDetermineKeyType. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Dave Mihalcik --- service/internal/security/in_process_provider_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/service/internal/security/in_process_provider_test.go b/service/internal/security/in_process_provider_test.go index 277a4348ea..e56eca2cfc 100644 --- a/service/internal/security/in_process_provider_test.go +++ b/service/internal/security/in_process_provider_test.go @@ -251,4 +251,7 @@ func TestInProcessProviderDetermineKeyTypeMLKEM(t *testing.T) { details, err = provider.FindKeyByID(t.Context(), trust.KeyIdentifier(material.mlkem1024Kid)) require.NoError(t, err) assert.Equal(t, ocrypto.KeyType(AlgorithmMLKEM1024), details.Algorithm()) + + _, err = provider.determineKeyType("missing") + require.Error(t, err) } From 7b7b5a713564fefd2dce29c63f48e4af12710951 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Fri, 29 May 2026 09:32:55 -0400 Subject: [PATCH 18/25] refactor(ocrypto): encode pure ML-KEM keys as SPKI/PKCS#8 with NIST OIDs Replace the custom "MLKEM768 PUBLIC KEY" / "MLKEM1024 PRIVATE KEY" PEM labels (raw key bytes with no ASN.1 envelope) with standard "PUBLIC KEY" and "PRIVATE KEY" labels carrying RFC 5280 SubjectPublicKeyInfo and RFC 5958 OneAsymmetricKey, with the algorithm conveyed by NIST OIDs 2.16.840.1.101.3.4.4.2 (ML-KEM-768) and 2.16.840.1.101.3.4.4.3 (ML-KEM-1024). The private-key PKCS#8 inner CHOICE uses [0] IMPLICIT OCTET STRING (seed form, 64 bytes) per draft-ietf-lamps-kyber-certificates. The encoding is hand-rolled rather than via crypto/x509 because stdlib ML-KEM support in MarshalPKIXPublicKey / MarshalPKCS8PrivateKey landed in Go 1.26 and this module pins go 1.25. FromPublicPEMWithSalt / FromPrivatePEMWithSalt now peek at the OID after PEM decode and route ML-KEM blobs to the existing encryptor/decryptor constructors. Non-ML-KEM blobs fall through to the existing RSA/EC parsers unchanged. The hybrid SECP256R1-MLKEM768, SECP384R1-MLKEM1024, and X-Wing schemes keep their custom PEM labels for now; conformance to IETF composite-KEM and X-Wing drafts is tracked under DSPX-3396 and will land in a separate PR off main. Co-Authored-By: Claude Opus 4.7 Signed-off-by: Dave Mihalcik --- lib/ocrypto/asym_decryption.go | 13 +++- lib/ocrypto/asym_encryption.go | 15 ++-- lib/ocrypto/ec_key_pair.go | 56 ++++++-------- lib/ocrypto/mlkem.go | 130 ++++++++++++++++++++++++++++++--- lib/ocrypto/mlkem_test.go | 70 ++++++++++++++++++ lib/ocrypto/rsa_key_pair.go | 4 +- 6 files changed, 233 insertions(+), 55 deletions(-) diff --git a/lib/ocrypto/asym_decryption.go b/lib/ocrypto/asym_decryption.go index 6656c92be9..93e2b158b0 100644 --- a/lib/ocrypto/asym_decryption.go +++ b/lib/ocrypto/asym_decryption.go @@ -50,10 +50,15 @@ func FromPrivatePEMWithSalt(privateKeyInPem string, salt, info []byte) (PrivateK return NewSaltedP256MLKEM768Decryptor(block.Bytes, salt, info) case PEMBlockP384MLKEM1024PrivateKey: return NewSaltedP384MLKEM1024Decryptor(block.Bytes, salt, info) - case PEMBlockMLKEM768PrivateKey: - return NewSaltedMLKEM768Decryptor(block.Bytes, salt, info) - case PEMBlockMLKEM1024PrivateKey: - return NewSaltedMLKEM1024Decryptor(block.Bytes, salt, info) + } + + switch oid, seed, err := parseMLKEMPrivatePKCS8(block.Bytes); { + case err == nil && oid.Equal(oidMLKEM768): + return NewSaltedMLKEM768Decryptor(seed, salt, info) + case err == nil && oid.Equal(oidMLKEM1024): + return NewSaltedMLKEM1024Decryptor(seed, salt, info) + case err != nil && !errors.Is(err, errNotMLKEM): + return AsymDecryption{}, err } priv, err := x509.ParsePKCS8PrivateKey(block.Bytes) diff --git a/lib/ocrypto/asym_encryption.go b/lib/ocrypto/asym_encryption.go index e3529084b3..dd90543df2 100644 --- a/lib/ocrypto/asym_encryption.go +++ b/lib/ocrypto/asym_encryption.go @@ -82,10 +82,15 @@ func FromPublicPEMWithSalt(publicKeyInPem string, salt, info []byte) (PublicKeyE return NewP256MLKEM768Encryptor(block.Bytes, salt, info) case PEMBlockP384MLKEM1024PublicKey: return NewP384MLKEM1024Encryptor(block.Bytes, salt, info) - case PEMBlockMLKEM768PublicKey: - return NewMLKEM768Encryptor(block.Bytes, salt, info) - case PEMBlockMLKEM1024PublicKey: - return NewMLKEM1024Encryptor(block.Bytes, salt, info) + } + + switch oid, key, err := parseMLKEMPublicSPKI(block.Bytes); { + case err == nil && oid.Equal(oidMLKEM768): + return NewMLKEM768Encryptor(key, salt, info) + case err == nil && oid.Equal(oidMLKEM1024): + return NewMLKEM1024Encryptor(key, salt, info) + case err != nil && !errors.Is(err, errNotMLKEM): + return nil, err } pub, err := getPublicPart(publicKeyInPem) @@ -242,7 +247,7 @@ func publicKeyInPemFormat(pk any) (string, error) { publicKeyPem := pem.EncodeToMemory( &pem.Block{ - Type: "PUBLIC KEY", + Type: pemBlockPublicKey, Bytes: publicKeyBytes, }, ) diff --git a/lib/ocrypto/ec_key_pair.go b/lib/ocrypto/ec_key_pair.go index ccac3d7dff..d28ad46799 100644 --- a/lib/ocrypto/ec_key_pair.go +++ b/lib/ocrypto/ec_key_pair.go @@ -228,7 +228,7 @@ func (keyPair ECKeyPair) PrivateKeyInPemFormat() (string, error) { privateKeyPem := pem.EncodeToMemory( &pem.Block{ - Type: "PRIVATE KEY", + Type: pemBlockPrivateKey, Bytes: privateKeyBytes, }, ) @@ -248,7 +248,7 @@ func (keyPair ECKeyPair) PublicKeyInPemFormat() (string, error) { publicKeyPem := pem.EncodeToMemory( &pem.Block{ - Type: "PUBLIC KEY", + Type: pemBlockPublicKey, Bytes: publicKeyBytes, }, ) @@ -468,7 +468,7 @@ func ECPrivateKeyInPemFormat(privateKey ecdsa.PrivateKey) (string, error) { privateKeyPem := pem.EncodeToMemory( &pem.Block{ - Type: "PRIVATE KEY", + Type: pemBlockPrivateKey, Bytes: privateKeyBytes, }, ) @@ -484,7 +484,7 @@ func ECPublicKeyInPemFormat(publicKey ecdsa.PublicKey) (string, error) { publicKeyPem := pem.EncodeToMemory( &pem.Block{ - Type: "PUBLIC KEY", + Type: pemBlockPublicKey, Bytes: pkb, }, ) @@ -549,13 +549,11 @@ func (keyPair MLKEMKeyPair) PrivateKeyInPemFormat() (string, error) { return "", errors.New("failed to generate PEM formatted private key") } - privateKeyPEM := pem.EncodeToMemory( - &pem.Block{ - Type: PEMBlockMLKEM768PrivateKey, - Bytes: keyPair.PrivateKey.Bytes(), - }, - ) - return string(privateKeyPEM), nil + der, err := marshalMLKEMPrivatePKCS8(oidMLKEM768, keyPair.PrivateKey.Bytes()) + if err != nil { + return "", fmt.Errorf("marshal ML-KEM-768 PKCS#8 failed: %w", err) + } + return string(pem.EncodeToMemory(&pem.Block{Type: pemBlockPrivateKey, Bytes: der})), nil } func (keyPair MLKEMKeyPair) PublicKeyInPemFormat() (string, error) { @@ -563,13 +561,11 @@ func (keyPair MLKEMKeyPair) PublicKeyInPemFormat() (string, error) { return "", errors.New("failed to generate PEM formatted public key") } - publicKeyPEM := pem.EncodeToMemory( - &pem.Block{ - Type: PEMBlockMLKEM768PublicKey, - Bytes: keyPair.PrivateKey.EncapsulationKey().Bytes(), - }, - ) - return string(publicKeyPEM), nil + der, err := marshalMLKEMPublicSPKI(oidMLKEM768, keyPair.PrivateKey.EncapsulationKey().Bytes()) + if err != nil { + return "", fmt.Errorf("marshal ML-KEM-768 SPKI failed: %w", err) + } + return string(pem.EncodeToMemory(&pem.Block{Type: pemBlockPublicKey, Bytes: der})), nil } func (keyPair MLKEMKeyPair) GetKeyType() KeyType { @@ -581,13 +577,11 @@ func (keyPair MLKEM1024KeyPair) PrivateKeyInPemFormat() (string, error) { return "", errors.New("failed to generate PEM formatted private key") } - privateKeyPEM := pem.EncodeToMemory( - &pem.Block{ - Type: PEMBlockMLKEM1024PrivateKey, - Bytes: keyPair.PrivateKey.Bytes(), - }, - ) - return string(privateKeyPEM), nil + der, err := marshalMLKEMPrivatePKCS8(oidMLKEM1024, keyPair.PrivateKey.Bytes()) + if err != nil { + return "", fmt.Errorf("marshal ML-KEM-1024 PKCS#8 failed: %w", err) + } + return string(pem.EncodeToMemory(&pem.Block{Type: pemBlockPrivateKey, Bytes: der})), nil } func (keyPair MLKEM1024KeyPair) PublicKeyInPemFormat() (string, error) { @@ -595,13 +589,11 @@ func (keyPair MLKEM1024KeyPair) PublicKeyInPemFormat() (string, error) { return "", errors.New("failed to generate PEM formatted public key") } - publicKeyPEM := pem.EncodeToMemory( - &pem.Block{ - Type: PEMBlockMLKEM1024PublicKey, - Bytes: keyPair.PrivateKey.EncapsulationKey().Bytes(), - }, - ) - return string(publicKeyPEM), nil + der, err := marshalMLKEMPublicSPKI(oidMLKEM1024, keyPair.PrivateKey.EncapsulationKey().Bytes()) + if err != nil { + return "", fmt.Errorf("marshal ML-KEM-1024 SPKI failed: %w", err) + } + return string(pem.EncodeToMemory(&pem.Block{Type: pemBlockPublicKey, Bytes: der})), nil } func (keyPair MLKEM1024KeyPair) GetKeyType() KeyType { diff --git a/lib/ocrypto/mlkem.go b/lib/ocrypto/mlkem.go index e87150aebf..aca3f9b610 100644 --- a/lib/ocrypto/mlkem.go +++ b/lib/ocrypto/mlkem.go @@ -5,12 +5,24 @@ import ( "crypto/sha256" "encoding/asn1" "encoding/pem" + "errors" "fmt" "io" "golang.org/x/crypto/hkdf" ) +// PEM block types defined by RFC 7468 for SPKI / PKCS#8 envelopes. +const ( + pemBlockPublicKey = "PUBLIC KEY" + pemBlockPrivateKey = "PRIVATE KEY" +) + +// errNotMLKEM is returned by the ML-KEM SPKI / PKCS#8 parsers when the supplied +// DER blob is not an ML-KEM key, signalling the caller to fall through to +// other algorithm parsers. +var errNotMLKEM = errors.New("not an ML-KEM key") + const ( MLKEM768PublicKeySize = 1184 // mlkem768 encapsulation key MLKEM768PrivateKeySize = 64 // mlkem768 seed (d || z) @@ -20,13 +32,107 @@ const ( MLKEM1024CiphertextSize = 1568 // mlkem1024 ciphertext mlkemWrapKeySize = 32 // AES-256 key size for wrap key derivation +) - PEMBlockMLKEM768PublicKey = "MLKEM768 PUBLIC KEY" - PEMBlockMLKEM768PrivateKey = "MLKEM768 PRIVATE KEY" - PEMBlockMLKEM1024PublicKey = "MLKEM1024 PUBLIC KEY" - PEMBlockMLKEM1024PrivateKey = "MLKEM1024 PRIVATE KEY" +// NIST-assigned OIDs for ML-KEM (FIPS 203). +var ( + oidMLKEM768 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 4, 2} + oidMLKEM1024 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 4, 3} ) +type mlkemAlgorithmIdentifier struct { + Algorithm asn1.ObjectIdentifier +} + +type mlkemSPKI struct { + Algorithm mlkemAlgorithmIdentifier + PublicKey asn1.BitString +} + +// mlkemPKCS8 mirrors RFC 5958 OneAsymmetricKey v1. +type mlkemPKCS8 struct { + Version int + Algorithm mlkemAlgorithmIdentifier + PrivateKey []byte +} + +const bitsPerByte = 8 + +// marshalMLKEMPublicSPKI encodes a raw ML-KEM encapsulation key as RFC 5280 SubjectPublicKeyInfo. +func marshalMLKEMPublicSPKI(oid asn1.ObjectIdentifier, rawKey []byte) ([]byte, error) { + return asn1.Marshal(mlkemSPKI{ + Algorithm: mlkemAlgorithmIdentifier{Algorithm: oid}, + PublicKey: asn1.BitString{Bytes: rawKey, BitLength: len(rawKey) * bitsPerByte}, + }) +} + +// marshalMLKEMPrivatePKCS8 encodes the ML-KEM seed as RFC 5958 OneAsymmetricKey, +// with the inner ML-KEM-PrivateKey CHOICE selected as [0] IMPLICIT OCTET STRING (seed). +func marshalMLKEMPrivatePKCS8(oid asn1.ObjectIdentifier, seed []byte) ([]byte, error) { + inner, err := asn1.MarshalWithParams(seed, "tag:0,implicit") + if err != nil { + return nil, fmt.Errorf("asn1.MarshalWithParams seed failed: %w", err) + } + return asn1.Marshal(mlkemPKCS8{ + Version: 0, + Algorithm: mlkemAlgorithmIdentifier{Algorithm: oid}, + PrivateKey: inner, + }) +} + +// parseMLKEMPublicSPKI returns the OID and raw encapsulation key bytes from an +// SPKI DER blob if the algorithm is ML-KEM-768 or ML-KEM-1024. If the blob is +// not ML-KEM the sentinel errNotMLKEM is returned so the caller can fall +// through to other parsers. +func parseMLKEMPublicSPKI(der []byte) (asn1.ObjectIdentifier, []byte, error) { + var s mlkemSPKI + rest, err := asn1.Unmarshal(der, &s) + if err != nil || len(rest) != 0 { + return nil, nil, errNotMLKEM + } + var oid asn1.ObjectIdentifier + switch { + case s.Algorithm.Algorithm.Equal(oidMLKEM768): + oid = oidMLKEM768 + case s.Algorithm.Algorithm.Equal(oidMLKEM1024): + oid = oidMLKEM1024 + default: + return nil, nil, errNotMLKEM + } + if s.PublicKey.BitLength%bitsPerByte != 0 { + return nil, nil, errors.New("ML-KEM SPKI bit string is not byte-aligned") + } + return oid, s.PublicKey.RightAlign(), nil +} + +// parseMLKEMPrivatePKCS8 returns the OID and raw seed bytes from a PKCS#8 DER +// blob if the algorithm is ML-KEM-768 or ML-KEM-1024. If the blob is not +// ML-KEM the sentinel errNotMLKEM is returned so the caller can fall through +// to other parsers. +func parseMLKEMPrivatePKCS8(der []byte) (asn1.ObjectIdentifier, []byte, error) { + var p mlkemPKCS8 + rest, err := asn1.Unmarshal(der, &p) + if err != nil || len(rest) != 0 { + return nil, nil, errNotMLKEM + } + var oid asn1.ObjectIdentifier + switch { + case p.Algorithm.Algorithm.Equal(oidMLKEM768): + oid = oidMLKEM768 + case p.Algorithm.Algorithm.Equal(oidMLKEM1024): + oid = oidMLKEM1024 + default: + return nil, nil, errNotMLKEM + } + + var innerSeed []byte + innerRest, err := asn1.UnmarshalWithParams(p.PrivateKey, &innerSeed, "tag:0,implicit") + if err != nil || len(innerRest) != 0 { + return nil, nil, fmt.Errorf("ML-KEM PKCS#8 inner seed parse failed: %w", err) + } + return oid, innerSeed, nil +} + type MLKEMWrappedKey struct { MLKEMCiphertext []byte `asn1:"tag:0"` EncryptedDEK []byte `asn1:"tag:1"` @@ -73,11 +179,11 @@ func (e *MLKEMEncryptor768) Encrypt(data []byte) ([]byte, error) { } func (e *MLKEMEncryptor768) PublicKeyInPemFormat() (string, error) { - pemBlock := &pem.Block{ - Type: PEMBlockMLKEM768PublicKey, - Bytes: e.publicKey, + der, err := marshalMLKEMPublicSPKI(oidMLKEM768, e.publicKey) + if err != nil { + return "", fmt.Errorf("marshal ML-KEM-768 SPKI failed: %w", err) } - return string(pem.EncodeToMemory(pemBlock)), nil + return string(pem.EncodeToMemory(&pem.Block{Type: pemBlockPublicKey, Bytes: der})), nil } func (e *MLKEMEncryptor768) Type() SchemeType { @@ -133,11 +239,11 @@ func (e *MLKEMEncryptor1024) Encrypt(data []byte) ([]byte, error) { } func (e *MLKEMEncryptor1024) PublicKeyInPemFormat() (string, error) { - pemBlock := &pem.Block{ - Type: PEMBlockMLKEM1024PublicKey, - Bytes: e.publicKey, + der, err := marshalMLKEMPublicSPKI(oidMLKEM1024, e.publicKey) + if err != nil { + return "", fmt.Errorf("marshal ML-KEM-1024 SPKI failed: %w", err) } - return string(pem.EncodeToMemory(pemBlock)), nil + return string(pem.EncodeToMemory(&pem.Block{Type: pemBlockPublicKey, Bytes: der})), nil } func (e *MLKEMEncryptor1024) Type() SchemeType { diff --git a/lib/ocrypto/mlkem_test.go b/lib/ocrypto/mlkem_test.go index bc85f72dad..185c568cc8 100644 --- a/lib/ocrypto/mlkem_test.go +++ b/lib/ocrypto/mlkem_test.go @@ -2,6 +2,8 @@ package ocrypto import ( "encoding/asn1" + "encoding/pem" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -206,3 +208,71 @@ func TestMLKEM1024EncapsulateInvalidKeySize(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "invalid ML-KEM-1024 public key size") } + +func TestMLKEM768PEMRoundTrip(t *testing.T) { + keyPair, err := NewMLKEMKeyPair() + require.NoError(t, err) + + pubPEM, err := keyPair.PublicKeyInPemFormat() + require.NoError(t, err) + assert.True(t, strings.HasPrefix(pubPEM, "-----BEGIN PUBLIC KEY-----")) + pubBlock, _ := pem.Decode([]byte(pubPEM)) + require.NotNil(t, pubBlock) + assert.Equal(t, "PUBLIC KEY", pubBlock.Type) + + privPEM, err := keyPair.PrivateKeyInPemFormat() + require.NoError(t, err) + assert.True(t, strings.HasPrefix(privPEM, "-----BEGIN PRIVATE KEY-----")) + privBlock, _ := pem.Decode([]byte(privPEM)) + require.NotNil(t, privBlock) + assert.Equal(t, "PRIVATE KEY", privBlock.Type) + + enc, err := FromPublicPEM(pubPEM) + require.NoError(t, err) + assert.Equal(t, MLKEM, enc.Type()) + assert.Equal(t, MLKEM768Key, enc.KeyType()) + + dek := []byte("ml-kem-768 round-trip data") + wrapped, err := enc.Encrypt(dek) + require.NoError(t, err) + + dec, err := FromPrivatePEM(privPEM) + require.NoError(t, err) + plaintext, err := dec.Decrypt(wrapped) + require.NoError(t, err) + assert.Equal(t, dek, plaintext) +} + +func TestMLKEM1024PEMRoundTrip(t *testing.T) { + keyPair, err := NewMLKEM1024KeyPair() + require.NoError(t, err) + + pubPEM, err := keyPair.PublicKeyInPemFormat() + require.NoError(t, err) + assert.True(t, strings.HasPrefix(pubPEM, "-----BEGIN PUBLIC KEY-----")) + pubBlock, _ := pem.Decode([]byte(pubPEM)) + require.NotNil(t, pubBlock) + assert.Equal(t, "PUBLIC KEY", pubBlock.Type) + + privPEM, err := keyPair.PrivateKeyInPemFormat() + require.NoError(t, err) + assert.True(t, strings.HasPrefix(privPEM, "-----BEGIN PRIVATE KEY-----")) + privBlock, _ := pem.Decode([]byte(privPEM)) + require.NotNil(t, privBlock) + assert.Equal(t, "PRIVATE KEY", privBlock.Type) + + enc, err := FromPublicPEM(pubPEM) + require.NoError(t, err) + assert.Equal(t, MLKEM, enc.Type()) + assert.Equal(t, MLKEM1024Key, enc.KeyType()) + + dek := []byte("ml-kem-1024 round-trip data") + wrapped, err := enc.Encrypt(dek) + require.NoError(t, err) + + dec, err := FromPrivatePEM(privPEM) + require.NoError(t, err) + plaintext, err := dec.Decrypt(wrapped) + require.NoError(t, err) + assert.Equal(t, dek, plaintext) +} diff --git a/lib/ocrypto/rsa_key_pair.go b/lib/ocrypto/rsa_key_pair.go index 914eb8f80c..5294e10c13 100644 --- a/lib/ocrypto/rsa_key_pair.go +++ b/lib/ocrypto/rsa_key_pair.go @@ -41,7 +41,7 @@ func (keyPair RsaKeyPair) PrivateKeyInPemFormat() (string, error) { privateKeyPem := pem.EncodeToMemory( &pem.Block{ - Type: "PRIVATE KEY", + Type: pemBlockPrivateKey, Bytes: privateKeyBytes, }, ) @@ -61,7 +61,7 @@ func (keyPair RsaKeyPair) PublicKeyInPemFormat() (string, error) { publicKeyPem := pem.EncodeToMemory( &pem.Block{ - Type: "PUBLIC KEY", + Type: pemBlockPublicKey, Bytes: publicKeyBytes, }, ) From 87600b458b292fda8abaa11e10641d3a16be0997 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Fri, 29 May 2026 15:15:45 -0400 Subject: [PATCH 19/25] feat(sdk): emit mlkem-wrapped type for pure ML-KEM KAOs Add support for generating KAOs with type=mlkem-wrapped when using pure ML-KEM wrapping keys (MLKEM768, MLKEM1024), while maintaining backwards compatibility for reading type=wrapped KAOs. Changes: - Add IsMLKEMKeyType() helper in lib/ocrypto/ec_key_pair.go - Add kMLKEMWrapped constant and ML-KEM case to createKeyAccess() in sdk/tdf.go - Implement generateWrapKeyWithMLKEM() in sdk/tdf.go - Add ML-KEM handling to wrapKeyWithPublicKey() in sdk/experimental/tdf/key_access.go - Implement wrapKeyWithMLKEM() in sdk/experimental/tdf/key_access.go This ensures integration tests pass which expect mlkem-wrapped type for pure ML-KEM keys, while type=wrapped continues for RSA keys. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Dave Mihalcik --- lib/ocrypto/ec_key_pair.go | 9 +++++++++ sdk/experimental/tdf/key_access.go | 24 ++++++++++++++++++++++ sdk/tdf.go | 32 +++++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/lib/ocrypto/ec_key_pair.go b/lib/ocrypto/ec_key_pair.go index d28ad46799..d55d5f266c 100644 --- a/lib/ocrypto/ec_key_pair.go +++ b/lib/ocrypto/ec_key_pair.go @@ -122,6 +122,15 @@ func IsRSAKeyType(kt KeyType) bool { } } +func IsMLKEMKeyType(kt KeyType) bool { + switch kt { //nolint:exhaustive // only handle mlkem types + case MLKEM768Key, MLKEM1024Key: + return true + default: + return false + } +} + // GetECCurveFromECCMode return elliptic curve from ecc mode func GetECCurveFromECCMode(mode ECCMode) (elliptic.Curve, error) { var c elliptic.Curve diff --git a/sdk/experimental/tdf/key_access.go b/sdk/experimental/tdf/key_access.go index 3974b06cf4..ec26aa6692 100644 --- a/sdk/experimental/tdf/key_access.go +++ b/sdk/experimental/tdf/key_access.go @@ -172,6 +172,10 @@ func wrapKeyWithPublicKey(symKey []byte, pubKeyInfo keysplit.KASPublicKey) (stri // Handle EC key wrapping return wrapKeyWithEC(ktype, pubKeyInfo.PEM, symKey) } + if ocrypto.IsMLKEMKeyType(ktype) { + // Handle ML-KEM key wrapping + return wrapKeyWithMLKEM(ktype, pubKeyInfo.PEM, symKey) + } // Handle RSA key wrapping wrapped, err := wrapKeyWithRSA(pubKeyInfo.PEM, symKey) return wrapped, "wrapped", "", err @@ -256,3 +260,23 @@ func wrapKeyWithHybrid(ktype ocrypto.KeyType, kasPublicKeyPEM string, symKey []b } return string(ocrypto.Base64Encode(wrappedDER)), "hybrid-wrapped", "", nil } + +func wrapKeyWithMLKEM(ktype ocrypto.KeyType, kasPublicKeyPEM string, symKey []byte) (string, string, string, error) { + var wrappedDER []byte + var err error + + switch ktype { + case ocrypto.MLKEM768Key: + wrappedDER, err = ocrypto.MLKEM768WrapDEK([]byte(kasPublicKeyPEM), symKey) + case ocrypto.MLKEM1024Key: + wrappedDER, err = ocrypto.MLKEM1024WrapDEK([]byte(kasPublicKeyPEM), symKey) + default: + return "", "", "", fmt.Errorf("unsupported ML-KEM key type: %s", ktype) + } + + if err != nil { + return "", "", "", fmt.Errorf("mlkem wrap failed: %w", err) + } + + return string(ocrypto.Base64Encode(wrappedDER)), "mlkem-wrapped", "", nil +} diff --git a/sdk/tdf.go b/sdk/tdf.go index 1d6ef1182c..bcee2846f1 100644 --- a/sdk/tdf.go +++ b/sdk/tdf.go @@ -44,6 +44,7 @@ const ( kWrapped = "wrapped" kECWrapped = "ec-wrapped" kHybridWrapped = "hybrid-wrapped" + kMLKEMWrapped = "mlkem-wrapped" kKasProtocol = "kas" kSplitKeyType = "split" kGCMCipherAlgorithm = "AES-256-GCM" @@ -695,6 +696,13 @@ func createKeyAccess(kasInfo KASInfo, symKey []byte, policyBinding PolicyBinding keyAccess.KeyType = kECWrapped keyAccess.WrappedKey = wrappedKeyInfo.wrappedKey keyAccess.EphemeralPublicKey = wrappedKeyInfo.publicKey + case ocrypto.IsMLKEMKeyType(ktype): + wrappedKey, err := generateWrapKeyWithMLKEM(kasInfo.Algorithm, kasInfo.PublicKey, symKey) + if err != nil { + return KeyAccess{}, err + } + keyAccess.KeyType = kMLKEMWrapped + keyAccess.WrappedKey = wrappedKey default: wrappedKey, err := generateWrapKeyWithRSA(kasInfo.PublicKey, symKey) if err != nil { @@ -778,6 +786,28 @@ func generateWrapKeyWithHybrid(algorithm, publicKeyPEM string, symKey []byte) (s return string(ocrypto.Base64Encode(wrappedDER)), nil } +func generateWrapKeyWithMLKEM(algorithm, publicKeyPEM string, symKey []byte) (string, error) { + ktype := ocrypto.KeyType(algorithm) + + var wrappedDER []byte + var err error + + switch ktype { + case ocrypto.MLKEM768Key: + wrappedDER, err = ocrypto.MLKEM768WrapDEK([]byte(publicKeyPEM), symKey) + case ocrypto.MLKEM1024Key: + wrappedDER, err = ocrypto.MLKEM1024WrapDEK([]byte(publicKeyPEM), symKey) + default: + return "", fmt.Errorf("unsupported ML-KEM key type: %s", algorithm) + } + + if err != nil { + return "", fmt.Errorf("generateWrapKeyWithMLKEM: %w", err) + } + + return string(ocrypto.Base64Encode(wrappedDER)), nil +} + // create policy object func createPolicyObject(attributes []AttributeValueFQN) (PolicyObject, error) { uuidObj, err := uuid.NewUUID() @@ -1247,7 +1277,7 @@ func createRewrapRequest(_ context.Context, r *Reader) (map[string]*kas.Unsigned invalidPolicy = !ok alg, ok = policyBinding["alg"].(string) invalidPolicy = invalidPolicy || !ok - case (PolicyBinding): + case PolicyBinding: hash = policyBinding.Hash alg = policyBinding.Alg default: From 292e3181f7f93dc49ea4bb792f28d4cc9e3fa76d Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Mon, 1 Jun 2026 16:57:20 -0400 Subject: [PATCH 20/25] feat(ocrypto): add format auto-detection for ML-KEM public keys Make MLKEM768WrapDEK and MLKEM1024WrapDEK accept multiple input formats: - Raw key bytes (1184/1568 bytes) - fast path - SPKI DER (1206/1590 bytes) - PEM-wrapped SPKI (~1686/~2206 bytes) This fixes the issue introduced in 6a7480dc where KAS started returning SPKI-encoded PEM keys but callers expected raw bytes. Instead of requiring all callers to decode manually, the crypto library now handles format detection transparently. Changes: - Add normalizeMLKEMPublicKey() helper for format detection - Export ParseMLKEMPublicSPKI() and OidMLKEM768/OidMLKEM1024 constants - Update all internal references to use exported names - Add comprehensive tests for format handling Benefits: - Backward compatible (raw keys still work) - Simpler callers (no manual PEM decoding needed) - Better encapsulation (format logic in crypto library) - Future-proof (handles new formats automatically) Co-Authored-By: Claude Sonnet 4.5 --- lib/ocrypto/asym_decryption.go | 4 +- lib/ocrypto/asym_encryption.go | 6 +- lib/ocrypto/ec_key_pair.go | 8 +- lib/ocrypto/mlkem.go | 90 +++++++++++++++++----- lib/ocrypto/mlkem_format_test.go | 124 +++++++++++++++++++++++++++++++ 5 files changed, 205 insertions(+), 27 deletions(-) create mode 100644 lib/ocrypto/mlkem_format_test.go diff --git a/lib/ocrypto/asym_decryption.go b/lib/ocrypto/asym_decryption.go index 93e2b158b0..a5bb5ce155 100644 --- a/lib/ocrypto/asym_decryption.go +++ b/lib/ocrypto/asym_decryption.go @@ -53,9 +53,9 @@ func FromPrivatePEMWithSalt(privateKeyInPem string, salt, info []byte) (PrivateK } switch oid, seed, err := parseMLKEMPrivatePKCS8(block.Bytes); { - case err == nil && oid.Equal(oidMLKEM768): + case err == nil && oid.Equal(OidMLKEM768): return NewSaltedMLKEM768Decryptor(seed, salt, info) - case err == nil && oid.Equal(oidMLKEM1024): + case err == nil && oid.Equal(OidMLKEM1024): return NewSaltedMLKEM1024Decryptor(seed, salt, info) case err != nil && !errors.Is(err, errNotMLKEM): return AsymDecryption{}, err diff --git a/lib/ocrypto/asym_encryption.go b/lib/ocrypto/asym_encryption.go index dd90543df2..86ee385b96 100644 --- a/lib/ocrypto/asym_encryption.go +++ b/lib/ocrypto/asym_encryption.go @@ -84,10 +84,10 @@ func FromPublicPEMWithSalt(publicKeyInPem string, salt, info []byte) (PublicKeyE return NewP384MLKEM1024Encryptor(block.Bytes, salt, info) } - switch oid, key, err := parseMLKEMPublicSPKI(block.Bytes); { - case err == nil && oid.Equal(oidMLKEM768): + switch oid, key, err := ParseMLKEMPublicSPKI(block.Bytes); { + case err == nil && oid.Equal(OidMLKEM768): return NewMLKEM768Encryptor(key, salt, info) - case err == nil && oid.Equal(oidMLKEM1024): + case err == nil && oid.Equal(OidMLKEM1024): return NewMLKEM1024Encryptor(key, salt, info) case err != nil && !errors.Is(err, errNotMLKEM): return nil, err diff --git a/lib/ocrypto/ec_key_pair.go b/lib/ocrypto/ec_key_pair.go index d55d5f266c..976089c3c3 100644 --- a/lib/ocrypto/ec_key_pair.go +++ b/lib/ocrypto/ec_key_pair.go @@ -558,7 +558,7 @@ func (keyPair MLKEMKeyPair) PrivateKeyInPemFormat() (string, error) { return "", errors.New("failed to generate PEM formatted private key") } - der, err := marshalMLKEMPrivatePKCS8(oidMLKEM768, keyPair.PrivateKey.Bytes()) + der, err := marshalMLKEMPrivatePKCS8(OidMLKEM768, keyPair.PrivateKey.Bytes()) if err != nil { return "", fmt.Errorf("marshal ML-KEM-768 PKCS#8 failed: %w", err) } @@ -570,7 +570,7 @@ func (keyPair MLKEMKeyPair) PublicKeyInPemFormat() (string, error) { return "", errors.New("failed to generate PEM formatted public key") } - der, err := marshalMLKEMPublicSPKI(oidMLKEM768, keyPair.PrivateKey.EncapsulationKey().Bytes()) + der, err := marshalMLKEMPublicSPKI(OidMLKEM768, keyPair.PrivateKey.EncapsulationKey().Bytes()) if err != nil { return "", fmt.Errorf("marshal ML-KEM-768 SPKI failed: %w", err) } @@ -586,7 +586,7 @@ func (keyPair MLKEM1024KeyPair) PrivateKeyInPemFormat() (string, error) { return "", errors.New("failed to generate PEM formatted private key") } - der, err := marshalMLKEMPrivatePKCS8(oidMLKEM1024, keyPair.PrivateKey.Bytes()) + der, err := marshalMLKEMPrivatePKCS8(OidMLKEM1024, keyPair.PrivateKey.Bytes()) if err != nil { return "", fmt.Errorf("marshal ML-KEM-1024 PKCS#8 failed: %w", err) } @@ -598,7 +598,7 @@ func (keyPair MLKEM1024KeyPair) PublicKeyInPemFormat() (string, error) { return "", errors.New("failed to generate PEM formatted public key") } - der, err := marshalMLKEMPublicSPKI(oidMLKEM1024, keyPair.PrivateKey.EncapsulationKey().Bytes()) + der, err := marshalMLKEMPublicSPKI(OidMLKEM1024, keyPair.PrivateKey.EncapsulationKey().Bytes()) if err != nil { return "", fmt.Errorf("marshal ML-KEM-1024 SPKI failed: %w", err) } diff --git a/lib/ocrypto/mlkem.go b/lib/ocrypto/mlkem.go index aca3f9b610..a2264ad83f 100644 --- a/lib/ocrypto/mlkem.go +++ b/lib/ocrypto/mlkem.go @@ -1,6 +1,7 @@ package ocrypto import ( + "bytes" "crypto/mlkem" "crypto/sha256" "encoding/asn1" @@ -36,8 +37,8 @@ const ( // NIST-assigned OIDs for ML-KEM (FIPS 203). var ( - oidMLKEM768 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 4, 2} - oidMLKEM1024 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 4, 3} + OidMLKEM768 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 4, 2} + OidMLKEM1024 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 4, 3} ) type mlkemAlgorithmIdentifier struct { @@ -80,11 +81,11 @@ func marshalMLKEMPrivatePKCS8(oid asn1.ObjectIdentifier, seed []byte) ([]byte, e }) } -// parseMLKEMPublicSPKI returns the OID and raw encapsulation key bytes from an +// ParseMLKEMPublicSPKI returns the OID and raw encapsulation key bytes from an // SPKI DER blob if the algorithm is ML-KEM-768 or ML-KEM-1024. If the blob is // not ML-KEM the sentinel errNotMLKEM is returned so the caller can fall // through to other parsers. -func parseMLKEMPublicSPKI(der []byte) (asn1.ObjectIdentifier, []byte, error) { +func ParseMLKEMPublicSPKI(der []byte) (asn1.ObjectIdentifier, []byte, error) { var s mlkemSPKI rest, err := asn1.Unmarshal(der, &s) if err != nil || len(rest) != 0 { @@ -92,10 +93,10 @@ func parseMLKEMPublicSPKI(der []byte) (asn1.ObjectIdentifier, []byte, error) { } var oid asn1.ObjectIdentifier switch { - case s.Algorithm.Algorithm.Equal(oidMLKEM768): - oid = oidMLKEM768 - case s.Algorithm.Algorithm.Equal(oidMLKEM1024): - oid = oidMLKEM1024 + case s.Algorithm.Algorithm.Equal(OidMLKEM768): + oid = OidMLKEM768 + case s.Algorithm.Algorithm.Equal(OidMLKEM1024): + oid = OidMLKEM1024 default: return nil, nil, errNotMLKEM } @@ -117,10 +118,10 @@ func parseMLKEMPrivatePKCS8(der []byte) (asn1.ObjectIdentifier, []byte, error) { } var oid asn1.ObjectIdentifier switch { - case p.Algorithm.Algorithm.Equal(oidMLKEM768): - oid = oidMLKEM768 - case p.Algorithm.Algorithm.Equal(oidMLKEM1024): - oid = oidMLKEM1024 + case p.Algorithm.Algorithm.Equal(OidMLKEM768): + oid = OidMLKEM768 + case p.Algorithm.Algorithm.Equal(OidMLKEM1024): + oid = OidMLKEM1024 default: return nil, nil, errNotMLKEM } @@ -179,7 +180,7 @@ func (e *MLKEMEncryptor768) Encrypt(data []byte) ([]byte, error) { } func (e *MLKEMEncryptor768) PublicKeyInPemFormat() (string, error) { - der, err := marshalMLKEMPublicSPKI(oidMLKEM768, e.publicKey) + der, err := marshalMLKEMPublicSPKI(OidMLKEM768, e.publicKey) if err != nil { return "", fmt.Errorf("marshal ML-KEM-768 SPKI failed: %w", err) } @@ -239,7 +240,7 @@ func (e *MLKEMEncryptor1024) Encrypt(data []byte) ([]byte, error) { } func (e *MLKEMEncryptor1024) PublicKeyInPemFormat() (string, error) { - der, err := marshalMLKEMPublicSPKI(oidMLKEM1024, e.publicKey) + der, err := marshalMLKEMPublicSPKI(OidMLKEM1024, e.publicKey) if err != nil { return "", fmt.Errorf("marshal ML-KEM-1024 SPKI failed: %w", err) } @@ -282,16 +283,69 @@ func (d *MLKEMDecryptor1024) Decrypt(data []byte) ([]byte, error) { return mlkem1024UnwrapDEK(d.privateKey, data, d.salt, d.info) } -func MLKEM768WrapDEK(publicKeyRaw, dek []byte) ([]byte, error) { - return mlkem768WrapDEK(publicKeyRaw, dek, defaultTDFSalt(), nil) +// normalizeMLKEMPublicKey detects the input format and returns raw key bytes. +// Accepts: raw key (1184/1568 bytes), SPKI DER (1206/1590 bytes), or PEM-wrapped SPKI. +func normalizeMLKEMPublicKey(input []byte, expectedRawSize int, expectedOID asn1.ObjectIdentifier) ([]byte, error) { + // Fast path: already raw? + if len(input) == expectedRawSize { + return input, nil + } + + // Check for PEM format + if bytes.HasPrefix(input, []byte("-----BEGIN")) { + block, _ := pem.Decode(input) + if block == nil { + return nil, errors.New("failed to decode PEM block") + } + if block.Type != pemBlockPublicKey { + return nil, fmt.Errorf("expected %s PEM block, got %s", pemBlockPublicKey, block.Type) + } + // Continue with DER bytes + input = block.Bytes + } + + // Try parsing as SPKI DER + oid, rawKey, err := ParseMLKEMPublicSPKI(input) + if err != nil { + if errors.Is(err, errNotMLKEM) { + return nil, errors.New("not an ML-KEM key in SPKI format") + } + return nil, fmt.Errorf("failed to parse SPKI: %w", err) + } + + // Verify OID matches expected variant + if !oid.Equal(expectedOID) { + return nil, fmt.Errorf("OID mismatch: expected %v, got %v", expectedOID, oid) + } + + // Verify extracted key is correct size + if len(rawKey) != expectedRawSize { + return nil, fmt.Errorf("extracted key has wrong size: got %d want %d", len(rawKey), expectedRawSize) + } + + return rawKey, nil +} + +func MLKEM768WrapDEK(publicKey, dek []byte) ([]byte, error) { + // Normalize input to raw key bytes (handles raw, SPKI DER, or PEM) + rawKey, err := normalizeMLKEMPublicKey(publicKey, MLKEM768PublicKeySize, OidMLKEM768) + if err != nil { + return nil, fmt.Errorf("invalid ML-KEM-768 public key: %w", err) + } + return mlkem768WrapDEK(rawKey, dek, defaultTDFSalt(), nil) } func MLKEM768UnwrapDEK(privateKeyRaw, wrappedDER []byte) ([]byte, error) { return mlkem768UnwrapDEK(privateKeyRaw, wrappedDER, defaultTDFSalt(), nil) } -func MLKEM1024WrapDEK(publicKeyRaw, dek []byte) ([]byte, error) { - return mlkem1024WrapDEK(publicKeyRaw, dek, defaultTDFSalt(), nil) +func MLKEM1024WrapDEK(publicKey, dek []byte) ([]byte, error) { + // Normalize input to raw key bytes (handles raw, SPKI DER, or PEM) + rawKey, err := normalizeMLKEMPublicKey(publicKey, MLKEM1024PublicKeySize, OidMLKEM1024) + if err != nil { + return nil, fmt.Errorf("invalid ML-KEM-1024 public key: %w", err) + } + return mlkem1024WrapDEK(rawKey, dek, defaultTDFSalt(), nil) } func MLKEM1024UnwrapDEK(privateKeyRaw, wrappedDER []byte) ([]byte, error) { diff --git a/lib/ocrypto/mlkem_format_test.go b/lib/ocrypto/mlkem_format_test.go new file mode 100644 index 0000000000..13b74d39f7 --- /dev/null +++ b/lib/ocrypto/mlkem_format_test.go @@ -0,0 +1,124 @@ +package ocrypto + +import ( + "encoding/pem" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestMLKEM768WrapDEKFormats verifies that MLKEM768WrapDEK accepts raw, SPKI DER, and PEM formats +func TestMLKEM768WrapDEKFormats(t *testing.T) { + keyPair, err := NewMLKEMKeyPair() + require.NoError(t, err) + + dek := []byte("0123456789abcdef0123456789abcdef") + + // Test 1: Raw key (1184 bytes) + rawKey := keyPair.PrivateKey.EncapsulationKey().Bytes() + require.Len(t, rawKey, MLKEM768PublicKeySize) + + wrappedFromRaw, err := MLKEM768WrapDEK(rawKey, dek) + require.NoError(t, err, "Should accept raw key") + + // Test 2: SPKI DER (1206 bytes) + spkiDER, err := marshalMLKEMPublicSPKI(OidMLKEM768, rawKey) + require.NoError(t, err) + require.Greater(t, len(spkiDER), len(rawKey), "SPKI DER should be larger than raw key") + + wrappedFromSPKI, err := MLKEM768WrapDEK(spkiDER, dek) + require.NoError(t, err, "Should accept SPKI DER") + + // Test 3: PEM-wrapped SPKI (~1686 bytes) + pemBytes := pem.EncodeToMemory(&pem.Block{Type: pemBlockPublicKey, Bytes: spkiDER}) + require.Greater(t, len(pemBytes), len(spkiDER), "PEM should be larger than DER") + + wrappedFromPEM, err := MLKEM768WrapDEK(pemBytes, dek) + require.NoError(t, err, "Should accept PEM-wrapped SPKI") + + // Verify we can unwrap all three (ML-KEM uses randomness, so wrapped results differ each time) + privateKeyBytes := keyPair.PrivateKey.Bytes() + + plaintext1, err := MLKEM768UnwrapDEK(privateKeyBytes, wrappedFromRaw) + require.NoError(t, err, "Should unwrap from raw key wrapping") + assert.Equal(t, dek, plaintext1) + + plaintext2, err := MLKEM768UnwrapDEK(privateKeyBytes, wrappedFromSPKI) + require.NoError(t, err, "Should unwrap from SPKI DER wrapping") + assert.Equal(t, dek, plaintext2) + + plaintext3, err := MLKEM768UnwrapDEK(privateKeyBytes, wrappedFromPEM) + require.NoError(t, err, "Should unwrap from PEM wrapping") + assert.Equal(t, dek, plaintext3) +} + +// TestMLKEM1024WrapDEKFormats verifies that MLKEM1024WrapDEK accepts raw, SPKI DER, and PEM formats +func TestMLKEM1024WrapDEKFormats(t *testing.T) { + keyPair, err := NewMLKEM1024KeyPair() + require.NoError(t, err) + + dek := []byte("0123456789abcdef0123456789abcdef") + + // Test 1: Raw key (1568 bytes) + rawKey := keyPair.PrivateKey.EncapsulationKey().Bytes() + require.Len(t, rawKey, MLKEM1024PublicKeySize) + + wrappedFromRaw, err := MLKEM1024WrapDEK(rawKey, dek) + require.NoError(t, err, "Should accept raw key") + + // Test 2: SPKI DER (1590 bytes) + spkiDER, err := marshalMLKEMPublicSPKI(OidMLKEM1024, rawKey) + require.NoError(t, err) + require.Greater(t, len(spkiDER), len(rawKey), "SPKI DER should be larger than raw key") + + wrappedFromSPKI, err := MLKEM1024WrapDEK(spkiDER, dek) + require.NoError(t, err, "Should accept SPKI DER") + + // Test 3: PEM-wrapped SPKI (~2206 bytes) + pemBytes := pem.EncodeToMemory(&pem.Block{Type: pemBlockPublicKey, Bytes: spkiDER}) + require.Greater(t, len(pemBytes), len(spkiDER), "PEM should be larger than DER") + + wrappedFromPEM, err := MLKEM1024WrapDEK(pemBytes, dek) + require.NoError(t, err, "Should accept PEM-wrapped SPKI") + + // Verify we can unwrap all three (ML-KEM uses randomness, so wrapped results differ each time) + privateKeyBytes := keyPair.PrivateKey.Bytes() + + plaintext1, err := MLKEM1024UnwrapDEK(privateKeyBytes, wrappedFromRaw) + require.NoError(t, err, "Should unwrap from raw key wrapping") + assert.Equal(t, dek, plaintext1) + + plaintext2, err := MLKEM1024UnwrapDEK(privateKeyBytes, wrappedFromSPKI) + require.NoError(t, err, "Should unwrap from SPKI DER wrapping") + assert.Equal(t, dek, plaintext2) + + plaintext3, err := MLKEM1024UnwrapDEK(privateKeyBytes, wrappedFromPEM) + require.NoError(t, err, "Should unwrap from PEM wrapping") + assert.Equal(t, dek, plaintext3) +} + +// TestMLKEM768WrapDEKInvalidFormats verifies error handling for invalid inputs +func TestMLKEM768WrapDEKInvalidFormats(t *testing.T) { + dek := []byte("0123456789abcdef0123456789abcdef") + + // Wrong size raw key + wrongSizeRaw := make([]byte, 100) + _, err := MLKEM768WrapDEK(wrongSizeRaw, dek) + require.Error(t, err, "Should reject wrong-size raw key") + + // Wrong OID in SPKI + keyPair1024, err := NewMLKEM1024KeyPair() + require.NoError(t, err) + spki1024, err := marshalMLKEMPublicSPKI(OidMLKEM1024, keyPair1024.PrivateKey.EncapsulationKey().Bytes()) + require.NoError(t, err) + + _, err = MLKEM768WrapDEK(spki1024, dek) + require.Error(t, err, "Should reject ML-KEM-1024 SPKI when expecting ML-KEM-768") + assert.Contains(t, err.Error(), "OID mismatch") + + // Invalid PEM + invalidPEM := []byte("-----BEGIN PUBLIC KEY-----\ninvalid base64\n-----END PUBLIC KEY-----") + _, err = MLKEM768WrapDEK(invalidPEM, dek) + require.Error(t, err, "Should reject invalid PEM") +} From 41769fc10244c434033ed64775a077ffa2d1f583 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Tue, 2 Jun 2026 10:16:08 -0400 Subject: [PATCH 21/25] feat(kas): add mlkem-wrapped case to rewrap handler Adds support for pure ML-KEM key agreement objects in the KAS rewrap handler. The SDK now emits type="mlkem-wrapped" for MLKEM768/MLKEM1024 KAOs, and this change adds the corresponding case to handle decryption. Follows the hybrid-wrapped pattern: uses HybridTDFEnabled flag, no ephemeral key processing, and generic error messages per security guidelines. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Dave Mihalcik --- service/kas/access/rewrap.go | 59 +++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/service/kas/access/rewrap.go b/service/kas/access/rewrap.go index aba7ffa84e..c5e18b34b6 100644 --- a/service/kas/access/rewrap.go +++ b/service/kas/access/rewrap.go @@ -160,7 +160,8 @@ func (p *Provider) parseSRT(ctx context.Context, srt string) (jwt.Token, string, "unable to validate or parse token", slog.Any("error", err), slog.Int("srt_length", len(srt)), - jwkThumbprintAttr(ctxAuth.GetJWKFromContext(ctx, p.Logger))) + jwkThumbprintAttr(ctxAuth.GetJWKFromContext(ctx, p.Logger)), + ) return nil, "", err401("could not parse token") } @@ -187,7 +188,8 @@ func (p *Provider) logSRTValidationFailure(ctx context.Context, token jwt.Token, issuedAt := token.IssuedAt() if !issuedAt.IsZero() { - fields = append(fields, + fields = append( + fields, slog.Time("iat", issuedAt), slog.Duration("iat_delta", issuedAt.Sub(now)), ) @@ -198,7 +200,8 @@ func (p *Provider) logSRTValidationFailure(ctx context.Context, token jwt.Token, expires := token.Expiration() if !expires.IsZero() { - fields = append(fields, + fields = append( + fields, slog.Time("exp", expires), slog.Duration("exp_delta", now.Sub(expires)), ) @@ -209,7 +212,8 @@ func (p *Provider) logSRTValidationFailure(ctx context.Context, token jwt.Token, notBefore := token.NotBefore() if !notBefore.IsZero() { - fields = append(fields, + fields = append( + fields, slog.Time("nbf", notBefore), slog.Duration("nbf_delta", notBefore.Sub(now)), ) @@ -261,7 +265,8 @@ func (p *Provider) verifySRTSignature(ctx context.Context, srt string, dpopJWK j ) if err != nil { if p.Logger != nil { - p.Logger.WarnContext(ctx, + p.Logger.WarnContext( + ctx, "unable to verify request token", slog.Int("srt_length", len(srt)), jwkThumbprintAttr(dpopJWK), @@ -370,7 +375,8 @@ func (p *Provider) extractSRTBody(ctx context.Context, headers http.Header, in * err := protojson.UnmarshalOptions{DiscardUnknown: true}.Unmarshal([]byte(rbString), &requestBody) // if there are no requests then it could be a v1 request if err != nil { - p.Logger.WarnContext(ctx, + p.Logger.WarnContext( + ctx, "invalid SRT", slog.Any("err_v2", err), slog.Int("rb_string_length", len(rbString)), @@ -382,7 +388,8 @@ func (p *Provider) extractSRTBody(ctx context.Context, headers http.Header, in * var errv1 error if requestBody, errv1 = extractAndConvertV1SRTBody([]byte(rbString)); errv1 != nil { - p.Logger.WarnContext(ctx, + p.Logger.WarnContext( + ctx, "invalid SRT", slog.Any("err_v1", errv1), slog.Int("rb_string_length", len(rbString)), @@ -393,7 +400,8 @@ func (p *Provider) extractSRTBody(ctx context.Context, headers http.Header, in * isV1 = true } // TODO: this log is too big and should be reconsidered or removed - p.Logger.DebugContext(ctx, + p.Logger.DebugContext( + ctx, "extracted request body", slog.String("rewrap_body", requestBody.String()), slog.String("rewrap_srt", rbString), @@ -594,7 +602,8 @@ func (p *Provider) Rewrap(ctx context.Context, req *connect.Request[kaspb.Rewrap } kaoResults := *getMapValue(results) if len(kaoResults) != 1 { - p.Logger.WarnContext(ctx, + p.Logger.WarnContext( + ctx, "status 400 due to wrong result set size", slog.Any("kao_results", kaoResults), slog.Any("results", results), @@ -681,7 +690,8 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned // Get EC key size and convert to mode keySize, err := ocrypto.GetECKeySize([]byte(ephemeralPubKeyPEM)) if err != nil { - p.Logger.WarnContext(ctx, + p.Logger.WarnContext( + ctx, "failed to get EC key size", slog.Any("kao", kao), slog.Any("error", err), @@ -692,7 +702,8 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned mode, err := ocrypto.ECSizeToMode(keySize) if err != nil { - p.Logger.WarnContext(ctx, + p.Logger.WarnContext( + ctx, "failed to convert key size to mode", slog.Any("kao", kao), slog.Any("error", err), @@ -704,7 +715,8 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned // Parse the PEM public key block, _ := pem.Decode([]byte(ephemeralPubKeyPEM)) if block == nil { - p.Logger.WarnContext(ctx, + p.Logger.WarnContext( + ctx, "failed to decode PEM block", slog.Any("kao", kao), slog.Any("error", err), @@ -715,7 +727,8 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned pub, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { - p.Logger.WarnContext(ctx, + p.Logger.WarnContext( + ctx, "failed to parse public key", slog.Any("kao", kao), slog.Any("error", err), @@ -760,6 +773,20 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned failedKAORewrap(results, kao, err400("bad request")) continue } + case "mlkem-wrapped": + if !p.HybridTDFEnabled && !p.Preview.HybridTDFEnabled { + p.Logger.WarnContext(ctx, "mlkem-wrapped not enabled") + failedKAORewrap(results, kao, err400("bad request")) + continue + } + + kid := trust.KeyIdentifier(kao.GetKeyAccessObject().GetKid()) + dek, err = p.KeyDelegator.Decrypt(ctx, kid, kao.GetKeyAccessObject().GetWrappedKey(), nil) + if err != nil { + p.Logger.WarnContext(ctx, "failed to decrypt ML-KEM key", slog.Any("error", err)) + failedKAORewrap(results, kao, err400("bad request")) + continue + } case "wrapped": var kidsToCheck []trust.KeyIdentifier if kao.GetKeyAccessObject().GetKid() != "" { @@ -898,7 +925,8 @@ func (p *Provider) tdf3Rewrap(ctx context.Context, requests []*kaspb.UnsignedRew // Store per-KAO results even on error so tamper signals (e.g. corrupted // policy body → generic "bad request") reach the SDK rather than being // replaced by a top-level "invalid request". - p.Logger.WarnContext(ctx, + p.Logger.WarnContext( + ctx, "rewrap: verifyRewrapRequests failed", slog.String("policy_id", policyID), slog.Any("error", err), @@ -916,7 +944,8 @@ func (p *Provider) tdf3Rewrap(ctx context.Context, requests []*kaspb.UnsignedRew pdpAccessResults, accessErr := p.canAccess(ctx, tok, policies, additionalRewrapContext.Obligations.FulfillableFQNs) if accessErr != nil { - p.Logger.DebugContext(ctx, + p.Logger.DebugContext( + ctx, "tdf3rewrap: cannot access policy", slog.Any("policies", policies), slog.Any("error", accessErr), From d2a32e0510867458a5165305385281bc7b43362a Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Wed, 3 Jun 2026 08:48:52 -0400 Subject: [PATCH 22/25] fixup exhaustive nolint lines --- sdk/experimental/tdf/key_access.go | 2 +- sdk/tdf.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/experimental/tdf/key_access.go b/sdk/experimental/tdf/key_access.go index ec26aa6692..534aa39b6a 100644 --- a/sdk/experimental/tdf/key_access.go +++ b/sdk/experimental/tdf/key_access.go @@ -265,7 +265,7 @@ func wrapKeyWithMLKEM(ktype ocrypto.KeyType, kasPublicKeyPEM string, symKey []by var wrappedDER []byte var err error - switch ktype { + switch ktype { //nolint:exhaustive // only handle mlkem types case ocrypto.MLKEM768Key: wrappedDER, err = ocrypto.MLKEM768WrapDEK([]byte(kasPublicKeyPEM), symKey) case ocrypto.MLKEM1024Key: diff --git a/sdk/tdf.go b/sdk/tdf.go index bcee2846f1..2aea39637e 100644 --- a/sdk/tdf.go +++ b/sdk/tdf.go @@ -792,7 +792,7 @@ func generateWrapKeyWithMLKEM(algorithm, publicKeyPEM string, symKey []byte) (st var wrappedDER []byte var err error - switch ktype { + switch ktype { //nolint:exhaustive // only handle mlkem types case ocrypto.MLKEM768Key: wrappedDER, err = ocrypto.MLKEM768WrapDEK([]byte(publicKeyPEM), symKey) case ocrypto.MLKEM1024Key: From 68317c651608a54065e14ebf0cc1450dd21174df Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Wed, 3 Jun 2026 13:17:31 -0400 Subject: [PATCH 23/25] refactor(ocrypto): unify ML-KEM, X-Wing, and NIST hybrid wrap paths Collapse the three near-identical KEM wrap/unwrap implementations behind one OID-keyed registry and a single internal kem contract. ML-KEM-768, ML-KEM-1024, X-Wing, P-256+ML-KEM-768, and P-384+ML-KEM-1024 now share one envelope type, one wrap function, one unwrap function, and one encryptor/decryptor pair routed through FromPublicPEM / FromPrivatePEM. Service and SDK callers shrink accordingly: StandardXWingCrypto, StandardHybridCrypto, and StandardMLKEMCrypto fold into a single StandardKEMCrypto; the per-algorithm wrap dispatch in sdk/tdf.go and sdk/experimental/tdf/key_access.go collapses to one IsKEMKeyType branch calling ocrypto.WrapDEK. Wire formats are preserved byte-for-byte (hybrid-wrapped, mlkem-wrapped). The OID registry leaves the planned hybrid-PEM-to-SPKI follow-up as a near-zero change: three OID constants plus three registry entries. Co-Authored-By: Claude Opus 4.7 Signed-off-by: Dave Mihalcik --- lib/ocrypto/asym_decryption.go | 18 +- lib/ocrypto/asym_encryption.go | 18 +- lib/ocrypto/benchmark_test.go | 58 +- lib/ocrypto/ec_key_pair.go | 8 +- lib/ocrypto/hybrid_common.go | 77 +-- lib/ocrypto/hybrid_nist.go | 335 +----------- lib/ocrypto/hybrid_nist_test.go | 46 +- lib/ocrypto/kem.go | 482 +++++++++++++++++ lib/ocrypto/mlkem.go | 508 +++--------------- lib/ocrypto/mlkem_format_test.go | 6 +- lib/ocrypto/mlkem_test.go | 44 +- lib/ocrypto/xwing.go | 203 +------ lib/ocrypto/xwing_test.go | 33 +- sdk/experimental/tdf/key_access.go | 42 +- sdk/tdf.go | 48 +- service/internal/security/basic_manager.go | 82 +-- .../internal/security/in_process_provider.go | 16 +- service/internal/security/standard_crypto.go | 152 +----- 18 files changed, 818 insertions(+), 1358 deletions(-) create mode 100644 lib/ocrypto/kem.go diff --git a/lib/ocrypto/asym_decryption.go b/lib/ocrypto/asym_decryption.go index a5bb5ce155..9fa66860ae 100644 --- a/lib/ocrypto/asym_decryption.go +++ b/lib/ocrypto/asym_decryption.go @@ -45,19 +45,19 @@ func FromPrivatePEMWithSalt(privateKeyInPem string, salt, info []byte) (PrivateK } switch block.Type { case PEMBlockXWingPrivateKey: - return NewSaltedXWingDecryptor(block.Bytes, salt, info) + return newKEMDecryptor(xwingKEM{}, block.Bytes, salt, info) case PEMBlockP256MLKEM768PrivateKey: - return NewSaltedP256MLKEM768Decryptor(block.Bytes, salt, info) + return newKEMDecryptor(nistHybridKEM{params: &p256mlkem768Params}, block.Bytes, salt, info) case PEMBlockP384MLKEM1024PrivateKey: - return NewSaltedP384MLKEM1024Decryptor(block.Bytes, salt, info) + return newKEMDecryptor(nistHybridKEM{params: &p384mlkem1024Params}, block.Bytes, salt, info) } - switch oid, seed, err := parseMLKEMPrivatePKCS8(block.Bytes); { - case err == nil && oid.Equal(OidMLKEM768): - return NewSaltedMLKEM768Decryptor(seed, salt, info) - case err == nil && oid.Equal(OidMLKEM1024): - return NewSaltedMLKEM1024Decryptor(seed, salt, info) - case err != nil && !errors.Is(err, errNotMLKEM): + switch oid, seed, err := parseKEMPrivatePKCS8(block.Bytes); { + case err == nil: + if k, ok := kemByOID(oid); ok { + return newKEMDecryptor(k, seed, salt, info) + } + case !errors.Is(err, errNotKEM): return AsymDecryption{}, err } diff --git a/lib/ocrypto/asym_encryption.go b/lib/ocrypto/asym_encryption.go index 86ee385b96..46794d9716 100644 --- a/lib/ocrypto/asym_encryption.go +++ b/lib/ocrypto/asym_encryption.go @@ -77,19 +77,19 @@ func FromPublicPEMWithSalt(publicKeyInPem string, salt, info []byte) (PublicKeyE } switch block.Type { case PEMBlockXWingPublicKey: - return NewXWingEncryptor(block.Bytes, salt, info) + return newKEMEncryptor(xwingKEM{}, block.Bytes, salt, info) case PEMBlockP256MLKEM768PublicKey: - return NewP256MLKEM768Encryptor(block.Bytes, salt, info) + return newKEMEncryptor(nistHybridKEM{params: &p256mlkem768Params}, block.Bytes, salt, info) case PEMBlockP384MLKEM1024PublicKey: - return NewP384MLKEM1024Encryptor(block.Bytes, salt, info) + return newKEMEncryptor(nistHybridKEM{params: &p384mlkem1024Params}, block.Bytes, salt, info) } - switch oid, key, err := ParseMLKEMPublicSPKI(block.Bytes); { - case err == nil && oid.Equal(OidMLKEM768): - return NewMLKEM768Encryptor(key, salt, info) - case err == nil && oid.Equal(OidMLKEM1024): - return NewMLKEM1024Encryptor(key, salt, info) - case err != nil && !errors.Is(err, errNotMLKEM): + switch oid, key, err := ParseKEMPublicSPKI(block.Bytes); { + case err == nil: + if k, ok := kemByOID(oid); ok { + return newKEMEncryptor(k, key, salt, info) + } + case !errors.Is(err, errNotKEM): return nil, err } diff --git a/lib/ocrypto/benchmark_test.go b/lib/ocrypto/benchmark_test.go index b86b3075e7..31a53d1651 100644 --- a/lib/ocrypto/benchmark_test.go +++ b/lib/ocrypto/benchmark_test.go @@ -198,7 +198,7 @@ func BenchmarkWrapDEK(b *testing.B) { if err != nil { b.Fatal(err) } - ss, ct, err := XWingEncapsulate(pubKey) + ss, ct, err := xwingKEM{}.encapsulate(pubKey) if err != nil { b.Fatal(err) } @@ -214,9 +214,9 @@ func BenchmarkWrapDEK(b *testing.B) { if err != nil { b.Fatal(err) } - sinkBytes, errSink = asn1.Marshal(HybridNISTWrappedKey{ - HybridCiphertext: ct, - EncryptedDEK: encDEK, + sinkBytes, errSink = asn1.Marshal(kemEnvelope{ + KEMCiphertext: ct, + EncryptedDEK: encDEK, }) } b.ReportMetric(float64(len(sinkBytes)), "wrapped-bytes") @@ -229,7 +229,7 @@ func BenchmarkWrapDEK(b *testing.B) { if err != nil { b.Fatal(err) } - ss, ct, err := P256MLKEM768Encapsulate(pubKey) + ss, ct, err := nistHybridKEM{params: &p256mlkem768Params}.encapsulate(pubKey) if err != nil { b.Fatal(err) } @@ -245,9 +245,9 @@ func BenchmarkWrapDEK(b *testing.B) { if err != nil { b.Fatal(err) } - sinkBytes, errSink = asn1.Marshal(HybridNISTWrappedKey{ - HybridCiphertext: ct, - EncryptedDEK: encDEK, + sinkBytes, errSink = asn1.Marshal(kemEnvelope{ + KEMCiphertext: ct, + EncryptedDEK: encDEK, }) } b.ReportMetric(float64(len(sinkBytes)), "wrapped-bytes") @@ -260,7 +260,7 @@ func BenchmarkWrapDEK(b *testing.B) { if err != nil { b.Fatal(err) } - ss, ct, err := P384MLKEM1024Encapsulate(pubKey) + ss, ct, err := nistHybridKEM{params: &p384mlkem1024Params}.encapsulate(pubKey) if err != nil { b.Fatal(err) } @@ -276,9 +276,9 @@ func BenchmarkWrapDEK(b *testing.B) { if err != nil { b.Fatal(err) } - sinkBytes, errSink = asn1.Marshal(HybridNISTWrappedKey{ - HybridCiphertext: ct, - EncryptedDEK: encDEK, + sinkBytes, errSink = asn1.Marshal(kemEnvelope{ + KEMCiphertext: ct, + EncryptedDEK: encDEK, }) } b.ReportMetric(float64(len(sinkBytes)), "wrapped-bytes") @@ -536,12 +536,16 @@ func BenchmarkUnwrapDEK(b *testing.B) { } func BenchmarkHybridSubOps(b *testing.B) { + xwingAdapter := xwingKEM{} + p256Adapter := nistHybridKEM{params: &p256mlkem768Params} + p384Adapter := nistHybridKEM{params: &p384mlkem1024Params} + // Setup X-Wing xwingKP, err := NewXWingKeyPair() if err != nil { b.Fatal(err) } - xwingSS, xwingCt, err := XWingEncapsulate(xwingKP.publicKey) + xwingSS, xwingCt, err := xwingAdapter.encapsulate(xwingKP.publicKey) if err != nil { b.Fatal(err) } @@ -551,7 +555,7 @@ func BenchmarkHybridSubOps(b *testing.B) { if err != nil { b.Fatal(err) } - p256SS, p256Ct, err := P256MLKEM768Encapsulate(p256KP.publicKey) + p256SS, p256Ct, err := p256Adapter.encapsulate(p256KP.publicKey) if err != nil { b.Fatal(err) } @@ -561,7 +565,7 @@ func BenchmarkHybridSubOps(b *testing.B) { if err != nil { b.Fatal(err) } - p384SS, p384Ct, err := P384MLKEM1024Encapsulate(p384KP.publicKey) + p384SS, p384Ct, err := p384Adapter.encapsulate(p384KP.publicKey) if err != nil { b.Fatal(err) } @@ -569,19 +573,19 @@ func BenchmarkHybridSubOps(b *testing.B) { salt := defaultTDFSalt() // Pre-derive a wrap key for AES-GCM benchmarks - wrapKey, err := deriveXWingWrapKey(xwingSS, salt, nil) + wrapKey, err := deriveKEMWrapKey(xwingSS, salt, nil) if err != nil { b.Fatal(err) } b.Run("XWing/Encapsulate", func(b *testing.B) { for b.Loop() { - sinkBytes, sinkBytes, errSink = XWingEncapsulate(xwingKP.publicKey) + sinkBytes, sinkBytes, errSink = xwingAdapter.encapsulate(xwingKP.publicKey) } }) b.Run("XWing/HKDF", func(b *testing.B) { for b.Loop() { - sinkBytes, errSink = deriveXWingWrapKey(xwingSS, salt, nil) + sinkBytes, errSink = deriveKEMWrapKey(xwingSS, salt, nil) } }) b.Run("XWing/AES-GCM-Encrypt", func(b *testing.B) { @@ -594,25 +598,25 @@ func BenchmarkHybridSubOps(b *testing.B) { } }) b.Run("XWing/ASN1-Marshal", func(b *testing.B) { - wrapped := XWingWrappedKey{XWingCiphertext: xwingCt, EncryptedDEK: testDEK} + wrapped := kemEnvelope{KEMCiphertext: xwingCt, EncryptedDEK: testDEK} for b.Loop() { sinkBytes, errSink = asn1.Marshal(wrapped) } }) // P256+MLKEM768 sub-ops - p256WrapKey, err := deriveHybridNISTWrapKey(p256SS, salt, nil) + p256WrapKey, err := deriveKEMWrapKey(p256SS, salt, nil) if err != nil { b.Fatal(err) } b.Run("P256_MLKEM768/Encapsulate", func(b *testing.B) { for b.Loop() { - sinkBytes, sinkBytes, errSink = P256MLKEM768Encapsulate(p256KP.publicKey) + sinkBytes, sinkBytes, errSink = p256Adapter.encapsulate(p256KP.publicKey) } }) b.Run("P256_MLKEM768/HKDF", func(b *testing.B) { for b.Loop() { - sinkBytes, errSink = deriveHybridNISTWrapKey(p256SS, salt, nil) + sinkBytes, errSink = deriveKEMWrapKey(p256SS, salt, nil) } }) b.Run("P256_MLKEM768/AES-GCM-Encrypt", func(b *testing.B) { @@ -625,25 +629,25 @@ func BenchmarkHybridSubOps(b *testing.B) { } }) b.Run("P256_MLKEM768/ASN1-Marshal", func(b *testing.B) { - wrapped := HybridNISTWrappedKey{HybridCiphertext: p256Ct, EncryptedDEK: testDEK} + wrapped := kemEnvelope{KEMCiphertext: p256Ct, EncryptedDEK: testDEK} for b.Loop() { sinkBytes, errSink = asn1.Marshal(wrapped) } }) // P384+MLKEM1024 sub-ops - p384WrapKey, err := deriveHybridNISTWrapKey(p384SS, salt, nil) + p384WrapKey, err := deriveKEMWrapKey(p384SS, salt, nil) if err != nil { b.Fatal(err) } b.Run("P384_MLKEM1024/Encapsulate", func(b *testing.B) { for b.Loop() { - sinkBytes, sinkBytes, errSink = P384MLKEM1024Encapsulate(p384KP.publicKey) + sinkBytes, sinkBytes, errSink = p384Adapter.encapsulate(p384KP.publicKey) } }) b.Run("P384_MLKEM1024/HKDF", func(b *testing.B) { for b.Loop() { - sinkBytes, errSink = deriveHybridNISTWrapKey(p384SS, salt, nil) + sinkBytes, errSink = deriveKEMWrapKey(p384SS, salt, nil) } }) b.Run("P384_MLKEM1024/AES-GCM-Encrypt", func(b *testing.B) { @@ -656,7 +660,7 @@ func BenchmarkHybridSubOps(b *testing.B) { } }) b.Run("P384_MLKEM1024/ASN1-Marshal", func(b *testing.B) { - wrapped := HybridNISTWrappedKey{HybridCiphertext: p384Ct, EncryptedDEK: testDEK} + wrapped := kemEnvelope{KEMCiphertext: p384Ct, EncryptedDEK: testDEK} for b.Loop() { sinkBytes, errSink = asn1.Marshal(wrapped) } diff --git a/lib/ocrypto/ec_key_pair.go b/lib/ocrypto/ec_key_pair.go index 976089c3c3..11a86d96fb 100644 --- a/lib/ocrypto/ec_key_pair.go +++ b/lib/ocrypto/ec_key_pair.go @@ -558,7 +558,7 @@ func (keyPair MLKEMKeyPair) PrivateKeyInPemFormat() (string, error) { return "", errors.New("failed to generate PEM formatted private key") } - der, err := marshalMLKEMPrivatePKCS8(OidMLKEM768, keyPair.PrivateKey.Bytes()) + der, err := marshalKEMPrivatePKCS8(OidMLKEM768, keyPair.PrivateKey.Bytes()) if err != nil { return "", fmt.Errorf("marshal ML-KEM-768 PKCS#8 failed: %w", err) } @@ -570,7 +570,7 @@ func (keyPair MLKEMKeyPair) PublicKeyInPemFormat() (string, error) { return "", errors.New("failed to generate PEM formatted public key") } - der, err := marshalMLKEMPublicSPKI(OidMLKEM768, keyPair.PrivateKey.EncapsulationKey().Bytes()) + der, err := marshalKEMPublicSPKI(OidMLKEM768, keyPair.PrivateKey.EncapsulationKey().Bytes()) if err != nil { return "", fmt.Errorf("marshal ML-KEM-768 SPKI failed: %w", err) } @@ -586,7 +586,7 @@ func (keyPair MLKEM1024KeyPair) PrivateKeyInPemFormat() (string, error) { return "", errors.New("failed to generate PEM formatted private key") } - der, err := marshalMLKEMPrivatePKCS8(OidMLKEM1024, keyPair.PrivateKey.Bytes()) + der, err := marshalKEMPrivatePKCS8(OidMLKEM1024, keyPair.PrivateKey.Bytes()) if err != nil { return "", fmt.Errorf("marshal ML-KEM-1024 PKCS#8 failed: %w", err) } @@ -598,7 +598,7 @@ func (keyPair MLKEM1024KeyPair) PublicKeyInPemFormat() (string, error) { return "", errors.New("failed to generate PEM formatted public key") } - der, err := marshalMLKEMPublicSPKI(OidMLKEM1024, keyPair.PrivateKey.EncapsulationKey().Bytes()) + der, err := marshalKEMPublicSPKI(OidMLKEM1024, keyPair.PrivateKey.EncapsulationKey().Bytes()) if err != nil { return "", fmt.Errorf("marshal ML-KEM-1024 SPKI failed: %w", err) } diff --git a/lib/ocrypto/hybrid_common.go b/lib/ocrypto/hybrid_common.go index 4f4ee05417..f919c107e2 100644 --- a/lib/ocrypto/hybrid_common.go +++ b/lib/ocrypto/hybrid_common.go @@ -6,42 +6,39 @@ import ( "fmt" ) -// HybridWrapDEK parses the recipient's hybrid public key PEM, encapsulates -// against it using the scheme implied by ktype, and returns the ASN.1-encoded -// wrapped DEK envelope used in `hybrid-wrapped` manifests. It dispatches across -// both the X-Wing and NIST EC + ML-KEM families so SDK call sites do not need -// to repeat the algorithm switch. +// WrapDEK parses the recipient's KEM public key PEM, encapsulates against it +// using the scheme implied by ktype, and returns the ASN.1-encoded wrapped DEK +// envelope used in `hybrid-wrapped` and `mlkem-wrapped` manifests. It covers +// every KEM family — pure ML-KEM, X-Wing, and the NIST EC + ML-KEM hybrids — +// so SDK call sites do not need to repeat the algorithm switch. // // The HKDF salt is the default TDF salt; callers that need a non-default salt -// should call the per-scheme `*WrapDEK` helpers directly. -func HybridWrapDEK(ktype KeyType, kasPublicKeyPEM string, dek []byte) ([]byte, error) { - switch ktype { //nolint:exhaustive // only handle hybrid types - case HybridXWingKey: - pubKey, err := XWingPubKeyFromPem([]byte(kasPublicKeyPEM)) - if err != nil { - return nil, fmt.Errorf("X-Wing public key: %w", err) - } - return XWingWrapDEK(pubKey, dek) - case HybridSecp256r1MLKEM768Key: - pubKey, err := P256MLKEM768PubKeyFromPem([]byte(kasPublicKeyPEM)) - if err != nil { - return nil, fmt.Errorf("P-256+ML-KEM-768 public key: %w", err) - } - return P256MLKEM768WrapDEK(pubKey, dek) - case HybridSecp384r1MLKEM1024Key: - pubKey, err := P384MLKEM1024PubKeyFromPem([]byte(kasPublicKeyPEM)) - if err != nil { - return nil, fmt.Errorf("P-384+ML-KEM-1024 public key: %w", err) - } - return P384MLKEM1024WrapDEK(pubKey, dek) - default: - return nil, fmt.Errorf("unsupported hybrid key type: %s", ktype) +// should construct an encryptor via FromPublicPEMWithSalt instead. +func WrapDEK(ktype KeyType, kasPublicKeyPEM string, dek []byte) ([]byte, error) { + if !IsKEMKeyType(ktype) { + return nil, fmt.Errorf("unsupported KEM key type: %s", ktype) + } + enc, err := FromPublicPEM(kasPublicKeyPEM) + if err != nil { + return nil, fmt.Errorf("parse %s public key: %w", ktype, err) } + if got := enc.KeyType(); got != ktype { + return nil, fmt.Errorf("KEM key type mismatch: want %s, got %s", ktype, got) + } + return enc.Encrypt(dek) +} + +// HybridWrapDEK is the legacy entrypoint for hybrid PQ/T wrapping. It now +// delegates to WrapDEK, which covers both hybrid and pure ML-KEM schemes. +// +// Deprecated: Use WrapDEK. +func HybridWrapDEK(ktype KeyType, kasPublicKeyPEM string, dek []byte) ([]byte, error) { + return WrapDEK(ktype, kasPublicKeyPEM, dek) } -// defaultTDFSalt returns the salt used for HKDF derivation in all TDF hybrid -// key wrapping schemes (X-Wing and NIST EC + ML-KEM). Defined here rather than -// in a per-scheme file so that any change applies uniformly across schemes. +// defaultTDFSalt returns the salt used for HKDF derivation in all TDF KEM key +// wrapping schemes (pure ML-KEM, X-Wing, and NIST EC + ML-KEM). Defined here +// rather than in a per-scheme file so any change applies uniformly. func defaultTDFSalt() []byte { digest := sha256.New() digest.Write([]byte("TDF")) @@ -66,6 +63,24 @@ func rawToPEM(blockType string, raw []byte, expectedSize int) (string, error) { return string(pemBytes), nil } +// decodeSizedPEMBlock decodes a PEM block of the given type and verifies its +// payload is exactly expectedSize bytes long. Used by X-Wing and NIST hybrid +// PEM helpers that carry raw-bytes PEM blobs (pre-SPKI-migration). +func decodeSizedPEMBlock(data []byte, blockType string, expectedSize int) ([]byte, error) { + block, _ := pem.Decode(data) + if block == nil { + return nil, fmt.Errorf("failed to parse PEM formatted %s", blockType) + } + if block.Type != blockType { + return nil, fmt.Errorf("unexpected PEM block type: got %s want %s", block.Type, blockType) + } + if len(block.Bytes) != expectedSize { + return nil, fmt.Errorf("invalid %s size: got %d want %d", blockType, len(block.Bytes), expectedSize) + } + + return append([]byte(nil), block.Bytes...), nil +} + // cloneOrNil returns a copy of data, or nil if data is empty. func cloneOrNil(data []byte) []byte { if len(data) == 0 { diff --git a/lib/ocrypto/hybrid_nist.go b/lib/ocrypto/hybrid_nist.go index ee0a575ac5..1de0f08b91 100644 --- a/lib/ocrypto/hybrid_nist.go +++ b/lib/ocrypto/hybrid_nist.go @@ -4,12 +4,7 @@ import ( "crypto/ecdh" "crypto/mlkem" "crypto/rand" - "crypto/sha256" - "encoding/asn1" "fmt" - "io" - - "golang.org/x/crypto/hkdf" ) const ( @@ -52,15 +47,6 @@ const ( PEMBlockP384MLKEM1024PrivateKey = "SECP384R1 MLKEM1024 PRIVATE KEY" ) -// AES-256 key size used for wrap key derivation. -const hybridNISTWrapKeySize = 32 - -// HybridNISTWrappedKey is the ASN.1 envelope stored in wrapped_key. -type HybridNISTWrappedKey struct { - HybridCiphertext []byte `asn1:"tag:0"` - EncryptedDEK []byte `asn1:"tag:1"` -} - // hybridNISTParams captures the curve-specific parameters for a NIST hybrid scheme. type hybridNISTParams struct { curve ecdh.Curve @@ -105,22 +91,6 @@ type HybridNISTKeyPair struct { params *hybridNISTParams } -// HybridNISTEncryptor implements PublicKeyEncryptor for NIST hybrid schemes. -type HybridNISTEncryptor struct { - publicKey []byte - salt []byte - info []byte - params *hybridNISTParams -} - -// HybridNISTDecryptor implements PrivateKeyDecryptor for NIST hybrid schemes. -type HybridNISTDecryptor struct { - privateKey []byte - salt []byte - info []byte - params *hybridNISTParams -} - // IsHybridKeyType returns true if the key type is a hybrid post-quantum type. func IsHybridKeyType(kt KeyType) bool { switch kt { //nolint:exhaustive // only handle hybrid types @@ -221,299 +191,34 @@ func P384MLKEM1024PrivateKeyFromPem(data []byte) ([]byte, error) { return decodeSizedPEMBlock(data, PEMBlockP384MLKEM1024PrivateKey, P384MLKEM1024PrivateKeySize) } -func NewP256MLKEM768Encryptor(publicKey, salt, info []byte) (*HybridNISTEncryptor, error) { - return newHybridNISTEncryptor(&p256mlkem768Params, publicKey, salt, info) -} - -func NewP384MLKEM1024Encryptor(publicKey, salt, info []byte) (*HybridNISTEncryptor, error) { - return newHybridNISTEncryptor(&p384mlkem1024Params, publicKey, salt, info) -} - -func newHybridNISTEncryptor(p *hybridNISTParams, publicKey, salt, info []byte) (*HybridNISTEncryptor, error) { - expectedSize := p.ecPubSize + p.mlkemPubSize - if len(publicKey) != expectedSize { - return nil, fmt.Errorf("invalid %s public key size: got %d want %d", p.keyType, len(publicKey), expectedSize) - } - return &HybridNISTEncryptor{ - publicKey: append([]byte(nil), publicKey...), - salt: cloneOrNil(salt), - info: cloneOrNil(info), - params: p, - }, nil -} - -func (e *HybridNISTEncryptor) Encrypt(data []byte) ([]byte, error) { - return hybridNISTWrapDEK(e.params, e.publicKey, data, e.salt, e.info) -} - -func (e *HybridNISTEncryptor) PublicKeyInPemFormat() (string, error) { - return rawToPEM(e.params.pubPEMBlock, e.publicKey, e.params.ecPubSize+e.params.mlkemPubSize) -} - -func (e *HybridNISTEncryptor) Type() SchemeType { return Hybrid } -func (e *HybridNISTEncryptor) KeyType() KeyType { return e.params.keyType } -func (e *HybridNISTEncryptor) EphemeralKey() []byte { return nil } - -func (e *HybridNISTEncryptor) Metadata() (map[string]string, error) { - return make(map[string]string), nil -} - -func NewP256MLKEM768Decryptor(privateKey []byte) (*HybridNISTDecryptor, error) { - return NewSaltedP256MLKEM768Decryptor(privateKey, defaultTDFSalt(), nil) -} - -func NewSaltedP256MLKEM768Decryptor(privateKey, salt, info []byte) (*HybridNISTDecryptor, error) { - return newHybridNISTDecryptor(&p256mlkem768Params, privateKey, salt, info) -} - -func NewP384MLKEM1024Decryptor(privateKey []byte) (*HybridNISTDecryptor, error) { - return NewSaltedP384MLKEM1024Decryptor(privateKey, defaultTDFSalt(), nil) -} - -func NewSaltedP384MLKEM1024Decryptor(privateKey, salt, info []byte) (*HybridNISTDecryptor, error) { - return newHybridNISTDecryptor(&p384mlkem1024Params, privateKey, salt, info) -} - -func newHybridNISTDecryptor(p *hybridNISTParams, privateKey, salt, info []byte) (*HybridNISTDecryptor, error) { - expectedSize := p.ecPrivSize + p.mlkemPrivSize - if len(privateKey) != expectedSize { - return nil, fmt.Errorf("invalid %s private key size: got %d want %d", p.keyType, len(privateKey), expectedSize) - } - return &HybridNISTDecryptor{ - privateKey: append([]byte(nil), privateKey...), - salt: cloneOrNil(salt), - info: cloneOrNil(info), - params: p, - }, nil -} - -func (d *HybridNISTDecryptor) Decrypt(data []byte) ([]byte, error) { - return hybridNISTUnwrapDEK(d.params, d.privateKey, data, d.salt, d.info) -} - +// P256MLKEM768WrapDEK wraps a DEK using the P-256 + ML-KEM-768 hybrid scheme. +// +// Deprecated: Use WrapDEK with HybridSecp256r1MLKEM768Key, or construct via +// FromPublicPEM. func P256MLKEM768WrapDEK(publicKeyRaw, dek []byte) ([]byte, error) { - return hybridNISTWrapDEK(&p256mlkem768Params, publicKeyRaw, dek, defaultTDFSalt(), nil) + return wrapDEKWithKEM(nistHybridKEM{params: &p256mlkem768Params}, publicKeyRaw, dek, defaultTDFSalt(), nil) } +// P256MLKEM768UnwrapDEK unwraps an envelope produced by P256MLKEM768WrapDEK +// using the supplied raw P-256 + ML-KEM-768 private key. This is the binary- +// bytes counterpart to FromPrivatePEM; callers that already hold raw key +// material can use it directly without re-encoding to PEM. func P256MLKEM768UnwrapDEK(privateKeyRaw, wrappedDER []byte) ([]byte, error) { - return hybridNISTUnwrapDEK(&p256mlkem768Params, privateKeyRaw, wrappedDER, defaultTDFSalt(), nil) + return unwrapDEKWithKEM(nistHybridKEM{params: &p256mlkem768Params}, privateKeyRaw, wrappedDER, defaultTDFSalt(), nil) } +// P384MLKEM1024WrapDEK wraps a DEK using the P-384 + ML-KEM-1024 hybrid scheme. +// +// Deprecated: Use WrapDEK with HybridSecp384r1MLKEM1024Key, or construct via +// FromPublicPEM. func P384MLKEM1024WrapDEK(publicKeyRaw, dek []byte) ([]byte, error) { - return hybridNISTWrapDEK(&p384mlkem1024Params, publicKeyRaw, dek, defaultTDFSalt(), nil) + return wrapDEKWithKEM(nistHybridKEM{params: &p384mlkem1024Params}, publicKeyRaw, dek, defaultTDFSalt(), nil) } +// P384MLKEM1024UnwrapDEK unwraps an envelope produced by P384MLKEM1024WrapDEK +// using the supplied raw P-384 + ML-KEM-1024 private key. This is the binary- +// bytes counterpart to FromPrivatePEM; callers that already hold raw key +// material can use it directly without re-encoding to PEM. func P384MLKEM1024UnwrapDEK(privateKeyRaw, wrappedDER []byte) ([]byte, error) { - return hybridNISTUnwrapDEK(&p384mlkem1024Params, privateKeyRaw, wrappedDER, defaultTDFSalt(), nil) -} - -// hybridNISTEncapsulate performs hybrid encapsulation: -// 1. Generates an ephemeral EC key and computes ECDH shared secret -// 2. Encapsulates ML-KEM to produce a post-quantum shared secret -// 3. Combines both secrets (ECDH || ML-KEM) -// 4. Builds hybrid ciphertext (ephemeral EC point || ML-KEM ciphertext) -// -// Returns (combinedSecret, hybridCiphertext) without applying KDF or encryption. -func hybridNISTEncapsulate(p *hybridNISTParams, publicKeyRaw []byte) ([]byte, []byte, error) { - expectedPubSize := p.ecPubSize + p.mlkemPubSize - if len(publicKeyRaw) != expectedPubSize { - return nil, nil, fmt.Errorf("invalid %s public key size: got %d want %d", p.keyType, len(publicKeyRaw), expectedPubSize) - } - - ecPubBytes := publicKeyRaw[:p.ecPubSize] - mlkemPubBytes := publicKeyRaw[p.ecPubSize:] - - // ECDH: generate ephemeral key, compute shared secret - ecPub, err := p.curve.NewPublicKey(ecPubBytes) - if err != nil { - return nil, nil, fmt.Errorf("invalid EC public key: %w", err) - } - ephemeral, err := p.curve.GenerateKey(rand.Reader) - if err != nil { - return nil, nil, fmt.Errorf("ECDH ephemeral key generation failed: %w", err) - } - ecdhSecret, err := ephemeral.ECDH(ecPub) - if err != nil { - return nil, nil, fmt.Errorf("ECDH failed: %w", err) - } - ephemeralPub := ephemeral.PublicKey().Bytes() - - // ML-KEM: encapsulate - var mlkemSecret, mlkemCt []byte - switch p.keyType { //nolint:exhaustive // only NIST hybrid types - case HybridSecp256r1MLKEM768Key: - ek, ekErr := mlkem.NewEncapsulationKey768(mlkemPubBytes) - if ekErr != nil { - return nil, nil, fmt.Errorf("mlkem768 encapsulation key: %w", ekErr) - } - mlkemSecret, mlkemCt = ek.Encapsulate() - case HybridSecp384r1MLKEM1024Key: - ek, ekErr := mlkem.NewEncapsulationKey1024(mlkemPubBytes) - if ekErr != nil { - return nil, nil, fmt.Errorf("mlkem1024 encapsulation key: %w", ekErr) - } - mlkemSecret, mlkemCt = ek.Encapsulate() - default: - return nil, nil, fmt.Errorf("unsupported ML-KEM key type: %s", p.keyType) - } - - // Combine secrets: ECDH || ML-KEM - combinedSecret := make([]byte, 0, len(ecdhSecret)+len(mlkemSecret)) - combinedSecret = append(combinedSecret, ecdhSecret...) - combinedSecret = append(combinedSecret, mlkemSecret...) - - // Build hybrid ciphertext: ephemeral EC point || ML-KEM ciphertext - hybridCt := make([]byte, 0, len(ephemeralPub)+len(mlkemCt)) - hybridCt = append(hybridCt, ephemeralPub...) - hybridCt = append(hybridCt, mlkemCt...) - - return combinedSecret, hybridCt, nil -} - -// P256MLKEM768Encapsulate performs P-256 ECDH + ML-KEM-768 hybrid encapsulation. -func P256MLKEM768Encapsulate(publicKeyRaw []byte) ([]byte, []byte, error) { - return hybridNISTEncapsulate(&p256mlkem768Params, publicKeyRaw) -} - -// P384MLKEM1024Encapsulate performs P-384 ECDH + ML-KEM-1024 hybrid encapsulation. -func P384MLKEM1024Encapsulate(publicKeyRaw []byte) ([]byte, []byte, error) { - return hybridNISTEncapsulate(&p384mlkem1024Params, publicKeyRaw) -} - -func hybridNISTWrapDEK(p *hybridNISTParams, publicKeyRaw, dek, salt, info []byte) ([]byte, error) { - combinedSecret, hybridCt, err := hybridNISTEncapsulate(p, publicKeyRaw) - if err != nil { - return nil, err - } - - // Derive AES-256 wrap key via HKDF - wrapKey, err := deriveHybridNISTWrapKey(combinedSecret, salt, info) - if err != nil { - return nil, err - } - - // AES-GCM encrypt DEK - gcm, err := NewAESGcm(wrapKey) - if err != nil { - return nil, fmt.Errorf("NewAESGcm failed: %w", err) - } - encryptedDEK, err := gcm.Encrypt(dek) - if err != nil { - return nil, fmt.Errorf("AES-GCM encrypt failed: %w", err) - } - - wrappedDER, err := asn1.Marshal(HybridNISTWrappedKey{ - HybridCiphertext: hybridCt, - EncryptedDEK: encryptedDEK, - }) - if err != nil { - return nil, fmt.Errorf("asn1.Marshal failed: %w", err) - } - - return wrappedDER, nil -} - -func hybridNISTUnwrapDEK(p *hybridNISTParams, privateKeyRaw, wrappedDER, salt, info []byte) ([]byte, error) { - expectedPrivSize := p.ecPrivSize + p.mlkemPrivSize - if len(privateKeyRaw) != expectedPrivSize { - return nil, fmt.Errorf("invalid %s private key size: got %d want %d", p.keyType, len(privateKeyRaw), expectedPrivSize) - } - - var wrapped HybridNISTWrappedKey - rest, err := asn1.Unmarshal(wrappedDER, &wrapped) - if err != nil { - return nil, fmt.Errorf("asn1.Unmarshal failed: %w", err) - } - if len(rest) != 0 { - return nil, fmt.Errorf("asn1.Unmarshal left %d trailing bytes", len(rest)) - } - - expectedCtSize := p.ecPubSize + p.mlkemCtSize - if len(wrapped.HybridCiphertext) != expectedCtSize { - return nil, fmt.Errorf("invalid %s ciphertext size: got %d want %d", - p.keyType, len(wrapped.HybridCiphertext), expectedCtSize) - } - - // Split hybrid ciphertext - ephemeralPubBytes := wrapped.HybridCiphertext[:p.ecPubSize] - mlkemCtBytes := wrapped.HybridCiphertext[p.ecPubSize:] - - // Split private key - ecPrivBytes := privateKeyRaw[:p.ecPrivSize] - mlkemPrivBytes := privateKeyRaw[p.ecPrivSize:] - - // ECDH: reconstruct shared secret - ecPriv, err := p.curve.NewPrivateKey(ecPrivBytes) - if err != nil { - return nil, fmt.Errorf("invalid EC private key: %w", err) - } - ephemeralPub, err := p.curve.NewPublicKey(ephemeralPubBytes) - if err != nil { - return nil, fmt.Errorf("invalid ephemeral EC public key: %w", err) - } - ecdhSecret, err := ecPriv.ECDH(ephemeralPub) - if err != nil { - return nil, fmt.Errorf("ECDH failed: %w", err) - } - - // ML-KEM: decapsulate. Implicit rejection (FIPS 203 §6.3) means a wrong-key - // ciphertext yields a pseudorandom shared secret without an error here; - // authentication is enforced by the AES-GCM decrypt below. - var mlkemSecret []byte - switch p.keyType { //nolint:exhaustive // only NIST hybrid types - case HybridSecp256r1MLKEM768Key: - dk, dkErr := mlkem.NewDecapsulationKey768(mlkemPrivBytes) - if dkErr != nil { - return nil, fmt.Errorf("mlkem768 decapsulation key: %w", dkErr) - } - mlkemSecret, err = dk.Decapsulate(mlkemCtBytes) - case HybridSecp384r1MLKEM1024Key: - dk, dkErr := mlkem.NewDecapsulationKey1024(mlkemPrivBytes) - if dkErr != nil { - return nil, fmt.Errorf("mlkem1024 decapsulation key: %w", dkErr) - } - mlkemSecret, err = dk.Decapsulate(mlkemCtBytes) - default: - return nil, fmt.Errorf("unsupported ML-KEM key type: %s", p.keyType) - } - if err != nil { - return nil, fmt.Errorf("ML-KEM decapsulate failed: %w", err) - } - - // Combine secrets: ECDH || ML-KEM - combinedSecret := make([]byte, 0, len(ecdhSecret)+len(mlkemSecret)) - combinedSecret = append(combinedSecret, ecdhSecret...) - combinedSecret = append(combinedSecret, mlkemSecret...) - - // Derive AES-256 wrap key via HKDF - wrapKey, err := deriveHybridNISTWrapKey(combinedSecret, salt, info) - if err != nil { - return nil, err - } - - // AES-GCM decrypt DEK - gcm, err := NewAESGcm(wrapKey) - if err != nil { - return nil, fmt.Errorf("NewAESGcm failed: %w", err) - } - plaintext, err := gcm.Decrypt(wrapped.EncryptedDEK) - if err != nil { - return nil, fmt.Errorf("AES-GCM decrypt failed: %w", err) - } - - return plaintext, nil -} - -func deriveHybridNISTWrapKey(combinedSecret, salt, info []byte) ([]byte, error) { - if len(salt) == 0 { - salt = defaultTDFSalt() - } - - hkdfObj := hkdf.New(sha256.New, combinedSecret, salt, info) - derivedKey := make([]byte, hybridNISTWrapKeySize) - if _, err := io.ReadFull(hkdfObj, derivedKey); err != nil { - return nil, fmt.Errorf("hkdf failure: %w", err) - } - - return derivedKey, nil + return unwrapDEKWithKEM(nistHybridKEM{params: &p384mlkem1024Params}, privateKeyRaw, wrappedDER, defaultTDFSalt(), nil) } diff --git a/lib/ocrypto/hybrid_nist_test.go b/lib/ocrypto/hybrid_nist_test.go index e7c9808aae..2a741644f4 100644 --- a/lib/ocrypto/hybrid_nist_test.go +++ b/lib/ocrypto/hybrid_nist_test.go @@ -115,16 +115,16 @@ func TestP384MLKEM1024WrapUnwrapWrongKeyFails(t *testing.T) { assert.ErrorContains(t, err, "AES-GCM decrypt failed") } -func TestHybridNISTWrappedKeyASN1RoundTrip(t *testing.T) { - original := HybridNISTWrappedKey{ - HybridCiphertext: []byte("hybrid-ciphertext-data"), - EncryptedDEK: []byte("encrypted-dek-data"), +func TestHybridNISTEnvelopeASN1RoundTrip(t *testing.T) { + original := kemEnvelope{ + KEMCiphertext: []byte("hybrid-ciphertext-data"), + EncryptedDEK: []byte("encrypted-dek-data"), } der, err := asn1.Marshal(original) require.NoError(t, err) - var decoded HybridNISTWrappedKey + var decoded kemEnvelope rest, err := asn1.Unmarshal(der, &decoded) require.NoError(t, err) assert.Empty(t, rest) @@ -146,23 +146,18 @@ func TestP256MLKEM768PEMDispatch(t *testing.T) { decryptor, err := FromPrivatePEMWithSalt(privatePEM, []byte("salt"), []byte("info")) require.NoError(t, err) - nistEncryptor, ok := encryptor.(*HybridNISTEncryptor) - require.True(t, ok) - assert.Equal(t, Hybrid, nistEncryptor.Type()) - assert.Equal(t, HybridSecp256r1MLKEM768Key, nistEncryptor.KeyType()) - assert.Nil(t, nistEncryptor.EphemeralKey()) + assert.Equal(t, Hybrid, encryptor.Type()) + assert.Equal(t, HybridSecp256r1MLKEM768Key, encryptor.KeyType()) + assert.Nil(t, encryptor.EphemeralKey()) - metadata, err := nistEncryptor.Metadata() + metadata, err := encryptor.Metadata() require.NoError(t, err) assert.Empty(t, metadata) - nistDecryptor, ok := decryptor.(*HybridNISTDecryptor) - require.True(t, ok) - - wrapped, err := nistEncryptor.Encrypt([]byte("dispatch-dek")) + wrapped, err := encryptor.Encrypt([]byte("dispatch-dek")) require.NoError(t, err) - plaintext, err := nistDecryptor.Decrypt(wrapped) + plaintext, err := decryptor.Decrypt(wrapped) require.NoError(t, err) assert.Equal(t, []byte("dispatch-dek"), plaintext) } @@ -182,19 +177,14 @@ func TestP384MLKEM1024PEMDispatch(t *testing.T) { decryptor, err := FromPrivatePEMWithSalt(privatePEM, []byte("salt"), []byte("info")) require.NoError(t, err) - nistEncryptor, ok := encryptor.(*HybridNISTEncryptor) - require.True(t, ok) - assert.Equal(t, Hybrid, nistEncryptor.Type()) - assert.Equal(t, HybridSecp384r1MLKEM1024Key, nistEncryptor.KeyType()) - assert.Nil(t, nistEncryptor.EphemeralKey()) - - nistDecryptor, ok := decryptor.(*HybridNISTDecryptor) - require.True(t, ok) + assert.Equal(t, Hybrid, encryptor.Type()) + assert.Equal(t, HybridSecp384r1MLKEM1024Key, encryptor.KeyType()) + assert.Nil(t, encryptor.EphemeralKey()) - wrapped, err := nistEncryptor.Encrypt([]byte("dispatch-dek-384")) + wrapped, err := encryptor.Encrypt([]byte("dispatch-dek-384")) require.NoError(t, err) - plaintext, err := nistDecryptor.Decrypt(wrapped) + plaintext, err := decryptor.Decrypt(wrapped) require.NoError(t, err) assert.Equal(t, []byte("dispatch-dek-384"), plaintext) } @@ -209,7 +199,7 @@ func TestP256MLKEM768Encapsulate(t *testing.T) { pubKeyRaw, err := P256MLKEM768PubKeyFromPem([]byte(pubKey)) require.NoError(t, err) - combinedSecret, hybridCt, err := P256MLKEM768Encapsulate(pubKeyRaw) + combinedSecret, hybridCt, err := nistHybridKEM{params: &p256mlkem768Params}.encapsulate(pubKeyRaw) require.NoError(t, err) assert.NotEmpty(t, combinedSecret) assert.Len(t, hybridCt, P256MLKEM768CiphertextSize) @@ -225,7 +215,7 @@ func TestP384MLKEM1024Encapsulate(t *testing.T) { pubKeyRaw, err := P384MLKEM1024PubKeyFromPem([]byte(pubKey)) require.NoError(t, err) - combinedSecret, hybridCt, err := P384MLKEM1024Encapsulate(pubKeyRaw) + combinedSecret, hybridCt, err := nistHybridKEM{params: &p384mlkem1024Params}.encapsulate(pubKeyRaw) require.NoError(t, err) assert.NotEmpty(t, combinedSecret) assert.Len(t, hybridCt, P384MLKEM1024CiphertextSize) diff --git a/lib/ocrypto/kem.go b/lib/ocrypto/kem.go new file mode 100644 index 0000000000..d80b57120d --- /dev/null +++ b/lib/ocrypto/kem.go @@ -0,0 +1,482 @@ +package ocrypto + +import ( + "crypto/mlkem" + "crypto/rand" + "crypto/sha256" + "encoding/asn1" + "encoding/pem" + "fmt" + "io" + + "github.com/cloudflare/circl/kem/xwing" + "golang.org/x/crypto/hkdf" +) + +// kem is the post-quantum KEM contract implemented by ML-KEM, X-Wing, and the +// NIST hybrid PQ/T schemes. Unifying behind this single interface collapses the +// `hybrid-wrapped` and `mlkem-wrapped` wrap/unwrap paths into one envelope, one +// HKDF derivation, and one AES-GCM encryption call site. +type kem interface { + keyType() KeyType + scheme() SchemeType + pubSize() int + privSize() int + ctSize() int + encapsulate(pub []byte) (sharedSecret, ciphertext []byte, err error) + decapsulate(priv, ct []byte) (sharedSecret []byte, err error) + // publicKeyPEM returns the PEM serialization for the given raw public key. + // Each adapter handles its own format. After the planned follow-up moves + // X-Wing and the NIST hybrid keys onto standard SPKI PEM blocks this + // per-adapter hook collapses to a single shared helper. + publicKeyPEM(pub []byte) (string, error) +} + +// kemEnvelope is the ASN.1 wire format for every KEM-wrapped DEK across +// `hybrid-wrapped` and `mlkem-wrapped` KAOs. It is byte-identical to the three +// legacy structs (MLKEMWrappedKey, XWingWrappedKey, HybridNISTWrappedKey) it +// replaces — same tags, same field order. +type kemEnvelope struct { + KEMCiphertext []byte `asn1:"tag:0"` + EncryptedDEK []byte `asn1:"tag:1"` +} + +// kemWrapKeySize is the AES-256 wrap key length derived via HKDF. +const kemWrapKeySize = 32 + +// kemRegistry maps the SPKI/PKCS#8 OID published for a KEM scheme to a +// constructor that returns a kem adapter bound to that scheme. ML-KEM is the +// only family with standardized OIDs landed today; the planned hybrid PQ/T +// SPKI follow-up adds X-Wing and the two NIST hybrid OIDs by inserting +// registry entries here. +var kemRegistry = map[string]func() kem{ + OidMLKEM768.String(): func() kem { return mlkemKEM{variant: mlkem768} }, + OidMLKEM1024.String(): func() kem { return mlkemKEM{variant: mlkem1024} }, +} + +// kemByOID returns the kem adapter registered for the supplied OID, or false +// if the OID is not a recognised KEM algorithm. +func kemByOID(oid asn1.ObjectIdentifier) (kem, bool) { + ctor, ok := kemRegistry[oid.String()] + if !ok { + return nil, false + } + return ctor(), true +} + +// kemByKeyType returns the kem adapter for the supplied KeyType, covering both +// pure ML-KEM and hybrid PQ/T schemes. This is the entry point for wrap-side +// dispatch where the caller knows the KAS algorithm but has not yet decoded a +// public key. +func kemByKeyType(kt KeyType) (kem, bool) { + switch kt { //nolint:exhaustive // only handle KEM types; other KeyTypes return false + case MLKEM768Key: + return mlkemKEM{variant: mlkem768}, true + case MLKEM1024Key: + return mlkemKEM{variant: mlkem1024}, true + case HybridXWingKey: + return xwingKEM{}, true + case HybridSecp256r1MLKEM768Key: + return nistHybridKEM{params: &p256mlkem768Params}, true + case HybridSecp384r1MLKEM1024Key: + return nistHybridKEM{params: &p384mlkem1024Params}, true + default: + return nil, false + } +} + +// IsKEMKeyType reports whether the supplied KeyType is one of the KEM schemes +// — pure ML-KEM or hybrid PQ/T — handled by the unified wrap/unwrap path. +func IsKEMKeyType(kt KeyType) bool { + _, ok := kemByKeyType(kt) + return ok +} + +// --- mlkemKEM adapter ------------------------------------------------------- + +type mlkemVariant int + +const ( + mlkem768 mlkemVariant = iota + mlkem1024 +) + +type mlkemKEM struct { + variant mlkemVariant +} + +func (m mlkemKEM) keyType() KeyType { + if m.variant == mlkem1024 { + return MLKEM1024Key + } + return MLKEM768Key +} + +func (mlkemKEM) scheme() SchemeType { return MLKEM } + +func (m mlkemKEM) pubSize() int { + if m.variant == mlkem1024 { + return MLKEM1024PublicKeySize + } + return MLKEM768PublicKeySize +} + +func (m mlkemKEM) privSize() int { + if m.variant == mlkem1024 { + return MLKEM1024PrivateKeySize + } + return MLKEM768PrivateKeySize +} + +func (m mlkemKEM) ctSize() int { + if m.variant == mlkem1024 { + return MLKEM1024CiphertextSize + } + return MLKEM768CiphertextSize +} + +func (m mlkemKEM) encapsulate(pub []byte) ([]byte, []byte, error) { + if len(pub) != m.pubSize() { + return nil, nil, fmt.Errorf("invalid %s public key size: got %d want %d", m.keyType(), len(pub), m.pubSize()) + } + if m.variant == mlkem1024 { + ek, err := mlkem.NewEncapsulationKey1024(pub) + if err != nil { + return nil, nil, fmt.Errorf("mlkem.NewEncapsulationKey1024 failed: %w", err) + } + ss, ct := ek.Encapsulate() + return ss, ct, nil + } + ek, err := mlkem.NewEncapsulationKey768(pub) + if err != nil { + return nil, nil, fmt.Errorf("mlkem.NewEncapsulationKey768 failed: %w", err) + } + ss, ct := ek.Encapsulate() + return ss, ct, nil +} + +func (m mlkemKEM) decapsulate(priv, ct []byte) ([]byte, error) { + if len(priv) != m.privSize() { + return nil, fmt.Errorf("invalid %s private key size: got %d want %d", m.keyType(), len(priv), m.privSize()) + } + if m.variant == mlkem1024 { + dk, err := mlkem.NewDecapsulationKey1024(priv) + if err != nil { + return nil, fmt.Errorf("mlkem.NewDecapsulationKey1024 failed: %w", err) + } + ss, err := dk.Decapsulate(ct) + if err != nil { + return nil, fmt.Errorf("mlkem1024 decapsulate failed: %w", err) + } + return ss, nil + } + dk, err := mlkem.NewDecapsulationKey768(priv) + if err != nil { + return nil, fmt.Errorf("mlkem.NewDecapsulationKey768 failed: %w", err) + } + ss, err := dk.Decapsulate(ct) + if err != nil { + return nil, fmt.Errorf("mlkem768 decapsulate failed: %w", err) + } + return ss, nil +} + +func (m mlkemKEM) publicKeyPEM(pub []byte) (string, error) { + oid := OidMLKEM768 + if m.variant == mlkem1024 { + oid = OidMLKEM1024 + } + der, err := marshalKEMPublicSPKI(oid, pub) + if err != nil { + return "", fmt.Errorf("marshal %s SPKI failed: %w", m.keyType(), err) + } + return string(pem.EncodeToMemory(&pem.Block{Type: pemBlockPublicKey, Bytes: der})), nil +} + +// --- xwingKEM adapter ------------------------------------------------------- + +type xwingKEM struct{} + +func (xwingKEM) keyType() KeyType { return HybridXWingKey } +func (xwingKEM) scheme() SchemeType { return Hybrid } +func (xwingKEM) pubSize() int { return XWingPublicKeySize } +func (xwingKEM) privSize() int { return XWingPrivateKeySize } +func (xwingKEM) ctSize() int { return XWingCiphertextSize } + +func (xwingKEM) encapsulate(pub []byte) ([]byte, []byte, error) { + if len(pub) != XWingPublicKeySize { + return nil, nil, fmt.Errorf("invalid X-Wing public key size: got %d want %d", len(pub), XWingPublicKeySize) + } + ss, ct, err := xwing.Encapsulate(pub, nil) + if err != nil { + return nil, nil, fmt.Errorf("xwing.Encapsulate failed: %w", err) + } + return ss, ct, nil +} + +func (xwingKEM) decapsulate(priv, ct []byte) ([]byte, error) { + if len(priv) != XWingPrivateKeySize { + return nil, fmt.Errorf("invalid X-Wing private key size: got %d want %d", len(priv), XWingPrivateKeySize) + } + return xwing.Decapsulate(ct, priv), nil +} + +func (xwingKEM) publicKeyPEM(pub []byte) (string, error) { + return rawToPEM(PEMBlockXWingPublicKey, pub, XWingPublicKeySize) +} + +// --- nistHybridKEM adapter -------------------------------------------------- + +type nistHybridKEM struct { + params *hybridNISTParams +} + +func (h nistHybridKEM) keyType() KeyType { return h.params.keyType } +func (nistHybridKEM) scheme() SchemeType { return Hybrid } +func (h nistHybridKEM) pubSize() int { return h.params.ecPubSize + h.params.mlkemPubSize } +func (h nistHybridKEM) privSize() int { return h.params.ecPrivSize + h.params.mlkemPrivSize } +func (h nistHybridKEM) ctSize() int { return h.params.ecPubSize + h.params.mlkemCtSize } + +// mlkemAdapter returns the ML-KEM half of this hybrid scheme. +func (h nistHybridKEM) mlkemAdapter() mlkemKEM { + if h.params.mlkemPubSize == MLKEM1024PublicKeySize { + return mlkemKEM{variant: mlkem1024} + } + return mlkemKEM{variant: mlkem768} +} + +func (h nistHybridKEM) encapsulate(pub []byte) ([]byte, []byte, error) { + if len(pub) != h.pubSize() { + return nil, nil, fmt.Errorf("invalid %s public key size: got %d want %d", h.keyType(), len(pub), h.pubSize()) + } + ecPubBytes := pub[:h.params.ecPubSize] + mlkemPubBytes := pub[h.params.ecPubSize:] + + ecPub, err := h.params.curve.NewPublicKey(ecPubBytes) + if err != nil { + return nil, nil, fmt.Errorf("invalid EC public key: %w", err) + } + ephemeral, err := h.params.curve.GenerateKey(rand.Reader) + if err != nil { + return nil, nil, fmt.Errorf("ECDH ephemeral key generation failed: %w", err) + } + ecdhSecret, err := ephemeral.ECDH(ecPub) + if err != nil { + return nil, nil, fmt.Errorf("ECDH failed: %w", err) + } + ephemeralPub := ephemeral.PublicKey().Bytes() + + mlkemSecret, mlkemCt, err := h.mlkemAdapter().encapsulate(mlkemPubBytes) + if err != nil { + return nil, nil, err + } + + combinedSecret := make([]byte, 0, len(ecdhSecret)+len(mlkemSecret)) + combinedSecret = append(combinedSecret, ecdhSecret...) + combinedSecret = append(combinedSecret, mlkemSecret...) + + hybridCt := make([]byte, 0, len(ephemeralPub)+len(mlkemCt)) + hybridCt = append(hybridCt, ephemeralPub...) + hybridCt = append(hybridCt, mlkemCt...) + + return combinedSecret, hybridCt, nil +} + +func (h nistHybridKEM) decapsulate(priv, ct []byte) ([]byte, error) { + if len(priv) != h.privSize() { + return nil, fmt.Errorf("invalid %s private key size: got %d want %d", h.keyType(), len(priv), h.privSize()) + } + if len(ct) != h.ctSize() { + return nil, fmt.Errorf("invalid %s ciphertext size: got %d want %d", h.keyType(), len(ct), h.ctSize()) + } + + ephemeralPubBytes := ct[:h.params.ecPubSize] + mlkemCtBytes := ct[h.params.ecPubSize:] + ecPrivBytes := priv[:h.params.ecPrivSize] + mlkemPrivBytes := priv[h.params.ecPrivSize:] + + ecPriv, err := h.params.curve.NewPrivateKey(ecPrivBytes) + if err != nil { + return nil, fmt.Errorf("invalid EC private key: %w", err) + } + ephemeralPub, err := h.params.curve.NewPublicKey(ephemeralPubBytes) + if err != nil { + return nil, fmt.Errorf("invalid ephemeral EC public key: %w", err) + } + ecdhSecret, err := ecPriv.ECDH(ephemeralPub) + if err != nil { + return nil, fmt.Errorf("ECDH failed: %w", err) + } + + // FIPS 203 §6.3 implicit rejection: a wrong-key ciphertext yields a + // pseudorandom shared secret without an error here; authentication is + // enforced by AES-GCM at the wrap layer. + mlkemSecret, err := h.mlkemAdapter().decapsulate(mlkemPrivBytes, mlkemCtBytes) + if err != nil { + return nil, err + } + + combinedSecret := make([]byte, 0, len(ecdhSecret)+len(mlkemSecret)) + combinedSecret = append(combinedSecret, ecdhSecret...) + combinedSecret = append(combinedSecret, mlkemSecret...) + + return combinedSecret, nil +} + +func (h nistHybridKEM) publicKeyPEM(pub []byte) (string, error) { + return rawToPEM(h.params.pubPEMBlock, pub, h.pubSize()) +} + +// --- wrap / unwrap ---------------------------------------------------------- + +// wrapDEKWithKEM encapsulates against pub, derives an AES-256 wrap key via +// HKDF-SHA256 over (sharedSecret, salt, info), and emits the kemEnvelope ASN.1 +// DER blob carrying (KEM ciphertext, AES-GCM-encrypted DEK). +func wrapDEKWithKEM(k kem, pub, dek, salt, info []byte) ([]byte, error) { + sharedSecret, ciphertext, err := k.encapsulate(pub) + if err != nil { + return nil, err + } + + wrapKey, err := deriveKEMWrapKey(sharedSecret, salt, info) + if err != nil { + return nil, err + } + + gcm, err := NewAESGcm(wrapKey) + if err != nil { + return nil, fmt.Errorf("NewAESGcm failed: %w", err) + } + + encryptedDEK, err := gcm.Encrypt(dek) + if err != nil { + return nil, fmt.Errorf("AES-GCM encrypt failed: %w", err) + } + + wrappedDER, err := asn1.Marshal(kemEnvelope{ + KEMCiphertext: ciphertext, + EncryptedDEK: encryptedDEK, + }) + if err != nil { + return nil, fmt.Errorf("asn1.Marshal failed: %w", err) + } + + return wrappedDER, nil +} + +// unwrapDEKWithKEM parses the kemEnvelope DER blob, decapsulates with priv to +// recover the shared secret, derives the same AES-256 wrap key, and AES-GCM +// decrypts the DEK. +func unwrapDEKWithKEM(k kem, priv, der, salt, info []byte) ([]byte, error) { + var env kemEnvelope + rest, err := asn1.Unmarshal(der, &env) + if err != nil { + return nil, fmt.Errorf("asn1.Unmarshal failed: %w", err) + } + if len(rest) != 0 { + return nil, fmt.Errorf("asn1.Unmarshal left %d trailing bytes", len(rest)) + } + if len(env.KEMCiphertext) != k.ctSize() { + return nil, fmt.Errorf("invalid %s ciphertext size: got %d want %d", k.keyType(), len(env.KEMCiphertext), k.ctSize()) + } + + sharedSecret, err := k.decapsulate(priv, env.KEMCiphertext) + if err != nil { + return nil, err + } + + wrapKey, err := deriveKEMWrapKey(sharedSecret, salt, info) + if err != nil { + return nil, err + } + + gcm, err := NewAESGcm(wrapKey) + if err != nil { + return nil, fmt.Errorf("NewAESGcm failed: %w", err) + } + + plaintext, err := gcm.Decrypt(env.EncryptedDEK) + if err != nil { + return nil, fmt.Errorf("AES-GCM decrypt failed: %w", err) + } + + return plaintext, nil +} + +func deriveKEMWrapKey(sharedSecret, salt, info []byte) ([]byte, error) { + if len(salt) == 0 { + salt = defaultTDFSalt() + } + hkdfObj := hkdf.New(sha256.New, sharedSecret, salt, info) + derivedKey := make([]byte, kemWrapKeySize) + if _, err := io.ReadFull(hkdfObj, derivedKey); err != nil { + return nil, fmt.Errorf("hkdf failure: %w", err) + } + return derivedKey, nil +} + +// --- unified encryptor / decryptor ------------------------------------------ + +// kemEncryptor satisfies PublicKeyEncryptor for every KEM family. It replaces +// the per-variant MLKEMEncryptor*, XWingEncryptor, and HybridNISTEncryptor +// types behind the FromPublicPEM factory. +type kemEncryptor struct { + k kem + publicKey []byte + salt []byte + info []byte +} + +func newKEMEncryptor(k kem, publicKey, salt, info []byte) (*kemEncryptor, error) { + if len(publicKey) != k.pubSize() { + return nil, fmt.Errorf("invalid %s public key size: got %d want %d", k.keyType(), len(publicKey), k.pubSize()) + } + return &kemEncryptor{ + k: k, + publicKey: append([]byte(nil), publicKey...), + salt: cloneOrNil(salt), + info: cloneOrNil(info), + }, nil +} + +func (e *kemEncryptor) Encrypt(data []byte) ([]byte, error) { + return wrapDEKWithKEM(e.k, e.publicKey, data, e.salt, e.info) +} + +func (e *kemEncryptor) PublicKeyInPemFormat() (string, error) { + return e.k.publicKeyPEM(e.publicKey) +} + +func (e *kemEncryptor) Type() SchemeType { return e.k.scheme() } +func (e *kemEncryptor) KeyType() KeyType { return e.k.keyType() } +func (e *kemEncryptor) EphemeralKey() []byte { return nil } + +func (e *kemEncryptor) Metadata() (map[string]string, error) { + return make(map[string]string), nil +} + +// kemDecryptor satisfies PrivateKeyDecryptor for every KEM family. It replaces +// the per-variant MLKEMDecryptor*, XWingDecryptor, and HybridNISTDecryptor +// types behind the FromPrivatePEM factory. +type kemDecryptor struct { + k kem + privateKey []byte + salt []byte + info []byte +} + +func newKEMDecryptor(k kem, privateKey, salt, info []byte) (*kemDecryptor, error) { + if len(privateKey) != k.privSize() { + return nil, fmt.Errorf("invalid %s private key size: got %d want %d", k.keyType(), len(privateKey), k.privSize()) + } + return &kemDecryptor{ + k: k, + privateKey: append([]byte(nil), privateKey...), + salt: cloneOrNil(salt), + info: cloneOrNil(info), + }, nil +} + +func (d *kemDecryptor) Decrypt(data []byte) ([]byte, error) { + return unwrapDEKWithKEM(d.k, d.privateKey, data, d.salt, d.info) +} diff --git a/lib/ocrypto/mlkem.go b/lib/ocrypto/mlkem.go index a2264ad83f..d879694cda 100644 --- a/lib/ocrypto/mlkem.go +++ b/lib/ocrypto/mlkem.go @@ -2,15 +2,10 @@ package ocrypto import ( "bytes" - "crypto/mlkem" - "crypto/sha256" "encoding/asn1" "encoding/pem" "errors" "fmt" - "io" - - "golang.org/x/crypto/hkdf" ) // PEM block types defined by RFC 7468 for SPKI / PKCS#8 envelopes. @@ -19,10 +14,10 @@ const ( pemBlockPrivateKey = "PRIVATE KEY" ) -// errNotMLKEM is returned by the ML-KEM SPKI / PKCS#8 parsers when the supplied -// DER blob is not an ML-KEM key, signalling the caller to fall through to -// other algorithm parsers. -var errNotMLKEM = errors.New("not an ML-KEM key") +// errNotKEM is returned by the generic SPKI / PKCS#8 KEM parsers when the +// supplied DER blob is not a recognised KEM algorithm, signalling the caller +// to fall through to other algorithm parsers. +var errNotKEM = errors.New("not a recognised KEM key") const ( MLKEM768PublicKeySize = 1184 // mlkem768 encapsulation key @@ -31,8 +26,6 @@ const ( MLKEM1024PublicKeySize = 1568 // mlkem1024 encapsulation key MLKEM1024PrivateKeySize = 64 // mlkem1024 seed (d || z) MLKEM1024CiphertextSize = 1568 // mlkem1024 ciphertext - - mlkemWrapKeySize = 32 // AES-256 key size for wrap key derivation ) // NIST-assigned OIDs for ML-KEM (FIPS 203). @@ -41,257 +34,110 @@ var ( OidMLKEM1024 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 4, 3} ) -type mlkemAlgorithmIdentifier struct { +type kemAlgorithmIdentifier struct { Algorithm asn1.ObjectIdentifier } -type mlkemSPKI struct { - Algorithm mlkemAlgorithmIdentifier +type kemSPKI struct { + Algorithm kemAlgorithmIdentifier PublicKey asn1.BitString } -// mlkemPKCS8 mirrors RFC 5958 OneAsymmetricKey v1. -type mlkemPKCS8 struct { +// kemPKCS8 mirrors RFC 5958 OneAsymmetricKey v1. +type kemPKCS8 struct { Version int - Algorithm mlkemAlgorithmIdentifier + Algorithm kemAlgorithmIdentifier PrivateKey []byte } const bitsPerByte = 8 -// marshalMLKEMPublicSPKI encodes a raw ML-KEM encapsulation key as RFC 5280 SubjectPublicKeyInfo. -func marshalMLKEMPublicSPKI(oid asn1.ObjectIdentifier, rawKey []byte) ([]byte, error) { - return asn1.Marshal(mlkemSPKI{ - Algorithm: mlkemAlgorithmIdentifier{Algorithm: oid}, +// marshalKEMPublicSPKI encodes a raw KEM encapsulation key as RFC 5280 +// SubjectPublicKeyInfo using the supplied algorithm OID. +func marshalKEMPublicSPKI(oid asn1.ObjectIdentifier, rawKey []byte) ([]byte, error) { + return asn1.Marshal(kemSPKI{ + Algorithm: kemAlgorithmIdentifier{Algorithm: oid}, PublicKey: asn1.BitString{Bytes: rawKey, BitLength: len(rawKey) * bitsPerByte}, }) } -// marshalMLKEMPrivatePKCS8 encodes the ML-KEM seed as RFC 5958 OneAsymmetricKey, -// with the inner ML-KEM-PrivateKey CHOICE selected as [0] IMPLICIT OCTET STRING (seed). -func marshalMLKEMPrivatePKCS8(oid asn1.ObjectIdentifier, seed []byte) ([]byte, error) { - inner, err := asn1.MarshalWithParams(seed, "tag:0,implicit") +// marshalKEMPrivatePKCS8 encodes a raw KEM seed (or private key) as RFC 5958 +// OneAsymmetricKey, with the inner KEM-PrivateKey CHOICE selected as [0] +// IMPLICIT OCTET STRING. +func marshalKEMPrivatePKCS8(oid asn1.ObjectIdentifier, rawSeedOrKey []byte) ([]byte, error) { + inner, err := asn1.MarshalWithParams(rawSeedOrKey, "tag:0,implicit") if err != nil { return nil, fmt.Errorf("asn1.MarshalWithParams seed failed: %w", err) } - return asn1.Marshal(mlkemPKCS8{ + return asn1.Marshal(kemPKCS8{ Version: 0, - Algorithm: mlkemAlgorithmIdentifier{Algorithm: oid}, + Algorithm: kemAlgorithmIdentifier{Algorithm: oid}, PrivateKey: inner, }) } -// ParseMLKEMPublicSPKI returns the OID and raw encapsulation key bytes from an -// SPKI DER blob if the algorithm is ML-KEM-768 or ML-KEM-1024. If the blob is -// not ML-KEM the sentinel errNotMLKEM is returned so the caller can fall -// through to other parsers. -func ParseMLKEMPublicSPKI(der []byte) (asn1.ObjectIdentifier, []byte, error) { - var s mlkemSPKI +// ParseKEMPublicSPKI returns the OID and raw encapsulation key bytes from any +// SPKI DER blob whose AlgorithmIdentifier has no parameters. If the blob is +// not a well-formed parameter-less SPKI structure the sentinel errNotKEM is +// returned so the caller can fall through to other parsers. +func ParseKEMPublicSPKI(der []byte) (asn1.ObjectIdentifier, []byte, error) { + var s kemSPKI rest, err := asn1.Unmarshal(der, &s) if err != nil || len(rest) != 0 { - return nil, nil, errNotMLKEM - } - var oid asn1.ObjectIdentifier - switch { - case s.Algorithm.Algorithm.Equal(OidMLKEM768): - oid = OidMLKEM768 - case s.Algorithm.Algorithm.Equal(OidMLKEM1024): - oid = OidMLKEM1024 - default: - return nil, nil, errNotMLKEM + return nil, nil, errNotKEM } if s.PublicKey.BitLength%bitsPerByte != 0 { - return nil, nil, errors.New("ML-KEM SPKI bit string is not byte-aligned") - } - return oid, s.PublicKey.RightAlign(), nil -} - -// parseMLKEMPrivatePKCS8 returns the OID and raw seed bytes from a PKCS#8 DER -// blob if the algorithm is ML-KEM-768 or ML-KEM-1024. If the blob is not -// ML-KEM the sentinel errNotMLKEM is returned so the caller can fall through -// to other parsers. -func parseMLKEMPrivatePKCS8(der []byte) (asn1.ObjectIdentifier, []byte, error) { - var p mlkemPKCS8 - rest, err := asn1.Unmarshal(der, &p) - if err != nil || len(rest) != 0 { - return nil, nil, errNotMLKEM - } - var oid asn1.ObjectIdentifier - switch { - case p.Algorithm.Algorithm.Equal(OidMLKEM768): - oid = OidMLKEM768 - case p.Algorithm.Algorithm.Equal(OidMLKEM1024): - oid = OidMLKEM1024 - default: - return nil, nil, errNotMLKEM - } - - var innerSeed []byte - innerRest, err := asn1.UnmarshalWithParams(p.PrivateKey, &innerSeed, "tag:0,implicit") - if err != nil || len(innerRest) != 0 { - return nil, nil, fmt.Errorf("ML-KEM PKCS#8 inner seed parse failed: %w", err) - } - return oid, innerSeed, nil -} - -type MLKEMWrappedKey struct { - MLKEMCiphertext []byte `asn1:"tag:0"` - EncryptedDEK []byte `asn1:"tag:1"` -} - -type MLKEMEncryptor768 struct { - publicKey []byte - salt []byte - info []byte -} - -type MLKEMDecryptor768 struct { - privateKey []byte - salt []byte - info []byte -} - -type MLKEMEncryptor1024 struct { - publicKey []byte - salt []byte - info []byte -} - -type MLKEMDecryptor1024 struct { - privateKey []byte - salt []byte - info []byte -} - -func NewMLKEM768Encryptor(publicKey, salt, info []byte) (*MLKEMEncryptor768, error) { - if len(publicKey) != MLKEM768PublicKeySize { - return nil, fmt.Errorf("invalid ML-KEM-768 public key size: got %d want %d", len(publicKey), MLKEM768PublicKeySize) + return nil, nil, errors.New("KEM SPKI bit string is not byte-aligned") } - - return &MLKEMEncryptor768{ - publicKey: append([]byte(nil), publicKey...), - salt: cloneOrNil(salt), - info: cloneOrNil(info), - }, nil -} - -func (e *MLKEMEncryptor768) Encrypt(data []byte) ([]byte, error) { - return mlkem768WrapDEK(e.publicKey, data, e.salt, e.info) + return s.Algorithm.Algorithm, s.PublicKey.RightAlign(), nil } -func (e *MLKEMEncryptor768) PublicKeyInPemFormat() (string, error) { - der, err := marshalMLKEMPublicSPKI(OidMLKEM768, e.publicKey) +// ParseMLKEMPublicSPKI returns the OID and raw encapsulation key bytes from an +// SPKI DER blob if the algorithm is ML-KEM-768 or ML-KEM-1024. +// +// Deprecated: Use ParseKEMPublicSPKI and verify the returned OID against the +// expected ML-KEM variant. +func ParseMLKEMPublicSPKI(der []byte) (asn1.ObjectIdentifier, []byte, error) { + oid, raw, err := ParseKEMPublicSPKI(der) if err != nil { - return "", fmt.Errorf("marshal ML-KEM-768 SPKI failed: %w", err) + return nil, nil, err } - return string(pem.EncodeToMemory(&pem.Block{Type: pemBlockPublicKey, Bytes: der})), nil -} - -func (e *MLKEMEncryptor768) Type() SchemeType { - return MLKEM -} - -func (e *MLKEMEncryptor768) KeyType() KeyType { - return MLKEM768Key -} - -func (e *MLKEMEncryptor768) EphemeralKey() []byte { - return nil -} - -func (e *MLKEMEncryptor768) Metadata() (map[string]string, error) { - return make(map[string]string), nil -} - -func NewMLKEM768Decryptor(privateKey []byte) (*MLKEMDecryptor768, error) { - return NewSaltedMLKEM768Decryptor(privateKey, defaultTDFSalt(), nil) -} - -func NewSaltedMLKEM768Decryptor(privateKey, salt, info []byte) (*MLKEMDecryptor768, error) { - if len(privateKey) != MLKEM768PrivateKeySize { - return nil, fmt.Errorf("invalid ML-KEM-768 private key size: got %d want %d", len(privateKey), MLKEM768PrivateKeySize) + if !oid.Equal(OidMLKEM768) && !oid.Equal(OidMLKEM1024) { + return nil, nil, errNotKEM } - - return &MLKEMDecryptor768{ - privateKey: append([]byte(nil), privateKey...), - salt: cloneOrNil(salt), - info: cloneOrNil(info), - }, nil -} - -func (d *MLKEMDecryptor768) Decrypt(data []byte) ([]byte, error) { - return mlkem768UnwrapDEK(d.privateKey, data, d.salt, d.info) + return oid, raw, nil } -func NewMLKEM1024Encryptor(publicKey, salt, info []byte) (*MLKEMEncryptor1024, error) { - if len(publicKey) != MLKEM1024PublicKeySize { - return nil, fmt.Errorf("invalid ML-KEM-1024 public key size: got %d want %d", len(publicKey), MLKEM1024PublicKeySize) +// parseKEMPrivatePKCS8 returns the OID and raw seed bytes from any PKCS#8 DER +// blob whose AlgorithmIdentifier matches a registered KEM scheme and whose +// inner private key is encoded as [0] IMPLICIT OCTET STRING. The sentinel +// errNotKEM is returned for any non-KEM PKCS#8 blob so the caller can fall +// through to other parsers. +func parseKEMPrivatePKCS8(der []byte) (asn1.ObjectIdentifier, []byte, error) { + var p kemPKCS8 + rest, err := asn1.Unmarshal(der, &p) + if err != nil || len(rest) != 0 { + return nil, nil, errNotKEM } - - return &MLKEMEncryptor1024{ - publicKey: append([]byte(nil), publicKey...), - salt: cloneOrNil(salt), - info: cloneOrNil(info), - }, nil -} - -func (e *MLKEMEncryptor1024) Encrypt(data []byte) ([]byte, error) { - return mlkem1024WrapDEK(e.publicKey, data, e.salt, e.info) -} - -func (e *MLKEMEncryptor1024) PublicKeyInPemFormat() (string, error) { - der, err := marshalMLKEMPublicSPKI(OidMLKEM1024, e.publicKey) - if err != nil { - return "", fmt.Errorf("marshal ML-KEM-1024 SPKI failed: %w", err) + if _, ok := kemRegistry[p.Algorithm.Algorithm.String()]; !ok { + return nil, nil, errNotKEM } - return string(pem.EncodeToMemory(&pem.Block{Type: pemBlockPublicKey, Bytes: der})), nil -} - -func (e *MLKEMEncryptor1024) Type() SchemeType { - return MLKEM -} - -func (e *MLKEMEncryptor1024) KeyType() KeyType { - return MLKEM1024Key -} -func (e *MLKEMEncryptor1024) EphemeralKey() []byte { - return nil -} - -func (e *MLKEMEncryptor1024) Metadata() (map[string]string, error) { - return make(map[string]string), nil -} - -func NewMLKEM1024Decryptor(privateKey []byte) (*MLKEMDecryptor1024, error) { - return NewSaltedMLKEM1024Decryptor(privateKey, defaultTDFSalt(), nil) -} - -func NewSaltedMLKEM1024Decryptor(privateKey, salt, info []byte) (*MLKEMDecryptor1024, error) { - if len(privateKey) != MLKEM1024PrivateKeySize { - return nil, fmt.Errorf("invalid ML-KEM-1024 private key size: got %d want %d", len(privateKey), MLKEM1024PrivateKeySize) + var innerSeed []byte + innerRest, err := asn1.UnmarshalWithParams(p.PrivateKey, &innerSeed, "tag:0,implicit") + if err != nil || len(innerRest) != 0 { + return nil, nil, fmt.Errorf("KEM PKCS#8 inner seed parse failed: %w", err) } - - return &MLKEMDecryptor1024{ - privateKey: append([]byte(nil), privateKey...), - salt: cloneOrNil(salt), - info: cloneOrNil(info), - }, nil -} - -func (d *MLKEMDecryptor1024) Decrypt(data []byte) ([]byte, error) { - return mlkem1024UnwrapDEK(d.privateKey, data, d.salt, d.info) + return p.Algorithm.Algorithm, innerSeed, nil } // normalizeMLKEMPublicKey detects the input format and returns raw key bytes. // Accepts: raw key (1184/1568 bytes), SPKI DER (1206/1590 bytes), or PEM-wrapped SPKI. func normalizeMLKEMPublicKey(input []byte, expectedRawSize int, expectedOID asn1.ObjectIdentifier) ([]byte, error) { - // Fast path: already raw? if len(input) == expectedRawSize { return input, nil } - // Check for PEM format if bytes.HasPrefix(input, []byte("-----BEGIN")) { block, _ := pem.Decode(input) if block == nil { @@ -300,25 +146,21 @@ func normalizeMLKEMPublicKey(input []byte, expectedRawSize int, expectedOID asn1 if block.Type != pemBlockPublicKey { return nil, fmt.Errorf("expected %s PEM block, got %s", pemBlockPublicKey, block.Type) } - // Continue with DER bytes input = block.Bytes } - // Try parsing as SPKI DER oid, rawKey, err := ParseMLKEMPublicSPKI(input) if err != nil { - if errors.Is(err, errNotMLKEM) { + if errors.Is(err, errNotKEM) { return nil, errors.New("not an ML-KEM key in SPKI format") } return nil, fmt.Errorf("failed to parse SPKI: %w", err) } - // Verify OID matches expected variant if !oid.Equal(expectedOID) { return nil, fmt.Errorf("OID mismatch: expected %v, got %v", expectedOID, oid) } - // Verify extracted key is correct size if len(rawKey) != expectedRawSize { return nil, fmt.Errorf("extracted key has wrong size: got %d want %d", len(rawKey), expectedRawSize) } @@ -326,230 +168,44 @@ func normalizeMLKEMPublicKey(input []byte, expectedRawSize int, expectedOID asn1 return rawKey, nil } +// MLKEM768WrapDEK encapsulates against an ML-KEM-768 public key (raw, SPKI +// DER, or PEM) and returns the ASN.1 DER envelope carrying the KEM ciphertext +// and AES-GCM-wrapped DEK. +// +// Deprecated: Use WrapDEK with MLKEM768Key, or construct via FromPublicPEM. func MLKEM768WrapDEK(publicKey, dek []byte) ([]byte, error) { - // Normalize input to raw key bytes (handles raw, SPKI DER, or PEM) rawKey, err := normalizeMLKEMPublicKey(publicKey, MLKEM768PublicKeySize, OidMLKEM768) if err != nil { return nil, fmt.Errorf("invalid ML-KEM-768 public key: %w", err) } - return mlkem768WrapDEK(rawKey, dek, defaultTDFSalt(), nil) + return wrapDEKWithKEM(mlkemKEM{variant: mlkem768}, rawKey, dek, defaultTDFSalt(), nil) } +// MLKEM768UnwrapDEK decapsulates the envelope produced by MLKEM768WrapDEK +// using the supplied raw ML-KEM-768 seed. This is the binary-bytes counterpart +// to FromPrivatePEM; callers that already hold a raw seed can use it directly +// without re-encoding to PKCS#8 PEM. func MLKEM768UnwrapDEK(privateKeyRaw, wrappedDER []byte) ([]byte, error) { - return mlkem768UnwrapDEK(privateKeyRaw, wrappedDER, defaultTDFSalt(), nil) + return unwrapDEKWithKEM(mlkemKEM{variant: mlkem768}, privateKeyRaw, wrappedDER, defaultTDFSalt(), nil) } +// MLKEM1024WrapDEK encapsulates against an ML-KEM-1024 public key (raw, SPKI +// DER, or PEM) and returns the ASN.1 DER envelope carrying the KEM ciphertext +// and AES-GCM-wrapped DEK. +// +// Deprecated: Use WrapDEK with MLKEM1024Key, or construct via FromPublicPEM. func MLKEM1024WrapDEK(publicKey, dek []byte) ([]byte, error) { - // Normalize input to raw key bytes (handles raw, SPKI DER, or PEM) rawKey, err := normalizeMLKEMPublicKey(publicKey, MLKEM1024PublicKeySize, OidMLKEM1024) if err != nil { return nil, fmt.Errorf("invalid ML-KEM-1024 public key: %w", err) } - return mlkem1024WrapDEK(rawKey, dek, defaultTDFSalt(), nil) + return wrapDEKWithKEM(mlkemKEM{variant: mlkem1024}, rawKey, dek, defaultTDFSalt(), nil) } +// MLKEM1024UnwrapDEK decapsulates the envelope produced by MLKEM1024WrapDEK +// using the supplied raw ML-KEM-1024 seed. This is the binary-bytes counterpart +// to FromPrivatePEM; callers that already hold a raw seed can use it directly +// without re-encoding to PKCS#8 PEM. func MLKEM1024UnwrapDEK(privateKeyRaw, wrappedDER []byte) ([]byte, error) { - return mlkem1024UnwrapDEK(privateKeyRaw, wrappedDER, defaultTDFSalt(), nil) -} - -// MLKEM768Encapsulate performs ML-KEM-768 encapsulation, returning the shared -// secret and ciphertext without applying KDF or encryption. -func MLKEM768Encapsulate(publicKeyRaw []byte) ([]byte, []byte, error) { - if len(publicKeyRaw) != MLKEM768PublicKeySize { - return nil, nil, fmt.Errorf("invalid ML-KEM-768 public key size: got %d want %d", len(publicKeyRaw), MLKEM768PublicKeySize) - } - - ek, err := mlkem.NewEncapsulationKey768(publicKeyRaw) - if err != nil { - return nil, nil, fmt.Errorf("mlkem.NewEncapsulationKey768 failed: %w", err) - } - - sharedSecret, ciphertext := ek.Encapsulate() - - return sharedSecret, ciphertext, nil -} - -// MLKEM1024Encapsulate performs ML-KEM-1024 encapsulation, returning the shared -// secret and ciphertext without applying KDF or encryption. -func MLKEM1024Encapsulate(publicKeyRaw []byte) ([]byte, []byte, error) { - if len(publicKeyRaw) != MLKEM1024PublicKeySize { - return nil, nil, fmt.Errorf("invalid ML-KEM-1024 public key size: got %d want %d", len(publicKeyRaw), MLKEM1024PublicKeySize) - } - - ek, err := mlkem.NewEncapsulationKey1024(publicKeyRaw) - if err != nil { - return nil, nil, fmt.Errorf("mlkem.NewEncapsulationKey1024 failed: %w", err) - } - - sharedSecret, ciphertext := ek.Encapsulate() - - return sharedSecret, ciphertext, nil -} - -func mlkem768WrapDEK(publicKeyRaw, dek, salt, info []byte) ([]byte, error) { - sharedSecret, ciphertext, err := MLKEM768Encapsulate(publicKeyRaw) - if err != nil { - return nil, err - } - - wrapKey, err := deriveMLKEMWrapKey(sharedSecret, salt, info) - if err != nil { - return nil, err - } - - gcm, err := NewAESGcm(wrapKey) - if err != nil { - return nil, fmt.Errorf("NewAESGcm failed: %w", err) - } - - encryptedDEK, err := gcm.Encrypt(dek) - if err != nil { - return nil, fmt.Errorf("AES-GCM encrypt failed: %w", err) - } - - wrappedDER, err := asn1.Marshal(MLKEMWrappedKey{ - MLKEMCiphertext: ciphertext, - EncryptedDEK: encryptedDEK, - }) - if err != nil { - return nil, fmt.Errorf("asn1.Marshal failed: %w", err) - } - - return wrappedDER, nil -} - -func mlkem768UnwrapDEK(privateKeyRaw, wrappedDER, salt, info []byte) ([]byte, error) { - if len(privateKeyRaw) != MLKEM768PrivateKeySize { - return nil, fmt.Errorf("invalid ML-KEM-768 private key size: got %d want %d", len(privateKeyRaw), MLKEM768PrivateKeySize) - } - - var wrappedKey MLKEMWrappedKey - rest, err := asn1.Unmarshal(wrappedDER, &wrappedKey) - if err != nil { - return nil, fmt.Errorf("asn1.Unmarshal failed: %w", err) - } - if len(rest) != 0 { - return nil, fmt.Errorf("asn1.Unmarshal left %d trailing bytes", len(rest)) - } - if len(wrappedKey.MLKEMCiphertext) != MLKEM768CiphertextSize { - return nil, fmt.Errorf("invalid ML-KEM-768 ciphertext size: got %d want %d", len(wrappedKey.MLKEMCiphertext), MLKEM768CiphertextSize) - } - - dk, err := mlkem.NewDecapsulationKey768(privateKeyRaw) - if err != nil { - return nil, fmt.Errorf("mlkem.NewDecapsulationKey768 failed: %w", err) - } - - sharedSecret, err := dk.Decapsulate(wrappedKey.MLKEMCiphertext) - if err != nil { - return nil, fmt.Errorf("mlkem768 decapsulate failed: %w", err) - } - - wrapKey, err := deriveMLKEMWrapKey(sharedSecret, salt, info) - if err != nil { - return nil, err - } - - gcm, err := NewAESGcm(wrapKey) - if err != nil { - return nil, fmt.Errorf("NewAESGcm failed: %w", err) - } - - plaintext, err := gcm.Decrypt(wrappedKey.EncryptedDEK) - if err != nil { - return nil, fmt.Errorf("AES-GCM decrypt failed: %w", err) - } - - return plaintext, nil -} - -func mlkem1024WrapDEK(publicKeyRaw, dek, salt, info []byte) ([]byte, error) { - sharedSecret, ciphertext, err := MLKEM1024Encapsulate(publicKeyRaw) - if err != nil { - return nil, err - } - - wrapKey, err := deriveMLKEMWrapKey(sharedSecret, salt, info) - if err != nil { - return nil, err - } - - gcm, err := NewAESGcm(wrapKey) - if err != nil { - return nil, fmt.Errorf("NewAESGcm failed: %w", err) - } - - encryptedDEK, err := gcm.Encrypt(dek) - if err != nil { - return nil, fmt.Errorf("AES-GCM encrypt failed: %w", err) - } - - wrappedDER, err := asn1.Marshal(MLKEMWrappedKey{ - MLKEMCiphertext: ciphertext, - EncryptedDEK: encryptedDEK, - }) - if err != nil { - return nil, fmt.Errorf("asn1.Marshal failed: %w", err) - } - - return wrappedDER, nil -} - -func mlkem1024UnwrapDEK(privateKeyRaw, wrappedDER, salt, info []byte) ([]byte, error) { - if len(privateKeyRaw) != MLKEM1024PrivateKeySize { - return nil, fmt.Errorf("invalid ML-KEM-1024 private key size: got %d want %d", len(privateKeyRaw), MLKEM1024PrivateKeySize) - } - - var wrappedKey MLKEMWrappedKey - rest, err := asn1.Unmarshal(wrappedDER, &wrappedKey) - if err != nil { - return nil, fmt.Errorf("asn1.Unmarshal failed: %w", err) - } - if len(rest) != 0 { - return nil, fmt.Errorf("asn1.Unmarshal left %d trailing bytes", len(rest)) - } - if len(wrappedKey.MLKEMCiphertext) != MLKEM1024CiphertextSize { - return nil, fmt.Errorf("invalid ML-KEM-1024 ciphertext size: got %d want %d", len(wrappedKey.MLKEMCiphertext), MLKEM1024CiphertextSize) - } - - dk, err := mlkem.NewDecapsulationKey1024(privateKeyRaw) - if err != nil { - return nil, fmt.Errorf("mlkem.NewDecapsulationKey1024 failed: %w", err) - } - - sharedSecret, err := dk.Decapsulate(wrappedKey.MLKEMCiphertext) - if err != nil { - return nil, fmt.Errorf("mlkem1024 decapsulate failed: %w", err) - } - - wrapKey, err := deriveMLKEMWrapKey(sharedSecret, salt, info) - if err != nil { - return nil, err - } - - gcm, err := NewAESGcm(wrapKey) - if err != nil { - return nil, fmt.Errorf("NewAESGcm failed: %w", err) - } - - plaintext, err := gcm.Decrypt(wrappedKey.EncryptedDEK) - if err != nil { - return nil, fmt.Errorf("AES-GCM decrypt failed: %w", err) - } - - return plaintext, nil -} - -func deriveMLKEMWrapKey(sharedSecret, salt, info []byte) ([]byte, error) { - if len(salt) == 0 { - salt = defaultTDFSalt() - } - - hkdfObj := hkdf.New(sha256.New, sharedSecret, salt, info) - derivedKey := make([]byte, mlkemWrapKeySize) - if _, err := io.ReadFull(hkdfObj, derivedKey); err != nil { - return nil, fmt.Errorf("hkdf failure: %w", err) - } - - return derivedKey, nil + return unwrapDEKWithKEM(mlkemKEM{variant: mlkem1024}, privateKeyRaw, wrappedDER, defaultTDFSalt(), nil) } diff --git a/lib/ocrypto/mlkem_format_test.go b/lib/ocrypto/mlkem_format_test.go index 13b74d39f7..0e954eee87 100644 --- a/lib/ocrypto/mlkem_format_test.go +++ b/lib/ocrypto/mlkem_format_test.go @@ -23,7 +23,7 @@ func TestMLKEM768WrapDEKFormats(t *testing.T) { require.NoError(t, err, "Should accept raw key") // Test 2: SPKI DER (1206 bytes) - spkiDER, err := marshalMLKEMPublicSPKI(OidMLKEM768, rawKey) + spkiDER, err := marshalKEMPublicSPKI(OidMLKEM768, rawKey) require.NoError(t, err) require.Greater(t, len(spkiDER), len(rawKey), "SPKI DER should be larger than raw key") @@ -68,7 +68,7 @@ func TestMLKEM1024WrapDEKFormats(t *testing.T) { require.NoError(t, err, "Should accept raw key") // Test 2: SPKI DER (1590 bytes) - spkiDER, err := marshalMLKEMPublicSPKI(OidMLKEM1024, rawKey) + spkiDER, err := marshalKEMPublicSPKI(OidMLKEM1024, rawKey) require.NoError(t, err) require.Greater(t, len(spkiDER), len(rawKey), "SPKI DER should be larger than raw key") @@ -110,7 +110,7 @@ func TestMLKEM768WrapDEKInvalidFormats(t *testing.T) { // Wrong OID in SPKI keyPair1024, err := NewMLKEM1024KeyPair() require.NoError(t, err) - spki1024, err := marshalMLKEMPublicSPKI(OidMLKEM1024, keyPair1024.PrivateKey.EncapsulationKey().Bytes()) + spki1024, err := marshalKEMPublicSPKI(OidMLKEM1024, keyPair1024.PrivateKey.EncapsulationKey().Bytes()) require.NoError(t, err) _, err = MLKEM768WrapDEK(spki1024, dek) diff --git a/lib/ocrypto/mlkem_test.go b/lib/ocrypto/mlkem_test.go index 185c568cc8..47045b3059 100644 --- a/lib/ocrypto/mlkem_test.go +++ b/lib/ocrypto/mlkem_test.go @@ -76,16 +76,16 @@ func TestMLKEM1024WrapUnwrapWrongKeyFails(t *testing.T) { assert.Contains(t, err.Error(), "AES-GCM decrypt failed") } -func TestMLKEMWrappedKeyASN1RoundTrip(t *testing.T) { - original := MLKEMWrappedKey{ - MLKEMCiphertext: []byte("ciphertext"), - EncryptedDEK: []byte("encrypted-dek"), +func TestKEMEnvelopeASN1RoundTrip(t *testing.T) { + original := kemEnvelope{ + KEMCiphertext: []byte("ciphertext"), + EncryptedDEK: []byte("encrypted-dek"), } der, err := asn1.Marshal(original) require.NoError(t, err) - var decoded MLKEMWrappedKey + var decoded kemEnvelope rest, err := asn1.Unmarshal(der, &decoded) require.NoError(t, err) assert.Empty(t, rest) @@ -98,9 +98,9 @@ func TestMLKEM768CiphertextSizeValidation(t *testing.T) { privateKeyBytes := keyPair.PrivateKey.Bytes() - invalidWrapped := MLKEMWrappedKey{ - MLKEMCiphertext: []byte("too-short"), - EncryptedDEK: []byte("encrypted-dek"), + invalidWrapped := kemEnvelope{ + KEMCiphertext: []byte("too-short"), + EncryptedDEK: []byte("encrypted-dek"), } der, err := asn1.Marshal(invalidWrapped) @@ -108,7 +108,7 @@ func TestMLKEM768CiphertextSizeValidation(t *testing.T) { _, err = MLKEM768UnwrapDEK(privateKeyBytes, der) require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ML-KEM-768 ciphertext size") + assert.Contains(t, err.Error(), "ciphertext size") } func TestMLKEM1024CiphertextSizeValidation(t *testing.T) { @@ -117,9 +117,9 @@ func TestMLKEM1024CiphertextSizeValidation(t *testing.T) { privateKeyBytes := keyPair.PrivateKey.Bytes() - invalidWrapped := MLKEMWrappedKey{ - MLKEMCiphertext: []byte("too-short"), - EncryptedDEK: []byte("encrypted-dek"), + invalidWrapped := kemEnvelope{ + KEMCiphertext: []byte("too-short"), + EncryptedDEK: []byte("encrypted-dek"), } der, err := asn1.Marshal(invalidWrapped) @@ -127,7 +127,7 @@ func TestMLKEM1024CiphertextSizeValidation(t *testing.T) { _, err = MLKEM1024UnwrapDEK(privateKeyBytes, der) require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ML-KEM-1024 ciphertext size") + assert.Contains(t, err.Error(), "ciphertext size") } func TestMLKEMCustomSaltInfo(t *testing.T) { @@ -140,10 +140,10 @@ func TestMLKEMCustomSaltInfo(t *testing.T) { customSalt := []byte("custom-salt-value") customInfo := []byte("custom-info-value") - encryptor, err := NewMLKEM768Encryptor(publicKeyBytes, customSalt, customInfo) + encryptor, err := newKEMEncryptor(mlkemKEM{variant: mlkem768}, publicKeyBytes, customSalt, customInfo) require.NoError(t, err) - decryptor, err := NewSaltedMLKEM768Decryptor(privateKeyBytes, customSalt, customInfo) + decryptor, err := newKEMDecryptor(mlkemKEM{variant: mlkem768}, privateKeyBytes, customSalt, customInfo) require.NoError(t, err) dek := []byte("test-dek-value-123456") @@ -161,7 +161,7 @@ func TestMLKEMEncryptorImplementsInterface(t *testing.T) { publicKeyBytes := keyPair.PrivateKey.EncapsulationKey().Bytes() - encryptor, err := NewMLKEM768Encryptor(publicKeyBytes, nil, nil) + encryptor, err := newKEMEncryptor(mlkemKEM{variant: mlkem768}, publicKeyBytes, nil, nil) require.NoError(t, err) assert.Equal(t, MLKEM, encryptor.Type()) @@ -179,7 +179,7 @@ func TestMLKEM768Encapsulate(t *testing.T) { publicKeyBytes := keyPair.PrivateKey.EncapsulationKey().Bytes() - sharedSecret, ciphertext, err := MLKEM768Encapsulate(publicKeyBytes) + sharedSecret, ciphertext, err := mlkemKEM{variant: mlkem768}.encapsulate(publicKeyBytes) require.NoError(t, err) assert.Len(t, sharedSecret, 32) assert.Len(t, ciphertext, MLKEM768CiphertextSize) @@ -191,22 +191,22 @@ func TestMLKEM1024Encapsulate(t *testing.T) { publicKeyBytes := keyPair.PrivateKey.EncapsulationKey().Bytes() - sharedSecret, ciphertext, err := MLKEM1024Encapsulate(publicKeyBytes) + sharedSecret, ciphertext, err := mlkemKEM{variant: mlkem1024}.encapsulate(publicKeyBytes) require.NoError(t, err) assert.Len(t, sharedSecret, 32) assert.Len(t, ciphertext, MLKEM1024CiphertextSize) } func TestMLKEM768EncapsulateInvalidKeySize(t *testing.T) { - _, _, err := MLKEM768Encapsulate([]byte("too-short")) + _, _, err := mlkemKEM{variant: mlkem768}.encapsulate([]byte("too-short")) require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ML-KEM-768 public key size") + assert.Contains(t, err.Error(), "public key size") } func TestMLKEM1024EncapsulateInvalidKeySize(t *testing.T) { - _, _, err := MLKEM1024Encapsulate([]byte("too-short")) + _, _, err := mlkemKEM{variant: mlkem1024}.encapsulate([]byte("too-short")) require.Error(t, err) - assert.Contains(t, err.Error(), "invalid ML-KEM-1024 public key size") + assert.Contains(t, err.Error(), "public key size") } func TestMLKEM768PEMRoundTrip(t *testing.T) { diff --git a/lib/ocrypto/xwing.go b/lib/ocrypto/xwing.go index 1ea9d5ab25..ce8ac47c01 100644 --- a/lib/ocrypto/xwing.go +++ b/lib/ocrypto/xwing.go @@ -2,14 +2,9 @@ package ocrypto import ( "crypto/rand" - "crypto/sha256" - "encoding/asn1" - "encoding/pem" "fmt" - "io" "github.com/cloudflare/circl/kem/xwing" - "golang.org/x/crypto/hkdf" ) const ( @@ -23,28 +18,11 @@ const ( PEMBlockXWingPrivateKey = "XWING PRIVATE KEY" ) -type XWingWrappedKey struct { - XWingCiphertext []byte `asn1:"tag:0"` - EncryptedDEK []byte `asn1:"tag:1"` -} - type XWingKeyPair struct { publicKey []byte privateKey []byte } -type XWingEncryptor struct { - publicKey []byte - salt []byte - info []byte -} - -type XWingDecryptor struct { - privateKey []byte - salt []byte - info []byte -} - func NewXWingKeyPair() (XWingKeyPair, error) { sk, pk, err := xwing.GenerateKeyPair(rand.Reader) if err != nil { @@ -82,179 +60,18 @@ func XWingPrivateKeyFromPem(data []byte) ([]byte, error) { return decodeSizedPEMBlock(data, PEMBlockXWingPrivateKey, XWingPrivateKeySize) } -func NewXWingEncryptor(publicKey, salt, info []byte) (*XWingEncryptor, error) { - if len(publicKey) != XWingPublicKeySize { - return nil, fmt.Errorf("invalid X-Wing public key size: got %d want %d", len(publicKey), XWingPublicKeySize) - } - - return &XWingEncryptor{ - publicKey: append([]byte(nil), publicKey...), - salt: cloneOrNil(salt), - info: cloneOrNil(info), - }, nil -} - -func (e *XWingEncryptor) Encrypt(data []byte) ([]byte, error) { - return xwingWrapDEK(e.publicKey, data, e.salt, e.info) -} - -func (e *XWingEncryptor) PublicKeyInPemFormat() (string, error) { - return rawToPEM(PEMBlockXWingPublicKey, e.publicKey, XWingPublicKeySize) -} - -func (e *XWingEncryptor) Type() SchemeType { - return Hybrid -} - -func (e *XWingEncryptor) KeyType() KeyType { - return HybridXWingKey -} - -func (e *XWingEncryptor) EphemeralKey() []byte { - return nil -} - -func (e *XWingEncryptor) Metadata() (map[string]string, error) { - return make(map[string]string), nil -} - -func NewXWingDecryptor(privateKey []byte) (*XWingDecryptor, error) { - return NewSaltedXWingDecryptor(privateKey, defaultTDFSalt(), nil) -} - -func NewSaltedXWingDecryptor(privateKey, salt, info []byte) (*XWingDecryptor, error) { - if len(privateKey) != XWingPrivateKeySize { - return nil, fmt.Errorf("invalid X-Wing private key size: got %d want %d", len(privateKey), XWingPrivateKeySize) - } - - return &XWingDecryptor{ - privateKey: append([]byte(nil), privateKey...), - salt: cloneOrNil(salt), - info: cloneOrNil(info), - }, nil -} - -func (d *XWingDecryptor) Decrypt(data []byte) ([]byte, error) { - return xwingUnwrapDEK(d.privateKey, data, d.salt, d.info) -} - +// XWingWrapDEK encapsulates against an X-Wing public key and returns the +// ASN.1 DER envelope carrying the KEM ciphertext and AES-GCM-wrapped DEK. +// +// Deprecated: Use WrapDEK with HybridXWingKey, or construct via FromPublicPEM. func XWingWrapDEK(publicKeyRaw, dek []byte) ([]byte, error) { - return xwingWrapDEK(publicKeyRaw, dek, defaultTDFSalt(), nil) + return wrapDEKWithKEM(xwingKEM{}, publicKeyRaw, dek, defaultTDFSalt(), nil) } +// XWingUnwrapDEK decapsulates the envelope produced by XWingWrapDEK using the +// supplied raw X-Wing private key. This is the binary-bytes counterpart to +// FromPrivatePEM (which works from PEM); callers that already hold raw key +// material can use it directly without re-encoding to PEM. func XWingUnwrapDEK(privateKeyRaw, wrappedDER []byte) ([]byte, error) { - return xwingUnwrapDEK(privateKeyRaw, wrappedDER, defaultTDFSalt(), nil) -} - -// XWingEncapsulate performs the X-Wing KEM encapsulation, returning the shared -// secret and ciphertext without applying KDF or encryption. -func XWingEncapsulate(publicKeyRaw []byte) ([]byte, []byte, error) { - if len(publicKeyRaw) != XWingPublicKeySize { - return nil, nil, fmt.Errorf("invalid X-Wing public key size: got %d want %d", len(publicKeyRaw), XWingPublicKeySize) - } - - sharedSecret, ciphertext, err := xwing.Encapsulate(publicKeyRaw, nil) - if err != nil { - return nil, nil, fmt.Errorf("xwing.Encapsulate failed: %w", err) - } - - return sharedSecret, ciphertext, nil -} - -func xwingWrapDEK(publicKeyRaw, dek, salt, info []byte) ([]byte, error) { - sharedSecret, ciphertext, err := XWingEncapsulate(publicKeyRaw) - if err != nil { - return nil, err - } - - wrapKey, err := deriveXWingWrapKey(sharedSecret, salt, info) - if err != nil { - return nil, err - } - - gcm, err := NewAESGcm(wrapKey) - if err != nil { - return nil, fmt.Errorf("NewAESGcm failed: %w", err) - } - - encryptedDEK, err := gcm.Encrypt(dek) - if err != nil { - return nil, fmt.Errorf("AES-GCM encrypt failed: %w", err) - } - - wrappedDER, err := asn1.Marshal(XWingWrappedKey{ - XWingCiphertext: ciphertext, - EncryptedDEK: encryptedDEK, - }) - if err != nil { - return nil, fmt.Errorf("asn1.Marshal failed: %w", err) - } - - return wrappedDER, nil -} - -func xwingUnwrapDEK(privateKeyRaw, wrappedDER, salt, info []byte) ([]byte, error) { - if len(privateKeyRaw) != XWingPrivateKeySize { - return nil, fmt.Errorf("invalid X-Wing private key size: got %d want %d", len(privateKeyRaw), XWingPrivateKeySize) - } - - var wrappedKey XWingWrappedKey - rest, err := asn1.Unmarshal(wrappedDER, &wrappedKey) - if err != nil { - return nil, fmt.Errorf("asn1.Unmarshal failed: %w", err) - } - if len(rest) != 0 { - return nil, fmt.Errorf("asn1.Unmarshal left %d trailing bytes", len(rest)) - } - if len(wrappedKey.XWingCiphertext) != XWingCiphertextSize { - return nil, fmt.Errorf("invalid X-Wing ciphertext size: got %d want %d", len(wrappedKey.XWingCiphertext), XWingCiphertextSize) - } - - sharedSecret := xwing.Decapsulate(wrappedKey.XWingCiphertext, privateKeyRaw) - - wrapKey, err := deriveXWingWrapKey(sharedSecret, salt, info) - if err != nil { - return nil, err - } - - gcm, err := NewAESGcm(wrapKey) - if err != nil { - return nil, fmt.Errorf("NewAESGcm failed: %w", err) - } - - plaintext, err := gcm.Decrypt(wrappedKey.EncryptedDEK) - if err != nil { - return nil, fmt.Errorf("AES-GCM decrypt failed: %w", err) - } - - return plaintext, nil -} - -func deriveXWingWrapKey(sharedSecret, salt, info []byte) ([]byte, error) { - if len(salt) == 0 { - salt = defaultTDFSalt() - } - - hkdfObj := hkdf.New(sha256.New, sharedSecret, salt, info) - derivedKey := make([]byte, xwing.SharedKeySize) - if _, err := io.ReadFull(hkdfObj, derivedKey); err != nil { - return nil, fmt.Errorf("hkdf failure: %w", err) - } - - return derivedKey, nil -} - -func decodeSizedPEMBlock(data []byte, blockType string, expectedSize int) ([]byte, error) { - block, _ := pem.Decode(data) - if block == nil { - return nil, fmt.Errorf("failed to parse PEM formatted %s", blockType) - } - if block.Type != blockType { - return nil, fmt.Errorf("unexpected PEM block type: got %s want %s", block.Type, blockType) - } - if len(block.Bytes) != expectedSize { - return nil, fmt.Errorf("invalid %s size: got %d want %d", blockType, len(block.Bytes), expectedSize) - } - - return append([]byte(nil), block.Bytes...), nil + return unwrapDEKWithKEM(xwingKEM{}, privateKeyRaw, wrappedDER, defaultTDFSalt(), nil) } diff --git a/lib/ocrypto/xwing_test.go b/lib/ocrypto/xwing_test.go index c0bf783ba9..1e363c93fc 100644 --- a/lib/ocrypto/xwing_test.go +++ b/lib/ocrypto/xwing_test.go @@ -60,16 +60,16 @@ func TestXWingWrapUnwrapWrongKeyFails(t *testing.T) { assert.Contains(t, err.Error(), "AES-GCM decrypt failed") } -func TestXWingWrappedKeyASN1RoundTrip(t *testing.T) { - original := XWingWrappedKey{ - XWingCiphertext: []byte("ciphertext"), - EncryptedDEK: []byte("encrypted-dek"), +func TestXWingEnvelopeASN1RoundTrip(t *testing.T) { + original := kemEnvelope{ + KEMCiphertext: []byte("ciphertext"), + EncryptedDEK: []byte("encrypted-dek"), } der, err := asn1.Marshal(original) require.NoError(t, err) - var decoded XWingWrappedKey + var decoded kemEnvelope rest, err := asn1.Unmarshal(der, &decoded) require.NoError(t, err) assert.Empty(t, rest) @@ -91,23 +91,18 @@ func TestXWingPEMDispatch(t *testing.T) { decryptor, err := FromPrivatePEMWithSalt(privatePEM, []byte("salt"), []byte("info")) require.NoError(t, err) - xwingEncryptor, ok := encryptor.(*XWingEncryptor) - require.True(t, ok) - assert.Equal(t, Hybrid, xwingEncryptor.Type()) - assert.Equal(t, HybridXWingKey, xwingEncryptor.KeyType()) - assert.Nil(t, xwingEncryptor.EphemeralKey()) + assert.Equal(t, Hybrid, encryptor.Type()) + assert.Equal(t, HybridXWingKey, encryptor.KeyType()) + assert.Nil(t, encryptor.EphemeralKey()) - metadata, err := xwingEncryptor.Metadata() + metadata, err := encryptor.Metadata() require.NoError(t, err) assert.Empty(t, metadata) - xwingDecryptor, ok := decryptor.(*XWingDecryptor) - require.True(t, ok) - - wrapped, err := xwingEncryptor.Encrypt([]byte("dispatch-dek")) + wrapped, err := encryptor.Encrypt([]byte("dispatch-dek")) require.NoError(t, err) - plaintext, err := xwingDecryptor.Decrypt(wrapped) + plaintext, err := decryptor.Decrypt(wrapped) require.NoError(t, err) assert.Equal(t, []byte("dispatch-dek"), plaintext) } @@ -116,14 +111,14 @@ func TestXWingEncapsulate(t *testing.T) { keyPair, err := NewXWingKeyPair() require.NoError(t, err) - sharedSecret, ciphertext, err := XWingEncapsulate(keyPair.publicKey) + sharedSecret, ciphertext, err := xwingKEM{}.encapsulate(keyPair.publicKey) require.NoError(t, err) assert.Len(t, sharedSecret, 32) assert.Len(t, ciphertext, XWingCiphertextSize) } func TestXWingEncapsulateInvalidKeySize(t *testing.T) { - _, _, err := XWingEncapsulate([]byte("too-short")) + _, _, err := xwingKEM{}.encapsulate([]byte("too-short")) require.Error(t, err) - assert.Contains(t, err.Error(), "invalid X-Wing public key size") + assert.Contains(t, err.Error(), "X-Wing public key size") } diff --git a/sdk/experimental/tdf/key_access.go b/sdk/experimental/tdf/key_access.go index 534aa39b6a..ad849c739c 100644 --- a/sdk/experimental/tdf/key_access.go +++ b/sdk/experimental/tdf/key_access.go @@ -165,17 +165,13 @@ func wrapKeyWithPublicKey(symKey []byte, pubKeyInfo keysplit.KASPublicKey) (stri // Determine key type based on algorithm ktype := ocrypto.KeyType(pubKeyInfo.Algorithm) - if ocrypto.IsHybridKeyType(ktype) { - return wrapKeyWithHybrid(ktype, pubKeyInfo.PEM, symKey) + if ocrypto.IsKEMKeyType(ktype) { + return wrapKeyWithKEM(ktype, pubKeyInfo.PEM, symKey) } if ocrypto.IsECKeyType(ktype) { // Handle EC key wrapping return wrapKeyWithEC(ktype, pubKeyInfo.PEM, symKey) } - if ocrypto.IsMLKEMKeyType(ktype) { - // Handle ML-KEM key wrapping - return wrapKeyWithMLKEM(ktype, pubKeyInfo.PEM, symKey) - } // Handle RSA key wrapping wrapped, err := wrapKeyWithRSA(pubKeyInfo.PEM, symKey) return wrapped, "wrapped", "", err @@ -253,30 +249,18 @@ func wrapKeyWithRSA(kasPublicKeyPEM string, symKey []byte) (string, error) { return string(ocrypto.Base64Encode(encryptedKey)), nil } -func wrapKeyWithHybrid(ktype ocrypto.KeyType, kasPublicKeyPEM string, symKey []byte) (string, string, string, error) { - wrappedDER, err := ocrypto.HybridWrapDEK(ktype, kasPublicKeyPEM, symKey) +// wrapKeyWithKEM wraps a DEK with any KEM scheme — pure ML-KEM or hybrid +// (X-Wing, NIST PQ/T). Returns the base64-encoded envelope, the manifest +// scheme name (`hybrid-wrapped` or `mlkem-wrapped`), and an empty ephemeral +// key string (KEMs do not emit one in this profile). +func wrapKeyWithKEM(ktype ocrypto.KeyType, kasPublicKeyPEM string, symKey []byte) (string, string, string, error) { + wrappedDER, err := ocrypto.WrapDEK(ktype, kasPublicKeyPEM, symKey) if err != nil { - return "", "", "", fmt.Errorf("hybrid wrap failed: %w", err) - } - return string(ocrypto.Base64Encode(wrappedDER)), "hybrid-wrapped", "", nil -} - -func wrapKeyWithMLKEM(ktype ocrypto.KeyType, kasPublicKeyPEM string, symKey []byte) (string, string, string, error) { - var wrappedDER []byte - var err error - - switch ktype { //nolint:exhaustive // only handle mlkem types - case ocrypto.MLKEM768Key: - wrappedDER, err = ocrypto.MLKEM768WrapDEK([]byte(kasPublicKeyPEM), symKey) - case ocrypto.MLKEM1024Key: - wrappedDER, err = ocrypto.MLKEM1024WrapDEK([]byte(kasPublicKeyPEM), symKey) - default: - return "", "", "", fmt.Errorf("unsupported ML-KEM key type: %s", ktype) + return "", "", "", fmt.Errorf("kem wrap failed: %w", err) } - - if err != nil { - return "", "", "", fmt.Errorf("mlkem wrap failed: %w", err) + scheme := "hybrid-wrapped" + if ocrypto.IsMLKEMKeyType(ktype) { + scheme = "mlkem-wrapped" } - - return string(ocrypto.Base64Encode(wrappedDER)), "mlkem-wrapped", "", nil + return string(ocrypto.Base64Encode(wrappedDER)), scheme, "", nil } diff --git a/sdk/tdf.go b/sdk/tdf.go index 2aea39637e..ce42099603 100644 --- a/sdk/tdf.go +++ b/sdk/tdf.go @@ -677,12 +677,12 @@ func createKeyAccess(kasInfo KASInfo, symKey []byte, policyBinding PolicyBinding ktype := ocrypto.KeyType(kasInfo.Algorithm) switch { - case ocrypto.IsHybridKeyType(ktype): - wrappedKey, err := generateWrapKeyWithHybrid(kasInfo.Algorithm, kasInfo.PublicKey, symKey) + case ocrypto.IsKEMKeyType(ktype): + wrappedKey, scheme, err := generateWrapKeyWithKEM(ktype, kasInfo.PublicKey, symKey) if err != nil { return KeyAccess{}, err } - keyAccess.KeyType = kHybridWrapped + keyAccess.KeyType = scheme keyAccess.WrappedKey = wrappedKey case ocrypto.IsECKeyType(ktype): mode, err := ocrypto.ECKeyTypeToMode(ktype) @@ -696,13 +696,6 @@ func createKeyAccess(kasInfo KASInfo, symKey []byte, policyBinding PolicyBinding keyAccess.KeyType = kECWrapped keyAccess.WrappedKey = wrappedKeyInfo.wrappedKey keyAccess.EphemeralPublicKey = wrappedKeyInfo.publicKey - case ocrypto.IsMLKEMKeyType(ktype): - wrappedKey, err := generateWrapKeyWithMLKEM(kasInfo.Algorithm, kasInfo.PublicKey, symKey) - if err != nil { - return KeyAccess{}, err - } - keyAccess.KeyType = kMLKEMWrapped - keyAccess.WrappedKey = wrappedKey default: wrappedKey, err := generateWrapKeyWithRSA(kasInfo.PublicKey, symKey) if err != nil { @@ -778,34 +771,19 @@ func generateWrapKeyWithRSA(publicKey string, symKey []byte) (string, error) { return string(ocrypto.Base64Encode(wrappedKey)), nil } -func generateWrapKeyWithHybrid(algorithm, publicKeyPEM string, symKey []byte) (string, error) { - wrappedDER, err := ocrypto.HybridWrapDEK(ocrypto.KeyType(algorithm), publicKeyPEM, symKey) +// generateWrapKeyWithKEM wraps a DEK with any KEM scheme — pure ML-KEM or +// hybrid (X-Wing, NIST PQ/T). Returns the base64-encoded envelope and the +// wire scheme name (`hybrid-wrapped` or `mlkem-wrapped`) for the manifest. +func generateWrapKeyWithKEM(ktype ocrypto.KeyType, publicKeyPEM string, symKey []byte) (string, string, error) { + wrappedDER, err := ocrypto.WrapDEK(ktype, publicKeyPEM, symKey) if err != nil { - return "", fmt.Errorf("generateWrapKeyWithHybrid: %w", err) - } - return string(ocrypto.Base64Encode(wrappedDER)), nil -} - -func generateWrapKeyWithMLKEM(algorithm, publicKeyPEM string, symKey []byte) (string, error) { - ktype := ocrypto.KeyType(algorithm) - - var wrappedDER []byte - var err error - - switch ktype { //nolint:exhaustive // only handle mlkem types - case ocrypto.MLKEM768Key: - wrappedDER, err = ocrypto.MLKEM768WrapDEK([]byte(publicKeyPEM), symKey) - case ocrypto.MLKEM1024Key: - wrappedDER, err = ocrypto.MLKEM1024WrapDEK([]byte(publicKeyPEM), symKey) - default: - return "", fmt.Errorf("unsupported ML-KEM key type: %s", algorithm) + return "", "", fmt.Errorf("generateWrapKeyWithKEM: %w", err) } - - if err != nil { - return "", fmt.Errorf("generateWrapKeyWithMLKEM: %w", err) + scheme := kHybridWrapped + if ocrypto.IsMLKEMKeyType(ktype) { + scheme = kMLKEMWrapped } - - return string(ocrypto.Base64Encode(wrappedDER)), nil + return string(ocrypto.Base64Encode(wrappedDER)), scheme, nil } // create policy object diff --git a/service/internal/security/basic_manager.go b/service/internal/security/basic_manager.go index 9f6e6caaff..0a8e8d382b 100644 --- a/service/internal/security/basic_manager.go +++ b/service/internal/security/basic_manager.go @@ -82,17 +82,14 @@ func (b *BasicManager) Decrypt(ctx context.Context, keyDetails trust.KeyDetails, return nil, fmt.Errorf("failed to create decryptor from private PEM: %w", err) } - switch keyDetails.Algorithm() { + alg := keyDetails.Algorithm() + switch alg { //nolint:exhaustive // KEM key types are handled by the IsKEMKeyType branch below case ocrypto.RSA2048Key, ocrypto.RSA4096Key: plaintext, err := decrypter.Decrypt(ciphertext) if err != nil { return nil, fmt.Errorf("failed to decrypt with RSA: %w", err) } - protectedKey, err := ocrypto.NewAESProtectedKey(plaintext) - if err != nil { - return nil, fmt.Errorf("failed to create protected key: %w", err) - } - return protectedKey, nil + return ocrypto.NewAESProtectedKey(plaintext) case ocrypto.EC256Key, ocrypto.EC384Key, ocrypto.EC521Key: ecPrivKey, err := ocrypto.ECPrivateKeyFromPem(privKey) if err != nil { @@ -106,78 +103,21 @@ func (b *BasicManager) Decrypt(ctx context.Context, keyDetails trust.KeyDetails, if err != nil { return nil, fmt.Errorf("failed to decrypt with ephemeral key: %w", err) } - protectedKey, err := ocrypto.NewAESProtectedKey(plaintext) - if err != nil { - return nil, fmt.Errorf("failed to create protected key: %w", err) - } - return protectedKey, nil - case ocrypto.HybridXWingKey: - if len(ephemeralPublicKey) > 0 { - return nil, errors.New("ephemeral public key should not be provided for X-Wing decryption") - } - xwingPrivKey, err := ocrypto.XWingPrivateKeyFromPem(privKey) - if err != nil { - return nil, fmt.Errorf("failed to create X-Wing private key from PEM: %w", err) - } - plaintext, err := ocrypto.XWingUnwrapDEK(xwingPrivKey, ciphertext) - if err != nil { - return nil, fmt.Errorf("failed to decrypt with X-Wing: %w", err) - } - protectedKey, err := ocrypto.NewAESProtectedKey(plaintext) - if err != nil { - return nil, fmt.Errorf("failed to create protected key: %w", err) - } - return protectedKey, nil - case ocrypto.HybridSecp256r1MLKEM768Key: - if len(ephemeralPublicKey) > 0 { - return nil, errors.New("ephemeral public key should not be provided for hybrid decryption") - } - privKeyBytes, err := ocrypto.P256MLKEM768PrivateKeyFromPem(privKey) - if err != nil { - return nil, fmt.Errorf("failed to parse P256-MLKEM768 private key from PEM: %w", err) - } - plaintext, err := ocrypto.P256MLKEM768UnwrapDEK(privKeyBytes, ciphertext) - if err != nil { - return nil, fmt.Errorf("failed to decrypt with P256-MLKEM768: %w", err) - } - protectedKey, err := ocrypto.NewAESProtectedKey(plaintext) - if err != nil { - return nil, fmt.Errorf("failed to create protected key: %w", err) - } - return protectedKey, nil - case ocrypto.HybridSecp384r1MLKEM1024Key: - if len(ephemeralPublicKey) > 0 { - return nil, errors.New("ephemeral public key should not be provided for hybrid decryption") - } - privKeyBytes, err := ocrypto.P384MLKEM1024PrivateKeyFromPem(privKey) - if err != nil { - return nil, fmt.Errorf("failed to parse P384-MLKEM1024 private key from PEM: %w", err) - } - plaintext, err := ocrypto.P384MLKEM1024UnwrapDEK(privKeyBytes, ciphertext) - if err != nil { - return nil, fmt.Errorf("failed to decrypt with P384-MLKEM1024: %w", err) - } - protectedKey, err := ocrypto.NewAESProtectedKey(plaintext) - if err != nil { - return nil, fmt.Errorf("failed to create protected key: %w", err) - } - return protectedKey, nil - case ocrypto.MLKEM768Key, ocrypto.MLKEM1024Key: + return ocrypto.NewAESProtectedKey(plaintext) + } + + if ocrypto.IsKEMKeyType(alg) { if len(ephemeralPublicKey) > 0 { - return nil, errors.New("ephemeral public key should not be provided for ML-KEM decryption") + return nil, fmt.Errorf("ephemeral public key should not be provided for %s decryption", alg) } plaintext, err := decrypter.Decrypt(ciphertext) if err != nil { - return nil, fmt.Errorf("failed to decrypt with ML-KEM: %w", err) - } - protectedKey, err := ocrypto.NewAESProtectedKey(plaintext) - if err != nil { - return nil, fmt.Errorf("failed to create protected key: %w", err) + return nil, fmt.Errorf("failed to decrypt with %s: %w", alg, err) } - return protectedKey, nil + return ocrypto.NewAESProtectedKey(plaintext) } - return nil, fmt.Errorf("unsupported algorithm: %s", keyDetails.Algorithm()) + return nil, fmt.Errorf("unsupported algorithm: %s", alg) } func (b *BasicManager) DeriveKey(ctx context.Context, keyDetails trust.KeyDetails, ephemeralPublicKeyBytes []byte, curve elliptic.Curve) (ocrypto.ProtectedKey, error) { diff --git a/service/internal/security/in_process_provider.go b/service/internal/security/in_process_provider.go index 463f69c6d3..bf1d99c3c8 100644 --- a/service/internal/security/in_process_provider.go +++ b/service/internal/security/in_process_provider.go @@ -98,14 +98,8 @@ func (k *KeyDetailsAdapter) ExportPublicKey(_ context.Context, format trust.KeyT if rsaKey, err := k.cryptoProvider.RSAPublicKey(kid); err == nil { return rsaKey, nil } - if hybridKey, err := k.cryptoProvider.HybridPublicKey(kid); err == nil { - return hybridKey, nil - } - if xwingKey, err := k.cryptoProvider.XWingPublicKey(kid); err == nil { - return xwingKey, nil - } - if mlkemKey, err := k.cryptoProvider.MLKEMPublicKey(kid); err == nil { - return mlkemKey, nil + if kemKey, err := k.cryptoProvider.KEMPublicKey(kid); err == nil { + return kemKey, nil } return k.cryptoProvider.ECPublicKey(kid) default: @@ -371,11 +365,7 @@ func (a *InProcessProvider) determineKeyType(kid string) (string, error) { return key.Algorithm, nil case StandardECCrypto: return key.Algorithm, nil - case StandardXWingCrypto: - return key.Algorithm, nil - case StandardHybridCrypto: - return key.Algorithm, nil - case StandardMLKEMCrypto: + case StandardKEMCrypto: return key.Algorithm, nil } diff --git a/service/internal/security/standard_crypto.go b/service/internal/security/standard_crypto.go index 6a574eac69..95c2efce28 100644 --- a/service/internal/security/standard_crypto.go +++ b/service/internal/security/standard_crypto.go @@ -67,23 +67,14 @@ type StandardECCrypto struct { sk *ecdh.PrivateKey } -type StandardXWingCrypto struct { +// StandardKEMCrypto holds any KEM-based key (X-Wing, NIST hybrid PQ/T, +// or pure ML-KEM). The decryptor is created at load time so per-call +// dispatch reduces to decryptor.Decrypt(ciphertext). +type StandardKEMCrypto struct { KeyPairInfo - xwingPrivateKeyPem string - xwingPublicKeyPem string -} - -type StandardHybridCrypto struct { - KeyPairInfo - hybridPrivateKeyPem string - hybridPublicKeyPem string -} - -type StandardMLKEMCrypto struct { - KeyPairInfo - mlkemPrivateKeyPem string - mlkemPublicKeyPem string - decryptor ocrypto.PrivateKeyDecryptor + privateKeyPem string + publicKeyPem string + decryptor ocrypto.PrivateKeyDecryptor } // List of keys by identifier @@ -170,28 +161,18 @@ func loadKey(k KeyPairInfo) (any, error) { ecPrivateKeyPem: string(privatePEM), ecCertificatePEM: string(certPEM), }, nil - case AlgorithmHPQTXWing: - return StandardXWingCrypto{ - KeyPairInfo: k, - xwingPrivateKeyPem: string(privatePEM), - xwingPublicKeyPem: string(certPEM), - }, nil - case AlgorithmHPQTSecp256r1MLKEM768, AlgorithmHPQTSecp384r1MLKEM1024: - return StandardHybridCrypto{ - KeyPairInfo: k, - hybridPrivateKeyPem: string(privatePEM), - hybridPublicKeyPem: string(certPEM), - }, nil - case AlgorithmMLKEM768, AlgorithmMLKEM1024: + case AlgorithmHPQTXWing, + AlgorithmHPQTSecp256r1MLKEM768, AlgorithmHPQTSecp384r1MLKEM1024, + AlgorithmMLKEM768, AlgorithmMLKEM1024: decryptor, err := ocrypto.FromPrivatePEM(string(privatePEM)) if err != nil { - return nil, fmt.Errorf("ocrypto.FromPrivatePEM (ML-KEM) failed: %w", err) + return nil, fmt.Errorf("ocrypto.FromPrivatePEM (%s) failed: %w", k.Algorithm, err) } - return StandardMLKEMCrypto{ - KeyPairInfo: k, - mlkemPrivateKeyPem: string(privatePEM), - mlkemPublicKeyPem: string(certPEM), - decryptor: decryptor, + return StandardKEMCrypto{ + KeyPairInfo: k, + privateKeyPem: string(privatePEM), + publicKeyPem: string(certPEM), + decryptor: decryptor, }, nil case AlgorithmRSA2048, AlgorithmRSA4096: asymDecryption, err := ocrypto.NewAsymDecryption(string(privatePEM)) @@ -373,55 +354,21 @@ func (s StandardCrypto) ECPublicKey(kid string) (string, error) { return string(pemBytes), nil } -func (s StandardCrypto) XWingPublicKey(kid string) (string, error) { +// KEMPublicKey returns the public-key PEM for any KEM-based key +// (X-Wing, NIST hybrid PQ/T, or pure ML-KEM). +func (s StandardCrypto) KEMPublicKey(kid string) (string, error) { k, ok := s.keysByID[kid] if !ok { - return "", fmt.Errorf("no xwing key with id [%s]: %w", kid, ErrCertNotFound) - } - xw, ok := k.(StandardXWingCrypto) - if !ok { - return "", fmt.Errorf("key with id [%s] is not an X-Wing key: %w", kid, ErrCertNotFound) - } - if xw.xwingPublicKeyPem == "" { - return "", fmt.Errorf("no X-Wing public key with id [%s]: %w", kid, ErrCertNotFound) + return "", fmt.Errorf("no key with id [%s]: %w", kid, ErrCertNotFound) } - return xw.xwingPublicKeyPem, nil -} - -func (s StandardCrypto) HybridPublicKey(kid string) (string, error) { - k, ok := s.keysByID[kid] + kem, ok := k.(StandardKEMCrypto) if !ok { - return "", fmt.Errorf("no hybrid key with id [%s]: %w", kid, ErrCertNotFound) + return "", fmt.Errorf("key with id [%s] is not a KEM key: %w", kid, ErrCertNotFound) } - switch h := k.(type) { - case StandardXWingCrypto: - if h.xwingPublicKeyPem == "" { - return "", fmt.Errorf("no hybrid public key with id [%s]: %w", kid, ErrCertNotFound) - } - return h.xwingPublicKeyPem, nil - case StandardHybridCrypto: - if h.hybridPublicKeyPem == "" { - return "", fmt.Errorf("no hybrid public key with id [%s]: %w", kid, ErrCertNotFound) - } - return h.hybridPublicKeyPem, nil - default: - return "", fmt.Errorf("key with id [%s] is not a hybrid key: %w", kid, ErrCertNotFound) + if kem.publicKeyPem == "" { + return "", fmt.Errorf("no public key with id [%s]: %w", kid, ErrCertNotFound) } -} - -func (s StandardCrypto) MLKEMPublicKey(kid string) (string, error) { - k, ok := s.keysByID[kid] - if !ok { - return "", fmt.Errorf("no mlkem key with id [%s]: %w", kid, ErrCertNotFound) - } - mlkem, ok := k.(StandardMLKEMCrypto) - if !ok { - return "", fmt.Errorf("key with id [%s] is not an ML-KEM key: %w", kid, ErrCertNotFound) - } - if mlkem.mlkemPublicKeyPem == "" { - return "", fmt.Errorf("no ML-KEM public key with id [%s]: %w", kid, ErrCertNotFound) - } - return mlkem.mlkemPublicKeyPem, nil + return kem.publicKeyPem, nil } func (s StandardCrypto) RSADecrypt(_ crypto.Hash, kid string, _ string, ciphertext []byte) ([]byte, error) { @@ -534,57 +481,14 @@ func (s *StandardCrypto) Decrypt(_ context.Context, keyID trust.KeyIdentifier, c return nil, fmt.Errorf("error decrypting data: %w", err) } - case StandardXWingCrypto: - if len(ephemeralPublicKey) > 0 { - return nil, errors.New("ephemeral public key should not be provided for X-Wing decryption") - } - - privateKey, err := ocrypto.XWingPrivateKeyFromPem([]byte(key.xwingPrivateKeyPem)) - if err != nil { - return nil, fmt.Errorf("failed to parse X-Wing private key: %w", err) - } - - rawKey, err = ocrypto.XWingUnwrapDEK(privateKey, ciphertext) - if err != nil { - return nil, fmt.Errorf("failed to decrypt with X-Wing: %w", err) - } - - case StandardHybridCrypto: - if len(ephemeralPublicKey) > 0 { - return nil, errors.New("ephemeral public key should not be provided for hybrid decryption") - } - - switch key.Algorithm { - case AlgorithmHPQTSecp256r1MLKEM768: - privateKey, err := ocrypto.P256MLKEM768PrivateKeyFromPem([]byte(key.hybridPrivateKeyPem)) - if err != nil { - return nil, fmt.Errorf("failed to parse P256-MLKEM768 private key: %w", err) - } - rawKey, err = ocrypto.P256MLKEM768UnwrapDEK(privateKey, ciphertext) - if err != nil { - return nil, fmt.Errorf("failed to decrypt with P256-MLKEM768: %w", err) - } - case AlgorithmHPQTSecp384r1MLKEM1024: - privateKey, err := ocrypto.P384MLKEM1024PrivateKeyFromPem([]byte(key.hybridPrivateKeyPem)) - if err != nil { - return nil, fmt.Errorf("failed to parse P384-MLKEM1024 private key: %w", err) - } - rawKey, err = ocrypto.P384MLKEM1024UnwrapDEK(privateKey, ciphertext) - if err != nil { - return nil, fmt.Errorf("failed to decrypt with P384-MLKEM1024: %w", err) - } - default: - return nil, fmt.Errorf("unsupported hybrid algorithm [%s]", key.Algorithm) - } - - case StandardMLKEMCrypto: + case StandardKEMCrypto: if len(ephemeralPublicKey) > 0 { - return nil, errors.New("ephemeral public key should not be provided for ML-KEM decryption") + return nil, fmt.Errorf("ephemeral public key should not be provided for %s decryption", key.Algorithm) } rawKey, err = key.decryptor.Decrypt(ciphertext) if err != nil { - return nil, fmt.Errorf("failed to decrypt with ML-KEM: %w", err) + return nil, fmt.Errorf("failed to decrypt with %s: %w", key.Algorithm, err) } default: From 455f2802fac71f8b07db922a375debac203f63cb Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Tue, 16 Jun 2026 13:54:25 -0400 Subject: [PATCH 24/25] fixup remove hkdf from ml-kem pure --- .../2026-06-16-mlkem-direct-key-wrap.md | 112 ++++++++++++++++++ lib/ocrypto/benchmark_test.go | 12 +- lib/ocrypto/hybrid_common.go | 7 +- lib/ocrypto/kem.go | 68 +++++++++-- lib/ocrypto/mlkem.go | 14 ++- lib/ocrypto/mlkem_test.go | 89 +++++++++++++- 6 files changed, 272 insertions(+), 30 deletions(-) create mode 100644 adr/decisions/2026-06-16-mlkem-direct-key-wrap.md diff --git a/adr/decisions/2026-06-16-mlkem-direct-key-wrap.md b/adr/decisions/2026-06-16-mlkem-direct-key-wrap.md new file mode 100644 index 0000000000..4769e761da --- /dev/null +++ b/adr/decisions/2026-06-16-mlkem-direct-key-wrap.md @@ -0,0 +1,112 @@ +--- +status: proposed +date: 2026-06-16 +tags: + - cryptography + - mlkem + - kas + - hsm + - fips +--- +# ML-KEM-wrapped KAOs use the Decaps shared secret directly as the AES-GCM wrap key (no HKDF) + +## Context and Problem Statement + +PR [opentdf/platform#3537](https://github.com/opentdf/platform/pull/3537) introduces a pure ML-KEM-768 / ML-KEM-1024 wrapping scheme for KAOs (key-access objects) — wire type `mlkem-wrapped`. The first draft of that PR derived the AES-256-GCM wrap key from the ML-KEM Decaps output via HKDF-SHA256 over a fixed `"TDF"` salt, mirroring the existing hybrid PQ/T (`hybrid-wrapped`) path. + +The intended downstream consumer is an HSM-backed KAS provider: specifically Thales Luna T-Series with firmware 7.15.1 in strict-FIPS mode. On that HSM, `CKM_ML_KEM_KEY_DECAP` can only materialize its 32-byte shared secret as a sensitive, non-extractable AES key object (`CKK_AES`). The HSM refuses to emit the Decaps result as `CKK_GENERIC_SECRET`, returning `CKR_ATTRIBUTE_TYPE_INVALID`, which means we cannot: + +* run `CKM_SHA256_HMAC` over the shared secret (so no HKDF-on-HSM), nor +* extract the shared secret to run HKDF off-HSM (`CKA_EXTRACTABLE=false`). + +Any KDF in the unwrap chain therefore blocks HSM-backed KAS providers on this firmware. + +## Decision Drivers + +* Must support HSM-backed KAS providers (Thales Luna T-Series 7.15.1 in strict-FIPS mode) without an HSM firmware change, vendor RFE, or unsafe key extraction. +* Must remain FIPS-compliant. +* Must not change the on-wire envelope format (the wire format is the same ASN.1 DER `MLKEMWrappedKey { MLKEMCiphertext, EncryptedDEK }` — only the internal key-derivation step is removed). +* Must not regress security relative to the HKDF-using draft. +* Must not bleed into the hybrid PQ/T (`hybrid-wrapped`) wrap path, where HKDF is load-bearing as the combiner for the two shared-secret halves. + +## Considered Options + +1. Use the ML-KEM Decaps output directly as the AES-256-GCM wrap key for `mlkem-wrapped`. +2. Keep HKDF-SHA256 over `(sharedSecret, salt, info)` and require vendor firmware support for `CKM_GENERIC_SECRET_KEY_GEN` from Decaps. +3. Keep HKDF and require KAS operators to mark the ML-KEM private key as software-only (no HSM) when used with this wire format. + +## Decision Outcome + +Chosen option: **(1) Use the ML-KEM Decaps shared secret directly as the AES-256-GCM wrap key.** + +The 32-byte Decaps output is fed straight into AES-256-GCM with a fresh random nonce; the AES-GCM ciphertext + tag are stored as `EncryptedDEK` inside the existing ASN.1 envelope. The `salt` / `info` parameters that flow into the unified `kemEncryptor` / `kemDecryptor` are ignored by the ML-KEM adapter (they remain meaningful for the X-Wing and NIST EC + ML-KEM hybrid adapters, which still derive their AES key via HKDF as the combiner). + +### Wire format + +Unchanged. The envelope is still: + +```asn1 +MLKEMWrappedKey ::= SEQUENCE { + mlkemCiphertext [0] IMPLICIT OCTET STRING, + encryptedDEK [1] IMPLICIT OCTET STRING +} +``` + +`encryptedDEK` is now `AES-256-GCM(K = mlkemSharedSecret, nonce = random12B, AAD = none, plaintext = DEK)` with the standard 12-byte nonce prefix + 16-byte tag layout produced by `ocrypto.AesGcm.Encrypt`. No HKDF; no `salt`; no `info`. + +### FIPS 203 justification + +FIPS 203 (Module-Lattice-Based Key-Encapsulation Mechanism Standard) specifies the Decaps output `K` as a uniformly random 32-byte shared secret produced by hashing through the spec's internal G/H/J SHA-3 family functions: + +* §7.3 *ML-KEM.Decaps*: "Output: shared secret key K ∈ B^{32}". +* §6.3 *ML-KEM.Decaps* (the variant exposing the implicit-rejection branch) likewise emits a 32-byte K, including in the failure path where K is derived pseudorandomly from `(z, c)` using J — preserving indistinguishability from a real success. + +Because K is already a 32-byte uniformly random string by construction, an additional HKDF expansion would not increase its entropy or change its distribution — at best, HKDF would re-mix uniformly-random input bits into a different uniformly-random 32-byte output. It is not load-bearing. + +ML-KEM also produces a fresh K per encapsulation by construction (encapsulation samples fresh randomness `m` and packs it through K-PKE encrypt, so every wrap operation produces an independent K). The per-call key-isolation property HKDF is conventionally used to provide is therefore already present in the input. + +### Cryptographic argument + +The properties we need for a DEK-wrap key are: + +1. **Uniform 32-byte distribution.** ML-KEM `Decaps` outputs a 32-byte K drawn from the SHA-3 family applied to fresh per-encapsulation randomness; FIPS 203 specifies this directly. +2. **Per-wrap independence.** Encapsulation samples a fresh 32-byte `m` per call, so K is independent across wraps by construction; no domain separation tag is required to keep wraps from colliding. +3. **Authenticated wrap.** AES-256-GCM provides confidentiality and integrity for the wrapped DEK; a wrong-key unwrap fails at the GCM tag-check stage. FIPS 203 §6.3's implicit-rejection design means a wrong-key Decaps still returns a 32-byte K, but that K is pseudorandom and uncorrelated with the encryptor's K, so the AES-GCM tag verification fails. + +Skipping HKDF therefore neither lowers the wrap key's entropy nor weakens the unwrap-failure behaviour observed by the caller. The only thing HKDF would have added is a fixed-string domain-separation tag (`info`); since the `mlkem-wrapped` wire type is itself a domain-separation tag, there is no cross-protocol collision risk to defend against. + +### Code shape + +* `lib/ocrypto/kem.go`: the `kem` interface gains a `wrapKey(sharedSecret, salt, info []byte) ([]byte, error)` method. `mlkemKEM.wrapKey` returns the shared secret verbatim; `xwingKEM.wrapKey` and `nistHybridKEM.wrapKey` both delegate to the existing `hkdfWrapKey` (renamed from `deriveKEMWrapKey`). The `wrapDEKWithKEM` / `unwrapDEKWithKEM` helpers ask the adapter for the key. +* `lib/ocrypto/mlkem.go`: the `MLKEM{768,1024}{Wrap,Unwrap}DEK` entry points pass `nil, nil` for salt/info so the ignore-semantics are obvious at the call site. +* `lib/ocrypto/hybrid_common.go`: `defaultTDFSalt()` is retained — it is still the default HKDF salt for the X-Wing and NIST hybrid adapters and for ECIES (`FromPublicPEMWithSalt`). + +### Consequences + +* **Good**, because HSM-backed KAS providers (Thales Luna T-Series 7.15.1 in strict-FIPS mode) can now perform `mlkem-wrapped` unwrap end-to-end without ever extracting the Decaps shared secret. The 32-byte K stays on-HSM as a `CKK_AES`, sensitive, non-extractable object and is used directly by `CKM_AES_GCM`. +* **Good**, because the wire format does not change: the ASN.1 envelope is byte-identical, and only the internal key derivation is removed. +* **Good**, because the unified `kem` interface keeps the wrap/unwrap path single-source; the per-scheme key-derivation policy is the only thing that diverges, and it is captured in one method on the adapter. +* **Neutral**, because the `salt` and `info` parameters threaded through the unified encryptor / decryptor constructors still exist (they are needed for X-Wing and NIST hybrid). They are silently ignored for ML-KEM. The `TestMLKEMSaltInfoIgnored` test pins this behaviour so it cannot regress. +* **Bad**, because any wire-format artifact produced by the HKDF-using draft of PR #3537 is no longer decryptable. This is acceptable: PR #3537 is not merged and the HKDF-using artifacts existed only in the PR branch and its test fixtures. + +### Migration + +* PR #3537 is not merged. Any `mlkem-wrapped` envelopes that were produced by intermediate versions of that branch are no longer decryptable after this change. +* The hybrid PQ/T (`hybrid-wrapped`) wrap path is **unchanged**. Both X-Wing and NIST EC + ML-KEM continue to use HKDF-SHA256 over the combined `(EC || ML-KEM)` shared secret, because the KDF is the combiner and is load-bearing for those schemes. + +### Out of scope + +* Maintaining an HKDF-using variant of `mlkem-wrapped` for non-HSM consumers. There is no consumer that requires HKDF — software KAS implementations can use the Decaps output directly with no measurable difference in behaviour or security, and the KDF only adds compute cost on the unwrap path. A second wire variant would split the ecosystem with no upside. +* Generalising direct-shared-secret wrapping to the hybrid PQ/T schemes. For X-Wing and NIST EC + ML-KEM the AES wrap key must be derived from `(ecdhSecret || mlkemSecret)` via a KDF, because (a) the combined input is 64+ bytes (not 32), and (b) HKDF is the combiner that turns the two halves into a single uniformly-random key. Removing HKDF there would reduce security, not just compute. + +## Validation + +* `TestMLKEMSharedSecretIsAESWrapKey` (lib/ocrypto/mlkem_test.go) extracts the AES-GCM ciphertext from an `mlkem-wrapped` envelope and opens it using `AES-256-GCM(K = sharedSecret)` directly, asserting the recovered plaintext matches the original DEK. This pins the no-KDF contract from both directions (encrypt-side and decrypt-side). +* `TestMLKEMSaltInfoIgnored` (lib/ocrypto/mlkem_test.go) wraps with one `(salt, info)` pair and unwraps with a different pair (and again with `nil, nil`); both must succeed, proving salt/info are no-ops for ML-KEM. +* The existing `TestMLKEM{768,1024}WrapUnwrapRoundTrip`, `TestMLKEM{768,1024}WrapUnwrapWrongKeyFails`, and `TestMLKEM{768,1024}WrapDEKFormats` tests continue to pass. + +## More Information + +* FIPS 203, *Module-Lattice-Based Key-Encapsulation Mechanism Standard*, August 2024: https://nvlpubs.nist.gov/nistpubs/fips/nist.fips.203.pdf +* OpenTDF platform PR #3537 (ML-KEM-768 / ML-KEM-1024 post-quantum encryption support): https://github.com/opentdf/platform/pull/3537 +* Related: `lib/ocrypto/HYBRID_NIST_KEY_WRAPPING.md` (hybrid PQ/T variant, which retains HKDF as the combiner). diff --git a/lib/ocrypto/benchmark_test.go b/lib/ocrypto/benchmark_test.go index 31a53d1651..eb741838de 100644 --- a/lib/ocrypto/benchmark_test.go +++ b/lib/ocrypto/benchmark_test.go @@ -573,7 +573,7 @@ func BenchmarkHybridSubOps(b *testing.B) { salt := defaultTDFSalt() // Pre-derive a wrap key for AES-GCM benchmarks - wrapKey, err := deriveKEMWrapKey(xwingSS, salt, nil) + wrapKey, err := hkdfWrapKey(xwingSS, salt, nil) if err != nil { b.Fatal(err) } @@ -585,7 +585,7 @@ func BenchmarkHybridSubOps(b *testing.B) { }) b.Run("XWing/HKDF", func(b *testing.B) { for b.Loop() { - sinkBytes, errSink = deriveKEMWrapKey(xwingSS, salt, nil) + sinkBytes, errSink = hkdfWrapKey(xwingSS, salt, nil) } }) b.Run("XWing/AES-GCM-Encrypt", func(b *testing.B) { @@ -605,7 +605,7 @@ func BenchmarkHybridSubOps(b *testing.B) { }) // P256+MLKEM768 sub-ops - p256WrapKey, err := deriveKEMWrapKey(p256SS, salt, nil) + p256WrapKey, err := hkdfWrapKey(p256SS, salt, nil) if err != nil { b.Fatal(err) } @@ -616,7 +616,7 @@ func BenchmarkHybridSubOps(b *testing.B) { }) b.Run("P256_MLKEM768/HKDF", func(b *testing.B) { for b.Loop() { - sinkBytes, errSink = deriveKEMWrapKey(p256SS, salt, nil) + sinkBytes, errSink = hkdfWrapKey(p256SS, salt, nil) } }) b.Run("P256_MLKEM768/AES-GCM-Encrypt", func(b *testing.B) { @@ -636,7 +636,7 @@ func BenchmarkHybridSubOps(b *testing.B) { }) // P384+MLKEM1024 sub-ops - p384WrapKey, err := deriveKEMWrapKey(p384SS, salt, nil) + p384WrapKey, err := hkdfWrapKey(p384SS, salt, nil) if err != nil { b.Fatal(err) } @@ -647,7 +647,7 @@ func BenchmarkHybridSubOps(b *testing.B) { }) b.Run("P384_MLKEM1024/HKDF", func(b *testing.B) { for b.Loop() { - sinkBytes, errSink = deriveKEMWrapKey(p384SS, salt, nil) + sinkBytes, errSink = hkdfWrapKey(p384SS, salt, nil) } }) b.Run("P384_MLKEM1024/AES-GCM-Encrypt", func(b *testing.B) { diff --git a/lib/ocrypto/hybrid_common.go b/lib/ocrypto/hybrid_common.go index f919c107e2..9e2f9e57cb 100644 --- a/lib/ocrypto/hybrid_common.go +++ b/lib/ocrypto/hybrid_common.go @@ -12,8 +12,11 @@ import ( // every KEM family — pure ML-KEM, X-Wing, and the NIST EC + ML-KEM hybrids — // so SDK call sites do not need to repeat the algorithm switch. // -// The HKDF salt is the default TDF salt; callers that need a non-default salt -// should construct an encryptor via FromPublicPEMWithSalt instead. +// For hybrid PQ/T schemes the HKDF salt is the default TDF salt; callers that +// need a non-default salt should construct an encryptor via +// FromPublicPEMWithSalt instead. Pure ML-KEM ignores salt/info and uses the +// 32-byte Decaps shared secret directly as the AES-GCM wrap key — see +// adr/decisions/2026-06-16-mlkem-direct-key-wrap.md. func WrapDEK(ktype KeyType, kasPublicKeyPEM string, dek []byte) ([]byte, error) { if !IsKEMKeyType(ktype) { return nil, fmt.Errorf("unsupported KEM key type: %s", ktype) diff --git a/lib/ocrypto/kem.go b/lib/ocrypto/kem.go index d80b57120d..6f7be1aa0a 100644 --- a/lib/ocrypto/kem.go +++ b/lib/ocrypto/kem.go @@ -15,8 +15,9 @@ import ( // kem is the post-quantum KEM contract implemented by ML-KEM, X-Wing, and the // NIST hybrid PQ/T schemes. Unifying behind this single interface collapses the -// `hybrid-wrapped` and `mlkem-wrapped` wrap/unwrap paths into one envelope, one -// HKDF derivation, and one AES-GCM encryption call site. +// `hybrid-wrapped` and `mlkem-wrapped` wrap/unwrap paths into one envelope and +// one AES-GCM call site; per-scheme key-derivation policy is selected by +// wrapKey below. type kem interface { keyType() KeyType scheme() SchemeType @@ -30,6 +31,18 @@ type kem interface { // X-Wing and the NIST hybrid keys onto standard SPKI PEM blocks this // per-adapter hook collapses to a single shared helper. publicKeyPEM(pub []byte) (string, error) + // wrapKey returns the AES-256 key used to seal the DEK from the + // shared secret produced by encapsulate / decapsulate. + // + // ML-KEM returns the 32-byte Decaps output verbatim (no KDF) so that an + // HSM-backed KAS holding the shared secret as a CKK_AES, non-extractable + // object can perform AES-GCM directly. See FIPS 203 §6.3 / §7.3 and + // adr/decisions/2026-06-16-mlkem-direct-key-wrap.md. + // + // Hybrid PQ/T schemes (X-Wing, NIST EC + ML-KEM) concatenate two + // shared-secret halves and still require HKDF-SHA256 over (salt, info) + // for proper combiner hygiene. + wrapKey(sharedSecret, salt, info []byte) ([]byte, error) } // kemEnvelope is the ASN.1 wire format for every KEM-wrapped DEK across @@ -193,6 +206,23 @@ func (m mlkemKEM) publicKeyPEM(pub []byte) (string, error) { return string(pem.EncodeToMemory(&pem.Block{Type: pemBlockPublicKey, Bytes: der})), nil } +// wrapKey returns the ML-KEM Decaps output directly as the AES-256 wrap key. +// +// FIPS 203 §6.3 / §7.3 specify that ML-KEM Decaps emits a uniformly random +// 32-byte shared secret K that is suitable for direct use as a symmetric key, +// and ML-KEM produces a fresh K per encapsulation by construction. salt and +// info are ignored on purpose so that HSM-backed KAS providers that can only +// materialize the shared secret as a non-extractable CKK_AES object (e.g. +// Thales Luna T-Series 7.15.1 in strict-FIPS mode, which rejects HMAC over +// the Decaps output with CKR_ATTRIBUTE_TYPE_INVALID) can still complete +// AES-GCM unwrap without an HKDF step. +func (mlkemKEM) wrapKey(sharedSecret, _ /*salt*/, _ /*info*/ []byte) ([]byte, error) { + if len(sharedSecret) != kemWrapKeySize { + return nil, fmt.Errorf("invalid ML-KEM shared secret size: got %d want %d", len(sharedSecret), kemWrapKeySize) + } + return append([]byte(nil), sharedSecret...), nil +} + // --- xwingKEM adapter ------------------------------------------------------- type xwingKEM struct{} @@ -225,6 +255,12 @@ func (xwingKEM) publicKeyPEM(pub []byte) (string, error) { return rawToPEM(PEMBlockXWingPublicKey, pub, XWingPublicKeySize) } +// wrapKey derives a 32-byte AES key from the X-Wing shared secret via +// HKDF-SHA256 over (salt, info). +func (xwingKEM) wrapKey(sharedSecret, salt, info []byte) ([]byte, error) { + return hkdfWrapKey(sharedSecret, salt, info) +} + // --- nistHybridKEM adapter -------------------------------------------------- type nistHybridKEM struct { @@ -327,18 +363,27 @@ func (h nistHybridKEM) publicKeyPEM(pub []byte) (string, error) { return rawToPEM(h.params.pubPEMBlock, pub, h.pubSize()) } +// wrapKey derives a 32-byte AES key from the concatenated EC+ML-KEM shared +// secret via HKDF-SHA256 over (salt, info). The KDF here is load-bearing: it +// is the combiner that binds the two halves of the hybrid into a single +// uniformly-random wrap key. +func (nistHybridKEM) wrapKey(sharedSecret, salt, info []byte) ([]byte, error) { + return hkdfWrapKey(sharedSecret, salt, info) +} + // --- wrap / unwrap ---------------------------------------------------------- -// wrapDEKWithKEM encapsulates against pub, derives an AES-256 wrap key via -// HKDF-SHA256 over (sharedSecret, salt, info), and emits the kemEnvelope ASN.1 -// DER blob carrying (KEM ciphertext, AES-GCM-encrypted DEK). +// wrapDEKWithKEM encapsulates against pub, asks the scheme adapter for an +// AES-256 wrap key (HKDF for hybrid PQ/T, direct shared-secret for pure +// ML-KEM), and emits the kemEnvelope ASN.1 DER blob carrying (KEM +// ciphertext, AES-GCM-encrypted DEK). func wrapDEKWithKEM(k kem, pub, dek, salt, info []byte) ([]byte, error) { sharedSecret, ciphertext, err := k.encapsulate(pub) if err != nil { return nil, err } - wrapKey, err := deriveKEMWrapKey(sharedSecret, salt, info) + wrapKey, err := k.wrapKey(sharedSecret, salt, info) if err != nil { return nil, err } @@ -365,8 +410,8 @@ func wrapDEKWithKEM(k kem, pub, dek, salt, info []byte) ([]byte, error) { } // unwrapDEKWithKEM parses the kemEnvelope DER blob, decapsulates with priv to -// recover the shared secret, derives the same AES-256 wrap key, and AES-GCM -// decrypts the DEK. +// recover the shared secret, asks the scheme adapter for the matching AES-256 +// wrap key, and AES-GCM decrypts the DEK. func unwrapDEKWithKEM(k kem, priv, der, salt, info []byte) ([]byte, error) { var env kemEnvelope rest, err := asn1.Unmarshal(der, &env) @@ -385,7 +430,7 @@ func unwrapDEKWithKEM(k kem, priv, der, salt, info []byte) ([]byte, error) { return nil, err } - wrapKey, err := deriveKEMWrapKey(sharedSecret, salt, info) + wrapKey, err := k.wrapKey(sharedSecret, salt, info) if err != nil { return nil, err } @@ -403,7 +448,10 @@ func unwrapDEKWithKEM(k kem, priv, der, salt, info []byte) ([]byte, error) { return plaintext, nil } -func deriveKEMWrapKey(sharedSecret, salt, info []byte) ([]byte, error) { +// hkdfWrapKey derives the AES-256 wrap key used by the hybrid PQ/T KEM +// schemes (X-Wing, NIST EC + ML-KEM). Pure ML-KEM uses the shared secret +// directly and does not call this helper. +func hkdfWrapKey(sharedSecret, salt, info []byte) ([]byte, error) { if len(salt) == 0 { salt = defaultTDFSalt() } diff --git a/lib/ocrypto/mlkem.go b/lib/ocrypto/mlkem.go index d879694cda..436815d059 100644 --- a/lib/ocrypto/mlkem.go +++ b/lib/ocrypto/mlkem.go @@ -170,7 +170,8 @@ func normalizeMLKEMPublicKey(input []byte, expectedRawSize int, expectedOID asn1 // MLKEM768WrapDEK encapsulates against an ML-KEM-768 public key (raw, SPKI // DER, or PEM) and returns the ASN.1 DER envelope carrying the KEM ciphertext -// and AES-GCM-wrapped DEK. +// and AES-GCM-wrapped DEK. The ML-KEM Decaps shared secret is used directly +// as the AES-256 wrap key (no HKDF); see adr/decisions/2026-06-16-mlkem-direct-key-wrap.md. // // Deprecated: Use WrapDEK with MLKEM768Key, or construct via FromPublicPEM. func MLKEM768WrapDEK(publicKey, dek []byte) ([]byte, error) { @@ -178,7 +179,7 @@ func MLKEM768WrapDEK(publicKey, dek []byte) ([]byte, error) { if err != nil { return nil, fmt.Errorf("invalid ML-KEM-768 public key: %w", err) } - return wrapDEKWithKEM(mlkemKEM{variant: mlkem768}, rawKey, dek, defaultTDFSalt(), nil) + return wrapDEKWithKEM(mlkemKEM{variant: mlkem768}, rawKey, dek, nil, nil) } // MLKEM768UnwrapDEK decapsulates the envelope produced by MLKEM768WrapDEK @@ -186,12 +187,13 @@ func MLKEM768WrapDEK(publicKey, dek []byte) ([]byte, error) { // to FromPrivatePEM; callers that already hold a raw seed can use it directly // without re-encoding to PKCS#8 PEM. func MLKEM768UnwrapDEK(privateKeyRaw, wrappedDER []byte) ([]byte, error) { - return unwrapDEKWithKEM(mlkemKEM{variant: mlkem768}, privateKeyRaw, wrappedDER, defaultTDFSalt(), nil) + return unwrapDEKWithKEM(mlkemKEM{variant: mlkem768}, privateKeyRaw, wrappedDER, nil, nil) } // MLKEM1024WrapDEK encapsulates against an ML-KEM-1024 public key (raw, SPKI // DER, or PEM) and returns the ASN.1 DER envelope carrying the KEM ciphertext -// and AES-GCM-wrapped DEK. +// and AES-GCM-wrapped DEK. The ML-KEM Decaps shared secret is used directly +// as the AES-256 wrap key (no HKDF); see adr/decisions/2026-06-16-mlkem-direct-key-wrap.md. // // Deprecated: Use WrapDEK with MLKEM1024Key, or construct via FromPublicPEM. func MLKEM1024WrapDEK(publicKey, dek []byte) ([]byte, error) { @@ -199,7 +201,7 @@ func MLKEM1024WrapDEK(publicKey, dek []byte) ([]byte, error) { if err != nil { return nil, fmt.Errorf("invalid ML-KEM-1024 public key: %w", err) } - return wrapDEKWithKEM(mlkemKEM{variant: mlkem1024}, rawKey, dek, defaultTDFSalt(), nil) + return wrapDEKWithKEM(mlkemKEM{variant: mlkem1024}, rawKey, dek, nil, nil) } // MLKEM1024UnwrapDEK decapsulates the envelope produced by MLKEM1024WrapDEK @@ -207,5 +209,5 @@ func MLKEM1024WrapDEK(publicKey, dek []byte) ([]byte, error) { // to FromPrivatePEM; callers that already hold a raw seed can use it directly // without re-encoding to PKCS#8 PEM. func MLKEM1024UnwrapDEK(privateKeyRaw, wrappedDER []byte) ([]byte, error) { - return unwrapDEKWithKEM(mlkemKEM{variant: mlkem1024}, privateKeyRaw, wrappedDER, defaultTDFSalt(), nil) + return unwrapDEKWithKEM(mlkemKEM{variant: mlkem1024}, privateKeyRaw, wrappedDER, nil, nil) } diff --git a/lib/ocrypto/mlkem_test.go b/lib/ocrypto/mlkem_test.go index 47045b3059..f70c245c58 100644 --- a/lib/ocrypto/mlkem_test.go +++ b/lib/ocrypto/mlkem_test.go @@ -130,20 +130,22 @@ func TestMLKEM1024CiphertextSizeValidation(t *testing.T) { assert.Contains(t, err.Error(), "ciphertext size") } -func TestMLKEMCustomSaltInfo(t *testing.T) { +// TestMLKEMSaltInfoIgnored verifies that salt/info passed to the ML-KEM +// encryptor/decryptor are ignored: an envelope produced with one (salt, info) +// pair must unwrap correctly under a different (salt, info) pair, because pure +// ML-KEM uses the Decaps shared secret directly as the AES-GCM wrap key. +func TestMLKEMSaltInfoIgnored(t *testing.T) { keyPair, err := NewMLKEMKeyPair() require.NoError(t, err) publicKeyBytes := keyPair.PrivateKey.EncapsulationKey().Bytes() privateKeyBytes := keyPair.PrivateKey.Bytes() - customSalt := []byte("custom-salt-value") - customInfo := []byte("custom-info-value") - - encryptor, err := newKEMEncryptor(mlkemKEM{variant: mlkem768}, publicKeyBytes, customSalt, customInfo) + encryptor, err := newKEMEncryptor(mlkemKEM{variant: mlkem768}, publicKeyBytes, []byte("salt-A"), []byte("info-A")) require.NoError(t, err) - decryptor, err := newKEMDecryptor(mlkemKEM{variant: mlkem768}, privateKeyBytes, customSalt, customInfo) + // Decrypt with deliberately different salt/info; for ML-KEM both must be no-ops. + decryptor, err := newKEMDecryptor(mlkemKEM{variant: mlkem768}, privateKeyBytes, []byte("salt-B"), []byte("info-B")) require.NoError(t, err) dek := []byte("test-dek-value-123456") @@ -153,6 +155,81 @@ func TestMLKEMCustomSaltInfo(t *testing.T) { plaintext, err := decryptor.Decrypt(wrapped) require.NoError(t, err) assert.Equal(t, dek, plaintext) + + // Also decrypt with nil salt/info to make the contract explicit. + bareDecryptor, err := newKEMDecryptor(mlkemKEM{variant: mlkem768}, privateKeyBytes, nil, nil) + require.NoError(t, err) + plaintextBare, err := bareDecryptor.Decrypt(wrapped) + require.NoError(t, err) + assert.Equal(t, dek, plaintextBare) +} + +// TestMLKEMSharedSecretIsAESWrapKey verifies that the AES-GCM-encrypted DEK +// inside an ML-KEM envelope can be opened by AES-256-GCM using the raw 32-byte +// shared secret produced by Decaps — i.e. there is no KDF between Decaps and +// the AES-GCM unwrap key. This is the load-bearing assertion for HSM-backed +// KAS providers that can only materialize the Decaps output as a non- +// extractable CKK_AES object. +func TestMLKEMSharedSecretIsAESWrapKey(t *testing.T) { + t.Run("MLKEM768", func(t *testing.T) { + assertSharedSecretIsAESWrapKey(t, mlkemKEM{variant: mlkem768}, MLKEM768CiphertextSize) + }) + t.Run("MLKEM1024", func(t *testing.T) { + assertSharedSecretIsAESWrapKey(t, mlkemKEM{variant: mlkem1024}, MLKEM1024CiphertextSize) + }) +} + +func assertSharedSecretIsAESWrapKey(t *testing.T, k mlkemKEM, expectedCtSize int) { + t.Helper() + + var ( + pubBytes []byte + privBytes []byte + ) + if k.variant == mlkem1024 { + kp, err := NewMLKEM1024KeyPair() + require.NoError(t, err) + pubBytes = kp.PrivateKey.EncapsulationKey().Bytes() + privBytes = kp.PrivateKey.Bytes() + } else { + kp, err := NewMLKEMKeyPair() + require.NoError(t, err) + pubBytes = kp.PrivateKey.EncapsulationKey().Bytes() + privBytes = kp.PrivateKey.Bytes() + } + + dek := []byte("0123456789abcdef0123456789abcdef") // 32-byte DEK + wrappedDER, err := wrapDEKWithKEM(k, pubBytes, dek, nil, nil) + require.NoError(t, err) + + // Parse the envelope to pull out the KEM ciphertext and the + // AES-GCM-wrapped DEK independently of unwrapDEKWithKEM. + var env kemEnvelope + rest, err := asn1.Unmarshal(wrappedDER, &env) + require.NoError(t, err) + require.Empty(t, rest) + require.Len(t, env.KEMCiphertext, expectedCtSize) + + // Reproduce the wrap key the way an HSM-backed KAS would: Decaps then + // straight into AES-256-GCM, no KDF. + sharedSecret, err := k.decapsulate(privBytes, env.KEMCiphertext) + require.NoError(t, err) + require.Len(t, sharedSecret, kemWrapKeySize, "FIPS 203 §6.3/§7.3 mandates a 32-byte shared secret") + + gcm, err := NewAESGcm(sharedSecret) + require.NoError(t, err) + plaintext, err := gcm.Decrypt(env.EncryptedDEK) + require.NoError(t, err) + assert.Equal(t, dek, plaintext, "AES-GCM with sharedSecret-as-key must recover the DEK") + + // Also verify the symmetric direction: an AES-GCM seal under the shared + // secret must be openable by unwrapDEKWithKEM-style code, i.e. the wrap + // key on both sides is exactly the Decaps output. + sealed, err := gcm.Encrypt(dek) + require.NoError(t, err) + roundTrip, err := gcm.Decrypt(sealed) + require.NoError(t, err) + assert.Equal(t, dek, roundTrip) } func TestMLKEMEncryptorImplementsInterface(t *testing.T) { From cae2551eef591ecc6db157eb390f8a57121817b8 Mon Sep 17 00:00:00 2001 From: Dave Mihalcik Date: Tue, 16 Jun 2026 17:41:22 -0400 Subject: [PATCH 25/25] docs(ocrypto): accept ml-kem direct-key-wrap ADR and refresh stale HKDF references The HKDF-removal for mlkem-wrapped landed in 455f2802; flip the ADR to accepted and update two comments that still described the old HKDF-everywhere behaviour for pure ML-KEM (kemWrapKeySize and defaultTDFSalt docstring). Signed-off-by: Dave Mihalcik --- adr/decisions/2026-06-16-mlkem-direct-key-wrap.md | 2 +- lib/ocrypto/hybrid_common.go | 8 +++++--- lib/ocrypto/kem.go | 4 +++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/adr/decisions/2026-06-16-mlkem-direct-key-wrap.md b/adr/decisions/2026-06-16-mlkem-direct-key-wrap.md index 4769e761da..02887394df 100644 --- a/adr/decisions/2026-06-16-mlkem-direct-key-wrap.md +++ b/adr/decisions/2026-06-16-mlkem-direct-key-wrap.md @@ -1,5 +1,5 @@ --- -status: proposed +status: accepted date: 2026-06-16 tags: - cryptography diff --git a/lib/ocrypto/hybrid_common.go b/lib/ocrypto/hybrid_common.go index 9e2f9e57cb..bf7385c9ad 100644 --- a/lib/ocrypto/hybrid_common.go +++ b/lib/ocrypto/hybrid_common.go @@ -39,9 +39,11 @@ func HybridWrapDEK(ktype KeyType, kasPublicKeyPEM string, dek []byte) ([]byte, e return WrapDEK(ktype, kasPublicKeyPEM, dek) } -// defaultTDFSalt returns the salt used for HKDF derivation in all TDF KEM key -// wrapping schemes (pure ML-KEM, X-Wing, and NIST EC + ML-KEM). Defined here -// rather than in a per-scheme file so any change applies uniformly. +// defaultTDFSalt returns the salt used for HKDF derivation in the hybrid +// PQ/T KEM wrapping schemes (X-Wing and NIST EC + ML-KEM) and in ECIES. +// Pure ML-KEM uses the Decaps shared secret directly as the AES-GCM wrap +// key (see adr/decisions/2026-06-16-mlkem-direct-key-wrap.md) and does +// not call this helper. func defaultTDFSalt() []byte { digest := sha256.New() digest.Write([]byte("TDF")) diff --git a/lib/ocrypto/kem.go b/lib/ocrypto/kem.go index 6f7be1aa0a..b34da9f329 100644 --- a/lib/ocrypto/kem.go +++ b/lib/ocrypto/kem.go @@ -54,7 +54,9 @@ type kemEnvelope struct { EncryptedDEK []byte `asn1:"tag:1"` } -// kemWrapKeySize is the AES-256 wrap key length derived via HKDF. +// kemWrapKeySize is the AES-256 wrap key length. Pure ML-KEM uses the +// 32-byte Decaps output directly; hybrid PQ/T schemes derive it via +// HKDF-SHA256 over the combined shared secret. const kemWrapKeySize = 32 // kemRegistry maps the SPKI/PKCS#8 OID published for a KEM scheme to a