diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index bb636213f..ee093fea3 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -12,58 +12,63 @@ }, { "ImportPath": "github.com/cloudflare/cfssl/auth", - "Comment": "1.1.0-268-ge32101b", - "Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136" + "Comment": "1.1.0-345-g3cc473b", + "Rev": "3cc473b970536c9c35099bc46be861cd33f8bda2" + }, + { + "ImportPath": "github.com/cloudflare/cfssl/certdb", + "Comment": "1.1.0-345-g3cc473b", + "Rev": "3cc473b970536c9c35099bc46be861cd33f8bda2" }, { "ImportPath": "github.com/cloudflare/cfssl/config", - "Comment": "1.1.0-268-ge32101b", - "Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136" + "Comment": "1.1.0-345-g3cc473b", + "Rev": "3cc473b970536c9c35099bc46be861cd33f8bda2" }, { "ImportPath": "github.com/cloudflare/cfssl/crypto/pkcs11key", - "Comment": "1.1.0-268-ge32101b", - "Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136" + "Comment": "1.1.0-345-g3cc473b", + "Rev": "3cc473b970536c9c35099bc46be861cd33f8bda2" }, { "ImportPath": "github.com/cloudflare/cfssl/crypto/pkcs7", - "Comment": "1.1.0-268-ge32101b", - "Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136" + "Comment": "1.1.0-345-g3cc473b", + "Rev": "3cc473b970536c9c35099bc46be861cd33f8bda2" }, { "ImportPath": "github.com/cloudflare/cfssl/csr", - "Comment": "1.1.0-268-ge32101b", - "Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136" + "Comment": "1.1.0-345-g3cc473b", + "Rev": "3cc473b970536c9c35099bc46be861cd33f8bda2" }, { "ImportPath": "github.com/cloudflare/cfssl/errors", - "Comment": "1.1.0-268-ge32101b", - "Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136" + "Comment": "1.1.0-345-g3cc473b", + "Rev": "3cc473b970536c9c35099bc46be861cd33f8bda2" }, { "ImportPath": "github.com/cloudflare/cfssl/helpers", - "Comment": "1.1.0-268-ge32101b", - "Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136" + "Comment": "1.1.0-345-g3cc473b", + "Rev": "3cc473b970536c9c35099bc46be861cd33f8bda2" }, { "ImportPath": "github.com/cloudflare/cfssl/info", - "Comment": "1.1.0-268-ge32101b", - "Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136" + "Comment": "1.1.0-345-g3cc473b", + "Rev": "3cc473b970536c9c35099bc46be861cd33f8bda2" }, { "ImportPath": "github.com/cloudflare/cfssl/log", - "Comment": "1.1.0-268-ge32101b", - "Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136" + "Comment": "1.1.0-345-g3cc473b", + "Rev": "3cc473b970536c9c35099bc46be861cd33f8bda2" }, { "ImportPath": "github.com/cloudflare/cfssl/ocsp", - "Comment": "1.1.0-268-ge32101b", - "Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136" + "Comment": "1.1.0-345-g3cc473b", + "Rev": "3cc473b970536c9c35099bc46be861cd33f8bda2" }, { "ImportPath": "github.com/cloudflare/cfssl/signer", - "Comment": "1.1.0-268-ge32101b", - "Rev": "e32101b1ae6cb0e1f1653c8ebec92c0113908136" + "Comment": "1.1.0-345-g3cc473b", + "Rev": "3cc473b970536c9c35099bc46be861cd33f8bda2" }, { "ImportPath": "github.com/codegangsta/cli", @@ -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", diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/LICENSE b/Godeps/_workspace/src/github.com/cloudflare/cfssl/LICENSE new file mode 100644 index 000000000..bc5841fa5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/LICENSE @@ -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. diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/README.md b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/README.md new file mode 100644 index 000000000..18c6a2803 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/README.md @@ -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"} + diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/certdb.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/certdb.go new file mode 100644 index 000000000..54fb8aa39 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/certdb.go @@ -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 +} diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/dbconf/db_config.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/dbconf/db_config.go new file mode 100644 index 000000000..f6a2bbfbf --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/dbconf/db_config.go @@ -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) +} diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/pg/dbconf.yml b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/pg/dbconf.yml new file mode 100644 index 000000000..7880fbd6e --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/pg/dbconf.yml @@ -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 diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/pg/migrations/001_CreateCertificates.sql b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/pg/migrations/001_CreateCertificates.sql new file mode 100644 index 000000000..82622c448 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/pg/migrations/001_CreateCertificates.sql @@ -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; diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/pg/migrations/002_CreateOCSPResponses.sql b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/pg/migrations/002_CreateOCSPResponses.sql new file mode 100644 index 000000000..f825bfee0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/pg/migrations/002_CreateOCSPResponses.sql @@ -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; + + diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/sql/database_accessor.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/sql/database_accessor.go new file mode 100644 index 000000000..0a5bf8129 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/sql/database_accessor.go @@ -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 +} diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/sqlite/dbconf.yml b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/sqlite/dbconf.yml new file mode 100644 index 000000000..9cc05e3bc --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/sqlite/dbconf.yml @@ -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 diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/sqlite/migrations/001_CreateCertificates.sql b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/sqlite/migrations/001_CreateCertificates.sql new file mode 100644 index 000000000..816f79552 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/sqlite/migrations/001_CreateCertificates.sql @@ -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; + diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/sqlite/migrations/002_CreateOCSPResponses.sql b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/sqlite/migrations/002_CreateOCSPResponses.sql new file mode 100644 index 000000000..05b0a2f54 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/sqlite/migrations/002_CreateOCSPResponses.sql @@ -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; + diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/testdb/certstore_development.db b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/testdb/certstore_development.db new file mode 100644 index 000000000..fa39f1f13 Binary files /dev/null and b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/testdb/certstore_development.db differ diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/testdb/testdb.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/testdb/testdb.go new file mode 100644 index 000000000..270c385ef --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/certdb/testdb/testdb.go @@ -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 +} diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/config/config.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/config/config.go index bea91ecca..087a3d24a 100644 --- a/Godeps/_workspace/src/github.com/cloudflare/cfssl/config/config.go +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/config/config.go @@ -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 diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/crypto/pkcs7/pkcs7.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/crypto/pkcs7/pkcs7.go index d70b49a10..a26be067b 100644 --- a/Godeps/_workspace/src/github.com/cloudflare/cfssl/crypto/pkcs7/pkcs7.go +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/crypto/pkcs7/pkcs7.go @@ -36,14 +36,14 @@ // this system's use of PKCS #7 data. Version is an integer type, note that PKCS #7 is // 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 andx509 certificates. Any sequence consisting -// of any number of extended certificates is not yet supported in this implementation +// between PKCS #6 extended certificates and x509 certificates. Any sequence consisting +// 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 diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/csr/csr.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/csr/csr.go index e3a102058..02fe65959 100644 --- a/Godeps/_workspace/src/github.com/cloudflare/cfssl/csr/csr.go +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/csr/csr.go @@ -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]) } diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/errors/doc.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/errors/doc.go index d09cb7c1f..1910e2662 100644 --- a/Godeps/_workspace/src/github.com/cloudflare/cfssl/errors/doc.go +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/errors/doc.go @@ -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 diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/errors/error.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/errors/error.go index 2f6805de3..88663b2c6 100644 --- a/Godeps/_workspace/src/github.com/cloudflare/cfssl/errors/error.go +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/errors/error.go @@ -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.", diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/helpers/helpers.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/helpers/helpers.go index d03586d27..7529d56bd 100644 --- a/Godeps/_workspace/src/github.com/cloudflare/cfssl/helpers/helpers.go +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/helpers/helpers.go @@ -421,6 +421,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 { diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/helpers/testsuite/testing_helpers.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/helpers/testsuite/testing_helpers.go index e9a3e2a04..2d11fe81a 100644 --- a/Godeps/_workspace/src/github.com/cloudflare/cfssl/helpers/testsuite/testing_helpers.go +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/helpers/testsuite/testing_helpers.go @@ -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, + }, +} diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/ocsp/ocsp.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/ocsp/ocsp.go index 2f3d6de2f..e90f6e223 100644 --- a/Godeps/_workspace/src/github.com/cloudflare/cfssl/ocsp/ocsp.go +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/ocsp/ocsp.go @@ -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) { @@ -138,11 +178,12 @@ func (s StandardSigner) Sign(req SignRequest) ([]byte, error) { } template := ocsp.Response{ - Status: status, - SerialNumber: req.Certificate.SerialNumber, - ThisUpdate: thisUpdate, - NextUpdate: nextUpdate, - Certificate: certificate, + Status: status, + SerialNumber: req.Certificate.SerialNumber, + ThisUpdate: thisUpdate, + NextUpdate: nextUpdate, + Certificate: certificate, + ExtraExtensions: req.Extensions, } if status == ocsp.Revoked { diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/local/local.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/local/local.go index ca248551a..914723635 100644 --- a/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/local/local.go +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/local/local.go @@ -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" - "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" ) @@ -34,10 +33,11 @@ import ( // Signer contains a signer that uses the standard library to // support both ECDSA and RSA CA keys. type Signer struct { - ca *x509.Certificate - priv crypto.Signer - policy *config.Signing - sigAlgo x509.SignatureAlgorithm + ca *x509.Certificate + priv crypto.Signer + policy *config.Signing + sigAlgo x509.SignatureAlgorithm + dbAccessor certdb.Accessor } // NewSigner creates a new Signer directly from a @@ -102,12 +102,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) @@ -228,6 +230,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,6 +250,11 @@ 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 { @@ -254,11 +264,25 @@ func (s *Signer) Sign(req signer.SignRequest) (cert []byte, err error) { } 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 +347,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 +426,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 diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/remote/remote.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/remote/remote.go index 380f43b6c..3ddb008ad 100644 --- a/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/remote/remote.go +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/remote/remote.go @@ -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 diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/signer.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/signer.go index a4ebf400c..82351242e 100644 --- a/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/signer.go +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/signer.go @@ -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" @@ -94,6 +95,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 +174,7 @@ func ParseCertificateRequest(s Signer, csrBytes []byte) (template *x509.Certific SignatureAlgorithm: s.SigAlgo(), DNSNames: csr.DNSNames, IPAddresses: csr.IPAddresses, + EmailAddresses: csr.EmailAddresses, } return diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/universal/universal.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/universal/universal.go index 609ec5908..f5f3fa108 100644 --- a/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/universal/universal.go +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/universal/universal.go @@ -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) - } - - 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 = newUniversalSigner(root, policy) + } else { + if policy.NeedsLocalSigner() { + s, err = newLocalSigner(root, policy) } - - if s == nil { - err = cferr.New(cferr.PrivateKeyError, cferr.Unknown) + if policy.NeedsRemoteSigner() { + s, err = remote.NewSigner(policy) } } - - 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 +} diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/whitelist/LICENSE b/Godeps/_workspace/src/github.com/cloudflare/cfssl/whitelist/LICENSE new file mode 100644 index 000000000..2387f3026 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/whitelist/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2014 Kyle Isom + +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. diff --git a/Godeps/_workspace/src/golang.org/x/crypto/LICENSE b/Godeps/_workspace/src/golang.org/x/crypto/LICENSE new file mode 100644 index 000000000..6a66aea5e --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/crypto/LICENSE @@ -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. diff --git a/Godeps/_workspace/src/golang.org/x/crypto/PATENTS b/Godeps/_workspace/src/golang.org/x/crypto/PATENTS new file mode 100644 index 000000000..733099041 --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/crypto/PATENTS @@ -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. diff --git a/Godeps/_workspace/src/golang.org/x/crypto/ocsp/ocsp.go b/Godeps/_workspace/src/golang.org/x/crypto/ocsp/ocsp.go index 911caa019..8737e3796 100644 --- a/Godeps/_workspace/src/golang.org/x/crypto/ocsp/ocsp.go +++ b/Godeps/_workspace/src/golang.org/x/crypto/ocsp/ocsp.go @@ -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 { @@ -85,12 +122,13 @@ type responseData struct { } type singleResponse struct { - CertID certID - Good asn1.Flag `asn1:"tag:0,optional"` - Revoked revokedInfo `asn1:"tag:1,optional"` - Unknown asn1.Flag `asn1:"tag:2,optional"` - ThisUpdate time.Time `asn1:"generalized"` - NextUpdate time.Time `asn1:"generalized,explicit,tag:0,optional"` + CertID certID + Good asn1.Flag `asn1:"tag:0,optional"` + Revoked revokedInfo `asn1:"tag:1,optional"` + 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 { @@ -534,8 +598,9 @@ func CreateResponse(issuer, responderCert *x509.Certificate, template Response, IssuerKeyHash: issuerKeyHash, SerialNumber: template.SerialNumber, }, - ThisUpdate: template.ThisUpdate.UTC(), - NextUpdate: template.NextUpdate.UTC(), + 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, diff --git a/Godeps/_workspace/src/golang.org/x/crypto/pkcs12/pkcs12.go b/Godeps/_workspace/src/golang.org/x/crypto/pkcs12/pkcs12.go index e8e179988..ad6341e60 100644 --- a/Godeps/_workspace/src/golang.org/x/crypto/pkcs12/pkcs12.go +++ b/Godeps/_workspace/src/golang.org/x/crypto/pkcs12/pkcs12.go @@ -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 { diff --git a/ca/certificate-authority_test.go b/ca/certificate-authority_test.go index bbc8fd86a..3889f1b62 100644 --- a/ca/certificate-authority_test.go +++ b/ca/certificate-authority_test.go @@ -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{ diff --git a/cmd/ocsp-responder/main.go b/cmd/ocsp-responder/main.go index 0aa9541be..e2a4ec642 100644 --- a/cmd/ocsp-responder/main.go +++ b/cmd/ocsp-responder/main.go @@ -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 } diff --git a/cmd/ocsp-responder/main_test.go b/cmd/ocsp-responder/main_test.go index ee1ae7e0e..29466ce7d 100644 --- a/cmd/ocsp-responder/main_test.go +++ b/cmd/ocsp-responder/main_test.go @@ -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 { diff --git a/mocks/mocks.go b/mocks/mocks.go index 2d446bb7e..f6349f94b 100644 --- a/mocks/mocks.go +++ b/mocks/mocks.go @@ -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 diff --git a/ra/registration-authority_test.go b/ra/registration-authority_test.go index 91719e57d..097d7f8af 100644 --- a/ra/registration-authority_test.go +++ b/ra/registration-authority_test.go @@ -198,6 +198,7 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, *sa.SQLStorageAut SignatureAlgorithm: true, DNSNames: true, }, + ClientProvidesSerialNumbers: true, }, }, }