Merge branch 'master' into delete_check_caa

This commit is contained in:
Jeff Hodges 2016-02-12 13:09:10 -08:00
commit fb657de5f1
42 changed files with 10034 additions and 8783 deletions

55
Godeps/Godeps.json generated
View File

@ -12,58 +12,63 @@
},
{
"ImportPath": "github.com/cloudflare/cfssl/auth",
"Comment": "1.1.0-268-ge32101b",
"Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136"
"Comment": "1.1.0-355-g3f3fa68",
"Rev": "3f3fa68e8d6ce6ceace60ea86461f8be41fa477b"
},
{
"ImportPath": "github.com/cloudflare/cfssl/certdb",
"Comment": "1.1.0-355-g3f3fa68",
"Rev": "3f3fa68e8d6ce6ceace60ea86461f8be41fa477b"
},
{
"ImportPath": "github.com/cloudflare/cfssl/config",
"Comment": "1.1.0-268-ge32101b",
"Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136"
"Comment": "1.1.0-355-g3f3fa68",
"Rev": "3f3fa68e8d6ce6ceace60ea86461f8be41fa477b"
},
{
"ImportPath": "github.com/cloudflare/cfssl/crypto/pkcs11key",
"Comment": "1.1.0-268-ge32101b",
"Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136"
"Comment": "1.1.0-355-g3f3fa68",
"Rev": "3f3fa68e8d6ce6ceace60ea86461f8be41fa477b"
},
{
"ImportPath": "github.com/cloudflare/cfssl/crypto/pkcs7",
"Comment": "1.1.0-268-ge32101b",
"Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136"
"Comment": "1.1.0-355-g3f3fa68",
"Rev": "3f3fa68e8d6ce6ceace60ea86461f8be41fa477b"
},
{
"ImportPath": "github.com/cloudflare/cfssl/csr",
"Comment": "1.1.0-268-ge32101b",
"Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136"
"Comment": "1.1.0-355-g3f3fa68",
"Rev": "3f3fa68e8d6ce6ceace60ea86461f8be41fa477b"
},
{
"ImportPath": "github.com/cloudflare/cfssl/errors",
"Comment": "1.1.0-268-ge32101b",
"Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136"
"Comment": "1.1.0-355-g3f3fa68",
"Rev": "3f3fa68e8d6ce6ceace60ea86461f8be41fa477b"
},
{
"ImportPath": "github.com/cloudflare/cfssl/helpers",
"Comment": "1.1.0-268-ge32101b",
"Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136"
"Comment": "1.1.0-355-g3f3fa68",
"Rev": "3f3fa68e8d6ce6ceace60ea86461f8be41fa477b"
},
{
"ImportPath": "github.com/cloudflare/cfssl/info",
"Comment": "1.1.0-268-ge32101b",
"Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136"
"Comment": "1.1.0-355-g3f3fa68",
"Rev": "3f3fa68e8d6ce6ceace60ea86461f8be41fa477b"
},
{
"ImportPath": "github.com/cloudflare/cfssl/log",
"Comment": "1.1.0-268-ge32101b",
"Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136"
"Comment": "1.1.0-355-g3f3fa68",
"Rev": "3f3fa68e8d6ce6ceace60ea86461f8be41fa477b"
},
{
"ImportPath": "github.com/cloudflare/cfssl/ocsp",
"Comment": "1.1.0-268-ge32101b",
"Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136"
"Comment": "1.1.0-355-g3f3fa68",
"Rev": "3f3fa68e8d6ce6ceace60ea86461f8be41fa477b"
},
{
"ImportPath": "github.com/cloudflare/cfssl/signer",
"Comment": "1.1.0-268-ge32101b",
"Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136"
"Comment": "1.1.0-355-g3f3fa68",
"Rev": "3f3fa68e8d6ce6ceace60ea86461f8be41fa477b"
},
{
"ImportPath": "github.com/codegangsta/cli",
@ -114,7 +119,7 @@
},
{
"ImportPath": "github.com/letsencrypt/net/publicsuffix",
"Rev": "13c140962d9691f998ad9ca4f60e82815bc4685d"
"Rev": "7eb31e4ef6d2609d3ef5597cbc65cb68c8027f62"
},
{
"ImportPath": "github.com/miekg/dns",
@ -134,11 +139,11 @@
},
{
"ImportPath": "golang.org/x/crypto/ocsp",
"Rev": "beef0f4390813b96e8e68fd78570396d0f4751fc"
"Rev": "1f22c0103821b9390939b6776727195525381532"
},
{
"ImportPath": "golang.org/x/crypto/pkcs12",
"Rev": "beef0f4390813b96e8e68fd78570396d0f4751fc"
"Rev": "1f22c0103821b9390939b6776727195525381532"
},
{
"ImportPath": "golang.org/x/net/context",

View File

@ -0,0 +1,24 @@
Copyright (c) 2014 CloudFlare Inc.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,58 @@
# certdb usage
Using a database enables additional functionality for existing commands when a
db config is provided:
- `sign` and `gencert` add a certificate to the certdb after signing it
- `serve` enables database functionality for the sign and revoke endpoints
A database is required for the following:
- `revoke` marks certificates revoked in the database with an optional reason
- `ocsprefresh` refreshes the table of cached OCSP responses
- `ocspdump` outputs cached OCSP responses in a concatenated base64-encoded format
## Setup/Migration
This directory stores [goose](https://bitbucket.org/liamstask/goose/) db migration scripts for various DB backends.
Currently supported:
- SQLite in sqlite
- PostgreSQL in pg
### Get goose
go get https://bitbucket.org/liamstask/goose/
### Use goose to start and terminate a SQLite DB
To start a SQLite DB using goose:
goose -path $GOPATH/src/github.com/cloudflare/cfssl/certdb/sqlite up'
To tear down a SQLite DB using goose
goose -path $GOPATH/src/github.com/cloudflare/cfssl/certdb/sqlite down
### Use goose to start and terminate a PostgreSQL DB
To start a PostgreSQL using goose:
goose -path $GOPATH/src/github.com/cloudflare/cfssl/certdb/pg up
To tear down a PostgreSQL DB using goose
goose -path $GOPATH/src/github.com/cloudflare/cfssl/certdb/pg down
Note: the administration of PostgreSQL DB is not included. We assume
the databases being connected to are already created and access control
are properly handled.
## CFSSL Configuration
Several cfssl commands take a -db-config flag. Create a file with a
JSON dictionary:
{"driver":"sqlite3","data_source":"certs.db"}
or
{"driver":"postgres","data_source":"postgres://user:password@host/db"}

View File

@ -0,0 +1,38 @@
package certdb
import (
"time"
)
// CertificateRecord encodes a certificate and its metadata
// that will be recorded in a database.
type CertificateRecord struct {
Serial string `sql:"serial"`
CALabel string `sql:"ca_label"`
Status string `sql:"status"`
Reason int `sql:"reason"`
Expiry time.Time `sql:"expiry"`
RevokedAt time.Time `sql:"revoked_at"`
PEM string `sql:"pem"`
}
// OCSPRecord encodes a OCSP response body and its metadata
// that will be recorded in a database.
type OCSPRecord struct {
Serial string `sql:"serial"`
Body string `sql:"body"`
Expiry time.Time `sql:"expiry"`
}
// Accessor abstracts the CRUD of certdb objects from a DB.
type Accessor interface {
InsertCertificate(cr *CertificateRecord) error
GetCertificate(serial string) (*CertificateRecord, error)
GetUnexpiredCertificates() ([]*CertificateRecord, error)
RevokeCertificate(serial string, reasonCode int) error
InsertOCSP(rr *OCSPRecord) error
GetOCSP(serial string) (*OCSPRecord, error)
GetUnexpiredOCSPs() ([]*OCSPRecord, error)
UpdateOCSP(serial, body string, expiry time.Time) error
UpsertOCSP(serial, body string, expiry time.Time) error
}

View File

@ -0,0 +1,58 @@
package dbconf
import (
"database/sql"
"encoding/json"
"errors"
"io/ioutil"
cferr "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/errors"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/log"
_ "github.com/lib/pq" // import just to initialize Postgres
_ "github.com/mattn/go-sqlite3" // import just to initialize SQLite
)
// DBConfig contains the database driver name and configuration to be passed to Open
type DBConfig struct {
DriverName string `json:"driver"`
DataSourceName string `json:"data_source"`
}
// LoadFile attempts to load the db configuration file stored at the path
// and returns the configuration. On error, it returns nil.
func LoadFile(path string) (cfg *DBConfig, err error) {
log.Debugf("loading db configuration file from %s", path)
if path == "" {
return nil, cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, errors.New("invalid path"))
}
var body []byte
body, err = ioutil.ReadFile(path)
if err != nil {
return nil, cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, errors.New("could not read configuration file"))
}
cfg = &DBConfig{}
err = json.Unmarshal(body, &cfg)
if err != nil {
return nil, cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy,
errors.New("failed to unmarshal configuration: "+err.Error()))
}
if cfg.DataSourceName == "" || cfg.DriverName == "" {
return nil, cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, errors.New("invalid db configuration"))
}
return
}
// DBFromConfig opens a sql.DB from settings in a db config file
func DBFromConfig(path string) (db *sql.DB, err error) {
var dbCfg *DBConfig
dbCfg, err = LoadFile(path)
if err != nil {
return nil, err
}
return sql.Open(dbCfg.DriverName, dbCfg.DataSourceName)
}

