193 lines
6.9 KiB
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()
|
|
}
|