mirror of
https://github.com/hanzoai/pubsub.git
synced 2026-10-30 15:06:26 +00:00
Reported-by: Trail of Bits <https://www.trailofbits.com> Signed-off-by: Neil Twigg <[email protected]>
277 lines
7.2 KiB
Go
277 lines
7.2 KiB
Go
// Copyright 2019-2024 The NATS Authors
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package testhelper
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ocsp"
|
|
)
|
|
|
|
const (
|
|
defaultResponseTTL = 4 * time.Second
|
|
defaultAddress = "127.0.0.1:8888"
|
|
)
|
|
|
|
func NewOCSPResponderCustomAddress(t *testing.T, issuerCertPEM, issuerKeyPEM string, addr string) *http.Server {
|
|
t.Helper()
|
|
return NewOCSPResponderBase(t, issuerCertPEM, issuerCertPEM, issuerKeyPEM, false, addr, defaultResponseTTL, "")
|
|
}
|
|
|
|
func NewOCSPResponder(t *testing.T, issuerCertPEM, issuerKeyPEM string) *http.Server {
|
|
t.Helper()
|
|
return NewOCSPResponderBase(t, issuerCertPEM, issuerCertPEM, issuerKeyPEM, false, defaultAddress, defaultResponseTTL, "")
|
|
}
|
|
|
|
func NewOCSPResponderDesignatedCustomAddress(t *testing.T, issuerCertPEM, respCertPEM, respKeyPEM string, addr string) *http.Server {
|
|
t.Helper()
|
|
return NewOCSPResponderBase(t, issuerCertPEM, respCertPEM, respKeyPEM, true, addr, defaultResponseTTL, "")
|
|
}
|
|
|
|
func NewOCSPResponderPreferringHTTPMethod(t *testing.T, issuerCertPEM, issuerKeyPEM, method string) *http.Server {
|
|
t.Helper()
|
|
return NewOCSPResponderBase(t, issuerCertPEM, issuerCertPEM, issuerKeyPEM, false, defaultAddress, defaultResponseTTL, method)
|
|
}
|
|
|
|
func NewOCSPResponderCustomTimeout(t *testing.T, issuerCertPEM, issuerKeyPEM string, responseTTL time.Duration) *http.Server {
|
|
t.Helper()
|
|
return NewOCSPResponderBase(t, issuerCertPEM, issuerCertPEM, issuerKeyPEM, false, defaultAddress, responseTTL, "")
|
|
}
|
|
|
|
func NewOCSPResponderBase(t *testing.T, issuerCertPEM, respCertPEM, respKeyPEM string, embed bool, addr string, responseTTL time.Duration, method string) *http.Server {
|
|
t.Helper()
|
|
var mu sync.Mutex
|
|
status := make(map[string]int)
|
|
|
|
issuerCert := parseCertPEM(t, issuerCertPEM)
|
|
respCert := parseCertPEM(t, respCertPEM)
|
|
respKey := parseKeyPEM(t, respKeyPEM)
|
|
|
|
mux := http.NewServeMux()
|
|
// The "/statuses/" endpoint is for directly setting a key-value pair in
|
|
// the CA's status database.
|
|
mux.HandleFunc("/statuses/", func(rw http.ResponseWriter, r *http.Request) {
|
|
defer r.Body.Close()
|
|
|
|
key := r.URL.Path[len("/statuses/"):]
|
|
switch r.Method {
|
|
case "GET":
|
|
mu.Lock()
|
|
n, ok := status[key]
|
|
if !ok {
|
|
n = ocsp.Unknown
|
|
}
|
|
mu.Unlock()
|
|
|
|
fmt.Fprintf(rw, "%s %d", key, n)
|
|
case "POST":
|
|
data, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
http.Error(rw, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
n, err := strconv.Atoi(string(data))
|
|
if err != nil {
|
|
http.Error(rw, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
mu.Lock()
|
|
status[key] = n
|
|
mu.Unlock()
|
|
|
|
fmt.Fprintf(rw, "%s %d", key, n)
|
|
default:
|
|
http.Error(rw, "Method Not Allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
})
|
|
// The "/" endpoint is for normal OCSP requests. This actually parses an
|
|
// OCSP status request and signs a response with a CA. Lightly based off:
|
|
// https://www.ietf.org/rfc/rfc2560.txt
|
|
mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
|
|
var reqData []byte
|
|
var err error
|
|
|
|
switch {
|
|
case r.Method == "GET":
|
|
if method != "" && r.Method != method {
|
|
http.Error(rw, "", http.StatusBadRequest)
|
|
return
|
|
}
|
|
reqData, err = base64.StdEncoding.DecodeString(r.URL.Path[1:])
|
|
case r.Method == "POST":
|
|
if method != "" && r.Method != method {
|
|
http.Error(rw, "", http.StatusBadRequest)
|
|
return
|
|
}
|
|
reqData, err = io.ReadAll(r.Body)
|
|
r.Body.Close()
|
|
default:
|
|
http.Error(rw, "Method Not Allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
if err != nil {
|
|
http.Error(rw, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
ocspReq, err := ocsp.ParseRequest(reqData)
|
|
if err != nil {
|
|
http.Error(rw, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
mu.Lock()
|
|
n, ok := status[ocspReq.SerialNumber.String()]
|
|
if !ok {
|
|
n = ocsp.Unknown
|
|
}
|
|
mu.Unlock()
|
|
|
|
tmpl := ocsp.Response{
|
|
Status: n,
|
|
SerialNumber: ocspReq.SerialNumber,
|
|
ThisUpdate: time.Now(),
|
|
}
|
|
if responseTTL != 0 {
|
|
tmpl.NextUpdate = tmpl.ThisUpdate.Add(responseTTL)
|
|
}
|
|
if embed {
|
|
tmpl.Certificate = respCert
|
|
}
|
|
respData, err := ocsp.CreateResponse(issuerCert, respCert, tmpl, respKey)
|
|
if err != nil {
|
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
rw.Header().Set("Content-Type", "application/ocsp-response")
|
|
rw.Header().Set("Content-Length", fmt.Sprint(len(respData)))
|
|
|
|
fmt.Fprint(rw, string(respData))
|
|
})
|
|
|
|
srv := &http.Server{
|
|
Addr: addr,
|
|
Handler: mux,
|
|
ReadTimeout: time.Second * 5,
|
|
}
|
|
go srv.ListenAndServe()
|
|
time.Sleep(1 * time.Second)
|
|
return srv
|
|
}
|
|
|
|
func parseCertPEM(t *testing.T, certPEM string) *x509.Certificate {
|
|
t.Helper()
|
|
block := parsePEM(t, certPEM)
|
|
|
|
cert, err := x509.ParseCertificate(block.Bytes)
|
|
if err != nil {
|
|
t.Fatalf("failed to parse cert '%s': %s", certPEM, err)
|
|
}
|
|
return cert
|
|
}
|
|
|
|
func parseKeyPEM(t *testing.T, keyPEM string) crypto.Signer {
|
|
t.Helper()
|
|
block := parsePEM(t, keyPEM)
|
|
|
|
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
t.Fatalf("failed to parse ikey %s: %s", keyPEM, err)
|
|
}
|
|
}
|
|
keyc := key.(crypto.Signer)
|
|
return keyc
|
|
}
|
|
|
|
func parsePEM(t *testing.T, pemPath string) *pem.Block {
|
|
t.Helper()
|
|
data, err := os.ReadFile(pemPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
block, _ := pem.Decode(data)
|
|
if block == nil {
|
|
t.Fatalf("failed to decode PEM %s", pemPath)
|
|
}
|
|
return block
|
|
}
|
|
|
|
func GetOCSPStatus(s tls.ConnectionState) (*ocsp.Response, error) {
|
|
if len(s.VerifiedChains) == 0 {
|
|
return nil, fmt.Errorf("missing TLS verified chains")
|
|
}
|
|
chain := s.VerifiedChains[0]
|
|
|
|
if got, want := len(chain), 2; got < want {
|
|
return nil, fmt.Errorf("incomplete cert chain, got %d, want at least %d", got, want)
|
|
}
|
|
leaf, issuer := chain[0], chain[1]
|
|
|
|
resp, err := ocsp.ParseResponseForCert(s.OCSPResponse, leaf, issuer)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse OCSP response: %w", err)
|
|
}
|
|
if err := resp.CheckSignatureFrom(issuer); err != nil {
|
|
return resp, err
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func SetOCSPStatus(t *testing.T, ocspURL, certPEM string, status int) {
|
|
t.Helper()
|
|
|
|
cert := parseCertPEM(t, certPEM)
|
|
|
|
hc := &http.Client{Timeout: 10 * time.Second}
|
|
resp, err := hc.Post(
|
|
fmt.Sprintf("%s/statuses/%s", ocspURL, cert.SerialNumber),
|
|
"",
|
|
strings.NewReader(fmt.Sprint(status)),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
data, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed to read OCSP HTTP response body: %s", err)
|
|
}
|
|
|
|
if got, want := resp.Status, "200 OK"; got != want {
|
|
t.Error(strings.TrimSpace(string(data)))
|
|
t.Fatalf("unexpected OCSP HTTP set status, got %q, want %q", got, want)
|
|
}
|
|
}
|