From 244d9b8b138dfc3440de4ba8fbe800bb9ceca692 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 | 2 +- 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, 578 insertions(+), 325 deletions(-) diff --git a/pkg/didcomm/packager/package_test.go b/pkg/didcomm/packager/package_test.go index 77411958d9..e7e9a781cd 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 614a089130..25d550ceef 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 fd621375ed..1b548be470 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 8a119d6d1a..5e82fd4741 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 ae334c2e50..ea6683f694 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 b3700f774c..8bb9805cf3 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 8535fa7435..6f0406b590 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 + } + + if je.skid != "" && je.senderKH != nil { + switch je.encAlg { + case A256GCM: + kwAlg = tinkcrypto.ECDH1PUA256KWAlg + case XC20P: + kwAlg = tinkcrypto.ECDH1PUXC20PKWAlg + + wrapOpts = append(wrapOpts, cryptoapi.WithXC20PKW()) + } + + 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 1673deaeb9..6e01add32e 100644 --- a/pkg/doc/jose/encrypter_decrypter_test.go +++ b/pkg/doc/jose/encrypter_decrypter_test.go @@ -419,7 +419,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(), kms.NISTP256ECDHKWType) require.NoError(t, err) return buf.Bytes(), kh, kid diff --git a/pkg/doc/jose/jwe.go b/pkg/doc/jose/jwe.go index 557a3755ea..83b7567a12 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 e0cb8cc0d0..6c986d67db 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 295a36250a..db278047e5 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 1781d42a5d..fd90faa707 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 0688f82f7e..24653ee42a 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 e913193745..c2c939e307 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 634d992841..73136e5fd4 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 2fdfa346a8..30a9640bed 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 82e20af3a9..915777f82e 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 cb747985aa..e910fce33c 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 2a78fd31f1..0f38da728e 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 8bf9a7b189..a83f92e9ff 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)