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

RFC for Configurable Lints #624

Closed
christopher-henderson opened this issue Jul 31, 2021 · 7 comments · Fixed by #648
Closed

RFC for Configurable Lints #624

christopher-henderson opened this issue Jul 31, 2021 · 7 comments · Fixed by #648
Assignees

Comments

@christopher-henderson
Copy link
Member

christopher-henderson commented Jul 31, 2021

Purpose

Today, the only facts that are provided to ZLint are those that are present within the target x509 certificate. That is to say, the only input to the linter is the certificate itself. This works fairly well, in general, due to the fact that most requirements refer to data encoded within the certificate itself.

This is not, however, universally true. A lint may require access to the full certificate chain in order to deterministically come to a conclusion (#491, Ocsp eku check for non tls certificates). Or language may specify that the requirement only takes effect if certain conditions regarding the certificate's cross signer are true (#619, Lint that the EKU values within a SubCA certificate comply with CABF_BR 7.1.2.2 letter g)). And ETSI lints have had a history of being inaccurate due to requiring outside data (#581, Bug: ESI lints are not consistent with ETSI EN 319 412-5 requirements regarding non-qualified certificates).

If the certificate in isolation is in its self insufficient for the accurate execution of a given lint, then it seems that additional context must be shuttled to the lint in some way.

Proposal

This proposal is to introduce a second, optional, input file into the ZLint command line interface which represents a configuration/context for that individual run. A supporting framework is also proposed for lints to declare an interest in arbitrary configurations.

Goals

  • Non-breaking change. All current invocations of ZLint, as well as all extant lints, MUST remain valid.
  • Operators SHOULD be able to easy discover, and generate, configurations relevant to the lints that they are targeting.
  • Lint authors SHOULD NOT be required to add or edit any further files than they already do. That is, lints should stay localized to their individual files and tests.
  • Lints that are configurable MUST have a testing framework available to them so that they may construct unit tests against any number of arbitrary configuration combinations.

Code Interface

Lints may implement a new, optional, interface - Configurable...

type Configurable interface {
    Configure() interface{}
}

...where the returned interface{} is a pointer to the target struct to deserialize your configuration into.

This struct may encode any arbitrary data that may be deserialized from TOML. Examples may include:

  • PEM encoded certificates or certificate chains
  • File paths
  • Resolvable DNS entries or URIs
  • Dates or Unix timestamps

...and so on. How stable and/or appropriate a given configuration field is is left as a code review exercise on a per-lint basis.

If a lint is Configurable then a new step is injected at the beginning of its lifecycle.


Non-Configurable Lifecycle
  • CheckApplies
  • CheckEffective
  • Execute
Configurable Lifecycle
  • Configure
  • CheckApplies
  • CheckEffective
  • Execute

Example Lints

Contrived Examples

The following is a set of complete examples showcasing how one might write lints to utilize this configuration framework. They are contrived and solve no real problem in-and-of themselves. They are merely demonstrative.

The following lint encodes its configuration within the struct that is the lint itself. The lint itself is rather silly in that it is asserting that the signing certificate authority must play certain games while on a road trip - information that is clearly not encoded in any certificate ever issued.

package cabf_br

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


type LintItselfIsConfigurable struct {
	BottlesOfBeerOnTheWall int `comment:"This MUST be set to the number of bottles of beer that were on the signing CAs wall at the time of issuance."`
	ISpyWithMyLittleEye    struct {
		Descriptions []string `comment:"Any number of descriptions that are valid for the subject."`
		Subject      string
        // Go naming convention does not often match external naming conventions, so the ability
        // to alias the name used within the target configuration is indeed useful.
	} `toml:"i_spy_with_my_little_eye"`
}

func init() {
	lint.RegisterLint(&lint.Lint{
		Name:          "e_ca_too_few_beers",
		Description:   "CA Certificates MUST have at least 99 beers.",
		Citation:      "BRs: 7.1.4.3.1",
		Source:        lint.CABFBaselineRequirements,
		EffectiveDate: util.CABV148Date,
		Lint:          NewLintItselfIsConfigurable,
	})
}

func NewLintItselfIsConfigurable() lint.LintInterface {
	// Go code encourages that the zero value for a struct be valid, however
	// if you wish for your Lint to have non-zero defaults then your constructor is
	// the place to do so. The value initialized in this constructor WILL appear
	// in the example configuration printed through the `-exampleConfig flag`.
	return &LintItselfIsConfigurable{ISpyWithMyLittleEye: struct {
		Descriptions []string `comment:"Any number of descriptions that are valid for the subject."`
		Subject      string
	}{Descriptions: []string{"larger than a bread box", "smaller than a barn"}, Subject: "A car"}}
}

// In this case, the struct that is the lint itself is the target for configuration.
func (l *LintItselfIsConfigurable) Configure() interface{} {
	return l
}

func (l *LintItselfIsConfigurable) CheckApplies(c *x509.Certificate) bool {
	return util.IsCACert(c)
}

func (l *LintItselfIsConfigurable) Execute(c *x509.Certificate) *lint.LintResult {
	if l.BottlesOfBeerOnTheWall < 99 {
		return &lint.LintResult{Status: lint.Error}
	} else {
		return &lint.LintResult{Status: lint.Pass}
	}
}

The following lint takes on a different strategy of aggregating a configurable struct rather than being the target struct itself.

package cabf_br

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


type LintEmbedsAConfiguration struct {
        // The configuration field is not exported, however it is
        // explicitly returned in the Configure method, so it will be
        // targeted for deserialization.
	configuration                        embeddedConfiguration
        // This field is exported, however it is not a member of
        // returned by Configure, thus effectively making it
        // private in the eyes of the configuration framework.
	SomeOtherFieldThatWeDontWantToExpose int
}

// This lints configuration has only one question - is the certificate intended for the web PKI?
type embeddedConfiguration struct {
	IsWebPKI bool `toml:"is_web_pki" comment:"Indicates that the certificate is intended for the Web PKI."`
}

func init() {
	lint.RegisterLint(&lint.Lint{
		Name:          "w_web_pki_cert",
		Description:   "CA Certificates SHOULD....something....about the web pki",
		Citation:      "BRs: 7.1.4.3.1",
		Source:        lint.CABFBaselineRequirements,
		EffectiveDate: util.CABV148Date,
		Lint:          NewLintEmbedsAConfiguration,
	})
}

// A pointer to an embedded struct may be passed to the framework
// if the author does not wish to expose certain fields in their primary struct.
func (l *LintEmbedsAConfiguration) Configure() interface{} {
	return &l.configuration
}

func NewLintEmbedsAConfiguration() lint.LintInterface {
	return &LintEmbedsAConfiguration{configuration: embeddedConfiguration{}}
}

func (l *LintEmbedsAConfiguration) CheckApplies(c *x509.Certificate) bool {
	return util.IsCACert(c)
}

func (l *LintEmbedsAConfiguration) Execute(c *x509.Certificate) *lint.LintResult {
	if l.configuration.IsWebPKI {
		return &lint.LintResult{Status: lint.Warn}
	} else {
		return &lint.LintResult{Status: lint.Pass}
	}
}

Given the above lints, operators may then generate a sample configuration which contains the defaults for every lint that is configurable.

./zlint -exampleConfig
[Apple]

[CABF_BR]

[CABF_EV]

[Community]

[ETSI_ESI]

[Mozilla]

[RFC5280]

[RFC5480]

[RFC5891]

[e_ca_too_few_beers]
# This MUST be set to the number of bottles of beer that were on the signing CAs wall at the time of issuance.
BottlesOfBeerOnTheWall = 0

[e_ca_too_few_beers.i_spy_with_my_little_eye]
# Any number of descriptions that are valid for the subject.
Descriptions = ["larger than a bread box", "smaller than a barn"]
Subject = "A car"

[w_web_pki_cert]
# Indicates that the certificate is intended for the Web PKI.
is_web_pki = false

Note that comments declared in the provided structs ARE present within example configuration generated by for operators. As such, lint authors SHOULD provide comments describing the purposes of their fields as well as the impact that those fields have on their given lint.

Higher Scoped Configurations

Lints may embed within theselves either pointers or structs to the following definitions.

type Global struct {}
type RFC5280Context struct{}
type RFC5480Context struct{}
type RFC5891Context struct{}
type CABFBaselineRequirementsContext struct {}
type CABFEVGuidelinesContext struct{}
type MozillaRootStorePolicyContext struct{}
type AppleRootStorePolicyContext struct{}
type CommunityContext struct{}
type EtsiEsiContext struct{}

NOTE: These structs are currently empty and the following are merely examples of the sort of data that may be stored in them. For the sake of maintainability, predictability, and usability by CAs these structs do not allow for arbitrary key:value mappings at runtime and thus must be statically defined. New fields may be added to these structs on a case-by-case bases when there is a consensus that a particular configuration may be valuable to share among many lints of a particular class.

Doing so allows for the reuse of common configurations among entire classes of lints.

some_global = true
some_other_global = "So long and thanks for all the fish"

[Apple]

[CABF_BR]

[CABF_EV]

[Community]

[ETSI_ESI]
is_web_pki = true

[Mozilla]

[RFC5280]

[RFC5480]

[RFC5891]

[e_must_be_good_weather]
was_sunny_during_signing_ceremony = true
type CaresAboutWebPKI struct {
    Etsi lint.EtsiEsiContext
}

type CaresAboutETSIandGlobal struct {
    Etsi    lint.EtsiEsiContext
    Global  lint.Global
}

type MustBeGoodWeather struct {
    Etsi lint.EtsiEsiContext
    WasSunnyDuringSigningCeremony bool `toml="was_sunny_during_signing_ceremony" comment="It MUST be a nice day"`
}

Lints receive copies of higher scoped configurations (even in the event of embedding a pointer) in order to avoid surprising mutations across many lints.

Live Example

The following example implementation is derived from #619. It serves as real world, and immediate, usecase for these new configuration facilities. There may be, however, a more stable and/or correct implementation - this is merely serving as a motivating example.

package cabf_br

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

func init() {
	lint.RegisterLint(&lint.Lint{
		Name:          "e_sub_ca_eku_incompatible_values",
		Description:   "Subordinate CA extkeyUsage: if serverAuth is present, then emailProtection, codeSigning, timeStamping, and anyExtendedKeyUsage MUST NOT be present.",
		Citation:      "BRs: 7.1.2.2",
		Source:        lint.CABFBaselineRequirements,
		EffectiveDate: util.CABFBRs_1_7_1_Date,
		Lint:          NewSubCAEkuIncompatibleValues,
	})
}

type subCAEkuIncompatibleValues struct {
	CrossSignerPEM string `comment:"Section 7.1.2.2: For Cross Certificates that share a Subject Distinguished Name and Subject Public Key with a Root Certificate operated in accordance with these Requirements, this extension MAY be present. If present, this extension SHOULD NOT be marked critical. This extension MUST only contain usages for which the issuing CA has verified the Cross Certificate is authorized to assert. This extension MAY contain the anyExtendedKeyUsage [RFC5280] usage, if the Root Certificate(s) associated with this Cross Certificate are operated by the same organization as the issuing Root Certificate."`
}

func (l *subCAEkuIncompatibleValues) Configure() interface{} {
	return l
}

func NewSubCAEkuIncompatibleValues() lint.LintInterface {
	return &subCAEkuIncompatibleValues{}
}

func (l *subCAEkuIncompatibleValues) CheckApplies(c *x509.Certificate) bool {
	return util.IsSubCA(c) && util.IsExtInCert(c, util.EkuSynOid)
}

func (l *subCAEkuIncompatibleValues) Execute(c *x509.Certificate) *lint.LintResult {
	// Check if l.CrossSigner was provided.
	// Parse to cert.
	// Do appropriate checks against section 7.1.2.2
	...
}

This structure generates the following configuration.

[e_sub_ca_eku_incompatible_values]
# Section 7.1.2.2:  For Cross Certificates that share a Subject Distinguished Name and Subject Public Key with a Root Certificate operated in accordance with these Requirements, this extension MAY be present. If present, this extension SHOULD NOT be marked critical. This extension MUST only contain usages for which the issuing CA has verified the Cross Certificate is authorized to assert. This extension MAY contain the anyExtendedKeyUsage [RFC5280] usage, if the Root Certificate(s) associated with this Cross Certificate are operated by the same organization as the issuing Root Certificate.
CrossSignerPEM = ""

With the cross signer certificate filled in...

[e_sub_ca_eku_incompatible_values]
CrossSignerPEM = """
-----BEGIN CERTIFICATE-----
MIIBBDCBrKADAgECAgEBMAoGCCqGSM49BAMCMAAwIhgPMDAwMTAxMDEwMDAwMDBa
GA85OTk4MTEzMDAwMDAwMFowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDaq
E7tJ1D0lKPlIYcMylZ47WsEAFypRlBVUni+Q8ZY4S+NPVHFDxpZ/Qzx8qnXmHvf6
8caqXJthylrSzH1HVGGjEzARMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwID
RwAwRAIgJklSU+eEIOc+oUHa/ugiNsBpTsrrrJudXuZ41hayNQsCIEwxc3quig63
ZI7LhYni7wlyXJBiXUlQkuacCGdqkv+/
-----END CERTIFICATE-----
"""

Example CLI Usages

Operators will be able to generate a configuration file which contains all available configurations with ZLint initialized to their defualt values.

Retrieving an Example Configuration

$ ./zlint -exampleConfig > config.toml
# Aside from the global configurations, this example shows
# that ZLint has exactly one configurable lint, which has one
# string value (for which the empty string is the default value).
$ cat config.toml
[e_sub_ca_eku_incompatible_values]
CrossSignerPEM = ""

Running Against a Configuration.

Running against a configuration is as simple as pointing the -config flag to a file path.

$ cat >> config.toml << EOF
[e_sub_ca_eku_incompatible_values]
CrossSignerPEM = """
-----BEGIN CERTIFICATE-----
MIIBBDCBrKADAgECAgEBMAoGCCqGSM49BAMCMAAwIhgPMDAwMTAxMDEwMDAwMDBa
GA85OTk4MTEzMDAwMDAwMFowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDaq
E7tJ1D0lKPlIYcMylZ47WsEAFypRlBVUni+Q8ZY4S+NPVHFDxpZ/Qzx8qnXmHvf6
8caqXJthylrSzH1HVGGjEzARMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwID
RwAwRAIgJklSU+eEIOc+oUHa/ugiNsBpTsrrrJudXuZ41hayNQsCIEwxc3quig63
ZI7LhYni7wlyXJBiXUlQkuacCGdqkv+/
-----END CERTIFICATE-----
"""
EOF
$ ./zlint -config config.toml /path/to/cert/to/lint

Testing

The linting test package provides the convenience function test.TestLint(lintName string, testCertFilename string).

func TestCaCommonNameNotMissing(t *testing.T) {
	inputPath := "caCommonNameNotMissing.pem"
	expected := lint.Pass
	out := test.TestLint("e_ca_common_name_missing", inputPath)
	if out.Status != expected {
		t.Errorf("%s: expected %s, got %s", inputPath, expected, out.Status)
	}
}

In addition to the above, a new facility shall be added that allows for the execution of a test against an arbitrary TOML string - test.TestLintWithCtx(lintName string, testCertFilename string, context string).

func TestCaCommonNameNotMissing22(t *testing.T) {
	inputPath := "caCommonNameNotMissing.pem"
	expected := lint.Pass
	ctx := `
            [e_ca_common_name_missing]
            some_field = true
	`
	out := test.TestLintWithCtx("e_ca_common_name_missing", inputPath, ctx)
	if out.Status != expected {
		t.Errorf("%s: expected %s, got %s", inputPath, expected, out.Status)
	}
}

Choice of TOML

vs. JSON

JSON does not allow for comments in its documents. This alone is the primary disqualifier.

Comments from lint authors to CAs are very important for conveying the purpose and impact of individual fields. Additionally, it is likely that CAs will version control (and possibly even template) their own configurations, in which case it is an attractive proposition that CAs be able to leave comments in their internal configurations in order to track their own policy decisions.

vs. YAML

YAML allows for comments. However, it is a far richer (and thus far more complex) lanuage than TOML is. It is not uncommon for humans to have difficulty writing and editing YAML by hand.

Should use cases arise such that the complexity of YAML becomes desirable then it may be a consideration for usage.

Noted Dissatisfactions

Long Single Line Comments

Lint authors provide comments in their configurations via the comment:"<text>" Go tag. Unfortunately, Go does not allow for multiline tags nor does it allow for usages of const or var strings within tags - all tags must be string literals. This means that, as is, it is very likely that the majority of comments are going to break code style rules simply by being even reasonably descriptive. This is seen immediately in the example provided in Live Example.

There may be remediations to this. One is to have an implicit, reflective, interface.

  1. Find all public fields within the struct.
  2. For every public field, attempt to find a public method named <Field>Comment that returns a string.
type MyLint struct {
    CrossSigner string
}

func CrossSignerComment() string {
    return `
For Cross Certificates that share a Subject Distinguished Name and 
Subject Public Key with a Root Certificate operated in accordance 
with these Requirements, this extension MAY be present. If present, 
this extension SHOULD NOT be marked critical. This extension MUST 
only contain usages for which the issuing CA has verified the 
Cross Certificate is authorized to assert. This extension MAY contain 
the anyExtendedKeyUsage [RFC5280] usage, if the Root Certificate(s) 
associated with this Cross Certificate are operated by the same 
organization as the issuing Root Certificate.
`
}

However, spike work has not yet been done on accomplishing an interface such as this. As such it is not yet known how conducive our TOML framework would be towards being extended in such a way.

Manual Certificate Parsing

The TOML serde framework does not have a notion of x509 certificates and, as such, lint authors are backed into the wall of declaring string fields and documenting that they MUST be properly formatted PEMs and then do the parsing and error handling themselves. This is seen in the Live Example.

Possible remediations include:

  • Providing a shim struct that both knows how to deserialize from TOML and how to convert itself into a *x509.Certificate.
type MyLint struct {
    CrossSigner *TomlCertificate
}

func (m *MyLint) Execute() {
    var cert *x509.Certificate = m.CrossSigner.GetCertificate()
}
  • Provide utils facilites.
type MyLint struct {
    CrossSigner string
}

func (m *MyLint) Execute() {
    var cert *x509.Certificate = utils.CertificateFromToml(m.CrossSigner)
}
@christopher-henderson christopher-henderson self-assigned this Jul 31, 2021
@christopher-henderson christopher-henderson changed the title Candidate RFC for Configurable Lints Draft RFC for Configurable Lints Jul 31, 2021
@christopher-henderson christopher-henderson changed the title Draft RFC for Configurable Lints Draft RFC for Configurable Lints [WIP] Jul 31, 2021
@sleevi
Copy link
Contributor

sleevi commented Aug 6, 2021

Just ACKing that I have seen it, I feel bad for not having meaningfully commented yet, and that I'm still noodling this (in part, to think of counterfactuals or edge cases to sort through)

@christopher-henderson
Copy link
Member Author

It's good @sleevi, there's a fair amount here. And regardless I wanted to figure out that the "higher scoped configurations" problem was at least technically feasible without being too filthy with reflection. I think I have some reasonable enough code local that I'm comfortable pitching the idea as something that can actually be delivered reasonably well.

@christopher-henderson
Copy link
Member Author

Howdy folks @sleevi @cpu @robplee @zakird. I've reached a point where I have enough of a complete working example on my local of the above proposal that I am comfortable in taking the [WIP] tag away and discussing concrete details and alternatives.

My goal is for this to be a non-breaking change, however it is a large change with user impact for both consumers of the CLI tool as well as lint authors. As such, I am eyeing getting this proposal out onto the mailing list by the end of the month so that I may solicit UX feedback from the community'/s stakeholders. I would appreciate a quick scan for anything that is glaringly bad or obtuse before doing so.

@sleevi
Copy link
Contributor

sleevi commented Aug 23, 2021

@christopher-henderson Thanks for the long - and detailed - comment on the design!

I'll be honest, I'm not too familiar with the Go idioms to be able to usefully comment on the implementation strategy, so this may feel a bit of 'high level' feedback without much concrete actionable, directed change. I definitely think getting feedback from CAs is useful, and I think the most useful feedback from them is from the sort of ergonomic example you mentioned re: #619 . In the W3C, we use the term for this of explainers, to sort of capture "problems to solve" / "possible shapes of solution" and use that to iterate on the understanding, before drilling down into the implementation details.

To the topic of ergonomics, I think we should look at existing CA software, to see how it's likely integrated. Far and away, the most common CA software implementation seems to be EJBCA in some variation. EJBCA has a configuration known as post-processing validators. Validators can be configured either for all certificates issued from a given CA, or only for particular certificates for a given profile.

So when we think about something like #619, it's likely to imagine that there is a "Subordinate CA Certificate" profile existing under the Root Certificate. And this profile will be used for multiple sub-CAs. As such, I'm a little concerned that a lint design like that outlined would be difficult - you wouldn't want to create a configuration-per-cert you're issuing, but instead want something dynamic. For example, for this lint to be useful, we'd probably want something like a filename of "all (existing) sub-CAs", such that CheckApplies and friends can open the file, and see if any certificates match the subject / issuer, and apply rules from there.

This suggests that we need a signal mechanism for invalid configurations - e.g. what if the file can't be loaded? But, naively, the design I suggest above also seems to have a problem: it fails open (i.e. if no matching certificate is found, it assumes it's not cross-signed) rather than fails closed (i.e. requires explicit confirmation/manual review that the condition holds).

