Merge master
This commit is contained in:
commit
5ea17d980a
|
|
@ -42,12 +42,7 @@ func TestChallenges(t *testing.T) {
|
||||||
|
|
||||||
var testCertificateRequestBadCSR = []byte(`{"csr":"AAAA"}`)
|
var testCertificateRequestBadCSR = []byte(`{"csr":"AAAA"}`)
|
||||||
var testCertificateRequestGood = []byte(`{
|
var testCertificateRequestGood = []byte(`{
|
||||||
"csr": "MIHRMHgCAQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQWUlnRrm5ErSVkTzBTk3isg1hNydfyY4NM1P_N1S-ZeD39HMrYJsQkUh2tKvy3ztfmEqWpekvO4WRktSa000BPoAAwCgYIKoZIzj0EAwMDSQAwRgIhAIZIBwu4xOUD_4dJuGgceSKaoXTFBQKA3BFBNVJvbpdsAiEAlfq3Dq_8dnYbtmyDdXgopeKkSV5_76VSpcog-wkwEwo",
|
"csr": "MIHRMHgCAQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQWUlnRrm5ErSVkTzBTk3isg1hNydfyY4NM1P_N1S-ZeD39HMrYJsQkUh2tKvy3ztfmEqWpekvO4WRktSa000BPoAAwCgYIKoZIzj0EAwMDSQAwRgIhAIZIBwu4xOUD_4dJuGgceSKaoXTFBQKA3BFBNVJvbpdsAiEAlfq3Dq_8dnYbtmyDdXgopeKkSV5_76VSpcog-wkwEwo"
|
||||||
"authorizations": [
|
|
||||||
"https://example.com/authz/1",
|
|
||||||
"https://example.com/authz/2",
|
|
||||||
"https://example.com/authz/3"
|
|
||||||
]
|
|
||||||
}`)
|
}`)
|
||||||
|
|
||||||
func TestCertificateRequest(t *testing.T) {
|
func TestCertificateRequest(t *testing.T) {
|
||||||
|
|
@ -61,9 +56,6 @@ func TestCertificateRequest(t *testing.T) {
|
||||||
if err = VerifyCSR(goodCR.CSR); err != nil {
|
if err = VerifyCSR(goodCR.CSR); err != nil {
|
||||||
t.Errorf("Valid CSR in CertificateRequest failed to verify: %v", err)
|
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
|
// Bad CSR
|
||||||
var badCR CertificateRequest
|
var badCR CertificateRequest
|
||||||
|
|
|
||||||
36
core/dns.go
36
core/dns.go
|
|
@ -122,13 +122,18 @@ func (dnsResolver *DNSResolverImpl) LookupHost(hostname string) ([]net.IP, time.
|
||||||
return addrs, aRtt, aaaaRtt, nil
|
return addrs, aRtt, aaaaRtt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LookupCNAME sends a DNS query to find a CNAME record associated hostname and returns the
|
// LookupCNAME returns the target name if a CNAME record exists for
|
||||||
// record target.
|
// 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) {
|
func (dnsResolver *DNSResolverImpl) LookupCNAME(hostname string) (string, time.Duration, error) {
|
||||||
r, rtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeCNAME)
|
r, rtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeCNAME)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, err
|
return "", 0, err
|
||||||
}
|
}
|
||||||
|
if r.Rcode == dns.RcodeNXRrset || r.Rcode == dns.RcodeNameError {
|
||||||
|
return "", rtt, nil
|
||||||
|
}
|
||||||
if r.Rcode != dns.RcodeSuccess {
|
if r.Rcode != dns.RcodeSuccess {
|
||||||
err = fmt.Errorf("DNS failure: %d-%s for CNAME query", r.Rcode, dns.RcodeToString[r.Rcode])
|
err = fmt.Errorf("DNS failure: %d-%s for CNAME query", r.Rcode, dns.RcodeToString[r.Rcode])
|
||||||
return "", rtt, err
|
return "", rtt, err
|
||||||
|
|
@ -143,9 +148,32 @@ func (dnsResolver *DNSResolverImpl) LookupCNAME(hostname string) (string, time.D
|
||||||
return "", rtt, nil
|
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
|
// LookupCAA sends a DNS query to find all CAA records associated with
|
||||||
// the provided hostname. If the response code from the resolver is SERVFAIL
|
// the provided hostname. If the response code from the resolver is
|
||||||
// an empty slice of CAA records is returned.
|
// SERVFAIL an empty slice of CAA records is returned.
|
||||||
func (dnsResolver *DNSResolverImpl) LookupCAA(hostname string) ([]*dns.CAA, time.Duration, error) {
|
func (dnsResolver *DNSResolverImpl) LookupCAA(hostname string) ([]*dns.CAA, time.Duration, error) {
|
||||||
r, rtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeCAA)
|
r, rtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeCAA)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -25,11 +26,14 @@ func mockDNSQuery(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
m.SetReply(r)
|
m.SetReply(r)
|
||||||
m.Compress = false
|
m.Compress = false
|
||||||
|
|
||||||
|
appendAnswer := func(rr dns.RR) {
|
||||||
|
m.Answer = append(m.Answer, rr)
|
||||||
|
}
|
||||||
for _, q := range r.Question {
|
for _, q := range r.Question {
|
||||||
|
q.Name = strings.ToLower(q.Name)
|
||||||
if q.Name == "servfail.com." {
|
if q.Name == "servfail.com." {
|
||||||
m.Rcode = dns.RcodeServerFailure
|
m.Rcode = dns.RcodeServerFailure
|
||||||
w.WriteMsg(m)
|
break
|
||||||
return
|
|
||||||
}
|
}
|
||||||
switch q.Qtype {
|
switch q.Qtype {
|
||||||
case dns.TypeSOA:
|
case dns.TypeSOA:
|
||||||
|
|
@ -42,31 +46,50 @@ func mockDNSQuery(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
record.Retry = 1
|
record.Retry = 1
|
||||||
record.Expire = 1
|
record.Expire = 1
|
||||||
record.Minttl = 1
|
record.Minttl = 1
|
||||||
|
appendAnswer(record)
|
||||||
m.Answer = append(m.Answer, record)
|
|
||||||
w.WriteMsg(m)
|
|
||||||
return
|
|
||||||
case dns.TypeA:
|
case dns.TypeA:
|
||||||
if q.Name == "cps.letsencrypt.org." {
|
if q.Name == "cps.letsencrypt.org." {
|
||||||
record := new(dns.A)
|
record := new(dns.A)
|
||||||
record.Hdr = dns.RR_Header{Name: "cps.letsencrypt.org.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 0}
|
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")
|
record.A = net.ParseIP("127.0.0.1")
|
||||||
|
appendAnswer(record)
|
||||||
m.Answer = append(m.Answer, record)
|
}
|
||||||
w.WriteMsg(m)
|
case dns.TypeCNAME:
|
||||||
return
|
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:
|
case dns.TypeCAA:
|
||||||
if q.Name == "bracewel.net." {
|
if q.Name == "bracewel.net." || q.Name == "caa.example.com." {
|
||||||
record := new(dns.CAA)
|
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.Tag = "issue"
|
||||||
record.Value = "letsencrypt.org"
|
record.Value = "letsencrypt.org"
|
||||||
record.Flag = 1
|
record.Flag = 1
|
||||||
|
appendAnswer(record)
|
||||||
m.Answer = append(m.Answer, record)
|
}
|
||||||
w.WriteMsg(m)
|
if q.Name == "cname.example.com." {
|
||||||
return
|
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")
|
test.AssertError(t, err, "LookupCNAME didn't return an error")
|
||||||
|
|
||||||
_, _, _, err = obj.LookupHost(bad)
|
_, _, _, 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
|
// CAA lookup ignores validation failures from the resolver for now
|
||||||
// and returns an empty list of CAA records.
|
// and returns an empty list of CAA records.
|
||||||
|
|
@ -204,4 +227,32 @@ func TestDNSLookupCAA(t *testing.T) {
|
||||||
caas, _, err = obj.LookupCAA("nonexistent.letsencrypt.org")
|
caas, _, err = obj.LookupCAA("nonexistent.letsencrypt.org")
|
||||||
test.AssertNotError(t, err, "CAA lookup failed")
|
test.AssertNotError(t, err, "CAA lookup failed")
|
||||||
test.Assert(t, len(caas) == 0, "Shouldn't have CAA records")
|
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.")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,7 @@ type StorageGetter interface {
|
||||||
GetRegistration(int64) (Registration, error)
|
GetRegistration(int64) (Registration, error)
|
||||||
GetRegistrationByKey(jose.JsonWebKey) (Registration, error)
|
GetRegistrationByKey(jose.JsonWebKey) (Registration, error)
|
||||||
GetAuthorization(string) (Authorization, error)
|
GetAuthorization(string) (Authorization, error)
|
||||||
|
GetLatestValidAuthorization(int64, AcmeIdentifier) (Authorization, error)
|
||||||
GetCertificate(string) (Certificate, error)
|
GetCertificate(string) (Certificate, error)
|
||||||
GetCertificateByShortSerial(string) (Certificate, error)
|
GetCertificateByShortSerial(string) (Certificate, error)
|
||||||
GetCertificateStatus(string) (CertificateStatus, error)
|
GetCertificateStatus(string) (CertificateStatus, error)
|
||||||
|
|
@ -144,6 +145,7 @@ type DNSResolver interface {
|
||||||
LookupTXT(string) ([]string, time.Duration, error)
|
LookupTXT(string) ([]string, time.Duration, error)
|
||||||
LookupHost(string) ([]net.IP, time.Duration, time.Duration, error)
|
LookupHost(string) ([]net.IP, time.Duration, time.Duration, error)
|
||||||
LookupCNAME(string) (string, time.Duration, error)
|
LookupCNAME(string) (string, time.Duration, error)
|
||||||
|
LookupDNAME(string) (string, time.Duration, error)
|
||||||
LookupCAA(string) ([]*dns.CAA, time.Duration, error)
|
LookupCAA(string) ([]*dns.CAA, time.Duration, error)
|
||||||
LookupMX(string) ([]string, time.Duration, error)
|
LookupMX(string) ([]string, time.Duration, error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -154,21 +154,17 @@ type AcmeIdentifier struct {
|
||||||
Value string `json:"value"` // The identifier itself
|
Value string `json:"value"` // The identifier itself
|
||||||
}
|
}
|
||||||
|
|
||||||
// CertificateRequest is just a CSR together with
|
// CertificateRequest is just a CSR
|
||||||
// URIs pointing to authorizations that should collectively
|
|
||||||
// authorize the certificate being requsted.
|
|
||||||
//
|
//
|
||||||
// This data is unmarshalled from JSON by way of rawCertificateRequest, which
|
// This data is unmarshalled from JSON by way of rawCertificateRequest, which
|
||||||
// represents the actual structure received from the client.
|
// represents the actual structure received from the client.
|
||||||
type CertificateRequest struct {
|
type CertificateRequest struct {
|
||||||
CSR *x509.CertificateRequest // The CSR
|
CSR *x509.CertificateRequest // The CSR
|
||||||
Authorizations []AcmeURL // Links to Authorization over the account key
|
|
||||||
Bytes []byte // The original bytes of the CSR, for logging.
|
Bytes []byte // The original bytes of the CSR, for logging.
|
||||||
}
|
}
|
||||||
|
|
||||||
type rawCertificateRequest struct {
|
type rawCertificateRequest struct {
|
||||||
CSR JSONBuffer `json:"csr"` // The encoded CSR
|
CSR JSONBuffer `json:"csr"` // The encoded CSR
|
||||||
Authorizations []AcmeURL `json:"authorizations"` // Authorizations
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON provides an implementation for decoding CertificateRequest objects.
|
// UnmarshalJSON provides an implementation for decoding CertificateRequest objects.
|
||||||
|
|
@ -184,7 +180,6 @@ func (cr *CertificateRequest) UnmarshalJSON(data []byte) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
cr.CSR = csr
|
cr.CSR = csr
|
||||||
cr.Authorizations = raw.Authorizations
|
|
||||||
cr.Bytes = raw.CSR
|
cr.Bytes = raw.CSR
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -193,7 +188,6 @@ func (cr *CertificateRequest) UnmarshalJSON(data []byte) error {
|
||||||
func (cr CertificateRequest) MarshalJSON() ([]byte, error) {
|
func (cr CertificateRequest) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(rawCertificateRequest{
|
return json.Marshal(rawCertificateRequest{
|
||||||
CSR: cr.CSR.Raw,
|
CSR: cr.CSR.Raw,
|
||||||
Authorizations: cr.Authorizations,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
// Load SQLite3 for test purposes
|
// Load SQLite3 for test purposes
|
||||||
|
|
@ -71,14 +72,56 @@ func (mock *MockDNS) LookupHost(hostname string) ([]net.IP, time.Duration, time.
|
||||||
|
|
||||||
// LookupCNAME is a mock
|
// LookupCNAME is a mock
|
||||||
func (mock *MockDNS) LookupCNAME(domain string) (string, time.Duration, error) {
|
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
|
// LookupCAA is a mock
|
||||||
func (mock *MockDNS) LookupCAA(domain string) ([]*dns.CAA, time.Duration, error) {
|
func (mock *MockDNS) LookupCAA(domain string) ([]*dns.CAA, time.Duration, error) {
|
||||||
var results []*dns.CAA
|
var results []*dns.CAA
|
||||||
var record dns.CAA
|
var record dns.CAA
|
||||||
switch domain {
|
switch strings.TrimRight(domain, ".") {
|
||||||
case "reserved.com":
|
case "reserved.com":
|
||||||
record.Tag = "issue"
|
record.Tag = "issue"
|
||||||
record.Value = "symantec.com"
|
record.Value = "symantec.com"
|
||||||
|
|
@ -100,7 +143,7 @@ func (mock *MockDNS) LookupCAA(domain string) ([]*dns.CAA, time.Duration, error)
|
||||||
|
|
||||||
// LookupMX is a mock
|
// LookupMX is a mock
|
||||||
func (mock *MockDNS) LookupMX(domain string) ([]string, time.Duration, error) {
|
func (mock *MockDNS) LookupMX(domain string) ([]string, time.Duration, error) {
|
||||||
switch domain {
|
switch strings.TrimRight(domain, ".") {
|
||||||
case "letsencrypt.org":
|
case "letsencrypt.org":
|
||||||
fallthrough
|
fallthrough
|
||||||
case "email.com":
|
case "email.com":
|
||||||
|
|
|
||||||
|
|
@ -279,50 +279,21 @@ func (ra *RegistrationAuthorityImpl) NewCertificate(req core.CertificateRequest,
|
||||||
return emptyCert, err
|
return emptyCert, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gather authorized domains from the referenced authorizations
|
// Check that each requested name has a valid authorization
|
||||||
authorizedDomains := map[string]bool{}
|
|
||||||
verificationMethodSet := map[string]bool{}
|
|
||||||
earliestExpiry := time.Date(2100, 01, 01, 0, 0, 0, 0, time.UTC)
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
for _, url := range req.Authorizations {
|
earliestExpiry := time.Date(2100, 01, 01, 0, 0, 0, 0, time.UTC)
|
||||||
id := lastPathSegment(url)
|
for _, name := range names {
|
||||||
authz, err := ra.SA.GetAuthorization(id)
|
authz, err := ra.SA.GetLatestValidAuthorization(registration.ID, core.AcmeIdentifier{Type: core.IdentifierDNS, Value: name})
|
||||||
if err != nil || // Couldn't find authorization
|
if err != nil || authz.Expires.Before(now) {
|
||||||
authz.RegistrationID != registration.ID || // Not for this account
|
// unable to find a valid authorization or authz is expired
|
||||||
authz.Status != core.StatusValid || // Not finalized or not successful
|
err = core.UnauthorizedError(fmt.Sprintf("Key not authorized for name %s", name))
|
||||||
authz.Expires.Before(now) || // Expired
|
logEvent.Error = err.Error()
|
||||||
authz.Identifier.Type != core.IdentifierDNS {
|
return emptyCert, err
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if authz.Expires.Before(earliestExpiry) {
|
if authz.Expires.Before(earliestExpiry) {
|
||||||
earliestExpiry = *authz.Expires
|
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
|
// Mark that we verified the CN and SANs
|
||||||
|
|
|
||||||
|
|
@ -425,10 +425,8 @@ func TestCertificateKeyNotEqualAccountKey(t *testing.T) {
|
||||||
test.AssertNotError(t, err, "Failed to parse CSR")
|
test.AssertNotError(t, err, "Failed to parse CSR")
|
||||||
sa.UpdatePendingAuthorization(authz)
|
sa.UpdatePendingAuthorization(authz)
|
||||||
sa.FinalizeAuthorization(authz)
|
sa.FinalizeAuthorization(authz)
|
||||||
authzURL, _ := url.Parse("http://doesnt.matter/" + authz.ID)
|
|
||||||
certRequest := core.CertificateRequest{
|
certRequest := core.CertificateRequest{
|
||||||
CSR: parsedCSR,
|
CSR: parsedCSR,
|
||||||
Authorizations: []core.AcmeURL{core.AcmeURL(*authzURL)},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registration id 1 has key == AccountKeyA
|
// Registration id 1 has key == AccountKeyA
|
||||||
|
|
@ -446,14 +444,10 @@ func TestAuthorizationRequired(t *testing.T) {
|
||||||
sa.UpdatePendingAuthorization(AuthzFinal)
|
sa.UpdatePendingAuthorization(AuthzFinal)
|
||||||
sa.FinalizeAuthorization(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,
|
// ExampleCSR requests not-example.com and www.not-example.com,
|
||||||
// but the authorization only covers not-example.com
|
// but the authorization only covers not-example.com
|
||||||
certRequest := core.CertificateRequest{
|
certRequest := core.CertificateRequest{
|
||||||
CSR: ExampleCSR,
|
CSR: ExampleCSR,
|
||||||
Authorizations: []core.AcmeURL{core.AcmeURL(*url1)},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := ra.NewCertificate(certRequest, 1)
|
_, err := ra.NewCertificate(certRequest, 1)
|
||||||
|
|
@ -475,13 +469,8 @@ func TestNewCertificate(t *testing.T) {
|
||||||
authzFinalWWW, _ = sa.NewPendingAuthorization(authzFinalWWW)
|
authzFinalWWW, _ = sa.NewPendingAuthorization(authzFinalWWW)
|
||||||
sa.FinalizeAuthorization(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{
|
certRequest := core.CertificateRequest{
|
||||||
CSR: ExampleCSR,
|
CSR: ExampleCSR,
|
||||||
Authorizations: []core.AcmeURL{core.AcmeURL(*url1), core.AcmeURL(*url2)},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cert, err := ra.NewCertificate(certRequest, 1)
|
cert, err := ra.NewCertificate(certRequest, 1)
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ const (
|
||||||
MethodGetRegistration = "GetRegistration" // SA
|
MethodGetRegistration = "GetRegistration" // SA
|
||||||
MethodGetRegistrationByKey = "GetRegistrationByKey" // RA, SA
|
MethodGetRegistrationByKey = "GetRegistrationByKey" // RA, SA
|
||||||
MethodGetAuthorization = "GetAuthorization" // SA
|
MethodGetAuthorization = "GetAuthorization" // SA
|
||||||
|
MethodGetLatestValidAuthorization = "GetLatestValidAuthorization" // SA
|
||||||
MethodGetCertificate = "GetCertificate" // SA
|
MethodGetCertificate = "GetCertificate" // SA
|
||||||
MethodGetCertificateByShortSerial = "GetCertificateByShortSerial" // SA
|
MethodGetCertificateByShortSerial = "GetCertificateByShortSerial" // SA
|
||||||
MethodGetCertificateStatus = "GetCertificateStatus" // SA
|
MethodGetCertificateStatus = "GetCertificateStatus" // SA
|
||||||
|
|
@ -84,6 +85,11 @@ type updateAuthorizationRequest struct {
|
||||||
Response core.Challenge
|
Response core.Challenge
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type latestValidAuthorizationRequest struct {
|
||||||
|
RegID int64
|
||||||
|
Identifier core.AcmeIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
type certificateRequest struct {
|
type certificateRequest struct {
|
||||||
Req core.CertificateRequest
|
Req core.CertificateRequest
|
||||||
RegID int64
|
RegID int64
|
||||||
|
|
@ -714,6 +720,28 @@ func NewStorageAuthorityServer(rpc RPCServer, impl core.StorageAuthority) error
|
||||||
return
|
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) {
|
rpc.Handle(MethodAddCertificate, func(req []byte) (response []byte, err error) {
|
||||||
var acReq addCertificateRequest
|
var acReq addCertificateRequest
|
||||||
err = json.Unmarshal(req, &acReq)
|
err = json.Unmarshal(req, &acReq)
|
||||||
|
|
@ -956,6 +984,27 @@ func (cac StorageAuthorityClient) GetAuthorization(id string) (authz core.Author
|
||||||
return
|
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
|
// GetCertificate sends a request to get a Certificate by ID
|
||||||
func (cac StorageAuthorityClient) GetCertificate(id string) (cert core.Certificate, err error) {
|
func (cac StorageAuthorityClient) GetCertificate(id string) (cert core.Certificate, err error) {
|
||||||
jsonCert, err := cac.rpc.DispatchSync(MethodGetCertificate, []byte(id))
|
jsonCert, err := cac.rpc.DispatchSync(MethodGetCertificate, []byte(id))
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,20 @@ func (ssa *SQLStorageAuthority) GetAuthorization(id string) (authz core.Authoriz
|
||||||
return
|
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
|
// GetCertificateByShortSerial takes an id consisting of the first, sequential half of a
|
||||||
// serial number and returns the first certificate whose full serial number is
|
// 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
|
// lexically greater than that id. This allows clients to query on the known
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,8 @@ func TestAddAuthorization(t *testing.T) {
|
||||||
combos[0] = []int{0, 1}
|
combos[0] = []int{0, 1}
|
||||||
|
|
||||||
exp := time.Now().AddDate(0, 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)
|
err = sa.UpdatePendingAuthorization(newPa)
|
||||||
test.AssertNotError(t, err, "Couldn't update pending authorization with ID "+PA.ID)
|
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)
|
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) {
|
func TestAddCertificate(t *testing.T) {
|
||||||
sa := initSA(t)
|
sa := initSA(t)
|
||||||
|
|
||||||
|
|
|
||||||
10
start.sh
10
start.sh
|
|
@ -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
|
|
||||||
|
|
@ -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/"
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
|
|
@ -24,6 +25,12 @@ import (
|
||||||
"github.com/letsencrypt/boulder/policy"
|
"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
|
// ValidationAuthorityImpl represents a VA
|
||||||
type ValidationAuthorityImpl struct {
|
type ValidationAuthorityImpl struct {
|
||||||
RA core.RegistrationAuthority
|
RA core.RegistrationAuthority
|
||||||
|
|
@ -437,36 +444,50 @@ func newCAASet(CAAs []*dns.CAA) *CAASet {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (va *ValidationAuthorityImpl) getCAASet(hostname string) (*CAASet, error) {
|
func (va *ValidationAuthorityImpl) getCAASet(hostname string) (*CAASet, error) {
|
||||||
hostname = strings.TrimRight(hostname, ".")
|
label := strings.TrimRight(hostname, ".")
|
||||||
splitDomain := strings.Split(hostname, ".")
|
cnames := 0
|
||||||
// RFC 6844 CAA set query sequence, 'x.y.z.com' => ['x.y.z.com', 'y.z.com', 'z.com']
|
// See RFC 6844 "Certification Authority Processing" for pseudocode.
|
||||||
for i := range splitDomain {
|
for {
|
||||||
queryDomain := strings.Join(splitDomain[i:], ".")
|
if strings.IndexRune(label, '.') == -1 {
|
||||||
// Don't query a public suffix
|
// Reached TLD
|
||||||
if _, present := policy.PublicSuffixList[queryDomain]; present {
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if _, present := policy.PublicSuffixList[label]; present {
|
||||||
// Query CAA records for domain and its alias if it has a CNAME
|
break
|
||||||
for _, alias := range []bool{false, true} {
|
}
|
||||||
if alias {
|
CAAs, _, err := va.DNSResolver.LookupCAA(label)
|
||||||
target, _, err := va.DNSResolver.LookupCNAME(queryDomain)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
queryDomain = target
|
|
||||||
}
|
|
||||||
CAAs, _, err := va.DNSResolver.LookupCAA(queryDomain)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(CAAs) > 0 {
|
if len(CAAs) > 0 {
|
||||||
return newCAASet(CAAs), nil
|
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
|
// no CAA records found
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -564,10 +564,23 @@ func TestCAAChecking(t *testing.T) {
|
||||||
CAATest{"reserved.com", true, false},
|
CAATest{"reserved.com", true, false},
|
||||||
// Critical
|
// Critical
|
||||||
CAATest{"critical.com", true, false},
|
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)
|
// Good (absent)
|
||||||
CAATest{"absent.com", false, true},
|
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)
|
// Good (present)
|
||||||
CAATest{"present.com", true, true},
|
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)
|
va := NewValidationAuthorityImpl(true)
|
||||||
|
|
@ -585,6 +598,20 @@ func TestCAAChecking(t *testing.T) {
|
||||||
test.AssertError(t, err, "servfail.com")
|
test.AssertError(t, err, "servfail.com")
|
||||||
test.Assert(t, !present, "Present should be false")
|
test.Assert(t, !present, "Present should be false")
|
||||||
test.Assert(t, !valid, "Valid 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) {
|
func TestDNSValidationFailure(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -703,7 +702,6 @@ func (wfe *WebFrontEndImpl) NewCertificate(response http.ResponseWriter, request
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
wfe.logCsr(request.RemoteAddr, init, reg)
|
wfe.logCsr(request.RemoteAddr, init, reg)
|
||||||
logEvent.Extra["Authorizations"] = init.Authorizations
|
|
||||||
logEvent.Extra["CSRDNSNames"] = init.CSR.DNSNames
|
logEvent.Extra["CSRDNSNames"] = init.CSR.DNSNames
|
||||||
logEvent.Extra["CSREmailAddresses"] = init.CSR.EmailAddresses
|
logEvent.Extra["CSREmailAddresses"] = init.CSR.EmailAddresses
|
||||||
logEvent.Extra["CSRIPAddresses"] = init.CSR.IPAddresses
|
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.Header().Set("Content-Type", "text/plain")
|
||||||
response.WriteHeader(http.StatusOK)
|
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 {
|
if _, err := fmt.Fprintln(response, detailsString); err != nil {
|
||||||
logEvent.Error = err.Error()
|
logEvent.Error = err.Error()
|
||||||
wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err))
|
wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err))
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ wk6Oiadty3eQqSBJv0HnpmiEdQVffIK5Pg4M8Dd+aOBnEkbopAJOuA==
|
||||||
)
|
)
|
||||||
|
|
||||||
type MockSA struct {
|
type MockSA struct {
|
||||||
// empty
|
authorizedDomains map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sa *MockSA) GetRegistration(id int64) (core.Registration, error) {
|
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
|
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) {
|
func (sa *MockSA) GetCertificate(serial string) (core.Certificate, error) {
|
||||||
// Serial ee == 238.crt
|
// Serial ee == 238.crt
|
||||||
if serial == "000000000000000000000000000000ee" {
|
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) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue