Home | History | Annotate | Download | only in security_RootCA
      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 
      5 import glob, json, logging, os, re, stat
      6 
      7 from autotest_lib.client.bin import test, utils
      8 from autotest_lib.client.common_lib import error
      9 from autotest_lib.client.common_lib import pexpect
     10 
     11 
     12 DEFAULT_BASELINE = 'baseline'
     13 
     14 FINGERPRINT_RE = re.compile(r'Fingerprint \(SHA1\):\n\s+(\b[:\w]+)\b')
     15 NSS_ISSUER_RE = re.compile(r'Object Token:(.+?)\s+C,.?,.?')
     16 
     17 NSSCERTUTIL = '/usr/local/bin/certutil'
     18 NSSMODUTIL = '/usr/local/bin/modutil'
     19 OPENSSL = '/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.
     26 OPENSSL_CERT_GLOB = '/etc/ssl/certs/' + '[0-9a-f]' * 8 + '.*'
     27 
     28 
     29 class 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