Skip to content

Commit

Permalink
Add thin interface._generate_*_keypair wrappers
Browse files Browse the repository at this point in the history
Based on discussions in #288, add the following keypair
generation interface functions for each key type:
- generate_and_write_*_keypair
- generate_and_write_*_keypair_with_prompt
- generate_and_write_unencrypted_*_keypair

Other than the underlying `_generate_*_keypair` functions, these
thin wrappers respect the principle of 'secure defaults', i.e.
encrypt private keys per default, and still don't surprise callers
with prompts, both unless made explicit through the function name.
  • Loading branch information
lukpueh committed Nov 5, 2020
1 parent dd2c5fc commit edcf74e
Show file tree
Hide file tree
Showing 4 changed files with 411 additions and 2 deletions.
284 changes: 284 additions & 0 deletions securesystemslib/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,109 @@ def _generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS,



def generate_and_write_rsa_keypair(password, filepath=None,
bits=DEFAULT_RSA_KEY_BITS):
"""Generates RSA key pair and writes PEM-encoded keys to disk.
The private key is encrypted using the best available encryption algorithm
chosen by 'pyca/cryptography', which may change over time. The private key is
written in PKCS#1 and the public key in X.509 SubjectPublicKeyInfo format.
NOTE: A signing scheme can be assigned on key import (see import functions).
Arguments:
password: An encryption password.
filepath (optional): The path to write the private key to. If not passed,
the key is written to CWD using the keyid as filename. The public key
is written to the same path as the private key using the suffix '.pub'.
bits (optional): The number of bits of the generated RSA key.
Raises:
UnsupportedLibraryError: pyca/cryptography is not available.
FormatError: Arguments are malformed.
ValueError: An empty string is passed as 'password'.
StorageError: Key files cannot be written.
Side Effects:
Writes key files to disk.
Returns:
The private key filepath.
"""
securesystemslib.formats.PASSWORD_SCHEMA.check_match(password)
return _generate_and_write_rsa_keypair(
filepath=filepath, bits=bits, password=password, prompt=False)



def generate_and_write_rsa_keypair_with_prompt(filepath=None,
bits=DEFAULT_RSA_KEY_BITS):
"""Generates RSA key pair and writes PEM-encoded keys to disk.
The private key is encrypted with a password entered on the prompt, using the
best available encryption algorithm chosen by 'pyca/cryptography', which may
change over time. The private key is written in PKCS#1 and the public key in
X.509 SubjectPublicKeyInfo format.
NOTE: A signing scheme can be assigned on key import (see import functions).
Arguments:
filepath (optional): The path to write the private key to. If not passed,
the key is written to CWD using the keyid as filename. The public key
is written to the same path as the private key using the suffix '.pub'.
bits (optional): The number of bits of the generated RSA key.
Raises:
UnsupportedLibraryError: pyca/cryptography is not available.
FormatError: Arguments are malformed.
StorageError: Key files cannot be written.
Side Effects:
Prompts user for a password.
Writes key files to disk.
Returns:
The private key filepath.
"""
return _generate_and_write_rsa_keypair(
filepath=filepath, bits=bits, password=None, prompt=True)



def generate_and_write_unencrypted_rsa_keypair(filepath=None,
bits=DEFAULT_RSA_KEY_BITS):
"""Generates RSA key pair and writes PEM-encoded keys to disk.
The private key is written in PKCS#1 and the public key in X.509
SubjectPublicKeyInfo format.
NOTE: A signing scheme can be assigned on key import (see import functions).
Arguments:
filepath (optional): The path to write the private key to. If not passed,
the key is written to CWD using the keyid as filename. The public key
is written to the same path as the private key using the suffix '.pub'.
bits (optional): The number of bits of the generated RSA key.
Raises:
UnsupportedLibraryError: pyca/cryptography is not available.
FormatError: Arguments are malformed.
StorageError: Key files cannot be written.
Side Effects:
Writes unencrypted key files to disk.
Returns:
The private key filepath.
"""
return _generate_and_write_rsa_keypair(
filepath=filepath, bits=bits, password=None, prompt=False)



def import_rsa_privatekey_from_file(filepath, password=None,
scheme='rsassa-pss-sha256', prompt=False,
storage_backend=None):
Expand Down Expand Up @@ -415,6 +518,96 @@ def _generate_and_write_ed25519_keypair(filepath=None, password=None,



def generate_and_write_ed25519_keypair(password, filepath=None):
"""Generates ed25519 key pair and writes custom JSON-formatted keys to disk.
The private key is encrypted using AES-256 in CTR mode, with the passed
password strengthened in PBKDF2-HMAC-SHA256.
NOTE: The custom key format includes 'ed25519' as signing scheme.
Arguments:
password: An encryption password.
filepath (optional): The path to write the private key to. If not passed,
the key is written to CWD using the keyid as filename. The public key
is written to the same path as the private key using the suffix '.pub'.
Raises:
UnsupportedLibraryError: pyca/pynacl or pyca/cryptography is not available.
FormatError: Arguments are malformed.
ValueError: An empty string is passed as 'password'.
StorageError: Key files cannot be written.
Side Effects:
Writes key files to disk.
Returns:
The private key filepath.
"""
securesystemslib.formats.PASSWORD_SCHEMA.check_match(password)
return _generate_and_write_ed25519_keypair(
filepath=filepath, password=password, prompt=False)



def generate_and_write_ed25519_keypair_with_prompt(filepath=None):
"""Generates ed25519 key pair and writes custom JSON-formatted keys to disk.
The private key is encrypted using AES-256 in CTR mode, with the password
entered on the prompt strengthened in PBKDF2-HMAC-SHA256.
NOTE: The custom key format includes 'ed25519' as signing scheme.
Arguments:
filepath (optional): The path to write the private key to. If not passed,
the key is written to CWD using the keyid as filename. The public key
is written to the same path as the private key using the suffix '.pub'.
Raises:
UnsupportedLibraryError: pyca/pynacl or pyca/cryptography is not available.
FormatError: Arguments are malformed.
StorageError: Key files cannot be written.
Side Effects:
Prompts user for a password.
Writes key files to disk.
Returns:
The private key filepath.
"""
return _generate_and_write_ed25519_keypair(
filepath=filepath, password=None, prompt=True)



def generate_and_write_unencrypted_ed25519_keypair(filepath=None):
"""Generates ed25519 key pair and writes custom JSON-formatted keys to disk.
NOTE: The custom key format includes 'ed25519' as signing scheme.
Arguments:
filepath (optional): The path to write the private key to. If not passed,
the key is written to CWD using the keyid as filename. The public key
is written to the same path as the private key using the suffix '.pub'.
Raises:
UnsupportedLibraryError: pyca/pynacl or pyca/cryptography is not available.
FormatError: Arguments are malformed.
StorageError: Key files cannot be written.
Side Effects:
Writes unencrypted key files to disk.
Returns:
The private key filepath.
"""
return _generate_and_write_ed25519_keypair(
filepath=filepath, password=None, prompt=False)


def import_ed25519_publickey_from_file(filepath):
"""Imports custom JSON-formatted ed25519 public key from disk.
Expand Down Expand Up @@ -570,6 +763,97 @@ def _generate_and_write_ecdsa_keypair(filepath=None, password=None,



def generate_and_write_ecdsa_keypair(password, filepath=None):
"""Generates ecdsa key pair and writes custom JSON-formatted keys to disk.
The private key is encrypted using AES-256 in CTR mode, with the passed
password strengthened in PBKDF2-HMAC-SHA256.
NOTE: The custom key format includes 'ecdsa-sha2-nistp256' as signing scheme.
Arguments:
password: An encryption password.
filepath (optional): The path to write the private key to. If not passed,
the key is written to CWD using the keyid as filename. The public key
is written to the same path as the private key using the suffix '.pub'.
Raises:
UnsupportedLibraryError: pyca/cryptography is not available.
FormatError: Arguments are malformed.
ValueError: An empty string is passed as 'password'.
StorageError: Key files cannot be written.
Side Effects:
Writes key files to disk.
Returns:
The private key filepath.
"""
securesystemslib.formats.PASSWORD_SCHEMA.check_match(password)
return _generate_and_write_ecdsa_keypair(
filepath=filepath, password=password, prompt=False)



def generate_and_write_ecdsa_keypair_with_prompt(filepath=None):
"""Generates ecdsa key pair and writes custom JSON-formatted keys to disk.
The private key is encrypted using AES-256 in CTR mode, with the password
entered on the prompt strengthened in PBKDF2-HMAC-SHA256.
NOTE: The custom key format includes 'ecdsa-sha2-nistp256' as signing scheme.
Arguments:
filepath (optional): The path to write the private key to. If not passed,
the key is written to CWD using the keyid as filename. The public key
is written to the same path as the private key using the suffix '.pub'.
Raises:
UnsupportedLibraryError: pyca/cryptography is not available.
FormatError: Arguments are malformed.
StorageError: Key files cannot be written.
Side Effects:
Prompts user for a password.
Writes key files to disk.
Returns:
The private key filepath.
"""
return _generate_and_write_ecdsa_keypair(
filepath=filepath, password=None, prompt=True)



def generate_and_write_unencrypted_ecdsa_keypair(filepath=None):
"""Generates ecdsa key pair and writes custom JSON-formatted keys to disk.
NOTE: The custom key format includes 'ecdsa-sha2-nistp256' as signing scheme.
Arguments:
filepath (optional): The path to write the private key to. If not passed,
the key is written to CWD using the keyid as filename. The public key
is written to the same path as the private key using the suffix '.pub'.
Raises:
UnsupportedLibraryError: pyca/cryptography is not available.
FormatError: Arguments are malformed.
StorageError: Key files cannot be written.
Side Effects:
Writes unencrypted key files to disk.
Returns:
The private key filepath.
"""
return _generate_and_write_ecdsa_keypair(
filepath=filepath, password=None, prompt=False)



def import_ecdsa_publickey_from_file(filepath):
"""Imports custom JSON-formatted ecdsa public key from disk.
Expand Down
55 changes: 53 additions & 2 deletions tests/check_public_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
import tempfile
import unittest

if sys.version_info >= (3, 3):
import unittest.mock as mock
else:
import mock

import securesystemslib.exceptions
import securesystemslib.gpg.constants
Expand All @@ -66,6 +70,25 @@ def test_interface(self):
securesystemslib.exceptions.UnsupportedLibraryError):
securesystemslib.interface._generate_and_write_rsa_keypair(password='pw')

