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