apps\iot\src\main\java\cn\nfarms\iot\ota\Ota.kt¶
源码路径:
apps\iot\src\main\java\cn\nfarms\iot\ota\Ota.kt· 174 行
package cn.nfarms.iot.ota
import cn.nfarms.core.secure.Crypto
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.Signature
import java.util.Base64
import kotlinx.serialization.Serializable
/**
* 固件升级包。
*
* ```text
* OtaPackage = {
* version : 目标版本号(单调递增,拒绝降级)
* target : 机型/硬件版本(拒绝刷错机器)
* payload : 固件字节(Base64)
* sha256 : payload 的摘要
* sigB64 : 对 "version|target|sha256" 的 ECDSA 签名
* keyId : 签名密钥标识(便于轮换)
* }
* ```
*
* # 三条验证顺序不能换
*
* 1. **先验签名**:签名不对,后面所有检查都无意义 —— 数据本身就是伪造的;
* 2. **再对摘要**:签名对但摘要不符 = 内容被改过(或签名被挪用到别的包上);
* 3. **最后才是版本/机型**:这两项是业务规则,不通过只说明"不该装",不是"被篡改"。
*
* 顺序反了会有一个具体后果:先比版本再验签,攻击者只要把 version 改大就能绕过
* 版本检查(签名验不过,但那时你已经把包当成"合法候选"了)。
*/
@Serializable
data class OtaPackage(
val version: String,
val target: String,
val payloadB64: String,
val sha256: String,
val sigB64: String,
val keyId: String = "demo-fw-1",
val releasedAt: Long = System.currentTimeMillis(),
val notes: String = "",
)
sealed interface OtaVerdict {
data object Ok : OtaVerdict
data class Rejected(val why: String, val stage: Stage) : OtaVerdict
enum class Stage { Signature, Digest, Version, Target, Size }
}
/**
* 验签 + 版本判定。
*
* 演示里密钥对在本地生成(扮演"厂商签名服务器");真机固件包里应当内置
* **厂商公钥**的指纹,并把公钥编译进只读资源 —— 决不能从服务器现拉,
* 那样攻击者换掉服务器就能换掉公钥。
*/
class OtaVerifier(private val publicKeyDer: ByteArray, private val keyId: String = "demo-fw-1") {
fun verify(pkg: OtaPackage, currentVersion: String, deviceTarget: String): OtaVerdict {
// 1. 签名
val signed = "${pkg.version}|${pkg.target}|${pkg.sha256}"
val sigOk = Crypto.verify(
publicKeyDer = publicKeyDer,
data = signed.toByteArray(Charsets.UTF_8),
signature = runCatching { Base64.getDecoder().decode(pkg.sigB64) }.getOrNull()
?: return OtaVerdict.Rejected("签名不是合法的 Base64", OtaVerdict.Stage.Signature),
signatureAlgorithm = "SHA256withECDSA",
keyAlgorithm = "EC",
)
if (!sigOk) return OtaVerdict.Rejected("签名无效 —— 这个包不是厂商签的", OtaVerdict.Stage.Signature)
if (pkg.keyId != keyId) return OtaVerdict.Rejected("keyId 不匹配(${pkg.keyId})", OtaVerdict.Stage.Signature)
// 2. 摘要
val payload = runCatching { Base64.getDecoder().decode(pkg.payloadB64) }.getOrNull()
?: return OtaVerdict.Rejected("固件内容不是合法 Base64", OtaVerdict.Stage.Digest)
if (payload.isEmpty()) return OtaVerdict.Rejected("固件是空的", OtaVerdict.Stage.Digest)
val actual = Crypto.sha256Hex(payload)
if (!actual.equals(pkg.sha256, ignoreCase = true)) {
return OtaVerdict.Rejected("SHA-256 不符:期望 ${pkg.sha256.take(16)}… 实际 ${actual.take(16)}…", OtaVerdict.Stage.Digest)
}
// 3. 机型
if (pkg.target != deviceTarget) {
return OtaVerdict.Rejected("机型不符:包是 ${pkg.target},设备是 $deviceTarget", OtaVerdict.Stage.Target)
}
// 4. 版本:只升不降
val cur = parseVersion(currentVersion)
val tgt = parseVersion(pkg.version)
if (tgt <= cur) {
return OtaVerdict.Rejected("版本不高于当前($currentVersion → ${pkg.version})", OtaVerdict.Stage.Version)
}
return OtaVerdict.Ok
}
/** `1.4.10` 这种必须按**数字段**比,不能按字符串比(`"1.4.10" < "1.4.9"` 是字符串序的陷阱)。 */
fun parseVersion(v: String): List<Int> =
v.split('.').map { it.filter(Char::isDigit).toIntOrNull() ?: 0 } +
List(maxOf(0, 4 - v.split('.').size)) { 0 }
private operator fun List<Int>.compareTo(other: List<Int>): Int {
val n = maxOf(size, other.size)
for (i in 0 until n) {
val a = getOrElse(i) { 0 }
val b = other.getOrElse(i) { 0 }
if (a != b) return a - b
}
return 0
}
}
/**
* 演示固件包工厂。
*
* 生成一份**真的能被验签**的包(ECDSA P-256 + SHA-256),
* 同时能生成三种"坏包"用来验证拒绝路径:
* - 改内容不改签名;
* - 用一个不同密钥签;
* - 版本号倒退。
*/
class FirmwareFactory {
val keys: KeyPair = KeyPairGenerator.getInstance("EC").apply { initialize(256) }.generateKeyPair()
fun make(
version: String,
target: String = "NF-Demo-01",
bytes: ByteArray = ByteArray(48 * 1024) { (it * 7 + 13).toByte() },
notes: String = "演示固件",
): OtaPackage {
val sha = Crypto.sha256Hex(bytes)
val signed = "$version|$target|$sha"
val sig = Signature.getInstance("SHA256withECDSA").run {
initSign(keys.private)
update(signed.toByteArray(Charsets.UTF_8))
sign()
}
return OtaPackage(
version = version,
target = target,
payloadB64 = Base64.getEncoder().encodeToString(bytes),
sha256 = sha,
sigB64 = Base64.getEncoder().encodeToString(sig),
notes = notes,
)
}
/** 坏包 1:改了内容(签名还是原来那份)。 */
fun tamperedContent(pkg: OtaPackage): OtaPackage {
val bytes = Base64.getDecoder().decode(pkg.payloadB64)
bytes[100] = (bytes[100] + 1).toByte()
return pkg.copy(payloadB64 = Base64.getEncoder().encodeToString(bytes))
}
/** 坏包 2:换个密钥签。 */
fun signedByOtherKey(pkg: OtaPackage): OtaPackage {
val other = KeyPairGenerator.getInstance("EC").apply { initialize(256) }.generateKeyPair()
val signed = "${pkg.version}|${pkg.target}|${pkg.sha256}"
val sig = Signature.getInstance("SHA256withECDSA").run {
initSign(other.private)
update(signed.toByteArray(Charsets.UTF_8))
sign()
}
return pkg.copy(sigB64 = Base64.getEncoder().encodeToString(sig))
}
/** 坏包 3:版本倒退。 */
fun downgrade(pkg: OtaPackage): OtaPackage = make(version = "0.9.0", target = pkg.target)
fun publicKeyDer(): ByteArray = keys.public.encoded
}