policy_testserver.py revision effb81e5f8246d0db0270817048dc992db66e9fb
1# Copyright (c) 2012 The Chromium 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"""A bare-bones test server for testing cloud policy support. 6 7This implements a simple cloud policy test server that can be used to test 8chrome's device management service client. The policy information is read from 9the file named device_management in the server's data directory. It contains 10enforced and recommended policies for the device and user scope, and a list 11of managed users. 12 13The format of the file is JSON. The root dictionary contains a list under the 14key "managed_users". It contains auth tokens for which the server will claim 15that the user is managed. The token string "*" indicates that all users are 16claimed to be managed. Other keys in the root dictionary identify request 17scopes. The user-request scope is described by a dictionary that holds two 18sub-dictionaries: "mandatory" and "recommended". Both these hold the policy 19definitions as key/value stores, their format is identical to what the Linux 20implementation reads from /etc. 21The device-scope holds the policy-definition directly as key/value stores in the 22protobuf-format. 23 24Example: 25 26{ 27 "google/chromeos/device" : { 28 "guest_mode_enabled" : false 29 }, 30 "google/chromeos/user" : { 31 "mandatory" : { 32 "HomepageLocation" : "http://www.chromium.org", 33 "IncognitoEnabled" : false 34 }, 35 "recommended" : { 36 "JavascriptEnabled": false 37 } 38 }, 39 "google/chromeos/publicaccount/user@example.com" : { 40 "mandatory" : { 41 "HomepageLocation" : "http://www.chromium.org" 42 }, 43 "recommended" : { 44 } 45 }, 46 "managed_users" : [ 47 "secret123456" 48 ], 49 "current_key_index": 0, 50 "robot_api_auth_code": "fake_auth_code", 51 "invalidation_source": 1025, 52 "invalidation_name": "UENUPOL" 53} 54 55""" 56 57import base64 58import BaseHTTPServer 59import cgi 60import google.protobuf.text_format 61import hashlib 62import logging 63import os 64import random 65import re 66import sys 67import time 68import tlslite 69import tlslite.api 70import tlslite.utils 71import tlslite.utils.cryptomath 72import urlparse 73 74# The name and availability of the json module varies in python versions. 75try: 76 import simplejson as json 77except ImportError: 78 try: 79 import json 80 except ImportError: 81 json = None 82 83import asn1der 84import testserver_base 85 86import device_management_backend_pb2 as dm 87import cloud_policy_pb2 as cp 88import chrome_extension_policy_pb2 as ep 89 90# Device policy is only available on Chrome OS builds. 91try: 92 import chrome_device_policy_pb2 as dp 93except ImportError: 94 dp = None 95 96# ASN.1 object identifier for PKCS#1/RSA. 97PKCS1_RSA_OID = '\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01' 98 99# List of bad machine identifiers that trigger the |valid_serial_number_missing| 100# flag to be set set in the policy fetch response. 101BAD_MACHINE_IDS = [ '123490EN400015' ] 102 103# List of machines that trigger the server to send kiosk enrollment response 104# for the register request. 105KIOSK_MACHINE_IDS = [ 'KIOSK' ] 106 107# Dictionary containing base64-encoded policy signing keys plus per-domain 108# signatures. Format is: 109# { 110# 'key': <base64-encoded PKCS8-format private key>, 111# 'signatures': { 112# <domain1>: <base64-encdoded SHA256 signature for key + domain1> 113# <domain2>: <base64-encdoded SHA256 signature for key + domain2> 114# ... 115# } 116# } 117SIGNING_KEYS = [ 118 # Key1 119 {'key': 120 'MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA2c3KzcPqvnJ5HCk3OZkf1' 121 'LMO8Ht4dw4FO2U0EmKvpo0zznj4RwUdmKobH1AFWzwZP4CDY2M67MsukE/1Jnbx1QIDAQ' 122 'ABAkBkKcLZa/75hHVz4PR3tZaw34PATlfxEG6RiRIwXlf/FFlfGIZOSxdW/I1A3XRl0/9' 123 'nZMuctBSKBrcTRZQWfT/hAiEA9g8xbQbMO6BEH/XCRSsQbPlvj4c9wDtVEzeAzZ/ht9kC' 124 'IQDiml+/lXS1emqml711jJcYJNYJzdy1lL/ieKogR59oXQIhAK+Pl4xa1U2VxAWpq7r+R' 125 'vH55wdZT03hB4p2h4gvEzXBAiAkw9kvE0eZPiBZoRrrHIFTOH7FnnHlwBmV2+/2RsiVPQ' 126 'IhAKqx/4qisivvmoM/xbzUagfoxwsu1A/4mGjhBKiS0BCq', 127 'signatures': 128 {'example.com': 129 'l+sT5mziei/GbmiP7VtRCCfwpZcg7uKbW2OlnK5B/TTELutjEIAMdHduNBwbO44qOn' 130 '/5c7YrtkXbBehaaDYFPGI6bGTbDmG9KRxhS+DaB7opgfCQWLi79Gn/jytKLZhRN/VS' 131 'y+PEbezqMi3d1/xDxlThwWZDNwnhv9ER/Nu/32ZTjzgtqonSn2CQtwXCIILm4FdV/1' 132 '/BdmZG+Ge4i4FTqYtInir5YFe611KXU/AveGhQGBIAXo4qYg1IqbVrvKBSU9dlI6Sl' 133 '9TJJLbJ3LGaXuljgFhyMAl3gcy7ftC9MohEmwa+sc7y2mOAgYQ5SSmyAtQwQgAkX9J' 134 '3+tfxjmoA/dg==', 135 'chromepolicytest.com': 136 'TzBiigZKwBdr6lyP6tUDsw+Q9wYO1Yepyxm0O4JZ4RID32L27sWzC1/hwC51fRcCvP' 137 'luEVIW6mH+BFODXMrteUFWfbbG7jgV+Wg+QdzMqgJjxhNKFXPTsZ7/286LAd1vBY/A' 138 'nGd8Wog6AhzfrgMbLNsH794GD0xIUwRvXUWFNP8pClj5VPgQnJrIA9aZwW8FNGbteA' 139 'HacFB0T/oqP5s7XT4Qvkj14RLmCgTwEM8Vcpqy5teJaF8yN17wniveddoOQGH6s0HC' 140 'ocprEccrH5fP/WVAPxCfx4vVYQY5q4CZ4K3f6dTC2FV4IDelM6dugEkvSS02YCzDaO' 141 'N+Z7IwElzTKg==', 142 'managedchrome.com': 143 'T0wXC5w3GXyovA09pyOLX7ui/NI603UfbZXYyTbHI7xtzCIaHVPH35Nx4zdqVrdsej' 144 'ErQ12yVLDDIJokY4Yl+/fj/zrkAPxThI+TNQ+jo0i+al05PuopfpzvCzIXiZBbkbyW' 145 '3XfedxXP3IPN2XU2/3vX+ZXUNG6pxeETem64kGezkjkUraqnHw3JVzwJYHhpMcwdLP' 146 'PYK6V23BbEHEVBtQZd/ledXacz7gOzm1zGni4e+vxA2roAdJWyhbjU0dTKNNUsZmMv' 147 'ryQH9Af1Jw+dqs0RAbhcJXm2i8EUWIgNv6aMn1Z2DzZwKKjXsKgcYSRo8pdYa8RZAo' 148 'UExd9roA9a5w==', 149 } 150 }, 151 # Key2 152 {'key': 153 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmZhreV04M3knCi6wibr49' 154 'oDesHny1G33PKOX9ko8pcxAiu9ZqsKCj7wNW2PGqnLi81fddACwQtYn5xdhCtzB9wIDAQ' 155 'ABAkA0z8m0cy8N08xundspoFZWO71WJLgv/peSDBYGI0RzJR1l9Np355EukQUQwRs5XrL' 156 '3vRQZy2vDqeiR96epkAhRAiEAzJ4DVI8k3pAl7CGv5icqFkJ02viExIwehhIEXBcB6p0C' 157 'IQDAKmzpoRpBEZRQ9xrTvPOi+Ea8Jnd478BU7CI/LFfgowIgMfLIoVWoDGRnvXKju60Hy' 158 'xNB70oHLut9cADp64j6QMkCIDrgxN4QbmrhaAAmtiGKE1wrlgCwCIsVamiasSOKAqLhAi' 159 'EAo/ItVcFtQPod97qG71CY/O4JzOciuU6AMhprs181vfM=', 160 'signatures': 161 # Key2 signatures 162 {'example.com': 163 'cO0nQjRptkeefKDw5QpJSQDavHABxUvbR9Wvoa235OG9Whw1RFqq2ye6pKnI3ezW6/' 164 '7b4ANcpi5a7HV5uF8K7gWyYdxY8NHLeyrbwXxg5j6HAmHmkP1UZcf/dAnWqo7cW8g4' 165 'DIQOhC43KkveMYJ2HnelwdXt/7zqkbe8/3Yj4nhjAUeARx86Sb8Nzydwkrvqs5Jw/x' 166 '5LG+BODExrXXcGu/ubDlW4ivJFqfNUPQysqBXSMY2XCHPJDx3eECLGVVN/fFAWWgjM' 167 'HFObAriAt0b18cc9Nr0mAt4Qq1oDzWcAHCPHE+5dr8Uf46BUrMLJRNRKCY7rrsoIin' 168 '9Be9gs3W+Aww==', 169 'chromepolicytest.com': 170 'mr+9CCYvR0cTvPwlzkxqlpGYy55gY7cPiIkPAPoql51yHK1tkMTOSFru8Dy/nMt+0o' 171 '4z7WO60F1wnIBGkQxnTj/DsO6QpCYi7oHqtLmZ2jsLQFlMyvPGUtpJEFvRwjr/TNbh' 172 '6RqUtz1LQFuJQ848kBrx7nkte1L8SuPDExgx+Q3LtbNj4SuTdvMUBMvEERXiLuwfFL' 173 'BefGjtsqfWETQVlJTCW7xcqOLedIX8UYgEDBpDOZ23A3GzCShuBsIut5m87R5mODht' 174 'EUmKNDK1+OMc6SyDpf+r48Wph4Db1bVaKy8fcpSNJOwEgsrmH7/+owKPGcN7I5jYAF' 175 'Z2PGxHTQ9JNA==', 176 'managedchrome.com': 177 'o5MVSo4bRwIJ/aooGyXpRXsEsWPG8fNA2UTG8hgwnLYhNeJCCnLs/vW2vdp0URE8jn' 178 'qiG4N8KjbuiGw0rJtO1EygdLfpnMEtqYlFjrOie38sy92l/AwohXj6luYzMWL+FqDu' 179 'WQeXasjgyY4s9BOLQVDEnEj3pvqhrk/mXvMwUeXGpbxTNbWAd0C8BTZrGOwU/kIXxo' 180 'vAMGg8L+rQaDwBTEnMsMZcvlrIyqSg5v4BxCWuL3Yd2xvUqZEUWRp1aKetsHRnz5hw' 181 'H7WK7DzvKepDn06XjPG9lchi448U3HB3PRKtCzfO3nD9YXMKTuqRpKPF8PeK11CWh1' 182 'DBvBYwi20vbQ==', 183 }, 184 }, 185] 186 187class PolicyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): 188 """Decodes and handles device management requests from clients. 189 190 The handler implements all the request parsing and protobuf message decoding 191 and encoding. It calls back into the server to lookup, register, and 192 unregister clients. 193 """ 194 195 def __init__(self, request, client_address, server): 196 """Initialize the handler. 197 198 Args: 199 request: The request data received from the client as a string. 200 client_address: The client address. 201 server: The TestServer object to use for (un)registering clients. 202 """ 203 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request, 204 client_address, server) 205 206 def GetUniqueParam(self, name): 207 """Extracts a unique query parameter from the request. 208 209 Args: 210 name: Names the parameter to fetch. 211 Returns: 212 The parameter value or None if the parameter doesn't exist or is not 213 unique. 214 """ 215 if not hasattr(self, '_params'): 216 self._params = cgi.parse_qs(self.path[self.path.find('?') + 1:]) 217 218 param_list = self._params.get(name, []) 219 if len(param_list) == 1: 220 return param_list[0] 221 return None 222 223 def do_GET(self): 224 """Handles GET requests. 225 226 Currently this is only used to serve external policy data.""" 227 sep = self.path.find('?') 228 path = self.path if sep == -1 else self.path[:sep] 229 if path == '/externalpolicydata': 230 http_response, raw_reply = self.HandleExternalPolicyDataRequest() 231 else: 232 http_response = 404 233 raw_reply = 'Invalid path' 234 self.send_response(http_response) 235 self.end_headers() 236 self.wfile.write(raw_reply) 237 238 def do_POST(self): 239 http_response, raw_reply = self.HandleRequest() 240 self.send_response(http_response) 241 if (http_response == 200): 242 self.send_header('Content-Type', 'application/x-protobuffer') 243 self.end_headers() 244 self.wfile.write(raw_reply) 245 246 def HandleExternalPolicyDataRequest(self): 247 """Handles a request to download policy data for a component.""" 248 policy_key = self.GetUniqueParam('key') 249 if not policy_key: 250 return (400, 'Missing key parameter') 251 data = self.server.ReadPolicyDataFromDataDir(policy_key) 252 if data is None: 253 return (404, 'Policy not found for ' + policy_key) 254 return (200, data) 255 256 def HandleRequest(self): 257 """Handles a request. 258 259 Parses the data supplied at construction time and returns a pair indicating 260 http status code and response data to be sent back to the client. 261 262 Returns: 263 A tuple of HTTP status code and response data to send to the client. 264 """ 265 rmsg = dm.DeviceManagementRequest() 266 length = int(self.headers.getheader('content-length')) 267 rmsg.ParseFromString(self.rfile.read(length)) 268 269 logging.debug('gaia auth token -> ' + 270 self.headers.getheader('Authorization', '')) 271 logging.debug('oauth token -> ' + str(self.GetUniqueParam('oauth_token'))) 272 logging.debug('deviceid -> ' + str(self.GetUniqueParam('deviceid'))) 273 self.DumpMessage('Request', rmsg) 274 275 request_type = self.GetUniqueParam('request') 276 # Check server side requirements, as defined in 277 # device_management_backend.proto. 278 if (self.GetUniqueParam('devicetype') != '2' or 279 self.GetUniqueParam('apptype') != 'Chrome' or 280 (request_type != 'ping' and 281 len(self.GetUniqueParam('deviceid')) >= 64) or 282 len(self.GetUniqueParam('agent')) >= 64): 283 return (400, 'Invalid request parameter') 284 if request_type == 'register': 285 response = self.ProcessRegister(rmsg.register_request) 286 elif request_type == 'api_authorization': 287 response = self.ProcessApiAuthorization(rmsg.service_api_access_request) 288 elif request_type == 'unregister': 289 response = self.ProcessUnregister(rmsg.unregister_request) 290 elif request_type == 'policy' or request_type == 'ping': 291 response = self.ProcessPolicy(rmsg, request_type) 292 elif request_type == 'enterprise_check': 293 response = self.ProcessAutoEnrollment(rmsg.auto_enrollment_request) 294 elif request_type == 'device_state_retrieval': 295 response = self.ProcessDeviceStateRetrievalRequest( 296 rmsg.device_state_retrieval_request) 297 else: 298 return (400, 'Invalid request parameter') 299 300 self.DumpMessage('Response', response[1]) 301 return (response[0], response[1].SerializeToString()) 302 303 def CreatePolicyForExternalPolicyData(self, policy_key): 304 """Returns an ExternalPolicyData protobuf for policy_key. 305 306 If there is policy data for policy_key then the download url will be 307 set so that it points to that data, and the appropriate hash is also set. 308 Otherwise, the protobuf will be empty. 309 310 Args: 311 policy_key: the policy type and settings entity id, joined by '/'. 312 313 Returns: 314 A serialized ExternalPolicyData. 315 """ 316 settings = ep.ExternalPolicyData() 317 data = self.server.ReadPolicyDataFromDataDir(policy_key) 318 if data: 319 settings.download_url = urlparse.urljoin( 320 self.server.GetBaseURL(), 'externalpolicydata?key=%s' % policy_key) 321 settings.secure_hash = hashlib.sha256(data).digest() 322 return settings.SerializeToString() 323 324 def CheckGoogleLogin(self): 325 """Extracts the auth token from the request and returns it. The token may 326 either be a GoogleLogin token from an Authorization header, or an OAuth V2 327 token from the oauth_token query parameter. Returns None if no token is 328 present. 329 """ 330 oauth_token = self.GetUniqueParam('oauth_token') 331 if oauth_token: 332 return oauth_token 333 334 match = re.match('GoogleLogin auth=(\\w+)', 335 self.headers.getheader('Authorization', '')) 336 if match: 337 return match.group(1) 338 339 return None 340 341 def ProcessRegister(self, msg): 342 """Handles a register request. 343 344 Checks the query for authorization and device identifier, registers the 345 device with the server and constructs a response. 346 347 Args: 348 msg: The DeviceRegisterRequest message received from the client. 349 350 Returns: 351 A tuple of HTTP status code and response data to send to the client. 352 """ 353 # Check the auth token and device ID. 354 auth = self.CheckGoogleLogin() 355 if not auth: 356 return (403, 'No authorization') 357 358 policy = self.server.GetPolicies() 359 if ('*' not in policy['managed_users'] and 360 auth not in policy['managed_users']): 361 return (403, 'Unmanaged') 362 363 device_id = self.GetUniqueParam('deviceid') 364 if not device_id: 365 return (400, 'Missing device identifier') 366 367 token_info = self.server.RegisterDevice(device_id, 368 msg.machine_id, 369 msg.type) 370 371 # Send back the reply. 372 response = dm.DeviceManagementResponse() 373 response.register_response.device_management_token = ( 374 token_info['device_token']) 375 response.register_response.machine_name = token_info['machine_name'] 376 response.register_response.enrollment_type = token_info['enrollment_mode'] 377 378 return (200, response) 379 380 def ProcessApiAuthorization(self, msg): 381 """Handles an API authorization request. 382 383 Args: 384 msg: The DeviceServiceApiAccessRequest message received from the client. 385 386 Returns: 387 A tuple of HTTP status code and response data to send to the client. 388 """ 389 policy = self.server.GetPolicies() 390 391 # Return the auth code from the config file if it's defined, 392 # else return a descriptive default value. 393 response = dm.DeviceManagementResponse() 394 response.service_api_access_response.auth_code = policy.get( 395 'robot_api_auth_code', 'policy_testserver.py-auth_code') 396 397 return (200, response) 398 399 def ProcessUnregister(self, msg): 400 """Handles a register request. 401 402 Checks for authorization, unregisters the device and constructs the 403 response. 404 405 Args: 406 msg: The DeviceUnregisterRequest message received from the client. 407 408 Returns: 409 A tuple of HTTP status code and response data to send to the client. 410 """ 411 # Check the management token. 412 token, response = self.CheckToken() 413 if not token: 414 return response 415 416 # Unregister the device. 417 self.server.UnregisterDevice(token['device_token']) 418 419 # Prepare and send the response. 420 response = dm.DeviceManagementResponse() 421 response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse()) 422 423 return (200, response) 424 425 def ProcessPolicy(self, msg, request_type): 426 """Handles a policy request. 427 428 Checks for authorization, encodes the policy into protobuf representation 429 and constructs the response. 430 431 Args: 432 msg: The DeviceManagementRequest message received from the client. 433 434 Returns: 435 A tuple of HTTP status code and response data to send to the client. 436 """ 437 token_info, error = self.CheckToken() 438 if not token_info: 439 return error 440 441 key_update_request = msg.device_state_key_update_request 442 if len(key_update_request.server_backed_state_key) > 0: 443 self.server.UpdateStateKeys(token_info['device_token'], 444 key_update_request.server_backed_state_key) 445 446 response = dm.DeviceManagementResponse() 447 for request in msg.policy_request.request: 448 fetch_response = response.policy_response.response.add() 449 if (request.policy_type in 450 ('google/android/user', 451 'google/chrome/extension', 452 'google/chromeos/device', 453 'google/chromeos/publicaccount', 454 'google/chromeos/user', 455 'google/chrome/user', 456 'google/ios/user')): 457 if request_type != 'policy': 458 fetch_response.error_code = 400 459 fetch_response.error_message = 'Invalid request type' 460 else: 461 self.ProcessCloudPolicy(request, token_info, fetch_response) 462 else: 463 fetch_response.error_code = 400 464 fetch_response.error_message = 'Invalid policy_type' 465 466 return (200, response) 467 468 def ProcessAutoEnrollment(self, msg): 469 """Handles an auto-enrollment check request. 470 471 The reply depends on the value of the modulus: 472 1: replies with no new modulus and the sha256 hash of "0" 473 2: replies with a new modulus, 4. 474 4: replies with a new modulus, 2. 475 8: fails with error 400. 476 16: replies with a new modulus, 16. 477 32: replies with a new modulus, 1. 478 anything else: replies with no new modulus and an empty list of hashes 479 480 These allow the client to pick the testing scenario its wants to simulate. 481 482 Args: 483 msg: The DeviceAutoEnrollmentRequest message received from the client. 484 485 Returns: 486 A tuple of HTTP status code and response data to send to the client. 487 """ 488 auto_enrollment_response = dm.DeviceAutoEnrollmentResponse() 489 490 if msg.modulus == 1: 491 auto_enrollment_response.hash.extend( 492 self.server.GetMatchingStateKeyHashes(msg.modulus, msg.remainder)) 493 elif msg.modulus == 2: 494 auto_enrollment_response.expected_modulus = 4 495 elif msg.modulus == 4: 496 auto_enrollment_response.expected_modulus = 2 497 elif msg.modulus == 8: 498 return (400, 'Server error') 499 elif msg.modulus == 16: 500 auto_enrollment_response.expected_modulus = 16 501 elif msg.modulus == 32: 502 auto_enrollment_response.expected_modulus = 1 503 504 response = dm.DeviceManagementResponse() 505 response.auto_enrollment_response.CopyFrom(auto_enrollment_response) 506 return (200, response) 507 508 def ProcessDeviceStateRetrievalRequest(self, msg): 509 """Handles a device state retrieval request. 510 511 Response data is taken from server configuration. 512 513 Returns: 514 A tuple of HTTP status code and response data to send to the client. 515 """ 516 device_state_retrieval_response = dm.DeviceStateRetrievalResponse() 517 518 client = self.server.LookupByStateKey(msg.server_backed_state_key) 519 if client is not None: 520 state = self.server.GetPolicies().get('device_state', {}) 521 FIELDS = [ 522 'management_domain', 523 'restore_mode', 524 ] 525 for field in FIELDS: 526 if field in state: 527 setattr(device_state_retrieval_response, field, state[field]) 528 529 response = dm.DeviceManagementResponse() 530 response.device_state_retrieval_response.CopyFrom( 531 device_state_retrieval_response) 532 return (200, response) 533 534 def SetProtobufMessageField(self, group_message, field, field_value): 535 '''Sets a field in a protobuf message. 536 537 Args: 538 group_message: The protobuf message. 539 field: The field of the message to set, it should be a member of 540 group_message.DESCRIPTOR.fields. 541 field_value: The value to set. 542 ''' 543 if field.label == field.LABEL_REPEATED: 544 assert type(field_value) == list 545 entries = group_message.__getattribute__(field.name) 546 if field.message_type is None: 547 for list_item in field_value: 548 entries.append(list_item) 549 else: 550 # This field is itself a protobuf. 551 sub_type = field.message_type 552 for sub_value in field_value: 553 assert type(sub_value) == dict 554 # Add a new sub-protobuf per list entry. 555 sub_message = entries.add() 556 # Now iterate over its fields and recursively add them. 557 for sub_field in sub_message.DESCRIPTOR.fields: 558 if sub_field.name in sub_value: 559 value = sub_value[sub_field.name] 560 self.SetProtobufMessageField(sub_message, sub_field, value) 561 return 562 elif field.type == field.TYPE_BOOL: 563 assert type(field_value) == bool 564 elif field.type == field.TYPE_STRING: 565 assert type(field_value) == str or type(field_value) == unicode 566 elif field.type == field.TYPE_INT64: 567 assert type(field_value) == int 568 elif (field.type == field.TYPE_MESSAGE and 569 field.message_type.name == 'StringList'): 570 assert type(field_value) == list 571 entries = group_message.__getattribute__(field.name).entries 572 for list_item in field_value: 573 entries.append(list_item) 574 return 575 else: 576 raise Exception('Unknown field type %s' % field.type) 577 group_message.__setattr__(field.name, field_value) 578 579 def GatherDevicePolicySettings(self, settings, policies): 580 '''Copies all the policies from a dictionary into a protobuf of type 581 CloudDeviceSettingsProto. 582 583 Args: 584 settings: The destination ChromeDeviceSettingsProto protobuf. 585 policies: The source dictionary containing policies in JSON format. 586 ''' 587 for group in settings.DESCRIPTOR.fields: 588 # Create protobuf message for group. 589 group_message = eval('dp.' + group.message_type.name + '()') 590 # Indicates if at least one field was set in |group_message|. 591 got_fields = False 592 # Iterate over fields of the message and feed them from the 593 # policy config file. 594 for field in group_message.DESCRIPTOR.fields: 595 field_value = None 596 if field.name in policies: 597 got_fields = True 598 field_value = policies[field.name] 599 self.SetProtobufMessageField(group_message, field, field_value) 600 if got_fields: 601 settings.__getattribute__(group.name).CopyFrom(group_message) 602 603 def GatherUserPolicySettings(self, settings, policies): 604 '''Copies all the policies from a dictionary into a protobuf of type 605 CloudPolicySettings. 606 607 Args: 608 settings: The destination: a CloudPolicySettings protobuf. 609 policies: The source: a dictionary containing policies under keys 610 'recommended' and 'mandatory'. 611 ''' 612 for field in settings.DESCRIPTOR.fields: 613 # |field| is the entry for a specific policy in the top-level 614 # CloudPolicySettings proto. 615 616 # Look for this policy's value in the mandatory or recommended dicts. 617 if field.name in policies.get('mandatory', {}): 618 mode = cp.PolicyOptions.MANDATORY 619 value = policies['mandatory'][field.name] 620 elif field.name in policies.get('recommended', {}): 621 mode = cp.PolicyOptions.RECOMMENDED 622 value = policies['recommended'][field.name] 623 else: 624 continue 625 626 # Create protobuf message for this policy. 627 policy_message = eval('cp.' + field.message_type.name + '()') 628 policy_message.policy_options.mode = mode 629 field_descriptor = policy_message.DESCRIPTOR.fields_by_name['value'] 630 self.SetProtobufMessageField(policy_message, field_descriptor, value) 631 settings.__getattribute__(field.name).CopyFrom(policy_message) 632 633 def ProcessCloudPolicy(self, msg, token_info, response): 634 """Handles a cloud policy request. (New protocol for policy requests.) 635 636 Encodes the policy into protobuf representation, signs it and constructs 637 the response. 638 639 Args: 640 msg: The CloudPolicyRequest message received from the client. 641 token_info: the token extracted from the request. 642 response: A PolicyFetchResponse message that should be filled with the 643 response data. 644 """ 645 646 if msg.machine_id: 647 self.server.UpdateMachineId(token_info['device_token'], msg.machine_id) 648 649 # Response is only given if the scope is specified in the config file. 650 # Normally 'google/chromeos/device', 'google/chromeos/user' and 651 # 'google/chromeos/publicaccount' should be accepted. 652 policy = self.server.GetPolicies() 653 policy_value = '' 654 policy_key = msg.policy_type 655 if msg.settings_entity_id: 656 policy_key += '/' + msg.settings_entity_id 657 if msg.policy_type in token_info['allowed_policy_types']: 658 if msg.policy_type in ('google/android/user', 659 'google/chromeos/publicaccount', 660 'google/chromeos/user', 661 'google/chrome/user', 662 'google/ios/user'): 663 settings = cp.CloudPolicySettings() 664 payload = self.server.ReadPolicyFromDataDir(policy_key, settings) 665 if payload is None: 666 self.GatherUserPolicySettings(settings, policy.get(policy_key, {})) 667 payload = settings.SerializeToString() 668 elif dp is not None and msg.policy_type == 'google/chromeos/device': 669 settings = dp.ChromeDeviceSettingsProto() 670 payload = self.server.ReadPolicyFromDataDir(policy_key, settings) 671 if payload is None: 672 self.GatherDevicePolicySettings(settings, policy.get(policy_key, {})) 673 payload = settings.SerializeToString() 674 elif msg.policy_type == 'google/chrome/extension': 675 settings = ep.ExternalPolicyData() 676 payload = self.server.ReadPolicyFromDataDir(policy_key, settings) 677 if payload is None: 678 payload = self.CreatePolicyForExternalPolicyData(policy_key) 679 else: 680 response.error_code = 400 681 response.error_message = 'Invalid policy type' 682 return 683 else: 684 response.error_code = 400 685 response.error_message = 'Request not allowed for the token used' 686 return 687 688 # Sign with 'current_key_index', defaulting to key 0. 689 signing_key = None 690 req_key = None 691 current_key_index = policy.get('current_key_index', 0) 692 nkeys = len(self.server.keys) 693 if (msg.signature_type == dm.PolicyFetchRequest.SHA1_RSA and 694 current_key_index in range(nkeys)): 695 signing_key = self.server.keys[current_key_index] 696 if msg.public_key_version in range(1, nkeys + 1): 697 # requested key exists, use for signing and rotate. 698 req_key = self.server.keys[msg.public_key_version - 1]['private_key'] 699 700 # Fill the policy data protobuf. 701 policy_data = dm.PolicyData() 702 policy_data.policy_type = msg.policy_type 703 policy_data.timestamp = int(time.time() * 1000) 704 policy_data.request_token = token_info['device_token'] 705 policy_data.policy_value = payload 706 policy_data.machine_name = token_info['machine_name'] 707 policy_data.valid_serial_number_missing = ( 708 token_info['machine_id'] in BAD_MACHINE_IDS) 709 policy_data.settings_entity_id = msg.settings_entity_id 710 policy_data.service_account_identity = policy.get( 711 'service_account_identity', 712 'policy_testserver.py-service_account_identity') 713 invalidation_source = policy.get('invalidation_source') 714 if invalidation_source is not None: 715 policy_data.invalidation_source = invalidation_source 716 # Since invalidation_name is type bytes in the proto, the Unicode name 717 # provided needs to be encoded as ASCII to set the correct byte pattern. 718 invalidation_name = policy.get('invalidation_name') 719 if invalidation_name is not None: 720 policy_data.invalidation_name = invalidation_name.encode('ascii') 721 722 if signing_key: 723 policy_data.public_key_version = current_key_index + 1 724 if msg.policy_type == 'google/chromeos/publicaccount': 725 policy_data.username = msg.settings_entity_id 726 else: 727 # For regular user/device policy, there is no way for the testserver to 728 # know the user name belonging to the GAIA auth token we received (short 729 # of actually talking to GAIA). To address this, we read the username from 730 # the policy configuration dictionary, or use a default. 731 policy_data.username = policy.get('policy_user', 'user@example.com') 732 policy_data.device_id = token_info['device_id'] 733 signed_data = policy_data.SerializeToString() 734 735 response.policy_data = signed_data 736 if signing_key: 737 response.policy_data_signature = ( 738 signing_key['private_key'].hashAndSign(signed_data).tostring()) 739 if msg.public_key_version != current_key_index + 1: 740 response.new_public_key = signing_key['public_key'] 741 742 # Set the verification signature appropriate for the policy domain. 743 # TODO(atwilson): Use the enrollment domain for public accounts when 744 # we add key validation for ChromeOS (http://crbug.com/328038). 745 if 'signatures' in signing_key: 746 verification_sig = self.GetSignatureForDomain( 747 signing_key['signatures'], policy_data.username) 748 749 if verification_sig: 750 assert len(verification_sig) == 256, \ 751 'bad signature size: %d' % len(verification_sig) 752 response.new_public_key_verification_signature = verification_sig 753 754 if req_key: 755 response.new_public_key_signature = ( 756 req_key.hashAndSign(response.new_public_key).tostring()) 757 758 return (200, response.SerializeToString()) 759 760 def GetSignatureForDomain(self, signatures, username): 761 parsed_username = username.split("@", 1) 762 if len(parsed_username) != 2: 763 logging.error('Could not extract domain from username: %s' % username) 764 return None 765 domain = parsed_username[1] 766 767 # Lookup the domain's signature in the passed dictionary. If none is found, 768 # fallback to a wildcard signature. 769 if domain in signatures: 770 return signatures[domain] 771 if '*' in signatures: 772 return signatures['*'] 773 774 # No key matching this domain. 775 logging.error('No verification signature matching domain: %s' % domain) 776 return None 777 778 def CheckToken(self): 779 """Helper for checking whether the client supplied a valid DM token. 780 781 Extracts the token from the request and passed to the server in order to 782 look up the client. 783 784 Returns: 785 A pair of token information record and error response. If the first 786 element is None, then the second contains an error code to send back to 787 the client. Otherwise the first element is the same structure that is 788 returned by LookupToken(). 789 """ 790 error = 500 791 dmtoken = None 792 request_device_id = self.GetUniqueParam('deviceid') 793 match = re.match('GoogleDMToken token=(\\w+)', 794 self.headers.getheader('Authorization', '')) 795 if match: 796 dmtoken = match.group(1) 797 if not dmtoken: 798 error = 401 799 else: 800 token_info = self.server.LookupToken(dmtoken) 801 if (not token_info or 802 not request_device_id or 803 token_info['device_id'] != request_device_id): 804 error = 410 805 else: 806 return (token_info, None) 807 808 logging.debug('Token check failed with error %d' % error) 809 810 return (None, (error, 'Server error %d' % error)) 811 812 def DumpMessage(self, label, msg): 813 """Helper for logging an ASCII dump of a protobuf message.""" 814 logging.debug('%s\n%s' % (label, str(msg))) 815 816 817class PolicyTestServer(testserver_base.BrokenPipeHandlerMixIn, 818 testserver_base.StoppableHTTPServer): 819 """Handles requests and keeps global service state.""" 820 821 def __init__(self, server_address, data_dir, policy_path, client_state_file, 822 private_key_paths, server_base_url): 823 """Initializes the server. 824 825 Args: 826 server_address: Server host and port. 827 policy_path: Names the file to read JSON-formatted policy from. 828 private_key_paths: List of paths to read private keys from. 829 """ 830 testserver_base.StoppableHTTPServer.__init__(self, server_address, 831 PolicyRequestHandler) 832 self._registered_tokens = {} 833 self.data_dir = data_dir 834 self.policy_path = policy_path 835 self.client_state_file = client_state_file 836 self.server_base_url = server_base_url 837 838 self.keys = [] 839 if private_key_paths: 840 # Load specified keys from the filesystem. 841 for key_path in private_key_paths: 842 try: 843 key_str = open(key_path).read() 844 except IOError: 845 print 'Failed to load private key from %s' % key_path 846 continue 847 try: 848 key = tlslite.api.parsePEMKey(key_str, private=True) 849 except SyntaxError: 850 key = tlslite.utils.Python_RSAKey.Python_RSAKey._parsePKCS8( 851 tlslite.utils.cryptomath.stringToBytes(key_str)) 852 853 assert key is not None 854 key_info = { 'private_key' : key } 855 856 # Now try to read in a signature, if one exists. 857 try: 858 key_sig = open(key_path + '.sig').read() 859 # Create a dictionary with the wildcard domain + signature 860 key_info['signatures'] = {'*': key_sig} 861 except IOError: 862 print 'Failed to read validation signature from %s.sig' % key_path 863 self.keys.append(key_info) 864 else: 865 # Use the canned private keys if none were passed from the command line. 866 for signing_key in SIGNING_KEYS: 867 decoded_key = base64.b64decode(signing_key['key']); 868 key = tlslite.utils.Python_RSAKey.Python_RSAKey._parsePKCS8( 869 tlslite.utils.cryptomath.stringToBytes(decoded_key)) 870 assert key is not None 871 # Grab the signature dictionary for this key and decode all of the 872 # signatures. 873 signature_dict = signing_key['signatures'] 874 decoded_signatures = {} 875 for domain in signature_dict: 876 decoded_signatures[domain] = base64.b64decode(signature_dict[domain]) 877 self.keys.append({'private_key': key, 878 'signatures': decoded_signatures}) 879 880 # Derive the public keys from the private keys. 881 for entry in self.keys: 882 key = entry['private_key'] 883 884 algorithm = asn1der.Sequence( 885 [ asn1der.Data(asn1der.OBJECT_IDENTIFIER, PKCS1_RSA_OID), 886 asn1der.Data(asn1der.NULL, '') ]) 887 rsa_pubkey = asn1der.Sequence([ asn1der.Integer(key.n), 888 asn1der.Integer(key.e) ]) 889 pubkey = asn1der.Sequence([ algorithm, asn1der.Bitstring(rsa_pubkey) ]) 890 entry['public_key'] = pubkey 891 892 # Load client state. 893 if self.client_state_file is not None: 894 try: 895 file_contents = open(self.client_state_file).read() 896 self._registered_tokens = json.loads(file_contents, strict=False) 897 except IOError: 898 pass 899 900 def GetPolicies(self): 901 """Returns the policies to be used, reloaded form the backend file every 902 time this is called. 903 """ 904 policy = {} 905 if json is None: 906 print 'No JSON module, cannot parse policy information' 907 else : 908 try: 909 policy = json.loads(open(self.policy_path).read(), strict=False) 910 except IOError: 911 print 'Failed to load policy from %s' % self.policy_path 912 return policy 913 914 def RegisterDevice(self, device_id, machine_id, type): 915 """Registers a device or user and generates a DM token for it. 916 917 Args: 918 device_id: The device identifier provided by the client. 919 920 Returns: 921 The newly generated device token for the device. 922 """ 923 dmtoken_chars = [] 924 while len(dmtoken_chars) < 32: 925 dmtoken_chars.append(random.choice('0123456789abcdef')) 926 dmtoken = ''.join(dmtoken_chars) 927 allowed_policy_types = { 928 dm.DeviceRegisterRequest.BROWSER: [ 929 'google/chrome/user', 930 'google/chrome/extension' 931 ], 932 dm.DeviceRegisterRequest.USER: [ 933 'google/chromeos/user', 934 'google/chrome/extension' 935 ], 936 dm.DeviceRegisterRequest.DEVICE: [ 937 'google/chromeos/device', 938 'google/chromeos/publicaccount' 939 ], 940 dm.DeviceRegisterRequest.ANDROID_BROWSER: [ 941 'google/android/user' 942 ], 943 dm.DeviceRegisterRequest.IOS_BROWSER: [ 944 'google/ios/user' 945 ], 946 dm.DeviceRegisterRequest.TT: ['google/chromeos/user', 947 'google/chrome/user'], 948 } 949 if machine_id in KIOSK_MACHINE_IDS: 950 enrollment_mode = dm.DeviceRegisterResponse.RETAIL 951 else: 952 enrollment_mode = dm.DeviceRegisterResponse.ENTERPRISE 953 self._registered_tokens[dmtoken] = { 954 'device_id': device_id, 955 'device_token': dmtoken, 956 'allowed_policy_types': allowed_policy_types[type], 957 'machine_name': 'chromeos-' + machine_id, 958 'machine_id': machine_id, 959 'enrollment_mode': enrollment_mode, 960 } 961 self.WriteClientState() 962 return self._registered_tokens[dmtoken] 963 964 def UpdateMachineId(self, dmtoken, machine_id): 965 """Updates the machine identifier for a registered device. 966 967 Args: 968 dmtoken: The device management token provided by the client. 969 machine_id: Updated hardware identifier value. 970 """ 971 if dmtoken in self._registered_tokens: 972 self._registered_tokens[dmtoken]['machine_id'] = machine_id 973 self.WriteClientState() 974 975 def UpdateStateKeys(self, dmtoken, state_keys): 976 """Updates the state keys for a given client. 977 978 Args: 979 dmtoken: The device management token provided by the client. 980 state_keys: The state keys to set. 981 """ 982 if dmtoken in self._registered_tokens: 983 self._registered_tokens[dmtoken]['state_keys'] = map( 984 lambda key : key.encode('hex'), state_keys) 985 self.WriteClientState() 986 987 def LookupToken(self, dmtoken): 988 """Looks up a device or a user by DM token. 989 990 Args: 991 dmtoken: The device management token provided by the client. 992 993 Returns: 994 A dictionary with information about a device or user that is registered by 995 dmtoken, or None if the token is not found. 996 """ 997 return self._registered_tokens.get(dmtoken, None) 998 999 def LookupByStateKey(self, state_key): 1000 """Looks up a device or a user by a state key. 1001 1002 Args: 1003 state_key: The state key provided by the client. 1004 1005 Returns: 1006 A dictionary with information about a device or user or None if there is 1007 no matching record. 1008 """ 1009 for client in self._registered_tokens.values(): 1010 if state_key.encode('hex') in client.get('state_keys', []): 1011 return client 1012 1013 return None 1014 1015 def GetMatchingStateKeyHashes(self, modulus, remainder): 1016 """Returns all clients registered with the server. 1017 1018 Returns: 1019 The list of registered clients. 1020 """ 1021 state_keys = sum([ c.get('state_keys', []) 1022 for c in self._registered_tokens.values() ], []) 1023 hashed_keys = map(lambda key: hashlib.sha256(key.decode('hex')).digest(), 1024 set(state_keys)) 1025 return filter( 1026 lambda hash : int(hash.encode('hex'), 16) % modulus == remainder, 1027 hashed_keys) 1028 1029 def UnregisterDevice(self, dmtoken): 1030 """Unregisters a device identified by the given DM token. 1031 1032 Args: 1033 dmtoken: The device management token provided by the client. 1034 """ 1035 if dmtoken in self._registered_tokens.keys(): 1036 del self._registered_tokens[dmtoken] 1037 self.WriteClientState() 1038 1039 def WriteClientState(self): 1040 """Writes the client state back to the file.""" 1041 if self.client_state_file is not None: 1042 json_data = json.dumps(self._registered_tokens) 1043 open(self.client_state_file, 'w').write(json_data) 1044 1045 def GetBaseFilename(self, policy_selector): 1046 """Returns the base filename for the given policy_selector. 1047 1048 Args: 1049 policy_selector: the policy type and settings entity id, joined by '/'. 1050 1051 Returns: 1052 The filename corresponding to the policy_selector, without a file 1053 extension. 1054 """ 1055 sanitized_policy_selector = re.sub('[^A-Za-z0-9.@-]', '_', policy_selector) 1056 return os.path.join(self.data_dir or '', 1057 'policy_%s' % sanitized_policy_selector) 1058 1059 def ReadPolicyFromDataDir(self, policy_selector, proto_message): 1060 """Tries to read policy payload from a file in the data directory. 1061 1062 First checks for a binary rendition of the policy protobuf in 1063 <data_dir>/policy_<sanitized_policy_selector>.bin. If that exists, returns 1064 it. If that file doesn't exist, tries 1065 <data_dir>/policy_<sanitized_policy_selector>.txt and decodes that as a 1066 protobuf using proto_message. If that fails as well, returns None. 1067 1068 Args: 1069 policy_selector: Selects which policy to read. 1070 proto_message: Optional protobuf message object used for decoding the 1071 proto text format. 1072 1073 Returns: 1074 The binary payload message, or None if not found. 1075 """ 1076 base_filename = self.GetBaseFilename(policy_selector) 1077 1078 # Try the binary payload file first. 1079 try: 1080 return open(base_filename + '.bin').read() 1081 except IOError: 1082 pass 1083 1084 # If that fails, try the text version instead. 1085 if proto_message is None: 1086 return None 1087 1088 try: 1089 text = open(base_filename + '.txt').read() 1090 google.protobuf.text_format.Merge(text, proto_message) 1091 return proto_message.SerializeToString() 1092 except IOError: 1093 return None 1094 except google.protobuf.text_format.ParseError: 1095 return None 1096 1097 def ReadPolicyDataFromDataDir(self, policy_selector): 1098 """Returns the external policy data for |policy_selector| if found. 1099 1100 Args: 1101 policy_selector: Selects which policy to read. 1102 1103 Returns: 1104 The data for the corresponding policy type and entity id, if found. 1105 """ 1106 base_filename = self.GetBaseFilename(policy_selector) 1107 try: 1108 return open(base_filename + '.data').read() 1109 except IOError: 1110 return None 1111 1112 def GetBaseURL(self): 1113 """Returns the server base URL. 1114 1115 Respects the |server_base_url| configuration parameter, if present. Falls 1116 back to construct the URL from the server hostname and port otherwise. 1117 1118 Returns: 1119 The URL to use for constructing URLs that get returned to clients. 1120 """ 1121 base_url = self.server_base_url 1122 if base_url is None: 1123 base_url = 'http://%s:%s' % self.server_address[:2] 1124 1125 return base_url 1126 1127 1128class PolicyServerRunner(testserver_base.TestServerRunner): 1129 1130 def __init__(self): 1131 super(PolicyServerRunner, self).__init__() 1132 1133 def create_server(self, server_data): 1134 data_dir = self.options.data_dir or '' 1135 config_file = (self.options.config_file or 1136 os.path.join(data_dir, 'device_management')) 1137 server = PolicyTestServer((self.options.host, self.options.port), 1138 data_dir, config_file, 1139 self.options.client_state_file, 1140 self.options.policy_keys, 1141 self.options.server_base_url) 1142 server_data['port'] = server.server_port 1143 return server 1144 1145 def add_options(self): 1146 testserver_base.TestServerRunner.add_options(self) 1147 self.option_parser.add_option('--client-state', dest='client_state_file', 1148 help='File that client state should be ' 1149 'persisted to. This allows the server to be ' 1150 'seeded by a list of pre-registered clients ' 1151 'and restarts without abandoning registered ' 1152 'clients.') 1153 self.option_parser.add_option('--policy-key', action='append', 1154 dest='policy_keys', 1155 help='Specify a path to a PEM-encoded ' 1156 'private key to use for policy signing. May ' 1157 'be specified multiple times in order to ' 1158 'load multiple keys into the server. If the ' 1159 'server has multiple keys, it will rotate ' 1160 'through them in at each request in a ' 1161 'round-robin fashion. The server will ' 1162 'use a canned key if none is specified ' 1163 'on the command line. The test server will ' 1164 'also look for a verification signature file ' 1165 'in the same location: <filename>.sig and if ' 1166 'present will add the signature to the ' 1167 'policy blob as appropriate via the ' 1168 'new_public_key_verification_signature ' 1169 'field.') 1170 self.option_parser.add_option('--log-level', dest='log_level', 1171 default='WARN', 1172 help='Log level threshold to use.') 1173 self.option_parser.add_option('--config-file', dest='config_file', 1174 help='Specify a configuration file to use ' 1175 'instead of the default ' 1176 '<data_dir>/device_management') 1177 self.option_parser.add_option('--server-base-url', dest='server_base_url', 1178 help='The server base URL to use when ' 1179 'constructing URLs to return to the client.') 1180 1181 def run_server(self): 1182 logger = logging.getLogger() 1183 logger.setLevel(getattr(logging, str(self.options.log_level).upper())) 1184 if (self.options.log_to_console): 1185 logger.addHandler(logging.StreamHandler()) 1186 if (self.options.log_file): 1187 logger.addHandler(logging.FileHandler(self.options.log_file)) 1188 1189 testserver_base.TestServerRunner.run_server(self) 1190 1191 1192if __name__ == '__main__': 1193 sys.exit(PolicyServerRunner().main()) 1194