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.
5 """A bare-bones test server for testing cloud policy support.
7 This implements a simple cloud policy test server that can be used to test
8 chrome's device management service client. The policy information is read from
9 the file named device_management in the server's data directory. It contains
10 enforced and recommended policies for the device and user scope, and a list
13 The format of the file is JSON. The root dictionary contains a list under the
14 key "managed_users". It contains auth tokens for which the server will claim
15 that the user is managed. The token string "*" indicates that all users are
16 claimed to be managed. Other keys in the root dictionary identify request
17 scopes. The user-request scope is described by a dictionary that holds two
18 sub-dictionaries: "mandatory" and "recommended". Both these hold the policy
19 definitions as key/value stores, their format is identical to what the Linux
20 implementation reads from /etc.
21 The device-scope holds the policy-definition directly as key/value stores in the
27 "google/chromeos/device" : {
28 "guest_mode_enabled" : false
30 "google/chromeos/user" : {
32 "HomepageLocation" : "http://www.chromium.org",
33 "IncognitoEnabled" : false
36 "JavascriptEnabled": false
39 "google/chromeos/publicaccount/user@example.com" : {
41 "HomepageLocation" : "http://www.chromium.org"
49 "current_key_index": 0,
50 "robot_api_auth_code": "fake_auth_code",
51 "invalidation_source": 1025,
52 "invalidation_name": "UENUPOL"
61 import google.protobuf.text_format
72 import tlslite.utils.cryptomath
75 # The name and availability of the json module varies in python versions.
77 import simplejson as json
85 import testserver_base
87 import device_management_backend_pb2 as dm
88 import cloud_policy_pb2 as cp
89 import chrome_extension_policy_pb2 as ep
91 # Device policy is only available on Chrome OS builds.
93 import chrome_device_policy_pb2 as dp
97 # ASN.1 object identifier for PKCS#1/RSA.
98 PKCS1_RSA_OID = '\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01'
100 # List of bad machine identifiers that trigger the |valid_serial_number_missing|
101 # flag to be set set in the policy fetch response.
102 BAD_MACHINE_IDS = [ '123490EN400015' ]
104 # List of machines that trigger the server to send kiosk enrollment response
105 # for the register request.
106 KIOSK_MACHINE_IDS = [ 'KIOSK' ]
108 # Dictionary containing base64-encoded policy signing keys plus per-domain
109 # signatures. Format is:
111 # 'key': <base64-encoded PKCS8-format private key>,
113 # <domain1>: <base64-encdoded SHA256 signature for key + domain1>
114 # <domain2>: <base64-encdoded SHA256 signature for key + domain2>
121 'MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA2c3KzcPqvnJ5HCk3OZkf1'
122 'LMO8Ht4dw4FO2U0EmKvpo0zznj4RwUdmKobH1AFWzwZP4CDY2M67MsukE/1Jnbx1QIDAQ'
123 'ABAkBkKcLZa/75hHVz4PR3tZaw34PATlfxEG6RiRIwXlf/FFlfGIZOSxdW/I1A3XRl0/9'
124 'nZMuctBSKBrcTRZQWfT/hAiEA9g8xbQbMO6BEH/XCRSsQbPlvj4c9wDtVEzeAzZ/ht9kC'
125 'IQDiml+/lXS1emqml711jJcYJNYJzdy1lL/ieKogR59oXQIhAK+Pl4xa1U2VxAWpq7r+R'
126 'vH55wdZT03hB4p2h4gvEzXBAiAkw9kvE0eZPiBZoRrrHIFTOH7FnnHlwBmV2+/2RsiVPQ'
127 'IhAKqx/4qisivvmoM/xbzUagfoxwsu1A/4mGjhBKiS0BCq',
130 'l+sT5mziei/GbmiP7VtRCCfwpZcg7uKbW2OlnK5B/TTELutjEIAMdHduNBwbO44qOn'
131 '/5c7YrtkXbBehaaDYFPGI6bGTbDmG9KRxhS+DaB7opgfCQWLi79Gn/jytKLZhRN/VS'
132 'y+PEbezqMi3d1/xDxlThwWZDNwnhv9ER/Nu/32ZTjzgtqonSn2CQtwXCIILm4FdV/1'
133 '/BdmZG+Ge4i4FTqYtInir5YFe611KXU/AveGhQGBIAXo4qYg1IqbVrvKBSU9dlI6Sl'
134 '9TJJLbJ3LGaXuljgFhyMAl3gcy7ftC9MohEmwa+sc7y2mOAgYQ5SSmyAtQwQgAkX9J'
136 'chromepolicytest.com':
137 'TzBiigZKwBdr6lyP6tUDsw+Q9wYO1Yepyxm0O4JZ4RID32L27sWzC1/hwC51fRcCvP'
138 'luEVIW6mH+BFODXMrteUFWfbbG7jgV+Wg+QdzMqgJjxhNKFXPTsZ7/286LAd1vBY/A'
139 'nGd8Wog6AhzfrgMbLNsH794GD0xIUwRvXUWFNP8pClj5VPgQnJrIA9aZwW8FNGbteA'
140 'HacFB0T/oqP5s7XT4Qvkj14RLmCgTwEM8Vcpqy5teJaF8yN17wniveddoOQGH6s0HC'
141 'ocprEccrH5fP/WVAPxCfx4vVYQY5q4CZ4K3f6dTC2FV4IDelM6dugEkvSS02YCzDaO'
144 'T0wXC5w3GXyovA09pyOLX7ui/NI603UfbZXYyTbHI7xtzCIaHVPH35Nx4zdqVrdsej'
145 'ErQ12yVLDDIJokY4Yl+/fj/zrkAPxThI+TNQ+jo0i+al05PuopfpzvCzIXiZBbkbyW'
146 '3XfedxXP3IPN2XU2/3vX+ZXUNG6pxeETem64kGezkjkUraqnHw3JVzwJYHhpMcwdLP'
147 'PYK6V23BbEHEVBtQZd/ledXacz7gOzm1zGni4e+vxA2roAdJWyhbjU0dTKNNUsZmMv'
148 'ryQH9Af1Jw+dqs0RAbhcJXm2i8EUWIgNv6aMn1Z2DzZwKKjXsKgcYSRo8pdYa8RZAo'
154 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmZhreV04M3knCi6wibr49'
155 'oDesHny1G33PKOX9ko8pcxAiu9ZqsKCj7wNW2PGqnLi81fddACwQtYn5xdhCtzB9wIDAQ'
156 'ABAkA0z8m0cy8N08xundspoFZWO71WJLgv/peSDBYGI0RzJR1l9Np355EukQUQwRs5XrL'
157 '3vRQZy2vDqeiR96epkAhRAiEAzJ4DVI8k3pAl7CGv5icqFkJ02viExIwehhIEXBcB6p0C'
158 'IQDAKmzpoRpBEZRQ9xrTvPOi+Ea8Jnd478BU7CI/LFfgowIgMfLIoVWoDGRnvXKju60Hy'
159 'xNB70oHLut9cADp64j6QMkCIDrgxN4QbmrhaAAmtiGKE1wrlgCwCIsVamiasSOKAqLhAi'
160 'EAo/ItVcFtQPod97qG71CY/O4JzOciuU6AMhprs181vfM=',
164 'cO0nQjRptkeefKDw5QpJSQDavHABxUvbR9Wvoa235OG9Whw1RFqq2ye6pKnI3ezW6/'
165 '7b4ANcpi5a7HV5uF8K7gWyYdxY8NHLeyrbwXxg5j6HAmHmkP1UZcf/dAnWqo7cW8g4'
166 'DIQOhC43KkveMYJ2HnelwdXt/7zqkbe8/3Yj4nhjAUeARx86Sb8Nzydwkrvqs5Jw/x'
167 '5LG+BODExrXXcGu/ubDlW4ivJFqfNUPQysqBXSMY2XCHPJDx3eECLGVVN/fFAWWgjM'
168 'HFObAriAt0b18cc9Nr0mAt4Qq1oDzWcAHCPHE+5dr8Uf46BUrMLJRNRKCY7rrsoIin'
170 'chromepolicytest.com':
171 'mr+9CCYvR0cTvPwlzkxqlpGYy55gY7cPiIkPAPoql51yHK1tkMTOSFru8Dy/nMt+0o'
172 '4z7WO60F1wnIBGkQxnTj/DsO6QpCYi7oHqtLmZ2jsLQFlMyvPGUtpJEFvRwjr/TNbh'
173 '6RqUtz1LQFuJQ848kBrx7nkte1L8SuPDExgx+Q3LtbNj4SuTdvMUBMvEERXiLuwfFL'
174 'BefGjtsqfWETQVlJTCW7xcqOLedIX8UYgEDBpDOZ23A3GzCShuBsIut5m87R5mODht'
175 'EUmKNDK1+OMc6SyDpf+r48Wph4Db1bVaKy8fcpSNJOwEgsrmH7/+owKPGcN7I5jYAF'
178 'o5MVSo4bRwIJ/aooGyXpRXsEsWPG8fNA2UTG8hgwnLYhNeJCCnLs/vW2vdp0URE8jn'
179 'qiG4N8KjbuiGw0rJtO1EygdLfpnMEtqYlFjrOie38sy92l/AwohXj6luYzMWL+FqDu'
180 'WQeXasjgyY4s9BOLQVDEnEj3pvqhrk/mXvMwUeXGpbxTNbWAd0C8BTZrGOwU/kIXxo'
181 'vAMGg8L+rQaDwBTEnMsMZcvlrIyqSg5v4BxCWuL3Yd2xvUqZEUWRp1aKetsHRnz5hw'
182 'H7WK7DzvKepDn06XjPG9lchi448U3HB3PRKtCzfO3nD9YXMKTuqRpKPF8PeK11CWh1'
188 class PolicyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
189 """Decodes and handles device management requests from clients.
191 The handler implements all the request parsing and protobuf message decoding
192 and encoding. It calls back into the server to lookup, register, and
196 def __init__(self, request, client_address, server):
197 """Initialize the handler.
200 request: The request data received from the client as a string.
201 client_address: The client address.
202 server: The TestServer object to use for (un)registering clients.
204 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request,
205 client_address, server)
207 def GetUniqueParam(self, name):
208 """Extracts a unique query parameter from the request.
211 name: Names the parameter to fetch.
213 The parameter value or None if the parameter doesn't exist or is not
216 if not hasattr(self, '_params'):
217 self._params = cgi.parse_qs(self.path[self.path.find('?') + 1:])
219 param_list = self._params.get(name, [])
220 if len(param_list) == 1:
225 """Handles GET requests.
227 Currently this is only used to serve external policy data."""
228 sep = self.path.find('?')
229 path = self.path if sep == -1 else self.path[:sep]
230 if path == '/externalpolicydata':
231 http_response, raw_reply = self.HandleExternalPolicyDataRequest()
234 raw_reply = 'Invalid path'
235 self.send_response(http_response)
237 self.wfile.write(raw_reply)
240 http_response, raw_reply = self.HandleRequest()
241 self.send_response(http_response)
242 if (http_response == 200):
243 self.send_header('Content-Type', 'application/x-protobuffer')
245 self.wfile.write(raw_reply)
247 def HandleExternalPolicyDataRequest(self):
248 """Handles a request to download policy data for a component."""
249 policy_key = self.GetUniqueParam('key')
251 return (400, 'Missing key parameter')
252 data = self.server.ReadPolicyDataFromDataDir(policy_key)
254 return (404, 'Policy not found for ' + policy_key)
257 def HandleRequest(self):
258 """Handles a request.
260 Parses the data supplied at construction time and returns a pair indicating
261 http status code and response data to be sent back to the client.
264 A tuple of HTTP status code and response data to send to the client.
266 rmsg = dm.DeviceManagementRequest()
267 length = int(self.headers.getheader('content-length'))
268 rmsg.ParseFromString(self.rfile.read(length))
270 logging.debug('gaia auth token -> ' +
271 self.headers.getheader('Authorization', ''))
272 logging.debug('oauth token -> ' + str(self.GetUniqueParam('oauth_token')))
273 logging.debug('deviceid -> ' + str(self.GetUniqueParam('deviceid')))
274 self.DumpMessage('Request', rmsg)
276 request_type = self.GetUniqueParam('request')
277 # Check server side requirements, as defined in
278 # device_management_backend.proto.
279 if (self.GetUniqueParam('devicetype') != '2' or
280 self.GetUniqueParam('apptype') != 'Chrome' or
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':
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)
298 return (400, 'Invalid request parameter')
300 self.DumpMessage('Response', response[1])
301 return (response[0], response[1].SerializeToString())
303 def CreatePolicyForExternalPolicyData(self, policy_key):
304 """Returns an ExternalPolicyData protobuf for policy_key.
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.
311 policy_key: The policy type and settings entity id, joined by '/'.
314 A serialized ExternalPolicyData.
316 settings = ep.ExternalPolicyData()
317 data = self.server.ReadPolicyDataFromDataDir(policy_key)
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()
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
330 oauth_token = self.GetUniqueParam('oauth_token')
334 match = re.match('GoogleLogin auth=(\\w+)',
335 self.headers.getheader('Authorization', ''))
337 return match.group(1)
341 def ProcessRegister(self, msg):
342 """Handles a register request.
344 Checks the query for authorization and device identifier, registers the
345 device with the server and constructs a response.
348 msg: The DeviceRegisterRequest message received from the client.
351 A tuple of HTTP status code and response data to send to the client.
353 # Check the auth token and device ID.
354 auth = self.CheckGoogleLogin()
356 return (403, 'No authorization')
358 policy = self.server.GetPolicies()
359 if ('*' not in policy['managed_users'] and
360 auth not in policy['managed_users']):
361 return (403, 'Unmanaged')
363 device_id = self.GetUniqueParam('deviceid')
365 return (400, 'Missing device identifier')
367 token_info = self.server.RegisterDevice(device_id,
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']
378 return (200, response)
380 def ProcessApiAuthorization(self, msg):
381 """Handles an API authorization request.
384 msg: The DeviceServiceApiAccessRequest message received from the client.
387 A tuple of HTTP status code and response data to send to the client.
389 policy = self.server.GetPolicies()
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')
397 return (200, response)
399 def ProcessUnregister(self, msg):
400 """Handles a register request.
402 Checks for authorization, unregisters the device and constructs the
406 msg: The DeviceUnregisterRequest message received from the client.
409 A tuple of HTTP status code and response data to send to the client.
411 # Check the management token.
412 token, response = self.CheckToken()
416 # Unregister the device.
417 self.server.UnregisterDevice(token['device_token'])
419 # Prepare and send the response.
420 response = dm.DeviceManagementResponse()
421 response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse())
423 return (200, response)
425 def ProcessPolicy(self, msg, request_type):
426 """Handles a policy request.
428 Checks for authorization, encodes the policy into protobuf representation
429 and constructs the response.
432 msg: The DeviceManagementRequest message received from the client.
435 A tuple of HTTP status code and response data to send to the client.
437 token_info, error = self.CheckToken()
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)
446 response = dm.DeviceManagementResponse()
447 for request in msg.policy_request.request:
448 if (request.policy_type in
449 ('google/android/user',
450 'google/chromeos/device',
451 'google/chromeos/publicaccount',
452 'google/chromeos/user',
453 'google/chrome/user',
455 fetch_response = response.policy_response.response.add()
456 self.ProcessCloudPolicy(request, token_info, fetch_response)
457 elif request.policy_type == 'google/chrome/extension':
458 self.ProcessCloudPolicyForExtensions(
459 request, response.policy_response, token_info)
461 fetch_response.error_code = 400
462 fetch_response.error_message = 'Invalid policy_type'
464 return (200, response)
466 def ProcessAutoEnrollment(self, msg):
467 """Handles an auto-enrollment check request.
469 The reply depends on the value of the modulus:
470 1: replies with no new modulus and the sha256 hash of "0"
471 2: replies with a new modulus, 4.
472 4: replies with a new modulus, 2.
473 8: fails with error 400.
474 16: replies with a new modulus, 16.
475 32: replies with a new modulus, 1.
476 anything else: replies with no new modulus and an empty list of hashes
478 These allow the client to pick the testing scenario its wants to simulate.
481 msg: The DeviceAutoEnrollmentRequest message received from the client.
484 A tuple of HTTP status code and response data to send to the client.
486 auto_enrollment_response = dm.DeviceAutoEnrollmentResponse()
489 auto_enrollment_response.hash.extend(
490 self.server.GetMatchingStateKeyHashes(msg.modulus, msg.remainder))
491 elif msg.modulus == 2:
492 auto_enrollment_response.expected_modulus = 4
493 elif msg.modulus == 4:
494 auto_enrollment_response.expected_modulus = 2
495 elif msg.modulus == 8:
496 return (400, 'Server error')
497 elif msg.modulus == 16:
498 auto_enrollment_response.expected_modulus = 16
499 elif msg.modulus == 32:
500 auto_enrollment_response.expected_modulus = 1
502 response = dm.DeviceManagementResponse()
503 response.auto_enrollment_response.CopyFrom(auto_enrollment_response)
504 return (200, response)
506 def ProcessDeviceStateRetrievalRequest(self, msg):
507 """Handles a device state retrieval request.
509 Response data is taken from server configuration.
512 A tuple of HTTP status code and response data to send to the client.
514 device_state_retrieval_response = dm.DeviceStateRetrievalResponse()
516 client = self.server.LookupByStateKey(msg.server_backed_state_key)
517 if client is not None:
518 state = self.server.GetPolicies().get('device_state', {})
525 setattr(device_state_retrieval_response, field, state[field])
527 response = dm.DeviceManagementResponse()
528 response.device_state_retrieval_response.CopyFrom(
529 device_state_retrieval_response)
530 return (200, response)
532 def SetProtobufMessageField(self, group_message, field, field_value):
533 """Sets a field in a protobuf message.
536 group_message: The protobuf message.
537 field: The field of the message to set, it should be a member of
538 group_message.DESCRIPTOR.fields.
539 field_value: The value to set.
541 if field.label == field.LABEL_REPEATED:
542 assert type(field_value) == list
543 entries = group_message.__getattribute__(field.name)
544 if field.message_type is None:
545 for list_item in field_value:
546 entries.append(list_item)
548 # This field is itself a protobuf.
549 sub_type = field.message_type
550 for sub_value in field_value:
551 assert type(sub_value) == dict
552 # Add a new sub-protobuf per list entry.
553 sub_message = entries.add()
554 # Now iterate over its fields and recursively add them.
555 for sub_field in sub_message.DESCRIPTOR.fields:
556 if sub_field.name in sub_value:
557 value = sub_value[sub_field.name]
558 self.SetProtobufMessageField(sub_message, sub_field, value)
560 elif field.type == field.TYPE_BOOL:
561 assert type(field_value) == bool
562 elif field.type == field.TYPE_STRING:
563 assert type(field_value) == str or type(field_value) == unicode
564 elif field.type == field.TYPE_INT64:
565 assert type(field_value) == int
566 elif (field.type == field.TYPE_MESSAGE and
567 field.message_type.name == 'StringList'):
568 assert type(field_value) == list
569 entries = group_message.__getattribute__(field.name).entries
570 for list_item in field_value:
571 entries.append(list_item)
574 raise Exception('Unknown field type %s' % field.type)
575 group_message.__setattr__(field.name, field_value)
577 def GatherDevicePolicySettings(self, settings, policies):
578 """Copies all the policies from a dictionary into a protobuf of type
579 CloudDeviceSettingsProto.
582 settings: The destination ChromeDeviceSettingsProto protobuf.
583 policies: The source dictionary containing policies in JSON format.
585 for group in settings.DESCRIPTOR.fields:
586 # Create protobuf message for group.
587 group_message = eval('dp.' + group.message_type.name + '()')
588 # Indicates if at least one field was set in |group_message|.
590 # Iterate over fields of the message and feed them from the
591 # policy config file.
592 for field in group_message.DESCRIPTOR.fields:
594 if field.name in policies:
596 field_value = policies[field.name]
597 self.SetProtobufMessageField(group_message, field, field_value)
599 settings.__getattribute__(group.name).CopyFrom(group_message)
601 def GatherUserPolicySettings(self, settings, policies):
602 """Copies all the policies from a dictionary into a protobuf of type
606 settings: The destination: a CloudPolicySettings protobuf.
607 policies: The source: a dictionary containing policies under keys
608 'recommended' and 'mandatory'.
610 for field in settings.DESCRIPTOR.fields:
611 # |field| is the entry for a specific policy in the top-level
612 # CloudPolicySettings proto.
614 # Look for this policy's value in the mandatory or recommended dicts.
615 if field.name in policies.get('mandatory', {}):
616 mode = cp.PolicyOptions.MANDATORY
617 value = policies['mandatory'][field.name]
618 elif field.name in policies.get('recommended', {}):
619 mode = cp.PolicyOptions.RECOMMENDED
620 value = policies['recommended'][field.name]
624 # Create protobuf message for this policy.
625 policy_message = eval('cp.' + field.message_type.name + '()')
626 policy_message.policy_options.mode = mode
627 field_descriptor = policy_message.DESCRIPTOR.fields_by_name['value']
628 self.SetProtobufMessageField(policy_message, field_descriptor, value)
629 settings.__getattribute__(field.name).CopyFrom(policy_message)
631 def ProcessCloudPolicyForExtensions(self, request, response, token_info):
632 """Handles a request for policy for extensions.
634 A request for policy for extensions is slightly different from the other
635 cloud policy requests, because it can trigger 0, one or many
636 PolicyFetchResponse messages in the response.
639 request: The PolicyFetchRequest that triggered this handler.
640 response: The DevicePolicyResponse message for the response. Multiple
641 PolicyFetchResponses will be appended to this message.
642 token_info: The token extracted from the request.
644 # Send one PolicyFetchResponse for each extension that has
645 # configuration data at the server.
646 ids = self.server.ListMatchingComponents('google/chrome/extension')
647 for settings_entity_id in ids:
648 # Reuse the extension policy request, to trigger the same signature
649 # type in the response.
650 request.settings_entity_id = settings_entity_id
651 fetch_response = response.response.add()
652 self.ProcessCloudPolicy(request, token_info, fetch_response)
653 # Don't do key rotations for these messages.
654 fetch_response.ClearField('new_public_key')
655 fetch_response.ClearField('new_public_key_signature')
656 fetch_response.ClearField('new_public_key_verification_signature')
658 def ProcessCloudPolicy(self, msg, token_info, response):
659 """Handles a cloud policy request. (New protocol for policy requests.)
661 Encodes the policy into protobuf representation, signs it and constructs
665 msg: The CloudPolicyRequest message received from the client.
666 token_info: The token extracted from the request.
667 response: A PolicyFetchResponse message that should be filled with the
672 self.server.UpdateMachineId(token_info['device_token'], msg.machine_id)
674 # Response is only given if the scope is specified in the config file.
675 # Normally 'google/chromeos/device', 'google/chromeos/user' and
676 # 'google/chromeos/publicaccount' should be accepted.
677 policy = self.server.GetPolicies()
679 policy_key = msg.policy_type
680 if msg.settings_entity_id:
681 policy_key += '/' + msg.settings_entity_id
682 if msg.policy_type in token_info['allowed_policy_types']:
683 if msg.policy_type in ('google/android/user',
684 'google/chromeos/publicaccount',
685 'google/chromeos/user',
686 'google/chrome/user',
688 settings = cp.CloudPolicySettings()
689 payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
691 self.GatherUserPolicySettings(settings, policy.get(policy_key, {}))
692 payload = settings.SerializeToString()
693 elif dp is not None and msg.policy_type == 'google/chromeos/device':
694 settings = dp.ChromeDeviceSettingsProto()
695 payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
697 self.GatherDevicePolicySettings(settings, policy.get(policy_key, {}))
698 payload = settings.SerializeToString()
699 elif msg.policy_type == 'google/chrome/extension':
700 settings = ep.ExternalPolicyData()
701 payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
703 payload = self.CreatePolicyForExternalPolicyData(policy_key)
705 response.error_code = 400
706 response.error_message = 'Invalid policy type'
709 response.error_code = 400
710 response.error_message = 'Request not allowed for the token used'
713 # Sign with 'current_key_index', defaulting to key 0.
716 current_key_index = policy.get('current_key_index', 0)
717 nkeys = len(self.server.keys)
718 if (msg.signature_type == dm.PolicyFetchRequest.SHA1_RSA and
719 current_key_index in range(nkeys)):
720 signing_key = self.server.keys[current_key_index]
721 if msg.public_key_version in range(1, nkeys + 1):
722 # requested key exists, use for signing and rotate.
723 req_key = self.server.keys[msg.public_key_version - 1]['private_key']
725 # Fill the policy data protobuf.
726 policy_data = dm.PolicyData()
727 policy_data.policy_type = msg.policy_type
728 policy_data.timestamp = int(time.time() * 1000)
729 policy_data.request_token = token_info['device_token']
730 policy_data.policy_value = payload
731 policy_data.machine_name = token_info['machine_name']
732 policy_data.valid_serial_number_missing = (
733 token_info['machine_id'] in BAD_MACHINE_IDS)
734 policy_data.settings_entity_id = msg.settings_entity_id
735 policy_data.service_account_identity = policy.get(
736 'service_account_identity',
737 'policy_testserver.py-service_account_identity')
738 invalidation_source = policy.get('invalidation_source')
739 if invalidation_source is not None:
740 policy_data.invalidation_source = invalidation_source
741 # Since invalidation_name is type bytes in the proto, the Unicode name
742 # provided needs to be encoded as ASCII to set the correct byte pattern.
743 invalidation_name = policy.get('invalidation_name')
744 if invalidation_name is not None:
745 policy_data.invalidation_name = invalidation_name.encode('ascii')
748 policy_data.public_key_version = current_key_index + 1
749 if msg.policy_type == 'google/chromeos/publicaccount':
750 policy_data.username = msg.settings_entity_id
752 # For regular user/device policy, there is no way for the testserver to
753 # know the user name belonging to the GAIA auth token we received (short
754 # of actually talking to GAIA). To address this, we read the username from
755 # the policy configuration dictionary, or use a default.
756 policy_data.username = policy.get('policy_user', 'user@example.com')
757 policy_data.device_id = token_info['device_id']
758 signed_data = policy_data.SerializeToString()
760 response.policy_data = signed_data
762 response.policy_data_signature = (
763 bytes(signing_key['private_key'].hashAndSign(signed_data)))
764 if msg.public_key_version != current_key_index + 1:
765 response.new_public_key = signing_key['public_key']
767 # Set the verification signature appropriate for the policy domain.
768 # TODO(atwilson): Use the enrollment domain for public accounts when
769 # we add key validation for ChromeOS (http://crbug.com/328038).
770 if 'signatures' in signing_key:
771 verification_sig = self.GetSignatureForDomain(
772 signing_key['signatures'], policy_data.username)
775 assert len(verification_sig) == 256, \
776 'bad signature size: %d' % len(verification_sig)
777 response.new_public_key_verification_signature = verification_sig
780 response.new_public_key_signature = (
781 bytes(req_key.hashAndSign(response.new_public_key)))
783 return (200, response.SerializeToString())
785 def GetSignatureForDomain(self, signatures, username):
786 parsed_username = username.split("@", 1)
787 if len(parsed_username) != 2:
788 logging.error('Could not extract domain from username: %s' % username)
790 domain = parsed_username[1]
792 # Lookup the domain's signature in the passed dictionary. If none is found,
793 # fallback to a wildcard signature.
794 if domain in signatures:
795 return signatures[domain]
796 if '*' in signatures:
797 return signatures['*']
799 # No key matching this domain.
800 logging.error('No verification signature matching domain: %s' % domain)
803 def CheckToken(self):
804 """Helper for checking whether the client supplied a valid DM token.
806 Extracts the token from the request and passed to the server in order to
810 A pair of token information record and error response. If the first
811 element is None, then the second contains an error code to send back to
812 the client. Otherwise the first element is the same structure that is
813 returned by LookupToken().
817 request_device_id = self.GetUniqueParam('deviceid')
818 match = re.match('GoogleDMToken token=(\\w+)',
819 self.headers.getheader('Authorization', ''))
821 dmtoken = match.group(1)
825 token_info = self.server.LookupToken(dmtoken)
826 if (not token_info or
827 not request_device_id or
828 token_info['device_id'] != request_device_id):
831 return (token_info, None)
833 logging.debug('Token check failed with error %d' % error)
835 return (None, (error, 'Server error %d' % error))
837 def DumpMessage(self, label, msg):
838 """Helper for logging an ASCII dump of a protobuf message."""
839 logging.debug('%s\n%s' % (label, str(msg)))
842 class PolicyTestServer(testserver_base.BrokenPipeHandlerMixIn,
843 testserver_base.StoppableHTTPServer):
844 """Handles requests and keeps global service state."""
846 def __init__(self, server_address, data_dir, policy_path, client_state_file,
847 private_key_paths, server_base_url):
848 """Initializes the server.
851 server_address: Server host and port.
852 policy_path: Names the file to read JSON-formatted policy from.
853 private_key_paths: List of paths to read private keys from.
855 testserver_base.StoppableHTTPServer.__init__(self, server_address,
856 PolicyRequestHandler)
857 self._registered_tokens = {}
858 self.data_dir = data_dir
859 self.policy_path = policy_path
860 self.client_state_file = client_state_file
861 self.server_base_url = server_base_url
864 if private_key_paths:
865 # Load specified keys from the filesystem.
866 for key_path in private_key_paths:
868 key_str = open(key_path).read()
870 print 'Failed to load private key from %s' % key_path
873 key = tlslite.api.parsePEMKey(key_str, private=True)
875 key = tlslite.utils.python_rsakey.Python_RSAKey._parsePKCS8(
878 assert key is not None
879 key_info = { 'private_key' : key }
881 # Now try to read in a signature, if one exists.
883 key_sig = open(key_path + '.sig').read()
884 # Create a dictionary with the wildcard domain + signature
885 key_info['signatures'] = {'*': key_sig}
887 print 'Failed to read validation signature from %s.sig' % key_path
888 self.keys.append(key_info)
890 # Use the canned private keys if none were passed from the command line.
891 for signing_key in SIGNING_KEYS:
892 decoded_key = base64.b64decode(signing_key['key']);
893 key = tlslite.utils.python_rsakey.Python_RSAKey._parsePKCS8(
894 bytearray(decoded_key))
895 assert key is not None
896 # Grab the signature dictionary for this key and decode all of the
898 signature_dict = signing_key['signatures']
899 decoded_signatures = {}
900 for domain in signature_dict:
901 decoded_signatures[domain] = base64.b64decode(signature_dict[domain])
902 self.keys.append({'private_key': key,
903 'signatures': decoded_signatures})
905 # Derive the public keys from the private keys.
906 for entry in self.keys:
907 key = entry['private_key']
909 algorithm = asn1der.Sequence(
910 [ asn1der.Data(asn1der.OBJECT_IDENTIFIER, PKCS1_RSA_OID),
911 asn1der.Data(asn1der.NULL, '') ])
912 rsa_pubkey = asn1der.Sequence([ asn1der.Integer(key.n),
913 asn1der.Integer(key.e) ])
914 pubkey = asn1der.Sequence([ algorithm, asn1der.Bitstring(rsa_pubkey) ])
915 entry['public_key'] = pubkey
918 if self.client_state_file is not None:
920 file_contents = open(self.client_state_file).read()
921 self._registered_tokens = json.loads(file_contents, strict=False)
925 def GetPolicies(self):
926 """Returns the policies to be used, reloaded form the backend file every
931 print 'No JSON module, cannot parse policy information'
934 policy = json.loads(open(self.policy_path).read(), strict=False)
936 print 'Failed to load policy from %s' % self.policy_path
939 def RegisterDevice(self, device_id, machine_id, type):
940 """Registers a device or user and generates a DM token for it.
943 device_id: The device identifier provided by the client.
946 The newly generated device token for the device.
949 while len(dmtoken_chars) < 32:
950 dmtoken_chars.append(random.choice('0123456789abcdef'))
951 dmtoken = ''.join(dmtoken_chars)
952 allowed_policy_types = {
953 dm.DeviceRegisterRequest.BROWSER: [
954 'google/chrome/user',
955 'google/chrome/extension'
957 dm.DeviceRegisterRequest.USER: [
958 'google/chromeos/user',
959 'google/chrome/extension'
961 dm.DeviceRegisterRequest.DEVICE: [
962 'google/chromeos/device',
963 'google/chromeos/publicaccount'
965 dm.DeviceRegisterRequest.ANDROID_BROWSER: [
966 'google/android/user'
968 dm.DeviceRegisterRequest.IOS_BROWSER: [
971 dm.DeviceRegisterRequest.TT: ['google/chromeos/user',
972 'google/chrome/user'],
974 if machine_id in KIOSK_MACHINE_IDS:
975 enrollment_mode = dm.DeviceRegisterResponse.RETAIL
977 enrollment_mode = dm.DeviceRegisterResponse.ENTERPRISE
978 self._registered_tokens[dmtoken] = {
979 'device_id': device_id,
980 'device_token': dmtoken,
981 'allowed_policy_types': allowed_policy_types[type],
982 'machine_name': 'chromeos-' + machine_id,
983 'machine_id': machine_id,
984 'enrollment_mode': enrollment_mode,
986 self.WriteClientState()
987 return self._registered_tokens[dmtoken]
989 def UpdateMachineId(self, dmtoken, machine_id):
990 """Updates the machine identifier for a registered device.
993 dmtoken: The device management token provided by the client.
994 machine_id: Updated hardware identifier value.
996 if dmtoken in self._registered_tokens:
997 self._registered_tokens[dmtoken]['machine_id'] = machine_id
998 self.WriteClientState()
1000 def UpdateStateKeys(self, dmtoken, state_keys):
1001 """Updates the state keys for a given client.
1004 dmtoken: The device management token provided by the client.
1005 state_keys: The state keys to set.
1007 if dmtoken in self._registered_tokens:
1008 self._registered_tokens[dmtoken]['state_keys'] = map(
1009 lambda key : key.encode('hex'), state_keys)
1010 self.WriteClientState()
1012 def LookupToken(self, dmtoken):
1013 """Looks up a device or a user by DM token.
1016 dmtoken: The device management token provided by the client.
1019 A dictionary with information about a device or user that is registered by
1020 dmtoken, or None if the token is not found.
1022 return self._registered_tokens.get(dmtoken, None)
1024 def LookupByStateKey(self, state_key):
1025 """Looks up a device or a user by a state key.
1028 state_key: The state key provided by the client.
1031 A dictionary with information about a device or user or None if there is
1034 for client in self._registered_tokens.values():
1035 if state_key.encode('hex') in client.get('state_keys', []):
1040 def GetMatchingStateKeyHashes(self, modulus, remainder):
1041 """Returns all clients registered with the server.
1044 The list of registered clients.
1046 state_keys = sum([ c.get('state_keys', [])
1047 for c in self._registered_tokens.values() ], [])
1048 hashed_keys = map(lambda key: hashlib.sha256(key.decode('hex')).digest(),
1051 lambda hash : int(hash.encode('hex'), 16) % modulus == remainder,
1054 def UnregisterDevice(self, dmtoken):
1055 """Unregisters a device identified by the given DM token.
1058 dmtoken: The device management token provided by the client.
1060 if dmtoken in self._registered_tokens.keys():
1061 del self._registered_tokens[dmtoken]
1062 self.WriteClientState()
1064 def WriteClientState(self):
1065 """Writes the client state back to the file."""
1066 if self.client_state_file is not None:
1067 json_data = json.dumps(self._registered_tokens)
1068 open(self.client_state_file, 'w').write(json_data)
1070 def GetBaseFilename(self, policy_selector):
1071 """Returns the base filename for the given policy_selector.
1074 policy_selector: The policy type and settings entity id, joined by '/'.
1077 The filename corresponding to the policy_selector, without a file
1080 sanitized_policy_selector = re.sub('[^A-Za-z0-9.@-]', '_', policy_selector)
1081 return os.path.join(self.data_dir or '',
1082 'policy_%s' % sanitized_policy_selector)
1084 def ListMatchingComponents(self, policy_type):
1085 """Returns a list of settings entity IDs that have a configuration file.
1088 policy_type: The policy type to look for. Only settings entity IDs for
1089 file selectors That match this policy_type will be returned.
1092 A list of settings entity IDs for the given |policy_type| that have a
1093 configuration file in this server (either as a .bin, .txt or .data file).
1095 base_name = self.GetBaseFilename(policy_type)
1096 files = glob.glob('%s_*.*' % base_name)
1097 len_base_name = len(base_name) + 1
1098 return [ file[len_base_name:file.rfind('.')] for file in files ]
1100 def ReadPolicyFromDataDir(self, policy_selector, proto_message):
1101 """Tries to read policy payload from a file in the data directory.
1103 First checks for a binary rendition of the policy protobuf in
1104 <data_dir>/policy_<sanitized_policy_selector>.bin. If that exists, returns
1105 it. If that file doesn't exist, tries
1106 <data_dir>/policy_<sanitized_policy_selector>.txt and decodes that as a
1107 protobuf using proto_message. If that fails as well, returns None.
1110 policy_selector: Selects which policy to read.
1111 proto_message: Optional protobuf message object used for decoding the
1115 The binary payload message, or None if not found.
1117 base_filename = self.GetBaseFilename(policy_selector)
1119 # Try the binary payload file first.
1121 return open(base_filename + '.bin').read()
1125 # If that fails, try the text version instead.
1126 if proto_message is None:
1130 text = open(base_filename + '.txt').read()
1131 google.protobuf.text_format.Merge(text, proto_message)
1132 return proto_message.SerializeToString()
1135 except google.protobuf.text_format.ParseError:
1138 def ReadPolicyDataFromDataDir(self, policy_selector):
1139 """Returns the external policy data for |policy_selector| if found.
1142 policy_selector: Selects which policy to read.
1145 The data for the corresponding policy type and entity id, if found.
1147 base_filename = self.GetBaseFilename(policy_selector)
1149 return open(base_filename + '.data').read()
1153 def GetBaseURL(self):
1154 """Returns the server base URL.
1156 Respects the |server_base_url| configuration parameter, if present. Falls
1157 back to construct the URL from the server hostname and port otherwise.
1160 The URL to use for constructing URLs that get returned to clients.
1162 base_url = self.server_base_url
1163 if base_url is None:
1164 base_url = 'http://%s:%s' % self.server_address[:2]
1169 class PolicyServerRunner(testserver_base.TestServerRunner):
1172 super(PolicyServerRunner, self).__init__()
1174 def create_server(self, server_data):
1175 data_dir = self.options.data_dir or ''
1176 config_file = (self.options.config_file or
1177 os.path.join(data_dir, 'device_management'))
1178 server = PolicyTestServer((self.options.host, self.options.port),
1179 data_dir, config_file,
1180 self.options.client_state_file,
1181 self.options.policy_keys,
1182 self.options.server_base_url)
1183 server_data['port'] = server.server_port
1186 def add_options(self):
1187 testserver_base.TestServerRunner.add_options(self)
1188 self.option_parser.add_option('--client-state', dest='client_state_file',
1189 help='File that client state should be '
1190 'persisted to. This allows the server to be '
1191 'seeded by a list of pre-registered clients '
1192 'and restarts without abandoning registered '
1194 self.option_parser.add_option('--policy-key', action='append',
1196 help='Specify a path to a PEM-encoded '
1197 'private key to use for policy signing. May '
1198 'be specified multiple times in order to '
1199 'load multiple keys into the server. If the '
1200 'server has multiple keys, it will rotate '
1201 'through them in at each request in a '
1202 'round-robin fashion. The server will '
1203 'use a canned key if none is specified '
1204 'on the command line. The test server will '
1205 'also look for a verification signature file '
1206 'in the same location: <filename>.sig and if '
1207 'present will add the signature to the '
1208 'policy blob as appropriate via the '
1209 'new_public_key_verification_signature '
1211 self.option_parser.add_option('--log-level', dest='log_level',
1213 help='Log level threshold to use.')
1214 self.option_parser.add_option('--config-file', dest='config_file',
1215 help='Specify a configuration file to use '
1216 'instead of the default '
1217 '<data_dir>/device_management')
1218 self.option_parser.add_option('--server-base-url', dest='server_base_url',
1219 help='The server base URL to use when '
1220 'constructing URLs to return to the client.')
1222 def run_server(self):
1223 logger = logging.getLogger()
1224 logger.setLevel(getattr(logging, str(self.options.log_level).upper()))
1225 if (self.options.log_to_console):
1226 logger.addHandler(logging.StreamHandler())
1227 if (self.options.log_file):
1228 logger.addHandler(logging.FileHandler(self.options.log_file))
1230 testserver_base.TestServerRunner.run_server(self)
1233 if __name__ == '__main__':
1234 sys.exit(PolicyServerRunner().main())