1
0
Fork 0

OCSP stapling

This commit is contained in:
Alessandro Chitolina 2025-06-06 17:44:04 +02:00 committed by GitHub
parent 2949995abc
commit b39ee8ede5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1576 additions and 178 deletions

View file

@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBhjCCASygAwIBAgIBATAKBggqhkjOPQQDAjASMRAwDgYDVQQDEwdUZXN0IENB
MB4XDTI1MDQyNDEzNTIzOFoXDTM1MDQyMjEzNTIzOFowEjEQMA4GA1UEAxMHVGVz
dCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPp8MoNUBbUxp3jW6FcDH+lg
Zft1SIpnGjkMVjLSbW9EzmRQ/oMRHQqJvE7wJbwDs/JUTigRtfZL0vOojnhHcPej
czBxMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQW
BBQXRlWLK295lmDy+931a4Ha8XVNNjAsBggrBgEFBQcBAQQgMB4wHAYIKwYBBQUH
MAGGEG9jc3AuZXhhbXBsZS5jb20wCgYIKoZIzj0EAwIDSAAwRQIgYH6lnce9jxcp
YIVhY4z55rnOKXqaI/5rUQKwjJ3dRsUCIQDThtkFgOPT/67xOYCTCEVSMSTwh2Gq
jbeucU+4c/InVg==
-----END CERTIFICATE-----

View file

@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIGV6FPfHeA42xfjVtpnyATG6tKCCu0QoY0OlBR/0xn2toAoGCCqGSM49
AwEHoUQDQgAE+nwyg1QFtTGneNboVwMf6WBl+3VIimcaOQxWMtJtb0TOZFD+gxEd
Com8TvAlvAOz8lROKBG19kvS86iOeEdw9w==
-----END EC PRIVATE KEY-----

View file

@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBjzCCATWgAwIBAgIIGDlFgswljYAwCgYIKoZIzj0EAwIwEjEQMA4GA1UEAxMH
VGVzdCBDQTAeFw0yNTA0MjQxMzUyMzhaFw0yNjA0MjQxMzUyMzhaMBgxFjAUBgNV
BAMTDWRlZmF1bHQubG9jYWwwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQGABZ/
zezTMQBwmmw3aifU0OkDQ4ZzxGG7dR93svJPgYnP7TpBVtPrxy0WgVZbbCHv0Srl
PlpO9rFkKf3D4E6Qo28wbTAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAf
BgNVHSMEGDAWgBQXRlWLK295lmDy+931a4Ha8XVNNjAsBggrBgEFBQcBAQQgMB4w
HAYIKwYBBQUHMAGGEG9jc3AuZXhhbXBsZS5jb20wCgYIKoZIzj0EAwIDSAAwRQIh
AJMF7RkU0BtNZlHf//PPgpPfDJybnYMIoX1Ek4I8JZ+QAiBpxjzeFE9jwqcJnx5X
KnOJMbgfvJliZZgVSuXBbulzAA==
-----END CERTIFICATE-----

View file

@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIO4UluA82wXVkaVH0m6oFGWyC8mzVcc7H9MI0ltXgkNuoAoGCCqGSM49
AwEHoUQDQgAEBgAWf83s0zEAcJpsN2on1NDpA0OGc8Rhu3Ufd7LyT4GJz+06QVbT
68ctFoFWW2wh79Eq5T5aTvaxZCn9w+BOkA==
-----END EC PRIVATE KEY-----

View file

