diff --git a/.go-version b/.go-version new file mode 100644 index 0000000..a6c2798 --- /dev/null +++ b/.go-version @@ -0,0 +1 @@ +1.23.0 diff --git a/README.md b/README.md index 6ef4834..9837f09 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,8 @@ Examples are provided under the `/examples` folder to illustrate correct use of ## Vulnerability check ```sh -$ govulncheck ./... ─╯ +$ govulncheck ./... + Scanning your code and 139 packages across 9 dependent modules for known vulnerabilities... No vulnerabilities found. diff --git a/common_test.go b/common_test.go index fc9b7e9..36581bf 100644 --- a/common_test.go +++ b/common_test.go @@ -1,7 +1,11 @@ package gose import ( + "encoding/base64" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "strings" + "testing" ) const ( @@ -38,3 +42,22 @@ func (mbm *MockBlockMode) CryptBlocks(dst, src []byte) { panic("unexpected mode") } } + +func VerifyJWEStructure(t *testing.T, jwe string) { + require.NotEmpty(t, jwe) + // verify the structure + splits := strings.Split(jwe, ".") + require.Equal(t, 5, len(splits)) + // For direct encryption, the encrypted key is nil + // we expected an empty string for the second part of the JWE + require.Empty(t, splits[1]) + // other parts should not be empty + require.NotEmpty(t, splits[0]) + require.NotEmpty(t, splits[2]) + require.NotEmpty(t, splits[3]) + require.NotEmpty(t, splits[4]) + // verify IV + iv, err := base64.RawURLEncoding.DecodeString(splits[2]) + require.NoError(t, err) + require.NotEmpty(t, iv) +} diff --git a/go.mod b/go.mod index edf2fef..4827180 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,12 @@ module github.com/ThalesGroup/gose +go 1.23.0 + require ( - github.com/ThalesGroup/crypto11 v1.2.6 - github.com/google/uuid v1.5.0 + github.com/ThalesGroup/crypto11 v1.3.0 + github.com/google/uuid v1.6.0 github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.10.0 ) require ( @@ -12,10 +14,8 @@ require ( github.com/miekg/pkcs11 v1.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/objx v0.1.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/thales-e-security/pool v0.0.2 // indirect - golang.org/x/sys v0.16.0 // indirect - gopkg.in/yaml.v3 v3.0.0 // indirect + golang.org/x/sys v0.29.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) - -go 1.21.6 diff --git a/go.sum b/go.sum index f97150c..be78c21 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,12 @@ -github.com/ThalesGroup/crypto11 v1.2.6 h1:KixeJpVw3Y9gLSsz393XHh/Pez7q+KBXit4TQebmOz4= -github.com/ThalesGroup/crypto11 v1.2.6/go.mod h1:Grol7G+6zQdI94hGq+j702L1QFHSlJA5lBLl8uWAhG0= +github.com/ThalesGroup/crypto11 v1.2.6-0.20250121102421-842e7b3e5ff9 h1:U9G+y4aUWy/6hsR0G20JY/gHrGT559uodF6h8qYmneA= +github.com/ThalesGroup/crypto11 v1.2.6-0.20250121102421-842e7b3e5ff9/go.mod h1:z5OUBxhVqPyKn9mm2ffyRpqCue76M3s5D5B/eWGRAOo= +github.com/ThalesGroup/crypto11 v1.3.0 h1:igSFx1K70pUFzPDS+EfK6Aeq9W5tRMxPBJlvHqWxefk= +github.com/ThalesGroup/crypto11 v1.3.0/go.mod h1:mHDkavriX0+GRTHIEjF9Q7gkLwL2j9IXNGAAsjJagNc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -13,17 +15,19 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg= github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/helpers.go b/helpers.go index 6e69715..9c4abc4 100644 --- a/helpers.go +++ b/helpers.go @@ -307,6 +307,8 @@ var inverseOps = map[jose.KeyOps]jose.KeyOps{ jose.KeyOpsVerify: jose.KeyOpsSign, } +// TODO this method always return PS algortihm for signature but never RSA alg for encryption. +// need to find a way to return encryption alg func rsaBitsToAlg(bitLen int) jose.Alg { /* Based on NIST recommendations from 2016. */ if bitLen >= 15360 { @@ -455,6 +457,9 @@ func JwkFromPublicKey(publicKey crypto.PublicKey, operations []jose.KeyOps, cert if v.E > math.MaxInt32 { return nil, ErrInvalidExponent } + // TODO add the possibility to choose the algorithm with an input + // here, only PS is returned, nothing about encryption + alg := rsaBitsToAlg(v.N.BitLen()) /* Key generation. */ var rsa jose.PublicRsaKey @@ -574,8 +579,15 @@ func LoadSymmetricAEAD(jwk jose.Jwk, required []jose.KeyOps) (a cipher.AEAD, err } } -// JwtToString returns the full string of the Jwt or error -func JwtToString(jwt jose.Jwt) (full string, err error) { - +// GetALFromAAD takes the AAD field of a JWE and compute the AL field +// AL is the octet string of the number of bits in AAD expressed as a big endian 64-bit unsigned integer. +// For example, if AAD is 51 bytes long, which is 408 bits long, the octet string AL, which is the number of bits in AAD expressed as a big endian 64 bit unsigned integer is [0, 0, 0, 0, 0, 0, 1, 152]. +func GetALFromAAD(aad []byte) (al []byte) { + // Convert the length to bits + aadLengthBits := uint64(len(aad) * 8) // 51 bytes * 8 bits/byte = 408 bits + // Create a byte slice to hold the big-endian 64-bit unsigned integer + al = make([]byte, 8) + // Convert the length in bits to a big-endian 64-bit unsigned integer + binary.BigEndian.PutUint64(al, aadLengthBits) return } diff --git a/helpers_test.go b/helpers_test.go index 7457f7a..658e7b4 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -26,6 +26,7 @@ import ( "crypto/rand" "crypto/rsa" "encoding/binary" + "fmt" "math/big" "regexp" "testing" @@ -375,43 +376,6 @@ func TestJwkToString(t *testing.T) { assert.Regexp(t, regexp.MustCompile(`{"key_ops":\["verify\"\],"alg":"PS256","kid":"[a-f0-9]{64}","n":"[a-zA-Z0-9-_]+","e":"[0-9A-Z]{4}","kty":"RSA"}`), jwkString) } -func TestJwtToString(t *testing.T) { - type args struct { - jwt jose.Jwt - } - tests := []struct { - name string - args args - wantFull string - wantErr bool - }{ - { - name: "ok", - args: args{ - jwt: jose.Jwt{ - Header: jose.JwsHeader{}, - Claims: jose.JwtClaims{}, - Signature: nil, - }, - }, - wantFull: "", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotFull, err := JwtToString(tt.args.jwt) - if (err != nil) != tt.wantErr { - t.Errorf("JwtToString() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotFull != tt.wantFull { - t.Errorf("JwtToString() gotFull = %v, want %v", gotFull, tt.wantFull) - } - }) - } -} - func TestUintToBytesBigEndian(t *testing.T) { var val1 uint64 val1 = 42 @@ -421,3 +385,15 @@ func TestUintToBytesBigEndian(t *testing.T) { val2 := binary.BigEndian.Uint64(be1) require.Equal(t, val1, val2) } + +func TestGetALFromAAD(t *testing.T) { + aad := make([]byte, 51) + _, err := rand.Read(aad); if err != nil { + t.Fatal(err) + } + + exp := "[0 0 0 0 0 0 1 152]" + res := GetALFromAAD(aad) + test := fmt.Sprintf("%v", res) + assert.Equal(t, exp, test) +} diff --git a/hsm/asymmetric_decryption_key.go b/hsm/asymmetric_decryption_key.go index 5953817..dbf43a3 100644 --- a/hsm/asymmetric_decryption_key.go +++ b/hsm/asymmetric_decryption_key.go @@ -10,21 +10,23 @@ import ( ) // AsymmetricDecryptionKey implements RSA OAEP using SHA1 decryption. +// This structure is made to provide a management of pkcs11-handled asymmetric key pairs type AsymmetricDecryptionKey struct { - kid string + kid []byte + keylabel []byte ctx *crypto11.Context key crypto11.SignerDecrypter } // Kid the unique identifier of this key. func (a *AsymmetricDecryptionKey) Kid() string { - return a.kid + return string(a.kid) } // Certificates associated x509 certificates. func (a *AsymmetricDecryptionKey) Certificates() []*x509.Certificate { // TODO: lookup certificates - cert, err := a.ctx.FindCertificate([]byte(a.kid), nil, nil) + cert, err := a.ctx.FindCertificate(a.kid, a.keylabel, nil) if err != nil { // TODO: return an error via an interface signature change in next major version. panic(err) @@ -37,15 +39,15 @@ func (a *AsymmetricDecryptionKey) Algorithm() jose.Alg { return jose.AlgRSAOAEP } -// Decrypt decrypt the given ciphertext data returning the derived plaintext. -func (a *AsymmetricDecryptionKey) Decrypt(_ jose.KeyOps, bytes []byte) ([]byte, error) { +// Decrypt the given ciphertext data returning the derived plaintext. +func (a *AsymmetricDecryptionKey) Decrypt(_ jose.KeyOps, hash crypto.Hash, bytes []byte) ([]byte, error) { randReader, err := a.ctx.NewRandomReader() if err != nil { return nil, err } return a.key.Decrypt(randReader, bytes, &rsa.OAEPOptions { - Hash: crypto.SHA1, + Hash: hash, Label: nil, }) } @@ -60,3 +62,15 @@ func (a *AsymmetricDecryptionKey) Encryptor() (gose.AsymmetricEncryptionKey, err } var _ gose.AsymmetricDecryptionKey = (*AsymmetricDecryptionKey)(nil) + +// NewAsymmetricDecryptionKey creates an instance with the given pkcs11 +// key handler. +// 'keyid' or 'keylabel' can be nil, but nut both. Provide at least one or both. +func NewAsymmetricDecryptionKey(pkcs11Context *crypto11.Context, key crypto11.SignerDecrypter, kid []byte, keylabel []byte) (*AsymmetricDecryptionKey, error) { + return &AsymmetricDecryptionKey{ + kid: kid, + keylabel: keylabel, + ctx: pkcs11Context, + key: key, + }, nil +} diff --git a/hsm/asymmetric_decryption_key_store.go b/hsm/asymmetric_decryption_key_store.go index 002b9a9..f56201b 100644 --- a/hsm/asymmetric_decryption_key_store.go +++ b/hsm/asymmetric_decryption_key_store.go @@ -21,7 +21,7 @@ func (a *AsymmetricDecryptionKeyStore) Get(kid string) (k gose.AsymmetricDecrypt return nil, gose.ErrInvalidKeyType } return &AsymmetricDecryptionKey{ - kid: kid, + kid: []byte(kid), ctx: a.ctx, key: rsaKeyPair, }, nil diff --git a/interfaces.go b/interfaces.go index a747480..587f90a 100644 --- a/interfaces.go +++ b/interfaces.go @@ -97,14 +97,14 @@ type AsymmetricEncryptionKey interface { MarshalableKey CertifiableKey Algorithmed - Encrypt(jose.KeyOps, []byte) ([]byte, error) + Encrypt(ops jose.KeyOps, hash crypto.Hash, bytes []byte) ([]byte, error) } // AsymmetricDecryptionKey provides asymmetric decryption (private key) capabilities. type AsymmetricDecryptionKey interface { Key Algorithmed - Decrypt(jose.KeyOps, []byte) ([]byte, error) + Decrypt(ops jose.KeyOps, hash crypto.Hash, bytes []byte) ([]byte, error) // Encryptor get the matching encryption key. Encryptor() (AsymmetricEncryptionKey, error) } diff --git a/jose/jwe.go b/jose/jwe.go index 7ac8f9f..c288345 100644 --- a/jose/jwe.go +++ b/jose/jwe.go @@ -259,6 +259,15 @@ func (jwe *Jwe) Marshal() string { } // Marshal a JWE to it's compact representation. +// Follow these steps: +// 1. encode BASE64URL(UTF8(JWE ProtectedHeader)) +// 2. Encode BASE64URL(JWE Encrypted Key) +// 3. Encode BASE64URL(JWE Initialization Vector) +// 4. Create AAD, which is already ASCII(BASE64URL(UTF8(JWE Protected Header))). +// 5. encode AL as an octet string for the unsigned int. Example : [0, 0, 0, 0, 0, 0, 1, 152]. +// 6. Encode BASE64URL(JWE Ciphertext). +// 7. Encode BASE64URL(JWE Authentication Tag). +// TODO add the aad and the al func (jwe *JweRfc7516Compact) Marshal() (marshalledJwe string, err error) { var marshalledHeader []byte if marshalledHeader, err = jwe.ProtectedHeader.MarshalProtectedHeader(); err != nil { diff --git a/jose/types.go b/jose/types.go index b4ee61b..c5b6a18 100644 --- a/jose/types.go +++ b/jose/types.go @@ -97,7 +97,14 @@ const ( // AlgDir direct encryption for use with JWEs AlgDir Alg = "dir" // AlgRSAOAEP RSA OAEP Key encryption for use with JWEs - AlgRSAOAEP = "RSA-OAEP" + AlgRSAOAEP Alg = "RSA-OAEP" + // AlgRSAOAEPSHA1 and AlgRSAOAEPSHA2 are here to differentiate RSA OAEP using SHA1 or SHA2 for + // encryption / decryption in the code, like in switch case statements for example. + // They have the same value as AlgRSAOAEP nonetheless. + // Because some KMS like SoftHSMv2 do not implement RSA-OAEP with SHA2 yet, but some others do, + // we need to support both of these modes in gose implementation. + AlgRSAOAEPSHA1 Alg = "RSA-OAEP" + AlgRSAOAEPSHA2 Alg = "RSA-OAEP" //CrvP256 NIST P-256 CrvP256 Crv = "P-256" diff --git a/jwe_direct_decryptor_block.go b/jwe_direct_decryptor_block.go index 6d45aaf..5f29a4e 100644 --- a/jwe_direct_decryptor_block.go +++ b/jwe_direct_decryptor_block.go @@ -54,10 +54,6 @@ func (decryptor *JweDirectDecryptorBlock) Decrypt(marshalledJwe string) (plainte if jwe.ProtectedHeader.Kid != decryptor.aesKey.Kid() { return nil, nil, fmt.Errorf("error checking the Key ID for decryption. ID is '%v' but expected is '%v'", jwe.ProtectedHeader.Kid, decryptor.aesKey.Kid()) } - // check that the CEK is empty for direct encryption - if len(jwe.EncryptedKey) != 0 { - return nil, nil, fmt.Errorf("error checking the encrypted key. Should be empty for empty encryption but was '%d' bytes long", len(jwe.EncryptedKey)) - } // INTEGRITY CHECK before decryption integrity, err := decryptor.jweVerifier.VerifyCompact(jwe); diff --git a/jwe_direct_encryptor_block.go b/jwe_direct_encryptor_block.go index d03a5a5..44ed350 100644 --- a/jwe_direct_encryptor_block.go +++ b/jwe_direct_encryptor_block.go @@ -29,6 +29,7 @@ import ( var ( cbcAlgToEncMap = map[jose.Alg]jose.Enc{ jose.AlgA256CBC: jose.EncA256CBC, + jose.AlgA256GCM: jose.EncA256GCM, } ) @@ -41,7 +42,7 @@ type JweDirectEncryptorBlock struct { jweVerifier JweHmacVerifierImpl } -// makeJwe builds the JWE structure +// makeJweProtectedHeader builds the JWE structure func (encryptor *JweDirectEncryptorBlock) makeJweProtectedHeader() *jose.JweProtectedHeader { return &jose.JweProtectedHeader{ JwsHeader: jose.JwsHeader{ @@ -86,7 +87,9 @@ func (encryptor *JweDirectEncryptorBlock) Encrypt(plaintext, aad []byte) (string // Create the JWE // we store the length of the plaintext in the additional data held by the protected header. // It can be used to return the proper plaintext after decryption. - jweProtectedHeader.OtherAad = &jose.Blob{B: uintToBytesBigEndian(uint64(len(plaintext)))} + jweProtectedHeader.OtherAad = &jose.Blob{ + B: uintToBytesBigEndian(uint64(len(plaintext))), + } jwe := &jose.JweRfc7516Compact{ ProtectedHeader: *jweProtectedHeader, EncryptedKey: nil, diff --git a/jwe_direct_encryptor_block_test.go b/jwe_direct_encryptor_block_test.go index e62605d..1372422 100644 --- a/jwe_direct_encryptor_block_test.go +++ b/jwe_direct_encryptor_block_test.go @@ -31,11 +31,6 @@ import ( "testing" ) -func requireNoIssue(t *testing.T, o interface{}, err error) { - require.NoError(t, err) - require.NotNil(t, o) -} - func TestJweDirectEncryptorBlock(t *testing.T) { // vars blockSize := 16 @@ -78,21 +73,27 @@ func testEncryptDecrypt(t *testing.T, cryptor *JweDirectEncryptorBlock, decrypto marshalledJwe, err := cryptor.Encrypt([]byte(mockExpectedCleartext), nil) require.NoError(t, err) require.NotEmpty(t, marshalledJwe) + // verify the structure splits := strings.Split(marshalledJwe, ".") require.Equal(t, 5, len(splits)) + // For direct encryption, the encrypted key is nil // we expected an empty string for the second part of the JWE require.Empty(t, splits[1]) + // other parts should not be empty require.NotEmpty(t, splits[0]) require.NotEmpty(t, splits[2]) require.NotEmpty(t, splits[3]) require.NotEmpty(t, splits[4]) - // verify structure + + // verify IV iv, err := base64.RawURLEncoding.DecodeString(splits[2]) require.NoError(t, err) require.Equal(t, expectedIV, iv) + + // verify ciphertext ciphertext, err := base64.RawURLEncoding.DecodeString(splits[3]) require.NoError(t, err) require.Contains(t, string(ciphertext), mockExpectedCiphertext) diff --git a/jwe_key_encryption_decryptor.go b/jwe_key_encryption_decryptor.go index 4d4b68d..7e6aa10 100644 --- a/jwe_key_encryption_decryptor.go +++ b/jwe_key_encryption_decryptor.go @@ -22,80 +22,83 @@ package gose import ( + "crypto" "crypto/aes" "crypto/cipher" + "fmt" "github.com/ThalesGroup/gose/jose" + "slices" ) +var supportedEncryptionAlgs = []jose.Enc{jose.EncA256GCM, jose.EncA128GCM, jose.EncA192GCM} + // JweRsaKeyEncryptionDecryptorImpl implements RSA Key Encryption CEK mode. type JweRsaKeyEncryptionDecryptorImpl struct { keystore AsymmetricDecryptionKeyStore } -// Decrypt decrypts the given JWE returning the contained plaintext and any additional authentic associated data. -func (d *JweRsaKeyEncryptionDecryptorImpl) Decrypt(jwe string) (plaintext, aad []byte, err error) { - var jweStruct jose.Jwe - if err = jweStruct.Unmarshal(jwe); err != nil { - return +// Decrypt decrypts the given JWE returning the contained plaintext and any additional authentic +// associated data. +// This method follow recommendations of https://datatracker.ietf.org/doc/html/rfc7516#section-5.2 +func (d *JweRsaKeyEncryptionDecryptorImpl) Decrypt(jweRaw string, oaepHash crypto.Hash) (plaintext, aad []byte, err error) { + // deserialize jwe + var jwe jose.JweRfc7516Compact + if err = jwe.Unmarshal(jweRaw); err != nil { + return nil, nil, fmt.Errorf("error unmarshalling the jwe: %w", err) } // We do not support zip compression - if jweStruct.Header.Zip != "" { + if jwe.ProtectedHeader.Zip != "" { err = ErrZipCompressionNotSupported return } + // check CEK encryption is supported + if ! slices.Contains(supportedEncryptionAlgs, jwe.ProtectedHeader.Enc) { + return nil, nil, ErrInvalidEncryption + } + + // load key from keystore info before CEK decryption var key AsymmetricDecryptionKey - key, err = d.keystore.Get(jweStruct.Header.Kid) + key, err = d.keystore.Get(jwe.ProtectedHeader.Kid) if err != nil { - return + return nil, nil, fmt.Errorf("error getting key from keystore: %w", err) } // Check alg is as expected - if jweStruct.Header.Alg != key.Algorithm() { - err = ErrInvalidAlgorithm - return + if jwe.ProtectedHeader.Alg != key.Algorithm() { + return nil, nil, ErrInvalidAlgorithm } - // Check the content encryption is a support algorithm - switch jweStruct.Header.Enc { - case jose.EncA128GCM, jose.EncA192GCM, jose.EncA256GCM: - // All good. - default: - err = ErrInvalidEncryption + // Decrypt CEK + var cek []byte + if cek, err = key.Decrypt(jose.KeyOpsDecrypt, oaepHash, jwe.EncryptedKey); err != nil { return } - // First decrypt the content encryption key. - var cekBytes []byte - cekBytes, err = key.Decrypt(jose.KeyOpsDecrypt, jweStruct.EncryptedKey) - if err != nil { - return - } + // decrypt cipher text with cek var block cipher.Block - block, err = aes.NewCipher(cekBytes) - if err != nil { - return + if block, err = aes.NewCipher(cek); err != nil { + return nil, nil, fmt.Errorf("error creating AES cipher: %w", err) } - var aead cipher.AEAD - aead, err = cipher.NewGCM(block) - if err != nil { - return + if aead, err = cipher.NewGCM(block); err != nil { + return nil, nil, fmt.Errorf("error creating GCM AEAD: %w", err) } - - // Decrypt the JWE payload. - ctAndTag := make([]byte, len(jweStruct.Ciphertext) + len(jweStruct.Tag)) - copy(ctAndTag[:len(jweStruct.Ciphertext)], jweStruct.Ciphertext) - copy(ctAndTag[len(jweStruct.Ciphertext):], jweStruct.Tag) - plaintext, err = aead.Open(nil, jweStruct.Iv, ctAndTag, jweStruct.MarshalledHeader) + // concatenate ciphertext and tag for authenticated decryption + // [ciphertext + tag] is the result of the encryption and needs to be provided for decryption + ctAndTag := make([]byte, len(jwe.Ciphertext) + len(jwe.AuthenticationTag)) + copy(ctAndTag[:len(jwe.Ciphertext)], jwe.Ciphertext) + copy(ctAndTag[len(jwe.Ciphertext):], jwe.AuthenticationTag) + // retrieve aad + if aad, err = jwe.ProtectedHeader.MarshalProtectedHeader(); err != nil { + return nil, nil, fmt.Errorf("error getting AAD: %w", err) + } + plaintext, err = aead.Open(nil, jwe.InitializationVector, ctAndTag, aad) if err != nil { return } - if jweStruct.Header.OtherAad != nil { - aad = jweStruct.Header.OtherAad.Bytes() - } return } diff --git a/jwe_key_encryption_decryptor_test.go b/jwe_key_encryption_decryptor_test.go index e788744..e07d6e9 100644 --- a/jwe_key_encryption_decryptor_test.go +++ b/jwe_key_encryption_decryptor_test.go @@ -2,18 +2,18 @@ package gose import ( "bytes" + "crypto" + "crypto/rand" "github.com/ThalesGroup/gose/jose" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "math/rand" - "reflect" "testing" ) const ( - // See https://tools.ietf.org/html/rfc7516#appendix-A.1 - oaepJweFromSpec = "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGeipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDbSv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaVmqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je81860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi6UklfCpIMfIjf7iGdXKHzg.48V1_ALb6US04U3b.5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A.XFBoMYUZodetZdvTiFvSkQ" - jwkFromSpec = ` + // See https://tools.ietf.org/html/rfc7516#appendix-A.1 + oaepJweFromSpec = "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGeipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDbSv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaVmqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je81860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi6UklfCpIMfIjf7iGdXKHzg.48V1_ALb6US04U3b.5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A.XFBoMYUZodetZdvTiFvSkQ" + jwkFromSpec = ` { "kty":"RSA", "kid":"1", @@ -28,62 +28,49 @@ const ( "dq":"Dq0gfgJ1DdFGXiLvQEZnuKEN0UUmsJBxkjydc3j4ZYdBiMRAy86x0vHCjywcMlYYg4yoC4YZa9hNVcsjqA3FeiL19rk8g6Qn29Tt0cj8qqyFpz9vNDBUfCAiJVeESOjJDZPYHdHY8v1b-o-Z2X5tvLx-TCekf7oxyeKDUqKWjis", "qi":"VIMpMYbPf47dT1w_zDUXfPimsSegnMOA1zTaX7aGk_8urY6R8-ZW1FxU7AlWAyLWybqq6t16VFd7hQd0y6flUK4SlOydB61gwanOsXGOAOv82cHq0E3eL4HrtZkUuKvnPrMnsUUFlfUdybVzxyjz9JF_XyaY14ardLSjf4L_FNY" }` - ) +) -// Known answer test (KAT). See https://tools.ietf.org/html/rfc7516#appendix-A.1 -func TestJweRsaKeyEncryptionDecryptorImpl_Decrypt_KAT(t *testing.T) { - // load JWK +func generateDecryptor(t *testing.T) (decryptor *JweRsaKeyEncryptionDecryptorImpl) { + // load testing JWK buf := bytes.NewReader([]byte(jwkFromSpec)) jwk, err := LoadJwk(buf, nil) require.NoError(t, err) key, err := NewRsaDecryptionKey(jwk) require.NoError(t, err) - store, err := NewAsymmetricDecryptionKeyStoreImpl(map[string]AsymmetricDecryptionKey {key.Kid():key}) + store, err := NewAsymmetricDecryptionKeyStoreImpl(map[string]AsymmetricDecryptionKey{key.Kid(): key}) require.NoError(t, err) - decryptor := NewJweRsaKeyEncryptionDecryptorImpl(store) - pt, aad, err := decryptor.Decrypt(oaepJweFromSpec) + decryptor = NewJweRsaKeyEncryptionDecryptorImpl(store) + + return +} + +// Known answer test (KAT). See https://tools.ietf.org/html/rfc7516#appendix-A.1 +func TestJweRsaKeyEncryptionDecryptorImpl_Decrypt_KAT(t *testing.T) { + decryptor := generateDecryptor(t) + pt, aad, err := decryptor.Decrypt(oaepJweFromSpec, crypto.SHA256) require.NoError(t, err) assert.Equal(t, "The true sign of intelligence is not knowledge but imagination.", string(pt)) - assert.Len(t, aad, 0) + assert.Len(t, aad, 46) } -func TestJweRsaKeyEncryptionDecryptorImpl_RoundTrip(t *testing.T) { - params := []struct { - contentEncryptionAlg jose.Alg - } { - { - contentEncryptionAlg: jose.AlgA256GCM, - }, - { - contentEncryptionAlg: jose.AlgA192GCM, - }, - { - contentEncryptionAlg: jose.AlgA128GCM, - }, - } - for _, testCase := range params { - t.Run(reflect.TypeOf(testCase.contentEncryptionAlg).Name(), func(tt *testing.T){ - randData := make([]byte, 32) - _, err := rand.Read(randData) - require.NoError(tt, err) - generator := &RsaKeyDecryptionKeyGenerator{} - decrytionKey, err := generator.Generate(jose.AlgRSAOAEP, 2048, []jose.KeyOps{jose.KeyOpsDecrypt}) - require.NoError(tt, err) - encryptionKey, err := decrytionKey.Encryptor() - require.NoError(tt, err) - publicJwk, err := encryptionKey.Jwk() - require.NoError(tt, err) - encryptor, err := NewJweRsaKeyEncryptionEncryptorImpl(publicJwk, testCase.contentEncryptionAlg) - require.NoError(t, err) - ct, err := encryptor.Encrypt(randData, []byte("aad")) - require.NoError(t, err) - store, err := NewAsymmetricDecryptionKeyStoreImpl(map[string]AsymmetricDecryptionKey {decrytionKey.Kid(): decrytionKey}) - require.NoError(t, err) - decryptor := NewJweRsaKeyEncryptionDecryptorImpl(store) - pt, aad, err := decryptor.Decrypt(ct) - require.NoError(t, err) - assert.Equal(t, randData, pt) - assert.Equal(t, []byte("aad"), aad) - }) - } +func TestJweRsaKeyOAEPEncryptionDecryption(t *testing.T) { + input := []byte("The true sign of intelligence is not knowledge but imagination.") + + generator := &RsaKeyDecryptionKeyGenerator{} + decryptionKey, err := generator.Generate(jose.AlgRSAOAEP, 2048, []jose.KeyOps{jose.KeyOpsDecrypt}) + require.NoError(t, err) + encryptionKey, err := decryptionKey.Encryptor() + require.NoError(t, err) + publicJwk, err := encryptionKey.Jwk() + require.NoError(t, err) + encryptor, err := NewJweRsaKeyEncryptionEncryptorImpl(publicJwk, rand.Reader) + require.NoError(t, err) + ct, err := encryptor.Encrypt(input, crypto.SHA256) + require.NoError(t, err) + store, err := NewAsymmetricDecryptionKeyStoreImpl(map[string]AsymmetricDecryptionKey{decryptionKey.Kid(): decryptionKey}) + require.NoError(t, err) + decryptor := NewJweRsaKeyEncryptionDecryptorImpl(store) + pt, _, err := decryptor.Decrypt(ct, crypto.SHA256) + require.NoError(t, err) + assert.Equal(t, input, pt) } diff --git a/jwe_key_encryption_encryptor.go b/jwe_key_encryption_encryptor.go index 748181d..3a2ff18 100644 --- a/jwe_key_encryption_encryptor.go +++ b/jwe_key_encryption_encryptor.go @@ -23,90 +23,138 @@ package gose import ( "crypto" - "crypto/rand" + "crypto/aes" + "crypto/cipher" "crypto/rsa" + "fmt" "github.com/ThalesGroup/gose/jose" + "io" ) +const cekSize uint8 = 32 + +const ivSize uint8 = 12 + +const cekAlgorithm = jose.AlgA256GCM + // JweRsaKeyEncryptionEncryptorImpl implements RSA Key Encryption CEK mode. type JweRsaKeyEncryptionEncryptorImpl struct { - recipientJwk jose.Jwk - recipientKey *rsa.PublicKey + rsaPublicKey *rsa.PublicKey + rsaPublicKid string + rsaAlg jose.Alg cekAlg jose.Alg + randomSource io.Reader } -// Encrypt encrypts the given plaintext into a compact JWE. Optional authenticated data can be included which is appended -// to the JWE protected header. -func (e *JweRsaKeyEncryptionEncryptorImpl) Encrypt(plaintext, aad []byte) (string, error) { - keyGenerator := &AuthenticatedEncryptionKeyGenerator{} - cek, jwk, err := keyGenerator.Generate(e.cekAlg, []jose.KeyOps{jose.KeyOpsDecrypt, jose.KeyOpsEncrypt}) - if err != nil { - return "", err +// Encrypt encrypts the given plaintext into a compact JWE. +// The Content Encryption Key is encrypted to the recipient using the RSAES-OAEP algorithm to +// produce the JWE Encrypted Key. +// Authenticated encryption is performed on the plaintext using the AES GCM algorithm with a 256-bit +// key to produce the ciphertext and the Authentication Tag. +func (e *JweRsaKeyEncryptionEncryptorImpl) Encrypt(plaintext []byte, oaepHash crypto.Hash) (jwe string, err error) { + // create the protected header + // {"alg":"RSA-OAEP","enc":"A256GCM"} + protectedHeader := e.makeJweProtectedHeader() + + // generate the 256-bit CEK, 32 bytes long + cek := make([]byte, cekSize) + if _, err = e.randomSource.Read(cek); err != nil { + return "", fmt.Errorf("unable to read random source to generate the CEK: %w", err) } - cekJwk := jwk.(*jose.OctSecretKey) - nonce, err := cek.GenerateNonce() + // encrypt the CEK using the recipient public key and RSAES OAEP + // SHA1 is still safe when used in the construction of OAEP. + encryptedCEK, err := rsa.EncryptOAEP(oaepHash.New(), e.randomSource, e.rsaPublicKey, cek, nil) if err != nil { - return "", err + return "", fmt.Errorf("failed to encrypt CEK: %w", err) + } + + // generate a random 96-bit initialization vector + iv := make([]byte, ivSize) + if _, err = e.randomSource.Read(iv); err != nil { + return "", fmt.Errorf("unable to read random source to generate the IV: %w", err) } - var blob *jose.Blob - var customHeaderFields jose.JweCustomHeaderFields - if len(aad) > 0 { - blob = &jose.Blob{B: aad} - customHeaderFields = jose.JweCustomHeaderFields{ - OtherAad: blob, - } + // Create AAD equal to ASCII(BASE64URL(UTF8(JWE Protected Header))). + var aad []byte + if aad, err = protectedHeader.MarshalProtectedHeader(); err != nil { + return "", fmt.Errorf("error marshalling the JWE Header: %w", err) } - encryptedKey, err := rsa.EncryptOAEP(crypto.SHA1.New(), rand.Reader, e.recipientKey, cekJwk.K.Bytes(), nil) + // encrypt the plaintext using the cek + var blockCipher cipher.Block + blockCipher, err = aes.NewCipher(cek); if err != nil { + return "", fmt.Errorf("error creating AES cipher: %w", err) + } + var aesGCM cipher.AEAD + aesGCM, err = cipher.NewGCM(blockCipher); if err != nil { + return "", fmt.Errorf("error creating GCM: %w", err) + } + var aesGCMCryptor AeadEncryptionKey + if aesGCMCryptor, err = NewAesGcmCryptor(aesGCM, e.randomSource, "", cekAlgorithm, []jose.KeyOps{jose.KeyOpsEncrypt}); err != nil { + return "", fmt.Errorf("error creating AES GCM Cryptor: %w", err) + } + ciphertext, tag, err := aesGCMCryptor.Seal(jose.KeyOpsEncrypt, iv, plaintext, aad); if err != nil { - return "", err + return "", fmt.Errorf("error encrypting the plaintext: %w", err) } - jwe := &jose.Jwe{ - Header: jose.JweHeader{ - JwsHeader: jose.JwsHeader{ - Alg: jose.AlgRSAOAEP, - Kid: e.recipientJwk.Kid(), - }, - Enc: gcmAlgToEncMap[cekJwk.Alg()], - JweCustomHeaderFields: customHeaderFields, - }, - EncryptedKey: encryptedKey, - Iv: nonce, - Plaintext: plaintext, + // create the compact representation of the jwe using the parameters above + jweData:= &jose.JweRfc7516Compact{ + ProtectedHeader: *protectedHeader, + EncryptedKey: encryptedCEK, + InitializationVector: iv, + Ciphertext: ciphertext, + AuthenticationTag: tag, } - if err = jwe.MarshalHeader(); err != nil { - return "", err + if jwe, err = jweData.Marshal(); err != nil { + return "", fmt.Errorf("error marshalling the JWE: %w", err) } - if jwe.Ciphertext, jwe.Tag, err = cek.Seal(jose.KeyOpsEncrypt, jwe.Iv, jwe.Plaintext, jwe.MarshalledHeader); err != nil { - return "", err + return +} + +// makeJweProtectedHeader builds the JWE structure +func (e *JweRsaKeyEncryptionEncryptorImpl) makeJweProtectedHeader() *jose.JweProtectedHeader { + return &jose.JweProtectedHeader{ + JwsHeader: jose.JwsHeader{ + // AlgRSAOAEP = "RSA-OAEP" + Alg: e.rsaAlg, + Kid: e.rsaPublicKid, + Typ: "JWT", + Cty: "JWT", + }, + // AlgA256GCM Alg = "A256GCM" + Enc: cbcAlgToEncMap[cekAlgorithm], } - return jwe.Marshal(), nil } // NewJweRsaKeyEncryptionEncryptorImpl returns an instance of JweRsaKeyEncryptionEncryptorImpl configured with the given // JWK. -func NewJweRsaKeyEncryptionEncryptorImpl(recipient jose.Jwk, contentEncryptionAlg jose.Alg) (*JweRsaKeyEncryptionEncryptorImpl, error) { - if _, ok := authenticatedEncryptionAlgs[contentEncryptionAlg]; !ok { - return nil, ErrInvalidAlgorithm - } - if !isSubset(recipient.Ops(), []jose.KeyOps{jose.KeyOpsEncrypt}) { +// rsaPublicKeyRecipient is the jwk describing the public key that will be used to encrypt the CEK. +// randomSource is the random generator used by : +// - the RSA OAEP algorithm for entropy source for each encryption operation on CEKs +// - the encryption of the data performed by the CEK with AES GCM algorithm, for the IV +func NewJweRsaKeyEncryptionEncryptorImpl(rsaPublicKeyRecipient jose.Jwk, randomSource io.Reader) (*JweRsaKeyEncryptionEncryptorImpl, error) { + // check if required operation is supported + if !isSubset(rsaPublicKeyRecipient.Ops(), []jose.KeyOps{jose.KeyOpsEncrypt}) { return nil, ErrInvalidOperations } - kek, err := LoadPublicKey(recipient, validEncryptionOpts) + // create the asymmetric public key structure from the recipient + kek, err := LoadPublicKey(rsaPublicKeyRecipient, validEncryptionOpts) if err != nil { return nil, err } + // parse for rsa rsaKek, ok := kek.(*rsa.PublicKey) if !ok { return nil, ErrInvalidKeyType } + return &JweRsaKeyEncryptionEncryptorImpl{ - recipientKey: rsaKek, - recipientJwk: recipient, - cekAlg: contentEncryptionAlg, + rsaPublicKey: rsaKek, + rsaAlg: rsaPublicKeyRecipient.Alg(), + rsaPublicKid: rsaPublicKeyRecipient.Kid(), + randomSource: randomSource, }, nil } diff --git a/jwe_key_encryption_encryptor_test.go b/jwe_key_encryption_encryptor_test.go index 769993e..b865287 100644 --- a/jwe_key_encryption_encryptor_test.go +++ b/jwe_key_encryption_encryptor_test.go @@ -1,15 +1,53 @@ package gose import ( + "bytes" + "crypto" + "crypto/rand" + "encoding/base64" + "encoding/json" "github.com/ThalesGroup/gose/jose" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "log" + "strings" "testing" ) -func TestNewJweRsaKeyEncryptionEncryptorImpl_UnsupportedContentEncryptionAlg(t *testing.T) { - _, err := NewJweRsaKeyEncryptionEncryptorImpl(nil, jose.AlgES256) - assert.Equal(t, ErrInvalidAlgorithm, err) +const jwkRSAOAEPEncryptionRaw = ` +{ + "kty":"RSA", + "kid": "1", + "key_ops": ["encrypt"], + "alg": "RSA-OAEP", + "n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", + "e":"AQAB" +}` + +func generateEncryptor(t *testing.T) *JweRsaKeyEncryptionEncryptorImpl { + b := make([]byte, 16) + random := rand.Reader + res, _ := random.Read(b) + log.Print(res) + + buf := bytes.NewReader([]byte(jwkRSAOAEPEncryptionRaw)) + jwkRSAOAEPEncryption, err := LoadJwk(buf, nil) + require.NoError(t, err) + + + rsaOAEPEncryptor, err := NewJweRsaKeyEncryptionEncryptorImpl(jwkRSAOAEPEncryption, rand.Reader) + require.NoError(t, err) + + return rsaOAEPEncryptor +} + +func TestNewJweRsaKeyEncryptionEncryptorImpl(t *testing.T) { + rsaOAEPEncryptor := generateEncryptor(t) + + assert.Equal(t, jose.AlgRSAOAEP, rsaOAEPEncryptor.rsaAlg) + assert.Equal(t, "1", rsaOAEPEncryptor.rsaPublicKid) + assert.NotNil(t, rsaOAEPEncryptor.rsaPublicKey) + assert.NotNil(t, rsaOAEPEncryptor.randomSource) } func TestNewJweRsaKeyEncryptionEncryptorImpl_InvalidJwk(t *testing.T) { @@ -20,6 +58,45 @@ func TestNewJweRsaKeyEncryptionEncryptorImpl_InvalidJwk(t *testing.T) { require.NoError(t, err) jwk, err := verifier.Jwk() require.NoError(t, err) - _, err = NewJweRsaKeyEncryptionEncryptorImpl(jwk, jose.AlgA256GCM) + _, err = NewJweRsaKeyEncryptionEncryptorImpl(jwk, rand.Reader) assert.Equal(t, ErrInvalidKeyType, err) +} + + +func TestRSAOAEPJWEEncrypt(t *testing.T) { + rsaOAEPEncryptor := generateEncryptor(t) + jwe, err := rsaOAEPEncryptor.Encrypt([]byte("plaintext"), crypto.SHA256) + require.NoError(t, err) + require.NotEmpty(t, jwe) + + // verify structure + splits := strings.Split(jwe, ".") + require.Equal(t, 5, len(splits)) + + // protected header + require.NotEmpty(t, splits[0]) + protectedHeaderRaw, err := base64.RawURLEncoding.DecodeString(splits[0]) + require.NoError(t, err) + var protectedHeader jose.JweProtectedHeader + err = json.Unmarshal(protectedHeaderRaw, &protectedHeader) + require.NoError(t, err) + assert.Equal(t, jose.AlgRSAOAEP, protectedHeader.Alg) + assert.Equal(t, "1", protectedHeader.Kid) + assert.Equal(t, jose.EncA256GCM, protectedHeader.Enc) + + // encrypted CEK + require.NotEmpty(t, splits[0]) + + // iv + encodedIV := splits[2] + require.NotEmpty(t, encodedIV) + iv, err := base64.RawURLEncoding.DecodeString(encodedIV) + require.NoError(t, err) + assert.Equal(t, 12, len(iv)) + + // ciphertext + require.NotEmpty(t, splits[3]) + + // tag + require.NotEmpty(t, splits[4]) } \ No newline at end of file diff --git a/rsa_private.go b/rsa_private.go index 5c317e1..1e93184 100644 --- a/rsa_private.go +++ b/rsa_private.go @@ -109,13 +109,13 @@ func (rsaKey *RsaPrivateKeyImpl) Certificates() []*x509.Certificate { } // Decrypt decrypt the given ciphertext returning the derived plaintext. -func (rsaKey *RsaPrivateKeyImpl) Decrypt(requested jose.KeyOps, ciphertext []byte) ([]byte, error) { +func (rsaKey *RsaPrivateKeyImpl) Decrypt(requested jose.KeyOps, hash crypto.Hash, ciphertext []byte) ([]byte, error) { ops := intersection(validDecryptionOps, rsaKey.jwk.Ops()) if !isSubset(ops, []jose.KeyOps{requested}) { return nil, ErrInvalidOperations } // SHA1 is still safe when used in the construction of OAEP. - return rsa.DecryptOAEP(crypto.SHA1.New(), rand.Reader, rsaKey.key, ciphertext, nil) + return rsa.DecryptOAEP(hash.New(), rand.Reader, rsaKey.key, ciphertext, nil) } func (rsaKey *RsaPrivateKeyImpl) publicKey() (*RsaPublicKeyImpl, error) { diff --git a/rsa_public.go b/rsa_public.go index 0cb646b..14c188e 100644 --- a/rsa_public.go +++ b/rsa_public.go @@ -116,14 +116,14 @@ func (k *RsaPublicKeyImpl) Verify(operation jose.KeyOps, data []byte, signature } // Encrypt encrypts the given plaintext returning the derived ciphertext. -func (k *RsaPublicKeyImpl) Encrypt(requested jose.KeyOps, data []byte) ([]byte, error) { +func (k *RsaPublicKeyImpl) Encrypt(requested jose.KeyOps, hash crypto.Hash, data []byte) ([]byte, error) { /* Verify the operation being requested is supported by the jwk. */ ops := intersection(validEncryptionOps, k.jwk.Ops()) if !isSubset(ops, []jose.KeyOps{requested}) { return nil, ErrInvalidOperations } // SHA1 is still safe when used in the construction of OAEP. - return rsa.EncryptOAEP(crypto.SHA1.New(), rand.Reader, &k.key, data, nil) + return rsa.EncryptOAEP(hash.New(), rand.Reader, &k.key, data, nil) } //Certificates for verification key