Audit M1: verify the signed log head against a pinned issuer key
The engine had no head-signature function at all and Trust.head() threw the signature away, so a fork was an accusation rather than evidence. KeyLog.verifyHead now checks the same string the relay signs, the issuer key is pinned in the vault on first sight, and a head that fails under the pin raises the standing fork alarm. Four tests pin the string and the refusals — including a head signed by a different key, which is the substitution the pin exists to stop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e435cd710d
commit
b0c5a108b2
@@ -429,6 +429,19 @@ class RelayClient(
|
||||
private fun getJson(path: String, query: Map<String, String> = emptyMap()): JSONObject =
|
||||
requestJson("GET", url(path, query), path, null)
|
||||
|
||||
/**
|
||||
* `/.well-known/flamenet/relay` — the relay's self-description, carrying
|
||||
* the issuer key its log heads are signed with (audit M1).
|
||||
*
|
||||
* ⚠️ Root-relative: [routePath] prefixes the REST mount, and a well-known
|
||||
* URI is not under it. Unauthenticated by design, so no proof is signed.
|
||||
*/
|
||||
fun relayDescriptor(): JSONObject {
|
||||
val url = base.newBuilder()
|
||||
.addPathSegments(".well-known/flamenet/relay").build()
|
||||
return requestJson("GET", url, "/.well-known/flamenet/relay", null)
|
||||
}
|
||||
|
||||
private fun postJson(path: String, body: JSONObject): JSONObject =
|
||||
requestJson("POST", url(path), path, body.toString().toByteArray(Charsets.UTF_8))
|
||||
|
||||
|
||||
@@ -461,7 +461,37 @@ class Trust(
|
||||
private data class Head(val size: Int, val root: String, val audience: String)
|
||||
|
||||
private fun head(): Head = relay.logHead().let {
|
||||
Head(it.getInt("size"), it.getString("root"), it.optString("audience"))
|
||||
val head = Head(it.getInt("size"), it.getString("root"), it.optString("audience"))
|
||||
verifyHeadSignature(head, it.optString("signature"))
|
||||
head
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the relay's signature over a head against the pinned issuer key
|
||||
* (§19.3, audit M1).
|
||||
*
|
||||
* ⚠️ Silent when nothing is published or the relay predates the field — a
|
||||
* deployment that cannot sign is weaker, not hostile, and refusing it
|
||||
* outright is a worse failure than the one prevented. A signature that is
|
||||
* present and WRONG, or that stops matching a key already pinned, is a
|
||||
* fork and is treated as one.
|
||||
*/
|
||||
private fun verifyHeadSignature(head: Head, signatureB64: String) {
|
||||
if (signatureB64.isEmpty()) return
|
||||
val signature = runCatching { Wire.unb64(signatureB64) }.getOrNull() ?: return
|
||||
var issuer = vault.keyLogIssuerPin()
|
||||
if (issuer == null) {
|
||||
val published = runCatching {
|
||||
Wire.unb64(relay.relayDescriptor().optString("issuerPublicKey"))
|
||||
}.getOrNull()
|
||||
if (published == null || published.isEmpty()) return
|
||||
vault.saveKeyLogIssuerPin(published)
|
||||
issuer = published
|
||||
}
|
||||
if (!KeyLog.verifyHead(head.size, head.root, signature, issuer)) {
|
||||
forkDetected = true
|
||||
throw KeyLogForkException("log head signature does not verify under the pinned key")
|
||||
}
|
||||
}
|
||||
|
||||
private fun consistent(from: Int, to: Int, oldRoot: String, newRoot: String): Boolean {
|
||||
|
||||
Reference in New Issue
Block a user