From 2a794b4b4ee6e8c7f9828349bdb45c8cf31d8c43 Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Wed, 5 Feb 2025 13:52:05 -0500 Subject: [PATCH 01/18] feat(core): Lets KAS use EC for TDF wraps - Lets KAS use an elliptic key based mechanism for key (split) encapsulation --- docs/grpc/index.html | 7 + go.work.sum | 3 + lib/ocrypto/asym_decryption.go | 120 +++++++- lib/ocrypto/asym_encrypt_decrypt_test.go | 4 +- lib/ocrypto/asym_encryption.go | 182 ++++++++++-- protocol/go/kas/kas.pb.go | 294 ++++++++++--------- sdk/manifest.go | 19 +- sdk/schema/manifest-lax.schema.json | 4 + sdk/schema/manifest.schema.json | 8 +- sdk/tdf.go | 2 +- service/internal/security/crypto_provider.go | 1 + service/internal/security/standard_crypto.go | 31 ++ service/kas/access/rewrap.go | 120 +++++--- service/kas/kas.proto | 3 + 14 files changed, 574 insertions(+), 224 deletions(-) diff --git a/docs/grpc/index.html b/docs/grpc/index.html index 309ca58dd..ee9a3a4a2 100644 --- a/docs/grpc/index.html +++ b/docs/grpc/index.html @@ -3557,6 +3557,13 @@

KeyAccess

header is only used for NanoTDFs

+ + ephemeral_public_key + bytes + +

For wrapping with an ECDH derived key, when type=ec-wrapped

