Skip to content

Commit

Permalink
Add lint to enforce SMIME BRs: 7.1.4.2.1 requirement for mailbox addr… (
Browse files Browse the repository at this point in the history
#800)

* Add lint to enforce SMIME BRs: 7.1.4.2.1 requirement for mailbox addresses

All mailbox addresses appearing in subjectDN or dirName must be repeated
in san:rfc822Name or san:otherName. This lint does its best to detect
mailbox address values in the subjectDN or dirName and if any are
detected ensures they are repeated.

* Add expected integration failures for new lint e_mailbox_address_shall_contain_an_rfc822_name

The failures all have email addresses that don't have an **exact** match
in the SAN.

How the integration tests were run:
`make integration INT_FLAGS="-lintSummary -fingerprintSummary -lintFilter='e_mailbox_address_shall_contain_an_rfc822_name'"`

Fingerprints of the relevant certificates:
3087f97b6cff020b5320e18d3e326074cbaa128142660f2debe4564ab1ab0179
5f3fcccca91a7b39e8995f79c35cb5e604d4ee0487ea1a41993c84304c0a5c99
63d23132c2511f33bb947f27c398bb824109ccf2d6a2037e3713fe9f7a43b15d
b034fa1aa9e501dc14b43d43dfe2210de3e5551744494b55d5f0abd865c67efc
c6ac841c78191101725ca7d5ed499be47c15ebeece7d74e6d095e2925e7bb404
e4dbfc94e616ffb59904e394d9dcdd3ab55c26c5586440f37c058eecb907a344

* Revert "Add expected integration failures for new lint e_mailbox_address_shall_contain_an_rfc822_name"

This reverts commit 037b5ec.

* Add expected integration failures for new lint e_mailbox_address_shall_contain_an_rfc822_name

This commit is a proper version of the previously reverted one. It was
reverted because I accidently ran the script to update the config only
for the failing lint, rather than lints.

The failures all have email addresses that don't have an **exact** match
in the SAN.

How the integration tests were run:
`make integration INT_FLAGS="-lintSummary -fingerprintSummary -lintFilter='e_mailbox_address_shall_contain_an_rfc822_name'"`

Fingerprints of the relevant certificates:
3087f97b6cff020b5320e18d3e326074cbaa128142660f2debe4564ab1ab0179
5f3fcccca91a7b39e8995f79c35cb5e604d4ee0487ea1a41993c84304c0a5c99
63d23132c2511f33bb947f27c398bb824109ccf2d6a2037e3713fe9f7a43b15d
b034fa1aa9e501dc14b43d43dfe2210de3e5551744494b55d5f0abd865c67efc
c6ac841c78191101725ca7d5ed499be47c15ebeece7d74e6d095e2925e7bb404
e4dbfc94e616ffb59904e394d9dcdd3ab55c26c5586440f37c058eecb907a344

* Use effective date from SMIME BR for mailbox_address_from_san lint

* Address code style to fit with established conventions

* Revert accidental changes to genTestCerts

* Apply DeMorgan's law to fix incorrect code simplification

* Remove redundant function literal

* Run gofmt

---------

Co-authored-by: Christopher Henderson <chris@chenderson.org>
  • Loading branch information
toddgaunt-gs and christopher-henderson authored Mar 3, 2024
1 parent a23de3d commit b9ff71f
Show file tree
Hide file tree
Showing 13 changed files with 639 additions and 4 deletions.
35 changes: 32 additions & 3 deletions v3/integration/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,8 @@
}
],
"Expected": {
"e_adobe_extensions_legacy_multipurpose_criticality": {},
"e_adobe_extensions_strict_presence": {},
"e_algorithm_identifier_improper_encoding": {},
"e_basic_constraints_not_critical": {
"ErrCount": 23
Expand Down Expand Up @@ -416,7 +418,10 @@
},
"e_dsa_unique_correct_representation": {},
"e_ec_improper_curves": {},
"e_ec_other_key_usages": {},
"e_ecdsa_allowed_ku": {},
"e_ecpublickey_key_usages": {},
"e_edwardspublickey_key_usages": {},
"e_ev_business_category_missing": {
"ErrCount": 2
},
Expand Down Expand Up @@ -546,6 +551,9 @@
"ErrCount": 31843
},
"e_key_usage_incorrect_length": {},
"e_key_usage_presence": {},
"e_mailbox_address_shall_contain_an_rfc822_name": {},
"e_mailbox_validated_enforce_subject_field_restrictions": {},
"e_mp_authority_key_identifier_correct": {
"ErrCount": 3704
},
Expand Down Expand Up @@ -583,6 +591,7 @@
"ErrCount": 17
},
"e_path_len_constraint_zero_or_less": {},
"e_policy_qualifiers_other_than_cps_not_permitted": {},
"e_prohibit_dsa_usage": {},
"e_public_key_type_not_allowed": {},
"e_qcstatem_etsi_present_qcs_critical": {},
Expand All @@ -598,6 +607,7 @@
"e_qcstatem_qcretentionperiod_valid": {},
"e_qcstatem_qcsscd_valid": {},
"e_qcstatem_qctype_valid": {},
"e_registration_scheme_id_matches_subject_country": {},
"e_rfc_dnsname_empty_label": {
"ErrCount": 16
},
Expand All @@ -624,10 +634,13 @@
},
"e_rsa_exp_negative": {},
"e_rsa_fermat_factorization": {},
"e_rsa_key_usage_legacy_multipurpose": {},
"e_rsa_key_usage_strict": {},
"e_rsa_mod_less_than_2048_bits": {
"ErrCount": 34006
},
"e_rsa_no_public_key": {},
"e_rsa_other_key_usages": {},
"e_rsa_public_exponent_not_odd": {},
"e_rsa_public_exponent_too_small": {},
"e_san_bare_wildcard": {
Expand All @@ -637,6 +650,7 @@
"e_san_dns_name_onion_invalid": {},
"e_san_dns_name_onion_not_ev_cert": {},
"e_san_dns_name_starts_with_period": {},
"e_san_shall_be_present": {},
"e_san_wildcard_not_first": {
"ErrCount": 12
},
Expand All @@ -649,9 +663,15 @@
"e_signature_algorithm_not_supported": {
"ErrCount": 23
},
"e_single_email_if_present": {},
"e_smime_legacy_aia_shall_have_one_http": {},
"e_smime_legacy_multipurpose_eku_check": {},
"e_smime_strict_aia_shall_have_http_only": {},
"e_smime_strict_eku_check": {},
"e_spki_rsa_encryption_parameter_not_null": {
"ErrCount": 7
},
"e_strict_multipurpose_smime_ext_subject_directory_attr": {},
"e_sub_ca_aia_marked_critical": {},
"e_sub_ca_aia_missing": {
"ErrCount": 292
Expand All @@ -673,6 +693,7 @@
"e_sub_cert_aia_missing": {
"ErrCount": 11935
},
"e_sub_cert_basic_constraints_not_critical": {},
"e_sub_cert_cert_policy_empty": {
"ErrCount": 738
},
Expand Down Expand Up @@ -774,6 +795,7 @@
"e_subject_state_name_max_length": {},
"e_subject_street_address_max_length": {},
"e_subject_surname_max_length": {},
"e_subscribers_shall_have_crl_distribution_points": {},
"e_superfluous_ku_encoding": {
"ErrCount": 3
},
Expand All @@ -784,6 +806,8 @@
"ErrCount": 3
},
"e_underscore_not_permissible_in_dnsname": {},
"e_underscore_permissible_in_dnsname_if_valid_when_replaced": {},
"e_underscore_present_with_too_long_validity": {},
"e_utc_time_does_not_include_seconds": {
"ErrCount": 1
},
Expand Down Expand Up @@ -865,12 +889,14 @@
"w_ext_subject_key_identifier_missing_sub_cert": {
"WarnCount": 119268
},
"w_ext_subject_key_identifier_not_recommended_subscriber": {},
"w_extra_subject_common_names": {
"WarnCount": 36
},
"w_ian_iana_pub_suffix_empty": {},
"w_issuer_dn_leading_whitespace": {},
"w_issuer_dn_trailing_whitespace": {},
"w_key_usage_criticality": {},
"w_multiple_issuer_rdn": {},
"w_name_constraint_on_edi_party_name": {},
"w_name_constraint_on_registered_id": {},
Expand All @@ -895,6 +921,8 @@
"w_rsa_public_exponent_not_in_range": {
"WarnCount": 110
},
"w_san_should_not_be_critical": {},
"w_smime_aia_contains_internal_names": {},
"w_sub_ca_aia_does_not_contain_issuing_ca_url": {
"WarnCount": 990
},
Expand All @@ -908,6 +936,9 @@
"w_sub_ca_name_constraints_not_critical": {
"WarnCount": 115
},
"w_sub_cert_aia_contains_internal_names": {
"WarnCount": 210
},
"w_sub_cert_aia_does_not_contain_issuing_ca_url": {
"WarnCount": 48465
},
Expand All @@ -918,6 +949,7 @@
"w_sub_cert_sha1_expiration_too_long": {
"WarnCount": 11058
},
"w_subject_common_name_included": {},
"w_subject_contains_malformed_arpa_ip": {
"WarnCount": 2
},
Expand All @@ -931,9 +963,6 @@
"w_subject_surname_recommended_max_length": {},
"w_tls_server_cert_valid_time_longer_than_397_days": {
"WarnCount": 223
},
"w_sub_cert_aia_contains_internal_names": {
"WarnCount": 210
}
}
}
113 changes: 113 additions & 0 deletions v3/lints/cabf_smime_br/mailbox_address_from_san.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package cabf_smime_br