Iterating on that design, then, it seems like a lint for #619 would say require that the configuration for this lint might be a list of (encoded) DNs, and an affirmative statement by the CA as to which state the DN is in (i.e. "This DN is not cross-certified" vs "This DN is cross-certified"). Any attempt to issue a new DN would cause the lint to fail (for a non-allowlisted DN), which would then prompt a manual review, and a manual reconfiguration of the lint, to explicitly state which it was.

For something like #491 , though, we might do something similar to what you proposed: for example, because we assume responders will only be for the CAs own certificate, have a file that contains every CA certificate from a given CA organization, and then the idea being the responder-validating lint could look up in that file to see all the issuers that match, see if any have the serverAuth bit, and determine from there.

Taking a step back, then, from these detailed cases, I guess my questions are:

  • Do we have a way to good way signal configuration validation errors? Especially with semantic concepts like "certificates we can parse" or "files on the filesystem that we need to open"
  • Is this current configuration optimized for "per-certificate invocation" (as the real world example read), or am I right in reading that it's flexible enough for the EJBCA-like configuration of "All certificates from this CA" and/or "All certificates according to this CA profile"?

@christopher-henderson
Copy link
Member Author

Thank you @sleevi! The Explainers...explanation...seems like it'll be useful, even in general. As in, it's just a good way to speak with other people.

