Skip to content

Commit 4663e18

Browse files
x1ddosAlex Vaghin
authored and
Alex Vaghin
committed
acme: update existing methods for RFC 8555
This adds RFC support to the existing methods which, in conjunction with the new order based methods implemented in golang.org/cl/192779, completes a Client capable of obtaining certificates from RFC compliant CAs. Updates golang/go#21081 Change-Id: I3aabc50928d3e4e49ee202eb6695135d5ad86821 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/194379 Reviewed-by: Filippo Valsorda <[email protected]>
1 parent 8834368 commit 4663e18

File tree

6 files changed

+404
-113
lines changed

6 files changed

+404
-113
lines changed

acme/acme.go

Lines changed: 100 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// Package acme provides an implementation of the
66
// Automatic Certificate Management Environment (ACME) spec.
77
// The intial implementation was based on ACME draft-02 and
8-
// is now being extended to comply with RFC8555.
8+
// is now being extended to comply with RFC 8555.
99
// See https://tools.ietf.org/html/draft-ietf-acme-acme-02
1010
// and https://tools.ietf.org/html/rfc8555 for details.
1111
//
@@ -60,7 +60,10 @@ var idPeACMEIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1}
6060

6161
const (
6262
maxChainLen = 5 // max depth and breadth of a certificate chain
63-
maxCertSize = 1 << 20 // max size of a certificate, in bytes
63+
maxCertSize = 1 << 20 // max size of a certificate, in DER bytes
64+
// Used for decoding certs from application/pem-certificate-chain response,
65+
// the default when in RFC mode.
66+
maxCertChainSize = maxCertSize * maxChainLen
6467

6568
// Max number of collected nonces kept in memory.
6669
// Expect usual peak of 1 or 2.
@@ -139,8 +142,7 @@ type Client struct {
139142
func (c *Client) accountKID(ctx context.Context) keyID {
140143
c.cacheMu.Lock()
141144
defer c.cacheMu.Unlock()
142-
if c.dir.OrderURL == "" {
143-
// Assume legacy CA.
145+
if !c.dir.rfcCompliant() {
144146
return noKeyID
145147
}
146148
if c.kid != noKeyID {
@@ -233,6 +235,9 @@ func (c *Client) directoryURL() string {
233235
}
234236

235237
// CreateCert requests a new certificate using the Certificate Signing Request csr encoded in DER format.
238+
// It is incompatible with RFC 8555. Callers should use CreateOrderCert when interfacing
239+
// with an RFC-compliant CA.
240+
//
236241
// The exp argument indicates the desired certificate validity duration. CA may issue a certificate
237242
// with a different duration.
238243
// If the bundle argument is true, the returned value will also contain the CA (issuer) certificate chain.
@@ -284,12 +289,22 @@ func (c *Client) CreateCert(ctx context.Context, csr []byte, exp time.Duration,
284289
// It retries the request until the certificate is successfully retrieved,
285290
// context is cancelled by the caller or an error response is received.
286291
//
287-
// The returned value will also contain the CA (issuer) certificate if the bundle argument is true.
292+
// If the bundle argument is true, the returned value also contains the CA (issuer)
293+
// certificate chain.
288294
//
289295
// FetchCert returns an error if the CA's response or chain was unreasonably large.
290296
// Callers are encouraged to parse the returned value to ensure the certificate is valid
291297
// and has expected features.
292298
func (c *Client) FetchCert(ctx context.Context, url string, bundle bool) ([][]byte, error) {
299+
dir, err := c.Discover(ctx)
300+
if err != nil {
301+
return nil, err
302+
}
303+
if dir.rfcCompliant() {
304+
return c.fetchCertRFC(ctx, url, bundle)
305+
}
306+
307+
// Legacy non-authenticated GET request.
293308
res, err := c.get(ctx, url, wantStatus(http.StatusOK))
294309
if err != nil {
295310
return nil, err
@@ -304,10 +319,15 @@ func (c *Client) FetchCert(ctx context.Context, url string, bundle bool) ([][]by
304319
// For instance, the key pair of the certificate may be authorized.
305320
// If the key is nil, c.Key is used instead.
306321
func (c *Client) RevokeCert(ctx context.Context, key crypto.Signer, cert []byte, reason CRLReasonCode) error {
307-
if _, err := c.Discover(ctx); err != nil {
322+
dir, err := c.Discover(ctx)
323+
if err != nil {
308324
return err
309325
}
326+
if dir.rfcCompliant() {
327+
return c.revokeCertRFC(ctx, key, cert, reason)
328+
}
310329

330+
// Legacy CA.
311331
body := &struct {
312332
Resource string `json:"resource"`
313333
Cert string `json:"certificate"`
@@ -317,7 +337,7 @@ func (c *Client) RevokeCert(ctx context.Context, key crypto.Signer, cert []byte,
317337
Cert: base64.RawURLEncoding.EncodeToString(cert),
318338
Reason: int(reason),
319339
}
320-
res, err := c.post(ctx, key, c.dir.RevokeURL, body, wantStatus(http.StatusOK))
340+
res, err := c.post(ctx, key, dir.RevokeURL, body, wantStatus(http.StatusOK))
321341
if err != nil {
322342
return err
323343
}
@@ -337,7 +357,7 @@ func AcceptTOS(tosURL string) bool { return true }
337357
// Register calls prompt with a TOS URL provided by the CA. Prompt should report
338358
// whether the caller agrees to the terms. To always accept the terms, the caller can use AcceptTOS.
339359
//
340-
// When interfacing with RFC compliant CA, non-RFC8555 compliant fields of acct are ignored
360+
// When interfacing with an RFC-compliant CA, non-RFC 8555 fields of acct are ignored
341361
// and prompt is called if Directory's Terms field is non-zero.
342362
// Also see Error's Instance field for when a CA requires already registered accounts to agree
343363
// to an updated Terms of Service.
@@ -346,9 +366,7 @@ func (c *Client) Register(ctx context.Context, acct *Account, prompt func(tosURL
346366
if err != nil {
347367
return nil, err
348368
}
349-
350-
// RFC8555 compliant account registration.
351-
if dir.OrderURL != "" {
369+
if dir.rfcCompliant() {
352370
return c.registerRFC(ctx, acct, prompt)
353371
}
354372

@@ -370,16 +388,14 @@ func (c *Client) Register(ctx context.Context, acct *Account, prompt func(tosURL
370388

371389
// GetReg retrieves an existing account associated with c.Key.
372390
//
373-
// The url argument is an Account URI used with pre-RFC8555 CAs.
374-
// It is ignored when interfacing with an RFC compliant CA.
391+
// The url argument is an Account URI used with pre-RFC 8555 CAs.
392+
// It is ignored when interfacing with an RFC-compliant CA.
375393
func (c *Client) GetReg(ctx context.Context, url string) (*Account, error) {
376394
dir, err := c.Discover(ctx)
377395
if err != nil {
378396
return nil, err
379397
}
380-
381-
// Assume RFC8555 compliant CA.
382-
if dir.OrderURL != "" {
398+
if dir.rfcCompliant() {
383399
return c.getRegRFC(ctx)
384400
}
385401

@@ -395,16 +411,14 @@ func (c *Client) GetReg(ctx context.Context, url string) (*Account, error) {
395411
// UpdateReg updates an existing registration.
396412
// It returns an updated account copy. The provided account is not modified.
397413
//
398-
// When interfacing with RFC compliant CAs, a.URI is ignored and the account URL
414+
// When interfacing with RFC-compliant CAs, a.URI is ignored and the account URL
399415
// associated with c.Key is used instead.
400416
func (c *Client) UpdateReg(ctx context.Context, acct *Account) (*Account, error) {
401417
dir, err := c.Discover(ctx)
402418
if err != nil {
403419
return nil, err
404420
}
405-
406-
// Assume RFC8555 compliant CA.
407-
if dir.OrderURL != "" {
421+
if dir.rfcCompliant() {
408422
return c.updateRegRFC(ctx, acct)
409423
}
410424

@@ -418,13 +432,21 @@ func (c *Client) UpdateReg(ctx context.Context, acct *Account) (*Account, error)
418432
return a, nil
419433
}
420434

421-
// Authorize performs the initial step in an authorization flow.
435+
// Authorize performs the initial step in the pre-authorization flow,
436+
// as opposed to order-based flow.
422437
// The caller will then need to choose from and perform a set of returned
423438
// challenges using c.Accept in order to successfully complete authorization.
424439
//
440+
// Once complete, the caller can use AuthorizeOrder which the CA
441+
// should provision with the already satisfied authorization.
442+
// For pre-RFC CAs, the caller can proceed directly to requesting a certificate
443+
// using CreateCert method.
444+
//
425445
// If an authorization has been previously granted, the CA may return
426-
// a valid authorization (Authorization.Status is StatusValid). If so, the caller
427-
// need not fulfill any challenge and can proceed to requesting a certificate.
446+
// a valid authorization which has its Status field set to StatusValid.
447+
//
448+
// More about pre-authorization can be found at
449+
// https://tools.ietf.org/html/rfc8555#section-7.4.1.
428450
func (c *Client) Authorize(ctx context.Context, domain string) (*Authorization, error) {
429451
return c.authorize(ctx, "dns", domain)
430452
}
@@ -476,7 +498,17 @@ func (c *Client) authorize(ctx context.Context, typ, val string) (*Authorization
476498
// If a caller needs to poll an authorization until its status is final,
477499
// see the WaitAuthorization method.
478500
func (c *Client) GetAuthorization(ctx context.Context, url string) (*Authorization, error) {
479-
res, err := c.get(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted))
501+
dir, err := c.Discover(ctx)
502+
if err != nil {
503+
return nil, err
504+
}
505+
506+
var res *http.Response
507+
if dir.rfcCompliant() {
508+
res, err = c.postAsGet(ctx, url, wantStatus(http.StatusOK))
509+
} else {
510+
res, err = c.get(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted))
511+
}
480512
if err != nil {
481513
return nil, err
482514
}
@@ -493,8 +525,8 @@ func (c *Client) GetAuthorization(ctx context.Context, url string) (*Authorizati
493525
// The url argument is an Authorization.URI value.
494526
//
495527
// If successful, the caller will be required to obtain a new authorization
496-
// using the Authorize method before being able to request a new certificate
497-
// for the domain associated with the authorization.
528+
// using the Authorize or AuthorizeOrder methods before being able to request
529+
// a new certificate for the domain associated with the authorization.
498530
//
499531
// It does not revoke existing certificates.
500532
func (c *Client) RevokeAuthorization(ctx context.Context, url string) error {
@@ -528,8 +560,18 @@ func (c *Client) RevokeAuthorization(ctx context.Context, url string) error {
528560
// In all other cases WaitAuthorization returns an error.
529561
// If the Status is StatusInvalid, the returned error is of type *AuthorizationError.
530562
func (c *Client) WaitAuthorization(ctx context.Context, url string) (*Authorization, error) {
563+
// Required for c.accountKID() when in RFC mode.
564+
dir, err := c.Discover(ctx)
565+
if err != nil {
566+
return nil, err
567+
}
568+
getfn := c.postAsGet
569+
if !dir.rfcCompliant() {
570+
getfn = c.get
571+
}
572+
531573
for {
532-
res, err := c.get(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted))
574+
res, err := getfn(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted))
533575
if err != nil {
534576
return nil, err
535577
}
@@ -572,10 +614,21 @@ func (c *Client) WaitAuthorization(ctx context.Context, url string) (*Authorizat
572614
//
573615
// A client typically polls a challenge status using this method.
574616
func (c *Client) GetChallenge(ctx context.Context, url string) (*Challenge, error) {
575-
res, err := c.get(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted))
617+
// Required for c.accountKID() when in RFC mode.
618+
dir, err := c.Discover(ctx)
619+
if err != nil {
620+
return nil, err
621+
}
622+
623+
getfn := c.postAsGet
624+
if !dir.rfcCompliant() {
625+
getfn = c.get
626+
}
627+
res, err := getfn(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted))
576628
if err != nil {
577629
return nil, err
578630
}
631+
579632
defer res.Body.Close()
580633
v := wireChallenge{URI: url}
581634
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
@@ -590,23 +643,26 @@ func (c *Client) GetChallenge(ctx context.Context, url string) (*Challenge, erro
590643
// The server will then perform the validation asynchronously.
591644
func (c *Client) Accept(ctx context.Context, chal *Challenge) (*Challenge, error) {
592645
// Required for c.accountKID() when in RFC mode.
593-
if _, err := c.Discover(ctx); err != nil {
594-
return nil, err
595-
}
596-
597-
auth, err := keyAuth(c.Key.Public(), chal.Token)
646+
dir, err := c.Discover(ctx)
598647
if err != nil {
599648
return nil, err
600649
}
601650

602-
req := struct {
603-
Resource string `json:"resource"`
604-
Type string `json:"type"`
605-
Auth string `json:"keyAuthorization"`
606-
}{
607-
Resource: "challenge",
608-
Type: chal.Type,
609-
Auth: auth,
651+
var req interface{} = json.RawMessage("{}") // RFC-compliant CA
652+
if !dir.rfcCompliant() {
653+
auth, err := keyAuth(c.Key.Public(), chal.Token)
654+
if err != nil {
655+
return nil, err
656+
}
657+
req = struct {
658+
Resource string `json:"resource"`
659+
Type string `json:"type"`
660+
Auth string `json:"keyAuthorization"`
661+
}{
662+
Resource: "challenge",
663+
Type: chal.Type,
664+
Auth: auth,
665+
}
610666
}
611667
res, err := c.post(ctx, nil, chal.URI, req, wantStatus(
612668
http.StatusOK, // according to the spec
@@ -658,21 +714,8 @@ func (c *Client) HTTP01ChallengePath(token string) string {
658714
}
659715

660716
// TLSSNI01ChallengeCert creates a certificate for TLS-SNI-01 challenge response.
661-
// Servers can present the certificate to validate the challenge and prove control
662-
// over a domain name.
663-
//
664-
// The implementation is incomplete in that the returned value is a single certificate,
665-
// computed only for Z0 of the key authorization. ACME CAs are expected to update
666-
// their implementations to use the newer version, TLS-SNI-02.
667-
// For more details on TLS-SNI-01 see https://tools.ietf.org/html/draft-ietf-acme-acme-01#section-7.3.
668-
//
669-
// The token argument is a Challenge.Token value.
670-
// If a WithKey option is provided, its private part signs the returned cert,
671-
// and the public part is used to specify the signee.
672-
// If no WithKey option is provided, a new ECDSA key is generated using P-256 curve.
673717
//
674-
// The returned certificate is valid for the next 24 hours and must be presented only when
675-
// the server name of the TLS ClientHello matches exactly the returned name value.
718+
// Deprecated: This challenge type is unused in both draft-02 and RFC versions of ACME spec.
676719
func (c *Client) TLSSNI01ChallengeCert(token string, opt ...CertOption) (cert tls.Certificate, name string, err error) {
677720
ka, err := keyAuth(c.Key.Public(), token)
678721
if err != nil {
@@ -689,17 +732,8 @@ func (c *Client) TLSSNI01ChallengeCert(token string, opt ...CertOption) (cert tl
689732
}
690733

691734
// TLSSNI02ChallengeCert creates a certificate for TLS-SNI-02 challenge response.
692-
// Servers can present the certificate to validate the challenge and prove control
693-
// over a domain name. For more details on TLS-SNI-02 see
694-
// https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.3.
695-
//
696-
// The token argument is a Challenge.Token value.
697-
// If a WithKey option is provided, its private part signs the returned cert,
698-
// and the public part is used to specify the signee.
699-
// If no WithKey option is provided, a new ECDSA key is generated using P-256 curve.
700735
//
701-
// The returned certificate is valid for the next 24 hours and must be presented only when
702-
// the server name in the TLS ClientHello matches exactly the returned name value.
736+
// Deprecated: This challenge type is unused in both draft-02 and RFC versions of ACME spec.
703737
func (c *Client) TLSSNI02ChallengeCert(token string, opt ...CertOption) (cert tls.Certificate, name string, err error) {
704738
b := sha256.Sum256([]byte(token))
705739
h := hex.EncodeToString(b[:])
@@ -766,7 +800,7 @@ func (c *Client) TLSALPN01ChallengeCert(token, domain string, opt ...CertOption)
766800
return tlsChallengeCert([]string{domain}, newOpt)
767801
}
768802

769-
// doReg sends all types of registration requests.
803+
// doReg sends all types of registration requests the old way (pre-RFC world).
770804
// The type of request is identified by typ argument, which is a "resource"
771805
// in the ACME spec terms.
772806
//

acme/acme_test.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ func TestGetAuthorization(t *testing.T) {
484484
}))
485485
defer ts.Close()
486486

487-
cl := Client{Key: testKeyEC}
487+
cl := Client{Key: testKeyEC, DirectoryURL: ts.URL}
488488
auth, err := cl.GetAuthorization(context.Background(), ts.URL)
489489
if err != nil {
490490
t.Fatal(err)
@@ -614,7 +614,7 @@ func runWaitAuthorization(ctx context.Context, t *testing.T, h http.HandlerFunc)
614614
}
615615
ch := make(chan res, 1)
616616
go func() {
617-
var client Client
617+
var client = Client{DirectoryURL: ts.URL}
618618
a, err := client.WaitAuthorization(ctx, ts.URL)
619619
ch <- res{a, err}
620620
}()
@@ -684,7 +684,7 @@ func TestPollChallenge(t *testing.T) {
684684
}))
685685
defer ts.Close()
686686

687-
cl := Client{Key: testKeyEC}
687+
cl := Client{Key: testKeyEC, DirectoryURL: ts.URL}
688688
chall, err := cl.GetChallenge(context.Background(), ts.URL)
689689
if err != nil {
690690
t.Fatal(err)
@@ -865,7 +865,8 @@ func TestFetchCert(t *testing.T) {
865865
w.Write([]byte{count})
866866
}))
867867
defer ts.Close()
868-
res, err := (&Client{}).FetchCert(context.Background(), ts.URL, true)
868+
cl := &Client{dir: &Directory{}} // skip discovery
869+
res, err := cl.FetchCert(context.Background(), ts.URL, true)
869870
if err != nil {
870871
t.Fatalf("FetchCert: %v", err)
871872
}
@@ -887,7 +888,8 @@ func TestFetchCertRetry(t *testing.T) {
887888
w.Write([]byte{1})
888889
}))
889890
defer ts.Close()
890-
res, err := (&Client{}).FetchCert(context.Background(), ts.URL, false)
891+
cl := &Client{dir: &Directory{}} // skip discovery
892+
res, err := cl.FetchCert(context.Background(), ts.URL, false)
891893
if err != nil {
892894
t.Fatalf("FetchCert: %v", err)
893895
}

0 commit comments

Comments
 (0)