boulder/cmd/ocsp-responder/main.go

193 lines
6.9 KiB
Go

// 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 main
import (
"bytes"
"crypto/x509"
"database/sql"
"encoding/hex"
"errors"
"fmt"
"net/http"
"net/url"
"time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
cfocsp "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/ocsp"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/facebookgo/httpdown"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/jmhodges/clock"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/golang.org/x/crypto/ocsp"
"github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/sa"
)
/*
DBSource maps a given Database schema to a CA Key Hash, so we can pick
from among them when presented with OCSP requests for different certs.
We assume that OCSP responses are stored in a very simple database table,
with two columns: serialNumber and response
CREATE TABLE ocsp_responses (serialNumber TEXT, response BLOB);
The serialNumber field may have any type to which Go will match a string,
so you can be more efficient than TEXT if you like. We use it to store the
serial number in base64. You probably want to have an index on the
serialNumber field, since we will always query on it.
*/
type DBSource struct {
dbMap dbSelector
caKeyHash []byte
log *blog.AuditLogger
}
// Since the only thing we use from gorp is the SelectOne method on the
// gorp.DbMap object, we just define the interface an interface with that method
// instead of importing all of gorp. This also allows us to simulate MySQL failures
// by mocking the interface.
type dbSelector interface {
SelectOne(holder interface{}, query string, args ...interface{}) error
}
// NewSourceFromDatabase produces a DBSource representing the binding of a
// given DB schema to a CA key.
func NewSourceFromDatabase(dbMap dbSelector, caKeyHash []byte, log *blog.AuditLogger) (src *DBSource, err error) {
src = &DBSource{dbMap: dbMap, caKeyHash: caKeyHash, log: log}
return
}
// Response is called by the HTTP server to handle a new OCSP request.
func (src *DBSource) Response(req *ocsp.Request) ([]byte, bool) {
// Check that this request is for the proper CA
if bytes.Compare(req.IssuerKeyHash, src.caKeyHash) != 0 {
src.log.Debug(fmt.Sprintf("Request intended for CA Cert ID: %s", hex.EncodeToString(req.IssuerKeyHash)))
return nil, false
}
serialString := core.SerialToString(req.SerialNumber)
src.log.Debug(fmt.Sprintf("Searching for OCSP issued by us for serial %s", serialString))
var response []byte
defer func() {
if len(response) != 0 {
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",
map[string]interface{}{"serial": serialString},
)
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
}
return response, true
}
func makeDBSource(dbMap dbSelector, issuerCert string, log *blog.AuditLogger) (*DBSource, error) {
// Load the CA's key so we can store its SubjectKey in the DB
caCertDER, err := cmd.LoadCert(issuerCert)
if err != nil {
return nil, fmt.Errorf("Could not read issuer cert %s: %s", issuerCert, err)
}
caCert, err := x509.ParseCertificate(caCertDER)
if err != nil {
return nil, fmt.Errorf("Could not parse issuer cert %s: %s", issuerCert, err)
}
if len(caCert.SubjectKeyId) == 0 {
return nil, fmt.Errorf("Empty subjectKeyID")
}
// Construct source from DB
return NewSourceFromDatabase(dbMap, caCert.SubjectKeyId, log)
}
func main() {
app := cmd.NewAppShell("boulder-ocsp-responder", "Handles OCSP requests")
app.Action = func(c cmd.Config, stats statsd.Statter, auditlogger *blog.AuditLogger) {
go cmd.DebugServer(c.OCSPResponder.DebugAddr)
go cmd.ProfileCmd("OCSP", stats)
config := c.OCSPResponder
var source cfocsp.Source
url, err := url.Parse(config.Source)
cmd.FailOnError(err, fmt.Sprintf("Source was not a URL: %s", config.Source))
if url.Scheme == "mysql+tcp" {
auditlogger.Info(fmt.Sprintf("Loading OCSP Database for CA Cert: %s", c.Common.IssuerCert))
dbMap, err := sa.NewDbMap(config.Source)
cmd.FailOnError(err, "Could not connect to database")
if c.SQL.SQLDebug {
sa.SetSQLDebug(dbMap, true)
}
source, err = makeDBSource(dbMap, c.Common.IssuerCert, auditlogger)
cmd.FailOnError(err, "Couldn't load OCSP DB")
} else if url.Scheme == "file" {
filename := url.Path
// Go interprets cwd-relative file urls (file:test/foo.txt) as having the
// relative part of the path in the 'Opaque' field.
if filename == "" {
filename = url.Opaque
}
source, err = cfocsp.NewSourceFromFile(filename)
cmd.FailOnError(err, fmt.Sprintf("Couldn't read file: %s", url.Path))
} else {
cmd.FailOnError(errors.New(`"source" parameter not found in JSON config`), "unable to start ocsp-responder")
}
stopTimeout, err := time.ParseDuration(c.OCSPResponder.ShutdownStopTimeout)
cmd.FailOnError(err, "Couldn't parse shutdown stop timeout")
killTimeout, err := time.ParseDuration(c.OCSPResponder.ShutdownKillTimeout)
cmd.FailOnError(err, "Couldn't parse shutdown kill timeout")
m := http.StripPrefix(c.OCSPResponder.Path, cfocsp.NewResponder(source))
httpMonitor := metrics.NewHTTPMonitor(stats, m, "OCSP")
srv := &http.Server{
Addr: c.OCSPResponder.ListenAddress,
Handler: httpMonitor.Handle(),
}
hd := &httpdown.HTTP{
StopTimeout: stopTimeout,
KillTimeout: killTimeout,
Stats: metrics.NewFBAdapter(stats, "OCSP", clock.Default()),
}
err = httpdown.ListenAndServe(srv, hd)
cmd.FailOnError(err, "Error starting HTTP server")
}
app.Run()
}