Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(45)

Side by Side Diff: chrome/browser/policy/test/policy_testserver.py

Issue 2530023002: Fix policy test server key rotation feature (Closed)
Patch Set: Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 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 2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file. 3 # found in the LICENSE file.
4 4
5 """A bare-bones test server for testing cloud policy support. 5 """A bare-bones test server for testing cloud policy support.
6 6
7 This implements a simple cloud policy test server that can be used to test 7 This implements a simple cloud policy test server that can be used to test
8 chrome's device management service client. The policy information is read from 8 chrome's device management service client. The policy information is read from
9 the file named device_management in the server's data directory. It contains 9 the file named device_management in the server's data directory. It contains
10 enforced and recommended policies for the device and user scope, and a list 10 enforced and recommended policies for the device and user scope, and a list
(...skipping 863 matching lines...) Expand 10 before | Expand all | Expand 10 after
874 payload = self.CreatePolicyForExternalPolicyData(policy_key) 874 payload = self.CreatePolicyForExternalPolicyData(policy_key)
875 else: 875 else:
876 response.error_code = 400 876 response.error_code = 400
877 response.error_message = 'Invalid policy type' 877 response.error_message = 'Invalid policy type'
878 return 878 return
879 else: 879 else:
880 response.error_code = 400 880 response.error_code = 400
881 response.error_message = 'Request not allowed for the token used' 881 response.error_message = 'Request not allowed for the token used'
882 return 882 return
883 883
884 # Sign with 'current_key_index', defaulting to key 0. 884 # Determine the current key on the client.
885 signing_key = None 885 client_key_version = None
886 req_key = None 886 client_key = None
887 current_key_index = policy.get('current_key_index', 0) 887 if msg.HasField('public_key_version'):
888 nkeys = len(self.server.keys) 888 client_key_version = msg.public_key_version
889 if (msg.signature_type == dm.PolicyFetchRequest.SHA1_RSA and 889 client_key = self.server.GetKeyByVersion(client_key_version)
890 current_key_index in range(nkeys)): 890 if client_key is None:
891 signing_key = self.server.keys[current_key_index] 891 response.error_code = 400
892 if msg.public_key_version in range(1, nkeys + 1): 892 response.error_message = 'Invalid public key version'
893 # requested key exists, use for signing and rotate. 893 return
894 req_key = self.server.keys[msg.public_key_version - 1]['private_key'] 894
895 # Choose the key for signing the policy.
896 signing_key_version = self.server.GetKeyVersionForSigning(
897 client_key_version)
898 signing_key = self.server.GetKeyByVersion(signing_key_version)
899 assert signing_key is not None
895 900
896 # Fill the policy data protobuf. 901 # Fill the policy data protobuf.
897 policy_data = dm.PolicyData() 902 policy_data = dm.PolicyData()
898 policy_data.policy_type = msg.policy_type 903 policy_data.policy_type = msg.policy_type
899 policy_data.timestamp = int(time.time() * 1000) 904 policy_data.timestamp = int(time.time() * 1000)
900 policy_data.request_token = token_info['device_token'] 905 policy_data.request_token = token_info['device_token']
901 policy_data.policy_value = payload 906 policy_data.policy_value = payload
902 policy_data.machine_name = token_info['machine_name'] 907 policy_data.machine_name = token_info['machine_name']
903 policy_data.valid_serial_number_missing = ( 908 policy_data.valid_serial_number_missing = (
904 token_info['machine_id'] in BAD_MACHINE_IDS) 909 token_info['machine_id'] in BAD_MACHINE_IDS)
905 policy_data.settings_entity_id = msg.settings_entity_id 910 policy_data.settings_entity_id = msg.settings_entity_id
906 policy_data.service_account_identity = policy.get( 911 policy_data.service_account_identity = policy.get(
907 'service_account_identity', 912 'service_account_identity',
908 'policy_testserver.py-service_account_identity') 913 'policy_testserver.py-service_account_identity')
909 invalidation_source = policy.get('invalidation_source') 914 invalidation_source = policy.get('invalidation_source')
910 if invalidation_source is not None: 915 if invalidation_source is not None:
911 policy_data.invalidation_source = invalidation_source 916 policy_data.invalidation_source = invalidation_source
912 # Since invalidation_name is type bytes in the proto, the Unicode name 917 # Since invalidation_name is type bytes in the proto, the Unicode name
913 # provided needs to be encoded as ASCII to set the correct byte pattern. 918 # provided needs to be encoded as ASCII to set the correct byte pattern.
914 invalidation_name = policy.get('invalidation_name') 919 invalidation_name = policy.get('invalidation_name')
915 if invalidation_name is not None: 920 if invalidation_name is not None:
916 policy_data.invalidation_name = invalidation_name.encode('ascii') 921 policy_data.invalidation_name = invalidation_name.encode('ascii')
917 922
918 if signing_key: 923 if msg.signature_type != dm.PolicyFetchRequest.NONE:
919 policy_data.public_key_version = current_key_index + 1 924 policy_data.public_key_version = signing_key_version
920 925
921 if username: 926 if username:
922 policy_data.username = username 927 policy_data.username = username
923 else: 928 else:
924 # If the correct |username| is unknown, rely on a manually-configured 929 # If the correct |username| is unknown, rely on a manually-configured
925 # username from the configuration file or use a default. 930 # username from the configuration file or use a default.
926 policy_data.username = policy.get('policy_user', 'username@example.com') 931 policy_data.username = policy.get('policy_user', 'username@example.com')
927 policy_data.device_id = token_info['device_id'] 932 policy_data.device_id = token_info['device_id']
928 933
929 # Set affiliation IDs so that user was managed on the device. 934 # Set affiliation IDs so that user was managed on the device.
930 device_affiliation_ids = policy.get('device_affiliation_ids') 935 device_affiliation_ids = policy.get('device_affiliation_ids')
931 if device_affiliation_ids: 936 if device_affiliation_ids:
932 policy_data.device_affiliation_ids.extend(device_affiliation_ids) 937 policy_data.device_affiliation_ids.extend(device_affiliation_ids)
933 938
934 user_affiliation_ids = policy.get('user_affiliation_ids') 939 user_affiliation_ids = policy.get('user_affiliation_ids')
935 if user_affiliation_ids: 940 if user_affiliation_ids:
936 policy_data.user_affiliation_ids.extend(user_affiliation_ids) 941 policy_data.user_affiliation_ids.extend(user_affiliation_ids)
937 942
938 signed_data = policy_data.SerializeToString() 943 response.policy_data = policy_data.SerializeToString()
939 944
940 response.policy_data = signed_data 945 # Sign the serialized policy data
941 if signing_key: 946 if msg.signature_type == dm.PolicyFetchRequest.SHA1_RSA:
942 response.policy_data_signature = ( 947 response.policy_data_signature = bytes(
943 bytes(signing_key['private_key'].hashAndSign(signed_data))) 948 signing_key['private_key'].hashAndSign(response.policy_data))
944 if msg.public_key_version != current_key_index + 1: 949 if msg.public_key_version != signing_key_version:
945 response.new_public_key = signing_key['public_key'] 950 response.new_public_key = signing_key['public_key']
946 951
947 # Set the verification signature appropriate for the policy domain. 952 # Set the verification signature appropriate for the policy domain.
948 # TODO(atwilson): Use the enrollment domain for public accounts when 953 # TODO(atwilson): Use the enrollment domain for public accounts when
949 # we add key validation for ChromeOS (http://crbug.com/328038). 954 # we add key validation for ChromeOS (http://crbug.com/328038).
950 if 'signatures' in signing_key: 955 if 'signatures' in signing_key:
951 verification_sig = self.GetSignatureForDomain( 956 verification_sig = self.GetSignatureForDomain(
952 signing_key['signatures'], policy_data.username) 957 signing_key['signatures'], policy_data.username)
953 958
954 if verification_sig: 959 if verification_sig:
955 assert len(verification_sig) == 256, \ 960 assert len(verification_sig) == 256, \
956 'bad signature size: %d' % len(verification_sig) 961 'bad signature size: %d' % len(verification_sig)
957 response.new_public_key_verification_signature_deprecated = ( 962 response.new_public_key_verification_signature_deprecated = (
958 verification_sig) 963 verification_sig)
959 964
960 if req_key: 965 if client_key is not None:
961 response.new_public_key_signature = ( 966 response.new_public_key_signature = bytes(
962 bytes(req_key.hashAndSign(response.new_public_key))) 967 client_key['private_key'].hashAndSign(response.new_public_key))
963 968
964 return (200, response.SerializeToString()) 969 return (200, response.SerializeToString())
965 970
966 def GetSignatureForDomain(self, signatures, username): 971 def GetSignatureForDomain(self, signatures, username):
967 parsed_username = username.split("@", 1) 972 parsed_username = username.split("@", 1)
968 if len(parsed_username) != 2: 973 if len(parsed_username) != 2:
969 logging.error('Could not extract domain from username: %s' % username) 974 logging.error('Could not extract domain from username: %s' % username)
970 return None 975 return None
971 domain = parsed_username[1] 976 domain = parsed_username[1]
972 977
(...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after
1018 def DumpMessage(self, label, msg): 1023 def DumpMessage(self, label, msg):
1019 """Helper for logging an ASCII dump of a protobuf message.""" 1024 """Helper for logging an ASCII dump of a protobuf message."""
1020 logging.debug('%s\n%s' % (label, str(msg))) 1025 logging.debug('%s\n%s' % (label, str(msg)))
1021 1026
1022 1027
1023 class PolicyTestServer(testserver_base.BrokenPipeHandlerMixIn, 1028 class PolicyTestServer(testserver_base.BrokenPipeHandlerMixIn,
1024 testserver_base.StoppableHTTPServer): 1029 testserver_base.StoppableHTTPServer):
1025 """Handles requests and keeps global service state.""" 1030 """Handles requests and keeps global service state."""
1026 1031
1027 def __init__(self, server_address, data_dir, policy_path, client_state_file, 1032 def __init__(self, server_address, data_dir, policy_path, client_state_file,
1028 private_key_paths, server_base_url): 1033 private_key_paths, rotate_keys_automatically, server_base_url):
1029 """Initializes the server. 1034 """Initializes the server.
1030 1035
1031 Args: 1036 Args:
1032 server_address: Server host and port. 1037 server_address: Server host and port.
1033 policy_path: Names the file to read JSON-formatted policy from. 1038 policy_path: Names the file to read JSON-formatted policy from.
1034 private_key_paths: List of paths to read private keys from. 1039 private_key_paths: List of paths to read private keys from.
1040 rotate_keys_automatically: Whether the keys should be rotated in a
1041 round-robin fashion for each policy request (by default, either the
1042 key specified in the config or the first key will be used for all
Andrew T Wilson (Slow) 2016/11/25 14:50:09 This is fine. As we discussed, we might get slight
emaxx 2016/11/25 16:17:38 This is a valid concern. I think ideally it should
1043 requests).
1035 """ 1044 """
1036 testserver_base.StoppableHTTPServer.__init__(self, server_address, 1045 testserver_base.StoppableHTTPServer.__init__(self, server_address,
1037 PolicyRequestHandler) 1046 PolicyRequestHandler)
1038 self._registered_tokens = {} 1047 self._registered_tokens = {}
1039 self.data_dir = data_dir 1048 self.data_dir = data_dir
1040 self.policy_path = policy_path 1049 self.policy_path = policy_path
1041 self.client_state_file = client_state_file 1050 self.client_state_file = client_state_file
1051 self.rotate_keys_automatically = rotate_keys_automatically
1042 self.server_base_url = server_base_url 1052 self.server_base_url = server_base_url
1043 1053
1044 self.keys = [] 1054 self.keys = []
1045 if private_key_paths: 1055 if private_key_paths:
1046 # Load specified keys from the filesystem. 1056 # Load specified keys from the filesystem.
1047 for key_path in private_key_paths: 1057 for key_path in private_key_paths:
1048 try: 1058 try:
1049 key_str = open(key_path).read() 1059 key_str = open(key_path).read()
1050 except IOError: 1060 except IOError:
1051 print 'Failed to load private key from %s' % key_path 1061 print 'Failed to load private key from %s' % key_path
(...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after
1110 policy = {} 1120 policy = {}
1111 if json is None: 1121 if json is None:
1112 logging.error('No JSON module, cannot parse policy information') 1122 logging.error('No JSON module, cannot parse policy information')
1113 else : 1123 else :
1114 try: 1124 try:
1115 policy = json.loads(open(self.policy_path).read(), strict=False) 1125 policy = json.loads(open(self.policy_path).read(), strict=False)
1116 except IOError: 1126 except IOError:
1117 logging.error('Failed to load policies from %s' % self.policy_path) 1127 logging.error('Failed to load policies from %s' % self.policy_path)
1118 return policy 1128 return policy
1119 1129
1130 def GetKeyByVersion(self, key_version):
1131 """Obtains the object containing key properties, given the key version.
1132
1133 Args:
1134 key_version: Integer key version.
1135
1136 Returns:
1137 The object containing key properties, or None if the key is not found.
1138 """
1139 key_index = key_version - 1
1140 if key_index < 0:
1141 return None
1142 if key_index >= len(self.keys):
1143 if self.rotate_keys_automatically:
1144 key_index %= len(self.keys)
1145 else:
1146 return None
1147 return self.keys[key_index]
1148
1149 def GetKeyVersionForSigning(self, client_key_version):
1150 """Determines the version of the key that should be used for signing policy.
1151
1152 Args:
1153 client_key_version: Either an integer representing the current key version
1154 provided by the client, or None if the client didn't provide any.
1155
1156 Returns:
1157 An integer representing the signing key version.
1158 """
1159 if self.rotate_keys_automatically and client_key_version is not None:
1160 return client_key_version + 1
1161 return self.GetPolicies().get('current_key_index', 0) + 1
1162
1120 def ResolveUser(self, auth_token): 1163 def ResolveUser(self, auth_token):
1121 """Tries to resolve an auth token to the corresponding user name. 1164 """Tries to resolve an auth token to the corresponding user name.
1122 1165
1123 If enabled, this makes a request to the token info endpoint to determine the 1166 If enabled, this makes a request to the token info endpoint to determine the
1124 user ID corresponding to the token. If token resolution is disabled or the 1167 user ID corresponding to the token. If token resolution is disabled or the
1125 request fails, this will return the policy_user config parameter. 1168 request fails, this will return the policy_user config parameter.
1126 """ 1169 """
1127 config = self.GetPolicies() 1170 config = self.GetPolicies()
1128 token_info_url = config.get('token_info_url') 1171 token_info_url = config.get('token_info_url')
1129 if token_info_url is not None: 1172 if token_info_url is not None:
(...skipping 245 matching lines...) Expand 10 before | Expand all | Expand 10 after
1375 super(PolicyServerRunner, self).__init__() 1418 super(PolicyServerRunner, self).__init__()
1376 1419
1377 def create_server(self, server_data): 1420 def create_server(self, server_data):
1378 data_dir = self.options.data_dir or '' 1421 data_dir = self.options.data_dir or ''
1379 config_file = (self.options.config_file or 1422 config_file = (self.options.config_file or
1380 os.path.join(data_dir, 'device_management')) 1423 os.path.join(data_dir, 'device_management'))
1381 server = PolicyTestServer((self.options.host, self.options.port), 1424 server = PolicyTestServer((self.options.host, self.options.port),
1382 data_dir, config_file, 1425 data_dir, config_file,
1383 self.options.client_state_file, 1426 self.options.client_state_file,
1384 self.options.policy_keys, 1427 self.options.policy_keys,
1428 self.options.rotate_keys_automatically,
1385 self.options.server_base_url) 1429 self.options.server_base_url)
1386 server_data['port'] = server.server_port 1430 server_data['port'] = server.server_port
1387 return server 1431 return server
1388 1432
1389 def add_options(self): 1433 def add_options(self):
1390 testserver_base.TestServerRunner.add_options(self) 1434 testserver_base.TestServerRunner.add_options(self)
1391 self.option_parser.add_option('--client-state', dest='client_state_file', 1435 self.option_parser.add_option('--client-state', dest='client_state_file',
1392 help='File that client state should be ' 1436 help='File that client state should be '
1393 'persisted to. This allows the server to be ' 1437 'persisted to. This allows the server to be '
1394 'seeded by a list of pre-registered clients ' 1438 'seeded by a list of pre-registered clients '
1395 'and restarts without abandoning registered ' 1439 'and restarts without abandoning registered '
1396 'clients.') 1440 'clients.')
1397 self.option_parser.add_option('--policy-key', action='append', 1441 self.option_parser.add_option('--policy-key', action='append',
1398 dest='policy_keys', 1442 dest='policy_keys',
1399 help='Specify a path to a PEM-encoded ' 1443 help='Specify a path to a PEM-encoded '
1400 'private key to use for policy signing. May ' 1444 'private key to use for policy signing. May '
1401 'be specified multiple times in order to ' 1445 'be specified multiple times in order to '
1402 'load multiple keys into the server. If the ' 1446 'load multiple keys into the server. The '
1403 'server has multiple keys, it will rotate ' 1447 'server will use a canned key if none is '
1404 'through them in at each request in a ' 1448 'specified on the command line. The test '
1405 'round-robin fashion. The server will ' 1449 'server will also look for a verification '
1406 'use a canned key if none is specified ' 1450 'signature file in the same location: '
1407 'on the command line. The test server will ' 1451 '<filename>.sig and if present will add the '
1408 'also look for a verification signature file ' 1452 'signature to the policy blob as appropriate '
1409 'in the same location: <filename>.sig and if ' 1453 'via the '
1410 'present will add the signature to the '
1411 'policy blob as appropriate via the '
1412 'new_public_key_verification_signature_deprecated ' 1454 'new_public_key_verification_signature_deprecated '
1413 'field.') 1455 'field.')
1456 self.option_parser.add_option('--rotate-policy-keys-automatically',
1457 action='store_true',
1458 dest='rotate_keys_automatically',
1459 help='If present, then the policy keys will '
1460 'be rotated in a round-robin fashion for '
1461 'each policy request (by default, either the '
1462 'key specified in the config or the first '
1463 'key will be used for all requests).')
1414 self.option_parser.add_option('--log-level', dest='log_level', 1464 self.option_parser.add_option('--log-level', dest='log_level',
1415 default='WARN', 1465 default='WARN',
1416 help='Log level threshold to use.') 1466 help='Log level threshold to use.')
1417 self.option_parser.add_option('--config-file', dest='config_file', 1467 self.option_parser.add_option('--config-file', dest='config_file',
1418 help='Specify a configuration file to use ' 1468 help='Specify a configuration file to use '
1419 'instead of the default ' 1469 'instead of the default '
1420 '<data_dir>/device_management') 1470 '<data_dir>/device_management')
1421 self.option_parser.add_option('--server-base-url', dest='server_base_url', 1471 self.option_parser.add_option('--server-base-url', dest='server_base_url',
1422 help='The server base URL to use when ' 1472 help='The server base URL to use when '
1423 'constructing URLs to return to the client.') 1473 'constructing URLs to return to the client.')
1424 1474
1425 def run_server(self): 1475 def run_server(self):
1426 logger = logging.getLogger() 1476 logger = logging.getLogger()
1427 logger.setLevel(getattr(logging, str(self.options.log_level).upper())) 1477 logger.setLevel(getattr(logging, str(self.options.log_level).upper()))
1428 if (self.options.log_to_console): 1478 if (self.options.log_to_console):
1429 logger.addHandler(logging.StreamHandler()) 1479 logger.addHandler(logging.StreamHandler())
1430 if (self.options.log_file): 1480 if (self.options.log_file):
1431 logger.addHandler(logging.FileHandler(self.options.log_file)) 1481 logger.addHandler(logging.FileHandler(self.options.log_file))
1432 1482
1433 testserver_base.TestServerRunner.run_server(self) 1483 testserver_base.TestServerRunner.run_server(self)
1434 1484
1435 1485
1436 if __name__ == '__main__': 1486 if __name__ == '__main__':
1437 sys.exit(PolicyServerRunner().main()) 1487 sys.exit(PolicyServerRunner().main())
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698