Repository: zmap/zlint
Pull request #917: Framework for linting OSCP responses
Author: login: kowshikRoy
Created: 2025-02-17T12:50:53Z | Merged: 2025-03-23T15:38:49Z

Support for OCSP Response Linting


Diff:
diff --git a/v3/lint/base.go b/v3/lint/base.go
index 27869ecde..73b78ee3e 100644
--- a/v3/lint/base.go
+++ b/v3/lint/base.go
@@ -20,6 +20,7 @@ import (
 
 	"github.com/zmap/zcrypto/x509"
 	"github.com/zmap/zlint/v3/util"
+	"golang.org/x/crypto/ocsp"
 )
 
 // LintInterface is implemented by each certificate linter.
@@ -325,3 +326,60 @@ func checkEffective(effective, ineffective, target time.Time) bool {
 	strictlyBeforeIneffective := ineffective.IsZero() || target.Before(ineffective)
 	return onOrAfterEffective && strictlyBeforeIneffective
 }
+
+// OcspResponseLintInterface is implemented by each OCSP linter.
+type OcspResponseLintInterface interface {
+	// CheckApplies runs once per OCSP response. It returns true if the Lint
+	// should run on the given OCSP response. If CheckApplies returns false, the
+	// Lint result is automatically set to NA without calling CheckEffective()
+	// or Run().
+	CheckApplies(o *ocsp.Response) bool
+
+	// Execute is the body of the lint. It is called for every OCSP response for
+	// which CheckApplies returns true.
+	Execute(o *ocsp.Response) *LintResult
+}
+
+// OcspResponseLint represents a single OCSP response linter.
+type OcspResponseLint struct {
+	// Metadata associated with the linter.
+	LintMetadata
+	// A constructor which returns the implementation of the linter.
+	Lint func() OcspResponseLintInterface `json:"-"`
+}
+
+// CheckEffective returns true if o was generated on or after the EffectiveDate
+// AND before (but not on) the Ineffective date. That is, CheckEffective
+// returns true if...
+//
+//	o.NextUpdate in [EffectiveDate, IneffectiveDate)
+//
+// If EffectiveDate is zero, then only IneffectiveDate is checked. Conversely,
+// if IneffectiveDate is zero then only EffectiveDate is checked. If both EffectiveDate
+// and IneffectiveDate are zero then CheckEffective always returns true.
+func (l *OcspResponseLint) CheckEffective(o *ocsp.Response) bool {
+	return checkEffective(l.EffectiveDate, l.IneffectiveDate, o.NextUpdate)
+}
+
+// Execute runs the lint against an OCSP response.
+// The ordering is as follows:
+//
+// Configure() ----> only if the lint implements Configurable
+// CheckApplies()
+// CheckEffective()
+// Execute()
+func (l *OcspResponseLint) Execute(o *ocsp.Response, config Configuration) *LintResult {
+	lint := l.Lint()
+	err := config.MaybeConfigure(lint, l.Name)
+	if err != nil {
+		return &LintResult{
+			Status:  Fatal,
+			Details: err.Error()}
+	}
+	if !lint.CheckApplies(o) {
+		return &LintResult{Status: NA}
+	} else if !l.CheckEffective(o) {
+		return &LintResult{Status: NE}
+	}
+	return lint.Execute(o)
+}
diff --git a/v3/lint/lint_lookup.go b/v3/lint/lint_lookup.go
index 273aaec9c..bed8a72e5 100644
--- a/v3/lint/lint_lookup.go
+++ b/v3/lint/lint_lookup.go
@@ -24,6 +24,7 @@ var (
 	_ linterLookup               = &linterLookupImpl{}
 	_ CertificateLinterLookup    = &certificateLinterLookupImpl{}
 	_ RevocationListLinterLookup = &revocationListLinterLookupImpl{}
+	_ OcspResponseLinterLookup   = &ocspResponseLinterLookupImpl{}
 )
 
 type linterLookup interface {
@@ -215,3 +216,76 @@ func newRevocationListLintLookup() revocationListLinterLookupImpl {
 		lints:            make([]*RevocationListLint, 0),
 	}
 }
