security_RootCA.py revision 654451e32abf7199a8c144950cc9f667f9b9088a
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, 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:(.+\b)\s+[CGA]*,[CGA]*,[CGA]*') 16 17NSSCERTUTIL = '/usr/local/bin/nsscertutil' 18NSSMODUTIL = '/usr/local/bin/nssmodutil' 19OPENSSL = '/usr/bin/openssl' 20 21OPENSSL_CERT_GLOB = '/etc/ssl/certs/*.pem' 22 23 24class security_RootCA(test.test): 25 version = 1 26 27 def get_baseline_sets(self, baseline_file): 28 """Returns a dictionary of sets. The keys are the names of 29 the ssl components and the values are the sets of fingerprints 30 we expect to find in that component's Root CA list. 31 """ 32 baselines = {'nss': set([]), 'openssl': set([])} 33 baseline_file = open(os.path.join(self.bindir, baseline_file)) 34 for line in baseline_file: 35 (lib, fingerprint) = line.rstrip().split() 36 if lib == 'both': 37 baselines['nss'].add(fingerprint) 38 baselines['openssl'].add(fingerprint) 39 else: 40 baselines[lib].add(fingerprint) 41 return baselines 42 43 def get_nss_certs(self): 44 """Returns the set of certificate fingerprints observed in nss.""" 45 tmpdir = self.tmpdir 46 47 # Create new empty cert DB. 48 child = pexpect.spawn('"%s" -N -d %s' % (NSSCERTUTIL, tmpdir)) 49 child.expect('Enter new password:') 50 child.sendline('foo') 51 child.expect('Re-enter password:') 52 child.sendline('foo') 53 child.close() 54 55 # Add the certs found in the compiled NSS shlib to a new module in DB. 56 cmd = ('"%s" -add testroots ' 57 '-libfile /usr/lib/libnssckbi.so' 58 ' -dbdir %s' % (NSSMODUTIL, tmpdir)) 59 nssmodutil = pexpect.spawn(cmd) 60 nssmodutil.expect('\'q <enter>\' to abort, or <enter> to continue:') 61 nssmodutil.sendline('\n') 62 ret = utils.system_output(NSSMODUTIL + ' -list ' 63 '-dbdir %s' % tmpdir) 64 self.assert_('2. testroots' in ret) 65 66 # Dump out the list of root certs. 67 all_certs = utils.system_output(NSSCERTUTIL + 68 ' -L -d %s -h all' % tmpdir) 69 certdict = {} # A map of {SHA1_Fingerprint : CA_Nickname}. 70 for cert in NSS_ISSUER_RE.findall(all_certs): 71 cert_dump = utils.system_output(NSSCERTUTIL + 72 ' -L -d %s -n ' 73 '\"Builtin Object Token:%s\"' % 74 (tmpdir, cert)) 75 f = FINGERPRINT_RE.search(cert_dump) 76 certdict[f.group(1)] = cert 77 return set(certdict) 78 79 80 def get_openssl_certs(self): 81 """Returns the set of certificate fingerprints observed in openssl.""" 82 fingerprint_cmd = ' '.join([OPENSSL, 'x509', '-fingerprint', 83 '-issuer', '-noout', 84 '-in %s']) 85 certdict = {} # A map of {SHA1_Fingerprint : CA_Nickname}. 86 87 for certfile in glob.glob(OPENSSL_CERT_GLOB): 88 f, i = utils.system_output(fingerprint_cmd % certfile).splitlines() 89 fingerprint = f.split('=')[1] 90 if (fingerprint != os.path.basename(certfile).split('.pem')[0]): 91 logging.warning('Filename %s doesn\'t match fingerprint %s' % 92 (certfile, fingerprint)) 93 for field in i.split('/'): 94 items = field.split('=') 95 # Compensate for stupidly malformed issuer fields. 96 if len(items) > 1: 97 if items[0] == 'CN': 98 certdict[fingerprint] = items[1] 99 break 100 elif items[0] == 'O': 101 certdict[fingerprint] = items[1] 102 break 103 else: 104 logging.warning('Malformed issuer string %s' % i) 105 # Check that we found a name for this fingerprint. 106 if not fingerprint in certdict: 107 raise error.TestFail('Couldn\'t find issuer string for %s' % 108 fingerprint) 109 return set(certdict) 110 111 112 def cert_perms_errors(self): 113 """Returns True if certificate files have bad permissions.""" 114 # Acts as a regression check for crosbug.com/19848 115 has_errors = False 116 for certfile in glob.glob(OPENSSL_CERT_GLOB): 117 s = os.stat(certfile) 118 if s.st_uid != 0 or stat.S_IMODE(s.st_mode) != 0644: 119 logging.error("Bad permissions: %s" % 120 utils.system_output("ls -lH %s" % certfile)) 121 has_errors = True 122 123 return has_errors 124 125 126 def run_once(self, opts=None): 127 """Entry point for command line (run_remote_test) use. Accepts 2 128 optional args, e.g. run_remote_test --args="relaxed baseline=foo". 129 Parses the args array and invokes the main test method. 130 """ 131 args = {'baseline': DEFAULT_BASELINE} 132 if opts: 133 args.update(dict([[k, v] for (k, e, v) in 134 [x.partition('=') for x in opts]])) 135 136 self.verify_rootcas(baseline_file=args['baseline'], 137 exact_match=('relaxed' not in args)) 138 139 140 def verify_rootcas(self, baseline_file=DEFAULT_BASELINE, exact_match=True): 141 """Verify installed Root CA's all appear on a specified whitelist. 142 Covers both nss and openssl. 143 """ 144 testfail = False 145 146 # Dump certificate info and run comparisons. 147 seen = {} 148 seen['nss'] = self.get_nss_certs() 149 seen['openssl'] = self.get_openssl_certs() 150 expected = self.get_baseline_sets(baseline_file) 151 152 for lib in seen.keys(): 153 missing = expected[lib].difference(seen[lib]) 154 unexpected = seen[lib].difference(expected[lib]) 155 if unexpected or (missing and exact_match): 156 testfail = True 157 logging.error('Results for %s' % lib) 158 logging.error('Unexpected') 159 for i in unexpected: 160 logging.error(i) 161 if exact_match: 162 logging.error('Missing') 163 for i in missing: 164 logging.error(i) 165 166 # cert_perms_errors() call first to avoid short-circuiting. 167 # Short circuiting could mask additional failures that would 168 # require a second build/test iteration to uncover. 169 if self.cert_perms_errors() or testfail: 170 raise error.TestFail('Unexpected Root CA findings') 171