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