Project

General

Profile

User Story #54 » 0018-feat-netcode-unify-DeviceCredential-with-the-server-.patch

knight8241, 08/07/2026 18:32

View differences:

common/src/commonMain/kotlin/com/aether/mofe/messaging/MeshBase64.kt
package com.aether.mofe.messaging
/**
* Standard Base64 (RFC 4648, `+/` alphabet, `=` padding, no line breaks) — the exact form Python's
* `base64.b64encode` produces on the server. The mesh CA credential carries `ed25519Pub` and
* `signature` as base64 STRINGS (so the client can reproduce the CA's canonical-JSON signed body
* byte-for-byte); this codec converts those to/from raw key/signature bytes for the crypto provider.
* Kept dependency-free (no experimental stdlib Base64, no platform API) so it is identical on every
* Kotlin target.
*/
object MeshBase64 {
private const val ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
private val DECODE = IntArray(128) { -1 }.also { for (i in ALPHABET.indices) it[ALPHABET[i].code] = i }
fun encode(data: ByteArray): String {
if (data.isEmpty()) return ""
val sb = StringBuilder((data.size + 2) / 3 * 4)
var i = 0
while (i + 3 <= data.size) {
val n = (data[i].toInt() and 0xff shl 16) or (data[i + 1].toInt() and 0xff shl 8) or (data[i + 2].toInt() and 0xff)
sb.append(ALPHABET[n ushr 18 and 0x3f]); sb.append(ALPHABET[n ushr 12 and 0x3f])
sb.append(ALPHABET[n ushr 6 and 0x3f]); sb.append(ALPHABET[n and 0x3f])
i += 3
}
when (data.size - i) {
1 -> {
val n = data[i].toInt() and 0xff shl 16
sb.append(ALPHABET[n ushr 18 and 0x3f]); sb.append(ALPHABET[n ushr 12 and 0x3f]); sb.append("==")
}
2 -> {
val n = (data[i].toInt() and 0xff shl 16) or (data[i + 1].toInt() and 0xff shl 8)
sb.append(ALPHABET[n ushr 18 and 0x3f]); sb.append(ALPHABET[n ushr 12 and 0x3f])
sb.append(ALPHABET[n ushr 6 and 0x3f]); sb.append('=')
}
}
return sb.toString()
}
/** Decode standard base64. Ignores `=` padding; throws on an invalid character. */
fun decode(s: String): ByteArray {
val clean = s.trimEnd('=')
val out = ByteArray(clean.length * 6 / 8)
var buffer = 0; var bits = 0; var oi = 0
for (c in clean) {
val v = if (c.code < 128) DECODE[c.code] else -1
require(v >= 0) { "invalid base64 character: '$c'" }
buffer = (buffer shl 6) or v; bits += 6
if (bits >= 8) { bits -= 8; out[oi++] = (buffer ushr bits and 0xff).toByte() }
}
return if (oi == out.size) out else out.copyOf(oi)
}
}
common/src/commonMain/kotlin/com/aether/mofe/messaging/MeshCodec.kt
package com.aether.mofe.messaging
import com.aether.mofe.model.messaging.DeviceCredential
import com.aether.mofe.model.messaging.MeshPayload
import com.aether.mofe.model.messaging.MessageEnvelope
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.json.Json
/**
* Single place that owns the wire format. CBOR on both mesh planes,
* JSON for the uplink and for debug logging.
*
* `ignoreUnknownKeys = true` is the schema-evolution contract: a v1 node
* can receive a v2 message that added fields and still decode it.
*/
@OptIn(ExperimentalSerializationApi::class)
object MeshCodec {
val cbor: Cbor = Cbor { ignoreUnknownKeys = true }
val json: Json = Json {
ignoreUnknownKeys = true
encodeDefaults = true
coerceInputValues = true // unknown enum → property default (e.g. CommandKind.UNKNOWN)
}
fun encode(envelope: MessageEnvelope): ByteArray =
cbor.encodeToByteArray(MessageEnvelope.serializer(), envelope)
/**
* Returns null instead of throwing on any decode failure — a malformed
* or future-typed datagram must never take down a collector loop.
* Callers count failures via [MessagingMetrics].
*/
fun decodeOrNull(bytes: ByteArray): MessageEnvelope? = try {
cbor.decodeFromByteArray(MessageEnvelope.serializer(), bytes)
} catch (_: Exception) {
null
}
fun encodeJson(envelope: MessageEnvelope): String =
json.encodeToString(MessageEnvelope.serializer(), envelope)
/** Handshake frames are bare MeshPayloads (no envelope — pre-session). */
fun encodeHandshake(payload: MeshPayload): ByteArray =
cbor.encodeToByteArray(MeshPayload.serializer(), payload)
inline fun <reified T : MeshPayload> decodeHandshake(bytes: ByteArray): T? = try {
cbor.decodeFromByteArray(MeshPayload.serializer(), bytes) as? T
} catch (_: Exception) { null }
/** Canonical CBOR of a credential with its signature field zeroed —
* the exact bytes the mesh CA signs and verifiers check (§6.3). */
fun encodeCredentialBody(c: DeviceCredential): ByteArray =
cbor.encodeToByteArray(DeviceCredential.serializer(), c.copy(signature = ByteArray(0)))
package com.aether.mofe.messaging
import com.aether.mofe.model.messaging.DeviceCredential
import com.aether.mofe.model.messaging.MeshPayload
import com.aether.mofe.model.messaging.MessageEnvelope
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.json.Json
/**
* Single place that owns the wire format. CBOR on both mesh planes,
* JSON for the uplink and for debug logging.
*
* `ignoreUnknownKeys = true` is the schema-evolution contract: a v1 node
* can receive a v2 message that added fields and still decode it.
*/
@OptIn(ExperimentalSerializationApi::class)
object MeshCodec {
val cbor: Cbor = Cbor { ignoreUnknownKeys = true }
val json: Json = Json {
ignoreUnknownKeys = true
encodeDefaults = true
coerceInputValues = true // unknown enum → property default (e.g. CommandKind.UNKNOWN)
}
fun encode(envelope: MessageEnvelope): ByteArray =
cbor.encodeToByteArray(MessageEnvelope.serializer(), envelope)
/**
* Returns null instead of throwing on any decode failure — a malformed
* or future-typed datagram must never take down a collector loop.
* Callers count failures via [MessagingMetrics].
*/
fun decodeOrNull(bytes: ByteArray): MessageEnvelope? = try {
cbor.decodeFromByteArray(MessageEnvelope.serializer(), bytes)
} catch (_: Exception) {
null
}
fun encodeJson(envelope: MessageEnvelope): String =
json.encodeToString(MessageEnvelope.serializer(), envelope)
/** Handshake frames are bare MeshPayloads (no envelope — pre-session). */
fun encodeHandshake(payload: MeshPayload): ByteArray =
cbor.encodeToByteArray(MeshPayload.serializer(), payload)
inline fun <reified T : MeshPayload> decodeHandshake(bytes: ByteArray): T? = try {
cbor.decodeFromByteArray(MeshPayload.serializer(), bytes) as? T
} catch (_: Exception) { null }
/**
* The EXACT bytes the mesh CA signs and verifiers check: the credential body (every field
* except `signature`) as canonical JSON — Python `json.dumps(body, sort_keys=True,
* separators=(",",":"), ensure_ascii=True)` — reproduced byte-for-byte so a tome-server-minted
* credential verifies on-device. Keys are emitted in sorted order; strings are escaped exactly
* like Python's `ensure_ascii` (control chars and every non-ASCII code unit → `\uXXXX`, lower
* 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.
*/
fun encodeCredentialBody(c: DeviceCredential): ByteArray {
val sb = StringBuilder(256)
sb.append('{')
sb.append("\"claims\":"); jsonArray(sb, c.claims); sb.append(',')
sb.append("\"deviceId\":"); jsonStr(sb, c.deviceId); sb.append(',')
sb.append("\"deviceName\":"); jsonStr(sb, c.deviceName); sb.append(',')
sb.append("\"deviceRoles\":"); jsonArray(sb, c.deviceRoles); sb.append(',')
sb.append("\"deviceType\":"); jsonStr(sb, c.deviceType); sb.append(',')
sb.append("\"ed25519Pub\":"); jsonStr(sb, c.ed25519Pub); sb.append(',')
sb.append("\"is_emulated\":").append(if (c.isEmulated) "true" else "false").append(',')
sb.append("\"meshId\":"); jsonStr(sb, c.meshId); sb.append(',')
sb.append("\"meshName\":"); jsonStr(sb, c.meshName); sb.append(',')
sb.append("\"notAfter\":").append(c.notAfter.toString())
sb.append('}')
return sb.toString().encodeToByteArray()
}
private fun jsonArray(sb: StringBuilder, items: List<String>) {
sb.append('[')
for (i in items.indices) { if (i > 0) sb.append(','); jsonStr(sb, items[i]) }
sb.append(']')
}
/** Append [s] as a JSON string escaped exactly like Python `json.dumps(ensure_ascii=True)`:
* the short escapes for the control chars that have them, `\uXXXX` (lower-hex) for every other
* code unit below 0x20 or above 0x7E, and `\"` / `\\` for quote and backslash. */
private fun jsonStr(sb: StringBuilder, s: String) {
sb.append('"')
for (c in s) {
when (c) {
'"' -> sb.append("\\\"")
'\\' -> sb.append("\\\\")
'\b' -> sb.append("\\b")
'\u000C' -> sb.append("\\f")
'\n' -> sb.append("\\n")
'\r' -> sb.append("\\r")
'\t' -> sb.append("\\t")
else ->
if (c.code < 0x20 || c.code > 0x7E) {
sb.append("\\u")
val hex = c.code.toString(16)
repeat(4 - hex.length) { sb.append('0') }
sb.append(hex)
} else sb.append(c)
}
}
sb.append('"')
}
}
common/src/commonMain/kotlin/com/aether/mofe/messaging/security/LinkHandshake.kt
package com.aether.mofe.messaging.security
import com.aether.mofe.messaging.ControlLink
import com.aether.mofe.messaging.MeshBase64
import com.aether.mofe.messaging.MeshCodec
import com.aether.mofe.model.messaging.DeviceCredential
import com.aether.mofe.model.messaging.SecHelloAck
......
val transcript1 = crypto.sha256(
MeshCodec.encodeHandshake(init) + MeshCodec.encodeHandshake(ack.copy(signature = ByteArray(0))),
)
if (!crypto.ed25519Verify(ack.credential.ed25519Public, transcript1, ack.signature))
if (!crypto.ed25519Verify(MeshBase64.decode(ack.credential.ed25519Pub), transcript1, ack.signature))
throw HandshakeException("leader transcript signature invalid")
val transcript2 = crypto.sha256(transcript1 + ack.signature)
......
val done = MeshCodec.decodeHandshake<SecHelloDone>(link.inboundFrames().first())
?: throw HandshakeException("expected helloDone")
val transcript2 = crypto.sha256(transcript1 + ack.signature)
if (!crypto.ed25519Verify(init.credential.ed25519Public, transcript2, done.signature))
if (!crypto.ed25519Verify(MeshBase64.decode(init.credential.ed25519Pub), transcript2, done.signature))
throw HandshakeException("member transcript signature invalid")
return deriveKeys(
......
}
private fun verifyCredential(c: DeviceCredential) {
val body = MeshCodec.encodeCredentialBody(c) // canonical CBOR, signature excluded
if (!crypto.ed25519Verify(meshCaPublicKey, body, c.signature))
val body = MeshCodec.encodeCredentialBody(c) // canonical JSON, signature excluded
if (!crypto.ed25519Verify(meshCaPublicKey, body, MeshBase64.decode(c.signature)))
throw HandshakeException("credential not signed by mesh CA")
// notAfterMicros checked against local clock by the caller (clock skew policy)
}
common/src/commonMain/kotlin/com/aether/mofe/model/messaging/SecurityMessages.kt
/**
* Mesh-CA-signed binding of a device identity to the mesh (Design §4.1).
* [signature] is Ed25519(meshCaPrivate, canonical CBOR of all other fields —
* see MeshCodec.encodeCredentialBody).
*
* This mirrors the tome-server mesh-enrollment CA credential FIELD-FOR-FIELD so ONE credential
* authenticates a device on both trust planes (the device↔server uplink and the device↔device
* netcode links). [signature] is Ed25519(meshCaPrivate, canonical JSON of all other fields) — the
* EXACT bytes the server signs: Python `json.dumps(body, sort_keys=True, separators=(",",":"),
* ensure_ascii=True)` with the signature excluded, reproduced byte-for-byte by
* [com.aether.mofe.messaging.MeshCodec.encodeCredentialBody]. [ed25519Pub] and [signature] are
* base64 strings (as in the CA's JSON) so that canonical body is reproducible verbatim; the security
* layer base64-decodes them for the crypto provider.
*
* CROSS-REPO LOCK-STEP: this field set, the sorted-key canonical JSON, and the base64 encoding must
* stay identical to tome-server `gateway/ca.py::MeshCA.mint`. Adding/removing a field here requires
* the same change there (and vice-versa), or signatures stop verifying.
*/
@Serializable
data class DeviceCredential(
val deviceId: String,
val ed25519Public: ByteArray,
/** base64 raw Ed25519 public key (server field `ed25519Pub`). */
val ed25519Pub: String,
val deviceType: String,
val deviceName: String,
val deviceRoles: List<String>,
val meshId: String,
val notAfterMicros: Long,
val voterEligible: Boolean,
val signature: ByteArray,
val meshName: String,
/** expiry, microseconds since epoch (server field `notAfter`). */
val notAfter: Long,
val claims: List<String>,
@SerialName("is_emulated") val isEmulated: Boolean,
/** base64 Ed25519 signature over the canonical body (server field `signature`). */
val signature: String,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as DeviceCredential
if (notAfterMicros != other.notAfterMicros) return false
if (voterEligible != other.voterEligible) return false
if (deviceId != other.deviceId) return false
if (!ed25519Public.contentEquals(other.ed25519Public)) return false
if (deviceType != other.deviceType) return false
if (meshId != other.meshId) return false
if (!signature.contentEquals(other.signature)) return false
/** Expiry as engine-clock micros — the CA field is named [notAfter]. */
val notAfterMicros: Long get() = notAfter
return true
}
/** Voter eligibility is carried by the CA as a claim, not a boolean field. */
val voterEligible: Boolean get() = CLAIM_VOTER_ELIGIBLE in claims
override fun hashCode(): Int {
var result = notAfterMicros.hashCode()
result = 31 * result + voterEligible.hashCode()
result = 31 * result + deviceId.hashCode()
result = 31 * result + ed25519Public.contentHashCode()
result = 31 * result + deviceType.hashCode()
result = 31 * result + meshId.hashCode()
result = 31 * result + signature.contentHashCode()
return result
companion object {
const val CLAIM_VOTER_ELIGIBLE = "voterEligible"
}
}
common/src/commonTest/kotlin/com/aether/mofe/messaging/MeshCodecTest.kt
@Test
fun handshakeHelpersRoundTripAndRejectWrongType() {
val init = SecHelloInit(
credential = DeviceCredential("d", byteArrayOf(1), "ANCHOR", "m", 9, true, byteArrayOf(2)),
credential = DeviceCredential(
deviceId = "d", ed25519Pub = "AQ==", deviceType = "ANCHOR", deviceName = "dev",
deviceRoles = listOf("ANCHOR"), meshId = "m", meshName = "mesh", notAfter = 9,
claims = listOf("voterEligible"), isEmulated = false, signature = "Ag==",
),
ephemeralX25519Public = byteArrayOf(3), nonce = byteArrayOf(4),
)
val bytes = MeshCodec.encodeHandshake(init)
......
@Test
fun credentialBodyExcludesSignature() {
val base = DeviceCredential("d", byteArrayOf(1), "ANCHOR", "m", 9, true, byteArrayOf(9, 9))
val other = base.copy(signature = byteArrayOf(7, 7, 7))
val base = DeviceCredential(
deviceId = "d", ed25519Pub = "AQ==", deviceType = "ANCHOR", deviceName = "dev",
deviceRoles = listOf("ANCHOR"), meshId = "m", meshName = "mesh", notAfter = 9,
claims = listOf("voterEligible"), isEmulated = false, signature = "CQk=",
)
val other = base.copy(signature = "Bwc=")
assertTrue(
MeshCodec.encodeCredentialBody(base).contentEquals(MeshCodec.encodeCredentialBody(other)),
"signed body must be identical regardless of the signature field",
)
}
@Test
fun credentialBodyMatchesServerCanonicalJsonByteForByte() {
// GROUND TRUTH captured verbatim from tome-server gateway/ca.py::_canonical, i.e.
// Python json.dumps(body_without_signature, sort_keys=True, separators=(",",":")) — the
// exact bytes the mesh CA signs. Exercises the tricky cases: an embedded double quote (\"),
// a non-ASCII BMP char (U+00E9 -> é, lower hex), and an astral char (U+1F697 -> the
// UTF-16 surrogate pair 🚗). Any drift in either repo's canonicalization fails
// this test — signatures would silently stop verifying cross-repo.
val cred = DeviceCredential(
deviceId = "dev-123",
ed25519Pub = "AAECAwQFBgcICQoLDA0ODw==",
deviceType = "ANCHOR",
deviceName = "Bob \"B\" café", // embedded quote + é
deviceRoles = listOf("ANCHOR", "ROOT"),
meshId = "mesh-abc",
meshName = "Garage 🚗", // astral emoji (surrogate pair)
notAfter = 1893456000000000L,
claims = listOf("voterEligible", "meshCreate"),
isEmulated = false,
signature = "this-field-is-excluded-from-the-body",
)
val expected =
"{\"claims\":[\"voterEligible\",\"meshCreate\"],\"deviceId\":\"dev-123\"," +
"\"deviceName\":\"Bob \\\"B\\\" caf\\u00e9\",\"deviceRoles\":[\"ANCHOR\",\"ROOT\"]," +
"\"deviceType\":\"ANCHOR\",\"ed25519Pub\":\"AAECAwQFBgcICQoLDA0ODw==\"," +
"\"is_emulated\":false,\"meshId\":\"mesh-abc\",\"meshName\":\"Garage \\ud83d\\ude97\"," +
"\"notAfter\":1893456000000000}"
assertEquals(expected, MeshCodec.encodeCredentialBody(cred).decodeToString())
}
}
common/src/commonTest/kotlin/com/aether/mofe/messaging/support/TestFactories.kt
import com.aether.mofe.model.Timestamp
import com.aether.mofe.model.Vector3D
import com.aether.mofe.model.messaging.DeviceCredential
import com.aether.mofe.messaging.MeshBase64
import com.aether.mofe.messaging.MeshCodec
import com.aether.mofe.platform.randomUUID
......
fun identity(crypto: MeshCryptoProvider, deviceId: String, caPriv: ByteArray, meshId: String = "m1"): DeviceIdentity {
val kp = crypto.generateEd25519()
val unsigned = DeviceCredential(
deviceId = deviceId, ed25519Public = kp.publicKey, deviceType = "ANCHOR",
meshId = meshId, notAfterMicros = Long.MAX_VALUE, voterEligible = true, signature = ByteArray(0),
deviceId = deviceId, ed25519Pub = MeshBase64.encode(kp.publicKey), deviceType = "ANCHOR",
deviceName = deviceId, deviceRoles = listOf("ANCHOR"), meshId = meshId, meshName = "mesh",
notAfter = Long.MAX_VALUE, claims = listOf(DeviceCredential.CLAIM_VOTER_ELIGIBLE),
isEmulated = false, signature = "",
)
val body = MeshCodec.encodeCredentialBody(unsigned)
val cred = unsigned.copy(signature = crypto.ed25519Sign(caPriv, body))
val cred = unsigned.copy(signature = MeshBase64.encode(crypto.ed25519Sign(caPriv, body)))
return DeviceIdentity(deviceId, kp.publicKey, kp.privateKey, cred)
}
}
    (1-1/1)