| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435 |
- package acme
- // Similar to golang.org/x/crypto/acme/autocert
- import (
- "context"
- "crypto/ecdsa"
- "crypto/elliptic"
- "crypto/rand"
- "crypto/tls"
- "crypto/x509"
- "crypto/x509/pkix"
- "encoding/pem"
- "errors"
- "fmt"
- "io/ioutil"
- "net/http"
- "path"
- "strings"
- "sync"
- )
- // HostCheck function prototype to implement for checking hosts against before issuing certificates
- type HostCheck func(host string) error
- // WhitelistHosts implements a simple whitelist HostCheck
- func WhitelistHosts(hosts ...string) HostCheck {
- m := map[string]bool{}
- for _, v := range hosts {
- m[v] = true
- }
- return func(host string) error {
- if !m[host] {
- return errors.New("autocert: host not whitelisted")
- }
- return nil
- }
- }
- // AutoCert is a stateful certificate manager for issuing certificates on connecting hosts
- type AutoCert struct {
- // Acme directory Url
- // If nil, uses `LetsEncryptStaging`
- DirectoryURL string
- // Options contains the options used for creating the acme client
- Options []OptionFunc
- // A function to check whether a host is allowed or not
- // If nil, all hosts allowed
- // Use `WhitelistHosts(hosts ...string)` for a simple white list of hostnames
- HostCheck HostCheck
- // Cache dir to store account data and certificates
- // If nil, does not write cache data to file
- CacheDir string
- // When using a staging environment, include a root certificate for verification purposes
- RootCert string
- // Called before updating challenges
- PreUpdateChallengeHook func(Account, Challenge)
- // Mapping of token -> keyauth
- // Protected by a mutex, but not rwmutex because tokens are deleted once read
- tokensLock sync.RWMutex
- tokens map[string][]byte
- // Mapping of cache key -> value
- cacheLock sync.Mutex
- cache map[string][]byte
- // read lock around getting existing certs
- // write lock around issuing new certificate
- certLock sync.RWMutex
- client Client
- }
- // HTTPHandler Wraps a handler and provides serving of http-01 challenge tokens from /.well-known/acme-challenge/
- // If handler is nil, will redirect all traffic otherwise to https
- func (m *AutoCert) HTTPHandler(handler http.Handler) http.Handler {
- if handler == nil {
- handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- http.Redirect(w, r, "https://"+r.Host+r.URL.RequestURI(), http.StatusMovedPermanently)
- })
- }
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if !strings.HasPrefix(r.URL.Path, "/.well-known/acme-challenge/") {
- handler.ServeHTTP(w, r)
- return
- }
- if err := m.checkHost(r.Host); err != nil {
- http.Error(w, err.Error(), http.StatusForbidden)
- return
- }
- token := path.Base(r.URL.Path)
- m.tokensLock.RLock()
- defer m.tokensLock.RUnlock()
- keyAuth := m.tokens[token]
- if len(keyAuth) == 0 {
- http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
- return
- }
- _, _ = w.Write(keyAuth)
- })
- }
- // GetCertificate implements a tls.Config.GetCertificate hook
- func (m *AutoCert) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
- name := strings.TrimSuffix(hello.ServerName, ".")
- if name == "" {
- return nil, errors.New("autocert: missing server name")
- }
- if !strings.Contains(strings.Trim(name, "."), ".") {
- return nil, errors.New("autocert: server name component count invalid")
- }
- if strings.ContainsAny(name, `/\`) {
- return nil, errors.New("autocert: server name contains invalid character")
- }
- // check the hostname is allowed
- if err := m.checkHost(name); err != nil {
- return nil, err
- }
- // check if there's an existing cert
- m.certLock.RLock()
- existingCert, _ := m.getExistingCert(name)
- m.certLock.RUnlock()
- if existingCert != nil {
- return existingCert, nil
- }
- // if not, attempt to issue a new cert
- m.certLock.Lock()
- defer m.certLock.Unlock()
- return m.issueCert(name)
- }
- func (m *AutoCert) getDirectoryURL() string {
- if m.DirectoryURL != "" {
- return m.DirectoryURL
- }
- return LetsEncryptStaging
- }
- func (m *AutoCert) getCache(keys ...string) []byte {
- key := strings.Join(keys, "-")
- m.cacheLock.Lock()
- defer m.cacheLock.Unlock()
- b := m.cache[key]
- if len(b) > 0 {
- return b
- }
- if m.CacheDir == "" {
- return nil
- }
- b, _ = ioutil.ReadFile(path.Join(m.CacheDir, key))
- if len(b) == 0 {
- return nil
- }
- if m.cache == nil {
- m.cache = map[string][]byte{}
- }
- m.cache[key] = b
- return b
- }
- func (m *AutoCert) putCache(data []byte, keys ...string) context.Context {
- ctx, cancel := context.WithCancel(context.Background())
- key := strings.Join(keys, "-")
- m.cacheLock.Lock()
- defer m.cacheLock.Unlock()
- if m.cache == nil {
- m.cache = map[string][]byte{}
- }
- m.cache[key] = data
- if m.CacheDir == "" {
- cancel()
- return ctx
- }
- go func() {
- _ = ioutil.WriteFile(path.Join(m.CacheDir, key), data, 0700)
- cancel()
- }()
- return ctx
- }
- func (m *AutoCert) checkHost(name string) error {
- if m.HostCheck == nil {
- return nil
- }
- return m.HostCheck(name)
- }
- func (m *AutoCert) getExistingCert(name string) (*tls.Certificate, error) {
- // check for a stored cert
- certData := m.getCache("cert", name)
- if len(certData) == 0 {
- return nil, errors.New("autocert: no existing certificate")
- }
- privBlock, pubData := pem.Decode(certData)
- if len(pubData) == 0 {
- return nil, errors.New("autocert: no public key data (cert/issuer)")
- }
- // decode pub chain
- var pubDER [][]byte
- var pub []byte
- for len(pubData) > 0 {
- var b *pem.Block
- b, pubData = pem.Decode(pubData)
- if b == nil {
- break
- }
- pubDER = append(pubDER, b.Bytes)
- pub = append(pub, b.Bytes...)
- }
- if len(pubData) > 0 {
- return nil, errors.New("autocert: leftover data in file - possibly corrupt")
- }
- certs, err := x509.ParseCertificates(pub)
- if err != nil {
- return nil, fmt.Errorf("autocert: bad certificate: %v", err)
- }
- leaf := certs[0]
- // add any intermediate certs if present
- var intermediates *x509.CertPool
- if len(certs) > 1 {
- intermediates = x509.NewCertPool()
- for i := 1; i < len(certs); i++ {
- intermediates.AddCert(certs[i])
- }
- }
- // add a root certificate if present
- var roots *x509.CertPool
- if m.RootCert != "" {
- block, rest := pem.Decode([]byte(m.RootCert))
- for block != nil {
- rootCert, err := x509.ParseCertificate(block.Bytes)
- if err != nil {
- return nil, errors.New("autocert: error parsing root certificate")
- }
- if roots == nil {
- roots = x509.NewCertPool()
- }
- roots.AddCert(rootCert)
- block, rest = pem.Decode(rest)
- }
- }
- opts := x509.VerifyOptions{
- DNSName: name,
- Intermediates: intermediates,
- Roots: roots,
- }
- if _, err := leaf.Verify(opts); err != nil {
- return nil, fmt.Errorf("autocert: unable to verify: %v", err)
- }
- privKey, err := x509.ParseECPrivateKey(privBlock.Bytes)
- if err != nil {
- return nil, errors.New("autocert: invalid private key")
- }
- return &tls.Certificate{
- Certificate: pubDER,
- PrivateKey: privKey,
- Leaf: leaf,
- }, nil
- }
- func (m *AutoCert) issueCert(domainName string) (*tls.Certificate, error) {
- // attempt to load an existing account key
- var privKey *ecdsa.PrivateKey
- if keyData := m.getCache("account"); len(keyData) > 0 {
- block, _ := pem.Decode(keyData)
- x509Encoded := block.Bytes
- privKey, _ = x509.ParseECPrivateKey(x509Encoded)
- }
- // otherwise generate a new one
- if privKey == nil {
- var err error
- privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
- if err != nil {
- return nil, fmt.Errorf("autocert: error generating new account key: %v", err)
- }
- x509Encoded, _ := x509.MarshalECPrivateKey(privKey)
- pemEncoded := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: x509Encoded})
- m.putCache(pemEncoded, "account")
- }
- // create a new client if one doesn't exist
- if m.client.Directory().URL == "" {
- var err error
- m.client, err = NewClient(m.getDirectoryURL(), m.Options...)
- if err != nil {
- return nil, err
- }
- }
- // create/fetch acme account
- account, err := m.client.NewAccount(privKey, false, true)
- if err != nil {
- return nil, fmt.Errorf("autocert: error creating/fetching account: %v", err)
- }
- // start a new order process
- order, err := m.client.NewOrderDomains(account, domainName)
- if err != nil {
- return nil, fmt.Errorf("autocert: error creating new order for domain %s: %v", domainName, err)
- }
- // loop through each of the provided authorization Urls
- for _, authURL := range order.Authorizations {
- auth, err := m.client.FetchAuthorization(account, authURL)
- if err != nil {
- return nil, fmt.Errorf("autocert: error fetching authorization Url %q: %v", authURL, err)
- }
- if auth.Status == "valid" {
- continue
- }
- chal, ok := auth.ChallengeMap[ChallengeTypeHTTP01]
- if !ok {
- return nil, fmt.Errorf("autocert: unable to find http-01 challenge for auth %s, Url: %s", auth.Identifier.Value, authURL)
- }
- m.tokensLock.Lock()
- if m.tokens == nil {
- m.tokens = map[string][]byte{}
- }
- m.tokens[chal.Token] = []byte(chal.KeyAuthorization)
- m.tokensLock.Unlock()
- if m.PreUpdateChallengeHook != nil {
- m.PreUpdateChallengeHook(account, chal)
- }
- chal, err = m.client.UpdateChallenge(account, chal)
- if err != nil {
- return nil, fmt.Errorf("autocert: error updating authorization %s challenge (Url: %s) : %v", auth.Identifier.Value, authURL, err)
- }
- m.tokensLock.Lock()
- delete(m.tokens, chal.Token)
- m.tokensLock.Unlock()
- }
- // generate private key for cert
- certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
- if err != nil {
- return nil, fmt.Errorf("autocert: error generating certificate key for %s: %v", domainName, err)
- }
- certKeyEnc, err := x509.MarshalECPrivateKey(certKey)
- if err != nil {
- return nil, fmt.Errorf("autocert: error encoding certificate key for %s: %v", domainName, err)
- }
- certKeyPem := pem.EncodeToMemory(&pem.Block{
- Type: "EC PRIVATE KEY",
- Bytes: certKeyEnc,
- })
- // create the new csr template
- tpl := &x509.CertificateRequest{
- SignatureAlgorithm: x509.ECDSAWithSHA256,
- PublicKeyAlgorithm: x509.ECDSA,
- PublicKey: certKey.Public(),
- Subject: pkix.Name{CommonName: domainName},
- DNSNames: []string{domainName},
- }
- csrDer, err := x509.CreateCertificateRequest(rand.Reader, tpl, certKey)
- if err != nil {
- return nil, fmt.Errorf("autocert: error creating certificate request for %s: %v", domainName, err)
- }
- csr, err := x509.ParseCertificateRequest(csrDer)
- if err != nil {
- return nil, fmt.Errorf("autocert: error parsing certificate request for %s: %v", domainName, err)
- }
- // finalize the order with the acme server given a csr
- order, err = m.client.FinalizeOrder(account, order, csr)
- if err != nil {
- return nil, fmt.Errorf("autocert: error finalizing order for %s: %v", domainName, err)
- }
- // fetch the certificate chain from the finalized order provided by the acme server
- certs, err := m.client.FetchCertificates(account, order.Certificate)
- if err != nil {
- return nil, fmt.Errorf("autocert: error fetching order certificates for %s: %v", domainName, err)
- }
- certPem := certKeyPem
- // var certDer [][]byte
- for _, c := range certs {
- b := pem.EncodeToMemory(&pem.Block{
- Type: "CERTIFICATE",
- Bytes: c.Raw,
- })
- certPem = append(certPem, b...)
- // certDer = append(certDer, c.Raw)
- }
- m.putCache(certPem, "cert", domainName)
- return m.getExistingCert(domainName)
- }
|