@ -0,0 +1,100 @@
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"os"
"time"
)
func main() {
// generate CA
caKey, caCert := generateCA("Test CA")
saveKeyAndCert("integration/fixtures/ocsp/ca.key", "integration/fixtures/ocsp/ca.crt", caKey, caCert)
// server certificate
serverKey, serverCert := generateCert("server.local", caKey, caCert)
saveKeyAndCert("integration/fixtures/ocsp/server.key", "integration/fixtures/ocsp/server.crt", serverKey, serverCert)
// default certificate
defaultKey, defaultCert := generateCert("default.local", caKey, caCert)
saveKeyAndCert("integration/fixtures/ocsp/default.key", "integration/fixtures/ocsp/default.crt", defaultKey, defaultCert)
}
func generateCA(commonName string) (*ecdsa.PrivateKey, *x509.Certificate) {
// generate a private key for the CA
caKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
// create a self-signed CA certificate
caTemplate := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: commonName,
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), // 10 ans
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 1,
OCSPServer: []string{"ocsp.example.com"},
}
caCertDER, _ := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey)
caCert, _ := x509.ParseCertificate(caCertDER)
return caKey, caCert
}
func generateCert(commonName string, caKey *ecdsa.PrivateKey, caCert *x509.Certificate) (*ecdsa.PrivateKey, *x509.Certificate) {
// create a private key for the certificate
certKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
// create a certificate signed by the CA
certTemplate := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().UnixNano()),
Subject: pkix.Name{
CommonName: commonName,
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(1 * 365 * 24 * time.Hour), // 1 an
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
BasicConstraintsValid: true,
OCSPServer: []string{"ocsp.example.com"},
}
certDER, _ := x509.CreateCertificate(rand.Reader, certTemplate, caCert, &certKey.PublicKey, caKey)
cert, _ := x509.ParseCertificate(certDER)
return certKey, cert
}
func saveKeyAndCert(keyFile, certFile string, key *ecdsa.PrivateKey, cert *x509.Certificate) {
// save the private key
keyOut, _ := os.Create(keyFile)
defer keyOut.Close()
// Marshal the private key to ASN.1 DER format
privateKey, err := x509.MarshalECPrivateKey(key)
if err != nil {
panic(err)
}
err = pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: privateKey})
if err != nil {
panic(err)
}
// save the certificate
certOut, _ := os.Create(certFile)
defer certOut.Close()
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
if err != nil {
panic(err)
}
}

View file

@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBjjCCATSgAwIBAgIIGDlFgswgB3AwCgYIKoZIzj0EAwIwEjEQMA4GA1UEAxMH
VGVzdCBDQTAeFw0yNTA0MjQxMzUyMzhaFw0yNjA0MjQxMzUyMzhaMBcxFTATBgNV
BAMTDHNlcnZlci5sb2NhbDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHpjZoVk
Qh15gTa26KMJfvzfVgGHGicUDg1UYppKAMY83rxSXqRHcVFAFRqWDTgCQRy6hPq+
6p5OwBziC2X/SOejbzBtMA4GA1UdDwEB/wQEAwIFoDAMBgNVHRMBAf8EAjAAMB8G
A1UdIwQYMBaAFBdGVYsrb3mWYPL73fVrgdrxdU02MCwGCCsGAQUFBwEBBCAwHjAc
BggrBgEFBQcwAYYQb2NzcC5leGFtcGxlLmNvbTAKBggqhkjOPQQDAgNIADBFAiEA
mp5LQixMUFh5h8yF1EtFsi4MKrO+dzD68TqIhq1rKjUCIEbB++M8qO4gtqjv8d06
AzSLTEfgNCmM574JI46YAKVx
-----END CERTIFICATE-----

View file

@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIFpVKKKxvw6cZe7hwRLHgXIsWiJYUQ66PKzO6iXINUH0oAoGCCqGSM49
AwEHoUQDQgAEemNmhWRCHXmBNrboowl+/N9WAYcaJxQODVRimkoAxjzevFJepEdx
UUAVGpYNOAJBHLqE+r7qnk7AHOILZf9I5w==
-----END EC PRIVATE KEY-----

View file

@ -0,0 +1,27 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[entryPoints]
[entryPoints.web]
address = ":8000"
[providers.file]
filename = "{{ .SelfFilename }}"
[ocsp.responderOverrides]
ocsp.example.com = "{{ .ResponderURL }}"
[log]
level="debug"
## dynamic configuration ##
[[tls.certificates]]
certFile = "fixtures/ocsp/server.crt"
keyFile = "fixtures/ocsp/server.key"
[tls.stores]
[tls.stores.default.defaultCertificate]
certFile = "fixtures/ocsp/default.crt"
keyFile = "fixtures/ocsp/default.key"

View file

