package main import ( "crypto/tls" "crypto/x509" "database/sql" "flag" "fmt" "os" "os/user" "sort" "strconv" "golang.org/x/net/context" "gopkg.in/go-gorp/gorp.v2" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/core" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/features" bgrpc "github.com/letsencrypt/boulder/grpc" blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" rapb "github.com/letsencrypt/boulder/ra/proto" "github.com/letsencrypt/boulder/revocation" "github.com/letsencrypt/boulder/sa" sapb "github.com/letsencrypt/boulder/sa/proto" ) const clientName = "AdminRevoker" const usageString = ` usage: admin-revoker serial-revoke --config admin-revoker reg-revoke --config admin-revoker list-reasons --config admin-revoker auth-revoke --config command descriptions: serial-revoke Revoke a single certificate by the hex serial number reg-revoke Revoke all certificates associated with a registration ID list-reasons List all revocation reason codes auth-revoke Revoke all pending/valid authorizations for a domain args: config File path to the configuration file for this service ` type config struct { Revoker struct { cmd.DBConfig // Similarly, the Revoker needs a TLSConfig to set up its GRPC client certs, // but doesn't get the TLS field from ServiceConfig, so declares its own. TLS cmd.TLSConfig RAService *cmd.GRPCClientConfig SAService *cmd.GRPCClientConfig Features map[string]bool } Syslog cmd.SyslogConfig } func setupContext(c config) (core.RegistrationAuthority, blog.Logger, *gorp.DbMap, core.StorageAuthority, metrics.Scope) { scope, logger := cmd.StatsAndLogging(c.Syslog) var tls *tls.Config var err error if c.Revoker.TLS.CertFile != nil { tls, err = c.Revoker.TLS.Load() cmd.FailOnError(err, "TLS config") } raConn, err := bgrpc.ClientSetup(c.Revoker.RAService, tls, scope) cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to RA") rac := bgrpc.NewRegistrationAuthorityClient(rapb.NewRegistrationAuthorityClient(raConn)) dbURL, err := c.Revoker.DBConfig.URL() cmd.FailOnError(err, "Couldn't load DB URL") dbMap, err := sa.NewDbMap(dbURL, c.Revoker.DBConfig.MaxDBConns) cmd.FailOnError(err, "Couldn't setup database connection") go sa.ReportDbConnCount(dbMap, scope) saConn, err := bgrpc.ClientSetup(c.Revoker.SAService, tls, scope) cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to SA") sac := bgrpc.NewStorageAuthorityClient(sapb.NewStorageAuthorityClient(saConn)) return rac, logger, dbMap, sac, scope } func revokeBySerial(ctx context.Context, serial string, reasonCode revocation.Reason, rac core.RegistrationAuthority, logger blog.Logger, tx *gorp.Transaction) (err error) { if reasonCode < 0 || reasonCode == 7 || reasonCode > 10 { panic(fmt.Sprintf("Invalid reason code: %d", reasonCode)) } certObj, err := sa.SelectCertificate(tx, "WHERE serial = ?", serial) if err == sql.ErrNoRows { return berrors.NotFoundError("certificate with serial %q not found", serial) } if err != nil { return err } cert, err := x509.ParseCertificate(certObj.DER) if err != nil { return } u, err := user.Current() err = rac.AdministrativelyRevokeCertificate(ctx, *cert, reasonCode, u.Username) if err != nil { return } logger.Info(fmt.Sprintf("Revoked certificate %s with reason '%s'", serial, revocation.ReasonToString[reasonCode])) return } func revokeByReg(ctx context.Context, regID int64, reasonCode revocation.Reason, rac core.RegistrationAuthority, logger blog.Logger, 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(ctx, cert.Serial, reasonCode, rac, logger, tx) if err != nil { return } } return } // This abstraction is needed so that we can use sort.Sort below type revocationCodes []revocation.Reason 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() { usage := func() { fmt.Fprintf(os.Stderr, usageString) os.Exit(1) } if len(os.Args) <= 2 { usage() } command := os.Args[1] flagSet := flag.NewFlagSet(command, flag.ContinueOnError) configFile := flagSet.String("config", "", "File path to the configuration file for this service") err := flagSet.Parse(os.Args[2:]) cmd.FailOnError(err, "Error parsing flagset") if *configFile == "" { usage() } var c config err = cmd.ReadConfigFile(*configFile, &c) cmd.FailOnError(err, "Reading JSON config file into config structure") err = features.Set(c.Revoker.Features) cmd.FailOnError(err, "Failed to set feature flags") ctx := context.Background() args := flagSet.Args() switch { case command == "serial-revoke" && len(args) == 2: // 1: serial, 2: reasonCode serial := args[0] reasonCode, err := strconv.Atoi(args[1]) cmd.FailOnError(err, "Reason code argument must be an integer") rac, logger, dbMap, _, _ := setupContext(c) tx, err := dbMap.Begin() if err != nil { cmd.FailOnError(sa.Rollback(tx, err), "Couldn't begin transaction") } err = revokeBySerial(ctx, serial, revocation.Reason(reasonCode), rac, logger, tx) if err != nil { cmd.FailOnError(sa.Rollback(tx, err), "Couldn't revoke certificate") } err = tx.Commit() cmd.FailOnError(err, "Couldn't cleanly close transaction") case command == "reg-revoke" && len(args) == 2: // 1: registration ID, 2: reasonCode regID, err := strconv.ParseInt(args[0], 10, 64) cmd.FailOnError(err, "Registration ID argument must be an integer") reasonCode, err := strconv.Atoi(args[1]) cmd.FailOnError(err, "Reason code argument must be an integer") rac, logger, dbMap, sac, _ := setupContext(c) defer logger.AuditPanic() tx, err := dbMap.Begin() if err != nil { cmd.FailOnError(sa.Rollback(tx, err), "Couldn't begin transaction") } _, err = sac.GetRegistration(ctx, regID) if err != nil { cmd.FailOnError(err, "Couldn't fetch registration") } err = revokeByReg(ctx, regID, revocation.Reason(reasonCode), rac, logger, tx) if err != nil { cmd.FailOnError(sa.Rollback(tx, err), "Couldn't revoke certificate") } err = tx.Commit() cmd.FailOnError(err, "Couldn't cleanly close transaction") case command == "list-reasons": var codes revocationCodes for k := range revocation.ReasonToString { 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, revocation.ReasonToString[k]) } case command == "auth-revoke" && len(args) == 1: domain := args[0] _, logger, _, sac, stats := setupContext(c) ident := core.AcmeIdentifier{Value: domain, Type: core.IdentifierDNS} authsRevoked, pendingAuthsRevoked, err := sac.RevokeAuthorizationsByDomain(ctx, ident) cmd.FailOnError(err, fmt.Sprintf("Failed to revoke authorizations for %s", ident.Value)) logger.Info(fmt.Sprintf( "Revoked %d pending authorizations and %d final authorizations\n", pendingAuthsRevoked, authsRevoked, )) stats.Inc("RevokedAuthorizations", authsRevoked) stats.Inc("RevokedPendingAuthorizations", pendingAuthsRevoked) default: usage() } }