diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index e39a80fcc..a095c0a00 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -126,7 +126,7 @@ }, { "ImportPath": "github.com/letsencrypt/net/publicsuffix", - "Rev": "b1959d2ce5b815bfc7aa5ca2d2af62293eb4abd5" + "Rev": "edf0080f92131f2a12f1c7763d74ae60e3ebf550" }, { "ImportPath": "gopkg.in/gorp.v1", diff --git a/Godeps/_workspace/src/github.com/letsencrypt/net/publicsuffix/list.go b/Godeps/_workspace/src/github.com/letsencrypt/net/publicsuffix/list.go index 8a69286ad..f0eb6bcc7 100644 --- a/Godeps/_workspace/src/github.com/letsencrypt/net/publicsuffix/list.go +++ b/Godeps/_workspace/src/github.com/letsencrypt/net/publicsuffix/list.go @@ -73,7 +73,7 @@ func getSuffix(domain string, icannOnly bool) (publicSuffix string, icann bool) s, suffix, wildcard := domain, len(domain), false var dot int loop: - for ;; s = s[:dot] { + for { dot = strings.LastIndex(s, ".") if wildcard { suffix = 1 + dot @@ -91,6 +91,10 @@ loop: // If we're only interested in ICANN suffixes, ignore any matches that are // not ICANN. if icannOnly && !icann { + if dot == -1 { + break + } + s = s[:dot] continue } u >>= nodesBitsICANN @@ -112,6 +116,7 @@ loop: if dot == -1 { break } + s = s[:dot] } if icannOnly && suffix < len(domain) { icann = true diff --git a/cmd/boulder-wfe/main.go b/cmd/boulder-wfe/main.go index 46c7b2eac..a00612320 100644 --- a/cmd/boulder-wfe/main.go +++ b/cmd/boulder-wfe/main.go @@ -76,7 +76,7 @@ func main() { go cmd.DebugServer(c.WFE.DebugAddr) - wfe, err := wfe.NewWebFrontEndImpl(stats) + wfe, err := wfe.NewWebFrontEndImpl(stats, clock.Default()) cmd.FailOnError(err, "Unable to create WFE") rac, sac, closeChan := setupWFE(c, auditlogger, stats) wfe.RA = &rac diff --git a/cmd/ocsp-responder/main.go b/cmd/ocsp-responder/main.go index 67478d10d..28af94b8b 100644 --- a/cmd/ocsp-responder/main.go +++ b/cmd/ocsp-responder/main.go @@ -8,6 +8,7 @@ package main import ( "bytes" "crypto/x509" + "database/sql" "encoding/hex" "errors" "fmt" @@ -20,7 +21,6 @@ import ( "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/facebookgo/httpdown" "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/jmhodges/clock" "github.com/letsencrypt/boulder/Godeps/_workspace/src/golang.org/x/crypto/ocsp" - gorp "github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/cmd" @@ -55,34 +55,41 @@ serialNumber field, since we will always query on it. */ type DBSource struct { - dbMap *gorp.DbMap + dbMap dbSelector caKeyHash []byte + log *blog.AuditLogger +} + +// Since the only thing we use from gorp is the SelectOne method on the +// gorp.DbMap object, we just define the interface an interface with that method +// instead of importing all of gorp. This also allows us to simulate MySQL failures +// by mocking the interface. +type dbSelector interface { + SelectOne(holder interface{}, query string, args ...interface{}) error } // NewSourceFromDatabase produces a DBSource representing the binding of a // given DB schema to a CA key. -func NewSourceFromDatabase(dbMap *gorp.DbMap, caKeyHash []byte) (src *DBSource, err error) { - src = &DBSource{dbMap: dbMap, caKeyHash: caKeyHash} +func NewSourceFromDatabase(dbMap dbSelector, caKeyHash []byte, log *blog.AuditLogger) (src *DBSource, err error) { + src = &DBSource{dbMap: dbMap, caKeyHash: caKeyHash, log: log} return } // Response is called by the HTTP server to handle a new OCSP request. func (src *DBSource) Response(req *ocsp.Request) ([]byte, bool) { - log := blog.GetAuditLogger() - // Check that this request is for the proper CA if bytes.Compare(req.IssuerKeyHash, src.caKeyHash) != 0 { - log.Debug(fmt.Sprintf("Request intended for CA Cert ID: %s", hex.EncodeToString(req.IssuerKeyHash))) + src.log.Debug(fmt.Sprintf("Request intended for CA Cert ID: %s", hex.EncodeToString(req.IssuerKeyHash))) return nil, false } serialString := core.SerialToString(req.SerialNumber) - log.Debug(fmt.Sprintf("Searching for OCSP issued by us for serial %s", serialString)) + src.log.Debug(fmt.Sprintf("Searching for OCSP issued by us for serial %s", serialString)) var response []byte defer func() { if len(response) != 0 { - log.Info(fmt.Sprintf("OCSP Response sent for CA=%s, Serial=%s", hex.EncodeToString(src.caKeyHash), serialString)) + src.log.Info(fmt.Sprintf("OCSP Response sent for CA=%s, Serial=%s", hex.EncodeToString(src.caKeyHash), serialString)) } }() // Note: we first check for an OCSP response in the certificateStatus table ( @@ -94,6 +101,9 @@ func (src *DBSource) Response(req *ocsp.Request) ([]byte, bool) { "SELECT ocspResponse FROM certificateStatus WHERE serial = :serial", map[string]interface{}{"serial": serialString}, ) + if err != nil && err != sql.ErrNoRows { + src.log.Err(fmt.Sprintf("Failed to retrieve response from certificateStatus table: %s", err)) + } // TODO(#970): Delete this ocspResponses check once the table has been removed if len(response) == 0 { // Ignoring possible error, if response hasn't been filled, attempt to find @@ -103,6 +113,9 @@ func (src *DBSource) Response(req *ocsp.Request) ([]byte, bool) { "SELECT response from ocspResponses WHERE serial = :serial ORDER BY id DESC LIMIT 1;", map[string]interface{}{"serial": serialString}, ) + if err != nil && err != sql.ErrNoRows { + src.log.Err(fmt.Sprintf("Failed to retrieve response from ocspResponses table: %s", err)) + } } if err != nil { return nil, false @@ -111,14 +124,7 @@ func (src *DBSource) Response(req *ocsp.Request) ([]byte, bool) { return response, true } -func makeDBSource(dbConnect, issuerCert string, sqlDebug bool) (*DBSource, error) { - // Configure DB - dbMap, err := sa.NewDbMap(dbConnect) - if err != nil { - return nil, fmt.Errorf("Could not connect to database: %s", err) - } - sa.SetSQLDebug(dbMap, sqlDebug) - +func makeDBSource(dbMap dbSelector, issuerCert string, log *blog.AuditLogger) (*DBSource, error) { // Load the CA's key so we can store its SubjectKey in the DB caCertDER, err := cmd.LoadCert(issuerCert) if err != nil { @@ -133,7 +139,7 @@ func makeDBSource(dbConnect, issuerCert string, sqlDebug bool) (*DBSource, error } // Construct source from DB - return NewSourceFromDatabase(dbMap, caCert.SubjectKeyId) + return NewSourceFromDatabase(dbMap, caCert.SubjectKeyId, log) } func main() { @@ -163,7 +169,12 @@ func main() { if url.Scheme == "mysql+tcp" { auditlogger.Info(fmt.Sprintf("Loading OCSP Database for CA Cert: %s", c.Common.IssuerCert)) - source, err = makeDBSource(config.Source, c.Common.IssuerCert, c.SQL.SQLDebug) + dbMap, err := sa.NewDbMap(config.Source) + cmd.FailOnError(err, "Could not connect to database") + if c.SQL.SQLDebug { + sa.SetSQLDebug(dbMap, true) + } + source, err = makeDBSource(dbMap, c.Common.IssuerCert, auditlogger) cmd.FailOnError(err, "Couldn't load OCSP DB") } else if url.Scheme == "file" { filename := url.Path diff --git a/cmd/ocsp-responder/main_test.go b/cmd/ocsp-responder/main_test.go index 2768aa6d6..9825f3c1f 100644 --- a/cmd/ocsp-responder/main_test.go +++ b/cmd/ocsp-responder/main_test.go @@ -12,7 +12,10 @@ import ( cfocsp "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/ocsp" "github.com/letsencrypt/boulder/Godeps/_workspace/src/golang.org/x/crypto/ocsp" + "github.com/letsencrypt/boulder/core" + blog "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/mocks" "github.com/letsencrypt/boulder/sa" "github.com/letsencrypt/boulder/test" ) @@ -62,11 +65,14 @@ func TestHandler(t *testing.T) { } func TestDBHandler(t *testing.T) { - src, err := makeDBSource("mysql+tcp://ocsp_resp@localhost:3306/boulder_sa_test", "./testdata/test-ca.der.pem", false) + dbMap, err := sa.NewDbMap("mysql+tcp://ocsp_resp@localhost:3306/boulder_sa_test") + test.AssertNotError(t, err, "Could not connect to database") + src, err := makeDBSource(dbMap, "./testdata/test-ca.der.pem", blog.GetAuditLogger()) if err != nil { t.Fatalf("makeDBSource: %s", err) } defer test.ResetSATestDatabase(t) + ocspResp, err := ocsp.ParseResponse(resp, nil) if err != nil { t.Fatalf("ocsp.ParseResponse: %s", err) @@ -99,7 +105,31 @@ func TestDBHandler(t *testing.T) { if !bytes.Equal(w.Body.Bytes(), resp) { t.Errorf("Mismatched body: want %#v, got %#v", resp, w.Body.Bytes()) } +} +// brokenSelector allows us to test what happens when gorp SelectOne statements +// throw errors and satisfies the dbSelector interface +type brokenSelector struct{} + +func (bs brokenSelector) SelectOne(_ interface{}, _ string, _ ...interface{}) error { + return fmt.Errorf("Failure!") +} + +func TestErrorLog(t *testing.T) { + src, err := makeDBSource(brokenSelector{}, "./testdata/test-ca.der.pem", blog.GetAuditLogger()) + test.AssertNotError(t, err, "Failed to create broken dbMap") + + src.log.SyslogWriter = mocks.NewSyslogWriter() + mockLog := src.log.SyslogWriter.(*mocks.SyslogWriter) + + ocspReq, err := ocsp.ParseRequest(req) + test.AssertNotError(t, err, "Failed to parse OCSP request") + + _, found := src.Response(ocspReq) + test.Assert(t, !found, "Somehow found OCSP response") + + test.AssertEquals(t, len(mockLog.GetAllMatching("Failed to retrieve response from certificateStatus table")), 1) + test.AssertEquals(t, len(mockLog.GetAllMatching("Failed to retrieve response from ocspResponses table")), 1) } func mustRead(path string) []byte { diff --git a/cmd/ocsp-updater/main.go b/cmd/ocsp-updater/main.go index 772d01cd4..209997813 100644 --- a/cmd/ocsp-updater/main.go +++ b/cmd/ocsp-updater/main.go @@ -227,7 +227,7 @@ func (updater *OCSPUpdater) generateRevokedResponse(status core.CertificateStatu return &status, nil } -func (updater *OCSPUpdater) storeResponse(status *core.CertificateStatus, statusGuard core.OCSPStatus) error { +func (updater *OCSPUpdater) storeResponse(status *core.CertificateStatus) error { // Update the certificateStatus table with the new OCSP response, the status // WHERE is used make sure we don't overwrite a revoked response with a one // containing a 'good' status and that we don't do the inverse when the OCSP @@ -240,7 +240,7 @@ func (updater *OCSPUpdater) storeResponse(status *core.CertificateStatus, status status.OCSPResponse, status.OCSPLastUpdated, status.Serial, - string(statusGuard), + string(status.Status), ) return err } @@ -291,7 +291,7 @@ func (updater *OCSPUpdater) revokedCertificatesTick(batchSize int) { updater.stats.Inc("OCSP.Errors.RevokedResponseGeneration", 1, 1.0) continue } - err = updater.storeResponse(meta, core.OCSPStatusRevoked) + err = updater.storeResponse(meta) if err != nil { updater.stats.Inc("OCSP.Errors.StoreRevokedResponse", 1, 1.0) updater.log.AuditErr(fmt.Errorf("Failed to store OCSP response: %s", err)) @@ -309,7 +309,7 @@ func (updater *OCSPUpdater) generateOCSPResponses(statuses []core.CertificateSta continue } updater.stats.Inc("OCSP.GeneratedResponses", 1, 1.0) - err = updater.storeResponse(meta, core.OCSPStatusGood) + err = updater.storeResponse(meta) if err != nil { updater.log.AuditErr(fmt.Errorf("Failed to store OCSP response: %s", err)) updater.stats.Inc("OCSP.Errors.StoreResponse", 1, 1.0) diff --git a/cmd/ocsp-updater/main_test.go b/cmd/ocsp-updater/main_test.go index f4c2a2807..201efdc6e 100644 --- a/cmd/ocsp-updater/main_test.go +++ b/cmd/ocsp-updater/main_test.go @@ -93,12 +93,12 @@ func TestGenerateAndStoreOCSPResponse(t *testing.T) { meta, err := updater.generateResponse(status) test.AssertNotError(t, err, "Couldn't generate OCSP response") - err = updater.storeResponse(meta, core.OCSPStatusGood) + err = updater.storeResponse(meta) test.AssertNotError(t, err, "Couldn't store certificate status") secondMeta, err := updater.generateRevokedResponse(status) test.AssertNotError(t, err, "Couldn't generate revoked OCSP response") - err = updater.storeResponse(secondMeta, core.OCSPStatusGood) + err = updater.storeResponse(secondMeta) test.AssertNotError(t, err, "Couldn't store certificate status") newStatus, err := sa.GetCertificateStatus(status.Serial) @@ -152,7 +152,7 @@ func TestFindStaleOCSPResponses(t *testing.T) { meta, err := updater.generateResponse(status) test.AssertNotError(t, err, "Couldn't generate OCSP response") - err = updater.storeResponse(meta, core.OCSPStatusGood) + err = updater.storeResponse(meta) test.AssertNotError(t, err, "Couldn't store OCSP response") certs, err = updater.findStaleOCSPResponses(earliest, 10) @@ -290,18 +290,27 @@ func TestStoreResponseGuard(t *testing.T) { status, err := sa.GetCertificateStatus(core.SerialToString(parsedCert.SerialNumber)) test.AssertNotError(t, err, "Failed to get certificate status") - status.OCSPResponse = []byte{0} - err = updater.storeResponse(&status, core.OCSPStatusRevoked) + err = sa.MarkCertificateRevoked(core.SerialToString(parsedCert.SerialNumber), 0) + test.AssertNotError(t, err, "Failed to revoked certificate") + + // Attempt to update OCSP response where status.Status is good but stored status + // is revoked, this should fail silently + status.OCSPResponse = []byte{0, 1, 1} + err = updater.storeResponse(&status) test.AssertNotError(t, err, "Failed to update certificate status") + // Make sure the OCSP response hasn't actually changed unchangedStatus, err := sa.GetCertificateStatus(core.SerialToString(parsedCert.SerialNumber)) test.AssertNotError(t, err, "Failed to get certificate status") test.AssertEquals(t, len(unchangedStatus.OCSPResponse), 0) - err = updater.storeResponse(&status, core.OCSPStatusGood) + // Changing the status to the stored status should allow the update to occur + status.Status = core.OCSPStatusRevoked + err = updater.storeResponse(&status) test.AssertNotError(t, err, "Failed to updated certificate status") + // Make sure the OCSP response has been updated changedStatus, err := sa.GetCertificateStatus(core.SerialToString(parsedCert.SerialNumber)) test.AssertNotError(t, err, "Failed to get certificate status") - test.AssertEquals(t, len(changedStatus.OCSPResponse), 1) + test.AssertEquals(t, len(changedStatus.OCSPResponse), 3) } diff --git a/cmd/shell.go b/cmd/shell.go index f48e06614..5d7c16ac9 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -375,25 +375,45 @@ func FailOnError(err error, msg string) { // ProfileCmd runs forever, sending Go runtime statistics to StatsD. func ProfileCmd(profileName string, stats statsd.Statter) { + var memoryStats runtime.MemStats + prevNumGC := int64(0) c := time.Tick(1 * time.Second) for range c { - var memoryStats runtime.MemStats runtime.ReadMemStats(&memoryStats) + // Gather goroutine count stats.Gauge(fmt.Sprintf("%s.Gostats.Goroutines", profileName), int64(runtime.NumGoroutine()), 1.0) + // Gather various heap metrics stats.Gauge(fmt.Sprintf("%s.Gostats.Heap.Alloc", profileName), int64(memoryStats.HeapAlloc), 1.0) stats.Gauge(fmt.Sprintf("%s.Gostats.Heap.Objects", profileName), int64(memoryStats.HeapObjects), 1.0) stats.Gauge(fmt.Sprintf("%s.Gostats.Heap.Idle", profileName), int64(memoryStats.HeapIdle), 1.0) stats.Gauge(fmt.Sprintf("%s.Gostats.Heap.InUse", profileName), int64(memoryStats.HeapInuse), 1.0) stats.Gauge(fmt.Sprintf("%s.Gostats.Heap.Released", profileName), int64(memoryStats.HeapReleased), 1.0) - // Calculate average and last and convert from nanoseconds to milliseconds - gcPauseAvg := (int64(memoryStats.PauseTotalNs) / int64(len(memoryStats.PauseNs))) / 1000000 - lastGC := int64(memoryStats.PauseNs[(memoryStats.NumGC+255)%256]) / 1000000 - stats.Timing(fmt.Sprintf("%s.Gostats.Gc.PauseAvg", profileName), gcPauseAvg, 1.0) - stats.Gauge(fmt.Sprintf("%s.Gostats.Gc.LastPauseLatency", profileName), lastGC, 1.0) + // Gather various GC related metrics + if memoryStats.NumGC > 0 { + totalRecentGC := uint64(0) + realBufSize := uint32(256) + if memoryStats.NumGC < 256 { + realBufSize = memoryStats.NumGC + } + for _, pause := range memoryStats.PauseNs { + totalRecentGC += pause + } + gcPauseAvg := totalRecentGC / uint64(realBufSize) + lastGC := memoryStats.PauseNs[(memoryStats.NumGC+255)%256] + stats.Timing(fmt.Sprintf("%s.Gostats.Gc.PauseAvg", profileName), int64(gcPauseAvg), 1.0) + stats.Gauge(fmt.Sprintf("%s.Gostats.Gc.LastPause", profileName), int64(lastGC), 1.0) + } stats.Gauge(fmt.Sprintf("%s.Gostats.Gc.NextAt", profileName), int64(memoryStats.NextGC), 1.0) + // Send both a counter and a gauge here we can much more easily observe + // the GC rate (versus the raw number of GCs) in graphing tools that don't + // like deltas + stats.Gauge(fmt.Sprintf("%s.Gostats.Gc.Count", profileName), int64(memoryStats.NumGC), 1.0) + gcInc := int64(memoryStats.NumGC) - prevNumGC + stats.Inc(fmt.Sprintf("%s.Gostats.Gc.Rate", profileName), gcInc, 1.0) + prevNumGC += gcInc } } diff --git a/docs/metrics/README.md b/docs/metrics/README.md index d219edefd..e3fb41db1 100644 --- a/docs/metrics/README.md +++ b/docs/metrics/README.md @@ -97,16 +97,19 @@ This list is split up into metric topics with the names of the clients that subm * Client performance profiling (`cmd/boulder-*`) ``` - [gauge] Boulder.{cmd-name}.Gostats.Goroutines - [gauge] Boulder.{cmd-name}.Gostats.Heap.Alloc - [gauge] Boulder.{cmd-name}.Gostats.Heap.Objects - [gauge] Boulder.{cmd-name}.Gostats.Heap.Idle - [gauge] Boulder.{cmd-name}.Gostats.Heap.InUse - [gauge] Boulder.{cmd-name}.Gostats.Heap.Released - [gauge] Boulder.{cmd-name}.Gostats.Gc.NextAt - [gauge] Boulder.{cmd-name}.Gostats.Gc.LastPauseLatency + [counter] Boulder.{cmd-name}.Gostats.Gc.Rate - [timing] Boulder.{cmd-name}.Gostats.Gc.PauseAvg + [gauge] Boulder.{cmd-name}.Gostats.Goroutines + [gauge] Boulder.{cmd-name}.Gostats.Heap.Alloc + [gauge] Boulder.{cmd-name}.Gostats.Heap.Objects + [gauge] Boulder.{cmd-name}.Gostats.Heap.Idle + [gauge] Boulder.{cmd-name}.Gostats.Heap.InUse + [gauge] Boulder.{cmd-name}.Gostats.Heap.Released + [gauge] Boulder.{cmd-name}.Gostats.Gc.NextAt + [gauge] Boulder.{cmd-name}.Gostats.Gc.Count + [gauge] Boulder.{cmd-name}.Gostats.Gc.LastPause + + [timing] Boulder.{cmd-name}.Gostats.Gc.PauseAvg ``` * External certificate store loading (`cmd/external-cert-importer`) diff --git a/mocks/mocks.go b/mocks/mocks.go index 8259dcb85..a100cb33d 100644 --- a/mocks/mocks.go +++ b/mocks/mocks.go @@ -14,6 +14,7 @@ import ( "strings" "time" + "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/jmhodges/clock" "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose" "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/miekg/dns" @@ -134,9 +135,16 @@ func (mock *DNSResolver) LookupMX(domain string) ([]string, time.Duration, error // StorageAuthority is a mock type StorageAuthority struct { + clk clock.Clock authorizedDomains map[string]bool } +// NewStorageAuthority creates a new mock storage authority +// with the given clock. +func NewStorageAuthority(clk clock.Clock) *StorageAuthority { + return &StorageAuthority{clk: clk} +} + const ( test1KeyPublicJSON = ` { @@ -198,24 +206,32 @@ func (sa *StorageAuthority) GetRegistrationByKey(jwk jose.JsonWebKey) (core.Regi // GetAuthorization is a mock func (sa *StorageAuthority) GetAuthorization(id string) (core.Authorization, error) { - if id == "valid" { - exp := time.Now().AddDate(100, 0, 0) - return core.Authorization{ - ID: "valid", - Status: core.StatusValid, - RegistrationID: 1, - Expires: &exp, - Identifier: core.AcmeIdentifier{Type: "dns", Value: "not-an-example.com"}, - Challenges: []core.Challenge{ - core.Challenge{ - ID: 23, - Type: "dns", - URI: "http://localhost:4300/acme/challenge/valid/23", - }, + authz := core.Authorization{ + ID: "valid", + Status: core.StatusValid, + RegistrationID: 1, + Identifier: core.AcmeIdentifier{Type: "dns", Value: "not-an-example.com"}, + Challenges: []core.Challenge{ + core.Challenge{ + ID: 23, + Type: "dns", }, - }, nil + }, } - return core.Authorization{}, nil + + if id == "valid" { + exp := sa.clk.Now().AddDate(100, 0, 0) + authz.Expires = &exp + authz.Challenges[0].URI = "http://localhost:4300/acme/challenge/valid/23" + return authz, nil + } else if id == "expired" { + exp := sa.clk.Now().AddDate(0, -1, 0) + authz.Expires = &exp + authz.Challenges[0].URI = "http://localhost:4300/acme/challenge/expired/23" + return authz, nil + } + + return core.Authorization{}, fmt.Errorf("authz not found") } // GetCertificate is a mock @@ -318,7 +334,7 @@ func (sa *StorageAuthority) AddSCTReceipt(sct core.SignedCertificateTimestamp) ( func (sa *StorageAuthority) 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) + exp := sa.clk.Now().AddDate(100, 0, 0) return core.Authorization{Status: core.StatusValid, RegistrationID: 1, Expires: &exp, Identifier: identifier}, nil } } diff --git a/publisher/publisher_test.go b/publisher/publisher_test.go index 6b0b754d4..2ae5d6d86 100644 --- a/publisher/publisher_test.go +++ b/publisher/publisher_test.go @@ -19,6 +19,8 @@ import ( "testing" "time" + "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/jmhodges/clock" + "github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/mocks" "github.com/letsencrypt/boulder/test" @@ -196,7 +198,7 @@ func setup(t *testing.T, port, retries int) (PublisherImpl, *x509.Certificate) { }) test.AssertNotError(t, err, "Couldn't create new Publisher") pub.issuerBundle = append(pub.issuerBundle, base64.StdEncoding.EncodeToString(intermediatePEM.Bytes)) - pub.SA = &mocks.StorageAuthority{} + pub.SA = mocks.NewStorageAuthority(clock.NewFake()) leafPEM, _ := pem.Decode([]byte(testLeaf)) leaf, err := x509.ParseCertificate(leafPEM.Bytes) diff --git a/ra/registration-authority.go b/ra/registration-authority.go index 930b0ac7b..d3469fed2 100644 --- a/ra/registration-authority.go +++ b/ra/registration-authority.go @@ -32,6 +32,11 @@ import ( // TODO(jsha): Read from a config file. const DefaultAuthorizationLifetime = 300 * 24 * time.Hour +// DefaultPendingAuthorizationLifetime is one week. If you can't respond to a +// challenge this quickly, then you need to request a new challenge. +// TODO(rlb): Read from a config file +const DefaultPendingAuthorizationLifetime = 7 * 24 * time.Hour + // RegistrationAuthorityImpl defines an RA. // // NOTE: All of the fields in RegistrationAuthorityImpl need to be @@ -46,12 +51,13 @@ type RegistrationAuthorityImpl struct { clk clock.Clock log *blog.AuditLogger // How long before a newly created authorization expires. - authorizationLifetime time.Duration - rlPolicies cmd.RateLimitConfig - tiMu *sync.RWMutex - totalIssuedCache int - lastIssuedCount *time.Time - maxContactsPerReg int + authorizationLifetime time.Duration + pendingAuthorizationLifetime time.Duration + rlPolicies cmd.RateLimitConfig + tiMu *sync.RWMutex + totalIssuedCache int + lastIssuedCount *time.Time + maxContactsPerReg int } // NewRegistrationAuthorityImpl constructs a new RA object. @@ -60,10 +66,11 @@ func NewRegistrationAuthorityImpl(clk clock.Clock, logger *blog.AuditLogger, sta stats: stats, clk: clk, log: logger, - authorizationLifetime: DefaultAuthorizationLifetime, - rlPolicies: policies, - tiMu: new(sync.RWMutex), - maxContactsPerReg: maxContactsPerReg, + authorizationLifetime: DefaultAuthorizationLifetime, + pendingAuthorizationLifetime: DefaultPendingAuthorizationLifetime, + rlPolicies: policies, + tiMu: new(sync.RWMutex), + maxContactsPerReg: maxContactsPerReg, } return ra } @@ -275,7 +282,7 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization // Create validations. The WFE will update them with URIs before sending them out. challenges, combinations, err := ra.PA.ChallengesFor(identifier, ®.Key) - expires := ra.clk.Now().Add(ra.authorizationLifetime) + expires := ra.clk.Now().Add(ra.pendingAuthorizationLifetime) // Partially-filled object authz = core.Authorization{ @@ -284,8 +291,7 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization Status: core.StatusPending, Combinations: combinations, Challenges: challenges, - // TODO(jsha): Pending authz should expire earlier than finalized authz. - Expires: &expires, + Expires: &expires, } // Get a pending Auth first so we can get our ID back, then update with challenges @@ -623,6 +629,12 @@ func (ra *RegistrationAuthorityImpl) UpdateRegistration(base core.Registration, // UpdateAuthorization updates an authorization with new values. func (ra *RegistrationAuthorityImpl) UpdateAuthorization(base core.Authorization, challengeIndex int, response core.Challenge) (authz core.Authorization, err error) { + // Refuse to update expired authorizations + if base.Expires == nil || base.Expires.Before(ra.clk.Now()) { + err = core.NotFoundError("Expired authorization") + return + } + // Copy information over that the client is allowed to supply authz = base if challengeIndex >= len(authz.Challenges) { diff --git a/ra/registration-authority_test.go b/ra/registration-authority_test.go index a763855be..530290924 100644 --- a/ra/registration-authority_test.go +++ b/ra/registration-authority_test.go @@ -460,6 +460,22 @@ func TestUpdateAuthorization(t *testing.T) { t.Log("DONE TestUpdateAuthorization") } +func TestUpdateAuthorizationExpired(t *testing.T) { + _, _, ra, fc, cleanUp := initAuthorities(t) + defer cleanUp() + + authz, err := ra.NewAuthorization(AuthzRequest, Registration.ID) + test.AssertNotError(t, err, "NewAuthorization failed") + + expiry := fc.Now().Add(-2 * time.Hour) + authz.Expires = &expiry + + response, err := makeResponse(authz.Challenges[ResponseIndex]) + + authz, err = ra.UpdateAuthorization(authz, ResponseIndex, response) + test.AssertError(t, err, "Updated expired authorization") +} + func TestUpdateAuthorizationReject(t *testing.T) { _, sa, ra, _, cleanUp := initAuthorities(t) defer cleanUp() diff --git a/test/amqp-integration-test.py b/test/amqp-integration-test.py index b3c6cd8d0..0814f6b0d 100644 --- a/test/amqp-integration-test.py +++ b/test/amqp-integration-test.py @@ -69,7 +69,7 @@ def get_ocsp(cert_file, url): with open(ocsp_resp_file, "w") as f: f.write(get_response) - ocsp_verify_cmd = "%s -CAfile ../test-ca.pem -respin %s" % (openssl_ocsp, ocsp_resp_file) + ocsp_verify_cmd = "%s -CAfile ../test-root.pem -respin %s" % (openssl_ocsp, ocsp_resp_file) print ocsp_verify_cmd try: output = subprocess.check_output(ocsp_verify_cmd, shell=True) diff --git a/test/test-ca.pem b/test/test-ca.pem index 760417fe9..fa00031d3 100644 --- a/test/test-ca.pem +++ b/test/test-ca.pem @@ -1,19 +1,24 @@ -----BEGIN CERTIFICATE----- -MIIDETCCAfmgAwIBAgIJAJzxkS6o1QkIMA0GCSqGSIb3DQEBCwUAMB8xHTAbBgNV -BAMMFGhhcHB5IGhhY2tlciBmYWtlIENBMB4XDTE1MDQwNzIzNTAzOFoXDTI1MDQw -NDIzNTAzOFowHzEdMBsGA1UEAwwUaGFwcHkgaGFja2VyIGZha2UgQ0EwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCCkd5mgXFErJ3F2M0E9dw+Ta/md5i -8TDId01HberAApqmydG7UZYF3zLTSzNjlNSOmtybvrSGUnZ9r9tSQcL8VM6WUOM8 -tnIpiIjEA2QkBycMwvRmZ/B2ltPdYs/R9BqNwO1g18GDZrHSzUYtNKNeFI6Glamj -7GK2Vr0SmiEamlNIR5ktAFsEErzf/d4jCF7sosMsJpMCm1p58QkP4LHLShVLXDa8 -BMfVoI+ipYcA08iNUFkgW8VWDclIDxcysa0psDDtMjX3+4aPkE/cefmP+1xOfUuD -HOGV8XFynsP4EpTfVOZr0/g9gYQ7ZArqXX7GTQkFqduwPm/w5qxSPTarAgMBAAGj -UDBOMB0GA1UdDgQWBBT7eE8S+WAVgyyfF380GbMuNupBiTAfBgNVHSMEGDAWgBT7 -eE8S+WAVgyyfF380GbMuNupBiTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA -A4IBAQAd9Da+Zv+TjMv7NTAmliqnWHY6d3UxEZN3hFEJ58IQVHbBZVZdW7zhRktB -vR05Kweac0HJeK91TKmzvXl21IXLvh0gcNLU/uweD3no/snfdB4OoFompljThmgl -zBqiqWoKBJQrLCA8w5UB+ReomRYd/EYXF/6TAfzm6hr//Xt5mPiUHPdvYt75lMAo -vRxLSbF8TSQ6b7BYxISWjPgFASNNqJNHEItWsmQMtAjjwzb9cs01XH9pChVAWn9L -oeMKa+SlHSYrWG93+EcrIH/dGU76uNOiaDzBSKvaehG53h25MHuO1anNICJvZovW -rFo4Uv1EnkKJm3vJFe50eJGhEKlx +MIIEHDCCAwSgAwIBAgICEl4wDQYJKoZIhvcNAQELBQAwKzEpMCcGA1UEAwwgY2Fj +a2xpbmcgY3J5cHRvZ3JhcGhlciBmYWtlIFJPT1QwHhcNMTUxMDIwMjAzMTQyWhcN +MjAxMDE4MjAzMTQyWjAfMR0wGwYDVQQDDBRoYXBweSBoYWNrZXIgZmFrZSBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMIKR3maBcUSsncXYzQT13D5 +Nr+Z3mLxMMh3TUdt6sACmqbJ0btRlgXfMtNLM2OU1I6a3Ju+tIZSdn2v21JBwvxU +zpZQ4zy2cimIiMQDZCQHJwzC9GZn8HaW091iz9H0Go3A7WDXwYNmsdLNRi00o14U +joaVqaPsYrZWvRKaIRqaU0hHmS0AWwQSvN/93iMIXuyiwywmkwKbWnnxCQ/gsctK +FUtcNrwEx9Wgj6KlhwDTyI1QWSBbxVYNyUgPFzKxrSmwMO0yNff7ho+QT9x5+Y/7 +XE59S4Mc4ZXxcXKew/gSlN9U5mvT+D2BhDtkCupdfsZNCQWp27A+b/DmrFI9NqsC +AwEAAaOCAVQwggFQMBIGA1UdEwEB/wQIMAYBAf8CAQAwQgYDVR0eBDswOaE3MAWC +A21pbDAKhwgAAAAAAAAAADAihyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAADAOBgNVHQ8BAf8EBAMCAYYwMQYIKwYBBQUHAQEEJTAjMCEGCCsGAQUFBzAB +hhVodHRwOi8vbG9jYWxob3N0OjQwMDMwHwYDVR0jBBgwFoAUz0nVEcH3uFO1Wlnl +fuQ+5Dpx0EcwSwYDVR0gBEQwQjAIBgZngQwBAgEwNgYLKwYBBAGC3xMBAQEwJzAl +BggrBgEFBQcCARYZaHR0cDovL2xvY2FsaG9zdDo0MDAwL2NwczAmBgNVHR8EHzAd +MBugGaAXhhVodHRwOi8vbG9jYWxob3N0OjU1NTUwHQYDVR0OBBYEFPt4TxL5YBWD +LJ8XfzQZsy426kGJMA0GCSqGSIb3DQEBCwUAA4IBAQCPKuy6CFUte+8CFWTm+jtP +tnwLFnvznPGsF/GGb/JHRFJMvMoQ4ycvbOfY6jztQqmbf+R54pdPkpKxApQHvwcF +Zd4IsgA39kQcQBQR8ToxsURrCMqTM5EXzXP/RFQHAf8aIcKuplxh1pjyJjbTEAx5 +n8W0HieqvWwy4JmBRI9N7KkpH4Xj5HBkesl4pGZOmt9kgYtN38jRX9BwJMM4IrXM +EdHHgSwOW2BGr1PivJw3KYu20gw0ArPOLgc0M+3CCa8VipVVJwQks8HfXX58gd91 +kfobxltCCeRtKfeVWvF29TfxtHMxkwpZaGLxx+QKWytXqfj8kKF+tYl8PAE7DGJt -----END CERTIFICATE----- diff --git a/test/test-root.pem b/test/test-root.pem new file mode 100644 index 000000000..b6a1df7bd --- /dev/null +++ b/test/test-root.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDGzCCAgOgAwIBAgIJANKZDvEMm9hrMA0GCSqGSIb3DQEBCwUAMCsxKTAnBgNV +BAMMIGNhY2tsaW5nIGNyeXB0b2dyYXBoZXIgZmFrZSBST09UMB4XDTE1MTAyMDIw +MzE0MloXDTI1MTAxNzIwMzE0MlowKzEpMCcGA1UEAwwgY2Fja2xpbmcgY3J5cHRv +Z3JhcGhlciBmYWtlIFJPT1QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCuiPhqvx+SJ1QQo+qFKJ05tDJ5IfOyxbtxuzQnC2JhHhB8PHonpUfYE/8Y96Pd +MWg7+TDRwKYSe1p+Z1VtWk9WmrO2xNMUqJ0+wfSzkg7RIOYARBQNgpd/edGweCjc +mpKe2m7SgBaMlmJWWoJtkYziejMGXnh2NIQStT0EDQTxIG42X5JVCOHl3vrYG/M8 +LkhU+1/Z3EwodMayh7XpAih33qr9pAaHLJYQ9I9q0GbXCBd7g50P6q1nc4DMOZxr +lKbakL6OLKmk5iencbZIER0Mi2HR6W1aep/dlvOSXrrhfkdiuwrw9yRhG6VoCi8G +ctwabow/+4NeRp8sB6I9zglZAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD +VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTPSdURwfe4U7VaWeV+5D7kOnHQRzANBgkq +hkiG9w0BAQsFAAOCAQEAQOWe0K86JT5RsCQGFdPF5gx+YUTPLhsR8o+wnFc3R0GH +mmVDz/FUfHg8RrpcIcc7Hn9PMpSChVJFxc0aOxGmetSQHOboieVq+wQ95fIgAkBE +YtW0LvVCg1AqPwOZOLHWfrnhAz9YUkouBfpUQv+7P3XWsaiJEWdv9TgeknaUSxt/ +Md/tUGLQC7+LaFIss/28QQm0n19KPhQ9Tj/Fyb/SPPHhJ5WHAH52H/m9rQy6q7Ts +8+ls03XFriP6QoU7c3Q+kW6TLw7mXGPccGHy8nY2GmBNwHA04fIVXTwok83wohnO +ge17w48vRJ+gNCvr3V5ucNQlA/wbGS71nExCSGE5dA== +-----END CERTIFICATE----- diff --git a/wfe/web-front-end.go b/wfe/web-front-end.go index 4d78e9cf7..6da36fa60 100644 --- a/wfe/web-front-end.go +++ b/wfe/web-front-end.go @@ -53,6 +53,7 @@ type WebFrontEndImpl struct { SA core.StorageGetter stats statsd.Statter log *blog.AuditLogger + clk clock.Clock // URL configuration parameters BaseURL string @@ -117,7 +118,7 @@ func statusCodeFromError(err interface{}) int { } // NewWebFrontEndImpl constructs a web service for Boulder -func NewWebFrontEndImpl(stats statsd.Statter) (WebFrontEndImpl, error) { +func NewWebFrontEndImpl(stats statsd.Statter, clk clock.Clock) (WebFrontEndImpl, error) { logger := blog.GetAuditLogger() logger.Notice("Web Front End Starting") @@ -128,6 +129,7 @@ func NewWebFrontEndImpl(stats statsd.Statter) (WebFrontEndImpl, error) { return WebFrontEndImpl{ log: logger, + clk: clk, nonceService: nonceService, stats: stats, }, nil @@ -881,6 +883,15 @@ func (wfe *WebFrontEndImpl) Challenge( notFound() return } + + // After expiring, challenges are inaccessible + if authz.Expires == nil || authz.Expires.Before(wfe.clk.Now()) { + msg := fmt.Sprintf("Authorization %v expired in the past (%v)", authz.ID, *authz.Expires) + logEvent.AddError(msg) + wfe.sendError(response, "Expired authorization", msg, http.StatusNotFound) + return + } + // Check that the requested challenge exists within the authorization challengeIndex := authz.FindChallenge(challengeID) if challengeIndex == -1 { @@ -1130,6 +1141,14 @@ func (wfe *WebFrontEndImpl) Authorization(logEvent *requestEvent, response http. logEvent.Extra["AuthorizationStatus"] = authz.Status logEvent.Extra["AuthorizationExpires"] = authz.Expires + // After expiring, authorizations are inaccessible + if authz.Expires == nil || authz.Expires.Before(wfe.clk.Now()) { + msg := fmt.Sprintf("Authorization %v expired in the past (%v)", authz.ID, *authz.Expires) + logEvent.AddError(msg) + wfe.sendError(response, "Expired authorization", msg, http.StatusNotFound) + return + } + wfe.prepAuthorizationForDisplay(&authz) jsonReply, err := json.Marshal(authz) diff --git a/wfe/web-front-end_test.go b/wfe/web-front-end_test.go index 7eb7d705d..f1822ba0f 100644 --- a/wfe/web-front-end_test.go +++ b/wfe/web-front-end_test.go @@ -197,9 +197,10 @@ func signRequest(t *testing.T, req string, nonceService *core.NonceService) stri return ret } -func setupWFE(t *testing.T) WebFrontEndImpl { +func setupWFE(t *testing.T) (WebFrontEndImpl, clock.FakeClock) { + fc := clock.NewFake() stats, _ := statsd.NewNoopClient() - wfe, err := NewWebFrontEndImpl(stats) + wfe, err := NewWebFrontEndImpl(stats, fc) test.AssertNotError(t, err, "Unable to create WFE") wfe.NewReg = wfe.BaseURL + NewRegPath @@ -213,11 +214,10 @@ func setupWFE(t *testing.T) WebFrontEndImpl { wfe.log.SyslogWriter = mocks.NewSyslogWriter() wfe.RA = &MockRegistrationAuthority{} - wfe.SA = &mocks.StorageAuthority{} + wfe.SA = mocks.NewStorageAuthority(fc) wfe.stats, _ = statsd.NewNoopClient() - wfe.SubscriberAgreementURL = agreementURL - return wfe + return wfe, fc } // makePostRequest creates an http.Request with method POST, the provided body, @@ -263,7 +263,7 @@ func addHeadIfGet(s []string) []string { } func TestHandleFunc(t *testing.T) { - wfe := setupWFE(t) + wfe, _ := setupWFE(t) var mux *http.ServeMux var rw *httptest.ResponseRecorder var stubCalled bool @@ -460,7 +460,7 @@ func TestHandleFunc(t *testing.T) { } func TestIndexPOST(t *testing.T) { - wfe := setupWFE(t) + wfe, _ := setupWFE(t) responseWriter := httptest.NewRecorder() url, _ := url.Parse("/") wfe.Index(newRequestEvent(), responseWriter, &http.Request{ @@ -471,7 +471,7 @@ func TestIndexPOST(t *testing.T) { } func TestPOST404(t *testing.T) { - wfe := setupWFE(t) + wfe, _ := setupWFE(t) responseWriter := httptest.NewRecorder() url, _ := url.Parse("/foobar") wfe.Index(newRequestEvent(), responseWriter, &http.Request{ @@ -482,7 +482,7 @@ func TestPOST404(t *testing.T) { } func TestIndex(t *testing.T) { - wfe := setupWFE(t) + wfe, _ := setupWFE(t) wfe.IndexCacheDuration = time.Second * 10 responseWriter := httptest.NewRecorder() @@ -510,7 +510,7 @@ func TestIndex(t *testing.T) { } func TestDirectory(t *testing.T) { - wfe := setupWFE(t) + wfe, _ := setupWFE(t) wfe.BaseURL = "http://localhost:4300" mux, err := wfe.Handler() test.AssertNotError(t, err, "Problem setting up HTTP handlers") @@ -530,7 +530,7 @@ func TestDirectory(t *testing.T) { // TODO: Write additional test cases for: // - RA returns with a failure func TestIssueCertificate(t *testing.T) { - wfe := setupWFE(t) + wfe, fc := setupWFE(t) mux, err := wfe.Handler() test.AssertNotError(t, err, "Problem setting up HTTP handlers") mockLog := wfe.log.SyslogWriter.(*mocks.SyslogWriter) @@ -538,20 +538,18 @@ func TestIssueCertificate(t *testing.T) { // The mock CA we use always returns the same test certificate, with a Not // Before of 2015-09-22. Since we're currently using a real RA instead of a // mock (see below), that date would trigger failures for excessive - // backdating. So we set the fakeClock's time to a time that matches that test - // certificate. - fakeClock := clock.NewFake() + // backdating. So we set the fake clock's time to a time that matches that + // test certificate. testTime := time.Date(2015, 9, 9, 22, 56, 0, 0, time.UTC) - fakeClock.Add(fakeClock.Now().Sub(testTime)) + fc.Add(fc.Now().Sub(testTime)) // TODO: Use a mock RA so we can test various conditions of authorized, not // authorized, etc. stats, _ := statsd.NewNoopClient(nil) - ra := ra.NewRegistrationAuthorityImpl(fakeClock, wfe.log, stats, cmd.RateLimitConfig{}, 0) - ra.SA = &mocks.StorageAuthority{} + ra := ra.NewRegistrationAuthorityImpl(fc, wfe.log, stats, cmd.RateLimitConfig{}, 0) + ra.SA = mocks.NewStorageAuthority(fc) ra.CA = &MockCA{} ra.PA = &MockPA{} - wfe.SA = &mocks.StorageAuthority{} wfe.RA = &ra responseWriter := httptest.NewRecorder() @@ -668,10 +666,7 @@ func TestIssueCertificate(t *testing.T) { } func TestGetChallenge(t *testing.T) { - wfe := setupWFE(t) - - wfe.RA = &MockRegistrationAuthority{} - wfe.SA = &mocks.StorageAuthority{} + wfe, _ := setupWFE(t) challengeURL := "/acme/challenge/valid/23" @@ -706,10 +701,7 @@ func TestGetChallenge(t *testing.T) { } func TestChallenge(t *testing.T) { - wfe := setupWFE(t) - - wfe.RA = &MockRegistrationAuthority{} - wfe.SA = &mocks.StorageAuthority{} + wfe, _ := setupWFE(t) responseWriter := httptest.NewRecorder() var key jose.JsonWebKey @@ -737,18 +729,23 @@ func TestChallenge(t *testing.T) { test.AssertEquals( t, responseWriter.Body.String(), `{"type":"dns","uri":"/acme/challenge/valid/23"}`) + + // Expired challenges should be inaccessible + challengeURL = "/acme/challenge/expired/23" + responseWriter = httptest.NewRecorder() + wfe.Challenge(newRequestEvent(), responseWriter, + makePostRequestWithPath(challengeURL, + signRequest(t, `{"resource":"challenge"}`, &wfe.nonceService))) + test.AssertEquals(t, responseWriter.Code, http.StatusNotFound) + test.AssertEquals(t, responseWriter.Body.String(), + `{"type":"urn:acme:error:malformed","detail":"Expired authorization"}`) } func TestNewRegistration(t *testing.T) { - wfe := setupWFE(t) + wfe, _ := setupWFE(t) mux, err := wfe.Handler() test.AssertNotError(t, err, "Problem setting up HTTP handlers") - wfe.RA = &MockRegistrationAuthority{} - wfe.SA = &mocks.StorageAuthority{} - wfe.stats, _ = statsd.NewNoopClient() - wfe.SubscriberAgreementURL = agreementURL - key, err := jose.LoadPrivateKey([]byte(test2KeyPrivatePEM)) test.AssertNotError(t, err, "Failed to load key") rsaKey, ok := key.(*rsa.PrivateKey) @@ -918,7 +915,7 @@ func makeRevokeRequestJSON() ([]byte, error) { // registration when GetRegistrationByKey is called, and we want to get a // NoSuchRegistrationError for tests that pass regCheck = false to verifyPOST. type mockSANoSuchRegistration struct { - mocks.StorageAuthority + core.StorageGetter } func (msa mockSANoSuchRegistration) GetRegistrationByKey(jwk jose.JsonWebKey) (core.Registration, error) { @@ -940,8 +937,8 @@ func TestRevokeCertificateCertKey(t *testing.T) { revokeRequestJSON, err := makeRevokeRequestJSON() test.AssertNotError(t, err, "Failed to make revokeRequestJSON") - wfe := setupWFE(t) - wfe.SA = &mockSANoSuchRegistration{mocks.StorageAuthority{}} + wfe, fc := setupWFE(t) + wfe.SA = &mockSANoSuchRegistration{mocks.NewStorageAuthority(fc)} responseWriter := httptest.NewRecorder() nonce, err := wfe.nonceService.Nonce() @@ -959,7 +956,7 @@ func TestRevokeCertificateAccountKey(t *testing.T) { revokeRequestJSON, err := makeRevokeRequestJSON() test.AssertNotError(t, err, "Failed to make revokeRequestJSON") - wfe := setupWFE(t) + wfe, _ := setupWFE(t) responseWriter := httptest.NewRecorder() test1JWK, err := jose.LoadPrivateKey([]byte(test1KeyPrivatePEM)) @@ -979,7 +976,7 @@ func TestRevokeCertificateAccountKey(t *testing.T) { // A revocation request signed by an unauthorized key. func TestRevokeCertificateWrongKey(t *testing.T) { - wfe := setupWFE(t) + wfe, _ := setupWFE(t) nonce, err := wfe.nonceService.Nonce() test.AssertNotError(t, err, "Unable to create nonce") responseWriter := httptest.NewRecorder() @@ -1028,12 +1025,10 @@ func TestRevokeCertificateAlreadyRevoked(t *testing.T) { test.AssertNotError(t, err, "Failed to marshal request") // POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON. - wfe := setupWFE(t) + wfe, fc := setupWFE(t) - wfe.RA = &MockRegistrationAuthority{} - wfe.SA = &mockSANoSuchRegistration{mocks.StorageAuthority{}} + wfe.SA = &mockSANoSuchRegistration{mocks.NewStorageAuthority(fc)} wfe.stats, _ = statsd.NewNoopClient() - wfe.SubscriberAgreementURL = agreementURL responseWriter := httptest.NewRecorder() responseWriter.Body.Reset() nonce, err := wfe.nonceService.Nonce() @@ -1047,13 +1042,10 @@ func TestRevokeCertificateAlreadyRevoked(t *testing.T) { } func TestAuthorization(t *testing.T) { - wfe := setupWFE(t) + wfe, _ := setupWFE(t) mux, err := wfe.Handler() test.AssertNotError(t, err, "Problem setting up HTTP handlers") - wfe.RA = &MockRegistrationAuthority{} - wfe.SA = &mocks.StorageAuthority{} - wfe.stats, _ = statsd.NewNoopClient() responseWriter := httptest.NewRecorder() // GET instead of POST should be rejected @@ -1123,6 +1115,17 @@ func TestAuthorization(t *testing.T) { var authz core.Authorization err = json.Unmarshal([]byte(responseWriter.Body.String()), &authz) test.AssertNotError(t, err, "Couldn't unmarshal returned authorization object") + + // Expired authorizations should be inaccessible + authzURL := "/acme/authz/expired" + responseWriter = httptest.NewRecorder() + wfe.Authorization(newRequestEvent(), responseWriter, &http.Request{ + Method: "GET", + URL: mustParseURL(authzURL), + }) + test.AssertEquals(t, responseWriter.Code, http.StatusNotFound) + test.AssertEquals(t, responseWriter.Body.String(), + `{"type":"urn:acme:error:malformed","detail":"Expired authorization"}`) } func contains(s []string, e string) bool { @@ -1135,14 +1138,9 @@ func contains(s []string, e string) bool { } func TestRegistration(t *testing.T) { - wfe := setupWFE(t) + wfe, _ := setupWFE(t) mux, err := wfe.Handler() test.AssertNotError(t, err, "Problem setting up HTTP handlers") - - wfe.RA = &MockRegistrationAuthority{} - wfe.SA = &mocks.StorageAuthority{} - wfe.stats, _ = statsd.NewNoopClient() - wfe.SubscriberAgreementURL = agreementURL responseWriter := httptest.NewRecorder() // Test invalid method @@ -1228,13 +1226,7 @@ func TestRegistration(t *testing.T) { } func TestTermsRedirect(t *testing.T) { - wfe := setupWFE(t) - - wfe.RA = &MockRegistrationAuthority{} - wfe.SA = &mocks.StorageAuthority{} - wfe.stats, _ = statsd.NewNoopClient() - wfe.SubscriberAgreementURL = agreementURL - + wfe, _ := setupWFE(t) responseWriter := httptest.NewRecorder() path, _ := url.Parse("/terms") @@ -1249,7 +1241,7 @@ func TestTermsRedirect(t *testing.T) { } func TestIssuer(t *testing.T) { - wfe := setupWFE(t) + wfe, _ := setupWFE(t) wfe.IssuerCacheDuration = time.Second * 10 wfe.IssuerCert = []byte{0, 0, 1} @@ -1264,13 +1256,12 @@ func TestIssuer(t *testing.T) { } func TestGetCertificate(t *testing.T) { - wfe := setupWFE(t) + wfe, _ := setupWFE(t) mux, err := wfe.Handler() test.AssertNotError(t, err, "Problem setting up HTTP handlers") wfe.CertCacheDuration = time.Second * 10 wfe.CertNoCacheExpirationWindow = time.Hour * 24 * 7 - wfe.SA = &mocks.StorageAuthority{} certPemBytes, _ := ioutil.ReadFile("test/178.crt") certBlock, _ := pem.Decode(certPemBytes) @@ -1340,12 +1331,12 @@ func TestLogCsrPem(t *testing.T) { const certificateRequestJSON = `{ "csr": "MIICWTCCAUECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAycX3ca-fViOuRWF38mssORISFxbJvspDfhPGRBZDxJ63NIqQzupB-6dp48xkcX7Z_KDaRJStcpJT2S0u33moNT4FHLklQBETLhExDk66cmlz6Xibp3LGZAwhWuec7wJoEwIgY8oq4rxihIyGq7HVIJoq9DqZGrUgfZMDeEJqbphukQOaXGEop7mD-eeu8-z5EVkB1LiJ6Yej6R8MAhVPHzG5fyOu6YVo6vY6QgwjRLfZHNj5XthxgPIEETZlUbiSoI6J19GYHvLURBTy5Ys54lYAPIGfNwcIBAH4gtH9FrYcDY68R22rp4iuxdvkf03ZWiT0F2W1y7_C9B2jayTzvQIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAHd6Do9DIZ2hvdt1GwBXYjsqprZidT_DYOMfYcK17KlvdkFT58XrBH88ulLZ72NXEpiFMeTyzfs3XEyGq_Bbe7TBGVYZabUEh-LOskYwhgcOuThVN7tHnH5rhN-gb7cEdysjTb1QL-vOUwYgV75CB6PE5JVYK-cQsMIVvo0Kz4TpNgjJnWzbcH7h0mtvub-fCv92vBPjvYq8gUDLNrok6rbg05tdOJkXsF2G_W-Q6sf2Fvx0bK5JeH4an7P7cXF9VG9nd4sRt5zd-L3IcyvHVKxNhIJXZVH0AOqh_1YrKI9R0QKQiZCEy0xN1okPlcaIVaFhb7IKAHPxTI3r5f72LXY" }` - wfe := setupWFE(t) + wfe, fc := setupWFE(t) var certificateRequest core.CertificateRequest err := json.Unmarshal([]byte(certificateRequestJSON), &certificateRequest) test.AssertNotError(t, err, "Unable to parse certificateRequest") - mockSA := mocks.StorageAuthority{} + mockSA := mocks.NewStorageAuthority(fc) reg, err := mockSA.GetRegistration(789) test.AssertNotError(t, err, "Unable to get registration") @@ -1363,7 +1354,7 @@ func TestLogCsrPem(t *testing.T) { } func TestLengthRequired(t *testing.T) { - wfe := setupWFE(t) + wfe, _ := setupWFE(t) _, _, _, err := wfe.verifyPOST(newRequestEvent(), &http.Request{ Method: "POST", URL: mustParseURL("/"), @@ -1374,7 +1365,7 @@ func TestLengthRequired(t *testing.T) { } type mockSADifferentStoredKey struct { - mocks.StorageAuthority + core.StorageGetter } func (sa mockSADifferentStoredKey) GetRegistrationByKey(jwk jose.JsonWebKey) (core.Registration, error) { @@ -1388,8 +1379,8 @@ func (sa mockSADifferentStoredKey) GetRegistrationByKey(jwk jose.JsonWebKey) (co } func TestVerifyPOSTUsesStoredKey(t *testing.T) { - wfe := setupWFE(t) - wfe.SA = &mockSADifferentStoredKey{mocks.StorageAuthority{}} + wfe, fc := setupWFE(t) + wfe.SA = &mockSADifferentStoredKey{mocks.NewStorageAuthority(fc)} // signRequest signs with test1Key, but our special mock returns a // registration with test2Key _, _, _, err := wfe.verifyPOST(newRequestEvent(), makePostRequest(signRequest(t, `{"resource":"foo"}`, &wfe.nonceService)), true, "foo") @@ -1397,7 +1388,7 @@ func TestVerifyPOSTUsesStoredKey(t *testing.T) { } func TestBadKeyCSR(t *testing.T) { - wfe := setupWFE(t) + wfe, _ := setupWFE(t) responseWriter := httptest.NewRecorder() // CSR with a bad (512 bit RSA) key.