barfab@chromium.org | b6d2993 | 2012-04-11 09:46:43 +0200 | [diff] [blame^] | 1 | # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
Eric Li | 479233e | 2011-03-04 13:06:15 -0800 | [diff] [blame] | 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
barfab@chromium.org | b6d2993 | 2012-04-11 09:46:43 +0200 | [diff] [blame^] | 5 | import dbus, logging, os, tempfile |
| 6 | |
| 7 | import common, constants, cros_ui, cryptohome |
Eric Li | 479233e | 2011-03-04 13:06:15 -0800 | [diff] [blame] | 8 | from autotest_lib.client.bin import utils |
| 9 | from autotest_lib.client.common_lib import autotemp, error |
Eric Li | 479233e | 2011-03-04 13:06:15 -0800 | [diff] [blame] | 10 | |
| 11 | |
Chris Masone | 105706e | 2011-04-29 14:37:11 -0700 | [diff] [blame] | 12 | class OwnershipError(error.TestError): |
| 13 | """Generic error for ownership-related failures.""" |
| 14 | pass |
| 15 | |
| 16 | |
Eric Li | 479233e | 2011-03-04 13:06:15 -0800 | [diff] [blame] | 17 | class scoped_tempfile(object): |
| 18 | """A wrapper that provides scoped semantics for temporary files. |
| 19 | |
| 20 | Providing a file path causes the scoped_tempfile to take ownership of the |
| 21 | file at the provided path. The file at the path will be deleted when this |
| 22 | object goes out of scope. If no path is provided, then a temporary file |
| 23 | object will be created for the lifetime of the scoped_tempfile |
| 24 | |
| 25 | autotemp.tempfile objects don't seem to play nicely with being |
| 26 | used in system commands, so they can't be used for my purposes. |
| 27 | """ |
| 28 | |
| 29 | tempdir = autotemp.tempdir(unique_id=__module__) |
| 30 | |
| 31 | def __init__(self, name=None): |
| 32 | self.name = name |
| 33 | if not self.name: |
| 34 | self.fo = tempfile.TemporaryFile() |
| 35 | |
| 36 | |
| 37 | def __del__(self): |
| 38 | if self.name: |
| 39 | if os.path.exists(self.name): |
| 40 | os.unlink(self.name) |
| 41 | else: |
| 42 | self.fo.close() # Will destroy the underlying tempfile |
| 43 | |
| 44 | |
| 45 | def system_output_on_fail(cmd): |
| 46 | """Run a |cmd|, capturing output and logging it only on error.""" |
| 47 | output = None |
| 48 | try: |
| 49 | output = utils.system_output(cmd) |
| 50 | except: |
| 51 | logging.error(output) |
| 52 | raise |
| 53 | |
| 54 | |
Chris Masone | eac4f4f | 2011-04-06 14:34:25 -0700 | [diff] [blame] | 55 | def __unlink(filename): |
| 56 | try: |
| 57 | os.unlink(filename) |
| 58 | except (IOError, OSError) as error: |
| 59 | logging.info(error) |
| 60 | |
| 61 | |
| 62 | def clear_ownership(): |
| 63 | __unlink(constants.OWNER_KEY_FILE) |
Chris Masone | eac4f4f | 2011-04-06 14:34:25 -0700 | [diff] [blame] | 64 | __unlink(constants.SIGNED_POLICY_FILE) |
| 65 | |
| 66 | |
Chris Masone | d6ce547 | 2011-04-14 16:38:34 -0700 | [diff] [blame] | 67 | def connect_to_session_manager(): |
| 68 | """Create and return a DBus connection to session_manager. |
| 69 | |
| 70 | Connects to the session manager over the DBus system bus. Returns |
| 71 | appropriately configured DBus interface object. |
| 72 | """ |
| 73 | bus = dbus.SystemBus() |
| 74 | proxy = bus.get_object('org.chromium.SessionManager', |
| 75 | '/org/chromium/SessionManager') |
| 76 | return dbus.Interface(proxy, 'org.chromium.SessionManagerInterface') |
| 77 | |
| 78 | |
Chris Masone | 105706e | 2011-04-29 14:37:11 -0700 | [diff] [blame] | 79 | def listen_to_session_manager_signal(callback, signal): |
| 80 | """Create and return a DBus connection to session_manager. |
| 81 | |
| 82 | Connects to the session manager over the DBus system bus. Returns |
| 83 | appropriately configured DBus interface object. |
| 84 | """ |
| 85 | bus = dbus.SystemBus() |
| 86 | bus.add_signal_receiver( |
| 87 | handler_function=callback, |
| 88 | signal_name=signal, |
| 89 | dbus_interface='org.chromium.Chromium', |
| 90 | bus_name=None, |
| 91 | path='/') |
| 92 | |
| 93 | POLICY_TYPE = 'google/chromeos/device' |
| 94 | |
| 95 | |
| 96 | def assert_has_policy_data(response_proto): |
| 97 | if not response_proto.HasField("policy_data"): |
| 98 | raise OwnershipError('Malformatted response.') |
| 99 | |
| 100 | |
| 101 | def assert_has_device_settings(data_proto): |
| 102 | if (not data_proto.HasField("policy_type") or |
| 103 | data_proto.policy_type != POLICY_TYPE or |
| 104 | not data_proto.HasField("policy_value")): |
| 105 | raise OwnershipError('Malformatted response.') |
| 106 | |
| 107 | |
| 108 | def assert_username(data_proto, username): |
| 109 | if data_proto.username != username: |
| 110 | raise OwnershipError('Incorrect username.') |
| 111 | |
| 112 | |
| 113 | def assert_guest_setting(settings, guests): |
| 114 | if not settings.HasField("guest_mode_enabled"): |
| 115 | raise OwnershipError('No guest mode setting protobuf.') |
| 116 | if not settings.guest_mode_enabled.HasField("guest_mode_enabled"): |
| 117 | raise OwnershipError('No guest mode setting.') |
| 118 | if settings.guest_mode_enabled.guest_mode_enabled != guests: |
| 119 | raise OwnershipError('Incorrect guest mode setting.') |
| 120 | |
| 121 | |
| 122 | def assert_show_users(settings, show_users): |
| 123 | if not settings.HasField("show_user_names"): |
| 124 | raise OwnershipError('No show users setting protobuf.') |
| 125 | if not settings.show_user_names.HasField("show_user_names"): |
| 126 | raise OwnershipError('No show users setting.') |
| 127 | if settings.show_user_names.show_user_names != show_users: |
| 128 | raise OwnershipError('Incorrect show users setting.') |
| 129 | |
| 130 | |
| 131 | def assert_roaming(settings, roaming): |
| 132 | if not settings.HasField("data_roaming_enabled"): |
| 133 | raise OwnershipError('No roaming setting protobuf.') |
| 134 | if not settings.data_roaming_enabled.HasField("data_roaming_enabled"): |
| 135 | raise OwnershipError('No roaming setting.') |
| 136 | if settings.data_roaming_enabled.data_roaming_enabled != roaming: |
| 137 | raise OwnershipError('Incorrect roaming setting.') |
| 138 | |
| 139 | |
| 140 | def assert_new_users(settings, new_users): |
| 141 | if not settings.HasField("allow_new_users"): |
| 142 | raise OwnershipError('No allow new users setting protobuf.') |
| 143 | if not settings.allow_new_users.HasField("allow_new_users"): |
| 144 | raise OwnershipError('No allow new users setting.') |
| 145 | if settings.allow_new_users.allow_new_users != new_users: |
| 146 | raise OwnershipError('Incorrect allow new users setting.') |
| 147 | |
| 148 | |
| 149 | def assert_users_on_whitelist(settings, users): |
| 150 | if settings.HasField("user_whitelist"): |
| 151 | for user in users: |
| 152 | if user not in settings.user_whitelist.user_whitelist: |
| 153 | raise OwnershipError(user + ' not whitelisted.') |
| 154 | else: |
| 155 | raise OwnershipError('No user whitelist.') |
| 156 | |
| 157 | |
| 158 | def assert_proxy_settings(settings, proxies): |
| 159 | if not settings.HasField("device_proxy_settings"): |
| 160 | raise OwnershipError('No proxy settings protobuf.') |
| 161 | if not settings.device_proxy_settings.HasField("proxy_mode"): |
| 162 | raise OwnershipError('No proxy_mode setting.') |
| 163 | if settings.device_proxy_settings.proxy_mode != proxies['proxy_mode']: |
| 164 | raise OwnershipError('Incorrect proxies: %s' % proxies) |
| 165 | |
| 166 | |
Eric Li | 479233e | 2011-03-04 13:06:15 -0800 | [diff] [blame] | 167 | NSSDB = constants.CRYPTOHOME_MOUNT_PT + '/.pki/nssdb' |
| 168 | PK12UTIL = 'nsspk12util' |
| 169 | OPENSSLP12 = 'openssl pkcs12' |
| 170 | OPENSSLX509 = 'openssl x509' |
| 171 | OPENSSLRSA = 'openssl rsa' |
| 172 | OPENSSLREQ = 'openssl req' |
| 173 | OPENSSLCRYPTO = 'openssl sha1' |
| 174 | |
| 175 | |
Chris Masone | 105706e | 2011-04-29 14:37:11 -0700 | [diff] [blame] | 176 | def use_known_ownerkeys(): |
| 177 | """Sets the system up to use a well-known keypair for owner operations. |
| 178 | |
| 179 | Assuming the appropriate cryptohome is already mounted, configures the |
| 180 | device to accept policies signed with the checked-in 'mock' owner key. |
| 181 | """ |
| 182 | dirname = os.path.dirname(__file__) |
| 183 | mock_keyfile = os.path.join(dirname, constants.MOCK_OWNER_KEY) |
| 184 | mock_certfile = os.path.join(dirname, constants.MOCK_OWNER_CERT) |
| 185 | push_to_nss(mock_keyfile, mock_certfile, NSSDB) |
| 186 | utils.open_write_close(constants.OWNER_KEY_FILE, |
| 187 | cert_extract_pubkey_der(mock_certfile)) |
| 188 | |
| 189 | |
| 190 | def known_privkey(): |
| 191 | """Returns the mock owner private key in PEM format. |
| 192 | """ |
| 193 | dirname = os.path.dirname(__file__) |
| 194 | return utils.read_file(os.path.join(dirname, constants.MOCK_OWNER_KEY)) |
| 195 | |
| 196 | |
| 197 | def known_pubkey(): |
| 198 | """Returns the mock owner public key in DER format. |
| 199 | """ |
| 200 | dirname = os.path.dirname(__file__) |
| 201 | return cert_extract_pubkey_der(os.path.join(dirname, |
| 202 | constants.MOCK_OWNER_CERT)) |
| 203 | |
| 204 | |
Eric Li | 479233e | 2011-03-04 13:06:15 -0800 | [diff] [blame] | 205 | def pairgen(): |
| 206 | """Generate a self-signed cert and associated private key. |
| 207 | |
| 208 | Generates a self-signed X509 certificate and the associated private key. |
| 209 | The key is 2048 bits. The generated material is stored in PEM format |
| 210 | and the paths to the two files are returned. |
| 211 | |
| 212 | The caller is responsible for cleaning up these files. |
| 213 | """ |
Chris Masone | bbd576f | 2011-04-04 11:40:11 -0700 | [diff] [blame] | 214 | keyfile = scoped_tempfile.tempdir.name + '/private.key' |
| 215 | certfile = scoped_tempfile.tempdir.name + '/cert.pem' |
Eric Li | 479233e | 2011-03-04 13:06:15 -0800 | [diff] [blame] | 216 | cmd = '%s -x509 -subj %s -newkey rsa:2048 -nodes -keyout %s -out %s' % ( |
| 217 | OPENSSLREQ, '/CN=me', keyfile, certfile) |
| 218 | system_output_on_fail(cmd) |
| 219 | return (keyfile, certfile) |
| 220 | |
| 221 | |
Chris Masone | eac4f4f | 2011-04-06 14:34:25 -0700 | [diff] [blame] | 222 | def pairgen_as_data(): |
| 223 | """Generates keypair, returns keys as data. |
| 224 | |
| 225 | Generates a fresh owner keypair and then passes back the |
| 226 | PEM-formatted private key and the DER-encoded public key. |
| 227 | """ |
| 228 | (keypath, certpath) = pairgen() |
| 229 | keyfile = scoped_tempfile(keypath) |
| 230 | certfile = scoped_tempfile(certpath) |
| 231 | return (utils.read_file(keyfile.name), |
| 232 | cert_extract_pubkey_der(certfile.name)) |
| 233 | |
| 234 | |
Eric Li | 479233e | 2011-03-04 13:06:15 -0800 | [diff] [blame] | 235 | def push_to_nss(keyfile, certfile, nssdb): |
| 236 | """Takes a pre-generated key pair and pushes them to an NSS DB. |
| 237 | |
| 238 | Given paths to a private key and cert in PEM format, stores the pair |
| 239 | in the provided nssdb. |
| 240 | """ |
Chris Masone | eac4f4f | 2011-04-06 14:34:25 -0700 | [diff] [blame] | 241 | for_push = scoped_tempfile(scoped_tempfile.tempdir.name + '/for_push.p12') |
Eric Li | 479233e | 2011-03-04 13:06:15 -0800 | [diff] [blame] | 242 | cmd = '%s -export -in %s -inkey %s -out %s ' % ( |
| 243 | OPENSSLP12, certfile, keyfile, for_push.name) |
| 244 | cmd += '-passin pass: -passout pass:' |
| 245 | system_output_on_fail(cmd) |
| 246 | cmd = '%s -d "sql:%s" -i %s -W ""' % (PK12UTIL, |
| 247 | nssdb, |
| 248 | for_push.name) |
| 249 | system_output_on_fail(cmd) |
| 250 | |
| 251 | |
| 252 | def generate_owner_creds(): |
| 253 | """Generates a keypair, registered with NSS, and returns key and cert. |
| 254 | |
| 255 | Generates a fresh self-signed cert and private key. Registers them |
| 256 | with NSS and then passes back paths to files containing the |
| 257 | PEM-formatted private key and certificate. |
| 258 | """ |
| 259 | (keyfile, certfile) = pairgen() |
| 260 | push_to_nss(keyfile, certfile, NSSDB) |
| 261 | return (keyfile, certfile) |
| 262 | |
| 263 | |
| 264 | |
| 265 | def cert_extract_pubkey_der(pem): |
| 266 | """Given a PEM-formatted cert, extracts the public key in DER format. |
| 267 | |
| 268 | Pass in an X509 certificate in PEM format, and you'll get back the |
| 269 | DER-formatted public key as a string. |
| 270 | """ |
Chris Masone | eac4f4f | 2011-04-06 14:34:25 -0700 | [diff] [blame] | 271 | outfile = scoped_tempfile(scoped_tempfile.tempdir.name + '/pubkey.der') |
Eric Li | 479233e | 2011-03-04 13:06:15 -0800 | [diff] [blame] | 272 | cmd = '%s -in %s -pubkey -noout ' % (OPENSSLX509, pem) |
| 273 | cmd += '| %s -outform DER -pubin -out %s' % (OPENSSLRSA, |
| 274 | outfile.name) |
| 275 | system_output_on_fail(cmd) |
| 276 | der = utils.read_file(outfile.name) |
| 277 | return der |
| 278 | |
| 279 | |
Chris Masone | eac4f4f | 2011-04-06 14:34:25 -0700 | [diff] [blame] | 280 | def generate_and_register_keypair(testuser, testpass): |
| 281 | """Generates keypair, registers with NSS, sets owner key, returns keypair. |
Eric Li | 479233e | 2011-03-04 13:06:15 -0800 | [diff] [blame] | 282 | |
| 283 | Generates a fresh owner keypair. Registers keys with NSS, |
| 284 | puts the owner public key in the right place, ensures that the |
| 285 | session_manager picks it up, ensures the owner's home dir is |
Chris Masone | eac4f4f | 2011-04-06 14:34:25 -0700 | [diff] [blame] | 286 | mounted, and then passes back the PEM-formatted private key and the |
| 287 | DER-encoded public key. |
Eric Li | 479233e | 2011-03-04 13:06:15 -0800 | [diff] [blame] | 288 | """ |
Chris Masone | eac4f4f | 2011-04-06 14:34:25 -0700 | [diff] [blame] | 289 | (keypath, certpath) = generate_owner_creds() |
| 290 | keyfile = scoped_tempfile(keypath) |
| 291 | certfile = scoped_tempfile(certpath) |
| 292 | |
| 293 | pubkey = cert_extract_pubkey_der(certfile.name) |
| 294 | utils.open_write_close(constants.OWNER_KEY_FILE, pubkey) |
| 295 | |
David James | d51ac9c | 2011-09-10 00:45:24 -0700 | [diff] [blame] | 296 | cros_ui.nuke() |
Eric Li | 479233e | 2011-03-04 13:06:15 -0800 | [diff] [blame] | 297 | cryptohome.mount_vault(testuser, testpass, create=False) |
Chris Masone | eac4f4f | 2011-04-06 14:34:25 -0700 | [diff] [blame] | 298 | return (utils.read_file(keyfile.name), pubkey) |
Eric Li | 479233e | 2011-03-04 13:06:15 -0800 | [diff] [blame] | 299 | |
| 300 | |
Chris Masone | eac4f4f | 2011-04-06 14:34:25 -0700 | [diff] [blame] | 301 | def sign(pem_key, data): |
| 302 | """Signs |data| with key from |pem_key|, returns signature. |
Eric Li | 479233e | 2011-03-04 13:06:15 -0800 | [diff] [blame] | 303 | |
Chris Masone | eac4f4f | 2011-04-06 14:34:25 -0700 | [diff] [blame] | 304 | Using the PEM-formatted private key in |pem_key|, generates an |
Eric Li | 479233e | 2011-03-04 13:06:15 -0800 | [diff] [blame] | 305 | RSA-with-SHA1 signature over |data| and returns the signature in |
| 306 | a string. |
| 307 | """ |
| 308 | sig = scoped_tempfile() |
| 309 | err = scoped_tempfile() |
| 310 | data_file = scoped_tempfile() |
| 311 | data_file.fo.write(data) |
| 312 | data_file.fo.seek(0) |
| 313 | |
Chris Masone | eac4f4f | 2011-04-06 14:34:25 -0700 | [diff] [blame] | 314 | pem_key_file = scoped_tempfile(scoped_tempfile.tempdir.name + '/pkey.pem') |
| 315 | utils.open_write_close(pem_key_file.name, pem_key) |
| 316 | |
| 317 | cmd = '%s -sign %s' % (OPENSSLCRYPTO, pem_key_file.name) |
Eric Li | 479233e | 2011-03-04 13:06:15 -0800 | [diff] [blame] | 318 | try: |
| 319 | utils.run(cmd, |
| 320 | stdin=data_file.fo, |
| 321 | stdout_tee=sig.fo, |
| 322 | stderr_tee=err.fo) |
| 323 | except: |
| 324 | err.fo.seek(0) |
| 325 | logging.error(err.fo.read()) |
| 326 | raise |
| 327 | |
| 328 | sig.fo.seek(0) |
| 329 | sig_data = sig.fo.read() |
| 330 | if not sig_data: |
Chris Masone | 105706e | 2011-04-29 14:37:11 -0700 | [diff] [blame] | 331 | raise error.OwnershipError('Empty signature!') |
Eric Li | 479233e | 2011-03-04 13:06:15 -0800 | [diff] [blame] | 332 | return sig_data |