with self.assertRaises(
securesystemslib.exceptions.UnsupportedLibraryError):
securesystemslib.interface.generate_and_write_rsa_keypair('pw')

with self.assertRaises(
securesystemslib.exceptions.UnsupportedLibraryError):
securesystemslib.interface.generate_and_write_rsa_keypair('pw')

with self.assertRaises(
securesystemslib.exceptions.UnsupportedLibraryError):
# Mock entry on prompt which is presented before lower-level functions
# raise UnsupportedLibraryError
with mock.patch("securesystemslib.interface.get_password", return_value=""):
securesystemslib.interface.generate_and_write_rsa_keypair_with_prompt()

with self.assertRaises(
securesystemslib.exceptions.UnsupportedLibraryError):
securesystemslib.interface.generate_and_write_unencrypted_rsa_keypair()

with self.assertRaises(
securesystemslib.exceptions.UnsupportedLibraryError):
path = os.path.join(self.temp_dir, 'rsa_key')
Expand All @@ -78,6 +101,21 @@ def test_interface(self):
securesystemslib.interface._generate_and_write_ed25519_keypair(
password='pw')

with self.assertRaises(
securesystemslib.exceptions.UnsupportedLibraryError):
securesystemslib.interface.generate_and_write_ed25519_keypair('pw')

with self.assertRaises(
securesystemslib.exceptions.UnsupportedLibraryError):
# Mock entry on prompt which is presented before lower-level functions
# raise UnsupportedLibraryError
with mock.patch("securesystemslib.interface.get_password", return_value=""):
securesystemslib.interface.generate_and_write_ed25519_keypair_with_prompt()

with self.assertRaises(
securesystemslib.exceptions.UnsupportedLibraryError):
securesystemslib.interface.generate_and_write_unencrypted_ed25519_keypair()

with self.assertRaises(
securesystemslib.exceptions.UnsupportedLibraryError):
path = os.path.join(self.temp_dir, 'ed25519_priv.json')
Expand All @@ -91,13 +129,26 @@ def test_interface(self):
securesystemslib.interface._generate_and_write_ecdsa_keypair(
password='pw')

with self.assertRaises(
securesystemslib.exceptions.UnsupportedLibraryError):
securesystemslib.interface.generate_and_write_ecdsa_keypair('pw')

with self.assertRaises(
securesystemslib.exceptions.UnsupportedLibraryError):
# Mock entry on prompt which is presented before lower-level functions
# raise UnsupportedLibraryError
with mock.patch("securesystemslib.interface.get_password", return_value=""):
securesystemslib.interface.generate_and_write_ecdsa_keypair_with_prompt()

with self.assertRaises(
securesystemslib.exceptions.UnsupportedLibraryError):
securesystemslib.interface.generate_and_write_unencrypted_ecdsa_keypair()

with self.assertRaises(
securesystemslib.exceptions.UnsupportedLibraryError):
path = os.path.join(self.temp_dir, 'ecddsa.priv')
with open(path, 'a') as f:
f.write('{}')
# TODO: this is the only none generate_and_write_ function
# that prompts for a password when password == None
securesystemslib.interface.import_ecdsa_privatekey_from_file(
path, password='pw')

Expand Down
Loading

0 comments on commit edcf74e

Please sign in to comment.