Skip to content

Commit

Permalink
refactor: add Wrap/Unwrap crypto functions
Browse files Browse the repository at this point in the history
This change introduces Wrap() and Unwrap() to Cryto api.

The functions work for ECDHES keys only for now. Other keys
(ECDH1PU, Ed25519) can be introduced in a future change.

It also includes setting APU and APV in the key derivation
process (they are set to empty `[]byte{}` in the current JWE
building service).

closes hyperledger-archives#2257

Signed-off-by: Baha Shaaban <baha.shaaban@securekey.com>
  • Loading branch information
Baha Shaaban committed Oct 20, 2020
1 parent b37cbb8 commit d762c89
Show file tree
Hide file tree
Showing 10 changed files with 593 additions and 10 deletions.
8 changes: 8 additions & 0 deletions pkg/crypto/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ SPDX-License-Identifier: Apache-2.0

package crypto

import "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/composite"

// package crypto contains the Crypto interface to be used by the framework.
// it will be created via Options creation in pkg/framework/context.Provider

Expand Down Expand Up @@ -37,4 +39,10 @@ type Crypto interface {
// VerifyMAC determines if mac is a correct authentication code (MAC) for data
// using a matching MAC primitive in kh key handle and returns nil if so, otherwise it returns an error.
VerifyMAC(mac, data []byte, kh interface{}) error

// WrapKey will execute key wrapping of cek using apu, apv and recipient public key found in kh.
WrapKey(cek, apu, apv []byte, kh interface{}) (*composite.RecipientWrappedKey, error)

// UnWrapKey unwraps a key in recWK using recipient private key kh.
UnWrapKey(recWK *composite.RecipientWrappedKey, kh interface{}) ([]byte, error)
}
154 changes: 144 additions & 10 deletions pkg/crypto/tinkcrypto/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,29 @@ SPDX-License-Identifier: Apache-2.0
package tinkcrypto

import (
"crypto/ecdsa"
"errors"
"fmt"
"math/big"

"github.com/google/tink/go/aead"
aeadsubtle "github.com/google/tink/go/aead/subtle"
"github.com/google/tink/go/core/primitiveset"
"github.com/google/tink/go/keyset"
"github.com/google/tink/go/mac"
"github.com/google/tink/go/signature"
josecipher "github.com/square/go-jose/v3/cipher"
"golang.org/x/crypto/chacha20poly1305"

"github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/composite"
ecdhessubtle "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/composite/ecdhes/subtle"
"github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/composite/keyio"
compositepb "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/proto/common_composite_go_proto"
)

const (
ecdhesKWAlg = ecdhessubtle.A256KWAlg
keySize = 32
)

var errBadKeyHandleFormat = errors.New("bad key handle format")
Expand All @@ -32,11 +45,12 @@ var errBadKeyHandleFormat = errors.New("bad key handle format")

// Crypto is the default Crypto SPI implementation using Tink.
type Crypto struct {
kw keyWrapper
}

// New creates a new Crypto instance.
func New() (*Crypto, error) {
return &Crypto{}, nil
return &Crypto{kw: &keyWrapperSupport{}}, nil
}

// Encrypt will encrypt msg using the implementation's corresponding encryption key and primitive in kh.
Expand All @@ -46,6 +60,11 @@ func (t *Crypto) Encrypt(msg, aad []byte, kh interface{}) ([]byte, []byte, error
return nil, nil, errBadKeyHandleFormat
}

ps, err := keyHandle.Primitives()
if err != nil {
return nil, nil, fmt.Errorf("get primitives: %w", err)
}

