| # Copyright (c) 2011 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import glob, json, logging, os, re, stat |
| |
| from autotest_lib.client.bin import test, utils |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.client.common_lib import pexpect |
| |
| |
| DEFAULT_BASELINE = 'baseline' |
| |
| FINGERPRINT_RE = re.compile(r'Fingerprint \(SHA1\):\n\s+(\b[:\w]+)\b') |
| NSS_ISSUER_RE = re.compile(r'Object Token:(.+?)\s+C,.?,.?') |
| |
| NSSCERTUTIL = '/usr/local/bin/certutil' |
| NSSMODUTIL = '/usr/local/bin/modutil' |
| OPENSSL = '/usr/bin/openssl' |
| |
| # This glob pattern is coupled to the snprintf() format in |
| # get_cert_by_subject() in crypto/x509/by_dir.c in the OpenSSL |
| # sources. In theory the glob can catch files not created by that |
| # snprintf(); such file names probably shouldn't be allowed to exist |
| # anyway. |
| OPENSSL_CERT_GLOB = '/etc/ssl/certs/' + '[0-9a-f]' * 8 + '.*' |
| |
| |
| class security_RootCA(test.test): |
| """Verifies that the root CAs trusted by both NSS and OpenSSL |
| match the expected set.""" |
| version = 1 |
| |
| def get_baseline_sets(self, baseline_file): |
| """Returns a dictionary of sets. The keys are the names of |
| the ssl components and the values are the sets of fingerprints |
| we expect to find in that component's Root CA list. |
| |
| @param baseline_file: name of JSON file containing baseline. |
| """ |
| baselines = {'nss': {}, 'openssl': {}} |
| baseline_file = open(os.path.join(self.bindir, baseline_file)) |
| raw_baselines = json.load(baseline_file) |
| for i in ['nss', 'openssl']: |
| baselines[i].update(raw_baselines[i]) |
| baselines[i].update(raw_baselines['both']) |
| return baselines |
| |
| def get_nss_certs(self): |
| """ |
| Returns the dict of certificate fingerprints observed in NSS, |
| or None if NSS is not available. |
| """ |
| tmpdir = self.tmpdir |
| |
| nss_shlib_glob = glob.glob('/usr/lib*/libnssckbi.so') |
| if len(nss_shlib_glob) == 0: |
| return None |
| elif len(nss_shlib_glob) > 1: |
| logging.warn("Found more than one copy of libnssckbi.so") |
| |
| # Create new empty cert DB. |
| child = pexpect.spawn('"%s" -N -d %s' % (NSSCERTUTIL, tmpdir)) |
| child.expect('Enter new password:') |
| child.sendline('foo') |
| child.expect('Re-enter password:') |
| child.sendline('foo') |
| child.close() |
| |
| # Add the certs found in the compiled NSS shlib to a new module in DB. |
| cmd = ('"%s" -add testroots -libfile %s -dbdir %s' % |
| (NSSMODUTIL, nss_shlib_glob[0], tmpdir)) |
| nssmodutil = pexpect.spawn(cmd) |
| nssmodutil.expect('\'q <enter>\' to abort, or <enter> to continue:') |
| nssmodutil.sendline('\n') |
| ret = utils.system_output(NSSMODUTIL + ' -list ' |
| '-dbdir %s' % tmpdir) |
| self.assert_('2. testroots' in ret) |
| |
| # Dump out the list of root certs. |
| all_certs = utils.system_output(NSSCERTUTIL + |
| ' -L -d %s -h all' % tmpdir, |
| retain_output=True) |
| certdict = {} # A map of {SHA1_Fingerprint : CA_Nickname}. |
| cert_matches = NSS_ISSUER_RE.findall(all_certs) |
| logging.debug('NSS_ISSUER_RE.findall returned: %s', cert_matches) |
| for cert in cert_matches: |
| cert_dump = utils.system_output(NSSCERTUTIL + |
| ' -L -d %s -n ' |
| '\"Builtin Object Token:%s\"' % |
| (tmpdir, cert), retain_output=True) |
| matches = FINGERPRINT_RE.findall(cert_dump) |
| for match in matches: |
| certdict[match] = cert |
| return certdict |
| |
| |
| def get_openssl_certs(self): |
| """Returns the dict of certificate fingerprints observed in OpenSSL.""" |
| fingerprint_cmd = ' '.join([OPENSSL, 'x509', '-fingerprint', |
| '-issuer', '-noout', |
| '-in %s']) |
| certdict = {} # A map of {SHA1_Fingerprint : CA_Nickname}. |
| |
| for certfile in glob.glob(OPENSSL_CERT_GLOB): |
| f, i = utils.system_output(fingerprint_cmd % certfile, |
| retain_output=True).splitlines() |
| fingerprint = f.split('=')[1] |
| for field in i.split('/'): |
| items = field.split('=') |
| # Compensate for stupidly malformed issuer fields. |
| if len(items) > 1: |
| if items[0] == 'CN': |
| certdict[fingerprint] = items[1] |
| break |
| elif items[0] == 'O': |
| certdict[fingerprint] = items[1] |
| break |
| else: |
| logging.warning('Malformed issuer string %s', i) |
| # Check that we found a name for this fingerprint. |
| if not fingerprint in certdict: |
| raise error.TestFail('Couldn\'t find issuer string for %s' % |
| fingerprint) |
| return certdict |
| |
| |
| def cert_perms_errors(self): |
| """Returns True if certificate files have bad permissions.""" |
| # Acts as a regression check for crosbug.com/19848 |
| has_errors = False |
| for certfile in glob.glob(OPENSSL_CERT_GLOB): |
| s = os.stat(certfile) |
| if s.st_uid != 0 or stat.S_IMODE(s.st_mode) != 0644: |
| logging.error("Bad permissions: %s", |
| utils.system_output("ls -lH %s" % certfile)) |
| has_errors = True |
| |
| return has_errors |
| |
| |
| def run_once(self, opts=None): |
| """Test entry point. |
| |
| Accepts 2 optional args, e.g. test_that --args="relaxed |
| baseline=foo". Parses the args array and invokes the main test |
| method. |
| |
| @param opts: string containing command line arguments. |
| """ |
| args = {'baseline': DEFAULT_BASELINE} |
| if opts: |
| args.update(dict([[k, v] for (k, e, v) in |
| [x.partition('=') for x in opts]])) |
| |
| self.verify_rootcas(baseline_file=args['baseline'], |
| exact_match=('relaxed' not in args)) |
| |
| |
| def verify_rootcas(self, baseline_file=DEFAULT_BASELINE, exact_match=True): |
| """Verify installed Root CA's all appear on a specified whitelist. |
| Covers both NSS and OpenSSL. |
| |
| @param baseline_file: name of baseline file to use in verification. |
| @param exact_match: boolean indicating if expected-but-missing CAs |
| should cause test failure. Defaults to True. |
| """ |
| testfail = False |
| |
| # Dump certificate info and run comparisons. |
| seen = {} |
| nss_store = self.get_nss_certs() |
| openssl_store = self.get_openssl_certs() |
| if nss_store is not None: |
| seen['nss'] = nss_store |
| if openssl_store is not None: |
| seen['openssl'] = openssl_store |
| |
| # Merge all 4 dictionaries (seen-nss, seen-openssl, expected-nss, |
| # and expected-openssl) into 1 so we have 1 place to lookup |
| # fingerprint -> comment for logging purposes. |
| expected = self.get_baseline_sets(baseline_file) |
| cert_details = {} |
| for store in seen.keys(): |
| for certdict in [expected, seen]: |
| cert_details.update(certdict[store]) |
| certdict[store] = set(certdict[store]) |
| |
| for store in seen.keys(): |
| missing = expected[store].difference(seen[store]) |
| unexpected = seen[store].difference(expected[store]) |
| if unexpected or (missing and exact_match): |
| testfail = True |
| logging.error('Results for %s', store) |
| logging.error('Unexpected') |
| for i in unexpected: |
| logging.error('"%s": "%s"', i, cert_details[i]) |
| if exact_match: |
| logging.error('Missing') |
| for i in missing: |
| logging.error('"%s": "%s"', i, cert_details[i]) |
| |
| # cert_perms_errors() call first to avoid short-circuiting. |
| # Short circuiting could mask additional failures that would |
| # require a second build/test iteration to uncover. |
| if self.cert_perms_errors() or testfail: |
| raise error.TestFail('Unexpected Root CA findings') |