端侧AI推理的安全沙箱设计:模型校验、数据隔离与结果可信

端侧AI推理的安全沙箱设计:模型校验、数据隔离与结果可信

一、端侧AI的安全困境与沙箱必要性

端侧AI推理正在从概念走向大规模部署。
手机、IoT设备、车载系统都在承载模型推理任务。
然而,安全防护却远滞后于功能迭代。

传统的端侧安全策略围绕应用权限展开。
它假定运行环境是可信的。
这一假设在AI推理场景下已经不再成立。

攻击面至少有三个维度。
其一,模型文件可能被篡改或替换。
攻击者植入后门模型后,推理结果完全受控。
其二,输入数据在跨进程传递时可能被窃听。
图片、语音、传感器数据暴露在无保护的内存区域。
其三,推理结果可能被中间人篡改后返回。
调用方拿到伪造结果却无从察觉。

上述风险并非理论推演。
2023年已有安全团队演示针对TFLite模型的权重注入攻击。
2024年的Black Hat上,研究者公开了移动端推理管道的劫持方案。

沙箱设计因此成为刚需。
它的核心原则是:信任必须建立在可验证的基础上。
任何未经校验的输入、任何未签名的模型、任何缺乏证明的推理结果,
都不应被系统采纳。

下图展示了端侧AI推理的安全架构全貌。

graph TB subgraph 不可信区域 A[应用层调用方] B[传感器/摄像头<br/>原始输入数据] end subgraph 沙箱边界_Sandbox Boundary C[输入校验层<br/>格式校验+大小限制+特征提取] D[模型完整性校验<br/>HMAC签名+版本比对] E[TEE可信执行环境<br/>模型加载+推理计算+结果签名] F[输出验证层<br/>可信签名验证+异常模式检测] end subgraph 可信存储 G[(加密模型仓库<br/>签名清单+版本链)] H[(TEE密钥管理<br/>AK/SK+证书链)] end A -->|用户输入/API调用| C B -->|传感器数据流| C C -->|校验通过的数据| E D -->|完整性报告| E G -->|模型二进制+签名| D G -->|解密后的模型| E H -->|签名密钥| E E -->|推理结果+签名| F F -->|可信结果| A F -->|异常告警| I[安全审计日志]

架构的核心思想是把不可信组件挡在沙箱之外。
TEE是其中最关键的隔离锚点。
它提供了硬件级的内存加密和执行环境保护。
即使在Root权限下,TEE内部的数据仍然不可读。

但沙箱设计不是简单地加一层TEE。
它需要贯穿模型加载、输入处理、推理执行、结果输出的全链路。


二、模型文件的完整性校验

模型文件是推理管道的基石。
一旦被篡改,后续所有安全措施都形同虚设。

常见的攻击手法包括权重翻转攻击和结构注入攻击。
前者修改模型中特定层的权重值,导致特定输入触发恶意输出。
后者在模型图中插入隐蔽的子图,实现数据外泄。

校验方案需要覆盖三个方面。
加载前签名校验,防止未授权模型被读取。
运行时内存校验,防止加载后的代码段被改写。
版本回滚防护,阻止攻击者降级到有已知漏洞的旧模型。

下面是基于HMAC-SHA256的模型完整性校验实现。

#include <openssl/hmac.h> #include <fstream> #include <vector> #include <stdexcept> #include <cstring> class ModelIntegrityVerifier { public: struct VerifyResult { bool valid; std::string model_version; std::string signer_id; }; ModelIntegrityVerifier(const std::string& key_path) { std::ifstream kf(key_path, std::ios::binary); if (!kf) { throw std::runtime_error("Failed to load signing key"); } signing_key_.assign( std::istreambuf_iterator<char>(kf), std::istreambuf_iterator<char>() ); } VerifyResult verify(const std::string& model_path, const std::string& sig_path) { // 读取模型二进制 std::vector<uint8_t> model_data = load_file(model_path); if (model_data.empty()) { throw std::runtime_error("Model file is empty"); } // 读取签名数据 std::vector<uint8_t> sig_data = load_file(sig_path); if (sig_data.size() < 64) { throw std::runtime_error("Signature file truncated"); } // 提取HMAC签名(前32字节) std::vector<uint8_t> expected_hmac(sig_data.begin(), sig_data.begin() + 32); // 提取版本号(32:64 字节) std::string version(sig_data.begin() + 32, sig_data.begin() + 64); // 计算实际HMAC unsigned int hmac_len = 0; unsigned char computed[EVP_MAX_MD_SIZE]; HMAC(EVP_sha256(), signing_key_.data(), signing_key_.size(), model_data.data(), model_data.size(), computed, &hmac_len); // 常量时间比较,防时序攻击 bool hmac_match = (hmac_len == 32) && (CRYPTO_memcmp(computed, expected_hmac.data(), 32) == 0); // 版本回滚检查 if (hmac_match && !version.empty()) { hmac_match = check_version_rollback(version); } return {hmac_match, version, "primary_signer"}; } private: std::string signing_key_; std::string last_verified_version_; std::vector<uint8_t> load_file(const std::string& path) { std::ifstream f(path, std::ios::binary | std::ios::ate); if (!f) { throw std::runtime_error("Cannot open: " + path); } size_t sz = f.tellg(); f.seekg(0, std::ios::beg); std::vector<uint8_t> buf(sz); f.read(reinterpret_cast<char*>(buf.data()), sz); return buf; } bool check_version_rollback(const std::string& version) { if (last_verified_version_.empty()) { last_verified_version_ = version; return true; } // 简化的版本比较:禁止版本号递减 return version >= last_verified_version_; } };

