毕设17 - DID数字身份 Rust 实现
复工复工。这一个月补了区块链的底层原理,过了个年,做了点AI项目。
总之,少不了在 Cosmos 里实现数字身份的内容,先把这个做了。
这一步还是得用上 Wasm ……吗?用吧,不用没有能扯的创新点了……
为什么不直接把数字身份的内容作为 Cosmos 的模块呢?……
作为模块,代码直接运行在REE中了,必须要让这部分代码跑在TEE里。那能不能核心生成VC,VP的代码跑在TEE里,其他跑在REE里呢?差不多,这就约等于全跑在TEE里了。那既然跑在TEE里了,就写的通用一点,做成智能合约的结构。
0 分析
大体上还是之前的结构,有几个地方要大改动:
- 不需要冗余的proxy,直接cosmos和host连通。
- 信息传输方式格式
- json格式效率低、不安全
- 初步打算改用 MessagePack,天然更小的体积,更快的速度,二进制格式天然对抗注入攻击,支持动态schema
- 链上状态和智能合约(aot)全部加密,秘钥留在TEE里,有验证和解密程序。
- wasm代码要能够让cosmos提供新的数据。
- 重写 TA 和 wasm,全部改用 Rust。
重点在于:
- rust 编译成 wamr 可以运行的 wasm
- wasm智能合约内部调用获取私钥,解密、加密数据
- wasm智能合约和cosmos进行通信
1 WAMR & Rust
参考资料:https://anoopelias.github.io/posts/wasm-micro-runtime-with-rust/
根据参考资料,按如下步骤准备:
源代码:
#![no_std]
#![no_main]
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
extern "C" {
fn puts(s: *const i8) -> i32;
}
#[no_mangle]
fn main() {
let hello = "Hello Rust World!";
let hello_ptr: *const u8 = hello.as_ptr() as *const u8;
unsafe {
puts(hello_ptr);
}
}
编译指令:
rustc -C link-self-contained=no \
-C link-args=--no-entry \
-C link-args=-zstack-size=32768 \
--target wasm32-wasip1 main.rs
AoT:
wamrc --target=aarch64 --disable-simd -o main.aot main.wasm
然后进入 optee 中,用 optee_wamr 运行这个 aot 文件,但是报错:
failed to call unlinked import function (env, puts)
是因为之前我实现的 optee_wamr 不支持导入外部函数。
更新了 OPTEE-WAMR 增加了导入外部函数的功能,现在运行会在安全世界里输出:Hello Rust World!
2 Wasm智能合约访问链上数据
2.1 cgo 回调 go
需要实现将 go 函数传递给 c 调用。
一个例子:
package main
/*
// 定义C函数指针类型
typedef void (*Callback)(const char*);
extern void goCallbackProxy(char* msg);
// C端触发回调的函数
static void triggerCallback(Callback cb) {
cb("Hello from C!");
}
*/
import "C"
import (
"fmt"
"runtime"
)
//export goCallbackProxy
func goCallbackProxy(msg *C.char) {
fmt.Println("Go received:", C.GoString(msg))
}
func main() {
// 将Go回调函数转换为C函数指针
cb := C.Callback(C.goCallbackProxy)
// 调用C函数触发回调
C.triggerCallback(cb)
// 防止cb被提前回收
runtime.KeepAlive(cb)
}
2.2 TA主动触发CA行为
思路为使用共享内存空间作为TA和CA沟通的桥梁。具体实现如下:
分配一块共享内存空间:
struct shared_mem
{
char data[256]; // 数据区域
int ready; // 准备标志
};
将 CA 分为两个线程:
- 负责调用TA
- 负责轮询监听共享内存空间
TA 需要传递数据给 CA 时:
- 将数据写入 data
- 修改 ready
进一步考虑可能存在的问题,进行防御性变成:
- 防止编译器优化掉
ready的读写或重排顺序 - 确保编译器生成的代码顺序与源码一致
最终伪代码为:
// 共享内存定义
typedef struct {
uint8_t data[DATA_SIZE];
volatile int ready;
} SharedMem;
// TA侧写入
while (shared_mem.ready != 0);
memcpy(shared_mem.data, data, size);
__asm__ __volatile__("" ::: "memory"); // 编译器屏障,防止编译器提前修改ready
shared_mem.ready = 1;
// CA侧读取
while (shared_mem.ready != 1);
process_data(shared_mem.data);
__asm__ __volatile__("" ::: "memory"); // 编译器屏障
shared_mem.ready = 0;
2.3 设计CA TA交互接口
通过共享内存交换数据发送请求,需要设计请求的接口。
TA 向 CA 发送请求:
-
set 状态
{ "type": "set", "key": "key", "value": "value" } -
get 状态
{ "type": "get", "key": "key" }
CA 向 TA 返回数据:
-
set 返回成功与否
{ "status": true } -
get 返回对应的值
{ "status": true, // 表示整体操作成功 "value": "value" }
原来想采用 MessagePack 序列化数据,但是库里依赖网络接口 #include <arpa/inet.h> ,过于复杂且 TEE
环境不支持。另外,我这里序列化的需求很简单,不如自己实现序列化。代码很简单,这里不记录了。
3 优化智能合约
3.1 Rust-wasm的内存分配
遇到第一个问题,纯C中要分配内存直接调用 malloc 即可,编译出的wasm文件可以直接wamrc预编译。
但是Rust的内存管理方式不同,需要自定义内存分配器。但是,如果自定义了内存分配器就无法wamrc预编译。
下面进行一系列测试:
3.1.1 编译指令设置
build-c.sh:
/opt/wasi-sdk/bin/clang --target=wasm32-wasi -O3 -nostdlib \
-Wl,--no-entry -Wl,--export=test -o test-c.wasm test-c.c
./wamrc --target=aarch64 --disable-simd -o test-c.aot test-c.wasm
wasm2wat test-c.wasm > test-c.wat
build-rs.sh
rustc -C link-self-contained=no \
-C link-args=-zstack-size=32768 \
-C link-args=-O3 \
-C link-args=--no-entry \
--target wasm32-wasip1 test-rs.rs
./wamrc --target=aarch64 --disable-simd -o test-rs.aot test-rs.wasm
wasm2wat test-rs.wasm > test-rs.wat
3.1.2 简单加法
int test()
{
return 1 + 2;
}
#![no_std]
#![no_main]
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
loop {}
}
#[no_mangle]
pub fn test() -> i32 {
1 + 2
}
编译结果非常类似:
(module $test-c.wasm
(type (;0;) (func (result i32)))
(func $test (type 0) (result i32)
i32.const 3)
(memory (;0;) 2)
(global $__stack_pointer (mut i32) (i32.const 66560))
(export "memory" (memory 0))
(export "test" (func $test)))
(module $test-rs.wasm
(type (;0;) (func (param i32)))
(type (;1;) (func (result i32)))
(type (;2;) (func (param i32 i32)))
(func $rust_begin_unwind (type 0) (param i32)
loop ;; label = @1
br 0 (;@1;)
end)
(func $test (type 1) (result i32)
(local i32 i32 i32 i32 i32)
......//省略
call $_ZN4core9panicking11panic_const24panic_const_add_overflow17h183f3e35055c840eE
unreachable)
(func $_ZN4core9panicking9panic_fmt17hd65c069160825202E (type 2) (param i32 i32)
......//省略
call $rust_begin_unwind
unreachable)
(func $_ZN4core9panicking11panic_const24panic_const_add_overflow17h183f3e35055c840eE (type 0) (param i32)
......//省略
call $_ZN4core9panicking9panic_fmt17hd65c069160825202E
unreachable)
(table (;0;) 1 1 funcref)
(memory (;0;) 1)
(global $__stack_pointer (mut i32) (i32.const 32768))
(export "memory" (memory 0))
(export "test" (func $test))
(data $.rodata (i32.const 32768) "test-rs.rs\00\00\00\80\00\00\0a\00\00\00\0b\00\00\00\05\00\00\00attempt to add with overflow\1c\80\00\00\1c\00\00\00"))
可以看到 Rust 的编译结果复杂很多,包含了大量的溢出检查(overflow checking)相关代码。
如果想让 Rust 编译结果和 C 类似,可以如下修改:
#[no_mangle]
pub fn test() -> i32 {
1_i32.wrapping_add(2)
}
(module $test-rs.wasm
(type (;0;) (func (result i32)))
(func $test (type 0) (result i32)
(local i32)
i32.const 3
local.set 0
local.get 0
return)
(memory (;0;) 1)
(global $__stack_pointer (mut i32) (i32.const 32768))
(export "memory" (memory 0))
(export "test" (func $test)))
纯C默认不做运行时检查,假设程序员知道自己在做什么;Rust默认的简单运算也会有很多安全保证。
3.1.3 返回字符串
const char *test()
{
return "Hello, World!";
}
#![no_std]
#![no_main]
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
loop {}
}
#[no_mangle]
pub fn test() -> *const u8 {
"Hello, World!".as_ptr()
}
(module $test-c.wasm
(type (;0;) (func (result i32)))
(func $test (type 0) (result i32)
i32.const 1024)
(memory (;0;) 2)
(global $__stack_pointer (mut i32) (i32.const 66576))
(export "memory" (memory 0))
(export "test" (func $test))
(data $.rodata (i32.const 1024) "Hello, World!\00"))
(module $test-rs.wasm
(type (;0;) (func (result i32)))
(func $test (type 0) (result i32)
(local i32)
i32.const 32768
local.set 0
local.get 0
return)
(memory (;0;) 1)
(global $__stack_pointer (mut i32) (i32.const 32768))
(export "memory" (memory 0))
(export "test" (func $test))
(data $.rodata (i32.const 32768) "Hello, World!"))
有个有意思的地方,Rust的字符串不会带尾零。侧面说明了 Rust 的一个习惯:传指针之后要传数组长度。纯C经常直接传指针,靠尾零区分的。
这里的 “Hello World!” 作为一个字符串常量,存储在栈空间中。
直接运行 wasm ,结果为:
# ./iwasm -f test test-c.wasm
0x400:i32
# ./iwasm -f test test-rs.wasm
0x8000:i32
返回的是一个指针地址。wasm 中的 (export "memory" (memory 0)) 暴露了内存空间,可以在这个内存空间的对应位置访问到字符串的值。
wamr中,export 的 memory 会映射在外部的内存空间中,通过 wasm_runtime_addr_app_to_native 函数将 wasm 内部的地址转换为外部的地址。
3.1.4 多内存提案
#![no_std]
#![no_main]
#![allow(static_mut_refs)]
use core::alloc::{GlobalAlloc, Layout};
use core::sync::atomic::{AtomicUsize, Ordering};
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
loop {}
}
const HEAP_SIZE: usize = 65536;
#[no_mangle]
static mut HEAP: [u8; HEAP_SIZE] = [0; HEAP_SIZE];
static HEAP_PTR: AtomicUsize = AtomicUsize::new(0);
struct SimpleAllocator;
unsafe impl GlobalAlloc for SimpleAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let offset = HEAP_PTR.fetch_add(layout.size(), Ordering::SeqCst);
if offset + layout.size() > HEAP_SIZE {
return core::ptr::null_mut();
}
HEAP.as_mut_ptr().add(offset)
}
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
// 这个简单实现不支持内存释放
}
}
#[global_allocator]
static ALLOCATOR: SimpleAllocator = SimpleAllocator;
extern crate alloc;
use alloc::string::String;
#[no_mangle]
pub fn test() -> *const u8 {
let mut buf = String::new();
buf.push_str("Hello, World!");
buf.as_ptr()
}
实验发现,使用 alloc 里的其他库貌似没有问题,唯独会用 String::new() 会让 wamrc 编译不通过。
直接运行 wasm ,报错:
[04:30:07:055 - 75C5E7216040]: warning: failed to link import function (env, memset)
[04:30:07:055 - 75C5E7216040]: warning: failed to link import function (env, memcpy)
[04:30:07:055 - 75C5E7216040]: warning: failed to link import function (env, memmove)
Error: ExecutionError(ExecError { message: "Exception: failed to call unlinked import function (env, memcpy)", exit_code: 0 })
实现了外部接口后,报错:
Error: ExecutionError(ExecError { message: "Exception: wasm operand stack overflow", exit_code: 0 })
直接用 iwasm 或 wamrc 报错:
WASM module load failed: zero byte expected
检查 iwasm 源码,发现是因为编译时未启用 WASM_ENABLE_MULTI_MEMORY 选项(多内存提案)
多内存提案(Multi-Memory Proposal)是 WebAssembly 的一个扩展特性,简单说就是让一个WASM模块能同时拥有多个独立的内存块。这就像给你的程序开了多个独立仓库,每个仓库可以存不同类型的东西,互不干扰。
传统模式 vs 多内存模式
传统单内存 多内存模式 内存数量 只能有1块内存(memory 0) 可声明多个内存(memory 0,1,2…) 内存操作 所有指令默认操作memory 0 指令需指定操作哪个内存(如 memory.load 1) 应用场景 简单场景 复杂模块化/沙盒/多线程场景
为什么需要这个功能?
模块化开发
不同库可以使用独立内存,避免内存污染(比如一个加密库和一个图形库各自用独立内存)安全隔离
高风险操作(如解析不可信数据)可放在独立内存,崩溃时不影响主内存并行优化
多线程中不同线程可绑定不同内存(需配合线程提案使用)内存复用
可创建专用内存(比如单独给GC分配一块内存)
实际代码示例
;; 声明两个内存 (module (memory $mem1 1) ;; 第一个内存初始1页(64KB) (memory $mem2 2) ;; 第二个内存初始2页 ;; 在第二个内存写入数据 (data (memory $mem2) (i32.const 0) "hello") ;; 从第二个内存加载数据 (func $get_hello (i32.load $mem2 (i32.const 0)) ) )
当前状态
- 2023年已成为官方标准(Phase 4)
- 需要运行时环境支持(如Wasmtime需开启
--enable-multi-memory)- 主流浏览器逐步支持中(Chrome 119+已实现)
所以解决方案有两个:
- 重新编译 iwasm 和 wamrc,使其支持 multi-memory
- 禁用 Rust 编译的时候启用多内存提案
还是重新编译 iwasm 和 wamrc 。
在 wasm-micro-runtime/product-mini/platforms/linux/CMakeLists.txt 的最后添加:
# 为所有目标添加预处理器宏
add_definitions(-DWASM_ENABLE_MULTI_MEMORY)
# 或者为特定目标添加预处理器宏
target_compile_definitions(iwasm PRIVATE WASM_ENABLE_MULTI_MEMORY)
重新编译得到的 iwasm 不会报错 zero byte expected,但是和直接用 wasm-sdk 运行一样报错:
wasm operand stack overflow
有可能是因为自定义实现的内存分配器有问题。改成使用 wee_alloc ,可以了。
3.2 移植 wamr
3.2.1 阅读文档重写代码
https://wamr.gitbook.io/document/wamr-in-practice/advance-tutorial/port_wamr
这部分略,修改后的代码之后开源。
主要思路就是实现新增的接口,根据报错信息(undefined reference to XXXX)实现新的接口。
3.2.2 简单的测试
最简单的wasm没有问题,不会报错。
但是当运行复杂一点程序时,TA 中报错:
D/TA: os_mmap:111 os_mmap(addr=0x40049000, size=144412, aligned size=147456, prot=0x7) memory allocated.
D/TA: os_mprotect:171 os_mprotect(addr=0x40049000, size=147456, prot=7) OK.
D/TA: os_mmap:123 os_mmap(addr=0x40049000, size=144412, aligned size=147456, prot=0x7) protection set.
D/TA: os_mmap:111 os_mmap(addr=0x4006d000, size=8192, aligned size=8192, prot=0x3) memory allocated.
D/TA: os_mprotect:171 os_mprotect(addr=0x4006d000, size=8192, prot=3) OK.
D/TA: os_mmap:123 os_mmap(addr=0x4006d000, size=8192, aligned size=8192, prot=0x3) protection set.
D/TC:? 0 tee_ta_invoke_command:798 Error: ffff000c of 4
E/TA: tee_map_zi:56 Invoke PTA_SYSTEM_MAP_ZI: res=0xffff000c
I/TA: os_mmap(size=155648, aligned size=155648, prot=0x3) failed.
0xffff000c 是 TEE_ERROR_OUT_OF_MEMORY 内存不足。
估计是最多只能分配256KB。
先重新试试之前的 wamr 看看内存分配多少。
D/TA: os_mmap:103 os_mmap(addr=0x40049000, size=141040, aligned size=143360, prot=0x7) memory allocated.
D/TA: os_mprotect:156 os_mprotect(addr=0x40049000, size=143360, prot=7) OK.
D/TA: os_mmap:114 os_mmap(addr=0x40049000, size=141040, aligned size=143360, prot=0x7) protection set.
D/TA: os_mmap:103 os_mmap(addr=0x4006c000, size=2306, aligned size=4096, prot=0x3) memory allocated.
D/TA: os_mprotect:156 os_mprotect(addr=0x4006c000, size=4096, prot=3) OK.
D/TA: os_mmap:114 os_mmap(addr=0x4006c000, size=2306, aligned size=4096, prot=0x3) protection set.
D/TA: os_mprotect:156 os_mprotect(addr=0x40049000, size=141040, prot=5) OK.
D/TA: os_mprotect:156 os_mprotect(addr=0x4006c000, size=2306, prot=1) OK.
E/TA: aot_instantiate:982 check heap size is OK.
E/TA: aot_instantiate:1000 calculate size of table data is OK.
E/TA: aot_instantiate:1007 Allocate module instance, global data, table data and heap data is OK.
E/TA: aot_instantiate:1019 Initialize global info is OK.
E/TA: aot_instantiate:1030 Initialize table info is OK.
E/TA: memories_instantiate:647 info: runtime_malloc is OK
E/TA: memories_instantiate:651 debug: 0
E/TA: memory_instantiate:361 info: memory_instantiate
E/TA: memory_instantiate:473 info: 1
E/TA: memory_instantiate:480 num_bytes_per_page=65536, init_page_count=3, max_page_count=16385, heap_offset=131072, heap_size=64512
E/TA: memory_instantiate:485 total_size=196608
总共分配的内存不到 200 KB。
发现是 TA 的 TA_InitializeWamrRuntime 里写死了 stack_size = 256 * 1024 。。。
归根结底是不了解 WAMR 的内存模型。
分P吧。
其他碎碎念
cosmos首次启动,调用TA生成RSA密钥对,私钥直接加密存储在TEE里,公钥返回在链上保存。
其他节点加入区块链,与种子节点建立TLS连接,将私钥加密传输。
自始至终,外界无法得知私钥。
如果外界写一个新的TA用户获取加密存储的数据,那只用知道私钥存放的id,不就可以获取私钥了吗?并不会,因为OPTEE的Secure Storage API是按照TA的UUID进行隔离存储的,别的恶意TA无法访问秘钥生成TA的安全空间。
所以应该只有一个TA,包含了生成私钥、执行智能合约等全部功能。