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