Add transient NPM certificate material

This commit is contained in:
Lyncoln
2026-09-20 00:27:47 -04:00
parent 381119a6b5
commit 2b4a6eea79
3 changed files with 344 additions and 10 deletions
+51
View File
@@ -0,0 +1,51 @@
from dataclasses import dataclass
@dataclass(frozen=True, repr=False)
class CertificateMaterial:
"""
Secret-bearing certificate material for transient use only.
Instances must never be persisted, placed in generic reconciliation
objects, or included in logs.
"""
certificate: bytes
certificate_key: bytes
intermediate_certificate: bytes | None = None
def __post_init__(self) -> None:
if not isinstance(self.certificate, bytes) or not self.certificate:
raise ValueError(
"Certificate material requires certificate bytes"
)
if (
not isinstance(self.certificate_key, bytes)
or not self.certificate_key
):
raise ValueError(
"Certificate material requires certificate-key bytes"
)
if self.intermediate_certificate is not None and (
not isinstance(self.intermediate_certificate, bytes)
or not self.intermediate_certificate
):
raise ValueError(
"Intermediate certificate must be non-empty bytes"
)
def __repr__(self) -> str:
intermediate = (
"<redacted>"
if self.intermediate_certificate is not None
else "None"
)
return (
"CertificateMaterial("
"certificate=<redacted>, "
"certificate_key=<redacted>, "
f"intermediate_certificate={intermediate}"
")"
)
+138 -7
View File
@@ -1,10 +1,14 @@
import os
from io import BytesIO
from pathlib import PurePosixPath
from typing import Any
from zipfile import BadZipFile, ZipFile
import requests
import urllib3
from .adapters import Endpoint
from .certificates.material import CertificateMaterial
class NpmClient:
@@ -236,6 +240,138 @@ class NpmClient:
)
return data
@staticmethod
def _pem_bytes(value: Any, label: str) -> bytes:
if not isinstance(value, str) or not value.strip():
raise RuntimeError(
f"NPM certificate is missing {label}"
)
return value.encode("utf-8")
@staticmethod
def _zip_pem_member(
archive: ZipFile,
prefix: str,
) -> bytes:
matches = []
for info in archive.infolist():
if info.is_dir():
continue
basename = PurePosixPath(info.filename).name
if not basename.endswith(".pem"):
continue
stem = basename[:-4]
if not stem.startswith(prefix):
continue
suffix = stem[len(prefix):]
if not suffix.isdigit():
continue
matches.append(info)
if len(matches) != 1:
raise RuntimeError(
"NPM certificate download must contain exactly one "
f"{prefix}N.pem member"
)
data = archive.read(matches[0])
if not data:
raise RuntimeError(
f"NPM certificate download {prefix} member is empty"
)
return data
def certificate_material(
self,
local_id: str | int,
) -> CertificateMaterial:
certificate_id = self._local_id(
local_id,
"certificate",
)
detail = self.certificate(certificate_id)
provider = str(
detail.get("provider") or ""
).strip().lower()
if provider == "other":
meta = detail.get("meta")
if not isinstance(meta, dict):
raise RuntimeError(
"NPM custom certificate has invalid metadata"
)
intermediate = meta.get(
"intermediate_certificate"
)
return CertificateMaterial(
certificate=self._pem_bytes(
meta.get("certificate"),
"certificate",
),
certificate_key=self._pem_bytes(
meta.get("certificate_key"),
"certificate_key",
),
intermediate_certificate=(
None
if intermediate in (None, "")
else self._pem_bytes(
intermediate,
"intermediate_certificate",
)
),
)
if provider == "letsencrypt":
response = self.request(
"GET",
(
"/nginx/certificates/"
f"{certificate_id}/download"
),
expected_status=200,
)
try:
with ZipFile(BytesIO(response.content)) as archive:
return CertificateMaterial(
certificate=self._zip_pem_member(
archive,
"cert",
),
certificate_key=self._zip_pem_member(
archive,
"privkey",
),
intermediate_certificate=(
self._zip_pem_member(
archive,
"chain",
)
),
)
except BadZipFile as exc:
raise RuntimeError(
"NPM certificate download was not a valid ZIP archive"
) from exc
raise RuntimeError(
f"Unsupported NPM certificate provider: {provider!r}"
)
def create_certificate(
self,
payload: dict[str, Any],
@@ -258,7 +394,7 @@ class NpmClient:
certificate: bytes,
certificate_key: bytes,
intermediate_certificate: bytes | None = None,
) -> dict[str, Any]:
) -> None:
certificate_id = self._local_id(
local_id,
"certificate",
@@ -290,14 +426,9 @@ class NpmClient:
intermediate_certificate,
)
response = self.request(
self.request(
"POST",
f"/nginx/certificates/{certificate_id}/upload",
expected_status=200,
files=files,
)
return self._json_object(
response,
"certificate upload",
)