blob: ee590c50b585de2083bfe5710c543ba3e5774b73 [file] [log] [blame]
Mao Huang700663d2015-08-12 09:58:59 +08001# Copyright 2015 The Chromium OS 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"""The DRM Keys Provisioning Server (DKPS) implementation."""
6
7# TODO(littlecvr): Implement "without filter mode", which lets OEM encrypts DRM
8# keys directly with ODM's public key, and the key server
9# merely stores them without knowing anything about them.
10
Mao Huang700663d2015-08-12 09:58:59 +080011import argparse
Mao Huang9076f632015-09-24 17:38:47 +080012import hashlib
Mao Huang700663d2015-08-12 09:58:59 +080013import imp
14import json
Mao Huang041483e2015-09-14 23:28:18 +080015import logging
Mao Huang901437f2016-06-24 11:39:15 +080016import logging.config
Mao Huang700663d2015-08-12 09:58:59 +080017import os
18import shutil
Mao Huang700663d2015-08-12 09:58:59 +080019import sqlite3
20import textwrap
Yilin Yang2a2bb112019-10-23 11:20:33 +080021import xmlrpc.server
Mao Huang700663d2015-08-12 09:58:59 +080022
23import gnupg
24
25
26SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
27FILTERS_DIR = os.path.join(SCRIPT_DIR, 'filters')
Mao Huang9076f632015-09-24 17:38:47 +080028PARSERS_DIR = os.path.join(SCRIPT_DIR, 'parsers')
Mao Huang700663d2015-08-12 09:58:59 +080029CREATE_DATABASE_SQL_FILE_PATH = os.path.join(
30 SCRIPT_DIR, 'sql', 'create_database.sql')
31
Mao Huang901437f2016-06-24 11:39:15 +080032DEFAULT_BIND_ADDR = '0.0.0.0' # all addresses
33DEFAULT_BIND_PORT = 5438
34
35DEFAULT_DATABASE_FILE_NAME = 'dkps.db'
36DEFAULT_GNUPG_DIR_NAME = 'gnupg'
37DEFAULT_LOG_FILE_NAME = 'dkps.log'
38
39DEFAULT_LOGGING_CONFIG = {
40 'version': 1,
41 'formatters': {
42 'default': {
43 'format': '%(asctime)s:%(levelname)s:%(funcName)s:'
44 '%(lineno)d:%(message)s'}},
45 'handlers': {
46 'file': {
47 'class': 'logging.handlers.RotatingFileHandler',
48 'formatter': 'default',
49 'filename': DEFAULT_LOG_FILE_NAME,
50 'maxBytes': 1024 * 1024, # 1M
51 'backupCount': 3},
52 'console': {
53 'class': 'logging.StreamHandler',
54 'formatter': 'default',
55 'stream': 'ext://sys.stdout'}},
56 'root': {
57 'level': 'INFO',
58 # only log to file by default, but also log to console if invoked
59 # directly from the command line
60 'handlers': ['file'] + ['console'] if __name__ == '__main__' else []}}
61
Mao Huang700663d2015-08-12 09:58:59 +080062
63class ProjectNotFoundException(ValueError):
64 """Raised when no project was found in the database."""
Mao Huang700663d2015-08-12 09:58:59 +080065
66
67class InvalidUploaderException(ValueError):
68 """Raised when the signature of the uploader can't be verified."""
Mao Huang700663d2015-08-12 09:58:59 +080069
70
71class InvalidRequesterException(ValueError):
72 """Raised when the signature of the requester can't be verified."""
Mao Huang700663d2015-08-12 09:58:59 +080073
74
75def GetSQLite3Connection(database_file_path):
76 """Returns a tuple of SQLite3's (connection, cursor) to database_file_path.
77
78 If the connection has been created before, it is returned directly. If it's
79 not, this function creates the connection, ensures that the foreign key
80 constraint is enabled, and returns.
81
82 Args:
83 database_file_path: path to the SQLite3 database file.
84 """
85 database_file_path = os.path.realpath(database_file_path)
86
87 # Return if the connection to database_file_path has been created before.
88 try:
89 connection = GetSQLite3Connection.connection_dict[database_file_path]
90 return (connection, connection.cursor())
91 except KeyError:
92 pass
93 except AttributeError:
94 GetSQLite3Connection.connection_dict = {}
95
96 # Create connection.
97 connection = sqlite3.connect(database_file_path)
98 connection.row_factory = sqlite3.Row
99 cursor = connection.cursor()
100
101 # Enable foreign key constraint since SQLite3 disables it by default.
102 cursor.execute('PRAGMA foreign_keys = ON')
103 # Check if foreign key constraint is enabled.
104 cursor.execute('PRAGMA foreign_keys')
105 if cursor.fetchone()[0] != 1:
106 raise RuntimeError('Failed to enable SQLite3 foreign key constraint')
107
108 GetSQLite3Connection.connection_dict[database_file_path] = connection
109
110 return (connection, cursor)
111
112
Fei Shaobd07c9a2020-06-15 19:04:50 +0800113class DRMKeysProvisioningServer:
Mao Huang700663d2015-08-12 09:58:59 +0800114 """The DRM Keys Provisioning Server (DKPS) class."""
115
116 def __init__(self, database_file_path, gnupg_homedir):
117 """DKPS constructor.
118
119 Args:
120 database_file_path: path to the SQLite3 database file.
121 gnupg_homedir: path to the GnuPG home directory.
122 """
123 self.database_file_path = database_file_path
124 self.gnupg_homedir = gnupg_homedir
125
126 if not os.path.isdir(self.gnupg_homedir):
127 self.gpg = None
128 else:
chuntsenf0780db2019-05-03 02:21:59 +0800129 self.gpg = gnupg.GPG(gnupghome=self.gnupg_homedir)
Mao Huang700663d2015-08-12 09:58:59 +0800130
131 if not os.path.isfile(self.database_file_path):
132 self.db_connection, self.db_cursor = (None, None)
133 else:
134 self.db_connection, self.db_cursor = GetSQLite3Connection(
135 self.database_file_path)
136
Mao Huang3963b372015-12-11 15:20:19 +0800137 def Initialize(self, gpg_gen_key_args_dict=None, server_key_file_path=None):
138 """Creates the SQLite3 database and GnuPG home, and imports, or generates a
139 GPG key for the server to use.
Mao Huang700663d2015-08-12 09:58:59 +0800140
141 Args:
142 gpg_gen_key_args_dict: will be passed directly as the keyword arguments to
Mao Huang3963b372015-12-11 15:20:19 +0800143 python-gnupg's gen_key() function if server_key_file_path is None.
144 Can be used to customize the key generator process, such as key_type,
145 key_length, etc. See python-gnupg's doc for what can be customized.
146 server_key_file_path: path to the server key to use. If not None, the
147 system will simply import this key and use it as the server key; if
148 None, the system will generate a new key.
Mao Huang700663d2015-08-12 09:58:59 +0800149
150 Raises:
151 RuntimeError is the database and GnuPG home have already been initialized.
152 """
Mao Huang700663d2015-08-12 09:58:59 +0800153 # Create GPG instance and database connection.
chuntsenf0780db2019-05-03 02:21:59 +0800154 self.gpg = gnupg.GPG(gnupghome=self.gnupg_homedir)
Mao Huang700663d2015-08-12 09:58:59 +0800155 self.db_connection, self.db_cursor = GetSQLite3Connection(
156 self.database_file_path)
157
Mao Huang3963b372015-12-11 15:20:19 +0800158 # If any key exists, the system has already been initialized.
159 if self.gpg.list_keys():
160 raise RuntimeError('Already initialized')
Mao Huang700663d2015-08-12 09:58:59 +0800161
Mao Huang3963b372015-12-11 15:20:19 +0800162 if server_key_file_path: # use existing key
Mao Huang901437f2016-06-24 11:39:15 +0800163 # TODO(littlecvr): make sure the server key doesn't have passphrase.
Mao Huang3963b372015-12-11 15:20:19 +0800164 server_key_fingerprint, _ = self._ImportGPGKey(server_key_file_path)
165 else: # generate a new GPG key
166 if gpg_gen_key_args_dict is None:
167 gpg_gen_key_args_dict = {}
168 if 'name_real' not in gpg_gen_key_args_dict:
169 gpg_gen_key_args_dict['name_real'] = 'DKPS Server'
170 if 'name_email' not in gpg_gen_key_args_dict:
171 gpg_gen_key_args_dict['name_email'] = 'chromeos-factory-dkps@google.com'
172 if 'name_comment' not in gpg_gen_key_args_dict:
173 gpg_gen_key_args_dict['name_comment'] = 'DRM Keys Provisioning Server'
174 key_input_data = self.gpg.gen_key_input(**gpg_gen_key_args_dict)
175 server_key_fingerprint = self.gpg.gen_key(key_input_data).fingerprint
Mao Huang700663d2015-08-12 09:58:59 +0800176
177 # Create and set up the schema of the database.
178 with open(CREATE_DATABASE_SQL_FILE_PATH) as f:
179 create_database_sql = f.read()
180 with self.db_connection:
181 self.db_cursor.executescript(create_database_sql)
182
183 # Record the server key fingerprint.
184 with self.db_connection:
185 self.db_cursor.execute(
186 'INSERT INTO settings (key, value) VALUES (?, ?)',
Mao Huang3963b372015-12-11 15:20:19 +0800187 ('server_key_fingerprint', server_key_fingerprint))
Mao Huang700663d2015-08-12 09:58:59 +0800188
189 def Destroy(self):
190 """Destroys the database and GnuPG home directory.
191
192 This is the opposite of Initialize(). It essentially removes the SQLite3
193 database file and GnuPG home directory.
194 """
195 # Remove database.
196 if self.db_connection:
197 self.db_connection.close()
198 if os.path.exists(self.database_file_path):
199 os.remove(self.database_file_path)
200
201 # Remove GnuPG home.
202 if self.gpg:
203 self.gpg = None
204 if os.path.exists(self.gnupg_homedir):
205 shutil.rmtree(self.gnupg_homedir)
206
207 def AddProject(self, name, uploader_key_file_path, requester_key_file_path,
Mao Huang9076f632015-09-24 17:38:47 +0800208 parser_module_file_name, filter_module_file_name=None):
Mao Huang700663d2015-08-12 09:58:59 +0800209 """Adds a project.
210
211 Args:
212 name: name of the project, must be unique.
213 uploader_key_file_path: path to the OEM's public key file.
214 requester_key_file_path: path to the ODM's public key file.
Mao Huang9076f632015-09-24 17:38:47 +0800215 parser_module_file_name: file name of the parser python module.
Mao Huang700663d2015-08-12 09:58:59 +0800216 filter_module_file_name: file name of the filter python module.
217
218 Raises:
219 ValueError if either the uploader's or requester's key are imported (which
220 means they are used by another project).
221 """
Mao Huang9076f632015-09-24 17:38:47 +0800222 # Try to load the parser and filter modules.
223 self._LoadParserModule(parser_module_file_name)
Mao Huang700663d2015-08-12 09:58:59 +0800224 if filter_module_file_name is not None:
225 self._LoadFilterModule(filter_module_file_name)
226
227 # Try to import uploader and requester keys and add project info into the
228 # database, if failed at any step, delete imported keys.
229 uploader_key_fingerprint, requester_key_fingerprint = (None, None)
230 uploader_key_already_exists, requester_key_already_exists = (False, False)
231 try:
232 uploader_key_fingerprint, uploader_key_already_exists = (
233 self._ImportGPGKey(uploader_key_file_path))
234 if uploader_key_already_exists:
235 raise ValueError('Uploader key already exists')
236 requester_key_fingerprint, requester_key_already_exists = (
237 self._ImportGPGKey(requester_key_file_path))
238 if requester_key_already_exists:
239 raise ValueError('Requester key already exists')
240 with self.db_connection:
241 self.db_cursor.execute(
Mao Huang9076f632015-09-24 17:38:47 +0800242 'INSERT INTO projects ('
243 ' name, uploader_key_fingerprint, requester_key_fingerprint, '
244 ' parser_module_file_name, filter_module_file_name) '
245 'VALUES (?, ?, ?, ?, ?)',
Mao Huang700663d2015-08-12 09:58:59 +0800246 (name, uploader_key_fingerprint, requester_key_fingerprint,
Mao Huang9076f632015-09-24 17:38:47 +0800247 parser_module_file_name, filter_module_file_name))
Mao Huang700663d2015-08-12 09:58:59 +0800248 except BaseException:
249 if not uploader_key_already_exists and uploader_key_fingerprint:
250 self.gpg.delete_keys(uploader_key_fingerprint)
251 if not requester_key_already_exists and requester_key_fingerprint:
252 self.gpg.delete_keys(requester_key_fingerprint)
253 raise
254
255 def UpdateProject(self, name, uploader_key_file_path=None,
256 requester_key_file_path=None, filter_module_file_name=None):
257 """Updates a project.
258
259 Args:
260 name: name of the project, must be unique.
261 uploader_key_file_path: path to the OEM's public key file.
262 requester_key_file_path: path to the ODM's public key file.
263 filter_module_file_name: file name of the filter python module.
264
265 Raises:
266 RuntimeError if SQLite3 can't update the project row (for any reason).
267 """
268 # Try to load the filter module.
269 if filter_module_file_name is not None:
270 self._LoadFilterModule(filter_module_file_name)
271
272 project = self._FetchProjectByName(name)
273
274 # Try to import uploader and requester keys and add project info into the
275 # database, if failed at any step, delete any newly imported keys.
276 uploader_key_fingerprint, requester_key_fingerprint = (None, None)
277 old_uploader_key_fingerprint = project['uploader_key_fingerprint']
278 old_requester_key_fingerprint = project['requester_key_fingerprint']
279 same_uploader_key, same_requester_key = (True, True)
280 try:
281 sql_set_clause_list = ['filter_module_file_name = ?']
282 sql_parameters = [filter_module_file_name]
283
284 if uploader_key_file_path:
285 uploader_key_fingerprint, same_uploader_key = self._ImportGPGKey(
286 uploader_key_file_path)
287 sql_set_clause_list.append('uploader_key_fingerprint = ?')
288 sql_parameters.append(uploader_key_fingerprint)
289
290 if requester_key_file_path:
291 requester_key_fingerprint, same_requester_key = self._ImportGPGKey(
292 uploader_key_file_path)
293 sql_set_clause_list.append('requester_key_fingerprint = ?')
294 sql_parameters.append(requester_key_fingerprint)
295
296 sql_set_clause = ','.join(sql_set_clause_list)
297 sql_parameters.append(name)
298 with self.db_connection:
299 self.db_cursor.execute(
300 'UPDATE projects SET %s WHERE name = ?' % sql_set_clause,
301 tuple(sql_parameters))
302 if self.db_cursor.rowcount != 1:
303 raise RuntimeError('Failed to update project %s' % name)
304 except BaseException:
305 if not same_uploader_key and uploader_key_fingerprint:
306 self.gpg.delete_keys(uploader_key_fingerprint)
307 if not same_requester_key and requester_key_fingerprint:
308 self.gpg.delete_keys(requester_key_fingerprint)
309 raise
310
311 if not same_uploader_key:
312 self.gpg.delete_keys(old_uploader_key_fingerprint)
313 if not same_requester_key:
314 self.gpg.delete_keys(old_requester_key_fingerprint)
315
316 def RemoveProject(self, name):
317 """Removes a project.
318
319 Args:
320 name: the name of the project specified when added.
321 """
322 project = self._FetchProjectByName(name)
323
324 self.gpg.delete_keys(project['uploader_key_fingerprint'])
325 self.gpg.delete_keys(project['requester_key_fingerprint'])
326
327 with self.db_connection:
328 self.db_cursor.execute(
329 'DELETE FROM drm_keys WHERE project_name = ?', (name,))
330 self.db_cursor.execute('DELETE FROM projects WHERE name = ?', (name,))
331
332 def ListProjects(self):
333 """Lists all projects."""
334 self.db_cursor.execute('SELECT * FROM projects ORDER BY name ASC')
335 return self.db_cursor.fetchall()
336
337 def Upload(self, encrypted_serialized_drm_keys):
338 """Uploads a list of DRM keys to the server. This is an atomic operation. It
339 will either succeed and save all the keys, or fail and save no keys.
340
341 Args:
342 encrypted_serialized_drm_keys: the serialized DRM keys signed by the
343 uploader and encrypted by the server's public key.
344
345 Raises:
346 InvalidUploaderException if the signature of the uploader can not be
347 verified.
348 """
349 decrypted_obj = self.gpg.decrypt(encrypted_serialized_drm_keys)
350 project = self._FetchProjectByUploaderKeyFingerprint(
351 decrypted_obj.fingerprint)
352 serialized_drm_keys = decrypted_obj.data
353
Mao Huang9076f632015-09-24 17:38:47 +0800354 # Pass to the parse function.
355 parser_module = self._LoadParserModule(project['parser_module_file_name'])
356 drm_key_list = parser_module.Parse(serialized_drm_keys)
357
358 drm_key_hash_list = []
359 for drm_key in drm_key_list:
Yilin Yang0412c272019-12-05 16:57:40 +0800360 drm_key_hash = hashlib.sha1(
361 json.dumps(drm_key).encode('utf-8')).hexdigest()
362 drm_key_hash_list.append(drm_key_hash)
Mao Huang9076f632015-09-24 17:38:47 +0800363
Mao Huangecbeb122016-06-22 20:30:38 +0800364 # Pass to the filter function if needed.
365 if project['filter_module_file_name']: # filter module can be null
366 filter_module = self._LoadFilterModule(project['filter_module_file_name'])
367 filtered_drm_key_list = filter_module.Filter(drm_key_list)
368 else:
369 # filter module is optional
370 filtered_drm_key_list = drm_key_list
Mao Huang700663d2015-08-12 09:58:59 +0800371
372 # Fetch server key for signing.
373 server_key_fingerprint = self._FetchServerKeyFingerprint()
374
375 # Sign and encrypt each key by server's private key and requester's public
376 # key, respectively.
377 encrypted_serialized_drm_key_list = []
378 requester_key_fingerprint = project['requester_key_fingerprint']
379 for drm_key in filtered_drm_key_list:
380 encrypted_obj = self.gpg.encrypt(
381 json.dumps(drm_key), requester_key_fingerprint,
chuntsenf0780db2019-05-03 02:21:59 +0800382 always_trust=True, sign=server_key_fingerprint)
Mao Huang700663d2015-08-12 09:58:59 +0800383 encrypted_serialized_drm_key_list.append(encrypted_obj.data)
384
385 # Insert into the database.
386 with self.db_connection:
387 self.db_cursor.executemany(
Mao Huang9076f632015-09-24 17:38:47 +0800388 'INSERT INTO drm_keys ('
389 ' project_name, drm_key_hash, encrypted_drm_key) '
390 'VALUES (?, ?, ?)',
Yilin Yang7c865822019-11-01 14:50:26 +0800391 list(zip([project['name']] * len(encrypted_serialized_drm_key_list),
392 drm_key_hash_list, encrypted_serialized_drm_key_list)))
Mao Huang700663d2015-08-12 09:58:59 +0800393
394 def AvailableKeyCount(self, requester_signature):
395 """Queries the number of remaining keys.
396
397 Args:
398 requester_signature: a message signed by the requester. Since the server
399 doesn't need any additional info from the requester, the requester can
400 simply sign a random string and send it here.
401
402 Returns:
403 The number of remaining keys that can be requested.
404
405 Raises:
406 InvalidRequesterException if the signature of the requester can not be
407 verified.
408 """
409 verified = self.gpg.verify(requester_signature)
410 if not verified:
411 raise InvalidRequesterException(
412 'Invalid requester, check your signing key')
413
414 project = self._FetchProjectByRequesterKeyFingerprint(verified.fingerprint)
415
416 self.db_cursor.execute(
417 'SELECT COUNT(*) AS available_key_count FROM drm_keys '
418 'WHERE project_name = ? AND device_serial_number IS NULL',
419 (project['name'],))
420 return self.db_cursor.fetchone()['available_key_count']
421
422 def Request(self, encrypted_device_serial_number):
423 """Requests a DRM key by device serial number.
424
425 Args:
426 encrypted_device_serial_number: the device serial number signed by the
427 requester and encrypted by the server's public key.
428
429 Raises:
430 InvalidRequesterException if the signature of the requester can not be
431 verified. RuntimeError if no available keys left in the database.
432 """
433 decrypted_obj = self.gpg.decrypt(encrypted_device_serial_number)
434 project = self._FetchProjectByRequesterKeyFingerprint(
435 decrypted_obj.fingerprint)
436 device_serial_number = decrypted_obj.data
437
438 def FetchDRMKeyByDeviceSerialNumber(project_name, device_serial_number):
439 self.db_cursor.execute(
440 'SELECT * FROM drm_keys WHERE project_name = ? AND '
441 'device_serial_number = ?',
442 (project_name, device_serial_number))
443 return self.db_cursor.fetchone()
444
445 row = FetchDRMKeyByDeviceSerialNumber(project['name'], device_serial_number)
446 if row: # the SN has already paired
447 return row['encrypted_drm_key']
448
449 # Find an unpaired key.
450 with self.db_connection:
451 # SQLite3 does not support using LIMIT clause in UPDATE statement by
452 # default, unless SQLITE_ENABLE_UPDATE_DELETE_LIMIT flag is defined during
453 # compilation. Since this script may be deployed on partner's computer,
454 # we'd better assume they don't have this flag on.
455 self.db_cursor.execute(
456 'UPDATE drm_keys SET device_serial_number = ? '
457 'WHERE id = (SELECT id FROM drm_keys WHERE project_name = ? AND '
458 ' device_serial_number IS NULL LIMIT 1)',
459 (device_serial_number, project['name']))
460 if self.db_cursor.rowcount != 1: # insufficient keys
461 raise RuntimeError(
462 'Insufficient DRM keys, ask for the OEM to upload more')
463
464 row = FetchDRMKeyByDeviceSerialNumber(project['name'], device_serial_number)
465 if row:
466 return row['encrypted_drm_key']
Fei Shao12ecf382020-06-23 18:32:26 +0800467 raise RuntimeError('Failed to find paired DRM key')
Mao Huang700663d2015-08-12 09:58:59 +0800468
469 def ListenForever(self, ip, port):
470 """Starts the XML RPC server waiting for commands.
471
472 Args:
473 ip: IP to bind.
474 port: port to bind.
475 """
Yilin Yang2a2bb112019-10-23 11:20:33 +0800476 class Server(xmlrpc.server.SimpleXMLRPCServer):
Mao Huang901437f2016-06-24 11:39:15 +0800477 def _dispatch(self, method, params):
478 # Catch exceptions and log them. Without this, SimpleXMLRPCServer simply
479 # output the error message to stdout, and we won't be able to see what
480 # happened in the log file.
481 logging.info('%s called', method)
482 try:
Yilin Yang2a2bb112019-10-23 11:20:33 +0800483 result = xmlrpc.server.SimpleXMLRPCServer._dispatch(
Mao Huang901437f2016-06-24 11:39:15 +0800484 self, method, params)
Mao Huang901437f2016-06-24 11:39:15 +0800485 return result
486 except BaseException as e:
487 logging.exception(e)
488 raise
489
490 server = Server((ip, port), allow_none=True)
Mao Huang700663d2015-08-12 09:58:59 +0800491
492 server.register_introspection_functions()
493 server.register_function(self.AvailableKeyCount)
494 server.register_function(self.Upload)
495 server.register_function(self.Request)
496
497 server.serve_forever()
498
499 def _ImportGPGKey(self, key_file_path):
500 """Imports a GPG key from a file.
501
502 Args:
503 key_file_path: path to the GPG key file.
504
505 Returns:
506 A tuple (key_fingerprint, key_already_exists). The 1st element is the
507 imported key's fingerprint, and the 2nd element is True if the key was
508 already in the database before importing, False otherwise.
509 """
510 with open(key_file_path) as f:
511 import_results = self.gpg.import_keys(f.read())
chuntsenf0780db2019-05-03 02:21:59 +0800512 key_already_exists = (import_results.imported == 0)
Mao Huang700663d2015-08-12 09:58:59 +0800513 key_fingerprint = import_results.fingerprints[0]
514 return (key_fingerprint, key_already_exists)
515
516 def _LoadFilterModule(self, filter_module_file_name):
517 """Loads the filter module.
518
519 Args:
Mao Huang9076f632015-09-24 17:38:47 +0800520 filter_module_file_name: file name of the filter module in FILTERS_DIR.
Mao Huang700663d2015-08-12 09:58:59 +0800521
522 Returns:
523 The loaded filter module on success.
524
525 Raises:
526 Exception if failed, see imp.load_source()'s doc for what could be raised.
527 """
528 return imp.load_source(
529 'filter_module', os.path.join(FILTERS_DIR, filter_module_file_name))
530
Mao Huang9076f632015-09-24 17:38:47 +0800531 def _LoadParserModule(self, parser_module_file_name):
532 """Loads the parser module.
533
534 Args:
535 parser_module_file_name: file name of the parser module in PARSERS_DIR.
536
537 Returns:
538 The loaded parser module on success.
539
540 Raises:
541 Exception if failed, see imp.load_source()'s doc for what could be raised.
542 """
543 return imp.load_source(
544 'parser_module', os.path.join(PARSERS_DIR, parser_module_file_name))
545
Mao Huang700663d2015-08-12 09:58:59 +0800546 def _FetchServerKeyFingerprint(self):
547 """Returns the server GPG key's fingerprint."""
548 self.db_cursor.execute(
549 "SELECT * FROM settings WHERE key = 'server_key_fingerprint'")
550 row = self.db_cursor.fetchone()
551 if not row:
552 raise ValueError('Server key fingerprint not exists')
553 return row['value']
554
Peter Shih86430492018-02-26 14:51:58 +0800555 def _FetchOneProject(self, name=None,
556 uploader_key_fingerprint=None,
557 requester_key_fingerprint=None,
Mao Huang700663d2015-08-12 09:58:59 +0800558 exception_type=None, error_msg=None):
559 """Fetches the project by name, uploader key fingerprint, or requester key
560 fingerprint.
561
562 This function combines the name, uploader_key_fingerprint,
563 requester_key_fingerprint conditions (if not None) with the AND operator,
564 and tries to fetch one project from the database.
565
566 Args:
567 name: name of the project.
568 uploader_key_fingerprint: uploader key fingerprint of the project.
569 requester_key_fingerprint: requester key fingerprint of the project.
570 exception_type: if no project was found and exception_type is not None,
571 raise exception_type with error_msg.
572 error_msg: if no project was found and exception_type is not None, raise
573 exception_type with error_msg.
574
575 Returns:
576 A project that matches the name, uploader_key_fingerprint, and
577 requester_key_fingerprint conditiions.
578
579 Raises:
580 exception_type with error_msg if not project was found.
581 """
Peter Shih86430492018-02-26 14:51:58 +0800582 # pylint: disable=unused-argument
Mao Huang700663d2015-08-12 09:58:59 +0800583 where_clause_list = []
584 params = []
585 local_vars = locals()
586 for param_name in ['name', 'uploader_key_fingerprint',
587 'requester_key_fingerprint']:
588 if local_vars[param_name] is not None:
589 where_clause_list.append('%s = ?' % param_name)
590 params.append(locals()[param_name])
591 if not where_clause_list:
592 raise ValueError('No conditions given to fetch the project')
593 where_clause = 'WHERE ' + ' AND '.join(where_clause_list)
594
595 self.db_cursor.execute(
596 'SELECT * FROM projects %s' % where_clause, tuple(params))
597 project = self.db_cursor.fetchone()
598
599 if not project and exception_type:
600 raise exception_type(error_msg)
601
602 return project
603
604 def _FetchProjectByName(self, name):
605 return self._FetchOneProject(
606 name=name, exception_type=ProjectNotFoundException,
607 error_msg=('Project %s not found' % name))
608
609 def _FetchProjectByUploaderKeyFingerprint(self, uploader_key_fingerprint):
610 return self._FetchOneProject(
611 uploader_key_fingerprint=uploader_key_fingerprint,
612 exception_type=InvalidUploaderException,
613 error_msg='Invalid uploader, check your signing key')
614
615 def _FetchProjectByRequesterKeyFingerprint(self, requester_key_fingerprint):
616 return self._FetchOneProject(
617 requester_key_fingerprint=requester_key_fingerprint,
618 exception_type=InvalidRequesterException,
619 error_msg='Invalid requester, check your signing key')
620
621
622def _ParseArguments():
623 parser = argparse.ArgumentParser()
624 parser.add_argument(
Mao Huang901437f2016-06-24 11:39:15 +0800625 '-d', '--database_file_path',
626 default=os.path.join(SCRIPT_DIR, DEFAULT_DATABASE_FILE_NAME),
Mao Huang700663d2015-08-12 09:58:59 +0800627 help='path to the SQLite3 database file, default to "dkps.db" in the '
628 'same directory of this script')
629 parser.add_argument(
Mao Huang901437f2016-06-24 11:39:15 +0800630 '-g', '--gnupg_homedir',
631 default=os.path.join(SCRIPT_DIR, DEFAULT_GNUPG_DIR_NAME),
Mao Huang700663d2015-08-12 09:58:59 +0800632 help='path to the GnuGP home directory, default to "gnupg" in the same '
633 'directory of this script')
Mao Huang901437f2016-06-24 11:39:15 +0800634 parser.add_argument(
635 '-l', '--log_file_path',
636 default=os.path.join(SCRIPT_DIR, DEFAULT_LOG_FILE_NAME),
637 help='path to the log file, default to "dkps.log" in the same directory '
638 'of this script')
Mao Huang700663d2015-08-12 09:58:59 +0800639 subparsers = parser.add_subparsers(dest='command')
640
641 parser_add = subparsers.add_parser('add', help='adds a new project')
642 parser_add.add_argument('-n', '--name', required=True,
643 help='name of the new project')
644 parser_add.add_argument('-u', '--uploader_key_file_path', required=True,
645 help="path to the uploader's public key file")
646 parser_add.add_argument('-r', '--requester_key_file_path', required=True,
647 help="path to the requester's public key file")
Mao Huang9076f632015-09-24 17:38:47 +0800648 parser_add.add_argument('-p', '--parser_module_file_name', required=True,
649 help='file name of the parser module')
Mao Huang700663d2015-08-12 09:58:59 +0800650 parser_add.add_argument('-f', '--filter_module_file_name', default=None,
651 help='file name of the filter module')
652
653 subparsers.add_parser('destroy', help='destroys the database')
654
655 parser_update = subparsers.add_parser('update',
656 help='updates an existing project')
657 parser_update.add_argument('-n', '--name', required=True,
658 help='name of the project')
659 parser_update.add_argument('-u', '--uploader_key_file_path', default=None,
660 help="path to the uploader's public key file")
661 parser_update.add_argument('-r', '--requester_key_file_path', default=None,
662 help="path to the requester's public key file")
663 parser_update.add_argument('-f', '--filter_module_file_name', default=None,
664 help='file name of the filter module')
665
666 parser_init = subparsers.add_parser('init', help='initializes the database')
667 parser_init.add_argument(
668 '-g', '--gpg_gen_key_args', action='append', nargs=2, default={},
669 help='arguments to use when generating GPG key for server')
Mao Huang24be1382016-06-23 17:53:57 +0800670 parser_init.add_argument(
671 '-s', '--server_key_file_path', default=None,
672 help="path to the server's private key file")
Mao Huang700663d2015-08-12 09:58:59 +0800673
674 subparsers.add_parser('list', help='lists all projects')
675
676 parser_listen = subparsers.add_parser(
677 'listen', help='starts the server, waiting for upload or request keys')
678 parser_listen.add_argument(
Mao Huang901437f2016-06-24 11:39:15 +0800679 '--ip', default=DEFAULT_BIND_ADDR,
680 help='IP to bind, default to %s' % DEFAULT_BIND_ADDR)
Mao Huang700663d2015-08-12 09:58:59 +0800681 parser_listen.add_argument(
Mao Huang901437f2016-06-24 11:39:15 +0800682 '--port', type=int, default=DEFAULT_BIND_PORT,
683 help='port to listen, default to %s' % DEFAULT_BIND_PORT)
Mao Huang700663d2015-08-12 09:58:59 +0800684
685 parser_rm = subparsers.add_parser('rm', help='removes an existing project')
686 parser_rm.add_argument('-n', '--name', required=True,
687 help='name of the project to remove')
688
689 return parser.parse_args()
690
691
692def main():
693 args = _ParseArguments()
694
Mao Huang901437f2016-06-24 11:39:15 +0800695 logging_config = DEFAULT_LOGGING_CONFIG
696 logging_config['handlers']['file']['filename'] = args.log_file_path
697 logging.config.dictConfig(logging_config)
Mao Huang041483e2015-09-14 23:28:18 +0800698
Mao Huang700663d2015-08-12 09:58:59 +0800699 dkps = DRMKeysProvisioningServer(args.database_file_path, args.gnupg_homedir)
700 if args.command == 'init':
701 # Convert from command line arguments to a dict.
702 gpg_gen_key_args_dict = {}
703 for pair in args.gpg_gen_key_args:
704 gpg_gen_key_args_dict[pair[0]] = pair[1]
Mao Huang24be1382016-06-23 17:53:57 +0800705 dkps.Initialize(gpg_gen_key_args_dict, args.server_key_file_path)
Mao Huang700663d2015-08-12 09:58:59 +0800706 elif args.command == 'destroy':
707 message = (
708 'This action will remove all projects and keys information and is NOT '
709 'recoverable! Are you sure? (y/N)')
Yilin Yang8cc5dfb2019-10-22 15:58:53 +0800710 answer = input(textwrap.fill(message, 80) + ' ')
Mao Huang700663d2015-08-12 09:58:59 +0800711 if answer.lower() != 'y' and answer.lower() != 'yes':
Yilin Yang71e39412019-09-24 09:26:46 +0800712 print('OK, nothing will be removed.')
Mao Huang700663d2015-08-12 09:58:59 +0800713 else:
Yilin Yang71e39412019-09-24 09:26:46 +0800714 print('Removing all projects and keys information...', end=' ')
Mao Huang700663d2015-08-12 09:58:59 +0800715 dkps.Destroy()
Yilin Yang71e39412019-09-24 09:26:46 +0800716 print('done.')
Mao Huang700663d2015-08-12 09:58:59 +0800717 elif args.command == 'listen':
718 dkps.ListenForever(args.ip, args.port)
719 elif args.command == 'list':
Yilin Yang71e39412019-09-24 09:26:46 +0800720 print(dkps.ListProjects())
Mao Huang700663d2015-08-12 09:58:59 +0800721 elif args.command == 'add':
722 dkps.AddProject(
723 args.name, args.uploader_key_file_path, args.requester_key_file_path,
Mao Huangecbeb122016-06-22 20:30:38 +0800724 args.parser_module_file_name, args.filter_module_file_name)
Mao Huang700663d2015-08-12 09:58:59 +0800725 elif args.command == 'update':
726 dkps.UpdateProject(
727 args.name, args.uploader_key_file_path, args.requester_key_file_path,
728 args.filter_module_file_name)
729 elif args.command == 'rm':
730 dkps.RemoveProject(args.name)
731 else:
732 raise ValueError('Unknown command %s' % args.command)
733
734
735if __name__ == '__main__':
736 main()