442 lines
13 KiB
Go
442 lines
13 KiB
Go
/*-
|
|
* Copyright 2014 Square Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package jose
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ecdsa"
|
|
"crypto/rsa"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"golang.org/x/crypto/ed25519"
|
|
|
|
"gopkg.in/square/go-jose.v2/json"
|
|
)
|
|
|
|
// NonceSource represents a source of random nonces to go into JWS objects
|
|
type NonceSource interface {
|
|
Nonce() (string, error)
|
|
}
|
|
|
|
// Signer represents a signer which takes a payload and produces a signed JWS object.
|
|
type Signer interface {
|
|
Sign(payload []byte) (*JSONWebSignature, error)
|
|
Options() SignerOptions
|
|
}
|
|
|
|
// SigningKey represents an algorithm/key used to sign a message.
|
|
type SigningKey struct {
|
|
Algorithm SignatureAlgorithm
|
|
Key interface{}
|
|
}
|
|
|
|
// SignerOptions represents options that can be set when creating signers.
|
|
type SignerOptions struct {
|
|
NonceSource NonceSource
|
|
EmbedJWK bool
|
|
|
|
// Optional map of additional keys to be inserted into the protected header
|
|
// of a JWS object. Some specifications which make use of JWS like to insert
|
|
// additional values here. All values must be JSON-serializable.
|
|
ExtraHeaders map[HeaderKey]interface{}
|
|
}
|
|
|
|
// WithHeader adds an arbitrary value to the ExtraHeaders map, initializing it
|
|
// if necessary. It returns itself and so can be used in a fluent style.
|
|
func (so *SignerOptions) WithHeader(k HeaderKey, v interface{}) *SignerOptions {
|
|
if so.ExtraHeaders == nil {
|
|
so.ExtraHeaders = map[HeaderKey]interface{}{}
|
|
}
|
|
so.ExtraHeaders[k] = v
|
|
return so
|
|
}
|
|
|
|
// WithContentType adds a content type ("cty") header and returns the updated
|
|
// SignerOptions.
|
|
func (so *SignerOptions) WithContentType(contentType ContentType) *SignerOptions {
|
|
return so.WithHeader(HeaderContentType, contentType)
|
|
}
|
|
|
|
// WithType adds a type ("typ") header and returns the updated SignerOptions.
|
|
func (so *SignerOptions) WithType(typ ContentType) *SignerOptions {
|
|
return so.WithHeader(HeaderType, typ)
|
|
}
|
|
|
|
// WithCritical adds the given names to the critical ("crit") header and returns
|
|
// the updated SignerOptions.
|
|
func (so *SignerOptions) WithCritical(names ...string) *SignerOptions {
|
|
if so.ExtraHeaders[headerCritical] == nil {
|
|
so.WithHeader(headerCritical, make([]string, 0, len(names)))
|
|
}
|
|
crit := so.ExtraHeaders[headerCritical].([]string)
|
|
so.ExtraHeaders[headerCritical] = append(crit, names...)
|
|
return so
|
|
}
|
|
|
|
// WithBase64 adds a base64url-encode payload ("b64") header and returns the updated
|
|
// SignerOptions. When the "b64" value is "false", the payload is not base64 encoded.
|
|
func (so *SignerOptions) WithBase64(b64 bool) *SignerOptions {
|
|
if !b64 {
|
|
so.WithHeader(headerB64, b64)
|
|
so.WithCritical(headerB64)
|
|
}
|
|
return so
|
|
}
|
|
|
|
type payloadSigner interface {
|
|
signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error)
|
|
}
|
|
|
|
type payloadVerifier interface {
|
|
verifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error
|
|
}
|
|
|
|
type genericSigner struct {
|
|
recipients []recipientSigInfo
|
|
nonceSource NonceSource
|
|
embedJWK bool
|
|
extraHeaders map[HeaderKey]interface{}
|
|
}
|
|
|
|
type recipientSigInfo struct {
|
|
sigAlg SignatureAlgorithm
|
|
publicKey func() *JSONWebKey
|
|
signer payloadSigner
|
|
}
|
|
|
|
func staticPublicKey(jwk *JSONWebKey) func() *JSONWebKey {
|
|
return func() *JSONWebKey {
|
|
return jwk
|
|
}
|
|
}
|
|
|
|
// NewSigner creates an appropriate signer based on the key type
|
|
func NewSigner(sig SigningKey, opts *SignerOptions) (Signer, error) {
|
|
return NewMultiSigner([]SigningKey{sig}, opts)
|
|
}
|
|
|
|
// NewMultiSigner creates a signer for multiple recipients
|
|
func NewMultiSigner(sigs []SigningKey, opts *SignerOptions) (Signer, error) {
|
|
signer := &genericSigner{recipients: []recipientSigInfo{}}
|
|
|
|
if opts != nil {
|
|
signer.nonceSource = opts.NonceSource
|
|
signer.embedJWK = opts.EmbedJWK
|
|
signer.extraHeaders = opts.ExtraHeaders
|
|
}
|
|
|
|
for _, sig := range sigs {
|
|
err := signer.addRecipient(sig.Algorithm, sig.Key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return signer, nil
|
|
}
|
|
|
|
// newVerifier creates a verifier based on the key type
|
|
func newVerifier(verificationKey interface{}) (payloadVerifier, error) {
|
|
switch verificationKey := verificationKey.(type) {
|
|
case ed25519.PublicKey:
|
|
return &edEncrypterVerifier{
|
|
publicKey: verificationKey,
|
|
}, nil
|
|
case *rsa.PublicKey:
|
|
return &rsaEncrypterVerifier{
|
|
publicKey: verificationKey,
|
|
}, nil
|
|
case *ecdsa.PublicKey:
|
|
return &ecEncrypterVerifier{
|
|
publicKey: verificationKey,
|
|
}, nil
|
|
case []byte:
|
|
return &symmetricMac{
|
|
key: verificationKey,
|
|
}, nil
|
|
case JSONWebKey:
|
|
return newVerifier(verificationKey.Key)
|
|
case *JSONWebKey:
|
|
return newVerifier(verificationKey.Key)
|
|
}
|
|
if ov, ok := verificationKey.(OpaqueVerifier); ok {
|
|
return &opaqueVerifier{verifier: ov}, nil
|
|
}
|
|
return nil, ErrUnsupportedKeyType
|
|
}
|
|
|
|
func (ctx *genericSigner) addRecipient(alg SignatureAlgorithm, signingKey interface{}) error {
|
|
recipient, err := makeJWSRecipient(alg, signingKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx.recipients = append(ctx.recipients, recipient)
|
|
return nil
|
|
}
|
|
|
|
func makeJWSRecipient(alg SignatureAlgorithm, signingKey interface{}) (recipientSigInfo, error) {
|
|
switch signingKey := signingKey.(type) {
|
|
case ed25519.PrivateKey:
|
|
return newEd25519Signer(alg, signingKey)
|
|
case *rsa.PrivateKey:
|
|
return newRSASigner(alg, signingKey)
|
|
case *ecdsa.PrivateKey:
|
|
return newECDSASigner(alg, signingKey)
|
|
case []byte:
|
|
return newSymmetricSigner(alg, signingKey)
|
|
case JSONWebKey:
|
|
return newJWKSigner(alg, signingKey)
|
|
case *JSONWebKey:
|
|
return newJWKSigner(alg, *signingKey)
|
|
}
|
|
if signer, ok := signingKey.(OpaqueSigner); ok {
|
|
return newOpaqueSigner(alg, signer)
|
|
}
|
|
return recipientSigInfo{}, ErrUnsupportedKeyType
|
|
}
|
|
|
|
func newJWKSigner(alg SignatureAlgorithm, signingKey JSONWebKey) (recipientSigInfo, error) {
|
|
recipient, err := makeJWSRecipient(alg, signingKey.Key)
|
|
if err != nil {
|
|
return recipientSigInfo{}, err
|
|
}
|
|
if recipient.publicKey != nil && recipient.publicKey() != nil {
|
|
// recipient.publicKey is a JWK synthesized for embedding when recipientSigInfo
|
|
// was created for the inner key (such as a RSA or ECDSA public key). It contains
|
|
// the pub key for embedding, but doesn't have extra params like key id.
|
|
publicKey := signingKey
|
|
publicKey.Key = recipient.publicKey().Key
|
|
recipient.publicKey = staticPublicKey(&publicKey)
|
|
|
|
// This should be impossible, but let's check anyway.
|
|
if !recipient.publicKey().IsPublic() {
|
|
return recipientSigInfo{}, errors.New("square/go-jose: public key was unexpectedly not public")
|
|
}
|
|
}
|
|
return recipient, nil
|
|
}
|
|
|
|
func (ctx *genericSigner) Sign(payload []byte) (*JSONWebSignature, error) {
|
|
obj := &JSONWebSignature{}
|
|
obj.payload = payload
|
|
obj.Signatures = make([]Signature, len(ctx.recipients))
|
|
|
|
for i, recipient := range ctx.recipients {
|
|
protected := map[HeaderKey]interface{}{
|
|
headerAlgorithm: string(recipient.sigAlg),
|
|
}
|
|
|
|
if recipient.publicKey != nil && recipient.publicKey() != nil {
|
|
// We want to embed the JWK or set the kid header, but not both. Having a protected
|
|
// header that contains an embedded JWK while also simultaneously containing the kid
|
|
// header is confusing, and at least in ACME the two are considered to be mutually
|
|
// exclusive. The fact that both can exist at the same time is a somewhat unfortunate
|
|
// result of the JOSE spec. We've decided that this library will only include one or
|
|
// the other to avoid this confusion.
|
|
//
|
|
// See https://github.com/square/go-jose/issues/157 for more context.
|
|
if ctx.embedJWK {
|
|
protected[headerJWK] = recipient.publicKey()
|
|
} else {
|
|
keyID := recipient.publicKey().KeyID
|
|
if keyID != "" {
|
|
protected[headerKeyID] = keyID
|
|
}
|
|
}
|
|
}
|
|
|
|
if ctx.nonceSource != nil {
|
|
nonce, err := ctx.nonceSource.Nonce()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("square/go-jose: Error generating nonce: %v", err)
|
|
}
|
|
protected[headerNonce] = nonce
|
|
}
|
|
|
|
for k, v := range ctx.extraHeaders {
|
|
protected[k] = v
|
|
}
|
|
|
|
serializedProtected := mustSerializeJSON(protected)
|
|
needsBase64 := true
|
|
|
|
if b64, ok := protected[headerB64]; ok {
|
|
if needsBase64, ok = b64.(bool); !ok {
|
|
return nil, errors.New("square/go-jose: Invalid b64 header parameter")
|
|
}
|
|
}
|
|
|
|
var input bytes.Buffer
|
|
|
|
input.WriteString(base64.RawURLEncoding.EncodeToString(serializedProtected))
|
|
input.WriteByte('.')
|
|
|
|
if needsBase64 {
|
|
input.WriteString(base64.RawURLEncoding.EncodeToString(payload))
|
|
} else {
|
|
input.Write(payload)
|
|
}
|
|
|
|
signatureInfo, err := recipient.signer.signPayload(input.Bytes(), recipient.sigAlg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
signatureInfo.protected = &rawHeader{}
|
|
for k, v := range protected {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("square/go-jose: Error marshalling item %#v: %v", k, err)
|
|
}
|
|
(*signatureInfo.protected)[k] = makeRawMessage(b)
|
|
}
|
|
obj.Signatures[i] = signatureInfo
|
|
}
|
|
|
|
return obj, nil
|
|
}
|
|
|
|
func (ctx *genericSigner) Options() SignerOptions {
|
|
return SignerOptions{
|
|
NonceSource: ctx.nonceSource,
|
|
EmbedJWK: ctx.embedJWK,
|
|
ExtraHeaders: ctx.extraHeaders,
|
|
}
|
|
}
|
|
|
|
// Verify validates the signature on the object and returns the payload.
|
|
// This function does not support multi-signature, if you desire multi-sig
|
|
// verification use VerifyMulti instead.
|
|
//
|
|
// Be careful when verifying signatures based on embedded JWKs inside the
|
|
// payload header. You cannot assume that the key received in a payload is
|
|
// trusted.
|
|
func (obj JSONWebSignature) Verify(verificationKey interface{}) ([]byte, error) {
|
|
err := obj.DetachedVerify(obj.payload, verificationKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return obj.payload, nil
|
|
}
|
|
|
|
// UnsafePayloadWithoutVerification returns the payload without
|
|
// verifying it. The content returned from this function cannot be
|
|
// trusted.
|
|
func (obj JSONWebSignature) UnsafePayloadWithoutVerification() []byte {
|
|
return obj.payload
|
|
}
|
|
|
|
// DetachedVerify validates a detached signature on the given payload. In
|
|
// most cases, you will probably want to use Verify instead. DetachedVerify
|
|
// is only useful if you have a payload and signature that are separated from
|
|
// each other.
|
|
func (obj JSONWebSignature) DetachedVerify(payload []byte, verificationKey interface{}) error {
|
|
verifier, err := newVerifier(verificationKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(obj.Signatures) > 1 {
|
|
return errors.New("square/go-jose: too many signatures in payload; expecting only one")
|
|
}
|
|
|
|
signature := obj.Signatures[0]
|
|
headers := signature.mergedHeaders()
|
|
critical, err := headers.getCritical()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, name := range critical {
|
|
if !supportedCritical[name] {
|
|
return ErrCryptoFailure
|
|
}
|
|
}
|
|
|
|
input, err := obj.computeAuthData(payload, &signature)
|
|
if err != nil {
|
|
return ErrCryptoFailure
|
|
}
|
|
|
|
alg := headers.getSignatureAlgorithm()
|
|
err = verifier.verifyPayload(input, signature.Signature, alg)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
return ErrCryptoFailure
|
|
}
|
|
|
|
// VerifyMulti validates (one of the multiple) signatures on the object and
|
|
// returns the index of the signature that was verified, along with the signature
|
|
// object and the payload. We return the signature and index to guarantee that
|
|
// callers are getting the verified value.
|
|
func (obj JSONWebSignature) VerifyMulti(verificationKey interface{}) (int, Signature, []byte, error) {
|
|
idx, sig, err := obj.DetachedVerifyMulti(obj.payload, verificationKey)
|
|
if err != nil {
|
|
return -1, Signature{}, nil, err
|
|
}
|
|
return idx, sig, obj.payload, nil
|
|
}
|
|
|
|
// DetachedVerifyMulti validates a detached signature on the given payload with
|
|
// a signature/object that has potentially multiple signers. This returns the index
|
|
// of the signature that was verified, along with the signature object. We return
|
|
// the signature and index to guarantee that callers are getting the verified value.
|
|
//
|
|
// In most cases, you will probably want to use Verify or VerifyMulti instead.
|
|
// DetachedVerifyMulti is only useful if you have a payload and signature that are
|
|
// separated from each other, and the signature can have multiple signers at the
|
|
// same time.
|
|
func (obj JSONWebSignature) DetachedVerifyMulti(payload []byte, verificationKey interface{}) (int, Signature, error) {
|
|
verifier, err := newVerifier(verificationKey)
|
|
if err != nil {
|
|
return -1, Signature{}, err
|
|
}
|
|
|
|
outer:
|
|
for i, signature := range obj.Signatures {
|
|
headers := signature.mergedHeaders()
|
|
critical, err := headers.getCritical()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
for _, name := range critical {
|
|
if !supportedCritical[name] {
|
|
continue outer
|
|
}
|
|
}
|
|
|
|
input, err := obj.computeAuthData(payload, &signature)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
alg := headers.getSignatureAlgorithm()
|
|
err = verifier.verifyPayload(input, signature.Signature, alg)
|
|
if err == nil {
|
|
return i, signature, nil
|
|
}
|
|
}
|
|
|
|
return -1, Signature{}, ErrCryptoFailure
|
|
}
|