Skip to content

Instantly share code, notes, and snippets.

@liweitianux
Created September 16, 2024 01:40
Show Gist options
  • Save liweitianux/ceff7dccd308a917e415dadb1c05418c to your computer and use it in GitHub Desktop.
Save liweitianux/ceff7dccd308a917e415dadb1c05418c to your computer and use it in GitHub Desktop.
OCSP Responder accompanying Pebble CA
// SPDX-License-Identifier: MIT
//
// OCSP Responder accompanying Pebble CA.
//
// Aaron LI
// 2024-09-13
//
// Credits:
// - How can a client get the revocation status of a certificate?
// https://github.com/letsencrypt/pebble/issues/177#issuecomment-515928913
// - Create a new parameter in configuration file to set the OCSP Responder URL in certificates
// https://github.com/letsencrypt/pebble/pull/242#issuecomment-505487998
//
package main
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
"flag"
"fmt"
"io"
"log"
"math"
"math/big"
"net/http"
"strconv"
"strings"
"time"
"golang.org/x/crypto/ocsp"
)
const (
// OCSP certificate Common Name prefix
ocspNamePrefix = "Pebble OCSP "
// Request path to fetch the OCSP key and certificate
ocspCertPath = "/.certificate"
ocspKeyPath = "/.key"
)
func main() {
addr := flag.String("addr", "0.0.0.0", "listening address")
port := flag.Int("port", 80, "listening port")
path := flag.String("path", "/", "serving path")
pebbleURL := flag.String("pebble-url", "https://127.0.0.1:15000", "Pebble management URL")
flag.Parse()
ocsp := &ocspHandler{
pebbleURL: *pebbleURL,
}
if err := ocsp.initialize(); err != nil {
log.Fatal(err)
}
log.Printf("OCSP initialized")
if *path == "/" {
http.Handle("/", ocsp)
} else {
*path = strings.TrimSuffix(*path, "/")
http.Handle(*path, http.StripPrefix(*path, ocsp))
http.Handle(*path+"/", http.StripPrefix(*path, ocsp))
}
listen := fmt.Sprintf("%s:%d", *addr, *port)
log.Printf("Serving at: http://%s%s", listen, *path)
log.Fatal(http.ListenAndServe(listen, nil))
}
type ocspHandler struct {
pebbleURL string
// Intermediate CA certificate and key
intermediateCert *x509.Certificate
intermediateKey *rsa.PrivateKey
// OCSP certificate issued from the above intermediate CA
ocspCert *x509.Certificate
ocspKey *rsa.PrivateKey
}
func (o *ocspHandler) initialize() error {
o.pebbleURL = strings.TrimSuffix(o.pebbleURL, "/")
log.Printf("Pebble management URL: %s", o.pebbleURL)
if cert, err := getCert(o.pebbleURL + "/intermediates/0"); err != nil {
log.Printf("ERROR: failed to get intermediate certificate")
return err
} else {
o.intermediateCert = cert
}
if key, err := getKey(o.pebbleURL + "/intermediate-keys/0"); err != nil {
log.Printf("ERROR: failed to get intermediate private key")
return err
} else {
o.intermediateKey = key
}
if key, cert, err := makeKeyAndCert(o.intermediateCert, o.intermediateKey); err != nil {
return err
} else {
o.ocspCert = cert
o.ocspKey = key
log.Printf("Created OCSP key and certificate [%s] from intermediate CA",
cert.SerialNumber.Text(16))
}
return nil
}
func (o *ocspHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
//log.Printf("[DEBUG] request: %+v", req)
switch req.URL.Path {
case "", "/":
o.handleOcsp(w, req)
case ocspCertPath:
o.handleFetchCert(w, req)
case ocspKeyPath:
o.handleFetchKey(w, req)
default:
http.Error(w, "404 not found", http.StatusNotFound)
}
}
func (o *ocspHandler) handleFetchCert(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
http.Error(w, "405 method not allowed", http.StatusMethodNotAllowed)
return
}
var buf bytes.Buffer
err := pem.Encode(&buf, &pem.Block{
Type: "CERTIFICATE",
Bytes: o.ocspCert.Raw,
})
if err != nil {
http.Error(w, "500 internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/pem-certificate-chain; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(buf.Bytes())
}
func (o *ocspHandler) handleFetchKey(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
http.Error(w, "405 method not allowed", http.StatusMethodNotAllowed)
return
}
var buf bytes.Buffer
err := pem.Encode(&buf, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(o.ocspKey),
})
if err != nil {
http.Error(w, "500 internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/x-pem-file; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(buf.Bytes())
}
func (o *ocspHandler) handleOcsp(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
http.Error(w, "405 method not allowed", http.StatusMethodNotAllowed)
return
}
mediaType := ""
if ct := req.Header.Get("Content-Type"); ct != "" {
mediaType = strings.ToLower(strings.TrimSpace(strings.Split(ct, ";")[0]))
}
if mediaType != "application/ocsp-request" {
http.Error(w, "400 bad request: Content-Type missing or invalid",
http.StatusBadRequest)
return
}
contentLength, err := strconv.Atoi(req.Header.Get("Content-Length"))
if err != nil {
http.Error(w, "400 bad request: Content-Length missing or invalid",
http.StatusBadRequest)
return
}
body := make([]byte, contentLength)
if n, err := req.Body.Read(body); n < contentLength || err != io.EOF {
log.Printf("ERROR: failed to read body: %v", err)
http.Error(w, "400 bad request: body reading failure",
http.StatusBadRequest)
return
}
// XXX: Request Extensions (e.g., Nonce) are not supported.
// See: https://github.com/grimm-co/GOCSP-responder/commit/a7dc72852c1f0431e97404f48c7259acaa20dfd8
oreq, err := ocsp.ParseRequest(body)
if err != nil {
log.Printf("ERROR: failed to parse OCSP request: %v", err)
http.Error(w, "400 bad request: invalid OCSP request",
http.StatusBadRequest)
return
}
log.Printf("[INFO] Got OCSP request: %+v", oreq)
template := ocsp.Response{
Status: ocsp.Good,
SerialNumber: oreq.SerialNumber,
Certificate: o.ocspCert, // NOTE: required!
ThisUpdate: time.Now(),
NextUpdate: time.Now().AddDate(0, 0, 1),
}
if cs, err := o.getCertStatus(oreq.SerialNumber); err != nil {
template.Status = ocsp.Unknown
} else if cs.Status == "Revoked" {
template.Status = ocsp.Revoked
template.RevocationReason = cs.Reason
template.RevokedAt = cs.RevokedAt_
}
//log.Printf("[DEBUG] OCSP response template: %+v", template)
resp, err := ocsp.CreateResponse(o.intermediateCert, o.ocspCert, template, o.ocspKey)
if err != nil {
log.Printf("ERROR: failed to create OCSP response: %v", err)
http.Error(w, "500 internal server error: ocsp response failure",
http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/ocsp-response")
w.WriteHeader(http.StatusOK)
w.Write(resp)
}
type certStatus struct {
Status string // Valid, Revoked
Serial string // in hex string, without 0x prefix
Certificate string // PEM-encoded certificate
Certificate_ *x509.Certificate // parsed from Certificate
Reason int // 0 if unspecified
RevokedAt string // e.g., 2024-09-13 03:00:30.442029447 +0000 UTC
RevokedAt_ time.Time // parsed from RevokedAt
}
func (o ocspHandler) getCertStatus(serial *big.Int) (*certStatus, error) {
url := o.pebbleURL + "/cert-status-by-serial/" + serial.Text(16)
body, err := request(url)
if err != nil {
return nil, err
}
cs := &certStatus{}
if err := json.Unmarshal(body, cs); err != nil {
log.Printf("ERROR: failed to unmarshal data: %s", string(body))
return nil, err
}
if cs.RevokedAt != "" {
layout := "2006-01-02 15:04:05.999999999 -0700 MST"
cs.RevokedAt_, err = time.Parse(layout, cs.RevokedAt)
if err != nil {
log.Printf("ERROR: failed to parse RevokedAt: %s", cs.RevokedAt)
return nil, err
}
}
block, _ := pem.Decode([]byte(cs.Certificate))
if block == nil {
log.Printf("ERROR: failed to PEM decode data: %s", cs.Certificate)
return nil, errors.New("bad PEM certificate")
}
cs.Certificate_, err = x509.ParseCertificate(block.Bytes)
if err != nil {
log.Printf("ERROR: failed to parse certificate: %s", cs.Certificate)
return nil, err
}
log.Printf("[INFO] Got cert status: serial=%s, status=%s", cs.Serial, cs.Status)
return cs, nil
}
//----------------------------------------------------------------------------
// Utilities
var client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
Timeout: 2 * time.Second,
}
func request(url string) (body []byte, err error) {
req, _ := http.NewRequest(http.MethodGet, url, nil)
resp, err := client.Do(req)
if err != nil {
log.Printf("ERROR: failed to request %s: %s", url, err)
return nil, err
}
defer resp.Body.Close()
if status := resp.StatusCode; status != http.StatusOK {
log.Printf("ERROR: bad status (%d) of request to %s", status, url)
return nil, errors.New("bad status code")
}
body, err = io.ReadAll(resp.Body)
if err != nil {
log.Printf("ERROR: failed to read body of request to %s: %s", url, err)
return nil, err
}
return body, nil
}
func getCert(url string) (*x509.Certificate, error) {
data, err := request(url)
if err != nil {
return nil, err
}
block, _ := pem.Decode(data)
if block == nil {
log.Printf("ERROR: failed to PEM decode data: %s", string(data))
return nil, errors.New("bad PEM data")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
log.Printf("ERROR: failed to parse certificate: %s", string(data))
return nil, err
}
log.Printf("Obtained certificate from %s", url)
return cert, nil
}
func getKey(url string) (*rsa.PrivateKey, error) {
data, err := request(url)
if err != nil {
return nil, err
}
block, _ := pem.Decode(data)
if block == nil {
log.Printf("ERROR: failed to PEM decode data: %s", string(data))
return nil, errors.New("bad PEM data")
}
// First try PKCS1 (RSA PRIVATE KEY)
if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil {
log.Printf("Obtained PKCS1 private key from %s", url)
return key, nil
}
// Fallback to PKCS8 (PRIVATE KEY)
if key, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil {
log.Printf("Obtained PKCS1 private key from %s", url)
rsaKey := key.(*rsa.PrivateKey)
return rsaKey, nil
}
log.Printf("ERROR: failed to get private key from %s", url)
return nil, errors.New("invalid private key")
}
// From pebble/ca/ca.go
func makeSerial() *big.Int {
serial, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
if err != nil {
panic(fmt.Sprintf("unable to create random serial number: %s", err.Error()))
}
return serial
}
// From pebble/ca/ca.go
// Adapted from MiniCA: https://github.com/jsha/minica/blob/3a621c05b61fa1c24bcb42fbde4b261db504a74f/main.go
func makeKey() (*rsa.PrivateKey, []byte, error) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, err
}
ski, err := makeSubjectKeyID(key.Public())
if err != nil {
return nil, nil, err
}
return key, ski, nil
}
// From pebble/ca/ca.go
// Taken from https://github.com/cloudflare/cfssl/blob/b94e044bb51ec8f5a7232c71b1ed05dbe4da96ce/signer/signer.go#L221-L244
func makeSubjectKeyID(key crypto.PublicKey) ([]byte, error) {
// Marshal the public key as ASN.1
pubAsDER, err := x509.MarshalPKIXPublicKey(key)
if err != nil {
return nil, err
}
// Unmarshal it again so we can extract the key bitstring bytes
var pubInfo struct {
Algorithm pkix.AlgorithmIdentifier
SubjectPublicKey asn1.BitString
}
_, err = asn1.Unmarshal(pubAsDER, &pubInfo)
if err != nil {
return nil, err
}
// Hash it according to https://tools.ietf.org/html/rfc5280#section-4.2.1.2 Method #1:
ski := sha1.Sum(pubInfo.SubjectPublicKey.Bytes)
return ski[:], nil
}
func makeKeyAndCert(
issuer *x509.Certificate,
signKey *rsa.PrivateKey,
) (*rsa.PrivateKey, *x509.Certificate, error) {
key, ski, err := makeKey()
if err != nil {
log.Printf("ERROR: failed to create OCSP key: %s", err)
return nil, nil, err
}
serial := makeSerial()
id := hex.EncodeToString(serial.Bytes()[:3])
subject := pkix.Name{
CommonName: ocspNamePrefix + id,
}
template := &x509.Certificate{
Subject: subject,
SerialNumber: serial,
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageOCSPSigning},
SubjectKeyId: ski,
BasicConstraintsValid: true,
IsCA: false,
}
der, err := x509.CreateCertificate(rand.Reader, template, issuer, key.Public(), signKey)
if err != nil {
log.Printf("ERROR: failed to create OCSP certificate: %s", err)
return nil, nil, err
}
cert, err := x509.ParseCertificate(der)
if err != nil {
log.Printf("ERROR: failed to parse OCSP certificate: %s", err)
return nil, nil, err
}
return key, cert, nil
}
@liweitianux
Copy link
Author

