From c97b5a52a13532fe637e26b7bd00ba313fc1c51c Mon Sep 17 00:00:00 2001 From: Jan Broer Date: Wed, 3 Feb 2016 05:03:03 +0100 Subject: [PATCH 01/43] Refactor DNS check * Gets a list of all authoritative nameservers by looking up the NS RRs for the root domain (zone apex) * Verifies that the expected TXT record exists on all nameservers before sending off the challenge to ACME server --- acme/dns_challenge.go | 151 +++++++++++++++++++++++++--------- acme/dns_challenge_route53.go | 2 +- acme/dns_challenge_test.go | 141 ++++++++++++++++++++++++++++++- 3 files changed, 252 insertions(+), 42 deletions(-) diff --git a/acme/dns_challenge.go b/acme/dns_challenge.go index f34fcccc..b0753499 100644 --- a/acme/dns_challenge.go +++ b/acme/dns_challenge.go @@ -6,17 +6,18 @@ import ( "errors" "fmt" "log" + "net" "strings" "time" "github.com/miekg/dns" ) -type preCheckDNSFunc func(domain, fqdn string) bool +type preCheckDNSFunc func(domain, fqdn, value string) error -var preCheckDNS preCheckDNSFunc = checkDNS +var preCheckDNS preCheckDNSFunc = checkDnsPropagation -var preCheckDNSFallbackCount = 5 +var recursionMaxDepth = 10 // DNS01Record returns a DNS record which will fulfill the `dns-01` challenge func DNS01Record(domain, keyAuth string) (fqdn string, value string, ttl int) { @@ -60,50 +61,121 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error { } }() - fqdn, _, _ := DNS01Record(domain, keyAuth) + fqdn, value, _ := DNS01Record(domain, keyAuth) - preCheckDNS(domain, fqdn) + logf("[INFO][%s] Checking DNS record propagation...", domain) + + if err = preCheckDNS(domain, fqdn, value); err != nil { + return err + } return s.validate(s.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) } -func checkDNS(domain, fqdn string) bool { - // check if the expected DNS entry was created. If not wait for some time and try again. - m := new(dns.Msg) - m.SetQuestion(domain+".", dns.TypeSOA) - c := new(dns.Client) - in, _, err := c.Exchange(m, "google-public-dns-a.google.com:53") +// checkDnsPropagation checks if the expected TXT record has been propagated to +// all authoritative nameservers. If not it waits and retries for some time. +func checkDnsPropagation(domain, fqdn, value string) error { + authoritativeNss, err := lookupNameservers(toFqdn(domain)) if err != nil { - return false + return err } - var authorativeNS string - for _, answ := range in.Answer { - soa := answ.(*dns.SOA) - authorativeNS = soa.Ns + if err = waitFor(30, 2, func() (bool, error) { + return checkAuthoritativeNss(fqdn, value, authoritativeNss) + }); err != nil { + return err } - fallbackCnt := 0 - for fallbackCnt < preCheckDNSFallbackCount { - m.SetQuestion(fqdn, dns.TypeTXT) - in, _, err = c.Exchange(m, authorativeNS+":53") + return nil +} + +// 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, ns, false) if err != nil { - return false + return false, err } - if len(in.Answer) > 0 { - return true + if r.Rcode != dns.RcodeSuccess { + return false, fmt.Errorf("%s returned RCode %s", ns, dns.RcodeToString[r.Rcode]) } - fallbackCnt++ - if fallbackCnt >= preCheckDNSFallbackCount { - return false + var found bool + for _, rr := range r.Answer { + if txt, ok := rr.(*dns.TXT); ok { + if strings.Join(txt.Txt, "") == value { + found = true + break + } + } } - time.Sleep(time.Second * time.Duration(fallbackCnt)) + if !found { + return false, fmt.Errorf("%s did not return the expected TXT record", ns) + } } - return false + return true, nil +} + +// dnsQuery sends a DNS query to the given nameserver. +func dnsQuery(fqdn string, rtype uint16, nameserver 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 + } + + in, err = dns.Exchange(m, net.JoinHostPort(nameserver, "53")) + if err == dns.ErrTruncated { + tcp := &dns.Client{Net: "tcp"} + in, _, err = tcp.Exchange(m, nameserver) + } + + return +} + +// lookupNameservers returns the authoritative nameservers for the given domain name. +func lookupNameservers(fqdn string) ([]string, error) { + var err error + var r *dns.Msg + var authoritativeNss []string + resolver := "google-public-dns-a.google.com" + + r, err = dnsQuery(fqdn, dns.TypeSOA, resolver, true) + if err != nil { + return nil, err + } + + // If there is a SOA RR in the Answer section then fqdn is the root domain. + for _, rr := range r.Answer { + if soa, ok := rr.(*dns.SOA); ok { + r, err = dnsQuery(soa.Hdr.Name, dns.TypeNS, resolver, 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)) + } + } + + return authoritativeNss, nil + } + } + + // Strip of the left most label to get the parent domain. + offset, _ := dns.NextLabel(fqdn, 0) + next := fqdn[offset:] + // Only the TLD label left. This should not happen if the domain DNS is healthy. + if dns.CountLabel(next) < 2 { + return nil, fmt.Errorf("Could not determine root domain") + } + + return lookupNameservers(fqdn[offset:]) } // toFqdn converts the name into a fqdn appending a trailing dot. @@ -124,22 +196,25 @@ func unFqdn(name string) string { return name } -// waitFor polls the given function 'f', once per second, up to 'timeout' seconds. -func waitFor(timeout int, f func() (bool, error)) error { - start := time.Now().Second() +// waitFor polls the given function 'f', once every 'interval' seconds, up to 'timeout' seconds. +func waitFor(timeout, interval int, f func() (bool, error)) error { + var lastErr string + timeup := time.After(time.Duration(timeout) * time.Second) for { - time.Sleep(1 * time.Second) - - if delta := time.Now().Second() - start; delta >= timeout { - return fmt.Errorf("Time limit exceeded (%d seconds)", delta) + select { + case <-timeup: + return fmt.Errorf("Time limit exceeded. Last error: %s", lastErr) + default: } stop, err := f() - if err != nil { - return err - } if stop { return nil } + if err != nil { + lastErr = err.Error() + } + + time.Sleep(time.Duration(interval) * time.Second) } } diff --git a/acme/dns_challenge_route53.go b/acme/dns_challenge_route53.go index 28ed0259..491117e2 100644 --- a/acme/dns_challenge_route53.go +++ b/acme/dns_challenge_route53.go @@ -68,7 +68,7 @@ func (r *DNSProviderRoute53) changeRecord(action, fqdn, value string, ttl int) e return err } - return waitFor(90, func() (bool, error) { + return waitFor(90, 5, func() (bool, error) { status, err := r.client.GetChange(resp.ChangeInfo.ID) if err != nil { return false, err diff --git a/acme/dns_challenge_test.go b/acme/dns_challenge_test.go index 0af40f71..046f792a 100644 --- a/acme/dns_challenge_test.go +++ b/acme/dns_challenge_test.go @@ -6,13 +6,75 @@ import ( "net/http" "net/http/httptest" "os" + "reflect" + "sort" + "strings" "testing" "time" ) +var lookupNameserversTestsOK = []struct { + fqdn string + nss []string +}{ + {"books.google.com.ng.", + []string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."}, + }, + {"www.google.com.", + []string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."}, + }, + {"physics.georgetown.edu.", + []string{"ns1.georgetown.edu.", "ns2.georgetown.edu.", "ns3.georgetown.edu."}, + }, +} + +var lookupNameserversTestsErr = []struct { + fqdn string + error string +}{ + // invalid tld + {"_null.n0n0.", + "Could not determine root domain", + }, + // invalid domain + {"_null.com.", + "Could not determine root domain", + }, +} + +var checkAuthoritativeNssTests = []struct { + fqdn, value string + ns []string + ok bool +}{ + // TXT RR w/ expected value + {"8.8.8.8.asn.routeviews.org.", "151698.8.8.024", []string{"asnums.routeviews.org."}, + true, + }, + // No TXT RR + {"ns1.google.com.", "", []string{"ns2.google.com."}, + false, + }, +} + +var checkAuthoritativeNssTestsErr = []struct { + fqdn, value string + ns []string + error string +}{ + // TXT RR /w unexpected value + {"8.8.8.8.asn.routeviews.org.", "fe01=", []string{"asnums.routeviews.org."}, + "did not return the expected TXT record", + }, + // No TXT RR + {"ns1.google.com.", "fe01=", []string{"ns2.google.com."}, + "did not return the expected TXT record", + }, +} + func TestDNSValidServerResponse(t *testing.T) { - preCheckDNS = func(domain, fqdn string) bool { - return true + preCheckDNS = func(domain, fqdn, value string) error { + return nil } privKey, _ := generatePrivateKey(rsakey, 512) @@ -39,7 +101,80 @@ func TestDNSValidServerResponse(t *testing.T) { } func TestPreCheckDNS(t *testing.T) { - if !preCheckDNS("api.letsencrypt.org", "acme-staging.api.letsencrypt.org") { + err := preCheckDNS("api.letsencrypt.org", "acme-staging.api.letsencrypt.org", "fe01=") + if err != nil { t.Errorf("preCheckDNS failed for acme-staging.api.letsencrypt.org") } } + +func TestLookupNameserversOK(t *testing.T) { + for _, tt := range lookupNameserversTestsOK { + nss, err := lookupNameservers(tt.fqdn) + if err != nil { + t.Fatalf("#%s: got %q; want nil", tt.fqdn, err) + } + + sort.Strings(nss) + sort.Strings(tt.nss) + + if !reflect.DeepEqual(nss, tt.nss) { + t.Errorf("#%s: got %v; want %v", tt.fqdn, nss, tt.nss) + } + } +} + +func TestLookupNameserversErr(t *testing.T) { + for _, tt := range lookupNameserversTestsErr { + _, err := lookupNameservers(tt.fqdn) + if err == nil { + t.Fatalf("#%s: expected %q (error); got ", tt.fqdn, tt.error) + } + + if !strings.Contains(err.Error(), tt.error) { + t.Errorf("#%s: expected %q (error); got %q", tt.fqdn, tt.error, err) + continue + } + } +} + +func TestCheckAuthoritativeNss(t *testing.T) { + for _, tt := range checkAuthoritativeNssTests { + ok, _ := checkAuthoritativeNss(tt.fqdn, tt.value, tt.ns) + if ok != tt.ok { + t.Errorf("#%s: got %t; want %t", tt.fqdn, tt.ok) + } + } +} + +func TestCheckAuthoritativeNssErr(t *testing.T) { + for _, tt := range checkAuthoritativeNssTestsErr { + _, err := checkAuthoritativeNss(tt.fqdn, tt.value, tt.ns) + if err == nil { + t.Fatalf("#%s: expected %q (error); got ", tt.fqdn, tt.error) + } + if !strings.Contains(err.Error(), tt.error) { + t.Errorf("#%s: expected %q (error); got %q", tt.fqdn, tt.error, err) + continue + } + } +} + +func TestWaitForTimeout(t *testing.T) { + c := make(chan error) + go func() { + err := waitFor(3, 1, func() (bool, error) { + return false, nil + }) + c <- err + }() + + timeout := time.After(4 * time.Second) + select { + case <-timeout: + t.Fatal("timeout exceeded") + case err := <-c: + if err == nil { + t.Errorf("expected timeout error; got ", err) + } + } +} From b594acbc2a17f1643568bb62d2ccf8250e444219 Mon Sep 17 00:00:00 2001 From: Jan Broer Date: Wed, 10 Feb 2016 15:52:53 +0100 Subject: [PATCH 02/43] Validation domain may be a CNAME or delegated to another NS --- acme/dns_challenge.go | 80 ++++++++++++++++++++------------------ acme/dns_challenge_test.go | 12 +++--- 2 files changed, 48 insertions(+), 44 deletions(-) diff --git a/acme/dns_challenge.go b/acme/dns_challenge.go index b0753499..ebbf1d9d 100644 --- a/acme/dns_challenge.go +++ b/acme/dns_challenge.go @@ -13,11 +13,11 @@ import ( "github.com/miekg/dns" ) -type preCheckDNSFunc func(domain, fqdn, value string) error +type preCheckDNSFunc func(fqdn, value string) (bool, error) var preCheckDNS preCheckDNSFunc = checkDnsPropagation -var recursionMaxDepth = 10 +var recursiveNameserver = "google-public-dns-a.google.com" // DNS01Record returns a DNS record which will fulfill the `dns-01` challenge func DNS01Record(domain, keyAuth string) (fqdn string, value string, ttl int) { @@ -65,28 +65,43 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error { logf("[INFO][%s] Checking DNS record propagation...", domain) - if err = preCheckDNS(domain, fqdn, value); err != nil { + err = waitFor(30, 2, func() (bool, error) { + return preCheckDNS(fqdn, value) + }) + if err != nil { return err } return s.validate(s.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) } -// checkDnsPropagation checks if the expected TXT record has been propagated to -// all authoritative nameservers. If not it waits and retries for some time. -func checkDnsPropagation(domain, fqdn, value string) error { - authoritativeNss, err := lookupNameservers(toFqdn(domain)) +// 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, recursiveNameserver, true) if err != nil { - return err + return false, err + } + if r.Rcode != dns.RcodeSuccess { + return false, fmt.Errorf("Could not resolve %s -> %s", fqdn, dns.RcodeToString[r.Rcode]) } - if err = waitFor(30, 2, func() (bool, error) { - return checkAuthoritativeNss(fqdn, value, authoritativeNss) - }); err != nil { - return err + // 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 + } + } } - return nil + 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. @@ -98,7 +113,7 @@ func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, erro } if r.Rcode != dns.RcodeSuccess { - return false, fmt.Errorf("%s returned RCode %s", ns, dns.RcodeToString[r.Rcode]) + return false, fmt.Errorf("NS %s returned %s for %s", ns, dns.RcodeToString[r.Rcode], fqdn) } var found bool @@ -112,7 +127,7 @@ func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, erro } if !found { - return false, fmt.Errorf("%s did not return the expected TXT record", ns) + return false, fmt.Errorf("NS %s did not return the expected TXT record", ns) } } @@ -124,6 +139,7 @@ func dnsQuery(fqdn string, rtype uint16, nameserver string, recursive bool) (in m := new(dns.Msg) m.SetQuestion(fqdn, rtype) m.SetEdns0(4096, false) + if !recursive { m.RecursionDesired = false } @@ -137,45 +153,33 @@ func dnsQuery(fqdn string, rtype uint16, nameserver string, recursive bool) (in return } -// lookupNameservers returns the authoritative nameservers for the given domain name. +// lookupNameservers returns the authoritative nameservers for the given fqdn. func lookupNameservers(fqdn string) ([]string, error) { - var err error - var r *dns.Msg var authoritativeNss []string - resolver := "google-public-dns-a.google.com" - r, err = dnsQuery(fqdn, dns.TypeSOA, resolver, true) + r, err := dnsQuery(fqdn, dns.TypeNS, recursiveNameserver, true) if err != nil { return nil, err } - // If there is a SOA RR in the Answer section then fqdn is the root domain. for _, rr := range r.Answer { - if soa, ok := rr.(*dns.SOA); ok { - r, err = dnsQuery(soa.Hdr.Name, dns.TypeNS, resolver, 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)) - } - } - - return authoritativeNss, nil + if ns, ok := rr.(*dns.NS); ok { + authoritativeNss = append(authoritativeNss, strings.ToLower(ns.Ns)) } } + if len(authoritativeNss) > 0 { + return authoritativeNss, nil + } + // Strip of the left most label to get the parent domain. offset, _ := dns.NextLabel(fqdn, 0) next := fqdn[offset:] - // Only the TLD label left. This should not happen if the domain DNS is healthy. if dns.CountLabel(next) < 2 { - return nil, fmt.Errorf("Could not determine root domain") + return nil, fmt.Errorf("Could not determine authoritative nameservers") } - - return lookupNameservers(fqdn[offset:]) + + return lookupNameservers(next) } // toFqdn converts the name into a fqdn appending a trailing dot. diff --git a/acme/dns_challenge_test.go b/acme/dns_challenge_test.go index 046f792a..031fa555 100644 --- a/acme/dns_challenge_test.go +++ b/acme/dns_challenge_test.go @@ -34,11 +34,11 @@ var lookupNameserversTestsErr = []struct { }{ // invalid tld {"_null.n0n0.", - "Could not determine root domain", + "Could not determine authoritative nameservers", }, // invalid domain {"_null.com.", - "Could not determine root domain", + "Could not determine authoritative nameservers", }, } @@ -73,8 +73,8 @@ var checkAuthoritativeNssTestsErr = []struct { } func TestDNSValidServerResponse(t *testing.T) { - preCheckDNS = func(domain, fqdn, value string) error { - return nil + preCheckDNS = func(fqdn, value string) (bool, error) { + return true, nil } privKey, _ := generatePrivateKey(rsakey, 512) @@ -101,8 +101,8 @@ func TestDNSValidServerResponse(t *testing.T) { } func TestPreCheckDNS(t *testing.T) { - err := preCheckDNS("api.letsencrypt.org", "acme-staging.api.letsencrypt.org", "fe01=") - if err != nil { + ok, err := preCheckDNS("acme-staging.api.letsencrypt.org", "fe01=") + if err != nil || !ok { t.Errorf("preCheckDNS failed for acme-staging.api.letsencrypt.org") } } From f18ec353f1b625ba573320fab05d4d46adb6970f Mon Sep 17 00:00:00 2001 From: xenolf Date: Fri, 12 Feb 2016 18:42:43 +0100 Subject: [PATCH 03/43] Add CONTRIBUTING.md --- CONTRIBUTING.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..9939a5ab --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# How to contribute to lego + +Contributions in the form of patches and proposals are essential to keep lego great and to make it even better. +To ensure a great and easy experience for everyone, please review the few guidelines in this document. + +## Bug reports + +- Use the issue search to see if the issue has already been reported. +- Also look for closed issues to see if your issue has already been fixed. +- If both of the above do not apply create a new issue and include as much information as possible. + +Bug reports should include all information a person could need to reproduce your problem without the need to +follow up for more information. If possible, provide detailed steps for us to reproduce it, the expected behaviour and the actual behaviour. + +## Feature proposals and requests + +Feature requests are welcome and should be discussed in an issue. +Please keep proposals focused on one thing at a time and be as detailed as possible. +It is up to you to make a strong point about your proposal and convince us of the merits and the added complexity of this feature. + +## Pull requests + +Patches, new features and improvements are a great way to help the project. +Please keep them focused on one thing and do not include unrelated commits. + +All pull requests which alter the behaviour of the program, add new behaviour or somehow alter code in a non-trivial way should **always** include tests. + +If you want to contribute a significant pull request (with a non-trivial workload for you) please **ask first**. We do not want you to spend +a lot of time on something the project's developers might not want to merge into the project. + +**IMPORTANT**: By submitting a patch, you agree to allow the project +owners to license your work under the terms of the [MIT License](LICENSE). From b3d25a9a61d09db2fb6be8c623c2dc09b3b1321e Mon Sep 17 00:00:00 2001 From: Philipp Kern Date: Sun, 7 Feb 2016 00:09:43 +0100 Subject: [PATCH 04/43] Allow to specify the TSIG algorithm for RFC2136 DNS-01 authentication. Add a new environment variable RFC2136_TSIG_ALGORITHM that accepts the TSIG algorithm pseudo-domain name. Let it default to "hmac-md5.sig-alg.reg.int." if unset. --- acme/dns_challenge_rfc2136.go | 24 +++++++++++++++--------- acme/dns_challenge_rfc2136_test.go | 8 ++++---- cli_handlers.go | 3 ++- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/acme/dns_challenge_rfc2136.go b/acme/dns_challenge_rfc2136.go index e6f93fa4..35f983e9 100644 --- a/acme/dns_challenge_rfc2136.go +++ b/acme/dns_challenge_rfc2136.go @@ -2,30 +2,36 @@ package acme import ( "fmt" - "github.com/miekg/dns" "time" + + "github.com/miekg/dns" ) // DNSProviderRFC2136 is an implementation of the ChallengeProvider interface that // uses dynamic DNS updates (RFC 2136) to create TXT records on a nameserver. type DNSProviderRFC2136 struct { - nameserver string - zone string - tsigKey string - tsigSecret string - records map[string]string + nameserver string + zone string + tsigAlgorithm string + tsigKey string + tsigSecret string + records map[string]string } // NewDNSProviderRFC2136 returns a new DNSProviderRFC2136 instance. -// To disable TSIG authentication 'tsigKey' and 'tsigSecret' must be set to the empty string. +// To disable TSIG authentication 'tsigAlgorithm, 'tsigKey' and 'tsigSecret' must be set to the empty string. // 'nameserver' must be a network address in the the form "host:port". 'zone' must be the fully // qualified name of the zone. -func NewDNSProviderRFC2136(nameserver, zone, tsigKey, tsigSecret string) (*DNSProviderRFC2136, error) { +func NewDNSProviderRFC2136(nameserver, zone, tsigAlgorithm, tsigKey, tsigSecret string) (*DNSProviderRFC2136, error) { d := &DNSProviderRFC2136{ nameserver: nameserver, zone: zone, records: make(map[string]string), } + if tsigAlgorithm == "" { + tsigAlgorithm = dns.HmacMD5 + } + d.tsigAlgorithm = tsigAlgorithm if len(tsigKey) > 0 && len(tsigSecret) > 0 { d.tsigKey = tsigKey d.tsigSecret = tsigSecret @@ -73,7 +79,7 @@ func (r *DNSProviderRFC2136) changeRecord(action, fqdn, value string, ttl int) e c.SingleInflight = true // TSIG authentication / msg signing if len(r.tsigKey) > 0 && len(r.tsigSecret) > 0 { - m.SetTsig(dns.Fqdn(r.tsigKey), dns.HmacMD5, 300, time.Now().Unix()) + m.SetTsig(dns.Fqdn(r.tsigKey), r.tsigAlgorithm, 300, time.Now().Unix()) c.TsigSecret = map[string]string{dns.Fqdn(r.tsigKey): r.tsigSecret} } diff --git a/acme/dns_challenge_rfc2136_test.go b/acme/dns_challenge_rfc2136_test.go index f9fc5dea..9af8f491 100644 --- a/acme/dns_challenge_rfc2136_test.go +++ b/acme/dns_challenge_rfc2136_test.go @@ -57,7 +57,7 @@ func TestRFC2136ServerSuccess(t *testing.T) { } defer server.Shutdown() - provider, err := NewDNSProviderRFC2136(addrstr, rfc2136TestZone, "", "") + provider, err := NewDNSProviderRFC2136(addrstr, rfc2136TestZone, "", "", "") if err != nil { t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err) } @@ -76,7 +76,7 @@ func TestRFC2136ServerError(t *testing.T) { } defer server.Shutdown() - provider, err := NewDNSProviderRFC2136(addrstr, rfc2136TestZone, "", "") + provider, err := NewDNSProviderRFC2136(addrstr, rfc2136TestZone, "", "", "") if err != nil { t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err) } @@ -97,7 +97,7 @@ func TestRFC2136TsigClient(t *testing.T) { } defer server.Shutdown() - provider, err := NewDNSProviderRFC2136(addrstr, rfc2136TestZone, rfc2136TestTsigKey, rfc2136TestTsigSecret) + provider, err := NewDNSProviderRFC2136(addrstr, rfc2136TestZone, "", rfc2136TestTsigKey, rfc2136TestTsigSecret) if err != nil { t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err) } @@ -135,7 +135,7 @@ func TestRFC2136ValidUpdatePacket(t *testing.T) { t.Fatalf("Error packing expect msg: %v", err) } - provider, err := NewDNSProviderRFC2136(addrstr, rfc2136TestZone, "", "") + provider, err := NewDNSProviderRFC2136(addrstr, rfc2136TestZone, "", "", "") if err != nil { t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err) } diff --git a/cli_handlers.go b/cli_handlers.go index 2b5b3fc0..42e3fece 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -69,10 +69,11 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { case "rfc2136": nameserver := os.Getenv("RFC2136_NAMESERVER") zone := os.Getenv("RFC2136_ZONE") + tsigAlgorithm := os.Getenv("RFC2136_TSIG_ALGORITHM") tsigKey := os.Getenv("RFC2136_TSIG_KEY") tsigSecret := os.Getenv("RFC2136_TSIG_SECRET") - provider, err = acme.NewDNSProviderRFC2136(nameserver, zone, tsigKey, tsigSecret) + provider, err = acme.NewDNSProviderRFC2136(nameserver, zone, tsigAlgorithm, tsigKey, tsigSecret) case "manual": provider, err = acme.NewDNSProviderManual() } From f00f09f19cf226e33c68fb2b12d59b77f8f9d2fc Mon Sep 17 00:00:00 2001 From: Philipp Kern Date: Sun, 7 Feb 2016 00:12:48 +0100 Subject: [PATCH 05/43] Allow to specify RFC2136_NAMESERVER without the port. Append the default DNS port if the nameserver specification does not contain any. --- acme/dns_challenge_rfc2136.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/acme/dns_challenge_rfc2136.go b/acme/dns_challenge_rfc2136.go index 35f983e9..e2954d25 100644 --- a/acme/dns_challenge_rfc2136.go +++ b/acme/dns_challenge_rfc2136.go @@ -2,6 +2,7 @@ package acme import ( "fmt" + "strings" "time" "github.com/miekg/dns" @@ -20,9 +21,13 @@ type DNSProviderRFC2136 struct { // NewDNSProviderRFC2136 returns a new DNSProviderRFC2136 instance. // To disable TSIG authentication 'tsigAlgorithm, 'tsigKey' and 'tsigSecret' must be set to the empty string. -// 'nameserver' must be a network address in the the form "host:port". 'zone' must be the fully +// 'nameserver' must be a network address in the the form "host" or "host:port". 'zone' must be the fully // qualified name of the zone. func NewDNSProviderRFC2136(nameserver, zone, tsigAlgorithm, tsigKey, tsigSecret string) (*DNSProviderRFC2136, error) { + // Append the default DNS port if none is specified. + if !strings.Contains(nameserver, ":") { + nameserver += ":53" + } d := &DNSProviderRFC2136{ nameserver: nameserver, zone: zone, From bf66ac9e173ceff534562c29dbfb23b955bcb274 Mon Sep 17 00:00:00 2001 From: xenolf Date: Sun, 14 Feb 2016 00:55:03 +0100 Subject: [PATCH 06/43] Resolve issue where the route53 tests would take 30secs to complete. The default AWS HTTP client retries three times with a deadline of 10 seconds in order to fetch metadata from EC2. Replaced the default HTTP client with one that does not retry and has a low timeout. --- acme/dns_challenge_route53_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/acme/dns_challenge_route53_test.go b/acme/dns_challenge_route53_test.go index 20eb008b..e57fdaa7 100644 --- a/acme/dns_challenge_route53_test.go +++ b/acme/dns_challenge_route53_test.go @@ -1,8 +1,10 @@ package acme import ( + "net/http" "os" "testing" + "time" "github.com/mitchellh/goamz/aws" "github.com/mitchellh/goamz/route53" @@ -116,9 +118,18 @@ func TestNewDNSProviderRoute53MissingAuthErr(t *testing.T) { os.Setenv("AWS_SECRET_ACCESS_KEY", "") os.Setenv("AWS_CREDENTIAL_FILE", "") // in case test machine has this variable set os.Setenv("HOME", "/") // in case test machine has ~/.aws/credentials + + // The default AWS HTTP client retries three times with a deadline of 10 seconds. + // Replace the default HTTP client with one that does not retry and has a low timeout. + awsClient := aws.RetryingClient + aws.RetryingClient = &http.Client{Timeout: time.Millisecond} + _, err := NewDNSProviderRoute53("", "", "us-east-1") assert.EqualError(t, err, "No valid AWS authentication found") restoreRoute53Env() + + // restore default AWS HTTP client + aws.RetryingClient = awsClient } func TestNewDNSProviderRoute53InvalidRegionErr(t *testing.T) { From 48cf387dd56a3b48c14b599ddcdd3352a55e06f3 Mon Sep 17 00:00:00 2001 From: xenolf Date: Sun, 14 Feb 2016 01:03:40 +0100 Subject: [PATCH 07/43] Run tests with multiple versions of go. --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index 4f2ee4d9..6150083b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1 +1,6 @@ language: go + +go: + - 1.4.3 + - 1.5.3 + - tip \ No newline at end of file From fdc05d2942ec613a3ea588d69bfc65fbc0dade55 Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Thu, 11 Feb 2016 19:47:47 -0600 Subject: [PATCH 08/43] --dns=foo means we specifically intend to fulfill a DNS challenge --- cli_handlers.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cli_handlers.go b/cli_handlers.go index 42e3fece..fb7e54bb 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -83,6 +83,10 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { } client.SetChallengeProvider(acme.DNS01, provider) + + // --dns=foo indicates that the user specifically want to do a DNS challenge + // infer that the user also wants to exclude all other challenges + client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01}) } return conf, acc, client From 030ba6877aed2866a2ca893db75c2b431f57016c Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Sat, 13 Feb 2016 18:23:50 -0600 Subject: [PATCH 09/43] Document that --dns=provider specifically selects the DNS challenge --- README.md | 6 ++++-- cli.go | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 00ce417b..317179d6 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ GLOBAL OPTIONS: --exclude, -x [--exclude option --exclude option] Explicitly disallow solvers by name from being used. Solvers: "http-01", "tls-sni-01". --http Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port --tls Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port - --dns Enable the DNS challenge for solving using a provider. + --dns Solve a DNS challenge using the specified provider. Credentials for providers have to be passed through environment variables. For a more detailed explanation of the parameters, please see the online docs. Valid providers: @@ -125,9 +125,11 @@ $ lego --email="foo@bar.com" --domains="example.com" renew Obtain a certificate using the DNS challenge and AWS Route 53: ```bash -$ AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=my_id AWS_SECRET_ACCESS_KEY=my_key lego --email="foo@bar.com" --domains="example.com" --dns="route53" --exclude="http-01" --exclude="tls-sni-01" run +$ AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=my_id AWS_SECRET_ACCESS_KEY=my_key lego --email="foo@bar.com" --domains="example.com" --dns="route53" run ``` +Note that `--dns=foo` implies `--exclude=http-01` and `--exclude=tls-sni-01`. lego will not attempt other challenges if you've told it to use DNS instead. + lego defaults to communicating with the production Let's Encrypt ACME server. If you'd like to test something without issuing real certificates, consider using the staging endpoint instead: ```bash diff --git a/cli.go b/cli.go index 3851c455..15c4a2f0 100644 --- a/cli.go +++ b/cli.go @@ -112,7 +112,7 @@ func main() { }, cli.StringFlag{ Name: "dns", - Usage: "Enable the DNS challenge for solving using a provider." + + Usage: "Solve a DNS challenge using the specified provider." + "\n\tCredentials for providers have to be passed through environment variables." + "\n\tFor a more detailed explanation of the parameters, please see the online docs." + "\n\tValid providers:" + From 3bceed427a63ab1789d7aa87e0d2360bc14da8c6 Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Sat, 13 Feb 2016 18:42:47 -0600 Subject: [PATCH 10/43] Make the --dns help message more explicit about disabling challenges --- README.md | 2 +- cli.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 317179d6..9d1807aa 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ GLOBAL OPTIONS: --exclude, -x [--exclude option --exclude option] Explicitly disallow solvers by name from being used. Solvers: "http-01", "tls-sni-01". --http Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port --tls Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port - --dns Solve a DNS challenge using the specified provider. + --dns Solve a DNS challenge using the specified provider. Disables all other solvers. Credentials for providers have to be passed through environment variables. For a more detailed explanation of the parameters, please see the online docs. Valid providers: diff --git a/cli.go b/cli.go index 15c4a2f0..ba216d07 100644 --- a/cli.go +++ b/cli.go @@ -112,7 +112,7 @@ func main() { }, cli.StringFlag{ Name: "dns", - Usage: "Solve a DNS challenge using the specified provider." + + Usage: "Solve a DNS challenge using the specified provider. Disables all other challenges." + "\n\tCredentials for providers have to be passed through environment variables." + "\n\tFor a more detailed explanation of the parameters, please see the online docs." + "\n\tValid providers:" + From 7475e7f9c25194fa6d874b6d351fcd3156aba7bd Mon Sep 17 00:00:00 2001 From: xenolf Date: Sun, 14 Feb 2016 16:56:14 +0100 Subject: [PATCH 11/43] Move the HTTP-01 and TLS-SNI-01 default solvers to a more unified layout. Made the solvers exported and added New... functions to them. --- acme/client.go | 4 ++-- acme/http_challenge_server.go | 22 ++++++++++++++++------ acme/tls_sni_challenge_server.go | 18 ++++++++++++++---- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/acme/client.go b/acme/client.go index 700aeab5..3851a69f 100644 --- a/acme/client.go +++ b/acme/client.go @@ -126,7 +126,7 @@ func (c *Client) SetHTTPAddress(iface string) error { } if chlng, ok := c.solvers[HTTP01]; ok { - chlng.(*httpChallenge).provider = &httpChallengeServer{iface: host, port: port} + chlng.(*httpChallenge).provider = NewHTTPProviderServer(host, port) } return nil @@ -142,7 +142,7 @@ func (c *Client) SetTLSAddress(iface string) error { } if chlng, ok := c.solvers[TLSSNI01]; ok { - chlng.(*tlsSNIChallenge).provider = &tlsSNIChallengeServer{iface: host, port: port} + chlng.(*tlsSNIChallenge).provider = NewTLSProviderServer(host, port) } return nil } diff --git a/acme/http_challenge_server.go b/acme/http_challenge_server.go index 33882236..42541380 100644 --- a/acme/http_challenge_server.go +++ b/acme/http_challenge_server.go @@ -7,16 +7,25 @@ import ( "strings" ) -// httpChallengeServer implements ChallengeProvider for `http-01` challenge -type httpChallengeServer struct { +// 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 } -// Present makes the token available at `HTTP01ChallengePath(token)` -func (s *httpChallengeServer) Present(domain, token, keyAuth string) error { +// 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" } @@ -32,7 +41,8 @@ func (s *httpChallengeServer) Present(domain, token, keyAuth string) error { return nil } -func (s *httpChallengeServer) CleanUp(domain, token, keyAuth string) error { +// 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 } @@ -41,7 +51,7 @@ func (s *httpChallengeServer) CleanUp(domain, token, keyAuth string) error { return nil } -func (s *httpChallengeServer) serve(domain, token, keyAuth string) { +func (s *HTTPProviderServer) serve(domain, token, keyAuth string) { path := HTTP01ChallengePath(token) // The handler validates the HOST header and request type. diff --git a/acme/tls_sni_challenge_server.go b/acme/tls_sni_challenge_server.go index 13749632..faaf16f6 100644 --- a/acme/tls_sni_challenge_server.go +++ b/acme/tls_sni_challenge_server.go @@ -7,16 +7,25 @@ import ( "net/http" ) -// tlsSNIChallengeServer implements ChallengeProvider for `TLS-SNI-01` challenge -type tlsSNIChallengeServer struct { +// TLSProviderServer implements ChallengeProvider for `TLS-SNI-01` challenge +// It may be instantiated without using the NewTLSProviderServer function if +// you want only to use the default values. +type TLSProviderServer struct { iface string port string done chan bool listener net.Listener } +// NewTLSProviderServer creates a new TLSProviderServer 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 443 respectively. +func NewTLSProviderServer(iface, port string) *TLSProviderServer { + return &TLSProviderServer{iface: iface, port: port} +} + // Present makes the keyAuth available as a cert -func (s *tlsSNIChallengeServer) Present(domain, token, keyAuth string) error { +func (s *TLSProviderServer) Present(domain, token, keyAuth string) error { if s.port == "" { s.port = "443" } @@ -42,7 +51,8 @@ func (s *tlsSNIChallengeServer) Present(domain, token, keyAuth string) error { return nil } -func (s *tlsSNIChallengeServer) CleanUp(domain, token, keyAuth string) error { +// CleanUp closes the HTTP server. +func (s *TLSProviderServer) CleanUp(domain, token, keyAuth string) error { if s.listener == nil { return nil } From 21de29e9022d4aa4965ae66bdf0cc9b49a5ae403 Mon Sep 17 00:00:00 2001 From: xenolf Date: Sun, 14 Feb 2016 16:57:06 +0100 Subject: [PATCH 12/43] Take the magic out of defaulting to the Server implementations of HTTP-01 and TLS-SNI-01 --- acme/client.go | 4 ++-- acme/http_challenge.go | 4 ---- acme/tls_sni_challenge.go | 4 ---- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/acme/client.go b/acme/client.go index 3851a69f..35dbcd64 100644 --- a/acme/client.go +++ b/acme/client.go @@ -95,8 +95,8 @@ func NewClient(caDirURL string, user User, keyBits int) (*Client, error) { // 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} - solvers[TLSSNI01] = &tlsSNIChallenge{jws: jws, validate: validate} + solvers[HTTP01] = &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}} + solvers[TLSSNI01] = &tlsSNIChallenge{jws: jws, validate: validate, provider: &TLSProviderServer{}} return &Client{directory: dir, user: user, jws: jws, keyBits: keyBits, solvers: solvers}, nil } diff --git a/acme/http_challenge.go b/acme/http_challenge.go index a9f8e5cf..1cc1f6e1 100644 --- a/acme/http_challenge.go +++ b/acme/http_challenge.go @@ -26,10 +26,6 @@ func (s *httpChallenge) Solve(chlng challenge, domain string) error { return err } - if s.provider == nil { - s.provider = &httpChallengeServer{} - } - err = s.provider.Present(domain, chlng.Token, keyAuth) if err != nil { return fmt.Errorf("[%s] error presenting token: %v", domain, err) diff --git a/acme/tls_sni_challenge.go b/acme/tls_sni_challenge.go index 2ab3abd0..dca886bd 100644 --- a/acme/tls_sni_challenge.go +++ b/acme/tls_sni_challenge.go @@ -27,10 +27,6 @@ func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error { return err } - if t.provider == nil { - t.provider = &tlsSNIChallengeServer{} - } - err = t.provider.Present(domain, chlng.Token, keyAuth) if err != nil { return fmt.Errorf("[%s] error presenting token: %v", domain, err) From a44384f52f9cbc2adcbf5e0c93e2111bd7755497 Mon Sep 17 00:00:00 2001 From: xenolf Date: Sun, 14 Feb 2016 22:07:27 +0100 Subject: [PATCH 13/43] Fix tests for new naming. --- acme/client_test.go | 12 ++++++------ acme/http_challenge_test.go | 4 ++-- acme/tls_sni_challenge_test.go | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/acme/client_test.go b/acme/client_test.go index c94d8f3e..9ba165ed 100644 --- a/acme/client_test.go +++ b/acme/client_test.go @@ -82,10 +82,10 @@ func TestClientOptPort(t *testing.T) { if httpSolver.jws != client.jws { t.Error("Expected http-01 to have same jws as client") } - if got := httpSolver.provider.(*httpChallengeServer).port; got != optPort { + if got := httpSolver.provider.(*HTTPProviderServer).port; got != optPort { t.Errorf("Expected http-01 to have port %s but was %s", optPort, got) } - if got := httpSolver.provider.(*httpChallengeServer).iface; got != optHost { + if got := httpSolver.provider.(*HTTPProviderServer).iface; got != optHost { t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got) } @@ -96,10 +96,10 @@ func TestClientOptPort(t *testing.T) { if httpsSolver.jws != client.jws { t.Error("Expected tls-sni-01 to have same jws as client") } - if got := httpsSolver.provider.(*tlsSNIChallengeServer).port; got != optPort { + if got := httpsSolver.provider.(*TLSProviderServer).port; got != optPort { t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, got) } - if got := httpsSolver.provider.(*tlsSNIChallengeServer).iface; got != optHost { + if got := httpsSolver.provider.(*TLSProviderServer).iface; got != optHost { t.Errorf("Expected tls-sni-01 to have port %s but was %s", optHost, got) } @@ -108,10 +108,10 @@ func TestClientOptPort(t *testing.T) { client.SetHTTPAddress(net.JoinHostPort(optHost, optPort)) client.SetTLSAddress(net.JoinHostPort(optHost, optPort)) - if got := httpSolver.provider.(*httpChallengeServer).iface; got != optHost { + if got := httpSolver.provider.(*HTTPProviderServer).iface; got != optHost { t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got) } - if got := httpsSolver.provider.(*tlsSNIChallengeServer).port; got != optPort { + if got := httpsSolver.provider.(*TLSProviderServer).port; got != optPort { t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, got) } } diff --git a/acme/http_challenge_test.go b/acme/http_challenge_test.go index 9c33d7d0..79b8b545 100644 --- a/acme/http_challenge_test.go +++ b/acme/http_challenge_test.go @@ -35,7 +35,7 @@ func TestHTTPChallenge(t *testing.T) { return nil } - solver := &httpChallenge{jws: j, validate: mockValidate, provider: &httpChallengeServer{port: "23457"}} + solver := &httpChallenge{jws: j, validate: mockValidate, provider: &HTTPProviderServer{port: "23457"}} if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil { t.Errorf("Solve error: got %v, want nil", err) @@ -46,7 +46,7 @@ func TestHTTPChallengeInvalidPort(t *testing.T) { privKey, _ := generatePrivateKey(rsakey, 128) j := &jws{privKey: privKey.(*rsa.PrivateKey)} clientChallenge := challenge{Type: HTTP01, Token: "http2"} - solver := &httpChallenge{jws: j, validate: stubValidate, provider: &httpChallengeServer{port: "123456"}} + solver := &httpChallenge{jws: j, validate: stubValidate, provider: &HTTPProviderServer{port: "123456"}} if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil { t.Errorf("Solve error: got %v, want error", err) diff --git a/acme/tls_sni_challenge_test.go b/acme/tls_sni_challenge_test.go index 3372912f..60f1498b 100644 --- a/acme/tls_sni_challenge_test.go +++ b/acme/tls_sni_challenge_test.go @@ -43,7 +43,7 @@ func TestTLSSNIChallenge(t *testing.T) { return nil } - solver := &tlsSNIChallenge{jws: j, validate: mockValidate, provider: &tlsSNIChallengeServer{port: "23457"}} + solver := &tlsSNIChallenge{jws: j, validate: mockValidate, provider: &TLSProviderServer{port: "23457"}} if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil { t.Errorf("Solve error: got %v, want nil", err) @@ -54,7 +54,7 @@ func TestTLSSNIChallengeInvalidPort(t *testing.T) { privKey, _ := generatePrivateKey(rsakey, 128) j := &jws{privKey: privKey.(*rsa.PrivateKey)} clientChallenge := challenge{Type: TLSSNI01, Token: "tlssni2"} - solver := &tlsSNIChallenge{jws: j, validate: stubValidate, provider: &tlsSNIChallengeServer{port: "123456"}} + solver := &tlsSNIChallenge{jws: j, validate: stubValidate, provider: &TLSProviderServer{port: "123456"}} if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil { t.Errorf("Solve error: got %v, want error", err) From 971541dc0a867f1a6cc52b927bd0bf64ec44abfc Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Sat, 13 Feb 2016 23:24:19 -0700 Subject: [PATCH 14/43] Use http client with timeout of 10s This will prevent indefinitely-hanging requests in case some service or middle box is malfunctioning. Fix vet errors and lint warnings Add vet to CI check Only get issuer certificate if it would be used No need to make a GET request if the OCSP server is not specified in leaf certificate Fix CI tests Make tests verbose --- .travis.yml | 14 +++++++++++--- acme/challenges.go | 1 + acme/crypto.go | 23 +++++++++++------------ acme/dns_challenge.go | 6 +++--- acme/dns_challenge_cloudflare.go | 2 +- acme/dns_challenge_dnsimple.go | 10 +++++----- acme/dns_challenge_route53.go | 11 ++++++----- acme/dns_challenge_route53_test.go | 8 ++++---- acme/dns_challenge_test.go | 4 ++-- acme/http.go | 10 +++++++--- configuration.go | 4 +++- 11 files changed, 54 insertions(+), 39 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6150083b..ba0113bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,14 @@ language: go go: - - 1.4.3 - - 1.5.3 - - tip \ No newline at end of file + - 1.4.3 + - 1.5.3 + - tip + +install: + - go get -t ./... + - go get golang.org/x/tools/cmd/vet + +script: + - go vet ./... + - go test -v ./... diff --git a/acme/challenges.go b/acme/challenges.go index 3f679e00..85790050 100644 --- a/acme/challenges.go +++ b/acme/challenges.go @@ -1,5 +1,6 @@ package acme +// Challenge is a string that identifies a particular type and version of ACME challenge. type Challenge string const ( diff --git a/acme/crypto.go b/acme/crypto.go index 347c9bc1..5ce5ea5f 100644 --- a/acme/crypto.go +++ b/acme/crypto.go @@ -56,14 +56,22 @@ func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) { return nil, nil, err } - // We only got one certificate, means we have no issuer certificate - get it. + // 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(certificates[0].IssuingCertificateURL) == 0 { + if len(issuedCert.IssuingCertificateURL) == 0 { return nil, nil, errors.New("no issuing certificate URL") } - resp, err := httpGet(certificates[0].IssuingCertificateURL[0]) + resp, err := httpGet(issuedCert.IssuingCertificateURL[0]) if err != nil { return nil, nil, err } @@ -83,17 +91,8 @@ func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) { // We want it ordered right SRV CRT -> CA certificates = append(certificates, issuerCert) } - - // We expect the certificate slice to be ordered downwards the chain. - // SRV CRT -> CA. We need to pull the cert and issuer cert out of it, - // which should always be the last two certificates. - issuedCert := certificates[0] issuerCert := certificates[1] - if len(issuedCert.OCSPServer) == 0 { - return nil, nil, errors.New("no OCSP server specified in cert") - } - // Finally kick off the OCSP request. ocspReq, err := ocsp.CreateRequest(issuedCert, issuerCert, nil) if err != nil { diff --git a/acme/dns_challenge.go b/acme/dns_challenge.go index ebbf1d9d..b45a964f 100644 --- a/acme/dns_challenge.go +++ b/acme/dns_challenge.go @@ -15,7 +15,7 @@ import ( type preCheckDNSFunc func(fqdn, value string) (bool, error) -var preCheckDNS preCheckDNSFunc = checkDnsPropagation +var preCheckDNS preCheckDNSFunc = checkDNSPropagation var recursiveNameserver = "google-public-dns-a.google.com" @@ -75,8 +75,8 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error { return s.validate(s.jws, domain, chlng.URI, challenge{Resource: "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) { +// 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, recursiveNameserver, true) if err != nil { diff --git a/acme/dns_challenge_cloudflare.go b/acme/dns_challenge_cloudflare.go index 4781ec5b..45a9e3a6 100644 --- a/acme/dns_challenge_cloudflare.go +++ b/acme/dns_challenge_cloudflare.go @@ -27,7 +27,7 @@ func NewDNSProviderCloudFlare(cloudflareEmail, cloudflareKey string) (*DNSProvid } c := &DNSProviderCloudFlare{ - client: cloudflare.New(&cloudflare.Options{cloudflareEmail, cloudflareKey}), + client: cloudflare.New(&cloudflare.Options{Email: cloudflareEmail, Key: cloudflareKey}), ctx: context.Background(), } diff --git a/acme/dns_challenge_dnsimple.go b/acme/dns_challenge_dnsimple.go index f22590c7..56b96f97 100644 --- a/acme/dns_challenge_dnsimple.go +++ b/acme/dns_challenge_dnsimple.go @@ -16,16 +16,16 @@ type DNSProviderDNSimple struct { // NewDNSProviderDNSimple returns a DNSProviderDNSimple instance with a configured dnsimple client. // Authentication is either done using the passed credentials or - when empty - using the environment // variables DNSIMPLE_EMAIL and DNSIMPLE_API_KEY. -func NewDNSProviderDNSimple(dnsimpleEmail, dnsimpleApiKey string) (*DNSProviderDNSimple, error) { - if dnsimpleEmail == "" || dnsimpleApiKey == "" { - dnsimpleEmail, dnsimpleApiKey = dnsimpleEnvAuth() - if dnsimpleEmail == "" || dnsimpleApiKey == "" { +func NewDNSProviderDNSimple(dnsimpleEmail, dnsimpleAPIKey string) (*DNSProviderDNSimple, error) { + if dnsimpleEmail == "" || dnsimpleAPIKey == "" { + dnsimpleEmail, dnsimpleAPIKey = dnsimpleEnvAuth() + if dnsimpleEmail == "" || dnsimpleAPIKey == "" { return nil, fmt.Errorf("DNSimple credentials missing") } } c := &DNSProviderDNSimple{ - client: dnsimple.NewClient(dnsimpleApiKey, dnsimpleEmail), + client: dnsimple.NewClient(dnsimpleAPIKey, dnsimpleEmail), } return c, nil diff --git a/acme/dns_challenge_route53.go b/acme/dns_challenge_route53.go index c4ce6eb2..68a05b0c 100644 --- a/acme/dns_challenge_route53.go +++ b/acme/dns_challenge_route53.go @@ -32,12 +32,13 @@ func NewDNSProviderRoute53(awsAccessKey, awsSecretKey, awsRegionName string) (*D // - uses AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY and optionally AWS_SECURITY_TOKEN, if provided // - uses EC2 instance metadata credentials (http://169.254.169.254/latest/meta-data/…), if available // ...and otherwise returns an error - if auth, err := aws.GetAuth(awsAccessKey, awsSecretKey); err != nil { + auth, err := aws.GetAuth(awsAccessKey, awsSecretKey) + if err != nil { return nil, err - } else { - client := route53.New(auth, region) - return &DNSProviderRoute53{client: client}, nil } + + client := route53.New(auth, region) + return &DNSProviderRoute53{client: client}, nil } // Present creates a TXT record using the specified parameters @@ -60,7 +61,7 @@ func (r *DNSProviderRoute53) changeRecord(action, fqdn, value string, ttl int) e return err } recordSet := newTXTRecordSet(fqdn, value, ttl) - update := route53.Change{action, recordSet} + update := route53.Change{Action: action, Record: recordSet} changes := []route53.Change{update} req := route53.ChangeResourceRecordSetsRequest{Comment: "Created by Lego", Changes: changes} resp, err := r.client.ChangeResourceRecordSets(hostedZoneID, &req) diff --git a/acme/dns_challenge_route53_test.go b/acme/dns_challenge_route53_test.go index e57fdaa7..2c58b263 100644 --- a/acme/dns_challenge_route53_test.go +++ b/acme/dns_challenge_route53_test.go @@ -65,9 +65,9 @@ var GetChangeAnswer = ` ` var serverResponseMap = testutil.ResponseMap{ - "/2013-04-01/hostedzone/": testutil.Response{200, nil, ListHostedZonesAnswer}, - "/2013-04-01/hostedzone/Z2K123214213123/rrset": testutil.Response{200, nil, ChangeResourceRecordSetsAnswer}, - "/2013-04-01/change/asdf": testutil.Response{200, nil, GetChangeAnswer}, + "/2013-04-01/hostedzone/": testutil.Response{Status: 200, Headers: nil, Body: ListHostedZonesAnswer}, + "/2013-04-01/hostedzone/Z2K123214213123/rrset": testutil.Response{Status: 200, Headers: nil, Body: ChangeResourceRecordSetsAnswer}, + "/2013-04-01/change/asdf": testutil.Response{Status: 200, Headers: nil, Body: GetChangeAnswer}, } func init() { @@ -92,7 +92,7 @@ func makeRoute53TestServer() *testutil.HTTPServer { } func makeRoute53Provider(server *testutil.HTTPServer) *DNSProviderRoute53 { - auth := aws.Auth{"abc", "123", ""} + auth := aws.Auth{AccessKey: "abc", SecretKey: "123", Token: ""} client := route53.NewWithClient(auth, aws.Region{Route53Endpoint: server.URL}, testutil.DefaultClient) return &DNSProviderRoute53{client: client} } diff --git a/acme/dns_challenge_test.go b/acme/dns_challenge_test.go index 031fa555..20090edc 100644 --- a/acme/dns_challenge_test.go +++ b/acme/dns_challenge_test.go @@ -141,7 +141,7 @@ func TestCheckAuthoritativeNss(t *testing.T) { for _, tt := range checkAuthoritativeNssTests { ok, _ := checkAuthoritativeNss(tt.fqdn, tt.value, tt.ns) if ok != tt.ok { - t.Errorf("#%s: got %t; want %t", tt.fqdn, tt.ok) + t.Errorf("%s: got %t; want %t", tt.fqdn, tt.ok, tt.ok) } } } @@ -174,7 +174,7 @@ func TestWaitForTimeout(t *testing.T) { t.Fatal("timeout exceeded") case err := <-c: if err == nil { - t.Errorf("expected timeout error; got ", err) + t.Errorf("expected timeout error; got %v", err) } } } diff --git a/acme/http.go b/acme/http.go index 6933899b..410aead6 100644 --- a/acme/http.go +++ b/acme/http.go @@ -8,11 +8,15 @@ import ( "net/http" "runtime" "strings" + "time" ) // UserAgent (if non-empty) will be tacked onto the User-Agent string in requests. var UserAgent string +// defaultClient is an HTTP client with a reasonable timeout value. +var defaultClient = http.Client{Timeout: 10 * 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. @@ -32,7 +36,7 @@ func httpHead(url string) (resp *http.Response, err error) { req.Header.Set("User-Agent", userAgent()) - resp, err = http.DefaultClient.Do(req) + resp, err = defaultClient.Do(req) if err != nil { return resp, err } @@ -50,7 +54,7 @@ func httpPost(url string, bodyType string, body io.Reader) (resp *http.Response, req.Header.Set("Content-Type", bodyType) req.Header.Set("User-Agent", userAgent()) - return http.DefaultClient.Do(req) + return defaultClient.Do(req) } // httpGet performs a GET request with a proper User-Agent string. @@ -62,7 +66,7 @@ func httpGet(url string) (resp *http.Response, err error) { } req.Header.Set("User-Agent", userAgent()) - return http.DefaultClient.Do(req) + return defaultClient.Do(req) } // getJSON performs an HTTP GET request and parses the response body diff --git a/configuration.go b/configuration.go index 503510f8..cbb157f3 100644 --- a/configuration.go +++ b/configuration.go @@ -25,6 +25,7 @@ func (c *Configuration) RsaBits() int { return c.context.GlobalInt("rsa-key-size") } +// ExcludedSolvers is a list of solvers that are to be excluded. func (c *Configuration) ExcludedSolvers() (cc []acme.Challenge) { for _, s := range c.context.GlobalStringSlice("exclude") { cc = append(cc, acme.Challenge(s)) @@ -39,6 +40,7 @@ func (c *Configuration) ServerPath() string { return strings.Replace(srvStr, "/", string(os.PathSeparator), -1) } +// CertPath gets the path for certificates. func (c *Configuration) CertPath() string { return path.Join(c.context.GlobalString("path"), "certificates") } @@ -54,7 +56,7 @@ func (c *Configuration) AccountPath(acc string) string { return path.Join(c.AccountsPath(), acc) } -// AccountPath returns the OS dependent path to the keys of a particular account +// AccountKeysPath returns the OS dependent path to the keys of a particular account func (c *Configuration) AccountKeysPath(acc string) string { return path.Join(c.AccountPath(acc), "keys") } From 78c36ef846b78aa0be98a961aaad8bb6999529e1 Mon Sep 17 00:00:00 2001 From: Michael Cross Date: Mon, 15 Feb 2016 14:18:31 +0000 Subject: [PATCH 15/43] Fix small typos in error messages --- cli_handlers.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli_handlers.go b/cli_handlers.go index fb7e54bb..06bdfbb0 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -23,7 +23,7 @@ func checkFolder(path string) error { func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { err := checkFolder(c.GlobalString("path")) if err != nil { - logger().Fatalf("Cound not check/create path: %s", err.Error()) + logger().Fatalf("Could not check/create path: %s", err.Error()) } conf := NewConfiguration(c) @@ -190,7 +190,7 @@ func run(c *cli.Context) { err := checkFolder(conf.CertPath()) if err != nil { - logger().Fatalf("Cound not check/create path: %s", err.Error()) + logger().Fatalf("Could not check/create path: %s", err.Error()) } saveCertRes(cert, conf) @@ -202,7 +202,7 @@ func revoke(c *cli.Context) { err := checkFolder(conf.CertPath()) if err != nil { - logger().Fatalf("Cound not check/create path: %s", err.Error()) + logger().Fatalf("Could not check/create path: %s", err.Error()) } for _, domain := range c.GlobalStringSlice("domains") { From d03fb496c0050804ba2a2eef0f98ef54883aeaed Mon Sep 17 00:00:00 2001 From: Jan Broer Date: Tue, 16 Feb 2016 15:50:24 +0100 Subject: [PATCH 16/43] Refactor CloudFlare provider to have no 3rd party dependencies --- acme/dns_challenge_cloudflare.go | 199 +++++++++++++++++--------- acme/dns_challenge_cloudflare_test.go | 2 +- 2 files changed, 132 insertions(+), 69 deletions(-) diff --git a/acme/dns_challenge_cloudflare.go b/acme/dns_challenge_cloudflare.go index 45a9e3a6..38f89dac 100644 --- a/acme/dns_challenge_cloudflare.go +++ b/acme/dns_challenge_cloudflare.go @@ -1,23 +1,26 @@ package acme import ( + "bytes" + "encoding/json" "fmt" + "io" + "net/http" "os" "strings" - - "github.com/crackcomm/cloudflare" - "golang.org/x/net/context" + "time" ) +const CloudFlareApiURL = "https://api.cloudflare.com/client/v4" + // DNSProviderCloudFlare is an implementation of the DNSProvider interface type DNSProviderCloudFlare struct { - client *cloudflare.Client - ctx context.Context + authEmail string + authKey string } // NewDNSProviderCloudFlare returns a DNSProviderCloudFlare instance with a configured cloudflare client. -// Authentication is either done using the passed credentials or - when empty - using the environment -// variables CLOUDFLARE_EMAIL and CLOUDFLARE_API_KEY. +// Credentials can either be passed as arguments or through CLOUDFLARE_EMAIL and CLOUDFLARE_API_KEY env vars. func NewDNSProviderCloudFlare(cloudflareEmail, cloudflareKey string) (*DNSProviderCloudFlare, error) { if cloudflareEmail == "" || cloudflareKey == "" { cloudflareEmail, cloudflareKey = cloudflareEnvAuth() @@ -26,26 +29,35 @@ func NewDNSProviderCloudFlare(cloudflareEmail, cloudflareKey string) (*DNSProvid } } - c := &DNSProviderCloudFlare{ - client: cloudflare.New(&cloudflare.Options{Email: cloudflareEmail, Key: cloudflareKey}), - ctx: context.Background(), - } - - return c, nil + return &DNSProviderCloudFlare{ + authEmail: cloudflareEmail, + authKey: cloudflareKey, + }, nil } // Present creates a TXT record to fulfil the dns-01 challenge func (c *DNSProviderCloudFlare) Present(domain, token, keyAuth string) error { - fqdn, value, ttl := DNS01Record(domain, keyAuth) + fqdn, value, _ := DNS01Record(domain, keyAuth) zoneID, err := c.getHostedZoneID(fqdn) if err != nil { return err } - record := newTxtRecord(zoneID, fqdn, value, ttl) - err = c.client.Records.Create(c.ctx, record) + rec := cloudFlareRecord{ + Type: "TXT", + Name: unFqdn(fqdn), + Content: value, + TTL: 120, + } + + body, err := json.Marshal(rec) if err != nil { - return fmt.Errorf("CloudFlare API call failed: %v", err) + return err + } + + _, err = c.makeRequest("POST", fmt.Sprintf("/zones/%s/dns_records", zoneID), bytes.NewReader(body)) + if err != nil { + return err } return nil @@ -54,85 +66,126 @@ func (c *DNSProviderCloudFlare) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (c *DNSProviderCloudFlare) CleanUp(domain, token, keyAuth string) error { fqdn, _, _ := DNS01Record(domain, keyAuth) - records, err := c.findTxtRecords(fqdn) + + record, err := c.findTxtRecord(fqdn) if err != nil { return err } - for _, rec := range records { - err := c.client.Records.Delete(c.ctx, rec.ZoneID, rec.ID) - if err != nil { - return err - } + _, err = c.makeRequest("DELETE", fmt.Sprintf("/zones/%s/dns_records/%s", record.ZoneID, record.ID), nil) + if err != nil { + return err } + return nil } -func (c *DNSProviderCloudFlare) findTxtRecords(fqdn string) ([]*cloudflare.Record, error) { - zoneID, err := c.getHostedZoneID(fqdn) - if err != nil { - return nil, err - } - - var records []*cloudflare.Record - result, err := c.client.Records.List(c.ctx, zoneID) - if err != nil { - return records, fmt.Errorf("CloudFlare API call has failed: %v", err) - } - - name := unFqdn(fqdn) - for _, rec := range result { - if rec.Name == name && rec.Type == "TXT" { - records = append(records, rec) - } - } - - return records, nil -} - func (c *DNSProviderCloudFlare) getHostedZoneID(fqdn string) (string, error) { - zones, err := c.client.Zones.List(c.ctx) - if err != nil { - return "", fmt.Errorf("CloudFlare API call failed: %v", err) + // HostedZone represents a CloudFlare DNS zone + type HostedZone struct { + ID string `json:"id"` + Name string `json:"name"` } - var hostedZone cloudflare.Zone + result, err := c.makeRequest("GET", "/zones?per_page=1000", nil) + if err != nil { + return "", err + } + + var zones []HostedZone + err = json.Unmarshal(result, &zones) + if err != nil { + return "", err + } + + var hostedZone HostedZone for _, zone := range zones { name := toFqdn(zone.Name) if strings.HasSuffix(fqdn, name) { if len(zone.Name) > len(hostedZone.Name) { - hostedZone = *zone + hostedZone = zone } } } if hostedZone.ID == "" { - return "", fmt.Errorf("No matching CloudFlare zone found for domain %s", fqdn) + return "", fmt.Errorf("No matching CloudFlare zone found for %s", fqdn) } return hostedZone.ID, nil } -func newTxtRecord(zoneID, fqdn, value string, ttl int) *cloudflare.Record { - name := unFqdn(fqdn) - return &cloudflare.Record{ - Type: "TXT", - Name: name, - Content: value, - TTL: sanitizeTTL(ttl), - ZoneID: zoneID, +func (c *DNSProviderCloudFlare) findTxtRecord(fqdn string) (*cloudFlareRecord, error) { + zoneID, err := c.getHostedZoneID(fqdn) + if err != nil { + return nil, err } + + result, err := c.makeRequest("GET", fmt.Sprintf("/zones/%s/dns_records?per_page=1000", zoneID), nil) + if err != nil { + return nil, err + } + + var records []cloudFlareRecord + err = json.Unmarshal(result, &records) + if err != nil { + return nil, err + } + + for _, rec := range records { + if rec.Name == unFqdn(fqdn) && rec.Type == "TXT" { + return &rec, nil + } + } + + return nil, fmt.Errorf("No existing record found for %s", fqdn) } -// TTL must be between 120 and 86400 seconds -func sanitizeTTL(ttl int) int { - switch { - case ttl < 120: - return 120 - case ttl > 86400: - return 86400 - default: - return ttl +func (c *DNSProviderCloudFlare) makeRequest(method, uri string, body io.Reader) (json.RawMessage, error) { + // ApiError contains error details for failed requests + type ApiError struct { + Code int `json:"code,omitempty"` + Message string `json:"message,omitempty"` } + + // ApiResponse represents a response from CloudFlare API + type ApiResponse struct { + Success bool `json:"success"` + Errors []*ApiError `json:"errors"` + Result json.RawMessage `json:"result"` + } + + req, err := http.NewRequest(method, fmt.Sprintf("%s%s", CloudFlareApiURL, uri), body) + if err != nil { + return nil, err + } + + req.Header.Set("X-Auth-Email", c.authEmail) + req.Header.Set("X-Auth-Key", c.authKey) + + client := http.DefaultClient + client.Timeout = time.Duration(30 * time.Second) + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("Error querying API -> %v", err) + } + + defer resp.Body.Close() + + var r ApiResponse + err = json.NewDecoder(resp.Body).Decode(&r) + if err != nil { + return nil, err + } + + if !r.Success { + if len(r.Errors) > 0 { + return nil, fmt.Errorf("API error -> %d: %s", r.Errors[0].Code, r.Errors[0].Message) + } + return nil, fmt.Errorf("API error") + } + + return r.Result, nil } func cloudflareEnvAuth() (email, apiKey string) { @@ -143,3 +196,13 @@ func cloudflareEnvAuth() (email, apiKey string) { } return } + +// cloudFlareRecord represents a CloudFlare DNS record +type cloudFlareRecord struct { + Name string `json:"name"` + Type string `json:"type"` + Content string `json:"content"` + ID string `json:"id,omitempty"` + TTL int `json:"ttl,omitempty"` + ZoneID string `json:"zone_id,omitempty"` +} diff --git a/acme/dns_challenge_cloudflare_test.go b/acme/dns_challenge_cloudflare_test.go index 8b3cd461..e628eba1 100644 --- a/acme/dns_challenge_cloudflare_test.go +++ b/acme/dns_challenge_cloudflare_test.go @@ -70,7 +70,7 @@ func TestCloudFlareCleanUp(t *testing.T) { t.Skip("skipping live test") } - time.Sleep(time.Second * 1) + time.Sleep(time.Second * 2) provider, err := NewDNSProviderCloudFlare(cflareEmail, cflareAPIKey) assert.NoError(t, err) From 93cfae053aad5f833a222f4ea27ad4ccdefa658b Mon Sep 17 00:00:00 2001 From: Jan Broer Date: Tue, 16 Feb 2016 18:38:51 +0100 Subject: [PATCH 17/43] Use custom lego user-agent in requests --- acme/dns_challenge_cloudflare.go | 1 + 1 file changed, 1 insertion(+) diff --git a/acme/dns_challenge_cloudflare.go b/acme/dns_challenge_cloudflare.go index 38f89dac..f6eea687 100644 --- a/acme/dns_challenge_cloudflare.go +++ b/acme/dns_challenge_cloudflare.go @@ -161,6 +161,7 @@ func (c *DNSProviderCloudFlare) makeRequest(method, uri string, body io.Reader) req.Header.Set("X-Auth-Email", c.authEmail) req.Header.Set("X-Auth-Key", c.authKey) + req.Header.Set("User-Agent", userAgent()) client := http.DefaultClient client.Timeout = time.Duration(30 * time.Second) From 453a3d6b3f42d193931aa51a824ec28c8efe976b Mon Sep 17 00:00:00 2001 From: Jan Broer Date: Thu, 18 Feb 2016 20:37:07 +0100 Subject: [PATCH 18/43] Declare own HTTP client --- acme/dns_challenge_cloudflare.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/acme/dns_challenge_cloudflare.go b/acme/dns_challenge_cloudflare.go index f6eea687..f55c28bd 100644 --- a/acme/dns_challenge_cloudflare.go +++ b/acme/dns_challenge_cloudflare.go @@ -163,9 +163,7 @@ func (c *DNSProviderCloudFlare) makeRequest(method, uri string, body io.Reader) req.Header.Set("X-Auth-Key", c.authKey) req.Header.Set("User-Agent", userAgent()) - client := http.DefaultClient - client.Timeout = time.Duration(30 * time.Second) - + client := http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("Error querying API -> %v", err) From fc64f8b99df0902a1bf6e33355c3c8e9eecbd9fd Mon Sep 17 00:00:00 2001 From: Michael Cross Date: Fri, 19 Feb 2016 10:19:03 +0000 Subject: [PATCH 19/43] DNS Challenge: Fix TestCheckAuthoritativeNss failure report --- acme/dns_challenge_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/dns_challenge_test.go b/acme/dns_challenge_test.go index 20090edc..e1e67efe 100644 --- a/acme/dns_challenge_test.go +++ b/acme/dns_challenge_test.go @@ -141,7 +141,7 @@ func TestCheckAuthoritativeNss(t *testing.T) { for _, tt := range checkAuthoritativeNssTests { ok, _ := checkAuthoritativeNss(tt.fqdn, tt.value, tt.ns) if ok != tt.ok { - t.Errorf("%s: got %t; want %t", tt.fqdn, tt.ok, tt.ok) + t.Errorf("%s: got %t; want %t", tt.fqdn, ok, tt.ok) } } } From 06b3802346b4a4c172c66bcde49532e875dfda2d Mon Sep 17 00:00:00 2001 From: Michael Cross Date: Fri, 19 Feb 2016 08:14:26 +0000 Subject: [PATCH 20/43] DNS Challenge: Fix handling of CNAMEs Prior to this commit, the checkDNSPropagation function was exiting early if the TXT record could not be found on the recursive nameserver, and thus the authoritative nameservers were not being queried until after the record showed up on the recursive nameserver causing a delay. This commit changes that behaviour so that the authoritative nameservers are queried on each execution of checkDNSPropagation when possible. --- acme/dns_challenge.go | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/acme/dns_challenge.go b/acme/dns_challenge.go index b45a964f..4cd58f50 100644 --- a/acme/dns_challenge.go +++ b/acme/dns_challenge.go @@ -82,16 +82,14 @@ func checkDNSPropagation(fqdn, value string) (bool, error) { if err != nil { return false, err } - if r.Rcode != dns.RcodeSuccess { - return false, fmt.Errorf("Could not resolve %s -> %s", fqdn, dns.RcodeToString[r.Rcode]) - } - - // 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 + 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 + } } } } From c3abd54dc7ceb2c5033e00658e83698e875aea2c Mon Sep 17 00:00:00 2001 From: Michael Cross Date: Fri, 19 Feb 2016 21:11:41 +0000 Subject: [PATCH 21/43] CLI: Give helpful error message if --http/--tls is given without colon Fixes #134 --- cli_handlers.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cli_handlers.go b/cli_handlers.go index 06bdfbb0..fe4f7a80 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -44,10 +44,16 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { } if c.GlobalIsSet("http") { + if strings.Index(c.GlobalString("http"), ":") == -1 { + logger().Fatalf("The --http switch only accepts interface:port or :port for its argument.") + } client.SetHTTPAddress(c.GlobalString("http")) } if c.GlobalIsSet("tls") { + if strings.Index(c.GlobalString("tls"), ":") == -1 { + logger().Fatalf("The --tls switch only accepts interface:port or :port for its argument.") + } client.SetTLSAddress(c.GlobalString("tls")) } From 416a63120e8d7637b4ca2c189892a8af937ab11a Mon Sep 17 00:00:00 2001 From: xenolf Date: Mon, 15 Feb 2016 03:51:59 +0100 Subject: [PATCH 22/43] Introduce --agree-tos switch. Fixes #128 --- CHANGELOG.md | 1 + cli.go | 4 +++ cli_handlers.go | 70 ++++++++++++++++++++++++++++++------------------- 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 850f6dfb..d6bc07e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added: - CLI: The `--dns` switch. To include the DNS challenge for consideration. Supported are the following solvers: cloudflare, digitalocean, dnsimple, route53, rfc2136 and manual. +- CLI: The `--accept-tos` switch. Indicates your acceptance of the Let's Encrypt terms of service without prompting you. - lib: A new type for challenge identifiers `Challenge` - lib: A new interface for custom challenge providers `ChallengeProvider` - lib: SetChallengeProvider function. Pass a challenge identifier and a Provider to replace the default behaviour of a challenge. diff --git a/cli.go b/cli.go index ba216d07..97d3a816 100644 --- a/cli.go +++ b/cli.go @@ -88,6 +88,10 @@ func main() { Name: "email, m", Usage: "Email used for registration and recovery contact.", }, + cli.BoolFlag{ + Name: "accept-tos, a", + Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.", + }, cli.IntFlag{ Name: "rsa-key-size, B", Value: 2048, diff --git a/cli_handlers.go b/cli_handlers.go index fe4f7a80..e6e71cbe 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -126,6 +126,47 @@ func saveCertRes(certRes acme.CertificateResource, conf *Configuration) { } } +func handleTOS(c *cli.Context, client *acme.Client, acc *Account) { + // Check for a global accept override + if c.GlobalBool("accept-tos") { + err := client.AgreeToTOS() + if err != nil { + logger().Fatalf("Could not agree to TOS: %s", err.Error()) + } + + acc.Save() + return + } + + reader := bufio.NewReader(os.Stdin) + logger().Printf("Please review the TOS at %s", acc.Registration.TosURL) + + for { + logger().Println("Do you accept the TOS? Y/n") + text, err := reader.ReadString('\n') + if err != nil { + logger().Fatalf("Could not read from console: %s", err.Error()) + } + + text = strings.Trim(text, "\r\n") + + if text == "n" { + logger().Fatal("You did not accept the TOS. Unable to proceed.") + } + + if text == "Y" || text == "y" || text == "" { + err = client.AgreeToTOS() + if err != nil { + logger().Fatalf("Could not agree to TOS: %s", err.Error()) + } + acc.Save() + break + } + + logger().Println("Your input was invalid. Please answer with one of Y/y, n or by pressing enter.") + } +} + func run(c *cli.Context) { conf, acc, client := setup(c) if acc.Registration == nil { @@ -148,34 +189,9 @@ func run(c *cli.Context) { } + // If the agreement URL is empty, the account still needs to accept the LE TOS. if acc.Registration.Body.Agreement == "" { - reader := bufio.NewReader(os.Stdin) - logger().Printf("Please review the TOS at %s", acc.Registration.TosURL) - - for { - logger().Println("Do you accept the TOS? Y/n") - text, err := reader.ReadString('\n') - if err != nil { - logger().Fatalf("Could not read from console -> %s", err.Error()) - } - - text = strings.Trim(text, "\r\n") - - if text == "n" { - logger().Fatal("You did not accept the TOS. Unable to proceed.") - } - - if text == "Y" || text == "y" || text == "" { - err = client.AgreeToTOS() - if err != nil { - logger().Fatalf("Could not agree to tos -> %s", err) - } - acc.Save() - break - } - - logger().Println("Your input was invalid. Please answer with one of Y/y, n or by pressing enter.") - } + handleTOS(c, client, acc) } if len(c.GlobalStringSlice("domains")) == 0 { From a4d8c0e6b97d31c67f99b8dc55f7e70596ed5fee Mon Sep 17 00:00:00 2001 From: xenolf Date: Mon, 15 Feb 2016 03:59:43 +0100 Subject: [PATCH 23/43] Fix a couple of misspelled words and lint errors. --- acme/client.go | 4 ++-- acme/dns_challenge_cloudflare.go | 18 ++++++++++-------- acme/dns_challenge_route53.go | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/acme/client.go b/acme/client.go index 35dbcd64..be9843e2 100644 --- a/acme/client.go +++ b/acme/client.go @@ -316,7 +316,7 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle bool) (Certif links := parseLinks(resp.Header["Link"]) issuerCert, err := c.getIssuerCertificate(links["up"]) if err != nil { - // If we fail to aquire the issuer cert, return the issued certificate - do not fail. + // If we fail to acquire the issuer cert, return the issued certificate - do not fail. logf("[ERROR][%s] acme: Could not bundle issuer certificate: %v", cert.Domain, err) } else { // Success - append the issuer cert to the issued cert. @@ -518,7 +518,7 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, links := parseLinks(resp.Header["Link"]) issuerCert, err := c.getIssuerCertificate(links["up"]) if err != nil { - // If we fail to aquire the issuer cert, return the issued certificate - do not fail. + // 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", commonName.Domain, err) } else { // Success - append the issuer cert to the issued cert. diff --git a/acme/dns_challenge_cloudflare.go b/acme/dns_challenge_cloudflare.go index f55c28bd..b5bf6d1f 100644 --- a/acme/dns_challenge_cloudflare.go +++ b/acme/dns_challenge_cloudflare.go @@ -11,7 +11,9 @@ import ( "time" ) -const CloudFlareApiURL = "https://api.cloudflare.com/client/v4" +// CloudFlareAPIURL represents the API endpoint to call. +// TODO: Unexport? +const CloudFlareAPIURL = "https://api.cloudflare.com/client/v4" // DNSProviderCloudFlare is an implementation of the DNSProvider interface type DNSProviderCloudFlare struct { @@ -141,20 +143,20 @@ func (c *DNSProviderCloudFlare) findTxtRecord(fqdn string) (*cloudFlareRecord, e } func (c *DNSProviderCloudFlare) makeRequest(method, uri string, body io.Reader) (json.RawMessage, error) { - // ApiError contains error details for failed requests - type ApiError struct { + // APIError contains error details for failed requests + type APIError struct { Code int `json:"code,omitempty"` Message string `json:"message,omitempty"` } - // ApiResponse represents a response from CloudFlare API - type ApiResponse struct { + // APIResponse represents a response from CloudFlare API + type APIResponse struct { Success bool `json:"success"` - Errors []*ApiError `json:"errors"` + Errors []*APIError `json:"errors"` Result json.RawMessage `json:"result"` } - req, err := http.NewRequest(method, fmt.Sprintf("%s%s", CloudFlareApiURL, uri), body) + req, err := http.NewRequest(method, fmt.Sprintf("%s%s", CloudFlareAPIURL, uri), body) if err != nil { return nil, err } @@ -171,7 +173,7 @@ func (c *DNSProviderCloudFlare) makeRequest(method, uri string, body io.Reader) defer resp.Body.Close() - var r ApiResponse + var r APIResponse err = json.NewDecoder(resp.Body).Decode(&r) if err != nil { return nil, err diff --git a/acme/dns_challenge_route53.go b/acme/dns_challenge_route53.go index 68a05b0c..43e42dab 100644 --- a/acme/dns_challenge_route53.go +++ b/acme/dns_challenge_route53.go @@ -16,7 +16,7 @@ type DNSProviderRoute53 struct { // NewDNSProviderRoute53 returns a DNSProviderRoute53 instance with a configured route53 client. // Authentication is either done using the passed credentials or - when empty - falling back to -// the customary AWS credential mechanisms, including the file refernced by $AWS_CREDENTIAL_FILE +// the customary AWS credential mechanisms, including the file referenced by $AWS_CREDENTIAL_FILE // (defaulting to $HOME/.aws/credentials) optionally scoped to $AWS_PROFILE, credentials // supplied by the environment variables AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY [ + AWS_SECURITY_TOKEN ], // and finally credentials available via the EC2 instance metadata service. From f203a8e3361e8f4a085bce814cfa511f5174f7c0 Mon Sep 17 00:00:00 2001 From: xenolf Date: Sun, 21 Feb 2016 04:14:32 +0100 Subject: [PATCH 24/43] Fix wrong variables being used in DNSimple test. --- acme/dns_challenge_dnsimple_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/dns_challenge_dnsimple_test.go b/acme/dns_challenge_dnsimple_test.go index 0f51afdd..3a85f1f9 100644 --- a/acme/dns_challenge_dnsimple_test.go +++ b/acme/dns_challenge_dnsimple_test.go @@ -71,7 +71,7 @@ func TestLiveDNSimpleCleanUp(t *testing.T) { time.Sleep(time.Second * 1) - provider, err := NewDNSProviderDNSimple(cflareEmail, cflareAPIKey) + provider, err := NewDNSProviderDNSimple(dnsimpleEmail, dnsimpleAPIKey) assert.NoError(t, err) err = provider.CleanUp(dnsimpleDomain, "", "123d==") From 0e26bb45ca14fb6befc3cbe710b90c70ebd0f0c4 Mon Sep 17 00:00:00 2001 From: xenolf Date: Wed, 27 Jan 2016 02:01:39 +0100 Subject: [PATCH 25/43] Add support for EC certificates / account keys --- account.go | 16 +++++++------- acme/client.go | 23 +++++++++----------- acme/crypto.go | 44 +++++++++++++++++++++++++++++---------- acme/dns_challenge.go | 2 +- acme/http_challenge.go | 2 +- acme/jws.go | 20 +++++++++++++++--- acme/messages.go | 18 +++++++--------- acme/tls_sni_challenge.go | 4 ++-- cli.go | 8 +++---- cli_handlers.go | 7 ++++++- configuration.go | 20 +++++++++++++++--- crypto.go | 29 ++++++++++++++++++++------ 12 files changed, 130 insertions(+), 63 deletions(-) diff --git a/account.go b/account.go index 13325470..85ac09f1 100644 --- a/account.go +++ b/account.go @@ -1,7 +1,7 @@ package main import ( - "crypto/rsa" + "crypto" "encoding/json" "io/ioutil" "os" @@ -13,7 +13,7 @@ import ( // Account represents a users local saved credentials type Account struct { Email string `json:"email"` - key *rsa.PrivateKey + key crypto.PrivateKey Registration *acme.RegistrationResource `json:"registration"` conf *Configuration @@ -28,16 +28,18 @@ func NewAccount(email string, conf *Configuration) *Account { logger().Fatalf("Could not check/create directory for account %s: %v", email, err) } - var privKey *rsa.PrivateKey + var privKey crypto.PrivateKey if _, err := os.Stat(accKeyPath); os.IsNotExist(err) { - logger().Printf("No key found for account %s. Generating a %v bit key.", email, conf.RsaBits()) - privKey, err = generateRsaKey(conf.RsaBits(), accKeyPath) + + logger().Printf("No key found for account %s. Generating a curve P384 EC key.", email) + privKey, err = generatePrivateKey(accKeyPath) if err != nil { logger().Fatalf("Could not generate RSA private account key for account %s: %v", email, err) } + logger().Printf("Saved key to %s", accKeyPath) } else { - privKey, err = loadRsaKey(accKeyPath) + privKey, err = loadPrivateKey(accKeyPath) if err != nil { logger().Fatalf("Could not load RSA private key from file %s: %v", accKeyPath, err) } @@ -73,7 +75,7 @@ func (a *Account) GetEmail() string { } // GetPrivateKey returns the private RSA account key. -func (a *Account) GetPrivateKey() *rsa.PrivateKey { +func (a *Account) GetPrivateKey() crypto.PrivateKey { return a.key } diff --git a/acme/client.go b/acme/client.go index be9843e2..769b17e0 100644 --- a/acme/client.go +++ b/acme/client.go @@ -3,7 +3,6 @@ package acme import ( "crypto" - "crypto/rsa" "crypto/x509" "encoding/base64" "encoding/json" @@ -38,7 +37,7 @@ func logf(format string, args ...interface{}) { type User interface { GetEmail() string GetRegistration() *RegistrationResource - GetPrivateKey() *rsa.PrivateKey + GetPrivateKey() crypto.PrivateKey } // Interface for all challenge solvers to implement. @@ -53,7 +52,7 @@ type Client struct { directory directory user User jws *jws - keyBits int + keyType KeyType issuerCert []byte solvers map[Challenge]solver } @@ -61,16 +60,12 @@ type Client struct { // 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. It will // generate private keys for certificates of size keyBits. -func NewClient(caDirURL string, user User, keyBits int) (*Client, error) { +func NewClient(caDirURL string, user User, keyType KeyType) (*Client, error) { privKey := user.GetPrivateKey() if privKey == nil { return nil, errors.New("private key was nil") } - if err := privKey.Validate(); err != nil { - return nil, fmt.Errorf("invalid private key: %v", err) - } - var dir directory if _, err := getJSON(caDirURL, &dir); err != nil { return nil, fmt.Errorf("get directory at '%s': %v", caDirURL, err) @@ -98,7 +93,7 @@ func NewClient(caDirURL string, user User, keyBits int) (*Client, error) { solvers[HTTP01] = &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}} solvers[TLSSNI01] = &tlsSNIChallenge{jws: jws, validate: validate, provider: &TLSProviderServer{}} - return &Client{directory: dir, user: user, jws: jws, keyBits: keyBits, solvers: solvers}, nil + return &Client{directory: dir, user: user, jws: jws, keyType: keyType, solvers: solvers}, nil } // SetChallengeProvider specifies a custom provider that will make the solution available @@ -197,8 +192,10 @@ func (c *Client) Register() (*RegistrationResource, error) { // AgreeToTOS updates the Client registration and sends the agreement to // the server. func (c *Client) AgreeToTOS() error { - c.user.GetRegistration().Body.Agreement = c.user.GetRegistration().TosURL - c.user.GetRegistration().Body.Resource = "reg" + reg := c.user.GetRegistration() + + reg.Body.Agreement = c.user.GetRegistration().TosURL + reg.Body.Resource = "reg" _, err := postJSON(c.jws, c.user.GetRegistration().URI, c.user.GetRegistration().Body, nil) return err } @@ -457,7 +454,7 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, commonName := authz[0] var err error if privKey == nil { - privKey, err = generatePrivateKey(rsakey, c.keyBits) + privKey, err = generatePrivateKey(c.keyType) if err != nil { return CertificateResource{}, err } @@ -471,7 +468,7 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, } // TODO: should the CSR be customizable? - csr, err := generateCsr(privKey.(*rsa.PrivateKey), commonName.Domain, san) + csr, err := generateCsr(privKey, commonName.Domain, san) if err != nil { return CertificateResource{}, err } diff --git a/acme/crypto.go b/acme/crypto.go index 5ce5ea5f..78a87847 100644 --- a/acme/crypto.go +++ b/acme/crypto.go @@ -25,12 +25,16 @@ import ( "golang.org/x/crypto/sha3" ) -type keyType int +// KeyType represents the key algo as well as the key size or curve to use. +type KeyType string type derCertificateBytes []byte const ( - eckey keyType = iota - rsakey + EC256 = KeyType("P256") + EC384 = KeyType("P348") + RSA2048 = KeyType("2048") + RSA4096 = KeyType("4096") + RSA8192 = KeyType("8192") ) const ( @@ -123,8 +127,16 @@ func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) { } 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 := keyAsJWK(key) + jwk := keyAsJWK(publicKey) if jwk == nil { return "", errors.New("Could not generate JWK from key.") } @@ -217,18 +229,25 @@ func parsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) { } } -func generatePrivateKey(t keyType, keyLength int) (crypto.PrivateKey, error) { - switch t { - case eckey: +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 rsakey: - return rsa.GenerateKey(rand.Reader, keyLength) + 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: %d", t) + return nil, fmt.Errorf("Invalid KeyType: %s", keyType) } -func generateCsr(privateKey *rsa.PrivateKey, domain string, san []string) ([]byte, error) { +func generateCsr(privateKey crypto.PrivateKey, domain string, san []string) ([]byte, error) { template := x509.CertificateRequest{ Subject: pkix.Name{ CommonName: domain, @@ -245,6 +264,9 @@ func generateCsr(privateKey *rsa.PrivateKey, domain string, san []string) ([]byt 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 diff --git a/acme/dns_challenge.go b/acme/dns_challenge.go index 4cd58f50..d187f63b 100644 --- a/acme/dns_challenge.go +++ b/acme/dns_challenge.go @@ -45,7 +45,7 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error { } // Generate the Key Authorization for the challenge - keyAuth, err := getKeyAuthorization(chlng.Token, &s.jws.privKey.PublicKey) + keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey) if err != nil { return err } diff --git a/acme/http_challenge.go b/acme/http_challenge.go index 1cc1f6e1..95cb1fd8 100644 --- a/acme/http_challenge.go +++ b/acme/http_challenge.go @@ -21,7 +21,7 @@ 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.PublicKey) + keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey) if err != nil { return err } diff --git a/acme/jws.go b/acme/jws.go index b676fe39..78d82724 100644 --- a/acme/jws.go +++ b/acme/jws.go @@ -2,7 +2,9 @@ package acme import ( "bytes" + "crypto" "crypto/ecdsa" + "crypto/elliptic" "crypto/rsa" "fmt" "net/http" @@ -12,7 +14,7 @@ import ( type jws struct { directoryURL string - privKey *rsa.PrivateKey + privKey crypto.PrivateKey nonces []string } @@ -46,8 +48,20 @@ func (j *jws) post(url string, content []byte) (*http.Response, error) { } func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) { - // TODO: support other algorithms - RS512 - signer, err := jose.NewSigner(jose.RS256, j.privKey) + + 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 + } + } + + signer, err := jose.NewSigner(alg, j.privKey) if err != nil { return nil, err } diff --git a/acme/messages.go b/acme/messages.go index 55e54321..d238df81 100644 --- a/acme/messages.go +++ b/acme/messages.go @@ -28,17 +28,13 @@ type registrationMessage struct { // Registration is returned by the ACME server after the registration // The client implementation should save this registration somewhere. type Registration struct { - Resource string `json:"resource,omitempty"` - ID int `json:"id"` - Key struct { - Kty string `json:"kty"` - N string `json:"n"` - E string `json:"e"` - } `json:"key"` - Contact []string `json:"contact"` - Agreement string `json:"agreement,omitempty"` - Authorizations string `json:"authorizations,omitempty"` - Certificates string `json:"certificates,omitempty"` + Resource string `json:"resource,omitempty"` + ID int `json:"id"` + Key jose.JsonWebKey `json:"key"` + Contact []string `json:"contact"` + Agreement string `json:"agreement,omitempty"` + Authorizations string `json:"authorizations,omitempty"` + Certificates string `json:"certificates,omitempty"` // RecoveryKey recoveryKeyMessage `json:"recoveryKey,omitempty"` } diff --git a/acme/tls_sni_challenge.go b/acme/tls_sni_challenge.go index dca886bd..c36f6acc 100644 --- a/acme/tls_sni_challenge.go +++ b/acme/tls_sni_challenge.go @@ -22,7 +22,7 @@ func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error { logf("[INFO][%s] acme: Trying to solve TLS-SNI-01", domain) // Generate the Key Authorization for the challenge - keyAuth, err := getKeyAuthorization(chlng.Token, &t.jws.privKey.PublicKey) + keyAuth, err := getKeyAuthorization(chlng.Token, t.jws.privKey) if err != nil { return err } @@ -43,7 +43,7 @@ func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error { // TLSSNI01ChallengeCert returns a certificate for the `tls-sni-01` challenge func TLSSNI01ChallengeCert(keyAuth string) (tls.Certificate, error) { // generate a new RSA key for the certificates - tempPrivKey, err := generatePrivateKey(rsakey, 2048) + tempPrivKey, err := generatePrivateKey(RSA2048) if err != nil { return tls.Certificate{}, err } diff --git a/cli.go b/cli.go index 97d3a816..893d1cda 100644 --- a/cli.go +++ b/cli.go @@ -92,10 +92,10 @@ func main() { Name: "accept-tos, a", Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.", }, - cli.IntFlag{ - Name: "rsa-key-size, B", - Value: 2048, - Usage: "Size of the RSA key.", + cli.StringFlag{ + Name: "key-type, k", + Value: "rsa2048", + Usage: "Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384", }, cli.StringFlag{ Name: "path", diff --git a/cli_handlers.go b/cli_handlers.go index e6e71cbe..cdf4dca8 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -34,7 +34,12 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { //TODO: move to account struct? Currently MUST pass email. acc := NewAccount(c.GlobalString("email"), conf) - client, err := acme.NewClient(c.GlobalString("server"), acc, conf.RsaBits()) + keyType, err := conf.KeyType() + if err != nil { + logger().Fatal(err.Error()) + } + + client, err := acme.NewClient(c.GlobalString("server"), acc, keyType) if err != nil { logger().Fatalf("Could not create client: %s", err.Error()) } diff --git a/configuration.go b/configuration.go index cbb157f3..a437fd56 100644 --- a/configuration.go +++ b/configuration.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "net/url" "os" "path" @@ -20,9 +21,22 @@ func NewConfiguration(c *cli.Context) *Configuration { return &Configuration{context: c} } -// RsaBits returns the current set RSA bit length for private keys -func (c *Configuration) RsaBits() int { - return c.context.GlobalInt("rsa-key-size") +// KeyType the type from which private keys should be generated +func (c *Configuration) KeyType() (acme.KeyType, error) { + switch strings.ToUpper(c.context.GlobalString("key-type")) { + case "RSA2048": + return acme.RSA2048, nil + case "RSA4096": + return acme.RSA4096, nil + case "RSA8192": + return acme.RSA8192, nil + case "EC256": + return acme.EC256, nil + case "EC384": + return acme.EC384, nil + } + + return "", fmt.Errorf("Unsupported KeyType: %s", c.context.GlobalString("key-type")) } // ExcludedSolvers is a list of solvers that are to be excluded. diff --git a/crypto.go b/crypto.go index 3644ed99..684b1100 100644 --- a/crypto.go +++ b/crypto.go @@ -1,21 +1,30 @@ package main import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" - "crypto/rsa" "crypto/x509" "encoding/pem" + "errors" "io/ioutil" "os" ) -func generateRsaKey(length int, file string) (*rsa.PrivateKey, error) { - privateKey, err := rsa.GenerateKey(rand.Reader, length) +func generatePrivateKey(file string) (crypto.PrivateKey, error) { + + privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) if err != nil { return nil, err } - pemKey := pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)} + keyBytes, err := x509.MarshalECPrivateKey(privateKey) + if err != nil { + return nil, err + } + + pemKey := pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes} certOut, err := os.Create(file) if err != nil { @@ -28,12 +37,20 @@ func generateRsaKey(length int, file string) (*rsa.PrivateKey, error) { return privateKey, nil } -func loadRsaKey(file string) (*rsa.PrivateKey, error) { +func loadPrivateKey(file string) (crypto.PrivateKey, error) { keyBytes, err := ioutil.ReadFile(file) if err != nil { return nil, err } keyBlock, _ := pem.Decode(keyBytes) - return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) + + switch keyBlock.Type { + case "RSA PRIVATE KEY": + x509.ParsePKCS1PrivateKey(keyBlock.Bytes) + case "EC PRIVATE KEY": + return x509.ParseECPrivateKey(keyBlock.Bytes) + } + + return nil, errors.New("Unknown private key type.") } From 1f777a0d774894b29681a44317bbd7d608d1ed2a Mon Sep 17 00:00:00 2001 From: xenolf Date: Wed, 27 Jan 2016 02:01:58 +0100 Subject: [PATCH 26/43] Adapt tests to EC changes --- acme/client_test.go | 16 +++++++++------- acme/crypto_test.go | 9 +++++---- acme/dns_challenge_test.go | 5 +++-- acme/http_challenge_test.go | 9 +++++---- acme/tls_sni_challenge_test.go | 9 +++++---- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/acme/client_test.go b/acme/client_test.go index 9ba165ed..49824735 100644 --- a/acme/client_test.go +++ b/acme/client_test.go @@ -1,6 +1,7 @@ package acme import ( + "crypto" "crypto/rand" "crypto/rsa" "encoding/json" @@ -13,6 +14,7 @@ import ( func TestNewClient(t *testing.T) { keyBits := 32 // small value keeps test fast + keyType := RSA2048 key, err := rsa.GenerateKey(rand.Reader, keyBits) if err != nil { t.Fatal("Could not generate test key:", err) @@ -28,7 +30,7 @@ func TestNewClient(t *testing.T) { w.Write(data) })) - client, err := NewClient(ts.URL, user, keyBits) + client, err := NewClient(ts.URL, user, keyType) if err != nil { t.Fatalf("Could not create client: %v", err) } @@ -40,8 +42,8 @@ func TestNewClient(t *testing.T) { t.Errorf("Expected jws.privKey to be %p but was %p", expected, actual) } - if client.keyBits != keyBits { - t.Errorf("Expected keyBits to be %d but was %d", keyBits, client.keyBits) + if client.keyType != keyType { + t.Errorf("Expected keyBits to be %d but was %d", keyType, client.keyType) } if expected, actual := 2, len(client.solvers); actual != expected { @@ -68,7 +70,7 @@ func TestClientOptPort(t *testing.T) { optPort := "1234" optHost := "" - client, err := NewClient(ts.URL, user, keyBits) + client, err := NewClient(ts.URL, user, RSA2048) if err != nil { t.Fatalf("Could not create client: %v", err) } @@ -140,8 +142,8 @@ func TestValidate(t *testing.T) { })) defer ts.Close() - privKey, _ := generatePrivateKey(rsakey, 512) - j := &jws{privKey: privKey.(*rsa.PrivateKey), directoryURL: ts.URL} + privKey, _ := rsa.GenerateKey(rand.Reader, 512) + j := &jws{privKey: privKey, directoryURL: ts.URL} tsts := []struct { name string @@ -193,4 +195,4 @@ type mockUser struct { func (u mockUser) GetEmail() string { return u.email } func (u mockUser) GetRegistration() *RegistrationResource { return u.regres } -func (u mockUser) GetPrivateKey() *rsa.PrivateKey { return u.privatekey } +func (u mockUser) GetPrivateKey() crypto.PrivateKey { return u.privatekey } diff --git a/acme/crypto_test.go b/acme/crypto_test.go index 81ab287e..d2fc5088 100644 --- a/acme/crypto_test.go +++ b/acme/crypto_test.go @@ -2,13 +2,14 @@ package acme import ( "bytes" + "crypto/rand" "crypto/rsa" "testing" "time" ) func TestGeneratePrivateKey(t *testing.T) { - key, err := generatePrivateKey(rsakey, 32) + key, err := generatePrivateKey(RSA2048) if err != nil { t.Error("Error generating private key:", err) } @@ -18,12 +19,12 @@ func TestGeneratePrivateKey(t *testing.T) { } func TestGenerateCSR(t *testing.T) { - key, err := generatePrivateKey(rsakey, 512) + key, err := rsa.GenerateKey(rand.Reader, 512) if err != nil { t.Fatal("Error generating private key:", err) } - csr, err := generateCsr(key.(*rsa.PrivateKey), "fizz.buzz", nil) + csr, err := generateCsr(key, "fizz.buzz", nil) if err != nil { t.Error("Error generating CSR:", err) } @@ -52,7 +53,7 @@ func TestPEMEncode(t *testing.T) { } func TestPEMCertExpiration(t *testing.T) { - privKey, err := generatePrivateKey(rsakey, 2048) + privKey, err := generatePrivateKey(RSA2048) if err != nil { t.Fatal("Error generating private key:", err) } diff --git a/acme/dns_challenge_test.go b/acme/dns_challenge_test.go index e1e67efe..850a0f59 100644 --- a/acme/dns_challenge_test.go +++ b/acme/dns_challenge_test.go @@ -2,6 +2,7 @@ package acme import ( "bufio" + "crypto/rand" "crypto/rsa" "net/http" "net/http/httptest" @@ -76,7 +77,7 @@ func TestDNSValidServerResponse(t *testing.T) { preCheckDNS = func(fqdn, value string) (bool, error) { return true, nil } - privKey, _ := generatePrivateKey(rsakey, 512) + privKey, _ := rsa.GenerateKey(rand.Reader, 512) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Replay-Nonce", "12345") @@ -84,7 +85,7 @@ func TestDNSValidServerResponse(t *testing.T) { })) manualProvider, _ := NewDNSProviderManual() - jws := &jws{privKey: privKey.(*rsa.PrivateKey), directoryURL: ts.URL} + jws := &jws{privKey: privKey, directoryURL: ts.URL} solver := &dnsChallenge{jws: jws, validate: validate, provider: manualProvider} clientChallenge := challenge{Type: "dns01", Status: "pending", URI: ts.URL, Token: "http8"} diff --git a/acme/http_challenge_test.go b/acme/http_challenge_test.go index 79b8b545..fdd8f4d2 100644 --- a/acme/http_challenge_test.go +++ b/acme/http_challenge_test.go @@ -1,6 +1,7 @@ package acme import ( + "crypto/rand" "crypto/rsa" "io/ioutil" "strings" @@ -8,8 +9,8 @@ import ( ) func TestHTTPChallenge(t *testing.T) { - privKey, _ := generatePrivateKey(rsakey, 512) - j := &jws{privKey: privKey.(*rsa.PrivateKey)} + privKey, _ := rsa.GenerateKey(rand.Reader, 512) + j := &jws{privKey: privKey} clientChallenge := challenge{Type: HTTP01, Token: "http1"} mockValidate := func(_ *jws, _, _ string, chlng challenge) error { uri := "http://localhost:23457/.well-known/acme-challenge/" + chlng.Token @@ -43,8 +44,8 @@ func TestHTTPChallenge(t *testing.T) { } func TestHTTPChallengeInvalidPort(t *testing.T) { - privKey, _ := generatePrivateKey(rsakey, 128) - j := &jws{privKey: privKey.(*rsa.PrivateKey)} + privKey, _ := rsa.GenerateKey(rand.Reader, 128) + j := &jws{privKey: privKey} clientChallenge := challenge{Type: HTTP01, Token: "http2"} solver := &httpChallenge{jws: j, validate: stubValidate, provider: &HTTPProviderServer{port: "123456"}} diff --git a/acme/tls_sni_challenge_test.go b/acme/tls_sni_challenge_test.go index 60f1498b..3aec7456 100644 --- a/acme/tls_sni_challenge_test.go +++ b/acme/tls_sni_challenge_test.go @@ -1,6 +1,7 @@ package acme import ( + "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/tls" @@ -11,8 +12,8 @@ import ( ) func TestTLSSNIChallenge(t *testing.T) { - privKey, _ := generatePrivateKey(rsakey, 512) - j := &jws{privKey: privKey.(*rsa.PrivateKey)} + privKey, _ := rsa.GenerateKey(rand.Reader, 512) + j := &jws{privKey: privKey} clientChallenge := challenge{Type: TLSSNI01, Token: "tlssni1"} mockValidate := func(_ *jws, _, _ string, chlng challenge) error { conn, err := tls.Dial("tcp", "localhost:23457", &tls.Config{ @@ -51,8 +52,8 @@ func TestTLSSNIChallenge(t *testing.T) { } func TestTLSSNIChallengeInvalidPort(t *testing.T) { - privKey, _ := generatePrivateKey(rsakey, 128) - j := &jws{privKey: privKey.(*rsa.PrivateKey)} + privKey, _ := rsa.GenerateKey(rand.Reader, 128) + j := &jws{privKey: privKey} clientChallenge := challenge{Type: TLSSNI01, Token: "tlssni2"} solver := &tlsSNIChallenge{jws: j, validate: stubValidate, provider: &TLSProviderServer{port: "123456"}} From d46b0db199079c884fbca77f5928c0e7c3f2ab0c Mon Sep 17 00:00:00 2001 From: xenolf Date: Sat, 6 Feb 2016 22:41:29 +0100 Subject: [PATCH 27/43] Fix missing return in loadPrivateKey --- crypto.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto.go b/crypto.go index 684b1100..8b23e2fc 100644 --- a/crypto.go +++ b/crypto.go @@ -47,7 +47,7 @@ func loadPrivateKey(file string) (crypto.PrivateKey, error) { switch keyBlock.Type { case "RSA PRIVATE KEY": - x509.ParsePKCS1PrivateKey(keyBlock.Bytes) + return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) case "EC PRIVATE KEY": return x509.ParseECPrivateKey(keyBlock.Bytes) } From a61e41c90e4f062c39647784908661c4b631f2b4 Mon Sep 17 00:00:00 2001 From: xenolf Date: Sun, 14 Feb 2016 22:31:17 +0100 Subject: [PATCH 28/43] Fix typo in the constant for the P384 curve. --- acme/crypto.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/acme/crypto.go b/acme/crypto.go index 78a87847..c46310f1 100644 --- a/acme/crypto.go +++ b/acme/crypto.go @@ -29,9 +29,10 @@ import ( type KeyType string type derCertificateBytes []byte +// Constants for all key types we support. const ( EC256 = KeyType("P256") - EC384 = KeyType("P348") + EC384 = KeyType("P384") RSA2048 = KeyType("2048") RSA4096 = KeyType("4096") RSA8192 = KeyType("8192") From c9e1d0a482ee02b18c80843a62bd94f19545c227 Mon Sep 17 00:00:00 2001 From: xenolf Date: Sun, 21 Feb 2016 04:22:03 +0100 Subject: [PATCH 29/43] Remove keyBits from tests, use keyType instead. --- acme/client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/client_test.go b/acme/client_test.go index 49824735..e309554f 100644 --- a/acme/client_test.go +++ b/acme/client_test.go @@ -43,7 +43,7 @@ func TestNewClient(t *testing.T) { } if client.keyType != keyType { - t.Errorf("Expected keyBits to be %d but was %d", keyType, client.keyType) + t.Errorf("Expected keyType to be %s but was %s", keyType, client.keyType) } if expected, actual := 2, len(client.solvers); actual != expected { From da7dd0f7b877b4bd9dd6da52d4348940d1ed664e Mon Sep 17 00:00:00 2001 From: xenolf Date: Sun, 21 Feb 2016 04:31:02 +0100 Subject: [PATCH 30/43] Remove no longer needed crypto function. ACME spec no longer requires this. --- acme/crypto.go | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/acme/crypto.go b/acme/crypto.go index 5ce5ea5f..a3181723 100644 --- a/acme/crypto.go +++ b/acme/crypto.go @@ -10,7 +10,6 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/base64" - "encoding/binary" "encoding/pem" "errors" "fmt" @@ -22,7 +21,6 @@ import ( "time" "golang.org/x/crypto/ocsp" - "golang.org/x/crypto/sha3" ) type keyType int @@ -143,39 +141,6 @@ func getKeyAuthorization(token string, key interface{}) (string, error) { return token + "." + keyThumb, nil } -// Derive the shared secret according to acme spec 5.6 -func performECDH(priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, outLen int, label string) []byte { - // Derive Z from the private and public keys according to SEC 1 Ver. 2.0 - 3.3.1 - Z, _ := priv.PublicKey.ScalarMult(pub.X, pub.Y, priv.D.Bytes()) - - if len(Z.Bytes())+len(label)+4 > 384 { - return nil - } - - if outLen < 384*(2^32-1) { - return nil - } - - // Derive the shared secret key using the ANS X9.63 KDF - SEC 1 Ver. 2.0 - 3.6.1 - hasher := sha3.New384() - buffer := make([]byte, outLen) - bufferLen := 0 - for i := 0; i < outLen/384; i++ { - hasher.Reset() - - // Ki = Hash(Z || Counter || [SharedInfo]) - hasher.Write(Z.Bytes()) - binary.Write(hasher, binary.BigEndian, i) - hasher.Write([]byte(label)) - - hash := hasher.Sum(nil) - copied := copy(buffer[bufferLen:], hash) - bufferLen += copied - } - - return buffer -} - // 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) { From ec18e5ce0755d5d1187e29014ca08836dee61f10 Mon Sep 17 00:00:00 2001 From: Pauline Middelink Date: Fri, 26 Feb 2016 02:52:13 +0100 Subject: [PATCH 31/43] Unneeded assignment --- acme/client.go | 1 - 1 file changed, 1 deletion(-) diff --git a/acme/client.go b/acme/client.go index be9843e2..690b440c 100644 --- a/acme/client.go +++ b/acme/client.go @@ -322,7 +322,6 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle bool) (Certif // Success - append the issuer cert to the issued cert. issuerCert = pemEncode(derCertificateBytes(issuerCert)) issuedCert = append(issuedCert, issuerCert...) - cert.Certificate = issuedCert } } From 6b0be6de614d2bd929ffcaa24f4b76c809aa11f4 Mon Sep 17 00:00:00 2001 From: Pauline Middelink Date: Fri, 26 Feb 2016 02:56:17 +0100 Subject: [PATCH 32/43] Update help+README for missing RFC2136_TSIG_ALGORITHM environment setting. --- README.md | 2 +- cli.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9d1807aa..c75a2e85 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ GLOBAL OPTIONS: digitalocean: DO_AUTH_TOKEN dnsimple: DNSIMPLE_EMAIL, DNSIMPLE_API_KEY route53: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION - rfc2136: RFC2136_TSIG_KEY, RFC2136_TSIG_SECRET, RFC2136_NAMESERVER, RFC2136_ZONE + rfc2136: RFC2136_TSIG_KEY, RFC2136_TSIG_SECRET, RFC2136_TSIG_ALGORITHM, RFC2136_NAMESERVER, RFC2136_ZONE manual: none --help, -h show help --version, -v print the version diff --git a/cli.go b/cli.go index 97d3a816..e08f809c 100644 --- a/cli.go +++ b/cli.go @@ -124,7 +124,7 @@ func main() { "\n\tdigitalocean: DO_AUTH_TOKEN" + "\n\tdnsimple: DNSIMPLE_EMAIL, DNSIMPLE_API_KEY" + "\n\troute53: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION" + - "\n\trfc2136: RFC2136_TSIG_KEY, RFC2136_TSIG_SECRET, RFC2136_NAMESERVER, RFC2136_ZONE" + + "\n\trfc2136: RFC2136_TSIG_KEY, RFC2136_TSIG_SECRET, RFC2136_TSIG_ALGORITHM, RFC2136_NAMESERVER, RFC2136_ZONE" + "\n\tmanual: none", }, } From 96762fa6ba5b14debae825ea1ed1f984aee02f30 Mon Sep 17 00:00:00 2001 From: Pauline Middelink Date: Fri, 26 Feb 2016 02:57:16 +0100 Subject: [PATCH 33/43] Add --nobundle flag to supress the default creation of certificate bundle. --- cli.go | 10 ++++++++++ cli_handlers.go | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/cli.go b/cli.go index e08f809c..a02ad78e 100644 --- a/cli.go +++ b/cli.go @@ -50,6 +50,12 @@ func main() { Name: "run", Usage: "Register an account, then create and install a certificate", Action: run, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "nobundle", + Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.", + }, + }, }, { Name: "revoke", @@ -70,6 +76,10 @@ func main() { Name: "reuse-key", Usage: "Used to indicate you want to reuse your current private key for the new certificate.", }, + cli.BoolFlag{ + Name: "nobundle", + Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.", + }, }, }, } diff --git a/cli_handlers.go b/cli_handlers.go index e6e71cbe..2b614ade 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -198,7 +198,7 @@ func run(c *cli.Context) { logger().Fatal("Please specify --domains or -d") } - cert, failures := client.ObtainCertificate(c.GlobalStringSlice("domains"), true, nil) + cert, failures := client.ObtainCertificate(c.GlobalStringSlice("domains"), !c.Bool("nobundle"), nil) if len(failures) > 0 { for k, v := range failures { logger().Printf("[%s] Could not obtain certificates\n\t%s", k, v.Error()) @@ -295,7 +295,7 @@ func renew(c *cli.Context) { certRes.Certificate = certBytes - newCert, err := client.RenewCertificate(certRes, true) + newCert, err := client.RenewCertificate(certRes, !c.Bool("nobundle")) if err != nil { logger().Fatalf("%s", err.Error()) } From 3b56b5a3e29a148f389275030827a33e946d9404 Mon Sep 17 00:00:00 2001 From: Pauline Middelink Date: Sat, 27 Feb 2016 10:46:13 +0100 Subject: [PATCH 34/43] As per request, renamed nobundle to no-bundle to be more in line with the other multi word switches. --- cli.go | 4 ++-- cli_handlers.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cli.go b/cli.go index a02ad78e..b23adc21 100644 --- a/cli.go +++ b/cli.go @@ -52,7 +52,7 @@ func main() { Action: run, Flags: []cli.Flag{ cli.BoolFlag{ - Name: "nobundle", + Name: "no-bundle", Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.", }, }, @@ -77,7 +77,7 @@ func main() { Usage: "Used to indicate you want to reuse your current private key for the new certificate.", }, cli.BoolFlag{ - Name: "nobundle", + Name: "no-bundle", Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.", }, }, diff --git a/cli_handlers.go b/cli_handlers.go index 2b614ade..32f4a8f4 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -198,7 +198,7 @@ func run(c *cli.Context) { logger().Fatal("Please specify --domains or -d") } - cert, failures := client.ObtainCertificate(c.GlobalStringSlice("domains"), !c.Bool("nobundle"), nil) + cert, failures := client.ObtainCertificate(c.GlobalStringSlice("domains"), !c.Bool("no-bundle"), nil) if len(failures) > 0 { for k, v := range failures { logger().Printf("[%s] Could not obtain certificates\n\t%s", k, v.Error()) @@ -295,7 +295,7 @@ func renew(c *cli.Context) { certRes.Certificate = certBytes - newCert, err := client.RenewCertificate(certRes, !c.Bool("nobundle")) + newCert, err := client.RenewCertificate(certRes, !c.Bool("no-bundle")) if err != nil { logger().Fatalf("%s", err.Error()) } From e772779cafafbfb96620798e6eac4893d1e02ef0 Mon Sep 17 00:00:00 2001 From: Pauline Middelink Date: Sat, 27 Feb 2016 23:50:42 +0100 Subject: [PATCH 35/43] Fix for issue/140: - Removal of RFC2136_ZONE from help text - Query nameserver directly to find zone we have to update - During insert, make sure the new record is the ONLY challence. (I had a few panics, hence 3 challences left. Not good.) --- README.md | 2 +- acme/dns_challenge_rfc2136.go | 78 +++++++++++++++++++++++++++++------ cli.go | 2 +- cli_handlers.go | 3 +- 4 files changed, 69 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 9d1807aa..b03d776c 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ GLOBAL OPTIONS: digitalocean: DO_AUTH_TOKEN dnsimple: DNSIMPLE_EMAIL, DNSIMPLE_API_KEY route53: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION - rfc2136: RFC2136_TSIG_KEY, RFC2136_TSIG_SECRET, RFC2136_NAMESERVER, RFC2136_ZONE + rfc2136: RFC2136_TSIG_KEY, RFC2136_TSIG_SECRET, RFC2136_NAMESERVER manual: none --help, -h show help --version, -v print the version diff --git a/acme/dns_challenge_rfc2136.go b/acme/dns_challenge_rfc2136.go index e2954d25..cab76817 100644 --- a/acme/dns_challenge_rfc2136.go +++ b/acme/dns_challenge_rfc2136.go @@ -12,26 +12,23 @@ import ( // uses dynamic DNS updates (RFC 2136) to create TXT records on a nameserver. type DNSProviderRFC2136 struct { nameserver string - zone string tsigAlgorithm string tsigKey string tsigSecret string - records map[string]string + domain2zone map[string]string } // NewDNSProviderRFC2136 returns a new DNSProviderRFC2136 instance. // To disable TSIG authentication 'tsigAlgorithm, 'tsigKey' and 'tsigSecret' must be set to the empty string. -// 'nameserver' must be a network address in the the form "host" or "host:port". 'zone' must be the fully -// qualified name of the zone. -func NewDNSProviderRFC2136(nameserver, zone, tsigAlgorithm, tsigKey, tsigSecret string) (*DNSProviderRFC2136, error) { +// 'nameserver' must be a network address in the the form "host" or "host:port". +func NewDNSProviderRFC2136(nameserver, tsigAlgorithm, tsigKey, tsigSecret string) (*DNSProviderRFC2136, error) { // Append the default DNS port if none is specified. if !strings.Contains(nameserver, ":") { nameserver += ":53" } d := &DNSProviderRFC2136{ - nameserver: nameserver, - zone: zone, - records: make(map[string]string), + nameserver: nameserver, + domain2zone: make(map[string]string), } if tsigAlgorithm == "" { tsigAlgorithm = dns.HmacMD5 @@ -48,18 +45,21 @@ func NewDNSProviderRFC2136(nameserver, zone, tsigAlgorithm, tsigKey, tsigSecret // Present creates a TXT record using the specified parameters func (r *DNSProviderRFC2136) Present(domain, token, keyAuth string) error { fqdn, value, ttl := DNS01Record(domain, keyAuth) - r.records[fqdn] = value return r.changeRecord("INSERT", fqdn, value, ttl) } // CleanUp removes the TXT record matching the specified parameters func (r *DNSProviderRFC2136) CleanUp(domain, token, keyAuth string) error { - fqdn, _, ttl := DNS01Record(domain, keyAuth) - value := r.records[fqdn] + fqdn, value, ttl := DNS01Record(domain, keyAuth) return r.changeRecord("REMOVE", fqdn, value, ttl) } func (r *DNSProviderRFC2136) changeRecord(action, fqdn, value string, ttl int) error { + zone, err := r.findZone(fqdn) + if err != nil { + return err + } + // Create RR rr := new(dns.TXT) rr.Hdr = dns.RR_Header{Name: fqdn, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: uint32(ttl)} @@ -69,9 +69,11 @@ func (r *DNSProviderRFC2136) changeRecord(action, fqdn, value string, ttl int) e // Create dynamic update packet m := new(dns.Msg) - m.SetUpdate(dns.Fqdn(r.zone)) + m.SetUpdate(zone) switch action { case "INSERT": + // Always remove old challenge left over from who knows what. + m.RemoveRRset(rrs) m.Insert(rrs) case "REMOVE": m.Remove(rrs) @@ -99,3 +101,55 @@ func (r *DNSProviderRFC2136) changeRecord(action, fqdn, value string, ttl int) e return nil } + +// findZone determines the zone of a qualifying cert DNSname +func (r *DNSProviderRFC2136) findZone(fqdn string) (string, error) { + // Do we have it cached? + if val, ok := r.domain2zone[fqdn]; ok { + return val, nil + } + + // Query the authorative nameserver for a hopefully non-existing SOA record, + // in the authority section of the reply it will have the SOA of the + // containing zone. rfc2308 has this to say on the subject: + // Name servers authoritative for a zone MUST include the SOA record of + // the zone in the authority section of the response when reporting an + // NXDOMAIN or indicating that no data (NODATA) of the requested type exists + m := new(dns.Msg) + m.SetQuestion(fqdn, dns.TypeSOA) + m.SetEdns0(4096, false) + m.RecursionDesired = true + m.Authoritative = true + + in, err := dns.Exchange(m, r.nameserver) + if err == dns.ErrTruncated { + tcp := &dns.Client{Net: "tcp"} + in, _, err = tcp.Exchange(m, r.nameserver) + } + if err != nil { + return "", err + } + if in.Rcode != dns.RcodeNameError { + if in.Rcode != dns.RcodeSuccess { + return "", fmt.Errorf("DNS Query for zone %q failed", fqdn) + } + // We have a success, so one of the answers has to be a SOA RR + for _, ans := range in.Answer { + if ans.Header().Rrtype == dns.TypeSOA { + zone := ans.Header().Name + r.domain2zone[fqdn] = zone + return zone, nil + } + } + // Or it is NODATA, fall through to NXDOMAIN + } + // Search the authority section for our precious SOA RR + for _, ns := range in.Ns { + if ns.Header().Rrtype == dns.TypeSOA { + zone := ns.Header().Name + r.domain2zone[fqdn] = zone + return zone, nil + } + } + return "", fmt.Errorf("Expected a SOA record in the authority section") +} diff --git a/cli.go b/cli.go index 893d1cda..91bef0cd 100644 --- a/cli.go +++ b/cli.go @@ -124,7 +124,7 @@ func main() { "\n\tdigitalocean: DO_AUTH_TOKEN" + "\n\tdnsimple: DNSIMPLE_EMAIL, DNSIMPLE_API_KEY" + "\n\troute53: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION" + - "\n\trfc2136: RFC2136_TSIG_KEY, RFC2136_TSIG_SECRET, RFC2136_NAMESERVER, RFC2136_ZONE" + + "\n\trfc2136: RFC2136_TSIG_KEY, RFC2136_TSIG_SECRET, RFC2136_NAMESERVER" + "\n\tmanual: none", }, } diff --git a/cli_handlers.go b/cli_handlers.go index cdf4dca8..3a8d3d6b 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -79,12 +79,11 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { provider, err = acme.NewDNSProviderRoute53("", "", awsRegion) case "rfc2136": nameserver := os.Getenv("RFC2136_NAMESERVER") - zone := os.Getenv("RFC2136_ZONE") tsigAlgorithm := os.Getenv("RFC2136_TSIG_ALGORITHM") tsigKey := os.Getenv("RFC2136_TSIG_KEY") tsigSecret := os.Getenv("RFC2136_TSIG_SECRET") - provider, err = acme.NewDNSProviderRFC2136(nameserver, zone, tsigAlgorithm, tsigKey, tsigSecret) + provider, err = acme.NewDNSProviderRFC2136(nameserver, tsigAlgorithm, tsigKey, tsigSecret) case "manual": provider, err = acme.NewDNSProviderManual() } From 4945919c69977bda1ae9c1f2181a6102e11a6b74 Mon Sep 17 00:00:00 2001 From: Pauline Middelink Date: Sun, 28 Feb 2016 21:09:05 +0100 Subject: [PATCH 36/43] - Moved findZone from rfc2136 to dns_challenge.go and renamed to findZoneByFqdn - Reworked the code in dns_challenge.go to not assume nameserver is port-less or defaults to 53. (messes up testing) - Updated nameserver test to clear the fqdn2zone cache and return a dummy SOA RR to make initial findZoneByFqdn call happy. - Used publicsuffix library to determine if the "authorative" zone we found is a public registry, in that case error out. (Also used by boulder btw) --- acme/dns_challenge.go | 76 +++++++++++++++++++++++++----- acme/dns_challenge_rfc2136.go | 71 +++++----------------------- acme/dns_challenge_rfc2136_test.go | 44 ++++++++++------- acme/dns_challenge_test.go | 4 +- 4 files changed, 106 insertions(+), 89 deletions(-) diff --git a/acme/dns_challenge.go b/acme/dns_challenge.go index d187f63b..bf837760 100644 --- a/acme/dns_challenge.go +++ b/acme/dns_challenge.go @@ -11,13 +11,17 @@ import ( "time" "github.com/miekg/dns" + "golang.org/x/net/publicsuffix" ) type preCheckDNSFunc func(fqdn, value string) (bool, error) -var preCheckDNS preCheckDNSFunc = checkDNSPropagation +var ( + preCheckDNS preCheckDNSFunc = checkDNSPropagation + fqdn2zone = map[string]string{} +) -var recursiveNameserver = "google-public-dns-a.google.com" +var recursiveNameserver = "google-public-dns-a.google.com:53" // DNS01Record returns a DNS record which will fulfill the `dns-01` challenge func DNS01Record(domain, keyAuth string) (fqdn string, value string, ttl int) { @@ -105,7 +109,7 @@ func checkDNSPropagation(fqdn, value string) (bool, error) { // 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, ns, false) + r, err := dnsQuery(fqdn, dns.TypeTXT, net.JoinHostPort(ns, "53"), false) if err != nil { return false, err } @@ -133,6 +137,7 @@ func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, erro } // dnsQuery sends a DNS query to the given nameserver. +// The nameserver should include a port, to facilitate testing where we talk to a mock dns server. func dnsQuery(fqdn string, rtype uint16, nameserver string, recursive bool) (in *dns.Msg, err error) { m := new(dns.Msg) m.SetQuestion(fqdn, rtype) @@ -142,7 +147,7 @@ func dnsQuery(fqdn string, rtype uint16, nameserver string, recursive bool) (in m.RecursionDesired = false } - in, err = dns.Exchange(m, net.JoinHostPort(nameserver, "53")) + in, err = dns.Exchange(m, nameserver) if err == dns.ErrTruncated { tcp := &dns.Client{Net: "tcp"} in, _, err = tcp.Exchange(m, nameserver) @@ -155,7 +160,12 @@ func dnsQuery(fqdn string, rtype uint16, nameserver string, recursive bool) (in func lookupNameservers(fqdn string) ([]string, error) { var authoritativeNss []string - r, err := dnsQuery(fqdn, dns.TypeNS, recursiveNameserver, true) + zone, err := findZoneByFqdn(fqdn, recursiveNameserver) + if err != nil { + return nil, err + } + + r, err := dnsQuery(zone, dns.TypeNS, recursiveNameserver, true) if err != nil { return nil, err } @@ -169,15 +179,59 @@ func lookupNameservers(fqdn string) ([]string, error) { if len(authoritativeNss) > 0 { return authoritativeNss, nil } + return nil, fmt.Errorf("Could not determine authoritative nameservers") +} - // Strip of the left most label to get the parent domain. - offset, _ := dns.NextLabel(fqdn, 0) - next := fqdn[offset:] - if dns.CountLabel(next) < 2 { - return nil, fmt.Errorf("Could not determine authoritative nameservers") +// findZoneByFqdn determines the zone of the given fqdn +func findZoneByFqdn(fqdn, nameserver string) (string, error) { + // Do we have it cached? + if zone, ok := fqdn2zone[fqdn]; ok { + return zone, nil } - return lookupNameservers(next) + // Query the authorative nameserver for a hopefully non-existing SOA record, + // in the authority section of the reply it will have the SOA of the + // containing zone. rfc2308 has this to say on the subject: + // Name servers authoritative for a zone MUST include the SOA record of + // the zone in the authority section of the response when reporting an + // NXDOMAIN or indicating that no data (NODATA) of the requested type exists + in, err := dnsQuery(fqdn, dns.TypeSOA, nameserver, true) + if err != nil { + return "", err + } + if in.Rcode != dns.RcodeNameError { + if in.Rcode != dns.RcodeSuccess { + return "", fmt.Errorf("NS %s returned %s for %s", nameserver, dns.RcodeToString[in.Rcode], fqdn) + } + // We have a success, so one of the answers has to be a SOA RR + for _, ans := range in.Answer { + if soa, ok := ans.(*dns.SOA); ok { + zone := soa.Hdr.Name + // If we ended up on one of the TLDs, it means the domain did not exist. + publicsuffix, _ := publicsuffix.PublicSuffix(unFqdn(zone)) + if publicsuffix == unFqdn(zone) { + return "", fmt.Errorf("Could not determine zone authoritively") + } + fqdn2zone[fqdn] = zone + return zone, nil + } + } + // Or it is NODATA, fall through to NXDOMAIN + } + // Search the authority section for our precious SOA RR + for _, ns := range in.Ns { + if soa, ok := ns.(*dns.SOA); ok { + zone := soa.Hdr.Name + // If we ended up on one of the TLDs, it means the domain did not exist. + publicsuffix, _ := publicsuffix.PublicSuffix(unFqdn(zone)) + if publicsuffix == unFqdn(zone) { + return "", fmt.Errorf("Could not determine zone authoritively") + } + fqdn2zone[fqdn] = zone + return zone, nil + } + } + return "", fmt.Errorf("NS %s did not return the expected SOA record in the authority section", nameserver) } // toFqdn converts the name into a fqdn appending a trailing dot. diff --git a/acme/dns_challenge_rfc2136.go b/acme/dns_challenge_rfc2136.go index cab76817..7c6900de 100644 --- a/acme/dns_challenge_rfc2136.go +++ b/acme/dns_challenge_rfc2136.go @@ -2,6 +2,7 @@ package acme import ( "fmt" + "net" "strings" "time" @@ -15,7 +16,6 @@ type DNSProviderRFC2136 struct { tsigAlgorithm string tsigKey string tsigSecret string - domain2zone map[string]string } // NewDNSProviderRFC2136 returns a new DNSProviderRFC2136 instance. @@ -23,12 +23,15 @@ type DNSProviderRFC2136 struct { // 'nameserver' must be a network address in the the form "host" or "host:port". func NewDNSProviderRFC2136(nameserver, tsigAlgorithm, tsigKey, tsigSecret string) (*DNSProviderRFC2136, error) { // Append the default DNS port if none is specified. - if !strings.Contains(nameserver, ":") { - nameserver += ":53" + if _, _, err := net.SplitHostPort(nameserver); err != nil { + if strings.Contains(err.Error(), "missing port") { + nameserver = net.JoinHostPort(nameserver, "53") + } else { + return nil, err + } } d := &DNSProviderRFC2136{ - nameserver: nameserver, - domain2zone: make(map[string]string), + nameserver: nameserver, } if tsigAlgorithm == "" { tsigAlgorithm = dns.HmacMD5 @@ -55,7 +58,8 @@ func (r *DNSProviderRFC2136) CleanUp(domain, token, keyAuth string) error { } func (r *DNSProviderRFC2136) changeRecord(action, fqdn, value string, ttl int) error { - zone, err := r.findZone(fqdn) + // Find the zone for the given fqdn + zone, err := findZoneByFqdn(fqdn, r.nameserver) if err != nil { return err } @@ -64,8 +68,7 @@ func (r *DNSProviderRFC2136) changeRecord(action, fqdn, value string, ttl int) e rr := new(dns.TXT) rr.Hdr = dns.RR_Header{Name: fqdn, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: uint32(ttl)} rr.Txt = []string{value} - rrs := make([]dns.RR, 1) - rrs[0] = rr + rrs := []dns.RR{rr} // Create dynamic update packet m := new(dns.Msg) @@ -101,55 +104,3 @@ func (r *DNSProviderRFC2136) changeRecord(action, fqdn, value string, ttl int) e return nil } - -// findZone determines the zone of a qualifying cert DNSname -func (r *DNSProviderRFC2136) findZone(fqdn string) (string, error) { - // Do we have it cached? - if val, ok := r.domain2zone[fqdn]; ok { - return val, nil - } - - // Query the authorative nameserver for a hopefully non-existing SOA record, - // in the authority section of the reply it will have the SOA of the - // containing zone. rfc2308 has this to say on the subject: - // Name servers authoritative for a zone MUST include the SOA record of - // the zone in the authority section of the response when reporting an - // NXDOMAIN or indicating that no data (NODATA) of the requested type exists - m := new(dns.Msg) - m.SetQuestion(fqdn, dns.TypeSOA) - m.SetEdns0(4096, false) - m.RecursionDesired = true - m.Authoritative = true - - in, err := dns.Exchange(m, r.nameserver) - if err == dns.ErrTruncated { - tcp := &dns.Client{Net: "tcp"} - in, _, err = tcp.Exchange(m, r.nameserver) - } - if err != nil { - return "", err - } - if in.Rcode != dns.RcodeNameError { - if in.Rcode != dns.RcodeSuccess { - return "", fmt.Errorf("DNS Query for zone %q failed", fqdn) - } - // We have a success, so one of the answers has to be a SOA RR - for _, ans := range in.Answer { - if ans.Header().Rrtype == dns.TypeSOA { - zone := ans.Header().Name - r.domain2zone[fqdn] = zone - return zone, nil - } - } - // Or it is NODATA, fall through to NXDOMAIN - } - // Search the authority section for our precious SOA RR - for _, ns := range in.Ns { - if ns.Header().Rrtype == dns.TypeSOA { - zone := ns.Header().Name - r.domain2zone[fqdn] = zone - return zone, nil - } - } - return "", fmt.Errorf("Expected a SOA record in the authority section") -} diff --git a/acme/dns_challenge_rfc2136_test.go b/acme/dns_challenge_rfc2136_test.go index 9af8f491..b5905556 100644 --- a/acme/dns_challenge_rfc2136_test.go +++ b/acme/dns_challenge_rfc2136_test.go @@ -2,6 +2,7 @@ package acme import ( "bytes" + "fmt" "net" "strings" "sync" @@ -25,6 +26,7 @@ var ( var reqChan = make(chan *dns.Msg, 10) func TestRFC2136CanaryLocalTestServer(t *testing.T) { + fqdn2zone = map[string]string{} dns.HandleFunc("example.com.", serverHandlerHello) defer dns.HandleRemove("example.com.") @@ -48,6 +50,7 @@ func TestRFC2136CanaryLocalTestServer(t *testing.T) { } func TestRFC2136ServerSuccess(t *testing.T) { + fqdn2zone = map[string]string{} dns.HandleFunc(rfc2136TestZone, serverHandlerReturnSuccess) defer dns.HandleRemove(rfc2136TestZone) @@ -57,7 +60,7 @@ func TestRFC2136ServerSuccess(t *testing.T) { } defer server.Shutdown() - provider, err := NewDNSProviderRFC2136(addrstr, rfc2136TestZone, "", "", "") + provider, err := NewDNSProviderRFC2136(addrstr, "", "", "") if err != nil { t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err) } @@ -67,6 +70,7 @@ func TestRFC2136ServerSuccess(t *testing.T) { } func TestRFC2136ServerError(t *testing.T) { + fqdn2zone = map[string]string{} dns.HandleFunc(rfc2136TestZone, serverHandlerReturnErr) defer dns.HandleRemove(rfc2136TestZone) @@ -76,7 +80,7 @@ func TestRFC2136ServerError(t *testing.T) { } defer server.Shutdown() - provider, err := NewDNSProviderRFC2136(addrstr, rfc2136TestZone, "", "", "") + provider, err := NewDNSProviderRFC2136(addrstr, "", "", "") if err != nil { t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err) } @@ -88,6 +92,7 @@ func TestRFC2136ServerError(t *testing.T) { } func TestRFC2136TsigClient(t *testing.T) { + fqdn2zone = map[string]string{} dns.HandleFunc(rfc2136TestZone, serverHandlerReturnSuccess) defer dns.HandleRemove(rfc2136TestZone) @@ -97,7 +102,7 @@ func TestRFC2136TsigClient(t *testing.T) { } defer server.Shutdown() - provider, err := NewDNSProviderRFC2136(addrstr, rfc2136TestZone, "", rfc2136TestTsigKey, rfc2136TestTsigSecret) + provider, err := NewDNSProviderRFC2136(addrstr, "", rfc2136TestTsigKey, rfc2136TestTsigSecret) if err != nil { t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err) } @@ -107,6 +112,7 @@ func TestRFC2136TsigClient(t *testing.T) { } func TestRFC2136ValidUpdatePacket(t *testing.T) { + fqdn2zone = map[string]string{} dns.HandleFunc(rfc2136TestZone, serverHandlerPassBackRequest) defer dns.HandleRemove(rfc2136TestZone) @@ -116,18 +122,11 @@ func TestRFC2136ValidUpdatePacket(t *testing.T) { } defer server.Shutdown() - rr := new(dns.TXT) - rr.Hdr = dns.RR_Header{ - Name: rfc2136TestFqdn, - Rrtype: dns.TypeTXT, - Class: dns.ClassINET, - Ttl: uint32(rfc2136TestTTL), - } - rr.Txt = []string{rfc2136TestValue} - rrs := make([]dns.RR, 1) - rrs[0] = rr + txtRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN TXT %s", rfc2136TestFqdn, rfc2136TestTTL, rfc2136TestValue)) + rrs := []dns.RR{txtRR} m := new(dns.Msg) - m.SetUpdate(dns.Fqdn(rfc2136TestZone)) + m.SetUpdate(rfc2136TestZone) + m.RemoveRRset(rrs) m.Insert(rrs) expectstr := m.String() expect, err := m.Pack() @@ -135,7 +134,7 @@ func TestRFC2136ValidUpdatePacket(t *testing.T) { t.Fatalf("Error packing expect msg: %v", err) } - provider, err := NewDNSProviderRFC2136(addrstr, rfc2136TestZone, "", "", "") + provider, err := NewDNSProviderRFC2136(addrstr, "", "", "") if err != nil { t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err) } @@ -198,6 +197,11 @@ func serverHandlerHello(w dns.ResponseWriter, req *dns.Msg) { func serverHandlerReturnSuccess(w dns.ResponseWriter, req *dns.Msg) { m := new(dns.Msg) m.SetReply(req) + if req.Opcode == dns.OpcodeQuery && req.Question[0].Qtype == dns.TypeSOA && req.Question[0].Qclass == dns.ClassINET { + // Return SOA to appease findZoneByFqdn() + soaRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN SOA ns1.%s admin.%s 2016022801 28800 7200 2419200 1200", rfc2136TestZone, rfc2136TestTTL, rfc2136TestZone, rfc2136TestZone)) + m.Answer = []dns.RR{soaRR} + } if t := req.IsTsig(); t != nil { if w.TsigStatus() == nil { @@ -218,6 +222,11 @@ func serverHandlerReturnErr(w dns.ResponseWriter, req *dns.Msg) { func serverHandlerPassBackRequest(w dns.ResponseWriter, req *dns.Msg) { m := new(dns.Msg) m.SetReply(req) + if req.Opcode == dns.OpcodeQuery && req.Question[0].Qtype == dns.TypeSOA && req.Question[0].Qclass == dns.ClassINET { + // Return SOA to appease findZoneByFqdn() + soaRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN SOA ns1.%s admin.%s 2016022801 28800 7200 2419200 1200", rfc2136TestZone, rfc2136TestTTL, rfc2136TestZone, rfc2136TestZone)) + m.Answer = []dns.RR{soaRR} + } if t := req.IsTsig(); t != nil { if w.TsigStatus() == nil { @@ -227,5 +236,8 @@ func serverHandlerPassBackRequest(w dns.ResponseWriter, req *dns.Msg) { } w.WriteMsg(m) - reqChan <- req + if req.Opcode != dns.OpcodeQuery || req.Question[0].Qtype != dns.TypeSOA || req.Question[0].Qclass != dns.ClassINET { + // Only talk back when it is not the SOA RR. + reqChan <- req + } } diff --git a/acme/dns_challenge_test.go b/acme/dns_challenge_test.go index 850a0f59..84b805f4 100644 --- a/acme/dns_challenge_test.go +++ b/acme/dns_challenge_test.go @@ -35,11 +35,11 @@ var lookupNameserversTestsErr = []struct { }{ // invalid tld {"_null.n0n0.", - "Could not determine authoritative nameservers", + "Could not determine zone authoritively", }, // invalid domain {"_null.com.", - "Could not determine authoritative nameservers", + "Could not determine zone authoritively", }, } From 8b90b1a3801fb7fd4a621fe9782c4441082467b1 Mon Sep 17 00:00:00 2001 From: Pauline Middelink Date: Mon, 29 Feb 2016 08:46:15 +0100 Subject: [PATCH 37/43] Added testcase for in-valid.co.uk Camelcased: fqdn2zone to fqdnToZone Grammatical fix in externally visible error message --- acme/dns_challenge.go | 17 +++++++++++------ acme/dns_challenge_rfc2136_test.go | 10 +++++----- acme/dns_challenge_test.go | 8 ++++++-- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/acme/dns_challenge.go b/acme/dns_challenge.go index bf837760..533431ca 100644 --- a/acme/dns_challenge.go +++ b/acme/dns_challenge.go @@ -18,7 +18,7 @@ type preCheckDNSFunc func(fqdn, value string) (bool, error) var ( preCheckDNS preCheckDNSFunc = checkDNSPropagation - fqdn2zone = map[string]string{} + fqdnToZone = map[string]string{} ) var recursiveNameserver = "google-public-dns-a.google.com:53" @@ -185,7 +185,7 @@ func lookupNameservers(fqdn string) ([]string, error) { // findZoneByFqdn determines the zone of the given fqdn func findZoneByFqdn(fqdn, nameserver string) (string, error) { // Do we have it cached? - if zone, ok := fqdn2zone[fqdn]; ok { + if zone, ok := fqdnToZone[fqdn]; ok { return zone, nil } @@ -210,9 +210,9 @@ func findZoneByFqdn(fqdn, nameserver string) (string, error) { // If we ended up on one of the TLDs, it means the domain did not exist. publicsuffix, _ := publicsuffix.PublicSuffix(unFqdn(zone)) if publicsuffix == unFqdn(zone) { - return "", fmt.Errorf("Could not determine zone authoritively") + return "", fmt.Errorf("Could not determine zone authoritatively") } - fqdn2zone[fqdn] = zone + fqdnToZone[fqdn] = zone return zone, nil } } @@ -225,9 +225,9 @@ func findZoneByFqdn(fqdn, nameserver string) (string, error) { // If we ended up on one of the TLDs, it means the domain did not exist. publicsuffix, _ := publicsuffix.PublicSuffix(unFqdn(zone)) if publicsuffix == unFqdn(zone) { - return "", fmt.Errorf("Could not determine zone authoritively") + return "", fmt.Errorf("Could not determine zone authoritatively") } - fqdn2zone[fqdn] = zone + fqdnToZone[fqdn] = zone return zone, nil } } @@ -252,6 +252,11 @@ func unFqdn(name string) string { return name } +// clearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing. +func clearFqdnCache() { + fqdnToZone = map[string]string{} +} + // waitFor polls the given function 'f', once every 'interval' seconds, up to 'timeout' seconds. func waitFor(timeout, interval int, f func() (bool, error)) error { var lastErr string diff --git a/acme/dns_challenge_rfc2136_test.go b/acme/dns_challenge_rfc2136_test.go index b5905556..1a0ed5cc 100644 --- a/acme/dns_challenge_rfc2136_test.go +++ b/acme/dns_challenge_rfc2136_test.go @@ -26,7 +26,7 @@ var ( var reqChan = make(chan *dns.Msg, 10) func TestRFC2136CanaryLocalTestServer(t *testing.T) { - fqdn2zone = map[string]string{} + clearFqdnCache() dns.HandleFunc("example.com.", serverHandlerHello) defer dns.HandleRemove("example.com.") @@ -50,7 +50,7 @@ func TestRFC2136CanaryLocalTestServer(t *testing.T) { } func TestRFC2136ServerSuccess(t *testing.T) { - fqdn2zone = map[string]string{} + clearFqdnCache() dns.HandleFunc(rfc2136TestZone, serverHandlerReturnSuccess) defer dns.HandleRemove(rfc2136TestZone) @@ -70,7 +70,7 @@ func TestRFC2136ServerSuccess(t *testing.T) { } func TestRFC2136ServerError(t *testing.T) { - fqdn2zone = map[string]string{} + clearFqdnCache() dns.HandleFunc(rfc2136TestZone, serverHandlerReturnErr) defer dns.HandleRemove(rfc2136TestZone) @@ -92,7 +92,7 @@ func TestRFC2136ServerError(t *testing.T) { } func TestRFC2136TsigClient(t *testing.T) { - fqdn2zone = map[string]string{} + clearFqdnCache() dns.HandleFunc(rfc2136TestZone, serverHandlerReturnSuccess) defer dns.HandleRemove(rfc2136TestZone) @@ -112,7 +112,7 @@ func TestRFC2136TsigClient(t *testing.T) { } func TestRFC2136ValidUpdatePacket(t *testing.T) { - fqdn2zone = map[string]string{} + clearFqdnCache() dns.HandleFunc(rfc2136TestZone, serverHandlerPassBackRequest) defer dns.HandleRemove(rfc2136TestZone) diff --git a/acme/dns_challenge_test.go b/acme/dns_challenge_test.go index 84b805f4..54d5e095 100644 --- a/acme/dns_challenge_test.go +++ b/acme/dns_challenge_test.go @@ -35,11 +35,15 @@ var lookupNameserversTestsErr = []struct { }{ // invalid tld {"_null.n0n0.", - "Could not determine zone authoritively", + "Could not determine zone authoritatively", }, // invalid domain {"_null.com.", - "Could not determine zone authoritively", + "Could not determine zone authoritatively", + }, + // invalid domain + {"in-valid.co.uk.", + "Could not determine zone authoritatively", }, } From b412c67aa6bc9b1dd0128d253a35f9a4d0073739 Mon Sep 17 00:00:00 2001 From: xenolf Date: Mon, 29 Feb 2016 03:48:41 +0100 Subject: [PATCH 38/43] Move providers out of ACME package. --- acme/dns_challenge.go | 18 ------- cli_handlers.go | 15 ++++-- .../dns/cloudflare/cloudflare.go | 17 ++++--- .../dns/cloudflare/cloudflare_test.go | 2 +- .../dns/digitalocean/digitalocean.go | 8 ++-- .../dns/digitalocean/digitalocean_test.go | 2 +- .../dns/dnsimple/dnsimple.go | 10 ++-- .../dns/dnsimple/dnsimple_test.go | 2 +- .../dns/rfc2136/rfc2136.go | 7 +-- .../dns/rfc2136/rfc2136_test.go | 2 +- .../dns/route53/route53.go | 10 ++-- .../dns/route53/route53_test.go | 2 +- providers/dns/utils.go | 47 +++++++++++++++++++ 13 files changed, 93 insertions(+), 49 deletions(-) rename acme/dns_challenge_cloudflare.go => providers/dns/cloudflare/cloudflare.go (93%) rename acme/dns_challenge_cloudflare_test.go => providers/dns/cloudflare/cloudflare_test.go (98%) rename acme/dns_challenge_digitalocean.go => providers/dns/digitalocean/digitalocean.go (95%) rename acme/dns_challenge_digitalocean_test.go => providers/dns/digitalocean/digitalocean_test.go (99%) rename acme/dns_challenge_dnsimple.go => providers/dns/dnsimple/dnsimple.go (94%) rename acme/dns_challenge_dnsimple_test.go => providers/dns/dnsimple/dnsimple_test.go (99%) rename acme/dns_challenge_rfc2136.go => providers/dns/rfc2136/rfc2136.go (94%) rename acme/dns_challenge_rfc2136_test.go => providers/dns/rfc2136/rfc2136_test.go (99%) rename acme/dns_challenge_route53.go => providers/dns/route53/route53.go (94%) rename acme/dns_challenge_route53_test.go => providers/dns/route53/route53_test.go (99%) create mode 100644 providers/dns/utils.go diff --git a/acme/dns_challenge.go b/acme/dns_challenge.go index 533431ca..49c1a264 100644 --- a/acme/dns_challenge.go +++ b/acme/dns_challenge.go @@ -234,24 +234,6 @@ func findZoneByFqdn(fqdn, nameserver string) (string, error) { return "", fmt.Errorf("NS %s did not return the expected SOA record in the authority section", nameserver) } -// 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 -} - // clearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing. func clearFqdnCache() { fqdnToZone = map[string]string{} diff --git a/cli_handlers.go b/cli_handlers.go index 9fc2ca0e..d360a407 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -11,6 +11,11 @@ import ( "github.com/codegangsta/cli" "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/providers/dns/cloudflare" + "github.com/xenolf/lego/providers/dns/digitalocean" + "github.com/xenolf/lego/providers/dns/dnsimple" + "github.com/xenolf/lego/providers/dns/rfc2136" + "github.com/xenolf/lego/providers/dns/route53" ) func checkFolder(path string) error { @@ -67,23 +72,23 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { var provider acme.ChallengeProvider switch c.GlobalString("dns") { case "cloudflare": - provider, err = acme.NewDNSProviderCloudFlare("", "") + provider, err = cloudflare.NewDNSProviderCloudFlare("", "") case "digitalocean": authToken := os.Getenv("DO_AUTH_TOKEN") - provider, err = acme.NewDNSProviderDigitalOcean(authToken) + provider, err = digitalocean.NewDNSProviderDigitalOcean(authToken) case "dnsimple": - provider, err = acme.NewDNSProviderDNSimple("", "") + provider, err = dnsimple.NewDNSProviderDNSimple("", "") case "route53": awsRegion := os.Getenv("AWS_REGION") - provider, err = acme.NewDNSProviderRoute53("", "", awsRegion) + provider, err = route53.NewDNSProviderRoute53("", "", awsRegion) case "rfc2136": nameserver := os.Getenv("RFC2136_NAMESERVER") tsigAlgorithm := os.Getenv("RFC2136_TSIG_ALGORITHM") tsigKey := os.Getenv("RFC2136_TSIG_KEY") tsigSecret := os.Getenv("RFC2136_TSIG_SECRET") - provider, err = acme.NewDNSProviderRFC2136(nameserver, tsigAlgorithm, tsigKey, tsigSecret) + provider, err = rfc2136.NewDNSProviderRFC2136(nameserver, tsigAlgorithm, tsigKey, tsigSecret) case "manual": provider, err = acme.NewDNSProviderManual() } diff --git a/acme/dns_challenge_cloudflare.go b/providers/dns/cloudflare/cloudflare.go similarity index 93% rename from acme/dns_challenge_cloudflare.go rename to providers/dns/cloudflare/cloudflare.go index b5bf6d1f..2b9337c4 100644 --- a/acme/dns_challenge_cloudflare.go +++ b/providers/dns/cloudflare/cloudflare.go @@ -1,4 +1,4 @@ -package acme +package cloudflare import ( "bytes" @@ -9,6 +9,9 @@ import ( "os" "strings" "time" + + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/providers/dns" ) // CloudFlareAPIURL represents the API endpoint to call. @@ -39,7 +42,7 @@ func NewDNSProviderCloudFlare(cloudflareEmail, cloudflareKey string) (*DNSProvid // Present creates a TXT record to fulfil the dns-01 challenge func (c *DNSProviderCloudFlare) Present(domain, token, keyAuth string) error { - fqdn, value, _ := DNS01Record(domain, keyAuth) + fqdn, value, _ := acme.DNS01Record(domain, keyAuth) zoneID, err := c.getHostedZoneID(fqdn) if err != nil { return err @@ -47,7 +50,7 @@ func (c *DNSProviderCloudFlare) Present(domain, token, keyAuth string) error { rec := cloudFlareRecord{ Type: "TXT", - Name: unFqdn(fqdn), + Name: dns.UnFqdn(fqdn), Content: value, TTL: 120, } @@ -67,7 +70,7 @@ func (c *DNSProviderCloudFlare) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (c *DNSProviderCloudFlare) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := DNS01Record(domain, keyAuth) + fqdn, _, _ := acme.DNS01Record(domain, keyAuth) record, err := c.findTxtRecord(fqdn) if err != nil { @@ -102,7 +105,7 @@ func (c *DNSProviderCloudFlare) getHostedZoneID(fqdn string) (string, error) { var hostedZone HostedZone for _, zone := range zones { - name := toFqdn(zone.Name) + name := dns.ToFqdn(zone.Name) if strings.HasSuffix(fqdn, name) { if len(zone.Name) > len(hostedZone.Name) { hostedZone = zone @@ -134,7 +137,7 @@ func (c *DNSProviderCloudFlare) findTxtRecord(fqdn string) (*cloudFlareRecord, e } for _, rec := range records { - if rec.Name == unFqdn(fqdn) && rec.Type == "TXT" { + if rec.Name == dns.UnFqdn(fqdn) && rec.Type == "TXT" { return &rec, nil } } @@ -163,7 +166,7 @@ func (c *DNSProviderCloudFlare) makeRequest(method, uri string, body io.Reader) req.Header.Set("X-Auth-Email", c.authEmail) req.Header.Set("X-Auth-Key", c.authKey) - req.Header.Set("User-Agent", userAgent()) + //req.Header.Set("User-Agent", userAgent()) client := http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) diff --git a/acme/dns_challenge_cloudflare_test.go b/providers/dns/cloudflare/cloudflare_test.go similarity index 98% rename from acme/dns_challenge_cloudflare_test.go rename to providers/dns/cloudflare/cloudflare_test.go index e628eba1..27b5c357 100644 --- a/acme/dns_challenge_cloudflare_test.go +++ b/providers/dns/cloudflare/cloudflare_test.go @@ -1,4 +1,4 @@ -package acme +package cloudflare import ( "os" diff --git a/acme/dns_challenge_digitalocean.go b/providers/dns/digitalocean/digitalocean.go similarity index 95% rename from acme/dns_challenge_digitalocean.go rename to providers/dns/digitalocean/digitalocean.go index 176439e6..34750265 100644 --- a/acme/dns_challenge_digitalocean.go +++ b/providers/dns/digitalocean/digitalocean.go @@ -1,4 +1,4 @@ -package acme +package digitalocean import ( "bytes" @@ -6,6 +6,8 @@ import ( "fmt" "net/http" "sync" + + "github.com/xenolf/lego/acme" ) // DNSProviderDigitalOcean is an implementation of the DNSProvider interface @@ -45,7 +47,7 @@ func (d *DNSProviderDigitalOcean) Present(domain, token, keyAuth string) error { } `json:"domain_record"` } - fqdn, value, _ := DNS01Record(domain, keyAuth) + fqdn, value, _ := acme.DNS01Record(domain, keyAuth) reqURL := fmt.Sprintf("%s/v2/domains/%s/records", digitalOceanBaseURL, domain) reqData := txtRecordRequest{RecordType: "TXT", Name: fqdn, Data: value} @@ -88,7 +90,7 @@ func (d *DNSProviderDigitalOcean) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (d *DNSProviderDigitalOcean) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := DNS01Record(domain, keyAuth) + fqdn, _, _ := acme.DNS01Record(domain, keyAuth) // get the record's unique ID from when we created it d.recordIDsMu.Lock() diff --git a/acme/dns_challenge_digitalocean_test.go b/providers/dns/digitalocean/digitalocean_test.go similarity index 99% rename from acme/dns_challenge_digitalocean_test.go rename to providers/dns/digitalocean/digitalocean_test.go index cf62090e..52bdedac 100644 --- a/acme/dns_challenge_digitalocean_test.go +++ b/providers/dns/digitalocean/digitalocean_test.go @@ -1,4 +1,4 @@ -package acme +package digitalocean import ( "fmt" diff --git a/acme/dns_challenge_dnsimple.go b/providers/dns/dnsimple/dnsimple.go similarity index 94% rename from acme/dns_challenge_dnsimple.go rename to providers/dns/dnsimple/dnsimple.go index 56b96f97..8b4f95e1 100644 --- a/acme/dns_challenge_dnsimple.go +++ b/providers/dns/dnsimple/dnsimple.go @@ -1,4 +1,4 @@ -package acme +package dnsimple import ( "fmt" @@ -6,6 +6,8 @@ import ( "strings" "github.com/weppos/dnsimple-go/dnsimple" + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/providers/dns" ) // DNSProviderDNSimple is an implementation of the DNSProvider interface. @@ -33,7 +35,7 @@ func NewDNSProviderDNSimple(dnsimpleEmail, dnsimpleAPIKey string) (*DNSProviderD // Present creates a TXT record to fulfil the dns-01 challenge. func (c *DNSProviderDNSimple) Present(domain, token, keyAuth string) error { - fqdn, value, ttl := DNS01Record(domain, keyAuth) + fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) zoneID, zoneName, err := c.getHostedZone(domain) if err != nil { @@ -51,7 +53,7 @@ func (c *DNSProviderDNSimple) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (c *DNSProviderDNSimple) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := DNS01Record(domain, keyAuth) + fqdn, _, _ := acme.DNS01Record(domain, keyAuth) records, err := c.findTxtRecords(domain, fqdn) if err != nil { @@ -122,7 +124,7 @@ func (c *DNSProviderDNSimple) newTxtRecord(zone, fqdn, value string, ttl int) *d } func (c *DNSProviderDNSimple) extractRecordName(fqdn, domain string) string { - name := unFqdn(fqdn) + name := dns.UnFqdn(fqdn) if idx := strings.Index(name, "."+domain); idx != -1 { return name[:idx] } diff --git a/acme/dns_challenge_dnsimple_test.go b/providers/dns/dnsimple/dnsimple_test.go similarity index 99% rename from acme/dns_challenge_dnsimple_test.go rename to providers/dns/dnsimple/dnsimple_test.go index 3a85f1f9..c5091949 100644 --- a/acme/dns_challenge_dnsimple_test.go +++ b/providers/dns/dnsimple/dnsimple_test.go @@ -1,4 +1,4 @@ -package acme +package dnsimple import ( "os" diff --git a/acme/dns_challenge_rfc2136.go b/providers/dns/rfc2136/rfc2136.go similarity index 94% rename from acme/dns_challenge_rfc2136.go rename to providers/dns/rfc2136/rfc2136.go index 7c6900de..b2832d50 100644 --- a/acme/dns_challenge_rfc2136.go +++ b/providers/dns/rfc2136/rfc2136.go @@ -1,4 +1,4 @@ -package acme +package rfc2136 import ( "fmt" @@ -7,6 +7,7 @@ import ( "time" "github.com/miekg/dns" + "github.com/xenolf/lego/acme" ) // DNSProviderRFC2136 is an implementation of the ChallengeProvider interface that @@ -47,13 +48,13 @@ func NewDNSProviderRFC2136(nameserver, tsigAlgorithm, tsigKey, tsigSecret string // Present creates a TXT record using the specified parameters func (r *DNSProviderRFC2136) Present(domain, token, keyAuth string) error { - fqdn, value, ttl := DNS01Record(domain, keyAuth) + fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) return r.changeRecord("INSERT", fqdn, value, ttl) } // CleanUp removes the TXT record matching the specified parameters func (r *DNSProviderRFC2136) CleanUp(domain, token, keyAuth string) error { - fqdn, value, ttl := DNS01Record(domain, keyAuth) + fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) return r.changeRecord("REMOVE", fqdn, value, ttl) } diff --git a/acme/dns_challenge_rfc2136_test.go b/providers/dns/rfc2136/rfc2136_test.go similarity index 99% rename from acme/dns_challenge_rfc2136_test.go rename to providers/dns/rfc2136/rfc2136_test.go index 1a0ed5cc..2b8fb9cd 100644 --- a/acme/dns_challenge_rfc2136_test.go +++ b/providers/dns/rfc2136/rfc2136_test.go @@ -1,4 +1,4 @@ -package acme +package rfc2136 import ( "bytes" diff --git a/acme/dns_challenge_route53.go b/providers/dns/route53/route53.go similarity index 94% rename from acme/dns_challenge_route53.go rename to providers/dns/route53/route53.go index 43e42dab..fc8cd8cc 100644 --- a/acme/dns_challenge_route53.go +++ b/providers/dns/route53/route53.go @@ -1,4 +1,4 @@ -package acme +package route53 import ( "fmt" @@ -7,6 +7,8 @@ import ( "github.com/mitchellh/goamz/aws" "github.com/mitchellh/goamz/route53" + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/providers/dns" ) // DNSProviderRoute53 is an implementation of the DNSProvider interface @@ -43,14 +45,14 @@ func NewDNSProviderRoute53(awsAccessKey, awsSecretKey, awsRegionName string) (*D // Present creates a TXT record using the specified parameters func (r *DNSProviderRoute53) Present(domain, token, keyAuth string) error { - fqdn, value, ttl := DNS01Record(domain, keyAuth) + fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) value = `"` + value + `"` return r.changeRecord("UPSERT", fqdn, value, ttl) } // CleanUp removes the TXT record matching the specified parameters func (r *DNSProviderRoute53) CleanUp(domain, token, keyAuth string) error { - fqdn, value, ttl := DNS01Record(domain, keyAuth) + fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) value = `"` + value + `"` return r.changeRecord("DELETE", fqdn, value, ttl) } @@ -69,7 +71,7 @@ func (r *DNSProviderRoute53) changeRecord(action, fqdn, value string, ttl int) e return err } - return waitFor(90, 5, func() (bool, error) { + return dns.WaitFor(90, 5, func() (bool, error) { status, err := r.client.GetChange(resp.ChangeInfo.ID) if err != nil { return false, err diff --git a/acme/dns_challenge_route53_test.go b/providers/dns/route53/route53_test.go similarity index 99% rename from acme/dns_challenge_route53_test.go rename to providers/dns/route53/route53_test.go index 2c58b263..03f1fa8b 100644 --- a/acme/dns_challenge_route53_test.go +++ b/providers/dns/route53/route53_test.go @@ -1,4 +1,4 @@ -package acme +package route53 import ( "net/http" diff --git a/providers/dns/utils.go b/providers/dns/utils.go new file mode 100644 index 00000000..9df2a8bb --- /dev/null +++ b/providers/dns/utils.go @@ -0,0 +1,47 @@ +package dns + +import ( + "fmt" + "time" +) + +// 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 +} + +// WaitFor polls the given function 'f', once every 'interval' seconds, up to 'timeout' seconds. +func WaitFor(timeout, interval int, f func() (bool, error)) error { + var lastErr string + timeup := time.After(time.Duration(timeout) * time.Second) + 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(time.Duration(interval) * time.Second) + } +} From 9008ec69493c278e1e1da187a95552f101d9cf5b Mon Sep 17 00:00:00 2001 From: xenolf Date: Fri, 11 Mar 2016 03:20:25 +0100 Subject: [PATCH 39/43] Move functions from dns package back into ACME. --- acme/dns_challenge.go | 38 +++++++++++++++------ acme/dns_challenge_test.go | 2 +- providers/dns/cloudflare/cloudflare.go | 7 ++-- providers/dns/dnsimple/dnsimple.go | 3 +- providers/dns/rfc2136/rfc2136.go | 2 +- providers/dns/route53/route53.go | 3 +- providers/dns/utils.go | 47 -------------------------- 7 files changed, 35 insertions(+), 67 deletions(-) delete mode 100644 providers/dns/utils.go diff --git a/acme/dns_challenge.go b/acme/dns_challenge.go index 49c1a264..361c76c5 100644 --- a/acme/dns_challenge.go +++ b/acme/dns_challenge.go @@ -69,7 +69,7 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error { logf("[INFO][%s] Checking DNS record propagation...", domain) - err = waitFor(30, 2, func() (bool, error) { + err = WaitFor(30, 2, func() (bool, error) { return preCheckDNS(fqdn, value) }) if err != nil { @@ -160,7 +160,7 @@ func dnsQuery(fqdn string, rtype uint16, nameserver string, recursive bool) (in func lookupNameservers(fqdn string) ([]string, error) { var authoritativeNss []string - zone, err := findZoneByFqdn(fqdn, recursiveNameserver) + zone, err := FindZoneByFqdn(fqdn, recursiveNameserver) if err != nil { return nil, err } @@ -182,8 +182,8 @@ func lookupNameservers(fqdn string) ([]string, error) { return nil, fmt.Errorf("Could not determine authoritative nameservers") } -// findZoneByFqdn determines the zone of the given fqdn -func findZoneByFqdn(fqdn, nameserver string) (string, error) { +// FindZoneByFqdn determines the zone of the given fqdn +func FindZoneByFqdn(fqdn, nameserver string) (string, error) { // Do we have it cached? if zone, ok := fqdnToZone[fqdn]; ok { return zone, nil @@ -208,8 +208,8 @@ func findZoneByFqdn(fqdn, nameserver string) (string, error) { if soa, ok := ans.(*dns.SOA); ok { zone := soa.Hdr.Name // If we ended up on one of the TLDs, it means the domain did not exist. - publicsuffix, _ := publicsuffix.PublicSuffix(unFqdn(zone)) - if publicsuffix == unFqdn(zone) { + publicsuffix, _ := publicsuffix.PublicSuffix(UnFqdn(zone)) + if publicsuffix == UnFqdn(zone) { return "", fmt.Errorf("Could not determine zone authoritatively") } fqdnToZone[fqdn] = zone @@ -223,8 +223,8 @@ func findZoneByFqdn(fqdn, nameserver string) (string, error) { if soa, ok := ns.(*dns.SOA); ok { zone := soa.Hdr.Name // If we ended up on one of the TLDs, it means the domain did not exist. - publicsuffix, _ := publicsuffix.PublicSuffix(unFqdn(zone)) - if publicsuffix == unFqdn(zone) { + publicsuffix, _ := publicsuffix.PublicSuffix(UnFqdn(zone)) + if publicsuffix == UnFqdn(zone) { return "", fmt.Errorf("Could not determine zone authoritatively") } fqdnToZone[fqdn] = zone @@ -239,8 +239,26 @@ func clearFqdnCache() { fqdnToZone = map[string]string{} } -// waitFor polls the given function 'f', once every 'interval' seconds, up to 'timeout' seconds. -func waitFor(timeout, interval int, f func() (bool, error)) error { +// 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 +} + +// WaitFor polls the given function 'f', once every 'interval' seconds, up to 'timeout' seconds. +func WaitFor(timeout, interval int, f func() (bool, error)) error { var lastErr string timeup := time.After(time.Duration(timeout) * time.Second) for { diff --git a/acme/dns_challenge_test.go b/acme/dns_challenge_test.go index 54d5e095..760c7991 100644 --- a/acme/dns_challenge_test.go +++ b/acme/dns_challenge_test.go @@ -167,7 +167,7 @@ func TestCheckAuthoritativeNssErr(t *testing.T) { func TestWaitForTimeout(t *testing.T) { c := make(chan error) go func() { - err := waitFor(3, 1, func() (bool, error) { + err := WaitFor(3, 1, func() (bool, error) { return false, nil }) c <- err diff --git a/providers/dns/cloudflare/cloudflare.go b/providers/dns/cloudflare/cloudflare.go index 2b9337c4..d531dfc6 100644 --- a/providers/dns/cloudflare/cloudflare.go +++ b/providers/dns/cloudflare/cloudflare.go @@ -11,7 +11,6 @@ import ( "time" "github.com/xenolf/lego/acme" - "github.com/xenolf/lego/providers/dns" ) // CloudFlareAPIURL represents the API endpoint to call. @@ -50,7 +49,7 @@ func (c *DNSProviderCloudFlare) Present(domain, token, keyAuth string) error { rec := cloudFlareRecord{ Type: "TXT", - Name: dns.UnFqdn(fqdn), + Name: acme.UnFqdn(fqdn), Content: value, TTL: 120, } @@ -105,7 +104,7 @@ func (c *DNSProviderCloudFlare) getHostedZoneID(fqdn string) (string, error) { var hostedZone HostedZone for _, zone := range zones { - name := dns.ToFqdn(zone.Name) + name := acme.ToFqdn(zone.Name) if strings.HasSuffix(fqdn, name) { if len(zone.Name) > len(hostedZone.Name) { hostedZone = zone @@ -137,7 +136,7 @@ func (c *DNSProviderCloudFlare) findTxtRecord(fqdn string) (*cloudFlareRecord, e } for _, rec := range records { - if rec.Name == dns.UnFqdn(fqdn) && rec.Type == "TXT" { + if rec.Name == acme.UnFqdn(fqdn) && rec.Type == "TXT" { return &rec, nil } } diff --git a/providers/dns/dnsimple/dnsimple.go b/providers/dns/dnsimple/dnsimple.go index 8b4f95e1..0c9f03e2 100644 --- a/providers/dns/dnsimple/dnsimple.go +++ b/providers/dns/dnsimple/dnsimple.go @@ -7,7 +7,6 @@ import ( "github.com/weppos/dnsimple-go/dnsimple" "github.com/xenolf/lego/acme" - "github.com/xenolf/lego/providers/dns" ) // DNSProviderDNSimple is an implementation of the DNSProvider interface. @@ -124,7 +123,7 @@ func (c *DNSProviderDNSimple) newTxtRecord(zone, fqdn, value string, ttl int) *d } func (c *DNSProviderDNSimple) extractRecordName(fqdn, domain string) string { - name := dns.UnFqdn(fqdn) + name := acme.UnFqdn(fqdn) if idx := strings.Index(name, "."+domain); idx != -1 { return name[:idx] } diff --git a/providers/dns/rfc2136/rfc2136.go b/providers/dns/rfc2136/rfc2136.go index b2832d50..df303001 100644 --- a/providers/dns/rfc2136/rfc2136.go +++ b/providers/dns/rfc2136/rfc2136.go @@ -60,7 +60,7 @@ func (r *DNSProviderRFC2136) CleanUp(domain, token, keyAuth string) error { func (r *DNSProviderRFC2136) changeRecord(action, fqdn, value string, ttl int) error { // Find the zone for the given fqdn - zone, err := findZoneByFqdn(fqdn, r.nameserver) + zone, err := acme.FindZoneByFqdn(fqdn, r.nameserver) if err != nil { return err } diff --git a/providers/dns/route53/route53.go b/providers/dns/route53/route53.go index fc8cd8cc..ce3bb975 100644 --- a/providers/dns/route53/route53.go +++ b/providers/dns/route53/route53.go @@ -8,7 +8,6 @@ import ( "github.com/mitchellh/goamz/aws" "github.com/mitchellh/goamz/route53" "github.com/xenolf/lego/acme" - "github.com/xenolf/lego/providers/dns" ) // DNSProviderRoute53 is an implementation of the DNSProvider interface @@ -71,7 +70,7 @@ func (r *DNSProviderRoute53) changeRecord(action, fqdn, value string, ttl int) e return err } - return dns.WaitFor(90, 5, func() (bool, error) { + return acme.WaitFor(90, 5, func() (bool, error) { status, err := r.client.GetChange(resp.ChangeInfo.ID) if err != nil { return false, err diff --git a/providers/dns/utils.go b/providers/dns/utils.go deleted file mode 100644 index 9df2a8bb..00000000 --- a/providers/dns/utils.go +++ /dev/null @@ -1,47 +0,0 @@ -package dns - -import ( - "fmt" - "time" -) - -// 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 -} - -// WaitFor polls the given function 'f', once every 'interval' seconds, up to 'timeout' seconds. -func WaitFor(timeout, interval int, f func() (bool, error)) error { - var lastErr string - timeup := time.After(time.Duration(timeout) * time.Second) - 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(time.Duration(interval) * time.Second) - } -} From 2ae35a755d1e2dcede30f5fd965b7f4db5c6dfa4 Mon Sep 17 00:00:00 2001 From: xenolf Date: Fri, 11 Mar 2016 03:46:09 +0100 Subject: [PATCH 40/43] Rename provider types as provider names are already in the package name. Added package level comments and fixed the name of the interface the providers are importing. --- acme/dns_challenge.go | 4 +-- cli_handlers.go | 10 +++---- providers/dns/cloudflare/cloudflare.go | 21 ++++++++------- providers/dns/cloudflare/cloudflare_test.go | 16 +++++------ providers/dns/digitalocean/digitalocean.go | 15 ++++++----- .../dns/digitalocean/digitalocean_test.go | 4 +-- providers/dns/dnsimple/dnsimple.go | 23 ++++++++-------- providers/dns/dnsimple/dnsimple_test.go | 16 +++++------ providers/dns/rfc2136/rfc2136.go | 17 ++++++------ providers/dns/rfc2136/rfc2136_test.go | 27 ++++++++++--------- providers/dns/route53/route53.go | 19 ++++++------- providers/dns/route53/route53_test.go | 20 +++++++------- 12 files changed, 99 insertions(+), 93 deletions(-) diff --git a/acme/dns_challenge.go b/acme/dns_challenge.go index 361c76c5..659d7082 100644 --- a/acme/dns_challenge.go +++ b/acme/dns_challenge.go @@ -234,8 +234,8 @@ func FindZoneByFqdn(fqdn, nameserver string) (string, error) { return "", fmt.Errorf("NS %s did not return the expected SOA record in the authority section", nameserver) } -// clearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing. -func clearFqdnCache() { +// ClearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing. +func ClearFqdnCache() { fqdnToZone = map[string]string{} } diff --git a/cli_handlers.go b/cli_handlers.go index d360a407..28c96d8b 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -72,23 +72,23 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { var provider acme.ChallengeProvider switch c.GlobalString("dns") { case "cloudflare": - provider, err = cloudflare.NewDNSProviderCloudFlare("", "") + provider, err = cloudflare.NewDNSProvider("", "") case "digitalocean": authToken := os.Getenv("DO_AUTH_TOKEN") - provider, err = digitalocean.NewDNSProviderDigitalOcean(authToken) + provider, err = digitalocean.NewDNSProvider(authToken) case "dnsimple": - provider, err = dnsimple.NewDNSProviderDNSimple("", "") + provider, err = dnsimple.NewDNSProvider("", "") case "route53": awsRegion := os.Getenv("AWS_REGION") - provider, err = route53.NewDNSProviderRoute53("", "", awsRegion) + provider, err = route53.NewDNSProvider("", "", awsRegion) case "rfc2136": nameserver := os.Getenv("RFC2136_NAMESERVER") tsigAlgorithm := os.Getenv("RFC2136_TSIG_ALGORITHM") tsigKey := os.Getenv("RFC2136_TSIG_KEY") tsigSecret := os.Getenv("RFC2136_TSIG_SECRET") - provider, err = rfc2136.NewDNSProviderRFC2136(nameserver, tsigAlgorithm, tsigKey, tsigSecret) + provider, err = rfc2136.NewDNSProvider(nameserver, tsigAlgorithm, tsigKey, tsigSecret) case "manual": provider, err = acme.NewDNSProviderManual() } diff --git a/providers/dns/cloudflare/cloudflare.go b/providers/dns/cloudflare/cloudflare.go index d531dfc6..307cc4ef 100644 --- a/providers/dns/cloudflare/cloudflare.go +++ b/providers/dns/cloudflare/cloudflare.go @@ -1,3 +1,4 @@ +// Package cloudflare implements a DNS provider for solving the DNS-01 challenge using cloudflare DNS. package cloudflare import ( @@ -17,15 +18,15 @@ import ( // TODO: Unexport? const CloudFlareAPIURL = "https://api.cloudflare.com/client/v4" -// DNSProviderCloudFlare is an implementation of the DNSProvider interface -type DNSProviderCloudFlare struct { +// DNSProvider is an implementation of the acme.ChallengeProvider interface +type DNSProvider struct { authEmail string authKey string } -// NewDNSProviderCloudFlare returns a DNSProviderCloudFlare instance with a configured cloudflare client. +// NewDNSProvider returns a DNSProvider instance with a configured cloudflare client. // Credentials can either be passed as arguments or through CLOUDFLARE_EMAIL and CLOUDFLARE_API_KEY env vars. -func NewDNSProviderCloudFlare(cloudflareEmail, cloudflareKey string) (*DNSProviderCloudFlare, error) { +func NewDNSProvider(cloudflareEmail, cloudflareKey string) (*DNSProvider, error) { if cloudflareEmail == "" || cloudflareKey == "" { cloudflareEmail, cloudflareKey = cloudflareEnvAuth() if cloudflareEmail == "" || cloudflareKey == "" { @@ -33,14 +34,14 @@ func NewDNSProviderCloudFlare(cloudflareEmail, cloudflareKey string) (*DNSProvid } } - return &DNSProviderCloudFlare{ + return &DNSProvider{ authEmail: cloudflareEmail, authKey: cloudflareKey, }, nil } // Present creates a TXT record to fulfil the dns-01 challenge -func (c *DNSProviderCloudFlare) Present(domain, token, keyAuth string) error { +func (c *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value, _ := acme.DNS01Record(domain, keyAuth) zoneID, err := c.getHostedZoneID(fqdn) if err != nil { @@ -68,7 +69,7 @@ func (c *DNSProviderCloudFlare) Present(domain, token, keyAuth string) error { } // CleanUp removes the TXT record matching the specified parameters -func (c *DNSProviderCloudFlare) CleanUp(domain, token, keyAuth string) error { +func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _, _ := acme.DNS01Record(domain, keyAuth) record, err := c.findTxtRecord(fqdn) @@ -84,7 +85,7 @@ func (c *DNSProviderCloudFlare) CleanUp(domain, token, keyAuth string) error { return nil } -func (c *DNSProviderCloudFlare) getHostedZoneID(fqdn string) (string, error) { +func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) { // HostedZone represents a CloudFlare DNS zone type HostedZone struct { ID string `json:"id"` @@ -118,7 +119,7 @@ func (c *DNSProviderCloudFlare) getHostedZoneID(fqdn string) (string, error) { return hostedZone.ID, nil } -func (c *DNSProviderCloudFlare) findTxtRecord(fqdn string) (*cloudFlareRecord, error) { +func (c *DNSProvider) findTxtRecord(fqdn string) (*cloudFlareRecord, error) { zoneID, err := c.getHostedZoneID(fqdn) if err != nil { return nil, err @@ -144,7 +145,7 @@ func (c *DNSProviderCloudFlare) findTxtRecord(fqdn string) (*cloudFlareRecord, e return nil, fmt.Errorf("No existing record found for %s", fqdn) } -func (c *DNSProviderCloudFlare) makeRequest(method, uri string, body io.Reader) (json.RawMessage, error) { +func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawMessage, error) { // APIError contains error details for failed requests type APIError struct { Code int `json:"code,omitempty"` diff --git a/providers/dns/cloudflare/cloudflare_test.go b/providers/dns/cloudflare/cloudflare_test.go index 27b5c357..63936ce6 100644 --- a/providers/dns/cloudflare/cloudflare_test.go +++ b/providers/dns/cloudflare/cloudflare_test.go @@ -29,26 +29,26 @@ func restoreCloudFlareEnv() { os.Setenv("CLOUDFLARE_API_KEY", cflareAPIKey) } -func TestNewDNSProviderCloudFlareValid(t *testing.T) { +func TestNewDNSProviderValid(t *testing.T) { os.Setenv("CLOUDFLARE_EMAIL", "") os.Setenv("CLOUDFLARE_API_KEY", "") - _, err := NewDNSProviderCloudFlare("123", "123") + _, err := NewDNSProvider("123", "123") assert.NoError(t, err) restoreCloudFlareEnv() } -func TestNewDNSProviderCloudFlareValidEnv(t *testing.T) { +func TestNewDNSProviderValidEnv(t *testing.T) { os.Setenv("CLOUDFLARE_EMAIL", "test@example.com") os.Setenv("CLOUDFLARE_API_KEY", "123") - _, err := NewDNSProviderCloudFlare("", "") + _, err := NewDNSProvider("", "") assert.NoError(t, err) restoreCloudFlareEnv() } -func TestNewDNSProviderCloudFlareMissingCredErr(t *testing.T) { +func TestNewDNSProviderMissingCredErr(t *testing.T) { os.Setenv("CLOUDFLARE_EMAIL", "") os.Setenv("CLOUDFLARE_API_KEY", "") - _, err := NewDNSProviderCloudFlare("", "") + _, err := NewDNSProvider("", "") assert.EqualError(t, err, "CloudFlare credentials missing") restoreCloudFlareEnv() } @@ -58,7 +58,7 @@ func TestCloudFlarePresent(t *testing.T) { t.Skip("skipping live test") } - provider, err := NewDNSProviderCloudFlare(cflareEmail, cflareAPIKey) + provider, err := NewDNSProvider(cflareEmail, cflareAPIKey) assert.NoError(t, err) err = provider.Present(cflareDomain, "", "123d==") @@ -72,7 +72,7 @@ func TestCloudFlareCleanUp(t *testing.T) { time.Sleep(time.Second * 2) - provider, err := NewDNSProviderCloudFlare(cflareEmail, cflareAPIKey) + provider, err := NewDNSProvider(cflareEmail, cflareAPIKey) assert.NoError(t, err) err = provider.CleanUp(cflareDomain, "", "123d==") diff --git a/providers/dns/digitalocean/digitalocean.go b/providers/dns/digitalocean/digitalocean.go index 34750265..fb3e00de 100644 --- a/providers/dns/digitalocean/digitalocean.go +++ b/providers/dns/digitalocean/digitalocean.go @@ -1,3 +1,4 @@ +// Package digitalocean implements a DNS provider for solving the DNS-01 challenge using digitalocean DNS. package digitalocean import ( @@ -10,26 +11,26 @@ import ( "github.com/xenolf/lego/acme" ) -// DNSProviderDigitalOcean is an implementation of the DNSProvider interface +// DNSProvider is an implementation of the acme.ChallengeProvider interface // that uses DigitalOcean's REST API to manage TXT records for a domain. -type DNSProviderDigitalOcean struct { +type DNSProvider struct { apiAuthToken string recordIDs map[string]int recordIDsMu sync.Mutex } -// NewDNSProviderDigitalOcean returns a new DNSProviderDigitalOcean instance. +// NewDNSProvider returns a new DNSProvider instance. // apiAuthToken is the personal access token created in the DigitalOcean account // control panel, and it will be sent in bearer authorization headers. -func NewDNSProviderDigitalOcean(apiAuthToken string) (*DNSProviderDigitalOcean, error) { - return &DNSProviderDigitalOcean{ +func NewDNSProvider(apiAuthToken string) (*DNSProvider, error) { + return &DNSProvider{ apiAuthToken: apiAuthToken, recordIDs: make(map[string]int), }, nil } // Present creates a TXT record using the specified parameters -func (d *DNSProviderDigitalOcean) Present(domain, token, keyAuth string) error { +func (d *DNSProvider) Present(domain, token, keyAuth string) error { // txtRecordRequest represents the request body to DO's API to make a TXT record type txtRecordRequest struct { RecordType string `json:"type"` @@ -89,7 +90,7 @@ func (d *DNSProviderDigitalOcean) Present(domain, token, keyAuth string) error { } // CleanUp removes the TXT record matching the specified parameters -func (d *DNSProviderDigitalOcean) CleanUp(domain, token, keyAuth string) error { +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _, _ := acme.DNS01Record(domain, keyAuth) // get the record's unique ID from when we created it diff --git a/providers/dns/digitalocean/digitalocean_test.go b/providers/dns/digitalocean/digitalocean_test.go index 52bdedac..8fc383a0 100644 --- a/providers/dns/digitalocean/digitalocean_test.go +++ b/providers/dns/digitalocean/digitalocean_test.go @@ -53,7 +53,7 @@ func TestDigitalOceanPresent(t *testing.T) { defer mock.Close() digitalOceanBaseURL = mock.URL - doprov, err := NewDNSProviderDigitalOcean(fakeDigitalOceanAuth) + doprov, err := NewDNSProvider(fakeDigitalOceanAuth) if doprov == nil { t.Fatal("Expected non-nil DigitalOcean provider, but was nil") } @@ -95,7 +95,7 @@ func TestDigitalOceanCleanUp(t *testing.T) { defer mock.Close() digitalOceanBaseURL = mock.URL - doprov, err := NewDNSProviderDigitalOcean(fakeDigitalOceanAuth) + doprov, err := NewDNSProvider(fakeDigitalOceanAuth) if doprov == nil { t.Fatal("Expected non-nil DigitalOcean provider, but was nil") } diff --git a/providers/dns/dnsimple/dnsimple.go b/providers/dns/dnsimple/dnsimple.go index 0c9f03e2..cde77298 100644 --- a/providers/dns/dnsimple/dnsimple.go +++ b/providers/dns/dnsimple/dnsimple.go @@ -1,3 +1,4 @@ +// Package dnsimple implements a DNS provider for solving the DNS-01 challenge using dnsimple DNS. package dnsimple import ( @@ -9,15 +10,15 @@ import ( "github.com/xenolf/lego/acme" ) -// DNSProviderDNSimple is an implementation of the DNSProvider interface. -type DNSProviderDNSimple struct { +// DNSProvider is an implementation of the acme.ChallengeProvider interface. +type DNSProvider struct { client *dnsimple.Client } -// NewDNSProviderDNSimple returns a DNSProviderDNSimple instance with a configured dnsimple client. +// NewDNSProvider returns a DNSProvider instance with a configured dnsimple client. // Authentication is either done using the passed credentials or - when empty - using the environment // variables DNSIMPLE_EMAIL and DNSIMPLE_API_KEY. -func NewDNSProviderDNSimple(dnsimpleEmail, dnsimpleAPIKey string) (*DNSProviderDNSimple, error) { +func NewDNSProvider(dnsimpleEmail, dnsimpleAPIKey string) (*DNSProvider, error) { if dnsimpleEmail == "" || dnsimpleAPIKey == "" { dnsimpleEmail, dnsimpleAPIKey = dnsimpleEnvAuth() if dnsimpleEmail == "" || dnsimpleAPIKey == "" { @@ -25,7 +26,7 @@ func NewDNSProviderDNSimple(dnsimpleEmail, dnsimpleAPIKey string) (*DNSProviderD } } - c := &DNSProviderDNSimple{ + c := &DNSProvider{ client: dnsimple.NewClient(dnsimpleAPIKey, dnsimpleEmail), } @@ -33,7 +34,7 @@ func NewDNSProviderDNSimple(dnsimpleEmail, dnsimpleAPIKey string) (*DNSProviderD } // Present creates a TXT record to fulfil the dns-01 challenge. -func (c *DNSProviderDNSimple) Present(domain, token, keyAuth string) error { +func (c *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) zoneID, zoneName, err := c.getHostedZone(domain) @@ -51,7 +52,7 @@ func (c *DNSProviderDNSimple) Present(domain, token, keyAuth string) error { } // CleanUp removes the TXT record matching the specified parameters. -func (c *DNSProviderDNSimple) CleanUp(domain, token, keyAuth string) error { +func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _, _ := acme.DNS01Record(domain, keyAuth) records, err := c.findTxtRecords(domain, fqdn) @@ -68,7 +69,7 @@ func (c *DNSProviderDNSimple) CleanUp(domain, token, keyAuth string) error { return nil } -func (c *DNSProviderDNSimple) getHostedZone(domain string) (string, string, error) { +func (c *DNSProvider) getHostedZone(domain string) (string, string, error) { domains, _, err := c.client.Domains.List() if err != nil { return "", "", fmt.Errorf("DNSimple API call failed: %v", err) @@ -89,7 +90,7 @@ func (c *DNSProviderDNSimple) getHostedZone(domain string) (string, string, erro return fmt.Sprintf("%v", hostedDomain.Id), hostedDomain.Name, nil } -func (c *DNSProviderDNSimple) findTxtRecords(domain, fqdn string) ([]dnsimple.Record, error) { +func (c *DNSProvider) findTxtRecords(domain, fqdn string) ([]dnsimple.Record, error) { zoneID, zoneName, err := c.getHostedZone(domain) if err != nil { return nil, err @@ -111,7 +112,7 @@ func (c *DNSProviderDNSimple) findTxtRecords(domain, fqdn string) ([]dnsimple.Re return records, nil } -func (c *DNSProviderDNSimple) newTxtRecord(zone, fqdn, value string, ttl int) *dnsimple.Record { +func (c *DNSProvider) newTxtRecord(zone, fqdn, value string, ttl int) *dnsimple.Record { name := c.extractRecordName(fqdn, zone) return &dnsimple.Record{ @@ -122,7 +123,7 @@ func (c *DNSProviderDNSimple) newTxtRecord(zone, fqdn, value string, ttl int) *d } } -func (c *DNSProviderDNSimple) extractRecordName(fqdn, domain string) string { +func (c *DNSProvider) extractRecordName(fqdn, domain string) string { name := acme.UnFqdn(fqdn) if idx := strings.Index(name, "."+domain); idx != -1 { return name[:idx] diff --git a/providers/dns/dnsimple/dnsimple_test.go b/providers/dns/dnsimple/dnsimple_test.go index c5091949..7cb19fea 100644 --- a/providers/dns/dnsimple/dnsimple_test.go +++ b/providers/dns/dnsimple/dnsimple_test.go @@ -29,25 +29,25 @@ func restoreDNSimpleEnv() { os.Setenv("DNSIMPLE_API_KEY", dnsimpleAPIKey) } -func TestNewDNSProviderDNSimpleValid(t *testing.T) { +func TestNewDNSProviderValid(t *testing.T) { os.Setenv("DNSIMPLE_EMAIL", "") os.Setenv("DNSIMPLE_API_KEY", "") - _, err := NewDNSProviderDNSimple("example@example.com", "123") + _, err := NewDNSProvider("example@example.com", "123") assert.NoError(t, err) restoreDNSimpleEnv() } -func TestNewDNSProviderDNSimpleValidEnv(t *testing.T) { +func TestNewDNSProviderValidEnv(t *testing.T) { os.Setenv("DNSIMPLE_EMAIL", "example@example.com") os.Setenv("DNSIMPLE_API_KEY", "123") - _, err := NewDNSProviderDNSimple("", "") + _, err := NewDNSProvider("", "") assert.NoError(t, err) restoreDNSimpleEnv() } -func TestNewDNSProviderDNSimpleMissingCredErr(t *testing.T) { +func TestNewDNSProviderMissingCredErr(t *testing.T) { os.Setenv("DNSIMPLE_EMAIL", "") os.Setenv("DNSIMPLE_API_KEY", "") - _, err := NewDNSProviderDNSimple("", "") + _, err := NewDNSProvider("", "") assert.EqualError(t, err, "DNSimple credentials missing") restoreDNSimpleEnv() } @@ -57,7 +57,7 @@ func TestLiveDNSimplePresent(t *testing.T) { t.Skip("skipping live test") } - provider, err := NewDNSProviderDNSimple(dnsimpleEmail, dnsimpleAPIKey) + provider, err := NewDNSProvider(dnsimpleEmail, dnsimpleAPIKey) assert.NoError(t, err) err = provider.Present(dnsimpleDomain, "", "123d==") @@ -71,7 +71,7 @@ func TestLiveDNSimpleCleanUp(t *testing.T) { time.Sleep(time.Second * 1) - provider, err := NewDNSProviderDNSimple(dnsimpleEmail, dnsimpleAPIKey) + provider, err := NewDNSProvider(dnsimpleEmail, dnsimpleAPIKey) assert.NoError(t, err) err = provider.CleanUp(dnsimpleDomain, "", "123d==") diff --git a/providers/dns/rfc2136/rfc2136.go b/providers/dns/rfc2136/rfc2136.go index df303001..3d32409b 100644 --- a/providers/dns/rfc2136/rfc2136.go +++ b/providers/dns/rfc2136/rfc2136.go @@ -1,3 +1,4 @@ +// Package rfc2136 implements a DNS provider for solving the DNS-01 challenge using the rfc2136 dynamic update. package rfc2136 import ( @@ -10,19 +11,19 @@ import ( "github.com/xenolf/lego/acme" ) -// DNSProviderRFC2136 is an implementation of the ChallengeProvider interface that +// DNSProvider is an implementation of the acme.ChallengeProvider interface that // uses dynamic DNS updates (RFC 2136) to create TXT records on a nameserver. -type DNSProviderRFC2136 struct { +type DNSProvider struct { nameserver string tsigAlgorithm string tsigKey string tsigSecret string } -// NewDNSProviderRFC2136 returns a new DNSProviderRFC2136 instance. +// NewDNSProvider returns a new DNSProvider instance. // To disable TSIG authentication 'tsigAlgorithm, 'tsigKey' and 'tsigSecret' must be set to the empty string. // 'nameserver' must be a network address in the the form "host" or "host:port". -func NewDNSProviderRFC2136(nameserver, tsigAlgorithm, tsigKey, tsigSecret string) (*DNSProviderRFC2136, error) { +func NewDNSProvider(nameserver, tsigAlgorithm, tsigKey, tsigSecret string) (*DNSProvider, error) { // Append the default DNS port if none is specified. if _, _, err := net.SplitHostPort(nameserver); err != nil { if strings.Contains(err.Error(), "missing port") { @@ -31,7 +32,7 @@ func NewDNSProviderRFC2136(nameserver, tsigAlgorithm, tsigKey, tsigSecret string return nil, err } } - d := &DNSProviderRFC2136{ + d := &DNSProvider{ nameserver: nameserver, } if tsigAlgorithm == "" { @@ -47,18 +48,18 @@ func NewDNSProviderRFC2136(nameserver, tsigAlgorithm, tsigKey, tsigSecret string } // Present creates a TXT record using the specified parameters -func (r *DNSProviderRFC2136) Present(domain, token, keyAuth string) error { +func (r *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) return r.changeRecord("INSERT", fqdn, value, ttl) } // CleanUp removes the TXT record matching the specified parameters -func (r *DNSProviderRFC2136) CleanUp(domain, token, keyAuth string) error { +func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) return r.changeRecord("REMOVE", fqdn, value, ttl) } -func (r *DNSProviderRFC2136) changeRecord(action, fqdn, value string, ttl int) error { +func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error { // Find the zone for the given fqdn zone, err := acme.FindZoneByFqdn(fqdn, r.nameserver) if err != nil { diff --git a/providers/dns/rfc2136/rfc2136_test.go b/providers/dns/rfc2136/rfc2136_test.go index 2b8fb9cd..2aa8aa22 100644 --- a/providers/dns/rfc2136/rfc2136_test.go +++ b/providers/dns/rfc2136/rfc2136_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/miekg/dns" + "github.com/xenolf/lego/acme" ) var ( @@ -26,7 +27,7 @@ var ( var reqChan = make(chan *dns.Msg, 10) func TestRFC2136CanaryLocalTestServer(t *testing.T) { - clearFqdnCache() + acme.ClearFqdnCache() dns.HandleFunc("example.com.", serverHandlerHello) defer dns.HandleRemove("example.com.") @@ -50,7 +51,7 @@ func TestRFC2136CanaryLocalTestServer(t *testing.T) { } func TestRFC2136ServerSuccess(t *testing.T) { - clearFqdnCache() + acme.ClearFqdnCache() dns.HandleFunc(rfc2136TestZone, serverHandlerReturnSuccess) defer dns.HandleRemove(rfc2136TestZone) @@ -60,9 +61,9 @@ func TestRFC2136ServerSuccess(t *testing.T) { } defer server.Shutdown() - provider, err := NewDNSProviderRFC2136(addrstr, "", "", "") + provider, err := NewDNSProvider(addrstr, "", "", "") if err != nil { - t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err) + t.Fatalf("Expected NewDNSProvider() to return no error but the error was -> %v", err) } if err := provider.Present(rfc2136TestDomain, "", rfc2136TestKeyAuth); err != nil { t.Errorf("Expected Present() to return no error but the error was -> %v", err) @@ -70,7 +71,7 @@ func TestRFC2136ServerSuccess(t *testing.T) { } func TestRFC2136ServerError(t *testing.T) { - clearFqdnCache() + acme.ClearFqdnCache() dns.HandleFunc(rfc2136TestZone, serverHandlerReturnErr) defer dns.HandleRemove(rfc2136TestZone) @@ -80,9 +81,9 @@ func TestRFC2136ServerError(t *testing.T) { } defer server.Shutdown() - provider, err := NewDNSProviderRFC2136(addrstr, "", "", "") + provider, err := NewDNSProvider(addrstr, "", "", "") if err != nil { - t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err) + t.Fatalf("Expected NewDNSProvider() to return no error but the error was -> %v", err) } if err := provider.Present(rfc2136TestDomain, "", rfc2136TestKeyAuth); err == nil { t.Errorf("Expected Present() to return an error but it did not.") @@ -92,7 +93,7 @@ func TestRFC2136ServerError(t *testing.T) { } func TestRFC2136TsigClient(t *testing.T) { - clearFqdnCache() + acme.ClearFqdnCache() dns.HandleFunc(rfc2136TestZone, serverHandlerReturnSuccess) defer dns.HandleRemove(rfc2136TestZone) @@ -102,9 +103,9 @@ func TestRFC2136TsigClient(t *testing.T) { } defer server.Shutdown() - provider, err := NewDNSProviderRFC2136(addrstr, "", rfc2136TestTsigKey, rfc2136TestTsigSecret) + provider, err := NewDNSProvider(addrstr, "", rfc2136TestTsigKey, rfc2136TestTsigSecret) if err != nil { - t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err) + t.Fatalf("Expected NewDNSProvider() to return no error but the error was -> %v", err) } if err := provider.Present(rfc2136TestDomain, "", rfc2136TestKeyAuth); err != nil { t.Errorf("Expected Present() to return no error but the error was -> %v", err) @@ -112,7 +113,7 @@ func TestRFC2136TsigClient(t *testing.T) { } func TestRFC2136ValidUpdatePacket(t *testing.T) { - clearFqdnCache() + acme.ClearFqdnCache() dns.HandleFunc(rfc2136TestZone, serverHandlerPassBackRequest) defer dns.HandleRemove(rfc2136TestZone) @@ -134,9 +135,9 @@ func TestRFC2136ValidUpdatePacket(t *testing.T) { t.Fatalf("Error packing expect msg: %v", err) } - provider, err := NewDNSProviderRFC2136(addrstr, "", "", "") + provider, err := NewDNSProvider(addrstr, "", "", "") if err != nil { - t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err) + t.Fatalf("Expected NewDNSProvider() to return no error but the error was -> %v", err) } if err := provider.Present(rfc2136TestDomain, "", "1234d=="); err != nil { diff --git a/providers/dns/route53/route53.go b/providers/dns/route53/route53.go index ce3bb975..bfca45ac 100644 --- a/providers/dns/route53/route53.go +++ b/providers/dns/route53/route53.go @@ -1,3 +1,4 @@ +// Package route53 implements a DNS provider for solving the DNS-01 challenge using route53 DNS. package route53 import ( @@ -10,18 +11,18 @@ import ( "github.com/xenolf/lego/acme" ) -// DNSProviderRoute53 is an implementation of the DNSProvider interface -type DNSProviderRoute53 struct { +// DNSProvider is an implementation of the acme.ChallengeProvider interface +type DNSProvider struct { client *route53.Route53 } -// NewDNSProviderRoute53 returns a DNSProviderRoute53 instance with a configured route53 client. +// NewDNSProvider returns a DNSProvider instance with a configured route53 client. // Authentication is either done using the passed credentials or - when empty - falling back to // the customary AWS credential mechanisms, including the file referenced by $AWS_CREDENTIAL_FILE // (defaulting to $HOME/.aws/credentials) optionally scoped to $AWS_PROFILE, credentials // supplied by the environment variables AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY [ + AWS_SECURITY_TOKEN ], // and finally credentials available via the EC2 instance metadata service. -func NewDNSProviderRoute53(awsAccessKey, awsSecretKey, awsRegionName string) (*DNSProviderRoute53, error) { +func NewDNSProvider(awsAccessKey, awsSecretKey, awsRegionName string) (*DNSProvider, error) { region, ok := aws.Regions[awsRegionName] if !ok { return nil, fmt.Errorf("Invalid AWS region name %s", awsRegionName) @@ -39,24 +40,24 @@ func NewDNSProviderRoute53(awsAccessKey, awsSecretKey, awsRegionName string) (*D } client := route53.New(auth, region) - return &DNSProviderRoute53{client: client}, nil + return &DNSProvider{client: client}, nil } // Present creates a TXT record using the specified parameters -func (r *DNSProviderRoute53) Present(domain, token, keyAuth string) error { +func (r *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) value = `"` + value + `"` return r.changeRecord("UPSERT", fqdn, value, ttl) } // CleanUp removes the TXT record matching the specified parameters -func (r *DNSProviderRoute53) CleanUp(domain, token, keyAuth string) error { +func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) value = `"` + value + `"` return r.changeRecord("DELETE", fqdn, value, ttl) } -func (r *DNSProviderRoute53) changeRecord(action, fqdn, value string, ttl int) error { +func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error { hostedZoneID, err := r.getHostedZoneID(fqdn) if err != nil { return err @@ -82,7 +83,7 @@ func (r *DNSProviderRoute53) changeRecord(action, fqdn, value string, ttl int) e }) } -func (r *DNSProviderRoute53) getHostedZoneID(fqdn string) (string, error) { +func (r *DNSProvider) getHostedZoneID(fqdn string) (string, error) { zones := []route53.HostedZone{} zoneResp, err := r.client.ListHostedZones("", 0) if err != nil { diff --git a/providers/dns/route53/route53_test.go b/providers/dns/route53/route53_test.go index 03f1fa8b..8bb675c3 100644 --- a/providers/dns/route53/route53_test.go +++ b/providers/dns/route53/route53_test.go @@ -91,29 +91,29 @@ func makeRoute53TestServer() *testutil.HTTPServer { return testServer } -func makeRoute53Provider(server *testutil.HTTPServer) *DNSProviderRoute53 { +func makeRoute53Provider(server *testutil.HTTPServer) *DNSProvider { auth := aws.Auth{AccessKey: "abc", SecretKey: "123", Token: ""} client := route53.NewWithClient(auth, aws.Region{Route53Endpoint: server.URL}, testutil.DefaultClient) - return &DNSProviderRoute53{client: client} + return &DNSProvider{client: client} } -func TestNewDNSProviderRoute53Valid(t *testing.T) { +func TestNewDNSProviderValid(t *testing.T) { os.Setenv("AWS_ACCESS_KEY_ID", "") os.Setenv("AWS_SECRET_ACCESS_KEY", "") - _, err := NewDNSProviderRoute53("123", "123", "us-east-1") + _, err := NewDNSProvider("123", "123", "us-east-1") assert.NoError(t, err) restoreRoute53Env() } -func TestNewDNSProviderRoute53ValidEnv(t *testing.T) { +func TestNewDNSProviderValidEnv(t *testing.T) { os.Setenv("AWS_ACCESS_KEY_ID", "123") os.Setenv("AWS_SECRET_ACCESS_KEY", "123") - _, err := NewDNSProviderRoute53("", "", "us-east-1") + _, err := NewDNSProvider("", "", "us-east-1") assert.NoError(t, err) restoreRoute53Env() } -func TestNewDNSProviderRoute53MissingAuthErr(t *testing.T) { +func TestNewDNSProviderMissingAuthErr(t *testing.T) { os.Setenv("AWS_ACCESS_KEY_ID", "") os.Setenv("AWS_SECRET_ACCESS_KEY", "") os.Setenv("AWS_CREDENTIAL_FILE", "") // in case test machine has this variable set @@ -124,7 +124,7 @@ func TestNewDNSProviderRoute53MissingAuthErr(t *testing.T) { awsClient := aws.RetryingClient aws.RetryingClient = &http.Client{Timeout: time.Millisecond} - _, err := NewDNSProviderRoute53("", "", "us-east-1") + _, err := NewDNSProvider("", "", "us-east-1") assert.EqualError(t, err, "No valid AWS authentication found") restoreRoute53Env() @@ -132,8 +132,8 @@ func TestNewDNSProviderRoute53MissingAuthErr(t *testing.T) { aws.RetryingClient = awsClient } -func TestNewDNSProviderRoute53InvalidRegionErr(t *testing.T) { - _, err := NewDNSProviderRoute53("123", "123", "us-east-3") +func TestNewDNSProviderInvalidRegionErr(t *testing.T) { + _, err := NewDNSProvider("123", "123", "us-east-3") assert.EqualError(t, err, "Invalid AWS region name us-east-3") } From c50baa67cb8b557d17842cb333f4affef8908ca7 Mon Sep 17 00:00:00 2001 From: xenolf Date: Fri, 11 Mar 2016 03:52:46 +0100 Subject: [PATCH 41/43] Move WaitFor into new utils.go and switch timeout and interval to time.Duration. --- acme/dns_challenge.go | 24 ------------------------ acme/dns_challenge_test.go | 20 -------------------- acme/utils.go | 29 +++++++++++++++++++++++++++++ acme/utils_test.go | 26 ++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 44 deletions(-) create mode 100644 acme/utils.go create mode 100644 acme/utils_test.go diff --git a/acme/dns_challenge.go b/acme/dns_challenge.go index 659d7082..b7be186f 100644 --- a/acme/dns_challenge.go +++ b/acme/dns_challenge.go @@ -8,7 +8,6 @@ import ( "log" "net" "strings" - "time" "github.com/miekg/dns" "golang.org/x/net/publicsuffix" @@ -256,26 +255,3 @@ func UnFqdn(name string) string { } return name } - -// WaitFor polls the given function 'f', once every 'interval' seconds, up to 'timeout' seconds. -func WaitFor(timeout, interval int, f func() (bool, error)) error { - var lastErr string - timeup := time.After(time.Duration(timeout) * time.Second) - 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(time.Duration(interval) * time.Second) - } -} diff --git a/acme/dns_challenge_test.go b/acme/dns_challenge_test.go index 760c7991..bfc66561 100644 --- a/acme/dns_challenge_test.go +++ b/acme/dns_challenge_test.go @@ -163,23 +163,3 @@ func TestCheckAuthoritativeNssErr(t *testing.T) { } } } - -func TestWaitForTimeout(t *testing.T) { - c := make(chan error) - go func() { - err := WaitFor(3, 1, func() (bool, error) { - return false, nil - }) - c <- err - }() - - timeout := time.After(4 * time.Second) - select { - case <-timeout: - t.Fatal("timeout exceeded") - case err := <-c: - if err == nil { - t.Errorf("expected timeout error; got %v", err) - } - } -} diff --git a/acme/utils.go b/acme/utils.go new file mode 100644 index 00000000..937a8f2d --- /dev/null +++ b/acme/utils.go @@ -0,0 +1,29 @@ +package acme + +import ( + "fmt" + "time" +) + +// WaitFor polls the given function 'f', once every 'interval' seconds, up to 'timeout' seconds. +func WaitFor(timeout, interval time.Duration, f func() (bool, error)) error { + var lastErr string + timeup := time.After(timeout * time.Second) + 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 * time.Second) + } +} diff --git a/acme/utils_test.go b/acme/utils_test.go new file mode 100644 index 00000000..cb837cd5 --- /dev/null +++ b/acme/utils_test.go @@ -0,0 +1,26 @@ +package acme + +import ( + "testing" + "time" +) + +func TestWaitForTimeout(t *testing.T) { + c := make(chan error) + go func() { + err := WaitFor(3, 1, func() (bool, error) { + return false, nil + }) + c <- err + }() + + timeout := time.After(4 * time.Second) + select { + case <-timeout: + t.Fatal("timeout exceeded") + case err := <-c: + if err == nil { + t.Errorf("expected timeout error; got %v", err) + } + } +} From 3252b0bcb971f5fa1bf204e6e099642b2f3c9fbc Mon Sep 17 00:00:00 2001 From: xenolf Date: Fri, 11 Mar 2016 04:51:02 +0100 Subject: [PATCH 42/43] Fix WaitFor calls --- acme/dns_challenge.go | 3 ++- acme/utils.go | 6 +++--- acme/utils_test.go | 2 +- providers/dns/route53/route53.go | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/acme/dns_challenge.go b/acme/dns_challenge.go index b7be186f..e5be0105 100644 --- a/acme/dns_challenge.go +++ b/acme/dns_challenge.go @@ -8,6 +8,7 @@ import ( "log" "net" "strings" + "time" "github.com/miekg/dns" "golang.org/x/net/publicsuffix" @@ -68,7 +69,7 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error { logf("[INFO][%s] Checking DNS record propagation...", domain) - err = WaitFor(30, 2, func() (bool, error) { + err = WaitFor(60*time.Second, 2*time.Second, func() (bool, error) { return preCheckDNS(fqdn, value) }) if err != nil { diff --git a/acme/utils.go b/acme/utils.go index 937a8f2d..2fa0db30 100644 --- a/acme/utils.go +++ b/acme/utils.go @@ -5,10 +5,10 @@ import ( "time" ) -// WaitFor polls the given function 'f', once every 'interval' seconds, up to 'timeout' seconds. +// 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 * time.Second) + timeup := time.After(timeout) for { select { case <-timeup: @@ -24,6 +24,6 @@ func WaitFor(timeout, interval time.Duration, f func() (bool, error)) error { lastErr = err.Error() } - time.Sleep(interval * time.Second) + time.Sleep(interval) } } diff --git a/acme/utils_test.go b/acme/utils_test.go index cb837cd5..158af411 100644 --- a/acme/utils_test.go +++ b/acme/utils_test.go @@ -8,7 +8,7 @@ import ( func TestWaitForTimeout(t *testing.T) { c := make(chan error) go func() { - err := WaitFor(3, 1, func() (bool, error) { + err := WaitFor(3*time.Second, 1*time.Second, func() (bool, error) { return false, nil }) c <- err diff --git a/providers/dns/route53/route53.go b/providers/dns/route53/route53.go index bfca45ac..eb1ffdf3 100644 --- a/providers/dns/route53/route53.go +++ b/providers/dns/route53/route53.go @@ -71,7 +71,7 @@ func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error { return err } - return acme.WaitFor(90, 5, func() (bool, error) { + return acme.WaitFor(90*time.Second, 5*time.Second, func() (bool, error) { status, err := r.client.GetChange(resp.ChangeInfo.ID) if err != nil { return false, err From 98c95e83c9c0ee8958ed34880eac17b407e30e83 Mon Sep 17 00:00:00 2001 From: xenolf Date: Mon, 14 Mar 2016 03:29:29 +0100 Subject: [PATCH 43/43] Add link to account to certificate meta data. --- acme/client.go | 1 + acme/messages.go | 1 + 2 files changed, 2 insertions(+) diff --git a/acme/client.go b/acme/client.go index bc641144..5bda6bc2 100644 --- a/acme/client.go +++ b/acme/client.go @@ -504,6 +504,7 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, if len(cert) > 0 { cerRes.CertStableURL = resp.Header.Get("Content-Location") + cerRes.AccountRef = c.user.GetRegistration().URI issuedCert := pemEncode(derCertificateBytes(cert)) // If bundle is true, we want to return a certificate bundle. diff --git a/acme/messages.go b/acme/messages.go index d238df81..fe3f5748 100644 --- a/acme/messages.go +++ b/acme/messages.go @@ -109,6 +109,7 @@ 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:"-"` }