Merge branch 'fix/bundle-4-est-scep-hardening' (Bundle 4: EST/SCEP Hardening, 3 audit findings)

This commit is contained in:
shankar0123
2026-04-25 21:14:57 +00:00
8 changed files with 496 additions and 1 deletions
+57
View File
@@ -630,6 +630,17 @@ func main() {
logger.Error("EST issuer not found in registry", "issuer_id", cfg.EST.IssuerID)
os.Exit(1)
}
// Bundle-4 / L-005: validate the issuer can actually serve a CA certificate
// at startup, not at first request time. ACME / DigiCert / Sectigo etc.
// return an error from GetCACertPEM because they don't expose a static
// CA chain; binding EST to one of those would silently degrade enrollment.
preflightCtx, preflightCancel := context.WithTimeout(context.Background(), 10*time.Second)
if err := preflightEnrollmentIssuer(preflightCtx, "EST", cfg.EST.IssuerID, issuerConn); err != nil {
preflightCancel()
logger.Error("startup refused: EST issuer cannot serve CA certificate", "error", err)
os.Exit(1)
}
preflightCancel()
estService := service.NewESTService(cfg.EST.IssuerID, issuerConn, auditService, logger)
estService.SetProfileRepo(profileRepo)
if cfg.EST.ProfileID != "" {
@@ -668,6 +679,15 @@ func main() {
logger.Error("SCEP issuer not found in registry", "issuer_id", cfg.SCEP.IssuerID)
os.Exit(1)
}
// Bundle-4 / L-005: validate the issuer can actually serve a CA certificate
// at startup. Same rationale as EST above.
preflightCtx, preflightCancel := context.WithTimeout(context.Background(), 10*time.Second)
if err := preflightEnrollmentIssuer(preflightCtx, "SCEP", cfg.SCEP.IssuerID, issuerConn); err != nil {
preflightCancel()
logger.Error("startup refused: SCEP issuer cannot serve CA certificate", "error", err)
os.Exit(1)
}
preflightCancel()
scepService := service.NewSCEPService(cfg.SCEP.IssuerID, issuerConn, auditService, logger, cfg.SCEP.ChallengePassword)
scepService.SetProfileRepo(profileRepo)
if cfg.SCEP.ProfileID != "" {
@@ -981,6 +1001,43 @@ func preflightSCEPChallengePassword(enabled bool, challengePassword string) erro
return nil
}
// preflightEnrollmentIssuer validates at startup that an EST/SCEP-bound issuer
// can actually serve a CA certificate. This closes audit finding L-005:
// pre-Bundle-4 the EST/SCEP startup path verified the issuer existed in the
// registry but did not verify the issuer TYPE could emit a CA cert. An
// operator who bound CERTCTL_EST_ISSUER_ID to an ACME issuer (which does
// not have a static CA cert — see internal/connector/issuer/acme/acme.go::
// GetCACertPEM returning an explicit error) would boot successfully and
// only see failures at the first /est/cacerts request, hiding the misconfig
// for hours/days behind a degraded enrollment surface.
//
// Strategy: call issuerConn.GetCACertPEM(ctx) at startup with a short
// timeout. If the issuer can serve a CA cert (local, vault, openssl,
// stepca, awsacmpca, etc.), the call succeeds and we proceed. If not
// (acme, digicert, sectigo, entrust, googlecas, ejbca, globalsign — most
// vendor-CA issuers that hand back chains per-issuance), the call fails
// loudly with the connector's own error string, and the caller os.Exit(1)s.
//
// Returns nil on success, non-nil error suitable for structured logging
// + os.Exit(1) by the caller. Caller is responsible for the timeout context.
func preflightEnrollmentIssuer(ctx context.Context, protocol, issuerID string, issuerConn service.IssuerConnector) error {
if issuerConn == nil {
return fmt.Errorf("%s issuer %q: connector is nil", protocol, issuerID)
}
caCertPEM, err := issuerConn.GetCACertPEM(ctx)
if err != nil {
return fmt.Errorf("%s issuer %q: cannot serve CA certificate (%w); "+
"choose an issuer type that exposes a static CA chain "+
"(local / vault / openssl / stepca / awsacmpca) or disable %s",
protocol, issuerID, err, protocol)
}
if caCertPEM == "" {
return fmt.Errorf("%s issuer %q: GetCACertPEM returned empty PEM with no error; "+
"choose an issuer type that exposes a static CA chain", protocol, issuerID)
}
return nil
}
// buildFinalHandler builds the outer HTTP dispatch handler that routes incoming
// requests to either the authenticated apiHandler chain or the unauthenticated
// noAuthHandler chain based on URL path prefix. Extracted from main() so the
+100
View File
@@ -0,0 +1,100 @@
package main
import (
"context"
"strings"
"testing"
"github.com/shankar0123/certctl/internal/service"
)
// fakeIssuerConn implements service.IssuerConnector enough for preflight tests.
type fakeIssuerConn struct {
caCertPEM string
caCertErr error
}
func (f *fakeIssuerConn) IssueCertificate(ctx context.Context, commonName string, sans []string, csrPEM string, ekus []string, maxTTLSeconds int) (*service.IssuanceResult, error) {
return nil, nil
}
func (f *fakeIssuerConn) RenewCertificate(ctx context.Context, commonName string, sans []string, csrPEM string, ekus []string, maxTTLSeconds int) (*service.IssuanceResult, error) {
return nil, nil
}
func (f *fakeIssuerConn) RevokeCertificate(ctx context.Context, serial string, reason string) error {
return nil
}
func (f *fakeIssuerConn) GenerateCRL(ctx context.Context, revokedCerts []service.CRLEntry) ([]byte, error) {
return nil, nil
}
func (f *fakeIssuerConn) SignOCSPResponse(ctx context.Context, req service.OCSPSignRequest) ([]byte, error) {
return nil, nil
}
func (f *fakeIssuerConn) GetCACertPEM(ctx context.Context) (string, error) {
return f.caCertPEM, f.caCertErr
}
func (f *fakeIssuerConn) GetRenewalInfo(ctx context.Context, certPEM string) (*service.RenewalInfoResult, error) {
return nil, nil
}
// TestPreflightEnrollmentIssuer covers Bundle-4 / L-005 startup validation
// for EST/SCEP issuer binding.
func TestPreflightEnrollmentIssuer(t *testing.T) {
cases := []struct {
name string
issuer service.IssuerConnector
wantErr bool
errContains string
}{
{
name: "nil_connector_fails",
issuer: nil,
wantErr: true,
errContains: "connector is nil",
},
{
name: "issuer_returns_error_fails",
issuer: &fakeIssuerConn{
caCertErr: errStub("ACME issuers do not provide a static CA certificate"),
},
wantErr: true,
errContains: "cannot serve CA certificate",
},
{
name: "issuer_returns_empty_pem_fails",
issuer: &fakeIssuerConn{
caCertPEM: "",
caCertErr: nil,
},
wantErr: true,
errContains: "empty PEM",
},
{
name: "issuer_returns_valid_pem_succeeds",
issuer: &fakeIssuerConn{
caCertPEM: "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----",
caCertErr: nil,
},
wantErr: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := preflightEnrollmentIssuer(context.Background(), "EST", "iss-test", tc.issuer)
if tc.wantErr && err == nil {
t.Fatalf("expected error, got nil")
}
if !tc.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tc.wantErr && tc.errContains != "" && !strings.Contains(err.Error(), tc.errContains) {
t.Fatalf("error %q missing substring %q", err.Error(), tc.errContains)
}
})
}
}
// errStub is a tiny error wrapper so test cases can use string literals
// without importing fmt in every test struct entry.
type errStub string
func (e errStub) Error() string { return string(e) }
+64
View File
@@ -109,6 +109,11 @@ func (h ESTHandler) SimpleEnroll(w http.ResponseWriter, r *http.Request) {
requestID := middleware.GetRequestID(r.Context())
if err := verifyESTTransport(r); err != nil {
ErrorWithRequestID(w, http.StatusBadRequest, fmt.Sprintf("EST transport precondition failed: %v", err), requestID)
return
}
csrPEM, err := h.readCSRFromRequest(r)
if err != nil {
ErrorWithRequestID(w, http.StatusBadRequest, fmt.Sprintf("Invalid CSR: %v", err), requestID)
@@ -134,6 +139,11 @@ func (h ESTHandler) SimpleReEnroll(w http.ResponseWriter, r *http.Request) {
requestID := middleware.GetRequestID(r.Context())
if err := verifyESTTransport(r); err != nil {
ErrorWithRequestID(w, http.StatusBadRequest, fmt.Sprintf("EST transport precondition failed: %v", err), requestID)
return
}
csrPEM, err := h.readCSRFromRequest(r)
if err != nil {
ErrorWithRequestID(w, http.StatusBadRequest, fmt.Sprintf("Invalid CSR: %v", err), requestID)
@@ -149,6 +159,60 @@ func (h ESTHandler) SimpleReEnroll(w http.ResponseWriter, r *http.Request) {
h.writeCertResponse(w, result)
}
// verifyESTTransport implements Bundle-4 / M-021 EST transport precondition.
//
// RFC 7030 §3.2.3 ("Linking Identity and POP Information") requires that when
// EST clients use certificate-based authentication AND send a Proof-of-Possession
// (PoP), the PoP MUST be cryptographically bound to the underlying TLS session
// via TLS-Unique (RFC 5929). With TLS 1.3 (which certctl pins via
// `tls.Config.MinVersion = tls.VersionTLS13` per the HTTPS-Everywhere milestone),
// TLS-Unique is unavailable; RFC 9266 defines `tls-exporter` as the TLS 1.3
// replacement.
//
// **Current scope of this function (Bundle-4 closure):** certctl does NOT
// currently support EST client certificate authentication. The EST endpoint
// accepts unauthenticated POSTs (the SCEP equivalent enforces a
// challenge-password via `preflightSCEPChallengePassword`; EST has no
// equivalent today). Per RFC 7030 §3.2.3, channel binding is REQUIRED only
// when client certificate authentication is in use; without that, the §3.2.3
// requirement is moot.
//
// What we DO enforce here as defense-in-depth:
//
// 1. r.TLS must be non-nil — the EST endpoint MUST be reached over TLS.
// Defensive: certctl pins HTTPS-only at the server-side TLS config, but
// a future routing-layer regression that exposes EST over plaintext
// would be caught here.
// 2. Negotiated TLS version must be >= TLS 1.2 — RFC 7030 doesn't mandate
// a specific TLS version, but a pre-1.2 negotiation indicates a
// misconfigured client/server pair. certctl's MinVersion is TLS 1.3
// so this should always hold.
// 3. r.TLS.HandshakeComplete must be true — defensive against partial-
// handshake replays.
//
// **Deferred to a future bundle (operator decision required):**
//
// - RFC 9266 `tls-exporter` channel binding when EST mTLS is added.
// - EST mTLS support itself — currently EST is unauth-or-bearer; mTLS
// would be a V3-aligned compliance feature.
//
// Returns nil if all preconditions pass; non-nil error otherwise.
func verifyESTTransport(r *http.Request) error {
if r.TLS == nil {
return fmt.Errorf("EST endpoint reached over plaintext; TLS required (RFC 7030 §3.2.1)")
}
if !r.TLS.HandshakeComplete {
return fmt.Errorf("EST request reached handler before TLS handshake completed")
}
// tls.VersionTLS12 == 0x0303; certctl's MinVersion is TLS 1.3 (0x0304).
// Defensive lower bound at TLS 1.2 lets us catch a future MinVersion
// regression cleanly without coupling this guard to the server config.
if r.TLS.Version < 0x0303 {
return fmt.Errorf("EST request negotiated TLS version 0x%04x; TLS 1.2 minimum required", r.TLS.Version)
}
return nil
}
// CSRAttrs handles GET /.well-known/est/csrattrs
// Returns the CSR attributes the server wants the client to include in enrollment requests.
func (h ESTHandler) CSRAttrs(w http.ResponseWriter, r *http.Request) {
+8
View File
@@ -5,6 +5,7 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
@@ -170,6 +171,7 @@ func TestESTSimpleEnroll_Success_PEM(t *testing.T) {
h := NewESTHandler(svc)
req := httptest.NewRequest(http.MethodPost, "/.well-known/est/simpleenroll", strings.NewReader(csrPEM))
req.TLS = &tls.ConnectionState{HandshakeComplete: true, Version: tls.VersionTLS13}
req.Header.Set("Content-Type", "application/pkcs10")
w := httptest.NewRecorder()
h.SimpleEnroll(w, req)
@@ -195,6 +197,7 @@ func TestESTSimpleEnroll_Success_Base64DER(t *testing.T) {
h := NewESTHandler(svc)
req := httptest.NewRequest(http.MethodPost, "/.well-known/est/simpleenroll", strings.NewReader(csrB64))
req.TLS = &tls.ConnectionState{HandshakeComplete: true, Version: tls.VersionTLS13}
req.Header.Set("Content-Type", "application/pkcs10")
w := httptest.NewRecorder()
h.SimpleEnroll(w, req)
@@ -222,6 +225,7 @@ func TestESTSimpleEnroll_EmptyBody(t *testing.T) {
h := NewESTHandler(svc)
req := httptest.NewRequest(http.MethodPost, "/.well-known/est/simpleenroll", strings.NewReader(""))
req.TLS = &tls.ConnectionState{HandshakeComplete: true, Version: tls.VersionTLS13}
w := httptest.NewRecorder()
h.SimpleEnroll(w, req)
@@ -235,6 +239,7 @@ func TestESTSimpleEnroll_InvalidCSR(t *testing.T) {
h := NewESTHandler(svc)
req := httptest.NewRequest(http.MethodPost, "/.well-known/est/simpleenroll", strings.NewReader("not-a-valid-csr"))
req.TLS = &tls.ConnectionState{HandshakeComplete: true, Version: tls.VersionTLS13}
w := httptest.NewRecorder()
h.SimpleEnroll(w, req)
@@ -251,6 +256,7 @@ func TestESTSimpleEnroll_ServiceError(t *testing.T) {
h := NewESTHandler(svc)
req := httptest.NewRequest(http.MethodPost, "/.well-known/est/simpleenroll", strings.NewReader(csrPEM))
req.TLS = &tls.ConnectionState{HandshakeComplete: true, Version: tls.VersionTLS13}
w := httptest.NewRecorder()
h.SimpleEnroll(w, req)
@@ -271,6 +277,7 @@ func TestESTSimpleReEnroll_Success(t *testing.T) {
h := NewESTHandler(svc)
req := httptest.NewRequest(http.MethodPost, "/.well-known/est/simplereenroll", strings.NewReader(csrPEM))
req.TLS = &tls.ConnectionState{HandshakeComplete: true, Version: tls.VersionTLS13}
w := httptest.NewRecorder()
h.SimpleReEnroll(w, req)
@@ -396,6 +403,7 @@ func TestESTSimpleReEnroll_ServiceError(t *testing.T) {
h := NewESTHandler(svc)
req := httptest.NewRequest(http.MethodPost, "/.well-known/est/simplereenroll", strings.NewReader(csrPEM))
req.TLS = &tls.ConnectionState{HandshakeComplete: true, Version: tls.VersionTLS13}
w := httptest.NewRecorder()
h.SimpleReEnroll(w, req)
@@ -0,0 +1,77 @@
package handler
import (
"crypto/tls"
"net/http"
"strings"
"testing"
)
// TestVerifyESTTransport_Bundle4_M021 covers the EST transport precondition
// added in Bundle-4 / M-021. See verifyESTTransport doc comment in est.go for
// scope rationale (RFC 7030 §3.2.3 channel binding is moot without EST mTLS;
// what we DO enforce is TLS pre-conditions).
func TestVerifyESTTransport_Bundle4_M021(t *testing.T) {
cases := []struct {
name string
req *http.Request
wantErr bool
errContains string
}{
{
name: "plaintext_request_rejected",
req: &http.Request{TLS: nil},
wantErr: true,
errContains: "plaintext",
},
{
name: "incomplete_handshake_rejected",
req: &http.Request{TLS: &tls.ConnectionState{
HandshakeComplete: false,
Version: tls.VersionTLS13,
}},
wantErr: true,
errContains: "handshake",
},
{
name: "tls10_rejected",
req: &http.Request{TLS: &tls.ConnectionState{
HandshakeComplete: true,
Version: tls.VersionTLS10,
}},
wantErr: true,
errContains: "TLS 1.2 minimum",
},
{
name: "tls12_accepted",
req: &http.Request{TLS: &tls.ConnectionState{
HandshakeComplete: true,
Version: tls.VersionTLS12,
}},
wantErr: false,
},
{
name: "tls13_accepted",
req: &http.Request{TLS: &tls.ConnectionState{
HandshakeComplete: true,
Version: tls.VersionTLS13,
}},
wantErr: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := verifyESTTransport(tc.req)
if tc.wantErr && err == nil {
t.Fatalf("verifyESTTransport(%s): expected error, got nil", tc.name)
}
if !tc.wantErr && err != nil {
t.Fatalf("verifyESTTransport(%s): unexpected error: %v", tc.name, err)
}
if tc.wantErr && tc.errContains != "" && !strings.Contains(err.Error(), tc.errContains) {
t.Fatalf("verifyESTTransport(%s): error %q missing substring %q", tc.name, err.Error(), tc.errContains)
}
})
}
}
+94
View File
@@ -0,0 +1,94 @@
package handler
import (
"encoding/hex"
"testing"
)
// FuzzExtractCSRFromPKCS7 exercises the SCEP PKCS#7 envelope parser at
// internal/api/handler/scep.go::extractCSRFromPKCS7. Bundle-4 / H-004:
// this parser is reachable by an anonymous network attacker via
// POST /scep?operation=PKIOperation. It calls into hand-written ASN.1
// unmarshaling logic in parseSignedDataForCSR (which uses encoding/asn1
// from stdlib but with manual structure layouts). Any panic, OOM, or
// allocation amplification surfaces here.
//
// Run locally:
//
// go test -run='^$' -fuzz=FuzzExtractCSRFromPKCS7 -fuzztime=10m \
// ./internal/api/handler/
//
// CI gate (Bundle-4 added in .github/workflows/ci.yml): runs at
// -fuzztime=2m on every PR. The full 10m runs are reserved for the
// scheduled overnight job to keep PR latency reasonable.
func FuzzExtractCSRFromPKCS7(f *testing.F) {
// Seed corpus: a few well-formed envelopes + a few deliberately
// malformed ones to give the fuzzer mutational starting points.
seeds := [][]byte{
// Minimal PKCS#7 ContentInfo OID + empty content.
mustHex("3013060B2A864886F70D010907020100"),
// Empty input — fuzzer should return error, not panic.
{},
// Single zero byte — parses as ASN.1 boolean false.
{0x00},
// Truncated SEQUENCE with bogus length.
{0x30, 0x81, 0xff},
// Recursive SEQUENCE wrapping (fuzzer + parser depth check).
{0x30, 0x80, 0x30, 0x80, 0x30, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
}
for _, seed := range seeds {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, data []byte) {
// Bound input size — the fuzzer otherwise tends to chase
// "find" rewards via 100MB inputs that aren't representative.
// Real network input is bounded by MaxBytesReader (1MB default).
if len(data) > 1<<20 {
return
}
// extractCSRFromPKCS7 returns (csrDER, challengePassword, transactionID, error).
// We don't care about the return values — we care that it doesn't
// panic, OOM, or allocate unbounded memory. The Go test harness
// reports panics as test failures.
_, _, _, _ = extractCSRFromPKCS7(data)
})
}
// FuzzParseSignedDataForCSR exercises the inner SignedData parser
// directly (the function extractCSRFromPKCS7 calls). Same scope as
// FuzzExtractCSRFromPKCS7 but narrower; helps the fuzzer find paths
// that the wrapping function's fallbacks would otherwise mask.
//
// Run locally:
//
// go test -run='^$' -fuzz=FuzzParseSignedDataForCSR -fuzztime=10m \
// ./internal/api/handler/
func FuzzParseSignedDataForCSR(f *testing.F) {
seeds := [][]byte{
mustHex("3013060B2A864886F70D010907020100"),
{},
{0x00},
{0x30, 0x80},
}
for _, seed := range seeds {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, data []byte) {
if len(data) > 1<<20 {
return
}
_, _ = parseSignedDataForCSR(data)
})
}
// mustHex decodes a hex string for fuzz seeds. Panics on malformed
// hex — only used at test setup with hard-coded constants.
func mustHex(s string) []byte {
b, err := hex.DecodeString(s)
if err != nil {
panic(err)
}
return b
}
+17 -1
View File
@@ -2,6 +2,7 @@ package integration
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
@@ -118,7 +119,22 @@ func setupTestServer(t *testing.T) (*httptest.Server, *mockCertificateRepository
// no Authorization header to verify the relying-party contract.
r.RegisterPKIHandlers(certificateHandler)
server := httptest.NewServer(r)
// Bundle-4 / M-021: the EST handler now requires `r.TLS != nil` per
// verifyESTTransport. The integration tests use httptest.NewServer (HTTP,
// not HTTPS) for simplicity. Wrap the router with a fake-TLS injector that
// sets a synthetic `*tls.ConnectionState` on every request — mimicking what
// the real TLS listener does in production. The injector is test-only;
// production paths use the real listener's `r.TLS`.
wrapped := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.TLS == nil {
req.TLS = &tls.ConnectionState{
HandshakeComplete: true,
Version: tls.VersionTLS13,
}
}
r.ServeHTTP(w, req)
})
server := httptest.NewServer(wrapped)
t.Cleanup(func() { server.Close() })
return server, certRepo, jobRepo, agentRepo
+79
View File
@@ -0,0 +1,79 @@
package pkcs7
import (
"testing"
)
// FuzzPEMToDERChain exercises the PEM-to-DER converter in
// internal/pkcs7/pkcs7.go::PEMToDERChain. Bundle-4 / H-004 (defense in depth):
// this function isn't directly network-reachable today (callers pass
// trusted PEM from issuer connectors), but it operates on byte input
// that traces back to upstream CA responses; a malicious-CA scenario
// could feed crafted PEM. Fuzz to ensure no panic, no allocation
// amplification.
//
// Run locally:
//
// go test -run='^$' -fuzz=FuzzPEMToDERChain -fuzztime=10m ./internal/pkcs7/
func FuzzPEMToDERChain(f *testing.F) {
seeds := []string{
// Empty input.
"",
// Minimal valid PEM (an empty CERTIFICATE block — not a real cert).
"-----BEGIN CERTIFICATE-----\nAA==\n-----END CERTIFICATE-----\n",
// Truncated header.
"-----BEGIN CERTIFICATE",
// Multiple BEGIN, no END.
"-----BEGIN CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n",
// Body with binary garbage.
"-----BEGIN CERTIFICATE-----\n\x00\xff\xfe\x80\n-----END CERTIFICATE-----\n",
}
for _, seed := range seeds {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, data string) {
// Bound input — same rationale as the SCEP fuzz.
if len(data) > 1<<20 {
return
}
_, _ = PEMToDERChain(data)
})
}
// FuzzASN1EncodeLength exercises the hand-rolled BER length encoder.
// Bundle-4 / H-004: the encoder is used when building PKCS#7 envelopes
// returned to EST/SCEP clients, so an attacker cannot directly feed
// untrusted bytes into it — but a future caller that did would be
// vulnerable to integer overflow / unbounded allocation. Fuzz the
// length values to confirm the encoder handles boundary conditions
// (negative, zero, MaxInt, etc.).
//
// Run locally:
//
// go test -run='^$' -fuzz=FuzzASN1EncodeLength -fuzztime=2m ./internal/pkcs7/
func FuzzASN1EncodeLength(f *testing.F) {
seeds := []int{0, 1, 127, 128, 255, 256, 65535, 65536, 1 << 20, 1 << 30, -1}
for _, seed := range seeds {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, length int) {
// Bound input — fuzz-generated lengths in the billions cause
// the encoder to allocate huge byte slices. Real PKCS#7 envelopes
// from certctl never exceed a few MB.
if length > 1<<24 || length < 0 {
return
}
out := ASN1EncodeLength(length)
// Sanity: encoder always returns at least one byte.
if len(out) == 0 {
t.Fatalf("ASN1EncodeLength(%d) returned empty slice", length)
}
// Sanity: encoder never returns more than 5 bytes for int input
// (1 length-of-length byte + 4 bytes for a 32-bit length).
if len(out) > 5 {
t.Fatalf("ASN1EncodeLength(%d) returned %d bytes; expected ≤5", length, len(out))
}
})
}