diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85e7c1d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea/ diff --git a/LICENCE.txt b/LICENCE.txt new file mode 100644 index 0000000..a2056f7 --- /dev/null +++ b/LICENCE.txt @@ -0,0 +1,22 @@ +MIT License. + +Copyright 2019 Thales e-Security, Inc + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..44791ad --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +.PHONY: all clean lint vet coverage +GOCMD:=go +GOCLEAN:=$(GOCMD) clean +GOTEST:=$(GOCMD) test +GOGET:=$(GOCMD) get +GOVET:=$(GOCMD) vet +GOLINT:=golint +SRCS:=$(wildcard *.go) $(wildcard jose/*.go) + +all: clean lint vet coverage + +clean: + $(GOCLEAN) + rm -f coverage.out + +lint: + $(GOLINT) -set_exit_status ./... + +vet: + $(GOVET) ./... + +test: + $(GOTEST) -gcflags=-l -short -race ./... + +coverage: coverage.out + +coverage.out: $(SRCS) + $(GOTEST) -gcflags=-l -coverprofile coverage.out ./... diff --git a/README.md b/README.md new file mode 100644 index 0000000..86e8ad8 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# GOSE - JOSE and friends for the Go developer + +## Overview + +GOSE is JOSE/JWT/JWK/JWS/JWKS implemented in Go with Helpers, and examples. + +It contains implementations of the JOSE suite of types and helpers for many different use cases. + + +## Mission + +- Simple +- Compliant +- Safe +- Efficient +- Extensible + +## Examples + +Examples are provided under the `/examples` folder to illustrate correct use of this package. + + + + diff --git a/aes_gcm_cryptor.go b/aes_gcm_cryptor.go new file mode 100644 index 0000000..8dce393 --- /dev/null +++ b/aes_gcm_cryptor.go @@ -0,0 +1,131 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "crypto/cipher" + "crypto/rand" + "io" + + "github.com/thalesignite/gose/jose" +) + +var validEncryptionOpts = []jose.KeyOps{jose.KeyOpsEncrypt} +var validDecryptionOpts = []jose.KeyOps{jose.KeyOpsDecrypt} +var validCryptorOpts = []jose.KeyOps{jose.KeyOpsEncrypt, jose.KeyOpsDecrypt} + +// AesGcmCryptor provides AES GCM encryption and decryption functions. +type AesGcmCryptor struct { + kid string + alg jose.Alg + aead cipher.AEAD + opts []jose.KeyOps + rng io.Reader +} + +// Kid the key identity +func (cryptor *AesGcmCryptor) Kid() string { + return cryptor.kid +} + +// Algorithm the supported algorithm +func (cryptor *AesGcmCryptor) Algorithm() jose.Alg { + return cryptor.alg +} + +// GenerateNonce generate a nonce of the correct size for use with GCM encryption/decryption from a random source. +func (cryptor *AesGcmCryptor) GenerateNonce() ([]byte, error) { + nonce := make([]byte, cryptor.aead.NonceSize()) + if _, err := cryptor.rng.Read(nonce); err != nil { + return nil, err + } + return nonce, nil +} + +// Open decrypt a previously encrypted ciphertext. +func (cryptor *AesGcmCryptor) Open(operation jose.KeyOps, nonce, ciphertext, aad, tag []byte) (plaintext []byte, err error) { + ops := intersection(validDecryptionOpts, cryptor.opts) + if !isSubset(ops, []jose.KeyOps{operation}) { + err = ErrInvalidOperations + return + } + dst := make([]byte, 0, len(ciphertext)) + ciphertextAndTag := make([]byte, len(ciphertext)+len(tag)) + _ = copy(ciphertextAndTag, ciphertext) + _ = copy(ciphertextAndTag[len(ciphertext):], tag) + if dst, err = cryptor.aead.Open(dst, nonce, ciphertextAndTag, aad); err != nil { + return + } + plaintext = dst + return +} + +// Seal encrypt a supplied plaintext and AAD. +func (cryptor *AesGcmCryptor) Seal(operation jose.KeyOps, nonce, plaintext, aad []byte) (ciphertext, tag []byte, err error) { + ops := intersection(validEncryptionOpts, cryptor.opts) + if !isSubset(ops, []jose.KeyOps{operation}) { + err = ErrInvalidOperations + return + } + if len(nonce) != cryptor.aead.NonceSize() { + err = ErrInvalidNonce + return + } + sz := cryptor.aead.Overhead() + len(plaintext) + dst := make([]byte, 0, sz) + dst = cryptor.aead.Seal(dst, nonce, plaintext, aad) + ciphertext = dst[:len(plaintext)] + tag = dst[len(plaintext):] + return +} + +// NewAesGcmCryptorFromJwk create a new instance of an AesGCmCryptor from a JWK. +func NewAesGcmCryptorFromJwk(jwk jose.Jwk, required []jose.KeyOps) (AuthenticatedEncryptionKey, error) { + /* Check jwk can be used to encrypt or decrypt */ + ops := intersection(validCryptorOpts, jwk.Ops()) + if len(ops) == 0 { + return nil, ErrInvalidOperations + } + /* Load the jwk */ + aead, err := LoadSymmetricAEAD(jwk, required) + if err != nil { + return nil, err + } + return &AesGcmCryptor{ + kid: jwk.Kid(), + alg: jwk.Alg(), + aead: aead, + rng: rand.Reader, + opts: jwk.Ops(), + }, nil +} + +// NewAesGcmCryptor create a new instance of an AesGCmCryptor from the supplied parameters. +func NewAesGcmCryptor(aead cipher.AEAD, rng io.Reader, kid string, alg jose.Alg, opeartions []jose.KeyOps) (AuthenticatedEncryptionKey, error) { + return &AesGcmCryptor{ + kid: kid, + alg: alg, + aead: aead, + rng: rng, + opts: opeartions, + }, nil +} diff --git a/aes_gcm_cryptor_test.go b/aes_gcm_cryptor_test.go new file mode 100644 index 0000000..dbd13cf --- /dev/null +++ b/aes_gcm_cryptor_test.go @@ -0,0 +1,183 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "crypto/rand" + "testing" + + "github.com/thalesignite/gose/jose" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + fakeKeyMaterial = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} + fakeNonce = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} + fakeTag = []byte{0x70, 0x7d, 0xcf, 0x80, 0xa1, 0xb9, 0xa6, 0x17, 0x03, 0xe7, 0x95, 0xd4, 0xd1, 0x09, 0xf0, 0xfd} + fakePlaintext = []byte{0x01} + fakeCiphertext = []byte{0xcf} +) + +func TestNewAesGcmCryptor_InvalidOps(t *testing.T) { + key := &jose.OctSecretKey{} + key.SetAlg(jose.AlgA256GCM) + key.SetOps([]jose.KeyOps{jose.KeyOpsEncrypt}) + cryptor, err := NewAesGcmCryptorFromJwk(key, []jose.KeyOps{jose.KeyOpsDecrypt}) + assert.Nil(t, cryptor) + assert.Equal(t, err, ErrInvalidOperations) +} + +func TestNewAesGcmCryptor_InvalidKey(t *testing.T) { + key := &jose.OctSecretKey{} + key.SetAlg(jose.AlgES256) + key.SetOps([]jose.KeyOps{jose.KeyOpsEncrypt, jose.KeyOpsDecrypt}) + cryptor, err := NewAesGcmCryptorFromJwk(key, []jose.KeyOps{jose.KeyOpsDecrypt}) + assert.Nil(t, cryptor) + assert.Equal(t, err, ErrInvalidKeyType) +} + +func TestNewAesGcmCryptor(t *testing.T) { + key := &jose.OctSecretKey{} + key.SetAlg(jose.AlgA256GCM) + key.SetOps([]jose.KeyOps{jose.KeyOpsEncrypt, jose.KeyOpsDecrypt}) + key.SetKid("something-unique") + key.K.SetBytes(fakeKeyMaterial) + cryptor, err := NewAesGcmCryptorFromJwk(key, []jose.KeyOps{jose.KeyOpsDecrypt}) + assert.NotNil(t, cryptor) + assert.NoError(t, err) +} + +func TestAesGcmCryptor_GenerateNonce(t *testing.T) { + key := &jose.OctSecretKey{} + key.SetAlg(jose.AlgA256GCM) + key.SetOps([]jose.KeyOps{jose.KeyOpsEncrypt, jose.KeyOpsDecrypt}) + key.SetKid("something-unique") + key.K.SetBytes(fakeKeyMaterial) + cryptor, err := NewAesGcmCryptorFromJwk(key, []jose.KeyOps{jose.KeyOpsDecrypt}) + require.NotNil(t, cryptor) + require.NoError(t, err) + + nonce, err := cryptor.GenerateNonce() + require.NoError(t, err) + assert.Len(t, nonce, 12) +} + +func TestAesGcmCryptor_Getters(t *testing.T) { + key := &jose.OctSecretKey{} + key.SetAlg(jose.AlgA256GCM) + key.SetOps([]jose.KeyOps{jose.KeyOpsEncrypt, jose.KeyOpsDecrypt}) + key.SetKid("something-unique") + key.K.SetBytes(fakeKeyMaterial) + cryptor, err := NewAesGcmCryptorFromJwk(key, []jose.KeyOps{jose.KeyOpsDecrypt}) + require.NotNil(t, cryptor) + require.NoError(t, err) + + assert.Equal(t, cryptor.Kid(), "something-unique") + assert.Equal(t, cryptor.Algorithm(), jose.AlgA256GCM) +} + +func TestAesGcmCryptor_Seal_InvalidOps(t *testing.T) { + key := &jose.OctSecretKey{} + key.SetAlg(jose.AlgA256GCM) + key.SetOps([]jose.KeyOps{jose.KeyOpsDecrypt}) + key.SetKid("something-unique") + key.K.SetBytes(fakeKeyMaterial) + cryptor, err := NewAesGcmCryptorFromJwk(key, []jose.KeyOps{jose.KeyOpsDecrypt}) + require.NotNil(t, cryptor) + require.NoError(t, err) + + ciphertext, tag, err := cryptor.Seal(jose.KeyOpsEncrypt, fakeNonce, fakePlaintext, nil) + assert.Nil(t, ciphertext) + assert.Nil(t, tag) + assert.Equal(t, err, ErrInvalidOperations) +} + +func TestAesGcmCryptor_Seal(t *testing.T) { + key := &jose.OctSecretKey{} + key.SetAlg(jose.AlgA256GCM) + key.SetOps([]jose.KeyOps{jose.KeyOpsEncrypt}) + key.SetKid("something-unique") + key.K.SetBytes(fakeKeyMaterial) + cryptor, err := NewAesGcmCryptorFromJwk(key, []jose.KeyOps{jose.KeyOpsEncrypt}) + require.NotNil(t, cryptor) + require.NoError(t, err) + + ciphertext, tag, err := cryptor.Seal(jose.KeyOpsEncrypt, fakeNonce, fakePlaintext, nil) + assert.Len(t, ciphertext, 1) + assert.Len(t, tag, 16) + assert.NoError(t, err) +} + +func TestAesGcmCryptor_Open_InvalidOps(t *testing.T) { + key := &jose.OctSecretKey{} + key.SetAlg(jose.AlgA256GCM) + key.SetOps([]jose.KeyOps{jose.KeyOpsEncrypt}) + key.SetKid("something-unique") + key.K.SetBytes(fakeKeyMaterial) + cryptor, err := NewAesGcmCryptorFromJwk(key, []jose.KeyOps{jose.KeyOpsEncrypt}) + require.NotNil(t, cryptor) + require.NoError(t, err) + + plaintext, err := cryptor.Open(jose.KeyOpsDecrypt, fakeNonce, fakeCiphertext, nil, fakeTag) + assert.Nil(t, plaintext) + assert.Equal(t, err, ErrInvalidOperations) +} + +func TestAesGcmCryptor_Open(t *testing.T) { + key := &jose.OctSecretKey{} + key.SetAlg(jose.AlgA256GCM) + key.SetOps([]jose.KeyOps{jose.KeyOpsDecrypt}) + key.SetKid("something-unique") + key.K.SetBytes(fakeKeyMaterial) + cryptor, err := NewAesGcmCryptorFromJwk(key, []jose.KeyOps{jose.KeyOpsDecrypt}) + require.NotNil(t, cryptor) + require.NoError(t, err) + + plaintext, err := cryptor.Open(jose.KeyOpsDecrypt, fakeNonce, fakeCiphertext, nil, fakeTag) + assert.Equal(t, plaintext, fakePlaintext) + assert.NoError(t, err) +} + +func TestAesGcmCryptor_RoundTrip(t *testing.T) { + key := &jose.OctSecretKey{} + key.SetAlg(jose.AlgA256GCM) + key.SetOps([]jose.KeyOps{jose.KeyOpsEncrypt, jose.KeyOpsDecrypt}) + key.SetKid("something-unique") + key.K.SetBytes(fakeKeyMaterial) + cryptor, err := NewAesGcmCryptorFromJwk(key, []jose.KeyOps{jose.KeyOpsDecrypt, jose.KeyOpsEncrypt}) + require.NotNil(t, cryptor) + require.NoError(t, err) + + for i := 0; i < 50; i++ { + toSeal := make([]byte, 374) + _, err = rand.Read(toSeal) + require.Nil(t, err) + nonce, err := cryptor.GenerateNonce() + require.NoError(t, err) + ciphertext, tag, err := cryptor.Seal(jose.KeyOpsEncrypt, nonce, toSeal, nil) + require.Nil(t, err) + plaintext, err := cryptor.Open(jose.KeyOpsDecrypt, nonce, ciphertext, nil, tag) + assert.NoError(t, err) + assert.Equal(t, plaintext, toSeal) + } +} diff --git a/coverage.out b/coverage.out new file mode 100644 index 0000000..f5d2095 --- /dev/null +++ b/coverage.out @@ -0,0 +1,655 @@ +mode: set +github.com/thalesignite/gose/jose/jwe.go:56.45,58.61 2 0 +github.com/thalesignite/gose/jose/jwe.go:61.2,62.8 2 0 +github.com/thalesignite/gose/jose/jwe.go:58.61,60.3 1 0 +github.com/thalesignite/gose/jose/jwe.go:66.51,69.21 2 0 +github.com/thalesignite/gose/jose/jwe.go:73.2,73.90 1 0 +github.com/thalesignite/gose/jose/jwe.go:76.2,76.73 1 0 +github.com/thalesignite/gose/jose/jwe.go:80.2,80.31 1 0 +github.com/thalesignite/gose/jose/jwe.go:85.2,85.76 1 0 +github.com/thalesignite/gose/jose/jwe.go:88.2,88.84 1 0 +github.com/thalesignite/gose/jose/jwe.go:91.2,91.77 1 0 +github.com/thalesignite/gose/jose/jwe.go:94.2,94.8 1 0 +github.com/thalesignite/gose/jose/jwe.go:69.21,72.3 2 0 +github.com/thalesignite/gose/jose/jwe.go:73.90,75.3 1 0 +github.com/thalesignite/gose/jose/jwe.go:76.73,78.3 1 0 +github.com/thalesignite/gose/jose/jwe.go:80.31,81.87 1 0 +github.com/thalesignite/gose/jose/jwe.go:81.87,83.4 1 0 +github.com/thalesignite/gose/jose/jwe.go:85.76,87.3 1 0 +github.com/thalesignite/gose/jose/jwe.go:88.84,90.3 1 0 +github.com/thalesignite/gose/jose/jwe.go:91.77,93.3 1 0 +github.com/thalesignite/gose/jose/jwe.go:98.34,107.2 2 0 +github.com/thalesignite/gose/jose/jwk.go:39.61,42.2 2 1 +github.com/thalesignite/gose/jose/jwk.go:45.61,47.69 2 1 +github.com/thalesignite/gose/jose/jwk.go:50.2,51.12 2 1 +github.com/thalesignite/gose/jose/jwk.go:47.69,49.3 1 0 +github.com/thalesignite/gose/jose/jwk.go:60.38,62.2 1 0 +github.com/thalesignite/gose/jose/jwk.go:65.57,68.2 2 0 +github.com/thalesignite/gose/jose/jwk.go:71.55,73.79 2 1 +github.com/thalesignite/gose/jose/jwk.go:76.2,76.25 1 1 +github.com/thalesignite/gose/jose/jwk.go:79.2,79.12 1 1 +github.com/thalesignite/gose/jose/jwk.go:73.79,75.3 1 0 +github.com/thalesignite/gose/jose/jwk.go:76.25,78.3 1 0 +github.com/thalesignite/gose/jose/jwk.go:83.61,84.25 1 1 +github.com/thalesignite/gose/jose/jwk.go:88.2,89.8 2 1 +github.com/thalesignite/gose/jose/jwk.go:84.25,87.3 2 0 +github.com/thalesignite/gose/jose/jwk.go:123.34,125.2 1 0 +github.com/thalesignite/gose/jose/jwk.go:127.40,129.2 1 0 +github.com/thalesignite/gose/jose/jwk.go:131.36,133.2 1 0 +github.com/thalesignite/gose/jose/jwk.go:135.42,137.2 1 0 +github.com/thalesignite/gose/jose/jwk.go:139.31,141.2 1 0 +github.com/thalesignite/gose/jose/jwk.go:143.37,145.2 1 0 +github.com/thalesignite/gose/jose/jwk.go:147.34,149.2 1 1 +github.com/thalesignite/gose/jose/jwk.go:151.40,153.2 1 1 +github.com/thalesignite/gose/jose/jwk.go:155.47,157.29 2 1 +github.com/thalesignite/gose/jose/jwk.go:160.2,160.14 1 1 +github.com/thalesignite/gose/jose/jwk.go:157.29,159.3 1 1 +github.com/thalesignite/gose/jose/jwk.go:163.53,164.24 1 1 +github.com/thalesignite/gose/jose/jwk.go:164.24,168.3 1 1 +github.com/thalesignite/gose/jose/jwk.go:171.40,173.2 1 0 +github.com/thalesignite/gose/jose/jwk.go:175.47,177.2 1 1 +github.com/thalesignite/gose/jose/jwk.go:179.46,181.23 1 1 +github.com/thalesignite/gose/jose/jwk.go:192.2,192.23 1 1 +github.com/thalesignite/gose/jose/jwk.go:204.2,204.12 1 1 +github.com/thalesignite/gose/jose/jwk.go:181.23,182.56 1 1 +github.com/thalesignite/gose/jose/jwk.go:182.56,183.40 1 1 +github.com/thalesignite/gose/jose/jwk.go:183.40,184.26 1 1 +github.com/thalesignite/gose/jose/jwk.go:184.26,186.6 1 1 +github.com/thalesignite/gose/jose/jwk.go:192.23,193.22 1 1 +github.com/thalesignite/gose/jose/jwk.go:193.22,195.73 2 1 +github.com/thalesignite/gose/jose/jwk.go:198.4,199.64 2 1 +github.com/thalesignite/gose/jose/jwk.go:195.73,197.5 1 0 +github.com/thalesignite/gose/jose/jwk.go:199.64,201.5 1 1 +github.com/thalesignite/gose/jose/jwk.go:220.34,222.2 1 1 +github.com/thalesignite/gose/jose/jwk.go:225.62,237.2 3 1 +github.com/thalesignite/gose/jose/jwk.go:240.62,250.57 2 1 +github.com/thalesignite/gose/jose/jwk.go:253.2,253.31 1 1 +github.com/thalesignite/gose/jose/jwk.go:256.2,257.8 2 1 +github.com/thalesignite/gose/jose/jwk.go:250.57,252.3 1 0 +github.com/thalesignite/gose/jose/jwk.go:253.31,255.3 1 0 +github.com/thalesignite/gose/jose/jwk.go:277.35,279.2 1 1 +github.com/thalesignite/gose/jose/jwk.go:282.63,296.2 3 1 +github.com/thalesignite/gose/jose/jwk.go:299.63,311.57 2 1 +github.com/thalesignite/gose/jose/jwk.go:314.2,314.31 1 1 +github.com/thalesignite/gose/jose/jwk.go:317.2,318.8 2 1 +github.com/thalesignite/gose/jose/jwk.go:311.57,313.3 1 0 +github.com/thalesignite/gose/jose/jwk.go:314.31,316.3 1 0 +github.com/thalesignite/gose/jose/jwk.go:335.33,337.2 1 1 +github.com/thalesignite/gose/jose/jwk.go:340.61,352.2 3 1 +github.com/thalesignite/gose/jose/jwk.go:355.61,365.57 2 1 +github.com/thalesignite/gose/jose/jwk.go:368.2,368.30 1 1 +github.com/thalesignite/gose/jose/jwk.go:371.2,372.8 2 1 +github.com/thalesignite/gose/jose/jwk.go:365.57,367.3 1 0 +github.com/thalesignite/gose/jose/jwk.go:368.30,370.3 1 0 +github.com/thalesignite/gose/jose/jwk.go:387.34,389.2 1 1 +github.com/thalesignite/gose/jose/jwk.go:392.62,406.2 3 1 +github.com/thalesignite/gose/jose/jwk.go:409.62,421.57 2 1 +github.com/thalesignite/gose/jose/jwk.go:424.2,424.30 1 1 +github.com/thalesignite/gose/jose/jwk.go:427.2,428.8 2 1 +github.com/thalesignite/gose/jose/jwk.go:421.57,423.3 1 0 +github.com/thalesignite/gose/jose/jwk.go:424.30,426.3 1 0 +github.com/thalesignite/gose/jose/jwk.go:443.34,445.2 1 1 +github.com/thalesignite/gose/jose/jwk.go:448.62,460.2 3 1 +github.com/thalesignite/gose/jose/jwk.go:463.62,473.57 2 1 +github.com/thalesignite/gose/jose/jwk.go:476.2,476.31 1 1 +github.com/thalesignite/gose/jose/jwk.go:479.2,480.8 2 1 +github.com/thalesignite/gose/jose/jwk.go:473.57,475.3 1 0 +github.com/thalesignite/gose/jose/jwk.go:476.31,478.3 1 0 +github.com/thalesignite/gose/jose/jwk.go:484.62,490.48 3 1 +github.com/thalesignite/gose/jose/jwk.go:493.2,493.55 1 1 +github.com/thalesignite/gose/jose/jwk.go:496.2,496.21 1 1 +github.com/thalesignite/gose/jose/jwk.go:530.2,530.8 1 1 +github.com/thalesignite/gose/jose/jwk.go:490.48,492.3 1 0 +github.com/thalesignite/gose/jose/jwk.go:493.55,495.3 1 0 +github.com/thalesignite/gose/jose/jwk.go:497.14,499.45 2 1 +github.com/thalesignite/gose/jose/jwk.go:503.3,503.32 1 1 +github.com/thalesignite/gose/jose/jwk.go:508.3,508.9 1 1 +github.com/thalesignite/gose/jose/jwk.go:509.13,511.44 2 1 +github.com/thalesignite/gose/jose/jwk.go:514.3,514.31 1 1 +github.com/thalesignite/gose/jose/jwk.go:519.3,519.9 1 1 +github.com/thalesignite/gose/jose/jwk.go:520.14,522.45 2 1 +github.com/thalesignite/gose/jose/jwk.go:525.3,525.13 1 1 +github.com/thalesignite/gose/jose/jwk.go:526.10,528.9 2 0 +github.com/thalesignite/gose/jose/jwk.go:499.45,501.4 1 0 +github.com/thalesignite/gose/jose/jwk.go:503.32,505.4 1 1 +github.com/thalesignite/gose/jose/jwk.go:505.9,507.4 1 0 +github.com/thalesignite/gose/jose/jwk.go:511.44,513.4 1 0 +github.com/thalesignite/gose/jose/jwk.go:514.31,516.4 1 1 +github.com/thalesignite/gose/jose/jwk.go:516.9,518.4 1 0 +github.com/thalesignite/gose/jose/jwk.go:522.45,524.4 1 0 +github.com/thalesignite/gose/jose/jwks.go:35.49,39.59 2 1 +github.com/thalesignite/gose/jose/jwks.go:42.2,42.40 1 1 +github.com/thalesignite/gose/jose/jwks.go:49.2,49.12 1 1 +github.com/thalesignite/gose/jose/jwks.go:39.59,41.3 1 0 +github.com/thalesignite/gose/jose/jwks.go:42.40,44.17 2 1 +github.com/thalesignite/gose/jose/jwks.go:47.3,47.31 1 1 +github.com/thalesignite/gose/jose/jwks.go:44.17,46.4 1 0 +github.com/thalesignite/gose/jose/jws.go:52.67,54.57 2 1 +github.com/thalesignite/gose/jose/jws.go:57.2,57.33 1 1 +github.com/thalesignite/gose/jose/jws.go:72.2,72.8 1 1 +github.com/thalesignite/gose/jose/jws.go:54.57,56.3 1 0 +github.com/thalesignite/gose/jose/jws.go:58.14,59.43 1 1 +github.com/thalesignite/gose/jose/jws.go:60.21,61.26 1 1 +github.com/thalesignite/gose/jose/jws.go:69.10,70.22 1 0 +github.com/thalesignite/gose/jose/jws.go:61.26,63.11 2 1 +github.com/thalesignite/gose/jose/jws.go:67.4,67.46 1 1 +github.com/thalesignite/gose/jose/jws.go:63.11,66.5 2 1 +github.com/thalesignite/gose/jose/jws.go:76.67,77.28 1 1 +github.com/thalesignite/gose/jose/jws.go:78.9,80.40 1 1 +github.com/thalesignite/gose/jose/jws.go:81.10,82.37 1 1 +github.com/thalesignite/gose/jose/jws.go:94.56,95.31 1 1 +github.com/thalesignite/gose/jose/jws.go:100.2,100.55 1 1 +github.com/thalesignite/gose/jose/jws.go:104.2,105.57 2 1 +github.com/thalesignite/gose/jose/jws.go:108.2,109.58 2 1 +github.com/thalesignite/gose/jose/jws.go:112.2,115.8 2 1 +github.com/thalesignite/gose/jose/jws.go:95.31,99.3 2 0 +github.com/thalesignite/gose/jose/jws.go:100.55,103.3 2 0 +github.com/thalesignite/gose/jose/jws.go:105.57,107.3 1 0 +github.com/thalesignite/gose/jose/jws.go:109.58,111.3 1 0 +github.com/thalesignite/gose/jose/jws.go:119.49,122.2 2 0 +github.com/thalesignite/gose/jose/jws.go:125.64,128.21 2 1 +github.com/thalesignite/gose/jose/jws.go:132.2,132.67 1 1 +github.com/thalesignite/gose/jose/jws.go:135.2,135.68 1 1 +github.com/thalesignite/gose/jose/jws.go:139.2,139.83 1 1 +github.com/thalesignite/gose/jose/jws.go:143.2,144.8 2 1 +github.com/thalesignite/gose/jose/jws.go:128.21,131.3 2 0 +github.com/thalesignite/gose/jose/jws.go:132.67,134.3 1 0 +github.com/thalesignite/gose/jose/jws.go:135.68,137.3 1 0 +github.com/thalesignite/gose/jose/jws.go:139.83,142.3 2 0 +github.com/thalesignite/gose/jose/jws.go:148.55,150.2 1 1 +github.com/thalesignite/gose/jose/jwt.go:73.92,76.39 2 1 +github.com/thalesignite/gose/jose/jwt.go:99.2,99.8 1 1 +github.com/thalesignite/gose/jose/jwt.go:76.39,79.62 2 1 +github.com/thalesignite/gose/jose/jwt.go:79.62,81.23 2 1 +github.com/thalesignite/gose/jose/jwt.go:85.4,86.45 2 1 +github.com/thalesignite/gose/jose/jwt.go:81.23,84.5 1 0 +github.com/thalesignite/gose/jose/jwt.go:86.45,87.25 1 1 +github.com/thalesignite/gose/jose/jwt.go:91.5,91.74 1 1 +github.com/thalesignite/gose/jose/jwt.go:95.5,95.25 1 1 +github.com/thalesignite/gose/jose/jwt.go:87.25,90.6 1 0 +github.com/thalesignite/gose/jose/jwt.go:91.74,93.6 1 0 +github.com/thalesignite/gose/jose/jwt.go:104.60,106.53 2 1 +github.com/thalesignite/gose/jose/jwt.go:110.2,110.75 1 1 +github.com/thalesignite/gose/jose/jwt.go:113.2,113.74 1 1 +github.com/thalesignite/gose/jose/jwt.go:117.2,118.8 2 1 +github.com/thalesignite/gose/jose/jwt.go:106.53,108.3 1 0 +github.com/thalesignite/gose/jose/jwt.go:110.75,112.3 1 0 +github.com/thalesignite/gose/jose/jwt.go:113.74,115.3 1 0 +github.com/thalesignite/gose/jose/jwt.go:122.80,124.8 2 1 +github.com/thalesignite/gose/jose/jwt.go:129.2,129.12 1 1 +github.com/thalesignite/gose/jose/jwt.go:124.8,125.60 1 1 +github.com/thalesignite/gose/jose/jwt.go:125.60,127.4 1 1 +github.com/thalesignite/gose/jose/jwt.go:133.59,147.57 3 1 +github.com/thalesignite/gose/jose/jwt.go:151.2,151.33 1 1 +github.com/thalesignite/gose/jose/jwt.go:167.2,170.57 3 1 +github.com/thalesignite/gose/jose/jwt.go:174.2,174.36 1 1 +github.com/thalesignite/gose/jose/jwt.go:177.2,177.39 1 1 +github.com/thalesignite/gose/jose/jwt.go:147.57,149.3 1 1 +github.com/thalesignite/gose/jose/jwt.go:151.33,153.50 1 1 +github.com/thalesignite/gose/jose/jwt.go:157.3,164.33 2 1 +github.com/thalesignite/gose/jose/jwt.go:153.50,156.4 2 0 +github.com/thalesignite/gose/jose/jwt.go:170.57,172.3 1 1 +github.com/thalesignite/gose/jose/jwt.go:174.36,176.3 1 1 +github.com/thalesignite/gose/jose/jwt.go:188.32,189.31 1 1 +github.com/thalesignite/gose/jose/jwt.go:193.2,193.55 1 1 +github.com/thalesignite/gose/jose/jwt.go:197.2,197.42 1 1 +github.com/thalesignite/gose/jose/jwt.go:202.2,202.12 1 1 +github.com/thalesignite/gose/jose/jwt.go:189.31,192.3 1 1 +github.com/thalesignite/gose/jose/jwt.go:193.55,196.3 1 1 +github.com/thalesignite/gose/jose/jwt.go:197.42,198.50 1 1 +github.com/thalesignite/gose/jose/jwt.go:198.50,200.4 1 1 +github.com/thalesignite/gose/jose/jwt.go:206.56,207.36 1 1 +github.com/thalesignite/gose/jose/jwt.go:210.2,214.26 2 1 +github.com/thalesignite/gose/jose/jwt.go:207.36,209.3 1 1 +github.com/thalesignite/gose/jose/jwt.go:218.64,226.48 3 1 +github.com/thalesignite/gose/jose/jwt.go:230.2,232.36 3 1 +github.com/thalesignite/gose/jose/jwt.go:236.2,236.8 1 1 +github.com/thalesignite/gose/jose/jwt.go:226.48,228.3 1 0 +github.com/thalesignite/gose/jose/jwt.go:232.36,235.3 2 0 +github.com/thalesignite/gose/jose/types.go:164.86,167.51 2 1 +github.com/thalesignite/gose/jose/types.go:172.2,175.66 4 1 +github.com/thalesignite/gose/jose/types.go:179.2,180.8 2 1 +github.com/thalesignite/gose/jose/types.go:167.51,170.3 2 1 +github.com/thalesignite/gose/jose/types.go:175.66,177.3 1 0 +github.com/thalesignite/gose/jose/types.go:183.84,185.19 1 1 +github.com/thalesignite/gose/jose/types.go:190.2,195.8 6 1 +github.com/thalesignite/gose/jose/types.go:185.19,188.3 2 1 +github.com/thalesignite/gose/jose/types.go:204.47,207.2 2 1 +github.com/thalesignite/gose/jose/types.go:210.44,213.2 2 0 +github.com/thalesignite/gose/jose/types.go:216.33,218.2 1 1 +github.com/thalesignite/gose/jose/types.go:221.31,223.2 1 0 +github.com/thalesignite/gose/jose/types.go:226.56,229.2 2 1 +github.com/thalesignite/gose/jose/types.go:232.56,234.74 2 1 +github.com/thalesignite/gose/jose/types.go:237.2,238.8 2 1 +github.com/thalesignite/gose/jose/types.go:234.74,236.3 1 1 +github.com/thalesignite/gose/jose/types.go:247.31,249.2 1 1 +github.com/thalesignite/gose/jose/types.go:252.48,256.2 3 1 +github.com/thalesignite/gose/jose/types.go:259.54,262.2 2 1 +github.com/thalesignite/gose/jose/types.go:265.43,268.2 2 1 +github.com/thalesignite/gose/jose/types.go:270.62,273.69 3 1 +github.com/thalesignite/gose/jose/types.go:277.2,277.49 1 1 +github.com/thalesignite/gose/jose/types.go:280.2,280.12 1 1 +github.com/thalesignite/gose/jose/types.go:273.69,275.3 1 0 +github.com/thalesignite/gose/jose/types.go:277.49,279.3 1 0 +github.com/thalesignite/gose/signer.go:64.78,67.19 2 1 +github.com/thalesignite/gose/signer.go:71.2,72.16 2 1 +github.com/thalesignite/gose/signer.go:76.2,76.20 1 1 +github.com/thalesignite/gose/signer.go:67.19,69.3 1 1 +github.com/thalesignite/gose/signer.go:72.16,74.3 1 1 +github.com/thalesignite/gose/signer.go:77.27,78.66 1 1 +github.com/thalesignite/gose/signer.go:79.26,80.67 1 1 +github.com/thalesignite/gose/signer.go:81.10,82.32 1 0 +github.com/thalesignite/gose/signer.go:87.51,89.2 1 0 +github.com/thalesignite/gose/signer.go:92.58,94.2 1 0 +github.com/thalesignite/gose/signer.go:97.44,100.2 1 1 +github.com/thalesignite/gose/signer.go:103.55,105.2 1 1 +github.com/thalesignite/gose/signer.go:108.52,110.2 1 1 +github.com/thalesignite/gose/signer.go:113.57,115.2 1 1 +github.com/thalesignite/gose/signer.go:118.60,121.32 3 1 +github.com/thalesignite/gose/signer.go:128.2,133.52 3 1 +github.com/thalesignite/gose/signer.go:136.2,136.36 1 1 +github.com/thalesignite/gose/signer.go:122.23,124.46 2 1 +github.com/thalesignite/gose/signer.go:125.10,126.35 1 0 +github.com/thalesignite/gose/signer.go:133.52,135.3 1 0 +github.com/thalesignite/gose/signer.go:140.88,143.46 2 1 +github.com/thalesignite/gose/signer.go:147.2,148.48 2 1 +github.com/thalesignite/gose/signer.go:151.2,153.51 3 1 +github.com/thalesignite/gose/signer.go:143.46,145.3 1 0 +github.com/thalesignite/gose/signer.go:148.48,150.3 1 0 +github.com/thalesignite/gose/signer.go:157.66,159.2 1 0 +github.com/thalesignite/gose/signer.go:162.67,164.16 2 1 +github.com/thalesignite/gose/signer.go:167.2,167.38 1 1 +github.com/thalesignite/gose/signer.go:164.16,166.3 1 0 +github.com/thalesignite/gose/jwt_verifier.go:36.120,39.52 3 1 +github.com/thalesignite/gose/jwt_verifier.go:42.2,43.34 2 1 +github.com/thalesignite/gose/jwt_verifier.go:47.2,47.36 1 1 +github.com/thalesignite/gose/jwt_verifier.go:51.2,51.64 1 1 +github.com/thalesignite/gose/jwt_verifier.go:56.2,57.37 2 1 +github.com/thalesignite/gose/jwt_verifier.go:62.2,62.12 1 1 +github.com/thalesignite/gose/jwt_verifier.go:69.2,69.32 1 1 +github.com/thalesignite/gose/jwt_verifier.go:73.2,74.16 2 1 +github.com/thalesignite/gose/jwt_verifier.go:79.2,79.41 1 1 +github.com/thalesignite/gose/jwt_verifier.go:84.2,84.69 1 1 +github.com/thalesignite/gose/jwt_verifier.go:88.2,90.8 3 1 +github.com/thalesignite/gose/jwt_verifier.go:39.52,41.3 1 0 +github.com/thalesignite/gose/jwt_verifier.go:43.34,46.3 2 1 +github.com/thalesignite/gose/jwt_verifier.go:47.36,50.3 2 1 +github.com/thalesignite/gose/jwt_verifier.go:51.64,54.3 2 1 +github.com/thalesignite/gose/jwt_verifier.go:57.37,58.50 1 1 +github.com/thalesignite/gose/jwt_verifier.go:58.50,60.4 1 1 +github.com/thalesignite/gose/jwt_verifier.go:62.12,65.3 2 1 +github.com/thalesignite/gose/jwt_verifier.go:69.32,72.3 2 0 +github.com/thalesignite/gose/jwt_verifier.go:74.16,77.3 2 0 +github.com/thalesignite/gose/jwt_verifier.go:79.41,82.3 2 0 +github.com/thalesignite/gose/jwt_verifier.go:84.69,87.3 2 1 +github.com/thalesignite/gose/jwt_verifier.go:94.53,96.2 1 1 +github.com/thalesignite/gose/key_generator.go:61.123,63.32 1 1 +github.com/thalesignite/gose/key_generator.go:66.2,66.32 1 1 +github.com/thalesignite/gose/key_generator.go:69.2,70.16 2 1 +github.com/thalesignite/gose/key_generator.go:73.2,74.16 2 1 +github.com/thalesignite/gose/key_generator.go:77.2,78.39 2 1 +github.com/thalesignite/gose/key_generator.go:63.32,65.3 1 1 +github.com/thalesignite/gose/key_generator.go:66.32,68.3 1 1 +github.com/thalesignite/gose/key_generator.go:70.16,72.3 1 1 +github.com/thalesignite/gose/key_generator.go:74.16,76.3 1 1 +github.com/thalesignite/gose/key_generator.go:86.105,89.9 2 1 +github.com/thalesignite/gose/key_generator.go:93.2,94.16 2 1 +github.com/thalesignite/gose/key_generator.go:97.2,98.16 2 1 +github.com/thalesignite/gose/key_generator.go:101.2,103.39 2 1 +github.com/thalesignite/gose/key_generator.go:89.9,91.3 1 0 +github.com/thalesignite/gose/key_generator.go:94.16,96.3 1 0 +github.com/thalesignite/gose/key_generator.go:98.16,100.3 1 0 +github.com/thalesignite/gose/key_generator.go:110.142,112.9 2 1 +github.com/thalesignite/gose/key_generator.go:116.2,117.42 2 1 +github.com/thalesignite/gose/key_generator.go:120.2,121.16 2 1 +github.com/thalesignite/gose/key_generator.go:124.2,126.16 3 1 +github.com/thalesignite/gose/key_generator.go:129.2,129.26 1 1 +github.com/thalesignite/gose/key_generator.go:112.9,114.3 1 1 +github.com/thalesignite/gose/key_generator.go:117.42,119.3 1 0 +github.com/thalesignite/gose/key_generator.go:121.16,123.3 1 0 +github.com/thalesignite/gose/key_generator.go:126.16,128.3 1 1 +github.com/thalesignite/gose/verifier.go:61.54,63.2 1 1 +github.com/thalesignite/gose/verifier.go:66.62,68.2 1 1 +github.com/thalesignite/gose/verifier.go:71.65,73.16 2 1 +github.com/thalesignite/gose/verifier.go:76.2,77.17 2 1 +github.com/thalesignite/gose/verifier.go:73.16,75.3 1 0 +github.com/thalesignite/gose/verifier.go:81.67,83.16 2 1 +github.com/thalesignite/gose/verifier.go:86.2,86.25 1 1 +github.com/thalesignite/gose/verifier.go:83.16,85.3 1 0 +github.com/thalesignite/gose/verifier.go:90.70,92.16 2 1 +github.com/thalesignite/gose/verifier.go:96.2,101.52 3 1 +github.com/thalesignite/gose/verifier.go:104.2,104.36 1 1 +github.com/thalesignite/gose/verifier.go:92.16,94.3 1 0 +github.com/thalesignite/gose/verifier.go:101.52,103.3 1 0 +github.com/thalesignite/gose/verifier.go:108.107,110.46 2 1 +github.com/thalesignite/gose/verifier.go:113.2,114.48 2 1 +github.com/thalesignite/gose/verifier.go:117.2,119.40 3 1 +github.com/thalesignite/gose/verifier.go:124.2,124.19 1 1 +github.com/thalesignite/gose/verifier.go:110.46,112.3 1 0 +github.com/thalesignite/gose/verifier.go:114.48,116.3 1 0 +github.com/thalesignite/gose/verifier.go:119.40,121.3 1 1 +github.com/thalesignite/gose/verifier.go:121.8,123.3 1 1 +github.com/thalesignite/gose/verifier.go:128.76,130.2 1 0 +github.com/thalesignite/gose/verifier.go:133.64,136.24 2 1 +github.com/thalesignite/gose/verifier.go:142.2,143.25 2 1 +github.com/thalesignite/gose/verifier.go:136.24,138.20 2 1 +github.com/thalesignite/gose/verifier.go:138.20,140.4 1 1 +github.com/thalesignite/gose/verifier.go:144.26,146.91 1 1 +github.com/thalesignite/gose/verifier.go:160.3,160.36 1 0 +github.com/thalesignite/gose/verifier.go:161.25,162.96 1 1 +github.com/thalesignite/gose/verifier.go:165.3,175.22 10 1 +github.com/thalesignite/gose/verifier.go:177.10,178.36 1 0 +github.com/thalesignite/gose/verifier.go:146.91,147.41 1 1 +github.com/thalesignite/gose/verifier.go:150.4,158.23 9 1 +github.com/thalesignite/gose/verifier.go:147.41,149.5 1 1 +github.com/thalesignite/gose/verifier.go:162.96,164.4 1 0 +github.com/thalesignite/gose/jwe_direct_decryptor.go:34.97,36.48 2 1 +github.com/thalesignite/gose/jwe_direct_decryptor.go:41.2,41.32 1 1 +github.com/thalesignite/gose/jwe_direct_decryptor.go:47.2,47.36 1 1 +github.com/thalesignite/gose/jwe_direct_decryptor.go:52.2,54.69 3 1 +github.com/thalesignite/gose/jwe_direct_decryptor.go:59.2,60.9 2 1 +github.com/thalesignite/gose/jwe_direct_decryptor.go:66.2,66.72 1 1 +github.com/thalesignite/gose/jwe_direct_decryptor.go:71.2,71.142 1 1 +github.com/thalesignite/gose/jwe_direct_decryptor.go:75.2,75.38 1 1 +github.com/thalesignite/gose/jwe_direct_decryptor.go:79.2,79.8 1 1 +github.com/thalesignite/gose/jwe_direct_decryptor.go:36.48,38.3 1 1 +github.com/thalesignite/gose/jwe_direct_decryptor.go:41.32,44.3 2 1 +github.com/thalesignite/gose/jwe_direct_decryptor.go:47.36,50.3 2 1 +github.com/thalesignite/gose/jwe_direct_decryptor.go:54.69,57.3 2 1 +github.com/thalesignite/gose/jwe_direct_decryptor.go:60.9,63.3 2 1 +github.com/thalesignite/gose/jwe_direct_decryptor.go:66.72,69.3 2 1 +github.com/thalesignite/gose/jwe_direct_decryptor.go:71.142,73.3 1 1 +github.com/thalesignite/gose/jwe_direct_decryptor.go:75.38,77.3 1 1 +github.com/thalesignite/gose/jwe_direct_decryptor.go:83.91,88.27 2 1 +github.com/thalesignite/gose/jwe_direct_decryptor.go:91.2,91.18 1 1 +github.com/thalesignite/gose/jwe_direct_decryptor.go:88.27,90.3 1 1 +github.com/thalesignite/gose/jwe_direct_encryptor.go:42.99,44.16 2 1 +github.com/thalesignite/gose/jwe_direct_encryptor.go:48.2,50.18 3 1 +github.com/thalesignite/gose/jwe_direct_encryptor.go:57.2,70.43 2 1 +github.com/thalesignite/gose/jwe_direct_encryptor.go:74.2,74.132 1 1 +github.com/thalesignite/gose/jwe_direct_encryptor.go:77.2,77.27 1 1 +github.com/thalesignite/gose/jwe_direct_encryptor.go:44.16,46.3 1 0 +github.com/thalesignite/gose/jwe_direct_encryptor.go:50.18,55.3 2 1 +github.com/thalesignite/gose/jwe_direct_encryptor.go:70.43,72.3 1 0 +github.com/thalesignite/gose/jwe_direct_encryptor.go:74.132,76.3 1 0 +github.com/thalesignite/gose/jwe_direct_encryptor.go:81.98,85.2 1 1 +github.com/thalesignite/gose/aes_gcm_cryptor.go:46.44,48.2 1 1 +github.com/thalesignite/gose/aes_gcm_cryptor.go:51.52,53.2 1 1 +github.com/thalesignite/gose/aes_gcm_cryptor.go:56.63,58.51 2 1 +github.com/thalesignite/gose/aes_gcm_cryptor.go:61.2,61.19 1 1 +github.com/thalesignite/gose/aes_gcm_cryptor.go:58.51,60.3 1 0 +github.com/thalesignite/gose/aes_gcm_cryptor.go:65.125,67.46 2 1 +github.com/thalesignite/gose/aes_gcm_cryptor.go:71.2,75.81 5 1 +github.com/thalesignite/gose/aes_gcm_cryptor.go:78.2,79.8 2 1 +github.com/thalesignite/gose/aes_gcm_cryptor.go:67.46,70.3 2 1 +github.com/thalesignite/gose/aes_gcm_cryptor.go:75.81,77.3 1 0 +github.com/thalesignite/gose/aes_gcm_cryptor.go:83.125,85.46 2 1 +github.com/thalesignite/gose/aes_gcm_cryptor.go:89.2,89.44 1 1 +github.com/thalesignite/gose/aes_gcm_cryptor.go:93.2,98.8 6 1 +github.com/thalesignite/gose/aes_gcm_cryptor.go:85.46,88.3 2 1 +github.com/thalesignite/gose/aes_gcm_cryptor.go:89.44,92.3 2 0 +github.com/thalesignite/gose/aes_gcm_cryptor.go:102.104,105.19 2 1 +github.com/thalesignite/gose/aes_gcm_cryptor.go:109.2,110.16 2 1 +github.com/thalesignite/gose/aes_gcm_cryptor.go:113.2,119.8 1 1 +github.com/thalesignite/gose/aes_gcm_cryptor.go:105.19,107.3 1 1 +github.com/thalesignite/gose/aes_gcm_cryptor.go:110.16,112.3 1 1 +github.com/thalesignite/gose/aes_gcm_cryptor.go:123.144,131.2 1 0 +github.com/thalesignite/gose/helpers.go:53.47,55.16 2 0 +github.com/thalesignite/gose/helpers.go:58.2,60.21 3 0 +github.com/thalesignite/gose/helpers.go:55.16,57.3 1 0 +github.com/thalesignite/gose/helpers.go:63.61,64.22 1 1 +github.com/thalesignite/gose/helpers.go:67.2,68.29 2 1 +github.com/thalesignite/gose/helpers.go:75.2,75.15 1 1 +github.com/thalesignite/gose/helpers.go:64.22,66.3 1 1 +github.com/thalesignite/gose/helpers.go:68.29,70.26 2 1 +github.com/thalesignite/gose/helpers.go:73.3,73.30 1 1 +github.com/thalesignite/gose/helpers.go:70.26,72.4 1 1 +github.com/thalesignite/gose/helpers.go:78.76,80.26 2 1 +github.com/thalesignite/gose/helpers.go:87.2,87.15 1 1 +github.com/thalesignite/gose/helpers.go:80.26,81.28 1 1 +github.com/thalesignite/gose/helpers.go:81.28,82.14 1 1 +github.com/thalesignite/gose/helpers.go:82.14,84.5 1 1 +github.com/thalesignite/gose/helpers.go:91.82,104.45 2 1 +github.com/thalesignite/gose/helpers.go:107.2,107.76 1 1 +github.com/thalesignite/gose/helpers.go:110.2,110.25 1 1 +github.com/thalesignite/gose/helpers.go:104.45,106.3 1 0 +github.com/thalesignite/gose/helpers.go:107.76,109.3 1 1 +github.com/thalesignite/gose/helpers.go:111.27,113.48 1 1 +github.com/thalesignite/gose/helpers.go:117.3,120.54 2 1 +github.com/thalesignite/gose/helpers.go:124.3,130.134 6 1 +github.com/thalesignite/gose/helpers.go:133.3,133.19 1 1 +github.com/thalesignite/gose/helpers.go:134.26,135.18 1 1 +github.com/thalesignite/gose/helpers.go:139.3,145.19 6 1 +github.com/thalesignite/gose/helpers.go:146.10,147.36 1 1 +github.com/thalesignite/gose/helpers.go:113.48,116.4 1 0 +github.com/thalesignite/gose/helpers.go:120.54,122.4 1 0 +github.com/thalesignite/gose/helpers.go:130.134,132.4 1 0 +github.com/thalesignite/gose/helpers.go:135.18,138.4 1 0 +github.com/thalesignite/gose/helpers.go:152.84,164.44 2 1 +github.com/thalesignite/gose/helpers.go:167.2,167.76 1 1 +github.com/thalesignite/gose/helpers.go:170.2,170.25 1 1 +github.com/thalesignite/gose/helpers.go:164.44,166.3 1 0 +github.com/thalesignite/gose/helpers.go:167.76,169.3 1 0 +github.com/thalesignite/gose/helpers.go:171.26,173.33 1 1 +github.com/thalesignite/gose/helpers.go:177.3,179.54 2 1 +github.com/thalesignite/gose/helpers.go:182.3,184.19 3 1 +github.com/thalesignite/gose/helpers.go:185.25,187.33 2 0 +github.com/thalesignite/gose/helpers.go:191.3,194.19 4 0 +github.com/thalesignite/gose/helpers.go:195.10,196.36 1 0 +github.com/thalesignite/gose/helpers.go:173.33,176.4 1 0 +github.com/thalesignite/gose/helpers.go:179.54,181.4 1 0 +github.com/thalesignite/gose/helpers.go:187.33,190.4 1 0 +github.com/thalesignite/gose/helpers.go:201.133,204.21 3 0 +github.com/thalesignite/gose/helpers.go:207.2,208.16 2 0 +github.com/thalesignite/gose/helpers.go:211.2,211.53 1 0 +github.com/thalesignite/gose/helpers.go:214.2,216.16 3 0 +github.com/thalesignite/gose/helpers.go:219.2,220.16 2 0 +github.com/thalesignite/gose/helpers.go:223.2,224.8 2 0 +github.com/thalesignite/gose/helpers.go:204.21,206.3 1 0 +github.com/thalesignite/gose/helpers.go:208.16,210.3 1 0 +github.com/thalesignite/gose/helpers.go:211.53,213.3 1 0 +github.com/thalesignite/gose/helpers.go:216.16,218.3 1 0 +github.com/thalesignite/gose/helpers.go:220.16,222.3 1 0 +github.com/thalesignite/gose/helpers.go:228.51,230.29 1 1 +github.com/thalesignite/gose/helpers.go:231.26,240.60 3 1 +github.com/thalesignite/gose/helpers.go:243.3,244.40 2 1 +github.com/thalesignite/gose/helpers.go:245.25,254.60 3 1 +github.com/thalesignite/gose/helpers.go:257.3,258.40 2 1 +github.com/thalesignite/gose/helpers.go:259.26,273.40 5 1 +github.com/thalesignite/gose/helpers.go:274.10,275.35 1 0 +github.com/thalesignite/gose/helpers.go:240.60,242.4 1 0 +github.com/thalesignite/gose/helpers.go:254.60,256.4 1 0 +github.com/thalesignite/gose/helpers.go:280.86,281.54 1 1 +github.com/thalesignite/gose/helpers.go:284.2,284.57 1 1 +github.com/thalesignite/gose/helpers.go:287.2,287.8 1 1 +github.com/thalesignite/gose/helpers.go:281.54,283.3 1 0 +github.com/thalesignite/gose/helpers.go:284.57,286.3 1 0 +github.com/thalesignite/gose/helpers.go:291.77,294.16 2 0 +github.com/thalesignite/gose/helpers.go:297.2,298.30 2 0 +github.com/thalesignite/gose/helpers.go:294.16,296.3 1 0 +github.com/thalesignite/gose/helpers.go:308.40,310.21 1 1 +github.com/thalesignite/gose/helpers.go:315.2,315.22 1 1 +github.com/thalesignite/gose/helpers.go:310.21,312.3 1 1 +github.com/thalesignite/gose/helpers.go:312.8,312.27 1 1 +github.com/thalesignite/gose/helpers.go:312.27,314.3 1 1 +github.com/thalesignite/gose/helpers.go:318.39,319.16 1 1 +github.com/thalesignite/gose/helpers.go:320.11,321.23 1 1 +github.com/thalesignite/gose/helpers.go:322.11,323.23 1 1 +github.com/thalesignite/gose/helpers.go:324.11,325.23 1 1 +github.com/thalesignite/gose/helpers.go:326.10,327.23 1 0 +github.com/thalesignite/gose/helpers.go:332.55,334.24 2 1 +github.com/thalesignite/gose/helpers.go:351.2,354.30 4 1 +github.com/thalesignite/gose/helpers.go:357.2,359.17 3 1 +github.com/thalesignite/gose/helpers.go:335.27,337.63 1 1 +github.com/thalesignite/gose/helpers.go:341.3,343.16 3 1 +github.com/thalesignite/gose/helpers.go:344.26,347.16 3 1 +github.com/thalesignite/gose/helpers.go:348.10,349.36 1 0 +github.com/thalesignite/gose/helpers.go:337.63,340.4 1 0 +github.com/thalesignite/gose/helpers.go:354.30,356.3 1 1 +github.com/thalesignite/gose/helpers.go:363.48,365.16 2 1 +github.com/thalesignite/gose/helpers.go:368.2,368.23 1 1 +github.com/thalesignite/gose/helpers.go:365.16,367.3 1 0 +github.com/thalesignite/gose/helpers.go:371.43,373.67 2 0 +github.com/thalesignite/gose/helpers.go:376.2,376.58 1 0 +github.com/thalesignite/gose/helpers.go:373.67,375.3 1 0 +github.com/thalesignite/gose/helpers.go:380.121,382.32 2 1 +github.com/thalesignite/gose/helpers.go:413.2,414.20 2 1 +github.com/thalesignite/gose/helpers.go:417.2,418.16 2 1 +github.com/thalesignite/gose/helpers.go:422.2,423.16 2 1 +github.com/thalesignite/gose/helpers.go:427.2,429.17 2 1 +github.com/thalesignite/gose/helpers.go:383.23,384.26 1 1 +github.com/thalesignite/gose/helpers.go:387.3,400.13 13 1 +github.com/thalesignite/gose/helpers.go:401.25,409.12 8 1 +github.com/thalesignite/gose/helpers.go:410.10,411.36 1 0 +github.com/thalesignite/gose/helpers.go:384.26,386.4 1 1 +github.com/thalesignite/gose/helpers.go:414.20,416.3 1 0 +github.com/thalesignite/gose/helpers.go:418.16,421.3 1 0 +github.com/thalesignite/gose/helpers.go:423.16,426.3 1 0 +github.com/thalesignite/gose/helpers.go:433.122,435.31 2 1 +github.com/thalesignite/gose/helpers.go:458.2,461.20 4 1 +github.com/thalesignite/gose/helpers.go:464.2,464.17 1 1 +github.com/thalesignite/gose/helpers.go:436.22,437.26 1 1 +github.com/thalesignite/gose/helpers.go:440.3,446.13 6 1 +github.com/thalesignite/gose/helpers.go:447.24,454.12 7 1 +github.com/thalesignite/gose/helpers.go:455.10,456.36 1 0 +github.com/thalesignite/gose/helpers.go:437.26,439.4 1 0 +github.com/thalesignite/gose/helpers.go:461.20,463.3 1 0 +github.com/thalesignite/gose/helpers.go:483.75,486.39 2 1 +github.com/thalesignite/gose/helpers.go:501.2,506.49 6 1 +github.com/thalesignite/gose/helpers.go:509.2,511.8 3 1 +github.com/thalesignite/gose/helpers.go:486.39,487.53 1 1 +github.com/thalesignite/gose/helpers.go:491.3,491.26 1 1 +github.com/thalesignite/gose/helpers.go:494.3,494.20 1 1 +github.com/thalesignite/gose/helpers.go:487.53,490.4 2 1 +github.com/thalesignite/gose/helpers.go:491.26,493.4 1 1 +github.com/thalesignite/gose/helpers.go:494.20,496.4 1 1 +github.com/thalesignite/gose/helpers.go:497.8,500.3 2 0 +github.com/thalesignite/gose/helpers.go:506.49,508.3 1 0 +github.com/thalesignite/gose/helpers.go:515.87,518.44 1 1 +github.com/thalesignite/gose/helpers.go:522.2,522.76 1 1 +github.com/thalesignite/gose/helpers.go:526.2,526.25 1 1 +github.com/thalesignite/gose/helpers.go:518.44,521.3 2 1 +github.com/thalesignite/gose/helpers.go:522.76,525.3 2 1 +github.com/thalesignite/gose/helpers.go:527.26,529.9 2 1 +github.com/thalesignite/gose/helpers.go:530.10,532.9 2 0 +github.com/thalesignite/gose/helpers.go:537.89,539.62 2 1 +github.com/thalesignite/gose/helpers.go:542.2,543.17 2 1 +github.com/thalesignite/gose/helpers.go:539.62,541.3 1 1 +github.com/thalesignite/gose/helpers.go:544.57,546.46 2 1 +github.com/thalesignite/gose/helpers.go:549.3,549.44 1 1 +github.com/thalesignite/gose/helpers.go:552.3,552.9 1 1 +github.com/thalesignite/gose/helpers.go:553.10,555.9 2 0 +github.com/thalesignite/gose/helpers.go:546.46,548.4 1 0 +github.com/thalesignite/gose/helpers.go:549.44,551.4 1 0 +github.com/thalesignite/gose/interfaces.go:37.42,39.2 1 0 +github.com/thalesignite/gose/jwt_signer.go:41.46,43.2 1 1 +github.com/thalesignite/gose/jwt_signer.go:46.115,48.20 2 1 +github.com/thalesignite/gose/jwt_signer.go:58.2,75.16 3 1 +github.com/thalesignite/gose/jwt_signer.go:78.2,79.16 2 1 +github.com/thalesignite/gose/jwt_signer.go:82.2,83.70 2 1 +github.com/thalesignite/gose/jwt_signer.go:87.2,87.56 1 1 +github.com/thalesignite/gose/jwt_signer.go:48.20,50.29 2 1 +github.com/thalesignite/gose/jwt_signer.go:50.29,52.18 2 1 +github.com/thalesignite/gose/jwt_signer.go:55.4,55.31 1 1 +github.com/thalesignite/gose/jwt_signer.go:52.18,54.5 1 0 +github.com/thalesignite/gose/jwt_signer.go:75.16,77.3 1 0 +github.com/thalesignite/gose/jwt_signer.go:79.16,81.3 1 1 +github.com/thalesignite/gose/jwt_signer.go:83.70,86.3 1 0 +github.com/thalesignite/gose/jwt_signer.go:91.65,93.2 1 1 +github.com/thalesignite/gose/keystore.go:41.72,42.21 1 1 +github.com/thalesignite/gose/keystore.go:46.2,48.46 3 1 +github.com/thalesignite/gose/keystore.go:51.2,51.56 1 1 +github.com/thalesignite/gose/keystore.go:54.2,55.12 2 1 +github.com/thalesignite/gose/keystore.go:42.21,45.3 1 0 +github.com/thalesignite/gose/keystore.go:48.46,50.3 1 1 +github.com/thalesignite/gose/keystore.go:51.56,53.3 1 0 +github.com/thalesignite/gose/keystore.go:59.65,62.46 3 0 +github.com/thalesignite/gose/keystore.go:65.2,66.13 2 0 +github.com/thalesignite/gose/keystore.go:62.46,64.3 1 0 +github.com/thalesignite/gose/keystore.go:70.73,73.42 3 1 +github.com/thalesignite/gose/keystore.go:80.2,80.12 1 0 +github.com/thalesignite/gose/keystore.go:73.42,74.33 1 1 +github.com/thalesignite/gose/keystore.go:74.33,75.55 1 1 +github.com/thalesignite/gose/keystore.go:75.55,77.5 1 1 +github.com/thalesignite/gose/keystore.go:84.91,87.36 3 1 +github.com/thalesignite/gose/keystore.go:92.2,93.8 2 1 +github.com/thalesignite/gose/keystore.go:87.36,88.45 1 1 +github.com/thalesignite/gose/keystore.go:88.45,90.4 1 0 +github.com/thalesignite/gose/keystore.go:97.82,102.16 5 0 +github.com/thalesignite/gose/keystore.go:105.2,105.58 1 0 +github.com/thalesignite/gose/keystore.go:108.2,108.37 1 0 +github.com/thalesignite/gose/keystore.go:117.2,118.8 2 0 +github.com/thalesignite/gose/keystore.go:102.16,104.3 1 0 +github.com/thalesignite/gose/keystore.go:105.58,107.3 1 0 +github.com/thalesignite/gose/keystore.go:108.37,110.79 2 0 +github.com/thalesignite/gose/keystore.go:113.3,113.45 1 0 +github.com/thalesignite/gose/keystore.go:110.79,112.4 1 0 +github.com/thalesignite/gose/keystore.go:113.45,115.4 1 0 +github.com/thalesignite/gose/ecdsa_signer.go:47.50,49.2 1 1 +github.com/thalesignite/gose/ecdsa_signer.go:61.52,63.2 1 1 +github.com/thalesignite/gose/ecdsa_signer.go:66.52,68.2 1 0 +github.com/thalesignite/gose/ecdsa_signer.go:74.103,76.46 2 1 +github.com/thalesignite/gose/ecdsa_signer.go:81.2,82.34 2 1 +github.com/thalesignite/gose/ecdsa_signer.go:87.2,88.54 2 1 +github.com/thalesignite/gose/ecdsa_signer.go:93.2,96.75 3 1 +github.com/thalesignite/gose/ecdsa_signer.go:120.2,120.8 1 0 +github.com/thalesignite/gose/ecdsa_signer.go:76.46,79.3 2 0 +github.com/thalesignite/gose/ecdsa_signer.go:82.34,85.3 2 0 +github.com/thalesignite/gose/ecdsa_signer.go:88.54,90.3 1 0 +github.com/thalesignite/gose/ecdsa_signer.go:96.75,99.37 3 1 +github.com/thalesignite/gose/ecdsa_signer.go:104.3,118.9 9 1 +github.com/thalesignite/gose/ecdsa_signer.go:99.37,102.4 2 0 +github.com/thalesignite/gose/ecdsa_signer.go:124.68,126.16 2 1 +github.com/thalesignite/gose/ecdsa_signer.go:129.2,129.38 1 1 +github.com/thalesignite/gose/ecdsa_signer.go:126.16,128.3 1 0 +github.com/thalesignite/gose/ecdsa_signer.go:133.45,136.2 1 1 +github.com/thalesignite/gose/ecdsa_signer.go:139.58,141.2 1 1 +github.com/thalesignite/gose/ecdsa_signer.go:146.67,149.92 3 1 +github.com/thalesignite/gose/ecdsa_signer.go:153.2,158.51 3 1 +github.com/thalesignite/gose/ecdsa_signer.go:161.2,161.36 1 1 +github.com/thalesignite/gose/ecdsa_signer.go:149.92,151.3 1 0 +github.com/thalesignite/gose/ecdsa_signer.go:158.51,160.3 1 0 +github.com/thalesignite/gose/ecdsa_signer.go:166.67,168.2 1 0 +github.com/thalesignite/gose/ecdsa_signer.go:171.56,174.2 1 1 +github.com/thalesignite/gose/ecdsa_verifier.go:48.61,50.2 1 1 +github.com/thalesignite/gose/ecdsa_verifier.go:56.106,58.46 2 1 +github.com/thalesignite/gose/ecdsa_verifier.go:63.2,66.33 4 1 +github.com/thalesignite/gose/ecdsa_verifier.go:70.2,74.37 3 1 +github.com/thalesignite/gose/ecdsa_verifier.go:77.2,78.54 2 1 +github.com/thalesignite/gose/ecdsa_verifier.go:83.2,83.55 1 1 +github.com/thalesignite/gose/ecdsa_verifier.go:58.46,60.3 1 0 +github.com/thalesignite/gose/ecdsa_verifier.go:66.33,68.3 1 0 +github.com/thalesignite/gose/ecdsa_verifier.go:74.37,76.3 1 0 +github.com/thalesignite/gose/ecdsa_verifier.go:78.54,80.3 1 0 +github.com/thalesignite/gose/ecdsa_verifier.go:87.75,89.2 1 0 +github.com/thalesignite/gose/ecdsa_verifier.go:92.64,94.2 1 1 +github.com/thalesignite/gose/ecdsa_verifier.go:97.66,99.16 2 1 +github.com/thalesignite/gose/ecdsa_verifier.go:102.2,102.25 1 1 +github.com/thalesignite/gose/ecdsa_verifier.go:99.16,101.3 1 0 +github.com/thalesignite/gose/ecdsa_verifier.go:106.69,108.16 2 1 +github.com/thalesignite/gose/ecdsa_verifier.go:112.2,117.52 3 1 +github.com/thalesignite/gose/ecdsa_verifier.go:120.2,120.36 1 1 +github.com/thalesignite/gose/ecdsa_verifier.go:108.16,110.3 1 0 +github.com/thalesignite/gose/ecdsa_verifier.go:117.52,119.3 1 0 +github.com/thalesignite/gose/ecdsa_verifier.go:124.53,126.2 1 1 +github.com/thalesignite/gose/jwks_truststore.go:47.69,49.2 1 1 +github.com/thalesignite/gose/jwks_truststore.go:52.62,54.2 1 1 +github.com/thalesignite/gose/jwks_truststore.go:57.70,58.28 1 1 +github.com/thalesignite/gose/jwks_truststore.go:102.2,102.12 1 0 +github.com/thalesignite/gose/jwks_truststore.go:58.28,61.34 3 1 +github.com/thalesignite/gose/jwks_truststore.go:67.3,68.17 2 1 +github.com/thalesignite/gose/jwks_truststore.go:72.3,72.43 1 1 +github.com/thalesignite/gose/jwks_truststore.go:76.3,78.47 3 1 +github.com/thalesignite/gose/jwks_truststore.go:82.3,83.33 2 1 +github.com/thalesignite/gose/jwks_truststore.go:92.3,95.34 2 1 +github.com/thalesignite/gose/jwks_truststore.go:61.34,62.24 1 1 +github.com/thalesignite/gose/jwks_truststore.go:62.24,64.5 1 1 +github.com/thalesignite/gose/jwks_truststore.go:68.17,71.4 2 1 +github.com/thalesignite/gose/jwks_truststore.go:72.43,75.4 2 1 +github.com/thalesignite/gose/jwks_truststore.go:78.47,81.4 2 1 +github.com/thalesignite/gose/jwks_truststore.go:83.33,85.18 2 1 +github.com/thalesignite/gose/jwks_truststore.go:89.4,89.40 1 1 +github.com/thalesignite/gose/jwks_truststore.go:85.18,88.5 2 0 +github.com/thalesignite/gose/jwks_truststore.go:95.34,96.24 1 1 +github.com/thalesignite/gose/jwks_truststore.go:96.24,98.5 1 1 +github.com/thalesignite/gose/jwks_truststore.go:107.58,113.2 1 1 diff --git a/ec_verifier_test.go b/ec_verifier_test.go new file mode 100644 index 0000000..6ac4055 --- /dev/null +++ b/ec_verifier_test.go @@ -0,0 +1,153 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "bytes" + "crypto/ecdsa" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "testing" + + "github.com/thalesignite/gose/jose" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewECVerifierSucceeds(t *testing.T) { + for _, curve := range curves { + // Setup + ecdsaKey, err := ecdsa.GenerateKey(curve, rand.Reader) + require.NoError(t, err) + jwk, err := JwkFromPublicKey(ecdsaKey.Public(), []jose.KeyOps{jose.KeyOpsVerify}, nil) + require.NoError(t, err) + + // Act + k, err := NewVerificationKey(jwk) + + // Assert + require.Nil(t, err) + require.NotNil(t, k) + require.NotEmpty(t, k.Kid()) + } +} + +func TestNewECVerifierFailsWithInvalidOps(t *testing.T) { + for _, curve := range curves { + // Setup + ecdsaKey, err := ecdsa.GenerateKey(curve, rand.Reader) + require.NoError(t, err) + + testCase := [][]jose.KeyOps{ + {jose.KeyOpsSign}, + } + for _, test := range testCase { + jwk, err := JwkFromPublicKey(ecdsaKey.Public(), test, nil) + require.NoError(t, err) + + // Act + k, err := NewVerificationKey(jwk) + + // Assert + assert.Nil(t, k) + assert.Equal(t, ErrInvalidOperations, err) + } + } +} + +func TestNewECVerifierMarshalSucceeds(t *testing.T) { + for _, curve := range curves { + // Setup + require.NotNil(t, rand.Reader) + ecdsaKey, err := ecdsa.GenerateKey(curve, rand.Reader) + require.NoError(t, err) + jwk, err := JwkFromPublicKey(ecdsaKey.Public(), []jose.KeyOps{jose.KeyOpsVerify}, nil) + require.NoError(t, err) + k, err := NewVerificationKey(jwk) + require.NoError(t, err) + + // Act + str, err := k.Marshal() + + // Assert + require.NoError(t, err) + require.NotEmpty(t, str) + buf := bytes.NewReader([]byte(str)) + jwkOut, err := LoadJwk(buf, nil) + require.Equal(t, jwk.Kid(), jwkOut.Kid()) + require.Equal(t, jwk.Kty(), jwkOut.Kty()) + require.Equal(t, jwk.Alg(), jwkOut.Alg()) + require.Equal(t, jwk.Ops(), jwkOut.Ops()) + + goodEcJwk := jwk.(*jose.PublicEcKey) + marshalledECJwk := jwkOut.(*jose.PublicEcKey) + require.Equal(t, 0, goodEcJwk.X.Int().Cmp(marshalledECJwk.X.Int())) + require.Equal(t, 0, goodEcJwk.Y.Int().Cmp(marshalledECJwk.Y.Int())) + } +} + +func TestNewECVerifierMarshalPemSucceeds(t *testing.T) { + for _, curve := range curves { + // Setup + ecdsaKey, err := ecdsa.GenerateKey(curve, rand.Reader) + require.NoError(t, err) + jwk, err := JwkFromPublicKey(ecdsaKey.Public(), []jose.KeyOps{jose.KeyOpsVerify}, nil) + require.NoError(t, err) + k, err := NewVerificationKey(jwk) + require.NoError(t, err) + + // Act + str, err := k.MarshalPem() + + // Assert + require.NoError(t, err) + require.NotEmpty(t, str) + block, overflow := pem.Decode([]byte(str)) + require.Empty(t, overflow) + require.Equal(t, block.Type, ecPublicKeyPemType) + recoveredKey, err := x509.ParsePKIXPublicKey(block.Bytes) + require.NoError(t, err) + recoveredECKey, ok := recoveredKey.(*ecdsa.PublicKey) + require.True(t, ok) + assert.Equal(t, recoveredECKey.X, ecdsaKey.X) + assert.Equal(t, recoveredECKey.Y, ecdsaKey.Y) + } +} + +func TestNewECVerifierFailsWhenNotAVerfierKey(t *testing.T) { + // Setup + var jwk jose.PrivateEcKey + jwk.SetAlg(jose.AlgES256) + jwk.SetOps([]jose.KeyOps{jose.KeyOpsSign}) + jwk.SetUse(jose.KeyUseSig) + jwk.X.SetBytes([]byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ")) + jwk.Y.SetBytes([]byte("AABABB")) + jwk.D.SetBytes([]byte("AABABB")) + + // Act + k, err := NewVerificationKey(&jwk) + + // Assert + require.Equal(t, ErrInvalidOperations, err) + require.Nil(t, k) +} diff --git a/ecdsa_signer.go b/ecdsa_signer.go new file mode 100644 index 0000000..5fedc41 --- /dev/null +++ b/ecdsa_signer.go @@ -0,0 +1,174 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "math/big" + + "github.com/thalesignite/gose/jose" + "github.com/sirupsen/logrus" +) + +//ECDSAOptions Implements crypto.SignerOpts +type ECDSAOptions struct { + Hash crypto.Hash + keySizeBytes int + curveBits int + curve elliptic.Curve +} + +//HashFunc returns the crypto.Hash +func (opts *ECDSAOptions) HashFunc() crypto.Hash { + return opts.Hash +} + +//------------------- + +//ECDSASigningKey implements ECDSA crypto.SigningKey +type ECDSASigningKey struct { + jwk jose.Jwk + key crypto.Signer + certs []*x509.Certificate +} + +//Algorithm returns the jose.Alg for this key +func (signer ECDSASigningKey) Algorithm() jose.Alg { + return signer.jwk.Alg() +} + +// Key returns the underlying key used to sign +func (signer *ECDSASigningKey) Key() crypto.Signer { + return signer.key +} + +// Sign digest and sign the given data. +// The output signature is encoded as r || s which is different to the standard go crypto interface specification. +// The serialization format is chosen instead to match that defined in the JSON Web Signature spec +// https://tools.ietf.org/html/rfc7515#appendix-A.3.1. +func (signer *ECDSASigningKey) Sign(requested jose.KeyOps, data []byte) (signature []byte, err error) { + ops := intersection(validSignerOps, signer.jwk.Ops()) + if !isSubset(ops, []jose.KeyOps{requested}) { + err = ErrInvalidOperations + return + } + + opts := algToOptsMap[signer.jwk.Alg()] + if !opts.HashFunc().Available() { + err = ErrHashUnavailable + return + } + + hasher := opts.HashFunc().New() + if _, err := hasher.Write([]byte(data)); err != nil { + logrus.Panicf("%s", err) + } + + // Sign the string and return r, s + key := signer.key.(*ecdsa.PrivateKey) + + var r, s *big.Int + if r, s, err = ecdsa.Sign(rand.Reader, key, hasher.Sum(nil)); err == nil { + curveBits := key.Curve.Params().BitSize + options := opts.(*ECDSAOptions) + if options.curveBits != curveBits { + err = ErrInvalidKey + return + } + + keyBytes := (curveBits + 7) / 8 + + // We serialize the outpus (r and s) into big-endian byte arrays and pad + // them with zeros on the left to make sure the sizes work out. Both arrays + // must be keyBytes long, and the output must be 2*keyBytes long. + rBytes := r.Bytes() + rBytesPadded := make([]byte, keyBytes) + copy(rBytesPadded[keyBytes-len(rBytes):], rBytes) + + sBytes := s.Bytes() + sBytesPadded := make([]byte, keyBytes) + copy(sBytesPadded[keyBytes-len(sBytes):], sBytes) + + signature = append(rBytesPadded, sBytesPadded...) + return + } + return +} + +// Verifier get the matching verification key. +func (signer *ECDSASigningKey) Verifier() (VerificationKey, error) { + publicJwk, err := PublicFromPrivate(signer.jwk) + if err != nil { + return nil, err + } + return NewVerificationKey(publicJwk) +} + +//Kid returns the kid string value +func (signer *ECDSASigningKey) Kid() string { + /* JIT jwk load. */ + return signer.jwk.Kid() +} + +//Marshal marshals the key into a compact JWK representation or error +func (signer *ECDSASigningKey) Marshal() (string, error) { + return JwkToString(signer.jwk) +} + +const ecdsaPrivateKeyPerType = "ECDSA PRIVATE KEY" + +//MarshalPem marshals the key into a PEM string or error +func (signer *ECDSASigningKey) MarshalPem() (p string, err error) { + pemType := ecdsaPrivateKeyPerType + var derEncoded []byte + if derEncoded, err = x509.MarshalECPrivateKey(signer.key.(*ecdsa.PrivateKey)); err != nil { + return + } + + block := pem.Block{ + Type: pemType, + Bytes: derEncoded, + } + output := bytes.Buffer{} + if err = pem.Encode(&output, &block); err != nil { + return + } + return string(output.Bytes()), nil + +} + +//Certificates returns certificate chain of this key +func (signer *ECDSASigningKey) Certificates() []*x509.Certificate { + return signer.certs +} + +//Jwk returns key as a jose.JWK type, or errors +func (signer *ECDSASigningKey) Jwk() (jose.Jwk, error) { + /* Return a copy of our JWK. */ + return JwkFromPrivateKey(signer.key, signer.jwk.Ops(), signer.certs) +} diff --git a/ecdsa_signer_test.go b/ecdsa_signer_test.go new file mode 100644 index 0000000..dcd9cbe --- /dev/null +++ b/ecdsa_signer_test.go @@ -0,0 +1,177 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "testing" + + "github.com/thalesignite/gose/jose" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var curves = []elliptic.Curve{elliptic.P256(), elliptic.P384(), elliptic.P521()} + +func TestNewEcdsaSigningKey_Succeeds(t *testing.T) { + for _, curve := range curves { + k, err := ecdsa.GenerateKey(curve, rand.Reader) + require.NoError(t, err) + jwk, err := JwkFromPrivateKey(k, []jose.KeyOps{jose.KeyOpsSign}, nil) + require.NoError(t, err) + + // Act + signer, err := NewSigningKey(jwk, []jose.KeyOps{jose.KeyOpsSign}) + + // Assert + require.NoError(t, err) + require.NotNil(t, signer) + } +} + +func TestNewEcdsaSigningKey_FailsWhenInvalidOperations(t *testing.T) { + for _, curve := range curves { + // Setup + k, err := ecdsa.GenerateKey(curve, rand.Reader) + require.NoError(t, err) + testCase := [][]jose.KeyOps{ + {jose.KeyOpsVerify}, + {jose.KeyOpsSign, jose.KeyOpsVerify}, + } + for _, test := range testCase { + jwk, err := JwkFromPrivateKey(k, []jose.KeyOps{jose.KeyOpsSign}, nil) + require.NoError(t, err) + + // Act + signer, err := NewSigningKey(jwk, test) + + // Assert + require.Nil(t, signer) + require.Equal(t, ErrInvalidOperations, err) + } + } +} + +func TestNewEcdsaSigningKey_FailsWhenInvalidJwk(t *testing.T) { + for _, curve := range curves { + // Setup + k, err := ecdsa.GenerateKey(curve, rand.Reader) + require.NoError(t, err) + jwk, err := JwkFromPublicKey(k.Public(), []jose.KeyOps{jose.KeyOpsSign}, nil) + require.NoError(t, err) + + // Act + signer, err := NewSigningKey(jwk, []jose.KeyOps{jose.KeyOpsSign}) + + // Assert + require.Error(t, err) + require.Nil(t, signer) + } +} + +func TestNewEcdsaSigningKey_MarshalSucceeds(t *testing.T) { + for _, curve := range curves { + // Setup + k, err := ecdsa.GenerateKey(curve, rand.Reader) + require.NoError(t, err) + jwk, err := JwkFromPrivateKey(k, []jose.KeyOps{jose.KeyOpsSign}, nil) + require.NoError(t, err) + signer, err := NewSigningKey(jwk, []jose.KeyOps{jose.KeyOpsSign}) + require.NoError(t, err) + + // Act + marshalled, err := signer.Marshal() + + // Assert + require.NoError(t, err) + require.NotNil(t, marshalled) + payload := bytes.NewReader([]byte(marshalled)) + unmarshalledJwk, err := LoadJwk(payload, nil) + require.NoError(t, err) + require.Equal(t, jwk.Alg(), unmarshalledJwk.Alg()) + require.Equal(t, jwk.Ops(), unmarshalledJwk.Ops()) + require.Equal(t, jwk.Kty(), unmarshalledJwk.Kty()) + require.Equal(t, jwk.Kid(), unmarshalledJwk.Kid()) + } +} + +func TestNewEcdsaSigningKey_MarshalPemSucceeds(t *testing.T) { + for _, curve := range curves { + // Setup + k, err := ecdsa.GenerateKey(curve, rand.Reader) + require.NoError(t, err) + jwk, err := JwkFromPrivateKey(k, []jose.KeyOps{jose.KeyOpsSign}, nil) + require.NoError(t, err) + signer, err := NewSigningKey(jwk, []jose.KeyOps{jose.KeyOpsSign}) + + require.NoError(t, err) + + // Act + marshalled, err := signer.MarshalPem() + + // Assert + require.NoError(t, err) + require.NotEmpty(t, marshalled) + block, overflow := pem.Decode([]byte(marshalled)) + require.Empty(t, overflow) + require.Equal(t, block.Type, ecdsaPrivateKeyPerType) + recoveredKey, err := x509.ParseECPrivateKey(block.Bytes) + require.NoError(t, err) + assert.Equal(t, recoveredKey.D, k.D) + assert.Equal(t, recoveredKey.X, k.X) + assert.Equal(t, recoveredKey.Y, k.Y) + } +} + +func TestEcdsaSigningKeyImpl_Verifier(t *testing.T) { + for _, curve := range curves { + // Setup + k, err := ecdsa.GenerateKey(curve, rand.Reader) + require.NoError(t, err) + jwk, err := JwkFromPrivateKey(k, []jose.KeyOps{jose.KeyOpsSign}, nil) + require.NoError(t, err) + signer, err := NewSigningKey(jwk, []jose.KeyOps{jose.KeyOpsSign}) + require.NoError(t, err) + + // Act + verifier, err := signer.Verifier() + + // Assert + assert.NoError(t, err) + assert.NotNil(t, verifier) + assert.Equal(t, verifier.Kid(), signer.Kid()) + + // Sign something, then verify + testData := make([]byte, 10) + _, err = rand.Read(testData) + require.NoError(t, err) + signature, err := signer.Sign(jose.KeyOpsSign, testData) + require.NoError(t, err) + matches := verifier.Verify(jose.KeyOpsVerify, testData, signature) + assert.True(t, matches) + } +} diff --git a/ecdsa_verifier.go b/ecdsa_verifier.go new file mode 100644 index 0000000..620c637 --- /dev/null +++ b/ecdsa_verifier.go @@ -0,0 +1,126 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "bytes" + "crypto/ecdsa" + "crypto/x509" + "encoding/pem" + "math/big" + + "github.com/thalesignite/gose/jose" + "github.com/sirupsen/logrus" +) + +// ECVerificationKeyImpl implements the ECDSA Verification Logic +type ECVerificationKeyImpl struct { + key ecdsa.PublicKey + ops []jose.KeyOps + opts *ECDSAOptions + alg jose.Alg + id string + certs []*x509.Certificate +} + +const ecPublicKeyPemType = "EC PUBLIC KEY" + +// Algorithm return algorithm +func (verifier *ECVerificationKeyImpl) Algorithm() jose.Alg { + return verifier.alg +} + +// Verify signed data matches signature and jwk +// The input signature is encoded as r || s which is different to the standard go crypto interface specification. +// The serialization format is chosen instead to match that defined in the JSON Web Signature spec +// https://tools.ietf.org/html/rfc7515#appendix-A.3.1. +func (verifier *ECVerificationKeyImpl) Verify(operation jose.KeyOps, data []byte, signature []byte) bool { + ops := intersection(validVerificationOps, verifier.ops) + if !isSubset(ops, []jose.KeyOps{operation}) { + return false + } + + // Get the key + ecdsaKey := verifier.key + opts := algToOptsMap[verifier.Algorithm()].(*ECDSAOptions) + keySize := opts.keySizeBytes + if len(signature) != 2*keySize { + return false + } + + r := big.NewInt(0).SetBytes(signature[:keySize]) + s := big.NewInt(0).SetBytes(signature[keySize:]) + + // Create hasher + if !verifier.opts.Hash.Available() { + return false + } + hasher := verifier.opts.Hash.New() + if _, err := hasher.Write([]byte(data)); err != nil { + logrus.Panicf("%s", err) + } + + // Verify the signature + return ecdsa.Verify(&ecdsaKey, hasher.Sum(nil), r, s) +} + +// Certificates returns the certs for this key +func (verifier *ECVerificationKeyImpl) Certificates() []*x509.Certificate { + return verifier.certs +} + +// Jwk returns the key as a jose.JWK type, or error +func (verifier *ECVerificationKeyImpl) Jwk() (jose.Jwk, error) { + return JwkFromPublicKey(&verifier.key, verifier.ops, verifier.certs) +} + +// Marshal marshals the key into a compact JWK string, or error +func (verifier *ECVerificationKeyImpl) Marshal() (string, error) { + jwk, err := JwkFromPublicKey(&verifier.key, verifier.ops, verifier.certs) + if err != nil { + return "", err + } + return JwkToString(jwk) +} + +// MarshalPem marshals the key as a PEM formatted string, or error +func (verifier *ECVerificationKeyImpl) MarshalPem() (string, error) { + derEncoded, err := x509.MarshalPKIXPublicKey(&verifier.key) + if err != nil { + return "", err + } + + block := pem.Block{ + Type: ecPublicKeyPemType, + Bytes: derEncoded, + } + output := bytes.Buffer{} + if err := pem.Encode(&output, &block); err != nil { + return "", err + } + return string(output.Bytes()), nil +} + +//Kid returns the key's id +func (verifier *ECVerificationKeyImpl) Kid() string { + return verifier.id +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..6db2103 --- /dev/null +++ b/errors.go @@ -0,0 +1,69 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import "errors" + +/* Errors. */ +var ( + ErrInvalidKey = errors.New("invalid jwk") + ErrInvalidKeyType = errors.New("invalid jwk type") + ErrUnsupportedKeyType = errors.New("unsupported jwk type") + ErrInvalidSigningKeyURL = errors.New("invalid signing jwk url") + ErrInvalidOperations = errors.New("the jwk is invalid in this context") + ErrInvalidCertificateHeader = errors.New("invalid certificate header") + ErrUnknownKey = errors.New("unknown jwk") + ErrInvalidSignature = errors.New("invalid signature") + ErrInconsistentKeyValues = errors.New("inconsistent jwk values") + ErrInvalidKeyLength = errors.New("invalid jwk length") + ErrHashUnavailable = errors.New("hash unavailable") + + // RSA errors + ErrInvalidExponentEncoding error = &InvalidFormat{"invalid exponent encoding"} + ErrInvalidExponent = errors.New("invalid exponent value") + ErrInvalidModulusEncoding error = &InvalidFormat{"invalid modulus encoding"} + + //EC errors + ErrInvalidXEncoding error = &InvalidFormat{("Invalid X encoding")} + ErrInvalidYEncoding error = &InvalidFormat{("Invalid Y encoding")} + + //GCM errors + ErrInvalidNonce = errors.New("invalid nonce") + + // JOSE errors + ErrInvalidJwsCompactEncoding error = &InvalidFormat{"invalid jws compact encoding"} + ErrInvalidJwsBase64HeaderEncoding error = &InvalidFormat{"invalid base64 jws header encoding"} + ErrInvalidJwsHeaderEncoding error = &InvalidFormat{"invalid jws header"} + ErrInvalidJwsBase64BodyEncoding error = &InvalidFormat{"invalid base64 jws body encoding"} + ErrInvalidJwsBase64SignatureEncoding error = &InvalidFormat{"invalid base64 jws signature encoding"} + ErrInvalidJwkEncoding error = &InvalidFormat{"invalid jwk encoding"} + ErrInvalidJwtEncoding error = &InvalidFormat{"invalid jwt encoding"} + ErrInvalidJwtTimeframe error = &InvalidFormat{"invalid jwt time frame"} + ErrInvalidKid error = &InvalidFormat{"invalid key ID"} + ErrNoExpectedAudience error = &InvalidFormat{"no expected audience"} + ErrInvalidDelegateEncoding error = &InvalidFormat{"invalid delegate encoding"} + ErrInvalidManifestEncoding error = &InvalidFormat{"invalid manifest encoding"} + ErrInvalidKeySize error = &InvalidFormat{"invalid jwk size"} + ErrInvalidAlgorithm error = &InvalidFormat{"invalid algorithm"} + ErrInvalidEncryption error = &InvalidFormat{"invalid encryption"} + ErrZipCompressionNotSupported error = &InvalidFormat{"zip compression not supported"} +) diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..d661820 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,11 @@ +# Examples + +Each sub-directory contains example usage of functionality offered by the gose +package. + +## JWE +The JWE example shows how to encrypt and decrypt data using the +JSON Web Encryption Direct Encryption Scheme using an AES key and GCM. + +## JWT +The JWT example shows how to sign and verify JSON Web Tokens. \ No newline at end of file diff --git a/examples/jwe/jwe.go b/examples/jwe/jwe.go new file mode 100644 index 0000000..a1792ca --- /dev/null +++ b/examples/jwe/jwe.go @@ -0,0 +1,85 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package main + +import ( + "fmt" + "github.com/thalesignite/gose" + "github.com/thalesignite/gose/jose" + "os" +) + +var ( + keyOps = []jose.KeyOps{jose.KeyOpsDecrypt, jose.KeyOpsEncrypt} +) + +const ( + secretData = "This is a really secret thing" + authenticatedData = "This data is authenticated and publicly readable" +) + +func fail(err error) { + fmt.Println(err.Error()) + os.Exit(1) +} + +func main() { + // Firstly we create an encryption key for encrypting data using a Direct Encryption JWE encryption scheme. + generator := gose.AuthenticatedEncryptionKeyGenerator{} + var jwk jose.Jwk + key, jwk, err := generator.Generate(jose.AlgA256GCM, keyOps) + if err != nil { + fail(err) + } + marshalled, err := gose.JwkToString(jwk) + if err != nil { + fail(err) + } + fmt.Printf("Created encryption key JWK: %s\n", marshalled) + + // Create an encryptor using our key. + encryptor := gose.NewJweDirectEncryptorImpl(key) + + // Our encryptor accepts both secret data ti be encrypted as well as additional data to be included in the JWE as an + // authenticated and non-repudiable value. The aad value is included in the JWE header in the _thales_aad field. + jwe, err := encryptor.Encrypt([]byte(secretData), []byte(authenticatedData)) + if err != nil { + fail(err) + } + fmt.Printf("Created JWE: %s\n", jwe) + + // Now we create a decryptor to decrypt and verify the authenticity of a previously created JWE. + key, err = gose.NewAesGcmCryptorFromJwk(jwk, []jose.KeyOps{jose.KeyOpsDecrypt}) + if err != nil { + fail(err) + } + decryptor := gose.NewJweDirectDecryptorImpl([]gose.AuthenticatedEncryptionKey{key}) + + // Decrypt a JWE blob verifying it's authenticity in the process. + plaintext, aad, err := decryptor.Decrypt(jwe) + if err != nil { + fail(err) + } + + fmt.Printf("Decrypted JWE plaintext: %s\n", string(plaintext)) + fmt.Printf("JWE Authenticated Data: %s\n", string(aad)) +} diff --git a/examples/jwt/jwt.go b/examples/jwt/jwt.go new file mode 100644 index 0000000..e57d8fb --- /dev/null +++ b/examples/jwt/jwt.go @@ -0,0 +1,154 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package main + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/thalesignite/gose" + "github.com/thalesignite/gose/jose" + "os" + "time" +) + +const ( + houseNumberClaim = "house_number" + streetClaim = "street" +) + +var ( + signingOperations = []jose.KeyOps{jose.KeyOpsSign} +) + +func fail(err error) { + fmt.Println(err.Error()) + os.Exit(1) +} + +func main() { + // First create a signing key to sign a JWT. We use a generator to create our key. + generator := &gose.RsaSigningKeyGenerator{} + signingKey, err := generator.Generate(jose.AlgRS256, 2048, signingOperations) + if err != nil { + fail(err) + } + + // Create a private key JWK and marshal to a string. + jwk, err := signingKey.Jwk() + if err != nil { + fail(err) + } + marshalled, err := gose.JwkToString(jwk) + if err != nil { + fail(err) + } + fmt.Printf("Created signing key JWK: %s\n", marshalled) + + // Create a JWT signer specifying the issuer string and our previously generated signing key + jwtSigner := gose.NewJwtSigner("issuer", signingKey) + + // Define the claims we want to include in our JWT. Settable claims are standard JWT claims a caller can set such as + // the subject (sub), audience/s (aud), expiration (exp) and not before (nbf) fields. + now := time.Now().Unix() + standardClaims := &jose.SettableJwtClaims{ + Subject: "my subject", + Audiences: jose.Audiences{Aud:[]string{"my audience"}}, + Expiration: now + 60, // expiration in 60 seconds + NotBefore: now, + } + // Custom or non-standard claims can also be specified + customClaims := map[string]interface{}{ + houseNumberClaim: 29, + streetClaim: "Acacia Road", + } + jwt, err := jwtSigner.Sign(standardClaims, customClaims) + if err != nil { + fail(err) + } + fmt.Printf("Created signed JWT: %s\n", jwt) + + // We can get the public key for use during JWT verification. + verificationKey, err := signingKey.Verifier() + if err != nil { + fail(err) + } + jwk, err = verificationKey.Jwk() + if err != nil { + fail(err) + } + marshalled, err = gose.JwkToString(jwk) + if err != nil { + fail(err) + } + fmt.Printf("Created verification key JWK: %s\n", marshalled) + + // JWT verification requires the use of a trust store. A trust store is a set of public JWKs and their issuer identifier. + // A key store can be loaded from a remote JWKS via the use of gose.NewJwksKeyStore("issuer", "jwks_url") which will + // load the JWKS via an HTTP GET request and cache keys and fetch them as required. + keyStore, err := gose.NewTrustKeyStore(map[string]jose.Jwk{"issuer": jwk}) + if err != nil { + fail(err) + } + // Create our verifier using our key store. + jwtVerifier := gose.NewJwtVerifier(keyStore) + + // Finally verify our JWT specifying acceptable audience claim entries. + kid, claims, err := jwtVerifier.Verify(jwt, []string{"my audience"}) + if err != nil { + fail(err) + } + fmt.Printf("Successfully verified JWT signed with key %s\n", kid) + + // We can now look at the claims includes in the JWT + fmt.Printf("JWT Subject: %s\n", claims.SettableJwtClaims.Subject) + fmt.Printf("JWT Audiences: %v\n", claims.SettableJwtClaims.Audiences.Aud) + fmt.Printf("JWT Expiry: %d\n", claims.SettableJwtClaims.Expiration) + fmt.Printf("JWT Not Before: %d\n", claims.SettableJwtClaims.NotBefore) + // Automatic claims are those specified by the JWT signer including the issued at (iat), issuer (iss) and the unique + // JWT ID (jti). + fmt.Printf("JWT Issued At: %d\n", claims.AutomaticJwtClaims.IssuedAt) + fmt.Printf("JWT Issuer: %s\n", claims.AutomaticJwtClaims.Issuer) + fmt.Printf("JWT unique ID: %s\n", claims.AutomaticJwtClaims.JwtID) + + // We can then access any custom claims we expect to be present. + rawClaim, exists := claims.UntypedClaims[houseNumberClaim] + if !exists { + fail(errors.New("missing house number claim")) + } + var houseNumber int + err = json.Unmarshal(rawClaim, &houseNumber) + if err != nil { + fail(err) + } + fmt.Printf("JWT custom claim house number: %d\n", houseNumber) + rawClaim, exists = claims.UntypedClaims[streetClaim] + if !exists { + fail(errors.New("missing street claim")) + } + var street string + err = json.Unmarshal(rawClaim, &street) + if err != nil { + fail(err) + } + fmt.Printf("JWT custom claim street: %s\n", street) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7d8f31b --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/thalesignite/gose + +require ( + github.com/NebulousLabs/fastrand v0.0.0-20180208210444-3cf7173006a0 + github.com/bouk/monkey v1.0.0 + github.com/google/uuid v1.0.0 + github.com/sirupsen/logrus v1.2.0 + github.com/stretchr/testify v1.3.0 +) + +go 1.13 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..02fcba9 --- /dev/null +++ b/go.sum @@ -0,0 +1,24 @@ +github.com/NebulousLabs/fastrand v0.0.0-20180208210444-3cf7173006a0/go.mod h1:Bdzq+51GR4/0DIhaICZEOm+OHvXGwwB2trKZ8B4Y6eQ= +github.com/bouk/monkey v1.0.0 h1:k6z8fLlPhETfn5l9rlWVE7Q6B23DoaqosTdArvNQRdc= +github.com/bouk/monkey v1.0.0/go.mod h1:PG/63f4XEUlVyW1ttIeOJmJhhe1+t9EC/je3eTjvFhE= +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.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +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.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..659a4d5 --- /dev/null +++ b/helpers.go @@ -0,0 +1,557 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "bytes" + "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/binary" + "fmt" + "io" + "math" + "math/big" + "os" + "strings" + + "crypto/ecdsa" + "encoding/json" + "log" + + "github.com/thalesignite/gose/jose" +) + +const ( + //Version1 of the JOSE + version1 = "v1" +) + +func fromBase64(b64 string) (*big.Int, error) { + b, err := base64.RawURLEncoding.DecodeString(b64) + if err != nil { + return nil, err + } + var result big.Int + result.SetBytes(b) + return &result, nil +} + +func isSubset(set []jose.KeyOps, subset []jose.KeyOps) bool { + if len(subset) == 0 { + return false + } + result := true + for _, req := range subset { + opExists := false + for _, op := range set { + opExists = opExists || (req == op) + } + result = result && opExists + } + return result +} + +func intersection(first []jose.KeyOps, second []jose.KeyOps) []jose.KeyOps { + var result []jose.KeyOps + for _, a := range first { + for _, b := range second { + if a == b { + result = append(result, a) + } + } + } + return result +} + +//LoadPrivateKey loads the jwk into a crypto.Signer for performing signing operations +func LoadPrivateKey(jwk jose.Jwk, required []jose.KeyOps) (crypto.Signer, error) { + privateKeyAlgs := map[jose.Alg]bool{ + jose.AlgRS256: true, + jose.AlgRS384: true, + jose.AlgRS512: true, + jose.AlgPS256: true, + jose.AlgPS384: true, + jose.AlgPS512: true, + jose.AlgES256: true, + jose.AlgES384: true, + jose.AlgES512: true, + } + + if _, ok := privateKeyAlgs[jwk.Alg()]; !ok { + return nil, ErrInvalidKeyType + } + if required != nil && len(required) > 0 && !isSubset(jwk.Ops(), required) { + return nil, ErrInvalidOperations + } + switch v := jwk.(type) { + case *jose.PrivateRsaKey: + /* Import RSA private key. */ + if v.D.Empty() || v.E.Empty() || v.N.Empty() { + /* This is a public RSA jwk. */ + return nil, ErrInvalidKeyType + } + var key rsa.PrivateKey + + /* Ensure positive 32-bit integer. */ + if v.E.Int().BitLen() > 32 || v.E.Int().Sign() < 1 { + return nil, ErrInvalidExponent + } + + key.Primes = []*big.Int{v.P.Int(), v.Q.Int()} + key.D = v.D.Int() + key.E = int(v.E.Int().Int64()) + key.N = v.N.Int() + key.Precompute() + // Check the consistency of the precomputable values contained in the JWK. + if key.Precomputed.Dp.Cmp(v.Dp.Int()) != 0 || key.Precomputed.Dq.Cmp(v.Dq.Int()) != 0 || key.Precomputed.Qinv.Cmp(v.Qi.Int()) != 0 { + return nil, ErrInconsistentKeyValues + } + return &key, nil + case *jose.PrivateEcKey: + if v.D.Empty() { + /* This is a public jwk. */ + return nil, ErrInvalidKeyType + } + var key ecdsa.PrivateKey + + key.X = v.X.Int() + key.Y = v.Y.Int() + key.D = v.D.Int() + key.Curve = algToOptsMap[v.Alg()].(*ECDSAOptions).curve + return &key, nil + default: + return nil, ErrUnsupportedKeyType + } +} + +//LoadPublicKey loads jwk as a public key for cryptographic verification operations. +func LoadPublicKey(jwk jose.Jwk, required []jose.KeyOps) (crypto.PublicKey, error) { + publicKeyAlgs := map[jose.Alg]bool{ + jose.AlgRS256: true, + jose.AlgRS384: true, + jose.AlgRS512: true, + jose.AlgPS256: true, + jose.AlgPS384: true, + jose.AlgPS512: true, + jose.AlgES256: true, + jose.AlgES384: true, + jose.AlgES512: true, + } + if _, ok := publicKeyAlgs[jwk.Alg()]; !ok { + return nil, ErrInvalidKeyType + } + if required != nil && len(required) > 0 && !isSubset(jwk.Ops(), required) { + return nil, ErrInvalidOperations + } + switch v := jwk.(type) { + case *jose.PublicRsaKey: + /* Import RSA private jwk. */ + if v.N.Empty() || v.E.Empty() { + /* There's no public parameters. */ + return nil, ErrInvalidKeyType + } + var key rsa.PublicKey + /* Ensure positive 32-bit integer. */ + if v.E.Int().BitLen() > 32 || v.E.Int().Sign() < 1 { + return nil, ErrInvalidExponent + } + key.E = int(v.E.Int().Int64()) + key.N = v.N.Int() + return &key, nil + case *jose.PublicEcKey: + var key ecdsa.PublicKey + if v.X.Empty() || v.Y.Empty() { + /* There's no public parameters. */ + return nil, ErrInvalidKeyType + } + key.X = v.X.Int() + key.Y = v.Y.Int() + key.Curve = algToOptsMap[v.Alg()].(*ECDSAOptions).curve + return &key, nil + default: + return nil, ErrUnsupportedKeyType + } +} + +//LoadJws loads signature, or errors +func LoadJws(jws string) (protectedHeader *jose.JwsHeader, header []byte, data []byte, payload []byte, signature []byte, err error) { + var tmp jose.JwsHeader + parts := strings.Split(jws, ".") + if len(parts) != 3 { + return nil, nil, nil, nil, nil, ErrInvalidJwsCompactEncoding + } + header, err = base64.RawURLEncoding.DecodeString(parts[0]) + if err != nil { + return nil, nil, nil, nil, nil, ErrInvalidJwsBase64HeaderEncoding + } + if err := json.Unmarshal(header, &tmp); err != nil { + return nil, nil, nil, nil, nil, ErrInvalidJwsHeaderEncoding + } + protectedHeader = &tmp + data, err = base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return nil, nil, nil, nil, nil, ErrInvalidJwsBase64BodyEncoding + } + signature, err = base64.RawURLEncoding.DecodeString(parts[2]) + if err != nil { + return nil, nil, nil, nil, nil, ErrInvalidJwsBase64SignatureEncoding + } + payload = []byte(fmt.Sprintf("%s.%s", parts[0], parts[1])) + return +} + +//CalculateKeyID deterministically calculates the ID for the given jwk +func CalculateKeyID(jwk jose.Jwk) (string, error) { + /* Deterministic calculation of a jwk's identity. */ + switch typed := jwk.(type) { + case *jose.PublicRsaKey: + encoded := strings.Join([]string{ + version1, + "jwk", + string(jwk.Kty()), + base64.RawURLEncoding.EncodeToString(typed.N.Int().Bytes()), + base64.RawURLEncoding.EncodeToString(typed.E.Int().Bytes()), + }, ".") + digester := sha256.New() + if _, err := digester.Write([]byte(encoded)); err != nil { + log.Panicf("%s", err) + } + digest := digester.Sum(nil) + return fmt.Sprintf("%x", digest), nil + case *jose.PublicEcKey: + encoded := strings.Join([]string{ + version1, + "jwk", + string(jwk.Kty()), + base64.RawURLEncoding.EncodeToString(typed.X.Int().Bytes()), + base64.RawURLEncoding.EncodeToString(typed.Y.Int().Bytes()), + }, ".") + digester := sha256.New() + if _, err := digester.Write([]byte(encoded)); err != nil { + log.Panicf("%s", err) + } + digest := digester.Sum(nil) + return fmt.Sprintf("%x", digest), nil + case *jose.OctSecretKey: + // Should we include alg in symmetric jwk + // identification? The spec is RFC7517 s4.5 and does + // not give a clear steer. I've adopted the answer + // 'no' for now but we might want to revisit this. + digester := sha256.New() + encoded := strings.Join([]string{ + version1, + "jwk", + string(jwk.Kty()), + base64.RawURLEncoding.EncodeToString(typed.K.Bytes()), + }, ".") + digester.Write([]byte(encoded)) + digest := digester.Sum(nil) + return fmt.Sprintf("%x", digest), nil + default: + return "", ErrUnsupportedKeyType + } +} + +//LoadJwk load io.ReadSeeker as a JWK or error +func LoadJwk(reader io.ReadSeeker, required []jose.KeyOps) (jwk jose.Jwk, err error) { + if jwk, err = jose.UnmarshalJwk(reader); err != nil { + return + } + if len(required) > 0 && !isSubset(jwk.Ops(), required) { + return + } + return +} + +//LoadJwkFromFile loads file as JWK or error +func LoadJwkFromFile(file string, required []jose.KeyOps) (jose.Jwk, error) { + /* Load jwk from file. */ + fd, err := os.Open(file) + if err != nil { + return nil, ErrInvalidSigningKeyURL + } + defer fd.Close() + return LoadJwk(fd, required) +} + +var inverseOps = map[jose.KeyOps]jose.KeyOps{ + jose.KeyOpsEncrypt: jose.KeyOpsDecrypt, + jose.KeyOpsDecrypt: jose.KeyOpsEncrypt, + jose.KeyOpsSign: jose.KeyOpsVerify, + jose.KeyOpsVerify: jose.KeyOpsSign, +} + +func rsaBitsToAlg(bitLen int) jose.Alg { + /* Based on NIST recommendations from 2016. */ + if bitLen >= 15360 { + return jose.AlgPS512 + } else if bitLen >= 7680 { + return jose.AlgPS384 + } + return jose.AlgPS256 +} + +func ecBitsToAlg(bitLen int) jose.Alg { + switch bitLen { + case 256: + return jose.AlgES256 + case 384: + return jose.AlgES384 + case 521: + return jose.AlgES512 + default: + return "Unsupported" + } +} + +//PublicFromPrivate extracts public jwk from private jwk in JWK format +func PublicFromPrivate(in jose.Jwk) (jose.Jwk, error) { + var out jose.Jwk + switch k := in.(type) { + case *jose.PrivateRsaKey: + if k.D.Empty() || k.Q.Empty() || k.Dq.Empty() || k.P.Empty() || + k.Dp.Empty() || k.Qi.Empty() || k.N.Empty() || k.E.Empty() { + /* This is either badly formed or a public jwk. */ + return nil, ErrInvalidKeyType + } + var result jose.PublicRsaKey + result.PublicRsaKeyFields = k.PublicRsaKeyFields + out = &result + case *jose.PrivateEcKey: + var result jose.PublicEcKey + result.PublicEcKeyFields = k.PublicEcKeyFields + out = &result + default: + return nil, ErrUnsupportedKeyType + } + out.SetKid(in.Kid()) + out.SetAlg(in.Alg()) + var ops []jose.KeyOps + for _, op := range in.Ops() { + ops = append(ops, inverseOps[op]) + } + out.SetOps(ops) + out.SetX5C(in.X5C()) + return out, nil +} + +//JwkToString return JWK as string +func JwkToString(jwk jose.Jwk) (string, error) { + b, err := json.Marshal(jwk) + if err != nil { + return "", err + } + return string(b), nil +} + +func base64EncodeInt32(val uint32) string { + var buf bytes.Buffer + if err := binary.Write(&buf, binary.BigEndian, &val); err != nil { + log.Panicf("%s", err) + } + return base64.RawURLEncoding.EncodeToString(buf.Bytes()) +} + +//JwkFromPrivateKey builds JWK, from a crypto.Signer, with certificates, and scoped to certain operations, or errors +func JwkFromPrivateKey(privateKey crypto.Signer, operations []jose.KeyOps, certs []*x509.Certificate) (jose.Jwk, error) { + var jwk jose.Jwk + switch v := privateKey.(type) { + case *rsa.PrivateKey: + if v.E > math.MaxInt32 { + return nil, ErrInvalidExponent + } + alg := rsaBitsToAlg(v.N.BitLen()) + /* Key generation. */ + v.Precompute() + var rsa jose.PrivateRsaKey + rsa.SetAlg(alg) + rsa.Q.Set(v.Primes[1]) + rsa.Qi.Set(v.Precomputed.Qinv) + rsa.Dq.Set(v.Precomputed.Dq) + rsa.P.Set(v.Primes[0]) + rsa.Dp.Set(v.Precomputed.Dp) + rsa.N.Set(v.N) + rsa.E.Set(big.NewInt(int64(v.E))) + rsa.D.Set(v.D) + jwk = &rsa + case *ecdsa.PrivateKey: + var ec jose.PrivateEcKey + alg := ecBitsToAlg(v.Curve.Params().BitSize) + ec.SetAlg(alg) + ec.X.Set(v.X) + ec.Y.Set(v.Y) + ec.D.Set(v.D) + ec.Crv = jose.Crv(v.Curve.Params().Name) + jwk = &ec + default: + return nil, ErrUnsupportedKeyType + } + jwk.SetOps(operations) + if len(certs) > 0 { + jwk.SetX5C(certs) + } + publicKey, err := PublicFromPrivate(jwk) + if err != nil { + // We should have erred before we ever get here. + log.Panic("Failed to derive public jwk from private") + } + kid, err := CalculateKeyID(publicKey) + if err != nil { + // We should have erred before we ever get here. + log.Panic("Failed to calculate Key ID") + } + jwk.SetKid(kid) + + return jwk, nil +} + +//JwkFromPublicKey builds public JWK, from a crypto.Signer, with certificates, and scoped to certain operations, or errors +func JwkFromPublicKey(publicKey crypto.PublicKey, operations []jose.KeyOps, certs []*x509.Certificate) (jose.Jwk, error) { + var jwk jose.Jwk + switch v := publicKey.(type) { + case *rsa.PublicKey: + if v.E > math.MaxInt32 { + return nil, ErrInvalidExponent + } + alg := rsaBitsToAlg(v.N.BitLen()) + /* Key generation. */ + var rsa jose.PublicRsaKey + rsa.SetAlg(alg) + rsa.N.Set(v.N) + rsa.E.Set(big.NewInt(int64(v.E))) + jwk = &rsa + case *ecdsa.PublicKey: + var ec jose.PublicEcKey + alg := ecBitsToAlg(v.Curve.Params().BitSize) + ec.SetAlg(alg) + ec.X.Set(v.X) + ec.Y.Set(v.Y) + ec.Crv = jose.Crv(v.Curve.Params().Name) + jwk = &ec + default: + return nil, ErrUnsupportedKeyType + } + jwk.SetOps(operations) + kid, _ := CalculateKeyID(jwk) + jwk.SetKid(kid) + if len(certs) > 0 { + jwk.SetX5C(certs) + } + return jwk, nil +} + +// Characteristics of symmetric algorithms +type symmetricAlgInfo struct { + minLen int + maxLen int + confidentiality bool + integrity bool +} + +// Table of known symmetric algorithms +var symmetricAlgs = map[jose.Alg]symmetricAlgInfo{ + jose.AlgA128GCM: {16, 16, true, true}, + jose.AlgA192GCM: {24, 24, true, true}, + jose.AlgA256GCM: {32, 32, true, true}, +} + +// JwkFromSymmetric converts a byte string to a jose.Jwk, given a particular JWK algorithm. +func JwkFromSymmetric(key []byte, alg jose.Alg) (jwk jose.Jwk, err error) { + // Validity checking & default ops + ops := make([]jose.KeyOps, 0, 4) + if sai, ok := symmetricAlgs[alg]; ok { + if len(key) < sai.minLen || len(key) > sai.maxLen { + err = ErrInvalidKeyLength + return + } + if sai.confidentiality { + ops = append(ops, jose.KeyOpsEncrypt, jose.KeyOpsDecrypt) + } + if sai.integrity { + ops = append(ops, jose.KeyOpsSign, jose.KeyOpsVerify) + } + } else { + err = ErrUnsupportedKeyType + return + } + oct := jose.OctSecretKey{} + oct.SetOps(ops) + oct.SetAlg(alg) + oct.K.SetBytes(key) + var kid string + if kid, err = CalculateKeyID(&oct); err != nil { + return + } + oct.SetKid(kid) + jwk = &oct + return +} + +// Extract the raw bytes of a symmetric jwk. Only 'oct' keys are supported. +func loadSymmetricBytes(jwk jose.Jwk, required []jose.KeyOps) (key []byte, err error) { + // TODO I made this private to discourage promiscuous use of + // raw key bytes, but if required it could easily be public. + if _, ok := symmetricAlgs[jwk.Alg()]; !ok { + err = ErrInvalidKeyType + return + } + if required != nil && len(required) > 0 && !isSubset(jwk.Ops(), required) { + err = ErrInvalidOperations + return + } + switch v := jwk.(type) { + case *jose.OctSecretKey: + key = v.K.Bytes() + return + default: + err = ErrUnsupportedKeyType + return + } +} + +// LoadSymmetricAEAD returns a cipher.AEAD for a jwk. +func LoadSymmetricAEAD(jwk jose.Jwk, required []jose.KeyOps) (a cipher.AEAD, err error) { + var key []byte + if key, err = loadSymmetricBytes(jwk, required); err != nil { + return + } + v := jwk.(*jose.OctSecretKey) // can't fail, previous call would have errored + switch v.Alg() { + case jose.AlgA128GCM, jose.AlgA192GCM, jose.AlgA256GCM: + var b cipher.Block + if b, err = aes.NewCipher(key); err != nil { + return + } + if a, err = cipher.NewGCM(b); err != nil { + return + } + return + default: + err = ErrUnsupportedKeyType + return + } +} diff --git a/helpers_test.go b/helpers_test.go new file mode 100644 index 0000000..3ede8c1 --- /dev/null +++ b/helpers_test.go @@ -0,0 +1,291 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "regexp" + "testing" + + "github.com/thalesignite/gose/jose" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateKID(t *testing.T) { + // Setup + var jwk jose.PublicRsaKey + jwk.N.SetBytes([]byte("12345678")) + jwk.E.SetBytes([]byte("87654321")) + + // Act + uid, err := CalculateKeyID(&jwk) + + // Assert + assert.NoError(t, err) + assert.Regexp(t, regexp.MustCompile("^[a-z0-9]{64}$"), uid) +} + +func TestPrivateKeySerializeationAndDeserialization(t *testing.T) { + // Setup + expectedOps := []jose.KeyOps{jose.KeyOpsSign} + originalKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + // Act + jwk, err := JwkFromPrivateKey(originalKey, expectedOps, nil) + require.NoError(t, err) + recoveredKey, err := LoadPrivateKey(jwk, nil) + require.NoError(t, err) + + // Assert + recoveredRsaKey, ok := recoveredKey.(*rsa.PrivateKey) + require.True(t, ok) + assert.Equal(t, originalKey.E, recoveredRsaKey.E) + assert.Equal(t, originalKey.N.Cmp(recoveredRsaKey.N), 0) + assert.Equal(t, originalKey.D.Cmp(recoveredRsaKey.D), 0) + require.Equal(t, len(originalKey.Primes), len(recoveredRsaKey.Primes)) + for i, prime := range originalKey.Primes { + assert.Equal(t, prime.Cmp(recoveredRsaKey.Primes[i]), 0) + } + assert.Equal(t, originalKey.Precomputed.Qinv.Cmp(recoveredRsaKey.Precomputed.Qinv), 0) + assert.Equal(t, originalKey.Precomputed.Dq.Cmp(recoveredRsaKey.Precomputed.Dq), 0) + assert.Equal(t, originalKey.Precomputed.Dp.Cmp(recoveredRsaKey.Precomputed.Dp), 0) + require.Equal(t, len(originalKey.Precomputed.CRTValues), len(recoveredRsaKey.Precomputed.CRTValues)) + for i, value := range originalKey.Precomputed.CRTValues { + assert.Equal(t, value.Coeff.Cmp(recoveredRsaKey.Precomputed.CRTValues[i].Coeff), 0) + assert.Equal(t, value.Exp.Cmp(recoveredRsaKey.Precomputed.CRTValues[i].Exp), 0) + assert.Equal(t, value.R.Cmp(recoveredRsaKey.Precomputed.CRTValues[i].R), 0) + } + require.Equal(t, len(expectedOps), len(jwk.Ops())) + for i, got := range jwk.Ops() { + assert.Equal(t, expectedOps[i], got) + } +} + +func TestPublicKeySerializeationAndDeserialization(t *testing.T) { + // Setup + expectedOps := []jose.KeyOps{jose.KeyOpsVerify} + originalKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + // Act + jwk, err := JwkFromPublicKey(originalKey.Public(), expectedOps, nil) + require.NoError(t, err) + recoveredKey, err := LoadPublicKey(jwk, nil) + require.NoError(t, err) + + // Assert + recoveredRsaKey, ok := recoveredKey.(*rsa.PublicKey) + require.True(t, ok) + assert.Equal(t, originalKey.E, recoveredRsaKey.E) + assert.Equal(t, originalKey.N.Cmp(recoveredRsaKey.N), 0) + require.Equal(t, len(expectedOps), len(jwk.Ops())) + for i, got := range jwk.Ops() { + assert.Equal(t, expectedOps[i], got) + } +} + +func TestAESKeySerializationAndDeserialization(t *testing.T) { + var err error + key8 := make([]byte, 8) + key16 := make([]byte, 16) + key24 := make([]byte, 24) + key32 := make([]byte, 32) + _, err = rand.Read(key8) + require.NoError(t, err) + _, err = rand.Read(key16) + require.NoError(t, err) + _, err = rand.Read(key24) + require.NoError(t, err) + _, err = rand.Read(key32) + require.NoError(t, err) + + var jwk16, jwk24, jwk32 jose.Jwk + jwk16, err = JwkFromSymmetric(key8, jose.AlgA128GCM) + assert.Error(t, err, ErrInvalidKeyLength) + jwk16, err = JwkFromSymmetric(key24, jose.AlgA128GCM) + assert.Error(t, err, ErrInvalidKeyLength) + + jwk16, err = JwkFromSymmetric(key16, jose.AlgA128GCM) + assert.NoError(t, err) + jwk24, err = JwkFromSymmetric(key24, jose.AlgA192GCM) + assert.NoError(t, err) + jwk32, err = JwkFromSymmetric(key32, jose.AlgA256GCM) + assert.NoError(t, err) + + var out16, out24, out32 []byte + out16, err = loadSymmetricBytes(jwk16, nil) + require.NoError(t, err) + assert.Equal(t, key16, out16) + out24, err = loadSymmetricBytes(jwk24, nil) + require.NoError(t, err) + assert.Equal(t, key24, out24) + out32, err = loadSymmetricBytes(jwk32, nil) + require.NoError(t, err) + assert.Equal(t, key32, out32) +} + +func TestAESKeyUse(t *testing.T) { + var err error + key16 := make([]byte, 16) + _, err = rand.Read(key16) + require.NoError(t, err) + var jwk16 jose.Jwk + jwk16, err = JwkFromSymmetric(key16, jose.AlgA128GCM) + require.NoError(t, err) + var aead cipher.AEAD + aead, err = LoadSymmetricAEAD(jwk16, nil) + require.NoError(t, err) + // Seal something + iv := make([]byte, 12) + _, err = rand.Read(iv) + require.NoError(t, err) + var ciphertext []byte + ciphertext = aead.Seal(ciphertext, iv, []byte("the boy's not right"), []byte("not secret")) + // Open and check + var plaintext []byte + plaintext, err = aead.Open(plaintext, iv, ciphertext, []byte("not secret")) + require.NoError(t, err) + require.Equal(t, plaintext, []byte("the boy's not right")) + // Negative tests + _, err = aead.Open(plaintext, iv, ciphertext, []byte("not right")) + assert.NotNil(t, err) + ciphertext[0] ^= 1 + _, err = aead.Open(plaintext, iv, ciphertext, []byte("not secret")) + assert.NotNil(t, err) + ciphertext[0] ^= 1 + ciphertext[len(ciphertext)-1] ^= 1 + _, err = aead.Open(plaintext, iv, ciphertext, []byte("not secret")) + assert.NotNil(t, err) +} + +func TestIsSubset(t *testing.T) { + // Setup + testCases := []struct { + set []jose.KeyOps + subset []jose.KeyOps + expected bool + }{ + { + set: []jose.KeyOps{jose.KeyOpsVerify}, + subset: []jose.KeyOps{jose.KeyOpsVerify}, + expected: true, + }, + { + set: []jose.KeyOps{jose.KeyOpsVerify}, + subset: []jose.KeyOps{jose.KeyOpsSign}, + expected: false, + }, + { + set: []jose.KeyOps{}, + subset: []jose.KeyOps{jose.KeyOpsVerify}, + expected: false, + }, + { + set: []jose.KeyOps{jose.KeyOpsVerify}, + subset: []jose.KeyOps{}, + expected: false, + }, + } + + // Act + Assert + for _, test := range testCases { + result := isSubset(test.set, test.subset) + assert.Equal(t, test.expected, result) + } +} + +func TestIntersection(t *testing.T) { + // Setup + testCases := []struct { + first []jose.KeyOps + second []jose.KeyOps + expected []jose.KeyOps + }{ + { + first: []jose.KeyOps{jose.KeyOpsVerify}, + second: []jose.KeyOps{jose.KeyOpsVerify}, + expected: []jose.KeyOps{jose.KeyOpsVerify}, + }, + { + first: []jose.KeyOps{jose.KeyOpsSign}, + second: []jose.KeyOps{jose.KeyOpsVerify}, + expected: []jose.KeyOps{}, + }, + { + first: []jose.KeyOps{jose.KeyOpsSign, jose.KeyOpsVerify}, + second: []jose.KeyOps{jose.KeyOpsVerify, jose.KeyOpsSign}, + expected: []jose.KeyOps{jose.KeyOpsVerify, jose.KeyOpsSign}, + }, + } + + for _, test := range testCases { + // Act + Assert + received := intersection(test.first, test.second) + require.Equal(t, len(test.expected), len(received)) + for _, expectedOp := range test.expected { + found := false + for _, receivedOp := range received { + if expectedOp == receivedOp { + found = true + } + } + assert.True(t, found) + } + } +} + +func TestRsaBitsToAlg(t *testing.T) { + testCases := []struct { + input int + expected jose.Alg + }{ + { + input: 1024, + expected: jose.AlgPS256, + }, + { + input: 2048, + expected: jose.AlgPS256, + }, + { + input: 3072, + expected: jose.AlgPS256, + }, + { + input: 7680, + expected: jose.AlgPS384, + }, + { + input: 15360, + expected: jose.AlgPS512, + }, + } + // Act + Assert + for _, test := range testCases { + result := rsaBitsToAlg(test.input) + assert.Equal(t, test.expected, result) + } +} diff --git a/integration_test.go b/integration_test.go new file mode 100644 index 0000000..d85a55e --- /dev/null +++ b/integration_test.go @@ -0,0 +1,162 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "bytes" + "testing" + + "github.com/thalesignite/gose/jose" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type generatorFunc func(alg jose.Alg) (SigningKey, error) + +func Test_JwtGenerateVerify(t *testing.T) { + rsaGenerator := &RsaSigningKeyGenerator{} + ecGenerator := &ECDSASigningKeyGenerator{} + + rsaGeneratorFunc := func(alg jose.Alg) (SigningKey, error) { + return rsaGenerator.Generate(alg, 2048, []jose.KeyOps{jose.KeyOpsSign}) + } + ecGeneratorFunc := func(alg jose.Alg) (SigningKey, error) { + return ecGenerator.Generate(alg, []jose.KeyOps{jose.KeyOpsSign}) + } + cases := []struct { + alg jose.Alg + generator generatorFunc + }{ + // RSxxx + { + alg: jose.AlgRS256, + generator: rsaGeneratorFunc, + }, + { + alg: jose.AlgRS384, + generator: rsaGeneratorFunc, + }, + { + alg: jose.AlgRS512, + generator: rsaGeneratorFunc, + }, + // PSxxx + { + alg: jose.AlgPS256, + generator: rsaGeneratorFunc, + }, + { + alg: jose.AlgPS384, + generator: rsaGeneratorFunc, + }, + { + alg: jose.AlgPS512, + generator: rsaGeneratorFunc, + }, + // ESxxx + { + alg: jose.AlgES256, + generator: ecGeneratorFunc, + }, + { + alg: jose.AlgES384, + generator: ecGeneratorFunc, + }, + { + alg: jose.AlgES512, + generator: ecGeneratorFunc, + }, + } + + for _, testCase := range cases { + // Setup + signingKey, err := testCase.generator(testCase.alg) + require.NoError(t, err) + verificationKey, err := signingKey.Verifier() + require.NoError(t, err) + jwk, err := verificationKey.Jwk() + require.NoError(t, err) + + jwtSigner := NewJwtSigner("issuer", signingKey) + + claims := jose.SettableJwtClaims{ + Audiences: jose.Audiences{Aud: []string{"audience"}}, + Subject: "subject", + } + + untyped := map[string]interface{}{ + "name": "John Doe", + } + + ks, err := NewTrustKeyStore(map[string]jose.Jwk{jwtSigner.Issuer(): jwk}) + require.NoError(t, err) + jwtVerifier := NewJwtVerifier(ks) + + // Act, Assert + jwt, err := jwtSigner.Sign(&claims, untyped) + require.NoError(t, err) + _, recoveredClaims, err := jwtVerifier.Verify(jwt, []string{"audience"}) + require.NoError(t, err) + assert.Equal(t, claims.Audiences.Aud, recoveredClaims.Audiences.Aud) + assert.Equal(t, claims.Subject, recoveredClaims.Subject) + assert.NotZero(t, recoveredClaims.IssuedAt) + assert.Equal(t, jwtSigner.Issuer(), recoveredClaims.Issuer) + + var name string + err = recoveredClaims.UnmarshalCustomClaim("name", &name) + require.NoError(t, err) + + assert.Equal(t, "John Doe", name) + } +} + +func Test_JwtVerifyKAT(t *testing.T) { + // Known Answer Tests were generated using https://jwt.io + // Setup + testCases := []struct { + jwk string + jwt string + }{ + { + jwk: `{"crv":"P-256","kid":"12345","key_ops":["verify"],"kty":"EC","alg":"ES256","x":"EVs_o5-uQbTjL3chynL4wXgUg2R9q9UU8I5mEovUf84","y":"kGe5DgSIycKp8w9aJmoHhB1sB3QTugfnRWm5nU_TzsY"}`, + jwt: `eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMzQ1In0.eyJpc3MiOiJ0ZXN0Iiwic3ViIjoiMTIzNDU2Nzg5MCIsImF1ZCI6Imdvc2UiLCJqdGkiOiIzMWM4NzY3ZC02NzMyLTRkYjQtYjQ4OC0yMWNmMzRlNTQxMmQiLCJleHAiOjI1MTQ5ODA5OTd9.TMtlay5iUSPZ2IQHXM7qK313meUYMTtrvvTzWT1BadapM_S92MAPDtHIJ8A--jUCvPoJ-wIdGlS-mThpsWkpng`, + }, + } + + // Act/Assert + for _, test := range testCases { + // Load JWK + reader := bytes.NewReader([]byte(test.jwk)) + jwk, err := jose.UnmarshalJwk(reader) + require.NoError(t, err) + // Create a KeyStore + ks, err := NewTrustKeyStore(map[string]jose.Jwk{ + "test": jwk, + }) + require.NoError(t, err) + // Create a verifier + verifier := NewJwtVerifier(ks) + // Do the deed + _, _, err = verifier.Verify(test.jwt, []string{"gose"}) + require.NoError(t, err) + } +} diff --git a/interfaces.go b/interfaces.go new file mode 100644 index 0000000..86aba26 --- /dev/null +++ b/interfaces.go @@ -0,0 +1,137 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "crypto" + "crypto/x509" + "fmt" + + "github.com/thalesignite/gose/jose" +) + +//InvalidFormat is an interface for handling invalid format errors +type InvalidFormat struct { + what string +} + +func (err *InvalidFormat) Error() string { + return fmt.Sprintf("Invalid format: %s", err.what) +} + +// Algorithmed is an interface that exposes which algorithm a type can be used +// with. +type Algorithmed interface { + Algorithm() jose.Alg +} + +// Key is an interface representing a cryptographic key. +type Key interface { + // Kid returns the identity of the key. + Kid() string +} + +// MarshalableKey is an interface representing a key that can be marshaled into a JWK. +type MarshalableKey interface { + // Jwk returns the Key as a JSON Web Key. + Jwk() (jose.Jwk, error) + // Marshal marshals a key to it's compact JWK string representation. + Marshal() (string, error) +} + + +// CertifiableKey is an interface representing a key that can have an associated certificate and PEM representation. +type CertifiableKey interface { + // MarshalPem marshals a key to it's PEM representation. + MarshalPem() (string, error) + // Certificates returns the certificate chain for the given key. + Certificates() []*x509.Certificate +} + +// SigningKey interface implementers both digest and signing of data. +type SigningKey interface { + Key + MarshalableKey + CertifiableKey + Algorithmed + // Key returns the underlying key used to sign + Key() crypto.Signer + // Sign digest and sign the given data. + Sign(jose.KeyOps, []byte) ([]byte, error) + // Verifier get the matching verification key. + Verifier() (VerificationKey, error) +} + +// VerificationKey implements verification of a cryptographic signature. +type VerificationKey interface { + Key + MarshalableKey + CertifiableKey + Algorithmed + // Verify verifies the operation being performed is supported and + // that the signature is derived from the data. + Verify(operation jose.KeyOps, data []byte, signature []byte) bool +} + +// AuthenticatedEncryptionKey implements authenticated encryption and decryption. +type AuthenticatedEncryptionKey interface { + Key + Algorithmed + // GenerateNonce generates a nonce of the correct size for use in Sealinging operations. + GenerateNonce() ([]byte, error) + // Seal the given plaintext returning ciphertext and authentication tag. + Seal(operation jose.KeyOps, nonce, plaintext, aad []byte) (ciphertext, tag []byte, err error) + // Open and validate the given ciphertext and tag returning the plaintext. + Open(operation jose.KeyOps, nonce, ciphertext, aad, tag []byte) (plaintext []byte, err error) +} + +// JwtSigner implements generation of signed compact JWTs as defined by https://tools.ietf.org/html/rfc7519. +type JwtSigner interface { + // Issuer returns the identity of the issuing authority + Issuer() string + // Sign signs a set of claims returning a serialized JWT. + Sign(claims *jose.SettableJwtClaims, untyped map[string]interface{}) (string, error) +} + +// JwtVerifier implements verification of signed compact JWTs as defined by https://tools.ietf.org/html/rfc7519. +type JwtVerifier interface { + // Verify verifies a JWT is a valid jwt where the caller can specify a number of allowable audiences. + Verify(jwt string, audience []string) (kid string, claims *jose.JwtClaims, err error) +} + +// TrustStore provides the ability to manage trusted root public keys for use when verifying cryptographic +// signatures. +type TrustStore interface { + Add(issuer string, jwk jose.Jwk) error + Remove(issuer, kid string) bool + Get(issuer, kid string) VerificationKey +} + +// JweEncryptor implements encryption of arbitary plaintext into a compact JWE as defined by https://tools.ietf.org/html/rfc7516. +type JweEncryptor interface { + Encrypt(plaintext, add []byte) (string, error) +} + +// JweDecryptor implements decryption and verification of a given ciphertext and aad to a plaintext as defined by https://tools.ietf.org/html/rfc7516. +type JweDecryptor interface { + Decrypt(jwe string) (plaintext, aad []byte, err error) +} diff --git a/jose/jwe.go b/jose/jwe.go new file mode 100644 index 0000000..421df92 --- /dev/null +++ b/jose/jwe.go @@ -0,0 +1,107 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package jose + +import ( + "encoding/base64" + "encoding/json" + "strings" +) + +// JweCustomHeaderFields custom JWE defined fields. +type JweCustomHeaderFields struct { + OtherAad *Blob `json:"_thales_aad,omitempty"` +} + +// JweHeader JWE header fields. +type JweHeader struct { + JwsHeader + JweCustomHeaderFields + Enc Enc `json:"enc"` + Zip Zip `json:"zip,omitempty"` +} + +// Jwe representation of a JWE. +type Jwe struct { + Header JweHeader + MarshalledHeader []byte + EncryptedKey []byte + Iv []byte + Ciphertext []byte + Plaintext []byte + Tag []byte +} + +// MarshalHeader marshal JWE header. Note this is not guaranteed to result in the same marshaled representation across +// invocations. +func (jwe *Jwe) MarshalHeader() (err error) { + var headerBytes []byte + if headerBytes, err = json.Marshal(jwe.Header); err != nil { + return + } + jwe.MarshalledHeader = headerBytes + return +} + +//Unmarshal to body string, or error +func (jwe *Jwe) Unmarshal(src string) (err error) { + /* Compact JWS encoding. */ + parts := strings.Split(src, ".") + if len(parts) != 5 { + err = ErrJweFormat + return + } + if jwe.MarshalledHeader, err = base64.RawURLEncoding.DecodeString(parts[0]); err != nil { + return + } + if err = json.Unmarshal(jwe.MarshalledHeader, &jwe.Header); err != nil { + return + } + // JWE Encrypted key can be a zero length key in scenarios such as direct encoding. + if len(jwe.EncryptedKey) > 0 { + if jwe.EncryptedKey, err = base64.RawURLEncoding.DecodeString(parts[1]); err != nil { + return + } + } + if jwe.Iv, err = base64.RawURLEncoding.DecodeString(parts[2]); err != nil { + return + } + if jwe.Ciphertext, err = base64.RawURLEncoding.DecodeString(parts[3]); err != nil { + return + } + if jwe.Tag, err = base64.RawURLEncoding.DecodeString(parts[4]); err != nil { + return + } + return +} + +// Marshal marshal a JWE to it's compact representation. +func (jwe *Jwe) Marshal() string { + stringz := []string{ + base64.RawURLEncoding.EncodeToString(jwe.MarshalledHeader), + base64.RawURLEncoding.EncodeToString(jwe.EncryptedKey), + base64.RawURLEncoding.EncodeToString(jwe.Iv), + base64.RawURLEncoding.EncodeToString(jwe.Ciphertext), + base64.RawURLEncoding.EncodeToString(jwe.Tag), + } + return strings.Join(stringz, ".") +} diff --git a/jose/jwk.go b/jose/jwk.go new file mode 100644 index 0000000..050115a --- /dev/null +++ b/jose/jwk.go @@ -0,0 +1,531 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package jose + +import ( + "crypto" + "crypto/subtle" + "crypto/x509" + "encoding/base64" + "encoding/json" + "io" +) + +//Certificate leaf for JWK +type Certificate struct { + Certificate *x509.Certificate +} + +//MarshalJSON as byte slice or error +func (c *Certificate) MarshalJSON() (dst []byte, err error) { + dst, err = marshalJSONBlob(c.Certificate.Raw, base64.StdEncoding) + return +} + +//UnmarshalJSON byte slice to certificate, or error +func (c *Certificate) UnmarshalJSON(src []byte) (err error) { + var b []byte + if b, err = unmarshalJSONBlob(src, base64.StdEncoding); err != nil { + return + } + c.Certificate, err = x509.ParseCertificate(b) + return err +} + +//Fingerprint represents a SHA1 digest +type Fingerprint struct { + digest []byte +} + +//Bytes of blob in byte slice +func (f *Fingerprint) Bytes() []byte { + return f.digest +} + +//SetBytes of Fingerprint +func (f *Fingerprint) SetBytes(val []byte) *Fingerprint { + f.digest = val + return f +} + +//UnmarshalJSON byte slice to Fingerprint, or error +func (f *Fingerprint) UnmarshalJSON(src []byte) error { + var err error + if f.digest, err = unmarshalJSONBlob(src, base64.RawURLEncoding); err != nil { + return err + } + if len(f.digest) != 20 { + return ErrJwkInvalidFingerprintfomat + } + return nil +} + +//MarshalJSON Fingerprint to byte slice +func (f *Fingerprint) MarshalJSON() (dst []byte, err error) { + if len(f.digest) != 20 { + err = ErrJwkInvalidFingerprintfomat + return + } + dst, err = marshalJSONBlob(f.digest, base64.RawURLEncoding) + return +} + +// Jwk provides an interface for setting and getting common fields +// irrespective of key type. +type Jwk interface { + Kty() Kty + Use() KeyUse + SetUse(use KeyUse) + Ops() []KeyOps + SetOps(ops []KeyOps) + Alg() Alg + SetAlg(alg Alg) + Kid() string + SetKid(kid string) + X5C() []*x509.Certificate + SetX5C(x5c []*x509.Certificate) + X5T() *Fingerprint + SetX5T(hash *Fingerprint) + // TODO: Add x5t#S256 handling including consistency checking + // CheckConsistency verify the JWK is well formed. + CheckConsistency() error +} + +// Common Jwk fields +type jwkFields struct { + KeyUse KeyUse `json:"use,omitempty"` + KeyOps []KeyOps `json:"key_ops,omitempty"` + KeyAlg Alg `json:"alg,omitempty"` + KeyKid string `json:"kid,omitempty"` + KeyX5C []Certificate `json:"x5c,omitempty"` + KeyX5T *Fingerprint `json:"x5t,omitempty"` +} + +func (j *jwkFields) Use() KeyUse { + return j.KeyUse +} + +func (j *jwkFields) SetUse(use KeyUse) { + j.KeyUse = use +} + +func (j *jwkFields) Ops() []KeyOps { + return j.KeyOps +} + +func (j *jwkFields) SetOps(ops []KeyOps) { + j.KeyOps = ops +} + +func (j *jwkFields) Alg() Alg { + return j.KeyAlg +} + +func (j *jwkFields) SetAlg(alg Alg) { + j.KeyAlg = alg +} + +func (j *jwkFields) Kid() string { + return j.KeyKid +} + +func (j *jwkFields) SetKid(kid string) { + j.KeyKid = kid +} + +func (j *jwkFields) X5C() []*x509.Certificate { + certs := make([]*x509.Certificate, 0, len(j.KeyX5C)) + for _, c := range j.KeyX5C { + certs = append(certs, c.Certificate) + } + return certs +} + +func (j *jwkFields) SetX5C(x5c []*x509.Certificate) { + for _, c := range x5c { + j.KeyX5C = append(j.KeyX5C, Certificate{ + Certificate: c, + }) + } +} + +func (j *jwkFields) X5T() *Fingerprint { + return j.KeyX5T +} + +func (j *jwkFields) SetX5T(blob *Fingerprint) { + j.KeyX5T = blob +} + +func (j *jwkFields) CheckConsistency() error { + // Check for duplicate KeyOps. + if len(j.KeyOps) > 0 { + for i, candidate := range j.KeyOps[:len(j.KeyOps)-1] { + for _, item := range j.KeyOps[i+1:] { + if candidate == item { + return ErrDuplicateKeyOps + } + } + } + } + // Check certificate and thumb-print matches + + if len(j.KeyX5C) > 0 { + if j.KeyX5T != nil { + digester := crypto.SHA1.New() + if _, err := digester.Write(j.KeyX5C[0].Certificate.Raw); err != nil { + return err + } + digest := digester.Sum(nil) + if subtle.ConstantTimeCompare(digest, j.KeyX5T.digest) != 1 { + return ErrJwkInconsistentCertificateFields + } + } + } + return nil +} + +//PublicRsaKeyFields Public RSA specific fields. +type PublicRsaKeyFields struct { + N BigNum `json:"n"` + E BigNum `json:"e"` +} + +//PublicRsaKey Public RSA JWK type. +type PublicRsaKey struct { + jwkFields + PublicRsaKeyFields +} + +//Kty key type +func (k *PublicRsaKey) Kty() Kty { + return KtyRSA +} + +//MarshalJSON to byte slice or error +func (k *PublicRsaKey) MarshalJSON() (dst []byte, err error) { + toMarshal := struct { + *jwkFields + *PublicRsaKeyFields + Kty Kty `json:"kty"` + }{ + jwkFields: &k.jwkFields, + PublicRsaKeyFields: &k.PublicRsaKeyFields, + Kty: KtyRSA, + } + dst, err = json.Marshal(&toMarshal) + return +} + +//UnmarshalJSON byte slice or error +func (k *PublicRsaKey) UnmarshalJSON(src []byte) (err error) { + toUnmarshal := struct { + *jwkFields + *PublicRsaKeyFields + Kty Kty `json:"kty"` + }{ + jwkFields: &k.jwkFields, + PublicRsaKeyFields: &k.PublicRsaKeyFields, + Kty: "", + } + if err = json.Unmarshal(src, &toUnmarshal); err != nil { + return + } + if toUnmarshal.Kty != KtyRSA { + err = ErrUnexpectedKeyType + } + err = k.CheckConsistency() + return +} + +//PrivateRsaKeyFields Private RSA specific fields. +type PrivateRsaKeyFields struct { + D BigNum `json:"d"` + P BigNum `json:"p"` + Q BigNum `json:"q"` + Dp BigNum `json:"dp"` + Dq BigNum `json:"dq"` + Qi BigNum `json:"qi"` +} + +//PrivateRsaKey Private RSA JWK type. +type PrivateRsaKey struct { + PublicRsaKey + PrivateRsaKeyFields +} + +//Kty key type +func (k *PrivateRsaKey) Kty() Kty { + return KtyRSA +} + +//MarshalJSON to byte slice or error +func (k *PrivateRsaKey) MarshalJSON() (dst []byte, err error) { + toMarshal := struct { + Kty Kty `json:"kty"` + *jwkFields + *PublicRsaKeyFields + *PrivateRsaKeyFields + }{ + Kty: KtyRSA, + jwkFields: &k.jwkFields, + PublicRsaKeyFields: &k.PublicRsaKeyFields, + PrivateRsaKeyFields: &k.PrivateRsaKeyFields, + } + dst, err = json.Marshal(&toMarshal) + return +} + +//UnmarshalJSON byte slice or error +func (k *PrivateRsaKey) UnmarshalJSON(src []byte) (err error) { + toUnmarshal := struct { + Kty Kty `json:"kty"` + *jwkFields + *PublicRsaKeyFields + *PrivateRsaKeyFields + }{ + Kty: "", + jwkFields: &k.jwkFields, + PublicRsaKeyFields: &k.PublicRsaKeyFields, + PrivateRsaKeyFields: &k.PrivateRsaKeyFields, + } + if err = json.Unmarshal(src, &toUnmarshal); err != nil { + return + } + if toUnmarshal.Kty != KtyRSA { + err = ErrUnexpectedKeyType + } + err = k.CheckConsistency() + return +} + +//PublicEcKeyFields Public EC specific fields. +type PublicEcKeyFields struct { + Crv Crv `json:"crv"` + X BigNum `json:"x"` + Y BigNum `json:"y"` +} + +//PublicEcKey Public EC JWK type. +type PublicEcKey struct { + jwkFields + PublicEcKeyFields +} + +//Kty key type +func (k *PublicEcKey) Kty() Kty { + return KtyEC +} + +//MarshalJSON to byte slice or error +func (k *PublicEcKey) MarshalJSON() (dst []byte, err error) { + toMarshal := struct { + *jwkFields + *PublicEcKeyFields + Kty Kty `json:"kty"` + }{ + jwkFields: &k.jwkFields, + PublicEcKeyFields: &k.PublicEcKeyFields, + Kty: KtyEC, + } + dst, err = json.Marshal(&toMarshal) + return +} + +//UnmarshalJSON byte slice or error +func (k *PublicEcKey) UnmarshalJSON(src []byte) (err error) { + toUnmarshal := struct { + Kty Kty `json:"kty"` + *jwkFields + *PublicEcKeyFields + }{ + Kty: "", + jwkFields: &k.jwkFields, + PublicEcKeyFields: &k.PublicEcKeyFields, + } + if err = json.Unmarshal(src, &toUnmarshal); err != nil { + return + } + if toUnmarshal.Kty != KtyEC { + err = ErrUnexpectedKeyType + } + err = k.CheckConsistency() + return +} + +//PrivateEcKeyFields Private EC specific fields. +type PrivateEcKeyFields struct { + D BigNum `json:"d"` +} + +//PrivateEcKey Private EC JWK type. +type PrivateEcKey struct { + PublicEcKey + PrivateEcKeyFields +} + +//Kty key type +func (k *PrivateEcKey) Kty() Kty { + return KtyEC +} + +//MarshalJSON to byte slice or error +func (k *PrivateEcKey) MarshalJSON() (dst []byte, err error) { + toMarshal := struct { + Kty Kty `json:"kty"` + *jwkFields + *PublicEcKeyFields + *PrivateEcKeyFields + }{ + Kty: KtyEC, + jwkFields: &k.jwkFields, + PublicEcKeyFields: &k.PublicEcKeyFields, + PrivateEcKeyFields: &k.PrivateEcKeyFields, + } + dst, err = json.Marshal(&toMarshal) + return +} + +//UnmarshalJSON byte slice or error +func (k *PrivateEcKey) UnmarshalJSON(src []byte) (err error) { + toUnmarshal := struct { + Kty Kty `json:"kty"` + *jwkFields + *PublicEcKeyFields + *PrivateEcKeyFields + }{ + Kty: "", + jwkFields: &k.PublicEcKey.jwkFields, + PublicEcKeyFields: &k.PublicEcKeyFields, + PrivateEcKeyFields: &k.PrivateEcKeyFields, + } + if err = json.Unmarshal(src, &toUnmarshal); err != nil { + return + } + if toUnmarshal.Kty != KtyEC { + err = ErrUnexpectedKeyType + } + err = k.CheckConsistency() + return +} + +//OctSecretKeyFields Secret key specific fields. +type OctSecretKeyFields struct { + K Blob `json:"k"` +} + +//OctSecretKey Secret key JWK type. +type OctSecretKey struct { + jwkFields + OctSecretKeyFields +} + +//Kty key type +func (k *OctSecretKey) Kty() Kty { + return KtyOct +} + +//MarshalJSON to byte slice or error +func (k *OctSecretKey) MarshalJSON() (dst []byte, err error) { + toMarshal := struct { + *jwkFields + *OctSecretKeyFields + Kty Kty `json:"kty"` + }{ + jwkFields: &k.jwkFields, + OctSecretKeyFields: &k.OctSecretKeyFields, + Kty: KtyOct, + } + dst, err = json.Marshal(&toMarshal) + return +} + +//UnmarshalJSON to to byte slice or error +func (k *OctSecretKey) UnmarshalJSON(src []byte) (err error) { + toUnmarshal := struct { + *jwkFields + *OctSecretKeyFields + Kty Kty `json:"kty"` + }{ + jwkFields: &k.jwkFields, + OctSecretKeyFields: &k.OctSecretKeyFields, + Kty: "", + } + if err = json.Unmarshal(src, &toUnmarshal); err != nil { + return + } + if toUnmarshal.Kty != KtyOct { + err = ErrUnexpectedKeyType + } + err = k.CheckConsistency() + return +} + +//UnmarshalJwk serialization into a concrete type. +func UnmarshalJwk(reader io.ReadSeeker) (jwk Jwk, err error) { + // First unmarshal Kty so that we can work out how to proceed. + decoder := json.NewDecoder(reader) + keyType := struct { + Kty Kty `json:"kty"` + }{} + if err = decoder.Decode(&keyType); err != nil { + return + } + if _, err = reader.Seek(0, io.SeekStart); err != nil { + return + } + switch keyType.Kty { + case KtyRSA: + var rsa PrivateRsaKey + if err = decoder.Decode(&rsa); err != nil { + return + } + // Look at D to assert whether this is a private or public RSA key. + if rsa.D.Int().BitLen() == 0 { + jwk = &rsa.PublicRsaKey + } else { + jwk = &rsa + } + return + case KtyEC: + var ec PrivateEcKey + if err = decoder.Decode(&ec); err != nil { + return + } + if ec.D.Int().BitLen() == 0 { + jwk = &ec.PublicEcKey + } else { + jwk = &ec + } + return + case KtyOct: + var oct OctSecretKey + if err = decoder.Decode(&oct); err != nil { + return + } + jwk = &oct + default: + err = ErrUnsupportedKeyType + return + } + return +} diff --git a/jose/jwk_test.go b/jose/jwk_test.go new file mode 100644 index 0000000..52df4bf --- /dev/null +++ b/jose/jwk_test.go @@ -0,0 +1,457 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package jose + +import ( + "bytes" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/json" + "fmt" + "reflect" + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestJsonUnmarshal_LastKeyWins(t *testing.T) { + // The various JOSE specifications state JSOn objects with duplicate entries should either be rejected or the last + // entry wins. As all our objects are typed we should hit this issue during encoding but we may in decoding. This test + // just clarifies the go runtime is doing what we want which is to take the last entry. + type TestType struct { + Field string + } + testCases := []struct { + input string + expected TestType + }{ + { + input: `{"Field":"first","Field":"last"}`, + expected: TestType{ + Field: "last", + }, + }, + { + input: `{"Field":"1","Field":"2","Field":"3"}`, + expected: TestType{ + Field: "3", + }, + }, + { + input: `{"Field":"one","Field":"more","Field":"test"}`, + expected: TestType{ + Field: "test", + }, + }, + } + + // Act//Assert + for i, test := range testCases { + t.Run(fmt.Sprintf("%d", i+1), + func(t *testing.T) { + var item TestType + err := json.Unmarshal([]byte(test.input), &item) + require.NoError(t, err) + assert.Equal(t, test.expected.Field, item.Field) + }) + } +} + +func TestPublicRSA_UnmarshalJSON(t *testing.T) { + // Setup + const input = ` +{ + "kty": "RSA", + "n": "BBBB", + "e": "AQAB", + "kid": "1", + "x5c": ["MIID9jCCAt6gAwIBAgIJAJjAfywQgInaMA0GCSqGSIb3DQEBCwUAMIGOMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTESMBAGA1UEBwwJc29tZS1jaXR5MRMwEQYDVQQKDAphY21lIGNvcnAuMQ0wCwYDVQQLDAR0ZXN0MREwDwYDVQQDDAhhY21lLmNvbTEfMB0GCSqGSIb3DQEJARYQbm93aGVyZUBhY21lLmNvbTAgFw0xODEyMTAxMjMxNTdaGA8yNTY2MDcxMDEyMzE1N1owgY4xCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlzb21lLWNpdHkxEzARBgNVBAoMCmFjbWUgY29ycC4xDTALBgNVBAsMBHRlc3QxETAPBgNVBAMMCGFjbWUuY29tMR8wHQYJKoZIhvcNAQkBFhBub3doZXJlQGFjbWUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzMi/g4y3Zgr4z5azcA3wgDfvqFBYYX9GQldp2Po8127I26Ln6fugWPN3vGPD++cM+eZbeTfbqomw3ZPmdmgPDwGx5ODoa9a32Ar9MdTQQxltvlSWvyF2c92ZZ9DUBedVNHArTJmbPRofBpqHuBDxn1NsY9neA75lgxFsatbzEHI2FKva3TSwREZlOEuSkYQVsfZAT3c3TnSMVRs0jY8qQijBs59inH0PjlGeiNnZNcpVC7jYrP4PjJFnBgSC4gz2aYy0cLNcLdPsckJM/84eepexQ97+SBERm9eMXTeoCxMUflEfXEa0DXn4WdGBMM7XT6hF7xqnHs62M0OeOYJMYwIDAQABo1MwUTAdBgNVHQ4EFgQUCiyj1kOqmpWm1WPl0pjihQskpXIwHwYDVR0jBBgwFoAUCiyj1kOqmpWm1WPl0pjihQskpXIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAsGVlC6nFydUA3NEoA2hqmZXVxXvPLHTTTh1ZSYUf9WhMNEPUUjrwx/KCY4UBEoVqF5LfH872Fjf+nQSorpnJAC0kPM2VoAp2n74Dj5IgDz6OUw0b4cSeVEnNm6gyf088VsiVyunbG5peiZ4rexBJ4dcRzWMOkUvtXZmTYkv/WM5WlmSTPLFdbaSr4Pkzk1bjGpP5Qc4k24tIFqKbh3AxZ04VkRPb39DHo/KATJPG1Or/c1TxsyfOKV8OrEpl0spyLeUDkQ6ZA7KDym8y6IUCGNff58zsdlgEpfa7PtcNl/AinkWC519MhkyWhQU15AnkXSQ42bAnoDuzM+xBuasDyw=="], + "x5t": "PCq4Z1N-ranPT0wQq1G5ezVk7b4" +} +` + // Act + var rsa PublicRsaKey + err := json.Unmarshal([]byte(input), &rsa) + + // Assert + assert.NoError(t, err) + assert.Equal(t, KtyRSA, rsa.Kty()) + assert.Equal(t, int64(65537), rsa.E.Int().Int64()) + assert.Equal(t, int64(266305), rsa.N.Int().Int64()) + assert.Equal(t, "1", rsa.Kid()) + assert.Equal(t, 1, len(rsa.X5C())) +} + +func TestPublicRSA_MarshalJSON(t *testing.T) { + // Setup + certBytes, err := base64.StdEncoding.DecodeString("MIID9jCCAt6gAwIBAgIJAJjAfywQgInaMA0GCSqGSIb3DQEBCwUAMIGOMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTESMBAGA1UEBwwJc29tZS1jaXR5MRMwEQYDVQQKDAphY21lIGNvcnAuMQ0wCwYDVQQLDAR0ZXN0MREwDwYDVQQDDAhhY21lLmNvbTEfMB0GCSqGSIb3DQEJARYQbm93aGVyZUBhY21lLmNvbTAgFw0xODEyMTAxMjMxNTdaGA8yNTY2MDcxMDEyMzE1N1owgY4xCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlzb21lLWNpdHkxEzARBgNVBAoMCmFjbWUgY29ycC4xDTALBgNVBAsMBHRlc3QxETAPBgNVBAMMCGFjbWUuY29tMR8wHQYJKoZIhvcNAQkBFhBub3doZXJlQGFjbWUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzMi/g4y3Zgr4z5azcA3wgDfvqFBYYX9GQldp2Po8127I26Ln6fugWPN3vGPD++cM+eZbeTfbqomw3ZPmdmgPDwGx5ODoa9a32Ar9MdTQQxltvlSWvyF2c92ZZ9DUBedVNHArTJmbPRofBpqHuBDxn1NsY9neA75lgxFsatbzEHI2FKva3TSwREZlOEuSkYQVsfZAT3c3TnSMVRs0jY8qQijBs59inH0PjlGeiNnZNcpVC7jYrP4PjJFnBgSC4gz2aYy0cLNcLdPsckJM/84eepexQ97+SBERm9eMXTeoCxMUflEfXEa0DXn4WdGBMM7XT6hF7xqnHs62M0OeOYJMYwIDAQABo1MwUTAdBgNVHQ4EFgQUCiyj1kOqmpWm1WPl0pjihQskpXIwHwYDVR0jBBgwFoAUCiyj1kOqmpWm1WPl0pjihQskpXIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAsGVlC6nFydUA3NEoA2hqmZXVxXvPLHTTTh1ZSYUf9WhMNEPUUjrwx/KCY4UBEoVqF5LfH872Fjf+nQSorpnJAC0kPM2VoAp2n74Dj5IgDz6OUw0b4cSeVEnNm6gyf088VsiVyunbG5peiZ4rexBJ4dcRzWMOkUvtXZmTYkv/WM5WlmSTPLFdbaSr4Pkzk1bjGpP5Qc4k24tIFqKbh3AxZ04VkRPb39DHo/KATJPG1Or/c1TxsyfOKV8OrEpl0spyLeUDkQ6ZA7KDym8y6IUCGNff58zsdlgEpfa7PtcNl/AinkWC519MhkyWhQU15AnkXSQ42bAnoDuzM+xBuasDyw==") + require.NoError(t, err) + cert, err := x509.ParseCertificate(certBytes) + require.NoError(t, err) + digestBytes, err := base64.RawURLEncoding.DecodeString("PCq4Z1N-ranPT0wQq1G5ezVk7b4") + require.NoError(t, err) + var rsa PublicRsaKey + rsa.SetKid("1") + rsa.SetX5C([]*x509.Certificate{cert}) + rsa.SetX5T(&Fingerprint{digest: digestBytes}) + rsa.N.SetBytes([]byte{0, 1, 2, 3, 4}) + rsa.E.SetBytes([]byte{1, 0, 1}) + + // Act + output, err := json.Marshal(&rsa) + + // Assert + assert.NoError(t, err) + assert.Regexp(t, regexp.MustCompile(`"kty":\s*"RSA"`), string(output)) + assert.Regexp(t, regexp.MustCompile(`"kid":\s*"1"`), string(output)) + assert.Regexp(t, regexp.MustCompile(`"n":\s*"AQIDBA"`), string(output)) + assert.Regexp(t, regexp.MustCompile(`"e":\s*"AQAB"`), string(output)) + assert.Regexp(t, regexp.MustCompile(`"x5t":\s*"PCq4Z1N-ranPT0wQq1G5ezVk7b4"`), string(output)) +} + +func TestPrivateRSA_UnmarshalJSON(t *testing.T) { + // Setup + const input = ` +{ + "kty": "RSA", + "n": "BBBB", + "e": "AQAB", + "d": "BBBB", + "p": "BBBB", + "q": "BBBB", + "dp": "BBBB", + "dq": "BBBB", + "qi": "BBBB", + "kid": "1" +}` + // Act + var rsa PrivateRsaKey + err := json.Unmarshal([]byte(input), &rsa) + + // Assert + assert.NoError(t, err) + assert.Equal(t, KtyRSA, rsa.Kty()) + assert.Equal(t, int64(65537), rsa.E.Int().Int64()) + assert.Equal(t, "1", rsa.Kid()) + assert.Equal(t, int64(266305), rsa.N.Int().Int64()) + assert.Equal(t, int64(266305), rsa.D.Int().Int64()) + assert.Equal(t, int64(266305), rsa.P.Int().Int64()) + assert.Equal(t, int64(266305), rsa.Q.Int().Int64()) + assert.Equal(t, int64(266305), rsa.Dp.Int().Int64()) + assert.Equal(t, int64(266305), rsa.Dq.Int().Int64()) + assert.Equal(t, int64(266305), rsa.Qi.Int().Int64()) +} + +func TestPrivateRSA_MarshalJSON(t *testing.T) { + // Setup + var rsa PrivateRsaKey + rsa.SetKid("1") + rsa.N.SetBytes([]byte{0, 1, 2, 3, 4}) + rsa.D.SetBytes([]byte{0, 1, 2, 3, 4}) + rsa.P.SetBytes([]byte{0, 1, 2, 3, 4}) + rsa.Q.SetBytes([]byte{0, 1, 2, 3, 4}) + rsa.Dp.SetBytes([]byte{0, 1, 2, 3, 4}) + rsa.Dq.SetBytes([]byte{0, 1, 2, 3, 4}) + rsa.Qi.SetBytes([]byte{0, 1, 2, 3, 4}) + rsa.E.SetBytes([]byte{1, 0, 1}) + + // Act + output, err := json.Marshal(&rsa) + + // Assert + assert.NoError(t, err) + assert.Regexp(t, regexp.MustCompile(`"kty":\s*"RSA"`), string(output)) + assert.Regexp(t, regexp.MustCompile(`"kid":\s*"1"`), string(output)) + assert.Regexp(t, regexp.MustCompile(`"n":\s*"AQIDBA"`), string(output)) + assert.Regexp(t, regexp.MustCompile(`"d":\s*"AQIDBA"`), string(output)) + assert.Regexp(t, regexp.MustCompile(`"p":\s*"AQIDBA"`), string(output)) + assert.Regexp(t, regexp.MustCompile(`"q":\s*"AQIDBA"`), string(output)) + assert.Regexp(t, regexp.MustCompile(`"dp":\s*"AQIDBA"`), string(output)) + assert.Regexp(t, regexp.MustCompile(`"dq":\s*"AQIDBA"`), string(output)) + assert.Regexp(t, regexp.MustCompile(`"qi":\s*"AQIDBA"`), string(output)) + assert.Regexp(t, regexp.MustCompile(`"e":\s*"AQAB"`), string(output)) +} + +func TestPublicEC_UnmarshalJSON(t *testing.T) { + // Setup + const input = ` +{ + "kty": "EC", + "crv": "P-256", + "x": "BBBB", + "y": "AQAB", + "kid": "1" +} +` + // Act + var ec PublicEcKey + err := json.Unmarshal([]byte(input), &ec) + + // Assert + assert.NoError(t, err) + assert.Equal(t, KtyEC, ec.Kty()) + assert.Equal(t, CrvP256, ec.Crv) + assert.Equal(t, int64(266305), ec.X.Int().Int64()) + assert.Equal(t, int64(65537), ec.Y.Int().Int64()) + assert.Equal(t, "1", ec.Kid()) +} + +func TestPublicEC_MarshalJSON(t *testing.T) { + // Setup + var ec PublicEcKey + ec.SetKid("1") + ec.Crv = CrvP256 + ec.X.Int().SetBytes([]byte{1, 0, 1}) + ec.Y.Int().SetBytes([]byte{1, 0, 1}) + + // Act + dst, err := json.Marshal(&ec) + + // Assert + assert.NoError(t, err) + assert.Regexp(t, regexp.MustCompile(`"kty":\s*"EC"`), string(dst)) + assert.Regexp(t, regexp.MustCompile(`"kid":\s*"1"`), string(dst)) + assert.Regexp(t, regexp.MustCompile(`"x":\s*"AQAB"`), string(dst)) + assert.Regexp(t, regexp.MustCompile(`"y":\s*"AQAB"`), string(dst)) +} + +func TestPrivateEC_UnmarshalJSON(t *testing.T) { + // Setup + const input = ` +{ + "kty": "EC", + "crv": "P-256", + "d": "CCCC", + "x": "BBBB", + "y": "AQAB", + "kid": "1" +} +` + // Act + var ec PrivateEcKey + err := json.Unmarshal([]byte(input), &ec) + + // Assert + assert.NoError(t, err) + assert.Equal(t, KtyEC, ec.Kty()) + assert.Equal(t, CrvP256, ec.Crv) + assert.Equal(t, int64(532610), ec.D.Int().Int64()) + assert.Equal(t, int64(266305), ec.X.Int().Int64()) + assert.Equal(t, int64(65537), ec.Y.Int().Int64()) + assert.Equal(t, "1", ec.Kid()) +} + +func TestPrivateEC_MarshalJSON(t *testing.T) { + // Setup + var ec PrivateEcKey + ec.SetKid("1") + ec.Crv = CrvP256 + ec.X.Int().SetBytes([]byte{1, 0, 1}) + ec.Y.Int().SetBytes([]byte{1, 0, 1}) + ec.D.Int().SetBytes([]byte{1, 0, 1}) + + // Act + dst, err := json.Marshal(&ec) + + // Assert + assert.NoError(t, err) + assert.Regexp(t, regexp.MustCompile(`"kty":\s*"EC"`), string(dst)) + assert.Regexp(t, regexp.MustCompile(`"kid":\s*"1"`), string(dst)) + assert.Regexp(t, regexp.MustCompile(`"x":\s*"AQAB"`), string(dst)) + assert.Regexp(t, regexp.MustCompile(`"y":\s*"AQAB"`), string(dst)) + assert.Regexp(t, regexp.MustCompile(`"d":\s*"AQAB"`), string(dst)) +} + +func TestOct_UnmarshalJSON(t *testing.T) { + // Setup + const input = ` +{ + "kty": "oct", + "k": "AQAB", + "kid": "1" +} +` + // Act + var oct OctSecretKey + err := json.Unmarshal([]byte(input), &oct) + + // Assert + assert.NoError(t, err) + assert.Equal(t, KtyOct, oct.Kty()) + assert.Equal(t, "1", oct.Kid()) + assert.Equal(t, []byte{1, 0, 1}, oct.K.Bytes()) + +} + +func TestOct_MarshalJSON(t *testing.T) { + // Setup + var oct OctSecretKey + oct.SetKid("1") + oct.K.SetBytes([]byte{1, 0, 1}) + + // Act + dst, err := json.Marshal(&oct) + + // Assert + assert.NoError(t, err) + assert.Regexp(t, regexp.MustCompile(`"kty":\s*"oct"`), string(dst)) + assert.Regexp(t, regexp.MustCompile(`"kid":\s*"1"`), string(dst)) + assert.Regexp(t, regexp.MustCompile(`"k":\s*"AQAB"`), string(dst)) +} + +func Test_Unmarshal(t *testing.T) { + testCase := []struct { + expected reflect.Type + encoded string + }{ + { + expected: reflect.TypeOf(&PublicRsaKey{}), + encoded: ` +{ + "kty": "RSA", + "n": "BBBB", + "e": "AQAB", + "kid": "1" +}`, + }, + { + expected: reflect.TypeOf(&PublicEcKey{}), + encoded: ` +{ + "kty": "EC", + "crv": "P-256", + "x": "BBBB", + "y": "AQAB", + "kid": "1" +}`, + }, + { + expected: reflect.TypeOf(&OctSecretKey{}), + encoded: ` +{ + "kty": "oct", + "k": "AQAB", + "kid": "1" +}`, + }, + } + + for _, test := range testCase { + // Act + k, e := UnmarshalJwk(bytes.NewReader([]byte(test.encoded))) + + // Assert + assert.NoError(t, e) + assert.Equal(t, test.expected, reflect.TypeOf(k)) + } +} + +func TestJwkFields_CheckConsistency(t *testing.T) { + // Setup + testCases := []struct { + field jwkFields + expected error + }{ + // Duplicate KeyOps + { + field: jwkFields{ + KeyOps: []KeyOps{KeyOpsDecrypt, KeyOpsDecrypt}, + }, + expected: ErrDuplicateKeyOps, + }, + // Invalid SHA1 certificate thumbprint + { + field: jwkFields{ + KeyX5C: []Certificate{ + { + Certificate: &x509.Certificate{}, + }, + }, + KeyX5T: &Fingerprint{ + digest: []byte("invalid"), + }, + }, + expected: ErrJwkInconsistentCertificateFields, + }, + } + + // Act/Assert + for i, test := range testCases { + t.Run(fmt.Sprintf("%d", i+1), + func(t *testing.T) { + err := test.field.CheckConsistency() + assert.Equal(t, test.expected, err) + }) + } + +} + +func TestJwkFields_X5C(t *testing.T) { + + testCases := []struct { + certs []*x509.Certificate + }{ + { + certs: nil, + }, + { + certs: []*x509.Certificate{}, + }, + { + certs: []*x509.Certificate{ + &x509.Certificate{ + Subject: pkix.Name{ + SerialNumber: "1", + }, + }, + }, + }, + { + certs: []*x509.Certificate{ + &x509.Certificate{ + Subject: pkix.Name{ + SerialNumber: "2", + }, + }, + &x509.Certificate{ + Subject: pkix.Name{ + SerialNumber: "3", + }, + }, + }, + }, + } + for i, test := range testCases { + t.Run(fmt.Sprintf("%v", i+1), func(t *testing.T) { + j := &jwkFields{} + j.SetX5C(test.certs) + certs := j.X5C() + require.Equal(t, len(test.certs), len(certs)) + for i := range test.certs { + assert.True(t, reflect.DeepEqual(test.certs[i], certs[i])) + } + }) + } +} diff --git a/jose/jwks.go b/jose/jwks.go new file mode 100644 index 0000000..f911a6d --- /dev/null +++ b/jose/jwks.go @@ -0,0 +1,50 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package jose + +import ( + "bytes" + "encoding/json" +) + +//Jwks key store +type Jwks struct { + Keys []Jwk `json:"keys"` +} + +//UnmarshalJSON byte slice into key store, or error +func (j *Jwks) UnmarshalJSON(data []byte) error { + var unmarshalTo struct { + Keys []json.RawMessage `json:"keys"` + } + if err := json.Unmarshal(data, &unmarshalTo); err != nil { + return err + } + for _, blob := range unmarshalTo.Keys { + jwk, err := UnmarshalJwk(bytes.NewReader(blob)) + if err != nil { + return err + } + j.Keys = append(j.Keys, jwk) + } + return nil +} diff --git a/jose/jwks_test.go b/jose/jwks_test.go new file mode 100644 index 0000000..17084bb --- /dev/null +++ b/jose/jwks_test.go @@ -0,0 +1,89 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package jose + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestJwks_UnmarshalJSON(t *testing.T) { + // Setup + const input = `{ + "keys": [ + { + "kty": "RSA", + "n": "BBBB", + "e": "AQAB", + "kid": "1" + }, + { + "kty": "RSA", + "n": "BBBB", + "e": "AQAB", + "kid": "2" + }] +} +` + var jwks Jwks + + // Act + err := json.Unmarshal([]byte(input), &jwks) + + // Assert + + assert.NoError(t, err) + require.Len(t, jwks.Keys, 2) + assert.Equal(t, "1", jwks.Keys[0].Kid()) + assert.Equal(t, "2", jwks.Keys[1].Kid()) +} + +func TestJwks_MarshalJSON(t *testing.T) { + // Setup + var rsa PrivateRsaKey + rsa.SetKid("1") + rsa.N.SetBytes([]byte{0, 1, 2, 3, 4}) + rsa.D.SetBytes([]byte{0, 1, 2, 3, 4}) + rsa.P.SetBytes([]byte{0, 1, 2, 3, 4}) + rsa.Q.SetBytes([]byte{0, 1, 2, 3, 4}) + rsa.Dp.SetBytes([]byte{0, 1, 2, 3, 4}) + rsa.Dq.SetBytes([]byte{0, 1, 2, 3, 4}) + rsa.Qi.SetBytes([]byte{0, 1, 2, 3, 4}) + rsa.E.SetBytes([]byte{1, 0, 1}) + + jwks := Jwks{ + Keys: []Jwk{ + &rsa, + &rsa, + }, + } + + // Act + marshalled, err := json.Marshal(&jwks) + + // Assert + assert.NoError(t, err) + assert.NotEmpty(t, marshalled) +} diff --git a/jose/jws.go b/jose/jws.go new file mode 100644 index 0000000..1c78a26 --- /dev/null +++ b/jose/jws.go @@ -0,0 +1,150 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package jose + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "strings" +) + +//JwsHeader header for JWS +type JwsHeader struct { + Alg Alg `json:"alg"` + Jku string `json:"jku,omitempty"` + //jwkFields []jwkFields `json:"jwk,omitempty"` TODO finish this + Kid string `json:"kid,omitempty"` + X5U string `json:"x5u,omitempty"` + X5C [][]byte `json:"x5c,omitempty"` + X5T *Blob `json:"x5t,omitempty"` + X5T256 *Blob `json:"x5t#S256,omitempty"` + Typ JwsType `json:"typ,omitempty"` + Cty JwsType `json:"cty,omitempty"` + Crit []string `json:"crit,omitempty"` +} + +//Audiences holds audience members +type Audiences struct { + Aud []string +} + +//UnmarshalJSON byte slice to audience members or error +func (audiences *Audiences) UnmarshalJSON(src []byte) (err error) { + var toUnmarshal interface{} + if err = json.Unmarshal(src, &toUnmarshal); err != nil { + return + } + switch t := toUnmarshal.(type) { + case string: + audiences.Aud = append(audiences.Aud, t) + case []interface{}: + for _, item := range t { + str, ok := item.(string) + if !ok { + err = ErrJSONFormat + return + } + audiences.Aud = append(audiences.Aud, str) + } + default: + err = ErrJSONFormat + } + return +} + +//MarshalJSON audience to byte slice or error +func (audiences *Audiences) MarshalJSON() (dst []byte, err error) { + switch len(audiences.Aud) { + case 1: + // Special case. + return json.Marshal(audiences.Aud[0]) + default: + return json.Marshal(audiences.Aud) + } +} + +//Jws jave web signature +type Jws struct { + Header *JwsHeader + Payload interface{} + Signature []byte +} + +// MarshalBody marshaled representation of the JWT Header and Claims. +func (jws *Jws) MarshalBody() (body string, err error) { + if jws.Header.Typ != JwtType { + /* Not a JWT. */ + err = ErrJwtFormat + return + } + if jws.Header.Cty != "" && jws.Header.Cty != JwtType { + err = ErrJwtFormat + return + } + var header []byte + if header, err = json.Marshal(&jws.Header); err != nil { + return + } + var claims []byte + if claims, err = json.Marshal(&jws.Payload); err != nil { + return + } + body = fmt.Sprintf("%s.%s", + base64.RawURLEncoding.EncodeToString(header), + base64.RawURLEncoding.EncodeToString(claims)) + return +} + +//Body return either the original JWS payload or alternatively one generated. +func (jws *Jws) Body() (body string, err error) { + body, err = jws.MarshalBody() + return +} + +//Unmarshal to body string, or error +func (jws *Jws) Unmarshal(src string) (body string, err error) { + /* Compact JWS encoding. */ + parts := strings.Split(src, ".") + if len(parts) != 3 { + err = ErrJwtFormat + return + } + if err = unmarshalURLBase64(parts[0], &(jws.Header)); err != nil { + return + } + if err = unmarshalURLBase64(parts[1], &(jws.Payload)); err != nil { + return + } + + if jws.Signature, err = base64.RawURLEncoding.DecodeString(parts[2]); err != nil { + err = ErrJwtFormat + return + } + body = strings.Join(parts[:2], ".") + return +} + +//MarshalJws body and signature to a string +func MarshalJws(body string, signature []byte) string { + return fmt.Sprintf("%s.%s", body, base64.RawURLEncoding.EncodeToString(signature)) +} diff --git a/jose/jws_test.go b/jose/jws_test.go new file mode 100644 index 0000000..45f6db7 --- /dev/null +++ b/jose/jws_test.go @@ -0,0 +1,110 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package jose + +import ( + "encoding/json" + "regexp" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAudiences_MarshalJSON(t *testing.T) { + // Setup + testCases := []struct { + audiences []string + expected string + }{ + { + audiences: []string{"one"}, + expected: `"one"`, + }, + { + audiences: []string{}, + expected: `\[\]`, + }, + { + audiences: []string{"one", "two"}, + expected: `^\[\s*"one",\s*"two"\s*\]?`, + }, + { + audiences: []string{}, + expected: `^\[\]?`, + }, + } + + // Act/Assert + for _, test := range testCases { + aud := Audiences{ + Aud: test.audiences, + } + result, err := json.Marshal(&aud) + assert.NoError(t, err) + assert.Regexp(t, regexp.MustCompile(test.expected), string(result)) + } +} + +func TestAudiences_UnmarshalJSON(t *testing.T) { + // Setup + testCases := []struct { + input string + expected []string + err error + }{ + { + input: `"one"`, + expected: []string{"one"}, + err: nil, + }, + { + input: `[]`, + expected: nil, + err: nil, + }, + { + input: `["one"]`, + expected: []string{"one"}, + err: nil, + }, + { + input: `["one", "two"]`, + expected: []string{"one", "two"}, + err: nil, + }, + { + input: `[1, "one"]`, + expected: nil, + err: ErrJSONFormat, + }, + } + + // Act/Assert + for _, test := range testCases { + var aud Audiences + err := aud.UnmarshalJSON([]byte(test.input)) + assert.Equal(t, test.err, err) + assert.Len(t, aud.Aud, len(test.expected)) + assert.Equal(t, test.expected, aud.Aud) + } + +} diff --git a/jose/jwt.go b/jose/jwt.go new file mode 100644 index 0000000..1f62db4 --- /dev/null +++ b/jose/jwt.go @@ -0,0 +1,237 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package jose + +import ( + "encoding/json" + "fmt" + "math" + "reflect" + "strings" + + "github.com/sirupsen/logrus" +) + +// Standard JWT claim names that cannot be used as untyped keys. +var reservedJwtClaims = map[string]bool{ + "iss": true, + "iat": true, + "jti": true, + "sub": true, + "aud": true, + "exp": true, + "nbf": true, +} + +// AutomaticJwtClaims represent standard JWT claims that should not generally be set by a caller. +// For example the iat (issued-at) claim should only be set by a signer not the caller who requests +// the JWT. +type AutomaticJwtClaims struct { + Issuer string `json:"iss,omitempty"` + IssuedAt int64 `json:"iat,omitempty"` + JwtID string `json:"jti,omitempty"` +} + +// SettableJwtClaims are claims generally requested by a caller and not a signer. +type SettableJwtClaims struct { + Subject string `json:"sub,omitempty"` + Audiences Audiences `json:"aud,omitempty"` + Expiration int64 `json:"exp,omitempty"` + NotBefore int64 `json:"nbf,omitempty"` +} + +//UntypedClaims for non-standard clains +type UntypedClaims map[string]json.RawMessage + +//JwtClaims claims for a JWT +type JwtClaims struct { + AutomaticJwtClaims + SettableJwtClaims + UntypedClaims +} + +// unmarshalClaims unmarshal typed claims into the struct |into| +func unmarshalTypedClaims(claims map[string]json.RawMessage, into interface{}) (err error) { + /* Loop through known claims extracting values as appropriate. */ + auto := reflect.TypeOf(into).Elem() + for i := 0; i < auto.NumField(); i++ { + field := reflect.ValueOf(into).Elem().Field(i) + // TODO: write a proper json tag parser when needed. All our known claims have a consistent struct tag format. + if tag, exists := auto.Field(i).Tag.Lookup("json"); exists { + values := strings.Split(tag, ",") + if len(values) < 1 { + // should never happen + logrus.Fatal("Broken json struct tag") + } + name := values[0] + if value, exists := claims[name]; exists { + if !field.CanAddr() { + // should never happen + logrus.Fatal("Broken json struct type, must be addressable") + } + if err = json.Unmarshal(value, field.Addr().Interface()); err != nil { + return + } + // Remove claim we have consumed. + delete(claims, name) + } + } + } + return + +} + +// UnmarshalJSON implements json.Unmarshaler interface method. +func (c *JwtClaims) UnmarshalJSON(data []byte) (err error) { + claims := make(map[string]json.RawMessage) + if err = json.Unmarshal(data, &claims); err != nil { + return + } + /* Decode typed claims. */ + if err = unmarshalTypedClaims(claims, &c.AutomaticJwtClaims); err != nil { + return + } + if err = unmarshalTypedClaims(claims, &c.SettableJwtClaims); err != nil { + return + } + // All remaining claims + c.UntypedClaims = claims + return +} + +// UnmarshalCustomClaim Unmarshals a custom claim. A Claim that do not exist is unset but no error is returned. +func (c *JwtClaims) UnmarshalCustomClaim(name string, claim interface{}) error { + targetClaim, ok := c.UntypedClaims[name] + if ok { + if err := json.Unmarshal(targetClaim, claim); err != nil { + return err + } + } + return nil +} + +// MarshalJSON implements json.Marshaler interface method. +func (c *JwtClaims) MarshalJSON() (dst []byte, err error) { + // Temporary type and instance. Note the use of references. + output := struct { + *AutomaticJwtClaims + *SettableJwtClaims + }{ + AutomaticJwtClaims: &c.AutomaticJwtClaims, + SettableJwtClaims: &c.SettableJwtClaims, + } + + // Dynamically generate a struct with typed and untyped fields for marshalling. + + // Copy struct fields from our temporary type + fields := make([]reflect.StructField, 0, reflect.TypeOf(output).NumField()) + for i := 0; i < reflect.TypeOf(output).NumField(); i++ { + fields = append(fields, reflect.TypeOf(output).Field(i)) + } + // Create struct fields for each untyped entry. + for k := range c.UntypedClaims { + // Validate untyped fields do not clash with standard JWT claims. + if _, invalid := reservedJwtClaims[k]; invalid { + err = ErrJwkReservedClaimName + return + } + field := reflect.StructField{ + Name: fmt.Sprintf("A%s", k), // Add the "A" to make sure the field is exported. + Type: reflect.TypeOf(json.RawMessage{}), + Tag: reflect.StructTag(fmt.Sprintf("json:\"%s\"", k)), // Fix the field. + Index: []int{len(fields)}, + Anonymous: false, + } + fields = append(fields, field) + } + // Create instance of our new dynamic type. + typ := reflect.StructOf(fields) + inst := reflect.New(typ) + // Copy the values from our typed fields. + for i := 0; i < reflect.TypeOf(output).NumField(); i++ { + inst.Elem().FieldByName(reflect.TypeOf(output).Field(i).Name).Set(reflect.ValueOf(output).Field(i)) + } + // Copy the values from our untyped fields. + for k, v := range c.UntypedClaims { + inst.Elem().FieldByName(fmt.Sprintf("A%s", k)).Set(reflect.ValueOf(v)) + } + return json.Marshal(inst.Interface()) +} + +//Jwt defines a Jave web token +type Jwt struct { + Header JwsHeader + Claims JwtClaims + Signature []byte +} + +//Verify JWT is valid or error +func (jwt *Jwt) Verify() error { + if jwt.Header.Typ != JwtType { + /* Not a JWT. */ + return ErrJwtFormat + } + if jwt.Header.Cty != "" && jwt.Header.Cty != JwtType { + return ErrJwtFormat + + } + for k := range jwt.Claims.UntypedClaims { + if _, invalid := reservedJwtClaims[k]; invalid { + return ErrJwkReservedClaimName + } + } + return nil +} + +//MarshalBody representation of the JWT Header and Claims. +func (jwt *Jwt) MarshalBody() (body string, err error) { + if err = jwt.Verify(); err != nil { + return + } + jws := Jws{ + Header: &jwt.Header, + Payload: &jwt.Claims, + } + return jws.MarshalBody() +} + +//Unmarshal string to JWT body, or error +func (jwt *Jwt) Unmarshal(src string) (body string, err error) { + /* Compact JWT encoding. */ + /* Default Exp field to maximum in case it is not set. */ + jwt.Claims.Expiration = math.MaxInt64 + jws := Jws{ + Header: &jwt.Header, + Payload: &jwt.Claims, + } + if body, err = jws.Unmarshal(src); err != nil { + return + } + // Ick some copying here but do we really care? + jwt.Signature = make([]byte, len(jws.Signature)) + _ = copy(jwt.Signature, jws.Signature) + if err = jwt.Verify(); err != nil { + body = "" + return + } + return +} diff --git a/jose/jwt_test.go b/jose/jwt_test.go new file mode 100644 index 0000000..40ea4e5 --- /dev/null +++ b/jose/jwt_test.go @@ -0,0 +1,336 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package jose + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestJwt_Verify(t *testing.T) { + testCases := []struct { + jwt Jwt + expected error + }{ + // Invalid header typ field. + { + jwt: Jwt{ + Header: JwsHeader{ + Typ: "invalid", + }, + }, + + expected: ErrJwtFormat, + }, + // Invalid header cty field. + { + jwt: Jwt{ + Header: JwsHeader{ + Typ: JwtType, + Cty: "invalid", + }, + }, + + expected: ErrJwtFormat, + }, + // Invalid untyped claims name. + { + jwt: Jwt{ + Header: JwsHeader{ + Typ: JwtType, + }, + Claims: JwtClaims{ + UntypedClaims: UntypedClaims{ + "sub": json.RawMessage{}, + }, + }, + }, + expected: ErrJwkReservedClaimName, + }, + // Happy day scenario. + { + jwt: Jwt{ + Header: JwsHeader{ + Typ: JwtType, + }, + Claims: JwtClaims{ + UntypedClaims: UntypedClaims{ + "name": json.RawMessage("John Doe"), + }, + }, + }, + expected: nil, + }, + } + + // Act/Assert + for i, test := range testCases { + t.Run(fmt.Sprintf("%d", i+1), + func(t *testing.T) { + err := test.jwt.Verify() + assert.Equal(t, test.expected, err) + }) + } +} + +func TestJwt_MarshalBody(t *testing.T) { + // Setup + testCases := []struct { + jwt Jwt + err error + }{ + // Invalid 'typ' field case + { + jwt: Jwt{ + Header: JwsHeader{ + Typ: "Wrong", + }, + }, + err: ErrJwtFormat, + }, + // Invalid 'cty' field case + { + jwt: Jwt{ + Header: JwsHeader{ + Typ: JwtType, + Cty: "Wrong", + }, + }, + err: ErrJwtFormat, + }, + // Happy days scenarios + { + jwt: Jwt{ + Header: JwsHeader{ + Typ: JwtType, + }, + Claims: JwtClaims{ + AutomaticJwtClaims: AutomaticJwtClaims{ + Issuer: "test", + IssuedAt: 123456789, + JwtID: "123456789", + }, + SettableJwtClaims: SettableJwtClaims{ + Audiences: Audiences{ + Aud: []string{"aud1"}, + }, + }, + UntypedClaims: UntypedClaims{ + "name": json.RawMessage(`"John Doe"`), + }, + }, + }, + err: nil, + }, + { + jwt: Jwt{ + Header: JwsHeader{ + Typ: JwtType, + Cty: JwtType, + }, + Claims: JwtClaims{ + AutomaticJwtClaims: AutomaticJwtClaims{ + Issuer: "test", + IssuedAt: 123456789, + JwtID: "123456789", + }, + SettableJwtClaims: SettableJwtClaims{ + Audiences: Audiences{ + Aud: []string{"aud1"}, + }, + }, + UntypedClaims: UntypedClaims{ + "name": json.RawMessage(`"John Doe"`), + }, + }, + }, + err: nil, + }, + } + + // Act/Assert + for i, test := range testCases { + t.Run(fmt.Sprintf("%d", i+1), + func(t *testing.T) { + dest, err := test.jwt.MarshalBody() + assert.Equal(t, test.err, err) + if test.err == nil { + assert.Regexp(t, "^[A-Za-z0-9_-]+.[A-Za-z0-9_-]+$", dest) + } else { + assert.Empty(t, dest) + } + }) + } +} + +func TestJwt_Unmarshal(t *testing.T) { + // Note test JWT serializations generated at https://jwt.io/#debugger + // Setup + testCases := []struct { + Input string + Expected Jwt + }{ + { + Input: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwiaXNzIjoiaXNzdWVyMSIsImV4cCI6MTUxNjIzOTA0MCwibmJmIjoxNTE2MjM5MDM4LCJhdWQiOiJvbmUifQ.TLHUIM0WqqIyHnai0Dy-EtJYX13WOXuWxYrd1A7T2V9cDGfqVlxddLzG0hAZJ9MvYfkoJsW0bQHey_qQNGN5hUluysHc68jtEaSgZqPqeZe64M3a7wVmbeNc6wMAVH_KX48ohTUDZ1tVC53hAdoph87JG6GRxTVvN6Fvk6bLbq8", + Expected: Jwt{ + Header: JwsHeader{ + Alg: AlgRS256, + Typ: JwtType, + }, + Claims: JwtClaims{ + AutomaticJwtClaims: AutomaticJwtClaims{ + Issuer: "issuer1", + IssuedAt: 1516239022, + }, + SettableJwtClaims: SettableJwtClaims{ + Subject: "1234567890", + NotBefore: 1516239038, + Expiration: 1516239040, + Audiences: Audiences{ + Aud: []string{"one"}, + }, + }, + UntypedClaims: UntypedClaims{ + "name": json.RawMessage(`"John Doe"`), + "admin": json.RawMessage("true"), + }, + }, + }, + }, + { + Input: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwiaXNzIjoiaXNzdWVyMSIsImV4cCI6MTUxNjIzOTA0MCwibmJmIjoxNTE2MjM5MDM4LCJhdWQiOlsib25lIiwidHdvIl19.Og8U8-Oq1zwZwlgJ69tAMMj_F0VlUKJJxv25mRsQn-zHgdpt1besO7sJGDyNN6hS60S35RP3J1c5klVNbLipALegfiYk7gdbghXu9AJ_2GdUCjokyouslMKH5fOIbgDIyQZy20VGEIexUohyZ3rVv_8Ql8PISKZn6fVQv64FucU", + Expected: Jwt{ + Header: JwsHeader{ + Alg: AlgRS256, + Typ: JwtType, + }, + Claims: JwtClaims{ + AutomaticJwtClaims: AutomaticJwtClaims{ + Issuer: "issuer1", + IssuedAt: 1516239022, + }, + SettableJwtClaims: SettableJwtClaims{ + Subject: "1234567890", + NotBefore: 1516239038, + Expiration: 1516239040, + Audiences: Audiences{ + Aud: []string{"one", "two"}, + }, + }, + UntypedClaims: UntypedClaims{ + "name": json.RawMessage(`"John Doe"`), + "admin": json.RawMessage("true"), + }, + }, + }, + }, + } + + // Act/assert + for i, test := range testCases { + t.Run(fmt.Sprintf("%d", i+1), + func(t *testing.T) { + var jwt Jwt + _, err := jwt.Unmarshal(test.Input) + require.NoError(t, err) + assert.Equal(t, test.Expected.Header.Alg, jwt.Header.Alg) + assert.Equal(t, test.Expected.Header.Typ, jwt.Header.Typ) + assert.Equal(t, test.Expected.Claims.AutomaticJwtClaims.IssuedAt, jwt.Claims.AutomaticJwtClaims.IssuedAt) + assert.Equal(t, test.Expected.Claims.AutomaticJwtClaims.Issuer, jwt.Claims.AutomaticJwtClaims.Issuer) + assert.Equal(t, test.Expected.Claims.SettableJwtClaims.Subject, jwt.Claims.SettableJwtClaims.Subject) + require.Equal(t, len(test.Expected.Claims.SettableJwtClaims.Audiences.Aud), len(jwt.Claims.SettableJwtClaims.Audiences.Aud)) + for j := range test.Expected.Claims.SettableJwtClaims.Audiences.Aud { + assert.Equal(t, test.Expected.Claims.SettableJwtClaims.Audiences.Aud[j], jwt.Claims.SettableJwtClaims.Audiences.Aud[j]) + } + assert.Equal(t, test.Expected.Claims.SettableJwtClaims.Expiration, jwt.Claims.SettableJwtClaims.Expiration) + assert.Equal(t, test.Expected.Claims.SettableJwtClaims.NotBefore, jwt.Claims.SettableJwtClaims.NotBefore) + + assert.Equal(t, len(test.Expected.Claims.UntypedClaims), len(jwt.Claims.UntypedClaims)) + for k, expected := range test.Expected.Claims.UntypedClaims { + got, exists := jwt.Claims.UntypedClaims[k] + require.True(t, exists) + assert.Equal(t, expected, got) + } + }) + } +} + +func TestJwt_Roundtrip(t *testing.T) { + // Setup + expected := Jwt{ + Header: JwsHeader{ + Alg: AlgRS256, + Typ: JwtType, + }, + Claims: JwtClaims{ + AutomaticJwtClaims: AutomaticJwtClaims{ + Issuer: "issuer1", + IssuedAt: 1516239022, + }, + SettableJwtClaims: SettableJwtClaims{ + Subject: "1234567890", + NotBefore: 1516239038, + Expiration: 1516239040, + Audiences: Audiences{ + Aud: []string{"one"}, + }, + }, + UntypedClaims: UntypedClaims{ + "name": json.RawMessage(`"John Doe"`), + "admin": json.RawMessage("true"), + }, + }, + Signature: []byte("123455"), + } + + // Act + body, err := expected.MarshalBody() + require.NoError(t, err) + marhsalled := MarshalJws(body, expected.Signature) + var unmarshalled Jwt + _, err = unmarshalled.Unmarshal(string(marhsalled)) + require.NoError(t, err) + + // Assert + assert.Equal(t, expected, unmarshalled) + +} + +func TestJwtClaims_UnmarshalCustomClaim(t *testing.T) { + claims := JwtClaims{ + UntypedClaims: UntypedClaims{ + "name": json.RawMessage([]byte("1")), + }, + } + + var stringName string + err := claims.UnmarshalCustomClaim("name", &stringName) + assert.Error(t, err) + var intName int + err = claims.UnmarshalCustomClaim("name", &intName) + require.NoError(t, err) + assert.Equal(t, 1, intName) +} diff --git a/jose/types.go b/jose/types.go new file mode 100644 index 0000000..098531a --- /dev/null +++ b/jose/types.go @@ -0,0 +1,281 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package jose + +import ( + "encoding/base64" + "encoding/json" + "errors" + "math/big" +) + +// Alg is a type for representing values destined for `alg` fields in JWK and JWTs. +type Alg string + +// Crv is a type for representing values destined for `crv` fields in JWKs. +type Crv string + +// Kty is a type for representing values destined for `kty` fields in JWKs. +type Kty string + +// KeyUse is a type for representing values destined for `use` fields in JWKs. +type KeyUse string + +// KeyOps is a type for representing values destined for `key_ops` fields in JWKs. +type KeyOps string + +// JwsType is a type for representing values destined for `typ` fields in JWS and JWTs. +type JwsType string + +// Enc is a type representing values destined for the `enc` field in a JWE header. +type Enc string + +// Zip is a type representing values destined for the `zip` field in a JWE header. +type Zip string + +const ( + // Supported Algorithms + + //AlgRS256 RSA PKCS #1 and SHA-2 256 + AlgRS256 Alg = "RS256" + //AlgRS384 RSA PKCS #1 and SHA-2 384 + AlgRS384 Alg = "RS384" + //AlgRS512 RSA PKCS #1 and SHA-2 512 + AlgRS512 Alg = "RS512" + //AlgPS256 RSA PSS signature with SHA-2 256 + AlgPS256 Alg = "PS256" + //AlgPS384 RSA PSS signature with SHA-2 384 + AlgPS384 Alg = "PS384" + //AlgPS512 RSA PSS signature with SHA-2 512 + AlgPS512 Alg = "PS512" + //AlgES256 EC DSA signature with SHA-2 256 + AlgES256 Alg = "ES256" + //AlgES384 EC DSA signature with SHA-2 384 + AlgES384 Alg = "ES384" + //AlgES512 EC DSA signature with SHA-2 512 + AlgES512 Alg = "ES512" + //AlgA128GCM AES GCM using 128-bit key + AlgA128GCM Alg = "A128GCM" + //AlgA192GCM AES GCM using 192-bit key + AlgA192GCM Alg = "A192GCM" + //AlgA256GCM AES GCM using 256-bit key + AlgA256GCM Alg = "A256GCM" + // AlgDir direct encryption for use with JWEs + AlgDir Alg = "dir" + + //CrvP256 NIST P-256 + CrvP256 Crv = "P-256" + //CrvP384 NIST P-384 + CrvP384 Crv = "P-384" + //CrvP521 NIST P-521 + CrvP521 Crv = "P-521" + + // Key Types + + //KtyRSA RSA key type + KtyRSA Kty = "RSA" + //KtyEC Elliptical Curve key type + KtyEC Kty = "EC" + //KtyOct Octet key type + KtyOct Kty = "oct" + + //KeyUseEnc encryption usage + KeyUseEnc KeyUse = "enc" + //KeyUseSig signing usage + KeyUseSig KeyUse = "sig" + + //Key Operations - Standard + + //KeyOpsSign sign stuff + KeyOpsSign KeyOps = "sign" + //KeyOpsVerify verify signed stuff + KeyOpsVerify KeyOps = "verify" + //KeyOpsEncrypt encrypt stuff + KeyOpsEncrypt KeyOps = "encrypt" + //KeyOpsDecrypt decrypt stuff + KeyOpsDecrypt KeyOps = "decrypt" + //KeyOpsWrapKey wrap keys + KeyOpsWrapKey KeyOps = "wrapKey" + //KeyOpsUnwrapKey unwrap keys + KeyOpsUnwrapKey KeyOps = "unwrapKey" + //KeyOpsDeriveKey derive a key + KeyOpsDeriveKey KeyOps = "deriveKey" + //KeyOpsDeriveBits derive bits + KeyOpsDeriveBits KeyOps = "deriveBits" + + //JwtType JWT type + JwtType JwsType = "JWT" + + // EncA128GCM AES GCM 128 Enc type + EncA128GCM Enc = "A128GCM" + // EncA192GCM AES GCM 192 Enc type + EncA192GCM Enc = "A192GCM" + // EncA256GCM AES GCM 256 Enc type + EncA256GCM Enc = "A256GCM" + + // DeflateZip deflate type + DeflateZip Zip = "DEF" +) + +var ( + //ErrJSONFormat when bad JSON string provided + ErrJSONFormat = errors.New("invalid JSON format") + //ErrBlobEmpty when bad Blob provided + ErrBlobEmpty = errors.New("invalid Blob format, may not be empty") + //ErrUnsupportedKeyType when a key type is unknown/unsupported + ErrUnsupportedKeyType = errors.New("unsupported key type") + //ErrUnexpectedKeyType when a key shows up in the wrong place. + ErrUnexpectedKeyType = errors.New("unexpected key type") + //ErrJwtFormat when a JWT isn't formatted correctly + ErrJwtFormat = errors.New("invalid JWT format") + //ErrDuplicateKeyOps too many of the same operation requested + ErrDuplicateKeyOps = errors.New("duplicate key_ops entries") + //ErrJwkInconsistentCertificateFields when a certificates fields are not what was expected + ErrJwkInconsistentCertificateFields = errors.New("inconsistent certificate fields") + //ErrJwkInvalidFingerprintfomat the fingerprint field (x5t) is encoded in an incorrect format + ErrJwkInvalidFingerprintfomat = errors.New("invalid fingerprint format") + + //ErrJwkReservedClaimName invalid use of a reserved/defined claim name + ErrJwkReservedClaimName = errors.New("incorrect use of reserved claim name") + + //ErrJweFormat when a JWE isn't formatted correctly + ErrJweFormat = errors.New("invalid JWE format") +) + +func unmarshalJSONBlob(src []byte, decoder *base64.Encoding) (dst []byte, err error) { + len := len(src) + // We always want at least 1 character pre and proceeded by a quote. + if len < 3 || src[0] != '"' || src[len-1] != '"' { + err = ErrBlobEmpty + return + } + // Allocate (possibly over allocate) our dst buffer. + dstLen := decoder.DecodedLen(len - 2) + tmp := make([]byte, dstLen) + var decoded int + if decoded, err = decoder.Decode(tmp, src[1:len-1]); err != nil { + return + } + // Only return the exact length buffer + dst = tmp[:decoded] + return +} + +func marshalJSONBlob(src []byte, encoder *base64.Encoding) (dst []byte, err error) { + + if len(src) == 0 { + err = ErrBlobEmpty + return + } + + len := encoder.EncodedLen(len(src)) + 2 + dst = make([]byte, len) + dst[0] = '"' + dst[len-1] = '"' + encoder.Encode(dst[1:len-1], src) + return +} + +//BigNum for managing big.Int +type BigNum struct { + b big.Int +} + +//SetBytes of BigNum +func (b *BigNum) SetBytes(val []byte) *BigNum { + b.b.SetBytes(val) + return b +} + +//Set bigNum with bit.Int +func (b *BigNum) Set(val *big.Int) *BigNum { + b.b.SetBytes(val.Bytes()) + return b +} + +//Int as big.Int +func (b *BigNum) Int() *big.Int { + return &b.b +} + +//Empty out BigNum +func (b *BigNum) Empty() bool { + return b.b.BitLen() == 0 +} + +//MarshalJSON as byte slice or error +func (b *BigNum) MarshalJSON() (dst []byte, err error) { + dst, err = marshalJSONBlob(b.b.Bytes(), base64.RawURLEncoding) + return +} + +//UnmarshalJSON byte slice or error +func (b *BigNum) UnmarshalJSON(src []byte) (err error) { + var dst []byte + if dst, err = unmarshalJSONBlob(src, base64.RawURLEncoding); err != nil { + return + } + b.SetBytes(dst) + return +} + +// Blob represents a url-safe base64 encoded byte block. +type Blob struct { + B []byte +} + +//Bytes of blob in byte slice +func (b *Blob) Bytes() []byte { + return b.B +} + +//UnmarshalJSON byte slice to Blob, or error +func (b *Blob) UnmarshalJSON(src []byte) error { + var err error + b.B, err = unmarshalJSONBlob(src, base64.RawURLEncoding) + return err +} + +//MarshalJSON blob to byte slice +func (b *Blob) MarshalJSON() (dst []byte, err error) { + dst, err = marshalJSONBlob(b.B, base64.RawURLEncoding) + return +} + +//SetBytes of blob +func (b *Blob) SetBytes(val []byte) *Blob { + b.B = val + return b +} + +func unmarshalURLBase64(data string, inst interface{}) error { + var raw []byte + var err error + if raw, err = base64.RawURLEncoding.DecodeString(data); err != nil { + return err + } + + if err = json.Unmarshal(raw, inst); err != nil { + return err + } + return nil +} diff --git a/jose/types_test.go b/jose/types_test.go new file mode 100644 index 0000000..e950e72 --- /dev/null +++ b/jose/types_test.go @@ -0,0 +1,70 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package jose + +import ( + "encoding/json" + "math/big" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBigNum_MarshalJSON(t *testing.T) { + // Setup + var val BigNum + val.SetBytes([]byte{1, 0, 1}) + + // Act + marshalled, err := json.Marshal(&val) + + // Assert + assert.NoError(t, err) + assert.Equal(t, `"AQAB"`, string(marshalled)) + + val.SetBytes([]byte{}) + marshalled, err = json.Marshal(&val) + + assert.Nil(t, marshalled) + assert.IsType(t, &json.MarshalerError{}, err) + assert.Equal(t, + "json: error calling MarshalJSON for type *jose.BigNum: invalid Blob format, may not be empty", + err.Error()) + +} + +func TestBigNum_UnmarshalJSON(t *testing.T) { + // Setup + var val BigNum + var expected big.Int + expected.SetInt64(65537) + + // Act + err := json.Unmarshal([]byte(`"AQAB"`), &val) + + // Assert + assert.NoError(t, err) + assert.True(t, expected.Cmp(val.Int()) == 0) + + // Act/Assert + assert.Equal(t, ErrBlobEmpty, json.Unmarshal([]byte(`""`), &val)) +} diff --git a/jwe_direct_decryptor.go b/jwe_direct_decryptor.go new file mode 100644 index 0000000..258e281 --- /dev/null +++ b/jwe_direct_decryptor.go @@ -0,0 +1,92 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import "github.com/thalesignite/gose/jose" + +var _ JweDecryptor = (*JweDirectDecryptorImpl)(nil) + +// JweDirectDecryptorImpl is a concrete implementation of the JweDirectDecryptor interface. +type JweDirectDecryptorImpl struct { + keystore map[string]AuthenticatedEncryptionKey +} + +// Decrypt and verify the given JWE returning both the plaintext and AAD. +func (decryptor *JweDirectDecryptorImpl) Decrypt(jwe string) (plaintext, aad []byte, err error) { + var jweStruct jose.Jwe + if err = jweStruct.Unmarshal(jwe); err != nil { + return + } + + // We do not support zip conpression + if jweStruct.Header.Zip != "" { + err = ErrZipCompressionNotSupported + return + } + + // If there's no key ID specified fail. + if len(jweStruct.Header.Kid) == 0 { + err = ErrInvalidKid + return + } + + var key AuthenticatedEncryptionKey + var exists bool + if key, exists = decryptor.keystore[jweStruct.Header.Kid]; !exists { + err = ErrUnknownKey + return + } + + enc, ok := algToEncMap[key.Algorithm()] + if !ok { + err = ErrInvalidEncryption + return + } + + // Check alg is as expected, it's a direct encryption. + if jweStruct.Header.Alg != jose.AlgDir || jweStruct.Header.Enc != enc { + err = ErrInvalidAlgorithm + return + } + + if plaintext, err = key.Open(jose.KeyOpsDecrypt, jweStruct.Iv, jweStruct.Ciphertext, jweStruct.MarshalledHeader, jweStruct.Tag); err != nil { + return + } + + if jweStruct.Header.OtherAad != nil { + aad = jweStruct.Header.OtherAad.Bytes() + } + + return +} + +// NewJweDirectDecryptorImpl create a new instance of a JweDirectDecryptorImpl. +func NewJweDirectDecryptorImpl(keys []AuthenticatedEncryptionKey) *JweDirectDecryptorImpl { + // Create map out of our list of keys. The map is keyed in Kid. + decryptor := &JweDirectDecryptorImpl{ + keystore: map[string]AuthenticatedEncryptionKey{}, + } + for _, key := range keys { + decryptor.keystore[key.Kid()] = key + } + return decryptor +} diff --git a/jwe_direct_decryptor_test.go b/jwe_direct_decryptor_test.go new file mode 100644 index 0000000..38b955b --- /dev/null +++ b/jwe_direct_decryptor_test.go @@ -0,0 +1,333 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/mock" + + "github.com/thalesignite/gose/jose" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewJweDirectDecryptorImpl(t *testing.T) { + keyMock := &authenticatedEncryptionKeyMock{} + keyMock.On("Kid").Return("unique").Once() + decryptor := NewJweDirectDecryptorImpl([]AuthenticatedEncryptionKey{keyMock}) + require.NotNil(t, decryptor) + assert.NotNil(t, decryptor.keystore) +} + +func TestJweDirectDecryptorImpl_Decrypt_InvalidJweFormat(t *testing.T) { + keyMock := &authenticatedEncryptionKeyMock{} + keyMock.On("Kid").Return("unique").Once() + decryptor := NewJweDirectDecryptorImpl([]AuthenticatedEncryptionKey{keyMock}) + require.NotNil(t, decryptor) + + pt, aad, err := decryptor.Decrypt("not a jwe") + + assert.Empty(t, pt) + assert.Empty(t, aad) + assert.Equal(t, jose.ErrJweFormat, err) +} + +func TestJweDirectDecryptorImpl_Decrypt_ZipCompressionNotSupport(t *testing.T) { + keyMock := &authenticatedEncryptionKeyMock{} + keyMock.On("Kid").Return("unique").Once() + decryptor := NewJweDirectDecryptorImpl([]AuthenticatedEncryptionKey{keyMock}) + require.NotNil(t, decryptor) + + fakeJwe := &jose.Jwe{ + Header: jose.JweHeader{ + JwsHeader: jose.JwsHeader{ + Alg: jose.AlgDir, + Kid: "", + }, + Enc: jose.EncA256GCM, + Zip: jose.DeflateZip, + }, + Iv: []byte("iv"), + Ciphertext: []byte("encrypted"), + Tag: []byte("tag"), + } + + err := fakeJwe.MarshalHeader() + require.NoError(t, err) + + marshalledJwe := fakeJwe.Marshal() + + pt, aad, err := decryptor.Decrypt(marshalledJwe) + + assert.Empty(t, pt) + assert.Empty(t, aad) + assert.Equal(t, ErrZipCompressionNotSupported, err) + + keyMock.AssertExpectations(t) +} + +func TestJweDirectDecryptorImpl_Decrypt_InvalidKeyId(t *testing.T) { + keyMock := &authenticatedEncryptionKeyMock{} + keyMock.On("Kid").Return("unique").Once() + decryptor := NewJweDirectDecryptorImpl([]AuthenticatedEncryptionKey{keyMock}) + require.NotNil(t, decryptor) + + fakeJwe := &jose.Jwe{ + Header: jose.JweHeader{ + JwsHeader: jose.JwsHeader{ + Alg: jose.AlgDir, + Kid: "", + }, + Enc: jose.EncA256GCM, + }, + Iv: []byte("iv"), + Ciphertext: []byte("encrypted"), + Tag: []byte("tag"), + } + + err := fakeJwe.MarshalHeader() + require.NoError(t, err) + + marshalledJwe := fakeJwe.Marshal() + + pt, aad, err := decryptor.Decrypt(marshalledJwe) + + assert.Empty(t, pt) + assert.Empty(t, aad) + assert.Equal(t, ErrInvalidKid, err) + + keyMock.AssertExpectations(t) +} + +func TestJweDirectDecryptorImpl_Decrypt_UnknownKeyId(t *testing.T) { + keyMock := &authenticatedEncryptionKeyMock{} + keyMock.On("Kid").Return("unique").Once() + decryptor := NewJweDirectDecryptorImpl([]AuthenticatedEncryptionKey{keyMock}) + require.NotNil(t, decryptor) + + fakeJwe := &jose.Jwe{ + Header: jose.JweHeader{ + JwsHeader: jose.JwsHeader{ + Alg: jose.AlgDir, + Kid: "unknown", + }, + Enc: jose.EncA256GCM, + }, + Iv: []byte("iv"), + Ciphertext: []byte("encrypted"), + Tag: []byte("tag"), + } + + err := fakeJwe.MarshalHeader() + require.NoError(t, err) + + marshalledJwe := fakeJwe.Marshal() + + pt, aad, err := decryptor.Decrypt(marshalledJwe) + + assert.Empty(t, pt) + assert.Empty(t, aad) + assert.Equal(t, ErrUnknownKey, err) + + keyMock.AssertExpectations(t) +} + +func TestJweDirectDecryptorImpl_Decrypt_InvalidKeyAlg(t *testing.T) { + keyMock := &authenticatedEncryptionKeyMock{} + keyMock.On("Kid").Return("unique").Once() + keyMock.On("Algorithm").Return(jose.AlgES256).Once() + decryptor := NewJweDirectDecryptorImpl([]AuthenticatedEncryptionKey{keyMock}) + require.NotNil(t, decryptor) + + fakeJwe := &jose.Jwe{ + Header: jose.JweHeader{ + JwsHeader: jose.JwsHeader{ + Alg: jose.AlgDir, + Kid: "unique", + }, + Enc: jose.EncA128GCM, + }, + Iv: []byte("iv"), + Ciphertext: []byte("encrypted"), + Tag: []byte("tag"), + } + + err := fakeJwe.MarshalHeader() + require.NoError(t, err) + + marshalledJwe := fakeJwe.Marshal() + + pt, aad, err := decryptor.Decrypt(marshalledJwe) + + assert.Empty(t, pt) + assert.Empty(t, aad) + assert.Equal(t, ErrInvalidEncryption, err) + + keyMock.AssertExpectations(t) +} + +func TestJweDirectDecryptorImpl_Decrypt_InvalidJweAlg(t *testing.T) { + keyMock := &authenticatedEncryptionKeyMock{} + keyMock.On("Kid").Return("unique").Once() + keyMock.On("Algorithm").Return(jose.AlgA256GCM).Once() + decryptor := NewJweDirectDecryptorImpl([]AuthenticatedEncryptionKey{keyMock}) + require.NotNil(t, decryptor) + + fakeJwe := &jose.Jwe{ + Header: jose.JweHeader{ + JwsHeader: jose.JwsHeader{ + Alg: jose.AlgRS256, + Kid: "unique", + }, + Enc: jose.EncA128GCM, + }, + Iv: []byte("iv"), + Ciphertext: []byte("encrypted"), + Tag: []byte("tag"), + } + + err := fakeJwe.MarshalHeader() + require.NoError(t, err) + + marshalledJwe := fakeJwe.Marshal() + + pt, aad, err := decryptor.Decrypt(marshalledJwe) + + assert.Empty(t, pt) + assert.Empty(t, aad) + assert.Equal(t, ErrInvalidAlgorithm, err) + + keyMock.AssertExpectations(t) +} + +func TestJweDirectDecryptorImpl_Decrypt_InvalidJweEnc(t *testing.T) { + keyMock := &authenticatedEncryptionKeyMock{} + keyMock.On("Kid").Return("unique").Once() + keyMock.On("Algorithm").Return(jose.AlgA256GCM).Once() + decryptor := NewJweDirectDecryptorImpl([]AuthenticatedEncryptionKey{keyMock}) + require.NotNil(t, decryptor) + + fakeJwe := &jose.Jwe{ + Header: jose.JweHeader{ + JwsHeader: jose.JwsHeader{ + Alg: jose.AlgDir, + Kid: "unique", + }, + Enc: jose.EncA128GCM, + }, + Iv: []byte("iv"), + Ciphertext: []byte("encrypted"), + Tag: []byte("tag"), + } + + err := fakeJwe.MarshalHeader() + require.NoError(t, err) + + marshalledJwe := fakeJwe.Marshal() + + pt, aad, err := decryptor.Decrypt(marshalledJwe) + + assert.Empty(t, pt) + assert.Empty(t, aad) + assert.Equal(t, ErrInvalidAlgorithm, err) + + keyMock.AssertExpectations(t) +} + +func TestJweDirectDecryptorImpl_Decrypt_InvalidCiphertextOrTag(t *testing.T) { + expectedError := errors.New("expected") + keyMock := &authenticatedEncryptionKeyMock{} + keyMock.On("Algorithm").Return(jose.AlgA256GCM).Once() + keyMock.On("Kid").Return("unique").Once() + keyMock.On("Open", jose.KeyOpsDecrypt, []byte("iv"), []byte("encrypted"), mock.Anything, []byte("tag")).Return([]byte(nil), expectedError).Once() + decryptor := NewJweDirectDecryptorImpl([]AuthenticatedEncryptionKey{keyMock}) + require.NotNil(t, decryptor) + + fakeJwe := &jose.Jwe{ + Header: jose.JweHeader{ + JwsHeader: jose.JwsHeader{ + Alg: jose.AlgDir, + Kid: "unique", + }, + Enc: jose.EncA256GCM, + }, + Iv: []byte("iv"), + Ciphertext: []byte("encrypted"), + Tag: []byte("tag"), + } + + err := fakeJwe.MarshalHeader() + require.NoError(t, err) + + marshalledJwe := fakeJwe.Marshal() + + pt, aad, err := decryptor.Decrypt(marshalledJwe) + + assert.Empty(t, pt) + assert.Empty(t, aad) + assert.Equal(t, expectedError, err) + + keyMock.AssertExpectations(t) +} + +func TestJweDirectDecryptorImpl_Decrypt(t *testing.T) { + keyMock := &authenticatedEncryptionKeyMock{} + keyMock.On("Algorithm").Return(jose.AlgA256GCM).Once() + keyMock.On("Kid").Return("unique").Once() + decryptor := NewJweDirectDecryptorImpl([]AuthenticatedEncryptionKey{keyMock}) + require.NotNil(t, decryptor) + + fakeJwe := &jose.Jwe{ + Header: jose.JweHeader{ + JwsHeader: jose.JwsHeader{ + Alg: jose.AlgDir, + Kid: "unique", + }, + JweCustomHeaderFields: jose.JweCustomHeaderFields{ + OtherAad: &jose.Blob{ + B: []byte("aad"), + }, + }, + Enc: jose.EncA256GCM, + }, + Iv: []byte("iv"), + Ciphertext: []byte("encrypted"), + Tag: []byte("tag"), + } + + err := fakeJwe.MarshalHeader() + require.NoError(t, err) + keyMock.On("Open", jose.KeyOpsDecrypt, []byte("iv"), []byte("encrypted"), fakeJwe.MarshalledHeader, []byte("tag")).Return([]byte("decrypted"), nil).Once() + + marshalledJwe := fakeJwe.Marshal() + + pt, aad, err := decryptor.Decrypt(marshalledJwe) + + assert.Equal(t, []byte("decrypted"), pt) + assert.Equal(t, []byte("aad"), aad) + assert.NoError(t, err) + + keyMock.AssertExpectations(t) +} diff --git a/jwe_direct_encryptor.go b/jwe_direct_encryptor.go new file mode 100644 index 0000000..7748bd6 --- /dev/null +++ b/jwe_direct_encryptor.go @@ -0,0 +1,85 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "github.com/thalesignite/gose/jose" +) + +var ( + algToEncMap = map[jose.Alg]jose.Enc{ + jose.AlgA128GCM: jose.EncA128GCM, + jose.AlgA192GCM: jose.EncA192GCM, + jose.AlgA256GCM: jose.EncA256GCM, + } +) + +// JweDirectEncryptionEncryptorImpl implementation of JweDirectEncryptionEncryptor interface. +type JweDirectEncryptionEncryptorImpl struct { + key AuthenticatedEncryptionKey +} + +// Encrypt encrypt and authenticate the given plaintext and AAD returning a compact JWE. +func (encryptor *JweDirectEncryptionEncryptorImpl) Encrypt(plaintext, aad []byte) (string, error) { + nonce, err := encryptor.key.GenerateNonce() + if err != nil { + return "", err + } + + var blob *jose.Blob + var customHeaderFields jose.JweCustomHeaderFields + if len(aad) > 0 { + blob = &jose.Blob{B: aad} + customHeaderFields = jose.JweCustomHeaderFields{ + OtherAad: blob, + } + } + + jwe := &jose.Jwe{ + Header: jose.JweHeader{ + JwsHeader: jose.JwsHeader{ + Alg: jose.AlgDir, + Kid: encryptor.key.Kid(), + }, + Enc: algToEncMap[encryptor.key.Algorithm()], + JweCustomHeaderFields: customHeaderFields, + }, + EncryptedKey: []byte{}, + Iv: nonce, + Plaintext: plaintext, + } + if err = jwe.MarshalHeader(); err != nil { + return "", err + } + + if jwe.Ciphertext, jwe.Tag, err = encryptor.key.Seal(jose.KeyOpsEncrypt, jwe.Iv, jwe.Plaintext, jwe.MarshalledHeader); err != nil { + return "", err + } + return jwe.Marshal(), nil +} + +// NewJweDirectEncryptorImpl construct an instance of a JweDirectEncryptionEncryptorImpl. +func NewJweDirectEncryptorImpl(key AuthenticatedEncryptionKey) *JweDirectEncryptionEncryptorImpl { + return &JweDirectEncryptionEncryptorImpl{ + key: key, + } +} diff --git a/jwe_direct_encryptor_test.go b/jwe_direct_encryptor_test.go new file mode 100644 index 0000000..61870e3 --- /dev/null +++ b/jwe_direct_encryptor_test.go @@ -0,0 +1,127 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "log" + "testing" + + "github.com/thalesignite/gose/jose" + "github.com/stretchr/testify/mock" + + "github.com/stretchr/testify/assert" +) + +type authenticatedEncryptionKeyMock struct { + mock.Mock +} + +func (encryptor *authenticatedEncryptionKeyMock) GenerateNonce() ([]byte, error) { + args := encryptor.Called() + return args.Get(0).([]byte), args.Error(1) +} + +func (encryptor *authenticatedEncryptionKeyMock) Seal(operation jose.KeyOps, nonce, plaintext, aad []byte) (ciphertext, tag []byte, err error) { + args := encryptor.Called(operation, nonce, plaintext, aad) + return args.Get(0).([]byte), args.Get(1).([]byte), args.Error(2) +} + +func (encryptor *authenticatedEncryptionKeyMock) Open(operation jose.KeyOps, nonce, ciphertext, aad, tag []byte) (plaintext []byte, err error) { + args := encryptor.Called(operation, nonce, ciphertext, aad, tag) + return args.Get(0).([]byte), args.Error(1) +} + +func (encryptor *authenticatedEncryptionKeyMock) Algorithm() jose.Alg { + args := encryptor.Called() + return args.Get(0).(jose.Alg) +} + +func (encryptor *authenticatedEncryptionKeyMock) Kid() string { + args := encryptor.Called() + return args.String(0) +} + +func (encryptor *authenticatedEncryptionKeyMock) Jwk() (jose.Jwk, error) { + args := encryptor.Called() + return args.Get(0).(jose.Jwk), args.Error(1) +} + +func (encryptor *authenticatedEncryptionKeyMock) Marshal() (string, error) { + args := encryptor.Called() + return args.String(0), args.Error(1) +} + +func TestNewJweEncryptorImpl(t *testing.T) { + keyMock := &authenticatedEncryptionKeyMock{} + encryptor := NewJweDirectEncryptorImpl(keyMock) + assert.NotNil(t, encryptor) +} + +func TestJweDirectEncryptionEncryptorImpl_Encrypt(t *testing.T) { + keyMock := &authenticatedEncryptionKeyMock{} + keyMock.On("GenerateNonce").Return([]byte("nonce"), nil).Once() + keyMock.On("Kid").Return("unique").Once() + keyMock.On("Algorithm").Return(jose.AlgA256GCM).Once() + keyMock.On("Seal", jose.KeyOpsEncrypt, []byte("nonce"), []byte("something"), mock.Anything).Return([]byte("encrypted"), []byte("tag"), nil).Once() + + encryptor := NewJweDirectEncryptorImpl(keyMock) + + jwe, err := encryptor.Encrypt([]byte("something"), []byte("else")) + assert.NoError(t, err) + assert.NotEmpty(t, jwe) + + keyMock.AssertExpectations(t) +} + +func TestExampleJweDirectEncryptionEncryptorImpl_EncryptDecrypt(t *testing.T) { + // First create a key which we use to encrypt and authenticate data. + generator := &AuthenticatedEncryptionKeyGenerator{} + cryptor, _, err := generator.Generate(jose.AlgA256GCM, []jose.KeyOps{jose.KeyOpsEncrypt, jose.KeyOpsDecrypt}) + if err != nil { + panic(err) + } + + // Now to encrypt and authenticate something . + toEncrypt := []byte("some_data_to_encrypt") + aad := []byte("some_data_to_authenticate") + + // Create a JWE cryptor + jweEncryptor := NewJweDirectEncryptorImpl(cryptor) + + // Now encrypt + jwe, err := jweEncryptor.Encrypt(toEncrypt, aad) + if err != nil { + panic(err) + } + + // print our JWE + log.Printf("Created JWE: %s", jwe) + + // Now to decrypt + jweDecryptor := NewJweDirectDecryptorImpl([]AuthenticatedEncryptionKey{cryptor}) + + recoveredPlaintext, recoveredAad, err := jweDecryptor.Decrypt(jwe) + if err != nil { + panic(err) + } + log.Printf("Recovered plaintext \"%s\" and AAD \"%s\"", string(recoveredPlaintext), string(recoveredAad)) +} diff --git a/jwks_truststore.go b/jwks_truststore.go new file mode 100644 index 0000000..a0f85b2 --- /dev/null +++ b/jwks_truststore.go @@ -0,0 +1,113 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "encoding/json" + "errors" + "github.com/thalesignite/gose/jose" + "github.com/sirupsen/logrus" + "net/http" + "sync" +) + +// Interface wrapper to allow mocking of http client. +type httpClient interface { + Get(url string) (resp *http.Response, err error) +} +// JwksTrustStore is an implementation of the TrustStore interface and can be used for accessing VerificationKeys. +type JwksTrustStore struct { + lock sync.Mutex + url string + issuer string + keys []VerificationKey + client httpClient +} + +// Add this method is not supported on a JwksTrustStore instance and will always return an error. +func (store *JwksTrustStore) Add(issuer string, jwk jose.Jwk) error { + return errors.New("read-only trust store") +} + +// Remove this method is not supported on a JwksTrustStore instance and will always return false. +func (store *JwksTrustStore) Remove(issuer, kid string) bool { + return false +} + +// Get returns a verification key for the given issuer and key id. If no key is found nil is returned. +func (store *JwksTrustStore) Get(issuer, kid string) VerificationKey { + if issuer == store.issuer { + store.lock.Lock() + defer store.lock.Unlock() + for _, key := range store.keys { + if key.Kid() == kid { + return key + } + } + // Not found. Refresh the keys + response, err := store.client.Get(store.url) + if err != nil { + logrus.Errorf("error encountered retrieving JWKS from %s: %v", store.url, err) + return nil + } + if response.StatusCode != http.StatusOK { + logrus.Errorf("error encountered retrieving JWKS from %s: %d %s", store.url, response.StatusCode, response.Status) + return nil + } + decoder := json.NewDecoder(response.Body) + var jwks jose.Jwks + if err := decoder.Decode(&jwks); err != nil { + logrus.Errorf("error encountered retrieving JWKS from %s: invalid encoding", store.url) + return nil + } + keys := make([]VerificationKey, 0, len(jwks.Keys)) + for _, jwk := range jwks.Keys { + verificationKey, err := NewVerificationKey(jwk) + if err != nil { + logrus.Errorf("failed to load verification key from JWK: %v", err) + return nil + } + keys = append(keys, verificationKey) + } + // Replace the keys for our store. + store.keys = keys + + // Try and find key in newly cached keys + for _, key := range store.keys { + if key.Kid() == kid { + return key + } + } + // No such currently valid key. + } + return nil +} + + +// NewJwksKeyStore creates a new instance of a TrustStore and can be used to load verification keys. +func NewJwksKeyStore(issuer, url string) *JwksTrustStore { + return &JwksTrustStore{ + url:url, + issuer:issuer, + client:http.DefaultClient, + } +} diff --git a/jwks_truststore_test.go b/jwks_truststore_test.go new file mode 100644 index 0000000..53ca540 --- /dev/null +++ b/jwks_truststore_test.go @@ -0,0 +1,144 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "bytes" + "errors" + "github.com/thalesignite/gose/jose" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "io/ioutil" + "net/http" + "testing" +) + +const ( + // Extracted from https://www.googleapis.com/oauth2/v3/certs + jwks = ` +{ + "keys": [ + { + "kid": "60f4060e58d75fd3f70beff88c794a775327aa31", + "e": "AQAB", + "kty": "RSA", + "alg": "RS256", + "n": "vFfCjiB67cRoJE-zyhZJyjDAUbdAd18Jt69ZkD4JTT8SJ6WviOR6Z5PV_mfF_LwxXy7UalFUZ4zCtWEyoHudcZV9s835-QPNPA2gZ55ChKNSlV3PJXnATf_87Ll50ewuIoe3eKzUFWBrPPB9-Q6SiRGN3STb2PTOXKgTnaUPi0fPwD5ZzhZOXTY67M0l-cX53WMliLguHpDUqbmlK_w4fBNXVWwlPtEhZag-FIavt3kH4hcNEj1hC-cju0_RHE7Dx6t3HFF3aGnsnqRPauAXIrVctLTQJVWDrpObRLOnpqDcoD4Y-cN2PaqLTK0vTnBTIAiP4sazDNCEOl-Zy1ul_w", + "use": "sig" + }, + { + "kid": "df8d9ee403bcc7185ad51041194bd3433742d9aa", + "e": "AQAB", + "kty": "RSA", + "alg": "RS256", + "n": "nQgOafNApTMwKerFuGXDj8HZ7hUSFPUV4_SzYj79SF5giP0IfF6Ksnb5Jy0pQ_MXQ6XNuh6eZqCfAPXUwHtoxE29jpe6L6DGKPLTr8RTbNhdIsorc1yXiPcail58gftq1fmegZw0KO6QtBpKYnBWoZw4PJkuP8ZdGanA0btsZRRRYVmSOKuYDNHfVJlcrD4cqAOL3BPjWQIrZszwTVmw0FjiU9KfGtU0rDYnas-mZv1qfetZkTA3YPTqSspCNZDbGCVXpJnr4pai0E7lxFgDNDN2IDk955Pf8eG8oNCfqkHXfnWDrTlXP7SSrYmEaBPcmMKOHdjyrYPk0lWI8-urXw", + "use": "sig" + } + ] +}` +) + +type httpClientMock struct { + mock.Mock +} + +func (client *httpClientMock) Get(url string) (resp *http.Response, err error) { + args := client.Called(url) + return args.Get(0).(*http.Response), args.Error(1) +} + +func TestJwksTrustStore_Add(t *testing.T) { + store := NewJwksKeyStore("", "") + err := store.Add("", &jose.PublicRsaKey{}) + assert.Error(t, err, "read-only trust store") +} + +func TestJwksTrustStore_Remove(t *testing.T) { + store := NewJwksKeyStore("", "") + removed := store.Remove("", "") + assert.False(t, removed) +} + +func TestJwksTrustStore_Get(t *testing.T) { + mockedClient := &httpClientMock{} + mockedClient.On("Get", "https://www.googleapis.com/oauth2/v3/certs").Return( + &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(jwks))), + }, nil).Once() + store := NewJwksKeyStore("https://accounts.google.com", "https://www.googleapis.com/oauth2/v3/certs") + store.client = mockedClient + key := store.Get("https://accounts.google.com", "60f4060e58d75fd3f70beff88c794a775327aa31") + assert.NotNil(t, key) + require.Len(t, store.keys, 2) + assert.NotNil(t, store.Get("https://accounts.google.com", "df8d9ee403bcc7185ad51041194bd3433742d9aa")) + mockedClient.AssertExpectations(t) +} + +func TestJwksTrustStore_GetHttpClientError(t *testing.T) { + mockedClient := &httpClientMock{} + mockedClient.On("Get", "https://www.googleapis.com/oauth2/v3/certs").Return( + (*http.Response)(nil), errors.New("expected")).Times(2) + store := NewJwksKeyStore("https://accounts.google.com", "https://www.googleapis.com/oauth2/v3/certs") + store.client = mockedClient + for i := 0; i < 2; i++ { + key := store.Get("https://accounts.google.com", "invalid") + assert.Nil(t, key) + require.Len(t, store.keys, 0) + } + mockedClient.AssertExpectations(t) +} + +func TestJwksTrustStore_GetHttpError(t *testing.T) { + mockedClient := &httpClientMock{} + mockedClient.On("Get", "https://www.googleapis.com/oauth2/v3/certs").Return( + &http.Response{ + StatusCode: http.StatusForbidden, + Body: ioutil.NopCloser(bytes.NewReader([]byte(jwks))), + }, nil).Times(2) + store := NewJwksKeyStore("https://accounts.google.com", "https://www.googleapis.com/oauth2/v3/certs") + store.client = mockedClient + for i := 0; i < 2; i++ { + key := store.Get("https://accounts.google.com", "invalid") + assert.Nil(t, key) + require.Len(t, store.keys, 0) + } + mockedClient.AssertExpectations(t) +} + +func TestJwksTrustStore_GetInvalidJwksEncoding(t *testing.T) { + mockedClient := &httpClientMock{} + mockedClient.On("Get", "https://www.googleapis.com/oauth2/v3/certs").Return( + &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte("invalid"))), + }, nil).Times(2) + store := NewJwksKeyStore("https://accounts.google.com", "https://www.googleapis.com/oauth2/v3/certs") + store.client = mockedClient + for i := 0; i < 2; i++ { + key := store.Get("https://accounts.google.com", "invalid") + assert.Nil(t, key) + require.Len(t, store.keys, 0) + } + mockedClient.AssertExpectations(t) +} diff --git a/jwt_signer.go b/jwt_signer.go new file mode 100644 index 0000000..671a91f --- /dev/null +++ b/jwt_signer.go @@ -0,0 +1,93 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "encoding/json" + "log" + "time" + + "github.com/thalesignite/gose/jose" + "github.com/google/uuid" +) + +var _ JwtSigner = (*JwtSignerImpl)(nil) +//JwtSignerImpl JWT implementation +type JwtSignerImpl struct { + key SigningKey + issuer string +} + +//Issuer returns issuer of JWT +func (signer *JwtSignerImpl) Issuer() string { + return signer.issuer +} + +//Sign claims to a JWT string +func (signer *JwtSignerImpl) Sign(claims *jose.SettableJwtClaims, untyped map[string]interface{}) (string, error) { + var encodedUntyped jose.UntypedClaims + if untyped != nil { + encodedUntyped = make(jose.UntypedClaims, len(untyped)) + for k, v := range untyped { + encoded, err := json.Marshal(v) + if err != nil { + return "", err + } + encodedUntyped[k] = encoded + } + } + jwt := jose.Jwt{ + Header: jose.JwsHeader{ + Alg: signer.key.Algorithm(), + Kid: signer.key.Kid(), + Typ: jose.JwtType, + }, + Claims: jose.JwtClaims{ + AutomaticJwtClaims: jose.AutomaticJwtClaims{ + IssuedAt: time.Now().Unix(), + Issuer: signer.issuer, + JwtID: uuid.New().String(), + }, + SettableJwtClaims: *claims, + UntypedClaims: encodedUntyped, + }, + } + toBeSigned, err := jwt.MarshalBody() + if err != nil { + return "", err + } + signature, err := signer.key.Sign(jose.KeyOpsSign, []byte(toBeSigned)) + if err != nil { + return "", err + } + jwt.Signature = make([]byte, len(signature)) + if count := copy(jwt.Signature, signature); count != len(signature) { + // This should never happen! + log.Panic("failed to copy all signature bytes") + } + return jose.MarshalJws(toBeSigned, jwt.Signature), nil +} + +//NewJwtSigner returns a JWT Signer for a issuer and jwk +func NewJwtSigner(issuer string, key SigningKey) *JwtSignerImpl { + return &JwtSignerImpl{key: key, issuer: issuer} +} diff --git a/jwt_signer_test.go b/jwt_signer_test.go new file mode 100644 index 0000000..d81703a --- /dev/null +++ b/jwt_signer_test.go @@ -0,0 +1,121 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "encoding/base64" + "encoding/json" + "errors" + "strings" + "testing" + + "github.com/thalesignite/gose/jose" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestNewJwtSigner(t *testing.T) { + // Setup + mockedKey := MockedSigner{} + // Act and Assert + assert.NotNil(t, NewJwtSigner("issuer", &mockedKey)) +} + +func TestJwtSignerImpl_Sign(t *testing.T) { + // Setup + expectedSignature := []byte{1, 2, 3, 4} + mockedKey := MockedSigner{} + mockedKey.On("Kid").Return("keyid") + mockedKey.On("Algorithm").Return(jose.AlgPS256) + mockedKey.On("Sign", jose.KeyOpsSign, mock.AnythingOfType("[]uint8")).Return(expectedSignature, nil) + signer := NewJwtSigner("issuer", &mockedKey) + claims := jose.SettableJwtClaims{ + Audiences: jose.Audiences{Aud: []string{"audience"}}, + Subject: "subject", + } + untyped := map[string]interface{}{ + "name": "John Doe", + } + + // Act + jwt, err := signer.Sign(&claims, untyped) + + // Assert + require.NotEmpty(t, jwt) + assert.NoError(t, err) + + parts := strings.Split(jwt, ".") + require.Len(t, parts, 3) + + // header verification + var header jose.JwsHeader + require.NoError(t, err) + headerBytes, err := base64.RawURLEncoding.DecodeString(parts[0]) + require.NoError(t, err) + err = json.Unmarshal(headerBytes, &header) + assert.NoError(t, err) + assert.Equal(t, jose.AlgPS256, header.Alg) + assert.Equal(t, jose.JwtType, header.Typ) + assert.Equal(t, "keyid", header.Kid) + assert.Empty(t, header.Crit) + + // MarshalBody verification + var recoveredClaims jose.JwtClaims + claimsBytes, err := base64.RawURLEncoding.DecodeString(parts[1]) + err = json.Unmarshal(claimsBytes, &recoveredClaims) + require.NoError(t, err) + assert.Equal(t, claims.Audiences, recoveredClaims.Audiences) + assert.Equal(t, "issuer", recoveredClaims.Issuer) + assert.Equal(t, claims.Subject, recoveredClaims.Subject) + assert.Len(t, recoveredClaims.UntypedClaims, 1) + var name string + err = recoveredClaims.UnmarshalCustomClaim("name", &name) + require.NoError(t, err) + assert.Equal(t, "John Doe", name) + + // Signature check + rawSignature, err := base64.RawURLEncoding.DecodeString(parts[2]) + require.NoError(t, err) + assert.Equal(t, expectedSignature, rawSignature) +} + +func TestJwtSignerImpl_Sign_FailsWhenSigningFails(t *testing.T) { + // Setup + expectedError := errors.New("Expected") + mockedKey := MockedSigner{} + mockedKey.On("Kid").Return("keyid") + mockedKey.On("Algorithm").Return(jose.AlgPS256) + mockedKey.On("Sign", jose.KeyOpsSign, mock.AnythingOfType("[]uint8")).Return(nil, expectedError) + signer := NewJwtSigner("issuer", &mockedKey) + claims := jose.SettableJwtClaims{ + Audiences: jose.Audiences{Aud: []string{"audience"}}, + Subject: "subject", + } + + // Act + jwt, err := signer.Sign(&claims, nil) + + // Assert + require.Empty(t, jwt) + assert.Equal(t, err, expectedError) +} diff --git a/jwt_verifier.go b/jwt_verifier.go new file mode 100644 index 0000000..7513435 --- /dev/null +++ b/jwt_verifier.go @@ -0,0 +1,96 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "time" + + "github.com/thalesignite/gose/jose" +) + +//JwtVerifierImpl implements the JWT Verification API +type JwtVerifierImpl struct { + store TrustStore +} + +//Verify the jwt and audience is valid +func (verifier *JwtVerifierImpl) Verify(jwt string, audience []string) (kid string, claims *jose.JwtClaims, err error) { + var token jose.Jwt + var signed string + if signed, err = token.Unmarshal(jwt); err != nil { + return + } + now := time.Now().Unix() + if token.Claims.NotBefore > now { + err = ErrInvalidJwtTimeframe + return + } + if token.Claims.Expiration <= now { + err = ErrInvalidJwtTimeframe + return + } + if len(token.Claims.Audiences.Aud) == 0 || len(audience) == 0 { + err = ErrNoExpectedAudience + return + } + // Check at least 1 audience exists + found := false + for _, candidate := range audience { + for _, aud := range token.Claims.Audiences.Aud { + found = found || candidate == aud + } + } + if !found { + err = ErrNoExpectedAudience + return + } + + // Though optional in the JWT spec we always require a Key ID to be present + // to resist various known attacks. + if len(token.Header.Kid) == 0 { + err = ErrInvalidKid + return + } + key := verifier.store.Get(token.Claims.Issuer, token.Header.Kid) + if key == nil { + err = ErrUnknownKey + return + } + // Ensure algorithms match! + if key.Algorithm() != token.Header.Alg { + err = ErrInvalidAlgorithm + return + } + + if !key.Verify(jose.KeyOpsVerify, []byte(signed), token.Signature) { + err = ErrInvalidSignature + return + } + kid = key.Kid() + claims = &token.Claims + return +} + +//NewJwtVerifier creates a JWT Verifier for a given truststore +func NewJwtVerifier(ks TrustStore) *JwtVerifierImpl { + return &JwtVerifierImpl{store: ks} +} diff --git a/jwt_verifier_test.go b/jwt_verifier_test.go new file mode 100644 index 0000000..ff99272 --- /dev/null +++ b/jwt_verifier_test.go @@ -0,0 +1,269 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "encoding/base64" + "testing" + "time" + + "crypto/x509" + + "github.com/thalesignite/gose/jose" + "github.com/bouk/monkey" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +const ( + /* The below jwt contains the following generated @ https://jwt.io/: + header: + - typ: JWT + - alg: PS256/ES256 + - kid: 12345 + claims: + - sub: 1234567890, + - aud: test, + - jti: 31c8767d-6732-4db4-b488-21cf34e5412d, + - iat: 1514977397, + - exp: 1514980997, + - nbf: 1514980997 + */ + rsaValidJwtPayload = "eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCIsICJraWQiOiIxMjM0NSJ9Cg.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoidGVzdCIsImp0aSI6IjMxYzg3NjdkLTY3MzItNGRiNC1iNDg4LTIxY2YzNGU1NDEyZCIsImlhdCI6MTUxNDk3NzM5NywibmJmIjoxNTE0OTc3Mzk3LCJleHAiOjE1MTQ5ODA5OTd9Cg" + rsaValidJwtSignature = "q0BpUU1b3rMraJR0dWGdpEffEBjmDhwT9O1AnSjvDGM" + rsaValidJwt = rsaValidJwtPayload + "." + rsaValidJwtSignature + rsaValidJwtAlg = jose.AlgPS256 + validJwtKid = "12345" + validJwtSub = "1234567890" + validJwtAud = "test" + validJwtJti = "31c8767d-6732-4db4-b488-21cf34e5412d" + validJwtIat int64 = 1514977397 + validJwtNbf int64 = 1514977397 + validJwtExp int64 = 1514980997 +) + +func unixStartTime() time.Time { + return time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) +} + +type MockedTrustKeyStore struct { + mock.Mock +} + +func (store *MockedTrustKeyStore) Add(issuer string, jwk jose.Jwk) error { + args := store.Called(issuer, jwk) + return args.Error(0) +} + +func (store *MockedTrustKeyStore) Remove(issuer, kid string) bool { + args := store.Called(issuer, kid) + return args.Bool(0) +} + +func (store *MockedTrustKeyStore) Get(issuer, kid string) VerificationKey { + args := store.Called(issuer, kid) + return args.Get(0).(VerificationKey) +} + +type MockedVerificationKey struct { + mock.Mock +} + +func (key *MockedVerificationKey) Algorithm() jose.Alg { + return key.Called().Get(0).(jose.Alg) +} + +func (key *MockedVerificationKey) Kid() string { + return key.Called().Get(0).(string) +} + +func (key *MockedVerificationKey) Jwk() (jose.Jwk, error) { + args := key.Called() + return args.Get(0).(jose.Jwk), args.Error(1) +} + +func (key *MockedVerificationKey) Marshal() (string, error) { + args := key.Called() + return args.String(0), args.Error(1) +} + +func (key *MockedVerificationKey) MarshalPem() (string, error) { + args := key.Called() + return args.String(0), args.Error(1) +} + +func (key *MockedVerificationKey) Certificates() []*x509.Certificate { + return key.Called().Get(0).([]*x509.Certificate) +} + +func (key *MockedVerificationKey) Verify(operation jose.KeyOps, data []byte, signature []byte) bool { + args := key.Called(operation, data, signature) + return args.Bool(0) +} + +func TestNewJwtVerifier(t *testing.T) { + // Setup + ks := MockedTrustKeyStore{} + + // Act + verifier := NewJwtVerifier(&ks) + + // Assert + require.NotNil(t, verifier) + assert.Equal(t, &ks, verifier.store) +} + +func TestJwtVerifierImpl_Verify(t *testing.T) { + // Setup + defer monkey.Patch(time.Now, func() time.Time { + return unixStartTime().Add(time.Duration(validJwtNbf) * time.Second) + }).Unpatch() + key := MockedVerificationKey{} + signatureBytes, err := base64.RawURLEncoding.DecodeString(rsaValidJwtSignature) + require.NoError(t, err) + key.On("Verify", jose.KeyOpsVerify, []byte(rsaValidJwtPayload), signatureBytes).Return(true) + key.On("Algorithm").Return(rsaValidJwtAlg) + key.On("Kid").Return(validJwtKid) + ks := MockedTrustKeyStore{} + ks.On("Get", mock.Anything, validJwtKid).Return(&key) + verifier := NewJwtVerifier(&ks) + + // Act + kid, claimSet, err := verifier.Verify(rsaValidJwt, []string{"test"}) + + // Assert + require.NoError(t, err) + require.NotNil(t, claimSet) + assert.Equal(t, validJwtKid, kid) + assert.Equal(t, validJwtAud, claimSet.Audiences.Aud[0]) + assert.Equal(t, validJwtSub, claimSet.Subject) + assert.NotEmpty(t, validJwtJti, claimSet.JwtID) + assert.Equal(t, validJwtIat, claimSet.IssuedAt) + assert.Equal(t, validJwtExp, claimSet.Expiration) + assert.Equal(t, validJwtNbf, claimSet.NotBefore) +} + +func TestJwtVerifierImpl_Verify_FailsWithInvalidSignature(t *testing.T) { + // Setup + defer monkey.Patch(time.Now, func() time.Time { + return unixStartTime().Add(time.Duration(validJwtNbf) * time.Second) + }).Unpatch() + key := MockedVerificationKey{} + signatureBytes, err := base64.RawURLEncoding.DecodeString(rsaValidJwtSignature) + require.NoError(t, err) + key.On("Verify", jose.KeyOpsVerify, []byte(rsaValidJwtPayload), signatureBytes).Return(false) + key.On("Algorithm").Return(rsaValidJwtAlg) + ks := MockedTrustKeyStore{} + ks.On("Verifier", validJwtKid).Return(&key) + ks.On("Get", mock.Anything, validJwtKid).Return(&key) + verifier := NewJwtVerifier(&ks) + + // Act + _, claimSet, err := verifier.Verify(rsaValidJwt, []string{"test"}) + + // Assert + require.Equal(t, ErrInvalidSignature, err) + require.Nil(t, claimSet) +} + +func TestJwtVerifierImpl_Verify_FailsWithNbfViolation(t *testing.T) { + // Setup + defer monkey.Patch(time.Now, func() time.Time { + return unixStartTime().Add(time.Duration(validJwtNbf-10) * time.Second) + }).Unpatch() + key := MockedVerificationKey{} + signatureBytes, err := base64.RawURLEncoding.DecodeString(rsaValidJwtSignature) + require.NoError(t, err) + key.On("Verify", jose.KeyOpsVerify, []byte(rsaValidJwtPayload), signatureBytes).Return(true) + key.On("Algorithm").Return(rsaValidJwtAlg) + ks := MockedTrustKeyStore{} + ks.On("Verifier", validJwtKid).Return(&key) + ks.On("Get", mock.Anything, validJwtKid).Return(&key) + verifier := NewJwtVerifier(&ks) + + // Act + kid, claimSet, err := verifier.Verify(rsaValidJwt, []string{"test"}) + + // Assert + require.Equal(t, ErrInvalidJwtTimeframe, err) + require.Empty(t, kid) + require.Nil(t, claimSet) +} + +func TestJwtVerifierImpl_Verify_FailsWithExpViolation(t *testing.T) { + // Setup + defer monkey.Patch(time.Now, func() time.Time { + return unixStartTime().Add(time.Duration(validJwtExp+10) * time.Second) + }).Unpatch() + key := MockedVerificationKey{} + signatureBytes, err := base64.RawURLEncoding.DecodeString(rsaValidJwtSignature) + require.NoError(t, err) + key.On("Verify", jose.KeyOpsVerify, []byte(rsaValidJwtPayload), signatureBytes).Return(true) + key.On("Algorithm").Return(rsaValidJwtAlg) + ks := MockedTrustKeyStore{} + ks.On("Verifier", validJwtKid).Return(&key) + ks.On("Get", mock.Anything, validJwtKid).Return(&key) + verifier := NewJwtVerifier(&ks) + + // Act + kid, claimSet, err := verifier.Verify(rsaValidJwt, []string{"test"}) + + // Assert + require.Equal(t, ErrInvalidJwtTimeframe, err) + require.Empty(t, kid) + require.Nil(t, claimSet) +} + +func TestJwtVerifierImpl_Verify_FailsWithNoKnownAudience(t *testing.T) { + testcases := []struct { + audiences []string + }{ + { + audiences: []string{"unknown"}, + }, + { + audiences: []string{}, + }, + } + // Setup + defer monkey.Patch(time.Now, func() time.Time { + return unixStartTime().Add(time.Duration(validJwtNbf) * time.Second) + }).Unpatch() + key := MockedVerificationKey{} + signatureBytes, err := base64.RawURLEncoding.DecodeString(rsaValidJwtSignature) + require.NoError(t, err) + key.On("Verify", jose.KeyOpsVerify, []byte(rsaValidJwtPayload), signatureBytes).Return(false) + key.On("Algorithm").Return(rsaValidJwtAlg) + ks := MockedTrustKeyStore{} + ks.On("Verifier", validJwtKid).Return(&key) + ks.On("Get", mock.Anything, validJwtKid).Return(&key) + verifier := NewJwtVerifier(&ks) + + for _, test := range testcases { + // Act + _, _, err := verifier.Verify(rsaValidJwt, test.audiences) + + // Assert + assert.Equal(t, ErrNoExpectedAudience, err) + } +} diff --git a/key_generator.go b/key_generator.go new file mode 100644 index 0000000..b873c8e --- /dev/null +++ b/key_generator.go @@ -0,0 +1,130 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + + "github.com/thalesignite/gose/jose" +) + +const minimumRsaKeySize = 2048 // The minimum RSA key size allowable as defined https://tools.ietf.org/html/rfc7518#section-3.5 +var ( + rsaAlgs = map[jose.Alg]bool{ + jose.AlgRS256: true, + jose.AlgRS384: true, + jose.AlgRS512: true, + jose.AlgPS256: true, + jose.AlgPS384: true, + jose.AlgPS512: true, + } + ecdsAlgs = map[jose.Alg]elliptic.Curve{ + jose.AlgES256: elliptic.P256(), + jose.AlgES384: elliptic.P384(), + jose.AlgES512: elliptic.P521(), + } + authenticatedEncryptionAlgs = map[jose.Alg]int{ + jose.AlgA128GCM: 16, + jose.AlgA192GCM: 24, + jose.AlgA256GCM: 32, + } +) + +//RsaSigningKeyGenerator handles generating a RSA signing key +type RsaSigningKeyGenerator struct { +} + +//Generate an RSA key using a given algorithm, length, and scope to certain jwk operations. +func (generator *RsaSigningKeyGenerator) Generate(alg jose.Alg, bitLen int, operations []jose.KeyOps) (SigningKey, error) { + /* Generate an RSA signing jwk. */ + if _, ok := rsaAlgs[alg]; !ok { + return nil, ErrInvalidAlgorithm + } + if bitLen < minimumRsaKeySize { + return nil, ErrInvalidKeySize + } + privateKey, err := rsa.GenerateKey(rand.Reader, bitLen) + if err != nil { + return nil, err + } + jwk, err := JwkFromPrivateKey(privateKey, operations, []*x509.Certificate{}) + if err != nil { + return nil, err + } + jwk.SetAlg(alg) + return NewSigningKey(jwk, operations) +} + +//ECDSASigningKeyGenerator handles generating an ECDSA signing key +type ECDSASigningKeyGenerator struct { +} + +//Generate an ECDSA key using a given algorithm, and scoped to certain jwk operations. +func (g *ECDSASigningKeyGenerator) Generate(alg jose.Alg, operations []jose.KeyOps) (SigningKey, error) { + + curve, ok := ecdsAlgs[alg] + if !ok { + return nil, ErrInvalidAlgorithm + } + + privateKey, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + return nil, err + } + jwk, err := JwkFromPrivateKey(privateKey, operations, []*x509.Certificate{}) + if err != nil { + return nil, err + } + jwk.SetAlg(alg) + + return NewSigningKey(jwk, operations) +} + +// AuthenticatedEncryptionKeyGenerator can be used to create AuthenticatedEncryptionKeys. +type AuthenticatedEncryptionKeyGenerator struct{} + +// Generate generate a Generate and JWK representation. +func (g *AuthenticatedEncryptionKeyGenerator) Generate(alg jose.Alg, operations []jose.KeyOps) (AuthenticatedEncryptionKey, jose.Jwk, error) { + sz, ok := authenticatedEncryptionAlgs[alg] + if !ok { + return nil, nil, ErrInvalidAlgorithm + } + + key := make([]byte, sz) + if _, err := rand.Read(key); err != nil { + return nil, nil, err + } + jwk, err := JwkFromSymmetric(key, alg) + if err != nil { + return nil, nil, err + } + jwk.SetOps(operations) + cryptor, err := NewAesGcmCryptorFromJwk(jwk, operations) + if err != nil { + return nil, nil, err + } + return cryptor, jwk, nil +} diff --git a/key_generator_test.go b/key_generator_test.go new file mode 100644 index 0000000..c92692a --- /dev/null +++ b/key_generator_test.go @@ -0,0 +1,266 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "crypto/rsa" + "errors" + "fmt" + "io" + "math" + "reflect" + "testing" + + "github.com/thalesignite/gose/jose" + "github.com/bouk/monkey" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func BenchmarkRsaSigningKeyGenerator_Generate(b *testing.B) { + setups := map[jose.Alg]int{ + jose.AlgRS256: 2048, + jose.AlgRS384: 2048, + jose.AlgRS512: 2048, + } + generator := new(RsaSigningKeyGenerator) + + for algo, length := range setups { + b.Run(fmt.Sprintf("%v_%v", algo, length), func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = generator.Generate(algo, length, []jose.KeyOps{jose.KeyOpsSign}) + } + }) + } +} + +func BenchmarkECDSASigningKeyGenerator_Generate(b *testing.B) { + setups := map[jose.Alg]int{ + jose.AlgES256: 256, + jose.AlgES384: 384, + jose.AlgES512: 512, + } + generator := new(ECDSASigningKeyGenerator) + + for algo, length := range setups { + + b.Run(fmt.Sprintf("%v_%v", algo, length), func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = generator.Generate(algo, []jose.KeyOps{jose.KeyOpsSign}) + } + }) + } +} + +func TestRSAGenerateSigningKeySucceeds(t *testing.T) { + // Setup + generator := new(RsaSigningKeyGenerator) + + cases := []struct { + bits int + alg jose.Alg + }{ + { + bits: 2048, + alg: jose.AlgRS256, + }, + { + bits: 2048, + alg: jose.AlgRS384, + }, + { + bits: 2048, + alg: jose.AlgRS512, + }, + { + bits: 2048, + alg: jose.AlgPS256, + }, + { + bits: 2048, + alg: jose.AlgPS384, + }, + { + bits: 2048, + alg: jose.AlgPS512, + }, + } + + // Act + for _, test := range cases { + key, err := generator.Generate(test.alg, test.bits, []jose.KeyOps{jose.KeyOpsSign}) + + // Assert + require.Nil(t, err) + require.NotNil(t, key) + jwk, err := key.Jwk() + require.NoError(t, err) + assert.Equal(t, test.alg, jwk.Alg()) + } +} + +func TestECDSAGenerateSigningKeySucceeds(t *testing.T) { + // Setup + generator := new(ECDSASigningKeyGenerator) + + cases := []struct { + bits int + alg jose.Alg + }{ + { + bits: 256, + alg: jose.AlgES256, + }, + { + bits: 384, + alg: jose.AlgES384, + }, + { + bits: 512, + alg: jose.AlgES512, + }, + } + + // Act + for _, test := range cases { + key, err := generator.Generate(test.alg, []jose.KeyOps{jose.KeyOpsSign}) + + // Assert + require.Nil(t, err) + require.NotNil(t, key) + jwk, err := key.Jwk() + require.NoError(t, err) + assert.Equal(t, test.alg, jwk.Alg()) + } +} + +func TestGenerateSigningKeyFailsWhenInvalidAlgorithm(t *testing.T) { + // Setup + generator := new(RsaSigningKeyGenerator) + + // Act + key, err := generator.Generate(jose.AlgES256, 2048, []jose.KeyOps{jose.KeyOpsSign}) + + // Assert + assert.Nil(t, key) + assert.Equal(t, ErrInvalidAlgorithm, err) +} + +func TestGenerateSigningKeyFailsWhenInvalidKeySize(t *testing.T) { + // Setup + generator := new(RsaSigningKeyGenerator) + + // Act + key, err := generator.Generate(jose.AlgRS256, 1024, []jose.KeyOps{jose.KeyOpsSign}) + + // Assert + assert.Nil(t, key) + assert.Equal(t, ErrInvalidKeySize, err) +} + +func TestGenerateSigningKeyFailsWhenInvalidOperation(t *testing.T) { + // Setup + testCase := [][]jose.KeyOps{ + {jose.KeyOpsVerify}, + } + generator := new(RsaSigningKeyGenerator) + + for _, test := range testCase { + // Act + key, err := generator.Generate(jose.AlgRS256, 2048, test) + + // Assert + assert.Nil(t, key) + assert.Equal(t, ErrInvalidOperations, err) + } +} + +func TestGenerateSigningKeyFailsWhenGenerateKeyFails(t *testing.T) { + // Setup + generator := new(RsaSigningKeyGenerator) + expectedError := errors.New("Expected error") + defer monkey.Patch(rsa.GenerateKey, func(reader io.Reader, bits int) (*rsa.PrivateKey, error) { + return nil, expectedError + }).Unpatch() + + // Act + k, e := generator.Generate(jose.AlgRS256, 2048, []jose.KeyOps{jose.KeyOpsSign}) + + // Assert + require.Nil(t, k) + require.Error(t, expectedError, e) +} + +func TestGenerateSigningKeyFailsWhenExponentTooBig(t *testing.T) { + // Setup + fakeKey := rsa.PrivateKey{} + fakeKey.E = math.MaxInt64 + defer monkey.PatchInstanceMethod(reflect.TypeOf(&fakeKey), "Validate", + func(*rsa.PrivateKey) error { return nil }, + ).Unpatch() + generator := new(RsaSigningKeyGenerator) + defer monkey.Patch(rsa.GenerateKey, func(reader io.Reader, bits int) (*rsa.PrivateKey, error) { + var k rsa.PrivateKey + k.E = math.MaxInt64 + return &k, nil + }).Unpatch() + + // Act + k, e := generator.Generate(jose.AlgRS256, 2048, []jose.KeyOps{jose.KeyOpsSign}) + + // Assert + require.Nil(t, k) + require.Error(t, ErrInvalidExponent, e) +} + +func TestAuthenticatedEncryptionKeyGenerator_Generate_InvalidAlgorithm(t *testing.T) { + generator := &AuthenticatedEncryptionKeyGenerator{} + + key, jwk, err := generator.Generate(jose.AlgES256, []jose.KeyOps{}) + + assert.Nil(t, key) + assert.Nil(t, jwk) + assert.Equal(t, ErrInvalidAlgorithm, err) +} + +func TestAuthenticatedEncryptionKeyGenerator_Generate_NoValidOperations(t *testing.T) { + generator := &AuthenticatedEncryptionKeyGenerator{} + + key, jwk, err := generator.Generate(jose.AlgA256GCM, []jose.KeyOps{}) + + assert.Nil(t, key) + assert.Nil(t, jwk) + assert.Equal(t, ErrInvalidOperations, err) +} + +func TestAuthenticatedEncryptionKeyGenerator_Generate(t *testing.T) { + generator := &AuthenticatedEncryptionKeyGenerator{} + + for _, alg := range []jose.Alg{jose.AlgA128GCM, jose.AlgA192GCM, jose.AlgA256GCM} { + + key, jwk, err := generator.Generate(alg, []jose.KeyOps{jose.KeyOpsDecrypt, jose.KeyOpsEncrypt}) + + assert.NotNil(t, key) + assert.NotNil(t, jwk) + assert.NoError(t, err) + } +} diff --git a/keystore.go b/keystore.go new file mode 100644 index 0000000..e39390f --- /dev/null +++ b/keystore.go @@ -0,0 +1,119 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "io/ioutil" + + "bytes" + "encoding/json" + "sync" + + "github.com/thalesignite/gose/jose" +) + +//TrustKeyStoreImpl implements the Trust Store API +type TrustKeyStoreImpl struct { + keys map[string]map[string]jose.Jwk + mtx sync.Mutex +} + +//Add add an issuer and JWK to the truststore +func (store *TrustKeyStoreImpl) Add(issuer string, jwk jose.Jwk) error { + if jwk.Kid() == "" { + // We want a Key ID and we want it now! + return ErrInvalidKey + } + store.mtx.Lock() + defer store.mtx.Unlock() + if _, exists := store.keys[issuer]; !exists { + store.keys[issuer] = make(map[string]jose.Jwk) + } + if _, exists := store.keys[issuer][jwk.Kid()]; exists { + return nil + } + store.keys[issuer][jwk.Kid()] = jwk + return nil +} + +//Remove remove JWK for issuer and jwk id +func (store *TrustKeyStoreImpl) Remove(issuer, kid string) bool { + store.mtx.Lock() + defer store.mtx.Unlock() + if _, exists := store.keys[issuer]; !exists { + return false + } + delete(store.keys[issuer], kid) + return true +} + +//Get get verification jwk for issuer and jwk id +func (store *TrustKeyStoreImpl) Get(issuer, kid string) VerificationKey { + store.mtx.Lock() + defer store.mtx.Unlock() + if keySet, ok := store.keys[issuer]; ok { + if jwk, ok := keySet[kid]; ok { + if key, err := NewVerificationKey(jwk); err == nil { + return key + } + } + } + return nil +} + +//NewTrustKeyStore loads truststore for map of jose.JWK +func NewTrustKeyStore(rootData map[string]jose.Jwk) (store *TrustKeyStoreImpl, err error) { + tmp := TrustKeyStoreImpl{} + tmp.keys = make(map[string]map[string]jose.Jwk) + for issuer, jwk := range rootData { + if err = tmp.Add(issuer, jwk); err != nil { + return + } + } + store = &tmp + return +} + +//NewTrustKeyStoreFromFile loads truststore for a +func NewTrustKeyStoreFromFile(root string) (store *TrustKeyStoreImpl, err error) { + tmp := TrustKeyStoreImpl{} + tmp.keys = make(map[string]map[string]jose.Jwk) + var entries map[string]json.RawMessage + rootData, err := ioutil.ReadFile(root) + if err != nil { + return nil, err + } + if err = json.Unmarshal(rootData, &entries); err != nil { + return + } + for issuer, entry := range entries { + var jwk jose.Jwk + if jwk, err = jose.UnmarshalJwk(bytes.NewReader([]byte(entry))); err != nil { + return + } + if err = tmp.Add(issuer, jwk); err != nil { + return + } + } + store = &tmp + return +} diff --git a/keystore_test.go b/keystore_test.go new file mode 100644 index 0000000..77fb2a9 --- /dev/null +++ b/keystore_test.go @@ -0,0 +1,60 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "testing" + + "github.com/thalesignite/gose/jose" + "github.com/stretchr/testify/assert" +) + +func BenchmarkNewTrustKeyStore(b *testing.B) { + // Setup + b.Helper() + keys := map[string]jose.Jwk{ + "issuer": &jose.PublicRsaKey{}, + } + for _, key := range keys { + key.SetKid("123456") + } + for i := 0; i < b.N; i++ { + _, _ = NewTrustKeyStore(keys) + + } +} + +func TestNewTrustKeyStore(t *testing.T) { + // Setup + keys := map[string]jose.Jwk{ + "issuer": &jose.PublicRsaKey{}, + } + for _, key := range keys { + key.SetKid("123456") + } + // Act + store, err := NewTrustKeyStore(keys) + + // Assert + assert.NoError(t, err) + assert.NotNil(t, store) +} diff --git a/signer.go b/signer.go new file mode 100644 index 0000000..6f4d828 --- /dev/null +++ b/signer.go @@ -0,0 +1,168 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "bytes" + "crypto" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + + "github.com/thalesignite/gose/jose" + "github.com/sirupsen/logrus" +) + +//SigningKeyImpl implements a RSA signing key +type SigningKeyImpl struct { + jwk jose.Jwk + key crypto.Signer + certs []*x509.Certificate +} + +/* Alg to digest map. */ +var algToOptsMap = map[jose.Alg]crypto.SignerOpts{ + jose.AlgPS256: &rsa.PSSOptions{SaltLength: 32, Hash: crypto.SHA256}, + jose.AlgPS384: &rsa.PSSOptions{SaltLength: 48, Hash: crypto.SHA384}, + jose.AlgPS512: &rsa.PSSOptions{SaltLength: 64, Hash: crypto.SHA512}, + jose.AlgRS256: crypto.SHA256, + jose.AlgRS384: crypto.SHA384, + jose.AlgRS512: crypto.SHA512, + jose.AlgES256: &ECDSAOptions{Hash: crypto.SHA256, keySizeBytes: 32, curveBits: 256, curve: elliptic.P256()}, + jose.AlgES384: &ECDSAOptions{Hash: crypto.SHA384, keySizeBytes: 48, curveBits: 384, curve: elliptic.P384()}, + jose.AlgES512: &ECDSAOptions{Hash: crypto.SHA512, keySizeBytes: 66, curveBits: 521, curve: elliptic.P521()}, +} + +var validSignerOps = []jose.KeyOps{ + jose.KeyOpsSign, +} + +const rsaPrivateKeyPemType = "RSA PRIVATE KEY" + +//NewSigningKey returns a SignignKey for a jose.JWK with required jwk operations +func NewSigningKey(jwk jose.Jwk, required []jose.KeyOps) (SigningKey, error) { + /* Check jwk can be used to sign */ + ops := intersection(validSignerOps, jwk.Ops()) + if len(ops) == 0 { + return nil, ErrInvalidOperations + } + /* Load the jwk */ + k, err := LoadPrivateKey(jwk, required) + if err != nil { + return nil, err + } + + switch jwk.(type) { + case *jose.PrivateRsaKey: + return &SigningKeyImpl{jwk: jwk, key: k, certs: jwk.X5C()}, nil + case *jose.PrivateEcKey: + return &ECDSASigningKey{jwk: jwk, key: k, certs: jwk.X5C()}, nil + default: + return nil, ErrInvalidKeyType + } +} + +//Key returns the crypto.Signer +func (signer *SigningKeyImpl) Key() crypto.Signer { + return signer.key +} + +//Operations returns the allowed operations for the SigningKey +func (signer *SigningKeyImpl) Operations() []jose.KeyOps { + return signer.jwk.Ops() +} + +//Kid returns the jwk id +func (signer *SigningKeyImpl) Kid() string { + /* JIT jwk load. */ + return signer.jwk.Kid() +} + +//Jwk returns the JWK +func (signer *SigningKeyImpl) Jwk() (jose.Jwk, error) { + return signer.jwk, nil +} + +//Algorithm returns the Algorithm +func (signer *SigningKeyImpl) Algorithm() jose.Alg { + return signer.jwk.Alg() +} + +//Marshal marshal the key to a JWK string, or error +func (signer *SigningKeyImpl) Marshal() (string, error) { + return JwkToString(signer.jwk) +} + +//MarshalPem marshal the key to a PEM string, or error +func (signer *SigningKeyImpl) MarshalPem() (string, error) { + var pemType string + var derEncoded []byte + switch k := signer.key.(type) { + case *rsa.PrivateKey: + pemType = rsaPrivateKeyPemType + derEncoded = x509.MarshalPKCS1PrivateKey(k) + default: + return "", ErrUnsupportedKeyType + } + block := pem.Block{ + Type: pemType, + Bytes: derEncoded, + } + output := bytes.Buffer{} + if err := pem.Encode(&output, &block); err != nil { + return "", err + } + return string(output.Bytes()), nil +} + +//Sign perform signing operations on data, or error +func (signer *SigningKeyImpl) Sign(requested jose.KeyOps, data []byte) ([]byte, error) { + /* Verify the operation being requested is supported by the jwk. */ + ops := intersection(validSignerOps, signer.jwk.Ops()) + if !isSubset(ops, []jose.KeyOps{requested}) { + return nil, ErrInvalidOperations + } + /* Calculate digest. */ + digester := algToOptsMap[signer.jwk.Alg()].HashFunc().New() + if _, err := digester.Write(data); err != nil { + logrus.Panicf("%s", err) + } + digest := digester.Sum(nil) + opts := algToOptsMap[signer.jwk.Alg()] + return signer.key.Sign(rand.Reader, digest, opts) +} + +//Certificates of signing key +func (signer *SigningKeyImpl) Certificates() []*x509.Certificate { + return signer.certs +} + +//Verifier verification key for signing jwk +func (signer *SigningKeyImpl) Verifier() (VerificationKey, error) { + publicJwk, err := PublicFromPrivate(signer.jwk) + if err != nil { + return nil, err + } + return NewVerificationKey(publicJwk) +} diff --git a/signer_test.go b/signer_test.go new file mode 100644 index 0000000..6e8ad64 --- /dev/null +++ b/signer_test.go @@ -0,0 +1,277 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "testing" + + "crypto" + + "github.com/thalesignite/gose/jose" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +type MockedJwk struct { + mock.Mock +} + +func (jwk *MockedJwk) Alg() jose.Alg { + args := jwk.Called() + return args.Get(0).(jose.Alg) +} + +func (jwk *MockedJwk) SetAlg(alg jose.Alg) { + jwk.Called(alg) +} + +func (jwk *MockedJwk) Ops() []jose.KeyOps { + args := jwk.Called() + return args.Get(0).([]jose.KeyOps) +} + +func (jwk *MockedJwk) SetOps(ops []jose.KeyOps) { + jwk.Called(ops) +} + +func (jwk *MockedJwk) Kid() string { + args := jwk.Called() + return args.String(0) +} + +func (jwk *MockedJwk) SetKid(kid string) { + jwk.Called(kid) +} + +func (jwk *MockedJwk) Kty() string { + args := jwk.Called() + return args.String(0) +} + +func (jwk *MockedJwk) SetKty(kty string) { + jwk.Called(kty) +} + +func (jwk *MockedJwk) Use() jose.KeyUse { + args := jwk.Called() + return args.Get(0).(jose.KeyUse) +} + +func (jwk *MockedJwk) SetUse(use jose.KeyUse) { + jwk.Called(use) +} + +func (jwk *MockedJwk) X5C() []*x509.Certificate { + args := jwk.Called() + return args.Get(0).([]*x509.Certificate) +} + +func (jwk *MockedJwk) SetX5C(certs []*x509.Certificate) { + jwk.Called(certs) +} + +type MockedSigner struct { + mock.Mock +} + +func (signer *MockedSigner) Key() crypto.Signer { + args := signer.Called() + return args.Get(0).(crypto.Signer) +} + +func (signer *MockedSigner) Algorithm() jose.Alg { + args := signer.Called() + return args.Get(0).(jose.Alg) +} + +func (signer *MockedSigner) Jwk() (jose.Jwk, error) { + args := signer.Called() + return args.Get(0).(jose.Jwk), args.Error(1) +} + +func (signer *MockedSigner) Sign(operation jose.KeyOps, payload []byte) (signature []byte, err error) { + args := signer.Called(operation, payload) + sig := args.Get(0) + err = args.Error(1) + if sig != nil { + signature = sig.([]byte) + } + return +} + +func (signer *MockedSigner) Marshal() (string, error) { + args := signer.Called() + return args.String(0), args.Error(1) +} + +func (signer *MockedSigner) MarshalPem() (string, error) { + args := signer.Called() + return args.String(0), args.Error(1) +} + +func (signer *MockedSigner) Certificates() []*x509.Certificate { + args := signer.Called() + return args.Get(0).([]*x509.Certificate) +} + +func (signer *MockedSigner) Kid() string { + args := signer.Called() + return args.Get(0).(string) +} + +func (signer *MockedSigner) Verifier() (VerificationKey, error) { + args := signer.Called() + return args.Get(0).(VerificationKey), args.Error(1) +} + +func TestNewSigningKey_Succeeds(t *testing.T) { + // Setup + k, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + jwk, err := JwkFromPrivateKey(k, []jose.KeyOps{jose.KeyOpsSign}, nil) + require.NoError(t, err) + + // Act + signer, err := NewSigningKey(jwk, []jose.KeyOps{jose.KeyOpsSign}) + + // Assert + require.NoError(t, err) + require.NotNil(t, signer) +} + +func TestNewSigningKey_FailsWhenInvalidOperations(t *testing.T) { + // Setup + k, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + testCase := [][]jose.KeyOps{ + {jose.KeyOpsVerify}, + {jose.KeyOpsSign, jose.KeyOpsVerify}, + } + for _, test := range testCase { + jwk, err := JwkFromPrivateKey(k, []jose.KeyOps{jose.KeyOpsSign}, nil) + require.NoError(t, err) + + // Act + signer, err := NewSigningKey(jwk, test) + + // Assert + require.Nil(t, signer) + require.Equal(t, ErrInvalidOperations, err) + } +} + +func TestNewSigningKey_FailsWhenInvalidJwk(t *testing.T) { + // Setup + k, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + jwk, err := JwkFromPublicKey(k.Public(), []jose.KeyOps{jose.KeyOpsSign}, nil) + require.NoError(t, err) + + // Act + signer, err := NewSigningKey(jwk, []jose.KeyOps{jose.KeyOpsSign}) + + // Assert + require.Error(t, err) + require.Nil(t, signer) +} + +func TestNewSigningKey_MarshalSucceeds(t *testing.T) { + // Setup + k, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + jwk, err := JwkFromPrivateKey(k, []jose.KeyOps{jose.KeyOpsSign}, nil) + require.NoError(t, err) + signer, err := NewSigningKey(jwk, []jose.KeyOps{jose.KeyOpsSign}) + require.NoError(t, err) + + // Act + marshalled, err := signer.Marshal() + + // Assert + require.NoError(t, err) + require.NotNil(t, marshalled) + unmarshalledJwk, err := LoadJwk(bytes.NewReader([]byte(marshalled)), nil) + require.NoError(t, err) + require.Equal(t, jwk.Alg(), unmarshalledJwk.Alg()) + require.Equal(t, jwk.Ops(), unmarshalledJwk.Ops()) + require.Equal(t, jwk.Kty(), unmarshalledJwk.Kty()) + require.Equal(t, jwk.Kid(), unmarshalledJwk.Kid()) +} + +func TestNewSigningKey_MarshalPemSucceeds(t *testing.T) { + // Setup + k, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + jwk, err := JwkFromPrivateKey(k, []jose.KeyOps{jose.KeyOpsSign}, nil) + require.NoError(t, err) + signer, err := NewSigningKey(jwk, []jose.KeyOps{jose.KeyOpsSign}) + require.NoError(t, err) + + // Act + marshalled, err := signer.MarshalPem() + + // Assert + require.NoError(t, err) + require.NotEmpty(t, marshalled) + block, overflow := pem.Decode([]byte(marshalled)) + require.Empty(t, overflow) + require.Equal(t, block.Type, rsaPrivateKeyPemType) + recoveredKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) + require.NoError(t, err) + assert.Equal(t, recoveredKey.E, k.E) + assert.Equal(t, recoveredKey.N.Cmp(k.N), 0) + assert.Equal(t, recoveredKey.D.Cmp(k.D), 0) + assert.Equal(t, recoveredKey.N.Cmp(k.N), 0) +} + +func TestSigningKeyImpl_Verifier(t *testing.T) { + // Setup + k, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + jwk, err := JwkFromPrivateKey(k, []jose.KeyOps{jose.KeyOpsSign}, nil) + require.NoError(t, err) + signer, err := NewSigningKey(jwk, []jose.KeyOps{jose.KeyOpsSign}) + require.NoError(t, err) + + // Act + verifier, err := signer.Verifier() + + // Assert + assert.NoError(t, err) + assert.NotNil(t, verifier) + assert.Equal(t, verifier.Kid(), signer.Kid()) + + // Sign something, then verify + testData := make([]byte, 10) + _, err = rand.Read(testData) + require.NoError(t, err) + signature, err := signer.Sign(jose.KeyOpsSign, testData) + require.NoError(t, err) + matches := verifier.Verify(jose.KeyOpsVerify, testData, signature) + assert.True(t, matches) +} diff --git a/verifier.go b/verifier.go new file mode 100644 index 0000000..ac5b424 --- /dev/null +++ b/verifier.go @@ -0,0 +1,180 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "bytes" + "crypto" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "math" + + "github.com/thalesignite/gose/jose" + "github.com/sirupsen/logrus" +) + +//RsaVerificationKeyImpl implements RSA verification API +type RsaVerificationKeyImpl struct { + key rsa.PublicKey + ops []jose.KeyOps + alg jose.Alg + opts crypto.SignerOpts + id string + certs []*x509.Certificate +} + +const rsaPublicKeyPemType = "RSA PUBLIC KEY" + +var ( + validVerificationOps = []jose.KeyOps{ + jose.KeyOpsVerify, + } + + pssAlgs = map[jose.Alg]bool{ + jose.AlgPS256: true, + jose.AlgPS384: true, + jose.AlgPS512: true, + } +) + +//Kid returns the key's id +func (verifier *RsaVerificationKeyImpl) Kid() string { + return verifier.id +} + +//Algorithm returns algorithm +func (verifier *RsaVerificationKeyImpl) Algorithm() jose.Alg { + return verifier.alg +} + +//Jwk returns the public JWK +func (verifier *RsaVerificationKeyImpl) Jwk() (jose.Jwk, error) { + jwk, err := JwkFromPublicKey(&verifier.key, verifier.ops, verifier.certs) + if err != nil { + return nil, err + } + jwk.SetAlg(verifier.alg) + return jwk, nil +} + +//Marshal returns the key marshalled to a JWK string, or error +func (verifier *RsaVerificationKeyImpl) Marshal() (string, error) { + jwk, err := verifier.Jwk() + if err != nil { + return "", err + } + return JwkToString(jwk) +} + +//MarshalPem returns the key marshalled to a PEM string, or error +func (verifier *RsaVerificationKeyImpl) MarshalPem() (string, error) { + derEncoded, err := x509.MarshalPKIXPublicKey(&verifier.key) + if err != nil { + return "", err + } + + block := pem.Block{ + Type: rsaPublicKeyPemType, + Bytes: derEncoded, + } + output := bytes.Buffer{} + if err := pem.Encode(&output, &block); err != nil { + return "", err + } + return string(output.Bytes()), nil +} + +//Verify data matches signature +func (verifier *RsaVerificationKeyImpl) Verify(operation jose.KeyOps, data []byte, signature []byte) bool { + ops := intersection(validVerificationOps, verifier.ops) + if !isSubset(ops, []jose.KeyOps{operation}) { + return false + } + digester := verifier.opts.HashFunc().New() + if _, err := digester.Write(data); err != nil { + logrus.Panicf("%s", err) + } + digest := digester.Sum(nil) + var err error + if _, ok := pssAlgs[verifier.alg]; ok { + err = rsa.VerifyPSS(&verifier.key, verifier.opts.HashFunc(), digest, signature, verifier.opts.(*rsa.PSSOptions)) + } else { + err = rsa.VerifyPKCS1v15(&verifier.key, verifier.opts.HashFunc(), digest, signature) + } + return err == nil +} + +//Certificates for verification key +func (verifier *RsaVerificationKeyImpl) Certificates() []*x509.Certificate { + return verifier.certs +} + +//NewVerificationKey for jwk or error +func NewVerificationKey(jwk jose.Jwk) (VerificationKey, error) { + /* Check jwk can be used to verify */ + ops := validVerificationOps + if len(jwk.Ops()) > 0 { + ops = intersection(validVerificationOps, jwk.Ops()) + if len(ops) == 0 { + return nil, ErrInvalidOperations + } + } + certs := jwk.X5C() + switch v := jwk.(type) { + case *jose.PublicRsaKey: + if jwk.Alg() == jose.AlgPS256 || jwk.Alg() == jose.AlgPS384 || jwk.Alg() == jose.AlgPS512 || + jwk.Alg() == jose.AlgRS256 || jwk.Alg() == jose.AlgRS384 || jwk.Alg() == jose.AlgRS512 { + if v.E.Int().Int64() > math.MaxInt32 { + return nil, ErrInvalidExponent + } + var result RsaVerificationKeyImpl + result.key.N = v.N.Int() + result.key.E = int(v.E.Int().Int64()) + result.opts = algToOptsMap[jwk.Alg()] + result.id = jwk.Kid() + result.certs = certs + result.ops = ops + result.alg = v.Alg() + return &result, nil + } + return nil, ErrUnsupportedKeyType + case *jose.PublicEcKey: + if !(jwk.Alg() == jose.AlgES256 || jwk.Alg() == jose.AlgES384 || jwk.Alg() == jose.AlgES512) { + return nil, ErrUnsupportedKeyType + } + var result ECVerificationKeyImpl + result.key.X = v.X.Int() + result.key.Y = v.Y.Int() + + result.opts = algToOptsMap[jwk.Alg()].(*ECDSAOptions) + result.key.Curve = result.opts.curve + result.id = jwk.Kid() + result.certs = certs + result.ops = ops + result.alg = v.Alg() + return &result, nil + // TODO: add symmetric verification. + default: + return nil, ErrUnsupportedKeyType + } +} diff --git a/verifier_test.go b/verifier_test.go new file mode 100644 index 0000000..3e07929 --- /dev/null +++ b/verifier_test.go @@ -0,0 +1,274 @@ +// Copyright 2019 Thales e-Security, Inc +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package gose + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "testing" + + "github.com/thalesignite/gose/jose" + "github.com/bouk/monkey" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewVerifierSucceeds(t *testing.T) { + // Setup + rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + jwk, err := JwkFromPublicKey(rsaKey.Public(), []jose.KeyOps{jose.KeyOpsVerify}, nil) + require.NoError(t, err) + + cases := []jose.Alg{ + jose.AlgPS256, + jose.AlgRS256, + } + + // Act + for _, test := range cases { + jwk.SetAlg(test) + k, err := NewVerificationKey(jwk) + + // Assert + require.Nil(t, err) + require.NotNil(t, k) + require.NotEmpty(t, k.Kid()) + require.Equal(t, test, jwk.Alg()) + } +} + +func TestNewVerifierFailsWithInvalidOps(t *testing.T) { + // Setup + rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + testCase := [][]jose.KeyOps{ + {jose.KeyOpsSign}, + } + for _, test := range testCase { + jwk, err := JwkFromPublicKey(rsaKey.Public(), test, nil) + require.NoError(t, err) + + // Act + k, err := NewVerificationKey(jwk) + + // Assert + assert.Nil(t, k) + assert.Equal(t, ErrInvalidOperations, err) + } +} + +func TestNewVerifierMarshalSucceeds(t *testing.T) { + // Setup + require.NotNil(t, rand.Reader) + rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + require.Nil(t, rsaKey.Validate()) + jwk, err := JwkFromPublicKey(rsaKey.Public(), []jose.KeyOps{jose.KeyOpsVerify}, nil) + require.NoError(t, err) + k, err := NewVerificationKey(jwk) + require.NoError(t, err) + + // Act + str, err := k.Marshal() + + // Assert + require.NoError(t, err) + require.NotEmpty(t, str) + buf := bytes.NewReader([]byte(str)) + jwkOut, err := LoadJwk(buf, nil) + require.Equal(t, jwk.Kid(), jwkOut.Kid()) + require.Equal(t, jwk.Kty(), jwkOut.Kty()) + require.Equal(t, jwk.Alg(), jwkOut.Alg()) + require.Equal(t, jwk.Ops(), jwkOut.Ops()) + + goodRsaJwk := jwk.(*jose.PublicRsaKey) + marshalledRsaJwk := jwkOut.(*jose.PublicRsaKey) + require.Equal(t, goodRsaJwk.N, marshalledRsaJwk.N) + require.Equal(t, goodRsaJwk.E, marshalledRsaJwk.E) +} + +func TestNewVerifierMarshalPemSucceeds(t *testing.T) { + // Setup + rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + jwk, err := JwkFromPublicKey(rsaKey.Public(), []jose.KeyOps{jose.KeyOpsVerify}, nil) + require.NoError(t, err) + k, err := NewVerificationKey(jwk) + require.NoError(t, err) + + // Act + str, err := k.MarshalPem() + + // Assert + require.NoError(t, err) + require.NotEmpty(t, str) + block, overflow := pem.Decode([]byte(str)) + require.Empty(t, overflow) + require.Equal(t, block.Type, rsaPublicKeyPemType) + recoveredKey, err := x509.ParsePKIXPublicKey(block.Bytes) + require.NoError(t, err) + recoveredRsaKey, ok := recoveredKey.(*rsa.PublicKey) + require.True(t, ok) + assert.Equal(t, recoveredRsaKey.E, rsaKey.E) + assert.Equal(t, recoveredRsaKey.N.Cmp(rsaKey.N), 0) +} + +func TestNewVerifierFailsWhenNotAVerfierKey(t *testing.T) { + // Setup + var jwk jose.PrivateRsaKey + jwk.SetAlg(jose.AlgPS256) + jwk.SetOps([]jose.KeyOps{jose.KeyOpsSign}) + + // Act + k, err := NewVerificationKey(&jwk) + + // Assert + require.Equal(t, ErrInvalidOperations, err) + require.Nil(t, k) +} + +func TestNewVerifierFailsWhenRsaExponentIsInvalid(t *testing.T) { + // Setup + var jwk jose.PublicRsaKey + jwk.SetAlg(jose.AlgPS256) + jwk.SetOps([]jose.KeyOps{jose.KeyOpsVerify}) + jwk.N.SetBytes([]byte("AABABB")) + jwk.E.SetBytes([]byte("=====")) + + // Act + k, err := NewVerificationKey(&jwk) + + // Assert + require.Equal(t, ErrInvalidExponent, err) + require.Nil(t, k) +} + +func TestNewVerifierVerifyPSSSucceeds(t *testing.T) { + // Setup + var jwk jose.PublicRsaKey + jwk.SetAlg(jose.AlgPS256) + jwk.SetOps([]jose.KeyOps{jose.KeyOpsVerify}) + jwk.N.SetBytes([]byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ")) + jwk.E.SetBytes([]byte("AQAB")) + + defer monkey.Patch(rsa.VerifyPSS, + func(pub *rsa.PublicKey, hash crypto.Hash, hashed []byte, sig []byte, opts *rsa.PSSOptions) error { + return nil + }).Unpatch() + + k, err := NewVerificationKey(&jwk) + + // Assert + require.Nil(t, err) + require.NotNil(t, k) + + // Act + result := k.Verify(jose.KeyOpsVerify, []byte("1234"), []byte("5678")) + + // Assert + require.True(t, result) +} + +func TestNewVerifierVerifyPkcs15Succeeds(t *testing.T) { + // Setup + var jwk jose.PublicRsaKey + jwk.SetAlg(jose.AlgRS256) + jwk.SetOps([]jose.KeyOps{jose.KeyOpsVerify}) + jwk.N.SetBytes([]byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ")) + jwk.E.SetBytes([]byte("AQAB")) + + defer monkey.Patch(rsa.VerifyPKCS1v15, + func(pub *rsa.PublicKey, hash crypto.Hash, hashed []byte, sig []byte) error { + return nil + }).Unpatch() + + k, err := NewVerificationKey(&jwk) + + // Assert + require.Nil(t, err) + require.NotNil(t, k) + + // Act + result := k.Verify(jose.KeyOpsVerify, []byte("1234"), []byte("5678")) + + // Assert + require.True(t, result) +} + +func TestNewVerifierVerifyFailsWhenPSSCryptoErrors(t *testing.T) { + // Setup + var jwk jose.PublicRsaKey + jwk.SetAlg(jose.AlgPS256) + jwk.SetOps([]jose.KeyOps{jose.KeyOpsVerify}) + jwk.N.SetBytes([]byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ")) + jwk.E.SetBytes([]byte("AQAB")) + + defer monkey.Patch(rsa.VerifyPSS, + func(pub *rsa.PublicKey, hash crypto.Hash, hashed []byte, sig []byte, options *rsa.PSSOptions) error { + return errors.New("Expected error") + }).Unpatch() + + k, err := NewVerificationKey(&jwk) + + // Assert + require.Nil(t, err) + require.NotNil(t, k) + + // Act + result := k.Verify(jose.KeyOpsVerify, []byte("1234"), []byte("5678")) + + // Assert + require.False(t, result) +} + +func TestNewVerifierVerifyFailsWhenPkcs15CryptoErrors(t *testing.T) { + // Setup + var jwk jose.PublicRsaKey + jwk.SetAlg(jose.AlgPS256) + jwk.SetOps([]jose.KeyOps{jose.KeyOpsVerify}) + jwk.N.SetBytes([]byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ")) + jwk.E.SetBytes([]byte("AQAB")) + + defer monkey.Patch(rsa.VerifyPKCS1v15, + func(pub *rsa.PublicKey, hash crypto.Hash, hashed []byte, sig []byte) error { + return errors.New("Expected error") + }).Unpatch() + + k, err := NewVerificationKey(&jwk) + + // Assert + require.Nil(t, err) + require.NotNil(t, k) + + // Act + result := k.Verify(jose.KeyOpsVerify, []byte("1234"), []byte("5678")) + + // Assert + require.False(t, result) +}