Skip to content

Commit 652266f

Browse files
feat(core): EXPERIMENTAL: EC-wrapped key support (#1902)
### Proposed Changes - Lets KAS use an elliptic key based mechanism for key (split) encapsulation - Adds a new `ec-wrapped` KAO type that uses a hybrid EC encryption scheme to wrap the values - Adds a feature flag (`services.kas.ec_tdf_enabled`) on the server. - Exposes feature flag to service launcher workflows as `ec-tdf-enabled` - To use with SDK, adds a new `WithWrappingKeyAlg` functional option ### Checklist - [ ] I have added or updated unit tests - [ ] I have added or updated integration tests (if appropriate) - [ ] I have added or updated documentation ### Testing Instructions <!-- branch-stack --> - `main` - \#1902 :point\_left: - \#1907 --------- Co-authored-by: sujan kota <sujankota@gmail.com>
1 parent f902295 commit 652266f

File tree

23 files changed

+1146
-297
lines changed

23 files changed

+1146
-297
lines changed

docs/grpc/index.html

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/cmd/decrypt.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"os"
99
"path/filepath"
1010

11+
"github.com/opentdf/platform/sdk"
12+
1113
"github.com/spf13/cobra"
1214
)
1315

@@ -18,6 +20,7 @@ func init() {
1820
RunE: decrypt,
1921
Args: cobra.MinimumNArgs(1),
2022
}
23+
decryptCmd.Flags().StringVarP(&alg, "rewrap-encapsulation-algorithm", "A", "rsa:2048", "Key wrap response algorithm algorithm:parameters")
2124
ExamplesCmd.AddCommand(decryptCmd)
2225
}
2326

@@ -81,7 +84,15 @@ func decrypt(cmd *cobra.Command, args []string) error {
8184
}
8285

8386
if !isNano {
84-
tdfreader, err := client.LoadTDF(file)
87+
opts := []sdk.TDFReaderOption{}
88+
if alg != "" {
89+
kt, err := keyTypeForKeyType(alg)
90+
if err != nil {
91+
return err
92+
}
93+
opts = append(opts, sdk.WithSessionKeyType(kt))
94+
}
95+
tdfreader, err := client.LoadTDF(file, opts...)
8596
if err != nil {
8697
return err
8798
}

examples/cmd/encrypt.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"strings"
1111

1212
"github.com/opentdf/platform/lib/ocrypto"
13-
1413
"github.com/opentdf/platform/sdk"
1514
"github.com/spf13/cobra"
1615
)
@@ -23,6 +22,7 @@ var (
2322
outputName string
2423
dataAttributes []string
2524
collection int
25+
alg string
2626
)
2727