Do we have a way to good way signal configuration validation errors?

It sounds like you are referring to semantic validation, right? As in, yeah, you gave me a file path, but I'm telling you it's just not there? In that case, I reckon that if the lint in question has taken on the burden of opening files (which, at this point, it would have to without putting in significantly more work on the surrounding infrastructure) that it can signal such a semantic failure from within its Execute method via lint.Fatal.

That is, we have the following lint statuses available to us:

const (
	// Unused / unset LintStatus
	Reserved LintStatus = 0

	// Not Applicable
	NA LintStatus = 1

	// Not Effective
	NE LintStatus = 2

	Pass   LintStatus = 3
	Notice LintStatus = 4
	Warn   LintStatus = 5
	Error  LintStatus = 6
	Fatal  LintStatus = 7
)

Of which I believe Fatal is intended for exactly this sort of thing (if not, then we can always add more).

type MyLint struct {
    config MyConfiguration
}

type MyConfiguration struct {
    subordinate string
}

func (l *MyLint) Execute(cert *x509.Certificate) {
    bytes, err := ioutil.ReadAll(l.config.subordinate)
    if err != nil {
        return &lint.LintResult{Status: lint.Fatal, Details: "I don't think so, Tim"}
    }
}

This could extend out to pretty much any ad-hoc validation error that could occur (parse failures, certificates that appear to be completely unrelated to the lint, stringly typed validation errors, and so on).

