diff --git a/cmd/bad-key-revoker/main.go b/cmd/bad-key-revoker/main.go new file mode 100644 index 000000000..9a0096f26 --- /dev/null +++ b/cmd/bad-key-revoker/main.go @@ -0,0 +1,431 @@ +package main + +import ( + "bytes" + "context" + "crypto/x509" + "flag" + "fmt" + "html/template" + "io/ioutil" + netmail "net/mail" + "os" + "strings" + "time" + + "github.com/letsencrypt/boulder/cmd" + "github.com/letsencrypt/boulder/core" + corepb "github.com/letsencrypt/boulder/core/proto" + "github.com/letsencrypt/boulder/db" + bgrpc "github.com/letsencrypt/boulder/grpc" + "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/mail" + rapb "github.com/letsencrypt/boulder/ra/proto" + "github.com/letsencrypt/boulder/revocation" + "github.com/letsencrypt/boulder/sa" + "github.com/prometheus/client_golang/prometheus" + + "google.golang.org/grpc" +) + +var keysProcessed = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "bad_keys_processed", + Help: "A counter of blockedKeys rows processed labelled by processing state", +}, []string{"state"}) +var certsRevoked = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "bad_keys_certs_revoked", + Help: "A counter of certificates associated with rows in blockedKeys that have been revoked", +}) +var mailErrors = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "bad_keys_mail_errors", + Help: "A counter of email send errors", +}) + +// revoker is an interface used to reduce the scope of a RA gRPC client +// to only the single method we need to use, this makes testing significantly +// simpler +type revoker interface { + AdministrativelyRevokeCertificate(ctx context.Context, in *rapb.AdministrativelyRevokeCertificateRequest, opts ...grpc.CallOption) (*corepb.Empty, error) +} + +type badKeyRevoker struct { + dbMap *db.WrappedMap + maxRevocations int + serialBatchSize int + raClient revoker + mailer mail.Mailer + emailSubject string + emailTemplate *template.Template + logger log.Logger +} + +// uncheckedBlockedKey represents a row in the blockedKeys table +type uncheckedBlockedKey struct { + KeyHash []byte + RevokedBy int64 +} + +func (bkr *badKeyRevoker) selectUncheckedKey() (uncheckedBlockedKey, error) { + var row uncheckedBlockedKey + err := bkr.dbMap.SelectOne( + &row, + "SELECT keyHash, revokedBy FROM blockedKeys WHERE extantCertificatesChecked = false", + ) + return row, err +} + +// unrevokedCertificate represents a yet to be revoked certificate +type unrevokedCertificate struct { + ID int + Serial string + DER []byte + RegistrationID int64 +} + +// findUnrevoked looks for all unexpired, currently valid certificates which have a specific SPKI hash, +// by looking first at the keyHashToSerial table and then the certificateStatus and certificates tables. +// If the number of certificates it finds is larger than bkr.maxRevocations it'll error out. +func (bkr *badKeyRevoker) findUnrevoked(unchecked uncheckedBlockedKey) ([]unrevokedCertificate, error) { + var unrevokedCerts []unrevokedCertificate + initialID := 0 + for { + var batch []struct { + ID int + CertSerial string + } + _, err := bkr.dbMap.Select( + &batch, + "SELECT id, certserial FROM keyHashToSerial WHERE keyHash = ? AND id > ? ORDER BY id LIMIT ?", + unchecked.KeyHash, + initialID, + bkr.serialBatchSize, + ) + if err != nil { + return nil, err + } + if len(batch) == 0 { + break + } + initialID = batch[len(batch)-1].ID + for _, serial := range batch { + var unrevokedCert unrevokedCertificate + err = bkr.dbMap.SelectOne( + &unrevokedCert, + `SELECT cs.id, cs.serial, c.registrationID, c.der + FROM certificateStatus AS cs + JOIN certificates AS c + ON cs.serial = c.serial + WHERE cs.serial = ? AND cs.isExpired = false AND cs.status != ?`, + serial.CertSerial, + string(core.StatusRevoked), + ) + if err != nil { + if db.IsNoRows(err) { + continue + } + return nil, err + } + unrevokedCerts = append(unrevokedCerts, unrevokedCert) + } + } + if len(unrevokedCerts) > bkr.maxRevocations { + return nil, fmt.Errorf("too many certificates to revoke associated with %x: got %d, max %d", unchecked.KeyHash, len(unrevokedCerts), bkr.maxRevocations) + } + return unrevokedCerts, nil +} + +// markRowChecked updates a row in the blockedKeys table to mark a keyHash +// as having been checked for extant unrevoked certificates. +func (bkr *badKeyRevoker) markRowChecked(unchecked uncheckedBlockedKey) error { + _, err := bkr.dbMap.Exec("UPDATE blockedKeys SET extantCertificatesChecked = true WHERE keyHash = ?", unchecked.KeyHash) + return err +} + +// resolveContacts builds a map of id -> email addresses +func (bkr *badKeyRevoker) resolveContacts(ids []int64) (map[int64][]string, error) { + idToEmail := map[int64][]string{} + for _, id := range ids { + var emails struct { + Contact []string + } + err := bkr.dbMap.SelectOne(&emails, "SELECT contact FROM registrations WHERE id = ?", id) + if err != nil { + return nil, err + } + if len(emails.Contact) != 0 { + for _, email := range emails.Contact { + idToEmail[id] = append(idToEmail[id], strings.TrimPrefix(email, "mailto:")) + } + } + } + return idToEmail, nil +} + +var maxSerials = 100 + +// sendMessage sends a single email to the provided address with the revoked +// serials +func (bkr *badKeyRevoker) sendMessage(addr string, serials []string) error { + err := bkr.mailer.Connect() + if err != nil { + return err + } + defer func() { + _ = bkr.mailer.Close() + }() + mutSerials := make([]string, len(serials)) + copy(mutSerials, serials) + if len(mutSerials) > maxSerials { + more := len(mutSerials) - maxSerials + mutSerials = mutSerials[:maxSerials] + mutSerials = append(mutSerials, fmt.Sprintf("and %d more certificates.", more)) + } + message := bytes.NewBuffer(nil) + err = bkr.emailTemplate.Execute(message, mutSerials) + if err != nil { + return err + } + err = bkr.mailer.SendMail([]string{addr}, bkr.emailSubject, message.String()) + if err != nil { + return err + } + return nil +} + +var keyCompromiseCode = int64(revocation.KeyCompromise) +var revokerName = "bad-key-revoker" + +// revokeCerts revokes all the certificates associated with a particular key hash and sends +// emails to the users that issued the certificates. Emails are not sent to the user which +// requested revocation of the original certificate which marked the key as compromised. +func (bkr *badKeyRevoker) revokeCerts(revokerEmails []string, emailToCerts map[string][]unrevokedCertificate) error { + revokerEmailsMap := map[string]bool{} + for _, email := range revokerEmails { + revokerEmailsMap[email] = true + } + alreadyRevoked := map[int]bool{} + for email, certs := range emailToCerts { + var revokedSerials []string + for _, cert := range certs { + revokedSerials = append(revokedSerials, cert.Serial) + if alreadyRevoked[cert.ID] { + continue + } + _, err := bkr.raClient.AdministrativelyRevokeCertificate(context.Background(), &rapb.AdministrativelyRevokeCertificateRequest{ + Cert: cert.DER, + Code: &keyCompromiseCode, + AdminName: &revokerName, + }) + if err != nil { + return err + } + certsRevoked.Inc() + alreadyRevoked[cert.ID] = true + } + // don't send emails to the person who revoked the certificate + if revokerEmailsMap[email] || email == "" { + continue + } + err := bkr.sendMessage(email, revokedSerials) + if err != nil { + mailErrors.Inc() + bkr.logger.Errf("failed to send message to %q: %s", email, err) + continue + } + } + return nil +} + +// invoke processes a single key in the blockedKeys table and returns whether +// there were any rows to process or not. +func (bkr *badKeyRevoker) invoke() (bool, error) { + // select a row to process + unchecked, err := bkr.selectUncheckedKey() + if err != nil { + if db.IsNoRows(err) { + return true, nil + } + return false, err + } + + // select all unrevoked, unexpired serials associated with the blocked key hash + unrevokedCerts, err := bkr.findUnrevoked(unchecked) + if err != nil { + return false, err + } + if len(unrevokedCerts) == 0 { + // mark row as checked + err = bkr.markRowChecked(unchecked) + if err != nil { + return false, err + } + return false, nil + } + + // build a map of registration ID -> certificates, and collect a + // list of unique registration IDs + ownedBy := map[int64][]unrevokedCertificate{} + var ids []int64 + for _, cert := range unrevokedCerts { + if ownedBy[cert.RegistrationID] == nil { + ids = append(ids, cert.RegistrationID) + } + ownedBy[cert.RegistrationID] = append(ownedBy[cert.RegistrationID], cert) + } + // get contact addresses for the list of IDs + idToEmails, err := bkr.resolveContacts(ids) + if err != nil { + return false, err + } + + // build a map of email -> certificates, this de-duplicates accounts with + // the same email addresses + emailsToCerts := map[string][]unrevokedCertificate{} + for id, emails := range idToEmails { + for _, email := range emails { + emailsToCerts[email] = append(emailsToCerts[email], ownedBy[id]...) + } + } + + // revoke each certificate and send emails to their owners + err = bkr.revokeCerts(idToEmails[unchecked.RevokedBy], emailsToCerts) + if err != nil { + return false, err + } + + // mark the key as checked + err = bkr.markRowChecked(unchecked) + if err != nil { + return false, err + } + return false, nil +} + +func main() { + var config struct { + BadKeyRevoker struct { + cmd.DBConfig + DebugAddr string + + TLS cmd.TLSConfig + RAService *cmd.GRPCClientConfig + + // MaximumRevocations specifies the maximum number of certificates associated with + // a key hash that bad-key-revoker will attempt to revoke. If the number of certificates + // is higher than MaximumRevocations bad-key-revoker will error out and refuse to + // progress until this is addressed. + MaximumRevocations int + // FindCertificatesBatchSize specifies the maximum number of serials to select from the + // keyHashToSerial table at once + FindCertificatesBatchSize int + + // Interval specifies how long bad-key-revoker should sleep between attempting to find + // blockedKeys rows to process when there is no work to do + Interval cmd.ConfigDuration + + Mailer struct { + cmd.SMTPConfig + // Path to a file containing a list of trusted root certificates for use + // during the SMTP connection (as opposed to the gRPC connections). + SMTPTrustedRootFile string + + From string + EmailSubject string + EmailTemplate string + } + } + + Syslog cmd.SyslogConfig + } + configPath := flag.String("config", "", "File path to the configuration file for this service") + flag.Parse() + + if *configPath == "" { + flag.Usage() + os.Exit(1) + } + err := cmd.ReadConfigFile(*configPath, &config) + cmd.FailOnError(err, "Failed reading config file") + + scope, logger := cmd.StatsAndLogging(config.Syslog, config.BadKeyRevoker.DebugAddr) + clk := cmd.Clock() + + scope.MustRegister(keysProcessed) + scope.MustRegister(certsRevoked) + scope.MustRegister(mailErrors) + + dbURL, err := config.BadKeyRevoker.DBConfig.URL() + cmd.FailOnError(err, "Couldn't load DB URL") + dbMap, err := sa.NewDbMap(dbURL, config.BadKeyRevoker.DBConfig.MaxDBConns) + cmd.FailOnError(err, "Could not connect to database") + sa.SetSQLDebug(dbMap, logger) + sa.InitDBMetrics(dbMap, scope) + + tlsConfig, err := config.BadKeyRevoker.TLS.Load() + cmd.FailOnError(err, "TLS config") + + clientMetrics := bgrpc.NewClientMetrics(scope) + conn, err := bgrpc.ClientSetup(config.BadKeyRevoker.RAService, tlsConfig, clientMetrics, clk) + cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to RA") + rac := rapb.NewRegistrationAuthorityClient(conn) + + var smtpRoots *x509.CertPool + if config.BadKeyRevoker.Mailer.SMTPTrustedRootFile != "" { + pem, err := ioutil.ReadFile(config.BadKeyRevoker.Mailer.SMTPTrustedRootFile) + cmd.FailOnError(err, "Loading trusted roots file") + smtpRoots = x509.NewCertPool() + if !smtpRoots.AppendCertsFromPEM(pem) { + cmd.FailOnError(nil, "Failed to parse root certs PEM") + } + } + + fromAddress, err := netmail.ParseAddress(config.BadKeyRevoker.Mailer.From) + cmd.FailOnError(err, fmt.Sprintf("Could not parse from address: %s", config.BadKeyRevoker.Mailer.From)) + + smtpPassword, err := config.BadKeyRevoker.Mailer.PasswordConfig.Pass() + cmd.FailOnError(err, "Failed to load SMTP password") + mailClient := mail.New( + config.BadKeyRevoker.Mailer.Server, + config.BadKeyRevoker.Mailer.Port, + config.BadKeyRevoker.Mailer.Username, + smtpPassword, + smtpRoots, + *fromAddress, + logger, + scope, + 1*time.Second, // reconnection base backoff + 5*60*time.Second, // reconnection maximum backoff + ) + + if config.BadKeyRevoker.Mailer.EmailSubject == "" { + cmd.Fail("BadKeyRevoker.Mailer.EmailSubject must be populated") + } + templateBytes, err := ioutil.ReadFile(config.BadKeyRevoker.Mailer.EmailTemplate) + cmd.FailOnError(err, fmt.Sprintf("failed to read email template %q: %s", config.BadKeyRevoker.Mailer.EmailTemplate, err)) + emailTemplate, err := template.New("email").Parse(string(templateBytes)) + cmd.FailOnError(err, fmt.Sprintf("failed to parse email template %q: %s", config.BadKeyRevoker.Mailer.EmailTemplate, err)) + + bkr := &badKeyRevoker{ + dbMap: dbMap, + maxRevocations: config.BadKeyRevoker.MaximumRevocations, + serialBatchSize: config.BadKeyRevoker.FindCertificatesBatchSize, + raClient: rac, + mailer: mailClient, + emailSubject: config.BadKeyRevoker.Mailer.EmailSubject, + emailTemplate: emailTemplate, + logger: logger, + } + for { + noWork, err := bkr.invoke() + if err != nil { + keysProcessed.WithLabelValues("error").Inc() + logger.Errf("failed to process blockedKeys row: %s", err) + continue + } + if noWork { + time.Sleep(config.BadKeyRevoker.Interval.Duration) + } else { + keysProcessed.WithLabelValues("success").Inc() + } + } +} diff --git a/cmd/bad-key-revoker/main_test.go b/cmd/bad-key-revoker/main_test.go new file mode 100644 index 000000000..d1bcc7a8e --- /dev/null +++ b/cmd/bad-key-revoker/main_test.go @@ -0,0 +1,295 @@ +package main + +import ( + "context" + "crypto/rand" + "fmt" + "html/template" + "os" + "strings" + "sync" + "testing" + "time" + + "github.com/letsencrypt/boulder/core" + corepb "github.com/letsencrypt/boulder/core/proto" + "github.com/letsencrypt/boulder/db" + "github.com/letsencrypt/boulder/mocks" + rapb "github.com/letsencrypt/boulder/ra/proto" + "github.com/letsencrypt/boulder/sa" + "github.com/letsencrypt/boulder/test" + "github.com/letsencrypt/boulder/test/vars" + "google.golang.org/grpc" +) + +func TestMain(m *testing.M) { + if !strings.HasSuffix(os.Getenv("BOULDER_CONFIG_DIR"), "config-next") { + os.Exit(0) + } + os.Exit(m.Run()) +} + +func randHash(t *testing.T) []byte { + t.Helper() + h := make([]byte, 32) + _, err := rand.Read(h) + test.AssertNotError(t, err, "failed to read rand") + return h +} + +func insertBlockedRow(t *testing.T, dbMap *db.WrappedMap, hash []byte, by int64, checked bool) { + t.Helper() + _, err := dbMap.Exec(`INSERT INTO blockedKeys + (keyHash, added, source, revokedBy, extantCertificatesChecked) + VALUES + (?, ?, ?, ?, ?)`, + hash, + time.Now(), + 1, + by, + checked, + ) + test.AssertNotError(t, err, "failed to add test row") +} + +func TestSelectUncheckedRows(t *testing.T) { + dbMap, err := sa.NewDbMap(vars.DBConnSAFullPerms, 0) + test.AssertNotError(t, err, "failed setting up db client") + defer test.ResetSATestDatabase(t)() + + bkr := &badKeyRevoker{dbMap: dbMap} + + hashA, hashB := randHash(t), randHash(t) + insertBlockedRow(t, dbMap, hashA, 1, true) + row, err := bkr.selectUncheckedKey() + test.AssertError(t, err, "selectUncheckedKey didn't fail with no rows to process") + test.Assert(t, db.IsNoRows(err), "returned error is not sql.ErrNoRows") + insertBlockedRow(t, dbMap, hashB, 1, false) + row, err = bkr.selectUncheckedKey() + test.AssertNotError(t, err, "selectUncheckKey failed") + test.AssertByteEquals(t, row.KeyHash, hashB) + test.AssertEquals(t, row.RevokedBy, int64(1)) +} + +func insertRegistration(t *testing.T, dbMap *db.WrappedMap, addrs ...string) int64 { + t.Helper() + jwkHash := make([]byte, 2) + _, err := rand.Read(jwkHash) + test.AssertNotError(t, err, "failed to read rand") + contactStr := "[]" + if len(addrs) > 0 { + contacts := []string{} + for _, addr := range addrs { + contacts = append(contacts, fmt.Sprintf(`"mailto:%s"`, addr)) + } + contactStr = fmt.Sprintf("[%s]", strings.Join(contacts, ",")) + } + res, err := dbMap.Exec( + "INSERT INTO registrations (jwk, jwk_sha256, contact, agreement, initialIP, createdAt, status, LockCol) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + []byte{}, + fmt.Sprintf("%x", jwkHash), + contactStr, + "yes", + []byte{}, + time.Now(), + string(core.StatusValid), + 0, + ) + test.AssertNotError(t, err, "failed to insert test registrations row") + regID, err := res.LastInsertId() + test.AssertNotError(t, err, "failed to get registration ID") + return regID +} + +func insertCert(t *testing.T, dbMap *db.WrappedMap, keyHash []byte, serial string, regID int64, expired bool, revoked bool) { + t.Helper() + _, err := dbMap.Exec( + "INSERT INTO keyHashToSerial (keyHash, certNotAfter, certSerial) VALUES (?, ?, ?)", + keyHash, + time.Now(), + serial, + ) + test.AssertNotError(t, err, "failed to insert test keyHashToSerial row") + + status := string(core.StatusValid) + if revoked { + status = string(core.StatusRevoked) + } + _, err = dbMap.Exec( + "INSERT INTO certificateStatus (serial, status, isExpired, ocspLAstUpdated, revokedDate, revokedReason, lastExpirationNagSent) VALUES (?, ?, ?, ?, ?, ?, ?)", + serial, + status, + expired, + time.Now(), + time.Time{}, + 0, + time.Time{}, + ) + test.AssertNotError(t, err, "failed to insert test certificateStatus row") + + _, err = dbMap.Exec( + "INSERT INTO certificates (serial, registrationID, der, digest, issued, expires) VALUES (?, ?, ?, ?, ?, ?)", + serial, + regID, + []byte{1, 2, 3}, + []byte{}, + time.Now(), + time.Now(), + ) + test.AssertNotError(t, err, "failed to insert test certificates row") +} + +func TestFindUnrevoked(t *testing.T) { + dbMap, err := sa.NewDbMap(vars.DBConnSAFullPerms, 0) + test.AssertNotError(t, err, "failed setting up db client") + defer test.ResetSATestDatabase(t)() + + regID := insertRegistration(t, dbMap, "") + + bkr := &badKeyRevoker{dbMap: dbMap, serialBatchSize: 1, maxRevocations: 10} + + hashA := randHash(t) + // insert valid, unexpired + insertCert(t, dbMap, hashA, "ff", regID, false, false) + // insert valid, expired + insertCert(t, dbMap, hashA, "ee", regID, true, false) + // insert revoked + insertCert(t, dbMap, hashA, "dd", regID, false, true) + + rows, err := bkr.findUnrevoked(uncheckedBlockedKey{KeyHash: hashA}) + test.AssertNotError(t, err, "findUnrevoked failed") + test.AssertEquals(t, len(rows), 1) + test.AssertEquals(t, rows[0].Serial, "ff") + test.AssertEquals(t, rows[0].RegistrationID, int64(1)) + test.AssertByteEquals(t, rows[0].DER, []byte{1, 2, 3}) + + bkr.maxRevocations = 0 + _, err = bkr.findUnrevoked(uncheckedBlockedKey{KeyHash: hashA}) + test.AssertError(t, err, "findUnrevoked didn't fail with 0 maxRevocations") + test.AssertEquals(t, err.Error(), fmt.Sprintf("too many certificates to revoke associated with %x: got 1, max 0", hashA)) +} + +func TestResolveContacts(t *testing.T) { + dbMap, err := sa.NewDbMap(vars.DBConnSAFullPerms, 0) + test.AssertNotError(t, err, "failed setting up db client") + defer test.ResetSATestDatabase(t)() + + bkr := &badKeyRevoker{dbMap: dbMap} + + regIDA := insertRegistration(t, dbMap, "") + regIDB := insertRegistration(t, dbMap, "example.com", "example-2.com") + regIDC := insertRegistration(t, dbMap, "example.com") + regIDD := insertRegistration(t, dbMap, "example-2.com") + + idToEmail, err := bkr.resolveContacts([]int64{regIDA, regIDB, regIDC, regIDD}) + test.AssertNotError(t, err, "resolveContacts failed") + test.AssertDeepEquals(t, idToEmail, map[int64][]string{ + regIDA: {""}, + regIDB: {"example.com", "example-2.com"}, + regIDC: {"example.com"}, + regIDD: {"example-2.com"}, + }) +} + +var testTemplate = template.Must(template.New("testing").Parse("{{range .}}{{.}}\n{{end}}")) + +func TestSendMessage(t *testing.T) { + mm := &mocks.Mailer{} + bkr := &badKeyRevoker{mailer: mm, emailSubject: "testing", emailTemplate: testTemplate} + + maxSerials = 2 + err := bkr.sendMessage("example.com", []string{"a", "b", "c"}) + test.AssertNotError(t, err, "sendMessages failed") + test.AssertEquals(t, len(mm.Messages), 1) + test.AssertEquals(t, mm.Messages[0].To, "example.com") + test.AssertEquals(t, mm.Messages[0].Subject, bkr.emailSubject) + test.AssertEquals(t, mm.Messages[0].Body, "a\nb\nand 1 more certificates.\n") + +} + +type mockRevoker struct { + revoked int + mu sync.Mutex +} + +func (mr *mockRevoker) AdministrativelyRevokeCertificate(ctx context.Context, in *rapb.AdministrativelyRevokeCertificateRequest, opts ...grpc.CallOption) (*corepb.Empty, error) { + mr.mu.Lock() + defer mr.mu.Unlock() + mr.revoked++ + return nil, nil +} + +func TestRevokeCerts(t *testing.T) { + dbMap, err := sa.NewDbMap(vars.DBConnSAFullPerms, 0) + test.AssertNotError(t, err, "failed setting up db client") + defer test.ResetSATestDatabase(t)() + + mm := &mocks.Mailer{} + mr := &mockRevoker{} + bkr := &badKeyRevoker{dbMap: dbMap, raClient: mr, mailer: mm, emailSubject: "testing", emailTemplate: testTemplate} + + err = bkr.revokeCerts([]string{"revoker@example.com", "revoker-b@example.com"}, map[string][]unrevokedCertificate{ + "revoker@example.com": {{ID: 0, Serial: "ff"}}, + "revoker-b@example.com": {{ID: 0, Serial: "ff"}}, + "other@example.com": {{ID: 1, Serial: "ee"}}, + }) + test.AssertNotError(t, err, "revokeCerts failed") + test.AssertEquals(t, len(mm.Messages), 1) + test.AssertEquals(t, mm.Messages[0].To, "other@example.com") + test.AssertEquals(t, mm.Messages[0].Subject, bkr.emailSubject) + test.AssertEquals(t, mm.Messages[0].Body, "ee\n") +} + +func TestInvoke(t *testing.T) { + dbMap, err := sa.NewDbMap(vars.DBConnSAFullPerms, 0) + test.AssertNotError(t, err, "failed setting up db client") + defer test.ResetSATestDatabase(t)() + + mm := &mocks.Mailer{} + mr := &mockRevoker{} + bkr := &badKeyRevoker{dbMap: dbMap, maxRevocations: 10, serialBatchSize: 1, raClient: mr, mailer: mm, emailSubject: "testing", emailTemplate: testTemplate} + + // populate DB with all the test data + regIDA := insertRegistration(t, dbMap, "example.com") + regIDB := insertRegistration(t, dbMap, "example.com") + regIDC := insertRegistration(t, dbMap, "other.example.com", "uno.example.com") + regIDD := insertRegistration(t, dbMap, "") + hashA := randHash(t) + insertBlockedRow(t, dbMap, hashA, regIDC, false) + insertCert(t, dbMap, hashA, "ff", regIDA, false, false) + insertCert(t, dbMap, hashA, "ee", regIDB, false, false) + insertCert(t, dbMap, hashA, "dd", regIDC, false, false) + insertCert(t, dbMap, hashA, "cc", regIDD, false, false) + + noWork, err := bkr.invoke() + test.AssertNotError(t, err, "invoke failed") + test.AssertEquals(t, noWork, false) + test.AssertEquals(t, mr.revoked, 4) + test.AssertEquals(t, len(mm.Messages), 1) + test.AssertEquals(t, mm.Messages[0].To, "example.com") + + var checked struct { + ExtantCertificatesChecked bool + } + err = dbMap.SelectOne(&checked, "SELECT extantCertificatesChecked FROM blockedKeys WHERE keyHash = ?", hashA) + test.AssertNotError(t, err, "failed to select row from blockedKeys") + test.AssertEquals(t, checked.ExtantCertificatesChecked, true) + + // add a row with no associated valid certificates + hashB := randHash(t) + insertBlockedRow(t, dbMap, hashB, regIDC, false) + insertCert(t, dbMap, hashB, "bb", regIDA, true, true) + + noWork, err = bkr.invoke() + test.AssertNotError(t, err, "invoke failed") + test.AssertEquals(t, noWork, false) + + checked.ExtantCertificatesChecked = false + err = dbMap.SelectOne(&checked, "SELECT extantCertificatesChecked FROM blockedKeys WHERE keyHash = ?", hashB) + test.AssertNotError(t, err, "failed to select row from blockedKeys") + test.AssertEquals(t, checked.ExtantCertificatesChecked, true) + + noWork, err = bkr.invoke() + test.AssertNotError(t, err, "invoke failed") + test.AssertEquals(t, noWork, true) +} diff --git a/features/featureflag_string.go b/features/featureflag_string.go index 6f7aa42e0..1d58f2f55 100644 --- a/features/featureflag_string.go +++ b/features/featureflag_string.go @@ -27,11 +27,12 @@ func _() { _ = x[StoreIssuerInfo-16] _ = x[StoreKeyHashes-17] _ = x[BlockedKeyTable-18] + _ = x[StoreRevokerInfo-19] } -const _FeatureFlag_name = "unusedWriteIssuedNamesPrecertHeadNonceStatusOKRemoveWFE2AccountIDCheckRenewalFirstParallelCheckFailedValidationDeleteUnusedChallengesCAAValidationMethodsCAAAccountURIEnforceMultiVAMultiVAFullResultsMandatoryPOSTAsGETAllowV1RegistrationV1DisableNewValidationsPrecertificateRevocationStripDefaultSchemePortStoreIssuerInfoStoreKeyHashesBlockedKeyTable" +const _FeatureFlag_name = "unusedWriteIssuedNamesPrecertHeadNonceStatusOKRemoveWFE2AccountIDCheckRenewalFirstParallelCheckFailedValidationDeleteUnusedChallengesCAAValidationMethodsCAAAccountURIEnforceMultiVAMultiVAFullResultsMandatoryPOSTAsGETAllowV1RegistrationV1DisableNewValidationsPrecertificateRevocationStripDefaultSchemePortStoreIssuerInfoStoreKeyHashesBlockedKeyTableStoreRevokerInfo" -var _FeatureFlag_index = [...]uint16{0, 6, 29, 46, 65, 82, 111, 133, 153, 166, 180, 198, 216, 235, 258, 282, 304, 319, 333, 348} +var _FeatureFlag_index = [...]uint16{0, 6, 29, 46, 65, 82, 111, 133, 153, 166, 180, 198, 216, 235, 258, 282, 304, 319, 333, 348, 364} func (i FeatureFlag) String() string { if i < 0 || i >= FeatureFlag(len(_FeatureFlag_index)-1) { diff --git a/features/features.go b/features/features.go index 0414ccee3..c1f5d6b9b 100644 --- a/features/features.go +++ b/features/features.go @@ -52,6 +52,10 @@ const ( // BlockedKeyTable enables storage, and checking, of the blockedKeys table in addition // to the blocked key list BlockedKeyTable + // StoreRevokerInfo enables storage of the revoker and a bool indicating if the row + // was checked for extant unrevoked certificates in the blockedKeys table. It should + // only be enabled if BlockedKeyTable is also enabled. + StoreRevokerInfo ) // List of features and their default value, protected by fMu @@ -75,6 +79,7 @@ var features = map[FeatureFlag]bool{ WriteIssuedNamesPrecert: false, StoreKeyHashes: false, BlockedKeyTable: false, + StoreRevokerInfo: false, } var fMu = new(sync.RWMutex) diff --git a/ra/ra.go b/ra/ra.go index c12f7b583..76674fbaa 100644 --- a/ra/ra.go +++ b/ra/ra.go @@ -1687,7 +1687,7 @@ func revokeEvent(state, serial, cn string, names []string, revocationCode revoca // revokeCertificate generates a revoked OCSP response for the given certificate, stores // the revocation information, and purges OCSP request URLs from Akamai. -func (ra *RegistrationAuthorityImpl) revokeCertificate(ctx context.Context, cert x509.Certificate, code revocation.Reason, source string, comment string) error { +func (ra *RegistrationAuthorityImpl) revokeCertificate(ctx context.Context, cert x509.Certificate, code revocation.Reason, revokedBy int64, source string, comment string) error { status := string(core.OCSPStatusRevoked) reason := int32(code) revokedAt := ra.clk.Now().UnixNano() @@ -1726,6 +1726,9 @@ func (ra *RegistrationAuthorityImpl) revokeCertificate(ctx context.Context, cert if comment != "" { req.Comment = &comment } + if features.Enabled(features.StoreRevokerInfo) && revokedBy != 0 { + req.RevokedBy = &revokedBy + } if _, err = ra.SA.AddBlockedKey(ctx, req); err != nil { return err } @@ -1745,7 +1748,7 @@ func (ra *RegistrationAuthorityImpl) revokeCertificate(ctx context.Context, cert // RevokeCertificateWithReg terminates trust in the certificate provided. func (ra *RegistrationAuthorityImpl) RevokeCertificateWithReg(ctx context.Context, cert x509.Certificate, revocationCode revocation.Reason, regID int64) error { serialString := core.SerialToString(cert.SerialNumber) - err := ra.revokeCertificate(ctx, cert, revocationCode, "API", "") + err := ra.revokeCertificate(ctx, cert, revocationCode, regID, "API", "") state := "Failure" defer func() { @@ -1777,7 +1780,7 @@ func (ra *RegistrationAuthorityImpl) AdministrativelyRevokeCertificate(ctx conte serialString := core.SerialToString(cert.SerialNumber) // TODO(#4774): allow setting the comment via the RPC, format should be: // "revoked by %s: %s", user, comment - err := ra.revokeCertificate(ctx, cert, revocationCode, "admin-revoker", fmt.Sprintf("revoked by %s", user)) + err := ra.revokeCertificate(ctx, cert, revocationCode, 0, "admin-revoker", fmt.Sprintf("revoked by %s", user)) state := "Failure" defer func() { diff --git a/sa/_db-next/migrations/20200420135619_AddRowsBlockedKeys.sql b/sa/_db-next/migrations/20200420135619_AddRowsBlockedKeys.sql new file mode 100644 index 000000000..677fe405d --- /dev/null +++ b/sa/_db-next/migrations/20200420135619_AddRowsBlockedKeys.sql @@ -0,0 +1,14 @@ + +-- +goose Up +-- SQL in section 'Up' is executed when this migration is applied + +ALTER TABLE blockedKeys ADD `revokedBy` BIGINT(20) DEFAULT 0; +ALTER TABLE blockedKeys ADD `extantCertificatesChecked` BOOLEAN DEFAULT FALSE; +CREATE INDEX `extantCertificatesChecked_idx` ON blockedKeys (`extantCertificatesChecked`); + +-- +goose Down +-- SQL section 'Down' is executed when this migration is rolled back + +ALTER TABLE blockedKeys DROP `revokedBy`; +ALTER TABLE blockedKeys DROP `extantCertificatesChecked`; +DROP INDEX `extantCertificatesChecked_idx` ON blockedKeys; diff --git a/sa/database.go b/sa/database.go index 139f5840c..0365102b8 100644 --- a/sa/database.go +++ b/sa/database.go @@ -140,5 +140,4 @@ func initTables(dbMap *gorp.DbMap) { dbMap.AddTableWithName(recordedSerialModel{}, "serials").SetKeys(true, "ID") dbMap.AddTableWithName(precertificateModel{}, "precertificates").SetKeys(true, "ID") dbMap.AddTableWithName(keyHashModel{}, "keyHashToSerial").SetKeys(true, "ID") - dbMap.AddTableWithName(blockedKeyModel{}, "blockedKeys").SetKeys(true, "ID") } diff --git a/sa/model.go b/sa/model.go index a97e7ca0a..539b4cd34 100644 --- a/sa/model.go +++ b/sa/model.go @@ -622,11 +622,3 @@ var stringToSourceInt = map[string]int{ "API": 1, "admin-revoker": 2, } - -type blockedKeyModel struct { - ID int64 - KeyHash []byte - Added time.Time - Source int - Comment *string -} diff --git a/sa/proto/sa.pb.go b/sa/proto/sa.pb.go index d4f2181a1..a63a2c224 100644 --- a/sa/proto/sa.pb.go +++ b/sa/proto/sa.pb.go @@ -1809,6 +1809,7 @@ type AddBlockedKeyRequest struct { Added *int64 `protobuf:"varint,2,opt,name=added" json:"added,omitempty"` Source *string `protobuf:"bytes,3,opt,name=source" json:"source,omitempty"` Comment *string `protobuf:"bytes,4,opt,name=comment" json:"comment,omitempty"` + RevokedBy *int64 `protobuf:"varint,5,opt,name=revokedBy" json:"revokedBy,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -1867,6 +1868,13 @@ func (m *AddBlockedKeyRequest) GetComment() string { return "" } +func (m *AddBlockedKeyRequest) GetRevokedBy() int64 { + if m != nil && m.RevokedBy != nil { + return *m.RevokedBy + } + return 0 +} + type KeyBlockedRequest struct { KeyHash []byte `protobuf:"bytes,1,opt,name=keyHash" json:"keyHash,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` @@ -1950,122 +1958,123 @@ func init() { func init() { proto.RegisterFile("sa/proto/sa.proto", fileDescriptor_099fb35e782a48a6) } var fileDescriptor_099fb35e782a48a6 = []byte{ - // 1830 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x58, 0xeb, 0x72, 0x1b, 0xb7, - 0x15, 0xe6, 0xc5, 0x94, 0xc9, 0xa3, 0x2b, 0x61, 0x99, 0xdd, 0xd0, 0xb2, 0x4d, 0x23, 0x8e, 0x47, - 0x99, 0x4e, 0x15, 0x67, 0x9b, 0x49, 0x32, 0xa3, 0xd6, 0x89, 0x14, 0xca, 0xb2, 0x62, 0x47, 0x66, - 0x96, 0xb5, 0xda, 0xe9, 0xf4, 0xcf, 0x86, 0x8b, 0xd0, 0x5b, 0x53, 0xbb, 0x0c, 0x00, 0x4a, 0xa6, - 0x7e, 0x77, 0xa6, 0x79, 0x82, 0x4e, 0x7f, 0xf6, 0x39, 0xfa, 0x12, 0x7d, 0xa5, 0x0e, 0x0e, 0xb0, - 0x57, 0xee, 0x52, 0xd5, 0xb4, 0xd3, 0x7f, 0x7b, 0x0e, 0xce, 0x0d, 0xc0, 0xb9, 0x7c, 0x58, 0x68, - 0x0b, 0xf7, 0x93, 0x29, 0x0f, 0x65, 0xf8, 0x89, 0x70, 0xf7, 0xf0, 0x83, 0xd4, 0x84, 0xdb, 0xbd, - 0x3b, 0x0a, 0x39, 0x33, 0x0b, 0xea, 0x53, 0x2f, 0xd1, 0x1e, 0x6c, 0x38, 0x6c, 0xec, 0x0b, 0xc9, - 0x5d, 0xe9, 0x87, 0xc1, 0x49, 0x9f, 0x6c, 0x40, 0xcd, 0xf7, 0xac, 0x6a, 0xaf, 0xba, 0x5b, 0x77, - 0x6a, 0xbe, 0x47, 0x1f, 0x00, 0x7c, 0x3b, 0x7c, 0x7d, 0xfa, 0x7b, 0xf6, 0xc3, 0x4b, 0x36, 0x27, - 0x5b, 0x50, 0xff, 0xf3, 0xe5, 0x3b, 0x5c, 0x5e, 0x73, 0xd4, 0x27, 0x7d, 0x04, 0x9b, 0x07, 0x33, - 0xf9, 0x36, 0xe4, 0xfe, 0xd5, 0xa2, 0x89, 0x16, 0x9a, 0xf8, 0x67, 0x15, 0x1e, 0x1c, 0x33, 0x39, - 0x60, 0x81, 0xe7, 0x07, 0xe3, 0x8c, 0xb4, 0xc3, 0x7e, 0x9a, 0x31, 0x21, 0xc9, 0x13, 0xd8, 0xe0, - 0x99, 0x38, 0x4c, 0x04, 0x39, 0xae, 0x92, 0xf3, 0x3d, 0x16, 0x48, 0xff, 0x47, 0x9f, 0xf1, 0xdf, - 0xcd, 0xa7, 0xcc, 0xaa, 0xa1, 0x9b, 0x1c, 0x97, 0xec, 0xc2, 0x66, 0xc2, 0x39, 0x73, 0x27, 0x33, - 0x66, 0xd5, 0x51, 0x30, 0xcf, 0x26, 0x0f, 0x00, 0x2e, 0xdc, 0x89, 0xef, 0xbd, 0x09, 0xa4, 0x3f, - 0xb1, 0x6e, 0xa1, 0xd7, 0x14, 0x87, 0x0a, 0xb8, 0x7f, 0xcc, 0xe4, 0x99, 0x62, 0x64, 0x22, 0x17, - 0x37, 0x0d, 0xdd, 0x82, 0xdb, 0x5e, 0x78, 0xee, 0xfa, 0x81, 0xb0, 0x6a, 0xbd, 0xfa, 0x6e, 0xcb, - 0x89, 0x48, 0x75, 0xa8, 0x41, 0x78, 0x89, 0x01, 0xd6, 0x1d, 0xf5, 0x49, 0xff, 0x51, 0x85, 0x3b, - 0x05, 0x2e, 0xc9, 0x97, 0xd0, 0xc0, 0xd0, 0xac, 0x6a, 0xaf, 0xbe, 0xbb, 0x6a, 0xd3, 0x3d, 0xe1, - 0xee, 0x15, 0xc8, 0xed, 0x7d, 0xe7, 0x4e, 0x8f, 0x26, 0xec, 0x9c, 0x05, 0xd2, 0xd1, 0x0a, 0xdd, - 0xd7, 0x00, 0x09, 0x93, 0x74, 0x60, 0x45, 0x3b, 0x37, 0xb7, 0x64, 0x28, 0xf2, 0x31, 0x34, 0xdc, - 0x99, 0x7c, 0x7b, 0x85, 0xa7, 0xba, 0x6a, 0xdf, 0xd9, 0xc3, 0x54, 0xc9, 0xde, 0x98, 0x96, 0xa0, - 0xff, 0xaa, 0x41, 0xfb, 0x1b, 0xc6, 0xd5, 0x51, 0x8e, 0x5c, 0xc9, 0x86, 0xd2, 0x95, 0x33, 0xa1, - 0x0c, 0x0b, 0xc6, 0x7d, 0x77, 0x12, 0x19, 0xd6, 0x14, 0xf2, 0x51, 0xc2, 0x5c, 0x83, 0xa1, 0xd4, - 0x3d, 0x85, 0x23, 0x31, 0x7d, 0xe5, 0x0a, 0xf9, 0x66, 0xea, 0xb9, 0x92, 0x79, 0xe6, 0x0a, 0xf2, - 0x6c, 0xd2, 0x83, 0x55, 0xce, 0x2e, 0xc2, 0x77, 0xcc, 0xeb, 0xbb, 0x92, 0x59, 0x0d, 0x94, 0x4a, - 0xb3, 0xc8, 0x63, 0x58, 0x37, 0xa4, 0xc3, 0x5c, 0x11, 0x06, 0xd6, 0x0a, 0xca, 0x64, 0x99, 0xe4, - 0x33, 0xb8, 0x3b, 0x71, 0x85, 0x3c, 0x7a, 0x3f, 0xf5, 0xf5, 0xd5, 0x9c, 0xba, 0xe3, 0x21, 0x0b, - 0xa4, 0x75, 0x1b, 0xa5, 0x8b, 0x17, 0x09, 0x85, 0x35, 0x15, 0x90, 0xc3, 0xc4, 0x34, 0x0c, 0x04, - 0xb3, 0x9a, 0x58, 0x00, 0x19, 0x1e, 0xe9, 0x42, 0x33, 0x08, 0xe5, 0xc1, 0x8f, 0x92, 0x71, 0xab, - 0x85, 0xc6, 0x62, 0x9a, 0xec, 0x40, 0xcb, 0x17, 0x68, 0x96, 0x79, 0x16, 0xf4, 0xaa, 0xbb, 0x4d, - 0x27, 0x61, 0x7c, 0x7b, 0xab, 0x59, 0xdb, 0xaa, 0xd3, 0x1e, 0xac, 0x0c, 0x93, 0xd3, 0x2a, 0x38, - 0x45, 0xba, 0x0f, 0x0d, 0xc7, 0x0d, 0xc6, 0xe8, 0x8a, 0xb9, 0x7c, 0xe2, 0x33, 0x21, 0x4d, 0xb6, - 0xc5, 0xb4, 0x52, 0x9e, 0xb8, 0x52, 0xad, 0xd4, 0x70, 0xc5, 0x50, 0xf4, 0x3e, 0x34, 0xbe, 0x09, - 0x67, 0x81, 0x24, 0xdb, 0xd0, 0x18, 0xa9, 0x0f, 0xa3, 0xa9, 0x09, 0xfa, 0x07, 0x78, 0x88, 0xcb, - 0xa9, 0x3b, 0x15, 0x87, 0xf3, 0x53, 0xf7, 0x9c, 0xc5, 0x99, 0xfe, 0x10, 0x1a, 0x5c, 0xb9, 0x47, - 0xc5, 0x55, 0xbb, 0xa5, 0xb2, 0x0f, 0xe3, 0x71, 0x34, 0x5f, 0x59, 0x0e, 0x94, 0x82, 0x49, 0x70, - 0x4d, 0xd0, 0xbf, 0x56, 0x61, 0x0d, 0x4d, 0x1b, 0x73, 0xe4, 0x2b, 0x58, 0x1b, 0xa5, 0x68, 0x93, - 0xcc, 0xf7, 0x94, 0xb9, 0xb4, 0x5c, 0x3a, 0x8b, 0x33, 0x0a, 0xdd, 0xcf, 0x33, 0xc9, 0x4c, 0xe0, - 0x96, 0x72, 0x64, 0xce, 0x0a, 0xbf, 0x93, 0x3d, 0xd6, 0xd2, 0x7b, 0x1c, 0xc0, 0x7d, 0x74, 0x90, - 0x6e, 0x79, 0xe2, 0x70, 0x7e, 0x32, 0x88, 0x76, 0xa8, 0x3a, 0xd7, 0xd4, 0x74, 0xb7, 0x9a, 0x3f, - 0x4d, 0x76, 0x5c, 0x2b, 0xde, 0x31, 0xfd, 0xb9, 0x0a, 0x8f, 0xd0, 0xe4, 0x49, 0x70, 0xf1, 0xdf, - 0xb7, 0x88, 0x2e, 0x34, 0xdf, 0x86, 0x42, 0xe2, 0x6e, 0x74, 0x5f, 0x8b, 0xe9, 0x24, 0x94, 0x7a, - 0x49, 0x28, 0x43, 0x20, 0x18, 0xc9, 0x6b, 0xee, 0x31, 0x1e, 0xbb, 0xde, 0x81, 0x96, 0x3b, 0xc2, - 0xdd, 0xc7, 0x5e, 0x13, 0xc6, 0xf5, 0xfb, 0x7b, 0x01, 0xdb, 0x68, 0xf4, 0xf9, 0xf7, 0xfd, 0xd3, - 0x21, 0x93, 0xb1, 0xd9, 0x0e, 0xac, 0x5c, 0xfa, 0x81, 0x17, 0x5e, 0x1a, 0x9b, 0x86, 0x2a, 0x6f, - 0x72, 0xf4, 0x29, 0x6c, 0x1b, 0x23, 0x47, 0xef, 0x7d, 0x91, 0x58, 0x4a, 0x69, 0x54, 0xb3, 0x1a, - 0x03, 0xe8, 0x0d, 0x38, 0xbb, 0xf0, 0xc3, 0x99, 0x48, 0x25, 0x65, 0x56, 0xbb, 0xac, 0x91, 0x6d, - 0x43, 0x83, 0xb3, 0xf1, 0x49, 0x3f, 0xba, 0x7f, 0x24, 0x54, 0x85, 0x69, 0x75, 0xa5, 0xc7, 0xf0, - 0x0b, 0xf5, 0x9a, 0x8e, 0xa1, 0xa8, 0x84, 0xad, 0x03, 0xcf, 0xd3, 0x65, 0x18, 0xf9, 0x88, 0x6d, - 0x55, 0x53, 0xb6, 0x52, 0x35, 0x5a, 0xcb, 0x74, 0x3a, 0x0b, 0x6e, 0x8f, 0x38, 0xc3, 0x4e, 0xa6, - 0x1b, 0x7a, 0x44, 0xaa, 0x15, 0x86, 0x05, 0x2f, 0x4c, 0x8f, 0x8b, 0x48, 0x55, 0x21, 0x77, 0x0f, - 0x3c, 0x2f, 0xb5, 0xcb, 0xc8, 0xf7, 0x16, 0xd4, 0x3d, 0xc6, 0xa3, 0x79, 0xeb, 0x31, 0x5e, 0xbc, - 0x33, 0x55, 0x03, 0xaa, 0x17, 0xa1, 0xcb, 0x35, 0x07, 0xbf, 0x55, 0x84, 0xbe, 0x10, 0xb3, 0xb8, - 0xa5, 0x1a, 0x4a, 0x65, 0x19, 0x7e, 0xf1, 0x93, 0xbe, 0x69, 0xa3, 0x31, 0x4d, 0x9f, 0x42, 0x27, - 0x1f, 0x88, 0xe9, 0x6e, 0xea, 0xa4, 0xfd, 0x71, 0xd4, 0x70, 0xd4, 0x49, 0x23, 0x45, 0x07, 0xb0, - 0x86, 0x19, 0x97, 0x2e, 0xa1, 0x14, 0x7e, 0x20, 0x4f, 0xe1, 0xce, 0x4c, 0xb0, 0x33, 0x3b, 0x5b, - 0x19, 0x18, 0x7d, 0xd3, 0x29, 0x5a, 0xa2, 0xaf, 0x80, 0x46, 0x13, 0x17, 0x2d, 0x17, 0xd7, 0x54, - 0xde, 0x4f, 0x07, 0x56, 0xdc, 0xd1, 0x48, 0xc6, 0x07, 0x63, 0x28, 0x3a, 0x87, 0x5f, 0x1c, 0x33, - 0x5d, 0x14, 0xcf, 0x43, 0x9e, 0xe9, 0x67, 0x89, 0x4a, 0x35, 0xad, 0x52, 0xdc, 0xc6, 0xca, 0x36, - 0x52, 0x2f, 0xdf, 0xc8, 0xdf, 0xab, 0x60, 0x1d, 0x33, 0xf9, 0x7f, 0x83, 0x0d, 0x6a, 0x9a, 0x72, - 0xf6, 0xd3, 0xcc, 0xe7, 0x26, 0x96, 0x2b, 0x9d, 0x69, 0x4d, 0x27, 0xcf, 0xa6, 0x7f, 0xab, 0xc2, - 0x46, 0x0e, 0x5b, 0xfc, 0x3a, 0x9a, 0xfd, 0xba, 0x1d, 0xdf, 0x57, 0xbd, 0x60, 0x09, 0xac, 0x40, - 0xd9, 0xff, 0x3d, 0xac, 0x78, 0x05, 0x0f, 0x0f, 0x3c, 0xaf, 0x08, 0x2a, 0xc6, 0x27, 0xf7, 0x71, - 0x36, 0xd0, 0x65, 0xd6, 0x1e, 0xc3, 0x56, 0x0e, 0x9c, 0xe2, 0xb1, 0xf9, 0x5e, 0xd4, 0x6c, 0xd4, - 0x27, 0xa5, 0x0b, 0x52, 0xf6, 0x02, 0x0c, 0xfe, 0x08, 0xda, 0x19, 0x19, 0x3b, 0x67, 0xaa, 0xae, - 0x4d, 0x5d, 0x81, 0xe5, 0x20, 0xdc, 0x28, 0xa8, 0xe5, 0x25, 0xd8, 0x88, 0x6b, 0xc0, 0x62, 0x32, - 0x57, 0x53, 0xaa, 0xa6, 0x15, 0xf4, 0x31, 0x17, 0x8c, 0xdf, 0xaa, 0x76, 0x79, 0x84, 0x41, 0x6e, - 0x61, 0xad, 0xc7, 0x34, 0xfd, 0x4b, 0x0d, 0x76, 0x9e, 0xfb, 0x81, 0x3b, 0xf1, 0xaf, 0x58, 0x21, - 0xc8, 0x2e, 0x28, 0x19, 0x03, 0xca, 0x6a, 0x19, 0x50, 0x96, 0x6a, 0x54, 0xf5, 0x4c, 0xa3, 0xc2, - 0x69, 0x22, 0x25, 0x3b, 0x9f, 0x46, 0x40, 0xad, 0xe5, 0x24, 0x0c, 0xd2, 0x87, 0x36, 0x0e, 0x41, - 0xe3, 0x74, 0x14, 0x72, 0x4f, 0x58, 0x0d, 0xbc, 0xa4, 0x8e, 0xbe, 0xa4, 0xb3, 0xdc, 0xb2, 0xb3, - 0xa8, 0x40, 0x9e, 0xc1, 0x66, 0xc2, 0x3c, 0xe2, 0x3c, 0xe4, 0x08, 0xe4, 0x56, 0xed, 0x6d, 0x6d, - 0x63, 0xc0, 0xc3, 0x1f, 0x26, 0xec, 0xbc, 0xcf, 0xa4, 0xeb, 0x4f, 0x84, 0x93, 0x17, 0xa6, 0xef, - 0x61, 0xfb, 0xc0, 0xf3, 0x0e, 0x27, 0xe1, 0xe8, 0x1d, 0xf3, 0x5e, 0xb2, 0x79, 0x6a, 0xd0, 0xbc, - 0x63, 0xf3, 0x17, 0xae, 0x78, 0x6b, 0xda, 0x69, 0x44, 0xaa, 0x7a, 0x77, 0x3d, 0x8f, 0x79, 0x51, - 0x4b, 0x45, 0x02, 0x4f, 0x27, 0x9c, 0xf1, 0x11, 0x8b, 0x21, 0x2b, 0x52, 0xd8, 0xe0, 0xc3, 0x73, - 0x95, 0xef, 0xe6, 0x04, 0x22, 0x92, 0xfe, 0x0a, 0xda, 0x2f, 0xd9, 0xdc, 0x78, 0xbe, 0xd6, 0xad, - 0xfd, 0xf3, 0x1d, 0xd8, 0x1a, 0xca, 0x90, 0xbb, 0xe3, 0xe8, 0xba, 0xe4, 0x9c, 0xec, 0xc3, 0xe6, - 0x31, 0xcb, 0x00, 0x14, 0x42, 0x70, 0x2a, 0x67, 0xba, 0x42, 0x97, 0xe8, 0xb3, 0x48, 0x73, 0x69, - 0x85, 0xfc, 0x06, 0xb6, 0x73, 0xca, 0x87, 0x73, 0xf5, 0x6a, 0xdb, 0x50, 0x16, 0x92, 0x57, 0x5c, - 0x89, 0xf6, 0xa7, 0xb0, 0x71, 0xcc, 0xd2, 0xf8, 0x8f, 0x80, 0xd2, 0xd3, 0xc3, 0xb0, 0xdb, 0xd6, - 0x3a, 0xa9, 0x65, 0x5a, 0x21, 0x9f, 0x41, 0x5b, 0x3d, 0xec, 0x38, 0x1b, 0xdd, 0x44, 0x6b, 0x1f, - 0xc3, 0x5c, 0x7c, 0x3c, 0xa4, 0x15, 0xef, 0x22, 0x1a, 0xcc, 0x8b, 0xd0, 0x0a, 0x19, 0x82, 0x55, - 0x86, 0x53, 0xc9, 0x87, 0x31, 0x84, 0x2c, 0x47, 0xb1, 0xdd, 0xad, 0x3c, 0xce, 0xa4, 0x15, 0xf2, - 0x02, 0x3a, 0xc5, 0xc0, 0x90, 0x3c, 0x8a, 0xa5, 0xcb, 0x40, 0x63, 0xb7, 0x15, 0x8b, 0xd0, 0x0a, - 0xf9, 0x0e, 0xee, 0x95, 0x48, 0x23, 0x42, 0xbe, 0xa9, 0x39, 0x1b, 0x56, 0x53, 0xa0, 0x8e, 0x74, - 0xe2, 0xb5, 0x0c, 0xca, 0xcb, 0xea, 0x7c, 0x0e, 0xeb, 0x19, 0xcc, 0x46, 0xac, 0x78, 0x35, 0x07, - 0xe3, 0xb2, 0x7a, 0x5f, 0xc0, 0x7a, 0x06, 0xa1, 0x69, 0xbd, 0x22, 0xd0, 0xd6, 0xc5, 0x9b, 0xd2, - 0x2c, 0x5a, 0x21, 0xaf, 0xe1, 0x83, 0x52, 0xa0, 0x46, 0x1e, 0x2b, 0xd1, 0xeb, 0x70, 0x5c, 0xce, - 0xe0, 0xd7, 0x98, 0x56, 0xd9, 0x7e, 0x4b, 0xb6, 0x17, 0x06, 0xd2, 0x49, 0xdf, 0xee, 0x16, 0x75, - 0x7f, 0xbc, 0x50, 0xb2, 0x30, 0x79, 0x6d, 0xb2, 0xa3, 0x4c, 0x94, 0x4d, 0xe4, 0x2e, 0x59, 0x9c, - 0x78, 0xb4, 0x42, 0xde, 0xe0, 0x0c, 0x2f, 0x1a, 0x48, 0x36, 0xa1, 0xc6, 0xde, 0x92, 0x3f, 0x1b, - 0x65, 0x01, 0x3e, 0x33, 0x79, 0x52, 0x38, 0xe9, 0xec, 0xc2, 0x9a, 0xcf, 0x5c, 0xd6, 0x9f, 0x60, - 0x67, 0x09, 0x48, 0xb2, 0xc9, 0x13, 0x13, 0xda, 0x35, 0x30, 0xaa, 0x64, 0xd3, 0xdf, 0x9b, 0xe8, - 0x0a, 0x5f, 0x35, 0x36, 0xf9, 0x28, 0x8e, 0x64, 0xd9, 0xb3, 0x27, 0x1b, 0xb0, 0x83, 0x38, 0xec, - 0xac, 0xc8, 0xdc, 0xa3, 0x74, 0xac, 0x37, 0x09, 0xf3, 0x53, 0x80, 0xa4, 0xe1, 0x12, 0x6c, 0x19, - 0x0b, 0x0d, 0x38, 0x97, 0x5a, 0xfb, 0xb0, 0x79, 0xca, 0x2e, 0x73, 0xfd, 0x75, 0xa1, 0x1b, 0x96, - 0x74, 0xc8, 0x2f, 0x80, 0xe8, 0xdf, 0x11, 0xd7, 0xea, 0xaf, 0x6a, 0xde, 0xd1, 0xf9, 0x54, 0xce, - 0x69, 0x85, 0x9c, 0xc0, 0x46, 0x16, 0x56, 0x93, 0x0f, 0x70, 0x43, 0x45, 0x98, 0xbf, 0xdb, 0x2d, - 0x5a, 0x32, 0x33, 0xbe, 0x42, 0x7e, 0x0b, 0x6d, 0x05, 0x90, 0xb2, 0x2d, 0x77, 0x89, 0xb5, 0x5c, - 0x24, 0x4f, 0xa1, 0x15, 0x3f, 0x70, 0x4c, 0x49, 0xe5, 0xde, 0x3b, 0x79, 0x8d, 0x7d, 0xe8, 0xf4, - 0x99, 0x3b, 0x92, 0xfe, 0xc5, 0xe2, 0xc6, 0x17, 0x93, 0x34, 0xa7, 0xfc, 0x04, 0x9a, 0xa7, 0xec, - 0x12, 0xf3, 0x8f, 0x98, 0x25, 0x24, 0xba, 0x69, 0x02, 0xc3, 0x22, 0x43, 0x83, 0xd2, 0x07, 0x3c, - 0x1c, 0x31, 0x21, 0xfc, 0x60, 0x5c, 0xa8, 0x11, 0x59, 0xfe, 0x25, 0xac, 0x47, 0x1a, 0x38, 0xf7, - 0xaf, 0x13, 0x8e, 0x90, 0x51, 0x79, 0x2c, 0x89, 0x70, 0x33, 0x7a, 0x31, 0x10, 0x1c, 0x16, 0xe9, - 0xf7, 0x4d, 0x3e, 0xf0, 0x67, 0xb0, 0x95, 0x7f, 0x5e, 0x90, 0x7b, 0x26, 0x9f, 0x8b, 0x1e, 0x1d, - 0x79, 0xfd, 0xaf, 0xa1, 0xbd, 0x00, 0x18, 0x75, 0x9f, 0x2a, 0xc3, 0x91, 0xf9, 0x70, 0x1d, 0x20, - 0xa7, 0xec, 0x32, 0x5f, 0x53, 0x1f, 0x9a, 0xab, 0x5d, 0x86, 0xa4, 0xf5, 0x90, 0x5d, 0x80, 0xb5, - 0x98, 0xaf, 0x9d, 0x42, 0x24, 0x69, 0x93, 0x1e, 0xce, 0x84, 0x25, 0x28, 0x33, 0x1f, 0xde, 0x57, - 0x60, 0x25, 0xe9, 0xf3, 0x1f, 0xb5, 0xf4, 0x9c, 0x81, 0x5d, 0x58, 0xd3, 0xf9, 0x69, 0x06, 0x4a, - 0x1a, 0x25, 0x64, 0x6b, 0xfb, 0x4b, 0x58, 0xcf, 0x20, 0x3f, 0x3d, 0xc0, 0x8a, 0xc0, 0x60, 0xce, - 0xc7, 0xe1, 0xed, 0x3f, 0x36, 0xf0, 0x7f, 0xf8, 0xbf, 0x03, 0x00, 0x00, 0xff, 0xff, 0x26, 0xa2, - 0x44, 0x9d, 0x3e, 0x17, 0x00, 0x00, + // 1842 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x58, 0x6d, 0x73, 0x1b, 0xb7, + 0x11, 0xe6, 0x8b, 0x29, 0x93, 0xab, 0x57, 0xc2, 0x32, 0x7b, 0xa1, 0x65, 0x9b, 0x46, 0x1c, 0x8f, + 0x32, 0x9d, 0x2a, 0xce, 0x35, 0x93, 0x64, 0x46, 0xad, 0x13, 0x29, 0x94, 0x65, 0xc5, 0x8e, 0xcc, + 0x1c, 0x6b, 0xb5, 0xd3, 0xe9, 0x97, 0x0b, 0x0f, 0xa1, 0xaf, 0xa6, 0xee, 0x18, 0x00, 0x94, 0x42, + 0x7d, 0xee, 0x4c, 0xfd, 0x0b, 0x3a, 0xfd, 0xd8, 0xdf, 0xd1, 0x3f, 0xd1, 0xbf, 0xd4, 0xc1, 0x02, + 0xf7, 0xca, 0x3b, 0xaa, 0x9a, 0x76, 0xfa, 0xed, 0x76, 0x81, 0x5d, 0x2c, 0x80, 0xdd, 0x67, 0x1f, + 0x1c, 0xb4, 0x85, 0xfb, 0xc9, 0x94, 0x87, 0x32, 0xfc, 0x44, 0xb8, 0x7b, 0xf8, 0x41, 0x6a, 0xc2, + 0xed, 0xde, 0x1d, 0x85, 0x9c, 0x99, 0x01, 0xf5, 0xa9, 0x87, 0x68, 0x0f, 0x36, 0x1c, 0x36, 0xf6, + 0x85, 0xe4, 0xae, 0xf4, 0xc3, 0xe0, 0xa4, 0x4f, 0x36, 0xa0, 0xe6, 0x7b, 0x56, 0xb5, 0x57, 0xdd, + 0xad, 0x3b, 0x35, 0xdf, 0xa3, 0x0f, 0x00, 0xbe, 0x1d, 0xbe, 0x3e, 0xfd, 0x3d, 0xfb, 0xe1, 0x25, + 0x9b, 0x93, 0x2d, 0xa8, 0xff, 0xf9, 0xf2, 0x1d, 0x0e, 0xaf, 0x39, 0xea, 0x93, 0x3e, 0x82, 0xcd, + 0x83, 0x99, 0x7c, 0x1b, 0x72, 0xff, 0x6a, 0xd1, 0x45, 0x0b, 0x5d, 0xfc, 0xb3, 0x0a, 0x0f, 0x8e, + 0x99, 0x1c, 0xb0, 0xc0, 0xf3, 0x83, 0x71, 0x66, 0xb6, 0xc3, 0x7e, 0x9a, 0x31, 0x21, 0xc9, 0x13, + 0xd8, 0xe0, 0x99, 0x38, 0x4c, 0x04, 0x39, 0xad, 0x9a, 0xe7, 0x7b, 0x2c, 0x90, 0xfe, 0x8f, 0x3e, + 0xe3, 0xbf, 0x9b, 0x4f, 0x99, 0x55, 0xc3, 0x65, 0x72, 0x5a, 0xb2, 0x0b, 0x9b, 0x89, 0xe6, 0xcc, + 0x9d, 0xcc, 0x98, 0x55, 0xc7, 0x89, 0x79, 0x35, 0x79, 0x00, 0x70, 0xe1, 0x4e, 0x7c, 0xef, 0x4d, + 0x20, 0xfd, 0x89, 0x75, 0x0b, 0x57, 0x4d, 0x69, 0xa8, 0x80, 0xfb, 0xc7, 0x4c, 0x9e, 0x29, 0x45, + 0x26, 0x72, 0x71, 0xd3, 0xd0, 0x2d, 0xb8, 0xed, 0x85, 0xe7, 0xae, 0x1f, 0x08, 0xab, 0xd6, 0xab, + 0xef, 0xb6, 0x9c, 0x48, 0x54, 0x87, 0x1a, 0x84, 0x97, 0x18, 0x60, 0xdd, 0x51, 0x9f, 0xf4, 0x1f, + 0x55, 0xb8, 0x53, 0xb0, 0x24, 0xf9, 0x12, 0x1a, 0x18, 0x9a, 0x55, 0xed, 0xd5, 0x77, 0x57, 0x6d, + 0xba, 0x27, 0xdc, 0xbd, 0x82, 0x79, 0x7b, 0xdf, 0xb9, 0xd3, 0xa3, 0x09, 0x3b, 0x67, 0x81, 0x74, + 0xb4, 0x41, 0xf7, 0x35, 0x40, 0xa2, 0x24, 0x1d, 0x58, 0xd1, 0x8b, 0x9b, 0x5b, 0x32, 0x12, 0xf9, + 0x18, 0x1a, 0xee, 0x4c, 0xbe, 0xbd, 0xc2, 0x53, 0x5d, 0xb5, 0xef, 0xec, 0x61, 0xaa, 0x64, 0x6f, + 0x4c, 0xcf, 0xa0, 0xff, 0xaa, 0x41, 0xfb, 0x1b, 0xc6, 0xd5, 0x51, 0x8e, 0x5c, 0xc9, 0x86, 0xd2, + 0x95, 0x33, 0xa1, 0x1c, 0x0b, 0xc6, 0x7d, 0x77, 0x12, 0x39, 0xd6, 0x12, 0xea, 0x71, 0x86, 0xb9, + 0x06, 0x23, 0xa9, 0x7b, 0x0a, 0x47, 0x62, 0xfa, 0xca, 0x15, 0xf2, 0xcd, 0xd4, 0x73, 0x25, 0xf3, + 0xcc, 0x15, 0xe4, 0xd5, 0xa4, 0x07, 0xab, 0x9c, 0x5d, 0x84, 0xef, 0x98, 0xd7, 0x77, 0x25, 0xb3, + 0x1a, 0x38, 0x2b, 0xad, 0x22, 0x8f, 0x61, 0xdd, 0x88, 0x0e, 0x73, 0x45, 0x18, 0x58, 0x2b, 0x38, + 0x27, 0xab, 0x24, 0x9f, 0xc1, 0xdd, 0x89, 0x2b, 0xe4, 0xd1, 0xcf, 0x53, 0x5f, 0x5f, 0xcd, 0xa9, + 0x3b, 0x1e, 0xb2, 0x40, 0x5a, 0xb7, 0x71, 0x76, 0xf1, 0x20, 0xa1, 0xb0, 0xa6, 0x02, 0x72, 0x98, + 0x98, 0x86, 0x81, 0x60, 0x56, 0x13, 0x0b, 0x20, 0xa3, 0x23, 0x5d, 0x68, 0x06, 0xa1, 0x3c, 0xf8, + 0x51, 0x32, 0x6e, 0xb5, 0xd0, 0x59, 0x2c, 0x93, 0x1d, 0x68, 0xf9, 0x02, 0xdd, 0x32, 0xcf, 0x82, + 0x5e, 0x75, 0xb7, 0xe9, 0x24, 0x8a, 0x6f, 0x6f, 0x35, 0x6b, 0x5b, 0x75, 0xda, 0x83, 0x95, 0x61, + 0x72, 0x5a, 0x05, 0xa7, 0x48, 0xf7, 0xa1, 0xe1, 0xb8, 0xc1, 0x18, 0x97, 0x62, 0x2e, 0x9f, 0xf8, + 0x4c, 0x48, 0x93, 0x6d, 0xb1, 0xac, 0x8c, 0x27, 0xae, 0x54, 0x23, 0x35, 0x1c, 0x31, 0x12, 0xbd, + 0x0f, 0x8d, 0x6f, 0xc2, 0x59, 0x20, 0xc9, 0x36, 0x34, 0x46, 0xea, 0xc3, 0x58, 0x6a, 0x81, 0xfe, + 0x01, 0x1e, 0xe2, 0x70, 0xea, 0x4e, 0xc5, 0xe1, 0xfc, 0xd4, 0x3d, 0x67, 0x71, 0xa6, 0x3f, 0x84, + 0x06, 0x57, 0xcb, 0xa3, 0xe1, 0xaa, 0xdd, 0x52, 0xd9, 0x87, 0xf1, 0x38, 0x5a, 0xaf, 0x3c, 0x07, + 0xca, 0xc0, 0x24, 0xb8, 0x16, 0xe8, 0x5f, 0xab, 0xb0, 0x86, 0xae, 0x8d, 0x3b, 0xf2, 0x15, 0xac, + 0x8d, 0x52, 0xb2, 0x49, 0xe6, 0x7b, 0xca, 0x5d, 0x7a, 0x5e, 0x3a, 0x8b, 0x33, 0x06, 0xdd, 0xcf, + 0x33, 0xc9, 0x4c, 0xe0, 0x96, 0x5a, 0xc8, 0x9c, 0x15, 0x7e, 0x27, 0x7b, 0xac, 0xa5, 0xf7, 0x38, + 0x80, 0xfb, 0xb8, 0x40, 0x1a, 0xf2, 0xc4, 0xe1, 0xfc, 0x64, 0x10, 0xed, 0x50, 0x21, 0xd7, 0xd4, + 0xa0, 0x5b, 0xcd, 0x9f, 0x26, 0x3b, 0xae, 0x15, 0xef, 0x98, 0xbe, 0xaf, 0xc2, 0x23, 0x74, 0x79, + 0x12, 0x5c, 0xfc, 0xf7, 0x10, 0xd1, 0x85, 0xe6, 0xdb, 0x50, 0x48, 0xdc, 0x8d, 0xc6, 0xb5, 0x58, + 0x4e, 0x42, 0xa9, 0x97, 0x84, 0x32, 0x04, 0x82, 0x91, 0xbc, 0xe6, 0x1e, 0xe3, 0xf1, 0xd2, 0x3b, + 0xd0, 0x72, 0x47, 0xb8, 0xfb, 0x78, 0xd5, 0x44, 0x71, 0xfd, 0xfe, 0x5e, 0xc0, 0x36, 0x3a, 0x7d, + 0xfe, 0x7d, 0xff, 0x74, 0xc8, 0x64, 0xec, 0xb6, 0x03, 0x2b, 0x97, 0x7e, 0xe0, 0x85, 0x97, 0xc6, + 0xa7, 0x91, 0xca, 0x41, 0x8e, 0x3e, 0x85, 0x6d, 0xe3, 0xe4, 0xe8, 0x67, 0x5f, 0x24, 0x9e, 0x52, + 0x16, 0xd5, 0xac, 0xc5, 0x00, 0x7a, 0x03, 0xce, 0x2e, 0xfc, 0x70, 0x26, 0x52, 0x49, 0x99, 0xb5, + 0x2e, 0x03, 0xb2, 0x6d, 0x68, 0x70, 0x36, 0x3e, 0xe9, 0x47, 0xf7, 0x8f, 0x82, 0xaa, 0x30, 0x6d, + 0xae, 0xec, 0x18, 0x7e, 0xa1, 0x5d, 0xd3, 0x31, 0x12, 0x95, 0xb0, 0x75, 0xe0, 0x79, 0xba, 0x0c, + 0xa3, 0x35, 0x62, 0x5f, 0xd5, 0x94, 0xaf, 0x54, 0x8d, 0xd6, 0x32, 0x48, 0x67, 0xc1, 0xed, 0x11, + 0x67, 0x88, 0x64, 0x1a, 0xd0, 0x23, 0x51, 0x8d, 0x30, 0x2c, 0x78, 0x61, 0x30, 0x2e, 0x12, 0x55, + 0x85, 0xdc, 0x3d, 0xf0, 0xbc, 0xd4, 0x2e, 0xa3, 0xb5, 0xb7, 0xa0, 0xee, 0x31, 0x1e, 0xf5, 0x5b, + 0x8f, 0xf1, 0xe2, 0x9d, 0xa9, 0x1a, 0x50, 0x58, 0x84, 0x4b, 0xae, 0x39, 0xf8, 0xad, 0x22, 0xf4, + 0x85, 0x98, 0xc5, 0x90, 0x6a, 0x24, 0x95, 0x65, 0xf8, 0xc5, 0x4f, 0xfa, 0x06, 0x46, 0x63, 0x99, + 0x3e, 0x85, 0x4e, 0x3e, 0x10, 0x83, 0x6e, 0xea, 0xa4, 0xfd, 0x71, 0x04, 0x38, 0xea, 0xa4, 0x51, + 0xa2, 0x03, 0x58, 0xc3, 0x8c, 0x4b, 0x97, 0x50, 0x8a, 0x3f, 0x90, 0xa7, 0x70, 0x67, 0x26, 0xd8, + 0x99, 0x9d, 0xad, 0x0c, 0x8c, 0xbe, 0xe9, 0x14, 0x0d, 0xd1, 0x57, 0x40, 0xa3, 0x8e, 0x8b, 0x9e, + 0x8b, 0x6b, 0x2a, 0xbf, 0x4e, 0x07, 0x56, 0xdc, 0xd1, 0x48, 0xc6, 0x07, 0x63, 0x24, 0x3a, 0x87, + 0x5f, 0x1c, 0x33, 0x5d, 0x14, 0xcf, 0x43, 0x9e, 0xc1, 0xb3, 0xc4, 0xa4, 0x9a, 0x36, 0x29, 0x86, + 0xb1, 0xb2, 0x8d, 0xd4, 0xcb, 0x37, 0xf2, 0xf7, 0x2a, 0x58, 0xc7, 0x4c, 0xfe, 0xdf, 0x68, 0x83, + 0xea, 0xa6, 0x9c, 0xfd, 0x34, 0xf3, 0xb9, 0x89, 0xe5, 0x4a, 0x67, 0x5a, 0xd3, 0xc9, 0xab, 0xe9, + 0xdf, 0xaa, 0xb0, 0x91, 0xe3, 0x16, 0xbf, 0x8e, 0x7a, 0xbf, 0x86, 0xe3, 0xfb, 0x0a, 0x0b, 0x96, + 0xd0, 0x0a, 0x9c, 0xfb, 0xbf, 0xa7, 0x15, 0xaf, 0xe0, 0xe1, 0x81, 0xe7, 0x15, 0x51, 0xc5, 0xf8, + 0xe4, 0x3e, 0xce, 0x06, 0xba, 0xcc, 0xdb, 0x63, 0xd8, 0xca, 0x91, 0x53, 0x3c, 0x36, 0xdf, 0x8b, + 0xc0, 0x46, 0x7d, 0x52, 0xba, 0x30, 0xcb, 0x5e, 0xa0, 0xc1, 0x1f, 0x41, 0x3b, 0x33, 0xc7, 0xce, + 0xb9, 0xaa, 0x6b, 0x57, 0x57, 0x60, 0x39, 0x48, 0x37, 0x0a, 0x6a, 0x79, 0x09, 0x37, 0xe2, 0x9a, + 0xb0, 0x98, 0xcc, 0xd5, 0x92, 0xaa, 0x69, 0x45, 0x7d, 0xcc, 0x05, 0xe3, 0xb7, 0xaa, 0x5d, 0x1e, + 0x71, 0x90, 0x5b, 0x58, 0xeb, 0xb1, 0x4c, 0xff, 0x52, 0x83, 0x9d, 0xe7, 0x7e, 0xe0, 0x4e, 0xfc, + 0x2b, 0x56, 0x48, 0xb2, 0x0b, 0x4a, 0xc6, 0x90, 0xb2, 0x5a, 0x86, 0x94, 0xa5, 0x80, 0xaa, 0x9e, + 0x01, 0x2a, 0xec, 0x26, 0x52, 0xb2, 0xf3, 0x69, 0x44, 0xd4, 0x5a, 0x4e, 0xa2, 0x20, 0x7d, 0x68, + 0x63, 0x13, 0x34, 0x8b, 0x8e, 0x42, 0xee, 0x09, 0xab, 0x81, 0x97, 0xd4, 0xd1, 0x97, 0x74, 0x96, + 0x1b, 0x76, 0x16, 0x0d, 0xc8, 0x33, 0xd8, 0x4c, 0x94, 0x47, 0x9c, 0x87, 0x1c, 0x89, 0xdc, 0xaa, + 0xbd, 0xad, 0x7d, 0x0c, 0x78, 0xf8, 0xc3, 0x84, 0x9d, 0xf7, 0x99, 0x74, 0xfd, 0x89, 0x70, 0xf2, + 0x93, 0x55, 0x6a, 0x6f, 0x1f, 0x78, 0xde, 0xe1, 0x24, 0x1c, 0xbd, 0x63, 0xde, 0x4b, 0x36, 0x4f, + 0x75, 0x9a, 0x77, 0x6c, 0xfe, 0xc2, 0x15, 0x6f, 0x0d, 0x9e, 0x46, 0xa2, 0x2a, 0x78, 0xd7, 0xf3, + 0x98, 0x17, 0x61, 0x2a, 0x0a, 0x78, 0x3c, 0xe1, 0x8c, 0x8f, 0x58, 0xcc, 0x59, 0x51, 0x42, 0x84, + 0x0f, 0xcf, 0x55, 0xc2, 0x9b, 0x23, 0x88, 0x44, 0x75, 0x3c, 0x86, 0x6c, 0x1e, 0xce, 0x0d, 0xb4, + 0x26, 0x0a, 0xfa, 0x2b, 0x68, 0xbf, 0x64, 0x73, 0x13, 0xd7, 0xb5, 0x41, 0xd9, 0xef, 0xef, 0xc0, + 0xd6, 0x50, 0x86, 0xdc, 0x1d, 0x47, 0xb7, 0x29, 0xe7, 0x64, 0x1f, 0x36, 0x8f, 0x59, 0x86, 0xbf, + 0x10, 0x82, 0x4d, 0x3b, 0x03, 0x1a, 0x5d, 0xa2, 0x8f, 0x2a, 0xad, 0xa5, 0x15, 0xf2, 0x1b, 0xd8, + 0xce, 0x19, 0x1f, 0xce, 0xd5, 0xa3, 0x6e, 0x43, 0x79, 0x48, 0x1e, 0x79, 0x25, 0xd6, 0x9f, 0xc2, + 0xc6, 0x31, 0x4b, 0xd3, 0x43, 0x02, 0xca, 0x4e, 0xf7, 0xca, 0x6e, 0x5b, 0xdb, 0xa4, 0x86, 0x69, + 0x85, 0x7c, 0x06, 0x6d, 0xf5, 0xee, 0xe3, 0x6c, 0x74, 0x13, 0xab, 0x7d, 0x0c, 0x73, 0xf1, 0x6d, + 0x91, 0x36, 0xbc, 0x8b, 0x64, 0x31, 0x3f, 0x85, 0x56, 0xc8, 0x10, 0xac, 0x32, 0x1a, 0x4b, 0x3e, + 0x8c, 0x19, 0x66, 0x39, 0xc9, 0xed, 0x6e, 0xe5, 0x69, 0x28, 0xad, 0x90, 0x17, 0xd0, 0x29, 0xe6, + 0x8d, 0xe4, 0x51, 0x3c, 0xbb, 0x8c, 0x53, 0x76, 0x5b, 0xf1, 0x14, 0x5a, 0x21, 0xdf, 0xc1, 0xbd, + 0x92, 0xd9, 0x48, 0xa0, 0x6f, 0xea, 0xce, 0x86, 0xd5, 0x14, 0xe7, 0x23, 0x9d, 0x78, 0x2c, 0x43, + 0x02, 0xb3, 0x36, 0x9f, 0xc3, 0x7a, 0x86, 0xd2, 0x11, 0x2b, 0x1e, 0xcd, 0xb1, 0xbc, 0xac, 0xdd, + 0x17, 0xb0, 0x9e, 0x21, 0x70, 0xda, 0xae, 0x88, 0xd3, 0x75, 0xf1, 0xa6, 0xb4, 0x8a, 0x56, 0xc8, + 0x6b, 0xf8, 0xa0, 0x94, 0xc7, 0x91, 0xc7, 0x6a, 0xea, 0x75, 0x34, 0x2f, 0xe7, 0xf0, 0x6b, 0x4c, + 0xab, 0x2c, 0x1c, 0x93, 0xed, 0x85, 0x7e, 0x75, 0xd2, 0xb7, 0xbb, 0x45, 0xcd, 0x01, 0x2f, 0x94, + 0x2c, 0x34, 0x66, 0x9b, 0xec, 0x28, 0x17, 0x65, 0x0d, 0xbb, 0x4b, 0x16, 0x1b, 0x22, 0xad, 0x90, + 0x37, 0xd8, 0xe2, 0x8b, 0xfa, 0x95, 0x4d, 0xa8, 0xf1, 0xb7, 0xe4, 0xc7, 0x47, 0x59, 0x80, 0xcf, + 0x4c, 0x9e, 0x14, 0x36, 0x42, 0xbb, 0xb0, 0xe6, 0x33, 0x97, 0xf5, 0x27, 0xd8, 0x59, 0xc2, 0xa1, + 0x6c, 0xf2, 0xc4, 0x84, 0x76, 0x0d, 0xcb, 0x2a, 0xd9, 0xf4, 0xf7, 0x26, 0xba, 0xc2, 0x47, 0x8f, + 0x4d, 0x3e, 0x8a, 0x23, 0x59, 0xf6, 0x2a, 0xca, 0x06, 0xec, 0x20, 0x4d, 0x3b, 0x2b, 0x72, 0xf7, + 0x28, 0x1d, 0xeb, 0x4d, 0xc2, 0xfc, 0x14, 0x20, 0x01, 0x5c, 0x82, 0x90, 0xb1, 0x00, 0xc0, 0xb9, + 0xd4, 0xda, 0x87, 0xcd, 0x53, 0x76, 0x99, 0xc3, 0xd7, 0x05, 0x34, 0x2c, 0x41, 0xc8, 0x2f, 0x80, + 0xe8, 0xbf, 0x15, 0xd7, 0xda, 0xaf, 0x6a, 0xdd, 0xd1, 0xf9, 0x54, 0xce, 0x69, 0x85, 0x9c, 0xc0, + 0x46, 0x96, 0x75, 0x93, 0x0f, 0x70, 0x43, 0x45, 0x4f, 0x82, 0x6e, 0xb7, 0x68, 0xc8, 0x50, 0x80, + 0x0a, 0xf9, 0x2d, 0xb4, 0x15, 0x7f, 0xca, 0x42, 0xee, 0x12, 0x6f, 0xb9, 0x48, 0x9e, 0x42, 0x2b, + 0x7e, 0xff, 0x98, 0x92, 0xca, 0x3d, 0x87, 0xf2, 0x16, 0xfb, 0xd0, 0xe9, 0x33, 0x77, 0x24, 0xfd, + 0x8b, 0xc5, 0x8d, 0x2f, 0x26, 0x69, 0xce, 0xf8, 0x09, 0x34, 0x4f, 0xd9, 0x25, 0xe6, 0x1f, 0x31, + 0x43, 0x28, 0x74, 0xd3, 0x02, 0x86, 0x45, 0x86, 0x86, 0xc4, 0x0f, 0x78, 0x38, 0x62, 0x42, 0xf8, + 0xc1, 0xb8, 0xd0, 0x22, 0xf2, 0xfc, 0x4b, 0x58, 0x8f, 0x2c, 0x90, 0x16, 0x5c, 0x37, 0x39, 0x22, + 0x4e, 0xe5, 0xb1, 0x24, 0x93, 0x9b, 0xd1, 0x83, 0x82, 0x60, 0xb3, 0x48, 0x3f, 0x7f, 0xf2, 0x81, + 0x3f, 0x83, 0xad, 0xfc, 0xeb, 0x83, 0xdc, 0x33, 0xf9, 0x5c, 0xf4, 0x26, 0xc9, 0xdb, 0x7f, 0x0d, + 0xed, 0x05, 0x3e, 0xa9, 0x71, 0xaa, 0x8c, 0x66, 0xe6, 0xc3, 0x75, 0x80, 0x9c, 0xb2, 0xcb, 0x7c, + 0x4d, 0x7d, 0x68, 0xae, 0x76, 0x19, 0xd1, 0xd6, 0x4d, 0x76, 0x81, 0xf5, 0x62, 0xbe, 0x76, 0x0a, + 0x89, 0xa6, 0x4d, 0x7a, 0xd8, 0x13, 0x96, 0x90, 0xd0, 0x7c, 0x78, 0x5f, 0x81, 0x95, 0xa4, 0xcf, + 0x7f, 0x04, 0xe9, 0x39, 0x07, 0xbb, 0xb0, 0xa6, 0xf3, 0xd3, 0x34, 0x94, 0x34, 0x4b, 0xc8, 0xd6, + 0xf6, 0x97, 0xb0, 0x9e, 0xe1, 0x85, 0xba, 0x81, 0x15, 0x51, 0xc5, 0xdc, 0x1a, 0x87, 0xb7, 0xff, + 0xd8, 0xc0, 0xdf, 0xe5, 0xff, 0x0e, 0x00, 0x00, 0xff, 0xff, 0x70, 0x4b, 0x22, 0xaf, 0x5d, 0x17, + 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. diff --git a/sa/proto/sa.proto b/sa/proto/sa.proto index bf8aaf2fa..1cb7a271a 100644 --- a/sa/proto/sa.proto +++ b/sa/proto/sa.proto @@ -250,6 +250,7 @@ message AddBlockedKeyRequest { optional int64 added = 2; // Unix timestamp (nanoseconds) optional string source = 3; optional string comment = 4; + optional int64 revokedBy = 5; } message KeyBlockedRequest { diff --git a/sa/sa.go b/sa/sa.go index 8079ab187..daf4b9280 100644 --- a/sa/sa.go +++ b/sa/sa.go @@ -21,6 +21,7 @@ import ( corepb "github.com/letsencrypt/boulder/core/proto" "github.com/letsencrypt/boulder/db" berrors "github.com/letsencrypt/boulder/errors" + "github.com/letsencrypt/boulder/features" bgrpc "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/identifier" blog "github.com/letsencrypt/boulder/log" @@ -1772,6 +1773,8 @@ func addKeyHash(db db.Inserter, cert *x509.Certificate) error { return db.Insert(khm) } +var blockedKeysColumns = "keyHash, added, source, comment" + // AddBlockedKey adds a key hash to the blockedKeys table func (ssa *SQLStorageAuthority) AddBlockedKey(ctx context.Context, req *sapb.AddBlockedKeyRequest) (*corepb.Empty, error) { if req == nil || req.KeyHash == nil || req.Added == nil || req.Source == nil { @@ -1781,12 +1784,22 @@ func (ssa *SQLStorageAuthority) AddBlockedKey(ctx context.Context, req *sapb.Add if !ok { return nil, errors.New("unknown source") } - err := ssa.dbMap.Insert(&blockedKeyModel{ - KeyHash: req.KeyHash, - Added: time.Unix(0, *req.Added), - Source: sourceInt, - Comment: req.Comment, - }) + cols, qs := blockedKeysColumns, "?, ?, ?, ?" + vals := []interface{}{ + req.KeyHash, + time.Unix(0, *req.Added), + sourceInt, + req.Comment, + } + if features.Enabled(features.StoreRevokerInfo) && req.RevokedBy != nil { + cols += ", revokedBy" + qs += ", ?" + vals = append(vals, *req.RevokedBy) + } + _, err := ssa.dbMap.Exec( + fmt.Sprintf("INSERT INTO blockedKeys (%s) VALUES (%s)", cols, qs), + vals..., + ) if err != nil { if db.IsDuplicate(err) { // Ignore duplicate inserts so multiple certs with the same key can @@ -1804,7 +1817,8 @@ func (ssa *SQLStorageAuthority) KeyBlocked(ctx context.Context, req *sapb.KeyBlo return nil, errIncompleteRequest } exists := false - if err := ssa.dbMap.SelectOne(&blockedKeyModel{}, `SELECT * FROM blockedKeys WHERE keyHash = ?`, req.KeyHash); err != nil { + var id int64 + if err := ssa.dbMap.SelectOne(&id, `SELECT ID FROM blockedKeys WHERE keyHash = ?`, req.KeyHash); err != nil { if db.IsNoRows(err) { return &sapb.Exists{Exists: &exists}, nil } diff --git a/sa/sa_test.go b/sa/sa_test.go index 02d41849a..12459749b 100644 --- a/sa/sa_test.go +++ b/sa/sa_test.go @@ -2321,3 +2321,33 @@ func TestAddBlockedKeyUnknownSource(t *testing.T) { test.AssertError(t, err, "AddBlockedKey didn't fail with unknown source") test.AssertEquals(t, err.Error(), "unknown source") } + +func TestBlockedKeyRevokedBy(t *testing.T) { + if !strings.HasSuffix(os.Getenv("BOULDER_CONFIG_DIR"), "config-next") { + return + } + + sa, _, cleanUp := initSA(t) + defer cleanUp() + + err := features.Set(map[string]bool{"StoreRevokerInfo": true}) + test.AssertNotError(t, err, "failed to set features") + defer features.Reset() + + added := int64(0) + source := "API" + _, err = sa.AddBlockedKey(context.Background(), &sapb.AddBlockedKeyRequest{ + KeyHash: []byte{1}, + Added: &added, + Source: &source, + }) + test.AssertNotError(t, err, "AddBlockedKey failed") + revoker := int64(1) + _, err = sa.AddBlockedKey(context.Background(), &sapb.AddBlockedKeyRequest{ + KeyHash: []byte{2}, + Added: &added, + Source: &source, + RevokedBy: &revoker, + }) + test.AssertNotError(t, err, "AddBlockedKey failed") +} diff --git a/test/config-next/bad-key-revoker.json b/test/config-next/bad-key-revoker.json new file mode 100644 index 000000000..4f9f39269 --- /dev/null +++ b/test/config-next/bad-key-revoker.json @@ -0,0 +1,33 @@ +{ + "BadKeyRevoker": { + "dbConnectFile": "test/secrets/badkeyrevoker_dburl", + "maxDBConns": 10, + "debugAddr": ":8020", + "tls": { + "caCertFile": "test/grpc-creds/minica.pem", + "certFile": "test/grpc-creds/bad-key-revoker.boulder/cert.pem", + "keyFile": "test/grpc-creds/bad-key-revoker.boulder/key.pem" + }, + "raService": { + "serverAddress": "ra.boulder:9094", + "timeout": "15s" + }, + "mailer": { + "server": "localhost", + "port": "9380", + "username": "cert-master@example.com", + "from": "bad key revoker ", + "passwordFile": "test/secrets/smtp_password", + "SMTPTrustedRootFile": "test/mail-test-srv/minica.pem", + "emailSubject": "Certificates you've issued have been revoked due to key compromise", + "emailTemplate": "test/example-bad-key-revoker-template" + }, + "maximumRevocations": 15, + "findCertificatesBatchSize": 10, + "interval": "1s" + }, + "syslog": { + "stdoutlevel": 6, + "sysloglevel": 4 + } +} diff --git a/test/config-next/ra.json b/test/config-next/ra.json index e6b761e2b..981c188bb 100644 --- a/test/config-next/ra.json +++ b/test/config-next/ra.json @@ -42,11 +42,13 @@ "address": ":9094", "clientNames": [ "wfe.boulder", - "admin-revoker.boulder" + "admin-revoker.boulder", + "bad-key-revoker.boulder" ] }, "features": { - "BlockedKeyTable": true + "BlockedKeyTable": true, + "StoreRevokerInfo": true }, "CTLogGroups2": [ { diff --git a/test/config-next/sa.json b/test/config-next/sa.json index 3e29eee5e..bab9c7e3d 100644 --- a/test/config-next/sa.json +++ b/test/config-next/sa.json @@ -25,7 +25,8 @@ }, "features": { "StoreIssuerInfo": true, - "StoreKeyHashes": true + "StoreKeyHashes": true, + "StoreRevokerInfo": true } }, diff --git a/test/example-bad-key-revoker-template b/test/example-bad-key-revoker-template new file mode 100644 index 000000000..51833fa30 --- /dev/null +++ b/test/example-bad-key-revoker-template @@ -0,0 +1,8 @@ +Hello, + +The public key associated with certificates which you have issued has been marked as compromised. As such we are required to revoke any certificates which contain this public key. + +The following currently unexpired certificates that you've issued contain this public key and have been revoked: +{{range . -}} +{{.}} +{{end}} diff --git a/test/grpc-creds/bad-key-revoker.boulder/cert.pem b/test/grpc-creds/bad-key-revoker.boulder/cert.pem new file mode 100644 index 000000000..0564ce6e9 --- /dev/null +++ b/test/grpc-creds/bad-key-revoker.boulder/cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDKTCCAhGgAwIBAgIIIyhasHOijBIwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVbWluaWNhIHJvb3QgY2EgM2I4YjJjMCAXDTIwMDQyMTAyNTI0MFoYDzIxMTAw +NDIxMDM1MjQwWjAiMSAwHgYDVQQDExdiYWQta2V5LXJldm9rZXIuYm91bGRlcjCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMK7gvPqYoNE2A2/TUe/LHzw +6ya992e0nPEdUsbw8T1dhSujK6JfGMEHTIwWRDpai8MCgBcMDI3nzF1lYKaKWrdW +uNa9fztF0S/cOeyoEi7+bBQsyK4rmEHzbfw5z7NaeEBXi83T6NZdc0lcXAX6rex7 +hZ8mjEWD4ATmYZR82enfxZWSLEoz0AkRSGtMnjlbXdKhrwaDImgKqlKK0LOea/+2 +i857s2ilgdjWQ63eGGXjp1jIJe8wbgrYBGDyXdFKN1M6PAAa971LEbJJjdJgXKSY +KAfo9oHYhm3ehX/r9PNcTNBYLzOnLhe7DFcJlfFVcu7qrK35L6EEb4K2Ndqun0cC +AwEAAaNjMGEwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggr +BgEFBQcDAjAMBgNVHRMBAf8EAjAAMCIGA1UdEQQbMBmCF2JhZC1rZXktcmV2b2tl +ci5ib3VsZGVyMA0GCSqGSIb3DQEBCwUAA4IBAQBwmkDXtY2nArUzO50btsq8jxA4 +/rGHeuaSP9WraNyClV659W0FHHT+YzMRpC4cLHVY+KLNeMjyUtpcdQnFZ7Z53nmK +Hls11FMGZqXJ5cox440FQG3qiWQdYEoz2lblZW2uhHny7gD2T8cEt99nfFFcpfQc ++ZFOF3L36ssTar0aJOfb2qcbZ60CvRTcY6COqRqENEooyEAv4mo04LCtx2OZhIB+ +AvJHs+4vPG0KKdiY+PmqytmoNtexZaVm3+D7ibVbZXrnjK7n6Ieh6nB92gvZ/eAW +qAShiiI6k9wFWDiSVyVHQ/Pu/O3jnuXWA687tUtY6yjegTgK2pcC9jCZgXYA +-----END CERTIFICATE----- diff --git a/test/grpc-creds/bad-key-revoker.boulder/key.pem b/test/grpc-creds/bad-key-revoker.boulder/key.pem new file mode 100644 index 000000000..993c21533 --- /dev/null +++ b/test/grpc-creds/bad-key-revoker.boulder/key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAwruC8+pig0TYDb9NR78sfPDrJr33Z7Sc8R1SxvDxPV2FK6Mr +ol8YwQdMjBZEOlqLwwKAFwwMjefMXWVgpopat1a41r1/O0XRL9w57KgSLv5sFCzI +riuYQfNt/DnPs1p4QFeLzdPo1l1zSVxcBfqt7HuFnyaMRYPgBOZhlHzZ6d/FlZIs +SjPQCRFIa0yeOVtd0qGvBoMiaAqqUorQs55r/7aLznuzaKWB2NZDrd4YZeOnWMgl +7zBuCtgEYPJd0Uo3Uzo8ABr3vUsRskmN0mBcpJgoB+j2gdiGbd6Ff+v081xM0Fgv +M6cuF7sMVwmV8VVy7uqsrfkvoQRvgrY12q6fRwIDAQABAoIBAQCNU17/vMxQLoeK +uprQhjs4VfSjglzqw9be2oQ346eA/L1oZRyG0/N4K97vED3mB87E8ayajWETH/Ze +lfOmCmU6B9NP7elH0Cy4SmEzkurXdkhj//iJBxSSUKQy2JYXuYHqWF8bOz8RTHMd ++8zBfiP5q8/XKDfHP6U2iSiqhk30f/CF24H1/LPO5LhpM79+H78cNsEhV7Jb/p1R +3Svh69jxmqUrx/PuP1aJru65+IqxNwy9g6n1F0UJLdKcFXbr0/UMJr1YNO14DVOH +NZkK9mk0vAbGPbpMWm9ZAY068Crm1Y+ckiMDoaql+KeM0KLMnYfr+DyEwFcdioON +twg/HUSRAoGBAM1zVfd4+Lfqugf90aXGWoR5qcg/I+KaBwfHQ0r2KkobWtNHhhhM +h9Yq+KTQyHgnQcPgw50P1OF8u6aX9OLjO+JcX7Di7kKa4H1UaHTvK7MSSyUQMyd/ +pcQQNIFGALED2oYvdLi1QLAaDtEl5z2hLfDMPar667/sp1+TJ2tGUpWvAoGBAPKl +FF9k8KhZ41zsEm+6VCfRGlHVOiUgTKqIxMEr2ifAPro1pdX7ESGFF4xluA+ujHTS +NRNNVWmZje3rTrn83mtEoSuhsnBVCGyWn+ONCvhMUYZOUIxARNAuEZUGSh0eQAG5 +zgF3hgEUYnVH7AXsohfwdTBvLpedfZey1MRr3Y3pAoGBALRFbHwuAIdYhg13EJrW +NhyhqHFVvcYaguq3VHuVDjxiTkqvKqFtnY81u2Da9dxADfuy39GTz6ZfTUR7d1wS +KTyQ80IBjTCSN0KhatqX9g81kQwfb9NLtQcZdQithPPNvtQZFeDw4abj5nZsPMAe +CnKMs9uwOmX4YFCDjYYaeWJvAoGAI3U+MeaFSITCNe0FkLAw5hSnfPfk5FIBAha0 +ceofmhl80SdP0aI70aMqWsjuidQfEF87hFOTvLfExtTRD1rFgfVofADIG6RBc+Ta +/py40qoMa8z79lLZ+3YP+bAOmoy2G8p0MUCvI29AKBVXh1IaKddouKg2rc9E8Csg +7oc4vCkCgYBKJiWIkqMqULozEAsbOcnKb03hXYqljvtS0sVzgxx7I1C9hk2+Lkd7 +ObCp+JqaGsth01P9EFgRhCBkwQVOVqtWaWwcV4v2b5NG3GYJ9Pp99/iLjBLE6N81 +a50Y6niW/q4EXQLgurki0ZHTrKC7qo9fZOL2VUdnDyGAIsMimnTNtg== +-----END RSA PRIVATE KEY----- diff --git a/test/grpc-creds/generate.sh b/test/grpc-creds/generate.sh index 5225f0880..4f04150f9 100755 --- a/test/grpc-creds/generate.sh +++ b/test/grpc-creds/generate.sh @@ -10,7 +10,8 @@ command -v minica >/dev/null 2>&1 || { } for HOSTNAME in admin-revoker.boulder expiration-mailer.boulder \ - ocsp-updater.boulder orphan-finder.boulder wfe.boulder akamai-purger.boulder nonce.boulder ; do + ocsp-updater.boulder orphan-finder.boulder wfe.boulder akamai-purger.boulder nonce.boulder \ + bad-key-revoker.boulder ; do minica -domains ${HOSTNAME} done diff --git a/test/integration/revocation_test.go b/test/integration/revocation_test.go index ffa5c8634..dd1b9b060 100644 --- a/test/integration/revocation_test.go +++ b/test/integration/revocation_test.go @@ -8,9 +8,12 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/x509" + "io/ioutil" + "net/http" "os" "strings" "testing" + "time" "github.com/letsencrypt/boulder/test" ocsp_helper "github.com/letsencrypt/boulder/test/ocsp/helper" @@ -167,3 +170,80 @@ func TestRevokeWithKeyCompromise(t *testing.T) { test.AssertError(t, err, "NewAccount didn't fail with a blacklisted key") test.AssertEquals(t, err.Error(), `acme: error code 400 "urn:ietf:params:acme:error:badPublicKey": public key is forbidden`) } + +func TestBadKeyRevoker(t *testing.T) { + t.Parallel() + if !strings.HasSuffix(os.Getenv("BOULDER_CONFIG_DIR"), "config-next") { + return + } + + os.Setenv("DIRECTORY", "http://boulder:4001/directory") + cA, err := makeClient("mailto:bad-key-revoker-revoker@letsencrypt.org", "mailto:bad-key-revoker-revoker-2@letsencrypt.org") + test.AssertNotError(t, err, "creating acme client") + cB, err := makeClient("mailto:bad-key-revoker-revoker-2@letsencrypt.org") + test.AssertNotError(t, err, "creating acme client") + cC, err := makeClient("mailto:bad-key-revoker-revokee@letsencrypt.org", "mailto:bad-key-revoker-revokee-2@letsencrypt.org") + test.AssertNotError(t, err, "creating acme client") + cD, err := makeClient("mailto:bad-key-revoker-revokee-2@letsencrypt.org", "mailto:bad-key-revoker-revokee@letsencrypt.org") + test.AssertNotError(t, err, "creating acme client") + cE, err := makeClient() + test.AssertNotError(t, err, "creating acme client") + + certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + test.AssertNotError(t, err, "failed to generate cert key") + + badCert, err := authAndIssue(cA, certKey, []string{random_domain()}) + test.AssertNotError(t, err, "authAndIssue failed") + + certs := []*x509.Certificate{} + for _, c := range []*client{cA, cB, cC, cD, cE} { + for i := 0; i < 2; i++ { + cert, err := authAndIssue(c, certKey, []string{random_domain()}) + test.AssertNotError(t, err, "authAndIssue failed") + certs = append(certs, cert.certs[0]) + } + } + + err = cA.RevokeCertificate( + cA.Account, + badCert.certs[0], + cA.Account.PrivateKey, + ocsp.KeyCompromise, + ) + test.AssertNotError(t, err, "failed to revoke certificate") + _, err = ocsp_helper.ReqDER(badCert.certs[0].Raw, ocsp.Revoked) + test.AssertNotError(t, err, "ReqDER failed") + + for _, cert := range certs { + for i := 0; i < 5; i++ { + _, err = ocsp_helper.ReqDER(cert.Raw, ocsp.Revoked) + if err == nil { + break + } + if i == 5 { + t.Fatal("timed out waiting for revoked OCSP status") + } + time.Sleep(time.Second) + } + } + + countResp, err := http.Get("http://boulder:9381/count?to=bad-key-revoker-revokee@letsencrypt.org") + test.AssertNotError(t, err, "mail-test-srv GET /count failed") + defer func() { _ = countResp.Body.Close() }() + body, err := ioutil.ReadAll(countResp.Body) + test.AssertNotError(t, err, "failed to read body") + test.AssertEquals(t, string(body), "1\n") + otherCountResp, err := http.Get("http://boulder:9381/count?to=bad-key-revoker-revokee-2@letsencrypt.org") + test.AssertNotError(t, err, "mail-test-srv GET /count failed") + defer func() { _ = otherCountResp.Body.Close() }() + body, err = ioutil.ReadAll(otherCountResp.Body) + test.AssertNotError(t, err, "failed to read body") + test.AssertEquals(t, string(body), "1\n") + + zeroCountResp, err := http.Get("http://boulder:9381/count?to=bad-key-revoker-revoker@letsencrypt.org") + test.AssertNotError(t, err, "mail-test-srv GET /count failed") + defer func() { _ = zeroCountResp.Body.Close() }() + body, err = ioutil.ReadAll(zeroCountResp.Body) + test.AssertNotError(t, err, "failed to read body") + test.AssertEquals(t, string(body), "0\n") +} diff --git a/test/sa_db_users.sql b/test/sa_db_users.sql index c74053b51..2507eed7b 100644 --- a/test/sa_db_users.sql +++ b/test/sa_db_users.sql @@ -13,6 +13,7 @@ CREATE USER IF NOT EXISTS 'ocsp_update'@'localhost'; CREATE USER IF NOT EXISTS 'test_setup'@'localhost'; CREATE USER IF NOT EXISTS 'purger'@'localhost'; CREATE USER IF NOT EXISTS 'janitor'@'localhost'; +CREATE USER IF NOT EXISTS 'badkeyrevoker'@'localhost'; -- Storage Authority GRANT SELECT,INSERT ON certificates TO 'sa'@'localhost'; @@ -67,5 +68,12 @@ GRANT SELECT,DELETE ON requestedNames TO 'janitor'@'localhost'; GRANT SELECT,DELETE ON orderFqdnSets TO 'janitor'@'localhost'; GRANT SELECT,DELETE ON orderToAuthz2 TO 'janitor'@'localhost'; +-- Bad Key Revoker +GRANT SELECT,UPDATE ON blockedKeys TO 'badkeyrevoker'@'localhost'; +GRANT SELECT ON keyHashToSerial TO 'badkeyrevoker'@'localhost'; +GRANT SELECT ON certificateStatus TO 'badkeyrevoker'@'localhost'; +GRANT SELECT ON certificates TO 'badkeyrevoker'@'localhost'; +GRANT SELECT ON registrations TO 'badkeyrevoker'@'localhost'; + -- Test setup and teardown GRANT ALL PRIVILEGES ON * to 'test_setup'@'localhost'; diff --git a/test/secrets/badkeyrevoker_dburl b/test/secrets/badkeyrevoker_dburl new file mode 100644 index 000000000..4639cdc16 --- /dev/null +++ b/test/secrets/badkeyrevoker_dburl @@ -0,0 +1 @@ +badkeyrevoker@tcp(boulder-mysql:3306)/boulder_sa_integration diff --git a/test/startservers.py b/test/startservers.py index 7c743e0f6..b0b6f83cb 100644 --- a/test/startservers.py +++ b/test/startservers.py @@ -63,6 +63,7 @@ def start(race_detection, fakeclock): progs.extend([ [8011, './bin/boulder-remoteva --config %s' % os.path.join(config_dir, "va-remote-a.json")], [8012, './bin/boulder-remoteva --config %s' % os.path.join(config_dir, "va-remote-b.json")], + [8020, './bin/bad-key-revoker --config %s' % os.path.join(config_dir, "bad-key-revoker.json")], ]) progs.extend([ [53, './bin/sd-test-srv --listen :53'], # Service discovery DNS server