github.com/letsencrypt/boulder@v0.20251208.0/test/integration/crl_test.go (about) 1 //go:build integration 2 3 package integration 4 5 import ( 6 "bytes" 7 "context" 8 "database/sql" 9 "errors" 10 "io" 11 "net" 12 "net/http" 13 "os" 14 "os/exec" 15 "path" 16 "path/filepath" 17 "strings" 18 "sync" 19 "syscall" 20 "testing" 21 "time" 22 23 "github.com/eggsampler/acme/v3" 24 "github.com/jmhodges/clock" 25 26 "github.com/letsencrypt/boulder/core" 27 "github.com/letsencrypt/boulder/test" 28 "github.com/letsencrypt/boulder/test/vars" 29 ) 30 31 // crlUpdaterMu controls access to `runUpdater`, because two crl-updaters running 32 // at once will result in errors trying to lease shards that are already leased. 33 var crlUpdaterMu sync.Mutex 34 35 // runUpdater executes the crl-updater binary with the -runOnce flag, and 36 // returns when it completes. 37 func runUpdater(t *testing.T, configFile string) { 38 t.Helper() 39 crlUpdaterMu.Lock() 40 defer crlUpdaterMu.Unlock() 41 42 // Reset the s3-test-srv so that it only knows about serials contained in 43 // this new batch of CRLs. 44 resp, err := http.Post("http://localhost:4501/reset", "", bytes.NewReader([]byte{})) 45 test.AssertNotError(t, err, "opening database connection") 46 test.AssertEquals(t, resp.StatusCode, http.StatusOK) 47 48 // Reset the "leasedUntil" column so this can be done alongside other 49 // updater runs without worrying about unclean state. 50 fc := clock.NewFake() 51 db, err := sql.Open("mysql", vars.DBConnSAIntegrationFullPerms) 52 test.AssertNotError(t, err, "opening database connection") 53 _, err = db.Exec(`UPDATE crlShards SET leasedUntil = ?`, fc.Now().Add(-time.Minute)) 54 test.AssertNotError(t, err, "resetting leasedUntil column") 55 56 binPath, err := filepath.Abs("bin/boulder") 57 test.AssertNotError(t, err, "computing boulder binary path") 58 59 c := exec.Command(binPath, "crl-updater", "-config", configFile, "-debug-addr", ":8022", "-runOnce") 60 out, err := c.CombinedOutput() 61 for _, line := range strings.Split(string(out), "\n") { 62 // Print the updater's stdout for debugging, but only if the test fails. 63 t.Log(line) 64 } 65 test.AssertNotError(t, err, "crl-updater failed") 66 } 67 68 // TestCRLUpdaterStartup ensures that the crl-updater can start in daemon mode. 69 // We do this here instead of in startservers so that we can shut it down after 70 // we've confirmed it is running. It's important that it not be running while 71 // other CRL integration tests are running, because otherwise they fight over 72 // database leases, leading to flaky test failures. 73 func TestCRLUpdaterStartup(t *testing.T) { 74 t.Parallel() 75 76 crlUpdaterMu.Lock() 77 defer crlUpdaterMu.Unlock() 78 79 ctx, cancel := context.WithCancel(context.Background()) 80 81 binPath, err := filepath.Abs("bin/boulder") 82 test.AssertNotError(t, err, "computing boulder binary path") 83 84 configDir, ok := os.LookupEnv("BOULDER_CONFIG_DIR") 85 test.Assert(t, ok, "failed to look up test config directory") 86 configFile := path.Join(configDir, "crl-updater.json") 87 88 c := exec.CommandContext(ctx, binPath, "crl-updater", "-config", configFile, "-debug-addr", ":8021") 89 90 var wg sync.WaitGroup 91 wg.Add(1) 92 go func() { 93 out, err := c.CombinedOutput() 94 // Log the output and error, but only if the main goroutine couldn't connect 95 // and declared the test failed. 96 for _, line := range strings.Split(string(out), "\n") { 97 t.Log(line) 98 } 99 t.Log(err) 100 wg.Done() 101 }() 102 103 for attempt := range 10 { 104 time.Sleep(core.RetryBackoff(attempt, 10*time.Millisecond, 1*time.Second, 2)) 105 106 conn, err := net.DialTimeout("tcp", "localhost:8021", 100*time.Millisecond) 107 if errors.Is(err, syscall.ECONNREFUSED) { 108 t.Logf("Connection attempt %d failed: %s", attempt, err) 109 continue 110 } 111 if err != nil { 112 t.Logf("Connection attempt %d failed unrecoverably: %s", attempt, err) 113 t.Fail() 114 break 115 } 116 t.Logf("Connection attempt %d succeeded", attempt) 117 defer conn.Close() 118 break 119 } 120 121 cancel() 122 wg.Wait() 123 } 124 125 // TestCRLPipeline runs an end-to-end test of the crl issuance process, ensuring 126 // that the correct number of properly-formed and validly-signed CRLs are sent 127 // to our fake S3 service. 128 func TestCRLPipeline(t *testing.T) { 129 // Basic setup. 130 configDir, ok := os.LookupEnv("BOULDER_CONFIG_DIR") 131 test.Assert(t, ok, "failed to look up test config directory") 132 configFile := path.Join(configDir, "crl-updater.json") 133 134 // Create a database connection so we can pretend to jump forward in time. 135 db, err := sql.Open("mysql", vars.DBConnSAIntegrationFullPerms) 136 test.AssertNotError(t, err, "creating database connection") 137 138 // Issue a test certificate and save its serial number. 139 client, err := makeClient() 140 test.AssertNotError(t, err, "creating acme client") 141 res, err := authAndIssue(client, nil, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "") 142 test.AssertNotError(t, err, "failed to create test certificate") 143 cert := res.certs[0] 144 serial := core.SerialToString(cert.SerialNumber) 145 146 // Confirm that the cert does not yet show up as revoked in the CRLs. 147 runUpdater(t, configFile) 148 resp, err := http.Get("http://localhost:4501/query?serial=" + serial) 149 test.AssertNotError(t, err, "s3-test-srv GET /query failed") 150 test.AssertEquals(t, resp.StatusCode, 404) 151 resp.Body.Close() 152 153 // Revoke the certificate. 154 err = client.RevokeCertificate(client.Account, cert, client.PrivateKey, 5) 155 test.AssertNotError(t, err, "failed to revoke test certificate") 156 157 // Confirm that the cert now *does* show up in the CRLs, with the right reason. 158 runUpdater(t, configFile) 159 resp, err = http.Get("http://localhost:4501/query?serial=" + serial) 160 test.AssertNotError(t, err, "s3-test-srv GET /query failed") 161 test.AssertEquals(t, resp.StatusCode, 200) 162 reason, err := io.ReadAll(resp.Body) 163 test.AssertNotError(t, err, "reading revocation reason") 164 test.AssertEquals(t, string(reason), "5") 165 resp.Body.Close() 166 167 // Manipulate the database so it appears that the certificate is going to 168 // expire very soon. The cert should still appear on the CRL. 169 _, err = db.Exec("UPDATE revokedCertificates SET notAfterHour = ? WHERE serial = ?", time.Now().Add(time.Hour).Truncate(time.Hour).Format(time.DateTime), serial) 170 test.AssertNotError(t, err, "updating expiry to near future") 171 runUpdater(t, configFile) 172 resp, err = http.Get("http://localhost:4501/query?serial=" + serial) 173 test.AssertNotError(t, err, "s3-test-srv GET /query failed") 174 test.AssertEquals(t, resp.StatusCode, 200) 175 reason, err = io.ReadAll(resp.Body) 176 test.AssertNotError(t, err, "reading revocation reason") 177 test.AssertEquals(t, string(reason), "5") 178 resp.Body.Close() 179 180 // Again update the database so that the certificate has expired in the 181 // very recent past. The cert should still appear on the CRL. 182 _, err = db.Exec("UPDATE revokedCertificates SET notAfterHour = ? WHERE serial = ?", time.Now().Add(-time.Hour).Truncate(time.Hour).Format(time.DateTime), serial) 183 test.AssertNotError(t, err, "updating expiry to recent past") 184 runUpdater(t, configFile) 185 resp, err = http.Get("http://localhost:4501/query?serial=" + serial) 186 test.AssertNotError(t, err, "s3-test-srv GET /query failed") 187 test.AssertEquals(t, resp.StatusCode, 200) 188 reason, err = io.ReadAll(resp.Body) 189 test.AssertNotError(t, err, "reading revocation reason") 190 test.AssertEquals(t, string(reason), "5") 191 resp.Body.Close() 192 193 // Finally update the database so that the certificate expired several CRL 194 // update cycles ago. The cert should now vanish from the CRL. 195 _, err = db.Exec("UPDATE revokedCertificates SET notAfterHour = ? WHERE serial = ?", time.Now().Add(-48*time.Hour).Truncate(time.Hour).Format(time.DateTime), serial) 196 test.AssertNotError(t, err, "updating expiry to far past") 197 runUpdater(t, configFile) 198 resp, err = http.Get("http://localhost:4501/query?serial=" + serial) 199 test.AssertNotError(t, err, "s3-test-srv GET /query failed") 200 test.AssertEquals(t, resp.StatusCode, 404) 201 resp.Body.Close() 202 }