UpdateMetadataSignature.kt

4.3 KB · raw

1package com.peachpasswords.updates23import java.util.Base644import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters5import org.bouncycastle.crypto.signers.Ed25519Signer6import org.json.JSONObject78/**9 * Public half of the Peach-vault-backed release metadata key. The private seed10 * is never shipped or read by the app. An empty value deliberately fails closed11 * until the owner provisions the generated public key in the release source.12 */13const val UPDATE_METADATA_PUBLIC_KEY_HEX = "d6a47f867d2add9c6490447c7a611c08a474a98ab7afa540f65d4347f26ba77c"1415internal fun canonicalReleaseMetadataPayload(16    version: String,17    platform: String,18    url: String,19    checksumHex: String,20    provenanceSource: String,21): ByteArray = "$version\n$platform\n$url\n$checksumHex\n$version\n$platform\n$checksumHex\n$provenanceSource"22    .toByteArray(Charsets.UTF_8)2324private fun decodeHex(value: String): ByteArray? {25    if (value.length != 64 || value.any { it !in "0123456789abcdefABCDEF" }) return null26    return ByteArray(32) { i ->27        value.substring(i * 2, i * 2 + 2).toInt(16).toByte()28    }29}3031private fun verifySignature(publicKeyHex: String, payload: ByteArray, encoded: String): Boolean {32    val key = decodeHex(publicKeyHex) ?: return false33    val signature = runCatching { Base64.getDecoder().decode(encoded) }.getOrNull() ?: return false34    if (signature.size != 64) return false35    return runCatching {36        Ed25519Signer().apply {37            init(false, Ed25519PublicKeyParameters(key, 0))38            update(payload, 0, payload.size)39        }.verifySignature(signature)40    }.getOrDefault(false)41}4243/**44 * Authenticates the platform object before [parseReleaseManifest] trusts its45 * URL, checksum, version, or notes. JSON is inspected only to locate the46 * signed fields; all semantic validation remains in the existing parser.47 */48fun parseSignedReleaseManifest(49    body: String,50    platformKey: String = ANDROID_PLATFORM_KEY,51    publicKeyHex: String = UPDATE_METADATA_PUBLIC_KEY_HEX,52): ReleaseManifest {53    val root = runCatching { JSONObject(body) }54        .getOrElse { throw ManifestFormatException("manifest is not JSON") }55    val version = root.optTextOrNull("version")56        ?: throw ManifestFormatException("signed manifest has no version")57    val platform = root.optJSONObject("platforms")?.optJSONObject(platformKey)58        ?: throw ManifestFormatException("signed manifest has no '$platformKey' artifact")59    val url = platform.optTextOrNull("url")60        ?: throw ManifestFormatException("signed artifact has no url")61    val checksum = parseArtifactChecksum(platform)62        ?: throw ManifestFormatException("signed artifact has no checksum")63    val checksumParts = checksum.split(':', limit = 2)64    val checksumHex = checksumParts.getOrNull(1)65        ?: throw ManifestFormatException("signed artifact checksum is malformed")66    val provenance = platform.optJSONObject("provenance")67        ?: throw ManifestFormatException("signed artifact has no provenance")68    val provenanceVersion = provenance.optTextOrNull("version")69        ?: throw ManifestFormatException("signed artifact provenance has no version")70    val provenancePlatform = provenance.optTextOrNull("platform")71        ?: throw ManifestFormatException("signed artifact provenance has no platform")72    val provenanceChecksum = provenance.optTextOrNull("checksum")73        ?: throw ManifestFormatException("signed artifact provenance has no checksum")74    val source = provenance.optTextOrNull("source")75        ?: throw ManifestFormatException("signed artifact provenance has no source")76    val signature = platform.optTextOrNull("signature")77        ?: throw ManifestFormatException("signed artifact has no signature")78    if (provenanceVersion != version || provenancePlatform != platformKey ||79        provenanceChecksum != checksum || checksumParts[0] != "blake3") {80        throw ManifestFormatException("signed artifact provenance does not match manifest")81    }82    if (!verifySignature(83            publicKeyHex,84            canonicalReleaseMetadataPayload(version, platformKey, url, checksumHex, source),85            signature,86        )87    ) {88        throw ManifestFormatException("release metadata signature verification failed")89    }90    return parseReleaseManifest(body, platformKey).copy(91        signature = signature,92        provenanceSource = source,93        platformKey = platformKey,94    )95}