mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-22 21:08:38 +00:00

We've been maintaining temporary dev forks of golang.org/x/crypto/{acme,ssh} in https://github.com/tailscale/golang-x-crypto instead of using this repo's tempfork directory as we do with other packages. The reason we were doing that was because x/crypto/ssh depended on x/crypto/ssh/internal/poly1305 and I hadn't noticed there are forwarding wrappers already available in x/crypto/poly1305. It also depended internal/bcrypt_pbkdf but we don't use that so it's easy to just delete that calling code in our tempfork/ssh. Now that our SSH changes have been upstreamed, we can soon unfork from SSH. That leaves ACME remaining. This change copies our tailscale/golang-x-crypto/acme code to tempfork/acme but adds a test that our vendored copied still matches our tailscale/golang-x-crypto repo, where we can continue to do development work and rebases with upstream. A comment on the new test describes the expected workflow. While we could continue to just import & use tailscale/golang-x-crypto/acme, it seems a bit nicer to not have that entire-fork-of-x-crypto visible at all in our transitive deps and the questions that invites. Showing just a fork of an ACME client is much less scary. It does add a step to the process of hacking on the ACME client code, but we do that approximately never anyway, and the extra step is very incremental compared to the existing tedious steps. Updates #8593 Updates #10238 Change-Id: I8af4378c04c1f82e63d31bf4d16dba9f510f9199 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
477 lines
14 KiB
Go
477 lines
14 KiB
Go
// Copyright 2019 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package acme
|
|
|
|
import (
|
|
"context"
|
|
"crypto"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// DeactivateReg permanently disables an existing account associated with c.Key.
|
|
// A deactivated account can no longer request certificate issuance or access
|
|
// resources related to the account, such as orders or authorizations.
|
|
//
|
|
// It only works with CAs implementing RFC 8555.
|
|
func (c *Client) DeactivateReg(ctx context.Context) error {
|
|
if _, err := c.Discover(ctx); err != nil { // required by c.accountKID
|
|
return err
|
|
}
|
|
url := string(c.accountKID(ctx))
|
|
if url == "" {
|
|
return ErrNoAccount
|
|
}
|
|
req := json.RawMessage(`{"status": "deactivated"}`)
|
|
res, err := c.post(ctx, nil, url, req, wantStatus(http.StatusOK))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
res.Body.Close()
|
|
return nil
|
|
}
|
|
|
|
// registerRFC is equivalent to c.Register but for CAs implementing RFC 8555.
|
|
// It expects c.Discover to have already been called.
|
|
func (c *Client) registerRFC(ctx context.Context, acct *Account, prompt func(tosURL string) bool) (*Account, error) {
|
|
c.cacheMu.Lock() // guard c.kid access
|
|
defer c.cacheMu.Unlock()
|
|
|
|
req := struct {
|
|
TermsAgreed bool `json:"termsOfServiceAgreed,omitempty"`
|
|
Contact []string `json:"contact,omitempty"`
|
|
ExternalAccountBinding *jsonWebSignature `json:"externalAccountBinding,omitempty"`
|
|
}{
|
|
Contact: acct.Contact,
|
|
}
|
|
if c.dir.Terms != "" {
|
|
req.TermsAgreed = prompt(c.dir.Terms)
|
|
}
|
|
|
|
// set 'externalAccountBinding' field if requested
|
|
if acct.ExternalAccountBinding != nil {
|
|
eabJWS, err := c.encodeExternalAccountBinding(acct.ExternalAccountBinding)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("acme: failed to encode external account binding: %v", err)
|
|
}
|
|
req.ExternalAccountBinding = eabJWS
|
|
}
|
|
|
|
res, err := c.post(ctx, c.Key, c.dir.RegURL, req, wantStatus(
|
|
http.StatusOK, // account with this key already registered
|
|
http.StatusCreated, // new account created
|
|
))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
a, err := responseAccount(res)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Cache Account URL even if we return an error to the caller.
|
|
// It is by all means a valid and usable "kid" value for future requests.
|
|
c.KID = KeyID(a.URI)
|
|
if res.StatusCode == http.StatusOK {
|
|
return nil, ErrAccountAlreadyExists
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// encodeExternalAccountBinding will encode an external account binding stanza
|
|
// as described in https://tools.ietf.org/html/rfc8555#section-7.3.4.
|
|
func (c *Client) encodeExternalAccountBinding(eab *ExternalAccountBinding) (*jsonWebSignature, error) {
|
|
jwk, err := jwkEncode(c.Key.Public())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return jwsWithMAC(eab.Key, eab.KID, c.dir.RegURL, []byte(jwk))
|
|
}
|
|
|
|
// updateRegRFC is equivalent to c.UpdateReg but for CAs implementing RFC 8555.
|
|
// It expects c.Discover to have already been called.
|
|
func (c *Client) updateRegRFC(ctx context.Context, a *Account) (*Account, error) {
|
|
url := string(c.accountKID(ctx))
|
|
if url == "" {
|
|
return nil, ErrNoAccount
|
|
}
|
|
req := struct {
|
|
Contact []string `json:"contact,omitempty"`
|
|
}{
|
|
Contact: a.Contact,
|
|
}
|
|
res, err := c.post(ctx, nil, url, req, wantStatus(http.StatusOK))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer res.Body.Close()
|
|
return responseAccount(res)
|
|
}
|
|
|
|
// getRegRFC is equivalent to c.GetReg but for CAs implementing RFC 8555.
|
|
// It expects c.Discover to have already been called.
|
|
func (c *Client) getRegRFC(ctx context.Context) (*Account, error) {
|
|
req := json.RawMessage(`{"onlyReturnExisting": true}`)
|
|
res, err := c.post(ctx, c.Key, c.dir.RegURL, req, wantStatus(http.StatusOK))
|
|
if e, ok := err.(*Error); ok && e.ProblemType == "urn:ietf:params:acme:error:accountDoesNotExist" {
|
|
return nil, ErrNoAccount
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
return responseAccount(res)
|
|
}
|
|
|
|
func responseAccount(res *http.Response) (*Account, error) {
|
|
var v struct {
|
|
Status string
|
|
Contact []string
|
|
Orders string
|
|
}
|
|
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
|
|
return nil, fmt.Errorf("acme: invalid account response: %v", err)
|
|
}
|
|
return &Account{
|
|
URI: res.Header.Get("Location"),
|
|
Status: v.Status,
|
|
Contact: v.Contact,
|
|
OrdersURL: v.Orders,
|
|
}, nil
|
|
}
|
|
|
|
// accountKeyRollover attempts to perform account key rollover.
|
|
// On success it will change client.Key to the new key.
|
|
func (c *Client) accountKeyRollover(ctx context.Context, newKey crypto.Signer) error {
|
|
dir, err := c.Discover(ctx) // Also required by c.accountKID
|
|
if err != nil {
|
|
return err
|
|
}
|
|
kid := c.accountKID(ctx)
|
|
if kid == noKeyID {
|
|
return ErrNoAccount
|
|
}
|
|
oldKey, err := jwkEncode(c.Key.Public())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
payload := struct {
|
|
Account string `json:"account"`
|
|
OldKey json.RawMessage `json:"oldKey"`
|
|
}{
|
|
Account: string(kid),
|
|
OldKey: json.RawMessage(oldKey),
|
|
}
|
|
inner, err := jwsEncodeJSON(payload, newKey, noKeyID, noNonce, dir.KeyChangeURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
res, err := c.post(ctx, nil, dir.KeyChangeURL, base64.RawURLEncoding.EncodeToString(inner), wantStatus(http.StatusOK))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
c.Key = newKey
|
|
return nil
|
|
}
|
|
|
|
// AuthorizeOrder initiates the order-based application for certificate issuance,
|
|
// as opposed to pre-authorization in Authorize.
|
|
// It is only supported by CAs implementing RFC 8555.
|
|
//
|
|
// The caller then needs to fetch each authorization with GetAuthorization,
|
|
// identify those with StatusPending status and fulfill a challenge using Accept.
|
|
// Once all authorizations are satisfied, the caller will typically want to poll
|
|
// order status using WaitOrder until it's in StatusReady state.
|
|
// To finalize the order and obtain a certificate, the caller submits a CSR with CreateOrderCert.
|
|
func (c *Client) AuthorizeOrder(ctx context.Context, id []AuthzID, opt ...OrderOption) (*Order, error) {
|
|
dir, err := c.Discover(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req := struct {
|
|
Identifiers []wireAuthzID `json:"identifiers"`
|
|
NotBefore string `json:"notBefore,omitempty"`
|
|
NotAfter string `json:"notAfter,omitempty"`
|
|
}{}
|
|
for _, v := range id {
|
|
req.Identifiers = append(req.Identifiers, wireAuthzID{
|
|
Type: v.Type,
|
|
Value: v.Value,
|
|
})
|
|
}
|
|
for _, o := range opt {
|
|
switch o := o.(type) {
|
|
case orderNotBeforeOpt:
|
|
req.NotBefore = time.Time(o).Format(time.RFC3339)
|
|
case orderNotAfterOpt:
|
|
req.NotAfter = time.Time(o).Format(time.RFC3339)
|
|
default:
|
|
// Package's fault if we let this happen.
|
|
panic(fmt.Sprintf("unsupported order option type %T", o))
|
|
}
|
|
}
|
|
|
|
res, err := c.post(ctx, nil, dir.OrderURL, req, wantStatus(http.StatusCreated))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer res.Body.Close()
|
|
return responseOrder(res)
|
|
}
|
|
|
|
// GetOrder retrives an order identified by the given URL.
|
|
// For orders created with AuthorizeOrder, the url value is Order.URI.
|
|
//
|
|
// If a caller needs to poll an order until its status is final,
|
|
// see the WaitOrder method.
|
|
func (c *Client) GetOrder(ctx context.Context, url string) (*Order, error) {
|
|
if _, err := c.Discover(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res, err := c.postAsGet(ctx, url, wantStatus(http.StatusOK))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer res.Body.Close()
|
|
return responseOrder(res)
|
|
}
|
|
|
|
// WaitOrder polls an order from the given URL until it is in one of the final states,
|
|
// StatusReady, StatusValid or StatusInvalid, the CA responded with a non-retryable error
|
|
// or the context is done.
|
|
//
|
|
// It returns a non-nil Order only if its Status is StatusReady or StatusValid.
|
|
// In all other cases WaitOrder returns an error.
|
|
// If the Status is StatusInvalid, the returned error is of type *OrderError.
|
|
func (c *Client) WaitOrder(ctx context.Context, url string) (*Order, error) {
|
|
if _, err := c.Discover(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
for {
|
|
res, err := c.postAsGet(ctx, url, wantStatus(http.StatusOK))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
o, err := responseOrder(res)
|
|
res.Body.Close()
|
|
switch {
|
|
case err != nil:
|
|
// Skip and retry.
|
|
case o.Status == StatusInvalid:
|
|
return nil, &OrderError{OrderURL: o.URI, Status: o.Status}
|
|
case o.Status == StatusReady || o.Status == StatusValid:
|
|
return o, nil
|
|
}
|
|
|
|
d := retryAfter(res.Header.Get("Retry-After"))
|
|
if d == 0 {
|
|
// Default retry-after.
|
|
// Same reasoning as in WaitAuthorization.
|
|
d = time.Second
|
|
}
|
|
t := time.NewTimer(d)
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Stop()
|
|
return nil, ctx.Err()
|
|
case <-t.C:
|
|
// Retry.
|
|
}
|
|
}
|
|
}
|
|
|
|
func responseOrder(res *http.Response) (*Order, error) {
|
|
var v struct {
|
|
Status string
|
|
Expires time.Time
|
|
Identifiers []wireAuthzID
|
|
NotBefore time.Time
|
|
NotAfter time.Time
|
|
Error *wireError
|
|
Authorizations []string
|
|
Finalize string
|
|
Certificate string
|
|
}
|
|
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
|
|
return nil, fmt.Errorf("acme: error reading order: %v", err)
|
|
}
|
|
o := &Order{
|
|
URI: res.Header.Get("Location"),
|
|
Status: v.Status,
|
|
Expires: v.Expires,
|
|
NotBefore: v.NotBefore,
|
|
NotAfter: v.NotAfter,
|
|
AuthzURLs: v.Authorizations,
|
|
FinalizeURL: v.Finalize,
|
|
CertURL: v.Certificate,
|
|
}
|
|
for _, id := range v.Identifiers {
|
|
o.Identifiers = append(o.Identifiers, AuthzID{Type: id.Type, Value: id.Value})
|
|
}
|
|
if v.Error != nil {
|
|
o.Error = v.Error.error(nil /* headers */)
|
|
}
|
|
return o, nil
|
|
}
|
|
|
|
// CreateOrderCert submits the CSR (Certificate Signing Request) to a CA at the specified URL.
|
|
// The URL is the FinalizeURL field of an Order created with AuthorizeOrder.
|
|
//
|
|
// If the bundle argument is true, the returned value also contain the CA (issuer)
|
|
// certificate chain. Otherwise, only a leaf certificate is returned.
|
|
// The returned URL can be used to re-fetch the certificate using FetchCert.
|
|
//
|
|
// This method is only supported by CAs implementing RFC 8555. See CreateCert for pre-RFC CAs.
|
|
//
|
|
// CreateOrderCert returns an error if the CA's response is unreasonably large.
|
|
// Callers are encouraged to parse the returned value to ensure the certificate is valid and has the expected features.
|
|
func (c *Client) CreateOrderCert(ctx context.Context, url string, csr []byte, bundle bool) (der [][]byte, certURL string, err error) {
|
|
if _, err := c.Discover(ctx); err != nil { // required by c.accountKID
|
|
return nil, "", err
|
|
}
|
|
|
|
// RFC describes this as "finalize order" request.
|
|
req := struct {
|
|
CSR string `json:"csr"`
|
|
}{
|
|
CSR: base64.RawURLEncoding.EncodeToString(csr),
|
|
}
|
|
res, err := c.post(ctx, nil, url, req, wantStatus(http.StatusOK))
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
defer res.Body.Close()
|
|
o, err := responseOrder(res)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
// Wait for CA to issue the cert if they haven't.
|
|
if o.Status != StatusValid {
|
|
o, err = c.WaitOrder(ctx, o.URI)
|
|
}
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
// The only acceptable status post finalize and WaitOrder is "valid".
|
|
if o.Status != StatusValid {
|
|
return nil, "", &OrderError{OrderURL: o.URI, Status: o.Status}
|
|
}
|
|
crt, err := c.fetchCertRFC(ctx, o.CertURL, bundle)
|
|
return crt, o.CertURL, err
|
|
}
|
|
|
|
// fetchCertRFC downloads issued certificate from the given URL.
|
|
// It expects the CA to respond with PEM-encoded certificate chain.
|
|
//
|
|
// The URL argument is the CertURL field of Order.
|
|
func (c *Client) fetchCertRFC(ctx context.Context, url string, bundle bool) ([][]byte, error) {
|
|
res, err := c.postAsGet(ctx, url, wantStatus(http.StatusOK))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
// Get all the bytes up to a sane maximum.
|
|
// Account very roughly for base64 overhead.
|
|
const max = maxCertChainSize + maxCertChainSize/33
|
|
b, err := io.ReadAll(io.LimitReader(res.Body, max+1))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("acme: fetch cert response stream: %v", err)
|
|
}
|
|
if len(b) > max {
|
|
return nil, errors.New("acme: certificate chain is too big")
|
|
}
|
|
|
|
// Decode PEM chain.
|
|
var chain [][]byte
|
|
for {
|
|
var p *pem.Block
|
|
p, b = pem.Decode(b)
|
|
if p == nil {
|
|
break
|
|
}
|
|
if p.Type != "CERTIFICATE" {
|
|
return nil, fmt.Errorf("acme: invalid PEM cert type %q", p.Type)
|
|
}
|
|
|
|
chain = append(chain, p.Bytes)
|
|
if !bundle {
|
|
return chain, nil
|
|
}
|
|
if len(chain) > maxChainLen {
|
|
return nil, errors.New("acme: certificate chain is too long")
|
|
}
|
|
}
|
|
if len(chain) == 0 {
|
|
return nil, errors.New("acme: certificate chain is empty")
|
|
}
|
|
return chain, nil
|
|
}
|
|
|
|
// sends a cert revocation request in either JWK form when key is non-nil or KID form otherwise.
|
|
func (c *Client) revokeCertRFC(ctx context.Context, key crypto.Signer, cert []byte, reason CRLReasonCode) error {
|
|
req := &struct {
|
|
Cert string `json:"certificate"`
|
|
Reason int `json:"reason"`
|
|
}{
|
|
Cert: base64.RawURLEncoding.EncodeToString(cert),
|
|
Reason: int(reason),
|
|
}
|
|
res, err := c.post(ctx, key, c.dir.RevokeURL, req, wantStatus(http.StatusOK))
|
|
if err != nil {
|
|
if isAlreadyRevoked(err) {
|
|
// Assume it is not an error to revoke an already revoked cert.
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
return nil
|
|
}
|
|
|
|
func isAlreadyRevoked(err error) bool {
|
|
e, ok := err.(*Error)
|
|
return ok && e.ProblemType == "urn:ietf:params:acme:error:alreadyRevoked"
|
|
}
|
|
|
|
// ListCertAlternates retrieves any alternate certificate chain URLs for the
|
|
// given certificate chain URL. These alternate URLs can be passed to FetchCert
|
|
// in order to retrieve the alternate certificate chains.
|
|
//
|
|
// If there are no alternate issuer certificate chains, a nil slice will be
|
|
// returned.
|
|
func (c *Client) ListCertAlternates(ctx context.Context, url string) ([]string, error) {
|
|
if _, err := c.Discover(ctx); err != nil { // required by c.accountKID
|
|
return nil, err
|
|
}
|
|
|
|
res, err := c.postAsGet(ctx, url, wantStatus(http.StatusOK))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
// We don't need the body but we need to discard it so we don't end up
|
|
// preventing keep-alive
|
|
if _, err := io.Copy(io.Discard, res.Body); err != nil {
|
|
return nil, fmt.Errorf("acme: cert alternates response stream: %v", err)
|
|
}
|
|
alts := linkHeader(res.Header, "alternate")
|
|
return alts, nil
|
|
}
|