diff --git a/config/keys/private_key.pem b/config/keys/private_key.pem new file mode 100644 index 0000000..b47ec82 --- /dev/null +++ b/config/keys/private_key.pem @@ -0,0 +1,9 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIBOgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu +KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm +o3qGy0t6z09AIJtH+5OeRV1be+N4cDYJKffGzDa88vQENZiRm0GRq6a+HPGQMd2k +TQIhAKMSvzIBnni7ot/OSie2TmJLY4SwTQAevXysE2RbFDYdAiEBCUEaRQnMnbp7 +9mxDXDf6AU0cN/RPBjb9qSHDcWZHGzUCIG2Es59z8ugGrDY+pxLQnwfotadxd+Uy +v/Ow5T0q5gIJAiEAyS4RaI9YG8EWx/2w0T67ZUVAw8eOMB6BIUg0Xcu+3okCIBOs +/5OiPgoTdSy7bcF9IGpSE8ZgGKzgYQVZeN97YE00 +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/config/keys/public_key.pem b/config/keys/public_key.pem new file mode 100644 index 0000000..9020b12 --- /dev/null +++ b/config/keys/public_key.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID0DCCArigAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJGUjET +MBEGA1UECAwKU29tZS1TdGF0ZTEOMAwGA1UEBwwFUGFyaXMxDTALBgNVBAoMBERp +bWkxDTALBgNVBAsMBE5TQlUxEDAOBgNVBAMMB0RpbWkgQ0ExGzAZBgkqhkiG9w0B +CQEWDGRpbWlAZGltaS5mcjAeFw0xNDAxMjgyMDM2NTVaFw0yNDAxMjYyMDM2NTVa +MFsxCzAJBgNVBAYTAkZSMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJ +bnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxFDASBgNVBAMMC3d3dy5kaW1pLmZyMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvpnaPKLIKdvx98KW68lz8pGa +RRcYersNGqPjpifMVjjE8LuCoXgPU0HePnNTUjpShBnynKCvrtWhN+haKbSp+QWX +SxiTrW99HBfAl1MDQyWcukoEb9Cw6INctVUN4iRvkn9T8E6q174RbcnwA/7yTc7p +1NCvw+6B/aAN9l1G2pQXgRdYC/+G6o1IZEHtWhqzE97nY5QKNuUVD0V09dc5CDYB +aKjqetwwv6DFk/GRdOSEd/6bW+20z0qSHpa3YNW6qSp+x5pyYmDrzRIR03os6Dau +ZkChSRyc/Whvurx6o85D6qpzywo8xwNaLZHxTQPgcIA5su9ZIytv9LH2E+lSwwID +AQABo3sweTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVy +YXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQU+tugFtyN+cXe1wxUqeA7X+yS3bgw +HwYDVR0jBBgwFoAUhMwqkbBrGp87HxfvwgPnlGgVR64wDQYJKoZIhvcNAQEFBQAD +ggEBAIEEmqqhEzeXZ4CKhE5UM9vCKzkj5Iv9TFs/a9CcQuepzplt7YVmevBFNOc0 ++1ZyR4tXgi4+5MHGzhYCIVvHo4hKqYm+J+o5mwQInf1qoAHuO7CLD3WNa1sKcVUV +vepIxc/1aHZrG+dPeEHt0MdFfOw13YdUc2FH6AqEdcEL4aV5PXq2eYR8hR4zKbc1 +fBtuqUsvA8NWSIyzQ16fyGve+ANf6vXvUizyvwDrPRv/kfvLNa3ZPnLMMxU98Mvh +PXy3PkB8++6U4Y3vdk2Ni2WYYlIls8yqbM4327IKmkDc2TimS8u60CT47mKU7aDY +cbTV5RDkrlaYwm5yqlTIglvCv7o= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/go.mod b/go.mod index baef675..f7d93b1 100644 --- a/go.mod +++ b/go.mod @@ -6,13 +6,16 @@ require github.com/sirupsen/logrus v1.9.3 require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/go-cmp v0.5.9 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/crypto v0.13.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( bou.ke/monkey v1.0.2 github.com/stretchr/testify v1.8.4 - golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + golang.org/x/sys v0.12.0 // indirect + gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 8145f49..f105c9b 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ bou.ke/monkey v1.0.2/go.mod h1:OqickVX3tNx6t33n1xvtTtu85YN5s6cKwVug+oHMaIA= 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/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -15,10 +17,51 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/config/config.go b/pkg/config/config.go index 87f5919..630075f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -19,11 +19,13 @@ const ( var config *Config type Config struct { - Jwt struct { - PrivateKey string `yaml:"private-key"` - PublicKey string `yaml:"public-key"` - Directory string `yaml:"keys-directory"` - } + Jwt Jwt +} + +type Jwt struct { + PrivateKey string `yaml:"private-key"` + PublicKey string `yaml:"public-key"` + Directory string `yaml:"keys-directory"` } func (config *Config) readConfigFile() (*os.File, error) { diff --git a/pkg/key/key.go b/pkg/key/key.go new file mode 100644 index 0000000..e98d9b7 --- /dev/null +++ b/pkg/key/key.go @@ -0,0 +1,169 @@ +package key + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "os" + "path" + "runtime" + "strings" + + "github.com/megablend/jwt-encryption/pkg/config" + logger "github.com/sirupsen/logrus" + "gopkg.in/square/go-jose.v2" +) + +type Key struct { + Config *config.Config +} + +type SignerType int + +func (s SignerType) String() jose.ContentType { + switch s { + case JWT: + return "JWT" + case JWE: + return "JWE" + default: + return "UNKNOWN" + } +} + +const ( + JWT SignerType = iota + JWE +) + +// PrivateKey returns a private key object from the configured path +func (k *Key) PrivateKey() (*rsa.PrivateKey, error) { + bytes, err := k.getFileBytes(k.Config.Jwt.Directory, k.Config.Jwt.PrivateKey) + if err != nil { + return nil, err + } + + block, err := k.getBlockBytes(bytes) + if err != nil { + return nil, err + } + + key, err := x509.ParsePKCS1PrivateKey(block) + if err != nil { + return nil, err + } + return key, nil +} + +func (k *Key) PublicKey() (*rsa.PublicKey, error) { + bytes, err := k.getFileBytes(k.Config.Jwt.Directory, k.Config.Jwt.PublicKey) + if err != nil { + return nil, err + } + + cert, err := k.parseFromCert(bytes) + if err != nil { + return nil, err + } + + key, ok := cert.PublicKey.(*rsa.PublicKey) + if !ok { + return nil, errors.New("unable to parse certificate into a private key") + } + return key, nil +} + +// Signer returns a preferred method of signing token based on the algorithm and type provided +func (k *Key) Signer(headers map[string]string, algorithm jose.SignatureAlgorithm, signerType SignerType) (jose.Signer, error) { + privateKey, err := k.PrivateKey() + if err != nil { + return nil, err + } + + signerKey := jose.SigningKey{Algorithm: algorithm, Key: privateKey} + + var signerOpts = jose.SignerOptions{} + signerOpts.WithType(signerType.String()) + + // populate custom headers + if len(headers) > 0 { + for k, v := range headers { + signerOpts.WithHeader(jose.HeaderKey(k), v) + } + } + + return jose.NewSigner(signerKey, &signerOpts) +} + +func (k *Key) getBlockBytes(bytes []byte) ([]byte, error) { + block, _ := pem.Decode(bytes) + encrypted := x509.IsEncryptedPEMBlock(block) + b := block.Bytes + + if encrypted { + logger.Warn("PEM block is encrypted") + var err error + b, err = x509.DecryptPEMBlock(block, nil) + if err != nil { + return nil, err + } + } + return b, nil +} + +func (k *Key) getFileBytes(dir, key string) ([]byte, error) { + dir, err := k.getBasePath(dir) + if err != nil { + return nil, err + } + + bytes, err := os.ReadFile(fmt.Sprintf("%s/%s", dir, key)) + if err != nil { + return nil, err + } + + return bytes, nil +} + +func (k *Key) parseFromCert(bytes []byte) (*x509.Certificate, error) { + b, err := k.getBlockBytes(bytes) + if err != nil { + return nil, err + } + + return x509.ParseCertificate(b) +} + +func (k *Key) getBasePath(keyPath string) (string, error) { + if _, err := os.Stat(keyPath); os.IsNotExist(err) { + logger.Warn("the configured keys directory does not exist") + + _, fileName, _, ok := runtime.Caller(0) + if !ok { + return "", errors.New("failed to retrieve caller information for key path configuration") + } + + subDir := "pkg/key" + dir := path.Dir(fileName) + if !strings.Contains(dir, subDir) { + return "", errors.New("invalid configuration folder path configured") + } + + basePath := strings.ReplaceAll(dir, subDir, k.Config.Jwt.Directory) + // ensure that the final base path is valid + if _, err := os.Stat(basePath); os.IsNotExist(err) { + return "", errors.New("invalid configuration base path") + } + + return basePath, nil + } + return keyPath, nil +} + +func New(config *config.Config) *Key { + return &Key{ + Config: config, + } +} diff --git a/pkg/key/key_test.go b/pkg/key/key_test.go new file mode 100644 index 0000000..411ce36 --- /dev/null +++ b/pkg/key/key_test.go @@ -0,0 +1,107 @@ +package key + +import ( + "testing" + + "github.com/megablend/jwt-encryption/pkg/config" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/square/go-jose.v2" +) + +func TestPrivateKey_shouldReturnValidKey(t *testing.T) { + config, configErr := config.New() + key := New(config) + + privateKey, keyErr := key.PrivateKey() + + require.NoError(t, configErr) + assert.NoError(t, keyErr) + assert.NotNil(t, privateKey) +} + +func TestPrivateKey_shouldReturnError(t *testing.T) { + cases := []struct { + name string + config *config.Config + }{ + {"when invalid configuration", &config.Config{}}, + {"when configuration with invalid directory", &config.Config{ + Jwt: config.Jwt{ + Directory: "invalid/directory", + }, + }}, + } + + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + key := New(testCase.config) + + privateKey, keyErr := key.PrivateKey() + + assert.Error(t, keyErr) + assert.Nil(t, privateKey) + }) + } +} + +func TestPublicKey_shouldReturnValidKey(t *testing.T) { + config, configErr := config.New() + key := New(config) + + publicKey, keyErr := key.PublicKey() + + require.NoError(t, configErr) + assert.NoError(t, keyErr) + assert.NotNil(t, publicKey) +} + +func TestPublicKey_shouldReturnError(t *testing.T) { + cases := []struct { + name string + config *config.Config + }{ + {"when invalid configuration", &config.Config{}}, + {"when configuration with invalid directory", &config.Config{ + Jwt: config.Jwt{ + Directory: "invalid/directory", + }, + }}, + } + + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + key := New(testCase.config) + + publicKey, keyErr := key.PublicKey() + + assert.Error(t, keyErr) + assert.Nil(t, publicKey) + }) + } +} + +func TestSigner_shouldReturnValidSignerObject(t *testing.T) { + config, configErr := config.New() + key := New(config) + + signer, signerErr := key.Signer(make(map[string]string), jose.RS256, JWT) + + require.NoError(t, configErr) + assert.NoError(t, signerErr) + assert.NotNil(t, signer) +} + +func TestSigner_shouldFail_whenUnableToGetPrivateKey(t *testing.T) { + config := &config.Config{ + Jwt: config.Jwt{ + Directory: "invalid/directory", + }, + } + key := New(config) + + signer, signerErr := key.Signer(make(map[string]string), jose.RS256, JWT) + + assert.Error(t, signerErr) + assert.Nil(t, signer) +}