1
0
Fork 0

Challenge certs PEM encoding

Signed-off-by: Emile Vauge <emile@vauge.com>
This commit is contained in:
Emile Vauge 2016-09-23 18:27:01 +02:00
parent a42845502e
commit e72e65858f
No known key found for this signature in database
GPG key ID: D808B4C167352E59
25 changed files with 490 additions and 107 deletions

View file

@ -20,7 +20,14 @@ type Account struct {
Registration *acme.RegistrationResource
PrivateKey []byte
DomainsCertificate DomainsCertificates
ChallengeCerts map[string][]byte
ChallengeCerts map[string]*ChallengeCert
}
// ChallengeCert stores a challenge certificate
type ChallengeCert struct {
Certificate []byte
PrivateKey []byte
certificate *tls.Certificate
}
// Init inits acccount struct
@ -29,9 +36,27 @@ func (a *Account) Init() error {
if err != nil {
return 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) (*Account, error) {
// Create a user. New accounts need an email and private key to start
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
@ -43,22 +68,22 @@ func NewAccount(email string) (*Account, error) {
return &Account{
Email: email,
PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey),
DomainsCertificate: domainsCerts,
ChallengeCerts: map[string][]byte{}}, nil
DomainsCertificate: DomainsCertificates{Certs: domainsCerts.Certs},
ChallengeCerts: map[string]*ChallengeCert{}}, nil
}
// GetEmail returns email
func (a Account) GetEmail() string {
func (a *Account) GetEmail() string {
return a.Email
}
// GetRegistration returns lets encrypt registration resource
func (a Account) GetRegistration() *acme.RegistrationResource {
func (a *Account) GetRegistration() *acme.RegistrationResource {
return a.Registration
}
// GetPrivateKey returns private key
func (a Account) GetPrivateKey() crypto.PrivateKey {
func (a *Account) GetPrivateKey() crypto.PrivateKey {
if privateKey, err := x509.ParsePKCS1PrivateKey(a.PrivateKey); err == nil {
return privateKey
}
@ -81,6 +106,7 @@ type DomainsCertificates struct {
lock sync.RWMutex
}
// Init inits DomainsCertificates
func (dc *DomainsCertificates) Init() error {
dc.lock.Lock()
defer dc.lock.Unlock()
@ -167,7 +193,7 @@ func (dc *DomainsCertificate) needRenew() bool {
return true
}
// <= 7 days left, renew certificate
if crt.NotAfter.Before(time.Now().Add(time.Duration(24 * 7 * time.Hour))) {
if crt.NotAfter.Before(time.Now().Add(time.Duration(24 * 30 * time.Hour))) {
return true
}
}

View file

@ -4,6 +4,7 @@ import (
"crypto/tls"
"errors"
"fmt"
"github.com/cenk/backoff"
"github.com/containous/staert"
"github.com/containous/traefik/cluster"
"github.com/containous/traefik/log"
@ -22,6 +23,7 @@ type ACME struct {
Email string `description:"Email address used for registration"`
Domains []Domain `description:"SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='main.net,san1.net,san2.net'"`
Storage string `description:"File or key used for certificates storage."`
StorageFile string // deprecated
OnDemand bool `description:"Enable on demand certificate. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."`
OnHostRule bool `description:"Enable certificate generation on frontends Host rules."`
CAServer string `description:"CA server to use."`
@ -82,6 +84,11 @@ func (a *ACME) init() error {
return err
}
a.defaultCertificate = cert
// TODO: to remove in the futurs
if len(a.StorageFile) > 0 && len(a.Storage) == 0 {
log.Warnf("ACME.StorageFile is deprecated, use ACME.Storage instead")
a.Storage = a.StorageFile
}
return nil
}
@ -160,7 +167,6 @@ func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tl
if err != nil {
return err
}
log.Debugf("buildACMEClient...")
a.client, err = a.buildACMEClient(account)
if err != nil {
return err
@ -179,7 +185,16 @@ func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tl
log.Debugf("AgreeToTOS...")
err = a.client.AgreeToTOS()
if err != nil {
return err
// Let's Encrypt Subscriber Agreement renew ?
reg, err := a.client.QueryRegistration()
if err != nil {
return err
}
account.Registration = reg
err = a.client.AgreeToTOS()
if err != nil {
log.Errorf("Error sending ACME agreement to TOS: %+v: %s", account, err.Error())
}
}
err = transaction.Commit(account)
if err != nil {
@ -302,7 +317,7 @@ func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificat
return challengeCert, nil
}
if domainCert, ok := account.DomainsCertificate.getCertificateForDomain(clientHello.ServerName); ok {
log.Debugf("ACME got domaincert %s", clientHello.ServerName)
log.Debugf("ACME got domain cert %s", clientHello.ServerName)
return domainCert.tlsCert, nil
}
if a.OnDemand {
@ -442,6 +457,22 @@ func (a *ACME) loadCertificateOnDemand(clientHello *tls.ClientHelloInfo) (*tls.C
// LoadCertificateForDomains loads certificates from ACME for given domains
func (a *ACME) LoadCertificateForDomains(domains []string) {
safe.Go(func() {
operation := func() error {
if a.client == nil {
return fmt.Errorf("ACME client still not built")
}
return nil
}
notify := func(err error, time time.Duration) {
log.Errorf("Error getting ACME client: %v, retrying in %s", err, time)
}
ebo := backoff.NewExponentialBackOff()
ebo.MaxElapsedTime = 30 * time.Second
err := backoff.RetryNotify(operation, ebo, notify)
if err != nil {
log.Errorf("Error getting ACME client: %v", err)
return
}
account := a.store.Get().(*Account)
var domain Domain
if len(domains) == 0 {

View file

@ -63,7 +63,7 @@ func TestDomainsSetAppend(t *testing.T) {
func TestCertificatesRenew(t *testing.T) {
domainsCertificates := DomainsCertificates{
lock: &sync.RWMutex{},
lock: sync.RWMutex{},
Certs: []*DomainsCertificate{
{
Domains: Domain{

View file

@ -2,23 +2,17 @@ package acme
import (
"crypto/tls"
"strings"
"sync"
"bytes"
"crypto/rsa"
"crypto/x509"
"encoding/gob"
"fmt"
"github.com/cenk/backoff"
"github.com/containous/traefik/cluster"
"github.com/containous/traefik/log"
"github.com/xenolf/lego/acme"
"time"
)
func init() {
gob.Register(rsa.PrivateKey{})
gob.Register(rsa.PublicKey{})
}
var _ acme.ChallengeProviderTimeout = (*challengeProvider)(nil)
type challengeProvider struct {
@ -34,34 +28,44 @@ func newMemoryChallengeProvider(store cluster.Store) *challengeProvider {
func (c *challengeProvider) getCertificate(domain string) (cert *tls.Certificate, exists bool) {
log.Debugf("Challenge GetCertificate %s", domain)
if !strings.HasSuffix(domain, ".acme.invalid") {
return nil, false
}
c.lock.RLock()
defer c.lock.RUnlock()
account := c.store.Get().(*Account)
if account.ChallengeCerts == nil {
return nil, false
}
if certBinary, ok := account.ChallengeCerts[domain]; ok {
cert := &tls.Certificate{}
var buffer bytes.Buffer
buffer.Write(certBinary)
dec := gob.NewDecoder(&buffer)
err := dec.Decode(cert)
if err != nil {
log.Errorf("Error unmarshaling challenge cert %s", err.Error())
return nil, false
account.Init()
var result *tls.Certificate
operation := func() error {
for _, cert := range account.ChallengeCerts {
for _, dns := range cert.certificate.Leaf.DNSNames {
if domain == dns {
result = cert.certificate
return nil
}
}
}
return cert, true
return fmt.Errorf("Cannot find challenge cert for domain %s", domain)
}
return nil, false
notify := func(err error, time time.Duration) {
log.Errorf("Error getting cert: %v, retrying in %s", err, time)
}
ebo := backoff.NewExponentialBackOff()
ebo.MaxElapsedTime = 60 * time.Second
err := backoff.RetryNotify(operation, ebo, notify)
if err != nil {
log.Errorf("Error getting cert: %v", err)
return nil, false
}
return result, true
}
func (c *challengeProvider) Present(domain, token, keyAuth string) error {
log.Debugf("Challenge Present %s", domain)
cert, _, err := acme.TLSSNI01ChallengeCert(keyAuth)
if err != nil {
return err
}
cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
cert, _, err := TLSSNI01ChallengeCert(keyAuth)
if err != nil {
return err
}
@ -74,18 +78,9 @@ func (c *challengeProvider) Present(domain, token, keyAuth string) error {
}
account := object.(*Account)
if account.ChallengeCerts == nil {
account.ChallengeCerts = map[string][]byte{}
}
for i := range cert.Leaf.DNSNames {
var buffer bytes.Buffer
enc := gob.NewEncoder(&buffer)
err := enc.Encode(cert)
if err != nil {
return err
}
account.ChallengeCerts[cert.Leaf.DNSNames[i]] = buffer.Bytes()
log.Debugf("Challenge Present cert: %s", cert.Leaf.DNSNames[i])
account.ChallengeCerts = map[string]*ChallengeCert{}
}
account.ChallengeCerts[domain] = &cert
return transaction.Commit(account)
}

View file

@ -1,6 +1,8 @@
package acme
import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
@ -76,3 +78,48 @@ func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain strin
return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
}
// TLSSNI01ChallengeCert returns a certificate and target domain for the `tls-sni-01` challenge
func TLSSNI01ChallengeCert(keyAuth string) (ChallengeCert, string, error) {
// generate a new RSA key for the certificates
var tempPrivKey crypto.PrivateKey
tempPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return ChallengeCert{}, "", err
}
rsaPrivKey := tempPrivKey.(*rsa.PrivateKey)
rsaPrivPEM := pemEncode(rsaPrivKey)
zBytes := sha256.Sum256([]byte(keyAuth))
z := hex.EncodeToString(zBytes[:sha256.Size])
domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:])
tempCertPEM, err := generatePemCert(rsaPrivKey, domain)
if err != nil {
return ChallengeCert{}, "", err
}
certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM)
if err != nil {
return ChallengeCert{}, "", err
}
return ChallengeCert{Certificate: tempCertPEM, PrivateKey: rsaPrivPEM, certificate: &certificate}, domain, nil
}
func pemEncode(data interface{}) []byte {
var pemBlock *pem.Block
switch key := data.(type) {
case *ecdsa.PrivateKey:
keyBytes, _ := x509.MarshalECPrivateKey(key)
pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}
case *rsa.PrivateKey:
pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
break
case *x509.CertificateRequest:
pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw}
break
case []byte:
pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.([]byte))}
}
return pem.EncodeToMemory(pemBlock)
}

View file

@ -51,17 +51,6 @@ func (s *LocalStore) Load() (cluster.Object, error) {
return account, nil
}
// func (s *LocalStore) saveAccount(account *Account) error {
// s.storageLock.Lock()
// defer s.storageLock.Unlock()
// // write account to file
// data, err := json.MarshalIndent(account, "", " ")
// if err != nil {
// return err
// }
// return ioutil.WriteFile(s.file, data, 0644)
// }
// Begin creates a transaction with the KV store.
func (s *LocalStore) Begin() (cluster.Transaction, cluster.Object, error) {
s.storageLock.Lock()
@ -88,7 +77,7 @@ func (t *localTransaction) Commit(object cluster.Object) error {
if err != nil {
return err
}
return ioutil.WriteFile(t.file, data, 0644)
err = ioutil.WriteFile(t.file, data, 0644)
if err != nil {
return err
}