Skip to content

Commit

Permalink
Update WithValidateKey, and add tests+docs
Browse files Browse the repository at this point in the history
  • Loading branch information
lestrrat committed Oct 27, 2023
1 parent dc881e4 commit bde608c
Show file tree
Hide file tree
Showing 10 changed files with 148 additions and 23 deletions.
11 changes: 11 additions & 0 deletions Changes
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ v2.0.16 UNRELEASED
it only means that the keys are in the right _format_, including the presence
of required fields and that certain fields are have proper length, etc.

[New Features]
* [jws] Added `jws.WithValidateKey()` to force calling `key.Validate()` before
signing or verification.

* [jws] `jws.Sign()` now returns a special type of error that can hold the
individual errors from the signers. The stringification is still the same
as before to preserve backwards compatibility.

* [jwk] Added `jwk.IsKeyValidationError` that checks if an error is an error
from `key.Validate()`.

v2.0.15 19 20 Oct 2023
[Bug fixes]
* [jws] jws.Sign() now properly check for valid algorithm / key type pair when
Expand Down
4 changes: 2 additions & 2 deletions jwk/ecdsa.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,14 +260,14 @@ func ecdsaValidateKey(k interface {

func (k *ecdsaPrivateKey) Validate() error {
if err := ecdsaValidateKey(k, true); err != nil {
return fmt.Errorf(`jwk.ECDSAPrivateKey: failed to validate key: %w`, err)
return NewKeyValidationError(fmt.Errorf(`jwk.ECDSAPrivateKey: %w`, err))
}
return nil
}

func (k *ecdsaPublicKey) Validate() error {
if err := ecdsaValidateKey(k, false); err != nil {
return fmt.Errorf(`jwk.ECDSAPublicKey: failed to validate key: %w`, err)
return NewKeyValidationError(fmt.Errorf(`jwk.ECDSAPublicKey: %w`, err))
}
return nil
}
30 changes: 30 additions & 0 deletions jwk/jwk.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io"
"math/big"
Expand Down Expand Up @@ -757,3 +758,32 @@ func IsPrivateKey(k Key) (bool, error) {
}
return false, fmt.Errorf("jwk.IsPrivateKey: %T is not an asymmetric key", k)
}

type keyValidationErr struct {
err error
}

func (e *keyValidationErr) Error() string {
return fmt.Sprintf(`key validation failed: %s`, e.err)
}

func (e *keyValidationErr) Unwrap() error {
return e.err
}

func (e *keyValidationErr) Is(target error) bool {
_, ok := target.(*keyValidationErr)
return ok
}

// NewKeyValidationError wraps the given error with an error that denotes
// `key.Validate()` has failed. This error type should ONLY be used as
// return value from the `Validate()` method.
func NewKeyValidationError(err error) error {
return &keyValidationErr{err: err}
}

func IsKeyValidationError(err error) bool {
var kve keyValidationErr
return errors.Is(err, &kve)
}
10 changes: 8 additions & 2 deletions jwk/okp.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,11 +211,17 @@ func validateOKPKey(key interface {
func (k *okpPublicKey) Validate() error {
k.mu.RLock()
defer k.mu.RUnlock()
return validateOKPKey(k)
if err := validateOKPKey(k); err != nil {
return NewKeyValidationError(fmt.Errorf(`jwk.OKPPublicKey: %w`, err))
}
return nil
}

func (k *okpPrivateKey) Validate() error {
k.mu.RLock()
defer k.mu.RUnlock()
return validateOKPKey(k)
if err := validateOKPKey(k); err != nil {
return NewKeyValidationError(fmt.Errorf(`jwk.OKPPrivateKey: %w`, err))
}
return nil
}
4 changes: 2 additions & 2 deletions jwk/rsa.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,14 +270,14 @@ func validateRSAKey(key interface {

func (k *rsaPrivateKey) Validate() error {
if err := validateRSAKey(k, true); err != nil {
return fmt.Errorf(`jwk.RSAPrivateKey: failed to validate key: %w`, err)
return NewKeyValidationError(fmt.Errorf(`jwk.RSAPrivateKey: %w`, err))
}
return nil
}

func (k *rsaPublicKey) Validate() error {
if err := validateRSAKey(k, false); err != nil {
return fmt.Errorf(`jwk.RSAPublicKey: failed to validate key: %w`, err)
return NewKeyValidationError(fmt.Errorf(`jwk.RSAPublicKey: %w`, err))
}
return nil
}
2 changes: 1 addition & 1 deletion jwk/symmetric.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (k *symmetricKey) PublicKey() (Key, error) {

func (k *symmetricKey) Validate() error {
if len(k.Octets()) == 0 {
return fmt.Errorf(`missing "k" field`)
return NewKeyValidationError(fmt.Errorf(`jwk.SymmetricKey: missing "k" field`))
}
return nil
}
67 changes: 56 additions & 11 deletions jws/jws.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"errors"
"fmt"
"io"
"reflect"
Expand Down Expand Up @@ -107,6 +108,21 @@ const (
var _ = fmtInvalid
var _ = fmtMax

func validateKeyBeforeUse(key interface{}) error {
jwkKey, ok := key.(jwk.Key)
if !ok {
converted, err := jwk.FromRaw(key)
if err != nil {
return fmt.Errorf(`could not convert key of type %T to jwk.Key for validation: %w`, key, err)
}
jwkKey = converted
}
if err := jwkKey.Validate(); err != nil {

Check warning on line 120 in jws/jws.go

View workflow job for this annotation

GitHub Actions / lint

if-return: redundant if ...; err != nil check, just return error instead. (revive)
return err
}
return nil
}

// Sign generates a JWS message for the given payload and returns
// it in serialized form, which can be in either compact or
// JSON format. Default is compact.
Expand Down Expand Up @@ -149,6 +165,7 @@ func Sign(payload []byte, options ...SignOption) ([]byte, error) {
var signers []*payloadSigner
var detached bool
var noneSignature *payloadSigner
var validateKey bool
for _, option := range options {
//nolint:forcetypeassert
switch option.Ident() {
Expand Down Expand Up @@ -185,6 +202,8 @@ func Sign(payload []byte, options ...SignOption) ([]byte, error) {
return nil, fmt.Errorf(`jws.Sign: payload must be nil when jws.WithDetachedPayload() is specified`)
}
payload = option.Value().([]byte)
case identValidateKey{}:
validateKey = option.Value().(bool)
}
}

Expand Down Expand Up @@ -239,6 +258,12 @@ func Sign(payload []byte, options ...SignOption) ([]byte, error) {
// cheat. FIXXXXXXMEEEEEE
detached: detached,
}

if validateKey {
if err := validateKeyBeforeUse(signer.key); err != nil {
return nil, fmt.Errorf(`jws.Verify: %w`, err)
}
}
_, _, err := sig.Sign(payload, signer.signer, signer.key)
if err != nil {
return nil, fmt.Errorf(`failed to generate signature for signer #%d (alg=%s): %w`, i, signer.Algorithm(), err)
Expand Down Expand Up @@ -353,6 +378,7 @@ func Verify(buf []byte, options ...VerifyOption) ([]byte, error) {
verifyBuf := pool.GetBytesBuffer()
defer pool.ReleaseBytesBuffer(verifyBuf)

var errs []error
for i, sig := range msg.signatures {
verifyBuf.Reset()

Expand Down Expand Up @@ -391,16 +417,8 @@ func Verify(buf []byte, options ...VerifyOption) ([]byte, error) {
key := pair.key

if validateKey {
jwkKey, ok := key.(jwk.Key)
if !ok {
converted, err := jwk.FromRaw(key)
if err != nil {
return nil, fmt.Errorf(`jws.Verify: could not convert key of type %T to jwk.Key for validation: %w`, key, err)
}
jwkKey = converted
}
if err := jwkKey.Validate(); err != nil {
return nil, fmt.Errorf(`jws.Verify: failed to validate key: %w`, err)
if err := validateKeyBeforeUse(key); err != nil {
return nil, fmt.Errorf(`jws.Verify: %w`, err)
}
}
verifier, err := NewVerifier(alg)
Expand All @@ -409,6 +427,7 @@ func Verify(buf []byte, options ...VerifyOption) ([]byte, error) {
}

if err := verifier.Verify(verifyBuf.Bytes(), sig.signature, key); err != nil {
errs = append(errs, err)
continue
}

Expand All @@ -426,7 +445,33 @@ func Verify(buf []byte, options ...VerifyOption) ([]byte, error) {
}
}
}
return nil, fmt.Errorf(`could not verify message using any of the signatures or keys`)
return nil, &verifyError{errs: errs}
}

type verifyError struct {
// Note: when/if we can ditch Go < 1.20, we can change this to a simple
// `err error`, where the value is the result of `errors.Join()`
//
// We also need to implement Unwrap:
// func (e *verifyError) Unwrap() error {
// return e.err
//}
//
// And finally, As() can go away
errs []error
}

func (e *verifyError) Error() string {
return `could not verify message using any of the signatures or keys`
}

func (e *verifyError) As(target interface{}) bool {
for _, err := range e.errs {
if errors.As(err, target) {
return true
}
}
return false
}

// get the value of b64 header field.
Expand Down
33 changes: 33 additions & 0 deletions jws/jws_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2121,3 +2121,36 @@ func TestUnpaddedSignatureR(t *testing.T) {
_, err = jws.Verify([]byte(unpadded), jws.WithKey(jwa.ES256, pubKey))
require.Error(t, err, `jws.Verify should fail`)
}

func TestValidateKey(t *testing.T) {
privKey, err := jwxtest.GenerateRsaJwk()
require.NoError(t, err, `jwxtest.GenerateRsaJwk should succeed`)

signed, err := jws.Sign([]byte("Lorem Ipsum"), jws.WithKey(jwa.RS256, privKey), jws.WithValidateKey(true))
require.NoError(t, err, `jws.Sign should succeed`)

// This should fail because D is empty
require.NoError(t, privKey.Set(jwk.RSADKey, []byte(nil)), `jwk.Set should succeed`)
_, err = jws.Sign([]byte("Lorem Ipsum"), jws.WithKey(jwa.RS256, privKey), jws.WithValidateKey(true))
require.Error(t, err, `jws.Sign should fail`)

pubKey, err := jwk.PublicKeyOf(privKey)
require.NoError(t, err, `jwk.PublicKeyOf should succeed`)

n := pubKey.(jwk.RSAPublicKey).N()

// Set N to an empty value
require.NoError(t, pubKey.Set(jwk.RSANKey, []byte(nil)), `jwk.Set should succeed`)

// This is going to fail regardless, because the public key is now
// invalid (empty N), but we want to make sure that it fails because
// of the validation failing
_, err = jws.Verify(signed, jws.WithKey(jwa.RS256, pubKey), jws.WithValidateKey(true))
require.Error(t, err, `jws.Verify should fail`)
require.True(t, jwk.IsKeyValidationError(err), `jwk.IsKeyValidationError should return true`)

// The following should now succeed, because N has been reinstated
require.NoError(t, pubKey.Set(jwk.RSANKey, n), `jwk.Set should succeed`)
_, err = jws.Verify(signed, jws.WithKey(jwa.RS256, pubKey), jws.WithValidateKey(true))
require.NoError(t, err, `jws.Verify should succeed`)
}
4 changes: 2 additions & 2 deletions jws/options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@ options:
have provided are instances of `jwk.Key` (remember that the
jwx API allows users to specify a raw key such as *rsa.PublicKey)
- ident: ValidateKey
interface: VerifyOption
interface: SignVerifyOption
argument_type: bool
comment: |
WithValidateKey specifies whether the key used for verification
WithValidateKey specifies whether the key used for signing or verification
should be validated before using. Note that this means calling
`key.Validate()` on the key, which in turn means that your key
must be a `jwk.Key` instance, or a key that can be converted to
Expand Down
6 changes: 3 additions & 3 deletions jws/options_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit bde608c

Please sign in to comment.