View File

@ -0,0 +1,15 @@
development:
driver: postgres
open: dbname=certdb_development sslmode=disable
test:
driver: postgres
open: dbname=certdb_test sslmode=disable
staging:
driver: postgres
open: dbname=certdb_staging sslmode=disable
production:
driver: postgres
open: dbname=certdb_production sslmode=disable

View File

@ -0,0 +1,18 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE certificates (
id serial,
serial bytea NOT NULL PRIMARY KEY,
ca_label bytea NOT NULL,
status bytea NOT NULL,
reason int,
expiry timestamptz,
revoked_at timestamptz,
pem bytea NOT NULL
);
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back
DROP TABLE certificates;

View File

@ -0,0 +1,16 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE ocsp_responses (
serial bytea NOT NULL PRIMARY KEY,
body bytea NOT NULL,
expiry timestamptz,
FOREIGN KEY(serial) REFERENCES certificates(serial)
);
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back
DROP TABLE ocsp_responses;

View File

@ -0,0 +1,327 @@
package sql
import (
"database/sql"
"errors"
"fmt"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb"
"time"
"github.com/kisielk/sqlstruct"
cferr "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/errors"
)
const (
insertSQL = `
INSERT INTO certificates (serial, ca_label, status, reason, expiry, revoked_at, pem)
VALUES ($1, $2, $3, $4, $5, $6, $7);`
selectSQL = `
SELECT %s FROM certificates
WHERE (serial = $1);`
selectAllSQL = `
SELECT %s FROM certificates;`
selectAllUnexpiredSQL = `
SELECT %s FROM certificates
WHERE CURRENT_TIMESTAMP < expiry;`
updateRevokeSQL = `
UPDATE certificates
SET status='revoked', revoked_at=CURRENT_TIMESTAMP, reason=$1
WHERE (serial = $2);`
insertOCSPSQL = `
INSERT INTO ocsp_responses (serial, body, expiry)
VALUES ($1, $2, $3);`
updateOCSPSQL = `
UPDATE ocsp_responses
SET expiry=$3, body=$2
WHERE (serial = $1);`
selectAllUnexpiredOCSPSQL = `
SELECT %s FROM ocsp_responses
WHERE CURRENT_TIMESTAMP < expiry;`
selectOCSPSQL = `
SELECT %s FROM ocsp_responses
WHERE (serial = $1);`
)
// Accessor implements certdb.Accessor interface.
type Accessor struct {
db *sql.DB
}
func wrapSQLError(err error) error {
if err != nil {
return cferr.Wrap(cferr.CertStoreError, cferr.Unknown, err)
}
return nil
}
func (d *Accessor) checkDB() error {
if d.db == nil {
return cferr.Wrap(cferr.CertStoreError, cferr.Unknown,
errors.New("unknown db object, please check SetDB method"))
}
return nil
}
// NewAccessor returns a new Accessor.
func NewAccessor(db *sql.DB) *Accessor {
return &Accessor{db: db}
}
// SetDB changes the underlying sql.DB object Accessor is manipulating.
func (d *Accessor) SetDB(db *sql.DB) {
d.db = db
return
}
// InsertCertificate puts a certdb.CertificateRecord into db.
func (d *Accessor) InsertCertificate(cr *certdb.CertificateRecord) error {
err := d.checkDB()
if err != nil {
return err
}
res, err := d.db.Exec(
insertSQL,
cr.Serial,
cr.CALabel,
cr.Status,
cr.Reason,
cr.Expiry.UTC(),
cr.RevokedAt,
cr.PEM,
)
if err != nil {
return wrapSQLError(err)
}
numRowsAffected, _ := res.RowsAffected()
if numRowsAffected == 0 {
return cferr.Wrap(cferr.CertStoreError, cferr.InsertionFailed, fmt.Errorf("failed to insert the certificate record"))
}
if numRowsAffected != 1 {
return wrapSQLError(fmt.Errorf("%d rows are affected, should be 1 row", numRowsAffected))
}
return nil
}
// GetCertificate gets a certdb.CertificateRecord indexed by serial.
func (d *Accessor) GetCertificate(serial string) (*certdb.CertificateRecord, error) {
err := d.checkDB()
if err != nil {
return nil, err
}
cr := new(certdb.CertificateRecord)
rows, err := d.db.Query(fmt.Sprintf(selectSQL, sqlstruct.Columns(*cr)), serial)
if err != nil {
return nil, wrapSQLError(err)
}
defer rows.Close()
if rows.Next() {
return cr, wrapSQLError(sqlstruct.Scan(cr, rows))
}
return nil, nil
}
// GetUnexpiredCertificates gets all unexpired certificate from db.
func (d *Accessor) GetUnexpiredCertificates() (crs []*certdb.CertificateRecord, err error) {
err = d.checkDB()
if err != nil {
return nil, err
}
cr := new(certdb.CertificateRecord)
rows, err := d.db.Query(fmt.Sprintf(selectAllUnexpiredSQL, sqlstruct.Columns(*cr)))
if err != nil {
return nil, wrapSQLError(err)
}
defer rows.Close()
for rows.Next() {
err = sqlstruct.Scan(cr, rows)
if err != nil {
return nil, wrapSQLError(err)
}
crs = append(crs, cr)
}
return crs, nil
}
// RevokeCertificate updates a certificate with a given serial number and marks it revoked.
func (d *Accessor) RevokeCertificate(serial string, reasonCode int) error {
err := d.checkDB()
if err != nil {
return err
}
result, err := d.db.Exec(updateRevokeSQL, reasonCode, serial)
if err != nil {
return wrapSQLError(err)
}
numRowsAffected, _ := result.RowsAffected()
if numRowsAffected == 0 {
return cferr.Wrap(cferr.CertStoreError, cferr.RecordNotFound, fmt.Errorf("failed to revoke the certificate: certificate not found"))
}
if numRowsAffected != 1 {
return wrapSQLError(fmt.Errorf("%d rows are affected, should be 1 row", numRowsAffected))
}
return nil
}
// InsertOCSP puts a new certdb.OCSPRecord into the db.
func (d *Accessor) InsertOCSP(rr *certdb.OCSPRecord) error {
err := d.checkDB()
if err != nil {
return err
}
res, err := d.db.Exec(
insertOCSPSQL,
rr.Serial,
rr.Body,
rr.Expiry.UTC(),
)
if err != nil {
return wrapSQLError(err)
}
numRowsAffected, _ := res.RowsAffected()
if numRowsAffected == 0 {
return cferr.Wrap(cferr.CertStoreError, cferr.InsertionFailed, fmt.Errorf("failed to insert the OCSP record"))
}
if numRowsAffected != 1 {
return wrapSQLError(fmt.Errorf("%d rows are affected, should be 1 row", numRowsAffected))
}
return nil
}
// GetOCSP retrieves a certdb.OCSPRecord from db by serial.
func (d *Accessor) GetOCSP(serial string) (rr *certdb.OCSPRecord, err error) {
err = d.checkDB()
if err != nil {
return nil, err
}
rr = new(certdb.OCSPRecord)
rows, err := d.db.Query(fmt.Sprintf(selectOCSPSQL, sqlstruct.Columns(*rr)), serial)
if err != nil {
return nil, wrapSQLError(err)
}
defer rows.Close()
if rows.Next() {
return rr, sqlstruct.Scan(rr, rows)
}
return nil, nil
}
// GetUnexpiredOCSPs retrieves all unexpired certdb.OCSPRecord from db.
func (d *Accessor) GetUnexpiredOCSPs() (rrs []*certdb.OCSPRecord, err error) {
err = d.checkDB()
if err != nil {
return nil, err
}
rr := new(certdb.OCSPRecord)
rows, err := d.db.Query(fmt.Sprintf(selectAllUnexpiredOCSPSQL, sqlstruct.Columns(*rr)))
if err != nil {
return nil, wrapSQLError(err)
}
defer rows.Close()
for rows.Next() {
err = sqlstruct.Scan(rr, rows)
if err != nil {
return nil, wrapSQLError(err)
}
rrs = append(rrs, rr)
}
return rrs, nil
}
// UpdateOCSP updates a ocsp response record with a given serial number.
func (d *Accessor) UpdateOCSP(serial, body string, expiry time.Time) error {
err := d.checkDB()
if err != nil {
return err
}
result, err := d.db.Exec(updateOCSPSQL, serial, body, expiry)
if err != nil {
return wrapSQLError(err)
}
numRowsAffected, err := result.RowsAffected()
if numRowsAffected == 0 {
return cferr.Wrap(cferr.CertStoreError, cferr.RecordNotFound, fmt.Errorf("failed to update the OCSP record"))
}
if numRowsAffected != 1 {
return wrapSQLError(fmt.Errorf("%d rows are affected, should be 1 row", numRowsAffected))
}
return err
}
// UpsertOCSP update a ocsp response record with a given serial number,
// or insert the record if it doesn't yet exist in the db
// Implementation note:
// We didn't implement 'upsert' with SQL statement and we lost race condition
// prevention provided by underlying DBMS.
// Reasoning:
// 1. it's diffcult to support multiple DBMS backends in the same time, the
// SQL syntax differs from one to another.
// 2. we don't need a strict simultaneous consistency between OCSP and certificate
// status. It's OK that a OCSP response still shows 'good' while the
// corresponding certificate is being revoked seconds ago, as long as the OCSP
// response catches up to be eventually consistent (within hours to days).
// Write race condition between OCSP writers on OCSP table is not a problem,
// since we don't have write race condition on Certificate table and OCSP
// writers should periodically use Certificate table to update OCSP table
// to catch up.
func (d *Accessor) UpsertOCSP(serial, body string, expiry time.Time) error {
err := d.checkDB()
if err != nil {
return err
}
result, err := d.db.Exec(updateOCSPSQL, serial, body, expiry)
if err != nil {
return wrapSQLError(err)
}
numRowsAffected, err := result.RowsAffected()
if numRowsAffected == 0 {
return d.InsertOCSP(&certdb.OCSPRecord{Serial: serial, Body: body, Expiry: expiry})
}
if numRowsAffected != 1 {
return wrapSQLError(fmt.Errorf("%d rows are affected, should be 1 row", numRowsAffected))
}
return err
}

