Skip to content

Commit 062657d

Browse files
Lukas PuehringerMVrachev
authored andcommitted
Adopt sslib keygen interface encryption changes
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]>
1 parent d055c42 commit 062657d

File tree

8 files changed

+104
-91
lines changed

8 files changed

+104
-91
lines changed

docs/TUTORIAL.md

Lines changed: 34 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,15 @@ text without prepended symbols is the output of a command.
9696
# following function creates an RSA key pair, where the private key is saved to
9797
# "root_key" and the public key to "root_key.pub" (both saved to the current
9898
# working directory).
99-
>>> generate_and_write_rsa_keypair("root_key", bits=2048, password="password")
99+
>>> generate_and_write_rsa_keypair(password="password", filepath="root_key", bits=2048)
100100

101101
# If the key length is unspecified, it defaults to 3072 bits. A length of less
102-
# than 2048 bits raises an exception. A password may be supplied as an
103-
# argument, otherwise a user prompt is presented. If an empty password
104-
# is entered, the private key is saved unencrypted.
105-
>>> generate_and_write_rsa_keypair("root_key2")
106-
Enter a password for the RSA key (/path/to/root_key2):
102+
# than 2048 bits raises an exception. A similar function is available to supply
103+
# a password on the prompt. If an empty password is entered, the private key
104+
# is saved unencrypted.
105+
>>> generate_and_write_rsa_keypair_with_prompt(filepath="root_key2")
106+
enter password to encrypt private key file '/path/to/root_key2'
107+
(leave empty if key should not be encrypted):
107108
Confirm:
108109
```
109110
The following four key files should now exist:
@@ -117,8 +118,9 @@ If a filepath is not given, the KEYID of the generated key is used as the
117118
filename. The key files are written to the current working directory.
118119
```python
119120
# Continuing from the previous section . . .
120-
>>> generate_and_write_rsa_keypair()
121-
Enter a password for the encrypted RSA key (/path/to/b5b8de8aeda674bce948fbe82cab07e309d6775fc0ec299199d16746dc2bd54c):
121+
>>> generate_and_write_rsa_keypair_with_prompt()
122+
enter password to encrypt private key file '/path/to/KEYID'
123+
(leave empty if key should not be encrypted):
122124
Confirm:
123125
```
124126

@@ -132,36 +134,27 @@ Confirm:
132134
# Import an existing private key. Importing a private key requires a password,
133135
# whereas importing a public key does not.
134136
>>> private_root_key = import_rsa_privatekey_from_file("root_key")
135-
Enter a password for the encrypted RSA key (/path/to/root_key):
136-
```
137-
138-
`import_rsa_privatekey_from_file()` raises a
139-
`securesystemslib.exceptions.CryptoError` exception if the key / password is
140-
invalid:
141-
142-
```
143-
securesystemslib.exceptions.CryptoError: RSA (public, private) tuple cannot be
144-
generated from the encrypted PEM string: Bad decrypt. Incorrect password?
137+
enter password to decrypt private key file '/path/to/root_key'
138+
(leave empty if key not encrypted):
145139
```
146140

147141
### Create and Import Ed25519 Keys ###
148142
```Python
149143
# Continuing from the previous section . . .
150144

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

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

162154
# and its corresponding private key.
163155
>>> private_ed25519_key = import_ed25519_privatekey_from_file('ed25519_key')
164-
Enter a password for the encrypted Ed25519 key (/path/to/ed25519_key):
156+
enter password to decrypt private key file '/path/to/ed25519_key'
157+
(leave empty if key should not be encrypted):
165158
```
166159

167160
Note: Methods are also available to generate and write keys from memory.
@@ -259,26 +252,20 @@ secure manner.
259252
>>> import datetime
260253

261254
# Generate keys for the remaining top-level roles. The root keys have been set above.
262-
# The password argument may be omitted if a password prompt is needed.
263-
>>> generate_and_write_rsa_keypair('targets_key', password='password')
264-
>>> generate_and_write_rsa_keypair('snapshot_key', password='password')
265-
>>> generate_and_write_rsa_keypair('timestamp_key', password='password')
255+
>>> generate_and_write_rsa_keypair(password='password', filepath='targets_key')
256+
>>> generate_and_write_rsa_keypair(password='password', filepath='snapshot_key')
257+
>>> generate_and_write_rsa_keypair(password='password', filepath='timestamp_key')
266258

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

269261
>>> repository.targets.add_verification_key(import_rsa_publickey_from_file('targets_key.pub'))
270262
>>> repository.snapshot.add_verification_key(import_rsa_publickey_from_file('snapshot_key.pub'))
271263
>>> repository.timestamp.add_verification_key(import_rsa_publickey_from_file('timestamp_key.pub'))
272264

