Skip to content

Commit 57fbcdd

Browse files
authored
Merge pull request #231 from sechkova/issue-222
Create private key files with read and write permissions for the user only
2 parents 320f89d + 9432937 commit 57fbcdd

File tree

5 files changed

+77
-13
lines changed

5 files changed

+77
-13
lines changed

securesystemslib/interface.py

100755100644
Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,6 @@
5454
DEFAULT_RSA_KEY_BITS = 3072
5555

5656

57-
58-
59-
6057
def get_password(prompt='Password: ', confirm=False):
6158
"""Prompts user to enter a password.
6259
@@ -204,6 +201,7 @@ def _generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS,
204201
Side Effects:
205202
Prompts user for a password if 'prompt' is True.
206203
Writes key files to disk.
204+
Overwrites files if they already exist.
207205
208206
Returns:
209207
The private key filepath.
@@ -239,7 +237,7 @@ def _generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS,
239237
# Write PEM-encoded private key to <filepath>
240238
file_object = tempfile.TemporaryFile()
241239
file_object.write(private.encode('utf-8'))
242-
util.persist_temp_file(file_object, filepath)
240+
util.persist_temp_file(file_object, filepath, restrict=True)
243241

244242
return filepath
245243

@@ -270,6 +268,7 @@ def generate_and_write_rsa_keypair(password, filepath=None,
270268
271269
Side Effects:
272270
Writes key files to disk.
271+
Overwrites files if they already exist.
273272
274273
Returns:
275274
The private key filepath.
@@ -306,6 +305,7 @@ def generate_and_write_rsa_keypair_with_prompt(filepath=None,
306305
Side Effects:
307306
Prompts user for a password.
308307
Writes key files to disk.
308+
Overwrites files if they already exist.
309309
310310
Returns:
311311
The private key filepath.
@@ -338,6 +338,7 @@ def generate_and_write_unencrypted_rsa_keypair(filepath=None,
338338
339339
Side Effects:
340340
Writes unencrypted key files to disk.
341+
Overwrites files if they already exist.
341342
342343
Returns:
343344
The private key filepath.
@@ -469,6 +470,7 @@ def _generate_and_write_ed25519_keypair(filepath=None, password=None,
469470
Side Effects:
470471
Prompts user for a password if 'prompt' is True.
471472
Writes key files to disk.
473+
Overwrites files if they already exist.
472474
473475
Returns:
474476
The private key filepath.
@@ -508,7 +510,7 @@ def _generate_and_write_ed25519_keypair(filepath=None, password=None,
508510
# Write private key to <filepath>
509511
file_object = tempfile.TemporaryFile()
510512
file_object.write(ed25519_key.encode('utf-8'))
511-
util.persist_temp_file(file_object, filepath)
513+
util.persist_temp_file(file_object, filepath, restrict=True)
512514

513515
return filepath
514516

@@ -536,6 +538,7 @@ def generate_and_write_ed25519_keypair(password, filepath=None):
536538
537539
Side Effects:
538540
Writes key files to disk.
541+
Overwrites files if they already exist.
539542
540543
Returns:
541544
The private key filepath.
@@ -568,6 +571,7 @@ def generate_and_write_ed25519_keypair_with_prompt(filepath=None):
568571
Side Effects:
569572
Prompts user for a password.
570573
Writes key files to disk.
574+
Overwrites files if they already exist.
571575
572576
Returns:
573577
The private key filepath.
@@ -595,6 +599,7 @@ def generate_and_write_unencrypted_ed25519_keypair(filepath=None):
595599
596600
Side Effects:
597601
Writes unencrypted key files to disk.
602+
Overwrites files if they already exist.
598603
599604
Returns:
600605
The private key filepath.
@@ -713,6 +718,7 @@ def _generate_and_write_ecdsa_keypair(filepath=None, password=None,
713718
Side Effects:
714719
Prompts user for a password if 'prompt' is True.
715720
Writes key files to disk.
721+
Overwrites files if they already exist.
716722
717723
Returns:
718724
The private key filepath.
@@ -752,7 +758,7 @@ def _generate_and_write_ecdsa_keypair(filepath=None, password=None,
752758
# Write private key to <filepath>
753759
file_object = tempfile.TemporaryFile()
754760
file_object.write(ecdsa_key.encode('utf-8'))
755-
util.persist_temp_file(file_object, filepath)
761+
util.persist_temp_file(file_object, filepath, restrict=True)
756762

757763
return filepath
758764

@@ -780,6 +786,7 @@ def generate_and_write_ecdsa_keypair(password, filepath=None):
780786
781787
Side Effects:
782788
Writes key files to disk.
789+
Overwrites files if they already exist.
783790
784791
Returns:
785792
The private key filepath.
@@ -812,6 +819,7 @@ def generate_and_write_ecdsa_keypair_with_prompt(filepath=None):
812819
Side Effects:
813820
Prompts user for a password.
814821
Writes key files to disk.
822+
Overwrites files if they already exist.
815823
816824
Returns:
817825
The private key filepath.
@@ -839,6 +847,7 @@ def generate_and_write_unencrypted_ecdsa_keypair(filepath=None):
839847
840848
Side Effects:
841849
Writes unencrypted key files to disk.
850+
Overwrites files if they already exist.
842851
843852
Returns:
844853
The private key filepath.

securesystemslib/storage.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@
2020
import logging
2121
import os
2222
import shutil
23+
import stat
2324
from contextlib import contextmanager
2425
from securesystemslib import exceptions
25-
from typing import BinaryIO, IO, Iterator, List
26+
from typing import BinaryIO, IO, Iterator, List, Optional
2627

2728
logger = logging.getLogger(__name__)
2829

@@ -65,7 +66,8 @@ def get(self, filepath: str) -> Iterator[BinaryIO]:
6566

6667

6768
@abc.abstractmethod
68-
def put(self, fileobj: IO, filepath: str) -> None:
69+
def put(self, fileobj: IO, filepath: str, restrict: Optional[bool] = False
70+
) -> None:
6971
"""
7072
<Purpose>
7173
Store a file-like object in the storage backend.
@@ -79,6 +81,13 @@ def put(self, fileobj: IO, filepath: str) -> None:
7981
filepath:
8082
The full path to the location where 'fileobj' will be stored.
8183
84+
restrict:
85+
Whether the file should be created with restricted permissions.
86+
What counts as restricted is backend-specific. For a filesystem on a
87+
UNIX-like operating system, that may mean read/write permissions only
88+
for the user (octal mode 0o600). For a cloud storage system, that
89+
likely means Cloud provider specific ACL restrictions.
90+
8291
<Exceptions>
8392
securesystemslib.exceptions.StorageError, if the file can not be stored.
8493
@@ -208,14 +217,35 @@ def get(self, filepath:str) -> Iterator[BinaryIO]:
208217
file_object.close()
209218

