1// Package acmeapi provides an API for accessing ACME servers.
2//
3// See type RealmClient for introductory documentation.
4package acmeapi // import "gopkg.in/hlandau/acmeapi.v2"
5
6import (
7	"context"
8	"crypto"
9	"crypto/ecdsa"
10	"crypto/rsa"
11	"encoding/json"
12	"errors"
13	"fmt"
14	gnet "github.com/hlandau/goutils/net"
15	"github.com/hlandau/xlog"
16	"github.com/peterhellberg/link"
17	"golang.org/x/net/context/ctxhttp"
18	"gopkg.in/square/go-jose.v2"
19	"io"
20	"mime"
21	"net/http"
22	"net/url"
23	"runtime"
24	"strings"
25	"sync"
26	"sync/atomic"
27	"time"
28)
29
30var log, Log = xlog.NewQuiet("acmeapi")
31
32// Sentinel value for doReq.
33var noAccountNeeded = Account{}
34
35// Internal use only. All ACME URLs must use "https" and not "http". However,
36// for testing purposes, if this is set, "http" URLs will be allowed. This is useful
37// for testing when a test ACME server doesn't have TLS configured.
38var TestingAllowHTTP = false
39
40// You should set this to a string identifying the code invoking this library.
41// Optional.
42//
43// You can alternatively set the user agent on a per-Client basis via
44// Client.UserAgent, but usually a user agent is set at program scope and it
45// makes more sense to set it here.
46var UserAgent string
47
48// Returns true if the URL given is (potentially) a valid ACME resource URL.
49//
50// The URL must be an HTTPS URL.
51func ValidURL(u string) bool {
52	ur, err := url.Parse(u)
53	return err == nil && (ur.Scheme == "https" || (TestingAllowHTTP && ur.Scheme == "http"))
54}
55
56// Configuration data used to instantiate a RealmClient.
57type RealmClientConfig struct {
58	// Optional but usually necessary. The Directory URL for the ACME realm (ACME
59	// server). This must be an HTTPS URL. This will usually be provided via
60	// out-of-band means; it is the root from which all other ACME resources are
61	// accessed.
62	//
63	// Specifying the directory URL is usually necessary, but it can be omitted
64	// in some cases; see the documentation for RealmClient.
65	DirectoryURL string
66
67	// Optional. HTTP client used to make HTTP requests. If this is nil, the
68	// default net/http Client is used, which will suffice in the vast majority
69	// of cases.
70	HTTPClient *http.Client
71
72	// Optional. Custom User-Agent string. If not specified, uses the global
73	// User-Agent string configured at acmeapi package level (UserAgent var).
74	UserAgent string
75}
76
77// Client used to access and mutate resources provided by an ACME server.
78//
79//
80// REALM TERMINOLOGY
81//
82// A “realm” means an ACME server, including all resources provided by it (e.g.
83// accounts, orders, nonces). A nonce can be used to issue a signed request
84// against a resource in a given realm if and only if it that nonce was issued
85// by the same realm. (This term is specific to this client, and not a general
86// ACME term. It is coined here to aid clarity in understanding the scope of
87// ACME resources.)
88//
89// You instantiate a RealmClient to consume the services of a realm. If you
90// want to consume the services of multiple ACME servers — that is, multiple
91// realms — you must create one RealmClient for each such realm. For example,
92// if you wanted to use both the ExampleCA Live ACME server (which issues live
93// certificates) and the ExampleCA Staging ACME server (which issues non-live
94// certificates), you would need to create one RealmClient for each, and make
95// any calls to the right RealmClient. Calling a method on the wrong
96// RealmClient will fail under most circumstances.
97//
98//
99// INSTANTIATION
100//
101// Call NewRealmClient to create a new RealmClient. When you create a RealmClient,
102// you begin by passing the realm's (ACME server's) directory URL as part of
103// the client configuration. This is the entrypoint for the consumption of the
104// services provided by an ACME server realm. See RealmClientConfig for details.
105//
106//
107// DIRECTORY AUTO-DISCOVERY
108//
109// It is possible to instantiate a RealmClient without passing a directory URL.
110// If you do this, it is still possible to access some resources, where their
111// particular URL is explicitly known. Moreover, a RealmClient which has no
112// particular directory URL configured will automatically ascertain the
113// appropriate directory URL when it (if ever) first loads a resource where the
114// response from the server states the directory URL for the realm of which
115// that resource is a member. Once this occurs, that RealmClient is thereafter
116// specific to that realm, and must not be used for other purposes.
117//
118// This directory auto-discovery mechanic is useful when you have an URL for a
119// specific resource of an ACME realm but don't know the directory URL or the
120// identity of the realm or any other information about the realm. This allows
121// e.g. a certificate to be revoked knowing only its URL and private key. (The
122// revocation endpoint is discoverable from the directory resource, which is
123// itself discoverable by a link provided at the certificate resource, which is
124// addressed via the certificate URL.)
125//
126//
127// CONCURRENCY
128//
129// All methods of RealmClient are concurrency-safe. This means you can make
130// multiple in-flight requests. This is useful, for example, when you create a
131// new order and wish to retrieve all the authorizations created as part of it,
132// which are referenced by URL and not serialized inline as part of the order
133// object.
134//
135//
136// STANDARD METHOD ARGUMENTS
137//
138// All methods which involve network I/O, or which may involve network I/O,
139// take a context, to facilitate timeouts and cancellations.
140//
141// All methods which involve making signed requests take an *Account argument.
142// This is used to provide the URL and private key for the account; the other
143// fields of *Account arguments are only used by methods which work directly
144// with account resources.
145//
146// The URL and PrivateKey fields of a provided *Account are mandatory in most cases.
147// They are optional only in the following cases:
148//
149// When calling UpsertAccount, the account URL may be omitted. (If the URL of
150// an existing account is not known, this method may (and must) be used to
151// discover the URL of the account before methods requiring an URL may be
152// called.)
153//
154// When calling Revoke, the account URL and account private key may be omitted,
155// but only if the revocation request is being authorized on the basis of
156// possession of the certificate's corresponding private key. All other
157// revocation requests require an account URL and account private key.
158type RealmClient struct {
159	cfg               RealmClientConfig
160	directoryURLMutex sync.RWMutex // Protects cfg.DirectoryURL.
161
162	nonceSource nonceSource
163
164	dir      atomic.Value // *directoryInfo
165	dirMutex sync.Mutex   // Ensures single flight for directory requests.
166}
167
168// Directory resource structure.
169type directoryInfo struct {
170	NewNonce   string    `json:"newNonce"`
171	NewAccount string    `json:"newAccount"`
172	NewOrder   string    `json:"newOrder"`
173	NewAuthz   string    `json:"newAuthz"`
174	RevokeCert string    `json:"revokeCert"`
175	KeyChange  string    `json:"keyChange"`
176	Meta       RealmMeta `json:"meta"`
177}
178
179// Metadata for a realm, retrieved from the directory resource.
180type RealmMeta struct {
181	// (Sent by server; optional.) If the CA requires agreement to certain terms of
182	// service, this is set to an URL for the terms of service document.
183	TermsOfServiceURL string `json:"termsOfService,omitempty"`
184
185	// (Sent by server; optional.) A website pertaining to the CA.
186	WebsiteURL string `json:"website,omitempty"`
187
188	// (Sent by server; optional.) List of domain names which the CA recognises as
189	// referring to itself for the purposes of CAA record validation.
190	CAAIdentities []string `json:"caaIdentities,omitempty"`
191
192	// (Sent by server; optional.) As per specification.
193	ExternalAccountRequired bool `json:"externalAccountRequired,omitempty"`
194}
195
196// Instantiates a new RealmClient.
197func NewRealmClient(cfg RealmClientConfig) (*RealmClient, error) {
198	rc := &RealmClient{
199		cfg: cfg,
200	}
201
202	if rc.cfg.DirectoryURL != "" && !ValidURL(rc.cfg.DirectoryURL) {
203		return nil, fmt.Errorf("not a valid directory URL: %q", rc.cfg.DirectoryURL)
204	}
205
206	rc.nonceSource.GetNonceFunc = rc.obtainNewNonce
207
208	return rc, nil
209}
210
211func (c *RealmClient) getDirectoryURL() string {
212	c.directoryURLMutex.RLock()
213	defer c.directoryURLMutex.RUnlock()
214
215	return c.cfg.DirectoryURL
216}
217
218// Directory Retrieval
219
220// Returns the directory information for the realm accessed by the RealmClient.
221//
222// This may return instantly (if the directory information has already been
223// retrieved and cached), or may cause a request to be made to retrieve and
224// cache the information, hence the context argument.
225//
226// Multiple concurrent calls to getDirectory with no directory information
227// cached result only in a single request being made; all of the callers to
228// getDirectory wait for the single request.
229func (c *RealmClient) getDirectory(ctx context.Context) (*directoryInfo, error) {
230	dir := c.getDirp()
231	if dir != nil {
232		return dir, nil
233	}
234
235	c.dirMutex.Lock()
236	defer c.dirMutex.Unlock()
237
238	if dir := c.getDirp(); dir != nil {
239		return dir, nil
240	}
241
242	dir, err := c.getDirectoryActual(ctx)
243	if err != nil {
244		return nil, err
245	}
246
247	c.setDirp(dir)
248	return dir, nil
249}
250
251func (c *RealmClient) getDirp() *directoryInfo {
252	v, _ := c.dir.Load().(*directoryInfo)
253	return v
254}
255
256func (c *RealmClient) setDirp(d *directoryInfo) {
257	c.dir.Store(d)
258}
259
260// Error returned when directory URL was needed for an operation but it is unknown.
261var ErrUnknownDirectoryURL = errors.New("unable to retrieve directory because the directory URL is unknown")
262
263// Error returned if directory does not provide endpoints required by the specification.
264var ErrMissingEndpoints = errors.New("directory does not provide required endpoints")
265
266// Make actual request to retrieve directory.
267func (c *RealmClient) getDirectoryActual(ctx context.Context) (*directoryInfo, error) {
268	directoryURL := c.getDirectoryURL()
269	if directoryURL == "" {
270		return nil, ErrUnknownDirectoryURL
271	}
272
273	var dir *directoryInfo
274	_, err := c.doReq(ctx, "GET", directoryURL, nil, nil, nil, &dir)
275	if err != nil {
276		return nil, err
277	}
278
279	if !ValidURL(dir.NewNonce) || !ValidURL(dir.NewAccount) || !ValidURL(dir.NewOrder) {
280		return nil, ErrMissingEndpoints
281	}
282
283	return dir, nil
284}
285
286// Returns the directory metadata for the realm.
287//
288// This method must be used to retrieve the realm's current Terms of Service
289// URI when calling UpsertAccount.
290func (c *RealmClient) GetMeta(ctx context.Context) (RealmMeta, error) {
291	di, err := c.getDirectory(ctx)
292	if err != nil {
293		return RealmMeta{}, err
294	}
295
296	return di.Meta, nil
297}
298
299// This method is configured as the GetNewNonce function for the nonceSource
300// which constitutes part of the RealmClient. It is called if the nonceSource's
301// cache of nonces is empty, meaning that an HTTP request must be made to
302// retrieve a new nonce.
303func (c *RealmClient) obtainNewNonce(ctx context.Context) error {
304	di, err := c.getDirectory(ctx)
305	if err != nil {
306		return err
307	}
308
309	// We don't need to cache the nonce explicitly; doReq automatically caches
310	// any fresh nonces provided in a reply.
311	res, err := c.doReq(ctx, "HEAD", di.NewNonce, nil, nil, nil, nil)
312	if res != nil {
313		res.Body.Close()
314	}
315
316	return err
317}
318
319// Request Methods
320
321// Makes an ACME request.
322//
323// method: HTTP method in uppercase.
324//
325// url: Absolute HTTPS URL.
326//
327// requestData: If non-nil, signed and sent as request body.
328//
329// responseData: If non-nil, response, if JSON, is unmarshalled into this.
330//
331// acct: Mandatory if requestData is non-nil. This is used to determine the
332// account URL, which is used for signing requests. If key is nil, acct.PrivateKey
333// is used to sign the request. If the request should be signed with an embedded JWK
334// rather than an URL account reference, pass the special sentinel value
335// &noAccountNeeded.
336//
337// key: Overrides the private key used; used instead of acct.PrivateKey if non-nil.
338// The HTTP response structure is returned; the state of the Body stream is undefined,
339// but need not be manually closed if err is non-nil or if responseData is non-nil.
340func (c *RealmClient) doReq(ctx context.Context, method, url string, acct *Account, key crypto.PrivateKey, requestData, responseData interface{}) (*http.Response, error) {
341	return c.doReqAccept(ctx, method, url, "application/json", acct, key, requestData, responseData)
342}
343
344func (c *RealmClient) doReqAccept(ctx context.Context, method, url, accepts string, acct *Account, key crypto.PrivateKey, requestData, responseData interface{}) (*http.Response, error) {
345	backoff := gnet.Backoff{
346		MaxTries:           20,
347		InitialDelay:       100 * time.Millisecond,
348		MaxDelay:           1 * time.Second,
349		MaxDelayAfterTries: 4,
350		Jitter:             0.10,
351	}
352
353	for {
354		res, err := c.doReqOneTry(ctx, method, url, accepts, acct, key, requestData, responseData)
355		if err == nil {
356			return res, nil
357		}
358
359		// If the error is specifically a "bad nonce" error, we are supposed to
360		// retry.
361		if he, ok := err.(*HTTPError); ok && he.Problem != nil && he.Problem.Type == "urn:ietf:params:acme:error:badNonce" {
362			if backoff.Sleep() {
363				log.Debugf("retrying after bad nonce: %v\n", he)
364				continue
365			}
366		}
367
368		// Other error, return.
369		return res, err
370	}
371}
372
373func (c *RealmClient) doReqOneTry(ctx context.Context, method, url, accepts string, acct *Account, key crypto.PrivateKey, requestData, responseData interface{}) (*http.Response, error) {
374	// Check input.
375	if !ValidURL(url) {
376		return nil, fmt.Errorf("invalid request URL: %q", url)
377	}
378
379	// Request marshalling and signing.
380	var rdr io.Reader
381	if requestData != nil {
382		if acct == nil {
383			return nil, fmt.Errorf("must provide account object when making signed requests")
384		}
385
386		if key == nil {
387			key = acct.PrivateKey
388		}
389
390		if key == nil {
391			return nil, fmt.Errorf("account key must be specified")
392		}
393
394		var b []byte
395		var err error
396		if s, ok := requestData.(string); ok && s == "" {
397			b = []byte{}
398		} else {
399			b, err = json.Marshal(requestData)
400			if err != nil {
401				return nil, err
402			}
403		}
404
405		kalg, err := algorithmFromKey(key)
406		if err != nil {
407			return nil, err
408		}
409
410		signKey := jose.SigningKey{
411			Algorithm: kalg,
412			Key:       key,
413		}
414		extraHeaders := map[jose.HeaderKey]interface{}{
415			"url": url,
416		}
417		useInlineKey := (acct == &noAccountNeeded)
418		if !useInlineKey {
419			accountURL := acct.URL
420			if !ValidURL(accountURL) {
421				return nil, fmt.Errorf("acct must have a valid URL, not %q", accountURL)
422			}
423
424			extraHeaders["kid"] = accountURL
425		}
426
427		signOptions := jose.SignerOptions{
428			NonceSource:  c.nonceSource.WithContext(ctx),
429			EmbedJWK:     useInlineKey,
430			ExtraHeaders: extraHeaders,
431		}
432
433		signer, err := jose.NewSigner(signKey, &signOptions)
434		if err != nil {
435			return nil, err
436		}
437
438		sig, err := signer.Sign(b)
439		if err != nil {
440			return nil, err
441		}
442
443		s := sig.FullSerialize()
444		if err != nil {
445			return nil, err
446		}
447
448		rdr = strings.NewReader(s)
449	}
450
451	// Make request.
452	req, err := http.NewRequest(method, url, rdr)
453	if err != nil {
454		return nil, err
455	}
456
457	req.Header.Set("Accept", accepts)
458	if method != "GET" && method != "HEAD" {
459		req.Header.Set("Content-Type", "application/jose+json")
460	}
461
462	res, err := c.doReqServer(ctx, req)
463	if err != nil {
464		return res, err
465	}
466
467	// Otherwise, if we are expecting response data, unmarshal into the provided
468	// struct.
469	if responseData != nil {
470		defer res.Body.Close()
471
472		mimeType, params, err := mime.ParseMediaType(res.Header.Get("Content-Type"))
473		if err != nil {
474			return res, err
475		}
476
477		err = validateContentType(mimeType, params, "application/json")
478		if err != nil {
479			return res, err
480		}
481
482		err = json.NewDecoder(res.Body).Decode(responseData)
483		if err != nil {
484			return res, err
485		}
486	}
487
488	// Done.
489	return res, nil
490}
491
492func validateContentType(mimeType string, params map[string]string, expectedMimeType string) error {
493	if mimeType != expectedMimeType {
494		return fmt.Errorf("unexpected response content type: %q", mimeType)
495	}
496
497	if ch, ok := params["charset"]; ok && ch != "" && strings.ToLower(ch) != "utf-8" {
498		return fmt.Errorf("content type charset is not UTF-8: %q, %q", mimeType, ch)
499	}
500
501	return nil
502}
503
504// Make an HTTP request to an ACME endpoint.
505func (c *RealmClient) doReqServer(ctx context.Context, req *http.Request) (*http.Response, error) {
506	res, err := c.doReqActual(ctx, req)
507	if err != nil {
508		return nil, err
509	}
510
511	// If the response includes a nonce, add it to our cache of nonces.
512	if n := res.Header.Get("Replay-Nonce"); n != "" {
513		c.nonceSource.AddNonce(n)
514	}
515
516	// Autodiscover directory URL if it we didn't prevously know it and it's
517	// specified in the response.
518	if c.getDirectoryURL() == "" {
519		func() {
520			c.directoryURLMutex.Lock()
521			defer c.directoryURLMutex.Unlock()
522
523			if c.cfg.DirectoryURL != "" {
524				return
525			}
526
527			if link := link.ParseResponse(res)["index"]; link != nil && ValidURL(link.URI) {
528				c.cfg.DirectoryURL = link.URI
529			}
530		}()
531	}
532
533	// If the response was an error, parse the response body as an error and return.
534	if res.StatusCode >= 400 && res.StatusCode < 600 {
535		defer res.Body.Close()
536		return res, newHTTPError(res)
537	}
538
539	return res, err
540}
541
542// Make an HTTP request. This is used by doReq and can also be used for
543// non-ACME requests (e.g. OCSP).
544func (c *RealmClient) doReqActual(ctx context.Context, req *http.Request) (*http.Response, error) {
545	req.Header.Set("User-Agent", formUserAgent(c.cfg.UserAgent))
546
547	return ctxhttp.Do(ctx, c.cfg.HTTPClient, req)
548}
549
550func algorithmFromKey(key crypto.PrivateKey) (jose.SignatureAlgorithm, error) {
551	switch v := key.(type) {
552	case *rsa.PrivateKey:
553		return jose.RS256, nil
554	case *ecdsa.PrivateKey:
555		name := v.Curve.Params().Name
556		switch name {
557		case "P-256":
558			return jose.ES256, nil
559		case "P-384":
560			return jose.ES384, nil
561		case "P-521":
562			return jose.ES512, nil
563		default:
564			return "", fmt.Errorf("unsupported ECDSA curve: %s", name)
565		}
566	default:
567		return "", fmt.Errorf("unsupported private key type: %T", key)
568	}
569}
570
571func formUserAgent(userAgent string) string {
572	if userAgent == "" {
573		userAgent = UserAgent
574	}
575
576	if userAgent != "" {
577		userAgent += " "
578	}
579
580	return fmt.Sprintf("%sacmeapi/2a Go-http-client/1.1 %s/%s", userAgent, runtime.GOOS, runtime.GOARCH)
581}
582