Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CRL linting infrastructure #699

Merged
merged 14 commits into from
Mar 26, 2023
194 changes: 183 additions & 11 deletions v3/lint/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,34 @@ package lint
*/

import (
ox509 "crypto/x509"
"fmt"
"time"

"github.com/zmap/zcrypto/x509"
"github.com/zmap/zlint/v3/util"
)

// LintInterface is implemented by each Lint.
type LintInterface interface { //nolint:revive
// LintInterface is implemented by each certificate linter.
//
// @deprecated - use CertificateLintInterface instead.
type LintInterface = CertificateLintInterface

// RevocationListLintInterface is implemented by each revocation list linter.
type RevocationListLintInterface interface {
// CheckApplies runs once per revocation list. It returns true if the
// Lint should run on the given certificate. If CheckApplies returns
// false, the Lint result is automatically set to NA without calling
// CheckEffective() or Run().
CheckApplies(r *ox509.RevocationList) bool

// Execute() is the body of the lint. It is called for every revocation list
// for which CheckApplies() returns true.
Execute(r *ox509.RevocationList) *LintResult
}

// CertificateLintInterface is implemented by each certificate linter.
type CertificateLintInterface interface {
// CheckApplies runs once per certificate. It returns true if the Lint should
// run on the given certificate. If CheckApplies returns false, the Lint
// result is automatically set to NA without calling CheckEffective() or
Expand All @@ -40,10 +59,39 @@ type Configurable interface {
Configure() interface{}
}

// LintMeta struct represents the metadata associated with a single lint.
type LintMeta struct {
// Name is a lowercase underscore-separated string describing what a given
// Lint checks. If Name beings with "w", the lint MUST NOT return Error, only
// Warn. If Name beings with "e", the Lint MUST NOT return Warn, only Error.
Name string `json:"name,omitempty"`

// A human-readable description of what the Lint checks. Usually copied
// directly from the CA/B Baseline Requirements or RFC 5280.
Description string `json:"description,omitempty"`

// The source of the check, e.g. "BRs: 6.1.6" or "RFC 5280: 4.1.2.6".
Citation string `json:"citation,omitempty"`

// Programmatic source of the check, BRs, RFC5280, or ZLint
Source LintSource `json:"source"`

// Lints automatically returns NE for all certificates where CheckApplies() is
// true but with NotBefore < EffectiveDate. This check is bypassed if
// EffectiveDate is zero. Please see CheckEffective for more information.
EffectiveDate time.Time `json:"-"`

// Lints automatically returns NE for all certificates where CheckApplies() is
// true but with NotBefore >= IneffectiveDate. This check is bypassed if
// IneffectiveDate is zero. Please see CheckEffective for more information.
IneffectiveDate time.Time `json:"-"`
}

// A Lint struct represents a single lint, e.g.
// "e_basic_constraints_not_critical". It contains an implementation of LintInterface.
//
// @deprecated - use CertificateLint instead.
type Lint struct {

// Name is a lowercase underscore-separated string describing what a given
// Lint checks. If Name beings with "w", the lint MUST NOT return Error, only
// Warn. If Name beings with "e", the Lint MUST NOT return Warn, only Error.
Expand All @@ -68,24 +116,40 @@ type Lint struct {
// true but with NotBefore >= IneffectiveDate. This check is bypassed if
// IneffectiveDate is zero. Please see CheckEffective for more information.
IneffectiveDate time.Time `json:"-"`

// A constructor which returns the implementation of the lint logic.
Lint func() LintInterface `json:"-"`
}

// toCertificateLint converts a Lint to a CertificateLint for backwards compatibility.
//
// @deprecated - Use CertificateLint directly.
func (l *Lint) toCertificateLint() *CertificateLint {
return &CertificateLint{
LintMeta: LintMeta{
Name: l.Name,
Description: l.Description,
Citation: l.Citation,
Source: l.Source,
EffectiveDate: l.EffectiveDate,
IneffectiveDate: l.IneffectiveDate,
},
Lint: l.Lint,
}
}

// CheckEffective returns true if c was issued on or after the EffectiveDate
// AND before (but not on) the Ineffective date. That is, CheckEffective
// returns true if...
//
// c.NotBefore in [EffectiveDate, IneffectiveDate)
// c.NotBefore 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.
//
// @deprecated - use CertificateLint instead.
func (l *Lint) CheckEffective(c *x509.Certificate) bool {
onOrAfterEffective := l.EffectiveDate.IsZero() || util.OnOrAfter(c.NotBefore, l.EffectiveDate)
strictlyBeforeIneffective := l.IneffectiveDate.IsZero() || c.NotBefore.Before(l.IneffectiveDate)
return onOrAfterEffective && strictlyBeforeIneffective
return l.toCertificateLint().CheckEffective(c)
}

// Execute runs the lint against a certificate. For lints that are
Expand All @@ -97,14 +161,68 @@ func (l *Lint) CheckEffective(c *x509.Certificate) bool {
// CheckApplies()
// CheckEffective()
// Execute()
//
// @deprecated - use CertificateLint instead
func (l *Lint) Execute(cert *x509.Certificate, config Configuration) *LintResult {
return l.toCertificateLint().Execute(cert, config)
}

// CertificateLint represents a single x509 certificate linter.
type CertificateLint struct {
// Metadata associated with the linter.
LintMeta
// A constructor which returns the implementation of the linter.
Lint func() CertificateLintInterface `json:"-"`
}

// toLint converts a CertificateLint to Lint for backwards compatibility
//
// @deprecated - use CertificateLint directly.
func (l *CertificateLint) toLint() *Lint {
return &Lint{
Name: l.Name,
Description: l.Description,
Citation: l.Citation,
Source: l.Source,
EffectiveDate: l.EffectiveDate,
IneffectiveDate: l.IneffectiveDate,
Lint: l.Lint,
}
}

// CheckEffective returns true if c was issued on or after the EffectiveDate
// AND before (but not on) the Ineffective date. That is, CheckEffective
// returns true if...
//
// c.NotBefore 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 *CertificateLint) CheckEffective(c *x509.Certificate) bool {
onOrAfterEffective := l.EffectiveDate.IsZero() || util.OnOrAfter(c.NotBefore, l.EffectiveDate)
strictlyBeforeIneffective := l.IneffectiveDate.IsZero() || c.NotBefore.Before(l.IneffectiveDate)
return onOrAfterEffective && strictlyBeforeIneffective
}

// Execute runs the lint against a certificate. For lints that are
// sourced from the CA/B Forum Baseline Requirements, we first determine
// if they are within the purview of the BRs. See CertificateLintInterface
// for details about the other methods called.
// The ordering is as follows:
//
// Configure() ----> only if the lint implements Configurable
// CheckApplies()
// CheckEffective()
// Execute()
func (l *CertificateLint) Execute(cert *x509.Certificate, config Configuration) *LintResult {
if l.Source == CABFBaselineRequirements && !util.IsServerAuthCert(cert) {
return &LintResult{Status: NA}
}
return l.execute(l.Lint(), cert, config)
}

func (l *Lint) execute(lint LintInterface, cert *x509.Certificate, config Configuration) *LintResult {
func (l *CertificateLint) execute(lint LintInterface, cert *x509.Certificate, config Configuration) *LintResult {
configurable, ok := lint.(Configurable)
if ok {
err := config.Configure(configurable.Configure(), l.Name)
Expand All @@ -125,6 +243,60 @@ func (l *Lint) execute(lint LintInterface, cert *x509.Certificate, config Config
} else if !l.CheckEffective(cert) {
return &LintResult{Status: NE}
}
res := lint.Execute(cert)
return res
return lint.Execute(cert)
}

// RevocationListLint represents a single x509 CRL linter.
type RevocationListLint struct {
// Metadata associated with the linter.
LintMeta
// A constructor which returns the implementation of the linter.
Lint func() RevocationListLintInterface `json:"-"`
}

// CheckEffective returns true if r was generated on or after the EffectiveDate
// AND before (but not on) the Ineffective date. That is, CheckEffective
// returns true if...
//
// r.ThisUpdate 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 *RevocationListLint) CheckEffective(r *ox509.RevocationList) bool {
onOrAfterEffective := l.EffectiveDate.IsZero() || util.OnOrAfter(r.ThisUpdate, l.EffectiveDate)
strictlyBeforeIneffective := l.IneffectiveDate.IsZero() || r.ThisUpdate.Before(l.IneffectiveDate)
return onOrAfterEffective && strictlyBeforeIneffective
}

// Execute runs the lint against a revocation list.
// The ordering is as follows:
//
// Configure() ----> only if the lint implements Configurable
// CheckApplies()
// CheckEffective()
// Execute()
func (l *RevocationListLint) Execute(r *ox509.RevocationList, config Configuration) *LintResult {
lint := l.Lint()
configurable, ok := lint.(Configurable)
if ok {
err := config.Configure(configurable.Configure(), l.Name)
if err != nil {
details := fmt.Sprintf(
"A fatal error occurred while attempting to configure %s. Please visit the [%s] section of "+
"your provided configuration and compare it with the output of `zlint -exampleConfig`. Error: %s",
l.Name,
l.Name,
err.Error())
return &LintResult{
Status: Fatal,
Details: details}
}
}
if !lint.CheckApplies(r) {
return &LintResult{Status: NA}
} else if !l.CheckEffective(r) {
return &LintResult{Status: NE}
}
return lint.Execute(r)
}
Loading