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:
committed by
Brad Fitzpatrick
parent
d81fdb778b
commit
c2f4947f41
+68
-29
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user