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  }