From 68cc826519f3eb10e9003ca5982738da305eff85 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 16 May 2018 11:44:03 +0200 Subject: [PATCH] Add option to select algorithm to generate ACME certificates --- acme/account.go | 7 ++- acme/acme.go | 5 +- acme/localStore.go | 3 + configuration/configuration.go | 1 + docs/configuration/acme.md | 9 +++ .../acme/manage_acme_docker_environment.sh | 6 +- integration/acme_test.go | 63 +++++++++++++++---- .../fixtures/provideracme/acme_onhost.toml | 2 +- .../provideracme/acme_onhost_ecdsa.toml | 38 +++++++++++ .../acme_onhost_invalid_algo.toml | 38 +++++++++++ provider/acme/account.go | 25 +++++++- provider/acme/provider.go | 5 +- 12 files changed, 179 insertions(+), 23 deletions(-) create mode 100644 integration/fixtures/provideracme/acme_onhost_ecdsa.toml create mode 100644 integration/fixtures/provideracme/acme_onhost_invalid_algo.toml diff --git a/acme/account.go b/acme/account.go index e32e68948..39efb4733 100644 --- a/acme/account.go +++ b/acme/account.go @@ -14,6 +14,7 @@ import ( "time" "github.com/containous/traefik/log" + acmeprovider "github.com/containous/traefik/provider/acme" "github.com/containous/traefik/types" acme "github.com/xenolf/lego/acmev2" ) @@ -23,6 +24,7 @@ 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 @@ -63,7 +65,9 @@ func (a *Account) Init() error { } // NewAccount creates an account -func NewAccount(email string, certs []*DomainsCertificate) (*Account, error) { +func NewAccount(email string, certs []*DomainsCertificate, keyTypeValue string) (*Account, error) { + keyType := acmeprovider.GetKeyType(keyTypeValue) + // Create a user. New accounts need an email and private key to start privateKey, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { @@ -79,6 +83,7 @@ func NewAccount(email string, certs []*DomainsCertificate) (*Account, error) { return &Account{ Email: email, PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey), + KeyType: keyType, DomainsCertificate: DomainsCertificates{Certs: domainsCerts.Certs}, ChallengeCerts: map[string]*ChallengeCert{}}, nil } diff --git a/acme/acme.go b/acme/acme.go index f32445aeb..f85b3177a 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -46,6 +46,7 @@ type ACME struct { OnHostRule bool `description:"Enable certificate generation on frontends Host rules."` CAServer string `description:"CA server to use."` EntryPoint string `description:"Entrypoint to proxy acme challenge to."` + KeyType string `description:"KeyType used for generating certificate private key. Allow value 'EC256', 'EC384', 'RSA2048', 'RSA4096', 'RSA8192'. Default to 'RSA4096'"` DNSChallenge *acmeprovider.DNSChallenge `description:"Activate DNS-01 Challenge"` HTTPChallenge *acmeprovider.HTTPChallenge `description:"Activate HTTP-01 Challenge"` DNSProvider string `description:"(Deprecated) Activate DNS-01 Challenge"` // Deprecated @@ -186,7 +187,7 @@ func (a *ACME) leadershipListener(elected bool) error { domainsCerts = account.DomainsCertificate } - account, err = NewAccount(a.Email, domainsCerts.Certs) + account, err = NewAccount(a.Email, domainsCerts.Certs, a.KeyType) if err != nil { return err } @@ -395,7 +396,7 @@ func (a *ACME) buildACMEClient(account *Account) (*acme.Client, error) { if len(a.CAServer) > 0 { caServer = a.CAServer } - client, err := acme.NewClient(caServer, account, acme.RSA4096) + client, err := acme.NewClient(caServer, account, account.KeyType) if err != nil { return nil, err } diff --git a/acme/localStore.go b/acme/localStore.go index 3cdef8a04..63397338d 100644 --- a/acme/localStore.go +++ b/acme/localStore.go @@ -64,6 +64,7 @@ func RemoveAccountV1Values(account *Account) error { account.Email = "" account.Registration = nil account.PrivateKey = nil + account.KeyType = "RSA4096" } } return nil @@ -113,6 +114,7 @@ func ConvertToNewFormat(fileName string) { PrivateKey: account.PrivateKey, Registration: account.Registration, Email: account.Email, + KeyType: account.KeyType, } var newCertificates []*acme.Certificate @@ -167,6 +169,7 @@ func FromNewToOldFormat(fileName string) (*Account, error) { PrivateKey: storeAccount.PrivateKey, Registration: storeAccount.Registration, DomainsCertificate: DomainsCertificates{}, + KeyType: storeAccount.KeyType, } } diff --git a/configuration/configuration.go b/configuration/configuration.go index ded9cddc6..85e4df1ff 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -381,6 +381,7 @@ func (gc *GlobalConfiguration) InitACMEProvider() *acmeprovider.Provider { if gc.Cluster == nil { provider := &acmeprovider.Provider{} provider.Configuration = &acmeprovider.Configuration{ + KeyType: gc.ACME.KeyType, OnHostRule: gc.ACME.OnHostRule, OnDemand: gc.ACME.OnDemand, Email: gc.ACME.Email, diff --git a/docs/configuration/acme.md b/docs/configuration/acme.md index 95f69b77f..092d736d5 100644 --- a/docs/configuration/acme.md +++ b/docs/configuration/acme.md @@ -86,6 +86,15 @@ entryPoint = "https" # # caServer = "https://acme-staging-v02.api.letsencrypt.org/directory" +# KeyType to use. +# +# Optional +# Default: "RSA4096" +# +# Available values : "EC256", "EC384", "RSA2048", "RSA4096", "RSA8192" +# +# KeyType = "RSA4096" + # Domains list. # Only domains defined here can generate wildcard certificates. # diff --git a/examples/acme/manage_acme_docker_environment.sh b/examples/acme/manage_acme_docker_environment.sh index a95483c9e..e007665b5 100755 --- a/examples/acme/manage_acme_docker_environment.sh +++ b/examples/acme/manage_acme_docker_environment.sh @@ -9,7 +9,7 @@ readonly doc_file=$basedir"/docker-compose.yml" down_environment() { echo "STOP Docker environment" ! docker-compose -f $doc_file down -v &>/dev/null && \ - echo "[ERROR] Impossible to stop the Docker environment" && exit 11 + echo "[ERROR] Unable to stop the Docker environment" && exit 11 } # Create and start Docker-compose environment or subpart of its services (if services are listed) @@ -17,7 +17,7 @@ down_environment() { up_environment() { echo "START Docker environment" ! docker-compose -f $doc_file up -d $@ &>/dev/null && \ - echo "[ERROR] Impossible to start Docker environment" && exit 21 + echo "[ERROR] Unable to start Docker environment" && exit 21 } # Init the environment : get IP address and create needed files @@ -40,7 +40,7 @@ start_boulder() { sleep 5 let waiting_counter-=1 if [[ $waiting_counter -eq 0 ]]; then - echo "[ERROR] Impossible to start boulder container in the allowed time, the Docker environment will be stopped" + echo "[ERROR] Unable to start boulder container in the allowed time, the Docker environment will be stopped" down_environment exit 41 fi diff --git a/integration/acme_test.go b/integration/acme_test.go index 395fc7f6c..e42473ccd 100644 --- a/integration/acme_test.go +++ b/integration/acme_test.go @@ -2,6 +2,7 @@ package integration import ( "crypto/tls" + "crypto/x509" "fmt" "net/http" "os" @@ -24,6 +25,7 @@ type AcmeTestCase struct { onDemand bool traefikConfFilePath string domainToCheck string + algorithm x509.PublicKeyAlgorithm } const ( @@ -60,7 +62,8 @@ func (s *AcmeSuite) TestACMEProviderAtStart(c *check.C) { testCase := AcmeTestCase{ traefikConfFilePath: "fixtures/provideracme/acme.toml", onDemand: false, - domainToCheck: acmeDomain} + domainToCheck: acmeDomain, + algorithm: x509.RSA} s.retrieveAcmeCertificate(c, testCase) } @@ -70,7 +73,8 @@ func (s *AcmeSuite) TestACMEProviderAtStartInSAN(c *check.C) { testCase := AcmeTestCase{ traefikConfFilePath: "fixtures/provideracme/acme_insan.toml", onDemand: false, - domainToCheck: "acme.wtf"} + domainToCheck: "acme.wtf", + algorithm: x509.RSA} s.retrieveAcmeCertificate(c, testCase) } @@ -80,7 +84,30 @@ func (s *AcmeSuite) TestACMEProviderOnHost(c *check.C) { testCase := AcmeTestCase{ traefikConfFilePath: "fixtures/provideracme/acme_onhost.toml", onDemand: false, - domainToCheck: acmeDomain} + domainToCheck: acmeDomain, + algorithm: x509.RSA} + + s.retrieveAcmeCertificate(c, testCase) +} + +// Test ACME provider with certificate at start ECDSA algo +func (s *AcmeSuite) TestACMEProviderOnHostECDSA(c *check.C) { + testCase := AcmeTestCase{ + traefikConfFilePath: "fixtures/provideracme/acme_onhost_ecdsa.toml", + onDemand: false, + domainToCheck: acmeDomain, + algorithm: x509.ECDSA} + + s.retrieveAcmeCertificate(c, testCase) +} + +// Test ACME provider with certificate at start invalid algo default RSA +func (s *AcmeSuite) TestACMEProviderOnHostInvalidAlgo(c *check.C) { + testCase := AcmeTestCase{ + traefikConfFilePath: "fixtures/provideracme/acme_onhost_invalid_algo.toml", + onDemand: false, + domainToCheck: acmeDomain, + algorithm: x509.RSA} s.retrieveAcmeCertificate(c, testCase) } @@ -90,7 +117,8 @@ func (s *AcmeSuite) TestACMEProviderOnHostWithNoACMEChallenge(c *check.C) { testCase := AcmeTestCase{ traefikConfFilePath: "fixtures/acme/no_challenge_acme.toml", onDemand: false, - domainToCheck: traefikDefaultDomain} + domainToCheck: traefikDefaultDomain, + algorithm: x509.RSA} s.retrieveAcmeCertificate(c, testCase) } @@ -100,7 +128,8 @@ func (s *AcmeSuite) TestOnDemandRetrieveAcmeCertificateHTTP01(c *check.C) { testCase := AcmeTestCase{ traefikConfFilePath: "fixtures/acme/acme_http01.toml", onDemand: true, - domainToCheck: acmeDomain} + domainToCheck: acmeDomain, + algorithm: x509.RSA} s.retrieveAcmeCertificate(c, testCase) } @@ -110,7 +139,8 @@ func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificateHTTP01(c *check.C) { testCase := AcmeTestCase{ traefikConfFilePath: "fixtures/acme/acme_http01.toml", onDemand: false, - domainToCheck: acmeDomain} + domainToCheck: acmeDomain, + algorithm: x509.RSA} s.retrieveAcmeCertificate(c, testCase) } @@ -120,7 +150,8 @@ func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificateHTTP01WithPath(c *check testCase := AcmeTestCase{ traefikConfFilePath: "fixtures/acme/acme_http01_web.toml", onDemand: false, - domainToCheck: acmeDomain} + domainToCheck: acmeDomain, + algorithm: x509.RSA} s.retrieveAcmeCertificate(c, testCase) } @@ -130,7 +161,8 @@ func (s *AcmeSuite) TestOnDemandRetrieveAcmeCertificateWithWildcard(c *check.C) testCase := AcmeTestCase{ traefikConfFilePath: "fixtures/acme/acme_provided.toml", onDemand: true, - domainToCheck: wildcardDomain} + domainToCheck: wildcardDomain, + algorithm: x509.RSA} s.retrieveAcmeCertificate(c, testCase) } @@ -140,7 +172,8 @@ func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificateWithWildcard(c *check.C testCase := AcmeTestCase{ traefikConfFilePath: "fixtures/acme/acme_provided.toml", onDemand: false, - domainToCheck: wildcardDomain} + domainToCheck: wildcardDomain, + algorithm: x509.RSA} s.retrieveAcmeCertificate(c, testCase) } @@ -150,7 +183,8 @@ func (s *AcmeSuite) TestOnDemandRetrieveAcmeCertificateWithDynamicWildcard(c *ch testCase := AcmeTestCase{ traefikConfFilePath: "fixtures/acme/acme_provided_dynamic.toml", onDemand: true, - domainToCheck: wildcardDomain} + domainToCheck: wildcardDomain, + algorithm: x509.RSA} s.retrieveAcmeCertificate(c, testCase) } @@ -160,7 +194,8 @@ func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificateWithDynamicWildcard(c * testCase := AcmeTestCase{ traefikConfFilePath: "fixtures/acme/acme_provided_dynamic.toml", onDemand: false, - domainToCheck: wildcardDomain} + domainToCheck: wildcardDomain, + algorithm: x509.RSA} s.retrieveAcmeCertificate(c, testCase) } @@ -181,8 +216,9 @@ func (s *AcmeSuite) TestNoValidLetsEncryptServer(c *check.C) { // Doing an HTTPS request and test the response certificate func (s *AcmeSuite) retrieveAcmeCertificate(c *check.C, testCase AcmeTestCase) { file := s.adaptFile(c, testCase.traefikConfFilePath, struct { - BoulderHost string - OnDemand, OnHostRule bool + BoulderHost string + OnDemand bool + OnHostRule bool }{ BoulderHost: s.boulderIP, OnDemand: testCase.onDemand, @@ -251,4 +287,5 @@ func (s *AcmeSuite) retrieveAcmeCertificate(c *check.C, testCase AcmeTestCase) { c.Assert(resp.StatusCode, checker.Equals, http.StatusOK) // Check Domain into response certificate c.Assert(resp.TLS.PeerCertificates[0].Subject.CommonName, checker.Equals, testCase.domainToCheck) + c.Assert(resp.TLS.PeerCertificates[0].PublicKeyAlgorithm, checker.Equals, testCase.algorithm) } diff --git a/integration/fixtures/provideracme/acme_onhost.toml b/integration/fixtures/provideracme/acme_onhost.toml index 26992769d..04ac1e9ad 100644 --- a/integration/fixtures/provideracme/acme_onhost.toml +++ b/integration/fixtures/provideracme/acme_onhost.toml @@ -12,7 +12,7 @@ defaultEntryPoints = ["http", "https"] [acme] email = "test@traefik.io" - storage = "/tmp/acme.jsonl" + storage = "/tmp/acme.json" entryPoint = "https" onDemand = {{.OnDemand}} onHostRule = {{.OnHostRule}} diff --git a/integration/fixtures/provideracme/acme_onhost_ecdsa.toml b/integration/fixtures/provideracme/acme_onhost_ecdsa.toml new file mode 100644 index 000000000..9a1f05373 --- /dev/null +++ b/integration/fixtures/provideracme/acme_onhost_ecdsa.toml @@ -0,0 +1,38 @@ +logLevel = "DEBUG" + +defaultEntryPoints = ["http", "https"] + +[entryPoints] + [entryPoints.http] + address = ":5002" + [entryPoints.https] + address = ":5001" + [entryPoints.https.tls] + + +[acme] + email = "test@traefik.io" + storage = "/tmp/acme.json" + entryPoint = "https" + onDemand = {{.OnDemand}} + onHostRule = {{.OnHostRule}} + caServer = "http://{{.BoulderHost}}:4001/directory" + keyType = "EC384" + [acme.httpChallenge] + entryPoint="http" + +[api] + +[file] + +[backends] + [backends.backend] + [backends.backend.servers.server1] + url = "http://127.0.0.1:9010" + weight = 1 + +[frontends] + [frontends.frontend] + backend = "backend" + [frontends.frontend.routes.test] + rule = "Host:traefik.acme.wtf" \ No newline at end of file diff --git a/integration/fixtures/provideracme/acme_onhost_invalid_algo.toml b/integration/fixtures/provideracme/acme_onhost_invalid_algo.toml new file mode 100644 index 000000000..3b3f389bb --- /dev/null +++ b/integration/fixtures/provideracme/acme_onhost_invalid_algo.toml @@ -0,0 +1,38 @@ +logLevel = "DEBUG" + +defaultEntryPoints = ["http", "https"] + +[entryPoints] + [entryPoints.http] + address = ":5002" + [entryPoints.https] + address = ":5001" + [entryPoints.https.tls] + + +[acme] + email = "test@traefik.io" + storage = "/tmp/acme.json" + entryPoint = "https" + onDemand = {{.OnDemand}} + onHostRule = {{.OnHostRule}} + caServer = "http://{{.BoulderHost}}:4001/directory" + keyType = "INVALID" + [acme.httpChallenge] + entryPoint="http" + +[api] + +[file] + +[backends] + [backends.backend] + [backends.backend.servers.server1] + url = "http://127.0.0.1:9010" + weight = 1 + +[frontends] + [frontends.frontend] + backend = "backend" + [frontends.frontend.routes.test] + rule = "Host:traefik.acme.wtf" \ No newline at end of file diff --git a/provider/acme/account.go b/provider/acme/account.go index 4af3ad0b8..f9bf2dc23 100644 --- a/provider/acme/account.go +++ b/provider/acme/account.go @@ -15,6 +15,7 @@ type Account struct { Email string Registration *acme.RegistrationResource PrivateKey []byte + KeyType acme.KeyType } const ( @@ -23,7 +24,9 @@ const ( ) // NewAccount creates an account -func NewAccount(email string) (*Account, error) { +func NewAccount(email string, keyTypeValue string) (*Account, error) { + keyType := GetKeyType(keyTypeValue) + // Create a user. New accounts need an email and private key to start privateKey, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { @@ -33,6 +36,7 @@ func NewAccount(email string) (*Account, error) { return &Account{ Email: email, PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey), + KeyType: keyType, }, nil } @@ -55,3 +59,22 @@ func (a *Account) GetPrivateKey() crypto.PrivateKey { log.Errorf("Cannot unmarshal private key %+v", a.PrivateKey) return nil } + +// GetKeyType used to determine which algo to used +func GetKeyType(value string) acme.KeyType { + switch value { + case "EC256": + return acme.EC256 + case "EC384": + return acme.EC384 + case "RSA2048": + return acme.RSA2048 + case "RSA4096": + return acme.RSA4096 + case "RSA8192": + return acme.RSA8192 + default: + log.Warnf("Unable to determine key type value %s. Use %s as default value", value, acme.RSA4096) + return acme.RSA4096 + } +} diff --git a/provider/acme/provider.go b/provider/acme/provider.go index 46e0c0a0b..c8d64eede 100644 --- a/provider/acme/provider.go +++ b/provider/acme/provider.go @@ -39,6 +39,7 @@ type Configuration struct { CAServer string `description:"CA server to use."` Storage string `description:"Storage to use."` EntryPoint string `description:"EntryPoint to use."` + KeyType string `description:"KeyType used for generating certificate private key. Allow value 'EC256', 'EC384', 'RSA2048', 'RSA4096', 'RSA8192'. Default to 'RSA4096'"` OnHostRule bool `description:"Enable certificate generation on frontends Host rules."` OnDemand bool `description:"Enable on demand certificate generation. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."` // Deprecated DNSChallenge *DNSChallenge `description:"Activate DNS-01 Challenge"` @@ -116,7 +117,7 @@ func (p *Provider) init() error { func (p *Provider) initAccount() (*Account, error) { if p.account == nil || len(p.account.Email) == 0 { var err error - p.account, err = NewAccount(p.Email) + p.account, err = NewAccount(p.Email, p.KeyType) if err != nil { return nil, err } @@ -246,7 +247,7 @@ func (p *Provider) getClient() (*acme.Client, error) { caServer = p.CAServer } log.Debugf(caServer) - client, err := acme.NewClient(caServer, account, acme.RSA4096) + client, err := acme.NewClient(caServer, account, account.KeyType) if err != nil { return nil, err }