From 3f123313879571139a26db7060300a5864389093 Mon Sep 17 00:00:00 2001 From: Baha Shaaban Date: Tue, 9 Jun 2020 00:45:16 -0400 Subject: [PATCH] feat: second change for ECDH-1PU crypto primitive Second chant to introduce ECDH-1PU Tink crypto primitive for Authcrypt (JWE) implementations: Includes new key/primitive key types, key managers and first core primitive logic with (incomplete, to be done in a followup change) 1PU key wrapping. Followup changes will include primitive factories, public key export/import logic, key templates and core 1PU logic. part of #1806 Signed-off-by: Baha Shaaban --- .../primitive/composite/ecdh1pu/ecdh1pu.go | 107 +++++ .../ecdh1pu_aes_aead_private_key_manager.go | 191 +++++++++ ...dh1pu_aes_aead_private_key_manager_test.go | 314 +++++++++++++++ .../ecdh1pu_aes_aead_public_key_manager.go | 140 +++++++ ...cdh1pu_aes_aead_public_key_manager_test.go | 262 ++++++++++++ .../composite/ecdh1pu/ecdh1pu_common.go | 25 ++ .../composite/ecdh1pu/ecdh1pu_common_test.go | 60 +++ .../register_ecdh1pu_aead_enc_helper.go | 204 ++++++++++ .../ecdh1pu/subtle/ecdh1pu_aead_enc_helper.go | 26 ++ .../ecdh1pu_aes_aead_composite_decrypt.go | 98 +++++ .../ecdh1pu_aes_aead_composite_encrypt.go | 201 ++++++++++ .../subtle/ecdh1pu_aes_aead_composite_test.go | 378 ++++++++++++++++++ .../subtle/ecdhes_1pukdf_recipient_kuw.go | 65 +++ .../ecdh1pu/subtle/ecdhes_1pukdf_sender_kw.go | 77 ++++ 14 files changed, 2148 insertions(+) create mode 100644 pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu.go create mode 100644 pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_aes_aead_private_key_manager.go create mode 100644 pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_aes_aead_private_key_manager_test.go create mode 100644 pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_aes_aead_public_key_manager.go create mode 100644 pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_aes_aead_public_key_manager_test.go create mode 100644 pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_common.go create mode 100644 pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_common_test.go create mode 100644 pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/register_ecdh1pu_aead_enc_helper.go create mode 100644 pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdh1pu_aead_enc_helper.go create mode 100644 pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdh1pu_aes_aead_composite_decrypt.go create mode 100644 pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdh1pu_aes_aead_composite_encrypt.go create mode 100644 pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdh1pu_aes_aead_composite_test.go create mode 100644 pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdhes_1pukdf_recipient_kuw.go create mode 100644 pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdhes_1pukdf_sender_kw.go diff --git a/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu.go b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu.go new file mode 100644 index 0000000000..45fa04f95e --- /dev/null +++ b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu.go @@ -0,0 +1,107 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +// Package ecdh1pu provides implementations of payload encryption using ECDH-1PU KW key wrapping with AEAD primitives. +// +// The functionality of ecdh1pu Encryption is represented as a pair of +// primitives (interfaces): +// +// * ECDH1PUEncrypt for encryption of data and aad for a given list of recipients keys +// +// * ECDH1PUDecrypt for decryption of data for a certain recipient key and returning decrypted plaintext +// +// +// Example: +// +// package main +// +// import ( +// "bytes" +// +// "github.com/google/tink/go/keyset" +// +// "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle" +// "github.com/aries-framework-go/pkg/crypto/tinkcrypto/composite/ecdh1pu" +// ) +// +// func main() { +// // create recipient side keyset handle +// recKH, err := keyset.NewHandle(ecdh1pu.ECDH1PU256KWAES256GCMKeyTemplate()) +// if err != nil { +// //handle error +// } +// +// // extract recipient public keyset handle and key +// recPubKH, err := recKH.Public() +// if err != nil { +// //handle error +// } +// +// buf := new(bytes.Buffer) +// pubKeyWriter := ecdh1pu.NewWriter(buf) +// err = recPubKH.WriteWithNoSecrets(pubKeyWriter) +// if err != nil { +// //handle error +// } +// +// ecPubKey := new(subtle.ECPublicKey) +// err := json.Unmarshal(buf.Bytes(), ecPubKey) +// +// // now create sender keyset handle with recipient public key (ecPubKey) +// sKH, err := keyset.NewHandle(ECDH1PU256KWAES256GCMKeyTemplateWithRecipients( +// []subtle.ECPublicKey{*ecPubKey})) +// if err != nil { +// // handle error +// } +// +// // for more recipient keys pass in a list: []subtle.ECPublicKey{*ecPubKey1, *ecPubKey2, *ecPubKey3, etc.}) +// // at least 1 recipient is required. +// +// // extract sender public keyset handle to encrypt +// senderPubKH, err := sKH.Public() +// if err != nil { +// //handle error +// } +// +// e := ecdh1pu.NewECDH1PUEncrypt(senderPubKH) +// +// ct, err = e.Encrypt([]byte("secret message"), []byte("some aad")) +// if err != nil { +// // handle error +// } +// +// // get a handle on the decryption key material for a recipient +// // this is usually reloading the recipient's keyset handle (ie: `recKH` above) from a kms +// refRecKH , err := keyset.NewHandle( .....reference/rebuild `recKH` here...); +// d := ecdh1pu.NewECDH1PUDecrypt(refRecKH) +// +// pt, err := d.Decrypt(ct) +// if err != nil { +// // handle error +// } +// } +package ecdh1pu + +import ( + "fmt" + + "github.com/google/tink/go/core/registry" +) + +// TODO - find a better way to setup tink than init. +// nolint: gochecknoinits +func init() { + // TODO - avoid the tink registry singleton (if possible). + err := registry.RegisterKeyManager(newECDH1PUPrivateKeyManager()) + if err != nil { + panic(fmt.Sprintf("ecdh1pu.init() failed: %v", err)) + } + + err = registry.RegisterKeyManager(newECDH1PUPublicKeyManager()) + if err != nil { + panic(fmt.Sprintf("ecdh1pu.init() failed: %v", err)) + } +} diff --git a/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_aes_aead_private_key_manager.go b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_aes_aead_private_key_manager.go new file mode 100644 index 0000000000..a00d54c8fb --- /dev/null +++ b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_aes_aead_private_key_manager.go @@ -0,0 +1,191 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package ecdh1pu + +import ( + "crypto/elliptic" + "fmt" + + "github.com/golang/protobuf/proto" + "github.com/google/tink/go/core/registry" + hybrid "github.com/google/tink/go/hybrid/subtle" + "github.com/google/tink/go/keyset" + tinkpb "github.com/google/tink/go/proto/tink_go_proto" + + "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle" + commonpb "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/proto/common_composite_go_proto" + ecdh1pupb "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/proto/ecdh1pu_aead_go_proto" +) + +const ( + ecdh1puAESPrivateKeyVersion = 0 + ecdh1puAESPrivateKeyTypeURL = "type.hyperledger.org/hyperledger.aries.crypto.tink.Ecdh1puAesAeadPrivateKey" +) + +// common errors +var errInvalidECDH1PUAESPrivateKey = fmt.Errorf("ecdh1pu_aes_private_key_manager: invalid key") +var errInvalidECDH1PUAESPrivateKeyFormat = fmt.Errorf("ecdh1pu_aes_private_key_manager: invalid key format") + +// ecdh1puAESPrivateKeyManager is an implementation of PrivateKeyManager interface. +// It generates new ECDHESPrivateKey (AES) keys and produces new instances of ECDH1PUAEADCompositeDecrypt subtle. +type ecdh1puAESPrivateKeyManager struct{} + +// Assert that ecdh1puAESPrivateKeyManager implements the PrivateKeyManager interface. +var _ registry.PrivateKeyManager = (*ecdh1puAESPrivateKeyManager)(nil) + +// newECDH1PUPrivateKeyManager creates a new ecdh1puAESPrivateKeyManager. +func newECDH1PUPrivateKeyManager() *ecdh1puAESPrivateKeyManager { + return new(ecdh1puAESPrivateKeyManager) +} + +// Primitive creates an ECDHESPrivateKey subtle for the given serialized ECDHESPrivateKey proto. +func (km *ecdh1puAESPrivateKeyManager) Primitive(serializedKey []byte) (interface{}, error) { + if len(serializedKey) == 0 { + return nil, errInvalidECDH1PUAESPrivateKey + } + + key := new(ecdh1pupb.Ecdh1PuAeadPrivateKey) + + err := proto.Unmarshal(serializedKey, key) + if err != nil { + return nil, errInvalidECDH1PUAESPrivateKey + } + + curve, err := km.validateKey(key) + if err != nil { + return nil, errInvalidECDH1PUAESPrivateKey + } + + pvt := hybrid.GetECPrivateKey(curve, key.KeyValue) + + rEnc, err := newRegisterECDH1PUAEADEncHelper(key.PublicKey.Params.EncParams.AeadEnc) + if err != nil { + return nil, err + } + + ptFormat := key.PublicKey.Params.EcPointFormat.String() + + return subtle.NewECDH1PUAEADCompositeDecrypt(pvt, ptFormat, rEnc, commonpb.KeyType_EC), nil +} + +// NewKey creates a new key according to the specification of ECDH1PUPrivateKey format. +func (km *ecdh1puAESPrivateKeyManager) NewKey(serializedKeyFormat []byte) (proto.Message, error) { + if len(serializedKeyFormat) == 0 { + return nil, errInvalidECDH1PUAESPrivateKeyFormat + } + + keyFormat := new(ecdh1pupb.Ecdh1PuAeadKeyFormat) + + err := proto.Unmarshal(serializedKeyFormat, keyFormat) + if err != nil { + return nil, errInvalidECDH1PUAESPrivateKeyFormat + } + + curve, err := validateKeyFormat(keyFormat.Params) + if err != nil { + return nil, errInvalidECDH1PUAESPrivateKeyFormat + } + + keyFormat.Params.KwParams.KeyType = commonpb.KeyType_EC + + pvt, err := hybrid.GenerateECDHKeyPair(curve) + if err != nil { + return nil, err + } + + return &ecdh1pupb.Ecdh1PuAeadPrivateKey{ + Version: ecdh1puAESPrivateKeyVersion, + KeyValue: pvt.D.Bytes(), + PublicKey: &ecdh1pupb.Ecdh1PuAeadPublicKey{ + Version: ecdh1puAESPrivateKeyVersion, + Params: keyFormat.Params, + X: pvt.PublicKey.Point.X.Bytes(), + Y: pvt.PublicKey.Point.Y.Bytes(), + }, + }, nil +} + +// NewKeyData creates a new KeyData according to the specification of ECDHESPrivateKey Format. +// It should be used solely by the key management API. +func (km *ecdh1puAESPrivateKeyManager) NewKeyData(serializedKeyFormat []byte) (*tinkpb.KeyData, error) { + key, err := km.NewKey(serializedKeyFormat) + if err != nil { + return nil, err + } + + serializedKey, err := proto.Marshal(key) + if err != nil { + return nil, err + } + + return &tinkpb.KeyData{ + TypeUrl: ecdh1puAESPrivateKeyTypeURL, + Value: serializedKey, + KeyMaterialType: tinkpb.KeyData_ASYMMETRIC_PRIVATE, + }, nil +} + +// PublicKeyData returns the enclosed public key data of serializedPrivKey +func (km *ecdh1puAESPrivateKeyManager) PublicKeyData(serializedPrivKey []byte) (*tinkpb.KeyData, error) { + privKey := new(ecdh1pupb.Ecdh1PuAeadPrivateKey) + + err := proto.Unmarshal(serializedPrivKey, privKey) + if err != nil { + return nil, errInvalidECDH1PUAESPrivateKey + } + + serializedPubKey, err := proto.Marshal(privKey.PublicKey) + if err != nil { + return nil, errInvalidECDH1PUAESPrivateKey + } + + return &tinkpb.KeyData{ + TypeUrl: ecdh1puAESPublicKeyTypeURL, + Value: serializedPubKey, + KeyMaterialType: tinkpb.KeyData_ASYMMETRIC_PUBLIC, + }, nil +} + +// DoesSupport indicates if this key manager supports the given key type. +func (km *ecdh1puAESPrivateKeyManager) DoesSupport(typeURL string) bool { + return typeURL == ecdh1puAESPrivateKeyTypeURL +} + +// TypeURL returns the key type of keys managed by this key manager. +func (km *ecdh1puAESPrivateKeyManager) TypeURL() string { + return ecdh1puAESPrivateKeyTypeURL +} + +// validateKey validates the given ECDH1PUPrivateKey and returns the KW curve. +func (km *ecdh1puAESPrivateKeyManager) validateKey(key *ecdh1pupb.Ecdh1PuAeadPrivateKey) (elliptic.Curve, error) { + err := keyset.ValidateKeyVersion(key.Version, ecdh1puAESPrivateKeyVersion) + if err != nil { + return nil, fmt.Errorf("ecdh1pu_private_key_manager: invalid key: %s", err) + } + + return validateKeyFormat(key.PublicKey.Params) +} + +// validateKeyFormat validates the given ECDHESKeyFormat and returns the KW Curve. +func validateKeyFormat(params *ecdh1pupb.Ecdh1PuAeadParams) (elliptic.Curve, error) { + c, err := hybrid.GetCurve(params.KwParams.CurveType.String()) + if err != nil { + return nil, err + } + + km, err := registry.GetKeyManager(params.EncParams.AeadEnc.TypeUrl) + if err != nil { + return nil, err + } + + _, err = km.NewKeyData(params.EncParams.AeadEnc.Value) + if err != nil { + return nil, err + } + + return c, nil +} diff --git a/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_aes_aead_private_key_manager_test.go b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_aes_aead_private_key_manager_test.go new file mode 100644 index 0000000000..6384b980ad --- /dev/null +++ b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_aes_aead_private_key_manager_test.go @@ -0,0 +1,314 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package ecdh1pu + +import ( + "bytes" + "crypto/elliptic" + "crypto/rand" + "strings" + "testing" + + "github.com/golang/protobuf/proto" + "github.com/google/tink/go/aead" + hybrid "github.com/google/tink/go/hybrid/subtle" + gcmpb "github.com/google/tink/go/proto/aes_gcm_go_proto" + commonpb "github.com/google/tink/go/proto/common_go_proto" + tinkpb "github.com/google/tink/go/proto/tink_go_proto" + "github.com/stretchr/testify/require" + + ecdh1pupb "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/proto/ecdh1pu_aead_go_proto" +) + +func TestECDH1PUPrivateKeyManager_Primitive(t *testing.T) { + km := newECDH1PUPrivateKeyManager() + + t.Run("Test private key manager Primitive() with empty serialized key", func(t *testing.T) { + p, err := km.Primitive([]byte("")) + require.EqualError(t, err, errInvalidECDH1PUAESPrivateKey.Error(), + "ECDH1PUPrivate primitive from empty serialized key must fail") + require.Empty(t, p) + }) + + t.Run("Test private key manager Primitive() with bad serialize key", func(t *testing.T) { + p, err := km.Primitive([]byte("bad.data")) + require.EqualError(t, err, errInvalidECDH1PUAESPrivateKey.Error(), + "ECDH1PUPrivate primitive from bad serialized key must fail") + require.Empty(t, p) + }) + + format := &gcmpb.AesGcmKeyFormat{ + KeySize: 32, + } + serializedFormat, err := proto.Marshal(format) + require.NoError(t, err) + + format = &gcmpb.AesGcmKeyFormat{ + KeySize: 99, // bad AES128GCM size + } + + badSerializedFormat, err := proto.Marshal(format) + require.NoError(t, err) + + var flagTests = []struct { + tcName string + version uint32 + curveType commonpb.EllipticCurveType + ecPtFmt commonpb.EcPointFormat + encTmp *tinkpb.KeyTemplate + }{ + { + tcName: "private key manager Primitive() using key with bad version", + version: 9999, + curveType: commonpb.EllipticCurveType_NIST_P256, + ecPtFmt: commonpb.EcPointFormat_COMPRESSED, + encTmp: aead.AES128GCMKeyTemplate(), + }, + { + tcName: "private key manager Primitive() using key with bad curve", + version: 0, + curveType: commonpb.EllipticCurveType_UNKNOWN_CURVE, + ecPtFmt: commonpb.EcPointFormat_COMPRESSED, + encTmp: aead.AES128GCMKeyTemplate(), + }, + { + tcName: "success private key manager Primitive()", + version: 0, + curveType: commonpb.EllipticCurveType_NIST_P256, + ecPtFmt: commonpb.EcPointFormat_UNCOMPRESSED, + encTmp: aead.AES128GCMKeyTemplate(), + }, + { + tcName: "private key manager Primitive() using key with bad key template URL", + version: 0, + curveType: commonpb.EllipticCurveType_NIST_P256, + ecPtFmt: commonpb.EcPointFormat_COMPRESSED, + encTmp: &tinkpb.KeyTemplate{ + TypeUrl: "bad.type/url/value", + Value: serializedFormat, + OutputPrefixType: tinkpb.OutputPrefixType_RAW, + }, + }, + { + tcName: "private key manager Primitive() using key with bad dem key size", + version: 0, + curveType: commonpb.EllipticCurveType_NIST_P256, + ecPtFmt: commonpb.EcPointFormat_COMPRESSED, + encTmp: &tinkpb.KeyTemplate{ + TypeUrl: aesGCMTypeURL, + Value: badSerializedFormat, + OutputPrefixType: tinkpb.OutputPrefixType_RAW, + }, + }, + } + + for _, tc := range flagTests { + tt := tc + t.Run("Test "+tt.tcName, func(t *testing.T) { + c := tt.curveType + encT := tt.encTmp + ptFmt := tt.ecPtFmt + v := tt.version + + // temporarily reset curvType if its unknown type so subtle.GetCurve() below doesn't fail + if tt.curveType.String() == commonpb.EllipticCurveType_UNKNOWN_CURVE.String() { + c = commonpb.EllipticCurveType_NIST_P256 + } + + crv, err := hybrid.GetCurve(c.String()) + require.NoError(t, err) + d, x, y, err := elliptic.GenerateKey(crv, rand.Reader) + require.NoError(t, err) + + // set back curvType if it was unknown to proceed with the test + if tt.curveType.String() == commonpb.EllipticCurveType_UNKNOWN_CURVE.String() { + c = tt.curveType + } + + pubKeyProto := &ecdh1pupb.Ecdh1PuAeadPrivateKey{ + Version: v, + PublicKey: &ecdh1pupb.Ecdh1PuAeadPublicKey{ + Version: v, // if v > 0 to force an error when calling km.Primitive() + Params: &ecdh1pupb.Ecdh1PuAeadParams{ + KwParams: &ecdh1pupb.Ecdh1PuKwParams{ + CurveType: c, // unknown curve type to force an error when calling km.Primitive() + }, + EncParams: &ecdh1pupb.Ecdh1PuAeadEncParams{ + AeadEnc: encT, // invalid data enc key template to get an error when calling km.Primitive() + }, + EcPointFormat: ptFmt, + }, + X: x.Bytes(), + Y: y.Bytes(), + }, + KeyValue: d, + } + + sPubKey, err := proto.Marshal(pubKeyProto) + require.NoError(t, err) + + p, err := km.Primitive(sPubKey) + if bytes.Equal(tt.encTmp.Value, badSerializedFormat) { + require.EqualError(t, err, errInvalidECDH1PUAESPrivateKey.Error(), + "ECDH1PUPrivate primitive from serialized key with invalid serialized key") + require.Empty(t, p) + + return + } + + if strings.Contains(tt.tcName, "success") { + require.NoError(t, err) + require.NotEmpty(t, p) + return + } + + require.Errorf(t, err, tt.tcName) + require.Empty(t, p) + }) + } +} + +func TestEcdh1PuPrivateKeyManager_DoesSupport(t *testing.T) { + km := newECDH1PUPrivateKeyManager() + require.False(t, km.DoesSupport("bad/url")) + require.True(t, km.DoesSupport(ecdh1puAESPrivateKeyTypeURL)) +} + +func TestEcdh1PuPrivateKeyManager_NewKey(t *testing.T) { + km := newECDH1PUPrivateKeyManager() + + t.Run("Test private key manager NewKey() with nil key", func(t *testing.T) { + k, err := km.NewKey(nil) + require.EqualError(t, err, errInvalidECDH1PUAESPrivateKeyFormat.Error()) + require.Empty(t, k) + }) + + t.Run("Test private key manager NewKey() with bad serialize key", func(t *testing.T) { + p, err := km.NewKey([]byte("bad.data")) + require.EqualError(t, err, errInvalidECDH1PUAESPrivateKeyFormat.Error(), + "ECDH1PUPrivate NewKey() from bad serialized key must fail") + require.Empty(t, p) + }) + + format := &gcmpb.AesGcmKeyFormat{ + KeySize: 32, + } + + serializedFormat, err := proto.Marshal(format) + require.NoError(t, err) + + format = &gcmpb.AesGcmKeyFormat{ + KeySize: 99, // bad AES128GCM size + } + + badSerializedFormat, err := proto.Marshal(format) + require.NoError(t, err) + + var flagTests = []struct { + tcName string + curveType commonpb.EllipticCurveType + ecPtFmt commonpb.EcPointFormat + encTmp *tinkpb.KeyTemplate + }{ + { + tcName: "success private key manager NewKey() and NewKeyData()", + curveType: commonpb.EllipticCurveType_NIST_P256, + ecPtFmt: commonpb.EcPointFormat_COMPRESSED, + encTmp: aead.AES128GCMKeyTemplate(), + }, + { + tcName: "private key manager NewKey() and NewKeyData() using key with bad curve", + curveType: commonpb.EllipticCurveType_UNKNOWN_CURVE, + ecPtFmt: commonpb.EcPointFormat_COMPRESSED, + encTmp: aead.AES128GCMKeyTemplate(), + }, + { + tcName: "private key manager NewKey() and NewKeyData() using key with bad key template URL", + curveType: commonpb.EllipticCurveType_NIST_P256, + ecPtFmt: commonpb.EcPointFormat_COMPRESSED, + encTmp: &tinkpb.KeyTemplate{ + TypeUrl: "bad.type/url/value", + Value: serializedFormat, + OutputPrefixType: tinkpb.OutputPrefixType_RAW, + }, + }, + { + tcName: "private key manager NewKey() and NewKeyData() using key with bad dem key size", + curveType: commonpb.EllipticCurveType_NIST_P256, + ecPtFmt: commonpb.EcPointFormat_COMPRESSED, + encTmp: &tinkpb.KeyTemplate{ + TypeUrl: aesGCMTypeURL, + Value: badSerializedFormat, + OutputPrefixType: tinkpb.OutputPrefixType_RAW, + }, + }, + } + + for _, tc := range flagTests { + tt := tc + t.Run("Test "+tt.tcName, func(t *testing.T) { + c := tt.curveType + encT := tt.encTmp + ptFmt := tt.ecPtFmt + + privKeyProto := &ecdh1pupb.Ecdh1PuAeadKeyFormat{ + Params: &ecdh1pupb.Ecdh1PuAeadParams{ + KwParams: &ecdh1pupb.Ecdh1PuKwParams{ + CurveType: c, // unknown curve type to force an error when calling km.Primitive() + }, + EncParams: &ecdh1pupb.Ecdh1PuAeadEncParams{ + AeadEnc: encT, // invalid data enc key template to force an error when calling km.Primitive() + }, + EcPointFormat: ptFmt, // unknown EC Point format type to force an error when calling km.Primitive() + }, + } + + sPrivKey, err := proto.Marshal(privKeyProto) + require.NoError(t, err) + + p, err := km.NewKey(sPrivKey) + if strings.Contains(tt.tcName, "success") { + require.NoError(t, err) + require.NotEmpty(t, p) + + sp, e := proto.Marshal(p) + require.NoError(t, e) + require.NotEmpty(t, sp) + + // try PublicKeyData() with bad serialized private key + pubK, e := km.PublicKeyData([]byte("bad serialized private key")) + require.Error(t, e) + require.Empty(t, pubK) + + // try PublicKeyData() with valid serialized private key + pubK, e = km.PublicKeyData(sp) + require.NoError(t, e) + require.NotEmpty(t, pubK) + } + + kd, err := km.NewKeyData(sPrivKey) + if strings.Contains(tt.tcName, "success") { + require.NoError(t, err) + require.NotEmpty(t, kd) + require.Equal(t, kd.TypeUrl, ecdh1puAESPrivateKeyTypeURL) + require.Equal(t, kd.KeyMaterialType, tinkpb.KeyData_ASYMMETRIC_PRIVATE) + return + } + + if bytes.Equal(tt.encTmp.Value, badSerializedFormat) { + require.EqualError(t, err, errInvalidECDH1PUAESPrivateKeyFormat.Error(), + "ECDH1PUPrivate NewKey from serialized key with invalid serialized key") + require.Empty(t, p) + + return + } + + require.Errorf(t, err, tt.tcName) + require.Empty(t, p) + }) + } +} diff --git a/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_aes_aead_public_key_manager.go b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_aes_aead_public_key_manager.go new file mode 100644 index 0000000000..c51b63c714 --- /dev/null +++ b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_aes_aead_public_key_manager.go @@ -0,0 +1,140 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package ecdh1pu + +import ( + "crypto/elliptic" + "fmt" + + "github.com/golang/protobuf/proto" + "github.com/google/tink/go/core/registry" + hybrid "github.com/google/tink/go/hybrid/subtle" + "github.com/google/tink/go/keyset" + tinkpb "github.com/google/tink/go/proto/tink_go_proto" + + "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/composite" + "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle" + commonpb "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/proto/common_composite_go_proto" + ecdh1pupb "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/proto/ecdh1pu_aead_go_proto" +) + +const ( + ecdh1puAESPublicKeyVersion = 0 + ecdh1puAESPublicKeyTypeURL = "type.hyperledger.org/hyperledger.aries.crypto.tink.Ecdh1puAesAeadPublicKey" +) + +// common errors +var errInvalidECDH1PUAESPublicKey = fmt.Errorf("ecdh1pu_aes_public_key_manager: invalid key") + +// ecdh1puPublicKeyManager is an implementation of KeyManager interface. +// It generates new ECDH1PUPublicKey (AES) keys and produces new instances of ECDH1PUAEADCompositeEncrypt subtle. +type ecdh1puPublicKeyManager struct{} + +// Assert that ecdh1puPublicKeyManager implements the KeyManager interface. +var _ registry.KeyManager = (*ecdh1puPublicKeyManager)(nil) + +// newECDH1PUPublicKeyManager creates a new ecdh1puPublicKeyManager. +func newECDH1PUPublicKeyManager() *ecdh1puPublicKeyManager { + return new(ecdh1puPublicKeyManager) +} + +// Primitive creates an ECDH1PUPublicKey subtle for the given serialized ECDH1PUPublicKey proto. +func (km *ecdh1puPublicKeyManager) Primitive(serializedKey []byte) (interface{}, error) { + if len(serializedKey) == 0 { + return nil, errInvalidECDH1PUAESPublicKey + } + + ecdh1puPubKey := new(ecdh1pupb.Ecdh1PuAeadPublicKey) + + err := proto.Unmarshal(serializedKey, ecdh1puPubKey) + if err != nil { + return nil, errInvalidECDH1PUAESPublicKey + } + + _, err = km.validateKey(ecdh1puPubKey) + if err != nil { + return nil, errInvalidECDH1PUAESPublicKey + } + + var recipientsKeys []*composite.PublicKey + + for _, recKey := range ecdh1puPubKey.Params.KwParams.Recipients { + e := km.validateRecKey(recKey) + if e != nil { + return nil, errInvalidECDH1PUAESPublicKey + } + + pub := &composite.PublicKey{ + KID: recKey.KID, + Type: recKey.KeyType.String(), + Curve: recKey.CurveType.String(), + X: recKey.X, + Y: recKey.Y, + } + + recipientsKeys = append(recipientsKeys, pub) + } + + rEnc, err := newRegisterECDH1PUAEADEncHelper(ecdh1puPubKey.Params.EncParams.AeadEnc) + if err != nil { + return nil, err + } + + ptFormat := ecdh1puPubKey.Params.EcPointFormat.String() + + return subtle.NewECDH1PUAEADCompositeEncrypt(recipientsKeys, ptFormat, rEnc, commonpb.KeyType_EC), nil +} + +// DoesSupport indicates if this key manager supports the given key type. +func (km *ecdh1puPublicKeyManager) DoesSupport(typeURL string) bool { + return typeURL == ecdh1puAESPublicKeyTypeURL +} + +// TypeURL returns the key type of keys managed by this key manager. +func (km *ecdh1puPublicKeyManager) TypeURL() string { + return ecdh1puAESPublicKeyTypeURL +} + +// NewKey is not implemented for public key manager. +func (km *ecdh1puPublicKeyManager) NewKey(serializedKeyFormat []byte) (proto.Message, error) { + return nil, fmt.Errorf("ecdh1pu_public_key_manager: NewKey not implemented") +} + +// NewKeyData is not implemented for public key manager. +func (km *ecdh1puPublicKeyManager) NewKeyData(serializedKeyFormat []byte) (*tinkpb.KeyData, error) { + return nil, fmt.Errorf("ecdh1pu_public_key_manager: NewKeyData not implemented") +} + +// validateKey validates the given ECDHESPublicKey. +func (km *ecdh1puPublicKeyManager) validateKey(key *ecdh1pupb.Ecdh1PuAeadPublicKey) (elliptic.Curve, error) { + err := keyset.ValidateKeyVersion(key.Version, ecdh1puAESPublicKeyVersion) + if err != nil { + return nil, fmt.Errorf("ecdh1pu_publie_key_manager: invalid key: %s", err) + } + + return validateKeyFormat(key.Params) +} + +// validateRecKey validates the given recipient's ECDHESPublicKey. +func (km *ecdh1puPublicKeyManager) validateRecKey(key *ecdh1pupb.Ecdh1PuAeadRecipientPublicKey) error { + err := keyset.ValidateKeyVersion(key.Version, ecdh1puAESPublicKeyVersion) + if err != nil { + return fmt.Errorf("ecdh1pu_public_key_manager: invalid key: %s", err) + } + + _, err = GetKeyType(key.KeyType.String()) + if err != nil { + return err + } + + _, err = hybrid.GetCurve(key.CurveType.String()) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_aes_aead_public_key_manager_test.go b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_aes_aead_public_key_manager_test.go new file mode 100644 index 0000000000..e1e7f75dc2 --- /dev/null +++ b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_aes_aead_public_key_manager_test.go @@ -0,0 +1,262 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package ecdh1pu + +import ( + "crypto/elliptic" + "crypto/rand" + "strings" + "testing" + + "github.com/golang/protobuf/proto" + "github.com/google/tink/go/aead" + hybrid "github.com/google/tink/go/hybrid/subtle" + gcmpb "github.com/google/tink/go/proto/aes_gcm_go_proto" + commonpb "github.com/google/tink/go/proto/common_go_proto" + tinkpb "github.com/google/tink/go/proto/tink_go_proto" + "github.com/stretchr/testify/require" + + compositepb "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/proto/common_composite_go_proto" + ecdh1pupb "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/proto/ecdh1pu_aead_go_proto" +) + +func TestECDH1PUPublicKeyManager_Primitive(t *testing.T) { + km := newECDH1PUPublicKeyManager() + + t.Run("Test public key manager Primitive() with empty serialized key", func(t *testing.T) { + p, err := km.Primitive([]byte("")) + require.EqualError(t, err, errInvalidECDH1PUAESPublicKey.Error(), + "ECDH1PUPublic primitive from empty serialized key must fail") + require.Empty(t, p) + }) + + t.Run("Test public key manager Primitive() with bad serialize key", func(t *testing.T) { + p, err := km.Primitive([]byte("bad.data")) + require.EqualError(t, err, errInvalidECDH1PUAESPublicKey.Error(), + "ECDH1PUPublic primitive from bad serialized key must fail") + require.Empty(t, p) + }) + + format := &gcmpb.AesGcmKeyFormat{ + KeySize: 32, + } + serializedFormat, err := proto.Marshal(format) + require.NoError(t, err) + + format = &gcmpb.AesGcmKeyFormat{ + KeySize: 99, // bad AES128GCM size + } + + badSerializedFormat, err := proto.Marshal(format) + require.NoError(t, err) + + recipients := generateRecipients(t) + + badRecipients := generateBadRecipients(t) + + var flagTests = []struct { + tcName string + version uint32 + curveType commonpb.EllipticCurveType + ecPtFmt commonpb.EcPointFormat + encTmp *tinkpb.KeyTemplate + recipients []*ecdh1pupb.Ecdh1PuAeadRecipientPublicKey + }{ + { + tcName: "public key manager Primitive() using key with bad version", + version: 9999, + curveType: commonpb.EllipticCurveType_NIST_P256, + ecPtFmt: commonpb.EcPointFormat_COMPRESSED, + encTmp: aead.AES128GCMKeyTemplate(), + recipients: recipients, + }, + { + tcName: "public key manager Primitive() using key with bad curve", + version: 0, + curveType: commonpb.EllipticCurveType_UNKNOWN_CURVE, + ecPtFmt: commonpb.EcPointFormat_COMPRESSED, + encTmp: aead.AES128GCMKeyTemplate(), + recipients: recipients, + }, + { + tcName: "success public key manager Primitive()", + version: 0, + curveType: commonpb.EllipticCurveType_NIST_P256, + ecPtFmt: commonpb.EcPointFormat_UNCOMPRESSED, + encTmp: aead.AES128GCMKeyTemplate(), + recipients: recipients, + }, + { + tcName: "public key manager Primitive() using key with bad key template URL", + version: 0, + curveType: commonpb.EllipticCurveType_NIST_P256, + ecPtFmt: commonpb.EcPointFormat_COMPRESSED, + encTmp: &tinkpb.KeyTemplate{ + TypeUrl: "bad.type/url/value", + Value: serializedFormat, + OutputPrefixType: tinkpb.OutputPrefixType_RAW, + }, + recipients: recipients, + }, + { + tcName: "public key manager Primitive() using key with bad content encryption key size", + version: 0, + curveType: commonpb.EllipticCurveType_NIST_P256, + ecPtFmt: commonpb.EcPointFormat_COMPRESSED, + encTmp: &tinkpb.KeyTemplate{ + TypeUrl: aesGCMTypeURL, + Value: badSerializedFormat, + OutputPrefixType: tinkpb.OutputPrefixType_RAW, + }, + recipients: recipients, + }, + { + tcName: "public key manager Primitive() with bad recipients keys", + version: 0, + curveType: commonpb.EllipticCurveType_NIST_P256, + ecPtFmt: commonpb.EcPointFormat_UNCOMPRESSED, + encTmp: aead.AES128GCMKeyTemplate(), + recipients: badRecipients, + }, + } + + for _, tc := range flagTests { + tt := tc + t.Run("Test "+tt.tcName, func(t *testing.T) { + c := tt.curveType + encT := tt.encTmp + ptFmt := tt.ecPtFmt + v := tt.version + recipientsKeys := tt.recipients + + // temporarily reset curvType if its unknown type so subtle.GetCurve() below doesn't fail + if tt.curveType.String() == commonpb.EllipticCurveType_UNKNOWN_CURVE.String() { + c = commonpb.EllipticCurveType_NIST_P256 + } + + crv, err := hybrid.GetCurve(c.String()) + require.NoError(t, err) + _, x, y, err := elliptic.GenerateKey(crv, rand.Reader) + require.NoError(t, err) + + // set back curvType if it was unknown to proceed with the test + if tt.curveType.String() == commonpb.EllipticCurveType_UNKNOWN_CURVE.String() { + c = tt.curveType + } + + pubKeyProto := &ecdh1pupb.Ecdh1PuAeadPublicKey{ + Version: v, // if v > 0 to force an error when calling km.Primitive() + Params: &ecdh1pupb.Ecdh1PuAeadParams{ + KwParams: &ecdh1pupb.Ecdh1PuKwParams{ + CurveType: c, // unknown curve type to force an error when calling km.Primitive() + Recipients: recipientsKeys, + }, + EncParams: &ecdh1pupb.Ecdh1PuAeadEncParams{ + AeadEnc: encT, // invalid data enc key template to force an error when calling km.Primitive() + }, + EcPointFormat: ptFmt, // unknown EC Pint format type to force an error when calling km.Primitive() + }, + X: x.Bytes(), + Y: y.Bytes(), + } + + sPubKey, err := proto.Marshal(pubKeyProto) + require.NoError(t, err) + + p, err := km.Primitive(sPubKey) + if strings.Contains(tt.tcName, "with bad content encryption key size") { + require.EqualError(t, err, errInvalidECDH1PUAESPublicKey.Error(), + "ECDH1PUPublic primitive from serialized key with invalid serialized key") + require.Empty(t, p) + + return + } + + if strings.Contains(tt.tcName, "success") { + require.NoError(t, err) + require.NotEmpty(t, p) + return + } + + require.Errorf(t, err, tt.tcName) + require.Empty(t, p) + }) + } +} + +func generateBadRecipients(t *testing.T) []*ecdh1pupb.Ecdh1PuAeadRecipientPublicKey { + recipients := generateRecipients(t) + + for _, rec := range recipients { + rec.Version = 999 + } + + return recipients +} + +func generateRecipients(t *testing.T) []*ecdh1pupb.Ecdh1PuAeadRecipientPublicKey { + t.Helper() + + curvProto := commonpb.EllipticCurveType_NIST_P256 + curve, err := hybrid.GetCurve(curvProto.String()) + require.NoError(t, err) + + recipient1Priv, err := hybrid.GenerateECDHKeyPair(curve) + require.NoError(t, err) + + recipient2Priv, err := hybrid.GenerateECDHKeyPair(curve) + require.NoError(t, err) + + recipient3Priv, err := hybrid.GenerateECDHKeyPair(curve) + require.NoError(t, err) + + return []*ecdh1pupb.Ecdh1PuAeadRecipientPublicKey{ + { + Version: 0, + KeyType: compositepb.KeyType_EC, + CurveType: curvProto, + X: recipient1Priv.PublicKey.Point.X.Bytes(), + Y: recipient1Priv.PublicKey.Point.Y.Bytes(), + }, + { + Version: 0, + KeyType: compositepb.KeyType_EC, + CurveType: curvProto, + X: recipient2Priv.PublicKey.Point.X.Bytes(), + Y: recipient2Priv.PublicKey.Point.Y.Bytes(), + }, + { + Version: 0, + KeyType: compositepb.KeyType_EC, + CurveType: curvProto, + X: recipient3Priv.PublicKey.Point.X.Bytes(), + Y: recipient3Priv.PublicKey.Point.Y.Bytes(), + }, + } +} + +func TestEcdh1puPublicKeyManager_DoesSupport(t *testing.T) { + km := newECDH1PUPublicKeyManager() + require.False(t, km.DoesSupport("bad/url")) + require.True(t, km.DoesSupport(ecdh1puAESPublicKeyTypeURL)) +} + +func TestEcdh1puPublicKeyManager_NewKeyAndNewKeyData(t *testing.T) { + km := newECDH1PUPublicKeyManager() + + t.Run("Test public key manager NewKey()", func(t *testing.T) { + k, err := km.NewKey(nil) + require.EqualError(t, err, "ecdh1pu_public_key_manager: NewKey not implemented") + require.Empty(t, k) + }) + + t.Run("Test private key manager NewKeyData()", func(t *testing.T) { + p, err := km.NewKeyData(nil) + require.EqualError(t, err, "ecdh1pu_public_key_manager: NewKeyData not implemented") + require.Empty(t, p) + }) +} diff --git a/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_common.go b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_common.go new file mode 100644 index 0000000000..32d2a083d9 --- /dev/null +++ b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_common.go @@ -0,0 +1,25 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package ecdh1pu + +import ( + "fmt" + + commonpb "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/proto/common_composite_go_proto" +) + +// GetKeyType is a utility function that converts a string type value into an proto KeyType +func GetKeyType(keyType string) (commonpb.KeyType, error) { + switch keyType { + case "EC": + return commonpb.KeyType_EC, nil + case "OKP": + return commonpb.KeyType_OKP, nil + default: + return 0, fmt.Errorf("key type %s not supported", keyType) + } +} diff --git a/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_common_test.go b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_common_test.go new file mode 100644 index 0000000000..05b897cac0 --- /dev/null +++ b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/ecdh1pu_common_test.go @@ -0,0 +1,60 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package ecdh1pu + +import ( + "testing" + + "github.com/stretchr/testify/require" + + compositepb "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/proto/common_composite_go_proto" +) + +func TestGetKeyType(t *testing.T) { + tcs := []struct { + tcName string + keyType string + expectedType compositepb.KeyType + isError bool + }{ + { + "test get EC KeyType", + "EC", + compositepb.KeyType_EC, + false, + }, + { + "test get OKP KeyType", + "OKP", + compositepb.KeyType_OKP, + false, + }, + { + "test get bad KeyType", + "bad", + compositepb.KeyType_UNKNOWN_KEY_TYPE, + true, + }, + } + + for _, tc := range tcs { + tt := tc + + t.Run(tt.tcName, func(t *testing.T) { + kt, err := GetKeyType(tt.keyType) + if tt.isError { + require.Error(t, err) + require.Zero(t, kt) + + return + } + + require.NoError(t, err) + require.EqualValues(t, kt.String(), tt.expectedType.String()) + }) + } +} diff --git a/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/register_ecdh1pu_aead_enc_helper.go b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/register_ecdh1pu_aead_enc_helper.go new file mode 100644 index 0000000000..6e992e51f8 --- /dev/null +++ b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/register_ecdh1pu_aead_enc_helper.go @@ -0,0 +1,204 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package ecdh1pu + +import ( + "fmt" + + "github.com/golang/protobuf/proto" + aead "github.com/google/tink/go/aead/subtle" + "github.com/google/tink/go/core/registry" + gcmpb "github.com/google/tink/go/proto/aes_gcm_go_proto" + chachapb "github.com/google/tink/go/proto/chacha20_poly1305_go_proto" + tinkpb "github.com/google/tink/go/proto/tink_go_proto" + xchachapb "github.com/google/tink/go/proto/xchacha20_poly1305_go_proto" + "github.com/google/tink/go/tink" + "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/poly1305" + + "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/composite/ecdhes/subtle" +) + +const ( + aesGCMTypeURL = "type.googleapis.com/google.crypto.tink.AesGcmKey" + chaCha20Poly1305TypeURL = "type.googleapis.com/google.crypto.tink.ChaCha20Poly1305Key" + xChaCha20Poly1305TypeURL = "type.googleapis.com/google.crypto.tink.XChaCha20Poly1305Key" +) + +// registerECDH1PUAEADEncHelper registers a content encryption helper +type registerECDH1PUAEADEncHelper struct { + encKeyURL string + keyData []byte + symmetricKeySize int + tagSize int + ivSize int +} + +var _ subtle.EncrypterHelper = (*registerECDH1PUAEADEncHelper)(nil) + +// newRegisterECDH1PUAEADEncHelper initializes and returns a registerECDH1PUAEADEncHelper +func newRegisterECDH1PUAEADEncHelper(k *tinkpb.KeyTemplate) (*registerECDH1PUAEADEncHelper, error) { + var ( + keySize, tagSize, ivSize int + skf []byte + err error + ) + + switch k.TypeUrl { + case aesGCMTypeURL: + gcmKeyFormat := new(gcmpb.AesGcmKeyFormat) + + err = proto.Unmarshal(k.Value, gcmKeyFormat) + if err != nil { + return nil, fmt.Errorf("registerECDH1PUAEADEncHelper: failed to unmarshal gcmKeyFormat: %w", err) + } + + keySize = int(gcmKeyFormat.KeySize) + tagSize = aead.AESGCMTagSize + ivSize = aead.AESGCMIVSize + + skf, err = proto.Marshal(gcmKeyFormat) + if err != nil { + return nil, fmt.Errorf("registerECDH1PUAEADEncHelper: failed to serialize key format, error: %w", err) + } + case chaCha20Poly1305TypeURL: + keySize = chacha20poly1305.KeySize + tagSize = poly1305.TagSize + ivSize = chacha20poly1305.NonceSize + case xChaCha20Poly1305TypeURL: + keySize = chacha20poly1305.KeySize + tagSize = poly1305.TagSize + ivSize = chacha20poly1305.NonceSizeX + default: + return nil, fmt.Errorf("registerECDH1PUAEADEncHelper: unsupported AEAD content encryption key type: %s", + k.TypeUrl) + } + + km, err := registry.GetKeyManager(k.TypeUrl) + if err != nil { + return nil, fmt.Errorf("registerECDH1PUAEADEncHelper: failed to fetch KeyManager, error: %w", err) + } + + // skf is nil for (X)Chahcha20Poly1305 km + key, err := km.NewKey(skf) + if err != nil { + return nil, fmt.Errorf("registerECDH1PUAEADEncHelper: failed to fetch key, error: %w", err) + } + + sk, err := proto.Marshal(key) + if err != nil { + return nil, fmt.Errorf("registerECDH1PUAEADEncHelper: failed to serialize key, error: %w", err) + } + + return ®isterECDH1PUAEADEncHelper{ + encKeyURL: k.TypeUrl, + keyData: sk, + symmetricKeySize: keySize, + tagSize: tagSize, + ivSize: ivSize, + }, nil +} + +// GetSymmetricKeySize returns the symmetric key size +func (r *registerECDH1PUAEADEncHelper) GetSymmetricKeySize() int { + return r.symmetricKeySize +} + +// GetTagSize returns the primitive tag size +func (r *registerECDH1PUAEADEncHelper) GetTagSize() int { + return r.tagSize +} + +// GetIVSize returns the primitive IV size +func (r *registerECDH1PUAEADEncHelper) GetIVSize() int { + return r.ivSize +} + +// GetAEAD returns the AEAD primitive from the DEM +func (r *registerECDH1PUAEADEncHelper) GetAEAD(symmetricKeyValue []byte) (tink.AEAD, error) { + if len(symmetricKeyValue) != r.GetSymmetricKeySize() { + return nil, fmt.Errorf("symmetric key has incorrect length") + } + + sk, err := r.getSerializedKey(symmetricKeyValue) + if err != nil { + return nil, err + } + + p, err := registry.Primitive(r.encKeyURL, sk) + if err != nil { + return nil, err + } + + g, ok := p.(tink.AEAD) + if !ok { + return nil, fmt.Errorf("invalid primitive") + } + + return g, nil +} + +func (r *registerECDH1PUAEADEncHelper) getSerializedKey(symmetricKeyValue []byte) ([]byte, error) { + var ( + sk []byte + err error + ) + + switch r.encKeyURL { + case aesGCMTypeURL: + sk, err = r.getSerializedAESGCMKey(symmetricKeyValue) + if err != nil { + return nil, fmt.Errorf("registerECDH1PUAEADEncHelper: failed to serialize key, error: %w", err) + } + case chaCha20Poly1305TypeURL: + chachaKey := new(chachapb.ChaCha20Poly1305Key) + + err = proto.Unmarshal(r.keyData, chachaKey) + if err != nil { + return nil, fmt.Errorf("registerECDH1PUAEADEncHelper: failed to unmarshal chacha key: %w", err) + } + + chachaKey.KeyValue = symmetricKeyValue + + sk, err = proto.Marshal(chachaKey) + if err != nil { + return nil, fmt.Errorf("registerECDH1PUAEADEncHelper: failed to serialize key, error: %w", err) + } + case xChaCha20Poly1305TypeURL: + xChachaKey := new(xchachapb.XChaCha20Poly1305Key) + + err = proto.Unmarshal(r.keyData, xChachaKey) + if err != nil { + return nil, fmt.Errorf("registerECDH1PUAEADEncHelper: failed to unmarshal xchacha key: %w", err) + } + + xChachaKey.KeyValue = symmetricKeyValue + + sk, err = proto.Marshal(xChachaKey) + if err != nil { + return nil, fmt.Errorf("registerECDH1PUAEADEncHelper: failed to serialize key, error: %w", err) + } + default: + return nil, fmt.Errorf("registerECDH1PUAEADEncHelper: unsupported AEAD content encryption key type: %s", + r.encKeyURL) + } + + return sk, err +} + +func (r *registerECDH1PUAEADEncHelper) getSerializedAESGCMKey(symmetricKeyValue []byte) ([]byte, error) { + gcmKey := new(gcmpb.AesGcmKey) + + err := proto.Unmarshal(r.keyData, gcmKey) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal gcmKeyFormat: %w", err) + } + + gcmKey.KeyValue = symmetricKeyValue + + return proto.Marshal(gcmKey) +} diff --git a/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdh1pu_aead_enc_helper.go b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdh1pu_aead_enc_helper.go new file mode 100644 index 0000000000..12f1c82a7c --- /dev/null +++ b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdh1pu_aead_enc_helper.go @@ -0,0 +1,26 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package subtle + +import ( + "github.com/google/tink/go/tink" +) + +// EncrypterHelper is a helper for Content Encryption of composite ECDH-1PU key wrapping + AEAD content encryption +type EncrypterHelper interface { + // GetSymmetricKeySize gives the size of the Encryption key (CEK) in bytes + GetSymmetricKeySize() int + + // GetAEAD returns the newly created AEAD primitive used for the content Encryption + GetAEAD(symmetricKeyValue []byte) (tink.AEAD, error) + + // GetTagSize provides the aead primitive tag size + GetTagSize() int + + // GetIVSize provides the aead primitive nonce size + GetIVSize() int +} diff --git a/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdh1pu_aes_aead_composite_decrypt.go b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdh1pu_aes_aead_composite_decrypt.go new file mode 100644 index 0000000000..3a8e2ab532 --- /dev/null +++ b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdh1pu_aes_aead_composite_decrypt.go @@ -0,0 +1,98 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package subtle + +import ( + "encoding/json" + "fmt" + + hybrid "github.com/google/tink/go/hybrid/subtle" + + "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/composite" + commonpb "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/proto/common_composite_go_proto" +) + +// package subtle provides the core crypto primitives to be used by ECDH-1PU composite primitives. It is intended for +// internal use only. + +// ECDH1PUAEADCompositeDecrypt is an instance of ECDH-1PU decryption with Concat KDF +// and AEAD content decryption +type ECDH1PUAEADCompositeDecrypt struct { + privateKey *hybrid.ECPrivateKey + pointFormat string + encHelper EncrypterHelper + keyType commonpb.KeyType +} + +// NewECDH1PUAEADCompositeDecrypt returns ECDH-ES composite decryption construct with Concat KDF/ECDH-1PU key unwrapping +// and AEAD payload decryption. +func NewECDH1PUAEADCompositeDecrypt(pvt *hybrid.ECPrivateKey, ptFormat string, encHelper EncrypterHelper, + keyType commonpb.KeyType) *ECDH1PUAEADCompositeDecrypt { + return &ECDH1PUAEADCompositeDecrypt{ + privateKey: pvt, + pointFormat: ptFormat, + encHelper: encHelper, + keyType: keyType, + } +} + +// Decrypt using composite ECDH-ES with a Concat KDF key unwrap and AEAD content decryption +func (d *ECDH1PUAEADCompositeDecrypt) Decrypt(ciphertext, aad []byte) ([]byte, error) { + if d.privateKey == nil { + return nil, fmt.Errorf("ECDH1PUAEADCompositeDecrypt: missing recipient private key for key unwrapping") + } + + keySize := d.encHelper.GetSymmetricKeySize() + + var cek []byte + + encData := new(composite.EncryptedData) + + err := json.Unmarshal(ciphertext, encData) + if err != nil { + return nil, err + } + + // TODO: add support for Chacha content encryption https://github.com/hyperledger/aries-framework-go/issues/1684 + switch d.keyType { + case commonpb.KeyType_EC: + if encData.EncAlg != A256GCM { + return nil, fmt.Errorf("invalid content encryption algorihm '%s' for Decrypt()", encData.EncAlg) + } + default: + return nil, fmt.Errorf("invalid key type '%s' for Decrypt()", d.keyType) + } + + for _, rec := range encData.Recipients { + recipientKW := &ECDH1PUConcatKDFRecipientKW{ + recipientPrivateKey: d.privateKey, + } + + // TODO: add support for 25519 key unwrapping https://github.com/hyperledger/aries-framework-go/issues/1637 + cek, err = recipientKW.unwrapKey(rec, keySize) + if err == nil { + break + } + } + + if cek == nil { + return nil, fmt.Errorf("ecdh-es decrypt: cek unwrap failed for all recipients keys") + } + + aead, err := d.encHelper.GetAEAD(cek) + if err != nil { + return nil, err + } + + iv := encData.IV + tag := encData.Tag + ct := encData.Ciphertext + finalCT := append(iv, ct...) + finalCT = append(finalCT, tag...) + + return aead.Decrypt(finalCT, aad) +} diff --git a/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdh1pu_aes_aead_composite_encrypt.go b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdh1pu_aes_aead_composite_encrypt.go new file mode 100644 index 0000000000..8494fb6333 --- /dev/null +++ b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdh1pu_aes_aead_composite_encrypt.go @@ -0,0 +1,201 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package subtle + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "encoding/base64" + "encoding/json" + "fmt" + "math/big" + + hybrid "github.com/google/tink/go/hybrid/subtle" + "github.com/google/tink/go/subtle/random" + "github.com/square/go-jose/v3" + + "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/composite" + "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/composite/api" + commonpb "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/proto/common_composite_go_proto" +) + +// A256GCM is the default content encryption algorithm value as per +// the JWA specification: https://tools.ietf.org/html/rfc7518#section-5.1 +const A256GCM = "A256GCM" + +type marshalFunc func(interface{}) ([]byte, error) + +// ECDH1PUAEADCompositeEncrypt is an instance of ECDH-ES encryption with Concat KDF +// and AEAD content encryption +type ECDH1PUAEADCompositeEncrypt struct { + recPublicKeys []*composite.PublicKey + pointFormat string + encHelper EncrypterHelper + keyType commonpb.KeyType + marshalFunc marshalFunc +} + +var _ api.CompositeEncrypt = (*ECDH1PUAEADCompositeEncrypt)(nil) + +// NewECDH1PUAEADCompositeEncrypt returns ECDH-ES encryption construct with Concat KDF key wrapping +// and AEAD content encryption +func NewECDH1PUAEADCompositeEncrypt(recipientsKeys []*composite.PublicKey, ptFormat string, + encHelper EncrypterHelper, keyType commonpb.KeyType) *ECDH1PUAEADCompositeEncrypt { + return &ECDH1PUAEADCompositeEncrypt{ + recPublicKeys: recipientsKeys, + pointFormat: ptFormat, + encHelper: encHelper, + keyType: keyType, + marshalFunc: json.Marshal, + } +} + +// Encrypt using composite ECDH-1PU with a 1PU KDF key wrap and AEAD content encryption +func (e *ECDH1PUAEADCompositeEncrypt) Encrypt(plaintext, aad []byte) ([]byte, error) { + if len(e.recPublicKeys) == 0 { + return nil, fmt.Errorf("ECDH1PUAEADCompositeEncrypt: missing recipients public keys for key wrapping") + } + + var eAlg, kwAlg string + + // TODO add chacha alg support too, https://github.com/hyperledger/aries-framework-go/issues/1684 + switch e.keyType { + case commonpb.KeyType_EC: + eAlg = A256GCM + kwAlg = A256KWAlg + default: + return nil, fmt.Errorf("ECDH1PUAEADCompositeEncrypt: bad key type: '%s'", e.keyType) + } + + keySize := e.encHelper.GetSymmetricKeySize() + cek := random.GetRandomBytes(uint32(keySize)) + + var recipientsWK []*composite.RecipientWrappedKey + + var singleRecipientAAD []byte + + for _, rec := range e.recPublicKeys { + senderKW := &ECDH1PUConcatKDFSenderKW{ + recipientPublicKey: rec, + cek: cek, + } + + // TODO: add support for 25519 key wrapping https://github.com/hyperledger/aries-framework-go/issues/1637 + kek, err := senderKW.wrapKey(kwAlg, keySize) + if err != nil { + return nil, err + } + + recipientsWK = append(recipientsWK, kek) + + if len(e.recPublicKeys) == 1 { + singleRecipientAAD, err = e.mergeSingleRecipientHeaders(kek, aad) + if err != nil { + return nil, err + } + + aad = singleRecipientAAD + } + } + + aead, err := e.encHelper.GetAEAD(cek) + if err != nil { + return nil, err + } + + ct, err := aead.Encrypt(plaintext, aad) + if err != nil { + return nil, err + } + + return e.buildEncData(eAlg, recipientsWK, ct, singleRecipientAAD) +} + +func (e *ECDH1PUAEADCompositeEncrypt) buildEncData(eAlg string, recipientsWK []*composite.RecipientWrappedKey, + ct, singleRecipientAAD []byte) ([]byte, error) { + tagSize := e.encHelper.GetTagSize() + ivSize := e.encHelper.GetIVSize() + iv := ct[:ivSize] + ctAndTag := ct[ivSize:] + tagOffset := len(ctAndTag) - tagSize + + encData := &composite.EncryptedData{ + EncAlg: eAlg, + Ciphertext: ctAndTag[:tagOffset], + IV: iv, + Tag: ctAndTag[tagOffset:], + Recipients: recipientsWK, + SingleRecipientAAD: singleRecipientAAD, + } + + return e.marshalFunc(encData) +} + +// for single recipient encryption, recipient header info is available in the key, update aad with this info +func (e *ECDH1PUAEADCompositeEncrypt) mergeSingleRecipientHeaders(recipientWK *composite.RecipientWrappedKey, + aad []byte) ([]byte, error) { + newAAD, err := base64.RawURLEncoding.DecodeString(string(aad)) + if err != nil { + return nil, err + } + + rawHeaders := map[string]json.RawMessage{} + + err = json.Unmarshal(newAAD, &rawHeaders) + if err != nil { + return nil, err + } + + kid, err := e.marshalFunc(recipientWK.KID) + if err != nil { + return nil, err + } + + rawHeaders["kid"] = kid + + alg, err := e.marshalFunc(recipientWK.Alg) + if err != nil { + return nil, err + } + + rawHeaders["alg"] = alg + + mEPK, err := convertRecKeyToMarshalledJWK(recipientWK) + if err != nil { + return nil, err + } + + rawHeaders["epk"] = mEPK + + mAAD, err := e.marshalFunc(rawHeaders) + if err != nil { + return nil, err + } + + return []byte(base64.RawURLEncoding.EncodeToString(mAAD)), nil +} + +func convertRecKeyToMarshalledJWK(rec *composite.RecipientWrappedKey) ([]byte, error) { + var c elliptic.Curve + + c, err := hybrid.GetCurve(rec.EPK.Curve) + if err != nil { + return nil, err + } + + recJWK := jose.JSONWebKey{ + KeyID: rec.KID, + Use: "enc", + Key: &ecdsa.PublicKey{ + Curve: c, + X: new(big.Int).SetBytes(rec.EPK.X), + Y: new(big.Int).SetBytes(rec.EPK.Y), + }, + } + + return recJWK.MarshalJSON() +} diff --git a/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdh1pu_aes_aead_composite_test.go b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdh1pu_aes_aead_composite_test.go new file mode 100644 index 0000000000..57caeb9387 --- /dev/null +++ b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdh1pu_aes_aead_composite_test.go @@ -0,0 +1,378 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package subtle + +import ( + "crypto/elliptic" + "encoding/base64" + "encoding/json" + "fmt" + "testing" + + "github.com/google/tink/go/aead" + subtleaead "github.com/google/tink/go/aead/subtle" + hybrid "github.com/google/tink/go/hybrid/subtle" + "github.com/google/tink/go/keyset" + commonpb "github.com/google/tink/go/proto/common_go_proto" + tinkpb "github.com/google/tink/go/proto/tink_go_proto" + "github.com/google/tink/go/tink" + "github.com/stretchr/testify/require" + + "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/composite" + compositepb "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/proto/common_composite_go_proto" +) + +func TestEncryptDecrypt(t *testing.T) { + recipientsPrivKeys, recipientsPubKeys := buildRecipientsKeys(t, 10) + aeadPrimitive := getAEADPrimitive(t, aead.AES256GCMKeyTemplate()) + + mEncHelper := &MockEncHelper{ + KeySizeValue: 32, + AEADValue: aeadPrimitive, + TagSizeValue: subtleaead.AESGCMTagSize, + IVSizeValue: subtleaead.AESGCMIVSize, + } + + cEnc := NewECDH1PUAEADCompositeEncrypt(recipientsPubKeys, commonpb.EcPointFormat_UNCOMPRESSED.String(), + mEncHelper, compositepb.KeyType_EC) + + pt := []byte("secret message") + aad := []byte("aad message") + + ct, err := cEnc.Encrypt(pt, aad) + require.NoError(t, err) + + for _, privKey := range recipientsPrivKeys { + dEnc := NewECDH1PUAEADCompositeDecrypt(privKey, commonpb.EcPointFormat_UNCOMPRESSED.String(), mEncHelper, + compositepb.KeyType_EC) + + dpt, err := dEnc.Decrypt(ct, aad) + require.NoError(t, err) + require.EqualValues(t, pt, dpt) + } +} + +func TestEncryptDecryptNegativeTCs(t *testing.T) { + recipientsPrivKeys, recipientsPubKeys := buildRecipientsKeys(t, 10) + aeadPrimitive := getAEADPrimitive(t, aead.AES256GCMKeyTemplate()) + + mEncHelper := &MockEncHelper{ + KeySizeValue: 32, + AEADValue: aeadPrimitive, + TagSizeValue: subtleaead.AESGCMTagSize, + IVSizeValue: subtleaead.AESGCMIVSize, + } + + pt := []byte("secret message") + aad := []byte("aad message") + + // test with empty recipients public keys + cEnc := NewECDH1PUAEADCompositeEncrypt(nil, commonpb.EcPointFormat_UNCOMPRESSED.String(), + mEncHelper, compositepb.KeyType_EC) + + // Encrypt should fail with empty recipients public keys + _, err := cEnc.Encrypt(pt, aad) + require.EqualError(t, err, "ECDH1PUAEADCompositeEncrypt: missing recipients public keys for key wrapping") + + // test with large key size + mEncHelper.KeySizeValue = 100 + + cEnc = NewECDH1PUAEADCompositeEncrypt(recipientsPubKeys, commonpb.EcPointFormat_UNCOMPRESSED.String(), + mEncHelper, compositepb.KeyType_EC) + + // Encrypt should fail with large AEAD key size value + _, err = cEnc.Encrypt(pt, aad) + require.EqualError(t, err, "crypto/aes: invalid key size 100") + + mEncHelper.KeySizeValue = 32 + + // Encrypt should fail with bad key type + cEnc.keyType = compositepb.KeyType_UNKNOWN_KEY_TYPE + + _, err = cEnc.Encrypt(pt, aad) + require.EqualError(t, err, fmt.Sprintf("ECDH1PUAEADCompositeEncrypt: bad key type: '%s'", + compositepb.KeyType_UNKNOWN_KEY_TYPE)) + + cEnc.keyType = compositepb.KeyType_EC + + // test with GetAEAD() returning error + mEncHelper.AEADErrValue = fmt.Errorf("error from GetAEAD") + + cEnc = NewECDH1PUAEADCompositeEncrypt(recipientsPubKeys, commonpb.EcPointFormat_UNCOMPRESSED.String(), + mEncHelper, compositepb.KeyType_EC) + + // Encrypt should fail with large AEAD key size value + _, err = cEnc.Encrypt(pt, aad) + require.EqualError(t, err, "error from GetAEAD") + + mEncHelper.AEADErrValue = nil + + // create a valid ciphertext to test Decrypt for all recipients + cEnc = NewECDH1PUAEADCompositeEncrypt(recipientsPubKeys, commonpb.EcPointFormat_UNCOMPRESSED.String(), + mEncHelper, compositepb.KeyType_EC) + + // test with empty plaintext + ct, err := cEnc.Encrypt([]byte{}, aad) + require.NoError(t, err) + + encData := new(composite.EncryptedData) + err = json.Unmarshal(ct, encData) + + require.NoError(t, err) + // encrypting empty plaintext should result in empty ciphertext + require.Empty(t, encData.Ciphertext) + require.Len(t, encData.Tag, subtleaead.AESGCMTagSize) + require.Len(t, encData.IV, subtleaead.AESGCMIVSize) + + ct, err = cEnc.Encrypt(pt, aad) + require.NoError(t, err) + + for _, privKey := range recipientsPrivKeys { + // test with nil recipient private key + dEnc := NewECDH1PUAEADCompositeDecrypt(nil, commonpb.EcPointFormat_UNCOMPRESSED.String(), mEncHelper, + compositepb.KeyType_EC) + + _, err = dEnc.Decrypt(ct, aad) + require.EqualError(t, err, "ECDH1PUAEADCompositeDecrypt: missing recipient private key for key"+ + " unwrapping") + + // test with large key size + mEncHelper.KeySizeValue = 100 + dEnc = NewECDH1PUAEADCompositeDecrypt(privKey, commonpb.EcPointFormat_UNCOMPRESSED.String(), mEncHelper, + compositepb.KeyType_EC) + + _, err = dEnc.Decrypt(ct, aad) + require.EqualError(t, err, "ecdh-es decrypt: cek unwrap failed for all recipients keys") + + mEncHelper.KeySizeValue = 32 + + // test with GetAEAD() returning error + mEncHelper.AEADErrValue = fmt.Errorf("error from GetAEAD") + + dEnc = NewECDH1PUAEADCompositeDecrypt(privKey, commonpb.EcPointFormat_UNCOMPRESSED.String(), mEncHelper, + compositepb.KeyType_EC) + + _, err = dEnc.Decrypt(ct, aad) + require.EqualError(t, err, "error from GetAEAD") + + mEncHelper.AEADErrValue = nil + + // create a valid Decrypt message and test against ct + dEnc = NewECDH1PUAEADCompositeDecrypt(privKey, commonpb.EcPointFormat_UNCOMPRESSED.String(), mEncHelper, + compositepb.KeyType_EC) + + // try decrypting empty ct + _, err = dEnc.Decrypt([]byte{}, aad) + require.EqualError(t, err, "unexpected end of JSON input") + + // try decrypting with empty encAlg + var encData composite.EncryptedData + err = json.Unmarshal(ct, &encData) + require.NoError(t, err) + + encData.EncAlg = "" + + emptyAlgCiphertext, err := json.Marshal(encData) + require.NoError(t, err) + + _, err = dEnc.Decrypt(emptyAlgCiphertext, aad) + require.EqualError(t, err, "invalid content encryption algorihm '' for Decrypt()") + + // finally try successful decrypt + dpt, err := dEnc.Decrypt(ct, aad) + require.NoError(t, err) + require.EqualValues(t, pt, dpt) + } +} + +func TestEncryptDecryptWithSingleRecipient(t *testing.T) { + recipientsPrivKeys, recipientsPubKeys := buildRecipientsKeys(t, 1) + aeadPrimitive := getAEADPrimitive(t, aead.AES256GCMKeyTemplate()) + + mEncHelper := &MockEncHelper{ + KeySizeValue: 32, + AEADValue: aeadPrimitive, + TagSizeValue: subtleaead.AESGCMTagSize, + IVSizeValue: subtleaead.AESGCMIVSize, + } + + pt := []byte("secret message") + aad := []byte("aad message") + + // test with single recipient public key + cEnc := NewECDH1PUAEADCompositeEncrypt(recipientsPubKeys, commonpb.EcPointFormat_UNCOMPRESSED.String(), + mEncHelper, compositepb.KeyType_EC) + + // Encrypt should fail with aad not base64URL encoded + _, err := cEnc.Encrypt(pt, aad) + require.EqualError(t, err, "illegal base64 data at input byte 3") + + newAAD := base64.RawURLEncoding.EncodeToString(aad) + + // Encrypt should fail with aad not being a marshalled json + _, err = cEnc.Encrypt(pt, []byte(newAAD)) + require.EqualError(t, err, "invalid character 'a' looking for beginning of value") + + newAAD = base64.RawURLEncoding.EncodeToString([]byte("{\"enc\":\"testAlg\"}")) + + // Encrypt should pass with base64Url encoded a valid json marshaled aad + ct, err := cEnc.Encrypt(pt, []byte(newAAD)) + require.NoError(t, err) + + encData := &composite.EncryptedData{} + err = json.Unmarshal(ct, encData) + require.NoError(t, err) + + for _, privKey := range recipientsPrivKeys { + dEnc := NewECDH1PUAEADCompositeDecrypt(privKey, commonpb.EcPointFormat_UNCOMPRESSED.String(), mEncHelper, + compositepb.KeyType_EC) + + dpt, err := dEnc.Decrypt(ct, encData.SingleRecipientAAD) + require.NoError(t, err) + require.EqualValues(t, pt, dpt) + } +} + +func buildRecipientsKeys(t *testing.T, nbOfRecipients int) ([]*hybrid.ECPrivateKey, []*composite.PublicKey) { + t.Helper() + + var ( + recipientsECPrivKeys []*hybrid.ECPrivateKey + recipientsPubKeys []*composite.PublicKey + ) + + curvProto := commonpb.EllipticCurveType_NIST_P256 + curve, err := hybrid.GetCurve(curvProto.String()) + require.NoError(t, err) + + for i := 0; i < nbOfRecipients; i++ { + recipientPriv, err := hybrid.GenerateECDHKeyPair(curve) + require.NoError(t, err) + + recipientPub := &recipientPriv.PublicKey + + recipientsECPrivKeys = append(recipientsECPrivKeys, recipientPriv) + recipientsPubKeys = append(recipientsPubKeys, &composite.PublicKey{ + Type: compositepb.KeyType_EC.String(), + Curve: recipientPub.Curve.Params().Name, + X: recipientPub.Point.X.Bytes(), + Y: recipientPub.Point.Y.Bytes(), + }) + } + + return recipientsECPrivKeys, recipientsPubKeys +} + +func getAEADPrimitive(t *testing.T, kt *tinkpb.KeyTemplate) tink.AEAD { + t.Helper() + + kh, err := keyset.NewHandle(kt) + require.NoError(t, err) + + ps, err := kh.Primitives() + require.NoError(t, err) + + p, ok := (ps.Primary.Primitive).(tink.AEAD) + require.True(t, ok) + + return p +} + +// MockEncHelper an mocked AEAD helper of Composite Encrypt/Decrypt primitives +type MockEncHelper struct { + KeySizeValue int + AEADValue tink.AEAD + AEADErrValue error + TagSizeValue int + IVSizeValue int +} + +// GetSymmetricKeySize gives the size of the Encryption key (CEK) in bytes +func (me *MockEncHelper) GetSymmetricKeySize() int { + return me.KeySizeValue +} + +// GetAEAD returns the newly created AEAD primitive used for the content Encryption +func (me *MockEncHelper) GetAEAD(symmetricKeyValue []byte) (tink.AEAD, error) { + return me.AEADValue, me.AEADErrValue +} + +// GetTagSize provides the aead primitive tag size +func (me *MockEncHelper) GetTagSize() int { + return me.TagSizeValue +} + +// GetIVSize provides the aead primitive nonce size +func (me *MockEncHelper) GetIVSize() int { + return me.IVSizeValue +} + +func TestMergeSingleRecipientsHeadersFailureWithUnsetCurve(t *testing.T) { + aad := map[string]string{"enc": "test"} + + mAAD, err := json.Marshal(aad) + require.NoError(t, err) + + wk := &composite.RecipientWrappedKey{ + EPK: composite.PublicKey{}, + } + + cEnc := NewECDH1PUAEADCompositeEncrypt(nil, "", nil, 0) + + // fail with epk curve not set + _, err = cEnc.mergeSingleRecipientHeaders(wk, []byte(base64.RawURLEncoding.EncodeToString(mAAD))) + require.EqualError(t, err, "unsupported curve") + + // set epk curve for subsequent tests + wk.EPK.Curve = elliptic.P256().Params().Name + + fm := &failingMarshaller{ + numTimesMarshalCalledBeforeReturnErr: 0, + } + + cEnc.marshalFunc = fm.failingMarshal + + // fail KID marshalling + _, err = cEnc.mergeSingleRecipientHeaders(wk, []byte(base64.RawURLEncoding.EncodeToString(mAAD))) + require.EqualError(t, err, errFailingMarshal.Error()) + + fm = &failingMarshaller{ + numTimesMarshalCalledBeforeReturnErr: 1, + } + + cEnc.marshalFunc = fm.failingMarshal + + // fail Alg marshalling + _, err = cEnc.mergeSingleRecipientHeaders(wk, []byte(base64.RawURLEncoding.EncodeToString(mAAD))) + require.EqualError(t, err, errFailingMarshal.Error()) + + fm = &failingMarshaller{ + numTimesMarshalCalledBeforeReturnErr: 2, + } + + cEnc.marshalFunc = fm.failingMarshal + // fail EPK marshalling + _, err = cEnc.mergeSingleRecipientHeaders(wk, []byte(base64.RawURLEncoding.EncodeToString(mAAD))) + require.EqualError(t, err, errFailingMarshal.Error()) +} + +var errFailingMarshal = fmt.Errorf("json marshal error") + +type failingMarshaller struct { + numTimesMarshalCalled int + numTimesMarshalCalledBeforeReturnErr int +} + +func (m *failingMarshaller) failingMarshal(v interface{}) ([]byte, error) { + if m.numTimesMarshalCalled == m.numTimesMarshalCalledBeforeReturnErr { + return nil, errFailingMarshal + } + + m.numTimesMarshalCalled++ + + return nil, nil +} diff --git a/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdhes_1pukdf_recipient_kuw.go b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdhes_1pukdf_recipient_kuw.go new file mode 100644 index 0000000000..498bf836c3 --- /dev/null +++ b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdhes_1pukdf_recipient_kuw.go @@ -0,0 +1,65 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package subtle + +import ( + "crypto/aes" + "crypto/ecdsa" + "fmt" + "math/big" + + hybrid "github.com/google/tink/go/hybrid/subtle" + josecipher "github.com/square/go-jose/v3/cipher" + + "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/composite" +) + +// ECDH1PUConcatKDFRecipientKW represents concat KDF based ECDH-1PU (One-Pass Unified Model) KW (key wrapping) +// for ECDH-1PU recipient's unwrapping of CEK +type ECDH1PUConcatKDFRecipientKW struct { + recipientPrivateKey *hybrid.ECPrivateKey +} + +// unwrapKey will do ECDH-ES key unwrapping +func (s *ECDH1PUConcatKDFRecipientKW) unwrapKey(recWK *composite.RecipientWrappedKey, keySize int) ([]byte, error) { + if recWK == nil { + return nil, fmt.Errorf("unwrapKey: RecipientWrappedKey is empty") + } + + // TODO: add support for 25519 key wrapping https://github.com/hyperledger/aries-framework-go/issues/1637 + + recPrivKey := &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{ + Curve: s.recipientPrivateKey.PublicKey.Curve, + X: s.recipientPrivateKey.PublicKey.Point.X, + Y: s.recipientPrivateKey.PublicKey.Point.Y, + }, + D: s.recipientPrivateKey.D, + } + + epkCurve, err := hybrid.GetCurve(recWK.EPK.Curve) + if err != nil { + return nil, err + } + + epkPubKey := &ecdsa.PublicKey{ + Curve: epkCurve, + X: new(big.Int).SetBytes(recWK.EPK.X), + Y: new(big.Int).SetBytes(recWK.EPK.Y), + } + + // TODO replace below calls with new 1PU key derivation algorithm + // DeriveECDHES checks if keys are on the same curve + kek := josecipher.DeriveECDHES(recWK.Alg, []byte{}, []byte{}, recPrivKey, epkPubKey, keySize) + + block, err := aes.NewCipher(kek) + if err != nil { + return nil, err + } + + return josecipher.KeyUnwrap(block, recWK.EncryptedCEK) +} diff --git a/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdhes_1pukdf_sender_kw.go b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdhes_1pukdf_sender_kw.go new file mode 100644 index 0000000000..e059c74fa2 --- /dev/null +++ b/pkg/crypto/tinkcrypto/primitive/composite/ecdh1pu/subtle/ecdhes_1pukdf_sender_kw.go @@ -0,0 +1,77 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package subtle + +import ( + "crypto/aes" + "crypto/ecdsa" + "crypto/rand" + "math/big" + + hybrid "github.com/google/tink/go/hybrid/subtle" + josecipher "github.com/square/go-jose/v3/cipher" + + "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/composite" + commonpb "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/proto/common_composite_go_proto" +) + +// A256KWAlg is the ECDH-1PU key wrapping algorithm +const A256KWAlg = "ECDH-1PU+A256KW" + +// ECDH1PUConcatKDFSenderKW represents concat KDF based ECDH-1PU KW (key wrapping) +// for ECDH-1PU sender +type ECDH1PUConcatKDFSenderKW struct { + recipientPublicKey *composite.PublicKey + cek []byte +} + +// wrapKey will do ECDH-1PU key wrapping +func (s *ECDH1PUConcatKDFSenderKW) wrapKey(kwAlg string, keySize int) (*composite.RecipientWrappedKey, error) { + // TODO: add support for 25519 key wrapping https://github.com/hyperledger/aries-framework-go/issues/1637 + keyType := commonpb.KeyType_EC.String() + + c, err := hybrid.GetCurve(s.recipientPublicKey.Curve) + if err != nil { + return nil, err + } + + recPubKey := &ecdsa.PublicKey{ + Curve: c, + X: new(big.Int).SetBytes(s.recipientPublicKey.X), + Y: new(big.Int).SetBytes(s.recipientPublicKey.Y), + } + + ephemeralPriv, err := ecdsa.GenerateKey(recPubKey.Curve, rand.Reader) + if err != nil { + return nil, err + } + + // TODO replace below key derivation/wrapping with 1PU algorithm + kek := josecipher.DeriveECDHES(kwAlg, []byte{}, []byte{}, ephemeralPriv, recPubKey, keySize) + + block, err := aes.NewCipher(kek) + if err != nil { + return nil, err + } + + wk, err := josecipher.KeyWrap(block, s.cek) + if err != nil { + return nil, err + } + + return &composite.RecipientWrappedKey{ + KID: s.recipientPublicKey.KID, + EncryptedCEK: wk, + EPK: composite.PublicKey{ + X: ephemeralPriv.PublicKey.X.Bytes(), + Y: ephemeralPriv.PublicKey.Y.Bytes(), + Curve: ephemeralPriv.PublicKey.Curve.Params().Name, + Type: keyType, + }, + Alg: kwAlg, + }, nil +}