210219

211-
def put(self, fileobj: IO, filepath: str) -> None:
220+
def put(self, fileobj: IO, filepath: str, restrict: Optional[bool] = False
221+
) -> None:
212222
# If we are passed an open file, seek to the beginning such that we are
213223
# copying the entire contents
214224
if not fileobj.closed:
215225
fileobj.seek(0)
216226

227+
# If a file with the same name already exists, the new permissions
228+
# may not be applied.
229+
try:
230+
os.remove(filepath)
231+
except OSError:
232+
pass
233+
217234
try:
218-
with open(filepath, 'wb') as destination_file:
235+
if restrict:
236+
# On UNIX-based systems restricted files are created with read and
237+
# write permissions for the user only (octal value 0o600).
238+
fd = os.open(filepath, os.O_WRONLY|os.O_CREAT,
239+
stat.S_IRUSR|stat.S_IWUSR)
240+
else:
241+
# Non-restricted files use the default 'mode' argument of os.open()
242+
# granting read, write, and execute for all users (octal mode 0o777).
243+
# NOTE: mode may be modified by the user's file mode creation mask
244+
# (umask) or on Windows limited to the smaller set of OS supported
245+
# permisssions.
246+
fd = os.open(filepath, os.O_WRONLY|os.O_CREAT)
247+
248+
with os.fdopen(fd, "wb") as destination_file:
219249
shutil.copyfileobj(fileobj, destination_file)
220250
# Force the destination file to be written to disk from Python's internal
221251
# and the operating system's buffers. os.fsync() should follow flush().

