blob: 5bf103b641370741c65e926942df8b40b120b04b [file] [log] [blame]
# 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, 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:(.+\b)\s+[CGA]*,[CGA]*,[CGA]*')
NSSCERTUTIL = '/usr/local/bin/nsscertutil'
NSSMODUTIL = '/usr/local/bin/nssmodutil'
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):
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.
"""
baselines = {'nss': set([]), 'openssl': set([])}
baseline_file = open(os.path.join(self.bindir, baseline_file))
for line in baseline_file:
(lib, fingerprint) = line.rstrip().split()
if lib == 'both':
baselines['nss'].add(fingerprint)
baselines['openssl'].add(fingerprint)
else:
baselines[lib].add(fingerprint)
return baselines
def get_nss_certs(self):
"""Returns the set of certificate fingerprints observed in nss."""
tmpdir = self.tmpdir
# 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, glob.glob('/usr/lib*/libnssckbi.so')[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)
certdict = {} # A map of {SHA1_Fingerprint : CA_Nickname}.
for cert in NSS_ISSUER_RE.findall(all_certs):
cert_dump = utils.system_output(NSSCERTUTIL +
' -L -d %s -n '
'\"Builtin Object Token:%s\"' %
(tmpdir, cert))
f = FINGERPRINT_RE.search(cert_dump)
certdict[f.group(1)] = cert
return set(certdict)
def get_openssl_certs(self):
"""Returns the set 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).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 set(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):
"""Entry point for command line (run_remote_test) use. Accepts 2
optional args, e.g. run_remote_test --args="relaxed baseline=foo".
Parses the args array and invokes the main test method.
"""
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.
"""
testfail = False
# Dump certificate info and run comparisons.
seen = {}
seen['nss'] = self.get_nss_certs()
seen['openssl'] = self.get_openssl_certs()
expected = self.get_baseline_sets(baseline_file)
for lib in seen.keys():
missing = expected[lib].difference(seen[lib])
unexpected = seen[lib].difference(expected[lib])
if unexpected or (missing and exact_match):
testfail = True
logging.error('Results for %s' % lib)
logging.error('Unexpected')
for i in unexpected:
logging.error(i)
if exact_match:
logging.error('Missing')
for i in missing:
logging.error(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')