refactor(local-identity): post-Stage4 cleanups and micro-fixes

- audit: chmod only on file creation, not every append (TOCTOU fix)
- jwt_utils: add extract_unverified_payload() helper
- cli: use extract_unverified_payload + JWTError instead of inline decode
- keys: extract _public_key_bytes() helper, import _b64url from jwt_utils
- security: FileNotFoundError try/except instead of path.exists() (TOCTOU fix)
- serve: cache JWK response at server init instead of per-request recompute

138 tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 08:25:21 +01:00
parent 3890dca25d
commit 52d44daec2
6 changed files with 37 additions and 20 deletions

View File

@@ -8,7 +8,6 @@ The corresponding public key is never stored separately — it is always
derived from the private key on load.
"""
import base64
import hashlib
import os
from pathlib import Path
@@ -17,6 +16,7 @@ from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
from .jwt_utils import _b64url_encode as _b64url
from .store import _store_dir
@@ -48,27 +48,28 @@ def ensure_signing_key() -> RSAPrivateKey:
return private_key
def _b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
def _public_key_bytes(private_key: RSAPrivateKey) -> tuple[bytes, bytes]:
"""Return (n_bytes, e_bytes) for the public key — shared by key_id and jwk_public."""
pub = private_key.public_key().public_numbers()
n_bytes = pub.n.to_bytes((pub.n.bit_length() + 7) // 8, byteorder="big")
e_bytes = pub.e.to_bytes((pub.e.bit_length() + 7) // 8, byteorder="big")
return n_bytes, e_bytes
def key_id(private_key: RSAPrivateKey) -> str:
"""Return a stable 16-hex-char key ID derived from the public key modulus."""
n = private_key.public_key().public_numbers().n
n_bytes = n.to_bytes((n.bit_length() + 7) // 8, byteorder="big")
n_bytes, _ = _public_key_bytes(private_key)
return hashlib.sha256(n_bytes).hexdigest()[:16]
def jwk_public(private_key: RSAPrivateKey) -> dict:
"""Return the RSA public key as a JWK dict (RS256, sig use)."""
pub = private_key.public_key().public_numbers()
n_bytes = pub.n.to_bytes((pub.n.bit_length() + 7) // 8, byteorder="big")
e_bytes = pub.e.to_bytes((pub.e.bit_length() + 7) // 8, byteorder="big")
n_bytes, e_bytes = _public_key_bytes(private_key)
return {
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": key_id(private_key),
"kid": hashlib.sha256(n_bytes).hexdigest()[:16],
"n": _b64url(n_bytes),
"e": _b64url(e_bytes),
}