Skip to content

Commit

Permalink
Implement serving public keys for legacy nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
jschlyter committed Jan 8, 2025
1 parent eb54e5e commit bc6ccdb
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 7 deletions.
25 changes: 24 additions & 1 deletion nodeman/nodes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import logging
from datetime import datetime, timezone
from pathlib import Path
from typing import Annotated

from cryptography import x509
Expand All @@ -11,6 +12,8 @@
from opentelemetry import metrics, trace
from pydantic_core import ValidationError

from dnstapir.key_resolver import KEY_ID_VALIDATOR

from .authn import get_current_username
from .db_models import TapirCertificate, TapirNode, TapirNodeEnrollment
from .jose import PublicEC, PublicOKP, PublicRSA
Expand Down Expand Up @@ -52,6 +55,20 @@ def find_node(name: str) -> TapirNode:
raise HTTPException(status.HTTP_404_NOT_FOUND)


def find_legacy_node(name: str, legacy_nodes_directory: Path) -> TapirNode:
"""Return node from fallback nodes directory"""
try:
if not KEY_ID_VALIDATOR.match(name):
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Invalid node name")
with open(legacy_nodes_directory / f"{name}.pem", "rb") as fp:
public_key = JWK.from_pem(fp.read())
logging.info("Returning legacy node %s", name)
return TapirNode(name=name, public_key=public_key.export(as_dict=True, private_key=False))
except FileNotFoundError:
pass
raise HTTPException(status.HTTP_404_NOT_FOUND)


def create_node_configuration(name: str, request: Request) -> NodeConfiguration:
return NodeConfiguration(
name=name,
Expand Down Expand Up @@ -200,7 +217,13 @@ async def get_node_public_key(
) -> Response:
"""Get public key (JWK/PEM) for node"""

node = find_node(name)
try:
node = find_node(name)
except HTTPException as exc:
if exc.status_code == 404 and request.app.settings.legacy_nodes_directory:
node = find_legacy_node(name, request.app.settings.legacy_nodes_directory)
else:
raise exc

span = trace.get_current_span()
span.set_attribute("node.name", name)
Expand Down
13 changes: 12 additions & 1 deletion nodeman/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@
from typing import Annotated, Self

from argon2 import PasswordHasher
from pydantic import AnyHttpUrl, BaseModel, Field, FilePath, StringConstraints, UrlConstraints, model_validator
from pydantic import (
AnyHttpUrl,
BaseModel,
DirectoryPath,
Field,
FilePath,
StringConstraints,
UrlConstraints,
model_validator,
)
from pydantic_core import Url
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, TomlConfigSettingsSource

Expand Down Expand Up @@ -79,6 +88,8 @@ class Settings(BaseSettings):

nodes: NodesSettings = Field(default=NodesSettings())

legacy_nodes_directory: DirectoryPath | None = None

model_config = SettingsConfigDict(toml_file="nodeman.toml")

@model_validator(mode="after")
Expand Down
59 changes: 55 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ nodeman_client = "nodeman.client:main"

[tool.poetry.dependencies]
python = "^3.12"
dnstapir = {git = "https://github.com/dnstapir/python-dnstapir.git", rev = "v1.2.0", extras = ["opentelemetry"]}
dnstapir = {git = "https://github.com/dnstapir/python-dnstapir.git", rev = "v1.2.3", extras = ["keymanager", "opentelemetry"]}
mongoengine = "^0.29.0"
fastapi = ">=0.114.0"
uvicorn = ">=0.30.1"
Expand Down
4 changes: 4 additions & 0 deletions tests/legacy/legacy.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6GJqVa8UM3qVnOaazv18vxs2moKR
wzGFVQY3BA2q6ORncf2WdH2Fsm0Zt0EG7Bmv7vuPhUl1d8FEo7Al3tjsBw==
-----END PUBLIC KEY-----
2 changes: 2 additions & 0 deletions tests/test.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
legacy_nodes_directory = "tests/legacy"

[mongodb]
server = "mongomock://localhost/nodes"

Expand Down
23 changes: 23 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,3 +429,26 @@ def test_not_found() -> None:

response = client.get(urljoin(server, f"/api/v1/node/{name}/public_key"), headers={"Accept": PublicKeyFormat.PEM})
assert response.status_code == status.HTTP_404_NOT_FOUND


def test_legacy_node_public_key() -> None:
client = get_test_client()
name = "legacy"
public_key_url = f"/api/v1/node/{name}/public_key"

response = client.get(public_key_url, headers={"Accept": PublicKeyFormat.JWK})
assert response.status_code == status.HTTP_200_OK
_ = JWK.from_json(response.text)

response = client.get(public_key_url, headers={"Accept": PublicKeyFormat.PEM})
assert response.status_code == status.HTTP_200_OK
_ = JWK.from_pem(response.text.encode())


def test_legacy_node_public_key_invalid_name() -> None:
client = get_test_client()
name = "räksmörgås"
public_key_url = f"/api/v1/node/{name}/public_key"

response = client.get(public_key_url, headers={"Accept": PublicKeyFormat.JWK})
assert response.status_code == status.HTTP_400_BAD_REQUEST

0 comments on commit bc6ccdb

Please sign in to comment.