Browse Source

acme: default values and discovery

Add Directory to the Client fields. This removes ACME resource URL
argument from almost all of the Client methods and makes it easier to
use.

Change-Id: I245c975fda914d50fe62ebbec55a4fcf34cb57f2
Reviewed-on: https://go-review.googlesource.com/23972
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Alex Vaghin 9 years ago
parent
commit
c2f4947f41
2 changed files with 86 additions and 46 deletions
  1. 68 29
      acme/internal/acme/acme.go
  2. 18 17
      acme/internal/acme/acme_test.go

+ 68 - 29
acme/internal/acme/acme.go

@@ -19,32 +19,68 @@ import (
 	"net/http"
 	"strconv"
 	"strings"
+	"sync"
 	"time"
 
 	"golang.org/x/net/context"
 )
 
+// LetsEncryptURL is the Directory endpoint of Let's Encrypt CA.
+const LetsEncryptURL = "https://acme-v01.api.letsencrypt.org/directory"
+
 // Client is an ACME client.
+// The only required field is Key. An example of creating a client with a new key
+// is as follows:
+//
+// 	key, err := rsa.GenerateKey(rand.Reader, 2048)
+// 	if err != nil {
+// 		log.Fatal(err)
+// 	}
+// 	client := &Client{Key: key}
+//
 type Client struct {
 	// HTTPClient optionally specifies an HTTP client to use
 	// instead of http.DefaultClient.
 	HTTPClient *http.Client
 
-	// Key is the account key used to register with a CA
-	// and sign requests.
+	// Key is the account key used to register with a CA and sign requests.
 	Key *rsa.PrivateKey
+
+	// DirectoryURL points to the CA directory endpoint.
+	// If empty, LetsEncryptURL is used.
+	// Mutating this value after a successful call of Client's Discover method
+	// will have no effect.
+	DirectoryURL string
+
+	dirMu sync.Mutex // guards writes to dir
+	dir   *Directory // cached result of Client's Discover method
 }
 
