Merge master

This commit is contained in:
Richard Barnes 2015-07-29 16:37:39 -04:00
commit 5ea17d980a
18 changed files with 433 additions and 359 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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.")
}

View File

@ -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)
}

View File

@ -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,
})
}

View File

@ -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":

View File

@ -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

View File

@ -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)

View File

@ -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))

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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/"
}

View File

@ -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)

View File

@ -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
}

View File

@ -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) {

View File

@ -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))

View File

@ -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
}