Is this current configuration optimized for "per-certificate invocation" (as the real world example read), or am I right in reading that it's flexible enough for the EJBCA-like configuration of "All certificates from this CA" and/or "All certificates according to this CA profile"?

In my mind, it would be easy enough to build configurations that can apply to a whole cache of certificates.

[some_lint]
web_pki =  false
[some_lint]
web_pki =  true
.
└── certs
    ├── internal_pki
    │   ├── cert1.crt
    │   ├── cert2.crt
    │   ├── cert3.crt
    │   └── config.toml <--- web_pki = false
    └── web_pki
        ├── cert1.crt
        ├── cert2.crt
        ├── cert3.crt
        └── config.toml <--- web_pki = true

You can even express hierarchies by concatenating/merging TOML documents. Let's take the following where some certs are intended for the web PKI but are not cross signed, and some certs are both intended for the web PKI and are cross signed.

.
└── certs
    └── web_pki
        ├── cert1.crt
        ├── cert2.crt
        ├── cert3.crt
        ├── config.toml
        └── cross_signed
            ├── cert1.crt
            ├── cert2.crt
            ├── cert3.crt
            └── config.toml

Let's say that certs/web_pki/config.toml is the following:

[some_lint]
web_pki = true

..and that certs/web_pki/cross_signed/config.toml is the following:

