Skip to content

Commit

Permalink
RSA OAEP support for asymmetric encryption (#20)
Browse files Browse the repository at this point in the history
* feature: update go version to 1.23.0

Signed-off-by: Louis Cailliot <[email protected]>

* feature: RSA OAEP encryption with RFC7516

Signed-off-by: Louis Cailliot <[email protected]>

* feature: SHA2 option for RSA OAEP

Signed-off-by: Louis Cailliot <[email protected]>

* feature: RSA OAEP decryption with existing KMS key

Signed-off-by: Louis Cailliot <[email protected]>

* feat: update crypto11 to v1.3.0

Signed-off-by: Louis Cailliot <[email protected]>

---------

Signed-off-by: Louis Cailliot <[email protected]>
  • Loading branch information
IceManGreen authored Feb 17, 2025
1 parent 5d20c95 commit 4b6dc6c
Show file tree
Hide file tree
Showing 21 changed files with 390 additions and 228 deletions.
1 change: 1 addition & 0 deletions .go-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.23.0
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 23 additions & 0 deletions common_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package gose

import (
"encoding/base64"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"strings"
"testing"
)

const (
Expand Down Expand Up @@ -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)
}
16 changes: 8 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
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 (
github.com/davecgh/go-spew v1.1.1 // indirect
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
24 changes: 14 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand All @@ -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=
18 changes: 15 additions & 3 deletions helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
50 changes: 13 additions & 37 deletions helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"crypto/rand"
"crypto/rsa"
"encoding/binary"
"fmt"
"math/big"
"regexp"
"testing"
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
26 changes: 20 additions & 6 deletions hsm/asymmetric_decryption_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
})
}
Expand All @@ -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
}
2 changes: 1 addition & 1 deletion hsm/asymmetric_decryption_key_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
9 changes: 9 additions & 0 deletions jose/jwe.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 8 additions & 1 deletion jose/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 0 additions & 4 deletions jwe_direct_decryptor_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 5 additions & 2 deletions jwe_direct_encryptor_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
var (
cbcAlgToEncMap = map[jose.Alg]jose.Enc{
jose.AlgA256CBC: jose.EncA256CBC,
jose.AlgA256GCM: jose.EncA256GCM,
}
)

Expand All @@ -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{
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 4b6dc6c

Please sign in to comment.