Skip to content

Commit

Permalink
Merge pull request #90 from letsencrypt/85-relational_db_in_ca
Browse files Browse the repository at this point in the history
Add Relational DB support to CA (Issue #85)
  • Loading branch information
jsha committed Apr 15, 2015
2 parents 0eeeec1 + 6c6abeb commit cb615e8
Show file tree
Hide file tree
Showing 10 changed files with 377 additions and 16 deletions.
140 changes: 140 additions & 0 deletions ca/certificate-authority-data.go
@@ -0,0 +1,140 @@
// Copyright 2015 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package ca

import (
"database/sql"
"errors"
"time"

"github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log"
)

// CertificateAuthorityDatabaseImpl represents a database used by the CA; it
// enforces transaction semantics, and is effectively single-threaded.
type CertificateAuthorityDatabaseImpl struct {
log *blog.AuditLogger
db *sql.DB
activeTx *sql.Tx
}

// NewCertificateAuthorityDatabaseImpl constructs a Database for the
// Certificate Authority.
func NewCertificateAuthorityDatabaseImpl(logger *blog.AuditLogger, driver string, name string) (cadb core.CertificateAuthorityDatabase, err error) {
if logger == nil {
err = errors.New("Nil logger not permitted")
return
}

db, err := sql.Open(driver, name)
if err != nil {
return
}
if err = db.Ping(); err != nil {
return
}

cadb = &CertificateAuthorityDatabaseImpl{
db: db,
log: logger,
}

err = createTablesIfNotExist(db)
return
}

// createTablesIfNotExist builds the database tables and inserts the initial
// state, if the tables do not already exist. It is not an error for the tables
// to already exist.
func createTablesIfNotExist(db *sql.DB) (err error) {
tx, err := db.Begin()
if err != nil {
return
}

// Create serial number table
_, err = tx.Exec("CREATE TABLE serialNumber (id INTEGER, number INTEGER, lastUpdated DATETIME);")
if err != nil {
// If the table exists, exit early
tx.Rollback()
return nil
}

// Initialize the serial number
_, err = tx.Exec("INSERT INTO serialNumber (id, number, lastUpdated) VALUES (1, 1, ?);", time.Now())
if err != nil {
tx.Rollback()
return
}

err = tx.Commit()
return
}

// Begin starts a Database transaction. There can only be one in this object
// at a time.
func (cadb *CertificateAuthorityDatabaseImpl) Begin() (err error) {
if cadb.activeTx != nil {
err = errors.New("Transaction already open")
return
}
cadb.activeTx, err = cadb.db.Begin()
return
}

// Commit makes permanent a database transaction; there must be an active
// transaction when called.
func (cadb *CertificateAuthorityDatabaseImpl) Commit() (err error) {
if cadb.activeTx == nil {
err = errors.New("Transaction already closed")
return
}
err = cadb.activeTx.Commit()
cadb.activeTx = nil
return
}

// Rollback cancels the ongoing database transaction; there must be an active
// transaction when called.
func (cadb *CertificateAuthorityDatabaseImpl) Rollback() (err error) {
if cadb.activeTx == nil {
err = errors.New("Transaction already closed")
return
}
err = cadb.activeTx.Rollback()
cadb.activeTx = nil
return
}

// IncrementAndGetSerial returns the next-available serial number, incrementing
// it in the database before returning. There must be an active transaction to
// call this method. Callers should Begin the transaction, call this method,
// perform any other work, and Commit at the end once the certificate is issued.
func (cadb *CertificateAuthorityDatabaseImpl) IncrementAndGetSerial() (val int, err error) {
if cadb.activeTx == nil {
err = errors.New("No transaction open")
return
}

row := cadb.activeTx.QueryRow("SELECT number FROM serialNumber LIMIT 1;")

err = row.Scan(&val)
if err != nil {
cadb.activeTx.Rollback()
return
}

val = val + 1

_, err = cadb.activeTx.Exec("UPDATE serialNumber SET number=?, lastUpdated=? WHERE id=1", val, time.Now())
if err != nil {
cadb.activeTx.Rollback()
return
}

return
}
93 changes: 93 additions & 0 deletions ca/certificate-authority-data_test.go
@@ -0,0 +1,93 @@
// Copyright 2015 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package ca

import (
"testing"

_ "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/mattn/go-sqlite3"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/test"
)

const badDriver = "nothing"
const badFilename = "/doesnotexist/nofile"
const sqliteDriver = "sqlite3"
const sqliteName = ":memory:"

func TestConstruction(t *testing.T) {
log, err := blog.Dial("", "", "tag")
test.AssertNotError(t, err, "Could not construct audit logger")

// Successful case
_, err = NewCertificateAuthorityDatabaseImpl(log, sqliteDriver, sqliteName)
test.AssertNotError(t, err, "Could not construct CA DB")

// Covers "sql.Open" error
_, err = NewCertificateAuthorityDatabaseImpl(log, badDriver, sqliteName)
test.AssertError(t, err, "Should have failed construction")

// Covers "db.Ping" error
_, err = NewCertificateAuthorityDatabaseImpl(log, sqliteDriver, badFilename)
test.AssertError(t, err, "Should have failed construction")

// Ensures no nil pointer exception in logging
_, err = NewCertificateAuthorityDatabaseImpl(nil, sqliteDriver, sqliteName)
test.AssertError(t, err, "Should have failed construction")
}

func TestBeginCommit(t *testing.T) {
log, err := blog.Dial("", "", "tag")
test.AssertNotError(t, err, "Could not construct audit logger")

cadb, err := NewCertificateAuthorityDatabaseImpl(log, sqliteDriver, sqliteName)
test.AssertNotError(t, err, "Could not construct CA DB")

err = cadb.Begin()
test.AssertNotError(t, err, "Could not begin")

err = cadb.Begin()
test.AssertError(t, err, "Should have already begun")

err = cadb.Commit()
test.AssertNotError(t, err, "Could not commit")

err = cadb.Commit()
test.AssertError(t, err, "Should have already committed")

}

func TestGetSetSequenceOutsideTx(t *testing.T) {
log, err := blog.Dial("", "", "tag")
test.AssertNotError(t, err, "Could not construct audit logger")

cadb, err := NewCertificateAuthorityDatabaseImpl(log, sqliteDriver, sqliteName)
test.AssertNotError(t, err, "Could not construct CA DB")

_, err = cadb.IncrementAndGetSerial()
test.AssertError(t, err, "Not permitted")
}

func TestGetSetSequenceNumber(t *testing.T) {
log, err := blog.Dial("", "", "tag")
test.AssertNotError(t, err, "Could not construct audit logger")

cadb, err := NewCertificateAuthorityDatabaseImpl(log, sqliteDriver, sqliteName)
test.AssertNotError(t, err, "Could not construct CA DB")

err = cadb.Begin()
test.AssertNotError(t, err, "Could not begin")

num, err := cadb.IncrementAndGetSerial()
test.AssertNotError(t, err, "Could not get number")

num2, err := cadb.IncrementAndGetSerial()
test.AssertNotError(t, err, "Could not get number")
test.Assert(t, num+1 == num2, "Numbers should be incrementing")

err = cadb.Commit()
test.AssertNotError(t, err, "Could not commit")
}
46 changes: 44 additions & 2 deletions ca/certificate-authority.go
Expand Up @@ -9,6 +9,7 @@ import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"time"

"github.com/letsencrypt/boulder/core"
Expand All @@ -17,16 +18,21 @@ import (

"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/auth"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/config"
cfcsr "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/csr"
"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"
)

// 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
}

