Skip to content

Add lint to check for a reserved policy identifier in S/MIME certificates#1011

Merged
christopher-henderson merged 6 commits into
zmap:masterfrom
defacto64:master
Jan 19, 2026
Merged

Add lint to check for a reserved policy identifier in S/MIME certificates#1011
christopher-henderson merged 6 commits into
zmap:masterfrom
defacto64:master

Conversation

@defacto64

Copy link
Copy Markdown
Contributor

I've noticed that ZLint doesn't raise any errors when linting an S/MIME certificate which lacks a reserved policy identifier among those required by the S/MIME BRs (see section 7.1.2.3), which seems counterintuitive to me. I am therefore proposing this trivial lint to resolve the issue.

(It could/should also be verified that there is only one such policy OID in the certificate, but I'll leave that to some other volunteer!)

lint.RegisterCertificateLint(&lint.CertificateLint{
LintMetadata: lint.LintMetadata{
Name: "e_cabf_policy_missing",
Description: "The subscriber cert SHALL include one of the reserved policy OIDs in §7.1.6.1",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I admit that I'm a bit confused.

First of all, unless my eyes are crossed, I believe that the requirement is that subscriber certs MUST NOT contain any of.

  • 2.23.140.1.2.1
  • 2.23.140.1.2.2
  • 2.23.140.1.2.3
  • 2.23.140.1.1

Additionally, util.IsSMIMEBRCertificate checks for the presence of at least one of these OIDs.

  • 2.23.140.1.5.1.1
  • 2.23.140.1.5.1.2
  • 2.23.140.1.5.1.3
  • 2.23.140.1.5.2.1
  • 2.23.140.1.5.2.2
  • 2.23.140.1.5.2.3
  • 2.23.140.1.5.3.1
  • 2.23.140.1.5.3.2
  • 2.23.140.1.5.3.3
  • 2.23.140.1.5.4.1
  • 2.23.140.1.5.4.2
  • 2.23.140.1.5.4.3

So unless I am misreading the BRs then I do not believe that we are checking the correct OIDs here.

image

@defacto64

Copy link
Copy Markdown
Contributor Author

@christopher-henderson , I think you may be wrongly assuming that this lint is for TLS certificates, but it is not: it's for S/MIME certificates. Besides, have you tried ZLinting an S/MIME certificates that lacks a CABF S/MIME BRs reserved policy OID? If you do, you'll find that Zlint has nothing to complain, which is weird. Please have a look at the S/MIME BRs, section 7.1.2.3.

(btw: happy new year!)

@christopher-henderson

christopher-henderson commented Jan 2, 2026

Copy link
Copy Markdown
Member

@defacto64 I face palm every time I spend an hour double checking everything, from the publication to the ZLint implementation, just to find that I'm reading the entirely wrong document 😮‍💨

image

Since CABF wants a cert to include exactly one of these policies, would you be amenable to using the following function (or something similar)? As it stands, we are testing whether at least one policy is present whereas we need to testing that exactly one policy is present.

func ContainsExactlyOneSMIMEPolicy(policies []asn1.ObjectIdentifier) bool {
	found := 0
	smimePolicies := map[string]bool{
		SMIMEBRMailboxValidatedLegacyOID.String():            true,
		SMIMEBRMailboxValidatedMultipurposeOID.String():      true,
		SMIMEBRMailboxValidatedStrictOID.String():            true,
		SMIMEBROrganizationValidatedLegacyOID.String():       true,
		SMIMEBROrganizationValidatedMultipurposeOID.String(): true,
		SMIMEBROrganizationValidatedStrictOID.String():       true,
		SMIMEBRSponsorValidatedLegacyOID.String():            true,
		SMIMEBRSponsorValidatedMultipurposeOID.String():      true,
		SMIMEBRSponsorValidatedStrictOID.String():            true,
		SMIMEBRIndividualValidatedLegacyOID.String():         true,
		SMIMEBRIndividualValidatedMultipurposeOID.String():   true,
		SMIMEBRIndividualValidatedStrictOID.String():         true,
	}
	for _, oid := range policies {
		if _, present := smimePolicies[oid.String()]; present {
			found++
		}
	}
	return found == 1
}

Here's a decent enough test suite for this function.

func TestExactlyOneSMIMEPolicy(t *testing.T) {
	tests := []struct {
		name     string
		policies []asn1.ObjectIdentifier
		want     bool
	}{
		{
			name:     "empty slice returns false",
			policies: []asn1.ObjectIdentifier{},
			want:     false,
		},
		{
			name:     "nil slice returns false",
			policies: nil,
			want:     false,
		},
		{
			name: "single mailbox validated legacy policy returns true",
			policies: []asn1.ObjectIdentifier{
				SMIMEBRMailboxValidatedLegacyOID,
			},
			want: true,
		},
		{
			name: "single mailbox validated multipurpose policy returns true",
			policies: []asn1.ObjectIdentifier{
				SMIMEBRMailboxValidatedMultipurposeOID,
			},
			want: true,
		},
		{
			name: "single mailbox validated strict policy returns true",
			policies: []asn1.ObjectIdentifier{
				SMIMEBRMailboxValidatedStrictOID,
			},
			want: true,
		},
		{
			name: "single organization validated legacy policy returns true",
			policies: []asn1.ObjectIdentifier{
				SMIMEBROrganizationValidatedLegacyOID,
			},
			want: true,
		},
		{
			name: "single organization validated multipurpose policy returns true",
			policies: []asn1.ObjectIdentifier{
				SMIMEBROrganizationValidatedMultipurposeOID,
			},
			want: true,
		},
		{
			name: "single organization validated strict policy returns true",
			policies: []asn1.ObjectIdentifier{
				SMIMEBROrganizationValidatedStrictOID,
			},
			want: true,
		},
		{
			name: "single sponsor validated legacy policy returns true",
			policies: []asn1.ObjectIdentifier{
				SMIMEBRSponsorValidatedLegacyOID,
			},
			want: true,
		},
		{
			name: "single sponsor validated multipurpose policy returns true",
			policies: []asn1.ObjectIdentifier{
				SMIMEBRSponsorValidatedMultipurposeOID,
			},
			want: true,
		},
		{
			name: "single sponsor validated strict policy returns true",
			policies: []asn1.ObjectIdentifier{
				SMIMEBRSponsorValidatedStrictOID,
			},
			want: true,
		},
		{
			name: "single individual validated legacy policy returns true",
			policies: []asn1.ObjectIdentifier{
				SMIMEBRIndividualValidatedLegacyOID,
			},
			want: true,
		},
		{
			name: "single individual validated multipurpose policy returns true",
			policies: []asn1.ObjectIdentifier{
				SMIMEBRIndividualValidatedMultipurposeOID,
			},
			want: true,
		},
		{
			name: "single individual validated strict policy returns true",
			policies: []asn1.ObjectIdentifier{
				SMIMEBRIndividualValidatedStrictOID,
			},
			want: true,
		},
		{
			name: "two different SMIME policies returns false",
			policies: []asn1.ObjectIdentifier{
				SMIMEBRMailboxValidatedLegacyOID,
				SMIMEBROrganizationValidatedLegacyOID,
			},
			want: false,
		},
		{
			name: "two same SMIME policies returns false",
			policies: []asn1.ObjectIdentifier{
				SMIMEBRMailboxValidatedLegacyOID,
				SMIMEBRMailboxValidatedLegacyOID,
			},
			want: false,
		},
		{
			name: "three SMIME policies returns false",
			policies: []asn1.ObjectIdentifier{
				SMIMEBRMailboxValidatedLegacyOID,
				SMIMEBROrganizationValidatedMultipurposeOID,
				SMIMEBRIndividualValidatedStrictOID,
			},
			want: false,
		},
		{
			name: "one SMIME policy with non-SMIME policies returns true",
			policies: []asn1.ObjectIdentifier{
				BRDomainValidatedOID,
				SMIMEBRMailboxValidatedLegacyOID,
				AnyPolicyOID,
			},
			want: true,
		},
		{
			name: "two SMIME policies with non-SMIME policies returns false",
			policies: []asn1.ObjectIdentifier{
				BRDomainValidatedOID,
				SMIMEBRMailboxValidatedLegacyOID,
				SMIMEBROrganizationValidatedMultipurposeOID,
				AnyPolicyOID,
			},
			want: false,
		},
		{
			name: "only non-SMIME policies returns false",
			policies: []asn1.ObjectIdentifier{
				BRDomainValidatedOID,
				BROrganizationValidatedOID,
				BRExtendedValidatedOID,
			},
			want: false,
		},
		{
			name: "SMIME policy at end of list returns true",
			policies: []asn1.ObjectIdentifier{
				BRDomainValidatedOID,
				BROrganizationValidatedOID,
				SMIMEBRIndividualValidatedStrictOID,
			},
			want: true,
		},
		{
			name: "SMIME policy at beginning of list returns true",
			policies: []asn1.ObjectIdentifier{
				SMIMEBRSponsorValidatedMultipurposeOID,
				BRDomainValidatedOID,
				BROrganizationValidatedOID,
			},
			want: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := ContainsExactlyOneSMIMEPolicy(tt.policies)
			if got != tt.want {
				t.Errorf("ContainsExactlyOneSMIMEPolicy() = %v, want %v", got, tt.want)
			}
		})
	}
}

@defacto64

Copy link
Copy Markdown
Contributor Author

Yes, it was already clear to me that the CABF S/MIME requirement is actually twofold: 1) there must be a Reserved Policy OID, but 2) there must not be more than one. However, it seemed OK to me to have one lint that handles point 1 and a second lint (to be implemented) for point 2. Frankly, I'm totally fine with a single lint that enforces both points, but sometimes I get the impression that you don't much like lints that perform multiple checks at once, while other times it seems like you actually like them :)
I'm fine with the revision you're proposing, but I need to find the time to implement it, and especially to create 22 test certificates. I plan to do so in the next few days, barring any impediments :)

