blob: a1e74b5bebff1d1ff2c403e0782b10616a01bff5 [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
11# TODO(littlecvr): Allow using pre-generated server GPG key when initializing.
12
13import 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 Huang700663d2015-08-12 09:58:59 +080018import os
19import shutil
20import SimpleXMLRPCServer
21import sqlite3
Mao Huang041483e2015-09-14 23:28:18 +080022import sys
Mao Huang700663d2015-08-12 09:58:59 +080023import textwrap
24
25import gnupg
26
27
28SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
Mao Huang041483e2015-09-14 23:28:18 +080029LOG_FILE_PATH = os.path.join(SCRIPT_DIR, 'dkps.log')
Mao Huang700663d2015-08-12 09:58:59 +080030FILTERS_DIR = os.path.join(SCRIPT_DIR, 'filters')
Mao Huang9076f632015-09-24 17:38:47 +080031PARSERS_DIR = os.path.join(SCRIPT_DIR, 'parsers')
Mao Huang700663d2015-08-12 09:58:59 +080032CREATE_DATABASE_SQL_FILE_PATH = os.path.join(
33 SCRIPT_DIR, 'sql', 'create_database.sql')
34
35
36class ProjectNotFoundException(ValueError):
37 """Raised when no project was found in the database."""
38 pass
39
40
41class InvalidUploaderException(ValueError):
42 """Raised when the signature of the uploader can't be verified."""
43 pass
44
45
46class InvalidRequesterException(ValueError):
47 """Raised when the signature of the requester can't be verified."""
48 pass
49
50
51def GetSQLite3Connection(database_file_path):
52 """Returns a tuple of SQLite3's (connection, cursor) to database_file_path.
53
54 If the connection has been created before, it is returned directly. If it's
55 not, this function creates the connection, ensures that the foreign key
56 constraint is enabled, and returns.
57
58 Args:
59 database_file_path: path to the SQLite3 database file.
60 """
61 database_file_path = os.path.realpath(database_file_path)
62
63 # Return if the connection to database_file_path has been created before.
64 try:
65 connection = GetSQLite3Connection.connection_dict[database_file_path]
66 return (connection, connection.cursor())
67 except KeyError:
68 pass
69 except AttributeError:
70 GetSQLite3Connection.connection_dict = {}
71
72 # Create connection.
73 connection = sqlite3.connect(database_file_path)
74 connection.row_factory = sqlite3.Row
75 cursor = connection.cursor()
76
77 # Enable foreign key constraint since SQLite3 disables it by default.
78 cursor.execute('PRAGMA foreign_keys = ON')
79 # Check if foreign key constraint is enabled.
80 cursor.execute('PRAGMA foreign_keys')
81 if cursor.fetchone()[0] != 1:
82 raise RuntimeError('Failed to enable SQLite3 foreign key constraint')
83
84 GetSQLite3Connection.connection_dict[database_file_path] = connection
85
86 return (connection, cursor)
87
88
89class DRMKeysProvisioningServer(object):
90 """The DRM Keys Provisioning Server (DKPS) class."""
91
92 def __init__(self, database_file_path, gnupg_homedir):
93 """DKPS constructor.
94
95 Args:
96 database_file_path: path to the SQLite3 database file.
97 gnupg_homedir: path to the GnuPG home directory.
98 """
99 self.database_file_path = database_file_path
100 self.gnupg_homedir = gnupg_homedir
101
102 if not os.path.isdir(self.gnupg_homedir):
103 self.gpg = None
104 else:
105 self.gpg = gnupg.GPG(gnupghome=self.gnupg_homedir)
106
107 if not os.path.isfile(self.database_file_path):
108 self.db_connection, self.db_cursor = (None, None)
109 else:
110 self.db_connection, self.db_cursor = GetSQLite3Connection(
111 self.database_file_path)
112
Mao Huang3963b372015-12-11 15:20:19 +0800113 def Initialize(self, gpg_gen_key_args_dict=None, server_key_file_path=None):
114 """Creates the SQLite3 database and GnuPG home, and imports, or generates a
115 GPG key for the server to use.
Mao Huang700663d2015-08-12 09:58:59 +0800116
117 Args:
118 gpg_gen_key_args_dict: will be passed directly as the keyword arguments to
Mao Huang3963b372015-12-11 15:20:19 +0800119 python-gnupg's gen_key() function if server_key_file_path is None.
120 Can be used to customize the key generator process, such as key_type,
121 key_length, etc. See python-gnupg's doc for what can be customized.
122 server_key_file_path: path to the server key to use. If not None, the
123 system will simply import this key and use it as the server key; if
124 None, the system will generate a new key.
Mao Huang700663d2015-08-12 09:58:59 +0800125
126 Raises:
127 RuntimeError is the database and GnuPG home have already been initialized.
128 """
Mao Huang700663d2015-08-12 09:58:59 +0800129 # Create GPG instance and database connection.
130 self.gpg = gnupg.GPG(gnupghome=self.gnupg_homedir)
131 self.db_connection, self.db_cursor = GetSQLite3Connection(
132 self.database_file_path)
133
Mao Huang3963b372015-12-11 15:20:19 +0800134 # If any key exists, the system has already been initialized.
135 if self.gpg.list_keys():
136 raise RuntimeError('Already initialized')
Mao Huang700663d2015-08-12 09:58:59 +0800137
Mao Huang3963b372015-12-11 15:20:19 +0800138 if server_key_file_path: # use existing key
139 server_key_fingerprint, _ = self._ImportGPGKey(server_key_file_path)
140 else: # generate a new GPG key
141 if gpg_gen_key_args_dict is None:
142 gpg_gen_key_args_dict = {}
143 if 'name_real' not in gpg_gen_key_args_dict:
144 gpg_gen_key_args_dict['name_real'] = 'DKPS Server'
145 if 'name_email' not in gpg_gen_key_args_dict:
146 gpg_gen_key_args_dict['name_email'] = 'chromeos-factory-dkps@google.com'
147 if 'name_comment' not in gpg_gen_key_args_dict:
148 gpg_gen_key_args_dict['name_comment'] = 'DRM Keys Provisioning Server'
149 key_input_data = self.gpg.gen_key_input(**gpg_gen_key_args_dict)
150 server_key_fingerprint = self.gpg.gen_key(key_input_data).fingerprint
Mao Huang700663d2015-08-12 09:58:59 +0800151
152 # Create and set up the schema of the database.
153 with open(CREATE_DATABASE_SQL_FILE_PATH) as f:
154 create_database_sql = f.read()
155 with self.db_connection:
156 self.db_cursor.executescript(create_database_sql)
157
158 # Record the server key fingerprint.
159 with self.db_connection:
160 self.db_cursor.execute(
161 'INSERT INTO settings (key, value) VALUES (?, ?)',
Mao Huang3963b372015-12-11 15:20:19 +0800162 ('server_key_fingerprint', server_key_fingerprint))
Mao Huang700663d2015-08-12 09:58:59 +0800163
164 def Destroy(self):
165 """Destroys the database and GnuPG home directory.
166
167 This is the opposite of Initialize(). It essentially removes the SQLite3
168 database file and GnuPG home directory.
169 """
170 # Remove database.
171 if self.db_connection:
172 self.db_connection.close()
173 if os.path.exists(self.database_file_path):
174 os.remove(self.database_file_path)
175
176 # Remove GnuPG home.
177 if self.gpg:
178 self.gpg = None
179 if os.path.exists(self.gnupg_homedir):
180 shutil.rmtree(self.gnupg_homedir)
181
182 def AddProject(self, name, uploader_key_file_path, requester_key_file_path,
Mao Huang9076f632015-09-24 17:38:47 +0800183 parser_module_file_name, filter_module_file_name=None):
Mao Huang700663d2015-08-12 09:58:59 +0800184 """Adds a project.
185
186 Args:
187 name: name of the project, must be unique.
188 uploader_key_file_path: path to the OEM's public key file.
189 requester_key_file_path: path to the ODM's public key file.
Mao Huang9076f632015-09-24 17:38:47 +0800190 parser_module_file_name: file name of the parser python module.
Mao Huang700663d2015-08-12 09:58:59 +0800191 filter_module_file_name: file name of the filter python module.
192
193 Raises:
194 ValueError if either the uploader's or requester's key are imported (which
195 means they are used by another project).
196 """
Mao Huang9076f632015-09-24 17:38:47 +0800197 # Try to load the parser and filter modules.
198 self._LoadParserModule(parser_module_file_name)
Mao Huang700663d2015-08-12 09:58:59 +0800199 if filter_module_file_name is not None:
200 self._LoadFilterModule(filter_module_file_name)
201
202 # Try to import uploader and requester keys and add project info into the
203 # database, if failed at any step, delete imported keys.
204 uploader_key_fingerprint, requester_key_fingerprint = (None, None)
205 uploader_key_already_exists, requester_key_already_exists = (False, False)
206 try:
207 uploader_key_fingerprint, uploader_key_already_exists = (
208 self._ImportGPGKey(uploader_key_file_path))
209 if uploader_key_already_exists:
210 raise ValueError('Uploader key already exists')
211 requester_key_fingerprint, requester_key_already_exists = (
212 self._ImportGPGKey(requester_key_file_path))
213 if requester_key_already_exists:
214 raise ValueError('Requester key already exists')
215 with self.db_connection:
216 self.db_cursor.execute(
Mao Huang9076f632015-09-24 17:38:47 +0800217 'INSERT INTO projects ('
218 ' name, uploader_key_fingerprint, requester_key_fingerprint, '
219 ' parser_module_file_name, filter_module_file_name) '
220 'VALUES (?, ?, ?, ?, ?)',
Mao Huang700663d2015-08-12 09:58:59 +0800221 (name, uploader_key_fingerprint, requester_key_fingerprint,
Mao Huang9076f632015-09-24 17:38:47 +0800222 parser_module_file_name, filter_module_file_name))
Mao Huang700663d2015-08-12 09:58:59 +0800223 except BaseException:
224 if not uploader_key_already_exists and uploader_key_fingerprint:
225 self.gpg.delete_keys(uploader_key_fingerprint)
226 if not requester_key_already_exists and requester_key_fingerprint:
227 self.gpg.delete_keys(requester_key_fingerprint)
228 raise
229
230 def UpdateProject(self, name, uploader_key_file_path=None,
231 requester_key_file_path=None, filter_module_file_name=None):
232 """Updates a project.
233
234 Args:
235 name: name of the project, must be unique.
236 uploader_key_file_path: path to the OEM's public key file.
237 requester_key_file_path: path to the ODM's public key file.
238 filter_module_file_name: file name of the filter python module.
239
240 Raises:
241 RuntimeError if SQLite3 can't update the project row (for any reason).
242 """
243 # Try to load the filter module.
244 if filter_module_file_name is not None:
245 self._LoadFilterModule(filter_module_file_name)
246
247 project = self._FetchProjectByName(name)
248
249 # Try to import uploader and requester keys and add project info into the
250 # database, if failed at any step, delete any newly imported keys.
251 uploader_key_fingerprint, requester_key_fingerprint = (None, None)
252 old_uploader_key_fingerprint = project['uploader_key_fingerprint']
253 old_requester_key_fingerprint = project['requester_key_fingerprint']
254 same_uploader_key, same_requester_key = (True, True)
255 try:
256 sql_set_clause_list = ['filter_module_file_name = ?']
257 sql_parameters = [filter_module_file_name]
258
259 if uploader_key_file_path:
260 uploader_key_fingerprint, same_uploader_key = self._ImportGPGKey(
261 uploader_key_file_path)
262 sql_set_clause_list.append('uploader_key_fingerprint = ?')
263 sql_parameters.append(uploader_key_fingerprint)
264
265 if requester_key_file_path:
266 requester_key_fingerprint, same_requester_key = self._ImportGPGKey(
267 uploader_key_file_path)
268 sql_set_clause_list.append('requester_key_fingerprint = ?')
269 sql_parameters.append(requester_key_fingerprint)
270
271 sql_set_clause = ','.join(sql_set_clause_list)
272 sql_parameters.append(name)
273 with self.db_connection:
274 self.db_cursor.execute(
275 'UPDATE projects SET %s WHERE name = ?' % sql_set_clause,
276 tuple(sql_parameters))
277 if self.db_cursor.rowcount != 1:
278 raise RuntimeError('Failed to update project %s' % name)
279 except BaseException:
280 if not same_uploader_key and uploader_key_fingerprint:
281 self.gpg.delete_keys(uploader_key_fingerprint)
282 if not same_requester_key and requester_key_fingerprint:
283 self.gpg.delete_keys(requester_key_fingerprint)
284 raise
285
286 if not same_uploader_key:
287 self.gpg.delete_keys(old_uploader_key_fingerprint)
288 if not same_requester_key:
289 self.gpg.delete_keys(old_requester_key_fingerprint)
290
291 def RemoveProject(self, name):
292 """Removes a project.
293
294 Args:
295 name: the name of the project specified when added.
296 """
297 project = self._FetchProjectByName(name)
298
299 self.gpg.delete_keys(project['uploader_key_fingerprint'])
300 self.gpg.delete_keys(project['requester_key_fingerprint'])
301
302 with self.db_connection:
303 self.db_cursor.execute(
304 'DELETE FROM drm_keys WHERE project_name = ?', (name,))
305 self.db_cursor.execute('DELETE FROM projects WHERE name = ?', (name,))
306
307 def ListProjects(self):
308 """Lists all projects."""
309 self.db_cursor.execute('SELECT * FROM projects ORDER BY name ASC')
310 return self.db_cursor.fetchall()
311
312 def Upload(self, encrypted_serialized_drm_keys):
313 """Uploads a list of DRM keys to the server. This is an atomic operation. It
314 will either succeed and save all the keys, or fail and save no keys.
315
316 Args:
317 encrypted_serialized_drm_keys: the serialized DRM keys signed by the
318 uploader and encrypted by the server's public key.
319
320 Raises:
321 InvalidUploaderException if the signature of the uploader can not be
322 verified.
323 """
324 decrypted_obj = self.gpg.decrypt(encrypted_serialized_drm_keys)
325 project = self._FetchProjectByUploaderKeyFingerprint(
326 decrypted_obj.fingerprint)
327 serialized_drm_keys = decrypted_obj.data
328
Mao Huang9076f632015-09-24 17:38:47 +0800329 # Pass to the parse function.
330 parser_module = self._LoadParserModule(project['parser_module_file_name'])
331 drm_key_list = parser_module.Parse(serialized_drm_keys)
332
333 drm_key_hash_list = []
334 for drm_key in drm_key_list:
335 drm_key_hash_list.append(hashlib.sha1(json.dumps(drm_key)).hexdigest())
336
Mao Huangecbeb122016-06-22 20:30:38 +0800337 # Pass to the filter function if needed.
338 if project['filter_module_file_name']: # filter module can be null
339 filter_module = self._LoadFilterModule(project['filter_module_file_name'])
340 filtered_drm_key_list = filter_module.Filter(drm_key_list)
341 else:
342 # filter module is optional
343 filtered_drm_key_list = drm_key_list
Mao Huang700663d2015-08-12 09:58:59 +0800344
345 # Fetch server key for signing.
346 server_key_fingerprint = self._FetchServerKeyFingerprint()
347
348 # Sign and encrypt each key by server's private key and requester's public
349 # key, respectively.
350 encrypted_serialized_drm_key_list = []
351 requester_key_fingerprint = project['requester_key_fingerprint']
352 for drm_key in filtered_drm_key_list:
353 encrypted_obj = self.gpg.encrypt(
354 json.dumps(drm_key), requester_key_fingerprint,
355 always_trust=True, sign=server_key_fingerprint)
356 encrypted_serialized_drm_key_list.append(encrypted_obj.data)
357
358 # Insert into the database.
359 with self.db_connection:
360 self.db_cursor.executemany(
Mao Huang9076f632015-09-24 17:38:47 +0800361 'INSERT INTO drm_keys ('
362 ' project_name, drm_key_hash, encrypted_drm_key) '
363 'VALUES (?, ?, ?)',
Mao Huang700663d2015-08-12 09:58:59 +0800364 zip([project['name']] * len(encrypted_serialized_drm_key_list),
Mao Huang9076f632015-09-24 17:38:47 +0800365 drm_key_hash_list, encrypted_serialized_drm_key_list))
Mao Huang700663d2015-08-12 09:58:59 +0800366
367 def AvailableKeyCount(self, requester_signature):
368 """Queries the number of remaining keys.
369
370 Args:
371 requester_signature: a message signed by the requester. Since the server
372 doesn't need any additional info from the requester, the requester can
373 simply sign a random string and send it here.
374
375 Returns:
376 The number of remaining keys that can be requested.
377
378 Raises:
379 InvalidRequesterException if the signature of the requester can not be
380 verified.
381 """
382 verified = self.gpg.verify(requester_signature)
383 if not verified:
384 raise InvalidRequesterException(
385 'Invalid requester, check your signing key')
386
387 project = self._FetchProjectByRequesterKeyFingerprint(verified.fingerprint)
388
389 self.db_cursor.execute(
390 'SELECT COUNT(*) AS available_key_count FROM drm_keys '
391 'WHERE project_name = ? AND device_serial_number IS NULL',
392 (project['name'],))
393 return self.db_cursor.fetchone()['available_key_count']
394
395 def Request(self, encrypted_device_serial_number):
396 """Requests a DRM key by device serial number.
397
398 Args:
399 encrypted_device_serial_number: the device serial number signed by the
400 requester and encrypted by the server's public key.
401
402 Raises:
403 InvalidRequesterException if the signature of the requester can not be
404 verified. RuntimeError if no available keys left in the database.
405 """
406 decrypted_obj = self.gpg.decrypt(encrypted_device_serial_number)
407 project = self._FetchProjectByRequesterKeyFingerprint(
408 decrypted_obj.fingerprint)
409 device_serial_number = decrypted_obj.data
410
411 def FetchDRMKeyByDeviceSerialNumber(project_name, device_serial_number):
412 self.db_cursor.execute(
413 'SELECT * FROM drm_keys WHERE project_name = ? AND '
414 'device_serial_number = ?',
415 (project_name, device_serial_number))
416 return self.db_cursor.fetchone()
417
418 row = FetchDRMKeyByDeviceSerialNumber(project['name'], device_serial_number)
419 if row: # the SN has already paired
420 return row['encrypted_drm_key']
421
422 # Find an unpaired key.
423 with self.db_connection:
424 # SQLite3 does not support using LIMIT clause in UPDATE statement by
425 # default, unless SQLITE_ENABLE_UPDATE_DELETE_LIMIT flag is defined during
426 # compilation. Since this script may be deployed on partner's computer,
427 # we'd better assume they don't have this flag on.
428 self.db_cursor.execute(
429 'UPDATE drm_keys SET device_serial_number = ? '
430 'WHERE id = (SELECT id FROM drm_keys WHERE project_name = ? AND '
431 ' device_serial_number IS NULL LIMIT 1)',
432 (device_serial_number, project['name']))
433 if self.db_cursor.rowcount != 1: # insufficient keys
434 raise RuntimeError(
435 'Insufficient DRM keys, ask for the OEM to upload more')
436
437 row = FetchDRMKeyByDeviceSerialNumber(project['name'], device_serial_number)
438 if row:
439 return row['encrypted_drm_key']
440 else:
441 raise RuntimeError('Failed to find paired DRM key')
442
443 def ListenForever(self, ip, port):
444 """Starts the XML RPC server waiting for commands.
445
446 Args:
447 ip: IP to bind.
448 port: port to bind.
449 """
450 server = SimpleXMLRPCServer.SimpleXMLRPCServer((ip, port), allow_none=True)
451
452 server.register_introspection_functions()
453 server.register_function(self.AvailableKeyCount)
454 server.register_function(self.Upload)
455 server.register_function(self.Request)
456
Mao Huang041483e2015-09-14 23:28:18 +0800457 # Redirect stdout and stderr to log file.
458 log_file = open(LOG_FILE_PATH, 'a')
459 sys.stdout = log_file
460 sys.stderr = log_file
461
Mao Huang700663d2015-08-12 09:58:59 +0800462 server.serve_forever()
463
464 def _ImportGPGKey(self, key_file_path):
465 """Imports a GPG key from a file.
466
467 Args:
468 key_file_path: path to the GPG key file.
469
470 Returns:
471 A tuple (key_fingerprint, key_already_exists). The 1st element is the
472 imported key's fingerprint, and the 2nd element is True if the key was
473 already in the database before importing, False otherwise.
474 """
475 with open(key_file_path) as f:
476 import_results = self.gpg.import_keys(f.read())
477 key_already_exists = (import_results.imported == 0)
478 key_fingerprint = import_results.fingerprints[0]
479 return (key_fingerprint, key_already_exists)
480
481 def _LoadFilterModule(self, filter_module_file_name):
482 """Loads the filter module.
483
484 Args:
Mao Huang9076f632015-09-24 17:38:47 +0800485 filter_module_file_name: file name of the filter module in FILTERS_DIR.
Mao Huang700663d2015-08-12 09:58:59 +0800486
487 Returns:
488 The loaded filter module on success.
489
490 Raises:
491 Exception if failed, see imp.load_source()'s doc for what could be raised.
492 """
493 return imp.load_source(
494 'filter_module', os.path.join(FILTERS_DIR, filter_module_file_name))
495
Mao Huang9076f632015-09-24 17:38:47 +0800496 def _LoadParserModule(self, parser_module_file_name):
497 """Loads the parser module.
498
499 Args:
500 parser_module_file_name: file name of the parser module in PARSERS_DIR.
501
502 Returns:
503 The loaded parser module on success.
504
505 Raises:
506 Exception if failed, see imp.load_source()'s doc for what could be raised.
507 """
508 return imp.load_source(
509 'parser_module', os.path.join(PARSERS_DIR, parser_module_file_name))
510
Mao Huang700663d2015-08-12 09:58:59 +0800511 def _FetchServerKeyFingerprint(self):
512 """Returns the server GPG key's fingerprint."""
513 self.db_cursor.execute(
514 "SELECT * FROM settings WHERE key = 'server_key_fingerprint'")
515 row = self.db_cursor.fetchone()
516 if not row:
517 raise ValueError('Server key fingerprint not exists')
518 return row['value']
519
520 def _FetchOneProject(self, name=None, # pylint: disable=W0613
521 uploader_key_fingerprint=None, # pylint: disable=W0613
522 requester_key_fingerprint=None, # pylint: disable=W0613
523 exception_type=None, error_msg=None):
524 """Fetches the project by name, uploader key fingerprint, or requester key
525 fingerprint.
526
527 This function combines the name, uploader_key_fingerprint,
528 requester_key_fingerprint conditions (if not None) with the AND operator,
529 and tries to fetch one project from the database.
530
531 Args:
532 name: name of the project.
533 uploader_key_fingerprint: uploader key fingerprint of the project.
534 requester_key_fingerprint: requester key fingerprint of the project.
535 exception_type: if no project was found and exception_type is not None,
536 raise exception_type with error_msg.
537 error_msg: if no project was found and exception_type is not None, raise
538 exception_type with error_msg.
539
540 Returns:
541 A project that matches the name, uploader_key_fingerprint, and
542 requester_key_fingerprint conditiions.
543
544 Raises:
545 exception_type with error_msg if not project was found.
546 """
547 where_clause_list = []
548 params = []
549 local_vars = locals()
550 for param_name in ['name', 'uploader_key_fingerprint',
551 'requester_key_fingerprint']:
552 if local_vars[param_name] is not None:
553 where_clause_list.append('%s = ?' % param_name)
554 params.append(locals()[param_name])
555 if not where_clause_list:
556 raise ValueError('No conditions given to fetch the project')
557 where_clause = 'WHERE ' + ' AND '.join(where_clause_list)
558
559 self.db_cursor.execute(
560 'SELECT * FROM projects %s' % where_clause, tuple(params))
561 project = self.db_cursor.fetchone()
562
563 if not project and exception_type:
564 raise exception_type(error_msg)
565
566 return project
567
568 def _FetchProjectByName(self, name):
569 return self._FetchOneProject(
570 name=name, exception_type=ProjectNotFoundException,
571 error_msg=('Project %s not found' % name))
572
573 def _FetchProjectByUploaderKeyFingerprint(self, uploader_key_fingerprint):
574 return self._FetchOneProject(
575 uploader_key_fingerprint=uploader_key_fingerprint,
576 exception_type=InvalidUploaderException,
577 error_msg='Invalid uploader, check your signing key')
578
579 def _FetchProjectByRequesterKeyFingerprint(self, requester_key_fingerprint):
580 return self._FetchOneProject(
581 requester_key_fingerprint=requester_key_fingerprint,
582 exception_type=InvalidRequesterException,
583 error_msg='Invalid requester, check your signing key')
584
585
586def _ParseArguments():
587 parser = argparse.ArgumentParser()
588 parser.add_argument(
589 '-d', '--database_file_path', default=os.path.join(SCRIPT_DIR, 'dkps.db'),
590 help='path to the SQLite3 database file, default to "dkps.db" in the '
591 'same directory of this script')
592 parser.add_argument(
593 '-g', '--gnupg_homedir', default=os.path.join(SCRIPT_DIR, 'gnupg'),
594 help='path to the GnuGP home directory, default to "gnupg" in the same '
595 'directory of this script')
596 subparsers = parser.add_subparsers(dest='command')
597
598 parser_add = subparsers.add_parser('add', help='adds a new project')
599 parser_add.add_argument('-n', '--name', required=True,
600 help='name of the new project')
601 parser_add.add_argument('-u', '--uploader_key_file_path', required=True,
602 help="path to the uploader's public key file")
603 parser_add.add_argument('-r', '--requester_key_file_path', required=True,
604 help="path to the requester's public key file")
Mao Huang9076f632015-09-24 17:38:47 +0800605 parser_add.add_argument('-p', '--parser_module_file_name', required=True,
606 help='file name of the parser module')
Mao Huang700663d2015-08-12 09:58:59 +0800607 parser_add.add_argument('-f', '--filter_module_file_name', default=None,
608 help='file name of the filter module')
609
610 subparsers.add_parser('destroy', help='destroys the database')
611
612 parser_update = subparsers.add_parser('update',
613 help='updates an existing project')
614 parser_update.add_argument('-n', '--name', required=True,
615 help='name of the project')
616 parser_update.add_argument('-u', '--uploader_key_file_path', default=None,
617 help="path to the uploader's public key file")
618 parser_update.add_argument('-r', '--requester_key_file_path', default=None,
619 help="path to the requester's public key file")
620 parser_update.add_argument('-f', '--filter_module_file_name', default=None,
621 help='file name of the filter module')
622
623 parser_init = subparsers.add_parser('init', help='initializes the database')
624 parser_init.add_argument(
625 '-g', '--gpg_gen_key_args', action='append', nargs=2, default={},
626 help='arguments to use when generating GPG key for server')
627
628 subparsers.add_parser('list', help='lists all projects')
629
630 parser_listen = subparsers.add_parser(
631 'listen', help='starts the server, waiting for upload or request keys')
632 parser_listen.add_argument(
633 '--ip', default='0.0.0.0', help='IP to bind, default to 0.0.0.0')
634 parser_listen.add_argument(
635 '--port', type=int, default=5438, help='port to listen, default to 5438')
636
637 parser_rm = subparsers.add_parser('rm', help='removes an existing project')
638 parser_rm.add_argument('-n', '--name', required=True,
639 help='name of the project to remove')
640
641 return parser.parse_args()
642
643
644def main():
645 args = _ParseArguments()
646
Mao Huang041483e2015-09-14 23:28:18 +0800647 # TODO(littlecvr): Customizable logging level.
648 # TODO(littlecvr): Also print on stdout if it's not running in background.
649 logging.basicConfig(
650 filename=LOG_FILE_PATH,
651 format='%(asctime)s:%(levelname)s:%(funcName)s:%(lineno)d:%(message)s',
652 level=logging.DEBUG)
653 logging.debug('Parsed arguments: %r', args)
654
Mao Huang700663d2015-08-12 09:58:59 +0800655 dkps = DRMKeysProvisioningServer(args.database_file_path, args.gnupg_homedir)
656 if args.command == 'init':
657 # Convert from command line arguments to a dict.
658 gpg_gen_key_args_dict = {}
659 for pair in args.gpg_gen_key_args:
660 gpg_gen_key_args_dict[pair[0]] = pair[1]
661 dkps.Initialize(gpg_gen_key_args_dict)
662 elif args.command == 'destroy':
663 message = (
664 'This action will remove all projects and keys information and is NOT '
665 'recoverable! Are you sure? (y/N)')
666 answer = raw_input(textwrap.fill(message, 80) + ' ')
667 if answer.lower() != 'y' and answer.lower() != 'yes':
668 print 'OK, nothing will be removed.'
669 else:
670 print 'Removing all projects and keys information...',
671 dkps.Destroy()
672 print 'done.'
673 elif args.command == 'listen':
674 dkps.ListenForever(args.ip, args.port)
675 elif args.command == 'list':
676 print dkps.ListProjects()
677 elif args.command == 'add':
678 dkps.AddProject(
679 args.name, args.uploader_key_file_path, args.requester_key_file_path,
Mao Huangecbeb122016-06-22 20:30:38 +0800680 args.parser_module_file_name, args.filter_module_file_name)
Mao Huang700663d2015-08-12 09:58:59 +0800681 elif args.command == 'update':
682 dkps.UpdateProject(
683 args.name, args.uploader_key_file_path, args.requester_key_file_path,
684 args.filter_module_file_name)
685 elif args.command == 'rm':
686 dkps.RemoveProject(args.name)
687 else:
688 raise ValueError('Unknown command %s' % args.command)
689
690
691if __name__ == '__main__':
692 main()