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