+
+// OcspResponseLinterLookup is an interface describing how registered OCSP response lints can be looked up.
+type OcspResponseLinterLookup interface {
+	linterLookup
+	// ByName returns a pointer to the registered lint with the given name, or nil
+	// if there is no such lint registered in the registry.
+	ByName(name string) *OcspResponseLint
+	// BySource returns a list of registered lints that have the same LintSource as
+	// provided (or nil if there were no such lints in the registry).
+	BySource(s LintSource) []*OcspResponseLint
+	// Lints returns a list of all the lints registered.
+	Lints() []*OcspResponseLint
+}
+
+type ocspResponseLinterLookupImpl struct {
+	linterLookupImpl
+	// lintsByName is a map of all registered lints by name.
+	lintsByName   map[string]*OcspResponseLint
+	lintsBySource map[LintSource][]*OcspResponseLint
+	lints         []*OcspResponseLint
+}
+
+// ByName returns the Lint previously registered under the given name with
+// Register, or nil if no matching lint name has been registered.
+func (lookup *ocspResponseLinterLookupImpl) ByName(name string) *OcspResponseLint {
+	lookup.RLock()
+	defer lookup.RUnlock()
+	return lookup.lintsByName[name]
+}
+
+// BySource returns a list of registered lints that have the same LintSource as
+// provided (or nil if there were no such lints).
+func (lookup *ocspResponseLinterLookupImpl) BySource(s LintSource) []*OcspResponseLint {
+	lookup.RLock()
+	defer lookup.RUnlock()
+	return lookup.lintsBySource[s]
+}
+
+// Lints returns a list of registered lints.
+func (lookup *ocspResponseLinterLookupImpl) Lints() []*OcspResponseLint {
+	lookup.RLock()
+	defer lookup.RUnlock()
+	return lookup.lints
+}
+
+func (lookup *ocspResponseLinterLookupImpl) register(lint *OcspResponseLint, name string, source LintSource) error {
+	if name == "" {
+		return errEmptyName
+	}
+	lookup.RLock()
+	defer lookup.RUnlock()
+
+	if existing := lookup.lintsByName[name]; existing != nil {
+		return &errDuplicateName{name}
+	}
+	lookup.lints = append(lookup.lints, lint)
+	lookup.lintNames = append(lookup.lintNames, name)
+	lookup.lintsByName[name] = lint
+
+	lookup.sources[source] = struct{}{}
+	lookup.lintsBySource[source] = append(lookup.lintsBySource[source], lint)
+	sort.Strings(lookup.lintNames)
+	return nil
+}
+
+func newOcspResponseLintLookup() ocspResponseLinterLookupImpl {
+	return ocspResponseLinterLookupImpl{
+		linterLookupImpl: newLinterLookup(),
+		lintsByName:      make(map[string]*OcspResponseLint),
+		lintsBySource:    make(map[LintSource][]*OcspResponseLint),
+		lints:            make([]*OcspResponseLint, 0),
+	}
+}
diff --git a/v3/lint/registration.go b/v3/lint/registration.go
index 01c09df83..5b2e3fa1c 100644
--- a/v3/lint/registration.go
+++ b/v3/lint/registration.go
@@ -112,12 +112,15 @@ type Registry interface { //nolint: interfacebloat // Somewhat unavoidable here.
 	CertificateLints() CertificateLinterLookup
 	// RevocationListLitns returns an interface used to lookup RevocationListLints.
 	RevocationListLints() RevocationListLinterLookup
+	// OcspResponseLints returns an interface used to lookup OcspResponseLints.
+	OcspResponseLints() OcspResponseLinterLookup
 }
 
 // registryImpl implements the Registry interface to provide a global collection
 // of Lints that have been registered.
 type registryImpl struct {
 	certificateLints    certificateLinterLookupImpl
+	ocspResponseLints   ocspResponseLinterLookupImpl
 	revocationListLints revocationListLinterLookupImpl
 	configuration       Configuration
 }
