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  }