policy_testserver.py revision a1401311d1ab56c4ed0a474bd38c108f75cb0cd9
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 keys = self.server.GetMatchingStateKeys(msg.modulus, msg.remainder) 492 auto_enrollment_response.hash.extend( 493 map(lambda key : hashlib.sha256(key.decode('hex')).digest(), keys)) 494 elif msg.modulus == 2: 495 auto_enrollment_response.expected_modulus = 4 496 elif msg.modulus == 4: 497 auto_enrollment_response.expected_modulus = 2 498 elif msg.modulus == 8: 499 return (400, 'Server error') 500 elif msg.modulus == 16: 501 auto_enrollment_response.expected_modulus = 16 502 elif msg.modulus == 32: 503 auto_enrollment_response.expected_modulus = 1 504 505 response = dm.DeviceManagementResponse() 506 response.auto_enrollment_response.CopyFrom(auto_enrollment_response) 507 return (200, response) 508 509 def ProcessDeviceStateRetrievalRequest(self, msg): 510 """Handles a device state retrieval request. 511 512 Response data is taken from server configuration. 513 514 Returns: 515 A tuple of HTTP status code and response data to send to the client. 516 """ 517 device_state_retrieval_response = dm.DeviceStateRetrievalResponse() 518 519 client = self.server.LookupByStateKey(msg.server_backed_state_key) 520 if client is not None: 521 state = self.server.GetPolicies().get('device_state', {}) 522 FIELDS = [ 523 'management_domain', 524 'restore_mode', 525 ] 526 for field in FIELDS: 527 if field in state: 528 setattr(device_state_retrieval_response, field, state[field]) 529 530 response = dm.DeviceManagementResponse() 531 response.device_state_retrieval_response.CopyFrom( 532 device_state_retrieval_response) 533 return (200, response) 534 535 def SetProtobufMessageField(self, group_message, field, field_value): 536 '''Sets a field in a protobuf message. 537 538 Args: 539 group_message: The protobuf message. 540 field: The field of the message to set, it should be a member of 541 group_message.DESCRIPTOR.fields. 542 field_value: The value to set. 543 ''' 544 if field.label == field.LABEL_REPEATED: 545 assert type(field_value) == list 546 entries = group_message.__getattribute__(field.name) 547 if field.message_type is None: 548 for list_item in field_value: 549 entries.append(list_item) 550 else: 551 # This field is itself a protobuf. 552 sub_type = field.message_type 553 for sub_value in field_value: 554 assert type(sub_value) == dict 555 # Add a new sub-protobuf per list entry. 556 sub_message = entries.add() 557 # Now iterate over its fields and recursively add them. 558 for sub_field in sub_message.DESCRIPTOR.fields: 559 if sub_field.name in sub_value: 560 value = sub_value[sub_field.name] 561 self.SetProtobufMessageField(sub_message, sub_field, value) 562 return 563 elif field.type == field.TYPE_BOOL: 564 assert type(field_value) == bool 565 elif field.type == field.TYPE_STRING: 566 assert type(field_value) == str or type(field_value) == unicode 567 elif field.type == field.TYPE_INT64: 568 assert type(field_value) == int 569 elif (field.type == field.TYPE_MESSAGE and 570 field.message_type.name == 'StringList'): 571 assert type(field_value) == list 572 entries = group_message.__getattribute__(field.name).entries 573 for list_item in field_value: 574 entries.append(list_item) 575 return 576 else: 577 raise Exception('Unknown field type %s' % field.type) 578 group_message.__setattr__(field.name, field_value) 579 580 def GatherDevicePolicySettings(self, settings, policies): 581 '''Copies all the policies from a dictionary into a protobuf of type 582 CloudDeviceSettingsProto. 583 584 Args: 585 settings: The destination ChromeDeviceSettingsProto protobuf. 586 policies: The source dictionary containing policies in JSON format. 587 ''' 588 for group in settings.DESCRIPTOR.fields: 589 # Create protobuf message for group. 590 group_message = eval('dp.' + group.message_type.name + '()') 591 # Indicates if at least one field was set in |group_message|. 592 got_fields = False 593 # Iterate over fields of the message and feed them from the 594 # policy config file. 595 for field in group_message.DESCRIPTOR.fields: 596 field_value = None 597 if field.name in policies: 598 got_fields = True 599 field_value = policies[field.name] 600 self.SetProtobufMessageField(group_message, field, field_value) 601 if got_fields: 602 settings.__getattribute__(group.name).CopyFrom(group_message) 603 604 def GatherUserPolicySettings(self, settings, policies): 605 '''Copies all the policies from a dictionary into a protobuf of type 606 CloudPolicySettings. 607 608 Args: 609 settings: The destination: a CloudPolicySettings protobuf. 610 policies: The source: a dictionary containing policies under keys 611 'recommended' and 'mandatory'. 612 ''' 613 for field in settings.DESCRIPTOR.fields: 614 # |field| is the entry for a specific policy in the top-level 615 # CloudPolicySettings proto. 616 617 # Look for this policy's value in the mandatory or recommended dicts. 618 if field.name in policies.get('mandatory', {}): 619 mode = cp.PolicyOptions.MANDATORY 620 value = policies['mandatory'][field.name] 621 elif field.name in policies.get('recommended', {}): 622 mode = cp.PolicyOptions.RECOMMENDED 623 value = policies['recommended'][field.name] 624 else: 625 continue 626 627 # Create protobuf message for this policy. 628 policy_message = eval('cp.' + field.message_type.name + '()') 629 policy_message.policy_options.mode = mode 630 field_descriptor = policy_message.DESCRIPTOR.fields_by_name['value'] 631 self.SetProtobufMessageField(policy_message, field_descriptor, value) 632 settings.__getattribute__(field.name).CopyFrom(policy_message) 633 634 def ProcessCloudPolicy(self, msg, token_info, response): 635 """Handles a cloud policy request. (New protocol for policy requests.) 636 637 Encodes the policy into protobuf representation, signs it and constructs 638 the response. 639 640 Args: 641 msg: The CloudPolicyRequest message received from the client. 642 token_info: the token extracted from the request. 643 response: A PolicyFetchResponse message that should be filled with the 644 response data. 645 """ 646 647 if msg.machine_id: 648 self.server.UpdateMachineId(token_info['device_token'], msg.machine_id) 649 650 # Response is only given if the scope is specified in the config file. 651 # Normally 'google/chromeos/device', 'google/chromeos/user' and 652 # 'google/chromeos/publicaccount' should be accepted. 653 policy = self.server.GetPolicies() 654 policy_value = '' 655 policy_key = msg.policy_type 656 if msg.settings_entity_id: 657 policy_key += '/' + msg.settings_entity_id 658 if msg.policy_type in token_info['allowed_policy_types']: 659 if msg.policy_type in ('google/android/user', 660 'google/chromeos/publicaccount', 661 'google/chromeos/user', 662 'google/chrome/user', 663 'google/ios/user'): 664 settings = cp.CloudPolicySettings() 665 payload = self.server.ReadPolicyFromDataDir(policy_key, settings) 666 if payload is None: 667 self.GatherUserPolicySettings(settings, policy.get(policy_key, {})) 668 payload = settings.SerializeToString() 669 elif dp is not None and msg.policy_type == 'google/chromeos/device': 670 settings = dp.ChromeDeviceSettingsProto() 671 payload = self.server.ReadPolicyFromDataDir(policy_key, settings) 672 if payload is None: 673 self.GatherDevicePolicySettings(settings, policy.get(policy_key, {})) 674 payload = settings.SerializeToString() 675 elif msg.policy_type == 'google/chrome/extension': 676 settings = ep.ExternalPolicyData() 677 payload = self.server.ReadPolicyFromDataDir(policy_key, settings) 678 if payload is None: 679 payload = self.CreatePolicyForExternalPolicyData(policy_key) 680 else: 681 response.error_code = 400 682 response.error_message = 'Invalid policy type' 683 return 684 else: 685 response.error_code = 400 686 response.error_message = 'Request not allowed for the token used' 687 return 688 689 # Sign with 'current_key_index', defaulting to key 0. 690 signing_key = None 691 req_key = None 692 current_key_index = policy.get('current_key_index', 0) 693 nkeys = len(self.server.keys) 694 if (msg.signature_type == dm.PolicyFetchRequest.SHA1_RSA and 695 current_key_index in range(nkeys)): 696 signing_key = self.server.keys[current_key_index] 697 if msg.public_key_version in range(1, nkeys + 1): 698 # requested key exists, use for signing and rotate. 699 req_key = self.server.keys[msg.public_key_version - 1]['private_key'] 700 701 # Fill the policy data protobuf. 702 policy_data = dm.PolicyData() 703 policy_data.policy_type = msg.policy_type 704 policy_data.timestamp = int(time.time() * 1000) 705 policy_data.request_token = token_info['device_token'] 706 policy_data.policy_value = payload 707 policy_data.machine_name = token_info['machine_name'] 708 policy_data.valid_serial_number_missing = ( 709 token_info['machine_id'] in BAD_MACHINE_IDS) 710 policy_data.settings_entity_id = msg.settings_entity_id 711 policy_data.service_account_identity = policy.get( 712 'service_account_identity', 713 'policy_testserver.py-service_account_identity') 714 invalidation_source = policy.get('invalidation_source') 715 if invalidation_source is not None: 716 policy_data.invalidation_source = invalidation_source 717 # Since invalidation_name is type bytes in the proto, the Unicode name 718 # provided needs to be encoded as ASCII to set the correct byte pattern. 719 invalidation_name = policy.get('invalidation_name') 720 if invalidation_name is not None: 721 policy_data.invalidation_name = invalidation_name.encode('ascii') 722 723 if signing_key: 724 policy_data.public_key_version = current_key_index + 1 725 if msg.policy_type == 'google/chromeos/publicaccount': 726 policy_data.username = msg.settings_entity_id 727 else: 728 # For regular user/device policy, there is no way for the testserver to 729 # know the user name belonging to the GAIA auth token we received (short 730 # of actually talking to GAIA). To address this, we read the username from 731 # the policy configuration dictionary, or use a default. 732 policy_data.username = policy.get('policy_user', 'user@example.com') 733 policy_data.device_id = token_info['device_id'] 734 signed_data = policy_data.SerializeToString() 735 736 response.policy_data = signed_data 737 if signing_key: 738 response.policy_data_signature = ( 739 signing_key['private_key'].hashAndSign(signed_data).tostring()) 740 if msg.public_key_version != current_key_index + 1: 741 response.new_public_key = signing_key['public_key'] 742 743 # Set the verification signature appropriate for the policy domain. 744 # TODO(atwilson): Use the enrollment domain for public accounts when 745 # we add key validation for ChromeOS (http://crbug.com/328038). 746 if 'signatures' in signing_key: 747 verification_sig = self.GetSignatureForDomain( 748 signing_key['signatures'], policy_data.username) 749 750 if verification_sig: 751 assert len(verification_sig) == 256, \ 752 'bad signature size: %d' % len(verification_sig) 753 response.new_public_key_verification_signature = verification_sig 754 755 if req_key: 756 response.new_public_key_signature = ( 757 req_key.hashAndSign(response.new_public_key).tostring()) 758 759 return (200, response.SerializeToString()) 760 761 def GetSignatureForDomain(self, signatures, username): 762 parsed_username = username.split("@", 1) 763 if len(parsed_username) != 2: 764 logging.error('Could not extract domain from username: %s' % username) 765 return None 766 domain = parsed_username[1] 767 768 # Lookup the domain's signature in the passed dictionary. If none is found, 769 # fallback to a wildcard signature. 770 if domain in signatures: 771 return signatures[domain] 772 if '*' in signatures: 773 return signatures['*'] 774 775 # No key matching this domain. 776 logging.error('No verification signature matching domain: %s' % domain) 777 return None 778 779 def CheckToken(self): 780 """Helper for checking whether the client supplied a valid DM token. 781 782 Extracts the token from the request and passed to the server in order to 783 look up the client. 784 785 Returns: 786 A pair of token information record and error response. If the first 787 element is None, then the second contains an error code to send back to 788 the client. Otherwise the first element is the same structure that is 789 returned by LookupToken(). 790 """ 791 error = 500 792 dmtoken = None 793 request_device_id = self.GetUniqueParam('deviceid') 794 match = re.match('GoogleDMToken token=(\\w+)', 795 self.headers.getheader('Authorization', '')) 796 if match: 797 dmtoken = match.group(1) 798 if not dmtoken: 799 error = 401 800 else: 801 token_info = self.server.LookupToken(dmtoken) 802 if (not token_info or 803 not request_device_id or 804 token_info['device_id'] != request_device_id): 805 error = 410 806 else: 807 return (token_info, None) 808 809 logging.debug('Token check failed with error %d' % error) 810 811 return (None, (error, 'Server error %d' % error)) 812 813 def DumpMessage(self, label, msg): 814 """Helper for logging an ASCII dump of a protobuf message.""" 815 logging.debug('%s\n%s' % (label, str(msg))) 816 817 818class PolicyTestServer(testserver_base.BrokenPipeHandlerMixIn, 819 testserver_base.StoppableHTTPServer): 820 """Handles requests and keeps global service state.""" 821 822 def __init__(self, server_address, data_dir, policy_path, client_state_file, 823 private_key_paths, server_base_url): 824 """Initializes the server. 825 826 Args: 827 server_address: Server host and port. 828 policy_path: Names the file to read JSON-formatted policy from. 829 private_key_paths: List of paths to read private keys from. 830 """ 831 testserver_base.StoppableHTTPServer.__init__(self, server_address, 832 PolicyRequestHandler) 833 self._registered_tokens = {} 834 self.data_dir = data_dir 835 self.policy_path = policy_path 836 self.client_state_file = client_state_file 837 self.server_base_url = server_base_url 838 839 self.keys = [] 840 if private_key_paths: 841 # Load specified keys from the filesystem. 842 for key_path in private_key_paths: 843 try: 844 key_str = open(key_path).read() 845 except IOError: 846 print 'Failed to load private key from %s' % key_path 847 continue 848 try: 849 key = tlslite.api.parsePEMKey(key_str, private=True) 850 except SyntaxError: 851 key = tlslite.utils.Python_RSAKey.Python_RSAKey._parsePKCS8( 852 tlslite.utils.cryptomath.stringToBytes(key_str)) 853 854 assert key is not None 855 key_info = { 'private_key' : key } 856 857 # Now try to read in a signature, if one exists. 858 try: 859 key_sig = open(key_path + '.sig').read() 860 # Create a dictionary with the wildcard domain + signature 861 key_info['signatures'] = {'*': key_sig} 862 except IOError: 863 print 'Failed to read validation signature from %s.sig' % key_path 864 self.keys.append(key_info) 865 else: 866 # Use the canned private keys if none were passed from the command line. 867 for signing_key in SIGNING_KEYS: 868 decoded_key = base64.b64decode(signing_key['key']); 869 key = tlslite.utils.Python_RSAKey.Python_RSAKey._parsePKCS8( 870 tlslite.utils.cryptomath.stringToBytes(decoded_key)) 871 assert key is not None 872 # Grab the signature dictionary for this key and decode all of the 873 # signatures. 874 signature_dict = signing_key['signatures'] 875 decoded_signatures = {} 876 for domain in signature_dict: 877 decoded_signatures[domain] = base64.b64decode(signature_dict[domain]) 878 self.keys.append({'private_key': key, 879 'signatures': decoded_signatures}) 880 881 # Derive the public keys from the private keys. 882 for entry in self.keys: 883 key = entry['private_key'] 884 885 algorithm = asn1der.Sequence( 886 [ asn1der.Data(asn1der.OBJECT_IDENTIFIER, PKCS1_RSA_OID), 887 asn1der.Data(asn1der.NULL, '') ]) 888 rsa_pubkey = asn1der.Sequence([ asn1der.Integer(key.n), 889 asn1der.Integer(key.e) ]) 890 pubkey = asn1der.Sequence([ algorithm, asn1der.Bitstring(rsa_pubkey) ]) 891 entry['public_key'] = pubkey 892 893 # Load client state. 894 if self.client_state_file is not None: 895 try: 896 file_contents = open(self.client_state_file).read() 897 self._registered_tokens = json.loads(file_contents, strict=False) 898 except IOError: 899 pass 900 901 def GetPolicies(self): 902 """Returns the policies to be used, reloaded form the backend file every 903 time this is called. 904 """ 905 policy = {} 906 if json is None: 907 print 'No JSON module, cannot parse policy information' 908 else : 909 try: 910 policy = json.loads(open(self.policy_path).read(), strict=False) 911 except IOError: 912 print 'Failed to load policy from %s' % self.policy_path 913 return policy 914 915 def RegisterDevice(self, device_id, machine_id, type): 916 """Registers a device or user and generates a DM token for it. 917 918 Args: 919 device_id: The device identifier provided by the client. 920 921 Returns: 922 The newly generated device token for the device. 923 """ 924 dmtoken_chars = [] 925 while len(dmtoken_chars) < 32: 926 dmtoken_chars.append(random.choice('0123456789abcdef')) 927 dmtoken = ''.join(dmtoken_chars) 928 allowed_policy_types = { 929 dm.DeviceRegisterRequest.BROWSER: [ 930 'google/chrome/user', 931 'google/chrome/extension' 932 ], 933 dm.DeviceRegisterRequest.USER: [ 934 'google/chromeos/user', 935 'google/chrome/extension' 936 ], 937 dm.DeviceRegisterRequest.DEVICE: [ 938 'google/chromeos/device', 939 'google/chromeos/publicaccount' 940 ], 941 dm.DeviceRegisterRequest.ANDROID_BROWSER: [ 942 'google/android/user' 943 ], 944 dm.DeviceRegisterRequest.IOS_BROWSER: [ 945 'google/ios/user' 946 ], 947 dm.DeviceRegisterRequest.TT: ['google/chromeos/user', 948 'google/chrome/user'], 949 } 950 if machine_id in KIOSK_MACHINE_IDS: 951 enrollment_mode = dm.DeviceRegisterResponse.RETAIL 952 else: 953 enrollment_mode = dm.DeviceRegisterResponse.ENTERPRISE 954 self._registered_tokens[dmtoken] = { 955 'device_id': device_id, 956 'device_token': dmtoken, 957 'allowed_policy_types': allowed_policy_types[type], 958 'machine_name': 'chromeos-' + machine_id, 959 'machine_id': machine_id, 960 'enrollment_mode': enrollment_mode, 961 } 962 self.WriteClientState() 963 return self._registered_tokens[dmtoken] 964 965 def UpdateMachineId(self, dmtoken, machine_id): 966 """Updates the machine identifier for a registered device. 967 968 Args: 969 dmtoken: The device management token provided by the client. 970 machine_id: Updated hardware identifier value. 971 """ 972 if dmtoken in self._registered_tokens: 973 self._registered_tokens[dmtoken]['machine_id'] = machine_id 974 self.WriteClientState() 975 976 def UpdateStateKeys(self, dmtoken, state_keys): 977 """Updates the state keys for a given client. 978 979 Args: 980 dmtoken: The device management token provided by the client. 981 state_keys: The state keys to set. 982 """ 983 if dmtoken in self._registered_tokens: 984 self._registered_tokens[dmtoken]['state_keys'] = map( 985 lambda key : key.encode('hex'), state_keys) 986 self.WriteClientState() 987 988 def LookupToken(self, dmtoken): 989 """Looks up a device or a user by DM token. 990 991 Args: 992 dmtoken: The device management token provided by the client. 993 994 Returns: 995 A dictionary with information about a device or user that is registered by 996 dmtoken, or None if the token is not found. 997 """ 998 return self._registered_tokens.get(dmtoken, None) 999 1000 def LookupByStateKey(self, state_key): 1001 """Looks up a device or a user by a state key. 1002 1003 Args: 1004 state_key: The state key provided by the client. 1005 1006 Returns: 1007 A dictionary with information about a device or user or None if there is 1008 no matching record. 1009 """ 1010 for client in self._registered_tokens.values(): 1011 if state_key.encode('hex') in client.get('state_keys', []): 1012 return client 1013 1014 return None 1015 1016 def GetMatchingStateKeys(self, modulus, remainder): 1017 """Returns all clients registered with the server. 1018 1019 Returns: 1020 The list of registered clients. 1021 """ 1022 state_keys = sum([ c.get('state_keys', []) 1023 for c in self._registered_tokens.values() ], []) 1024 return filter(lambda key : int(key, 16) & modulus == remainder, state_keys) 1025 1026 def UnregisterDevice(self, dmtoken): 1027 """Unregisters a device identified by the given DM token. 1028 1029 Args: 1030 dmtoken: The device management token provided by the client. 1031 """ 1032 if dmtoken in self._registered_tokens.keys(): 1033 del self._registered_tokens[dmtoken] 1034 self.WriteClientState() 1035 1036 def WriteClientState(self): 1037 """Writes the client state back to the file.""" 1038 if self.client_state_file is not None: 1039 json_data = json.dumps(self._registered_tokens) 1040 open(self.client_state_file, 'w').write(json_data) 1041 1042 def GetBaseFilename(self, policy_selector): 1043 """Returns the base filename for the given policy_selector. 1044 1045 Args: 1046 policy_selector: the policy type and settings entity id, joined by '/'. 1047 1048 Returns: 1049 The filename corresponding to the policy_selector, without a file 1050 extension. 1051 """ 1052 sanitized_policy_selector = re.sub('[^A-Za-z0-9.@-]', '_', policy_selector) 1053 return os.path.join(self.data_dir or '', 1054 'policy_%s' % sanitized_policy_selector) 1055 1056 def ReadPolicyFromDataDir(self, policy_selector, proto_message): 1057 """Tries to read policy payload from a file in the data directory. 1058 1059 First checks for a binary rendition of the policy protobuf in 1060 <data_dir>/policy_<sanitized_policy_selector>.bin. If that exists, returns 1061 it. If that file doesn't exist, tries 1062 <data_dir>/policy_<sanitized_policy_selector>.txt and decodes that as a 1063 protobuf using proto_message. If that fails as well, returns None. 1064 1065 Args: 1066 policy_selector: Selects which policy to read. 1067 proto_message: Optional protobuf message object used for decoding the 1068 proto text format. 1069 1070 Returns: 1071 The binary payload message, or None if not found. 1072 """ 1073 base_filename = self.GetBaseFilename(policy_selector) 1074 1075 # Try the binary payload file first. 1076 try: 1077 return open(base_filename + '.bin').read() 1078 except IOError: 1079 pass 1080 1081 # If that fails, try the text version instead. 1082 if proto_message is None: 1083 return None 1084 1085 try: 1086 text = open(base_filename + '.txt').read() 1087 google.protobuf.text_format.Merge(text, proto_message) 1088 return proto_message.SerializeToString() 1089 except IOError: 1090 return None 1091 except google.protobuf.text_format.ParseError: 1092 return None 1093 1094 def ReadPolicyDataFromDataDir(self, policy_selector): 1095 """Returns the external policy data for |policy_selector| if found. 1096 1097 Args: 1098 policy_selector: Selects which policy to read. 1099 1100 Returns: 1101 The data for the corresponding policy type and entity id, if found. 1102 """ 1103 base_filename = self.GetBaseFilename(policy_selector) 1104 try: 1105 return open(base_filename + '.data').read() 1106 except IOError: 1107 return None 1108 1109 def GetBaseURL(self): 1110 """Returns the server base URL. 1111 1112 Respects the |server_base_url| configuration parameter, if present. Falls 1113 back to construct the URL from the server hostname and port otherwise. 1114 1115 Returns: 1116 The URL to use for constructing URLs that get returned to clients. 1117 """ 1118 base_url = self.server_base_url 1119 if base_url is None: 1120 base_url = 'http://%s:%s' % self.server_address[:2] 1121 1122 return base_url 1123 1124 1125class PolicyServerRunner(testserver_base.TestServerRunner): 1126 1127 def __init__(self): 1128 super(PolicyServerRunner, self).__init__() 1129 1130 def create_server(self, server_data): 1131 data_dir = self.options.data_dir or '' 1132 config_file = (self.options.config_file or 1133 os.path.join(data_dir, 'device_management')) 1134 server = PolicyTestServer((self.options.host, self.options.port), 1135 data_dir, config_file, 1136 self.options.client_state_file, 1137 self.options.policy_keys, 1138 self.options.server_base_url) 1139 server_data['port'] = server.server_port 1140 return server 1141 1142 def add_options(self): 1143 testserver_base.TestServerRunner.add_options(self) 1144 self.option_parser.add_option('--client-state', dest='client_state_file', 1145 help='File that client state should be ' 1146 'persisted to. This allows the server to be ' 1147 'seeded by a list of pre-registered clients ' 1148 'and restarts without abandoning registered ' 1149 'clients.') 1150 self.option_parser.add_option('--policy-key', action='append', 1151 dest='policy_keys', 1152 help='Specify a path to a PEM-encoded ' 1153 'private key to use for policy signing. May ' 1154 'be specified multiple times in order to ' 1155 'load multiple keys into the server. If the ' 1156 'server has multiple keys, it will rotate ' 1157 'through them in at each request in a ' 1158 'round-robin fashion. The server will ' 1159 'use a canned key if none is specified ' 1160 'on the command line. The test server will ' 1161 'also look for a verification signature file ' 1162 'in the same location: <filename>.sig and if ' 1163 'present will add the signature to the ' 1164 'policy blob as appropriate via the ' 1165 'new_public_key_verification_signature ' 1166 'field.') 1167 self.option_parser.add_option('--log-level', dest='log_level', 1168 default='WARN', 1169 help='Log level threshold to use.') 1170 self.option_parser.add_option('--config-file', dest='config_file', 1171 help='Specify a configuration file to use ' 1172 'instead of the default ' 1173 '<data_dir>/device_management') 1174 self.option_parser.add_option('--server-base-url', dest='server_base_url', 1175 help='The server base URL to use when ' 1176 'constructing URLs to return to the client.') 1177 1178 def run_server(self): 1179 logger = logging.getLogger() 1180 logger.setLevel(getattr(logging, str(self.options.log_level).upper())) 1181 if (self.options.log_to_console): 1182 logger.addHandler(logging.StreamHandler()) 1183 if (self.options.log_file): 1184 logger.addHandler(logging.FileHandler(self.options.log_file)) 1185 1186 testserver_base.TestServerRunner.run_server(self) 1187 1188 1189if __name__ == '__main__': 1190 sys.exit(PolicyServerRunner().main()) 1191