Start Android with the engine, held to the published vectors
Android does not exist, and the first thing it needs is not a screen. It needs the protocol implemented a third time — which is only worth anything if the third implementation is checked against the spec and the published vectors rather than against a reading of our own Swift. So `android/engine` is a plain Kotlin/JVM library with no Android in it: no SDK, no emulator, no device, and therefore gateable in CI on Linux like everything else. It reads engine/test/vectors/protocol-v1.json from its one location rather than carrying a copy, because a vector transcribed into the test that checks it is a vector that agrees with itself. Covered so far: safety numbers (§7) and key transparency verification (§19) — RFC 6962 leaf and node hashing, the unbalanced split, and bottom-up audit paths. Nine tests, all green against the same file the JavaScript and Swift engines answer to. The §19 tests are the ones worth having. A top-down reading of a bottom-up audit path yields the correct root for every left-spine index, so it passes any suite that only checks index 0 — the vectors pin a right-descending index, and the tests assert both that the published path verifies and that the same path reversed does not. android/README.md lists what is missing — X25519, X3DH, the ratchet, sealed sender, both post-quantum halves, the relay client, the app — as a table rather than as an omission. Android stays honestly marked "not started" on the site until that list is shorter. Bytecode targets 17 without demanding a JDK 17 be installed: a jvmToolchain(17) fails on any newer JDK, which is a poor trade for a library with no JDK 17-specific behaviour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
package io.flamenet.e2e
|
||||
|
||||
/**
|
||||
* Key transparency verification (spec §19), client side.
|
||||
*
|
||||
* Verification only, deliberately. There is nothing here that can *build* a
|
||||
* tree, because a client able to construct a proof is a client that can be
|
||||
* talked into believing one it constructed itself.
|
||||
*
|
||||
* A transparency log without a client that refuses unproven keys is just a
|
||||
* log: it faithfully records the substitution, and nobody reads it.
|
||||
*/
|
||||
object KeyLog {
|
||||
|
||||
/** RFC 6962 domain separation: leaves under 0x00, interior nodes under 0x01. */
|
||||
fun leafHash(data: ByteArray): ByteArray = Bytes.sha256(Bytes.concat(byteArrayOf(0x00), data))
|
||||
|
||||
fun nodeHash(left: ByteArray, right: ByteArray): ByteArray =
|
||||
Bytes.sha256(Bytes.concat(byteArrayOf(0x01), left, right))
|
||||
|
||||
/**
|
||||
* The largest power of two strictly below [n].
|
||||
*
|
||||
* Not n/2. The unbalanced split is what makes the tree's shape depend only
|
||||
* on its size, which is what lets two verifiers agree without exchanging
|
||||
* anything about its structure.
|
||||
*/
|
||||
fun split(n: Int): Int {
|
||||
var k = 1
|
||||
while (k shl 1 < n) k = k shl 1
|
||||
return k
|
||||
}
|
||||
|
||||
/**
|
||||
* The exact bytes the relay commits to for one key publication (§19.2).
|
||||
*
|
||||
* This binding is the part that matters. Proving that *some* leaf sits in
|
||||
* the log proves nothing at all; what has to hold is that the leaf at the
|
||||
* claimed index is this device's published identity and no other.
|
||||
*/
|
||||
fun canonicalLeaf(
|
||||
kind: String,
|
||||
userId: Long,
|
||||
deviceId: String,
|
||||
ikSigB64: String,
|
||||
ikDhB64: String,
|
||||
): ByteArray = "v1|$kind|$userId|$deviceId|$ikSigB64|$ikDhB64".toByteArray(Charsets.UTF_8)
|
||||
|
||||
/**
|
||||
* Recompute the root from a leaf and its audit path.
|
||||
*
|
||||
* The path is consumed **bottom-up** — the leaf's own sibling first, the
|
||||
* highest sibling last. That is the order RFC 6962 specifies and the order
|
||||
* the relay emits, and getting it backwards is not a loud failure: a
|
||||
* top-down reading produces the correct root for any left-spine index, so
|
||||
* it passes any test that only ever checks index 0. The vector suite pins a
|
||||
* right-descending index precisely to catch that.
|
||||
*
|
||||
* Returns null rather than throwing: a bad proof is an expected answer from
|
||||
* a hostile server, not an exceptional condition.
|
||||
*/
|
||||
fun rootFromInclusion(
|
||||
leaf: ByteArray,
|
||||
index: Int,
|
||||
size: Int,
|
||||
proof: List<ByteArray>,
|
||||
): ByteArray? {
|
||||
if (index < 0 || index >= size) return null
|
||||
var fn = index
|
||||
var sn = size - 1
|
||||
var r = leaf
|
||||
var i = 0
|
||||
while (sn > 0) {
|
||||
if (i >= proof.size) return null
|
||||
if ((fn and 1) != 0 || fn == sn) {
|
||||
r = nodeHash(proof[i++], r)
|
||||
while (fn != 0 && (fn and 1) == 0) { fn = fn shr 1; sn = sn shr 1 }
|
||||
} else {
|
||||
r = nodeHash(r, proof[i++])
|
||||
}
|
||||
fn = fn shr 1
|
||||
sn = sn shr 1
|
||||
}
|
||||
// Leftover path elements mean the proof does not describe this tree.
|
||||
return if (i == proof.size) r else null
|
||||
}
|
||||
|
||||
fun verifyInclusion(
|
||||
leaf: ByteArray,
|
||||
index: Int,
|
||||
size: Int,
|
||||
proof: List<ByteArray>,
|
||||
root: ByteArray,
|
||||
): Boolean {
|
||||
val got = rootFromInclusion(leaf, index, size, proof) ?: return false
|
||||
return Bytes.constantTimeEquals(got, root)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user