// +build integration package integration import ( "crypto" "crypto/ecdsa" "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" "golang.org/x/crypto/ocsp" ) // isPrecert returns true if the provided cert has an extension with the OID // equal to OIDExtensionCTPoison. func isPrecert(cert *x509.Certificate) bool { for _, ext := range cert.Extensions { if ext.Id.Equal(OIDExtensionCTPoison) { return true } } return false } // TestPrecertificateRevocation tests that a precertificate without a matching // certificate can be revoked using all of the available RFC 8555 revocation // authentication mechansims. func TestPrecertificateRevocation(t *testing.T) { t.Parallel() // Create a base account to use for revocation tests. os.Setenv("DIRECTORY", "http://boulder:4001/directory") c, err := makeClient("mailto:example@letsencrypt.org") test.AssertNotError(t, err, "creating acme client") // Create a specific key for CSRs so that it is possible to test revocation // with the cert key. certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "creating random cert key") // Create a second account to test revocation with an equally authorized account otherAccount, err := makeClient() test.AssertNotError(t, err, "creating second acme client") // Preauthorize a specific domain with the other account before it has been // added to the ct-test-srv reject list. preAuthDomain := random_domain() _, err = authAndIssue(otherAccount, nil, []string{preAuthDomain}) test.AssertNotError(t, err, "preauthorizing second acme client") testCases := []struct { name string domain string revokeClient *client revokeKey crypto.Signer }{ { name: "revocation by certificate key", revokeKey: certKey, }, { name: "revocation by owner account key", revokeKey: c.Account.PrivateKey, }, { name: "equivalently authorized account key", revokeClient: otherAccount, revokeKey: otherAccount.Account.PrivateKey, domain: preAuthDomain, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // If the test case didn't specify a domain make one up randomly if tc.domain == "" { tc.domain = random_domain() } // If the test case didn't specify a different client to use for // revocation use c. if tc.revokeClient == nil { tc.revokeClient = c } // Make sure the ct-test-srv will reject issuance for the domain err := ctAddRejectHost(tc.domain) test.AssertNotError(t, err, "adding ct-test-srv reject host") // Issue a certificate for the name using the `c` client. It should fail // because not enough SCTs can be collected, leaving a precert without // a matching final cert. _, err = authAndIssue(c, certKey, []string{tc.domain}) test.AssertError(t, err, "expected error from authAndIssue, was nil") if !strings.Contains(err.Error(), "urn:ietf:params:acme:error:serverInternal") || !strings.Contains(err.Error(), "SCT embedding") { t.Fatal(err) } // Try to find a precertificate matching the domain from one of the // configured ct-test-srv instances. cert, err := ctFindRejection([]string{tc.domain}) if err != nil || cert == nil { t.Fatalf("couldn't find rejected precert for %q", tc.domain) } // To be confident that we're testing the right thing also verify that the // rejection is a poisoned precertificate. if !isPrecert(cert) { t.Fatal("precert was missing poison extension") } // To start with the precertificate should have a Good OCSP response. ocspConfig := ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Good) _, err = ocsp_helper.ReqDER(cert.Raw, ocspConfig) test.AssertNotError(t, err, "requesting OCSP for precert") // Revoke the precertificate using the specified key and client err = tc.revokeClient.RevokeCertificate( tc.revokeClient.Account, cert, tc.revokeKey, ocsp.Unspecified) test.AssertNotError(t, err, "revoking precert") // Check the OCSP response for the precertificate again. It should now be // revoked. ocspConfig = ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Revoked) _, err = ocsp_helper.ReqDER(cert.Raw, ocspConfig) test.AssertNotError(t, err, "requesting OCSP for revoked precert") }) } } func TestRevokeWithKeyCompromise(t *testing.T) { t.Parallel() os.Setenv("DIRECTORY", "http://boulder:4001/directory") c, err := makeClient("mailto:example@letsencrypt.org") test.AssertNotError(t, err, "creating acme client") certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "failed to generate cert key") res, err := authAndIssue(c, certKey, []string{random_domain()}) test.AssertNotError(t, err, "authAndIssue failed") cert := res.certs[0] err = c.RevokeCertificate( c.Account, cert, c.Account.PrivateKey, ocsp.KeyCompromise, ) test.AssertNotError(t, err, "failed to revoke certificate") // attempt to create a new account using the blacklisted key _, err = c.NewAccount(certKey, false, true) 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`) // Check the OCSP response. It should be revoked with reason = 1 (keyCompromise) ocspConfig := ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Revoked) response, err := ocsp_helper.ReqDER(cert.Raw, ocspConfig) test.AssertNotError(t, err, "requesting OCSP for revoked cert") test.AssertEquals(t, response.RevocationReason, 1) } func TestBadKeyRevoker(t *testing.T) { t.Parallel() 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") ocspConfig := ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Revoked) _, err = ocsp_helper.ReqDER(badCert.certs[0].Raw, ocspConfig) test.AssertNotError(t, err, "ReqDER failed") for _, cert := range certs { for i := 0; i < 5; i++ { _, err = ocsp_helper.ReqDER(cert.Raw, ocspConfig) 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") }