Skip to content

Commit 4d0682f

Browse files
committed
Protect PEM key storage
1 parent 7e9ddbf commit 4d0682f

1 file changed

Lines changed: 41 additions & 45 deletions

File tree

src/hybrid_pki/classical/keygen.py

Lines changed: 41 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,37 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import tempfile
15
from pathlib import Path
26

37
from cryptography.hazmat.primitives import serialization
48
from cryptography.hazmat.primitives.asymmetric import ec, ed25519, rsa
59

610

711
def generate_rsa_private_key(key_size: int = 3072):
8-
"""
9-
Generate an RSA private key.
10-
11-
Recommended sizes:
12-
- 3072 bits for modern security
13-
- 4096 bits for stronger security
14-
"""
15-
return rsa.generate_private_key(
16-
public_exponent=65537,
17-
key_size=key_size,
18-
)
12+
"""Generate an RSA private key."""
13+
if key_size < 2048:
14+
raise ValueError("RSA key size must be at least 2048 bits")
15+
return rsa.generate_private_key(public_exponent=65537, key_size=key_size)
1916

2017

2118
def generate_ecdsa_private_key():
22-
"""
23-
Generate an ECDSA private key using curve P-256.
24-
"""
19+
"""Generate an ECDSA private key using curve P-256."""
2520
return ec.generate_private_key(ec.SECP256R1())
2621

2722

2823
def generate_ed25519_private_key():
29-
"""
30-
Generate an Ed25519 private key.
31-
"""
24+
"""Generate an Ed25519 private key."""
3225
return ed25519.Ed25519PrivateKey.generate()
3326

3427

3528
def serialize_private_key(private_key, password: bytes | None = None) -> bytes:
36-
"""
37-
Serialize a private key to PEM format.
38-
39-
If password is provided, the private key is encrypted.
40-
"""
29+
"""Serialize a private key to encrypted or unencrypted PKCS#8 PEM."""
4130
encryption_algorithm = (
4231
serialization.BestAvailableEncryption(password)
4332
if password
4433
else serialization.NoEncryption()
4534
)
46-
4735
return private_key.private_bytes(
4836
encoding=serialization.Encoding.PEM,
4937
format=serialization.PrivateFormat.PKCS8,
@@ -52,40 +40,48 @@ def serialize_private_key(private_key, password: bytes | None = None) -> bytes:
5240

5341

5442
def serialize_public_key(public_key) -> bytes:
55-
"""
56-
Serialize a public key to PEM format.
57-
"""
43+
"""Serialize a public key to SubjectPublicKeyInfo PEM."""
5844
return public_key.public_bytes(
5945
encoding=serialization.Encoding.PEM,
6046
format=serialization.PublicFormat.SubjectPublicKeyInfo,
6147
)
6248

6349

6450
def save_pem_file(path: str | Path, data: bytes) -> None:
65-
"""
66-
Save PEM data to a file.
67-
"""
51+
"""Atomically save PEM data with owner-only permissions."""
6852
file_path = Path(path)
6953
file_path.parent.mkdir(parents=True, exist_ok=True)
70-
file_path.write_bytes(data)
54+
55+
descriptor, temporary_name = tempfile.mkstemp(
56+
prefix=f".{file_path.name}.",
57+
dir=file_path.parent,
58+
)
59+
temporary_path = Path(temporary_name)
60+
try:
61+
os.fchmod(descriptor, 0o600)
62+
with os.fdopen(descriptor, "wb") as stream:
63+
stream.write(data)
64+
stream.flush()
65+
os.fsync(stream.fileno())
66+
os.replace(temporary_path, file_path)
67+
try:
68+
file_path.chmod(0o600)
69+
except OSError:
70+
pass
71+
except Exception:
72+
try:
73+
os.close(descriptor)
74+
except OSError:
75+
pass
76+
temporary_path.unlink(missing_ok=True)
77+
raise
7178

7279

7380
def load_private_key(path: str | Path, password: bytes | None = None):
74-
"""
75-
Load a private key from a PEM file.
76-
"""
77-
data = Path(path).read_bytes()
78-
79-
return serialization.load_pem_private_key(
80-
data,
81-
password=password,
82-
)
81+
"""Load a private key from a PEM file."""
82+
return serialization.load_pem_private_key(Path(path).read_bytes(), password=password)
8383

8484

8585
def load_public_key(path: str | Path):
86-
"""
87-
Load a public key from a PEM file.
88-
"""
89-
data = Path(path).read_bytes()
90-
91-
return serialization.load_pem_public_key(data)
86+
"""Load a public key from a PEM file."""
87+
return serialization.load_pem_public_key(Path(path).read_bytes())

0 commit comments

Comments
 (0)