+ + diff --git a/go.work.sum b/go.work.sum index 53524d537..2adf47a11 100644 --- a/go.work.sum +++ b/go.work.sum @@ -2043,6 +2043,7 @@ golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -2192,6 +2193,7 @@ golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808 h1:+Kc94D8UVEVxJnLXp/+FMfqQARZtWHfVrcRtcG8aT3g= @@ -2225,6 +2227,7 @@ golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/lib/ocrypto/asym_decryption.go b/lib/ocrypto/asym_decryption.go index 1cf2eba9d..83a41dcf0 100644 --- a/lib/ocrypto/asym_decryption.go +++ b/lib/ocrypto/asym_decryption.go @@ -2,20 +2,34 @@ package ocrypto import ( "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/ecdh" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rsa" + "crypto/sha256" "crypto/x509" "encoding/pem" "errors" "fmt" + "io" "strings" + + "golang.org/x/crypto/hkdf" ) type AsymDecryption struct { PrivateKey *rsa.PrivateKey } -// NewAsymDecryption creates and returns a new AsymDecryption. -func NewAsymDecryption(privateKeyInPem string) (AsymDecryption, error) { +type PrivateKeyDecryptor interface { + // Decrypt decrypts ciphertext with private key. + Decrypt(data []byte) ([]byte, error) +} + +// FromPrivatePEM creates and returns a new AsymDecryption. +func FromPrivatePEM(privateKeyInPem string) (PrivateKeyDecryptor, error) { block, _ := pem.Decode([]byte(privateKeyInPem)) if block == nil { return AsymDecryption{}, errors.New("failed to parse PEM formatted private key") @@ -40,13 +54,34 @@ func NewAsymDecryption(privateKeyInPem string) (AsymDecryption, error) { } switch privateKey := priv.(type) { + case *ecdsa.PrivateKey: + if sk, err := privateKey.ECDH(); err != nil { + return nil, fmt.Errorf("unable to create ECDH key: %w", err) + } else { + return NewECDecryptor(sk) + } + case *ecdh.PrivateKey: + return NewECDecryptor(privateKey) case *rsa.PrivateKey: return AsymDecryption{privateKey}, nil default: break } - return AsymDecryption{}, errors.New("not an rsa PEM formatted private key") + return nil, errors.New("not a supported PEM formatted private key") +} + +func NewAsymDecryption(privateKeyInPem string) (AsymDecryption, error) { + d, err := FromPrivatePEM(privateKeyInPem) + if err != nil { + return AsymDecryption{}, err + } + switch d := d.(type) { + case AsymDecryption: + return d, nil + default: + return AsymDecryption{}, errors.New("not an RSA private key") + } } // Decrypt decrypts ciphertext with private key. @@ -64,3 +99,82 @@ func (asymDecryption AsymDecryption) Decrypt(data []byte) ([]byte, error) { return bytes, nil } + +type ECDecryptor struct { + sk *ecdh.PrivateKey + salt []byte + info []byte +} + +func NewECDecryptor(sk *ecdh.PrivateKey) (ECDecryptor, error) { + // TK Make these reasonable? IIRC salt should be longer, info maybe a parameters? + salt := []byte("salt") + info := []byte("info") + return ECDecryptor{sk, salt, info}, nil +} + +func (e ECDecryptor) Decrypt(_ []byte) ([]byte, error) { + // TK How to get the ephmeral key into here? + return nil, errors.New("ecdh standard decrypt unimplemented") +} + +func (e ECDecryptor) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) { + ekDSA, err := UncompressECPubKey(convCurve(e.sk.Curve()), ephemeral) + if err != nil { + return nil, err + } + + ek, err := ekDSA.ECDH() + if err != nil { + return nil, fmt.Errorf("ecdh failure: %w", err) + } + + ikm, err := e.sk.ECDH(ek) + if err != nil { + return nil, fmt.Errorf("ecdh failure: %w", err) + } + + hkdfObj := hkdf.New(sha256.New, ikm, e.salt, e.info) + + derivedKey := make([]byte, len(ikm)) + if _, err := io.ReadFull(hkdfObj, derivedKey); err != nil { + return nil, fmt.Errorf("hkdf failure: %w", err) + } + + // Encrypt data with derived key using aes-gcm + block, err := aes.NewCipher(derivedKey) + 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 convCurve(c ecdh.Curve) elliptic.Curve { + switch c { + case ecdh.P256(): + return elliptic.P256() + case ecdh.P384(): + return elliptic.P384() + case ecdh.P521(): + return elliptic.P521() + default: + return nil + } +} diff --git a/lib/ocrypto/asym_encrypt_decrypt_test.go b/lib/ocrypto/asym_encrypt_decrypt_test.go index a40e93834..534145104 100644 --- a/lib/ocrypto/asym_encrypt_decrypt_test.go +++ b/lib/ocrypto/asym_encrypt_decrypt_test.go @@ -218,7 +218,7 @@ wVyElqp317Ksz+GtTIc+DE6oryxK3tZd4hrj9fXT4KiJvQ4pcRjpePgH7B8= } for _, test := range rsaKeys { - asymEncryptor, err := NewAsymEncryption(test.publicKey) + asymEncryptor, err := FromPublicPEM(test.publicKey) if err != nil { t.Fatalf("NewAsymEncryption - failed: %v", err) } @@ -229,7 +229,7 @@ wVyElqp317Ksz+GtTIc+DE6oryxK3tZd4hrj9fXT4KiJvQ4pcRjpePgH7B8= t.Fatalf("AsymEncryption encrypt failed: %v", err) } - asymDecryptor, err := NewAsymDecryption(test.privateKey) + asymDecryptor, err := FromPrivatePEM(test.privateKey) if err != nil { t.Fatalf("NewAsymDecryption - failed: %v", err) } diff --git a/lib/ocrypto/asym_encryption.go b/lib/ocrypto/asym_encryption.go index 65ef5a115..39c2c1765 100644 --- a/lib/ocrypto/asym_encryption.go +++ b/lib/ocrypto/asym_encryption.go @@ -1,63 +1,159 @@ package ocrypto import ( + "crypto/aes" + "crypto/cipher" + "crypto/ecdh" "crypto/rand" "crypto/rsa" "crypto/sha1" //nolint:gosec // used for padding which is safe + "crypto/sha256" "crypto/x509" "encoding/pem" "errors" "fmt" + "io" "strings" + + "golang.org/x/crypto/hkdf" ) +type SchemeType string + +const ( + RSA SchemeType = "wrapped" + EC SchemeType = "ec-wrapped" +) + +type PublicKeyEncryptor interface { + // Encrypt encrypts data with public key. + Encrypt(data []byte) ([]byte, error) + + // PublicKeyInPemFormat Returns public key in pem format, or the empty string if not present + PublicKeyInPemFormat() (string, error) + + // Type required to use the scheme for encryption - notably, if it procduces extra metadata. + Type() SchemeType + + // For EC schemes, this method returns the public part of the ephemeral key. + // Otherwise, it returns nil. + EphemeralKey() []byte + + // Any extra metadata, e.g. the ephemeral public key for EC scheme keys. + Metadata() (map[string]string, error) +} + type AsymEncryption struct { PublicKey *rsa.PublicKey } +type ECEncryptor struct { + pub *ecdh.PublicKey + ek *ecdh.PrivateKey + salt []byte + info []byte +} + +func FromPublicPEM(publicKeyInPem string) (PublicKeyEncryptor, error) { + pub, err := getPublicPart(publicKeyInPem) + if err != nil { + return nil, err + } + + switch pub := pub.(type) { + case *rsa.PublicKey: + return &AsymEncryption{pub}, nil + case *ecdh.PublicKey: + return newECIES(pub) + default: + break + } + + return nil, errors.New("not an supported type of public key") +} + +func newECIES(pub *ecdh.PublicKey) (ECEncryptor, error) { + ek, err := pub.Curve().GenerateKey(rand.Reader) + // TK Make these reasonable? IIRC salt should be longer, info maybe a parameters? + salt := []byte("salt") + info := []byte("info") + return ECEncryptor{pub, ek, salt, info}, err +} + // NewAsymEncryption creates and returns a new AsymEncryption. +// Deprecated: Use FromPublicPEM instead. func NewAsymEncryption(publicKeyInPem string) (AsymEncryption, error) { + pub, err := getPublicPart(publicKeyInPem) + if err != nil { + return AsymEncryption{}, err + } + + switch pub := pub.(type) { + case *rsa.PublicKey: + return AsymEncryption{pub}, nil + default: + break + } + + return AsymEncryption{}, errors.New("not an supported type of public key") +} + +func getPublicPart(publicKeyInPem string) (any, error) { block, _ := pem.Decode([]byte(publicKeyInPem)) if block == nil { - return AsymEncryption{}, errors.New("failed to parse PEM formatted public key") + return nil, errors.New("failed to parse PEM formatted public key") } var pub any if strings.Contains(publicKeyInPem, "BEGIN CERTIFICATE") { cert, err := x509.ParseCertificate(block.Bytes) if err != nil { - return AsymEncryption{}, fmt.Errorf("x509.ParseCertificate failed: %w", err) + return nil, fmt.Errorf("x509.ParseCertificate failed: %w", err) } - var ok bool - if pub, ok = cert.PublicKey.(*rsa.PublicKey); !ok { - return AsymEncryption{}, errors.New("failed to parse PEM formatted public key") - } + pub = cert.PublicKey } else { var err error pub, err = x509.ParsePKIXPublicKey(block.Bytes) if err != nil { - return AsymEncryption{}, fmt.Errorf("x509.ParsePKIXPublicKey failed: %w", err) + return nil, fmt.Errorf("x509.ParsePKIXPublicKey failed: %w", err) } } + return pub, nil +} - switch pub := pub.(type) { - case *rsa.PublicKey: - return AsymEncryption{pub}, nil - default: - break - } +func (e AsymEncryption) Type() SchemeType { + return RSA +} + +func (e ECEncryptor) Type() SchemeType { + return EC +} - return AsymEncryption{}, errors.New("not an rsa PEM formatted public key") +func (e AsymEncryption) EphemeralKey() []byte { + return nil } -// Encrypt encrypts data with public key. -func (asymEncryption AsymEncryption) Encrypt(data []byte) ([]byte, error) { - if asymEncryption.PublicKey == nil { +func (e ECEncryptor) EphemeralKey() []byte { + return e.ek.PublicKey().Bytes() +} + +func (e AsymEncryption) Metadata() (map[string]string, error) { + return make(map[string]string), nil +} + +func (e ECEncryptor) Metadata() (map[string]string, error) { + m := make(map[string]string) + m["ephemeralPublicKey"] = string(e.ek.PublicKey().Bytes()) + return m, nil +} + +func (e AsymEncryption) Encrypt(data []byte) ([]byte, error) { + if e.PublicKey == nil { return nil, errors.New("failed to encrypt, public key is empty") } - bytes, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, asymEncryption.PublicKey, data, nil) //nolint:gosec // used for padding which is safe + bytes, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, e.PublicKey, data, nil) //nolint:gosec // used for padding which is safe if err != nil { return nil, fmt.Errorf("rsa.EncryptOAEP failed: %w", err) } @@ -65,13 +161,12 @@ func (asymEncryption AsymEncryption) Encrypt(data []byte) ([]byte, error) { return bytes, nil } -// PublicKeyInPemFormat Returns public key in pem format. -func (asymEncryption AsymEncryption) PublicKeyInPemFormat() (string, error) { - if asymEncryption.PublicKey == nil { +func publicKeyInPemFormat(pk any) (string, error) { + if pk == nil { return "", errors.New("failed to generate PEM formatted public key") } - publicKeyBytes, err := x509.MarshalPKIXPublicKey(asymEncryption.PublicKey) + publicKeyBytes, err := x509.MarshalPKIXPublicKey(pk) if err != nil { return "", fmt.Errorf("x509.MarshalPKIXPublicKey failed: %w", err) } @@ -85,3 +180,46 @@ func (asymEncryption AsymEncryption) PublicKeyInPemFormat() (string, error) { return string(publicKeyPem), nil } + +func (e AsymEncryption) PublicKeyInPemFormat() (string, error) { + return publicKeyInPemFormat(e.PublicKey) +} + +// Encrypts the data with the EC public key. +func (e ECEncryptor) Encrypt(data []byte) ([]byte, error) { + ikm, err := e.ek.ECDH(e.pub) + if err != nil { + return nil, fmt.Errorf("ecdh failure: %w", err) + } + + hkdfObj := hkdf.New(sha256.New, ikm, e.salt, e.info) + + derivedKey := make([]byte, len(ikm)) + if _, err := io.ReadFull(hkdfObj, derivedKey); err != nil { + return nil, fmt.Errorf("hkdf failure: %w", err) + } + + // Encrypt data with derived key using aes-gcm + block, err := aes.NewCipher(derivedKey) + 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) + } + + ciphertext := gcm.Seal(nonce, nonce, data, nil) + return ciphertext, nil +} + +// PublicKeyInPemFormat Returns public key in pem format. +func (e ECEncryptor) PublicKeyInPemFormat() (string, error) { + return publicKeyInPemFormat(e.pub) +} diff --git a/protocol/go/kas/kas.pb.go b/protocol/go/kas/kas.pb.go index 7451da9c9..a78c98a81 100644 --- a/protocol/go/kas/kas.pb.go +++ b/protocol/go/kas/kas.pb.go @@ -227,6 +227,8 @@ type KeyAccess struct { WrappedKey []byte `protobuf:"bytes,8,opt,name=wrapped_key,json=wrappedKey,proto3" json:"wrapped_key,omitempty"` // header is only used for NanoTDFs Header []byte `protobuf:"bytes,9,opt,name=header,proto3" json:"header,omitempty"` + // For wrapping with an ECDH derived key, when type=ec-wrapped + EphemeralPublicKey []byte `protobuf:"bytes,10,opt,name=ephemeral_public_key,json=ephemeralPublicKey,proto3" json:"ephemeral_public_key,omitempty"` } func (x *KeyAccess) Reset() { @@ -324,6 +326,13 @@ func (x *KeyAccess) GetHeader() []byte { return nil } +func (x *KeyAccess) GetEphemeralPublicKey() []byte { + if x != nil { + return x.EphemeralPublicKey + } + return nil +} + type UnsignedRewrapRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -986,7 +995,7 @@ var file_kas_kas_proto_rawDesc = []byte{ 0x6d, 0x22, 0x3b, 0x0a, 0x0d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x16, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x61, 0x6c, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, - 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0xa1, + 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0xd3, 0x02, 0x0a, 0x09, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x2d, 0x0a, 0x12, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, @@ -1005,147 +1014,150 @@ var file_kas_kas_proto_rawDesc = []byte{ 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, - 0x65, 0x72, 0x22, 0x95, 0x04, 0x0a, 0x15, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x52, - 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x11, - 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x50, - 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x48, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x6b, 0x61, 0x73, - 0x2e, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x57, 0x69, 0x74, 0x68, 0x50, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x73, 0x1a, 0x30, 0x0a, 0x0a, 0x57, 0x69, 0x74, 0x68, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, - 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x62, 0x6f, 0x64, 0x79, 0x1a, 0x82, 0x01, 0x0a, 0x13, 0x57, 0x69, 0x74, 0x68, 0x4b, 0x65, 0x79, - 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x2f, 0x0a, 0x14, - 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6f, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x6b, 0x65, 0x79, 0x41, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x3a, 0x0a, - 0x11, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6f, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x4b, - 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x0f, 0x6b, 0x65, 0x79, 0x41, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x1a, 0xce, 0x01, 0x0a, 0x11, 0x57, 0x69, - 0x74, 0x68, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x5c, 0x0a, 0x12, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6f, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x6b, 0x61, - 0x73, 0x2e, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x57, 0x69, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x41, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x10, 0x6b, 0x65, 0x79, - 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x3d, 0x0a, - 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, + 0x65, 0x72, 0x12, 0x30, 0x0a, 0x14, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x5f, + 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x12, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x50, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x4b, 0x65, 0x79, 0x22, 0x95, 0x04, 0x0a, 0x15, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, + 0x64, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, + 0x0a, 0x11, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x48, 0x0a, 0x08, 0x72, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x6b, + 0x61, 0x73, 0x2e, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x52, 0x65, 0x77, 0x72, 0x61, + 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x57, 0x69, 0x74, 0x68, 0x50, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x73, 0x1a, 0x30, 0x0a, 0x0a, 0x57, 0x69, 0x74, 0x68, 0x50, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, + 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x1a, 0x82, 0x01, 0x0a, 0x13, 0x57, 0x69, 0x74, 0x68, 0x4b, + 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x2f, + 0x0a, 0x14, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6f, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x6b, 0x65, + 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, + 0x3a, 0x0a, 0x11, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6b, 0x61, 0x73, + 0x2e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x0f, 0x6b, 0x65, 0x79, 0x41, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x1a, 0xce, 0x01, 0x0a, 0x11, + 0x57, 0x69, 0x74, 0x68, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x5c, 0x0a, 0x12, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, + 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x52, 0x65, 0x77, 0x72, - 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x57, 0x69, 0x74, 0x68, 0x50, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x52, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x1c, 0x0a, 0x09, - 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x22, 0xb1, 0x01, 0x0a, 0x10, 0x50, - 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x51, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x42, 0x33, 0x92, 0x41, 0x30, 0x32, 0x2e, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, - 0x68, 0x6d, 0x20, 0x74, 0x79, 0x70, 0x65, 0x20, 0x72, 0x73, 0x61, 0x3a, 0x3c, 0x6b, 0x65, 0x79, - 0x73, 0x69, 0x7a, 0x65, 0x3e, 0x20, 0x6f, 0x72, 0x20, 0x65, 0x63, 0x3a, 0x3c, 0x63, 0x75, 0x72, - 0x76, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x3e, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, - 0x68, 0x6d, 0x12, 0x26, 0x0a, 0x03, 0x66, 0x6d, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, - 0x14, 0x92, 0x41, 0x11, 0x32, 0x0f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x20, 0x66, - 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x03, 0x66, 0x6d, 0x74, 0x12, 0x22, 0x0a, 0x01, 0x76, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x14, 0x92, 0x41, 0x11, 0x32, 0x0f, 0x72, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x01, 0x76, 0x22, 0x44, - 0x0a, 0x11, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, - 0x65, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x69, 0x64, 0x22, 0x4f, 0x0a, 0x0d, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x30, 0x0a, 0x14, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, - 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x12, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x52, 0x06, 0x62, - 0x65, 0x61, 0x72, 0x65, 0x72, 0x22, 0xc7, 0x02, 0x0a, 0x15, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, - 0x44, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x28, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x2e, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2f, 0x0a, 0x14, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x5f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x11, 0x6b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4f, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x28, - 0x0a, 0x0f, 0x6b, 0x61, 0x73, 0x5f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, - 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x0d, 0x6b, 0x61, 0x73, 0x57, 0x72, - 0x61, 0x70, 0x70, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x1a, 0x53, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, - 0x67, 0x0a, 0x12, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, - 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x49, 0x64, 0x12, 0x34, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, - 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x22, 0xea, 0x02, 0x0a, 0x0e, 0x52, 0x65, 0x77, - 0x72, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x08, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, - 0x6b, 0x61, 0x73, 0x2e, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x42, 0x02, 0x18, 0x01, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x30, - 0x0a, 0x12, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, - 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x02, 0x18, 0x01, 0x52, 0x10, - 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x4b, 0x65, 0x79, - 0x12, 0x2c, 0x0a, 0x12, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x75, 0x62, 0x6c, - 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x73, 0x65, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x29, - 0x0a, 0x0e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0d, 0x73, 0x63, 0x68, 0x65, - 0x6d, 0x61, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x35, 0x0a, 0x09, 0x72, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6b, - 0x61, 0x73, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, - 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x09, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, - 0x1a, 0x53, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x32, 0xce, 0x02, 0x0a, 0x0d, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x69, 0x0a, 0x09, 0x50, 0x75, 0x62, 0x6c, 0x69, - 0x63, 0x4b, 0x65, 0x79, 0x12, 0x15, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, - 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x6b, 0x61, - 0x73, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x2d, 0x92, 0x41, 0x09, 0x4a, 0x07, 0x0a, 0x03, 0x32, 0x30, 0x30, 0x12, - 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x12, 0x16, 0x2f, 0x6b, 0x61, 0x73, 0x2f, 0x76, 0x32, - 0x2f, 0x6b, 0x61, 0x73, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x90, - 0x02, 0x01, 0x12, 0x78, 0x0a, 0x0f, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x50, 0x75, 0x62, 0x6c, - 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1b, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x4c, 0x65, 0x67, 0x61, - 0x63, 0x79, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x22, 0x2a, 0x92, 0x41, 0x09, 0x4a, 0x07, 0x0a, 0x03, 0x32, 0x30, 0x30, 0x12, 0x00, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x15, 0x12, 0x13, 0x2f, 0x6b, 0x61, 0x73, 0x2f, 0x6b, 0x61, 0x73, 0x5f, 0x70, - 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x90, 0x02, 0x01, 0x12, 0x58, 0x0a, 0x06, - 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x12, 0x12, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x52, 0x65, 0x77, - 0x72, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x6b, 0x61, 0x73, - 0x2e, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x25, 0x92, 0x41, 0x09, 0x4a, 0x07, 0x0a, 0x03, 0x32, 0x30, 0x30, 0x12, 0x00, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x6b, 0x61, 0x73, 0x2f, 0x76, 0x32, 0x2f, - 0x72, 0x65, 0x77, 0x72, 0x61, 0x70, 0x42, 0xe2, 0x01, 0x92, 0x41, 0x73, 0x12, 0x71, 0x0a, 0x1a, - 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x44, 0x46, 0x20, 0x4b, 0x65, 0x79, 0x20, 0x41, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x20, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2a, 0x4c, 0x0a, 0x12, 0x42, 0x53, - 0x44, 0x20, 0x33, 0x2d, 0x43, 0x6c, 0x61, 0x75, 0x73, 0x65, 0x20, 0x43, 0x6c, 0x65, 0x61, 0x72, - 0x12, 0x36, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x62, 0x61, 0x63, - 0x6b, 0x65, 0x6e, 0x64, 0x2f, 0x62, 0x6c, 0x6f, 0x62, 0x2f, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, - 0x2f, 0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, 0x32, 0x05, 0x31, 0x2e, 0x35, 0x2e, 0x30, 0x0a, - 0x07, 0x63, 0x6f, 0x6d, 0x2e, 0x6b, 0x61, 0x73, 0x42, 0x08, 0x4b, 0x61, 0x73, 0x50, 0x72, 0x6f, - 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2b, 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, 0x6b, 0x61, - 0x73, 0xa2, 0x02, 0x03, 0x4b, 0x58, 0x58, 0xaa, 0x02, 0x03, 0x4b, 0x61, 0x73, 0xca, 0x02, 0x03, - 0x4b, 0x61, 0x73, 0xe2, 0x02, 0x0f, 0x4b, 0x61, 0x73, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x03, 0x4b, 0x61, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x57, 0x69, 0x74, 0x68, 0x4b, 0x65, + 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x10, 0x6b, + 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, + 0x3d, 0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x25, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x55, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x52, 0x65, + 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x57, 0x69, 0x74, 0x68, + 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x1c, + 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x22, 0xb1, 0x01, 0x0a, + 0x10, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x51, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x33, 0x92, 0x41, 0x30, 0x32, 0x2e, 0x61, 0x6c, 0x67, 0x6f, 0x72, + 0x69, 0x74, 0x68, 0x6d, 0x20, 0x74, 0x79, 0x70, 0x65, 0x20, 0x72, 0x73, 0x61, 0x3a, 0x3c, 0x6b, + 0x65, 0x79, 0x73, 0x69, 0x7a, 0x65, 0x3e, 0x20, 0x6f, 0x72, 0x20, 0x65, 0x63, 0x3a, 0x3c, 0x63, + 0x75, 0x72, 0x76, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x3e, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, + 0x69, 0x74, 0x68, 0x6d, 0x12, 0x26, 0x0a, 0x03, 0x66, 0x6d, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x14, 0x92, 0x41, 0x11, 0x32, 0x0f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x03, 0x66, 0x6d, 0x74, 0x12, 0x22, 0x0a, 0x01, + 0x76, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x14, 0x92, 0x41, 0x11, 0x32, 0x0f, 0x72, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x01, 0x76, + 0x22, 0x44, 0x0a, 0x11, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x4b, 0x65, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x69, 0x64, 0x22, 0x4f, 0x0a, 0x0d, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x30, 0x0a, 0x14, 0x73, 0x69, 0x67, 0x6e, 0x65, + 0x64, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x52, + 0x06, 0x62, 0x65, 0x61, 0x72, 0x65, 0x72, 0x22, 0xc7, 0x02, 0x0a, 0x15, 0x4b, 0x65, 0x79, 0x41, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x12, 0x44, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x4b, 0x65, 0x79, 0x41, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x2e, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2f, 0x0a, 0x14, 0x6b, 0x65, 0x79, 0x5f, 0x61, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x6b, 0x65, 0x79, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x61, 0x73, 0x5f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, + 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x0d, 0x6b, 0x61, 0x73, + 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x05, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x1a, 0x53, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x22, 0x67, 0x0a, 0x12, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x77, 0x72, 0x61, + 0x70, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x4b, 0x65, 0x79, 0x41, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x52, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x22, 0xea, 0x02, 0x0a, 0x0e, 0x52, + 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, + 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x21, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x42, 0x02, 0x18, 0x01, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x12, 0x30, 0x0a, 0x12, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x77, 0x72, 0x61, 0x70, 0x70, + 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x02, 0x18, 0x01, + 0x52, 0x10, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x4b, + 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x12, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, + 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, + 0x12, 0x29, 0x0a, 0x0e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0d, 0x73, 0x63, + 0x68, 0x65, 0x6d, 0x61, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x35, 0x0a, 0x09, 0x72, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, + 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x77, 0x72, 0x61, + 0x70, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x09, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x73, 0x1a, 0x53, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x32, 0xce, 0x02, 0x0a, 0x0d, 0x41, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x69, 0x0a, 0x09, 0x50, 0x75, 0x62, + 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x15, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x50, 0x75, 0x62, + 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, + 0x6b, 0x61, 0x73, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2d, 0x92, 0x41, 0x09, 0x4a, 0x07, 0x0a, 0x03, 0x32, 0x30, + 0x30, 0x12, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x12, 0x16, 0x2f, 0x6b, 0x61, 0x73, 0x2f, + 0x76, 0x32, 0x2f, 0x6b, 0x61, 0x73, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, + 0x79, 0x90, 0x02, 0x01, 0x12, 0x78, 0x0a, 0x0f, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x50, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1b, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x4c, 0x65, + 0x67, 0x61, 0x63, 0x79, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x22, 0x2a, 0x92, 0x41, 0x09, 0x4a, 0x07, 0x0a, 0x03, 0x32, 0x30, 0x30, 0x12, 0x00, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x12, 0x13, 0x2f, 0x6b, 0x61, 0x73, 0x2f, 0x6b, 0x61, 0x73, + 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x90, 0x02, 0x01, 0x12, 0x58, + 0x0a, 0x06, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x12, 0x12, 0x2e, 0x6b, 0x61, 0x73, 0x2e, 0x52, + 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x6b, + 0x61, 0x73, 0x2e, 0x52, 0x65, 0x77, 0x72, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x25, 0x92, 0x41, 0x09, 0x4a, 0x07, 0x0a, 0x03, 0x32, 0x30, 0x30, 0x12, 0x00, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x6b, 0x61, 0x73, 0x2f, 0x76, + 0x32, 0x2f, 0x72, 0x65, 0x77, 0x72, 0x61, 0x70, 0x42, 0xe2, 0x01, 0x92, 0x41, 0x73, 0x12, 0x71, + 0x0a, 0x1a, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x44, 0x46, 0x20, 0x4b, 0x65, 0x79, 0x20, 0x41, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x20, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2a, 0x4c, 0x0a, 0x12, + 0x42, 0x53, 0x44, 0x20, 0x33, 0x2d, 0x43, 0x6c, 0x61, 0x75, 0x73, 0x65, 0x20, 0x43, 0x6c, 0x65, + 0x61, 0x72, 0x12, 0x36, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x64, 0x66, 0x2f, 0x62, + 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2f, 0x62, 0x6c, 0x6f, 0x62, 0x2f, 0x6d, 0x61, 0x73, 0x74, + 0x65, 0x72, 0x2f, 0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, 0x32, 0x05, 0x31, 0x2e, 0x35, 0x2e, + 0x30, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x2e, 0x6b, 0x61, 0x73, 0x42, 0x08, 0x4b, 0x61, 0x73, 0x50, + 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2b, 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, + 0x6b, 0x61, 0x73, 0xa2, 0x02, 0x03, 0x4b, 0x58, 0x58, 0xaa, 0x02, 0x03, 0x4b, 0x61, 0x73, 0xca, + 0x02, 0x03, 0x4b, 0x61, 0x73, 0xe2, 0x02, 0x0f, 0x4b, 0x61, 0x73, 0x5c, 0x47, 0x50, 0x42, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x03, 0x4b, 0x61, 0x73, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/sdk/manifest.go b/sdk/manifest.go index 9316dbf6b..fc3034fdd 100644 --- a/sdk/manifest.go +++ b/sdk/manifest.go @@ -20,15 +20,16 @@ type IntegrityInformation struct { } type KeyAccess struct { - KeyType string `json:"type"` - KasURL string `json:"url"` - Protocol string `json:"protocol"` - WrappedKey string `json:"wrappedKey"` - PolicyBinding interface{} `json:"policyBinding"` - EncryptedMetadata string `json:"encryptedMetadata,omitempty"` - KID string `json:"kid,omitempty"` - SplitID string `json:"sid,omitempty"` - SchemaVersion string `json:"schemaVersion,omitempty"` + KeyType string `json:"type"` + KasURL string `json:"url"` + Protocol string `json:"protocol"` + WrappedKey string `json:"wrappedKey"` + PolicyBinding interface{} `json:"policyBinding"` + EncryptedMetadata string `json:"encryptedMetadata,omitempty"` + KID string `json:"kid,omitempty"` + SplitID string `json:"sid,omitempty"` + SchemaVersion string `json:"schemaVersion,omitempty"` + EphemeralPublicKey string `json:"ephemeralPublicKey,omitempty"` } type PolicyBinding struct { diff --git a/sdk/schema/manifest-lax.schema.json b/sdk/schema/manifest-lax.schema.json index 532c95099..a31abd75f 100644 --- a/sdk/schema/manifest-lax.schema.json +++ b/sdk/schema/manifest-lax.schema.json @@ -99,6 +99,10 @@ "encryptedMetadata": { "description": "Metadata associated with the TDF, and the request. The contents of the metadata are freeform, and are used to pass information from the client, and any plugins that may be in use by the KAS. The metadata stored here should not be used for primary access decisions. Base64.", "type": ["string", "null"] + }, + "ephemeralPublicKey": { + "description": "For ECC wrapped keys, the client public key portion used, with the KAS public key identified with the key id, to derive a shared key that encrypts the wrapped key.", + "type": ["string", "null"] } } }, diff --git a/sdk/schema/manifest.schema.json b/sdk/schema/manifest.schema.json index dfb920d38..6488623fa 100644 --- a/sdk/schema/manifest.schema.json +++ b/sdk/schema/manifest.schema.json @@ -52,7 +52,7 @@ "type": { "description": "The type of key access object.", "type": "string", - "enum": ["wrapped", "remote"] + "enum": ["ec-wrapped", "remote", "wrapped"] }, "url": { "description": "A fully qualified URL pointing to a key access service responsible for managing access to the encryption keys.", @@ -64,7 +64,7 @@ "enum": ["kas"] }, "wrappedKey": { - "description": "The symmetric key used to encrypt the payload. It has been encrypted using the public key of the KAS, then base64 encoded.", + "description": "The symmetric key used to encrypt the payload. It has been encrypted using the public key of the KAS, then base64 encoded. Options", "type": "string" }, "sid": { @@ -99,6 +99,10 @@ "encryptedMetadata": { "description": "Metadata associated with the TDF, and the request. The contents of the metadata are freeform, and are used to pass information from the client, and any plugins that may be in use by the KAS. The metadata stored here should not be used for primary access decisions. Base64.", "type": "string" + }, + "ephemeralPublicKey": { + "description": "For ECC wrapped keys, the client public key portion used, with the KAS public key identified with the key id, to derive a shared key that encrypts the wrapped key.", + "type": "string" } } }, diff --git a/sdk/tdf.go b/sdk/tdf.go index d286a5389..f665d11d4 100644 --- a/sdk/tdf.go +++ b/sdk/tdf.go @@ -480,7 +480,7 @@ func (s SDK) prepareManifest(ctx context.Context, t *TDFObject, tdfConfig TDFCon } // wrap the key with kas public key - asymEncrypt, err := ocrypto.NewAsymEncryption(kasInfo.PublicKey) + asymEncrypt, err := ocrypto.FromPublicPEM(kasInfo.PublicKey) if err != nil { return fmt.Errorf("ocrypto.NewAsymEncryption failed:%w", err) } diff --git a/service/internal/security/crypto_provider.go b/service/internal/security/crypto_provider.go index 9538d3d6a..8c535fa55 100644 --- a/service/internal/security/crypto_provider.go +++ b/service/internal/security/crypto_provider.go @@ -19,6 +19,7 @@ type CryptoProvider interface { RSAPublicKey(keyID string) (string, error) RSAPublicKeyAsJSON(keyID string) (string, error) RSADecrypt(hash crypto.Hash, keyID string, keyLabel string, ciphertext []byte) ([]byte, error) + ECDecrypt(keyID string, ephemeralPublicKey, ciphertext []byte) ([]byte, error) ECPublicKey(keyID string) (string, error) ECCertificate(keyID string) (string, error) diff --git a/service/internal/security/standard_crypto.go b/service/internal/security/standard_crypto.go index 3fb91fc49..702e421b4 100644 --- a/service/internal/security/standard_crypto.go +++ b/service/internal/security/standard_crypto.go @@ -2,6 +2,7 @@ package security import ( "crypto" + "crypto/ecdh" "crypto/elliptic" "crypto/sha256" "crypto/x509" @@ -60,6 +61,9 @@ type StandardECCrypto struct { KeyPairInfo ecPrivateKeyPem string ecCertificatePEM string + + // Lazily filled in + sk *ecdh.PrivateKey } // List of keys by identifier @@ -427,3 +431,30 @@ func versionSalt() []byte { digest.Write([]byte(kNanoTDFMagicStringAndVersion)) return digest.Sum(nil) } + +// ECDecrypt uses hybrid ECIES to decrypt the data. +func (s *StandardCrypto) ECDecrypt(keyID string, ephemeralPublicKey, ciphertext []byte) ([]byte, error) { + ska, ok := s.keysByID[keyID] + if !ok { + return nil, fmt.Errorf("key [%s] not found", keyID) + } + sk, ok := ska.(StandardECCrypto) + if !ok { + return nil, fmt.Errorf("key [%s] is not an EC key", keyID) + } + if sk.sk == nil { + // Parse the private key + loaded, err := ocrypto.ECPrivateKeyFromPem([]byte(sk.ecPrivateKeyPem)) + if err != nil { + return nil, fmt.Errorf("failed to parse EC private key: %w", err) + } + sk.sk = loaded + } + + ed, err := ocrypto.NewECDecryptor(sk.sk) + if err != nil { + return nil, fmt.Errorf("failed to create EC decryptor: %w", err) + } + + return ed.DecryptWithEphemeralKey(ciphertext, ephemeralPublicKey) +} diff --git a/service/kas/access/rewrap.go b/service/kas/access/rewrap.go index 5507cdcc0..f5adbb23a 100644 --- a/service/kas/access/rewrap.go +++ b/service/kas/access/rewrap.go @@ -66,11 +66,16 @@ type entityInfo struct { } type kaoResult struct { - ID string - Key []byte - Error error + ID string + DEK []byte + Encapped []byte + Error error + + // Optional: Present for EC wrapped responses + EphemeralPublicKey []byte } +// From policy ID to KAO ID to result type policyKAOResults map[string]map[string]kaoResult const ( @@ -335,9 +340,9 @@ func addResultsToResponse(response *kaspb.RewrapResponse, result policyKAOResult case kaoRes.Error != nil: kaoResult.Status = kFailedStatus kaoResult.Result = &kaspb.KeyAccessRewrapResult_Error{Error: kaoRes.Error.Error()} - case kaoRes.Key != nil: + case kaoRes.Encapped != nil: kaoResult.Status = kPermitStatus - kaoResult.Result = &kaspb.KeyAccessRewrapResult_KasWrappedKey{KasWrappedKey: kaoRes.Key} + kaoResult.Result = &kaspb.KeyAccessRewrapResult_KasWrappedKey{KasWrappedKey: kaoRes.Encapped} default: kaoResult.Status = kFailedStatus kaoResult.Result = &kaspb.KeyAccessRewrapResult_Error{Error: "kao not processed by kas"} @@ -388,7 +393,7 @@ func (p *Provider) Rewrap(ctx context.Context, req *connect.Request[kaspb.Rewrap } var results policyKAOResults if len(tdf3Reqs) > 0 { - results = p.tdf3Rewrap(ctx, tdf3Reqs, body.GetClientPublicKey(), entityInfo) + resp.SessionPublicKey, results = p.tdf3Rewrap(ctx, tdf3Reqs, body.GetClientPublicKey(), entityInfo) addResultsToResponse(resp, results) } else { resp.SessionPublicKey, results = p.nanoTDFRewrap(ctx, nanoReqs, body.GetClientPublicKey(), entityInfo) @@ -408,7 +413,7 @@ func (p *Provider) Rewrap(ctx context.Context, req *connect.Request[kaspb.Rewrap if kao.Error != nil { return nil, kao.Error } - resp.EntityWrappedKey = kao.Key //nolint:staticcheck // deprecated but keeping behavior for backwards compatibility + resp.EntityWrappedKey = kao.Encapped //nolint:staticcheck // deprecated but keeping behavior for backwards compatibility } return connect.NewResponse(resp), err @@ -429,30 +434,38 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned failedKAORewrap(results, kao, err400("bad request")) continue } - var kidsToCheck []string - if kao.GetKeyAccessObject().GetKid() != "" { - kidsToCheck = []string{kao.GetKeyAccessObject().GetKid()} - } else { - p.Logger.InfoContext(ctx, "kid free kao") - for _, k := range p.KASConfig.Keyring { - if k.Algorithm == security.AlgorithmRSA2048 && k.Legacy { - kidsToCheck = append(kidsToCheck, k.KID) + + var symKey []byte + var err error + switch kao.GetKeyAccessObject().GetKeyType() { + case "ec-wrapped": + symKey, err = p.CryptoProvider.ECDecrypt(kao.GetKeyAccessObject().GetKid(), kao.GetKeyAccessObject().GetEphemeralPublicKey(), kao.GetKeyAccessObject().GetWrappedKey()) + case "wrapped": + var kidsToCheck []string + if kao.GetKeyAccessObject().GetKid() != "" { + kidsToCheck = []string{kao.GetKeyAccessObject().GetKid()} + } else { + p.Logger.InfoContext(ctx, "kid free kao") + for _, k := range p.KASConfig.Keyring { + if k.Algorithm == security.AlgorithmRSA2048 && k.Legacy { + kidsToCheck = append(kidsToCheck, k.KID) + } + } + if len(kidsToCheck) == 0 { + p.Logger.WarnContext(ctx, "failure to find legacy kids for rsa") + failedKAORewrap(results, kao, err400("bad request")) + continue } } - if len(kidsToCheck) == 0 { - p.Logger.WarnContext(ctx, "failure to find legacy kids for rsa") - failedKAORewrap(results, kao, err400("bad request")) - continue - } - } - symKey, err := p.CryptoProvider.RSADecrypt(crypto.SHA1, kidsToCheck[0], "", kao.GetKeyAccessObject().GetWrappedKey()) - for _, kid := range kidsToCheck[1:] { - p.Logger.WarnContext(ctx, "continue paging through legacy KIDs for kid free kao", "err", err) - if err == nil { - break + symKey, err = p.CryptoProvider.RSADecrypt(crypto.SHA1, kidsToCheck[0], "", kao.GetKeyAccessObject().GetWrappedKey()) + for _, kid := range kidsToCheck[1:] { + p.Logger.WarnContext(ctx, "continue paging through legacy KIDs for kid free kao", "err", err) + if err == nil { + break + } + symKey, err = p.CryptoProvider.RSADecrypt(crypto.SHA1, kid, "", kao.GetKeyAccessObject().GetWrappedKey()) } - symKey, err = p.CryptoProvider.RSADecrypt(crypto.SHA1, kid, "", kao.GetKeyAccessObject().GetWrappedKey()) } if err != nil { p.Logger.WarnContext(ctx, "failure to decrypt dek", "err", err) @@ -460,14 +473,13 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned continue } - err = verifyPolicyBinding(ctx, []byte(req.GetPolicy().GetBody()), kao, symKey, *p.Logger) - if err != nil { + if err := verifyPolicyBinding(ctx, []byte(req.GetPolicy().GetBody()), kao, symKey, *p.Logger); err != nil { failedKAORewrap(results, kao, err) continue } results[kao.GetKeyAccessObjectId()] = kaoResult{ ID: kao.GetKeyAccessObjectId(), - Key: symKey, + DEK: symKey, } anyValidKAOs = true @@ -485,7 +497,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned return policy, results, nil } -func (p *Provider) tdf3Rewrap(ctx context.Context, requests []*kaspb.UnsignedRewrapRequest_WithPolicyRequest, clientPublicKey string, entity *entityInfo) policyKAOResults { +func (p *Provider) tdf3Rewrap(ctx context.Context, requests []*kaspb.UnsignedRewrapRequest_WithPolicyRequest, clientPublicKey string, entity *entityInfo) (string, policyKAOResults) { if p.Tracer != nil { var span trace.Span ctx, span = p.Tracer.Start(ctx, "rewrap-tdf3") @@ -512,19 +524,37 @@ func (p *Provider) tdf3Rewrap(ctx context.Context, requests []*kaspb.UnsignedRew pdpAccessResults, accessErr := p.canAccess(ctx, tok, policies) if accessErr != nil { failAllKaos(requests, results, err403("could not perform access")) - return results + return "", results } - asymEncrypt, err := ocrypto.NewAsymEncryption(clientPublicKey) + asymEncrypt, err := ocrypto.FromPublicPEM(clientPublicKey) if err != nil { p.Logger.WarnContext(ctx, "ocrypto.NewAsymEncryption:", "err", err) + failAllKaos(requests, results, err400("invalid request")) + return "", results + } + + var sessionKey string + if e, ok := asymEncrypt.(ocrypto.ECEncryptor); ok { + sessionKey, err = e.PublicKeyInPemFormat() + if err != nil { + p.Logger.ErrorContext(ctx, "unable to serialize ephemeral key", "err", err) + // This may be a 500, but could also be caused by a bad clientPublicKey + failAllKaos(requests, results, err400("invalid request")) + return "", results + } } for _, pdpAccess := range pdpAccessResults { policy := pdpAccess.Policy req, ok := policyReqs[policy] - kaoResults := results[req.GetPolicy().GetId()] + if !ok { + p.Logger.WarnContext(ctx, "policy not found in policyReqs", "policy.uuid", policy.UUID) + continue + } + kaoResults, ok := results[req.GetPolicy().GetId()] if !ok { // this should not happen + p.Logger.WarnContext(ctx, "policy not found in policyReq response", "policy.uuid", policy.UUID) continue } access := pdpAccess.Access @@ -533,7 +563,8 @@ func (p *Provider) tdf3Rewrap(ctx context.Context, requests []*kaspb.UnsignedRew kasPolicy := ConvertToAuditKasPolicy(*policy) for _, kao := range req.GetKeyAccessObjects() { - kaoRes := kaoResults[kao.GetKeyAccessObjectId()] + kaoID := kao.GetKeyAccessObjectId() + kaoRes := kaoResults[kaoID] if kaoRes.Error != nil { continue } @@ -553,22 +584,23 @@ func (p *Provider) tdf3Rewrap(ctx context.Context, requests []*kaspb.UnsignedRew continue } - rewrappedKey, err := asymEncrypt.Encrypt(kaoRes.Key) + rewrappedKey, err := asymEncrypt.Encrypt(kaoRes.DEK) if err != nil { p.Logger.WarnContext(ctx, "rewrap: ocrypto.AsymEncryption.encrypt failed", "err", err, "clientPublicKey", clientPublicKey) p.Logger.Audit.RewrapFailure(ctx, auditEventParams) failedKAORewrap(kaoResults, kao, err400("bad key for rewrap")) continue } - kaoResults[kao.GetKeyAccessObjectId()] = kaoResult{ - ID: kao.GetKeyAccessObjectId(), - Key: rewrappedKey, + kaoResults[kaoID] = kaoResult{ + ID: kaoID, + Encapped: rewrappedKey, + EphemeralPublicKey: asymEncrypt.EphemeralKey(), } p.Logger.Audit.RewrapSuccess(ctx, auditEventParams) } } - return results + return sessionKey, results } func (p *Provider) nanoTDFRewrap(ctx context.Context, requests []*kaspb.UnsignedRewrapRequest_WithPolicyRequest, clientPublicKey string, entity *entityInfo) (string, policyKAOResults) { @@ -645,7 +677,7 @@ func (p *Provider) nanoTDFRewrap(ctx context.Context, requests []*kaspb.Unsigned failedKAORewrap(kaoResults, kao, err403("forbidden")) continue } - cipherText, err := wrapKeyAES(sessionKey, kaoInfo.Key) + cipherText, err := wrapKeyAES(sessionKey, kaoInfo.DEK) if err != nil { p.Logger.Audit.RewrapFailure(ctx, auditEventParams) failedKAORewrap(kaoResults, kao, err403("forbidden")) @@ -653,8 +685,8 @@ func (p *Provider) nanoTDFRewrap(ctx context.Context, requests []*kaspb.Unsigned } kaoResults[kao.GetKeyAccessObjectId()] = kaoResult{ - ID: kao.GetKeyAccessObjectId(), - Key: cipherText, + ID: kao.GetKeyAccessObjectId(), + Encapped: cipherText, } p.Logger.Audit.RewrapSuccess(ctx, auditEventParams) @@ -725,7 +757,7 @@ func (p *Provider) verifyNanoRewrapRequests(ctx context.Context, req *kaspb.Unsi } results[kao.GetKeyAccessObjectId()] = kaoResult{ ID: kao.GetKeyAccessObjectId(), - Key: symmetricKey, + DEK: symmetricKey, } return policy, results } diff --git a/service/kas/kas.proto b/service/kas/kas.proto index 312540360..227d894d4 100644 --- a/service/kas/kas.proto +++ b/service/kas/kas.proto @@ -47,6 +47,9 @@ message KeyAccess { bytes wrapped_key = 8; // header is only used for NanoTDFs bytes header = 9; + + // For wrapping with an ECDH derived key, when type=ec-wrapped + bytes ephemeral_public_key = 10; } message UnsignedRewrapRequest { From 86ba189b646f7079315ef5d8065cd1231975baf2 Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Wed, 5 Feb 2025 17:02:17 -0500 Subject: [PATCH 02/18] Lets ephemeral key be PKIX encoded --- lib/ocrypto/asym_decryption.go | 31 ++++++++++++++------ lib/ocrypto/asym_encrypt_decrypt_test.go | 36 ++++++++++++++++++++---- lib/ocrypto/asym_encryption.go | 15 ++++++++-- 3 files changed, 67 insertions(+), 15 deletions(-) diff --git a/lib/ocrypto/asym_decryption.go b/lib/ocrypto/asym_decryption.go index 83a41dcf0..a08797f7e 100644 --- a/lib/ocrypto/asym_decryption.go +++ b/lib/ocrypto/asym_decryption.go @@ -119,14 +119,29 @@ func (e ECDecryptor) Decrypt(_ []byte) ([]byte, error) { } func (e ECDecryptor) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) { - ekDSA, err := UncompressECPubKey(convCurve(e.sk.Curve()), ephemeral) - if err != nil { - return nil, err - } - - ek, err := ekDSA.ECDH() - if err != nil { - return nil, fmt.Errorf("ecdh failure: %w", err) + var ek *ecdh.PublicKey + + if pubFromDSN, err := x509.ParsePKIXPublicKey(ephemeral); err == nil { + switch pubFromDSN := pubFromDSN.(type) { + case *ecdsa.PublicKey: + ek, err = ConvertToECDHPublicKey(pubFromDSN) + if err != nil { + return nil, fmt.Errorf("ecdh conversion failure: %w", err) + } + case *ecdh.PublicKey: + ek = pubFromDSN + default: + return nil, errors.New("not an supported type of public key") + } + } else { + ekDSA, err := UncompressECPubKey(convCurve(e.sk.Curve()), ephemeral) + if err != nil { + return nil, err + } + ek, err = ekDSA.ECDH() + if err != nil { + return nil, fmt.Errorf("ecdh failure: %w", err) + } } ikm, err := e.sk.ECDH(ek) diff --git a/lib/ocrypto/asym_encrypt_decrypt_test.go b/lib/ocrypto/asym_encrypt_decrypt_test.go index 534145104..29a9d33de 100644 --- a/lib/ocrypto/asym_encrypt_decrypt_test.go +++ b/lib/ocrypto/asym_encrypt_decrypt_test.go @@ -5,7 +5,7 @@ import ( ) func TestAsymEncryptionAndDecryption(t *testing.T) { - var rsaKeys = []struct { + var keypairs = []struct { privateKey string publicKey string }{ @@ -215,9 +215,24 @@ I099IoRfC5djHUYYLMU/VkOIHuPC3sb7J65pSN26eR8bTMVNagk187V/xNwUuvkf wVyElqp317Ksz+GtTIc+DE6oryxK3tZd4hrj9fXT4KiJvQ4pcRjpePgH7B8= -----END CERTIFICATE-----`, }, + {`-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgwQlQvwfqC0sEaPVi +l1CdHNqAndukGsrqMsfiIefXHQChRANCAAQSZSoVakwpWhKBZIR9dmmTkKv7GK6n +6d0yFeGzOyqB7l9LOzOwlCDdm9k0jBQBw597Dyy7KQzW73zi+pSpgfYr +-----END PRIVATE KEY----- +`, `-----BEGIN CERTIFICATE----- +MIIBcTCCARegAwIBAgIUQBzVxCvhpTzXU+i7qyiTNniBL4owCgYIKoZIzj0EAwIw +DjEMMAoGA1UEAwwDa2FzMB4XDTI1MDExMDE2MzQ1NVoXDTI2MDExMDE2MzQ1NVow +DjEMMAoGA1UEAwwDa2FzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEmUqFWpM +KVoSgWSEfXZpk5Cr+xiup+ndMhXhszsqge5fSzszsJQg3ZvZNIwUAcOfew8suykM +1u984vqUqYH2K6NTMFEwHQYDVR0OBBYEFCAo/c694aHwmw/0kUTKuFvAQ4OcMB8G +A1UdIwQYMBaAFCAo/c694aHwmw/0kUTKuFvAQ4OcMA8GA1UdEwEB/wQFMAMBAf8w +CgYIKoZIzj0EAwIDSAAwRQIgUzKsJS6Pcu2aZ6BFfuqob552Ebdel4uFGZMqWrwW +bW0CIQDT5QED+8mHFot9JXSx2q1c5mnRvl4yElK0fiHeatBdqw== +-----END CERTIFICATE-----`}, } - for _, test := range rsaKeys { + for _, test := range keypairs { asymEncryptor, err := FromPublicPEM(test.publicKey) if err != nil { t.Fatalf("NewAsymEncryption - failed: %v", err) @@ -234,9 +249,20 @@ wVyElqp317Ksz+GtTIc+DE6oryxK3tZd4hrj9fXT4KiJvQ4pcRjpePgH7B8= t.Fatalf("NewAsymDecryption - failed: %v", err) } - decryptedText, err := asymDecryptor.Decrypt(cipherText) - if err != nil { - t.Fatalf("AsymDecryption decrypt failed: %v", err) + var decryptedText []byte + ek := asymEncryptor.EphemeralKey() + if ek == nil { + decryptedText, err = asymDecryptor.Decrypt(cipherText) + if err != nil { + t.Fatalf("AsymDecryption decrypt failed: %v", err) + } + } else if ecd, ok := asymDecryptor.(ECDecryptor); ok { + decryptedText, err = ecd.DecryptWithEphemeralKey(cipherText, ek) + if err != nil { + t.Fatalf("AsymDecryption decrypt failed: %v", err) + } + } else { + t.Fatalf("AsymDecryption wrong type: %T", asymDecryptor) } if string(decryptedText) != plainText { diff --git a/lib/ocrypto/asym_encryption.go b/lib/ocrypto/asym_encryption.go index 39c2c1765..ecffc0057 100644 --- a/lib/ocrypto/asym_encryption.go +++ b/lib/ocrypto/asym_encryption.go @@ -4,6 +4,7 @@ import ( "crypto/aes" "crypto/cipher" "crypto/ecdh" + "crypto/ecdsa" "crypto/rand" "crypto/rsa" "crypto/sha1" //nolint:gosec // used for padding which is safe @@ -63,6 +64,12 @@ func FromPublicPEM(publicKeyInPem string) (PublicKeyEncryptor, error) { switch pub := pub.(type) { case *rsa.PublicKey: return &AsymEncryption{pub}, nil + case *ecdsa.PublicKey: + e, err := pub.ECDH() + if err != nil { + return nil, err + } + return newECIES(e) case *ecdh.PublicKey: return newECIES(pub) default: @@ -135,7 +142,11 @@ func (e AsymEncryption) EphemeralKey() []byte { } func (e ECEncryptor) EphemeralKey() []byte { - return e.ek.PublicKey().Bytes() + publicKeyBytes, err := x509.MarshalPKIXPublicKey(e.ek.PublicKey()) + if err != nil { + return nil + } + return publicKeyBytes } func (e AsymEncryption) Metadata() (map[string]string, error) { @@ -144,7 +155,7 @@ func (e AsymEncryption) Metadata() (map[string]string, error) { func (e ECEncryptor) Metadata() (map[string]string, error) { m := make(map[string]string) - m["ephemeralPublicKey"] = string(e.ek.PublicKey().Bytes()) + m["ephemeralPublicKey"] = string(e.EphemeralKey()) return m, nil } From addf5eff9602bacf3e15b45e5d7ed4fddd48285b Mon Sep 17 00:00:00 2001 From: sujan kota Date: Fri, 7 Feb 2025 15:51:56 -0500 Subject: [PATCH 03/18] sdk side changes to support ecc --- examples/cmd/decrypt.go | 5 +- examples/cmd/encrypt.go | 2 +- lib/ocrypto/asym_decryption.go | 3 +- lib/ocrypto/ec_key_pair.go | 67 ++++++++++++ lib/ocrypto/rsa_key_pair.go | 5 + sdk/kas_client.go | 68 ++++++++++-- sdk/tdf.go | 182 +++++++++++++++++++++++++-------- sdk/tdf_config.go | 148 ++++++++++++++++++++++++--- service/kas/access/rewrap.go | 42 +++++++- 9 files changed, 448 insertions(+), 74 deletions(-) diff --git a/examples/cmd/decrypt.go b/examples/cmd/decrypt.go index 430465e1d..0ef5f841a 100644 --- a/examples/cmd/decrypt.go +++ b/examples/cmd/decrypt.go @@ -4,10 +4,12 @@ import ( "bytes" "errors" "fmt" + "github.com/opentdf/platform/sdk" "io" "os" "path/filepath" + "github.com/opentdf/platform/lib/ocrypto" "github.com/spf13/cobra" ) @@ -81,7 +83,8 @@ func decrypt(cmd *cobra.Command, args []string) error { } if !isNano { - tdfreader, err := client.LoadTDF(file) + opts := []sdk.TDFReaderOption{sdk.WithSessionKeyType(ocrypto.ECKey, 256)} + tdfreader, err := client.LoadTDF(file, opts...) if err != nil { return err } diff --git a/examples/cmd/encrypt.go b/examples/cmd/encrypt.go index fdf242792..093c3ce69 100644 --- a/examples/cmd/encrypt.go +++ b/examples/cmd/encrypt.go @@ -10,7 +10,6 @@ import ( "strings" "github.com/opentdf/platform/lib/ocrypto" - "github.com/opentdf/platform/sdk" "github.com/spf13/cobra" ) @@ -102,6 +101,7 @@ func encrypt(cmd *cobra.Command, args []string) error { opts := []sdk.TDFOption{sdk.WithDataAttributes(dataAttributes...)} if !autoconfigure { opts = append(opts, sdk.WithAutoconfigure(autoconfigure)) + opts = append(opts, sdk.WithKeyType(ocrypto.ECKey, 256)) opts = append(opts, sdk.WithKasInformation( sdk.KASInfo{ // examples assume insecure http diff --git a/lib/ocrypto/asym_decryption.go b/lib/ocrypto/asym_decryption.go index a08797f7e..4744eb91a 100644 --- a/lib/ocrypto/asym_decryption.go +++ b/lib/ocrypto/asym_decryption.go @@ -109,8 +109,7 @@ type ECDecryptor struct { func NewECDecryptor(sk *ecdh.PrivateKey) (ECDecryptor, error) { // TK Make these reasonable? IIRC salt should be longer, info maybe a parameters? salt := []byte("salt") - info := []byte("info") - return ECDecryptor{sk, salt, info}, nil + return ECDecryptor{sk, salt, nil}, nil } func (e ECDecryptor) Decrypt(_ []byte) ([]byte, error) { diff --git a/lib/ocrypto/ec_key_pair.go b/lib/ocrypto/ec_key_pair.go index 6b9d0a960..144ade78c 100644 --- a/lib/ocrypto/ec_key_pair.go +++ b/lib/ocrypto/ec_key_pair.go @@ -19,6 +19,13 @@ import ( type ECCMode uint8 +type KeyType int + +const ( + RSAKey KeyType = iota + ECKey +) + const ( ECCModeSecp256r1 ECCMode = 0 ECCModeSecp384r1 ECCMode = 1 @@ -26,6 +33,18 @@ const ( ECCModeSecp256k1 ECCMode = 3 ) +const ( + ECCurveP256Size = 256 + ECCurveP384Size = 384 + ECCurveP521Size = 521 +) + +type KeyPair interface { + PublicKeyInPemFormat() (string, error) + PrivateKeyInPemFormat() (string, error) + GetKeyType() KeyType +} + type ECKeyPair struct { PrivateKey *ecdsa.PrivateKey } @@ -65,6 +84,20 @@ func (mode ECCMode) String() string { return "unspecified" } +// ECSizeToMode converts a curve size to an ECCMode +func ECSizeToMode(size int) (ECCMode, error) { + switch size { + case ECCurveP256Size: + return ECCModeSecp256r1, nil + case ECCurveP384Size: + return ECCModeSecp384r1, nil + case ECCurveP521Size: + return ECCModeSecp521r1, nil + default: + return 0, fmt.Errorf("unsupported EC curve size: %d", size) + } +} + // NewECKeyPair Generates an EC key pair of the given bit size. func NewECKeyPair(mode ECCMode) (ECKeyPair, error) { var c elliptic.Curve @@ -361,3 +394,37 @@ func ECPublicKeyInPemFormat(publicKey ecdsa.PublicKey) (string, error) { return string(publicKeyPem), nil } + +// GetECKeySize returns the curve size from a PEM-encoded EC public key +func GetECKeySize(pemData []byte) (int, error) { + block, _ := pem.Decode(pemData) + if block == nil { + return 0, fmt.Errorf("failed to parse PEM block") + } + + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return 0, fmt.Errorf("failed to parse public key: %w", err) + } + + ecKey, ok := pub.(*ecdsa.PublicKey) + if !ok { + return 0, fmt.Errorf("not an EC key") + } + + switch ecKey.Curve { + case elliptic.P256(): + return ECCurveP256Size, nil + case elliptic.P384(): + return ECCurveP384Size, nil + case elliptic.P521(): + return ECCurveP521Size, nil + default: + return 0, fmt.Errorf("unknown curve") + } +} + +// GetKeyType returns the key type (ECKey) +func (keyPair ECKeyPair) GetKeyType() KeyType { + return ECKey +} diff --git a/lib/ocrypto/rsa_key_pair.go b/lib/ocrypto/rsa_key_pair.go index e52a5428a..068cb66c4 100644 --- a/lib/ocrypto/rsa_key_pair.go +++ b/lib/ocrypto/rsa_key_pair.go @@ -76,3 +76,8 @@ func (keyPair RsaKeyPair) KeySize() (int, error) { } return keyPair.privateKey.N.BitLen(), nil } + +// GetKeyType returns the key type (RSAKey) +func (keyPair RsaKeyPair) GetKeyType() KeyType { + return RSAKey +} diff --git a/sdk/kas_client.go b/sdk/kas_client.go index 1c4779a28..642bcca47 100644 --- a/sdk/kas_client.go +++ b/sdk/kas_client.go @@ -20,12 +20,13 @@ import ( const ( secondsPerMinute = 60 + statusPermit = "permit" ) type KASClient struct { accessTokenSource auth.AccessTokenSource dialOptions []grpc.DialOption - sessionKey *ocrypto.RsaKeyPair + sessionKey ocrypto.KeyPair } type kaoResult struct { @@ -39,7 +40,7 @@ type decryptor interface { Decrypt(ctx context.Context, results []kaoResult) (int, error) } -func newKASClient(dialOptions []grpc.DialOption, accessTokenSource auth.AccessTokenSource, sessionKey *ocrypto.RsaKeyPair) *KASClient { +func newKASClient(dialOptions []grpc.DialOption, accessTokenSource auth.AccessTokenSource, sessionKey ocrypto.KeyPair) *KASClient { return &KASClient{ accessTokenSource: accessTokenSource, dialOptions: dialOptions, @@ -113,7 +114,7 @@ func (k *KASClient) nanoUnwrap(ctx context.Context, requests ...*kas.UnsignedRew for _, results := range response.GetResponses() { var kaoKeys []kaoResult for _, kao := range results.GetResults() { - if kao.GetStatus() == "permit" { + if kao.GetStatus() == statusPermit { wrappedKey := kao.GetKasWrappedKey() key, err := aesGcm.Decrypt(wrappedKey) if err != nil { @@ -144,6 +145,57 @@ func (k *KASClient) unwrap(ctx context.Context, requests ...*kas.UnsignedRewrapR return nil, fmt.Errorf("error making rewrap request to kas: %w", err) } + if k.sessionKey.GetKeyType() == ocrypto.ECKey { + return k.handleECKeyResponse(response) + } + return k.handleRSAKeyResponse(response) +} + +func (k *KASClient) handleECKeyResponse(response *kas.RewrapResponse) (map[string][]kaoResult, error) { + kasEphemeralPublicKey := response.GetSessionPublicKey() + clientPrivateKey, err := k.sessionKey.PrivateKeyInPemFormat() + if err != nil { + return nil, fmt.Errorf("failed to get private key: %w", err) + } + ecdhKey, err := ocrypto.ComputeECDHKey([]byte(clientPrivateKey), []byte(kasEphemeralPublicKey)) + if err != nil { + return nil, fmt.Errorf("ocrypto.ComputeECDHKey failed: %w", err) + } + sessionKey, err := ocrypto.CalculateHKDF([]byte("salt"), ecdhKey) + if err != nil { + return nil, fmt.Errorf("ocrypto.CalculateHKDF failed: %w", err) + } + + aesGcm, err := ocrypto.NewAESGcm(sessionKey) + if err != nil { + return nil, fmt.Errorf("ocrypto.NewAESGcm failed: %w", err) + } + + return k.processECResponse(response, aesGcm) +} + +func (k *KASClient) processECResponse(response *kas.RewrapResponse, aesGcm ocrypto.AesGcm) (map[string][]kaoResult, error) { + policyResults := make(map[string][]kaoResult) + for _, results := range response.GetResponses() { + var kaoKeys []kaoResult + for _, kao := range results.GetResults() { + if kao.GetStatus() == statusPermit { + key, err := aesGcm.Decrypt(kao.GetKasWrappedKey()) + if err != nil { + kaoKeys = append(kaoKeys, kaoResult{KeyAccessObjectID: kao.GetKeyAccessObjectId(), Error: err}) + } else { + kaoKeys = append(kaoKeys, kaoResult{KeyAccessObjectID: kao.GetKeyAccessObjectId(), SymmetricKey: key}) + } + } else { + kaoKeys = append(kaoKeys, kaoResult{KeyAccessObjectID: kao.GetKeyAccessObjectId(), Error: errors.New(kao.GetError())}) + } + } + policyResults[results.GetPolicyId()] = kaoKeys + } + return policyResults, nil +} + +func (k *KASClient) handleRSAKeyResponse(response *kas.RewrapResponse) (map[string][]kaoResult, error) { clientPrivateKey, err := k.sessionKey.PrivateKeyInPemFormat() if err != nil { return nil, fmt.Errorf("ocrypto.PrivateKeyInPemFormat failed: %w", err) @@ -154,13 +206,16 @@ func (k *KASClient) unwrap(ctx context.Context, requests ...*kas.UnsignedRewrapR return nil, fmt.Errorf("ocrypto.NewAsymDecryption failed: %w", err) } + return k.processRSAResponse(response, asymDecryption) +} + +func (k *KASClient) processRSAResponse(response *kas.RewrapResponse, asymDecryption ocrypto.AsymDecryption) (map[string][]kaoResult, error) { policyResults := make(map[string][]kaoResult) for _, results := range response.GetResponses() { var kaoKeys []kaoResult for _, kao := range results.GetResults() { - if kao.GetStatus() == "permit" { - wrappedKey := kao.GetKasWrappedKey() - key, err := asymDecryption.Decrypt(wrappedKey) + if kao.GetStatus() == statusPermit { + key, err := asymDecryption.Decrypt(kao.GetKasWrappedKey()) if err != nil { kaoKeys = append(kaoKeys, kaoResult{KeyAccessObjectID: kao.GetKeyAccessObjectId(), Error: err}) } else { @@ -172,7 +227,6 @@ func (k *KASClient) unwrap(ctx context.Context, requests ...*kas.UnsignedRewrapR } policyResults[results.GetPolicyId()] = kaoKeys } - return policyResults, nil } diff --git a/sdk/tdf.go b/sdk/tdf.go index f665d11d4..16666bdd0 100644 --- a/sdk/tdf.go +++ b/sdk/tdf.go @@ -34,6 +34,7 @@ const ( tdfZipReference = "reference" kKeySize = 32 kWrapped = "wrapped" + kECWrapped = "ec-wrapped" kKasProtocol = "kas" kSplitKeyType = "split" kGCMCipherAlgorithm = "AES-256-GCM" @@ -65,7 +66,7 @@ type Reader struct { aesGcm ocrypto.AesGcm payloadSize int64 payloadKey []byte - kasSessionKey ocrypto.RsaKeyPair + kasSessionKey ocrypto.KeyPair config TDFReaderConfig } @@ -81,6 +82,11 @@ type tdf3DecryptHandler struct { reader *Reader } +type ecKeyWrappedKeyInfo struct { + publicKey string + wrappedKey string +} + func (r *tdf3DecryptHandler) Decrypt(ctx context.Context, results []kaoResult) (int, error) { err := r.reader.buildKey(ctx, results) if err != nil { @@ -412,12 +418,20 @@ func (s SDK) prepareManifest(ctx context.Context, t *TDFObject, tdfConfig TDFCon conjunction := make(map[string][]KASInfo) var splitIDs []string + var keyAlgorithm string + if tdfConfig.keyType == ocrypto.RSAKey { + keyAlgorithm = fmt.Sprintf("rsa:%d", tdfConfig.keySize) + } else { + mode, _ := ocrypto.ECSizeToMode(tdfConfig.keySize) + keyAlgorithm = mode.String() + } + for _, splitInfo := range tdfConfig.splitPlan { // Public key was passed in with kasInfoList // TODO first look up in attribute information / add to split plan? ki, ok := latestKASInfo[splitInfo.KAS] if !ok || ki.PublicKey == "" { - k, err := s.getPublicKey(ctx, splitInfo.KAS, "rsa:2048") + k, err := s.getPublicKey(ctx, splitInfo.KAS, keyAlgorithm) if err != nil { return fmt.Errorf("unable to retrieve public key from KAS at [%s]: %w", splitInfo.KAS, err) } @@ -451,27 +465,10 @@ func (s SDK) prepareManifest(ctx context.Context, t *TDFObject, tdfConfig TDFCon // add meta data var encryptedMetadata string if len(tdfConfig.metaData) > 0 { - gcm, err := ocrypto.NewAESGcm(symKey) - if err != nil { - return fmt.Errorf("ocrypto.NewAESGcm failed:%w", err) - } - - emb, err := gcm.Encrypt([]byte(tdfConfig.metaData)) + encryptedMetadata, err = encryptMetadata(symKey, tdfConfig.metaData) if err != nil { - return fmt.Errorf("ocrypto.AesGcm.encrypt failed:%w", err) + return err } - - iv := emb[:ocrypto.GcmStandardNonceSize] - metadata := EncryptedMetadata{ - Cipher: string(ocrypto.Base64Encode(emb)), - Iv: string(ocrypto.Base64Encode(iv)), - } - - metadataJSON, err := json.Marshal(metadata) - if err != nil { - return fmt.Errorf(" json.Marshal failed:%w", err) - } - encryptedMetadata = string(ocrypto.Base64Encode(metadataJSON)) } for _, kasInfo := range conjunction[splitID] { @@ -479,27 +476,9 @@ func (s SDK) prepareManifest(ctx context.Context, t *TDFObject, tdfConfig TDFCon return fmt.Errorf("splitID:[%s], kas:[%s]: %w", splitID, kasInfo.URL, errKasPubKeyMissing) } - // wrap the key with kas public key - asymEncrypt, err := ocrypto.FromPublicPEM(kasInfo.PublicKey) + keyAccess, err := createKeyAccess(tdfConfig, kasInfo, symKey, policyBinding, encryptedMetadata, splitID) if err != nil { - return fmt.Errorf("ocrypto.NewAsymEncryption failed:%w", err) - } - - wrappedKey, err := asymEncrypt.Encrypt(symKey) - if err != nil { - return fmt.Errorf("ocrypto.AsymEncryption.encrypt failed:%w", err) - } - - keyAccess := KeyAccess{ - KeyType: kWrapped, - KasURL: kasInfo.URL, - KID: kasInfo.KID, - Protocol: kKasProtocol, - PolicyBinding: policyBinding, - EncryptedMetadata: encryptedMetadata, - SplitID: splitID, - WrappedKey: string(ocrypto.Base64Encode(wrappedKey)), - SchemaVersion: keyAccessSchemaVersion, + return err } manifest.EncryptionInformation.KeyAccessObjs = append(manifest.EncryptionInformation.KeyAccessObjs, keyAccess) @@ -526,6 +505,118 @@ func (s SDK) prepareManifest(ctx context.Context, t *TDFObject, tdfConfig TDFCon return nil } +func encryptMetadata(symKey []byte, metaData string) (string, error) { + gcm, err := ocrypto.NewAESGcm(symKey) + if err != nil { + return "", fmt.Errorf("ocrypto.NewAESGcm failed:%w", err) + } + + emb, err := gcm.Encrypt([]byte(metaData)) + if err != nil { + return "", fmt.Errorf("ocrypto.AesGcm.encrypt failed:%w", err) + } + + iv := emb[:ocrypto.GcmStandardNonceSize] + metadata := EncryptedMetadata{ + Cipher: string(ocrypto.Base64Encode(emb)), + Iv: string(ocrypto.Base64Encode(iv)), + } + + metadataJSON, err := json.Marshal(metadata) + if err != nil { + return "", fmt.Errorf(" json.Marshal failed:%w", err) + } + return string(ocrypto.Base64Encode(metadataJSON)), nil +} + +func createKeyAccess(tdfConfig TDFConfig, kasInfo KASInfo, symKey []byte, policyBinding PolicyBinding, encryptedMetadata, splitID string) (KeyAccess, error) { + keyAccess := KeyAccess{ + KeyType: kWrapped, + KasURL: kasInfo.URL, + KID: kasInfo.KID, + Protocol: kKasProtocol, + PolicyBinding: policyBinding, + EncryptedMetadata: encryptedMetadata, + SplitID: splitID, + SchemaVersion: keyAccessSchemaVersion, + } + + if tdfConfig.keyType == ocrypto.ECKey { + wrappedKeyInfo, err := generateWrapKeyWithEC(tdfConfig.keySize, kasInfo.PublicKey, symKey) + if err != nil { + return KeyAccess{}, err + } + keyAccess.KeyType = kECWrapped + keyAccess.WrappedKey = wrappedKeyInfo.wrappedKey + keyAccess.EphemeralPublicKey = wrappedKeyInfo.publicKey + } else { + wrappedKey, err := generateWrapKeyWithRSA(kasInfo.PublicKey, symKey) + if err != nil { + return KeyAccess{}, err + } + keyAccess.WrappedKey = wrappedKey + } + + return keyAccess, nil +} + +func generateWrapKeyWithEC(keySize int, kasPublicKey string, symKey []byte) (ecKeyWrappedKeyInfo, error) { + mode, _ := ocrypto.ECSizeToMode(keySize) + ecKeyPair, err := ocrypto.NewECKeyPair(mode) + if err != nil { + return ecKeyWrappedKeyInfo{}, fmt.Errorf("ocrypto.NewECKeyPair failed:%w", err) + } + + emphermalPublicKey, err := ecKeyPair.PublicKeyInPemFormat() + if err != nil { + return ecKeyWrappedKeyInfo{}, fmt.Errorf("failed to get EC public key: %w", err) + } + + emphermalPrivateKey, err := ecKeyPair.PrivateKeyInPemFormat() + if err != nil { + return ecKeyWrappedKeyInfo{}, fmt.Errorf("failed to get EC private key: %w", err) + } + + ecdhKey, err := ocrypto.ComputeECDHKey([]byte(emphermalPrivateKey), []byte(kasPublicKey)) + if err != nil { + return ecKeyWrappedKeyInfo{}, fmt.Errorf("ocrypto.ComputeECDHKey failed:%w", err) + } + + sessionKey, err := ocrypto.CalculateHKDF([]byte("salt"), ecdhKey) + if err != nil { + return ecKeyWrappedKeyInfo{}, fmt.Errorf("ocrypto.CalculateHKDF failed:%w", err) + } + + gcm, err := ocrypto.NewAESGcm(sessionKey) + if err != nil { + return ecKeyWrappedKeyInfo{}, fmt.Errorf("ocrypto.NewAESGcm failed:%w", err) + } + + wrappedKey, err := gcm.Encrypt(symKey) + if err != nil { + return ecKeyWrappedKeyInfo{}, fmt.Errorf("ocrypto.AESGcm.Encrypt failed:%w", err) + } + + return ecKeyWrappedKeyInfo{ + publicKey: emphermalPublicKey, + wrappedKey: string(ocrypto.Base64Encode(wrappedKey)), + }, nil +} + +func generateWrapKeyWithRSA(publicKey string, symKey []byte) (string, error) { + asymEncrypt, err := ocrypto.NewAsymEncryption(publicKey) + if err != nil { + return "", fmt.Errorf("ocrypto.NewAsymEncryption failed:%w", err) + } + + wrappedKey, err := asymEncrypt.Encrypt(symKey) + if err != nil { + return "", fmt.Errorf("ocrypto.AsymEncryption.encrypt failed:%w", err) + } + + return string(ocrypto.Base64Encode(wrappedKey)), nil +} + // create policy object func createPolicyObject(attributes []AttributeValueFQN) (PolicyObject, error) { uuidObj, err := uuid.NewUUID() @@ -585,7 +676,7 @@ func (s SDK) LoadTDF(reader io.ReadSeeker, opts ...TDFReaderOption) (*Reader, er dialOptions: s.dialOptions, tdfReader: tdfReader, manifest: *manifestObj, - kasSessionKey: *s.config.kasSessionKey, + kasSessionKey: config.kasSessionKey, config: *config, }, nil } @@ -885,8 +976,9 @@ func createRewrapRequest(_ context.Context, r *Reader) (map[string]*kas.Unsigned Hash: hash, Algorithm: alg, }, - SplitId: kao.SplitID, - WrappedKey: key, + SplitId: kao.SplitID, + WrappedKey: key, + EphemeralPublicKey: []byte(kao.EphemeralPublicKey), }, } if req, ok := kasReqs[kao.KasURL]; ok { @@ -1095,7 +1187,7 @@ func (r *Reader) buildKey(_ context.Context, results []kaoResult) error { // Unwraps the payload key, if possible, using the access service func (r *Reader) doPayloadKeyUnwrap(ctx context.Context) error { //nolint:gocognit // Better readability keeping it as is - kasClient := newKASClient(r.dialOptions, r.tokenSource, &r.kasSessionKey) + kasClient := newKASClient(r.dialOptions, r.tokenSource, r.kasSessionKey) var kaoResults []kaoResult reqFail := func(err error, req *kas.UnsignedRewrapRequest_WithPolicyRequest) { diff --git a/sdk/tdf_config.go b/sdk/tdf_config.go index e0c182bb6..295e1c58d 100644 --- a/sdk/tdf_config.go +++ b/sdk/tdf_config.go @@ -13,6 +13,10 @@ const ( maxSegmentSize = defaultSegmentSize * 2 minSegmentSize = 16 * 1024 kasPublicKeyPath = "/kas_public_key" + DefaultRSAKeySize = 2048 + ECKeySize256 = 256 + ECKeySize384 = 384 + ECKeySize521 = 521 ) type TDFFormat = int @@ -63,33 +67,20 @@ type TDFConfig struct { attributeValues []*policy.Value kasInfoList []KASInfo splitPlan []keySplitStep + keyType ocrypto.KeyType + keySize int // For RSA this is key size, for EC this is curve size } func newTDFConfig(opt ...TDFOption) (*TDFConfig, error) { - rsaKeyPair, err := ocrypto.NewRSAKeyPair(tdf3KeySize) - if err != nil { - return nil, fmt.Errorf("ocrypto.NewRSAKeyPair failed: %w", err) - } - - publicKey, err := rsaKeyPair.PublicKeyInPemFormat() - if err != nil { - return nil, fmt.Errorf("ocrypto.PublicKeyInPemFormat failed: %w", err) - } - - privateKey, err := rsaKeyPair.PublicKeyInPemFormat() - if err != nil { - return nil, fmt.Errorf("ocrypto.PrivateKeyInPemFormat failed: %w", err) - } - c := &TDFConfig{ autoconfigure: true, - tdfPrivateKey: privateKey, - tdfPublicKey: publicKey, defaultSegmentSize: defaultSegmentSize, enableEncryption: true, tdfFormat: JSONFormat, integrityAlgorithm: HS256, segmentIntegrityAlgorithm: GMAC, + keyType: ocrypto.RSAKey, // default to RSA + keySize: DefaultRSAKeySize, // default size } for _, o := range opt { @@ -99,9 +90,60 @@ func newTDFConfig(opt ...TDFOption) (*TDFConfig, error) { } } + publicKey, privateKey, err := generateKeyPair(c.keyType, c.keySize) + if err != nil { + return nil, err + } + + c.tdfPrivateKey = privateKey + c.tdfPublicKey = publicKey + return c, nil } +func generateKeyPair(keyType ocrypto.KeyType, keySize int) (string, string, error) { + if keyType == ocrypto.RSAKey { + return generateRSAKeyPair(keySize) + } + return generateECKeyPair(keySize) +} + +func generateRSAKeyPair(keySize int) (string, string, error) { + rsaKeyPair, err := ocrypto.NewRSAKeyPair(keySize) + if err != nil { + return "", "", fmt.Errorf("ocrypto.NewRSAKeyPair failed: %w", err) + } + publicKey, err := rsaKeyPair.PublicKeyInPemFormat() + if err != nil { + return "", "", fmt.Errorf("ocrypto.PublicKeyInPemFormat failed: %w", err) + } + privateKey, err := rsaKeyPair.PrivateKeyInPemFormat() + if err != nil { + return "", "", fmt.Errorf("ocrypto.PrivateKeyInPemFormat failed: %w", err) + } + return publicKey, privateKey, nil +} + +func generateECKeyPair(keySize int) (string, string, error) { + mode, err := ocrypto.ECSizeToMode(keySize) + if err != nil { + return "", "", err + } + ecKeyPair, err := ocrypto.NewECKeyPair(mode) + if err != nil { + return "", "", fmt.Errorf("ocrypto.NewECKeyPair failed: %w", err) + } + publicKey, err := ecKeyPair.PublicKeyInPemFormat() + if err != nil { + return "", "", fmt.Errorf("ocrypto.PublicKeyInPemFormat failed: %w", err) + } + privateKey, err := ecKeyPair.PrivateKeyInPemFormat() + if err != nil { + return "", "", fmt.Errorf("ocrypto.PrivateKeyInPemFormat failed: %w", err) + } + return publicKey, privateKey, nil +} + // WithDataAttributes appends the given data attributes to the bound policy func WithDataAttributes(attributes ...string) TDFOption { return func(c *TDFConfig) error { @@ -213,6 +255,30 @@ func WithAutoconfigure(enable bool) TDFOption { } } +func WithKeyType(keyType ocrypto.KeyType, size int) TDFOption { + return func(c *TDFConfig) error { + switch keyType { + case ocrypto.RSAKey: + if size < 2048 || size > 4096 { + return fmt.Errorf("invalid RSA key size: %d, must be between 2048 and 4096", size) + } + case ocrypto.ECKey: + switch size { + case ECKeySize256, ECKeySize384, ECKeySize521: + // valid sizes + default: + return fmt.Errorf("invalid EC curve size: %d, must be one of 256, 384, or 521", size) + } + default: + return fmt.Errorf("unsupported key type") + } + + c.keyType = keyType + c.keySize = size + return nil + } +} + // Schema Validation where 0 = none (skip), 1 = lax (allowing novel entries, 'falsy' values for unkowns), 2 = strict (rejecting novel entries, strict match to manifest schema) type SchemaValidationIntensity int @@ -230,12 +296,19 @@ type TDFReaderConfig struct { disableAssertionVerification bool schemaValidationIntensity SchemaValidationIntensity + kasSessionKey ocrypto.KeyPair + keyType ocrypto.KeyType + keySize int // For RSA this is key size, for EC this is curve size } func newTDFReaderConfig(opt ...TDFReaderOption) (*TDFReaderConfig, error) { + var err error c := &TDFReaderConfig{ disableAssertionVerification: false, + keyType: ocrypto.RSAKey, + keySize: DefaultRSAKeySize, } + for _, o := range opt { err := o(c) if err != nil { @@ -243,6 +316,23 @@ func newTDFReaderConfig(opt ...TDFReaderOption) (*TDFReaderConfig, error) { } } + if c.keyType == ocrypto.RSAKey { + c.kasSessionKey, err = ocrypto.NewRSAKeyPair(c.keySize) + if err != nil { + return nil, fmt.Errorf("failed to create RSA key pair: %w", err) + } + } else { + var eccMode ocrypto.ECCMode + eccMode, err = ocrypto.ECSizeToMode(c.keySize) + if err != nil { + return nil, err + } + c.kasSessionKey, err = ocrypto.NewECKeyPair(eccMode) + if err != nil { + return nil, fmt.Errorf("failed to create EC key pair: %w", err) + } + } + return c, nil } @@ -266,3 +356,27 @@ func WithDisableAssertionVerification(disable bool) TDFReaderOption { return nil } } + +func WithSessionKeyType(keyType ocrypto.KeyType, size int) TDFReaderOption { + return func(c *TDFReaderConfig) error { + switch keyType { + case ocrypto.RSAKey: + if size < 2048 || size > 4096 { + return fmt.Errorf("invalid RSA key size: %d, must be between 2048 and 4096", size) + } + case ocrypto.ECKey: + switch size { + case ECKeySize256, ECKeySize384, ECKeySize521: + // valid sizes + default: + return fmt.Errorf("invalid EC curve size: %d, must be one of 256, 384, or 521", size) + } + default: + return fmt.Errorf("unsupported key type") + } + + c.keyType = keyType + c.keySize = size + return nil + } +} diff --git a/service/kas/access/rewrap.go b/service/kas/access/rewrap.go index f5adbb23a..29b4a7992 100644 --- a/service/kas/access/rewrap.go +++ b/service/kas/access/rewrap.go @@ -439,7 +439,47 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned var err error switch kao.GetKeyAccessObject().GetKeyType() { case "ec-wrapped": - symKey, err = p.CryptoProvider.ECDecrypt(kao.GetKeyAccessObject().GetKid(), kao.GetKeyAccessObject().GetEphemeralPublicKey(), kao.GetKeyAccessObject().GetWrappedKey()) + + // Get the ephemeral public key in PEM format + ephemeralPubKeyPEM := kao.GetKeyAccessObject().GetEphemeralPublicKey() + + // Get EC key size and convert to mode + keySize, err := ocrypto.GetECKeySize(ephemeralPubKeyPEM) + if err != nil { + return nil, results, fmt.Errorf("failed to get EC key size: %w", err) + } + + mode, err := ocrypto.ECSizeToMode(keySize) + if err != nil { + return nil, results, fmt.Errorf("failed to convert key size to mode: %w", err) + } + + // Parse the PEM public key + block, _ := pem.Decode(ephemeralPubKeyPEM) + if block == nil { + return nil, results, fmt.Errorf("failed to decode PEM block") + } + + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, results, fmt.Errorf("failed to parse public key: %w", err) + } + + ecPub, ok := pub.(*ecdsa.PublicKey) + if !ok { + return nil, results, fmt.Errorf("not an EC public key") + } + + // Compress the public key + compressedKey, err := ocrypto.CompressedECPublicKey(mode, *ecPub) + if err != nil { + return nil, results, fmt.Errorf("failed to compress public key: %w", err) + } + + symKey, err = p.CryptoProvider.ECDecrypt(kao.GetKeyAccessObject().GetKid(), compressedKey, kao.GetKeyAccessObject().GetWrappedKey()) + if err != nil { + return nil, results, fmt.Errorf("failed to decrypt EC key: %w", err) + } case "wrapped": var kidsToCheck []string if kao.GetKeyAccessObject().GetKid() != "" { From e3be2efaea5e0926f377c4a9605870f21a1054bc Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Fri, 7 Feb 2025 15:55:19 -0500 Subject: [PATCH 04/18] removes 'info' info --- lib/ocrypto/asym_encryption.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/ocrypto/asym_encryption.go b/lib/ocrypto/asym_encryption.go index ecffc0057..1799e3195 100644 --- a/lib/ocrypto/asym_encryption.go +++ b/lib/ocrypto/asym_encryption.go @@ -83,8 +83,7 @@ func newECIES(pub *ecdh.PublicKey) (ECEncryptor, error) { ek, err := pub.Curve().GenerateKey(rand.Reader) // TK Make these reasonable? IIRC salt should be longer, info maybe a parameters? salt := []byte("salt") - info := []byte("info") - return ECEncryptor{pub, ek, salt, info}, err + return ECEncryptor{pub, ek, salt, nil}, err } // NewAsymEncryption creates and returns a new AsymEncryption. From 35a99571c0cf314a4842199a4bf59c9ba6464fcc Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Fri, 7 Feb 2025 16:50:58 -0500 Subject: [PATCH 05/18] WIP examples and rt tests --- examples/cmd/decrypt.go | 18 +++++++++++++++--- examples/cmd/encrypt.go | 36 ++++++++++++++++++++++++++++++++++++ test/tdf-roundtrips.bats | 28 ++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/examples/cmd/decrypt.go b/examples/cmd/decrypt.go index 0ef5f841a..76016eb13 100644 --- a/examples/cmd/decrypt.go +++ b/examples/cmd/decrypt.go @@ -4,12 +4,12 @@ import ( "bytes" "errors" "fmt" - "github.com/opentdf/platform/sdk" "io" "os" "path/filepath" - "github.com/opentdf/platform/lib/ocrypto" + "github.com/opentdf/platform/sdk" + "github.com/spf13/cobra" ) @@ -20,6 +20,7 @@ func init() { RunE: decrypt, Args: cobra.MinimumNArgs(1), } + decryptCmd.Flags().StringVarP(&alg, "rewrap-encapsulation-algorithm", "a", "rsa:2048", "Key wrap response algorithm algorithm:parameters") ExamplesCmd.AddCommand(decryptCmd) } @@ -83,7 +84,18 @@ func decrypt(cmd *cobra.Command, args []string) error { } if !isNano { - opts := []sdk.TDFReaderOption{sdk.WithSessionKeyType(ocrypto.ECKey, 256)} + opts := []sdk.TDFReaderOption{} + if alg != "" { + kt, err := keyTypeForKeyType(alg) + if err != nil { + return err + } + bits, err := bitsForKeyType(alg) + if err != nil { + return err + } + opts = append(opts, sdk.WithSessionKeyType(kt, bits)) + } tdfreader, err := client.LoadTDF(file, opts...) if err != nil { return err diff --git a/examples/cmd/encrypt.go b/examples/cmd/encrypt.go index 093c3ce69..d8ed11e7b 100644 --- a/examples/cmd/encrypt.go +++ b/examples/cmd/encrypt.go @@ -22,6 +22,7 @@ var ( outputName string dataAttributes []string collection int + alg string ) func init() { @@ -37,6 +38,7 @@ func init() { encryptCmd.Flags().BoolVar(&noKIDInKAO, "no-kid-in-kao", false, "[deprecated] Disable storing key identifiers in TDF KAOs") encryptCmd.Flags().BoolVar(&noKIDInNano, "no-kid-in-nano", true, "Disable storing key identifiers in nanoTDF KAS ResourceLocator") encryptCmd.Flags().StringVarP(&outputName, "output", "o", "sensitive.txt.tdf", "name or path of output file; - for stdout") + encryptCmd.Flags().StringVarP(&alg, "key-encapsulation-algorithm", "a", "rsa:2048", "Key wrap algorithm algorithm:parameters") encryptCmd.Flags().IntVarP(&collection, "collection", "c", 0, "number of nano's to create for collection. If collection >0 (default) then output will be _") ExamplesCmd.AddCommand(&encryptCmd) @@ -109,6 +111,17 @@ func encrypt(cmd *cobra.Command, args []string) error { PublicKey: "", })) } + if alg != "" { + kt, err := keyTypeForKeyType(alg) + if err != nil { + return err + } + bits, err := bitsForKeyType(alg) + if err != nil { + return err + } + opts = append(opts, sdk.WithKeyType(kt, bits)) + } tdf, err := client.CreateTDF(out, in, opts...) if err != nil { return err @@ -156,6 +169,29 @@ func encrypt(cmd *cobra.Command, args []string) error { return nil } +func bitsForKeyType(alg string) (int, error) { + switch alg { + case "rsa:2048": + return 2048, nil + case "ec:secp256r1": + return 256, nil + default: + return 0, fmt.Errorf("unsupported key type [%s]", alg) + } +} + +func keyTypeForKeyType(alg string) (ocrypto.KeyType, error) { + switch { + case strings.HasPrefix(alg, "rsa:"): + return ocrypto.RSAKey, nil + case strings.HasPrefix(alg, "ec:"): + return ocrypto.ECKey, nil + default: + // do not submit add ocrypto.UnknownKey + return ocrypto.RSAKey, fmt.Errorf("unsupported key type [%s]", alg) + } +} + func cat(cmd *cobra.Command, nTdfFile string) error { f, err := os.Open(nTdfFile) if err != nil { diff --git a/test/tdf-roundtrips.bats b/test/tdf-roundtrips.bats index 72604164a..b46084640 100755 --- a/test/tdf-roundtrips.bats +++ b/test/tdf-roundtrips.bats @@ -25,6 +25,34 @@ run go run ./examples decrypt sensitive.txt.tdf echo "$output" printf '%s\n' "$output" | grep "Hello Zero Trust" + + echo "[INFO] decrypting with EC..." + run go run ./examples decrypt -a 'ec:secp256r1' sensitive.txt.tdf + echo "$output" + printf '%s\n' "$output" | grep "Hello Zero Trust" +} + +@test "examples: roundtrip Z-TDF with EC wrapped KAO" { + # TODO: add subject mapping here to remove reliance on `provision fixtures` + echo "[INFO] create a tdf3 format file" + run go run ./examples encrypt -o sensitive-with-ec.txt.tdf --autoconfigure=false -a "ec:secp256r1" "Hello EC wrappers!" + echo "[INFO] echoing output; if successful, this is just the manifest" + echo "$output" + + echo "[INFO] Validate the manifest lists the expected kid in its KAO" + kaotype=$(jq -r '.encryptionInformation.keyAccess[0].type' <<<"${output}") + echo "$kaotype" + [ "$kaotype" = ec-wrapped ] + + echo "[INFO] decrypting..." + run go run ./examples decrypt sensitive-with-ec.txt.tdf + echo "$output" + printf '%s\n' "$output" | grep "Hello EC wrappers!" + + echo "[INFO] decrypting with EC..." + run go run ./examples decrypt -a 'ec:secp256r1' sensitive-with-ec.txt.tdf + echo "$output" + printf '%s\n' "$output" | grep "Hello EC wrappers!" } @test "examples: roundtrip Z-TDF with extra unnecessary, invalid kas" { From dbee5b4f8687f0d91abdef9cbad507ea09e0a95e Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Fri, 7 Feb 2025 17:00:33 -0500 Subject: [PATCH 06/18] flag conflict fix --- examples/cmd/decrypt.go | 2 +- examples/cmd/encrypt.go | 2 +- test/tdf-roundtrips.bats | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/cmd/decrypt.go b/examples/cmd/decrypt.go index 76016eb13..4160ba9a4 100644 --- a/examples/cmd/decrypt.go +++ b/examples/cmd/decrypt.go @@ -20,7 +20,7 @@ func init() { RunE: decrypt, Args: cobra.MinimumNArgs(1), } - decryptCmd.Flags().StringVarP(&alg, "rewrap-encapsulation-algorithm", "a", "rsa:2048", "Key wrap response algorithm algorithm:parameters") + decryptCmd.Flags().StringVarP(&alg, "rewrap-encapsulation-algorithm", "A", "rsa:2048", "Key wrap response algorithm algorithm:parameters") ExamplesCmd.AddCommand(decryptCmd) } diff --git a/examples/cmd/encrypt.go b/examples/cmd/encrypt.go index d8ed11e7b..94c6a6883 100644 --- a/examples/cmd/encrypt.go +++ b/examples/cmd/encrypt.go @@ -38,7 +38,7 @@ func init() { encryptCmd.Flags().BoolVar(&noKIDInKAO, "no-kid-in-kao", false, "[deprecated] Disable storing key identifiers in TDF KAOs") encryptCmd.Flags().BoolVar(&noKIDInNano, "no-kid-in-nano", true, "Disable storing key identifiers in nanoTDF KAS ResourceLocator") encryptCmd.Flags().StringVarP(&outputName, "output", "o", "sensitive.txt.tdf", "name or path of output file; - for stdout") - encryptCmd.Flags().StringVarP(&alg, "key-encapsulation-algorithm", "a", "rsa:2048", "Key wrap algorithm algorithm:parameters") + encryptCmd.Flags().StringVarP(&alg, "key-encapsulation-algorithm", "A", "rsa:2048", "Key wrap algorithm algorithm:parameters") encryptCmd.Flags().IntVarP(&collection, "collection", "c", 0, "number of nano's to create for collection. If collection >0 (default) then output will be _") ExamplesCmd.AddCommand(&encryptCmd) diff --git a/test/tdf-roundtrips.bats b/test/tdf-roundtrips.bats index b46084640..5081f0baf 100755 --- a/test/tdf-roundtrips.bats +++ b/test/tdf-roundtrips.bats @@ -27,7 +27,7 @@ printf '%s\n' "$output" | grep "Hello Zero Trust" echo "[INFO] decrypting with EC..." - run go run ./examples decrypt -a 'ec:secp256r1' sensitive.txt.tdf + run go run ./examples decrypt -A 'ec:secp256r1' sensitive.txt.tdf echo "$output" printf '%s\n' "$output" | grep "Hello Zero Trust" } @@ -35,7 +35,7 @@ @test "examples: roundtrip Z-TDF with EC wrapped KAO" { # TODO: add subject mapping here to remove reliance on `provision fixtures` echo "[INFO] create a tdf3 format file" - run go run ./examples encrypt -o sensitive-with-ec.txt.tdf --autoconfigure=false -a "ec:secp256r1" "Hello EC wrappers!" + run go run ./examples encrypt -o sensitive-with-ec.txt.tdf --autoconfigure=false -A "ec:secp256r1" "Hello EC wrappers!" echo "[INFO] echoing output; if successful, this is just the manifest" echo "$output" @@ -50,7 +50,7 @@ printf '%s\n' "$output" | grep "Hello EC wrappers!" echo "[INFO] decrypting with EC..." - run go run ./examples decrypt -a 'ec:secp256r1' sensitive-with-ec.txt.tdf + run go run ./examples decrypt -A 'ec:secp256r1' sensitive-with-ec.txt.tdf echo "$output" printf '%s\n' "$output" | grep "Hello EC wrappers!" } From 8837357de37c4b363b558a62a7679a1898d26ad8 Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Mon, 10 Feb 2025 12:04:04 -0500 Subject: [PATCH 07/18] feat(core): Adds kas feature flag for ec-wrapped --- service/kas/access/provider.go | 5 +++++ service/kas/access/rewrap.go | 11 +++++++++++ test/tdf-roundtrips.bats | 1 + 3 files changed, 17 insertions(+) diff --git a/service/kas/access/provider.go b/service/kas/access/provider.go index c417a72a6..975a0cd8a 100644 --- a/service/kas/access/provider.go +++ b/service/kas/access/provider.go @@ -36,6 +36,11 @@ type KASConfig struct { ECCertID string `mapstructure:"eccertid" json:"eccertid"` // Deprecated RSACertID string `mapstructure:"rsacertid" json:"rsacertid"` + + // Enables experimental EC rewrap support in TDFs + // Enabling is required to parse KAOs with the `ec-wrapped` type, + // and (currently) also enables responding with ECIES encrypted responses. + ECWrappedEnabled bool `mapstructure:"ecwrappedenabled" json:"ecwrappedenabled"` } // Specifies the preferred/default key for a given algorithm type. diff --git a/service/kas/access/rewrap.go b/service/kas/access/rewrap.go index 29b4a7992..46bdeac8d 100644 --- a/service/kas/access/rewrap.go +++ b/service/kas/access/rewrap.go @@ -440,6 +440,12 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned switch kao.GetKeyAccessObject().GetKeyType() { case "ec-wrapped": + if !p.KASConfig.ECWrappedEnabled { + p.Logger.WarnContext(ctx, "ec-wrapped not enabled") + failedKAORewrap(results, kao, err400("bad request")) + continue + } + // Get the ephemeral public key in PEM format ephemeralPubKeyPEM := kao.GetKeyAccessObject().GetEphemeralPublicKey() @@ -583,6 +589,11 @@ func (p *Provider) tdf3Rewrap(ctx context.Context, requests []*kaspb.UnsignedRew failAllKaos(requests, results, err400("invalid request")) return "", results } + if !p.KASConfig.ECWrappedEnabled { + p.Logger.ErrorContext(ctx, "ec rewrap not enabled") + failAllKaos(requests, results, err400("invalid request")) + return "", results + } } for _, pdpAccess := range pdpAccessResults { diff --git a/test/tdf-roundtrips.bats b/test/tdf-roundtrips.bats index 5081f0baf..6f89f8957 100755 --- a/test/tdf-roundtrips.bats +++ b/test/tdf-roundtrips.bats @@ -238,6 +238,7 @@ logger: services: kas: enabled: true + ecwrappedenabled: true keyring: - kid: ${ec_current_key} alg: ec:secp256r1 From 2a9a741f68f8b957e1af0aab49566bf5bd69e328 Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Mon, 10 Feb 2025 14:42:25 -0500 Subject: [PATCH 08/18] Update asym_encryption.go --- lib/ocrypto/asym_encryption.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ocrypto/asym_encryption.go b/lib/ocrypto/asym_encryption.go index 1799e3195..f8cfd68b9 100644 --- a/lib/ocrypto/asym_encryption.go +++ b/lib/ocrypto/asym_encryption.go @@ -231,5 +231,5 @@ func (e ECEncryptor) Encrypt(data []byte) ([]byte, error) { // PublicKeyInPemFormat Returns public key in pem format. func (e ECEncryptor) PublicKeyInPemFormat() (string, error) { - return publicKeyInPemFormat(e.pub) + return publicKeyInPemFormat(e.ek.Public()) } From e1cd20fafa8ef0a764d4329a9cdf1afe3d323196 Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Mon, 10 Feb 2025 15:56:19 -0500 Subject: [PATCH 09/18] update actions --- test/start-additional-kas/action.yaml | 6 ++++++ test/start-up-with-containers/action.yaml | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/test/start-additional-kas/action.yaml b/test/start-additional-kas/action.yaml index 722ac9bbb..7078b593a 100644 --- a/test/start-additional-kas/action.yaml +++ b/test/start-additional-kas/action.yaml @@ -12,6 +12,11 @@ inputs: kas-name: required: true description: 'The name for the additional KAS' + ecc-enabled: + default: false + description: 'Whether to enable ECC wrapping for TDFs' + required: false + type: boolean runs: using: 'composite' @@ -23,6 +28,7 @@ runs: opentdf-${{ inputs.kas-name }}.yaml yq e ' (.server.port = ${{ inputs.kas-port }}) | (.mode = ["kas"]) + | (.services.kas.ecwrappedenabled = ${{ inputs.ecc-enabled }}) | (.sdk_config = {"client_id":"opentdf","client_secret":"secret","core":{"endpoint":"http://localhost:8080","plaintext":true}}) ' && .github/scripts/watch.sh opentdf-${{ inputs.kas-name }}.yaml ./opentdf --config-file ./opentdf-${{ inputs.kas-name }}.yaml start diff --git a/test/start-up-with-containers/action.yaml b/test/start-up-with-containers/action.yaml index 6130bb842..cf1608039 100644 --- a/test/start-up-with-containers/action.yaml +++ b/test/start-up-with-containers/action.yaml @@ -11,6 +11,11 @@ inputs: required: false description: A JSON array containing extra keys for the KAS to load. Each object should have 'kid', 'alg', 'private', and 'cert' fields. default: '[]' + ecc-enabled: + default: false + description: 'Whether to enable ECC wrapping for TDFs' + required: false + type: boolean outputs: platform-working-dir: @@ -84,6 +89,10 @@ runs: opentdf.yaml yq e "${yq_command}" working-directory: otdf-test-platform + - name: Enable ECC wrapping for TDFs + if: ${{ inputs.ecc-enabled }} + run: | + yq e '.services.kas.ecwrappedenabled = true' -i opentdf.yaml - name: Trust the generated certs shell: bash run: | From b08cb678e3d7f15350673917a0ced9748c31b1fb Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Mon, 10 Feb 2025 15:59:27 -0500 Subject: [PATCH 10/18] rename --- test/start-additional-kas/action.yaml | 4 ++-- test/start-up-with-containers/action.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/start-additional-kas/action.yaml b/test/start-additional-kas/action.yaml index 7078b593a..f0c929c15 100644 --- a/test/start-additional-kas/action.yaml +++ b/test/start-additional-kas/action.yaml @@ -12,7 +12,7 @@ inputs: kas-name: required: true description: 'The name for the additional KAS' - ecc-enabled: + ec-wrapped-enabled: default: false description: 'Whether to enable ECC wrapping for TDFs' required: false @@ -28,7 +28,7 @@ runs: opentdf-${{ inputs.kas-name }}.yaml yq e ' (.server.port = ${{ inputs.kas-port }}) | (.mode = ["kas"]) - | (.services.kas.ecwrappedenabled = ${{ inputs.ecc-enabled }}) + | (.services.kas.ecwrappedenabled = ${{ inputs.ec-wrapped-enabled }}) | (.sdk_config = {"client_id":"opentdf","client_secret":"secret","core":{"endpoint":"http://localhost:8080","plaintext":true}}) ' && .github/scripts/watch.sh opentdf-${{ inputs.kas-name }}.yaml ./opentdf --config-file ./opentdf-${{ inputs.kas-name }}.yaml start diff --git a/test/start-up-with-containers/action.yaml b/test/start-up-with-containers/action.yaml index cf1608039..b17f62346 100644 --- a/test/start-up-with-containers/action.yaml +++ b/test/start-up-with-containers/action.yaml @@ -11,7 +11,7 @@ inputs: required: false description: A JSON array containing extra keys for the KAS to load. Each object should have 'kid', 'alg', 'private', and 'cert' fields. default: '[]' - ecc-enabled: + ec-wrapped-enabled: default: false description: 'Whether to enable ECC wrapping for TDFs' required: false @@ -90,7 +90,7 @@ runs: working-directory: otdf-test-platform - name: Enable ECC wrapping for TDFs - if: ${{ inputs.ecc-enabled }} + if: ${{ inputs.ec-wrapped-enabled }} run: | yq e '.services.kas.ecwrappedenabled = true' -i opentdf.yaml - name: Trust the generated certs From 43f50ccb1315830e4d258fcfb543e4162e61b77d Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Tue, 11 Feb 2025 10:01:58 -0500 Subject: [PATCH 11/18] s/ec-wrapped-enabled/ec-tdf-enabled/g --- service/kas/access/provider.go | 2 +- service/kas/access/rewrap.go | 4 ++-- test/start-additional-kas/action.yaml | 4 ++-- test/start-up-with-containers/action.yaml | 6 +++--- test/tdf-roundtrips.bats | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/service/kas/access/provider.go b/service/kas/access/provider.go index 975a0cd8a..aa30113dd 100644 --- a/service/kas/access/provider.go +++ b/service/kas/access/provider.go @@ -40,7 +40,7 @@ type KASConfig struct { // Enables experimental EC rewrap support in TDFs // Enabling is required to parse KAOs with the `ec-wrapped` type, // and (currently) also enables responding with ECIES encrypted responses. - ECWrappedEnabled bool `mapstructure:"ecwrappedenabled" json:"ecwrappedenabled"` + ECTDFEnabled bool `mapstructure:"ectdfenabled" json:"ectdfenabled"` } // Specifies the preferred/default key for a given algorithm type. diff --git a/service/kas/access/rewrap.go b/service/kas/access/rewrap.go index 46bdeac8d..31eb3da9a 100644 --- a/service/kas/access/rewrap.go +++ b/service/kas/access/rewrap.go @@ -440,7 +440,7 @@ func (p *Provider) verifyRewrapRequests(ctx context.Context, req *kaspb.Unsigned switch kao.GetKeyAccessObject().GetKeyType() { case "ec-wrapped": - if !p.KASConfig.ECWrappedEnabled { + if !p.KASConfig.ECTDFEnabled { p.Logger.WarnContext(ctx, "ec-wrapped not enabled") failedKAORewrap(results, kao, err400("bad request")) continue @@ -589,7 +589,7 @@ func (p *Provider) tdf3Rewrap(ctx context.Context, requests []*kaspb.UnsignedRew failAllKaos(requests, results, err400("invalid request")) return "", results } - if !p.KASConfig.ECWrappedEnabled { + if !p.KASConfig.ECTDFEnabled { p.Logger.ErrorContext(ctx, "ec rewrap not enabled") failAllKaos(requests, results, err400("invalid request")) return "", results diff --git a/test/start-additional-kas/action.yaml b/test/start-additional-kas/action.yaml index f0c929c15..f88d9a090 100644 --- a/test/start-additional-kas/action.yaml +++ b/test/start-additional-kas/action.yaml @@ -12,7 +12,7 @@ inputs: kas-name: required: true description: 'The name for the additional KAS' - ec-wrapped-enabled: + ec-tdf-enabled: default: false description: 'Whether to enable ECC wrapping for TDFs' required: false @@ -28,7 +28,7 @@ runs: opentdf-${{ inputs.kas-name }}.yaml yq e ' (.server.port = ${{ inputs.kas-port }}) | (.mode = ["kas"]) - | (.services.kas.ecwrappedenabled = ${{ inputs.ec-wrapped-enabled }}) + | (.services.kas.ectdfenabled = ${{ inputs.ec-tdf-enabled }}) | (.sdk_config = {"client_id":"opentdf","client_secret":"secret","core":{"endpoint":"http://localhost:8080","plaintext":true}}) ' && .github/scripts/watch.sh opentdf-${{ inputs.kas-name }}.yaml ./opentdf --config-file ./opentdf-${{ inputs.kas-name }}.yaml start diff --git a/test/start-up-with-containers/action.yaml b/test/start-up-with-containers/action.yaml index b17f62346..3ea706be2 100644 --- a/test/start-up-with-containers/action.yaml +++ b/test/start-up-with-containers/action.yaml @@ -11,7 +11,7 @@ inputs: required: false description: A JSON array containing extra keys for the KAS to load. Each object should have 'kid', 'alg', 'private', and 'cert' fields. default: '[]' - ec-wrapped-enabled: + ec-tdf-enabled: default: false description: 'Whether to enable ECC wrapping for TDFs' required: false @@ -90,9 +90,9 @@ runs: working-directory: otdf-test-platform - name: Enable ECC wrapping for TDFs - if: ${{ inputs.ec-wrapped-enabled }} + if: ${{ inputs.ec-tdf-enabled }} run: | - yq e '.services.kas.ecwrappedenabled = true' -i opentdf.yaml + yq e '.services.kas.ectdfenabled = true' -i opentdf.yaml - name: Trust the generated certs shell: bash run: | diff --git a/test/tdf-roundtrips.bats b/test/tdf-roundtrips.bats index 6f89f8957..28ee13aee 100755 --- a/test/tdf-roundtrips.bats +++ b/test/tdf-roundtrips.bats @@ -238,7 +238,7 @@ logger: services: kas: enabled: true - ecwrappedenabled: true + ectdfenabled: true keyring: - kid: ${ec_current_key} alg: ec:secp256r1 From ba7114503d90877f00e9b7440a53c1a855c28c17 Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Tue, 11 Feb 2025 10:04:04 -0500 Subject: [PATCH 12/18] underscored --- service/kas/access/provider.go | 2 +- test/start-additional-kas/action.yaml | 2 +- test/start-up-with-containers/action.yaml | 2 +- test/tdf-roundtrips.bats | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/service/kas/access/provider.go b/service/kas/access/provider.go index aa30113dd..c10f71ddd 100644 --- a/service/kas/access/provider.go +++ b/service/kas/access/provider.go @@ -40,7 +40,7 @@ type KASConfig struct { // Enables experimental EC rewrap support in TDFs // Enabling is required to parse KAOs with the `ec-wrapped` type, // and (currently) also enables responding with ECIES encrypted responses. - ECTDFEnabled bool `mapstructure:"ectdfenabled" json:"ectdfenabled"` + ECTDFEnabled bool `mapstructure:"ec_tdf_enabled" json:"ec_tdf_enabled"` } // Specifies the preferred/default key for a given algorithm type. diff --git a/test/start-additional-kas/action.yaml b/test/start-additional-kas/action.yaml index f88d9a090..120a88a3e 100644 --- a/test/start-additional-kas/action.yaml +++ b/test/start-additional-kas/action.yaml @@ -28,7 +28,7 @@ runs: opentdf-${{ inputs.kas-name }}.yaml yq e ' (.server.port = ${{ inputs.kas-port }}) | (.mode = ["kas"]) - | (.services.kas.ectdfenabled = ${{ inputs.ec-tdf-enabled }}) + | (.services.kas.ec_tdf_enabled = ${{ inputs.ec-tdf-enabled }}) | (.sdk_config = {"client_id":"opentdf","client_secret":"secret","core":{"endpoint":"http://localhost:8080","plaintext":true}}) ' && .github/scripts/watch.sh opentdf-${{ inputs.kas-name }}.yaml ./opentdf --config-file ./opentdf-${{ inputs.kas-name }}.yaml start diff --git a/test/start-up-with-containers/action.yaml b/test/start-up-with-containers/action.yaml index 3ea706be2..eb24dd95e 100644 --- a/test/start-up-with-containers/action.yaml +++ b/test/start-up-with-containers/action.yaml @@ -92,7 +92,7 @@ runs: - name: Enable ECC wrapping for TDFs if: ${{ inputs.ec-tdf-enabled }} run: | - yq e '.services.kas.ectdfenabled = true' -i opentdf.yaml + yq e '.services.kas.ec_tdf_enabled = true' -i opentdf.yaml - name: Trust the generated certs shell: bash run: | diff --git a/test/tdf-roundtrips.bats b/test/tdf-roundtrips.bats index 28ee13aee..884e41f90 100755 --- a/test/tdf-roundtrips.bats +++ b/test/tdf-roundtrips.bats @@ -238,7 +238,7 @@ logger: services: kas: enabled: true - ectdfenabled: true + ec_tdf_enabled: true keyring: - kid: ${ec_current_key} alg: ec:secp256r1 From 31f34bd4cb42f35126313bef78fc88ce1f97d1fd Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Tue, 11 Feb 2025 10:34:26 -0500 Subject: [PATCH 13/18] consolidate on single enum for key type --- examples/cmd/decrypt.go | 6 +-- examples/cmd/encrypt.go | 31 ++++----------- lib/ocrypto/ec_key_pair.go | 49 ++++++++++++++++++++++-- lib/ocrypto/rsa_key_pair.go | 2 +- sdk/kas_client.go | 2 +- sdk/tdf.go | 19 ++++------ sdk/tdf_config.go | 76 +++++++++++++------------------------ 7 files changed, 91 insertions(+), 94 deletions(-) diff --git a/examples/cmd/decrypt.go b/examples/cmd/decrypt.go index 4160ba9a4..f8040cc5a 100644 --- a/examples/cmd/decrypt.go +++ b/examples/cmd/decrypt.go @@ -90,11 +90,7 @@ func decrypt(cmd *cobra.Command, args []string) error { if err != nil { return err } - bits, err := bitsForKeyType(alg) - if err != nil { - return err - } - opts = append(opts, sdk.WithSessionKeyType(kt, bits)) + opts = append(opts, sdk.WithSessionKeyType(kt)) } tdfreader, err := client.LoadTDF(file, opts...) if err != nil { diff --git a/examples/cmd/encrypt.go b/examples/cmd/encrypt.go index 94c6a6883..7287facd5 100644 --- a/examples/cmd/encrypt.go +++ b/examples/cmd/encrypt.go @@ -103,7 +103,7 @@ func encrypt(cmd *cobra.Command, args []string) error { opts := []sdk.TDFOption{sdk.WithDataAttributes(dataAttributes...)} if !autoconfigure { opts = append(opts, sdk.WithAutoconfigure(autoconfigure)) - opts = append(opts, sdk.WithKeyType(ocrypto.ECKey, 256)) + opts = append(opts, sdk.WithWrappingKeyAlg(ocrypto.EC256Key)) opts = append(opts, sdk.WithKasInformation( sdk.KASInfo{ // examples assume insecure http @@ -116,11 +116,7 @@ func encrypt(cmd *cobra.Command, args []string) error { if err != nil { return err } - bits, err := bitsForKeyType(alg) - if err != nil { - return err - } - opts = append(opts, sdk.WithKeyType(kt, bits)) + opts = append(opts, sdk.WithWrappingKeyAlg(kt)) } tdf, err := client.CreateTDF(out, in, opts...) if err != nil { @@ -169,26 +165,15 @@ func encrypt(cmd *cobra.Command, args []string) error { return nil } -func bitsForKeyType(alg string) (int, error) { - switch alg { - case "rsa:2048": - return 2048, nil - case "ec:secp256r1": - return 256, nil - default: - return 0, fmt.Errorf("unsupported key type [%s]", alg) - } -} - func keyTypeForKeyType(alg string) (ocrypto.KeyType, error) { - switch { - case strings.HasPrefix(alg, "rsa:"): - return ocrypto.RSAKey, nil - case strings.HasPrefix(alg, "ec:"): - return ocrypto.ECKey, nil + switch alg { + case string(ocrypto.RSA2048Key): + return ocrypto.RSA2048Key, nil + case string(ocrypto.EC256Key): + return ocrypto.EC256Key, nil default: // do not submit add ocrypto.UnknownKey - return ocrypto.RSAKey, fmt.Errorf("unsupported key type [%s]", alg) + return ocrypto.RSA2048Key, fmt.Errorf("unsupported key type [%s]", alg) } } diff --git a/lib/ocrypto/ec_key_pair.go b/lib/ocrypto/ec_key_pair.go index 144ade78c..305824cc0 100644 --- a/lib/ocrypto/ec_key_pair.go +++ b/lib/ocrypto/ec_key_pair.go @@ -19,11 +19,13 @@ import ( type ECCMode uint8 -type KeyType int +type KeyType string const ( - RSAKey KeyType = iota - ECKey + RSA2048Key KeyType = "rsa:2048" + EC256Key KeyType = "ec:secp256r1" + EC384Key KeyType = "ec:secp384r1" + EC521Key KeyType = "ec:secp521r1" ) const ( @@ -49,6 +51,23 @@ type ECKeyPair struct { PrivateKey *ecdsa.PrivateKey } +func IsECKeyType(kt KeyType) bool { + switch kt { + case EC256Key, EC384Key, EC521Key: + return true + } + return false +} + +func IsRSAKeyType(kt KeyType) bool { + switch kt { + case RSA2048Key: + return true + default: + return false + } +} + // GetECCurveFromECCMode return elliptic curve from ecc mode func GetECCurveFromECCMode(mode ECCMode) (elliptic.Curve, error) { var c elliptic.Curve @@ -98,6 +117,28 @@ func ECSizeToMode(size int) (ECCMode, error) { } } +func ECKeyTypeToMode(kt KeyType) (ECCMode, error) { + switch kt { + case EC256Key: + return ECCModeSecp256r1, nil + case EC384Key: + return ECCModeSecp384r1, nil + case EC521Key: + return ECCModeSecp521r1, nil + default: + return 0, fmt.Errorf("unsupported type: %v", kt) + } +} + +func RSAKeyTypeToBits(kt KeyType) (int, error) { + switch kt { + case RSA2048Key: + return 2048, nil + default: + return 0, fmt.Errorf("unsupported type: %v", kt) + } +} + // NewECKeyPair Generates an EC key pair of the given bit size. func NewECKeyPair(mode ECCMode) (ECKeyPair, error) { var c elliptic.Curve @@ -426,5 +467,5 @@ func GetECKeySize(pemData []byte) (int, error) { // GetKeyType returns the key type (ECKey) func (keyPair ECKeyPair) GetKeyType() KeyType { - return ECKey + return EC256Key } diff --git a/lib/ocrypto/rsa_key_pair.go b/lib/ocrypto/rsa_key_pair.go index 068cb66c4..914eb8f80 100644 --- a/lib/ocrypto/rsa_key_pair.go +++ b/lib/ocrypto/rsa_key_pair.go @@ -79,5 +79,5 @@ func (keyPair RsaKeyPair) KeySize() (int, error) { // GetKeyType returns the key type (RSAKey) func (keyPair RsaKeyPair) GetKeyType() KeyType { - return RSAKey + return RSA2048Key } diff --git a/sdk/kas_client.go b/sdk/kas_client.go index 642bcca47..8093d5055 100644 --- a/sdk/kas_client.go +++ b/sdk/kas_client.go @@ -145,7 +145,7 @@ func (k *KASClient) unwrap(ctx context.Context, requests ...*kas.UnsignedRewrapR return nil, fmt.Errorf("error making rewrap request to kas: %w", err) } - if k.sessionKey.GetKeyType() == ocrypto.ECKey { + if ocrypto.IsECKeyType(k.sessionKey.GetKeyType()) { return k.handleECKeyResponse(response) } return k.handleRSAKeyResponse(response) diff --git a/sdk/tdf.go b/sdk/tdf.go index 16666bdd0..08be44591 100644 --- a/sdk/tdf.go +++ b/sdk/tdf.go @@ -418,13 +418,7 @@ func (s SDK) prepareManifest(ctx context.Context, t *TDFObject, tdfConfig TDFCon conjunction := make(map[string][]KASInfo) var splitIDs []string - var keyAlgorithm string - if tdfConfig.keyType == ocrypto.RSAKey { - keyAlgorithm = fmt.Sprintf("rsa:%d", tdfConfig.keySize) - } else { - mode, _ := ocrypto.ECSizeToMode(tdfConfig.keySize) - keyAlgorithm = mode.String() - } + keyAlgorithm := string(tdfConfig.keyType) for _, splitInfo := range tdfConfig.splitPlan { // Public key was passed in with kasInfoList @@ -541,8 +535,12 @@ func createKeyAccess(tdfConfig TDFConfig, kasInfo KASInfo, symKey []byte, policy SchemaVersion: keyAccessSchemaVersion, } - if tdfConfig.keyType == ocrypto.ECKey { - wrappedKeyInfo, err := generateWrapKeyWithEC(tdfConfig.keySize, kasInfo.PublicKey, symKey) + if ocrypto.IsECKeyType(tdfConfig.keyType) { + mode, err := ocrypto.ECKeyTypeToMode(tdfConfig.keyType) + if err != nil { + return KeyAccess{}, err + } + wrappedKeyInfo, err := generateWrapKeyWithEC(mode, kasInfo.PublicKey, symKey) if err != nil { return KeyAccess{}, err } @@ -560,8 +558,7 @@ func createKeyAccess(tdfConfig TDFConfig, kasInfo KASInfo, symKey []byte, policy return keyAccess, nil } -func generateWrapKeyWithEC(keySize int, kasPublicKey string, symKey []byte) (ecKeyWrappedKeyInfo, error) { - mode, _ := ocrypto.ECSizeToMode(keySize) +func generateWrapKeyWithEC(mode ocrypto.ECCMode, kasPublicKey string, symKey []byte) (ecKeyWrappedKeyInfo, error) { ecKeyPair, err := ocrypto.NewECKeyPair(mode) if err != nil { return ecKeyWrappedKeyInfo{}, fmt.Errorf("ocrypto.NewECKeyPair failed:%w", err) diff --git a/sdk/tdf_config.go b/sdk/tdf_config.go index 295e1c58d..f35921237 100644 --- a/sdk/tdf_config.go +++ b/sdk/tdf_config.go @@ -68,7 +68,6 @@ type TDFConfig struct { kasInfoList []KASInfo splitPlan []keySplitStep keyType ocrypto.KeyType - keySize int // For RSA this is key size, for EC this is curve size } func newTDFConfig(opt ...TDFOption) (*TDFConfig, error) { @@ -79,8 +78,7 @@ func newTDFConfig(opt ...TDFOption) (*TDFConfig, error) { tdfFormat: JSONFormat, integrityAlgorithm: HS256, segmentIntegrityAlgorithm: GMAC, - keyType: ocrypto.RSAKey, // default to RSA - keySize: DefaultRSAKeySize, // default size + keyType: ocrypto.RSA2048Key, // default to RSA } for _, o := range opt { @@ -90,7 +88,7 @@ func newTDFConfig(opt ...TDFOption) (*TDFConfig, error) { } } - publicKey, privateKey, err := generateKeyPair(c.keyType, c.keySize) + publicKey, privateKey, err := generateKeyPair(c.keyType) if err != nil { return nil, err } @@ -101,11 +99,23 @@ func newTDFConfig(opt ...TDFOption) (*TDFConfig, error) { return c, nil } -func generateKeyPair(keyType ocrypto.KeyType, keySize int) (string, string, error) { - if keyType == ocrypto.RSAKey { - return generateRSAKeyPair(keySize) +func generateKeyPair(keyType ocrypto.KeyType) (string, string, error) { + switch keyType { + case ocrypto.RSA2048Key: + ks, err := ocrypto.RSAKeyTypeToBits(keyType) + if err != nil { + return "", "", err + } + return generateRSAKeyPair(ks) + case ocrypto.EC256Key, ocrypto.EC384Key, ocrypto.EC521Key: + mode, err := ocrypto.ECKeyTypeToMode(keyType) + if err != nil { + return "", "", err + } + return generateECKeyPair(mode) + default: + return "", "", fmt.Errorf("unsupported key type") } - return generateECKeyPair(keySize) } func generateRSAKeyPair(keySize int) (string, string, error) { @@ -124,11 +134,7 @@ func generateRSAKeyPair(keySize int) (string, string, error) { return publicKey, privateKey, nil } -func generateECKeyPair(keySize int) (string, string, error) { - mode, err := ocrypto.ECSizeToMode(keySize) - if err != nil { - return "", "", err - } +func generateECKeyPair(mode ocrypto.ECCMode) (string, string, error) { ecKeyPair, err := ocrypto.NewECKeyPair(mode) if err != nil { return "", "", fmt.Errorf("ocrypto.NewECKeyPair failed: %w", err) @@ -255,26 +261,12 @@ func WithAutoconfigure(enable bool) TDFOption { } } -func WithKeyType(keyType ocrypto.KeyType, size int) TDFOption { +func WithWrappingKeyAlg(keyType ocrypto.KeyType) TDFOption { return func(c *TDFConfig) error { - switch keyType { - case ocrypto.RSAKey: - if size < 2048 || size > 4096 { - return fmt.Errorf("invalid RSA key size: %d, must be between 2048 and 4096", size) - } - case ocrypto.ECKey: - switch size { - case ECKeySize256, ECKeySize384, ECKeySize521: - // valid sizes - default: - return fmt.Errorf("invalid EC curve size: %d, must be one of 256, 384, or 521", size) - } - default: - return fmt.Errorf("unsupported key type") + if c.keyType == "" { + return fmt.Errorf("key type missing") } - c.keyType = keyType - c.keySize = size return nil } } @@ -305,7 +297,7 @@ func newTDFReaderConfig(opt ...TDFReaderOption) (*TDFReaderConfig, error) { var err error c := &TDFReaderConfig{ disableAssertionVerification: false, - keyType: ocrypto.RSAKey, + keyType: ocrypto.RSA2048Key, keySize: DefaultRSAKeySize, } @@ -316,7 +308,7 @@ func newTDFReaderConfig(opt ...TDFReaderOption) (*TDFReaderConfig, error) { } } - if c.keyType == ocrypto.RSAKey { + if c.keyType == ocrypto.RSA2048Key { c.kasSessionKey, err = ocrypto.NewRSAKeyPair(c.keySize) if err != nil { return nil, fmt.Errorf("failed to create RSA key pair: %w", err) @@ -357,26 +349,12 @@ func WithDisableAssertionVerification(disable bool) TDFReaderOption { } } -func WithSessionKeyType(keyType ocrypto.KeyType, size int) TDFReaderOption { +func WithSessionKeyType(keyType ocrypto.KeyType) TDFReaderOption { return func(c *TDFReaderConfig) error { - switch keyType { - case ocrypto.RSAKey: - if size < 2048 || size > 4096 { - return fmt.Errorf("invalid RSA key size: %d, must be between 2048 and 4096", size) - } - case ocrypto.ECKey: - switch size { - case ECKeySize256, ECKeySize384, ECKeySize521: - // valid sizes - default: - return fmt.Errorf("invalid EC curve size: %d, must be one of 256, 384, or 521", size) - } - default: - return fmt.Errorf("unsupported key type") + if c.keyType == "" { + return fmt.Errorf("key type missing") } - c.keyType = keyType - c.keySize = size return nil } } From 02efe01a45ad736be94011f81b2ca8c05a547aa9 Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Tue, 11 Feb 2025 10:48:05 -0500 Subject: [PATCH 14/18] lint suggestions --- lib/ocrypto/ec_key_pair.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/ocrypto/ec_key_pair.go b/lib/ocrypto/ec_key_pair.go index 305824cc0..7f2412a87 100644 --- a/lib/ocrypto/ec_key_pair.go +++ b/lib/ocrypto/ec_key_pair.go @@ -39,6 +39,7 @@ const ( ECCurveP256Size = 256 ECCurveP384Size = 384 ECCurveP521Size = 521 + RSA2048Size = 2048 ) type KeyPair interface { @@ -52,15 +53,16 @@ type ECKeyPair struct { } func IsECKeyType(kt KeyType) bool { - switch kt { + switch kt { //nolint:exhaustive // only handle ec types case EC256Key, EC384Key, EC521Key: return true + default: + return false } - return false } func IsRSAKeyType(kt KeyType) bool { - switch kt { + switch kt { //nolint:exhaustive // only handle rsa types case RSA2048Key: return true default: @@ -118,7 +120,7 @@ func ECSizeToMode(size int) (ECCMode, error) { } func ECKeyTypeToMode(kt KeyType) (ECCMode, error) { - switch kt { + switch kt { //nolint:exhaustive // only handle ec types case EC256Key: return ECCModeSecp256r1, nil case EC384Key: @@ -131,9 +133,9 @@ func ECKeyTypeToMode(kt KeyType) (ECCMode, error) { } func RSAKeyTypeToBits(kt KeyType) (int, error) { - switch kt { + switch kt { //nolint:exhaustive // only handle rsa types case RSA2048Key: - return 2048, nil + return RSA2048Size, nil default: return 0, fmt.Errorf("unsupported type: %v", kt) } From 41efe6815e2936e60732a48b99f83617fe867bcf Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Tue, 11 Feb 2025 11:11:47 -0500 Subject: [PATCH 15/18] consolidate --- lib/ocrypto/ec_key_pair.go | 19 ++++++++++++ sdk/tdf_config.go | 63 +++++--------------------------------- 2 files changed, 26 insertions(+), 56 deletions(-) diff --git a/lib/ocrypto/ec_key_pair.go b/lib/ocrypto/ec_key_pair.go index 7f2412a87..504c13408 100644 --- a/lib/ocrypto/ec_key_pair.go +++ b/lib/ocrypto/ec_key_pair.go @@ -48,6 +48,25 @@ type KeyPair interface { GetKeyType() KeyType } +func NewKeyPair(kt KeyType) (KeyPair, error) { + switch kt { + case RSA2048Key: + bits, err := RSAKeyTypeToBits(kt) + if err != nil { + return nil, err + } + return NewRSAKeyPair(bits) + case EC256Key, EC384Key, EC521Key: + mode, err := ECKeyTypeToMode(kt) + if err != nil { + return nil, err + } + return NewECKeyPair(mode) + default: + return nil, fmt.Errorf("unsupported key type: %v", kt) + } +} + type ECKeyPair struct { PrivateKey *ecdsa.PrivateKey } diff --git a/sdk/tdf_config.go b/sdk/tdf_config.go index f35921237..0159b7a4b 100644 --- a/sdk/tdf_config.go +++ b/sdk/tdf_config.go @@ -99,51 +99,16 @@ func newTDFConfig(opt ...TDFOption) (*TDFConfig, error) { return c, nil } -func generateKeyPair(keyType ocrypto.KeyType) (string, string, error) { - switch keyType { - case ocrypto.RSA2048Key: - ks, err := ocrypto.RSAKeyTypeToBits(keyType) - if err != nil { - return "", "", err - } - return generateRSAKeyPair(ks) - case ocrypto.EC256Key, ocrypto.EC384Key, ocrypto.EC521Key: - mode, err := ocrypto.ECKeyTypeToMode(keyType) - if err != nil { - return "", "", err - } - return generateECKeyPair(mode) - default: - return "", "", fmt.Errorf("unsupported key type") - } -} - -func generateRSAKeyPair(keySize int) (string, string, error) { - rsaKeyPair, err := ocrypto.NewRSAKeyPair(keySize) +func generateKeyPair(kt ocrypto.KeyType) (string, string, error) { + keyPair, err := ocrypto.NewKeyPair(kt) if err != nil { return "", "", fmt.Errorf("ocrypto.NewRSAKeyPair failed: %w", err) } - publicKey, err := rsaKeyPair.PublicKeyInPemFormat() - if err != nil { - return "", "", fmt.Errorf("ocrypto.PublicKeyInPemFormat failed: %w", err) - } - privateKey, err := rsaKeyPair.PrivateKeyInPemFormat() - if err != nil { - return "", "", fmt.Errorf("ocrypto.PrivateKeyInPemFormat failed: %w", err) - } - return publicKey, privateKey, nil -} - -func generateECKeyPair(mode ocrypto.ECCMode) (string, string, error) { - ecKeyPair, err := ocrypto.NewECKeyPair(mode) - if err != nil { - return "", "", fmt.Errorf("ocrypto.NewECKeyPair failed: %w", err) - } - publicKey, err := ecKeyPair.PublicKeyInPemFormat() + publicKey, err := keyPair.PublicKeyInPemFormat() if err != nil { return "", "", fmt.Errorf("ocrypto.PublicKeyInPemFormat failed: %w", err) } - privateKey, err := ecKeyPair.PrivateKeyInPemFormat() + privateKey, err := keyPair.PrivateKeyInPemFormat() if err != nil { return "", "", fmt.Errorf("ocrypto.PrivateKeyInPemFormat failed: %w", err) } @@ -290,7 +255,6 @@ type TDFReaderConfig struct { schemaValidationIntensity SchemaValidationIntensity kasSessionKey ocrypto.KeyPair keyType ocrypto.KeyType - keySize int // For RSA this is key size, for EC this is curve size } func newTDFReaderConfig(opt ...TDFReaderOption) (*TDFReaderConfig, error) { @@ -298,7 +262,6 @@ func newTDFReaderConfig(opt ...TDFReaderOption) (*TDFReaderConfig, error) { c := &TDFReaderConfig{ disableAssertionVerification: false, keyType: ocrypto.RSA2048Key, - keySize: DefaultRSAKeySize, } for _, o := range opt { @@ -308,21 +271,9 @@ func newTDFReaderConfig(opt ...TDFReaderOption) (*TDFReaderConfig, error) { } } - if c.keyType == ocrypto.RSA2048Key { - c.kasSessionKey, err = ocrypto.NewRSAKeyPair(c.keySize) - if err != nil { - return nil, fmt.Errorf("failed to create RSA key pair: %w", err) - } - } else { - var eccMode ocrypto.ECCMode - eccMode, err = ocrypto.ECSizeToMode(c.keySize) - if err != nil { - return nil, err - } - c.kasSessionKey, err = ocrypto.NewECKeyPair(eccMode) - if err != nil { - return nil, fmt.Errorf("failed to create EC key pair: %w", err) - } + c.kasSessionKey, err = ocrypto.NewKeyPair(c.keyType) + if err != nil { + return nil, fmt.Errorf("failed to create RSA key pair: %w", err) } return c, nil From efc5a0613d2c110fb99e1628b5f6ee3953835fb7 Mon Sep 17 00:00:00 2001 From: sujan kota Date: Mon, 17 Feb 2025 13:42:52 -0500 Subject: [PATCH 16/18] Update unit test to handle ec-wrapped --- sdk/tdf_test.go | 196 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 154 insertions(+), 42 deletions(-) diff --git a/sdk/tdf_test.go b/sdk/tdf_test.go index ed84c7f41..5d95e6245 100644 --- a/sdk/tdf_test.go +++ b/sdk/tdf_test.go @@ -4,10 +4,13 @@ import ( "archive/zip" "bytes" "context" + "crypto/ecdsa" "crypto/rand" "crypto/rsa" "crypto/sha256" + "crypto/x509" "encoding/json" + "encoding/pem" "fmt" "io" "log/slog" @@ -204,6 +207,36 @@ Iuxu2zA7cGQNhhUi6MKr5cUWl6tBprAghzdwEH1cZQsBiV3ki7fCCiDURIJaTlNq uOnQR2c7Dix39LZQCiEfPSUnTAKJCyMpolky7Vq31PsPKk+gK19XftfH/Aul21vt ZwVW7fLwZ2SSmC9cOjSkzZw/eDwwIRNgo94OL4mw5cXSPOuMeO8Tugc6LO4v91SO yg== +-----END CERTIFICATE-----` + mockECPrivateKey1 = `-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgokydHKV9HW88nqn9 +2U2J1AqvcjrLDRCH6NBdNVqYLJOhRANCAASu1haeL6ckVfALALUlJKsehW8xomA9 +dcWMuYTECCukuGCklqiD0ofQAo+stVTRjen+zxM7C6MJaHdsbE4Pf088 +-----END PRIVATE KEY-----` + mockECPublicKey1 = `-----BEGIN CERTIFICATE----- +MIIBcTCCARegAwIBAgIURFydDqs4150ytI73sMRmya2fvTMwCgYIKoZIzj0EAwIw +DjEMMAoGA1UEAwwDa2FzMB4XDTI0MDYxMTAxNTU0N1oXDTI1MDYxMTAxNTU0N1ow +DjEMMAoGA1UEAwwDa2FzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAErtYWni+n +JFXwCwC1JSSrHoVvMaJgPXXFjLmExAgrpLhgpJaog9KH0AKPrLVU0Y3p/s8TOwuj +CWh3bGxOD39PPKNTMFEwHQYDVR0OBBYEFLg9mMeD25ZGvmjSYaunIPoeekzlMB8G +A1UdIwQYMBaAFLg9mMeD25ZGvmjSYaunIPoeekzlMA8GA1UdEwEB/wQFMAMBAf8w +CgYIKoZIzj0EAwIDSAAwRQIhALYXC70t37RlmIkRDlUTehiVEHpSQXz04wQ9Ivw+ +4h4hAiBNR3rD3KieiJaiJrCfM6TPJL7TIch7pAhMHdG6IPJMoQ== +-----END CERTIFICATE-----` + mockECPrivateKey2 = `-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgokydHKV9HW88nqn9 +2U2J1AqvcjrLDRCH6NBdNVqYLJOhRANCAASu1haeL6ckVfALALUlJKsehW8xomA9 +dcWMuYTECCukuGCklqiD0ofQAo+stVTRjen+zxM7C6MJaHdsbE4Pf088 +-----END PRIVATE KEY-----` + mockECPublicKey2 = `-----BEGIN CERTIFICATE----- +MIIBcTCCARegAwIBAgIURFydDqs4150ytI73sMRmya2fvTMwCgYIKoZIzj0EAwIw +DjEMMAoGA1UEAwwDa2FzMB4XDTI0MDYxMTAxNTU0N1oXDTI1MDYxMTAxNTU0N1ow +DjEMMAoGA1UEAwwDa2FzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAErtYWni+n +JFXwCwC1JSSrHoVvMaJgPXXFjLmExAgrpLhgpJaog9KH0AKPrLVU0Y3p/s8TOwuj +CWh3bGxOD39PPKNTMFEwHQYDVR0OBBYEFLg9mMeD25ZGvmjSYaunIPoeekzlMB8G +A1UdIwQYMBaAFLg9mMeD25ZGvmjSYaunIPoeekzlMA8GA1UdEwEB/wQFMAMBAf8w +CgYIKoZIzj0EAwIDSAAwRQIhALYXC70t37RlmIkRDlUTehiVEHpSQXz04wQ9Ivw+ +4h4hAiBNR3rD3KieiJaiJrCfM6TPJL7TIch7pAhMHdG6IPJMoQ== -----END CERTIFICATE-----` ) @@ -263,6 +296,11 @@ func TestTDF(t *testing.T) { } func (s *TDFSuite) Test_SimpleTDF() { + type TestConfig struct { + tdfOptions []TDFOption + tdfReadOptions []TDFReaderOption + } + metaData := []byte(`{"displayName" : "openTDF go sdk"}`) attributes := []string{ "https://example.com/attr/Classification/value/S", @@ -272,14 +310,37 @@ func (s *TDFSuite) Test_SimpleTDF() { expectedTdfSize := int64(2058) tdfFilename := "secure-text.tdf" plainText := "Virtru" - { - kasURLs := []KASInfo{ - { - URL: "https://a.kas/", - PublicKey: "", + + // add opts ...TDFOption to TestConfig + testConfigs := []TestConfig{ + { + tdfOptions: []TDFOption{ + WithKasInformation(KASInfo{ + URL: "https://a.kas/", + PublicKey: "", + }), + WithMetaData(string(metaData)), + WithDataAttributes(attributes...), }, - } + tdfReadOptions: []TDFReaderOption{}, + }, + { + tdfOptions: []TDFOption{ + WithKasInformation(KASInfo{ + URL: "https://d.kas/", + PublicKey: "", + }), + WithMetaData(string(metaData)), + WithDataAttributes(attributes...), + WithWrappingKeyAlg(ocrypto.EC256Key), + }, + tdfReadOptions: []TDFReaderOption{ + WithSessionKeyType(ocrypto.EC256Key), + }, + }, + } + for _, config := range testConfigs { inBuf := bytes.NewBufferString(plainText) bufReader := bytes.NewReader(inBuf.Bytes()) @@ -291,18 +352,12 @@ func (s *TDFSuite) Test_SimpleTDF() { s.Require().NoError(err) }(fileWriter) - tdfObj, err := s.sdk.CreateTDF(fileWriter, bufReader, - WithKasInformation(kasURLs...), - WithMetaData(string(metaData)), - WithDataAttributes(attributes...), - ) + tdfObj, err := s.sdk.CreateTDF(fileWriter, bufReader, config.tdfOptions...) s.Require().NoError(err) - s.InDelta(float64(expectedTdfSize), float64(tdfObj.size), 32.0) - } + s.InDelta(float64(expectedTdfSize), float64(tdfObj.size), 36.0) - // test meta data and build meta data - { + // test meta data and build meta data readSeeker, err := os.Open(tdfFilename) s.Require().NoError(err) @@ -311,28 +366,23 @@ func (s *TDFSuite) Test_SimpleTDF() { s.Require().NoError(err) }(readSeeker) - r, err := s.sdk.LoadTDF(readSeeker) - + r, err := s.sdk.LoadTDF(readSeeker, config.tdfReadOptions...) s.Require().NoError(err) unencryptedMetaData, err := r.UnencryptedMetadata() s.Require().NoError(err) - s.EqualValues(metaData, unencryptedMetaData) dataAttributes, err := r.DataAttributes() s.Require().NoError(err) - s.Equal(attributes, dataAttributes) payloadKey, err := r.UnsafePayloadKeyRetrieval() s.Require().NoError(err) s.Len(payloadKey, kKeySize) - } - // test reader - { - readSeeker, err := os.Open(tdfFilename) + // test reader + readSeeker, err = os.Open(tdfFilename) s.Require().NoError(err) defer func(readSeeker *os.File) { @@ -341,8 +391,7 @@ func (s *TDFSuite) Test_SimpleTDF() { }(readSeeker) buf := make([]byte, 8) - - r, err := s.sdk.LoadTDF(readSeeker) + r, err = s.sdk.LoadTDF(readSeeker, config.tdfReadOptions...) s.Require().NoError(err) offset := 2 @@ -353,9 +402,9 @@ func (s *TDFSuite) Test_SimpleTDF() { expectedPlainTxt := plainText[offset : offset+n] s.Equal(expectedPlainTxt, string(buf[:n])) - } - _ = os.Remove(tdfFilename) + _ = os.Remove(tdfFilename) + } } func (s *TDFSuite) Test_TDFWithAssertion() { @@ -1613,7 +1662,7 @@ func (s *TDFSuite) startBackend() { return l.Dial() } - s.kases = make([]FakeKas, 10) + s.kases = make([]FakeKas, 12) for i, ki := range []struct { url, private, public string @@ -1623,6 +1672,8 @@ func (s *TDFSuite) startBackend() { {"https://a.kas/", mockRSAPrivateKey1, mockRSAPublicKey1}, {"https://b.kas/", mockRSAPrivateKey2, mockRSAPublicKey2}, {"https://c.kas/", mockRSAPrivateKey3, mockRSAPublicKey3}, + {"https://d.kas/", mockECPrivateKey1, mockECPublicKey1}, + {"https://e.kas/", mockECPrivateKey2, mockECPublicKey2}, {kasAu, mockRSAPrivateKey1, mockRSAPublicKey1}, {kasCa, mockRSAPrivateKey2, mockRSAPublicKey2}, {kasUk, mockRSAPrivateKey2, mockRSAPublicKey2}, @@ -1757,22 +1808,83 @@ func (f *FakeKas) getRewrapResponse(rewrapRequest string) *kaspb.RewrapResponse kao := kaoReq.GetKeyAccessObject() wrappedKey := kaoReq.GetKeyAccessObject().GetWrappedKey() - kasPrivateKey := strings.ReplaceAll(f.privateKey, "\n\t", "\n") - if kao.GetKid() != "" && kao.GetKid() != f.KID { - // old kid - lk, ok := f.legakeys[kaoReq.GetKeyAccessObject().GetKid()] - f.s.Require().True(ok, "unable to find key [%s]", kao.GetKid()) - kasPrivateKey = strings.ReplaceAll(lk.private, "\n\t", "\n") + var entityWrappedKey []byte + switch kaoReq.GetKeyAccessObject().GetKeyType() { + case "ec-wrapped": + // Get the ephemeral public key in PEM format + ephemeralPubKeyPEM := kaoReq.GetKeyAccessObject().GetEphemeralPublicKey() + + // Get EC key size and convert to mode + keySize, err := ocrypto.GetECKeySize(ephemeralPubKeyPEM) + f.s.Require().NoError(err, "failed to get EC key size") + + mode, err := ocrypto.ECSizeToMode(keySize) + f.s.Require().NoError(err, "failed to convert key size to mode") + + // Parse the PEM public key + block, _ := pem.Decode(ephemeralPubKeyPEM) + f.s.Require().NoError(err, "failed to decode PEM block") + + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + f.s.Require().NoError(err, "failed to parse public key") + + ecPub, ok := pub.(*ecdsa.PublicKey) + if !ok { + f.s.Require().Error(err, "not an EC public key") + } + + // Compress the public key + compressedKey, err := ocrypto.CompressedECPublicKey(mode, *ecPub) + f.s.Require().NoError(err, "failed to compress public key") + + kasPrivateKey := strings.ReplaceAll(f.privateKey, "\n\t", "\n") + if kao.GetKid() != "" && kao.GetKid() != f.KID { + // old kid + lk, found := f.legakeys[kaoReq.GetKeyAccessObject().GetKid()] + f.s.Require().True(found, "unable to find key [%s]", kao.GetKid()) + kasPrivateKey = strings.ReplaceAll(lk.private, "\n\t", "\n") + } + + privateKey, err := ocrypto.ECPrivateKeyFromPem([]byte(kasPrivateKey)) + f.s.Require().NoError(err, "failed to extract private key from PEM") + + ed, err := ocrypto.NewECDecryptor(privateKey) + f.s.Require().NoError(err, "failed to create EC decryptor") + + symmetricKey, err := ed.DecryptWithEphemeralKey(wrappedKey, compressedKey) + f.s.Require().NoError(err, "failed to decrypt") + + asymEncrypt, err := ocrypto.FromPublicPEM(bodyData.GetClientPublicKey()) + f.s.Require().NoError(err, "ocrypto.FromPublicPEM failed") + + var sessionKey string + if e, found := asymEncrypt.(ocrypto.ECEncryptor); found { + sessionKey, err = e.PublicKeyInPemFormat() + f.s.Require().NoError(err, "unable to serialize ephemeral key") + } + resp.SessionPublicKey = sessionKey + entityWrappedKey, err = asymEncrypt.Encrypt(symmetricKey) + f.s.Require().NoError(err, "ocrypto.AsymEncryption.encrypt failed") + + case "wrapped": + kasPrivateKey := strings.ReplaceAll(f.privateKey, "\n\t", "\n") + if kao.GetKid() != "" && kao.GetKid() != f.KID { + // old kid + lk, ok := f.legakeys[kaoReq.GetKeyAccessObject().GetKid()] + f.s.Require().True(ok, "unable to find key [%s]", kao.GetKid()) + kasPrivateKey = strings.ReplaceAll(lk.private, "\n\t", "\n") + } + + asymDecrypt, err := ocrypto.NewAsymDecryption(kasPrivateKey) + f.s.Require().NoError(err, "ocrypto.NewAsymDecryption failed") + symmetricKey, err := asymDecrypt.Decrypt(wrappedKey) + f.s.Require().NoError(err, "ocrypto.Decrypt failed") + asymEncrypt, err := ocrypto.NewAsymEncryption(bodyData.GetClientPublicKey()) + f.s.Require().NoError(err, "ocrypto.NewAsymEncryption failed") + entityWrappedKey, err = asymEncrypt.Encrypt(symmetricKey) + f.s.Require().NoError(err, "ocrypto.encrypt failed") } - asymDecrypt, err := ocrypto.NewAsymDecryption(kasPrivateKey) - f.s.Require().NoError(err, "ocrypto.NewAsymDecryption failed") - symmetricKey, err := asymDecrypt.Decrypt(wrappedKey) - f.s.Require().NoError(err, "ocrypto.Decrypt failed") - asymEncrypt, err := ocrypto.NewAsymEncryption(bodyData.GetClientPublicKey()) - f.s.Require().NoError(err, "ocrypto.NewAsymEncryption failed") - entityWrappedKey, err := asymEncrypt.Encrypt(symmetricKey) - f.s.Require().NoError(err, "ocrypto.encrypt failed") kaoResult := &kaspb.KeyAccessRewrapResult{ Result: &kaspb.KeyAccessRewrapResult_KasWrappedKey{KasWrappedKey: entityWrappedKey}, Status: "permit", From 1b7116daa845998fde281f71214bb7f29371cbc3 Mon Sep 17 00:00:00 2001 From: sujan kota Date: Mon, 3 Mar 2025 09:51:31 -0500 Subject: [PATCH 17/18] fix the build --- sdk/tdf_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/tdf_test.go b/sdk/tdf_test.go index 5d95e6245..1fd073f95 100644 --- a/sdk/tdf_test.go +++ b/sdk/tdf_test.go @@ -961,7 +961,7 @@ func (s *TDFSuite) Test_TDFWithAssertionNegativeTests() { } } -func (s *TDFSuite) Test_TDFReader() { //nolint:gocognit // requires for testing tdf +func (s *TDFSuite) Test_TDFReader() { //nolint:gocognit // requires for testing tdf for _, test := range []partialReadTdfTest{ //nolint:gochecknoglobals // requires for testing tdf { payload: payload, // len: 62 @@ -1815,14 +1815,14 @@ func (f *FakeKas) getRewrapResponse(rewrapRequest string) *kaspb.RewrapResponse ephemeralPubKeyPEM := kaoReq.GetKeyAccessObject().GetEphemeralPublicKey() // Get EC key size and convert to mode - keySize, err := ocrypto.GetECKeySize(ephemeralPubKeyPEM) + keySize, err := ocrypto.GetECKeySize([]byte(ephemeralPubKeyPEM)) f.s.Require().NoError(err, "failed to get EC key size") mode, err := ocrypto.ECSizeToMode(keySize) f.s.Require().NoError(err, "failed to convert key size to mode") // Parse the PEM public key - block, _ := pem.Decode(ephemeralPubKeyPEM) + block, _ := pem.Decode([]byte(ephemeralPubKeyPEM)) f.s.Require().NoError(err, "failed to decode PEM block") pub, err := x509.ParsePKIXPublicKey(block.Bytes) From f2a0b724175d06dcc214f07c2f0ec66540f0efc9 Mon Sep 17 00:00:00 2001 From: sujan kota Date: Mon, 3 Mar 2025 10:29:47 -0500 Subject: [PATCH 18/18] fix the build --- sdk/tdf_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/tdf_test.go b/sdk/tdf_test.go index 1fd073f95..d257509eb 100644 --- a/sdk/tdf_test.go +++ b/sdk/tdf_test.go @@ -961,7 +961,7 @@ func (s *TDFSuite) Test_TDFWithAssertionNegativeTests() { } } -func (s *TDFSuite) Test_TDFReader() { //nolint:gocognit // requires for testing tdf +func (s *TDFSuite) Test_TDFReader() { //nolint:gocognit // requires for testing tdf for _, test := range []partialReadTdfTest{ //nolint:gochecknoglobals // requires for testing tdf { payload: payload, // len: 62