mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 16:21:30 +00:00
21aeed4f4e
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
174 lines
6.2 KiB
Go
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.)
|