blob: 6a60545feae128c2ad16959d5638ab9053e3bf21 [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
19import SimpleXMLRPCServer
20import sqlite3
21import textwrap
22
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."""
65 pass
66
67
68class InvalidUploaderException(ValueError):
69 """Raised when the signature of the uploader can't be verified."""
70 pass
71
72
73class InvalidRequesterException(ValueError):
74 """Raised when the signature of the requester can't be verified."""
75 pass
76
77
78def GetSQLite3Connection(database_file_path):
79 """Returns a tuple of SQLite3's (connection, cursor) to database_file_path.
80
81 If the connection has been created before, it is returned directly. If it's
82 not, this function creates the connection, ensures that the foreign key
83 constraint is enabled, and returns.
84
85 Args:
86 database_file_path: path to the SQLite3 database file.
87 """
88 database_file_path = os.path.realpath(database_file_path)
89
90 # Return if the connection to database_file_path has been created before.
91 try:
92 connection = GetSQLite3Connection.connection_dict[database_file_path]
93 return (connection, connection.cursor())
94 except KeyError:
95 pass
96 except AttributeError:
97 GetSQLite3Connection.connection_dict = {}
98
99 # Create connection.
100 connection = sqlite3.connect(database_file_path)
101 connection.row_factory = sqlite3.Row
102 cursor = connection.cursor()
103
104 # Enable foreign key constraint since SQLite3 disables it by default.
105 cursor.execute('PRAGMA foreign_keys = ON')
106 # Check if foreign key constraint is enabled.
107 cursor.execute('PRAGMA foreign_keys')
108 if cursor.fetchone()[0] != 1:
109 raise RuntimeError('Failed to enable SQLite3 foreign key constraint')
110
111 GetSQLite3Connection.connection_dict[database_file_path] = connection
112
113 return (connection, cursor)
114
115
116class DRMKeysProvisioningServer(object):
117 """The DRM Keys Provisioning Server (DKPS) class."""
118
119 def __init__(self, database_file_path, gnupg_homedir):
120 """DKPS constructor.
121
122 Args:
123 database_file_path: path to the SQLite3 database file.
124 gnupg_homedir: path to the GnuPG home directory.
125 """
126 self.database_file_path = database_file_path
127 self.gnupg_homedir = gnupg_homedir
128
129 if not os.path.isdir(self.gnupg_homedir):
130 self.gpg = None
131 else:
132 self.gpg = gnupg.GPG(gnupghome=self.gnupg_homedir)
133
134 if not os.path.isfile(self.database_file_path):
135 self.db_connection, self.db_cursor = (None, None)
136 else:
137 self.db_connection, self.db_cursor = GetSQLite3Connection(
138 self.database_file_path)
139
Mao Huang3963b372015-12-11 15:20:19 +0800140 def Initialize(self, gpg_gen_key_args_dict=None, server_key_file_path=None):
141 """Creates the SQLite3 database and GnuPG home, and imports, or generates a
142 GPG key for the server to use.
Mao Huang700663d2015-08-12 09:58:59 +0800143
144 Args:
145 gpg_gen_key_args_dict: will be passed directly as the keyword arguments to
Mao Huang3963b372015-12-11 15:20:19 +0800146 python-gnupg's gen_key() function if server_key_file_path is None.
147 Can be used to customize the key generator process, such as key_type,
148 key_length, etc. See python-gnupg's doc for what can be customized.
149 server_key_file_path: path to the server key to use. If not None, the
150 system will simply import this key and use it as the server key; if
151 None, the system will generate a new key.
Mao Huang700663d2015-08-12 09:58:59 +0800152
153 Raises:
154 RuntimeError is the database and GnuPG home have already been initialized.
155 """
Mao Huang700663d2015-08-12 09:58:59 +0800156 # Create GPG instance and database connection.
157 self.gpg = gnupg.GPG(gnupghome=self.gnupg_homedir)
158 self.db_connection, self.db_cursor = GetSQLite3Connection(
159 self.database_file_path)
160
Mao Huang3963b372015-12-11 15:20:19 +0800161 # If any key exists, the system has already been initialized.
162 if self.gpg.list_keys():
163 raise RuntimeError('Already initialized')
Mao Huang700663d2015-08-12 09:58:59 +0800164
Mao Huang3963b372015-12-11 15:20:19 +0800165 if server_key_file_path: # use existing key
Mao Huang901437f2016-06-24 11:39:15 +0800166 # TODO(littlecvr): make sure the server key doesn't have passphrase.
Mao Huang3963b372015-12-11 15:20:19 +0800167 server_key_fingerprint, _ = self._ImportGPGKey(server_key_file_path)
168 else: # generate a new GPG key
169 if gpg_gen_key_args_dict is None:
170 gpg_gen_key_args_dict = {}
171 if 'name_real' not in gpg_gen_key_args_dict:
172 gpg_gen_key_args_dict['name_real'] = 'DKPS Server'
173 if 'name_email' not in gpg_gen_key_args_dict:
174 gpg_gen_key_args_dict['name_email'] = 'chromeos-factory-dkps@google.com'
175 if 'name_comment' not in gpg_gen_key_args_dict:
176 gpg_gen_key_args_dict['name_comment'] = 'DRM Keys Provisioning Server'
177 key_input_data = self.gpg.gen_key_input(**gpg_gen_key_args_dict)
178 server_key_fingerprint = self.gpg.gen_key(key_input_data).fingerprint
Mao Huang700663d2015-08-12 09:58:59 +0800179
180 # Create and set up the schema of the database.
181 with open(CREATE_DATABASE_SQL_FILE_PATH) as f:
182 create_database_sql = f.read()
183 with self.db_connection:
184 self.db_cursor.executescript(create_database_sql)
185
186 # Record the server key fingerprint.
187 with self.db_connection:
188 self.db_cursor.execute(
189 'INSERT INTO settings (key, value) VALUES (?, ?)',
Mao Huang3963b372015-12-11 15:20:19 +0800190 ('server_key_fingerprint', server_key_fingerprint))
Mao Huang700663d2015-08-12 09:58:59 +0800191
192 def Destroy(self):
193 """Destroys the database and GnuPG home directory.
194
195 This is the opposite of Initialize(). It essentially removes the SQLite3
196 database file and GnuPG home directory.
197 """
198 # Remove database.
199 if self.db_connection:
200 self.db_connection.close()
201 if os.path.exists(self.database_file_path):
202 os.remove(self.database_file_path)
203
204 # Remove GnuPG home.
205 if self.gpg:
206 self.gpg = None
207 if os.path.exists(self.gnupg_homedir):
208 shutil.rmtree(self.gnupg_homedir)
209
210 def AddProject(self, name, uploader_key_file_path, requester_key_file_path,
Mao Huang9076f632015-09-24 17:38:47 +0800211 parser_module_file_name, filter_module_file_name=None):
Mao Huang700663d2015-08-12 09:58:59 +0800212 """Adds a project.
213
214 Args:
215 name: name of the project, must be unique.
216 uploader_key_file_path: path to the OEM's public key file.
217 requester_key_file_path: path to the ODM's public key file.
Mao Huang9076f632015-09-24 17:38:47 +0800218 parser_module_file_name: file name of the parser python module.
Mao Huang700663d2015-08-12 09:58:59 +0800219 filter_module_file_name: file name of the filter python module.
220
221 Raises:
222 ValueError if either the uploader's or requester's key are imported (which
223 means they are used by another project).
224 """
Mao Huang9076f632015-09-24 17:38:47 +0800225 # Try to load the parser and filter modules.
226 self._LoadParserModule(parser_module_file_name)
Mao Huang700663d2015-08-12 09:58:59 +0800227 if filter_module_file_name is not None:
228 self._LoadFilterModule(filter_module_file_name)
229
230 # Try to import uploader and requester keys and add project info into the
231 # database, if failed at any step, delete imported keys.
232 uploader_key_fingerprint, requester_key_fingerprint = (None, None)
233 uploader_key_already_exists, requester_key_already_exists = (False, False)
234 try:
235 uploader_key_fingerprint, uploader_key_already_exists = (
236 self._ImportGPGKey(uploader_key_file_path))
237 if uploader_key_already_exists:
238 raise ValueError('Uploader key already exists')
239 requester_key_fingerprint, requester_key_already_exists = (
240 self._ImportGPGKey(requester_key_file_path))
241 if requester_key_already_exists:
242 raise ValueError('Requester key already exists')
243 with self.db_connection:
244 self.db_cursor.execute(
Mao Huang9076f632015-09-24 17:38:47 +0800245 'INSERT INTO projects ('
246 ' name, uploader_key_fingerprint, requester_key_fingerprint, '
247 ' parser_module_file_name, filter_module_file_name) '
248 'VALUES (?, ?, ?, ?, ?)',
Mao Huang700663d2015-08-12 09:58:59 +0800249 (name, uploader_key_fingerprint, requester_key_fingerprint,
Mao Huang9076f632015-09-24 17:38:47 +0800250 parser_module_file_name, filter_module_file_name))
Mao Huang700663d2015-08-12 09:58:59 +0800251 except BaseException:
252 if not uploader_key_already_exists and uploader_key_fingerprint:
253 self.gpg.delete_keys(uploader_key_fingerprint)
254 if not requester_key_already_exists and requester_key_fingerprint:
255 self.gpg.delete_keys(requester_key_fingerprint)
256 raise
257
258 def UpdateProject(self, name, uploader_key_file_path=None,
259 requester_key_file_path=None, filter_module_file_name=None):
260 """Updates a project.
261
262 Args:
263 name: name of the project, must be unique.
264 uploader_key_file_path: path to the OEM's public key file.
265 requester_key_file_path: path to the ODM's public key file.
266 filter_module_file_name: file name of the filter python module.
267
268 Raises:
269 RuntimeError if SQLite3 can't update the project row (for any reason).
270 """
271 # Try to load the filter module.
272 if filter_module_file_name is not None:
273 self._LoadFilterModule(filter_module_file_name)
274
275 project = self._FetchProjectByName(name)
276
277 # Try to import uploader and requester keys and add project info into the
278 # database, if failed at any step, delete any newly imported keys.
279 uploader_key_fingerprint, requester_key_fingerprint = (None, None)
280 old_uploader_key_fingerprint = project['uploader_key_fingerprint']
281 old_requester_key_fingerprint = project['requester_key_fingerprint']
282 same_uploader_key, same_requester_key = (True, True)
283 try:
284 sql_set_clause_list = ['filter_module_file_name = ?']
285 sql_parameters = [filter_module_file_name]
286
287 if uploader_key_file_path:
288 uploader_key_fingerprint, same_uploader_key = self._ImportGPGKey(
289 uploader_key_file_path)
290 sql_set_clause_list.append('uploader_key_fingerprint = ?')
291 sql_parameters.append(uploader_key_fingerprint)
292
293 if requester_key_file_path:
294 requester_key_fingerprint, same_requester_key = self._ImportGPGKey(
295 uploader_key_file_path)
296 sql_set_clause_list.append('requester_key_fingerprint = ?')
297 sql_parameters.append(requester_key_fingerprint)
298
299 sql_set_clause = ','.join(sql_set_clause_list)
300 sql_parameters.append(name)
301 with self.db_connection:
302 self.db_cursor.execute(
303 'UPDATE projects SET %s WHERE name = ?' % sql_set_clause,
304 tuple(sql_parameters))
305 if self.db_cursor.rowcount != 1:
306 raise RuntimeError('Failed to update project %s' % name)
307 except BaseException:
308 if not same_uploader_key and uploader_key_fingerprint:
309 self.gpg.delete_keys(uploader_key_fingerprint)
310 if not same_requester_key and requester_key_fingerprint:
311 self.gpg.delete_keys(requester_key_fingerprint)
312 raise
313
314 if not same_uploader_key:
315 self.gpg.delete_keys(old_uploader_key_fingerprint)
316 if not same_requester_key:
317 self.gpg.delete_keys(old_requester_key_fingerprint)
318
319 def RemoveProject(self, name):
320 """Removes a project.
321
322 Args:
323 name: the name of the project specified when added.
324 """
325 project = self._FetchProjectByName(name)
326
327 self.gpg.delete_keys(project['uploader_key_fingerprint'])
328 self.gpg.delete_keys(project['requester_key_fingerprint'])
329
330 with self.db_connection:
331 self.db_cursor.execute(
332 'DELETE FROM drm_keys WHERE project_name = ?', (name,))
333 self.db_cursor.execute('DELETE FROM projects WHERE name = ?', (name,))
334
335 def ListProjects(self):
336 """Lists all projects."""
337 self.db_cursor.execute('SELECT * FROM projects ORDER BY name ASC')
338 return self.db_cursor.fetchall()
339
340 def Upload(self, encrypted_serialized_drm_keys):
341 """Uploads a list of DRM keys to the server. This is an atomic operation. It
342 will either succeed and save all the keys, or fail and save no keys.
343
344 Args:
345 encrypted_serialized_drm_keys: the serialized DRM keys signed by the
346 uploader and encrypted by the server's public key.
347
348 Raises:
349 InvalidUploaderException if the signature of the uploader can not be
350 verified.
351 """
352 decrypted_obj = self.gpg.decrypt(encrypted_serialized_drm_keys)
353 project = self._FetchProjectByUploaderKeyFingerprint(
354 decrypted_obj.fingerprint)
355 serialized_drm_keys = decrypted_obj.data
356
Mao Huang9076f632015-09-24 17:38:47 +0800357 # Pass to the parse function.
358 parser_module = self._LoadParserModule(project['parser_module_file_name'])
359 drm_key_list = parser_module.Parse(serialized_drm_keys)
360
361 drm_key_hash_list = []
362 for drm_key in drm_key_list:
363 drm_key_hash_list.append(hashlib.sha1(json.dumps(drm_key)).hexdigest())
364
Mao Huangecbeb122016-06-22 20:30:38 +0800365 # Pass to the filter function if needed.
366 if project['filter_module_file_name']: # filter module can be null
367 filter_module = self._LoadFilterModule(project['filter_module_file_name'])
368 filtered_drm_key_list = filter_module.Filter(drm_key_list)
369 else:
370 # filter module is optional
371 filtered_drm_key_list = drm_key_list
Mao Huang700663d2015-08-12 09:58:59 +0800372
373 # Fetch server key for signing.
374 server_key_fingerprint = self._FetchServerKeyFingerprint()
375
376 # Sign and encrypt each key by server's private key and requester's public
377 # key, respectively.
378 encrypted_serialized_drm_key_list = []
379 requester_key_fingerprint = project['requester_key_fingerprint']
380 for drm_key in filtered_drm_key_list:
381 encrypted_obj = self.gpg.encrypt(
382 json.dumps(drm_key), requester_key_fingerprint,
383 always_trust=True, sign=server_key_fingerprint)
384 encrypted_serialized_drm_key_list.append(encrypted_obj.data)
385
386 # Insert into the database.
387 with self.db_connection:
388 self.db_cursor.executemany(
Mao Huang9076f632015-09-24 17:38:47 +0800389 'INSERT INTO drm_keys ('
390 ' project_name, drm_key_hash, encrypted_drm_key) '
391 'VALUES (?, ?, ?)',
Mao Huang700663d2015-08-12 09:58:59 +0800392 zip([project['name']] * len(encrypted_serialized_drm_key_list),
Mao Huang9076f632015-09-24 17:38:47 +0800393 drm_key_hash_list, encrypted_serialized_drm_key_list))
Mao Huang700663d2015-08-12 09:58:59 +0800394
395 def AvailableKeyCount(self, requester_signature):
396 """Queries the number of remaining keys.
397
398 Args:
399 requester_signature: a message signed by the requester. Since the server
400 doesn't need any additional info from the requester, the requester can
401 simply sign a random string and send it here.
402
403 Returns:
404 The number of remaining keys that can be requested.
405
406 Raises:
407 InvalidRequesterException if the signature of the requester can not be
408 verified.
409 """
410 verified = self.gpg.verify(requester_signature)
411 if not verified:
412 raise InvalidRequesterException(
413 'Invalid requester, check your signing key')
414
415 project = self._FetchProjectByRequesterKeyFingerprint(verified.fingerprint)
416
417 self.db_cursor.execute(
418 'SELECT COUNT(*) AS available_key_count FROM drm_keys '
419 'WHERE project_name = ? AND device_serial_number IS NULL',
420 (project['name'],))
421 return self.db_cursor.fetchone()['available_key_count']
422
423 def Request(self, encrypted_device_serial_number):
424 """Requests a DRM key by device serial number.
425
426 Args:
427 encrypted_device_serial_number: the device serial number signed by the
428 requester and encrypted by the server's public key.
429
430 Raises:
431 InvalidRequesterException if the signature of the requester can not be
432 verified. RuntimeError if no available keys left in the database.
433 """
434 decrypted_obj = self.gpg.decrypt(encrypted_device_serial_number)
435 project = self._FetchProjectByRequesterKeyFingerprint(
436 decrypted_obj.fingerprint)
437 device_serial_number = decrypted_obj.data
438
439 def FetchDRMKeyByDeviceSerialNumber(project_name, device_serial_number):
440 self.db_cursor.execute(
441 'SELECT * FROM drm_keys WHERE project_name = ? AND '
442 'device_serial_number = ?',
443 (project_name, device_serial_number))
444 return self.db_cursor.fetchone()
445
446 row = FetchDRMKeyByDeviceSerialNumber(project['name'], device_serial_number)
447 if row: # the SN has already paired
448 return row['encrypted_drm_key']
449
450 # Find an unpaired key.
451 with self.db_connection:
452 # SQLite3 does not support using LIMIT clause in UPDATE statement by
453 # default, unless SQLITE_ENABLE_UPDATE_DELETE_LIMIT flag is defined during
454 # compilation. Since this script may be deployed on partner's computer,
455 # we'd better assume they don't have this flag on.
456 self.db_cursor.execute(
457 'UPDATE drm_keys SET device_serial_number = ? '
458 'WHERE id = (SELECT id FROM drm_keys WHERE project_name = ? AND '
459 ' device_serial_number IS NULL LIMIT 1)',
460 (device_serial_number, project['name']))
461 if self.db_cursor.rowcount != 1: # insufficient keys
462 raise RuntimeError(
463 'Insufficient DRM keys, ask for the OEM to upload more')
464
465 row = FetchDRMKeyByDeviceSerialNumber(project['name'], device_serial_number)
466 if row:
467 return row['encrypted_drm_key']
468 else:
469 raise RuntimeError('Failed to find paired DRM key')
470
471 def ListenForever(self, ip, port):
472 """Starts the XML RPC server waiting for commands.
473
474 Args:
475 ip: IP to bind.
476 port: port to bind.
477 """
Mao Huang901437f2016-06-24 11:39:15 +0800478 class Server(SimpleXMLRPCServer.SimpleXMLRPCServer):
479 def _dispatch(self, method, params):
480 # Catch exceptions and log them. Without this, SimpleXMLRPCServer simply
481 # output the error message to stdout, and we won't be able to see what
482 # happened in the log file.
483 logging.info('%s called', method)
484 try:
485 result = SimpleXMLRPCServer.SimpleXMLRPCServer._dispatch(
486 self, method, params)
487 logging.warning('MAO %r', result)
488 return result
489 except BaseException as e:
490 logging.exception(e)
491 raise
492
493 server = Server((ip, port), allow_none=True)
Mao Huang700663d2015-08-12 09:58:59 +0800494
495 server.register_introspection_functions()
496 server.register_function(self.AvailableKeyCount)
497 server.register_function(self.Upload)
498 server.register_function(self.Request)
499
500 server.serve_forever()
501
502 def _ImportGPGKey(self, key_file_path):
503 """Imports a GPG key from a file.
504
505 Args:
506 key_file_path: path to the GPG key file.
507
508 Returns:
509 A tuple (key_fingerprint, key_already_exists). The 1st element is the
510 imported key's fingerprint, and the 2nd element is True if the key was
511 already in the database before importing, False otherwise.
512 """
513 with open(key_file_path) as f:
514 import_results = self.gpg.import_keys(f.read())
515 key_already_exists = (import_results.imported == 0)
516 key_fingerprint = import_results.fingerprints[0]
517 return (key_fingerprint, key_already_exists)
518
519 def _LoadFilterModule(self, filter_module_file_name):
520 """Loads the filter module.
521
522 Args:
Mao Huang9076f632015-09-24 17:38:47 +0800523 filter_module_file_name: file name of the filter module in FILTERS_DIR.
Mao Huang700663d2015-08-12 09:58:59 +0800524
525 Returns:
526 The loaded filter module on success.
527
528 Raises:
529 Exception if failed, see imp.load_source()'s doc for what could be raised.
530 """
531 return imp.load_source(
532 'filter_module', os.path.join(FILTERS_DIR, filter_module_file_name))
533
Mao Huang9076f632015-09-24 17:38:47 +0800534 def _LoadParserModule(self, parser_module_file_name):
535 """Loads the parser module.
536
537 Args:
538 parser_module_file_name: file name of the parser module in PARSERS_DIR.
539
540 Returns:
541 The loaded parser module on success.
542
543 Raises:
544 Exception if failed, see imp.load_source()'s doc for what could be raised.
545 """
546 return imp.load_source(
547 'parser_module', os.path.join(PARSERS_DIR, parser_module_file_name))
548
Mao Huang700663d2015-08-12 09:58:59 +0800549 def _FetchServerKeyFingerprint(self):
550 """Returns the server GPG key's fingerprint."""
551 self.db_cursor.execute(
552 "SELECT * FROM settings WHERE key = 'server_key_fingerprint'")
553 row = self.db_cursor.fetchone()
554 if not row:
555 raise ValueError('Server key fingerprint not exists')
556 return row['value']
557
558 def _FetchOneProject(self, name=None, # pylint: disable=W0613
559 uploader_key_fingerprint=None, # pylint: disable=W0613
560 requester_key_fingerprint=None, # pylint: disable=W0613
561 exception_type=None, error_msg=None):
562 """Fetches the project by name, uploader key fingerprint, or requester key
563 fingerprint.
564
565 This function combines the name, uploader_key_fingerprint,
566 requester_key_fingerprint conditions (if not None) with the AND operator,
567 and tries to fetch one project from the database.
568
569 Args:
570 name: name of the project.
571 uploader_key_fingerprint: uploader key fingerprint of the project.
572 requester_key_fingerprint: requester key fingerprint of the project.
573 exception_type: if no project was found and exception_type is not None,
574 raise exception_type with error_msg.
575 error_msg: if no project was found and exception_type is not None, raise
576 exception_type with error_msg.
577
578 Returns:
579 A project that matches the name, uploader_key_fingerprint, and
580 requester_key_fingerprint conditiions.
581
582 Raises:
583 exception_type with error_msg if not project was found.
584 """
585 where_clause_list = []
586 params = []
587 local_vars = locals()
588 for param_name in ['name', 'uploader_key_fingerprint',
589 'requester_key_fingerprint']:
590 if local_vars[param_name] is not None:
591 where_clause_list.append('%s = ?' % param_name)
592 params.append(locals()[param_name])
593 if not where_clause_list:
594 raise ValueError('No conditions given to fetch the project')
595 where_clause = 'WHERE ' + ' AND '.join(where_clause_list)
596
597 self.db_cursor.execute(
598 'SELECT * FROM projects %s' % where_clause, tuple(params))
599 project = self.db_cursor.fetchone()
600
601 if not project and exception_type:
602 raise exception_type(error_msg)
603
604 return project
605
606 def _FetchProjectByName(self, name):
607 return self._FetchOneProject(
608 name=name, exception_type=ProjectNotFoundException,
609 error_msg=('Project %s not found' % name))
610
611 def _FetchProjectByUploaderKeyFingerprint(self, uploader_key_fingerprint):
612 return self._FetchOneProject(
613 uploader_key_fingerprint=uploader_key_fingerprint,
614 exception_type=InvalidUploaderException,
615 error_msg='Invalid uploader, check your signing key')
616
617 def _FetchProjectByRequesterKeyFingerprint(self, requester_key_fingerprint):
618 return self._FetchOneProject(
619 requester_key_fingerprint=requester_key_fingerprint,
620 exception_type=InvalidRequesterException,
621 error_msg='Invalid requester, check your signing key')
622
623
624def _ParseArguments():
625 parser = argparse.ArgumentParser()
626 parser.add_argument(
Mao Huang901437f2016-06-24 11:39:15 +0800627 '-d', '--database_file_path',
628 default=os.path.join(SCRIPT_DIR, DEFAULT_DATABASE_FILE_NAME),
Mao Huang700663d2015-08-12 09:58:59 +0800629 help='path to the SQLite3 database file, default to "dkps.db" in the '
630 'same directory of this script')
631 parser.add_argument(
Mao Huang901437f2016-06-24 11:39:15 +0800632 '-g', '--gnupg_homedir',
633 default=os.path.join(SCRIPT_DIR, DEFAULT_GNUPG_DIR_NAME),
Mao Huang700663d2015-08-12 09:58:59 +0800634 help='path to the GnuGP home directory, default to "gnupg" in the same '
635 'directory of this script')
Mao Huang901437f2016-06-24 11:39:15 +0800636 parser.add_argument(
637 '-l', '--log_file_path',
638 default=os.path.join(SCRIPT_DIR, DEFAULT_LOG_FILE_NAME),
639 help='path to the log file, default to "dkps.log" in the same directory '
640 'of this script')
Mao Huang700663d2015-08-12 09:58:59 +0800641 subparsers = parser.add_subparsers(dest='command')
642
643 parser_add = subparsers.add_parser('add', help='adds a new project')
644 parser_add.add_argument('-n', '--name', required=True,
645 help='name of the new project')
646 parser_add.add_argument('-u', '--uploader_key_file_path', required=True,
647 help="path to the uploader's public key file")
648 parser_add.add_argument('-r', '--requester_key_file_path', required=True,
649 help="path to the requester's public key file")
Mao Huang9076f632015-09-24 17:38:47 +0800650 parser_add.add_argument('-p', '--parser_module_file_name', required=True,
651 help='file name of the parser module')
Mao Huang700663d2015-08-12 09:58:59 +0800652 parser_add.add_argument('-f', '--filter_module_file_name', default=None,
653 help='file name of the filter module')
654
655 subparsers.add_parser('destroy', help='destroys the database')
656
657 parser_update = subparsers.add_parser('update',
658 help='updates an existing project')
659 parser_update.add_argument('-n', '--name', required=True,
660 help='name of the project')
661 parser_update.add_argument('-u', '--uploader_key_file_path', default=None,
662 help="path to the uploader's public key file")
663 parser_update.add_argument('-r', '--requester_key_file_path', default=None,
664 help="path to the requester's public key file")
665 parser_update.add_argument('-f', '--filter_module_file_name', default=None,
666 help='file name of the filter module')
667
668 parser_init = subparsers.add_parser('init', help='initializes the database')
669 parser_init.add_argument(
670 '-g', '--gpg_gen_key_args', action='append', nargs=2, default={},
671 help='arguments to use when generating GPG key for server')
Mao Huang24be1382016-06-23 17:53:57 +0800672 parser_init.add_argument(
673 '-s', '--server_key_file_path', default=None,
674 help="path to the server's private key file")
Mao Huang700663d2015-08-12 09:58:59 +0800675
676 subparsers.add_parser('list', help='lists all projects')
677
678 parser_listen = subparsers.add_parser(
679 'listen', help='starts the server, waiting for upload or request keys')
680 parser_listen.add_argument(
Mao Huang901437f2016-06-24 11:39:15 +0800681 '--ip', default=DEFAULT_BIND_ADDR,
682 help='IP to bind, default to %s' % DEFAULT_BIND_ADDR)
Mao Huang700663d2015-08-12 09:58:59 +0800683 parser_listen.add_argument(
Mao Huang901437f2016-06-24 11:39:15 +0800684 '--port', type=int, default=DEFAULT_BIND_PORT,
685 help='port to listen, default to %s' % DEFAULT_BIND_PORT)
Mao Huang700663d2015-08-12 09:58:59 +0800686
687 parser_rm = subparsers.add_parser('rm', help='removes an existing project')
688 parser_rm.add_argument('-n', '--name', required=True,
689 help='name of the project to remove')
690
691 return parser.parse_args()
692
693
694def main():
695 args = _ParseArguments()
696
Mao Huang901437f2016-06-24 11:39:15 +0800697 logging_config = DEFAULT_LOGGING_CONFIG
698 logging_config['handlers']['file']['filename'] = args.log_file_path
699 logging.config.dictConfig(logging_config)
Mao Huang041483e2015-09-14 23:28:18 +0800700
Mao Huang700663d2015-08-12 09:58:59 +0800701 dkps = DRMKeysProvisioningServer(args.database_file_path, args.gnupg_homedir)
702 if args.command == 'init':
703 # Convert from command line arguments to a dict.
704 gpg_gen_key_args_dict = {}
705 for pair in args.gpg_gen_key_args:
706 gpg_gen_key_args_dict[pair[0]] = pair[1]
Mao Huang24be1382016-06-23 17:53:57 +0800707 dkps.Initialize(gpg_gen_key_args_dict, args.server_key_file_path)
Mao Huang700663d2015-08-12 09:58:59 +0800708 elif args.command == 'destroy':
709 message = (
710 'This action will remove all projects and keys information and is NOT '
711 'recoverable! Are you sure? (y/N)')
712 answer = raw_input(textwrap.fill(message, 80) + ' ')
713 if answer.lower() != 'y' and answer.lower() != 'yes':
714 print 'OK, nothing will be removed.'
715 else:
716 print 'Removing all projects and keys information...',
717 dkps.Destroy()
718 print 'done.'
719 elif args.command == 'listen':
720 dkps.ListenForever(args.ip, args.port)
721 elif args.command == 'list':
722 print dkps.ListProjects()
723 elif args.command == 'add':
724 dkps.AddProject(
725 args.name, args.uploader_key_file_path, args.requester_key_file_path,
Mao Huangecbeb122016-06-22 20:30:38 +0800726 args.parser_module_file_name, args.filter_module_file_name)
Mao Huang700663d2015-08-12 09:58:59 +0800727 elif args.command == 'update':
728 dkps.UpdateProject(
729 args.name, args.uploader_key_file_path, args.requester_key_file_path,
730 args.filter_module_file_name)
731 elif args.command == 'rm':
732 dkps.RemoveProject(args.name)
733 else:
734 raise ValueError('Unknown command %s' % args.command)
735
736
737if __name__ == '__main__':
738 main()