From bdd3d550759678f3f233429d920e1e15e8a71437 Mon Sep 17 00:00:00 2001 From: Baha <29608896+Baha-sk@users.noreply.github.com> Date: Thu, 14 Jan 2021 08:55:52 -0500 Subject: [PATCH] feat: X25519 ECDH export pub key (#2448) --- pkg/crypto/tinkcrypto/crypto.go | 2 +- pkg/crypto/tinkcrypto/key_wrapper.go | 2 +- .../subtle/ecdh_aes_aead_composite_test.go | 12 +- .../composite/keyio/composite_key_export.go | 116 +++++++++++++----- .../keyio/composite_key_export_import_test.go | 58 +++++++-- 5 files changed, 147 insertions(+), 43 deletions(-) diff --git a/pkg/crypto/tinkcrypto/crypto.go b/pkg/crypto/tinkcrypto/crypto.go index 4705cd8af7..af790f2053 100644 --- a/pkg/crypto/tinkcrypto/crypto.go +++ b/pkg/crypto/tinkcrypto/crypto.go @@ -270,7 +270,7 @@ func (t *Crypto) UnwrapKey(recWK *cryptoapi.RecipientWrappedKey, kh interface{}, return nil, fmt.Errorf("unwrapKey: %w", err) } - // TODO: add support for 25519 key wrapping https://github.com/hyperledger/aries-framework-go/issues/1637 + // TODO: add support for 25519 key wrapping https://github.com/hyperledger/aries-framework-go/issues/2445 recPrivKey := hybridECPrivToECDSAKey(recipientPrivateKey) epkCurve, err := t.kw.getCurve(recWK.EPK.Curve) diff --git a/pkg/crypto/tinkcrypto/key_wrapper.go b/pkg/crypto/tinkcrypto/key_wrapper.go index 88c3259947..4bb8f272e2 100644 --- a/pkg/crypto/tinkcrypto/key_wrapper.go +++ b/pkg/crypto/tinkcrypto/key_wrapper.go @@ -84,7 +84,7 @@ func (t *Crypto) deriveKEKAndWrap(cek, apu, apv []byte, senderKH interface{}, ep recPubKey *ecdsa.PublicKey, recKID string) (*cryptoapi.RecipientWrappedKey, error) { var kek []byte - // TODO: add support for 25519 key wrapping https://github.com/hyperledger/aries-framework-go/issues/1637 + // TODO: add support for 25519 key wrapping https://github.com/hyperledger/aries-framework-go/issues/2445 keyType := ecdhpb.KeyType_EC.String() wrappingAlg := ECDHESA256KWAlg diff --git a/pkg/crypto/tinkcrypto/primitive/composite/ecdh/subtle/ecdh_aes_aead_composite_test.go b/pkg/crypto/tinkcrypto/primitive/composite/ecdh/subtle/ecdh_aes_aead_composite_test.go index a706a27f88..0c687219d7 100644 --- a/pkg/crypto/tinkcrypto/primitive/composite/ecdh/subtle/ecdh_aes_aead_composite_test.go +++ b/pkg/crypto/tinkcrypto/primitive/composite/ecdh/subtle/ecdh_aes_aead_composite_test.go @@ -67,12 +67,17 @@ func TestEncryptDecryptNegativeTCs(t *testing.T) { cEnc := NewECDHAEADCompositeEncrypt(mEncHelper, cek) - // Encrypt should fail with large AEAD key size value + // Encrypt should fail with AEAD error value _, err := cEnc.Encrypt(pt, aad) require.EqualError(t, err, "error from GetAEAD") mEncHelper.AEADErrValue = nil + // Encrypt should fail with nil cek + cEncNilCEK := NewECDHAEADCompositeEncrypt(mEncHelper, nil) + _, err = cEncNilCEK.Encrypt(pt, aad) + require.EqualError(t, err, "ecdhAEADCompositeEncrypt: missing cek") + // create a valid ciphertext to test Decrypt for all recipients cEnc = NewECDHAEADCompositeEncrypt(mEncHelper, cek) @@ -102,6 +107,11 @@ func TestEncryptDecryptNegativeTCs(t *testing.T) { mEncHelper.AEADErrValue = nil + // Decrypt should fail with nil cek + dEncNilCEK := NewECDHAEADCompositeDecrypt(mEncHelper, nil) + _, err = dEncNilCEK.Decrypt(ct, aad) + require.EqualError(t, err, "ecdh decrypt: missing cek") + // create a valid Decrypt message and test against ct dEnc = NewECDHAEADCompositeDecrypt(mEncHelper, cek) diff --git a/pkg/crypto/tinkcrypto/primitive/composite/keyio/composite_key_export.go b/pkg/crypto/tinkcrypto/primitive/composite/keyio/composite_key_export.go index 56f396f3c7..45cad9fddc 100644 --- a/pkg/crypto/tinkcrypto/primitive/composite/keyio/composite_key_export.go +++ b/pkg/crypto/tinkcrypto/primitive/composite/keyio/composite_key_export.go @@ -30,14 +30,20 @@ import ( // key (aka PublicKeyToHandle to be used as a valid Tink key) const ( - ecdhAESPublicKeyTypeURL = "type.hyperledger.org/hyperledger.aries.crypto.tink.EcdhNistPKwAesAeadPublicKey" - ecdhXChachaPublicKeyTypeURL = "type.hyperledger.org/hyperledger.aries.crypto.tink.EcdhX25519KwAesAeadPublicKey" // nolint:lll + ecdhNISTPAESPublicKeyTypeURL = "type.hyperledger.org/hyperledger.aries.crypto.tink.EcdhNistPKwAesAeadPublicKey" + ecdhNISTPXChachaPublicKeyTypeURL = "type.hyperledger.org/hyperledger.aries.crypto.tink.EcdhNistPKwXChachaAeadPublicKey" // nolint:lll + ecdhX25519AESPublicKeyTypeURL = "type.hyperledger.org/hyperledger.aries.crypto.tink.EcdhX25519KwAesAeadPublicKey" + ecdhX25519XChachaPublicKeyTypeURL = "type.hyperledger.org/hyperledger.aries.crypto.tink.EcdhX25519KwXChachaAeadPublicKey" // nolint:lll ) // PubKeyWriter will write the raw bytes of a Tink KeySet's primary public key. The raw bytes are a marshaled // composite.VerificationMethod type. -// The keyset must have a keyURL value equal to `ecdhAESPublicKeyTypeURL` or `ecdhXChachaPublicKeyTypeURL` constant of -// ecdh package. +// The keyset must have a keyURL value equal to either one of the public key URLs: +// - `ecdhNISTPAESPublicKeyTypeURL` +// - `ecdhX25519AESPublicKeyTypeURL` +// - `ecdhNISTPXChachaPublicKeyTypeURL` +// - `ecdhX25519XChachaPublicKeyTypeURL` +// constants of ecdh package. // Note: This writer should be used only for ECDH public key exports. Other export of public keys should be // called via localkms package. type PubKeyWriter struct { @@ -112,16 +118,12 @@ func protoToCompositeKey(keyData *tinkpb.KeyData) (*cryptoapi.PublicKey, error) ) switch keyData.TypeUrl { - case ecdhAESPublicKeyTypeURL: + case ecdhNISTPAESPublicKeyTypeURL, ecdhNISTPXChachaPublicKeyTypeURL, ecdhX25519AESPublicKeyTypeURL, + ecdhX25519XChachaPublicKeyTypeURL: cKey, err = newECDHKey(keyData.Value) if err != nil { return nil, err } - case ecdhXChachaPublicKeyTypeURL: - cKey, err = newECDHXChachaKey(keyData.Value) - if err != nil { - return nil, err - } default: return nil, fmt.Errorf("can't export key with keyURL:%s", keyData.TypeUrl) } @@ -130,7 +132,6 @@ func protoToCompositeKey(keyData *tinkpb.KeyData) (*cryptoapi.PublicKey, error) } func buildKey(c compositeKeyGetter) (*cryptoapi.PublicKey, error) { - // TODO build XChacha key differently curveName := c.curveName() keyTypeName := c.keyType() @@ -138,10 +139,20 @@ func buildKey(c compositeKeyGetter) (*cryptoapi.PublicKey, error) { } func buildCompositeKey(kid, keyType, curve string, x, y []byte) (*cryptoapi.PublicKey, error) { - // validate curve - _, err := hybrid.GetCurve(curve) - if err != nil { - return nil, fmt.Errorf("undefined curve: %w", err) + // validate keyType and curve + switch keyType { + case ecdhpb.KeyType_EC.String(): + // validate NIST P curves + _, err := hybrid.GetCurve(curve) + if err != nil { + return nil, fmt.Errorf("undefined EC curve: %w", err) + } + case ecdhpb.KeyType_OKP.String(): + if curve != commonpb.EllipticCurveType_CURVE25519.String() { + return nil, fmt.Errorf("invalid OKP curve: %s", curve) + } + default: + return nil, fmt.Errorf("invalid keyType: %s", keyType) } return &cryptoapi.PublicKey{ @@ -173,11 +184,6 @@ func newECDHKey(mKey []byte) (compositeKeyGetter, error) { return nil, err } - // validate key type - if pubKeyProto.Params.KwParams.KeyType != ecdhpb.KeyType_EC { - return nil, fmt.Errorf("undefined key type: '%s'", pubKeyProto.Params.KwParams.KeyType) - } - return &ecdhKey{protoKey: pubKeyProto}, nil } @@ -241,23 +247,49 @@ func writePubKeyFromKeyHandle(handle *keyset.Handle) ([]byte, error) { // PublicKeyToKeysetHandle converts pubKey into a *keyset.Handle where pubKey could be either a sender or a recipient // key. The resulting handle cannot be directly used for primitive execution as the cek is not set. This function serves -// as a helper to get a senderKH to be used as an option for ECDH execution (for ECDH-1PU/authcrypt). +// as a helper to get a senderKH to be used as an option for ECDH execution (for ECDH-1PU/authcrypt). The keyset handle +// will be set with AES256-GCM AEAD key template for content encryption. func PublicKeyToKeysetHandle(pubKey *cryptoapi.PublicKey) (*keyset.Handle, error) { + return publicKeyWithEncTemplateToKeysetHandle(pubKey, true) +} + +// PublicKeyToKeysetHandleXChacha converts pubKey into a *keyset.Handle where pubKey could be either a sender or a +// recipient key. The resulting handle cannot be directly used for primitive execution as the cek is not set. This +// as a helper to get a senderKH to be used as an option for ECDH execution (for ECDH-1PU/authcrypt). The keyset handle +// will be set with XChacha20Poly1305 AEAD key template for content encryption. +func PublicKeyToKeysetHandleXChacha(pubKey *cryptoapi.PublicKey) (*keyset.Handle, error) { + return publicKeyWithEncTemplateToKeysetHandle(pubKey, false) +} + +func publicKeyWithEncTemplateToKeysetHandle(pubKey *cryptoapi.PublicKey, isAES bool) (*keyset.Handle, error) { // validate curve cp, err := getCurveProto(pubKey.Curve) if err != nil { return nil, fmt.Errorf("publicKeyToKeysetHandle: failed to convert curve string to proto: %w", err) } + kt, err := getKeyType(pubKey.Type) + if err != nil { + return nil, fmt.Errorf("publicKeyToKeysetHandle: failed to convert key type to proto: %w", err) + } + + encT := aead.AES256GCMKeyTemplate() + + if !isAES { + encT = aead.XChaCha20Poly1305KeyTemplate() + } + + keyURL := getKeyURL(isAES, kt) + protoKey := &ecdhpb.EcdhAeadPublicKey{ Version: 0, Params: &ecdhpb.EcdhAeadParams{ KwParams: &ecdhpb.EcdhKwParams{ CurveType: cp, - KeyType: ecdhpb.KeyType_EC, + KeyType: kt, }, EncParams: &ecdhpb.EcdhAeadEncParams{ - AeadEnc: aead.AES256GCMKeyTemplate(), + AeadEnc: encT, }, EcPointFormat: commonpb.EcPointFormat_UNCOMPRESSED, }, @@ -271,7 +303,7 @@ func PublicKeyToKeysetHandle(pubKey *cryptoapi.PublicKey) (*keyset.Handle, error return nil, fmt.Errorf("publicKeyToKeysetHandle: failed to marshal proto: %w", err) } - ks := newKeySet(ecdhAESPublicKeyTypeURL, marshalledKey, tinkpb.KeyData_ASYMMETRIC_PUBLIC) + ks := newKeySet(keyURL, marshalledKey, tinkpb.KeyData_ASYMMETRIC_PUBLIC) memReader := &keyset.MemReaderWriter{Keyset: ks} @@ -283,6 +315,22 @@ func PublicKeyToKeysetHandle(pubKey *cryptoapi.PublicKey) (*keyset.Handle, error return parsedHandle, nil } +func getKeyURL(aes bool, kt ecdhpb.KeyType) string { + if aes { + if kt == ecdhpb.KeyType_EC { + return ecdhNISTPAESPublicKeyTypeURL + } + + return ecdhX25519AESPublicKeyTypeURL + } + + if kt == ecdhpb.KeyType_EC { + return ecdhNISTPXChachaPublicKeyTypeURL + } + + return ecdhX25519XChachaPublicKeyTypeURL +} + func getCurveProto(c string) (commonpb.EllipticCurveType, error) { switch c { case "secp256r1", "NIST_P256", "P-256", "EllipticCurveType_NIST_P256": @@ -291,8 +339,21 @@ func getCurveProto(c string) (commonpb.EllipticCurveType, error) { return commonpb.EllipticCurveType_NIST_P384, nil case "secp521r1", "NIST_P521", "P-521", "EllipticCurveType_NIST_P521": return commonpb.EllipticCurveType_NIST_P521, nil + case commonpb.EllipticCurveType_CURVE25519.String(): + return commonpb.EllipticCurveType_CURVE25519, nil default: - return 0, errors.New("unsupported curve") + return commonpb.EllipticCurveType_UNKNOWN_CURVE, errors.New("unsupported curve") + } +} + +func getKeyType(k string) (ecdhpb.KeyType, error) { + switch k { + case ecdhpb.KeyType_EC.String(): + return ecdhpb.KeyType_EC, nil + case ecdhpb.KeyType_OKP.String(): + return ecdhpb.KeyType_OKP, nil + default: + return ecdhpb.KeyType_UNKNOWN_KEY_TYPE, errors.New("unsupported key type") } } @@ -316,8 +377,3 @@ func newKeySet(tURL string, marshalledKey []byte, keyMaterialType tinkpb.KeyData PrimaryKeyId: 1, } } - -func newECDHXChachaKey(mKey []byte) (compositeKeyGetter, error) { - // TODO implement - return nil, nil -} diff --git a/pkg/crypto/tinkcrypto/primitive/composite/keyio/composite_key_export_import_test.go b/pkg/crypto/tinkcrypto/primitive/composite/keyio/composite_key_export_import_test.go index b286de0f59..4707b2e72f 100644 --- a/pkg/crypto/tinkcrypto/primitive/composite/keyio/composite_key_export_import_test.go +++ b/pkg/crypto/tinkcrypto/primitive/composite/keyio/composite_key_export_import_test.go @@ -10,6 +10,7 @@ import ( "bytes" "encoding/json" "errors" + "strings" "testing" "github.com/golang/protobuf/proto" @@ -30,17 +31,37 @@ func TestPubKeyExport(t *testing.T) { keyTemplate *tinkpb.KeyTemplate }{ { - tcName: "export then read AES256GCM with ECDHES P-256 public recipient key", + tcName: "export then read AES256GCM with ECDH NIST P-256 public recipient key", keyTemplate: ecdh.ECDH256KWAES256GCMKeyTemplate(), }, { - tcName: "export then read AES256GCM with ECDHES P-384 public recipient key", + tcName: "export then read AES256GCM with ECDH NIST P-384 public recipient key", keyTemplate: ecdh.ECDH384KWAES256GCMKeyTemplate(), }, { - tcName: "export then read AES256GCM with ECDHES P-521 public recipient key", + tcName: "export then read AES256GCM with ECDH NIST P-521 public recipient key", keyTemplate: ecdh.ECDH521KWAES256GCMKeyTemplate(), }, + { + tcName: "export then read XChacha20Poly1305 with ECDH NIST P-256 public recipient key", + keyTemplate: ecdh.ECDH256KWXChachaKeyTemplate(), + }, + { + tcName: "export then read XChacha20Poly1305 with ECDH NIST P-384 public recipient key", + keyTemplate: ecdh.ECDH384KWXChachaKeyTemplate(), + }, + { + tcName: "export then read XChacha20Poly1305 with ECDH NIST P-521 public recipient key", + keyTemplate: ecdh.ECDH521KWXChachaKeyTemplate(), + }, + { + tcName: "export then read AES256GCM with X25519 public recipient key", + keyTemplate: ecdh.X25519AES256GCMECDHKeyTemplate(), + }, + { + tcName: "export then read XChacha20Poly1305 with X25519 public recipient key", + keyTemplate: ecdh.X25519XChachaECDHKeyTemplate(), + }, } for _, tc := range flagTests { @@ -62,13 +83,25 @@ func TestPubKeyExport(t *testing.T) { require.EqualValues(t, ecPubKey, extractedPubKey) - // now convert back ecPubKey to *keyset.Handle + // now convert back ecPubKey to *keyset.Handle using default AES content encryption template xPubKH, err := PublicKeyToKeysetHandle(ecPubKey) require.NoError(t, err) + // now convert back ecPubKey to *keyset.Handle using XChacha content encryption template + x2PubKH, err := PublicKeyToKeysetHandleXChacha(ecPubKey) + require.NoError(t, err) + xk, err := ExtractPrimaryPublicKey(xPubKH) require.NoError(t, err) require.EqualValues(t, ecPubKey, xk) + + x2k, err := ExtractPrimaryPublicKey(x2PubKH) + require.NoError(t, err) + require.EqualValues(t, ecPubKey, x2k) + if strings.Contains(tt.keyTemplate.TypeUrl, "X25519Kw") { + require.EqualValues(t, x2k.Curve, commonpb.EllipticCurveType_CURVE25519.String()) + require.EqualValues(t, xk.Curve, commonpb.EllipticCurveType_CURVE25519.String()) + } }) } } @@ -106,9 +139,14 @@ func TestNegativeCases(t *testing.T) { require.Empty(t, exportedKeyBytes) }) - t.Run("test buildCompositeKey() with bad curve", func(t *testing.T) { - _, err := buildCompositeKey("", "", "BAD", nil, nil) - require.EqualError(t, err, "undefined curve: unsupported curve") + t.Run("test buildCompositeKey() with bad EC curve", func(t *testing.T) { + _, err := buildCompositeKey("", ecdhpb.KeyType_EC.String(), "BAD", nil, nil) + require.EqualError(t, err, "undefined EC curve: unsupported curve") + }) + + t.Run("test buildCompositeKey() with bad OKP curve", func(t *testing.T) { + _, err := buildCompositeKey("", ecdhpb.KeyType_OKP.String(), "BAD", nil, nil) + require.EqualError(t, err, "invalid OKP curve: BAD") }) t.Run("test protoToCompositeKey() with bad key type", func(t *testing.T) { @@ -129,11 +167,11 @@ func TestNegativeCases(t *testing.T) { require.NoError(t, err) _, err = protoToCompositeKey(&tinkpb.KeyData{ - TypeUrl: ecdhAESPublicKeyTypeURL, + TypeUrl: ecdhNISTPAESPublicKeyTypeURL, Value: mKey, KeyMaterialType: 0, }) - require.EqualError(t, err, "undefined key type: 'UNKNOWN_KEY_TYPE'") + require.EqualError(t, err, "invalid keyType: UNKNOWN_KEY_TYPE") }) t.Run("test WriteEncrypted() should fail since it's not supported by Writer", func(t *testing.T) { @@ -182,7 +220,7 @@ func TestNegativeCases(t *testing.T) { Key: []*tinkpb.Keyset_Key{ { KeyData: &tinkpb.KeyData{ - TypeUrl: ecdhAESPublicKeyTypeURL, + TypeUrl: ecdhNISTPAESPublicKeyTypeURL, Value: mKey, KeyMaterialType: 0, },