blob: 02e4a3bed0f2b1a0967b6976a1ddfb748c5f45fa [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
Yilin Yang71e39412019-09-24 09:26:46 +080011from __future__ import print_function
12
Mao Huang700663d2015-08-12 09:58:59 +080013import argparse
Mao Huang9076f632015-09-24 17:38:47 +080014import hashlib
Mao Huang700663d2015-08-12 09:58:59 +080015import imp
16import json
Mao Huang041483e2015-09-14 23:28:18 +080017import logging
Mao Huang901437f2016-06-24 11:39:15 +080018import logging.config
Mao Huang700663d2015-08-12 09:58:59 +080019import os
20import shutil
Mao Huang700663d2015-08-12 09:58:59 +080021import sqlite3
22import textwrap
Yilin Yang2a2bb112019-10-23 11:20:33 +080023import xmlrpc.server
Mao Huang700663d2015-08-12 09:58:59 +080024
25import gnupg
Yilin Yang8cc5dfb2019-10-22 15:58:53 +080026from six.moves import input
Mao Huang700663d2015-08-12 09:58:59 +080027
28
29SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
30FILTERS_DIR = os.path.join(SCRIPT_DIR, 'filters')
Mao Huang9076f632015-09-24 17:38:47 +080031PARSERS_DIR = os.path.join(SCRIPT_DIR, 'parsers')
Mao Huang700663d2015-08-12 09:58:59 +080032CREATE_DATABASE_SQL_FILE_PATH = os.path.join(
33 SCRIPT_DIR, 'sql', 'create_database.sql')
34
Mao Huang901437f2016-06-24 11:39:15 +080035DEFAULT_BIND_ADDR = '0.0.0.0' # all addresses
36DEFAULT_BIND_PORT = 5438
37
38DEFAULT_DATABASE_FILE_NAME = 'dkps.db'
39DEFAULT_GNUPG_DIR_NAME = 'gnupg'
40DEFAULT_LOG_FILE_NAME = 'dkps.log'
41
42DEFAULT_LOGGING_CONFIG = {
43 'version': 1,
44 'formatters': {
45 'default': {
46 'format': '%(asctime)s:%(levelname)s:%(funcName)s:'
47 '%(lineno)d:%(message)s'}},
48 'handlers': {
49 'file': {
50 'class': 'logging.handlers.RotatingFileHandler',
51 'formatter': 'default',
52 'filename': DEFAULT_LOG_FILE_NAME,
53 'maxBytes': 1024 * 1024, # 1M
54 'backupCount': 3},
55 'console': {
56 'class': 'logging.StreamHandler',
57 'formatter': 'default',
58 'stream': 'ext://sys.stdout'}},
59 'root': {
60 'level': 'INFO',
61 # only log to file by default, but also log to console if invoked
62 # directly from the command line
63 'handlers': ['file'] + ['console'] if __name__ == '__main__' else []}}
64
Mao Huang700663d2015-08-12 09:58:59 +080065
66class ProjectNotFoundException(ValueError):
67 """Raised when no project was found in the database."""
68 pass
69
70
71class InvalidUploaderException(ValueError):
72 """Raised when the signature of the uploader can't be verified."""
73 pass
74
75
76class InvalidRequesterException(ValueError):
77 """Raised when the signature of the requester can't be verified."""
78 pass
79
80
81def GetSQLite3Connection(database_file_path):
82 """Returns a tuple of SQLite3's (connection, cursor) to database_file_path.
83
84 If the connection has been created before, it is returned directly. If it's
85 not, this function creates the connection, ensures that the foreign key
86 constraint is enabled, and returns.
87
88 Args:
89 database_file_path: path to the SQLite3 database file.
90 """
91 database_file_path = os.path.realpath(database_file_path)
92
93 # Return if the connection to database_file_path has been created before.
94 try:
95 connection = GetSQLite3Connection.connection_dict[database_file_path]
96 return (connection, connection.cursor())
97 except KeyError:
98 pass
99 except AttributeError:
100 GetSQLite3Connection.connection_dict = {}
101
102 # Create connection.
103 connection = sqlite3.connect(database_file_path)
104 connection.row_factory = sqlite3.Row
105 cursor = connection.cursor()
106
107 # Enable foreign key constraint since SQLite3 disables it by default.
108 cursor.execute('PRAGMA foreign_keys = ON')
109 # Check if foreign key constraint is enabled.
110 cursor.execute('PRAGMA foreign_keys')
111 if cursor.fetchone()[0] != 1:
112 raise RuntimeError('Failed to enable SQLite3 foreign key constraint')
113
114 GetSQLite3Connection.connection_dict[database_file_path] = connection
115
116 return (connection, cursor)
117
118
119class DRMKeysProvisioningServer(object):
120 """The DRM Keys Provisioning Server (DKPS) class."""
121
122 def __init__(self, database_file_path, gnupg_homedir):
123 """DKPS constructor.
124
125 Args:
126 database_file_path: path to the SQLite3 database file.
127 gnupg_homedir: path to the GnuPG home directory.
128 """
129 self.database_file_path = database_file_path
130 self.gnupg_homedir = gnupg_homedir
131
132 if not os.path.isdir(self.gnupg_homedir):
133 self.gpg = None
134 else:
chuntsenf0780db2019-05-03 02:21:59 +0800135 self.gpg = gnupg.GPG(gnupghome=self.gnupg_homedir)
Mao Huang700663d2015-08-12 09:58:59 +0800136
137 if not os.path.isfile(self.database_file_path):
138 self.db_connection, self.db_cursor = (None, None)
139 else:
140 self.db_connection, self.db_cursor = GetSQLite3Connection(
141 self.database_file_path)
142
Mao Huang3963b372015-12-11 15:20:19 +0800143 def Initialize(self, gpg_gen_key_args_dict=None, server_key_file_path=None):
144 """Creates the SQLite3 database and GnuPG home, and imports, or generates a
145 GPG key for the server to use.
Mao Huang700663d2015-08-12 09:58:59 +0800146
147 Args:
148 gpg_gen_key_args_dict: will be passed directly as the keyword arguments to
Mao Huang3963b372015-12-11 15:20:19 +0800149 python-gnupg's gen_key() function if server_key_file_path is None.
150 Can be used to customize the key generator process, such as key_type,
151 key_length, etc. See python-gnupg's doc for what can be customized.
152 server_key_file_path: path to the server key to use. If not None, the
153 system will simply import this key and use it as the server key; if
154 None, the system will generate a new key.
Mao Huang700663d2015-08-12 09:58:59 +0800155
156 Raises:
157 RuntimeError is the database and GnuPG home have already been initialized.
158 """
Mao Huang700663d2015-08-12 09:58:59 +0800159 # Create GPG instance and database connection.
chuntsenf0780db2019-05-03 02:21:59 +0800160 self.gpg = gnupg.GPG(gnupghome=self.gnupg_homedir)
Mao Huang700663d2015-08-12 09:58:59 +0800161 self.db_connection, self.db_cursor = GetSQLite3Connection(
162 self.database_file_path)
163
Mao Huang3963b372015-12-11 15:20:19 +0800164 # If any key exists, the system has already been initialized.
165 if self.gpg.list_keys():
166 raise RuntimeError('Already initialized')
Mao Huang700663d2015-08-12 09:58:59 +0800167
Mao Huang3963b372015-12-11 15:20:19 +0800168 if server_key_file_path: # use existing key
Mao Huang901437f2016-06-24 11:39:15 +0800169 # TODO(littlecvr): make sure the server key doesn't have passphrase.
Mao Huang3963b372015-12-11 15:20:19 +0800170 server_key_fingerprint, _ = self._ImportGPGKey(server_key_file_path)
171 else: # generate a new GPG key
172 if gpg_gen_key_args_dict is None:
173 gpg_gen_key_args_dict = {}
174 if 'name_real' not in gpg_gen_key_args_dict:
175 gpg_gen_key_args_dict['name_real'] = 'DKPS Server'
176 if 'name_email' not in gpg_gen_key_args_dict:
177 gpg_gen_key_args_dict['name_email'] = 'chromeos-factory-dkps@google.com'
178 if 'name_comment' not in gpg_gen_key_args_dict:
179 gpg_gen_key_args_dict['name_comment'] = 'DRM Keys Provisioning Server'
180 key_input_data = self.gpg.gen_key_input(**gpg_gen_key_args_dict)
181 server_key_fingerprint = self.gpg.gen_key(key_input_data).fingerprint
Mao Huang700663d2015-08-12 09:58:59 +0800182
183 # Create and set up the schema of the database.
184 with open(CREATE_DATABASE_SQL_FILE_PATH) as f:
185 create_database_sql = f.read()
186 with self.db_connection:
187 self.db_cursor.executescript(create_database_sql)
188
189 # Record the server key fingerprint.
190 with self.db_connection:
191 self.db_cursor.execute(
192 'INSERT INTO settings (key, value) VALUES (?, ?)',
Mao Huang3963b372015-12-11 15:20:19 +0800193 ('server_key_fingerprint', server_key_fingerprint))
Mao Huang700663d2015-08-12 09:58:59 +0800194
195 def Destroy(self):
196 """Destroys the database and GnuPG home directory.
197
198 This is the opposite of Initialize(). It essentially removes the SQLite3
199 database file and GnuPG home directory.
200 """
201 # Remove database.
202 if self.db_connection:
203 self.db_connection.close()
204 if os.path.exists(self.database_file_path):
205 os.remove(self.database_file_path)
206
207 # Remove GnuPG home.
208 if self.gpg:
209 self.gpg = None
210 if os.path.exists(self.gnupg_homedir):
211 shutil.rmtree(self.gnupg_homedir)
212
213 def AddProject(self, name, uploader_key_file_path, requester_key_file_path,
Mao Huang9076f632015-09-24 17:38:47 +0800214 parser_module_file_name, filter_module_file_name=None):
Mao Huang700663d2015-08-12 09:58:59 +0800215 """Adds a project.
216
217 Args:
218 name: name of the project, must be unique.
219 uploader_key_file_path: path to the OEM's public key file.
220 requester_key_file_path: path to the ODM's public key file.
Mao Huang9076f632015-09-24 17:38:47 +0800221 parser_module_file_name: file name of the parser python module.
Mao Huang700663d2015-08-12 09:58:59 +0800222 filter_module_file_name: file name of the filter python module.
223
224 Raises:
225 ValueError if either the uploader's or requester's key are imported (which
226 means they are used by another project).
227 """
Mao Huang9076f632015-09-24 17:38:47 +0800228 # Try to load the parser and filter modules.
229 self._LoadParserModule(parser_module_file_name)
Mao Huang700663d2015-08-12 09:58:59 +0800230 if filter_module_file_name is not None:
231 self._LoadFilterModule(filter_module_file_name)
232
233 # Try to import uploader and requester keys and add project info into the
234 # database, if failed at any step, delete imported keys.
235 uploader_key_fingerprint, requester_key_fingerprint = (None, None)
236 uploader_key_already_exists, requester_key_already_exists = (False, False)
237 try:
238 uploader_key_fingerprint, uploader_key_already_exists = (
239 self._ImportGPGKey(uploader_key_file_path))
240 if uploader_key_already_exists:
241 raise ValueError('Uploader key already exists')
242 requester_key_fingerprint, requester_key_already_exists = (
243 self._ImportGPGKey(requester_key_file_path))
244 if requester_key_already_exists:
245 raise ValueError('Requester key already exists')
246 with self.db_connection:
247 self.db_cursor.execute(
Mao Huang9076f632015-09-24 17:38:47 +0800248 'INSERT INTO projects ('
249 ' name, uploader_key_fingerprint, requester_key_fingerprint, '
250 ' parser_module_file_name, filter_module_file_name) '
251 'VALUES (?, ?, ?, ?, ?)',
Mao Huang700663d2015-08-12 09:58:59 +0800252 (name, uploader_key_fingerprint, requester_key_fingerprint,
Mao Huang9076f632015-09-24 17:38:47 +0800253 parser_module_file_name, filter_module_file_name))
Mao Huang700663d2015-08-12 09:58:59 +0800254 except BaseException:
255 if not uploader_key_already_exists and uploader_key_fingerprint:
256 self.gpg.delete_keys(uploader_key_fingerprint)
257 if not requester_key_already_exists and requester_key_fingerprint:
258 self.gpg.delete_keys(requester_key_fingerprint)
259 raise
260
261 def UpdateProject(self, name, uploader_key_file_path=None,
262 requester_key_file_path=None, filter_module_file_name=None):
263 """Updates a project.
264
265 Args:
266 name: name of the project, must be unique.
267 uploader_key_file_path: path to the OEM's public key file.
268 requester_key_file_path: path to the ODM's public key file.
269 filter_module_file_name: file name of the filter python module.
270
271 Raises:
272 RuntimeError if SQLite3 can't update the project row (for any reason).
273 """
274 # Try to load the filter module.
275 if filter_module_file_name is not None:
276 self._LoadFilterModule(filter_module_file_name)
277
278 project = self._FetchProjectByName(name)
279
280 # Try to import uploader and requester keys and add project info into the
281 # database, if failed at any step, delete any newly imported keys.
282 uploader_key_fingerprint, requester_key_fingerprint = (None, None)
283 old_uploader_key_fingerprint = project['uploader_key_fingerprint']
284 old_requester_key_fingerprint = project['requester_key_fingerprint']
285 same_uploader_key, same_requester_key = (True, True)
286 try:
287 sql_set_clause_list = ['filter_module_file_name = ?']
288 sql_parameters = [filter_module_file_name]
289
290 if uploader_key_file_path:
291 uploader_key_fingerprint, same_uploader_key = self._ImportGPGKey(
292 uploader_key_file_path)
293 sql_set_clause_list.append('uploader_key_fingerprint = ?')
294 sql_parameters.append(uploader_key_fingerprint)
295
296 if requester_key_file_path:
297 requester_key_fingerprint, same_requester_key = self._ImportGPGKey(
298 uploader_key_file_path)
299 sql_set_clause_list.append('requester_key_fingerprint = ?')
300 sql_parameters.append(requester_key_fingerprint)
301
302 sql_set_clause = ','.join(sql_set_clause_list)
303 sql_parameters.append(name)
304 with self.db_connection:
305 self.db_cursor.execute(
306 'UPDATE projects SET %s WHERE name = ?' % sql_set_clause,
307 tuple(sql_parameters))
308 if self.db_cursor.rowcount != 1:
309 raise RuntimeError('Failed to update project %s' % name)
310 except BaseException:
311 if not same_uploader_key and uploader_key_fingerprint:
312 self.gpg.delete_keys(uploader_key_fingerprint)
313 if not same_requester_key and requester_key_fingerprint:
314 self.gpg.delete_keys(requester_key_fingerprint)
315 raise
316
317 if not same_uploader_key:
318 self.gpg.delete_keys(old_uploader_key_fingerprint)
319 if not same_requester_key:
320 self.gpg.delete_keys(old_requester_key_fingerprint)
321
322 def RemoveProject(self, name):
323 """Removes a project.
324
325 Args:
326 name: the name of the project specified when added.
327 """
328 project = self._FetchProjectByName(name)
329
330 self.gpg.delete_keys(project['uploader_key_fingerprint'])
331 self.gpg.delete_keys(project['requester_key_fingerprint'])
332
333 with self.db_connection:
334 self.db_cursor.execute(
335 'DELETE FROM drm_keys WHERE project_name = ?', (name,))
336 self.db_cursor.execute('DELETE FROM projects WHERE name = ?', (name,))
337
338 def ListProjects(self):
339 """Lists all projects."""
340 self.db_cursor.execute('SELECT * FROM projects ORDER BY name ASC')
341 return self.db_cursor.fetchall()
342
343 def Upload(self, encrypted_serialized_drm_keys):
344 """Uploads a list of DRM keys to the server. This is an atomic operation. It
345 will either succeed and save all the keys, or fail and save no keys.
346
347 Args:
348 encrypted_serialized_drm_keys: the serialized DRM keys signed by the
349 uploader and encrypted by the server's public key.
350
351 Raises:
352 InvalidUploaderException if the signature of the uploader can not be
353 verified.
354 """
355 decrypted_obj = self.gpg.decrypt(encrypted_serialized_drm_keys)
356 project = self._FetchProjectByUploaderKeyFingerprint(
357 decrypted_obj.fingerprint)
358 serialized_drm_keys = decrypted_obj.data
359
Mao Huang9076f632015-09-24 17:38:47 +0800360 # Pass to the parse function.
361 parser_module = self._LoadParserModule(project['parser_module_file_name'])
362 drm_key_list = parser_module.Parse(serialized_drm_keys)
363
364 drm_key_hash_list = []
365 for drm_key in drm_key_list:
366 drm_key_hash_list.append(hashlib.sha1(json.dumps(drm_key)).hexdigest())
367
Mao Huangecbeb122016-06-22 20:30:38 +0800368 # Pass to the filter function if needed.
369 if project['filter_module_file_name']: # filter module can be null
370 filter_module = self._LoadFilterModule(project['filter_module_file_name'])
371 filtered_drm_key_list = filter_module.Filter(drm_key_list)
372 else:
373 # filter module is optional
374 filtered_drm_key_list = drm_key_list
Mao Huang700663d2015-08-12 09:58:59 +0800375
376 # Fetch server key for signing.
377 server_key_fingerprint = self._FetchServerKeyFingerprint()
378
379 # Sign and encrypt each key by server's private key and requester's public
380 # key, respectively.
381 encrypted_serialized_drm_key_list = []
382 requester_key_fingerprint = project['requester_key_fingerprint']
383 for drm_key in filtered_drm_key_list:
384 encrypted_obj = self.gpg.encrypt(
385 json.dumps(drm_key), requester_key_fingerprint,
chuntsenf0780db2019-05-03 02:21:59 +0800386 always_trust=True, sign=server_key_fingerprint)
Mao Huang700663d2015-08-12 09:58:59 +0800387 encrypted_serialized_drm_key_list.append(encrypted_obj.data)
388
389 # Insert into the database.
390 with self.db_connection:
391 self.db_cursor.executemany(
Mao Huang9076f632015-09-24 17:38:47 +0800392 'INSERT INTO drm_keys ('
393 ' project_name, drm_key_hash, encrypted_drm_key) '
394 'VALUES (?, ?, ?)',
Yilin Yang7c865822019-11-01 14:50:26 +0800395 list(zip([project['name']] * len(encrypted_serialized_drm_key_list),
396 drm_key_hash_list, encrypted_serialized_drm_key_list)))
Mao Huang700663d2015-08-12 09:58:59 +0800397
398 def AvailableKeyCount(self, requester_signature):
399 """Queries the number of remaining keys.
400
401 Args:
402 requester_signature: a message signed by the requester. Since the server
403 doesn't need any additional info from the requester, the requester can
404 simply sign a random string and send it here.
405
406 Returns:
407 The number of remaining keys that can be requested.
408
409 Raises:
410 InvalidRequesterException if the signature of the requester can not be
411 verified.
412 """
413 verified = self.gpg.verify(requester_signature)
414 if not verified:
415 raise InvalidRequesterException(
416 'Invalid requester, check your signing key')
417
418 project = self._FetchProjectByRequesterKeyFingerprint(verified.fingerprint)
419
420 self.db_cursor.execute(
421 'SELECT COUNT(*) AS available_key_count FROM drm_keys '
422 'WHERE project_name = ? AND device_serial_number IS NULL',
423 (project['name'],))
424 return self.db_cursor.fetchone()['available_key_count']
425
426 def Request(self, encrypted_device_serial_number):
427 """Requests a DRM key by device serial number.
428
429 Args:
430 encrypted_device_serial_number: the device serial number signed by the
431 requester and encrypted by the server's public key.
432
433 Raises:
434 InvalidRequesterException if the signature of the requester can not be
435 verified. RuntimeError if no available keys left in the database.
436 """
437 decrypted_obj = self.gpg.decrypt(encrypted_device_serial_number)
438 project = self._FetchProjectByRequesterKeyFingerprint(
439 decrypted_obj.fingerprint)
440 device_serial_number = decrypted_obj.data
441
442 def FetchDRMKeyByDeviceSerialNumber(project_name, device_serial_number):
443 self.db_cursor.execute(
444 'SELECT * FROM drm_keys WHERE project_name = ? AND '
445 'device_serial_number = ?',
446 (project_name, device_serial_number))
447 return self.db_cursor.fetchone()
448
449 row = FetchDRMKeyByDeviceSerialNumber(project['name'], device_serial_number)
450 if row: # the SN has already paired
451 return row['encrypted_drm_key']
452
453 # Find an unpaired key.
454 with self.db_connection:
455 # SQLite3 does not support using LIMIT clause in UPDATE statement by
456 # default, unless SQLITE_ENABLE_UPDATE_DELETE_LIMIT flag is defined during
457 # compilation. Since this script may be deployed on partner's computer,
458 # we'd better assume they don't have this flag on.
459 self.db_cursor.execute(
460 'UPDATE drm_keys SET device_serial_number = ? '
461 'WHERE id = (SELECT id FROM drm_keys WHERE project_name = ? AND '
462 ' device_serial_number IS NULL LIMIT 1)',
463 (device_serial_number, project['name']))
464 if self.db_cursor.rowcount != 1: # insufficient keys
465 raise RuntimeError(
466 'Insufficient DRM keys, ask for the OEM to upload more')
467
468 row = FetchDRMKeyByDeviceSerialNumber(project['name'], device_serial_number)
469 if row:
470 return row['encrypted_drm_key']
471 else:
472 raise RuntimeError('Failed to find paired DRM key')
473
474 def ListenForever(self, ip, port):
475 """Starts the XML RPC server waiting for commands.
476
477 Args:
478 ip: IP to bind.
479 port: port to bind.
480 """
Yilin Yang2a2bb112019-10-23 11:20:33 +0800481 class Server(xmlrpc.server.SimpleXMLRPCServer):
Mao Huang901437f2016-06-24 11:39:15 +0800482 def _dispatch(self, method, params):
483 # Catch exceptions and log them. Without this, SimpleXMLRPCServer simply
484 # output the error message to stdout, and we won't be able to see what
485 # happened in the log file.
486 logging.info('%s called', method)
487 try:
Yilin Yang2a2bb112019-10-23 11:20:33 +0800488 result = xmlrpc.server.SimpleXMLRPCServer._dispatch(
Mao Huang901437f2016-06-24 11:39:15 +0800489 self, method, params)
Mao Huang901437f2016-06-24 11:39:15 +0800490 return result
491 except BaseException as e:
492 logging.exception(e)
493 raise
494
495 server = Server((ip, port), allow_none=True)
Mao Huang700663d2015-08-12 09:58:59 +0800496
497 server.register_introspection_functions()
498 server.register_function(self.AvailableKeyCount)
499 server.register_function(self.Upload)
500 server.register_function(self.Request)
501
502 server.serve_forever()
503
504 def _ImportGPGKey(self, key_file_path):
505 """Imports a GPG key from a file.
506
507 Args:
508 key_file_path: path to the GPG key file.
509
510 Returns:
511 A tuple (key_fingerprint, key_already_exists). The 1st element is the
512 imported key's fingerprint, and the 2nd element is True if the key was
513 already in the database before importing, False otherwise.
514 """
515 with open(key_file_path) as f:
516 import_results = self.gpg.import_keys(f.read())
chuntsenf0780db2019-05-03 02:21:59 +0800517 key_already_exists = (import_results.imported == 0)
Mao Huang700663d2015-08-12 09:58:59 +0800518 key_fingerprint = import_results.fingerprints[0]
519 return (key_fingerprint, key_already_exists)
520
521 def _LoadFilterModule(self, filter_module_file_name):
522 """Loads the filter module.
523
524 Args:
Mao Huang9076f632015-09-24 17:38:47 +0800525 filter_module_file_name: file name of the filter module in FILTERS_DIR.
Mao Huang700663d2015-08-12 09:58:59 +0800526
527 Returns:
528 The loaded filter module on success.
529
530 Raises:
531 Exception if failed, see imp.load_source()'s doc for what could be raised.
532 """
533 return imp.load_source(
534 'filter_module', os.path.join(FILTERS_DIR, filter_module_file_name))
535
Mao Huang9076f632015-09-24 17:38:47 +0800536 def _LoadParserModule(self, parser_module_file_name):
537 """Loads the parser module.
538
539 Args:
540 parser_module_file_name: file name of the parser module in PARSERS_DIR.
541
542 Returns:
543 The loaded parser module on success.
544
545 Raises:
546 Exception if failed, see imp.load_source()'s doc for what could be raised.
547 """
548 return imp.load_source(
549 'parser_module', os.path.join(PARSERS_DIR, parser_module_file_name))
550
Mao Huang700663d2015-08-12 09:58:59 +0800551 def _FetchServerKeyFingerprint(self):
552 """Returns the server GPG key's fingerprint."""
553 self.db_cursor.execute(
554 "SELECT * FROM settings WHERE key = 'server_key_fingerprint'")
555 row = self.db_cursor.fetchone()
556 if not row:
557 raise ValueError('Server key fingerprint not exists')
558 return row['value']
559
Peter Shih86430492018-02-26 14:51:58 +0800560 def _FetchOneProject(self, name=None,
561 uploader_key_fingerprint=None,
562 requester_key_fingerprint=None,
Mao Huang700663d2015-08-12 09:58:59 +0800563 exception_type=None, error_msg=None):
564 """Fetches the project by name, uploader key fingerprint, or requester key
565 fingerprint.
566
567 This function combines the name, uploader_key_fingerprint,
568 requester_key_fingerprint conditions (if not None) with the AND operator,
569 and tries to fetch one project from the database.
570
571 Args:
572 name: name of the project.
573 uploader_key_fingerprint: uploader key fingerprint of the project.
574 requester_key_fingerprint: requester key fingerprint of the project.
575 exception_type: if no project was found and exception_type is not None,
576 raise exception_type with error_msg.
577 error_msg: if no project was found and exception_type is not None, raise
578 exception_type with error_msg.
579
580 Returns:
581 A project that matches the name, uploader_key_fingerprint, and
582 requester_key_fingerprint conditiions.
583
584 Raises:
585 exception_type with error_msg if not project was found.
586 """
Peter Shih86430492018-02-26 14:51:58 +0800587 # pylint: disable=unused-argument
Mao Huang700663d2015-08-12 09:58:59 +0800588 where_clause_list = []
589 params = []
590 local_vars = locals()
591 for param_name in ['name', 'uploader_key_fingerprint',
592 'requester_key_fingerprint']:
593 if local_vars[param_name] is not None:
594 where_clause_list.append('%s = ?' % param_name)
595 params.append(locals()[param_name])
596 if not where_clause_list:
597 raise ValueError('No conditions given to fetch the project')
598 where_clause = 'WHERE ' + ' AND '.join(where_clause_list)
599
600 self.db_cursor.execute(
601 'SELECT * FROM projects %s' % where_clause, tuple(params))
602 project = self.db_cursor.fetchone()
603
604 if not project and exception_type:
605 raise exception_type(error_msg)
606
607 return project
608
609 def _FetchProjectByName(self, name):
610 return self._FetchOneProject(
611 name=name, exception_type=ProjectNotFoundException,
612 error_msg=('Project %s not found' % name))
613
614 def _FetchProjectByUploaderKeyFingerprint(self, uploader_key_fingerprint):
615 return self._FetchOneProject(
616 uploader_key_fingerprint=uploader_key_fingerprint,
617 exception_type=InvalidUploaderException,
618 error_msg='Invalid uploader, check your signing key')
619
620 def _FetchProjectByRequesterKeyFingerprint(self, requester_key_fingerprint):
621 return self._FetchOneProject(
622 requester_key_fingerprint=requester_key_fingerprint,
623 exception_type=InvalidRequesterException,
624 error_msg='Invalid requester, check your signing key')
625
626
627def _ParseArguments():
628 parser = argparse.ArgumentParser()
629 parser.add_argument(
Mao Huang901437f2016-06-24 11:39:15 +0800630 '-d', '--database_file_path',
631 default=os.path.join(SCRIPT_DIR, DEFAULT_DATABASE_FILE_NAME),
Mao Huang700663d2015-08-12 09:58:59 +0800632 help='path to the SQLite3 database file, default to "dkps.db" in the '
633 'same directory of this script')
634 parser.add_argument(
Mao Huang901437f2016-06-24 11:39:15 +0800635 '-g', '--gnupg_homedir',
636 default=os.path.join(SCRIPT_DIR, DEFAULT_GNUPG_DIR_NAME),
Mao Huang700663d2015-08-12 09:58:59 +0800637 help='path to the GnuGP home directory, default to "gnupg" in the same '
638 'directory of this script')
Mao Huang901437f2016-06-24 11:39:15 +0800639 parser.add_argument(
640 '-l', '--log_file_path',
641 default=os.path.join(SCRIPT_DIR, DEFAULT_LOG_FILE_NAME),
642 help='path to the log file, default to "dkps.log" in the same directory '
643 'of this script')
Mao Huang700663d2015-08-12 09:58:59 +0800644 subparsers = parser.add_subparsers(dest='command')
645
646 parser_add = subparsers.add_parser('add', help='adds a new project')
647 parser_add.add_argument('-n', '--name', required=True,
648 help='name of the new project')
649 parser_add.add_argument('-u', '--uploader_key_file_path', required=True,
650 help="path to the uploader's public key file")
651 parser_add.add_argument('-r', '--requester_key_file_path', required=True,
652 help="path to the requester's public key file")
Mao Huang9076f632015-09-24 17:38:47 +0800653 parser_add.add_argument('-p', '--parser_module_file_name', required=True,
654 help='file name of the parser module')
Mao Huang700663d2015-08-12 09:58:59 +0800655 parser_add.add_argument('-f', '--filter_module_file_name', default=None,
656 help='file name of the filter module')
657
658 subparsers.add_parser('destroy', help='destroys the database')
659
660 parser_update = subparsers.add_parser('update',
661 help='updates an existing project')
662 parser_update.add_argument('-n', '--name', required=True,
663 help='name of the project')
664 parser_update.add_argument('-u', '--uploader_key_file_path', default=None,
665 help="path to the uploader's public key file")
666 parser_update.add_argument('-r', '--requester_key_file_path', default=None,
667 help="path to the requester's public key file")
668 parser_update.add_argument('-f', '--filter_module_file_name', default=None,
669 help='file name of the filter module')
670
671 parser_init = subparsers.add_parser('init', help='initializes the database')
672 parser_init.add_argument(
673 '-g', '--gpg_gen_key_args', action='append', nargs=2, default={},
674 help='arguments to use when generating GPG key for server')
Mao Huang24be1382016-06-23 17:53:57 +0800675 parser_init.add_argument(
676 '-s', '--server_key_file_path', default=None,
677 help="path to the server's private key file")
Mao Huang700663d2015-08-12 09:58:59 +0800678
679 subparsers.add_parser('list', help='lists all projects')
680
681 parser_listen = subparsers.add_parser(
682 'listen', help='starts the server, waiting for upload or request keys')
683 parser_listen.add_argument(
Mao Huang901437f2016-06-24 11:39:15 +0800684 '--ip', default=DEFAULT_BIND_ADDR,
685 help='IP to bind, default to %s' % DEFAULT_BIND_ADDR)
Mao Huang700663d2015-08-12 09:58:59 +0800686 parser_listen.add_argument(
Mao Huang901437f2016-06-24 11:39:15 +0800687 '--port', type=int, default=DEFAULT_BIND_PORT,
688 help='port to listen, default to %s' % DEFAULT_BIND_PORT)
Mao Huang700663d2015-08-12 09:58:59 +0800689
690 parser_rm = subparsers.add_parser('rm', help='removes an existing project')
691 parser_rm.add_argument('-n', '--name', required=True,
692 help='name of the project to remove')
693
694 return parser.parse_args()
695
696
697def main():
698 args = _ParseArguments()
699
Mao Huang901437f2016-06-24 11:39:15 +0800700 logging_config = DEFAULT_LOGGING_CONFIG
701 logging_config['handlers']['file']['filename'] = args.log_file_path
702 logging.config.dictConfig(logging_config)
Mao Huang041483e2015-09-14 23:28:18 +0800703
Mao Huang700663d2015-08-12 09:58:59 +0800704 dkps = DRMKeysProvisioningServer(args.database_file_path, args.gnupg_homedir)
705 if args.command == 'init':
706 # Convert from command line arguments to a dict.
707 gpg_gen_key_args_dict = {}
708 for pair in args.gpg_gen_key_args:
709 gpg_gen_key_args_dict[pair[0]] = pair[1]
Mao Huang24be1382016-06-23 17:53:57 +0800710 dkps.Initialize(gpg_gen_key_args_dict, args.server_key_file_path)
Mao Huang700663d2015-08-12 09:58:59 +0800711 elif args.command == 'destroy':
712 message = (
713 'This action will remove all projects and keys information and is NOT '
714 'recoverable! Are you sure? (y/N)')
Yilin Yang8cc5dfb2019-10-22 15:58:53 +0800715 answer = input(textwrap.fill(message, 80) + ' ')
Mao Huang700663d2015-08-12 09:58:59 +0800716 if answer.lower() != 'y' and answer.lower() != 'yes':
Yilin Yang71e39412019-09-24 09:26:46 +0800717 print('OK, nothing will be removed.')
Mao Huang700663d2015-08-12 09:58:59 +0800718 else:
Yilin Yang71e39412019-09-24 09:26:46 +0800719 print('Removing all projects and keys information...', end=' ')
Mao Huang700663d2015-08-12 09:58:59 +0800720 dkps.Destroy()
Yilin Yang71e39412019-09-24 09:26:46 +0800721 print('done.')
Mao Huang700663d2015-08-12 09:58:59 +0800722 elif args.command == 'listen':
723 dkps.ListenForever(args.ip, args.port)
724 elif args.command == 'list':
Yilin Yang71e39412019-09-24 09:26:46 +0800725 print(dkps.ListProjects())
Mao Huang700663d2015-08-12 09:58:59 +0800726 elif args.command == 'add':
727 dkps.AddProject(
728 args.name, args.uploader_key_file_path, args.requester_key_file_path,
Mao Huangecbeb122016-06-22 20:30:38 +0800729 args.parser_module_file_name, args.filter_module_file_name)
Mao Huang700663d2015-08-12 09:58:59 +0800730 elif args.command == 'update':
731 dkps.UpdateProject(
732 args.name, args.uploader_key_file_path, args.requester_key_file_path,
733 args.filter_module_file_name)
734 elif args.command == 'rm':
735 dkps.RemoveProject(args.name)
736 else:
737 raise ValueError('Unknown command %s' % args.command)
738
739
740if __name__ == '__main__':
741 main()