securesystemslib/util.py

100755100644
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,8 @@ def persist_temp_file(
179179
temp_file: IO,
180180
persist_path: str,
181181
storage_backend: Optional[StorageBackendInterface] = None,
182-
should_close: bool = True
182+
should_close: bool = True,
183+
restrict: bool = False
183184
) -> None:
184185
"""
185186
<Purpose>
@@ -203,6 +204,11 @@ def persist_temp_file(
203204
A boolean indicating whether the file should be closed after it has been
204205
persisted. Default is True, the file is closed.
205206
207+
restrict:
208+
A boolean indicating whether the file should have restricted privileges.
209+
What evactly counts as restricted privileges is an implementation detail
210+
of the backing StorageBackendInterface implementation.
211+
206212
<Exceptions>
207213
securesystemslib.exceptions.StorageError: If file cannot be written.
208214
@@ -213,7 +219,7 @@ def persist_temp_file(
213219
if storage_backend is None:
214220
storage_backend = FilesystemBackend()
215221

216-
storage_backend.put(temp_file, persist_path)
222+
storage_backend.put(temp_file, persist_path, restrict=restrict)
217223

218224
if should_close:
219225
temp_file.close()

tests/test_interface.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,7 @@ def test_generate_keypair_wrappers(self):
633633
634634
"""
635635
key_pw = "pw"
636+
expected_priv_mode = stat.S_IFREG|stat.S_IRUSR|stat.S_IWUSR
636637
for idx, (gen, gen_prompt, gen_plain, import_priv, schema) in enumerate([
637638
(
638639
generate_and_write_rsa_keypair,
@@ -661,6 +662,10 @@ def test_generate_keypair_wrappers(self):
661662
priv = import_priv(fn_encrypted, key_pw)
662663
self.assertTrue(schema.matches(priv), assert_msg)
663664

665+
# Test that encrypted private key is generated with read and write
666+
# permissions for user only
667+
self.assertEqual(os.stat(fn_encrypted).st_mode, expected_priv_mode)
668+
664669
# Test generate_and_write_*_keypair errors if password is None or empty
665670
with self.assertRaises(FormatError, msg=assert_msg):
666671
fn_encrypted = gen(None)
@@ -688,6 +693,9 @@ def test_generate_keypair_wrappers(self):
688693
priv = import_priv(fn_unencrypted)
689694
self.assertTrue(schema.matches(priv), assert_msg)
690695

696+
# Test that unencrypted private key is generated with read and write
697+
# permissions for user only
698+
self.assertEqual(os.stat(fn_unencrypted).st_mode, expected_priv_mode)
691699

692700

693701
def test_import_publickeys_from_file(self):

tests/test_util.py

100755100644
Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import tempfile
2525
import unittest
2626
import timeit
27+
import stat
2728

2829
import securesystemslib.settings
2930
import securesystemslib.hash
@@ -240,8 +241,18 @@ def test_B9_persist_temp_file(self):
240241
dest_path = os.path.join(dest_temp_dir, self.random_string())
241242
tmpfile = tempfile.TemporaryFile()
242243
tmpfile.write(self.random_string().encode('utf-8'))
243-
securesystemslib.util.persist_temp_file(tmpfile, dest_path)
244+
245+
# Write a file with restricted permissions
246+
securesystemslib.util.persist_temp_file(tmpfile, dest_path, restrict=True)
244247
self.assertTrue(dest_path)
248+
249+
# Need to set also the stat.S_IFREG bit to match the st_mode output
250+
# stat.S_IFREG - Regular file
251+
expected_mode = stat.S_IFREG | stat.S_IRUSR | stat.S_IWUSR
252+
if os.name == 'nt':
253+
# Windows only supports setting the read-only attribute.
254+
expected_mode = stat.S_IFREG | stat.S_IWUSR | stat.S_IRUSR | stat.S_IWGRP | stat.S_IRGRP | stat.S_IWOTH | stat.S_IROTH
255+
self.assertEqual(os.stat(dest_path).st_mode, expected_mode)
245256
self.assertTrue(tmpfile.closed)
246257

247258
# Test persisting a file without automatically closing the tmpfile

0 commit comments

Comments
 (0)