Skip to content

Commit

Permalink
Adopt sslib keygen interface encryption changes
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 furthermore adds a protected
`_generate_and_write_*_keypair`, which is not exposed publicly
because it does not encrypt by default, but is more flexible and
thus convenient e.g. to consume all arguments from a key generation
command line tool such as 'repo.py'.

This commit adds the new public functions to the tuf namespace and
adopts their usage accordingly.

NOTE regarding repo.py:
This commit does not fix any problematic password behavior of
'repo.py' like default passwords, etc. (see #881). It only adopts
the sslib#288 changes to maintain the current behvior, plus
removing one glaringly obsolete password prompt.

NOTE regarding key import:
The securesystemslib private key import functions were also changed
to no longer auto-prompt for decryption passwords , TUF, however,
only exposes custom wrappers (see repository_lib) that do
auto-prompt. sslib#288 changes to the prompt texts are nevertheless
propagated to tuf and reflected in this commit.

Signed-off-by: Lukas Puehringer <[email protected]>
  • Loading branch information
lukpueh committed Nov 11, 2020
1 parent 9908f8e commit 2ce08c0
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 91 deletions.
79 changes: 34 additions & 45 deletions docs/TUTORIAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +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 be supplied as an
# argument, otherwise a user prompt is presented. If an empty password
# is entered, the private key is saved unencrypted.
>>> generate_and_write_rsa_keypair("root_key2")
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 @@ -117,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()
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 @@ -132,36 +134,27 @@ 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')
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'
(leave empty if key should not be encrypted):
```

Note: Methods are also available to generate and write keys from memory.
Expand Down Expand Up @@ -259,26 +252,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 @@ -390,18 +377,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 @@ -451,7 +441,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 @@ -489,7 +479,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 @@ -502,8 +492,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
18 changes: 9 additions & 9 deletions tests/test_tutorial.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,11 @@ def test_tutorial(self):

# ----- Tutorial Section: Keys

generate_and_write_rsa_keypair('root_key', bits=2048, password='password')
generate_and_write_rsa_keypair(password='password', filepath='root_key', bits=2048)

# Skipping user entry of password
## generate_and_write_rsa_keypair('root_key2')
generate_and_write_rsa_keypair('root_key2', password='password')
## generate_and_write_rsa_keypair_with_prompt('root_key2')
generate_and_write_rsa_keypair(password='password', filepath='root_key2')

# Tutorial tells users to expect these files to exist:
# ['root_key', 'root_key.pub', 'root_key2', 'root_key2.pub']
Expand Down Expand Up @@ -109,8 +109,8 @@ def test_tutorial(self):
# ----- Tutorial Section: Create and Import Ed25519 Keys

# Skipping user entry of password
## generate_and_write_ed25519_keypair('ed25519_key')
generate_and_write_ed25519_keypair('ed25519_key', password='password')
## generate_and_write_ed25519_keypair_with_prompt('ed25519_key')
generate_and_write_ed25519_keypair(password='password', filepath='ed25519_key')

public_ed25519_key = import_ed25519_publickey_from_file('ed25519_key.pub')

Expand Down Expand Up @@ -157,9 +157,9 @@ def test_tutorial(self):
repr('targets') + " role contains 0 / 1 signatures."
], [args[0] for args, _ in mock_logger.info.call_args_list])

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')

repository.targets.add_verification_key(import_rsa_publickey_from_file(
'targets_key.pub'))
Expand Down Expand Up @@ -309,7 +309,7 @@ def test_tutorial(self):

# ----- Tutorial Section: Delegations
generate_and_write_rsa_keypair(
'unclaimed_key', bits=2048, password='password')
password='password', filepath='unclaimed_key', bits=2048)
public_unclaimed_key = import_rsa_publickey_from_file('unclaimed_key.pub')
repository.targets.delegate(
'unclaimed', [public_unclaimed_key], ['myproject/*.txt'])
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")
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
60 changes: 34 additions & 26 deletions tuf/scripts/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,23 +379,30 @@ def gen_key(parsed_arguments):

keypath = None

keygen_kwargs = {
"password": parsed_arguments.pw,
"filepath": parsed_arguments.filename,
"prompt": (not parsed_arguments.pw) # prompt if no default or passed pw
}

if parsed_arguments.key not in SUPPORTED_CLI_KEYTYPES:
tuf.exceptions.Error(
'Invalid key type: ' + repr(parsed_arguments.key) + '. Supported'
' key types: ' + repr(SUPPORTED_CLI_KEYTYPES))

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

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

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


# If a filename is not given, the generated keypair is saved to the current
# working directory. By default, the keypair is written to <KEYID>.pub
Expand Down Expand Up @@ -889,26 +896,27 @@ def set_top_level_keys(repository, parsed_arguments):
Generate, write, and set the top-level keys. 'repository' is modified.
"""

# Examples of how the --pw command-line option is interpreted:
# repo.py --init': parsed_arguments.pw = 'pw'
# repo.py --init --pw my_pw: parsed_arguments.pw = 'my_pw'
# repo.py --init --pw: The user is prompted for a password, here.
if not parsed_arguments.pw:
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(
os.path.join(parsed_arguments.path, KEYSTORE_DIR,
ROOT_KEY_NAME), password=parsed_arguments.root_pw)
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(
os.path.join(parsed_arguments.path, KEYSTORE_DIR,
SNAPSHOT_KEY_NAME), password=parsed_arguments.snapshot_pw)
repo_tool.generate_and_write_ed25519_keypair(
os.path.join(parsed_arguments.path, KEYSTORE_DIR,
TIMESTAMP_KEY_NAME), password=parsed_arguments.timestamp_pw)
# Examples of how the --*_pw command-line options are interpreted:
# repo.py --init': parsed_arguments.*_pw = 'pw'
# repo.py --init --*_pw my_pw: parsed_arguments.*_pw = 'my_pw'
# repo.py --init --*_pw: The user is prompted for a password.

securesystemslib.interface._generate_and_write_ed25519_keypair(
password=parsed_arguments.root_pw,
filepath=os.path.join(parsed_arguments.path, KEYSTORE_DIR, ROOT_KEY_NAME),
prompt=(not parsed_arguments.root_pw))
securesystemslib.interface._generate_and_write_ed25519_keypair(
password=parsed_arguments.targets_pw,
filepath=os.path.join(parsed_arguments.path, KEYSTORE_DIR, TARGETS_KEY_NAME),
prompt=(not parsed_arguments.targets_pw))
securesystemslib.interface._generate_and_write_ed25519_keypair(
password=parsed_arguments.snapshot_pw,
filepath=os.path.join(parsed_arguments.path, KEYSTORE_DIR, SNAPSHOT_KEY_NAME),
prompt=(not parsed_arguments.snapshot_pw))
securesystemslib.interface._generate_and_write_ed25519_keypair(
password=parsed_arguments.timestamp_pw,
filepath=os.path.join(parsed_arguments.path, KEYSTORE_DIR, TIMESTAMP_KEY_NAME),
prompt=(not parsed_arguments.timestamp_pw))

# Import the private keys. They are needed to generate the signatures
# included in metadata.
Expand Down

0 comments on commit 2ce08c0

Please sign in to comment.