Merge pull request #223 from rolandshoemaker/revoker

admin-revoker tool
This commit is contained in:
Jacob Hoffman-Andrews 2015-05-26 14:37:33 -07:00
commit 19fd285859
10 changed files with 300 additions and 84 deletions

View File

@ -176,7 +176,7 @@ func dupeNames(names []string) bool {
return false
}
func (ca *CertificateAuthorityImpl) RevokeCertificate(serial string) (err error) {
func (ca *CertificateAuthorityImpl) RevokeCertificate(serial string, reasonCode int) (err error) {
certDER, err := ca.SA.GetCertificate(serial)
if err != nil {
return err
@ -186,21 +186,17 @@ func (ca *CertificateAuthorityImpl) RevokeCertificate(serial string) (err error)
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,
Reason: reasonCode,
RevokedAt: time.Now(),
}
ocspResponse, err := ca.OCSPSigner.Sign(signRequest)
if err != nil {
return err
}
err = ca.SA.MarkCertificateRevoked(serial, ocspResponse, reason)
err = ca.SA.MarkCertificateRevoked(serial, ocspResponse, reasonCode)
return err
}

View File

@ -368,7 +368,7 @@ func TestRevoke(t *testing.T) {
cert, err := x509.ParseCertificate(certObj.DER)
test.AssertNotError(t, err, "Certificate failed to parse")
serialString := core.SerialToString(cert.SerialNumber)
err = ca.RevokeCertificate(serialString)
err = ca.RevokeCertificate(serialString, 0)
test.AssertNotError(t, err, "Revocation failed")
status, err := storageAuthority.GetCertificateStatus(serialString)

242
cmd/admin-revoker/main.go Normal file
View File

@ -0,0 +1,242 @@
// 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 (
"crypto/x509"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"sort"
"strconv"
"strings"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/codegangsta/cli"
// Load both drivers to allow configuring either
_ "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/go-sql-driver/mysql"
_ "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/mattn/go-sqlite3"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/rpc"
"github.com/letsencrypt/boulder/sa"
gorp "github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
)
var reasons map[int]string = map[int]string{
0: "unspecified",
1: "keyCompromise",
2: "cACompromise",
3: "affiliationChanged",
4: "superseded",
5: "cessationOfOperation",
6: "certificateHold",
// 7 is unused
8: "removeFromCRL", // needed?
9: "privilegeWithdrawn",
10: "aAcompromise",
}
func loadConfig(c *cli.Context) (config cmd.Config, err error) {
configFileName := c.GlobalString("config")
configJSON, err := ioutil.ReadFile(configFileName)
if err != nil {
return
}
err = json.Unmarshal(configJSON, &config)
return
}
func setupContext(context *cli.Context) (rpc.CertificateAuthorityClient, *blog.AuditLogger, *gorp.DbMap) {
c, err := loadConfig(context)
cmd.FailOnError(err, "Failed to load Boulder configuration")
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")
blog.SetAuditLogger(auditlogger)
ch := cmd.AmqpChannel(c.AMQP.Server)
cac, err := rpc.NewCertificateAuthorityClient(c.AMQP.CA.Client, c.AMQP.CA.Server, ch)
cmd.FailOnError(err, "Unable to create CA client")
dbMap, err := sa.NewDbMap(c.Revoker.DBDriver, c.Revoker.DBName)
cmd.FailOnError(err, "Couldn't setup database connection")
return cac, auditlogger, dbMap
}
func addDeniedNames(tx *gorp.Transaction, names []string) (err error) {
sort.Strings(names)
deniedCSR := &core.DeniedCsr{Names: strings.ToLower(strings.Join(names, ","))}
err = tx.Insert(deniedCSR)
return
}
func revokeBySerial(serial string, reasonCode int, deny bool, cac rpc.CertificateAuthorityClient, auditlogger *blog.AuditLogger, tx *gorp.Transaction) (err error) {
if reasonCode < 0 || reasonCode == 7 || reasonCode > 10 {
panic(fmt.Sprintf("Invalid reason code: %d", reasonCode))
}
if deny {
// Retrieve DNS names associated with serial
var certificate core.Certificate
err = tx.SelectOne(&certificate, "SELECT * FROM certificates WHERE serial = :serial",
map[string]interface{}{"serial": serial})
if err != nil {
return
}
var cert *x509.Certificate
cert, err = x509.ParseCertificate(certificate.DER)
if err != nil {
return
}
err = addDeniedNames(tx, append(cert.DNSNames, cert.Subject.CommonName))
if err != nil {
return
}
}
err = cac.RevokeCertificate(serial, reasonCode)
if err != nil {
return
}
auditlogger.Info(fmt.Sprintf("Revoked certificate %s with reason '%s'", serial, reasons[reasonCode]))
return
}
func revokeByReg(regID int, reasonCode int, deny bool, cac rpc.CertificateAuthorityClient, auditlogger *blog.AuditLogger, tx *gorp.Transaction) (err error) {
_, err = tx.Get(core.Registration{}, regID)
if err != nil {
return
}
var certs []core.Certificate
_, err = tx.Select(certs, "SELECT serial FROM certificates WHERE registrationID = :regID", map[string]interface{}{"regID": regID})
if err != nil {
return
}
for _, cert := range certs {
err = revokeBySerial(cert.Serial, reasonCode, deny, cac, auditlogger, tx)
if err != nil {
return
}
}
return
}
var version string = "0.0.1"
func main() {
app := cli.NewApp()
app.Name = "admin-revoker"
app.Version = version
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "config",
Value: "config.json",
EnvVar: "BOULDER_CONFIG",
Usage: "Path to Boulder JSON configuration file",
},
cli.BoolFlag{
Name: "deny-future",
Usage: "Add certificate DNS names to the denied list",
},
}
app.Commands = []cli.Command{
{
Name: "serial-revoke",
Usage: "Revoke a single certificate by the hex serial number",
Action: func(c *cli.Context) {
// 1: serial, 2: reasonCode (3: deny flag)
serial := c.Args().First()
reasonCode, err := strconv.Atoi(c.Args().Get(1))
cmd.FailOnError(err, "Reason code argument must be a integer")
deny := c.GlobalBool("deny")
cac, auditlogger, dbMap := setupContext(c)
tx, err := dbMap.Begin()
if err != nil {
tx.Rollback()
}
cmd.FailOnError(err, "Couldn't begin transaction")
err = revokeBySerial(serial, reasonCode, deny, cac, auditlogger, tx)
if err != nil {
tx.Rollback()
}
cmd.FailOnError(err, "Couldn't revoke certificate")
err = tx.Commit()
cmd.FailOnError(err, "Couldn't cleanly close transaction")
},
},
{
Name: "reg-revoke",
Usage: "Revoke all certificates associated with a registration ID",
Action: func(c *cli.Context) {
// 1: registration ID, 2: reasonCode (3: deny flag)
regID, err := strconv.Atoi(c.Args().First())
cmd.FailOnError(err, "Registration ID argument must be a integer")
reasonCode, err := strconv.Atoi(c.Args().Get(1))
cmd.FailOnError(err, "Reason code argument must be a integer")
deny := c.GlobalBool("deny")
cac, auditlogger, dbMap := setupContext(c)
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
defer auditlogger.AuditPanic()
tx, err := dbMap.Begin()
if err != nil {
tx.Rollback()
}
cmd.FailOnError(err, "Couldn't begin transaction")
err = revokeByReg(regID, reasonCode, deny, cac, auditlogger, tx)
if err != nil {
tx.Rollback()
}
cmd.FailOnError(err, "Couldn't revoke certificate")
err = tx.Commit()
cmd.FailOnError(err, "Couldn't cleanly close transaction")
},
},
{
Name: "list-reasons",
Usage: "List all revocation reason codes",
Action: func(c *cli.Context) {
var codes []int
for k, _ := range reasons {
codes = append(codes, k)
}
sort.Ints(codes)
fmt.Printf("Revocation reason codes\n-----------------------\n\n")
for _, k := range codes {
fmt.Printf("%d: %s\n", k, reasons[k])
}
},
},
}
err := app.Run(os.Args)
cmd.FailOnError(err, "Failed to run application")
}

View File

@ -80,6 +80,11 @@ type Config struct {
Tag string
}
Revoker struct {
DBDriver string
DBName string
}
Mail struct {
Server string
Port string

View File

@ -79,7 +79,7 @@ type ValidationAuthority interface {
type CertificateAuthority interface {
// [RegistrationAuthority]
IssueCertificate(x509.CertificateRequest, int64) (Certificate, error)
RevokeCertificate(serial string) error
RevokeCertificate(string, int) error
}
type PolicyAuthority interface {
@ -107,8 +107,6 @@ type StorageAdder interface {
MarkCertificateRevoked(serial string, ocspResponse []byte, reasonCode int) error
AddCertificate([]byte, int64) (string, error)
AddDeniedCSR([]string) error
}
// StorageAuthority interface represents a simple key/value

View File

@ -280,7 +280,7 @@ func (ra *RegistrationAuthorityImpl) UpdateAuthorization(base core.Authorization
func (ra *RegistrationAuthorityImpl) RevokeCertificate(cert x509.Certificate) error {
serialString := core.SerialToString(cert.SerialNumber)
err := ra.CA.RevokeCertificate(serialString)
err := ra.CA.RevokeCertificate(serialString, 0)
// AUDIT[ Revocation Requests ] 4e85d791-09c0-4ab3-a837-d3d67e945134
if err != nil {

View File

@ -459,7 +459,16 @@ func NewCertificateAuthorityServer(serverQueue string, channel *amqp.Channel, im
})
rpc.Handle(MethodRevokeCertificateCA, func(req []byte) []byte {
if err := impl.RevokeCertificate(string(req)); err != nil {
var revokeReq struct {
Serial string
ReasonCode int
}
err := json.Unmarshal(req, &revokeReq)
if err != nil {
return nil
}
if err := impl.RevokeCertificate(revokeReq.Serial, revokeReq.ReasonCode); err != nil {
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
errorCondition(MethodRevokeCertificateCA, err, req)
}
@ -506,8 +515,20 @@ func (cac CertificateAuthorityClient) IssueCertificate(csr x509.CertificateReque
return
}
func (cac CertificateAuthorityClient) RevokeCertificate(serial string) (err error) {
_, err = cac.rpc.DispatchSync(MethodRevokeCertificateCA, []byte(serial))
func (cac CertificateAuthorityClient) RevokeCertificate(serial string, reasonCode int) (err error) {
var revokeReq struct {
Serial string
ReasonCode int
}
revokeReq.Serial = serial
revokeReq.ReasonCode = reasonCode
data, err := json.Marshal(revokeReq)
if err != nil {
return
}
_, err = cac.rpc.DispatchSync(MethodRevokeCertificateCA, data)
return
}
@ -715,24 +736,6 @@ func NewStorageAuthorityServer(serverQueue string, channel *amqp.Channel, impl c
return nil
})
rpc.Handle(MethodAddDeniedCSR, func(req []byte) []byte {
var csrReq struct {
Names []string
}
if err := json.Unmarshal(req, csrReq); err != nil {
// AUDIT[ Improper Messages ] 0786b6f2-91ca-4f48-9883-842a19084c64
improperMessage(MethodAddDeniedCSR, err, req)
return nil
}
if err := impl.AddDeniedCSR(csrReq.Names); err != nil {
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
errorCondition(MethodAddDeniedCSR, err, csrReq)
}
return nil
})
rpc.Handle(MethodAlreadyDeniedCSR, func(req []byte) []byte {
var csrReq struct {
Names []string
@ -944,21 +947,6 @@ func (cac StorageAuthorityClient) AddCertificate(cert []byte, regID int64) (id s
return
}
func (cac StorageAuthorityClient) AddDeniedCSR(names []string) (err error) {
var sliceReq struct {
Names []string
}
sliceReq.Names = names
data, err := json.Marshal(sliceReq)
if err != nil {
return
}
_, err = cac.rpc.DispatchSync(MethodAddDeniedCSR, data)
return
}
func (cac StorageAuthorityClient) AlreadyDeniedCSR(names []string) (exists bool, err error) {
var sliceReq struct {
Names []string
@ -972,7 +960,7 @@ func (cac StorageAuthorityClient) AlreadyDeniedCSR(names []string) (exists bool,
response, err := cac.rpc.DispatchSync(MethodAlreadyDeniedCSR, data)
if err != nil || len(response) == 0 {
err = errors.New("AddDeniedCSR RPC failed") // XXX
err = errors.New("AlreadyDeniedCSR RPC failed") // XXX
return
}

View File

@ -140,11 +140,8 @@ func (tc boulderTypeConverter) FromDb(target interface{}) (gorp.CustomScanner, b
}
}
func NewSQLStorageAuthority(driver string, name string) (ssa *SQLStorageAuthority, err error) {
logger := blog.GetAuditLogger()
logger.Notice("Storage Authority Starting")
db, err := sql.Open(driver, name)
func NewDbMap(driver, dbName string) (dbMap *gorp.DbMap, err error) {
db, err := sql.Open(driver, dbName)
if err != nil {
return
}
@ -158,10 +155,21 @@ func NewSQLStorageAuthority(driver string, name string) (ssa *SQLStorageAuthorit
return
}
dbmap := &gorp.DbMap{Db: db, Dialect: dialect, TypeConverter: boulderTypeConverter{}}
dbMap = &gorp.DbMap{Db: db, Dialect: dialect, TypeConverter: boulderTypeConverter{}}
return
}
func NewSQLStorageAuthority(driver string, name string) (ssa *SQLStorageAuthority, err error) {
logger := blog.GetAuditLogger()
logger.Notice("Storage Authority Starting")
dbMap, err := NewDbMap(driver, name)
if err != nil {
return
}
ssa = &SQLStorageAuthority{
dbMap: dbmap,
dbMap: dbMap,
log: logger,
bucket: make(map[string]interface{}),
}
@ -182,7 +190,7 @@ func (ssa *SQLStorageAuthority) InitTables() (err error) {
ssa.dbMap.AddTableWithName(core.CertificateStatus{}, "certificateStatus").SetKeys(false, "Serial").SetVersionCol("LockCol")
ssa.dbMap.AddTableWithName(core.OcspResponse{}, "ocspResponses").SetKeys(true, "ID")
ssa.dbMap.AddTableWithName(core.Crl{}, "crls").SetKeys(false, "Serial")
ssa.dbMap.AddTableWithName(core.DeniedCsr{}, "deniedCsrs").SetKeys(true, "ID").ColMap("Names").SetUnique(true)
ssa.dbMap.AddTableWithName(core.DeniedCsr{}, "deniedCsrs").SetKeys(true, "ID")
err = ssa.dbMap.CreateTablesIfNotExists()
return
@ -680,25 +688,6 @@ func (ssa *SQLStorageAuthority) AddCertificate(certDER []byte, regID int64) (dig
return
}
func (ssa *SQLStorageAuthority) AddDeniedCSR(names []string) (err error) {
sort.Strings(names)
deniedCSR := &core.DeniedCsr{Names: strings.ToLower(strings.Join(names, ","))}
tx, err := ssa.dbMap.Begin()
if err != nil {
return
}
err = tx.Insert(deniedCSR)
if err != nil {
tx.Rollback()
return
}
err = tx.Commit()
return
}
func (ssa *SQLStorageAuthority) AlreadyDeniedCSR(names []string) (already bool, err error) {
sort.Strings(names)

View File

@ -203,11 +203,4 @@ func TestDeniedCSR(t *testing.T) {
exists, err := sa.AlreadyDeniedCSR(append(csr.DNSNames, csr.Subject.CommonName))
test.AssertNotError(t, err, "AlreadyDeniedCSR failed")
test.Assert(t, !exists, "Found non-existent CSR")
err = sa.AddDeniedCSR(append(csr.DNSNames, csr.Subject.CommonName))
test.AssertNotError(t, err, "Couldn't add the denied CSR to the DB")
exists, err = sa.AlreadyDeniedCSR(append(csr.DNSNames, csr.Subject.CommonName))
test.AssertNotError(t, err, "AlreadyDeniedCSR failed")
test.Assert(t, exists, "Couldn't find denied CSR in DB")
}

View File

@ -46,7 +46,7 @@
"issuerCert": "test/test-ca.pem",
"_comment": "This should only be present in testMode. In prod use an HSM.",
"issuerKey": "test/test-ca.key",
"expiry": "8760h",
"expiry": "8760h"
},
"sa": {
@ -54,6 +54,11 @@
"dbName": ":memory:"
},
"revoker": {
"dbDriver": "sqlite3",
"dbName": ":memory:"
},
"mail": {
"server": "mail.example.com",
"port": "25",