第一章:Go二进制中TLS证书硬编码风险的本质与危害
TLS证书硬编码是指将CA根证书、客户端证书或私钥等敏感凭证以明文或Base64编码形式直接嵌入Go源码(如var certPEM = "-----BEGIN CERTIFICATE..."),并在编译时静态链接进最终二进制文件。这种做法看似简化了部署,实则彻底破坏了TLS信任链的动态性与可维护性。
证书不可更新性导致的信任失效
Go二进制一旦分发,内嵌证书即固化为只读字节序列。当证书过期、被吊销或CA变更(如Let’s Encrypt更换交叉签名根)时,用户必须重新编译并全量分发新版本——无法通过配置热更新或远程证书轮换修复。例如:
// ❌ 危险示例:硬编码根证书(截断示意)
const caCert = `-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAN...
-----END CERTIFICATE-----`
func init() {
pool := x509.NewCertPool()
pool.AppendCertsFromPEM([]byte(caCert)) // 静态加载,无法运行时替换
}
逆向工程可直接提取全部密钥材料
Go二进制未加壳时,strings命令即可定位PEM块,openssl x509 -in <extracted> -text 可解析公钥信息;若私钥被硬编码(如tls.X509KeyPair([]byte(cert), []byte(key))),攻击者可通过objdump -s -j .rodata ./binary | grep -A 20 '-----BEGIN'快速提取完整密钥对。
安全实践对照表
| 风险项 | 硬编码方式 | 推荐替代方案 |
|---|---|---|
| 根证书管理 | 编译时固化 | 运行时读取系统证书库(crypto/tls 默认行为)或挂载ConfigMap |
| 客户端身份认证 | 内联[]byte{...}私钥 |
使用os.ReadFile()加载外部证书文件,并校验文件权限(0400) |
| 证书吊销响应 | 依赖版本升级 | 集成OCSP Stapling或定期HTTP拉取CRL |
避免硬编码的最小可行改造:删除所有const certPEM声明,改用ioutil.ReadFile("/etc/tls/ca.crt")(Go 1.16+ 推荐os.ReadFile),并在启动时验证文件存在性与权限,缺失则panic退出——强制证书与二进制解耦。
第二章:Go二进制文件结构与PEM证书嵌入机制剖析
2.1 Go编译产物中静态数据段的布局原理与strings可提取性验证
Go 程序在 go build -ldflags="-s -w" 后,.rodata 段仍保留未加密的字符串字面量(如 fmt.Println("hello") 中的 "hello"),因其被直接嵌入只读数据段。
静态字符串存储位置
- 编译器将字符串字面量统一归入
.rodata(ELF)或__TEXT,__const(Mach-O) - 每个字符串以 null 结尾,地址对齐至 8 字节边界
strings 命令可提取性验证
# 提取所有可打印 ASCII 字符串(≥4 字节)
strings -n 4 ./main | grep -E '^hello|admin|token'
该命令基于字节扫描,不解析 ELF 结构,故能命中
.rodata中明文字符串。-n 4避免噪声短串;默认编码为 UTF-8/ASCII,不覆盖宽字符。
| 工具 | 是否解析段结构 | 能否识别 Go string header | 输出是否含偏移 |
|---|---|---|---|
strings |
❌ | ❌ | ❌ |
readelf -x .rodata |
✅ | ❌ | ✅ |
objdump -s -j .rodata |
✅ | ❌ | ✅ |
graph TD
A[Go源码中的string literal] --> B[编译器生成runtime.string结构体]
B --> C[首字段:指针→.rodata中实际字节数组]
C --> D[数组内容为明文UTF-8+\\0]
D --> E[strings工具线性扫描命中]
2.2 PEM边界标记(—–BEGIN CERTIFICATE—–)在ELF/Mach-O中的内存对齐与字符串驻留实践
PEM边界标记虽为纯文本,但在二进制格式中需兼顾加载器语义与内存布局约束。
字符串驻留策略对比
| 格式 | 段名 | 对齐要求 | 是否常量段 |
|---|---|---|---|
| ELF | .rodata |
8-byte | ✅ |
| Mach-O | __TEXT,__const |
16-byte | ✅ |
内存对齐示例(x86-64 ELF)
.section .rodata, "a", @progbits
.align 8
pem_begin_cert:
.ascii "-----BEGIN CERTIFICATE-----\n"
.byte 0
.align 8 确保后续符号地址满足SHT_PROGBITS最小对齐粒度;.byte 0 显式终止,避免与相邻字符串发生跨段合并(如链接器 --gc-sections 启用时)。
驻留优化流程
graph TD
A[源码中定义PEM字符串] --> B[编译器归入.rodata]
B --> C{链接器检查对齐}
C -->|不足8字节| D[填充NOP/零字节]
C -->|已对齐| E[直接映射至只读页]
D --> E
关键参数:-z relro 启用RELRO后,该段将被mprotect为 PROT_READ,防止运行时篡改。
2.3 TLS证书硬编码在Go build -ldflags -X场景下的符号注入路径复现实验
场景还原:利用-ldflags -X注入证书内容
Go 编译时可通过 -X 将字符串值注入 var 变量,常用于版本、配置注入。但若误将 PEM 格式证书(含换行)直接传入,会因 \n 被截断导致 TLS 握手失败。
go build -ldflags "-X 'main.TLSCertPEM=-----BEGIN CERTIFICATE-----\nMIIB...'" main.go
逻辑分析:
-X仅支持单行字符串;\n在 shell 解析中不被保留,实际注入为字面量\n字符而非换行,使 PEM 结构损坏。参数说明:-X importpath.name=value中value不支持转义换行,需预处理。
安全加固路径
- ✅ 使用 Base64 编码 PEM 后注入,运行时解码
- ❌ 禁止直接拼接多行 PEM 到
-X
| 方法 | 是否保留换行 | 运行时可用性 | 安全风险 |
|---|---|---|---|
| 原生 PEM 注入 | 否(被截断) | 失败 | 高 |
| Base64 编码 | 是(解码后还原) | 正常 | 低 |
注入流程可视化
graph TD
A[原始 PEM 证书] --> B[Base64 编码]
B --> C[go build -ldflags -X 'main.CertB64=...']
C --> D[程序启动时 base64.StdDecode]
D --> E[TLS Config 加载有效证书]
2.4 使用objdump + readelf定位.rodata节中证书PEM块的十六进制取证流程
PEM证书在二进制中的典型特征
.rodata节常存放只读常量数据,OpenSSL加载的证书PEM块以-----BEGIN CERTIFICATE-----开头,以-----END CERTIFICATE-----结尾,Base64编码后字节流具有高熵但ASCII可读边界。
快速定位.rodata节范围
readelf -S firmware.elf | grep "\.rodata"
# 输出示例:[14] .rodata PROGBITS 0000000000405000 00005000 00001a30 00 WA 0 0 8
readelf -S解析节头表;PROGBITS标识含初始化数据,WA标志表示可写+可读(实际只读),00005000为文件偏移,00001a30为长度。
提取并扫描十六进制内容
objdump -s -j .rodata firmware.elf | grep -A 20 -B 5 "BEGIN CERTIFICATE"
objdump -s以十六进制+ASCII双列导出指定节;-j .rodata限定范围,避免全量扫描;grep -A/-B捕获上下文便于验证PEM完整性。
关键取证字段对照表
| 字段 | 十六进制值(ASCII) | 说明 |
|---|---|---|
-----BEGIN |
2d 2d 2d 2d 2d 42 | 连续5个’-‘ + ‘B’ |
CERTIFICATE |
43 45 52 54 49 46 49 43 41 54 45 | 大写ASCII编码 |
-----END |
2d 2d 2d 2d 2d 45 | 同上逻辑 |
二进制取证流程
graph TD
A[readelf -S 获取.rodata偏移/长度] --> B[objdump -s -j .rodata 导出原始字节]
B --> C[grep匹配PEM ASCII标记]
C --> D[定位起始地址与长度]
D --> E[hexdump -C -s OFFSET -n LENGTH 提取原始PEM]
2.5 基于go tool compile -S反汇编分析证书字面量如何转化为只读数据引用
Go 编译器将 const 或包级 var 字面量(如 PEM 格式证书字符串)自动归入只读数据段(.rodata),而非可写 .data 段。
反汇编观察
go tool compile -S main.go | grep -A5 "const cert"
输出中可见类似:
"".cert SRODATA dupok size=1248
0x0000 00000 (main.go:5) PCDATA $2, $0
0x0000 00000 (main.go:5) PCDATA $0, $0
0x0000 00000 (main.go:5) TEXT "".cert(SB), RODATA|NOPTR, $0-0
SRODATA表示静态只读数据;NOPTR表明该数据不含指针,允许 GC 忽略扫描;dupok允许链接器合并重复字面量。
内存布局关键标识
| 属性 | 含义 |
|---|---|
RODATA |
只读数据段,加载后不可修改 |
NOPTR |
无指针字段,避免 GC 扫描开销 |
dupok |
支持字面量去重,减小二进制体积 |
数据引用机制
const certPEM = `-----BEGIN CERTIFICATE-----
MIIB...`
var _ = []byte(certPEM) // 触发地址取用
当 certPEM 被取地址(如 &certPEM[0])或转为 []byte 时,编译器生成 LEAQ 指令直接引用 .rodata 中的起始地址,不经过运行时分配。
graph TD
A[源码中的字符串字面量] --> B[编译期静态分析]
B --> C[归入 .rodata 段 + NOPTR 标记]
C --> D[生成 LEAQ 指令直接寻址]
D --> E[运行时零拷贝只读访问]
第三章:典型攻击面与真实漏洞案例还原
3.1 某IoT网关固件中硬编码CA证书导致中间人攻击的渗透复现
固件解包与证书定位
使用 binwalk -e firmware.bin 提取文件系统,进入 /etc/ssl/certs/ 目录发现 ca-root.crt —— 一个2048位RSA自签名CA证书,被硬编码于TLS客户端验证逻辑中。
证书滥用验证
# 构造恶意中间人服务器(使用该CA私钥签发网关域名证书)
openssl req -x509 -newkey rsa:2048 -keyout ca.key -out ca.crt -days 3650 -nodes -subj "/CN=IoT-Gateway-CA"
openssl req -newkey rsa:2048 -keyout server.key -out server.csr -nodes -subj "/CN=gateway.local"
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365
此流程复现攻击者利用泄露的CA私钥签发任意合法域名证书的能力。
-CAcreateserial自动生成序列号文件,-nodes跳过密钥加密,便于自动化中间人代理部署。
信任链绕过路径
| 组件 | 状态 | 风险等级 |
|---|---|---|
| 客户端证书验证 | 强制校验CA | 高 |
| CA证书来源 | 固件只读文件 | 中 |
| 私钥保护 | 未加密存储于开发镜像 | 极高 |
MITM流量劫持流程
graph TD
A[攻击者控制局域网] --> B[ARP欺骗重定向网关HTTPS流量]
B --> C[mitmproxy加载ca-root.crt私钥签发证书]
C --> D[网关TLS握手接受伪造证书]
D --> E[明文HTTP流量解密与篡改]
3.2 Kubernetes Operator二进制泄露私钥PEM边界的CI/CD流水线审计实践
Operator构建镜像时若将secrets/目录误打包,私钥PEM文件(如tls.key)可能以明文嵌入二进制。需在CI阶段拦截此类敏感边界。
审计关键检查点
- 构建上下文是否包含
.gitignore未覆盖的*.pem/*.key文件 Dockerfile中是否存在COPY . /app等宽泛拷贝指令- CI日志是否输出
writing key to /tmp/...类可疑路径
PEM边界检测脚本(GitLab CI before_script)
# 扫描工作区所有PEM格式私钥(OpenSSL标准边界)
find . -type f \( -name "*.key" -o -name "*.pem" \) -exec \
sh -c 'grep -q "-----BEGIN RSA PRIVATE KEY-----" "$1" && echo "ALERT: $1 contains private key!" && exit 1 || true' _ {} \;
逻辑说明:
grep -q静默匹配OpenSSL私钥PEM头;-exec ... {} \;对每个匹配文件执行检测;exit 1触发CI失败,阻断流水线。参数sh -c避免find -exec grep无法传递变量。
| 检查项 | 合规值 | 风险示例 |
|---|---|---|
Dockerfile COPY范围 |
COPY config/ app/ |
COPY . /app(含secret) |
| CI环境变量屏蔽 | GIT_STRATEGY: none |
GIT_STRATEGY: clone(拉取完整历史) |
graph TD
A[CI Job Start] --> B{扫描工作区PEM文件}
B -->|发现私钥| C[立即fail并告警]
B -->|无匹配| D[继续构建]
D --> E[静态分析Dockerfile COPY指令]
E --> F[验证镜像层是否含/secret/]
3.3 Go微服务容器镜像中strings命令一键提取证书链的红队评估报告
在无调试工具的受限容器环境中,strings 成为轻量级证书链提取的关键入口。
提取原理与限制
Go 编译的二进制默认静态链接,TLS 证书 PEM 数据常以明文嵌入 .rodata 段。但需规避噪声干扰:
- 仅匹配
-----BEGIN CERTIFICATE-----到-----END CERTIFICATE-----完整块 - 过滤长度
一键提取命令
# 从容器内运行的Go二进制中提取完整证书链
strings /app/service | grep -A 20 "BEGIN CERTIFICATE" | \
awk '/BEGIN CERTIFICATE/{f=1; buf=$0; next} /END CERTIFICATE/{f=0; print buf "\n" $0; next} f{buf = buf "\n" $0}' | \
grep -E "(BEGIN|END CERTIFICATE|^[A-Za-z0-9+/]*={0,2}$)" | \
sed '/^$/d' > chain.pem
逻辑说明:
strings提取所有可读字符串;grep -A 20捕获 BEGIN 后最多20行以覆盖单证书(典型约16–18行);awk精确拼接跨行 PEM 块;sed '/^$/d'清除空行确保 PEM 格式合规。
典型输出结构
| 字段 | 值 |
|---|---|
| 证书数量 | 3(根→中间→终端) |
| 最长证书长度 | 1724 字节 |
| Base64 行宽一致性 | 符合 RFC 7468(64字符/行) |
graph TD
A[strings /app/service] --> B[过滤含 BEGIN CERTIFICATE 行]
B --> C[按 PEM 边界聚合成完整块]
C --> D[校验 Base64 行格式]
D --> E[输出 chain.pem]
第四章:防御体系构建与工程化缓解策略
4.1 使用Go 1.19+ embed包实现证书资源安全加载与运行时解密实践
传统硬编码或文件路径加载 TLS 证书易暴露敏感信息,且破坏不可变镜像原则。Go 1.19 引入的 embed.FS 提供编译期资源固化能力,结合运行时 AES-GCM 解密,可实现零明文证书分发。
嵌入加密证书资源
import "embed"
//go:embed certs/*.enc
var certFS embed.FS
certs/*.enc 表示仅嵌入 .enc 后缀的加密证书文件(如 server.crt.enc),避免误嵌明文;embed.FS 在编译时将文件内容以只读字节流形式打包进二进制,杜绝运行时文件系统依赖。
运行时解密流程
func loadCert(name string) (tls.Certificate, error) {
data, _ := certFS.ReadFile("certs/" + name)
plain, _ := aesgcm.Decrypt(key, nonce, data, nil)
return tls.X509KeyPair(plain, plain) // 简化示意
}
aesgcm.Decrypt 使用预置密钥与非重复 nonce 解密,确保前向安全性;tls.X509KeyPair 接收 PEM 格式私钥与证书合并体(实际需分离解析)。
| 阶段 | 安全优势 |
|---|---|
| 编译嵌入 | 资源与代码强绑定,无外部 IO |
| 运行时解密 | 内存中仅存在解密后短暂明文 |
| 密钥隔离 | key/nonce 来自 KMS 或环境变量 |
graph TD
A[编译期] -->|embed.FS 打包 .enc 文件| B[二进制内只存密文]
B --> C[运行时读取密文]
C --> D[AES-GCM 解密]
D --> E[内存中构造 tls.Certificate]
E --> F[立即用于 ListenAndServeTLS]
4.2 构建自定义build tag与init函数拦截机制,动态注入证书而非硬编码
Go 的 build tag 与 init() 函数协同可实现编译期条件注入,规避证书硬编码风险。
证书注入原理
利用 //go:build cert_embed 标签隔离敏感逻辑,配合 init() 在包加载时动态注册证书源:
//go:build cert_embed
package tls
import "crypto/tls"
func init() {
// 从环境变量或文件系统加载 PEM 数据(非硬编码)
certBytes := loadCertFromEnv() // 支持 K8s Secret 挂载路径
keyBytes := loadKeyFromEnv()
cert, _ := tls.X509KeyPair(certBytes, keyBytes)
defaultCert = &cert // 注入全局 TLS 配置
}
逻辑分析:
loadCertFromEnv()读取CERT_PATH环境变量指定路径的 PEM 文件;defaultCert为预声明的*tls.Certificate全局变量,供http.Server.TLSConfig直接复用。
构建流程控制
| 场景 | 构建命令 | 注入效果 |
|---|---|---|
| 开发环境(无证书) | go build -tags 'dev' . |
跳过 cert_embed 块 |
| 生产部署 | go build -tags 'cert_embed' . |
触发 init() 加载证书 |
graph TD
A[go build -tags cert_embed] --> B{build tag 匹配?}
B -->|是| C[编译 cert_embed.go]
B -->|否| D[跳过该文件]
C --> E[链接时执行 init]
E --> F[动态注册证书到 TLS 配置]
4.3 集成gosec与custom linter检测硬编码PEM模式的CI阶段自动化门禁
为什么需要双重检测
硬编码 PEM(如 -----BEGIN RSA PRIVATE KEY-----)是高危安全反模式。gosec 擅长识别标准密钥字面量,但易漏掉 Base64 变形或注释包裹场景;自定义 linter(如基于 go/ast 的规则)可精准匹配 PEM 边界与上下文语义。
检测规则协同设计
gosec启用-exclude=G101(默认已含 PEM 检测)并增强正则:-config=gosec.yaml- 自定义 linter 使用
golang.org/x/tools/go/analysis实现pemHardcoded分析器,扫描*ast.BasicLit中匹配(?s)-----BEGIN\s+(?:RSA|EC|DSA)\s+PRIVATE\s+KEY-----.*?-----END的字符串字面量
CI 门禁脚本示例
# .github/workflows/security.yml
- name: Run security scanners
run: |
# 并行执行,任一失败即中断
gosec -fmt=json -out=gosec-report.json ./... &
go run ./cmd/pem-linter ./... > pem-report.txt 2>&1 &
wait
# 合并判定逻辑
if [ -s pem-report.txt ] || jq -e '.Issues | length > 0' gosec-report.json >/dev/null; then
echo "❌ Hardcoded PEM detected"; exit 1
fi
逻辑分析:
gosec输出 JSON 便于结构化解析;pem-linter直接输出文本行便于grep -q快速判空;wait确保双进程完成后再合并结果。-out和重定向避免竞态。
检测能力对比
| 工具 | PEM 变形检测 | 注释内 PEM | 上下文敏感 |
|---|---|---|---|
| gosec | ✅ 基础 | ❌ | ❌ |
| custom linter | ✅ 正则增强 | ✅ | ✅(函数名/变量名) |
graph TD
A[Go源码] --> B[gosec扫描]
A --> C[custom linter扫描]
B --> D{发现PEM?}
C --> E{发现PEM?}
D -->|是| F[CI失败]
E -->|是| F
D -->|否| G[继续]
E -->|否| G
4.4 基于BPF eBPF探针监控进程内存扫描行为,实时阻断strings类工具取证尝试
核心监控思路
利用 kprobe 拦截 mmap()、process_vm_readv() 及 ptrace(PTRACE_ATTACH) 等高风险系统调用,结合用户态符号解析识别 strings、xxd、volatility 等取证工具进程名。
关键eBPF检测逻辑(简化版)
// bpf_prog.c:在 mmap() 入口处检查调用者是否为可疑工具
SEC("kprobe/do_mmap")
int BPF_KPROBE(do_mmap_entry, struct file *file, unsigned long addr,
unsigned long len, unsigned long prot, unsigned long flags) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
char comm[TASK_COMM_LEN];
bpf_get_current_comm(&comm, sizeof(comm));
if (bpf_strncmp(comm, sizeof(comm), "strings", 7) == 0 &&
(prot & PROT_READ) && len > 0x10000) { // 扫描大内存页
bpf_override_return(ctx, -EPERM); // 立即拒绝映射
}
return 0;
}
逻辑分析:该探针在内核态拦截
do_mmap,通过bpf_get_current_comm()获取进程名,匹配"strings"字符串;若同时满足PROT_READ权限与大内存申请(>64KB),则调用bpf_override_return()强制返回-EPERM,使工具无法构建可读内存视图。
阻断效果对比
| 工具 | 传统审计方式 | eBPF实时阻断 |
|---|---|---|
strings /proc/1234/mem |
日志告警,已泄露数据 | 系统调用级拒绝,零字节输出 |
gdb -p 1234 + dump memory |
无法拦截 ptrace 附加 | PTRACE_ATTACH 被 kprobe 拦截并覆写返回值 |
部署流程(简略)
- 编译 eBPF 程序 → 加载至内核 → 关联
kprobe到目标函数 → 用户态守护进程接收 perf event 并记录告警。
第五章:超越strings——构建面向零信任的二进制可信供应链
在2023年SolarWinds事件余波未平之际,CNCF Sig-Reliability团队对127个主流开源项目的CI/CD流水线进行逆向审计,发现89%的项目在发布阶段仍依赖未签名的二进制分发包,其中63%的项目甚至将strings命令作为唯一“反恶意代码扫描工具”。这种将文本可读性等同于可信性的认知偏差,已成为零信任落地的最大技术盲区。
从符号表到签名链的范式迁移
传统strings binary | grep -i "http"仅能捕获明文硬编码URL,却对混淆后的base64载荷、动态拼接的API端点完全失效。真实案例:某金融中间件v2.4.1版本中,攻击者将C2域名api[.]cloudsec[.]xyz拆解为"api" + "." + "cloudsec" + "." + "xyz"并嵌入静态库,strings输出中无任何可疑字符串,但readelf -d libcrypto.so | grep NEEDED暴露出异常依赖项libobfus.so——该库在官方源码树中根本不存在。
SBOM驱动的二进制溯源矩阵
可信供应链必须建立可验证的构件谱系。以下为某Kubernetes插件的SBOM片段(SPDX格式):
| SPDXID | fileName | checksums | originSHA |
|---|---|---|---|
| SPDXRef-123 | kube-proxy | SHA256: a1b2…c7d8 | 9f3e…4a1 |
| SPDXRef-456 | libseccomp.so | SHA256: e9f0…1c2d (from seccomp v2.5.4) | 2d1a…7f9 |
该矩阵要求每个二进制文件同时携带:① 构建环境哈希(buildkit生成的attestation);② 源码提交指纹(git commit --no-verify -S签名);③ 硬件级证明(Intel SGX远程证明报告)。
flowchart LR
A[源码仓库] -->|Git commit signed with GPG| B(构建节点)
B -->|Sigstore Fulcio证书| C[容器镜像]
C -->|Cosign签名+TUF元数据| D[生产集群]
D -->|SPIRE工作负载身份| E[运行时策略引擎]
E -->|拒绝未声明的syscall| F[强制执行]
动态符号解析的可信边界
nm -D ./nginx | grep "dlopen\|dlsym"可快速识别动态加载行为,但零信任要求更深层控制。某云厂商在Envoy代理中植入--enable-dynamic-loading=false编译标志,并通过BPF程序在execveat()系统调用层拦截所有/tmp/*.so路径加载请求。实际拦截日志显示:2024年Q1共阻断17次尝试加载未签名的libhook.so行为,全部源自被入侵的CI节点。
硬件根的信任锚点
当软件签名遭遇密钥泄露时,TPM 2.0 PCR寄存器成为最终防线。某Linux发行版将内核启动度量值写入PCR[0],而initramfs完整性哈希写入PCR[12]。通过tpm2_pcrread sha256:12命令可实时验证:若返回值与SBOM中声明的initramfs-sha256不匹配,则自动触发安全启动失败熔断。
供应链攻击面收敛实践
某支付网关项目实施“三不原则”:不接受任何未附带Sigstore签名的二进制依赖;不执行任何LD_PRELOAD环境变量指定的库;不在容器内挂载/proc/sys/kernel/kptr_restrict=0。上线后,其ELF文件平均符号表大小从217KB降至12KB,objdump -t输出中外部符号数量减少94%,有效压缩了攻击者利用符号劫持的窗口。
零信任不是添加更多检查点,而是重构二进制从构建到执行的每个原子操作的信任契约。
