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