[some_other_lint]
cross_signed_by = "/path/to/cert.crt"

Well, the simple concatenation of those two config files is itself a valid config file.

[some_lint]
web_pki = true
[some_other_lint]
cross_signed_by = "/path/to/cert.crt"

In that way you can achieve a hierarchy through some simple bash scripting.

MERGED=$(mktemp)
cat web_pki/config.toml web_pki/cross_signed/config.toml > "${MERGED}"
for cert in web_pki/cross_signed/*.crt; do
    zlint --config "${MERGED}" "${cert}"
done

Alternatively, zlint can always take in a list of configurations and merge them internally.

zlint --configList confg1.toml,config2.toml,config3.toml cert.crt

@sleevi
Copy link
Contributor

sleevi commented Aug 28, 2021 via email

@christopher-henderson
Copy link
Member Author

Is this the right way to be thinking about how linting works? That is, our
two main use cases for linting are CAs doing pre-issuance lints and
observers doing post-issuance lints.

Well perhaps change the word cache to class and that might help. As in, this is the configuration we use for all new issuances of <some category>.

or we just KISS and say you need to merge all of this beforehand.

You are absolutely right in that duplicate keys/sections become very difficult to reason with regard to which one would take affect. If I were using this system I would do what you mention, which seems to be simply writing your different configuration files for different classes of certs, be done with it, and go on vacation. I suppose that I was trying to show was that operators could get clever with this should they find the need, although I would not necessarily recommend doing so.

@christopher-henderson christopher-henderson changed the title Draft RFC for Configurable Lints [WIP] Draft RFC for Configurable Lints Sep 1, 2021
@christopher-henderson christopher-henderson changed the title Draft RFC for Configurable Lints RFC for Configurable Lints Sep 18, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants