Harden Sigil: sigil/keys.py
This commit is contained in:
parent
752e006e0d
commit
dc55b9a7aa
@ -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
|
||||||
Loading…
x
Reference in New Issue
Block a user