Build and start the OCSP responder:

$ go build -mod=mod -v -o pebble-ocsp pebble-ocsp.go
$ sudo ./pebble-ocsp -port 80

Example queries:

$ openssl ocsp -CAfile root.crt -issuer intermediate.crt -cert revoked.example.com/cert.pem -url http://127.0.0.1                    
WARNING: no nonce in response
Response verify OK
revoked.example.com/cert.pem: revoked
	This Update: Sep 16 01:37:35 2024 GMT
	Next Update: Sep 17 01:37:35 2024 GMT
	Revocation Time: Sep 14 07:01:04 2024 GMT

$ openssl ocsp -CAfile root.crt -issuer intermediate.crt -cert www.example.com/cert.pem -url http://127.0.0.1    
WARNING: no nonce in response
Response verify OK
www.example.com/cert.pem: good
	This Update: Sep 16 01:38:00 2024 GMT
	Next Update: Sep 17 01:38:00 2024 GMT

Example responder output:

% sudo ./pebble-ocsp -port 80
2024/09/16 09:37:20 Pebble management URL: https://127.0.0.1:15000
2024/09/16 09:37:20 Obtained certificate from https://127.0.0.1:15000/intermediates/0
2024/09/16 09:37:20 Obtained PKCS1 private key from https://127.0.0.1:15000/intermediate-keys/0
2024/09/16 09:37:20 Created OCSP key and certificate [72d5c3b7cbc0228a] from intermediate CA
2024/09/16 09:37:20 OCSP initialized
2024/09/16 09:37:20 Serving at: http://0.0.0.0:80/
2024/09/16 09:37:35 [INFO] Got OCSP request: &{HashAlgorithm:SHA-1 IssuerNameHash:[9 67 18 76 46 98 20 35 202 77 85 44 123 138 136 64 147 241 245 173] IssuerKeyHash:[20 122 18 21 197 55 75 2 69 170 124 41 111 38 97 173 76 114 141 13] SerialNumber:+5314199856862732533}
2024/09/16 09:37:35 [INFO] Got cert status: serial=49bfd49d2dbc20f5, status=Revoked
2024/09/16 09:38:00 [INFO] Got OCSP request: &{HashAlgorithm:SHA-1 IssuerNameHash:[9 67 18 76 46 98 20 35 202 77 85 44 123 138 136 64 147 241 245 173] IssuerKeyHash:[20 122 18 21 197 55 75 2 69 170 124 41 111 38 97 173 76 114 141 13] SerialNumber:+6248767529407721405}
2024/09/16 09:38:00 [INFO] Got cert status: serial=56b814f5ea57afbd, status=Valid

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment