253 lines
7.2 KiB
Go
253 lines
7.2 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 (
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/user"
|
|
"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"
|
|
gorp "github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
|
|
"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"
|
|
)
|
|
|
|
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.RegistrationAuthorityClient, *blog.AuditLogger, *gorp.DbMap, rpc.StorageAuthorityClient) {
|
|
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, err := rpc.AmqpChannel(c)
|
|
cmd.FailOnError(err, "Could not connect to AMQP")
|
|
|
|
raRPC, err := rpc.NewAmqpRPCClient("AdminRevoker->RA", c.AMQP.RA.Server, ch, stats)
|
|
cmd.FailOnError(err, "Unable to create RPC client")
|
|
|
|
rac, err := rpc.NewRegistrationAuthorityClient(raRPC)
|
|
cmd.FailOnError(err, "Unable to create CA client")
|
|
|
|
dbMap, err := sa.NewDbMap(c.Revoker.DBConnect)
|
|
cmd.FailOnError(err, "Couldn't setup database connection")
|
|
|
|
saRPC, err := rpc.NewAmqpRPCClient("AdminRevoker->SA", c.AMQP.SA.Server, ch, stats)
|
|
cmd.FailOnError(err, "Unable to create RPC client")
|
|
|
|
sac, err := rpc.NewStorageAuthorityClient(saRPC)
|
|
cmd.FailOnError(err, "Failed to create SA client")
|
|
|
|
return rac, auditlogger, dbMap, sac
|
|
}
|
|
|
|
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 core.RevocationCode, deny bool, rac rpc.RegistrationAuthorityClient, auditlogger *blog.AuditLogger, tx *gorp.Transaction) (err error) {
|
|
if reasonCode < 0 || reasonCode == 7 || reasonCode > 10 {
|
|
panic(fmt.Sprintf("Invalid reason code: %d", reasonCode))
|
|
}
|
|
|
|
certObj, err := tx.Get(core.Certificate{}, serial)
|
|
if err != nil {
|
|
return
|
|
}
|
|
certificate, ok := certObj.(*core.Certificate)
|
|
if !ok {
|
|
err = fmt.Errorf("Cast failure")
|
|
return
|
|
}
|
|
cert, err := x509.ParseCertificate(certificate.DER)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if deny {
|
|
// Retrieve DNS names associated with serial
|
|
err = addDeniedNames(tx, append(cert.DNSNames, cert.Subject.CommonName))
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
u, err := user.Current()
|
|
err = rac.AdministrativelyRevokeCertificate(*cert, reasonCode, u.Username)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
auditlogger.Info(fmt.Sprintf("Revoked certificate %s with reason '%s'", serial, core.RevocationReasons[reasonCode]))
|
|
return
|
|
}
|
|
|
|
func revokeByReg(regID int64, reasonCode core.RevocationCode, deny bool, rac rpc.RegistrationAuthorityClient, auditlogger *blog.AuditLogger, tx *gorp.Transaction) (err error) {
|
|
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, rac, auditlogger, tx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// This abstraction is needed so that we can use sort.Sort below
|
|
type revocationCodes []core.RevocationCode
|
|
|
|
func (rc revocationCodes) Len() int {
|
|
return len(rc)
|
|
}
|
|
|
|
func (rc revocationCodes) Less(i, j int) bool {
|
|
return rc[i] < rc[j]
|
|
}
|
|
|
|
func (rc revocationCodes) Swap(i, j int) {
|
|
rc[i], rc[j] = rc[j], rc[i]
|
|
}
|
|
|
|
func main() {
|
|
app := cli.NewApp()
|
|
app.Name = "admin-revoker"
|
|
app.Usage = "Revokes issued certificates"
|
|
app.Version = cmd.Version()
|
|
app.Author = "Boulder contributors"
|
|
app.Email = "ca-dev@letsencrypt.org"
|
|
|
|
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",
|
|
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, core.RevocationCode(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.ParseInt(c.Args().First(), 10, 64)
|
|
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, sac := 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 = sac.GetRegistration(regID)
|
|
if err != nil {
|
|
cmd.FailOnError(err, "Couldn't fetch registration")
|
|
}
|
|
|
|
err = revokeByReg(regID, core.RevocationCode(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 revocationCodes
|
|
for k := range core.RevocationReasons {
|
|
codes = append(codes, k)
|
|
}
|
|
sort.Sort(codes)
|
|
fmt.Printf("Revocation reason codes\n-----------------------\n\n")
|
|
for _, k := range codes {
|
|
fmt.Printf("%d: %s\n", k, core.RevocationReasons[k])
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
err := app.Run(os.Args)
|
|
cmd.FailOnError(err, "Failed to run application")
|
|
}
|