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