Skip to content

Commit

Permalink
Work with crypto.Signer (lestrrat-go#452)
Browse files Browse the repository at this point in the history
* Accept crypto.Signer for Ed25519 signing

* Add crypto.Signer compatiblity for ECDSA

* appease linter

* Implement crypto.Signer compatibility for RSA

* Add documentation
  • Loading branch information
lestrrat authored Aug 24, 2021
1 parent b548fca commit fd774b7
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 220 deletions.
3 changes: 3 additions & 0 deletions Changes
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ Changes
=======

v1.2.6 (NOT YET RELEASED)
[New features]
* Support `crypto.Signer` keys for RSA, ECDSA, and EdDSA family
of signatures in `jws.Sign`
[Miscellaneous]
* `jwx.GuessFormat()` now requires the presense of both `payload` and
`signatures` keys for it to guess that a JSON object is a JWS message.
Expand Down
170 changes: 105 additions & 65 deletions jws/ecdsa.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"io"

"github.com/lestrrat-go/jwx/internal/keyconv"
"github.com/lestrrat-go/jwx/internal/pool"
"github.com/lestrrat-go/jwx/jwa"
"github.com/pkg/errors"
)

var ecdsaSignFuncs = map[jwa.SignatureAlgorithm]ecdsaSignFunc{}
var ecdsaVerifyFuncs = map[jwa.SignatureAlgorithm]ecdsaVerifyFunc{}
var ecdsaSigners map[jwa.SignatureAlgorithm]*ecdsaSigner
var ecdsaVerifiers map[jwa.SignatureAlgorithm]*ecdsaVerifier

