Merge pull request #134 from letsencrypt/ocsp-table

More steps towards revocation / OCSP support
This commit is contained in:
jsha 2015-05-02 11:22:23 -07:00
commit c4497aca72
12 changed files with 413 additions and 131 deletions

View File

@ -6,10 +6,12 @@
package ca
import (
"crypto"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"time"
"github.com/letsencrypt/boulder/core"
@ -17,21 +19,41 @@ import (
"github.com/letsencrypt/boulder/policy"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/auth"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/config"
cfsslConfig "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/config"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/helpers"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/ocsp"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/remote"
)
type Config struct {
Server string
AuthKey string
Profile string
TestMode bool
DBDriver string
DBName string
SerialPrefix int
// A PEM-encoded copy of the issuer certificate.
IssuerCert string
// This field is only allowed if TestMode is true, indicating that we are
// signing with a local key. In production we will use an HSM and this
// IssuerKey must be empty (and TestMode must be false). PEM-encoded private
// key used for signing certificates and OCSP responses.
IssuerKey string
}
// CertificateAuthorityImpl represents a CA that signs certificates, CRLs, and
// OCSP responses.
type CertificateAuthorityImpl struct {
profile string
Signer signer.Signer
SA core.StorageAuthority
PA core.PolicyAuthority
DB core.CertificateAuthorityDatabase
log *blog.AuditLogger
Prefix int // Prepended to the serial number
profile string
Signer signer.Signer
OCSPSigner ocsp.Signer
SA core.StorageAuthority
PA core.PolicyAuthority
DB core.CertificateAuthorityDatabase
log *blog.AuditLogger
Prefix int // Prepended to the serial number
}
// NewCertificateAuthorityImpl creates a CA that talks to a remote CFSSL
@ -40,41 +62,122 @@ type CertificateAuthorityImpl struct {
// using CFSSL's authenticated signature scheme. A CA created in this way
// issues for a single profile on the remote signer, which is indicated
// by name in this constructor.
func NewCertificateAuthorityImpl(hostport string, authKey string, profile string, serialPrefix int, cadb core.CertificateAuthorityDatabase) (ca *CertificateAuthorityImpl, err error) {
func NewCertificateAuthorityImpl(cadb core.CertificateAuthorityDatabase, config Config) (ca *CertificateAuthorityImpl, err error) {
logger := blog.GetAuditLogger()
logger.Notice("Certificate Authority Starting")
if config.SerialPrefix == 0 {
err = errors.New("Must have non-zero serial prefix for CA.")
return
}
// Create the remote signer
localProfile := config.SigningProfile{
Expiry: time.Hour, // BOGUS: Required by CFSSL, but not used
RemoteName: hostport, // BOGUS: Only used as a flag by CFSSL
RemoteServer: hostport,
localProfile := cfsslConfig.SigningProfile{
Expiry: time.Hour, // BOGUS: Required by CFSSL, but not used
RemoteName: config.Server, // BOGUS: Only used as a flag by CFSSL
RemoteServer: config.Server,
UseSerialSeq: true,
}
localProfile.Provider, err = auth.New(authKey, nil)
localProfile.Provider, err = auth.New(config.AuthKey, nil)
if err != nil {
return
return nil, err
}
signer, err := remote.NewSigner(&config.Signing{Default: &localProfile})
signer, err := remote.NewSigner(&cfsslConfig.Signing{Default: &localProfile})
if err != nil {
return
return nil, err
}
issuer, err := loadIssuer(config.IssuerCert)
if err != nil {
return nil, err
}
// In test mode, load a private key from a file. In production, use an HSM.
if !config.TestMode {
err = errors.New("OCSP signing with a PKCS#11 key not yet implemented.")
return nil, err
}
issuerKey, err := loadIssuerKey(config.IssuerKey)
if err != nil {
return nil, err
}
// Set up our OCSP signer. Note this calls for both the issuer cert and the
// OCSP signing cert, which are the same in our case.
ocspSigner, err := ocsp.NewSigner(issuer, issuer, issuerKey,
time.Hour*24*4)
pa := policy.NewPolicyAuthorityImpl()
ca = &CertificateAuthorityImpl{
Signer: signer,
profile: profile,
PA: pa,
DB: cadb,
Prefix: serialPrefix,
log: logger,
Signer: signer,
OCSPSigner: ocspSigner,
profile: config.Profile,
PA: pa,
DB: cadb,
Prefix: config.SerialPrefix,
log: logger,
}
return ca, err
}
func loadIssuer(filename string) (issuerCert *x509.Certificate, err error) {
if filename == "" {
err = errors.New("Issuer certificate was not provided in config.")
return
}
issuerCertPEM, err := ioutil.ReadFile(filename)
if err != nil {
return
}
issuerCert, err = helpers.ParseCertificatePEM(issuerCertPEM)
return
}
func loadIssuerKey(filename string) (issuerKey crypto.Signer, err error) {
if filename == "" {
err = errors.New("IssuerKey must be provided in test mode.")
return
}
pem, err := ioutil.ReadFile(filename)
if err != nil {
return
}
issuerKey, err = helpers.ParsePrivateKeyPEM(pem)
return
}
func (ca *CertificateAuthorityImpl) RevokeCertificate(serial string) (err error) {
certDER, err := ca.SA.GetCertificate(serial)
if err != nil {
return err
}
cert, err := x509.ParseCertificate(certDER)
if err != nil {
return err
}
// Per https://tools.ietf.org/html/rfc5280, CRLReason 0 is "unspecified."
// TODO: Add support for specifying reason.
reason := 0
signRequest := ocsp.SignRequest{
Certificate: cert,
Status: string(core.OCSPStatusRevoked),
Reason: reason,
RevokedAt: time.Now(),
}
ocspResponse, err := ca.OCSPSigner.Sign(signRequest)
if err != nil {
return err
}
err = ca.SA.MarkCertificateRevoked(serial, ocspResponse, reason)
return err
}
// IssueCertificate attempts to convert a CSR into a signed Certificate, while
// enforcing all policies.
func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest) (cert core.Certificate, err error) {

View File

@ -6,18 +6,22 @@
package ca
import (
"bytes"
"crypto/x509"
"crypto"
"encoding/asn1"
"encoding/hex"
"encoding/pem"
"fmt"
"io/ioutil"
"net/http"
"testing"
"time"
"os"
apisign "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/api/sign"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/auth"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/config"
cfsslConfig "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/config"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/helpers"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/local"
_ "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/mattn/go-sqlite3"
"github.com/letsencrypt/boulder/core"
@ -219,6 +223,63 @@ var NO_NAME_CSR_HEX = "308202523082013a020100300d310b300906035504061302555330820
"58c004d9e1e55af59ea517dfbd2bccca58216d8130b9f77c90328b2aa54b" +
"1778a629b584f2bc059489a236131de9b444adca90218c31a499a485"
// CFSSL config
const hostPort = "localhost:9000"
const authKey = "79999d86250c367a2b517a1ae7d409c1"
const profileName = "ee"
const caKeyFile = "../test/test-ca.key"
const caCertFile = "../test/test-ca.pem"
var cfsslSigner *local.Signer
var caKey crypto.PrivateKey
var caCert x509.Certificate
func TestMain(m *testing.M) {
caKeyPEM, _ := ioutil.ReadFile(caKeyFile)
caKey, _ := helpers.ParsePrivateKeyPEM(caKeyPEM)
caCertPEM, _ := ioutil.ReadFile(caCertFile)
caCert, _ := helpers.ParseCertificatePEM(caCertPEM)
// Create an online CFSSL instance
// This is designed to mimic what LE plans to do
authHandler, _ := auth.New(authKey, nil)
policy := &cfsslConfig.Signing{
Profiles: map[string]*cfsslConfig.SigningProfile{
profileName: &cfsslConfig.SigningProfile{
Usage: []string{"server auth"},
CA: false,
IssuerURL: []string{"http://not-example.com/issuer-url"},
OCSP: "http://not-example.com/ocsp",
CRL: "http://not-example.com/crl",
Policies: []asn1.ObjectIdentifier{
asn1.ObjectIdentifier{2, 23, 140, 1, 2, 1},
},
Expiry: 8760 * time.Hour,
Backdate: time.Hour,
Provider: authHandler,
CSRWhitelist: &cfsslConfig.CSRWhitelist{
PublicKeyAlgorithm: true,
PublicKey: true,
SignatureAlgorithm: true,
},
},
},
Default: &cfsslConfig.SigningProfile{
Expiry: time.Hour,
},
}
cfsslSigner, _ = local.NewSigner(caKey, caCert, x509.SHA256WithRSA, policy)
signHandler, _ := apisign.NewAuthHandlerFromSigner(cfsslSigner)
http.Handle("/api/v1/cfssl/authsign", signHandler)
// This goroutine should get killed when main() return
go (func() { http.ListenAndServe(hostPort, nil) })()
os.Exit(m.Run())
}
type MockCADatabase struct {
// empty
}
@ -243,71 +304,75 @@ func (cadb *MockCADatabase) IncrementAndGetSerial() (int, error) {
return 1, nil
}
func TestIssueCertificate(t *testing.T) {
// Decode pre-generated values
caKeyPEM, _ := pem.Decode([]byte(CA_KEY_PEM))
caKey, _ := x509.ParsePKCS1PrivateKey(caKeyPEM.Bytes)
caCertPEM, _ := pem.Decode([]byte(CA_CERT_PEM))
caCert, _ := x509.ParseCertificate(caCertPEM.Bytes)
// Uncomment to create a CFSSL local signer
// CFSSL config
hostPort := "localhost:9000"
authKey := "79999d86250c367a2b517a1ae7d409c1"
profileName := "ee"
func setup(t *testing.T) (cadb core.CertificateAuthorityDatabase, storageAuthority core.StorageAuthority, caConfig Config) {
// Create an SA
sa, err := sa.NewSQLStorageAuthority("sqlite3", ":memory:")
ssa, err := sa.NewSQLStorageAuthority("sqlite3", ":memory:")
test.AssertNotError(t, err, "Failed to create SA")
sa.InitTables()
ssa.InitTables()
storageAuthority = ssa
// Create an online CFSSL instance
// This is designed to mimic what LE plans to do
authHandler, err := auth.New(authKey, nil)
test.AssertNotError(t, err, "Failed to create authentication handler")
policy := &config.Signing{
Profiles: map[string]*config.SigningProfile{
profileName: &config.SigningProfile{
Usage: []string{"server auth"},
CA: false,
IssuerURL: []string{"http://not-example.com/issuer-url"},
OCSP: "http://not-example.com/ocsp",
CRL: "http://not-example.com/crl",
Policies: []asn1.ObjectIdentifier{
asn1.ObjectIdentifier{2, 23, 140, 1, 2, 1},
},
Expiry: 8760 * time.Hour,
Backdate: time.Hour,
Provider: authHandler,
CSRWhitelist: &config.CSRWhitelist{
PublicKeyAlgorithm: true,
PublicKey: true,
SignatureAlgorithm: true,
},
},
},
Default: &config.SigningProfile{
Expiry: time.Hour,
},
}
signer, err := local.NewSigner(caKey, caCert, x509.SHA256WithRSA, policy)
test.AssertNotError(t, err, "Failed to create signer")
signHandler, err := apisign.NewAuthHandlerFromSigner(signer)
test.AssertNotError(t, err, "Failed to create signing API endpoint")
http.Handle("/api/v1/cfssl/authsign", signHandler)
// This goroutine should get killed when main() return
go (func() { http.ListenAndServe(hostPort, nil) })()
cadb, err := NewMockCertificateAuthorityDatabase()
cadb, _ = NewMockCertificateAuthorityDatabase()
// Create a CA
// Uncomment to test with a remote signer
ca, err := NewCertificateAuthorityImpl(hostPort, authKey, profileName, 17, cadb)
caConfig = Config{
Server: hostPort,
AuthKey: authKey,
Profile: profileName,
SerialPrefix: 17,
IssuerCert: "../test/test-ca.pem",
IssuerKey: "../test/test-ca.key",
TestMode: true,
}
return cadb, storageAuthority, caConfig
}
func TestFailNoSerial(t *testing.T) {
cadb, _, caConfig := setup(t)
caConfig.SerialPrefix = 0
_, err := NewCertificateAuthorityImpl(cadb, caConfig)
test.AssertError(t, err, "CA should have failed with no SerialPrefix")
}
func TestFailNoTestMode(t *testing.T) {
cadb, _, caConfig := setup(t)
caConfig.TestMode = false
_, err := NewCertificateAuthorityImpl(cadb, caConfig)
test.AssertError(t, err, "CA should have failed with TestMode = false, but key provided")
}
func TestRevoke(t *testing.T) {
cadb, storageAuthority, caConfig := setup(t)
ca, err := NewCertificateAuthorityImpl(cadb, caConfig)
ca.SA = storageAuthority
csrDER, _ := hex.DecodeString(CN_AND_SAN_CSR_HEX)
csr, _ := x509.ParseCertificateRequest(csrDER)
certObj, err := ca.IssueCertificate(*csr)
test.AssertNotError(t, err, "Failed to sign certificate")
if err != nil {
return
}
cert, err := x509.ParseCertificate(certObj.DER)
test.AssertNotError(t, err, "Certificate failed to parse")
serialString := fmt.Sprintf("%032x", cert.SerialNumber)
err = ca.RevokeCertificate(serialString)
test.AssertNotError(t, err, "Revocation failed")
status, err := storageAuthority.GetCertificateStatus(serialString)
test.AssertNotError(t, err, "Failed to get cert status")
test.AssertEquals(t, status.Status, core.OCSPStatusRevoked)
test.Assert(t, time.Now().Sub(status.OCSPLastUpdated) > time.Second,
fmt.Sprintf("OCSP LastUpdated was wrong: %v", status.OCSPLastUpdated))
}
func TestIssueCertificate(t *testing.T) {
cadb, storageAuthority, caConfig := setup(t)
ca, err := NewCertificateAuthorityImpl(cadb, caConfig)
test.AssertNotError(t, err, "Failed to create CA")
ca.SA = sa
ca.SA = storageAuthority
/*
// Uncomment to test with a local signer
@ -326,6 +391,9 @@ func TestIssueCertificate(t *testing.T) {
// Sign CSR
certObj, err := ca.IssueCertificate(*csr)
test.AssertNotError(t, err, "Failed to sign certificate")
if err != nil {
continue
}
// Verify cert contents
cert, err := x509.ParseCertificate(certObj.DER)
@ -347,10 +415,17 @@ func TestIssueCertificate(t *testing.T) {
}
// Verify that the cert got stored in the DB
shortSerial := fmt.Sprintf("%032x", cert.SerialNumber)[0:16]
_, err = sa.GetCertificate(shortSerial)
serialString := fmt.Sprintf("%032x", cert.SerialNumber)
certBytes, err := storageAuthority.GetCertificate(serialString)
test.AssertNotError(t, err,
fmt.Sprintf("Certificate %032x not found in database", cert.SerialNumber))
fmt.Sprintf("Certificate %s not found in database", serialString))
test.Assert(t, bytes.Equal(certBytes, certObj.DER), "Retrieved cert not equal to issued cert.")
certStatus, err := storageAuthority.GetCertificateStatus(serialString)
test.AssertNotError(t, err,
fmt.Sprintf("Error fetching status for certificate %s", serialString))
test.Assert(t, certStatus.Status == core.OCSPStatusGood, "Certificate status was not good")
test.Assert(t, certStatus.SubscriberApproved == false, "Subscriber shouldn't have approved cert yet.")
}
// Test that the CA rejects CSRs with no names

View File

@ -33,7 +33,7 @@ func main() {
cadb, err := ca.NewCertificateAuthorityDatabaseImpl(c.CA.DBDriver, c.CA.DBName)
cmd.FailOnError(err, "Failed to create CA database")
cai, err := ca.NewCertificateAuthorityImpl(c.CA.Server, c.CA.AuthKey, c.CA.Profile, c.CA.SerialPrefix, cadb)
cai, err := ca.NewCertificateAuthorityImpl(cadb, c.CA)
cmd.FailOnError(err, "Failed to create CA impl")
go cmd.ProfileCmd("CA", stats)

View File

@ -81,7 +81,7 @@ func main() {
cadb, err := ca.NewCertificateAuthorityDatabaseImpl(c.CA.DBDriver, c.CA.DBName)
cmd.FailOnError(err, "Failed to create CA database")
ca, err := ca.NewCertificateAuthorityImpl(c.CA.Server, c.CA.AuthKey, c.CA.Profile, c.CA.SerialPrefix, cadb)
ca, err := ca.NewCertificateAuthorityImpl(cadb, c.CA)
cmd.FailOnError(err, "Unable to create CA")
// Wire them up

View File

@ -37,6 +37,7 @@ import (
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/streadway/amqp"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/rpc"
"github.com/letsencrypt/boulder/ca"
)
// Config stores configuration parameters that applications
@ -59,15 +60,7 @@ type Config struct {
ListenAddress string
}
CA struct {
Server string
AuthKey string
Profile string
TestMode bool
DBDriver string
DBName string
SerialPrefix int
}
CA ca.Config
SA struct {
DBDriver string

View File

@ -79,6 +79,7 @@ type ValidationAuthority interface {
type CertificateAuthority interface {
// [RegistrationAuthority]
IssueCertificate(x509.CertificateRequest) (Certificate, error)
RevokeCertificate(serial string) error
}
type PolicyAuthority interface {
@ -90,6 +91,8 @@ type StorageGetter interface {
GetRegistration(string) (Registration, error)
GetAuthorization(string) (Authorization, error)
GetCertificate(string) ([]byte, error)
GetCertificateByShortSerial(string) ([]byte, error)
GetCertificateStatus(string) (CertificateStatus, error)
}
type StorageAdder interface {
@ -99,6 +102,7 @@ type StorageAdder interface {
NewPendingAuthorization() (string, error)
UpdatePendingAuthorization(Authorization) error
FinalizeAuthorization(Authorization) error
MarkCertificateRevoked(serial string, ocspResponse []byte, reasonCode int) error
AddCertificate([]byte) (string, error)
}

View File

@ -238,10 +238,9 @@ func TestNewCertificate(t *testing.T) {
test.AssertNotError(t, err, "Failed to issue certificate")
parsedCert, err := x509.ParseCertificate(cert.DER)
test.AssertNotError(t, err, "Failed to parse certificate")
shortSerial := fmt.Sprintf("%032x", parsedCert.SerialNumber)[0:16]
// Verify that cert shows up and is as expected
dbCert, err := sa.GetCertificate(shortSerial)
dbCert, err := sa.GetCertificate(fmt.Sprintf("%032x", parsedCert.SerialNumber))
test.AssertNotError(t, err, fmt.Sprintf("Could not fetch certificate %032x from database",
parsedCert.SerialNumber))
test.Assert(t, bytes.Compare(cert.DER, dbCert) == 0, "Certificates differ")

View File

@ -44,6 +44,7 @@ const (
MethodGetRegistration = "GetRegistration" // SA
MethodGetAuthorization = "GetAuthorization" // SA
MethodGetCertificate = "GetCertificate" // SA
MethodGetCertificateByShortSerial = "GetCertificateByShortSerial" // SA
MethodNewPendingAuthorization = "NewPendingAuthorization" // SA
MethodUpdatePendingAuthorization = "UpdatePendingAuthorization" // SA
MethodFinalizeAuthorization = "FinalizeAuthorization" // SA
@ -419,7 +420,7 @@ func NewStorageAuthorityServer(serverQueue string, channel *amqp.Channel, impl c
rpc = NewAmqpRPCServer(serverQueue, channel)
rpc.Handle(MethodGetRegistration, func(req []byte) (response []byte) {
reg, err := impl.GetCertificate(string(req))
reg, err := impl.GetRegistration(string(req))
if err != nil {
return
}
@ -433,7 +434,7 @@ func NewStorageAuthorityServer(serverQueue string, channel *amqp.Channel, impl c
})
rpc.Handle(MethodGetAuthorization, func(req []byte) []byte {
authz, err := impl.AddCertificate(req)
authz, err := impl.GetAuthorization(string(req))
if err != nil {
return nil
}
@ -501,6 +502,14 @@ func NewStorageAuthorityServer(serverQueue string, channel *amqp.Channel, impl c
return
})
rpc.Handle(MethodGetCertificateByShortSerial, func(req []byte) (response []byte) {
cert, err := impl.GetCertificateByShortSerial(string(req))
if err == nil {
response = []byte(cert)
}
return
})
return
}
@ -550,7 +559,7 @@ func (cac StorageAuthorityClient) UpdateRegistration(reg core.Registration) (err
}
// XXX: Is this catching all the errors?
_, err = cac.rpc.DispatchSync(MethodUpdatePendingAuthorization, jsonReg)
_, err = cac.rpc.DispatchSync(MethodUpdateRegistration, jsonReg)
return
}

View File

@ -68,10 +68,11 @@ func (ssa *SQLStorageAuthority) InitTables() (err error) {
statements := []string{
// Create registrations table
// TODO: Add NOT NULL to thumbprint and value.
`CREATE TABLE IF NOT EXISTS registrations (
id VARCHAR(255) NOT NULL,
thumbprint VARCHAR(255) NOT NULL,
value BLOB NOT NULL
thumbprint VARCHAR(255),
value BLOB
);`,
// Create pending authorizations table
@ -93,7 +94,7 @@ func (ssa *SQLStorageAuthority) InitTables() (err error) {
// Create certificates table. This should be effectively append-only, enforced
// by DB permissions.
`CREATE TABLE IF NOT EXISTS certificates (
serial VARCHAR(255) NOT NULL,
serial VARCHAR(255) PRIMARY KEY NOT NULL,
digest VARCHAR(255) NOT NULL,
value BLOB NOT NULL,
issued DATETIME NOT NULL
@ -109,16 +110,49 @@ func (ssa *SQLStorageAuthority) InitTables() (err error) {
// with status 'good' but don't necessarily get fresh OCSP responses.
// revokedDate: If status is 'revoked', this is the date and time it was
// revoked. Otherwise it has the zero value of time.Time, i.e. Jan 1 1970.
// revokedReason: If status is 'revoked', this is the reason code for the
// revocation. Otherwise it is zero (which happens to be the reason
// code for 'unspecified').
// ocspLastUpdated: The date and time of the last time we generated an OCSP
// response. If we have never generated one, this has the zero value of
// time.Time, i.e. Jan 1 1970.
`CREATE TABLE IF NOT EXISTS certificateStatus (
serial VARCHAR(255) NOT NULL,
serial VARCHAR(255) PRIMARY KEY NOT NULL,
subscriberApproved INTEGER NOT NULL,
status VARCHAR(255) NOT NULL,
revokedDate DATETIME NOT NULL,
revokedReason INT NOT NULL,
ocspLastUpdated DATETIME NOT NULL
);`,
// A large table of OCSP responses. This contains all historical OCSP
// responses we've signed, is append-only, and is likely to get quite
// large. We'll probably want administratively truncate it at some point.
// serial: Same as certificate serial.
// createdAt: The date the response was signed.
// response: The encoded and signed CRL.
`CREATE TABLE IF NOT EXISTS ocspResponses (
id INT AUTO_INCREMENT PRIMARY KEY,
serial VARCHAR(255) NOT NULL,
createdAt DATETIME NOT NULL,
response BLOB
);`,
// This index allows us to quickly serve the most recent OCSP response.
`CREATE INDEX IF NOT EXISTS serial_createdAt on ocspResponses (serial, createdAt)`,
// A large table of signed CRLs. This contains all historical CRLs
// we've signed, is append-only, and is likely to get quite large.
// serial: Same as certificate serial.
// createdAt: The date the CRL was signed.
// crl: The encoded and signed CRL.
`CREATE TABLE IF NOT EXISTS crls (
serial VARCHAR(255) PRIMARY KEY NOT NULL,
createdAt DATETIME NOT NULL,
crl BLOB
);`,
`CREATE INDEX IF NOT EXISTS serial_createdAt on crls (serial, createdAt)`,
}
for _, statement := range statements {
@ -211,30 +245,42 @@ func (ssa *SQLStorageAuthority) GetAuthorization(id string) (authz core.Authoriz
return
}
// GetCertificate takes an id consisting of the first, sequential half of a
// GetCertificateByShortSerial takes an id consisting of the first, sequential half of a
// serial number and returns the first certificate whose full serial number is
// lexically greater than that id. This allows clients to query on the known
// sequential half of our serial numbers to enumerate all certificates.
// TODO: Add index on certificates table
// TODO: Implement error when there are multiple certificates with the same
// sequential half.
func (ssa *SQLStorageAuthority) GetCertificate(id string) (cert []byte, err error) {
if len(id) != 16 {
err = errors.New("Invalid certificate serial " + id)
func (ssa *SQLStorageAuthority) GetCertificateByShortSerial(shortSerial string) (cert []byte, err error) {
if len(shortSerial) != 16 {
err = errors.New("Invalid certificate short serial " + shortSerial)
return
}
err = ssa.db.QueryRow(
"SELECT value FROM certificates WHERE serial LIKE ? LIMIT 1;",
id+"%").Scan(&cert)
shortSerial+"%").Scan(&cert)
return
}
// GetCertificate takes a serial number and returns the corresponding
// certificate, or error if it does not exist.
func (ssa *SQLStorageAuthority) GetCertificate(serial string) (cert []byte, err error) {
if len(serial) != 32 {
err = errors.New("Invalid certificate serial " + serial)
return
}
err = ssa.db.QueryRow(
"SELECT value FROM certificates WHERE serial = ? LIMIT 1;",
serial).Scan(&cert)
return
}
// GetCertificateStatus takes a hexadecimal string representing the full 128-bit serial
// number of a certificate and returns data about that certificate's current
// validity.
func (ssa *SQLStorageAuthority) GetCertificateStatus(id string) (status core.CertificateStatus, err error) {
if len(id) != 32 {
err = errors.New("Invalid certificate serial " + id)
func (ssa *SQLStorageAuthority) GetCertificateStatus(serial string) (status core.CertificateStatus, err error) {
if len(serial) != 32 {
err = errors.New("Invalid certificate serial " + serial)
return
}
var statusString string
@ -242,7 +288,7 @@ func (ssa *SQLStorageAuthority) GetCertificateStatus(id string) (status core.Cer
`SELECT subscriberApproved, status, ocspLastUpdated
FROM certificateStatus
WHERE serial = ?
LIMIT 1;`, id).Scan(&status.SubscriberApproved, &statusString, &status.OCSPLastUpdated)
LIMIT 1;`, serial).Scan(&status.SubscriberApproved, &statusString, &status.OCSPLastUpdated)
if err != nil {
return
}
@ -277,6 +323,44 @@ func (ssa *SQLStorageAuthority) NewRegistration() (id string, err error) {
return
}
// MarkCertificateRevoked stores the fact that a certificate is revoked, along
// with a timestamp and a reason.
func (ssa *SQLStorageAuthority) MarkCertificateRevoked(serial string, ocspResponse []byte, reasonCode int) (err error) {
if _, err = ssa.GetCertificate(serial); err != nil {
return errors.New(fmt.Sprintf(
"Unable to mark certificate %s revoked: cert not found.", serial))
}
if _, err = ssa.GetCertificateStatus(serial); err != nil {
return errors.New(fmt.Sprintf(
"Unable to mark certificate %s revoked: cert status not found.", serial))
}
tx, err := ssa.db.Begin()
if err != nil {
return
}
// TODO: Also update crls.
_, err = tx.Exec(`INSERT INTO ocspResponses (serial, createdAt, response)
values (?, ?, ?)`,
serial, time.Now(), ocspResponse)
if err != nil {
tx.Rollback()
return
}
_, err = tx.Exec(`UPDATE certificateStatus SET
status=?, revokedDate=?, revokedReason=? WHERE serial=?`,
string(core.OCSPStatusRevoked), time.Now(), reasonCode, serial)
if err != nil {
tx.Rollback()
return
}
err = tx.Commit()
return
}
func (ssa *SQLStorageAuthority) UpdateRegistration(reg core.Registration) (err error) {
tx, err := ssa.db.Begin()
if err != nil {
@ -453,9 +537,9 @@ func (ssa *SQLStorageAuthority) AddCertificate(certDER []byte) (digest string, e
_, err = tx.Exec(`
INSERT INTO certificateStatus
(serial, subscriberApproved, status, revokedDate, ocspLastUpdated)
VALUES (?, 0, 'good', ?, ?);
`, serial, time.Time{}, time.Time{})
(serial, subscriberApproved, status, revokedDate, revokedReason, ocspLastUpdated)
VALUES (?, 0, 'good', ?, ?, ?);
`, serial, time.Time{}, 0, time.Time{})
if err != nil {
tx.Rollback()
return

View File

@ -15,8 +15,12 @@ import (
func TestAddCertificate(t *testing.T) {
sa, err := NewSQLStorageAuthority("sqlite3", ":memory:")
test.AssertNotError(t, err, "Failed to create SA")
sa.InitTables()
if err != nil {
t.Fatalf("Failed to create SA")
}
if err = sa.InitTables(); err != nil {
t.Fatalf("Failed to create SA")
}
// An example cert taken from EFF's website
certDER, err := ioutil.ReadFile("www.eff.org.der")
@ -27,8 +31,12 @@ func TestAddCertificate(t *testing.T) {
test.AssertEquals(t, digest, "qWoItDZmR4P9eFbeYgXXP3SR4ApnkQj8x4LsB_ORKBo")
// Example cert serial is 0x21bd4, so a prefix of all zeroes should fetch it.
retrievedDER, err := sa.GetCertificate("0000000000000000")
test.AssertNotError(t, err, "Couldn't get www.eff.org.der")
retrievedDER, err := sa.GetCertificateByShortSerial("0000000000000000")
test.AssertNotError(t, err, "Couldn't get www.eff.org.der by short serial")
test.AssertByteEquals(t, certDER, retrievedDER)
retrievedDER, err = sa.GetCertificate("00000000000000000000000000021bd4")
test.AssertNotError(t, err, "Couldn't get www.eff.org.der by full serial")
test.AssertByteEquals(t, certDER, retrievedDER)
certificateStatus, err := sa.GetCertificateStatus("00000000000000000000000000021bd4")
@ -46,7 +54,11 @@ func TestAddCertificate(t *testing.T) {
test.AssertEquals(t, digest2, "CMVYqWzyqUW7pfBF2CxL0Uk6I0Upsk7p4EWSnd_vYx4")
// Example cert serial is 0x21bd4, so a prefix of all zeroes should fetch it.
retrievedDER2, err := sa.GetCertificate("ff00000000000002")
retrievedDER2, err := sa.GetCertificateByShortSerial("ff00000000000002")
test.AssertNotError(t, err, "Couldn't get test-cert.der")
test.AssertByteEquals(t, certDER2, retrievedDER2)
retrievedDER2, err = sa.GetCertificate("ff00000000000002238054509817da5a")
test.AssertNotError(t, err, "Couldn't get test-cert.der")
test.AssertByteEquals(t, certDER2, retrievedDER2)
@ -57,16 +69,16 @@ func TestAddCertificate(t *testing.T) {
test.Assert(t, certificateStatus2.OCSPLastUpdated.IsZero(), "OCSPLastUpdated should be nil")
}
// TestGetCertificate tests some failure conditions for GetCertificate.
// TestGetCertificateByShortSerial tests some failure conditions for GetCertificate.
// Success conditions are tested above in TestAddCertificate.
func TestGetCertificate(t *testing.T) {
func TestGetCertificateByShortSerial(t *testing.T) {
sa, err := NewSQLStorageAuthority("sqlite3", ":memory:")
test.AssertNotError(t, err, "Failed to create SA")
sa.InitTables()
_, err = sa.GetCertificate("")
_, err = sa.GetCertificateByShortSerial("")
test.AssertError(t, err, "Should've failed on empty serial")
_, err = sa.GetCertificate("01020304050607080102030405060708")
_, err = sa.GetCertificateByShortSerial("01020304050607080102030405060708")
test.AssertError(t, err, "Should've failed on too-long serial")
}

View File

@ -41,13 +41,16 @@
"serialPrefix": 255,
"profile": "ee",
"dbDriver": "sqlite3",
"dbName": "boulder-ca.sqlite3",
"testMode": true
"dbName": ":memory:",
"testMode": true,
"issuerCert": "test/test-ca.pem",
"_comment": "This should only be present in testMode. In prod use an HSM.",
"issuerKey": "test/test-ca.key"
},
"sa": {
"dbDriver": "sqlite3",
"dbName": "boulder.sqlite3"
"dbName": ":memory:"
},
"mail": {

View File

@ -522,7 +522,7 @@ func (wfe *WebFrontEndImpl) Certificate(response http.ResponseWriter, request *h
}
wfe.log.Notice(fmt.Sprintf("Requested certificate ID %s", serial))
cert, err := wfe.SA.GetCertificate(serial)
cert, err := wfe.SA.GetCertificateByShortSerial(serial)
if err != nil {
wfe.notFound(response)
return