@@ -173,7 +176,7 @@ func (r *registryImpl) registerCertificateLint(l *CertificateLint) error {
 	return r.certificateLints.register(l, l.Name, l.Source)
 }
 
-// registerCertificateLint registers a CertificateLint to the registry.
+// registerRevocationListLint registers a RevocationListLint to the registry.
 //
 // An error is returned if the lint or lint's Lint pointer is nil, if the Lint
 // has an empty Name or if the Name was previously registered.
@@ -187,6 +190,23 @@ func (r *registryImpl) registerRevocationListLint(l *RevocationListLint) error {
 	return r.revocationListLints.register(l, l.Name, l.Source)
 }
 
+// register OcspResponseLint registers a OcspResponseLint to the registry.
+//
+// An error is returned if the lint or lint's Lint pointer is nil, if the Lint
+// has an empty Name or if the Name was previously registered.
+func (r *registryImpl) registerOcspResponseLint(l *OcspResponseLint) error {
+	if l == nil {
+		return errNilLint
+	}
+	if l.Lint() == nil {
+		return errNilLintPtr
+	}
+	if l.Name == "" {
+		return errEmptyName
+	}
+	return r.ocspResponseLints.register(l, l.Name, l.Source)
+}
+
 // ByName returns the Lint previously registered under the given name with
 // Register, or nil if no matching lint name has been registered.
 //
