Merge branch 'master' into ocsp-decoding
Conflicts: test/amqp-integration-test.py
This commit is contained in:
commit
a0ba72ea35
36
.travis.yml
36
.travis.yml
|
|
@ -23,7 +23,6 @@ sudo: false
|
||||||
|
|
||||||
services:
|
services:
|
||||||
- rabbitmq
|
- rabbitmq
|
||||||
- mysql
|
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
|
|
@ -40,37 +39,20 @@ branches:
|
||||||
- /^test-.*$/
|
- /^test-.*$/
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
# Travis does shallow clones, so there is no master branch present.
|
- source test/travis-before-install.sh
|
||||||
# But test-no-outdated-migrations.sh needs to check diffs against master.
|
|
||||||
# Fetch just the master branch from origin.
|
|
||||||
- git fetch origin master
|
|
||||||
# Github-PR-Status secret
|
|
||||||
- openssl aes-256-cbc -K $encrypted_53b2630f0fb4_key -iv $encrypted_53b2630f0fb4_iv -in test/github-secret.json.enc -out test/github-secret.json -d || true
|
|
||||||
|
|
||||||
- travis_retry go get golang.org/x/tools/cmd/vet
|
# Override default Travis install command to prevent it from adding
|
||||||
- travis_retry go get golang.org/x/tools/cmd/cover
|
# Godeps/_workspace to GOPATH. When that happens, it hides failures that should
|
||||||
- travis_retry go get github.com/golang/lint/golint
|
# arise from importing non-vendorized paths.
|
||||||
- travis_retry go get github.com/mattn/goveralls
|
install:
|
||||||
- travis_retry go get github.com/modocache/gover
|
- true
|
||||||
- travis_retry go get github.com/jcjones/github-pr-status
|
|
||||||
- travis_retry go get bitbucket.org/liamstask/goose/cmd/goose
|
|
||||||
|
|
||||||
# Boulder consists of multiple Go packages, which
|
|
||||||
# refer to each other by their absolute GitHub path,
|
|
||||||
# e.g. github.com/letsencrypt/boulder/analysis. That means, by default, if
|
|
||||||
# someone forks the repo, Travis won't pass on their own repo. To fix that,
|
|
||||||
# we add a symlink.
|
|
||||||
- mkdir -p $TRAVIS_BUILD_DIR $GOPATH/src/github.com/letsencrypt
|
|
||||||
- test ! -d $GOPATH/src/github.com/letsencrypt/boulder && ln -s $TRAVIS_BUILD_DIR $GOPATH/src/github.com/letsencrypt/boulder || true
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- LETSENCRYPT_PATH=/tmp/letsencrypt
|
- LETSENCRYPT_PATH=$HOME/letsencrypt
|
||||||
matrix:
|
matrix:
|
||||||
- SKIP_INTEGRATION_TESTS=1
|
- RUN="integration vet lint fmt migrations"
|
||||||
- SKIP_UNIT_TESTS=1
|
- RUN="unit"
|
||||||
- RUN_MAKE=1 SKIP_UNIT_TESTS=1 SKIP_INTEGRATION_TESTS=1
|
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- bash test/travis_make.sh
|
|
||||||
- bash test.sh
|
- bash test.sh
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/jmhodges/clock",
|
"ImportPath": "github.com/jmhodges/clock",
|
||||||
"Rev": "c560df7e034994569e9742f6e7716c173f6286eb"
|
"Rev": "3c4ebd218625c9364c33db6d39c276d80c3090c6"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/letsencrypt/go-jose",
|
"ImportPath": "github.com/letsencrypt/go-jose",
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,7 @@ language: go
|
||||||
|
|
||||||
go:
|
go:
|
||||||
- 1.3
|
- 1.3
|
||||||
- tip
|
- 1.4
|
||||||
|
- 1.5
|
||||||
|
|
||||||
|
sudo: false
|
||||||
|
|
@ -3,8 +3,25 @@ clock
|
||||||
|
|
||||||
[](https://travis-ci.org/jmhodges/clock)
|
[](https://travis-ci.org/jmhodges/clock)
|
||||||
|
|
||||||
A Go package that provides an abstraction for system time that enables
|
Package clock provides an abstraction for system time that enables
|
||||||
testing of time-sensitive code.
|
testing of time-sensitive code.
|
||||||
|
|
||||||
|
Where you'd use time.Now, instead use clk.Now where clk is an instance
|
||||||
|
of Clock.
|
||||||
|
|
||||||
|
When running your code in production, pass it a Clock given by
|
||||||
|
Default() and when you're running it in your tests, pass it an instance of Clock from NewFake().
|
||||||
|
|
||||||
|
When you do that, you can use FakeClock's Add and Set methods to
|
||||||
|
control how time behaves in your code making them more reliable while
|
||||||
|
also expanding the space of problems you can test.
|
||||||
|
|
||||||
|
This code intentionally does not attempt to provide an abstraction
|
||||||
|
over time.Ticker and time.Timer because Go does not have the runtime
|
||||||
|
or API hooks available to do reliably. See
|
||||||
|
https://github.com/golang/go/issues/8869
|
||||||
|
|
||||||
|
Be sure to test Time equality with time.Time#Equal, not ==.
|
||||||
|
|
||||||
For documentation, see the
|
For documentation, see the
|
||||||
[godoc](http://godoc.org/github.com/jmhodges/clock).
|
[godoc](http://godoc.org/github.com/jmhodges/clock).
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,21 @@
|
||||||
// Package clock provides an abstraction for system time that enables
|
// Package clock provides an abstraction for system time that enables
|
||||||
// testing of time-sensitive code.
|
// testing of time-sensitive code.
|
||||||
//
|
//
|
||||||
// By passing in Default to production code, you can then use NewFake
|
// Where you'd use time.Now, instead use clk.Now where clk is an
|
||||||
// in tests to create Clocks that control what time the production
|
// instance of Clock.
|
||||||
// code sees.
|
//
|
||||||
|
// When running your code in production, pass it a Clock given by
|
||||||
|
// Default() and when you're running it in your tests, pass it an
|
||||||
|
// instance of Clock from NewFake().
|
||||||
|
//
|
||||||
|
// When you do that, you can use FakeClock's Add and Set methods to
|
||||||
|
// control how time behaves in your code making them more reliable
|
||||||
|
// while also expanding the space of problems you can test.
|
||||||
|
//
|
||||||
|
// This code intentionally does not attempt to provide an abstraction
|
||||||
|
// over time.Ticker and time.Timer because Go does not have the
|
||||||
|
// runtime or API hooks available to do reliably. See
|
||||||
|
// https://github.com/golang/go/issues/8869
|
||||||
//
|
//
|
||||||
// Be sure to test Time equality with time.Time#Equal, not ==.
|
// Be sure to test Time equality with time.Time#Equal, not ==.
|
||||||
package clock
|
package clock
|
||||||
|
|
@ -29,6 +41,7 @@ type Clock interface {
|
||||||
// Now returns the Clock's current view of the time. Mutating the
|
// Now returns the Clock's current view of the time. Mutating the
|
||||||
// returned Time will not mutate the clock's time.
|
// returned Time will not mutate the clock's time.
|
||||||
Now() time.Time
|
Now() time.Time
|
||||||
|
Sleep(time.Duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
type sysClock struct{}
|
type sysClock struct{}
|
||||||
|
|
@ -37,6 +50,10 @@ func (s sysClock) Now() time.Time {
|
||||||
return time.Now()
|
return time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s sysClock) Sleep(d time.Duration) {
|
||||||
|
time.Sleep(d)
|
||||||
|
}
|
||||||
|
|
||||||
// NewFake returns a FakeClock to be used in tests that need to
|
// NewFake returns a FakeClock to be used in tests that need to
|
||||||
// manipulate time. Its initial value is always the unix epoch in the
|
// manipulate time. Its initial value is always the unix epoch in the
|
||||||
// UTC timezone. The FakeClock returned is thread-safe.
|
// UTC timezone. The FakeClock returned is thread-safe.
|
||||||
|
|
@ -54,6 +71,9 @@ type FakeClock interface {
|
||||||
Clock
|
Clock
|
||||||
// Adjust the time that will be returned by Now.
|
// Adjust the time that will be returned by Now.
|
||||||
Add(d time.Duration)
|
Add(d time.Duration)
|
||||||
|
|
||||||
|
// Set the Clock's time to exactly the time given.
|
||||||
|
Set(t time.Time)
|
||||||
}
|
}
|
||||||
|
|
||||||
// To prevent mistakes with the API, we hide this behind NewFake. It's
|
// To prevent mistakes with the API, we hide this behind NewFake. It's
|
||||||
|
|
@ -71,8 +91,22 @@ func (f *fake) Now() time.Time {
|
||||||
return f.t
|
return f.t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fake) Sleep(d time.Duration) {
|
||||||
|
if d < 0 {
|
||||||
|
// time.Sleep just returns immediately. Do the same.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.Add(d)
|
||||||
|
}
|
||||||
|
|
||||||
func (f *fake) Add(d time.Duration) {
|
func (f *fake) Add(d time.Duration) {
|
||||||
f.Lock()
|
f.Lock()
|
||||||
defer f.Unlock()
|
defer f.Unlock()
|
||||||
f.t = f.t.Add(d)
|
f.t = f.t.Add(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fake) Set(t time.Time) {
|
||||||
|
f.Lock()
|
||||||
|
defer f.Unlock()
|
||||||
|
f.t = t
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import (
|
||||||
func TestFakeClockGoldenPath(t *testing.T) {
|
func TestFakeClockGoldenPath(t *testing.T) {
|
||||||
clk := NewFake()
|
clk := NewFake()
|
||||||
second := NewFake()
|
second := NewFake()
|
||||||
|
oldT := clk.Now()
|
||||||
|
|
||||||
if !clk.Now().Equal(second.Now()) {
|
if !clk.Now().Equal(second.Now()) {
|
||||||
t.Errorf("clocks must start out at the same time but didn't: %#v vs %#v", clk.Now(), second.Now())
|
t.Errorf("clocks must start out at the same time but didn't: %#v vs %#v", clk.Now(), second.Now())
|
||||||
}
|
}
|
||||||
|
|
@ -16,6 +18,27 @@ func TestFakeClockGoldenPath(t *testing.T) {
|
||||||
if clk.Now().Equal(second.Now()) {
|
if clk.Now().Equal(second.Now()) {
|
||||||
t.Errorf("clocks different must differ: %#v vs %#v", clk.Now(), second.Now())
|
t.Errorf("clocks different must differ: %#v vs %#v", clk.Now(), second.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clk.Set(oldT)
|
||||||
|
if !clk.Now().Equal(second.Now()) {
|
||||||
|
t.Errorf("clk should have been been set backwards: %#v vs %#v", clk.Now(), second.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
clk.Sleep(time.Second)
|
||||||
|
if clk.Now().Equal(second.Now()) {
|
||||||
|
t.Errorf("clk should have been set forwards (by sleeping): %#v vs %#v", clk.Now(), second.Now())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNegativeSleep(t *testing.T) {
|
||||||
|
clk := NewFake()
|
||||||
|
clk.Add(1 * time.Hour)
|
||||||
|
first := clk.Now()
|
||||||
|
clk.Sleep(-10 * time.Second)
|
||||||
|
if !clk.Now().Equal(first) {
|
||||||
|
t.Errorf("clk should not move in time on a negative sleep")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleClock() {
|
func ExampleClock() {
|
||||||
|
|
|
||||||
29
Makefile
29
Makefile
|
|
@ -9,8 +9,12 @@ VERSION ?= 1.0.0
|
||||||
EPOCH ?= 1
|
EPOCH ?= 1
|
||||||
MAINTAINER ?= "Community"
|
MAINTAINER ?= "Community"
|
||||||
|
|
||||||
CMD_OBJECTS = $(shell find ./cmd -maxdepth 1 -mindepth 1 -type d -exec basename '{}' \;)
|
CMDS = $(shell find ./cmd -maxdepth 1 -mindepth 1 -type d)
|
||||||
OBJECTS = $(CMD_OBJECTS) pkcs11bench
|
CMD_BASENAMES = $(shell echo $(CMDS) | xargs -n1 basename)
|
||||||
|
CMD_BINS = $(addprefix $(OBJDIR)/, $(CMD_BASENAMES) )
|
||||||
|
OBJECTS = $(CMD_BINS) $(OBJDIR)/pkcs11bench
|
||||||
|
|
||||||
|
GO_BUILD_FLAGS =
|
||||||
|
|
||||||
# Build environment variables (referencing core/util.go)
|
# Build environment variables (referencing core/util.go)
|
||||||
COMMIT_ID = $(shell git rev-parse --short HEAD)
|
COMMIT_ID = $(shell git rev-parse --short HEAD)
|
||||||
|
|
@ -29,16 +33,15 @@ all: build
|
||||||
|
|
||||||
build: $(OBJECTS)
|
build: $(OBJECTS)
|
||||||
|
|
||||||
pre:
|
$(OBJDIR):
|
||||||
@mkdir -p $(OBJDIR)
|
@mkdir -p $(OBJDIR)
|
||||||
|
|
||||||
# Compile each of the binaries
|
$(CMD_BINS): build_cmds
|
||||||
$(CMD_OBJECTS): pre
|
|
||||||
@echo [go] bin/$@
|
build_cmds: | $(OBJDIR)
|
||||||
@go build -o ./bin/$@ -ldflags \
|
GOBIN=$(OBJDIR) go install $(GO_BUILD_FLAGS) -ldflags \
|
||||||
"-X \"$(BUILD_ID_VAR)=$(BUILD_ID)\" -X \"$(BUILD_TIME_VAR)=$(BUILD_TIME)\" \
|
"-X \"$(BUILD_ID_VAR)=$(BUILD_ID)\" -X \"$(BUILD_TIME_VAR)=$(BUILD_TIME)\" \
|
||||||
-X \"$(BUILD_HOST_VAR)=$(BUILD_HOST)\"" \
|
-X \"$(BUILD_HOST_VAR)=$(BUILD_HOST)\"" ./...
|
||||||
./cmd/$@/
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f $(OBJDIR)/*
|
rm -f $(OBJDIR)/*
|
||||||
|
|
@ -64,7 +67,7 @@ archive:
|
||||||
# Version and Epoch, such as:
|
# Version and Epoch, such as:
|
||||||
#
|
#
|
||||||
# VERSION=0.1.9 EPOCH=52 MAINTAINER="$(whoami)" ARCHIVEDIR=/tmp make build rpm
|
# VERSION=0.1.9 EPOCH=52 MAINTAINER="$(whoami)" ARCHIVEDIR=/tmp make build rpm
|
||||||
rpm:
|
rpm: build
|
||||||
fpm -s dir -t rpm --rpm-digest sha256 --name "boulder" \
|
fpm -s dir -t rpm --rpm-digest sha256 --name "boulder" \
|
||||||
--license "Mozilla Public License v2.0" --vendor "ISRG" \
|
--license "Mozilla Public License v2.0" --vendor "ISRG" \
|
||||||
--url "https://github.com/letsencrypt/boulder" --prefix=/opt/boulder \
|
--url "https://github.com/letsencrypt/boulder" --prefix=/opt/boulder \
|
||||||
|
|
@ -72,7 +75,7 @@ rpm:
|
||||||
--package $(ARCHIVEDIR)/boulder-$(VERSION)-$(COMMIT_ID).x86_64.rpm \
|
--package $(ARCHIVEDIR)/boulder-$(VERSION)-$(COMMIT_ID).x86_64.rpm \
|
||||||
--description "Boulder is an ACME-compatible X.509 Certificate Authority" \
|
--description "Boulder is an ACME-compatible X.509 Certificate Authority" \
|
||||||
--depends "libtool-ltdl" --maintainer "$(MAINTAINER)" \
|
--depends "libtool-ltdl" --maintainer "$(MAINTAINER)" \
|
||||||
test/boulder-config.json sa/_db ca/_db $(foreach var,$(OBJECTS), $(OBJDIR)/$(var))
|
test/boulder-config.json sa/_db $(OBJECTS)
|
||||||
|
|
||||||
pkcs11bench: pre
|
$(OBJDIR)/pkcs11bench: ./Godeps/_workspace/src/github.com/cloudflare/cfssl/crypto/pkcs11key/*.go | $(OBJDIR)
|
||||||
go test -o ./bin/pkcs11bench -c ./Godeps/_workspace/src/github.com/cloudflare/cfssl/crypto/pkcs11key/
|
go test -o $(OBJDIR)/pkcs11bench -c ./Godeps/_workspace/src/github.com/cloudflare/cfssl/crypto/pkcs11key/
|
||||||
|
|
|
||||||
|
|
@ -376,34 +376,12 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest
|
||||||
return emptyCert, err
|
return emptyCert, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to generate the OCSP Response now. If this raises an error, it is
|
// Submit the certificate to any configured CT logs
|
||||||
// logged but is not returned to the caller, as an error at this point does
|
|
||||||
// not constitute an issuance failure.
|
|
||||||
certObj, err := x509.ParseCertificate(certDER)
|
certObj, err := x509.ParseCertificate(certDER)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ca.log.Warning(fmt.Sprintf("Post-Issuance OCSP failed parsing Certificate: %s", err))
|
ca.log.Warning(fmt.Sprintf("Post-Issuance OCSP failed parsing Certificate: %s", err))
|
||||||
return cert, nil
|
return cert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
serial := core.SerialToString(certObj.SerialNumber)
|
|
||||||
signRequest := ocsp.SignRequest{
|
|
||||||
Certificate: certObj,
|
|
||||||
Status: string(core.OCSPStatusGood),
|
|
||||||
}
|
|
||||||
|
|
||||||
ocspResponse, err := ca.OCSPSigner.Sign(signRequest)
|
|
||||||
if err != nil {
|
|
||||||
ca.log.Warning(fmt.Sprintf("Post-Issuance OCSP failed signing: %s", err))
|
|
||||||
return cert, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = ca.SA.UpdateOCSP(serial, ocspResponse)
|
|
||||||
if err != nil {
|
|
||||||
ca.log.Warning(fmt.Sprintf("Post-Issuance OCSP failed storing: %s", err))
|
|
||||||
return cert, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit the certificate to any configured CT logs
|
|
||||||
go ca.Publisher.SubmitToCT(certObj.Raw)
|
go ca.Publisher.SubmitToCT(certObj.Raw)
|
||||||
|
|
||||||
// Do not return an err at this point; caller must know that the Certificate
|
// Do not return an err at this point; caller must know that the Certificate
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
|
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
|
||||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/codegangsta/cli"
|
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/codegangsta/cli"
|
||||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/facebookgo/httpdown"
|
"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/github.com/streadway/amqp"
|
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/streadway/amqp"
|
||||||
|
|
||||||
"github.com/letsencrypt/boulder/cmd"
|
"github.com/letsencrypt/boulder/cmd"
|
||||||
|
|
@ -125,14 +126,14 @@ func main() {
|
||||||
|
|
||||||
auditlogger.Info(fmt.Sprintf("Server running, listening on %s...\n", c.WFE.ListenAddress))
|
auditlogger.Info(fmt.Sprintf("Server running, listening on %s...\n", c.WFE.ListenAddress))
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: c.WFE.ListenAddress,
|
Addr: c.WFE.ListenAddress,
|
||||||
ConnState: httpMonitor.ConnectionMonitor,
|
Handler: httpMonitor.Handle(),
|
||||||
Handler: httpMonitor.Handle(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hd := &httpdown.HTTP{
|
hd := &httpdown.HTTP{
|
||||||
StopTimeout: wfe.ShutdownStopTimeout,
|
StopTimeout: wfe.ShutdownStopTimeout,
|
||||||
KillTimeout: wfe.ShutdownKillTimeout,
|
KillTimeout: wfe.ShutdownKillTimeout,
|
||||||
|
Stats: metrics.NewFBAdapter(stats, "WFE", clock.Default()),
|
||||||
}
|
}
|
||||||
err = httpdown.ListenAndServe(srv, hd)
|
err = httpdown.ListenAndServe(srv, hd)
|
||||||
cmd.FailOnError(err, "Error starting HTTP server")
|
cmd.FailOnError(err, "Error starting HTTP server")
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ func (f fakeRegStore) GetRegistration(id int64) (core.Registration, error) {
|
||||||
r, ok := f.RegById[id]
|
r, ok := f.RegById[id]
|
||||||
if !ok {
|
if !ok {
|
||||||
msg := fmt.Sprintf("no such registration %d", id)
|
msg := fmt.Sprintf("no such registration %d", id)
|
||||||
return r, sa.NoSuchRegistrationError{Msg: msg}
|
return r, core.NoSuchRegistrationError(msg)
|
||||||
}
|
}
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
|
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
|
||||||
cfocsp "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/ocsp"
|
cfocsp "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/ocsp"
|
||||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/facebookgo/httpdown"
|
"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"
|
"github.com/letsencrypt/boulder/Godeps/_workspace/src/golang.org/x/crypto/ocsp"
|
||||||
gorp "github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
|
gorp "github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
|
||||||
"github.com/letsencrypt/boulder/metrics"
|
"github.com/letsencrypt/boulder/metrics"
|
||||||
|
|
@ -172,14 +173,14 @@ func main() {
|
||||||
|
|
||||||
httpMonitor := metrics.NewHTTPMonitor(stats, m, "OCSP")
|
httpMonitor := metrics.NewHTTPMonitor(stats, m, "OCSP")
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: c.OCSPResponder.ListenAddress,
|
Addr: c.OCSPResponder.ListenAddress,
|
||||||
ConnState: httpMonitor.ConnectionMonitor,
|
Handler: httpMonitor.Handle(),
|
||||||
Handler: httpMonitor.Handle(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hd := &httpdown.HTTP{
|
hd := &httpdown.HTTP{
|
||||||
StopTimeout: stopTimeout,
|
StopTimeout: stopTimeout,
|
||||||
KillTimeout: killTimeout,
|
KillTimeout: killTimeout,
|
||||||
|
Stats: metrics.NewFBAdapter(stats, "OCSP", clock.Default()),
|
||||||
}
|
}
|
||||||
err = httpdown.ListenAndServe(srv, hd)
|
err = httpdown.ListenAndServe(srv, hd)
|
||||||
cmd.FailOnError(err, "Error starting HTTP server")
|
cmd.FailOnError(err, "Error starting HTTP server")
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,11 @@ import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
|
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
|
||||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/codegangsta/cli"
|
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/jmhodges/clock"
|
||||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/streadway/amqp"
|
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/streadway/amqp"
|
||||||
gorp "github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
|
gorp "github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
|
||||||
|
|
||||||
|
|
@ -23,20 +24,70 @@ import (
|
||||||
"github.com/letsencrypt/boulder/sa"
|
"github.com/letsencrypt/boulder/sa"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FatalError indicates the updater should stop execution
|
|
||||||
type FatalError string
|
|
||||||
|
|
||||||
func (e FatalError) Error() string { return string(e) }
|
|
||||||
|
|
||||||
// OCSPUpdater contains the useful objects for the Updater
|
// OCSPUpdater contains the useful objects for the Updater
|
||||||
type OCSPUpdater struct {
|
type OCSPUpdater struct {
|
||||||
stats statsd.Statter
|
stats statsd.Statter
|
||||||
log *blog.AuditLogger
|
log *blog.AuditLogger
|
||||||
cac rpc.CertificateAuthorityClient
|
clk clock.Clock
|
||||||
|
|
||||||
dbMap *gorp.DbMap
|
dbMap *gorp.DbMap
|
||||||
|
|
||||||
|
cac core.CertificateAuthority
|
||||||
|
|
||||||
|
// Bits various loops need but don't really fit in the looper struct
|
||||||
|
ocspMinTimeToExpiry time.Duration
|
||||||
|
|
||||||
|
newCertificatesLoop *looper
|
||||||
|
oldOCSPResponsesLoop *looper
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupClients(c cmd.Config, stats statsd.Statter) (rpc.CertificateAuthorityClient, chan *amqp.Error) {
|
// This is somewhat gross but can be pared down a bit once the publisher and this
|
||||||
|
// are fully smooshed together
|
||||||
|
func newUpdater(
|
||||||
|
log *blog.AuditLogger,
|
||||||
|
stats statsd.Statter,
|
||||||
|
clk clock.Clock,
|
||||||
|
dbMap *gorp.DbMap,
|
||||||
|
ca core.CertificateAuthority,
|
||||||
|
config cmd.OCSPUpdaterConfig,
|
||||||
|
) (*OCSPUpdater, error) {
|
||||||
|
if config.NewCertificateBatchSize == 0 ||
|
||||||
|
config.OldOCSPBatchSize == 0 {
|
||||||
|
return nil, fmt.Errorf("Batch sizes must be non-zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
updater := OCSPUpdater{
|
||||||
|
stats: stats,
|
||||||
|
log: log,
|
||||||
|
clk: clk,
|
||||||
|
dbMap: dbMap,
|
||||||
|
cac: ca,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup loops
|
||||||
|
updater.newCertificatesLoop = &looper{
|
||||||
|
clk: clk,
|
||||||
|
stats: stats,
|
||||||
|
batchSize: config.NewCertificateBatchSize,
|
||||||
|
tickDur: config.NewCertificateWindow.Duration,
|
||||||
|
tickFunc: updater.newCertificateTick,
|
||||||
|
name: "NewCertificates",
|
||||||
|
}
|
||||||
|
updater.oldOCSPResponsesLoop = &looper{
|
||||||
|
clk: clk,
|
||||||
|
stats: stats,
|
||||||
|
batchSize: config.OldOCSPBatchSize,
|
||||||
|
tickDur: config.OldOCSPWindow.Duration,
|
||||||
|
tickFunc: updater.oldOCSPResponsesTick,
|
||||||
|
name: "OldOCSPResponses",
|
||||||
|
}
|
||||||
|
|
||||||
|
updater.ocspMinTimeToExpiry = config.OCSPMinTimeToExpiry.Duration
|
||||||
|
|
||||||
|
return &updater, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupClients(c cmd.Config, stats statsd.Statter) (core.CertificateAuthority, chan *amqp.Error) {
|
||||||
ch, err := rpc.AmqpChannel(c)
|
ch, err := rpc.AmqpChannel(c)
|
||||||
cmd.FailOnError(err, "Could not connect to AMQP")
|
cmd.FailOnError(err, "Could not connect to AMQP")
|
||||||
|
|
||||||
|
|
@ -51,28 +102,65 @@ func setupClients(c cmd.Config, stats statsd.Statter) (rpc.CertificateAuthorityC
|
||||||
return cac, closeChan
|
return cac, closeChan
|
||||||
}
|
}
|
||||||
|
|
||||||
func (updater *OCSPUpdater) processResponse(tx *gorp.Transaction, serial string) error {
|
func (updater *OCSPUpdater) findStaleOCSPResponses(oldestLastUpdatedTime time.Time, batchSize int) ([]core.CertificateStatus, error) {
|
||||||
certObj, err := tx.Get(core.Certificate{}, serial)
|
var statuses []core.CertificateStatus
|
||||||
if err != nil {
|
_, err := updater.dbMap.Select(
|
||||||
return err
|
&statuses,
|
||||||
}
|
`SELECT cs.*
|
||||||
statusObj, err := tx.Get(core.CertificateStatus{}, serial)
|
FROM certificateStatus AS cs
|
||||||
if err != nil {
|
JOIN certificates AS cert
|
||||||
return err
|
ON cs.serial = cert.serial
|
||||||
|
WHERE cs.ocspLastUpdated < :lastUpdate
|
||||||
|
AND cert.expires > now()
|
||||||
|
ORDER BY cs.ocspLastUpdated ASC
|
||||||
|
LIMIT :limit`,
|
||||||
|
map[string]interface{}{
|
||||||
|
"lastUpdate": oldestLastUpdatedTime,
|
||||||
|
"limit": batchSize,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return statuses, nil
|
||||||
}
|
}
|
||||||
|
return statuses, err
|
||||||
|
}
|
||||||
|
|
||||||
cert, ok := certObj.(*core.Certificate)
|
func (updater *OCSPUpdater) getCertificatesWithMissingResponses(batchSize int) ([]core.CertificateStatus, error) {
|
||||||
if !ok {
|
var statuses []core.CertificateStatus
|
||||||
return fmt.Errorf("Cast failure")
|
_, err := updater.dbMap.Select(
|
||||||
|
&statuses,
|
||||||
|
`SELECT * FROM certificateStatus
|
||||||
|
WHERE ocspLastUpdated = 0
|
||||||
|
LIMIT :limit`,
|
||||||
|
map[string]interface{}{
|
||||||
|
"limit": batchSize,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return statuses, nil
|
||||||
}
|
}
|
||||||
status, ok := statusObj.(*core.CertificateStatus)
|
return statuses, err
|
||||||
if !ok {
|
}
|
||||||
return fmt.Errorf("Cast failure")
|
|
||||||
|
type responseMeta struct {
|
||||||
|
*core.OCSPResponse
|
||||||
|
*core.CertificateStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (updater *OCSPUpdater) generateResponse(status core.CertificateStatus) (responseMeta, error) {
|
||||||
|
var cert core.Certificate
|
||||||
|
err := updater.dbMap.SelectOne(
|
||||||
|
&cert,
|
||||||
|
"SELECT * FROM certificates WHERE serial = :serial",
|
||||||
|
map[string]interface{}{"serial": status.Serial},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return responseMeta{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = x509.ParseCertificate(cert.DER)
|
_, err = x509.ParseCertificate(cert.DER)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return responseMeta{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
signRequest := core.OCSPSigningRequest{
|
signRequest := core.OCSPSigningRequest{
|
||||||
|
|
@ -84,21 +172,28 @@ func (updater *OCSPUpdater) processResponse(tx *gorp.Transaction, serial string)
|
||||||
|
|
||||||
ocspResponse, err := updater.cac.GenerateOCSP(signRequest)
|
ocspResponse, err := updater.cac.GenerateOCSP(signRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return responseMeta{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
timeStamp := time.Now()
|
timestamp := updater.clk.Now()
|
||||||
|
status.OCSPLastUpdated = timestamp
|
||||||
|
ocspResp := &core.OCSPResponse{
|
||||||
|
Serial: cert.Serial,
|
||||||
|
CreatedAt: timestamp,
|
||||||
|
Response: ocspResponse,
|
||||||
|
}
|
||||||
|
return responseMeta{ocspResp, &status}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (updater *OCSPUpdater) storeResponse(tx *gorp.Transaction, meta responseMeta) error {
|
||||||
// Record the response.
|
// Record the response.
|
||||||
ocspResp := &core.OCSPResponse{Serial: serial, CreatedAt: timeStamp, Response: ocspResponse}
|
err := tx.Insert(meta.OCSPResponse)
|
||||||
err = tx.Insert(ocspResp)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the update clock
|
// Reset the update clock
|
||||||
status.OCSPLastUpdated = timeStamp
|
_, err = tx.Update(meta.CertificateStatus)
|
||||||
_, err = tx.Update(status)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -107,97 +202,101 @@ func (updater *OCSPUpdater) processResponse(tx *gorp.Transaction, serial string)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Produce one OCSP response for the given serial, returning err
|
// newCertificateTick checks for certificates issued since the last tick and
|
||||||
// if anything went wrong. This method will open and commit a transaction.
|
// generates and stores OCSP responses for these certs
|
||||||
func (updater *OCSPUpdater) updateOneSerial(serial string) error {
|
func (updater *OCSPUpdater) newCertificateTick(batchSize int) {
|
||||||
innerStart := time.Now()
|
// Check for anything issued between now and previous tick and generate first
|
||||||
// Each response gets a transaction. In the future we can increase
|
// OCSP responses
|
||||||
// performance by batching transactions.
|
statuses, err := updater.getCertificatesWithMissingResponses(batchSize)
|
||||||
// The key thing to think through is the cost of rollbacks, and whether
|
|
||||||
// we should rollback if CA/HSM fails to sign the response or only
|
|
||||||
// upon a partial DB insert.
|
|
||||||
tx, err := updater.dbMap.Begin()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
updater.log.Err(fmt.Sprintf("OCSP %s: Error starting transaction, aborting: %s", serial, err))
|
return
|
||||||
updater.stats.Inc("OCSP.Updates.Failed", 1, 1.0)
|
|
||||||
tx.Rollback()
|
|
||||||
// Failure to begin transaction is a fatal error.
|
|
||||||
return FatalError(err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := updater.processResponse(tx, serial); err != nil {
|
updater.generateOCSPResponses(statuses)
|
||||||
updater.log.Err(fmt.Sprintf("OCSP %s: Could not process OCSP Response, skipping: %s", serial, err))
|
|
||||||
updater.stats.Inc("OCSP.Updates.Failed", 1, 1.0)
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tx.Commit()
|
|
||||||
if err != nil {
|
|
||||||
updater.log.Err(fmt.Sprintf("OCSP %s: Error committing transaction, skipping: %s", serial, err))
|
|
||||||
updater.stats.Inc("OCSP.Updates.Failed", 1, 1.0)
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
updater.log.Info(fmt.Sprintf("OCSP %s: OK", serial))
|
|
||||||
updater.stats.Inc("OCSP.Updates.Processed", 1, 1.0)
|
|
||||||
updater.stats.TimingDuration("OCSP.Updates.UpdateLatency", time.Since(innerStart), 1.0)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// findStaleResponses opens a transaction and processes up to responseLimit
|
func (updater *OCSPUpdater) generateOCSPResponses(statuses []core.CertificateStatus) {
|
||||||
// responses in a single batch. The responseLimit should be relatively small,
|
responses := []responseMeta{}
|
||||||
// so as to limit the chance of the transaction failing due to concurrent
|
for _, status := range statuses {
|
||||||
// updates.
|
meta, err := updater.generateResponse(status)
|
||||||
func (updater *OCSPUpdater) findStaleResponses(oldestLastUpdatedTime time.Time, responseLimit int) error {
|
if err != nil {
|
||||||
var certificateStatus []core.CertificateStatus
|
updater.log.AuditErr(fmt.Errorf("Failed to generate OCSP response: %s", err))
|
||||||
_, err := updater.dbMap.Select(&certificateStatus,
|
updater.stats.Inc("OCSP.Errors.ResponseGeneration", 1, 1.0)
|
||||||
`SELECT cs.* FROM certificateStatus AS cs JOIN certificates AS cert ON cs.serial = cert.serial
|
continue
|
||||||
WHERE cs.ocspLastUpdated < ? AND cert.expires > now()
|
|
||||||
ORDER BY cs.ocspLastUpdated ASC
|
|
||||||
LIMIT ?`, oldestLastUpdatedTime, responseLimit)
|
|
||||||
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
updater.log.Info("All up to date. No OCSP responses needed.")
|
|
||||||
} else if err != nil {
|
|
||||||
updater.log.Err(fmt.Sprintf("Error loading certificate status: %s", err))
|
|
||||||
} else {
|
|
||||||
updater.log.Info(fmt.Sprintf("Processing OCSP Responses...\n"))
|
|
||||||
outerStart := time.Now()
|
|
||||||
|
|
||||||
for i, status := range certificateStatus {
|
|
||||||
updater.log.Debug(fmt.Sprintf("OCSP %s: (%d/%d)", status.Serial, i, responseLimit))
|
|
||||||
|
|
||||||
err = updater.updateOneSerial(status.Serial)
|
|
||||||
// Abort if we recieve a fatal error
|
|
||||||
if _, ok := err.(FatalError); ok {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
responses = append(responses, meta)
|
||||||
updater.stats.TimingDuration("OCSP.Updates.BatchLatency", time.Since(outerStart), 1.0)
|
updater.stats.Inc("OCSP.GeneratedResponses", 1, 1.0)
|
||||||
updater.stats.Inc("OCSP.Updates.BatchesProcessed", 1, 1.0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
tx, err := updater.dbMap.Begin()
|
||||||
|
if err != nil {
|
||||||
|
updater.log.AuditErr(fmt.Errorf("Failed to open OCSP response transaction: %s", err))
|
||||||
|
updater.stats.Inc("OCSP.Errors.OpenTransaction", 1, 1.0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, meta := range responses {
|
||||||
|
err = updater.storeResponse(tx, 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)
|
||||||
|
tx.Rollback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
updater.log.AuditErr(fmt.Errorf("Failed to commit OCSP response transaction: %s", err))
|
||||||
|
updater.stats.Inc("OCSP.Errors.CommitTransaction", 1, 1.0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updater.stats.Inc("OCSP.StoredResponses", int64(len(responses)), 1.0)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// oldOCSPResponsesTick looks for certificates with stale OCSP responses and
|
||||||
|
// generates/stores new ones
|
||||||
|
func (updater *OCSPUpdater) oldOCSPResponsesTick(batchSize int) {
|
||||||
|
now := time.Now()
|
||||||
|
statuses, err := updater.findStaleOCSPResponses(now.Add(-updater.ocspMinTimeToExpiry), batchSize)
|
||||||
|
if err != nil {
|
||||||
|
updater.stats.Inc("OCSP.Errors.FindStaleResponses", 1, 1.0)
|
||||||
|
updater.log.AuditErr(fmt.Errorf("Failed to find stale OCSP responses: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updater.generateOCSPResponses(statuses)
|
||||||
|
}
|
||||||
|
|
||||||
|
type looper struct {
|
||||||
|
clk clock.Clock
|
||||||
|
stats statsd.Statter
|
||||||
|
batchSize int
|
||||||
|
tickDur time.Duration
|
||||||
|
tickFunc func(int)
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *looper) loop() {
|
||||||
|
for {
|
||||||
|
tickStart := l.clk.Now()
|
||||||
|
l.tickFunc(l.batchSize)
|
||||||
|
l.stats.TimingDuration(fmt.Sprintf("OCSP.%s.TickDuration", l.name), time.Since(tickStart), 1.0)
|
||||||
|
l.stats.Inc(fmt.Sprintf("OCSP.%s.Ticks", l.name), 1, 1.0)
|
||||||
|
tickEnd := tickStart.Add(time.Since(tickStart))
|
||||||
|
expectedTickEnd := tickStart.Add(l.tickDur)
|
||||||
|
if tickEnd.After(expectedTickEnd) {
|
||||||
|
l.stats.Inc(fmt.Sprintf("OCSP.%s.LongTicks", l.name), 1, 1.0)
|
||||||
|
}
|
||||||
|
// Sleep for the remaining tick period (if this is a negative number sleep
|
||||||
|
// will not do anything and carry on)
|
||||||
|
l.clk.Sleep(expectedTickEnd.Sub(tickEnd))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := cmd.NewAppShell("ocsp-updater", "Generates and updates OCSP responses")
|
app := cmd.NewAppShell("ocsp-updater", "Generates and updates OCSP responses")
|
||||||
|
|
||||||
app.App.Flags = append(app.App.Flags, cli.IntFlag{
|
|
||||||
Name: "limit",
|
|
||||||
Value: 100,
|
|
||||||
EnvVar: "OCSP_LIMIT",
|
|
||||||
Usage: "Count of responses to process per run",
|
|
||||||
})
|
|
||||||
|
|
||||||
app.Config = func(c *cli.Context, config cmd.Config) cmd.Config {
|
|
||||||
config.OCSPUpdater.ResponseLimit = c.GlobalInt("limit")
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
app.Action = func(c cmd.Config) {
|
app.Action = func(c cmd.Config) {
|
||||||
// Set up logging
|
// Set up logging
|
||||||
stats, err := statsd.NewClient(c.Statsd.Server, c.Statsd.Prefix)
|
stats, err := statsd.NewClient(c.Statsd.Server, c.Statsd.Prefix)
|
||||||
|
|
@ -207,12 +306,13 @@ func main() {
|
||||||
cmd.FailOnError(err, "Could not connect to Syslog")
|
cmd.FailOnError(err, "Could not connect to Syslog")
|
||||||
auditlogger.Info(app.VersionString())
|
auditlogger.Info(app.VersionString())
|
||||||
|
|
||||||
|
blog.SetAuditLogger(auditlogger)
|
||||||
|
|
||||||
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
|
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
|
||||||
defer auditlogger.AuditPanic()
|
defer auditlogger.AuditPanic()
|
||||||
|
|
||||||
blog.SetAuditLogger(auditlogger)
|
|
||||||
|
|
||||||
go cmd.DebugServer(c.OCSPUpdater.DebugAddr)
|
go cmd.DebugServer(c.OCSPUpdater.DebugAddr)
|
||||||
|
go cmd.ProfileCmd("OCSP-Updater", stats)
|
||||||
|
|
||||||
// Configure DB
|
// Configure DB
|
||||||
dbMap, err := sa.NewDbMap(c.OCSPUpdater.DBConnect)
|
dbMap, err := sa.NewDbMap(c.OCSPUpdater.DBConnect)
|
||||||
|
|
@ -220,40 +320,28 @@ func main() {
|
||||||
|
|
||||||
cac, closeChan := setupClients(c, stats)
|
cac, closeChan := setupClients(c, stats)
|
||||||
|
|
||||||
go func() {
|
updater, err := newUpdater(
|
||||||
// Abort if we disconnect from AMQP
|
auditlogger,
|
||||||
for {
|
stats,
|
||||||
for err := range closeChan {
|
clock.Default(),
|
||||||
auditlogger.Warning(fmt.Sprintf(" [!] AMQP Channel closed, aborting early: [%s]", err))
|
dbMap,
|
||||||
panic(err)
|
cac,
|
||||||
}
|
// Necessary evil for now
|
||||||
}
|
c.OCSPUpdater,
|
||||||
}()
|
)
|
||||||
|
|
||||||
updater := &OCSPUpdater{
|
go updater.newCertificatesLoop.loop()
|
||||||
cac: cac,
|
go updater.oldOCSPResponsesLoop.loop()
|
||||||
dbMap: dbMap,
|
|
||||||
stats: stats,
|
|
||||||
log: auditlogger,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the cut-off timestamp
|
cmd.FailOnError(err, "Failed to create updater")
|
||||||
if c.OCSPUpdater.MinTimeToExpiry == "" {
|
|
||||||
panic("Config must specify a MinTimeToExpiry period.")
|
|
||||||
}
|
|
||||||
dur, err := time.ParseDuration(c.OCSPUpdater.MinTimeToExpiry)
|
|
||||||
cmd.FailOnError(err, "Could not parse MinTimeToExpiry from config.")
|
|
||||||
|
|
||||||
oldestLastUpdatedTime := time.Now().Add(-dur)
|
// TODO(): When the channel falls over so do we for now, if the AMQP channel
|
||||||
auditlogger.Info(fmt.Sprintf("Searching for OCSP responses older than %s", oldestLastUpdatedTime))
|
// has already closed there is no real cleanup we can do. This is due to
|
||||||
|
// really needing to change the underlying AMQP Server/Client reconnection
|
||||||
// When we choose to batch responses, it may be best to restrict count here,
|
// logic.
|
||||||
// change the transaction to survive the whole findStaleResponses, and to
|
err = <-closeChan
|
||||||
// loop this method call however many times is appropriate.
|
auditlogger.AuditErr(fmt.Errorf(" [!] AMQP Channel closed, exiting: [%s]", err))
|
||||||
err = updater.findStaleResponses(oldestLastUpdatedTime, c.OCSPUpdater.ResponseLimit)
|
os.Exit(1)
|
||||||
if err != nil {
|
|
||||||
auditlogger.WarningErr(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Run()
|
app.Run()
|
||||||
|
|
|
||||||
|
|
@ -1 +1,197 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
|
||||||
|
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/jmhodges/clock"
|
||||||
|
"github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
|
||||||
|
"github.com/letsencrypt/boulder/core"
|
||||||
|
|
||||||
|
"github.com/letsencrypt/boulder/sa"
|
||||||
|
"github.com/letsencrypt/boulder/sa/satest"
|
||||||
|
"github.com/letsencrypt/boulder/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockCA struct{}
|
||||||
|
|
||||||
|
func (ca *mockCA) IssueCertificate(csr x509.CertificateRequest, regID int64) (core.Certificate, error) {
|
||||||
|
return core.Certificate{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ca *mockCA) GenerateOCSP(xferObj core.OCSPSigningRequest) (ocsp []byte, err error) {
|
||||||
|
ocsp = []byte{1, 2, 3}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ca *mockCA) RevokeCertificate(serial string, reasonCode core.RevocationCode) (err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbConnStr = "mysql+tcp://boulder@localhost:3306/boulder_sa_test"
|
||||||
|
|
||||||
|
func setup(t *testing.T) (OCSPUpdater, core.StorageAuthority, *gorp.DbMap, clock.FakeClock, func()) {
|
||||||
|
dbMap, err := sa.NewDbMap(dbConnStr)
|
||||||
|
test.AssertNotError(t, err, "Failed to create dbMap")
|
||||||
|
|
||||||
|
fc := clock.NewFake()
|
||||||
|
fc.Add(1 * time.Hour)
|
||||||
|
|
||||||
|
sa, err := sa.NewSQLStorageAuthority(dbMap, fc)
|
||||||
|
test.AssertNotError(t, err, "Failed to create SA")
|
||||||
|
|
||||||
|
cleanUp := test.ResetTestDatabase(t, dbMap.Db)
|
||||||
|
|
||||||
|
stats, _ := statsd.NewNoopClient(nil)
|
||||||
|
|
||||||
|
updater := OCSPUpdater{
|
||||||
|
dbMap: dbMap,
|
||||||
|
clk: fc,
|
||||||
|
cac: &mockCA{},
|
||||||
|
stats: stats,
|
||||||
|
}
|
||||||
|
|
||||||
|
return updater, sa, dbMap, fc, cleanUp
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateAndStoreOCSPResponse(t *testing.T) {
|
||||||
|
updater, sa, dbMap, _, cleanUp := setup(t)
|
||||||
|
defer cleanUp()
|
||||||
|
|
||||||
|
reg := satest.CreateWorkingRegistration(t, sa)
|
||||||
|
parsedCert, err := core.LoadCert("test-cert.pem")
|
||||||
|
test.AssertNotError(t, err, "Couldn't read test certificate")
|
||||||
|
_, err = sa.AddCertificate(parsedCert.Raw, reg.ID)
|
||||||
|
test.AssertNotError(t, err, "Couldn't add www.eff.org.der")
|
||||||
|
|
||||||
|
status, err := sa.GetCertificateStatus(core.SerialToString(parsedCert.SerialNumber))
|
||||||
|
test.AssertNotError(t, err, "Couldn't get the core.CertificateStatus from the database")
|
||||||
|
|
||||||
|
meta, err := updater.generateResponse(status)
|
||||||
|
test.AssertNotError(t, err, "Couldn't generate OCSP response")
|
||||||
|
tx, err := dbMap.Begin()
|
||||||
|
test.AssertNotError(t, err, "Couldn't open a transaction")
|
||||||
|
err = updater.storeResponse(tx, meta)
|
||||||
|
test.AssertNotError(t, err, "Couldn't store OCSP response")
|
||||||
|
err = tx.Commit()
|
||||||
|
test.AssertNotError(t, err, "Couldn't close transaction")
|
||||||
|
|
||||||
|
var ocspResponse core.OCSPResponse
|
||||||
|
err = dbMap.SelectOne(
|
||||||
|
&ocspResponse,
|
||||||
|
"SELECT * from ocspResponses WHERE serial = :serial ORDER BY id DESC LIMIT 1;",
|
||||||
|
map[string]interface{}{"serial": status.Serial},
|
||||||
|
)
|
||||||
|
test.AssertNotError(t, err, "Couldn't get OCSP response from database")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateOCSPResponses(t *testing.T) {
|
||||||
|
updater, sa, _, fc, cleanUp := setup(t)
|
||||||
|
defer cleanUp()
|
||||||
|
|
||||||
|
reg := satest.CreateWorkingRegistration(t, sa)
|
||||||
|
parsedCert, err := core.LoadCert("test-cert.pem")
|
||||||
|
test.AssertNotError(t, err, "Couldn't read test certificate")
|
||||||
|
_, err = sa.AddCertificate(parsedCert.Raw, reg.ID)
|
||||||
|
test.AssertNotError(t, err, "Couldn't add test-cert.pem")
|
||||||
|
parsedCert, err = core.LoadCert("test-cert-b.pem")
|
||||||
|
test.AssertNotError(t, err, "Couldn't read test certificate")
|
||||||
|
_, err = sa.AddCertificate(parsedCert.Raw, reg.ID)
|
||||||
|
test.AssertNotError(t, err, "Couldn't add test-cert-b.pem")
|
||||||
|
|
||||||
|
earliest := fc.Now().Add(-time.Hour)
|
||||||
|
certs, err := updater.findStaleOCSPResponses(earliest, 10)
|
||||||
|
test.AssertNotError(t, err, "Couldn't find stale responses")
|
||||||
|
test.AssertEquals(t, len(certs), 2)
|
||||||
|
|
||||||
|
updater.generateOCSPResponses(certs)
|
||||||
|
|
||||||
|
certs, err = updater.findStaleOCSPResponses(earliest, 10)
|
||||||
|
test.AssertNotError(t, err, "Failed to find stale responses")
|
||||||
|
test.AssertEquals(t, len(certs), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindStaleOCSPResponses(t *testing.T) {
|
||||||
|
updater, sa, dbMap, fc, cleanUp := setup(t)
|
||||||
|
defer cleanUp()
|
||||||
|
|
||||||
|
reg := satest.CreateWorkingRegistration(t, sa)
|
||||||
|
parsedCert, err := core.LoadCert("test-cert.pem")
|
||||||
|
test.AssertNotError(t, err, "Couldn't read test certificate")
|
||||||
|
_, err = sa.AddCertificate(parsedCert.Raw, reg.ID)
|
||||||
|
test.AssertNotError(t, err, "Couldn't add www.eff.org.der")
|
||||||
|
|
||||||
|
earliest := fc.Now().Add(-time.Hour)
|
||||||
|
certs, err := updater.findStaleOCSPResponses(earliest, 10)
|
||||||
|
test.AssertNotError(t, err, "Couldn't find certificate")
|
||||||
|
test.AssertEquals(t, len(certs), 1)
|
||||||
|
|
||||||
|
status, err := sa.GetCertificateStatus(core.SerialToString(parsedCert.SerialNumber))
|
||||||
|
test.AssertNotError(t, err, "Couldn't get the core.Certificate from the database")
|
||||||
|
|
||||||
|
meta, err := updater.generateResponse(status)
|
||||||
|
test.AssertNotError(t, err, "Couldn't generate OCSP response")
|
||||||
|
tx, err := dbMap.Begin()
|
||||||
|
test.AssertNotError(t, err, "Couldn't open a transaction")
|
||||||
|
err = updater.storeResponse(tx, meta)
|
||||||
|
test.AssertNotError(t, err, "Couldn't store OCSP response")
|
||||||
|
err = tx.Commit()
|
||||||
|
test.AssertNotError(t, err, "Couldn't close transaction")
|
||||||
|
|
||||||
|
certs, err = updater.findStaleOCSPResponses(earliest, 10)
|
||||||
|
test.AssertNotError(t, err, "Failed to find stale responses")
|
||||||
|
test.AssertEquals(t, len(certs), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCertificatesWithMissingResponses(t *testing.T) {
|
||||||
|
updater, sa, _, _, cleanUp := setup(t)
|
||||||
|
defer cleanUp()
|
||||||
|
|
||||||
|
reg := satest.CreateWorkingRegistration(t, sa)
|
||||||
|
cert, err := core.LoadCert("test-cert.pem")
|
||||||
|
test.AssertNotError(t, err, "Couldn't read test certificate")
|
||||||
|
_, err = sa.AddCertificate(cert.Raw, reg.ID)
|
||||||
|
test.AssertNotError(t, err, "Couldn't add www.eff.org.der")
|
||||||
|
|
||||||
|
statuses, err := updater.getCertificatesWithMissingResponses(10)
|
||||||
|
test.AssertNotError(t, err, "Couldn't get status")
|
||||||
|
test.AssertEquals(t, len(statuses), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewCertificateTick(t *testing.T) {
|
||||||
|
updater, sa, _, fc, cleanUp := setup(t)
|
||||||
|
defer cleanUp()
|
||||||
|
|
||||||
|
reg := satest.CreateWorkingRegistration(t, sa)
|
||||||
|
parsedCert, err := core.LoadCert("test-cert.pem")
|
||||||
|
test.AssertNotError(t, err, "Couldn't read test certificate")
|
||||||
|
_, err = sa.AddCertificate(parsedCert.Raw, reg.ID)
|
||||||
|
test.AssertNotError(t, err, "Couldn't add www.eff.org.der")
|
||||||
|
|
||||||
|
prev := fc.Now().Add(-time.Hour)
|
||||||
|
updater.newCertificateTick(10)
|
||||||
|
|
||||||
|
certs, err := updater.findStaleOCSPResponses(prev, 10)
|
||||||
|
test.AssertNotError(t, err, "Failed to find stale responses")
|
||||||
|
test.AssertEquals(t, len(certs), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOldOCSPResponsesTick(t *testing.T) {
|
||||||
|
updater, sa, _, fc, cleanUp := setup(t)
|
||||||
|
defer cleanUp()
|
||||||
|
|
||||||
|
reg := satest.CreateWorkingRegistration(t, sa)
|
||||||
|
parsedCert, err := core.LoadCert("test-cert.pem")
|
||||||
|
test.AssertNotError(t, err, "Couldn't read test certificate")
|
||||||
|
_, err = sa.AddCertificate(parsedCert.Raw, reg.ID)
|
||||||
|
test.AssertNotError(t, err, "Couldn't add www.eff.org.der")
|
||||||
|
|
||||||
|
updater.ocspMinTimeToExpiry = 1 * time.Hour
|
||||||
|
updater.oldOCSPResponsesTick(10)
|
||||||
|
|
||||||
|
certs, err := updater.findStaleOCSPResponses(fc.Now().Add(-updater.ocspMinTimeToExpiry), 10)
|
||||||
|
test.AssertNotError(t, err, "Failed to find stale responses")
|
||||||
|
test.AssertEquals(t, len(certs), 0)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIC6DCCAdCgAwIBAgICALIwDQYJKoZIhvcNAQELBQAwDjEMMAoGA1UEAwwDMTc4
|
||||||
|
MB4XDTE1MDYxMzAwMTY1OFoXDTE2MDYxMjAwMTY1OFowDjEMMAoGA1UEAwwDMTc4
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmqs7nue5oFxKBk2WaFZJ
|
||||||
|
Ama2nm1oFyPIq19gYEAdQN4mWvaJ8RjzHFkDMYUrlIrGxCYuFJDHFUk9dh19Na1M
|
||||||
|
IY+NVLgcSbyNcOML3bLbLEwGmvXPbbEOflBA9mxUS9TLMgXW5ghf/qbt4vmSGKlo
|
||||||
|
Iim41QXt55QFW6O+84s8Kd2OE6df0wTsEwLhZB3j5pDU+t7j5vTMv4Tc7EptaPkO
|
||||||
|
dfQn+68viUJjlYM/4yIBVRhWCdexFdylCKVLg0obsghQEwULKYCUjdg6F0VJUI11
|
||||||
|
5DU49tzscXU/3FS3CyY8rchunuYszBNkdmgpAwViHNWuP7ESdEd/emrj1xuioSe6
|
||||||
|
PwIDAQABo1AwTjAdBgNVHQ4EFgQUEaQm2CFKX3v9tNF3LLRNqd5mGcMwHwYDVR0j
|
||||||
|
BBgwFoAUEaQm2CFKX3v9tNF3LLRNqd5mGcMwDAYDVR0TBAUwAwEB/zANBgkqhkiG
|
||||||
|
9w0BAQsFAAOCAQEAdTi8Mt6JwfXPJU6ILNIXlySl01s7pfNf8Qz43k7AaZSJeI2A
|
||||||
|
blM6ilFwbXpWls64XKFQRYfsQ9+wPA044pF1zR05PSI8PJwzIVAjW34myJnbsywb
|
||||||
|
Yc1eQXlz0Di7R+w9HRkpVHG2CgnIBGJFa1H7p0FG9tyI7SaJ/Qri5BRJhnu2gYjx
|
||||||
|
B+JV3ol+0oYYMhVVaGXwHpyjelsEiWaIFoO3o0YxfW19NM90QQnJ3BGX7ibJSxAr
|
||||||
|
Lwbh8DWnWi4X3MdIPG88BKoavcXlJ/pyW2PvarUe31xVBNbyDlcZvrTZ8PXVw7TA
|
||||||
|
lumboAhMDLhYNBWrJTJe5LOiapEJaOBNN/ZMFQ==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIC6DCCAdCgAwIBAgICAO4wDQYJKoZIhvcNAQELBQAwDjEMMAoGA1UEAwwDMjM4
|
||||||
|
MB4XDTE1MDYxMzAwMTU1NVoXDTE2MDYxMjAwMTU1NVowDjEMMAoGA1UEAwwDMjM4
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvP9z1YFDa1WD9hVI9W3K
|
||||||
|
lWQmUGfLW35x6xkgDm8o3OTWR2QoxjXratacKhm2VevV22QjCBvHXeHx3fxSp5w/
|
||||||
|
p4CH+Ul76wCq3+WAPidO42YCP7SZdqYUR4GHKQ/oOyistRAKEamg4aPAbIs7l1Kn
|
||||||
|
T5YHFdSzCWpe6F2+ceoluvKEn6vFVloXKghaeEyTDKnnJKs3/04TdtZjVM5OObvQ
|
||||||
|
CGFlQlysDJxWahtVM93gylB8WYgyiekDAx1I3lCd3Vv0hF+x04xT3fwVRzmaKNzT
|
||||||
|
wN+znae643Qfg2oSSLV066K2WYepgzqKwv3IUdrLbes331AMs+FbdxHanMrOU1i+
|
||||||
|
OQIDAQABo1AwTjAdBgNVHQ4EFgQUjog7s8eJhAvSKMvu6xHZxPnnjsgwHwYDVR0j
|
||||||
|
BBgwFoAUjog7s8eJhAvSKMvu6xHZxPnnjsgwDAYDVR0TBAUwAwEB/zANBgkqhkiG
|
||||||
|
9w0BAQsFAAOCAQEAIugSn0o0HQMLy02inFZDso4PiRfoqahsT60oIMmWhF3nY3Jq
|
||||||
|
GZmkozKGnvyNDeKPlf6TV04VLq6dRg7+yQDL6LCiq2wcGZ+8feMLjyRFwZDSjAJe
|
||||||
|
sAMhNq9OQdGNfUV1iZF0SUzqrT68BCT0JTtuDpwlMcmH1O+jFf2HCzROLLBdRC3w
|
||||||
|
tJGiA6DH2TqVnucql6sMrnxPVEB+uVfFaKNc9YzwDCp8dSmBbCz7wRmLobGKcnbQ
|
||||||
|
lByD5j4dxYkFvJ6n/YX1HKJzwqTWhLQaxvFW7YvnPWepEiXiB6BaIsRgyK7Qa8EW
|
||||||
|
3jL5yiB1Dd8OQ7aV7+PNwBNXHd3J1Vie2k52KA==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
26
cmd/shell.go
26
cmd/shell.go
|
|
@ -174,14 +174,7 @@ type Config struct {
|
||||||
DebugAddr string
|
DebugAddr string
|
||||||
}
|
}
|
||||||
|
|
||||||
OCSPUpdater struct {
|
OCSPUpdater OCSPUpdaterConfig
|
||||||
DBConnect string
|
|
||||||
MinTimeToExpiry string
|
|
||||||
ResponseLimit int
|
|
||||||
|
|
||||||
// DebugAddr is the address to run the /debug handlers on.
|
|
||||||
DebugAddr string
|
|
||||||
}
|
|
||||||
|
|
||||||
Publisher struct {
|
Publisher struct {
|
||||||
CT publisher.CTConfig
|
CT publisher.CTConfig
|
||||||
|
|
@ -270,6 +263,23 @@ type Queue struct {
|
||||||
Server string
|
Server string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OCSPUpdaterConfig provides the various window tick times and batch sizes needed
|
||||||
|
// for the OCSP (and SCT) updater
|
||||||
|
type OCSPUpdaterConfig struct {
|
||||||
|
DBConnect string
|
||||||
|
|
||||||
|
NewCertificateWindow ConfigDuration
|
||||||
|
OldOCSPWindow ConfigDuration
|
||||||
|
|
||||||
|
NewCertificateBatchSize int
|
||||||
|
OldOCSPBatchSize int
|
||||||
|
|
||||||
|
OCSPMinTimeToExpiry ConfigDuration
|
||||||
|
|
||||||
|
// DebugAddr is the address to run the /debug handlers on.
|
||||||
|
DebugAddr string
|
||||||
|
}
|
||||||
|
|
||||||
// RateLimitConfig contains all application layer rate limiting policies
|
// RateLimitConfig contains all application layer rate limiting policies
|
||||||
type RateLimitConfig struct {
|
type RateLimitConfig struct {
|
||||||
TotalCertificates RateLimitPolicy `yaml:"totalCertificates"`
|
TotalCertificates RateLimitPolicy `yaml:"totalCertificates"`
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,9 @@ type SignatureValidationError string
|
||||||
// for some reason.
|
// for some reason.
|
||||||
type CertificateIssuanceError string
|
type CertificateIssuanceError string
|
||||||
|
|
||||||
|
// NoSuchRegistrationError indicates that a registration could not be found.
|
||||||
|
type NoSuchRegistrationError string
|
||||||
|
|
||||||
// RateLimitedError indicates the user has hit a rate limit
|
// RateLimitedError indicates the user has hit a rate limit
|
||||||
type RateLimitedError string
|
type RateLimitedError string
|
||||||
|
|
||||||
|
|
@ -93,6 +96,7 @@ func (e LengthRequiredError) Error() string { return string(e) }
|
||||||
func (e SyntaxError) Error() string { return string(e) }
|
func (e SyntaxError) Error() string { return string(e) }
|
||||||
func (e SignatureValidationError) Error() string { return string(e) }
|
func (e SignatureValidationError) Error() string { return string(e) }
|
||||||
func (e CertificateIssuanceError) Error() string { return string(e) }
|
func (e CertificateIssuanceError) Error() string { return string(e) }
|
||||||
|
func (e NoSuchRegistrationError) Error() string { return string(e) }
|
||||||
func (e RateLimitedError) Error() string { return string(e) }
|
func (e RateLimitedError) Error() string { return string(e) }
|
||||||
|
|
||||||
// Base64 functions
|
// Base64 functions
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,13 @@ package metrics
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
|
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
|
||||||
|
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/jmhodges/clock"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTPMonitor stores some server state
|
// HTTPMonitor stores some server state
|
||||||
|
|
@ -22,7 +22,6 @@ type HTTPMonitor struct {
|
||||||
statsPrefix string
|
statsPrefix string
|
||||||
handler http.Handler
|
handler http.Handler
|
||||||
connectionsInFlight int64
|
connectionsInFlight int64
|
||||||
openConnections int64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHTTPMonitor returns a new initialized HTTPMonitor
|
// NewHTTPMonitor returns a new initialized HTTPMonitor
|
||||||
|
|
@ -32,26 +31,9 @@ func NewHTTPMonitor(stats statsd.Statter, handler http.Handler, prefix string) H
|
||||||
handler: handler,
|
handler: handler,
|
||||||
statsPrefix: prefix,
|
statsPrefix: prefix,
|
||||||
connectionsInFlight: 0,
|
connectionsInFlight: 0,
|
||||||
openConnections: 0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectionMonitor provides states on open connection state
|
|
||||||
func (h *HTTPMonitor) ConnectionMonitor(_ net.Conn, state http.ConnState) {
|
|
||||||
var open int64
|
|
||||||
switch state {
|
|
||||||
case http.StateNew:
|
|
||||||
open = atomic.AddInt64(&h.openConnections, 1)
|
|
||||||
case http.StateHijacked:
|
|
||||||
fallthrough
|
|
||||||
case http.StateClosed:
|
|
||||||
open = atomic.AddInt64(&h.openConnections, -1)
|
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.stats.Gauge(fmt.Sprintf("%s.HTTP.OpenConnections", h.statsPrefix), open, 1.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle wraps handlers and records various metrics about requests to these handlers
|
// Handle wraps handlers and records various metrics about requests to these handlers
|
||||||
// and sends them to StatsD
|
// and sends them to StatsD
|
||||||
func (h *HTTPMonitor) Handle() http.Handler {
|
func (h *HTTPMonitor) Handle() http.Handler {
|
||||||
|
|
@ -85,3 +67,53 @@ func (h *HTTPMonitor) watchAndServe(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
h.stats.TimingDuration(fmt.Sprintf("%s.HTTP.ResponseTime.%s", h.statsPrefix, endpoint), cClosed, 1.0)
|
h.stats.TimingDuration(fmt.Sprintf("%s.HTTP.ResponseTime.%s", h.statsPrefix, endpoint), cClosed, 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FBAdapter provides a facebookgo/stats client interface that sends metrics via
|
||||||
|
// a StatsD client
|
||||||
|
type FBAdapter struct {
|
||||||
|
stats statsd.Statter
|
||||||
|
prefix string
|
||||||
|
clk clock.Clock
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFBAdapter returns a new adapter
|
||||||
|
func NewFBAdapter(stats statsd.Statter, prefix string, clock clock.Clock) FBAdapter {
|
||||||
|
return FBAdapter{stats: stats, prefix: prefix, clk: clock}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BumpAvg is essentially statsd.Statter.Gauge
|
||||||
|
func (fba FBAdapter) BumpAvg(key string, val float64) {
|
||||||
|
fba.stats.Gauge(fmt.Sprintf("%s.%s", fba.prefix, key), int64(val), 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BumpSum is essentially statsd.Statter.Inc (httpdown only ever uses positive
|
||||||
|
// deltas)
|
||||||
|
func (fba FBAdapter) BumpSum(key string, val float64) {
|
||||||
|
fba.stats.Inc(fmt.Sprintf("%s.%s", fba.prefix, key), int64(val), 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
type btHolder struct {
|
||||||
|
key string
|
||||||
|
stats statsd.Statter
|
||||||
|
started time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bth btHolder) End() {
|
||||||
|
bth.stats.TimingDuration(bth.key, time.Since(bth.started), 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BumpTime is essentially a (much better) statsd.Statter.TimingDuration
|
||||||
|
func (fba FBAdapter) BumpTime(key string) interface {
|
||||||
|
End()
|
||||||
|
} {
|
||||||
|
return btHolder{
|
||||||
|
key: fmt.Sprintf("%s.%s", fba.prefix, key),
|
||||||
|
started: fba.clk.Now(),
|
||||||
|
stats: fba.stats,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BumpHistogram isn't used by facebookgo/httpdown
|
||||||
|
func (fba FBAdapter) BumpHistogram(_ string, _ float64) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
package mocks
|
package mocks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -180,7 +179,7 @@ func (sa *MockSA) GetRegistrationByKey(jwk jose.JsonWebKey) (core.Registration,
|
||||||
|
|
||||||
if core.KeyDigestEquals(jwk, test2KeyPublic) {
|
if core.KeyDigestEquals(jwk, test2KeyPublic) {
|
||||||
// No key found
|
// No key found
|
||||||
return core.Registration{ID: 2}, sql.ErrNoRows
|
return core.Registration{ID: 2}, core.NoSuchRegistrationError("reg not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a fake registration. Make sure to fill the key field to avoid marshaling errors.
|
// Return a fake registration. Make sure to fill the key field to avoid marshaling errors.
|
||||||
|
|
|
||||||
|
|
@ -192,10 +192,10 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, *sa.SQLStorageAut
|
||||||
OCSPSigner: ocspSigner,
|
OCSPSigner: ocspSigner,
|
||||||
SA: ssa,
|
SA: ssa,
|
||||||
PA: pa,
|
PA: pa,
|
||||||
Publisher: &mocks.MockPublisher{},
|
|
||||||
ValidityPeriod: time.Hour * 2190,
|
ValidityPeriod: time.Hour * 2190,
|
||||||
NotAfter: time.Now().Add(time.Hour * 8761),
|
NotAfter: time.Now().Add(time.Hour * 8761),
|
||||||
Clk: fc,
|
Clk: fc,
|
||||||
|
Publisher: &mocks.MockPublisher{},
|
||||||
}
|
}
|
||||||
cleanUp := func() {
|
cleanUp := func() {
|
||||||
saDBCleanUp()
|
saDBCleanUp()
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,8 @@ func wrapError(err error) (rpcError RPCError) {
|
||||||
rpcError.Type = "SignatureValidationError"
|
rpcError.Type = "SignatureValidationError"
|
||||||
case core.CertificateIssuanceError:
|
case core.CertificateIssuanceError:
|
||||||
rpcError.Type = "CertificateIssuanceError"
|
rpcError.Type = "CertificateIssuanceError"
|
||||||
|
case core.NoSuchRegistrationError:
|
||||||
|
rpcError.Type = "NoSuchRegistrationError"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
@ -249,6 +251,8 @@ func unwrapError(rpcError RPCError) (err error) {
|
||||||
err = core.SignatureValidationError(rpcError.Value)
|
err = core.SignatureValidationError(rpcError.Value)
|
||||||
case "CertificateIssuanceError":
|
case "CertificateIssuanceError":
|
||||||
err = core.CertificateIssuanceError(rpcError.Value)
|
err = core.CertificateIssuanceError(rpcError.Value)
|
||||||
|
case "NoSuchRegistrationError":
|
||||||
|
err = core.NoSuchRegistrationError(rpcError.Value)
|
||||||
default:
|
default:
|
||||||
err = errors.New(rpcError.Value)
|
err = errors.New(rpcError.Value)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
-- +goose Up
|
||||||
|
-- SQL in section 'Up' is executed when this migration is applied
|
||||||
|
|
||||||
|
CREATE INDEX `ocspLastUpdated_certificateStatus_idx` on `certificateStatus` (`ocspLastUpdated`);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- SQL section 'Down' is executed when this migration is rolled back
|
||||||
|
|
||||||
|
DROP INDEX `ocspLastUpdated_certificateStatus_idx` on `certificateStatus`;
|
||||||
|
|
@ -119,14 +119,6 @@ func updateChallenges(authID string, challenges []core.Challenge, tx *gorp.Trans
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type NoSuchRegistrationError struct {
|
|
||||||
Msg string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e NoSuchRegistrationError) Error() string {
|
|
||||||
return e.Msg
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRegistration obtains a Registration by ID
|
// GetRegistration obtains a Registration by ID
|
||||||
func (ssa *SQLStorageAuthority) GetRegistration(id int64) (core.Registration, error) {
|
func (ssa *SQLStorageAuthority) GetRegistration(id int64) (core.Registration, error) {
|
||||||
regObj, err := ssa.dbMap.Get(regModel{}, id)
|
regObj, err := ssa.dbMap.Get(regModel{}, id)
|
||||||
|
|
@ -135,7 +127,7 @@ func (ssa *SQLStorageAuthority) GetRegistration(id int64) (core.Registration, er
|
||||||
}
|
}
|
||||||
if regObj == nil {
|
if regObj == nil {
|
||||||
msg := fmt.Sprintf("No registrations with ID %d", id)
|
msg := fmt.Sprintf("No registrations with ID %d", id)
|
||||||
return core.Registration{}, NoSuchRegistrationError{Msg: msg}
|
return core.Registration{}, core.NoSuchRegistrationError(msg)
|
||||||
}
|
}
|
||||||
regPtr, ok := regObj.(*regModel)
|
regPtr, ok := regObj.(*regModel)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -155,7 +147,7 @@ func (ssa *SQLStorageAuthority) GetRegistrationByKey(key jose.JsonWebKey) (core.
|
||||||
|
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
msg := fmt.Sprintf("No registrations with public key sha256 %s", sha)
|
msg := fmt.Sprintf("No registrations with public key sha256 %s", sha)
|
||||||
return core.Registration{}, NoSuchRegistrationError{Msg: msg}
|
return core.Registration{}, core.NoSuchRegistrationError(msg)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return core.Registration{}, err
|
return core.Registration{}, err
|
||||||
|
|
@ -442,7 +434,7 @@ func (ssa *SQLStorageAuthority) UpdateRegistration(reg core.Registration) error
|
||||||
}
|
}
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
msg := fmt.Sprintf("Requested registration not found %v", reg.ID)
|
msg := fmt.Sprintf("Requested registration not found %v", reg.ID)
|
||||||
return NoSuchRegistrationError{Msg: msg}
|
return core.NoSuchRegistrationError(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -116,18 +116,18 @@ func TestNoSuchRegistrationErrors(t *testing.T) {
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
_, err := sa.GetRegistration(100)
|
_, err := sa.GetRegistration(100)
|
||||||
if _, ok := err.(NoSuchRegistrationError); !ok {
|
if _, ok := err.(core.NoSuchRegistrationError); !ok {
|
||||||
t.Errorf("GetRegistration: expected NoSuchRegistrationError, got %T type error (%s)", err, err)
|
t.Errorf("GetRegistration: expected NoSuchRegistrationError, got %T type error (%s)", err, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
jwk := satest.GoodJWK()
|
jwk := satest.GoodJWK()
|
||||||
_, err = sa.GetRegistrationByKey(jwk)
|
_, err = sa.GetRegistrationByKey(jwk)
|
||||||
if _, ok := err.(NoSuchRegistrationError); !ok {
|
if _, ok := err.(core.NoSuchRegistrationError); !ok {
|
||||||
t.Errorf("GetRegistrationByKey: expected a NoSuchRegistrationError, got %T type error (%s)", err, err)
|
t.Errorf("GetRegistrationByKey: expected a NoSuchRegistrationError, got %T type error (%s)", err, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = sa.UpdateRegistration(core.Registration{ID: 100, Key: jwk})
|
err = sa.UpdateRegistration(core.Registration{ID: 100, Key: jwk})
|
||||||
if _, ok := err.(NoSuchRegistrationError); !ok {
|
if _, ok := err.(core.NoSuchRegistrationError); !ok {
|
||||||
t.Errorf("UpdateRegistration: expected a NoSuchRegistrationError, got %T type error (%v)", err, err)
|
t.Errorf("UpdateRegistration: expected a NoSuchRegistrationError, got %T type error (%v)", err, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
175
test.sh
175
test.sh
|
|
@ -5,6 +5,11 @@ if type realpath >/dev/null 2>&1 ; then
|
||||||
cd $(realpath $(dirname $0))
|
cd $(realpath $(dirname $0))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# The list of segments to run. To run only some of these segments, pre-set the
|
||||||
|
# RUN variable with the ones you want (see .travis.yml for an example).
|
||||||
|
# Order doesn't matter.
|
||||||
|
RUN=${RUN:-vet lint fmt migrations unit integration}
|
||||||
|
|
||||||
FAILURE=0
|
FAILURE=0
|
||||||
|
|
||||||
TESTPATHS=$(go list -f '{{ .ImportPath }}' ./...)
|
TESTPATHS=$(go list -f '{{ .ImportPath }}' ./...)
|
||||||
|
|
@ -19,7 +24,7 @@ if [ "x${TRAVIS_PULL_REQUEST}" != "x" ] ; then
|
||||||
TRIGGER_COMMIT=${revs##* }
|
TRIGGER_COMMIT=${revs##* }
|
||||||
fi
|
fi
|
||||||
|
|
||||||
GITHUB_SECRET_FILE="$(pwd)/test/github-secret.json"
|
GITHUB_SECRET_FILE="/tmp/github-secret.json"
|
||||||
|
|
||||||
start_context() {
|
start_context() {
|
||||||
CONTEXT="$1"
|
CONTEXT="$1"
|
||||||
|
|
@ -139,116 +144,116 @@ GOBIN=${GOBIN:-$HOME/gopath/bin}
|
||||||
#
|
#
|
||||||
# Run Go Vet, a correctness-focused static analysis tool
|
# Run Go Vet, a correctness-focused static analysis tool
|
||||||
#
|
#
|
||||||
|
if [[ "$RUN" =~ "vet" ]] ; then
|
||||||
start_context "test/vet"
|
start_context "test/vet"
|
||||||
run_and_comment go vet ./...
|
run_and_comment go vet ./...
|
||||||
end_context #test/vet
|
end_context #test/vet
|
||||||
|
fi
|
||||||
|
|
||||||
#
|
#
|
||||||
# Run Go Lint, a style-focused static analysis tool
|
# Run Go Lint, a style-focused static analysis tool
|
||||||
#
|
#
|
||||||
|
if [[ "$RUN" =~ "lint" ]] ; then
|
||||||
start_context "test/golint"
|
start_context "test/golint"
|
||||||
[ -x "$(which golint)" ] && run golint ./...
|
[ -x "$(which golint)" ] && run golint ./...
|
||||||
end_context #test/golint
|
end_context #test/golint
|
||||||
|
fi
|
||||||
|
|
||||||
#
|
#
|
||||||
# Ensure all files are formatted per the `go fmt` tool
|
# Ensure all files are formatted per the `go fmt` tool
|
||||||
#
|
#
|
||||||
start_context "test/gofmt"
|
if [[ "$RUN" =~ "fmt" ]] ; then
|
||||||
check_gofmt() {
|
start_context "test/gofmt"
|
||||||
unformatted=$(find . -name "*.go" -not -path "./Godeps/*" -print | xargs -n1 gofmt -l)
|
check_gofmt() {
|
||||||
if [ "x${unformatted}" == "x" ] ; then
|
unformatted=$(find . -name "*.go" -not -path "./Godeps/*" -print | xargs -n1 gofmt -l)
|
||||||
return 0
|
if [ "x${unformatted}" == "x" ] ; then
|
||||||
else
|
return 0
|
||||||
V="Unformatted files found.
|
else
|
||||||
Please run 'go fmt' on each of these files and amend your commit to continue."
|
V="Unformatted files found.
|
||||||
|
Please run 'go fmt' on each of these files and amend your commit to continue."
|
||||||
|
|
||||||
for f in ${unformatted}; do
|
for f in ${unformatted}; do
|
||||||
V=$(printf "%s\n - %s" "${V}" "${f}")
|
V=$(printf "%s\n - %s" "${V}" "${f}")
|
||||||
done
|
done
|
||||||
|
|
||||||
# Print to stdout
|
# Print to stdout
|
||||||
printf "%s\n\n" "${V}"
|
printf "%s\n\n" "${V}"
|
||||||
[ "${TRAVIS}" == "true" ] || exit 1 # Stop here if running locally
|
[ "${TRAVIS}" == "true" ] || exit 1 # Stop here if running locally
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
run_and_comment check_gofmt
|
run_and_comment check_gofmt
|
||||||
end_context #test/gofmt
|
end_context #test/gofmt
|
||||||
|
fi
|
||||||
|
|
||||||
start_context "test/migrations"
|
if [[ "$RUN" =~ "migrations" ]] ; then
|
||||||
run_and_comment ./test/test-no-outdated-migrations.sh
|
start_context "test/migrations"
|
||||||
end_context "test/migrations"
|
run_and_comment ./test/test-no-outdated-migrations.sh
|
||||||
|
end_context "test/migrations"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "${TRAVIS}" == "true" ]; then
|
#
|
||||||
|
# Prepare the database for unittests and integration tests
|
||||||
|
#
|
||||||
|
if [[ "${TRAVIS}" == "true" ]] ; then
|
||||||
./test/create_db.sh || die "unable to create the boulder database with test/create_db.sh"
|
./test/create_db.sh || die "unable to create the boulder database with test/create_db.sh"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
#
|
#
|
||||||
# Unit Tests. These do not receive a context or status updates,
|
# Unit Tests.
|
||||||
# as they are reflected in our eventual exit code.
|
|
||||||
#
|
#
|
||||||
|
if [[ "$RUN" =~ "unit" ]] ; then
|
||||||
if [ "${SKIP_UNIT_TESTS}" == "1" ]; then
|
|
||||||
echo "Skipping unit tests."
|
|
||||||
else
|
|
||||||
run_unit_tests
|
run_unit_tests
|
||||||
fi
|
# If the unittests failed, exit before trying to run the integration test.
|
||||||
|
if [ ${FAILURE} != 0 ]; then
|
||||||
# If the unittests failed, exit before trying to run the integration test.
|
echo "--------------------------------------------------"
|
||||||
if [ ${FAILURE} != 0 ]; then
|
echo "--- A unit test or tool failed. ---"
|
||||||
echo "--------------------------------------------------"
|
echo "--- Stopping before running integration tests. ---"
|
||||||
echo "--- A unit test or tool failed. ---"
|
echo "--------------------------------------------------"
|
||||||
echo "--- Stopping before running integration tests. ---"
|
exit ${FAILURE}
|
||||||
echo "--------------------------------------------------"
|
fi
|
||||||
exit ${FAILURE}
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${SKIP_INTEGRATION_TESTS}" = "1" ]; then
|
|
||||||
echo "Skipping integration tests."
|
|
||||||
exit ${FAILURE}
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
#
|
#
|
||||||
# Integration tests
|
# Integration tests
|
||||||
#
|
#
|
||||||
|
if [[ "$RUN" =~ "integration" ]] ; then
|
||||||
|
# Set context to integration, and force a pending state
|
||||||
|
start_context "test/integration"
|
||||||
|
update_status --state pending --description "Integration Tests in progress"
|
||||||
|
|
||||||
# Set context to integration, and force a pending state
|
if [ -z "$LETSENCRYPT_PATH" ]; then
|
||||||
start_context "test/integration"
|
export LETSENCRYPT_PATH=$(mktemp -d -t leXXXX)
|
||||||
update_status --state pending --description "Integration Tests in progress"
|
echo "------------------------------------------------"
|
||||||
|
echo "--- Checking out letsencrypt client is slow. ---"
|
||||||
|
echo "--- Recommend setting \$LETSENCRYPT_PATH to ---"
|
||||||
|
echo "--- client repo with initialized virtualenv ---"
|
||||||
|
echo "------------------------------------------------"
|
||||||
|
build_letsencrypt
|
||||||
|
elif [ ! -d "${LETSENCRYPT_PATH}" ]; then
|
||||||
|
build_letsencrypt
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -z "$LETSENCRYPT_PATH" ]; then
|
python test/amqp-integration-test.py
|
||||||
export LETSENCRYPT_PATH=$(mktemp -d -t leXXXX)
|
case $? in
|
||||||
echo "------------------------------------------------"
|
0) # Success
|
||||||
echo "--- Checking out letsencrypt client is slow. ---"
|
update_status --state success
|
||||||
echo "--- Recommend setting \$LETSENCRYPT_PATH to ---"
|
;;
|
||||||
echo "--- client repo with initialized virtualenv ---"
|
1) # Python client failed
|
||||||
echo "------------------------------------------------"
|
update_status --state success --description "Python integration failed."
|
||||||
build_letsencrypt
|
FAILURE=1
|
||||||
elif [ ! -d "${LETSENCRYPT_PATH}" ]; then
|
;;
|
||||||
build_letsencrypt
|
2) # Node client failed
|
||||||
|
update_status --state failure --description "NodeJS integration failed."
|
||||||
|
FAILURE=1
|
||||||
|
;;
|
||||||
|
*) # Error occurred
|
||||||
|
update_status --state error --description "Unknown error occurred."
|
||||||
|
FAILURE=1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
end_context #test/integration
|
||||||
fi
|
fi
|
||||||
|
|
||||||
python test/amqp-integration-test.py
|
|
||||||
case $? in
|
|
||||||
0) # Success
|
|
||||||
update_status --state success
|
|
||||||
;;
|
|
||||||
1) # Python client failed
|
|
||||||
update_status --state success --description "Python integration failed."
|
|
||||||
FAILURE=1
|
|
||||||
;;
|
|
||||||
2) # Node client failed
|
|
||||||
update_status --state failure --description "NodeJS integration failed."
|
|
||||||
FAILURE=1
|
|
||||||
;;
|
|
||||||
*) # Error occurred
|
|
||||||
update_status --state error --description "Unknown error occurred."
|
|
||||||
FAILURE=1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
end_context #test/integration
|
|
||||||
|
|
||||||
exit ${FAILURE}
|
exit ${FAILURE}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import urllib
|
import urllib
|
||||||
|
import time
|
||||||
import urllib2
|
import urllib2
|
||||||
|
|
||||||
import startservers
|
import startservers
|
||||||
|
|
@ -84,8 +85,11 @@ def get_ocsp(cert_file, url):
|
||||||
def verify_ocsp_good(certFile, url):
|
def verify_ocsp_good(certFile, url):
|
||||||
output = get_ocsp(certFile, url)
|
output = get_ocsp(certFile, url)
|
||||||
if not re.search(": good", output):
|
if not re.search(": good", output):
|
||||||
print "Expected OCSP response 'good', got something else."
|
if not re.search(" unauthorized \(6\)", output):
|
||||||
die(ExitStatus.OCSPFailure)
|
print "Expected OCSP response 'unauthorized', got something else."
|
||||||
|
die(ExitStatus.OCSPFailure)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def verify_ocsp_revoked(certFile, url):
|
def verify_ocsp_revoked(certFile, url):
|
||||||
output = get_ocsp(certFile, url)
|
output = get_ocsp(certFile, url)
|
||||||
|
|
@ -94,6 +98,17 @@ def verify_ocsp_revoked(certFile, url):
|
||||||
die(ExitStatus.OCSPFailure)
|
die(ExitStatus.OCSPFailure)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# loop_check expects the function passed as action will return True/False to indicate
|
||||||
|
# success/failure
|
||||||
|
def loop_check(failureStatus, action, *args):
|
||||||
|
timeout = time.time() + 5
|
||||||
|
while True:
|
||||||
|
if action(*args):
|
||||||
|
break
|
||||||
|
if time.time() > timeout:
|
||||||
|
die(failureStatus)
|
||||||
|
time.sleep(0.25)
|
||||||
|
|
||||||
def verify_ct_submission(expectedSubmissions, url):
|
def verify_ct_submission(expectedSubmissions, url):
|
||||||
resp = urllib2.urlopen(url)
|
resp = urllib2.urlopen(url)
|
||||||
submissionStr = resp.read()
|
submissionStr = resp.read()
|
||||||
|
|
@ -126,10 +141,15 @@ def run_node_test():
|
||||||
|
|
||||||
ee_ocsp_url = "http://localhost:4002"
|
ee_ocsp_url = "http://localhost:4002"
|
||||||
issuer_ocsp_url = "http://localhost:4003"
|
issuer_ocsp_url = "http://localhost:4003"
|
||||||
verify_ocsp_good(certFile, ee_ocsp_url)
|
|
||||||
# Also verify that the static OCSP responder, which answers with a
|
# Also verify that the static OCSP responder, which answers with a
|
||||||
# pre-signed, long-lived response for the CA cert, also works.
|
# pre-signed, long-lived response for the CA cert, also works.
|
||||||
verify_ocsp_good("../test-ca.der", issuer_ocsp_url)
|
verify_ocsp_good("../test-ca.der", issuer_ocsp_url)
|
||||||
|
|
||||||
|
# As OCSP-Updater is generating responses indepedantly of the CA we sit in a loop
|
||||||
|
# checking OCSP until we either see a good response or we timeout (5s).
|
||||||
|
loop_check(ExitStatus.OCSPFailure, verify_ocsp_good, certFile, ee_ocsp_url)
|
||||||
|
|
||||||
verify_ct_submission(1, "http://localhost:4500/submissions")
|
verify_ct_submission(1, "http://localhost:4500/submissions")
|
||||||
|
|
||||||
if subprocess.Popen('''
|
if subprocess.Popen('''
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,14 @@
|
||||||
|
|
||||||
"ocspUpdater": {
|
"ocspUpdater": {
|
||||||
"dbConnect": "mysql+tcp://boulder@localhost:3306/boulder_sa_integration",
|
"dbConnect": "mysql+tcp://boulder@localhost:3306/boulder_sa_integration",
|
||||||
"minTimeToExpiry": "72h",
|
"newCertificateWindow": "1s",
|
||||||
|
"oldOCSPWindow": "2s",
|
||||||
|
"missingSCTWindow": "1m",
|
||||||
|
"newCertificateBatchSize": 1000,
|
||||||
|
"oldOCSPBatchSize": 5000,
|
||||||
|
"missingSCTBatchSize": 5000,
|
||||||
|
"ocspMinTimeToExpiry": "72h",
|
||||||
|
"sctOldestIssued": "72h",
|
||||||
"debugAddr": "localhost:8006"
|
"debugAddr": "localhost:8006"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,29 +32,19 @@ if default_config is None:
|
||||||
processes = []
|
processes = []
|
||||||
|
|
||||||
|
|
||||||
def install(progs, race_detection):
|
def install(race_detection):
|
||||||
cmd = "go install"
|
# Pass empty BUILD_TIME and BUILD_ID flags to avoid constantly invalidating the
|
||||||
|
# build cache with new BUILD_TIMEs, or invalidating it on merges with a new
|
||||||
|
# BUILD_ID.
|
||||||
|
cmd = "make BUILD_TIME='' BUILD_ID='' "
|
||||||
if race_detection:
|
if race_detection:
|
||||||
cmd = """go install -race"""
|
cmd = cmd + " GO_BUILD_FLAGS=-race"
|
||||||
|
|
||||||
for prog in progs:
|
return subprocess.call(cmd, shell=True) == 0
|
||||||
cmd += " ./" + prog
|
|
||||||
p = subprocess.Popen(cmd, shell=True)
|
|
||||||
out, err = p.communicate()
|
|
||||||
if p.returncode != 0:
|
|
||||||
sys.stderr.write("unable to run go install: %s\n" % cmd)
|
|
||||||
if out:
|
|
||||||
sys.stderr.write("stdout:\n" + out + "\n")
|
|
||||||
if err:
|
|
||||||
sys.stderr.write("stderr: \n" + err + "\n")
|
|
||||||
return False
|
|
||||||
print('installed %s with pid %d' % (cmd, p.pid))
|
|
||||||
return True
|
|
||||||
|
|
||||||
def run(path, race_detection, config=default_config):
|
def run(binary, race_detection, config=default_config):
|
||||||
binary = os.path.basename(path)
|
|
||||||
# Note: Must use exec here so that killing this process kills the command.
|
# Note: Must use exec here so that killing this process kills the command.
|
||||||
cmd = """GORACE="halt_on_error=1" exec %s --config %s""" % (binary, config)
|
cmd = """GORACE="halt_on_error=1" exec ./bin/%s --config %s""" % (binary, config)
|
||||||
p = subprocess.Popen(cmd, shell=True)
|
p = subprocess.Popen(cmd, shell=True)
|
||||||
p.cmd = cmd
|
p.cmd = cmd
|
||||||
print('started %s with pid %d' % (p.cmd, p.pid))
|
print('started %s with pid %d' % (p.cmd, p.pid))
|
||||||
|
|
@ -72,17 +62,18 @@ def start(race_detection):
|
||||||
t.daemon = True
|
t.daemon = True
|
||||||
t.start()
|
t.start()
|
||||||
progs = [
|
progs = [
|
||||||
'cmd/boulder-wfe',
|
'boulder-wfe',
|
||||||
'cmd/boulder-ra',
|
'boulder-ra',
|
||||||
'cmd/boulder-sa',
|
'boulder-sa',
|
||||||
'cmd/boulder-ca',
|
'boulder-ca',
|
||||||
'cmd/boulder-va',
|
'boulder-va',
|
||||||
'cmd/boulder-publisher',
|
'boulder-publisher',
|
||||||
'cmd/ocsp-responder',
|
'ocsp-updater',
|
||||||
'test/ct-test-srv',
|
'ocsp-responder',
|
||||||
'test/dns-test-srv'
|
'ct-test-srv',
|
||||||
|
'dns-test-srv'
|
||||||
]
|
]
|
||||||
if not install(progs, race_detection):
|
if not install(race_detection):
|
||||||
return False
|
return False
|
||||||
for prog in progs:
|
for prog in progs:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -o xtrace
|
||||||
|
|
||||||
|
# Travis does shallow clones, so there is no master branch present.
|
||||||
|
# But test-no-outdated-migrations.sh needs to check diffs against master.
|
||||||
|
# Fetch just the master branch from origin.
|
||||||
|
git fetch origin master
|
||||||
|
git branch master FETCH_HEAD
|
||||||
|
# Github-PR-Status secret
|
||||||
|
if [ -n "$encrypted_53b2630f0fb4_key" ]; then
|
||||||
|
openssl aes-256-cbc \
|
||||||
|
-K $encrypted_53b2630f0fb4_key -iv $encrypted_53b2630f0fb4_iv \
|
||||||
|
-in test/github-secret.json.enc -out /tmp/github-secret.json -d
|
||||||
|
fi
|
||||||
|
|
||||||
|
travis_retry go get \
|
||||||
|
golang.org/x/tools/cmd/vet \
|
||||||
|
golang.org/x/tools/cmd/cover \
|
||||||
|
github.com/golang/lint/golint \
|
||||||
|
github.com/mattn/goveralls \
|
||||||
|
github.com/modocache/gover \
|
||||||
|
github.com/jcjones/github-pr-status \
|
||||||
|
github.com/letsencrypt/goose/cmd/goose
|
||||||
|
|
||||||
|
# Boulder consists of multiple Go packages, which
|
||||||
|
# refer to each other by their absolute GitHub path,
|
||||||
|
# e.g. github.com/letsencrypt/boulder/analysis. That means, by default, if
|
||||||
|
# someone forks the repo, Travis won't pass on their own repo. To fix that,
|
||||||
|
# we add a symlink.
|
||||||
|
mkdir -p $TRAVIS_BUILD_DIR $GOPATH/src/github.com/letsencrypt
|
||||||
|
if [ ! -d $GOPATH/src/github.com/letsencrypt/boulder ] ; then
|
||||||
|
ln -s $TRAVIS_BUILD_DIR $GOPATH/src/github.com/letsencrypt/boulder
|
||||||
|
fi
|
||||||
|
|
||||||
|
set +o xtrace
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# A script to make it easier to parallelize the build by building make
|
|
||||||
# conditionally.
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
|
|
||||||
if [ "${TRAVIS}" != "true" ]; then
|
|
||||||
echo "Not to be run outside of TravisCI" > /dev/stderr
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${RUN_MAKE}" == "1" ]; then
|
|
||||||
make -j4 # Travis has 2 cores per build instance
|
|
||||||
fi
|
|
||||||
|
|
@ -333,30 +333,6 @@ func (va *ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdenti
|
||||||
return challenge, err
|
return challenge, err
|
||||||
}
|
}
|
||||||
|
|
||||||
contentTypes, ok := httpResponse.Header["Content-Type"]
|
|
||||||
if ok && len(contentTypes) != 1 {
|
|
||||||
challenge.Status = core.StatusInvalid
|
|
||||||
challenge.Error = &core.ProblemDetails{
|
|
||||||
Type: core.UnauthorizedProblem,
|
|
||||||
Detail: "Multiple Content-Type headers provided",
|
|
||||||
}
|
|
||||||
return challenge, challenge.Error
|
|
||||||
} else if ok && len(contentTypes) == 1 && contentTypes[0] != "application/jose+json" {
|
|
||||||
challenge.Status = core.StatusInvalid
|
|
||||||
challenge.Error = &core.ProblemDetails{
|
|
||||||
Type: core.UnauthorizedProblem,
|
|
||||||
Detail: "Invalid content type",
|
|
||||||
}
|
|
||||||
return challenge, challenge.Error
|
|
||||||
} else if !ok {
|
|
||||||
challenge.Status = core.StatusInvalid
|
|
||||||
challenge.Error = &core.ProblemDetails{
|
|
||||||
Type: core.UnauthorizedProblem,
|
|
||||||
Detail: "No Content-Type header provided",
|
|
||||||
}
|
|
||||||
return challenge, challenge.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read body & test
|
// Read body & test
|
||||||
body, readErr := ioutil.ReadAll(httpResponse.Body)
|
body, readErr := ioutil.ReadAll(httpResponse.Body)
|
||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,6 @@ func simpleSrv(t *testing.T, token string, enableTLS bool) *httptest.Server {
|
||||||
currentToken := defaultToken
|
currentToken := defaultToken
|
||||||
|
|
||||||
m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/jose+json")
|
|
||||||
if !strings.HasPrefix(r.Host, "localhost:") && r.Host != "other.valid" && r.Host != "other.valid:8080" {
|
if !strings.HasPrefix(r.Host, "localhost:") && r.Host != "other.valid" && r.Host != "other.valid:8080" {
|
||||||
t.Errorf("Bad Host header: " + r.Host)
|
t.Errorf("Bad Host header: " + r.Host)
|
||||||
}
|
}
|
||||||
|
|
@ -132,15 +131,6 @@ func simpleSrv(t *testing.T, token string, enableTLS bool) *httptest.Server {
|
||||||
} else if strings.HasSuffix(r.URL.Path, pathRedirectPort) {
|
} else if strings.HasSuffix(r.URL.Path, pathRedirectPort) {
|
||||||
t.Logf("SIMPLESRV: Got a port redirect req\n")
|
t.Logf("SIMPLESRV: Got a port redirect req\n")
|
||||||
http.Redirect(w, r, "http://other.valid:8080/path", 302)
|
http.Redirect(w, r, "http://other.valid:8080/path", 302)
|
||||||
} else if strings.HasSuffix(r.URL.Path, "bad-content-type") {
|
|
||||||
w.Header().Set("Content-Type", "application/bad")
|
|
||||||
t.Logf("SIMPLESRV: Got bad content type header req\n")
|
|
||||||
} else if strings.HasSuffix(r.URL.Path, "multi-content-type") {
|
|
||||||
w.Header()["Content-Type"] = []string{"application/jose+json", "application/bad"}
|
|
||||||
t.Logf("SIMPLESRV: Got bad content type header req\n")
|
|
||||||
} else if strings.HasSuffix(r.URL.Path, "no-content-type") {
|
|
||||||
w.Header().Del("Content-Type")
|
|
||||||
t.Logf("SIMPLESRV: Got bad content type header req\n")
|
|
||||||
} else {
|
} else {
|
||||||
t.Logf("SIMPLESRV: Got a valid req\n")
|
t.Logf("SIMPLESRV: Got a valid req\n")
|
||||||
fmt.Fprint(w, createValidation(currentToken, enableTLS))
|
fmt.Fprint(w, createValidation(currentToken, enableTLS))
|
||||||
|
|
@ -314,30 +304,6 @@ func TestSimpleHttp(t *testing.T) {
|
||||||
test.AssertNotError(t, err, "Error validating simpleHttp")
|
test.AssertNotError(t, err, "Error validating simpleHttp")
|
||||||
test.AssertEquals(t, len(log.GetAllMatching(`^\[AUDIT\] `)), 1)
|
test.AssertEquals(t, len(log.GetAllMatching(`^\[AUDIT\] `)), 1)
|
||||||
|
|
||||||
log.Clear()
|
|
||||||
chall.Token = "bad-content-type"
|
|
||||||
invalidChall, err = va.validateSimpleHTTP(ident, chall)
|
|
||||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
|
||||||
test.AssertError(t, err, "Error validating simpleHttp")
|
|
||||||
test.AssertEquals(t, invalidChall.Error.Type, core.UnauthorizedProblem)
|
|
||||||
test.AssertEquals(t, len(log.GetAllMatching(`^\[AUDIT\] `)), 1)
|
|
||||||
|
|
||||||
log.Clear()
|
|
||||||
chall.Token = "multi-content-type"
|
|
||||||
invalidChall, err = va.validateSimpleHTTP(ident, chall)
|
|
||||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
|
||||||
test.AssertError(t, err, "Error validating simpleHttp")
|
|
||||||
test.AssertEquals(t, invalidChall.Error.Type, core.UnauthorizedProblem)
|
|
||||||
test.AssertEquals(t, len(log.GetAllMatching(`^\[AUDIT\] `)), 1)
|
|
||||||
|
|
||||||
log.Clear()
|
|
||||||
chall.Token = "no-content-type"
|
|
||||||
invalidChall, err = va.validateSimpleHTTP(ident, chall)
|
|
||||||
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
|
|
||||||
test.AssertError(t, err, "Error validating simpleHttp")
|
|
||||||
test.AssertEquals(t, invalidChall.Error.Type, core.UnauthorizedProblem)
|
|
||||||
test.AssertEquals(t, len(log.GetAllMatching(`^\[AUDIT\] `)), 1)
|
|
||||||
|
|
||||||
log.Clear()
|
log.Clear()
|
||||||
chall.Token = path404
|
chall.Token = path404
|
||||||
invalidChall, err = va.validateSimpleHTTP(ident, chall)
|
invalidChall, err = va.validateSimpleHTTP(ident, chall)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ package wfe
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
|
@ -302,9 +301,22 @@ const (
|
||||||
malformedJWS = "Unable to read/verify body"
|
malformedJWS = "Unable to read/verify body"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// verifyPOST reads and parses the request body, looks up the Registration
|
||||||
|
// corresponding to its JWK, verifies the JWS signature,
|
||||||
|
// checks that the resource field is present and correct in the JWS protected
|
||||||
|
// header, and returns the JWS payload bytes, the key used to verify, and the
|
||||||
|
// corresponding Registration (or error).
|
||||||
|
// If regCheck is false, verifyPOST will still try to look up a registration
|
||||||
|
// object, and will return it if found. However, if no registration object is
|
||||||
|
// found, verifyPOST will attempt to verify the JWS using the key in the JWS
|
||||||
|
// headers, and return the key plus a dummy registration if successful. If a
|
||||||
|
// caller passes regCheck = false, it should plan on validating the key itself.
|
||||||
func (wfe *WebFrontEndImpl) verifyPOST(request *http.Request, regCheck bool, resource core.AcmeResource) ([]byte, *jose.JsonWebKey, core.Registration, error) {
|
func (wfe *WebFrontEndImpl) verifyPOST(request *http.Request, regCheck bool, resource core.AcmeResource) ([]byte, *jose.JsonWebKey, core.Registration, error) {
|
||||||
var err error
|
var err error
|
||||||
var reg core.Registration
|
// TODO: We should return a pointer to a registration, which can be nil,
|
||||||
|
// rather the a registration value with a sentinel value.
|
||||||
|
// https://github.com/letsencrypt/boulder/issues/877
|
||||||
|
reg := core.Registration{ID: -1}
|
||||||
|
|
||||||
if _, ok := request.Header["Content-Length"]; !ok {
|
if _, ok := request.Header["Content-Length"]; !ok {
|
||||||
err = core.LengthRequiredError("Content-Length header is required for POST.")
|
err = core.LengthRequiredError("Content-Length header is required for POST.")
|
||||||
|
|
@ -351,7 +363,34 @@ func (wfe *WebFrontEndImpl) verifyPOST(request *http.Request, regCheck bool, res
|
||||||
wfe.log.Debug(err.Error())
|
wfe.log.Debug(err.Error())
|
||||||
return nil, nil, reg, err
|
return nil, nil, reg, err
|
||||||
}
|
}
|
||||||
key := parsedJws.Signatures[0].Header.JsonWebKey
|
submittedKey := parsedJws.Signatures[0].Header.JsonWebKey
|
||||||
|
if submittedKey == nil {
|
||||||
|
err = core.SignatureValidationError("No JWK in JWS header")
|
||||||
|
wfe.log.Debug(err.Error())
|
||||||
|
return nil, nil, reg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var key *jose.JsonWebKey
|
||||||
|
reg, err = wfe.SA.GetRegistrationByKey(*submittedKey)
|
||||||
|
// Special case: If no registration was found, but regCheck is false, use an
|
||||||
|
// empty registration and the submitted key. The caller is expected to do some
|
||||||
|
// validation on the returned key.
|
||||||
|
if _, ok := err.(core.NoSuchRegistrationError); ok && !regCheck {
|
||||||
|
// When looking up keys from the registrations DB, we can be confident they
|
||||||
|
// are "good". But when we are verifying against any submitted key, we want
|
||||||
|
// to check its quality before doing the verify.
|
||||||
|
if err = core.GoodKey(submittedKey.Key); err != nil {
|
||||||
|
return nil, nil, reg, err
|
||||||
|
}
|
||||||
|
key = submittedKey
|
||||||
|
} else if err != nil {
|
||||||
|
// For all other errors, or if regCheck is true, return error immediately.
|
||||||
|
return nil, nil, reg, err
|
||||||
|
} else {
|
||||||
|
// If the lookup was successful, use that key.
|
||||||
|
key = ®.Key
|
||||||
|
}
|
||||||
|
|
||||||
payload, header, err := parsedJws.Verify(key)
|
payload, header, err := parsedJws.Verify(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
puberr := core.SignatureValidationError("JWS verification error")
|
puberr := core.SignatureValidationError("JWS verification error")
|
||||||
|
|
@ -359,11 +398,6 @@ func (wfe *WebFrontEndImpl) verifyPOST(request *http.Request, regCheck bool, res
|
||||||
wfe.log.Debug(fmt.Sprintf("%v :: %v", puberr.Error(), err.Error()))
|
wfe.log.Debug(fmt.Sprintf("%v :: %v", puberr.Error(), err.Error()))
|
||||||
return nil, nil, reg, puberr
|
return nil, nil, reg, puberr
|
||||||
}
|
}
|
||||||
if key == nil {
|
|
||||||
err = core.SignatureValidationError("No JWK in JWS header")
|
|
||||||
wfe.log.Debug(err.Error())
|
|
||||||
return nil, nil, reg, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the request has a known anti-replay nonce
|
// Check that the request has a known anti-replay nonce
|
||||||
// i.e., Nonce is in protected header and
|
// i.e., Nonce is in protected header and
|
||||||
|
|
@ -377,18 +411,6 @@ func (wfe *WebFrontEndImpl) verifyPOST(request *http.Request, regCheck bool, res
|
||||||
return nil, nil, reg, err
|
return nil, nil, reg, err
|
||||||
}
|
}
|
||||||
|
|
||||||
reg, err = wfe.SA.GetRegistrationByKey(*key)
|
|
||||||
if err != nil {
|
|
||||||
// If we are requiring a valid registration, any failure to look up the
|
|
||||||
// registration is an overall failure to verify.
|
|
||||||
if regCheck {
|
|
||||||
return nil, nil, reg, err
|
|
||||||
}
|
|
||||||
// Otherwise we just return an empty registration. The caller is expected
|
|
||||||
// to use the returned key instead.
|
|
||||||
reg = core.Registration{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the "resource" field is present and has the correct value
|
// Check that the "resource" field is present and has the correct value
|
||||||
var parsedRequest struct {
|
var parsedRequest struct {
|
||||||
Resource string `json:"resource"`
|
Resource string `json:"resource"`
|
||||||
|
|
@ -543,7 +565,7 @@ func (wfe *WebFrontEndImpl) NewAuthorization(response http.ResponseWriter, reque
|
||||||
logEvent.Error = err.Error()
|
logEvent.Error = err.Error()
|
||||||
respMsg := malformedJWS
|
respMsg := malformedJWS
|
||||||
respCode := statusCodeFromError(err)
|
respCode := statusCodeFromError(err)
|
||||||
if err == sql.ErrNoRows {
|
if _, ok := err.(core.NoSuchRegistrationError); ok {
|
||||||
respMsg = unknownKey
|
respMsg = unknownKey
|
||||||
respCode = http.StatusForbidden
|
respCode = http.StatusForbidden
|
||||||
}
|
}
|
||||||
|
|
@ -713,7 +735,7 @@ func (wfe *WebFrontEndImpl) NewCertificate(response http.ResponseWriter, request
|
||||||
logEvent.Error = err.Error()
|
logEvent.Error = err.Error()
|
||||||
respMsg := malformedJWS
|
respMsg := malformedJWS
|
||||||
respCode := statusCodeFromError(err)
|
respCode := statusCodeFromError(err)
|
||||||
if err == sql.ErrNoRows {
|
if _, ok := err.(core.NoSuchRegistrationError); ok {
|
||||||
respMsg = unknownKey
|
respMsg = unknownKey
|
||||||
respCode = http.StatusForbidden
|
respCode = http.StatusForbidden
|
||||||
}
|
}
|
||||||
|
|
@ -911,7 +933,7 @@ func (wfe *WebFrontEndImpl) postChallenge(
|
||||||
logEvent.Error = err.Error()
|
logEvent.Error = err.Error()
|
||||||
respMsg := malformedJWS
|
respMsg := malformedJWS
|
||||||
respCode := http.StatusBadRequest
|
respCode := http.StatusBadRequest
|
||||||
if err == sql.ErrNoRows {
|
if _, ok := err.(core.NoSuchRegistrationError); ok {
|
||||||
respMsg = unknownKey
|
respMsg = unknownKey
|
||||||
respCode = http.StatusForbidden
|
respCode = http.StatusForbidden
|
||||||
}
|
}
|
||||||
|
|
@ -987,7 +1009,7 @@ func (wfe *WebFrontEndImpl) Registration(response http.ResponseWriter, request *
|
||||||
logEvent.Error = err.Error()
|
logEvent.Error = err.Error()
|
||||||
respMsg := malformedJWS
|
respMsg := malformedJWS
|
||||||
respCode := statusCodeFromError(err)
|
respCode := statusCodeFromError(err)
|
||||||
if err == sql.ErrNoRows {
|
if _, ok := err.(core.NoSuchRegistrationError); ok {
|
||||||
respMsg = unknownKey
|
respMsg = unknownKey
|
||||||
respCode = http.StatusForbidden
|
respCode = http.StatusForbidden
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -184,11 +184,10 @@ func makeBody(s string) io.ReadCloser {
|
||||||
}
|
}
|
||||||
|
|
||||||
func signRequest(t *testing.T, req string, nonceService *core.NonceService) string {
|
func signRequest(t *testing.T, req string, nonceService *core.NonceService) string {
|
||||||
accountKeyJSON := []byte(`{"kty":"RSA","n":"z2NsNdHeqAiGdPP8KuxfQXat_uatOK9y12SyGpfKw1sfkizBIsNxERjNDke6Wp9MugN9srN3sr2TDkmQ-gK8lfWo0v1uG_QgzJb1vBdf_hH7aejgETRGLNJZOdaKDsyFnWq1WGJq36zsHcd0qhggTk6zVwqczSxdiWIAZzEakIUZ13KxXvoepYLY0Q-rEEQiuX71e4hvhfeJ4l7m_B-awn22UUVvo3kCqmaRlZT-36vmQhDGoBsoUo1KBEU44jfeK5PbNRk7vDJuH0B7qinr_jczHcvyD-2TtPzKaCioMtNh_VZbPNDaG67sYkQlC15-Ff3HPzKKJW2XvkVG91qMvQ","e":"AQAB","d":"BhAmDbzBAbCeHbU0Xhzi_Ar4M0eTMOEQPnPXMSfW6bc0SRW938JO_-z1scEvFY8qsxV_C0Zr7XHVZsmHz4dc9BVmhiSan36XpuOS85jLWaY073e7dUVN9-l-ak53Ys9f6KZB_v-BmGB51rUKGB70ctWiMJ1C0EzHv0h6Moog-LCd_zo03uuZD5F5wtnPrAB3SEM3vRKeZHzm5eiGxNUsaCEzGDApMYgt6YkQuUlkJwD8Ky2CkAE6lLQSPwddAfPDhsCug-12SkSIKw1EepSHz86ZVfJEnvY-h9jHIdI57mR1v7NTCDcWqy6c6qIzxwh8n2X94QTbtWT3vGQ6HXM5AQ","p":"2uhvZwNS5i-PzeI9vGx89XbdsVmeNjVxjH08V3aRBVY0dzUzwVDYk3z7sqBIj6de53Lx6W1hjmhPIqAwqQgjIKH5Z3uUCinGguKkfGDL3KgLCzYL2UIvZMvTzr9NWLc0AHMZdee5utxWKCGnZBOqy1Rd4V-6QrqjEDBvanoqA60","q":"8odNkMEiriaDKmvwDv-vOOu3LaWbu03yB7VhABu-hK5Xx74bHcvDP2HuCwDGGJY2H-xKdMdUPs0HPwbfHMUicD2vIEUDj6uyrMMZHtbcZ3moh3-WESg3TaEaJ6vhwcWXWG7Wc46G-HbCChkuVenFYYkoi68BAAjloqEUl1JBT1E"}`)
|
accountKey, err := jose.LoadPrivateKey([]byte(test1KeyPrivatePEM))
|
||||||
var accountKey jose.JsonWebKey
|
test.AssertNotError(t, err, "Failed to load key")
|
||||||
err := json.Unmarshal(accountKeyJSON, &accountKey)
|
|
||||||
test.AssertNotError(t, err, "Failed to unmarshal key")
|
signer, err := jose.NewSigner("RS256", accountKey)
|
||||||
signer, err := jose.NewSigner("RS256", &accountKey)
|
|
||||||
test.AssertNotError(t, err, "Failed to make signer")
|
test.AssertNotError(t, err, "Failed to make signer")
|
||||||
nonce, err := nonceService.Nonce()
|
nonce, err := nonceService.Nonce()
|
||||||
test.AssertNotError(t, err, "Failed to make nonce")
|
test.AssertNotError(t, err, "Failed to make nonce")
|
||||||
|
|
@ -769,6 +768,18 @@ func makeRevokeRequestJSON() ([]byte, error) {
|
||||||
return revokeRequestJSON, nil
|
return revokeRequestJSON, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An SA mock that always returns NoSuchRegistrationError. This is necessary
|
||||||
|
// because the standard mock in our mocks package always returns a given test
|
||||||
|
// registration when GetRegistrationByKey is called, and we want to get a
|
||||||
|
// NoSuchRegistrationError for tests that pass regCheck = false to verifyPOST.
|
||||||
|
type mockSANoSuchRegistration struct {
|
||||||
|
mocks.MockSA
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msa mockSANoSuchRegistration) GetRegistrationByKey(jwk jose.JsonWebKey) (core.Registration, error) {
|
||||||
|
return core.Registration{}, core.NoSuchRegistrationError("reg not found")
|
||||||
|
}
|
||||||
|
|
||||||
// Valid revocation request for existing, non-revoked cert, signed with cert
|
// Valid revocation request for existing, non-revoked cert, signed with cert
|
||||||
// key.
|
// key.
|
||||||
func TestRevokeCertificateCertKey(t *testing.T) {
|
func TestRevokeCertificateCertKey(t *testing.T) {
|
||||||
|
|
@ -785,6 +796,7 @@ func TestRevokeCertificateCertKey(t *testing.T) {
|
||||||
test.AssertNotError(t, err, "Failed to make revokeRequestJSON")
|
test.AssertNotError(t, err, "Failed to make revokeRequestJSON")
|
||||||
|
|
||||||
wfe := setupWFE(t)
|
wfe := setupWFE(t)
|
||||||
|
wfe.SA = &mockSANoSuchRegistration{mocks.MockSA{}}
|
||||||
responseWriter := httptest.NewRecorder()
|
responseWriter := httptest.NewRecorder()
|
||||||
|
|
||||||
nonce, err := wfe.nonceService.Nonce()
|
nonce, err := wfe.nonceService.Nonce()
|
||||||
|
|
@ -874,7 +886,7 @@ func TestRevokeCertificateAlreadyRevoked(t *testing.T) {
|
||||||
wfe := setupWFE(t)
|
wfe := setupWFE(t)
|
||||||
|
|
||||||
wfe.RA = &MockRegistrationAuthority{}
|
wfe.RA = &MockRegistrationAuthority{}
|
||||||
wfe.SA = &mocks.MockSA{}
|
wfe.SA = &mockSANoSuchRegistration{mocks.MockSA{}}
|
||||||
wfe.stats, _ = statsd.NewNoopClient()
|
wfe.stats, _ = statsd.NewNoopClient()
|
||||||
wfe.SubscriberAgreementURL = agreementURL
|
wfe.SubscriberAgreementURL = agreementURL
|
||||||
responseWriter := httptest.NewRecorder()
|
responseWriter := httptest.NewRecorder()
|
||||||
|
|
@ -1206,6 +1218,29 @@ func TestLengthRequired(t *testing.T) {
|
||||||
test.Assert(t, ok, "Error code for missing content-length wasn't 411.")
|
test.Assert(t, ok, "Error code for missing content-length wasn't 411.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mockSADifferentStoredKey struct {
|
||||||
|
mocks.MockSA
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sa mockSADifferentStoredKey) GetRegistrationByKey(jwk jose.JsonWebKey) (core.Registration, error) {
|
||||||
|
keyJSON := []byte(test2KeyPublicJSON)
|
||||||
|
var parsedKey jose.JsonWebKey
|
||||||
|
parsedKey.UnmarshalJSON(keyJSON)
|
||||||
|
|
||||||
|
return core.Registration{
|
||||||
|
Key: parsedKey,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyPOSTUsesStoredKey(t *testing.T) {
|
||||||
|
wfe := setupWFE(t)
|
||||||
|
wfe.SA = &mockSADifferentStoredKey{mocks.MockSA{}}
|
||||||
|
// signRequest signs with test1Key, but our special mock returns a
|
||||||
|
// registration with test2Key
|
||||||
|
_, _, _, err := wfe.verifyPOST(makePostRequest(signRequest(t, `{"resource":"foo"}`, &wfe.nonceService)), true, "foo")
|
||||||
|
test.AssertError(t, err, "No error returned when provided key differed from stored key.")
|
||||||
|
}
|
||||||
|
|
||||||
func TestBadKeyCSR(t *testing.T) {
|
func TestBadKeyCSR(t *testing.T) {
|
||||||
wfe := setupWFE(t)
|
wfe := setupWFE(t)
|
||||||
responseWriter := httptest.NewRecorder()
|
responseWriter := httptest.NewRecorder()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue