File: lint_san_dns_name_onion_invalid.go

package info (click to toggle)
golang-github-zmap-zlint 3.6.2-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 10,008 kB
  • sloc: sh: 162; makefile: 38
file content (153 lines) | stat: -rw-r--r-- 5,733 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/*
 * 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.
 */

package cabf_br

import (
	"fmt"
	"regexp"
	"strings"

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

var (
	// Per 2.4 of Rendezvous v2:
	//   Valid onion addresses contain 16 characters in a-z2-7 plus ".onion"
	onionV2Len = 16

	// Per 1.2 of Rendezvous v3:
	//   A hidden service's name is its long term master identity key.  This is
	//   encoded as a hostname by encoding the entire key in Base 32, including
	//   a version byte and a checksum, and then appending the string ".onion"
	//   at the end. The result is a 56-character domain name.
	onionV3Len = 56

	// Per RFC 4648, Section 6, the Base-32 alphabet is A-Z, 2-7, and =.
	// Because v2/v3 addresses are always aligned, they should never be padded,
	// and so omit = from the character set, as it's also not permitted in a
	// domain in the "preferred name syntax". Because `.onion` names appear in
	// DNS, which is case insensitive, the alphabet is extended to include a-z,
	// as the names are tested for well-formedness prior to normalization to
	// uppercase.
	base32SubsetRegex = regexp.MustCompile(`^[a-zA-Z2-7]+$`)
)

type onionNotValid struct{}

/*******************************************************************
https://tools.ietf.org/html/rfc7686#section-1

   Note that .onion names are required to conform with DNS name syntax
   (as defined in Section 3.5 of [RFC1034] and Section 2.1 of
   [RFC1123]), as they will still be exposed to DNS implementations.

   See [tor-address] and [tor-rendezvous] for the details of the
   creation and use of .onion names.

Baseline Requirements, v1.6.9, Appendix C (Ballot SC27)

The Domain Name MUST contain at least two labels, where the right-most label
is "onion", and the label immediately preceding the right-most "onion" label
is a valid Version 3 Onion Address, as defined in section 6 of the Tor
Rendezvous Specification - Version 3 located at
https://spec.torproject.org/rend-spec-v3.

Explanation:
Since CA/Browser Forum Ballot 144, `.onion` names have been permitted,
predating the ratification of RFC 7686. RFC 7686 introduced a normative
dependency on the Tor address and rendezvous specifications, which describe
v2 addresses. As the EV Guidelines have, since v1.5.3, required that the CA
obtain a demonstration of control from the Applicant, which effectively
requires the `.onion` name to be well-formed, even prior to RFC 7686.

See also https://github.com/cabforum/documents/issues/191
*******************************************************************/

func init() {
	lint.RegisterCertificateLint(&lint.CertificateLint{
		LintMetadata: lint.LintMetadata{
			Name:          "e_san_dns_name_onion_invalid",
			Description:   "certificates with a .onion subject name must be issued in accordance with the Tor address/rendezvous specification",
			Citation:      "RFC 7686, EVGs v1.7.2: Appendix F, BRs v1.6.9: Appendix C",
			Source:        lint.CABFBaselineRequirements,
			EffectiveDate: util.OnionOnlyEVDate,
		},
		Lint: NewOnionNotValid,
	})
}

func NewOnionNotValid() lint.LintInterface {
	return &onionNotValid{}
}

// CheckApplies returns true if the certificate contains one or more subject
// names ending in `.onion`.
func (l *onionNotValid) CheckApplies(c *x509.Certificate) bool {
	// TODO(sleevi): This should also be extended to support nameConstraints
	// in the future.
	return util.CertificateSubjInTLD(c, util.OnionTLD)
}

// Execute will lint the provided certificate. A lint.Error lint.LintResult will
// be returned if:
//
//  1. The certificate contains a Tor Rendezvous Spec v2 address and is not an
//     EV certificate (BRs: Appendix C).
//  2. The certificate contains a `.onion` subject name/SAN that is neither a
//     Rendezvous Spec v2 or v3 address.
func (l *onionNotValid) Execute(c *x509.Certificate) *lint.LintResult {
	for _, subj := range append(c.DNSNames, c.Subject.CommonName) {
		if !strings.HasSuffix(subj, util.OnionTLD) {
			continue
		}
		labels := strings.Split(subj, ".")
		if len(labels) < 2 {
			return &lint.LintResult{
				Status: lint.Error,
				Details: fmt.Sprintf("certificate contained a %s domain with too "+
					"few labels: %q", util.OnionTLD, subj),
			}
		}
		onionDomain := labels[len(labels)-2]
		if len(onionDomain) == onionV2Len {
			// Onion v2 address. These are only permitted for EV, per BRs Appendix C.
			if !util.IsEV(c.PolicyIdentifiers) {
				return &lint.LintResult{
					Status: lint.Error,
					Details: fmt.Sprintf("%q is a v2 address, but the certificate is not "+
						"EV", subj),
				}
			}
		} else if len(onionDomain) == onionV3Len {
			// Onion v3 address. Permitted for all certificates by CA/Browser Forum
			// Ballot SC27.
		} else {
			return &lint.LintResult{
				Status:  lint.Error,
				Details: fmt.Sprintf("%q is not a v2 or v3 Tor address", subj),
			}
		}
		if !base32SubsetRegex.MatchString(onionDomain) {
			return &lint.LintResult{
				Status: lint.Error,
				Details: fmt.Sprintf("%q contains invalid characters not permitted "+
					"within base-32", subj),
			}
		}
	}
	return &lint.LintResult{Status: lint.Pass}
}