-// Discover performs ACME server discovery using the provided discovery endpoint URL.
-func (c *Client) Discover(url string) (*Directory, error) {
-	res, err := c.httpClient().Get(url)
+// Discover performs ACME server discovery using c.DirectoryURL.
+//
+// It caches successful result. So, subsequent calls will not result in
+// a network round-trip. This also means mutating c.DirectoryURL after successful call
+// of this method will have no effect.
+func (c *Client) Discover() (Directory, error) {
+	c.dirMu.Lock()
+	defer c.dirMu.Unlock()
+	if c.dir != nil {
+		return *c.dir, nil
+	}
+
+	dirURL := c.DirectoryURL
+	if dirURL == "" {
+		dirURL = LetsEncryptURL
+	}
+	res, err := c.httpClient().Get(dirURL)
 	if err != nil {
-		return nil, err
+		return Directory{}, err
 	}
 	defer res.Body.Close()
 	if res.StatusCode != http.StatusOK {
-		return nil, responseError(res)
+		return Directory{}, responseError(res)
 	}
+
 	var v struct {
 		Reg    string `json:"new-reg"`
 		Authz  string `json:"new-authz"`
@@ -57,9 +93,9 @@ func (c *Client) Discover(url string) (*Directory, error) {
 		}
 	}
 	if json.NewDecoder(res.Body).Decode(&v); err != nil {
-		return nil, err
+		return Directory{}, err
 	}
-	return &Directory{
+	c.dir = &Directory{
 		RegURL:    v.Reg,
 		AuthzURL:  v.Authz,
 		CertURL:   v.Cert,
@@ -67,7 +103,8 @@ func (c *Client) Discover(url string) (*Directory, error) {
 		Terms:     v.Meta.Terms,
 		Website:   v.Meta.Website,
 		CAA:       v.Meta.CAA,
-	}, nil
+	}
+	return *c.dir, nil
 }
 
 // CreateCert requests a new certificate.
@@ -76,9 +113,12 @@ func (c *Client) Discover(url string) (*Directory, error) {
 // In such scenario the caller can cancel the polling with ctx.
 //
 // If the bundle is true, the returned value will also contain CA (the issuer) certificate.
-// The url argument is an Directory.CertURL value, typically obtained from c.Discover.
 // The csr is a DER encoded certificate signing request.
-func (c *Client) CreateCert(ctx context.Context, url string, csr []byte, exp time.Duration, bundle bool) (der [][]byte, certURL string, err error) {
+func (c *Client) CreateCert(ctx context.Context, csr []byte, exp time.Duration, bundle bool) (der [][]byte, certURL string, err error) {
+	if _, err := c.Discover(); err != nil {
+		return nil, "", err
+	}
+
 	req := struct {
 		Resource  string `json:"resource"`
 		CSR       string `json:"csr"`
@@ -94,7 +134,7 @@ func (c *Client) CreateCert(ctx context.Context, url string, csr []byte, exp tim
 		req.NotAfter = now.Add(exp).Format(time.RFC3339)
 	}
 
-	res, err := c.postJWS(url, req)
+	res, err := c.postJWS(c.dir.CertURL, req)
 	if err != nil {
 		return nil, "", err
 	}
@@ -118,9 +158,7 @@ func (c *Client) CreateCert(ctx context.Context, url string, csr []byte, exp tim
 // It retries the request until the certificate is successfully retrieved,
 // context is cancelled by the caller or an error response is received.
 //
-// The returned value will also contain CA (the issuer) certificate if bundle == true.
-//
-// http.DefaultClient is used if client argument is nil.
+// The returned value will also contain CA (the issuer) certificate if bundle is true.
 func (c *Client) FetchCert(ctx context.Context, url string, bundle bool) ([][]byte, error) {
 	for {
 		res, err := c.httpClient().Get(url)
@@ -149,14 +187,15 @@ func (c *Client) FetchCert(ctx context.Context, url string, bundle bool) ([][]by
 
 // Register creates a new account registration by following the "new-reg" flow.
 // It returns registered account. The a argument is not modified.
-//
-// The url argument is typically an Directory.RegURL obtained from c.Discover.
-func (c *Client) Register(url string, a *Account) (*Account, error) {
-	return c.doReg(url, "new-reg", a)
+func (c *Client) Register(a *Account) (*Account, error) {
+	if _, err := c.Discover(); err != nil {
+		return nil, err
+	}
+	return c.doReg(c.dir.RegURL, "new-reg", a)
 }
 
 // GetReg retrieves an existing registration.
-// The url argument is an Account.URI, typically obtained from c.Register.
+// The url argument is an Account URI.
 func (c *Client) GetReg(url string) (*Account, error) {
 	a := &Account{URI: url}
 	return c.doReg(url, "reg", a)
@@ -164,18 +203,18 @@ func (c *Client) GetReg(url string) (*Account, error) {
 
 // UpdateReg updates an existing registration.
 // It returns an updated account copy. The provided account is not modified.
-//
-// The url argument is an Account.URI, usually obtained with c.Register.
-func (c *Client) UpdateReg(url string, a *Account) (*Account, error) {
-	return c.doReg(url, "reg", a)
+func (c *Client) UpdateReg(a *Account) (*Account, error) {
+	return c.doReg(a.URI, "reg", a)
 }
 
 // Authorize performs the initial step in an authorization flow.
 // The caller will then need to choose from and perform a set of returned
 // challenges using c.Accept in order to successfully complete authorization.
-//
-// The url argument is an authz URL, usually obtained with c.Register.
-func (c *Client) Authorize(url, domain string) (*Authorization, error) {
+func (c *Client) Authorize(domain string) (*Authorization, error) {
+	if _, err := c.Discover(); err != nil {
+		return nil, err
+	}
+
 	type authzID struct {
 		Type  string `json:"type"`
 		Value string `json:"value"`
@@ -187,7 +226,7 @@ func (c *Client) Authorize(url, domain string) (*Authorization, error) {
 		Resource:   "new-authz",
 		Identifier: authzID{Type: "dns", Value: domain},
 	}
-	res, err := c.postJWS(url, req)
+	res, err := c.postJWS(c.dir.AuthzURL, req)
 	if err != nil {
 		return nil, err
 	}

+ 18 - 17
acme/internal/acme/acme_test.go

@@ -58,21 +58,22 @@ func TestDiscover(t *testing.T) {
 		}`, reg, authz, cert, revoke)
 	}))
 	defer ts.Close()
-	ep, err := (&Client{}).Discover(ts.URL)
+	c := Client{DirectoryURL: ts.URL}
+	dir, err := c.Discover()
 	if err != nil {
 		t.Fatal(err)
 	}
-	if ep.RegURL != reg {
-		t.Errorf("RegURL = %q; want %q", ep.RegURL, reg)
+	if dir.RegURL != reg {
+		t.Errorf("dir.RegURL = %q; want %q", dir.RegURL, reg)
 	}
-	if ep.AuthzURL != authz {
-		t.Errorf("authzURL = %q; want %q", ep.AuthzURL, authz)
+	if dir.AuthzURL != authz {
+		t.Errorf("dir.AuthzURL = %q; want %q", dir.AuthzURL, authz)
 	}
-	if ep.CertURL != cert {
-		t.Errorf("certURL = %q; want %q", ep.CertURL, cert)
+	if dir.CertURL != cert {
+		t.Errorf("dir.CertURL = %q; want %q", dir.CertURL, cert)
 	}
-	if ep.RevokeURL != revoke {
-		t.Errorf("revokeURL = %q; want %q", ep.RevokeURL, revoke)
+	if dir.RevokeURL != revoke {
+		t.Errorf("dir.RevokeURL = %q; want %q", dir.RevokeURL, revoke)
 	}
 }
 
@@ -116,10 +117,10 @@ func TestRegister(t *testing.T) {
 	}))
 	defer ts.Close()
 
-	c := Client{Key: testKey}
+	c := Client{Key: testKey, dir: &Directory{RegURL: ts.URL}}
 	a := &Account{Contact: contacts}
 	var err error
-	if a, err = c.Register(ts.URL, a); err != nil {
+	if a, err = c.Register(a); err != nil {
 		t.Fatal(err)
 	}
 	if a.URI != "https://ca.tld/acme/reg/1" {
@@ -181,9 +182,9 @@ func TestUpdateReg(t *testing.T) {
 	defer ts.Close()
 
 	c := Client{Key: testKey}
-	a := &Account{Contact: contacts, AgreedTerms: terms}
+	a := &Account{URI: ts.URL, Contact: contacts, AgreedTerms: terms}
 	var err error
-	if a, err = c.UpdateReg(ts.URL, a); err != nil {
+	if a, err = c.UpdateReg(a); err != nil {
 		t.Fatal(err)
 	}
 	if a.Authz != "https://ca.tld/acme/new-authz" {
@@ -311,8 +312,8 @@ func TestAuthorize(t *testing.T) {
 	}))
 	defer ts.Close()
 
-	cl := Client{Key: testKey}
-	auth, err := cl.Authorize(ts.URL, "example.com")
+	cl := Client{Key: testKey, dir: &Directory{AuthzURL: ts.URL}}
+	auth, err := cl.Authorize("example.com")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -605,8 +606,8 @@ func TestNewCert(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	c := Client{Key: testKey}
-	cert, certURL, err := c.CreateCert(context.Background(), ts.URL, csrb, notAfter.Sub(notBefore), false)
+	c := Client{Key: testKey, dir: &Directory{CertURL: ts.URL}}
+	cert, certURL, err := c.CreateCert(context.Background(), csrb, notAfter.Sub(notBefore), false)
 	if err != nil {
 		t.Fatal(err)
 	}