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 <james@munnelly.eu> Change-Id: Icd0337fddbff49e7e79fb9105c2679609f990285 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/269279 Run-TryBot: Katie Hockman <katie@golang.org> TryBot-Result: Go Bot <gobot@golang.org> Trust: Katie Hockman <katie@golang.org> Trust: Roland Shoemaker <roland@golang.org> Reviewed-by: Roland Shoemaker <roland@golang.org>
This commit is contained in:
committed by
Roland Shoemaker
co-authored by
James Munnelly
parent
8b5274cf68
commit
9d13527586
@@ -363,6 +363,10 @@ func AcceptTOS(tosURL string) bool { return true }
|
||||
// Also see Error's Instance field for when a CA requires already registered accounts to agree
|
||||
// to an updated Terms of Service.
|
||||
func (c *Client) Register(ctx context.Context, acct *Account, prompt func(tosURL string) bool) (*Account, error) {
|
||||
if c.Key == nil {
|
||||
return nil, errors.New("acme: client.Key must be set to Register")
|
||||
}
|
||||
|
||||
dir, err := c.Discover(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -188,6 +188,31 @@ func TestRegister(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterWithoutKey(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "HEAD" {
|
||||
w.Header().Set("Replay-Nonce", "test-nonce")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
fmt.Fprint(w, `{}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
// First verify that using a complete client results in success.
|
||||
c := Client{
|
||||
Key: testKeyEC,
|
||||
DirectoryURL: ts.URL,
|
||||
dir: &Directory{RegURL: ts.URL},
|
||||
}
|
||||
if _, err := c.Register(context.Background(), &Account{}, AcceptTOS); err != nil {
|
||||
t.Fatalf("c.Register() = %v; want success with a complete test client", err)
|
||||
}
|
||||
c.Key = nil
|
||||
if _, err := c.Register(context.Background(), &Account{}, AcceptTOS); err == nil {
|
||||
t.Error("c.Register() from client without key succeeded, wanted error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateReg(t *testing.T) {
|
||||
const terms = "https://ca.tld/acme/terms"
|
||||
contacts := []string{"mailto:admin@example.com"}
|
||||
|
||||
+63
-6
@@ -7,17 +7,31 @@ package acme
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
_ "crypto/sha512" // need for EC keys
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// MACAlgorithm represents a JWS MAC signature algorithm.
|
||||
// See https://tools.ietf.org/html/rfc7518#section-3.1 for more details.
|
||||
type MACAlgorithm string
|
||||
|
||||
const (
|
||||
MACAlgorithmHS256 = MACAlgorithm("HS256")
|
||||
MACAlgorithmHS384 = MACAlgorithm("HS384")
|
||||
MACAlgorithmHS512 = MACAlgorithm("HS512")
|
||||
)
|
||||
|
||||
// keyID is the account identity provided by a CA during registration.
|
||||
type keyID string
|
||||
|
||||
@@ -31,6 +45,14 @@ const noKeyID = keyID("")
|
||||
// See https://tools.ietf.org/html/rfc8555#section-6.3 for more details.
|
||||
const noPayload = ""
|
||||
|
||||
// jsonWebSignature can be easily serialized into a JWS following
|
||||
// https://tools.ietf.org/html/rfc7515#section-3.2.
|
||||
type jsonWebSignature struct {
|
||||
Protected string `json:"protected"`
|
||||
Payload string `json:"payload"`
|
||||
Sig string `json:"signature"`
|
||||
}
|
||||
|
||||
// jwsEncodeJSON signs claimset using provided key and a nonce.
|
||||
// The result is serialized in JSON format containing either kid or jwk
|
||||
// fields based on the provided keyID value.
|
||||
@@ -71,12 +93,7 @@ func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid keyID, nonce, ur
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
enc := struct {
|
||||
Protected string `json:"protected"`
|
||||
Payload string `json:"payload"`
|
||||
Sig string `json:"signature"`
|
||||
}{
|
||||
enc := jsonWebSignature{
|
||||
Protected: phead,
|
||||
Payload: payload,
|
||||
Sig: base64.RawURLEncoding.EncodeToString(sig),
|
||||
@@ -84,6 +101,32 @@ func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid keyID, nonce, ur
|
||||
return json.Marshal(&enc)
|
||||
}
|
||||
|
||||
// jwsWithMAC creates and signs a JWS using the given key and algorithm.
|
||||
// "rawProtected" and "rawPayload" should not be base64-URL-encoded.
|
||||
func jwsWithMAC(key []byte, alg MACAlgorithm, rawProtected, rawPayload []byte) (*jsonWebSignature, error) {
|
||||
if len(key) == 0 {
|
||||
return nil, errors.New("acme: cannot sign JWS with an empty MAC key")
|
||||
}
|
||||
protected := base64.RawURLEncoding.EncodeToString(rawProtected)
|
||||
payload := base64.RawURLEncoding.EncodeToString(rawPayload)
|
||||
|
||||
// Only HMACs are currently supported.
|
||||
hmac, err := newHMAC(key, alg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := hmac.Write([]byte(protected + "." + payload)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mac := hmac.Sum(nil)
|
||||
|
||||
return &jsonWebSignature{
|
||||
Protected: protected,
|
||||
Payload: payload,
|
||||
Sig: base64.RawURLEncoding.EncodeToString(mac),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// jwkEncode encodes public part of an RSA or ECDSA key into a JWK.
|
||||
// The result is also suitable for creating a JWK thumbprint.
|
||||
// https://tools.ietf.org/html/rfc7517
|
||||
@@ -175,6 +218,20 @@ func jwsHasher(pub crypto.PublicKey) (string, crypto.Hash) {
|
||||
return "", 0
|
||||
}
|
||||
|
||||
// newHMAC returns an appropriate HMAC for the given MACAlgorithm.
|
||||
func newHMAC(key []byte, alg MACAlgorithm) (hash.Hash, error) {
|
||||
switch alg {
|
||||
case MACAlgorithmHS256:
|
||||
return hmac.New(sha256.New, key), nil
|
||||
case MACAlgorithmHS384:
|
||||
return hmac.New(sha512.New384, key), nil
|
||||
case MACAlgorithmHS512:
|
||||
return hmac.New(sha512.New, key), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("acme: unsupported MAC algorithm: %v", alg)
|
||||
}
|
||||
}
|
||||
|
||||
// JWKThumbprint creates a JWK thumbprint out of pub
|
||||
// as specified in https://tools.ietf.org/html/rfc7638.
|
||||
func JWKThumbprint(pub crypto.PublicKey) (string, error) {
|
||||
|
||||
+91
-3
@@ -376,7 +376,7 @@ func TestJWSEncodeJSONCustom(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var j struct{ Protected, Payload, Signature string }
|
||||
var j jsonWebSignature
|
||||
if err := json.Unmarshal(b, &j); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -386,8 +386,66 @@ func TestJWSEncodeJSONCustom(t *testing.T) {
|
||||
if j.Payload != payload {
|
||||
t.Errorf("j.Payload = %q\nwant %q", j.Payload, payload)
|
||||
}
|
||||
if j.Signature != tc.jwsig {
|
||||
t.Errorf("j.Signature = %q\nwant %q", j.Signature, tc.jwsig)
|
||||
if j.Sig != tc.jwsig {
|
||||
t.Errorf("j.Sig = %q\nwant %q", j.Sig, tc.jwsig)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWSWithMAC(t *testing.T) {
|
||||
// Example from RFC 7520 Section 4.4.3.
|
||||
// https://tools.ietf.org/html/rfc7520#section-4.4.3
|
||||
b64Key := "hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg"
|
||||
alg := MACAlgorithmHS256
|
||||
rawProtected := []byte(`{"alg":"HS256","kid":"018c0ae5-4d9b-471b-bfd6-eef314bc7037"}`)
|
||||
rawPayload := []byte("It\xe2\x80\x99s a dangerous business, Frodo, going out your " +
|
||||
"door. You step onto the road, and if you don't keep your feet, " +
|
||||
"there\xe2\x80\x99s no knowing where you might be swept off " +
|
||||
"to.")
|
||||
protected := "eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LW" +
|
||||
"VlZjMxNGJjNzAzNyJ9"
|
||||
payload := "SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywg" +
|
||||
"Z29pbmcgb3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9h" +
|
||||
"ZCwgYW5kIGlmIHlvdSBkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXi" +
|
||||
"gJlzIG5vIGtub3dpbmcgd2hlcmUgeW91IG1pZ2h0IGJlIHN3ZXB0IG9m" +
|
||||
"ZiB0by4"
|
||||
sig := "s0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0"
|
||||
|
||||
key, err := base64.RawURLEncoding.DecodeString(b64Key)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to decode key: %q", b64Key)
|
||||
}
|
||||
got, err := jwsWithMAC(key, alg, rawProtected, rawPayload)
|
||||
if err != nil {
|
||||
t.Fatalf("jwsWithMAC() = %q", err)
|
||||
}
|
||||
if got.Protected != protected {
|
||||
t.Errorf("got.Protected = %q\nwant %q", got.Protected, protected)
|
||||
}
|
||||
if got.Payload != payload {
|
||||
t.Errorf("got.Payload = %q\nwant %q", got.Payload, payload)
|
||||
}
|
||||
if got.Sig != sig {
|
||||
t.Errorf("got.Signature = %q\nwant %q", got.Sig, sig)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWSWithMACError(t *testing.T) {
|
||||
tt := []struct {
|
||||
desc string
|
||||
alg MACAlgorithm
|
||||
key []byte
|
||||
}{
|
||||
{"Unknown Algorithm", MACAlgorithm("UNKNOWN-ALG"), []byte("hmac-key")},
|
||||
{"Empty Key", MACAlgorithmHS256, nil},
|
||||
}
|
||||
for _, tc := range tt {
|
||||
tc := tc
|
||||
t.Run(string(tc.desc), func(t *testing.T) {
|
||||
p := "{}"
|
||||
if _, err := jwsWithMAC(tc.key, tc.alg, []byte(p), []byte(p)); err == nil {
|
||||
t.Errorf("jwsWithMAC(%v, %v, %s, %s) = success; want err", tc.key, tc.alg, p, p)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -467,3 +525,33 @@ func TestJWKThumbprintErrUnsupportedKey(t *testing.T) {
|
||||
t.Errorf("err = %q; want %q", err, ErrUnsupportedKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHMAC(t *testing.T) {
|
||||
tt := []struct {
|
||||
alg MACAlgorithm
|
||||
wantSize int
|
||||
}{
|
||||
{MACAlgorithmHS256, 32},
|
||||
{MACAlgorithmHS384, 48},
|
||||
{MACAlgorithmHS512, 64},
|
||||
}
|
||||
for _, tc := range tt {
|
||||
tc := tc
|
||||
t.Run(string(tc.alg), func(t *testing.T) {
|
||||
h, err := newHMAC([]byte("key"), tc.alg)
|
||||
if err != nil {
|
||||
t.Fatalf("newHMAC(%v) = %q", tc.alg, err)
|
||||
}
|
||||
gotSize := len(h.Sum(nil))
|
||||
if gotSize != tc.wantSize {
|
||||
t.Errorf("HMAC produced signature with unexpected length; got %d want %d", gotSize, tc.wantSize)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHMACError(t *testing.T) {
|
||||
if h, err := newHMAC([]byte("key"), MACAlgorithm("UNKNOWN-ALG")); err == nil {
|
||||
t.Errorf("newHMAC(UNKNOWN-ALG) = %T, nil; want error", h)
|
||||
}
|
||||
}
|
||||
|
||||
+28
-5
@@ -5,6 +5,7 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"encoding/base64"
|
||||
@@ -37,22 +38,32 @@ func (c *Client) DeactivateReg(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// registerRFC is quivalent to c.Register but for CAs implementing RFC 8555.
|
||||
// registerRFC is equivalent to c.Register but for CAs implementing RFC 8555.
|
||||
// It expects c.Discover to have already been called.
|
||||
// TODO: Implement externalAccountBinding.
|
||||
func (c *Client) registerRFC(ctx context.Context, acct *Account, prompt func(tosURL string) bool) (*Account, error) {
|
||||
c.cacheMu.Lock() // guard c.kid access
|
||||
defer c.cacheMu.Unlock()
|
||||
|
||||
req := struct {
|
||||
TermsAgreed bool `json:"termsOfServiceAgreed,omitempty"`
|
||||
Contact []string `json:"contact,omitempty"`
|
||||
TermsAgreed bool `json:"termsOfServiceAgreed,omitempty"`
|
||||
Contact []string `json:"contact,omitempty"`
|
||||
ExternalAccountBinding *jsonWebSignature `json:"externalAccountBinding,omitempty"`
|
||||
}{
|
||||
Contact: acct.Contact,
|
||||
}
|
||||
if c.dir.Terms != "" {
|
||||
req.TermsAgreed = prompt(c.dir.Terms)
|
||||
}
|
||||
|
||||
// set 'externalAccountBinding' field if requested
|
||||
if acct.ExternalAccountBinding != nil {
|
||||
eabJWS, err := c.encodeExternalAccountBinding(acct.ExternalAccountBinding)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acme: failed to encode external account binding: %v", err)
|
||||
}
|
||||
req.ExternalAccountBinding = eabJWS
|
||||
}
|
||||
|
||||
res, err := c.post(ctx, c.Key, c.dir.RegURL, req, wantStatus(
|
||||
http.StatusOK, // account with this key already registered
|
||||
http.StatusCreated, // new account created
|
||||
@@ -75,7 +86,19 @@ func (c *Client) registerRFC(ctx context.Context, acct *Account, prompt func(tos
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// updateGegRFC is equivalent to c.UpdateReg but for CAs implementing RFC 8555.
|
||||
// encodeExternalAccountBinding will encode an external account binding stanza
|
||||
// as described in https://tools.ietf.org/html/rfc8555#section-7.3.4.
|
||||
func (c *Client) encodeExternalAccountBinding(eab *ExternalAccountBinding) (*jsonWebSignature, error) {
|
||||
jwk, err := jwkEncode(c.Key.Public())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rProtected bytes.Buffer
|
||||
fmt.Fprintf(&rProtected, `{"alg":%q,"kid":%q,"url":%q}`, eab.Algorithm, eab.KID, c.dir.RegURL)
|
||||
return jwsWithMAC(eab.Key, eab.Algorithm, rProtected.Bytes(), []byte(jwk))
|
||||
}
|
||||
|
||||
// updateRegRFC is equivalent to c.UpdateReg but for CAs implementing RFC 8555.
|
||||
// It expects c.Discover to have already been called.
|
||||
func (c *Client) updateRegRFC(ctx context.Context, a *Account) (*Account, error) {
|
||||
url := string(c.accountKID(ctx))
|
||||
|
||||
@@ -7,9 +7,12 @@ package acme
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
@@ -346,6 +349,145 @@ func TestRFC_Register(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRFC_RegisterExternalAccountBinding(t *testing.T) {
|
||||
eab := &ExternalAccountBinding{
|
||||
KID: "kid-1",
|
||||
Key: []byte("secret"),
|
||||
Algorithm: MACAlgorithmHS256,
|
||||
}
|
||||
|
||||
type protected struct {
|
||||
Algorithm string `json:"alg"`
|
||||
KID string `json:"kid"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
const email = "mailto:user@example.org"
|
||||
|
||||
s := newACMEServer()
|
||||
s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Location", s.url("/accounts/1"))
|
||||
if r.Method != "POST" {
|
||||
t.Errorf("r.Method = %q; want POST", r.Method)
|
||||
}
|
||||
|
||||
var j struct {
|
||||
Protected string
|
||||
Contact []string
|
||||
TermsOfServiceAgreed bool
|
||||
ExternalaccountBinding struct {
|
||||
Protected string
|
||||
Payload string
|
||||
Signature string
|
||||
}
|
||||
}
|
||||
decodeJWSRequest(t, &j, r.Body)
|
||||
protData, err := base64.RawURLEncoding.DecodeString(j.ExternalaccountBinding.Protected)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var prot protected
|
||||
err = json.Unmarshal(protData, &prot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(j.Contact, []string{email}) {
|
||||
t.Errorf("j.Contact = %v; want %v", j.Contact, []string{email})
|
||||
}
|
||||
if !j.TermsOfServiceAgreed {
|
||||
t.Error("j.TermsOfServiceAgreed = false; want true")
|
||||
}
|
||||
|
||||
// Ensure same KID.
|
||||
if prot.KID != eab.KID {
|
||||
t.Errorf("j.ExternalAccountBinding.KID = %s; want %s", prot.KID, eab.KID)
|
||||
}
|
||||
// Ensure same Algorithm.
|
||||
if prot.Algorithm != string(eab.Algorithm) {
|
||||
t.Errorf("j.ExternalAccountBinding.Alg = %s; want %s",
|
||||
prot.Algorithm, eab.Algorithm)
|
||||
}
|
||||
|
||||
// Ensure same URL as outer JWS.
|
||||
url := fmt.Sprintf("http://%s/acme/new-account", r.Host)
|
||||
if prot.URL != url {
|
||||
t.Errorf("j.ExternalAccountBinding.URL = %s; want %s",
|
||||
prot.URL, url)
|
||||
}
|
||||
|
||||
// Ensure payload is base64URL encoded string of JWK in outer JWS
|
||||
jwk, err := jwkEncode(testKeyEC.Public())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
decodedPayload, err := base64.RawURLEncoding.DecodeString(j.ExternalaccountBinding.Payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if jwk != string(decodedPayload) {
|
||||
t.Errorf("j.ExternalAccountBinding.Payload = %s; want %s", decodedPayload, jwk)
|
||||
}
|
||||
|
||||
// Check signature on inner external account binding JWS
|
||||
hmac := hmac.New(sha256.New, []byte("secret"))
|
||||
_, err = hmac.Write([]byte(j.ExternalaccountBinding.Protected + "." + j.ExternalaccountBinding.Payload))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mac := hmac.Sum(nil)
|
||||
encodedMAC := base64.RawURLEncoding.EncodeToString(mac)
|
||||
|
||||
if !bytes.Equal([]byte(encodedMAC), []byte(j.ExternalaccountBinding.Signature)) {
|
||||
t.Errorf("j.ExternalAccountBinding.Signature = %v; want %v",
|
||||
[]byte(j.ExternalaccountBinding.Signature), encodedMAC)
|
||||
}
|
||||
|
||||
w.Header().Set("Location", s.url("/accounts/1"))
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
b, _ := json.Marshal([]string{email})
|
||||
fmt.Fprintf(w, `{"status":"valid","orders":"%s","contact":%s}`, s.url("/accounts/1/orders"), b)
|
||||
})
|
||||
s.start()
|
||||
defer s.close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
cl := &Client{
|
||||
Key: testKeyEC,
|
||||
DirectoryURL: s.url("/"),
|
||||
}
|
||||
|
||||
var didPrompt bool
|
||||
a := &Account{Contact: []string{email}, ExternalAccountBinding: eab}
|
||||
acct, err := cl.Register(ctx, a, func(tos string) bool {
|
||||
didPrompt = true
|
||||
terms := s.url("/terms")
|
||||
if tos != terms {
|
||||
t.Errorf("tos = %q; want %q", tos, terms)
|
||||
}
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
okAccount := &Account{
|
||||
URI: s.url("/accounts/1"),
|
||||
Status: StatusValid,
|
||||
Contact: []string{email},
|
||||
OrdersURL: s.url("/accounts/1/orders"),
|
||||
}
|
||||
if !reflect.DeepEqual(acct, okAccount) {
|
||||
t.Errorf("acct = %+v; want %+v", acct, okAccount)
|
||||
}
|
||||
if !didPrompt {
|
||||
t.Error("tos prompt wasn't called")
|
||||
}
|
||||
if v := cl.accountKID(ctx); v != keyID(okAccount.URI) {
|
||||
t.Errorf("account kid = %q; want %q", v, okAccount.URI)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRFC_RegisterExisting(t *testing.T) {
|
||||
s := newACMEServer()
|
||||
s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -199,6 +199,31 @@ type Account struct {
|
||||
//
|
||||
// It is non-RFC 8555 compliant and is obsoleted by OrdersURL.
|
||||
Certificates string
|
||||
|
||||
// ExternalAccountBinding represents an arbitrary binding to an account of
|
||||
// the CA which the ACME server is tied to.
|
||||
// See https://tools.ietf.org/html/rfc8555#section-7.3.4 for more details.
|
||||
ExternalAccountBinding *ExternalAccountBinding
|
||||
}
|
||||
|
||||
// ExternalAccountBinding contains the data needed to form a request with
|
||||
// an external account binding.
|
||||
// See https://tools.ietf.org/html/rfc8555#section-7.3.4 for more details.
|
||||
type ExternalAccountBinding struct {
|
||||
// KID is the Key ID of the symmetric MAC key that the CA provides to
|
||||
// identify an external account from ACME.
|
||||
KID string
|
||||
|
||||
// Key is the bytes of the symmetric key that the CA provides to identify
|
||||
// the account. Key must correspond to the KID.
|
||||
Key []byte
|
||||
|
||||
// Algorithm used to sign the JWS.
|
||||
Algorithm MACAlgorithm
|
||||
}
|
||||
|
||||
func (e *ExternalAccountBinding) String() string {
|
||||
return fmt.Sprintf("&{KID: %q, Key: redacted, Algorithm: %v}", e.KID, e.Algorithm)
|
||||
}
|
||||
|
||||
// Directory is ACME server discovery data.
|
||||
|
||||
@@ -11,6 +11,19 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestExternalAccountBindingString(t *testing.T) {
|
||||
eab := ExternalAccountBinding{
|
||||
KID: "kid",
|
||||
Key: []byte("key"),
|
||||
Algorithm: MACAlgorithmHS256,
|
||||
}
|
||||
got := eab.String()
|
||||
want := `&{KID: "kid", Key: redacted, Algorithm: HS256}`
|
||||
if got != want {
|
||||
t.Errorf("eab.String() = %q, want: %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimit(t *testing.T) {
|
||||
now := time.Date(2017, 04, 27, 10, 0, 0, 0, time.UTC)
|
||||
f := timeNow
|
||||
|
||||
Reference in New Issue
Block a user