View File

@ -0,0 +1,15 @@
development:
driver: sqlite3
open: ./certstore_development.db
test:
driver: sqlite3
open: ./certstore_test.db
staging:
driver: sqlite3
open: ./certstore_staging.db
production:
driver: sqlite3
open: ./certstore_production.db

View File

@ -0,0 +1,19 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE certificates (
id serial,
serial bytea NOT NULL PRIMARY KEY,
ca_label bytea NOT NULL,
status bytea NOT NULL,
reason int,
expiry timestamp,
revoked_at timestamp,
pem bytea NOT NULL
);
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back
DROP TABLE certificates;

View File

@ -0,0 +1,15 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE ocsp_responses (
serial bytea NOT NULL PRIMARY KEY,
body bytea NOT NULL,
expiry timestamp,
FOREIGN KEY(serial) REFERENCES certificates(serial)
);
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back
DROP TABLE ocsp_responses;

View File

@ -0,0 +1,68 @@
package testdb
import (
"database/sql"
"os"
_ "github.com/lib/pq" // register postgresql driver
_ "github.com/mattn/go-sqlite3" // register sqlite3 driver
)
const (
pgTruncateTables = `
CREATE OR REPLACE FUNCTION truncate_tables() RETURNS void AS $$
DECLARE
statements CURSOR FOR
SELECT tablename FROM pg_tables
WHERE tablename != 'goose_db_version'
AND tableowner = session_user
AND schemaname = 'public';
BEGIN
FOR stmt IN statements LOOP
EXECUTE 'TRUNCATE TABLE ' || quote_ident(stmt.tablename) || ' CASCADE;';
END LOOP;
END;
$$ LANGUAGE plpgsql;
SELECT truncate_tables();
`
sqliteTruncateTables = `
DELETE FROM certificates;
DELETE FROM ocsp_responses;
`
)
// PostgreSQLDB returns a PostgreSQL db instance for certdb testing.
func PostgreSQLDB() *sql.DB {
connStr := "dbname=certdb_development sslmode=disable"
if dbURL := os.Getenv("DATABASE_URL"); dbURL != "" {
connStr = dbURL
}
db, err := sql.Open("postgres", connStr)
if err != nil {
panic(err)
}
if _, err := db.Exec(pgTruncateTables); err != nil {
panic(err)
}
return db
}
// SQLiteDB returns a SQLite db instance for certdb testing.
func SQLiteDB(dbpath string) *sql.DB {
db, err := sql.Open("sqlite3", dbpath)
if err != nil {
panic(err)
}
if _, err := db.Exec(sqliteTruncateTables); err != nil {
panic(err)
}
return db
}

View File

