Skip to content

Commit

Permalink
feat(core): EXPERIMENTAL: EC-wrapped key support (#1902)
Browse files Browse the repository at this point in the history
### 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>
  • Loading branch information
dmihalcik-virtru and sujankota authored Feb 14, 2025
1 parent f902295 commit 652266f
Show file tree
Hide file tree
Showing 23 changed files with 1,146 additions and 297 deletions.
7 changes: 7 additions & 0 deletions docs/grpc/index.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion examples/cmd/decrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"os"
"path/filepath"

"github.com/opentdf/platform/sdk"

"github.com/spf13/cobra"
)

Expand All @@ -18,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)
}

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

if !isNano {
tdfreader, err := client.LoadTDF(file)
opts := []sdk.TDFReaderOption{}
if alg != "" {
kt, err := keyTypeForKeyType(alg)
if err != nil {
return err
}
opts = append(opts, sdk.WithSessionKeyType(kt))
}
tdfreader, err := client.LoadTDF(file, opts...)
if err != nil {
return err
}
Expand Down
23 changes: 22 additions & 1 deletion examples/cmd/encrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"strings"

"github.com/opentdf/platform/lib/ocrypto"

"github.com/opentdf/platform/sdk"
"github.com/spf13/cobra"
)
Expand All @@ -23,6 +22,7 @@ var (
outputName string
dataAttributes []string
collection int
alg string
)

func init() {
Expand All @@ -38,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 <iteration>_<output>")

ExamplesCmd.AddCommand(&encryptCmd)
Expand Down Expand Up @@ -102,13 +103,21 @@ 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.WithWrappingKeyAlg(ocrypto.EC256Key))
opts = append(opts, sdk.WithKasInformation(
sdk.KASInfo{
// examples assume insecure http
URL: fmt.Sprintf("http://%s", platformEndpoint),
PublicKey: "",
}))
}
if alg != "" {
kt, err := keyTypeForKeyType(alg)
if err != nil {
return err
}
opts = append(opts, sdk.WithWrappingKeyAlg(kt))
}
tdf, err := client.CreateTDF(out, in, opts...)
if err != nil {
return err
Expand Down Expand Up @@ -156,6 +165,18 @@ func encrypt(cmd *cobra.Command, args []string) error {
return nil
}

func keyTypeForKeyType(alg string) (ocrypto.KeyType, error) {
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.RSA2048Key, fmt.Errorf("unsupported key type [%s]", alg)
}
}

func cat(cmd *cobra.Command, nTdfFile string) error {
f, err := os.Open(nTdfFile)
if err != nil {
Expand Down
134 changes: 131 additions & 3 deletions lib/ocrypto/asym_decryption.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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.
Expand All @@ -64,3 +99,96 @@ 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")
return ECDecryptor{sk, salt, nil}, 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) {
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)
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
}
}
40 changes: 33 additions & 7 deletions lib/ocrypto/asym_encrypt_decrypt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
)

func TestAsymEncryptionAndDecryption(t *testing.T) {
var rsaKeys = []struct {
var keypairs = []struct {
privateKey string
publicKey string
}{
Expand Down Expand Up @@ -215,10 +215,25 @@ 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 {
asymEncryptor, err := NewAsymEncryption(test.publicKey)
for _, test := range keypairs {
asymEncryptor, err := FromPublicPEM(test.publicKey)
if err != nil {
t.Fatalf("NewAsymEncryption - failed: %v", err)
}
Expand All @@ -229,14 +244,25 @@ 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)
}

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 {
Expand Down
Loading

0 comments on commit 652266f

Please sign in to comment.