From 2b4a688bf91e7b663aef80138937ab5879754ead Mon Sep 17 00:00:00 2001 From: Kenneth Jenkins <51246568+kenjenkins@users.noreply.github.com> Date: Tue, 28 Nov 2023 20:26:51 -0800 Subject: [PATCH] Add OCSP stapling unit tests (#259) Add a few tests to exercise OCSP stapling, using an httptest.Server acting as the OCSP responder. These tests are very simplistic: - a "good" OCSP response should be stapled - a "revoked" OCSP response should not be stapled - the DisableStapling option should be honored - OCSP stapling requires an issuing certificate No attempt is made to provide sensible timestamps, either in the test certificates or in the OCSP responses. This brings unit test coverage for ocsp.go from 21% to 70%. --- ocsp_test.go | 173 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 158 insertions(+), 15 deletions(-) diff --git a/ocsp_test.go b/ocsp_test.go index f50a4fef..4c3df27e 100644 --- a/ocsp_test.go +++ b/ocsp_test.go @@ -1,39 +1,182 @@ package certmagic import ( + "bytes" "context" + "crypto" "errors" + "io" + "net/http" + "net/http/httptest" "testing" + + "golang.org/x/crypto/ocsp" ) -// certWithoutOCSPServer is a minimal self-signed certificate. +const certWithOCSPServer = `-----BEGIN CERTIFICATE----- +MIIBgjCCASegAwIBAgICIAAwCgYIKoZIzj0EAwIwEjEQMA4GA1UEAxMHVGVzdCBD +QTAeFw0yMzAxMDExMjAwMDBaFw0yMzAyMDExMjAwMDBaMCAxHjAcBgNVBAMTFU9D +U1AgVGVzdCBDZXJ0aWZpY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIoe +I/bjo34qony8LdRJD+Jhuk8/S8YHXRHl6rH9t5VFCFtX8lIPN/Ll1zCrQ2KB3Wlb +fxSgiQyLrCpZyrdhVPSjXzBdMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAU+Eo3 +5sST4LRrwS4dueIdGBZ5d7IwLAYIKwYBBQUHAQEEIDAeMBwGCCsGAQUFBzABhhBv +Y3NwLmV4YW1wbGUuY29tMAoGCCqGSM49BAMCA0kAMEYCIQDg94xY/+/VepESdvTT +ykCwiWOS2aCpjyryrKpwMKkR0AIhAPc/+ZEz4W10OENxC1t+NUTvS8JbEGOwulkZ +z9yfaLuD +-----END CERTIFICATE-----` + const certWithoutOCSPServer = `-----BEGIN CERTIFICATE----- -MIIBEDCBtqADAgECAgEBMAoGCCqGSM49BAMCMAAwIhgPMDAwMTAxMDEwMDAwMDBa -GA8wMDAxMDEwMTAwMDAwMFowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJ0p -7FKiv9p5rMMzntQeEBesKQnFR4XYFZ/SVlgJHFzd/QZ2sSxW+Mlbz78TTp4DMMIZ -J0z/Tw2+6fWdvoCYCW2jHTAbMBkGA1UdEQEB/wQPMA2CC2V4YW1wbGUuY29tMAoG -CCqGSM49BAMCA0kAMEYCIQDMbDvbJ/SXgRoblhBmt80F5iAyuOA0v20x0gpImK01 -oQIhANxdGJPvBaz0wOVBCSpd5jHbPxPxwqKZYJEes6y7eM+I +MIIBUzCB+aADAgECAgIgADAKBggqhkjOPQQDAjASMRAwDgYDVQQDEwdUZXN0IENB +MB4XDTIzMDEwMTEyMDAwMFoXDTIzMDIwMTEyMDAwMFowIDEeMBwGA1UEAxMVT0NT +UCBUZXN0IENlcnRpZmljYXRlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEih4j +9uOjfiqifLwt1EkP4mG6Tz9LxgddEeXqsf23lUUIW1fyUg838uXXMKtDYoHdaVt/ +FKCJDIusKlnKt2FU9KMxMC8wDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBT4Sjfm +xJPgtGvBLh254h0YFnl3sjAKBggqhkjOPQQDAgNJADBGAiEA3rWetLGblfSuNZKf +5CpZxhj3A0BjEocEh+2P+nAgIdUCIQDIgptabR1qTLQaF2u0hJsEX2IKuIUvYWH3 +6Lb92+zIHg== -----END CERTIFICATE-----` -const privateKey = `-----BEGIN EC PRIVATE KEY----- +// certKey is the private key for both certWithOCSPServer and +// certWithoutOCSPServer. +const certKey = `-----BEGIN EC PRIVATE KEY----- +MHcCAQEEINnVcgrSNh4HlThWlZpegq14M8G/p9NVDtdVjZrseUGLoAoGCCqGSM49 +AwEHoUQDQgAEih4j9uOjfiqifLwt1EkP4mG6Tz9LxgddEeXqsf23lUUIW1fyUg83 +8uXXMKtDYoHdaVt/FKCJDIusKlnKt2FU9A== +-----END EC PRIVATE KEY-----` + +// caCert is the issuing certificate for certWithOCSPServer and +// certWithoutOCSPServer. +const caCert = `-----BEGIN CERTIFICATE----- +MIIBazCCARGgAwIBAgICEAAwCgYIKoZIzj0EAwIwEjEQMA4GA1UEAxMHVGVzdCBD +QTAeFw0yMzAxMDExMjAwMDBaFw0yMzAyMDExMjAwMDBaMBIxEDAOBgNVBAMTB1Rl +c3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASdKexSor/aeazDM57UHhAX +rCkJxUeF2BWf0lZYCRxc3f0GdrEsVvjJW8+/E06eAzDCGSdM/08Nvun1nb6AmAlt +o1cwVTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwkwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQU+Eo35sST4LRrwS4dueIdGBZ5d7IwCgYIKoZI +zj0EAwIDSAAwRQIgGbA39+kETTB/YMLBFoC2fpZe1cDWfFB7TUdfINUqdH4CIQCR +ByUFC8A+hRNkK5YNH78bgjnKk/88zUQF5ONy4oPGdQ== +-----END CERTIFICATE-----` + +const caKey = `-----BEGIN EC PRIVATE KEY----- MHcCAQEEIDJ59ptjq3MzILH4zn5IKoH1sYn+zrUeq2kD8+DD2x+OoAoGCCqGSM49 AwEHoUQDQgAEnSnsUqK/2nmswzOe1B4QF6wpCcVHhdgVn9JWWAkcXN39BnaxLFb4 yVvPvxNOngMwwhknTP9PDb7p9Z2+gJgJbQ== -----END EC PRIVATE KEY-----` -func TestOCSPServerNotSpecified(t *testing.T) { - var config OCSPConfig +func TestStapleOCSP(t *testing.T) { + ctx := context.Background() storage := &FileStorage{Path: t.TempDir()} - pemCert := []byte(certWithoutOCSPServer) - cert, err := makeCertificate(pemCert, []byte(privateKey)) + t.Run("disabled", func(t *testing.T) { + cert := mustMakeCertificate(t, certWithOCSPServer, certKey) + config := OCSPConfig{DisableStapling: true} + err := stapleOCSP(ctx, config, storage, &cert, nil) + if err != nil { + t.Error("unexpected error:", err) + } else if cert.Certificate.OCSPStaple != nil { + t.Error("unexpected OCSP staple") + } + }) + t.Run("no OCSP server", func(t *testing.T) { + cert := mustMakeCertificate(t, certWithoutOCSPServer, certKey) + err := stapleOCSP(ctx, OCSPConfig{}, storage, &cert, nil) + if !errors.Is(err, ErrNoOCSPServerSpecified) { + t.Error("expected ErrNoOCSPServerSpecified in error", err) + } + }) + + // Start an OCSP responder test server. + responses := make(map[string][]byte) + responder := startOCSPResponder(t, responses) + t.Cleanup(responder.Close) + + ca := mustMakeCertificate(t, caCert, caKey) + + // The certWithOCSPServer certificate has a bogus ocsp.example.com endpoint. + // Use the ResponderOverrides option to point to the test server instead. + config := OCSPConfig{ + ResponderOverrides: map[string]string{ + "ocsp.example.com": responder.URL, + }, + } + + t.Run("ok", func(t *testing.T) { + cert := mustMakeCertificate(t, certWithOCSPServer, certKey) + tpl := ocsp.Response{ + Status: ocsp.Good, + SerialNumber: cert.Leaf.SerialNumber, + } + r, err := ocsp.CreateResponse( + ca.Leaf, ca.Leaf, tpl, ca.PrivateKey.(crypto.Signer)) + if err != nil { + t.Fatal("couldn't create OCSP response", err) + } + responses[cert.Leaf.SerialNumber.String()] = r + + bundle := []byte(certWithOCSPServer + "\n" + caCert) + err = stapleOCSP(ctx, config, storage, &cert, bundle) + if err != nil { + t.Error("unexpected error:", err) + } else if !bytes.Equal(cert.Certificate.OCSPStaple, r) { + t.Error("expected OCSP response to be stapled to certificate") + } + }) + t.Run("revoked", func(t *testing.T) { + cert := mustMakeCertificate(t, certWithOCSPServer, certKey) + tpl := ocsp.Response{ + Status: ocsp.Revoked, + SerialNumber: cert.Leaf.SerialNumber, + } + r, err := ocsp.CreateResponse( + ca.Leaf, ca.Leaf, tpl, ca.PrivateKey.(crypto.Signer)) + if err != nil { + t.Fatal("couldn't create OCSP response", err) + } + responses[cert.Leaf.SerialNumber.String()] = r + + bundle := []byte(certWithOCSPServer + "\n" + caCert) + err = stapleOCSP(ctx, config, storage, &cert, bundle) + if err != nil { + t.Error("unexpected error:", err) + } else if cert.Certificate.OCSPStaple != nil { + t.Error("revoked OCSP response should not be stapled") + } + }) + t.Run("no issuing cert", func(t *testing.T) { + cert := mustMakeCertificate(t, certWithOCSPServer, certKey) + err := stapleOCSP(ctx, config, storage, &cert, nil) + expected := "no OCSP stapling for [ocsp test certificate]: " + + "no URL to issuing certificate" + if err == nil || err.Error() != expected { + t.Errorf("expected error %q but got %q", expected, err) + } + }) +} + +func mustMakeCertificate(t *testing.T, cert, key string) Certificate { + t.Helper() + c, err := makeCertificate([]byte(cert), []byte(key)) if err != nil { t.Fatal("couldn't make certificate:", err) } + return c +} - err = stapleOCSP(context.Background(), config, storage, &cert, pemCert) - if !errors.Is(err, ErrNoOCSPServerSpecified) { - t.Error("expected ErrOCSPServerNotSpecified in error", err) +func startOCSPResponder( + t *testing.T, responses map[string][]byte, +) *httptest.Server { + h := func(w http.ResponseWriter, r *http.Request) { + ct := r.Header.Get("Content-Type") + if ct != "application/ocsp-request" { + t.Errorf("unexpected request Content-Type %q", ct) + } + b, _ := io.ReadAll(r.Body) + request, err := ocsp.ParseRequest(b) + if err != nil { + t.Fatal(err) + } + w.Header().Set("Content-Type", "application/ocsp-response") + w.Write(responses[request.SerialNumber.String()]) } + return httptest.NewServer(http.HandlerFunc(h)) }