diff --git a/nodeman/db_models.py b/nodeman/db_models.py index c6e5d8d..c1499e7 100644 --- a/nodeman/db_models.py +++ b/nodeman/db_models.py @@ -2,7 +2,9 @@ from contextlib import suppress from typing import Self -from mongoengine import DateTimeField, DictField, Document, StringField +from cryptography import x509 +from cryptography.hazmat.primitives import serialization +from mongoengine import DateTimeField, DictField, Document, StringField, ValidationError from mongoengine.errors import NotUniqueError from .names import get_deterministic_name, get_random_name @@ -41,3 +43,43 @@ class TapirNodeEnrollment(Document): name = StringField(unique=True) key = DictField() + + +class TapirCertificate(Document): + meta = { + "collection": "certificates", + "indexes": [ + {"fields": ["name"]}, + {"fields": ["issuer", "serial"], "unique": True}, + ], + } + + name = StringField(required=True) + + issuer = StringField(required=True) + subject = StringField(required=True) + serial = StringField(required=True) + + not_valid_before = DateTimeField(required=True) + not_valid_after = DateTimeField(required=True) + + certificate = StringField(required=True) + + @classmethod + def from_x509_certificate(cls, name: str, x509_certificate: x509.Certificate) -> Self: + return cls( + name=name, + issuer=x509_certificate.issuer.rfc4514_string(), + subject=x509_certificate.subject.rfc4514_string(), + certificate=x509_certificate.public_bytes(serialization.Encoding.PEM).decode(), + serial=str(x509_certificate.serial_number), + not_valid_before=x509_certificate.not_valid_before_utc, + not_valid_after=x509_certificate.not_valid_after_utc, + ) + + def clean(self): + """Validate certificate field format""" + try: + x509.load_pem_x509_certificate(self.certificate.encode()) + except ValueError as exc: + raise ValidationError("Invalid certificate PEM format") from exc diff --git a/nodeman/models.py b/nodeman/models.py index a06c1a2..0e03040 100644 --- a/nodeman/models.py +++ b/nodeman/models.py @@ -72,6 +72,7 @@ class NodeCertificate(BaseModel): x509_certificate: str = Field(title="X.509 Client Certificate Bundle") x509_ca_certificate: str = Field(title="X.509 CA Certificate Bundle") x509_certificate_serial_number: int | None = Field(default=None, exclude=True) + x509_certificate_not_valid_after: datetime @field_validator("x509_certificate", "x509_ca_certificate") @classmethod diff --git a/nodeman/nodes.py b/nodeman/nodes.py index a4810b6..1b60b23 100644 --- a/nodeman/nodes.py +++ b/nodeman/nodes.py @@ -277,6 +277,7 @@ async def enroll_node( x509_certificate=node_certificate.x509_certificate, x509_ca_certificate=node_certificate.x509_ca_certificate, x509_certificate_serial_number=node_certificate.x509_certificate_serial_number, + x509_certificate_not_valid_after=node_certificate.x509_certificate_not_valid_after, ) @@ -297,6 +298,7 @@ async def renew_node( node = find_node(name) if not node.activated: + logging.debug("Renewal attempt for non-activated node %s", name, extra={"nodename": name}) raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Node not activated") span = trace.get_current_span() diff --git a/nodeman/x509.py b/nodeman/x509.py index e061427..79b8930 100644 --- a/nodeman/x509.py +++ b/nodeman/x509.py @@ -13,6 +13,7 @@ from cryptography.x509.oid import ExtensionOID, NameOID from fastapi import HTTPException, Request, status +from .db_models import TapirCertificate from .models import NodeCertificate RSA_EXPONENT = 65537 @@ -145,6 +146,8 @@ def process_csr_request(request: Request, csr: x509.CertificateSigningRequest, n x509_certificate_serial_number = x509_certificate.serial_number x509_not_valid_after_utc = x509_certificate.not_valid_after_utc.isoformat() + TapirCertificate.from_x509_certificate(name=name, x509_certificate=x509_certificate).save() + logger.info( "Issued certificate for name=%s serial=%d not_valid_after=%s", name, @@ -161,6 +164,7 @@ def process_csr_request(request: Request, csr: x509.CertificateSigningRequest, n x509_certificate=x509_certificate_pem, x509_ca_certificate=x509_ca_certificate_pem, x509_certificate_serial_number=x509_certificate_serial_number, + x509_certificate_not_valid_after=x509_certificate.not_valid_after_utc, )