// 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/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 } const clientName = "AdminRevoker" 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, auditlogger := cmd.StatsAndLogging(c.Statsd, c.Syslog) amqpConf := c.Revoker.AMQP rac, err := rpc.NewRegistrationAuthorityClient(clientName, amqpConf, stats) cmd.FailOnError(err, "Unable to create CA client") dbMap, err := sa.NewDbMap(c.Revoker.DBConnect) cmd.FailOnError(err, "Couldn't setup database connection") sac, err := rpc.NewStorageAuthorityClient(clientName, amqpConf, stats) 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") }