From c5c0f56b72bc0f195308878b8839c8c840418622 Mon Sep 17 00:00:00 2001 From: Baha Shaaban Date: Mon, 25 Jan 2021 16:23:31 -0500 Subject: [PATCH] feat: add KMS key type for X25519 key This is the last change about X25519 keys for ECDH KW. It updates the old ECDH key types into the new type names and add the X25519 key type as well. It includes JWE encryption/decryption updates to support XC20P content encryption along with recipients kw using both NIST P curved keys and X25519 keys. Also part of this change is the removal of remnant code from legacyKMS which was removed from the framework last year. closes #2447 closes #1684 closes #815 closes #475 closes #596 also part of #857 Signed-off-by: Baha Shaaban --- pkg/didcomm/packager/package_test.go | 12 +- pkg/didcomm/packer/anoncrypt/pack_test.go | 85 ++-- pkg/didcomm/packer/authcrypt/pack_test.go | 108 +++-- pkg/doc/jose/common.go | 2 + pkg/doc/jose/decrypter.go | 31 +- pkg/doc/jose/encrypt_test.go | 12 +- pkg/doc/jose/encrypter.go | 85 ++-- pkg/doc/jose/encrypter_decrypter_test.go | 473 ++++++++++++++-------- pkg/doc/jose/jwe.go | 1 - pkg/doc/jose/jwk.go | 107 +++++ pkg/doc/jose/jwk_test.go | 51 +++ pkg/doc/util/jwkkid/kid_creator.go | 72 +++- pkg/doc/util/jwkkid/kid_creator_test.go | 63 ++- pkg/internal/cryptoutil/utils.go | 131 +----- pkg/internal/cryptoutil/utils_test.go | 61 +-- pkg/kms/api.go | 28 +- pkg/kms/localkms/kid_creator_test.go | 23 +- pkg/kms/localkms/localkms.go | 8 +- pkg/kms/localkms/localkms_test.go | 9 +- pkg/kms/localkms/pubkey_writer.go | 12 +- 20 files changed, 877 insertions(+), 497 deletions(-) diff --git a/pkg/didcomm/packager/package_test.go b/pkg/didcomm/packager/package_test.go index 77411958d..e7e9a781c 100644 --- a/pkg/didcomm/packager/package_test.go +++ b/pkg/didcomm/packager/package_test.go @@ -138,7 +138,7 @@ func TestBaseKMSInPackager_UnpackMessage(t *testing.T) { require.NoError(t, err) // fromKey is stored in the KMS - fromKID, fromKey, err := customKMS.CreateAndExportPubKeyBytes(kms.ECDH256KWAES256GCMType) + fromKID, fromKey, err := customKMS.CreateAndExportPubKeyBytes(kms.NISTP256ECDHKWType) require.NoError(t, err) // for authcrypt, sender key should be in third party store, must use base58 wrapped store to match kms store. @@ -149,7 +149,7 @@ func TestBaseKMSInPackager_UnpackMessage(t *testing.T) { require.NoError(t, err) // toVerKey is stored in the KMS as well - toKID, toKey, err := customKMS.CreateAndExportPubKeyBytes(kms.ECDH256KWAES256GCM) + toKID, toKey, err := customKMS.CreateAndExportPubKeyBytes(kms.NISTP256ECDHKW) require.NoError(t, err) // PackMessage should pass with both value from and to keys @@ -207,10 +207,10 @@ func TestBaseKMSInPackager_UnpackMessage(t *testing.T) { require.NoError(t, err) // use ECDH1PU type as we are using a sender key (ie: packer's FromKey is not empty aka authcrypt) - fromKID, _, err := customKMS.Create(kms.ECDH384KWAES256GCMType) + fromKID, _, err := customKMS.Create(kms.NISTP384ECDHKWType) require.NoError(t, err) - _, toKey, err := customKMS.CreateAndExportPubKeyBytes(kms.ECDH384KWAES256GCMType) + _, toKey, err := customKMS.CreateAndExportPubKeyBytes(kms.NISTP384ECDHKWType) require.NoError(t, err) // try pack with nil envelope - should fail @@ -282,10 +282,10 @@ func TestBaseKMSInPackager_UnpackMessage(t *testing.T) { packager, err := New(mockedProviders) require.NoError(t, err) - fromKID, fromKey, err := customKMS.CreateAndExportPubKeyBytes(kms.ECDH256KWAES256GCMType) + fromKID, fromKey, err := customKMS.CreateAndExportPubKeyBytes(kms.NISTP256ECDHKWType) require.NoError(t, err) - _, toKey, err := customKMS.CreateAndExportPubKeyBytes(kms.ECDH256KWAES256GCMType) + _, toKey, err := customKMS.CreateAndExportPubKeyBytes(kms.NISTP256ECDHKWType) require.NoError(t, err) // pack an non empty envelope - should pass diff --git a/pkg/didcomm/packer/anoncrypt/pack_test.go b/pkg/didcomm/packer/anoncrypt/pack_test.go index 614a08913..25d550cee 100644 --- a/pkg/didcomm/packer/anoncrypt/pack_test.go +++ b/pkg/didcomm/packer/anoncrypt/pack_test.go @@ -28,43 +28,78 @@ import ( func TestAnoncryptPackerSuccess(t *testing.T) { k := createKMS(t) - _, recipientsKeys, keyHandles := createRecipients(t, k, 10) - cryptoSvc, err := tinkcrypto.New() - require.NoError(t, err) + tests := []struct { + name string + keyType kms.KeyType + encAlg jose.EncAlg + }{ + { + "anoncrypt using NISTP256ECDHKW and AES256-GCM", + kms.NISTP256ECDHKWType, + jose.A256GCM, + }, + { + "anoncrypt using X25519ECDHKW and XChacha20Poly1305", + kms.X25519ECDHKWType, + jose.XC20P, + }, + { + "anoncrypt using NISTP256ECDHKW and XChacha20Poly1305", + kms.NISTP256ECDHKWType, + jose.XC20P, + }, + { + "anoncrypt using X25519ECDHKW and AES256-GCM", + kms.X25519ECDHKWType, + jose.A256GCM, + }, + } - anonPacker, err := New(newMockProvider(k, cryptoSvc), jose.A256GCM) - require.NoError(t, err) + t.Parallel() - origMsg := []byte("secret message") - ct, err := anonPacker.Pack(origMsg, nil, recipientsKeys) - require.NoError(t, err) + for _, tt := range tests { + tc := tt + t.Run(fmt.Sprintf("running %s", tc.name), func(t *testing.T) { + _, recipientsKeys, keyHandles := createRecipientsByKeyType(t, k, 3, tc.keyType) - msg, err := anonPacker.Unpack(ct) - require.NoError(t, err) + cryptoSvc, err := tinkcrypto.New() + require.NoError(t, err) - recKey, err := exportPubKeyBytes(keyHandles[0]) - require.NoError(t, err) + anonPacker, err := New(newMockProvider(k, cryptoSvc), jose.A256GCM) + require.NoError(t, err) - require.EqualValues(t, &transport.Envelope{Message: origMsg, ToKey: recKey}, msg) + origMsg := []byte("secret message") + ct, err := anonPacker.Pack(origMsg, nil, recipientsKeys) + require.NoError(t, err) - // try with only 1 recipient - ct, err = anonPacker.Pack(origMsg, nil, [][]byte{recipientsKeys[0]}) - require.NoError(t, err) + msg, err := anonPacker.Unpack(ct) + require.NoError(t, err) - msg, err = anonPacker.Unpack(ct) - require.NoError(t, err) + recKey, err := exportPubKeyBytes(keyHandles[0]) + require.NoError(t, err) - require.EqualValues(t, &transport.Envelope{Message: origMsg, ToKey: recKey}, msg) + require.EqualValues(t, &transport.Envelope{Message: origMsg, ToKey: recKey}, msg) - require.Equal(t, encodingType, anonPacker.EncodingType()) + // try with only 1 recipient + ct, err = anonPacker.Pack(origMsg, nil, [][]byte{recipientsKeys[0]}) + require.NoError(t, err) + + msg, err = anonPacker.Unpack(ct) + require.NoError(t, err) + + require.EqualValues(t, &transport.Envelope{Message: origMsg, ToKey: recKey}, msg) + + require.Equal(t, encodingType, anonPacker.EncodingType()) + }) + } } func TestAnoncryptPackerSuccessWithDifferentCurvesSuccess(t *testing.T) { k := createKMS(t) _, recipientsKey1, keyHandles1 := createRecipients(t, k, 1) - _, recipientsKey2, _ := createRecipientsByKeyType(t, k, 1, kms.ECDH384KWAES256GCM) - _, recipientsKey3, _ := createRecipientsByKeyType(t, k, 1, kms.ECDH521KWAES256GCM) + _, recipientsKey2, _ := createRecipientsByKeyType(t, k, 1, kms.NISTP384ECDHKW) + _, recipientsKey3, _ := createRecipientsByKeyType(t, k, 1, kms.NISTP521ECDHKW) recipientsKeys := make([][]byte, 3) recipientsKeys[0] = make([]byte, len(recipientsKey1[0])) @@ -183,10 +218,10 @@ func TestAnoncryptPackerFail(t *testing.T) { require.NoError(t, err) // rotate keys to update keyID and force a failure - _, _, err = k.Rotate(kms.ECDH256KWAES256GCMType, kids[0]) + _, _, err = k.Rotate(kms.NISTP256ECDHKWType, kids[0]) require.NoError(t, err) - _, _, err = k.Rotate(kms.ECDH256KWAES256GCMType, kids[1]) + _, _, err = k.Rotate(kms.NISTP256ECDHKWType, kids[1]) require.NoError(t, err) _, err = validAnonPacker.Unpack(ct) @@ -196,7 +231,7 @@ func TestAnoncryptPackerFail(t *testing.T) { // createRecipients and return their public key and keyset.Handle. func createRecipients(t *testing.T, k *localkms.LocalKMS, recipientsCount int) ([]string, [][]byte, []*keyset.Handle) { - return createRecipientsByKeyType(t, k, recipientsCount, kms.ECDH256KWAES256GCM) + return createRecipientsByKeyType(t, k, recipientsCount, kms.NISTP256ECDHKW) } func createRecipientsByKeyType(t *testing.T, k *localkms.LocalKMS, recipientsCount int, diff --git a/pkg/didcomm/packer/authcrypt/pack_test.go b/pkg/didcomm/packer/authcrypt/pack_test.go index fd621375e..1b548be47 100644 --- a/pkg/didcomm/packer/authcrypt/pack_test.go +++ b/pkg/didcomm/packer/authcrypt/pack_test.go @@ -31,56 +31,92 @@ import ( func TestAuthryptPackerSuccess(t *testing.T) { k := createKMS(t) - _, recipientsKeys, keyHandles := createRecipients(t, k, 10) - skid, senderKey, _ := createAndMarshalKey(t, k) + tests := []struct { + name string + keyType kms.KeyType + encAlg jose.EncAlg + }{ + { + "authpack using NISTP256ECDHKW and AES256-GCM", + kms.NISTP256ECDHKWType, + jose.A256GCM, + }, + { + "authpack using X25519ECDHKW and XChacha20Poly1305", + kms.X25519ECDHKWType, + jose.XC20P, + }, + { + "authpack using NISTP256ECDHKW and XChacha20Poly1305", + kms.NISTP256ECDHKWType, + jose.XC20P, + }, + { + "authpack using X25519ECDHKW and AES256-GCM", + kms.X25519ECDHKWType, + jose.A256GCM, + }, + } - thirdPartyKeyStore := make(map[string][]byte) - mockStoreProvider := &mockstorage.MockStoreProvider{Store: &mockstorage.MockStore{ - Store: thirdPartyKeyStore, - }} + t.Parallel() - cryptoSvc, err := tinkcrypto.New() - require.NoError(t, err) + for _, tt := range tests { + tc := tt + t.Run(fmt.Sprintf("running %s", tc.name), func(t *testing.T) { + _, recipientsKeys, keyHandles := createRecipientsByKeyType(t, k, 3, tc.keyType) - authPacker, err := New(newMockProvider(mockStoreProvider, k, cryptoSvc), jose.A256GCM) - require.NoError(t, err) + skid, senderKey, _ := createAndMarshalKeyByKeyType(t, k, tc.keyType) - // add sender key in thirdPartyKS (prep step before Authcrypt.Pack()/Unpack()) - fromWrappedKID := prefix.StorageKIDPrefix + skid - thirdPartyKeyStore[fromWrappedKID] = senderKey + thirdPartyKeyStore := make(map[string][]byte) + mockStoreProvider := &mockstorage.MockStoreProvider{Store: &mockstorage.MockStore{ + Store: thirdPartyKeyStore, + }} - origMsg := []byte("secret message") - ct, err := authPacker.Pack(origMsg, []byte(skid), recipientsKeys) - require.NoError(t, err) + cryptoSvc, err := tinkcrypto.New() + require.NoError(t, err) - msg, err := authPacker.Unpack(ct) - require.NoError(t, err) + authPacker, err := New(newMockProvider(mockStoreProvider, k, cryptoSvc), tc.encAlg) + require.NoError(t, err) - recKey, err := exportPubKeyBytes(keyHandles[0]) - require.NoError(t, err) + // add sender key in thirdPartyKS (prep step before Authcrypt.Pack()/Unpack()) + fromWrappedKID := prefix.StorageKIDPrefix + skid + thirdPartyKeyStore[fromWrappedKID] = senderKey - require.EqualValues(t, &transport.Envelope{Message: origMsg, ToKey: recKey}, msg) + origMsg := []byte("secret message") + ct, err := authPacker.Pack(origMsg, []byte(skid), recipientsKeys) + require.NoError(t, err) - // try with only 1 recipient - ct, err = authPacker.Pack(origMsg, []byte(skid), [][]byte{recipientsKeys[0]}) - require.NoError(t, err) + msg, err := authPacker.Unpack(ct) + require.NoError(t, err) - msg, err = authPacker.Unpack(ct) - require.NoError(t, err) + recKey, err := exportPubKeyBytes(keyHandles[0]) + require.NoError(t, err) - require.EqualValues(t, &transport.Envelope{Message: origMsg, ToKey: recKey}, msg) + require.EqualValues(t, &transport.Envelope{Message: origMsg, ToKey: recKey}, msg) - require.Equal(t, encodingType, authPacker.EncodingType()) + // try with only 1 recipient to force compact JWE serialization + ct, err = authPacker.Pack(origMsg, []byte(skid), [][]byte{recipientsKeys[0]}) + require.NoError(t, err) + + msg, err = authPacker.Unpack(ct) + require.NoError(t, err) + + require.EqualValues(t, &transport.Envelope{Message: origMsg, ToKey: recKey}, msg) + + require.Equal(t, encodingType, authPacker.EncodingType()) + }) + } } func TestAuthryptPackerUsingKeysWithDifferentCurvesSuccess(t *testing.T) { k := createKMS(t) _, recipientsKey1, keyHandles1 := createRecipients(t, k, 1) - // since authcrypt does ECDH kw using the sender key, the recipient keys must be on the same curve as the sender's. - // this is why recipient keys with different curves are not supported for authcrypt. - _, recipientsKey2, _ := createRecipients(t, k, 1) // can't create key with kms.ECDH384KWAES256GCM - _, recipientsKey3, _ := createRecipients(t, k, 1) // can't create key with kms.ECDH521KWAES256GCM + // since authcrypt does ECDH kw using the sender key, the recipient keys must be on the same curve (for NIST P keys) + // and the same key type (for NIST P / X25519 keys) as the sender's. + // this is why recipient keys with different curves/type are not supported for authcrypt. + _, recipientsKey2, _ := createRecipients(t, k, 1) // can't create key with kms.NISTP384ECDHKW + _, recipientsKey3, _ := createRecipients(t, k, 1) // can't create key with kms.NISTP521ECDHKW recipientsKeys := make([][]byte, 3) recipientsKeys[0] = make([]byte, len(recipientsKey1[0])) @@ -251,10 +287,10 @@ func TestAuthcryptPackerFail(t *testing.T) { require.NoError(t, err) // rotate keys to update keyID and force a failure - _, _, err = k.Rotate(kms.ECDH256KWAES256GCMType, kids[0]) + _, _, err = k.Rotate(kms.NISTP256ECDHKWType, kids[0]) require.NoError(t, err) - _, _, err = k.Rotate(kms.ECDH256KWAES256GCMType, kids[1]) + _, _, err = k.Rotate(kms.NISTP256ECDHKWType, kids[1]) require.NoError(t, err) _, err = validAuthPacker.Unpack(ct) @@ -264,7 +300,7 @@ func TestAuthcryptPackerFail(t *testing.T) { // createRecipients and return their public key and keyset.Handle. func createRecipients(t *testing.T, k *localkms.LocalKMS, recipientsCount int) ([]string, [][]byte, []*keyset.Handle) { - return createRecipientsByKeyType(t, k, recipientsCount, kms.ECDH256KWAES256GCM) + return createRecipientsByKeyType(t, k, recipientsCount, kms.NISTP256ECDHKW) } func createRecipientsByKeyType(t *testing.T, k *localkms.LocalKMS, recipientsCount int, @@ -291,7 +327,7 @@ func createRecipientsByKeyType(t *testing.T, k *localkms.LocalKMS, recipientsCou // createAndMarshalKey creates a new recipient keyset.Handle, extracts public key, marshals it and returns // both marshalled public key and original recipient keyset.Handle. func createAndMarshalKey(t *testing.T, k *localkms.LocalKMS) (string, []byte, *keyset.Handle) { - return createAndMarshalKeyByKeyType(t, k, kms.ECDH256KWAES256GCMType) + return createAndMarshalKeyByKeyType(t, k, kms.NISTP256ECDHKWType) } func createAndMarshalKeyByKeyType(t *testing.T, k *localkms.LocalKMS, kt kms.KeyType) (string, []byte, *keyset.Handle) { diff --git a/pkg/doc/jose/common.go b/pkg/doc/jose/common.go index 8a119d6d1..5e82fd474 100644 --- a/pkg/doc/jose/common.go +++ b/pkg/doc/jose/common.go @@ -85,6 +85,8 @@ const ( // A256GCMALG is the default content encryption algorithm value as per // the JWA specification: https://tools.ietf.org/html/rfc7518#section-5.1 A256GCMALG = "A256GCM" + // XC20PALG represented XChacha20Poly1305 content encryption algorithm value. + XC20PALG = "XC20P" // DIDCommEncType representing the JWE 'Typ' protected type header. DIDCommEncType = "didcomm-envelope-enc" ) diff --git a/pkg/doc/jose/decrypter.go b/pkg/doc/jose/decrypter.go index ae334c2e5..ea6683f69 100644 --- a/pkg/doc/jose/decrypter.go +++ b/pkg/doc/jose/decrypter.go @@ -19,6 +19,7 @@ import ( "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/composite/api" "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/composite/ecdh" "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/composite/keyio" + ecdhpb "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/proto/ecdh_aead_go_proto" "github.com/hyperledger/aries-framework-go/pkg/kms" "github.com/hyperledger/aries-framework-go/pkg/storage" ) @@ -47,9 +48,13 @@ func NewJWEDecrypt(store storage.Store, c cryptoapi.Crypto, k kms.KeyManager) *J } } -func getECDHDecPrimitive(cek []byte) (api.CompositeDecrypt, error) { +func getECDHDecPrimitive(cek []byte, encAlg string) (api.CompositeDecrypt, error) { kt := ecdh.NISTPECDHAES256GCMKeyTemplateWithCEK(cek) + if encAlg == XC20PALG { + kt = ecdh.X25519ECDHXChachaKeyTemplateWithCEK(cek) + } + kh, err := keyset.NewHandle(kt) if err != nil { return nil, err @@ -95,13 +100,23 @@ func (jd *JWEDecrypt) unwrapCEK(recWK []*cryptoapi.RecipientWrappedKey, var cek []byte for _, rec := range recWK { + var unwrapOpts []cryptoapi.WrapKeyOpts + recKH, err := jd.kms.Get(rec.KID) if err != nil { continue } + if rec.EPK.Type == ecdhpb.KeyType_OKP.String() { + unwrapOpts = append(unwrapOpts, cryptoapi.WithXC20PKW()) + } + if senderOpt != nil { - cek, err = jd.crypto.UnwrapKey(rec, recKH, senderOpt) + unwrapOpts = append(unwrapOpts, senderOpt) + } + + if len(unwrapOpts) > 0 { + cek, err = jd.crypto.UnwrapKey(rec, recKH, unwrapOpts...) } else { cek, err = jd.crypto.UnwrapKey(rec, recKH) } @@ -119,7 +134,12 @@ func (jd *JWEDecrypt) unwrapCEK(recWK []*cryptoapi.RecipientWrappedKey, } func (jd *JWEDecrypt) decryptJWE(jwe *JSONWebEncryption, cek []byte) ([]byte, error) { - decPrimitive, err := getECDHDecPrimitive(cek) + encAlg, ok := jwe.ProtectedHeaders.Encryption() + if !ok { + return nil, fmt.Errorf("jwedecrypt: JWE 'enc' protected header is missing") + } + + decPrimitive, err := getECDHDecPrimitive(cek, encAlg) if err != nil { return nil, fmt.Errorf("jwedecrypt: failed to get decryption primitive: %w", err) } @@ -173,9 +193,8 @@ func (jd *JWEDecrypt) validateAndExtractProtectedHeaders(jwe *JSONWebEncryption) return fmt.Errorf("jwe is missing encryption algorithm 'enc' header") } - // TODO add support for Chacha content encryption, issue #1684 switch encAlg { - case string(A256GCM): + case string(A256GCM), string(XC20P): default: return fmt.Errorf("encryption algorithm '%s' not supported", encAlg) } @@ -294,6 +313,8 @@ func convertMarshalledJWKToRecKey(marshalledJWK []byte) (*cryptoapi.RecipientWra case *ecdsa.PublicKey: epk.X = key.X.Bytes() epk.Y = key.Y.Bytes() + case []byte: + epk.X = key default: return nil, fmt.Errorf("unsupported recipient key type") } diff --git a/pkg/doc/jose/encrypt_test.go b/pkg/doc/jose/encrypt_test.go index b3700f774..8bb9805cf 100644 --- a/pkg/doc/jose/encrypt_test.go +++ b/pkg/doc/jose/encrypt_test.go @@ -28,11 +28,21 @@ func TestFailConvertRecKeyToMarshalledJWK(t *testing.T) { recKey := &cryptoapi.RecipientWrappedKey{ EPK: cryptoapi.PublicKey{ Curve: "badCurveName", + Type: "EC", }, } _, err := convertRecEPKToMarshalledJWK(recKey) require.EqualError(t, err, "unsupported curve") + + recKey = &cryptoapi.RecipientWrappedKey{ + EPK: cryptoapi.PublicKey{ + Curve: "badCurveName", + }, + } + + _, err = convertRecEPKToMarshalledJWK(recKey) + require.EqualError(t, err, "invalid key type") } func TestBadSenderKeyType(t *testing.T) { @@ -68,7 +78,7 @@ func TestMergeSingleRecipientsHeadersFailureWithUnsetCurve(t *testing.T) { require.NoError(t, err) wk := &cryptoapi.RecipientWrappedKey{ - EPK: cryptoapi.PublicKey{}, + EPK: cryptoapi.PublicKey{Type: "EC"}, } // fail with aad not base64URL encoded diff --git a/pkg/doc/jose/encrypter.go b/pkg/doc/jose/encrypter.go index 8535fa743..ee2088b21 100644 --- a/pkg/doc/jose/encrypter.go +++ b/pkg/doc/jose/encrypter.go @@ -34,6 +34,8 @@ type EncAlg string const ( // A256GCM for AES256GCM content encryption. A256GCM = EncAlg(A256GCMALG) + // XC20P for XChacha20Poly1305 content encryption. + XC20P = EncAlg(XC20PALG) ) // Encrypter interface to Encrypt/Decrypt JWE messages. @@ -64,9 +66,8 @@ func NewJWEEncrypt(encAlg EncAlg, encType, senderKID string, senderKH *keyset.Ha return nil, fmt.Errorf("empty recipientsPubKeys list") } - // TODO add support for Chacha content encryption, issue #1684 switch encAlg { - case A256GCM: + case A256GCM, XC20P: default: return nil, fmt.Errorf("encryption algorithm '%s' not supported", encAlg) } @@ -92,9 +93,13 @@ func NewJWEEncrypt(encAlg EncAlg, encType, senderKID string, senderKH *keyset.Ha }, nil } -func getECDHEncPrimitive(cek []byte) (api.CompositeEncrypt, error) { +func (je *JWEEncrypt) getECDHEncPrimitive(cek []byte) (api.CompositeEncrypt, error) { kt := ecdh.NISTPECDHAES256GCMKeyTemplateWithCEK(cek) + if je.encAlg == XC20P { + kt = ecdh.X25519ECDHXChachaKeyTemplateWithCEK(cek) + } + kh, err := keyset.NewHandle(kt) if err != nil { return nil, err @@ -127,7 +132,7 @@ func (je *JWEEncrypt) EncryptWithAuthData(plaintext, aad []byte) (*JSONWebEncryp cek := random.GetRandomBytes(uint32(cryptoapi.DefKeySize)) // creating the crypto primitive requires a pre-built cek - encPrimitive, err := getECDHEncPrimitive(cek) + encPrimitive, err := je.getECDHEncPrimitive(cek) if err != nil { return nil, fmt.Errorf("jweencrypt: failed to get encryption primitive: %w", err) } @@ -190,26 +195,20 @@ func (je *JWEEncrypt) wrapCEKForRecipients(cek, apu, apv, aad []byte, } var ( - senderOpt cryptoapi.WrapKeyOpts recipientsWK []*cryptoapi.RecipientWrappedKey singleRecipientAAD []byte ) - kwAlg := tinkcrypto.ECDHESA256KWAlg - - if je.skid != "" && je.senderKH != nil { - kwAlg = tinkcrypto.ECDH1PUA256KWAlg - senderOpt = cryptoapi.WithSender(je.senderKH) - } - for i, recPubKey := range je.recipientsKeys { var ( kek *cryptoapi.RecipientWrappedKey err error ) - if senderOpt != nil { - kek, err = je.crypto.WrapKey(cek, apu, apv, recPubKey, senderOpt) + kwAlg, wrapOpts := je.getWrapKeyOpts() + + if len(wrapOpts) > 0 { + kek, err = je.crypto.WrapKey(cek, apu, apv, recPubKey, wrapOpts...) } else { kek, err = je.crypto.WrapKey(cek, apu, apv, recPubKey) } @@ -232,6 +231,31 @@ func (je *JWEEncrypt) wrapCEKForRecipients(cek, apu, apv, aad []byte, return recipientsWK, singleRecipientAAD, nil } +func (je *JWEEncrypt) getWrapKeyOpts() (string, []cryptoapi.WrapKeyOpts) { + var wrapOpts []cryptoapi.WrapKeyOpts + + kwAlg := tinkcrypto.ECDHESA256KWAlg + + if je.encAlg == XC20P { + kwAlg = tinkcrypto.ECDHESXC20PKWAlg + + wrapOpts = append(wrapOpts, cryptoapi.WithXC20PKW()) + } + + if je.skid != "" && je.senderKH != nil { + switch je.encAlg { + case A256GCM: + kwAlg = tinkcrypto.ECDH1PUA256KWAlg + case XC20P: + kwAlg = tinkcrypto.ECDH1PUXC20PKWAlg + } + + wrapOpts = append(wrapOpts, cryptoapi.WithSender(je.senderKH)) + } + + return kwAlg, wrapOpts +} + // mergeSingleRecipientHeaders for single recipient encryption, recipient header info is available in the key, update // AAD with this info and return the marshalled merged result. func mergeSingleRecipientHeaders(recipientWK *cryptoapi.RecipientWrappedKey, @@ -332,23 +356,36 @@ func buildRecipientHeaders(rec *cryptoapi.RecipientWrappedKey) (*RecipientHeader } func convertRecEPKToMarshalledJWK(rec *cryptoapi.RecipientWrappedKey) ([]byte, error) { - var c elliptic.Curve + var ( + c elliptic.Curve + err error + key interface{} + ) - c, err := hybrid.GetCurve(rec.EPK.Curve) - if err != nil { - return nil, err + switch rec.EPK.Type { + case ecdhpb.KeyType_EC.String(): + c, err = hybrid.GetCurve(rec.EPK.Curve) + if err != nil { + return nil, err + } + + key = &ecdsa.PublicKey{ + Curve: c, + X: new(big.Int).SetBytes(rec.EPK.X), + Y: new(big.Int).SetBytes(rec.EPK.Y), + } + case ecdhpb.KeyType_OKP.String(): + key = rec.EPK.X + default: + return nil, errors.New("invalid key type") } recJWK := JWK{ JSONWebKey: jose.JSONWebKey{ Use: HeaderEncryption, - Key: &ecdsa.PublicKey{ - Curve: c, - X: new(big.Int).SetBytes(rec.EPK.X), - Y: new(big.Int).SetBytes(rec.EPK.Y), - }, + Key: key, }, - Kty: ecdhpb.KeyType_EC.String(), // TODO add support for X25519 (OKP) content encryption, issue #1684 + Kty: rec.EPK.Type, Crv: rec.EPK.Curve, } diff --git a/pkg/doc/jose/encrypter_decrypter_test.go b/pkg/doc/jose/encrypter_decrypter_test.go index 1673deaeb..f09d20fc4 100644 --- a/pkg/doc/jose/encrypter_decrypter_test.go +++ b/pkg/doc/jose/encrypter_decrypter_test.go @@ -16,6 +16,7 @@ import ( "testing" "github.com/google/tink/go/keyset" + tinkpb "github.com/google/tink/go/proto/tink_go_proto" "github.com/google/tink/go/subtle" "github.com/square/go-jose/v3" "github.com/stretchr/testify/require" @@ -36,135 +37,196 @@ func TestJWEEncryptRoundTrip(t *testing.T) { require.EqualError(t, err, "empty recipientsPubKeys list", "NewJWEEncrypt should fail with empty recipientPubKeys") - recECKeys, recKHs, _ := createRecipients(t, 20) - - cryptoSvc, kmsSvc := createCryptoAndKMSServices(t, recKHs) - - _, err = ariesjose.NewJWEEncrypt("", "", "", nil, recECKeys, cryptoSvc) - require.EqualError(t, err, "encryption algorithm '' not supported", - "NewJWEEncrypt should fail with empty encAlg") - - jweEncrypter, err := ariesjose.NewJWEEncrypt(ariesjose.A256GCM, ariesjose.DIDCommEncType, - "", nil, recECKeys, cryptoSvc) - require.NoError(t, err, "NewJWEEncrypt should not fail with non empty recipientPubKeys") - - pt := []byte("some msg") - jwe, err := jweEncrypter.EncryptWithAuthData(pt, []byte("aad value")) - require.NoError(t, err) - require.Equal(t, len(recECKeys), len(jwe.Recipients)) - - serializedJWE, err := jwe.FullSerialize(json.Marshal) - require.NoError(t, err) - require.NotEmpty(t, serializedJWE) - - // try to deserialize with go-jose (can't decrypt in go-jose since private key is protected by Tink) - joseJWE, err := jose.ParseEncrypted(serializedJWE) - require.NoError(t, err) - require.NotEmpty(t, joseJWE) - - // try to deserialize with local package - localJWE, err := ariesjose.Deserialize(serializedJWE) - require.NoError(t, err) - - t.Run("Decrypting JWE tests failures", func(t *testing.T) { - jweDecrypter := ariesjose.NewJWEDecrypt(nil, cryptoSvc, kmsSvc) - - // decrypt empty JWE - _, err = jweDecrypter.Decrypt(nil) - require.EqualError(t, err, "jwedecrypt: jwe is nil") - - var badJWE *ariesjose.JSONWebEncryption - - badJWE, err = ariesjose.Deserialize(serializedJWE) - require.NoError(t, err) - - ph := badJWE.ProtectedHeaders - badJWE.ProtectedHeaders = nil - - // decrypt JWE with empty ProtectHeaders - _, err = jweDecrypter.Decrypt(badJWE) - require.EqualError(t, err, "jwedecrypt: jwe is missing protected headers") - - badJWE.ProtectedHeaders = ariesjose.Headers{} - badJWE.ProtectedHeaders["somKey"] = "badKey" - _, err = jweDecrypter.Decrypt(badJWE) - require.EqualError(t, err, "jwedecrypt: jwe is missing encryption algorithm 'enc' header") - - badJWE.ProtectedHeaders = map[string]interface{}{ - ariesjose.HeaderEncryption: "badEncHeader", - ariesjose.HeaderType: "test", - } - - // decrypt JWE with bad Enc header value - _, err = jweDecrypter.Decrypt(badJWE) - require.EqualError(t, err, "jwedecrypt: encryption algorithm 'badEncHeader' not supported") - - badJWE.ProtectedHeaders = ph - - // decrypt JWE with invalid recipient key - badJWE.Recipients = []*ariesjose.Recipient{ - { - EncryptedKey: "someKey", - Header: &ariesjose.RecipientHeaders{ - EPK: []byte("somerawbytes"), - }, - }, - { - EncryptedKey: "someOtherKey", - Header: &ariesjose.RecipientHeaders{ - EPK: []byte("someotherrawbytes"), - }, - }, - } - - _, err = jweDecrypter.Decrypt(badJWE) - require.EqualError(t, err, "jwedecrypt: failed to build recipients WK: unable to read "+ - "JWK: invalid character 's' looking for beginning of value") - - // decrypt JWE with unsupported recipient key - var privKey *rsa.PrivateKey - - privKey, err = rsa.GenerateKey(rand.Reader, 2048) - - unsupportedJWK := ariesjose.JWK{ - JSONWebKey: jose.JSONWebKey{ - Key: &privKey.PublicKey, - }, - } - - var mk []byte - - mk, err = unsupportedJWK.MarshalJSON() - require.NoError(t, err) - - badJWE.Recipients = []*ariesjose.Recipient{ - { - EncryptedKey: "someKey", - Header: &ariesjose.RecipientHeaders{ - EPK: mk, - }, - }, - { - EncryptedKey: "someOtherKey", - Header: &ariesjose.RecipientHeaders{ - EPK: mk, - }, - }, - } - - _, err = jweDecrypter.Decrypt(badJWE) - require.EqualError(t, err, "jwedecrypt: failed to build recipients WK: unsupported recipient key type") - }) - - t.Run("Decrypting JWE test success ", func(t *testing.T) { - jweDecrypter := ariesjose.NewJWEDecrypt(nil, cryptoSvc, kmsSvc) + tests := []struct { + name string + kt *tinkpb.KeyTemplate + enc ariesjose.EncAlg + keyType kms.KeyType + }{ + { + name: "P-256 ECDH KW and AES256GCM encryption", + kt: ecdh.NISTP256ECDHKWKeyTemplate(), + enc: ariesjose.A256GCM, + keyType: kms.NISTP256ECDHKWType, + }, + { + name: "P-384 ECDH KW and AES256GCM encryption", + kt: ecdh.NISTP384ECDHKWKeyTemplate(), + enc: ariesjose.A256GCM, + keyType: kms.NISTP384ECDHKWType, + }, + { + name: "P-521 ECDH KW and AES256GCM encryption", + kt: ecdh.NISTP521ECDHKWKeyTemplate(), + enc: ariesjose.A256GCM, + keyType: kms.NISTP521ECDHKWType, + }, + { + name: "X25519 ECDH KW and AES256GCM encryption", + kt: ecdh.X25519ECDHKWKeyTemplate(), + enc: ariesjose.A256GCM, + keyType: kms.X25519ECDHKWType, + }, + { + name: "P-256 ECDH KW and XChacha20Poly1305 encryption", + kt: ecdh.NISTP256ECDHKWKeyTemplate(), + enc: ariesjose.XC20P, + keyType: kms.NISTP256ECDHKWType, + }, + { + name: "P-384 ECDH KW and XChacha20Poly1305 encryption", + kt: ecdh.NISTP384ECDHKWKeyTemplate(), + enc: ariesjose.XC20P, + keyType: kms.NISTP384ECDHKWType, + }, + { + name: "P-521 ECDH KW and XChacha20Poly1305 encryption", + kt: ecdh.NISTP521ECDHKWKeyTemplate(), + enc: ariesjose.XC20P, + keyType: kms.NISTP521ECDHKWType, + }, + { + name: "X25519 ECDH KW and XChacha20Poly1305 encryption", + kt: ecdh.X25519ECDHKWKeyTemplate(), + enc: ariesjose.XC20P, + keyType: kms.X25519ECDHKWType, + }, + } - var msg []byte + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + recECKeys, recKHs, _ := createRecipientsByKeyTemplate(t, 3, tc.kt, tc.keyType) - msg, err = jweDecrypter.Decrypt(localJWE) - require.NoError(t, err) - require.EqualValues(t, pt, msg) - }) + cryptoSvc, kmsSvc := createCryptoAndKMSServices(t, recKHs) + + _, err = ariesjose.NewJWEEncrypt("", "", "", nil, recECKeys, cryptoSvc) + require.EqualError(t, err, "encryption algorithm '' not supported", + "NewJWEEncrypt should fail with empty encAlg") + + jweEncrypter, err := ariesjose.NewJWEEncrypt(tc.enc, ariesjose.DIDCommEncType, + "", nil, recECKeys, cryptoSvc) + require.NoError(t, err, "NewJWEEncrypt should not fail with non empty recipientPubKeys") + + pt := []byte("some msg") + jwe, err := jweEncrypter.EncryptWithAuthData(pt, []byte("aad value")) + require.NoError(t, err) + require.Equal(t, len(recECKeys), len(jwe.Recipients)) + + serializedJWE, err := jwe.FullSerialize(json.Marshal) + require.NoError(t, err) + require.NotEmpty(t, serializedJWE) + + // try to deserialize with go-jose (can't decrypt in go-jose since private key is protected by Tink) + joseJWE, err := jose.ParseEncrypted(serializedJWE) + require.NoError(t, err) + require.NotEmpty(t, joseJWE) + + // try to deserialize with local package + localJWE, err := ariesjose.Deserialize(serializedJWE) + require.NoError(t, err) + + t.Run("Decrypting JWE tests failures", func(t *testing.T) { + jweDecrypter := ariesjose.NewJWEDecrypt(nil, cryptoSvc, kmsSvc) + + // decrypt empty JWE + _, err = jweDecrypter.Decrypt(nil) + require.EqualError(t, err, "jwedecrypt: jwe is nil") + + var badJWE *ariesjose.JSONWebEncryption + + badJWE, err = ariesjose.Deserialize(serializedJWE) + require.NoError(t, err) + + ph := badJWE.ProtectedHeaders + badJWE.ProtectedHeaders = nil + + // decrypt JWE with empty ProtectHeaders + _, err = jweDecrypter.Decrypt(badJWE) + require.EqualError(t, err, "jwedecrypt: jwe is missing protected headers") + + badJWE.ProtectedHeaders = ariesjose.Headers{} + badJWE.ProtectedHeaders["somKey"] = "badKey" + _, err = jweDecrypter.Decrypt(badJWE) + require.EqualError(t, err, "jwedecrypt: jwe is missing encryption algorithm 'enc' header") + + badJWE.ProtectedHeaders = map[string]interface{}{ + ariesjose.HeaderEncryption: "badEncHeader", + ariesjose.HeaderType: "test", + } + + // decrypt JWE with bad Enc header value + _, err = jweDecrypter.Decrypt(badJWE) + require.EqualError(t, err, "jwedecrypt: encryption algorithm 'badEncHeader' not supported") + + badJWE.ProtectedHeaders = ph + + // decrypt JWE with invalid recipient key + badJWE.Recipients = []*ariesjose.Recipient{ + { + EncryptedKey: "someKey", + Header: &ariesjose.RecipientHeaders{ + EPK: []byte("somerawbytes"), + }, + }, + { + EncryptedKey: "someOtherKey", + Header: &ariesjose.RecipientHeaders{ + EPK: []byte("someotherrawbytes"), + }, + }, + } + + _, err = jweDecrypter.Decrypt(badJWE) + require.EqualError(t, err, "jwedecrypt: failed to build recipients WK: unable to read "+ + "JWK: invalid character 's' looking for beginning of value") + + // decrypt JWE with unsupported recipient key + var privKey *rsa.PrivateKey + + privKey, err = rsa.GenerateKey(rand.Reader, 2048) + + unsupportedJWK := ariesjose.JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: &privKey.PublicKey, + }, + } + + var mk []byte + + mk, err = unsupportedJWK.MarshalJSON() + require.NoError(t, err) + + badJWE.Recipients = []*ariesjose.Recipient{ + { + EncryptedKey: "someKey", + Header: &ariesjose.RecipientHeaders{ + EPK: mk, + }, + }, + { + EncryptedKey: "someOtherKey", + Header: &ariesjose.RecipientHeaders{ + EPK: mk, + }, + }, + } + + _, err = jweDecrypter.Decrypt(badJWE) + require.EqualError(t, err, "jwedecrypt: failed to build recipients WK: unsupported recipient key type") + }) + + t.Run("Decrypting JWE test success ", func(t *testing.T) { + jweDecrypter := ariesjose.NewJWEDecrypt(nil, cryptoSvc, kmsSvc) + + var msg []byte + + msg, err = jweDecrypter.Decrypt(localJWE) + require.NoError(t, err) + require.EqualValues(t, pt, msg) + }) + }) + } } func TestJWEEncryptRoundTripWithSingleRecipient(t *testing.T) { @@ -374,8 +436,13 @@ func convertToGoJoseRecipients(t *testing.T, keys []*cryptoapi.PublicKey, kids [ return joseRecipients } -// createRecipients and return their public key and keyset.Handle. func createRecipients(t *testing.T, nbOfEntities int) ([]*cryptoapi.PublicKey, map[string]*keyset.Handle, []string) { + return createRecipientsByKeyTemplate(t, nbOfEntities, ecdh.NISTP256ECDHKWKeyTemplate(), kms.NISTP256ECDHKWType) +} + +// createRecipients and return their public key and keyset.Handle. +func createRecipientsByKeyTemplate(t *testing.T, nbOfEntities int, kt *tinkpb.KeyTemplate, + kType kms.KeyType) ([]*cryptoapi.PublicKey, map[string]*keyset.Handle, []string) { t.Helper() r := make([]*cryptoapi.PublicKey, 0) @@ -383,7 +450,7 @@ func createRecipients(t *testing.T, nbOfEntities int) ([]*cryptoapi.PublicKey, m rKID := make([]string, 0) for i := 0; i < nbOfEntities; i++ { - mrKey, kh, kid := createAndMarshalEntityKey(t) + mrKey, kh, kid := createAndMarshalEntityKey(t, kt, kType) ecPubKey := new(cryptoapi.PublicKey) err := json.Unmarshal(mrKey, ecPubKey) @@ -401,12 +468,11 @@ func createRecipients(t *testing.T, nbOfEntities int) ([]*cryptoapi.PublicKey, m // createAndMarshalEntityKey creates a new recipient keyset.Handle, extracts public key, marshals it and returns // both marshalled public key and original recipient keyset.Handle. -func createAndMarshalEntityKey(t *testing.T) ([]byte, *keyset.Handle, string) { +func createAndMarshalEntityKey(t *testing.T, kt *tinkpb.KeyTemplate, + kType kms.KeyType) ([]byte, *keyset.Handle, string) { t.Helper() - tmpl := ecdh.NISTP256ECDHKWKeyTemplate() - - kh, err := keyset.NewHandle(tmpl) + kh, err := keyset.NewHandle(kt) require.NoError(t, err) pubKH, err := kh.Public() @@ -419,7 +485,7 @@ func createAndMarshalEntityKey(t *testing.T) ([]byte, *keyset.Handle, string) { err = pubKH.WriteWithNoSecrets(pubKeyWriter) require.NoError(t, err) - kid, err := jwkkid.CreateKID(buf.Bytes(), kms.ECDH256KWAES256GCMType) + kid, err := jwkkid.CreateKID(buf.Bytes(), kType) require.NoError(t, err) return buf.Bytes(), kh, kid @@ -436,59 +502,120 @@ func TestFailNewJWEEncrypt(t *testing.T) { } func TestECDH1PU(t *testing.T) { - recipients, recKHs, _ := createRecipients(t, 2) - senders, senderKHs, senderKIDs := createRecipients(t, 1) + tests := []struct { + name string + kt *tinkpb.KeyTemplate + enc ariesjose.EncAlg + keyType kms.KeyType + }{ + { + name: "P-256 ECDH KW and AES256GCM encryption", + kt: ecdh.NISTP256ECDHKWKeyTemplate(), + enc: ariesjose.A256GCM, + keyType: kms.NISTP256ECDHKWType, + }, + { + name: "P-384 ECDH KW and AES256GCM encryption", + kt: ecdh.NISTP384ECDHKWKeyTemplate(), + enc: ariesjose.A256GCM, + keyType: kms.NISTP384ECDHKWType, + }, + { + name: "P-521 ECDH KW and AES256GCM encryption", + kt: ecdh.NISTP521ECDHKWKeyTemplate(), + enc: ariesjose.A256GCM, + keyType: kms.NISTP521ECDHKWType, + }, + { + name: "X25519 ECDH KW and AES256GCM encryption", + kt: ecdh.X25519ECDHKWKeyTemplate(), + enc: ariesjose.A256GCM, + keyType: kms.X25519ECDHKWType, + }, + { + name: "P-256 ECDH KW and XChacha20Poly1305 encryption", + kt: ecdh.NISTP256ECDHKWKeyTemplate(), + enc: ariesjose.XC20P, + keyType: kms.NISTP256ECDHKWType, + }, + { + name: "P-384 ECDH KW and XChacha20Poly1305 encryption", + kt: ecdh.NISTP384ECDHKWKeyTemplate(), + enc: ariesjose.XC20P, + keyType: kms.NISTP384ECDHKWType, + }, + { + name: "P-521 ECDH KW and XChacha20Poly1305 encryption", + kt: ecdh.NISTP521ECDHKWKeyTemplate(), + enc: ariesjose.XC20P, + keyType: kms.NISTP521ECDHKWType, + }, + { + name: "X25519 ECDH KW and XChacha20Poly1305 encryption", + kt: ecdh.X25519ECDHKWKeyTemplate(), + enc: ariesjose.XC20P, + keyType: kms.X25519ECDHKWType, + }, + } - c, k := createCryptoAndKMSServices(t, recKHs) + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + recipients, recKHs, _ := createRecipientsByKeyTemplate(t, 2, tc.kt, tc.keyType) + senders, senderKHs, senderKIDs := createRecipientsByKeyTemplate(t, 1, tc.kt, tc.keyType) - senderPubKey, err := json.Marshal(senders[0]) - require.NoError(t, err) + c, k := createCryptoAndKMSServices(t, recKHs) - jweEnc, err := ariesjose.NewJWEEncrypt(ariesjose.A256GCM, ariesjose.DIDCommEncType, senderKIDs[0], - senderKHs[senderKIDs[0]], recipients, c) - require.NoError(t, err) - require.NotEmpty(t, jweEnc) + senderPubKey, err := json.Marshal(senders[0]) + require.NoError(t, err) - mockStoreMap := make(map[string][]byte) - mockStore := &mockstorage.MockStore{ - Store: mockStoreMap, - } + jweEnc, err := ariesjose.NewJWEEncrypt(tc.enc, ariesjose.DIDCommEncType, senderKIDs[0], + senderKHs[senderKIDs[0]], recipients, c) + require.NoError(t, err) + require.NotEmpty(t, jweEnc) - pt := []byte("plaintext payload") + mockStoreMap := make(map[string][]byte) + mockStore := &mockstorage.MockStore{ + Store: mockStoreMap, + } - // test JWEEncrypt for ECDH1PU - jwe, err := jweEnc.Encrypt(pt) - require.NoError(t, err) + pt := []byte("plaintext payload") - serializedJWE, err := jwe.FullSerialize(json.Marshal) - require.NoError(t, err) - require.NotEmpty(t, serializedJWE) + // test JWEEncrypt for ECDH1PU + jwe, err := jweEnc.Encrypt(pt) + require.NoError(t, err) - localJWE, err := ariesjose.Deserialize(serializedJWE) - require.NoError(t, err) + serializedJWE, err := jwe.FullSerialize(json.Marshal) + require.NoError(t, err) + require.NotEmpty(t, serializedJWE) - t.Run("Decrypting JWE message without sender key in the third party store should fail", func(t *testing.T) { - jd := ariesjose.NewJWEDecrypt(mockStore, c, k) - require.NotEmpty(t, jd) + localJWE, err := ariesjose.Deserialize(serializedJWE) + require.NoError(t, err) - _, err = jd.Decrypt(localJWE) - require.EqualError(t, err, "jwedecrypt: failed to add sender public key for skid: failed to get sender"+ - " key from DB: data not found") - }) + t.Run("Decrypting JWE message without sender key in the third party store should fail", func(t *testing.T) { + jd := ariesjose.NewJWEDecrypt(mockStore, c, k) + require.NotEmpty(t, jd) - // add sender pubkey into the recipient's mock store to prepare for a successful JWEDecrypt() for each recipient - mockStoreMap[senderKIDs[0]] = senderPubKey + _, err = jd.Decrypt(localJWE) + require.EqualError(t, err, "jwedecrypt: failed to add sender public key for skid: failed to get sender"+ + " key from DB: data not found") + }) - t.Run("Decrypting JWE message test success", func(t *testing.T) { - jd := ariesjose.NewJWEDecrypt(mockStore, c, k) - require.NotEmpty(t, jd) + // add sender pubkey into the recipient's mock store to prepare for a successful JWEDecrypt() for each recipient + mockStoreMap[senderKIDs[0]] = senderPubKey - var msg []byte + t.Run("Decrypting JWE message test success", func(t *testing.T) { + jd := ariesjose.NewJWEDecrypt(mockStore, c, k) + require.NotEmpty(t, jd) - msg, err = jd.Decrypt(localJWE) - require.NoError(t, err) - require.EqualValues(t, pt, msg) - }) + var msg []byte + + msg, err = jd.Decrypt(localJWE) + require.NoError(t, err) + require.EqualValues(t, pt, msg) + }) + }) + } } func createCryptoAndKMSServices(t *testing.T, keys map[string]*keyset.Handle) (cryptoapi.Crypto, kms.KeyManager) { diff --git a/pkg/doc/jose/jwe.go b/pkg/doc/jose/jwe.go index 557a3755e..83b7567a1 100644 --- a/pkg/doc/jose/jwe.go +++ b/pkg/doc/jose/jwe.go @@ -64,7 +64,6 @@ type RecipientHeaders struct { Tag string `json:"tag,omitempty"` KID string `json:"kid,omitempty"` EPK json.RawMessage `json:"epk,omitempty"` - SPK json.RawMessage `json:"spk,omitempty"` } // rawJSONWebEncryption represents a RAW JWE that is used for serialization/deserialization. diff --git a/pkg/doc/jose/jwk.go b/pkg/doc/jose/jwk.go index e0cb8cc0d..6c986d67d 100644 --- a/pkg/doc/jose/jwk.go +++ b/pkg/doc/jose/jwk.go @@ -21,6 +21,8 @@ import ( "github.com/btcsuite/btcd/btcec" "github.com/square/go-jose/v3" "golang.org/x/crypto/ed25519" + + "github.com/hyperledger/aries-framework-go/pkg/internal/cryptoutil" ) const ( @@ -29,6 +31,8 @@ const ( secp256k1Kty = "EC" secp256k1Size = 32 bitsPerByte = 8 + x25519Crv = "X25519" + x25519Kty = "OKP" ) // JWK (JSON Web Key) is a JSON data structure that represents a cryptographic key. @@ -62,6 +66,32 @@ func JWKFromPublicKey(pubKey interface{}) (*JWK, error) { return key, nil } +// JWEFromX25519Key is similar to JWKFromPublicKey but is specific to X25519 keys when using a public key as raw []byte. +// This builder function presets the curve and key type in the JWK. +// Using JWKFromPublicKey for X25519 raw keys will not have these fields set and will not provide the right JWK output. +func JWEFromX25519Key(pubKey []byte) (*JWK, error) { + key := &JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: pubKey, + }, + Crv: "X25519", + Kty: "OKP", + } + + // marshal/unmarshal to get all JWK's fields other than Key filled. + keyBytes, err := key.MarshalJSON() + if err != nil { + return nil, fmt.Errorf("create JWK: %w", err) + } + + err = key.UnmarshalJSON(keyBytes) + if err != nil { + return nil, fmt.Errorf("create JWK: %w", err) + } + + return key, nil +} + // PublicKeyBytes converts a public key to bytes. func (j *JWK) PublicKeyBytes() ([]byte, error) { if j.isSecp256k1() { @@ -81,6 +111,15 @@ func (j *JWK) PublicKeyBytes() ([]byte, error) { return pubKey.SerializeCompressed(), nil } + if j.isX25519() { + x25519Key, ok := j.Key.([]byte) + if !ok { + return nil, fmt.Errorf("invalid public key in kid '%s'", j.KeyID) + } + + return x25519Key, nil + } + switch pubKey := j.Public().Key.(type) { case ed25519.PublicKey: return pubKey, nil @@ -102,12 +141,20 @@ func (j *JWK) UnmarshalJSON(jwkBytes []byte) error { return fmt.Errorf("unable to read JWK: %w", marshalErr) } + // nolint: gocritic, nestif if isSecp256k1(key.Alg, key.Kty, key.Crv) { jwk, err := unmarshalSecp256k1(&key) if err != nil { return fmt.Errorf("unable to read JWK: %w", err) } + *j = *jwk + } else if isX25519(key.Kty, key.Crv) { + jwk, err := unmarshalX25519(&key) + if err != nil { + return fmt.Errorf("unable to read X25519 JWE: %w", err) + } + *j = *jwk } else { var joseJWK jose.JSONWebKey @@ -132,9 +179,22 @@ func (j *JWK) MarshalJSON() ([]byte, error) { return marshalSecp256k1(j) } + if j.isX25519() { + return marshalX25519(j) + } + return (&j.JSONWebKey).MarshalJSON() } +func (j *JWK) isX25519() bool { + switch j.Key.(type) { + case []byte: + return isX25519(j.Kty, j.Crv) + default: + return false + } +} + func (j *JWK) isSecp256k1() bool { return isSecp256k1Key(j.Key) || isSecp256k1(j.Algorithm, j.Kty, j.Crv) } @@ -150,6 +210,10 @@ func isSecp256k1Key(pubKey interface{}) bool { } } +func isX25519(kty, crv string) bool { + return strings.EqualFold(kty, x25519Kty) && strings.EqualFold(crv, x25519Crv) +} + func isSecp256k1(alg, kty, crv string) bool { return strings.EqualFold(alg, secp256k1Alg) || (strings.EqualFold(kty, secp256k1Kty) && strings.EqualFold(crv, secp256k1Crv)) @@ -211,6 +275,49 @@ func unmarshalSecp256k1(jwk *jsonWebKey) (*JWK, error) { }, nil } +func unmarshalX25519(jwk *jsonWebKey) (*JWK, error) { + if jwk.X == nil { + return nil, ErrInvalidKey + } + + if len(jwk.X.data) != cryptoutil.Curve25519KeySize { + return nil, ErrInvalidKey + } + + return &JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: jwk.X.data, KeyID: jwk.Kid, Algorithm: jwk.Alg, Use: jwk.Use, + }, + Crv: jwk.Crv, + Kty: jwk.Kty, + }, nil +} + +func marshalX25519(jwk *JWK) ([]byte, error) { + var raw jsonWebKey + + key, ok := jwk.Key.([]byte) + if !ok { + return nil, errors.New("marshalX25519: invalid key") + } + + if len(key) != cryptoutil.Curve25519KeySize { + return nil, errors.New("marshalX25519: invalid key") + } + + raw = jsonWebKey{ + Kty: x25519Kty, + Crv: x25519Crv, + X: newFixedSizeBuffer(key, cryptoutil.Curve25519KeySize), + } + + raw.Kid = jwk.KeyID + raw.Alg = jwk.Algorithm + raw.Use = jwk.Use + + return json.Marshal(raw) +} + func marshalSecp256k1(jwk *JWK) ([]byte, error) { var raw jsonWebKey diff --git a/pkg/doc/jose/jwk_test.go b/pkg/doc/jose/jwk_test.go index 295a36250..db278047e 100644 --- a/pkg/doc/jose/jwk_test.go +++ b/pkg/doc/jose/jwk_test.go @@ -10,6 +10,7 @@ import ( "crypto/ed25519" "crypto/elliptic" "crypto/rand" + "strings" "testing" "github.com/btcsuite/btcd/btcec" @@ -76,6 +77,16 @@ func TestDecodePublicKey(t *testing.T) { "alg": "EdDSA" }`, }, + { + name: "get public key bytes X25519 JWK", + jwkJSON: `{ + "kty": "OKP", + "use": "enc", + "crv": "X25519", + "kid": "sample@sample.id", + "x": "sEHL6KXs8bUz9Ss2qSWWjhhRMHVjrog0lzFENM132R8" + }`, + }, { name: "get public key bytes RSA JWK", jwkJSON: `{ @@ -229,6 +240,16 @@ func TestDecodePublicKey(t *testing.T) { }`, err: "invalid JWK", }, + { + name: "X is not defined X25519", + jwkJSON: `{ + "kty": "OKP", + "use": "enc", + "crv": "X25519", + "kid": "sample@sample.id" + }`, + err: "invalid JWK", + }, { name: "Y is not defined", jwkJSON: `{ @@ -281,6 +302,17 @@ func TestDecodePublicKey(t *testing.T) { }`, err: "unable to read JWK", }, + { + name: "invalid X25519", + jwkJSON: `{ + "kty": "OKP", + "use": "enc", + "crv": "X25519", + "x": "wQehEGTVCu32yp8IwTaBCqPUIYslyd-WoFRsfDKE9IIrIJO8RmkExUecJ5i15L9OC7rl7pwmYFR8QQgdM1ERWO", + "kid": "sample@sample.id" + }`, + err: "unable to read X25519 JWE: invalid JWK", + }, } t.Parallel() @@ -318,6 +350,25 @@ func TestJWKFromPublicKeyFailure(t *testing.T) { require.Nil(t, key) } +func TestJWKFromX25519KeyFailure(t *testing.T) { + key, err := JWEFromX25519Key([]byte(strings.Repeat("a", 33))) // try to create a key larger than X25519 + require.EqualError(t, err, "create JWK: marshalX25519: invalid key") + require.Nil(t, key) + + key, err = JWEFromX25519Key(nil) // try to create a nil key + require.EqualError(t, err, "create JWK: marshalX25519: invalid key") + require.Nil(t, key) + + key = &JWK{ + JSONWebKey: jose.JSONWebKey{ + Key: "abc", // try to create an invalid X25519 key type (string instead of []byte) + }, + } + + _, err = marshalX25519(key) + require.EqualError(t, err, "marshalX25519: invalid key") +} + func TestJWK_PublicKeyBytesValidation(t *testing.T) { jwk := &JWK{ JSONWebKey: jose.JSONWebKey{ diff --git a/pkg/doc/util/jwkkid/kid_creator.go b/pkg/doc/util/jwkkid/kid_creator.go index 1781d42a5..fd90faa70 100644 --- a/pkg/doc/util/jwkkid/kid_creator.go +++ b/pkg/doc/util/jwkkid/kid_creator.go @@ -22,6 +22,7 @@ import ( cryptoapi "github.com/hyperledger/aries-framework-go/pkg/crypto" "github.com/hyperledger/aries-framework-go/pkg/doc/jose" + "github.com/hyperledger/aries-framework-go/pkg/internal/cryptoutil" "github.com/hyperledger/aries-framework-go/pkg/kms" ) @@ -33,6 +34,16 @@ var errInvalidKeyType = errors.New("key type is not supported") // - base64 raw (no padding) URL encoded KID // - error in case of error func CreateKID(keyBytes []byte, kt kms.KeyType) (string, error) { + // X25519 JWK is not supported by go jose, manually build it and build its resulting KID. + if kt == kms.X25519ECDHKWType { + x25519KID, err := createX25519KID(keyBytes) + if err != nil { + return "", fmt.Errorf("createKID: %w", err) + } + + return x25519KID, nil + } + jwk, err := buildJWK(keyBytes, kt) if err != nil { return "", fmt.Errorf("createKID: failed to build jwk: %w", err) @@ -64,7 +75,7 @@ func buildJWK(keyBytes []byte, kt kms.KeyType) (*jose.JWK, error) { return nil, fmt.Errorf("buildJWK: failed to build JWK from ed25519 key: %w", err) } case kms.ECDSAP256TypeIEEEP1363, kms.ECDSAP384TypeIEEEP1363, kms.ECDSAP521TypeIEEEP1363: - c := getCurve(kt) + c := getCurveByKMSKeyType(kt) x, y := elliptic.Unmarshal(c, keyBytes) pubKey := &ecdsa.PublicKey{ @@ -77,7 +88,7 @@ func buildJWK(keyBytes []byte, kt kms.KeyType) (*jose.JWK, error) { if err != nil { return nil, fmt.Errorf("buildJWK: failed to build JWK from ecdsa key in IEEE1363 format: %w", err) } - case kms.ECDH256KWAES256GCMType, kms.ECDH384KWAES256GCMType, kms.ECDH521KWAES256GCMType: + case kms.NISTP256ECDHKWType, kms.NISTP384ECDHKWType, kms.NISTP521ECDHKWType: jwk, err = generateJWKFromECDH(keyBytes) if err != nil { return nil, fmt.Errorf("buildJWK: failed to build JWK from ecdh key: %w", err) @@ -99,11 +110,9 @@ func generateJWKFromDERECDSA(keyBytes []byte) (*jose.JWK, error) { } func generateJWKFromECDH(keyBytes []byte) (*jose.JWK, error) { - compositeKey := &cryptoapi.PublicKey{} - - err := json.Unmarshal(keyBytes, compositeKey) + compositeKey, err := unmarshalECDHKey(keyBytes) if err != nil { - return nil, fmt.Errorf("generateJWKFromECDH: failed to unmarshal ECDH key: %w", err) + return nil, fmt.Errorf("generateJWKFromECDH: %w", err) } c, err := hybrid.GetCurve(compositeKey.Curve) @@ -120,7 +129,7 @@ func generateJWKFromECDH(keyBytes []byte) (*jose.JWK, error) { return jose.JWKFromPublicKey(pubKey) } -func getCurve(kt kms.KeyType) elliptic.Curve { +func getCurveByKMSKeyType(kt kms.KeyType) elliptic.Curve { switch kt { case kms.ECDSAP256TypeIEEEP1363: return elliptic.P256() @@ -133,3 +142,52 @@ func getCurve(kt kms.KeyType) elliptic.Curve { // should never be called but added for linting return elliptic.P256() } + +func unmarshalECDHKey(keyBytes []byte) (*cryptoapi.PublicKey, error) { + compositeKey := &cryptoapi.PublicKey{} + + err := json.Unmarshal(keyBytes, compositeKey) + if err != nil { + return nil, fmt.Errorf("unmarshalECDHKey: failed to unmarshal ECDH key: %w", err) + } + + return compositeKey, nil +} + +func createX25519KID(marshalledKey []byte) (string, error) { + compositeKey, err := unmarshalECDHKey(marshalledKey) + if err != nil { + return "", fmt.Errorf("createX25519KID: %w", err) + } + + jwk, err := buildX25519JWK(compositeKey.X) + if err != nil { + return "", fmt.Errorf("createX25519KID: %w", err) + } + + thumbprint := thumbprintX25519(jwk) + + return base64.RawURLEncoding.EncodeToString(thumbprint), nil +} + +func buildX25519JWK(keyBytes []byte) (string, error) { + const x25519ThumbprintTemplate = `{"crv":"X25519","kty":"OKP",x":"%s"}` + + if len(keyBytes) > cryptoutil.Curve25519KeySize { + return "", errors.New("buildX25519JWK: invalid ECDH X25519 key") + } + + pad := make([]byte, cryptoutil.Curve25519KeySize-len(keyBytes)) + x25519RawKey := append(pad, keyBytes...) + + jwk := fmt.Sprintf(x25519ThumbprintTemplate, base64.RawURLEncoding.EncodeToString(x25519RawKey)) + + return jwk, nil +} + +func thumbprintX25519(jwk string) []byte { + h := crypto.SHA256.New() + _, _ = h.Write([]byte(jwk)) // nolint: errcheck // SHA256 digest returns empty error on Write() + + return h.Sum(nil) +} diff --git a/pkg/doc/util/jwkkid/kid_creator_test.go b/pkg/doc/util/jwkkid/kid_creator_test.go index 0688f82f7..24653ee42 100644 --- a/pkg/doc/util/jwkkid/kid_creator_test.go +++ b/pkg/doc/util/jwkkid/kid_creator_test.go @@ -11,10 +11,16 @@ import ( "crypto/ed25519" "crypto/elliptic" "crypto/rand" + "encoding/json" + "strings" "testing" + commonpb "github.com/google/tink/go/proto/common_go_proto" "github.com/stretchr/testify/require" + cryptoapi "github.com/hyperledger/aries-framework-go/pkg/crypto" + ecdhpb "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/proto/ecdh_aead_go_proto" + "github.com/hyperledger/aries-framework-go/pkg/internal/cryptoutil" "github.com/hyperledger/aries-framework-go/pkg/kms" ) @@ -30,19 +36,39 @@ func TestCreateKID(t *testing.T) { require.EqualError(t, err, "createKID: failed to build jwk: buildJWK: failed to build JWK from ed25519 "+ "key: create JWK: unable to read jose JWK, square/go-jose: unknown curve Ed25519'") + _, x, y, err := elliptic.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + ecdhKey := &cryptoapi.PublicKey{ + Curve: elliptic.P256().Params().Name, + X: x.Bytes(), + Y: y.Bytes(), + } + + ecdhKeyMarshalled, err := json.Marshal(ecdhKey) + require.NoError(t, err) + + kid, err = CreateKID(ecdhKeyMarshalled, kms.NISTP256ECDHKWType) + require.NoError(t, err) + require.NotEmpty(t, kid) + _, err = CreateKID(pubKey, "badType") require.EqualError(t, err, "createKID: failed to build jwk: buildJWK: key type is not supported: 'badType'") badPubKey := ed25519.PublicKey{} - _, err = CreateKID(badPubKey, kms.ECDH256KWAES256GCMType) + _, err = CreateKID(badPubKey, kms.NISTP256ECDHKWType) require.EqualError(t, err, "createKID: failed to build jwk: buildJWK: failed to build JWK from ecdh "+ - "key: generateJWKFromECDH: failed to unmarshal ECDH key: unexpected end of JSON input") + "key: generateJWKFromECDH: unmarshalECDHKey: failed to unmarshal ECDH key: unexpected end of JSON input") _, err = CreateKID(badPubKey, kms.ECDSAP256TypeDER) require.EqualError(t, err, "createKID: failed to build jwk: buildJWK: failed to build JWK from ecdsa "+ "DER key: generateJWKFromDERECDSA: failed to parse ecdsa key in DER format: asn1: syntax error: sequence "+ "truncated") + _, err = CreateKID(badPubKey, kms.X25519ECDHKWType) + require.EqualError(t, err, "createKID: createX25519KID: unmarshalECDHKey: failed to unmarshal ECDH key: "+ + "unexpected end of JSON input") + ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) @@ -52,9 +78,38 @@ func TestCreateKID(t *testing.T) { } func TestGetCurve(t *testing.T) { - c := getCurve(kms.ECDSAP384TypeIEEEP1363) + c := getCurveByKMSKeyType(kms.ECDSAP384TypeIEEEP1363) require.Equal(t, c, elliptic.P384()) - c = getCurve(kms.AES128GCMType) // default P-256 if curve not found + c = getCurveByKMSKeyType(kms.AES128GCMType) // default P-256 if curve not found require.Equal(t, c, elliptic.P256()) } + +func TestGenerateJWKFromECDH(t *testing.T) { + keyBytesWithBadCurve := &cryptoapi.PublicKey{ + Curve: commonpb.EllipticCurveType_UNKNOWN_CURVE.String(), + X: []byte{}, + Y: []byte{}, + } + + badKeyMarshalled, err := json.Marshal(keyBytesWithBadCurve) + require.NoError(t, err) + + _, err = generateJWKFromECDH(badKeyMarshalled) + require.EqualError(t, err, "generateJWKFromECDH: failed to get Curve for ECDH key: unsupported curve") +} + +func TestCreateX25519KID_Failure(t *testing.T) { + key := &cryptoapi.PublicKey{ + Curve: "X25519", + X: []byte(strings.Repeat("a", cryptoutil.Curve25519KeySize+1)), // public key > X25519 key size + Y: []byte{}, + Type: ecdhpb.KeyType_OKP.String(), + } + + mKey, err := json.Marshal(key) + require.NoError(t, err) + + _, err = createX25519KID(mKey) + require.EqualError(t, err, "createX25519KID: buildX25519JWK: invalid ECDH X25519 key") +} diff --git a/pkg/internal/cryptoutil/utils.go b/pkg/internal/cryptoutil/utils.go index e91319374..c2c939e30 100644 --- a/pkg/internal/cryptoutil/utils.go +++ b/pkg/internal/cryptoutil/utils.go @@ -19,134 +19,11 @@ import ( "golang.org/x/crypto/curve25519" ) -// errEmptyRecipients is used when recipients list is empty. -var errEmptyRecipients = errors.New("empty recipients") - -// errInvalidKeypair is used when a keypair is invalid. -var errInvalidKeypair = errors.New("invalid keypair") - -// SignatureAlgorithm represents a signature algorithm. -type SignatureAlgorithm string - -// EncryptionAlgorithm represents a content encryption algorithm. -type EncryptionAlgorithm string - -const ( - // encryption key types. - - // Curve25519 encryption key type. - Curve25519 = EncryptionAlgorithm("Curve25519") - - // signing key types. - - // EdDSA signature key type. - EdDSA = SignatureAlgorithm("EdDSA") -) - -// VerifyKeys is a utility function that verifies if sender key pair and recipients keys are valid (not empty). -func VerifyKeys(sender KeyPair, recipients [][]byte) error { - if len(recipients) == 0 { - return errEmptyRecipients - } - - if !isKeyPairValid(sender) { - return errInvalidKeypair - } - - if !IsChachaKeyValid(sender.Priv) || !IsChachaKeyValid(sender.Pub) { - return ErrInvalidKey - } - - return nil -} - -// IsChachaKeyValid will return true if key size is the same as chacha20poly1305.keySize -// false otherwise. -func IsChachaKeyValid(key []byte) bool { - return len(key) == chacha.KeySize -} - -// KeyPair represents a private/public key pair. -type KeyPair struct { - // Priv is a private key - Priv []byte `json:"priv,omitempty"` - // Pub is a public key - Pub []byte `json:"pub,omitempty"` -} - -// EncKeyPair represents a private/public encryption key pair. -type EncKeyPair struct { - KeyPair `json:"keypair,omitempty"` - // Alg is the encryption algorithm of keys enclosed in this key pair - Alg EncryptionAlgorithm `json:"alg,omitempty"` -} - -// SigKeyPair represents a private/public signature (verification) key pair. -type SigKeyPair struct { - KeyPair `json:"keypair,omitempty"` - // Alg is the signature algorithm of keys enclosed in this key pair - Alg SignatureAlgorithm `json:"alg,omitempty"` -} - -// MessagingKeys represents a pair of key pairs, one for encryption and one for signature -// usually stored in a KMS, it helps prevent converting signing keys into encryption ones -// TODO refactor this structure and all KeyPair handling as per issue #596. -type MessagingKeys struct { - *EncKeyPair `json:"enckeypair,omitempty"` - *SigKeyPair `json:"sigkeypair,omitempty"` -} - -// isKeyPairValid is a utility function that validates a KeyPair. -func isKeyPairValid(kp KeyPair) bool { - if kp.Priv == nil || kp.Pub == nil { - return false - } - - return true -} - -// IsEncKeyPairValid is a utility function that validates an EncKeyPair. -func IsEncKeyPairValid(kp *EncKeyPair) bool { - if !isKeyPairValid(kp.KeyPair) { - return false - } - - switch kp.Alg { - case Curve25519: - return true - default: - return false - } -} - -// IsSigKeyPairValid is a utility function that validates an EncKeyPair. -func IsSigKeyPairValid(kp *SigKeyPair) bool { - if !isKeyPairValid(kp.KeyPair) { - return false - } - - switch kp.Alg { - case EdDSA: - return true - default: - return false - } -} - -// IsMessagingKeysValid is a utility function that validates a KeyPair. -func IsMessagingKeysValid(kpb *MessagingKeys) bool { - if !IsSigKeyPairValid(kpb.SigKeyPair) || !IsEncKeyPairValid(kpb.EncKeyPair) { - return false - } - - return true -} - // Derive25519KEK is a utility function that will derive an ephemeral // symmetric key (kek) using fromPrivKey and toPubKey. func Derive25519KEK(alg, apu, apv []byte, fromPrivKey, toPubKey *[chacha.KeySize]byte) ([]byte, error) { if fromPrivKey == nil || toPubKey == nil { - return nil, ErrInvalidKey + return nil, errors.New("invalid key") } const ( @@ -251,9 +128,3 @@ func SecretEd25519toCurve25519(priv []byte) ([]byte, error) { return sKOut[:], nil } - -// ErrKeyNotFound is returned when key not found. -var ErrKeyNotFound = errors.New("key not found") - -// ErrInvalidKey is used when a key is invalid. -var ErrInvalidKey = errors.New("invalid key") diff --git a/pkg/internal/cryptoutil/utils_test.go b/pkg/internal/cryptoutil/utils_test.go index 634d99284..73136e5fd 100644 --- a/pkg/internal/cryptoutil/utils_test.go +++ b/pkg/internal/cryptoutil/utils_test.go @@ -15,66 +15,9 @@ import ( chacha "golang.org/x/crypto/chacha20poly1305" ) -func TestIsKeyPairValid(t *testing.T) { - require.False(t, isKeyPairValid(KeyPair{})) - - pubKey := []byte("testpublickey") - privKey := []byte("testprivatekey") - - validChachaKey, err := base64.RawURLEncoding.DecodeString("c8CSJr_27PN9xWCpzXNmepRndD6neQcnO9DS0YWjhNs") - require.NoError(t, err) - - require.False(t, isKeyPairValid(KeyPair{Priv: privKey, Pub: nil})) - require.False(t, isKeyPairValid(KeyPair{Priv: nil, Pub: pubKey})) - require.True(t, isKeyPairValid(KeyPair{Priv: privKey, Pub: pubKey})) - - require.EqualError(t, - VerifyKeys( - KeyPair{Priv: privKey, Pub: pubKey}, - [][]byte{[]byte("abc"), []byte("def")}), - ErrInvalidKey.Error()) - require.EqualError(t, - VerifyKeys( - KeyPair{Priv: privKey, Pub: pubKey}, - [][]byte{}), - errEmptyRecipients.Error()) - require.EqualError(t, VerifyKeys(KeyPair{}, [][]byte{[]byte("abc"), []byte("def")}), errInvalidKeypair.Error()) - require.NoError(t, VerifyKeys(KeyPair{Priv: validChachaKey, Pub: validChachaKey}, [][]byte{validChachaKey})) - - require.False(t, IsMessagingKeysValid(&MessagingKeys{ - &EncKeyPair{KeyPair{Priv: privKey, Pub: nil}, Curve25519}, - &SigKeyPair{KeyPair{Priv: nil, Pub: pubKey}, EdDSA}, - })) - - require.False(t, IsMessagingKeysValid(&MessagingKeys{ - &EncKeyPair{KeyPair{Priv: nil, Pub: pubKey}, Curve25519}, - &SigKeyPair{KeyPair{Priv: privKey, Pub: pubKey}, EdDSA}, - })) - - require.False(t, IsMessagingKeysValid(&MessagingKeys{ - &EncKeyPair{KeyPair{Priv: nil, Pub: pubKey}, Curve25519}, - &SigKeyPair{KeyPair{Priv: privKey, Pub: nil}, EdDSA}, - })) - - require.False(t, IsMessagingKeysValid(&MessagingKeys{ - &EncKeyPair{KeyPair{Priv: privKey, Pub: pubKey}, ""}, - &SigKeyPair{KeyPair{Priv: privKey, Pub: pubKey}, EdDSA}, - })) - - require.False(t, IsMessagingKeysValid(&MessagingKeys{ - &EncKeyPair{KeyPair{Priv: privKey, Pub: pubKey}, Curve25519}, - &SigKeyPair{KeyPair{Priv: privKey, Pub: pubKey}, ""}, - })) - - require.True(t, IsMessagingKeysValid(&MessagingKeys{ - &EncKeyPair{KeyPair{Priv: privKey, Pub: pubKey}, Curve25519}, - &SigKeyPair{KeyPair{Priv: privKey, Pub: pubKey}, EdDSA}, - })) -} - func TestDeriveKEK_Util(t *testing.T) { kek, err := Derive25519KEK(nil, nil, nil, nil, nil) - require.EqualError(t, err, ErrInvalidKey.Error()) + require.EqualError(t, err, "invalid key") require.Empty(t, kek) validChachaKey, err := base64.RawURLEncoding.DecodeString("c8CSJr_27PN9xWCpzXNmepRndD6neQcnO9DS0YWjhNs") @@ -83,7 +26,7 @@ func TestDeriveKEK_Util(t *testing.T) { chachaKey := new([chacha.KeySize]byte) copy(chachaKey[:], validChachaKey) kek, err = Derive25519KEK(nil, nil, nil, chachaKey, nil) - require.EqualError(t, err, ErrInvalidKey.Error()) + require.EqualError(t, err, "invalid key") require.Empty(t, kek) validChachaKey2, err := base64.RawURLEncoding.DecodeString("AAjrHjiFLw6kf6CZ5zqH1ooG3y2aQhuqxmUvqJnIvDI") diff --git a/pkg/kms/api.go b/pkg/kms/api.go index 2fdfa346a..30a9640be 100644 --- a/pkg/kms/api.go +++ b/pkg/kms/api.go @@ -110,12 +110,14 @@ const ( RSAPS256 = "RSAPS256" // HMACSHA256Tag256 key type value. HMACSHA256Tag256 = "HMACSHA256Tag256" - // ECDH256KWAES256GCM key type value. - ECDH256KWAES256GCM = "ECDH256KWAES256GCM" - // ECDH384KWAES256GCM key type value. - ECDH384KWAES256GCM = "ECDH384KWAES256GCM" - // ECDH521KWAES256GCM key type value. - ECDH521KWAES256GCM = "ECDH521KWAES256GCM" + // NISTP256ECDHKW key type value. + NISTP256ECDHKW = "NISTP256ECDHKW" + // NISTP384ECDHKW key type value. + NISTP384ECDHKW = "NISTP384ECDHKW" + // NISTP521ECDHKW key type value. + NISTP521ECDHKW = "NISTP521ECDHKW" + // X25519ECDHKW key type value. + X25519ECDHKW = "X25519ECDHKW" ) // KeyType represents a key type supported by the KMS. @@ -154,12 +156,14 @@ const ( RSAPS256Type = KeyType(RSAPS256) // HMACSHA256Tag256Type key type value. HMACSHA256Tag256Type = KeyType(HMACSHA256Tag256) - // ECDH256KWAES256GCMType key type value. - ECDH256KWAES256GCMType = KeyType(ECDH256KWAES256GCM) - // ECDH384KWAES256GCMType key type value. - ECDH384KWAES256GCMType = KeyType(ECDH384KWAES256GCM) - // ECDH521KWAES256GCMType key type value. - ECDH521KWAES256GCMType = KeyType(ECDH521KWAES256GCM) + // NISTP256ECDHKWType key type value. + NISTP256ECDHKWType = KeyType(NISTP256ECDHKW) + // NISTP384ECDHKWType key type value. + NISTP384ECDHKWType = KeyType(NISTP384ECDHKW) + // NISTP521ECDHKWType key type value. + NISTP521ECDHKWType = KeyType(NISTP521ECDHKW) + // X25519ECDHKWType key type value. + X25519ECDHKWType = KeyType(X25519ECDHKW) ) // CryptoBox is a libsodium crypto service used by legacy authcrypt packer. diff --git a/pkg/kms/localkms/kid_creator_test.go b/pkg/kms/localkms/kid_creator_test.go index 82e20af3a..915777f82 100644 --- a/pkg/kms/localkms/kid_creator_test.go +++ b/pkg/kms/localkms/kid_creator_test.go @@ -9,10 +9,13 @@ package localkms import ( "crypto/ed25519" "crypto/rand" + "encoding/json" "testing" "github.com/stretchr/testify/require" + cryptoapi "github.com/hyperledger/aries-framework-go/pkg/crypto" + ecdhpb "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/proto/ecdh_aead_go_proto" "github.com/hyperledger/aries-framework-go/pkg/kms" ) @@ -28,12 +31,28 @@ func TestCreateKID(t *testing.T) { require.EqualError(t, err, "createKID: failed to build jwk: buildJWK: key type is not supported: 'badType'") badPubKey := ed25519.PublicKey{} - _, err = CreateKID(badPubKey, kms.ECDH256KWAES256GCMType) + _, err = CreateKID(badPubKey, kms.NISTP256ECDHKWType) require.EqualError(t, err, "createKID: failed to build jwk: buildJWK: failed to build JWK from ecdh "+ - "key: generateJWKFromECDH: failed to unmarshal ECDH key: unexpected end of JSON input") + "key: generateJWKFromECDH: unmarshalECDHKey: failed to unmarshal ECDH key: unexpected end of JSON input") _, err = CreateKID(badPubKey, kms.ECDSAP256TypeDER) require.EqualError(t, err, "createKID: failed to build jwk: buildJWK: failed to build JWK from ecdsa "+ "DER key: generateJWKFromDERECDSA: failed to parse ecdsa key in DER format: asn1: syntax error: sequence "+ "truncated") + + randomKey := make([]byte, 32) + _, err = rand.Read(randomKey) + require.NoError(t, err) + + x25519Key := &cryptoapi.PublicKey{ + Curve: "X25519", + Type: ecdhpb.KeyType_OKP.String(), + X: randomKey, + } + mX25519Key, err := json.Marshal(x25519Key) + require.NoError(t, err) + + kid, err = CreateKID(mX25519Key, kms.X25519ECDHKWType) + require.NoError(t, err) + require.NotEmpty(t, kid) } diff --git a/pkg/kms/localkms/localkms.go b/pkg/kms/localkms/localkms.go index cb747985a..e910fce33 100644 --- a/pkg/kms/localkms/localkms.go +++ b/pkg/kms/localkms/localkms.go @@ -204,12 +204,14 @@ func getKeyTemplate(keyType kms.KeyType) (*tinkpb.KeyTemplate, error) { return signature.ED25519KeyWithoutPrefixTemplate(), nil case kms.HMACSHA256Tag256Type: return mac.HMACSHA256Tag256KeyTemplate(), nil - case kms.ECDH256KWAES256GCMType: + case kms.NISTP256ECDHKWType: return ecdh.NISTP256ECDHKWKeyTemplate(), nil - case kms.ECDH384KWAES256GCMType: + case kms.NISTP384ECDHKWType: return ecdh.NISTP384ECDHKWKeyTemplate(), nil - case kms.ECDH521KWAES256GCMType: + case kms.NISTP521ECDHKWType: return ecdh.NISTP521ECDHKWKeyTemplate(), nil + case kms.X25519ECDHKWType: + return ecdh.X25519ECDHKWKeyTemplate(), nil default: return nil, fmt.Errorf("getKeyTemplate: key type '%s' unrecognized", keyType) } diff --git a/pkg/kms/localkms/localkms_test.go b/pkg/kms/localkms/localkms_test.go index 2a78fd31f..0f38da728 100644 --- a/pkg/kms/localkms/localkms_test.go +++ b/pkg/kms/localkms/localkms_test.go @@ -260,15 +260,16 @@ func TestLocalKMS_Success(t *testing.T) { kms.ECDSAP384TypeIEEEP1363, kms.ECDSAP521TypeIEEEP1363, kms.ED25519Type, - kms.ECDH256KWAES256GCMType, - kms.ECDH384KWAES256GCMType, - kms.ECDH521KWAES256GCMType, + kms.NISTP256ECDHKWType, + kms.NISTP384ECDHKWType, + kms.NISTP521ECDHKWType, + kms.X25519ECDHKWType, } for _, v := range keyTemplates { // test Create() a new key keyID, newKeyHandle, e := kmsService.Create(v) - require.NoError(t, e) + require.NoError(t, e, "failed on template %v", v) require.NotEmpty(t, newKeyHandle) require.NotEmpty(t, keyID) diff --git a/pkg/kms/localkms/pubkey_writer.go b/pkg/kms/localkms/pubkey_writer.go index 8bf9a7b18..a83f92e9f 100644 --- a/pkg/kms/localkms/pubkey_writer.go +++ b/pkg/kms/localkms/pubkey_writer.go @@ -25,9 +25,10 @@ import ( ) const ( - ecdsaVerifierTypeURL = "type.googleapis.com/google.crypto.tink.EcdsaPublicKey" - ed25519VerifierTypeURL = "type.googleapis.com/google.crypto.tink.Ed25519PublicKey" - ecdhAESPublicKeyTypeURL = "type.hyperledger.org/hyperledger.aries.crypto.tink.NistPEcdhKwPublicKey" + ecdsaVerifierTypeURL = "type.googleapis.com/google.crypto.tink.EcdsaPublicKey" + ed25519VerifierTypeURL = "type.googleapis.com/google.crypto.tink.Ed25519PublicKey" + nistPECDHKWPublicKeyTypeURL = "type.hyperledger.org/hyperledger.aries.crypto.tink.NistPEcdhKwPublicKey" + x25519ECDHKWPublicKeyTypeURL = "type.hyperledger.org/hyperledger.aries.crypto.tink.X25519EcdhKwPublicKey" ) // PubKeyWriter will write the raw bytes of a Tink KeySet's primary public key @@ -70,7 +71,7 @@ func write(w io.Writer, msg *tinkpb.Keyset) error { if err != nil { return err } - case ecdhAESPublicKeyTypeURL: + case nistPECDHKWPublicKeyTypeURL, x25519ECDHKWPublicKeyTypeURL: pkW := keyio.NewWriter(w) err = pkW.Write(msg) @@ -97,7 +98,8 @@ func write(w io.Writer, msg *tinkpb.Keyset) error { func writePubKey(w io.Writer, key *tinkpb.Keyset_Key) (bool, error) { var marshaledRawPubKey []byte - // TODO add other key types than the ones below and other than ecdhAESPublicKeyTypeURL + // TODO add other key types than the ones below and other than nistPECDHKWPublicKeyTypeURL and + // TODO x25519ECDHKWPublicKeyTypeURL(eg: BBS+, secp256k1 when introduced in KMS). switch key.KeyData.TypeUrl { case ecdsaVerifierTypeURL: pubKeyProto := new(ecdsapb.EcdsaPublicKey)