# Copyright (c) 2012 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 logging, os, shutil, tempfile
import common, constants, cryptohome
from autotest_lib.client.bin import utils
from autotest_lib.client.common_lib import autotemp, error
from autotest_lib.client.cros import cros_ui
PK12UTIL = 'pk12util'
CERTUTIL = 'certutil'
OPENSSLP12 = 'openssl pkcs12'
OPENSSLX509 = 'openssl x509'
OPENSSLRSA = 'openssl rsa'
OPENSSLREQ = 'openssl req'
OPENSSLCRYPTO = 'openssl sha1'
TESTUSER = 'ownership_test@chromium.org'
TESTPASS = 'testme'
class OwnershipError(error.TestError):
"""Generic error for ownership-related failures."""
pass
class scoped_tempfile(object):
"""A wrapper that provides scoped semantics for temporary files.
Providing a file path causes the scoped_tempfile to take ownership of the
file at the provided path. The file at the path will be deleted when this
object goes out of scope. If no path is provided, then a temporary file
object will be created for the lifetime of the scoped_tempfile
autotemp.tempfile objects don't seem to play nicely with being
used in system commands, so they can't be used for my purposes.
"""
tempdir = autotemp.tempdir(unique_id='ownership')
def __init__(self, name=None):
self.name = name
if not self.name:
self.fo = tempfile.TemporaryFile()
def __del__(self):
if self.name:
if os.path.exists(self.name):
os.unlink(self.name)
else:
self.fo.close() # Will destroy the underlying tempfile
def system_output_on_fail(cmd):
"""Run a |cmd|, capturing output and logging it only on error.
@param cmd: the command to run.
"""
output = None
try:
output = utils.system_output(cmd)
except:
logging.error(output)
raise
def __unlink(filename):
"""unlink a file, but log OSError and IOError instead of raising.
This allows unlinking files that don't exist safely.
@param filename: the file to attempt to unlink.
"""
try:
os.unlink(filename)
except (IOError, OSError) as error:
logging.info(error)
def restart_ui_to_clear_ownership_files():
"""Remove on-disk state related to device ownership.
The UI must be stopped while we do this, or the session_manager will
write the policy and key files out again.
"""
cros_ui.stop(allow_fail=not cros_ui.is_up())
clear_ownership_files_no_restart()
cros_ui.start()
def clear_ownership_files_no_restart():
"""Remove on-disk state related to device ownership.
The UI must be stopped while we do this, or the session_manager will
write the policy and key files out again.
"""
if cros_ui.is_up():
raise error.TestError("Tried to clear ownership with UI running.")
__unlink(constants.OWNER_KEY_FILE)
__unlink(constants.SIGNED_POLICY_FILE)
__unlink(os.path.join(constants.USER_DATA_DIR, 'Local State'))
def fake_ownership():
"""Fake ownership by generating the necessary magic files."""
# Determine the module directory.
dirname = os.path.dirname(__file__)
mock_certfile = os.path.join(dirname, constants.MOCK_OWNER_CERT)
mock_signedpolicyfile = os.path.join(dirname,
constants.MOCK_OWNER_POLICY)
utils.open_write_close(constants.OWNER_KEY_FILE,
cert_extract_pubkey_der(mock_certfile))
shutil.copy(mock_signedpolicyfile,
constants.SIGNED_POLICY_FILE)
POLICY_TYPE = 'google/chromeos/device'
def assert_has_policy_data(response_proto):
"""Assert that given protobuf has a policy_data field.
@param response_proto: a PolicyFetchResponse protobuf.
@raises OwnershipError on failure.
"""
if not response_proto.HasField("policy_data"):
raise OwnershipError('Malformatted response.')
def assert_has_device_settings(data_proto):
"""Assert that given protobuf is a policy with device settings in it.
@param data_proto: a PolicyData protobuf.
@raises OwnershipError if this isn't CrOS policy, or has no settings inside.
"""
if (not data_proto.HasField("policy_type") or
data_proto.policy_type != POLICY_TYPE or
not data_proto.HasField("policy_value")):
raise OwnershipError('Malformatted response.')
def assert_username(data_proto, username):
"""Assert that given protobuf is a policy associated with the given user.
@param data_proto: a PolicyData protobuf.
@param username: the username to check for
@raises OwnershipError if data_proto isn't associated with username
"""
if data_proto.username != username:
raise OwnershipError('Incorrect username.')
def assert_guest_setting(settings, guests):
"""Assert that given protobuf has given guest-related settings.
@param settings: a ChromeDeviceSettingsProto protobuf.
@param guests: boolean indicating whether guests are allowed to sign in.
@raises OwnershipError if settings doesn't enforce the provided setting.
"""
if not settings.HasField("guest_mode_enabled"):
raise OwnershipError('No guest mode setting protobuf.')
if not settings.guest_mode_enabled.HasField("guest_mode_enabled"):
raise OwnershipError('No guest mode setting.')
if settings.guest_mode_enabled.guest_mode_enabled != guests:
raise OwnershipError('Incorrect guest mode setting.')
def assert_show_users(settings, show_users):
"""Assert that given protobuf has given user-avatar-showing settings.
@param settings: a ChromeDeviceSettingsProto protobuf.
@param show_users: boolean indicating whether avatars are shown on sign in.
@raises OwnershipError if settings doesn't enforce the provided setting.
"""
if not settings.HasField("show_user_names"):
raise OwnershipError('No show users setting protobuf.')
if not settings.show_user_names.HasField("show_user_names"):
raise OwnershipError('No show users setting.')
if settings.show_user_names.show_user_names != show_users:
raise OwnershipError('Incorrect show users setting.')
def assert_roaming(settings, roaming):
"""Assert that given protobuf has given roaming settings.
@param settings: a ChromeDeviceSettingsProto protobuf.
@param roaming: boolean indicating whether roaming is allowed.
@raises OwnershipError if settings doesn't enforce the provided setting.
"""
if not settings.HasField("data_roaming_enabled"):
raise OwnershipError('No roaming setting protobuf.')
if not settings.data_roaming_enabled.HasField("data_roaming_enabled"):
raise OwnershipError('No roaming setting.')
if settings.data_roaming_enabled.data_roaming_enabled != roaming:
raise OwnershipError('Incorrect roaming setting.')
def assert_new_users(settings, new_users):
"""Assert that given protobuf has given new user settings.
@param settings: a ChromeDeviceSettingsProto protobuf.
@param new_users: boolean indicating whether adding users is allowed.
@raises OwnershipError if settings doesn't enforce the provided setting.
"""
if not settings.HasField("allow_new_users"):
raise OwnershipError('No allow new users setting protobuf.')
if not settings.allow_new_users.HasField("allow_new_users"):
raise OwnershipError('No allow new users setting.')
if settings.allow_new_users.allow_new_users != new_users:
raise OwnershipError('Incorrect allow new users setting.')
def assert_users_on_whitelist(settings, users):
"""Assert that given protobuf has given users on the whitelist.
@param settings: a ChromeDeviceSettingsProto protobuf.
@param users: iterable containing usernames that should be on whitelist.
@raises OwnershipError if settings doesn't enforce the provided setting.
"""
if settings.HasField("user_whitelist"):
for user in users:
if user not in settings.user_whitelist.user_whitelist:
raise OwnershipError(user + ' not whitelisted.')
else:
raise OwnershipError('No user whitelist.')
def __user_nssdb(user):
"""Returns the path to the NSSDB for the provided user.
@param user: the user whose NSSDB the caller wants.
@return: absolute path to user's NSSDB.
"""
return os.path.join(cryptohome.user_path(user), '.pki', 'nssdb')
def use_known_ownerkeys(user):
"""Sets the system up to use a well-known keypair for owner operations.
Assuming the appropriate cryptohome is already mounted, configures the
device to accept policies signed with the checked-in 'mock' owner key.
@param user: the user whose NSSDB should be populated with key material.
"""
dirname = os.path.dirname(__file__)
mock_keyfile = os.path.join(dirname, constants.MOCK_OWNER_KEY)
mock_certfile = os.path.join(dirname, constants.MOCK_OWNER_CERT)
push_to_nss(mock_keyfile, mock_certfile, __user_nssdb(user))
utils.open_write_close(constants.OWNER_KEY_FILE,
cert_extract_pubkey_der(mock_certfile))
def known_privkey():
"""Returns the mock owner private key in PEM format.
@return: mock owner private key in PEM format.
"""
dirname = os.path.dirname(__file__)
return utils.read_file(os.path.join(dirname, constants.MOCK_OWNER_KEY))
def known_pubkey():
"""Returns the mock owner public key in DER format.
@return: mock owner public key in DER format.
"""
dirname = os.path.dirname(__file__)
return cert_extract_pubkey_der(os.path.join(dirname,
constants.MOCK_OWNER_CERT))
def pairgen():
"""Generate a self-signed cert and associated private key.
Generates a self-signed X509 certificate and the associated private key.
The key is 2048 bits. The generated material is stored in PEM format
and the paths to the two files are returned.
The caller is responsible for cleaning up these files.
@return: (/path/to/private_key, /path/to/self-signed_cert)
"""
keyfile = scoped_tempfile.tempdir.name + '/private.key'
certfile = scoped_tempfile.tempdir.name + '/cert.pem'
cmd = '%s -x509 -subj %s -newkey rsa:2048 -nodes -keyout %s -out %s' % (
OPENSSLREQ, '/CN=me', keyfile, certfile)
system_output_on_fail(cmd)
return (keyfile, certfile)
def pairgen_as_data():
"""Generates keypair, returns keys as data.
Generates a fresh owner keypair and then passes back the
PEM-encoded private key and the DER-encoded public key.
@return: (PEM-encoded private key, DER-encoded public key)
"""
(keypath, certpath) = pairgen()
keyfile = scoped_tempfile(keypath)
certfile = scoped_tempfile(certpath)
return (utils.read_file(keyfile.name),
cert_extract_pubkey_der(certfile.name))
def push_to_nss(keyfile, certfile, nssdb):
"""Takes a pre-generated key pair and pushes them to an NSS DB.
Given paths to a private key and cert in PEM format, stores the pair
in the provided nssdb.
@param keyfile: path to PEM-formatted private key file.
@param certfile: path to PEM-formatted cert file for associated public key.
@param nssdb: path to NSSDB to be populated with the provided keys.
"""
for_push = scoped_tempfile(scoped_tempfile.tempdir.name + '/for_push.p12')
cmd = '%s -export -in %s -inkey %s -out %s ' % (
OPENSSLP12, certfile, keyfile, for_push.name)
cmd += '-passin pass: -passout pass:'
system_output_on_fail(cmd)
cmd = '%s -d "sql:%s" -i %s -W ""' % (PK12UTIL,
nssdb,
for_push.name)
system_output_on_fail(cmd)
def cert_extract_pubkey_der(pem):
"""Given a PEM-formatted cert, extracts the public key in DER format.
Pass in an X509 certificate in PEM format, and you'll get back the
DER-formatted public key as a string.
@param pem: path to a PEM-formatted cert file.
@return: DER-encoded public key from cert, as a string.
"""
outfile = scoped_tempfile(scoped_tempfile.tempdir.name + '/pubkey.der')
cmd = '%s -in %s -pubkey -noout ' % (OPENSSLX509, pem)
cmd += '| %s -outform DER -pubin -out %s' % (OPENSSLRSA,
outfile.name)
system_output_on_fail(cmd)
der = utils.read_file(outfile.name)
return der
def sign(pem_key, data):
"""Signs |data| with key from |pem_key|, returns signature.
Using the PEM-formatted private key in |pem_key|, generates an
RSA-with-SHA1 signature over |data| and returns the signature in
a string.
@param pem_key: PEM-formatted private key, as a string.
@param data: data to be signed.
@return: signature as a string.
"""
sig = scoped_tempfile()
err = scoped_tempfile()
data_file = scoped_tempfile()
data_file.fo.write(data)
data_file.fo.seek(0)
pem_key_file = scoped_tempfile(scoped_tempfile.tempdir.name + '/pkey.pem')
utils.open_write_close(pem_key_file.name, pem_key)
cmd = '%s -sign %s' % (OPENSSLCRYPTO, pem_key_file.name)
try:
utils.run(cmd,
stdin=data_file.fo,
stdout_tee=sig.fo,
stderr_tee=err.fo)
except:
err.fo.seek(0)
logging.error(err.fo.read())
raise
sig.fo.seek(0)
sig_data = sig.fo.read()
if not sig_data:
raise error.OwnershipError('Empty signature!')
return sig_data
def get_user_policy_key_filename(username):
"""Returns the path to the user policy key for the given username.
@param username: the user whose policy key we want the path to.
@return: absolute path to user's policy key file.
"""
return os.path.join(constants.USER_POLICY_DIR,
cryptohome.get_user_hash(username),
constants.USER_POLICY_KEY_FILENAME)