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
5package acme
6
7import (
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 identity provided by a CA during registration.
24type keyID string
25
26// noKeyID indicates that jwsEncodeJSON should compute and use JWK instead of a KID.
27// See jwsEncodeJSON for details.
28const 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.
34const noPayload = ""
35
36// jsonWebSignature can be easily serialized into a JWS following
37// https://tools.ietf.org/html/rfc7515#section-3.2.
38type jsonWebSignature struct {
39	Protected string `json:"protected"`
40	Payload   string `json:"payload"`
41	Sig       string `json:"signature"`
42}
43
44// jwsEncodeJSON signs claimset using provided key and a nonce.
45// The result is serialized in JSON format containing either kid or jwk
46// fields based on the provided keyID value.
47//
48// If kid is non-empty, its quoted value is inserted in the protected head
49// as "kid" field value. Otherwise, JWK is computed using jwkEncode and inserted
50// as "jwk" field value. The "jwk" and "kid" fields are mutually exclusive.
51//
52// See https://tools.ietf.org/html/rfc7515#section-7.
53func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid keyID, nonce, url string) ([]byte, error) {
54	alg, sha := jwsHasher(key.Public())
55	if alg == "" || !sha.Available() {
56		return nil, ErrUnsupportedKey
57	}
58	var phead string
59	switch kid {
60	case noKeyID:
61		jwk, err := jwkEncode(key.Public())
62		if err != nil {
63			return nil, err
64		}
65		phead = fmt.Sprintf(`{"alg":%q,"jwk":%s,"nonce":%q,"url":%q}`, alg, jwk, nonce, url)
66	default:
67		phead = fmt.Sprintf(`{"alg":%q,"kid":%q,"nonce":%q,"url":%q}`, alg, kid, nonce, url)
68	}
69	phead = base64.RawURLEncoding.EncodeToString([]byte(phead))
70	var payload string
71	if claimset != noPayload {
72		cs, err := json.Marshal(claimset)
73		if err != nil {
74			return nil, err
75		}
76		payload = base64.RawURLEncoding.EncodeToString(cs)
77	}
78	hash := sha.New()
79	hash.Write([]byte(phead + "." + payload))
80	sig, err := jwsSign(key, sha, hash.Sum(nil))
81	if err != nil {
82		return nil, err
83	}
84	enc := jsonWebSignature{
85		Protected: phead,
86		Payload:   payload,
87		Sig:       base64.RawURLEncoding.EncodeToString(sig),
88	}
89	return json.Marshal(&enc)
90}
91
92// jwsWithMAC creates and signs a JWS using the given key and the HS256
93// algorithm. kid and url are included in the protected header. rawPayload
94// should not be base64-URL-encoded.
95func jwsWithMAC(key []byte, kid, url string, rawPayload []byte) (*jsonWebSignature, error) {
96	if len(key) == 0 {
97		return nil, errors.New("acme: cannot sign JWS with an empty MAC key")
98	}
99	header := struct {
100		Algorithm string `json:"alg"`
101		KID       string `json:"kid"`
102		URL       string `json:"url,omitempty"`
103	}{
104		// Only HMAC-SHA256 is supported.
105		Algorithm: "HS256",
106		KID:       kid,
107		URL:       url,
108	}
109	rawProtected, err := json.Marshal(header)
110	if err != nil {
111		return nil, err
112	}
113	protected := base64.RawURLEncoding.EncodeToString(rawProtected)
114	payload := base64.RawURLEncoding.EncodeToString(rawPayload)
115
116	h := hmac.New(sha256.New, key)
117	if _, err := h.Write([]byte(protected + "." + payload)); err != nil {
118		return nil, err
119	}
120	mac := h.Sum(nil)
121
122	return &jsonWebSignature{
123		Protected: protected,
124		Payload:   payload,
125		Sig:       base64.RawURLEncoding.EncodeToString(mac),
126	}, nil
127}
128
129// jwkEncode encodes public part of an RSA or ECDSA key into a JWK.
130// The result is also suitable for creating a JWK thumbprint.
131// https://tools.ietf.org/html/rfc7517
132func jwkEncode(pub crypto.PublicKey) (string, error) {
133	switch pub := pub.(type) {
134	case *rsa.PublicKey:
135		// https://tools.ietf.org/html/rfc7518#section-6.3.1
136		n := pub.N
137		e := big.NewInt(int64(pub.E))
138		// Field order is important.
139		// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
140		return fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`,
141			base64.RawURLEncoding.EncodeToString(e.Bytes()),
142			base64.RawURLEncoding.EncodeToString(n.Bytes()),
143		), nil
144	case *ecdsa.PublicKey:
145		// https://tools.ietf.org/html/rfc7518#section-6.2.1
146		p := pub.Curve.Params()
147		n := p.BitSize / 8
148		if p.BitSize%8 != 0 {
149			n++
150		}
151		x := pub.X.Bytes()
152		if n > len(x) {
153			x = append(make([]byte, n-len(x)), x...)
154		}
155		y := pub.Y.Bytes()
156		if n > len(y) {
157			y = append(make([]byte, n-len(y)), y...)
158		}
159		// Field order is important.
160		// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
161		return fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`,
162			p.Name,
163			base64.RawURLEncoding.EncodeToString(x),
164			base64.RawURLEncoding.EncodeToString(y),
165		), nil
166	}
167	return "", ErrUnsupportedKey
168}
169
170// jwsSign signs the digest using the given key.
171// The hash is unused for ECDSA keys.
172func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) {
173	switch pub := key.Public().(type) {
174	case *rsa.PublicKey:
175		return key.Sign(rand.Reader, digest, hash)
176	case *ecdsa.PublicKey:
177		sigASN1, err := key.Sign(rand.Reader, digest, hash)
178		if err != nil {
179			return nil, err
180		}
181
182		var rs struct{ R, S *big.Int }
183		if _, err := asn1.Unmarshal(sigASN1, &rs); err != nil {
184			return nil, err
185		}
186
187		rb, sb := rs.R.Bytes(), rs.S.Bytes()
188		size := pub.Params().BitSize / 8
189		if size%8 > 0 {
190			size++
191		}
192		sig := make([]byte, size*2)
193		copy(sig[size-len(rb):], rb)
194		copy(sig[size*2-len(sb):], sb)
195		return sig, nil
196	}
197	return nil, ErrUnsupportedKey
198}
199
200// jwsHasher indicates suitable JWS algorithm name and a hash function
201// to use for signing a digest with the provided key.
202// It returns ("", 0) if the key is not supported.
203func jwsHasher(pub crypto.PublicKey) (string, crypto.Hash) {
204	switch pub := pub.(type) {
205	case *rsa.PublicKey:
206		return "RS256", crypto.SHA256
207	case *ecdsa.PublicKey:
208		switch pub.Params().Name {
209		case "P-256":
210			return "ES256", crypto.SHA256
211		case "P-384":
212			return "ES384", crypto.SHA384
213		case "P-521":
214			return "ES512", crypto.SHA512
215		}
216	}
217	return "", 0
218}
219
220// JWKThumbprint creates a JWK thumbprint out of pub
221// as specified in https://tools.ietf.org/html/rfc7638.
222func JWKThumbprint(pub crypto.PublicKey) (string, error) {
223	jwk, err := jwkEncode(pub)
224	if err != nil {
225		return "", err
226	}
227	b := sha256.Sum256([]byte(jwk))
228	return base64.RawURLEncoding.EncodeToString(b[:]), nil
229}
230