boulder/cmd/ocsp-responder/main.go

182 lines
6.1 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"
"encoding/hex"
"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/golang.org/x/crypto/ocsp"
gorp "github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
"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 *gorp.DbMap
caKeyHash []byte
}
// NewSourceFromDatabase produces a DBSource representing the binding of a
// given DB schema to a CA key.
func NewSourceFromDatabase(dbMap *gorp.DbMap, caKeyHash []byte) (src *DBSource, err error) {
src = &DBSource{dbMap: dbMap, caKeyHash: caKeyHash}
return
}
// Response is called by the HTTP server to handle a new OCSP request.
func (src *DBSource) Response(req *ocsp.Request) (response []byte, present bool) {
log := blog.GetAuditLogger()
// Check that this request is for the proper CA
if bytes.Compare(req.IssuerKeyHash, src.caKeyHash) != 0 {
log.Debug(fmt.Sprintf("Request intended for CA Cert ID: %s", hex.EncodeToString(req.IssuerKeyHash)))
present = false
return
}
serialString := core.SerialToString(req.SerialNumber)
log.Debug(fmt.Sprintf("Searching for OCSP issued by us for serial %s", serialString))
var ocspResponse core.OCSPResponse
// Note: we order by id rather than createdAt, because otherwise we sometimes
// get the wrong result if a certificate is revoked in the same second as its
// last update (e.g. client issues and instant revokes).
err := src.dbMap.SelectOne(&ocspResponse, "SELECT * from ocspResponses WHERE serial = :serial ORDER BY id DESC LIMIT 1;",
map[string]interface{}{"serial": serialString})
if err != nil {
present = false
return
}
log.Info(fmt.Sprintf("OCSP Response sent for CA=%s, Serial=%s", hex.EncodeToString(src.caKeyHash), serialString))
response = ocspResponse.Response
present = true
return
}
func makeDBSource(dbConnect, issuerCert string, sqlDebug bool) (cfocsp.Source, error) {
var noSource cfocsp.Source
// Configure DB
dbMap, err := sa.NewDbMap(dbConnect)
if err != nil {
return noSource, fmt.Errorf("Could not connect to database: %s", err)
}
sa.SetSQLDebug(dbMap, sqlDebug)
// Load the CA's key so we can store its SubjectKey in the DB
caCertDER, err := cmd.LoadCert(issuerCert)
if err != nil {
return noSource, fmt.Errorf("Could not read issuer cert %s: %s", issuerCert, err)
}
caCert, err := x509.ParseCertificate(caCertDER)
if err != nil {
return noSource, fmt.Errorf("Could not parse issuer cert %s: %s", issuerCert, err)
}
if len(caCert.SubjectKeyId) == 0 {
return noSource, fmt.Errorf("Empty subjectKeyID")
}
// Construct source from DB
return NewSourceFromDatabase(dbMap, caCert.SubjectKeyId)
}
func main() {
app := cmd.NewAppShell("boulder-ocsp-responder", "Handles OCSP requests")
app.Action = func(c cmd.Config) {
// Set up logging
stats, err := statsd.NewClient(c.Statsd.Server, c.Statsd.Prefix)
cmd.FailOnError(err, "Couldn't connect to statsd")
auditlogger, err := blog.Dial(c.Syslog.Network, c.Syslog.Server, c.Syslog.Tag, stats)
cmd.FailOnError(err, "Could not connect to Syslog")
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
defer auditlogger.AuditPanic()
blog.SetAuditLogger(auditlogger)
go cmd.DebugServer(c.OCSPResponder.DebugAddr)
go cmd.ProfileCmd("OCSP", stats)
auditlogger.Info(app.VersionString())
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))
source, err = makeDBSource(config.Source, c.Common.IssuerCert, c.SQL.SQLDebug)
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))
}
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")
// Configure HTTP
m := http.NewServeMux()
m.Handle(c.OCSPResponder.Path, cfocsp.Responder{Source: source})
httpMonitor := metrics.NewHTTPMonitor(stats, m, "OCSP")
srv := &http.Server{
Addr: c.OCSPResponder.ListenAddress,
ConnState: httpMonitor.ConnectionMonitor,
Handler: httpMonitor.Handle(),
}
hd := &httpdown.HTTP{
StopTimeout: stopTimeout,
KillTimeout: killTimeout,
}
err = httpdown.ListenAndServe(srv, hd)
cmd.FailOnError(err, "Error starting HTTP server")
}
app.Run()
}