334 lines
8.4 KiB
Go
334 lines
8.4 KiB
Go
package acme
|
|
|
|
import (
|
|
"context"
|
|
"crypto"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"reflect"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/containous/traefik/log"
|
|
acmeprovider "github.com/containous/traefik/provider/acme"
|
|
"github.com/containous/traefik/types"
|
|
"github.com/xenolf/lego/acme"
|
|
)
|
|
|
|
// Account is used to store lets encrypt registration info
|
|
type Account struct {
|
|
Email string
|
|
Registration *acme.RegistrationResource
|
|
PrivateKey []byte
|
|
KeyType acme.KeyType
|
|
DomainsCertificate DomainsCertificates
|
|
ChallengeCerts map[string]*ChallengeCert
|
|
HTTPChallenge map[string]map[string][]byte
|
|
}
|
|
|
|
// ChallengeCert stores a challenge certificate
|
|
type ChallengeCert struct {
|
|
Certificate []byte
|
|
PrivateKey []byte
|
|
certificate *tls.Certificate
|
|
}
|
|
|
|
// Init account struct
|
|
func (a *Account) Init() error {
|
|
err := a.DomainsCertificate.Init()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = a.RemoveAccountV1Values()
|
|
if err != nil {
|
|
log.Errorf("Unable to remove ACME Account V1 values during account initialization: %v", err)
|
|
}
|
|
|
|
for _, cert := range a.ChallengeCerts {
|
|
if cert.certificate == nil {
|
|
certificate, err := tls.X509KeyPair(cert.Certificate, cert.PrivateKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cert.certificate = &certificate
|
|
}
|
|
|
|
if cert.certificate.Leaf == nil {
|
|
leaf, err := x509.ParseCertificate(cert.certificate.Certificate[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cert.certificate.Leaf = leaf
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// NewAccount creates an account
|
|
func NewAccount(email string, certs []*DomainsCertificate, keyTypeValue string) (*Account, error) {
|
|
keyType := acmeprovider.GetKeyType(context.Background(), keyTypeValue)
|
|
|
|
// Create a user. New accounts need an email and private key to start
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
domainsCerts := DomainsCertificates{Certs: certs}
|
|
err = domainsCerts.Init()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Account{
|
|
Email: email,
|
|
PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey),
|
|
KeyType: keyType,
|
|
DomainsCertificate: DomainsCertificates{Certs: domainsCerts.Certs},
|
|
ChallengeCerts: map[string]*ChallengeCert{}}, nil
|
|
}
|
|
|
|
// GetEmail returns email
|
|
func (a *Account) GetEmail() string {
|
|
return a.Email
|
|
}
|
|
|
|
// GetRegistration returns lets encrypt registration resource
|
|
func (a *Account) GetRegistration() *acme.RegistrationResource {
|
|
return a.Registration
|
|
}
|
|
|
|
// GetPrivateKey returns private key
|
|
func (a *Account) GetPrivateKey() crypto.PrivateKey {
|
|
if privateKey, err := x509.ParsePKCS1PrivateKey(a.PrivateKey); err == nil {
|
|
return privateKey
|
|
}
|
|
|
|
log.Errorf("Cannot unmarshall private key %+v", a.PrivateKey)
|
|
return nil
|
|
}
|
|
|
|
// RemoveAccountV1Values removes ACME account V1 values
|
|
func (a *Account) RemoveAccountV1Values() error {
|
|
// Check if ACME Account is in ACME V1 format
|
|
if a.Registration != nil {
|
|
isOldRegistration, err := regexp.MatchString(acmeprovider.RegistrationURLPathV1Regexp, a.Registration.URI)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if isOldRegistration {
|
|
a.reset()
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *Account) reset() {
|
|
log.Debug("Reset ACME account object.")
|
|
a.Email = ""
|
|
a.Registration = nil
|
|
a.PrivateKey = nil
|
|
}
|
|
|
|
// Certificate is used to store certificate info
|
|
type Certificate struct {
|
|
Domain string
|
|
CertURL string
|
|
CertStableURL string
|
|
PrivateKey []byte
|
|
Certificate []byte
|
|
}
|
|
|
|
// DomainsCertificates stores a certificate for multiple domains
|
|
type DomainsCertificates struct {
|
|
Certs []*DomainsCertificate
|
|
lock sync.RWMutex
|
|
}
|
|
|
|
func (dc *DomainsCertificates) Len() int {
|
|
return len(dc.Certs)
|
|
}
|
|
|
|
func (dc *DomainsCertificates) Swap(i, j int) {
|
|
dc.Certs[i], dc.Certs[j] = dc.Certs[j], dc.Certs[i]
|
|
}
|
|
|
|
func (dc *DomainsCertificates) Less(i, j int) bool {
|
|
if reflect.DeepEqual(dc.Certs[i].Domains, dc.Certs[j].Domains) {
|
|
return dc.Certs[i].tlsCert.Leaf.NotAfter.After(dc.Certs[j].tlsCert.Leaf.NotAfter)
|
|
}
|
|
|
|
if dc.Certs[i].Domains.Main == dc.Certs[j].Domains.Main {
|
|
return strings.Join(dc.Certs[i].Domains.SANs, ",") < strings.Join(dc.Certs[j].Domains.SANs, ",")
|
|
}
|
|
|
|
return dc.Certs[i].Domains.Main < dc.Certs[j].Domains.Main
|
|
}
|
|
|
|
func (dc *DomainsCertificates) removeDuplicates() {
|
|
sort.Sort(dc)
|
|
for i := 0; i < len(dc.Certs); i++ {
|
|
for i2 := i + 1; i2 < len(dc.Certs); i2++ {
|
|
if reflect.DeepEqual(dc.Certs[i].Domains, dc.Certs[i2].Domains) {
|
|
// delete
|
|
log.Warnf("Remove duplicate cert: %+v, expiration :%s", dc.Certs[i2].Domains, dc.Certs[i2].tlsCert.Leaf.NotAfter.String())
|
|
dc.Certs = append(dc.Certs[:i2], dc.Certs[i2+1:]...)
|
|
i2--
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (dc *DomainsCertificates) removeEmpty() {
|
|
var certs []*DomainsCertificate
|
|
for _, cert := range dc.Certs {
|
|
if cert.Certificate != nil && len(cert.Certificate.Certificate) > 0 && len(cert.Certificate.PrivateKey) > 0 {
|
|
certs = append(certs, cert)
|
|
}
|
|
}
|
|
dc.Certs = certs
|
|
}
|
|
|
|
// Init DomainsCertificates
|
|
func (dc *DomainsCertificates) Init() error {
|
|
dc.lock.Lock()
|
|
defer dc.lock.Unlock()
|
|
|
|
dc.removeEmpty()
|
|
|
|
for _, domainsCertificate := range dc.Certs {
|
|
tlsCert, err := tls.X509KeyPair(domainsCertificate.Certificate.Certificate, domainsCertificate.Certificate.PrivateKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
domainsCertificate.tlsCert = &tlsCert
|
|
|
|
if domainsCertificate.tlsCert.Leaf == nil {
|
|
leaf, err := x509.ParseCertificate(domainsCertificate.tlsCert.Certificate[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
domainsCertificate.tlsCert.Leaf = leaf
|
|
}
|
|
}
|
|
|
|
dc.removeDuplicates()
|
|
return nil
|
|
}
|
|
|
|
func (dc *DomainsCertificates) renewCertificates(acmeCert *Certificate, domain types.Domain) error {
|
|
dc.lock.Lock()
|
|
defer dc.lock.Unlock()
|
|
|
|
for _, domainsCertificate := range dc.Certs {
|
|
if reflect.DeepEqual(domain, domainsCertificate.Domains) {
|
|
tlsCert, err := tls.X509KeyPair(acmeCert.Certificate, acmeCert.PrivateKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
domainsCertificate.Certificate = acmeCert
|
|
domainsCertificate.tlsCert = &tlsCert
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("certificate to renew not found for domain %s", domain.Main)
|
|
}
|
|
|
|
func (dc *DomainsCertificates) addCertificateForDomains(acmeCert *Certificate, domain types.Domain) (*DomainsCertificate, error) {
|
|
dc.lock.Lock()
|
|
defer dc.lock.Unlock()
|
|
|
|
tlsCert, err := tls.X509KeyPair(acmeCert.Certificate, acmeCert.PrivateKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cert := DomainsCertificate{Domains: domain, Certificate: acmeCert, tlsCert: &tlsCert}
|
|
dc.Certs = append(dc.Certs, &cert)
|
|
return &cert, nil
|
|
}
|
|
|
|
func (dc *DomainsCertificates) getCertificateForDomain(domainToFind string) (*DomainsCertificate, bool) {
|
|
dc.lock.RLock()
|
|
defer dc.lock.RUnlock()
|
|
|
|
for _, domainsCertificate := range dc.Certs {
|
|
for _, domain := range domainsCertificate.Domains.ToStrArray() {
|
|
if strings.HasPrefix(domain, "*.") && types.MatchDomain(domainToFind, domain) {
|
|
return domainsCertificate, true
|
|
}
|
|
if domain == domainToFind {
|
|
return domainsCertificate, true
|
|
}
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func (dc *DomainsCertificates) exists(domainToFind types.Domain) (*DomainsCertificate, bool) {
|
|
dc.lock.RLock()
|
|
defer dc.lock.RUnlock()
|
|
|
|
for _, domainsCertificate := range dc.Certs {
|
|
if reflect.DeepEqual(domainToFind, domainsCertificate.Domains) {
|
|
return domainsCertificate, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func (dc *DomainsCertificates) toDomainsMap() map[string]*tls.Certificate {
|
|
domainsCertificatesMap := make(map[string]*tls.Certificate)
|
|
|
|
for _, domainCertificate := range dc.Certs {
|
|
certKey := domainCertificate.Domains.Main
|
|
|
|
if domainCertificate.Domains.SANs != nil {
|
|
sort.Strings(domainCertificate.Domains.SANs)
|
|
|
|
for _, dnsName := range domainCertificate.Domains.SANs {
|
|
if dnsName != domainCertificate.Domains.Main {
|
|
certKey += fmt.Sprintf(",%s", dnsName)
|
|
}
|
|
}
|
|
}
|
|
domainsCertificatesMap[certKey] = domainCertificate.tlsCert
|
|
}
|
|
return domainsCertificatesMap
|
|
}
|
|
|
|
// DomainsCertificate contains a certificate for multiple domains
|
|
type DomainsCertificate struct {
|
|
Domains types.Domain
|
|
Certificate *Certificate
|
|
tlsCert *tls.Certificate
|
|
}
|
|
|
|
func (dc *DomainsCertificate) needRenew() bool {
|
|
for _, c := range dc.tlsCert.Certificate {
|
|
crt, err := x509.ParseCertificate(c)
|
|
if err != nil {
|
|
// If there's an error, we assume the cert is broken, and needs update
|
|
return true
|
|
}
|
|
|
|
// <= 30 days left, renew certificate
|
|
if crt.NotAfter.Before(time.Now().Add(24 * 30 * time.Hour)) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|