Skip to content

Commit

Permalink
Save issued certificates to database (#44)
Browse files Browse the repository at this point in the history
* Save issued certificates to database

* Improve TapirCertificate document model

* Log renew for non-activated nodes
  • Loading branch information
jschlyter authored Dec 19, 2024
1 parent bd6c969 commit 2e8b85d
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 1 deletion.
44 changes: 43 additions & 1 deletion nodeman/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions nodeman/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions nodeman/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)


Expand All @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions nodeman/x509.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
)


Expand Down

0 comments on commit 2e8b85d

Please sign in to comment.