|
|
@@ -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
|
|
|
}
|