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>
This commit is contained in:
Alex Vaghin
2016-07-09 14:46:51 +00:00
committed by Brad Fitzpatrick
parent d81fdb778b
commit c2f4947f41
2 changed files with 86 additions and 46 deletions
+68 -29
View File
@@ -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
View File
@@ -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)
}