@ -3,7 +3,9 @@ package integration
import (
"bufio"
"bytes"
"crypto"
"crypto/rand"
"crypto/tls"
"encoding/json"
"fmt"
"io"
@ -24,6 +26,7 @@ import (
"github.com/stretchr/testify/suite"
"github.com/traefik/traefik/v3/integration/try"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"golang.org/x/crypto/ocsp"
)
// SimpleSuite tests suite.
@ -1598,6 +1601,132 @@ func (s *SimpleSuite) TestMaxHeaderBytes() {
}
}
func (s *SimpleSuite) TestSimpleOCSP() {
defaultCert, err := tls.LoadX509KeyPair("fixtures/ocsp/default.crt", "fixtures/ocsp/default.key")
require.NoError(s.T(), err)
serverCert, err := tls.LoadX509KeyPair("fixtures/ocsp/server.crt", "fixtures/ocsp/server.key")
require.NoError(s.T(), err)
defaultOCSPResponseTmpl := ocsp.Response{
SerialNumber: defaultCert.Leaf.SerialNumber,
Status: ocsp.Good,
ThisUpdate: defaultCert.Leaf.NotBefore,
NextUpdate: defaultCert.Leaf.NotAfter,
}
defaultOCSPResponse, err := ocsp.CreateResponse(defaultCert.Leaf, defaultCert.Leaf, defaultOCSPResponseTmpl, defaultCert.PrivateKey.(crypto.Signer))
require.NoError(s.T(), err)
serverOCSPResponseTmpl := ocsp.Response{
SerialNumber: serverCert.Leaf.SerialNumber,
Status: ocsp.Good,
ThisUpdate: serverCert.Leaf.NotBefore,
NextUpdate: serverCert.Leaf.NotAfter,
}
serverOCSPResponse, err := ocsp.CreateResponse(serverCert.Leaf, serverCert.Leaf, serverOCSPResponseTmpl, serverCert.PrivateKey.(crypto.Signer))
require.NoError(s.T(), err)
responderCalled := make(chan struct{})
responder := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
ct := req.Header.Get("Content-Type")
assert.Equal(s.T(), "application/ocsp-request", ct)
reqBytes, err := io.ReadAll(req.Body)
require.NoError(s.T(), err)
ocspReq, err := ocsp.ParseRequest(reqBytes)
require.NoError(s.T(), err)
var ocspResponse []byte
switch ocspReq.SerialNumber.String() {
case defaultCert.Leaf.SerialNumber.String():
ocspResponse = defaultOCSPResponse
case serverCert.Leaf.SerialNumber.String():
ocspResponse = serverOCSPResponse
default:
s.T().Fatalf("Unexpected OCSP request for serial number: %s", ocspReq.SerialNumber)
}
rw.Header().Set("Content-Type", "application/ocsp-response")
_, err = rw.Write(ocspResponse)
require.NoError(s.T(), err)
responderCalled <- struct{}{}
}))
s.T().Cleanup(responder.Close)
file := s.adaptFile("fixtures/ocsp/simple.toml", struct {
ResponderURL string
}{responder.URL})
s.traefikCmd(withConfigFile(file))
select {
case <-responderCalled:
case <-time.After(5 * time.Second):
s.T().Fatal("OCSP responder was not called")
}
select {
case <-responderCalled:
case <-time.After(5 * time.Second):
s.T().Fatal("OCSP responder was not called")
}
// Check that the response is stapled.
// Create a TLS client configuration that checks for OCSP stapling for the default cert.
var verifyCallCount int
clientConfig := &tls.Config{
InsecureSkipVerify: true,
ServerName: "unknown",
VerifyConnection: func(state tls.ConnectionState) error {
s.T().Helper()
verifyCallCount++
assert.Equal(s.T(), "default.local", state.PeerCertificates[0].Subject.CommonName)
assert.Equal(s.T(), defaultOCSPResponse, state.OCSPResponse)
return nil
},
}
// Connect to the server and verify OCSP stapling.
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: 5 * time.Second}, "tcp", "127.0.0.1:8000", clientConfig)
require.NoError(s.T(), err)
s.T().Cleanup(func() {
_ = conn.Close()
})
assert.Equal(s.T(), 1, verifyCallCount)
// Create a TLS client configuration that checks for OCSP stapling for a cert in the store.
verifyCallCount = 0
clientConfig = &tls.Config{
InsecureSkipVerify: true,
ServerName: "server.local",
VerifyConnection: func(state tls.ConnectionState) error {
s.T().Helper()
verifyCallCount++
assert.Equal(s.T(), "server.local", state.PeerCertificates[0].Subject.CommonName)
assert.Equal(s.T(), serverOCSPResponse, state.OCSPResponse)
return nil
},
}
// Connect to the server and verify OCSP stapling.
conn, err = tls.DialWithDialer(&net.Dialer{Timeout: 5 * time.Second}, "tcp", "127.0.0.1:8000", clientConfig)
require.NoError(s.T(), err)
s.T().Cleanup(func() {
_ = conn.Close()
})
assert.Equal(s.T(), 1, verifyCallCount)
}
func (s *SimpleSuite) TestSanitizePath() {
s.createComposeProject("base")