From 16bb9b68365cece816bf6fe05e67e16b44c985de Mon Sep 17 00:00:00 2001 From: NicoMen Date: Mon, 26 Mar 2018 14:12:03 +0200 Subject: [PATCH] ACME V2 Integration --- Gopkg.lock | 17 +- Gopkg.toml | 2 +- README.md | 2 +- acme/account.go | 13 +- acme/acme.go | 212 ++- acme/acme_test.go | 93 +- acme/challenge_http_provider.go | 2 +- acme/localStore.go | 68 +- cmd/storeconfig/storeconfig.go | 66 +- docs/configuration/acme.md | 85 +- docs/index.md | 2 +- docs/user-guide/examples.md | 41 +- examples/acme/acme.toml | 2 +- examples/acme/docker-compose.yml | 33 +- .../acme/manage_acme_docker_environment.sh | 2 +- examples/cluster/docker-compose.yml | 326 ++--- .../manage_cluster_docker_environment.sh | 12 +- examples/cluster/traefik.toml.tmpl | 2 +- integration/acme_test.go | 2 +- integration/fixtures/acme/acme_http01.toml | 2 +- .../fixtures/acme/acme_http01_web.toml | 2 +- integration/fixtures/acme/acme_provided.toml | 2 +- .../fixtures/acme/acme_provided_dynamic.toml | 2 +- .../fixtures/acme/no_challenge_acme.toml | 2 +- integration/fixtures/acme/wrong_acme.toml | 2 +- integration/fixtures/provideracme/acme.toml | 2 +- .../fixtures/provideracme/acme_insan.toml | 2 +- .../fixtures/provideracme/acme_onhost.toml | 2 +- integration/resources/compose/boulder.yml | 21 +- provider/acme/account.go | 7 +- provider/acme/challenge.go | 2 +- provider/acme/local_store.go | 12 + provider/acme/provider.go | 204 ++- provider/acme/provider_test.go | 342 +++++ types/domain_test.go | 90 ++ types/domains.go | 17 + .../xenolf/lego/acmev2/challenges.go | 13 + .../github.com/xenolf/lego/acmev2/client.go | 801 +++++++++++ .../github.com/xenolf/lego/acmev2/crypto.go | 343 +++++ .../xenolf/lego/acmev2/dns_challenge.go | 309 +++++ .../lego/acmev2/dns_challenge_manual.go | 53 + vendor/github.com/xenolf/lego/acmev2/error.go | 78 ++ vendor/github.com/xenolf/lego/acmev2/http.go | 160 +++ .../xenolf/lego/acmev2/http_challenge.go | 41 + .../lego/acmev2/http_challenge_server.go | 79 ++ vendor/github.com/xenolf/lego/acmev2/jws.go | 138 ++ .../github.com/xenolf/lego/acmev2/messages.go | 103 ++ .../xenolf/lego/acmev2/pop_challenge.go | 1 + .../github.com/xenolf/lego/acmev2/provider.go | 28 + vendor/github.com/xenolf/lego/acmev2/utils.go | 29 + vendor/gopkg.in/square/go-jose.v2/LICENSE | 202 +++ .../gopkg.in/square/go-jose.v2/asymmetric.go | 591 ++++++++ .../square/go-jose.v2/cipher/cbc_hmac.go | 196 +++ .../square/go-jose.v2/cipher/concat_kdf.go | 75 ++ .../square/go-jose.v2/cipher/ecdh_es.go | 62 + .../square/go-jose.v2/cipher/key_wrap.go | 109 ++ vendor/gopkg.in/square/go-jose.v2/crypter.go | 510 +++++++ vendor/gopkg.in/square/go-jose.v2/doc.go | 27 + vendor/gopkg.in/square/go-jose.v2/encoding.go | 178 +++ .../gopkg.in/square/go-jose.v2/json/LICENSE | 27 + .../gopkg.in/square/go-jose.v2/json/decode.go | 1183 ++++++++++++++++ .../gopkg.in/square/go-jose.v2/json/encode.go | 1197 +++++++++++++++++ .../gopkg.in/square/go-jose.v2/json/indent.go | 141 ++ .../square/go-jose.v2/json/scanner.go | 623 +++++++++ .../gopkg.in/square/go-jose.v2/json/stream.go | 480 +++++++ .../gopkg.in/square/go-jose.v2/json/tags.go | 44 + vendor/gopkg.in/square/go-jose.v2/jwe.go | 291 ++++ vendor/gopkg.in/square/go-jose.v2/jwk.go | 556 ++++++++ vendor/gopkg.in/square/go-jose.v2/jws.go | 321 +++++ vendor/gopkg.in/square/go-jose.v2/shared.go | 417 ++++++ vendor/gopkg.in/square/go-jose.v2/signing.go | 343 +++++ .../gopkg.in/square/go-jose.v2/symmetric.go | 360 +++++ 72 files changed, 11401 insertions(+), 403 deletions(-) create mode 100644 provider/acme/provider_test.go create mode 100644 types/domain_test.go create mode 100644 vendor/github.com/xenolf/lego/acmev2/challenges.go create mode 100644 vendor/github.com/xenolf/lego/acmev2/client.go create mode 100644 vendor/github.com/xenolf/lego/acmev2/crypto.go create mode 100644 vendor/github.com/xenolf/lego/acmev2/dns_challenge.go create mode 100644 vendor/github.com/xenolf/lego/acmev2/dns_challenge_manual.go create mode 100644 vendor/github.com/xenolf/lego/acmev2/error.go create mode 100644 vendor/github.com/xenolf/lego/acmev2/http.go create mode 100644 vendor/github.com/xenolf/lego/acmev2/http_challenge.go create mode 100644 vendor/github.com/xenolf/lego/acmev2/http_challenge_server.go create mode 100644 vendor/github.com/xenolf/lego/acmev2/jws.go create mode 100644 vendor/github.com/xenolf/lego/acmev2/messages.go create mode 100644 vendor/github.com/xenolf/lego/acmev2/pop_challenge.go create mode 100644 vendor/github.com/xenolf/lego/acmev2/provider.go create mode 100644 vendor/github.com/xenolf/lego/acmev2/utils.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/LICENSE create mode 100644 vendor/gopkg.in/square/go-jose.v2/asymmetric.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/cipher/cbc_hmac.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/cipher/concat_kdf.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/cipher/ecdh_es.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/cipher/key_wrap.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/crypter.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/doc.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/encoding.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/json/LICENSE create mode 100644 vendor/gopkg.in/square/go-jose.v2/json/decode.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/json/encode.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/json/indent.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/json/scanner.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/json/stream.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/json/tags.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/jwe.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/jwk.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/jws.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/shared.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/signing.go create mode 100644 vendor/gopkg.in/square/go-jose.v2/symmetric.go diff --git a/Gopkg.lock b/Gopkg.lock index 49c972af5..6d55659e5 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1213,10 +1213,11 @@ revision = "0c8571ac0ce161a5feb57375a9cdf148c98c0f70" [[projects]] - branch = "master" + branch = "acmev2" name = "github.com/xenolf/lego" packages = [ "acme", + "acmev2", "providers/dns", "providers/dns/auroradns", "providers/dns/azure", @@ -1243,7 +1244,7 @@ "providers/dns/route53", "providers/dns/vultr" ] - revision = "06a8e7c475c03ef8d4773284ac63357d2810601b" + revision = "a149e7d6506feb4003da7093cbf818c6b75ed4a4" [[projects]] branch = "master" @@ -1423,6 +1424,16 @@ revision = "aa2e30fdd1fe9dd3394119af66451ae790d50e0d" version = "v1.1.0" +[[projects]] + name = "gopkg.in/square/go-jose.v2" + packages = [ + ".", + "cipher", + "json" + ] + revision = "6ee92191fea850cdcab9a18867abf5f521cdbadb" + version = "v2.1.4" + [[projects]] name = "gopkg.in/yaml.v2" packages = ["."] @@ -1632,6 +1643,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "ab328aeda9dbd2c4dc87061c25dbfbd151dbdc5946b6f512f676b39bebba8d8e" + inputs-digest = "5643c4ca177618882a194021e8894c3dc32950da646048883151bee925416771" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 571a70995..4db5fc2ed 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -181,7 +181,7 @@ name = "github.com/vulcand/oxy" [[constraint]] - branch = "master" + branch = "acmev2" name = "github.com/xenolf/lego" [[constraint]] diff --git a/README.md b/README.md index 7a910885e..ea2074957 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ _(But if you'd rather configure some of your routes manually, Træfik supports t - Continuously updates its configuration (No restarts!) - Supports multiple load balancing algorithms -- Provides HTTPS to your microservices by leveraging [Let's Encrypt](https://letsencrypt.org) +- Provides HTTPS to your microservices by leveraging [Let's Encrypt](https://letsencrypt.org) (wildcard certificates support) - Circuit breakers, retry - High Availability with cluster mode (beta) - See the magic through its clean web UI diff --git a/acme/account.go b/acme/account.go index 4730641b2..3215257c8 100644 --- a/acme/account.go +++ b/acme/account.go @@ -15,7 +15,7 @@ import ( "github.com/containous/traefik/log" "github.com/containous/traefik/types" - "github.com/xenolf/lego/acme" + acme "github.com/xenolf/lego/acmev2" ) // Account is used to store lets encrypt registration info @@ -63,15 +63,14 @@ func (a *Account) Init() error { } // NewAccount creates an account -func NewAccount(email string) (*Account, error) { +func NewAccount(email string, certs []*DomainsCertificate) (*Account, error) { // 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: []*DomainsCertificate{}} - + domainsCerts := DomainsCertificates{Certs: certs} err = domainsCerts.Init() if err != nil { return nil, err @@ -211,7 +210,6 @@ func (dc *DomainsCertificates) addCertificateForDomains(acmeCert *Certificate, d cert := DomainsCertificate{Domains: domain, Certificate: acmeCert, tlsCert: &tlsCert} dc.Certs = append(dc.Certs, &cert) - return &cert, nil } @@ -220,10 +218,7 @@ func (dc *DomainsCertificates) getCertificateForDomain(domainToFind string) (*Do defer dc.lock.RUnlock() for _, domainsCertificate := range dc.Certs { - domains := []string{domainsCertificate.Domains.Main} - domains = append(domains, domainsCertificate.Domains.SANs...) - - for _, domain := range domains { + for _, domain := range domainsCertificate.Domains.ToStrArray() { if domain == domainToFind { return domainsCertificate, true } diff --git a/acme/acme.go b/acme/acme.go index 6188fc12a..ec4da2ba6 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -10,6 +10,7 @@ import ( "net" "net/http" "os" + "reflect" "regexp" "strings" "time" @@ -26,7 +27,7 @@ import ( "github.com/containous/traefik/tls/generate" "github.com/containous/traefik/types" "github.com/eapache/channels" - "github.com/xenolf/lego/acme" + acme "github.com/xenolf/lego/acmev2" "github.com/xenolf/lego/providers/dns" ) @@ -184,20 +185,30 @@ func (a *ACME) leadershipListener(elected bool) error { if err != nil { return err } + transaction, object, err := a.store.Begin() if err != nil { return err } + account := object.(*Account) account.Init() + var needRegister bool if account == nil || len(account.Email) == 0 { - account, err = NewAccount(a.Email) + domainsCerts := DomainsCertificates{Certs: []*DomainsCertificate{}} + if account != nil { + domainsCerts = account.DomainsCertificate + } + + account, err = NewAccount(a.Email, domainsCerts.Certs) if err != nil { return err } + needRegister = true } + a.client, err = a.buildACMEClient(account) if err != nil { return err @@ -205,29 +216,15 @@ func (a *ACME) leadershipListener(elected bool) error { if needRegister { // New users will need to register; be sure to save it log.Debug("Register...") - reg, err := a.client.Register() + + reg, err := a.client.Register(true) if err != nil { return err } + account.Registration = reg } - // The client has a URL to the current Let's Encrypt Subscriber - // Agreement. The user will need to agree to it. - log.Debug("AgreeToTOS...") - err = a.client.AgreeToTOS() - if err != nil { - log.Debug(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 { return err @@ -265,36 +262,50 @@ func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificat func (a *ACME) retrieveCertificates() { a.jobs.In() <- func() { log.Info("Retrieving ACME certificates...") - for _, domain := range a.Domains { + + a.deleteUnnecessaryDomains() + + for i := 0; i < len(a.Domains); i++ { + domain := a.Domains[i] + // check if cert isn't already loaded account := a.store.Get().(*Account) if _, exists := account.DomainsCertificate.exists(domain); !exists { - domains := []string{} + var domains []string domains = append(domains, domain.Main) domains = append(domains, domain.SANs...) + domains, err := a.getValidDomains(domains, true) + if err != nil { + log.Errorf("Error validating ACME certificate for domain %q: %s", domains, err) + continue + } + certificateResource, err := a.getDomainsCertificates(domains) if err != nil { - log.Errorf("Error getting ACME certificate for domain %s: %s", domains, err.Error()) + log.Errorf("Error getting ACME certificate for domain %q: %s", domains, err) continue } + transaction, object, err := a.store.Begin() if err != nil { - log.Errorf("Error creating ACME store transaction from domain %s: %s", domain, err.Error()) + log.Errorf("Error creating ACME store transaction from domain %q: %s", domain, err) continue } + account = object.(*Account) _, err = account.DomainsCertificate.addCertificateForDomains(certificateResource, domain) if err != nil { - log.Errorf("Error adding ACME certificate for domain %s: %s", domains, err.Error()) + log.Errorf("Error adding ACME certificate for domain %q: %s", domains, err) continue } if err = transaction.Commit(account); err != nil { - log.Errorf("Error Saving ACME account %+v: %s", account, err.Error()) + log.Errorf("Error Saving ACME account %+v: %s", account, err) continue } } } + log.Info("Retrieved ACME certificates") } } @@ -395,7 +406,7 @@ func dnsOverrideDelay(delay flaeg.Duration) error { func (a *ACME) buildACMEClient(account *Account) (*acme.Client, error) { log.Debug("Building ACME client...") - caServer := "https://acme-v01.api.letsencrypt.org/directory" + caServer := "https://acme-v02.api.letsencrypt.org/directory" if len(a.CAServer) > 0 { caServer = a.CAServer } @@ -418,11 +429,11 @@ func (a *ACME) buildACMEClient(account *Account) (*acme.Client, error) { return nil, err } - client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01}) + client.ExcludeChallenges([]acme.Challenge{acme.HTTP01}) err = client.SetChallengeProvider(acme.DNS01, provider) } else if a.HTTPChallenge != nil && len(a.HTTPChallenge.EntryPoint) > 0 { log.Debug("Using HTTP Challenge provider.") - client.ExcludeChallenges([]acme.Challenge{acme.DNS01, acme.TLSSNI01}) + client.ExcludeChallenges([]acme.Challenge{acme.DNS01}) a.challengeHTTPProvider = &challengeHTTPProvider{store: a.store} err = client.SetChallengeProvider(acme.HTTP01, a.challengeHTTPProvider) } else { @@ -467,13 +478,12 @@ func (a *ACME) LoadCertificateForDomains(domains []string) { a.jobs.In() <- func() { log.Debugf("LoadCertificateForDomains %v...", domains) - if len(domains) == 0 { - // no domain + domains, err := a.getValidDomains(domains, false) + if err != nil { + log.Errorf("Error getting valid domain: %v", err) return } - domains = fun.Map(types.CanonicalDomain, domains).([]string) - operation := func() error { if a.client == nil { return errors.New("ACME client still not built") @@ -485,7 +495,7 @@ func (a *ACME) LoadCertificateForDomains(domains []string) { } ebo := backoff.NewExponentialBackOff() ebo.MaxElapsedTime = 30 * time.Second - err := backoff.RetryNotify(safe.OperationWithRecover(operation), ebo, notify) + err = backoff.RetryNotify(safe.OperationWithRecover(operation), ebo, notify) if err != nil { log.Errorf("Error getting ACME client: %v", err) return @@ -547,7 +557,7 @@ func searchProvidedCertificateForDomains(domain string, certs map[string]*tls.Ce for certDomains := range certs { domainCheck := false for _, certDomain := range strings.Split(certDomains, ",") { - selector := "^" + strings.Replace(certDomain, "*.", "[^\\.]*\\.?", -1) + "$" + selector := "^" + strings.Replace(certDomain, "*.", "[^\\.]*\\.", -1) + "$" domainCheck, _ = regexp.MatchString(selector, domain) if domainCheck { break @@ -586,31 +596,25 @@ func (a *ACME) getUncheckedDomains(domains []string, account *Account) []string } } + // Get Configuration Domains + for i := 0; i < len(a.Domains); i++ { + allCerts[a.Domains[i].Main] = &tls.Certificate{} + for _, san := range a.Domains[i].SANs { + allCerts[san] = &tls.Certificate{} + } + } + return searchUncheckedDomains(domains, allCerts) } func searchUncheckedDomains(domains []string, certs map[string]*tls.Certificate) []string { - uncheckedDomains := []string{} + var uncheckedDomains []string for _, domainToCheck := range domains { - domainCheck := false - for certDomains := range certs { - domainCheck = false - for _, certDomain := range strings.Split(certDomains, ",") { - // Use regex to test for provided certs that might have been added into TLSConfig - selector := "^" + strings.Replace(certDomain, "*.", "[^\\.]*\\.?", -1) + "$" - domainCheck, _ = regexp.MatchString(selector, domainToCheck) - if domainCheck { - break - } - } - if domainCheck { - break - } - } - if !domainCheck { + if !isDomainAlreadyChecked(domainToCheck, certs) { uncheckedDomains = append(uncheckedDomains, domainToCheck) } } + if len(uncheckedDomains) == 0 { log.Debugf("No ACME certificate to generate for domains %q.", domains) } else { @@ -646,3 +650,107 @@ func (a *ACME) runJobs() { } }) } + +// getValidDomains checks if given domain is allowed to generate a ACME certificate and return it +func (a *ACME) getValidDomains(domains []string, wildcardAllowed bool) ([]string, error) { + if len(domains) == 0 || (len(domains) == 1 && len(domains[0]) == 0) { + return nil, errors.New("unable to generate a certificate when no domain is given") + } + + if strings.HasPrefix(domains[0], "*") { + if !wildcardAllowed { + return nil, fmt.Errorf("unable to generate a wildcard certificate for domain %q from a 'Host' rule", strings.Join(domains, ",")) + } + + if a.DNSChallenge == nil && len(a.DNSProvider) == 0 { + return nil, fmt.Errorf("unable to generate a wildcard certificate for domain %q : ACME needs a DNSChallenge", strings.Join(domains, ",")) + } + + if len(domains) > 1 { + return nil, fmt.Errorf("unable to generate a wildcard certificate for domain %q : SANs are not allowed", strings.Join(domains, ",")) + } + } else { + for _, san := range domains[1:] { + if strings.HasPrefix(san, "*") { + return nil, fmt.Errorf("unable to generate a certificate in ACME provider for domains %q: SANs can not be a wildcard domain", strings.Join(domains, ",")) + } + } + } + + domains = fun.Map(types.CanonicalDomain, domains).([]string) + return domains, nil +} + +func isDomainAlreadyChecked(domainToCheck string, existentDomains map[string]*tls.Certificate) bool { + for certDomains := range existentDomains { + for _, certDomain := range strings.Split(certDomains, ",") { + // Use regex to test for provided existentDomains that might have been added into TLSConfig + selector := "^" + strings.Replace(certDomain, "*.", "[^\\.]*\\.", -1) + "$" + domainCheck, err := regexp.MatchString(selector, domainToCheck) + if err != nil { + log.Errorf("Unable to compare %q and %q : %s", domainToCheck, certDomain, err) + continue + } + + if domainCheck { + return true + } + } + } + return false +} + +// deleteUnnecessaryDomains deletes from the configuration : +// - Duplicated domains +// - Domains which are checked by wildcard domain +func (a *ACME) deleteUnnecessaryDomains() { + var newDomains []types.Domain + + for idxDomainToCheck, domainToCheck := range a.Domains { + keepDomain := true + + for idxDomain, domain := range a.Domains { + if idxDomainToCheck == idxDomain { + continue + } + + if reflect.DeepEqual(domain, domainToCheck) { + if idxDomainToCheck > idxDomain { + log.Warnf("The domain %v is duplicated in the configuration but will be process by ACME only once.", domainToCheck) + keepDomain = false + } + break + } else if strings.HasPrefix(domain.Main, "*") && domain.SANs == nil { + // Check if domains can be validated by the wildcard domain + + var newDomainsToCheck []string + + // Check if domains can be validated by the wildcard domain + domainsMap := make(map[string]*tls.Certificate) + domainsMap[domain.Main] = &tls.Certificate{} + + for _, domainProcessed := range domainToCheck.ToStrArray() { + if isDomainAlreadyChecked(domainProcessed, domainsMap) { + log.Warnf("Domain %q will not be processed by ACME because it is validated by the wildcard %q", domainProcessed, domain.Main) + continue + } + newDomainsToCheck = append(newDomainsToCheck, domainProcessed) + } + + // Delete the domain if both Main and SANs can be validated by the wildcard domain + // otherwise keep the unchecked values + if newDomainsToCheck == nil { + keepDomain = false + break + } + domainToCheck.Set(newDomainsToCheck) + } + } + + if keepDomain { + newDomains = append(newDomains, domainToCheck) + } + } + + a.Domains = newDomains +} diff --git a/acme/acme_test.go b/acme/acme_test.go index 4ecc77c5f..39ef373cb 100644 --- a/acme/acme_test.go +++ b/acme/acme_test.go @@ -14,7 +14,7 @@ import ( "github.com/containous/traefik/tls/generate" "github.com/containous/traefik/types" "github.com/stretchr/testify/assert" - "github.com/xenolf/lego/acme" + acme "github.com/xenolf/lego/acmev2" ) func TestDomainsSet(t *testing.T) { @@ -299,10 +299,15 @@ llJh9MC0svjevGtNlxJoE3lmEQIhAKXy1wfZ32/XtcrnENPvi6lzxI0T94X7s5pP3aCoPPoJAiEAl cijFkALeQp/qyeXdFld2v9gUN3eCgljgcl0QweRoIc=---`) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{ -"new-authz": "https://foo/acme/new-authz", -"new-cert": "https://foo/acme/new-cert", -"new-reg": "https://foo/acme/new-reg", -"revoke-cert": "https://foo/acme/revoke-cert" + "GPHhmRVEDas": "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417", + "keyChange": "https://foo/acme/key-change", + "meta": { + "termsOfService": "https://boulder:4431/terms/v7" + }, + "newAccount": "https://foo/acme/new-acct", + "newNonce": "https://foo/acme/new-nonce", + "newOrder": "https://foo/acme/new-order", + "revokeCert": "https://foo/acme/revoke-cert" }`)) })) defer ts.Close() @@ -361,3 +366,81 @@ func TestAcme_getProvidedCertificate(t *testing.T) { certificate = a.getProvidedCertificate(domain) assert.Nil(t, certificate) } + +func TestAcme_getValidDomain(t *testing.T) { + testCases := []struct { + desc string + domains []string + wildcardAllowed bool + dnsChallenge *acmeprovider.DNSChallenge + expectedErr string + expectedDomains []string + }{ + { + desc: "valid wildcard", + domains: []string{"*.traefik.wtf"}, + dnsChallenge: &acmeprovider.DNSChallenge{}, + wildcardAllowed: true, + expectedErr: "", + expectedDomains: []string{"*.traefik.wtf"}, + }, + { + desc: "no wildcard", + domains: []string{"traefik.wtf", "foo.traefik.wtf"}, + dnsChallenge: &acmeprovider.DNSChallenge{}, + expectedErr: "", + wildcardAllowed: true, + expectedDomains: []string{"traefik.wtf", "foo.traefik.wtf"}, + }, + { + desc: "unauthorized wildcard", + domains: []string{"*.traefik.wtf"}, + dnsChallenge: &acmeprovider.DNSChallenge{}, + wildcardAllowed: false, + expectedErr: "unable to generate a wildcard certificate for domain \"*.traefik.wtf\" from a 'Host' rule", + expectedDomains: nil, + }, + { + desc: "no domain", + domains: []string{}, + dnsChallenge: nil, + wildcardAllowed: true, + expectedErr: "unable to generate a certificate when no domain is given", + expectedDomains: nil, + }, + { + desc: "no DNSChallenge", + domains: []string{"*.traefik.wtf", "foo.traefik.wtf"}, + dnsChallenge: nil, + wildcardAllowed: true, + expectedErr: "unable to generate a wildcard certificate for domain \"*.traefik.wtf,foo.traefik.wtf\" : ACME needs a DNSChallenge", + expectedDomains: nil, + }, + { + desc: "unexpected SANs", + domains: []string{"*.traefik.wtf", "foo.traefik.wtf"}, + dnsChallenge: &acmeprovider.DNSChallenge{}, + wildcardAllowed: true, + expectedErr: "unable to generate a wildcard certificate for domain \"*.traefik.wtf,foo.traefik.wtf\" : SANs are not allowed", + expectedDomains: nil, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + a := ACME{} + if test.dnsChallenge != nil { + a.DNSChallenge = test.dnsChallenge + } + domains, err := a.getValidDomains(test.domains, test.wildcardAllowed) + + if len(test.expectedErr) > 0 { + assert.EqualError(t, err, test.expectedErr, "Unexpected error.") + } else { + assert.Equal(t, len(test.expectedDomains), len(domains), "Unexpected domains.") + } + }) + } +} diff --git a/acme/challenge_http_provider.go b/acme/challenge_http_provider.go index 901fa5594..6d29a04d7 100644 --- a/acme/challenge_http_provider.go +++ b/acme/challenge_http_provider.go @@ -9,7 +9,7 @@ import ( "github.com/containous/traefik/cluster" "github.com/containous/traefik/log" "github.com/containous/traefik/safe" - "github.com/xenolf/lego/acme" + acme "github.com/xenolf/lego/acmev2" ) var _ acme.ChallengeProviderTimeout = (*challengeHTTPProvider)(nil) diff --git a/acme/localStore.go b/acme/localStore.go index 0822fd3c8..bb4c1bdc1 100644 --- a/acme/localStore.go +++ b/acme/localStore.go @@ -4,6 +4,7 @@ import ( "encoding/json" "io/ioutil" "os" + "regexp" "github.com/containous/traefik/log" "github.com/containous/traefik/provider/acme" @@ -45,19 +46,41 @@ func (s *LocalStore) Get() (*Account, error) { if err := json.Unmarshal(file, &account); err != nil { return nil, err } + + // Check if ACME Account is in ACME V1 format + if account != nil && account.Registration != nil { + isOldRegistration, err := regexp.MatchString(acme.RegistrationURLPathV1Regexp, account.Registration.URI) + if err != nil { + return nil, err + } + + if isOldRegistration { + account.Email = "" + account.Registration = nil + account.PrivateKey = nil + } + } } + return account, nil } // ConvertToNewFormat converts old acme.json format to the new one and store the result into the file (used for the backward compatibility) func ConvertToNewFormat(fileName string) { localStore := acme.NewLocalStore(fileName) + storeAccount, err := localStore.GetAccount() if err != nil { log.Warnf("Failed to read new account, ACME data conversion is not available : %v", err) return } + storeCertificates, err := localStore.GetCertificates() + if err != nil { + log.Warnf("Failed to read new certificates, ACME data conversion is not available : %v", err) + return + } + if storeAccount == nil { localStore := NewLocalStore(fileName) @@ -67,8 +90,10 @@ func ConvertToNewFormat(fileName string) { return } - if account != nil { - newAccount := &acme.Account{ + // Convert ACME data from old to new format + newAccount := &acme.Account{} + if account != nil && len(account.Email) > 0 { + newAccount = &acme.Account{ PrivateKey: account.PrivateKey, Registration: account.Registration, Email: account.Email, @@ -82,9 +107,15 @@ func ConvertToNewFormat(fileName string) { Domain: cert.Domains, }) } - newLocalStore := acme.NewLocalStore(fileName) - newLocalStore.SaveDataChan <- &acme.StoredData{Account: newAccount, Certificates: newCertificates} + // If account is in the old format, storeCertificates is nil or empty + // and has to be initialized + storeCertificates = newCertificates } + + // Store the data in new format into the file even if account is nil + // to delete Account in ACME v1 format and keeping the certificates + newLocalStore := acme.NewLocalStore(fileName) + newLocalStore.SaveDataChan <- &acme.StoredData{Account: newAccount, Certificates: storeCertificates} } } @@ -102,15 +133,28 @@ func FromNewToOldFormat(fileName string) (*Account, error) { return nil, err } + // Convert ACME Account from new to old format + // (Needed by the KV stores) + var account *Account if storeAccount != nil { - account := &Account{} - account.Email = storeAccount.Email - account.PrivateKey = storeAccount.PrivateKey - account.Registration = storeAccount.Registration - account.DomainsCertificate = DomainsCertificates{} + account = &Account{ + Email: storeAccount.Email, + PrivateKey: storeAccount.PrivateKey, + Registration: storeAccount.Registration, + DomainsCertificate: DomainsCertificates{}, + } + } + // Convert ACME Certificates from new to old format + // (Needed by the KV stores) + if len(storeCertificates) > 0 { + // Account can be nil if data are migrated from new format + // with a ACME V1 Account + if account == nil { + account = &Account{} + } for _, cert := range storeCertificates { - _, err = account.DomainsCertificate.addCertificateForDomains(&Certificate{ + _, err := account.DomainsCertificate.addCertificateForDomains(&Certificate{ Domain: cert.Domain.Main, Certificate: cert.Certificate, PrivateKey: cert.Key, @@ -119,7 +163,7 @@ func FromNewToOldFormat(fileName string) (*Account, error) { return nil, err } } - return account, nil } - return nil, nil + + return account, nil } diff --git a/cmd/storeconfig/storeconfig.go b/cmd/storeconfig/storeconfig.go index 1baeca92e..4beb75ba5 100644 --- a/cmd/storeconfig/storeconfig.go +++ b/cmd/storeconfig/storeconfig.go @@ -75,36 +75,60 @@ func Run(kv *staert.KvSource, traefikConfiguration *cmd.TraefikConfiguration) fu } if traefikConfiguration.GlobalConfiguration.ACME != nil { + account := &acme.Account{} + + // Migrate ACME data from file to KV store if needed if len(traefikConfiguration.GlobalConfiguration.ACME.StorageFile) > 0 { - return migrateACMEData(traefikConfiguration.GlobalConfiguration.ACME.StorageFile, traefikConfiguration.GlobalConfiguration.ACME.Storage, kv) + account, err = migrateACMEData(traefikConfiguration.GlobalConfiguration.ACME.StorageFile) + if err != nil { + return err + } } + + // Store the ACME Account into the KV Store + meta := cluster.NewMetadata(account) + err = meta.Marshall() + if err != nil { + return err + } + + source := staert.KvSource{ + Store: kv, + Prefix: traefikConfiguration.GlobalConfiguration.ACME.Storage, + } + + err = source.StoreConfig(meta) + if err != nil { + return err + } + + // Force to delete storagefile + return kv.Delete(kv.Prefix + "/acme/storagefile") } return nil } } // migrateACMEData allows migrating data from acme.json file to KV store in function of the file format -func migrateACMEData(fileName, storageKey string, kv *staert.KvSource) error { - var object cluster.Object +func migrateACMEData(fileName string) (*acme.Account, error) { f, err := os.Open(fileName) if err != nil { - return err + return nil, err } defer f.Close() file, err := ioutil.ReadAll(f) if err != nil { - return err + return nil, err } - // Create an empty account to create all the keys into the KV store - account := &acme.Account{} // Check if the storage file is not empty before to get data + account := &acme.Account{} if len(file) > 0 { accountFromNewFormat, err := acme.FromNewToOldFormat(fileName) if err != nil { - return err + return nil, err } if accountFromNewFormat == nil { @@ -112,7 +136,7 @@ func migrateACMEData(fileName, storageKey string, kv *staert.KvSource) error { localStore := acme.NewLocalStore(fileName) account, err = localStore.Get() if err != nil { - return err + return nil, err } } else { account = accountFromNewFormat @@ -122,29 +146,7 @@ func migrateACMEData(fileName, storageKey string, kv *staert.KvSource) error { } err = account.Init() - if err != nil { - return err - } - - object = account - meta := cluster.NewMetadata(object) - err = meta.Marshall() - if err != nil { - return err - } - - source := staert.KvSource{ - Store: kv, - Prefix: storageKey, - } - - err = source.StoreConfig(meta) - if err != nil { - return err - } - - // Force to delete storagefile - return kv.Delete(kv.Prefix + "/acme/storagefile") + return account, err } // CreateKvSource creates KvSource diff --git a/docs/configuration/acme.md b/docs/configuration/acme.md index c3364c164..d089a7409 100644 --- a/docs/configuration/acme.md +++ b/docs/configuration/acme.md @@ -82,11 +82,12 @@ entryPoint = "https" # - Leave comment to go to prod. # # Optional -# Default: "https://acme-v01.api.letsencrypt.org/directory" +# Default: "https://acme-v02.api.letsencrypt.org/directory" # -# caServer = "https://acme-staging.api.letsencrypt.org/directory" +# caServer = "https://acme-staging-v02.api.letsencrypt.org/directory" # Domains list. +# Only domains defined here can generate wildcard certificates. # # [[acme.domains]] # main = "local1.com" @@ -111,7 +112,8 @@ entryPoint = "https" # entryPoint = "http" -# Use a DNS-01 acme challenge rather than HTTP-01 challenge. +# Use a DNS-01/DNS-02 acme challenge rather than HTTP-01 challenge. +# Note : Mandatory for wildcard certificates generation. # # Optional # @@ -137,6 +139,10 @@ entryPoint = "https" If `HTTP-01` challenge is used, `acme.httpChallenge.entryPoint` has to be defined and reachable by Let's Encrypt through the port 80. These are Let's Encrypt limitations as described on the [community forum](https://community.letsencrypt.org/t/support-for-ports-other-than-80-and-443/3419/72). +!!! note + Wildcard certificates can be generated only if `acme.dnsChallenge` +option is enable. + ### Let's Encrypt downtime Let's Encrypt functionality will be limited until Træfik is restarted. @@ -215,7 +221,7 @@ Because KV stores (like Consul) have limited entries size, the certificates list !!! note It's possible to store up to approximately 100 ACME certificates in Consul. -### `acme.httpChallenge` +### `httpChallenge` Use `HTTP-01` challenge to generate/renew ACME certificates. @@ -256,9 +262,9 @@ defaultEntryPoints = ["http", "https"] `acme.httpChallenge.entryPoint` has to be reachable by Let's Encrypt through the port 80. It's a Let's Encrypt limitation as described on the [community forum](https://community.letsencrypt.org/t/support-for-ports-other-than-80-and-443/3419/72). -### `acme.dnsChallenge` +### `dnsChallenge` -Use `DNS-01` challenge to generate/renew ACME certificates. +Use `DNS-01/DNS-02` challenge to generate/renew ACME certificates. ```toml [acme] @@ -269,6 +275,9 @@ Use `DNS-01` challenge to generate/renew ACME certificates. # ... ``` +!!! note + ACME wildcard certificates can only be generated thanks to a `DNS-02` challenge. + #### `provider` Select the provider that matches the DNS domain that will host the challenge TXT record, and provide environment variables to enable setting it: @@ -348,12 +357,16 @@ This will request a certificate from Let's Encrypt for each frontend with a Host For example, a rule `Host:test1.traefik.io,test2.traefik.io` will request a certificate with main domain `test1.traefik.io` and SAN `test2.traefik.io`. +!!! warning + `onHostRule` option can not be used to generate wildcard certificates. + Refer to [the wildcard generation section](/configuration/acme/#wildcard-domain) for more information. + ### `caServer` ```toml [acme] # ... -caServer = "https://acme-staging.api.letsencrypt.org/directory" +caServer = "https://acme-staging-v02.api.letsencrypt.org/directory" # ... ``` @@ -362,7 +375,7 @@ CA server to use. - Uncomment the line to run on the staging Let's Encrypt server. - Leave comment to go to prod. -### `acme.domains` +### `domains` ```toml [acme] @@ -376,10 +389,22 @@ CA server to use. [[acme.domains]] main = "local3.com" [[acme.domains]] - main = "local4.com" + main = "*.local4.com" # ... ``` +#### Wildcard domains + +Wildcard domain has to be defined as a main domain **with no SANs** (alternative domains). +All domains must have A/AAAA records pointing to Træfik. + +!!! warning + Note that Let's Encrypt has [rate limiting](https://letsencrypt.org/docs/rate-limits). + +Each domain & SANs will lead to a certificate request. + +#### Others domains + You can provide SANs (alternative domains) to each main domain. All domains must have A/AAAA records pointing to Træfik. @@ -391,9 +416,47 @@ Each domain & SANs will lead to a certificate request. ### `dnsProvider` (Deprecated) !!! danger "DEPRECATED" - This option is deprecated, use [dnsChallenge.provider](/configuration/acme/#acmednschallenge) instead. + This option is deprecated, use [dnsChallenge.provider](/configuration/acme/#dnschallenge) instead. ### `delayDontCheckDNS` (Deprecated) !!! danger "DEPRECATED" - This option is deprecated, use [dnsChallenge.delayBeforeCheck](/configuration/acme/#acmednschallenge) instead. + This option is deprecated, use [dnsChallenge.delayBeforeCheck](/configuration/acme/#dnschallenge) instead. + +## Wildcard certificates + +[ACME V2](https://community.letsencrypt.org/t/acme-v2-and-wildcard-certificate-support-is-live/55579) allows wildcard certificate support. +However, this feature needs a specific configuration. + +### DNS-02 Challenge + +As described in [Let's Encrypt post](https://community.letsencrypt.org/t/staging-endpoint-for-acme-v2/49605), wildcard certificates can only be generated through a `DNS-02`Challenge. +This challenge is linked to the Træfik option `acme.dnsChallenge`. + +```toml +[acme] +# ... +[acme.dnsChallenge] + provider = "digitalocean" + delayBeforeCheck = 0 +# ... +``` + +For more information about this option, please refer to the [dnsChallenge section](/configuration/acme/#dnschallenge). + +### Wildcard domain + +Wildcard domains can currently be provided only by to the `acme.domains` option. +Theses domains can not have SANs. + +```toml +[acme] +# ... +[[acme.domains]] + main = "*local1.com" +[[acme.domains]] + main = "*.local2.com" +# ... +``` + +For more information about this option, please refer to the [domains section](/configuration/acme/#domains). diff --git a/docs/index.md b/docs/index.md index 00d2c30d9..f18d0ee4e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -34,7 +34,7 @@ _(But if you'd rather configure some of your routes manually, Træfik supports t - Continuously updates its configuration (No restarts!) - Supports multiple load balancing algorithms -- Provides HTTPS to your microservices by leveraging [Let's Encrypt](https://letsencrypt.org) +- Provides HTTPS to your microservices by leveraging [Let's Encrypt](https://letsencrypt.org) (wildcard certificates support) - Circuit breakers, retry - High Availability with cluster mode (beta) - See the magic through its clean web UI diff --git a/docs/user-guide/examples.md b/docs/user-guide/examples.md index 29824a05e..61116dcfd 100644 --- a/docs/user-guide/examples.md +++ b/docs/user-guide/examples.md @@ -55,10 +55,6 @@ defaultEntryPoints = ["http", "https"] ## Let's Encrypt support -!!! note - Even if `TLS-SNI-01` challenge is [disabled](https://community.letsencrypt.org/t/2018-01-11-update-regarding-acme-tls-sni-and-shared-hosting-infrastructure/50188), for the moment, it stays the _by default_ ACME Challenge in Træfik but all the examples use the `HTTP-01` challenge (except DNS challenge examples). - If `TLS-SNI-01` challenge is not re-enabled in the future, it we will be removed from Træfik. - ### Basic example with HTTP challenge ```toml @@ -190,10 +186,45 @@ entryPoint = "https" ``` DNS challenge needs environment variables to be executed. -These variables have to be set on the machine/container which host Træfik. +These variables have to be set on the machine/container that host Træfik. These variables are described [in this section](/configuration/acme/#provider). +### DNS challenge with wildcard domains + +```toml +[entryPoints] + [entryPoints.https] + address = ":443" + [entryPoints.https.tls] + +[acme] +email = "test@traefik.io" +storage = "acme.json" +caServer = "https://acme-staging-v02.api.letsencrypt.org/directory" +entryPoint = "https" + [acme.dnsChallenge] + provider = "digitalocean" # DNS Provider name (cloudflare, OVH, gandi...) + delayBeforeCheck = 0 + +[[acme.domains]] + main = "*.local1.com" +[[acme.domains]] + main = "local2.com" + sans = ["test1.local2.com", "test2x.local2.com"] +[[acme.domains]] + main = "*.local3.com" +[[acme.domains]] + main = "*.local4.com" +``` + +DNS challenge needs environment variables to be executed. +These variables have to be set on the machine/container that host Træfik. + +These variables are described [in this section](/configuration/acme/#provider). + +More information about wildcard certificates are available [in this section](/configuration/acme/#wildcard-domain). + ### OnHostRule option and provided certificates (with HTTP challenge) ```toml diff --git a/examples/acme/acme.toml b/examples/acme/acme.toml index 85340658d..7428f9336 100644 --- a/examples/acme/acme.toml +++ b/examples/acme/acme.toml @@ -17,7 +17,7 @@ storage = "/etc/traefik/conf/acme.json" entryPoint = "https" onDemand = false OnHostRule = true -caServer = "http://traefik.boulder.com:4000/directory" +caServer = "http://traefik.boulder.com:4001/directory" [acme.httpChallenge] entryPoint="http" diff --git a/examples/acme/docker-compose.yml b/examples/acme/docker-compose.yml index 096524aff..fb3f73eb1 100644 --- a/examples/acme/docker-compose.yml +++ b/examples/acme/docker-compose.yml @@ -3,40 +3,50 @@ version: "2" services : boulder: - image: containous/boulder:containous-fork + # To minimize fetching this should be the same version used below + image: containous/boulder:containous-acmev2 environment: FAKE_DNS: 172.17.0.1 PKCS11_PROXY_SOCKET: tcp://boulder-hsm:5657 + restart: unless-stopped extra_hosts: - le.wtf:127.0.0.1 - boulder:127.0.0.1 ports: - 4000:4000 # ACME + - 4001:4001 # ACMEv2 - 4002:4002 # OCSP - 4003:4003 # OCSP + - 4430:4430 # ACME via HTTPS + - 4431:4431 # ACMEv2 via HTTPS - 4500:4500 # ct-test-srv + - 6000:6000 # gsb-test-srv - 8000:8000 # debug ports - 8001:8001 - 8002:8002 - 8003:8003 - 8004:8004 + - 8005:8005 + - 8006:8006 + - 8008:8008 + - 8009:8009 + - 8010:8010 - 8055:8055 # dns-test-srv updates - 9380:9380 # mail-test-srv - 9381:9381 # mail-test-srv - restart: unless-stopped depends_on: - bhsm - bmysql - - brabbitmq networks: - default bhsm: - image: letsencrypt/boulder-tools:2016-11-02 + # To minimize fetching this should be the same version used above + image: letsencrypt/boulder-tools:2018-03-07 hostname: boulder-hsm environment: PKCS11_DAEMON_SOCKET: tcp://0.0.0.0:5657 - command: /usr/local/bin/pkcs11-daemon /usr/lib/softhsm/libsofthsm.so + command: /usr/local/bin/pkcs11-daemon /usr/lib/softhsm/libsofthsm2.so expose: - 5657 networks: @@ -49,21 +59,14 @@ services : hostname: boulder-mysql environment: MYSQL_ALLOW_EMPTY_PASSWORD: "yes" + command: mysqld --bind-address=0.0.0.0 + logging: + driver: none networks: default: aliases: - boulder-mysql - brabbitmq: - image: rabbitmq:3-alpine - hostname: boulder-rabbitmq - environment: - RABBITMQ_NODE_IP_ADDRESS: "0.0.0.0" - networks: - default: - aliases: - - boulder-rabbitmq - ## TRAEFIK part ## traefik: diff --git a/examples/acme/manage_acme_docker_environment.sh b/examples/acme/manage_acme_docker_environment.sh index 8e21da667..a95483c9e 100755 --- a/examples/acme/manage_acme_docker_environment.sh +++ b/examples/acme/manage_acme_docker_environment.sh @@ -32,7 +32,7 @@ init_environment() { start_boulder() { init_environment echo "Start boulder environment" - up_environment bmysql brabbitmq bhsm boulder + up_environment bmysql bhsm boulder waiting_counter=12 # Not start Traefik if boulder is not started echo "WAIT for boulder..." diff --git a/examples/cluster/docker-compose.yml b/examples/cluster/docker-compose.yml index ad76ccc3b..d8398fdd1 100644 --- a/examples/cluster/docker-compose.yml +++ b/examples/cluster/docker-compose.yml @@ -7,193 +7,195 @@ services: # CONSUL consul: - image: progrium/consul - command: -server -bootstrap -log-level debug -ui-dir /ui - ports: - - "8400:8400" - - "8500:8500" - - "8600:53/udp" - expose: - - "8300" - - "8301" - - "8301/udp" - - "8302" - - "8302/udp" - networks: - net: - ipv4_address: 10.0.1.2 + image: progrium/consul + command: -server -bootstrap -log-level debug -ui-dir /ui + ports: + - "8400:8400" + - "8500:8500" + - "8600:53/udp" + expose: + - "8300" + - "8301" + - "8301/udp" + - "8302" + - "8302/udp" + networks: + net: + ipv4_address: 10.0.1.2 # ETCD V3 etcd3: - image: quay.io/coreos/etcd:v3.2.9 - command: /usr/local/bin/etcd --data-dir=/etcd-data --name node1 --initial-advertise-peer-urls http://10.0.1.12:2380 --listen-peer-urls http://10.0.1.12:2380 --advertise-client-urls http://10.0.1.12:2379,http://10.0.1.12:4001 --listen-client-urls http://10.0.1.12:2379,http://10.0.1.12:4001 --initial-cluster node1=http://10.0.1.12:2380 --debug - ports: - - "4001:4001" - - "2380:2380" - - "2379:2379" - networks: - net: - ipv4_address: 10.0.1.12 + image: quay.io/coreos/etcd:v3.2.9 + command: /usr/local/bin/etcd --data-dir=/etcd-data --name node1 --initial-advertise-peer-urls http://10.0.1.12:2380 --listen-peer-urls http://10.0.1.12:2380 --advertise-client-urls http://10.0.1.12:2379,http://10.0.1.12:4001 --listen-client-urls http://10.0.1.12:2379,http://10.0.1.12:4001 --initial-cluster node1=http://10.0.1.12:2380 --debug + ports: + - "4001:4001" + - "2380:2380" + - "2379:2379" + networks: + net: + ipv4_address: 10.0.1.12 etcdctl-ping: - image: tenstartups/etcdctl - command: --endpoints=[10.0.1.12:2379] get "traefik/acme/storage" - environment: - ETCDCTL_DIAL_: "TIMEOUT 10s" - ETCDCTL_API : "3" - networks: + image: tenstartups/etcdctl + command: --endpoints=[10.0.1.12:2379] get "traefik/acme/storage" + environment: + ETCDCTL_DIAL_: "TIMEOUT 10s" + ETCDCTL_API : "3" + networks: - net ## BOULDER part ## boulder: - image: containous/boulder:containous-fork - environment: - FAKE_DNS: 172.17.0.1 - PKCS11_PROXY_SOCKET: tcp://boulder-hsm:5657 - extra_hosts: - - le.wtf:127.0.0.1 - - boulder:127.0.0.1 - ports: - - 4000:4000 # ACME - - 4002:4002 # OCSP - - 4003:4003 # OCSP - - 4500:4500 # ct-test-srv - - 8000:8000 # debug ports - - 8001:8001 - - 8002:8002 - - 8003:8003 - - 8004:8004 - - 8055:8055 # dns-test-srv updates - - 9380:9380 # mail-test-srv - - 9381:9381 # mail-test-srv - restart: unless-stopped - depends_on: - - bhsm - - bmysql - - brabbitmq - networks: - net: - ipv4_address: 10.0.1.3 + # To minimize fetching this should be the same version used below + image: containous/boulder:containous-acmev2 + environment: + FAKE_DNS: 172.17.0.1 + PKCS11_PROXY_SOCKET: tcp://boulder-hsm:5657 + restart: unless-stopped + extra_hosts: + - le.wtf:127.0.0.1 + - boulder:127.0.0.1 + ports: + - 4000:4000 # ACME + - 4001:4001 # ACMEv2 + - 4002:4002 # OCSP + - 4003:4003 # OCSP + - 4430:4430 # ACME via HTTPS + - 4431:4431 # ACMEv2 via HTTPS + - 4500:4500 # ct-test-srv + - 6000:6000 # gsb-test-srv + - 8000:8000 # debug ports + - 8001:8001 + - 8002:8002 + - 8003:8003 + - 8004:8004 + - 8005:8005 + - 8006:8006 + - 8008:8008 + - 8009:8009 + - 8010:8010 + - 8055:8055 # dns-test-srv updates + - 9380:9380 # mail-test-srv + - 9381:9381 # mail-test-srv + depends_on: + - bhsm + - bmysql + networks: + net: + ipv4_address: 10.0.1.3 bhsm: - image: letsencrypt/boulder-tools:2016-11-02 - hostname: boulder-hsm - environment: - PKCS11_DAEMON_SOCKET: tcp://0.0.0.0:5657 - command: /usr/local/bin/pkcs11-daemon /usr/lib/softhsm/libsofthsm.so - expose: - - 5657 - networks: - net: - ipv4_address: 10.0.1.4 - aliases: - - boulder-hsm + # To minimize fetching this should be the same version used above + image: letsencrypt/boulder-tools:2018-03-07 + hostname: boulder-hsm + environment: + PKCS11_DAEMON_SOCKET: tcp://0.0.0.0:5657 + command: /usr/local/bin/pkcs11-daemon /usr/lib/softhsm/libsofthsm2.so + expose: + - 5657 + networks: + net: + ipv4_address: 10.0.1.4 + aliases: + - boulder-hsm bmysql: - image: mariadb:10.1 - hostname: boulder-mysql - environment: - MYSQL_ALLOW_EMPTY_PASSWORD: "yes" - networks: - net: - ipv4_address: 10.0.1.5 - aliases: - - boulder-mysql - - brabbitmq: - image: rabbitmq:3-alpine - hostname: boulder-rabbitmq - environment: - RABBITMQ_NODE_IP_ADDRESS: "0.0.0.0" - networks: - net: - ipv4_address: 10.0.1.6 - aliases: - - boulder-rabbitmq + image: mariadb:10.1 + hostname: boulder-mysql + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: "yes" + command: mysqld --bind-address=0.0.0.0 + logging: + driver: none + networks: + net: + ipv4_address: 10.0.1.5 + aliases: + - boulder-mysql ## TRAEFIK part ## - traefik-storeconfig: - build: - context: ../.. - image: containous/traefik - volumes: - - "./traefik.toml:/traefik.toml:ro" - command: storeconfig --debug - networks: - - net + storeconfig: + build: + context: ../.. + image: containous/traefik + volumes: + - "./traefik.toml:/traefik.toml:ro" + command: storeconfig --debug + networks: + - net traefik01: - build: - context: ../.. - image: containous/traefik - command: ${TRAEFIK_CMD} - extra_hosts: - - traefik.boulder.com:172.17.0.1 - volumes: - - "/var/run/docker.sock:/var/run/docker.sock:ro" - expose: - - "443" - - "5001" - - "5002" - ports: - - "80:80" - - "8080:8080" - - "443:443" - - "5001:443" # Needed for SNI challenge - - "5002:80" # Needed for HTTP challenge - networks: - net: - ipv4_address: 10.0.1.8 + build: + context: ../.. + image: containous/traefik + command: ${TRAEFIK_CMD} + extra_hosts: + - traefik.boulder.com:172.17.0.1 + volumes: + - "/var/run/docker.sock:/var/run/docker.sock:ro" + expose: + - "443" + - "5001" + - "5002" + ports: + - "80:80" + - "8080:8080" + - "443:443" + - "5001:443" # Needed for SNI challenge + - "5002:80" # Needed for HTTP challenge + networks: + net: + ipv4_address: 10.0.1.8 traefik02: - build: - context: ../.. - image: containous/traefik - command: ${TRAEFIK_CMD} - extra_hosts: - - traefik.boulder.com:172.17.0.1 - volumes: - - "/var/run/docker.sock:/var/run/docker.sock:ro" - expose: - - "443" - - "5001" - - "5002" - ports: - - "88:80" - - "8888:8080" - - "8443:443" - depends_on: - - traefik01 - networks: - net: - ipv4_address: 10.0.1.9 + build: + context: ../.. + image: containous/traefik + command: ${TRAEFIK_CMD} + extra_hosts: + - traefik.boulder.com:172.17.0.1 + volumes: + - "/var/run/docker.sock:/var/run/docker.sock:ro" + expose: + - "443" + - "5001" + - "5002" + ports: + - "88:80" + - "8888:8080" + - "8443:443" + depends_on: + - traefik01 + networks: + net: + ipv4_address: 10.0.1.9 whoami01: - image: emilevauge/whoami - expose: - - "80" - labels: - - "traefik.port=80" - - "traefik.backend=wam01" - - "traefik.frontend.rule=Host:who01.localhost.com" - - "traefik.enable=true" - networks: - net: - ipv4_address: 10.0.1.10 + image: emilevauge/whoami + expose: + - "80" + labels: + - "traefik.port=80" + - "traefik.backend=wam01" + - "traefik.frontend.rule=Host:who01.localhost.com" + - "traefik.enable=true" + networks: + net: + ipv4_address: 10.0.1.10 whoami02: - image: emilevauge/whoami - expose: - - "80" - labels: - - "traefik.port=80" - - "traefik.backend=wam02" - - "traefik.frontend.rule=Host:who02.localhost.com" - - "traefik.enable=true" - networks: - - net + image: emilevauge/whoami + expose: + - "80" + labels: + - "traefik.port=80" + - "traefik.backend=wam02" + - "traefik.frontend.rule=Host:who02.localhost.com" + - "traefik.enable=true" + networks: + - net networks: net: diff --git a/examples/cluster/manage_cluster_docker_environment.sh b/examples/cluster/manage_cluster_docker_environment.sh index 7f1196157..0e4fcd640 100755 --- a/examples/cluster/manage_cluster_docker_environment.sh +++ b/examples/cluster/manage_cluster_docker_environment.sh @@ -74,10 +74,10 @@ start_storeconfig_consul() { endpoint = "10.0.1.2:8500" watch = true prefix = "traefik"' >> $basedir/traefik.toml - up_environment traefik-storeconfig + up_environment storeconfig rm -f $basedir/traefik.toml waiting_counter=5 - delete_services traefik-storeconfig + delete_services storeconfig } @@ -90,7 +90,7 @@ start_storeconfig_etcd3() { watch = true prefix = "/traefik" useAPIV3 = true' >> $basedir/traefik.toml - up_environment traefik-storeconfig + up_environment storeconfig rm -f $basedir/traefik.toml waiting_counter=5 # Don't start Traefik store config if ETCD3 is not started @@ -99,7 +99,7 @@ start_storeconfig_etcd3() { sleep 5 let waiting_counter-=1 done - delete_services traefik-storeconfig etcdctl-ping + delete_services storeconfig etcdctl-ping } start_traefik() { @@ -136,11 +136,11 @@ start_traefik() { # Start boulder services start_boulder() { echo "Start boulder environment" - up_environment bmysql brabbitmq bhsm boulder + up_environment bmysql bhsm boulder waiting_counter=12 # Not start Traefik if boulder is not started echo "WAIT for boulder..." - while [[ -z $(curl -s http://10.0.1.3:4000/directory) ]]; do + while [[ -z $(curl -s http://10.0.1.3:4001/directory) ]]; do sleep 5 let waiting_counter-=1 if [[ $waiting_counter -eq 0 ]]; then diff --git a/examples/cluster/traefik.toml.tmpl b/examples/cluster/traefik.toml.tmpl index 478027f00..95b1c639a 100644 --- a/examples/cluster/traefik.toml.tmpl +++ b/examples/cluster/traefik.toml.tmpl @@ -14,7 +14,7 @@ email = "test@traefik.io" storage = "traefik/acme/account" entryPoint = "https" OnHostRule = true -caServer = "http://traefik.boulder.com:4000/directory" +caServer = "http://traefik.boulder.com:4001/directory" [acme.httpChallenge] entryPoint="http" diff --git a/integration/acme_test.go b/integration/acme_test.go index c1b2b41c8..73ae9cf8f 100644 --- a/integration/acme_test.go +++ b/integration/acme_test.go @@ -44,7 +44,7 @@ func (s *AcmeSuite) SetUpSuite(c *check.C) { s.boulderIP = s.composeProject.Container(c, "boulder").NetworkSettings.IPAddress // wait for boulder - err := try.GetRequest("http://"+s.boulderIP+":4000/directory", 120*time.Second, try.StatusCodeIs(http.StatusOK)) + err := try.GetRequest("http://"+s.boulderIP+":4001/directory", 120*time.Second, try.StatusCodeIs(http.StatusOK)) c.Assert(err, checker.IsNil) } diff --git a/integration/fixtures/acme/acme_http01.toml b/integration/fixtures/acme/acme_http01.toml index d85cc74a6..287cd207e 100644 --- a/integration/fixtures/acme/acme_http01.toml +++ b/integration/fixtures/acme/acme_http01.toml @@ -16,7 +16,7 @@ storage = "/dev/null" entryPoint = "https" onDemand = {{.OnDemand}} OnHostRule = {{.OnHostRule}} -caServer = "http://{{.BoulderHost}}:4000/directory" +caServer = "http://{{.BoulderHost}}:4001/directory" [acme.httpchallenge] entrypoint="http" diff --git a/integration/fixtures/acme/acme_http01_web.toml b/integration/fixtures/acme/acme_http01_web.toml index 258675a04..1b8af748a 100644 --- a/integration/fixtures/acme/acme_http01_web.toml +++ b/integration/fixtures/acme/acme_http01_web.toml @@ -15,7 +15,7 @@ storage = "/dev/null" entryPoint = "https" onDemand = {{.OnDemand}} OnHostRule = {{.OnHostRule}} -caServer = "http://{{.BoulderHost}}:4000/directory" +caServer = "http://{{.BoulderHost}}:4001/directory" [acme.httpchallenge] entrypoint="http" diff --git a/integration/fixtures/acme/acme_provided.toml b/integration/fixtures/acme/acme_provided.toml index 81dd2bc52..108f23427 100644 --- a/integration/fixtures/acme/acme_provided.toml +++ b/integration/fixtures/acme/acme_provided.toml @@ -18,7 +18,7 @@ storage = "/dev/null" entryPoint = "https" onDemand = {{.OnDemand}} OnHostRule = {{.OnHostRule}} -caServer = "http://{{.BoulderHost}}:4000/directory" +caServer = "http://{{.BoulderHost}}:4001/directory" [acme.httpChallenge] entryPoint="http" diff --git a/integration/fixtures/acme/acme_provided_dynamic.toml b/integration/fixtures/acme/acme_provided_dynamic.toml index cf2e15642..29e923835 100644 --- a/integration/fixtures/acme/acme_provided_dynamic.toml +++ b/integration/fixtures/acme/acme_provided_dynamic.toml @@ -16,7 +16,7 @@ storage = "/dev/null" entryPoint = "https" onDemand = {{.OnDemand}} OnHostRule = {{.OnHostRule}} -caServer = "http://{{.BoulderHost}}:4000/directory" +caServer = "http://{{.BoulderHost}}:4001/directory" [acme.httpChallenge] entryPoint="http" diff --git a/integration/fixtures/acme/no_challenge_acme.toml b/integration/fixtures/acme/no_challenge_acme.toml index 6a728e6f5..c986f6352 100644 --- a/integration/fixtures/acme/no_challenge_acme.toml +++ b/integration/fixtures/acme/no_challenge_acme.toml @@ -17,7 +17,7 @@ email = "test@traefik.io" storage = "/dev/null" entryPoint = "https" OnHostRule = true -caServer = "http://{{.BoulderHost}}:4000/directory" +caServer = "http://{{.BoulderHost}}:4001/directory" # No challenge defined [file] diff --git a/integration/fixtures/acme/wrong_acme.toml b/integration/fixtures/acme/wrong_acme.toml index 3f8f65051..abff4d71b 100644 --- a/integration/fixtures/acme/wrong_acme.toml +++ b/integration/fixtures/acme/wrong_acme.toml @@ -17,7 +17,7 @@ email = "test@traefik.io" storage = "/dev/null" entryPoint = "https" OnHostRule = true -caServer = "http://wrongurl:4000/directory" +caServer = "http://wrongurl:4001/directory" [file] diff --git a/integration/fixtures/provideracme/acme.toml b/integration/fixtures/provideracme/acme.toml index c8d5097b2..9e8629142 100644 --- a/integration/fixtures/provideracme/acme.toml +++ b/integration/fixtures/provideracme/acme.toml @@ -16,7 +16,7 @@ storage = "/dev/null" entryPoint = "https" onDemand = {{.OnDemand}} OnHostRule = {{.OnHostRule}} -caServer = "http://{{.BoulderHost}}:4000/directory" +caServer = "http://{{.BoulderHost}}:4001/directory" [acme.httpChallenge] entryPoint="http" [[acme.domains]] diff --git a/integration/fixtures/provideracme/acme_insan.toml b/integration/fixtures/provideracme/acme_insan.toml index 32c8cfdcd..71bea57ab 100644 --- a/integration/fixtures/provideracme/acme_insan.toml +++ b/integration/fixtures/provideracme/acme_insan.toml @@ -16,7 +16,7 @@ storage = "/dev/null" entryPoint = "https" onDemand = false OnHostRule = false -caServer = "http://{{.BoulderHost}}:4000/directory" +caServer = "http://{{.BoulderHost}}:4001/directory" [acme.httpChallenge] entryPoint="http" [[acme.domains]] diff --git a/integration/fixtures/provideracme/acme_onhost.toml b/integration/fixtures/provideracme/acme_onhost.toml index ce33e1afa..54322cf92 100644 --- a/integration/fixtures/provideracme/acme_onhost.toml +++ b/integration/fixtures/provideracme/acme_onhost.toml @@ -16,7 +16,7 @@ storage = "/dev/null" entryPoint = "https" onDemand = {{.OnDemand}} OnHostRule = {{.OnHostRule}} -caServer = "http://{{.BoulderHost}}:4000/directory" +caServer = "http://{{.BoulderHost}}:4001/directory" [acme.httpChallenge] entryPoint="http" diff --git a/integration/resources/compose/boulder.yml b/integration/resources/compose/boulder.yml index cb77823ab..dba52860d 100644 --- a/integration/resources/compose/boulder.yml +++ b/integration/resources/compose/boulder.yml @@ -1,5 +1,5 @@ boulder: - image: containous/boulder:containous-fork + image: containous/boulder:containous-acmev2 environment: FAKE_DNS: ${DOCKER_HOST_IP} PKCS11_PROXY_SOCKET: tcp://boulder-hsm:5657 @@ -8,37 +8,42 @@ boulder: - boulder:127.0.0.1 ports: - 4000:4000 # ACME + - 4001:4001 # ACMEv2 - 4002:4002 # OCSP - 4003:4003 # OCSP + - 4430:4430 # ACME via HTTPS + - 4431:4431 # ACMEv2 via HTTPS - 4500:4500 # ct-test-srv + - 6000:6000 # gsb-test-srv - 8000:8000 # debug ports - 8001:8001 - 8002:8002 - 8003:8003 - 8004:8004 + - 8005:8005 + - 8006:8006 + - 8008:8008 + - 8009:8009 + - 8010:8010 - 8055:8055 # dns-test-srv updates - 9380:9380 # mail-test-srv - 9381:9381 # mail-test-srv links: - bhsm:boulder-hsm - bmysql:boulder-mysql - - brabbitmq:boulder-rabbitmq bhsm: # To minimize the fetching of various layers this should match # the FROM image and tag in boulder/Dockerfile - image: letsencrypt/boulder-tools:2016-11-02 + image: letsencrypt/boulder-tools:2018-03-07 environment: PKCS11_DAEMON_SOCKET: tcp://0.0.0.0:5657 - command: /usr/local/bin/pkcs11-daemon /usr/lib/softhsm/libsofthsm.so + command: /usr/local/bin/pkcs11-daemon /usr/lib/softhsm/libsofthsm2.so expose: - 5657 bmysql: image: mariadb:10.1 environment: MYSQL_ALLOW_EMPTY_PASSWORD: "yes" + command: mysqld --bind-address=0.0.0.0 log_driver: none -brabbitmq: - image: rabbitmq:3-alpine - environment: - RABBITMQ_NODE_IP_ADDRESS: "0.0.0.0" diff --git a/provider/acme/account.go b/provider/acme/account.go index 7f19642dc..e57607229 100644 --- a/provider/acme/account.go +++ b/provider/acme/account.go @@ -7,7 +7,7 @@ import ( "crypto/x509" "github.com/containous/traefik/log" - "github.com/xenolf/lego/acme" + acme "github.com/xenolf/lego/acmev2" ) // Account is used to store lets encrypt registration info @@ -17,6 +17,11 @@ type Account struct { PrivateKey []byte } +const ( + // RegistrationURLPathV1Regexp is a regexp which match ACME registration URL in the V1 format + RegistrationURLPathV1Regexp string = `^.*/acme/reg/\d+$` +) + // NewAccount creates an account func NewAccount(email string) (*Account, error) { // Create a user. New accounts need an email and private key to start diff --git a/provider/acme/challenge.go b/provider/acme/challenge.go index 0ed638131..aec4a45ab 100644 --- a/provider/acme/challenge.go +++ b/provider/acme/challenge.go @@ -8,7 +8,7 @@ import ( "github.com/containous/flaeg" "github.com/containous/traefik/log" "github.com/containous/traefik/safe" - "github.com/xenolf/lego/acme" + acme "github.com/xenolf/lego/acmev2" ) func dnsOverrideDelay(delay flaeg.Duration) error { diff --git a/provider/acme/local_store.go b/provider/acme/local_store.go index 0bb1dd7c6..4156cd965 100644 --- a/provider/acme/local_store.go +++ b/provider/acme/local_store.go @@ -4,6 +4,7 @@ import ( "encoding/json" "io/ioutil" "os" + "regexp" "github.com/containous/traefik/log" "github.com/containous/traefik/safe" @@ -45,6 +46,17 @@ func (s *LocalStore) get() (*StoredData, error) { return nil, err } } + // Check if ACME Account is in ACME V1 format + if s.storedData.Account != nil && s.storedData.Account.Registration != nil { + isOldRegistration, err := regexp.MatchString(RegistrationURLPathV1Regexp, s.storedData.Account.Registration.URI) + if err != nil { + return nil, err + } + if isOldRegistration { + s.storedData.Account = nil + s.SaveDataChan <- s.storedData + } + } } return s.storedData, nil diff --git a/provider/acme/provider.go b/provider/acme/provider.go index 321d4a44b..3be1f46b9 100644 --- a/provider/acme/provider.go +++ b/provider/acme/provider.go @@ -24,7 +24,7 @@ import ( traefikTLS "github.com/containous/traefik/tls" "github.com/containous/traefik/types" "github.com/pkg/errors" - "github.com/xenolf/lego/acme" + acme "github.com/xenolf/lego/acmev2" "github.com/xenolf/lego/providers/dns" ) @@ -45,7 +45,7 @@ type Configuration struct { 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"` HTTPChallenge *HTTPChallenge `description:"Activate HTTP-01 Challenge"` - Domains []types.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'"` + Domains []types.Domain `description:"CN and SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='*.main.net'. No SANs for wildcards domain. Wildcard domains only accepted with DNSChallenge"` } // Provider holds configurations of the provider. @@ -142,9 +142,9 @@ func (p *Provider) ListenConfiguration(config types.Configuration) { p.configFromListenerChan <- config } -// ListenRequest resolves new certificates for a domain from an incoming request and retrun a valid Certificate to serve (onDemand option) +// ListenRequest resolves new certificates for a domain from an incoming request and return a valid Certificate to serve (onDemand option) func (p *Provider) ListenRequest(domain string) (*tls.Certificate, error) { - acmeCert, err := p.resolveCertificate(types.Domain{Main: domain}) + acmeCert, err := p.resolveCertificate(types.Domain{Main: domain}, false) if acmeCert == nil || err != nil { return nil, err } @@ -183,7 +183,7 @@ func (p *Provider) watchNewDomains() { } safe.Go(func() { - if _, err := p.resolveCertificate(domain); err != nil { + if _, err := p.resolveCertificate(domain, false); err != nil { log.Errorf("Unable to obtain ACME certificate for domains %q detected thanks to rule %q : %v", strings.Join(domains, ","), route.Rule, err) } }) @@ -207,16 +207,14 @@ func (p *Provider) SetStaticCertificates(staticCerts map[string]*tls.Certificate p.staticCerts = staticCerts } -func (p *Provider) resolveCertificate(domain types.Domain) (*acme.CertificateResource, error) { - domains := []string{domain.Main} - domains = append(domains, domain.SANs...) - if len(domains) == 0 { - return nil, nil +func (p *Provider) resolveCertificate(domain types.Domain, domainFromConfigurationFile bool) (*acme.CertificateResource, error) { + domains, err := p.getValidDomains(domain, domainFromConfigurationFile) + if err != nil { + return nil, err } - domains = fun.Map(types.CanonicalDomain, domains).([]string) // Check provided certificates - uncheckedDomains := p.getUncheckedDomains(domains) + uncheckedDomains := p.getUncheckedDomains(domains, !domainFromConfigurationFile) if len(uncheckedDomains) == 0 { return nil, nil } @@ -255,7 +253,7 @@ func (p *Provider) getClient() (*acme.Client, error) { } log.Debug("Building ACME client...") - caServer := "https://acme-v01.api.letsencrypt.org/directory" + caServer := "https://acme-v02.api.letsencrypt.org/directory" if len(p.CAServer) > 0 { caServer = p.CAServer } @@ -267,28 +265,13 @@ func (p *Provider) getClient() (*acme.Client, error) { if account.GetRegistration() == nil { // New users will need to register; be sure to save it log.Info("Register...") - reg, err := client.Register() + reg, err := client.Register(true) if err != nil { return nil, err } account.Registration = reg } - log.Debug("AgreeToTOS...") - err = client.AgreeToTOS() - if err != nil { - // Let's Encrypt Subscriber Agreement renew ? - reg, err := client.QueryRegistration() - if err != nil { - return nil, err - } - account.Registration = reg - err = client.AgreeToTOS() - if err != nil { - return nil, fmt.Errorf("error sending ACME agreement to TOS: %+v: %v", account, err) - } - } - // Save the account once before all the certificates generation/storing // No certificate can be generated if account is not initialized err = p.Store.SaveAccount(account) @@ -310,14 +293,14 @@ func (p *Provider) getClient() (*acme.Client, error) { return nil, err } - client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01}) + client.ExcludeChallenges([]acme.Challenge{acme.HTTP01}) err = client.SetChallengeProvider(acme.DNS01, provider) if err != nil { return nil, err } } else if p.HTTPChallenge != nil && len(p.HTTPChallenge.EntryPoint) > 0 { log.Debug("Using HTTP Challenge provider.") - client.ExcludeChallenges([]acme.Challenge{acme.DNS01, acme.TLSSNI01}) + client.ExcludeChallenges([]acme.Challenge{acme.DNS01}) err = client.SetChallengeProvider(acme.HTTP01, p) if err != nil { return nil, err @@ -353,12 +336,13 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s p.configurationChan = configurationChan p.refreshCertificates() - for _, domain := range p.Domains { + p.deleteUnnecessaryDomains() + for i := 0; i < len(p.Domains); i++ { + domain := p.Domains[i] safe.Go(func() { - if _, err := p.resolveCertificate(domain); err != nil { - domains := []string{domain.Main} - domains = append(domains, domain.SANs...) - log.Errorf("Unable to obtain ACME certificate for domains %q : %v", domains, err) + if _, err := p.resolveCertificate(domain, true); err != nil { + log.Errorf("Unable to obtain ACME certificate for domains %q : %v", strings.Join(domain.ToStrArray(), ","), err) + } else { } }) } @@ -506,51 +490,48 @@ func (p *Provider) AddRoutes(router *mux.Router) { // Get provided certificate which check a domains list (Main and SANs) // from static and dynamic provided certificates -func (p *Provider) getUncheckedDomains(domains []string) []string { - log.Debugf("Looking for provided certificate(s) to validate %q...", domains) - allCerts := make(map[string]*tls.Certificate) +func (p *Provider) getUncheckedDomains(domainsToCheck []string, checkConfigurationDomains bool) []string { + log.Debugf("Looking for provided certificate(s) to validate %q...", domainsToCheck) + var allCerts []string // Get static certificates - for domains, certificate := range p.staticCerts { - allCerts[domains] = certificate + for domains := range p.staticCerts { + allCerts = append(allCerts, domains) } // Get dynamic certificates if p.dynamicCerts != nil && p.dynamicCerts.Get() != nil { - for domains, certificate := range p.dynamicCerts.Get().(map[string]*tls.Certificate) { - allCerts[domains] = certificate + for domains := range p.dynamicCerts.Get().(map[string]*tls.Certificate) { + allCerts = append(allCerts, domains) } } - return searchUncheckedDomains(domains, allCerts) + // Get ACME certificates + for _, certificate := range p.certificates { + allCerts = append(allCerts, strings.Join(certificate.Domain.ToStrArray(), ",")) + } + + // Get Configuration Domains + if checkConfigurationDomains { + for i := 0; i < len(p.Domains); i++ { + allCerts = append(allCerts, strings.Join(p.Domains[i].ToStrArray(), ",")) + } + } + + return searchUncheckedDomains(domainsToCheck, allCerts) } -func searchUncheckedDomains(domains []string, certs map[string]*tls.Certificate) []string { +func searchUncheckedDomains(domainsToCheck []string, existentDomains []string) []string { uncheckedDomains := []string{} - for _, domainToCheck := range domains { - domainCheck := false - for certDomains := range certs { - domainCheck = false - for _, certDomain := range strings.Split(certDomains, ",") { - // Use regex to test for provided certs that might have been added into TLSConfig - selector := "^" + strings.Replace(certDomain, "*.", "[^\\.]*\\.?", -1) + "$" - domainCheck, _ = regexp.MatchString(selector, domainToCheck) - if domainCheck { - break - } - } - if domainCheck { - break - } - } - if !domainCheck { + for _, domainToCheck := range domainsToCheck { + if !isDomainAlreadyChecked(domainToCheck, existentDomains) { uncheckedDomains = append(uncheckedDomains, domainToCheck) } } if len(uncheckedDomains) == 0 { - log.Debugf("No ACME certificate to generate for domains %q.", domains) + log.Debugf("No ACME certificate to generate for domains %q.", domainsToCheck) } else { - log.Debugf("Domains %q need ACME certificates generation for domains %q.", domains, strings.Join(uncheckedDomains, ",")) + log.Debugf("Domains %q need ACME certificates generation for domains %q.", domainsToCheck, strings.Join(uncheckedDomains, ",")) } return uncheckedDomains } @@ -571,3 +552,98 @@ func getX509Certificate(certificate *Certificate) (*x509.Certificate, error) { } return crt, err } + +// getValidDomains checks if given domain is allowed to generate a ACME certificate and return it +func (p *Provider) getValidDomains(domain types.Domain, wildcardAllowed bool) ([]string, error) { + domains := domain.ToStrArray() + if len(domains) == 0 { + return nil, errors.New("unable to generate a certificate in ACME provider when no domain is given") + } + if strings.HasPrefix(domain.Main, "*") { + if !wildcardAllowed { + return nil, fmt.Errorf("unable to generate a wildcard certificate in ACME provider for domain %q from a 'Host' rule", strings.Join(domains, ",")) + } + if p.DNSChallenge == nil { + return nil, fmt.Errorf("unable to generate a wildcard certificate in ACME provider for domain %q : ACME needs a DNSChallenge", strings.Join(domains, ",")) + } + if len(domain.SANs) > 0 { + return nil, fmt.Errorf("unable to generate a wildcard certificate in ACME provider for domain %q : SANs are not allowed", strings.Join(domains, ",")) + } + } else { + for _, san := range domain.SANs { + if strings.HasPrefix(san, "*") { + return nil, fmt.Errorf("unable to generate a certificate in ACME provider for domains %q: SANs can not be a wildcard domain", strings.Join(domains, ",")) + } + } + } + domains = fun.Map(types.CanonicalDomain, domains).([]string) + return domains, nil +} + +func isDomainAlreadyChecked(domainToCheck string, existentDomains []string) bool { + for _, certDomains := range existentDomains { + for _, certDomain := range strings.Split(certDomains, ",") { + // Use regex to test for provided existentDomains that might have been added into TLSConfig + selector := "^" + strings.Replace(certDomain, "*.", "[^\\.]*\\.", -1) + "$" + domainCheck, err := regexp.MatchString(selector, domainToCheck) + if err != nil { + log.Errorf("Unable to compare %q and %q in ACME provider : %s", domainToCheck, certDomain, err) + continue + } + if domainCheck { + return true + } + } + } + return false +} + +// deleteUnnecessaryDomains deletes from the configuration : +// - Duplicated domains +// - Domains which are checked by wildcard domain +func (p *Provider) deleteUnnecessaryDomains() { + var newDomains []types.Domain + + for idxDomainToCheck, domainToCheck := range p.Domains { + keepDomain := true + + for idxDomain, domain := range p.Domains { + if idxDomainToCheck == idxDomain { + continue + } + + if reflect.DeepEqual(domain, domainToCheck) { + if idxDomainToCheck > idxDomain { + log.Warnf("The domain %v is duplicated in the configuration but will be process by ACME provider only once.", domainToCheck) + keepDomain = false + } + break + } else if strings.HasPrefix(domain.Main, "*") && domain.SANs == nil { + + // Check if domains can be validated by the wildcard domain + var newDomainsToCheck []string + for _, domainProcessed := range domainToCheck.ToStrArray() { + if isDomainAlreadyChecked(domainProcessed, domain.ToStrArray()) { + log.Warnf("Domain %q will not be processed by ACME provider because it is validated by the wildcard %q", domainProcessed, domain.Main) + continue + } + newDomainsToCheck = append(newDomainsToCheck, domainProcessed) + } + + // Delete the domain if both Main and SANs can be validated by the wildcard domain + // otherwise keep the unchecked values + if newDomainsToCheck == nil { + keepDomain = false + break + } + domainToCheck.Set(newDomainsToCheck) + } + } + + if keepDomain { + newDomains = append(newDomains, domainToCheck) + } + } + + p.Domains = newDomains +} diff --git a/provider/acme/provider_test.go b/provider/acme/provider_test.go new file mode 100644 index 000000000..97f3318a5 --- /dev/null +++ b/provider/acme/provider_test.go @@ -0,0 +1,342 @@ +package acme + +import ( + "crypto/tls" + "testing" + + "github.com/containous/traefik/safe" + "github.com/containous/traefik/types" + "github.com/stretchr/testify/assert" +) + +func TestGetUncheckedCertificates(t *testing.T) { + wildcardMap := make(map[string]*tls.Certificate) + wildcardMap["*.traefik.wtf"] = &tls.Certificate{} + + wildcardSafe := &safe.Safe{} + wildcardSafe.Set(wildcardMap) + + domainMap := make(map[string]*tls.Certificate) + domainMap["traefik.wtf"] = &tls.Certificate{} + + domainSafe := &safe.Safe{} + domainSafe.Set(domainMap) + + testCases := []struct { + desc string + dynamicCerts *safe.Safe + staticCerts map[string]*tls.Certificate + acmeCertificates []*Certificate + domains []string + expectedDomains []string + }{ + { + desc: "wildcard to generate", + domains: []string{"*.traefik.wtf"}, + expectedDomains: []string{"*.traefik.wtf"}, + }, + { + desc: "wildcard already exists in dynamic certificates", + domains: []string{"*.traefik.wtf"}, + dynamicCerts: wildcardSafe, + expectedDomains: nil, + }, + { + desc: "wildcard already exists in static certificates", + domains: []string{"*.traefik.wtf"}, + staticCerts: wildcardMap, + expectedDomains: nil, + }, + { + desc: "wildcard already exists in ACME certificates", + domains: []string{"*.traefik.wtf"}, + acmeCertificates: []*Certificate{ + { + Domain: types.Domain{Main: "*.traefik.wtf"}, + }, + }, + expectedDomains: nil, + }, + { + desc: "domain CN and SANs to generate", + domains: []string{"traefik.wtf", "foo.traefik.wtf"}, + expectedDomains: []string{"traefik.wtf", "foo.traefik.wtf"}, + }, + { + desc: "domain CN already exists in dynamic certificates and SANs to generate", + domains: []string{"traefik.wtf", "foo.traefik.wtf"}, + dynamicCerts: domainSafe, + expectedDomains: []string{"foo.traefik.wtf"}, + }, + { + desc: "domain CN already exists in static certificates and SANs to generate", + domains: []string{"traefik.wtf", "foo.traefik.wtf"}, + staticCerts: domainMap, + expectedDomains: []string{"foo.traefik.wtf"}, + }, + { + desc: "domain CN already exists in ACME certificates and SANs to generate", + domains: []string{"traefik.wtf", "foo.traefik.wtf"}, + acmeCertificates: []*Certificate{ + { + Domain: types.Domain{Main: "traefik.wtf"}, + }, + }, + expectedDomains: []string{"foo.traefik.wtf"}, + }, + { + desc: "domain already exists in dynamic certificates", + domains: []string{"traefik.wtf"}, + dynamicCerts: domainSafe, + expectedDomains: nil, + }, + { + desc: "domain already exists in static certificates", + domains: []string{"traefik.wtf"}, + staticCerts: domainMap, + expectedDomains: nil, + }, + { + desc: "domain already exists in ACME certificates", + domains: []string{"traefik.wtf"}, + acmeCertificates: []*Certificate{ + { + Domain: types.Domain{Main: "traefik.wtf"}, + }, + }, + expectedDomains: nil, + }, + { + desc: "domain matched by wildcard in dynamic certificates", + domains: []string{"who.traefik.wtf", "foo.traefik.wtf"}, + dynamicCerts: wildcardSafe, + expectedDomains: nil, + }, + { + desc: "domain matched by wildcard in static certificates", + domains: []string{"who.traefik.wtf", "foo.traefik.wtf"}, + staticCerts: wildcardMap, + expectedDomains: nil, + }, + { + desc: "domain matched by wildcard in ACME certificates", + domains: []string{"who.traefik.wtf", "foo.traefik.wtf"}, + acmeCertificates: []*Certificate{ + { + Domain: types.Domain{Main: "*.traefik.wtf"}, + }, + }, + expectedDomains: nil, + }, + { + desc: "root domain with wildcard in ACME certificates", + domains: []string{"traefik.wtf", "foo.traefik.wtf"}, + acmeCertificates: []*Certificate{ + { + Domain: types.Domain{Main: "*.traefik.wtf"}, + }, + }, + expectedDomains: []string{"traefik.wtf"}, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + acmeProvider := Provider{ + dynamicCerts: test.dynamicCerts, + staticCerts: test.staticCerts, + certificates: test.acmeCertificates, + } + + domains := acmeProvider.getUncheckedDomains(test.domains, false) + assert.Equal(t, len(test.expectedDomains), len(domains), "Unexpected domains.") + }) + } +} + +func TestGetValidDomain(t *testing.T) { + testCases := []struct { + desc string + domains types.Domain + wildcardAllowed bool + dnsChallenge *DNSChallenge + expectedErr string + expectedDomains []string + }{ + { + desc: "valid wildcard", + domains: types.Domain{Main: "*.traefik.wtf"}, + dnsChallenge: &DNSChallenge{}, + wildcardAllowed: true, + expectedErr: "", + expectedDomains: []string{"*.traefik.wtf"}, + }, + { + desc: "no wildcard", + domains: types.Domain{Main: "traefik.wtf", SANs: []string{"foo.traefik.wtf"}}, + dnsChallenge: &DNSChallenge{}, + expectedErr: "", + wildcardAllowed: true, + expectedDomains: []string{"traefik.wtf", "foo.traefik.wtf"}, + }, + { + desc: "unauthorized wildcard", + domains: types.Domain{Main: "*.traefik.wtf"}, + dnsChallenge: &DNSChallenge{}, + wildcardAllowed: false, + expectedErr: "unable to generate a wildcard certificate in ACME provider for domain \"*.traefik.wtf\" from a 'Host' rule", + expectedDomains: nil, + }, + { + desc: "no domain", + domains: types.Domain{}, + dnsChallenge: nil, + wildcardAllowed: true, + expectedErr: "unable to generate a certificate in ACME provider when no domain is given", + expectedDomains: nil, + }, + { + desc: "no DNSChallenge", + domains: types.Domain{Main: "*.traefik.wtf", SANs: []string{"foo.traefik.wtf"}}, + dnsChallenge: nil, + wildcardAllowed: true, + expectedErr: "unable to generate a wildcard certificate in ACME provider for domain \"*.traefik.wtf,foo.traefik.wtf\" : ACME needs a DNSChallenge", + expectedDomains: nil, + }, + { + desc: "unexpected SANs", + domains: types.Domain{Main: "*.traefik.wtf", SANs: []string{"foo.traefik.wtf"}}, + dnsChallenge: &DNSChallenge{}, + wildcardAllowed: true, + expectedErr: "unable to generate a wildcard certificate in ACME provider for domain \"*.traefik.wtf,foo.traefik.wtf\" : SANs are not allowed", + expectedDomains: nil, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + acmeProvider := Provider{Configuration: &Configuration{DNSChallenge: test.dnsChallenge}} + + domains, err := acmeProvider.getValidDomains(test.domains, test.wildcardAllowed) + + if len(test.expectedErr) > 0 { + assert.EqualError(t, err, test.expectedErr, "Unexpected error.") + } else { + assert.Equal(t, len(test.expectedDomains), len(domains), "Unexpected domains.") + } + }) + } +} + +func TestDeleteUnnecessaryDomains(t *testing.T) { + testCases := []struct { + desc string + domains []types.Domain + expectedDomains []types.Domain + }{ + { + desc: "no domain to delete", + domains: []types.Domain{ + { + Main: "acme.wtf", + SANs: []string{"traefik.acme.wtf", "foo.bar"}, + }, + { + Main: "*.foo.acme.wtf", + }, + { + Main: "acme.wtf", + SANs: []string{"traefik.acme.wtf", "bar.foo"}, + }, + }, + expectedDomains: []types.Domain{ + { + Main: "acme.wtf", + SANs: []string{"traefik.acme.wtf", "foo.bar"}, + }, + { + Main: "*.foo.acme.wtf", + }, + { + Main: "acme.wtf", + SANs: []string{"traefik.acme.wtf", "bar.foo"}, + }, + }, + }, + { + desc: "2 domains with same values", + domains: []types.Domain{ + { + Main: "acme.wtf", + SANs: []string{"traefik.acme.wtf", "foo.bar"}, + }, + { + Main: "acme.wtf", + SANs: []string{"traefik.acme.wtf", "foo.bar"}, + }, + }, + expectedDomains: []types.Domain{ + { + Main: "acme.wtf", + SANs: []string{"traefik.acme.wtf", "foo.bar"}, + }, + }, + }, + { + desc: "domain totally checked by wildcard", + domains: []types.Domain{ + { + Main: "who.acme.wtf", + SANs: []string{"traefik.acme.wtf", "bar.acme.wtf"}, + }, + { + Main: "*.acme.wtf", + }, + }, + expectedDomains: []types.Domain{ + { + Main: "*.acme.wtf", + }, + }, + }, + { + desc: "domain partially checked by wildcard", + domains: []types.Domain{ + { + Main: "traefik.acme.wtf", + SANs: []string{"acme.wtf", "foo.bar"}, + }, + { + Main: "*.acme.wtf", + }, + }, + expectedDomains: []types.Domain{ + { + Main: "acme.wtf", + SANs: []string{"foo.bar"}, + }, + { + Main: "*.acme.wtf", + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + acmeProvider := Provider{Configuration: &Configuration{Domains: test.domains}} + + acmeProvider.deleteUnnecessaryDomains() + assert.Equal(t, test.expectedDomains, acmeProvider.Domains, "unexpected domain") + }) + } +} diff --git a/types/domain_test.go b/types/domain_test.go new file mode 100644 index 000000000..911064a9c --- /dev/null +++ b/types/domain_test.go @@ -0,0 +1,90 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDomain_ToStrArray(t *testing.T) { + testCases := []struct { + desc string + domain Domain + expected []string + }{ + { + desc: "with Main and SANs", + domain: Domain{ + Main: "foo.com", + SANs: []string{"bar.foo.com", "bir.foo.com"}, + }, + expected: []string{"foo.com", "bar.foo.com", "bir.foo.com"}, + }, + { + desc: "without SANs", + domain: Domain{ + Main: "foo.com", + }, + expected: []string{"foo.com"}, + }, + { + desc: "without Main", + domain: Domain{ + SANs: []string{"bar.foo.com", "bir.foo.com"}, + }, + expected: []string{"bar.foo.com", "bir.foo.com"}, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + domains := test.domain.ToStrArray() + assert.EqualValues(t, test.expected, domains) + }) + } +} + +func TestDomain_Set(t *testing.T) { + testCases := []struct { + desc string + rawDomains []string + expected Domain + }{ + { + desc: "with 3 domains", + rawDomains: []string{"foo.com", "bar.foo.com", "bir.foo.com"}, + expected: Domain{ + Main: "foo.com", + SANs: []string{"bar.foo.com", "bir.foo.com"}, + }, + }, + { + desc: "with 1 domain", + rawDomains: []string{"foo.com"}, + expected: Domain{ + Main: "foo.com", + SANs: []string{}, + }, + }, + { + desc: "", + rawDomains: nil, + expected: Domain{}, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + domain := Domain{} + domain.Set(test.rawDomains) + + assert.Equal(t, test.expected, domain) + }) + } +} diff --git a/types/domains.go b/types/domains.go index fee857fc0..47bae5468 100644 --- a/types/domains.go +++ b/types/domains.go @@ -11,6 +11,23 @@ type Domain struct { SANs []string } +// ToStrArray convert a domain into an array of strings +func (d *Domain) ToStrArray() []string { + var domains []string + if len(d.Main) > 0 { + domains = []string{d.Main} + } + return append(domains, d.SANs...) +} + +// Set sets a domains from an array of strings +func (d *Domain) Set(domains []string) { + if len(domains) > 0 { + d.Main = domains[0] + d.SANs = domains[1:] + } +} + // Domains parse []Domain type Domains []Domain diff --git a/vendor/github.com/xenolf/lego/acmev2/challenges.go b/vendor/github.com/xenolf/lego/acmev2/challenges.go new file mode 100644 index 000000000..cf7bd7f75 --- /dev/null +++ b/vendor/github.com/xenolf/lego/acmev2/challenges.go @@ -0,0 +1,13 @@ +package acme + +// Challenge is a string that identifies a particular type and version of ACME challenge. +type Challenge string + +const ( + // HTTP01 is the "http-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http + // Note: HTTP01ChallengePath returns the URL path to fulfill this challenge + HTTP01 = Challenge("http-01") + // DNS01 is the "dns-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns + // Note: DNS01Record returns a DNS record which will fulfill this challenge + DNS01 = Challenge("dns-01") +) diff --git a/vendor/github.com/xenolf/lego/acmev2/client.go b/vendor/github.com/xenolf/lego/acmev2/client.go new file mode 100644 index 000000000..40acdbc14 --- /dev/null +++ b/vendor/github.com/xenolf/lego/acmev2/client.go @@ -0,0 +1,801 @@ +// Package acme implements the ACME protocol for Let's Encrypt and other conforming providers. +package acme + +import ( + "crypto" + "crypto/x509" + "encoding/base64" + "errors" + "fmt" + "io/ioutil" + "log" + "net" + "regexp" + "strconv" + "strings" + "time" +) + +var ( + // Logger is an optional custom logger. + Logger *log.Logger +) + +const ( + // maxBodySize is the maximum size of body that we will read. + maxBodySize = 1024 * 1024 + + // overallRequestLimit is the overall number of request per second limited on the + // “new-reg”, “new-authz” and “new-cert” endpoints. From the documentation the + // limitation is 20 requests per second, but using 20 as value doesn't work but 18 do + overallRequestLimit = 18 +) + +// logf writes a log entry. It uses Logger if not +// nil, otherwise it uses the default log.Logger. +func logf(format string, args ...interface{}) { + if Logger != nil { + Logger.Printf(format, args...) + } else { + log.Printf(format, args...) + } +} + +// User interface is to be implemented by users of this library. +// It is used by the client type to get user specific information. +type User interface { + GetEmail() string + GetRegistration() *RegistrationResource + GetPrivateKey() crypto.PrivateKey +} + +// Interface for all challenge solvers to implement. +type solver interface { + Solve(challenge challenge, domain string) error +} + +type validateFunc func(j *jws, domain, uri string, chlng challenge) error + +// Client is the user-friendy way to ACME +type Client struct { + directory directory + user User + jws *jws + keyType KeyType + solvers map[Challenge]solver +} + +// NewClient creates a new ACME client on behalf of the user. The client will depend on +// the ACME directory located at caDirURL for the rest of its actions. A private +// key of type keyType (see KeyType contants) will be generated when requesting a new +// certificate if one isn't provided. +func NewClient(caDirURL string, user User, keyType KeyType) (*Client, error) { + privKey := user.GetPrivateKey() + if privKey == nil { + return nil, errors.New("private key was nil") + } + + var dir directory + if _, err := getJSON(caDirURL, &dir); err != nil { + return nil, fmt.Errorf("get directory at '%s': %v", caDirURL, err) + } + + if dir.NewAccountURL == "" { + return nil, errors.New("directory missing new registration URL") + } + if dir.NewOrderURL == "" { + return nil, errors.New("directory missing new order URL") + } + /*if dir.RevokeCertURL == "" { + return nil, errors.New("directory missing revoke certificate URL") + }*/ + + jws := &jws{privKey: privKey, getNonceURL: dir.NewNonceURL} + if reg := user.GetRegistration(); reg != nil { + jws.kid = reg.URI + } + + // REVIEW: best possibility? + // Add all available solvers with the right index as per ACME + // spec to this map. Otherwise they won`t be found. + solvers := make(map[Challenge]solver) + solvers[HTTP01] = &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}} + + return &Client{directory: dir, user: user, jws: jws, keyType: keyType, solvers: solvers}, nil +} + +// SetChallengeProvider specifies a custom provider p that can solve the given challenge type. +func (c *Client) SetChallengeProvider(challenge Challenge, p ChallengeProvider) error { + switch challenge { + case HTTP01: + c.solvers[challenge] = &httpChallenge{jws: c.jws, validate: validate, provider: p} + case DNS01: + c.solvers[challenge] = &dnsChallenge{jws: c.jws, validate: validate, provider: p} + default: + return fmt.Errorf("Unknown challenge %v", challenge) + } + return nil +} + +// SetHTTPAddress specifies a custom interface:port to be used for HTTP based challenges. +// If this option is not used, the default port 80 and all interfaces will be used. +// To only specify a port and no interface use the ":port" notation. +// +// NOTE: This REPLACES any custom HTTP provider previously set by calling +// c.SetChallengeProvider with the default HTTP challenge provider. +func (c *Client) SetHTTPAddress(iface string) error { + host, port, err := net.SplitHostPort(iface) + if err != nil { + return err + } + + if chlng, ok := c.solvers[HTTP01]; ok { + chlng.(*httpChallenge).provider = NewHTTPProviderServer(host, port) + } + + return nil +} + +// ExcludeChallenges explicitly removes challenges from the pool for solving. +func (c *Client) ExcludeChallenges(challenges []Challenge) { + // Loop through all challenges and delete the requested one if found. + for _, challenge := range challenges { + delete(c.solvers, challenge) + } +} + +// GetToSURL returns the current ToS URL from the Directory +func (c *Client) GetToSURL() string { + return c.directory.Meta.TermsOfService +} + +// Register the current account to the ACME server. +func (c *Client) Register(tosAgreed bool) (*RegistrationResource, error) { + if c == nil || c.user == nil { + return nil, errors.New("acme: cannot register a nil client or user") + } + logf("[INFO] acme: Registering account for %s", c.user.GetEmail()) + + accMsg := accountMessage{} + if c.user.GetEmail() != "" { + accMsg.Contact = []string{"mailto:" + c.user.GetEmail()} + } else { + accMsg.Contact = []string{} + } + accMsg.TermsOfServiceAgreed = tosAgreed + + var serverReg accountMessage + hdr, err := postJSON(c.jws, c.directory.NewAccountURL, accMsg, &serverReg) + if err != nil { + remoteErr, ok := err.(RemoteError) + if ok && remoteErr.StatusCode == 409 { + } else { + return nil, err + } + } + + reg := &RegistrationResource{ + URI: hdr.Get("Location"), + Body: serverReg, + } + c.jws.kid = reg.URI + + return reg, nil +} + +// ResolveAccountByKey will attempt to look up an account using the given account key +// and return its registration resource. +func (c *Client) ResolveAccountByKey() (*RegistrationResource, error) { + logf("[INFO] acme: Trying to resolve account by key") + + acc := accountMessage{OnlyReturnExisting: true} + hdr, err := postJSON(c.jws, c.directory.NewAccountURL, acc, &acc) + if err != nil { + return nil, err + } + + accountLink := hdr.Get("Location") + if accountLink == "" { + return nil, errors.New("Server did not return the account link") + } + + var retAccount accountMessage + c.jws.kid = accountLink + hdr, err = postJSON(c.jws, accountLink, accountMessage{}, &retAccount) + if err != nil { + return nil, err + } + + return &RegistrationResource{URI: accountLink, Body: retAccount}, nil +} + +// DeleteRegistration deletes the client's user registration from the ACME +// server. +func (c *Client) DeleteRegistration() error { + if c == nil || c.user == nil { + return errors.New("acme: cannot unregister a nil client or user") + } + logf("[INFO] acme: Deleting account for %s", c.user.GetEmail()) + + accMsg := accountMessage{ + Status: "deactivated", + } + + _, err := postJSON(c.jws, c.user.GetRegistration().URI, accMsg, nil) + if err != nil { + return err + } + + return nil +} + +// QueryRegistration runs a POST request on the client's registration and +// returns the result. +// +// This is similar to the Register function, but acting on an existing +// registration link and resource. +func (c *Client) QueryRegistration() (*RegistrationResource, error) { + if c == nil || c.user == nil { + return nil, errors.New("acme: cannot query the registration of a nil client or user") + } + // Log the URL here instead of the email as the email may not be set + logf("[INFO] acme: Querying account for %s", c.user.GetRegistration().URI) + + accMsg := accountMessage{} + + var serverReg accountMessage + _, err := postJSON(c.jws, c.user.GetRegistration().URI, accMsg, &serverReg) + if err != nil { + return nil, err + } + + reg := &RegistrationResource{Body: serverReg} + + // Location: header is not returned so this needs to be populated off of + // existing URI + reg.URI = c.user.GetRegistration().URI + + return reg, nil +} + +// ObtainCertificateForCSR tries to obtain a certificate matching the CSR passed into it. +// The domains are inferred from the CommonName and SubjectAltNames, if any. The private key +// for this CSR is not required. +// If bundle is true, the []byte contains both the issuer certificate and +// your issued certificate as a bundle. +// This function will never return a partial certificate. If one domain in the list fails, +// the whole certificate will fail. +func (c *Client) ObtainCertificateForCSR(csr x509.CertificateRequest, bundle bool) (CertificateResource, map[string]error) { + // figure out what domains it concerns + // start with the common name + domains := []string{csr.Subject.CommonName} + + // loop over the SubjectAltName DNS names +DNSNames: + for _, sanName := range csr.DNSNames { + for _, existingName := range domains { + if existingName == sanName { + // duplicate; skip this name + continue DNSNames + } + } + + // name is unique + domains = append(domains, sanName) + } + + if bundle { + logf("[INFO][%s] acme: Obtaining bundled SAN certificate given a CSR", strings.Join(domains, ", ")) + } else { + logf("[INFO][%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", ")) + } + + order, err := c.createOrderForIdentifiers(domains) + if err != nil { + identErrors := make(map[string]error) + for _, auth := range order.Identifiers { + identErrors[auth.Value] = err + } + return CertificateResource{}, identErrors + } + authz, failures := c.getAuthzForOrder(order) + // If any challenge fails - return. Do not generate partial SAN certificates. + if len(failures) > 0 { + /*for _, auth := range authz { + c.disableAuthz(auth) + }*/ + + return CertificateResource{}, failures + } + + errs := c.solveChallengeForAuthz(authz) + // If any challenge fails - return. Do not generate partial SAN certificates. + if len(errs) > 0 { + return CertificateResource{}, errs + } + + logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) + + cert, err := c.requestCertificateForCsr(order, bundle, csr.Raw, nil) + if err != nil { + for _, chln := range authz { + failures[chln.Identifier.Value] = err + } + } + + // Add the CSR to the certificate so that it can be used for renewals. + cert.CSR = pemEncode(&csr) + + return cert, failures +} + +// ObtainCertificate tries to obtain a single certificate using all domains passed into it. +// The first domain in domains is used for the CommonName field of the certificate, all other +// domains are added using the Subject Alternate Names extension. A new private key is generated +// for every invocation of this function. If you do not want that you can supply your own private key +// in the privKey parameter. If this parameter is non-nil it will be used instead of generating a new one. +// If bundle is true, the []byte contains both the issuer certificate and +// your issued certificate as a bundle. +// This function will never return a partial certificate. If one domain in the list fails, +// the whole certificate will fail. +func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (CertificateResource, map[string]error) { + if bundle { + logf("[INFO][%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", ")) + } else { + logf("[INFO][%s] acme: Obtaining SAN certificate", strings.Join(domains, ", ")) + } + + order, err := c.createOrderForIdentifiers(domains) + if err != nil { + identErrors := make(map[string]error) + for _, auth := range order.Identifiers { + identErrors[auth.Value] = err + } + return CertificateResource{}, identErrors + } + authz, failures := c.getAuthzForOrder(order) + // If any challenge fails - return. Do not generate partial SAN certificates. + if len(failures) > 0 { + /*for _, auth := range authz { + c.disableAuthz(auth) + }*/ + + return CertificateResource{}, failures + } + + errs := c.solveChallengeForAuthz(authz) + // If any challenge fails - return. Do not generate partial SAN certificates. + if len(errs) > 0 { + return CertificateResource{}, errs + } + + logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) + + cert, err := c.requestCertificateForOrder(order, bundle, privKey, mustStaple) + if err != nil { + for _, auth := range authz { + failures[auth.Identifier.Value] = err + } + } + + return cert, failures +} + +// RevokeCertificate takes a PEM encoded certificate or bundle and tries to revoke it at the CA. +func (c *Client) RevokeCertificate(certificate []byte) error { + certificates, err := parsePEMBundle(certificate) + if err != nil { + return err + } + + x509Cert := certificates[0] + if x509Cert.IsCA { + return fmt.Errorf("Certificate bundle starts with a CA certificate") + } + + encodedCert := base64.URLEncoding.EncodeToString(x509Cert.Raw) + + _, err = postJSON(c.jws, c.directory.RevokeCertURL, revokeCertMessage{Certificate: encodedCert}, nil) + return err +} + +// RenewCertificate takes a CertificateResource and tries to renew the certificate. +// If the renewal process succeeds, the new certificate will ge returned in a new CertResource. +// Please be aware that this function will return a new certificate in ANY case that is not an error. +// If the server does not provide us with a new cert on a GET request to the CertURL +// this function will start a new-cert flow where a new certificate gets generated. +// If bundle is true, the []byte contains both the issuer certificate and +// your issued certificate as a bundle. +// For private key reuse the PrivateKey property of the passed in CertificateResource should be non-nil. +func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple bool) (CertificateResource, error) { + // Input certificate is PEM encoded. Decode it here as we may need the decoded + // cert later on in the renewal process. The input may be a bundle or a single certificate. + certificates, err := parsePEMBundle(cert.Certificate) + if err != nil { + return CertificateResource{}, err + } + + x509Cert := certificates[0] + if x509Cert.IsCA { + return CertificateResource{}, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", cert.Domain) + } + + // This is just meant to be informal for the user. + timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC()) + logf("[INFO][%s] acme: Trying renewal with %d hours remaining", cert.Domain, int(timeLeft.Hours())) + + // We always need to request a new certificate to renew. + // Start by checking to see if the certificate was based off a CSR, and + // use that if it's defined. + if len(cert.CSR) > 0 { + csr, err := pemDecodeTox509CSR(cert.CSR) + if err != nil { + return CertificateResource{}, err + } + newCert, failures := c.ObtainCertificateForCSR(*csr, bundle) + return newCert, failures[cert.Domain] + } + + var privKey crypto.PrivateKey + if cert.PrivateKey != nil { + privKey, err = parsePEMPrivateKey(cert.PrivateKey) + if err != nil { + return CertificateResource{}, err + } + } + + var domains []string + var failures map[string]error + // check for SAN certificate + if len(x509Cert.DNSNames) > 1 { + domains = append(domains, x509Cert.Subject.CommonName) + for _, sanDomain := range x509Cert.DNSNames { + if sanDomain == x509Cert.Subject.CommonName { + continue + } + domains = append(domains, sanDomain) + } + } else { + domains = append(domains, x509Cert.Subject.CommonName) + } + + newCert, failures := c.ObtainCertificate(domains, bundle, privKey, mustStaple) + return newCert, failures[cert.Domain] +} + +func (c *Client) createOrderForIdentifiers(domains []string) (orderResource, error) { + + var identifiers []identifier + for _, domain := range domains { + identifiers = append(identifiers, identifier{Type: "dns", Value: domain}) + } + + order := orderMessage{ + Identifiers: identifiers, + } + + var response orderMessage + hdr, err := postJSON(c.jws, c.directory.NewOrderURL, order, &response) + if err != nil { + return orderResource{}, err + } + + orderRes := orderResource{ + URL: hdr.Get("Location"), + orderMessage: response, + } + return orderRes, nil +} + +// Looks through the challenge combinations to find a solvable match. +// Then solves the challenges in series and returns. +func (c *Client) solveChallengeForAuthz(authorizations []authorization) map[string]error { + // loop through the resources, basically through the domains. + failures := make(map[string]error) + for _, authz := range authorizations { + if authz.Status == "valid" { + // Boulder might recycle recent validated authz (see issue #267) + logf("[INFO][%s] acme: Authorization already valid; skipping challenge", authz.Identifier.Value) + continue + } + + // no solvers - no solving + if i, solver := c.chooseSolver(authz, authz.Identifier.Value); solver != nil { + err := solver.Solve(authz.Challenges[i], authz.Identifier.Value) + if err != nil { + //c.disableAuthz(authz.Identifier) + failures[authz.Identifier.Value] = err + } + } else { + //c.disableAuthz(authz) + failures[authz.Identifier.Value] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Identifier.Value) + } + } + + return failures +} + +// Checks all challenges from the server in order and returns the first matching solver. +func (c *Client) chooseSolver(auth authorization, domain string) (int, solver) { + for i, challenge := range auth.Challenges { + if solver, ok := c.solvers[Challenge(challenge.Type)]; ok { + return i, solver + } + logf("[INFO][%s] acme: Could not find solver for: %s", domain, challenge.Type) + } + return 0, nil +} + +// Get the challenges needed to proof our identifier to the ACME server. +func (c *Client) getAuthzForOrder(order orderResource) ([]authorization, map[string]error) { + resc, errc := make(chan authorization), make(chan domainError) + + delay := time.Second / overallRequestLimit + + for _, authzURL := range order.Authorizations { + time.Sleep(delay) + + go func(authzURL string) { + var authz authorization + _, err := getJSON(authzURL, &authz) + if err != nil { + errc <- domainError{Domain: authz.Identifier.Value, Error: err} + return + } + + resc <- authz + }(authzURL) + } + + var responses []authorization + failures := make(map[string]error) + for i := 0; i < len(order.Authorizations); i++ { + select { + case res := <-resc: + responses = append(responses, res) + case err := <-errc: + failures[err.Domain] = err.Error + } + } + + logAuthz(order) + + close(resc) + close(errc) + + return responses, failures +} + +func logAuthz(order orderResource) { + for i, auth := range order.Authorizations { + logf("[INFO][%s] AuthURL: %s", order.Identifiers[i].Value, auth) + } +} + +// cleanAuthz loops through the passed in slice and disables any auths which are not "valid" +func (c *Client) disableAuthz(authURL string) error { + var disabledAuth authorization + _, err := postJSON(c.jws, authURL, deactivateAuthMessage{Status: "deactivated"}, &disabledAuth) + return err +} + +func (c *Client) requestCertificateForOrder(order orderResource, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (CertificateResource, error) { + + var err error + if privKey == nil { + privKey, err = generatePrivateKey(c.keyType) + if err != nil { + return CertificateResource{}, err + } + } + + // determine certificate name(s) based on the authorization resources + commonName := order.Identifiers[0].Value + var san []string + for _, auth := range order.Identifiers { + san = append(san, auth.Value) + } + + // TODO: should the CSR be customizable? + csr, err := generateCsr(privKey, commonName, san, mustStaple) + if err != nil { + return CertificateResource{}, err + } + + return c.requestCertificateForCsr(order, bundle, csr, pemEncode(privKey)) +} + +func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr []byte, privateKeyPem []byte) (CertificateResource, error) { + commonName := order.Identifiers[0].Value + + var authURLs []string + for _, auth := range order.Identifiers[1:] { + authURLs = append(authURLs, auth.Value) + } + + csrString := base64.RawURLEncoding.EncodeToString(csr) + var retOrder orderMessage + _, error := postJSON(c.jws, order.Finalize, csrMessage{Csr: csrString}, &retOrder) + if error != nil { + return CertificateResource{}, error + } + + if retOrder.Status == "invalid" { + return CertificateResource{}, error + } + + certRes := CertificateResource{ + Domain: commonName, + CertURL: retOrder.Certificate, + PrivateKey: privateKeyPem, + } + + if retOrder.Status == "valid" { + // if the certificate is available right away, short cut! + ok, err := c.checkCertResponse(retOrder, &certRes, bundle) + if err != nil { + return CertificateResource{}, err + } + + if ok { + return certRes, nil + } + } + + maxChecks := 1000 + for i := 0; i < maxChecks; i++ { + _, err := getJSON(order.URL, &retOrder) + if err != nil { + return CertificateResource{}, err + } + done, err := c.checkCertResponse(retOrder, &certRes, bundle) + if err != nil { + return CertificateResource{}, err + } + if done { + break + } + if i == maxChecks-1 { + return CertificateResource{}, fmt.Errorf("polled for certificate %d times; giving up", i) + } + } + + return certRes, nil +} + +// checkCertResponse checks to see if the certificate is ready and a link is contained in the +// response. if so, loads it into certRes and returns true. If the cert +// is not yet ready, it returns false. The certRes input +// should already have the Domain (common name) field populated. If bundle is +// true, the certificate will be bundled with the issuer's cert. +func (c *Client) checkCertResponse(order orderMessage, certRes *CertificateResource, bundle bool) (bool, error) { + + switch order.Status { + case "valid": + resp, err := httpGet(order.Certificate) + if err != nil { + return false, err + } + + cert, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize)) + if err != nil { + return false, err + } + + // The issuer certificate link is always supplied via an "up" link + // in the response headers of a new certificate. + links := parseLinks(resp.Header["Link"]) + if link, ok := links["up"]; ok { + issuerCert, err := c.getIssuerCertificate(link) + + if err != nil { + // If we fail to acquire the issuer cert, return the issued certificate - do not fail. + logf("[WARNING][%s] acme: Could not bundle issuer certificate: %v", certRes.Domain, err) + } else { + issuerCert = pemEncode(derCertificateBytes(issuerCert)) + + // If bundle is true, we want to return a certificate bundle. + // To do this, we append the issuer cert to the issued cert. + if bundle { + cert = append(cert, issuerCert...) + } + + certRes.IssuerCertificate = issuerCert + } + } + + certRes.Certificate = cert + certRes.CertURL = order.Certificate + certRes.CertStableURL = order.Certificate + logf("[INFO][%s] Server responded with a certificate.", certRes.Domain) + return true, nil + + case "processing": + return false, nil + case "invalid": + return false, errors.New("Order has invalid state: invalid") + } + + return false, nil +} + +// getIssuerCertificate requests the issuer certificate +func (c *Client) getIssuerCertificate(url string) ([]byte, error) { + logf("[INFO] acme: Requesting issuer cert from %s", url) + resp, err := httpGet(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize)) + if err != nil { + return nil, err + } + + _, err = x509.ParseCertificate(issuerBytes) + if err != nil { + return nil, err + } + + return issuerBytes, err +} + +func parseLinks(links []string) map[string]string { + aBrkt := regexp.MustCompile("[<>]") + slver := regexp.MustCompile("(.+) *= *\"(.+)\"") + linkMap := make(map[string]string) + + for _, link := range links { + + link = aBrkt.ReplaceAllString(link, "") + parts := strings.Split(link, ";") + + matches := slver.FindStringSubmatch(parts[1]) + if len(matches) > 0 { + linkMap[matches[2]] = parts[0] + } + } + + return linkMap +} + +// validate makes the ACME server start validating a +// challenge response, only returning once it is done. +func validate(j *jws, domain, uri string, c challenge) error { + var chlng challenge + + hdr, err := postJSON(j, uri, c, &chlng) + if err != nil { + return err + } + + // After the path is sent, the ACME server will access our server. + // Repeatedly check the server for an updated status on our request. + for { + switch chlng.Status { + case "valid": + logf("[INFO][%s] The server validated our request", domain) + return nil + case "pending": + break + case "invalid": + return handleChallengeError(chlng) + default: + return errors.New("The server returned an unexpected state") + } + + ra, err := strconv.Atoi(hdr.Get("Retry-After")) + if err != nil { + // The ACME server MUST return a Retry-After. + // If it doesn't, we'll just poll hard. + ra = 5 + } + time.Sleep(time.Duration(ra) * time.Second) + + hdr, err = getJSON(uri, &chlng) + if err != nil { + return err + } + } +} diff --git a/vendor/github.com/xenolf/lego/acmev2/crypto.go b/vendor/github.com/xenolf/lego/acmev2/crypto.go new file mode 100644 index 000000000..e50ca30d8 --- /dev/null +++ b/vendor/github.com/xenolf/lego/acmev2/crypto.go @@ -0,0 +1,343 @@ +package acme + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" + "io" + "io/ioutil" + "math/big" + "net/http" + "time" + + "encoding/asn1" + + "golang.org/x/crypto/ocsp" + jose "gopkg.in/square/go-jose.v2" +) + +// KeyType represents the key algo as well as the key size or curve to use. +type KeyType string +type derCertificateBytes []byte + +// Constants for all key types we support. +const ( + EC256 = KeyType("P256") + EC384 = KeyType("P384") + RSA2048 = KeyType("2048") + RSA4096 = KeyType("4096") + RSA8192 = KeyType("8192") +) + +const ( + // OCSPGood means that the certificate is valid. + OCSPGood = ocsp.Good + // OCSPRevoked means that the certificate has been deliberately revoked. + OCSPRevoked = ocsp.Revoked + // OCSPUnknown means that the OCSP responder doesn't know about the certificate. + OCSPUnknown = ocsp.Unknown + // OCSPServerFailed means that the OCSP responder failed to process the request. + OCSPServerFailed = ocsp.ServerFailed +) + +// Constants for OCSP must staple +var ( + tlsFeatureExtensionOID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24} + ocspMustStapleFeature = []byte{0x30, 0x03, 0x02, 0x01, 0x05} +) + +// GetOCSPForCert takes a PEM encoded cert or cert bundle returning the raw OCSP response, +// the parsed response, and an error, if any. The returned []byte can be passed directly +// into the OCSPStaple property of a tls.Certificate. If the bundle only contains the +// issued certificate, this function will try to get the issuer certificate from the +// IssuingCertificateURL in the certificate. If the []byte and/or ocsp.Response return +// values are nil, the OCSP status may be assumed OCSPUnknown. +func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) { + certificates, err := parsePEMBundle(bundle) + if err != nil { + return nil, nil, err + } + + // We expect the certificate slice to be ordered downwards the chain. + // SRV CRT -> CA. We need to pull the leaf and issuer certs out of it, + // which should always be the first two certificates. If there's no + // OCSP server listed in the leaf cert, there's nothing to do. And if + // we have only one certificate so far, we need to get the issuer cert. + issuedCert := certificates[0] + if len(issuedCert.OCSPServer) == 0 { + return nil, nil, errors.New("no OCSP server specified in cert") + } + if len(certificates) == 1 { + // TODO: build fallback. If this fails, check the remaining array entries. + if len(issuedCert.IssuingCertificateURL) == 0 { + return nil, nil, errors.New("no issuing certificate URL") + } + + resp, err := httpGet(issuedCert.IssuingCertificateURL[0]) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024)) + if err != nil { + return nil, nil, err + } + + issuerCert, err := x509.ParseCertificate(issuerBytes) + if err != nil { + return nil, nil, err + } + + // Insert it into the slice on position 0 + // We want it ordered right SRV CRT -> CA + certificates = append(certificates, issuerCert) + } + issuerCert := certificates[1] + + // Finally kick off the OCSP request. + ocspReq, err := ocsp.CreateRequest(issuedCert, issuerCert, nil) + if err != nil { + return nil, nil, err + } + + reader := bytes.NewReader(ocspReq) + req, err := httpPost(issuedCert.OCSPServer[0], "application/ocsp-request", reader) + if err != nil { + return nil, nil, err + } + defer req.Body.Close() + + ocspResBytes, err := ioutil.ReadAll(limitReader(req.Body, 1024*1024)) + ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert) + if err != nil { + return nil, nil, err + } + + return ocspResBytes, ocspRes, nil +} + +func getKeyAuthorization(token string, key interface{}) (string, error) { + var publicKey crypto.PublicKey + switch k := key.(type) { + case *ecdsa.PrivateKey: + publicKey = k.Public() + case *rsa.PrivateKey: + publicKey = k.Public() + } + + // Generate the Key Authorization for the challenge + jwk := &jose.JSONWebKey{Key: publicKey} + if jwk == nil { + return "", errors.New("Could not generate JWK from key") + } + thumbBytes, err := jwk.Thumbprint(crypto.SHA256) + if err != nil { + return "", err + } + + // unpad the base64URL + keyThumb := base64.RawURLEncoding.EncodeToString(thumbBytes) + + return token + "." + keyThumb, nil +} + +// parsePEMBundle parses a certificate bundle from top to bottom and returns +// a slice of x509 certificates. This function will error if no certificates are found. +func parsePEMBundle(bundle []byte) ([]*x509.Certificate, error) { + var certificates []*x509.Certificate + var certDERBlock *pem.Block + + for { + certDERBlock, bundle = pem.Decode(bundle) + if certDERBlock == nil { + break + } + + if certDERBlock.Type == "CERTIFICATE" { + cert, err := x509.ParseCertificate(certDERBlock.Bytes) + if err != nil { + return nil, err + } + certificates = append(certificates, cert) + } + } + + if len(certificates) == 0 { + return nil, errors.New("No certificates were found while parsing the bundle") + } + + return certificates, nil +} + +func parsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) { + keyBlock, _ := pem.Decode(key) + + switch keyBlock.Type { + case "RSA PRIVATE KEY": + return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) + case "EC PRIVATE KEY": + return x509.ParseECPrivateKey(keyBlock.Bytes) + default: + return nil, errors.New("Unknown PEM header value") + } +} + +func generatePrivateKey(keyType KeyType) (crypto.PrivateKey, error) { + + switch keyType { + case EC256: + return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + case EC384: + return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + case RSA2048: + return rsa.GenerateKey(rand.Reader, 2048) + case RSA4096: + return rsa.GenerateKey(rand.Reader, 4096) + case RSA8192: + return rsa.GenerateKey(rand.Reader, 8192) + } + + return nil, fmt.Errorf("Invalid KeyType: %s", keyType) +} + +func generateCsr(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) { + template := x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: domain, + }, + } + + if len(san) > 0 { + template.DNSNames = san + } + + if mustStaple { + template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{ + Id: tlsFeatureExtensionOID, + Value: ocspMustStapleFeature, + }) + } + + return x509.CreateCertificateRequest(rand.Reader, &template, privateKey) +} + +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 derCertificateBytes: + pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(derCertificateBytes))} + } + + return pem.EncodeToMemory(pemBlock) +} + +func pemDecode(data []byte) (*pem.Block, error) { + pemBlock, _ := pem.Decode(data) + if pemBlock == nil { + return nil, fmt.Errorf("Pem decode did not yield a valid block. Is the certificate in the right format?") + } + + return pemBlock, nil +} + +func pemDecodeTox509(pem []byte) (*x509.Certificate, error) { + pemBlock, err := pemDecode(pem) + if pemBlock == nil { + return nil, err + } + + return x509.ParseCertificate(pemBlock.Bytes) +} + +func pemDecodeTox509CSR(pem []byte) (*x509.CertificateRequest, error) { + pemBlock, err := pemDecode(pem) + if pemBlock == nil { + return nil, err + } + + if pemBlock.Type != "CERTIFICATE REQUEST" { + return nil, fmt.Errorf("PEM block is not a certificate request") + } + + return x509.ParseCertificateRequest(pemBlock.Bytes) +} + +// GetPEMCertExpiration returns the "NotAfter" date of a PEM encoded certificate. +// The certificate has to be PEM encoded. Any other encodings like DER will fail. +func GetPEMCertExpiration(cert []byte) (time.Time, error) { + pemBlock, err := pemDecode(cert) + if pemBlock == nil { + return time.Time{}, err + } + + return getCertExpiration(pemBlock.Bytes) +} + +// getCertExpiration returns the "NotAfter" date of a DER encoded certificate. +func getCertExpiration(cert []byte) (time.Time, error) { + pCert, err := x509.ParseCertificate(cert) + if err != nil { + return time.Time{}, err + } + + return pCert.NotAfter, nil +} + +func generatePemCert(privKey *rsa.PrivateKey, domain string) ([]byte, error) { + derBytes, err := generateDerCert(privKey, time.Time{}, domain) + if err != nil { + return nil, err + } + + return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil +} + +func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain string) ([]byte, error) { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, err + } + + if expiration.IsZero() { + expiration = time.Now().Add(365) + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: "ACME Challenge TEMP", + }, + NotBefore: time.Now(), + NotAfter: expiration, + + KeyUsage: x509.KeyUsageKeyEncipherment, + BasicConstraintsValid: true, + DNSNames: []string{domain}, + } + + return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey) +} + +func limitReader(rd io.ReadCloser, numBytes int64) io.ReadCloser { + return http.MaxBytesReader(nil, rd, numBytes) +} diff --git a/vendor/github.com/xenolf/lego/acmev2/dns_challenge.go b/vendor/github.com/xenolf/lego/acmev2/dns_challenge.go new file mode 100644 index 000000000..d129dacc6 --- /dev/null +++ b/vendor/github.com/xenolf/lego/acmev2/dns_challenge.go @@ -0,0 +1,309 @@ +package acme + +import ( + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + "log" + "net" + "strings" + "time" + + "github.com/miekg/dns" +) + +type preCheckDNSFunc func(fqdn, value string) (bool, error) + +var ( + // PreCheckDNS checks DNS propagation before notifying ACME that + // the DNS challenge is ready. + PreCheckDNS preCheckDNSFunc = checkDNSPropagation + fqdnToZone = map[string]string{} +) + +const defaultResolvConf = "/etc/resolv.conf" + +var defaultNameservers = []string{ + "google-public-dns-a.google.com:53", + "google-public-dns-b.google.com:53", +} + +// RecursiveNameservers are used to pre-check DNS propagations +var RecursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers) + +// DNSTimeout is used to override the default DNS timeout of 10 seconds. +var DNSTimeout = 10 * time.Second + +// getNameservers attempts to get systems nameservers before falling back to the defaults +func getNameservers(path string, defaults []string) []string { + config, err := dns.ClientConfigFromFile(path) + if err != nil || len(config.Servers) == 0 { + return defaults + } + + systemNameservers := []string{} + for _, server := range config.Servers { + // ensure all servers have a port number + if _, _, err := net.SplitHostPort(server); err != nil { + systemNameservers = append(systemNameservers, net.JoinHostPort(server, "53")) + } else { + systemNameservers = append(systemNameservers, server) + } + } + return systemNameservers +} + +// DNS01Record returns a DNS record which will fulfill the `dns-01` challenge +func DNS01Record(domain, keyAuth string) (fqdn string, value string, ttl int) { + keyAuthShaBytes := sha256.Sum256([]byte(keyAuth)) + // base64URL encoding without padding + value = base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size]) + ttl = 120 + fqdn = fmt.Sprintf("_acme-challenge.%s.", domain) + return +} + +// dnsChallenge implements the dns-01 challenge according to ACME 7.5 +type dnsChallenge struct { + jws *jws + validate validateFunc + provider ChallengeProvider +} + +func (s *dnsChallenge) Solve(chlng challenge, domain string) error { + logf("[INFO][%s] acme: Trying to solve DNS-01", domain) + + if s.provider == nil { + return errors.New("No DNS Provider configured") + } + + // Generate the Key Authorization for the challenge + keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey) + if err != nil { + return err + } + + err = s.provider.Present(domain, chlng.Token, keyAuth) + if err != nil { + return fmt.Errorf("Error presenting token: %s", err) + } + defer func() { + err := s.provider.CleanUp(domain, chlng.Token, keyAuth) + if err != nil { + log.Printf("Error cleaning up %s: %v ", domain, err) + } + }() + + fqdn, value, _ := DNS01Record(domain, keyAuth) + + logf("[INFO][%s] Checking DNS record propagation using %+v", domain, RecursiveNameservers) + + var timeout, interval time.Duration + switch provider := s.provider.(type) { + case ChallengeProviderTimeout: + timeout, interval = provider.Timeout() + default: + timeout, interval = 60*time.Second, 2*time.Second + } + + err = WaitFor(timeout, interval, func() (bool, error) { + return PreCheckDNS(fqdn, value) + }) + if err != nil { + return err + } + + return s.validate(s.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) +} + +// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers. +func checkDNSPropagation(fqdn, value string) (bool, error) { + // Initial attempt to resolve at the recursive NS + r, err := dnsQuery(fqdn, dns.TypeTXT, RecursiveNameservers, true) + if err != nil { + return false, err + } + if r.Rcode == dns.RcodeSuccess { + // If we see a CNAME here then use the alias + for _, rr := range r.Answer { + if cn, ok := rr.(*dns.CNAME); ok { + if cn.Hdr.Name == fqdn { + fqdn = cn.Target + break + } + } + } + } + + authoritativeNss, err := lookupNameservers(fqdn) + if err != nil { + return false, err + } + + return checkAuthoritativeNss(fqdn, value, authoritativeNss) +} + +// checkAuthoritativeNss queries each of the given nameservers for the expected TXT record. +func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, error) { + for _, ns := range nameservers { + r, err := dnsQuery(fqdn, dns.TypeTXT, []string{net.JoinHostPort(ns, "53")}, false) + if err != nil { + return false, err + } + + if r.Rcode != dns.RcodeSuccess { + return false, fmt.Errorf("NS %s returned %s for %s", ns, dns.RcodeToString[r.Rcode], fqdn) + } + + var found bool + for _, rr := range r.Answer { + if txt, ok := rr.(*dns.TXT); ok { + if strings.Join(txt.Txt, "") == value { + found = true + break + } + } + } + + if !found { + return false, fmt.Errorf("NS %s did not return the expected TXT record", ns) + } + } + + return true, nil +} + +// dnsQuery will query a nameserver, iterating through the supplied servers as it retries +// The nameserver should include a port, to facilitate testing where we talk to a mock dns server. +func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (in *dns.Msg, err error) { + m := new(dns.Msg) + m.SetQuestion(fqdn, rtype) + m.SetEdns0(4096, false) + + if !recursive { + m.RecursionDesired = false + } + + // Will retry the request based on the number of servers (n+1) + for i := 1; i <= len(nameservers)+1; i++ { + ns := nameservers[i%len(nameservers)] + udp := &dns.Client{Net: "udp", Timeout: DNSTimeout} + in, _, err = udp.Exchange(m, ns) + + if err == dns.ErrTruncated { + tcp := &dns.Client{Net: "tcp", Timeout: DNSTimeout} + // If the TCP request succeeds, the err will reset to nil + in, _, err = tcp.Exchange(m, ns) + } + + if err == nil { + break + } + } + return +} + +// lookupNameservers returns the authoritative nameservers for the given fqdn. +func lookupNameservers(fqdn string) ([]string, error) { + var authoritativeNss []string + + zone, err := FindZoneByFqdn(fqdn, RecursiveNameservers) + if err != nil { + return nil, fmt.Errorf("Could not determine the zone: %v", err) + } + + r, err := dnsQuery(zone, dns.TypeNS, RecursiveNameservers, true) + if err != nil { + return nil, err + } + + for _, rr := range r.Answer { + if ns, ok := rr.(*dns.NS); ok { + authoritativeNss = append(authoritativeNss, strings.ToLower(ns.Ns)) + } + } + + if len(authoritativeNss) > 0 { + return authoritativeNss, nil + } + return nil, fmt.Errorf("Could not determine authoritative nameservers") +} + +// FindZoneByFqdn determines the zone apex for the given fqdn by recursing up the +// domain labels until the nameserver returns a SOA record in the answer section. +func FindZoneByFqdn(fqdn string, nameservers []string) (string, error) { + // Do we have it cached? + if zone, ok := fqdnToZone[fqdn]; ok { + return zone, nil + } + + labelIndexes := dns.Split(fqdn) + for _, index := range labelIndexes { + domain := fqdn[index:] + + in, err := dnsQuery(domain, dns.TypeSOA, nameservers, true) + if err != nil { + return "", err + } + + // Any response code other than NOERROR and NXDOMAIN is treated as error + if in.Rcode != dns.RcodeNameError && in.Rcode != dns.RcodeSuccess { + return "", fmt.Errorf("Unexpected response code '%s' for %s", + dns.RcodeToString[in.Rcode], domain) + } + + // Check if we got a SOA RR in the answer section + if in.Rcode == dns.RcodeSuccess { + + // CNAME records cannot/should not exist at the root of a zone. + // So we skip a domain when a CNAME is found. + if dnsMsgContainsCNAME(in) { + continue + } + + for _, ans := range in.Answer { + if soa, ok := ans.(*dns.SOA); ok { + zone := soa.Hdr.Name + fqdnToZone[fqdn] = zone + return zone, nil + } + } + } + } + + return "", fmt.Errorf("Could not find the start of authority") +} + +// dnsMsgContainsCNAME checks for a CNAME answer in msg +func dnsMsgContainsCNAME(msg *dns.Msg) bool { + for _, ans := range msg.Answer { + if _, ok := ans.(*dns.CNAME); ok { + return true + } + } + return false +} + +// ClearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing. +func ClearFqdnCache() { + fqdnToZone = map[string]string{} +} + +// ToFqdn converts the name into a fqdn appending a trailing dot. +func ToFqdn(name string) string { + n := len(name) + if n == 0 || name[n-1] == '.' { + return name + } + return name + "." +} + +// UnFqdn converts the fqdn into a name removing the trailing dot. +func UnFqdn(name string) string { + n := len(name) + if n != 0 && name[n-1] == '.' { + return name[:n-1] + } + return name +} diff --git a/vendor/github.com/xenolf/lego/acmev2/dns_challenge_manual.go b/vendor/github.com/xenolf/lego/acmev2/dns_challenge_manual.go new file mode 100644 index 000000000..240384e60 --- /dev/null +++ b/vendor/github.com/xenolf/lego/acmev2/dns_challenge_manual.go @@ -0,0 +1,53 @@ +package acme + +import ( + "bufio" + "fmt" + "os" +) + +const ( + dnsTemplate = "%s %d IN TXT \"%s\"" +) + +// DNSProviderManual is an implementation of the ChallengeProvider interface +type DNSProviderManual struct{} + +// NewDNSProviderManual returns a DNSProviderManual instance. +func NewDNSProviderManual() (*DNSProviderManual, error) { + return &DNSProviderManual{}, nil +} + +// Present prints instructions for manually creating the TXT record +func (*DNSProviderManual) Present(domain, token, keyAuth string) error { + fqdn, value, ttl := DNS01Record(domain, keyAuth) + dnsRecord := fmt.Sprintf(dnsTemplate, fqdn, ttl, value) + + authZone, err := FindZoneByFqdn(fqdn, RecursiveNameservers) + if err != nil { + return err + } + + logf("[INFO] acme: Please create the following TXT record in your %s zone:", authZone) + logf("[INFO] acme: %s", dnsRecord) + logf("[INFO] acme: Press 'Enter' when you are done") + + reader := bufio.NewReader(os.Stdin) + _, _ = reader.ReadString('\n') + return nil +} + +// CleanUp prints instructions for manually removing the TXT record +func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error { + fqdn, _, ttl := DNS01Record(domain, keyAuth) + dnsRecord := fmt.Sprintf(dnsTemplate, fqdn, ttl, "...") + + authZone, err := FindZoneByFqdn(fqdn, RecursiveNameservers) + if err != nil { + return err + } + + logf("[INFO] acme: You can now remove this TXT record from your %s zone:", authZone) + logf("[INFO] acme: %s", dnsRecord) + return nil +} diff --git a/vendor/github.com/xenolf/lego/acmev2/error.go b/vendor/github.com/xenolf/lego/acmev2/error.go new file mode 100644 index 000000000..650270b1e --- /dev/null +++ b/vendor/github.com/xenolf/lego/acmev2/error.go @@ -0,0 +1,78 @@ +package acme + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" +) + +const ( + tosAgreementError = "Terms of service have changed" + invalidNonceError = "urn:ietf:params:acme:error:badNonce" +) + +// RemoteError is the base type for all errors specific to the ACME protocol. +type RemoteError struct { + StatusCode int `json:"status,omitempty"` + Type string `json:"type"` + Detail string `json:"detail"` +} + +func (e RemoteError) Error() string { + return fmt.Sprintf("acme: Error %d - %s - %s", e.StatusCode, e.Type, e.Detail) +} + +// TOSError represents the error which is returned if the user needs to +// accept the TOS. +// TODO: include the new TOS url if we can somehow obtain it. +type TOSError struct { + RemoteError +} + +// NonceError represents the error which is returned if the +// nonce sent by the client was not accepted by the server. +type NonceError struct { + RemoteError +} + +type domainError struct { + Domain string + Error error +} + +func handleHTTPError(resp *http.Response) error { + var errorDetail RemoteError + + contentType := resp.Header.Get("Content-Type") + if contentType == "application/json" || strings.HasPrefix(contentType, "application/problem+json") { + err := json.NewDecoder(resp.Body).Decode(&errorDetail) + if err != nil { + return err + } + } else { + detailBytes, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize)) + if err != nil { + return err + } + errorDetail.Detail = string(detailBytes) + } + + errorDetail.StatusCode = resp.StatusCode + + // Check for errors we handle specifically + if errorDetail.StatusCode == http.StatusForbidden && errorDetail.Detail == tosAgreementError { + return TOSError{errorDetail} + } + + if errorDetail.StatusCode == http.StatusBadRequest && errorDetail.Type == invalidNonceError { + return NonceError{errorDetail} + } + + return errorDetail +} + +func handleChallengeError(chlng challenge) error { + return chlng.Error +} diff --git a/vendor/github.com/xenolf/lego/acmev2/http.go b/vendor/github.com/xenolf/lego/acmev2/http.go new file mode 100644 index 000000000..b93e53445 --- /dev/null +++ b/vendor/github.com/xenolf/lego/acmev2/http.go @@ -0,0 +1,160 @@ +package acme + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "runtime" + "strings" + "time" +) + +// UserAgent (if non-empty) will be tacked onto the User-Agent string in requests. +var UserAgent string + +// HTTPClient is an HTTP client with a reasonable timeout value. +var HTTPClient = http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 15 * time.Second, + ResponseHeaderTimeout: 15 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, +} + +const ( + // defaultGoUserAgent is the Go HTTP package user agent string. Too + // bad it isn't exported. If it changes, we should update it here, too. + defaultGoUserAgent = "Go-http-client/1.1" + + // ourUserAgent is the User-Agent of this underlying library package. + ourUserAgent = "xenolf-acme" +) + +// httpHead performs a HEAD request with a proper User-Agent string. +// The response body (resp.Body) is already closed when this function returns. +func httpHead(url string) (resp *http.Response, err error) { + req, err := http.NewRequest("HEAD", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to head %q: %v", url, err) + } + + req.Header.Set("User-Agent", userAgent()) + + resp, err = HTTPClient.Do(req) + if err != nil { + return resp, fmt.Errorf("failed to do head %q: %v", url, err) + } + resp.Body.Close() + return resp, err +} + +// httpPost performs a POST request with a proper User-Agent string. +// Callers should close resp.Body when done reading from it. +func httpPost(url string, bodyType string, body io.Reader) (resp *http.Response, err error) { + req, err := http.NewRequest("POST", url, body) + if err != nil { + return nil, fmt.Errorf("failed to post %q: %v", url, err) + } + req.Header.Set("Content-Type", bodyType) + req.Header.Set("User-Agent", userAgent()) + + return HTTPClient.Do(req) +} + +// httpGet performs a GET request with a proper User-Agent string. +// Callers should close resp.Body when done reading from it. +func httpGet(url string) (resp *http.Response, err error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to get %q: %v", url, err) + } + req.Header.Set("User-Agent", userAgent()) + + return HTTPClient.Do(req) +} + +// getJSON performs an HTTP GET request and parses the response body +// as JSON, into the provided respBody object. +func getJSON(uri string, respBody interface{}) (http.Header, error) { + resp, err := httpGet(uri) + if err != nil { + return nil, fmt.Errorf("failed to get json %q: %v", uri, err) + } + defer resp.Body.Close() + + if resp.StatusCode >= http.StatusBadRequest { + return resp.Header, handleHTTPError(resp) + } + + return resp.Header, json.NewDecoder(resp.Body).Decode(respBody) +} + +// postJSON performs an HTTP POST request and parses the response body +// as JSON, into the provided respBody object. +func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, error) { + jsonBytes, err := json.Marshal(reqBody) + if err != nil { + return nil, errors.New("Failed to marshal network message") + } + + resp, err := j.post(uri, jsonBytes) + if err != nil { + return nil, fmt.Errorf("Failed to post JWS message. -> %v", err) + } + + defer resp.Body.Close() + + if resp.StatusCode >= http.StatusBadRequest { + + err := handleHTTPError(resp) + + switch err.(type) { + + case NonceError: + + // Retry once if the nonce was invalidated + + retryResp, err := j.post(uri, jsonBytes) + if err != nil { + return nil, fmt.Errorf("Failed to post JWS message. -> %v", err) + } + + defer retryResp.Body.Close() + + if retryResp.StatusCode >= http.StatusBadRequest { + return retryResp.Header, handleHTTPError(retryResp) + } + + if respBody == nil { + return retryResp.Header, nil + } + + return retryResp.Header, json.NewDecoder(retryResp.Body).Decode(respBody) + + default: + return resp.Header, err + + } + + } + + if respBody == nil { + return resp.Header, nil + } + + return resp.Header, json.NewDecoder(resp.Body).Decode(respBody) +} + +// userAgent builds and returns the User-Agent string to use in requests. +func userAgent() string { + ua := fmt.Sprintf("%s (%s; %s) %s %s", defaultGoUserAgent, runtime.GOOS, runtime.GOARCH, ourUserAgent, UserAgent) + return strings.TrimSpace(ua) +} diff --git a/vendor/github.com/xenolf/lego/acmev2/http_challenge.go b/vendor/github.com/xenolf/lego/acmev2/http_challenge.go new file mode 100644 index 000000000..b6c969fe2 --- /dev/null +++ b/vendor/github.com/xenolf/lego/acmev2/http_challenge.go @@ -0,0 +1,41 @@ +package acme + +import ( + "fmt" + "log" +) + +type httpChallenge struct { + jws *jws + validate validateFunc + provider ChallengeProvider +} + +// HTTP01ChallengePath returns the URL path for the `http-01` challenge +func HTTP01ChallengePath(token string) string { + return "/.well-known/acme-challenge/" + token +} + +func (s *httpChallenge) Solve(chlng challenge, domain string) error { + + logf("[INFO][%s] acme: Trying to solve HTTP-01", domain) + + // Generate the Key Authorization for the challenge + keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey) + if err != nil { + return err + } + + err = s.provider.Present(domain, chlng.Token, keyAuth) + if err != nil { + return fmt.Errorf("[%s] error presenting token: %v", domain, err) + } + defer func() { + err := s.provider.CleanUp(domain, chlng.Token, keyAuth) + if err != nil { + log.Printf("[%s] error cleaning up: %v", domain, err) + } + }() + + return s.validate(s.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) +} diff --git a/vendor/github.com/xenolf/lego/acmev2/http_challenge_server.go b/vendor/github.com/xenolf/lego/acmev2/http_challenge_server.go new file mode 100644 index 000000000..64c6a8280 --- /dev/null +++ b/vendor/github.com/xenolf/lego/acmev2/http_challenge_server.go @@ -0,0 +1,79 @@ +package acme + +import ( + "fmt" + "net" + "net/http" + "strings" +) + +// HTTPProviderServer implements ChallengeProvider for `http-01` challenge +// It may be instantiated without using the NewHTTPProviderServer function if +// you want only to use the default values. +type HTTPProviderServer struct { + iface string + port string + done chan bool + listener net.Listener +} + +// NewHTTPProviderServer creates a new HTTPProviderServer on the selected interface and port. +// Setting iface and / or port to an empty string will make the server fall back to +// the "any" interface and port 80 respectively. +func NewHTTPProviderServer(iface, port string) *HTTPProviderServer { + return &HTTPProviderServer{iface: iface, port: port} +} + +// Present starts a web server and makes the token available at `HTTP01ChallengePath(token)` for web requests. +func (s *HTTPProviderServer) Present(domain, token, keyAuth string) error { + if s.port == "" { + s.port = "80" + } + + var err error + s.listener, err = net.Listen("tcp", net.JoinHostPort(s.iface, s.port)) + if err != nil { + return fmt.Errorf("Could not start HTTP server for challenge -> %v", err) + } + + s.done = make(chan bool) + go s.serve(domain, token, keyAuth) + return nil +} + +// CleanUp closes the HTTP server and removes the token from `HTTP01ChallengePath(token)` +func (s *HTTPProviderServer) CleanUp(domain, token, keyAuth string) error { + if s.listener == nil { + return nil + } + s.listener.Close() + <-s.done + return nil +} + +func (s *HTTPProviderServer) serve(domain, token, keyAuth string) { + path := HTTP01ChallengePath(token) + + // The handler validates the HOST header and request type. + // For validation it then writes the token the server returned with the challenge + mux := http.NewServeMux() + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.Host, domain) && r.Method == "GET" { + w.Header().Add("Content-Type", "text/plain") + w.Write([]byte(keyAuth)) + logf("[INFO][%s] Served key authentication", domain) + } else { + logf("[WARN] Received request for domain %s with method %s but the domain did not match any challenge. Please ensure your are passing the HOST header properly.", r.Host, r.Method) + w.Write([]byte("TEST")) + } + }) + + httpServer := &http.Server{ + Handler: mux, + } + // Once httpServer is shut down we don't want any lingering + // connections, so disable KeepAlives. + httpServer.SetKeepAlivesEnabled(false) + httpServer.Serve(s.listener) + s.done <- true +} diff --git a/vendor/github.com/xenolf/lego/acmev2/jws.go b/vendor/github.com/xenolf/lego/acmev2/jws.go new file mode 100644 index 000000000..9b87e437a --- /dev/null +++ b/vendor/github.com/xenolf/lego/acmev2/jws.go @@ -0,0 +1,138 @@ +package acme + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "fmt" + "net/http" + "sync" + + "gopkg.in/square/go-jose.v2" +) + +type jws struct { + getNonceURL string + privKey crypto.PrivateKey + kid string + nonces nonceManager +} + +// Posts a JWS signed message to the specified URL. +// It does NOT close the response body, so the caller must +// do that if no error was returned. +func (j *jws) post(url string, content []byte) (*http.Response, error) { + signedContent, err := j.signContent(url, content) + if err != nil { + return nil, fmt.Errorf("Failed to sign content -> %s", err.Error()) + } + + data := bytes.NewBuffer([]byte(signedContent.FullSerialize())) + resp, err := httpPost(url, "application/jose+json", data) + if err != nil { + return nil, fmt.Errorf("Failed to HTTP POST to %s -> %s", url, err.Error()) + } + + nonce, nonceErr := getNonceFromResponse(resp) + if nonceErr == nil { + j.nonces.Push(nonce) + } + + return resp, nil +} + +func (j *jws) signContent(url string, content []byte) (*jose.JSONWebSignature, error) { + + var alg jose.SignatureAlgorithm + switch k := j.privKey.(type) { + case *rsa.PrivateKey: + alg = jose.RS256 + case *ecdsa.PrivateKey: + if k.Curve == elliptic.P256() { + alg = jose.ES256 + } else if k.Curve == elliptic.P384() { + alg = jose.ES384 + } + } + + jsonKey := jose.JSONWebKey{ + Key: j.privKey, + KeyID: j.kid, + } + + signKey := jose.SigningKey{ + Algorithm: alg, + Key: jsonKey, + } + options := jose.SignerOptions{ + NonceSource: j, + ExtraHeaders: make(map[jose.HeaderKey]interface{}), + } + options.ExtraHeaders["url"] = url + if j.kid == "" { + options.EmbedJWK = true + } + + signer, err := jose.NewSigner(signKey, &options) + if err != nil { + return nil, fmt.Errorf("Failed to create jose signer -> %s", err.Error()) + } + + signed, err := signer.Sign(content) + if err != nil { + return nil, fmt.Errorf("Failed to sign content -> %s", err.Error()) + } + return signed, nil +} + +func (j *jws) Nonce() (string, error) { + if nonce, ok := j.nonces.Pop(); ok { + return nonce, nil + } + + return getNonce(j.getNonceURL) +} + +type nonceManager struct { + nonces []string + sync.Mutex +} + +func (n *nonceManager) Pop() (string, bool) { + n.Lock() + defer n.Unlock() + + if len(n.nonces) == 0 { + return "", false + } + + nonce := n.nonces[len(n.nonces)-1] + n.nonces = n.nonces[:len(n.nonces)-1] + return nonce, true +} + +func (n *nonceManager) Push(nonce string) { + n.Lock() + defer n.Unlock() + n.nonces = append(n.nonces, nonce) +} + +func getNonce(url string) (string, error) { + resp, err := httpHead(url) + if err != nil { + return "", fmt.Errorf("Failed to get nonce from HTTP HEAD -> %s", err.Error()) + } + + return getNonceFromResponse(resp) +} + +func getNonceFromResponse(resp *http.Response) (string, error) { + nonce := resp.Header.Get("Replay-Nonce") + if nonce == "" { + return "", fmt.Errorf("Server did not respond with a proper nonce header") + } + + return nonce, nil +} diff --git a/vendor/github.com/xenolf/lego/acmev2/messages.go b/vendor/github.com/xenolf/lego/acmev2/messages.go new file mode 100644 index 000000000..9981851d9 --- /dev/null +++ b/vendor/github.com/xenolf/lego/acmev2/messages.go @@ -0,0 +1,103 @@ +package acme + +import ( + "time" +) + +// RegistrationResource represents all important informations about a registration +// of which the client needs to keep track itself. +type RegistrationResource struct { + Body accountMessage `json:"body,omitempty"` + URI string `json:"uri,omitempty"` +} + +type directory struct { + NewNonceURL string `json:"newNonce"` + NewAccountURL string `json:"newAccount"` + NewOrderURL string `json:"newOrder"` + RevokeCertURL string `json:"revokeCert"` + KeyChangeURL string `json:"keyChange"` + Meta struct { + TermsOfService string `json:"termsOfService"` + Website string `json:"website"` + CaaIdentities []string `json:"caaIdentities"` + ExternalAccountRequired bool `json:"externalAccountRequired"` + } `json:"meta"` +} + +type accountMessage struct { + Status string `json:"status,omitempty"` + Contact []string `json:"contact,omitempty"` + TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"` + Orders string `json:"orders,omitempty"` + OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"` +} + +type orderResource struct { + URL string `json:"url,omitempty"` + orderMessage `json:"body,omitempty"` +} + +type orderMessage struct { + Status string `json:"status,omitempty"` + Expires string `json:"expires,omitempty"` + Identifiers []identifier `json:"identifiers"` + NotBefore string `json:"notBefore,omitempty"` + NotAfter string `json:"notAfter,omitempty"` + Authorizations []string `json:"authorizations,omitempty"` + Finalize string `json:"finalize,omitempty"` + Certificate string `json:"certificate,omitempty"` +} + +type authorization struct { + Status string `json:"status"` + Expires time.Time `json:"expires"` + Identifier identifier `json:"identifier"` + Challenges []challenge `json:"challenges"` +} + +type identifier struct { + Type string `json:"type"` + Value string `json:"value"` +} + +type challenge struct { + URL string `json:"url"` + Type string `json:"type"` + Status string `json:"status"` + Token string `json:"token"` + Validated time.Time `json:"validated"` + KeyAuthorization string `json:"keyAuthorization"` + Error RemoteError `json:"error"` +} + +type csrMessage struct { + Csr string `json:"csr"` +} + +type emptyObjectMessage struct { +} + +type revokeCertMessage struct { + Certificate string `json:"certificate"` +} + +type deactivateAuthMessage struct { + Status string `jsom:"status"` +} + +// CertificateResource represents a CA issued certificate. +// PrivateKey, Certificate and IssuerCertificate are all +// already PEM encoded and can be directly written to disk. +// Certificate may be a certificate bundle, depending on the +// options supplied to create it. +type CertificateResource struct { + Domain string `json:"domain"` + CertURL string `json:"certUrl"` + CertStableURL string `json:"certStableUrl"` + AccountRef string `json:"accountRef,omitempty"` + PrivateKey []byte `json:"-"` + Certificate []byte `json:"-"` + IssuerCertificate []byte `json:"-"` + CSR []byte `json:"-"` +} diff --git a/vendor/github.com/xenolf/lego/acmev2/pop_challenge.go b/vendor/github.com/xenolf/lego/acmev2/pop_challenge.go new file mode 100644 index 000000000..8d2a213b0 --- /dev/null +++ b/vendor/github.com/xenolf/lego/acmev2/pop_challenge.go @@ -0,0 +1 @@ +package acme diff --git a/vendor/github.com/xenolf/lego/acmev2/provider.go b/vendor/github.com/xenolf/lego/acmev2/provider.go new file mode 100644 index 000000000..d177ff07a --- /dev/null +++ b/vendor/github.com/xenolf/lego/acmev2/provider.go @@ -0,0 +1,28 @@ +package acme + +import "time" + +// ChallengeProvider enables implementing a custom challenge +// provider. Present presents the solution to a challenge available to +// be solved. CleanUp will be called by the challenge if Present ends +// in a non-error state. +type ChallengeProvider interface { + Present(domain, token, keyAuth string) error + CleanUp(domain, token, keyAuth string) error +} + +// ChallengeProviderTimeout allows for implementing a +// ChallengeProvider where an unusually long timeout is required when +// waiting for an ACME challenge to be satisfied, such as when +// checking for DNS record progagation. If an implementor of a +// ChallengeProvider provides a Timeout method, then the return values +// of the Timeout method will be used when appropriate by the acme +// package. The interval value is the time between checks. +// +// The default values used for timeout and interval are 60 seconds and +// 2 seconds respectively. These are used when no Timeout method is +// defined for the ChallengeProvider. +type ChallengeProviderTimeout interface { + ChallengeProvider + Timeout() (timeout, interval time.Duration) +} diff --git a/vendor/github.com/xenolf/lego/acmev2/utils.go b/vendor/github.com/xenolf/lego/acmev2/utils.go new file mode 100644 index 000000000..2fa0db304 --- /dev/null +++ b/vendor/github.com/xenolf/lego/acmev2/utils.go @@ -0,0 +1,29 @@ +package acme + +import ( + "fmt" + "time" +) + +// WaitFor polls the given function 'f', once every 'interval', up to 'timeout'. +func WaitFor(timeout, interval time.Duration, f func() (bool, error)) error { + var lastErr string + timeup := time.After(timeout) + for { + select { + case <-timeup: + return fmt.Errorf("Time limit exceeded. Last error: %s", lastErr) + default: + } + + stop, err := f() + if stop { + return nil + } + if err != nil { + lastErr = err.Error() + } + + time.Sleep(interval) + } +} diff --git a/vendor/gopkg.in/square/go-jose.v2/LICENSE b/vendor/gopkg.in/square/go-jose.v2/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/gopkg.in/square/go-jose.v2/asymmetric.go b/vendor/gopkg.in/square/go-jose.v2/asymmetric.go new file mode 100644 index 000000000..15e9d11a2 --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/asymmetric.go @@ -0,0 +1,591 @@ +/*- + * Copyright 2014 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jose + +import ( + "crypto" + "crypto/aes" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "errors" + "fmt" + "math/big" + + "golang.org/x/crypto/ed25519" + "gopkg.in/square/go-jose.v2/cipher" + "gopkg.in/square/go-jose.v2/json" +) + +// A generic RSA-based encrypter/verifier +type rsaEncrypterVerifier struct { + publicKey *rsa.PublicKey +} + +// A generic RSA-based decrypter/signer +type rsaDecrypterSigner struct { + privateKey *rsa.PrivateKey +} + +// A generic EC-based encrypter/verifier +type ecEncrypterVerifier struct { + publicKey *ecdsa.PublicKey +} + +type edEncrypterVerifier struct { + publicKey ed25519.PublicKey +} + +// A key generator for ECDH-ES +type ecKeyGenerator struct { + size int + algID string + publicKey *ecdsa.PublicKey +} + +// A generic EC-based decrypter/signer +type ecDecrypterSigner struct { + privateKey *ecdsa.PrivateKey +} + +type edDecrypterSigner struct { + privateKey ed25519.PrivateKey +} + +// newRSARecipient creates recipientKeyInfo based on the given key. +func newRSARecipient(keyAlg KeyAlgorithm, publicKey *rsa.PublicKey) (recipientKeyInfo, error) { + // Verify that key management algorithm is supported by this encrypter + switch keyAlg { + case RSA1_5, RSA_OAEP, RSA_OAEP_256: + default: + return recipientKeyInfo{}, ErrUnsupportedAlgorithm + } + + if publicKey == nil { + return recipientKeyInfo{}, errors.New("invalid public key") + } + + return recipientKeyInfo{ + keyAlg: keyAlg, + keyEncrypter: &rsaEncrypterVerifier{ + publicKey: publicKey, + }, + }, nil +} + +// newRSASigner creates a recipientSigInfo based on the given key. +func newRSASigner(sigAlg SignatureAlgorithm, privateKey *rsa.PrivateKey) (recipientSigInfo, error) { + // Verify that key management algorithm is supported by this encrypter + switch sigAlg { + case RS256, RS384, RS512, PS256, PS384, PS512: + default: + return recipientSigInfo{}, ErrUnsupportedAlgorithm + } + + if privateKey == nil { + return recipientSigInfo{}, errors.New("invalid private key") + } + + return recipientSigInfo{ + sigAlg: sigAlg, + publicKey: &JSONWebKey{ + Key: &privateKey.PublicKey, + }, + signer: &rsaDecrypterSigner{ + privateKey: privateKey, + }, + }, nil +} + +func newEd25519Signer(sigAlg SignatureAlgorithm, privateKey ed25519.PrivateKey) (recipientSigInfo, error) { + if sigAlg != EdDSA { + return recipientSigInfo{}, ErrUnsupportedAlgorithm + } + + if privateKey == nil { + return recipientSigInfo{}, errors.New("invalid private key") + } + return recipientSigInfo{ + sigAlg: sigAlg, + publicKey: &JSONWebKey{ + Key: privateKey.Public(), + }, + signer: &edDecrypterSigner{ + privateKey: privateKey, + }, + }, nil +} + +// newECDHRecipient creates recipientKeyInfo based on the given key. +func newECDHRecipient(keyAlg KeyAlgorithm, publicKey *ecdsa.PublicKey) (recipientKeyInfo, error) { + // Verify that key management algorithm is supported by this encrypter + switch keyAlg { + case ECDH_ES, ECDH_ES_A128KW, ECDH_ES_A192KW, ECDH_ES_A256KW: + default: + return recipientKeyInfo{}, ErrUnsupportedAlgorithm + } + + if publicKey == nil || !publicKey.Curve.IsOnCurve(publicKey.X, publicKey.Y) { + return recipientKeyInfo{}, errors.New("invalid public key") + } + + return recipientKeyInfo{ + keyAlg: keyAlg, + keyEncrypter: &ecEncrypterVerifier{ + publicKey: publicKey, + }, + }, nil +} + +// newECDSASigner creates a recipientSigInfo based on the given key. +func newECDSASigner(sigAlg SignatureAlgorithm, privateKey *ecdsa.PrivateKey) (recipientSigInfo, error) { + // Verify that key management algorithm is supported by this encrypter + switch sigAlg { + case ES256, ES384, ES512: + default: + return recipientSigInfo{}, ErrUnsupportedAlgorithm + } + + if privateKey == nil { + return recipientSigInfo{}, errors.New("invalid private key") + } + + return recipientSigInfo{ + sigAlg: sigAlg, + publicKey: &JSONWebKey{ + Key: &privateKey.PublicKey, + }, + signer: &ecDecrypterSigner{ + privateKey: privateKey, + }, + }, nil +} + +// Encrypt the given payload and update the object. +func (ctx rsaEncrypterVerifier) encryptKey(cek []byte, alg KeyAlgorithm) (recipientInfo, error) { + encryptedKey, err := ctx.encrypt(cek, alg) + if err != nil { + return recipientInfo{}, err + } + + return recipientInfo{ + encryptedKey: encryptedKey, + header: &rawHeader{}, + }, nil +} + +// Encrypt the given payload. Based on the key encryption algorithm, +// this will either use RSA-PKCS1v1.5 or RSA-OAEP (with SHA-1 or SHA-256). +func (ctx rsaEncrypterVerifier) encrypt(cek []byte, alg KeyAlgorithm) ([]byte, error) { + switch alg { + case RSA1_5: + return rsa.EncryptPKCS1v15(randReader, ctx.publicKey, cek) + case RSA_OAEP: + return rsa.EncryptOAEP(sha1.New(), randReader, ctx.publicKey, cek, []byte{}) + case RSA_OAEP_256: + return rsa.EncryptOAEP(sha256.New(), randReader, ctx.publicKey, cek, []byte{}) + } + + return nil, ErrUnsupportedAlgorithm +} + +// Decrypt the given payload and return the content encryption key. +func (ctx rsaDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) { + return ctx.decrypt(recipient.encryptedKey, headers.getAlgorithm(), generator) +} + +// Decrypt the given payload. Based on the key encryption algorithm, +// this will either use RSA-PKCS1v1.5 or RSA-OAEP (with SHA-1 or SHA-256). +func (ctx rsaDecrypterSigner) decrypt(jek []byte, alg KeyAlgorithm, generator keyGenerator) ([]byte, error) { + // Note: The random reader on decrypt operations is only used for blinding, + // so stubbing is meanlingless (hence the direct use of rand.Reader). + switch alg { + case RSA1_5: + defer func() { + // DecryptPKCS1v15SessionKey sometimes panics on an invalid payload + // because of an index out of bounds error, which we want to ignore. + // This has been fixed in Go 1.3.1 (released 2014/08/13), the recover() + // only exists for preventing crashes with unpatched versions. + // See: https://groups.google.com/forum/#!topic/golang-dev/7ihX6Y6kx9k + // See: https://code.google.com/p/go/source/detail?r=58ee390ff31602edb66af41ed10901ec95904d33 + _ = recover() + }() + + // Perform some input validation. + keyBytes := ctx.privateKey.PublicKey.N.BitLen() / 8 + if keyBytes != len(jek) { + // Input size is incorrect, the encrypted payload should always match + // the size of the public modulus (e.g. using a 2048 bit key will + // produce 256 bytes of output). Reject this since it's invalid input. + return nil, ErrCryptoFailure + } + + cek, _, err := generator.genKey() + if err != nil { + return nil, ErrCryptoFailure + } + + // When decrypting an RSA-PKCS1v1.5 payload, we must take precautions to + // prevent chosen-ciphertext attacks as described in RFC 3218, "Preventing + // the Million Message Attack on Cryptographic Message Syntax". We are + // therefore deliberately ignoring errors here. + _ = rsa.DecryptPKCS1v15SessionKey(rand.Reader, ctx.privateKey, jek, cek) + + return cek, nil + case RSA_OAEP: + // Use rand.Reader for RSA blinding + return rsa.DecryptOAEP(sha1.New(), rand.Reader, ctx.privateKey, jek, []byte{}) + case RSA_OAEP_256: + // Use rand.Reader for RSA blinding + return rsa.DecryptOAEP(sha256.New(), rand.Reader, ctx.privateKey, jek, []byte{}) + } + + return nil, ErrUnsupportedAlgorithm +} + +// Sign the given payload +func (ctx rsaDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) { + var hash crypto.Hash + + switch alg { + case RS256, PS256: + hash = crypto.SHA256 + case RS384, PS384: + hash = crypto.SHA384 + case RS512, PS512: + hash = crypto.SHA512 + default: + return Signature{}, ErrUnsupportedAlgorithm + } + + hasher := hash.New() + + // According to documentation, Write() on hash never fails + _, _ = hasher.Write(payload) + hashed := hasher.Sum(nil) + + var out []byte + var err error + + switch alg { + case RS256, RS384, RS512: + out, err = rsa.SignPKCS1v15(randReader, ctx.privateKey, hash, hashed) + case PS256, PS384, PS512: + out, err = rsa.SignPSS(randReader, ctx.privateKey, hash, hashed, &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + }) + } + + if err != nil { + return Signature{}, err + } + + return Signature{ + Signature: out, + protected: &rawHeader{}, + }, nil +} + +// Verify the given payload +func (ctx rsaEncrypterVerifier) verifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error { + var hash crypto.Hash + + switch alg { + case RS256, PS256: + hash = crypto.SHA256 + case RS384, PS384: + hash = crypto.SHA384 + case RS512, PS512: + hash = crypto.SHA512 + default: + return ErrUnsupportedAlgorithm + } + + hasher := hash.New() + + // According to documentation, Write() on hash never fails + _, _ = hasher.Write(payload) + hashed := hasher.Sum(nil) + + switch alg { + case RS256, RS384, RS512: + return rsa.VerifyPKCS1v15(ctx.publicKey, hash, hashed, signature) + case PS256, PS384, PS512: + return rsa.VerifyPSS(ctx.publicKey, hash, hashed, signature, nil) + } + + return ErrUnsupportedAlgorithm +} + +// Encrypt the given payload and update the object. +func (ctx ecEncrypterVerifier) encryptKey(cek []byte, alg KeyAlgorithm) (recipientInfo, error) { + switch alg { + case ECDH_ES: + // ECDH-ES mode doesn't wrap a key, the shared secret is used directly as the key. + return recipientInfo{ + header: &rawHeader{}, + }, nil + case ECDH_ES_A128KW, ECDH_ES_A192KW, ECDH_ES_A256KW: + default: + return recipientInfo{}, ErrUnsupportedAlgorithm + } + + generator := ecKeyGenerator{ + algID: string(alg), + publicKey: ctx.publicKey, + } + + switch alg { + case ECDH_ES_A128KW: + generator.size = 16 + case ECDH_ES_A192KW: + generator.size = 24 + case ECDH_ES_A256KW: + generator.size = 32 + } + + kek, header, err := generator.genKey() + if err != nil { + return recipientInfo{}, err + } + + block, err := aes.NewCipher(kek) + if err != nil { + return recipientInfo{}, err + } + + jek, err := josecipher.KeyWrap(block, cek) + if err != nil { + return recipientInfo{}, err + } + + return recipientInfo{ + encryptedKey: jek, + header: &header, + }, nil +} + +// Get key size for EC key generator +func (ctx ecKeyGenerator) keySize() int { + return ctx.size +} + +// Get a content encryption key for ECDH-ES +func (ctx ecKeyGenerator) genKey() ([]byte, rawHeader, error) { + priv, err := ecdsa.GenerateKey(ctx.publicKey.Curve, randReader) + if err != nil { + return nil, rawHeader{}, err + } + + out := josecipher.DeriveECDHES(ctx.algID, []byte{}, []byte{}, priv, ctx.publicKey, ctx.size) + + b, err := json.Marshal(&JSONWebKey{ + Key: &priv.PublicKey, + }) + if err != nil { + return nil, nil, err + } + + headers := rawHeader{ + headerEPK: makeRawMessage(b), + } + + return out, headers, nil +} + +// Decrypt the given payload and return the content encryption key. +func (ctx ecDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) { + epk, err := headers.getEPK() + if err != nil { + return nil, errors.New("square/go-jose: invalid epk header") + } + if epk == nil { + return nil, errors.New("square/go-jose: missing epk header") + } + + publicKey, ok := epk.Key.(*ecdsa.PublicKey) + if publicKey == nil || !ok { + return nil, errors.New("square/go-jose: invalid epk header") + } + + if !ctx.privateKey.Curve.IsOnCurve(publicKey.X, publicKey.Y) { + return nil, errors.New("square/go-jose: invalid public key in epk header") + } + + apuData, err := headers.getAPU() + if err != nil { + return nil, errors.New("square/go-jose: invalid apu header") + } + apvData, err := headers.getAPV() + if err != nil { + return nil, errors.New("square/go-jose: invalid apv header") + } + + deriveKey := func(algID string, size int) []byte { + return josecipher.DeriveECDHES(algID, apuData.bytes(), apvData.bytes(), ctx.privateKey, publicKey, size) + } + + var keySize int + + algorithm := headers.getAlgorithm() + switch algorithm { + case ECDH_ES: + // ECDH-ES uses direct key agreement, no key unwrapping necessary. + return deriveKey(string(headers.getEncryption()), generator.keySize()), nil + case ECDH_ES_A128KW: + keySize = 16 + case ECDH_ES_A192KW: + keySize = 24 + case ECDH_ES_A256KW: + keySize = 32 + default: + return nil, ErrUnsupportedAlgorithm + } + + key := deriveKey(string(algorithm), keySize) + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + return josecipher.KeyUnwrap(block, recipient.encryptedKey) +} +func (ctx edDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) { + if alg != EdDSA { + return Signature{}, ErrUnsupportedAlgorithm + } + + sig, err := ctx.privateKey.Sign(randReader, payload, crypto.Hash(0)) + if err != nil { + return Signature{}, err + } + + return Signature{ + Signature: sig, + protected: &rawHeader{}, + }, nil +} + +func (ctx edEncrypterVerifier) verifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error { + if alg != EdDSA { + return ErrUnsupportedAlgorithm + } + ok := ed25519.Verify(ctx.publicKey, payload, signature) + if !ok { + return errors.New("square/go-jose: ed25519 signature failed to verify") + } + return nil +} + +// Sign the given payload +func (ctx ecDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) { + var expectedBitSize int + var hash crypto.Hash + + switch alg { + case ES256: + expectedBitSize = 256 + hash = crypto.SHA256 + case ES384: + expectedBitSize = 384 + hash = crypto.SHA384 + case ES512: + expectedBitSize = 521 + hash = crypto.SHA512 + } + + curveBits := ctx.privateKey.Curve.Params().BitSize + if expectedBitSize != curveBits { + return Signature{}, fmt.Errorf("square/go-jose: expected %d bit key, got %d bits instead", expectedBitSize, curveBits) + } + + hasher := hash.New() + + // According to documentation, Write() on hash never fails + _, _ = hasher.Write(payload) + hashed := hasher.Sum(nil) + + r, s, err := ecdsa.Sign(randReader, ctx.privateKey, hashed) + if err != nil { + return Signature{}, err + } + + keyBytes := curveBits / 8 + if curveBits%8 > 0 { + keyBytes++ + } + + // We serialize the outpus (r and s) into big-endian byte arrays and pad + // them with zeros on the left to make sure the sizes work out. Both arrays + // must be keyBytes long, and the output must be 2*keyBytes long. + rBytes := r.Bytes() + rBytesPadded := make([]byte, keyBytes) + copy(rBytesPadded[keyBytes-len(rBytes):], rBytes) + + sBytes := s.Bytes() + sBytesPadded := make([]byte, keyBytes) + copy(sBytesPadded[keyBytes-len(sBytes):], sBytes) + + out := append(rBytesPadded, sBytesPadded...) + + return Signature{ + Signature: out, + protected: &rawHeader{}, + }, nil +} + +// Verify the given payload +func (ctx ecEncrypterVerifier) verifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error { + var keySize int + var hash crypto.Hash + + switch alg { + case ES256: + keySize = 32 + hash = crypto.SHA256 + case ES384: + keySize = 48 + hash = crypto.SHA384 + case ES512: + keySize = 66 + hash = crypto.SHA512 + default: + return ErrUnsupportedAlgorithm + } + + if len(signature) != 2*keySize { + return fmt.Errorf("square/go-jose: invalid signature size, have %d bytes, wanted %d", len(signature), 2*keySize) + } + + hasher := hash.New() + + // According to documentation, Write() on hash never fails + _, _ = hasher.Write(payload) + hashed := hasher.Sum(nil) + + r := big.NewInt(0).SetBytes(signature[:keySize]) + s := big.NewInt(0).SetBytes(signature[keySize:]) + + match := ecdsa.Verify(ctx.publicKey, hashed, r, s) + if !match { + return errors.New("square/go-jose: ecdsa signature failed to verify") + } + + return nil +} diff --git a/vendor/gopkg.in/square/go-jose.v2/cipher/cbc_hmac.go b/vendor/gopkg.in/square/go-jose.v2/cipher/cbc_hmac.go new file mode 100644 index 000000000..126b85ce2 --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/cipher/cbc_hmac.go @@ -0,0 +1,196 @@ +/*- + * Copyright 2014 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package josecipher + +import ( + "bytes" + "crypto/cipher" + "crypto/hmac" + "crypto/sha256" + "crypto/sha512" + "crypto/subtle" + "encoding/binary" + "errors" + "hash" +) + +const ( + nonceBytes = 16 +) + +// NewCBCHMAC instantiates a new AEAD based on CBC+HMAC. +func NewCBCHMAC(key []byte, newBlockCipher func([]byte) (cipher.Block, error)) (cipher.AEAD, error) { + keySize := len(key) / 2 + integrityKey := key[:keySize] + encryptionKey := key[keySize:] + + blockCipher, err := newBlockCipher(encryptionKey) + if err != nil { + return nil, err + } + + var hash func() hash.Hash + switch keySize { + case 16: + hash = sha256.New + case 24: + hash = sha512.New384 + case 32: + hash = sha512.New + } + + return &cbcAEAD{ + hash: hash, + blockCipher: blockCipher, + authtagBytes: keySize, + integrityKey: integrityKey, + }, nil +} + +// An AEAD based on CBC+HMAC +type cbcAEAD struct { + hash func() hash.Hash + authtagBytes int + integrityKey []byte + blockCipher cipher.Block +} + +func (ctx *cbcAEAD) NonceSize() int { + return nonceBytes +} + +func (ctx *cbcAEAD) Overhead() int { + // Maximum overhead is block size (for padding) plus auth tag length, where + // the length of the auth tag is equivalent to the key size. + return ctx.blockCipher.BlockSize() + ctx.authtagBytes +} + +// Seal encrypts and authenticates the plaintext. +func (ctx *cbcAEAD) Seal(dst, nonce, plaintext, data []byte) []byte { + // Output buffer -- must take care not to mangle plaintext input. + ciphertext := make([]byte, uint64(len(plaintext))+uint64(ctx.Overhead()))[:len(plaintext)] + copy(ciphertext, plaintext) + ciphertext = padBuffer(ciphertext, ctx.blockCipher.BlockSize()) + + cbc := cipher.NewCBCEncrypter(ctx.blockCipher, nonce) + + cbc.CryptBlocks(ciphertext, ciphertext) + authtag := ctx.computeAuthTag(data, nonce, ciphertext) + + ret, out := resize(dst, uint64(len(dst))+uint64(len(ciphertext))+uint64(len(authtag))) + copy(out, ciphertext) + copy(out[len(ciphertext):], authtag) + + return ret +} + +// Open decrypts and authenticates the ciphertext. +func (ctx *cbcAEAD) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) { + if len(ciphertext) < ctx.authtagBytes { + return nil, errors.New("square/go-jose: invalid ciphertext (too short)") + } + + offset := len(ciphertext) - ctx.authtagBytes + expectedTag := ctx.computeAuthTag(data, nonce, ciphertext[:offset]) + match := subtle.ConstantTimeCompare(expectedTag, ciphertext[offset:]) + if match != 1 { + return nil, errors.New("square/go-jose: invalid ciphertext (auth tag mismatch)") + } + + cbc := cipher.NewCBCDecrypter(ctx.blockCipher, nonce) + + // Make copy of ciphertext buffer, don't want to modify in place + buffer := append([]byte{}, []byte(ciphertext[:offset])...) + + if len(buffer)%ctx.blockCipher.BlockSize() > 0 { + return nil, errors.New("square/go-jose: invalid ciphertext (invalid length)") + } + + cbc.CryptBlocks(buffer, buffer) + + // Remove padding + plaintext, err := unpadBuffer(buffer, ctx.blockCipher.BlockSize()) + if err != nil { + return nil, err + } + + ret, out := resize(dst, uint64(len(dst))+uint64(len(plaintext))) + copy(out, plaintext) + + return ret, nil +} + +// Compute an authentication tag +func (ctx *cbcAEAD) computeAuthTag(aad, nonce, ciphertext []byte) []byte { + buffer := make([]byte, uint64(len(aad))+uint64(len(nonce))+uint64(len(ciphertext))+8) + n := 0 + n += copy(buffer, aad) + n += copy(buffer[n:], nonce) + n += copy(buffer[n:], ciphertext) + binary.BigEndian.PutUint64(buffer[n:], uint64(len(aad))*8) + + // According to documentation, Write() on hash.Hash never fails. + hmac := hmac.New(ctx.hash, ctx.integrityKey) + _, _ = hmac.Write(buffer) + + return hmac.Sum(nil)[:ctx.authtagBytes] +} + +// resize ensures the the given slice has a capacity of at least n bytes. +// If the capacity of the slice is less than n, a new slice is allocated +// and the existing data will be copied. +func resize(in []byte, n uint64) (head, tail []byte) { + if uint64(cap(in)) >= n { + head = in[:n] + } else { + head = make([]byte, n) + copy(head, in) + } + + tail = head[len(in):] + return +} + +// Apply padding +func padBuffer(buffer []byte, blockSize int) []byte { + missing := blockSize - (len(buffer) % blockSize) + ret, out := resize(buffer, uint64(len(buffer))+uint64(missing)) + padding := bytes.Repeat([]byte{byte(missing)}, missing) + copy(out, padding) + return ret +} + +// Remove padding +func unpadBuffer(buffer []byte, blockSize int) ([]byte, error) { + if len(buffer)%blockSize != 0 { + return nil, errors.New("square/go-jose: invalid padding") + } + + last := buffer[len(buffer)-1] + count := int(last) + + if count == 0 || count > blockSize || count > len(buffer) { + return nil, errors.New("square/go-jose: invalid padding") + } + + padding := bytes.Repeat([]byte{last}, count) + if !bytes.HasSuffix(buffer, padding) { + return nil, errors.New("square/go-jose: invalid padding") + } + + return buffer[:len(buffer)-count], nil +} diff --git a/vendor/gopkg.in/square/go-jose.v2/cipher/concat_kdf.go b/vendor/gopkg.in/square/go-jose.v2/cipher/concat_kdf.go new file mode 100644 index 000000000..f62c3bdba --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/cipher/concat_kdf.go @@ -0,0 +1,75 @@ +/*- + * Copyright 2014 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package josecipher + +import ( + "crypto" + "encoding/binary" + "hash" + "io" +) + +type concatKDF struct { + z, info []byte + i uint32 + cache []byte + hasher hash.Hash +} + +// NewConcatKDF builds a KDF reader based on the given inputs. +func NewConcatKDF(hash crypto.Hash, z, algID, ptyUInfo, ptyVInfo, supPubInfo, supPrivInfo []byte) io.Reader { + buffer := make([]byte, uint64(len(algID))+uint64(len(ptyUInfo))+uint64(len(ptyVInfo))+uint64(len(supPubInfo))+uint64(len(supPrivInfo))) + n := 0 + n += copy(buffer, algID) + n += copy(buffer[n:], ptyUInfo) + n += copy(buffer[n:], ptyVInfo) + n += copy(buffer[n:], supPubInfo) + copy(buffer[n:], supPrivInfo) + + hasher := hash.New() + + return &concatKDF{ + z: z, + info: buffer, + hasher: hasher, + cache: []byte{}, + i: 1, + } +} + +func (ctx *concatKDF) Read(out []byte) (int, error) { + copied := copy(out, ctx.cache) + ctx.cache = ctx.cache[copied:] + + for copied < len(out) { + ctx.hasher.Reset() + + // Write on a hash.Hash never fails + _ = binary.Write(ctx.hasher, binary.BigEndian, ctx.i) + _, _ = ctx.hasher.Write(ctx.z) + _, _ = ctx.hasher.Write(ctx.info) + + hash := ctx.hasher.Sum(nil) + chunkCopied := copy(out[copied:], hash) + copied += chunkCopied + ctx.cache = hash[chunkCopied:] + + ctx.i++ + } + + return copied, nil +} diff --git a/vendor/gopkg.in/square/go-jose.v2/cipher/ecdh_es.go b/vendor/gopkg.in/square/go-jose.v2/cipher/ecdh_es.go new file mode 100644 index 000000000..c128e327f --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/cipher/ecdh_es.go @@ -0,0 +1,62 @@ +/*- + * Copyright 2014 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package josecipher + +import ( + "crypto" + "crypto/ecdsa" + "encoding/binary" +) + +// DeriveECDHES derives a shared encryption key using ECDH/ConcatKDF as described in JWE/JWA. +// It is an error to call this function with a private/public key that are not on the same +// curve. Callers must ensure that the keys are valid before calling this function. Output +// size may be at most 1<<16 bytes (64 KiB). +func DeriveECDHES(alg string, apuData, apvData []byte, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, size int) []byte { + if size > 1<<16 { + panic("ECDH-ES output size too large, must be less than or equal to 1<<16") + } + + // algId, partyUInfo, partyVInfo inputs must be prefixed with the length + algID := lengthPrefixed([]byte(alg)) + ptyUInfo := lengthPrefixed(apuData) + ptyVInfo := lengthPrefixed(apvData) + + // suppPubInfo is the encoded length of the output size in bits + supPubInfo := make([]byte, 4) + binary.BigEndian.PutUint32(supPubInfo, uint32(size)*8) + + if !priv.PublicKey.Curve.IsOnCurve(pub.X, pub.Y) { + panic("public key not on same curve as private key") + } + + z, _ := priv.PublicKey.Curve.ScalarMult(pub.X, pub.Y, priv.D.Bytes()) + reader := NewConcatKDF(crypto.SHA256, z.Bytes(), algID, ptyUInfo, ptyVInfo, supPubInfo, []byte{}) + + key := make([]byte, size) + + // Read on the KDF will never fail + _, _ = reader.Read(key) + return key +} + +func lengthPrefixed(data []byte) []byte { + out := make([]byte, len(data)+4) + binary.BigEndian.PutUint32(out, uint32(len(data))) + copy(out[4:], data) + return out +} diff --git a/vendor/gopkg.in/square/go-jose.v2/cipher/key_wrap.go b/vendor/gopkg.in/square/go-jose.v2/cipher/key_wrap.go new file mode 100644 index 000000000..1d36d5015 --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/cipher/key_wrap.go @@ -0,0 +1,109 @@ +/*- + * Copyright 2014 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package josecipher + +import ( + "crypto/cipher" + "crypto/subtle" + "encoding/binary" + "errors" +) + +var defaultIV = []byte{0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6} + +// KeyWrap implements NIST key wrapping; it wraps a content encryption key (cek) with the given block cipher. +func KeyWrap(block cipher.Block, cek []byte) ([]byte, error) { + if len(cek)%8 != 0 { + return nil, errors.New("square/go-jose: key wrap input must be 8 byte blocks") + } + + n := len(cek) / 8 + r := make([][]byte, n) + + for i := range r { + r[i] = make([]byte, 8) + copy(r[i], cek[i*8:]) + } + + buffer := make([]byte, 16) + tBytes := make([]byte, 8) + copy(buffer, defaultIV) + + for t := 0; t < 6*n; t++ { + copy(buffer[8:], r[t%n]) + + block.Encrypt(buffer, buffer) + + binary.BigEndian.PutUint64(tBytes, uint64(t+1)) + + for i := 0; i < 8; i++ { + buffer[i] = buffer[i] ^ tBytes[i] + } + copy(r[t%n], buffer[8:]) + } + + out := make([]byte, (n+1)*8) + copy(out, buffer[:8]) + for i := range r { + copy(out[(i+1)*8:], r[i]) + } + + return out, nil +} + +// KeyUnwrap implements NIST key unwrapping; it unwraps a content encryption key (cek) with the given block cipher. +func KeyUnwrap(block cipher.Block, ciphertext []byte) ([]byte, error) { + if len(ciphertext)%8 != 0 { + return nil, errors.New("square/go-jose: key wrap input must be 8 byte blocks") + } + + n := (len(ciphertext) / 8) - 1 + r := make([][]byte, n) + + for i := range r { + r[i] = make([]byte, 8) + copy(r[i], ciphertext[(i+1)*8:]) + } + + buffer := make([]byte, 16) + tBytes := make([]byte, 8) + copy(buffer[:8], ciphertext[:8]) + + for t := 6*n - 1; t >= 0; t-- { + binary.BigEndian.PutUint64(tBytes, uint64(t+1)) + + for i := 0; i < 8; i++ { + buffer[i] = buffer[i] ^ tBytes[i] + } + copy(buffer[8:], r[t%n]) + + block.Decrypt(buffer, buffer) + + copy(r[t%n], buffer[8:]) + } + + if subtle.ConstantTimeCompare(buffer[:8], defaultIV) == 0 { + return nil, errors.New("square/go-jose: failed to unwrap key") + } + + out := make([]byte, n*8) + for i := range r { + copy(out[i*8:], r[i]) + } + + return out, nil +} diff --git a/vendor/gopkg.in/square/go-jose.v2/crypter.go b/vendor/gopkg.in/square/go-jose.v2/crypter.go new file mode 100644 index 000000000..0681c8119 --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/crypter.go @@ -0,0 +1,510 @@ +/*- + * Copyright 2014 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jose + +import ( + "crypto/ecdsa" + "crypto/rsa" + "errors" + "fmt" + "reflect" + + "gopkg.in/square/go-jose.v2/json" +) + +// Encrypter represents an encrypter which produces an encrypted JWE object. +type Encrypter interface { + Encrypt(plaintext []byte) (*JSONWebEncryption, error) + EncryptWithAuthData(plaintext []byte, aad []byte) (*JSONWebEncryption, error) + Options() EncrypterOptions +} + +// A generic content cipher +type contentCipher interface { + keySize() int + encrypt(cek []byte, aad, plaintext []byte) (*aeadParts, error) + decrypt(cek []byte, aad []byte, parts *aeadParts) ([]byte, error) +} + +// A key generator (for generating/getting a CEK) +type keyGenerator interface { + keySize() int + genKey() ([]byte, rawHeader, error) +} + +// A generic key encrypter +type keyEncrypter interface { + encryptKey(cek []byte, alg KeyAlgorithm) (recipientInfo, error) // Encrypt a key +} + +// A generic key decrypter +type keyDecrypter interface { + decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) // Decrypt a key +} + +// A generic encrypter based on the given key encrypter and content cipher. +type genericEncrypter struct { + contentAlg ContentEncryption + compressionAlg CompressionAlgorithm + cipher contentCipher + recipients []recipientKeyInfo + keyGenerator keyGenerator + extraHeaders map[HeaderKey]interface{} +} + +type recipientKeyInfo struct { + keyID string + keyAlg KeyAlgorithm + keyEncrypter keyEncrypter +} + +// EncrypterOptions represents options that can be set on new encrypters. +type EncrypterOptions struct { + Compression CompressionAlgorithm + + // Optional map of additional keys to be inserted into the protected header + // of a JWS object. Some specifications which make use of JWS like to insert + // additional values here. All values must be JSON-serializable. + ExtraHeaders map[HeaderKey]interface{} +} + +// WithHeader adds an arbitrary value to the ExtraHeaders map, initializing it +// if necessary. It returns itself and so can be used in a fluent style. +func (eo *EncrypterOptions) WithHeader(k HeaderKey, v interface{}) *EncrypterOptions { + if eo.ExtraHeaders == nil { + eo.ExtraHeaders = map[HeaderKey]interface{}{} + } + eo.ExtraHeaders[k] = v + return eo +} + +// WithContentType adds a content type ("cty") header and returns the updated +// EncrypterOptions. +func (eo *EncrypterOptions) WithContentType(contentType ContentType) *EncrypterOptions { + return eo.WithHeader(HeaderContentType, contentType) +} + +// WithType adds a type ("typ") header and returns the updated EncrypterOptions. +func (eo *EncrypterOptions) WithType(typ ContentType) *EncrypterOptions { + return eo.WithHeader(HeaderType, typ) +} + +// Recipient represents an algorithm/key to encrypt messages to. +type Recipient struct { + Algorithm KeyAlgorithm + Key interface{} + KeyID string +} + +// NewEncrypter creates an appropriate encrypter based on the key type +func NewEncrypter(enc ContentEncryption, rcpt Recipient, opts *EncrypterOptions) (Encrypter, error) { + encrypter := &genericEncrypter{ + contentAlg: enc, + recipients: []recipientKeyInfo{}, + cipher: getContentCipher(enc), + } + if opts != nil { + encrypter.compressionAlg = opts.Compression + encrypter.extraHeaders = opts.ExtraHeaders + } + + if encrypter.cipher == nil { + return nil, ErrUnsupportedAlgorithm + } + + var keyID string + var rawKey interface{} + switch encryptionKey := rcpt.Key.(type) { + case JSONWebKey: + keyID, rawKey = encryptionKey.KeyID, encryptionKey.Key + case *JSONWebKey: + keyID, rawKey = encryptionKey.KeyID, encryptionKey.Key + default: + rawKey = encryptionKey + } + + switch rcpt.Algorithm { + case DIRECT: + // Direct encryption mode must be treated differently + if reflect.TypeOf(rawKey) != reflect.TypeOf([]byte{}) { + return nil, ErrUnsupportedKeyType + } + encrypter.keyGenerator = staticKeyGenerator{ + key: rawKey.([]byte), + } + recipientInfo, _ := newSymmetricRecipient(rcpt.Algorithm, rawKey.([]byte)) + recipientInfo.keyID = keyID + if rcpt.KeyID != "" { + recipientInfo.keyID = rcpt.KeyID + } + encrypter.recipients = []recipientKeyInfo{recipientInfo} + return encrypter, nil + case ECDH_ES: + // ECDH-ES (w/o key wrapping) is similar to DIRECT mode + typeOf := reflect.TypeOf(rawKey) + if typeOf != reflect.TypeOf(&ecdsa.PublicKey{}) { + return nil, ErrUnsupportedKeyType + } + encrypter.keyGenerator = ecKeyGenerator{ + size: encrypter.cipher.keySize(), + algID: string(enc), + publicKey: rawKey.(*ecdsa.PublicKey), + } + recipientInfo, _ := newECDHRecipient(rcpt.Algorithm, rawKey.(*ecdsa.PublicKey)) + recipientInfo.keyID = keyID + if rcpt.KeyID != "" { + recipientInfo.keyID = rcpt.KeyID + } + encrypter.recipients = []recipientKeyInfo{recipientInfo} + return encrypter, nil + default: + // Can just add a standard recipient + encrypter.keyGenerator = randomKeyGenerator{ + size: encrypter.cipher.keySize(), + } + err := encrypter.addRecipient(rcpt) + return encrypter, err + } +} + +// NewMultiEncrypter creates a multi-encrypter based on the given parameters +func NewMultiEncrypter(enc ContentEncryption, rcpts []Recipient, opts *EncrypterOptions) (Encrypter, error) { + cipher := getContentCipher(enc) + + if cipher == nil { + return nil, ErrUnsupportedAlgorithm + } + if rcpts == nil || len(rcpts) == 0 { + return nil, fmt.Errorf("square/go-jose: recipients is nil or empty") + } + + encrypter := &genericEncrypter{ + contentAlg: enc, + recipients: []recipientKeyInfo{}, + cipher: cipher, + keyGenerator: randomKeyGenerator{ + size: cipher.keySize(), + }, + } + + if opts != nil { + encrypter.compressionAlg = opts.Compression + } + + for _, recipient := range rcpts { + err := encrypter.addRecipient(recipient) + if err != nil { + return nil, err + } + } + + return encrypter, nil +} + +func (ctx *genericEncrypter) addRecipient(recipient Recipient) (err error) { + var recipientInfo recipientKeyInfo + + switch recipient.Algorithm { + case DIRECT, ECDH_ES: + return fmt.Errorf("square/go-jose: key algorithm '%s' not supported in multi-recipient mode", recipient.Algorithm) + } + + recipientInfo, err = makeJWERecipient(recipient.Algorithm, recipient.Key) + if recipient.KeyID != "" { + recipientInfo.keyID = recipient.KeyID + } + + if err == nil { + ctx.recipients = append(ctx.recipients, recipientInfo) + } + return err +} + +func makeJWERecipient(alg KeyAlgorithm, encryptionKey interface{}) (recipientKeyInfo, error) { + switch encryptionKey := encryptionKey.(type) { + case *rsa.PublicKey: + return newRSARecipient(alg, encryptionKey) + case *ecdsa.PublicKey: + return newECDHRecipient(alg, encryptionKey) + case []byte: + return newSymmetricRecipient(alg, encryptionKey) + case *JSONWebKey: + recipient, err := makeJWERecipient(alg, encryptionKey.Key) + recipient.keyID = encryptionKey.KeyID + return recipient, err + default: + return recipientKeyInfo{}, ErrUnsupportedKeyType + } +} + +// newDecrypter creates an appropriate decrypter based on the key type +func newDecrypter(decryptionKey interface{}) (keyDecrypter, error) { + switch decryptionKey := decryptionKey.(type) { + case *rsa.PrivateKey: + return &rsaDecrypterSigner{ + privateKey: decryptionKey, + }, nil + case *ecdsa.PrivateKey: + return &ecDecrypterSigner{ + privateKey: decryptionKey, + }, nil + case []byte: + return &symmetricKeyCipher{ + key: decryptionKey, + }, nil + case JSONWebKey: + return newDecrypter(decryptionKey.Key) + case *JSONWebKey: + return newDecrypter(decryptionKey.Key) + default: + return nil, ErrUnsupportedKeyType + } +} + +// Implementation of encrypt method producing a JWE object. +func (ctx *genericEncrypter) Encrypt(plaintext []byte) (*JSONWebEncryption, error) { + return ctx.EncryptWithAuthData(plaintext, nil) +} + +// Implementation of encrypt method producing a JWE object. +func (ctx *genericEncrypter) EncryptWithAuthData(plaintext, aad []byte) (*JSONWebEncryption, error) { + obj := &JSONWebEncryption{} + obj.aad = aad + + obj.protected = &rawHeader{} + err := obj.protected.set(headerEncryption, ctx.contentAlg) + if err != nil { + return nil, err + } + + obj.recipients = make([]recipientInfo, len(ctx.recipients)) + + if len(ctx.recipients) == 0 { + return nil, fmt.Errorf("square/go-jose: no recipients to encrypt to") + } + + cek, headers, err := ctx.keyGenerator.genKey() + if err != nil { + return nil, err + } + + obj.protected.merge(&headers) + + for i, info := range ctx.recipients { + recipient, err := info.keyEncrypter.encryptKey(cek, info.keyAlg) + if err != nil { + return nil, err + } + + err = recipient.header.set(headerAlgorithm, info.keyAlg) + if err != nil { + return nil, err + } + + if info.keyID != "" { + err = recipient.header.set(headerKeyID, info.keyID) + if err != nil { + return nil, err + } + } + obj.recipients[i] = recipient + } + + if len(ctx.recipients) == 1 { + // Move per-recipient headers into main protected header if there's + // only a single recipient. + obj.protected.merge(obj.recipients[0].header) + obj.recipients[0].header = nil + } + + if ctx.compressionAlg != NONE { + plaintext, err = compress(ctx.compressionAlg, plaintext) + if err != nil { + return nil, err + } + + err = obj.protected.set(headerCompression, ctx.compressionAlg) + if err != nil { + return nil, err + } + } + + for k, v := range ctx.extraHeaders { + b, err := json.Marshal(v) + if err != nil { + return nil, err + } + (*obj.protected)[k] = makeRawMessage(b) + } + + authData := obj.computeAuthData() + parts, err := ctx.cipher.encrypt(cek, authData, plaintext) + if err != nil { + return nil, err + } + + obj.iv = parts.iv + obj.ciphertext = parts.ciphertext + obj.tag = parts.tag + + return obj, nil +} + +func (ctx *genericEncrypter) Options() EncrypterOptions { + return EncrypterOptions{ + Compression: ctx.compressionAlg, + ExtraHeaders: ctx.extraHeaders, + } +} + +// Decrypt and validate the object and return the plaintext. Note that this +// function does not support multi-recipient, if you desire multi-recipient +// decryption use DecryptMulti instead. +func (obj JSONWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error) { + headers := obj.mergedHeaders(nil) + + if len(obj.recipients) > 1 { + return nil, errors.New("square/go-jose: too many recipients in payload; expecting only one") + } + + critical, err := headers.getCritical() + if err != nil { + return nil, fmt.Errorf("square/go-jose: invalid crit header") + } + + if len(critical) > 0 { + return nil, fmt.Errorf("square/go-jose: unsupported crit header") + } + + decrypter, err := newDecrypter(decryptionKey) + if err != nil { + return nil, err + } + + cipher := getContentCipher(headers.getEncryption()) + if cipher == nil { + return nil, fmt.Errorf("square/go-jose: unsupported enc value '%s'", string(headers.getEncryption())) + } + + generator := randomKeyGenerator{ + size: cipher.keySize(), + } + + parts := &aeadParts{ + iv: obj.iv, + ciphertext: obj.ciphertext, + tag: obj.tag, + } + + authData := obj.computeAuthData() + + var plaintext []byte + recipient := obj.recipients[0] + recipientHeaders := obj.mergedHeaders(&recipient) + + cek, err := decrypter.decryptKey(recipientHeaders, &recipient, generator) + if err == nil { + // Found a valid CEK -- let's try to decrypt. + plaintext, err = cipher.decrypt(cek, authData, parts) + } + + if plaintext == nil { + return nil, ErrCryptoFailure + } + + // The "zip" header parameter may only be present in the protected header. + if comp := obj.protected.getCompression(); comp != "" { + plaintext, err = decompress(comp, plaintext) + } + + return plaintext, err +} + +// DecryptMulti decrypts and validates the object and returns the plaintexts, +// with support for multiple recipients. It returns the index of the recipient +// for which the decryption was successful, the merged headers for that recipient, +// and the plaintext. +func (obj JSONWebEncryption) DecryptMulti(decryptionKey interface{}) (int, Header, []byte, error) { + globalHeaders := obj.mergedHeaders(nil) + + critical, err := globalHeaders.getCritical() + if err != nil { + return -1, Header{}, nil, fmt.Errorf("square/go-jose: invalid crit header") + } + + if len(critical) > 0 { + return -1, Header{}, nil, fmt.Errorf("square/go-jose: unsupported crit header") + } + + decrypter, err := newDecrypter(decryptionKey) + if err != nil { + return -1, Header{}, nil, err + } + + encryption := globalHeaders.getEncryption() + cipher := getContentCipher(encryption) + if cipher == nil { + return -1, Header{}, nil, fmt.Errorf("square/go-jose: unsupported enc value '%s'", string(encryption)) + } + + generator := randomKeyGenerator{ + size: cipher.keySize(), + } + + parts := &aeadParts{ + iv: obj.iv, + ciphertext: obj.ciphertext, + tag: obj.tag, + } + + authData := obj.computeAuthData() + + index := -1 + var plaintext []byte + var headers rawHeader + + for i, recipient := range obj.recipients { + recipientHeaders := obj.mergedHeaders(&recipient) + + cek, err := decrypter.decryptKey(recipientHeaders, &recipient, generator) + if err == nil { + // Found a valid CEK -- let's try to decrypt. + plaintext, err = cipher.decrypt(cek, authData, parts) + if err == nil { + index = i + headers = recipientHeaders + break + } + } + } + + if plaintext == nil || err != nil { + return -1, Header{}, nil, ErrCryptoFailure + } + + // The "zip" header parameter may only be present in the protected header. + if comp := obj.protected.getCompression(); comp != "" { + plaintext, err = decompress(comp, plaintext) + } + + sanitized, err := headers.sanitized() + if err != nil { + return -1, Header{}, nil, fmt.Errorf("square/go-jose: failed to sanitize header: %v", err) + } + + return index, sanitized, plaintext, err +} diff --git a/vendor/gopkg.in/square/go-jose.v2/doc.go b/vendor/gopkg.in/square/go-jose.v2/doc.go new file mode 100644 index 000000000..dd1387f3f --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/doc.go @@ -0,0 +1,27 @@ +/*- + * Copyright 2014 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + +Package jose aims to provide an implementation of the Javascript Object Signing +and Encryption set of standards. It implements encryption and signing based on +the JSON Web Encryption and JSON Web Signature standards, with optional JSON +Web Token support available in a sub-package. The library supports both the +compact and full serialization formats, and has optional support for multiple +recipients. + +*/ +package jose diff --git a/vendor/gopkg.in/square/go-jose.v2/encoding.go b/vendor/gopkg.in/square/go-jose.v2/encoding.go new file mode 100644 index 000000000..9f37ef465 --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/encoding.go @@ -0,0 +1,178 @@ +/*- + * Copyright 2014 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jose + +import ( + "bytes" + "compress/flate" + "encoding/base64" + "encoding/binary" + "encoding/json" + "io" + "math/big" + "regexp" +) + +var stripWhitespaceRegex = regexp.MustCompile("\\s") + +// Helper function to serialize known-good objects. +// Precondition: value is not a nil pointer. +func mustSerializeJSON(value interface{}) []byte { + out, err := json.Marshal(value) + if err != nil { + panic(err) + } + // We never want to serialize the top-level value "null," since it's not a + // valid JOSE message. But if a caller passes in a nil pointer to this method, + // MarshalJSON will happily serialize it as the top-level value "null". If + // that value is then embedded in another operation, for instance by being + // base64-encoded and fed as input to a signing algorithm + // (https://github.com/square/go-jose/issues/22), the result will be + // incorrect. Because this method is intended for known-good objects, and a nil + // pointer is not a known-good object, we are free to panic in this case. + // Note: It's not possible to directly check whether the data pointed at by an + // interface is a nil pointer, so we do this hacky workaround. + // https://groups.google.com/forum/#!topic/golang-nuts/wnH302gBa4I + if string(out) == "null" { + panic("Tried to serialize a nil pointer.") + } + return out +} + +// Strip all newlines and whitespace +func stripWhitespace(data string) string { + return stripWhitespaceRegex.ReplaceAllString(data, "") +} + +// Perform compression based on algorithm +func compress(algorithm CompressionAlgorithm, input []byte) ([]byte, error) { + switch algorithm { + case DEFLATE: + return deflate(input) + default: + return nil, ErrUnsupportedAlgorithm + } +} + +// Perform decompression based on algorithm +func decompress(algorithm CompressionAlgorithm, input []byte) ([]byte, error) { + switch algorithm { + case DEFLATE: + return inflate(input) + default: + return nil, ErrUnsupportedAlgorithm + } +} + +// Compress with DEFLATE +func deflate(input []byte) ([]byte, error) { + output := new(bytes.Buffer) + + // Writing to byte buffer, err is always nil + writer, _ := flate.NewWriter(output, 1) + _, _ = io.Copy(writer, bytes.NewBuffer(input)) + + err := writer.Close() + return output.Bytes(), err +} + +// Decompress with DEFLATE +func inflate(input []byte) ([]byte, error) { + output := new(bytes.Buffer) + reader := flate.NewReader(bytes.NewBuffer(input)) + + _, err := io.Copy(output, reader) + if err != nil { + return nil, err + } + + err = reader.Close() + return output.Bytes(), err +} + +// byteBuffer represents a slice of bytes that can be serialized to url-safe base64. +type byteBuffer struct { + data []byte +} + +func newBuffer(data []byte) *byteBuffer { + if data == nil { + return nil + } + return &byteBuffer{ + data: data, + } +} + +func newFixedSizeBuffer(data []byte, length int) *byteBuffer { + if len(data) > length { + panic("square/go-jose: invalid call to newFixedSizeBuffer (len(data) > length)") + } + pad := make([]byte, length-len(data)) + return newBuffer(append(pad, data...)) +} + +func newBufferFromInt(num uint64) *byteBuffer { + data := make([]byte, 8) + binary.BigEndian.PutUint64(data, num) + return newBuffer(bytes.TrimLeft(data, "\x00")) +} + +func (b *byteBuffer) MarshalJSON() ([]byte, error) { + return json.Marshal(b.base64()) +} + +func (b *byteBuffer) UnmarshalJSON(data []byte) error { + var encoded string + err := json.Unmarshal(data, &encoded) + if err != nil { + return err + } + + if encoded == "" { + return nil + } + + decoded, err := base64.RawURLEncoding.DecodeString(encoded) + if err != nil { + return err + } + + *b = *newBuffer(decoded) + + return nil +} + +func (b *byteBuffer) base64() string { + return base64.RawURLEncoding.EncodeToString(b.data) +} + +func (b *byteBuffer) bytes() []byte { + // Handling nil here allows us to transparently handle nil slices when serializing. + if b == nil { + return nil + } + return b.data +} + +func (b byteBuffer) bigInt() *big.Int { + return new(big.Int).SetBytes(b.data) +} + +func (b byteBuffer) toInt() int { + return int(b.bigInt().Int64()) +} diff --git a/vendor/gopkg.in/square/go-jose.v2/json/LICENSE b/vendor/gopkg.in/square/go-jose.v2/json/LICENSE new file mode 100644 index 000000000..744875676 --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/json/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/gopkg.in/square/go-jose.v2/json/decode.go b/vendor/gopkg.in/square/go-jose.v2/json/decode.go new file mode 100644 index 000000000..37457e5a8 --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/json/decode.go @@ -0,0 +1,1183 @@ +// Copyright 2010 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. + +// Represents JSON data structure using native Go types: booleans, floats, +// strings, arrays, and maps. + +package json + +import ( + "bytes" + "encoding" + "encoding/base64" + "errors" + "fmt" + "reflect" + "runtime" + "strconv" + "unicode" + "unicode/utf16" + "unicode/utf8" +) + +// Unmarshal parses the JSON-encoded data and stores the result +// in the value pointed to by v. +// +// Unmarshal uses the inverse of the encodings that +// Marshal uses, allocating maps, slices, and pointers as necessary, +// with the following additional rules: +// +// To unmarshal JSON into a pointer, Unmarshal first handles the case of +// the JSON being the JSON literal null. In that case, Unmarshal sets +// the pointer to nil. Otherwise, Unmarshal unmarshals the JSON into +// the value pointed at by the pointer. If the pointer is nil, Unmarshal +// allocates a new value for it to point to. +// +// To unmarshal JSON into a struct, Unmarshal matches incoming object +// keys to the keys used by Marshal (either the struct field name or its tag), +// preferring an exact match but also accepting a case-insensitive match. +// Unmarshal will only set exported fields of the struct. +// +// To unmarshal JSON into an interface value, +// Unmarshal stores one of these in the interface value: +// +// bool, for JSON booleans +// float64, for JSON numbers +// string, for JSON strings +// []interface{}, for JSON arrays +// map[string]interface{}, for JSON objects +// nil for JSON null +// +// To unmarshal a JSON array into a slice, Unmarshal resets the slice length +// to zero and then appends each element to the slice. +// As a special case, to unmarshal an empty JSON array into a slice, +// Unmarshal replaces the slice with a new empty slice. +// +// To unmarshal a JSON array into a Go array, Unmarshal decodes +// JSON array elements into corresponding Go array elements. +// If the Go array is smaller than the JSON array, +// the additional JSON array elements are discarded. +// If the JSON array is smaller than the Go array, +// the additional Go array elements are set to zero values. +// +// To unmarshal a JSON object into a string-keyed map, Unmarshal first +// establishes a map to use, If the map is nil, Unmarshal allocates a new map. +// Otherwise Unmarshal reuses the existing map, keeping existing entries. +// Unmarshal then stores key-value pairs from the JSON object into the map. +// +// If a JSON value is not appropriate for a given target type, +// or if a JSON number overflows the target type, Unmarshal +// skips that field and completes the unmarshaling as best it can. +// If no more serious errors are encountered, Unmarshal returns +// an UnmarshalTypeError describing the earliest such error. +// +// The JSON null value unmarshals into an interface, map, pointer, or slice +// by setting that Go value to nil. Because null is often used in JSON to mean +// ``not present,'' unmarshaling a JSON null into any other Go type has no effect +// on the value and produces no error. +// +// When unmarshaling quoted strings, invalid UTF-8 or +// invalid UTF-16 surrogate pairs are not treated as an error. +// Instead, they are replaced by the Unicode replacement +// character U+FFFD. +// +func Unmarshal(data []byte, v interface{}) error { + // Check for well-formedness. + // Avoids filling out half a data structure + // before discovering a JSON syntax error. + var d decodeState + err := checkValid(data, &d.scan) + if err != nil { + return err + } + + d.init(data) + return d.unmarshal(v) +} + +// Unmarshaler is the interface implemented by objects +// that can unmarshal a JSON description of themselves. +// The input can be assumed to be a valid encoding of +// a JSON value. UnmarshalJSON must copy the JSON data +// if it wishes to retain the data after returning. +type Unmarshaler interface { + UnmarshalJSON([]byte) error +} + +// An UnmarshalTypeError describes a JSON value that was +// not appropriate for a value of a specific Go type. +type UnmarshalTypeError struct { + Value string // description of JSON value - "bool", "array", "number -5" + Type reflect.Type // type of Go value it could not be assigned to + Offset int64 // error occurred after reading Offset bytes +} + +func (e *UnmarshalTypeError) Error() string { + return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String() +} + +// An UnmarshalFieldError describes a JSON object key that +// led to an unexported (and therefore unwritable) struct field. +// (No longer used; kept for compatibility.) +type UnmarshalFieldError struct { + Key string + Type reflect.Type + Field reflect.StructField +} + +func (e *UnmarshalFieldError) Error() string { + return "json: cannot unmarshal object key " + strconv.Quote(e.Key) + " into unexported field " + e.Field.Name + " of type " + e.Type.String() +} + +// An InvalidUnmarshalError describes an invalid argument passed to Unmarshal. +// (The argument to Unmarshal must be a non-nil pointer.) +type InvalidUnmarshalError struct { + Type reflect.Type +} + +func (e *InvalidUnmarshalError) Error() string { + if e.Type == nil { + return "json: Unmarshal(nil)" + } + + if e.Type.Kind() != reflect.Ptr { + return "json: Unmarshal(non-pointer " + e.Type.String() + ")" + } + return "json: Unmarshal(nil " + e.Type.String() + ")" +} + +func (d *decodeState) unmarshal(v interface{}) (err error) { + defer func() { + if r := recover(); r != nil { + if _, ok := r.(runtime.Error); ok { + panic(r) + } + err = r.(error) + } + }() + + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Ptr || rv.IsNil() { + return &InvalidUnmarshalError{reflect.TypeOf(v)} + } + + d.scan.reset() + // We decode rv not rv.Elem because the Unmarshaler interface + // test must be applied at the top level of the value. + d.value(rv) + return d.savedError +} + +// A Number represents a JSON number literal. +type Number string + +// String returns the literal text of the number. +func (n Number) String() string { return string(n) } + +// Float64 returns the number as a float64. +func (n Number) Float64() (float64, error) { + return strconv.ParseFloat(string(n), 64) +} + +// Int64 returns the number as an int64. +func (n Number) Int64() (int64, error) { + return strconv.ParseInt(string(n), 10, 64) +} + +// isValidNumber reports whether s is a valid JSON number literal. +func isValidNumber(s string) bool { + // This function implements the JSON numbers grammar. + // See https://tools.ietf.org/html/rfc7159#section-6 + // and http://json.org/number.gif + + if s == "" { + return false + } + + // Optional - + if s[0] == '-' { + s = s[1:] + if s == "" { + return false + } + } + + // Digits + switch { + default: + return false + + case s[0] == '0': + s = s[1:] + + case '1' <= s[0] && s[0] <= '9': + s = s[1:] + for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { + s = s[1:] + } + } + + // . followed by 1 or more digits. + if len(s) >= 2 && s[0] == '.' && '0' <= s[1] && s[1] <= '9' { + s = s[2:] + for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { + s = s[1:] + } + } + + // e or E followed by an optional - or + and + // 1 or more digits. + if len(s) >= 2 && (s[0] == 'e' || s[0] == 'E') { + s = s[1:] + if s[0] == '+' || s[0] == '-' { + s = s[1:] + if s == "" { + return false + } + } + for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { + s = s[1:] + } + } + + // Make sure we are at the end. + return s == "" +} + +// decodeState represents the state while decoding a JSON value. +type decodeState struct { + data []byte + off int // read offset in data + scan scanner + nextscan scanner // for calls to nextValue + savedError error + useNumber bool +} + +// errPhase is used for errors that should not happen unless +// there is a bug in the JSON decoder or something is editing +// the data slice while the decoder executes. +var errPhase = errors.New("JSON decoder out of sync - data changing underfoot?") + +func (d *decodeState) init(data []byte) *decodeState { + d.data = data + d.off = 0 + d.savedError = nil + return d +} + +// error aborts the decoding by panicking with err. +func (d *decodeState) error(err error) { + panic(err) +} + +// saveError saves the first err it is called with, +// for reporting at the end of the unmarshal. +func (d *decodeState) saveError(err error) { + if d.savedError == nil { + d.savedError = err + } +} + +// next cuts off and returns the next full JSON value in d.data[d.off:]. +// The next value is known to be an object or array, not a literal. +func (d *decodeState) next() []byte { + c := d.data[d.off] + item, rest, err := nextValue(d.data[d.off:], &d.nextscan) + if err != nil { + d.error(err) + } + d.off = len(d.data) - len(rest) + + // Our scanner has seen the opening brace/bracket + // and thinks we're still in the middle of the object. + // invent a closing brace/bracket to get it out. + if c == '{' { + d.scan.step(&d.scan, '}') + } else { + d.scan.step(&d.scan, ']') + } + + return item +} + +// scanWhile processes bytes in d.data[d.off:] until it +// receives a scan code not equal to op. +// It updates d.off and returns the new scan code. +func (d *decodeState) scanWhile(op int) int { + var newOp int + for { + if d.off >= len(d.data) { + newOp = d.scan.eof() + d.off = len(d.data) + 1 // mark processed EOF with len+1 + } else { + c := d.data[d.off] + d.off++ + newOp = d.scan.step(&d.scan, c) + } + if newOp != op { + break + } + } + return newOp +} + +// value decodes a JSON value from d.data[d.off:] into the value. +// it updates d.off to point past the decoded value. +func (d *decodeState) value(v reflect.Value) { + if !v.IsValid() { + _, rest, err := nextValue(d.data[d.off:], &d.nextscan) + if err != nil { + d.error(err) + } + d.off = len(d.data) - len(rest) + + // d.scan thinks we're still at the beginning of the item. + // Feed in an empty string - the shortest, simplest value - + // so that it knows we got to the end of the value. + if d.scan.redo { + // rewind. + d.scan.redo = false + d.scan.step = stateBeginValue + } + d.scan.step(&d.scan, '"') + d.scan.step(&d.scan, '"') + + n := len(d.scan.parseState) + if n > 0 && d.scan.parseState[n-1] == parseObjectKey { + // d.scan thinks we just read an object key; finish the object + d.scan.step(&d.scan, ':') + d.scan.step(&d.scan, '"') + d.scan.step(&d.scan, '"') + d.scan.step(&d.scan, '}') + } + + return + } + + switch op := d.scanWhile(scanSkipSpace); op { + default: + d.error(errPhase) + + case scanBeginArray: + d.array(v) + + case scanBeginObject: + d.object(v) + + case scanBeginLiteral: + d.literal(v) + } +} + +type unquotedValue struct{} + +// valueQuoted is like value but decodes a +// quoted string literal or literal null into an interface value. +// If it finds anything other than a quoted string literal or null, +// valueQuoted returns unquotedValue{}. +func (d *decodeState) valueQuoted() interface{} { + switch op := d.scanWhile(scanSkipSpace); op { + default: + d.error(errPhase) + + case scanBeginArray: + d.array(reflect.Value{}) + + case scanBeginObject: + d.object(reflect.Value{}) + + case scanBeginLiteral: + switch v := d.literalInterface().(type) { + case nil, string: + return v + } + } + return unquotedValue{} +} + +// indirect walks down v allocating pointers as needed, +// until it gets to a non-pointer. +// if it encounters an Unmarshaler, indirect stops and returns that. +// if decodingNull is true, indirect stops at the last pointer so it can be set to nil. +func (d *decodeState) indirect(v reflect.Value, decodingNull bool) (Unmarshaler, encoding.TextUnmarshaler, reflect.Value) { + // If v is a named type and is addressable, + // start with its address, so that if the type has pointer methods, + // we find them. + if v.Kind() != reflect.Ptr && v.Type().Name() != "" && v.CanAddr() { + v = v.Addr() + } + for { + // Load value from interface, but only if the result will be + // usefully addressable. + if v.Kind() == reflect.Interface && !v.IsNil() { + e := v.Elem() + if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) { + v = e + continue + } + } + + if v.Kind() != reflect.Ptr { + break + } + + if v.Elem().Kind() != reflect.Ptr && decodingNull && v.CanSet() { + break + } + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + if v.Type().NumMethod() > 0 { + if u, ok := v.Interface().(Unmarshaler); ok { + return u, nil, reflect.Value{} + } + if u, ok := v.Interface().(encoding.TextUnmarshaler); ok { + return nil, u, reflect.Value{} + } + } + v = v.Elem() + } + return nil, nil, v +} + +// array consumes an array from d.data[d.off-1:], decoding into the value v. +// the first byte of the array ('[') has been read already. +func (d *decodeState) array(v reflect.Value) { + // Check for unmarshaler. + u, ut, pv := d.indirect(v, false) + if u != nil { + d.off-- + err := u.UnmarshalJSON(d.next()) + if err != nil { + d.error(err) + } + return + } + if ut != nil { + d.saveError(&UnmarshalTypeError{"array", v.Type(), int64(d.off)}) + d.off-- + d.next() + return + } + + v = pv + + // Check type of target. + switch v.Kind() { + case reflect.Interface: + if v.NumMethod() == 0 { + // Decoding into nil interface? Switch to non-reflect code. + v.Set(reflect.ValueOf(d.arrayInterface())) + return + } + // Otherwise it's invalid. + fallthrough + default: + d.saveError(&UnmarshalTypeError{"array", v.Type(), int64(d.off)}) + d.off-- + d.next() + return + case reflect.Array: + case reflect.Slice: + break + } + + i := 0 + for { + // Look ahead for ] - can only happen on first iteration. + op := d.scanWhile(scanSkipSpace) + if op == scanEndArray { + break + } + + // Back up so d.value can have the byte we just read. + d.off-- + d.scan.undo(op) + + // Get element of array, growing if necessary. + if v.Kind() == reflect.Slice { + // Grow slice if necessary + if i >= v.Cap() { + newcap := v.Cap() + v.Cap()/2 + if newcap < 4 { + newcap = 4 + } + newv := reflect.MakeSlice(v.Type(), v.Len(), newcap) + reflect.Copy(newv, v) + v.Set(newv) + } + if i >= v.Len() { + v.SetLen(i + 1) + } + } + + if i < v.Len() { + // Decode into element. + d.value(v.Index(i)) + } else { + // Ran out of fixed array: skip. + d.value(reflect.Value{}) + } + i++ + + // Next token must be , or ]. + op = d.scanWhile(scanSkipSpace) + if op == scanEndArray { + break + } + if op != scanArrayValue { + d.error(errPhase) + } + } + + if i < v.Len() { + if v.Kind() == reflect.Array { + // Array. Zero the rest. + z := reflect.Zero(v.Type().Elem()) + for ; i < v.Len(); i++ { + v.Index(i).Set(z) + } + } else { + v.SetLen(i) + } + } + if i == 0 && v.Kind() == reflect.Slice { + v.Set(reflect.MakeSlice(v.Type(), 0, 0)) + } +} + +var nullLiteral = []byte("null") + +// object consumes an object from d.data[d.off-1:], decoding into the value v. +// the first byte ('{') of the object has been read already. +func (d *decodeState) object(v reflect.Value) { + // Check for unmarshaler. + u, ut, pv := d.indirect(v, false) + if u != nil { + d.off-- + err := u.UnmarshalJSON(d.next()) + if err != nil { + d.error(err) + } + return + } + if ut != nil { + d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) + d.off-- + d.next() // skip over { } in input + return + } + v = pv + + // Decoding into nil interface? Switch to non-reflect code. + if v.Kind() == reflect.Interface && v.NumMethod() == 0 { + v.Set(reflect.ValueOf(d.objectInterface())) + return + } + + // Check type of target: struct or map[string]T + switch v.Kind() { + case reflect.Map: + // map must have string kind + t := v.Type() + if t.Key().Kind() != reflect.String { + d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) + d.off-- + d.next() // skip over { } in input + return + } + if v.IsNil() { + v.Set(reflect.MakeMap(t)) + } + case reflect.Struct: + + default: + d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) + d.off-- + d.next() // skip over { } in input + return + } + + var mapElem reflect.Value + keys := map[string]bool{} + + for { + // Read opening " of string key or closing }. + op := d.scanWhile(scanSkipSpace) + if op == scanEndObject { + // closing } - can only happen on first iteration. + break + } + if op != scanBeginLiteral { + d.error(errPhase) + } + + // Read key. + start := d.off - 1 + op = d.scanWhile(scanContinue) + item := d.data[start : d.off-1] + key, ok := unquote(item) + if !ok { + d.error(errPhase) + } + + // Check for duplicate keys. + _, ok = keys[key] + if !ok { + keys[key] = true + } else { + d.error(fmt.Errorf("json: duplicate key '%s' in object", key)) + } + + // Figure out field corresponding to key. + var subv reflect.Value + destring := false // whether the value is wrapped in a string to be decoded first + + if v.Kind() == reflect.Map { + elemType := v.Type().Elem() + if !mapElem.IsValid() { + mapElem = reflect.New(elemType).Elem() + } else { + mapElem.Set(reflect.Zero(elemType)) + } + subv = mapElem + } else { + var f *field + fields := cachedTypeFields(v.Type()) + for i := range fields { + ff := &fields[i] + if bytes.Equal(ff.nameBytes, []byte(key)) { + f = ff + break + } + } + if f != nil { + subv = v + destring = f.quoted + for _, i := range f.index { + if subv.Kind() == reflect.Ptr { + if subv.IsNil() { + subv.Set(reflect.New(subv.Type().Elem())) + } + subv = subv.Elem() + } + subv = subv.Field(i) + } + } + } + + // Read : before value. + if op == scanSkipSpace { + op = d.scanWhile(scanSkipSpace) + } + if op != scanObjectKey { + d.error(errPhase) + } + + // Read value. + if destring { + switch qv := d.valueQuoted().(type) { + case nil: + d.literalStore(nullLiteral, subv, false) + case string: + d.literalStore([]byte(qv), subv, true) + default: + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal unquoted value into %v", subv.Type())) + } + } else { + d.value(subv) + } + + // Write value back to map; + // if using struct, subv points into struct already. + if v.Kind() == reflect.Map { + kv := reflect.ValueOf(key).Convert(v.Type().Key()) + v.SetMapIndex(kv, subv) + } + + // Next token must be , or }. + op = d.scanWhile(scanSkipSpace) + if op == scanEndObject { + break + } + if op != scanObjectValue { + d.error(errPhase) + } + } +} + +// literal consumes a literal from d.data[d.off-1:], decoding into the value v. +// The first byte of the literal has been read already +// (that's how the caller knows it's a literal). +func (d *decodeState) literal(v reflect.Value) { + // All bytes inside literal return scanContinue op code. + start := d.off - 1 + op := d.scanWhile(scanContinue) + + // Scan read one byte too far; back up. + d.off-- + d.scan.undo(op) + + d.literalStore(d.data[start:d.off], v, false) +} + +// convertNumber converts the number literal s to a float64 or a Number +// depending on the setting of d.useNumber. +func (d *decodeState) convertNumber(s string) (interface{}, error) { + if d.useNumber { + return Number(s), nil + } + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return nil, &UnmarshalTypeError{"number " + s, reflect.TypeOf(0.0), int64(d.off)} + } + return f, nil +} + +var numberType = reflect.TypeOf(Number("")) + +// literalStore decodes a literal stored in item into v. +// +// fromQuoted indicates whether this literal came from unwrapping a +// string from the ",string" struct tag option. this is used only to +// produce more helpful error messages. +func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool) { + // Check for unmarshaler. + if len(item) == 0 { + //Empty string given + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + return + } + wantptr := item[0] == 'n' // null + u, ut, pv := d.indirect(v, wantptr) + if u != nil { + err := u.UnmarshalJSON(item) + if err != nil { + d.error(err) + } + return + } + if ut != nil { + if item[0] != '"' { + if fromQuoted { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) + } + return + } + s, ok := unquoteBytes(item) + if !ok { + if fromQuoted { + d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.error(errPhase) + } + } + err := ut.UnmarshalText(s) + if err != nil { + d.error(err) + } + return + } + + v = pv + + switch c := item[0]; c { + case 'n': // null + switch v.Kind() { + case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: + v.Set(reflect.Zero(v.Type())) + // otherwise, ignore null for primitives/string + } + case 't', 'f': // true, false + value := c == 't' + switch v.Kind() { + default: + if fromQuoted { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.saveError(&UnmarshalTypeError{"bool", v.Type(), int64(d.off)}) + } + case reflect.Bool: + v.SetBool(value) + case reflect.Interface: + if v.NumMethod() == 0 { + v.Set(reflect.ValueOf(value)) + } else { + d.saveError(&UnmarshalTypeError{"bool", v.Type(), int64(d.off)}) + } + } + + case '"': // string + s, ok := unquoteBytes(item) + if !ok { + if fromQuoted { + d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.error(errPhase) + } + } + switch v.Kind() { + default: + d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) + case reflect.Slice: + if v.Type().Elem().Kind() != reflect.Uint8 { + d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) + break + } + b := make([]byte, base64.StdEncoding.DecodedLen(len(s))) + n, err := base64.StdEncoding.Decode(b, s) + if err != nil { + d.saveError(err) + break + } + v.SetBytes(b[:n]) + case reflect.String: + v.SetString(string(s)) + case reflect.Interface: + if v.NumMethod() == 0 { + v.Set(reflect.ValueOf(string(s))) + } else { + d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) + } + } + + default: // number + if c != '-' && (c < '0' || c > '9') { + if fromQuoted { + d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.error(errPhase) + } + } + s := string(item) + switch v.Kind() { + default: + if v.Kind() == reflect.String && v.Type() == numberType { + v.SetString(s) + if !isValidNumber(s) { + d.error(fmt.Errorf("json: invalid number literal, trying to unmarshal %q into Number", item)) + } + break + } + if fromQuoted { + d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.error(&UnmarshalTypeError{"number", v.Type(), int64(d.off)}) + } + case reflect.Interface: + n, err := d.convertNumber(s) + if err != nil { + d.saveError(err) + break + } + if v.NumMethod() != 0 { + d.saveError(&UnmarshalTypeError{"number", v.Type(), int64(d.off)}) + break + } + v.Set(reflect.ValueOf(n)) + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + n, err := strconv.ParseInt(s, 10, 64) + if err != nil || v.OverflowInt(n) { + d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) + break + } + v.SetInt(n) + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + n, err := strconv.ParseUint(s, 10, 64) + if err != nil || v.OverflowUint(n) { + d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) + break + } + v.SetUint(n) + + case reflect.Float32, reflect.Float64: + n, err := strconv.ParseFloat(s, v.Type().Bits()) + if err != nil || v.OverflowFloat(n) { + d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) + break + } + v.SetFloat(n) + } + } +} + +// The xxxInterface routines build up a value to be stored +// in an empty interface. They are not strictly necessary, +// but they avoid the weight of reflection in this common case. + +// valueInterface is like value but returns interface{} +func (d *decodeState) valueInterface() interface{} { + switch d.scanWhile(scanSkipSpace) { + default: + d.error(errPhase) + panic("unreachable") + case scanBeginArray: + return d.arrayInterface() + case scanBeginObject: + return d.objectInterface() + case scanBeginLiteral: + return d.literalInterface() + } +} + +// arrayInterface is like array but returns []interface{}. +func (d *decodeState) arrayInterface() []interface{} { + var v = make([]interface{}, 0) + for { + // Look ahead for ] - can only happen on first iteration. + op := d.scanWhile(scanSkipSpace) + if op == scanEndArray { + break + } + + // Back up so d.value can have the byte we just read. + d.off-- + d.scan.undo(op) + + v = append(v, d.valueInterface()) + + // Next token must be , or ]. + op = d.scanWhile(scanSkipSpace) + if op == scanEndArray { + break + } + if op != scanArrayValue { + d.error(errPhase) + } + } + return v +} + +// objectInterface is like object but returns map[string]interface{}. +func (d *decodeState) objectInterface() map[string]interface{} { + m := make(map[string]interface{}) + keys := map[string]bool{} + + for { + // Read opening " of string key or closing }. + op := d.scanWhile(scanSkipSpace) + if op == scanEndObject { + // closing } - can only happen on first iteration. + break + } + if op != scanBeginLiteral { + d.error(errPhase) + } + + // Read string key. + start := d.off - 1 + op = d.scanWhile(scanContinue) + item := d.data[start : d.off-1] + key, ok := unquote(item) + if !ok { + d.error(errPhase) + } + + // Check for duplicate keys. + _, ok = keys[key] + if !ok { + keys[key] = true + } else { + d.error(fmt.Errorf("json: duplicate key '%s' in object", key)) + } + + // Read : before value. + if op == scanSkipSpace { + op = d.scanWhile(scanSkipSpace) + } + if op != scanObjectKey { + d.error(errPhase) + } + + // Read value. + m[key] = d.valueInterface() + + // Next token must be , or }. + op = d.scanWhile(scanSkipSpace) + if op == scanEndObject { + break + } + if op != scanObjectValue { + d.error(errPhase) + } + } + return m +} + +// literalInterface is like literal but returns an interface value. +func (d *decodeState) literalInterface() interface{} { + // All bytes inside literal return scanContinue op code. + start := d.off - 1 + op := d.scanWhile(scanContinue) + + // Scan read one byte too far; back up. + d.off-- + d.scan.undo(op) + item := d.data[start:d.off] + + switch c := item[0]; c { + case 'n': // null + return nil + + case 't', 'f': // true, false + return c == 't' + + case '"': // string + s, ok := unquote(item) + if !ok { + d.error(errPhase) + } + return s + + default: // number + if c != '-' && (c < '0' || c > '9') { + d.error(errPhase) + } + n, err := d.convertNumber(string(item)) + if err != nil { + d.saveError(err) + } + return n + } +} + +// getu4 decodes \uXXXX from the beginning of s, returning the hex value, +// or it returns -1. +func getu4(s []byte) rune { + if len(s) < 6 || s[0] != '\\' || s[1] != 'u' { + return -1 + } + r, err := strconv.ParseUint(string(s[2:6]), 16, 64) + if err != nil { + return -1 + } + return rune(r) +} + +// unquote converts a quoted JSON string literal s into an actual string t. +// The rules are different than for Go, so cannot use strconv.Unquote. +func unquote(s []byte) (t string, ok bool) { + s, ok = unquoteBytes(s) + t = string(s) + return +} + +func unquoteBytes(s []byte) (t []byte, ok bool) { + if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { + return + } + s = s[1 : len(s)-1] + + // Check for unusual characters. If there are none, + // then no unquoting is needed, so return a slice of the + // original bytes. + r := 0 + for r < len(s) { + c := s[r] + if c == '\\' || c == '"' || c < ' ' { + break + } + if c < utf8.RuneSelf { + r++ + continue + } + rr, size := utf8.DecodeRune(s[r:]) + if rr == utf8.RuneError && size == 1 { + break + } + r += size + } + if r == len(s) { + return s, true + } + + b := make([]byte, len(s)+2*utf8.UTFMax) + w := copy(b, s[0:r]) + for r < len(s) { + // Out of room? Can only happen if s is full of + // malformed UTF-8 and we're replacing each + // byte with RuneError. + if w >= len(b)-2*utf8.UTFMax { + nb := make([]byte, (len(b)+utf8.UTFMax)*2) + copy(nb, b[0:w]) + b = nb + } + switch c := s[r]; { + case c == '\\': + r++ + if r >= len(s) { + return + } + switch s[r] { + default: + return + case '"', '\\', '/', '\'': + b[w] = s[r] + r++ + w++ + case 'b': + b[w] = '\b' + r++ + w++ + case 'f': + b[w] = '\f' + r++ + w++ + case 'n': + b[w] = '\n' + r++ + w++ + case 'r': + b[w] = '\r' + r++ + w++ + case 't': + b[w] = '\t' + r++ + w++ + case 'u': + r-- + rr := getu4(s[r:]) + if rr < 0 { + return + } + r += 6 + if utf16.IsSurrogate(rr) { + rr1 := getu4(s[r:]) + if dec := utf16.DecodeRune(rr, rr1); dec != unicode.ReplacementChar { + // A valid pair; consume. + r += 6 + w += utf8.EncodeRune(b[w:], dec) + break + } + // Invalid surrogate; fall back to replacement rune. + rr = unicode.ReplacementChar + } + w += utf8.EncodeRune(b[w:], rr) + } + + // Quote, control characters are invalid. + case c == '"', c < ' ': + return + + // ASCII + case c < utf8.RuneSelf: + b[w] = c + r++ + w++ + + // Coerce to well-formed UTF-8. + default: + rr, size := utf8.DecodeRune(s[r:]) + r += size + w += utf8.EncodeRune(b[w:], rr) + } + } + return b[0:w], true +} diff --git a/vendor/gopkg.in/square/go-jose.v2/json/encode.go b/vendor/gopkg.in/square/go-jose.v2/json/encode.go new file mode 100644 index 000000000..1dae8bb7c --- /dev/null +++ b/vendor/gopkg.in/square/go-jose.v2/json/encode.go @@ -0,0 +1,1197 @@ +// Copyright 2010 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 json implements encoding and decoding of JSON objects as defined in +// RFC 4627. The mapping between JSON objects and Go values is described +// in the documentation for the Marshal and Unmarshal functions. +// +// See "JSON and Go" for an introduction to this package: +// https://golang.org/doc/articles/json_and_go.html +package json + +import ( + "bytes" + "encoding" + "encoding/base64" + "fmt" + "math" + "reflect" + "runtime" + "sort" + "strconv" + "strings" + "sync" + "unicode" + "unicode/utf8" +) + +// Marshal returns the JSON encoding of v. +// +// Marshal traverses the value v recursively. +// If an encountered value implements the Marshaler interface +// and is not a nil pointer, Marshal calls its MarshalJSON method +// to produce JSON. If no MarshalJSON method is present but the +// value implements encoding.TextMarshaler instead, Marshal calls +// its MarshalText method. +// The nil pointer exception is not strictly necessary +// but mimics a similar, necessary exception in the behavior of +// UnmarshalJSON. +// +// Otherwise, Marshal uses the following type-dependent default encodings: +// +// Boolean values encode as JSON booleans. +// +// Floating point, integer, and Number values encode as JSON numbers. +// +// String values encode as JSON strings coerced to valid UTF-8, +// replacing invalid bytes with the Unicode replacement rune. +// The angle brackets "<" and ">" are escaped to "\u003c" and "\u003e" +// to keep some browsers from misinterpreting JSON output as HTML. +// Ampersand "&" is also escaped to "\u0026" for the same reason. +// +// Array and slice values encode as JSON arrays, except that +// []byte encodes as a base64-encoded string, and a nil slice +// encodes as the null JSON object. +// +// Struct values encode as JSON objects. Each exported struct field +// becomes a member of the object unless +// - the field's tag is "-", or +// - the field is empty and its tag specifies the "omitempty" option. +// The empty values are false, 0, any +// nil pointer or interface value, and any array, slice, map, or string of +// length zero. The object's default key string is the struct field name +// but can be specified in the struct field's tag value. The "json" key in +// the struct field's tag value is the key name, followed by an optional comma +// and options. Examples: +// +// // Field is ignored by this package. +// Field int `json:"-"` +// +// // Field appears in JSON as key "myName". +// Field int `json:"myName"` +// +// // Field appears in JSON as key "myName" and +// // the field is omitted from the object if its value is empty, +// // as defined above. +// Field int `json:"myName,omitempty"` +// +// // Field appears in JSON as key "Field" (the default), but +// // the field is skipped if empty. +// // Note the leading comma. +// Field int `json:",omitempty"` +// +// The "string" option signals that a field is stored as JSON inside a +// JSON-encoded string. It applies only to fields of string, floating point, +// integer, or boolean types. This extra level of encoding is sometimes used +// when communicating with JavaScript programs: +// +// Int64String int64 `json:",string"` +// +// The key name will be used if it's a non-empty string consisting of +// only Unicode letters, digits, dollar signs, percent signs, hyphens, +// underscores and slashes. +// +// Anonymous struct fields are usually marshaled as if their inner exported fields +// were fields in the outer struct, subject to the usual Go visibility rules amended +// as described in the next paragraph. +// An anonymous struct field with a name given in its JSON tag is treated as +// having that name, rather than being anonymous. +// An anonymous struct field of interface type is treated the same as having +// that type as its name, rather than being anonymous. +// +// The Go visibility rules for struct fields are amended for JSON when +// deciding which field to marshal or unmarshal. If there are +// multiple fields at the same level, and that level is the least +// nested (and would therefore be the nesting level selected by the +// usual Go rules), the following extra rules apply: +// +// 1) Of those fields, if any are JSON-tagged, only tagged fields are considered, +// even if there are multiple untagged fields that would otherwise conflict. +// 2) If there is exactly one field (tagged or not according to the first rule), that is selected. +// 3) Otherwise there are multiple fields, and all are ignored; no error occurs. +// +// Handling of anonymous struct fields is new in Go 1.1. +// Prior to Go 1.1, anonymous struct fields were ignored. To force ignoring of +// an anonymous struct field in both current and earlier versions, give the field +// a JSON tag of "-". +// +// Map values encode as JSON objects. +// The map's key type must be string; the map keys are used as JSON object +// keys, subject to the UTF-8 coercion described for string values above. +// +// Pointer values encode as the value pointed to. +// A nil pointer encodes as the null JSON object. +// +// Interface values encode as the value contained in the interface. +// A nil interface value encodes as the null JSON object. +// +// Channel, complex, and function values cannot be encoded in JSON. +// Attempting to encode such a value causes Marshal to return +// an UnsupportedTypeError. +// +// JSON cannot represent cyclic data structures and Marshal does not +// handle them. Passing cyclic structures to Marshal will result in +// an infinite recursion. +// +func Marshal(v interface{}) ([]byte, error) { + e := &encodeState{} + err := e.marshal(v) + if err != nil { + return nil, err + } + return e.Bytes(), nil +} + +// MarshalIndent is like Marshal but applies Indent to format the output. +func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) { + b, err := Marshal(v) + if err != nil { + return nil, err + } + var buf bytes.Buffer + err = Indent(&buf, b, prefix, indent) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// HTMLEscape appends to dst the JSON-encoded src with <, >, &, U+2028 and U+2029 +// characters inside string literals changed to \u003c, \u003e, \u0026, \u2028, \u2029 +// so that the JSON will be safe to embed inside HTML