Add integration test for when CRL entries are removed (#8084)
We already have an integration test showing that a serial does not show up on any CRL before its certificate has been revoked, and does show up afterwards. Extend that test to cover three new times: - shortly before the certificate expires, when the entry must still appear; - shortly after the certificate expires, when the entry must still appear; and - significantly after the certificate expires, when the entry may be removed. To facilitate this, augment the s3-test-srv with a new reset endpoint, so that the integration test can query the contents of only the most-recently-generated set of CRLs. I have confirmed that the new integration test fails with https://github.com/letsencrypt/boulder/pull/8072 reverted. Fixes https://github.com/letsencrypt/boulder/issues/8083
This commit is contained in:
parent
037c654d3d
commit
c0e31f9a4f
|
@ -3,6 +3,7 @@
|
|||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
|
@ -42,6 +43,12 @@ func runUpdater(t *testing.T, configFile string) {
|
|||
crlUpdaterMu.Lock()
|
||||
defer crlUpdaterMu.Unlock()
|
||||
|
||||
// Reset the s3-test-srv so that it only knows about serials contained in
|
||||
// this new batch of CRLs.
|
||||
resp, err := http.Post("http://localhost:4501/reset", "", bytes.NewReader([]byte{}))
|
||||
test.AssertNotError(t, err, "opening database connection")
|
||||
test.AssertEquals(t, resp.StatusCode, http.StatusOK)
|
||||
|
||||
// Reset the "leasedUntil" column so this can be done alongside other
|
||||
// updater runs without worrying about unclean state.
|
||||
fc := clock.NewFake()
|
||||
|
@ -123,13 +130,15 @@ func TestCRLUpdaterStartup(t *testing.T) {
|
|||
// that the correct number of properly-formed and validly-signed CRLs are sent
|
||||
// to our fake S3 service.
|
||||
func TestCRLPipeline(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Basic setup.
|
||||
configDir, ok := os.LookupEnv("BOULDER_CONFIG_DIR")
|
||||
test.Assert(t, ok, "failed to look up test config directory")
|
||||
configFile := path.Join(configDir, "crl-updater.json")
|
||||
|
||||
// Create a database connection so we can pretend to jump forward in time.
|
||||
db, err := sql.Open("mysql", vars.DBConnSAIntegrationFullPerms)
|
||||
test.AssertNotError(t, err, "creating database connection")
|
||||
|
||||
// Issue a test certificate and save its serial number.
|
||||
client, err := makeClient()
|
||||
test.AssertNotError(t, err, "creating acme client")
|
||||
|
@ -149,22 +158,54 @@ func TestCRLPipeline(t *testing.T) {
|
|||
err = client.RevokeCertificate(client.Account, cert, client.PrivateKey, 5)
|
||||
test.AssertNotError(t, err, "failed to revoke test certificate")
|
||||
|
||||
// Confirm that the cert now *does* show up in the CRLs.
|
||||
// Confirm that the cert now *does* show up in the CRLs, with the right reason.
|
||||
runUpdater(t, configFile)
|
||||
resp, err = http.Get("http://localhost:4501/query?serial=" + serial)
|
||||
test.AssertNotError(t, err, "s3-test-srv GET /query failed")
|
||||
test.AssertEquals(t, resp.StatusCode, 200)
|
||||
|
||||
// Confirm that the revoked certificate entry has the correct reason.
|
||||
reason, err := io.ReadAll(resp.Body)
|
||||
test.AssertNotError(t, err, "reading revocation reason")
|
||||
test.AssertEquals(t, string(reason), "5")
|
||||
resp.Body.Close()
|
||||
|
||||
// Manipulate the database so it appears that the certificate is going to
|
||||
// expire very soon. The cert should still appear on the CRL.
|
||||
_, err = db.Exec("UPDATE revokedCertificates SET notAfterHour = ? WHERE serial = ?", time.Now().Add(time.Hour).Truncate(time.Hour).Format(time.DateTime), serial)
|
||||
test.AssertNotError(t, err, "updating expiry to near future")
|
||||
runUpdater(t, configFile)
|
||||
resp, err = http.Get("http://localhost:4501/query?serial=" + serial)
|
||||
test.AssertNotError(t, err, "s3-test-srv GET /query failed")
|
||||
test.AssertEquals(t, resp.StatusCode, 200)
|
||||
reason, err = io.ReadAll(resp.Body)
|
||||
test.AssertNotError(t, err, "reading revocation reason")
|
||||
test.AssertEquals(t, string(reason), "5")
|
||||
resp.Body.Close()
|
||||
|
||||
// Again update the database so that the certificate has expired in the
|
||||
// very recent past. The cert should still appear on the CRL.
|
||||
_, err = db.Exec("UPDATE revokedCertificates SET notAfterHour = ? WHERE serial = ?", time.Now().Add(-time.Hour).Truncate(time.Hour).Format(time.DateTime), serial)
|
||||
test.AssertNotError(t, err, "updating expiry to recent past")
|
||||
runUpdater(t, configFile)
|
||||
resp, err = http.Get("http://localhost:4501/query?serial=" + serial)
|
||||
test.AssertNotError(t, err, "s3-test-srv GET /query failed")
|
||||
test.AssertEquals(t, resp.StatusCode, 200)
|
||||
reason, err = io.ReadAll(resp.Body)
|
||||
test.AssertNotError(t, err, "reading revocation reason")
|
||||
test.AssertEquals(t, string(reason), "5")
|
||||
resp.Body.Close()
|
||||
|
||||
// Finally update the database so that the certificate expired several CRL
|
||||
// update cycles ago. The cert should now vanish from the CRL.
|
||||
_, err = db.Exec("UPDATE revokedCertificates SET notAfterHour = ? WHERE serial = ?", time.Now().Add(-48*time.Hour).Truncate(time.Hour).Format(time.DateTime), serial)
|
||||
test.AssertNotError(t, err, "updating expiry to far past")
|
||||
runUpdater(t, configFile)
|
||||
resp, err = http.Get("http://localhost:4501/query?serial=" + serial)
|
||||
test.AssertNotError(t, err, "s3-test-srv GET /query failed")
|
||||
test.AssertEquals(t, resp.StatusCode, 404)
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
func TestCRLTemporalAndExplicitShardingCoexist(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := sql.Open("mysql", vars.DBConnSAIntegrationFullPerms)
|
||||
if err != nil {
|
||||
t.Fatalf("sql.Open: %s", err)
|
||||
|
|
|
@ -178,8 +178,6 @@ func checkRevoked(t *testing.T, revocations map[string][]*x509.RevocationList, c
|
|||
// precerts (with no corresponding final cert), and for both the Unspecified and
|
||||
// keyCompromise revocation reasons.
|
||||
func TestRevocation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type authMethod string
|
||||
var (
|
||||
byAccount authMethod = "byAccount"
|
||||
|
|
|
@ -27,21 +27,21 @@ func (srv *s3TestSrv) handleS3(w http.ResponseWriter, r *http.Request) {
|
|||
} else if r.Method == "GET" {
|
||||
srv.handleDownload(w, r)
|
||||
} else {
|
||||
w.WriteHeader(405)
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *s3TestSrv) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("failed to read request body"))
|
||||
return
|
||||
}
|
||||
|
||||
crl, err := x509.ParseRevocationList(body)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(fmt.Sprintf("failed to parse body: %s", err)))
|
||||
return
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ func (srv *s3TestSrv) handleUpload(w http.ResponseWriter, r *http.Request) {
|
|||
srv.allSerials[core.SerialToString(rc.SerialNumber)] = revocation.Reason(rc.ReasonCode)
|
||||
}
|
||||
|
||||
w.WriteHeader(200)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("{}"))
|
||||
}
|
||||
|
||||
|
@ -62,22 +62,22 @@ func (srv *s3TestSrv) handleDownload(w http.ResponseWriter, r *http.Request) {
|
|||
defer srv.RUnlock()
|
||||
body, ok := srv.allShards[r.URL.Path]
|
||||
if !ok {
|
||||
w.WriteHeader(404)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(body)
|
||||
}
|
||||
|
||||
func (srv *s3TestSrv) handleQuery(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
w.WriteHeader(405)
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
serial := r.URL.Query().Get("serial")
|
||||
if serial == "" {
|
||||
w.WriteHeader(400)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -85,14 +85,28 @@ func (srv *s3TestSrv) handleQuery(w http.ResponseWriter, r *http.Request) {
|
|||
defer srv.RUnlock()
|
||||
reason, ok := srv.allSerials[serial]
|
||||
if !ok {
|
||||
w.WriteHeader(404)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(200)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(fmt.Sprintf("%d", reason)))
|
||||
}
|
||||
|
||||
func (srv *s3TestSrv) handleReset(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
srv.Lock()
|
||||
defer srv.Unlock()
|
||||
srv.allSerials = make(map[string]revocation.Reason)
|
||||
srv.allShards = make(map[string][]byte)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func main() {
|
||||
listenAddr := flag.String("listen", "0.0.0.0:4501", "Address to listen on")
|
||||
flag.Parse()
|
||||
|
@ -104,6 +118,7 @@ func main() {
|
|||
|
||||
http.HandleFunc("/", srv.handleS3)
|
||||
http.HandleFunc("/query", srv.handleQuery)
|
||||
http.HandleFunc("/reset", srv.handleReset)
|
||||
|
||||
s := http.Server{
|
||||
ReadTimeout: 30 * time.Second,
|
||||
|
|
Loading…
Reference in New Issue