github.com/zmap/zlint@v1.1.0/lints/lint_ext_tor_service_descriptor_hash_invalid.go (about) 1 /* 2 * ZLint Copyright 2019 Regents of the University of Michigan 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy 6 * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 * 8 * Unless required by applicable law or agreed to in writing, software 9 * distributed under the License is distributed on an "AS IS" BASIS, 10 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 11 * implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package lints 16 17 import ( 18 "fmt" 19 "net/url" 20 "strings" 21 22 "github.com/zmap/zcrypto/x509" 23 "github.com/zmap/zlint/util" 24 ) 25 26 type torServiceDescHashInvalid struct{} 27 28 func (l *torServiceDescHashInvalid) Initialize() error { 29 // There is nothing to initialize for a torServiceDescHashInvalid linter. 30 return nil 31 } 32 33 // CheckApplies returns true if the certificate is a subscriber certificate that 34 // contains a subject name ending in `.onion`. 35 func (l *torServiceDescHashInvalid) CheckApplies(c *x509.Certificate) bool { 36 return util.IsSubscriberCert(c) && util.CertificateSubjInTLD(c, onionTLD) 37 } 38 39 // failResult is a small utility function for creating a failed lint result. 40 func failResult(format string, args ...interface{}) *LintResult { 41 return &LintResult{ 42 Status: Error, 43 Details: fmt.Sprintf(format, args...), 44 } 45 } 46 47 // torServiceDescExtName is a common string prefix used in many lint result 48 // detail messages to identify the extension at fault. 49 var torServiceDescExtName = fmt.Sprintf( 50 "TorServiceDescriptor extension (oid %s)", 51 util.BRTorServiceDescriptor.String()) 52 53 // lintOnionURL verifies that an Onion URI value from a TorServiceDescriptorHash 54 // is: 55 // 56 // 1) a valid parseable url. 57 // 2) a URL with a non-empty hostname 58 // 3) a URL with an https:// protocol scheme 59 // 60 // If all of the above hold then nil is returned. If any of the above conditions 61 // are not met an error lint result pointer is returned. 62 func lintOnionURL(onion string) *LintResult { 63 if onionURL, err := url.Parse(onion); err != nil { 64 return failResult( 65 "%s contained "+ 66 "TorServiceDescriptorHash object with invalid Onion URI", 67 torServiceDescExtName) 68 } else if onionURL.Host == "" { 69 return failResult( 70 "%s contained "+ 71 "TorServiceDescriptorHash object with Onion URI missing a hostname", 72 torServiceDescExtName) 73 } else if onionURL.Scheme != "https" { 74 return failResult( 75 "%s contained "+ 76 "TorServiceDescriptorHash object with Onion URI using a non-HTTPS "+ 77 "protocol scheme", 78 torServiceDescExtName) 79 } 80 return nil 81 } 82 83 // Execute will lint the provided certificate. An Error LintResult will be 84 // returned if: 85 // 86 // 1) There is no TorServiceDescriptor extension present. 87 // 2) There were no TorServiceDescriptors parsed by zcrypto 88 // 3) There are TorServiceDescriptorHash entries with an invalid Onion URL. 89 // 4) There are TorServiceDescriptorHash entries with an unknown hash 90 // algorithm or incorrect hash bit length. 91 // 5) There is a TorServiceDescriptorHash entry that doesn't correspond to 92 // an onion subject in the cert. 93 // 6) There is an onion subject in the cert that doesn't correspond to 94 // a TorServiceDescriptorHash. 95 func (l *torServiceDescHashInvalid) Execute(c *x509.Certificate) *LintResult { 96 // If the BRTorServiceDescriptor extension is missing return a lint error. We 97 // know the cert contains one or more `.onion` subjects because of 98 // `CheckApplies` and all such certs are expected to have this extension after 99 // util.CABV201Date. 100 if ext := util.GetExtFromCert(c, util.BRTorServiceDescriptor); ext == nil { 101 return failResult( 102 "certificate contained a %s domain but is missing a TorServiceDescriptor "+ 103 "extension (oid %s)", 104 onionTLD, util.BRTorServiceDescriptor.String()) 105 } 106 107 // The certificate should have at least one TorServiceDescriptorHash in the 108 // TorServiceDescriptor extension. 109 descriptors := c.TorServiceDescriptors 110 if len(descriptors) == 0 { 111 return failResult( 112 "certificate contained a %s domain but TorServiceDescriptor "+ 113 "extension (oid %s) had no TorServiceDescriptorHash objects", 114 onionTLD, util.BRTorServiceDescriptor.String()) 115 } 116 117 // Build a map of all the eTLD+1 onion subjects in the cert to compare against 118 // the service descriptors. 119 onionETLDPlusOneMap := make(map[string]string) 120 for _, subj := range append(c.DNSNames, c.Subject.CommonName) { 121 if !strings.HasSuffix(subj, onionTLD) { 122 continue 123 } 124 labels := strings.Split(subj, ".") 125 if len(labels) < 2 { 126 return failResult("certificate contained a %s domain with too few "+ 127 "labels: %q", 128 onionTLD, subj) 129 } 130 eTLDPlusOne := strings.Join(labels[len(labels)-2:], ".") 131 onionETLDPlusOneMap[eTLDPlusOne] = subj 132 } 133 134 expectedHashBits := map[string]int{ 135 "SHA256": 256, 136 "SHA384": 384, 137 "SHA512": 512, 138 } 139 140 // Build a map of onion hostname -> TorServiceDescriptorHash using the parsed 141 // TorServiceDescriptors from zcrypto. 142 descriptorMap := make(map[string]*x509.TorServiceDescriptorHash) 143 for _, descriptor := range descriptors { 144 // each descriptor's Onion URL must be valid 145 if errResult := lintOnionURL(descriptor.Onion); errResult != nil { 146 return errResult 147 } 148 // each descriptor should have a known hash algorithm and the correct 149 // corresponding size of hash. 150 if expectedBits, found := expectedHashBits[descriptor.AlgorithmName]; !found { 151 return failResult( 152 "%s contained a TorServiceDescriptorHash for Onion URI %q with an "+ 153 "unknown hash algorithm", 154 torServiceDescExtName, descriptor.Onion) 155 } else if expectedBits != descriptor.HashBits { 156 return failResult( 157 "%s contained a TorServiceDescriptorHash with hash algorithm %q but "+ 158 "only %d bits of hash not %d", 159 torServiceDescExtName, descriptor.AlgorithmName, 160 descriptor.HashBits, expectedBits) 161 } 162 // NOTE(@cpu): Throwing out the err result here because lintOnionURL already 163 // ensured the URL is valid. 164 url, _ := url.Parse(descriptor.Onion) 165 hostname := url.Hostname() 166 // there should only be one TorServiceDescriptorHash for each Onion hostname. 167 if _, exists := descriptorMap[hostname]; exists { 168 return failResult( 169 "%s contained more than one TorServiceDescriptorHash for base "+ 170 "Onion URI %q", 171 torServiceDescExtName, descriptor.Onion) 172 } 173 // there shouldn't be a TorServiceDescriptorHash for a Onion hostname that 174 // isn't an eTLD+1 in the certificate's subjects. 175 if _, found := onionETLDPlusOneMap[hostname]; !found { 176 return failResult( 177 "%s contained a TorServiceDescriptorHash with a hostname (%q) not "+ 178 "present as a subject in the certificate", 179 torServiceDescExtName, hostname) 180 } 181 descriptorMap[hostname] = descriptor 182 } 183 184 // Check if any of the onion subjects in the certificate don't have 185 // a TorServiceDescriptorHash for the eTLD+1 in the descriptorMap. 186 for eTLDPlusOne, subjDomain := range onionETLDPlusOneMap { 187 if _, found := descriptorMap[eTLDPlusOne]; !found { 188 return failResult( 189 "%s subject domain name %q does not have a corresponding "+ 190 "TorServiceDescriptorHash for its eTLD+1", 191 onionTLD, subjDomain) 192 } 193 } 194 195 // Everything checks out! 196 return &LintResult{ 197 Status: Pass, 198 } 199 } 200 201 func init() { 202 RegisterLint(&Lint{ 203 Name: "e_ext_tor_service_descriptor_hash_invalid", 204 Description: "certificates with .onion names need valid TorServiceDescriptors in extension", 205 Citation: "BRS: Ballot 201", 206 Source: CABFBaselineRequirements, 207 EffectiveDate: util.CABV201Date, 208 Lint: &torServiceDescHashInvalid{}, 209 }) 210 }