security_RootCA.py revision 4bcdd2ba40de41d306d0ece671f076660582a145
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 """Returns a dict of certificate fingerprints observed in nss.""" 51 tmpdir = self.tmpdir 52 53 # Create new empty cert DB. 54 child = pexpect.spawn('"%s" -N -d %s' % (NSSCERTUTIL, tmpdir)) 55 child.expect('Enter new password:') 56 child.sendline('foo') 57 child.expect('Re-enter password:') 58 child.sendline('foo') 59 child.close() 60 61 # Add the certs found in the compiled NSS shlib to a new module in DB. 62 cmd = ('"%s" -add testroots -libfile %s -dbdir %s' % 63 (NSSMODUTIL, glob.glob('/usr/lib*/libnssckbi.so')[0], tmpdir)) 64 nssmodutil = pexpect.spawn(cmd) 65 nssmodutil.expect('\'q <enter>\' to abort, or <enter> to continue:') 66 nssmodutil.sendline('\n') 67 ret = utils.system_output(NSSMODUTIL + ' -list ' 68 '-dbdir %s' % tmpdir) 69 self.assert_('2. testroots' in ret) 70 71 # Dump out the list of root certs. 72 all_certs = utils.system_output(NSSCERTUTIL + 73 ' -L -d %s -h all' % tmpdir, 74 retain_output=True) 75 certdict = {} # A map of {SHA1_Fingerprint : CA_Nickname}. 76 cert_matches = NSS_ISSUER_RE.findall(all_certs) 77 logging.debug('NSS_ISSUER_RE.findall returned: %s', cert_matches) 78 for cert in cert_matches: 79 cert_dump = utils.system_output(NSSCERTUTIL + 80 ' -L -d %s -n ' 81 '\"Builtin Object Token:%s\"' % 82 (tmpdir, cert), retain_output=True) 83 matches = FINGERPRINT_RE.findall(cert_dump) 84 for match in matches: 85 certdict[match] = cert 86 return certdict 87 88 89 def get_openssl_certs(self): 90 """Returns the dict of certificate fingerprints observed in openssl.""" 91 fingerprint_cmd = ' '.join([OPENSSL, 'x509', '-fingerprint', 92 '-issuer', '-noout', 93 '-in %s']) 94 certdict = {} # A map of {SHA1_Fingerprint : CA_Nickname}. 95 96 for certfile in glob.glob(OPENSSL_CERT_GLOB): 97 f, i = utils.system_output(fingerprint_cmd % certfile, 98 retain_output=True).splitlines() 99 fingerprint = f.split('=')[1] 100 for field in i.split('/'): 101 items = field.split('=') 102 # Compensate for stupidly malformed issuer fields. 103 if len(items) > 1: 104 if items[0] == 'CN': 105 certdict[fingerprint] = items[1] 106 break 107 elif items[0] == 'O': 108 certdict[fingerprint] = items[1] 109 break 110 else: 111 logging.warning('Malformed issuer string %s', i) 112 # Check that we found a name for this fingerprint. 113 if not fingerprint in certdict: 114 raise error.TestFail('Couldn\'t find issuer string for %s' % 115 fingerprint) 116 return certdict 117 118 119 def cert_perms_errors(self): 120 """Returns True if certificate files have bad permissions.""" 121 # Acts as a regression check for crosbug.com/19848 122 has_errors = False 123 for certfile in glob.glob(OPENSSL_CERT_GLOB): 124 s = os.stat(certfile) 125 if s.st_uid != 0 or stat.S_IMODE(s.st_mode) != 0644: 126 logging.error("Bad permissions: %s", 127 utils.system_output("ls -lH %s" % certfile)) 128 has_errors = True 129 130 return has_errors 131 132 133 def run_once(self, opts=None): 134 """Entry point for command line (run_remote_test) use. Accepts 2 135 optional args, e.g. run_remote_test --args="relaxed baseline=foo". 136 Parses the args array and invokes the main test method. 137 138 @param opts: string containing command line arguments. 139 """ 140 args = {'baseline': DEFAULT_BASELINE} 141 if opts: 142 args.update(dict([[k, v] for (k, e, v) in 143 [x.partition('=') for x in opts]])) 144 145 self.verify_rootcas(baseline_file=args['baseline'], 146 exact_match=('relaxed' not in args)) 147 148 149 def verify_rootcas(self, baseline_file=DEFAULT_BASELINE, exact_match=True): 150 """Verify installed Root CA's all appear on a specified whitelist. 151 Covers both nss and openssl. 152 153 @param baseline_file: name of baseline file to use in verification. 154 @param exact_match: boolean indicating if expected-but-missing CAs 155 should cause test failure. Defaults to True. 156 """ 157 testfail = False 158 159 # Dump certificate info and run comparisons. 160 seen = {} 161 seen['nss'] = self.get_nss_certs() 162 seen['openssl'] = self.get_openssl_certs() 163 # Merge all 4 dictionaries (seen-nss, seen-openssl, expected-nss, 164 # and expected-openssl) into 1 so we have 1 place to lookup 165 # fingerprint -> comment for logging purposes. 166 expected = self.get_baseline_sets(baseline_file) 167 cert_details = {} 168 for certdict in [expected, seen]: 169 for i in ['openssl', 'nss']: 170 cert_details.update(certdict[i]) 171 certdict[i] = set(certdict[i]) 172 173 for lib in seen.keys(): 174 missing = expected[lib].difference(seen[lib]) 175 unexpected = seen[lib].difference(expected[lib]) 176 if unexpected or (missing and exact_match): 177 testfail = True 178 logging.error('Results for %s', lib) 179 logging.error('Unexpected') 180 for i in unexpected: 181 logging.error('"%s": "%s"', i, cert_details[i]) 182 if exact_match: 183 logging.error('Missing') 184 for i in missing: 185 logging.error('"%s": "%s"', i, cert_details[i]) 186 187 # cert_perms_errors() call first to avoid short-circuiting. 188 # Short circuiting could mask additional failures that would 189 # require a second build/test iteration to uncover. 190 if self.cert_perms_errors() or testfail: 191 raise error.TestFail('Unexpected Root CA findings') 192