Skip to content

Commit 9d13527

Browse files
James Kastenmunnerz
authored andcommitted
acme: add external account binding support
Implements https://tools.ietf.org/html/rfc8555#section-7.3.4 Fixes golang/go#41430 Co-authored-by: James Munnelly <[email protected]> Change-Id: Icd0337fddbff49e7e79fb9105c2679609f990285 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/269279 Run-TryBot: Katie Hockman <[email protected]> TryBot-Result: Go Bot <[email protected]> Trust: Katie Hockman <[email protected]> Trust: Roland Shoemaker <[email protected]> Reviewed-by: Roland Shoemaker <[email protected]>
1 parent 8b5274c commit 9d13527

File tree

8 files changed

+391
-14
lines changed

8 files changed

+391
-14
lines changed

acme/acme.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,10 @@ func AcceptTOS(tosURL string) bool { return true }
363363
// Also see Error's Instance field for when a CA requires already registered accounts to agree
364364
// to an updated Terms of Service.
365365
func (c *Client) Register(ctx context.Context, acct *Account, prompt func(tosURL string) bool) (*Account, error) {
366+
if c.Key == nil {
367+
return nil, errors.New("acme: client.Key must be set to Register")
368+
}
369+
366370
dir, err := c.Discover(ctx)
367371
if err != nil {
368372
return nil, err

acme/acme_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,31 @@ func TestRegister(t *testing.T) {
188188
}
189189
}
190190

191+
func TestRegisterWithoutKey(t *testing.T) {
192+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
193+
if r.Method == "HEAD" {
194+
w.Header().Set("Replay-Nonce", "test-nonce")
195+
return
196+
}
197+
w.WriteHeader(http.StatusCreated)
198+
fmt.Fprint(w, `{}`)
199+
}))
200+
defer ts.Close()
201+
// First verify that using a complete client results in success.
202+
c := Client{
203+
Key: testKeyEC,
204+
DirectoryURL: ts.URL,
205+
dir: &Directory{RegURL: ts.URL},
206+
}
207+
if _, err := c.Register(context.Background(), &Account{}, AcceptTOS); err != nil {
208+
t.Fatalf("c.Register() = %v; want success with a complete test client", err)
209+
}
210+
c.Key = nil
211+
if _, err := c.Register(context.Background(), &Account{}, AcceptTOS); err == nil {
212+
t.Error("c.Register() from client without key succeeded, wanted error")
213+
}
214+
}
215+
191216
func TestUpdateReg(t *testing.T) {
192217
const terms = "https://ca.tld/acme/terms"
193218
contacts := []string{"mailto:[email protected]"}

acme/jws.go

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,31 @@ package acme
77
import (
88
"crypto"
99
"crypto/ecdsa"
10+
"crypto/hmac"
1011
"crypto/rand"
1112
"crypto/rsa"
1213
"crypto/sha256"
14+
"crypto/sha512"
1315
_ "crypto/sha512" // need for EC keys
1416
"encoding/asn1"
1517
"encoding/base64"
1618
"encoding/json"
19+
"errors"
1720
"fmt"
21+
"hash"
1822
"math/big"
1923
)
2024

25+
// MACAlgorithm represents a JWS MAC signature algorithm.
26+
// See https://tools.ietf.org/html/rfc7518#section-3.1 for more details.
27+
type MACAlgorithm string
28+
29+
const (
30+
MACAlgorithmHS256 = MACAlgorithm("HS256")
31+
MACAlgorithmHS384 = MACAlgorithm("HS384")
32+
MACAlgorithmHS512 = MACAlgorithm("HS512")
33+
)
34+
2135
// keyID is the account identity provided by a CA during registration.
2236
type keyID string
2337

@@ -31,6 +45,14 @@ const noKeyID = keyID("")
3145
// See https://tools.ietf.org/html/rfc8555#section-6.3 for more details.
3246
const noPayload = ""
3347

48+
// jsonWebSignature can be easily serialized into a JWS following
49+
// https://tools.ietf.org/html/rfc7515#section-3.2.
50+
type jsonWebSignature struct {
51+
Protected string `json:"protected"`
52+
Payload string `json:"payload"`
53+
Sig string `json:"signature"`
54+
}
55+
3456
// jwsEncodeJSON signs claimset using provided key and a nonce.
3557
// The result is serialized in JSON format containing either kid or jwk
3658
// fields based on the provided keyID value.
@@ -71,19 +93,40 @@ func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid keyID, nonce, ur
7193
if err != nil {
7294
return nil, err
7395
}
74-
75-
enc := struct {
76-
Protected string `json:"protected"`
77-
Payload string `json:"payload"`
78-
Sig string `json:"signature"`
79-
}{
96+
enc := jsonWebSignature{
8097
Protected: phead,
8198
Payload: payload,
8299
Sig: base64.RawURLEncoding.EncodeToString(sig),
83100
}
84101
return json.Marshal(&enc)
85102
}
86103

104+
// jwsWithMAC creates and signs a JWS using the given key and algorithm.
105+
// "rawProtected" and "rawPayload" should not be base64-URL-encoded.
106+
func jwsWithMAC(key []byte, alg MACAlgorithm, rawProtected, rawPayload []byte) (*jsonWebSignature, error) {
107+
if len(key) == 0 {
108+
return nil, errors.New("acme: cannot sign JWS with an empty MAC key")
109+
}
110+
protected := base64.RawURLEncoding.EncodeToString(rawProtected)
111+
payload := base64.RawURLEncoding.EncodeToString(rawPayload)
112+
113+
// Only HMACs are currently supported.
114+
hmac, err := newHMAC(key, alg)
115+
if err != nil {
116+
return nil, err
117+
}
118+
if _, err := hmac.Write([]byte(protected + "." + payload)); err != nil {
119+
return nil, err
120+
}
121+
mac := hmac.Sum(nil)
122+
123+
return &jsonWebSignature{
124+
Protected: protected,
125+
Payload: payload,
126+
Sig: base64.RawURLEncoding.EncodeToString(mac),
127+
}, nil
128+
}
129+
87130
// jwkEncode encodes public part of an RSA or ECDSA key into a JWK.
88131
// The result is also suitable for creating a JWK thumbprint.
89132
// https://tools.ietf.org/html/rfc7517
@@ -175,6 +218,20 @@ func jwsHasher(pub crypto.PublicKey) (string, crypto.Hash) {
175218
return "", 0
176219
}
177220

221+
// newHMAC returns an appropriate HMAC for the given MACAlgorithm.
222+
func newHMAC(key []byte, alg MACAlgorithm) (hash.Hash, error) {
223+
switch alg {
224+
case MACAlgorithmHS256:
225+
return hmac.New(sha256.New, key), nil
226+
case MACAlgorithmHS384:
227+
return hmac.New(sha512.New384, key), nil
228+
case MACAlgorithmHS512:
229+
return hmac.New(sha512.New, key), nil
230+
default:
231+
return nil, fmt.Errorf("acme: unsupported MAC algorithm: %v", alg)
232+
}
233+
}
234+
178235
// JWKThumbprint creates a JWK thumbprint out of pub
179236
// as specified in https://tools.ietf.org/html/rfc7638.
180237
func JWKThumbprint(pub crypto.PublicKey) (string, error) {

acme/jws_test.go

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ func TestJWSEncodeJSONCustom(t *testing.T) {
376376
if err != nil {
377377
t.Fatal(err)
378378
}
379-
var j struct{ Protected, Payload, Signature string }
379+
var j jsonWebSignature
380380
if err := json.Unmarshal(b, &j); err != nil {
381381
t.Fatal(err)
382382
}
@@ -386,8 +386,66 @@ func TestJWSEncodeJSONCustom(t *testing.T) {
386386
if j.Payload != payload {
387387
t.Errorf("j.Payload = %q\nwant %q", j.Payload, payload)
388388
}
389-
if j.Signature != tc.jwsig {
390-
t.Errorf("j.Signature = %q\nwant %q", j.Signature, tc.jwsig)
389+
if j.Sig != tc.jwsig {
390+
t.Errorf("j.Sig = %q\nwant %q", j.Sig, tc.jwsig)
391+
}
392+
})
393+
}
394+
}
395+
396+
func TestJWSWithMAC(t *testing.T) {
397+
// Example from RFC 7520 Section 4.4.3.
398+
// https://tools.ietf.org/html/rfc7520#section-4.4.3
399+
b64Key := "hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg"
400+
alg := MACAlgorithmHS256
401+
rawProtected := []byte(`{"alg":"HS256","kid":"018c0ae5-4d9b-471b-bfd6-eef314bc7037"}`)
402+
rawPayload := []byte("It\xe2\x80\x99s a dangerous business, Frodo, going out your " +
403+
"door. You step onto the road, and if you don't keep your feet, " +
404+
"there\xe2\x80\x99s no knowing where you might be swept off " +
405+
"to.")
406+
protected := "eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LW" +
407+
"VlZjMxNGJjNzAzNyJ9"
408+
payload := "SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywg" +
409+
"Z29pbmcgb3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9h" +
410+
"ZCwgYW5kIGlmIHlvdSBkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXi" +
411+
"gJlzIG5vIGtub3dpbmcgd2hlcmUgeW91IG1pZ2h0IGJlIHN3ZXB0IG9m" +
412+
"ZiB0by4"
413+
sig := "s0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0"
414+
415+
key, err := base64.RawURLEncoding.DecodeString(b64Key)
416+
if err != nil {
417+
t.Fatalf("unable to decode key: %q", b64Key)
418+
}
419+
got, err := jwsWithMAC(key, alg, rawProtected, rawPayload)
420+
if err != nil {
421+
t.Fatalf("jwsWithMAC() = %q", err)
422+
}
423+
if got.Protected != protected {
424+
t.Errorf("got.Protected = %q\nwant %q", got.Protected, protected)
425+
}
426+
if got.Payload != payload {
427+
t.Errorf("got.Payload = %q\nwant %q", got.Payload, payload)
428+
}
429+
if got.Sig != sig {
430+
t.Errorf("got.Signature = %q\nwant %q", got.Sig, sig)
431+
}
432+
}
433+
434+
func TestJWSWithMACError(t *testing.T) {
435+
tt := []struct {
436+
desc string
437+
alg MACAlgorithm
438+
key []byte
439+
}{
440+
{"Unknown Algorithm", MACAlgorithm("UNKNOWN-ALG"), []byte("hmac-key")},
441+
{"Empty Key", MACAlgorithmHS256, nil},
442+
}
443+
for _, tc := range tt {
444+
tc := tc
445+
t.Run(string(tc.desc), func(t *testing.T) {
446+
p := "{}"
447+
if _, err := jwsWithMAC(tc.key, tc.alg, []byte(p), []byte(p)); err == nil {
448+
t.Errorf("jwsWithMAC(%v, %v, %s, %s) = success; want err", tc.key, tc.alg, p, p)
391449
}
392450
})
393451
}
@@ -467,3 +525,33 @@ func TestJWKThumbprintErrUnsupportedKey(t *testing.T) {
467525
t.Errorf("err = %q; want %q", err, ErrUnsupportedKey)
468526
}
469527
}
528+
529+
func TestNewHMAC(t *testing.T) {
530+
tt := []struct {
531+
alg MACAlgorithm
532+
wantSize int
533+
}{
534+
{MACAlgorithmHS256, 32},
535+
{MACAlgorithmHS384, 48},
536+
{MACAlgorithmHS512, 64},
537+
}
538+
for _, tc := range tt {
539+
tc := tc
540+
t.Run(string(tc.alg), func(t *testing.T) {
541+
h, err := newHMAC([]byte("key"), tc.alg)
542+
if err != nil {
543+
t.Fatalf("newHMAC(%v) = %q", tc.alg, err)
544+
}
545+
gotSize := len(h.Sum(nil))
546+
if gotSize != tc.wantSize {
547+
t.Errorf("HMAC produced signature with unexpected length; got %d want %d", gotSize, tc.wantSize)
548+
}
549+
})
550+
}
551+
}
552+
553+
func TestNewHMACError(t *testing.T) {
554+
if h, err := newHMAC([]byte("key"), MACAlgorithm("UNKNOWN-ALG")); err == nil {
555+
t.Errorf("newHMAC(UNKNOWN-ALG) = %T, nil; want error", h)
556+
}
557+
}

acme/rfc8555.go

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package acme
66

77
import (
8+
"bytes"
89
"context"
910
"crypto"
1011
"encoding/base64"
@@ -37,22 +38,32 @@ func (c *Client) DeactivateReg(ctx context.Context) error {
3738
return nil
3839
}
3940

40-
// registerRFC is quivalent to c.Register but for CAs implementing RFC 8555.
41+
// registerRFC is equivalent to c.Register but for CAs implementing RFC 8555.
4142
// It expects c.Discover to have already been called.
42-
// TODO: Implement externalAccountBinding.
4343
func (c *Client) registerRFC(ctx context.Context, acct *Account, prompt func(tosURL string) bool) (*Account, error) {
4444
c.cacheMu.Lock() // guard c.kid access
4545
defer c.cacheMu.Unlock()
4646

4747
req := struct {
48-
TermsAgreed bool `json:"termsOfServiceAgreed,omitempty"`
49-
Contact []string `json:"contact,omitempty"`
48+
TermsAgreed bool `json:"termsOfServiceAgreed,omitempty"`
49+
Contact []string `json:"contact,omitempty"`
50+
ExternalAccountBinding *jsonWebSignature `json:"externalAccountBinding,omitempty"`
5051
}{
5152
Contact: acct.Contact,
5253
}
5354
if c.dir.Terms != "" {
5455
req.TermsAgreed = prompt(c.dir.Terms)
5556
}
57+
58+
// set 'externalAccountBinding' field if requested
59+
if acct.ExternalAccountBinding != nil {
60+
eabJWS, err := c.encodeExternalAccountBinding(acct.ExternalAccountBinding)
61+
if err != nil {
62+
return nil, fmt.Errorf("acme: failed to encode external account binding: %v", err)
63+
}
64+
req.ExternalAccountBinding = eabJWS
65+
}
66+
5667
res, err := c.post(ctx, c.Key, c.dir.RegURL, req, wantStatus(
5768
http.StatusOK, // account with this key already registered
5869
http.StatusCreated, // new account created
@@ -75,7 +86,19 @@ func (c *Client) registerRFC(ctx context.Context, acct *Account, prompt func(tos
7586
return a, nil
7687
}
7788

78-
// updateGegRFC is equivalent to c.UpdateReg but for CAs implementing RFC 8555.
89+
// encodeExternalAccountBinding will encode an external account binding stanza
90+
// as described in https://tools.ietf.org/html/rfc8555#section-7.3.4.
91+
func (c *Client) encodeExternalAccountBinding(eab *ExternalAccountBinding) (*jsonWebSignature, error) {
92+
jwk, err := jwkEncode(c.Key.Public())
93+
if err != nil {
94+
return nil, err
95+
}
96+
var rProtected bytes.Buffer
97+
fmt.Fprintf(&rProtected, `{"alg":%q,"kid":%q,"url":%q}`, eab.Algorithm, eab.KID, c.dir.RegURL)
98+
return jwsWithMAC(eab.Key, eab.Algorithm, rProtected.Bytes(), []byte(jwk))
99+
}
100+
101+
// updateRegRFC is equivalent to c.UpdateReg but for CAs implementing RFC 8555.
79102
// It expects c.Discover to have already been called.
80103
func (c *Client) updateRegRFC(ctx context.Context, a *Account) (*Account, error) {
81104
url := string(c.accountKID(ctx))

0 commit comments

Comments
 (0)