Merge branch 'master' into mailer

This commit is contained in:
Roland Shoemaker 2015-07-23 15:33:43 -07:00
commit 6c2f3ea8cc
26 changed files with 757 additions and 281 deletions

View File

@ -1,7 +1,13 @@
# This Makefile also tricks Travis into not running 'go get' for our
# build. See http://docs.travis-ci.com/user/languages/go/
OBJDIR = ./bin
OBJDIR ?= ./bin
DESTDIR ?= /usr/local/bin
ARCHIVEDIR ?= /tmp
VERSION ?= 1.0.0
EPOCH ?= 1
MAINTAINER ?= "Community"
OBJECTS = activity-monitor \
admin-revoker \
@ -15,7 +21,9 @@ OBJECTS = activity-monitor \
ocsp-responder
# Build environment variables (referencing core/util.go)
BUILD_ID = $(shell git symbolic-ref --short HEAD 2>/dev/null) +$(shell git rev-parse --short HEAD)
COMMIT_ID = $(shell git rev-parse --short HEAD)
BUILD_ID = $(shell git symbolic-ref --short HEAD 2>/dev/null) +$(COMMIT_ID)
BUILD_ID_VAR = github.com/letsencrypt/boulder/core.BuildID
BUILD_HOST = $(shell whoami)@$(shell hostname)
@ -45,3 +53,33 @@ $(OBJECTS): pre
clean:
rm -f $(OBJDIR)/*
rmdir $(OBJDIR)
# Install to a destination directory. Defaults to /usr/local/, but you can
# override it with the DESTDIR variable. Example:
#
# DESTDIR=~/bin make install
install:
@mkdir -p $(DESTDIR)
$(foreach var,$(OBJECTS), install -m 0755 $(OBJDIR)/$(var) $(DESTDIR)/;)
# Produce a tarball of the current commit; you can set the destination in the
# ARCHIVEDIR variable.
archive:
git archive --output=$(ARCHIVEDIR)/boulder-$(COMMIT_ID).tar.gz \
--prefix=boulder-$(COMMIT_ID)/ $(COMMIT_ID)
# Building an RPM requires `fpm` from https://github.com/jordansissel/fpm
# which you can install with `gem install fpm`.
# It is recommended that maintainers use environment overrides to specify
# Version and Epoch, such as:
#
# VERSION=0.1.9 EPOCH=52 MAINTAINER="$(whoami)" ARCHIVEDIR=/tmp make build rpm
rpm:
fpm -s dir -t rpm --rpm-digest sha256 --name "boulder" \
--license "Mozilla Public License v2.0" --vendor "ISRG" \
--url "https://github.com/letsencrypt/boulder" --prefix=/opt/boulder \
--version $(VERSION) --iteration $(COMMIT_ID) --epoch $(EPOCH) \
--package $(ARCHIVEDIR)/boulder-$(VERSION)-$(COMMIT_ID).x86_64.rpm \
--description "Boulder is an ACME-compatible X.509 Certificate Authority" \
--depends "libtool-ltdl" --maintainer "$(MAINTAINER)" \
test/boulder-config.json $(foreach var,$(OBJECTS), $(OBJDIR)/$(var))

View File

@ -332,6 +332,8 @@ var BadAlgorithmCSRhex = "308202aa30820192020100300d310b300906035504061302" +
var FarFuture = time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC)
var FarPast = time.Date(1950, 1, 1, 0, 0, 0, 0, time.UTC)
var log = mocks.UseMockLog()
// CFSSL config
const profileName = "ee"
const caKeyFile = "../test/test-ca.key"

View File

@ -6,8 +6,11 @@
package main
import (
"time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/streadway/amqp"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/cmd"
blog "github.com/letsencrypt/boulder/log"
@ -36,6 +39,9 @@ func main() {
rai := ra.NewRegistrationAuthorityImpl()
rai.AuthzBase = c.Common.BaseURL + wfe.AuthzPath
rai.MaxKeySize = c.Common.MaxKeySize
raDNSTimeout, err := time.ParseDuration(c.RA.DNSTimeout)
cmd.FailOnError(err, "Couldn't parse RA DNS timeout")
rai.DNSResolver = core.NewDNSResolverImpl(raDNSTimeout, []string{c.RA.DNSResolver})
go cmd.ProfileCmd("RA", stats)

View File

@ -97,6 +97,15 @@ func main() {
wfe.Stats = stats
wfe.SubscriberAgreementURL = c.SubscriberAgreementURL
wfe.CertCacheDuration, err = time.ParseDuration(c.WFE.CertCacheDuration)
cmd.FailOnError(err, "Couldn't parse certificate caching duration")
wfe.CertNoCacheExpirationWindow, err = time.ParseDuration(c.WFE.CertNoCacheExpirationWindow)
cmd.FailOnError(err, "Couldn't parse certificate expiration no-cache window")
wfe.IndexCacheDuration, err = time.ParseDuration(c.WFE.IndexCacheDuration)
cmd.FailOnError(err, "Couldn't parse index caching duration")
wfe.IssuerCacheDuration, err = time.ParseDuration(c.WFE.IssuerCacheDuration)
cmd.FailOnError(err, "Couldn't parse issuer caching duration")
wfe.IssuerCert, err = cmd.LoadCert(c.Common.IssuerCert)
cmd.FailOnError(err, fmt.Sprintf("Couldn't read issuer cert [%s]", c.Common.IssuerCert))
@ -120,7 +129,8 @@ func main() {
// Set up paths
wfe.BaseURL = c.Common.BaseURL
h := wfe.Handler()
h, err := wfe.Handler()
cmd.FailOnError(err, "Problem setting up HTTP handlers")
auditlogger.Info(app.VersionString())

View File

@ -82,12 +82,24 @@ func main() {
cmd.FailOnError(err, "Unable to create SA")
sa.SetSQLDebug(c.SQL.SQLDebug)
wfei.CertCacheDuration, err = time.ParseDuration(c.WFE.CertCacheDuration)
cmd.FailOnError(err, "Couldn't parse certificate caching duration")
wfei.CertNoCacheExpirationWindow, err = time.ParseDuration(c.WFE.CertNoCacheExpirationWindow)
cmd.FailOnError(err, "Couldn't parse certificate expiration no-cache window")
wfei.IndexCacheDuration, err = time.ParseDuration(c.WFE.IndexCacheDuration)
cmd.FailOnError(err, "Couldn't parse index caching duration")
wfei.IssuerCacheDuration, err = time.ParseDuration(c.WFE.IssuerCacheDuration)
cmd.FailOnError(err, "Couldn't parse issuer caching duration")
ra := ra.NewRegistrationAuthorityImpl()
raDNSTimeout, err := time.ParseDuration(c.RA.DNSTimeout)
cmd.FailOnError(err, "Couldn't parse RA DNS timeout")
ra.DNSResolver = core.NewDNSResolverImpl(raDNSTimeout, []string{c.RA.DNSResolver})
va := va.NewValidationAuthorityImpl(c.CA.TestMode)
dnsTimeout, err := time.ParseDuration(c.VA.DNSTimeout)
cmd.FailOnError(err, "Couldn't parse DNS timeout")
va.DNSResolver = core.NewDNSResolverImpl(dnsTimeout, []string{c.VA.DNSResolver})
vaDNSTimeout, err := time.ParseDuration(c.VA.DNSTimeout)
cmd.FailOnError(err, "Couldn't parse VA DNS timeout")
va.DNSResolver = core.NewDNSResolverImpl(vaDNSTimeout, []string{c.VA.DNSResolver})
va.UserAgent = c.VA.UserAgent
cadb, err := ca.NewCertificateAuthorityDatabaseImpl(c.CA.DBDriver, c.CA.DBConnect)
@ -122,7 +134,8 @@ func main() {
// Set up paths
ra.AuthzBase = c.Common.BaseURL + wfe.AuthzPath
wfei.BaseURL = c.Common.BaseURL
h := wfei.Handler()
h, err := wfei.Handler()
cmd.FailOnError(err, "Problem setting up HTTP handlers")
ra.MaxKeySize = c.Common.MaxKeySize
ca.MaxKeySize = c.Common.MaxKeySize

View File

@ -9,7 +9,6 @@ import (
"crypto/x509"
"database/sql"
"fmt"
"math"
"time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
@ -24,8 +23,6 @@ import (
"github.com/letsencrypt/boulder/sa"
)
const ocspResponseLimit int = 128
// FatalError indicates the updater should stop execution
type FatalError string
@ -191,7 +188,7 @@ func main() {
app.App.Flags = append(app.App.Flags, cli.IntFlag{
Name: "limit",
Value: ocspResponseLimit,
Value: 100,
EnvVar: "OCSP_LIMIT",
Usage: "Count of responses to process per run",
})
@ -251,12 +248,10 @@ func main() {
oldestLastUpdatedTime := time.Now().Add(-dur)
auditlogger.Info(fmt.Sprintf("Searching for OCSP responses older than %s", oldestLastUpdatedTime))
count := int(math.Min(float64(ocspResponseLimit), float64(c.OCSPUpdater.ResponseLimit)))
// When we choose to batch responses, it may be best to restrict count here,
// change the transaction to survive the whole findStaleResponses, and to
// loop this method call however many times is appropriate.
err = updater.findStaleResponses(oldestLastUpdatedTime, count)
err = updater.findStaleResponses(oldestLastUpdatedTime, c.OCSPUpdater.ResponseLimit)
if err != nil {
auditlogger.WarningErr(err)
}

View File

@ -73,6 +73,11 @@ type Config struct {
BaseURL string
ListenAddress string
CertCacheDuration string
CertNoCacheExpirationWindow string
IndexCacheDuration string
IssuerCacheDuration string
// DebugAddr is the address to run the /debug handlers on.
DebugAddr string
}
@ -85,6 +90,9 @@ type Config struct {
}
RA struct {
DNSResolver string
DNSTimeout string
// DebugAddr is the address to run the /debug handlers on.
DebugAddr string
}

View File

@ -6,7 +6,6 @@
package core
import (
"errors"
"fmt"
"math/rand"
"net"
@ -15,15 +14,6 @@ import (
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/miekg/dns"
)
// DNSSECError indicates an error caused by DNSSEC failing.
type DNSSECError struct {
}
// Error gives the DNSSEC failure notice.
func (err DNSSECError) Error() string {
return "DNSSEC validation failure"
}
// DNSResolverImpl represents a resolver system
type DNSResolverImpl struct {
DNSClient *dns.Client
@ -42,8 +32,16 @@ func NewDNSResolverImpl(dialTimeout time.Duration, servers []string) *DNSResolve
}
// ExchangeOne performs a single DNS exchange with a randomly chosen server
// out of the server list, returning the response, time, and error (if any)
func (dnsResolver *DNSResolverImpl) ExchangeOne(m *dns.Msg) (rsp *dns.Msg, rtt time.Duration, err error) {
// out of the server list, returning the response, time, and error (if any).
// This method sets the DNSSEC OK bit on the message to true before sending
// it to the resolver in case validation isn't the resolvers default behaviour.
func (dnsResolver *DNSResolverImpl) ExchangeOne(hostname string, qtype uint16) (rsp *dns.Msg, rtt time.Duration, err error) {
m := new(dns.Msg)
// Set question type
m.SetQuestion(dns.Fqdn(hostname), qtype)
// Set DNSSEC OK bit for resolver
m.SetEdns0(4096, true)
if len(dnsResolver.Servers) < 1 {
err = fmt.Errorf("Not configured with at least one DNS Server")
return
@ -55,60 +53,25 @@ func (dnsResolver *DNSResolverImpl) ExchangeOne(m *dns.Msg) (rsp *dns.Msg, rtt t
return dnsResolver.DNSClient.Exchange(m, chosenServer)
}
// LookupDNSSEC sends the provided DNS message to a randomly chosen server (see
// ExchangeOne) with DNSSEC enabled. If the lookup fails, this method sends a
// clarification query to determine if it's because DNSSEC was invalid or just
// a run-of-the-mill error. If it's because of DNSSEC, it returns ErrorDNSSEC.
func (dnsResolver *DNSResolverImpl) LookupDNSSEC(m *dns.Msg) (*dns.Msg, time.Duration, error) {
// Set DNSSEC OK bit
m.SetEdns0(4096, true)
r, rtt, err := dnsResolver.ExchangeOne(m)
if err != nil {
return r, rtt, err
}
if r.Rcode != dns.RcodeSuccess && r.Rcode != dns.RcodeNameError && r.Rcode != dns.RcodeNXRrset {
if r.Rcode == dns.RcodeServerFailure {
// Re-send query with +cd to see if SERVFAIL was caused by DNSSEC
// validation failure at the resolver
m.CheckingDisabled = true
checkR, _, err := dnsResolver.ExchangeOne(m)
if err != nil {
return r, rtt, err
}
if checkR.Rcode != dns.RcodeServerFailure {
// DNSSEC error, so we return the testable object.
err = DNSSECError{}
return r, rtt, err
}
}
err = fmt.Errorf("Invalid response code: %d-%s", r.Rcode, dns.RcodeToString[r.Rcode])
return r, rtt, err
}
return r, rtt, err
}
// LookupTXT uses a DNSSEC-enabled query to find all TXT records associated with
// the provided hostname. If the query fails due to DNSSEC, error will be
// set to ErrorDNSSEC.
// LookupTXT sends a DNS query to find all TXT records associated with
// the provided hostname.
func (dnsResolver *DNSResolverImpl) LookupTXT(hostname string) ([]string, time.Duration, error) {
var txt []string
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(hostname), dns.TypeTXT)
r, rtt, err := dnsResolver.LookupDNSSEC(m)
r, rtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeTXT)
if err != nil {
return nil, 0, err
}
if r.Rcode != dns.RcodeSuccess {
err = fmt.Errorf("DNS failure: %d-%s for TXT query", r.Rcode, dns.RcodeToString[r.Rcode])
return nil, 0, err
}
for _, answer := range r.Answer {
if answer.Header().Rrtype == dns.TypeTXT {
txtRec := answer.(*dns.TXT)
for _, field := range txtRec.Txt {
txt = append(txt, field)
if txtRec, ok := answer.(*dns.TXT); ok {
for _, field := range txtRec.Txt {
txt = append(txt, field)
}
}
}
}
@ -116,97 +79,114 @@ func (dnsResolver *DNSResolverImpl) LookupTXT(hostname string) ([]string, time.D
return txt, rtt, err
}
// LookupHost uses a DNSSEC-enabled query to find all A/AAAA records associated with
// the provided hostname. If the query fails due to DNSSEC, error will be
// set to ErrorDNSSEC.
func (dnsResolver *DNSResolverImpl) LookupHost(hostname string) ([]net.IP, time.Duration, error) {
// LookupHost sends a DNS query to find all A/AAAA records associated with
// the provided hostname.
func (dnsResolver *DNSResolverImpl) LookupHost(hostname string) ([]net.IP, time.Duration, time.Duration, error) {
var addrs []net.IP
var answers []dns.RR
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(hostname), dns.TypeA)
r, aRtt, err := dnsResolver.LookupDNSSEC(m)
r, aRtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeA)
if err != nil {
return addrs, aRtt, err
return addrs, 0, 0, err
}
if r.Rcode != dns.RcodeSuccess {
err = fmt.Errorf("DNS failure: %d-%s for A query", r.Rcode, dns.RcodeToString[r.Rcode])
return nil, aRtt, 0, err
}
answers = append(answers, r.Answer...)
m.SetQuestion(dns.Fqdn(hostname), dns.TypeAAAA)
r, aaaaRtt, err := dnsResolver.LookupDNSSEC(m)
r, aaaaRtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeAAAA)
if err != nil {
return addrs, aRtt + aaaaRtt, err
return addrs, aRtt, 0, err
}
if r.Rcode != dns.RcodeSuccess {
err = fmt.Errorf("DNS failure: %d-%s for AAAA query", r.Rcode, dns.RcodeToString[r.Rcode])
return nil, aRtt, aaaaRtt, err
}
answers = append(answers, r.Answer...)
for _, answer := range answers {
if answer.Header().Rrtype == dns.TypeA {
a := answer.(*dns.A)
addrs = append(addrs, a.A)
if a, ok := answer.(*dns.A); ok {
addrs = append(addrs, a.A)
}
} else if answer.Header().Rrtype == dns.TypeAAAA {
aaaa := answer.(*dns.AAAA)
addrs = append(addrs, aaaa.AAAA)
if aaaa, ok := answer.(*dns.AAAA); ok {
addrs = append(addrs, aaaa.AAAA)
}
}
}
return addrs, aRtt + aaaaRtt, nil
return addrs, aRtt, aaaaRtt, nil
}
// LookupCNAME uses a DNSSEC-enabled query to records for domain and returns either
// the target, "", or a if the query fails due to DNSSEC, error will be set to
// ErrorDNSSEC.
func (dnsResolver *DNSResolverImpl) LookupCNAME(domain string) (string, error) {
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(domain), dns.TypeCNAME)
r, _, err := dnsResolver.LookupDNSSEC(m)
// LookupCNAME sends a DNS query to find a CNAME record associated hostname and returns the
// record target.
func (dnsResolver *DNSResolverImpl) LookupCNAME(hostname string) (string, time.Duration, error) {
r, rtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeCNAME)
if err != nil {
return "", err
return "", 0, err
}
if r.Rcode != dns.RcodeSuccess {
err = fmt.Errorf("DNS failure: %d-%s for CNAME query", r.Rcode, dns.RcodeToString[r.Rcode])
return "", rtt, err
}
for _, answer := range r.Answer {
if cname, ok := answer.(*dns.CNAME); ok {
return cname.Target, nil
return cname.Target, rtt, nil
}
}
return "", nil
return "", rtt, nil
}
// LookupCAA uses a DNSSEC-enabled query to find all CAA records associated with
// the provided hostname. If the query fails due to DNSSEC, error will be
// set to ErrorDNSSEC.
func (dnsResolver *DNSResolverImpl) LookupCAA(domain string, alias bool) ([]*dns.CAA, error) {
if alias {
// Check if there is a CNAME record for domain
canonName, err := dnsResolver.LookupCNAME(domain)
if err != nil {
return nil, err
}
if canonName == "" || canonName == domain {
return []*dns.CAA{}, nil
}
domain = canonName
}
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(domain), dns.TypeCAA)
r, _, err := dnsResolver.LookupDNSSEC(m)
// 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.
func (dnsResolver *DNSResolverImpl) LookupCAA(hostname string) ([]*dns.CAA, time.Duration, error) {
r, rtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeCAA)
if err != nil {
return nil, err
return nil, 0, err
}
// On resolver validation failure, or other server failures, return empty an
// set and no error.
var CAAs []*dns.CAA
if r.Rcode == dns.RcodeServerFailure {
return CAAs, rtt, nil
}
for _, answer := range r.Answer {
if answer.Header().Rrtype == dns.TypeCAA {
caaR, ok := answer.(*dns.CAA)
if !ok {
err = errors.New("Badly formatted record")
return nil, err
if caaR, ok := answer.(*dns.CAA); ok {
CAAs = append(CAAs, caaR)
}
CAAs = append(CAAs, caaR)
}
}
return CAAs, rtt, nil
}
// LookupMX sends a DNS query to find a MX record associated hostname and returns the
// record target.
func (dnsResolver *DNSResolverImpl) LookupMX(hostname string) ([]string, time.Duration, error) {
r, rtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeMX)
if err != nil {
return nil, 0, err
}
if r.Rcode != dns.RcodeSuccess {
err = fmt.Errorf("DNS failure: %d-%s for MX query", r.Rcode, dns.RcodeToString[r.Rcode])
return nil, rtt, err
}
var results []string
for _, answer := range r.Answer {
if mx, ok := answer.(*dns.MX); ok {
results = append(results, mx.Mx)
}
}
return CAAs, nil
return results, rtt, nil
}

View File

@ -26,6 +26,11 @@ func mockDNSQuery(w dns.ResponseWriter, r *dns.Msg) {
m.Compress = false
for _, q := range r.Question {
if q.Name == "servfail.com." {
m.Rcode = dns.RcodeServerFailure
w.WriteMsg(m)
return
}
switch q.Qtype {
case dns.TypeSOA:
record := new(dns.SOA)
@ -42,8 +47,7 @@ func mockDNSQuery(w dns.ResponseWriter, r *dns.Msg) {
w.WriteMsg(m)
return
case dns.TypeA:
switch q.Name {
case "cps.letsencrypt.org.":
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")
@ -51,12 +55,6 @@ func mockDNSQuery(w dns.ResponseWriter, r *dns.Msg) {
m.Answer = append(m.Answer, record)
w.WriteMsg(m)
return
case "sigfail.verteiltesysteme.net.":
if !r.CheckingDisabled {
m.Rcode = dns.RcodeServerFailure
}
w.WriteMsg(m)
return
}
case dns.TypeCAA:
if q.Name == "bracewel.net." {
@ -111,8 +109,7 @@ func TestMain(m *testing.M) {
func TestDNSNoServers(t *testing.T) {
obj := NewDNSResolverImpl(time.Hour, []string{})
m := new(dns.Msg)
_, _, err := obj.ExchangeOne(m)
_, _, err := obj.ExchangeOne("letsencrypt.org", dns.TypeA)
test.AssertError(t, err, "No servers")
}
@ -120,9 +117,7 @@ func TestDNSNoServers(t *testing.T) {
func TestDNSOneServer(t *testing.T) {
obj := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr})
m := new(dns.Msg)
m.SetQuestion("letsencrypt.org.", dns.TypeSOA)
_, _, err := obj.ExchangeOne(m)
_, _, err := obj.ExchangeOne("letsencrypt.org", dns.TypeSOA)
test.AssertNotError(t, err, "No message")
}
@ -130,9 +125,7 @@ func TestDNSOneServer(t *testing.T) {
func TestDNSDuplicateServers(t *testing.T) {
obj := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr, dnsLoopbackAddr})
m := new(dns.Msg)
m.SetQuestion("letsencrypt.org.", dns.TypeSOA)
_, _, err := obj.ExchangeOne(m)
_, _, err := obj.ExchangeOne("letsencrypt.org", dns.TypeSOA)
test.AssertNotError(t, err, "No message")
}
@ -143,39 +136,34 @@ func TestDNSLookupsNoServer(t *testing.T) {
_, _, err := obj.LookupTXT("letsencrypt.org")
test.AssertError(t, err, "No servers")
_, _, err = obj.LookupHost("letsencrypt.org")
_, _, _, err = obj.LookupHost("letsencrypt.org")
test.AssertError(t, err, "No servers")
_, err = obj.LookupCNAME("letsencrypt.org")
_, _, err = obj.LookupCNAME("letsencrypt.org")
test.AssertError(t, err, "No servers")
_, err = obj.LookupCAA("letsencrypt.org", false)
_, _, err = obj.LookupCAA("letsencrypt.org")
test.AssertError(t, err, "No servers")
}
func TestDNSLookupDNSSEC(t *testing.T) {
goodServer := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr})
func TestDNSServFail(t *testing.T) {
obj := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr})
bad := "servfail.com"
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn("sigfail.verteiltesysteme.net"), dns.TypeA)
_, _, err := obj.LookupTXT(bad)
test.AssertError(t, err, "LookupTXT didn't return an error")
_, _, err := goodServer.LookupDNSSEC(m)
test.AssertError(t, err, "DNSSEC failure")
_, ok := err.(DNSSECError)
fmt.Println(err)
test.Assert(t, ok, "Should have been a DNSSECError")
_, _, err = obj.LookupCNAME(bad)
test.AssertError(t, err, "LookupCNAME didn't return an error")
m.SetQuestion(dns.Fqdn("sigok.verteiltesysteme.net"), dns.TypeA)
_, _, _, err = obj.LookupHost(bad)
test.AssertError(t, err, "LookupCNAME didn't return an error")
_, _, err = goodServer.LookupDNSSEC(m)
test.AssertNotError(t, err, "DNSSEC should have worked")
badServer := NewDNSResolverImpl(time.Second*10, []string{"127.0.0.1:99"})
_, _, err = badServer.LookupDNSSEC(m)
test.AssertError(t, err, "Should have failed")
_, ok = err.(DNSSECError)
test.Assert(t, !ok, "Shouldn't have been a DNSSECError")
// CAA lookup ignores validation failures from the resolver for now
// and returns an empty list of CAA records.
emptyCaa, _, err := obj.LookupCAA(bad)
test.Assert(t, len(emptyCaa) == 0, "Query returned non-empty list of CAA records")
test.AssertNotError(t, err, "LookupCAA returned an error")
}
func TestDNSLookupTXT(t *testing.T) {
@ -190,30 +178,30 @@ func TestDNSLookupTXT(t *testing.T) {
func TestDNSLookupHost(t *testing.T) {
obj := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr})
ip, _, err := obj.LookupHost("sigfail.verteiltesysteme.net")
t.Logf("sigfail.verteiltesysteme.net - IP: %s, Err: %s", ip, err)
test.AssertError(t, err, "DNSSEC failure")
ip, _, _, err := obj.LookupHost("servfail.com")
t.Logf("servfail.com - IP: %s, Err: %s", ip, err)
test.AssertError(t, err, "Server failure")
test.Assert(t, len(ip) == 0, "Should not have IPs")
ip, _, err = obj.LookupHost("nonexistent.letsencrypt.org")
ip, _, _, err = obj.LookupHost("nonexistent.letsencrypt.org")
t.Logf("nonexistent.letsencrypt.org - IP: %s, Err: %s", ip, err)
test.AssertNotError(t, err, "Not an error to not exist")
test.Assert(t, len(ip) == 0, "Should not have IPs")
ip, _, err = obj.LookupHost("cps.letsencrypt.org")
ip, _, _, err = obj.LookupHost("cps.letsencrypt.org")
t.Logf("cps.letsencrypt.org - IP: %s, Err: %s", ip, err)
test.AssertNotError(t, err, "Not an error to be a CNAME")
test.AssertNotError(t, err, "Not an error to exist")
test.Assert(t, len(ip) > 0, "Should have IPs")
}
func TestDNSLookupCAA(t *testing.T) {
obj := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr})
caas, err := obj.LookupCAA("bracewel.net", false)
caas, _, err := obj.LookupCAA("bracewel.net")
test.AssertNotError(t, err, "CAA lookup failed")
test.Assert(t, len(caas) > 0, "Should have CAA records")
caas, err = obj.LookupCAA("nonexistent.letsencrypt.org", false)
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")
}

View File

@ -140,10 +140,10 @@ type CertificateAuthorityDatabase interface {
// DNSResolver defines methods used for DNS resolution
type DNSResolver interface {
ExchangeOne(*dns.Msg) (*dns.Msg, time.Duration, error)
LookupDNSSEC(*dns.Msg) (*dns.Msg, time.Duration, error)
ExchangeOne(string, uint16) (*dns.Msg, time.Duration, error)
LookupTXT(string) ([]string, time.Duration, error)
LookupHost(string) ([]net.IP, time.Duration, error)
LookupCNAME(string) (string, error)
LookupCAA(string, bool) ([]*dns.CAA, error)
LookupHost(string) ([]net.IP, time.Duration, time.Duration, error)
LookupCNAME(string) (string, time.Duration, error)
LookupCAA(string) ([]*dns.CAA, time.Duration, error)
LookupMX(string) ([]string, time.Duration, error)
}

View File

@ -66,7 +66,6 @@ const (
// Error types that can be used in ACME payloads
const (
ConnectionProblem = ProblemType("urn:acme:error:connection")
DNSSECProblem = ProblemType("urn:acme:error:dnssec")
MalformedProblem = ProblemType("urn:acme:error:malformed")
ServerInternalProblem = ProblemType("urn:acme:error:serverInternal")
TLSProblem = ProblemType("urn:acme:error:tls")

View File

@ -19,6 +19,21 @@ import (
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
)
// A SyslogWriter logs messages with explicit priority levels. It is
// implemented by a logging back-end like syslog.Writer or
// mocks.SyslogWriter.
type SyslogWriter interface {
Close() error
Alert(m string) error
Crit(m string) error
Debug(m string) error
Emerg(m string) error
Err(m string) error
Info(m string) error
Notice(m string) error
Warning(m string) error
}
// singleton defines the object of a Singleton pattern
type singleton struct {
once sync.Once
@ -42,13 +57,11 @@ func defaultEmergencyExit() {
os.Exit(emergencyReturnValue)
}
// AuditLogger is a System Logger with additional audit-specific methods.
// In addition to all the standard syslog.Writer methods from
// http://golang.org/pkg/log/syslog/#Writer, you can also call
// auditLogger.Audit(msg string)
// to send a message as an audit event.
// AuditLogger implements SyslogWriter, and has additional
// audit-specific methods, like Audit(), for indicating which messages
// should be classified as audit events.
type AuditLogger struct {
*syslog.Writer
SyslogWriter
Stats statsd.Statter
exitFunction exitFunction
}
@ -64,9 +77,9 @@ func Dial(network, raddr string, tag string, stats statsd.Statter) (*AuditLogger
return NewAuditLogger(syslogger, stats)
}
// NewAuditLogger constructs an Audit Logger that decorates a normal
// System Logger. All methods in log/syslog continue to work.
func NewAuditLogger(log *syslog.Writer, stats statsd.Statter) (*AuditLogger, error) {
// NewAuditLogger returns a new AuditLogger that uses the given
// SyslogWriter as a backend.
func NewAuditLogger(log SyslogWriter, stats statsd.Statter) (*AuditLogger, error) {
if log == nil {
return nil, errors.New("Attempted to use a nil System Logger.")
}
@ -125,21 +138,21 @@ func (log *AuditLogger) logAtLevel(level, msg string) (err error) {
switch level {
case "Logging.Alert":
err = log.Writer.Alert(msg)
err = log.SyslogWriter.Alert(msg)
case "Logging.Crit":
err = log.Writer.Crit(msg)
err = log.SyslogWriter.Crit(msg)
case "Logging.Debug":
err = log.Writer.Debug(msg)
err = log.SyslogWriter.Debug(msg)
case "Logging.Emerg":
err = log.Writer.Emerg(msg)
err = log.SyslogWriter.Emerg(msg)
case "Logging.Err":
err = log.Writer.Err(msg)
err = log.SyslogWriter.Err(msg)
case "Logging.Info":
err = log.Writer.Info(msg)
err = log.SyslogWriter.Info(msg)
case "Logging.Warning":
err = log.Writer.Warning(msg)
err = log.SyslogWriter.Warning(msg)
case "Logging.Notice":
err = log.Writer.Notice(msg)
err = log.SyslogWriter.Notice(msg)
default:
err = fmt.Errorf("Unknown logging level: %s", level)
}

172
mocks/log.go Normal file
View File

@ -0,0 +1,172 @@
// Copyright 2015 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package mocks
import (
"log"
"log/syslog"
"regexp"
blog "github.com/letsencrypt/boulder/log"
)
// MockSyslogWriter implements the blog.SyslogWriter interface. It
// stores all logged messages in a buffer for inspection by test
// functions (via GetAll()) instead of sending them to syslog.
type MockSyslogWriter struct {
logged []*LogMessage
msgChan chan<- *LogMessage
getChan <-chan []*LogMessage
clearChan chan<- struct{}
closeChan chan<- struct{}
}
// LogMessage is a log entry that has been sent to a MockSyslogWriter.
type LogMessage struct {
Priority syslog.Priority // aka Log level
Message string // content of log message
}
var levelName = map[syslog.Priority]string{
syslog.LOG_EMERG: "EMERG",
syslog.LOG_ALERT: "ALERT",
syslog.LOG_CRIT: "CRIT",
syslog.LOG_ERR: "ERR",
syslog.LOG_WARNING: "WARNING",
syslog.LOG_NOTICE: "NOTICE",
syslog.LOG_INFO: "INFO",
syslog.LOG_DEBUG: "DEBUG",
}
func (lm *LogMessage) String() string {
return levelName[lm.Priority&7] + ": " + lm.Message
}
// UseMockLog changes the SyslogWriter used by the current singleton
// audit logger to a new mock logger, and returns the mock. Example:
//
// var log = mocks.UseMockLog()
// func TestFoo(t *testing.T) {
// log.Clear()
// // ...
// Assert(t, len(log.GetAll()) > 0, "Should have logged something")
// }
func UseMockLog() *MockSyslogWriter {
sw := NewSyslogWriter()
blog.GetAuditLogger().SyslogWriter = sw
return sw
}
// NewSyslogWriter returns a new MockSyslogWriter.
func NewSyslogWriter() *MockSyslogWriter {
msgChan := make(chan *LogMessage)
getChan := make(chan []*LogMessage)
clearChan := make(chan struct{})
closeChan := make(chan struct{})
msw := &MockSyslogWriter{
logged: []*LogMessage{},
msgChan: msgChan,
getChan: getChan,
clearChan: clearChan,
closeChan: closeChan,
}
go func() {
for {
select {
case logMsg := <-msgChan:
log.Print("MockSyslog:" + logMsg.String())
msw.logged = append(msw.logged, logMsg)
case getChan <- msw.logged:
case <-clearChan:
msw.logged = []*LogMessage{}
case <-closeChan:
close(getChan)
return
}
}
}()
return msw
}
func (msw *MockSyslogWriter) write(m string, priority syslog.Priority) error {
msw.msgChan <- &LogMessage{Message: m, Priority: priority}
return nil
}
// GetAll returns all LogMessages logged (since the last call to
// Clear(), if applicable).
//
// The caller must not modify the returned slice or its elements.
func (msw *MockSyslogWriter) GetAll() []*LogMessage {
return <-msw.getChan
}
// GetAllMatching returns all LogMessages logged (since the last
// Clear()) whose text matches the given regexp. The regexp is
// accepted as a string and compiled on the fly, because convenience
// is more important than performance.
//
// The caller must not modify the elements of the returned slice.
func (msw *MockSyslogWriter) GetAllMatching(reString string) (matches []*LogMessage) {
re := regexp.MustCompile(reString)
for _, logMsg := range <-msw.getChan {
if re.MatchString(logMsg.Message) {
matches = append(matches, logMsg)
}
}
return
}
// Clear resets the log buffer.
func (msw *MockSyslogWriter) Clear() {
msw.clearChan <- struct{}{}
}
// Close releases resources. No other methods may be called after this.
func (msw *MockSyslogWriter) Close() error {
msw.closeChan <- struct{}{}
return nil
}
// Alert logs at LOG_ALERT
func (msw *MockSyslogWriter) Alert(m string) error {
return msw.write(m, syslog.LOG_ALERT)
}
// Crit logs at LOG_CRIT
func (msw *MockSyslogWriter) Crit(m string) error {
return msw.write(m, syslog.LOG_CRIT)
}
// Debug logs at LOG_DEBUG
func (msw *MockSyslogWriter) Debug(m string) error {
return msw.write(m, syslog.LOG_DEBUG)
}
// Emerg logs at LOG_EMERG
func (msw *MockSyslogWriter) Emerg(m string) error {
return msw.write(m, syslog.LOG_EMERG)
}
// Err logs at LOG_ERR
func (msw *MockSyslogWriter) Err(m string) error {
return msw.write(m, syslog.LOG_ERR)
}
// Info logs at LOG_INFO
func (msw *MockSyslogWriter) Info(m string) error {
return msw.write(m, syslog.LOG_INFO)
}
// Notice logs at LOG_NOTICE
func (msw *MockSyslogWriter) Notice(m string) error {
return msw.write(m, syslog.LOG_NOTICE)
}
// Warning logs at LOG_WARNING
func (msw *MockSyslogWriter) Warning(m string) error {
return msw.write(m, syslog.LOG_WARNING)
}

View File

@ -7,6 +7,7 @@ package mocks
import (
"database/sql"
"fmt"
"net"
"time"
@ -14,7 +15,6 @@ import (
_ "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/mattn/go-sqlite3"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/miekg/dns"
gorp "github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
"github.com/letsencrypt/boulder/core"
)
// MockCADatabase is a mock
@ -52,35 +52,30 @@ type MockDNS struct {
}
// ExchangeOne is a mock
func (mock *MockDNS) ExchangeOne(m *dns.Msg) (rsp *dns.Msg, rtt time.Duration, err error) {
return m, 0, nil
func (mock *MockDNS) ExchangeOne(hostname string, qt uint16) (rsp *dns.Msg, rtt time.Duration, err error) {
return nil, 0, nil
}
// LookupTXT is a mock
func (mock *MockDNS) LookupTXT(hostname string) ([]string, time.Duration, error) {
if hostname == "_acme-challenge.dnssec-failed.org" {
return nil, 0, core.DNSSECError{}
if hostname == "_acme-challenge.servfail.com" {
return nil, 0, fmt.Errorf("SERVFAIL")
}
return []string{"hostname"}, 0, nil
}
// LookupDNSSEC is a mock
func (mock *MockDNS) LookupDNSSEC(m *dns.Msg) (*dns.Msg, time.Duration, error) {
return m, 0, nil
}
// LookupHost is a mock
func (mock *MockDNS) LookupHost(hostname string) ([]net.IP, time.Duration, error) {
return nil, 0, nil
func (mock *MockDNS) LookupHost(hostname string) ([]net.IP, time.Duration, time.Duration, error) {
return nil, 0, 0, nil
}
// LookupCNAME is a mock
func (mock *MockDNS) LookupCNAME(domain string) (string, error) {
return "hostname", nil
func (mock *MockDNS) LookupCNAME(domain string) (string, time.Duration, error) {
return "hostname", 0, nil
}
// LookupCAA is a mock
func (mock *MockDNS) LookupCAA(domain string, alias bool) ([]*dns.CAA, error) {
func (mock *MockDNS) LookupCAA(domain string) ([]*dns.CAA, time.Duration, error) {
var results []*dns.CAA
var record dns.CAA
switch domain {
@ -97,8 +92,19 @@ func (mock *MockDNS) LookupCAA(domain string, alias bool) ([]*dns.CAA, error) {
record.Tag = "issue"
record.Value = "letsencrypt.org"
results = append(results, &record)
case "dnssec-failed.org":
return results, core.DNSSECError{}
case "servfail.com":
return results, 0, fmt.Errorf("SERVFAIL")
}
return results, nil
return results, 0, nil
}
// LookupMX is a mock
func (mock *MockDNS) LookupMX(domain string) ([]string, time.Duration, error) {
switch domain {
case "letsencrypt.org":
fallthrough
case "email.com":
return []string{"mail.email.com"}, 0, nil
}
return nil, 0, nil
}

View File

@ -9,8 +9,11 @@ import (
"testing"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/mocks"
)
var log = mocks.UseMockLog()
func TestWillingToIssue(t *testing.T) {
shouldBeSyntaxError := []string{
``, // Empty name

View File

@ -9,7 +9,6 @@ import (
"crypto/x509"
"errors"
"fmt"
"net"
"net/mail"
"net/url"
"regexp"
@ -27,11 +26,12 @@ import (
// NOTE: All of the fields in RegistrationAuthorityImpl need to be
// populated, or there is a risk of panic.
type RegistrationAuthorityImpl struct {
CA core.CertificateAuthority
VA core.ValidationAuthority
SA core.StorageAuthority
PA core.PolicyAuthority
log *blog.AuditLogger
CA core.CertificateAuthority
VA core.ValidationAuthority
SA core.StorageAuthority
PA core.PolicyAuthority
DNSResolver core.DNSResolver
log *blog.AuditLogger
AuthzBase string
MaxKeySize int
@ -53,7 +53,7 @@ func lastPathSegment(url core.AcmeURL) string {
return allButLastPathSegment.ReplaceAllString(url.Path, "")
}
func validateEmail(address string) (err error) {
func validateEmail(address string, resolver core.DNSResolver) (err error) {
_, err = mail.ParseAddress(address)
if err != nil {
err = core.MalformedRequestError(fmt.Sprintf("%s is not a valid e-mail address", address))
@ -61,8 +61,8 @@ func validateEmail(address string) (err error) {
}
splitEmail := strings.SplitN(address, "@", -1)
domain := strings.ToLower(splitEmail[len(splitEmail)-1])
var mx []*net.MX
mx, err = net.LookupMX(domain)
var mx []string
mx, _, err = resolver.LookupMX(domain)
if err != nil || len(mx) == 0 {
err = core.MalformedRequestError(fmt.Sprintf("No MX record for domain %s", domain))
return
@ -70,13 +70,13 @@ func validateEmail(address string) (err error) {
return
}
func validateContacts(contacts []core.AcmeURL) (err error) {
func validateContacts(contacts []core.AcmeURL, resolver core.DNSResolver) (err error) {
for _, contact := range contacts {
switch contact.Scheme {
case "tel":
continue
case "mailto":
err = validateEmail(contact.Opaque)
err = validateEmail(contact.Opaque, resolver)
if err != nil {
return
}
@ -116,7 +116,7 @@ func (ra *RegistrationAuthorityImpl) NewRegistration(init core.Registration) (re
}
reg.MergeUpdate(init)
err = validateContacts(reg.Contact)
err = validateContacts(reg.Contact, ra.DNSResolver)
if err != nil {
return
}
@ -367,7 +367,7 @@ func (ra *RegistrationAuthorityImpl) NewCertificate(req core.CertificateRequest,
func (ra *RegistrationAuthorityImpl) UpdateRegistration(base core.Registration, update core.Registration) (reg core.Registration, err error) {
base.MergeUpdate(update)
err = validateContacts(base.Contact)
err = validateContacts(base.Contact, ra.DNSResolver)
if err != nil {
return
}

View File

@ -132,6 +132,8 @@ var (
}
AuthzUpdated = core.Authorization{}
AuthzFinal = core.Authorization{}
log = mocks.UseMockLog()
)
func initAuthorities(t *testing.T) (core.CertificateAuthority, *DummyValidationAuthority, *sa.SQLStorageAuthority, core.RegistrationAuthority) {
@ -198,6 +200,7 @@ func initAuthorities(t *testing.T) (core.CertificateAuthority, *DummyValidationA
ra.PA = pa
ra.AuthzBase = "http://acme.invalid/authz/"
ra.MaxKeySize = 4096
ra.DNSResolver = &mocks.MockDNS{}
AuthzInitial.RegistrationID = Registration.ID
@ -228,38 +231,38 @@ func TestValidateContacts(t *testing.T) {
invalidEmail, _ := url.Parse("mailto:admin@example.com")
malformedEmail, _ := url.Parse("mailto:admin.com")
err := validateContacts([]core.AcmeURL{})
err := validateContacts([]core.AcmeURL{}, &mocks.MockDNS{})
test.AssertNotError(t, err, "No Contacts")
err = validateContacts([]core.AcmeURL{core.AcmeURL(*tel)})
err = validateContacts([]core.AcmeURL{core.AcmeURL(*tel)}, &mocks.MockDNS{})
test.AssertNotError(t, err, "Simple Telephone")
err = validateContacts([]core.AcmeURL{core.AcmeURL(*validEmail)})
err = validateContacts([]core.AcmeURL{core.AcmeURL(*validEmail)}, &mocks.MockDNS{})
test.AssertNotError(t, err, "Valid Email")
err = validateContacts([]core.AcmeURL{core.AcmeURL(*invalidEmail)})
err = validateContacts([]core.AcmeURL{core.AcmeURL(*invalidEmail)}, &mocks.MockDNS{})
test.AssertError(t, err, "Invalid Email")
err = validateContacts([]core.AcmeURL{core.AcmeURL(*malformedEmail)})
err = validateContacts([]core.AcmeURL{core.AcmeURL(*malformedEmail)}, &mocks.MockDNS{})
test.AssertError(t, err, "Malformed Email")
err = validateContacts([]core.AcmeURL{core.AcmeURL(*ansible)})
err = validateContacts([]core.AcmeURL{core.AcmeURL(*ansible)}, &mocks.MockDNS{})
test.AssertError(t, err, "Unknown scehme")
}
func TestValidateEmail(t *testing.T) {
err := validateEmail("an email`")
err := validateEmail("an email`", &mocks.MockDNS{})
test.AssertError(t, err, "Malformed")
err = validateEmail("a@not.a.domain")
err = validateEmail("a@not.a.domain", &mocks.MockDNS{})
test.AssertError(t, err, "Cannot resolve")
t.Logf("No Resolve: %s", err)
err = validateEmail("a@example.com")
err = validateEmail("a@example.com", &mocks.MockDNS{})
test.AssertError(t, err, "No MX Record")
t.Logf("No MX: %s", err)
err = validateEmail("a@email.com")
err = validateEmail("a@email.com", &mocks.MockDNS{})
test.AssertNotError(t, err, "Valid")
}

View File

@ -13,9 +13,12 @@ import (
jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/square/go-jose"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/mocks"
"github.com/letsencrypt/boulder/test"
)
var log = mocks.UseMockLog()
const JWK1JSON = `{
"kty": "RSA",
"n": "vuc785P8lBj3fUxyZchF_uZw6WtbxcorqgTyq-qapF5lrO1U82Tp93rpXlmctj6fyFHBVVB5aXnUHJ7LZeVPod7Wnfl8p5OyhlHQHC8BnzdzCqCMKmWZNX5DtETDId0qzU7dPzh0LP0idt5buU7L9QNaabChw3nnaL47iu_1Di5Wp264p2TwACeedv2hfRDjDlJmaQXuS8Rtv9GnRWyC9JBu7XmGvGDziumnJH7Hyzh3VNu-kSPQD3vuAFgMZS6uUzOztCkT0fpOalZI6hqxtWLvXUMj-crXrn-Maavz8qRhpAyp5kcYk3jiHGgQIi7QSK2JIdRJ8APyX9HlmTN5AQ",

View File

@ -18,11 +18,14 @@ import (
_ "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/mattn/go-sqlite3"
jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/square/go-jose"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/mocks"
"github.com/letsencrypt/boulder/test"
"io/ioutil"
"testing"
)
var log = mocks.UseMockLog()
func initSA(t *testing.T) *SQLStorageAuthority {
sa, err := NewSQLStorageAuthority("sqlite3", ":memory:")
if err != nil {

View File

@ -37,6 +37,10 @@
"wfe": {
"listenAddress": "127.0.0.1:4000",
"certCacheDuration": "6h",
"certNoCacheExpirationWindow": "96h",
"indexCacheDuration": "24h",
"issuerCacheDuration": "48h",
"debugAddr": "localhost:8000"
},
@ -110,6 +114,8 @@
},
"ra": {
"dnsResolver": "8.8.8.8:53",
"dnsTimeout": "10s",
"debugAddr": "localhost:8002"
},

View File

@ -32,6 +32,10 @@
"wfe": {
"listenAddress": "127.0.0.1:4000",
"certCacheDuration": "6h",
"certNoCacheExpirationWindow": "96h",
"indexCacheDuration": "24h",
"issuerCacheDuration": "48h",
"debugAddr": "localhost:8000"
},
@ -97,6 +101,8 @@
},
"ra": {
"dnsResolver": "8.8.8.8:53",
"dnsTimeout": "10s",
"debugAddr": "localhost:8002"
},

View File

@ -32,6 +32,10 @@
"wfe": {
"listenAddress": "127.0.0.1:4300",
"certCacheDuration": "6h",
"certNoCacheExpirationWindow": "8765h",
"indexCacheDuration": "24h",
"issuerCacheDuration": "48h",
"debugAddr": "localhost:8000"
},
@ -101,6 +105,8 @@
},
"ra": {
"dnsResolver": "127.0.0.1:8053",
"dnsTimeout": "10s",
"debugAddr": "localhost:8002"
},

View File

@ -54,6 +54,23 @@ type verificationRequestEvent struct {
// Validation methods
// setChallengeErrorFromDNSError checks the error returned from Lookup...
// methods and tests if the error was an underlying net.OpError or an error
// caused by resolver returning SERVFAIL or other invalid Rcodes and sets
// the challenge.Error field accordingly.
func setChallengeErrorFromDNSError(err error, challenge *core.Challenge) {
challenge.Error = &core.ProblemDetails{Type: core.ConnectionProblem}
if netErr, ok := err.(*net.OpError); ok {
if netErr.Timeout() {
challenge.Error.Detail = "DNS query timed out"
} else if netErr.Temporary() {
challenge.Error.Detail = "Temporary network connectivity error"
}
} else {
challenge.Error.Detail = "Server failure at resolver"
}
}
func (va ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdentifier, input core.Challenge) (core.Challenge, error) {
challenge := input
@ -117,9 +134,14 @@ func (va ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdentif
// connection immediately.
DisableKeepAlives: true,
}
logRedirect := func(req *http.Request, via []*http.Request) error {
va.log.Info(fmt.Sprintf("validateSimpleHTTP [%s] redirect from %q to %q", identifier, via[len(via)-1].URL.String(), req.URL.String()))
return nil
}
client := http.Client{
Transport: tr,
Timeout: 5 * time.Second,
Transport: tr,
CheckRedirect: logRedirect,
Timeout: 5 * time.Second,
}
httpResponse, err := client.Do(httpRequest)
@ -296,19 +318,9 @@ func (va ValidationAuthorityImpl) validateDNS(identifier core.AcmeIdentifier, in
txts, _, err := va.DNSResolver.LookupTXT(challengeSubdomain)
if err != nil {
if dnssecErr, ok := err.(core.DNSSECError); ok {
challenge.Error = &core.ProblemDetails{
Type: core.DNSSECProblem,
Detail: dnssecErr.Error(),
}
} else {
challenge.Error = &core.ProblemDetails{
Type: core.ServerInternalProblem,
Detail: "Unable to communicate with DNS server",
}
}
challenge.Status = core.StatusInvalid
va.log.Debug(fmt.Sprintf("DNS [%s] DNS failure: %s", identifier, err))
setChallengeErrorFromDNSError(err, &challenge)
va.log.Debug(fmt.Sprintf("%s [%s] DNS failure: %s", challenge.Type, identifier, err))
return challenge, challenge.Error
}
@ -424,9 +436,9 @@ func newCAASet(CAAs []*dns.CAA) *CAASet {
return &filtered
}
func (va *ValidationAuthorityImpl) getCAASet(domain string, dnsResolver core.DNSResolver) (*CAASet, error) {
domain = strings.TrimRight(domain, ".")
splitDomain := strings.Split(domain, ".")
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:], ".")
@ -437,7 +449,14 @@ func (va *ValidationAuthorityImpl) getCAASet(domain string, dnsResolver core.DNS
// Query CAA records for domain and its alias if it has a CNAME
for _, alias := range []bool{false, true} {
CAAs, err := va.DNSResolver.LookupCAA(queryDomain, alias)
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
}
@ -455,8 +474,8 @@ func (va *ValidationAuthorityImpl) getCAASet(domain string, dnsResolver core.DNS
// CheckCAARecords verifies that, if the indicated subscriber domain has any CAA
// records, they authorize the configured CA domain to issue a certificate
func (va *ValidationAuthorityImpl) CheckCAARecords(identifier core.AcmeIdentifier) (present, valid bool, err error) {
domain := strings.ToLower(identifier.Value)
caaSet, err := va.getCAASet(domain, va.DNSResolver)
hostname := strings.ToLower(identifier.Value)
caaSet, err := va.getCAASet(hostname)
if err != nil {
return
}
@ -472,7 +491,7 @@ func (va *ValidationAuthorityImpl) CheckCAARecords(identifier core.AcmeIdentifie
} else if len(caaSet.Issue) > 0 || len(caaSet.Issuewild) > 0 {
present = true
var checkSet []*dns.CAA
if strings.SplitN(domain, ".", 2)[0] == "*" {
if strings.SplitN(hostname, ".", 2)[0] == "*" {
checkSet = caaSet.Issuewild
} else {
checkSet = caaSet.Issue

View File

@ -14,6 +14,7 @@ import (
"crypto/x509/pkix"
"encoding/base64"
"fmt"
"log/syslog"
"math/big"
"net"
"net/http"
@ -51,9 +52,15 @@ var TheKey = rsa.PrivateKey{
var ident = core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "localhost"}
var log = mocks.UseMockLog()
const expectedToken = "THETOKEN"
const pathWrongToken = "wrongtoken"
const path404 = "404"
const pathFound = "302"
const pathMoved = "301"
const pathUnsafe = "%"
const pathUnsafe302 = "302-to-unsafe"
func simpleSrv(t *testing.T, token string, stopChan, waitChan chan bool, enableTLS bool) {
m := http.NewServeMux()
@ -65,6 +72,15 @@ func simpleSrv(t *testing.T, token string, stopChan, waitChan chan bool, enableT
} else if strings.HasSuffix(r.URL.Path, pathWrongToken) {
t.Logf("SIMPLESRV: Got a wrongtoken req\n")
fmt.Fprintf(w, "wrongtoken")
} else if strings.HasSuffix(r.URL.Path, pathMoved) {
t.Logf("SIMPLESRV: Got a 301 redirect req\n")
http.Redirect(w, r, "valid", 301)
} else if strings.HasSuffix(r.URL.Path, pathFound) {
t.Logf("SIMPLESRV: Got a 302 redirect req\n")
http.Redirect(w, r, pathMoved, 302)
} else if strings.HasSuffix(r.URL.Path, pathUnsafe302) {
t.Logf("SIMPLESRV: Got a 302-to-unsafe-path req\n")
http.Redirect(w, r, pathUnsafe, 302)
} else if strings.HasSuffix(r.URL.Path, "wait") {
t.Logf("SIMPLESRV: Got a wait req\n")
time.Sleep(time.Second * 3)
@ -210,9 +226,13 @@ func TestSimpleHttpTLS(t *testing.T) {
defer func() { stopChan <- true }()
<-waitChan
log.Clear()
finChall, err := va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, chall.Path)
logs := log.GetAllMatching(`^\[AUDIT\] Attempting to validate SimpleHTTPS for `)
test.AssertEquals(t, len(logs), 1)
test.AssertEquals(t, logs[0].Priority, syslog.LOG_NOTICE)
}
func TestSimpleHttp(t *testing.T) {
@ -233,15 +253,34 @@ func TestSimpleHttp(t *testing.T) {
defer func() { stopChan <- true }()
<-waitChan
log.Clear()
finChall, err := va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, chall.Path)
test.AssertEquals(t, len(log.GetAllMatching(`^\[AUDIT\] `)), 1)
log.Clear()
chall.Path = pathMoved
finChall, err = va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, chall.Path)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/301" to ".*/valid"`)), 1)
log.Clear()
chall.Path = pathFound
finChall, err = va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, chall.Path)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/302" to ".*/301"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/301" to ".*/valid"`)), 1)
log.Clear()
chall.Path = path404
invalidChall, err = va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "Should have found a 404 for the challenge.")
test.AssertEquals(t, invalidChall.Error.Type, core.UnauthorizedProblem)
test.AssertEquals(t, len(log.GetAllMatching(`^\[AUDIT\] `)), 1)
chall.Path = pathWrongToken
invalidChall, err = va.validateSimpleHTTP(ident, chall)
@ -269,12 +308,18 @@ func TestSimpleHttp(t *testing.T) {
test.AssertEquals(t, invalidChall.Error.Type, core.UnknownHostProblem)
va.TestMode = true
chall.Path = "%"
chall.Path = pathUnsafe
invalidChall, err = va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "Path doesn't consist of URL-safe characters.")
test.AssertEquals(t, invalidChall.Error.Type, core.MalformedProblem)
chall.Path = pathUnsafe302
invalidChall, err = va.validateSimpleHTTP(ident, chall)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "Redirect should have failed.")
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
chall.Path = "wait-long"
started := time.Now()
invalidChall, err = va.validateSimpleHTTP(ident, chall)
@ -320,8 +365,8 @@ func TestDvsni(t *testing.T) {
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "Domain name is invalid.")
test.AssertEquals(t, invalidChall.Error.Type, core.UnknownHostProblem)
va.TestMode = true
va.TestMode = true
chall.R = ba[5:]
invalidChall, err = va.validateDvsni(ident, chall)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
@ -536,8 +581,8 @@ func TestCAAChecking(t *testing.T) {
test.AssertEquals(t, caaTest.Valid, valid)
}
present, valid, err := va.CheckCAARecords(core.AcmeIdentifier{Type: "dns", Value: "dnssec-failed.org"})
test.AssertError(t, err, "dnssec-failed.org")
present, valid, err := va.CheckCAARecords(core.AcmeIdentifier{Type: "dns", Value: "servfail.com"})
test.AssertError(t, err, "servfail.com")
test.Assert(t, !present, "Present should be false")
test.Assert(t, !valid, "Valid should be false")
}
@ -630,7 +675,7 @@ func TestDNSValidationNotSane(t *testing.T) {
}
}
func TestDNSValidationBadDNSSEC(t *testing.T) {
func TestDNSValidationServFail(t *testing.T) {
va := NewValidationAuthorityImpl(true)
va.DNSResolver = &mocks.MockDNS{}
mockRA := &MockRegistrationAuthority{}
@ -638,21 +683,21 @@ func TestDNSValidationBadDNSSEC(t *testing.T) {
chalDNS := core.DNSChallenge()
badDNSSEC := core.AcmeIdentifier{
badIdent := core.AcmeIdentifier{
Type: core.IdentifierDNS,
Value: "dnssec-failed.org",
Value: "servfail.com",
}
var authz = core.Authorization{
ID: core.NewToken(),
RegistrationID: 1,
Identifier: badDNSSEC,
Identifier: badIdent,
Challenges: []core.Challenge{chalDNS},
}
va.validate(authz, 0)
test.AssertNotNil(t, mockRA.lastAuthz, "Should have gotten an authorization")
test.Assert(t, authz.Challenges[0].Status == core.StatusInvalid, "Should be invalid.")
test.AssertEquals(t, authz.Challenges[0].Error.Type, core.DNSSECProblem)
test.AssertEquals(t, authz.Challenges[0].Error.Type, core.ConnectionProblem)
}
func TestDNSValidationNoServer(t *testing.T) {
@ -672,7 +717,7 @@ func TestDNSValidationNoServer(t *testing.T) {
test.AssertNotNil(t, mockRA.lastAuthz, "Should have gotten an authorization")
test.Assert(t, authz.Challenges[0].Status == core.StatusInvalid, "Should be invalid.")
test.AssertEquals(t, authz.Challenges[0].Error.Type, core.ServerInternalProblem)
test.AssertEquals(t, authz.Challenges[0].Error.Type, core.ConnectionProblem)
}
// TestDNSValidationLive is an integration test, depending on

View File

@ -30,6 +30,7 @@ import (
// Paths are the ACME-spec identified URL path-segments for various methods
const (
DirectoryPath = "/directory"
NewRegPath = "/acme/new-reg"
RegPath = "/acme/reg/"
NewAuthzPath = "/acme/new-authz"
@ -58,6 +59,9 @@ type WebFrontEndImpl struct {
NewCert string
CertBase string
// JSON encoded endpoint directory
DirectoryJSON []byte
// Issuer certificate (DER) for /acme/issuer-cert
IssuerCert []byte
@ -66,6 +70,12 @@ type WebFrontEndImpl struct {
// Register of anti-replay nonces
nonceService core.NonceService
// Cache settings
CertCacheDuration time.Duration
CertNoCacheExpirationWindow time.Duration
IndexCacheDuration time.Duration
IssuerCacheDuration time.Duration
}
func statusCodeFromError(err interface{}) int {
@ -182,7 +192,7 @@ func (wfe *WebFrontEndImpl) HandleFunc(mux *http.ServeMux, pattern string, h fun
// Handler returns an http.Handler that uses various functions for
// various ACME-specified paths.
func (wfe *WebFrontEndImpl) Handler() http.Handler {
func (wfe *WebFrontEndImpl) Handler() (http.Handler, error) {
wfe.NewReg = wfe.BaseURL + NewRegPath
wfe.RegBase = wfe.BaseURL + RegPath
wfe.NewAuthz = wfe.BaseURL + NewAuthzPath
@ -190,8 +200,22 @@ func (wfe *WebFrontEndImpl) Handler() http.Handler {
wfe.NewCert = wfe.BaseURL + NewCertPath
wfe.CertBase = wfe.BaseURL + CertPath
// Only generate directory once
directory := map[string]string{
"new-reg": wfe.NewReg,
"new-authz": wfe.NewAuthz,
"new-cert": wfe.NewCert,
"revoke-cert": wfe.BaseURL + RevokeCertPath,
}
directoryJSON, err := json.Marshal(directory)
if err != nil {
return nil, err
}
wfe.DirectoryJSON = directoryJSON
m := http.NewServeMux()
wfe.HandleFunc(m, "/", wfe.Index, "GET")
wfe.HandleFunc(m, DirectoryPath, wfe.Directory, "GET")
wfe.HandleFunc(m, NewRegPath, wfe.NewRegistration, "POST")
wfe.HandleFunc(m, NewAuthzPath, wfe.NewAuthorization, "POST")
wfe.HandleFunc(m, NewCertPath, wfe.NewCertificate, "POST")
@ -202,7 +226,7 @@ func (wfe *WebFrontEndImpl) Handler() http.Handler {
wfe.HandleFunc(m, TermsPath, wfe.Terms, "GET")
wfe.HandleFunc(m, IssuerPath, wfe.Issuer, "GET")
wfe.HandleFunc(m, BuildIDPath, wfe.BuildID, "GET")
return m
return m, nil
}
// Method implementations
@ -231,6 +255,19 @@ func (wfe *WebFrontEndImpl) Index(response http.ResponseWriter, request *http.Re
`))
tmpl.Execute(response, wfe)
response.Header().Set("Content-Type", "text/html")
addCacheHeader(response, wfe.IndexCacheDuration.Seconds())
}
func addNoCacheHeader(w http.ResponseWriter) {
w.Header().Add("Cache-Control", "public, max-age=0, no-cache")
}
func addCacheHeader(w http.ResponseWriter, age float64) {
w.Header().Add("Cache-Control", fmt.Sprintf("public, max-age=%.f", age))
}
func (wfe *WebFrontEndImpl) Directory(response http.ResponseWriter, request *http.Request) {
response.Write(wfe.DirectoryJSON)
}
// The ID is always the last slash-separated token in the path
@ -933,12 +970,14 @@ func (wfe *WebFrontEndImpl) Certificate(response http.ResponseWriter, request *h
if !strings.HasPrefix(path, CertPath) {
logEvent.Error = "Certificate not found"
wfe.sendError(response, logEvent.Error, path, http.StatusNotFound)
addNoCacheHeader(response)
return
}
serial := path[len(CertPath):]
if len(serial) != 16 || !allHex.Match([]byte(serial)) {
logEvent.Error = "Certificate not found"
wfe.sendError(response, logEvent.Error, serial, http.StatusNotFound)
addNoCacheHeader(response)
return
}
wfe.log.Debug(fmt.Sprintf("Requested certificate ID %s", serial))
@ -950,11 +989,14 @@ func (wfe *WebFrontEndImpl) Certificate(response http.ResponseWriter, request *h
if strings.HasPrefix(err.Error(), "gorp: multiple rows returned") {
wfe.sendError(response, "Multiple certificates with same short serial", err, http.StatusConflict)
} else {
wfe.sendError(response, "Not found", err, http.StatusNotFound)
addNoCacheHeader(response)
wfe.sendError(response, "Certificate not found", err, http.StatusNotFound)
}
return
}
addCacheHeader(response, wfe.CertCacheDuration.Seconds())
// TODO Content negotiation
response.Header().Set("Content-Type", "application/pkix-cert")
response.Header().Add("Link", link(IssuerPath, "up"))
@ -985,6 +1027,8 @@ func (wfe *WebFrontEndImpl) Issuer(response http.ResponseWriter, request *http.R
logEvent := wfe.populateRequestEvent(request)
defer wfe.logRequestDetails(&logEvent)
addCacheHeader(response, wfe.IssuerCacheDuration.Seconds())
// TODO Content negotiation
response.Header().Set("Content-Type", "application/pkix-cert")
response.WriteHeader(http.StatusOK)

View File

@ -6,6 +6,7 @@
package wfe
import (
"bytes"
"crypto/rsa"
"crypto/x509"
"database/sql"
@ -15,6 +16,7 @@ import (
"errors"
"io"
"io/ioutil"
"log/syslog"
"net/http"
"net/http/httptest"
"net/url"
@ -28,6 +30,7 @@ import (
jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/square/go-jose"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/mocks"
"github.com/letsencrypt/boulder/ra"
"github.com/letsencrypt/boulder/test"
)
@ -125,6 +128,8 @@ wk6Oiadty3eQqSBJv0HnpmiEdQVffIK5Pg4M8Dd+aOBnEkbopAJOuA==
"5dd9c885526136d810fc7640f5ba56281e2b75fa3ff7c91a7d23bab7fd4"
)
var log = mocks.UseMockLog()
type MockSA struct {
// empty
}
@ -189,13 +194,12 @@ func (sa *MockSA) GetCertificate(serial string) (core.Certificate, error) {
RegistrationID: 1,
DER: certBlock.Bytes,
}, nil
} else {
return core.Certificate{}, errors.New("No cert")
}
return core.Certificate{}, errors.New("No cert")
}
func (sa *MockSA) GetCertificateByShortSerial(string) (core.Certificate, error) {
return core.Certificate{}, nil
func (sa *MockSA) GetCertificateByShortSerial(serial string) (core.Certificate, error) {
return sa.GetCertificate("0000000000000000" + serial)
}
func (sa *MockSA) GetCertificateStatus(serial string) (core.CertificateStatus, error) {
@ -374,7 +378,7 @@ func TestHandleFunc(t *testing.T) {
{[]string{"GET", "POST"}, "POST", true},
{[]string{"GET"}, "", false},
{[]string{"GET"}, "POST", false},
{[]string{"GET"}, "OPTIONS", false}, // TODO, #469
{[]string{"GET"}, "OPTIONS", false}, // TODO, #469
{[]string{"GET"}, "MAKE-COFFEE", false}, // 405, or 418?
} {
runWrappedHandler(&http.Request{Method: c.reqMethod}, c.allowed...)
@ -408,7 +412,8 @@ func TestHandleFunc(t *testing.T) {
func TestStandardHeaders(t *testing.T) {
wfe := setupWFE(t)
mux := wfe.Handler()
mux, err := wfe.Handler()
test.AssertNotError(t, err, "Problem setting up HTTP handlers")
cases := []struct {
path string
@ -442,6 +447,7 @@ func TestStandardHeaders(t *testing.T) {
func TestIndex(t *testing.T) {
wfe := setupWFE(t)
wfe.IndexCacheDuration = time.Second * 10
responseWriter := httptest.NewRecorder()
@ -454,21 +460,42 @@ func TestIndex(t *testing.T) {
test.AssertNotEquals(t, responseWriter.Body.String(), "404 page not found\n")
test.Assert(t, strings.Contains(responseWriter.Body.String(), wfe.NewReg),
"new-reg not found")
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=10")
responseWriter.Body.Reset()
responseWriter.Header().Del("Cache-Control")
url, _ = url.Parse("/foo")
wfe.Index(responseWriter, &http.Request{
URL: url,
})
//test.AssertEquals(t, responseWriter.Code, http.StatusNotFound)
test.AssertEquals(t, responseWriter.Body.String(), "404 page not found\n")
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "")
}
func TestDirectory(t *testing.T) {
wfe := setupWFE(t)
wfe.BaseURL = "http://localhost:4300"
mux, err := wfe.Handler()
test.AssertNotError(t, err, "Problem setting up HTTP handlers")
responseWriter := httptest.NewRecorder()
url, _ := url.Parse("/directory")
mux.ServeHTTP(responseWriter, &http.Request{
Method: "GET",
URL: url,
})
test.AssertEquals(t, responseWriter.Code, http.StatusOK)
test.AssertEquals(t, responseWriter.Body.String(), `{"new-authz":"http://localhost:4300/acme/new-authz","new-cert":"http://localhost:4300/acme/new-cert","new-reg":"http://localhost:4300/acme/new-reg","revoke-cert":"http://localhost:4300/acme/revoke-cert"}`)
}
// TODO: Write additional test cases for:
// - RA returns with a failure
func TestIssueCertificate(t *testing.T) {
wfe := setupWFE(t)
mux := wfe.Handler()
mux, err := wfe.Handler()
test.AssertNotError(t, err, "Problem setting up HTTP handlers")
// TODO: Use a mock RA so we can test various conditions of authorized, not authorized, etc.
ra := ra.NewRegistrationAuthorityImpl()
@ -574,6 +601,7 @@ func TestIssueCertificate(t *testing.T) {
responseWriter.Body.String(),
"{\"type\":\"urn:acme:error:unauthorized\",\"detail\":\"Error creating new cert :: Key not authorized for name meep.com\"}")
log.Clear()
responseWriter.Body.Reset()
wfe.NewCertificate(responseWriter, &http.Request{
Method: "POST",
@ -595,6 +623,11 @@ func TestIssueCertificate(t *testing.T) {
test.AssertEquals(
t, responseWriter.Header().Get("Content-Type"),
"application/pkix-cert")
reqlogs := log.GetAllMatching(`Certificate request - successful`)
test.AssertEquals(t, len(reqlogs), 1)
test.AssertEquals(t, reqlogs[0].Priority, syslog.LOG_NOTICE)
test.AssertContains(t, reqlogs[0].Message, `[AUDIT] `)
test.AssertContains(t, reqlogs[0].Message, `"Names":["not-an-example.com"]`)
}
func TestChallenge(t *testing.T) {
@ -649,7 +682,8 @@ func TestChallenge(t *testing.T) {
func TestNewRegistration(t *testing.T) {
wfe := setupWFE(t)
mux := wfe.Handler()
mux, err := wfe.Handler()
test.AssertNotError(t, err, "Problem setting up HTTP handlers")
wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &MockSA{}
@ -893,7 +927,8 @@ func TestRevokeCertificateAlreadyRevoked(t *testing.T) {
func TestAuthorization(t *testing.T) {
wfe := setupWFE(t)
mux := wfe.Handler()
mux, err := wfe.Handler()
test.AssertNotError(t, err, "Problem setting up HTTP handlers")
wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &MockSA{}
@ -972,13 +1007,14 @@ func TestAuthorization(t *testing.T) {
test.AssertEquals(t, responseWriter.Body.String(), "{\"identifier\":{\"type\":\"dns\",\"value\":\"test.com\"}}")
var authz core.Authorization
err := json.Unmarshal([]byte(responseWriter.Body.String()), &authz)
err = json.Unmarshal([]byte(responseWriter.Body.String()), &authz)
test.AssertNotError(t, err, "Couldn't unmarshal returned authorization object")
}
func TestRegistration(t *testing.T) {
wfe := setupWFE(t)
mux := wfe.Handler()
mux, err := wfe.Handler()
test.AssertNotError(t, err, "Problem setting up HTTP handlers")
wfe.RA = &MockRegistrationAuthority{}
wfe.SA = &MockSA{}
@ -1100,3 +1136,75 @@ func TestTermsRedirect(t *testing.T) {
agreementURL)
test.AssertEquals(t, responseWriter.Code, 302)
}
func TestIssuer(t *testing.T) {
wfe := setupWFE(t)
wfe.IssuerCacheDuration = time.Second * 10
wfe.IssuerCert = []byte{0, 0, 1}
responseWriter := httptest.NewRecorder()
wfe.Issuer(responseWriter, &http.Request{
Method: "GET",
})
test.AssertEquals(t, responseWriter.Code, http.StatusOK)
test.Assert(t, bytes.Compare(responseWriter.Body.Bytes(), wfe.IssuerCert) == 0, "Incorrect bytes returned")
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=10")
}
func TestGetCertificate(t *testing.T) {
wfe := setupWFE(t)
wfe.CertCacheDuration = time.Second * 10
wfe.CertNoCacheExpirationWindow = time.Hour * 24 * 7
wfe.SA = &MockSA{}
certPemBytes, _ := ioutil.ReadFile("test/178.crt")
certBlock, _ := pem.Decode(certPemBytes)
responseWriter := httptest.NewRecorder()
// Valid short serial, cached
path, _ := url.Parse("/acme/cert/00000000000000b2")
wfe.Certificate(responseWriter, &http.Request{
Method: "GET",
URL: path,
})
test.AssertEquals(t, responseWriter.Code, 200)
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=10")
test.AssertEquals(t, responseWriter.Header().Get("Content-Type"), "application/pkix-cert")
test.Assert(t, bytes.Compare(responseWriter.Body.Bytes(), certBlock.Bytes) == 0, "Certificates don't match")
// Unused short serial, no cache
responseWriter = httptest.NewRecorder()
path, _ = url.Parse("/acme/cert/00000000000000ff")
wfe.Certificate(responseWriter, &http.Request{
Method: "GET",
URL: path,
})
test.AssertEquals(t, responseWriter.Code, 404)
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=0, no-cache")
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Certificate not found"}`)
// Invalid short serial, no cache
responseWriter = httptest.NewRecorder()
path, _ = url.Parse("/acme/cert/nothex")
wfe.Certificate(responseWriter, &http.Request{
Method: "GET",
URL: path,
})
test.AssertEquals(t, responseWriter.Code, 404)
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=0, no-cache")
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Certificate not found"}`)
// Invalid short serial, no cache
responseWriter = httptest.NewRecorder()
path, _ = url.Parse("/acme/cert/00000000000000")
wfe.Certificate(responseWriter, &http.Request{
Method: "GET",
URL: path,
})
test.AssertEquals(t, responseWriter.Code, 404)
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=0, no-cache")
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Certificate not found"}`)
}