1#!/usr/bin/env python
2
3"""
4<Program Name>
5  interface.py
6
7<Author>
8  Vladimir Diaz <vladimir.v.diaz@gmail.com>
9
10<Started>
11  January 5, 2017.
12
13<Copyright>
14  See LICENSE for licensing information.
15
16<Purpose>
17  Provide an interface to the cryptography functions available in
18  securesystemslib.  The interface can be used with the Python interpreter in
19  interactive mode, or imported directly into a Python module.  See
20  'securesystemslib/README' for the complete guide to using 'interface.py'.
21"""
22
23# Help with Python 3 compatibility, where the print statement is a function, an
24# implicit relative import is invalid, and the '/' operator performs true
25# division.  Example:  print 'hello world' raises a 'SyntaxError' exception.
26from __future__ import print_function
27from __future__ import absolute_import
28from __future__ import division
29from __future__ import unicode_literals
30
31import os
32import errno
33import sys
34import time
35import datetime
36import getpass
37import logging
38import tempfile
39import shutil
40import json
41import gzip
42import random
43
44import securesystemslib.formats
45import securesystemslib.settings
46import securesystemslib.util
47import securesystemslib.keys
48
49import six
50
51from colorama import Fore
52
53# See 'log.py' to learn how logging is handled in securesystemslib.
54logger = logging.getLogger('securesystemslib_interface')
55
56
57# Recommended RSA key sizes:
58# https://en.wikipedia.org/wiki/Key_size#Asymmetric_algorithm_key_lengths
59# Based on the above, RSA keys of size 3072 bits are expected to provide
60# security through 2031 and beyond.
61DEFAULT_RSA_KEY_BITS = 3072
62
63# Supported key types.
64SUPPORTED_KEY_TYPES = ['rsa', 'ed25519']
65
66
67
68def _prompt(message, result_type=str):
69  """
70    Non-public function that prompts the user for input by logging 'message',
71    converting the input to 'result_type', and returning the value to the
72    caller.
73  """
74
75  return result_type(six.moves.input(message))
76
77
78
79
80
81def get_password(prompt='Password: ', confirm=False):
82  """
83  <Purpose>
84    Return the password entered by the user.  If 'confirm' is True, the user is
85    asked to enter the previously entered password once again.  If they match,
86    the password is returned to the caller.
87
88  <Arguments>
89    prompt:
90      The text of the password prompt that is displayed to the user.
91
92    confirm:
93      Boolean indicating whether the user should be prompted for the password
94      a second time.  The two entered password must match, otherwise the
95      user is again prompted for a password.
96
97  <Exceptions>
98    None.
99
100  <Side Effects>
101    None.
102
103  <Returns>
104    The password entered by the user.
105  """
106
107  # Are the arguments the expected type?
108  # If not, raise 'securesystemslib.exceptions.FormatError'.
109  securesystemslib.formats.TEXT_SCHEMA.check_match(prompt)
110  securesystemslib.formats.BOOLEAN_SCHEMA.check_match(confirm)
111
112  while True:
113    # getpass() prompts the user for a password without echoing
114    # the user input.
115    password = getpass.getpass(prompt, sys.stderr)
116
117    if not confirm:
118      return password
119    password2 = getpass.getpass('Confirm: ', sys.stderr)
120
121    if password == password2:
122      return password
123
124    else:
125      print('Mismatch; try again.')
126
127
128
129
130
131def generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS,
132    password=None):
133  """
134  <Purpose>
135    Generate an RSA key pair.  The public portion of the generated RSA key is
136    saved to <'filepath'>.pub, whereas the private key portion is saved to
137    <'filepath'>.  If no password is given, the user is prompted for one.  If
138    the 'password' is an empty string, the private key is saved unencrypted to
139    <'filepath'>.  If the filepath is not given, the KEYID is used as the
140    filename and the keypair saved to the current working directory.
141
142    The best available form of encryption, for a given key's backend, is used
143    with pyca/cryptography.  According to their documentation, "it is a curated
144    encryption choice and the algorithm may change over time."
145
146  <Arguments>
147    filepath:
148      The public and private key files are saved to <filepath>.pub and
149      <filepath>, respectively.  If the filepath is not given, the public and
150      private keys are saved to the current working directory as <KEYID>.pub
151      and <KEYID>.  KEYID is the generated key's KEYID.
152
153    bits:
154      The number of bits of the generated RSA key.
155
156    password:
157      The password to encrypt 'filepath'.  If None, the user is prompted for a
158      password.  If an empty string is given, the private key is written to
159      disk unencrypted.
160
161  <Exceptions>
162    securesystemslib.exceptions.FormatError, if the arguments are improperly
163    formatted.
164
165  <Side Effects>
166    Writes key files to '<filepath>' and '<filepath>.pub'.
167
168  <Returns>
169    The 'filepath' of the written key.
170  """
171
172  # Does 'bits' have the correct format?
173  # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
174  securesystemslib.formats.RSAKEYBITS_SCHEMA.check_match(bits)
175
176  # Generate the public and private RSA keys.
177  rsa_key = securesystemslib.keys.generate_rsa_key(bits)
178  public = rsa_key['keyval']['public']
179  private = rsa_key['keyval']['private']
180
181  if not filepath:
182    filepath = os.path.join(os.getcwd(), rsa_key['keyid'])
183
184  else:
185    logger.debug('The filepath has been specified.  Not using the key\'s'
186        ' KEYID as the default filepath.')
187
188  # Does 'filepath' have the correct format?
189  securesystemslib.formats.PATH_SCHEMA.check_match(filepath)
190
191  # If the caller does not provide a password argument, prompt for one.
192  if password is None: # pragma: no cover
193
194    # It is safe to specify the full path of 'filepath' in the prompt and not
195    # worry about leaking sensitive information about the key's location.
196    # However, care should be taken when including the full path in exceptions
197    # and log files.
198    password = get_password('Enter a password for the encrypted RSA'
199        ' key (' + Fore.RED + filepath + Fore.RESET + '): ',
200        confirm=True)
201
202  else:
203    logger.debug('The password has been specified.  Not prompting for one')
204
205  # Does 'password' have the correct format?
206  securesystemslib.formats.PASSWORD_SCHEMA.check_match(password)
207
208  # Encrypt the private key if 'password' is set.
209  if len(password):
210    private = securesystemslib.keys.create_rsa_encrypted_pem(private, password)
211
212  else:
213    logger.debug('An empty password was given.  Not encrypting the private key.')
214
215  # If the parent directory of filepath does not exist,
216  # create it (and all its parent directories, if necessary).
217  securesystemslib.util.ensure_parent_dir(filepath)
218
219  # Write the public key (i.e., 'public', which is in PEM format) to
220  # '<filepath>.pub'.  (1) Create a temporary file, (2) write the contents of
221  # the public key, and (3) move to final destination.
222  file_object = securesystemslib.util.TempFile()
223  file_object.write(public.encode('utf-8'))
224  # The temporary file is closed after the final move.
225  file_object.move(filepath + '.pub')
226
227  # Write the private key in encrypted PEM format to '<filepath>'.
228  # Unlike the public key file, the private key does not have a file
229  # extension.
230  file_object = securesystemslib.util.TempFile()
231  file_object.write(private.encode('utf-8'))
232  file_object.move(filepath)
233
234  return filepath
235
236
237
238
239def import_rsa_privatekey_from_file(filepath, password=None,
240    scheme='rsassa-pss-sha256', prompt=False):
241  """
242  <Purpose>
243    Import the PEM file in 'filepath' containing the private key.
244
245    If password is passed use passed password for decryption.
246    If prompt is True use entered password for decryption.
247    If no password is passed and either prompt is False or if the password
248    entered at the prompt is an empty string, omit decryption, treating the
249    key as if it is not encrypted.
250    If password is passed and prompt is True, an error is raised. (See below.)
251
252    The returned key is an object in the
253    'securesystemslib.formats.RSAKEY_SCHEMA' format.
254
255  <Arguments>
256    filepath:
257      <filepath> file, an RSA encrypted PEM file.  Unlike the public RSA PEM
258      key file, 'filepath' does not have an extension.
259
260    password:
261      The passphrase to decrypt 'filepath'.
262
263    scheme:
264      The signature scheme used by the imported key.
265
266    prompt:
267      If True the user is prompted for a passphrase to decrypt 'filepath'.
268      Default is False.
269
270  <Exceptions>
271    ValueError, if 'password' is passed and 'prompt' is True.
272
273    ValueError, if 'password' is passed and it is an empty string.
274
275    securesystemslib.exceptions.FormatError, if the arguments are improperly
276    formatted.
277
278    securesystemslib.exceptions.FormatError, if the entered password is
279    improperly formatted.
280
281    IOError, if 'filepath' can't be loaded.
282
283    securesystemslib.exceptions.CryptoError, if a password is available
284    and 'filepath' is not a valid key file encrypted using that password.
285
286    securesystemslib.exceptions.CryptoError, if no password is available
287    and 'filepath' is not a valid non-encrypted key file.
288
289  <Side Effects>
290    The contents of 'filepath' are read, optionally decrypted, and returned.
291
292  <Returns>
293    An RSA key object, conformant to 'securesystemslib.formats.RSAKEY_SCHEMA'.
294
295  """
296
297  # Does 'filepath' have the correct format?
298  # Ensure the arguments have the appropriate number of objects and object
299  # types, and that all dict keys are properly named.
300  # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
301  securesystemslib.formats.PATH_SCHEMA.check_match(filepath)
302
303  # Is 'scheme' properly formatted?
304  securesystemslib.formats.RSA_SCHEME_SCHEMA.check_match(scheme)
305
306  if password and prompt:
307    raise ValueError("Passing 'password' and 'prompt' True is not allowed.")
308
309  # If 'password' was passed check format and that it is not empty.
310  if password is not None:
311    securesystemslib.formats.PASSWORD_SCHEMA.check_match(password)
312
313    # TODO: PASSWORD_SCHEMA should be securesystemslib.schema.AnyString(min=1)
314    if not len(password):
315      raise ValueError('Password must be 1 or more characters')
316
317  elif prompt:
318    # Password confirmation disabled here, which should ideally happen only
319    # when creating encrypted key files (i.e., improve usability).
320    # It is safe to specify the full path of 'filepath' in the prompt and not
321    # worry about leaking sensitive information about the key's location.
322    # However, care should be taken when including the full path in exceptions
323    # and log files.
324    # NOTE: A user who gets prompted for a password, can only signal that the
325    # key is not encrypted by entering no password in the prompt, as opposed
326    # to a programmer who can call the function with or without a 'password'.
327    # Hence, we treat an empty password here, as if no 'password' was passed.
328    password = get_password('Enter a password for an encrypted RSA'
329        ' file \'' + Fore.RED + filepath + Fore.RESET + '\': ',
330        confirm=False) or None
331
332  if password is not None:
333    # This check will not fail, because a mal-formatted passed password fails
334    # above and an entered password will always be a string (see get_password)
335    # However, we include it in case PASSWORD_SCHEMA or get_password changes.
336    securesystemslib.formats.PASSWORD_SCHEMA.check_match(password)
337
338  else:
339    logger.debug('No password was given. Attempting to import an'
340        ' unencrypted file.')
341
342  # Read the contents of 'filepath' that should be a PEM formatted private key.
343  with open(filepath, 'rb') as file_object:
344    pem_key = file_object.read().decode('utf-8')
345
346  # Convert 'pem_key' to 'securesystemslib.formats.RSAKEY_SCHEMA' format.
347  # Raise 'securesystemslib.exceptions.CryptoError' if 'pem_key' is invalid.
348  # If 'password' is None decryption will be omitted.
349  rsa_key = securesystemslib.keys.import_rsakey_from_private_pem(pem_key,
350      scheme, password)
351
352  return rsa_key
353
354
355
356
357
358def import_rsa_publickey_from_file(filepath):
359  """
360  <Purpose>
361    Import the RSA key stored in 'filepath'.  The key object returned is in the
362    format 'securesystemslib.formats.RSAKEY_SCHEMA'.  If the RSA PEM in
363    'filepath' contains a private key, it is discarded.
364
365  <Arguments>
366    filepath:
367      <filepath>.pub file, an RSA PEM file.
368
369  <Exceptions>
370    securesystemslib.exceptions.FormatError, if 'filepath' is improperly
371    formatted.
372
373    securesystemslib.exceptions.Error, if a valid RSA key object cannot be
374    generated.  This may be caused by an improperly formatted PEM file.
375
376  <Side Effects>
377    'filepath' is read and its contents extracted.
378
379  <Returns>
380    An RSA key object conformant to 'securesystemslib.formats.RSAKEY_SCHEMA'.
381  """
382
383  # Does 'filepath' have the correct format?
384  # Ensure the arguments have the appropriate number of objects and object
385  # types, and that all dict keys are properly named.
386  # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
387  securesystemslib.formats.PATH_SCHEMA.check_match(filepath)
388
389  # Read the contents of the key file that should be in PEM format and contains
390  # the public portion of the RSA key.
391  with open(filepath, 'rb') as file_object:
392    rsa_pubkey_pem = file_object.read().decode('utf-8')
393
394  # Convert 'rsa_pubkey_pem' to 'securesystemslib.formats.RSAKEY_SCHEMA' format.
395  try:
396    rsakey_dict = securesystemslib.keys.import_rsakey_from_public_pem(rsa_pubkey_pem)
397
398  except securesystemslib.exceptions.FormatError as e:
399    raise securesystemslib.exceptions.Error('Cannot import improperly formatted'
400      ' PEM file.' + repr(str(e)))
401
402  return rsakey_dict
403
404
405
406
407
408def generate_and_write_ed25519_keypair(filepath=None, password=None):
409  """
410  <Purpose>
411    Generate an Ed25519 keypair, where the encrypted key (using 'password' as
412    the passphrase) is saved to <'filepath'>.  The public key portion of the
413    generated Ed25519 key is saved to <'filepath'>.pub.  If the filepath is not
414    given, the KEYID is used as the filename and the keypair saved to the
415    current working directory.
416
417    The private key is encrypted according to 'cryptography's approach:
418    "Encrypt using the best available encryption for a given key's backend.
419    This is a curated encryption choice and the algorithm may change over
420    time."
421
422  <Arguments>
423    filepath:
424      The public and private key files are saved to <filepath>.pub and
425      <filepath>, respectively.  If the filepath is not given, the public and
426      private keys are saved to the current working directory as <KEYID>.pub
427      and <KEYID>.  KEYID is the generated key's KEYID.
428
429    password:
430      The password, or passphrase, to encrypt the private portion of the
431      generated Ed25519 key.  A symmetric encryption key is derived from
432      'password', so it is not directly used.
433
434  <Exceptions>
435    securesystemslib.exceptions.FormatError, if the arguments are improperly
436    formatted.
437
438    securesystemslib.exceptions.CryptoError, if 'filepath' cannot be encrypted.
439
440  <Side Effects>
441    Writes key files to '<filepath>' and '<filepath>.pub'.
442
443  <Returns>
444    The 'filepath' of the written key.
445  """
446
447  # Generate a new Ed25519 key object.
448  ed25519_key = securesystemslib.keys.generate_ed25519_key()
449
450  if not filepath:
451    filepath = os.path.join(os.getcwd(), ed25519_key['keyid'])
452
453  else:
454    logger.debug('The filepath has been specified.  Not using the key\'s'
455        ' KEYID as the default filepath.')
456
457  # Does 'filepath' have the correct format?
458  # Ensure the arguments have the appropriate number of objects and object
459  # types, and that all dict keys are properly named.
460  # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
461  securesystemslib.formats.PATH_SCHEMA.check_match(filepath)
462
463  # If the caller does not provide a password argument, prompt for one.
464  if password is None: # pragma: no cover
465
466    # It is safe to specify the full path of 'filepath' in the prompt and not
467    # worry about leaking sensitive information about the key's location.
468    # However, care should be taken when including the full path in exceptions
469    # and log files.
470    password = get_password('Enter a password for the Ed25519'
471        ' key (' + Fore.RED + filepath + Fore.RESET + '): ',
472        confirm=True)
473
474  else:
475    logger.debug('The password has been specified. Not prompting for one.')
476
477  # Does 'password' have the correct format?
478  securesystemslib.formats.PASSWORD_SCHEMA.check_match(password)
479
480  # If the parent directory of filepath does not exist,
481  # create it (and all its parent directories, if necessary).
482  securesystemslib.util.ensure_parent_dir(filepath)
483
484  # Create a temporary file, write the contents of the public key, and move
485  # to final destination.
486  file_object = securesystemslib.util.TempFile()
487
488  # Generate the ed25519 public key file contents in metadata format (i.e.,
489  # does not include the keyid portion).
490  keytype = ed25519_key['keytype']
491  keyval = ed25519_key['keyval']
492  scheme = ed25519_key['scheme']
493  ed25519key_metadata_format = securesystemslib.keys.format_keyval_to_metadata(
494      keytype, scheme, keyval, private=False)
495
496  file_object.write(json.dumps(ed25519key_metadata_format).encode('utf-8'))
497
498  # Write the public key (i.e., 'public', which is in PEM format) to
499  # '<filepath>.pub'.  (1) Create a temporary file, (2) write the contents of
500  # the public key, and (3) move to final destination.
501  # The temporary file is closed after the final move.
502  file_object.move(filepath + '.pub')
503
504  # Write the encrypted key string, conformant to
505  # 'securesystemslib.formats.ENCRYPTEDKEY_SCHEMA', to '<filepath>'.
506  file_object = securesystemslib.util.TempFile()
507
508  # Encrypt the private key if 'password' is set.
509  if len(password):
510    ed25519_key = securesystemslib.keys.encrypt_key(ed25519_key, password)
511
512  else:
513    logger.debug('An empty password was given. '
514                 'Not encrypting the private key.')
515    ed25519_key = json.dumps(ed25519_key)
516
517  # Raise 'securesystemslib.exceptions.CryptoError' if 'ed25519_key' cannot be
518  # encrypted.
519  file_object.write(ed25519_key.encode('utf-8'))
520  file_object.move(filepath)
521
522  return filepath
523
524
525
526
527def import_ed25519_publickey_from_file(filepath):
528  """
529  <Purpose>
530    Load the ED25519 public key object (conformant to
531    'securesystemslib.formats.KEY_SCHEMA') stored in 'filepath'.  Return
532    'filepath' in securesystemslib.formats.ED25519KEY_SCHEMA format.
533
534    If the key object in 'filepath' contains a private key, it is discarded.
535
536  <Arguments>
537    filepath:
538      <filepath>.pub file, a public key file.
539
540  <Exceptions>
541    securesystemslib.exceptions.FormatError, if 'filepath' is improperly
542    formatted or is an unexpected key type.
543
544  <Side Effects>
545    The contents of 'filepath' is read and saved.
546
547  <Returns>
548    An ED25519 key object conformant to
549    'securesystemslib.formats.ED25519KEY_SCHEMA'.
550  """
551
552  # Does 'filepath' have the correct format?
553  # Ensure the arguments have the appropriate number of objects and object
554  # types, and that all dict keys are properly named.
555  # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
556  securesystemslib.formats.PATH_SCHEMA.check_match(filepath)
557
558  # ED25519 key objects are saved in json and metadata format.  Return the
559  # loaded key object in securesystemslib.formats.ED25519KEY_SCHEMA' format that
560  # also includes the keyid.
561  ed25519_key_metadata = securesystemslib.util.load_json_file(filepath)
562  ed25519_key, junk = \
563    securesystemslib.keys.format_metadata_to_key(ed25519_key_metadata)
564
565  # Raise an exception if an unexpected key type is imported.  Redundant
566  # validation of 'keytype'.  'securesystemslib.keys.format_metadata_to_key()'
567  # should have fully validated 'ed25519_key_metadata'.
568  if ed25519_key['keytype'] != 'ed25519': # pragma: no cover
569    message = 'Invalid key type loaded: ' + repr(ed25519_key['keytype'])
570    raise securesystemslib.exceptions.FormatError(message)
571
572  return ed25519_key
573
574
575
576
577
578def import_ed25519_privatekey_from_file(filepath, password=None, prompt=False):
579  """
580  <Purpose>
581    Import the encrypted ed25519 key file in 'filepath', decrypt it, and return
582    the key object in 'securesystemslib.formats.ED25519KEY_SCHEMA' format.
583
584    The private key (may also contain the public part) is encrypted with AES
585    256 and CTR the mode of operation.  The password is strengthened with
586    PBKDF2-HMAC-SHA256.
587
588  <Arguments>
589    filepath:
590      <filepath> file, an RSA encrypted key file.
591
592    password:
593      The password, or passphrase, to import the private key (i.e., the
594      encrypted key file 'filepath' must be decrypted before the ed25519 key
595      object can be returned.
596
597    prompt:
598      If True the user is prompted for a passphrase to decrypt 'filepath'.
599      Default is False.
600
601  <Exceptions>
602    securesystemslib.exceptions.FormatError, if the arguments are improperly
603    formatted or the imported key object contains an invalid key type (i.e.,
604    not 'ed25519').
605
606    securesystemslib.exceptions.CryptoError, if 'filepath' cannot be decrypted.
607
608  <Side Effects>
609    'password' is used to decrypt the 'filepath' key file.
610
611  <Returns>
612    An ed25519 key object of the form:
613    'securesystemslib.formats.ED25519KEY_SCHEMA'.
614  """
615
616  # Does 'filepath' have the correct format?
617  # Ensure the arguments have the appropriate number of objects and object
618  # types, and that all dict keys are properly named.
619  # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
620  securesystemslib.formats.PATH_SCHEMA.check_match(filepath)
621
622  if password and prompt:
623    raise ValueError("Passing 'password' and 'prompt' True is not allowed.")
624
625  # If 'password' was passed check format and that it is not empty.
626  if password is not None:
627    securesystemslib.formats.PASSWORD_SCHEMA.check_match(password)
628
629    # TODO: PASSWORD_SCHEMA should be securesystemslib.schema.AnyString(min=1)
630    if not len(password):
631      raise ValueError('Password must be 1 or more characters')
632
633  elif prompt:
634    # Password confirmation disabled here, which should ideally happen only
635    # when creating encrypted key files (i.e., improve usability).
636    # It is safe to specify the full path of 'filepath' in the prompt and not
637    # worry about leaking sensitive information about the key's location.
638    # However, care should be taken when including the full path in exceptions
639    # and log files.
640    # NOTE: A user who gets prompted for a password, can only signal that the
641    # key is not encrypted by entering no password in the prompt, as opposed
642    # to a programmer who can call the function with or without a 'password'.
643    # Hence, we treat an empty password here, as if no 'password' was passed.
644    password = get_password('Enter a password for an encrypted RSA'
645        ' file \'' + Fore.RED + filepath + Fore.RESET + '\': ',
646        confirm=False)
647
648    # If user sets an empty string for the password, explicitly set the
649    # password to None, because some functions may expect this later.
650    if len(password) == 0: # pragma: no cover
651      password = None
652
653  # Finally, regardless of password, try decrypting the key, if necessary.
654  # Otherwise, load it straight from the disk.
655  with open(filepath, 'rb') as file_object:
656    json_str = file_object.read()
657    return securesystemslib.keys.\
658           import_ed25519key_from_private_json(json_str, password=password)
659
660
661
662
663
664def generate_and_write_ecdsa_keypair(filepath=None, password=None):
665  """
666  <Purpose>
667    Generate an ECDSA keypair, where the encrypted key (using 'password' as the
668    passphrase) is saved to <'filepath'>.  The public key portion of the
669    generated ECDSA key is saved to <'filepath'>.pub.  If the filepath is not
670    given, the KEYID is used as the filename and the keypair saved to the
671    current working directory.
672
673    The 'cryptography' library is currently supported.  The private key is
674    encrypted according to 'cryptography's approach: "Encrypt using the best
675    available encryption for a given key's backend. This is a curated
676    encryption choice and the algorithm may change over time."
677
678  <Arguments>
679    filepath:
680      The public and private key files are saved to <filepath>.pub and
681      <filepath>, respectively.  If the filepath is not given, the public and
682      private keys are saved to the current working directory as <KEYID>.pub
683      and <KEYID>.  KEYID is the generated key's KEYID.
684
685    password:
686      The password, or passphrase, to encrypt the private portion of the
687      generated ECDSA key.  A symmetric encryption key is derived from
688      'password', so it is not directly used.
689
690  <Exceptions>
691    securesystemslib.exceptions.FormatError, if the arguments are improperly
692    formatted.
693
694    securesystemslib.exceptions.CryptoError, if 'filepath' cannot be encrypted.
695
696  <Side Effects>
697    Writes key files to '<filepath>' and '<filepath>.pub'.
698
699  <Returns>
700    The 'filepath' of the written key.
701  """
702
703  # Generate a new ECDSA key object.  The 'cryptography' library is currently
704  # supported and performs the actual cryptographic operations.
705  ecdsa_key = securesystemslib.keys.generate_ecdsa_key()
706
707  if not filepath:
708    filepath = os.path.join(os.getcwd(), ecdsa_key['keyid'])
709
710  else:
711    logger.debug('The filepath has been specified.  Not using the key\'s'
712        ' KEYID as the default filepath.')
713
714  # Does 'filepath' have the correct format?
715  # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
716  securesystemslib.formats.PATH_SCHEMA.check_match(filepath)
717
718  # If the caller does not provide a password argument, prompt for one.
719  if password is None: # pragma: no cover
720
721    # It is safe to specify the full path of 'filepath' in the prompt and not
722    # worry about leaking sensitive information about the key's location.
723    # However, care should be taken when including the full path in exceptions
724    # and log files.
725    password = get_password('Enter a password for the ECDSA'
726        ' key (' + Fore.RED + filepath + Fore.RESET + '): ',
727        confirm=True)
728
729  else:
730    logger.debug('The password has been specified.  Not prompting for one')
731
732  # Does 'password' have the correct format?
733  securesystemslib.formats.PASSWORD_SCHEMA.check_match(password)
734
735  # If the parent directory of filepath does not exist,
736  # create it (and all its parent directories, if necessary).
737  securesystemslib.util.ensure_parent_dir(filepath)
738
739  # Create a temporary file, write the contents of the public key, and move
740  # to final destination.
741  file_object = securesystemslib.util.TempFile()
742
743  # Generate the ECDSA public key file contents in metadata format (i.e., does
744  # not include the keyid portion).
745  keytype = ecdsa_key['keytype']
746  keyval = ecdsa_key['keyval']
747  scheme = ecdsa_key['scheme']
748  ecdsakey_metadata_format = securesystemslib.keys.format_keyval_to_metadata(
749      keytype, scheme, keyval, private=False)
750
751  file_object.write(json.dumps(ecdsakey_metadata_format).encode('utf-8'))
752
753  # Write the public key (i.e., 'public', which is in PEM format) to
754  # '<filepath>.pub'.  (1) Create a temporary file, (2) write the contents of
755  # the public key, and (3) move to final destination.
756  file_object.move(filepath + '.pub')
757
758  # Write the encrypted key string, conformant to
759  # 'securesystemslib.formats.ENCRYPTEDKEY_SCHEMA', to '<filepath>'.
760  file_object = securesystemslib.util.TempFile()
761  # Raise 'securesystemslib.exceptions.CryptoError' if 'ecdsa_key' cannot be
762  # encrypted.
763  encrypted_key = securesystemslib.keys.encrypt_key(ecdsa_key, password)
764  file_object.write(encrypted_key.encode('utf-8'))
765  file_object.move(filepath)
766
767  return filepath
768
769
770
771
772def import_ecdsa_publickey_from_file(filepath):
773  """
774  <Purpose>
775    Load the ECDSA public key object (conformant to
776    'securesystemslib.formats.KEY_SCHEMA') stored in 'filepath'.  Return
777    'filepath' in securesystemslib.formats.ECDSAKEY_SCHEMA format.
778
779    If the key object in 'filepath' contains a private key, it is discarded.
780
781  <Arguments>
782    filepath:
783      <filepath>.pub file, a public key file.
784
785  <Exceptions>
786    securesystemslib.exceptions.FormatError, if 'filepath' is improperly
787    formatted or is an unexpected key type.
788
789  <Side Effects>
790    The contents of 'filepath' is read and saved.
791
792  <Returns>
793    An ECDSA key object conformant to
794    'securesystemslib.formats.ECDSAKEY_SCHEMA'.
795  """
796
797  # Does 'filepath' have the correct format?
798  # Ensure the arguments have the appropriate number of objects and object
799  # types, and that all dict keys are properly named.
800  # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
801  securesystemslib.formats.PATH_SCHEMA.check_match(filepath)
802
803  # ECDSA key objects are saved in json and metadata format.  Return the
804  # loaded key object in securesystemslib.formats.ECDSAKEY_SCHEMA' format that
805  # also includes the keyid.
806  ecdsa_key_metadata = securesystemslib.util.load_json_file(filepath)
807  ecdsa_key, junk = \
808    securesystemslib.keys.format_metadata_to_key(ecdsa_key_metadata)
809
810  # Raise an exception if an unexpected key type is imported.  Redundant
811  # validation of 'keytype'.  'securesystemslib.keys.format_metadata_to_key()'
812  # should have fully validated 'ecdsa_key_metadata'.
813  if ecdsa_key['keytype'] != 'ecdsa-sha2-nistp256': # pragma: no cover
814    message = 'Invalid key type loaded: ' + repr(ecdsa_key['keytype'])
815    raise securesystemslib.exceptions.FormatError(message)
816
817  return ecdsa_key
818
819
820
821
822
823def import_ecdsa_privatekey_from_file(filepath, password=None):
824  """
825  <Purpose>
826    Import the encrypted ECDSA key file in 'filepath', decrypt it, and return
827    the key object in 'securesystemslib.formats.ECDSAKEY_SCHEMA' format.
828
829    The 'cryptography' library is currently supported and performs the actual
830    cryptographic routine.
831
832  <Arguments>
833    filepath:
834      <filepath> file, an ECDSA encrypted key file.
835
836    password:
837      The password, or passphrase, to import the private key (i.e., the
838      encrypted key file 'filepath' must be decrypted before the ECDSA key
839      object can be returned.
840
841  <Exceptions>
842    securesystemslib.exceptions.FormatError, if the arguments are improperly
843    formatted or the imported key object contains an invalid key type (i.e.,
844    not 'ecdsa-sha2-nistp256').
845
846    securesystemslib.exceptions.CryptoError, if 'filepath' cannot be decrypted.
847
848  <Side Effects>
849    'password' is used to decrypt the 'filepath' key file.
850
851  <Returns>
852    An ECDSA key object of the form: 'securesystemslib.formats.ECDSAKEY_SCHEMA'.
853  """
854
855  # Does 'filepath' have the correct format?
856  # Ensure the arguments have the appropriate number of objects and object
857  # types, and that all dict keys are properly named.
858  # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
859  securesystemslib.formats.PATH_SCHEMA.check_match(filepath)
860
861  # If the caller does not provide a password argument, prompt for one.
862  # Password confirmation disabled here, which should ideally happen only
863  # when creating encrypted key files (i.e., improve usability).
864  if password is None: # pragma: no cover
865
866    # It is safe to specify the full path of 'filepath' in the prompt and not
867    # worry about leaking sensitive information about the key's location.
868    # However, care should be taken when including the full path in exceptions
869    # and log files.
870    password = get_password('Enter a password for the encrypted ECDSA'
871        ' key (' + Fore.RED + filepath + Fore.RESET + '): ',
872        confirm=False)
873
874  # Does 'password' have the correct format?
875  securesystemslib.formats.PASSWORD_SCHEMA.check_match(password)
876
877  # Store the encrypted contents of 'filepath' prior to calling the decryption
878  # routine.
879  encrypted_key = None
880
881  with open(filepath, 'rb') as file_object:
882    encrypted_key = file_object.read()
883
884  # Decrypt the loaded key file, calling the 'cryptography' library to generate
885  # the derived encryption key from 'password'.  Raise
886  # 'securesystemslib.exceptions.CryptoError' if the decryption fails.
887  key_object = securesystemslib.keys.decrypt_key(encrypted_key.decode('utf-8'),
888      password)
889
890  # Raise an exception if an unexpected key type is imported.
891  if key_object['keytype'] != 'ecdsa-sha2-nistp256':
892    message = 'Invalid key type loaded: ' + repr(key_object['keytype'])
893    raise securesystemslib.exceptions.FormatError(message)
894
895  # Add "keyid_hash_algorithms" so that equal ecdsa keys with different keyids
896  # can be associated using supported keyid_hash_algorithms.
897  key_object['keyid_hash_algorithms'] = \
898      securesystemslib.settings.HASH_ALGORITHMS
899
900  return key_object
901
902
903
904if __name__ == '__main__':
905  # The interactive sessions of the documentation strings can
906  # be tested by running interface.py as a standalone module:
907  # $ python interface.py.
908  import doctest
909  doctest.testmod()
910