blob: 1418af9e27fc2cd303184d156963da5080aaea44 [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)
Pin-Yen Lin70cb2562021-03-25 18:07:59 +0800175 if 'passphrase' in gpg_gen_key_args_dict:
176 raise RuntimeError('Passphrase for the server key is not supported.')
177 # TODO(treapking): Use no_protection keyword after we upgrade python-gnupg
178 # to 0.4.7, or support passphrase for the server key.
179 key_input_data = key_input_data.replace('%commit\n',
180 '%no-protection\n%commit\n')
Mao Huang3963b372015-12-11 15:20:19 +0800181 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(
Pin-Yen Lin70cb2562021-03-25 18:07:59 +0800298 requester_key_file_path)
Mao Huang700663d2015-08-12 09:58:59 +0800299 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')
Pin-Yen Lin70cb2562021-03-25 18:07:59 +0800341 return [dict(row) for row in self.db_cursor.fetchall()]
Mao Huang700663d2015-08-12 09:58:59 +0800342
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 """
Pin-Yen Lin70cb2562021-03-25 18:07:59 +0800355 decrypted_obj = self.gpg.decrypt(encrypted_serialized_drm_keys.data)
Mao Huang700663d2015-08-12 09:58:59 +0800356 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:
Yilin Yang0412c272019-12-05 16:57:40 +0800366 drm_key_hash = hashlib.sha1(
367 json.dumps(drm_key).encode('utf-8')).hexdigest()
368 drm_key_hash_list.append(drm_key_hash)
Mao Huang9076f632015-09-24 17:38:47 +0800369
Mao Huangecbeb122016-06-22 20:30:38 +0800370 # Pass to the filter function if needed.
371 if project['filter_module_file_name']: # filter module can be null
372 filter_module = self._LoadFilterModule(project['filter_module_file_name'])
373 filtered_drm_key_list = filter_module.Filter(drm_key_list)
374 else:
375 # filter module is optional
376 filtered_drm_key_list = drm_key_list
Mao Huang700663d2015-08-12 09:58:59 +0800377
378 # Fetch server key for signing.
379 server_key_fingerprint = self._FetchServerKeyFingerprint()
380
381 # Sign and encrypt each key by server's private key and requester's public
382 # key, respectively.
383 encrypted_serialized_drm_key_list = []
384 requester_key_fingerprint = project['requester_key_fingerprint']
385 for drm_key in filtered_drm_key_list:
386 encrypted_obj = self.gpg.encrypt(
387 json.dumps(drm_key), requester_key_fingerprint,
chuntsenf0780db2019-05-03 02:21:59 +0800388 always_trust=True, sign=server_key_fingerprint)
Mao Huang700663d2015-08-12 09:58:59 +0800389 encrypted_serialized_drm_key_list.append(encrypted_obj.data)
390
391 # Insert into the database.
392 with self.db_connection:
393 self.db_cursor.executemany(
Mao Huang9076f632015-09-24 17:38:47 +0800394 'INSERT INTO drm_keys ('
395 ' project_name, drm_key_hash, encrypted_drm_key) '
396 'VALUES (?, ?, ?)',
Yilin Yang7c865822019-11-01 14:50:26 +0800397 list(zip([project['name']] * len(encrypted_serialized_drm_key_list),
398 drm_key_hash_list, encrypted_serialized_drm_key_list)))
Mao Huang700663d2015-08-12 09:58:59 +0800399
400 def AvailableKeyCount(self, requester_signature):
401 """Queries the number of remaining keys.
402
403 Args:
404 requester_signature: a message signed by the requester. Since the server
405 doesn't need any additional info from the requester, the requester can
406 simply sign a random string and send it here.
407
408 Returns:
409 The number of remaining keys that can be requested.
410
411 Raises:
412 InvalidRequesterException if the signature of the requester can not be
413 verified.
414 """
Pin-Yen Lin70cb2562021-03-25 18:07:59 +0800415 verified = self.gpg.verify(requester_signature.data)
Mao Huang700663d2015-08-12 09:58:59 +0800416 if not verified:
417 raise InvalidRequesterException(
418 'Invalid requester, check your signing key')
419
420 project = self._FetchProjectByRequesterKeyFingerprint(verified.fingerprint)
421
422 self.db_cursor.execute(
423 'SELECT COUNT(*) AS available_key_count FROM drm_keys '
424 'WHERE project_name = ? AND device_serial_number IS NULL',
425 (project['name'],))
426 return self.db_cursor.fetchone()['available_key_count']
427
428 def Request(self, encrypted_device_serial_number):
429 """Requests a DRM key by device serial number.
430
431 Args:
432 encrypted_device_serial_number: the device serial number signed by the
433 requester and encrypted by the server's public key.
434
435 Raises:
436 InvalidRequesterException if the signature of the requester can not be
437 verified. RuntimeError if no available keys left in the database.
438 """
Pin-Yen Lin70cb2562021-03-25 18:07:59 +0800439 decrypted_obj = self.gpg.decrypt(encrypted_device_serial_number.data)
Mao Huang700663d2015-08-12 09:58:59 +0800440 project = self._FetchProjectByRequesterKeyFingerprint(
441 decrypted_obj.fingerprint)
442 device_serial_number = decrypted_obj.data
443
444 def FetchDRMKeyByDeviceSerialNumber(project_name, device_serial_number):
445 self.db_cursor.execute(
446 'SELECT * FROM drm_keys WHERE project_name = ? AND '
447 'device_serial_number = ?',
448 (project_name, device_serial_number))
449 return self.db_cursor.fetchone()
450
451 row = FetchDRMKeyByDeviceSerialNumber(project['name'], device_serial_number)
452 if row: # the SN has already paired
453 return row['encrypted_drm_key']
454
455 # Find an unpaired key.
456 with self.db_connection:
457 # SQLite3 does not support using LIMIT clause in UPDATE statement by
458 # default, unless SQLITE_ENABLE_UPDATE_DELETE_LIMIT flag is defined during
459 # compilation. Since this script may be deployed on partner's computer,
460 # we'd better assume they don't have this flag on.
461 self.db_cursor.execute(
462 'UPDATE drm_keys SET device_serial_number = ? '
463 'WHERE id = (SELECT id FROM drm_keys WHERE project_name = ? AND '
464 ' device_serial_number IS NULL LIMIT 1)',
465 (device_serial_number, project['name']))
466 if self.db_cursor.rowcount != 1: # insufficient keys
467 raise RuntimeError(
468 'Insufficient DRM keys, ask for the OEM to upload more')
469
470 row = FetchDRMKeyByDeviceSerialNumber(project['name'], device_serial_number)
471 if row:
472 return row['encrypted_drm_key']
Fei Shao12ecf382020-06-23 18:32:26 +0800473 raise RuntimeError('Failed to find paired DRM key')
Mao Huang700663d2015-08-12 09:58:59 +0800474
475 def ListenForever(self, ip, port):
476 """Starts the XML RPC server waiting for commands.
477
478 Args:
479 ip: IP to bind.
480 port: port to bind.
481 """
Yilin Yang2a2bb112019-10-23 11:20:33 +0800482 class Server(xmlrpc.server.SimpleXMLRPCServer):
Mao Huang901437f2016-06-24 11:39:15 +0800483 def _dispatch(self, method, params):
484 # Catch exceptions and log them. Without this, SimpleXMLRPCServer simply
485 # output the error message to stdout, and we won't be able to see what
486 # happened in the log file.
487 logging.info('%s called', method)
488 try:
Yilin Yang2a2bb112019-10-23 11:20:33 +0800489 result = xmlrpc.server.SimpleXMLRPCServer._dispatch(
Mao Huang901437f2016-06-24 11:39:15 +0800490 self, method, params)
Mao Huang901437f2016-06-24 11:39:15 +0800491 return result
492 except BaseException as e:
493 logging.exception(e)
494 raise
495
496 server = Server((ip, port), allow_none=True)
Mao Huang700663d2015-08-12 09:58:59 +0800497
498 server.register_introspection_functions()
499 server.register_function(self.AvailableKeyCount)
500 server.register_function(self.Upload)
501 server.register_function(self.Request)
502
503 server.serve_forever()
504
505 def _ImportGPGKey(self, key_file_path):
506 """Imports a GPG key from a file.
507
508 Args:
509 key_file_path: path to the GPG key file.
510
511 Returns:
512 A tuple (key_fingerprint, key_already_exists). The 1st element is the
513 imported key's fingerprint, and the 2nd element is True if the key was
514 already in the database before importing, False otherwise.
515 """
516 with open(key_file_path) as f:
517 import_results = self.gpg.import_keys(f.read())
chuntsenf0780db2019-05-03 02:21:59 +0800518 key_already_exists = (import_results.imported == 0)
Mao Huang700663d2015-08-12 09:58:59 +0800519 key_fingerprint = import_results.fingerprints[0]
520 return (key_fingerprint, key_already_exists)
521
522 def _LoadFilterModule(self, filter_module_file_name):
523 """Loads the filter module.
524
525 Args:
Mao Huang9076f632015-09-24 17:38:47 +0800526 filter_module_file_name: file name of the filter module in FILTERS_DIR.
Mao Huang700663d2015-08-12 09:58:59 +0800527
528 Returns:
529 The loaded filter module on success.
530
531 Raises:
532 Exception if failed, see imp.load_source()'s doc for what could be raised.
533 """
534 return imp.load_source(
535 'filter_module', os.path.join(FILTERS_DIR, filter_module_file_name))
536
Mao Huang9076f632015-09-24 17:38:47 +0800537 def _LoadParserModule(self, parser_module_file_name):
538 """Loads the parser module.
539
540 Args:
541 parser_module_file_name: file name of the parser module in PARSERS_DIR.
542
543 Returns:
544 The loaded parser module on success.
545
546 Raises:
547 Exception if failed, see imp.load_source()'s doc for what could be raised.
548 """
549 return imp.load_source(
550 'parser_module', os.path.join(PARSERS_DIR, parser_module_file_name))
551
Mao Huang700663d2015-08-12 09:58:59 +0800552 def _FetchServerKeyFingerprint(self):
553 """Returns the server GPG key's fingerprint."""
554 self.db_cursor.execute(
555 "SELECT * FROM settings WHERE key = 'server_key_fingerprint'")
556 row = self.db_cursor.fetchone()
557 if not row:
558 raise ValueError('Server key fingerprint not exists')
559 return row['value']
560
Peter Shih86430492018-02-26 14:51:58 +0800561 def _FetchOneProject(self, name=None,
562 uploader_key_fingerprint=None,
563 requester_key_fingerprint=None,
Mao Huang700663d2015-08-12 09:58:59 +0800564 exception_type=None, error_msg=None):
565 """Fetches the project by name, uploader key fingerprint, or requester key
566 fingerprint.
567
568 This function combines the name, uploader_key_fingerprint,
569 requester_key_fingerprint conditions (if not None) with the AND operator,
570 and tries to fetch one project from the database.
571
572 Args:
573 name: name of the project.
574 uploader_key_fingerprint: uploader key fingerprint of the project.
575 requester_key_fingerprint: requester key fingerprint of the project.
576 exception_type: if no project was found and exception_type is not None,
577 raise exception_type with error_msg.
578 error_msg: if no project was found and exception_type is not None, raise
579 exception_type with error_msg.
580
581 Returns:
582 A project that matches the name, uploader_key_fingerprint, and
583 requester_key_fingerprint conditiions.
584
585 Raises:
586 exception_type with error_msg if not project was found.
587 """
Peter Shih86430492018-02-26 14:51:58 +0800588 # pylint: disable=unused-argument
Mao Huang700663d2015-08-12 09:58:59 +0800589 where_clause_list = []
590 params = []
591 local_vars = locals()
592 for param_name in ['name', 'uploader_key_fingerprint',
593 'requester_key_fingerprint']:
594 if local_vars[param_name] is not None:
595 where_clause_list.append('%s = ?' % param_name)
596 params.append(locals()[param_name])
597 if not where_clause_list:
598 raise ValueError('No conditions given to fetch the project')
599 where_clause = 'WHERE ' + ' AND '.join(where_clause_list)
600
601 self.db_cursor.execute(
602 'SELECT * FROM projects %s' % where_clause, tuple(params))
603 project = self.db_cursor.fetchone()
604
605 if not project and exception_type:
606 raise exception_type(error_msg)
607
608 return project
609
610 def _FetchProjectByName(self, name):
611 return self._FetchOneProject(
612 name=name, exception_type=ProjectNotFoundException,
613 error_msg=('Project %s not found' % name))
614
615 def _FetchProjectByUploaderKeyFingerprint(self, uploader_key_fingerprint):
616 return self._FetchOneProject(
617 uploader_key_fingerprint=uploader_key_fingerprint,
618 exception_type=InvalidUploaderException,
619 error_msg='Invalid uploader, check your signing key')
620
621 def _FetchProjectByRequesterKeyFingerprint(self, requester_key_fingerprint):
622 return self._FetchOneProject(
623 requester_key_fingerprint=requester_key_fingerprint,
624 exception_type=InvalidRequesterException,
625 error_msg='Invalid requester, check your signing key')
626
627
628def _ParseArguments():
629 parser = argparse.ArgumentParser()
630 parser.add_argument(
Mao Huang901437f2016-06-24 11:39:15 +0800631 '-d', '--database_file_path',
632 default=os.path.join(SCRIPT_DIR, DEFAULT_DATABASE_FILE_NAME),
Mao Huang700663d2015-08-12 09:58:59 +0800633 help='path to the SQLite3 database file, default to "dkps.db" in the '
634 'same directory of this script')
635 parser.add_argument(
Mao Huang901437f2016-06-24 11:39:15 +0800636 '-g', '--gnupg_homedir',
637 default=os.path.join(SCRIPT_DIR, DEFAULT_GNUPG_DIR_NAME),
Mao Huang700663d2015-08-12 09:58:59 +0800638 help='path to the GnuGP home directory, default to "gnupg" in the same '
639 'directory of this script')
Mao Huang901437f2016-06-24 11:39:15 +0800640 parser.add_argument(
641 '-l', '--log_file_path',
642 default=os.path.join(SCRIPT_DIR, DEFAULT_LOG_FILE_NAME),
643 help='path to the log file, default to "dkps.log" in the same directory '
644 'of this script')
Mao Huang700663d2015-08-12 09:58:59 +0800645 subparsers = parser.add_subparsers(dest='command')
646
647 parser_add = subparsers.add_parser('add', help='adds a new project')
648 parser_add.add_argument('-n', '--name', required=True,
649 help='name of the new project')
650 parser_add.add_argument('-u', '--uploader_key_file_path', required=True,
651 help="path to the uploader's public key file")
652 parser_add.add_argument('-r', '--requester_key_file_path', required=True,
653 help="path to the requester's public key file")
Mao Huang9076f632015-09-24 17:38:47 +0800654 parser_add.add_argument('-p', '--parser_module_file_name', required=True,
655 help='file name of the parser module')
Mao Huang700663d2015-08-12 09:58:59 +0800656 parser_add.add_argument('-f', '--filter_module_file_name', default=None,
657 help='file name of the filter module')
658
659 subparsers.add_parser('destroy', help='destroys the database')
660
661 parser_update = subparsers.add_parser('update',
662 help='updates an existing project')
663 parser_update.add_argument('-n', '--name', required=True,
664 help='name of the project')
665 parser_update.add_argument('-u', '--uploader_key_file_path', default=None,
666 help="path to the uploader's public key file")
667 parser_update.add_argument('-r', '--requester_key_file_path', default=None,
668 help="path to the requester's public key file")
669 parser_update.add_argument('-f', '--filter_module_file_name', default=None,
670 help='file name of the filter module')
671
672 parser_init = subparsers.add_parser('init', help='initializes the database')
673 parser_init.add_argument(
Pin-Yen Lin70cb2562021-03-25 18:07:59 +0800674 '-g', '--gpg_gen_key_args', action='append', nargs=2, default=[],
Mao Huang700663d2015-08-12 09:58:59 +0800675 help='arguments to use when generating GPG key for server')
Mao Huang24be1382016-06-23 17:53:57 +0800676 parser_init.add_argument(
677 '-s', '--server_key_file_path', default=None,
678 help="path to the server's private key file")
Mao Huang700663d2015-08-12 09:58:59 +0800679
680 subparsers.add_parser('list', help='lists all projects')
681
682 parser_listen = subparsers.add_parser(
683 'listen', help='starts the server, waiting for upload or request keys')
684 parser_listen.add_argument(
Mao Huang901437f2016-06-24 11:39:15 +0800685 '--ip', default=DEFAULT_BIND_ADDR,
686 help='IP to bind, default to %s' % DEFAULT_BIND_ADDR)
Mao Huang700663d2015-08-12 09:58:59 +0800687 parser_listen.add_argument(
Mao Huang901437f2016-06-24 11:39:15 +0800688 '--port', type=int, default=DEFAULT_BIND_PORT,
689 help='port to listen, default to %s' % DEFAULT_BIND_PORT)
Mao Huang700663d2015-08-12 09:58:59 +0800690
691 parser_rm = subparsers.add_parser('rm', help='removes an existing project')
692 parser_rm.add_argument('-n', '--name', required=True,
693 help='name of the project to remove')
694
695 return parser.parse_args()
696
697
698def main():
699 args = _ParseArguments()
700
Mao Huang901437f2016-06-24 11:39:15 +0800701 logging_config = DEFAULT_LOGGING_CONFIG
702 logging_config['handlers']['file']['filename'] = args.log_file_path
703 logging.config.dictConfig(logging_config)
Mao Huang041483e2015-09-14 23:28:18 +0800704
Mao Huang700663d2015-08-12 09:58:59 +0800705 dkps = DRMKeysProvisioningServer(args.database_file_path, args.gnupg_homedir)
706 if args.command == 'init':
707 # Convert from command line arguments to a dict.
708 gpg_gen_key_args_dict = {}
709 for pair in args.gpg_gen_key_args:
710 gpg_gen_key_args_dict[pair[0]] = pair[1]
Mao Huang24be1382016-06-23 17:53:57 +0800711 dkps.Initialize(gpg_gen_key_args_dict, args.server_key_file_path)
Mao Huang700663d2015-08-12 09:58:59 +0800712 elif args.command == 'destroy':
713 message = (
714 'This action will remove all projects and keys information and is NOT '
715 'recoverable! Are you sure? (y/N)')
Yilin Yang8cc5dfb2019-10-22 15:58:53 +0800716 answer = input(textwrap.fill(message, 80) + ' ')
Mao Huang700663d2015-08-12 09:58:59 +0800717 if answer.lower() != 'y' and answer.lower() != 'yes':
Yilin Yang71e39412019-09-24 09:26:46 +0800718 print('OK, nothing will be removed.')
Mao Huang700663d2015-08-12 09:58:59 +0800719 else:
Yilin Yang71e39412019-09-24 09:26:46 +0800720 print('Removing all projects and keys information...', end=' ')
Mao Huang700663d2015-08-12 09:58:59 +0800721 dkps.Destroy()
Yilin Yang71e39412019-09-24 09:26:46 +0800722 print('done.')
Mao Huang700663d2015-08-12 09:58:59 +0800723 elif args.command == 'listen':
724 dkps.ListenForever(args.ip, args.port)
725 elif args.command == 'list':
Yilin Yang71e39412019-09-24 09:26:46 +0800726 print(dkps.ListProjects())
Mao Huang700663d2015-08-12 09:58:59 +0800727 elif args.command == 'add':
728 dkps.AddProject(
729 args.name, args.uploader_key_file_path, args.requester_key_file_path,
Mao Huangecbeb122016-06-22 20:30:38 +0800730 args.parser_module_file_name, args.filter_module_file_name)
Mao Huang700663d2015-08-12 09:58:59 +0800731 elif args.command == 'update':
732 dkps.UpdateProject(
733 args.name, args.uploader_key_file_path, args.requester_key_file_path,
734 args.filter_module_file_name)
735 elif args.command == 'rm':
736 dkps.RemoveProject(args.name)
737 else:
738 raise ValueError('Unknown command %s' % args.command)
739
740
741if __name__ == '__main__':
742 main()