func init() {
algs := map[jwa.SignatureAlgorithm]crypto.Hash{
Expand All @@ -21,97 +22,119 @@ func init() {
jwa.ES512: crypto.SHA512,
jwa.ES256K: crypto.SHA256,
}
ecdsaSigners = make(map[jwa.SignatureAlgorithm]*ecdsaSigner)
ecdsaVerifiers = make(map[jwa.SignatureAlgorithm]*ecdsaVerifier)

for alg, h := range algs {
ecdsaSignFuncs[alg] = makeECDSASignFunc(h)
ecdsaVerifyFuncs[alg] = makeECDSAVerifyFunc(h)
}
}

func makeECDSASignFunc(hash crypto.Hash) ecdsaSignFunc {
return func(payload []byte, key *ecdsa.PrivateKey) ([]byte, error) {
curveBits := key.Curve.Params().BitSize
keyBytes := curveBits / 8
// Curve bits do not need to be a multiple of 8.
if curveBits%8 > 0 {
keyBytes++
for alg, hash := range algs {
ecdsaSigners[alg] = &ecdsaSigner{
alg: alg,
hash: hash,
}
h := hash.New()
if _, err := h.Write(payload); err != nil {
return nil, errors.Wrap(err, "failed to write payload using ecdsa")
ecdsaVerifiers[alg] = &ecdsaVerifier{
alg: alg,
hash: hash,
}
r, s, err := ecdsa.Sign(rand.Reader, key, h.Sum(nil))
if err != nil {
return nil, errors.Wrap(err, "failed to sign payload using ecdsa")
}

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)

out := append(rBytesPadded, sBytesPadded...)
return out, nil
}
}

func newECDSASigner(alg jwa.SignatureAlgorithm) Signer {
return &ECDSASigner{
alg: alg,
sign: ecdsaSignFuncs[alg], // we know this will succeed
}
return ecdsaSigners[alg]
}

func (s ECDSASigner) Algorithm() jwa.SignatureAlgorithm {
// ecdsaSigners are immutable.
type ecdsaSigner struct {
alg jwa.SignatureAlgorithm
hash crypto.Hash
}

func (s ecdsaSigner) Algorithm() jwa.SignatureAlgorithm {
return s.alg
}

func (s ECDSASigner) Sign(payload []byte, key interface{}) ([]byte, error) {
type ecdsaCryptoSigner struct {
key *ecdsa.PrivateKey
hash crypto.Hash
}

func (s *ecdsaSigner) Sign(payload []byte, key interface{}) ([]byte, error) {
if key == nil {
return nil, errors.New(`missing private key while signing payload`)
}

var privkey ecdsa.PrivateKey
if err := keyconv.ECDSAPrivateKey(&privkey, key); err != nil {
return nil, errors.Wrapf(err, `failed to retrieve ecdsa.PrivateKey out of %T`, key)
signer, ok := key.(crypto.Signer)
if ok {
// We support crypto.Signer, but we DON'T support
// ecdsa.PrivateKey as a crypto.Signer, because it encodes
// the result in ASN1 format.
if pk, ok := key.(*ecdsa.PrivateKey); ok {
signer = newECDSACryptoSigner(pk, s.hash)
}
} else {
var privkey ecdsa.PrivateKey
if err := keyconv.ECDSAPrivateKey(&privkey, key); err != nil {
return nil, errors.Wrapf(err, `failed to retrieve ecdsa.PrivateKey out of %T`, key)
}
signer = newECDSACryptoSigner(&privkey, s.hash)
}

return s.sign(payload, &privkey)
h := s.hash.New()
if _, err := h.Write(payload); err != nil {
return nil, errors.Wrap(err, "failed to write payload using ecdsa")
}
return signer.Sign(rand.Reader, h.Sum(nil), s.hash)
}

func makeECDSAVerifyFunc(hash crypto.Hash) ecdsaVerifyFunc {
return func(payload []byte, signature []byte, key *ecdsa.PublicKey) error {
r := pool.GetBigInt()
s := pool.GetBigInt()
defer pool.ReleaseBigInt(r)
defer pool.ReleaseBigInt(s)
func newECDSACryptoSigner(key *ecdsa.PrivateKey, hash crypto.Hash) crypto.Signer {
return &ecdsaCryptoSigner{
key: key,
hash: hash,
}
}

n := len(signature) / 2
r.SetBytes(signature[:n])
s.SetBytes(signature[n:])
func (cs *ecdsaCryptoSigner) Public() crypto.PublicKey {
return cs.key.PublicKey
}

h := hash.New()
if _, err := h.Write(payload); err != nil {
return errors.Wrap(err, "failed to write payload using ecdsa")
}
func (cs *ecdsaCryptoSigner) Sign(seed io.Reader, digest []byte, _ crypto.SignerOpts) ([]byte, error) {
r, s, err := ecdsa.Sign(seed, cs.key, digest)
if err != nil {
return nil, errors.Wrap(err, "failed to sign payload using ecdsa")
}

if !ecdsa.Verify(key, h.Sum(nil), r, s) {
return errors.New(`failed to verify signature using ecdsa`)
}
return nil
curveBits := cs.key.Curve.Params().BitSize
keyBytes := curveBits / 8
// Curve bits do not need to be a multiple of 8.
if curveBits%8 > 0 {
keyBytes++
}

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)

out := append(rBytesPadded, sBytesPadded...)
return out, nil
}

// ecdsaVerifiers are immutable.
type ecdsaVerifier struct {
alg jwa.SignatureAlgorithm
hash crypto.Hash
}

func newECDSAVerifier(alg jwa.SignatureAlgorithm) Verifier {
return &ECDSAVerifier{
verify: ecdsaVerifyFuncs[alg], // we know this will succeed
}
return ecdsaVerifiers[alg]
}

func (v ecdsaVerifier) Algorithm() jwa.SignatureAlgorithm {
return v.alg
}

func (v ECDSAVerifier) Verify(payload []byte, signature []byte, key interface{}) error {
func (v *ecdsaVerifier) Verify(payload []byte, signature []byte, key interface{}) error {
if key == nil {
return errors.New(`missing public key while verifying payload`)
}
Expand All @@ -121,5 +144,22 @@ func (v ECDSAVerifier) Verify(payload []byte, signature []byte, key interface{})
return errors.Wrapf(err, `failed to retrieve ecdsa.PublicKey out of %T`, key)
}

return v.verify(payload, signature, &pubkey)
r := pool.GetBigInt()
s := pool.GetBigInt()
defer pool.ReleaseBigInt(r)
defer pool.ReleaseBigInt(s)

n := len(signature) / 2
r.SetBytes(signature[:n])
s.SetBytes(signature[n:])

h := v.hash.New()
if _, err := h.Write(payload); err != nil {
return errors.Wrap(err, "failed to write payload using ecdsa")
}

if !ecdsa.Verify(&pubkey, h.Sum(nil), r, s) {
return errors.New(`failed to verify signature using ecdsa`)
}
return nil
}
32 changes: 23 additions & 9 deletions jws/eddsa.go
Original file line number Diff line number Diff line change
@@ -1,38 +1,52 @@
package jws

import (
"crypto"
"crypto/ed25519"
"crypto/rand"

"github.com/lestrrat-go/jwx/internal/keyconv"
"github.com/lestrrat-go/jwx/jwa"
"github.com/pkg/errors"
)

type eddsaSigner struct{}

func newEdDSASigner() Signer {
return &EdDSASigner{}
return &eddsaSigner{}
}

func (s EdDSASigner) Algorithm() jwa.SignatureAlgorithm {
func (s eddsaSigner) Algorithm() jwa.SignatureAlgorithm {
return jwa.EdDSA
}

func (s EdDSASigner) Sign(payload []byte, key interface{}) ([]byte, error) {
func (s eddsaSigner) Sign(payload []byte, key interface{}) ([]byte, error) {
if key == nil {
return nil, errors.New(`missing private key while signing payload`)
}

var privkey ed25519.PrivateKey
if err := keyconv.Ed25519PrivateKey(&privkey, key); err != nil {
return nil, errors.Wrapf(err, `failed to retrieve ed25519.PrivateKey out of %T`, key)
// The ed25519.PrivateKey object implements crypto.Signer, so we should
// simply accept a crypto.Signer here.
signer, ok := key.(crypto.Signer)
if !ok {
// This fallback exists for cases when jwk.Key was passed, or
// users gave us a pointer instead of non-pointer, etc.
var privkey ed25519.PrivateKey
if err := keyconv.Ed25519PrivateKey(&privkey, key); err != nil {
return nil, errors.Wrapf(err, `failed to retrieve ed25519.PrivateKey out of %T`, key)
}
signer = privkey
}
return ed25519.Sign(privkey, payload), nil
return signer.Sign(rand.Reader, payload, crypto.Hash(0))
}

type eddsaVerifier struct{}

func newEdDSAVerifier() Verifier {
return &EdDSAVerifier{}
return &eddsaVerifier{}
}

func (v EdDSAVerifier) Verify(payload, signature []byte, key interface{}) (err error) {
func (v eddsaVerifier) Verify(payload, signature []byte, key interface{}) (err error) {
if key == nil {
return errors.New(`missing public key while verifying payload`)
}
Expand Down
37 changes: 0 additions & 37 deletions jws/interface.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package jws

import (
"crypto/ecdsa"
"crypto/rsa"

"github.com/lestrrat-go/iter/mapiter"
"github.com/lestrrat-go/jwx/internal/iter"
"github.com/lestrrat-go/jwx/jwa"
Expand Down Expand Up @@ -78,22 +75,6 @@ type Signer interface {
Algorithm() jwa.SignatureAlgorithm
}

type rsaSignFunc func([]byte, *rsa.PrivateKey) ([]byte, error)

// RSASigner uses crypto/rsa to sign the payloads.
type RSASigner struct {
sign rsaSignFunc
alg jwa.SignatureAlgorithm
}

type ecdsaSignFunc func([]byte, *ecdsa.PrivateKey) ([]byte, error)

// ECDSASigner uses crypto/ecdsa to sign the payloads.
type ECDSASigner struct {
alg jwa.SignatureAlgorithm
sign ecdsaSignFunc
}

type hmacSignFunc func([]byte, []byte) ([]byte, error)

// HMACSigner uses crypto/hmac to sign the payloads.
Expand All @@ -102,9 +83,6 @@ type HMACSigner struct {
sign hmacSignFunc
}

type EdDSASigner struct {
}

type Verifier interface {
// Verify checks whether the payload and signature are valid for
// the given key.
Expand All @@ -116,21 +94,6 @@ type Verifier interface {
Verify(payload []byte, signature []byte, key interface{}) error
}

type rsaVerifyFunc func([]byte, []byte, *rsa.PublicKey) error

type RSAVerifier struct {
verify rsaVerifyFunc
}

type ecdsaVerifyFunc func([]byte, []byte, *ecdsa.PublicKey) error

type ECDSAVerifier struct {
verify ecdsaVerifyFunc
}

type HMACVerifier struct {
signer Signer
}

type EdDSAVerifier struct {
}
15 changes: 12 additions & 3 deletions jws/jws.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,18 @@ var muSigner = &sync.Mutex{}
// it in compact serialization format. In this format you may NOT use
// multiple signers.
//
// It accepts either a raw key (e.g. rsa.PrivateKey, ecdsa.PrivateKey, etc)
// or a jwk.Key, and the name of the algorithm that should be used to sign
// the token.
// The `alg` parameter is the identifier for the signature algorithm
// that should be used.
//
// For the `key` parameter, any of the following is accepted:
// * A "raw" key (e.g. rsa.PrivateKey, ecdsa.PrivateKey, etc)
// * A crypto.Signer
// * A jwk.Key
//
// A `crypto.Signer` is used when the private part of a key is
// kept in an inaccessible location, such as hardware.
// `crypto.Signer` is currently supported for RSA, ECDSA, and EdDSA
// family of algorithms.
//
// If the key is a jwk.Key and the key contains a key ID (`kid` field),
// then it is added to the protected header generated by the signature
Expand Down
Loading

0 comments on commit fd774b7

Please sign in to comment.