diff --git a/core/core_test.go b/core/core_test.go index b3ba866d9..1da40cbb0 100644 --- a/core/core_test.go +++ b/core/core_test.go @@ -42,12 +42,7 @@ func TestChallenges(t *testing.T) { var testCertificateRequestBadCSR = []byte(`{"csr":"AAAA"}`) var testCertificateRequestGood = []byte(`{ - "csr": "MIHRMHgCAQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQWUlnRrm5ErSVkTzBTk3isg1hNydfyY4NM1P_N1S-ZeD39HMrYJsQkUh2tKvy3ztfmEqWpekvO4WRktSa000BPoAAwCgYIKoZIzj0EAwMDSQAwRgIhAIZIBwu4xOUD_4dJuGgceSKaoXTFBQKA3BFBNVJvbpdsAiEAlfq3Dq_8dnYbtmyDdXgopeKkSV5_76VSpcog-wkwEwo", - "authorizations": [ - "https://example.com/authz/1", - "https://example.com/authz/2", - "https://example.com/authz/3" - ] + "csr": "MIHRMHgCAQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQWUlnRrm5ErSVkTzBTk3isg1hNydfyY4NM1P_N1S-ZeD39HMrYJsQkUh2tKvy3ztfmEqWpekvO4WRktSa000BPoAAwCgYIKoZIzj0EAwMDSQAwRgIhAIZIBwu4xOUD_4dJuGgceSKaoXTFBQKA3BFBNVJvbpdsAiEAlfq3Dq_8dnYbtmyDdXgopeKkSV5_76VSpcog-wkwEwo" }`) func TestCertificateRequest(t *testing.T) { @@ -61,9 +56,6 @@ func TestCertificateRequest(t *testing.T) { if err = VerifyCSR(goodCR.CSR); err != nil { t.Errorf("Valid CSR in CertificateRequest failed to verify: %v", err) } - if len(goodCR.Authorizations) == 0 { - t.Errorf("Certificate request parsing failed to parse authorizations") - } // Bad CSR var badCR CertificateRequest diff --git a/core/dns.go b/core/dns.go index ffd03e3d5..00d49bfc4 100644 --- a/core/dns.go +++ b/core/dns.go @@ -122,13 +122,18 @@ func (dnsResolver *DNSResolverImpl) LookupHost(hostname string) ([]net.IP, time. return addrs, aRtt, aaaaRtt, nil } -// LookupCNAME sends a DNS query to find a CNAME record associated hostname and returns the -// record target. +// LookupCNAME returns the target name if a CNAME record exists for +// the given domain name. If the CNAME does not exist (NXDOMAIN, +// NXRRSET, or a successful response with no CNAME records), it +// returns the empty string and a nil error. func (dnsResolver *DNSResolverImpl) LookupCNAME(hostname string) (string, time.Duration, error) { r, rtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeCNAME) if err != nil { return "", 0, err } + if r.Rcode == dns.RcodeNXRrset || r.Rcode == dns.RcodeNameError { + return "", rtt, nil + } if r.Rcode != dns.RcodeSuccess { err = fmt.Errorf("DNS failure: %d-%s for CNAME query", r.Rcode, dns.RcodeToString[r.Rcode]) return "", rtt, err @@ -143,9 +148,32 @@ func (dnsResolver *DNSResolverImpl) LookupCNAME(hostname string) (string, time.D return "", rtt, nil } +// LookupDNAME is LookupCNAME, but for DNAME. +func (dnsResolver *DNSResolverImpl) LookupDNAME(hostname string) (string, time.Duration, error) { + r, rtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeDNAME) + if err != nil { + return "", 0, err + } + if r.Rcode == dns.RcodeNXRrset || r.Rcode == dns.RcodeNameError { + return "", rtt, nil + } + if r.Rcode != dns.RcodeSuccess { + err = fmt.Errorf("DNS failure: %d-%s for DNAME query", r.Rcode, dns.RcodeToString[r.Rcode]) + return "", rtt, err + } + + for _, answer := range r.Answer { + if cname, ok := answer.(*dns.DNAME); ok { + return cname.Target, rtt, nil + } + } + + return "", rtt, nil +} + // LookupCAA sends a DNS query to find all CAA records associated with -// the provided hostname. If the response code from the resolver is SERVFAIL -// an empty slice of CAA records is returned. +// the provided hostname. If the response code from the resolver is +// SERVFAIL an empty slice of CAA records is returned. func (dnsResolver *DNSResolverImpl) LookupCAA(hostname string) ([]*dns.CAA, time.Duration, error) { r, rtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeCAA) if err != nil { diff --git a/core/dns_test.go b/core/dns_test.go index a3718944c..e61624ae5 100644 --- a/core/dns_test.go +++ b/core/dns_test.go @@ -9,6 +9,7 @@ import ( "fmt" "net" "os" + "strings" "testing" "time" @@ -25,11 +26,14 @@ func mockDNSQuery(w dns.ResponseWriter, r *dns.Msg) { m.SetReply(r) m.Compress = false + appendAnswer := func(rr dns.RR) { + m.Answer = append(m.Answer, rr) + } for _, q := range r.Question { + q.Name = strings.ToLower(q.Name) if q.Name == "servfail.com." { m.Rcode = dns.RcodeServerFailure - w.WriteMsg(m) - return + break } switch q.Qtype { case dns.TypeSOA: @@ -42,31 +46,50 @@ func mockDNSQuery(w dns.ResponseWriter, r *dns.Msg) { record.Retry = 1 record.Expire = 1 record.Minttl = 1 - - m.Answer = append(m.Answer, record) - w.WriteMsg(m) - return + appendAnswer(record) case dns.TypeA: if q.Name == "cps.letsencrypt.org." { record := new(dns.A) record.Hdr = dns.RR_Header{Name: "cps.letsencrypt.org.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 0} record.A = net.ParseIP("127.0.0.1") - - m.Answer = append(m.Answer, record) - w.WriteMsg(m) - return + appendAnswer(record) + } + case dns.TypeCNAME: + if q.Name == "cname.letsencrypt.org." { + record := new(dns.CNAME) + record.Hdr = dns.RR_Header{Name: "cname.letsencrypt.org.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: 30} + record.Target = "cps.letsencrypt.org." + appendAnswer(record) + } + if q.Name == "cname.example.com." { + record := new(dns.CNAME) + record.Hdr = dns.RR_Header{Name: "cname.example.com.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: 30} + record.Target = "CAA.example.com." + appendAnswer(record) + } + case dns.TypeDNAME: + if q.Name == "dname.letsencrypt.org." { + record := new(dns.DNAME) + record.Hdr = dns.RR_Header{Name: "dname.letsencrypt.org.", Rrtype: dns.TypeDNAME, Class: dns.ClassINET, Ttl: 30} + record.Target = "cps.letsencrypt.org." + appendAnswer(record) } case dns.TypeCAA: - if q.Name == "bracewel.net." { + if q.Name == "bracewel.net." || q.Name == "caa.example.com." { record := new(dns.CAA) - record.Hdr = dns.RR_Header{Name: "bracewel.net.", Rrtype: dns.TypeCAA, Class: dns.ClassINET, Ttl: 0} + record.Hdr = dns.RR_Header{Name: q.Name, Rrtype: dns.TypeCAA, Class: dns.ClassINET, Ttl: 0} record.Tag = "issue" record.Value = "letsencrypt.org" record.Flag = 1 - - m.Answer = append(m.Answer, record) - w.WriteMsg(m) - return + appendAnswer(record) + } + if q.Name == "cname.example.com." { + record := new(dns.CAA) + record.Hdr = dns.RR_Header{Name: "caa.example.com.", Rrtype: dns.TypeCAA, Class: dns.ClassINET, Ttl: 0} + record.Tag = "issue" + record.Value = "letsencrypt.org" + record.Flag = 1 + appendAnswer(record) } } } @@ -157,7 +180,7 @@ func TestDNSServFail(t *testing.T) { test.AssertError(t, err, "LookupCNAME didn't return an error") _, _, _, err = obj.LookupHost(bad) - test.AssertError(t, err, "LookupCNAME didn't return an error") + test.AssertError(t, err, "LookupHost didn't return an error") // CAA lookup ignores validation failures from the resolver for now // and returns an empty list of CAA records. @@ -204,4 +227,32 @@ func TestDNSLookupCAA(t *testing.T) { caas, _, err = obj.LookupCAA("nonexistent.letsencrypt.org") test.AssertNotError(t, err, "CAA lookup failed") test.Assert(t, len(caas) == 0, "Shouldn't have CAA records") + + caas, _, err = obj.LookupCAA("cname.example.com") + test.AssertNotError(t, err, "CAA lookup failed") + test.Assert(t, len(caas) > 0, "Should follow CNAME to find CAA") +} + +func TestDNSLookupCNAME(t *testing.T) { + obj := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr}) + + target, _, err := obj.LookupCNAME("cps.letsencrypt.org") + test.AssertNotError(t, err, "CNAME lookup failed") + test.AssertEquals(t, target, "") + + target, _, err = obj.LookupCNAME("cname.letsencrypt.org") + test.AssertNotError(t, err, "CNAME lookup failed") + test.AssertEquals(t, target, "cps.letsencrypt.org.") +} + +func TestDNSLookupDNAME(t *testing.T) { + obj := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr}) + + target, _, err := obj.LookupDNAME("cps.letsencrypt.org") + test.AssertNotError(t, err, "DNAME lookup failed") + test.AssertEquals(t, target, "") + + target, _, err = obj.LookupDNAME("dname.letsencrypt.org") + test.AssertNotError(t, err, "DNAME lookup failed") + test.AssertEquals(t, target, "cps.letsencrypt.org.") } diff --git a/core/interfaces.go b/core/interfaces.go index 5373bdf38..e33b8da34 100644 --- a/core/interfaces.go +++ b/core/interfaces.go @@ -103,6 +103,7 @@ type StorageGetter interface { GetRegistration(int64) (Registration, error) GetRegistrationByKey(jose.JsonWebKey) (Registration, error) GetAuthorization(string) (Authorization, error) + GetLatestValidAuthorization(int64, AcmeIdentifier) (Authorization, error) GetCertificate(string) (Certificate, error) GetCertificateByShortSerial(string) (Certificate, error) GetCertificateStatus(string) (CertificateStatus, error) @@ -144,6 +145,7 @@ type DNSResolver interface { LookupTXT(string) ([]string, time.Duration, error) LookupHost(string) ([]net.IP, time.Duration, time.Duration, error) LookupCNAME(string) (string, time.Duration, error) + LookupDNAME(string) (string, time.Duration, error) LookupCAA(string) ([]*dns.CAA, time.Duration, error) LookupMX(string) ([]string, time.Duration, error) } diff --git a/core/objects.go b/core/objects.go index 3283ffc11..4eae30057 100644 --- a/core/objects.go +++ b/core/objects.go @@ -154,21 +154,17 @@ type AcmeIdentifier struct { Value string `json:"value"` // The identifier itself } -// CertificateRequest is just a CSR together with -// URIs pointing to authorizations that should collectively -// authorize the certificate being requsted. +// CertificateRequest is just a CSR // // This data is unmarshalled from JSON by way of rawCertificateRequest, which // represents the actual structure received from the client. type CertificateRequest struct { - CSR *x509.CertificateRequest // The CSR - Authorizations []AcmeURL // Links to Authorization over the account key - Bytes []byte // The original bytes of the CSR, for logging. + CSR *x509.CertificateRequest // The CSR + Bytes []byte // The original bytes of the CSR, for logging. } type rawCertificateRequest struct { - CSR JSONBuffer `json:"csr"` // The encoded CSR - Authorizations []AcmeURL `json:"authorizations"` // Authorizations + CSR JSONBuffer `json:"csr"` // The encoded CSR } // UnmarshalJSON provides an implementation for decoding CertificateRequest objects. @@ -184,7 +180,6 @@ func (cr *CertificateRequest) UnmarshalJSON(data []byte) error { } cr.CSR = csr - cr.Authorizations = raw.Authorizations cr.Bytes = raw.CSR return nil } @@ -192,8 +187,7 @@ func (cr *CertificateRequest) UnmarshalJSON(data []byte) error { // MarshalJSON provides an implementation for encoding CertificateRequest objects. func (cr CertificateRequest) MarshalJSON() ([]byte, error) { return json.Marshal(rawCertificateRequest{ - CSR: cr.CSR.Raw, - Authorizations: cr.Authorizations, + CSR: cr.CSR.Raw, }) } diff --git a/mocks/mocks.go b/mocks/mocks.go index 10966c8bd..a773a397b 100644 --- a/mocks/mocks.go +++ b/mocks/mocks.go @@ -9,6 +9,7 @@ import ( "database/sql" "fmt" "net" + "strings" "time" // Load SQLite3 for test purposes @@ -71,14 +72,56 @@ func (mock *MockDNS) LookupHost(hostname string) ([]net.IP, time.Duration, time. // LookupCNAME is a mock func (mock *MockDNS) LookupCNAME(domain string) (string, time.Duration, error) { - return "hostname", 0, nil + switch strings.TrimRight(domain, ".") { + case "cname-absent.com": + return "absent.com.", 30, nil + case "cname-critical.com": + return "critical.com.", 30, nil + case "cname-present.com", "cname-and-dname.com": + return "cname-target.present.com.", 30, nil + case "cname2-present.com": + return "cname-present.com.", 30, nil + case "a.cname-loop.com": + return "b.cname-loop.com.", 30, nil + case "b.cname-loop.com": + return "a.cname-loop.com.", 30, nil + case "www.caa-loop.com": + // nothing wrong with CNAME, but prevents CAA algorithm from terminating + return "oops.www.caa-loop.com.", 30, nil + case "cname2servfail.com": + return "servfail.com.", 30, nil + case "cname-servfail.com": + return "", 0, fmt.Errorf("SERVFAIL") + case "cname2dname.com": + return "dname2cname.com.", 30, nil + default: + return "", 0, nil + } +} + +// LookupDNAME is a mock +func (mock *MockDNS) LookupDNAME(domain string) (string, time.Duration, error) { + switch strings.TrimRight(domain, ".") { + case "cname-and-dname.com", "dname-present.com": + return "dname-target.present.com.", time.Minute, nil + case "a.dname-loop.com": + return "b.dname-loop.com.", time.Minute, nil + case "b.dname-loop.com": + return "a.dname-loop.com.", time.Minute, nil + case "dname2cname.com": + return "cname2-present.com.", time.Minute, nil + case "dname-servfail.com": + return "", time.Minute, fmt.Errorf("SERVFAIL") + default: + return "", 0, nil + } } // LookupCAA is a mock func (mock *MockDNS) LookupCAA(domain string) ([]*dns.CAA, time.Duration, error) { var results []*dns.CAA var record dns.CAA - switch domain { + switch strings.TrimRight(domain, ".") { case "reserved.com": record.Tag = "issue" record.Value = "symantec.com" @@ -100,7 +143,7 @@ func (mock *MockDNS) LookupCAA(domain string) ([]*dns.CAA, time.Duration, error) // LookupMX is a mock func (mock *MockDNS) LookupMX(domain string) ([]string, time.Duration, error) { - switch domain { + switch strings.TrimRight(domain, ".") { case "letsencrypt.org": fallthrough case "email.com": diff --git a/ra/registration-authority.go b/ra/registration-authority.go index 65b182ac3..b24a5de79 100644 --- a/ra/registration-authority.go +++ b/ra/registration-authority.go @@ -279,50 +279,21 @@ func (ra *RegistrationAuthorityImpl) NewCertificate(req core.CertificateRequest, return emptyCert, err } - // Gather authorized domains from the referenced authorizations - authorizedDomains := map[string]bool{} - verificationMethodSet := map[string]bool{} - earliestExpiry := time.Date(2100, 01, 01, 0, 0, 0, 0, time.UTC) + // Check that each requested name has a valid authorization now := time.Now() - for _, url := range req.Authorizations { - id := lastPathSegment(url) - authz, err := ra.SA.GetAuthorization(id) - if err != nil || // Couldn't find authorization - authz.RegistrationID != registration.ID || // Not for this account - authz.Status != core.StatusValid || // Not finalized or not successful - authz.Expires.Before(now) || // Expired - authz.Identifier.Type != core.IdentifierDNS { - // XXX: It may be good to fail here instead of ignoring invalid authorizations. - // However, it seems like this treatment is more in the spirit of Postel's - // law, and it hides information from attackers. - continue + earliestExpiry := time.Date(2100, 01, 01, 0, 0, 0, 0, time.UTC) + for _, name := range names { + authz, err := ra.SA.GetLatestValidAuthorization(registration.ID, core.AcmeIdentifier{Type: core.IdentifierDNS, Value: name}) + if err != nil || authz.Expires.Before(now) { + // unable to find a valid authorization or authz is expired + err = core.UnauthorizedError(fmt.Sprintf("Key not authorized for name %s", name)) + logEvent.Error = err.Error() + return emptyCert, err } if authz.Expires.Before(earliestExpiry) { earliestExpiry = *authz.Expires } - - for _, challenge := range authz.Challenges { - if challenge.Status == core.StatusValid { - verificationMethodSet[challenge.Type] = true - } - } - - authorizedDomains[authz.Identifier.Value] = true - } - verificationMethods := []string{} - for method := range verificationMethodSet { - verificationMethods = append(verificationMethods, method) - } - logEvent.VerificationMethods = verificationMethods - - // Validate all domains - for _, name := range names { - if !authorizedDomains[name] { - err = core.UnauthorizedError(fmt.Sprintf("Key not authorized for name %s", name)) - logEvent.Error = err.Error() - return emptyCert, err - } } // Mark that we verified the CN and SANs diff --git a/ra/registration-authority_test.go b/ra/registration-authority_test.go index 703dd27eb..253293781 100644 --- a/ra/registration-authority_test.go +++ b/ra/registration-authority_test.go @@ -425,10 +425,8 @@ func TestCertificateKeyNotEqualAccountKey(t *testing.T) { test.AssertNotError(t, err, "Failed to parse CSR") sa.UpdatePendingAuthorization(authz) sa.FinalizeAuthorization(authz) - authzURL, _ := url.Parse("http://doesnt.matter/" + authz.ID) certRequest := core.CertificateRequest{ - CSR: parsedCSR, - Authorizations: []core.AcmeURL{core.AcmeURL(*authzURL)}, + CSR: parsedCSR, } // Registration id 1 has key == AccountKeyA @@ -446,14 +444,10 @@ func TestAuthorizationRequired(t *testing.T) { sa.UpdatePendingAuthorization(AuthzFinal) sa.FinalizeAuthorization(AuthzFinal) - // Construct a cert request referencing the authorization - url1, _ := url.Parse("http://doesnt.matter/" + AuthzFinal.ID) - // ExampleCSR requests not-example.com and www.not-example.com, // but the authorization only covers not-example.com certRequest := core.CertificateRequest{ - CSR: ExampleCSR, - Authorizations: []core.AcmeURL{core.AcmeURL(*url1)}, + CSR: ExampleCSR, } _, err := ra.NewCertificate(certRequest, 1) @@ -475,13 +469,8 @@ func TestNewCertificate(t *testing.T) { authzFinalWWW, _ = sa.NewPendingAuthorization(authzFinalWWW) sa.FinalizeAuthorization(authzFinalWWW) - // Construct a cert request referencing the two authorizations - url1, _ := url.Parse("http://doesnt.matter/" + AuthzFinal.ID) - url2, _ := url.Parse("http://doesnt.matter/" + authzFinalWWW.ID) - certRequest := core.CertificateRequest{ - CSR: ExampleCSR, - Authorizations: []core.AcmeURL{core.AcmeURL(*url1), core.AcmeURL(*url2)}, + CSR: ExampleCSR, } cert, err := ra.NewCertificate(certRequest, 1) diff --git a/rpc/rpc-wrappers.go b/rpc/rpc-wrappers.go index 1450a162b..dc943871f 100644 --- a/rpc/rpc-wrappers.go +++ b/rpc/rpc-wrappers.go @@ -48,6 +48,7 @@ const ( MethodGetRegistration = "GetRegistration" // SA MethodGetRegistrationByKey = "GetRegistrationByKey" // RA, SA MethodGetAuthorization = "GetAuthorization" // SA + MethodGetLatestValidAuthorization = "GetLatestValidAuthorization" // SA MethodGetCertificate = "GetCertificate" // SA MethodGetCertificateByShortSerial = "GetCertificateByShortSerial" // SA MethodGetCertificateStatus = "GetCertificateStatus" // SA @@ -84,6 +85,11 @@ type updateAuthorizationRequest struct { Response core.Challenge } +type latestValidAuthorizationRequest struct { + RegID int64 + Identifier core.AcmeIdentifier +} + type certificateRequest struct { Req core.CertificateRequest RegID int64 @@ -714,6 +720,28 @@ func NewStorageAuthorityServer(rpc RPCServer, impl core.StorageAuthority) error return }) + rpc.Handle(MethodGetLatestValidAuthorization, func(req []byte) (response []byte, err error) { + var lvar latestValidAuthorizationRequest + if err = json.Unmarshal(req, &lvar); err != nil { + // AUDIT[ Improper Messages ] 0786b6f2-91ca-4f48-9883-842a19084c64 + improperMessage(MethodNewAuthorization, err, req) + return + } + + authz, err := impl.GetLatestValidAuthorization(lvar.RegID, lvar.Identifier) + if err != nil { + return + } + + response, err = json.Marshal(authz) + if err != nil { + // AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3 + errorCondition(MethodGetLatestValidAuthorization, err, req) + return + } + return + }) + rpc.Handle(MethodAddCertificate, func(req []byte) (response []byte, err error) { var acReq addCertificateRequest err = json.Unmarshal(req, &acReq) @@ -956,6 +984,27 @@ func (cac StorageAuthorityClient) GetAuthorization(id string) (authz core.Author return } +// GetLatestValidAuthorization sends a request to get an Authorization by RegID, Identifier +func (cac StorageAuthorityClient) GetLatestValidAuthorization(registrationId int64, identifier core.AcmeIdentifier) (authz core.Authorization, err error) { + + var lvar latestValidAuthorizationRequest + lvar.RegID = registrationId + lvar.Identifier = identifier + + data, err := json.Marshal(lvar) + if err != nil { + return + } + + jsonAuthz, err := cac.rpc.DispatchSync(MethodGetLatestValidAuthorization, data) + if err != nil { + return + } + + err = json.Unmarshal(jsonAuthz, &authz) + return +} + // GetCertificate sends a request to get a Certificate by ID func (cac StorageAuthorityClient) GetCertificate(id string) (cert core.Certificate, err error) { jsonCert, err := cac.rpc.DispatchSync(MethodGetCertificate, []byte(id)) diff --git a/sa/storage-authority.go b/sa/storage-authority.go index 4b83b29f7..57d2969cb 100644 --- a/sa/storage-authority.go +++ b/sa/storage-authority.go @@ -166,6 +166,20 @@ func (ssa *SQLStorageAuthority) GetAuthorization(id string) (authz core.Authoriz return } +// Get the valid authorization with biggest expire date for a given domain and registrationId +func (ssa *SQLStorageAuthority) GetLatestValidAuthorization(registrationId int64, identifier core.AcmeIdentifier) (authz core.Authorization, err error) { + ident, err := json.Marshal(identifier) + if err != nil { + return + } + err = ssa.dbMap.SelectOne(&authz, "SELECT id, identifier, registrationID, status, expires, challenges, combinations "+ + "FROM authz "+ + "WHERE identifier = :identifier AND registrationID = :registrationId AND status = 'valid' "+ + "ORDER BY expires DESC LIMIT 1", + map[string]interface{}{"identifier": string(ident), "registrationId": registrationId}) + return +} + // GetCertificateByShortSerial takes an id consisting of the first, sequential half of a // serial number and returns the first certificate whose full serial number is // lexically greater than that id. This allows clients to query on the known diff --git a/sa/storage-authority_test.go b/sa/storage-authority_test.go index 228415e2a..c548e2a1f 100644 --- a/sa/storage-authority_test.go +++ b/sa/storage-authority_test.go @@ -123,7 +123,8 @@ func TestAddAuthorization(t *testing.T) { combos[0] = []int{0, 1} exp := time.Now().AddDate(0, 0, 1) - newPa := core.Authorization{ID: PA.ID, Identifier: core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "wut.com"}, RegistrationID: 0, Status: core.StatusPending, Expires: &exp, Challenges: []core.Challenge{chall}, Combinations: combos} + identifier := core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "wut.com"} + newPa := core.Authorization{ID: PA.ID, Identifier: identifier, RegistrationID: 0, Status: core.StatusPending, Expires: &exp, Challenges: []core.Challenge{chall}, Combinations: combos} err = sa.UpdatePendingAuthorization(newPa) test.AssertNotError(t, err, "Couldn't update pending authorization with ID "+PA.ID) @@ -135,6 +136,119 @@ func TestAddAuthorization(t *testing.T) { test.AssertNotError(t, err, "Couldn't get authorization with ID "+PA.ID) } +func CreateDomainAuth(t *testing.T, domainName string, sa *SQLStorageAuthority) (authz core.Authorization) { + // create pending auth + authz, err := sa.NewPendingAuthorization(core.Authorization{}) + test.AssertNotError(t, err, "Couldn't create new pending authorization") + test.Assert(t, authz.ID != "", "ID shouldn't be blank") + + // prepare challenge for auth + uu, err := url.Parse(domainName) + test.AssertNotError(t, err, "Couldn't parse domainName "+domainName) + u := core.AcmeURL(*uu) + chall := core.Challenge{Type: "simpleHttp", Status: core.StatusValid, URI: u, Token: "THISWOULDNTBEAGOODTOKEN", Path: "test-me"} + combos := make([][]int, 1) + combos[0] = []int{0, 1} + exp := time.Now().AddDate(0, 0, 1) // expire in 1 day + + // validate pending auth + authz.Status = core.StatusPending + authz.Identifier = core.AcmeIdentifier{Type: core.IdentifierDNS, Value: domainName} + authz.RegistrationID = 42 + authz.Expires = &exp + authz.Challenges = []core.Challenge{chall} + authz.Combinations = combos + + // save updated auth + err = sa.UpdatePendingAuthorization(authz) + test.AssertNotError(t, err, "Couldn't update pending authorization with ID "+authz.ID) + + return +} + +// Ensure we get only valid authorization with correct RegID +func TestGetLatestValidAuthorizationBasic(t *testing.T) { + sa := initSA(t) + + // attempt to get unauthorized domain + authz, err := sa.GetLatestValidAuthorization(0, core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "example.org"}) + test.AssertError(t, err, "Should not have found a valid auth for example.org") + + // authorize "example.org" + authz = CreateDomainAuth(t, "example.org", sa) + + // finalize auth + authz.Status = core.StatusValid + err = sa.FinalizeAuthorization(authz) + test.AssertNotError(t, err, "Couldn't finalize pending authorization with ID "+authz.ID) + + // attempt to get authorized domain with wrong RegID + authz, err = sa.GetLatestValidAuthorization(0, core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "example.org"}) + test.AssertError(t, err, "Should not have found a valid auth for example.org and regID 0") + + // get authorized domain + authz, err = sa.GetLatestValidAuthorization(42, core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "example.org"}) + test.AssertNotError(t, err, "Should have found a valid auth for example.org and regID 42") + test.AssertEquals(t, authz.Status, core.StatusValid) + test.AssertEquals(t, authz.Identifier.Type, core.IdentifierDNS) + test.AssertEquals(t, authz.Identifier.Value, "example.org") + test.AssertEquals(t, authz.RegistrationID, int64(42)) +} + +// Ensure we get the latest valid authorization for an ident +func TestGetLatestValidAuthorizationMultiple(t *testing.T) { + sa := initSA(t) + domain := "example.org" + ident := core.AcmeIdentifier{Type: core.IdentifierDNS, Value: domain} + regID := int64(42) + var err error + + // create invalid authz + authz := CreateDomainAuth(t, domain, sa) + exp := time.Now().AddDate(0, 0, 10) // expire in 10 day + authz.Expires = &exp + authz.Status = core.StatusInvalid + err = sa.FinalizeAuthorization(authz) + test.AssertNotError(t, err, "Couldn't finalize pending authorization with ID "+authz.ID) + + // should not get the auth + authz, err = sa.GetLatestValidAuthorization(regID, ident) + test.AssertError(t, err, "Should not have found a valid auth for "+domain) + + // create valid auth + authz = CreateDomainAuth(t, domain, sa) + exp = time.Now().AddDate(0, 0, 1) // expire in 1 day + authz.Expires = &exp + authz.Status = core.StatusValid + err = sa.FinalizeAuthorization(authz) + test.AssertNotError(t, err, "Couldn't finalize pending authorization with ID "+authz.ID) + + // should get the valid auth even if it's expire date is lower than the invalid one + authz, err = sa.GetLatestValidAuthorization(regID, ident) + test.AssertNotError(t, err, "Should have found a valid auth for "+domain) + test.AssertEquals(t, authz.Status, core.StatusValid) + test.AssertEquals(t, authz.Identifier.Type, ident.Type) + test.AssertEquals(t, authz.Identifier.Value, ident.Value) + test.AssertEquals(t, authz.RegistrationID, regID) + + // create a newer auth + newAuthz := CreateDomainAuth(t, domain, sa) + exp = time.Now().AddDate(0, 0, 2) // expire in 2 day + newAuthz.Expires = &exp + newAuthz.Status = core.StatusValid + err = sa.FinalizeAuthorization(newAuthz) + test.AssertNotError(t, err, "Couldn't finalize pending authorization with ID "+newAuthz.ID) + + authz, err = sa.GetLatestValidAuthorization(regID, ident) + test.AssertNotError(t, err, "Should have found a valid auth for "+domain) + test.AssertEquals(t, authz.Status, core.StatusValid) + test.AssertEquals(t, authz.Identifier.Type, ident.Type) + test.AssertEquals(t, authz.Identifier.Value, ident.Value) + test.AssertEquals(t, authz.RegistrationID, regID) + // make sure we got the latest auth + test.AssertEquals(t, authz.ID, newAuthz.ID) +} + func TestAddCertificate(t *testing.T) { sa := initSA(t) diff --git a/start.sh b/start.sh deleted file mode 100755 index 4d3ff8807..000000000 --- a/start.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -# Run both boulder and cfssl, using test configs. -if type realpath >/dev/null 2>/dev/null; then - cd $(realpath $(dirname $0)) -fi - -# Kill all children on exit. -export BOULDER_CONFIG=${BOULDER_CONFIG:-test/boulder-config.json} - -exec go run ./cmd/boulder/main.go diff --git a/test/boulder-test-config.json b/test/boulder-test-config.json deleted file mode 100644 index b524583dc..000000000 --- a/test/boulder-test-config.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "syslog": { - "network": "", - "server": "", - "tag": "boulder" - }, - - "amqp": { - "server": "amqp://guest:guest@localhost:5672", - "RA": { - "client": "RA.client", - "server": "RA.server" - }, - "VA": { - "client": "VA.client", - "server": "VA.server" - }, - "SA": { - "client": "SA.client", - "server": "SA.server" - }, - "CA": { - "client": "CA.client", - "server": "CA.server" - } - }, - - "statsd": { - "server": "localhost:8125", - "prefix": "Boulder" - }, - - "wfe": { - "listenAddress": "127.0.0.1:4300", - "certCacheDuration": "6h", - "certNoCacheExpirationWindow": "8765h", - "indexCacheDuration": "24h", - "issuerCacheDuration": "48h", - "debugAddr": "localhost:8000" - }, - - "ca": { - "serialPrefix": 255, - "profile": "ee", - "dbDriver": "sqlite3", - "dbConnect": ":memory:", - "debugAddr": "localhost:8001", - "testMode": true, - "_comment": "This should only be present in testMode. In prod use an HSM.", - "Key": { - "File": "test/test-ca.key" - }, - "expiry": "2160h", - "lifespanOCSP": "96h", - "maxNames": 1000, - "cfssl": { - "signing": { - "profiles": { - "ee": { - "usages": [ - "digital signature", - "key encipherment", - "server auth", - "client auth" - ], - "backdate": "1h", - "is_ca": false, - "issuer_urls": [ - "http://int-x1.letsencrypt.org/cert" - ], - "ocsp_url": "http://int-x1.letsencrypt.org/ocsp", - "crl_url": "http://int-x1.letsencrypt.org/crl", - "policies": [ - { - "ID": "2.23.140.1.2.1" - }, - { - "ID": "1.2.3.4", - "Qualifiers": [ { - "type": "id-qt-cps", - "value": "http://example.com/cps" - }, { - "type": "id-qt-unotice", - "value": "Do What Thou Wilt" - } ] - } - ], - "expiry": "8760h", - "CSRWhitelist": { - "PublicKeyAlgorithm": true, - "PublicKey": true, - "SignatureAlgorithm": true - }, - "UseSerialSeq": true - } - }, - "default": { - "usages": [ - "digital signature" - ], - "expiry": "8760h" - } - } - } - }, - - "ra": { - "debugAddr": "localhost:8002" - }, - - "sa": { - "dbDriver": "sqlite3", - "dbConnect": ":memory:", - "debugAddr": "localhost:8003" - }, - - "va": { - "userAgent": "boulder", - "debugAddr": "localhost:8004" - }, - - "sql": { - "SQLDebug": false, - "CreateTables": true - }, - - "mail": { - "server": "mail.example.com", - "port": "25", - "username": "cert-master@example.com", - "password": "password" - }, - - "common": { - "baseURL": "http://localhost:4300", - "issuerCert": "test/test-ca.pem", - "maxKeySize": 4096, - "dnsResolver": "127.0.0.1:8053", - "dnsTimeout": "10s" - }, - - "subscriberAgreementURL": "http://localhost:4300/terms/" -} diff --git a/test/integration-test.py b/test/integration-test.py deleted file mode 100755 index 43c50f724..000000000 --- a/test/integration-test.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python2.7 -import os -import shutil -import socket -import subprocess -import sys -import tempfile -import time - -tempdir = tempfile.mkdtemp() - -exit_status = 0 - -def die(): - global exit_status - exit_status = 1 - sys.exit(1) - -def build(path): - cmd = 'go build -o %s/%s %s' % (tempdir, os.path.basename(path), path) - print(cmd) - if subprocess.Popen(cmd, shell=True).wait() != 0: - die() - -build('./cmd/boulder') - -boulder = subprocess.Popen(''' - exec %s/boulder --config test/boulder-test-config.json - ''' % tempdir, shell=True) - -def run_test(): - os.chdir('test/js') - - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - # Wait up to 7 seconds for Boulder to come up. - for i in range(0, 7): - try: - s.connect(('localhost', 4300)) - break - except socket.error, e: - time.sleep(1) - - if subprocess.Popen('npm install', shell=True).wait() != 0: - die() - - issue = subprocess.Popen(''' - node test.js --email foo@letsencrypt.org --agree true \ - --domains foo.com --new-reg http://localhost:4300/acme/new-reg \ - --certKey %s/key.pem --cert %s/cert.der - ''' % (tempdir, tempdir), shell=True) - if issue.wait() != 0: - die() - revoke = subprocess.Popen(''' - node revoke.js %s/cert.der %s/key.pem http://localhost:4300/acme/revoke-cert - ''' % (tempdir, tempdir), shell=True) - if revoke.wait() != 0: - die() - -try: - run_test() -except Exception as e: - exit_status = 1 - print e -finally: - # Check whether boulder died. This can happen, for instance, if there was an - # existing boulder already listening on the port. - if boulder.poll() is not None: - print("Boulder died") - exit_status = 1 - else: - boulder.kill() - - shutil.rmtree(tempdir) - - if exit_status == 0: - print("SUCCESS") - else: - print("FAILURE") - sys.exit(exit_status) diff --git a/va/validation-authority.go b/va/validation-authority.go index a6c3bf285..5f299f7fe 100644 --- a/va/validation-authority.go +++ b/va/validation-authority.go @@ -9,6 +9,7 @@ import ( "crypto/sha256" "crypto/subtle" "crypto/tls" + "errors" "fmt" "io/ioutil" "net" @@ -24,6 +25,12 @@ import ( "github.com/letsencrypt/boulder/policy" ) +const maxCNAME = 16 // Prevents infinite loops. Same limit as BIND. + +// Returned by CheckCAARecords if it has to follow too many +// consecutive CNAME lookups. +var ErrTooManyCNAME = errors.New("too many CNAME/DNAME lookups") + // ValidationAuthorityImpl represents a VA type ValidationAuthorityImpl struct { RA core.RegistrationAuthority @@ -437,36 +444,50 @@ func newCAASet(CAAs []*dns.CAA) *CAASet { } func (va *ValidationAuthorityImpl) getCAASet(hostname string) (*CAASet, error) { - hostname = strings.TrimRight(hostname, ".") - splitDomain := strings.Split(hostname, ".") - // RFC 6844 CAA set query sequence, 'x.y.z.com' => ['x.y.z.com', 'y.z.com', 'z.com'] - for i := range splitDomain { - queryDomain := strings.Join(splitDomain[i:], ".") - // Don't query a public suffix - if _, present := policy.PublicSuffixList[queryDomain]; present { + label := strings.TrimRight(hostname, ".") + cnames := 0 + // See RFC 6844 "Certification Authority Processing" for pseudocode. + for { + if strings.IndexRune(label, '.') == -1 { + // Reached TLD break } - - // Query CAA records for domain and its alias if it has a CNAME - for _, alias := range []bool{false, true} { - if alias { - target, _, err := va.DNSResolver.LookupCNAME(queryDomain) - if err != nil { - return nil, err - } - queryDomain = target - } - CAAs, _, err := va.DNSResolver.LookupCAA(queryDomain) - if err != nil { - return nil, err - } - - if len(CAAs) > 0 { - return newCAASet(CAAs), nil - } + if _, present := policy.PublicSuffixList[label]; present { + break + } + CAAs, _, err := va.DNSResolver.LookupCAA(label) + if err != nil { + return nil, err + } + if len(CAAs) > 0 { + return newCAASet(CAAs), nil + } + cname, _, err := va.DNSResolver.LookupCNAME(label) + if err != nil { + return nil, err + } + dname, _, err := va.DNSResolver.LookupDNAME(label) + if err != nil { + return nil, err + } + if cname == "" && dname == "" { + // Try parent domain (note we confirmed + // earlier that label contains '.') + label = label[strings.IndexRune(label, '.')+1:] + continue + } + if cname != "" && dname != "" && cname != dname { + return nil, errors.New("both CNAME and DNAME exist for " + label) + } + if cname != "" { + label = cname + } else { + label = dname + } + if cnames++; cnames > maxCNAME { + return nil, ErrTooManyCNAME } } - // no CAA records found return nil, nil } diff --git a/va/validation-authority_test.go b/va/validation-authority_test.go index 608a34d4f..b24502ffb 100644 --- a/va/validation-authority_test.go +++ b/va/validation-authority_test.go @@ -564,10 +564,23 @@ func TestCAAChecking(t *testing.T) { CAATest{"reserved.com", true, false}, // Critical CAATest{"critical.com", true, false}, + CAATest{"nx.critical.com", true, false}, + CAATest{"cname-critical.com", true, false}, + CAATest{"nx.cname-critical.com", true, false}, // Good (absent) CAATest{"absent.com", false, true}, + CAATest{"cname-absent.com", false, true}, + CAATest{"nx.cname-absent.com", false, true}, + CAATest{"cname-nx.com", false, true}, + CAATest{"example.co.uk", false, true}, // Good (present) CAATest{"present.com", true, true}, + CAATest{"cname-present.com", true, true}, + CAATest{"cname2-present.com", true, true}, + CAATest{"nx.cname2-present.com", true, true}, + CAATest{"dname-present.com", true, true}, + CAATest{"dname2cname.com", true, true}, + // CNAME to critical } va := NewValidationAuthorityImpl(true) @@ -585,6 +598,20 @@ func TestCAAChecking(t *testing.T) { test.AssertError(t, err, "servfail.com") test.Assert(t, !present, "Present should be false") test.Assert(t, !valid, "Valid should be false") + + for _, name := range []string{ + "www.caa-loop.com", + "a.cname-loop.com", + "a.dname-loop.com", + "cname-servfail.com", + "cname2servfail.com", + "dname-servfail.com", + "cname-and-dname.com", + "servfail.com", + } { + _, _, err = va.CheckCAARecords(core.AcmeIdentifier{Type: "dns", Value: name}) + test.AssertError(t, err, name) + } } func TestDNSValidationFailure(t *testing.T) { diff --git a/wfe/web-front-end.go b/wfe/web-front-end.go index 956bb1d85..84f640cf3 100644 --- a/wfe/web-front-end.go +++ b/wfe/web-front-end.go @@ -16,7 +16,6 @@ import ( "net/http" "net/url" "regexp" - "runtime" "strconv" "strings" "time" @@ -703,7 +702,6 @@ func (wfe *WebFrontEndImpl) NewCertificate(response http.ResponseWriter, request return } wfe.logCsr(request.RemoteAddr, init, reg) - logEvent.Extra["Authorizations"] = init.Authorizations logEvent.Extra["CSRDNSNames"] = init.CSR.DNSNames logEvent.Extra["CSREmailAddresses"] = init.CSR.EmailAddresses logEvent.Extra["CSRIPAddresses"] = init.CSR.IPAddresses @@ -1083,7 +1081,7 @@ func (wfe *WebFrontEndImpl) BuildID(response http.ResponseWriter, request *http. response.Header().Set("Content-Type", "text/plain") response.WriteHeader(http.StatusOK) - detailsString := fmt.Sprintf("Boulder=(%s %s) Golang=(%s) BuildHost=(%s)", core.GetBuildID(), core.GetBuildTime(), runtime.Version(), core.GetBuildHost()) + detailsString := fmt.Sprintf("Boulder=(%s %s)", core.GetBuildID(), core.GetBuildTime()) if _, err := fmt.Fprintln(response, detailsString); err != nil { logEvent.Error = err.Error() wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err)) diff --git a/wfe/web-front-end_test.go b/wfe/web-front-end_test.go index bb5ca3e93..0f029c241 100644 --- a/wfe/web-front-end_test.go +++ b/wfe/web-front-end_test.go @@ -130,7 +130,7 @@ wk6Oiadty3eQqSBJv0HnpmiEdQVffIK5Pg4M8Dd+aOBnEkbopAJOuA== ) type MockSA struct { - // empty + authorizedDomains map[string]bool } func (sa *MockSA) GetRegistration(id int64) (core.Registration, error) { @@ -177,6 +177,16 @@ func (sa *MockSA) GetAuthorization(id string) (core.Authorization, error) { return core.Authorization{}, nil } +func (sa *MockSA) GetLatestValidAuthorization(registrationId int64, identifier core.AcmeIdentifier) (authz core.Authorization, err error) { + if registrationId == 1 && identifier.Type == "dns" { + if sa.authorizedDomains[identifier.Value] || identifier.Value == "not-an-example.com" { + exp := time.Now().AddDate(100, 0, 0) + return core.Authorization{Status: core.StatusValid, RegistrationID: 1, Expires: &exp, Identifier: identifier}, nil + } + } + return core.Authorization{}, errors.New("no authz") +} + func (sa *MockSA) GetCertificate(serial string) (core.Certificate, error) { // Serial ee == 238.crt if serial == "000000000000000000000000000000ee" { @@ -225,6 +235,9 @@ func (sa *MockSA) AddCertificate(certDER []byte, regID int64) (digest string, er } func (sa *MockSA) FinalizeAuthorization(authz core.Authorization) (err error) { + if authz.Status == core.StatusValid && authz.Identifier.Type == core.IdentifierDNS { + sa.authorizedDomains[authz.Identifier.Value] = true + } return }