User Story #54 » 0018-feat-netcode-unify-DeviceCredential-with-the-server-.patch
| 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)
|
||
|
}
|
||
|
}
|
||