几点实现细节值得注意。
CRYPTO_memcmp做常量时间比较,规避时序侧信道。
版本号嵌入签名文件后半段,与模型绑定。
密钥存储在TEE安全存储区,外部不可读。
所有文件读取都有异常路径处理,不留隐式假设。

仅靠HMAC不足以防御重放攻击。
实际部署时建议叠加nonce或时间戳。
这要求签名服务端与终端时钟保持同步。


三、输入数据的沙箱隔离

模型校验是静态防线,输入隔离则是运行时防线。
攻击者可能构造恶意输入来触发缓冲区溢出。
或者利用对抗样本绕过模型的安全检测。

输入沙箱的核心设计原则是:最小权限
推理进程只能访问显式声明的输入缓冲区。
不能读写文件系统,不能发起网络连接。
不能通过IPC与外部进程通信。

Linux上常用的隔离机制包括seccomp、namespace和cgroup。
Android环境下可以利用isolatedProcess标记。
但最严格的方案仍然是TEE。

以下是通过OPTEE Trusted Application实现的输入隔离框架。

// Trusted Application: 推理输入处理器 #include <tee_internal_api.h> #include <tee_internal_api_extensions.h> #define TA_INFERENCE_UUID \ { 0x8e4f2a1b, 0xd3c5, 0x47a9, \ { 0xb6, 0xe1, 0x9f, 0x7d, 0x2c, 0x8a, 0x3e, 0x15 } } #define INPUT_BUFFER_MAX (4 * 1024 * 1024) // 4MB #define OUTPUT_BUFFER_MAX (2 * 1024 * 1024) // 2MB TEE_Result TA_CreateEntryPoint(void) { IMSG("TA Create: inference sandbox initialized"); return TEE_SUCCESS; } TEE_Result TA_OpenSessionEntryPoint(uint32_t param_types, TEE_Param params[4], void **sess_ctx) { // 校验调用方身份 if (!verify_caller_identity()) { EMSG("Caller identity verification failed"); return TEE_ERROR_ACCESS_DENIED; } *sess_ctx = NULL; return TEE_SUCCESS; } static TEE_Result process_inference_input( TEE_Param *input, TEE_Param *output) { const uint8_t *in_data = TEE_MemMove( NULL, input->memref.buffer, input->memref.size); if (!in_data || input->memref.size > INPUT_BUFFER_MAX) { EMSG("Input buffer validation failed"); return TEE_ERROR_BAD_PARAMETERS; } // 在安全世界内执行格式校验 if (!validate_input_format(in_data, input->memref.size)) { EMSG("Input format rejected by sandbox"); return TEE_ERROR_SECURITY; } // 执行模型推理(安全世界内) TEE_Result rc = run_model_inference( in_data, input->memref.size, output->memref.buffer, &output->memref.size); if (rc != TEE_SUCCESS) { EMSG("Inference execution failed: 0x%x", rc); } return rc; } TEE_Result TA_InvokeCommandEntryPoint( void *sess_ctx, uint32_t cmd_id, uint32_t param_types, TEE_Param params[4]) { switch (cmd_id) { case CMD_INFERENCE_PROCESS: if (TEE_PARAM_TYPE_GET(param_types, 0) != TEE_PARAM_TYPE_MEMREF_INPUT || TEE_PARAM_TYPE_GET(param_types, 1) != TEE_PARAM_TYPE_MEMREF_OUTPUT) { return TEE_ERROR_BAD_PARAMETERS; } return process_inference_input(&params[0], &params[1]); case CMD_GET_SANDBOX_STATS: return report_sandbox_statistics(&params[0]); default: return TEE_ERROR_NOT_SUPPORTED; } } void TA_CloseSessionEntryPoint(void *sess_ctx) { // 清除会话级缓存,防止数据残留 wipe_session_buffers(); } void TA_DestroyEntryPoint(void) { IMSG("TA Destroy: sandbox resources released"); }

关键设计点如下。
输入缓冲区大小有硬限制(4MB),超出直接拒绝。
格式校验先于推理执行,拦截畸形数据。
会话关闭时强制清除缓存,杜绝数据泄漏。
所有错误路径都有明确的返回码和日志记录。

在Android端,调用方通过KeyStore API与TA交互。
World切换(Normal World ↔ Secure World)有一定开销。
典型延迟在微秒级,对推理吞吐影响可控。


四、推理结果的可信验证

