blob: 0e0c3b715cbb5eaf2572f694710b95a7d970c74a [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:
chuntsen486152e2017-07-21 14:48:01 +0800132 self.gpg = gnupg.GPG(homedir=self.gnupg_homedir)
Mao Huang700663d2015-08-12 09:58:59 +0800133
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.
chuntsen486152e2017-07-21 14:48:01 +0800157 self.gpg = gnupg.GPG(homedir=self.gnupg_homedir)
Mao Huang700663d2015-08-12 09:58:59 +0800158 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,
chuntsen486152e2017-07-21 14:48:01 +0800383 always_trust=True, default_key=server_key_fingerprint)
Mao Huang700663d2015-08-12 09:58:59 +0800384 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)
Mao Huang901437f2016-06-24 11:39:15 +0800487 return result
488 except BaseException as e:
489 logging.exception(e)
490 raise
491
492 server = Server((ip, port), allow_none=True)
Mao Huang700663d2015-08-12 09:58:59 +0800493
494 server.register_introspection_functions()
495 server.register_function(self.AvailableKeyCount)
496 server.register_function(self.Upload)
497 server.register_function(self.Request)
498
499 server.serve_forever()
500
501 def _ImportGPGKey(self, key_file_path):
502 """Imports a GPG key from a file.
503
504 Args:
505 key_file_path: path to the GPG key file.
506
507 Returns:
508 A tuple (key_fingerprint, key_already_exists). The 1st element is the
509 imported key's fingerprint, and the 2nd element is True if the key was
510 already in the database before importing, False otherwise.
511 """
512 with open(key_file_path) as f:
513 import_results = self.gpg.import_keys(f.read())
chuntsen486152e2017-07-21 14:48:01 +0800514 key_already_exists = (import_results.counts['imported'] == 0)
Mao Huang700663d2015-08-12 09:58:59 +0800515 key_fingerprint = import_results.fingerprints[0]
516 return (key_fingerprint, key_already_exists)
517
518 def _LoadFilterModule(self, filter_module_file_name):
519 """Loads the filter module.
520
521 Args:
Mao Huang9076f632015-09-24 17:38:47 +0800522 filter_module_file_name: file name of the filter module in FILTERS_DIR.
Mao Huang700663d2015-08-12 09:58:59 +0800523
524 Returns:
525 The loaded filter module on success.
526
527 Raises:
528 Exception if failed, see imp.load_source()'s doc for what could be raised.
529 """
530 return imp.load_source(
531 'filter_module', os.path.join(FILTERS_DIR, filter_module_file_name))
532
Mao Huang9076f632015-09-24 17:38:47 +0800533 def _LoadParserModule(self, parser_module_file_name):
534 """Loads the parser module.
535
536 Args:
537 parser_module_file_name: file name of the parser module in PARSERS_DIR.
538
539 Returns:
540 The loaded parser module on success.
541
542 Raises:
543 Exception if failed, see imp.load_source()'s doc for what could be raised.
544 """
545 return imp.load_source(
546 'parser_module', os.path.join(PARSERS_DIR, parser_module_file_name))
547
Mao Huang700663d2015-08-12 09:58:59 +0800548 def _FetchServerKeyFingerprint(self):
549 """Returns the server GPG key's fingerprint."""
550 self.db_cursor.execute(
551 "SELECT * FROM settings WHERE key = 'server_key_fingerprint'")
552 row = self.db_cursor.fetchone()
553 if not row:
554 raise ValueError('Server key fingerprint not exists')
555 return row['value']
556
557 def _FetchOneProject(self, name=None, # pylint: disable=W0613
558 uploader_key_fingerprint=None, # pylint: disable=W0613
559 requester_key_fingerprint=None, # pylint: disable=W0613
560 exception_type=None, error_msg=None):
561 """Fetches the project by name, uploader key fingerprint, or requester key
562 fingerprint.
563
564 This function combines the name, uploader_key_fingerprint,
565 requester_key_fingerprint conditions (if not None) with the AND operator,
566 and tries to fetch one project from the database.
567
568 Args:
569 name: name of the project.
570 uploader_key_fingerprint: uploader key fingerprint of the project.
571 requester_key_fingerprint: requester key fingerprint of the project.
572 exception_type: if no project was found and exception_type is not None,
573 raise exception_type with error_msg.
574 error_msg: if no project was found and exception_type is not None, raise
575 exception_type with error_msg.
576
577 Returns:
578 A project that matches the name, uploader_key_fingerprint, and
579 requester_key_fingerprint conditiions.
580
581 Raises:
582 exception_type with error_msg if not project was found.
583 """
584 where_clause_list = []
585 params = []
586 local_vars = locals()
587 for param_name in ['name', 'uploader_key_fingerprint',
588 'requester_key_fingerprint']:
589 if local_vars[param_name] is not None:
590 where_clause_list.append('%s = ?' % param_name)
591 params.append(locals()[param_name])
592 if not where_clause_list:
593 raise ValueError('No conditions given to fetch the project')
594 where_clause = 'WHERE ' + ' AND '.join(where_clause_list)
595
596 self.db_cursor.execute(
597 'SELECT * FROM projects %s' % where_clause, tuple(params))
598 project = self.db_cursor.fetchone()
599
600 if not project and exception_type:
601 raise exception_type(error_msg)
602
603 return project
604
605 def _FetchProjectByName(self, name):
606 return self._FetchOneProject(
607 name=name, exception_type=ProjectNotFoundException,
608 error_msg=('Project %s not found' % name))
609
610 def _FetchProjectByUploaderKeyFingerprint(self, uploader_key_fingerprint):
611 return self._FetchOneProject(
612 uploader_key_fingerprint=uploader_key_fingerprint,
613 exception_type=InvalidUploaderException,
614 error_msg='Invalid uploader, check your signing key')
615
616 def _FetchProjectByRequesterKeyFingerprint(self, requester_key_fingerprint):
617 return self._FetchOneProject(
618 requester_key_fingerprint=requester_key_fingerprint,
619 exception_type=InvalidRequesterException,
620 error_msg='Invalid requester, check your signing key')
621
622
623def _ParseArguments():
624 parser = argparse.ArgumentParser()
625 parser.add_argument(
Mao Huang901437f2016-06-24 11:39:15 +0800626 '-d', '--database_file_path',
627 default=os.path.join(SCRIPT_DIR, DEFAULT_DATABASE_FILE_NAME),
Mao Huang700663d2015-08-12 09:58:59 +0800628 help='path to the SQLite3 database file, default to "dkps.db" in the '
629 'same directory of this script')
630 parser.add_argument(
Mao Huang901437f2016-06-24 11:39:15 +0800631 '-g', '--gnupg_homedir',
632 default=os.path.join(SCRIPT_DIR, DEFAULT_GNUPG_DIR_NAME),
Mao Huang700663d2015-08-12 09:58:59 +0800633 help='path to the GnuGP home directory, default to "gnupg" in the same '
634 'directory of this script')
Mao Huang901437f2016-06-24 11:39:15 +0800635 parser.add_argument(
636 '-l', '--log_file_path',
637 default=os.path.join(SCRIPT_DIR, DEFAULT_LOG_FILE_NAME),
638 help='path to the log file, default to "dkps.log" in the same directory '
639 'of this script')
Mao Huang700663d2015-08-12 09:58:59 +0800640 subparsers = parser.add_subparsers(dest='command')
641
642 parser_add = subparsers.add_parser('add', help='adds a new project')
643 parser_add.add_argument('-n', '--name', required=True,
644 help='name of the new project')
645 parser_add.add_argument('-u', '--uploader_key_file_path', required=True,
646 help="path to the uploader's public key file")
647 parser_add.add_argument('-r', '--requester_key_file_path', required=True,
648 help="path to the requester's public key file")
Mao Huang9076f632015-09-24 17:38:47 +0800649 parser_add.add_argument('-p', '--parser_module_file_name', required=True,
650 help='file name of the parser module')
Mao Huang700663d2015-08-12 09:58:59 +0800651 parser_add.add_argument('-f', '--filter_module_file_name', default=None,
652 help='file name of the filter module')
653
654 subparsers.add_parser('destroy', help='destroys the database')
655
656 parser_update = subparsers.add_parser('update',
657 help='updates an existing project')
658 parser_update.add_argument('-n', '--name', required=True,
659 help='name of the project')
660 parser_update.add_argument('-u', '--uploader_key_file_path', default=None,
661 help="path to the uploader's public key file")
662 parser_update.add_argument('-r', '--requester_key_file_path', default=None,
663 help="path to the requester's public key file")
664 parser_update.add_argument('-f', '--filter_module_file_name', default=None,
665 help='file name of the filter module')
666
667 parser_init = subparsers.add_parser('init', help='initializes the database')
668 parser_init.add_argument(
669 '-g', '--gpg_gen_key_args', action='append', nargs=2, default={},
670 help='arguments to use when generating GPG key for server')
Mao Huang24be1382016-06-23 17:53:57 +0800671 parser_init.add_argument(
672 '-s', '--server_key_file_path', default=None,
673 help="path to the server's private key file")
Mao Huang700663d2015-08-12 09:58:59 +0800674
675 subparsers.add_parser('list', help='lists all projects')
676
677 parser_listen = subparsers.add_parser(
678 'listen', help='starts the server, waiting for upload or request keys')
679 parser_listen.add_argument(
Mao Huang901437f2016-06-24 11:39:15 +0800680 '--ip', default=DEFAULT_BIND_ADDR,
681 help='IP to bind, default to %s' % DEFAULT_BIND_ADDR)
Mao Huang700663d2015-08-12 09:58:59 +0800682 parser_listen.add_argument(
Mao Huang901437f2016-06-24 11:39:15 +0800683 '--port', type=int, default=DEFAULT_BIND_PORT,
684 help='port to listen, default to %s' % DEFAULT_BIND_PORT)
Mao Huang700663d2015-08-12 09:58:59 +0800685
686 parser_rm = subparsers.add_parser('rm', help='removes an existing project')
687 parser_rm.add_argument('-n', '--name', required=True,
688 help='name of the project to remove')
689
690 return parser.parse_args()
691
692
693def main():
694 args = _ParseArguments()
695
Mao Huang901437f2016-06-24 11:39:15 +0800696 logging_config = DEFAULT_LOGGING_CONFIG
697 logging_config['handlers']['file']['filename'] = args.log_file_path
698 logging.config.dictConfig(logging_config)
Mao Huang041483e2015-09-14 23:28:18 +0800699
Mao Huang700663d2015-08-12 09:58:59 +0800700 dkps = DRMKeysProvisioningServer(args.database_file_path, args.gnupg_homedir)
701 if args.command == 'init':
702 # Convert from command line arguments to a dict.
703 gpg_gen_key_args_dict = {}
704 for pair in args.gpg_gen_key_args:
705 gpg_gen_key_args_dict[pair[0]] = pair[1]
Mao Huang24be1382016-06-23 17:53:57 +0800706 dkps.Initialize(gpg_gen_key_args_dict, args.server_key_file_path)
Mao Huang700663d2015-08-12 09:58:59 +0800707 elif args.command == 'destroy':
708 message = (
709 'This action will remove all projects and keys information and is NOT '
710 'recoverable! Are you sure? (y/N)')
711 answer = raw_input(textwrap.fill(message, 80) + ' ')
712 if answer.lower() != 'y' and answer.lower() != 'yes':
713 print 'OK, nothing will be removed.'
714 else:
715 print 'Removing all projects and keys information...',
716 dkps.Destroy()
717 print 'done.'
718 elif args.command == 'listen':
719 dkps.ListenForever(args.ip, args.port)
720 elif args.command == 'list':
721 print dkps.ListProjects()
722 elif args.command == 'add':
723 dkps.AddProject(
724 args.name, args.uploader_key_file_path, args.requester_key_file_path,
Mao Huangecbeb122016-06-22 20:30:38 +0800725 args.parser_module_file_name, args.filter_module_file_name)
Mao Huang700663d2015-08-12 09:58:59 +0800726 elif args.command == 'update':
727 dkps.UpdateProject(
728 args.name, args.uploader_key_file_path, args.requester_key_file_path,
729 args.filter_module_file_name)
730 elif args.command == 'rm':
731 dkps.RemoveProject(args.name)
732 else:
733 raise ValueError('Unknown command %s' % args.command)
734
735
736if __name__ == '__main__':
737 main()