毕设17 - DID数字身份 Rust 实现

| 笔记 | 11651 | 30分钟 | 毕设区块链数字身份

复工复工。这一个月补了区块链的底层原理,过了个年,做了点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。

重点在于:

  1. rust 编译成 wamr 可以运行的 wasm
  2. wasm智能合约内部调用获取私钥,解密、加密数据
  3. 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 分为两个线程:

  1. 负责调用TA
  2. 负责轮询监听共享内存空间

TA 需要传递数据给 CA 时:

  1. 将数据写入 data
  2. 修改 ready

进一步考虑可能存在的问题,进行防御性变成:

  1. 防止编译器优化掉 ready 的读写或重排顺序
  2. 确保编译器生成的代码顺序与源码一致

最终伪代码为:

// 共享内存定义
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 发送请求:

  1. set 状态

    {
      "type": "set",
      "key": "key",
      "value": "value"
    }
  2. get 状态

    {
      "type": "get",
      "key": "key"
    }

CA 向 TA 返回数据:

  1. set 返回成功与否

    {
      "status": true
    }
  2. 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)
应用场景简单场景复杂模块化/沙盒/多线程场景

为什么需要这个功能?

  1. 模块化开发
    不同库可以使用独立内存,避免内存污染(比如一个加密库和一个图形库各自用独立内存)

  2. 安全隔离
    高风险操作(如解析不可信数据)可放在独立内存,崩溃时不影响主内存

  3. 并行优化
    多线程中不同线程可绑定不同内存(需配合线程提案使用)

  4. 内存复用
    可创建专用内存(比如单独给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.

0xffff000cTEE_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,包含了生成私钥、执行智能合约等全部功能。