From 1bd80788cbee58f75024dee1b6788ab479784cfb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 23:34:12 +0000 Subject: [PATCH] feat(gateway): publish the mesh CA public key for fetch-and-pin at startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds GET /api/v1/mesh/ca returning {alg:"Ed25519", meshCaPublicKey:, keyId:} from the configured mesh_ca_public_key — the same Ed25519 trust anchor the gateway already verifies leader credentials against. This is the CA-distribution gap from the trust-root assessment: today the mesh CA public key is only *configured* server-side and expected to be embedded in the client build. Exposing it lets a device fetch and PIN the anchor at mesh startup over this server's TLS channel (the web-PKI bootstrap), so the single central Ed25519 mesh CA is consolidated by distribution, not by embedding. Public + unauthenticated (a public key is not secret), mirroring the JWKS endpoint; guarded to 404 when no mesh CA is configured. The route is placed so the / converter cannot shadow the static /ca path. keyId is a stable deterministic id so the client can pin and, later, distinguish a rotated key. Component scenario added (mesh_gateway.feature) asserting the endpoint returns the configured test CA key with an Ed25519 alg + keyId. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01WppuiKZt4CuQxX4N7k6SVR --- app/com/aether/tome/api/routes/mesh.py | 18 ++++++++++++++++++ .../component/features/mesh_gateway.feature | 11 ++++++++++- app/test/component/steps/mesh_gateway_steps.py | 18 +++++++++++++++++- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/app/com/aether/tome/api/routes/mesh.py b/app/com/aether/tome/api/routes/mesh.py index 2077745..50f2f63 100644 --- a/app/com/aether/tome/api/routes/mesh.py +++ b/app/com/aether/tome/api/routes/mesh.py @@ -1,6 +1,7 @@ # Vendor imports import base64 +import hashlib from http.client import BAD_REQUEST, CONFLICT, FORBIDDEN, INTERNAL_SERVER_ERROR, NOT_FOUND, SERVICE_UNAVAILABLE import logging import json @@ -578,6 +579,23 @@ def mesh_device_roles_requests(m_id): return resp_ok({"requests": pending_role_requests(UUID(m_id))}) +@mesh_bp.route("/ca", methods=["GET"]) +def mesh_ca_public_key(): # A1 trust-anchor publish + """Publish the mesh enrollment CA's Ed25519 public trust anchor so a device can FETCH and PIN + it at mesh startup over this server's TLS channel — the web-PKI bootstrap — instead of embedding + it in the client build. A public key is not secret, so this is unauthenticated, mirroring the + JWKS endpoint. [keyId] is a stable, deterministic id (SHA-256 of the raw key, truncated) so the + client can pin the anchor and, later, distinguish a rotated key without guessing.""" + if _gw_conf is None or not _gw_conf.mesh_ca_public_key: + abort(NOT_FOUND, "mesh CA is not configured on this server") + pub_b64 = _gw_conf.mesh_ca_public_key + try: + key_id = hashlib.sha256(base64.b64decode(pub_b64)).hexdigest()[:16] + except Exception: + abort(SERVICE_UNAVAILABLE, "mesh CA public key is misconfigured") + return resp_ok({"alg": "Ed25519", "meshCaPublicKey": pub_b64, "keyId": key_id}) + + @mesh_bp.route("//device//credential", methods=["GET"]) @require_login def mesh_device_credential(m_id, d_id): # A1 pull diff --git a/app/test/component/features/mesh_gateway.feature b/app/test/component/features/mesh_gateway.feature index 4060de3..57bc8b1 100644 --- a/app/test/component/features/mesh_gateway.feature +++ b/app/test/component/features/mesh_gateway.feature @@ -96,4 +96,13 @@ Feature: Node Mesh Gateway (REST fallback + console endpoints) @gateway @error_handling Scenario: Link status for a mesh that does not exist is not found When I request the link status for a random unknown mesh - Then the response status is 404 \ No newline at end of file + Then the response status is 404 + # ═══════════════════════════════════════════════════════════════════════════ + # Trust-anchor publish — mesh CA public key (fetch + pin at mesh startup) + # ═══════════════════════════════════════════════════════════════════════════ + + @gateway @happy_path + Scenario: Publish the mesh CA public key so a device can fetch and pin it + When I request the mesh CA public key + Then the response status is 200 + And the response publishes the configured Ed25519 mesh CA public key diff --git a/app/test/component/steps/mesh_gateway_steps.py b/app/test/component/steps/mesh_gateway_steps.py index 09608e9..80629ee 100644 --- a/app/test/component/steps/mesh_gateway_steps.py +++ b/app/test/component/steps/mesh_gateway_steps.py @@ -197,4 +197,20 @@ def then_members_list(context: dict): body = context["response"].json() assert "members" in body and isinstance(body["members"], list), ( f"Expected a 'members' list in response: {body}" - ) \ No newline at end of file + ) + +# ── Trust-anchor publish — GET /api/v1/mesh/ca ──────────────────────────────── + +@when("I request the mesh CA public key") +def when_get_mesh_ca(context: dict, api_client: TomeApiClient): + context["response"] = api_client.get("/api/v1/mesh/ca") + + +@then("the response publishes the configured Ed25519 mesh CA public key") +def then_mesh_ca_published(context: dict): + body = context["response"].json() + assert body["alg"] == "Ed25519", body + # Must equal mesh_ca_public_key in tome-test-config.yaml (the deterministic test CA), + # i.e. the SAME anchor the gateway verifies leader credentials against. + assert body["meshCaPublicKey"] == "A6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg=", body + assert isinstance(body.get("keyId"), str) and body["keyId"], body -- 2.43.0