输入过滤和模型校验确保了"输入可信"。
但推理结果的真实性同样需要证明。
调用方必须能验证:结果确实来自指定模型,且未经篡改。

这里引入**推理证明(Inference Attestation)**机制。
核心思路是TEE对推理结果进行签名。
签名覆盖模型标识、输入哈希、输出数据和时间戳。
调用方校验签名后即可确认结果来源。

签名方案有两种选择。
对称密钥方案用HMAC,性能高但需要密钥共享。
非对称方案用ECDSA/Ed25519,支持公开验证。
生产环境推荐Ed25519,签名速度远快于ECDSA。

#include <sodium.h> #include <vector> #include <string> #include <array> #include <cstring> class InferenceAttestator { public: static constexpr size_t PUBKEY_SIZE = crypto_sign_PUBLICKEYBYTES; static constexpr size_t SECKEY_SIZE = crypto_sign_SECRETKEYBYTES; static constexpr size_t SIG_SIZE = crypto_sign_BYTES; struct Attestation { std::array<uint8_t, SIG_SIZE> signature; std::string model_id; std::array<uint8_t, 32> input_hash; uint64_t timestamp; }; InferenceAttestator() { if (sodium_init() < 0) { throw std::runtime_error("libsodium init failed"); } pub_key_.resize(PUBKEY_SIZE); sec_key_.resize(SECKEY_SIZE); crypto_sign_keypair(pub_key_.data(), sec_key_.data()); } Attestation sign_result( const std::string& model_id, const std::vector<uint8_t>& input, const std::vector<uint8_t>& output) { // 计算输入哈希 std::array<uint8_t, 32> input_hash; crypto_hash_sha256(input_hash.data(), input.data(), input.size()); // 构建待签名消息 Attestation att; att.model_id = model_id; att.input_hash = input_hash; att.timestamp = get_secure_timestamp(); std::vector<uint8_t> message = build_message(att, output); // Ed25519签名 unsigned long long sig_actual_len = 0; if (crypto_sign_detached( att.signature.data(), &sig_actual_len, message.data(), message.size(), sec_key_.data()) != 0) { throw std::runtime_error("Signature computation failed"); } return att; } bool verify_attestation( const Attestation& att, const std::vector<uint8_t>& output, const std::vector<uint8_t>& pub_key) { if (pub_key.size() != PUBKEY_SIZE) { return false; } // 重放保护:时间戳偏差检查 uint64_t now = get_secure_timestamp(); if (att.timestamp > now || now - att.timestamp > MAX_TIMESTAMP_DRIFT_SEC) { return false; } std::vector<uint8_t> message = build_message(att, output); return crypto_sign_verify_detached( att.signature.data(), message.data(), message.size(), pub_key.data()) == 0; } private: std::vector<uint8_t> pub_key_; std::vector<uint8_t> sec_key_; static constexpr uint64_t MAX_TIMESTAMP_DRIFT_SEC = 300; uint64_t get_secure_timestamp() { // 从TEE安全时钟获取,而非系统时钟 // 简化实现,实际应调用TEE API return static_cast<uint64_t>(time(nullptr)); } std::vector<uint8_t> build_message( const Attestation& att, const std::vector<uint8_t>& output) { std::vector<uint8_t> msg; msg.insert(msg.end(), att.model_id.begin(), att.model_id.end()); msg.insert(msg.end(), att.input_hash.begin(), att.input_hash.end()); msg.insert(msg.end(), output.begin(), output.end()); uint64_t ts_be = htobe64(att.timestamp); uint8_t* ts_bytes = reinterpret_cast<uint8_t*>(&ts_be); msg.insert(msg.end(), ts_bytes, ts_bytes + sizeof(ts_be)); return msg; } };

验证流程包含三个检查点。
签名正确性用crypto_sign_verify_detached验证。
时间戳防重放,偏差超过5分钟拒绝。
调用方持有的公钥必须来自可信渠道,不能与签名一同传输。

生产环境中,公钥分发通常通过设备出厂预置或远程证明完成。
这与Android的Key Attestation机制配合使用。
设备完整性 + 推理可信性,构成双重担保。


五、总结

本文围绕端侧AI推理的安全沙箱,给出了从模型加载到结果签名的完整技术方案。

模型完整性校验是静态防线。
HMAC签名 + 版本回滚保护,确保加载的模型未被篡改。

输入数据沙箱隔离是运行时防线。
基于OPTEE的TA实现,输入仅存在于Secure World内。
格式校验优先于推理执行,杜绝缓冲区溢出。

推理结果可信验证是输出防线。
Ed25519签名覆盖输入哈希、模型ID和时间戳。
调用方可独立验证结果来源,无需信任传输通道。

三项措施各自解决一个信任问题。
组合后形成纵深防御,缺一不可。
落地时需权衡性能开销与安全等级。
对于毫秒级推理场景,World切换延迟在可接受范围。
对于超大模型(>1GB),密钥管理复杂度会上升。

安全是没有终点的增量过程。
今天的设计是明天的基线。
与各位同学共勉。