a, err := aead.New(keyHandle)
if err != nil {
return nil, nil, fmt.Errorf("create new aead: %w", err)
Expand All @@ -56,11 +75,6 @@ func (t *Crypto) Encrypt(msg, aad []byte, kh interface{}) ([]byte, []byte, error
return nil, nil, fmt.Errorf("encrypt msg: %w", err)
}

ps, err := keyHandle.Primitives()
if err != nil {
return nil, nil, fmt.Errorf("get primitives: %w", err)
}

// Tink appends a key prefix + nonce to ciphertext, let's remove them to get the raw ciphertext
ivSize := nonceSize(ps)
prefixLength := len(ps.Primary.Prefix)
Expand Down Expand Up @@ -92,14 +106,14 @@ func (t *Crypto) Decrypt(cipher, aad, nonce []byte, kh interface{}) ([]byte, err
return nil, errBadKeyHandleFormat
}

a, err := aead.New(keyHandle)
ps, err := keyHandle.Primitives()
if err != nil {
return nil, fmt.Errorf("create new aead: %w", err)
return nil, fmt.Errorf("get primitives: %w", err)
}

ps, err := keyHandle.Primitives()
a, err := aead.New(keyHandle)
if err != nil {
return nil, fmt.Errorf("get primitives: %w", err)
return nil, fmt.Errorf("create new aead: %w", err)
}

// since Tink expects the key prefix + nonce as the ciphertext prefix, prepend them prior to calling its Decrypt()
Expand Down Expand Up @@ -187,3 +201,123 @@ func (t *Crypto) VerifyMAC(macBytes, data []byte, kh interface{}) error {

return macPrimitive.VerifyMAC(macBytes, data)
}

// WrapKey will do ECDHES key wrapping of cek using apu, apv and recipient public key found in kh.
func (t *Crypto) WrapKey(cek, apu, apv []byte, kh interface{}) (*composite.RecipientWrappedKey, error) {
// TODO: add support for 25519 key wrapping https://github.com/hyperledger/aries-framework-go/issues/1637
keyType := compositepb.KeyType_EC.String()

pubKH, ok := kh.(*keyset.Handle)
if !ok {
return nil, fmt.Errorf("wrapKey: %w", errBadKeyHandleFormat)
}

pubKey, err := keyio.ExtractPrimaryPublicKey(pubKH)
if err != nil {
return nil, fmt.Errorf("wrapKey: failed to extract recipient public key from kh: %w", err)
}

c, err := t.kw.getCurve(pubKey.Curve)
if err != nil {
return nil, fmt.Errorf("wrapKey: failed to get curve of recipient key: %w", err)
}

recPubKey := &ecdsa.PublicKey{
Curve: c,
X: new(big.Int).SetBytes(pubKey.X),
Y: new(big.Int).SetBytes(pubKey.Y),
}

ephemeralPriv, err := t.kw.generateKey(recPubKey.Curve)
if err != nil {
return nil, fmt.Errorf("wrapKey: failed to generate EPK: %w", err)
}

kek := josecipher.DeriveECDHES(ecdhesKWAlg, apu, apv, ephemeralPriv, recPubKey, keySize)

block, err := t.kw.createCipher(kek)
if err != nil {
return nil, fmt.Errorf("wrapKey: failed to create new Cipher: %w", err)
}

wk, err := t.kw.wrap(block, cek)
if err != nil {
return nil, fmt.Errorf("wrapKey: failed to wrap key: %w", err)
}

return &composite.RecipientWrappedKey{
KID: pubKey.KID,
EncryptedCEK: wk,
EPK: composite.PublicKey{
X: ephemeralPriv.PublicKey.X.Bytes(),
Y: ephemeralPriv.PublicKey.Y.Bytes(),
Curve: ephemeralPriv.PublicKey.Curve.Params().Name,
Type: keyType,
},
APU: apu,
APV: apv,
Alg: ecdhesKWAlg,
}, nil
}

// UnWrapKey unwraps a key in recWK using ECDHES with recipient private key kh.
func (t *Crypto) UnWrapKey(recWK *composite.RecipientWrappedKey, kh interface{}) ([]byte, error) {
if recWK == nil {
return nil, fmt.Errorf("unWrapKey: RecipientWrappedKey is empty")
}

// only ECDHES KW alg is supported (for now)
if recWK.Alg != ecdhesKWAlg {
return nil, fmt.Errorf("unWrapKey: unsupported JWE KW Alg '%s'", recWK.Alg)
}

privKey, ok := kh.(*keyset.Handle)
if !ok {
return nil, fmt.Errorf("unWrapKey: %w", errBadKeyHandleFormat)
}

recipientPrivateKey, err := extractPrivKey(privKey)
if err != nil {
return nil, fmt.Errorf("unWrapKey: %w", err)
}

// TODO: add support for 25519 key wrapping https://github.com/hyperledger/aries-framework-go/issues/1637
recPrivKey := &ecdsa.PrivateKey{
PublicKey: ecdsa.PublicKey{
Curve: recipientPrivateKey.PublicKey.Curve,
X: recipientPrivateKey.PublicKey.Point.X,
Y: recipientPrivateKey.PublicKey.Point.Y,
},
D: recipientPrivateKey.D,
}

epkCurve, err := t.kw.getCurve(recWK.EPK.Curve)
if err != nil {
return nil, fmt.Errorf("unWrapKey: failed to GetCurve: %w", err)
}

if recipientPrivateKey.PublicKey.Curve != epkCurve {
return nil, errors.New("unWrapKey: recipient and epk keys are not on the same curve")
}

epkPubKey := &ecdsa.PublicKey{
Curve: epkCurve,
X: new(big.Int).SetBytes(recWK.EPK.X),
Y: new(big.Int).SetBytes(recWK.EPK.Y),
}

// DeriveECDHES checks if keys are on the same curve
kek := josecipher.DeriveECDHES(recWK.Alg, recWK.APU, recWK.APV, recPrivKey, epkPubKey, keySize)

block, err := t.kw.createCipher(kek)
if err != nil {
return nil, fmt.Errorf("unWrapKey: failed to create new Cipher: %w", err)
}

wk, err := t.kw.unWrap(block, recWK.EncryptedCEK)
if err != nil {
return nil, fmt.Errorf("unWrapKey: failed to unwrap key: %w", err)
}

return wk, nil
}
83 changes: 83 additions & 0 deletions pkg/crypto/tinkcrypto/crypto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import (
"github.com/google/tink/go/keyset"
"github.com/google/tink/go/mac"
"github.com/google/tink/go/signature"
"github.com/google/tink/go/subtle/random"
"github.com/stretchr/testify/require"
chacha "golang.org/x/crypto/chacha20poly1305"

"github.com/hyperledger/aries-framework-go/pkg/crypto"
"github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto/primitive/composite/ecdhes"
)

const testMessage = "test message"
Expand All @@ -38,6 +40,9 @@ func TestCrypto_EncryptDecrypt(t *testing.T) {
badKH, err := keyset.NewHandle(aead.KMSEnvelopeAEADKeyTemplate("babdUrl", nil))
require.NoError(t, err)

badKH2, err := keyset.NewHandle(ecdhes.ECDHES256KWAES256GCMKeyTemplate())
require.NoError(t, err)

c := Crypto{}
msg := []byte(testMessage)
aad := []byte("some additional data")
Expand All @@ -50,6 +55,10 @@ func TestCrypto_EncryptDecrypt(t *testing.T) {
_, _, err = c.Encrypt(msg, aad, badKH)
require.Error(t, err)

// encrypt with another bad key handle - should fail
_, _, err = c.Encrypt(msg, aad, badKH2)
require.Error(t, err)

plainText, err := c.Decrypt(cipherText, aad, nonce, kh)
require.NoError(t, err)
require.Equal(t, msg, plainText)
Expand All @@ -58,6 +67,10 @@ func TestCrypto_EncryptDecrypt(t *testing.T) {
_, err = c.Decrypt(cipherText, aad, nonce, badKH)
require.Error(t, err)

// decrypt with another bad key handle - should fail
_, err = c.Decrypt(cipherText, aad, nonce, badKH2)
require.Error(t, err)

// decrypt with bad nonce - should fail
plainText, err = c.Decrypt(cipherText, aad, []byte("bad nonce"), kh)
require.Error(t, err)
Expand Down Expand Up @@ -238,3 +251,73 @@ func TestCrypto_VerifyMAC(t *testing.T) {
require.EqualError(t, err, "mac_factory: not a MAC primitive")
})
}

func TestCrypto_Wrap_UnWrap_Key(t *testing.T) {
recipientKey, err := keyset.NewHandle(ecdhes.ECDHES256KWAES256GCMKeyTemplate())
require.NoError(t, err)

c, err := New()
require.NoError(t, err)

cek := random.GetRandomBytes(uint32(keySize))
apu := random.GetRandomBytes(uint32(10)) // or sender name
apv := random.GetRandomBytes(uint32(10)) // or recipient name

// test WrapKey with bad key 1
badKey1 := &[]byte{}

_, err = c.WrapKey(cek, apu, apv, badKey1)
require.EqualError(t, err, "wrapKey: bad key handle format")

// test WrapKey with bad key 2 (symmetric key)
badKey2, err := keyset.NewHandle(aead.AES256GCMKeyTemplate())
require.NoError(t, err)

_, err = c.WrapKey(cek, apu, apv, badKey2)
require.EqualError(t, err, "wrapKey: failed to extract recipient public key from kh: "+
"extractPrimaryPublicKey: failed to get public key content: keyset.Handle: keyset.Handle: "+
"keyset contains a non-private key")

// now test WrapKey with good key
wrappedKey, err := c.WrapKey(cek, apu, apv, recipientKey) // WrapKey will extract public key from recipientKey
require.NoError(t, err)
require.NotEmpty(t, wrappedKey.EncryptedCEK)
require.NotEmpty(t, wrappedKey.EPK)
require.EqualValues(t, wrappedKey.APU, apu)
require.EqualValues(t, wrappedKey.APV, apv)
require.Equal(t, wrappedKey.Alg, ecdhesKWAlg)

// test UnWrapKey with empty recWK and/or kh
_, err = c.UnWrapKey(nil, nil)
require.EqualError(t, err, "unWrapKey: RecipientWrappedKey is empty")

_, err = c.UnWrapKey(nil, recipientKey)
require.EqualError(t, err, "unWrapKey: RecipientWrappedKey is empty")

_, err = c.UnWrapKey(wrappedKey, nil)
require.EqualError(t, err, "unWrapKey: bad key handle format")

// test UnWrapKey with ECDHES key but different curve
ecdh384Key, err := keyset.NewHandle(ecdhes.ECDHES384KWAES256GCMKeyTemplate())
require.NoError(t, err)

_, err = c.UnWrapKey(wrappedKey, ecdh384Key)
require.EqualError(t, err, "unWrapKey: recipient and epk keys are not on the same curve")

// test UnWrapKey with invalid key (symmetric key)
_, err = c.UnWrapKey(wrappedKey, badKey2)
require.EqualError(t, err, "unWrapKey: extractPrivKey: can't extract unsupported private key")

// test UnWrapKey with wrappedKey using different algorithm
origAlg := wrappedKey.Alg
wrappedKey.Alg = "badAlg"
_, err = c.UnWrapKey(wrappedKey, recipientKey)
require.EqualError(t, err, "unWrapKey: unsupported JWE KW Alg 'badAlg'")

wrappedKey.Alg = origAlg

// finally test with valid wrappedKey and recipientKey
uCEK, err := c.UnWrapKey(wrappedKey, recipientKey) // UnWrapKey will extract private key from recipientKey
require.NoError(t, err)
require.EqualValues(t, cek, uCEK)
}
48 changes: 48 additions & 0 deletions pkg/crypto/tinkcrypto/key_wrapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
Copyright SecureKey Technologies Inc. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package tinkcrypto

import (
"crypto/aes"
"crypto/cipher"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"

hybrid "github.com/google/tink/go/hybrid/subtle"
josecipher "github.com/square/go-jose/v3/cipher"
)

type keyWrapper interface {
getCurve(curve string) (elliptic.Curve, error)
generateKey(curve elliptic.Curve) (*ecdsa.PrivateKey, error)
createCipher(key []byte) (cipher.Block, error)
wrap(block cipher.Block, cek []byte) ([]byte, error)
unWrap(block cipher.Block, encryptedKey []byte) ([]byte, error)
}

type keyWrapperSupport struct{}

func (w *keyWrapperSupport) getCurve(curve string) (elliptic.Curve, error) {
return hybrid.GetCurve(curve)
}

func (w *keyWrapperSupport) generateKey(curve elliptic.Curve) (*ecdsa.PrivateKey, error) {
return ecdsa.GenerateKey(curve, rand.Reader)
}

func (w *keyWrapperSupport) createCipher(kek []byte) (cipher.Block, error) {
return aes.NewCipher(kek)
}

func (w *keyWrapperSupport) wrap(block cipher.Block, cek []byte) ([]byte, error) {
return josecipher.KeyWrap(block, cek)
}

func (w *keyWrapperSupport) unWrap(block cipher.Block, encryptedKey []byte) ([]byte, error) {
return josecipher.KeyUnwrap(block, encryptedKey)
}
Loading

0 comments on commit d762c89

Please sign in to comment.