Merge master

This commit is contained in:
Roland Shoemaker 2015-08-25 16:52:24 -07:00
commit be751bd948
54 changed files with 1460 additions and 657 deletions

View File

@ -1,7 +1,7 @@
language: go
go:
- 1.4.1
- 1.5
addons:
hosts:
@ -43,6 +43,7 @@ before_install:
- go get github.com/mattn/goveralls
- go get github.com/modocache/gover
- go get github.com/jcjones/github-pr-status
- go get bitbucket.org/liamstask/goose/cmd/goose
# Boulder consists of multiple Go packages, which
# refer to each other by their absolute GitHub path,

24
Godeps/Godeps.json generated
View File

@ -12,51 +12,51 @@
},
{
"ImportPath": "github.com/cloudflare/cfssl/auth",
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
"Rev": "190c5f9713ef6c1460fb31ee785044b43bdb1b09"
},
{
"ImportPath": "github.com/cloudflare/cfssl/config",
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
"Rev": "190c5f9713ef6c1460fb31ee785044b43bdb1b09"
},
{
"ImportPath": "github.com/cloudflare/cfssl/crypto/pkcs11key",
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
"Rev": "190c5f9713ef6c1460fb31ee785044b43bdb1b09"
},
{
"ImportPath": "github.com/cloudflare/cfssl/crypto/pkcs12",
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
"Rev": "190c5f9713ef6c1460fb31ee785044b43bdb1b09"
},
{
"ImportPath": "github.com/cloudflare/cfssl/crypto/pkcs7",
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
"Rev": "190c5f9713ef6c1460fb31ee785044b43bdb1b09"
},
{
"ImportPath": "github.com/cloudflare/cfssl/csr",
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
"Rev": "190c5f9713ef6c1460fb31ee785044b43bdb1b09"
},
{
"ImportPath": "github.com/cloudflare/cfssl/errors",
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
"Rev": "190c5f9713ef6c1460fb31ee785044b43bdb1b09"
},
{
"ImportPath": "github.com/cloudflare/cfssl/helpers",
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
"Rev": "190c5f9713ef6c1460fb31ee785044b43bdb1b09"
},
{
"ImportPath": "github.com/cloudflare/cfssl/info",
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
"Rev": "190c5f9713ef6c1460fb31ee785044b43bdb1b09"
},
{
"ImportPath": "github.com/cloudflare/cfssl/log",
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
"Rev": "190c5f9713ef6c1460fb31ee785044b43bdb1b09"
},
{
"ImportPath": "github.com/cloudflare/cfssl/ocsp",
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
"Rev": "190c5f9713ef6c1460fb31ee785044b43bdb1b09"
},
{
"ImportPath": "github.com/cloudflare/cfssl/signer",
"Rev": "e46a042fbff1afcb445a5164d392ab2bf1b938be"
"Rev": "190c5f9713ef6c1460fb31ee785044b43bdb1b09"
},
{
"ImportPath": "github.com/codegangsta/cli",

View File

@ -27,6 +27,20 @@ const OneYear = 8760 * time.Hour
// OneDay is a time.Duration representing a day's worth of seconds.
const OneDay = 24 * time.Hour
// InclusiveDate returns the time.Time representation of a date - 1
// nanosecond. This allows time.After to be used inclusively.
func InclusiveDate(year int, month time.Month, day int) time.Time {
return time.Date(year, month, day, 0, 0, 0, 0, time.UTC).Add(-1 * time.Nanosecond)
}
// Jul2012 is the July 2012 CAB Forum deadline for when CAs must stop
// issuing certificates valid for more than 5 years.
var Jul2012 = InclusiveDate(2012, time.July, 01)
// April2015 is the April 2015 CAB Forum deadline for when CAs must stop
// issuing certificates valid for more than 39 months.
var Apr2015 = InclusiveDate(2015, time.April, 01)
// KeyLength returns the bit size of ECDSA or RSA PublicKey
func KeyLength(key interface{}) int {
if key == nil {
@ -55,6 +69,42 @@ func ExpiryTime(chain []*x509.Certificate) *time.Time {
return &notAfter
}
// MonthsValid returns the number of months for which a certificate is valid.
func MonthsValid(c *x509.Certificate) int {
issued := c.NotBefore
expiry := c.NotAfter
years := (expiry.Year() - issued.Year())
months := years*12 + int(expiry.Month()) - int(issued.Month())
// Round up if valid for less than a full month
if expiry.Day() > issued.Day() {
months++
}
return months
}
// ValidExpiry determines if a certificate is valid for an acceptable
// length of time per the CA/Browser Forum baseline requirements.
// See https://cabforum.org/wp-content/uploads/CAB-Forum-BR-1.3.0.pdf
func ValidExpiry(c *x509.Certificate) bool {
issued := c.NotBefore
var maxMonths int
switch {
case issued.After(Apr2015):
maxMonths = 39
case issued.After(Jul2012):
maxMonths = 60
case issued.Before(Jul2012):
maxMonths = 120
}
if MonthsValid(c) > maxMonths {
return false
}
return true
}
// SignatureString returns the TLS signature string corresponding to
// an X509 signature algorithm.
func SignatureString(alg x509.SignatureAlgorithm) string {
@ -157,7 +207,6 @@ func ParseCertificatesDER(certsDER []byte, password string) ([]*x509.Certificate
if err != nil {
certs, err = x509.ParseCertificates(certsDER)
if err != nil {
//fmt.Println("\n\n\n\n\n\nCRITICALZONE\n\n\n\n\n\n\n\n\n\n")
return nil, nil, cferr.New(cferr.CertificateError, cferr.DecodeFailed)
}
} else {
@ -166,7 +215,7 @@ func ParseCertificatesDER(certsDER []byte, password string) ([]*x509.Certificate
}
} else {
if pkcs7data.ContentInfo != "SignedData" {
return nil, nil, cferr.Wrap(cferr.CertificateError, cferr.DecodeFailed, errors.New("Can only extract certificates from signed data content info"))
return nil, nil, cferr.Wrap(cferr.CertificateError, cferr.DecodeFailed, errors.New("can only extract certificates from signed data content info"))
}
certs = pkcs7data.Content.SignedData.Certificates
}
@ -200,9 +249,9 @@ func ParseCertificatePEM(certPEM []byte) (*x509.Certificate, error) {
} else if cert == nil {
return nil, cferr.New(cferr.CertificateError, cferr.DecodeFailed)
} else if len(rest) > 0 {
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, errors.New("The PEM file should contain only one object."))
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, errors.New("the PEM file should contain only one object"))
} else if len(cert) > 1 {
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, errors.New("The PKCS7 object in the PEM file should contain only one certificate"))
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, errors.New("the PKCS7 object in the PEM file should contain only one certificate"))
}
return cert[0], nil
}
@ -225,7 +274,7 @@ func ParseOneCertificateFromPEM(certsPEM []byte) ([]*x509.Certificate, []byte, e
return nil, rest, err
}
if pkcs7data.ContentInfo != "SignedData" {
return nil, rest, errors.New("Only PKCS #7 Signed Data Content Info supported for certificate parsing")
return nil, rest, errors.New("only PKCS #7 Signed Data Content Info supported for certificate parsing")
}
certs := pkcs7data.Content.SignedData.Certificates
if certs == nil {

View File

@ -111,7 +111,58 @@ func TestExpiryTime(t *testing.T) {
if *out != expected {
t.Fatalf("Expected %v, got %v", expected, *out)
}
}
func TestMonthsValid(t *testing.T) {
var cert = &x509.Certificate{
NotBefore: time.Date(2015, time.April, 01, 0, 0, 0, 0, time.UTC),
NotAfter: time.Date(2015, time.April, 01, 0, 0, 0, 0, time.UTC),
}
if MonthsValid(cert) != 0 {
t.Fail()
}
cert.NotAfter = time.Date(2016, time.April, 01, 0, 0, 0, 0, time.UTC)
if MonthsValid(cert) != 12 {
t.Fail()
}
// extra days should be rounded up to 1 month
cert.NotAfter = time.Date(2016, time.April, 02, 0, 0, 0, 0, time.UTC)
if MonthsValid(cert) != 13 {
t.Fail()
}
}
func HasValidExpiry(t *testing.T) {
// Issue period > April 1, 2015
var cert = &x509.Certificate{
NotBefore: time.Date(2015, time.April, 01, 0, 0, 0, 0, time.UTC),
NotAfter: time.Date(2016, time.April, 01, 0, 0, 0, 0, time.UTC),
}
if !ValidExpiry(cert) {
t.Fail()
}
cert.NotAfter = time.Date(2019, time.April, 01, 01, 0, 0, 0, time.UTC)
if ValidExpiry(cert) {
t.Fail()
}
// Issue period < July 1, 2012
cert.NotBefore = time.Date(2009, time.March, 01, 0, 0, 0, 0, time.UTC)
if ValidExpiry(cert) {
t.Fail()
}
// Issue period July 1, 2012 - April 1, 2015
cert.NotBefore = time.Date(2012, time.July, 01, 0, 0, 0, 0, time.UTC)
cert.NotAfter = time.Date(2017, time.July, 01, 0, 0, 0, 0, time.UTC)
if !ValidExpiry(cert) {
t.Fail()
}
}
func TestHashAlgoString(t *testing.T) {

View File

@ -8,6 +8,7 @@ package log
import (
"fmt"
golog "log"
"os"
)
// The following constants represent logging levels in increasing levels of seriousness.
@ -17,6 +18,7 @@ const (
LevelWarning
LevelError
LevelCritical
LevelFatal
)
var levelPrefix = [...]string{
@ -25,6 +27,7 @@ var levelPrefix = [...]string{
LevelWarning: "[WARNING] ",
LevelError: "[ERROR] ",
LevelCritical: "[CRITICAL] ",
LevelFatal: "[FATAL] ",
}
// Level stores the current logging level.
@ -42,6 +45,19 @@ func output(l int, v []interface{}) {
}
}
// Fatalf logs a formatted message at the "fatal" level and then exits. The
// arguments are handled in the same manner as fmt.Printf.
func Fatalf(format string, v ...interface{}) {
outputf(LevelFatal, format, v)
os.Exit(1)
}
// Fatal logs its arguments at the "fatal" level and then exits.
func Fatal(v ...interface{}) {
output(LevelFatal, v)
os.Exit(1)
}
// Criticalf logs a formatted message at the "critical" level. The
// arguments are handled in the same manner as fmt.Printf.
func Criticalf(format string, v ...interface{}) {

View File

@ -118,8 +118,8 @@ func (s StandardSigner) Sign(req SignRequest) ([]byte, error) {
return nil, cferr.New(cferr.OCSPError, cferr.IssuerMismatch)
}
// Round thisUpdate times to the nearest hour
thisUpdate := time.Now().Round(time.Hour)
// Round thisUpdate times down to the nearest hour
thisUpdate := time.Now().Truncate(time.Hour)
nextUpdate := thisUpdate.Add(s.interval)
status, ok := statusCode[req.Status]

View File

@ -9,7 +9,7 @@ VERSION ?= 1.0.0
EPOCH ?= 1
MAINTAINER ?= "Community"
OBJECTS = $(shell find ./cmd -type d -maxdepth 1 -mindepth 1 | xargs basename)
OBJECTS = $(shell find ./cmd -maxdepth 1 -mindepth 1 -type d -exec basename '{}' \;)
# Build environment variables (referencing core/util.go)
COMMIT_ID = $(shell git rev-parse --short HEAD)

View File

@ -45,34 +45,41 @@ To run a single module, specifying the AMQP server, you might use something more
Quickstart
----------
Install RabbitMQ from https://rabbitmq.com/download.html. It's required to run
tests.
Install libtool-ltdl dev libraries, which are required for Boulder's PKCS11
support.
Boulder requires an installation of RabbitMQ, libtool-ltdl, and
MariaDB 10 to work correctly. On Ubuntu and CentOS, you may have to
install RabbitMQ from https://rabbitmq.com/download.html to get a
recent version.
Ubuntu:
`sudo apt-get install libltdl3-dev`
sudo apt-get install libltdl3-dev mariadb-server rabbitmq-server
CentOS:
`sudo yum install libtool-ltdl-devel`
sudo yum install libtool-ltdl-devel MariaDB-server MariaDB-client rabbitmq-server
OS X:
`sudo port install libtool` or `brew install libtool`
brew install libtool mariadb rabbitmq
or
sudo port install libtool mariadb-server rabbitmq-server
(On OS X, using port, you will have to add `CGO_CFLAGS="-I/opt/local/include" CGO_LDFLAGS="-L/opt/local/lib"` to your environment or `go` invocations.)
```
> go get github.com/letsencrypt/boulder # Ignore errors about no buildable files
> go get github.com/letsencrypt/boulder/ # Ignore errors about no buildable files
> cd $GOPATH/src/github.com/letsencrypt/boulder
# This starts each Boulder component with test configs. Ctrl-C kills all.
> python ./start.py
> cd test/js
> npm install
> nodejs test.js
> ./test.sh
```
The databases that boulder requires to operate in development and
testing can be created using test/create\_db.sh. It uses the root
MariaDB user, so if you have disabled that account you may have to
adjust the file or recreate the commands.
You can also check out the official client from
https://github.com/letsencrypt/lets-encrypt-preview/ and follow the setup
instructions there.

9
ca/_db/dbconf.yml Normal file
View File

@ -0,0 +1,9 @@
development:
driver: mysql
open: boulder@tcp(localhost:3306)/boulder_ca_development
test:
driver: mysql
open: boulder@tcp(localhost:3306)/boulder_ca_test
integration:
driver: mysql
open: boulder@tcp(localhost:3306)/boulder_ca_integration

View File

@ -0,0 +1,23 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE `serialNumber` (
`id` int(11) DEFAULT NULL,
`number` int(11) DEFAULT NULL,
`lastUpdated` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `serialNumber`
(`id`,
`number`,
`lastUpdated`)
VALUES (1,
1,
now()
);
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back
DROP TABLE `serialNumber`

View File

@ -9,7 +9,6 @@ import (
"fmt"
"time"
"github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log"
gorp "github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
@ -31,7 +30,7 @@ type SerialNumber struct {
// NewCertificateAuthorityDatabaseImpl constructs a Database for the
// Certificate Authority.
func NewCertificateAuthorityDatabaseImpl(dbMap *gorp.DbMap) (cadb core.CertificateAuthorityDatabase, err error) {
func NewCertificateAuthorityDatabaseImpl(dbMap *gorp.DbMap) (cadb *CertificateAuthorityDatabaseImpl, err error) {
logger := blog.GetAuditLogger()
dbMap.AddTableWithName(SerialNumber{}, "serialNumber").SetKeys(true, "ID")
@ -43,21 +42,6 @@ func NewCertificateAuthorityDatabaseImpl(dbMap *gorp.DbMap) (cadb core.Certifica
return cadb, nil
}
// CreateTablesIfNotExists builds the database tables and inserts the initial
// state, if the tables do not already exist. It is not an error for the tables
// to already exist.
func (cadb *CertificateAuthorityDatabaseImpl) CreateTablesIfNotExists() (err error) {
// Create serial number table
err = cadb.dbMap.CreateTablesIfNotExists()
if err != nil {
return
}
// Initialize the serial number
err = cadb.dbMap.Insert(&SerialNumber{ID: 1, Number: 1, LastUpdated: time.Now()})
return
}
// Begin starts a transaction at the GORP wrapper.
func (cadb *CertificateAuthorityDatabaseImpl) Begin() (*gorp.Transaction, error) {
return cadb.dbMap.Begin()

View File

@ -7,8 +7,8 @@ package ca
import (
"testing"
"time"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/sa"
"github.com/letsencrypt/boulder/test"
)
@ -46,8 +46,8 @@ func TestGetSetSequenceNumber(t *testing.T) {
test.AssertNotError(t, err, "Could not commit")
}
func caDBImpl(t *testing.T) (core.CertificateAuthorityDatabase, func()) {
dbMap, err := sa.NewDbMap(dbConnStr)
func caDBImpl(t *testing.T) (*CertificateAuthorityDatabaseImpl, func()) {
dbMap, err := sa.NewDbMap(caDBConnStr)
if err != nil {
t.Fatalf("Could not construct dbMap: %s", err)
}
@ -57,33 +57,16 @@ func caDBImpl(t *testing.T) (core.CertificateAuthorityDatabase, func()) {
t.Fatalf("Could not construct CA DB: %s", err)
}
// We intentionally call CreateTablesIfNotExists twice before
// returning because of the weird insert inside it. The
// CADatabaseImpl code expects the existence of a single row in
// its serialIds table or else it errors. CreateTablesIfNotExists
// currently inserts that row and TruncateTables will remove
// it. But we need to make sure the tables exist before
// TruncateTables can be called to reset the table. So, two calls
// to CreateTablesIfNotExists.
cleanUp := test.ResetTestDatabase(t, dbMap.Db)
err = cadb.CreateTablesIfNotExists()
// This row is required to exist for caDBImpl to work correctly. We
// can no longer use dbMap.Insert(&SerialNumber{...}) for this
// because gorp will ignore the ID and insert a new row at a new
// autoincrement id.
// TODO(jmhodges): gen ids flickr-style, no row needed a head of time
_, err = dbMap.Db.Exec("insert into serialNumber (id, number, lastUpdated) VALUES (?, ?, ?)", 1, 1, time.Now())
if err != nil {
t.Fatalf("Could not construct tables: %s", err)
t.Fatalf("unable to create the serial number row: %s", err)
}
err = dbMap.TruncateTables()
if err != nil {
t.Fatalf("Could not truncate tables: %s", err)
}
err = cadb.CreateTablesIfNotExists()
if err != nil {
t.Fatalf("Could not construct tables: %s", err)
}
cleanUp := func() {
if err := dbMap.TruncateTables(); err != nil {
t.Fatalf("Could not truncate tables after the test: %s", err)
}
dbMap.Db.Close()
}
return cadb, cleanUp
}

View File

@ -18,6 +18,7 @@ import (
ocspConfig "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/ocsp/config"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/mocks"
"github.com/letsencrypt/boulder/sa/satest"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/sa"
@ -333,22 +334,32 @@ var FarPast = time.Date(1950, 1, 1, 0, 0, 0, 0, time.UTC)
var log = mocks.UseMockLog()
var exPA = cmd.PAConfig{
DBConnect: paDBConnStr,
}
// CFSSL config
const profileName = "ee"
const caKeyFile = "../test/test-ca.key"
const caCertFile = "../test/test-ca.pem"
const issuerCert = "../test/test-ca.pem"
const (
paDBConnStr = "mysql+tcp://boulder@localhost:3306/boulder_pa_test"
caDBConnStr = "mysql+tcp://boulder@localhost:3306/boulder_ca_test"
saDBConnStr = "mysql+tcp://boulder@localhost:3306/boulder_sa_test"
)
// TODO(jmhodges): change this to boulder_ca_test database
var dbConnStr = "mysql+tcp://boulder@localhost:3306/boulder_test"
var exPA = cmd.PAConfig{
DBConnect: dbConnStr,
type testCtx struct {
caDB core.CertificateAuthorityDatabase
sa core.StorageAuthority
caConfig cmd.CAConfig
reg core.Registration
cleanUp func()
}
func setup(t *testing.T) (core.CertificateAuthorityDatabase, core.StorageAuthority, cmd.CAConfig, func()) {
func setup(t *testing.T) *testCtx {
// Create an SA
dbMap, err := sa.NewDbMap(dbConnStr)
dbMap, err := sa.NewDbMap(saDBConnStr)
if err != nil {
t.Fatalf("Failed to create dbMap: %s", err)
}
@ -356,22 +367,16 @@ func setup(t *testing.T) (core.CertificateAuthorityDatabase, core.StorageAuthori
if err != nil {
t.Fatalf("Failed to create SA: %s", err)
}
if err = ssa.CreateTablesIfNotExists(); err != nil {
t.Fatalf("Failed to create tables: %s", err)
}
if err = dbMap.TruncateTables(); err != nil {
t.Fatalf("Failed to truncate tables: %s", err)
}
saDBCleanUp := test.ResetTestDatabase(t, dbMap.Db)
cadb, caDBCleanUp := caDBImpl(t)
cleanUp := func() {
if err = dbMap.TruncateTables(); err != nil {
t.Fatalf("Failed to truncate tables after the test: %s", err)
}
dbMap.Db.Close()
saDBCleanUp()
caDBCleanUp()
}
// TODO(jmhodges): use of this pkg here is a bug caused by using a real SA
reg := satest.CreateWorkingRegistration(t, ssa)
// Create a CA
caConfig := cmd.CAConfig{
Profile: profileName,
@ -412,38 +417,38 @@ func setup(t *testing.T) (core.CertificateAuthorityDatabase, core.StorageAuthori
},
},
OCSP: &ocspConfig.Config{
CACertFile: issuerCert,
ResponderCertFile: issuerCert,
CACertFile: caCertFile,
ResponderCertFile: caCertFile,
KeyFile: caKeyFile,
},
},
}
return cadb, ssa, caConfig, cleanUp
return &testCtx{cadb, ssa, caConfig, reg, cleanUp}
}
func TestFailNoSerial(t *testing.T) {
cadb, _, caConfig, cleanUp := setup(t)
defer cleanUp()
ctx := setup(t)
defer ctx.cleanUp()
caConfig.SerialPrefix = 0
_, err := NewCertificateAuthorityImpl(cadb, caConfig, issuerCert, exPA)
ctx.caConfig.SerialPrefix = 0
_, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, caCertFile, exPA)
test.AssertError(t, err, "CA should have failed with no SerialPrefix")
}
func TestRevoke(t *testing.T) {
cadb, storageAuthority, caConfig, cleanUp := setup(t)
defer cleanUp()
ca, err := NewCertificateAuthorityImpl(cadb, caConfig, caCertFile, exPA)
ctx := setup(t)
defer ctx.cleanUp()
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, caCertFile, exPA)
test.AssertNotError(t, err, "Failed to create CA")
if err != nil {
return
}
ca.SA = storageAuthority
ca.SA = ctx.sa
ca.MaxKeySize = 4096
csrDER, _ := hex.DecodeString(CNandSANCSRhex)
csr, _ := x509.ParseCertificateRequest(csrDER)
certObj, err := ca.IssueCertificate(*csr, 1, FarFuture)
certObj, err := ca.IssueCertificate(*csr, ctx.reg.ID, FarFuture)
test.AssertNotError(t, err, "Failed to sign certificate")
if err != nil {
return
@ -454,7 +459,7 @@ func TestRevoke(t *testing.T) {
err = ca.RevokeCertificate(serialString, 0)
test.AssertNotError(t, err, "Revocation failed")
status, err := storageAuthority.GetCertificateStatus(serialString)
status, err := ctx.sa.GetCertificateStatus(serialString)
test.AssertNotError(t, err, "Failed to get cert status")
test.AssertEquals(t, status.Status, core.OCSPStatusRevoked)
@ -464,11 +469,11 @@ func TestRevoke(t *testing.T) {
}
func TestIssueCertificate(t *testing.T) {
cadb, storageAuthority, caConfig, cleanUp := setup(t)
defer cleanUp()
ca, err := NewCertificateAuthorityImpl(cadb, caConfig, caCertFile, exPA)
ctx := setup(t)
defer ctx.cleanUp()
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, caCertFile, exPA)
test.AssertNotError(t, err, "Failed to create CA")
ca.SA = storageAuthority
ca.SA = ctx.sa
ca.MaxKeySize = 4096
/*
@ -486,7 +491,7 @@ func TestIssueCertificate(t *testing.T) {
csr, _ := x509.ParseCertificateRequest(csrDER)
// Sign CSR
issuedCert, err := ca.IssueCertificate(*csr, 1, FarFuture)
issuedCert, err := ca.IssueCertificate(*csr, ctx.reg.ID, FarFuture)
test.AssertNotError(t, err, "Failed to sign certificate")
if err != nil {
continue
@ -527,12 +532,12 @@ func TestIssueCertificate(t *testing.T) {
// Verify that the cert got stored in the DB
serialString := core.SerialToString(cert.SerialNumber)
storedCert, err := storageAuthority.GetCertificate(serialString)
storedCert, err := ctx.sa.GetCertificate(serialString)
test.AssertNotError(t, err,
fmt.Sprintf("Certificate %s not found in database", serialString))
test.Assert(t, bytes.Equal(issuedCert.DER, storedCert.DER), "Retrieved cert not equal to issued cert.")
certStatus, err := storageAuthority.GetCertificateStatus(serialString)
certStatus, err := ctx.sa.GetCertificateStatus(serialString)
test.AssertNotError(t, err,
fmt.Sprintf("Error fetching status for certificate %s", serialString))
test.Assert(t, certStatus.Status == core.OCSPStatusGood, "Certificate status was not good")
@ -541,48 +546,48 @@ func TestIssueCertificate(t *testing.T) {
}
func TestRejectNoName(t *testing.T) {
cadb, storageAuthority, caConfig, cleanUp := setup(t)
defer cleanUp()
ca, err := NewCertificateAuthorityImpl(cadb, caConfig, caCertFile, exPA)
ctx := setup(t)
defer ctx.cleanUp()
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, caCertFile, exPA)
test.AssertNotError(t, err, "Failed to create CA")
ca.SA = storageAuthority
ca.SA = ctx.sa
ca.MaxKeySize = 4096
// Test that the CA rejects CSRs with no names
csrDER, _ := hex.DecodeString(NoNameCSRhex)
csr, _ := x509.ParseCertificateRequest(csrDER)
_, err = ca.IssueCertificate(*csr, 1, FarFuture)
_, err = ca.IssueCertificate(*csr, ctx.reg.ID, FarFuture)
if err == nil {
t.Errorf("CA improperly agreed to create a certificate with no name")
}
}
func TestRejectTooManyNames(t *testing.T) {
cadb, storageAuthority, caConfig, cleanUp := setup(t)
defer cleanUp()
ca, err := NewCertificateAuthorityImpl(cadb, caConfig, caCertFile, exPA)
ctx := setup(t)
defer ctx.cleanUp()
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, caCertFile, exPA)
test.AssertNotError(t, err, "Failed to create CA")
ca.SA = storageAuthority
ca.SA = ctx.sa
// Test that the CA rejects a CSR with too many names
csrDER, _ := hex.DecodeString(TooManyNameCSRhex)
csr, _ := x509.ParseCertificateRequest(csrDER)
_, err = ca.IssueCertificate(*csr, 1, FarFuture)
_, err = ca.IssueCertificate(*csr, ctx.reg.ID, FarFuture)
test.Assert(t, err != nil, "Issued certificate with too many names")
}
func TestDeduplication(t *testing.T) {
cadb, storageAuthority, caConfig, cleanUp := setup(t)
defer cleanUp()
ca, err := NewCertificateAuthorityImpl(cadb, caConfig, caCertFile, exPA)
ctx := setup(t)
defer ctx.cleanUp()
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, caCertFile, exPA)
test.AssertNotError(t, err, "Failed to create CA")
ca.SA = storageAuthority
ca.SA = ctx.sa
ca.MaxKeySize = 4096
// Test that the CA collapses duplicate names
csrDER, _ := hex.DecodeString(DupeNameCSRhex)
csr, _ := x509.ParseCertificateRequest(csrDER)
cert, err := ca.IssueCertificate(*csr, 1, FarFuture)
cert, err := ca.IssueCertificate(*csr, ctx.reg.ID, FarFuture)
test.AssertNotError(t, err, "Failed to gracefully handle a CSR with duplicate names")
if err != nil {
return
@ -602,17 +607,17 @@ func TestDeduplication(t *testing.T) {
}
func TestRejectValidityTooLong(t *testing.T) {
cadb, storageAuthority, caConfig, cleanUp := setup(t)
defer cleanUp()
ca, err := NewCertificateAuthorityImpl(cadb, caConfig, caCertFile, exPA)
ctx := setup(t)
defer ctx.cleanUp()
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, caCertFile, exPA)
test.AssertNotError(t, err, "Failed to create CA")
ca.SA = storageAuthority
ca.SA = ctx.sa
ca.MaxKeySize = 4096
// Test that the CA rejects CSRs that would expire after the intermediate cert
csrDER, _ := hex.DecodeString(NoCNCSRhex)
csr, _ := x509.ParseCertificateRequest(csrDER)
_, err = ca.IssueCertificate(*csr, 1, FarPast)
_, err = ca.IssueCertificate(*csr, ctx.reg.ID, FarPast)
test.Assert(t, err == nil, "Can issue a certificate that expires after the underlying authorization.")
// Test that the CA rejects CSRs that would expire after the intermediate cert
@ -624,29 +629,29 @@ func TestRejectValidityTooLong(t *testing.T) {
}
func TestShortKey(t *testing.T) {
cadb, storageAuthority, caConfig, cleanUp := setup(t)
defer cleanUp()
ca, err := NewCertificateAuthorityImpl(cadb, caConfig, caCertFile, exPA)
ca.SA = storageAuthority
ctx := setup(t)
defer ctx.cleanUp()
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, caCertFile, exPA)
ca.SA = ctx.sa
ca.MaxKeySize = 4096
// Test that the CA rejects CSRs that would expire after the intermediate cert
csrDER, _ := hex.DecodeString(ShortKeyCSRhex)
csr, _ := x509.ParseCertificateRequest(csrDER)
_, err = ca.IssueCertificate(*csr, 1, FarFuture)
_, err = ca.IssueCertificate(*csr, ctx.reg.ID, FarFuture)
test.Assert(t, err != nil, "Issued a certificate with too short a key.")
}
func TestRejectBadAlgorithm(t *testing.T) {
cadb, storageAuthority, caConfig, cleanUp := setup(t)
defer cleanUp()
ca, err := NewCertificateAuthorityImpl(cadb, caConfig, caCertFile, exPA)
ca.SA = storageAuthority
ctx := setup(t)
defer ctx.cleanUp()
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, caCertFile, exPA)
ca.SA = ctx.sa
ca.MaxKeySize = 4096
// Test that the CA rejects CSRs that would expire after the intermediate cert
csrDER, _ := hex.DecodeString(BadAlgorithmCSRhex)
csr, _ := x509.ParseCertificateRequest(csrDER)
_, err = ca.IssueCertificate(*csr, 1, FarFuture)
_, err = ca.IssueCertificate(*csr, ctx.reg.ID, FarFuture)
test.Assert(t, err != nil, "Issued a certificate based on a CSR with a weak algorithm.")
}

View File

@ -139,7 +139,7 @@ func startMonitor(rpcCh *amqp.Channel, logger *blog.AuditLogger, stats statsd.St
}
func main() {
app := cmd.NewAppShell("activity-monitor")
app := cmd.NewAppShell("activity-monitor", "RPC activity monitor")
app.Action = func(c cmd.Config) {
stats, err := statsd.NewClient(c.Statsd.Server, c.Statsd.Prefix)

View File

@ -143,12 +143,13 @@ func revokeByReg(regID int64, reasonCode int, deny bool, cac rpc.CertificateAuth
return
}
var version = "0.0.1"
func main() {
app := cli.NewApp()
app.Name = "admin-revoker"
app.Version = version
app.Usage = "Revokes issued certificates"
app.Version = cmd.Version()
app.Author = "Boulder contributors"
app.Email = "ca-dev@letsencrypt.org"
app.Flags = []cli.Flag{
cli.StringFlag{

View File

@ -15,7 +15,7 @@ import (
)
func main() {
app := cmd.NewAppShell("boulder-ca")
app := cmd.NewAppShell("boulder-ca", "Handles issuance operations")
app.Action = func(c cmd.Config) {
stats, err := statsd.NewClient(c.Statsd.Server, c.Statsd.Prefix)
cmd.FailOnError(err, "Couldn't connect to statsd")
@ -37,11 +37,6 @@ func main() {
cadb, err := ca.NewCertificateAuthorityDatabaseImpl(dbMap)
cmd.FailOnError(err, "Failed to create CA database")
if c.SQL.CreateTables {
err = cadb.CreateTablesIfNotExists()
cmd.FailOnError(err, "Failed to create CA tables")
}
cai, err := ca.NewCertificateAuthorityImpl(cadb, c.CA, c.Common.IssuerCert, c.PA)
cmd.FailOnError(err, "Failed to create CA impl")
cai.MaxKeySize = c.Common.MaxKeySize

View File

@ -19,7 +19,7 @@ import (
)
func main() {
app := cmd.NewAppShell("boulder-ra")
app := cmd.NewAppShell("boulder-ra", "Handles service orchestration")
app.Action = func(c cmd.Config) {
stats, err := statsd.NewClient(c.Statsd.Server, c.Statsd.Prefix)
cmd.FailOnError(err, "Couldn't connect to statsd")

View File

@ -15,7 +15,7 @@ import (
)
func main() {
app := cmd.NewAppShell("boulder-sa")
app := cmd.NewAppShell("boulder-sa", "Handles SQL operations")
app.Action = func(c cmd.Config) {
stats, err := statsd.NewClient(c.Statsd.Server, c.Statsd.Prefix)
cmd.FailOnError(err, "Couldn't connect to statsd")
@ -38,11 +38,6 @@ func main() {
cmd.FailOnError(err, "Failed to create SA impl")
sai.SetSQLDebug(c.SQL.SQLDebug)
if c.SQL.CreateTables {
err = sai.CreateTablesIfNotExists()
cmd.FailOnError(err, "Failed to create tables")
}
go cmd.ProfileCmd("SA", stats)
connectionHandler := func(*rpc.AmqpRPCServer) {}

View File

@ -18,7 +18,7 @@ import (
)
func main() {
app := cmd.NewAppShell("boulder-va")
app := cmd.NewAppShell("boulder-va", "Handles challenge validation")
app.Action = func(c cmd.Config) {
stats, err := statsd.NewClient(c.Statsd.Server, c.Statsd.Prefix)
cmd.FailOnError(err, "Couldn't connect to statsd")

View File

@ -75,7 +75,7 @@ func HandlerTimer(handler http.Handler, stats statsd.Statter) http.Handler {
}
func main() {
app := cmd.NewAppShell("boulder-wfe")
app := cmd.NewAppShell("boulder-wfe", "Handles HTTP API requests")
addrFlag := cli.StringFlag{
Name: "addr",
Value: "",

View File

@ -206,7 +206,7 @@ func (ds durationSlice) Swap(a, b int) {
}
func main() {
app := cmd.NewAppShell("expiration-mailer")
app := cmd.NewAppShell("expiration-mailer", "Sends certificate expiration emails")
app.App.Flags = append(app.App.Flags, cli.IntFlag{
Name: "cert_limit",

View File

@ -140,42 +140,30 @@ var testKey = rsa.PrivateKey{
Primes: []*big.Int{p, q},
}
// TODO(jmhodges): Turn this into boulder_sa_test
var dbConnStr = "mysql+tcp://boulder@localhost:3306/boulder_test"
const dbConnStr = "mysql+tcp://boulder@localhost:3306/boulder_sa_test"
func TestFindExpiringCertificates(t *testing.T) {
dbMap, err := sa.NewDbMap(dbConnStr)
if err != nil {
t.Fatalf("Couldn't connect the database: %s", err)
}
err = dbMap.CreateTablesIfNotExists()
cleanUp := test.ResetTestDatabase(t, dbMap.Db)
ssa, err := sa.NewSQLStorageAuthority(dbMap)
if err != nil {
t.Fatalf("Couldn't create tables: %s", err)
t.Fatalf("unable to create SQLStorageAuthority: %s", err)
}
err = dbMap.TruncateTables()
if err != nil {
t.Fatalf("Couldn't truncate tables: %s", err)
}
defer func() {
err = dbMap.TruncateTables()
if err != nil {
t.Fatalf("Couldn't truncate tables after the test: %s", err)
}
dbMap.Db.Close()
}()
defer cleanUp()
tmpl, err := template.New("expiry-email").Parse(testTmpl)
test.AssertNotError(t, err, "Couldn't parse test email template")
stats, _ := statsd.NewNoopClient(nil)
mc := mockMail{}
rs := newFakeRegStore()
m := mailer{
log: blog.GetAuditLogger(),
stats: stats,
mailer: &mc,
emailTemplate: tmpl,
dbMap: dbMap,
rs: rs,
rs: ssa,
nagTimes: []time.Duration{time.Hour * 24, time.Hour * 24 * 4, time.Hour * 24 * 7},
limit: 100,
}
@ -208,6 +196,15 @@ func TestFindExpiringCertificates(t *testing.T) {
},
Key: keyB,
}
regA, err = ssa.NewRegistration(regA)
if err != nil {
t.Fatalf("Couldn't store regA: %s", err)
}
regB, err = ssa.NewRegistration(regB)
if err != nil {
t.Fatalf("Couldn't store regB: %s", err)
}
rawCertA := x509.Certificate{
Subject: pkix.Name{
CommonName: "happy A",
@ -218,7 +215,7 @@ func TestFindExpiringCertificates(t *testing.T) {
}
certDerA, _ := x509.CreateCertificate(rand.Reader, &rawCertA, &rawCertA, &testKey.PublicKey, &testKey)
certA := &core.Certificate{
RegistrationID: 1,
RegistrationID: regA.ID,
Status: core.StatusValid,
Serial: "001",
Expires: time.Now().AddDate(0, 0, 1),
@ -236,7 +233,7 @@ func TestFindExpiringCertificates(t *testing.T) {
}
certDerB, _ := x509.CreateCertificate(rand.Reader, &rawCertB, &rawCertB, &testKey.PublicKey, &testKey)
certB := &core.Certificate{
RegistrationID: 1,
RegistrationID: regA.ID,
Status: core.StatusValid,
Serial: "002",
Expires: time.Now().AddDate(0, 0, 3),
@ -254,15 +251,13 @@ func TestFindExpiringCertificates(t *testing.T) {
}
certDerC, _ := x509.CreateCertificate(rand.Reader, &rawCertC, &rawCertC, &testKey.PublicKey, &testKey)
certC := &core.Certificate{
RegistrationID: 2,
RegistrationID: regB.ID,
Status: core.StatusValid,
Serial: "003",
Expires: time.Now().AddDate(0, 0, 7),
DER: certDerC,
}
certStatusC := &core.CertificateStatus{Serial: "003"}
rs.RegById[regA.ID] = regA
rs.RegById[regB.ID] = regB
err = dbMap.Insert(certA)
test.AssertNotError(t, err, "Couldn't add certA")

View File

@ -119,7 +119,7 @@ func removeInvalidCerts(csvFilename string, dbMap *gorp.DbMap, stats statsd.Stat
}
func main() {
app := cmd.NewAppShell("external-cert-importer")
app := cmd.NewAppShell("external-cert-importer", "Imports external certificates for POP checks")
app.App.Flags = append(app.App.Flags, cli.StringFlag{
Name: "a, valid-certs-file",
@ -164,8 +164,6 @@ func main() {
dbMap.AddTableWithName(core.ExternalCert{}, "externalCerts").SetKeys(false, "SHA1")
dbMap.AddTableWithName(core.IdentifierData{}, "identifierData").SetKeys(false, "CertSHA1")
err = dbMap.CreateTablesIfNotExists()
cmd.FailOnError(err, "Could not create the tables")
// Note that this order of operations is intentional: we first add
// new certs to the database. Then, since certs are identified by

View File

@ -98,7 +98,10 @@ func (src *DBSource) Response(req *ocsp.Request) (response []byte, present bool)
log.Debug(fmt.Sprintf("Searching for OCSP issued by us for serial %s", serialString))
var ocspResponse core.OCSPResponse
err := src.dbMap.SelectOne(&ocspResponse, "SELECT * from ocspResponses WHERE serial = :serial ORDER BY createdAt DESC LIMIT 1;",
// Note: we order by id rather than createdAt, because otherwise we sometimes
// get the wrong result if a certificate is revoked in the same second as its
// last update (e.g. client issues and instant revokes).
err := src.dbMap.SelectOne(&ocspResponse, "SELECT * from ocspResponses WHERE serial = :serial ORDER BY id DESC LIMIT 1;",
map[string]interface{}{"serial": serialString})
if err != nil {
present = false
@ -113,7 +116,7 @@ func (src *DBSource) Response(req *ocsp.Request) (response []byte, present bool)
}
func main() {
app := cmd.NewAppShell("boulder-ocsp-responder")
app := cmd.NewAppShell("boulder-ocsp-responder", "Handles OCSP requests")
app.Action = func(c cmd.Config) {
// Set up logging
stats, err := statsd.NewClient(c.Statsd.Server, c.Statsd.Prefix)

View File

@ -184,7 +184,7 @@ func (updater *OCSPUpdater) findStaleResponses(oldestLastUpdatedTime time.Time,
}
func main() {
app := cmd.NewAppShell("ocsp-updater")
app := cmd.NewAppShell("ocsp-updater", "Generates and updates OCSP responses")
app.App.Flags = append(app.App.Flags, cli.IntFlag{
Name: "limit",

View File

@ -104,8 +104,7 @@ type Config struct {
}
SQL struct {
CreateTables bool
SQLDebug bool
SQLDebug bool
}
Statsd struct {
@ -239,12 +238,19 @@ type AppShell struct {
App *cli.App
}
func Version() string {
return fmt.Sprintf("0.1.0 [%s]", core.GetBuildID())
}
// NewAppShell creates a basic AppShell object containing CLI metadata
func NewAppShell(name string) (shell *AppShell) {
func NewAppShell(name, usage string) (shell *AppShell) {
app := cli.NewApp()
app.Name = name
app.Version = fmt.Sprintf("0.1.0 [%s]", core.GetBuildID())
app.Usage = usage
app.Version = Version()
app.Author = "Boulder contributors"
app.Email = "ca-dev@letsencrypt.org"
app.Flags = []cli.Flag{
cli.StringFlag{

View File

@ -15,7 +15,26 @@ import (
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/miekg/dns"
)
// DNSResolverImpl represents a resolver system
var (
// Private CIDRs to ignore per RFC1918
// 10.0.0.0/8
rfc1918_10 = net.IPNet{
IP: []byte{10, 0, 0, 0},
Mask: []byte{255, 0, 0, 0},
}
// 172.16.0.0/12
rfc1918_172_16 = net.IPNet{
IP: []byte{172, 16, 0, 0},
Mask: []byte{255, 240, 0, 0},
}
// 192.168.0.0/16
rfc1918_192_168 = net.IPNet{
IP: []byte{192, 168, 0, 0},
Mask: []byte{255, 255, 0, 0},
}
)
// DNSResolverImpl represents a client that talks to an external resolver
type DNSResolverImpl struct {
DNSClient *dns.Client
Servers []string
@ -78,47 +97,34 @@ func (dnsResolver *DNSResolverImpl) LookupTXT(hostname string) ([]string, time.D
return txt, rtt, err
}
// LookupHost sends a DNS query to find all A/AAAA records associated with
// the provided hostname.
func (dnsResolver *DNSResolverImpl) LookupHost(hostname string) ([]net.IP, time.Duration, time.Duration, error) {
var addrs []net.IP
var answers []dns.RR
func isPrivateV4(ip net.IP) bool {
return rfc1918_10.Contains(ip) || rfc1918_172_16.Contains(ip) || rfc1918_192_168.Contains(ip)
}
r, aRtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeA)
// LookupHost sends a DNS query to find all A records associated with the provided
// hostname. This method assumes that the external resolver will chase CNAME/DNAME
// aliases and return relevant A records.
func (dnsResolver *DNSResolverImpl) LookupHost(hostname string) ([]net.IP, time.Duration, error) {
var addrs []net.IP
r, rtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeA)
if err != nil {
return addrs, 0, 0, err
return addrs, rtt, err
}
if r.Rcode != dns.RcodeSuccess {
err = fmt.Errorf("DNS failure: %d-%s for A query", r.Rcode, dns.RcodeToString[r.Rcode])
return nil, aRtt, 0, err
return nil, rtt, err
}
answers = append(answers, r.Answer...)
r, aaaaRtt, err := dnsResolver.ExchangeOne(hostname, dns.TypeAAAA)
if err != nil {
return addrs, aRtt, 0, err
}
if r.Rcode != dns.RcodeSuccess {
err = fmt.Errorf("DNS failure: %d-%s for AAAA query", r.Rcode, dns.RcodeToString[r.Rcode])
return nil, aRtt, aaaaRtt, err
}
answers = append(answers, r.Answer...)
for _, answer := range answers {
for _, answer := range r.Answer {
if answer.Header().Rrtype == dns.TypeA {
if a, ok := answer.(*dns.A); ok {
if a, ok := answer.(*dns.A); ok && a.A.To4() != nil && !isPrivateV4(a.A) {
addrs = append(addrs, a.A)
}
} else if answer.Header().Rrtype == dns.TypeAAAA {
if aaaa, ok := answer.(*dns.AAAA); ok {
addrs = append(addrs, aaaa.AAAA)
}
}
}
return addrs, aRtt, aaaaRtt, nil
return addrs, rtt, nil
}
// LookupCNAME returns the target name if a CNAME record exists for

View File

@ -47,6 +47,13 @@ func mockDNSQuery(w dns.ResponseWriter, r *dns.Msg) {
record.Expire = 1
record.Minttl = 1
appendAnswer(record)
case dns.TypeAAAA:
if q.Name == "v6.letsencrypt.org." {
record := new(dns.AAAA)
record.Hdr = dns.RR_Header{Name: "v6.letsencrypt.org.", Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 0}
record.AAAA = net.ParseIP("::1")
appendAnswer(record)
}
case dns.TypeA:
if q.Name == "cps.letsencrypt.org." {
record := new(dns.A)
@ -166,7 +173,7 @@ func TestDNSLookupsNoServer(t *testing.T) {
_, _, err := obj.LookupTXT("letsencrypt.org")
test.AssertError(t, err, "No servers")
_, _, _, err = obj.LookupHost("letsencrypt.org")
_, _, err = obj.LookupHost("letsencrypt.org")
test.AssertError(t, err, "No servers")
_, _, err = obj.LookupCNAME("letsencrypt.org")
@ -186,7 +193,7 @@ func TestDNSServFail(t *testing.T) {
_, _, err = obj.LookupCNAME(bad)
test.AssertError(t, err, "LookupCNAME didn't return an error")
_, _, _, err = obj.LookupHost(bad)
_, _, err = obj.LookupHost(bad)
test.AssertError(t, err, "LookupHost didn't return an error")
// CAA lookup ignores validation failures from the resolver for now
@ -213,20 +220,31 @@ func TestDNSLookupTXT(t *testing.T) {
func TestDNSLookupHost(t *testing.T) {
obj := NewDNSResolverImpl(time.Second*10, []string{dnsLoopbackAddr})
ip, _, _, err := obj.LookupHost("servfail.com")
ip, _, err := obj.LookupHost("servfail.com")
t.Logf("servfail.com - IP: %s, Err: %s", ip, err)
test.AssertError(t, err, "Server failure")
test.Assert(t, len(ip) == 0, "Should not have IPs")
ip, _, _, err = obj.LookupHost("nonexistent.letsencrypt.org")
ip, _, err = obj.LookupHost("nonexistent.letsencrypt.org")
t.Logf("nonexistent.letsencrypt.org - IP: %s, Err: %s", ip, err)
test.AssertNotError(t, err, "Not an error to not exist")
test.Assert(t, len(ip) == 0, "Should not have IPs")
ip, _, _, err = obj.LookupHost("cps.letsencrypt.org")
// Single IPv4 address
ip, _, err = obj.LookupHost("cps.letsencrypt.org")
t.Logf("cps.letsencrypt.org - IP: %s, Err: %s", ip, err)
test.AssertNotError(t, err, "Not an error to exist")
test.Assert(t, len(ip) > 0, "Should have IPs")
test.Assert(t, len(ip) == 1, "Should have IP")
ip, _, err = obj.LookupHost("cps.letsencrypt.org")
t.Logf("cps.letsencrypt.org - IP: %s, Err: %s", ip, err)
test.AssertNotError(t, err, "Not an error to exist")
test.Assert(t, len(ip) == 1, "Should have IP")
// No IPv6
ip, _, err = obj.LookupHost("v6.letsencrypt.org")
t.Logf("v6.letsencrypt.org - IP: %s, Err: %s", ip, err)
test.AssertNotError(t, err, "Not an error to exist")
test.Assert(t, len(ip) == 0, "Should not have IPs")
}
func TestDNSLookupCAA(t *testing.T) {

View File

@ -134,7 +134,6 @@ type StorageAuthority interface {
// CertificateAuthorityDatabase represents an atomic sequence source
type CertificateAuthorityDatabase interface {
CreateTablesIfNotExists() error
IncrementAndGetSerial(*gorp.Transaction) (int64, error)
Begin() (*gorp.Transaction, error)
}
@ -143,7 +142,7 @@ type CertificateAuthorityDatabase interface {
type DNSResolver interface {
ExchangeOne(string, uint16) (*dns.Msg, time.Duration, error)
LookupTXT(string) ([]string, time.Duration, error)
LookupHost(string) ([]net.IP, time.Duration, time.Duration, error)
LookupHost(string) ([]net.IP, time.Duration, error)
LookupCNAME(string) (string, time.Duration, error)
LookupDNAME(string) (string, time.Duration, error)
LookupCAA(string) ([]*dns.CAA, time.Duration, error)

View File

@ -222,6 +222,19 @@ func (r *Registration) MergeUpdate(input Registration) {
}
}
// ValidationRecord represents a validation attempt against a specific URL/hostname
// and the IP addresses that were resolved and used
type ValidationRecord struct {
// SimpleHTTP only
URL string `json:"url,omitempty"`
// Shared
Hostname string `json:"hostname"`
Port string `json:"port"`
AddressesResolved []net.IP `json:"addressesResolved"`
AddressUsed net.IP `json:"addressUsed"`
}
// Challenge is an aggregate of all data needed for any challenges.
//
// Rather than define individual types for different types of
@ -252,6 +265,43 @@ type Challenge struct {
// Used by dns and dvsni challenges
Validation *jose.JsonWebSignature `json:"validation,omitempty"`
// Contains information about URLs used or redirected to and IPs resolved and
// used
ValidationRecord []ValidationRecord `json:"validationRecord,omitempty"`
}
// RecordsSane checks the sanity of a ValidationRecord object before sending it
// back to the RA to be stored.
func (ch Challenge) RecordsSane() bool {
if ch.ValidationRecord == nil || len(ch.ValidationRecord) == 0 {
return false
}
switch ch.Type {
case ChallengeTypeSimpleHTTP:
for _, rec := range ch.ValidationRecord {
if rec.URL == "" || rec.Hostname == "" || rec.Port == "" || rec.AddressUsed == nil ||
len(rec.AddressesResolved) == 0 {
return false
}
}
case ChallengeTypeDVSNI:
if len(ch.ValidationRecord) > 1 {
return false
}
if ch.ValidationRecord[0].URL != "" {
return false
}
if ch.ValidationRecord[0].Hostname == "" || ch.ValidationRecord[0].Port == "" ||
ch.ValidationRecord[0].AddressUsed == nil || len(ch.ValidationRecord[0].AddressesResolved) == 0 {
return false
}
case ChallengeTypeDNS:
// Nothing for now
}
return true
}
// IsSane checks the sanity of a challenge object before issued to the client
@ -300,7 +350,6 @@ func (ch Challenge) IsSane(completed bool) bool {
if completed && ch.Validation == nil {
return false
}
default:
return false
}
@ -361,7 +410,7 @@ type Authorization struct {
// in process, these are challenges to be fulfilled; for
// final authorizations, they describe the evidence that
// the server used in support of granting the authorization.
Challenges []Challenge `json:"challenges,omitempty" db:"challenges"`
Challenges []Challenge `json:"challenges,omitempty" db:"-"`
// The server may suggest combinations of challenges if it
// requires more than one challenge to be completed.

View File

@ -7,6 +7,7 @@ package core
import (
"encoding/json"
"net"
"testing"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
@ -39,7 +40,33 @@ func TestRegistrationUpdate(t *testing.T) {
test.Assert(t, reg.Agreement == update.Agreement, "Agreement was not updated")
}
func TestSanityCheck(t *testing.T) {
func TestRecordSanityCheck(t *testing.T) {
rec := []ValidationRecord{
ValidationRecord{
URL: "http://localhost/test",
Hostname: "localhost",
Port: "80",
AddressesResolved: []net.IP{net.IP{127, 0, 0, 1}},
AddressUsed: net.IP{127, 0, 0, 1},
},
}
chall := Challenge{Type: ChallengeTypeSimpleHTTP, ValidationRecord: rec}
test.Assert(t, chall.RecordsSane(), "Record should be sane")
chall.ValidationRecord[0].URL = ""
test.Assert(t, !chall.RecordsSane(), "Record should not be sane")
chall = Challenge{Type: ChallengeTypeDVSNI, ValidationRecord: rec}
chall.ValidationRecord[0].URL = ""
test.Assert(t, chall.RecordsSane(), "Record should be sane")
chall.ValidationRecord[0].Hostname = ""
test.Assert(t, !chall.RecordsSane(), "Record should not be sane")
chall.ValidationRecord = append(chall.ValidationRecord, rec...)
test.Assert(t, !chall.RecordsSane(), "Record should not be sane")
}
func TestChallengeSanityCheck(t *testing.T) {
types := []string{ChallengeTypeSimpleHTTP, ChallengeTypeDVSNI, ChallengeTypeDNS}
for _, challengeType := range types {
chall := Challenge{Type: challengeType, Status: StatusInvalid}
@ -58,9 +85,26 @@ func TestSanityCheck(t *testing.T) {
if challengeType == ChallengeTypeSimpleHTTP {
tls := true
chall.TLS = &tls
chall.ValidationRecord = []ValidationRecord{ValidationRecord{
URL: "",
Hostname: "localhost",
Port: "80",
AddressesResolved: []net.IP{net.IP{127, 0, 0, 1}},
AddressUsed: net.IP{127, 0, 0, 1},
}}
test.Assert(t, chall.IsSane(false), "IsSane should be true")
} else if challengeType == ChallengeTypeDVSNI || challengeType == ChallengeTypeDNS {
chall.Validation = new(jose.JsonWebSignature)
if challengeType == ChallengeTypeDVSNI {
chall.ValidationRecord = []ValidationRecord{ValidationRecord{
Hostname: "localhost",
Port: "80",
AddressesResolved: []net.IP{net.IP{127, 0, 0, 1}},
AddressUsed: net.IP{127, 0, 0, 1},
}}
} else {
chall.ValidationRecord = []ValidationRecord{}
}
test.Assert(t, chall.IsSane(true), "IsSane should be true")
}
}

View File

@ -1,5 +1,12 @@
These `.sql` files define the table layout, indicies, relationships, and users default to Boulder. Implementors should use these as starting points for their own configuration.
The `sql` files here define the database user relationships between
the various databases and services. Implementors should use these as
starting points for their own configuration. The actual schemas are
managed by [goose](https://bitbucket.org/liamstask/goose) and can be
found in `./ca/_db` and `./sa/_db`.
The currently supported database is MariaDB 10.
mysql -u root -e "create database boulder_test; create database boulder_development; grant all privileges on boulder_test.* to 'boulder'@'localhost';"
The databases that boulder requires to operate in development and
testing can be created using test/create\_db.sh. It uses the root
MariaDB user, so if you have disabled that account you may have to
adjust the file or recreate the commands.

View File

@ -1,26 +0,0 @@
--
-- Copyright 2015 ISRG. All rights reserved
-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.
--
-- This file defines the table schema, foreign keys and indicies of the
-- Certificate Authority database, which is logically separate from that which
-- is utilized by the Storage Authority and administrator tools.
--
CREATE TABLE `serialNumber` (
`id` int(11) DEFAULT NULL,
`number` int(11) DEFAULT NULL,
`lastUpdated` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `serialNumber`
(`id`,
`number`,
`lastUpdated`)
VALUES
(1,
1,
now() );

View File

@ -32,8 +32,12 @@ func (mock *MockDNS) LookupTXT(hostname string) ([]string, time.Duration, error)
}
// LookupHost is a mock
func (mock *MockDNS) LookupHost(hostname string) ([]net.IP, time.Duration, time.Duration, error) {
return nil, 0, 0, nil
func (mock *MockDNS) LookupHost(hostname string) ([]net.IP, time.Duration, error) {
if hostname == "always.invalid" || hostname == "invalid.invalid" {
return []net.IP{}, 0, nil
}
ip := net.ParseIP("127.0.0.1")
return []net.IP{ip}, 0, nil
}
// LookupCNAME is a mock

View File

@ -178,6 +178,7 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization
RegistrationID: regID,
Status: core.StatusPending,
Combinations: combinations,
Challenges: challenges,
}
// Get a pending Auth first so we can get our ID back, then update with challenges
@ -190,22 +191,19 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization
}
// Construct all the challenge URIs
for i := range challenges {
for i := range authz.Challenges {
// Ignoring these errors because we construct the URLs to be correct
challengeURI, _ := core.ParseAcmeURL(ra.AuthzBase + authz.ID + "?challenge=" + strconv.Itoa(i))
challenges[i].URI = challengeURI
authz.Challenges[i].URI = challengeURI
if !challenges[i].IsSane(false) {
if !authz.Challenges[i].IsSane(false) {
// InternalServerError because we generated these challenges, they should
// be OK.
err = core.InternalServerError(fmt.Sprintf("Challenge didn't pass sanity check: %+v", challenges[i]))
err = core.InternalServerError(fmt.Sprintf("Challenge didn't pass sanity check: %+v", authz.Challenges[i]))
return authz, err
}
}
// Update object
authz.Challenges = challenges
// Store the authorization object, then return it
err = ra.SA.UpdatePendingAuthorization(authz)
if err != nil {

View File

@ -124,14 +124,18 @@ var (
log = mocks.UseMockLog()
// TODO(jmhodges): Turn this into boulder_sa_test
dbConnStr = "mysql+tcp://boulder@localhost:3306/boulder_test"
common = cmd.PAConfig{
DBConnect: dbConnStr,
common = cmd.PAConfig{
DBConnect: paDBConnStr,
}
)
func initAuthorities(t *testing.T) (core.CertificateAuthority, *DummyValidationAuthority, *sa.SQLStorageAuthority, *RegistrationAuthorityImpl, func()) {
const (
paDBConnStr = "mysql+tcp://boulder@localhost:3306/boulder_pa_test"
caDBConnStr = "mysql+tcp://boulder@localhost:3306/boulder_ca_test"
saDBConnStr = "mysql+tcp://boulder@localhost:3306/boulder_sa_test"
)
func initAuthorities(t *testing.T) (*DummyValidationAuthority, *sa.SQLStorageAuthority, *RegistrationAuthorityImpl, func()) {
err := json.Unmarshal(AccountKeyJSONA, &AccountKeyA)
test.AssertNotError(t, err, "Failed to unmarshal public JWK")
err = json.Unmarshal(AccountKeyJSONB, &AccountKeyB)
@ -145,7 +149,7 @@ func initAuthorities(t *testing.T) (core.CertificateAuthority, *DummyValidationA
err = json.Unmarshal(ShortKeyJSON, &ShortKey)
test.AssertNotError(t, err, "Failed to unmarshall JWK")
dbMap, err := sa.NewDbMap(dbConnStr)
dbMap, err := sa.NewDbMap(saDBConnStr)
if err != nil {
t.Fatalf("Failed to create dbMap: %s", err)
}
@ -154,14 +158,7 @@ func initAuthorities(t *testing.T) (core.CertificateAuthority, *DummyValidationA
t.Fatalf("Failed to create SA: %s", err)
}
err = ssa.CreateTablesIfNotExists()
if err != nil {
t.Fatalf("Failed to create SA tables: %s", err)
}
if err = dbMap.TruncateTables(); err != nil {
t.Fatalf("Failed to truncate SA tables: %s", err)
}
saDBCleanUp := test.ResetTestDatabase(t, dbMap.Db)
va := &DummyValidationAuthority{}
@ -198,17 +195,13 @@ func initAuthorities(t *testing.T) (core.CertificateAuthority, *DummyValidationA
MaxKeySize: 4096,
}
cleanUp := func() {
if err = dbMap.TruncateTables(); err != nil {
t.Fatalf("Failed to truncate tables after the test: %s", err)
}
dbMap.Db.Close()
saDBCleanUp()
caDBCleanUp()
}
csrDER, _ := hex.DecodeString(CSRhex)
ExampleCSR, _ = x509.ParseCertificateRequest(csrDER)
// This registration implicitly gets ID = 1
Registration, _ = ssa.NewRegistration(core.Registration{Key: AccountKeyA})
ra, err := NewRegistrationAuthorityImpl(common)
@ -231,7 +224,7 @@ func initAuthorities(t *testing.T) (core.CertificateAuthority, *DummyValidationA
AuthzFinal.Expires = &exp
AuthzFinal.Challenges[0].Status = "valid"
return &ca, va, ssa, &ra, cleanUp
return va, ssa, &ra, cleanUp
}
// This is an unfortunate bit of tech debt that is being taken on in
@ -241,7 +234,7 @@ func initAuthorities(t *testing.T) (core.CertificateAuthority, *DummyValidationA
// CertificateAuthorityClient, so this is only marginally worse.
// TODO(Issue #628): use a CAClient fake instead of a CAImpl instance
func caDBImpl(t *testing.T) (core.CertificateAuthorityDatabase, func()) {
dbMap, err := sa.NewDbMap(dbConnStr)
dbMap, err := sa.NewDbMap(caDBConnStr)
if err != nil {
t.Fatalf("Could not construct dbMap: %s", err)
}
@ -251,34 +244,17 @@ func caDBImpl(t *testing.T) (core.CertificateAuthorityDatabase, func()) {
t.Fatalf("Could not construct CA DB: %s", err)
}
// We intentionally call CreateTablesIfNotExists twice before
// returning because of the weird insert inside it. The
// CADatabaseImpl code expects the existence of a single row in
// its serialIds table or else it errors. CreateTablesIfNotExists
// currently inserts that row and TruncateTables will remove
// it. But we need to make sure the tables exist before
// TruncateTables can be called to reset the table. So, two calls
// to CreateTablesIfNotExists.
cleanUp := test.ResetTestDatabase(t, dbMap.Db)
err = cadb.CreateTablesIfNotExists()
// This row is required to exist for caDBImpl to work
// correctly. We can no longer use
// dbMap.Insert(&SerialNumber{...}) for this because gorp will
// ignore the ID and insert a new row at a new autoincrement id.
// TODO(jmhodges): gen ids flickr-style, no row needed a head of time
_, err = dbMap.Db.Exec("insert into serialNumber (id, number, lastUpdated) VALUES (?, ?, ?)", 1, 1, time.Now())
if err != nil {
t.Fatalf("Could not construct tables: %s", err)
t.Fatalf("unable to create the serial number row: %s", err)
}
err = dbMap.TruncateTables()
if err != nil {
t.Fatalf("Could not truncate tables: %s", err)
}
err = cadb.CreateTablesIfNotExists()
if err != nil {
t.Fatalf("Could not construct tables: %s", err)
}
cleanUp := func() {
if err := dbMap.TruncateTables(); err != nil {
t.Fatalf("Could not truncate tables after the test: %s", err)
}
dbMap.Db.Close()
}
return cadb, cleanUp
}
@ -333,7 +309,7 @@ func TestValidateEmail(t *testing.T) {
}
func TestNewRegistration(t *testing.T) {
_, _, sa, ra, cleanUp := initAuthorities(t)
_, sa, ra, cleanUp := initAuthorities(t)
defer cleanUp()
mailto, _ := core.ParseAcmeURL("mailto:foo@letsencrypt.org")
input := core.Registration{
@ -358,7 +334,7 @@ func TestNewRegistration(t *testing.T) {
}
func TestNewRegistrationNoFieldOverwrite(t *testing.T) {
_, _, _, ra, cleanUp := initAuthorities(t)
_, _, ra, cleanUp := initAuthorities(t)
defer cleanUp()
mailto, _ := core.ParseAcmeURL("mailto:foo@letsencrypt.org")
input := core.Registration{
@ -386,7 +362,7 @@ func TestNewRegistrationNoFieldOverwrite(t *testing.T) {
}
func TestNewRegistrationBadKey(t *testing.T) {
_, _, _, ra, cleanUp := initAuthorities(t)
_, _, ra, cleanUp := initAuthorities(t)
defer cleanUp()
mailto, _ := core.ParseAcmeURL("mailto:foo@letsencrypt.org")
input := core.Registration{
@ -399,12 +375,12 @@ func TestNewRegistrationBadKey(t *testing.T) {
}
func TestNewAuthorization(t *testing.T) {
_, _, sa, ra, cleanUp := initAuthorities(t)
_, sa, ra, cleanUp := initAuthorities(t)
defer cleanUp()
_, err := ra.NewAuthorization(AuthzRequest, 0)
test.AssertError(t, err, "Authorization cannot have registrationID == 0")
authz, err := ra.NewAuthorization(AuthzRequest, 1)
authz, err := ra.NewAuthorization(AuthzRequest, Registration.ID)
test.AssertNotError(t, err, "NewAuthorization failed")
// Verify that returned authz same as DB
@ -413,7 +389,7 @@ func TestNewAuthorization(t *testing.T) {
assertAuthzEqual(t, authz, dbAuthz)
// Verify that the returned authz has the right information
test.Assert(t, authz.RegistrationID == 1, "Initial authz did not get the right registration ID")
test.Assert(t, authz.RegistrationID == Registration.ID, "Initial authz did not get the right registration ID")
test.Assert(t, authz.Identifier == AuthzRequest.Identifier, "Initial authz had wrong identifier")
test.Assert(t, authz.Status == core.StatusPending, "Initial authz not pending")
@ -427,7 +403,7 @@ func TestNewAuthorization(t *testing.T) {
}
func TestUpdateAuthorization(t *testing.T) {
_, va, sa, ra, cleanUp := initAuthorities(t)
va, sa, ra, cleanUp := initAuthorities(t)
defer cleanUp()
AuthzInitial, _ = sa.NewPendingAuthorization(AuthzInitial)
sa.UpdatePendingAuthorization(AuthzInitial)
@ -451,7 +427,7 @@ func TestUpdateAuthorization(t *testing.T) {
}
func TestOnValidationUpdate(t *testing.T) {
_, _, sa, ra, cleanUp := initAuthorities(t)
_, sa, ra, cleanUp := initAuthorities(t)
defer cleanUp()
AuthzUpdated, _ = sa.NewPendingAuthorization(AuthzUpdated)
sa.UpdatePendingAuthorization(AuthzUpdated)
@ -474,7 +450,7 @@ func TestOnValidationUpdate(t *testing.T) {
}
func TestCertificateKeyNotEqualAccountKey(t *testing.T) {
_, _, sa, ra, cleanUp := initAuthorities(t)
_, sa, ra, cleanUp := initAuthorities(t)
defer cleanUp()
authz := core.Authorization{}
authz, _ = sa.NewPendingAuthorization(authz)
@ -497,8 +473,8 @@ func TestCertificateKeyNotEqualAccountKey(t *testing.T) {
CSR: parsedCSR,
}
// Registration id 1 has key == AccountKeyA
_, err = ra.NewCertificate(certRequest, 1)
// Registration has key == AccountKeyA
_, err = ra.NewCertificate(certRequest, Registration.ID)
test.AssertError(t, err, "Should have rejected cert with key = account key")
test.AssertEquals(t, err.Error(), "Certificate public key must be different than account key")
@ -506,7 +482,7 @@ func TestCertificateKeyNotEqualAccountKey(t *testing.T) {
}
func TestAuthorizationRequired(t *testing.T) {
_, _, sa, ra, cleanUp := initAuthorities(t)
_, sa, ra, cleanUp := initAuthorities(t)
defer cleanUp()
AuthzFinal.RegistrationID = 1
AuthzFinal, _ = sa.NewPendingAuthorization(AuthzFinal)
@ -526,9 +502,9 @@ func TestAuthorizationRequired(t *testing.T) {
}
func TestNewCertificate(t *testing.T) {
_, _, sa, ra, cleanUp := initAuthorities(t)
_, sa, ra, cleanUp := initAuthorities(t)
defer cleanUp()
AuthzFinal.RegistrationID = 1
AuthzFinal.RegistrationID = Registration.ID
AuthzFinal, _ = sa.NewPendingAuthorization(AuthzFinal)
sa.UpdatePendingAuthorization(AuthzFinal)
sa.FinalizeAuthorization(AuthzFinal)
@ -543,7 +519,7 @@ func TestNewCertificate(t *testing.T) {
CSR: ExampleCSR,
}
cert, err := ra.NewCertificate(certRequest, 1)
cert, err := ra.NewCertificate(certRequest, Registration.ID)
test.AssertNotError(t, err, "Failed to issue certificate")
if err != nil {
return

View File

@ -353,7 +353,10 @@ func (rpc *AmqpRPCServer) processMessage(msg amqp.Delivery) {
rpc.log.Audit(fmt.Sprintf(" [s>][%s][%s] Error condition marshalling RPC response %s [%s]", rpc.serverQueue, msg.ReplyTo, msg.Type, msg.CorrelationId))
return
}
rpc.log.Info(fmt.Sprintf(" [s>][%s][%s] replying %s(%s) [%s]", rpc.serverQueue, msg.ReplyTo, msg.Type, core.B64enc(jsonResponse), msg.CorrelationId))
if response.Error.Value != "" {
rpc.log.Info(fmt.Sprintf(" [s>][%s][%s] %s failed, replying: %s (%s) [%s]", rpc.serverQueue, msg.ReplyTo, msg.Type, response.Error.Value, response.Error.Type, msg.CorrelationId))
}
rpc.log.Debug(fmt.Sprintf(" [s>][%s][%s] replying %s(%s) [%s]", rpc.serverQueue, msg.ReplyTo, msg.Type, core.B64enc(jsonResponse), msg.CorrelationId))
rpc.Channel.Publish(
AmqpExchange,
msg.ReplyTo,

9
sa/_db/dbconf.yml Normal file
View File

@ -0,0 +1,9 @@
development:
driver: mysql
open: boulder@tcp(localhost:3306)/boulder_sa_development
test:
driver: mysql
open: boulder@tcp(localhost:3306)/boulder_sa_test
integration:
driver: mysql
open: boulder@tcp(localhost:3306)/boulder_sa_integration

View File

@ -1,13 +1,6 @@
--
-- Copyright 2015 ISRG. All rights reserved
-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.
--
-- This file defines the table schema, foreign keys and indicies of the
-- primary database, used by all the parts of Boulder except the Certificate
-- Authority module, which utilizes its own database.
--
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE `registrations` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
@ -26,7 +19,6 @@ CREATE TABLE `authz` (
`registrationID` bigint(20) DEFAULT NULL,
`status` varchar(255) DEFAULT NULL,
`expires` datetime DEFAULT NULL,
`challenges` varchar(1536) DEFAULT NULL,
`combinations` varchar(255) DEFAULT NULL,
`sequence` bigint(20) DEFAULT NULL,
PRIMARY KEY (`id`),
@ -59,6 +51,22 @@ CREATE TABLE `certificateStatus` (
PRIMARY KEY (`serial`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `challenges` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`authorizationID` varchar(255) NOT NULL,
`LockCol` bigint(20) DEFAULT NULL,
`type` varchar(255) NOT NULL,
`status` varchar(255) NOT NULL,
`error` mediumblob DEFAULT NULL,
`validated` datetime DEFAULT NULL,
`uri` varchar(255) DEFAULT NULL,
`token` varchar(255) NOT NULL,
`tls` tinyint(1) DEFAULT NULL,
`validation` mediumblob,
`validationRecord` mediumblob,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `crls` (
`serial` varchar(255) NOT NULL,
`createdAt` datetime NOT NULL,
@ -87,7 +95,6 @@ CREATE TABLE `pending_authz` (
`registrationID` bigint(20) DEFAULT NULL,
`status` varchar(255) DEFAULT NULL,
`expires` datetime DEFAULT NULL,
`challenges` varchar(1536) DEFAULT NULL,
`combinations` varchar(255) DEFAULT NULL,
`LockCol` bigint(20) DEFAULT NULL,
PRIMARY KEY (`id`),
@ -113,3 +120,21 @@ CREATE TABLE `externalCerts` (
`rawDERCert` blob DEFAULT NULL,
UNIQUE INDEX (sha1)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back
ALTER TABLE `pending_authz` DROP FOREIGN KEY `regId_pending_authz`;
ALTER TABLE `certificates` DROP FOREIGN KEY `regId_certificates`;
ALTER TABLE `authz` DROP FOREIGN KEY `regId_authz`;
DROP TABLE `registrations`;
DROP TABLE `authz`;
DROP TABLE `certificates`;
DROP TABLE `certificateStatus`;
DROP TABLE `crls`;
DROP TABLE `deniedCSRs`;
DROP TABLE `ocspResponses`;
DROP TABLE `pending_authz`;
DROP TABLE `identifierData`;
DROP TABLE `externalCerts`;

View File

@ -59,7 +59,7 @@ func NewDbMap(dbConnect string) (*gorp.DbMap, error) {
// the mysql driver. Similarly, the driver needs the password and
// username unescaped. Compromise by doing the leg work if the config
// says the database URL's scheme is a fake one called
// "mysqltcp://". See
// "mysql+tcp://". See
// https://github.com/go-sql-driver/mysql/issues/362 for why we have
// to futz around and avoid URL.String.
func recombineURLForDB(dbConnect string) (string, error) {
@ -85,6 +85,15 @@ func recombineURLForDB(dbConnect string) (string, error) {
// instead of the number of rows changed by the UPDATE.
dsnVals.Set("clientFoundRows", "true")
// Ensures that MySQL/MariaDB warnings are treated as errors. This
// avoids a number of nasty edge conditions we could wander
// into. Common things this discovers includes places where data
// being sent had a different type than what is in the schema,
// strings being truncated, writing null to a NOT NULL column, and
// so on. See
// <https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sql-mode-strict>.
dsnVals.Set("strict", "true")
user := dbURL.User.Username()
passwd, hasPass := dbURL.User.Password()
dbConn := ""
@ -95,8 +104,7 @@ func recombineURLForDB(dbConnect string) (string, error) {
dbConn += ":" + passwd
}
dbConn += "@tcp(" + dbURL.Host + ")"
// TODO(jmhodges): should be dbURL.EscapedPath() but Travis doesn't have 1.5
return dbConn + dbURL.Path + "?" + dsnVals.Encode(), nil
return dbConn + dbURL.EscapedPath() + "?" + dsnVals.Encode(), nil
}
// SetSQLDebug enables/disables GORP SQL-level Debugging
@ -116,11 +124,10 @@ type SQLLogger struct {
// Printf adapts the AuditLogger to GORP's interface
func (log *SQLLogger) Printf(format string, v ...interface{}) {
log.log.Debug(fmt.Sprintf(format, v))
log.log.Debug(fmt.Sprintf(format, v...))
}
// initTables constructs the table map for the ORM. If you want to also create
// the tables, call CreateTablesIfNotExists on the DbMap.
// initTables constructs the table map for the ORM.
func initTables(dbMap *gorp.DbMap) {
regTable := dbMap.AddTableWithName(regModel{}, "registrations").SetKeys(true, "ID")
regTable.SetVersionCol("LockCol")
@ -128,11 +135,8 @@ func initTables(dbMap *gorp.DbMap) {
regTable.ColMap("KeySHA256").SetNotNull(true).SetUnique(true)
pendingAuthzTable := dbMap.AddTableWithName(pendingauthzModel{}, "pending_authz").SetKeys(false, "ID")
pendingAuthzTable.SetVersionCol("LockCol")
pendingAuthzTable.ColMap("Challenges").SetMaxSize(1536)
authzTable := dbMap.AddTableWithName(authzModel{}, "authz").SetKeys(false, "ID")
authzTable.ColMap("Challenges").SetMaxSize(1536)
dbMap.AddTableWithName(authzModel{}, "authz").SetKeys(false, "ID")
dbMap.AddTableWithName(challModel{}, "challenges").SetKeys(true, "ID").SetVersionCol("LockCol")
dbMap.AddTableWithName(core.Certificate{}, "certificates").SetKeys(false, "Serial")
dbMap.AddTableWithName(core.CertificateStatus{}, "certificateStatus").SetKeys(false, "Serial").SetVersionCol("LockCol")
dbMap.AddTableWithName(core.OCSPResponse{}, "ocspResponses").SetKeys(true, "ID")

View File

@ -1,13 +1,22 @@
// Copyright 2015 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package sa
import (
"encoding/json"
"fmt"
"math"
"time"
jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
"github.com/letsencrypt/boulder/core"
)
var mediumBlobSize = int(math.Pow(2, 24))
// regModel is the description of a core.Registration in the database.
type regModel struct {
ID int64 `db:"id"`
@ -18,6 +27,24 @@ type regModel struct {
LockCol int64
}
// challModel is the description of a core.Challenge in the database
type challModel struct {
ID int64 `db:"id"`
AuthorizationID string `db:"authorizationID"`
Type string `db:"type"`
Status core.AcmeStatus `db:"status"`
Error []byte `db:"error"`
Validated *time.Time `db:"validated"`
URI string `db:"uri"`
Token string `db:"token"`
TLS *bool `db:"tls"`
Validation []byte `db:"validation"`
ValidationRecord []byte `db:"validationRecord"`
LockCol int64
}
// newReg creates a reg model object from a core.Registration
func registrationToModel(r *core.Registration) (*regModel, error) {
key, err := json.Marshal(r.Key)
@ -54,3 +81,88 @@ func modelToRegistration(rm *regModel) (core.Registration, error) {
}
return r, nil
}
func challengeToModel(c *core.Challenge, authID string) (*challModel, error) {
cm := challModel{
AuthorizationID: authID,
Type: c.Type,
Status: c.Status,
Validated: c.Validated,
Token: c.Token,
TLS: c.TLS,
}
if c.Validation != nil {
cm.Validation = []byte(c.Validation.FullSerialize())
if len(cm.Validation) > mediumBlobSize {
return nil, fmt.Errorf("Validation object is too large to store in the database")
}
}
if c.Error != nil {
errJSON, err := json.Marshal(c.Error)
if err != nil {
return nil, err
}
if len(errJSON) > mediumBlobSize {
return nil, fmt.Errorf("Error object is too large to store in the database")
}
cm.Error = errJSON
}
if c.URI != nil {
if len(c.URI.String()) > 255 {
return nil, fmt.Errorf("URI is too long to store in the database")
}
cm.URI = c.URI.String()
}
if len(c.ValidationRecord) > 0 {
vrJSON, err := json.Marshal(c.ValidationRecord)
if err != nil {
return nil, err
}
if len(vrJSON) > mediumBlobSize {
return nil, fmt.Errorf("Validation Record object is too large to store in the database")
}
cm.ValidationRecord = vrJSON
}
return &cm, nil
}
func modelToChallenge(cm *challModel) (core.Challenge, error) {
c := core.Challenge{
Type: cm.Type,
Status: cm.Status,
Validated: cm.Validated,
Token: cm.Token,
TLS: cm.TLS,
}
if len(cm.URI) > 0 {
uri, err := core.ParseAcmeURL(cm.URI)
if err != nil {
return core.Challenge{}, err
}
c.URI = uri
}
if len(cm.Validation) > 0 {
val, err := jose.ParseSigned(string(cm.Validation))
if err != nil {
return core.Challenge{}, err
}
c.Validation = val
}
if len(cm.Error) > 0 {
var problem core.ProblemDetails
err := json.Unmarshal(cm.Error, &problem)
if err != nil {
return core.Challenge{}, err
}
c.Error = &problem
}
if len(cm.ValidationRecord) > 0 {
var vr []core.ValidationRecord
err := json.Unmarshal(cm.ValidationRecord, &vr)
if err != nil {
return core.Challenge{}, err
}
c.ValidationRecord = vr
}
return c, nil
}

52
sa/satest/satest.go Normal file
View File

@ -0,0 +1,52 @@
package satest
import (
"encoding/json"
"testing"
jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
"github.com/letsencrypt/boulder/core"
)
var theKey = `{
"kty": "RSA",
"n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw",
"e": "AQAB"
}`
// GoodJWK returns a known-good JsonWebKey that is always the
// same. This a hack to allow both the CA and SA tests to benefit
// because the CA tests currently require a full-fledged
// SQLSAImpl. Long term, when the CA tests no longer need
// CreateWorkingRegistration, this and CreateWorkingRegistration can
// be pushed back into the SA tests proper.
func GoodJWK() jose.JsonWebKey {
var jwk jose.JsonWebKey
err := json.Unmarshal([]byte(theKey), &jwk)
if err != nil {
panic("known-good theKey is no longer known-good")
}
return jwk
}
// CreateWorkingRegistration inserts a new, correct Registration into
// SA using GoodKey under the hood. This a hack to allow both the CA
// and SA tests to benefit because the CA tests currently require a
// full-fledged SQLSAImpl. Long term, when the CA tests no longer need
// CreateWorkingRegistration, this and CreateWorkingRegistration can
// be pushed back into the SA tests proper.
func CreateWorkingRegistration(t *testing.T, sa core.StorageAuthority) core.Registration {
contact, err := core.ParseAcmeURL("mailto:foo@example.com")
if err != nil {
t.Fatalf("unable to parse contact link: %s", err)
}
contacts := []*core.AcmeURL{contact}
reg, err := sa.NewRegistration(core.Registration{
Key: GoodJWK(),
Contact: contacts,
})
if err != nil {
t.Fatalf("Unable to create new registration")
}
return reg
}

View File

@ -22,6 +22,8 @@ import (
blog "github.com/letsencrypt/boulder/log"
)
const getChallengesQuery = "SELECT * FROM challenges WHERE authorizationID = :authID ORDER BY id ASC"
// SQLStorageAuthority defines a Storage Authority
type SQLStorageAuthority struct {
dbMap *gorp.DbMap
@ -67,12 +69,6 @@ func (ssa *SQLStorageAuthority) SetSQLDebug(state bool) {
SetSQLDebug(ssa.dbMap, state)
}
// CreateTablesIfNotExists instructs the ORM to create any missing tables.
func (ssa *SQLStorageAuthority) CreateTablesIfNotExists() (err error) {
err = ssa.dbMap.CreateTablesIfNotExists()
return
}
func statusIsPending(status core.AcmeStatus) bool {
return status == core.StatusPending || status == core.StatusProcessing || status == core.StatusUnknown
}
@ -95,6 +91,33 @@ func existingRegistration(tx *gorp.Transaction, id int64) bool {
return count > 0
}
func updateChallenges(authID string, challenges []core.Challenge, tx *gorp.Transaction) error {
var challs []challModel
_, err := tx.Select(
&challs,
getChallengesQuery,
map[string]interface{}{"authID": authID},
)
if err != nil {
return err
}
if len(challs) != len(challenges) {
return fmt.Errorf("Invalid number of challenges provided")
}
for i, authChall := range challenges {
chall, err := challengeToModel(&authChall, challs[i].AuthorizationID)
if err != nil {
return err
}
chall.ID = challs[i].ID
_, err = tx.Update(chall)
if err != nil {
return err
}
}
return nil
}
type NoSuchRegistrationError struct {
Msg string
}
@ -152,7 +175,10 @@ func (ssa *SQLStorageAuthority) GetAuthorization(id string) (authz core.Authoriz
tx.Rollback()
return
}
if authObj == nil {
if authObj != nil {
authD := *authObj.(*pendingauthzModel)
authz = authD.Authorization
} else {
authObj, err = tx.Get(authzModel{}, id)
if err != nil {
tx.Rollback()
@ -165,29 +191,49 @@ func (ssa *SQLStorageAuthority) GetAuthorization(id string) (authz core.Authoriz
}
authD := authObj.(*authzModel)
authz = authD.Authorization
}
err = tx.Commit()
var challObjs []challModel
_, err = tx.Select(
&challObjs,
getChallengesQuery,
map[string]interface{}{"authID": authz.ID},
)
if err != nil {
tx.Rollback()
return
}
authD := *authObj.(*pendingauthzModel)
authz = authD.Authorization
var challs []core.Challenge
for _, c := range challObjs {
chall, err := modelToChallenge(&c)
if err != nil {
tx.Rollback()
return core.Authorization{}, err
}
challs = append(challs, chall)
}
authz.Challenges = challs
err = tx.Commit()
return
}
// Get the valid authorization with biggest expire date for a given domain and registrationId
// GetLatestValidAuthorization gets the valid authorization with biggest expire date for a given domain and registrationId
func (ssa *SQLStorageAuthority) GetLatestValidAuthorization(registrationId int64, identifier core.AcmeIdentifier) (authz core.Authorization, err error) {
ident, err := json.Marshal(identifier)
if err != nil {
return
}
err = ssa.dbMap.SelectOne(&authz, "SELECT id, identifier, registrationID, status, expires, challenges, combinations "+
"FROM authz "+
var auth core.Authorization
err = ssa.dbMap.SelectOne(&auth, "SELECT id FROM authz "+
"WHERE identifier = :identifier AND registrationID = :registrationId AND status = 'valid' "+
"ORDER BY expires DESC LIMIT 1",
map[string]interface{}{"identifier": string(ident), "registrationId": registrationId})
return
if err != nil {
return
}
return ssa.GetAuthorization(auth.ID)
}
// GetCertificateByShortSerial takes an id consisting of the first, sequential half of a
@ -386,8 +432,22 @@ func (ssa *SQLStorageAuthority) NewPendingAuthorization(authz core.Authorization
return
}
for _, c := range authz.Challenges {
chall, err := challengeToModel(&c, pendingAuthz.ID)
if err != nil {
tx.Rollback()
return core.Authorization{}, err
}
err = tx.Insert(chall)
if err != nil {
tx.Rollback()
return core.Authorization{}, err
}
}
err = tx.Commit()
output = pendingAuthz.Authorization
output.Challenges = authz.Challenges
return
}
@ -429,6 +489,12 @@ func (ssa *SQLStorageAuthority) UpdatePendingAuthorization(authz core.Authorizat
return
}
err = updateChallenges(authz.ID, authz.Challenges, tx)
if err != nil {
tx.Rollback()
return
}
err = tx.Commit()
return
}
@ -484,6 +550,12 @@ func (ssa *SQLStorageAuthority) FinalizeAuthorization(authz core.Authorization)
return
}
err = updateChallenges(authz.ID, authz.Challenges, tx)
if err != nil {
tx.Rollback()
return
}
err = tx.Commit()
return
}

View File

@ -19,13 +19,13 @@ import (
jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/mocks"
"github.com/letsencrypt/boulder/sa/satest"
"github.com/letsencrypt/boulder/test"
)
var log = mocks.UseMockLog()
const dbConnStr = "mysql+tcp://boulder@localhost:3306/boulder_sa_test"
// TODO(jmhodges): change this to boulder_sa_test database
var dbConnStr = "mysql+tcp://boulder@localhost:3306/boulder_test"
var log = mocks.UseMockLog()
// initSA constructs a SQLStorageAuthority and a clean up function
// that should be defer'ed to the end of the test.
@ -39,26 +39,11 @@ func initSA(t *testing.T) (*SQLStorageAuthority, func()) {
if err != nil {
t.Fatalf("Failed to create SA: %s", err)
}
if err = sa.CreateTablesIfNotExists(); err != nil {
t.Fatalf("Failed to create tables: %s", err)
}
if err = sa.dbMap.TruncateTables(); err != nil {
t.Fatalf("Failed to truncate tables: %s", err)
}
return sa, func() {
if err = sa.dbMap.TruncateTables(); err != nil {
t.Fatalf("Failed to truncate tables after the test: %s", err)
}
sa.dbMap.Db.Close()
}
cleanUp := test.ResetTestDatabase(t, dbMap.Db)
return sa, cleanUp
}
var (
theKey = `{
"kty": "RSA",
"n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw",
"e": "AQAB"
}`
anotherKey = `{
"kty":"RSA",
"n": "vd7rZIoTLEe-z1_8G1FcXSw9CQFEJgV4g9V277sER7yx5Qjz_Pkf2YVth6wwwFJEmzc0hoKY-MMYFNwBE4hQHw",
@ -70,12 +55,7 @@ func TestAddRegistration(t *testing.T) {
sa, cleanUp := initSA(t)
defer cleanUp()
var jwk jose.JsonWebKey
err := json.Unmarshal([]byte(theKey), &jwk)
if err != nil {
t.Errorf("JSON unmarshal error: %+v", err)
return
}
jwk := satest.GoodJWK()
contact, err := core.ParseAcmeURL("mailto:foo@example.com")
if err != nil {
@ -132,9 +112,7 @@ func TestNoSuchRegistrationErrors(t *testing.T) {
t.Errorf("GetRegistration: expected NoSuchRegistrationError, got %T type error (%s)", err, err)
}
var jwk jose.JsonWebKey
err = json.Unmarshal([]byte(theKey), &jwk)
test.AssertNotError(t, err, "Unmarshal")
jwk := satest.GoodJWK()
_, err = sa.GetRegistrationByKey(jwk)
if _, ok := err.(NoSuchRegistrationError); !ok {
t.Errorf("GetRegistrationByKey: expected a NoSuchRegistrationError, got %T type error (%s)", err, err)
@ -150,7 +128,8 @@ func TestAddAuthorization(t *testing.T) {
sa, cleanUp := initSA(t)
defer cleanUp()
PA := core.Authorization{}
reg := satest.CreateWorkingRegistration(t, sa)
PA := core.Authorization{RegistrationID: reg.ID}
PA, err := sa.NewPendingAuthorization(PA)
test.AssertNotError(t, err, "Couldn't create new pending authorization")
@ -163,21 +142,12 @@ func TestAddAuthorization(t *testing.T) {
expectedPa := core.Authorization{ID: PA.ID}
test.AssertMarshaledEquals(t, dbPa.ID, expectedPa.ID)
var jwk jose.JsonWebKey
err = json.Unmarshal([]byte(theKey), &jwk)
if err != nil {
t.Errorf("JSON unmarshal error: %+v", err)
return
}
chall := core.SimpleHTTPChallenge()
combos := make([][]int, 1)
combos[0] = []int{0, 1}
exp := time.Now().AddDate(0, 0, 1)
identifier := core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "wut.com"}
newPa := core.Authorization{ID: PA.ID, Identifier: identifier, RegistrationID: 0, Status: core.StatusPending, Expires: &exp, Challenges: []core.Challenge{chall}, Combinations: combos}
newPa := core.Authorization{ID: PA.ID, Identifier: identifier, RegistrationID: reg.ID, Status: core.StatusPending, Expires: &exp, Combinations: combos}
err = sa.UpdatePendingAuthorization(newPa)
test.AssertNotError(t, err, "Couldn't update pending authorization with ID "+PA.ID)
@ -190,9 +160,16 @@ func TestAddAuthorization(t *testing.T) {
}
func CreateDomainAuth(t *testing.T, domainName string, sa *SQLStorageAuthority) (authz core.Authorization) {
return CreateDomainAuthWithRegId(t, domainName, sa, 42)
}
func CreateDomainAuthWithRegId(t *testing.T, domainName string, sa *SQLStorageAuthority, regID int64) (authz core.Authorization) {
// create pending auth
authz, err := sa.NewPendingAuthorization(core.Authorization{})
test.AssertNotError(t, err, "Couldn't create new pending authorization")
authz, err := sa.NewPendingAuthorization(core.Authorization{RegistrationID: regID, Challenges: []core.Challenge{core.Challenge{}}})
if err != nil {
t.Fatalf("Couldn't create new pending authorization: %s", err)
}
test.Assert(t, authz.ID != "", "ID shouldn't be blank")
// prepare challenge for auth
@ -206,7 +183,6 @@ func CreateDomainAuth(t *testing.T, domainName string, sa *SQLStorageAuthority)
// validate pending auth
authz.Status = core.StatusPending
authz.Identifier = core.AcmeIdentifier{Type: core.IdentifierDNS, Value: domainName}
authz.RegistrationID = 42
authz.Expires = &exp
authz.Challenges = []core.Challenge{chall}
authz.Combinations = combos
@ -227,8 +203,10 @@ func TestGetLatestValidAuthorizationBasic(t *testing.T) {
authz, err := sa.GetLatestValidAuthorization(0, core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "example.org"})
test.AssertError(t, err, "Should not have found a valid auth for example.org")
reg := satest.CreateWorkingRegistration(t, sa)
// authorize "example.org"
authz = CreateDomainAuth(t, "example.org", sa)
authz = CreateDomainAuthWithRegId(t, "example.org", sa, reg.ID)
// finalize auth
authz.Status = core.StatusValid
@ -240,12 +218,12 @@ func TestGetLatestValidAuthorizationBasic(t *testing.T) {
test.AssertError(t, err, "Should not have found a valid auth for example.org and regID 0")
// get authorized domain
authz, err = sa.GetLatestValidAuthorization(42, core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "example.org"})
authz, err = sa.GetLatestValidAuthorization(reg.ID, core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "example.org"})
test.AssertNotError(t, err, "Should have found a valid auth for example.org and regID 42")
test.AssertEquals(t, authz.Status, core.StatusValid)
test.AssertEquals(t, authz.Identifier.Type, core.IdentifierDNS)
test.AssertEquals(t, authz.Identifier.Value, "example.org")
test.AssertEquals(t, authz.RegistrationID, int64(42))
test.AssertEquals(t, authz.RegistrationID, reg.ID)
}
// Ensure we get the latest valid authorization for an ident
@ -255,11 +233,11 @@ func TestGetLatestValidAuthorizationMultiple(t *testing.T) {
domain := "example.org"
ident := core.AcmeIdentifier{Type: core.IdentifierDNS, Value: domain}
regID := int64(42)
var err error
reg := satest.CreateWorkingRegistration(t, sa)
// create invalid authz
authz := CreateDomainAuth(t, domain, sa)
authz := CreateDomainAuthWithRegId(t, domain, sa, reg.ID)
exp := time.Now().AddDate(0, 0, 10) // expire in 10 day
authz.Expires = &exp
authz.Status = core.StatusInvalid
@ -267,11 +245,11 @@ func TestGetLatestValidAuthorizationMultiple(t *testing.T) {
test.AssertNotError(t, err, "Couldn't finalize pending authorization with ID "+authz.ID)
// should not get the auth
authz, err = sa.GetLatestValidAuthorization(regID, ident)
authz, err = sa.GetLatestValidAuthorization(reg.ID, ident)
test.AssertError(t, err, "Should not have found a valid auth for "+domain)
// create valid auth
authz = CreateDomainAuth(t, domain, sa)
authz = CreateDomainAuthWithRegId(t, domain, sa, reg.ID)
exp = time.Now().AddDate(0, 0, 1) // expire in 1 day
authz.Expires = &exp
authz.Status = core.StatusValid
@ -279,27 +257,27 @@ func TestGetLatestValidAuthorizationMultiple(t *testing.T) {
test.AssertNotError(t, err, "Couldn't finalize pending authorization with ID "+authz.ID)
// should get the valid auth even if it's expire date is lower than the invalid one
authz, err = sa.GetLatestValidAuthorization(regID, ident)
authz, err = sa.GetLatestValidAuthorization(reg.ID, ident)
test.AssertNotError(t, err, "Should have found a valid auth for "+domain)
test.AssertEquals(t, authz.Status, core.StatusValid)
test.AssertEquals(t, authz.Identifier.Type, ident.Type)
test.AssertEquals(t, authz.Identifier.Value, ident.Value)
test.AssertEquals(t, authz.RegistrationID, regID)
test.AssertEquals(t, authz.RegistrationID, reg.ID)
// create a newer auth
newAuthz := CreateDomainAuth(t, domain, sa)
newAuthz := CreateDomainAuthWithRegId(t, domain, sa, reg.ID)
exp = time.Now().AddDate(0, 0, 2) // expire in 2 day
newAuthz.Expires = &exp
newAuthz.Status = core.StatusValid
err = sa.FinalizeAuthorization(newAuthz)
test.AssertNotError(t, err, "Couldn't finalize pending authorization with ID "+newAuthz.ID)
authz, err = sa.GetLatestValidAuthorization(regID, ident)
authz, err = sa.GetLatestValidAuthorization(reg.ID, ident)
test.AssertNotError(t, err, "Should have found a valid auth for "+domain)
test.AssertEquals(t, authz.Status, core.StatusValid)
test.AssertEquals(t, authz.Identifier.Type, ident.Type)
test.AssertEquals(t, authz.Identifier.Value, ident.Value)
test.AssertEquals(t, authz.RegistrationID, regID)
test.AssertEquals(t, authz.RegistrationID, reg.ID)
// make sure we got the latest auth
test.AssertEquals(t, authz.ID, newAuthz.ID)
}
@ -308,11 +286,13 @@ func TestAddCertificate(t *testing.T) {
sa, cleanUp := initSA(t)
defer cleanUp()
reg := satest.CreateWorkingRegistration(t, sa)
// An example cert taken from EFF's website
certDER, err := ioutil.ReadFile("www.eff.org.der")
test.AssertNotError(t, err, "Couldn't read example cert DER")
digest, err := sa.AddCertificate(certDER, 1)
digest, err := sa.AddCertificate(certDER, reg.ID)
test.AssertNotError(t, err, "Couldn't add www.eff.org.der")
test.AssertEquals(t, digest, "qWoItDZmR4P9eFbeYgXXP3SR4ApnkQj8x4LsB_ORKBo")
@ -335,7 +315,7 @@ func TestAddCertificate(t *testing.T) {
certDER2, err := ioutil.ReadFile("test-cert.der")
test.AssertNotError(t, err, "Couldn't read example cert DER")
digest2, err := sa.AddCertificate(certDER2, 1)
digest2, err := sa.AddCertificate(certDER2, reg.ID)
test.AssertNotError(t, err, "Couldn't add test-cert.der")
test.AssertEquals(t, digest2, "CMVYqWzyqUW7pfBF2CxL0Uk6I0Upsk7p4EWSnd_vYx4")
@ -389,10 +369,11 @@ func TestUpdateOCSP(t *testing.T) {
sa, cleanUp := initSA(t)
defer cleanUp()
reg := satest.CreateWorkingRegistration(t, sa)
// Add a cert to the DB to test with.
certDER, err := ioutil.ReadFile("www.eff.org.der")
test.AssertNotError(t, err, "Couldn't read example cert DER")
_, err = sa.AddCertificate(certDER, 1)
_, err = sa.AddCertificate(certDER, reg.ID)
test.AssertNotError(t, err, "Couldn't add www.eff.org.der")
serial := "00000000000000000000000000021bd4"

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python2.7
import atexit
import os
import re
import shutil
import socket
import subprocess
@ -11,7 +12,7 @@ import startservers
class ExitStatus:
OK, PythonFailure, NodeFailure, Error = range(4)
OK, PythonFailure, NodeFailure, Error, OCSPFailure = range(5)
class ProcInfo:
@ -32,6 +33,34 @@ def die(status):
exit_status = status
sys.exit(exit_status)
def get_ocsp(certFile):
openssl_ocsp_cmd = ("""
openssl x509 -in %s -out %s.pem -inform der -outform pem;
openssl ocsp -no_nonce -issuer ../test-ca.pem -CAfile ../test-ca.pem -cert %s.pem -url http://localhost:4002
""" % (certFile, certFile, certFile))
try:
print openssl_ocsp_cmd
output = subprocess.check_output(openssl_ocsp_cmd, shell=True)
except subprocess.CalledProcessError as e:
output = e.output
print output
print "OpenSSL returned non-zero: %s" % e
die(ExitStatus.OCSPFailure)
print output
return output
def verify_ocsp_good(certFile):
output = get_ocsp(certFile)
if not re.search(": good", output):
print "Expected OCSP response 'good', got something else."
die(ExitStatus.OCSPFailure)
def verify_ocsp_revoked(certFile):
output = get_ocsp(certFile)
if not re.search(": revoked", output):
print "Expected OCSP response 'revoked', got something else."
die(ExitStatus.OCSPFailure)
pass
def run_node_test():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@ -46,19 +75,26 @@ def run_node_test():
if subprocess.Popen('npm install', shell=True).wait() != 0:
print("\n Installing NPM modules failed")
die(ExitStatus.Error)
certFile = os.path.join(tempdir, "cert.der")
keyFile = os.path.join(tempdir, "key.pem")
if subprocess.Popen('''
node test.js --email foo@letsencrypt.org --agree true \
--domains foo.com --new-reg http://localhost:4000/acme/new-reg \
--certKey %s/key.pem --cert %s/cert.der
''' % (tempdir, tempdir), shell=True).wait() != 0:
--certKey %s --cert %s
''' % (keyFile, certFile), shell=True).wait() != 0:
print("\nIssuing failed")
die(ExitStatus.NodeFailure)
verify_ocsp_good(certFile)
if subprocess.Popen('''
node revoke.js %s/cert.der %s/key.pem http://localhost:4000/acme/revoke-cert
''' % (tempdir, tempdir), shell=True).wait() != 0:
node revoke.js %s %s http://localhost:4000/acme/revoke-cert
''' % (certFile, keyFile), shell=True).wait() != 0:
print("\nRevoking failed")
die(ExitStatus.NodeFailure)
verify_ocsp_revoked(certFile)
return 0
@ -80,7 +116,7 @@ def cleanup():
if exit_status == ExitStatus.OK:
print("\n\nSUCCESS")
else:
print("\n\nFAILURE")
print("\n\nFAILURE %d" % exit_status)
exit_status = ExitStatus.OK

View File

@ -48,7 +48,7 @@
"ca": {
"serialPrefix": 255,
"profile": "ee",
"dbConnect": "mysql+tcp://boulder@localhost:3306/boulder_test",
"dbConnect": "mysql+tcp://boulder@localhost:3306/boulder_ca_integration",
"debugAddr": "localhost:8001",
"testMode": true,
"_comment": "This should only be present in testMode. In prod use an HSM.",
@ -71,10 +71,10 @@
"backdate": "1h",
"is_ca": false,
"issuer_urls": [
"http://int-x1.letsencrypt.org/cert"
"http://localhost:4000/acme/issuer-cert"
],
"ocsp_url": "http://int-x1.letsencrypt.org/ocsp",
"crl_url": "http://int-x1.letsencrypt.org/crl",
"ocsp_url": "http://localhost:4002/ocsp",
"crl_url": "http://example.com/crl",
"policies": [
{
"ID": "2.23.140.1.2.1"
@ -118,7 +118,7 @@
},
"sa": {
"dbConnect": "mysql+tcp://boulder@localhost:3306/boulder_test",
"dbConnect": "mysql+tcp://boulder@localhost:3306/boulder_sa_integration",
"debugAddr": "localhost:8003"
},
@ -128,23 +128,22 @@
},
"sql": {
"SQLDebug": true,
"CreateTables": true
"SQLDebug": true
},
"revoker": {
"dbConnect": "mysql+tcp://boulder@localhost:3306/boulder_test"
"dbConnect": "mysql+tcp://boulder@localhost:3306/boulder_sa_integration"
},
"ocspResponder": {
"dbConnect": "mysql+tcp://boulder@localhost:3306/boulder_test",
"dbConnect": "mysql+tcp://boulder@localhost:3306/boulder_sa_integration",
"path": "/",
"listenAddress": "localhost:4001",
"listenAddress": "localhost:4002",
"debugAddr": "localhost:8005"
},
"ocspUpdater": {
"dbConnect": "mysql+tcp://boulder@localhost:3306/boulder_test",
"dbConnect": "mysql+tcp://boulder@localhost:3306/boulder_sa_integration",
"minTimeToExpiry": "72h",
"debugAddr": "localhost:8006"
},
@ -158,7 +157,7 @@
"port": "25",
"username": "cert-master@example.com",
"password": "password",
"dbConnect": "mysql+tcp://boulder@localhost:3306/boulder_test",
"dbConnect": "mysql+tcp://boulder@localhost:3306/boulder_sa_integration",
"messageLimit": 0,
"nagTimes": ["24h", "72h", "168h", "336h"],
"emailTemplate": "test/example-expiration-template",
@ -173,5 +172,5 @@
"dnsTimeout": "10s"
},
"subscriberAgreementURL": "http://localhost:4000/terms/v1"
"subscriberAgreementURL": "http://localhost:4001/terms/v1"
}

View File

@ -114,8 +114,7 @@
},
"sql": {
"SQLDebug": true,
"CreateTables": true
"SQLDebug": true
},
"revoker": {

View File

@ -7,5 +7,21 @@ function die() {
exit 1
}
mysql -u root -e "create database boulder_test; grant all privileges on boulder_test.* to 'boulder'@'localhost'" || die "unable to create boulder_test"
echo "created boulder_test database"
SERVICES="ca
sa"
DBENVS="development
test
integration"
for svc in $SERVICES; do
for dbenv in $DBENVS; do
db="boulder_${svc}_${dbenv}"
mysql -u root -e "drop database if exists \`${db}\`; create database if not exists \`${db}\`; grant all privileges on ${db}.* to 'boulder'@'localhost'" || die "unable to create ${db}"
echo "created empty ${db} database"
goose -path=./$svc/_db/ -env=$dbenv up || die "unable to migrate ${db}"
echo "migrated ${db} database"
done
done
echo "created all databases"

84
test/db.go Normal file
View File

@ -0,0 +1,84 @@
package test
import (
"database/sql"
"io"
"testing"
)
var (
_ CleanUpDB = &sql.DB{}
)
// CleanUpDB is an interface with only what is needed to delete all
// rows in all tables in a database plus close the database
// connection. It is satisfied by *sql.DB.
type CleanUpDB interface {
Exec(query string, args ...interface{}) (sql.Result, error)
Query(query string, args ...interface{}) (*sql.Rows, error)
io.Closer
}
// ResetTestDatabase deletes all rows in all tables available to the
// passed in CleanUpDB, failing the tests if that errors and returning
// a clean up function that will attempt the same plus close the
// database. "Tables available" means all tables that can be seen in
// the MariaDB configuration by the database user except for ones that
// are configuration only like goose_db_version (for migrations) or
// the ones describing the internal configuration of the server.To be
// used only in test code.
func ResetTestDatabase(t *testing.T, db CleanUpDB) func() {
if err := deleteEverythingInAllTables(db); err != nil {
t.Fatalf("Failed to delete everything: %s", err)
}
return func() {
if err := deleteEverythingInAllTables(db); err != nil {
t.Fatalf("Failed to truncate tables after the test: %s", err)
}
db.Close()
}
}
// clearEverythingInAllTables deletes all rows in the tables
// available to the CleanUpDB passed in and resets the autoincrement
// counters. See allTableNamesInDB for what is meant by "all tables
// available". To be used only in test code.
func deleteEverythingInAllTables(db CleanUpDB) error {
ts, err := allTableNamesInDB(db)
if err != nil {
return nil
}
for _, tn := range ts {
// 1 = 1 here prevents the MariaDB i_am_a_dummy setting from
// rejecting the DELETE for not having a WHERE clause.
_, err := db.Exec("delete from `" + tn + "` where 1 = 1")
if err != nil {
return err
}
}
return nil
}
// allTableNamesInDB returns the names of the tables available to the
// CleanUpDB passed in. "Tables available" means all tables that can
// be seen in the MariaDB configuration by the database user except
// for ones that are configuration only like goose_db_version (for
// migrations) or the ones describing the internal configuration of
// the server. To be used only in test code.
func allTableNamesInDB(db CleanUpDB) ([]string, error) {
r, err := db.Query("select table_name from information_schema.tables t where t.table_schema = DATABASE() and t.table_name != 'goose_db_version';")
if err != nil {
return nil, err
}
var ts []string
for r.Next() {
tableName := ""
err = r.Scan(&tableName)
if err != nil {
return nil, err
}
ts = append(ts, tableName)
}
return ts, r.Err()
}

View File

@ -19,8 +19,10 @@ function main() {
console.log('Usage: js revoke.js cert.der key.pem REVOKE_URL');
process.exit(1);
}
var certFile = process.argv[2];
console.log('Attempting to revoke', certFile);
var key = crypto.importPemPrivateKey(fs.readFileSync(process.argv[3]));
var certDER = fs.readFileSync(process.argv[2])
var certDER = fs.readFileSync(certFile)
var revokeUrl = process.argv[4];
var certDERB64URL = util.b64enc(new Buffer(certDER))
var revokeMessage = JSON.stringify({
@ -34,9 +36,8 @@ function main() {
console.log(error);
process.exit(1);
} else if (response.statusCode != 200) {
console.log("Got non-200 response: ", response.statusCode);
console.log("Got non-200 response asking for nonce (non-fatal):", response.statusCode);
}
console.log(response.headers);
var nonce = response.headers["replay-nonce"];
if (!nonce) {
console.log("Server HEAD response did not include a replay nonce");
@ -45,16 +46,14 @@ function main() {
var jws = crypto.generateSignature(key, new Buffer(revokeMessage), nonce);
var payload = JSON.stringify(jws);
console.log(payload);
var req = request.post(revokeUrl, function(error, response) {
if (error) {
if (error || Math.floor(response.statusCode / 100) != 2) {
console.log('Error: ', error);
process.exit(1);
} else {
console.log("Revocation request successful.");
}
console.log(response.statusCode);
console.log(response.headers);
console.log(response.body);
});
req.write(payload);
req.end();

View File

@ -62,6 +62,7 @@ def start(race_detection):
'cmd/boulder-sa',
'cmd/boulder-ca',
'cmd/boulder-va',
'cmd/ocsp-responder',
'test/dns-test-srv']:
try:
processes.append(run(prog, race_detection))

View File

@ -17,6 +17,7 @@ import (
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
@ -29,6 +30,9 @@ import (
)
const maxCNAME = 16 // Prevents infinite loops. Same limit as BIND.
const maxRedirect = 10
var validationTimeout = time.Second * 5
// Returned by CheckCAARecords if it has to follow too many
// consecutive CNAME lookups.
@ -36,12 +40,14 @@ var ErrTooManyCNAME = errors.New("too many CNAME/DNAME lookups")
// ValidationAuthorityImpl represents a VA
type ValidationAuthorityImpl struct {
RA core.RegistrationAuthority
log *blog.AuditLogger
DNSResolver core.DNSResolver
IssuerDomain string
TestMode bool
UserAgent string
RA core.RegistrationAuthority
log *blog.AuditLogger
DNSResolver core.DNSResolver
IssuerDomain string
simpleHTTPPort int
simpleHTTPSPort int
dvsniPort int
UserAgent string
}
// NewValidationAuthorityImpl constructs a new VA, and may place it
@ -49,7 +55,23 @@ type ValidationAuthorityImpl struct {
func NewValidationAuthorityImpl(tm bool) ValidationAuthorityImpl {
logger := blog.GetAuditLogger()
logger.Notice("Validation Authority Starting")
return ValidationAuthorityImpl{log: logger, TestMode: tm}
// TODO(jsha): Remove TestMode entirely. Instead, the various validation ports
// should be exported, so the cmd file can set them based on a config.
if tm {
return ValidationAuthorityImpl{
log: logger,
simpleHTTPPort: 5001,
simpleHTTPSPort: 5001,
dvsniPort: 5001,
}
} else {
return ValidationAuthorityImpl{
log: logger,
simpleHTTPPort: 80,
simpleHTTPSPort: 443,
dvsniPort: 443,
}
}
}
// Used for audit logging
@ -63,7 +85,6 @@ type verificationRequestEvent struct {
}
func verifyValidationJWS(validation *jose.JsonWebSignature, accountKey *jose.JsonWebKey, target map[string]interface{}) error {
if len(validation.Signatures) > 1 {
return fmt.Errorf("Too many signatures on validation JWS")
}
@ -98,25 +119,82 @@ func verifyValidationJWS(validation *jose.JsonWebSignature, accountKey *jose.Jso
return nil
}
// Validation methods
// setChallengeErrorFromDNSError checks the error returned from Lookup...
// problemDetailsFromDNSError checks the error returned from Lookup...
// methods and tests if the error was an underlying net.OpError or an error
// caused by resolver returning SERVFAIL or other invalid Rcodes and sets
// the challenge.Error field accordingly.
func setChallengeErrorFromDNSError(err error, challenge *core.Challenge) {
challenge.Error = &core.ProblemDetails{Type: core.ConnectionProblem}
// caused by resolver returning SERVFAIL or other invalid Rcodes and returns
// the relevant core.ProblemDetails.
func problemDetailsFromDNSError(err error) *core.ProblemDetails {
problem := &core.ProblemDetails{Type: core.ConnectionProblem}
if netErr, ok := err.(*net.OpError); ok {
if netErr.Timeout() {
challenge.Error.Detail = "DNS query timed out"
problem.Detail = "DNS query timed out"
} else if netErr.Temporary() {
challenge.Error.Detail = "Temporary network connectivity error"
problem.Detail = "Temporary network connectivity error"
}
} else {
challenge.Error.Detail = "Server failure at resolver"
problem.Detail = "Server failure at resolver"
}
return problem
}
// getAddr will query for all A records associated with hostname and return the
// prefered address, the first net.IP in the addrs slice, and all addresses resolved.
// This is the same choice made by the Go internal resolution library used by
// net/http, except we only send A queries and accept IPv4 addresses.
// TODO(#593): Add IPv6 support
func (va ValidationAuthorityImpl) getAddr(hostname string) (addr net.IP, addrs []net.IP, problem *core.ProblemDetails) {
addrs, _, err := va.DNSResolver.LookupHost(hostname)
if err != nil {
problem = problemDetailsFromDNSError(err)
va.log.Debug(fmt.Sprintf("%s DNS failure: %s", hostname, err))
return
}
if len(addrs) == 0 {
problem = &core.ProblemDetails{
Type: core.UnknownHostProblem,
Detail: fmt.Sprintf("No IPv4 addresses found for %s", hostname),
}
return
}
addr = addrs[0]
va.log.Info(fmt.Sprintf("Resolved addresses for %s [using %s]: %s", hostname, addr, addrs))
return
}
type dialer struct {
record core.ValidationRecord
}
func (d *dialer) Dial(_, _ string) (net.Conn, error) {
realDialer := net.Dialer{Timeout: validationTimeout}
return realDialer.Dial("tcp", net.JoinHostPort(d.record.AddressUsed.String(), d.record.Port))
}
// resolveAndConstructDialer gets the prefered address using va.getAddr and returns
// the chosen address and dialer for that address and correct port.
func (va ValidationAuthorityImpl) resolveAndConstructDialer(name, defaultPort string) (dialer, *core.ProblemDetails) {
port := fmt.Sprintf("%d", va.simpleHTTPPort)
if defaultPort != "" {
port = defaultPort
}
d := dialer{
record: core.ValidationRecord{
Hostname: name,
Port: port,
},
}
addr, allAddrs, err := va.getAddr(name)
if err != nil {
return d, err
}
d.record.AddressesResolved = allAddrs
d.record.AddressUsed = addr
return d, nil
}
// Validation methods
func (va ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdentifier, input core.Challenge, accountKey jose.JsonWebKey) (core.Challenge, error) {
challenge := input
@ -130,23 +208,29 @@ func (va ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdentif
va.log.Debug(fmt.Sprintf("SimpleHTTP [%s] Identifier failure", identifier))
return challenge, challenge.Error
}
hostName := identifier.Value
host := identifier.Value
var scheme string
var port int
if input.TLS == nil || (input.TLS != nil && *input.TLS) {
scheme = "https"
port = va.simpleHTTPSPort
} else {
scheme = "http"
port = va.simpleHTTPPort
}
if va.TestMode {
hostName = "localhost:5001"
}
portString := fmt.Sprintf("%d", port)
hostPort := net.JoinHostPort(host, portString)
url := fmt.Sprintf("%s://%s/.well-known/acme-challenge/%s", scheme, hostName, challenge.Token)
url := url.URL{
Scheme: scheme,
Host: hostPort,
Path: fmt.Sprintf(".well-known/acme-challenge/%s", challenge.Token),
}
// AUDIT[ Certificate Requests ] 11917fa4-10ef-4e0d-9105-bacbe7836a3c
va.log.Audit(fmt.Sprintf("Attempting to validate Simple%s for %s", strings.ToUpper(scheme), url))
httpRequest, err := http.NewRequest("GET", url, nil)
httpRequest, err := http.NewRequest("GET", url.String(), nil)
if err != nil {
challenge.Error = &core.ProblemDetails{
Type: core.MalformedProblem,
@ -161,7 +245,16 @@ func (va ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdentif
httpRequest.Header["User-Agent"] = []string{va.UserAgent}
}
httpRequest.Host = hostName
httpRequest.Host = hostPort
dialer, prob := va.resolveAndConstructDialer(host, portString)
dialer.record.URL = url.String()
challenge.ValidationRecord = append(challenge.ValidationRecord, dialer.record)
if prob != nil {
challenge.Status = core.StatusInvalid
challenge.Error = prob
return challenge, prob
}
tr := &http.Transport{
// We are talking to a client that does not yet have a certificate,
// so we accept a temporary, invalid one.
@ -169,18 +262,51 @@ func (va ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdentif
// We don't expect to make multiple requests to a client, so close
// connection immediately.
DisableKeepAlives: true,
// Intercept Dial in order to connect to the IP address we
// select.
Dial: dialer.Dial,
}
logRedirect := func(req *http.Request, via []*http.Request) error {
va.log.Info(fmt.Sprintf("validateSimpleHTTP [%s] redirect from %q to %q", identifier, via[len(via)-1].URL.String(), req.URL.String()))
if len(challenge.ValidationRecord) >= maxRedirect {
return fmt.Errorf("Too many redirects")
}
reqHost := req.URL.Host
reqPort := ""
if strings.Contains(reqHost, ":") {
splitHost := strings.SplitN(reqHost, ":", 2)
if len(splitHost) <= 1 {
return fmt.Errorf("Malformed host")
}
reqHost, reqPort = splitHost[0], splitHost[1]
portNum, err := strconv.Atoi(reqPort)
if err != nil {
return err
}
if portNum < 0 || portNum > 65535 {
return fmt.Errorf("Invalid port number in redirect")
}
} else if strings.ToLower(req.URL.Scheme) == "https" {
reqPort = "443"
}
dialer, err := va.resolveAndConstructDialer(reqHost, reqPort)
dialer.record.URL = req.URL.String()
challenge.ValidationRecord = append(challenge.ValidationRecord, dialer.record)
if err != nil {
return err
}
tr.Dial = dialer.Dial
va.log.Info(fmt.Sprintf("validateSimpleHTTP [%s] redirect from %q to %q [%s]", identifier, via[len(via)-1].URL.String(), req.URL.String(), dialer.record.AddressUsed))
return nil
}
client := http.Client{
Transport: tr,
CheckRedirect: logRedirect,
Timeout: 5 * time.Second,
Timeout: validationTimeout,
}
httpResponse, err := client.Do(httpRequest)
if err != nil {
challenge.Status = core.StatusInvalid
challenge.Error = &core.ProblemDetails{
@ -195,8 +321,8 @@ func (va ValidationAuthorityImpl) validateSimpleHTTP(identifier core.AcmeIdentif
challenge.Status = core.StatusInvalid
challenge.Error = &core.ProblemDetails{
Type: core.UnauthorizedProblem,
Detail: fmt.Sprintf("Invalid response from %s: %d",
url, httpResponse.StatusCode),
Detail: fmt.Sprintf("Invalid response from %s [%s]: %d",
url.String(), dialer.record.AddressUsed, httpResponse.StatusCode),
}
err = challenge.Error
return challenge, err
@ -288,14 +414,27 @@ func (va ValidationAuthorityImpl) validateDvsni(identifier core.AcmeIdentifier,
Z := hex.EncodeToString(h.Sum(nil))
ZName := fmt.Sprintf("%s.%s.%s", Z[:32], Z[32:], core.DVSNISuffix)
// Make a connection with SNI = nonceName
hostPort := identifier.Value + ":443"
if va.TestMode {
hostPort = "localhost:5001"
addr, allAddrs, problem := va.getAddr(identifier.Value)
challenge.ValidationRecord = []core.ValidationRecord{
core.ValidationRecord{
Hostname: identifier.Value,
AddressesResolved: allAddrs,
AddressUsed: addr,
},
}
if problem != nil {
challenge.Status = core.StatusInvalid
challenge.Error = problem
return challenge, challenge.Error
}
// Make a connection with SNI = nonceName
portString := fmt.Sprintf("%d", va.dvsniPort)
hostPort := net.JoinHostPort(addr.String(), portString)
challenge.ValidationRecord[0].Port = portString
va.log.Notice(fmt.Sprintf("DVSNI [%s] Attempting to validate DVSNI for %s %s",
identifier, hostPort, ZName))
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: 5 * time.Second}, "tcp", hostPort, &tls.Config{
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: validationTimeout}, "tcp", hostPort, &tls.Config{
ServerName: ZName,
InsecureSkipVerify: true,
})
@ -396,7 +535,7 @@ func (va ValidationAuthorityImpl) validateDNS(identifier core.AcmeIdentifier, in
if err != nil {
challenge.Status = core.StatusInvalid
setChallengeErrorFromDNSError(err, &challenge)
challenge.Error = problemDetailsFromDNSError(err)
va.log.Debug(fmt.Sprintf("%s [%s] DNS failure: %s", challenge.Type, identifier, err))
return challenge, challenge.Error
}
@ -419,9 +558,6 @@ func (va ValidationAuthorityImpl) validateDNS(identifier core.AcmeIdentifier, in
// Overall validation process
func (va ValidationAuthorityImpl) validate(authz core.Authorization, challengeIndex int, accountKey jose.JsonWebKey) {
// Select the first supported validation method
// XXX: Remove the "break" lines to process all supported validations
logEvent := verificationRequestEvent{
ID: authz.ID,
Requester: authz.RegistrationID,
@ -440,19 +576,22 @@ func (va ValidationAuthorityImpl) validate(authz core.Authorization, challengeIn
switch authz.Challenges[challengeIndex].Type {
case core.ChallengeTypeSimpleHTTP:
authz.Challenges[challengeIndex], err = va.validateSimpleHTTP(authz.Identifier, authz.Challenges[challengeIndex], accountKey)
break
case core.ChallengeTypeDVSNI:
authz.Challenges[challengeIndex], err = va.validateDvsni(authz.Identifier, authz.Challenges[challengeIndex], accountKey)
break
case core.ChallengeTypeDNS:
authz.Challenges[challengeIndex], err = va.validateDNS(authz.Identifier, authz.Challenges[challengeIndex], accountKey)
break
}
logEvent.Challenge = authz.Challenges[challengeIndex]
if err != nil {
logEvent.Error = err.Error()
} else if !authz.Challenges[challengeIndex].RecordsSane() {
chall := &authz.Challenges[challengeIndex]
chall.Status = core.StatusInvalid
chall.Error = &core.ProblemDetails{Type: core.ServerInternalProblem,
Detail: "Records for validation failed sanity check"}
logEvent.Error = chall.Error.Detail
}
logEvent.Challenge = authz.Challenges[challengeIndex]
}
// AUDIT[ Certificate Requests ] 11917fa4-10ef-4e0d-9105-bacbe7836a3c

View File

@ -20,6 +20,9 @@ import (
"math/big"
"net"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
"time"
@ -65,6 +68,9 @@ const pathWrongToken = "wrongtoken"
const path404 = "404"
const pathFound = "302"
const pathMoved = "301"
const pathRedirectLookup = "re-lookup"
const pathRedirectLookupInvalid = "re-lookup-invalid"
const pathRedirectPort = "port-redirect"
func createValidation(token string, enableTLS bool) string {
payload, _ := json.Marshal(map[string]interface{}{
@ -77,13 +83,16 @@ func createValidation(token string, enableTLS bool) string {
return obj.FullSerialize()
}
func simpleSrv(t *testing.T, token string, stopChan, waitChan chan bool, enableTLS bool) {
func simpleSrv(t *testing.T, token string, enableTLS bool) *httptest.Server {
m := http.NewServeMux()
defaultToken := token
currentToken := defaultToken
m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.Host, "localhost:") && r.Host != "other.valid" && r.Host != "other.valid:8080" {
t.Errorf("Bad Host header: " + r.Host)
}
if strings.HasSuffix(r.URL.Path, path404) {
t.Logf("SIMPLESRV: Got a 404 req\n")
http.NotFound(w, r)
@ -105,28 +114,32 @@ func simpleSrv(t *testing.T, token string, stopChan, waitChan chan bool, enableT
} else if strings.HasSuffix(r.URL.Path, "wait-long") {
t.Logf("SIMPLESRV: Got a wait-long req\n")
time.Sleep(time.Second * 10)
} else if strings.HasSuffix(r.URL.Path, "re-lookup") {
t.Logf("SIMPLESRV: Got a redirect req to a valid hostname\n")
if currentToken == defaultToken {
currentToken = "re-lookup"
}
http.Redirect(w, r, "http://other.valid/path", 302)
} else if strings.HasSuffix(r.URL.Path, "re-lookup-invalid") {
t.Logf("SIMPLESRV: Got a redirect req to a invalid hostname\n")
http.Redirect(w, r, "http://invalid.invalid/path", 302)
} else if strings.HasSuffix(r.URL.Path, "looper") {
t.Logf("SIMPLESRV: Got a loop req\n")
http.Redirect(w, r, r.URL.String(), 301)
} else if strings.HasSuffix(r.URL.Path, pathRedirectPort) {
t.Logf("SIMPLESRV: Got a port redirect req\n")
http.Redirect(w, r, "http://other.valid:8080/path", 302)
} else {
t.Logf("SIMPLESRV: Got a valid req\n")
fmt.Fprintf(w, "%s", createValidation(currentToken, enableTLS))
fmt.Fprint(w, createValidation(currentToken, enableTLS))
currentToken = defaultToken
}
})
server := &http.Server{Addr: "localhost:5001", Handler: m}
conn, err := net.Listen("tcp", server.Addr)
if err != nil {
waitChan <- true
t.Fatalf("Couldn't listen on %s: %s", server.Addr, err)
}
server := httptest.NewUnstartedServer(m)
go func() {
<-stopChan
conn.Close()
}()
var listener net.Listener
if !enableTLS {
listener = conn
server.Start()
} else {
template := &x509.Certificate{
SerialNumber: big.NewInt(1337),
@ -149,18 +162,17 @@ func simpleSrv(t *testing.T, token string, stopChan, waitChan chan bool, enableT
PrivateKey: &TheKey,
}
tlsConfig := &tls.Config{
server.TLS = &tls.Config{
Certificates: []tls.Certificate{*cert},
}
listener = tls.NewListener(conn, tlsConfig)
server.StartTLS()
}
waitChan <- true
server.Serve(listener)
return server
}
func dvsniSrv(t *testing.T, chall core.Challenge, stopChan, waitChan chan bool) {
func dvsniSrv(t *testing.T, chall core.Challenge) *httptest.Server {
encodedSig := core.B64enc(chall.Validation.Signatures[0].Signature)
h := sha256.New()
h.Write([]byte(encodedSig))
@ -201,52 +213,35 @@ func dvsniSrv(t *testing.T, chall core.Challenge, stopChan, waitChan chan bool)
NextProtos: []string{"http/1.1"},
}
httpsServer := &http.Server{Addr: "localhost:5001"}
conn, err := net.Listen("tcp", httpsServer.Addr)
if err != nil {
waitChan <- true
t.Fatalf("Couldn't listen on %s: %s", httpsServer.Addr, err)
}
tlsListener := tls.NewListener(conn, tlsConfig)
go func() {
<-stopChan
conn.Close()
}()
waitChan <- true
httpsServer.Serve(tlsListener)
hs := httptest.NewUnstartedServer(http.DefaultServeMux)
hs.TLS = tlsConfig
hs.StartTLS()
return hs
}
func brokenTLSSrv(t *testing.T, stopChan, waitChan chan bool) {
httpsServer := &http.Server{Addr: "localhost:5001"}
conn, err := net.Listen("tcp", httpsServer.Addr)
if err != nil {
waitChan <- true
t.Fatalf("Couldn't listen on %s: %s", httpsServer.Addr, err)
func brokenTLSSrv() *httptest.Server {
server := httptest.NewUnstartedServer(http.DefaultServeMux)
server.TLS = &tls.Config{
GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return nil, fmt.Errorf("Failing on purpose")
},
}
tlsListener := tls.NewListener(conn, &tls.Config{})
go func() {
<-stopChan
conn.Close()
}()
waitChan <- true
httpsServer.Serve(tlsListener)
server.StartTLS()
return server
}
func TestSimpleHttpTLS(t *testing.T) {
va := NewValidationAuthorityImpl(true)
va := NewValidationAuthorityImpl(false)
va.DNSResolver = &mocks.MockDNS{}
chall := core.Challenge{Type: core.ChallengeTypeSimpleHTTP, Token: expectedToken}
chall := core.Challenge{Type: core.ChallengeTypeSimpleHTTP, Token: expectedToken, ValidationRecord: []core.ValidationRecord{}}
stopChan := make(chan bool, 1)
waitChan := make(chan bool, 1)
go simpleSrv(t, expectedToken, stopChan, waitChan, true)
defer func() { stopChan <- true }()
<-waitChan
hs := simpleSrv(t, expectedToken, true)
defer hs.Close()
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
va.simpleHTTPSPort = port
log.Clear()
finChall, err := va.validateSimpleHTTP(ident, chall, AccountKey)
@ -258,23 +253,36 @@ func TestSimpleHttpTLS(t *testing.T) {
}
func TestSimpleHttp(t *testing.T) {
va := NewValidationAuthorityImpl(true)
va := NewValidationAuthorityImpl(false)
va.DNSResolver = &mocks.MockDNS{}
tls := false
chall := core.Challenge{Type: core.ChallengeTypeSimpleHTTP, Token: expectedToken, TLS: &tls}
chall := core.Challenge{Type: core.ChallengeTypeSimpleHTTP, Token: expectedToken, TLS: &tls, ValidationRecord: []core.ValidationRecord{}}
// NOTE: We do not attempt to shut down the server. The problem is that the
// "wait-long" handler sleeps for ten seconds, but this test finishes in less
// than that. So if we try to call hs.Close() at the end of the test, we'll be
// closing the test server while a request is still pending. Unfortunately,
// there appears to be an issue in httptest that trips Go's race detector when
// that happens, failing the test. So instead, we live with leaving the server
// around till the process exits.
hs := simpleSrv(t, expectedToken, tls)
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
// Attempt to fail a challenge by telling the VA to connect to a port we are
// not listening on.
va.simpleHTTPPort = port + 1
if va.simpleHTTPPort == 65536 {
va.simpleHTTPPort = port - 1
}
invalidChall, err := va.validateSimpleHTTP(ident, chall, AccountKey)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "Server's not up yet; expected refusal. Where did we connect?")
test.AssertError(t, err, "Server's down; expected refusal. Where did we connect?")
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
stopChan := make(chan bool, 1)
waitChan := make(chan bool, 1)
go simpleSrv(t, expectedToken, stopChan, waitChan, tls)
defer func() { stopChan <- true }()
<-waitChan
va.simpleHTTPPort = port
log.Clear()
finChall, err := va.validateSimpleHTTP(ident, chall, AccountKey)
test.AssertEquals(t, finChall.Status, core.StatusValid)
@ -320,12 +328,10 @@ func TestSimpleHttp(t *testing.T) {
test.AssertError(t, err, "IdentifierType IP shouldn't have worked.")
test.AssertEquals(t, invalidChall.Error.Type, core.MalformedProblem)
va.TestMode = false
invalidChall, err = va.validateSimpleHTTP(core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "always.invalid"}, chall, AccountKey)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "Domain name is invalid.")
test.AssertEquals(t, invalidChall.Error.Type, core.UnknownHostProblem)
va.TestMode = true
chall.Token = "wait-long"
started := time.Now()
@ -339,40 +345,133 @@ func TestSimpleHttp(t *testing.T) {
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
}
func TestDvsni(t *testing.T) {
va := NewValidationAuthorityImpl(true)
func TestSimpleHttpRedirectLookup(t *testing.T) {
va := NewValidationAuthorityImpl(false)
va.DNSResolver = &mocks.MockDNS{}
tls := false
chall := core.Challenge{Token: expectedToken, TLS: &tls, ValidationRecord: []core.ValidationRecord{}}
hs := simpleSrv(t, expectedToken, tls)
defer hs.Close()
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
va.simpleHTTPPort = port
log.Clear()
chall.Token = pathMoved
finChall, err := va.validateSimpleHTTP(ident, chall, AccountKey)
test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, chall.Token)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/301" to ".*/valid"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 2)
log.Clear()
chall.Token = pathFound
finChall, err = va.validateSimpleHTTP(ident, chall, AccountKey)
test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, chall.Token)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/302" to ".*/301"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/301" to ".*/valid"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 3)
log.Clear()
chall.Token = pathRedirectLookupInvalid
finChall, err = va.validateSimpleHTTP(ident, chall, AccountKey)
test.AssertEquals(t, finChall.Status, core.StatusInvalid)
test.AssertError(t, err, chall.Token)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`No IPv4 addresses found for invalid.invalid`)), 1)
log.Clear()
chall.Token = pathRedirectLookup
finChall, err = va.validateSimpleHTTP(ident, chall, AccountKey)
test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, chall.Token)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/re-lookup" to ".*other.valid/path"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for other.valid \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
log.Clear()
chall.Token = pathRedirectPort
finChall, err = va.validateSimpleHTTP(ident, chall, AccountKey)
fmt.Println(finChall.ValidationRecord)
test.AssertEquals(t, finChall.Status, core.StatusInvalid)
test.AssertError(t, err, chall.Token)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/port-redirect" to ".*other.valid:8080/path"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for other.valid \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
}
func TestSimpleHttpRedirectLoop(t *testing.T) {
va := NewValidationAuthorityImpl(false)
va.DNSResolver = &mocks.MockDNS{}
tls := false
chall := core.Challenge{Token: "looper", TLS: &tls, ValidationRecord: []core.ValidationRecord{}}
hs := simpleSrv(t, expectedToken, tls)
defer hs.Close()
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
va.simpleHTTPPort = port
log.Clear()
finChall, err := va.validateSimpleHTTP(ident, chall, AccountKey)
test.AssertEquals(t, finChall.Status, core.StatusInvalid)
test.AssertError(t, err, chall.Token)
fmt.Println(finChall)
}
func getPort(hs *httptest.Server) (int, error) {
url, err := url.Parse(hs.URL)
if err != nil {
return 0, err
}
_, portString, err := net.SplitHostPort(url.Host)
if err != nil {
return 0, err
}
port, err := strconv.ParseInt(portString, 10, 64)
if err != nil {
return 0, err
}
return int(port), nil
}
func TestDvsni(t *testing.T) {
chall := createChallenge(core.ChallengeTypeDVSNI)
invalidChall, err := va.validateDvsni(ident, chall, AccountKey)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "Server's not up yet; expected refusal. Where did we connect?")
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
hs := dvsniSrv(t, chall)
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
waitChan := make(chan bool, 1)
stopChan := make(chan bool, 1)
go dvsniSrv(t, chall, stopChan, waitChan)
defer func() { stopChan <- true }()
<-waitChan
va := NewValidationAuthorityImpl(false)
va.dvsniPort = port
va.DNSResolver = &mocks.MockDNS{}
log.Clear()
finChall, err := va.validateDvsni(ident, chall, AccountKey)
test.AssertEquals(t, finChall.Status, core.StatusValid)
test.AssertNotError(t, err, "")
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
invalidChall, err = va.validateDvsni(core.AcmeIdentifier{Type: core.IdentifierType("ip"), Value: "127.0.0.1"}, chall, AccountKey)
log.Clear()
invalidChall, err := va.validateDvsni(core.AcmeIdentifier{
Type: core.IdentifierType("ip"),
Value: net.JoinHostPort("127.0.0.1", fmt.Sprintf("%d", port)),
}, chall, AccountKey)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "IdentifierType IP shouldn't have worked.")
test.AssertEquals(t, invalidChall.Error.Type, core.MalformedProblem)
va.TestMode = false
log.Clear()
invalidChall, err = va.validateDvsni(core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "always.invalid"}, chall, AccountKey)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "Domain name is invalid.")
test.AssertError(t, err, "Domain name was supposed to be invalid.")
test.AssertEquals(t, invalidChall.Error.Type, core.UnknownHostProblem)
va.TestMode = true
// Need to re-sign to get an unknown SNI (from the signature value)
chall.Token = core.NewToken()
validationPayload, _ := json.Marshal(map[string]interface{}{
@ -382,6 +481,7 @@ func TestDvsni(t *testing.T) {
signer, _ := jose.NewSigner(jose.RS256, &TheKey)
chall.Validation, _ = signer.Sign(validationPayload, "")
log.Clear()
started := time.Now()
invalidChall, err = va.validateDvsni(ident, chall, AccountKey)
took := time.Since(started)
@ -391,18 +491,26 @@ func TestDvsni(t *testing.T) {
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "Connection should've timed out")
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
// Take down DVSNI validation server and check that validation fails.
hs.Close()
invalidChall, err = va.validateDvsni(ident, chall, AccountKey)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
test.AssertError(t, err, "Server's down; expected refusal. Where did we connect?")
test.AssertEquals(t, invalidChall.Error.Type, core.ConnectionProblem)
}
func TestTLSError(t *testing.T) {
va := NewValidationAuthorityImpl(true)
va := NewValidationAuthorityImpl(false)
va.DNSResolver = &mocks.MockDNS{}
chall := createChallenge(core.ChallengeTypeDVSNI)
waitChan := make(chan bool, 1)
stopChan := make(chan bool, 1)
go brokenTLSSrv(t, stopChan, waitChan)
defer func() { stopChan <- true }()
<-waitChan
hs := brokenTLSSrv()
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
va.dvsniPort = port
invalidChall, err := va.validateDvsni(ident, chall, AccountKey)
test.AssertEquals(t, invalidChall.Status, core.StatusInvalid)
@ -411,7 +519,7 @@ func TestTLSError(t *testing.T) {
}
func TestValidateHTTP(t *testing.T) {
va := NewValidationAuthorityImpl(true)
va := NewValidationAuthorityImpl(false)
va.DNSResolver = &mocks.MockDNS{}
mockRA := &MockRegistrationAuthority{}
va.RA = mockRA
@ -419,18 +527,13 @@ func TestValidateHTTP(t *testing.T) {
tls := false
challHTTP := core.SimpleHTTPChallenge()
challHTTP.TLS = &tls
challHTTP.ValidationRecord = []core.ValidationRecord{}
stopChanHTTP := make(chan bool, 1)
waitChanHTTP := make(chan bool, 1)
go simpleSrv(t, challHTTP.Token, stopChanHTTP, waitChanHTTP, tls)
// Let them start
<-waitChanHTTP
// shutdown cleanly
defer func() {
stopChanHTTP <- true
}()
hs := simpleSrv(t, challHTTP.Token, tls)
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
va.simpleHTTPPort = port
defer hs.Close()
var authz = core.Authorization{
ID: core.NewToken(),
@ -446,9 +549,10 @@ func TestValidateHTTP(t *testing.T) {
// challengeType == "dvsni" or "dns", since they're the same
func createChallenge(challengeType string) core.Challenge {
chall := core.Challenge{
Type: challengeType,
Status: core.StatusPending,
Token: core.NewToken(),
Type: challengeType,
Status: core.StatusPending,
Token: core.NewToken(),
ValidationRecord: []core.ValidationRecord{},
}
validationPayload, _ := json.Marshal(map[string]interface{}{
@ -462,23 +566,18 @@ func createChallenge(challengeType string) core.Challenge {
}
func TestValidateDvsni(t *testing.T) {
va := NewValidationAuthorityImpl(true)
va := NewValidationAuthorityImpl(false)
va.DNSResolver = &mocks.MockDNS{}
mockRA := &MockRegistrationAuthority{}
va.RA = mockRA
chall := createChallenge(core.ChallengeTypeDVSNI)
waitChanDvsni := make(chan bool, 1)
stopChanDvsni := make(chan bool, 1)
go dvsniSrv(t, chall, stopChanDvsni, waitChanDvsni)
hs := dvsniSrv(t, chall)
defer hs.Close()
// Let them start
<-waitChanDvsni
// shutdown cleanly
defer func() {
stopChanDvsni <- true
}()
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
va.dvsniPort = port
var authz = core.Authorization{
ID: core.NewToken(),
@ -492,23 +591,12 @@ func TestValidateDvsni(t *testing.T) {
}
func TestValidateDvsniNotSane(t *testing.T) {
va := NewValidationAuthorityImpl(true)
va := NewValidationAuthorityImpl(false)
va.DNSResolver = &mocks.MockDNS{}
mockRA := &MockRegistrationAuthority{}
va.RA = mockRA
chall := createChallenge(core.ChallengeTypeDVSNI)
waitChanDvsni := make(chan bool, 1)
stopChanDvsni := make(chan bool, 1)
go dvsniSrv(t, chall, stopChanDvsni, waitChanDvsni)
// Let them start
<-waitChanDvsni
// shutdown cleanly
defer func() {
stopChanDvsni <- true
}()
chall.Token = "not sane"
@ -524,7 +612,7 @@ func TestValidateDvsniNotSane(t *testing.T) {
}
func TestUpdateValidations(t *testing.T) {
va := NewValidationAuthorityImpl(true)
va := NewValidationAuthorityImpl(false)
va.DNSResolver = &mocks.MockDNS{}
mockRA := &MockRegistrationAuthority{}
va.RA = mockRA
@ -532,18 +620,7 @@ func TestUpdateValidations(t *testing.T) {
tls := false
challHTTP := core.SimpleHTTPChallenge()
challHTTP.TLS = &tls
stopChanHTTP := make(chan bool, 1)
waitChanHTTP := make(chan bool, 1)
go simpleSrv(t, challHTTP.Token, stopChanHTTP, waitChanHTTP, tls)
// Let them start
<-waitChanHTTP
// shutdown cleanly
defer func() {
stopChanHTTP <- true
}()
challHTTP.ValidationRecord = []core.ValidationRecord{}
var authz = core.Authorization{
ID: core.NewToken(),
@ -590,7 +667,7 @@ func TestCAAChecking(t *testing.T) {
// CNAME to critical
}
va := NewValidationAuthorityImpl(true)
va := NewValidationAuthorityImpl(false)
va.DNSResolver = &mocks.MockDNS{}
va.IssuerDomain = "letsencrypt.org"
for _, caaTest := range tests {
@ -622,7 +699,7 @@ func TestCAAChecking(t *testing.T) {
}
func TestDNSValidationFailure(t *testing.T) {
va := NewValidationAuthorityImpl(true)
va := NewValidationAuthorityImpl(false)
va.DNSResolver = &mocks.MockDNS{}
mockRA := &MockRegistrationAuthority{}
va.RA = mockRA
@ -658,7 +735,7 @@ func TestDNSValidationInvalid(t *testing.T) {
Challenges: []core.Challenge{chalDNS},
}
va := NewValidationAuthorityImpl(true)
va := NewValidationAuthorityImpl(false)
va.DNSResolver = &mocks.MockDNS{}
mockRA := &MockRegistrationAuthority{}
va.RA = mockRA
@ -671,7 +748,7 @@ func TestDNSValidationInvalid(t *testing.T) {
}
func TestDNSValidationNotSane(t *testing.T) {
va := NewValidationAuthorityImpl(true)
va := NewValidationAuthorityImpl(false)
va.DNSResolver = &mocks.MockDNS{}
mockRA := &MockRegistrationAuthority{}
va.RA = mockRA
@ -701,7 +778,7 @@ func TestDNSValidationNotSane(t *testing.T) {
}
func TestDNSValidationServFail(t *testing.T) {
va := NewValidationAuthorityImpl(true)
va := NewValidationAuthorityImpl(false)
va.DNSResolver = &mocks.MockDNS{}
mockRA := &MockRegistrationAuthority{}
va.RA = mockRA
@ -726,7 +803,7 @@ func TestDNSValidationServFail(t *testing.T) {
}
func TestDNSValidationNoServer(t *testing.T) {
va := NewValidationAuthorityImpl(true)
va := NewValidationAuthorityImpl(false)
va.DNSResolver = core.NewDNSResolverImpl(time.Second*5, []string{})
mockRA := &MockRegistrationAuthority{}
va.RA = mockRA

View File

@ -971,12 +971,11 @@ func (wfe *WebFrontEndImpl) Authorization(response http.ResponseWriter, request
return
}
// Blank out ID and regID
switch request.Method {
case "GET":
// Blank out ID and regID
authz.ID = ""
authz.RegistrationID = 0
jsonReply, err := json.Marshal(authz)
if err != nil {
logEvent.Error = err.Error()