Project

General

Profile

User Story #55 » 0019-feat-netcode-fetch-pin-the-mesh-CA-and-assemble-the-.patch

knight8241, 08/07/2026 18:32

View differences:

androidApp/src/main/java/com/aether/mofe/platform/control/ControlPlaneAPi.kt
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
/** Session-cookie authed control-plane client (manual serialization, ktor-core). */
......
authed(); jsonBody(buildJsonObject { put("target_user_id", targetUserId) }.toString())
}.status.isSuccess()
/**
* Fetch the mesh CA trust anchor — GET /api/v1/mesh/ca → {alg, meshCaPublicKey, keyId}. Public
* (no auth); the device pins the returned Ed25519 key over this web-PKI-authenticated TLS
* channel so every peer link verifies against a single central CA (see the trust-root
* assessment). Returns (base64 public key, keyId) or null if unavailable.
*/
suspend fun fetchMeshCaPublicKey(): Pair<String, String?>? {
val r = http.get("$baseUrl/api/v1/mesh/ca")
if (!r.status.isSuccess()) return null
val o = json.parseToJsonElement(r.bodyAsText()).jsonObject
val key = o["meshCaPublicKey"]?.jsonPrimitive?.contentOrNull ?: return null
return key to o["keyId"]?.jsonPrimitive?.contentOrNull
}
suspend fun pullCredential(meshId: String, deviceId: String, pub: String): JsonObject? {
val r = http.get("$baseUrl/api/v1/mesh/$meshId/device/$deviceId/credential") {
authed(); parameter("pub", pub)
androidApp/src/main/java/com/aether/mofe/platform/control/MeshRegistrar.kt
import com.aether.mofe.platform.band.BandNodeRecord
import com.aether.mofe.platform.identity.DeviceIdentityProvider
import com.aether.mofe.platform.identity.RawSeedProvider
import com.aether.mofe.platform.netcode.NetcodeTrust
class MeshRegistrar(
private val api: ControlPlaneApi,
......
if (cred == null) return false // mesh gone / not enrolled → caller clears stale state
repo.setCanonicalMeshId(meshId)
// Pin the mesh CA trust anchor for the netcode assembly, fetched over the server's TLS
// channel (the web-PKI bootstrap). Best-effort: a server without the endpoint, or offline,
// must not block enrollment — the mesh runs on the band regardless. NetcodeTrust then has
// both inputs MeshNodeDependencies needs (this pinned CA key + a DeviceIdentity built from
// the pulled credential); constructing the MeshNode itself remains gated on peer discovery.
runCatching {
api.fetchMeshCaPublicKey()?.let { (key, kid) -> NetcodeTrust.pinMeshCa(key, kid) }
}
// Resolve THIS device's real relationship to the mesh from the server,
// rather than assuming. The mesh OWNER (its creator) is the GATEWAY: it
// runs the single leader uplink and holds GATEWAY/ROOT/ANCHOR. Every
androidApp/src/main/java/com/aether/mofe/platform/netcode/NetcodeTrust.kt
package com.aether.mofe.platform.netcode
import android.util.Base64
import android.util.Log
import com.aether.mofe.messaging.MeshCodec
import com.aether.mofe.messaging.security.DeviceIdentity
import com.aether.mofe.platform.identity.DeviceIdentityProvider
import com.aether.mofe.platform.identity.RawSeedProvider
/**
* Assembles the two trust inputs the netcode assembly (`MeshNode.build` → `MeshNodeDependencies`)
* needs, from material this app already holds — the on-device realization of the CA trust-root
* consolidation:
*
* • [meshCaPublicKey] — the Ed25519 mesh-CA trust anchor, FETCHED and PINNED from the server's
* `GET /api/v1/mesh/ca` over the web-PKI-authenticated TLS channel (see
* `ControlPlaneApi.fetchMeshCaPublicKey` and the trust-root assessment). Pinning it once means
* every peer credential is verified against ONE central CA rather than a per-build embedded key.
* • [deviceIdentity] — this device's Ed25519 keypair (from its seed) bound to its mesh-CA-signed
* credential, parsed from the server's JSON into the unified `DeviceCredential`
* (`MeshCodec.decodeCredentialJson`). The same credential now authenticates the device on both
* the uplink and the peer-to-peer netcode planes.
*
* This is the seam `MeshNodeDependencies(meshCaPublicKey = …, identity = …)` reads from. NOTE:
* constructing and starting the actual `MeshNode` is the remaining netcode go-live step and is
* still gated on peer IP discovery (the band carries UWB addresses, not IPs); this class only makes
* the trust material ready so that step is a straight wire-up.
*/
object NetcodeTrust {
private const val TAG = "NetcodeTrust"
@Volatile private var pinnedCaPublicKey: ByteArray? = null
@Volatile private var pinnedKeyId: String? = null
/**
* Pin the mesh CA anchor fetched from the server. Idempotent; logs (does not silently accept) a
* changed key id, since a rotated CA key is a trust event the app should surface.
*/
fun pinMeshCa(meshCaPublicKeyB64: String, keyId: String?) {
val decoded = runCatching { Base64.decode(meshCaPublicKeyB64, Base64.DEFAULT) }.getOrNull() ?: return
val prevId = pinnedKeyId
if (prevId != null && keyId != null && prevId != keyId) {
Log.w(TAG, "mesh CA key id changed ($prevId → $keyId) — CA rotated or endpoint switched")
}
pinnedCaPublicKey = decoded
pinnedKeyId = keyId
}
/** The pinned Ed25519 mesh CA public key (raw bytes), or null until [pinMeshCa] has run. */
fun meshCaPublicKey(): ByteArray? = pinnedCaPublicKey
/** The pinned CA key id (for diagnostics / rotation detection), or null. */
fun keyId(): String? = pinnedKeyId
/**
* Build the netcode [DeviceIdentity] for `MeshNodeDependencies` from this device's seed and the
* mesh-CA credential JSON pulled from the server (`ControlPlaneApi.pullCredential`, stringified).
* The 32-byte Ed25519 seed IS the private key. Returns null if the seed is unavailable (a
* Keystore-backed identity that can't export it) or the credential can't be parsed.
*/
fun deviceIdentity(self: DeviceIdentityProvider, credentialJson: String): DeviceIdentity? {
val seedB64 = (self as? RawSeedProvider)?.seedB64 ?: return null
val cred = MeshCodec.decodeCredentialJson(credentialJson) ?: return null
val priv = runCatching { Base64.decode(seedB64, Base64.DEFAULT) }.getOrNull() ?: return null
val pub = runCatching { Base64.decode(self.ed25519PubB64, Base64.DEFAULT) }.getOrNull() ?: return null
return DeviceIdentity(self.deviceIdV5(), pub, priv, cred)
}
}
common/src/commonMain/kotlin/com/aether/mofe/messaging/MeshCodec.kt
* hex). This is a CROSS-REPO LOCK-STEP contract with `gateway/ca.py::_canonical` — see
* [DeviceCredential]. `notAfter` is an integer (microseconds); `is_emulated` a bare bool.
*/
/**
* Parse a mesh-CA [DeviceCredential] from the tome-server's JSON — the `credential` object
* returned by enroll / pullCredential. Field names match the server exactly (see
* [DeviceCredential], incl. `is_emulated`), so kotlinx JSON deserializes it directly; the parsed
* credential re-encodes via [encodeCredentialBody] to the SAME canonical bytes the CA signed, so
* it verifies against the mesh CA public key unchanged. Returns null on malformed input. This is
* the on-device bridge from the JSON control plane to the netcode credential type.
*/
fun decodeCredentialJson(jsonText: String): DeviceCredential? = try {
json.decodeFromString(DeviceCredential.serializer(), jsonText)
} catch (_: Exception) { null }
fun encodeCredentialBody(c: DeviceCredential): ByteArray {
val sb = StringBuilder(256)
sb.append('{')
common/src/commonTest/kotlin/com/aether/mofe/messaging/MeshCodecTest.kt
"\"notAfter\":1893456000000000}"
assertEquals(expected, MeshCodec.encodeCredentialBody(cred).decodeToString())
}
@Test
fun decodeCredentialJsonParsesServerCredentialAndReencodesToCanonicalBody() {
// A credential object shaped exactly like the tome-server enroll/pullCredential response
// (arbitrary key order, whitespace). Parsing it and re-encoding the body MUST reproduce the
// CA's signed canonical bytes, so the server signature verifies unchanged on-device.
val serverJson =
"""{"deviceId": "11111111-2222-3333-4444-555555555555", "ed25519Pub":""" +
""" "TWVzaFB1YktleTAxMjM0NTY3ODlBQkNERUZHSA==", "deviceType": "ANCHOR",""" +
""" "deviceName": "Aether 1", "deviceRoles": ["GATEWAY", "ROOT", "ANCHOR"],""" +
""" "meshId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "meshName": "Warehouse A",""" +
""" "notAfter": 1900000000000000, "claims": ["voterEligible", "meshCreate"],""" +
""" "is_emulated": false, "signature": "c2lnbmF0dXJlLXBsYWNlaG9sZGVy"}"""
val cred = MeshCodec.decodeCredentialJson(serverJson)!!
assertEquals("11111111-2222-3333-4444-555555555555", cred.deviceId)
assertEquals(1900000000000000L, cred.notAfterMicros)
assertTrue(cred.voterEligible) // derived from the claims list
assertTrue("meshCreate" in cred.claims)
assertEquals(false, cred.isEmulated)
val expectedCanonical =
"{\"claims\":[\"voterEligible\",\"meshCreate\"]," +
"\"deviceId\":\"11111111-2222-3333-4444-555555555555\",\"deviceName\":\"Aether 1\"," +
"\"deviceRoles\":[\"GATEWAY\",\"ROOT\",\"ANCHOR\"],\"deviceType\":\"ANCHOR\"," +
"\"ed25519Pub\":\"TWVzaFB1YktleTAxMjM0NTY3ODlBQkNERUZHSA==\",\"is_emulated\":false," +
"\"meshId\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\"meshName\":\"Warehouse A\"," +
"\"notAfter\":1900000000000000}"
assertEquals(expectedCanonical, MeshCodec.encodeCredentialBody(cred).decodeToString())
}
}
(1-1/2)