jws.go raw

   1  // Copyright 2015 The Go Authors. All rights reserved.
   2  // Use of this source code is governed by a BSD-style
   3  // license that can be found in the LICENSE file.
   4  
   5  package acme
   6  
   7  import (
   8  	"crypto"
   9  	"crypto/ecdsa"
  10  	"crypto/hmac"
  11  	"crypto/rand"
  12  	"crypto/rsa"
  13  	"crypto/sha256"
  14  	_ "crypto/sha512" // need for EC keys
  15  	"encoding/asn1"
  16  	"encoding/base64"
  17  	"encoding/json"
  18  	"errors"
  19  	"fmt"
  20  	"math/big"
  21  )
  22  
  23  // KeyID is the account key identity provided by a CA during registration.
  24  type KeyID string
  25  
  26  // noKeyID indicates that jwsEncodeJSON should compute and use JWK instead of a KID.
  27  // See jwsEncodeJSON for details.
  28  const noKeyID = KeyID("")
  29  
  30  // noPayload indicates jwsEncodeJSON will encode zero-length octet string
  31  // in a JWS request. This is called POST-as-GET in RFC 8555 and is used to make
  32  // authenticated GET requests via POSTing with an empty payload.
  33  // See https://tools.ietf.org/html/rfc8555#section-6.3 for more details.
  34  const noPayload = ""
  35  
  36  // noNonce indicates that the nonce should be omitted from the protected header.
  37  // See jwsEncodeJSON for details.
  38  const noNonce = ""
  39  
  40  // jsonWebSignature can be easily serialized into a JWS following
  41  // https://tools.ietf.org/html/rfc7515#section-3.2.
  42  type jsonWebSignature struct {
  43  	Protected string `json:"protected"`
  44  	Payload   string `json:"payload"`
  45  	Sig       string `json:"signature"`
  46  }
  47  
  48  // jwsEncodeJSON signs claimset using provided key and a nonce.
  49  // The result is serialized in JSON format containing either kid or jwk
  50  // fields based on the provided KeyID value.
  51  //
  52  // The claimset is marshalled using json.Marshal unless it is a string.
  53  // In which case it is inserted directly into the message.
  54  //
  55  // If kid is non-empty, its quoted value is inserted in the protected header
  56  // as "kid" field value. Otherwise, JWK is computed using jwkEncode and inserted
  57  // as "jwk" field value. The "jwk" and "kid" fields are mutually exclusive.
  58  //
  59  // If nonce is non-empty, its quoted value is inserted in the protected header.
  60  //
  61  // See https://tools.ietf.org/html/rfc7515#section-7.
  62  func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid KeyID, nonce, url string) ([]byte, error) {
  63  	if key == nil {
  64  		return nil, errors.New("nil key")
  65  	}
  66  	alg, sha := jwsHasher(key.Public())
  67  	if alg == "" || !sha.Available() {
  68  		return nil, ErrUnsupportedKey
  69  	}
  70  	headers := struct {
  71  		Alg   string          `json:"alg"`
  72  		KID   string          `json:"kid,omitempty"`
  73  		JWK   json.RawMessage `json:"jwk,omitempty"`
  74  		Nonce string          `json:"nonce,omitempty"`
  75  		URL   string          `json:"url"`
  76  	}{
  77  		Alg:   alg,
  78  		Nonce: nonce,
  79  		URL:   url,
  80  	}
  81  	switch kid {
  82  	case noKeyID:
  83  		jwk, err := jwkEncode(key.Public())
  84  		if err != nil {
  85  			return nil, err
  86  		}
  87  		headers.JWK = json.RawMessage(jwk)
  88  	default:
  89  		headers.KID = string(kid)
  90  	}
  91  	phJSON, err := json.Marshal(headers)
  92  	if err != nil {
  93  		return nil, err
  94  	}
  95  	phead := base64.RawURLEncoding.EncodeToString(phJSON)
  96  	var payload string
  97  	if val, ok := claimset.(string); ok {
  98  		payload = val
  99  	} else {
 100  		cs, err := json.Marshal(claimset)
 101  		if err != nil {
 102  			return nil, err
 103  		}
 104  		payload = base64.RawURLEncoding.EncodeToString(cs)
 105  	}
 106  	hash := sha.New()
 107  	hash.Write([]byte(phead + "." + payload))
 108  	sig, err := jwsSign(key, sha, hash.Sum(nil))
 109  	if err != nil {
 110  		return nil, err
 111  	}
 112  	enc := jsonWebSignature{
 113  		Protected: phead,
 114  		Payload:   payload,
 115  		Sig:       base64.RawURLEncoding.EncodeToString(sig),
 116  	}
 117  	return json.Marshal(&enc)
 118  }
 119  
 120  // jwsWithMAC creates and signs a JWS using the given key and the HS256
 121  // algorithm. kid and url are included in the protected header. rawPayload
 122  // should not be base64-URL-encoded.
 123  func jwsWithMAC(key []byte, kid, url string, rawPayload []byte) (*jsonWebSignature, error) {
 124  	if len(key) == 0 {
 125  		return nil, errors.New("acme: cannot sign JWS with an empty MAC key")
 126  	}
 127  	header := struct {
 128  		Algorithm string `json:"alg"`
 129  		KID       string `json:"kid"`
 130  		URL       string `json:"url,omitempty"`
 131  	}{
 132  		// Only HMAC-SHA256 is supported.
 133  		Algorithm: "HS256",
 134  		KID:       kid,
 135  		URL:       url,
 136  	}
 137  	rawProtected, err := json.Marshal(header)
 138  	if err != nil {
 139  		return nil, err
 140  	}
 141  	protected := base64.RawURLEncoding.EncodeToString(rawProtected)
 142  	payload := base64.RawURLEncoding.EncodeToString(rawPayload)
 143  
 144  	h := hmac.New(sha256.New, key)
 145  	if _, err := h.Write([]byte(protected + "." + payload)); err != nil {
 146  		return nil, err
 147  	}
 148  	mac := h.Sum(nil)
 149  
 150  	return &jsonWebSignature{
 151  		Protected: protected,
 152  		Payload:   payload,
 153  		Sig:       base64.RawURLEncoding.EncodeToString(mac),
 154  	}, nil
 155  }
 156  
 157  // jwkEncode encodes public part of an RSA or ECDSA key into a JWK.
 158  // The result is also suitable for creating a JWK thumbprint.
 159  // https://tools.ietf.org/html/rfc7517
 160  func jwkEncode(pub crypto.PublicKey) (string, error) {
 161  	switch pub := pub.(type) {
 162  	case *rsa.PublicKey:
 163  		// https://tools.ietf.org/html/rfc7518#section-6.3.1
 164  		n := pub.N
 165  		e := big.NewInt(int64(pub.E))
 166  		// Field order is important.
 167  		// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
 168  		return fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`,
 169  			base64.RawURLEncoding.EncodeToString(e.Bytes()),
 170  			base64.RawURLEncoding.EncodeToString(n.Bytes()),
 171  		), nil
 172  	case *ecdsa.PublicKey:
 173  		// https://tools.ietf.org/html/rfc7518#section-6.2.1
 174  		p := pub.Curve.Params()
 175  		n := p.BitSize / 8
 176  		if p.BitSize%8 != 0 {
 177  			n++
 178  		}
 179  		x := pub.X.Bytes()
 180  		if n > len(x) {
 181  			x = append(make([]byte, n-len(x)), x...)
 182  		}
 183  		y := pub.Y.Bytes()
 184  		if n > len(y) {
 185  			y = append(make([]byte, n-len(y)), y...)
 186  		}
 187  		// Field order is important.
 188  		// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
 189  		return fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`,
 190  			p.Name,
 191  			base64.RawURLEncoding.EncodeToString(x),
 192  			base64.RawURLEncoding.EncodeToString(y),
 193  		), nil
 194  	}
 195  	return "", ErrUnsupportedKey
 196  }
 197  
 198  // jwsSign signs the digest using the given key.
 199  // The hash is unused for ECDSA keys.
 200  func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) {
 201  	switch pub := key.Public().(type) {
 202  	case *rsa.PublicKey:
 203  		return key.Sign(rand.Reader, digest, hash)
 204  	case *ecdsa.PublicKey:
 205  		sigASN1, err := key.Sign(rand.Reader, digest, hash)
 206  		if err != nil {
 207  			return nil, err
 208  		}
 209  
 210  		var rs struct{ R, S *big.Int }
 211  		if _, err := asn1.Unmarshal(sigASN1, &rs); err != nil {
 212  			return nil, err
 213  		}
 214  
 215  		rb, sb := rs.R.Bytes(), rs.S.Bytes()
 216  		size := pub.Params().BitSize / 8
 217  		if size%8 > 0 {
 218  			size++
 219  		}
 220  		sig := make([]byte, size*2)
 221  		copy(sig[size-len(rb):], rb)
 222  		copy(sig[size*2-len(sb):], sb)
 223  		return sig, nil
 224  	}
 225  	return nil, ErrUnsupportedKey
 226  }
 227  
 228  // jwsHasher indicates suitable JWS algorithm name and a hash function
 229  // to use for signing a digest with the provided key.
 230  // It returns ("", 0) if the key is not supported.
 231  func jwsHasher(pub crypto.PublicKey) (string, crypto.Hash) {
 232  	switch pub := pub.(type) {
 233  	case *rsa.PublicKey:
 234  		return "RS256", crypto.SHA256
 235  	case *ecdsa.PublicKey:
 236  		switch pub.Params().Name {
 237  		case "P-256":
 238  			return "ES256", crypto.SHA256
 239  		case "P-384":
 240  			return "ES384", crypto.SHA384
 241  		case "P-521":
 242  			return "ES512", crypto.SHA512
 243  		}
 244  	}
 245  	return "", 0
 246  }
 247  
 248  // JWKThumbprint creates a JWK thumbprint out of pub
 249  // as specified in https://tools.ietf.org/html/rfc7638.
 250  func JWKThumbprint(pub crypto.PublicKey) (string, error) {
 251  	jwk, err := jwkEncode(pub)
 252  	if err != nil {
 253  		return "", err
 254  	}
 255  	b := sha256.Sum256([]byte(jwk))
 256  	return base64.RawURLEncoding.EncodeToString(b[:]), nil
 257  }
 258