Resolve NPM certificate material sources

This commit is contained in:
Lyncoln
2026-09-20 00:43:51 -04:00
parent 939d51d661
commit 44489db3ff
2 changed files with 540 additions and 18 deletions
+136 -18
View File
@@ -9,6 +9,7 @@ from ...adapters import (
NormalizedObject,
ObjectReference,
)
from ...certificates.material import CertificateMaterial
from ...npm_client import NpmClient
from ...object_normalizer import normalize_discovered_object
@@ -180,6 +181,137 @@ class Plugin:
for row in self.client.certificates()
]
@staticmethod
def _portable_certificate_matches(
actual: NormalizedObject,
desired: NormalizedObject,
) -> bool:
if actual.object_type != CERTIFICATES:
return False
if desired.object_type != CERTIFICATES:
return False
if actual.logical_key != desired.logical_key:
return False
for field in (
"domain_names",
"valid_from",
"expires_on",
):
if (
actual.payload.get(field)
!= desired.payload.get(field)
):
return False
return True
def _certificate_material_source(
self,
desired: NormalizedObject,
) -> CertificateMaterial:
if desired.object_type != CERTIFICATES:
raise RuntimeError(
"Certificate material source requires "
"a certificate object"
)
matches: list[
tuple[str, str, CertificateMaterial]
] = []
source_errors: list[str] = []
for endpoint in sorted(
self.config.npm_endpoints,
key=lambda item: item.identity,
):
if endpoint.identity == self.endpoint.identity:
continue
client = NpmClient(
self.config,
endpoint,
)
try:
rows = client.certificates()
except Exception as exc:
source_errors.append(
f"{endpoint.identity}: "
f"{type(exc).__name__}"
)
continue
for row in rows:
candidate = normalize_discovered_object(
self._certificate_from_row(row)
)
if not self._portable_certificate_matches(
candidate,
desired,
):
continue
if candidate.local_key is None:
raise RuntimeError(
"Matching NPM certificate has no "
"endpoint-local key"
)
material = client.certificate_material(
candidate.local_key
)
matches.append(
(
endpoint.identity,
candidate.local_key,
material,
)
)
if not matches:
detail = ""
if source_errors:
detail = (
"; unavailable sources: "
+ ", ".join(source_errors)
)
raise RuntimeError(
"No NPM source certificate material found for "
f"{desired.logical_key!r}"
f"{detail}"
)
baseline = matches[0][2]
for _, _, material in matches[1:]:
if material != baseline:
endpoints = ", ".join(
sorted(
{
endpoint_id
for (
endpoint_id,
_,
_,
) in matches
}
)
)
raise RuntimeError(
"Conflicting NPM certificate material "
f"sources for {desired.logical_key!r}: "
f"{endpoints}"
)
return baseline
def apply(self, action):
raise RuntimeError(
f"{self.endpoint.identity} does not support apply yet"
@@ -210,28 +342,14 @@ class Plugin:
if actual.local_key != object.local_key:
return False
if actual.logical_key != object.logical_key:
return False
# Provider, display name, and renewal flags describe how
# this NPM endpoint manages the certificate. They are not
# portable certificate identity. A copied Let's Encrypt
# certificate becomes provider="other" on the recipient.
#
# The portable verification state is the domain identity
# plus the certificate validity period exposed by NPM.
for field in (
"domain_names",
"valid_from",
"expires_on",
):
if (
actual.payload.get(field)
!= object.payload.get(field)
):
return False
return True
return self._portable_certificate_matches(
actual,
object,
)
raise RuntimeError(
f"{self.endpoint.identity} cannot verify "