273-
# Import the signing keys of the remaining top-level roles. Prompt for passwords.
274-
>>> private_targets_key = import_rsa_privatekey_from_file('targets_key')
275-
Enter a password for the encrypted RSA key (/path/to/targets_key):
276-
277-
>>> private_snapshot_key = import_rsa_privatekey_from_file('snapshot_key')
278-
Enter a password for the encrypted RSA key (/path/to/snapshot_key):
279-
280-
>>> private_timestamp_key = import_rsa_privatekey_from_file('timestamp_key')
281-
Enter a password for the encrypted RSA key (/path/to/timestamp_key):
265+
# Import the signing keys of the remaining top-level roles.
266+
>>> private_targets_key = import_rsa_privatekey_from_file('targets_key', password='password')
267+
>>> private_snapshot_key = import_rsa_privatekey_from_file('snapshot_key', password='password')
268+
>>> private_timestamp_key = import_rsa_privatekey_from_file('timestamp_key', password='password')
282269

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

395383
>>> repository.targets.load_signing_key(private_targets_key)
396384

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

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

407397
# Mark roles for metadata update (see #964, #958)
@@ -451,7 +441,7 @@ threshold, it needs to be added to `root.json`, e.g. via
451441
>>> from securesystemslib.formats import encode_canonical
452442
>>> from securesystemslib.keys import create_signature
453443
>>> private_ed25519_key = import_ed25519_privatekey_from_file('ed25519_key')
454-
Enter a password for the encrypted Ed25519 key (/path/to/ed25519_key):
444+
enter password to decrypt private key file '/path/to/ed25519_key'
455445
>>> signature = create_signature(
456446
... private_ed25519_key, encode_canonical(signable_content).encode())
457447
```
@@ -489,7 +479,7 @@ targets and generate signed metadata.
489479
# Continuing from the previous section . . .
490480

491481
# Generate a key for a new delegated role named "unclaimed".
492-
>>> generate_and_write_rsa_keypair('unclaimed_key', bits=2048, password='password')
482+
>>> generate_and_write_rsa_keypair(password='password', filepath='unclaimed_key', bits=2048)
493483
>>> public_unclaimed_key = import_rsa_publickey_from_file('unclaimed_key.pub')
494484

495485
# Make a delegation (delegate trust of 'myproject/*.txt' files) from "targets"
@@ -502,8 +492,7 @@ targets and generate signed metadata.
502492

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

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

tests/repository_data/generate.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,11 @@
5959
# Generate public and private key files for the top-level roles, and two
6060
# delegated roles (these number of keys should be sufficient for most of the
6161
# unit tests). Unit tests may generate additional keys, if needed.
62-
generate_and_write_rsa_keypair(root_key_file, password='password')
63-
generate_and_write_ed25519_keypair(targets_key_file, password='password')
64-
generate_and_write_ed25519_keypair(snapshot_key_file, password='password')
65-
generate_and_write_ed25519_keypair(timestamp_key_file, password='password')
66-
generate_and_write_ed25519_keypair(delegation_key_file, password='password')
62+
generate_and_write_rsa_keypair(password='password', filepath=root_key_file)
63+
generate_and_write_ed25519_keypair(password='password', filepath=targets_key_file)
64+
generate_and_write_ed25519_keypair(password='password', filepath=snapshot_key_file)
65+
generate_and_write_ed25519_keypair(password='password', filepath=timestamp_key_file)
66+
generate_and_write_ed25519_keypair(password='password', filepath=delegation_key_file)
6767

6868
# Import the public keys. These keys are needed so that metadata roles are
6969
# assigned verification keys, which clients use to verify the signatures created

tests/test_repository_lib.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ def test_import_ed25519_privatekey_from_file(self):
143143
temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory)
144144
ed25519_keypath = os.path.join(temporary_directory, 'ed25519_key')
145145
securesystemslib.interface.generate_and_write_ed25519_keypair(
146-
ed25519_keypath, password='pw')
146+
password='pw', filepath=ed25519_keypath)
147147

148148
imported_ed25519_key = \
149149
repo_lib.import_ed25519_privatekey_from_file(ed25519_keypath, 'pw')

tests/test_tutorial.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,11 @@ def test_tutorial(self):
7575

7676
# ----- Tutorial Section: Keys
7777

78-
generate_and_write_rsa_keypair('root_key', bits=2048, password='password')
78+
generate_and_write_rsa_keypair(password='password', filepath='root_key', bits=2048)
7979

8080
# Skipping user entry of password
81-
## generate_and_write_rsa_keypair('root_key2')
82-
generate_and_write_rsa_keypair('root_key2', password='password')
81+
## generate_and_write_rsa_keypair_with_prompt('root_key2')
82+
generate_and_write_rsa_keypair(password='password', filepath='root_key2')
8383

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

111111
# Skipping user entry of password
112-
## generate_and_write_ed25519_keypair('ed25519_key')
113-
generate_and_write_ed25519_keypair('ed25519_key', password='password')
112+
## generate_and_write_ed25519_keypair_with_prompt('ed25519_key')
113+
generate_and_write_ed25519_keypair(password='password', filepath='ed25519_key')
114114

115115
public_ed25519_key = import_ed25519_publickey_from_file('ed25519_key.pub')
116116

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

160-
generate_and_write_rsa_keypair('targets_key', password='password')
161-
generate_and_write_rsa_keypair('snapshot_key', password='password')
162-
generate_and_write_rsa_keypair('timestamp_key', password='password')
160+
generate_and_write_rsa_keypair(password='password', filepath='targets_key')
161+
generate_and_write_rsa_keypair(password='password', filepath='snapshot_key')
162+
generate_and_write_rsa_keypair(password='password', filepath='timestamp_key')
163163

164164
repository.targets.add_verification_key(import_rsa_publickey_from_file(
165165
'targets_key.pub'))
@@ -309,7 +309,7 @@ def test_tutorial(self):
309309

310310
# ----- Tutorial Section: Delegations
311311
generate_and_write_rsa_keypair(
312-
'unclaimed_key', bits=2048, password='password')
312+
password='password', filepath='unclaimed_key', bits=2048)
313313
public_unclaimed_key = import_rsa_publickey_from_file('unclaimed_key.pub')
314314
repository.targets.delegate(
315315
'unclaimed', [public_unclaimed_key], ['myproject/*.txt'])

tuf/README-developer-tools.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,16 @@ is the private key.
5353

5454
```
5555
>>> from tuf.developer_tool import *
56-
>>> generate_and_write_rsa_keypair("path/to/key")
57-
Enter a password for the RSA key:
56+
>>> generate_and_write_rsa_keypair_with_prompt(filepath="path/to/key")
57+
enter password to encrypt private key file 'path/to/key'
58+
(leave empty if key should not be encrypted):
5859
Confirm:
5960
>>>
6061
```
6162

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

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

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

259260
```
260-
>>> generate_and_write_rsa_keypair("path/to/key", bits=2048, password="pw")
261+
>>> generate_and_write_rsa_keypair(password="pw", filepath="path/to/key", bits=2048)
261262
```
262263
The bits parameter defaults to 3072, and values below 2048 will raise an error.
263264
The password parameter is only intended to be used in scripts.

tuf/developer_tool.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,14 @@
7676

7777
from securesystemslib.interface import (
7878
generate_and_write_rsa_keypair,
79+
generate_and_write_rsa_keypair_with_prompt,
80+
generate_and_write_unencrypted_rsa_keypair,
81+
generate_and_write_ecdsa_keypair,
82+
generate_and_write_ecdsa_keypair_with_prompt,
83+
generate_and_write_unencrypted_ecdsa_keypair,
7984
generate_and_write_ed25519_keypair,
85+
generate_and_write_ed25519_keypair_with_prompt,
86+
generate_and_write_unencrypted_ed25519_keypair,
8087
import_rsa_publickey_from_file,
8188
import_ed25519_publickey_from_file,
8289
import_ed25519_privatekey_from_file)

tuf/repository_tool.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,14 @@
7474

7575
from securesystemslib.interface import (
7676
generate_and_write_rsa_keypair,
77+
generate_and_write_rsa_keypair_with_prompt,
78+
generate_and_write_unencrypted_rsa_keypair,
7779
generate_and_write_ecdsa_keypair,
80+
generate_and_write_ecdsa_keypair_with_prompt,
81+
generate_and_write_unencrypted_ecdsa_keypair,
7882
generate_and_write_ed25519_keypair,
83+
generate_and_write_ed25519_keypair_with_prompt,
84+
generate_and_write_unencrypted_ed25519_keypair,
7985
import_rsa_publickey_from_file,
8086
import_ecdsa_publickey_from_file,
8187
import_ed25519_publickey_from_file,

tuf/scripts/repo.py

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@
189189
# securesystemslib.
190190
SUPPORTED_KEY_TYPES = ('ed25519', 'ecdsa-sha2-nistp256', 'rsa')
191191

192+
# pylint: disable=protected-access
193+
# ... to allow use of sslib _generate_and_write_*_keypair convenience methods
192194

193195
def process_command_line_arguments(parsed_arguments):
194196
"""
@@ -379,23 +381,30 @@ def gen_key(parsed_arguments):
379381

380382
keypath = None
381383

384+
keygen_kwargs = {
385+
"password": parsed_arguments.pw,
386+
"filepath": parsed_arguments.filename,
387+
"prompt": (not parsed_arguments.pw) # prompt if no default or passed pw
388+
}
389+
382390
if parsed_arguments.key not in SUPPORTED_CLI_KEYTYPES:
383391
tuf.exceptions.Error(
384392
'Invalid key type: ' + repr(parsed_arguments.key) + '. Supported'
385393
' key types: ' + repr(SUPPORTED_CLI_KEYTYPES))
386394

387395
elif parsed_arguments.key == ECDSA_KEYTYPE:
388-
keypath = securesystemslib.interface.generate_and_write_ecdsa_keypair(
389-
parsed_arguments.filename, password=parsed_arguments.pw)
396+
keypath = securesystemslib.interface._generate_and_write_ecdsa_keypair(
397+
**keygen_kwargs)
390398

391399
elif parsed_arguments.key == ED25519_KEYTYPE:
392-
keypath = securesystemslib.interface.generate_and_write_ed25519_keypair(
393-
parsed_arguments.filename, password=parsed_arguments.pw)
400+
keypath = securesystemslib.interface._generate_and_write_ed25519_keypair(
401+
**keygen_kwargs)
394402

395403
# RSA key..
396404
else:
397-
keypath = securesystemslib.interface.generate_and_write_rsa_keypair(
398-
parsed_arguments.filename, password=parsed_arguments.pw)
405+
keypath = securesystemslib.interface._generate_and_write_rsa_keypair(
406+
**keygen_kwargs)
407+
399408

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

892-
# Examples of how the --pw command-line option is interpreted:
893-
# repo.py --init': parsed_arguments.pw = 'pw'
894-
# repo.py --init --pw my_pw: parsed_arguments.pw = 'my_pw'
895-
# repo.py --init --pw: The user is prompted for a password, here.
896-
if not parsed_arguments.pw:
897-
parsed_arguments.pw = securesystemslib.interface.get_password(
898-
prompt='Enter a password for the top-level role keys: ', confirm=True)
899-
900-
repo_tool.generate_and_write_ed25519_keypair(
901-
os.path.join(parsed_arguments.path, KEYSTORE_DIR,
902-
ROOT_KEY_NAME), password=parsed_arguments.root_pw)
903-
repo_tool.generate_and_write_ed25519_keypair(
904-
os.path.join(parsed_arguments.path, KEYSTORE_DIR,
905-
TARGETS_KEY_NAME), password=parsed_arguments.targets_pw)
906-
repo_tool.generate_and_write_ed25519_keypair(
907-
os.path.join(parsed_arguments.path, KEYSTORE_DIR,
908-
SNAPSHOT_KEY_NAME), password=parsed_arguments.snapshot_pw)
909-
repo_tool.generate_and_write_ed25519_keypair(
910-
os.path.join(parsed_arguments.path, KEYSTORE_DIR,
911-
TIMESTAMP_KEY_NAME), password=parsed_arguments.timestamp_pw)
901+
# Examples of how the --*_pw command-line options are interpreted:
902+
# repo.py --init': parsed_arguments.*_pw = 'pw'
903+
# repo.py --init --*_pw my_pw: parsed_arguments.*_pw = 'my_pw'
904+
# repo.py --init --*_pw: The user is prompted for a password.
905+
906+
securesystemslib.interface._generate_and_write_ed25519_keypair(
907+
password=parsed_arguments.root_pw,
908+
filepath=os.path.join(parsed_arguments.path, KEYSTORE_DIR, ROOT_KEY_NAME),
909+
prompt=(not parsed_arguments.root_pw))
910+
securesystemslib.interface._generate_and_write_ed25519_keypair(
911+
password=parsed_arguments.targets_pw,
912+
filepath=os.path.join(parsed_arguments.path, KEYSTORE_DIR, TARGETS_KEY_NAME),
913+
prompt=(not parsed_arguments.targets_pw))
914+
securesystemslib.interface._generate_and_write_ed25519_keypair(
915+
password=parsed_arguments.snapshot_pw,
916+
filepath=os.path.join(parsed_arguments.path, KEYSTORE_DIR, SNAPSHOT_KEY_NAME),
917+
prompt=(not parsed_arguments.snapshot_pw))
918+
securesystemslib.interface._generate_and_write_ed25519_keypair(
919+
password=parsed_arguments.timestamp_pw,
920+
filepath=os.path.join(parsed_arguments.path, KEYSTORE_DIR, TIMESTAMP_KEY_NAME),
921+
prompt=(not parsed_arguments.timestamp_pw))
912922

913923
# Import the private keys. They are needed to generate the signatures
914924
# included in metadata.

0 commit comments

Comments
 (0)