Merge pull request #134 from letsencrypt/ocsp-table
More steps towards revocation / OCSP support
This commit is contained in:
commit
c4497aca72
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
11
cmd/shell.go
11
cmd/shell.go
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue