Skip to content

Commit

Permalink
Adopt sslib keygen interface encryption changes (WIP) [ci skip]
Browse files Browse the repository at this point in the history
secure-systems-lab/securesystemslib#288 changes the key generation
interface functions in such a way that it is clear if a call opens
a blocking prompt, or writes the key unencrypted. To do this two
functions are added per key type:
 - `generate_and_write_*_keypair_with_prompt`
 - `generate_and_write_unencrypted_*_keypair`

The default generate_and_write_*_keypair function now only allows
encrypted keys and only using a passed password.
This respects the principle of secure defaults and least surprise.

sslib#288 also adds a protected _generate_and_write_*_keypair
function per keytype which may be used

NOTE: The securesystemslib private key import functions do not
auto-prompt for decryption passwords either, TUF, however, only
exposes custom wrappers (see repository_lib) that do auto-prompt.
The sslib#288 does change prompt texts for encryption and also
decryption keys, which is reflected in this commit.

TODO:
- Adopt changes in TUTORIAL.md in test_tutorial.py
- Proof read ('repo.py' in particular)

Signed-off-by: Lukas Puehringer <[email protected]>
  • Loading branch information
lukpueh committed Nov 5, 2020
1 parent ff106ea commit bc470b8
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 62 deletions.
77 changes: 33 additions & 44 deletions docs/TUTORIAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,15 @@ text without prepended symbols is the output of a command.
# following function creates an RSA key pair, where the private key is saved to
# "root_key" and the public key to "root_key.pub" (both saved to the current
# working directory).
>>> generate_and_write_rsa_keypair("root_key", bits=2048, password="password")
>>> generate_and_write_rsa_keypair(password="password", filepath="root_key", bits=2048)

# If the key length is unspecified, it defaults to 3072 bits. A length of less
# than 2048 bits raises an exception. A password may also be supplied on the
# prompt. If an empty password is entered, the private key is saved unencrypted.
>>> generate_and_write_rsa_keypair("root_key2", prompt=True)
Enter a password for the RSA key (/path/to/root_key2):
# than 2048 bits raises an exception. A similar function is available to supply
# a password on the prompt. If an empty password is entered, the private key
# is saved unencrypted.
>>> generate_and_write_rsa_keypair_with_prompt(filepath="root_key2")
enter password to encrypt private key file '/path/to/root_key2'
(leave empty if key should not be encrypted):
Confirm:
```
The following four key files should now exist:
Expand All @@ -116,8 +118,9 @@ If a filepath is not given, the KEYID of the generated key is used as the
filename. The key files are written to the current working directory.
```python
# Continuing from the previous section . . .
>>> generate_and_write_rsa_keypair(prompt=True)
Enter a password for the encrypted RSA key (/path/to/b5b8de8aeda674bce948fbe82cab07e309d6775fc0ec299199d16746dc2bd54c):
>>> generate_and_write_rsa_keypair_with_prompt()
enter password to encrypt private key file '/path/to/KEYID'
(leave empty if key should not be encrypted):
Confirm:
```

Expand All @@ -131,36 +134,26 @@ Confirm:
# Import an existing private key. Importing a private key requires a password,
# whereas importing a public key does not.
>>> private_root_key = import_rsa_privatekey_from_file("root_key")
Enter a password for the encrypted RSA key (/path/to/root_key):
```

`import_rsa_privatekey_from_file()` raises a
`securesystemslib.exceptions.CryptoError` exception if the key / password is
invalid:

```
securesystemslib.exceptions.CryptoError: RSA (public, private) tuple cannot be
generated from the encrypted PEM string: Bad decrypt. Incorrect password?
enter password to decrypt private key file '/path/to/root_key'
(leave empty if key not encrypted):
```

### Create and Import Ed25519 Keys ###
```Python
# Continuing from the previous section . . .

# Generate and write an Ed25519 key pair. A 'password' argument may be
# supplied, otherwise a prompt is presented. The private key is saved
# encrypted if a non-empty password is given, and unencrypted if the password
# is empty.
>>> generate_and_write_ed25519_keypair('ed25519_key', prompt=True)
Enter a password for the Ed25519 key (/path/to/ed25519_key):
# The same generation and import functions as for rsa keys exist for ed25519
>>> generate_and_write_ed25519_keypair_with_prompt(filepath='ed25519_key')
enter password to encrypt private key file '/path/to/ed25519_key'
(leave empty if key should not be encrypted):
Confirm:

# Import the ed25519 public key just created . . .
>>> public_ed25519_key = import_ed25519_publickey_from_file('ed25519_key.pub')

# and its corresponding private key.
>>> private_ed25519_key = import_ed25519_privatekey_from_file('ed25519_key')
Enter a password for the encrypted Ed25519 key (/path/to/ed25519_key):
enter password to decrypt private key file '/path/to/ed25519_key'
```

Note: Methods are also available to generate and write keys from memory.
Expand Down Expand Up @@ -258,26 +251,20 @@ secure manner.
>>> import datetime

# Generate keys for the remaining top-level roles. The root keys have been set above.
# The password argument may be omitted if a password prompt is needed.
>>> generate_and_write_rsa_keypair('targets_key', password='password')
>>> generate_and_write_rsa_keypair('snapshot_key', password='password')
>>> generate_and_write_rsa_keypair('timestamp_key', password='password')
>>> generate_and_write_rsa_keypair(password='password', filepath='targets_key')
>>> generate_and_write_rsa_keypair(password='password', filepath='snapshot_key')
>>> generate_and_write_rsa_keypair(password='password', filepath='timestamp_key')

# Add the verification keys of the remaining top-level roles.

>>> repository.targets.add_verification_key(import_rsa_publickey_from_file('targets_key.pub'))
>>> repository.snapshot.add_verification_key(import_rsa_publickey_from_file('snapshot_key.pub'))
>>> repository.timestamp.add_verification_key(import_rsa_publickey_from_file('timestamp_key.pub'))

# Import the signing keys of the remaining top-level roles. Prompt for passwords.
>>> private_targets_key = import_rsa_privatekey_from_file('targets_key')
Enter a password for the encrypted RSA key (/path/to/targets_key):

>>> private_snapshot_key = import_rsa_privatekey_from_file('snapshot_key')
Enter a password for the encrypted RSA key (/path/to/snapshot_key):

>>> private_timestamp_key = import_rsa_privatekey_from_file('timestamp_key')
Enter a password for the encrypted RSA key (/path/to/timestamp_key):
# Import the signing keys of the remaining top-level roles.
>>> private_targets_key = import_rsa_privatekey_from_file('targets_key', password='password')
>>> private_snapshot_key = import_rsa_privatekey_from_file('snapshot_key', password='password')
>>> private_timestamp_key = import_rsa_privatekey_from_file('timestamp_key', password='password')

# Load the signing keys of the remaining roles so that valid signatures are
# generated when repository.writeall() is called.
Expand Down Expand Up @@ -389,18 +376,21 @@ metadata. `snapshot.json` keys must be loaded and its metadata signed because
# The private key of the updated targets metadata must be re-loaded before it
# can be signed and written (Note the load_repository() call above).
>>> private_targets_key = import_rsa_privatekey_from_file('targets_key')
Enter a password for the encrypted RSA key (/path/to/targets_key):
enter password to decrypt private key file '/path/to/targets_key'
(leave empty if key not encrypted):

>>> repository.targets.load_signing_key(private_targets_key)

# Due to the load_repository() and new versions of metadata, we must also load
# the private keys of Snapshot and Timestamp to generate a valid set of metadata.
>>> private_snapshot_key = import_rsa_privatekey_from_file('snapshot_key')
Enter a password for the encrypted RSA key (/path/to/snapshot_key):
enter password to decrypt private key file '/path/to/snapshot_key'
(leave empty if key not encrypted):
>>> repository.snapshot.load_signing_key(private_snapshot_key)

>>> private_timestamp_key = import_rsa_privatekey_from_file('timestamp_key')
Enter a password for the encrypted RSA key (/path/to/timestamp_key):
enter password to decrypt private key file '/path/to/timestamp_key'
(leave empty if key not encrypted):
>>> repository.timestamp.load_signing_key(private_timestamp_key)

