github.com/letsencrypt/boulder@v0.20251208.0/test/integration/revocation_test.go (about)

     1  //go:build integration
     2  
     3  package integration
     4  
     5  import (
     6  	"crypto"
     7  	"crypto/ecdsa"
     8  	"crypto/elliptic"
     9  	"crypto/rand"
    10  	"crypto/x509"
    11  	"encoding/hex"
    12  	"encoding/json"
    13  	"encoding/pem"
    14  	"fmt"
    15  	"io"
    16  	"net/http"
    17  	"os"
    18  	"os/exec"
    19  	"path"
    20  	"strings"
    21  	"sync"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/eggsampler/acme/v3"
    26  
    27  	"github.com/letsencrypt/boulder/core"
    28  	"github.com/letsencrypt/boulder/crl/idp"
    29  	"github.com/letsencrypt/boulder/revocation"
    30  	"github.com/letsencrypt/boulder/test"
    31  )
    32  
    33  // isPrecert returns true if the provided cert has an extension with the OID
    34  // equal to OIDExtensionCTPoison.
    35  func isPrecert(cert *x509.Certificate) bool {
    36  	for _, ext := range cert.Extensions {
    37  		if ext.Id.Equal(OIDExtensionCTPoison) {
    38  			return true
    39  		}
    40  	}
    41  	return false
    42  }
    43  
    44  // getALLCRLs fetches and parses each certificate for each configured CA.
    45  // Returns a map from issuer SKID (hex) to a list of that issuer's CRLs.
    46  func getAllCRLs(t *testing.T) map[string][]*x509.RevocationList {
    47  	t.Helper()
    48  	b, err := os.ReadFile(path.Join(os.Getenv("BOULDER_CONFIG_DIR"), "ca.json"))
    49  	if err != nil {
    50  		t.Fatalf("reading CA config: %s", err)
    51  	}
    52  
    53  	var conf struct {
    54  		CA struct {
    55  			Issuance struct {
    56  				Issuers []struct {
    57  					CRLURLBase string
    58  					Location   struct {
    59  						CertFile string
    60  					}
    61  				}
    62  			}
    63  		}
    64  	}
    65  
    66  	err = json.Unmarshal(b, &conf)
    67  	if err != nil {
    68  		t.Fatalf("unmarshaling CA config: %s", err)
    69  	}
    70  
    71  	ret := make(map[string][]*x509.RevocationList)
    72  
    73  	for _, issuer := range conf.CA.Issuance.Issuers {
    74  		issuerPEMBytes, err := os.ReadFile(issuer.Location.CertFile)
    75  		if err != nil {
    76  			t.Fatalf("reading CRL issuer: %s", err)
    77  		}
    78  
    79  		block, _ := pem.Decode(issuerPEMBytes)
    80  		issuerCert, err := x509.ParseCertificate(block.Bytes)
    81  		if err != nil {
    82  			t.Fatalf("parsing CRL issuer: %s", err)
    83  		}
    84  
    85  		issuerSKID := hex.EncodeToString(issuerCert.SubjectKeyId)
    86  
    87  		// 10 is the number of shards configured in test/config*/crl-updater.json
    88  		for i := range 10 {
    89  			crlURL := fmt.Sprintf("%s%d.crl", issuer.CRLURLBase, i+1)
    90  			list := getCRL(t, crlURL, issuerCert)
    91  
    92  			ret[issuerSKID] = append(ret[issuerSKID], list)
    93  		}
    94  	}
    95  	return ret
    96  }
    97  
    98  // getCRL fetches a CRL, parses it, verifies that it has the correct IDP,
    99  // and checks the signature (if an issuer was provided).
   100  func getCRL(t *testing.T, crlURL string, issuerCert *x509.Certificate) *x509.RevocationList {
   101  	t.Helper()
   102  	resp, err := http.Get(crlURL)
   103  	if err != nil {
   104  		t.Fatalf("getting CRL from %s: %s", crlURL, err)
   105  	}
   106  	if resp.StatusCode != http.StatusOK {
   107  		t.Fatalf("fetching %s: status code %d", crlURL, resp.StatusCode)
   108  	}
   109  	body, err := io.ReadAll(resp.Body)
   110  	if err != nil {
   111  		t.Fatalf("reading CRL from %s: %s", crlURL, err)
   112  	}
   113  	resp.Body.Close()
   114  
   115  	list, err := x509.ParseRevocationList(body)
   116  	if err != nil {
   117  		t.Fatalf("parsing CRL from %s: %s (bytes: %x)", crlURL, err, body)
   118  	}
   119  
   120  	if issuerCert != nil {
   121  		err = list.CheckSignatureFrom(issuerCert)
   122  		if err != nil {
   123  			t.Errorf("checking CRL signature on %s from %s: %s",
   124  				crlURL, issuerCert.Subject, err)
   125  		}
   126  	}
   127  
   128  	idpURIs, err := idp.GetIDPURIs(list.Extensions)
   129  	if err != nil {
   130  		t.Fatalf("getting IDP URIs: %s", err)
   131  	}
   132  	if len(idpURIs) != 1 {
   133  		t.Errorf("CRL at %s: expected 1 IDP URI, got %s", crlURL, idpURIs)
   134  	}
   135  	if idpURIs[0] != crlURL {
   136  		t.Errorf("fetched CRL from %s, got IDP of %s (should be same)", crlURL, idpURIs[0])
   137  	}
   138  	return list
   139  }
   140  
   141  // waitAndCheckRevoked ensures that the given certificate appears on the correct
   142  // CRL with the desired reason. It is willing to repeatedly regenerate CRLs up
   143  // to four times, and wait up to 5 seconds, before reporting failure.
   144  //
   145  // The issuer argument is optional: it is used to verify the signature on the
   146  // fetched CRL, but is not always available in our tests (e.g. if the finalize
   147  // call purposefully failed so no chain file was provided).
   148  func waitAndCheckRevoked(t *testing.T, cert *x509.Certificate, issuer *x509.Certificate, wantReason revocation.Reason) {
   149  	t.Helper()
   150  
   151  	if len(cert.CRLDistributionPoints) != 1 {
   152  		t.Errorf("expected certificate to have one CRLDistributionPoints field")
   153  	}
   154  	crlURL := cert.CRLDistributionPoints[0]
   155  
   156  	for try := range 4 {
   157  		time.Sleep(core.RetryBackoff(try, time.Second, 2*time.Second, 1.5))
   158  
   159  		// These steps can terminate the loop early, but that's okay, because
   160  		// failing to generate or fetch CRLs is a more fundamental error than
   161  		// whatever behavior the test is actually looking for.
   162  		runUpdater(t, path.Join(os.Getenv("BOULDER_CONFIG_DIR"), "crl-updater.json"))
   163  
   164  		list := getCRL(t, crlURL, issuer)
   165  		var reasons []revocation.Reason
   166  		for _, entry := range list.RevokedCertificateEntries {
   167  			if entry.SerialNumber.Cmp(cert.SerialNumber) == 0 {
   168  				reasons = append(reasons, revocation.Reason(entry.ReasonCode))
   169  			}
   170  		}
   171  
   172  		if len(reasons) == 1 && reasons[0] == wantReason {
   173  			// Success, the cert was revoked for the correct reason.
   174  			return
   175  		} else if len(reasons) == 1 && reasons[0] != wantReason {
   176  			// We're okay terminating the loop early because an incorrect revocation
   177  			// reason should never happen.
   178  			t.Fatalf("found %x revoked with reason %d, but want reason %d", cert.SerialNumber, reasons[0], wantReason)
   179  		} else if len(reasons) > 1 {
   180  			// We're okay terminating the loop early because multiple entries for the
   181  			// same cert should never happen.
   182  			t.Fatalf("found multiple CRL entries for %x", cert.SerialNumber)
   183  		}
   184  	}
   185  
   186  	t.Errorf("no CRL entry found for %x", cert.SerialNumber)
   187  }
   188  
   189  func checkUnrevoked(t *testing.T, revocations map[string][]*x509.RevocationList, cert *x509.Certificate) {
   190  	t.Helper()
   191  	for _, singleIssuerCRLs := range revocations {
   192  		for _, crl := range singleIssuerCRLs {
   193  			for _, entry := range crl.RevokedCertificateEntries {
   194  				if entry.SerialNumber == cert.SerialNumber {
   195  					t.Errorf("expected %x to be unrevoked, but found it on a CRL", cert.SerialNumber)
   196  				}
   197  			}
   198  		}
   199  	}
   200  }
   201  
   202  func checkRevoked(t *testing.T, revocations map[string][]*x509.RevocationList, cert *x509.Certificate, expectedReason revocation.Reason) {
   203  	t.Helper()
   204  	akid := hex.EncodeToString(cert.AuthorityKeyId)
   205  	if len(revocations[akid]) == 0 {
   206  		t.Errorf("no CRLs found for authorityKeyID %s", akid)
   207  	}
   208  	var matchingCRLs []string
   209  	var count int
   210  	for _, list := range revocations[akid] {
   211  		for _, entry := range list.RevokedCertificateEntries {
   212  			count++
   213  			if entry.SerialNumber.Cmp(cert.SerialNumber) == 0 {
   214  				idpURIs, err := idp.GetIDPURIs(list.Extensions)
   215  				if err != nil {
   216  					t.Errorf("getting IDP URIs: %s", err)
   217  				}
   218  				idpURI := idpURIs[0]
   219  				if revocation.Reason(entry.ReasonCode) != expectedReason {
   220  					t.Errorf("revoked certificate %x in CRL %s: revocation reason %d, want %d", cert.SerialNumber, idpURI, entry.ReasonCode, expectedReason)
   221  				}
   222  				matchingCRLs = append(matchingCRLs, idpURI)
   223  			}
   224  		}
   225  	}
   226  	if len(matchingCRLs) == 0 {
   227  		t.Errorf("searching for %x in CRLs: no entry on combined CRLs of length %d", cert.SerialNumber, count)
   228  	}
   229  
   230  	// If the cert has a CRLDP, it must be listed on the CRL served at that URL.
   231  	if len(cert.CRLDistributionPoints) > 0 {
   232  		expectedCRLDP := cert.CRLDistributionPoints[0]
   233  		found := false
   234  		for _, crl := range matchingCRLs {
   235  			if crl == expectedCRLDP {
   236  				found = true
   237  			}
   238  		}
   239  		if !found {
   240  			t.Errorf("revoked certificate %x: seen on CRLs %s, want to see on CRL %s", cert.SerialNumber, matchingCRLs, expectedCRLDP)
   241  		}
   242  	}
   243  }
   244  
   245  // TestRevocation tests that a certificate can be revoked using all of the
   246  // RFC 8555 revocation authentication mechanisms. It does so for both certs and
   247  // precerts (with no corresponding final cert), and for both the Unspecified and
   248  // keyCompromise revocation reasons.
   249  func TestRevocation(t *testing.T) {
   250  	type authMethod string
   251  	var (
   252  		byAccount authMethod = "byAccount"
   253  		byAuth    authMethod = "byAuth"
   254  		byKey     authMethod = "byKey"
   255  		byAdmin   authMethod = "byAdmin"
   256  	)
   257  
   258  	type certKind string
   259  	var (
   260  		finalcert certKind = "cert"
   261  		precert   certKind = "precert"
   262  	)
   263  
   264  	type testCase struct {
   265  		method authMethod
   266  		reason revocation.Reason
   267  		kind   certKind
   268  	}
   269  
   270  	issueAndRevoke := func(tc testCase) *x509.Certificate {
   271  		issueClient, err := makeClient()
   272  		test.AssertNotError(t, err, "creating acme client")
   273  
   274  		certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   275  		test.AssertNotError(t, err, "creating random cert key")
   276  
   277  		domain := random_domain()
   278  
   279  		// Try to issue a certificate for the name.
   280  		var cert *x509.Certificate
   281  		switch tc.kind {
   282  		case finalcert:
   283  			res, err := authAndIssue(issueClient, certKey, []acme.Identifier{{Type: "dns", Value: domain}}, true, "")
   284  			test.AssertNotError(t, err, "authAndIssue failed")
   285  			cert = res.certs[0]
   286  
   287  		case precert:
   288  			// Make sure the ct-test-srv will reject generating SCTs for the domain,
   289  			// so we only get a precert and no final cert.
   290  			err := ctAddRejectHost(domain)
   291  			test.AssertNotError(t, err, "adding ct-test-srv reject host")
   292  
   293  			_, err = authAndIssue(issueClient, certKey, []acme.Identifier{{Type: "dns", Value: domain}}, true, "")
   294  			test.AssertError(t, err, "expected error from authAndIssue, was nil")
   295  			if !strings.Contains(err.Error(), "urn:ietf:params:acme:error:serverInternal") ||
   296  				!strings.Contains(err.Error(), "SCT embedding") {
   297  				t.Fatal(err)
   298  			}
   299  
   300  			// Instead recover the precertificate from CT.
   301  			cert, err = ctFindRejection([]string{domain})
   302  			if err != nil || cert == nil {
   303  				t.Fatalf("couldn't find rejected precert for %q", domain)
   304  			}
   305  			// And make sure the cert we found is in fact a precert.
   306  			if !isPrecert(cert) {
   307  				t.Fatal("precert was missing poison extension")
   308  			}
   309  
   310  		default:
   311  			t.Fatalf("unrecognized cert kind %q", tc.kind)
   312  		}
   313  
   314  		// Set up the account and key that we'll use to revoke the cert.
   315  		switch tc.method {
   316  		case byAccount:
   317  			// When revoking by account, use the same client and key as were used
   318  			// for the original issuance.
   319  			err = issueClient.RevokeCertificate(
   320  				issueClient.Account,
   321  				cert,
   322  				issueClient.PrivateKey,
   323  				int(tc.reason),
   324  			)
   325  			test.AssertNotError(t, err, "revocation should have succeeded")
   326  
   327  		case byAuth:
   328  			// When revoking by auth, create a brand new client, authorize it for
   329  			// the same domain, and use that account and key for revocation. Ignore
   330  			// errors from authAndIssue because all we need is the auth, not the
   331  			// issuance.
   332  			newClient, err := makeClient()
   333  			test.AssertNotError(t, err, "creating second acme client")
   334  			_, _ = authAndIssue(newClient, certKey, []acme.Identifier{{Type: "dns", Value: domain}}, true, "")
   335  
   336  			err = newClient.RevokeCertificate(
   337  				newClient.Account,
   338  				cert,
   339  				newClient.PrivateKey,
   340  				int(tc.reason),
   341  			)
   342  			test.AssertNotError(t, err, "revocation should have succeeded")
   343  
   344  		case byKey:
   345  			// When revoking by key, create a brand new client and use it with
   346  			// the cert's key for revocation.
   347  			newClient, err := makeClient()
   348  			test.AssertNotError(t, err, "creating second acme client")
   349  			err = newClient.RevokeCertificate(
   350  				newClient.Account,
   351  				cert,
   352  				certKey,
   353  				int(tc.reason),
   354  			)
   355  			test.AssertNotError(t, err, "revocation should have succeeded")
   356  
   357  		case byAdmin:
   358  			// Invoke the admin tool to perform the revocation via gRPC, rather than
   359  			// using the external-facing ACME API.
   360  			config := fmt.Sprintf("%s/%s", os.Getenv("BOULDER_CONFIG_DIR"), "admin.json")
   361  			cmd := exec.Command(
   362  				"./bin/admin",
   363  				"-config", config,
   364  				"-dry-run=false",
   365  				"revoke-cert",
   366  				"-serial", core.SerialToString(cert.SerialNumber),
   367  				"-reason", tc.reason.String())
   368  			output, err := cmd.CombinedOutput()
   369  			t.Logf("admin revoke-cert output: %s\n", string(output))
   370  			test.AssertNotError(t, err, "revocation should have succeeded")
   371  
   372  		default:
   373  			t.Fatalf("unrecognized revocation method %q", tc.method)
   374  		}
   375  
   376  		return cert
   377  	}
   378  
   379  	// revocationCheck represents a deferred that a specific certificate is revoked.
   380  	//
   381  	// We defer these checks for performance reasons: we want to run crl-updater once,
   382  	// after all certificates have been revoked.
   383  	type revocationCheck func(t *testing.T, allCRLs map[string][]*x509.RevocationList)
   384  	var revocationChecks []revocationCheck
   385  	var rcMu sync.Mutex
   386  	var wg sync.WaitGroup
   387  
   388  	for _, kind := range []certKind{precert, finalcert} {
   389  		for _, reason := range []revocation.Reason{revocation.Unspecified, revocation.KeyCompromise, revocation.Superseded} {
   390  			for _, method := range []authMethod{byAccount, byAuth, byKey, byAdmin} {
   391  				wg.Add(1)
   392  				go func() {
   393  					defer wg.Done()
   394  					cert := issueAndRevoke(testCase{
   395  						method: method,
   396  						reason: reason,
   397  						kind:   kind,
   398  						// We do not expect any of these revocation requests to error.
   399  						// The ones done byAccount will succeed as requested, but will not
   400  						// result in the key being blocked for future issuance.
   401  						// The ones done byAuth will succeed, but will be overwritten to have
   402  						// reason code 5 (cessationOfOperation).
   403  						// The ones done byKey will succeed, but will be overwritten to have
   404  						// reason code 1 (keyCompromise), and will block the key.
   405  					})
   406  
   407  					// If the request was made by demonstrating control over the
   408  					// names, the reason should be overwritten to CessationOfOperation (5),
   409  					// and if the request was made by key, then the reason should be set to
   410  					// KeyCompromise (1).
   411  					expectedReason := reason
   412  					switch method {
   413  					case byAuth:
   414  						expectedReason = revocation.CessationOfOperation
   415  					case byKey:
   416  						expectedReason = revocation.KeyCompromise
   417  					default:
   418  					}
   419  
   420  					check := func(t *testing.T, allCRLs map[string][]*x509.RevocationList) {
   421  						checkRevoked(t, allCRLs, cert, expectedReason)
   422  					}
   423  
   424  					rcMu.Lock()
   425  					revocationChecks = append(revocationChecks, check)
   426  					rcMu.Unlock()
   427  				}()
   428  			}
   429  		}
   430  	}
   431  
   432  	wg.Wait()
   433  
   434  	runUpdater(t, path.Join(os.Getenv("BOULDER_CONFIG_DIR"), "crl-updater.json"))
   435  	allCRLs := getAllCRLs(t)
   436  
   437  	for _, check := range revocationChecks {
   438  		check(t, allCRLs)
   439  	}
   440  }
   441  
   442  // TestReRevocation verifies that a certificate can have its revocation
   443  // information updated only when both of the following are true:
   444  // a) The certificate was not initially revoked for reason keyCompromise; and
   445  // b) The second request is authenticated using the cert's keypair.
   446  // In which case the revocation reason (but not revocation date) will be
   447  // updated to be keyCompromise.
   448  func TestReRevocation(t *testing.T) {
   449  	type authMethod string
   450  	var (
   451  		byAccount authMethod = "byAccount"
   452  		byKey     authMethod = "byKey"
   453  	)
   454  
   455  	type testCase struct {
   456  		method1     authMethod
   457  		reason1     revocation.Reason
   458  		method2     authMethod
   459  		reason2     revocation.Reason
   460  		expectError bool
   461  	}
   462  
   463  	testCases := []testCase{
   464  		{method1: byAccount, reason1: revocation.Unspecified, method2: byAccount, reason2: revocation.Unspecified, expectError: true},
   465  		{method1: byAccount, reason1: revocation.KeyCompromise, method2: byAccount, reason2: revocation.KeyCompromise, expectError: true},
   466  		{method1: byAccount, reason1: revocation.Unspecified, method2: byKey, reason2: revocation.KeyCompromise, expectError: false},
   467  		{method1: byAccount, reason1: revocation.KeyCompromise, method2: byKey, reason2: revocation.KeyCompromise, expectError: true},
   468  		{method1: byKey, reason1: revocation.KeyCompromise, method2: byKey, reason2: revocation.KeyCompromise, expectError: true},
   469  	}
   470  
   471  	for i, tc := range testCases {
   472  		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
   473  			issueClient, err := makeClient()
   474  			test.AssertNotError(t, err, "creating acme client")
   475  
   476  			certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   477  			test.AssertNotError(t, err, "creating random cert key")
   478  
   479  			// Try to issue a certificate for the name.
   480  			res, err := authAndIssue(issueClient, certKey, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "")
   481  			test.AssertNotError(t, err, "authAndIssue failed")
   482  			cert := res.certs[0]
   483  			issuer := res.certs[1]
   484  
   485  			// Set up the account and key that we'll use to revoke the cert.
   486  			var revokeClient *client
   487  			var revokeKey crypto.Signer
   488  			switch tc.method1 {
   489  			case byAccount:
   490  				// When revoking by account, use the same client and key as were used
   491  				// for the original issuance.
   492  				revokeClient = issueClient
   493  				revokeKey = revokeClient.PrivateKey
   494  
   495  			case byKey:
   496  				// When revoking by key, create a brand new client and use it with
   497  				// the cert's key for revocation.
   498  				revokeClient, err = makeClient()
   499  				test.AssertNotError(t, err, "creating second acme client")
   500  				revokeKey = certKey
   501  
   502  			default:
   503  				t.Fatalf("unrecognized revocation method %q", tc.method1)
   504  			}
   505  
   506  			// Revoke the cert using the specified key and client.
   507  			err = revokeClient.RevokeCertificate(
   508  				revokeClient.Account,
   509  				cert,
   510  				revokeKey,
   511  				int(tc.reason1),
   512  			)
   513  			test.AssertNotError(t, err, "initial revocation should have succeeded")
   514  
   515  			// Check the CRL for the certificate again. It should now be
   516  			// revoked.
   517  			waitAndCheckRevoked(t, cert, issuer, tc.reason1)
   518  
   519  			// Set up the account and key that we'll use to *re*-revoke the cert.
   520  			switch tc.method2 {
   521  			case byAccount:
   522  				// When revoking by account, use the same client and key as were used
   523  				// for the original issuance.
   524  				revokeClient = issueClient
   525  				revokeKey = revokeClient.PrivateKey
   526  
   527  			case byKey:
   528  				// When revoking by key, create a brand new client and use it with
   529  				// the cert's key for revocation.
   530  				revokeClient, err = makeClient()
   531  				test.AssertNotError(t, err, "creating second acme client")
   532  				revokeKey = certKey
   533  
   534  			default:
   535  				t.Fatalf("unrecognized revocation method %q", tc.method2)
   536  			}
   537  
   538  			// Re-revoke the cert using the specified key and client.
   539  			err = revokeClient.RevokeCertificate(
   540  				revokeClient.Account,
   541  				cert,
   542  				revokeKey,
   543  				int(tc.reason2),
   544  			)
   545  
   546  			switch tc.expectError {
   547  			case true:
   548  				test.AssertError(t, err, "second revocation should have failed")
   549  
   550  				// Check the CRL for the certificate again. It should still be
   551  				// revoked, with the same reason.
   552  				waitAndCheckRevoked(t, cert, issuer, tc.reason1)
   553  
   554  			case false:
   555  				test.AssertNotError(t, err, "second revocation should have succeeded")
   556  
   557  				// Check the CRL for the certificate again. It should now be
   558  				// revoked with reason keyCompromise.
   559  				waitAndCheckRevoked(t, cert, issuer, tc.reason2)
   560  			}
   561  		})
   562  	}
   563  }
   564  
   565  func TestRevokeWithKeyCompromiseBlocksKey(t *testing.T) {
   566  	type authMethod string
   567  	var (
   568  		byAccount authMethod = "byAccount"
   569  		byKey     authMethod = "byKey"
   570  	)
   571  
   572  	// Test keyCompromise revocation both when revoking by certificate key and
   573  	// revoking by subscriber key. Both should work, although with slightly
   574  	// different behavior.
   575  	for _, method := range []authMethod{byKey, byAccount} {
   576  		c, err := makeClient("mailto:example@letsencrypt.org")
   577  		test.AssertNotError(t, err, "creating acme client")
   578  
   579  		certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   580  		test.AssertNotError(t, err, "failed to generate cert key")
   581  
   582  		res, err := authAndIssue(c, certKey, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "")
   583  		test.AssertNotError(t, err, "authAndIssue failed")
   584  		cert := res.certs[0]
   585  		issuer := res.certs[1]
   586  
   587  		// Revoke the cert with reason keyCompromise, either authenticated via the
   588  		// issuing account, or via the certificate key itself.
   589  		switch method {
   590  		case byAccount:
   591  			err = c.RevokeCertificate(c.Account, cert, c.PrivateKey, int(revocation.KeyCompromise))
   592  		case byKey:
   593  			err = c.RevokeCertificate(acme.Account{}, cert, certKey, int(revocation.KeyCompromise))
   594  		}
   595  		test.AssertNotError(t, err, "failed to revoke certificate")
   596  
   597  		// Check the CRL. It should be revoked with reason = 1 (keyCompromise).
   598  		waitAndCheckRevoked(t, cert, issuer, revocation.KeyCompromise)
   599  
   600  		// Attempt to create a new account using the compromised key. This should
   601  		// work when the key was just *reported* as compromised, but fail when
   602  		// the compromise was demonstrated/proven.
   603  		_, err = c.NewAccount(certKey, false, true)
   604  		switch method {
   605  		case byAccount:
   606  			test.AssertNotError(t, err, "NewAccount failed with a non-blocklisted key")
   607  		case byKey:
   608  			test.AssertError(t, err, "NewAccount didn't fail with a blocklisted key")
   609  			test.AssertEquals(t, err.Error(), `acme: error code 400 "urn:ietf:params:acme:error:badPublicKey": Unable to validate JWS :: invalid request signing key: public key is forbidden`)
   610  		}
   611  	}
   612  }
   613  
   614  func TestBadKeyRevoker(t *testing.T) {
   615  	revokerClient, err := makeClient()
   616  	test.AssertNotError(t, err, "creating acme client")
   617  	revokeeClient, err := makeClient()
   618  	test.AssertNotError(t, err, "creating acme client")
   619  	neutralClient, err := makeClient()
   620  	test.AssertNotError(t, err, "creating acme client")
   621  
   622  	certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   623  	test.AssertNotError(t, err, "failed to generate cert key")
   624  
   625  	// Issue a cert from the revokee client, which we'll revoke soon
   626  	toBeRevoked, err := authAndIssue(revokeeClient, certKey, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "")
   627  	test.AssertNotError(t, err, "authAndIssue failed")
   628  	t.Logf("Generated to-be-revoked cert with serial %x", toBeRevoked.certs[0].SerialNumber)
   629  
   630  	// Issue two more certs from two more accounts, one of which we'll use to
   631  	// revoke the original cert.
   632  	bundles := []*issuanceResult{}
   633  	for _, c := range []*client{revokerClient, neutralClient} {
   634  		bundle, err := authAndIssue(c, certKey, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "")
   635  		test.AssertNotError(t, err, "authAndIssue failed")
   636  		t.Logf("TestBadKeyRevoker: Issued cert with serial %x", bundle.certs[0].SerialNumber)
   637  		bundles = append(bundles, bundle)
   638  	}
   639  
   640  	// Sign the revocation request using the certificate key, so we treat it as
   641  	// a demonstration of compromise and cascade the revocation.
   642  	err = revokerClient.RevokeCertificate(
   643  		acme.Account{},
   644  		toBeRevoked.certs[0],
   645  		certKey,
   646  		int(revocation.KeyCompromise),
   647  	)
   648  	test.AssertNotError(t, err, "failed to revoke certificate")
   649  
   650  	waitAndCheckRevoked(t, toBeRevoked.certs[0], toBeRevoked.certs[1], revocation.KeyCompromise)
   651  
   652  	for _, bundle := range bundles {
   653  		waitAndCheckRevoked(t, bundle.certs[0], bundle.certs[1], revocation.KeyCompromise)
   654  	}
   655  }
   656  
   657  func TestBadKeyRevokerByAccount(t *testing.T) {
   658  	revokeClient, err := makeClient()
   659  	test.AssertNotError(t, err, "creating acme client")
   660  	neutralClient, err := makeClient()
   661  	test.AssertNotError(t, err, "creating acme client")
   662  
   663  	certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   664  	test.AssertNotError(t, err, "failed to generate cert key")
   665  
   666  	// Issue a cert from the revoke client, which we'll revoke soon
   667  	toBeRevoked, err := authAndIssue(revokeClient, certKey, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "")
   668  	test.AssertNotError(t, err, "authAndIssue failed")
   669  	t.Logf("Generated to-be-revoked cert with serial %x", toBeRevoked.certs[0].SerialNumber)
   670  
   671  	// Issue two more certs, one from the original account and one from an
   672  	// unrelated account. We don't use separatze revoker/revokee accounts here
   673  	// because you can only revoke *your own* certs when signing the request with
   674  	// your account key.
   675  	bundles := []*issuanceResult{}
   676  	for _, c := range []*client{revokeClient, neutralClient} {
   677  		bundle, err := authAndIssue(c, certKey, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "")
   678  		test.AssertNotError(t, err, "authAndIssue failed")
   679  		t.Logf("TestBadKeyRevokerByAccount: Issued cert with serial %x", bundle.certs[0].SerialNumber)
   680  		bundles = append(bundles, bundle)
   681  	}
   682  
   683  	// Sign the revocation request using the revokeClient's account key, so we
   684  	// don't treat it as a demonstration of compromise and don't cascade it.
   685  	err = revokeClient.RevokeCertificate(
   686  		revokeClient.Account,
   687  		toBeRevoked.certs[0],
   688  		revokeClient.PrivateKey,
   689  		int(revocation.KeyCompromise),
   690  	)
   691  	test.AssertNotError(t, err, "failed to revoke certificate")
   692  
   693  	waitAndCheckRevoked(t, toBeRevoked.certs[0], toBeRevoked.certs[1], revocation.KeyCompromise)
   694  
   695  	allCRLs := getAllCRLs(t)
   696  	for _, bundle := range bundles {
   697  		checkUnrevoked(t, allCRLs, bundle.certs[0])
   698  	}
   699  }