From 48baa89c49e477e223ac2c4644046530869d8af7 Mon Sep 17 00:00:00 2001 From: Christopher Henderson Date: Wed, 27 Sep 2023 16:16:51 -0700 Subject: [PATCH] Permit underscores in DNSNames if-and-only-if replacing all underscores results in valid LDH labels during BR 1.6.2's permissibility period (#661) Co-authored-by: David Adrian --- ...sible_in_dnsname_if_valid_when_replaced.go | 57 +++++++++++++++++++ ..._in_dnsname_if_valid_when_replaced_test.go | 54 ++++++++++++++++++ .../dNSNameUnderscoreNotValidWhenReplaced.pem | 37 ++++++++++++ .../dNSNameUnderscoreValidWhenReplaced.pem | 37 ++++++++++++ ...NSUnderscoresPermissibleOutOfDateRange.pem | 40 ++++++------- v3/util/fqdn.go | 12 ++++ v3/util/fqdn_test.go | 24 ++++++++ 7 files changed, 241 insertions(+), 20 deletions(-) create mode 100644 v3/lints/cabf_br/lint_underscore_permissible_in_dnsname_if_valid_when_replaced.go create mode 100644 v3/lints/cabf_br/lint_underscore_permissible_in_dnsname_if_valid_when_replaced_test.go create mode 100644 v3/testdata/dNSNameUnderscoreNotValidWhenReplaced.pem create mode 100644 v3/testdata/dNSNameUnderscoreValidWhenReplaced.pem diff --git a/v3/lints/cabf_br/lint_underscore_permissible_in_dnsname_if_valid_when_replaced.go b/v3/lints/cabf_br/lint_underscore_permissible_in_dnsname_if_valid_when_replaced.go new file mode 100644 index 000000000..2861410f0 --- /dev/null +++ b/v3/lints/cabf_br/lint_underscore_permissible_in_dnsname_if_valid_when_replaced.go @@ -0,0 +1,57 @@ +/* + * ZLint Copyright 2021 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. + */ + +package cabf_br + +import ( + "fmt" + "strings" + + "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_underscore_permissible_in_dnsname_if_valid_when_replaced", + Description: "From December 10th 2018 to April 1st 2019 DNSNames may contain underscores if-and-only-if every label within each DNS name is a valid LDH label after replacing all underscores with hyphens", + Citation: "BR 7.1.4.2.1", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABFBRs_1_6_2_Date, + IneffectiveDate: util.CABFBRs_1_6_2_UnderscorePermissibilitySunsetDate, + Lint: func() lint.LintInterface { return &UnderscorePermissibleInDNSNameIfValidWhenReplaced{} }, + }) +} + +type UnderscorePermissibleInDNSNameIfValidWhenReplaced struct{} + +func (l *UnderscorePermissibleInDNSNameIfValidWhenReplaced) CheckApplies(c *x509.Certificate) bool { + return util.IsSubscriberCert(c) && util.DNSNamesExist(c) +} + +func (l *UnderscorePermissibleInDNSNameIfValidWhenReplaced) Execute(c *x509.Certificate) *lint.LintResult { + for _, dns := range c.DNSNames { + for _, label := range strings.Split(dns, ".") { + if !strings.Contains(label, "_") || label == "*" { + continue + } + replaced := strings.ReplaceAll(label, "_", "-") + if !util.IsLDHLabel(replaced) { + return &lint.LintResult{Status: lint.Error, Details: fmt.Sprintf("When all underscores (_) in %q are replaced with hypens (-) the result is %q which not a valid LDH label", label, replaced)} + } + } + } + return &lint.LintResult{Status: lint.Pass} +} diff --git a/v3/lints/cabf_br/lint_underscore_permissible_in_dnsname_if_valid_when_replaced_test.go b/v3/lints/cabf_br/lint_underscore_permissible_in_dnsname_if_valid_when_replaced_test.go new file mode 100644 index 000000000..d39cd749d --- /dev/null +++ b/v3/lints/cabf_br/lint_underscore_permissible_in_dnsname_if_valid_when_replaced_test.go @@ -0,0 +1,54 @@ +/* + * ZLint Copyright 2021 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. + */ + +package cabf_br + +import ( + "testing" + + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/test" +) + +func TestUnderscoresInPermissibilityPeriodBecomeValidAfterReplacement(t *testing.T) { + testCases := []struct { + Name string + InputFilename string + ExpectedResult lint.LintStatus + }{ + { + Name: "Valid when replaced", + InputFilename: "dNSNameUnderscoreValidWhenReplaced.pem", + ExpectedResult: lint.Pass, + }, + { + Name: "Invalid when replaced", + InputFilename: "dNSNameUnderscoreNotValidWhenReplaced.pem", + ExpectedResult: lint.Error, + }, + { + Name: "Not effective", + InputFilename: "dNSUnderscoresPermissibleOutOfDateRange.pem", + ExpectedResult: lint.NE, + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + result := test.TestLint("e_underscore_permissible_in_dnsname_if_valid_when_replaced", tc.InputFilename) + if result.Status != tc.ExpectedResult { + t.Errorf("expected result %v was %v", tc.ExpectedResult, result.Status) + } + }) + } +} diff --git a/v3/testdata/dNSNameUnderscoreNotValidWhenReplaced.pem b/v3/testdata/dNSNameUnderscoreNotValidWhenReplaced.pem new file mode 100644 index 000000000..76eace469 --- /dev/null +++ b/v3/testdata/dNSNameUnderscoreNotValidWhenReplaced.pem @@ -0,0 +1,37 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 3 (0x3) + Signature Algorithm: ecdsa-with-SHA256 + Issuer: + Validity + Not Before: Dec 10 00:00:00 2018 GMT + Not After : Jan 10 00:00:00 2019 GMT + Subject: + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:76:81:ad:a1:7c:e7:08:12:02:3d:82:3f:e6:5c: + 7a:09:bb:88:70:3e:64:e3:51:ec:e1:c1:62:0c:71: + 21:87:48:9c:8e:43:d5:75:42:82:58:02:19:0b:1e: + 7d:cf:dc:f1:eb:62:5b:5d:e0:e7:77:63:ff:f5:97: + 82:cc:ee:49:81 + ASN1 OID: prime256v1 + NIST CURVE: P-256 + X509v3 extensions: + X509v3 Subject Alternative Name: + DNS:with._an_underscore.test + Signature Algorithm: ecdsa-with-SHA256 + 30:45:02:20:62:de:b0:2a:43:04:88:12:c9:22:de:fe:db:33: + 3a:77:01:cf:51:e1:e0:60:cb:5f:fb:c8:a6:44:b7:ab:91:45: + 02:21:00:e7:b4:95:a8:f6:dd:2b:4a:d1:6a:e7:f6:d0:21:90: + 6c:70:97:ce:2b:d5:07:b6:1a:63:49:34:64:88:90:25:13 +-----BEGIN CERTIFICATE----- +MIIBFTCBvKADAgECAgEDMAoGCCqGSM49BAMCMAAwHhcNMTgxMjEwMDAwMDAwWhcN +MTkwMTEwMDAwMDAwWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEdoGtoXzn +CBICPYI/5lx6CbuIcD5k41Hs4cFiDHEhh0icjkPVdUKCWAIZCx59z9zx62JbXeDn +d2P/9ZeCzO5JgaMnMCUwIwYDVR0RBBwwGoIYd2l0aC5fYW5fdW5kZXJzY29yZS50 +ZXN0MAoGCCqGSM49BAMCA0gAMEUCIGLesCpDBIgSySLe/tszOncBz1Hh4GDLX/vI +pkS3q5FFAiEA57SVqPbdK0rRauf20CGQbHCXzivVB7YaY0k0ZIiQJRM= +-----END CERTIFICATE----- diff --git a/v3/testdata/dNSNameUnderscoreValidWhenReplaced.pem b/v3/testdata/dNSNameUnderscoreValidWhenReplaced.pem new file mode 100644 index 000000000..9dd847a23 --- /dev/null +++ b/v3/testdata/dNSNameUnderscoreValidWhenReplaced.pem @@ -0,0 +1,37 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 3 (0x3) + Signature Algorithm: ecdsa-with-SHA256 + Issuer: + Validity + Not Before: Dec 10 00:00:00 2018 GMT + Not After : Jan 10 00:00:00 2019 GMT + Subject: + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:60:1e:5b:1d:9d:18:b4:6f:85:97:c8:02:18:7e: + 4b:ba:f4:24:9f:e2:34:e4:85:1c:17:b2:e1:e5:be: + cc:56:b9:84:e7:f8:88:21:5d:e1:ba:59:7d:7e:6b: + 0e:cb:ec:f7:0c:e7:73:cb:6c:27:79:71:2c:4b:ba: + 1b:3e:a9:12:a5 + ASN1 OID: prime256v1 + NIST CURVE: P-256 + X509v3 extensions: + X509v3 Subject Alternative Name: + DNS:with.an_underscore.test + Signature Algorithm: ecdsa-with-SHA256 + 30:45:02:20:32:84:d7:38:0d:28:2c:fe:9e:6e:40:64:27:05: + 42:64:e7:c8:de:ba:91:cc:ce:f9:9c:34:77:55:a5:58:4f:38: + 02:21:00:fd:a3:73:bb:b0:45:b3:b3:85:61:db:ad:85:af:6c: + a1:69:ed:0c:9e:bb:ec:a8:41:14:db:c3:73:4c:1c:40:ef +-----BEGIN CERTIFICATE----- +MIIBFDCBu6ADAgECAgEDMAoGCCqGSM49BAMCMAAwHhcNMTgxMjEwMDAwMDAwWhcN +MTkwMTEwMDAwMDAwWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYB5bHZ0Y +tG+Fl8gCGH5LuvQkn+I05IUcF7Lh5b7MVrmE5/iIIV3hull9fmsOy+z3DOdzy2wn +eXEsS7obPqkSpaMmMCQwIgYDVR0RBBswGYIXd2l0aC5hbl91bmRlcnNjb3JlLnRl +c3QwCgYIKoZIzj0EAwIDSAAwRQIgMoTXOA0oLP6ebkBkJwVCZOfI3rqRzM75nDR3 +VaVYTzgCIQD9o3O7sEWzs4Vh262Fr2yhae0MnrvsqEEU28NzTBxA7w== +-----END CERTIFICATE----- diff --git a/v3/testdata/dNSUnderscoresPermissibleOutOfDateRange.pem b/v3/testdata/dNSUnderscoresPermissibleOutOfDateRange.pem index 0919e74c0..02f20515a 100644 --- a/v3/testdata/dNSUnderscoresPermissibleOutOfDateRange.pem +++ b/v3/testdata/dNSUnderscoresPermissibleOutOfDateRange.pem @@ -3,35 +3,35 @@ Certificate: Version: 3 (0x2) Serial Number: 3 (0x3) Signature Algorithm: ecdsa-with-SHA256 - Issuer: + Issuer: Validity Not Before: May 1 00:00:00 2008 GMT - Not After : Jan 10 00:00:00 2019 GMT - Subject: + Not After : Dec 10 00:00:00 2018 GMT + Subject: Subject Public Key Info: Public Key Algorithm: id-ecPublicKey Public-Key: (256 bit) pub: - 04:10:59:6c:21:8d:c7:61:6d:07:e4:d0:31:28:67: - 5b:da:36:96:a0:d6:92:75:0e:e6:9c:4d:6c:8b:e7: - ac:fd:87:4c:7b:fd:a0:fb:b6:ef:f9:ff:21:b3:9b: - bb:31:6b:0f:8e:41:b0:9d:1b:93:c1:78:8f:81:39: - 51:42:97:c1:17 + 04:da:65:3e:9c:55:66:12:20:df:6a:79:3d:59:a8: + a9:00:1c:91:b7:c3:61:00:3c:4f:ba:19:a5:05:7e: + b0:63:a5:60:08:cf:d9:a5:8d:9e:57:71:05:d6:4a: + 55:f9:33:c5:23:24:2d:32:1f:94:f3:1f:29:03:09: + 98:36:b1:b7:26 ASN1 OID: prime256v1 NIST CURVE: P-256 X509v3 extensions: - X509v3 Subject Alternative Name: - DNS:this.has_underscores.test + X509v3 Subject Alternative Name: + DNS:with._an_underscore.test Signature Algorithm: ecdsa-with-SHA256 - 30:44:02:20:62:99:44:cb:f3:0c:6c:f9:62:26:5e:6c:4b:62: - bb:38:fa:f7:f5:fc:93:ee:03:8e:99:5e:a0:7b:10:16:a2:8c: - 02:20:70:07:4d:5f:84:eb:4c:30:12:c4:31:b1:85:d1:6c:cb: - 52:ae:4d:a6:53:40:ff:8c:98:ba:96:ee:dc:66:9a:82 + 30:45:02:20:4f:a4:45:ee:97:f6:37:3e:ad:c1:28:7b:d9:f8: + 68:df:cb:52:4e:93:0c:81:ab:ca:94:aa:fa:58:f2:9a:2f:07: + 02:21:00:ed:42:41:c5:12:2c:62:8e:a3:64:7e:20:2a:d8:b3: + f8:6a:7f:3f:29:8e:fc:0d:aa:ac:17:14:e4:18:f4:cd:f2 -----BEGIN CERTIFICATE----- -MIIBFTCBvaADAgECAgEDMAoGCCqGSM49BAMCMAAwHhcNMDgwNTAxMDAwMDAwWhcN -MTkwMTEwMDAwMDAwWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEFlsIY3H -YW0H5NAxKGdb2jaWoNaSdQ7mnE1si+es/YdMe/2g+7bv+f8hs5u7MWsPjkGwnRuT -wXiPgTlRQpfBF6MoMCYwJAYDVR0RBB0wG4IZdGhpcy5oYXNfdW5kZXJzY29yZXMu -dGVzdDAKBggqhkjOPQQDAgNHADBEAiBimUTL8wxs+WImXmxLYrs4+vf1/JPuA46Z -XqB7EBaijAIgcAdNX4TrTDASxDGxhdFsy1KuTaZTQP+MmLqW7txmmoI= +MIIBFTCBvKADAgECAgEDMAoGCCqGSM49BAMCMAAwHhcNMDgwNTAxMDAwMDAwWhcN +MTgxMjEwMDAwMDAwWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2mU+nFVm +EiDfank9WaipAByRt8NhADxPuhmlBX6wY6VgCM/ZpY2eV3EF1kpV+TPFIyQtMh+U +8x8pAwmYNrG3JqMnMCUwIwYDVR0RBBwwGoIYd2l0aC5fYW5fdW5kZXJzY29yZS50 +ZXN0MAoGCCqGSM49BAMCA0gAMEUCIE+kRe6X9jc+rcEoe9n4aN/LUk6TDIGrypSq ++ljymi8HAiEA7UJBxRIsYo6jZH4gKtiz+Gp/PymO/A2qrBcU5Bj0zfI= -----END CERTIFICATE----- diff --git a/v3/util/fqdn.go b/v3/util/fqdn.go index 4be2ffb9f..bcf3f8e23 100644 --- a/v3/util/fqdn.go +++ b/v3/util/fqdn.go @@ -17,6 +17,7 @@ package util import ( "net" "net/url" + "regexp" "strings" zcutil "github.com/zmap/zcrypto/util" @@ -117,3 +118,14 @@ func CommonNameIsIP(cert *x509.Certificate) bool { return true } } + +var nonLDHCharacterRegex = regexp.MustCompile(`[^a-zA-Z0-9\-]`) + +func IsLDHLabel(label string) bool { + return len(label) > 0 && + len(label) <= 63 && + !nonLDHCharacterRegex.MatchString(label) && + !strings.HasPrefix(label, "-") && + !strings.HasSuffix(label, "-") && + !(HasReservedLabelPrefix(label) && !HasXNLabelPrefix(label)) +} diff --git a/v3/util/fqdn_test.go b/v3/util/fqdn_test.go index bf01e5846..00150ebe4 100644 --- a/v3/util/fqdn_test.go +++ b/v3/util/fqdn_test.go @@ -1012,3 +1012,27 @@ func TestGetHostWithUserinfoWithPortWithAbsolutePathWithQueryWithFragment(t *tes ) } } + +func TestIsLDHLabel(t *testing.T) { + data := map[string]bool{ + "": false, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": false, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": true, + "9": true, + "9a": true, + "a9": true, + "a": true, + ".": false, + "a-b": true, + "-a": false, + "a-": false, + "-": false, + "%": false, + } + for input, want := range data { + got := IsLDHLabel(input) + if got != want { + t.Errorf("expected %v got %v for '%s'", want, got, input) + } + } +}