# Mark roles for metadata update (see #964, #958)
Expand Down Expand Up @@ -450,7 +440,7 @@ threshold, it needs to be added to `root.json`, e.g. via
>>> from securesystemslib.formats import encode_canonical
>>> from securesystemslib.keys import create_signature
>>> private_ed25519_key = import_ed25519_privatekey_from_file('ed25519_key')
Enter a password for the encrypted Ed25519 key (/path/to/ed25519_key):
enter password to decrypt private key file '/path/to/ed25519_key'
>>> signature = create_signature(
... private_ed25519_key, encode_canonical(signable_content).encode())
```
Expand Down Expand Up @@ -488,7 +478,7 @@ targets and generate signed metadata.
# Continuing from the previous section . . .

# Generate a key for a new delegated role named "unclaimed".
>>> generate_and_write_rsa_keypair('unclaimed_key', bits=2048, password='password')
>>> generate_and_write_rsa_keypair(password='password', filepath='unclaimed_key', bits=2048)
>>> public_unclaimed_key = import_rsa_publickey_from_file('unclaimed_key.pub')

# Make a delegation (delegate trust of 'myproject/*.txt' files) from "targets"
Expand All @@ -501,8 +491,7 @@ targets and generate signed metadata.

# Load the private key of "unclaimed" so that unclaimed's metadata can be
# signed, and valid metadata created.
>>> private_unclaimed_key = import_rsa_privatekey_from_file('unclaimed_key')
Enter a password for the encrypted RSA key (/path/to/unclaimed_key):
>>> private_unclaimed_key = import_rsa_privatekey_from_file('unclaimed_key', password='password')

>>> repository.targets("unclaimed").load_signing_key(private_unclaimed_key)

Expand Down
10 changes: 5 additions & 5 deletions tests/repository_data/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@
# Generate public and private key files for the top-level roles, and two
# delegated roles (these number of keys should be sufficient for most of the
# unit tests). Unit tests may generate additional keys, if needed.
generate_and_write_rsa_keypair(root_key_file, password='password')
generate_and_write_ed25519_keypair(targets_key_file, password='password')
generate_and_write_ed25519_keypair(snapshot_key_file, password='password')
generate_and_write_ed25519_keypair(timestamp_key_file, password='password')
generate_and_write_ed25519_keypair(delegation_key_file, password='password')
generate_and_write_rsa_keypair(password='password', filepath=root_key_file)
generate_and_write_ed25519_keypair(password='password', filepath=targets_key_file)
generate_and_write_ed25519_keypair(password='password', filepath=snapshot_key_file)
generate_and_write_ed25519_keypair(password='password', filepath=timestamp_key_file)
generate_and_write_ed25519_keypair(password='password', filepath=delegation_key_file)

# Import the public keys. These keys are needed so that metadata roles are
# assigned verification keys, which clients use to verify the signatures created
Expand Down
2 changes: 1 addition & 1 deletion tests/test_repository_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def test_import_ed25519_privatekey_from_file(self):
temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory)
ed25519_keypath = os.path.join(temporary_directory, 'ed25519_key')
securesystemslib.interface.generate_and_write_ed25519_keypair(
ed25519_keypath, password='pw')
password='pw', filepath=ed25519_keypath)

imported_ed25519_key = \
repo_lib.import_ed25519_privatekey_from_file(ed25519_keypath, 'pw')
Expand Down
11 changes: 6 additions & 5 deletions tuf/README-developer-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,16 @@ is the private key.

```
>>> from tuf.developer_tool import *
>>> generate_and_write_rsa_keypair("path/to/key", prompt=True)
Enter a password for the RSA key:
>>> generate_and_write_rsa_keypair_with_prompt(filepath="path/to/key")
enter password to encrypt private key file 'path/to/key'
(leave empty if key should not be encrypted):
Confirm:
>>>
```

We can also use the bits parameter to set a different key length (the default
is 3072). We can also provide the password parameter in order to suppress the
password prompt.
is 3072). We can also `generate_and_write_rsa_keypair` with a `password`
parameter if a prompt is not desired.

In this example we will be using rsa keys, but ed25519 keys are also supported.

Expand Down Expand Up @@ -257,7 +258,7 @@ When generating keys, it is possible to specify the length of the key in bits
and its password as parameters:

```
>>> generate_and_write_rsa_keypair("path/to/key", bits=2048, password="pw")
>>> generate_and_write_rsa_keypair(password="pw", filepath="path/to/key", bits=2048)
```
The bits parameter defaults to 3072, and values below 2048 will raise an error.
The password parameter is only intended to be used in scripts.
Expand Down
7 changes: 7 additions & 0 deletions tuf/developer_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,14 @@

from securesystemslib.interface import (
generate_and_write_rsa_keypair,
generate_and_write_rsa_keypair_with_prompt,
generate_and_write_unencrypted_rsa_keypair,
generate_and_write_ecdsa_keypair,
generate_and_write_ecdsa_keypair_with_prompt,
generate_and_write_unencrypted_ecdsa_keypair,
generate_and_write_ed25519_keypair,
generate_and_write_ed25519_keypair_with_prompt,
generate_and_write_unencrypted_ed25519_keypair,
import_rsa_publickey_from_file,
import_ed25519_publickey_from_file,
import_ed25519_privatekey_from_file)
Expand Down
6 changes: 6 additions & 0 deletions tuf/repository_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,14 @@

from securesystemslib.interface import (
generate_and_write_rsa_keypair,
generate_and_write_rsa_keypair_with_prompt,
generate_and_write_unencrypted_rsa_keypair,
generate_and_write_ecdsa_keypair,
generate_and_write_ecdsa_keypair_with_prompt,
generate_and_write_unencrypted_ecdsa_keypair,
generate_and_write_ed25519_keypair,
generate_and_write_ed25519_keypair_with_prompt,
generate_and_write_unencrypted_ed25519_keypair,
import_rsa_publickey_from_file,
import_ecdsa_publickey_from_file,
import_ed25519_publickey_from_file,
Expand Down
14 changes: 7 additions & 7 deletions tuf/scripts/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,16 +385,16 @@ def gen_key(parsed_arguments):
' key types: ' + repr(SUPPORTED_CLI_KEYTYPES))

elif parsed_arguments.key == ECDSA_KEYTYPE:
keypath = securesystemslib.interface.generate_and_write_ecdsa_keypair(
keypath = securesystemslib.interface._generate_and_write_ecdsa_keypair(
parsed_arguments.filename, password=parsed_arguments.pw)

elif parsed_arguments.key == ED25519_KEYTYPE:
keypath = securesystemslib.interface.generate_and_write_ed25519_keypair(
keypath = securesystemslib.interface._generate_and_write_ed25519_keypair(
parsed_arguments.filename, password=parsed_arguments.pw)

# RSA key..
else:
keypath = securesystemslib.interface.generate_and_write_rsa_keypair(
keypath = securesystemslib.interface._generate_and_write_rsa_keypair(
parsed_arguments.filename, password=parsed_arguments.pw)

# If a filename is not given, the generated keypair is saved to the current
Expand Down Expand Up @@ -897,16 +897,16 @@ def set_top_level_keys(repository, parsed_arguments):
parsed_arguments.pw = securesystemslib.interface.get_password(
prompt='Enter a password for the top-level role keys: ', confirm=True)

repo_tool.generate_and_write_ed25519_keypair(
repo_tool._generate_and_write_ed25519_keypair(
os.path.join(parsed_arguments.path, KEYSTORE_DIR,
ROOT_KEY_NAME), password=parsed_arguments.root_pw)
repo_tool.generate_and_write_ed25519_keypair(
repo_tool._generate_and_write_ed25519_keypair(
os.path.join(parsed_arguments.path, KEYSTORE_DIR,
TARGETS_KEY_NAME), password=parsed_arguments.targets_pw)
repo_tool.generate_and_write_ed25519_keypair(
repo_tool._generate_and_write_ed25519_keypair(
os.path.join(parsed_arguments.path, KEYSTORE_DIR,
SNAPSHOT_KEY_NAME), password=parsed_arguments.snapshot_pw)
repo_tool.generate_and_write_ed25519_keypair(
repo_tool._generate_and_write_ed25519_keypair(
os.path.join(parsed_arguments.path, KEYSTORE_DIR,
TIMESTAMP_KEY_NAME), password=parsed_arguments.timestamp_pw)

Expand Down

0 comments on commit bc470b8

Please sign in to comment.