@ -31,7 +31,7 @@ import (
// mechanism.
type CSRWhitelist struct {
Subject, PublicKeyAlgorithm, PublicKey, SignatureAlgorithm bool
DNSNames, IPAddresses bool
DNSNames, IPAddresses, EmailAddresses bool
}
// OID is our own version of asn1's ObjectIdentifier, so we can define a custom
@ -78,6 +78,7 @@ type SigningProfile struct {
AuthRemote AuthRemote `json:"auth_remote"`
CTLogServers []string `json:"ct_log_servers"`
AllowedExtensions []OID `json:"allowed_extensions"`
CertStore string `json:"cert_store"`
Policies []CertificatePolicy
Expiry time.Duration

View File

@ -37,13 +37,13 @@
// recursive, this second layer of ContentInfo is similar ignored for our degenerate
// usage. The ExtendedCertificatesAndCertificates type consists of a sequence of choices
// between PKCS #6 extended certificates and x509 certificates. Any sequence consisting
// of any number of extended certificates is not yet supported in this implementation
// of any number of extended certificates is not yet supported in this implementation.
//
// The ContentType Data is simpy a raw octet string and is parsed directly into a Go []byte
// The ContentType Data is simply a raw octet string and is parsed directly into a Go []byte slice.
//
// The ContentType encryptedData is the most complicated and its form can be gathered by
// the go type below. It essentially contains a raw octet string of encrypted data and an
// algorithm identifier for use in decrypting this data
// algorithm identifier for use in decrypting this data.
package pkcs7
import (
@ -55,7 +55,7 @@ import (
cferr "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/errors"
)
// Types used for asn1 Unmarshaling
// Types used for asn1 Unmarshaling.
type signedData struct {
Version int
@ -72,7 +72,7 @@ type initPKCS7 struct {
Content asn1.RawValue `asn1:"tag:0,explicit,optional"`
}
// Object identifiers strings of the three implemented PKCS7 types
// Object identifier strings of the three implemented PKCS7 types.
const (
ObjIDData = "1.2.840.113549.1.7.1"
ObjIDSignedData = "1.2.840.113549.1.7.2"
@ -84,21 +84,21 @@ const (
// the ContentInfo field, the other two being nil. SignedData
// is the degenerate SignedData Content info without signature used
// to hold certificates and crls. Data is raw bytes, and EncryptedData
// is as defined in PKCS #7 standard
// is as defined in PKCS #7 standard.
type PKCS7 struct {
Raw asn1.RawContent
ContentInfo string
Content Content
}
// Content implements three of the six possible PKCS7 data types. Only one is non-nil
// Content implements three of the six possible PKCS7 data types. Only one is non-nil.
type Content struct {
Data []byte
SignedData SignedData
EncryptedData EncryptedData
}
// SignedData defines the typical carrier of certificates and crls
// SignedData defines the typical carrier of certificates and crls.
type SignedData struct {
Raw asn1.RawContent
Version int
@ -106,19 +106,19 @@ type SignedData struct {
Crl *pkix.CertificateList
}
// Data contains raw bytes. Used as a subtype in PKCS12
// Data contains raw bytes. Used as a subtype in PKCS12.
type Data struct {
Bytes []byte
}
// EncryptedData contains encrypted data. Used as a subtype in PKCS12
// EncryptedData contains encrypted data. Used as a subtype in PKCS12.
type EncryptedData struct {
Raw asn1.RawContent
Version int
EncryptedContentInfo EncryptedContentInfo
}
// EncryptedContentInfo is a subtype of PKCS7EncryptedData
// EncryptedContentInfo is a subtype of PKCS7EncryptedData.
type EncryptedContentInfo struct {
Raw asn1.RawContent
ContentType asn1.ObjectIdentifier
@ -127,7 +127,7 @@ type EncryptedContentInfo struct {
}
// ParsePKCS7 attempts to parse the DER encoded bytes of a
// PKCS7 structure
// PKCS7 structure.
func ParsePKCS7(raw []byte) (msg *PKCS7, err error) {
var pkcs7 initPKCS7

View File

@ -12,6 +12,7 @@ import (
"encoding/pem"
"errors"
"net"
"net/mail"
"strings"
cferr "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/errors"
@ -145,7 +146,7 @@ type CertificateRequest struct {
// BasicKeyRequest.
func New() *CertificateRequest {
return &CertificateRequest{
KeyRequest: &BasicKeyRequest{},
KeyRequest: NewBasicKeyRequest(),
}
}
@ -221,6 +222,8 @@ func ParseRequest(req *CertificateRequest) (csr, key []byte, err error) {
for i := range req.Hosts {
if ip := net.ParseIP(req.Hosts[i]); ip != nil {
tpl.IPAddresses = append(tpl.IPAddresses, ip)
} else if email, err := mail.ParseAddress(req.Hosts[i]); err == nil && email != nil {
tpl.EmailAddresses = append(tpl.EmailAddresses, req.Hosts[i])
} else {
tpl.DNSNames = append(tpl.DNSNames, req.Hosts[i])
}
@ -271,6 +274,9 @@ func getHosts(cert *x509.Certificate) []string {
for _, dns := range cert.DNSNames {
hosts = append(hosts, dns)
}
for _, email := range cert.EmailAddresses {
hosts = append(hosts, email)
}
return hosts
}
@ -380,6 +386,8 @@ func Generate(priv crypto.Signer, req *CertificateRequest) (csr []byte, err erro
for i := range req.Hosts {
if ip := net.ParseIP(req.Hosts[i]); ip != nil {
tpl.IPAddresses = append(tpl.IPAddresses, ip)
} else if email, err := mail.ParseAddress(req.Hosts[i]); err == nil && email != nil {
tpl.EmailAddresses = append(tpl.EmailAddresses, email.Address)
} else {
tpl.DNSNames = append(tpl.DNSNames, req.Hosts[i])
}

View File

@ -37,6 +37,7 @@ The index of codes are listed below:
5100: NoKeyUsages
5200: InvalidPolicy
5300: InvalidRequest
5400: UnknownProfile
6XXX: DialError
2. Type HttpError is intended for CF SSL API to consume. It contains a HTTP status code that will be read and returned

View File

@ -55,6 +55,9 @@ const (
// CTError indicates a problem with the certificate transparency process
CTError // 10XXX
// CertStoreError indicates a problem with the certificate store
CertStoreError // 11XXX
)
// None is a non-specified error.
@ -143,6 +146,9 @@ const (
// InvalidRequest indicates a certificate request violated the
// constraints of the policy being applied to the request.
InvalidRequest // 53XX
// UnknownProfile indicates that the profile does not exist.
UnknownProfile // 54XX
)
// The following are API client related errors, and should be
@ -185,6 +191,15 @@ const (
PrecertSubmissionFailed = 100 * (iota + 1)
)
// Certificate persistence related errors specified with CertStoreError
const (
// InsertionFailed occurs when a SQL insert query failes to complete.
InsertionFailed = 100 * (iota + 1)
// RecordNotFound occurs when a SQL query targeting on one unique
// record failes to update the specified row in the table.
RecordNotFound
)
// The error interface implementation, which formats to a JSON object string.
func (e *Error) Error() string {
marshaled, err := json.Marshal(e)
@ -296,6 +311,8 @@ func New(category Category, reason Reason) *Error {
msg = "Invalid or unknown policy"
case InvalidRequest:
msg = "Policy violation request"
case UnknownProfile:
msg = "Unknown policy profile"
default:
panic(fmt.Sprintf("Unsupported CFSSL error reason %d under category PolicyError.",
reason))
@ -348,6 +365,13 @@ func New(category Category, reason Reason) *Error {
default:
panic(fmt.Sprintf("Unsupported CF-SSL error reason %d under category CTError.", reason))
}
case CertStoreError:
switch reason {
case Unknown:
msg = "Certificate store action failed due to unknown error"
default:
panic(fmt.Sprintf("Unsupported CF-SSL error reason %d under category CertStoreError.", reason))
}
default:
panic(fmt.Sprintf("Unsupported CFSSL error type: %d.",
@ -383,7 +407,8 @@ func Wrap(category Category, reason Reason, err error) *Error {
errorCode += unknownAuthority
}
}
case PrivateKeyError, IntermediatesError, RootError, PolicyError, DialError, APIClientError, CSRError, CTError:
case PrivateKeyError, IntermediatesError, RootError, PolicyError, DialError,
APIClientError, CSRError, CTError, CertStoreError:
// no-op, just use the error
default:
panic(fmt.Sprintf("Unsupported CFSSL error type: %d.",

View File

@ -327,7 +327,14 @@ func LoadPEMCertPool(certsFile string) (*x509.CertPool, error) {
// key. The private key may be either an unencrypted PKCS#8, PKCS#1,
// or elliptic private key.
func ParsePrivateKeyPEM(keyPEM []byte) (key crypto.Signer, err error) {
keyDER, err := GetKeyDERFromPEM(keyPEM)
return ParsePrivateKeyPEMWithPassword(keyPEM, nil)
}
// ParsePrivateKeyPEMWithPassword parses and returns a PEM-encoded private
// key. The private key may be a potentially encrypted PKCS#8, PKCS#1,
// or elliptic private key.
func ParsePrivateKeyPEMWithPassword(keyPEM []byte, password []byte) (key crypto.Signer, err error) {
keyDER, err := GetKeyDERFromPEM(keyPEM, password)
if err != nil {
return nil, err
}
@ -336,11 +343,14 @@ func ParsePrivateKeyPEM(keyPEM []byte) (key crypto.Signer, err error) {
}
// GetKeyDERFromPEM parses a PEM-encoded private key and returns DER-format key bytes.
func GetKeyDERFromPEM(in []byte) ([]byte, error) {
func GetKeyDERFromPEM(in []byte, password []byte) ([]byte, error) {
keyDER, _ := pem.Decode(in)
if keyDER != nil {
if procType, ok := keyDER.Headers["Proc-Type"]; ok {
if strings.Contains(procType, "ENCRYPTED") {
if password != nil {
return x509.DecryptPEMBlock(keyDER, password)
}
return nil, cferr.New(cferr.PrivateKeyError, cferr.Encrypted)
}
}
@ -421,6 +431,21 @@ func ParseCSR(in []byte) (csr *x509.CertificateRequest, rest []byte, err error)
return csr, rest, nil
}
// ParseCSRPEM parses a PEM-encoded certificiate signing request.
// It does not check the signature. This is useful for dumping data from a CSR
// locally.
func ParseCSRPEM(csrPEM []byte) (*x509.CertificateRequest, error) {
block, _ := pem.Decode([]byte(csrPEM))
der := block.Bytes
csrObject, err := x509.ParseCertificateRequest(der)
if err != nil {
return nil, err
}
return csrObject, nil
}
// SignerAlgo returns an X.509 signature algorithm corresponding to
// the crypto.Hash provided from a crypto.Signer.
func SignerAlgo(priv crypto.Signer, h crypto.Hash) x509.SignatureAlgorithm {

View File

@ -4,6 +4,7 @@ package testsuite
import (
"bufio"
"testing"
// "crypto/tls"
"encoding/json"
"errors"
@ -14,6 +15,7 @@ import (
"strings"
"time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/config"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/csr"
// "github.com/cloudflare/cfssl/helpers/testsuite/stoppable"
)
@ -359,3 +361,64 @@ func cleanCLIOutput(CLIOutput []byte, item string) (cleanedOutput []byte, err er
return []byte(outputString), nil
}
// NewConfig returns a config object from the data passed.
func NewConfig(t *testing.T, configBytes []byte) *config.Config {
conf, err := config.LoadConfig([]byte(configBytes))
if err != nil {
t.Fatal("config loading error:", err)
}
if !conf.Valid() {
t.Fatal("config is not valid")
}
return conf
}
// CSRTest holds information about CSR test files.
type CSRTest struct {
File string
KeyAlgo string
KeyLen int
// Error checking function
ErrorCallback func(*testing.T, error)
}
// CSRTests define a set of CSR files for testing.
var CSRTests = []CSRTest{
{
File: "../../signer/local/testdata/rsa2048.csr",
KeyAlgo: "rsa",
KeyLen: 2048,
ErrorCallback: nil,
},
{
File: "../../signer/local/testdata/rsa3072.csr",
KeyAlgo: "rsa",
KeyLen: 3072,
ErrorCallback: nil,
},
{
File: "../../signer/local/testdata/rsa4096.csr",
KeyAlgo: "rsa",
KeyLen: 4096,
ErrorCallback: nil,
},
{
File: "../../signer/local/testdata/ecdsa256.csr",
KeyAlgo: "ecdsa",
KeyLen: 256,
ErrorCallback: nil,
},
{
File: "../../signer/local/testdata/ecdsa384.csr",
KeyAlgo: "ecdsa",
KeyLen: 384,
ErrorCallback: nil,
},
{
File: "../../signer/local/testdata/ecdsa521.csr",
KeyAlgo: "ecdsa",
KeyLen: 521,
ErrorCallback: nil,
},
}

View File

@ -6,43 +6,82 @@
package log
import (
"flag"
"fmt"
golog "log"
"log"
"log/syslog"
"os"
)
// The following constants represent logging levels in increasing levels of seriousness.
const (
// LevelDebug is the log level for Debug statements.
LevelDebug = iota
// LevelInfo is the log level for Info statements.
LevelInfo
// LevelWarning is the log level for Warning statements.
LevelWarning
// LevelError is the log level for Error statements.
LevelError
// LevelCritical is the log level for Critical statements.
LevelCritical
// LevelFatal is the log level for Fatal statements.
LevelFatal
)
var levelPrefix = [...]string{
LevelDebug: "[DEBUG] ",
LevelInfo: "[INFO] ",
LevelWarning: "[WARNING] ",
LevelError: "[ERROR] ",
LevelCritical: "[CRITICAL] ",
LevelFatal: "[FATAL] ",
LevelDebug: "DEBUG",
LevelInfo: "INFO",
LevelWarning: "WARNING",
LevelError: "ERROR",
LevelCritical: "CRITICAL",
LevelFatal: "FATAL",
}
var (
// Level stores the current logging level.
var Level = LevelDebug
Level = LevelInfo
// SysLogger is a syslog Writer to be used if not nil.
SysLogger *syslog.Writer
)
func init() {
flag.IntVar(&Level, "loglevel", LevelInfo, "Log level (0 = DEBUG, 5 = FATAL)")
}
func print(l int, msg string) {
if l >= Level {
if SysLogger != nil {
var err error
switch l {
case LevelDebug:
err = SysLogger.Debug(msg)
case LevelInfo:
err = SysLogger.Info(msg)
case LevelWarning:
err = SysLogger.Warning(msg)
case LevelError:
err = SysLogger.Err(msg)
case LevelCritical:
err = SysLogger.Crit(msg)
case LevelFatal:
err = SysLogger.Emerg(msg)
}
if err != nil {
log.Printf("Unable to write syslog: %v for msg: %s\n", err, msg)
}
} else {
log.Printf("[%s] %s", levelPrefix[l], msg)
}
}
}
func outputf(l int, format string, v []interface{}) {
if l >= Level {
golog.Printf(fmt.Sprint(levelPrefix[l], format), v...)
}
print(l, fmt.Sprintf(format, v...))
}
func output(l int, v []interface{}) {
if l >= Level {
golog.Print(levelPrefix[l], fmt.Sprint(v...))
}
print(l, fmt.Sprint(v...))
}
// Fatalf logs a formatted message at the "fatal" level and then exits. The

View File

@ -11,7 +11,10 @@ import (
"bytes"
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"io/ioutil"
"strconv"
"strings"
"time"
cferr "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/errors"
@ -20,6 +23,21 @@ import (
"github.com/letsencrypt/boulder/Godeps/_workspace/src/golang.org/x/crypto/ocsp"
)
// revocationReasonCodes is a map between string reason codes
// to integers as defined in RFC 5280
var revocationReasonCodes = map[string]int{
"unspecified": ocsp.Unspecified,
"keycompromise": ocsp.KeyCompromise,
"cacompromise": ocsp.CACompromise,
"affiliationchanged": ocsp.AffiliationChanged,
"superseded": ocsp.Superseded,
"cessationofoperation": ocsp.CessationOfOperation,
"certificatehold": ocsp.CertificateHold,
"removefromcrl": ocsp.RemoveFromCRL,
"privilegewithdrawn": ocsp.PrivilegeWithdrawn,
"aacompromise": ocsp.AACompromise,
}
// StatusCode is a map between string statuses sent by cli/api
// to ocsp int statuses
var StatusCode = map[string]int{
@ -35,6 +53,7 @@ type SignRequest struct {
Status string
Reason int
RevokedAt time.Time
Extensions []pkix.Extension
}
// Signer represents a general signer of OCSP responses. It is
@ -56,6 +75,27 @@ type StandardSigner struct {
interval time.Duration
}
// ReasonStringToCode tries to convert a reason string to an integer code
func ReasonStringToCode(reason string) (reasonCode int, err error) {
// default to 0
if reason == "" {
return 0, nil
}
reasonCode, present := revocationReasonCodes[strings.ToLower(reason)]
if !present {
reasonCode, err = strconv.Atoi(reason)
if err != nil {
return
}
if reasonCode >= ocsp.AACompromise || reasonCode <= ocsp.Unspecified {
return 0, cferr.New(cferr.OCSPError, cferr.InvalidStatus)
}
}
return
}
// NewSignerFromFile reads the issuer cert, the responder cert and the responder key
// from PEM files, and takes an interval in seconds
func NewSignerFromFile(issuerFile, responderFile, keyFile string, interval time.Duration) (Signer, error) {
@ -143,6 +183,7 @@ func (s StandardSigner) Sign(req SignRequest) ([]byte, error) {
ThisUpdate: thisUpdate,
NextUpdate: nextUpdate,
Certificate: certificate,
ExtraExtensions: req.Extensions,
}
if status == ocsp.Revoked {

View File

@ -7,26 +7,25 @@ import (
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/binary"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"io"
"io/ioutil"
"math"
"math/big"
"net"
"net/mail"
"os"
"encoding/asn1"
"encoding/binary"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/config"
cferr "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/errors"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/helpers"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/info"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/log"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/google/certificate-transparency/go"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/google/certificate-transparency/go/client"
)
@ -38,6 +37,7 @@ type Signer struct {
priv crypto.Signer
policy *config.Signing
sigAlgo x509.SignatureAlgorithm
dbAccessor certdb.Accessor
}
// NewSigner creates a new Signer directly from a
@ -80,7 +80,13 @@ func NewSignerFromFile(caFile, caKeyFile string, policy *config.Signing) (*Signe
return nil, err
}
priv, err := helpers.ParsePrivateKeyPEM(cakey)
strPassword := os.Getenv("CFSSL_CA_PK_PASSWORD")
password := []byte(strPassword)
if strPassword == "" {
password = nil
}
priv, err := helpers.ParsePrivateKeyPEMWithPassword(cakey, password)
if err != nil {
log.Debug("Malformed private key %v", err)
return nil, err
@ -102,12 +108,14 @@ func (s *Signer) sign(template *x509.Certificate, profile *config.SigningProfile
return
}
template.DNSNames = nil
template.EmailAddresses = nil
s.ca = template
initRoot = true
template.MaxPathLen = signer.MaxPathLen
} else if template.IsCA {
template.MaxPathLen = 1
template.DNSNames = nil
template.EmailAddresses = nil
}
derBytes, err := x509.CreateCertificate(rand.Reader, template, s.ca, template.PublicKey, s.priv)
@ -154,7 +162,9 @@ func PopulateSubjectFromCSR(s *signer.Subject, req pkix.Name) pkix.Name {
replaceSliceIfEmpty(&name.Locality, &req.Locality)
replaceSliceIfEmpty(&name.Organization, &req.Organization)
replaceSliceIfEmpty(&name.OrganizationalUnit, &req.OrganizationalUnit)
if name.SerialNumber == "" {
name.SerialNumber = req.SerialNumber
}
return name
}
@ -228,6 +238,9 @@ func (s *Signer) Sign(req signer.SignRequest) (cert []byte, err error) {
if profile.CSRWhitelist.IPAddresses {
safeTemplate.IPAddresses = csrTemplate.IPAddresses
}
if profile.CSRWhitelist.EmailAddresses {
safeTemplate.EmailAddresses = csrTemplate.EmailAddresses
}
}
OverrideHosts(&safeTemplate, req.Hosts)
@ -245,20 +258,38 @@ func (s *Signer) Sign(req signer.SignRequest) (cert []byte, err error) {
return nil, cferr.New(cferr.PolicyError, cferr.InvalidPolicy)
}
}
for _, name := range safeTemplate.EmailAddresses {
if profile.NameWhitelist.Find([]byte(name)) == nil {
return nil, cferr.New(cferr.PolicyError, cferr.InvalidPolicy)
}
}
}
if profile.ClientProvidesSerialNumbers {
if req.Serial == nil {
fmt.Printf("xx %#v\n", profile)
return nil, cferr.New(cferr.CertificateError, cferr.MissingSerial)
}
safeTemplate.SerialNumber = req.Serial
} else {
serialNumber, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64))
// RFC 5280 4.1.2.2:
// Certificate users MUST be able to handle serialNumber
// values up to 20 octets. Conforming CAs MUST NOT use
// serialNumber values longer than 20 octets.
//
// If CFSSL is providing the serial numbers, it makes
// sense to use the max supported size.
serialNumber := make([]byte, 20)
_, err = io.ReadFull(rand.Reader, serialNumber)
if err != nil {
return nil, cferr.Wrap(cferr.CertificateError, cferr.Unknown, err)
}
safeTemplate.SerialNumber = serialNumber
// SetBytes interprets buf as the bytes of a big-endian
// unsigned integer. The leading byte should be masked
// off to ensure it isn't negative.
serialNumber[0] &= 0x7F
safeTemplate.SerialNumber = new(big.Int).SetBytes(serialNumber)
}
if len(req.Extensions) > 0 {
@ -323,7 +354,29 @@ func (s *Signer) Sign(req signer.SignRequest) (cert []byte, err error) {
var SCTListExtension = pkix.Extension{Id: signer.SCTListOID, Critical: false, Value: serializedSCTList}
certTBS.ExtraExtensions = append(certTBS.ExtraExtensions, SCTListExtension)
}
return s.sign(&certTBS, profile)
var signedCert []byte
signedCert, err = s.sign(&certTBS, profile)
if err != nil {
return nil, err
}
if s.dbAccessor != nil {
var certRecord = &certdb.CertificateRecord{
Serial: certTBS.SerialNumber.String(),
CALabel: req.Label,
Status: "good",
Expiry: certTBS.NotAfter,
PEM: string(signedCert),
}
err = s.dbAccessor.InsertCertificate(certRecord)
if err != nil {
return nil, err
}
log.Debug("saved certificate with serial number ", certTBS.SerialNumber)
}
return signedCert, nil
}
func serializeSCTList(sctList []ct.SignedCertificateTimestamp) ([]byte, error) {
@ -380,6 +433,11 @@ func (s *Signer) SetPolicy(policy *config.Signing) {
s.policy = policy
}
// SetDBAccessor sets the signers' cert db accessor
func (s *Signer) SetDBAccessor(dba certdb.Accessor) {
s.dbAccessor = dba
}
// Policy returns the signer's policy.
func (s *Signer) Policy() *config.Signing {
return s.policy

View File

@ -6,6 +6,7 @@ import (
"errors"
"github.com/cloudflare/cfssl/api/client"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/config"
cferr "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/errors"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/info"
@ -106,6 +107,11 @@ func (s *Signer) SetPolicy(policy *config.Signing) {
s.policy = policy
}
// SetDBAccessor sets the signers' cert db accessor
func (s *Signer) SetDBAccessor(dba certdb.Accessor) {
// noop
}
// Policy returns the signer's policy.
func (s *Signer) Policy() *config.Signing {
return s.policy

View File

@ -15,6 +15,7 @@ import (
"strings"
"time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/config"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/csr"
cferr "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/errors"
@ -30,6 +31,7 @@ var MaxPathLen = 2
type Subject struct {
CN string
Names []csr.Name `json:"names"`
SerialNumber string
}
// Extension represents a raw extension to be included in the certificate. The
@ -76,6 +78,7 @@ func (s *Subject) Name() pkix.Name {
appendIf(n.O, &name.Organization)
appendIf(n.OU, &name.OrganizationalUnit)
}
name.SerialNumber = s.SerialNumber
return name
}
@ -94,6 +97,7 @@ func SplitHosts(hostList string) []string {
type Signer interface {
Info(info.Req) (*info.Resp, error)
Policy() *config.Signing
SetDBAccessor(certdb.Accessor)
SetPolicy(*config.Signing)
SigAlgo() x509.SignatureAlgorithm
Sign(req SignRequest) (cert []byte, err error)
@ -172,6 +176,7 @@ func ParseCertificateRequest(s Signer, csrBytes []byte) (template *x509.Certific
SignatureAlgorithm: s.SigAlgo(),
DNSNames: csr.DNSNames,
IPAddresses: csr.IPAddresses,
EmailAddresses: csr.EmailAddresses,
}
return

View File

@ -2,14 +2,26 @@
package universal
import (
"crypto/x509"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/config"
cferr "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/errors"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/info"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/local"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/pkcs11"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/remote"
)
// Signer represents a universal signer which is both local and remote
// to fulfill the signer.Signer interface.
type Signer struct {
local signer.Signer
remote signer.Signer
policy *config.Signing
}
// Root is used to define where the universal signer gets its public
// certificate and private keys for signing.
type Root struct {
@ -67,6 +79,54 @@ var localSignerList = []localSignerCheck{
fileBackedSigner,
}
func newLocalSigner(root Root, policy *config.Signing) (s signer.Signer, err error) {
// shouldProvide indicates whether the
// function *should* have produced a key. If
// it's true, we should use the signer and
// error returned. Otherwise, keep looking for
// signers.
var shouldProvide bool
// localSignerList is defined in the
// universal_signers*.go files. These activate
// and deactivate signers based on build
// flags; for example,
// universal_signers_pkcs11.go contains a list
// of valid signers when PKCS #11 is turned
// on.
for _, possibleSigner := range localSignerList {
s, shouldProvide, err = possibleSigner(&root, policy)
if shouldProvide {
break
}
}
if s == nil {
err = cferr.New(cferr.PrivateKeyError, cferr.Unknown)
}
return s, err
}
func newUniversalSigner(root Root, policy *config.Signing) (*Signer, error) {
ls, err := newLocalSigner(root, policy)
if err != nil {
return nil, err
}
rs, err := remote.NewSigner(policy)
if err != nil {
return nil, err
}
s := &Signer{
policy: policy,
local: ls,
remote: rs,
}
return s, err
}
// NewSigner generates a new certificate signer from a Root structure.
// This is one of two standard signers: local or remote. If the root
// structure specifies a force remote, then a remote signer is created,
@ -91,40 +151,89 @@ func NewSigner(root Root, policy *config.Signing) (signer.Signer, error) {
s, err = remote.NewSigner(policy)
} else {
if policy.NeedsLocalSigner() && policy.NeedsRemoteSigner() {
// Currently we don't support a hybrid signer
return nil, cferr.New(cferr.PolicyError, cferr.InvalidPolicy)
}
s, err = newUniversalSigner(root, policy)
} else {
if policy.NeedsLocalSigner() {
// shouldProvide indicates whether the
// function *should* have produced a key. If
// it's true, we should use the signer and
// error returned. Otherwise, keep looking for
// signers.
var shouldProvide bool
// localSignerList is defined in the
// universal_signers*.go files. These activate
// and deactivate signers based on build
// flags; for example,
// universal_signers_pkcs11.go contains a list
// of valid signers when PKCS #11 is turned
// on.
for _, possibleSigner := range localSignerList {
s, shouldProvide, err = possibleSigner(&root, policy)
if shouldProvide {
break
s, err = newLocalSigner(root, policy)
}
}
if s == nil {
err = cferr.New(cferr.PrivateKeyError, cferr.Unknown)
}
}
if policy.NeedsRemoteSigner() {
s, err = remote.NewSigner(policy)
}
}
}
return s, err
}
// getMatchingProfile returns the SigningProfile that matches the profile passed.
// if an empty profile string is passed it returns the default profile.
func (s *Signer) getMatchingProfile(profile string) (*config.SigningProfile, error) {
if profile == "" {
return s.policy.Default, nil
}
for p, signingProfile := range s.policy.Profiles {
if p == profile {
return signingProfile, nil
}
}
return nil, cferr.New(cferr.PolicyError, cferr.UnknownProfile)
}
// Sign sends a signature request to either the remote or local signer,
// receiving a signed certificate or an error in response.
func (s *Signer) Sign(req signer.SignRequest) (cert []byte, err error) {
profile, err := s.getMatchingProfile(req.Profile)
if err != nil {
return cert, err
}
if profile.RemoteServer != "" {
return s.remote.Sign(req)
}
return s.local.Sign(req)
}
// Info sends an info request to the remote or local CFSSL server
// receiving an Resp struct or an error in response.
func (s *Signer) Info(req info.Req) (resp *info.Resp, err error) {
profile, err := s.getMatchingProfile(req.Profile)
if err != nil {
return resp, err
}
if profile.RemoteServer != "" {
return s.remote.Info(req)
}
return s.local.Info(req)
}
// SetDBAccessor sets the signer's cert db accessor.
func (s *Signer) SetDBAccessor(dba certdb.Accessor) {
s.local.SetDBAccessor(dba)
s.remote.SetDBAccessor(dba)
}
// SigAlgo returns the RSA signer's signature algorithm.
func (s *Signer) SigAlgo() x509.SignatureAlgorithm {
if s.local != nil {
return s.local.SigAlgo()
}
// currently remote.SigAlgo just returns
// x509.UnknownSignatureAlgorithm.
return s.remote.SigAlgo()
}
// SetPolicy sets the signer's signature policy.
func (s *Signer) SetPolicy(policy *config.Signing) {
s.policy = policy
}
// Policy returns the signer's policy.
func (s *Signer) Policy() *config.Signing {
return s.policy
}

View File

@ -0,0 +1,13 @@
Copyright (c) 2014 Kyle Isom <kyle@gokyle.org>
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

File diff suppressed because it is too large Load Diff

27
Godeps/_workspace/src/golang.org/x/crypto/LICENSE generated vendored Normal file
View File

@ -0,0 +1,27 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

22
Godeps/_workspace/src/golang.org/x/crypto/PATENTS generated vendored Normal file
View File

@ -0,0 +1,22 @@
Additional IP Rights Grant (Patents)
"This implementation" means the copyrightable works distributed by
Google as part of the Go project.
Google hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as stated in this section)
patent license to make, have made, use, offer to sell, sell, import,
transfer and otherwise run, modify and propagate the contents of this
implementation of Go, where such license applies only to those patent
claims, both currently owned or controlled by Google and acquired in
the future, licensable by Google that are necessarily infringed by this
implementation of Go. This grant does not include claims that would be
infringed only as a consequence of further modification of this
implementation. If you or your agent or exclusive licensee institute or
order or agree to the institution of patent litigation against any
entity (including a cross-claim or counterclaim in a lawsuit) alleging
that this implementation of Go or any code incorporated within this
implementation of Go constitutes direct or contributory patent
infringement, or inducement of patent infringement, then any patent
rights granted to you under this License for this implementation of Go
shall terminate as of the date such litigation is filed.

View File

@ -19,23 +19,60 @@ import (
"encoding/asn1"
"errors"
"math/big"
"strconv"
"time"
)
var idPKIXOCSPBasic = asn1.ObjectIdentifier([]int{1, 3, 6, 1, 5, 5, 7, 48, 1, 1})
// These are internal structures that reflect the ASN.1 structure of an OCSP
// response. See RFC 2560, section 4.2.
// ResponseStatus contains the result of an OCSP request. See
// https://tools.ietf.org/html/rfc6960#section-2.3
type ResponseStatus int
const (
ocspSuccess = 0
ocspMalformed = 1
ocspInternalError = 2
ocspTryLater = 3
ocspSigRequired = 4
ocspUnauthorized = 5
Success ResponseStatus = 0
Malformed ResponseStatus = 1
InternalError ResponseStatus = 2
TryLater ResponseStatus = 3
// Status code four is ununsed in OCSP. See
// https://tools.ietf.org/html/rfc6960#section-4.2.1
SignatureRequired ResponseStatus = 5
Unauthorized ResponseStatus = 6
)
func (r ResponseStatus) String() string {
switch r {
case Success:
return "success"
case Malformed:
return "malformed"
case InternalError:
return "internal error"
case TryLater:
return "try later"
case SignatureRequired:
return "signature required"
case Unauthorized:
return "unauthorized"
default:
return "unknown OCSP status: " + strconv.Itoa(int(r))
}
}
// ResponseError is an error that may be returned by ParseResponse to indicate
// that the response itself is an error, not just that its indicating that a
// certificate is revoked, unknown, etc.
type ResponseError struct {
Status ResponseStatus
}
func (r ResponseError) Error() string {
return "ocsp: error from server: " + r.Status.String()
}
// These are internal structures that reflect the ASN.1 structure of an OCSP
// response. See RFC 2560, section 4.2.
type certID struct {
HashAlgorithm pkix.AlgorithmIdentifier
NameHash []byte
@ -60,7 +97,7 @@ type request struct {
type responseASN1 struct {
Status asn1.Enumerated
Response responseBytes `asn1:"explicit,tag:0"`
Response responseBytes `asn1:"explicit,tag:0,optional"`
}
type responseBytes struct {
@ -91,6 +128,7 @@ type singleResponse struct {
Unknown asn1.Flag `asn1:"tag:2,optional"`
ThisUpdate time.Time `asn1:"generalized"`
NextUpdate time.Time `asn1:"generalized,explicit,tag:0,optional"`
SingleExtensions []pkix.Extension `asn1:"explicit,tag:1,optional"`
}
type revokedInfo struct {
@ -235,11 +273,13 @@ const (
// Good means that the certificate is valid.
Good = iota
// Revoked means that the certificate has been deliberately revoked.
Revoked = iota
Revoked
// Unknown means that the OCSP responder doesn't know about the certificate.
Unknown = iota
// ServerFailed means that the OCSP responder failed to process the request.
ServerFailed = iota
Unknown
// ServerFailed is unused and was never used (see
// https://go-review.googlesource.com/#/c/18944). ParseResponse will
// return a ResponseError when an error response is parsed.
ServerFailed
)
// The enumerated reasons for revoking a certificate. See RFC 5280.
@ -257,7 +297,7 @@ const (
AACompromise = iota
)
// Request represents an OCSP request. See RFC 2560.
// Request represents an OCSP request. See RFC 6960.
type Request struct {
HashAlgorithm crypto.Hash
IssuerNameHash []byte
@ -265,9 +305,10 @@ type Request struct {
SerialNumber *big.Int
}
// Response represents an OCSP response. See RFC 2560.
// Response represents an OCSP response containing a single SingleResponse. See
// RFC 6960.
type Response struct {
// Status is one of {Good, Revoked, Unknown, ServerFailed}
// Status is one of {Good, Revoked, Unknown}
Status int
SerialNumber *big.Int
ProducedAt, ThisUpdate, NextUpdate, RevokedAt time.Time
@ -278,6 +319,20 @@ type Response struct {
TBSResponseData []byte
Signature []byte
SignatureAlgorithm x509.SignatureAlgorithm
// Extensions contains raw X.509 extensions from the singleExtensions field
// of the OCSP response. When parsing certificates, this can be used to
// extract non-critical extensions that are not parsed by this package. When
// marshaling OCSP responses, the Extensions field is ignored, see
// ExtraExtensions.
Extensions []pkix.Extension
// ExtraExtensions contains extensions to be copied, raw, into any marshaled
// OCSP response (in the singleExtensions field). Values override any
// extensions that would otherwise be produced based on the other fields. The
// ExtraExtensions field is not populated when parsing certificates, see
// Extensions.
ExtraExtensions []pkix.Extension
}
// These are pre-serialized error responses for the various non-success codes
@ -342,8 +397,10 @@ func ParseRequest(bytes []byte) (*Request, error) {
// ParseResponse parses an OCSP response in DER form. It only supports
// responses for a single certificate. If the response contains a certificate
// then the signature over the response is checked. If issuer is not nil then
// it will be used to validate the signature or embedded certificate. Invalid
// signatures or parse failures will result in a ParseError.
// it will be used to validate the signature or embedded certificate.
//
// Invalid signatures or parse failures will result in a ParseError. Error
// responses will result in a ResponseError.
func ParseResponse(bytes []byte, issuer *x509.Certificate) (*Response, error) {
var resp responseASN1
rest, err := asn1.Unmarshal(bytes, &resp)
@ -354,10 +411,8 @@ func ParseResponse(bytes []byte, issuer *x509.Certificate) (*Response, error) {
return nil, ParseError("trailing data in OCSP response")
}
ret := new(Response)
if resp.Status != ocspSuccess {
ret.Status = ServerFailed
return ret, nil
if status := ResponseStatus(resp.Status); status != Success {
return nil, ResponseError{status}
}
if !resp.Response.ResponseType.Equal(idPKIXOCSPBasic) {
@ -378,9 +433,11 @@ func ParseResponse(bytes []byte, issuer *x509.Certificate) (*Response, error) {
return nil, ParseError("OCSP response contains bad number of responses")
}
ret.TBSResponseData = basicResp.TBSResponseData.Raw
ret.Signature = basicResp.Signature.RightAlign()
ret.SignatureAlgorithm = getSignatureAlgorithmFromOID(basicResp.SignatureAlgorithm.Algorithm)
ret := &Response{
TBSResponseData: basicResp.TBSResponseData.Raw,
Signature: basicResp.Signature.RightAlign(),
SignatureAlgorithm: getSignatureAlgorithmFromOID(basicResp.SignatureAlgorithm.Algorithm),
}
if len(basicResp.Certificates) > 0 {
ret.Certificate, err = x509.ParseCertificate(basicResp.Certificates[0].FullBytes)
@ -405,6 +462,13 @@ func ParseResponse(bytes []byte, issuer *x509.Certificate) (*Response, error) {
r := basicResp.TBSResponseData.Responses[0]
for _, ext := range r.SingleExtensions {
if ext.Critical {
return nil, ParseError("unsupported critical extension")
}
}
ret.Extensions = r.SingleExtensions
ret.SerialNumber = r.CertID.SerialNumber
switch {
@ -536,6 +600,7 @@ func CreateResponse(issuer, responderCert *x509.Certificate, template Response,
},
ThisUpdate: template.ThisUpdate.UTC(),
NextUpdate: template.NextUpdate.UTC(),
SingleExtensions: template.ExtraExtensions,
}
switch template.Status {
@ -599,7 +664,7 @@ func CreateResponse(issuer, responderCert *x509.Certificate, template Response,
}
return asn1.Marshal(responseASN1{
Status: ocspSuccess,
Status: asn1.Enumerated(Success),
Response: responseBytes{
ResponseType: idPKIXOCSPBasic,
Response: responseDER,

View File

@ -65,7 +65,7 @@ type safeBag struct {
type pkcs12Attribute struct {
Id asn1.ObjectIdentifier
Value asn1.RawValue `ans1:"set"`
Value asn1.RawValue `asn1:"set"`
}
type encryptedPrivateKeyInfo struct {

View File

@ -1,7 +1,7 @@
# This Makefile also tricks Travis into not running 'go get' for our
# build. See http://docs.travis-ci.com/user/languages/go/
OBJDIR ?= ./bin
OBJDIR ?= $(shell pwd)/bin
DESTDIR ?= /usr/local/bin
ARCHIVEDIR ?= /tmp

View File

@ -107,6 +107,29 @@ func (mock *MockDNSResolver) LookupCAA(_ context.Context, domain string) ([]*dns
secondRecord := record
secondRecord.Value = "letsencrypt.org"
results = append(results, &secondRecord)
case "unknown-critical.com":
record.Flag = 128
record.Tag = "foo"
record.Value = "bar"
results = append(results, &record)
case "unknown-critical2.com":
record.Flag = 1
record.Tag = "foo"
record.Value = "bar"
results = append(results, &record)
case "unknown-noncritical.com":
record.Flag = 0x7E // all bits we don't treat as meaning "critical"
record.Tag = "foo"
record.Value = "bar"
results = append(results, &record)
case "present-with-parameter.com":
record.Tag = "issue"
record.Value = " letsencrypt.org ;foo=bar;baz=bar"
results = append(results, &record)
case "unsatisfiable.com":
record.Tag = "issue"
record.Value = ";"
results = append(results, &record)
}
return results, nil
}

View File

@ -204,6 +204,7 @@ func setup(t *testing.T) *testCtx {
PublicKey: true,
SignatureAlgorithm: true,
},
ClientProvidesSerialNumbers: true,
},
ecdsaProfileName: &cfsslConfig.SigningProfile{
Usage: []string{"digital signature", "server auth"},
@ -224,6 +225,7 @@ func setup(t *testing.T) *testCtx {
PublicKey: true,
SignatureAlgorithm: true,
},
ClientProvidesSerialNumbers: true,
},
},
Default: &cfsslConfig.SigningProfile{

View File

@ -82,10 +82,6 @@ func (src *DBSource) Response(req *ocsp.Request) ([]byte, bool) {
src.log.Info(fmt.Sprintf("OCSP Response sent for CA=%s, Serial=%s", hex.EncodeToString(src.caKeyHash), serialString))
}
}()
// Note: we first check for an OCSP response in the certificateStatus table (
// the new method) if we don't find a response there we instead look in the
// ocspResponses table (the old method) while transitioning between the two
// tables.
err := src.dbMap.SelectOne(
&response,
"SELECT ocspResponse FROM certificateStatus WHERE serial = :serial",
@ -94,19 +90,6 @@ func (src *DBSource) Response(req *ocsp.Request) ([]byte, bool) {
if err != nil && err != sql.ErrNoRows {
src.log.Err(fmt.Sprintf("Failed to retrieve response from certificateStatus table: %s", err))
}
// TODO(#970): Delete this ocspResponses check once the table has been removed
if len(response) == 0 {
// Ignoring possible error, if response hasn't been filled, attempt to find
// response in old table
err = src.dbMap.SelectOne(
&response,
"SELECT response from ocspResponses WHERE serial = :serial ORDER BY id DESC LIMIT 1;",
map[string]interface{}{"serial": serialString},
)
if err != nil && err != sql.ErrNoRows {
src.log.Err(fmt.Sprintf("Failed to retrieve response from ocspResponses table: %s", err))
}
}
if err != nil {
return nil, false
}

View File

@ -124,7 +124,6 @@ func TestErrorLog(t *testing.T) {
test.Assert(t, !found, "Somehow found OCSP response")
test.AssertEquals(t, len(mockLog.GetAllMatching("Failed to retrieve response from certificateStatus table")), 1)
test.AssertEquals(t, len(mockLog.GetAllMatching("Failed to retrieve response from ocspResponses table")), 1)
}
func mustRead(path string) []byte {

View File

@ -15,6 +15,7 @@ import (
"time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/config"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/info"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/ocsp"
@ -311,6 +312,11 @@ func (bhs BadHSMSigner) SetPolicy(*config.Signing) {
return
}
// SetDBAccessor is a mock.
func (bhs BadHSMSigner) SetDBAccessor(certdb.Accessor) {
return
}
// SigAlgo is a mock
func (bhs BadHSMSigner) SigAlgo() x509.SignatureAlgorithm {
return x509.UnknownSignatureAlgorithm

View File

@ -194,6 +194,7 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, *sa.SQLStorageAut
SignatureAlgorithm: true,
DNSNames: true,
},
ClientProvidesSerialNumbers: true,
},
},
}

View File

@ -568,9 +568,13 @@ type CAASet struct {
func (caaSet CAASet) criticalUnknown() bool {
if len(caaSet.Unknown) > 0 {
for _, caaRecord := range caaSet.Unknown {
// Critical flag is 1, but according to RFC 6844 any flag other than
// 0 should currently be interpreted as critical.
if caaRecord.Flag > 0 {
// The critical flag is the bit with significance 128. However, many CAA
// record users have misinterpreted the RFC and concluded that the bit
// with significance 1 is the critical bit. This is sufficiently
// widespread that that bit must reasonably be considered an alias for
// the critical bit. The remaining bits are 0/ignore as proscribed by the
// RFC.
if (caaRecord.Flag & (128 | 1)) != 0 {
return true
}
}
@ -628,35 +632,69 @@ func (va *ValidationAuthorityImpl) checkCAARecords(ctx context.Context, identifi
hostname := strings.ToLower(identifier.Value)
caaSet, err := va.getCAASet(ctx, hostname)
if err != nil {
return
return false, false, err
}
if caaSet == nil {
// No CAA records found, can issue
present = false
valid = true
return
} else if caaSet.criticalUnknown() {
present = true
valid = false
return
} else if len(caaSet.Issue) > 0 || len(caaSet.Issuewild) > 0 {
present = true
var checkSet []*dns.CAA
if strings.SplitN(hostname, ".", 2)[0] == "*" {
checkSet = caaSet.Issuewild
} else {
checkSet = caaSet.Issue
va.stats.Inc("VA.CAA.None", 1, 1.0)
return false, true, nil
}
for _, caa := range checkSet {
if caa.Value == va.IssuerDomain {
valid = true
return
// Record stats on directives not currently processed.
if len(caaSet.Iodef) > 0 {
va.stats.Inc("VA.CAA.WithIodef", 1, 1.0)
}
if caaSet.criticalUnknown() {
// Contains unknown critical directives.
va.stats.Inc("VA.CAA.UnknownCritical", 1, 1.0)
return true, false, nil
}
if len(caaSet.Unknown) > 0 {
va.stats.Inc("VA.CAA.WithUnknownNoncritical", 1, 1.0)
}
if len(caaSet.Issue) == 0 {
// Although CAA records exist, none of them pertain to issuance in this case.
// (e.g. there is only an issuewild directive, but we are checking for a
// non-wildcard identifier, or there is only an iodef or non-critical unknown
// directive.)
va.stats.Inc("VA.CAA.NoneRelevant", 1, 1.0)
return true, true, nil
}
// There are CAA records pertaining to issuance in our case. Note that this
// includes the case of the unsatisfiable CAA record value ";", used to
// prevent issuance by any CA under any circumstance.
//
// Our CAA identity must be found in the chosen checkSet.
for _, caa := range caaSet.Issue {
if extractIssuerDomain(caa) == va.IssuerDomain {
va.stats.Inc("VA.CAA.Authorized", 1, 1.0)
return true, true, nil
}
}
valid = false
return
// The list of authorized issuers is non-empty, but we are not in it. Fail.
va.stats.Inc("VA.CAA.Unauthorized", 1, 1.0)
return true, false, nil
}
return
// Given a CAA record, assume that the Value is in the issue/issuewild format,
// that is, a domain name with zero or more additional key-value parameters.
// Returns the domain name, which may be "" (unsatisfiable).
func extractIssuerDomain(caa *dns.CAA) string {
v := caa.Value
v = strings.Trim(v, " \t") // Value can start and end with whitespace.
idx := strings.IndexByte(v, ';')
if idx < 0 {
return v // no parameters; domain only
}
// Currently, ignore parameters. Unfortunately, the RFC makes no statement on
// whether any parameters are critical. Treat unknown parameters as
// non-critical.
return strings.Trim(v[0:idx], " \t")
}

View File

@ -681,6 +681,15 @@ func TestCAAChecking(t *testing.T) {
CAATest{"present.com", true, true},
// Good (multiple critical, one matching)
CAATest{"multi-crit-present.com", true, true},
// Bad (unknown critical)
CAATest{"unknown-critical.com", true, false},
CAATest{"unknown-critical2.com", true, false},
// Good (unknown noncritical, no issue/issuewild records)
CAATest{"unknown-noncritical.com", true, true},
// Good (issue record with unknown parameters)
CAATest{"present-with-parameter.com", true, true},
// Bad (unsatisfiable issue record)
CAATest{"unsatisfiable.com", true, false},
}
stats, _ := statsd.NewNoopClient()