2828
func init() {
@@ -38,6 +38,7 @@ func init() {
3838
encryptCmd.Flags().BoolVar(&noKIDInKAO, "no-kid-in-kao", false, "[deprecated] Disable storing key identifiers in TDF KAOs")
3939
encryptCmd.Flags().BoolVar(&noKIDInNano, "no-kid-in-nano", true, "Disable storing key identifiers in nanoTDF KAS ResourceLocator")
4040
encryptCmd.Flags().StringVarP(&outputName, "output", "o", "sensitive.txt.tdf", "name or path of output file; - for stdout")
41+
encryptCmd.Flags().StringVarP(&alg, "key-encapsulation-algorithm", "A", "rsa:2048", "Key wrap algorithm algorithm:parameters")
4142
encryptCmd.Flags().IntVarP(&collection, "collection", "c", 0, "number of nano's to create for collection. If collection >0 (default) then output will be <iteration>_<output>")
4243

4344
ExamplesCmd.AddCommand(&encryptCmd)
@@ -102,13 +103,21 @@ func encrypt(cmd *cobra.Command, args []string) error {
102103
opts := []sdk.TDFOption{sdk.WithDataAttributes(dataAttributes...)}
103104
if !autoconfigure {
104105
opts = append(opts, sdk.WithAutoconfigure(autoconfigure))
106+
opts = append(opts, sdk.WithWrappingKeyAlg(ocrypto.EC256Key))
105107
opts = append(opts, sdk.WithKasInformation(
106108
sdk.KASInfo{
107109
// examples assume insecure http
108110
URL: fmt.Sprintf("http://%s", platformEndpoint),
109111
PublicKey: "",
110112
}))
111113
}
114+
if alg != "" {
115+
kt, err := keyTypeForKeyType(alg)
116+
if err != nil {
117+
return err
118+
}
119+
opts = append(opts, sdk.WithWrappingKeyAlg(kt))
120+
}
112121
tdf, err := client.CreateTDF(out, in, opts...)
113122
if err != nil {
114123
return err
@@ -156,6 +165,18 @@ func encrypt(cmd *cobra.Command, args []string) error {
156165
return nil
157166
}
158167

168+
func keyTypeForKeyType(alg string) (ocrypto.KeyType, error) {
169+
switch alg {
170+
case string(ocrypto.RSA2048Key):
171+
return ocrypto.RSA2048Key, nil
172+
case string(ocrypto.EC256Key):
173+
return ocrypto.EC256Key, nil
174+
default:
175+
// do not submit add ocrypto.UnknownKey
176+
return ocrypto.RSA2048Key, fmt.Errorf("unsupported key type [%s]", alg)
177+
}
178+
}
179+
159180
func cat(cmd *cobra.Command, nTdfFile string) error {
160181
f, err := os.Open(nTdfFile)
161182
if err != nil {

lib/ocrypto/asym_decryption.go

Lines changed: 131 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,34 @@ package ocrypto
22

33
import (
44
"crypto"
5+
"crypto/aes"
6+
"crypto/cipher"
7+
"crypto/ecdh"
8+
"crypto/ecdsa"
9+
"crypto/elliptic"
510
"crypto/rsa"
11+
"crypto/sha256"
612
"crypto/x509"
713
"encoding/pem"
814
"errors"
915
"fmt"
16+
"io"
1017
"strings"
18+
19+
"golang.org/x/crypto/hkdf"
1120
)
1221

1322
type AsymDecryption struct {
1423
PrivateKey *rsa.PrivateKey
1524
}
1625

17-
// NewAsymDecryption creates and returns a new AsymDecryption.
18-
func NewAsymDecryption(privateKeyInPem string) (AsymDecryption, error) {
26+
type PrivateKeyDecryptor interface {
27+
// Decrypt decrypts ciphertext with private key.
28+
Decrypt(data []byte) ([]byte, error)
29+
}
30+
31+
// FromPrivatePEM creates and returns a new AsymDecryption.
32+
func FromPrivatePEM(privateKeyInPem string) (PrivateKeyDecryptor, error) {
1933
block, _ := pem.Decode([]byte(privateKeyInPem))
2034
if block == nil {
2135
return AsymDecryption{}, errors.New("failed to parse PEM formatted private key")
@@ -40,13 +54,34 @@ func NewAsymDecryption(privateKeyInPem string) (AsymDecryption, error) {
4054
}
4155

4256
switch privateKey := priv.(type) {
57+
case *ecdsa.PrivateKey:
58+
if sk, err := privateKey.ECDH(); err != nil {
59+
return nil, fmt.Errorf("unable to create ECDH key: %w", err)
60+
} else {
61+
return NewECDecryptor(sk)
62+
}
63+
case *ecdh.PrivateKey:
64+
return NewECDecryptor(privateKey)
4365
case *rsa.PrivateKey:
4466
return AsymDecryption{privateKey}, nil
4567
default:
4668
break
4769
}
4870

49-
return AsymDecryption{}, errors.New("not an rsa PEM formatted private key")
71+
return nil, errors.New("not a supported PEM formatted private key")
72+
}
73+
74+
func NewAsymDecryption(privateKeyInPem string) (AsymDecryption, error) {
75+
d, err := FromPrivatePEM(privateKeyInPem)
76+
if err != nil {
77+
return AsymDecryption{}, err
78+
}
79+
switch d := d.(type) {
80+
case AsymDecryption:
81+
return d, nil
82+
default:
83+
return AsymDecryption{}, errors.New("not an RSA private key")
84+
}
5085
}
5186

5287
// Decrypt decrypts ciphertext with private key.
@@ -64,3 +99,96 @@ func (asymDecryption AsymDecryption) Decrypt(data []byte) ([]byte, error) {
6499

65100
return bytes, nil
66101
}
102+
103+
type ECDecryptor struct {
104+
sk *ecdh.PrivateKey
105+
salt []byte
106+
info []byte
107+
}
108+
109+
func NewECDecryptor(sk *ecdh.PrivateKey) (ECDecryptor, error) {
110+
// TK Make these reasonable? IIRC salt should be longer, info maybe a parameters?
111+
salt := []byte("salt")
112+
return ECDecryptor{sk, salt, nil}, nil
113+
}
114+
115+
func (e ECDecryptor) Decrypt(_ []byte) ([]byte, error) {
116+
// TK How to get the ephmeral key into here?
117+
return nil, errors.New("ecdh standard decrypt unimplemented")
118+
}
119+
120+
func (e ECDecryptor) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) {
121+
var ek *ecdh.PublicKey
122+
123+
if pubFromDSN, err := x509.ParsePKIXPublicKey(ephemeral); err == nil {
124+
switch pubFromDSN := pubFromDSN.(type) {
125+
case *ecdsa.PublicKey:
126+
ek, err = ConvertToECDHPublicKey(pubFromDSN)
127+
if err != nil {
128+
return nil, fmt.Errorf("ecdh conversion failure: %w", err)
129+
}
130+
case *ecdh.PublicKey:
131+
ek = pubFromDSN
132+
default:
133+
return nil, errors.New("not an supported type of public key")
134+
}
135+
} else {
136+
ekDSA, err := UncompressECPubKey(convCurve(e.sk.Curve()), ephemeral)
137+
if err != nil {
138+
return nil, err
139+
}
140+
ek, err = ekDSA.ECDH()
141+
if err != nil {
142+
return nil, fmt.Errorf("ecdh failure: %w", err)
143+
}
144+
}
145+
146+
ikm, err := e.sk.ECDH(ek)
147+
if err != nil {
148+
return nil, fmt.Errorf("ecdh failure: %w", err)
149+
}
150+
151+
hkdfObj := hkdf.New(sha256.New, ikm, e.salt, e.info)
152+
153+
derivedKey := make([]byte, len(ikm))
154+
if _, err := io.ReadFull(hkdfObj, derivedKey); err != nil {
155+
return nil, fmt.Errorf("hkdf failure: %w", err)
156+
}
157+
158+
// Encrypt data with derived key using aes-gcm
159+
block, err := aes.NewCipher(derivedKey)
160+
if err != nil {
161+
return nil, fmt.Errorf("aes.NewCipher failure: %w", err)
162+
}
163+
164+
gcm, err := cipher.NewGCM(block)
165+
if err != nil {
166+
return nil, fmt.Errorf("cipher.NewGCM failure: %w", err)
167+
}
168+
169+
nonceSize := gcm.NonceSize()
170+
if len(data) < nonceSize {
171+
return nil, errors.New("ciphertext too short")
172+
}
173+
174+
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
175+
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
176+
if err != nil {
177+
return nil, fmt.Errorf("gcm.Open failure: %w", err)
178+
}
179+
180+
return plaintext, nil
181+
}
182+
183+
func convCurve(c ecdh.Curve) elliptic.Curve {
184+
switch c {
185+
case ecdh.P256():
186+
return elliptic.P256()
187+
case ecdh.P384():
188+
return elliptic.P384()
189+
case ecdh.P521():
190+
return elliptic.P521()
191+
default:
192+
return nil
193+
}
194+
}

lib/ocrypto/asym_encrypt_decrypt_test.go

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
)
66

77
func TestAsymEncryptionAndDecryption(t *testing.T) {
8-
var rsaKeys = []struct {
8+
var keypairs = []struct {
99
privateKey string
1010
publicKey string
1111
}{
@@ -215,10 +215,25 @@ I099IoRfC5djHUYYLMU/VkOIHuPC3sb7J65pSN26eR8bTMVNagk187V/xNwUuvkf
215215
wVyElqp317Ksz+GtTIc+DE6oryxK3tZd4hrj9fXT4KiJvQ4pcRjpePgH7B8=
216216
-----END CERTIFICATE-----`,
217217
},
218+
{`-----BEGIN PRIVATE KEY-----
219+
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgwQlQvwfqC0sEaPVi
220+
l1CdHNqAndukGsrqMsfiIefXHQChRANCAAQSZSoVakwpWhKBZIR9dmmTkKv7GK6n
221+
6d0yFeGzOyqB7l9LOzOwlCDdm9k0jBQBw597Dyy7KQzW73zi+pSpgfYr
222+
-----END PRIVATE KEY-----
223+
`, `-----BEGIN CERTIFICATE-----
224+
MIIBcTCCARegAwIBAgIUQBzVxCvhpTzXU+i7qyiTNniBL4owCgYIKoZIzj0EAwIw
225+
DjEMMAoGA1UEAwwDa2FzMB4XDTI1MDExMDE2MzQ1NVoXDTI2MDExMDE2MzQ1NVow
226+
DjEMMAoGA1UEAwwDa2FzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEmUqFWpM
227+
KVoSgWSEfXZpk5Cr+xiup+ndMhXhszsqge5fSzszsJQg3ZvZNIwUAcOfew8suykM
228+
1u984vqUqYH2K6NTMFEwHQYDVR0OBBYEFCAo/c694aHwmw/0kUTKuFvAQ4OcMB8G
229+
A1UdIwQYMBaAFCAo/c694aHwmw/0kUTKuFvAQ4OcMA8GA1UdEwEB/wQFMAMBAf8w
230+
CgYIKoZIzj0EAwIDSAAwRQIgUzKsJS6Pcu2aZ6BFfuqob552Ebdel4uFGZMqWrwW
231+
bW0CIQDT5QED+8mHFot9JXSx2q1c5mnRvl4yElK0fiHeatBdqw==
232+
-----END CERTIFICATE-----`},
218233
}
219234

220-
for _, test := range rsaKeys {
221-
asymEncryptor, err := NewAsymEncryption(test.publicKey)
235+
for _, test := range keypairs {
236+
asymEncryptor, err := FromPublicPEM(test.publicKey)
222237
if err != nil {
223238
t.Fatalf("NewAsymEncryption - failed: %v", err)
224239
}
@@ -229,14 +244,25 @@ wVyElqp317Ksz+GtTIc+DE6oryxK3tZd4hrj9fXT4KiJvQ4pcRjpePgH7B8=
229244
t.Fatalf("AsymEncryption encrypt failed: %v", err)
230245
}
231246

232-
asymDecryptor, err := NewAsymDecryption(test.privateKey)
247+
asymDecryptor, err := FromPrivatePEM(test.privateKey)
233248
if err != nil {
234249
t.Fatalf("NewAsymDecryption - failed: %v", err)
235250
}
236251

237-
decryptedText, err := asymDecryptor.Decrypt(cipherText)
238-
if err != nil {
239-
t.Fatalf("AsymDecryption decrypt failed: %v", err)
252+
var decryptedText []byte
253+
ek := asymEncryptor.EphemeralKey()
254+
if ek == nil {
255+
decryptedText, err = asymDecryptor.Decrypt(cipherText)
256+
if err != nil {
257+
t.Fatalf("AsymDecryption decrypt failed: %v", err)
258+
}
259+
} else if ecd, ok := asymDecryptor.(ECDecryptor); ok {
260+
decryptedText, err = ecd.DecryptWithEphemeralKey(cipherText, ek)
261+
if err != nil {
262+
t.Fatalf("AsymDecryption decrypt failed: %v", err)
263+
}
264+
} else {
265+
t.Fatalf("AsymDecryption wrong type: %T", asymDecryptor)
240266
}
241267

242268
if string(decryptedText) != plainText {

0 commit comments

Comments
 (0)