diff --git a/ca/certificate-authority.go b/ca/certificate-authority.go index e588f062f..6001200b4 100644 --- a/ca/certificate-authority.go +++ b/ca/certificate-authority.go @@ -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 } diff --git a/ca/certificate-authority_test.go b/ca/certificate-authority_test.go index 081649daf..0bf0ab1a9 100644 --- a/ca/certificate-authority_test.go +++ b/ca/certificate-authority_test.go @@ -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) diff --git a/cmd/admin-revoker/main.go b/cmd/admin-revoker/main.go new file mode 100644 index 000000000..0169e1c05 --- /dev/null +++ b/cmd/admin-revoker/main.go @@ -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") +} diff --git a/cmd/shell.go b/cmd/shell.go index 23d668539..614d10942 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -80,6 +80,11 @@ type Config struct { Tag string } + Revoker struct { + DBDriver string + DBName string + } + Mail struct { Server string Port string diff --git a/core/interfaces.go b/core/interfaces.go index 24ea5c21e..89b4709a8 100644 --- a/core/interfaces.go +++ b/core/interfaces.go @@ -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 diff --git a/ra/registration-authority.go b/ra/registration-authority.go index 0f11129c5..5eb42f822 100644 --- a/ra/registration-authority.go +++ b/ra/registration-authority.go @@ -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 { diff --git a/rpc/rpc-wrappers.go b/rpc/rpc-wrappers.go index 37e5b3bec..094b6729a 100644 --- a/rpc/rpc-wrappers.go +++ b/rpc/rpc-wrappers.go @@ -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 } diff --git a/sa/storage-authority.go b/sa/storage-authority.go index 2fa6a3925..045850eb2 100644 --- a/sa/storage-authority.go +++ b/sa/storage-authority.go @@ -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) diff --git a/sa/storage-authority_test.go b/sa/storage-authority_test.go index 66e76e77b..35a820b95 100644 --- a/sa/storage-authority_test.go +++ b/sa/storage-authority_test.go @@ -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") } diff --git a/test/boulder-config.json b/test/boulder-config.json index 0752a870a..f43b08744 100644 --- a/test/boulder-config.json +++ b/test/boulder-config.json @@ -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",