@@ -205,6 +225,7 @@ func (r *registryImpl) ByName(name string) *Lint {
 func (r *registryImpl) Names() []string {
 	var names []string
 	names = append(names, r.certificateLints.lintNames...)
+	names = append(names, r.ocspResponseLints.lintNames...)
 	names = append(names, r.revocationListLints.lintNames...)
 
 	sort.Strings(names)
@@ -236,6 +257,7 @@ func (r *registryImpl) Sources() SourceList {
 
 	sources = append(sources, r.certificateLints.Sources()...)
 	sources = append(sources, r.revocationListLints.Sources()...)
+	sources = append(sources, r.ocspResponseLints.Sources()...)
 	return sources
 }
 
@@ -247,6 +269,10 @@ func (r *registryImpl) RevocationListLints() RevocationListLinterLookup {
 	return &r.revocationListLints
 }
 
+func (r *registryImpl) OcspResponseLints() OcspResponseLinterLookup {
+	return &r.ocspResponseLints
+}
+
 // lintNamesToMap converts a list of lit names into a bool hashmap useful for
 // filtering. If any of the lint names are not known by the registry an error is
 // returned.
@@ -262,6 +288,11 @@ func (r *registryImpl) lintNamesToMap(names []string) (map[string]bool, error) {
 			namesMap[n] = true
 			continue
 		}
+		if l := r.ocspResponseLints.ByName(n); l != nil {
+			namesMap[n] = true
+			continue
+		}
+
 		if l := r.revocationListLints.ByName(n); l != nil {
 			namesMap[n] = true
 			continue
@@ -326,6 +357,14 @@ func (r *registryImpl) Filter(opts FilterOptions) (Registry, error) {
 			registerFunc = func() error {
 				return filteredRegistry.registerCertificateLint(l)
 			}
+		} else if l := r.ocspResponseLints.ByName(name); l != nil {
+			meta = l.LintMetadata
+			registerFunc = func() error {
+				if err := filteredRegistry.registerOcspResponseLint(l); err != nil {
+					return err
+				}
+				return nil
+			}
 		} else if l := r.revocationListLints.ByName(name); l != nil {
 			meta = l.LintMetadata
 			registerFunc = func() error {
@@ -366,6 +405,10 @@ func (r *registryImpl) WriteJSON(w io.Writer) {
 		//nolint:errchkjson
 		_ = enc.Encode(lint)
 	}
+	for _, lint := range r.ocspResponseLints.Lints() {
+		//nolint:errchkjson
+		_ = enc.Encode(lint)
+	}
 
 	for _, lint := range r.revocationListLints.Lints() {
 		//nolint:errchkjson
@@ -402,6 +445,15 @@ func (r *registryImpl) defaultConfiguration(globals []GlobalConfiguration) ([]by
 		}
 	}
 
+	for name, lint := range r.ocspResponseLints.lintsByName {
+		switch configurable := lint.Lint().(type) {
+		case Configurable:
+			configurables[name] = stripGlobalsFromExample(configurable.Configure())
+		default:
+
+		}
+	}
+
 	for name, lint := range r.revocationListLints.lintsByName {
 		switch configurable := lint.Lint().(type) {
 		case Configurable:
@@ -446,6 +498,7 @@ func (r *registryImpl) defaultConfiguration(globals []GlobalConfiguration) ([]by
 func NewRegistry() *registryImpl {
 	registry := &registryImpl{
 		certificateLints:    newCertificateLintLookup(),
+		ocspResponseLints:   newOcspResponseLintLookup(),
 		revocationListLints: newRevocationListLintLookup(),
 	}
 	registry.SetConfiguration(NewEmptyConfig())
@@ -482,6 +535,22 @@ func RegisterCertificateLint(l *CertificateLint) {
 	}
 }
 
+// RegisterOcspResponseLint must be called once for each OcspResponseLint to be executed.
+// Normally, RegisterOcspResponseLint is called from the Go init() function of a lint implementation.
+//
+// IMPORTANT: RegisterOcspResponseLint will panic if given a nil lint, or a lint
+// with a nil Lint pointer, or if the lint name matches a previously registered
+// lint's name. These conditions all indicate a bug that should be addressed by
+// a developer.
+func RegisterOcspResponseLint(l *OcspResponseLint) {
+	// RegisterLint always sets initialize to true. It's assumed this is called by
+	// the package init() functions and therefore must be doing the first
+	// initialization of a lint.
+	if err := globalRegistry.registerOcspResponseLint(l); err != nil {
+		panic(fmt.Sprintf("RegisterLint error: %v\n", err.Error()))
+	}
+}
+
 // RegisterRevocationListLint must be called once for each RevocationListLint to be executed.
 // Normally, RegisterRevocationListLint is called from the Go init() function of a lint implementation.
 //
diff --git a/v3/lints/rfc/lint_ocsp_this_update_not_after_produced_at.go b/v3/lints/rfc/lint_ocsp_this_update_not_after_produced_at.go
new file mode 100644
index 000000000..fabb88267
--- /dev/null
+++ b/v3/lints/rfc/lint_ocsp_this_update_not_after_produced_at.go
@@ -0,0 +1,62 @@
+package rfc
+
+/*
+ * ZLint Copyright 2024 Regents of the University of Michigan
+ *
+ * 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.
+ */
+
+import (
+	"github.com/zmap/zlint/v3/lint"
+	"github.com/zmap/zlint/v3/util"
+	"golang.org/x/crypto/ocsp"
+)
+
+type OCSPThisUpdateNotAfterProducedAt struct{}
+
+/*
+***********************************************
+RFC 5280: 5.3.1
+
+	CRL issuers are strongly
+	  encouraged to include meaningful reason codes in CRL entries;
+	  however, the reason code CRL entry extension SHOULD be absent instead
+	  of using the unspecified (0) reasonCode value.
+
+***********************************************
+*/
+func init() {
+	lint.RegisterOcspResponseLint(&lint.OcspResponseLint{
+		LintMetadata: lint.LintMetadata{
+			Name:          "e_this_update_not_after_produced_at",
+			Description:   "If a CRL entry has a reason code, it MUST be in RFC5280 section 5.3.1 and SHOULD be absent instead of using unspecified (0)",
+			Citation:      "RFC 5280: 5.3.1",
+			Source:        lint.RFC5280,
+			EffectiveDate: util.RFC5280Date,
+		},
+		Lint: NewOCSPThisUpdateNotAfterProducedAt,
+	})
+}
+
+func NewOCSPThisUpdateNotAfterProducedAt() lint.OcspResponseLintInterface {
+	return OCSPThisUpdateNotAfterProducedAt{}
+}
+
+func (l OCSPThisUpdateNotAfterProducedAt) CheckApplies(c *ocsp.Response) bool {
+	return true
+}
+
+func (l OCSPThisUpdateNotAfterProducedAt) Execute(c *ocsp.Response) *lint.LintResult {
+	if c.ThisUpdate.After(c.ProducedAt) {
+		return &lint.LintResult{Status: lint.Error}
+	}
+	return &lint.LintResult{Status: lint.Pass}
+}
diff --git a/v3/lints/rfc/lint_ocsp_this_update_not_after_produced_at_test.go b/v3/lints/rfc/lint_ocsp_this_update_not_after_produced_at_test.go
new file mode 100644
index 000000000..25a84bf71
--- /dev/null
+++ b/v3/lints/rfc/lint_ocsp_this_update_not_after_produced_at_test.go
@@ -0,0 +1,46 @@
+package rfc
+
+import (
+	"testing"
+
+	"github.com/zmap/zlint/v3/lint"
+	"github.com/zmap/zlint/v3/test"
+)
+
+/*
+ * ZLint Copyright 2024 Regents of the University of Michigan
+ *
+ * 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.
+ */
+
+func TestOCSPThisUpdateNotAfterProducedAt(t *testing.T) {
+	cases := []struct {
+		inputPath string
+		want      lint.LintStatus
+	}{
+		{
+			inputPath: "ocspThisUpdateNotAfterProducedAt",
+			want:      lint.Pass,
+		},
+		{
+			inputPath: "ocspThisUpdateAfterProducedAt",
+			want:      lint.Error,
+		},
+	}
+	for _, tc := range cases {
+		t.Run(tc.inputPath, func(t *testing.T) {
+			got := test.TestOCSPResponseLint(t, "e_this_update_not_after_produced_at", tc.inputPath).Status
+			if tc.want != got {
+				t.Errorf("%s: expected %s, got %s", tc.inputPath, tc.want, got)
+			}
+		})
+	}
+}
diff --git a/v3/resultset.go b/v3/resultset.go
index 5fd34d814..b6b95960d 100644
--- a/v3/resultset.go
+++ b/v3/resultset.go
@@ -17,6 +17,7 @@ package zlint
 import (
 	"github.com/zmap/zcrypto/x509"
 	"github.com/zmap/zlint/v3/lint"
+	"golang.org/x/crypto/ocsp"
 )
 
 // ResultSet contains the output of running all lints in a registry against
@@ -59,6 +60,20 @@ func (z *ResultSet) executeRevocationList(o *x509.RevocationList, registry lint.
 	}
 }
 
+// Execute lints on the given OCSP response with all of the lints in the provided
+// registry. The ResultSet is mutated to trace the lint results obtained from
+// linting the OCSP response.
+func (z *ResultSet) executeOcspResponse(o *ocsp.Response, registry lint.Registry) {
+	z.Results = make(map[string]*lint.LintResult, len(registry.Names()))
+	// Run each lints from the registry.
+	for _, lint := range registry.OcspResponseLints().Lints() {
+		res := lint.Execute(o, registry.GetConfiguration())
+		res.LintMetadata = lint.LintMetadata
+		z.Results[lint.Name] = res
+		z.updateErrorStatePresent(res)
+	}
+}
+
 func (z *ResultSet) updateErrorStatePresent(result *lint.LintResult) {
 	switch result.Status {
 	case lint.Notice:
diff --git a/v3/test/helpers.go b/v3/test/helpers.go
index 799591352..0af3852a1 100644
--- a/v3/test/helpers.go
+++ b/v3/test/helpers.go
@@ -17,6 +17,7 @@ package test
 // Contains resources necessary to the Unit Test Cases
 
 import (
+	"encoding/base64"
 	"encoding/pem"
 	"fmt"
 	"os"
@@ -26,6 +27,7 @@ import (
 
 	"github.com/zmap/zcrypto/x509"
 	"github.com/zmap/zlint/v3/lint"
+	"golang.org/x/crypto/ocsp"
 )
 
 // TestLint executes the given lintName against a certificate read from
@@ -68,6 +70,25 @@ func TestRevocationListLintWithConfig(tb testing.TB, lintName string, testCRLFil
 	return TestLintRevocationList(tb, lintName, ReadTestRevocationList(tb, testCRLFilename), config)
 }
 
+// TestOCSPResponseLint executes the given lintName against a OCSP Response read from
+// a testocspresponse data file with the given filename. Filenames should be relative to
+// `testdata/` and not absolute file paths.
+//
+//nolint:revive
+func TestOCSPResponseLint(tb testing.TB, lintName string, testOCSPResponseFilename string) *lint.LintResult {
+	tb.Helper()
+	return TestOCSPResponseLintWithConfig(tb, lintName, testOCSPResponseFilename, "")
+}
+
+func TestOCSPResponseLintWithConfig(tb testing.TB, lintName string, testOCSPResponseFilename string, configuration string) *lint.LintResult {
+	tb.Helper()
+	config, err := lint.NewConfigFromString(configuration)
+	if err != nil {
+		tb.Fatal(err)
+	}
+	return TestLintOCSPResponse(tb, lintName, ReadTestOCSPResponse(tb, testOCSPResponseFilename), config)
+}
+
 // TestLintCert executes a lint with the given name against an already parsed
 // certificate. This is useful when a unit test reads a certificate from disk
 // and then mutates it in some way before trying to lint it.
@@ -118,6 +139,30 @@ func TestLintRevocationList(tb testing.TB, lintName string, crl *x509.Revocation
 	return res
 }
 
+// TestLintOCSPResponse executes a lint with the given name against an already parsed
+// OCSP Response. This is useful when a unit test reads a OCSP Response from disk
+// and then mutates it in some way before trying to lint it.
+//
+//nolint:revive
+func TestLintOCSPResponse(tb testing.TB, lintName string, ocspResponse *ocsp.Response, ctx lint.Configuration) *lint.LintResult {
+	tb.Helper()
+	l := lint.GlobalRegistry().OcspResponseLints().ByName(lintName)
+	if l == nil {
+		tb.Fatalf(
+			"Lint name %q does not exist in lint.Lints. "+
+				"Did you forget to RegisterLint?\n",
+			lintName)
+	}
+	res := l.Execute(ocspResponse, ctx)
+	// We never expect a lint to return a nil LintResult
+	if res == nil {
+		tb.Fatalf(
+			"Running lint %q on test ocsp response generated a nil LintResult.\n",
+			lintName)
+	}
+	return res
+}
+
 // ReadTestCert loads a x509.Certificate from the given inPath which is assumed
 // to be relative to `testdata/`.
 //
@@ -192,3 +237,38 @@ func ReadTestRevocationList(tb testing.TB, inPath string) *x509.RevocationList {
 
 	return theCrl
 }
+
+// ReadTestOCSPResponse loads a ocsp.Response from the given inPath which is assumed
+// to be relative to `testdata/`. The OCSP file must contain the OCSP response in
+// Base64 encoding. openssl ocsp -resp_text -respin <(base64 -d the_filename)
+// To decode it using OpenSSL, store the base64-encoded in string in a file, and run:
+// Important: ReadTestOCSPResponse is only appropriate for unit tests. It will panic if
+// the inPath file can not be loaded.
+func ReadTestOCSPResponse(tb testing.TB, inPath string) *ocsp.Response {
+	tb.Helper()
+	fullPath := "../../testdata/" + inPath
+	base64Data, err := os.ReadFile(fullPath)
+	if err != nil {
+		tb.Fatalf("Failed to read file: %v", err)
+	}
+	data, err := base64.StdEncoding.DecodeString(string(base64Data))
+	if err != nil {
+		tb.Fatalf("Failed to decode base64 data: %v", err)
+	}
+	if err != nil {
+		tb.Fatalf(
+			"Unable to read test ocsp response from %q - %q "+
+				"Does a unit test have an incorrect test file name?\n",
+			fullPath, err)
+	}
+
+	theOcspResponse, err := ocsp.ParseResponse(data, nil)
+	if err != nil {
+		tb.Fatalf(
+			"Failed to parse ocsp response from %q - %q "+
+				"Does a unit test have a buggy test file?\n",
+			fullPath, err)
+	}
+
+	return theOcspResponse
+}
diff --git a/v3/testdata/ocspThisUpdateAfterProducedAt b/v3/testdata/ocspThisUpdateAfterProducedAt
new file mode 100644
index 000000000..b027fa8cc
--- /dev/null
+++ b/v3/testdata/ocspThisUpdateAfterProducedAt
@@ -0,0 +1,5 @@
+MIHuCgEAoIHoMIHlBgkrBgEFBQcwAQEEgdcwgdQwe6ECMAAYDzIwMjUwMjA0MTUyODAwWjBkMGIw
+OjAJBgUrDgMCGgUABBRDT/Kpele1XpFZGGOVYd+WA/SsxQQUqm/MRGGKLqVE27HcxUnzFM5U6PYC
+ASqAABgPMjAyNTAyMDQxNjI4NDVaoBEYDzIwMjUwMjA1MTUyODQ1WjAKBggqhkjOPQQDAgNJADBG
+AiEA0rOFxjS0O2ULpOHvz5Jv93PJRl0BDm48wajn6mwq7fQCIQC8kGIKMczLMaFhqdpjMWXMrZDH
+8VEut90a3SQ5EdQOug==
\ No newline at end of file
diff --git a/v3/testdata/ocspThisUpdateNotAfterProducedAt b/v3/testdata/ocspThisUpdateNotAfterProducedAt
new file mode 100644
index 000000000..de2b6631d
--- /dev/null
+++ b/v3/testdata/ocspThisUpdateNotAfterProducedAt
@@ -0,0 +1,5 @@
+MIHuCgEAoIHoMIHlBgkrBgEFBQcwAQEEgdcwgdQwe6ECMAAYDzIwMjUwMjA0MTUyNzAwWjBkMGIw
+OjAJBgUrDgMCGgUABBRDT/Kpele1XpFZGGOVYd+WA/SsxQQUdsgj8WVbiYiO6ywqW7jfqHcJD3kC
+ASqAABgPMjAyNTAyMDQxNDI3MDRaoBEYDzIwMjUwMjA1MTUyNzA0WjAKBggqhkjOPQQDAgNJADBG
+AiEAy6Yv3aPSXRZVoymldMckP4yksplvahFh/tCn4gbOGJsCIQCiKLtK0U2RZOXY8B9JG6qjwOsL
+7xHkVgk8n+/zBrTocA==
\ No newline at end of file
diff --git a/v3/zlint.go b/v3/zlint.go
index 93c7a2e14..07cd33b2b 100644
--- a/v3/zlint.go
+++ b/v3/zlint.go
@@ -30,6 +30,7 @@ import (
 	_ "github.com/zmap/zlint/v3/lints/etsi"
 	_ "github.com/zmap/zlint/v3/lints/mozilla"
 	_ "github.com/zmap/zlint/v3/lints/rfc"
+	"golang.org/x/crypto/ocsp"
 )
 
 const Version int64 = 3
@@ -90,3 +91,31 @@ func LintRevocationListEx(r *x509.RevocationList, registry lint.Registry) *Resul
 	res.Timestamp = time.Now().Unix()
 	return res
 }
+
+// LintOcspResponse runs all registered lints on o using default options,
+// producing a ResultSet.
+//
+// Using LintOcspResponse(o) is equivalent to calling LintOcspResponseEx(o, nil).
+func LintOcspResponse(o *ocsp.Response) *ResultSet {
+	return LintOcspResponseEx(o, nil)
+}
+
+// LintOcspResponseEx runs lints from the provided registry on o producing
+// a ResultSet. Providing an explicit registry allows the caller to filter the
+// lints that will be run. (See lint.Registry.Filter())
+//
+// If registry is nil then the global registry of all lints is used and this
+// function is equivalent to calling LintOcspResponse(o).
+func LintOcspResponseEx(o *ocsp.Response, registry lint.Registry) *ResultSet {
+	if o == nil {
+		return nil
+	}
+	if registry == nil {
+		registry = lint.GlobalRegistry()
+	}
+	res := new(ResultSet)
+	res.executeOcspResponse(o, registry)
+	res.Version = Version
+	res.Timestamp = time.Now().Unix()
+	return res
+}


Reviews:
user: login: christopher-henderson
body: Thank you very much for doing this legwork @kowshikRoy. Both on the code itself as well as digging up past conversations.

This isn't an action item for this pull request, more of a meta comment to myself, but it is quite clear that this codebase has gotten a bit redundant. It could benefit from, perhaps, generics. I had shyed away from generics several years ago as ZLint is indeed used as a library and I didn't want to force a Golang upgrade across the ecosystem. But it's been some time, so I may revisit the topic.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-23T14:58:12Z

Review comments:
diff_hunk: @@ -326,6 +357,14 @@ func (r *registryImpl) Filter(opts FilterOptions) (Registry, error) {
 			registerFunc = func() error {
 				return filteredRegistry.registerCertificateLint(l)
 			}
+		} else if l := r.ocspResponseLints.ByName(name); l != nil {
+			meta = l.LintMetadata
+			registerFunc = func() error {
+				if err := filteredRegistry.registerOcspResponseLint(l); err != nil {
path: v3/lint/registration.go
user: login: christopher-henderson
body: ```suggestion
				return filteredRegistry.registerOcspResponseLint(l) {
```
created_at: 2025-03-23T14:51:21Z
author_association: MEMBER
diff_hunk: @@ -482,6 +535,22 @@ func RegisterCertificateLint(l *CertificateLint) {
 	}
 }
 
+// RegisterOcspResponseLint must be called once for each OcspResponseLint to be executed.
+// Normally, RegisterOcspResponseLint is called from the Go init() function of a lint implementation.
+//
+// IMPORTANT: RegisterOcspResponseLint will panic if given a nil lint, or a lint
path: v3/lint/registration.go
user: login: christopher-henderson
body: Thank you for the panic documentation.
created_at: 2025-03-23T14:53:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,62 @@
+package rfc
+
+/*
+ * ZLint Copyright 2024 Regents of the University of Michigan
+ *
+ * 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.
+ */
+
+import (
path: v3/lints/rfc/lint_ocsp_this_update_not_after_produced_at.go
user: login: christopher-henderson
body: Thank you for some first lints to show the system in action.
created_at: 2025-03-23T14:54:23Z
author_association: MEMBER

PR comments:
user: login: christopher-henderson
created_at: 2025-03-08T14:33:21Z
body: Howdy @kowshikRoy! This is very cool, although I do need help understanding the impetus for support OSCP response linting.

Is there a history of non-compliance with [RFC 6960](https://www.rfc-editor.org/rfc/rfc6960) that would help me get a sense of the need for an OCSP linter? Or would you happen to have insights into CAs wishing that they had this linter?

I'm not opposed to such additions to ZLint. I would consider OCSP within the realm of the web PKI and thus not outside ZLint's jurisdiction in the slightest. However, I also do not wish to maintain code for which no CA has shown an interest in using.
author_association: MEMBER
user: login: kowshikRoy
created_at: 2025-03-17T14:55:07Z
body: Hi @christopher-henderson 
We're Google Trust Services(https://pki.goog) are interested to have OCSP linting. We currently use zlint to lint our certificates and CRLs. We are planning to add some checks related to OCSP. 
I see that there are previous issues #458 where people have shown interested having this linting within an industry-standard tooling.
author_association: CONTRIBUTOR