Harden Sigil: sigil/keys.py

This commit is contained in:
bigpickle 2026-10-05 21:24:40 +08:00 committed by leora
parent 752e006e0d
commit dc55b9a7aa

View File

@ -11,7 +11,7 @@ from __future__ import annotations
import base64 import base64
import hashlib import hashlib
import os import os
from typing import Tuple from typing import Optional, Tuple
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import ( from cryptography.hazmat.primitives.asymmetric.ed25519 import (
@ -75,22 +75,56 @@ def verify(public: PublicKey, signature: bytes, message: bytes) -> bool:
return False return False
def save_private(private: PrivateKey, path: str) -> None: def save_private(private: PrivateKey, path: str, passphrase: Optional[str] = None) -> None:
"""Write a private key to disk with owner-only permissions. """Write a private key to disk with owner-only permissions.
Created 0600 from the outset rather than chmod-ed after, so there is no Created 0600 from the outset rather than chmod-ed after, so there is no
window where the key is briefly world-readable. window where the key is briefly world-readable.
If a passphrase is given the key is encrypted at rest (PBKDF2-HMAC-SHA256,
480k iterations, AES-256). Prefer that for anything long-lived: 0600 stops
other *users*, not root, a backup, or anyone who copies the disk.
""" """
if passphrase:
if len(passphrase) < 12:
raise ValueError(
"passphrase for a private key must be at least 12 characters"
)
encryption = serialization.BestAvailableEncryption(passphrase.encode("utf-8"))
else:
encryption = serialization.NoEncryption()
pem = private.private_bytes( pem = private.private_bytes(
encoding=serialization.Encoding.PEM, encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8, format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(), encryption_algorithm=encryption,
) )
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "wb") as handle: with os.fdopen(fd, "wb") as handle:
handle.write(pem) handle.write(pem)
def load_private(path: str) -> PrivateKey: def is_encrypted(path: str) -> bool:
"""True if the file on disk holds an encrypted key."""
with open(path, "rb") as handle: with open(path, "rb") as handle:
return serialization.load_pem_private_key(handle.read(), password=None) head = handle.read()
return b"ENCRYPTED" in head
def load_private(path: str, passphrase: Optional[str] = None) -> PrivateKey:
"""Load a private key.
A wrong or missing passphrase raises rather than yielding a wrong key, so
a caller cannot accidentally proceed with a key it did not really open.
"""
with open(path, "rb") as handle:
data = handle.read()
if b"ENCRYPTED" in data and not passphrase:
raise ValueError(
f"{path} is encrypted: a passphrase is required"
)
key = serialization.load_pem_private_key(
data, password=passphrase.encode("utf-8") if passphrase else None
)
if not isinstance(key, Ed25519PrivateKey):
raise ValueError(f"{path} is not an Ed25519 private key")
return key