/*
* 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/zcrypto/encoding/asn1"
"github.com/zmap/zcrypto/x509"
"github.com/zmap/zcrypto/x509/pkix"
"github.com/zmap/zlint/v3/lint"
"github.com/zmap/zlint/v3/util"
)

// MailboxAddressFromSAN - linter to enforce MAY/SHALL NOT requirements for SMIME certificates
type MailboxAddressFromSAN struct {
}

func init() {
lint.RegisterLint(&lint.Lint{
Name: "e_mailbox_address_shall_contain_an_rfc822_name",
Description: "All Mailbox Addresses in the subject field or entries of type dirName of this extension SHALL be repeated as rfc822Name or otherName values of type id-on-SmtpUTF8Mailbox in this extension",
Citation: "SMIME BRs: 7.1.4.2.1",
Source: lint.CABFSMIMEBaselineRequirements,
EffectiveDate: util.CABF_SMIME_BRs_1_0_0_Date,
Lint: NewMailboxAddressFromSAN,
})
}

// NewMailboxAddressFromSAN creates a new linter to enforce the requirement that all Mailbox Addresses in SMIME BR certificates must be copied from the SAN
func NewMailboxAddressFromSAN() lint.LintInterface {
return &MailboxAddressFromSAN{}
}

// CheckApplies is returns true if the certificate's policies assert that it conforms to the SMIME BRs
func (l *MailboxAddressFromSAN) CheckApplies(c *x509.Certificate) bool {
if util.HasEKU(c, x509.ExtKeyUsageEmailProtection) || util.HasEKU(c, x509.ExtKeyUsageAny) {
return true
}

return util.IsMailboxValidatedCertificate(c) && util.IsSubscriberCert(c)
}

// Execute checks all the places where Mailbox Addresses may be found in an SMIME certificate and confirms that they are present in the SAN rfc822Name or SAN otherName
func (l *MailboxAddressFromSAN) Execute(c *x509.Certificate) *lint.LintResult {
lintErr := &lint.LintResult{
Status: lint.Error,
Details: "all certificate mailbox addresses must be present in san:emailAddresses or san:otherNames in addition to any other field they may appear",
}

// build list of Mailbox addresses from subject:commonName, subject:emailAddress, dirName
toFindMailboxAddresses := getMailboxAddressesFromDistinguishedName(c.Subject)

for _, dirName := range c.DirectoryNames {
toFindMailboxAddresses = append(toFindMailboxAddresses, getMailboxAddressesFromDistinguishedName(dirName)...)
}

sanNames := map[string]bool{}
for _, rfc822Name := range c.EmailAddresses {
sanNames[rfc822Name] = true
}

for _, otherName := range c.OtherNames {
if otherName.TypeID.Equal(util.OidIdOnSmtpUtf8Mailbox) {
// The otherName needs to be specially unmarshalled since it is
// stored as a UTF-8 string rather than what the asn1 package
// describes as a PrintableString.
var otherNameValue string
rest, err := asn1.UnmarshalWithParams(otherName.Value.Bytes, &otherNameValue, "utf8")
if len(rest) > 0 || err != nil {
return lintErr
}

sanNames[otherNameValue] = true
}
}

for _, mailboxAddress := range toFindMailboxAddresses {
if _, found := sanNames[mailboxAddress]; !found {
return lintErr
}
}

return &lint.LintResult{Status: lint.Pass}
}

func getMailboxAddressesFromDistinguishedName(name pkix.Name) []string {
mailboxAddresses := []string{}

for _, commonName := range name.CommonNames {
if util.IsMailboxAddress(commonName) {
mailboxAddresses = append(mailboxAddresses, commonName)
}
}

for _, emailAddress := range name.EmailAddress {
if util.IsMailboxAddress(emailAddress) {
mailboxAddresses = append(mailboxAddresses, emailAddress)
}
}

return mailboxAddresses
}
103 changes: 103 additions & 0 deletions v3/lints/cabf_smime_br/mailbox_address_from_san_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package cabf_smime_br

/*
* 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 (
"testing"

"github.com/zmap/zlint/v3/lint"
"github.com/zmap/zlint/v3/test"
)

func TestMailboxAddressFromSANLint(t *testing.T) {
testCases := []struct {
Name string
InputFilename string

ExpectedResult lint.LintStatus
ExpectedDetails string
}{
{
Name: "pass - subject:commonName email address matches san:otherName",
InputFilename: "WithOtherNameMatched.pem",

ExpectedResult: lint.Pass,
},
{
Name: "pass - subject:commonName email address matches san:emailAddress",
InputFilename: "WithSANEmailMatched.pem",

ExpectedResult: lint.Pass,
},
{
Name: "pass - only contains one san:emailAddress value",
InputFilename: "WithOnlySANEmail.pem",

ExpectedResult: lint.Pass,
},
{
Name: "pass - only contains one san:otherName value",
InputFilename: "WithOnlySANOtherName.pem",

ExpectedResult: lint.Pass,
},
{
Name: "NE - before effective date",
InputFilename: "NotEffective.pem",

ExpectedResult: lint.NE,
},
{
Name: "NA - does not contain smime certificate policy",
InputFilename: "NotApplicable.pem",

ExpectedResult: lint.NA,
},
{
Name: "fail - subject:commonName email address does not match san:otherName",
InputFilename: "WithOtherNameUnmatched.pem",

ExpectedResult: lint.Error,
ExpectedDetails: "all certificate mailbox addresses must be present in san:emailAddresses or san:otherNames in addition to any other field they may appear",
},
{
Name: "fail - subject:commonName email address does not match the email value under san:otherName",
InputFilename: "WithOtherNameIncorrectType.pem",

ExpectedResult: lint.Error,
ExpectedDetails: "all certificate mailbox addresses must be present in san:emailAddresses or san:otherNames in addition to any other field they may appear",
},
{
Name: "fail - subject:commonName email address does not match san:emailAddress",
InputFilename: "WithSANEmailUnmatched.pem",

ExpectedResult: lint.Error,
ExpectedDetails: "all certificate mailbox addresses must be present in san:emailAddresses or san:otherNames in addition to any other field they may appear",
},
}

for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
result := test.TestLint("e_mailbox_address_shall_contain_an_rfc822_name", "smime/MailboxAddressFromSAN/"+tc.InputFilename)
if result.Status != tc.ExpectedResult {
t.Errorf("expected result %v was %v", tc.ExpectedResult, result.Status)
}

if tc.ExpectedResult == lint.Error && tc.ExpectedDetails != result.Details {
t.Errorf("expected details: %q, was %q", tc.ExpectedDetails, result.Details)
}
})
}
}
Loading

0 comments on commit b9ff71f

Please sign in to comment.