1# Copyright (c) 2011 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import glob, json, logging, os, re, stat 6 7from autotest_lib.client.bin import test, utils 8from autotest_lib.client.common_lib import error 9from autotest_lib.client.common_lib import pexpect 10 11 12DEFAULT_BASELINE = 'baseline' 13 14FINGERPRINT_RE = re.compile(r'Fingerprint \(SHA1\):\n\s+(\b[:\w]+)\b') 15NSS_ISSUER_RE = re.compile(r'Object Token:(.+?)\s+C,.?,.?') 16 17NSSCERTUTIL = '/usr/local/bin/certutil' 18NSSMODUTIL = '/usr/local/bin/modutil' 19OPENSSL = '/usr/bin/openssl' 20 21# This glob pattern is coupled to the snprintf() format in 22# get_cert_by_subject() in crypto/x509/by_dir.c in the OpenSSL 23# sources. In theory the glob can catch files not created by that 24# snprintf(); such file names probably shouldn't be allowed to exist 25# anyway. 26OPENSSL_CERT_GLOB = '/etc/ssl/certs/' + '[0-9a-f]' * 8 + '.*' 27 28 29class security_RootCA(test.test): 30 """Verifies that the root CAs trusted by both NSS and OpenSSL 31 match the expected set.""" 32 version = 1 33 34 def get_baseline_sets(self, baseline_file): 35 """Returns a dictionary of sets. The keys are the names of 36 the ssl components and the values are the sets of fingerprints 37 we expect to find in that component's Root CA list. 38 39 @param baseline_file: name of JSON file containing baseline. 40 """ 41 baselines = {'nss': {}, 'openssl': {}} 42 baseline_file = open(os.path.join(self.bindir, baseline_file)) 43 raw_baselines = json.load(baseline_file) 44 for i in ['nss', 'openssl']: 45 baselines[i].update(raw_baselines[i]) 46 baselines[i].update(raw_baselines['both']) 47 return baselines 48 49 def get_nss_certs(self): 50 """ 51 Returns the dict of certificate fingerprints observed in NSS, 52 or None if NSS is not available. 53 """ 54 tmpdir = self.tmpdir 55 56 nss_shlib_glob = glob.glob('/usr/lib*/libnssckbi.so') 57 if len(nss_shlib_glob) == 0: 58 return None 59 elif len(nss_shlib_glob) > 1: 60 logging.warn("Found more than one copy of libnssckbi.so") 61 62 # Create new empty cert DB. 63 child = pexpect.spawn('"%s" -N -d %s' % (NSSCERTUTIL, tmpdir)) 64 child.expect('Enter new password:') 65 child.sendline('foo') 66 child.expect('Re-enter password:') 67 child.sendline('foo') 68 child.close() 69 70 # Add the certs found in the compiled NSS shlib to a new module in DB. 71 cmd = ('"%s" -add testroots -libfile %s -dbdir %s' % 72 (NSSMODUTIL, nss_shlib_glob[0], tmpdir)) 73 nssmodutil = pexpect.spawn(cmd) 74 nssmodutil.expect('\'q <enter>\' to abort, or <enter> to continue:') 75 nssmodutil.sendline('\n') 76 ret = utils.system_output(NSSMODUTIL + ' -list ' 77 '-dbdir %s' % tmpdir) 78 self.assert_('2. testroots' in ret) 79 80 # Dump out the list of root certs. 81 all_certs = utils.system_output(NSSCERTUTIL + 82 ' -L -d %s -h all' % tmpdir, 83 retain_output=True) 84 certdict = {} # A map of {SHA1_Fingerprint : CA_Nickname}. 85 cert_matches = NSS_ISSUER_RE.findall(all_certs) 86 logging.debug('NSS_ISSUER_RE.findall returned: %s', cert_matches) 87 for cert in cert_matches: 88 cert_dump = utils.system_output(NSSCERTUTIL + 89 ' -L -d %s -n ' 90 '\"Builtin Object Token:%s\"' % 91 (tmpdir, cert), retain_output=True) 92 matches = FINGERPRINT_RE.findall(cert_dump) 93 for match in matches: 94 certdict[match] = cert 95 return certdict 96 97 98 def get_openssl_certs(self): 99 """Returns the dict of certificate fingerprints observed in OpenSSL.""" 100 fingerprint_cmd = ' '.join([OPENSSL, 'x509', '-fingerprint', 101 '-issuer', '-noout', 102 '-in %s']) 103 certdict = {} # A map of {SHA1_Fingerprint : CA_Nickname}. 104 105 for certfile in glob.glob(OPENSSL_CERT_GLOB): 106 f, i = utils.system_output(fingerprint_cmd % certfile, 107 retain_output=True).splitlines() 108 fingerprint = f.split('=')[1] 109 for field in i.split('/'): 110 items = field.split('=') 111 # Compensate for stupidly malformed issuer fields. 112 if len(items) > 1: 113 if items[0] == 'CN': 114 certdict[fingerprint] = items[1] 115 break 116 elif items[0] == 'O': 117 certdict[fingerprint] = items[1] 118 break 119 else: 120 logging.warning('Malformed issuer string %s', i) 121 # Check that we found a name for this fingerprint. 122 if not fingerprint in certdict: 123 raise error.TestFail('Couldn\'t find issuer string for %s' % 124 fingerprint) 125 return certdict 126 127 128 def cert_perms_errors(self): 129 """Returns True if certificate files have bad permissions.""" 130 # Acts as a regression check for crosbug.com/19848 131 has_errors = False 132 for certfile in glob.glob(OPENSSL_CERT_GLOB): 133 s = os.stat(certfile) 134 if s.st_uid != 0 or stat.S_IMODE(s.st_mode) != 0644: 135 logging.error("Bad permissions: %s", 136 utils.system_output("ls -lH %s" % certfile)) 137 has_errors = True 138 139 return has_errors 140 141 142 def run_once(self, opts=None): 143 """Test entry point. 144 145 Accepts 2 optional args, e.g. test_that --args="relaxed 146 baseline=foo". Parses the args array and invokes the main test 147 method. 148 149 @param opts: string containing command line arguments. 150 """ 151 args = {'baseline': DEFAULT_BASELINE} 152 if opts: 153 args.update(dict([[k, v] for (k, e, v) in 154 [x.partition('=') for x in opts]])) 155 156 self.verify_rootcas(baseline_file=args['baseline'], 157 exact_match=('relaxed' not in args)) 158 159 160 def verify_rootcas(self, baseline_file=DEFAULT_BASELINE, exact_match=True): 161 """Verify installed Root CA's all appear on a specified whitelist. 162 Covers both NSS and OpenSSL. 163 164 @param baseline_file: name of baseline file to use in verification. 165 @param exact_match: boolean indicating if expected-but-missing CAs 166 should cause test failure. Defaults to True. 167 """ 168 testfail = False 169 170 # Dump certificate info and run comparisons. 171 seen = {} 172 nss_store = self.get_nss_certs() 173 openssl_store = self.get_openssl_certs() 174 if nss_store is not None: 175 seen['nss'] = nss_store 176 if openssl_store is not None: 177 seen['openssl'] = openssl_store 178 179 # Merge all 4 dictionaries (seen-nss, seen-openssl, expected-nss, 180 # and expected-openssl) into 1 so we have 1 place to lookup 181 # fingerprint -> comment for logging purposes. 182 expected = self.get_baseline_sets(baseline_file) 183 cert_details = {} 184 for store in seen.keys(): 185 for certdict in [expected, seen]: 186 cert_details.update(certdict[store]) 187 certdict[store] = set(certdict[store]) 188 189 for store in seen.keys(): 190 missing = expected[store].difference(seen[store]) 191 unexpected = seen[store].difference(expected[store]) 192 if unexpected or (missing and exact_match): 193 testfail = True 194 logging.error('Results for %s', store) 195 logging.error('Unexpected') 196 for i in unexpected: 197 logging.error('"%s": "%s"', i, cert_details[i]) 198 if exact_match: 199 logging.error('Missing') 200 for i in missing: 201 logging.error('"%s": "%s"', i, cert_details[i]) 202 203 # cert_perms_errors() call first to avoid short-circuiting. 204 # Short circuiting could mask additional failures that would 205 # require a second build/test iteration to uncover. 206 if self.cert_perms_errors() or testfail: 207 raise error.TestFail('Unexpected Root CA findings') 208