// NewCertificateAuthorityImpl creates a CA that talks to a remote CFSSL
Expand All @@ -35,14 +41,15 @@ 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(logger *blog.AuditLogger, hostport string, authKey string, profile string) (ca *CertificateAuthorityImpl, err error) {
func NewCertificateAuthorityImpl(logger *blog.AuditLogger, hostport string, authKey string, profile string, serialPrefix int, cadb core.CertificateAuthorityDatabase) (ca *CertificateAuthorityImpl, err error) {
logger.Notice("Certificate Authority Starting")

// 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,
// UseSerialSeq: true, // TODO: Awaiting CFSSL upgrade (Issue #83)
}

localProfile.Provider, err = auth.New(authKey, nil)
Expand All @@ -57,10 +64,19 @@ func NewCertificateAuthorityImpl(logger *blog.AuditLogger, hostport string, auth

pa := policy.NewPolicyAuthorityImpl(logger)

ca = &CertificateAuthorityImpl{Signer: signer, profile: profile, PA: pa, log: logger}
ca = &CertificateAuthorityImpl{
Signer: signer,
profile: profile,
PA: pa,
DB: cadb,
Prefix: serialPrefix,
log: logger,
}
return
}

// 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) {
// XXX Take in authorizations and verify that union covers CSR?
// Pull hostnames from CSR
Expand Down Expand Up @@ -101,37 +117,61 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest
Bytes: csr.Raw,
}))

// Get the next serial number
ca.DB.Begin()
serialDec, err := ca.DB.IncrementAndGetSerial()
if err != nil {
return
}
serialHex := fmt.Sprintf("%01X%014X", ca.Prefix, serialDec)
_ = serialHex // TODO: Remove when used below

// Send the cert off for signing
req := signer.SignRequest{
Request: csrPEM,
Profile: ca.profile,
Hosts: hostNames,
Subject: &signer.Subject{
CN: commonName,
Names: []cfcsr.Name{
{
C: "",
ST: "",
L: "",
O: "",
OU: "",
},
},
},
// SerialSeq: serialHex, // TODO: Awaiting CFSSL upgrade (Issue #83)
}

certPEM, err := ca.Signer.Sign(req)
if err != nil {
ca.DB.Rollback()
return
}

if len(certPEM) == 0 {
err = errors.New("No certificate returned by server")
ca.log.WarningErr(err)
ca.DB.Rollback()
return
}

block, _ := pem.Decode(certPEM)
if block == nil || block.Type != "CERTIFICATE" {
err = errors.New("Invalid certificate value returned")
ca.log.WarningErr(err)
ca.DB.Rollback()
return
}
certDER := block.Bytes

// Store the cert with the certificate authority, if provided
certID, err := ca.SA.AddCertificate(certDER)
if err != nil {
ca.DB.Rollback()
return
}

Expand All @@ -140,5 +180,7 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest
DER: certDER,
Status: core.StatusValid,
}

ca.DB.Commit()
return
}

0 comments on commit cb615e8

Please sign in to comment.