Files
shankar0123 21aeed4f4e legal: addlicense headers + normalize legacy variants (Phase 0 RED-4)
Phase 0 closure (Path B2, post-rewrite):

addlicense sweep — adds the canonical certctl LLC copyright + BUSL-1.1
SPDX header to every production Go file. Template:

  // Copyright 2026 certctl LLC. All rights reserved.
  // SPDX-License-Identifier: BUSL-1.1

Coverage: 338 / 338 production Go files (cmd/ + internal/, excluding
*_test.go and **/testdata/**). Pre-sweep coverage was 22 / 338 (6.5%);
post-sweep is 338 / 338 (100%).

Normalized 22 pre-existing legacy headers (`// Copyright (c) certctl`
+ `// SPDX-License-Identifier: BSL-1.1`) and 1 file using a
`Certctl Contributors` attribution. The legacy SPDX ID `BSL-1.1`
is non-standard; the official SPDX identifier for Business Source
License 1.1 is `BUSL-1.1` (capital U). All 338 files now share the
canonical form.

Generated via:
  addlicense -c "certctl LLC" -y 2026 \
    -f cowork/legal/copyright-header.tpl \
    -ignore '**/testdata/**' -ignore '**/*_test.go' \
    cmd/ internal/

Verification:
  find cmd internal -name '*.go' -not -name '*_test.go' \
    -not -path '*/testdata/*' \
    -exec grep -L '^// Copyright 2026 certctl LLC' {} \; | wc -l

  Returns: 0

gofmt clean. Header additions are comments only, no compile impact.

Closes: cowork/certctl-architecture-diligence-audit.html#fix-RED-4
2026-05-13 21:23:35 +00:00

174 lines
6.2 KiB
Go

// Copyright 2026 certctl LLC. All rights reserved.
// SPDX-License-Identifier: BUSL-1.1
package service
import (
"encoding/asn1"
"errors"
)
// Production hardening II Phase 1.1 — OCSP nonce extension parsing.
//
// RFC 6960 §4.4.1 defines the optional id-pkix-ocsp-nonce extension
// (OID 1.3.6.1.5.5.7.48.1.2) that defends against replay attacks.
// When present in the request, the responder MUST echo the same
// nonce value in the response. When absent, the response MUST NOT
// include a nonce.
//
// `golang.org/x/crypto/ocsp.Request` does NOT expose the request's
// extensions field — we have to walk the raw DER ourselves to extract
// the nonce. The grammar (RFC 6960 §4.1.1):
//
// OCSPRequest ::= SEQUENCE {
// tbsRequest TBSRequest,
// optionalSignature [0] EXPLICIT Signature OPTIONAL
// }
// TBSRequest ::= SEQUENCE {
// version [0] EXPLICIT Version DEFAULT v1,
// requestorName [1] EXPLICIT GeneralName OPTIONAL,
// requestList SEQUENCE OF Request,
// requestExtensions [2] EXPLICIT Extensions OPTIONAL
// }
// Extension ::= SEQUENCE {
// extnID OBJECT IDENTIFIER,
// critical BOOLEAN DEFAULT FALSE,
// extnValue OCTET STRING
// }
//
// The nonce extension's extnValue is itself a DER-encoded OCTET STRING
// containing the nonce bytes (per RFC 6960 §4.4.1 — "The value of the
// extension SHALL be the value of a Nonce ::= OCTET STRING").
// OIDOCSPNonce is the id-pkix-ocsp-nonce extension OID (RFC 6960 §4.4.1).
var OIDOCSPNonce = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 48, 1, 2}
// MaxOCSPNonceLength is the per-CA/B-Forum-guidance cap on the nonce
// payload size. Larger nonces are rejected as malformed (Phase 1
// frozen decision). The CA/B Forum Baseline Requirements §4.10.2
// notes that nonces SHOULD be at most 32 octets.
const MaxOCSPNonceLength = 32
// ErrOCSPNonceMalformed is returned when the request carries a nonce
// extension but the nonce value violates the documented constraints
// (empty, oversized, or unparseable). The handler maps this to an
// "unauthorized" OCSP response (status 6 per RFC 6960 §2.3) rather
// than echoing potentially-malicious bytes back to the relying party.
var ErrOCSPNonceMalformed = errors.New("OCSP request: nonce extension malformed")
// ParseOCSPRequestNonce extracts the nonce value (if any) from the
// OCSP request's TBSRequest.requestExtensions field.
//
// Returns:
// - (nonceBytes, true, nil) — well-formed nonce, echo it.
// - (nil, false, nil) — no nonce extension present (back-compat).
// - (nil, false, ErrOCSPNonceMalformed) — nonce present but malformed
// (zero length OR > MaxOCSPNonceLength). Handler MUST NOT echo;
// return an unauthorized OCSP response.
//
// The function is tolerant of arbitrary OCSP requests including those
// with an optionalSignature: it parses the OCSPRequest envelope first,
// then walks tbsRequest.
func ParseOCSPRequestNonce(reqDER []byte) (nonce []byte, present bool, err error) {
// OCSPRequest ::= SEQUENCE { tbsRequest, [0] OPTIONAL signature }
var ocspReq asn1.RawValue
if _, err := asn1.Unmarshal(reqDER, &ocspReq); err != nil {
// Not our problem — ocsp.ParseRequest already validated this
// path. Return "no nonce" rather than surfacing a redundant
// parse error to the caller.
return nil, false, nil
}
// Walk the SEQUENCE: tbsRequest is the first element.
var tbsRequest asn1.RawValue
rest, err := asn1.Unmarshal(ocspReq.Bytes, &tbsRequest)
if err != nil {
return nil, false, nil
}
_ = rest // optionalSignature ignored — we never validate request signatures
// TBSRequest ::= SEQUENCE { [0] version OPTIONAL, [1] requestorName
// OPTIONAL, requestList, [2] requestExtensions
// OPTIONAL }
//
// Walk the elements; pick out the [2] EXPLICIT tag.
tail := tbsRequest.Bytes
for len(tail) > 0 {
var elem asn1.RawValue
var rerr error
tail, rerr = asn1.Unmarshal(tail, &elem)
if rerr != nil {
return nil, false, nil
}
if elem.Class != asn1.ClassContextSpecific || elem.Tag != 2 {
continue
}
// elem.Bytes is the inner Extensions (which is a SEQUENCE OF
// Extension). Unmarshal into []pkix.Extension-equivalent.
return extractNonceFromExtensions(elem.Bytes)
}
return nil, false, nil
}
// extractNonceFromExtensions walks a SEQUENCE OF Extension looking for
// the id-pkix-ocsp-nonce OID. Returns the OCTET STRING contents on
// match, or (nil, false, nil) on no-match.
func extractNonceFromExtensions(extBytes []byte) ([]byte, bool, error) {
// extBytes is the SEQUENCE OF Extension wrapped in its outer
// SEQUENCE tag. Unwrap once.
var extSeq asn1.RawValue
if _, err := asn1.Unmarshal(extBytes, &extSeq); err != nil {
return nil, false, nil
}
tail := extSeq.Bytes
for len(tail) > 0 {
var ext struct {
ExtnID asn1.ObjectIdentifier
Critical bool `asn1:"optional"`
ExtnValue []byte
}
var rerr error
tail, rerr = asn1.Unmarshal(tail, &ext)
if rerr != nil {
// Try the no-Critical form (DER allows the BOOLEAN to be
// omitted entirely when DEFAULT FALSE).
var ext2 struct {
ExtnID asn1.ObjectIdentifier
ExtnValue []byte
}
tail2, rerr2 := asn1.Unmarshal(tail, &ext2)
if rerr2 != nil {
return nil, false, nil
}
tail = tail2
ext.ExtnID = ext2.ExtnID
ext.ExtnValue = ext2.ExtnValue
}
if !ext.ExtnID.Equal(OIDOCSPNonce) {
continue
}
// extnValue is itself a DER-encoded OCTET STRING (per RFC 6960
// §4.4.1: "The value of the extension SHALL be the value of a
// Nonce ::= OCTET STRING"). Unwrap once more.
var nonce []byte
if _, err := asn1.Unmarshal(ext.ExtnValue, &nonce); err != nil {
return nil, false, ErrOCSPNonceMalformed
}
if len(nonce) == 0 {
return nil, false, ErrOCSPNonceMalformed
}
if len(nonce) > MaxOCSPNonceLength {
return nil, false, ErrOCSPNonceMalformed
}
return nonce, true, nil
}
return nil, false, nil
}
// (The inverse — wrapping the nonce bytes back into an extnValue
// OCTET STRING — happens inline in the local issuer's
// SignOCSPResponse, where the response's ExtraExtensions field is
// populated. There's no need for a separate marshaling helper here:
// asn1.Marshal([]byte) produces the canonical OCTET STRING DER and
// is the entire extnValue payload per RFC 6960 §4.4.1.)