@christopher-henderson

christopher-henderson commented Jan 3, 2026

Copy link
Copy Markdown
Member

but sometimes I get the impression that you don't much like lints that perform multiple checks at once, while other times it seems like you actually like them :)

Well in my own mental model this is not checking two things. There must be exactly one reserved policy is a singular clause that evaluates to one boolean expression. If this were there must be exactly one reserved policy AND it must be.... then I would break it up.

especially to create 22 test certificates. I plan to do so in the next few days, barring any impediments :)

This is why I wrote the function to accept a list of OIDs rather than certificates, it is much easier to unit test. I would be fine leaning on the unit test provided and keeping the current certs for a smoke check.

@defacto64

Copy link
Copy Markdown
Contributor Author

So, first I added your ContainsExactlyOneSMIMEPolicy function to the smime_policies.go source (package util), then I called this function in my lint. Then, in the test source of my lint, I integrated your test suite for the function itself, but I also added a certificate containing two CABF SMIME policy OIDs to the test data — for completeness. Finally, I changed the lint's name because "missing" would be misleading in some cases. How does it seem to you?

@christopher-henderson christopher-henderson merged commit 4f6ffa4 into zmap:master Jan 19, 2026
4 checks passed
@christopher-henderson

Copy link
Copy Markdown
Member

Thank you for another valuable lint @defacto64!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants