diff --git a/cmd/radiod/radiod.go b/cmd/radiod/radiod.go index f93484a0e2b146a8068656232ae90cd5431d8e9f..f7fb319d19c8c623baee83d84e61fa89aa3dbfef 100644 --- a/cmd/radiod/radiod.go +++ b/cmd/radiod/radiod.go @@ -23,10 +23,11 @@ import ( ) var ( - name = flag.String("name", shortHostname(), "Name for this node") + name = flag.String("name", hostname(), "Name for this node (FQDN)") publicIPs = util.IPListFlag("public-ip", "Public IP for this machine (may be specified more than once). If unset, the program will try to resolve the local hostname, or it will fall back to inspecting network devices.") peerIP = util.IPFlag("peer-ip", "Internal IP for this machine (within the cluster), if different from --ip") httpPort = flag.Int("http-port", 80, "HTTP port") + httpsPort = flag.Int("https-port", 443, "HTTPS port (if 0, disable HTTPS)") dnsPort = flag.Int("dns-port", 53, "DNS port") gossipPort = flag.Int("gossip-port", 2323, "Gossip GRPC port") metricsPort = flag.Int("monitoring-port", 2424, "HTTP port for monitoring") @@ -37,6 +38,8 @@ var ( domain = flag.String("domain", "", "public DNS domain") lbSpec = flag.String("lb-policy", "listeners_available,listeners_score,weighted", "Load balancing rules specification (see godoc documentation for details)") nameservers = flag.String("nameservers", "", "Comma-separated list of name servers (not IPs) for the zone specified in --domain") + acmeEmail = flag.String("acme-email", "", "Email address for Letsencrypt account") + acmeNames = flag.String("acme-cert-names", "", "Names to put on the SSL certificate (comma-separated list)") icecastConfigPath = flag.String("icecast-config", "/var/lib/autoradio/icecast.xml", "Icecast configuration file") icecastAdminPwPath = flag.String("icecast-pwfile", "/var/lib/autoradio/.admin_pw", "Path to file with Icecast admin password") @@ -44,11 +47,8 @@ var ( sessionTTL = 5 ) -func shortHostname() string { +func hostname() string { hostname, _ := os.Hostname() - if r := strings.Index(hostname, "."); r >= 0 { - return hostname[:r] - } return hostname } @@ -81,6 +81,9 @@ func main() { if *name == "" { log.Fatal("--name must be set") } + if !strings.Contains(*name, ".") && *httpsPort > 0 { + log.Fatal("--name must be a FQDN when SSL is enabled") + } if *domain == "" { log.Fatal("--domain must be set") } @@ -163,7 +166,24 @@ func main() { // Start all the network services. DNS will listen on all // non-loopback addresses on all interfaces, to let people run // a loopback cache if necessary. - srv := node.NewServer(ctx, n, *domain, strings.Split(*nameservers, ","), nonLocalAddrs(), *peerIP, *httpPort, *dnsPort, *gossipPort, autoradio.IcecastPort, *metricsPort) + config := node.Config{ + Domain: *domain, + Nameservers: strings.Split(*nameservers, ","), + DNSAddrs: nonLocalAddrs(), + PeerAddr: *peerIP, + HTTPPort: *httpPort, + HTTPSPort: *httpsPort, + DNSPort: *dnsPort, + GossipPort: *gossipPort, + IcecastPort: autoradio.IcecastPort, + MetricsPort: *metricsPort, + ACMEEmail: *acmeEmail, + CertNames: strings.Split(*acmeNames, ","), + } + srv, err := node.NewServer(ctx, etcd, n, &config) + if err != nil { + log.Fatalf("could not initialize server: %v", err) + } // Wait until the Node and the Server terminate. A failure in // either the network services or the Node itself should cause diff --git a/node/acme/acme.go b/node/acme/acme.go new file mode 100644 index 0000000000000000000000000000000000000000..0533fa645d9be9aa60ee288224c34e5264e33444 --- /dev/null +++ b/node/acme/acme.go @@ -0,0 +1,297 @@ +package acme + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "errors" + "fmt" + "io" + "log" + "net/http" + "sync" + "time" + + "golang.org/x/crypto/acme" +) + +const http01Challenge = "http-01" + +var acmeRPCTimeout = 30 * time.Second + +type keyBackend interface { + Put(context.Context, []byte) error + Get(context.Context) ([]byte, error) +} + +type tokenBackend interface { + Put(context.Context, string, string) error + Get(context.Context, string) (string, error) + Delete(context.Context, string) +} + +// The ACME object implements the ACME protocol machinery, and can be +// used to create and renew certificates. It also serves as an HTTP +// handler to satisfy http-01 validation requests. +type ACME struct { + email string + directoryURL string + + mx sync.Mutex + client *acme.Client + keystore keyBackend + tokens tokenBackend +} + +func NewACME(email, directoryURL string, keystore keyBackend, tokens tokenBackend) *ACME { + if directoryURL == "" { + directoryURL = acme.LetsEncryptURL + } + return &ACME{ + email: email, + directoryURL: directoryURL, + keystore: keystore, + tokens: tokens, + } +} + +// Load the account key from the state file and return it, possibly +// initializing it if it does not exist. +func (a *ACME) accountKey(ctx context.Context) (crypto.Signer, error) { + var keyData []byte + var err error + + // Run a get/put loop a few times to resolve eventual + // conflicts when servers have synchronized startups. + for i := 0; i < 10; i++ { + cctx, cancel := context.WithTimeout(ctx, acmeRPCTimeout) + keyData, err = a.keystore.Get(cctx) + cancel() + + if err != nil { + log.Printf("acme: generating new account key") + eckey, kerr := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if kerr != nil { + return nil, kerr + } + keyData, kerr = x509.MarshalECPrivateKey(eckey) + if kerr != nil { + return nil, kerr + } + + cctx, cancel = context.WithTimeout(ctx, acmeRPCTimeout) + err = a.keystore.Put(cctx, keyData) + cancel() + } + + if err == nil { + break + } + } + if err != nil { + return nil, err + } + + return parsePrivateKey(keyData) +} + +// Return a new acme.Client, possibly initializing the account key if +// it does not exist yet. The Client is created on the first call and +// cached thereafter. +func (a *ACME) acmeClient(ctx context.Context) (*acme.Client, error) { + a.mx.Lock() + defer a.mx.Unlock() + + if a.client != nil { + return a.client, nil + } + + client := &acme.Client{ + DirectoryURL: a.directoryURL, + } + key, err := a.accountKey(ctx) + if err != nil { + return nil, err + } + client.Key = key + ac := &acme.Account{ + Contact: []string{"mailto:" + a.email}, + } + + // Register the account (accept TOS) if necessary. If the + // account is already registered we get a StatusConflict, + // which we can ignore. + _, err = client.Register(ctx, ac, func(_ string) bool { return true }) + if ae, ok := err.(*acme.Error); err == nil || err == acme.ErrAccountAlreadyExists || (ok && ae.StatusCode == http.StatusConflict) { + a.client = client + err = nil + } + return a.client, err +} + +// fulfill the validation request by storing the token. +func (a *ACME) fulfill(ctx context.Context, client *acme.Client, _ string, chal *acme.Challenge) (func(), error) { + resp, err := client.HTTP01ChallengeResponse(chal.Token) + if err != nil { + return nil, err + } + path := client.HTTP01ChallengePath(chal.Token) + + if err := a.tokens.Put(ctx, path, resp); err != nil { + return nil, err + } + + return func() { + go func() { + a.tokens.Delete(ctx, path) + }() + }, nil +} + +// getCertificate returns a certificate chain (and the parsed leaf +// cert), given a private key and a list of domains. The first domain +// will be the subject CN, the others will be added as subjectAltNames. +func (a *ACME) getCertificate(ctx context.Context, key crypto.Signer, names []string) (der [][]byte, leaf *x509.Certificate, err error) { + client, err := a.acmeClient(ctx) + if err != nil { + return nil, nil, err + } + + o, err := a.verifyAll(ctx, client, names) + if err != nil { + return nil, nil, err + } + + csr, err := certRequest(key, names) + if err != nil { + return nil, nil, err + } + der, _, err = client.CreateOrderCert(ctx, o.FinalizeURL, csr, true) + if err != nil { + return nil, nil, err + } + leaf, err = validCert(names, der, key) + if err != nil { + return nil, nil, err + } + return der, leaf, nil +} + +func (a *ACME) verifyAll(ctx context.Context, client *acme.Client, names []string) (*acme.Order, error) { + // Make an authorization request to the ACME server, and + // verify that it returns a valid response with challenges. + o, err := client.AuthorizeOrder(ctx, acme.DomainIDs(names...)) + if err != nil { + return nil, fmt.Errorf("AuthorizeOrder failed: %v", err) + } + + switch o.Status { + case acme.StatusReady: + return o, nil // already authorized + case acme.StatusPending: + default: + return nil, fmt.Errorf("invalid new order status %q", o.Status) + } + + for _, zurl := range o.AuthzURLs { + z, err := client.GetAuthorization(ctx, zurl) + if err != nil { + return nil, fmt.Errorf("GetAuthorization(%s) failed: %v", zurl, err) + } + if z.Status != acme.StatusPending { + continue + } + // Pick a challenge that matches our preferences and the + // available validators. The validator fulfills the challenge, + // and returns a cleanup function that we're going to call + // before we return. All steps are sequential and idempotent. + chal := pickChallenge(z.Challenges) + if chal == nil { + return nil, fmt.Errorf("unable to authorize %q", names) + } + + // We only support http-01 challenges. + if chal.Type != http01Challenge { + return nil, fmt.Errorf("challenge type '%s' is not available", chal.Type) + } + + for _, domain := range names { + cleanup, err := a.fulfill(ctx, client, domain, chal) + if err != nil { + return nil, fmt.Errorf("fulfillment failed: %v", err) + } + defer cleanup() + } + + if _, err := client.Accept(ctx, chal); err != nil { + return nil, fmt.Errorf("challenge accept failed: %v", err) + } + if _, err := client.WaitAuthorization(ctx, z.URI); err != nil { + return nil, fmt.Errorf("WaitAuthorization(%s) failed: %v", z.URI, err) + } + } + + // Authorizations are satisfied, wait for the CA + // to update the order status. + if _, err = client.WaitOrder(ctx, o.URI); err != nil { + return nil, err + } + return o, nil +} + +func (a *ACME) ServeHTTP(w http.ResponseWriter, req *http.Request) { + token, err := a.tokens.Get(req.Context(), req.URL.Path) + if err != nil { + http.NotFound(w, req) + return + } + io.WriteString(w, token) // nolint: errcheck +} + +// Pick a challenge with the right type from the Challenge response +// returned by the ACME server. We only support http-01, so we just +// search for that one. +func pickChallenge(chal []*acme.Challenge) *acme.Challenge { + for _, ch := range chal { + if ch.Type == http01Challenge { + return ch + } + } + return nil +} + +func certRequest(key crypto.Signer, domains []string) ([]byte, error) { + req := &x509.CertificateRequest{ + Subject: pkix.Name{CommonName: domains[0]}, + } + if len(domains) > 1 { + req.DNSNames = domains[1:] + } + return x509.CreateCertificateRequest(rand.Reader, req, key) +} + +func parsePrivateKey(der []byte) (crypto.Signer, error) { + if key, err := x509.ParsePKCS1PrivateKey(der); err == nil { + return key, nil + } + if key, err := x509.ParsePKCS8PrivateKey(der); err == nil { + switch key := key.(type) { + case *rsa.PrivateKey: + return key, nil + case *ecdsa.PrivateKey: + return key, nil + default: + return nil, errors.New("unknown private key type in PKCS#8 wrapping") + } + } + if key, err := x509.ParseECPrivateKey(der); err == nil { + return key, nil + } + + return nil, errors.New("failed to parse private key") +} diff --git a/node/acme/manager.go b/node/acme/manager.go new file mode 100644 index 0000000000000000000000000000000000000000..c04cdee9ee703e1eb354d391de03fab899474a87 --- /dev/null +++ b/node/acme/manager.go @@ -0,0 +1,301 @@ +package acme + +import ( + "context" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "fmt" + "log" + "math/big" + mrand "math/rand" + "sync" + "time" + + "go.etcd.io/etcd/clientv3" +) + +const ( + certPath = "acme/cert" + keyPath = "acme/key" +) + +var ( + checkIntervalSeconds = 9600 + renewalTimeout = 1800 * time.Second + renewalDays = 7 +) + +type Cert struct { + Names []string + Priv []byte + Pub [][]byte +} + +func (c *Cert) TLSCertificate() (*tls.Certificate, error) { + pkey, err := parsePrivateKey(c.Priv) + if err != nil { + return nil, err + } + return &tls.Certificate{ + Certificate: c.Pub, + PrivateKey: pkey, + }, nil +} + +func (c *Cert) NotAfter() time.Time { + cert, err := x509.ParseCertificate(c.Pub[0]) + if err != nil { + return time.Time{} + } + return cert.NotAfter +} + +// A Manager is responsible for a single SSL certificate (which may +// have multiple names). It will store the certificate itself, and the +// ACME state, on etcd, so that it is replicated to all HTTPS servers. +// +// Renewal is handled via (internal) cron jobs, with random schedules +// to avoid having to implement leader-election for such a simple task. +// +type Manager struct { + *ACME + + names []string + cli *clientv3.Client + + // Keep the Certificate and the parsed version in sync. + mx sync.RWMutex + cert *Cert + tlsCert *tls.Certificate + renewalDeadline time.Time +} + +func NewManager(ctx context.Context, cli *clientv3.Client, email, directoryURL string, certNames []string) (*Manager, error) { + // Instantiate the ACME protocol handler and have it store its + // validation tokens on etcd. + acmeMgr := NewACME(email, directoryURL, newEtcdKeyStore(cli, keyPath), newEtcdTokenStore(cli)) + + // Try to fetch the existing certificate from etcd, or + // generate a self-signed one. + cert, rev, err := fetchCert(ctx, cli, certPath) + if err != nil { + log.Printf("error fetching certificate: %v", err) + } + if cert == nil { + cert, err = makeSelfSignedCert(certNames) + if err != nil { + return nil, fmt.Errorf("failed to create self-signed certificate: %v", err) + } + } + tlsCert, err := cert.TLSCertificate() + if err != nil { + return nil, err + } + + m := &Manager{ + ACME: acmeMgr, + names: certNames, + cli: cli, + cert: cert, + tlsCert: tlsCert, + } + + // Update m.cert using a watcher. + go m.watch(ctx, certPath, rev) + + // Start the background renewer job. + go m.renewLoop(ctx) + + return m, nil +} + +func (m *Manager) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) { + m.mx.RLock() + defer m.mx.RUnlock() + return m.tlsCert, nil +} + +func (m *Manager) setCert(cert *Cert) error { + tlsCert, err := cert.TLSCertificate() + if err != nil { + return err + } + + m.mx.Lock() + m.cert = cert + m.tlsCert = tlsCert + m.renewalDeadline = cert.NotAfter().AddDate(0, 0, -renewalDays) + m.mx.Unlock() + + return nil +} + +func (m *Manager) shouldRenew() bool { + m.mx.RLock() + defer m.mx.RUnlock() + return time.Now().After(m.renewalDeadline) || !listsEqual(m.cert.Names, m.names) +} + +func (m *Manager) renewLoop(ctx context.Context) { + // Initial delay to stagger concurrent initialization. + time.Sleep(time.Duration(mrand.Intn(30)) * time.Second) + + for { + if m.shouldRenew() { + log.Printf("attempting to renew SSL certificate...") + if err := m.renew(ctx); err != nil { + log.Printf("renewal failed: %v", err) + } + } + + // Sleep a semi-random amount of time. + t := time.After(time.Duration(checkIntervalSeconds/2+mrand.Intn(checkIntervalSeconds)) * time.Second) + select { + case <-ctx.Done(): + return + case <-t: + } + } +} + +func (m *Manager) renew(ctx context.Context) error { + ctx, cancel := context.WithTimeout(ctx, renewalTimeout) + defer cancel() + + m.mx.RLock() + cert := m.cert + m.mx.RUnlock() + + var key crypto.Signer + var err error + + if len(cert.Priv) == 0 { + key, err = rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return err + } + cert.Priv, err = x509.MarshalPKCS8PrivateKey(key) + } else { + key, err = parsePrivateKey(cert.Priv) + } + if err != nil { + return err + } + + cert.Pub, _, err = m.ACME.getCertificate(ctx, key, m.names) + if err != nil { + return err + } + + return storeCert(ctx, m.cli, certPath, cert) +} + +func (m *Manager) watchOnce(ctx context.Context, path string, rev int64) error { + for resp := range m.cli.Watch(ctx, path, clientv3.WithRev(rev)) { + for _, ev := range resp.Events { + if ev.Type != clientv3.EventTypePut { + continue + } + var cert Cert + if err := json.Unmarshal(ev.Kv.Value, &cert); err != nil { + log.Printf("error unmarshaling cert: %v", err) + continue + } + + if err := m.setCert(&cert); err != nil { + log.Printf("error reading saved cert: %v", err) + } + } + } + + // Watcher is gone, recover. + return nil +} + +func (m *Manager) watch(ctx context.Context, path string, rev int64) { + for { + err := m.watchOnce(ctx, path, rev) + if err == context.Canceled { + return + } else if err != nil { + log.Printf("watcher error: %s: %v", path, err) + } + + time.Sleep(watcherErrDelay) + + cert, newRev, err := fetchCert(ctx, m.cli, path) + if err != nil { + log.Printf("fetch error: %s: %v", path, err) + } else if cert != nil { + if err := m.setCert(cert); err != nil { + log.Printf("error reading saved cert: %v", err) + } + rev = newRev + } + } +} + +func makeSelfSignedCert(names []string) (*Cert, error) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + + keyBytes, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return nil, fmt.Errorf("marshaling error: %v", err) + } + + notBefore := time.Now().AddDate(0, 0, -1) + notAfter := notBefore.AddDate(1, 0, 0) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, fmt.Errorf("failed to generate serial number: %v", err) + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Autoradio"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + DNSNames: names, + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) + if err != nil { + return nil, err + } + + return &Cert{ + Names: names, + Pub: [][]byte{derBytes}, + Priv: keyBytes, + }, nil +} + +func listsEqual(a, b []string) bool { + tmp := make(map[string]struct{}) + for _, aa := range a { + tmp[aa] = struct{}{} + } + for _, bb := range b { + if _, ok := tmp[bb]; !ok { + return false + } + delete(tmp, bb) + } + return len(tmp) == 0 +} diff --git a/node/acme/manager_test.go b/node/acme/manager_test.go new file mode 100644 index 0000000000000000000000000000000000000000..266b4530e61d055134ae8c379f025751c28d7270 --- /dev/null +++ b/node/acme/manager_test.go @@ -0,0 +1,13 @@ +package acme + +import "testing" + +func TestMakeSelfSignedCert(t *testing.T) { + cert, err := makeSelfSignedCert([]string{"name1", "name2"}) + if err != nil { + t.Fatal(err) + } + if len(cert.Pub) == 0 { + t.Fatal("empty cert") + } +} diff --git a/node/acme/storage.go b/node/acme/storage.go new file mode 100644 index 0000000000000000000000000000000000000000..fb13345cd52815e1282ce70af290f6bf2340a32a --- /dev/null +++ b/node/acme/storage.go @@ -0,0 +1,110 @@ +package acme + +import ( + "context" + "encoding/json" + "errors" + "time" + + "go.etcd.io/etcd/clientv3" +) + +var ( + watcherErrDelay = 1 * time.Second + certOpTimeout = 10 * time.Second +) + +func fetchCert(ctx context.Context, cli *clientv3.Client, path string) (*Cert, int64, error) { + // Create a context with a timeout. + cctx, cancel := context.WithTimeout(ctx, certOpTimeout) + defer cancel() + + // Run a Get first. + resp, err := cli.Get(cctx, path) + if err != nil { + return nil, 0, err + } + rev := resp.Header.Revision + if len(resp.Kvs) == 0 { + return nil, rev, nil + } + var cert Cert + if err := json.Unmarshal(resp.Kvs[0].Value, &cert); err != nil { + return nil, rev, err + } + return &cert, rev, nil +} + +func storeCert(ctx context.Context, cli *clientv3.Client, path string, cert *Cert) error { + // Create a context with a timeout. + cctx, cancel := context.WithTimeout(ctx, certOpTimeout) + defer cancel() + + data, err := json.Marshal(cert) + if err != nil { + return err + } + + _, err = cli.Put(cctx, path, string(data)) + return err +} + +const tokenPrefix = "acme/tokens" + +type etcdTokenStore struct { + cli *clientv3.Client +} + +func newEtcdTokenStore(cli *clientv3.Client) *etcdTokenStore { + return &etcdTokenStore{cli: cli} +} + +func (t *etcdTokenStore) Put(ctx context.Context, key, value string) error { + _, err := t.cli.Put(ctx, tokenPrefix+key, value) + return err +} + +func (t *etcdTokenStore) Get(ctx context.Context, key string) (string, error) { + resp, err := t.cli.Get(ctx, tokenPrefix+key) + if err != nil { + return "", err + } + if len(resp.Kvs) == 0 { + return "", errors.New("not found") + } + return string(resp.Kvs[0].Value), nil +} + +func (t *etcdTokenStore) Delete(ctx context.Context, key string) { + t.cli.Delete(ctx, tokenPrefix+key) // nolint: errcheck +} + +type etcdKeyStore struct { + cli *clientv3.Client + path string +} + +func newEtcdKeyStore(cli *clientv3.Client, path string) *etcdKeyStore { + return &etcdKeyStore{cli: cli, path: path} +} + +func (k *etcdKeyStore) Put(ctx context.Context, value []byte) error { + kvc := clientv3.NewKV(k.cli) + + _, err := kvc.Txn(ctx). + If(clientv3.CreateRevision(k.path)). + Then(clientv3.OpPut(k.path, string(value))). + Commit() + return err +} + +func (k *etcdKeyStore) Get(ctx context.Context) ([]byte, error) { + resp, err := k.cli.Get(ctx, k.path) + if err != nil { + return nil, err + } + if len(resp.Kvs) == 0 { + return nil, errors.New("not found") + } + return resp.Kvs[0].Value, nil +} diff --git a/node/acme/valid_cert.go b/node/acme/valid_cert.go new file mode 100644 index 0000000000000000000000000000000000000000..d734738233cb70d47680961d3a634f925959b4af --- /dev/null +++ b/node/acme/valid_cert.go @@ -0,0 +1,106 @@ +package acme + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "errors" + "fmt" + "time" +) + +// Verify that a certificate is valid according both to the current +// time and to the specified parameters. +func validCert(domains []string, der [][]byte, key crypto.Signer) (*x509.Certificate, error) { + leaf, err := leafCertFromDER(der) + if err != nil { + return nil, err + } + + // Verify the leaf is not expired. + if err := isCertExpired(leaf); err != nil { + return nil, err + } + + // Verify that it matches the given domains. + if err := certMatchesDomains(leaf, domains); err != nil { + return nil, err + } + + // Verify that it matches the private key. + if err := certMatchesPrivateKey(leaf, key); err != nil { + return nil, err + } + + return leaf, nil +} + +func leafCertFromDER(der [][]byte) (*x509.Certificate, error) { + x509Cert, err := x509.ParseCertificates(concatDER(der)) + if err != nil { + return nil, err + } + if len(x509Cert) == 0 { + return nil, errors.New("no public key found") + } + return x509Cert[0], nil +} + +func isCertExpired(cert *x509.Certificate) error { + now := time.Now() + if now.Before(cert.NotBefore) { + return errors.New("certificate isn't valid yet") + } + if now.After(cert.NotAfter) { + return errors.New("certificate expired") + } + return nil +} + +func certMatchesDomains(cert *x509.Certificate, domains []string) error { + for _, domain := range domains { + if err := cert.VerifyHostname(domain); err != nil { + return fmt.Errorf("certificate does not match domain %q", domain) + } + } + return nil +} + +func certMatchesPrivateKey(cert *x509.Certificate, key crypto.Signer) error { + switch pub := cert.PublicKey.(type) { + case *rsa.PublicKey: + prv, ok := key.(*rsa.PrivateKey) + if !ok { + return errors.New("private key type does not match public key type") + } + if pub.N.Cmp(prv.N) != 0 { + return errors.New("private key does not match public key") + } + case *ecdsa.PublicKey: + prv, ok := key.(*ecdsa.PrivateKey) + if !ok { + return errors.New("private key type does not match public key type") + } + if pub.X.Cmp(prv.X) != 0 || pub.Y.Cmp(prv.Y) != 0 { + return errors.New("private key does not match public key") + } + default: + return errors.New("unsupported public key algorithm") + } + return nil +} + +func concatDER(der [][]byte) []byte { + // Append DERs to a single []byte buffer and parse the results. + var n int + for _, b := range der { + n += len(b) + } + out := make([]byte, n) + n = 0 + for _, b := range der { + n += copy(out[n:], b) + } + return out +} diff --git a/node/debug.go b/node/debug.go index 2382650372c07b88d15b281f0d3fd6e8901b37d0..0b9b4104b346b6171d53e9b1257500aa59a5ffc5 100644 --- a/node/debug.go +++ b/node/debug.go @@ -2,7 +2,6 @@ package node import ( "bytes" - "html/template" "log" "net/http" "sort" @@ -93,7 +92,7 @@ func mountsToStatus(mounts []*pb.Mount, nodes []*nodeInfo, icecastMounts map[str return msl } -func serveStatusPage(n *Node, w http.ResponseWriter, r *http.Request, tpl *template.Template, domain string) { +func serveStatusPage(n *Node, w http.ResponseWriter, r *http.Request, domain string) { // Convert the list of nodes to just the status. While we're // at it, build a map of mount path -> exemplary IcecastMount, // which we use to show the current song artist / title. diff --git a/node/http.go b/node/http.go index ba362ace9193e90c0cdee9f1f99e5c8a24794e7e..1e75ae624b07d912c8f0a12a271611d0127f8fe4 100644 --- a/node/http.go +++ b/node/http.go @@ -5,6 +5,7 @@ package node import ( "context" + "crypto/tls" "flag" "fmt" "html/template" @@ -19,6 +20,7 @@ import ( "time" "git.autistici.org/ale/autoradio" + "git.autistici.org/ale/autoradio/node/acme" pb "git.autistici.org/ale/autoradio/proto" assetfs "github.com/elazarl/go-bindata-assetfs" "github.com/lpar/gzipped" @@ -27,12 +29,17 @@ import ( var ( disableDebugHandlers = flag.Bool("http-disable-debug", false, "disable HTTP /debug handlers") restrictDebugHandlers = flag.Bool("http-restrict-debug", false, "restrict access to /debug from localhost only") + + tpl *template.Template ) +func init() { + tpl = mustParseEmbeddedTemplates() +} + // Build the HTTP handler for the public HTTP endpoint. func newHTTPHandler(n *Node, icecastPort int, domain string) http.Handler { mux := http.NewServeMux() - tpl := mustParseEmbeddedTemplates() // Serve /static/ from builtin assets. Also serve directly // /favicon.ico using the same mechanism. @@ -115,7 +122,7 @@ func newHTTPHandler(n *Node, icecastPort int, domain string) http.Handler { case r.Method == "SOURCE" || r.Method == "PUT": sourceHandler.ServeHTTP(w, r) case r.Method == "GET" && (r.URL.Path == "" || r.URL.Path == "/"): - serveStatusPage(n, w, r, tpl, domain) + serveStatusPage(n, w, r, domain) case r.Method == "GET": redirectHandler.ServeHTTP(w, r) default: @@ -182,29 +189,31 @@ func serveRedirect(lb *loadBalancer, mount *pb.Mount, w http.ResponseWriter, r * return } - targetAddr := randomIPWithMatchingProtocol(targetNode.parsedAddrs, r.RemoteAddr) - if targetAddr == "" { - // This should not happen if the protocol filter in - // the load balancer evaluation did its job properly. - log.Printf("http: %s: protocol unavailable", mount.Path) - http.Error(w, "No nodes available with the right IP protocol", http.StatusServiceUnavailable) - return + // Use the node hostname for the redirect (compatible with using SSL certs). + targetURL := url.URL{ + Scheme: schemeFromRequest(r), + Host: targetNode.ep.Name, + Path: autoradio.MountPathToIcecastPath(mount.Path), } - targetURL := fmt.Sprintf("http://%s%s", targetAddr, autoradio.MountPathToIcecastPath(mount.Path)) - sendRedirect(w, r, targetURL) + sendRedirect(w, r, targetURL.String()) } // Serve a M3U response. This simply points back at the stream // redirect handler by dropping the .m3u suffix in the request URL. func sendM3U(w http.ResponseWriter, r *http.Request) { - // Build a fully qualified URL using the Host header. - m3u := fmt.Sprintf("http://%s%s\n", r.Host, strings.TrimSuffix(r.URL.Path, ".m3u")) + // Build a fully qualified URL using the Host header from the incoming request. + m3url := url.URL{ + Scheme: schemeFromRequest(r), + Host: r.Host, + Path: strings.TrimSuffix(r.URL.Path, ".m3u"), + } + m3us := m3url.String() - w.Header().Set("Content-Length", strconv.Itoa(len(m3u))) + w.Header().Set("Content-Length", strconv.Itoa(len(m3us))) w.Header().Set("Content-Type", "audio/x-mpegurl") addDefaultHeaders(w) - io.WriteString(w, m3u) //nolint + io.WriteString(w, m3us) //nolint } func sendRedirect(w http.ResponseWriter, r *http.Request, targetURL string) { @@ -227,6 +236,13 @@ func addDefaultHeaders(w http.ResponseWriter) { w.Header().Set("Cache-Control", "no-cache,no-store") } +func schemeFromRequest(r *http.Request) string { + if r.TLS != nil { + return "https" + } + return "http" +} + // Parse the templates that are embedded with the binary (in bindata.go). func mustParseEmbeddedTemplates() *template.Template { root := template.New("") @@ -275,8 +291,8 @@ func newHTTPServer(name, addr string, h http.Handler) *httpServer { Handler: h, ReadTimeout: 10 * time.Second, ReadHeaderTimeout: 3 * time.Second, - WriteTimeout: 10 * time.Second, - IdleTimeout: 30 * time.Second, + //WriteTimeout: 10 * time.Second, + IdleTimeout: 30 * time.Second, }, name: name, } @@ -285,19 +301,53 @@ func newHTTPServer(name, addr string, h http.Handler) *httpServer { func (s *httpServer) Name() string { return s.name } func (s *httpServer) Start(ctx context.Context) error { - return runHTTPServerWithContext(ctx, s.Server) + go func() { + <-ctx.Done() + + // Create an standalone context with a short timeout. + sctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) + s.Server.Shutdown(sctx) // nolint + cancel() + }() + err := s.Server.ListenAndServe() + if err == http.ErrServerClosed { + err = nil + } + return err +} + +type httpsServer struct { + *httpServer +} + +func newHTTPSServer(name, addr string, h http.Handler, mgr *acme.Manager) *httpsServer { + s := &httpsServer{ + httpServer: newHTTPServer(name, addr, h), + } + s.httpServer.Server.TLSConfig = &tls.Config{ + SessionTicketsDisabled: true, + PreferServerCipherSuites: true, + MinVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + }, + GetCertificate: mgr.GetCertificate, + } + + return s } -func runHTTPServerWithContext(ctx context.Context, srv *http.Server) error { +func (s *httpsServer) Start(ctx context.Context) error { go func() { <-ctx.Done() // Create an standalone context with a short timeout. sctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) - srv.Shutdown(sctx) // nolint + s.Server.Shutdown(sctx) // nolint cancel() }() - err := srv.ListenAndServe() + err := s.Server.ListenAndServeTLS("", "") if err == http.ErrServerClosed { err = nil } diff --git a/node/loadbalancing.go b/node/loadbalancing.go index 0c4363fe90e808b64fb9d31bd8d2c062516165f4..6f5df0e7c271dd464a38f0e871c07b831c628e33 100644 --- a/node/loadbalancing.go +++ b/node/loadbalancing.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "log" - "math/rand" "net" "net/http" "strconv" @@ -309,27 +308,3 @@ func filterIPByProto(ips []ipPort, v6 bool) []ipPort { } return candidates } - -// Pick a random IP for the specified proto. -func randomIPByProto(ips []ipPort, v6 bool) ipPort { - candidates := filterIPByProto(ips, v6) - if len(candidates) > 0 { - return candidates[rand.Intn(len(candidates))] - } - return ipPort{} -} - -// Select a random IP address from ips, with an IP protocol that -// matches remoteAddr. -func randomIPWithMatchingProtocol(ips []ipPort, remoteAddr string) string { - var isV6 bool - if host, _, err := net.SplitHostPort(remoteAddr); err == nil { - addr := net.ParseIP(host) - isV6 = (addr != nil && addr.To4() == nil) - } - ipp := randomIPByProto(ips, isV6) - if ipp.ip == nil { - return "" - } - return ipp.String() -} diff --git a/node/node.go b/node/node.go index a0071a9b6c7265a32b94894bcdf96c812aaa4c89..b73767f389159805d28581dd25ccbf83c40ead20 100644 --- a/node/node.go +++ b/node/node.go @@ -96,7 +96,11 @@ func New(parentCtx context.Context, session *concurrency.Session, ice Icecast, n n.updateIcecast() } }() - <-configReady + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-configReady: + } // Register the Icecast endpoints. First the gossip service, below // StatusEndpointPrefix with gossipPort, then the public Icecast diff --git a/node/proxy.go b/node/proxy.go index b9176e54fe7f393c5877b204bb37c65f970665a2..9b94d829ff6c0ccec17900db06acc760b66f50a2 100644 --- a/node/proxy.go +++ b/node/proxy.go @@ -14,6 +14,7 @@ import ( "log" "net" "net/http" + "net/http/httputil" "net/url" "strings" "sync" @@ -102,6 +103,18 @@ func doIcecastProxy(rw http.ResponseWriter, req *http.Request, target *url.URL, outreq.Header.Set("X-Forwarded-For", clientIP) } + // SSL requests can't be hijacked, so we just fire up a normal + // httputil.ReverseProxy (not fully functional for sources, but + // meh). + if req.TLS != nil { + log.Printf("TLS connection, switching to dumb reverse proxy mode") + u := *outreq.URL + u.Path = "/" + rp := httputil.NewSingleHostReverseProxy(&u) + rp.ServeHTTP(rw, outreq) + return + } + // Create the upstream connection and write the HTTP request // to it. upstream, err := dialer.Dial("tcp", outreq.URL.Host) @@ -147,7 +160,7 @@ func doIcecastProxy(rw http.ResponseWriter, req *http.Request, target *url.URL, } if conn == nil { log.Printf("http: proxy error: could not find hijackable connection") - rw.WriteHeader(http.StatusInternalServerError) + http.Error(rw, "could not find hijackable connection", http.StatusInternalServerError) return } defer conn.Close() @@ -156,21 +169,17 @@ func doIcecastProxy(rw http.ResponseWriter, req *http.Request, target *url.URL, } // Run two-way proxying. - handleProxy(conn.(*net.TCPConn), upstream.(*net.TCPConn), streamName) + handleProxy(conn, upstream, streamName) } // Copy data between two network connections. On recent Go versions // (>1.11), this is quite fast as io.CopyBuffer uses the splice() // system call internally (in exchange we lose the ability to figure // out which end of the connection is the source of the error). -func copyStream(tag string, out, in *net.TCPConn, promCounter prometheus.Counter, cntr *uint64) { +func copyStream(tag string, out io.WriteCloser, in io.ReadCloser, promCounter prometheus.Counter, cntr *uint64) { buf := getBuf() defer releaseBuf(buf) - // We used to do this in order to support half-closed connections. - //defer in.CloseRead() //nolint - //defer out.CloseWrite() //nolint - // Instead we do this and shut down the entire connection on error. // We end up calling Close() twice but that's not a huge problem. defer in.Close() //nolint @@ -195,7 +204,7 @@ func isCloseError(err error) bool { // Simple two-way TCP proxy that copies data in both directions and // can shutdown each direction of the connection independently. -func handleProxy(conn *net.TCPConn, upstream *net.TCPConn, streamName string) { +func handleProxy(conn, upstream io.ReadWriteCloser, streamName string) { l := streamListeners.WithLabelValues(streamName) l.Inc() var wg sync.WaitGroup diff --git a/node/server.go b/node/server.go index 5c3b7530096388cf1caa3d40284b3754a08504e6..efffd44027ae489094156d6601fd88e55bc75f55 100644 --- a/node/server.go +++ b/node/server.go @@ -5,9 +5,13 @@ import ( "fmt" "log" "net" + "net/http" "strconv" + "strings" "time" + "git.autistici.org/ale/autoradio/node/acme" + "go.etcd.io/etcd/clientv3" "golang.org/x/sync/errgroup" ) @@ -65,6 +69,25 @@ func (s *Server) Wait() error { return s.eg.Wait() } +// Config holds all the configuration parameters for a +// Server. NewServer has too many arguments otherwise. +type Config struct { + Domain string + Nameservers []string + DNSAddrs []net.IP + PeerAddr net.IP + HTTPPort int + HTTPSPort int + DNSPort int + GossipPort int + IcecastPort int + MetricsPort int + + ACMEEmail string + ACMEDirectoryURL string + CertNames []string +} + // NewServer creates a new Server. Will use publicAddrs / peerAddr to // build all the necessary addr/port combinations. // @@ -72,22 +95,42 @@ func (s *Server) Wait() error { // DNS servers will bind only to the dnsAddrs (both TCP and // UDP). The metrics and the status services, which are internal, will // bind on peerAddr. -func NewServer(ctx context.Context, n *Node, domain string, nameservers []string, dnsAddrs []net.IP, peerAddr net.IP, httpPort, dnsPort, gossipPort, icecastPort, metricsPort int) *Server { - httpHandler := newHTTPHandler(n, icecastPort, domain) - dnsHandler := newDNSHandler(n, domain, nameservers) - - servers := []genericServer{ - newStatusServer(mkaddr(peerAddr, gossipPort), n.statusMgr), - newHTTPServer("main", fmt.Sprintf(":%d", httpPort), httpHandler), - newHTTPServer("metrics", fmt.Sprintf(":%d", metricsPort), newMetricsHandler()), +func NewServer(ctx context.Context, etcd *clientv3.Client, n *Node, config *Config) (*Server, error) { + httpHandler := newHTTPHandler(n, config.IcecastPort, config.Domain) + dnsHandler := newDNSHandler(n, config.Domain, config.Nameservers) + + var servers []genericServer + + // If HTTPS is requested, inject the ACME handler on the HTTP handler. + if config.HTTPSPort > 0 { + acmeMgr, err := acme.NewManager(ctx, etcd, config.ACMEEmail, config.ACMEDirectoryURL, config.CertNames) + if err != nil { + return nil, err + } + + httpsHandler := httpHandler + servers = append(servers, newHTTPSServer("https", fmt.Sprintf(":%d", config.HTTPSPort), httpsHandler, acmeMgr)) + httpHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if strings.HasPrefix(req.URL.Path, "/.well-known/acme-challenge/") { + acmeMgr.ServeHTTP(w, req) + return + } + httpsHandler.ServeHTTP(w, req) + }) } - for _, ip := range dnsAddrs { + + servers = append(servers, newStatusServer(mkaddr(config.PeerAddr, config.GossipPort), n.statusMgr)) + servers = append(servers, newHTTPServer("main", fmt.Sprintf(":%d", config.HTTPPort), httpHandler)) + servers = append(servers, newHTTPServer("metrics", fmt.Sprintf(":%d", config.MetricsPort), newMetricsHandler())) + + for _, ip := range config.DNSAddrs { servers = append(servers, - newDNSServer("dns(udp)", mkaddr(ip, dnsPort), "udp", dnsHandler), - newDNSServer("dns(tcp)", mkaddr(ip, dnsPort), "tcp", dnsHandler), + newDNSServer("dns(udp)", mkaddr(ip, config.DNSPort), "udp", dnsHandler), + newDNSServer("dns(tcp)", mkaddr(ip, config.DNSPort), "tcp", dnsHandler), ) } - return buildServer(ctx, servers...) + + return buildServer(ctx, servers...), nil } func mkaddr(ip net.IP, port int) string { diff --git a/vendor/golang.org/x/crypto/acme/acme.go b/vendor/golang.org/x/crypto/acme/acme.go new file mode 100644 index 0000000000000000000000000000000000000000..02fde12db59f1c051ae8eae980d0a217c022e0ab --- /dev/null +++ b/vendor/golang.org/x/crypto/acme/acme.go @@ -0,0 +1,1097 @@ +// Copyright 2015 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 provides an implementation of the +// Automatic Certificate Management Environment (ACME) spec. +// The intial implementation was based on ACME draft-02 and +// is now being extended to comply with RFC 8555. +// See https://tools.ietf.org/html/draft-ietf-acme-acme-02 +// and https://tools.ietf.org/html/rfc8555 for details. +// +// Most common scenarios will want to use autocert subdirectory instead, +// which provides automatic access to certificates from Let's Encrypt +// and any other ACME-based CA. +// +// This package is a work in progress and makes no API stability promises. +package acme + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/base64" + "encoding/hex" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "io" + "io/ioutil" + "math/big" + "net/http" + "strings" + "sync" + "time" +) + +const ( + // LetsEncryptURL is the Directory endpoint of Let's Encrypt CA. + LetsEncryptURL = "https://acme-v02.api.letsencrypt.org/directory" + + // ALPNProto is the ALPN protocol name used by a CA server when validating + // tls-alpn-01 challenges. + // + // Package users must ensure their servers can negotiate the ACME ALPN in + // order for tls-alpn-01 challenge verifications to succeed. + // See the crypto/tls package's Config.NextProtos field. + ALPNProto = "acme-tls/1" +) + +// idPeACMEIdentifierV1 is the OID for the ACME extension for the TLS-ALPN challenge. +var idPeACMEIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1} + +const ( + maxChainLen = 5 // max depth and breadth of a certificate chain + maxCertSize = 1 << 20 // max size of a certificate, in DER bytes + // Used for decoding certs from application/pem-certificate-chain response, + // the default when in RFC mode. + maxCertChainSize = maxCertSize * maxChainLen + + // Max number of collected nonces kept in memory. + // Expect usual peak of 1 or 2. + maxNonces = 100 +) + +// 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 { + // Key is the account key used to register with a CA and sign requests. + // Key.Public() must return a *rsa.PublicKey or *ecdsa.PublicKey. + // + // The following algorithms are supported: + // RS256, ES256, ES384 and ES512. + // See RFC7518 for more details about the algorithms. + Key crypto.Signer + + // HTTPClient optionally specifies an HTTP client to use + // instead of http.DefaultClient. + HTTPClient *http.Client + + // 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 + + // RetryBackoff computes the duration after which the nth retry of a failed request + // should occur. The value of n for the first call on failure is 1. + // The values of r and resp are the request and response of the last failed attempt. + // If the returned value is negative or zero, no more retries are done and an error + // is returned to the caller of the original method. + // + // Requests which result in a 4xx client error are not retried, + // except for 400 Bad Request due to "bad nonce" errors and 429 Too Many Requests. + // + // If RetryBackoff is nil, a truncated exponential backoff algorithm + // with the ceiling of 10 seconds is used, where each subsequent retry n + // is done after either ("Retry-After" + jitter) or (2^n seconds + jitter), + // preferring the former if "Retry-After" header is found in the resp. + // The jitter is a random value up to 1 second. + RetryBackoff func(n int, r *http.Request, resp *http.Response) time.Duration + + // UserAgent is prepended to the User-Agent header sent to the ACME server, + // which by default is this package's name and version. + // + // Reusable libraries and tools in particular should set this value to be + // identifiable by the server, in case they are causing issues. + UserAgent string + + cacheMu sync.Mutex + dir *Directory // cached result of Client's Discover method + kid keyID // cached Account.URI obtained from registerRFC or getAccountRFC + + noncesMu sync.Mutex + nonces map[string]struct{} // nonces collected from previous responses +} + +// accountKID returns a key ID associated with c.Key, the account identity +// provided by the CA during RFC based registration. +// It assumes c.Discover has already been called. +// +// accountKID requires at most one network roundtrip. +// It caches only successful result. +// +// When in pre-RFC mode or when c.getRegRFC responds with an error, accountKID +// returns noKeyID. +func (c *Client) accountKID(ctx context.Context) keyID { + c.cacheMu.Lock() + defer c.cacheMu.Unlock() + if !c.dir.rfcCompliant() { + return noKeyID + } + if c.kid != noKeyID { + return c.kid + } + a, err := c.getRegRFC(ctx) + if err != nil { + return noKeyID + } + c.kid = keyID(a.URI) + return c.kid +} + +// 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(ctx context.Context) (Directory, error) { + c.cacheMu.Lock() + defer c.cacheMu.Unlock() + if c.dir != nil { + return *c.dir, nil + } + + res, err := c.get(ctx, c.directoryURL(), wantStatus(http.StatusOK)) + if err != nil { + return Directory{}, err + } + defer res.Body.Close() + c.addNonce(res.Header) + + var v struct { + Reg string `json:"new-reg"` + RegRFC string `json:"newAccount"` + Authz string `json:"new-authz"` + AuthzRFC string `json:"newAuthz"` + OrderRFC string `json:"newOrder"` + Cert string `json:"new-cert"` + Revoke string `json:"revoke-cert"` + RevokeRFC string `json:"revokeCert"` + NonceRFC string `json:"newNonce"` + KeyChangeRFC string `json:"keyChange"` + Meta struct { + Terms string `json:"terms-of-service"` + TermsRFC string `json:"termsOfService"` + WebsiteRFC string `json:"website"` + CAA []string `json:"caa-identities"` + CAARFC []string `json:"caaIdentities"` + ExternalAcctRFC bool `json:"externalAccountRequired"` + } + } + if err := json.NewDecoder(res.Body).Decode(&v); err != nil { + return Directory{}, err + } + if v.OrderRFC == "" { + // Non-RFC compliant ACME CA. + c.dir = &Directory{ + RegURL: v.Reg, + AuthzURL: v.Authz, + CertURL: v.Cert, + RevokeURL: v.Revoke, + Terms: v.Meta.Terms, + Website: v.Meta.WebsiteRFC, + CAA: v.Meta.CAA, + } + return *c.dir, nil + } + // RFC compliant ACME CA. + c.dir = &Directory{ + RegURL: v.RegRFC, + AuthzURL: v.AuthzRFC, + OrderURL: v.OrderRFC, + RevokeURL: v.RevokeRFC, + NonceURL: v.NonceRFC, + KeyChangeURL: v.KeyChangeRFC, + Terms: v.Meta.TermsRFC, + Website: v.Meta.WebsiteRFC, + CAA: v.Meta.CAARFC, + ExternalAccountRequired: v.Meta.ExternalAcctRFC, + } + return *c.dir, nil +} + +func (c *Client) directoryURL() string { + if c.DirectoryURL != "" { + return c.DirectoryURL + } + return LetsEncryptURL +} + +// CreateCert requests a new certificate using the Certificate Signing Request csr encoded in DER format. +// It is incompatible with RFC 8555. Callers should use CreateOrderCert when interfacing +// with an RFC-compliant CA. +// +// The exp argument indicates the desired certificate validity duration. CA may issue a certificate +// with a different duration. +// If the bundle argument is true, the returned value will also contain the CA (issuer) certificate chain. +// +// In the case where CA server does not provide the issued certificate in the response, +// CreateCert will poll certURL using c.FetchCert, which will result in additional round-trips. +// In such a scenario, the caller can cancel the polling with ctx. +// +// CreateCert returns an error if the CA's response or chain was unreasonably large. +// Callers are encouraged to parse the returned value to ensure the certificate is valid and has the expected features. +func (c *Client) CreateCert(ctx context.Context, csr []byte, exp time.Duration, bundle bool) (der [][]byte, certURL string, err error) { + if _, err := c.Discover(ctx); err != nil { + return nil, "", err + } + + req := struct { + Resource string `json:"resource"` + CSR string `json:"csr"` + NotBefore string `json:"notBefore,omitempty"` + NotAfter string `json:"notAfter,omitempty"` + }{ + Resource: "new-cert", + CSR: base64.RawURLEncoding.EncodeToString(csr), + } + now := timeNow() + req.NotBefore = now.Format(time.RFC3339) + if exp > 0 { + req.NotAfter = now.Add(exp).Format(time.RFC3339) + } + + res, err := c.post(ctx, nil, c.dir.CertURL, req, wantStatus(http.StatusCreated)) + if err != nil { + return nil, "", err + } + defer res.Body.Close() + + curl := res.Header.Get("Location") // cert permanent URL + if res.ContentLength == 0 { + // no cert in the body; poll until we get it + cert, err := c.FetchCert(ctx, curl, bundle) + return cert, curl, err + } + // slurp issued cert and CA chain, if requested + cert, err := c.responseCert(ctx, res, bundle) + return cert, curl, err +} + +// FetchCert retrieves already issued certificate from the given url, in DER format. +// It retries the request until the certificate is successfully retrieved, +// context is cancelled by the caller or an error response is received. +// +// If the bundle argument is true, the returned value also contains the CA (issuer) +// certificate chain. +// +// FetchCert returns an error if the CA's response or chain was unreasonably large. +// Callers are encouraged to parse the returned value to ensure the certificate is valid +// and has expected features. +func (c *Client) FetchCert(ctx context.Context, url string, bundle bool) ([][]byte, error) { + dir, err := c.Discover(ctx) + if err != nil { + return nil, err + } + if dir.rfcCompliant() { + return c.fetchCertRFC(ctx, url, bundle) + } + + // Legacy non-authenticated GET request. + res, err := c.get(ctx, url, wantStatus(http.StatusOK)) + if err != nil { + return nil, err + } + return c.responseCert(ctx, res, bundle) +} + +// RevokeCert revokes a previously issued certificate cert, provided in DER format. +// +// The key argument, used to sign the request, must be authorized +// to revoke the certificate. It's up to the CA to decide which keys are authorized. +// For instance, the key pair of the certificate may be authorized. +// If the key is nil, c.Key is used instead. +func (c *Client) RevokeCert(ctx context.Context, key crypto.Signer, cert []byte, reason CRLReasonCode) error { + dir, err := c.Discover(ctx) + if err != nil { + return err + } + if dir.rfcCompliant() { + return c.revokeCertRFC(ctx, key, cert, reason) + } + + // Legacy CA. + body := &struct { + Resource string `json:"resource"` + Cert string `json:"certificate"` + Reason int `json:"reason"` + }{ + Resource: "revoke-cert", + Cert: base64.RawURLEncoding.EncodeToString(cert), + Reason: int(reason), + } + res, err := c.post(ctx, key, dir.RevokeURL, body, wantStatus(http.StatusOK)) + if err != nil { + return err + } + defer res.Body.Close() + return nil +} + +// AcceptTOS always returns true to indicate the acceptance of a CA's Terms of Service +// during account registration. See Register method of Client for more details. +func AcceptTOS(tosURL string) bool { return true } + +// Register creates a new account with the CA using c.Key. +// It returns the registered account. The account acct is not modified. +// +// The registration may require the caller to agree to the CA's Terms of Service (TOS). +// If so, and the account has not indicated the acceptance of the terms (see Account for details), +// Register calls prompt with a TOS URL provided by the CA. Prompt should report +// whether the caller agrees to the terms. To always accept the terms, the caller can use AcceptTOS. +// +// When interfacing with an RFC-compliant CA, non-RFC 8555 fields of acct are ignored +// and prompt is called if Directory's Terms field is non-zero. +// Also see Error's Instance field for when a CA requires already registered accounts to agree +// to an updated Terms of Service. +func (c *Client) Register(ctx context.Context, acct *Account, prompt func(tosURL string) bool) (*Account, error) { + dir, err := c.Discover(ctx) + if err != nil { + return nil, err + } + if dir.rfcCompliant() { + return c.registerRFC(ctx, acct, prompt) + } + + // Legacy ACME draft registration flow. + a, err := c.doReg(ctx, dir.RegURL, "new-reg", acct) + if err != nil { + return nil, err + } + var accept bool + if a.CurrentTerms != "" && a.CurrentTerms != a.AgreedTerms { + accept = prompt(a.CurrentTerms) + } + if accept { + a.AgreedTerms = a.CurrentTerms + a, err = c.UpdateReg(ctx, a) + } + return a, err +} + +// GetReg retrieves an existing account associated with c.Key. +// +// The url argument is an Account URI used with pre-RFC 8555 CAs. +// It is ignored when interfacing with an RFC-compliant CA. +func (c *Client) GetReg(ctx context.Context, url string) (*Account, error) { + dir, err := c.Discover(ctx) + if err != nil { + return nil, err + } + if dir.rfcCompliant() { + return c.getRegRFC(ctx) + } + + // Legacy CA. + a, err := c.doReg(ctx, url, "reg", nil) + if err != nil { + return nil, err + } + a.URI = url + return a, nil +} + +// UpdateReg updates an existing registration. +// It returns an updated account copy. The provided account is not modified. +// +// When interfacing with RFC-compliant CAs, a.URI is ignored and the account URL +// associated with c.Key is used instead. +func (c *Client) UpdateReg(ctx context.Context, acct *Account) (*Account, error) { + dir, err := c.Discover(ctx) + if err != nil { + return nil, err + } + if dir.rfcCompliant() { + return c.updateRegRFC(ctx, acct) + } + + // Legacy CA. + uri := acct.URI + a, err := c.doReg(ctx, uri, "reg", acct) + if err != nil { + return nil, err + } + a.URI = uri + return a, nil +} + +// Authorize performs the initial step in the pre-authorization flow, +// as opposed to order-based 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. +// +// Once complete, the caller can use AuthorizeOrder which the CA +// should provision with the already satisfied authorization. +// For pre-RFC CAs, the caller can proceed directly to requesting a certificate +// using CreateCert method. +// +// If an authorization has been previously granted, the CA may return +// a valid authorization which has its Status field set to StatusValid. +// +// More about pre-authorization can be found at +// https://tools.ietf.org/html/rfc8555#section-7.4.1. +func (c *Client) Authorize(ctx context.Context, domain string) (*Authorization, error) { + return c.authorize(ctx, "dns", domain) +} + +// AuthorizeIP is the same as Authorize but requests IP address authorization. +// Clients which successfully obtain such authorization may request to issue +// a certificate for IP addresses. +// +// See the ACME spec extension for more details about IP address identifiers: +// https://tools.ietf.org/html/draft-ietf-acme-ip. +func (c *Client) AuthorizeIP(ctx context.Context, ipaddr string) (*Authorization, error) { + return c.authorize(ctx, "ip", ipaddr) +} + +func (c *Client) authorize(ctx context.Context, typ, val string) (*Authorization, error) { + if _, err := c.Discover(ctx); err != nil { + return nil, err + } + + type authzID struct { + Type string `json:"type"` + Value string `json:"value"` + } + req := struct { + Resource string `json:"resource"` + Identifier authzID `json:"identifier"` + }{ + Resource: "new-authz", + Identifier: authzID{Type: typ, Value: val}, + } + res, err := c.post(ctx, nil, c.dir.AuthzURL, req, wantStatus(http.StatusCreated)) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var v wireAuthz + if err := json.NewDecoder(res.Body).Decode(&v); err != nil { + return nil, fmt.Errorf("acme: invalid response: %v", err) + } + if v.Status != StatusPending && v.Status != StatusValid { + return nil, fmt.Errorf("acme: unexpected status: %s", v.Status) + } + return v.authorization(res.Header.Get("Location")), nil +} + +// GetAuthorization retrieves an authorization identified by the given URL. +// +// If a caller needs to poll an authorization until its status is final, +// see the WaitAuthorization method. +func (c *Client) GetAuthorization(ctx context.Context, url string) (*Authorization, error) { + dir, err := c.Discover(ctx) + if err != nil { + return nil, err + } + + var res *http.Response + if dir.rfcCompliant() { + res, err = c.postAsGet(ctx, url, wantStatus(http.StatusOK)) + } else { + res, err = c.get(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted)) + } + if err != nil { + return nil, err + } + defer res.Body.Close() + var v wireAuthz + if err := json.NewDecoder(res.Body).Decode(&v); err != nil { + return nil, fmt.Errorf("acme: invalid response: %v", err) + } + return v.authorization(url), nil +} + +// RevokeAuthorization relinquishes an existing authorization identified +// by the given URL. +// The url argument is an Authorization.URI value. +// +// If successful, the caller will be required to obtain a new authorization +// using the Authorize or AuthorizeOrder methods before being able to request +// a new certificate for the domain associated with the authorization. +// +// It does not revoke existing certificates. +func (c *Client) RevokeAuthorization(ctx context.Context, url string) error { + // Required for c.accountKID() when in RFC mode. + if _, err := c.Discover(ctx); err != nil { + return err + } + + req := struct { + Resource string `json:"resource"` + Status string `json:"status"` + Delete bool `json:"delete"` + }{ + Resource: "authz", + Status: "deactivated", + Delete: true, + } + res, err := c.post(ctx, nil, url, req, wantStatus(http.StatusOK)) + if err != nil { + return err + } + defer res.Body.Close() + return nil +} + +// WaitAuthorization polls an authorization at the given URL +// until it is in one of the final states, StatusValid or StatusInvalid, +// the ACME CA responded with a 4xx error code, or the context is done. +// +// It returns a non-nil Authorization only if its Status is StatusValid. +// In all other cases WaitAuthorization returns an error. +// If the Status is StatusInvalid, the returned error is of type *AuthorizationError. +func (c *Client) WaitAuthorization(ctx context.Context, url string) (*Authorization, error) { + // Required for c.accountKID() when in RFC mode. + dir, err := c.Discover(ctx) + if err != nil { + return nil, err + } + getfn := c.postAsGet + if !dir.rfcCompliant() { + getfn = c.get + } + + for { + res, err := getfn(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted)) + if err != nil { + return nil, err + } + + var raw wireAuthz + err = json.NewDecoder(res.Body).Decode(&raw) + res.Body.Close() + switch { + case err != nil: + // Skip and retry. + case raw.Status == StatusValid: + return raw.authorization(url), nil + case raw.Status == StatusInvalid: + return nil, raw.error(url) + } + + // Exponential backoff is implemented in c.get above. + // This is just to prevent continuously hitting the CA + // while waiting for a final authorization status. + d := retryAfter(res.Header.Get("Retry-After")) + if d == 0 { + // Given that the fastest challenges TLS-SNI and HTTP-01 + // require a CA to make at least 1 network round trip + // and most likely persist a challenge state, + // this default delay seems reasonable. + d = time.Second + } + t := time.NewTimer(d) + select { + case <-ctx.Done(): + t.Stop() + return nil, ctx.Err() + case <-t.C: + // Retry. + } + } +} + +// GetChallenge retrieves the current status of an challenge. +// +// A client typically polls a challenge status using this method. +func (c *Client) GetChallenge(ctx context.Context, url string) (*Challenge, error) { + // Required for c.accountKID() when in RFC mode. + dir, err := c.Discover(ctx) + if err != nil { + return nil, err + } + + getfn := c.postAsGet + if !dir.rfcCompliant() { + getfn = c.get + } + res, err := getfn(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted)) + if err != nil { + return nil, err + } + + defer res.Body.Close() + v := wireChallenge{URI: url} + if err := json.NewDecoder(res.Body).Decode(&v); err != nil { + return nil, fmt.Errorf("acme: invalid response: %v", err) + } + return v.challenge(), nil +} + +// Accept informs the server that the client accepts one of its challenges +// previously obtained with c.Authorize. +// +// The server will then perform the validation asynchronously. +func (c *Client) Accept(ctx context.Context, chal *Challenge) (*Challenge, error) { + // Required for c.accountKID() when in RFC mode. + dir, err := c.Discover(ctx) + if err != nil { + return nil, err + } + + var req interface{} = json.RawMessage("{}") // RFC-compliant CA + if !dir.rfcCompliant() { + auth, err := keyAuth(c.Key.Public(), chal.Token) + if err != nil { + return nil, err + } + req = struct { + Resource string `json:"resource"` + Type string `json:"type"` + Auth string `json:"keyAuthorization"` + }{ + Resource: "challenge", + Type: chal.Type, + Auth: auth, + } + } + res, err := c.post(ctx, nil, chal.URI, req, wantStatus( + http.StatusOK, // according to the spec + http.StatusAccepted, // Let's Encrypt: see https://goo.gl/WsJ7VT (acme-divergences.md) + )) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var v wireChallenge + if err := json.NewDecoder(res.Body).Decode(&v); err != nil { + return nil, fmt.Errorf("acme: invalid response: %v", err) + } + return v.challenge(), nil +} + +// DNS01ChallengeRecord returns a DNS record value for a dns-01 challenge response. +// A TXT record containing the returned value must be provisioned under +// "_acme-challenge" name of the domain being validated. +// +// The token argument is a Challenge.Token value. +func (c *Client) DNS01ChallengeRecord(token string) (string, error) { + ka, err := keyAuth(c.Key.Public(), token) + if err != nil { + return "", err + } + b := sha256.Sum256([]byte(ka)) + return base64.RawURLEncoding.EncodeToString(b[:]), nil +} + +// HTTP01ChallengeResponse returns the response for an http-01 challenge. +// Servers should respond with the value to HTTP requests at the URL path +// provided by HTTP01ChallengePath to validate the challenge and prove control +// over a domain name. +// +// The token argument is a Challenge.Token value. +func (c *Client) HTTP01ChallengeResponse(token string) (string, error) { + return keyAuth(c.Key.Public(), token) +} + +// HTTP01ChallengePath returns the URL path at which the response for an http-01 challenge +// should be provided by the servers. +// The response value can be obtained with HTTP01ChallengeResponse. +// +// The token argument is a Challenge.Token value. +func (c *Client) HTTP01ChallengePath(token string) string { + return "/.well-known/acme-challenge/" + token +} + +// TLSSNI01ChallengeCert creates a certificate for TLS-SNI-01 challenge response. +// +// Deprecated: This challenge type is unused in both draft-02 and RFC versions of ACME spec. +func (c *Client) TLSSNI01ChallengeCert(token string, opt ...CertOption) (cert tls.Certificate, name string, err error) { + ka, err := keyAuth(c.Key.Public(), token) + if err != nil { + return tls.Certificate{}, "", err + } + b := sha256.Sum256([]byte(ka)) + h := hex.EncodeToString(b[:]) + name = fmt.Sprintf("%s.%s.acme.invalid", h[:32], h[32:]) + cert, err = tlsChallengeCert([]string{name}, opt) + if err != nil { + return tls.Certificate{}, "", err + } + return cert, name, nil +} + +// TLSSNI02ChallengeCert creates a certificate for TLS-SNI-02 challenge response. +// +// Deprecated: This challenge type is unused in both draft-02 and RFC versions of ACME spec. +func (c *Client) TLSSNI02ChallengeCert(token string, opt ...CertOption) (cert tls.Certificate, name string, err error) { + b := sha256.Sum256([]byte(token)) + h := hex.EncodeToString(b[:]) + sanA := fmt.Sprintf("%s.%s.token.acme.invalid", h[:32], h[32:]) + + ka, err := keyAuth(c.Key.Public(), token) + if err != nil { + return tls.Certificate{}, "", err + } + b = sha256.Sum256([]byte(ka)) + h = hex.EncodeToString(b[:]) + sanB := fmt.Sprintf("%s.%s.ka.acme.invalid", h[:32], h[32:]) + + cert, err = tlsChallengeCert([]string{sanA, sanB}, opt) + if err != nil { + return tls.Certificate{}, "", err + } + return cert, sanA, nil +} + +// TLSALPN01ChallengeCert creates a certificate for TLS-ALPN-01 challenge response. +// Servers can present the certificate to validate the challenge and prove control +// over a domain name. For more details on TLS-ALPN-01 see +// https://tools.ietf.org/html/draft-shoemaker-acme-tls-alpn-00#section-3 +// +// The token argument is a Challenge.Token value. +// If a WithKey option is provided, its private part signs the returned cert, +// and the public part is used to specify the signee. +// If no WithKey option is provided, a new ECDSA key is generated using P-256 curve. +// +// The returned certificate is valid for the next 24 hours and must be presented only when +// the server name in the TLS ClientHello matches the domain, and the special acme-tls/1 ALPN protocol +// has been specified. +func (c *Client) TLSALPN01ChallengeCert(token, domain string, opt ...CertOption) (cert tls.Certificate, err error) { + ka, err := keyAuth(c.Key.Public(), token) + if err != nil { + return tls.Certificate{}, err + } + shasum := sha256.Sum256([]byte(ka)) + extValue, err := asn1.Marshal(shasum[:]) + if err != nil { + return tls.Certificate{}, err + } + acmeExtension := pkix.Extension{ + Id: idPeACMEIdentifierV1, + Critical: true, + Value: extValue, + } + + tmpl := defaultTLSChallengeCertTemplate() + + var newOpt []CertOption + for _, o := range opt { + switch o := o.(type) { + case *certOptTemplate: + t := *(*x509.Certificate)(o) // shallow copy is ok + tmpl = &t + default: + newOpt = append(newOpt, o) + } + } + tmpl.ExtraExtensions = append(tmpl.ExtraExtensions, acmeExtension) + newOpt = append(newOpt, WithTemplate(tmpl)) + return tlsChallengeCert([]string{domain}, newOpt) +} + +// doReg sends all types of registration requests the old way (pre-RFC world). +// The type of request is identified by typ argument, which is a "resource" +// in the ACME spec terms. +// +// A non-nil acct argument indicates whether the intention is to mutate data +// of the Account. Only Contact and Agreement of its fields are used +// in such cases. +func (c *Client) doReg(ctx context.Context, url string, typ string, acct *Account) (*Account, error) { + req := struct { + Resource string `json:"resource"` + Contact []string `json:"contact,omitempty"` + Agreement string `json:"agreement,omitempty"` + }{ + Resource: typ, + } + if acct != nil { + req.Contact = acct.Contact + req.Agreement = acct.AgreedTerms + } + res, err := c.post(ctx, nil, url, req, wantStatus( + http.StatusOK, // updates and deletes + http.StatusCreated, // new account creation + http.StatusAccepted, // Let's Encrypt divergent implementation + )) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var v struct { + Contact []string + Agreement string + Authorizations string + Certificates string + } + if err := json.NewDecoder(res.Body).Decode(&v); err != nil { + return nil, fmt.Errorf("acme: invalid response: %v", err) + } + var tos string + if v := linkHeader(res.Header, "terms-of-service"); len(v) > 0 { + tos = v[0] + } + var authz string + if v := linkHeader(res.Header, "next"); len(v) > 0 { + authz = v[0] + } + return &Account{ + URI: res.Header.Get("Location"), + Contact: v.Contact, + AgreedTerms: v.Agreement, + CurrentTerms: tos, + Authz: authz, + Authorizations: v.Authorizations, + Certificates: v.Certificates, + }, nil +} + +// popNonce returns a nonce value previously stored with c.addNonce +// or fetches a fresh one from c.dir.NonceURL. +// If NonceURL is empty, it first tries c.directoryURL() and, failing that, +// the provided url. +func (c *Client) popNonce(ctx context.Context, url string) (string, error) { + c.noncesMu.Lock() + defer c.noncesMu.Unlock() + if len(c.nonces) == 0 { + if c.dir != nil && c.dir.NonceURL != "" { + return c.fetchNonce(ctx, c.dir.NonceURL) + } + dirURL := c.directoryURL() + v, err := c.fetchNonce(ctx, dirURL) + if err != nil && url != dirURL { + v, err = c.fetchNonce(ctx, url) + } + return v, err + } + var nonce string + for nonce = range c.nonces { + delete(c.nonces, nonce) + break + } + return nonce, nil +} + +// clearNonces clears any stored nonces +func (c *Client) clearNonces() { + c.noncesMu.Lock() + defer c.noncesMu.Unlock() + c.nonces = make(map[string]struct{}) +} + +// addNonce stores a nonce value found in h (if any) for future use. +func (c *Client) addNonce(h http.Header) { + v := nonceFromHeader(h) + if v == "" { + return + } + c.noncesMu.Lock() + defer c.noncesMu.Unlock() + if len(c.nonces) >= maxNonces { + return + } + if c.nonces == nil { + c.nonces = make(map[string]struct{}) + } + c.nonces[v] = struct{}{} +} + +func (c *Client) fetchNonce(ctx context.Context, url string) (string, error) { + r, err := http.NewRequest("HEAD", url, nil) + if err != nil { + return "", err + } + resp, err := c.doNoRetry(ctx, r) + if err != nil { + return "", err + } + defer resp.Body.Close() + nonce := nonceFromHeader(resp.Header) + if nonce == "" { + if resp.StatusCode > 299 { + return "", responseError(resp) + } + return "", errors.New("acme: nonce not found") + } + return nonce, nil +} + +func nonceFromHeader(h http.Header) string { + return h.Get("Replay-Nonce") +} + +func (c *Client) responseCert(ctx context.Context, res *http.Response, bundle bool) ([][]byte, error) { + b, err := ioutil.ReadAll(io.LimitReader(res.Body, maxCertSize+1)) + if err != nil { + return nil, fmt.Errorf("acme: response stream: %v", err) + } + if len(b) > maxCertSize { + return nil, errors.New("acme: certificate is too big") + } + cert := [][]byte{b} + if !bundle { + return cert, nil + } + + // Append CA chain cert(s). + // At least one is required according to the spec: + // https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-6.3.1 + up := linkHeader(res.Header, "up") + if len(up) == 0 { + return nil, errors.New("acme: rel=up link not found") + } + if len(up) > maxChainLen { + return nil, errors.New("acme: rel=up link is too large") + } + for _, url := range up { + cc, err := c.chainCert(ctx, url, 0) + if err != nil { + return nil, err + } + cert = append(cert, cc...) + } + return cert, nil +} + +// chainCert fetches CA certificate chain recursively by following "up" links. +// Each recursive call increments the depth by 1, resulting in an error +// if the recursion level reaches maxChainLen. +// +// First chainCert call starts with depth of 0. +func (c *Client) chainCert(ctx context.Context, url string, depth int) ([][]byte, error) { + if depth >= maxChainLen { + return nil, errors.New("acme: certificate chain is too deep") + } + + res, err := c.get(ctx, url, wantStatus(http.StatusOK)) + if err != nil { + return nil, err + } + defer res.Body.Close() + b, err := ioutil.ReadAll(io.LimitReader(res.Body, maxCertSize+1)) + if err != nil { + return nil, err + } + if len(b) > maxCertSize { + return nil, errors.New("acme: certificate is too big") + } + chain := [][]byte{b} + + uplink := linkHeader(res.Header, "up") + if len(uplink) > maxChainLen { + return nil, errors.New("acme: certificate chain is too large") + } + for _, up := range uplink { + cc, err := c.chainCert(ctx, up, depth+1) + if err != nil { + return nil, err + } + chain = append(chain, cc...) + } + + return chain, nil +} + +// linkHeader returns URI-Reference values of all Link headers +// with relation-type rel. +// See https://tools.ietf.org/html/rfc5988#section-5 for details. +func linkHeader(h http.Header, rel string) []string { + var links []string + for _, v := range h["Link"] { + parts := strings.Split(v, ";") + for _, p := range parts { + p = strings.TrimSpace(p) + if !strings.HasPrefix(p, "rel=") { + continue + } + if v := strings.Trim(p[4:], `"`); v == rel { + links = append(links, strings.Trim(parts[0], "<>")) + } + } + } + return links +} + +// keyAuth generates a key authorization string for a given token. +func keyAuth(pub crypto.PublicKey, token string) (string, error) { + th, err := JWKThumbprint(pub) + if err != nil { + return "", err + } + return fmt.Sprintf("%s.%s", token, th), nil +} + +// defaultTLSChallengeCertTemplate is a template used to create challenge certs for TLS challenges. +func defaultTLSChallengeCertTemplate() *x509.Certificate { + return &x509.Certificate{ + SerialNumber: big.NewInt(1), + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } +} + +// tlsChallengeCert creates a temporary certificate for TLS-SNI challenges +// with the given SANs and auto-generated public/private key pair. +// The Subject Common Name is set to the first SAN to aid debugging. +// To create a cert with a custom key pair, specify WithKey option. +func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) { + var key crypto.Signer + tmpl := defaultTLSChallengeCertTemplate() + for _, o := range opt { + switch o := o.(type) { + case *certOptKey: + if key != nil { + return tls.Certificate{}, errors.New("acme: duplicate key option") + } + key = o.key + case *certOptTemplate: + t := *(*x509.Certificate)(o) // shallow copy is ok + tmpl = &t + default: + // package's fault, if we let this happen: + panic(fmt.Sprintf("unsupported option type %T", o)) + } + } + if key == nil { + var err error + if key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader); err != nil { + return tls.Certificate{}, err + } + } + tmpl.DNSNames = san + if len(san) > 0 { + tmpl.Subject.CommonName = san[0] + } + + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) + if err != nil { + return tls.Certificate{}, err + } + return tls.Certificate{ + Certificate: [][]byte{der}, + PrivateKey: key, + }, nil +} + +// encodePEM returns b encoded as PEM with block of type typ. +func encodePEM(typ string, b []byte) []byte { + pb := &pem.Block{Type: typ, Bytes: b} + return pem.EncodeToMemory(pb) +} + +// timeNow is useful for testing for fixed current time. +var timeNow = time.Now diff --git a/vendor/golang.org/x/crypto/acme/http.go b/vendor/golang.org/x/crypto/acme/http.go new file mode 100644 index 0000000000000000000000000000000000000000..c51943e71a420c64962aeff72cbfb7e2a0248239 --- /dev/null +++ b/vendor/golang.org/x/crypto/acme/http.go @@ -0,0 +1,321 @@ +// Copyright 2018 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 ( + "bytes" + "context" + "crypto" + "crypto/rand" + "encoding/json" + "fmt" + "io/ioutil" + "math/big" + "net/http" + "strconv" + "strings" + "time" +) + +// retryTimer encapsulates common logic for retrying unsuccessful requests. +// It is not safe for concurrent use. +type retryTimer struct { + // backoffFn provides backoff delay sequence for retries. + // See Client.RetryBackoff doc comment. + backoffFn func(n int, r *http.Request, res *http.Response) time.Duration + // n is the current retry attempt. + n int +} + +func (t *retryTimer) inc() { + t.n++ +} + +// backoff pauses the current goroutine as described in Client.RetryBackoff. +func (t *retryTimer) backoff(ctx context.Context, r *http.Request, res *http.Response) error { + d := t.backoffFn(t.n, r, res) + if d <= 0 { + return fmt.Errorf("acme: no more retries for %s; tried %d time(s)", r.URL, t.n) + } + wakeup := time.NewTimer(d) + defer wakeup.Stop() + select { + case <-ctx.Done(): + return ctx.Err() + case <-wakeup.C: + return nil + } +} + +func (c *Client) retryTimer() *retryTimer { + f := c.RetryBackoff + if f == nil { + f = defaultBackoff + } + return &retryTimer{backoffFn: f} +} + +// defaultBackoff provides default Client.RetryBackoff implementation +// using a truncated exponential backoff algorithm, +// as described in Client.RetryBackoff. +// +// The n argument is always bounded between 1 and 30. +// The returned value is always greater than 0. +func defaultBackoff(n int, r *http.Request, res *http.Response) time.Duration { + const max = 10 * time.Second + var jitter time.Duration + if x, err := rand.Int(rand.Reader, big.NewInt(1000)); err == nil { + // Set the minimum to 1ms to avoid a case where + // an invalid Retry-After value is parsed into 0 below, + // resulting in the 0 returned value which would unintentionally + // stop the retries. + jitter = (1 + time.Duration(x.Int64())) * time.Millisecond + } + if v, ok := res.Header["Retry-After"]; ok { + return retryAfter(v[0]) + jitter + } + + if n < 1 { + n = 1 + } + if n > 30 { + n = 30 + } + d := time.Duration(1<<uint(n-1))*time.Second + jitter + if d > max { + return max + } + return d +} + +// retryAfter parses a Retry-After HTTP header value, +// trying to convert v into an int (seconds) or use http.ParseTime otherwise. +// It returns zero value if v cannot be parsed. +func retryAfter(v string) time.Duration { + if i, err := strconv.Atoi(v); err == nil { + return time.Duration(i) * time.Second + } + t, err := http.ParseTime(v) + if err != nil { + return 0 + } + return t.Sub(timeNow()) +} + +// resOkay is a function that reports whether the provided response is okay. +// It is expected to keep the response body unread. +type resOkay func(*http.Response) bool + +// wantStatus returns a function which reports whether the code +// matches the status code of a response. +func wantStatus(codes ...int) resOkay { + return func(res *http.Response) bool { + for _, code := range codes { + if code == res.StatusCode { + return true + } + } + return false + } +} + +// get issues an unsigned GET request to the specified URL. +// It returns a non-error value only when ok reports true. +// +// get retries unsuccessful attempts according to c.RetryBackoff +// until the context is done or a non-retriable error is received. +func (c *Client) get(ctx context.Context, url string, ok resOkay) (*http.Response, error) { + retry := c.retryTimer() + for { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + res, err := c.doNoRetry(ctx, req) + switch { + case err != nil: + return nil, err + case ok(res): + return res, nil + case isRetriable(res.StatusCode): + retry.inc() + resErr := responseError(res) + res.Body.Close() + // Ignore the error value from retry.backoff + // and return the one from last retry, as received from the CA. + if retry.backoff(ctx, req, res) != nil { + return nil, resErr + } + default: + defer res.Body.Close() + return nil, responseError(res) + } + } +} + +// postAsGet is POST-as-GET, a replacement for GET in RFC8555 +// as described in https://tools.ietf.org/html/rfc8555#section-6.3. +// It makes a POST request in KID form with zero JWS payload. +// See nopayload doc comments in jws.go. +func (c *Client) postAsGet(ctx context.Context, url string, ok resOkay) (*http.Response, error) { + return c.post(ctx, nil, url, noPayload, ok) +} + +// post issues a signed POST request in JWS format using the provided key +// to the specified URL. If key is nil, c.Key is used instead. +// It returns a non-error value only when ok reports true. +// +// post retries unsuccessful attempts according to c.RetryBackoff +// until the context is done or a non-retriable error is received. +// It uses postNoRetry to make individual requests. +func (c *Client) post(ctx context.Context, key crypto.Signer, url string, body interface{}, ok resOkay) (*http.Response, error) { + retry := c.retryTimer() + for { + res, req, err := c.postNoRetry(ctx, key, url, body) + if err != nil { + return nil, err + } + if ok(res) { + return res, nil + } + resErr := responseError(res) + res.Body.Close() + switch { + // Check for bad nonce before isRetriable because it may have been returned + // with an unretriable response code such as 400 Bad Request. + case isBadNonce(resErr): + // Consider any previously stored nonce values to be invalid. + c.clearNonces() + case !isRetriable(res.StatusCode): + return nil, resErr + } + retry.inc() + // Ignore the error value from retry.backoff + // and return the one from last retry, as received from the CA. + if err := retry.backoff(ctx, req, res); err != nil { + return nil, resErr + } + } +} + +// postNoRetry signs the body with the given key and POSTs it to the provided url. +// It is used by c.post to retry unsuccessful attempts. +// The body argument must be JSON-serializable. +// +// If key argument is nil, c.Key is used to sign the request. +// If key argument is nil and c.accountKID returns a non-zero keyID, +// the request is sent in KID form. Otherwise, JWK form is used. +// +// In practice, when interfacing with RFC-compliant CAs most requests are sent in KID form +// and JWK is used only when KID is unavailable: new account endpoint and certificate +// revocation requests authenticated by a cert key. +// See jwsEncodeJSON for other details. +func (c *Client) postNoRetry(ctx context.Context, key crypto.Signer, url string, body interface{}) (*http.Response, *http.Request, error) { + kid := noKeyID + if key == nil { + key = c.Key + kid = c.accountKID(ctx) + } + nonce, err := c.popNonce(ctx, url) + if err != nil { + return nil, nil, err + } + b, err := jwsEncodeJSON(body, key, kid, nonce, url) + if err != nil { + return nil, nil, err + } + req, err := http.NewRequest("POST", url, bytes.NewReader(b)) + if err != nil { + return nil, nil, err + } + req.Header.Set("Content-Type", "application/jose+json") + res, err := c.doNoRetry(ctx, req) + if err != nil { + return nil, nil, err + } + c.addNonce(res.Header) + return res, req, nil +} + +// doNoRetry issues a request req, replacing its context (if any) with ctx. +func (c *Client) doNoRetry(ctx context.Context, req *http.Request) (*http.Response, error) { + req.Header.Set("User-Agent", c.userAgent()) + res, err := c.httpClient().Do(req.WithContext(ctx)) + if err != nil { + select { + case <-ctx.Done(): + // Prefer the unadorned context error. + // (The acme package had tests assuming this, previously from ctxhttp's + // behavior, predating net/http supporting contexts natively) + // TODO(bradfitz): reconsider this in the future. But for now this + // requires no test updates. + return nil, ctx.Err() + default: + return nil, err + } + } + return res, nil +} + +func (c *Client) httpClient() *http.Client { + if c.HTTPClient != nil { + return c.HTTPClient + } + return http.DefaultClient +} + +// packageVersion is the version of the module that contains this package, for +// sending as part of the User-Agent header. It's set in version_go112.go. +var packageVersion string + +// userAgent returns the User-Agent header value. It includes the package name, +// the module version (if available), and the c.UserAgent value (if set). +func (c *Client) userAgent() string { + ua := "golang.org/x/crypto/acme" + if packageVersion != "" { + ua += "@" + packageVersion + } + if c.UserAgent != "" { + ua = c.UserAgent + " " + ua + } + return ua +} + +// isBadNonce reports whether err is an ACME "badnonce" error. +func isBadNonce(err error) bool { + // According to the spec badNonce is urn:ietf:params:acme:error:badNonce. + // However, ACME servers in the wild return their versions of the error. + // See https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-5.4 + // and https://github.com/letsencrypt/boulder/blob/0e07eacb/docs/acme-divergences.md#section-66. + ae, ok := err.(*Error) + return ok && strings.HasSuffix(strings.ToLower(ae.ProblemType), ":badnonce") +} + +// isRetriable reports whether a request can be retried +// based on the response status code. +// +// Note that a "bad nonce" error is returned with a non-retriable 400 Bad Request code. +// Callers should parse the response and check with isBadNonce. +func isRetriable(code int) bool { + return code <= 399 || code >= 500 || code == http.StatusTooManyRequests +} + +// responseError creates an error of Error type from resp. +func responseError(resp *http.Response) error { + // don't care if ReadAll returns an error: + // json.Unmarshal will fail in that case anyway + b, _ := ioutil.ReadAll(resp.Body) + e := &wireError{Status: resp.StatusCode} + if err := json.Unmarshal(b, e); err != nil { + // this is not a regular error response: + // populate detail with anything we received, + // e.Status will already contain HTTP response code value + e.Detail = string(b) + if e.Detail == "" { + e.Detail = resp.Status + } + } + return e.error(resp.Header) +} diff --git a/vendor/golang.org/x/crypto/acme/jws.go b/vendor/golang.org/x/crypto/acme/jws.go new file mode 100644 index 0000000000000000000000000000000000000000..76e3fdacf1889a5528c096a9cd7a3d4182740ae5 --- /dev/null +++ b/vendor/golang.org/x/crypto/acme/jws.go @@ -0,0 +1,187 @@ +// Copyright 2015 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 ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + _ "crypto/sha512" // need for EC keys + "encoding/asn1" + "encoding/base64" + "encoding/json" + "fmt" + "math/big" +) + +// keyID is the account identity provided by a CA during registration. +type keyID string + +// noKeyID indicates that jwsEncodeJSON should compute and use JWK instead of a KID. +// See jwsEncodeJSON for details. +const noKeyID = keyID("") + +// noPayload indicates jwsEncodeJSON will encode zero-length octet string +// in a JWS request. This is called POST-as-GET in RFC 8555 and is used to make +// authenticated GET requests via POSTing with an empty payload. +// See https://tools.ietf.org/html/rfc8555#section-6.3 for more details. +const noPayload = "" + +// jwsEncodeJSON signs claimset using provided key and a nonce. +// The result is serialized in JSON format containing either kid or jwk +// fields based on the provided keyID value. +// +// If kid is non-empty, its quoted value is inserted in the protected head +// as "kid" field value. Otherwise, JWK is computed using jwkEncode and inserted +// as "jwk" field value. The "jwk" and "kid" fields are mutually exclusive. +// +// See https://tools.ietf.org/html/rfc7515#section-7. +func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid keyID, nonce, url string) ([]byte, error) { + alg, sha := jwsHasher(key.Public()) + if alg == "" || !sha.Available() { + return nil, ErrUnsupportedKey + } + var phead string + switch kid { + case noKeyID: + jwk, err := jwkEncode(key.Public()) + if err != nil { + return nil, err + } + phead = fmt.Sprintf(`{"alg":%q,"jwk":%s,"nonce":%q,"url":%q}`, alg, jwk, nonce, url) + default: + phead = fmt.Sprintf(`{"alg":%q,"kid":%q,"nonce":%q,"url":%q}`, alg, kid, nonce, url) + } + phead = base64.RawURLEncoding.EncodeToString([]byte(phead)) + var payload string + if claimset != noPayload { + cs, err := json.Marshal(claimset) + if err != nil { + return nil, err + } + payload = base64.RawURLEncoding.EncodeToString(cs) + } + hash := sha.New() + hash.Write([]byte(phead + "." + payload)) + sig, err := jwsSign(key, sha, hash.Sum(nil)) + if err != nil { + return nil, err + } + + enc := struct { + Protected string `json:"protected"` + Payload string `json:"payload"` + Sig string `json:"signature"` + }{ + Protected: phead, + Payload: payload, + Sig: base64.RawURLEncoding.EncodeToString(sig), + } + return json.Marshal(&enc) +} + +// jwkEncode encodes public part of an RSA or ECDSA key into a JWK. +// The result is also suitable for creating a JWK thumbprint. +// https://tools.ietf.org/html/rfc7517 +func jwkEncode(pub crypto.PublicKey) (string, error) { + switch pub := pub.(type) { + case *rsa.PublicKey: + // https://tools.ietf.org/html/rfc7518#section-6.3.1 + n := pub.N + e := big.NewInt(int64(pub.E)) + // Field order is important. + // See https://tools.ietf.org/html/rfc7638#section-3.3 for details. + return fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`, + base64.RawURLEncoding.EncodeToString(e.Bytes()), + base64.RawURLEncoding.EncodeToString(n.Bytes()), + ), nil + case *ecdsa.PublicKey: + // https://tools.ietf.org/html/rfc7518#section-6.2.1 + p := pub.Curve.Params() + n := p.BitSize / 8 + if p.BitSize%8 != 0 { + n++ + } + x := pub.X.Bytes() + if n > len(x) { + x = append(make([]byte, n-len(x)), x...) + } + y := pub.Y.Bytes() + if n > len(y) { + y = append(make([]byte, n-len(y)), y...) + } + // Field order is important. + // See https://tools.ietf.org/html/rfc7638#section-3.3 for details. + return fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`, + p.Name, + base64.RawURLEncoding.EncodeToString(x), + base64.RawURLEncoding.EncodeToString(y), + ), nil + } + return "", ErrUnsupportedKey +} + +// jwsSign signs the digest using the given key. +// The hash is unused for ECDSA keys. +func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) { + switch pub := key.Public().(type) { + case *rsa.PublicKey: + return key.Sign(rand.Reader, digest, hash) + case *ecdsa.PublicKey: + sigASN1, err := key.Sign(rand.Reader, digest, hash) + if err != nil { + return nil, err + } + + var rs struct{ R, S *big.Int } + if _, err := asn1.Unmarshal(sigASN1, &rs); err != nil { + return nil, err + } + + rb, sb := rs.R.Bytes(), rs.S.Bytes() + size := pub.Params().BitSize / 8 + if size%8 > 0 { + size++ + } + sig := make([]byte, size*2) + copy(sig[size-len(rb):], rb) + copy(sig[size*2-len(sb):], sb) + return sig, nil + } + return nil, ErrUnsupportedKey +} + +// jwsHasher indicates suitable JWS algorithm name and a hash function +// to use for signing a digest with the provided key. +// It returns ("", 0) if the key is not supported. +func jwsHasher(pub crypto.PublicKey) (string, crypto.Hash) { + switch pub := pub.(type) { + case *rsa.PublicKey: + return "RS256", crypto.SHA256 + case *ecdsa.PublicKey: + switch pub.Params().Name { + case "P-256": + return "ES256", crypto.SHA256 + case "P-384": + return "ES384", crypto.SHA384 + case "P-521": + return "ES512", crypto.SHA512 + } + } + return "", 0 +} + +// JWKThumbprint creates a JWK thumbprint out of pub +// as specified in https://tools.ietf.org/html/rfc7638. +func JWKThumbprint(pub crypto.PublicKey) (string, error) { + jwk, err := jwkEncode(pub) + if err != nil { + return "", err + } + b := sha256.Sum256([]byte(jwk)) + return base64.RawURLEncoding.EncodeToString(b[:]), nil +} diff --git a/vendor/golang.org/x/crypto/acme/rfc8555.go b/vendor/golang.org/x/crypto/acme/rfc8555.go new file mode 100644 index 0000000000000000000000000000000000000000..dfb57a66fd4befd4f5f7e5836012aa8167e4fa86 --- /dev/null +++ b/vendor/golang.org/x/crypto/acme/rfc8555.go @@ -0,0 +1,392 @@ +// 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" + "io/ioutil" + "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 { + 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 quivalent to c.Register but for CAs implementing RFC 8555. +// It expects c.Discover to have already been called. +// TODO: Implement externalAccountBinding. +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"` + }{ + Contact: acct.Contact, + } + if c.dir.Terms != "" { + req.TermsAgreed = prompt(c.dir.Terms) + } + 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 +} + +// updateGegRFC 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) +} + +// getGegRFC 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 +} + +// 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 := ioutil.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" +} diff --git a/vendor/golang.org/x/crypto/acme/types.go b/vendor/golang.org/x/crypto/acme/types.go new file mode 100644 index 0000000000000000000000000000000000000000..9c59097a05103d9e6ce2ce0d5a89a5d764a25f32 --- /dev/null +++ b/vendor/golang.org/x/crypto/acme/types.go @@ -0,0 +1,548 @@ +// Copyright 2016 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 ( + "crypto" + "crypto/x509" + "errors" + "fmt" + "net/http" + "strings" + "time" +) + +// ACME status values of Account, Order, Authorization and Challenge objects. +// See https://tools.ietf.org/html/rfc8555#section-7.1.6 for details. +const ( + StatusDeactivated = "deactivated" + StatusExpired = "expired" + StatusInvalid = "invalid" + StatusPending = "pending" + StatusProcessing = "processing" + StatusReady = "ready" + StatusRevoked = "revoked" + StatusUnknown = "unknown" + StatusValid = "valid" +) + +// CRLReasonCode identifies the reason for a certificate revocation. +type CRLReasonCode int + +// CRL reason codes as defined in RFC 5280. +const ( + CRLReasonUnspecified CRLReasonCode = 0 + CRLReasonKeyCompromise CRLReasonCode = 1 + CRLReasonCACompromise CRLReasonCode = 2 + CRLReasonAffiliationChanged CRLReasonCode = 3 + CRLReasonSuperseded CRLReasonCode = 4 + CRLReasonCessationOfOperation CRLReasonCode = 5 + CRLReasonCertificateHold CRLReasonCode = 6 + CRLReasonRemoveFromCRL CRLReasonCode = 8 + CRLReasonPrivilegeWithdrawn CRLReasonCode = 9 + CRLReasonAACompromise CRLReasonCode = 10 +) + +var ( + // ErrUnsupportedKey is returned when an unsupported key type is encountered. + ErrUnsupportedKey = errors.New("acme: unknown key type; only RSA and ECDSA are supported") + + // ErrAccountAlreadyExists indicates that the Client's key has already been registered + // with the CA. It is returned by Register method. + ErrAccountAlreadyExists = errors.New("acme: account already exists") + + // ErrNoAccount indicates that the Client's key has not been registered with the CA. + ErrNoAccount = errors.New("acme: account does not exist") +) + +// Error is an ACME error, defined in Problem Details for HTTP APIs doc +// http://tools.ietf.org/html/draft-ietf-appsawg-http-problem. +type Error struct { + // StatusCode is The HTTP status code generated by the origin server. + StatusCode int + // ProblemType is a URI reference that identifies the problem type, + // typically in a "urn:acme:error:xxx" form. + ProblemType string + // Detail is a human-readable explanation specific to this occurrence of the problem. + Detail string + // Instance indicates a URL that the client should direct a human user to visit + // in order for instructions on how to agree to the updated Terms of Service. + // In such an event CA sets StatusCode to 403, ProblemType to + // "urn:ietf:params:acme:error:userActionRequired" and a Link header with relation + // "terms-of-service" containing the latest TOS URL. + Instance string + // Header is the original server error response headers. + // It may be nil. + Header http.Header +} + +func (e *Error) Error() string { + return fmt.Sprintf("%d %s: %s", e.StatusCode, e.ProblemType, e.Detail) +} + +// AuthorizationError indicates that an authorization for an identifier +// did not succeed. +// It contains all errors from Challenge items of the failed Authorization. +type AuthorizationError struct { + // URI uniquely identifies the failed Authorization. + URI string + + // Identifier is an AuthzID.Value of the failed Authorization. + Identifier string + + // Errors is a collection of non-nil error values of Challenge items + // of the failed Authorization. + Errors []error +} + +func (a *AuthorizationError) Error() string { + e := make([]string, len(a.Errors)) + for i, err := range a.Errors { + e[i] = err.Error() + } + return fmt.Sprintf("acme: authorization error for %s: %s", a.Identifier, strings.Join(e, "; ")) +} + +// OrderError is returned from Client's order related methods. +// It indicates the order is unusable and the clients should start over with +// AuthorizeOrder. +// +// The clients can still fetch the order object from CA using GetOrder +// to inspect its state. +type OrderError struct { + OrderURL string + Status string +} + +func (oe *OrderError) Error() string { + return fmt.Sprintf("acme: order %s status: %s", oe.OrderURL, oe.Status) +} + +// RateLimit reports whether err represents a rate limit error and +// any Retry-After duration returned by the server. +// +// See the following for more details on rate limiting: +// https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-5.6 +func RateLimit(err error) (time.Duration, bool) { + e, ok := err.(*Error) + if !ok { + return 0, false + } + // Some CA implementations may return incorrect values. + // Use case-insensitive comparison. + if !strings.HasSuffix(strings.ToLower(e.ProblemType), ":ratelimited") { + return 0, false + } + if e.Header == nil { + return 0, true + } + return retryAfter(e.Header.Get("Retry-After")), true +} + +// Account is a user account. It is associated with a private key. +// Non-RFC 8555 fields are empty when interfacing with a compliant CA. +type Account struct { + // URI is the account unique ID, which is also a URL used to retrieve + // account data from the CA. + // When interfacing with RFC 8555-compliant CAs, URI is the "kid" field + // value in JWS signed requests. + URI string + + // Contact is a slice of contact info used during registration. + // See https://tools.ietf.org/html/rfc8555#section-7.3 for supported + // formats. + Contact []string + + // Status indicates current account status as returned by the CA. + // Possible values are StatusValid, StatusDeactivated, and StatusRevoked. + Status string + + // OrdersURL is a URL from which a list of orders submitted by this account + // can be fetched. + OrdersURL string + + // The terms user has agreed to. + // A value not matching CurrentTerms indicates that the user hasn't agreed + // to the actual Terms of Service of the CA. + // + // It is non-RFC 8555 compliant. Package users can store the ToS they agree to + // during Client's Register call in the prompt callback function. + AgreedTerms string + + // Actual terms of a CA. + // + // It is non-RFC 8555 compliant. Use Directory's Terms field. + // When a CA updates their terms and requires an account agreement, + // a URL at which instructions to do so is available in Error's Instance field. + CurrentTerms string + + // Authz is the authorization URL used to initiate a new authz flow. + // + // It is non-RFC 8555 compliant. Use Directory's AuthzURL or OrderURL. + Authz string + + // Authorizations is a URI from which a list of authorizations + // granted to this account can be fetched via a GET request. + // + // It is non-RFC 8555 compliant and is obsoleted by OrdersURL. + Authorizations string + + // Certificates is a URI from which a list of certificates + // issued for this account can be fetched via a GET request. + // + // It is non-RFC 8555 compliant and is obsoleted by OrdersURL. + Certificates string +} + +// Directory is ACME server discovery data. +// See https://tools.ietf.org/html/rfc8555#section-7.1.1 for more details. +type Directory struct { + // NonceURL indicates an endpoint where to fetch fresh nonce values from. + NonceURL string + + // RegURL is an account endpoint URL, allowing for creating new accounts. + // Pre-RFC 8555 CAs also allow modifying existing accounts at this URL. + RegURL string + + // OrderURL is used to initiate the certificate issuance flow + // as described in RFC 8555. + OrderURL string + + // AuthzURL is used to initiate identifier pre-authorization flow. + // Empty string indicates the flow is unsupported by the CA. + AuthzURL string + + // CertURL is a new certificate issuance endpoint URL. + // It is non-RFC 8555 compliant and is obsoleted by OrderURL. + CertURL string + + // RevokeURL is used to initiate a certificate revocation flow. + RevokeURL string + + // KeyChangeURL allows to perform account key rollover flow. + KeyChangeURL string + + // Term is a URI identifying the current terms of service. + Terms string + + // Website is an HTTP or HTTPS URL locating a website + // providing more information about the ACME server. + Website string + + // CAA consists of lowercase hostname elements, which the ACME server + // recognises as referring to itself for the purposes of CAA record validation + // as defined in RFC6844. + CAA []string + + // ExternalAccountRequired indicates that the CA requires for all account-related + // requests to include external account binding information. + ExternalAccountRequired bool +} + +// rfcCompliant reports whether the ACME server implements RFC 8555. +// Note that some servers may have incomplete RFC implementation +// even if the returned value is true. +// If rfcCompliant reports false, the server most likely implements draft-02. +func (d *Directory) rfcCompliant() bool { + return d.OrderURL != "" +} + +// Order represents a client's request for a certificate. +// It tracks the request flow progress through to issuance. +type Order struct { + // URI uniquely identifies an order. + URI string + + // Status represents the current status of the order. + // It indicates which action the client should take. + // + // Possible values are StatusPending, StatusReady, StatusProcessing, StatusValid and StatusInvalid. + // Pending means the CA does not believe that the client has fulfilled the requirements. + // Ready indicates that the client has fulfilled all the requirements and can submit a CSR + // to obtain a certificate. This is done with Client's CreateOrderCert. + // Processing means the certificate is being issued. + // Valid indicates the CA has issued the certificate. It can be downloaded + // from the Order's CertURL. This is done with Client's FetchCert. + // Invalid means the certificate will not be issued. Users should consider this order + // abandoned. + Status string + + // Expires is the timestamp after which CA considers this order invalid. + Expires time.Time + + // Identifiers contains all identifier objects which the order pertains to. + Identifiers []AuthzID + + // NotBefore is the requested value of the notBefore field in the certificate. + NotBefore time.Time + + // NotAfter is the requested value of the notAfter field in the certificate. + NotAfter time.Time + + // AuthzURLs represents authorizations to complete before a certificate + // for identifiers specified in the order can be issued. + // It also contains unexpired authorizations that the client has completed + // in the past. + // + // Authorization objects can be fetched using Client's GetAuthorization method. + // + // The required authorizations are dictated by CA policies. + // There may not be a 1:1 relationship between the identifiers and required authorizations. + // Required authorizations can be identified by their StatusPending status. + // + // For orders in the StatusValid or StatusInvalid state these are the authorizations + // which were completed. + AuthzURLs []string + + // FinalizeURL is the endpoint at which a CSR is submitted to obtain a certificate + // once all the authorizations are satisfied. + FinalizeURL string + + // CertURL points to the certificate that has been issued in response to this order. + CertURL string + + // The error that occurred while processing the order as received from a CA, if any. + Error *Error +} + +// OrderOption allows customizing Client.AuthorizeOrder call. +type OrderOption interface { + privateOrderOpt() +} + +// WithOrderNotBefore sets order's NotBefore field. +func WithOrderNotBefore(t time.Time) OrderOption { + return orderNotBeforeOpt(t) +} + +// WithOrderNotAfter sets order's NotAfter field. +func WithOrderNotAfter(t time.Time) OrderOption { + return orderNotAfterOpt(t) +} + +type orderNotBeforeOpt time.Time + +func (orderNotBeforeOpt) privateOrderOpt() {} + +type orderNotAfterOpt time.Time + +func (orderNotAfterOpt) privateOrderOpt() {} + +// Authorization encodes an authorization response. +type Authorization struct { + // URI uniquely identifies a authorization. + URI string + + // Status is the current status of an authorization. + // Possible values are StatusPending, StatusValid, StatusInvalid, StatusDeactivated, + // StatusExpired and StatusRevoked. + Status string + + // Identifier is what the account is authorized to represent. + Identifier AuthzID + + // The timestamp after which the CA considers the authorization invalid. + Expires time.Time + + // Wildcard is true for authorizations of a wildcard domain name. + Wildcard bool + + // Challenges that the client needs to fulfill in order to prove possession + // of the identifier (for pending authorizations). + // For valid authorizations, the challenge that was validated. + // For invalid authorizations, the challenge that was attempted and failed. + // + // RFC 8555 compatible CAs require users to fuflfill only one of the challenges. + Challenges []*Challenge + + // A collection of sets of challenges, each of which would be sufficient + // to prove possession of the identifier. + // Clients must complete a set of challenges that covers at least one set. + // Challenges are identified by their indices in the challenges array. + // If this field is empty, the client needs to complete all challenges. + // + // This field is unused in RFC 8555. + Combinations [][]int +} + +// AuthzID is an identifier that an account is authorized to represent. +type AuthzID struct { + Type string // The type of identifier, "dns" or "ip". + Value string // The identifier itself, e.g. "example.org". +} + +// DomainIDs creates a slice of AuthzID with "dns" identifier type. +func DomainIDs(names ...string) []AuthzID { + a := make([]AuthzID, len(names)) + for i, v := range names { + a[i] = AuthzID{Type: "dns", Value: v} + } + return a +} + +// IPIDs creates a slice of AuthzID with "ip" identifier type. +// Each element of addr is textual form of an address as defined +// in RFC1123 Section 2.1 for IPv4 and in RFC5952 Section 4 for IPv6. +func IPIDs(addr ...string) []AuthzID { + a := make([]AuthzID, len(addr)) + for i, v := range addr { + a[i] = AuthzID{Type: "ip", Value: v} + } + return a +} + +// wireAuthzID is ACME JSON representation of authorization identifier objects. +type wireAuthzID struct { + Type string `json:"type"` + Value string `json:"value"` +} + +// wireAuthz is ACME JSON representation of Authorization objects. +type wireAuthz struct { + Identifier wireAuthzID + Status string + Expires time.Time + Wildcard bool + Challenges []wireChallenge + Combinations [][]int +} + +func (z *wireAuthz) authorization(uri string) *Authorization { + a := &Authorization{ + URI: uri, + Status: z.Status, + Identifier: AuthzID{Type: z.Identifier.Type, Value: z.Identifier.Value}, + Expires: z.Expires, + Wildcard: z.Wildcard, + Challenges: make([]*Challenge, len(z.Challenges)), + Combinations: z.Combinations, // shallow copy + } + for i, v := range z.Challenges { + a.Challenges[i] = v.challenge() + } + return a +} + +func (z *wireAuthz) error(uri string) *AuthorizationError { + err := &AuthorizationError{ + URI: uri, + Identifier: z.Identifier.Value, + } + for _, raw := range z.Challenges { + if raw.Error != nil { + err.Errors = append(err.Errors, raw.Error.error(nil)) + } + } + return err +} + +// Challenge encodes a returned CA challenge. +// Its Error field may be non-nil if the challenge is part of an Authorization +// with StatusInvalid. +type Challenge struct { + // Type is the challenge type, e.g. "http-01", "tls-alpn-01", "dns-01". + Type string + + // URI is where a challenge response can be posted to. + URI string + + // Token is a random value that uniquely identifies the challenge. + Token string + + // Status identifies the status of this challenge. + // In RFC 8555, possible values are StatusPending, StatusProcessing, StatusValid, + // and StatusInvalid. + Status string + + // Validated is the time at which the CA validated this challenge. + // Always zero value in pre-RFC 8555. + Validated time.Time + + // Error indicates the reason for an authorization failure + // when this challenge was used. + // The type of a non-nil value is *Error. + Error error +} + +// wireChallenge is ACME JSON challenge representation. +type wireChallenge struct { + URL string `json:"url"` // RFC + URI string `json:"uri"` // pre-RFC + Type string + Token string + Status string + Validated time.Time + Error *wireError +} + +func (c *wireChallenge) challenge() *Challenge { + v := &Challenge{ + URI: c.URL, + Type: c.Type, + Token: c.Token, + Status: c.Status, + } + if v.URI == "" { + v.URI = c.URI // c.URL was empty; use legacy + } + if v.Status == "" { + v.Status = StatusPending + } + if c.Error != nil { + v.Error = c.Error.error(nil) + } + return v +} + +// wireError is a subset of fields of the Problem Details object +// as described in https://tools.ietf.org/html/rfc7807#section-3.1. +type wireError struct { + Status int + Type string + Detail string + Instance string +} + +func (e *wireError) error(h http.Header) *Error { + return &Error{ + StatusCode: e.Status, + ProblemType: e.Type, + Detail: e.Detail, + Instance: e.Instance, + Header: h, + } +} + +// CertOption is an optional argument type for the TLS ChallengeCert methods for +// customizing a temporary certificate for TLS-based challenges. +type CertOption interface { + privateCertOpt() +} + +// WithKey creates an option holding a private/public key pair. +// The private part signs a certificate, and the public part represents the signee. +func WithKey(key crypto.Signer) CertOption { + return &certOptKey{key} +} + +type certOptKey struct { + key crypto.Signer +} + +func (*certOptKey) privateCertOpt() {} + +// WithTemplate creates an option for specifying a certificate template. +// See x509.CreateCertificate for template usage details. +// +// In TLS ChallengeCert methods, the template is also used as parent, +// resulting in a self-signed certificate. +// The DNSNames field of t is always overwritten for tls-sni challenge certs. +func WithTemplate(t *x509.Certificate) CertOption { + return (*certOptTemplate)(t) +} + +type certOptTemplate x509.Certificate + +func (*certOptTemplate) privateCertOpt() {} diff --git a/vendor/golang.org/x/crypto/acme/version_go112.go b/vendor/golang.org/x/crypto/acme/version_go112.go new file mode 100644 index 0000000000000000000000000000000000000000..b58f2456bea89071111d3fbb87902aaaa31d4e64 --- /dev/null +++ b/vendor/golang.org/x/crypto/acme/version_go112.go @@ -0,0 +1,27 @@ +// 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. + +// +build go1.12 + +package acme + +import "runtime/debug" + +func init() { + // Set packageVersion if the binary was built in modules mode and x/crypto + // was not replaced with a different module. + info, ok := debug.ReadBuildInfo() + if !ok { + return + } + for _, m := range info.Deps { + if m.Path != "golang.org/x/crypto" { + continue + } + if m.Replace == nil { + packageVersion = m.Version + } + break + } +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 094c4ba32271302954084f9f5e8982afb3abe698..2b80b50049768d64e9e1fb9ab2fc51a1a4a709f6 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -823,6 +823,12 @@ "revision": "a621d807f061e1dd635033a8d6bc261461429e27", "revisionTime": "2019-04-01T20:57:24Z" }, + { + "checksumSHA1": "Cx9g8+7oSWclS/irgksPsNmp1uQ=", + "path": "golang.org/x/crypto/acme", + "revision": "ecb85df213405b7d32e4d73cb5bbaace2ec88881", + "revisionTime": "2020-01-18T22:47:15Z" + }, { "checksumSHA1": "oCH3J96RWvO8W4xjix47PModpio=", "origin": "go.etcd.io/etcd/vendor/golang.org/x/crypto/bcrypt",