mirror of https://github.com/k3s-io/k3s
Update dynamiclistener
parent
2d32337334
commit
25e9cfb0cc
|
@ -225,7 +225,8 @@ import:
|
||||||
- package: github.com/prometheus/procfs
|
- package: github.com/prometheus/procfs
|
||||||
version: 65c1f6f8f0fc1e2185eb9863a3bc751496404259
|
version: 65c1f6f8f0fc1e2185eb9863a3bc751496404259
|
||||||
- package: github.com/rancher/dynamiclistener
|
- package: github.com/rancher/dynamiclistener
|
||||||
version: 4716ac2362986f28bede3f3caf5d1ce347da55b0
|
version: c08b499d17195fbc2c1764b21c322951811629a5
|
||||||
|
repo: https://github.com/erikwilson/rancher-dynamiclistener.git
|
||||||
- package: github.com/rancher/helm-controller
|
- package: github.com/rancher/helm-controller
|
||||||
version: v0.2.1
|
version: v0.2.1
|
||||||
- package: github.com/rancher/remotedialer
|
- package: github.com/rancher/remotedialer
|
||||||
|
|
|
@ -13,7 +13,7 @@ k8s.io/kubernetes v1.14.4-k3s.1 ht
|
||||||
|
|
||||||
github.com/rancher/wrangler 7737c167e16514a38229bc64c839cee8cd14e6d3
|
github.com/rancher/wrangler 7737c167e16514a38229bc64c839cee8cd14e6d3
|
||||||
github.com/rancher/wrangler-api v0.1.4
|
github.com/rancher/wrangler-api v0.1.4
|
||||||
github.com/rancher/dynamiclistener 4716ac2362986f28bede3f3caf5d1ce347da55b0
|
github.com/rancher/dynamiclistener c08b499d17195fbc2c1764b21c322951811629a5 https://github.com/erikwilson/rancher-dynamiclistener.git
|
||||||
github.com/rancher/remotedialer 4a5a661be67697d6369df54ef62d5a30b0385697
|
github.com/rancher/remotedialer 4a5a661be67697d6369df54ef62d5a30b0385697
|
||||||
github.com/rancher/helm-controller v0.2.1
|
github.com/rancher/helm-controller v0.2.1
|
||||||
github.com/matryer/moq ee5226d43009 https://github.com/rancher/moq.git
|
github.com/matryer/moq ee5226d43009 https://github.com/rancher/moq.git
|
||||||
|
|
|
@ -26,11 +26,6 @@ func ReadTLSConfig(userConfig *UserConfig) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
userConfig.Mode = "https"
|
|
||||||
if len(userConfig.Domains) > 0 {
|
|
||||||
userConfig.Mode = "acme"
|
|
||||||
}
|
|
||||||
|
|
||||||
valid := false
|
valid := false
|
||||||
if userConfig.Key != "" && userConfig.Cert != "" {
|
if userConfig.Key != "" && userConfig.Cert != "" {
|
||||||
valid = true
|
valid = true
|
||||||
|
|
|
@ -1,37 +1,27 @@
|
||||||
package dynamiclistener
|
package dynamiclistener
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/md5"
|
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
lru "github.com/hashicorp/golang-lru"
|
|
||||||
cert "github.com/rancher/dynamiclistener/cert"
|
cert "github.com/rancher/dynamiclistener/cert"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/crypto/acme/autocert"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
httpsMode = "https"
|
|
||||||
acmeMode = "acme"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type server struct {
|
type server struct {
|
||||||
|
@ -39,28 +29,28 @@ type server struct {
|
||||||
|
|
||||||
userConfig UserConfig
|
userConfig UserConfig
|
||||||
listenConfigStorage ListenerConfigStorage
|
listenConfigStorage ListenerConfigStorage
|
||||||
certs map[string]*tls.Certificate
|
tlsCert *tls.Certificate
|
||||||
ips *lru.Cache
|
|
||||||
|
ips map[string]bool
|
||||||
|
domains map[string]bool
|
||||||
|
cn string
|
||||||
|
|
||||||
listeners []net.Listener
|
listeners []net.Listener
|
||||||
servers []*http.Server
|
servers []*http.Server
|
||||||
|
|
||||||
// dynamic config change on refresh
|
activeCA *x509.Certificate
|
||||||
activeCert *tls.Certificate
|
activeCAKey crypto.Signer
|
||||||
activeCA *x509.Certificate
|
|
||||||
activeCAKey crypto.Signer
|
|
||||||
activeCAKeyString string
|
|
||||||
domains map[string]bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(listenConfigStorage ListenerConfigStorage, config UserConfig) (ServerInterface, error) {
|
func NewServer(listenConfigStorage ListenerConfigStorage, config UserConfig) (ServerInterface, error) {
|
||||||
s := &server{
|
s := &server{
|
||||||
userConfig: config,
|
userConfig: config,
|
||||||
listenConfigStorage: listenConfigStorage,
|
listenConfigStorage: listenConfigStorage,
|
||||||
certs: map[string]*tls.Certificate{},
|
cn: "cattle",
|
||||||
}
|
}
|
||||||
|
|
||||||
s.ips, _ = lru.New(20)
|
s.ips = map[string]bool{}
|
||||||
|
s.domains = map[string]bool{}
|
||||||
|
|
||||||
if err := s.userConfigure(); err != nil {
|
if err := s.userConfigure(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -81,16 +71,7 @@ func (s *server) CACert() (string, error) {
|
||||||
if s.userConfig.CACerts != "" {
|
if s.userConfig.CACerts != "" {
|
||||||
return s.userConfig.CACerts, nil
|
return s.userConfig.CACerts, nil
|
||||||
}
|
}
|
||||||
status, err := s.listenConfigStorage.Get()
|
return "", fmt.Errorf("ca cert not found")
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if status.CACert == "" {
|
|
||||||
return "", fmt.Errorf("ca cert not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
return status.CACert, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func marshalPrivateKey(privateKey crypto.Signer) (string, []byte, error) {
|
func marshalPrivateKey(privateKey crypto.Signer) (string, []byte, error) {
|
||||||
|
@ -127,78 +108,25 @@ func newPrivateKey() (crypto.Signer, error) {
|
||||||
return caKeyIFace.(crypto.Signer), nil
|
return caKeyIFace.(crypto.Signer), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) save() {
|
func (s *server) save() (_err error) {
|
||||||
if s.activeCert != nil {
|
defer func() {
|
||||||
return
|
if _err != nil {
|
||||||
|
logrus.Errorf("Saving cert error: %s", _err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
certStr, err := certToString(s.tlsCert)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Lock()
|
|
||||||
defer s.Unlock()
|
|
||||||
|
|
||||||
changed := false
|
|
||||||
cfg, err := s.listenConfigStorage.Get()
|
cfg, err := s.listenConfigStorage.Get()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
cfg.GeneratedCerts = map[string]string{s.cn: certStr}
|
||||||
|
|
||||||
if cfg.GeneratedCerts == nil {
|
_, err = s.listenConfigStorage.Set(cfg)
|
||||||
cfg.GeneratedCerts = map[string]string{}
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.KnownIPs == nil {
|
|
||||||
cfg.KnownIPs = map[string]bool{}
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, cert := range s.certs {
|
|
||||||
certStr, err := certToString(cert)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if cfg.GeneratedCerts[key] != certStr {
|
|
||||||
cfg.GeneratedCerts[key] = certStr
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, obj := range s.ips.Keys() {
|
|
||||||
ip, _ := obj.(string)
|
|
||||||
if !cfg.KnownIPs[ip] {
|
|
||||||
cfg.KnownIPs[ip] = true
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.CAKey == "" && s.activeCAKey != nil && s.activeCA != nil {
|
|
||||||
caCertBuffer := bytes.Buffer{}
|
|
||||||
if err := pem.Encode(&caCertBuffer, &pem.Block{
|
|
||||||
Type: cert.CertificateBlockType,
|
|
||||||
Bytes: s.activeCA.Raw,
|
|
||||||
}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
caKeyBuffer := bytes.Buffer{}
|
|
||||||
keyType, keyBytes, err := marshalPrivateKey(s.activeCAKey)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := pem.Encode(&caKeyBuffer, &pem.Block{
|
|
||||||
Type: keyType,
|
|
||||||
Bytes: keyBytes,
|
|
||||||
}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.CACert = string(caCertBuffer.Bytes())
|
|
||||||
cfg.CAKey = string(caKeyBuffer.Bytes())
|
|
||||||
s.activeCAKeyString = cfg.CAKey
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if changed {
|
|
||||||
s.listenConfigStorage.Set(cfg)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) userConfigure() error {
|
func (s *server) userConfigure() error {
|
||||||
|
@ -206,39 +134,42 @@ func (s *server) userConfigure() error {
|
||||||
s.userConfig.HTTPSPort = 8443
|
s.userConfig.HTTPSPort = 8443
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.userConfig.Mode == "" {
|
|
||||||
if len(s.userConfig.Domains) > 0 {
|
|
||||||
s.userConfig.Mode = acmeMode
|
|
||||||
} else {
|
|
||||||
s.userConfig.Mode = httpsMode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.domains = map[string]bool{}
|
|
||||||
for _, d := range s.userConfig.Domains {
|
for _, d := range s.userConfig.Domains {
|
||||||
s.domains[d] = true
|
s.domains[d] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.userConfig.Key != "" && s.userConfig.Cert != "" {
|
for _, ip := range s.userConfig.KnownIPs {
|
||||||
cert, err := tls.X509KeyPair([]byte(s.userConfig.Cert), []byte(s.userConfig.Key))
|
if netIP := net.ParseIP(ip); netIP != nil {
|
||||||
if err != nil {
|
s.ips[ip] = true
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
s.activeCert = &cert
|
|
||||||
s.userConfig.Mode = httpsMode
|
|
||||||
return s.reload()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ip := range s.userConfig.KnownIPs {
|
if bindAddress := net.ParseIP(s.userConfig.BindAddress); bindAddress != nil {
|
||||||
netIP := net.ParseIP(ip)
|
s.ips[s.userConfig.BindAddress] = true
|
||||||
if netIP != nil {
|
}
|
||||||
s.ips.Add(ip, netIP)
|
|
||||||
|
if s.activeCA == nil && s.activeCAKey == nil {
|
||||||
|
if s.userConfig.CACerts != "" && s.userConfig.CAKey != "" {
|
||||||
|
ca, err := cert.ParseCertsPEM([]byte(s.userConfig.CACerts))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key, err := cert.ParsePrivateKeyPEM([]byte(s.userConfig.CAKey))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.activeCA = ca[0]
|
||||||
|
s.activeCAKey = key.(crypto.Signer)
|
||||||
|
} else {
|
||||||
|
ca, key, err := genCA()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.activeCA = ca
|
||||||
|
s.activeCAKey = key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bindAddress := net.ParseIP(s.userConfig.BindAddress)
|
|
||||||
if bindAddress != nil {
|
|
||||||
s.ips.Add(s.userConfig.BindAddress, bindAddress)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,61 +190,76 @@ func genCA() (*x509.Certificate, crypto.Signer, error) {
|
||||||
return caCert, caKey, nil
|
return caCert, caKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) Update(status *ListenerStatus) error {
|
func (s *server) Update(status *ListenerStatus) (_err error) {
|
||||||
s.Lock()
|
s.Lock()
|
||||||
defer s.getCertificate(&tls.ClientHelloInfo{ServerName: "localhost"})
|
defer func() {
|
||||||
|
s.Unlock()
|
||||||
if status.CACert != "" && status.CAKey != "" && s.activeCAKeyString != status.CAKey {
|
if _err != nil {
|
||||||
cert, err := tls.X509KeyPair([]byte(status.CACert), []byte(status.CAKey))
|
logrus.Errorf("Update cert error: %s", _err)
|
||||||
if err != nil {
|
|
||||||
s.Unlock()
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
s.activeCAKey = cert.PrivateKey.(crypto.Signer)
|
if s.tlsCert == nil {
|
||||||
s.activeCAKeyString = status.CAKey
|
s.getCertificate(&tls.ClientHelloInfo{ServerName: "localhost"})
|
||||||
|
|
||||||
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
|
|
||||||
if err != nil {
|
|
||||||
s.Unlock()
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
s.activeCA = x509Cert
|
}()
|
||||||
s.certs = map[string]*tls.Certificate{}
|
|
||||||
|
certString := status.GeneratedCerts[s.cn]
|
||||||
|
tlsCert, err := stringToCert(certString)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Update cert unable to convert string to cert: %s", err)
|
||||||
|
s.tlsCert = nil
|
||||||
}
|
}
|
||||||
|
if tlsCert != nil {
|
||||||
|
s.tlsCert = tlsCert
|
||||||
|
for i, certBytes := range tlsCert.Certificate {
|
||||||
|
cert, err := x509.ParseCertificate(certBytes)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Update cert %d parse error: %s", i, err)
|
||||||
|
s.tlsCert = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
for ipStr := range status.KnownIPs {
|
ips := map[string]bool{}
|
||||||
ip := net.ParseIP(ipStr)
|
for _, ip := range cert.IPAddresses {
|
||||||
if len(ip) > 0 {
|
ips[ip.String()] = true
|
||||||
s.ips.ContainsOrAdd(ipStr, ip)
|
}
|
||||||
|
|
||||||
|
domains := map[string]bool{}
|
||||||
|
for _, domain := range cert.DNSNames {
|
||||||
|
domains[domain] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(reflect.DeepEqual(ips, s.ips) && reflect.DeepEqual(domains, s.domains)) {
|
||||||
|
subset := true
|
||||||
|
for ip := range s.ips {
|
||||||
|
if !ips[ip] {
|
||||||
|
subset = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if subset {
|
||||||
|
for domain := range s.domains {
|
||||||
|
if !domains[domain] {
|
||||||
|
subset = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !subset {
|
||||||
|
s.tlsCert = nil
|
||||||
|
}
|
||||||
|
for ip := range ips {
|
||||||
|
s.ips[ip] = true
|
||||||
|
}
|
||||||
|
for domain := range domains {
|
||||||
|
s.domains[domain] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, certString := range status.GeneratedCerts {
|
|
||||||
cert := stringToCert(certString)
|
|
||||||
if cert != nil {
|
|
||||||
s.certs[key] = cert
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Unlock()
|
|
||||||
return s.reload()
|
return s.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) hostPolicy(ctx context.Context, host string) error {
|
|
||||||
s.Lock()
|
|
||||||
defer s.Unlock()
|
|
||||||
|
|
||||||
if s.domains[host] {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New("acme/autocert: host not configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) prompt(tos string) bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) shutdown() error {
|
func (s *server) shutdown() error {
|
||||||
for _, listener := range s.listeners {
|
for _, listener := range s.listeners {
|
||||||
if err := listener.Close(); err != nil {
|
if err := listener.Close(); err != nil {
|
||||||
|
@ -339,114 +285,53 @@ func (s *server) reload() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
switch s.userConfig.Mode {
|
if err := s.serveHTTPS(); err != nil {
|
||||||
case acmeMode:
|
return err
|
||||||
if err := s.serveACME(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case httpsMode:
|
|
||||||
if err := s.serveHTTPS(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) ipMapKey() string {
|
func (s *server) getCertificate(hello *tls.ClientHelloInfo) (_servingCert *tls.Certificate, _err error) {
|
||||||
len := s.ips.Len()
|
|
||||||
keys := s.ips.Keys()
|
|
||||||
if len == 0 {
|
|
||||||
return fmt.Sprintf("local/%d", len)
|
|
||||||
} else if len == 1 {
|
|
||||||
return fmt.Sprintf("local/%s", keys[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(keys, func(i, j int) bool {
|
|
||||||
l, _ := keys[i].(string)
|
|
||||||
r, _ := keys[j].(string)
|
|
||||||
return l < r
|
|
||||||
})
|
|
||||||
if len < 6 {
|
|
||||||
return fmt.Sprintf("local/%v", keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
digest := md5.New()
|
|
||||||
for _, k := range keys {
|
|
||||||
s, _ := k.(string)
|
|
||||||
digest.Write([]byte(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("local/%v", hex.EncodeToString(digest.Sum(nil)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
||||||
s.Lock()
|
s.Lock()
|
||||||
if s.activeCert != nil {
|
|
||||||
s.Unlock()
|
|
||||||
return s.activeCert, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
changed := false
|
changed := false
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
defer s.Unlock()
|
||||||
|
|
||||||
|
if _err != nil {
|
||||||
|
logrus.Errorf("Get certificate error: %s", _err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if changed {
|
if changed {
|
||||||
s.save()
|
s.save()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
defer s.Unlock()
|
|
||||||
|
|
||||||
mapKey := hello.ServerName
|
if hello.ServerName != "" && !s.domains[hello.ServerName] {
|
||||||
cn := hello.ServerName
|
s.tlsCert = nil
|
||||||
dnsNames := []string{cn}
|
s.domains[hello.ServerName] = true
|
||||||
ipBased := false
|
|
||||||
var ips []net.IP
|
|
||||||
|
|
||||||
if cn == "" {
|
|
||||||
mapKey = s.ipMapKey()
|
|
||||||
ipBased = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
serverNameCert, ok := s.certs[mapKey]
|
if s.tlsCert != nil {
|
||||||
if ok {
|
return s.tlsCert, nil
|
||||||
return serverNameCert, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ipBased {
|
ips := []net.IP{}
|
||||||
cn = "cattle"
|
for ipStr := range s.ips {
|
||||||
for _, ipStr := range s.ips.Keys() {
|
if ip := net.ParseIP(ipStr); ip != nil {
|
||||||
ip := net.ParseIP(ipStr.(string))
|
ips = append(ips, ip)
|
||||||
if len(ip) > 0 {
|
|
||||||
ips = append(ips, ip)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
changed = true
|
dnsNames := []string{}
|
||||||
|
for domain := range s.domains {
|
||||||
if s.activeCA == nil {
|
dnsNames = append(dnsNames, domain)
|
||||||
if s.userConfig.CACerts != "" && s.userConfig.CAKey != "" {
|
|
||||||
ca, err := cert.ParseCertsPEM([]byte(s.userConfig.CACerts))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
key, err := cert.ParsePrivateKeyPEM([]byte(s.userConfig.CAKey))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.activeCA = ca[0]
|
|
||||||
s.activeCAKey = key.(crypto.Signer)
|
|
||||||
} else {
|
|
||||||
ca, key, err := genCA()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.activeCA = ca
|
|
||||||
s.activeCAKey = key
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := cert.Config{
|
cfg := cert.Config{
|
||||||
CommonName: cn,
|
CommonName: s.cn,
|
||||||
Organization: s.activeCA.Subject.Organization,
|
Organization: s.activeCA.Subject.Organization,
|
||||||
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
AltNames: cert.AltNames{
|
AltNames: cert.AltNames{
|
||||||
|
@ -472,23 +357,31 @@ func (s *server) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, e
|
||||||
PrivateKey: key,
|
PrivateKey: key,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.certs[mapKey] = tlsCert
|
changed = true
|
||||||
|
s.tlsCert = tlsCert
|
||||||
return tlsCert, nil
|
return tlsCert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) cacheIPHandler(handler http.Handler) http.Handler {
|
func (s *server) cacheHandler(handler http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||||
h, _, err := net.SplitHostPort(req.Host)
|
h, _, err := net.SplitHostPort(req.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h = req.Host
|
h = req.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
ip := net.ParseIP(h)
|
s.Lock()
|
||||||
if len(ip) > 0 {
|
if ip := net.ParseIP(h); ip != nil {
|
||||||
if ok, _ := s.ips.ContainsOrAdd(h, ip); ok {
|
if !s.ips[h] {
|
||||||
go s.save()
|
s.ips[h] = true
|
||||||
|
s.tlsCert = nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !s.domains[h] {
|
||||||
|
s.domains[h] = true
|
||||||
|
s.tlsCert = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
s.Unlock()
|
||||||
|
|
||||||
handler.ServeHTTP(resp, req)
|
handler.ServeHTTP(resp, req)
|
||||||
})
|
})
|
||||||
|
@ -508,7 +401,7 @@ func (s *server) serveHTTPS() error {
|
||||||
|
|
||||||
logger := logrus.StandardLogger()
|
logger := logrus.StandardLogger()
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Handler: s.cacheIPHandler(s.Handler()),
|
Handler: s.cacheHandler(s.Handler()),
|
||||||
ErrorLog: log.New(logger.WriterLevel(logrus.DebugLevel), "", log.LstdFlags),
|
ErrorLog: log.New(logger.WriterLevel(logrus.DebugLevel), "", log.LstdFlags),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -522,7 +415,7 @@ func (s *server) serveHTTPS() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
httpServer := &http.Server{
|
httpServer := &http.Server{
|
||||||
Handler: s.cacheIPHandler(httpRedirect(s.Handler())),
|
Handler: s.cacheHandler(httpRedirect(s.Handler())),
|
||||||
ErrorLog: log.New(logger.WriterLevel(logrus.DebugLevel), "", log.LstdFlags),
|
ErrorLog: log.New(logger.WriterLevel(logrus.DebugLevel), "", log.LstdFlags),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -598,97 +491,48 @@ func (s *server) newListener(ip string, port int, config *tls.Config) (net.Liste
|
||||||
return l, nil
|
return l, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) serveACME() error {
|
func stringToCert(certString string) (*tls.Certificate, error) {
|
||||||
manager := autocert.Manager{
|
|
||||||
Cache: autocert.DirCache("certs-cache"),
|
|
||||||
Prompt: s.prompt,
|
|
||||||
HostPolicy: s.hostPolicy,
|
|
||||||
}
|
|
||||||
conf := &tls.Config{
|
|
||||||
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
||||||
if hello.ServerName == "localhost" || hello.ServerName == "" {
|
|
||||||
newHello := *hello
|
|
||||||
newHello.ServerName = s.userConfig.Domains[0]
|
|
||||||
return manager.GetCertificate(&newHello)
|
|
||||||
}
|
|
||||||
return manager.GetCertificate(hello)
|
|
||||||
},
|
|
||||||
NextProtos: []string{"h2", "http/1.1"},
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.userConfig.HTTPPort > 0 {
|
|
||||||
httpListener, err := s.newListener(s.userConfig.BindAddress, s.userConfig.HTTPPort, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
httpServer := &http.Server{
|
|
||||||
Handler: manager.HTTPHandler(nil),
|
|
||||||
ErrorLog: log.New(logrus.StandardLogger().Writer(), "", log.LstdFlags),
|
|
||||||
}
|
|
||||||
s.servers = append(s.servers, httpServer)
|
|
||||||
go func() {
|
|
||||||
if err := httpServer.Serve(httpListener); err != nil {
|
|
||||||
logrus.Errorf("http server returned err: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
httpsListener, err := s.newListener(s.userConfig.BindAddress, s.userConfig.HTTPSPort, conf)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
httpsServer := &http.Server{
|
|
||||||
Handler: s.Handler(),
|
|
||||||
ErrorLog: log.New(logrus.StandardLogger().Writer(), "", log.LstdFlags),
|
|
||||||
}
|
|
||||||
s.servers = append(s.servers, httpsServer)
|
|
||||||
go func() {
|
|
||||||
if err := httpsServer.Serve(httpsListener); err != nil {
|
|
||||||
logrus.Errorf("https server returned err: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringToCert(certString string) *tls.Certificate {
|
|
||||||
parts := strings.Split(certString, "#")
|
parts := strings.Split(certString, "#")
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
return nil
|
return nil, errors.New("Unable to split cert into two parts")
|
||||||
}
|
}
|
||||||
|
|
||||||
certPart, keyPart := parts[0], parts[1]
|
certPart, keyPart := parts[0], parts[1]
|
||||||
keyBytes, err := base64.StdEncoding.DecodeString(keyPart)
|
keyBytes, err := base64.StdEncoding.DecodeString(keyPart)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err := cert.ParsePrivateKeyPEM(keyBytes)
|
key, err := cert.ParsePrivateKeyPEM(keyBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
certBytes, err := base64.StdEncoding.DecodeString(certPart)
|
certBytes, err := base64.StdEncoding.DecodeString(certPart)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &tls.Certificate{
|
return &tls.Certificate{
|
||||||
Certificate: [][]byte{certBytes},
|
Certificate: [][]byte{certBytes},
|
||||||
PrivateKey: key,
|
PrivateKey: key,
|
||||||
}
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func certToString(cert *tls.Certificate) (string, error) {
|
func certToString(cert *tls.Certificate) (string, error) {
|
||||||
_, keyBytes, err := marshalPrivateKey(cert.PrivateKey.(crypto.Signer))
|
keyType, keyBytes, err := marshalPrivateKey(cert.PrivateKey.(crypto.Signer))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
privateKeyPemBlock := &pem.Block{
|
||||||
|
Type: keyType,
|
||||||
|
Bytes: keyBytes,
|
||||||
|
}
|
||||||
|
pemBytes := pem.EncodeToMemory(privateKeyPemBlock)
|
||||||
|
|
||||||
certString := base64.StdEncoding.EncodeToString(cert.Certificate[0])
|
certString := base64.StdEncoding.EncodeToString(cert.Certificate[0])
|
||||||
keyString := base64.StdEncoding.EncodeToString(keyBytes)
|
keyString := base64.StdEncoding.EncodeToString(pemBytes)
|
||||||
return certString + "#" + keyString, nil
|
return certString + "#" + keyString, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,911 +0,0 @@
|
||||||
// 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.
|
|
||||||
// See https://tools.ietf.org/html/draft-ietf-acme-acme-02 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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LetsEncryptURL is the Directory endpoint of Let's Encrypt CA.
|
|
||||||
const LetsEncryptURL = "https://acme-v01.api.letsencrypt.org/directory"
|
|
||||||
|
|
||||||
// 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 bytes
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
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
|
|
||||||
|
|
||||||
dirMu sync.Mutex // guards writes to dir
|
|
||||||
dir *Directory // cached result of Client's Discover method
|
|
||||||
|
|
||||||
noncesMu sync.Mutex
|
|
||||||
nonces map[string]struct{} // nonces collected from previous responses
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.dirMu.Lock()
|
|
||||||
defer c.dirMu.Unlock()
|
|
||||||
if c.dir != nil {
|
|
||||||
return *c.dir, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
dirURL := c.DirectoryURL
|
|
||||||
if dirURL == "" {
|
|
||||||
dirURL = LetsEncryptURL
|
|
||||||
}
|
|
||||||
res, err := c.get(ctx, dirURL, 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"`
|
|
||||||
Authz string `json:"new-authz"`
|
|
||||||
Cert string `json:"new-cert"`
|
|
||||||
Revoke string `json:"revoke-cert"`
|
|
||||||
Meta struct {
|
|
||||||
Terms string `json:"terms-of-service"`
|
|
||||||
Website string `json:"website"`
|
|
||||||
CAA []string `json:"caa-identities"`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
|
|
||||||
return Directory{}, err
|
|
||||||
}
|
|
||||||
c.dir = &Directory{
|
|
||||||
RegURL: v.Reg,
|
|
||||||
AuthzURL: v.Authz,
|
|
||||||
CertURL: v.Cert,
|
|
||||||
RevokeURL: v.Revoke,
|
|
||||||
Terms: v.Meta.Terms,
|
|
||||||
Website: v.Meta.Website,
|
|
||||||
CAA: v.Meta.CAA,
|
|
||||||
}
|
|
||||||
return *c.dir, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateCert requests a new certificate using the Certificate Signing Request csr encoded in DER format.
|
|
||||||
// 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, c.Key, 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.
|
|
||||||
//
|
|
||||||
// The returned value will also contain the CA (issuer) certificate if the bundle argument is true.
|
|
||||||
//
|
|
||||||
// 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) {
|
|
||||||
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 {
|
|
||||||
if _, err := c.Discover(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
if key == nil {
|
|
||||||
key = c.Key
|
|
||||||
}
|
|
||||||
res, err := c.post(ctx, key, c.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 registration by following the "new-reg" flow.
|
|
||||||
// It returns the registered account. The account 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.
|
|
||||||
func (c *Client) Register(ctx context.Context, a *Account, prompt func(tosURL string) bool) (*Account, error) {
|
|
||||||
if _, err := c.Discover(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if a, err = c.doReg(ctx, c.dir.RegURL, "new-reg", a); 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 registration.
|
|
||||||
// The url argument is an Account URI.
|
|
||||||
func (c *Client) GetReg(ctx context.Context, url string) (*Account, error) {
|
|
||||||
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.
|
|
||||||
func (c *Client) UpdateReg(ctx context.Context, a *Account) (*Account, error) {
|
|
||||||
uri := a.URI
|
|
||||||
a, err := c.doReg(ctx, uri, "reg", a)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
a.URI = uri
|
|
||||||
return a, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authorize performs the initial step in an authorization 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.
|
|
||||||
//
|
|
||||||
// If an authorization has been previously granted, the CA may return
|
|
||||||
// a valid authorization (Authorization.Status is StatusValid). If so, the caller
|
|
||||||
// need not fulfill any challenge and can proceed to requesting a certificate.
|
|
||||||
func (c *Client) Authorize(ctx context.Context, domain 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: "dns", Value: domain},
|
|
||||||
}
|
|
||||||
res, err := c.post(ctx, c.Key, 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) {
|
|
||||||
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 method 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 {
|
|
||||||
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, c.Key, 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) {
|
|
||||||
for {
|
|
||||||
res, err := c.get(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) {
|
|
||||||
res, err := c.get(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) {
|
|
||||||
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, c.Key, 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.
|
|
||||||
// Servers can present the certificate to validate the challenge and prove control
|
|
||||||
// over a domain name.
|
|
||||||
//
|
|
||||||
// The implementation is incomplete in that the returned value is a single certificate,
|
|
||||||
// computed only for Z0 of the key authorization. ACME CAs are expected to update
|
|
||||||
// their implementations to use the newer version, TLS-SNI-02.
|
|
||||||
// For more details on TLS-SNI-01 see https://tools.ietf.org/html/draft-ietf-acme-acme-01#section-7.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 of the TLS ClientHello matches exactly the returned name value.
|
|
||||||
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.
|
|
||||||
// Servers can present the certificate to validate the challenge and prove control
|
|
||||||
// over a domain name. For more details on TLS-SNI-02 see
|
|
||||||
// https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.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 exactly the returned name value.
|
|
||||||
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 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, c.Key, url, req, wantStatus(
|
|
||||||
http.StatusOK, // updates and deletes
|
|
||||||
http.StatusCreated, // new account creation
|
|
||||||
))
|
|
||||||
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 the given URL.
|
|
||||||
func (c *Client) popNonce(ctx context.Context, url string) (string, error) {
|
|
||||||
c.noncesMu.Lock()
|
|
||||||
defer c.noncesMu.Unlock()
|
|
||||||
if len(c.nonces) == 0 {
|
|
||||||
return c.fetchNonce(ctx, url)
|
|
||||||
}
|
|
||||||
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
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,130 +0,0 @@
|
||||||
// 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 autocert
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrCacheMiss is returned when a certificate is not found in cache.
|
|
||||||
var ErrCacheMiss = errors.New("acme/autocert: certificate cache miss")
|
|
||||||
|
|
||||||
// Cache is used by Manager to store and retrieve previously obtained certificates
|
|
||||||
// and other account data as opaque blobs.
|
|
||||||
//
|
|
||||||
// Cache implementations should not rely on the key naming pattern. Keys can
|
|
||||||
// include any printable ASCII characters, except the following: \/:*?"<>|
|
|
||||||
type Cache interface {
|
|
||||||
// Get returns a certificate data for the specified key.
|
|
||||||
// If there's no such key, Get returns ErrCacheMiss.
|
|
||||||
Get(ctx context.Context, key string) ([]byte, error)
|
|
||||||
|
|
||||||
// Put stores the data in the cache under the specified key.
|
|
||||||
// Underlying implementations may use any data storage format,
|
|
||||||
// as long as the reverse operation, Get, results in the original data.
|
|
||||||
Put(ctx context.Context, key string, data []byte) error
|
|
||||||
|
|
||||||
// Delete removes a certificate data from the cache under the specified key.
|
|
||||||
// If there's no such key in the cache, Delete returns nil.
|
|
||||||
Delete(ctx context.Context, key string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// DirCache implements Cache using a directory on the local filesystem.
|
|
||||||
// If the directory does not exist, it will be created with 0700 permissions.
|
|
||||||
type DirCache string
|
|
||||||
|
|
||||||
// Get reads a certificate data from the specified file name.
|
|
||||||
func (d DirCache) Get(ctx context.Context, name string) ([]byte, error) {
|
|
||||||
name = filepath.Join(string(d), name)
|
|
||||||
var (
|
|
||||||
data []byte
|
|
||||||
err error
|
|
||||||
done = make(chan struct{})
|
|
||||||
)
|
|
||||||
go func() {
|
|
||||||
data, err = ioutil.ReadFile(name)
|
|
||||||
close(done)
|
|
||||||
}()
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
case <-done:
|
|
||||||
}
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil, ErrCacheMiss
|
|
||||||
}
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put writes the certificate data to the specified file name.
|
|
||||||
// The file will be created with 0600 permissions.
|
|
||||||
func (d DirCache) Put(ctx context.Context, name string, data []byte) error {
|
|
||||||
if err := os.MkdirAll(string(d), 0700); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
var err error
|
|
||||||
go func() {
|
|
||||||
defer close(done)
|
|
||||||
var tmp string
|
|
||||||
if tmp, err = d.writeTempFile(name, data); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
// Don't overwrite the file if the context was canceled.
|
|
||||||
default:
|
|
||||||
newName := filepath.Join(string(d), name)
|
|
||||||
err = os.Rename(tmp, newName)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-done:
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes the specified file name.
|
|
||||||
func (d DirCache) Delete(ctx context.Context, name string) error {
|
|
||||||
name = filepath.Join(string(d), name)
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
done = make(chan struct{})
|
|
||||||
)
|
|
||||||
go func() {
|
|
||||||
err = os.Remove(name)
|
|
||||||
close(done)
|
|
||||||
}()
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-done:
|
|
||||||
}
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeTempFile writes b to a temporary file, closes the file and returns its path.
|
|
||||||
func (d DirCache) writeTempFile(prefix string, b []byte) (string, error) {
|
|
||||||
// TempFile uses 0600 permissions
|
|
||||||
f, err := ioutil.TempFile(string(d), prefix)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if _, err := f.Write(b); err != nil {
|
|
||||||
f.Close()
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return f.Name(), f.Close()
|
|
||||||
}
|
|
|
@ -1,160 +0,0 @@
|
||||||
// Copyright 2017 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 autocert
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewListener returns a net.Listener that listens on the standard TLS
|
|
||||||
// port (443) on all interfaces and returns *tls.Conn connections with
|
|
||||||
// LetsEncrypt certificates for the provided domain or domains.
|
|
||||||
//
|
|
||||||
// It enables one-line HTTPS servers:
|
|
||||||
//
|
|
||||||
// log.Fatal(http.Serve(autocert.NewListener("example.com"), handler))
|
|
||||||
//
|
|
||||||
// NewListener is a convenience function for a common configuration.
|
|
||||||
// More complex or custom configurations can use the autocert.Manager
|
|
||||||
// type instead.
|
|
||||||
//
|
|
||||||
// Use of this function implies acceptance of the LetsEncrypt Terms of
|
|
||||||
// Service. If domains is not empty, the provided domains are passed
|
|
||||||
// to HostWhitelist. If domains is empty, the listener will do
|
|
||||||
// LetsEncrypt challenges for any requested domain, which is not
|
|
||||||
// recommended.
|
|
||||||
//
|
|
||||||
// Certificates are cached in a "golang-autocert" directory under an
|
|
||||||
// operating system-specific cache or temp directory. This may not
|
|
||||||
// be suitable for servers spanning multiple machines.
|
|
||||||
//
|
|
||||||
// The returned listener uses a *tls.Config that enables HTTP/2, and
|
|
||||||
// should only be used with servers that support HTTP/2.
|
|
||||||
//
|
|
||||||
// The returned Listener also enables TCP keep-alives on the accepted
|
|
||||||
// connections. The returned *tls.Conn are returned before their TLS
|
|
||||||
// handshake has completed.
|
|
||||||
func NewListener(domains ...string) net.Listener {
|
|
||||||
m := &Manager{
|
|
||||||
Prompt: AcceptTOS,
|
|
||||||
}
|
|
||||||
if len(domains) > 0 {
|
|
||||||
m.HostPolicy = HostWhitelist(domains...)
|
|
||||||
}
|
|
||||||
dir := cacheDir()
|
|
||||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
|
||||||
log.Printf("warning: autocert.NewListener not using a cache: %v", err)
|
|
||||||
} else {
|
|
||||||
m.Cache = DirCache(dir)
|
|
||||||
}
|
|
||||||
return m.Listener()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listener listens on the standard TLS port (443) on all interfaces
|
|
||||||
// and returns a net.Listener returning *tls.Conn connections.
|
|
||||||
//
|
|
||||||
// The returned listener uses a *tls.Config that enables HTTP/2, and
|
|
||||||
// should only be used with servers that support HTTP/2.
|
|
||||||
//
|
|
||||||
// The returned Listener also enables TCP keep-alives on the accepted
|
|
||||||
// connections. The returned *tls.Conn are returned before their TLS
|
|
||||||
// handshake has completed.
|
|
||||||
//
|
|
||||||
// Unlike NewListener, it is the caller's responsibility to initialize
|
|
||||||
// the Manager m's Prompt, Cache, HostPolicy, and other desired options.
|
|
||||||
func (m *Manager) Listener() net.Listener {
|
|
||||||
ln := &listener{
|
|
||||||
m: m,
|
|
||||||
conf: &tls.Config{
|
|
||||||
GetCertificate: m.GetCertificate, // bonus: panic on nil m
|
|
||||||
NextProtos: []string{"h2", "http/1.1"}, // Enable HTTP/2
|
|
||||||
},
|
|
||||||
}
|
|
||||||
ln.tcpListener, ln.tcpListenErr = net.Listen("tcp", ":443")
|
|
||||||
return ln
|
|
||||||
}
|
|
||||||
|
|
||||||
type listener struct {
|
|
||||||
m *Manager
|
|
||||||
conf *tls.Config
|
|
||||||
|
|
||||||
tcpListener net.Listener
|
|
||||||
tcpListenErr error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ln *listener) Accept() (net.Conn, error) {
|
|
||||||
if ln.tcpListenErr != nil {
|
|
||||||
return nil, ln.tcpListenErr
|
|
||||||
}
|
|
||||||
conn, err := ln.tcpListener.Accept()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tcpConn := conn.(*net.TCPConn)
|
|
||||||
|
|
||||||
// Because Listener is a convenience function, help out with
|
|
||||||
// this too. This is not possible for the caller to set once
|
|
||||||
// we return a *tcp.Conn wrapping an inaccessible net.Conn.
|
|
||||||
// If callers don't want this, they can do things the manual
|
|
||||||
// way and tweak as needed. But this is what net/http does
|
|
||||||
// itself, so copy that. If net/http changes, we can change
|
|
||||||
// here too.
|
|
||||||
tcpConn.SetKeepAlive(true)
|
|
||||||
tcpConn.SetKeepAlivePeriod(3 * time.Minute)
|
|
||||||
|
|
||||||
return tls.Server(tcpConn, ln.conf), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ln *listener) Addr() net.Addr {
|
|
||||||
if ln.tcpListener != nil {
|
|
||||||
return ln.tcpListener.Addr()
|
|
||||||
}
|
|
||||||
// net.Listen failed. Return something non-nil in case callers
|
|
||||||
// call Addr before Accept:
|
|
||||||
return &net.TCPAddr{IP: net.IP{0, 0, 0, 0}, Port: 443}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ln *listener) Close() error {
|
|
||||||
if ln.tcpListenErr != nil {
|
|
||||||
return ln.tcpListenErr
|
|
||||||
}
|
|
||||||
return ln.tcpListener.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func homeDir() string {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
|
|
||||||
}
|
|
||||||
if h := os.Getenv("HOME"); h != "" {
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
return "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
func cacheDir() string {
|
|
||||||
const base = "golang-autocert"
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "darwin":
|
|
||||||
return filepath.Join(homeDir(), "Library", "Caches", base)
|
|
||||||
case "windows":
|
|
||||||
for _, ev := range []string{"APPDATA", "CSIDL_APPDATA", "TEMP", "TMP"} {
|
|
||||||
if v := os.Getenv(ev); v != "" {
|
|
||||||
return filepath.Join(v, base)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Worst case:
|
|
||||||
return filepath.Join(homeDir(), base)
|
|
||||||
}
|
|
||||||
if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" {
|
|
||||||
return filepath.Join(xdg, base)
|
|
||||||
}
|
|
||||||
return filepath.Join(homeDir(), ".cache", base)
|
|
||||||
}
|
|
|
@ -1,141 +0,0 @@
|
||||||
// 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 autocert
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// renewJitter is the maximum deviation from Manager.RenewBefore.
|
|
||||||
const renewJitter = time.Hour
|
|
||||||
|
|
||||||
// domainRenewal tracks the state used by the periodic timers
|
|
||||||
// renewing a single domain's cert.
|
|
||||||
type domainRenewal struct {
|
|
||||||
m *Manager
|
|
||||||
ck certKey
|
|
||||||
key crypto.Signer
|
|
||||||
|
|
||||||
timerMu sync.Mutex
|
|
||||||
timer *time.Timer
|
|
||||||
}
|
|
||||||
|
|
||||||
// start starts a cert renewal timer at the time
|
|
||||||
// defined by the certificate expiration time exp.
|
|
||||||
//
|
|
||||||
// If the timer is already started, calling start is a noop.
|
|
||||||
func (dr *domainRenewal) start(exp time.Time) {
|
|
||||||
dr.timerMu.Lock()
|
|
||||||
defer dr.timerMu.Unlock()
|
|
||||||
if dr.timer != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dr.timer = time.AfterFunc(dr.next(exp), dr.renew)
|
|
||||||
}
|
|
||||||
|
|
||||||
// stop stops the cert renewal timer.
|
|
||||||
// If the timer is already stopped, calling stop is a noop.
|
|
||||||
func (dr *domainRenewal) stop() {
|
|
||||||
dr.timerMu.Lock()
|
|
||||||
defer dr.timerMu.Unlock()
|
|
||||||
if dr.timer == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dr.timer.Stop()
|
|
||||||
dr.timer = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// renew is called periodically by a timer.
|
|
||||||
// The first renew call is kicked off by dr.start.
|
|
||||||
func (dr *domainRenewal) renew() {
|
|
||||||
dr.timerMu.Lock()
|
|
||||||
defer dr.timerMu.Unlock()
|
|
||||||
if dr.timer == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
|
||||||
defer cancel()
|
|
||||||
// TODO: rotate dr.key at some point?
|
|
||||||
next, err := dr.do(ctx)
|
|
||||||
if err != nil {
|
|
||||||
next = renewJitter / 2
|
|
||||||
next += time.Duration(pseudoRand.int63n(int64(next)))
|
|
||||||
}
|
|
||||||
dr.timer = time.AfterFunc(next, dr.renew)
|
|
||||||
testDidRenewLoop(next, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateState locks and replaces the relevant Manager.state item with the given
|
|
||||||
// state. It additionally updates dr.key with the given state's key.
|
|
||||||
func (dr *domainRenewal) updateState(state *certState) {
|
|
||||||
dr.m.stateMu.Lock()
|
|
||||||
defer dr.m.stateMu.Unlock()
|
|
||||||
dr.key = state.key
|
|
||||||
dr.m.state[dr.ck] = state
|
|
||||||
}
|
|
||||||
|
|
||||||
// do is similar to Manager.createCert but it doesn't lock a Manager.state item.
|
|
||||||
// Instead, it requests a new certificate independently and, upon success,
|
|
||||||
// replaces dr.m.state item with a new one and updates cache for the given domain.
|
|
||||||
//
|
|
||||||
// It may lock and update the Manager.state if the expiration date of the currently
|
|
||||||
// cached cert is far enough in the future.
|
|
||||||
//
|
|
||||||
// The returned value is a time interval after which the renewal should occur again.
|
|
||||||
func (dr *domainRenewal) do(ctx context.Context) (time.Duration, error) {
|
|
||||||
// a race is likely unavoidable in a distributed environment
|
|
||||||
// but we try nonetheless
|
|
||||||
if tlscert, err := dr.m.cacheGet(ctx, dr.ck); err == nil {
|
|
||||||
next := dr.next(tlscert.Leaf.NotAfter)
|
|
||||||
if next > dr.m.renewBefore()+renewJitter {
|
|
||||||
signer, ok := tlscert.PrivateKey.(crypto.Signer)
|
|
||||||
if ok {
|
|
||||||
state := &certState{
|
|
||||||
key: signer,
|
|
||||||
cert: tlscert.Certificate,
|
|
||||||
leaf: tlscert.Leaf,
|
|
||||||
}
|
|
||||||
dr.updateState(state)
|
|
||||||
return next, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
der, leaf, err := dr.m.authorizedCert(ctx, dr.key, dr.ck)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
state := &certState{
|
|
||||||
key: dr.key,
|
|
||||||
cert: der,
|
|
||||||
leaf: leaf,
|
|
||||||
}
|
|
||||||
tlscert, err := state.tlscert()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if err := dr.m.cachePut(ctx, dr.ck, tlscert); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
dr.updateState(state)
|
|
||||||
return dr.next(leaf.NotAfter), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dr *domainRenewal) next(expiry time.Time) time.Duration {
|
|
||||||
d := expiry.Sub(timeNow()) - dr.m.renewBefore()
|
|
||||||
// add a bit of randomness to renew deadline
|
|
||||||
n := pseudoRand.int63n(int64(renewJitter))
|
|
||||||
d -= time.Duration(n)
|
|
||||||
if d < 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
var testDidRenewLoop = func(next time.Duration, err error) {}
|
|
|
@ -1,281 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// post issues a signed POST request in JWS format using the provided key
|
|
||||||
// to the specified URL.
|
|
||||||
// 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.
|
|
||||||
// The body argument must be JSON-serializable.
|
|
||||||
// It is used by c.post to retry unsuccessful attempts.
|
|
||||||
func (c *Client) postNoRetry(ctx context.Context, key crypto.Signer, url string, body interface{}) (*http.Response, *http.Request, error) {
|
|
||||||
nonce, err := c.popNonce(ctx, url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
b, err := jwsEncodeJSON(body, key, nonce)
|
|
||||||
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) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
|
@ -1,153 +0,0 @@
|
||||||
// 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/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
)
|
|
||||||
|
|
||||||
// jwsEncodeJSON signs claimset using provided key and a nonce.
|
|
||||||
// The result is serialized in JSON format.
|
|
||||||
// See https://tools.ietf.org/html/rfc7515#section-7.
|
|
||||||
func jwsEncodeJSON(claimset interface{}, key crypto.Signer, nonce string) ([]byte, error) {
|
|
||||||
jwk, err := jwkEncode(key.Public())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
alg, sha := jwsHasher(key)
|
|
||||||
if alg == "" || !sha.Available() {
|
|
||||||
return nil, ErrUnsupportedKey
|
|
||||||
}
|
|
||||||
phead := fmt.Sprintf(`{"alg":%q,"jwk":%s,"nonce":%q}`, alg, jwk, nonce)
|
|
||||||
phead = base64.RawURLEncoding.EncodeToString([]byte(phead))
|
|
||||||
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.
|
|
||||||
// It returns ErrUnsupportedKey if the key type is unknown.
|
|
||||||
// The hash is used only for RSA keys.
|
|
||||||
func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) {
|
|
||||||
switch key := key.(type) {
|
|
||||||
case *rsa.PrivateKey:
|
|
||||||
return key.Sign(rand.Reader, digest, hash)
|
|
||||||
case *ecdsa.PrivateKey:
|
|
||||||
r, s, err := ecdsa.Sign(rand.Reader, key, digest)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rb, sb := r.Bytes(), s.Bytes()
|
|
||||||
size := key.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(key crypto.Signer) (string, crypto.Hash) {
|
|
||||||
switch key := key.(type) {
|
|
||||||
case *rsa.PrivateKey:
|
|
||||||
return "RS256", crypto.SHA256
|
|
||||||
case *ecdsa.PrivateKey:
|
|
||||||
switch key.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
|
|
||||||
}
|
|
|
@ -1,329 +0,0 @@
|
||||||
// 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 server response statuses used to describe Authorization and Challenge states.
|
|
||||||
const (
|
|
||||||
StatusUnknown = "unknown"
|
|
||||||
StatusPending = "pending"
|
|
||||||
StatusProcessing = "processing"
|
|
||||||
StatusValid = "valid"
|
|
||||||
StatusInvalid = "invalid"
|
|
||||||
StatusRevoked = "revoked"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrUnsupportedKey is returned when an unsupported key type is encountered.
|
|
||||||
var ErrUnsupportedKey = errors.New("acme: unknown key type; only RSA and ECDSA are supported")
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// 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, "; "))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
type Account struct {
|
|
||||||
// URI is the account unique ID, which is also a URL used to retrieve
|
|
||||||
// account data from the CA.
|
|
||||||
URI string
|
|
||||||
|
|
||||||
// Contact is a slice of contact info used during registration.
|
|
||||||
Contact []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.
|
|
||||||
AgreedTerms string
|
|
||||||
|
|
||||||
// Actual terms of a CA.
|
|
||||||
CurrentTerms string
|
|
||||||
|
|
||||||
// Authz is the authorization URL used to initiate a new authz flow.
|
|
||||||
Authz string
|
|
||||||
|
|
||||||
// Authorizations is a URI from which a list of authorizations
|
|
||||||
// granted to this account can be fetched via a GET request.
|
|
||||||
Authorizations string
|
|
||||||
|
|
||||||
// Certificates is a URI from which a list of certificates
|
|
||||||
// issued for this account can be fetched via a GET request.
|
|
||||||
Certificates string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Directory is ACME server discovery data.
|
|
||||||
type Directory struct {
|
|
||||||
// RegURL is an account endpoint URL, allowing for creating new
|
|
||||||
// and modifying existing accounts.
|
|
||||||
RegURL string
|
|
||||||
|
|
||||||
// AuthzURL is used to initiate Identifier Authorization flow.
|
|
||||||
AuthzURL string
|
|
||||||
|
|
||||||
// CertURL is a new certificate issuance endpoint URL.
|
|
||||||
CertURL string
|
|
||||||
|
|
||||||
// RevokeURL is used to initiate a certificate revocation flow.
|
|
||||||
RevokeURL 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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-sni-02", "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.
|
|
||||||
Status string
|
|
||||||
|
|
||||||
// Error indicates the reason for an authorization failure
|
|
||||||
// when this challenge was used.
|
|
||||||
// The type of a non-nil value is *Error.
|
|
||||||
Error error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authorization encodes an authorization response.
|
|
||||||
type Authorization struct {
|
|
||||||
// URI uniquely identifies a authorization.
|
|
||||||
URI string
|
|
||||||
|
|
||||||
// Status identifies the status of an authorization.
|
|
||||||
Status string
|
|
||||||
|
|
||||||
// Identifier is what the account is authorized to represent.
|
|
||||||
Identifier AuthzID
|
|
||||||
|
|
||||||
// Challenges that the client needs to fulfill in order to prove possession
|
|
||||||
// of the identifier (for pending authorizations).
|
|
||||||
// For final authorizations, the challenges that were used.
|
|
||||||
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.
|
|
||||||
Combinations [][]int
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthzID is an identifier that an account is authorized to represent.
|
|
||||||
type AuthzID struct {
|
|
||||||
Type string // The type of identifier, e.g. "dns".
|
|
||||||
Value string // The identifier itself, e.g. "example.org".
|
|
||||||
}
|
|
||||||
|
|
||||||
// wireAuthz is ACME JSON representation of Authorization objects.
|
|
||||||
type wireAuthz struct {
|
|
||||||
Status string
|
|
||||||
Challenges []wireChallenge
|
|
||||||
Combinations [][]int
|
|
||||||
Identifier struct {
|
|
||||||
Type string
|
|
||||||
Value string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (z *wireAuthz) authorization(uri string) *Authorization {
|
|
||||||
a := &Authorization{
|
|
||||||
URI: uri,
|
|
||||||
Status: z.Status,
|
|
||||||
Identifier: AuthzID{Type: z.Identifier.Type, Value: z.Identifier.Value},
|
|
||||||
Combinations: z.Combinations, // shallow copy
|
|
||||||
Challenges: make([]*Challenge, len(z.Challenges)),
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// wireChallenge is ACME JSON challenge representation.
|
|
||||||
type wireChallenge struct {
|
|
||||||
URI string `json:"uri"`
|
|
||||||
Type string
|
|
||||||
Token string
|
|
||||||
Status string
|
|
||||||
Error *wireError
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *wireChallenge) challenge() *Challenge {
|
|
||||||
v := &Challenge{
|
|
||||||
URI: c.URI,
|
|
||||||
Type: c.Type,
|
|
||||||
Token: c.Token,
|
|
||||||
Status: c.Status,
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *wireError) error(h http.Header) *Error {
|
|
||||||
return &Error{
|
|
||||||
StatusCode: e.Status,
|
|
||||||
ProblemType: e.Type,
|
|
||||||
Detail: e.Detail,
|
|
||||||
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() {}
|
|
Loading…
Reference in New Issue