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%.
This commit is contained in:
parent
4574cfafa8
commit
2b4a688bf9
179
ocsp_test.go
179
ocsp_test.go
@ -1,39 +1,182 @@
|
|||||||
package certmagic
|
package certmagic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ocsp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// certWithoutOCSPServer is a minimal self-signed certificate.
|
const certWithOCSPServer = `-----BEGIN CERTIFICATE-----
|
||||||
const certWithoutOCSPServer = `-----BEGIN CERTIFICATE-----
|
MIIBgjCCASegAwIBAgICIAAwCgYIKoZIzj0EAwIwEjEQMA4GA1UEAxMHVGVzdCBD
|
||||||
MIIBEDCBtqADAgECAgEBMAoGCCqGSM49BAMCMAAwIhgPMDAwMTAxMDEwMDAwMDBa
|
QTAeFw0yMzAxMDExMjAwMDBaFw0yMzAyMDExMjAwMDBaMCAxHjAcBgNVBAMTFU9D
|
||||||
GA8wMDAxMDEwMTAwMDAwMFowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJ0p
|
U1AgVGVzdCBDZXJ0aWZpY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIoe
|
||||||
7FKiv9p5rMMzntQeEBesKQnFR4XYFZ/SVlgJHFzd/QZ2sSxW+Mlbz78TTp4DMMIZ
|
I/bjo34qony8LdRJD+Jhuk8/S8YHXRHl6rH9t5VFCFtX8lIPN/Ll1zCrQ2KB3Wlb
|
||||||
J0z/Tw2+6fWdvoCYCW2jHTAbMBkGA1UdEQEB/wQPMA2CC2V4YW1wbGUuY29tMAoG
|
fxSgiQyLrCpZyrdhVPSjXzBdMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAU+Eo3
|
||||||
CCqGSM49BAMCA0kAMEYCIQDMbDvbJ/SXgRoblhBmt80F5iAyuOA0v20x0gpImK01
|
5sST4LRrwS4dueIdGBZ5d7IwLAYIKwYBBQUHAQEEIDAeMBwGCCsGAQUFBzABhhBv
|
||||||
oQIhANxdGJPvBaz0wOVBCSpd5jHbPxPxwqKZYJEes6y7eM+I
|
Y3NwLmV4YW1wbGUuY29tMAoGCCqGSM49BAMCA0kAMEYCIQDg94xY/+/VepESdvTT
|
||||||
|
ykCwiWOS2aCpjyryrKpwMKkR0AIhAPc/+ZEz4W10OENxC1t+NUTvS8JbEGOwulkZ
|
||||||
|
z9yfaLuD
|
||||||
-----END CERTIFICATE-----`
|
-----END CERTIFICATE-----`
|
||||||
|
|
||||||
const privateKey = `-----BEGIN EC PRIVATE KEY-----
|
const certWithoutOCSPServer = `-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBUzCB+aADAgECAgIgADAKBggqhkjOPQQDAjASMRAwDgYDVQQDEwdUZXN0IENB
|
||||||
|
MB4XDTIzMDEwMTEyMDAwMFoXDTIzMDIwMTEyMDAwMFowIDEeMBwGA1UEAxMVT0NT
|
||||||
|
UCBUZXN0IENlcnRpZmljYXRlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEih4j
|
||||||
|
9uOjfiqifLwt1EkP4mG6Tz9LxgddEeXqsf23lUUIW1fyUg838uXXMKtDYoHdaVt/
|
||||||
|
FKCJDIusKlnKt2FU9KMxMC8wDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBT4Sjfm
|
||||||
|
xJPgtGvBLh254h0YFnl3sjAKBggqhkjOPQQDAgNJADBGAiEA3rWetLGblfSuNZKf
|
||||||
|
5CpZxhj3A0BjEocEh+2P+nAgIdUCIQDIgptabR1qTLQaF2u0hJsEX2IKuIUvYWH3
|
||||||
|
6Lb92+zIHg==
|
||||||
|
-----END CERTIFICATE-----`
|
||||||
|
|
||||||
|
// 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
|
MHcCAQEEIDJ59ptjq3MzILH4zn5IKoH1sYn+zrUeq2kD8+DD2x+OoAoGCCqGSM49
|
||||||
AwEHoUQDQgAEnSnsUqK/2nmswzOe1B4QF6wpCcVHhdgVn9JWWAkcXN39BnaxLFb4
|
AwEHoUQDQgAEnSnsUqK/2nmswzOe1B4QF6wpCcVHhdgVn9JWWAkcXN39BnaxLFb4
|
||||||
yVvPvxNOngMwwhknTP9PDb7p9Z2+gJgJbQ==
|
yVvPvxNOngMwwhknTP9PDb7p9Z2+gJgJbQ==
|
||||||
-----END EC PRIVATE KEY-----`
|
-----END EC PRIVATE KEY-----`
|
||||||
|
|
||||||
func TestOCSPServerNotSpecified(t *testing.T) {
|
func TestStapleOCSP(t *testing.T) {
|
||||||
var config OCSPConfig
|
ctx := context.Background()
|
||||||
storage := &FileStorage{Path: t.TempDir()}
|
storage := &FileStorage{Path: t.TempDir()}
|
||||||
|
|
||||||
pemCert := []byte(certWithoutOCSPServer)
|
t.Run("disabled", func(t *testing.T) {
|
||||||
cert, err := makeCertificate(pemCert, []byte(privateKey))
|
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 {
|
if err != nil {
|
||||||
t.Fatal("couldn't make certificate:", err)
|
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))
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user