osservatorionessuno/patela
GitHub: osservatorionessuno/patela
基于 TPM 2.0 硬件认证与远程证明的无磁盘 Tor 节点配置管理系统,支持动态中继部署与密钥硬件存储。
Stars: 36 | Forks: 1
# Patela
Patela 是一个基于 pull(拉取)模式的配置管理器,依赖 tpm 进行身份和加密操作。
该仓库同时提供用于无磁盘 tor 节点配置的客户端和服务器代码。
**Patela** 是 [piedmont](https://en.wikipedia.org/wiki/Piedmont) 语中“踢”的单词。
## 主要组件
- [actix-web](https://actix.rs):web 服务器
- [rustls](https://github.com/rustls/rustls):(m)Tls 的嵌入式 openssl 替代品
- [tss-esapi](https://github.com/parallaxsecond/rust-tss-esapi):tpm2 绑定
- [biscuit](https://www.biscuitsec.org/):session token
- [sqlx](https://github.com/launchbadge/sqlx):简单的 sql 库
## 核心概念
- **客户端身份**:每个客户端由其 TPM 的认可密钥(Endorsement Key, EK)唯一标识,而证明密钥(Attestation Key, AK)则在每次运行时动态生成。
- **远程证明即认证**:服务器专门针对客户端的 TPM 加密一个 bearer token。如果客户端成功解密,我们可以断定它运行在预期的 TPM 硬件上。
- **幂等运行**:客户端可以在已配置的节点上安全地重新运行注册流程,而不会破坏现有设置,从而实现动态升级。
- **数据持久化**:Tor 长期密钥存储在 TPM 的非易失性内存中,消除了远程备份的需求。
## 主流程
### 启动
我们依赖 [stboot](https://git.glasklar.is/system-transparency/core/stboot),这是 [System Transparency](https://docs.system-transparency.org/st-1.0.0/) 开发的一个漂亮的引导加载程序。
1. 从 usb live 启动(未来将支持 iPxe)
2. stboot 硬件验证
3. dhcp 管理接口
4. 从服务器获取 linux 主阶段镜像
### 首次运行(V2 - TPM 证明)
1. `client`:加载 TPM 认可密钥(EK)和证明密钥(AK)
2. `client`:发送包含 EK 公钥、AK 公钥和 AK name 的认证请求
3. `server`:通过匹配 TPM 密钥(EK + AK + AK name)创建或检索节点
4. `server`:检查节点是否已被管理员手动启用
5. `server`:使用 make_credential 创建 TPM 证明挑战
6. `server`:使用 TPM 挑战加密 Biscuit session token
7. `client`:使用 TPM 激活凭证以解密挑战
8. `client`:从解密后的挑战响应中提取 bearer token
9. `client`:报告硬件资源(CPU 核心、内存等)
10. `server`:根据规格计算中继数量(核心数、内存/1GB 的最小值)
11. `server`:为中继分配 IP 和 cheese 名称
12. `server`:构建配置层级(global → node → relay)
13. `database`:存储带有已分配资源的中继信息
14. `client`:获取包含已解析 Tor 设置的中继配置
15. `client`:创建 Tor 中继实例(用户和目录)
16. `client`:应用网络配置(通过 rtnetlink 进行 IP 绑定)
17. `client`:配置基于 UID 的源路由(nftables SNAT)
18. `client`:使用包含中继特定设置的模板生成 torrc 文件
19. `client`:启动 Tor 中继 systemd 服务
20. `client`:将 Tor 密钥存储在 TPM NV 索引中(V2 中不进行远程备份)
### 后续运行
流程与首次运行相同,除了:
1. `server`:通过 TPM 密钥识别现有节点(返回 200 OK 而非 201 CREATED)
2. `server`:返回现有的中继配置,而不是分配新的
3. `client`:从 TPM NV 存储恢复 Tor 密钥(而非从远程备份)
### 架构图
```
sequenceDiagram
autonumber
participant Client
participant TPM
participant Server
participant Database
Note over Client,Database: Boot Phase (stboot)
Client->>Client: Boot from USB/iPXE
Client->>Client: stboot hardware validation
Client->>Client: DHCP on mgmt interface
Client->>Server: Fetch Linux main stage image
Note over Client,Database: First Run: TPM Attestation & Authentication
Client->>TPM: Load EK and AK keys
TPM-->>Client: EK public, AK public, AK name
Client->>Server: POST /public/auth
{ek_public, ak_public, ak_name} Server->>Database: get_or_create_node_by_ek() Database-->>Server: node (enabled=0 for new nodes) alt Node not enabled Server-->>Client: 401 Unauthorized
"Node not yet enabled" Note over Client: Poll every 3s for 15min
until admin runs: patela enable
end
Server->>TPM: make_credential(AK, challenge_secret)
Note over Server: challenge_secret = Biscuit bearer token
TPM-->>Server: {blob, encrypted_secret}
alt First boot
Server-->>Client: 201 CREATED + {blob, secret}
else Subsequent boot
Server-->>Client: 200 OK + {blob, secret}
end
Client->>TPM: activate_credential(AK, EK, blob, secret)
TPM-->>Client: Decrypted bearer token
Note over Client,Database: Hardware Specs & Relay Allocation
Client->>Client: Collect hardware specs
(CPU cores, memory, network) Client->>Server: POST /private/specs + bearer token
{n_cpus, memory, cpu_name} Server->>Server: Calculate relay_count
min(memory/1GB, n_cpus) Server->>Database: Allocate cheese names
Allocate IPs (incremental) Server->>Database: Create relay records Database-->>Server: Success Server-->>Client: 200 OK Note over Client,Database: Configuration & Deployment Client->>Server: GET /private/config/node + bearer token Server->>Database: Fetch global_conf, node.tor_conf, relay.tor_conf Server->>Server: Resolve configuration hierarchy
(global → node → relay) Server-->>Client: Array of ResolvedRelayRecord
{name, ip_v4, ip_v6, or_port, dir_port, torrc} loop For each relay Client->>Client: Create system user _tor-{name} Client->>Client: Create /etc/tor/instances/{name}/ Client->>Client: Generate torrc from template end Client->>Server: GET /private/config/resolved/node + bearer token Server-->>Client: NodeConfig {network: {gateway_v4, gateway_v6, dns}} Client->>Client: Find network interface (starts with 'e', no IP) loop For each relay Client->>Client: rtnetlink: Add IP to interface Client->>Client: nftables: SNAT by relay UID
owner match → mark → source IP end loop For each relay Client->>Client: systemctl start tor@{name} end Client->>TPM: Store Tor relay keys in NV index Note over Client,TPM: V2: No remote backup,
keys stay in TPM only Note over Client,Database: Subsequent Boots Note over Server,Database: Server returns 200 OK (not 201)
Returns existing relay configs
No new IP/name allocation Note over Client,TPM: Client restores keys from TPM NV
Same configuration flow ``` ### 远程证明 V2 使用 `make_credential` / `activate_credential` 挑战-响应协议实现了基于 TPM 的远程证明: **工作原理**: 1. 客户端从 TPM 加载 EK(认可密钥)和 AK(证明密钥) 2. 客户端将公钥发送到服务器 3. 服务器使用 `make_credential` 创建一个加密到特定 TPM 的挑战 4. 只有具有匹配 EK 的 TPM 才能通过 `activate_credential` 解密 5. 这证明了客户端拥有特定的 TPM 硬件 **安全属性**: - 节点身份绑定到 TPM 硬件(EK + AK + AK Name) - 如果没有物理 TPM 访问权限,无法克隆 - 没有可窃取的共享密钥或证书 - 新节点需要手动管理员批准(`enabled` 标志) **与 V1 的比较**:V1 使用硬编码的客户端证书,这些证书可能会被窃取。V2 的 TPM 证明提供了无法从客户端二进制文件中提取的硬件绑定身份。 ## 快速开始 为了让 tpm 和 sqlite 正常工作,最好配置环境文件 ``` mv example.env .env ``` 生成服务器证书,如果您在不同的机器上运行,请添加网络地址或域名 ``` mkcert -install localhost 127.0.0.1 ::1 ``` 定位权威证书 ``` mkcert -CAROOT localhost 127.0.0.1 ::1 ``` 生成一个 biscuit 密钥对并复制私钥 `Private key: ed25519-private/`
```
biscuit keypair
```
设置本地数据库
```
cargo sqlx database setup --source server/migrations
cargo run -p client
```
检查变量配置并导出环境
```
set -a && source .env && set +a
```
测试服务器
```
cargo run -p patela-server -- run -vv
```
对于开发,这对于日志记录和重新加载非常有用
```
watchexec -w server -r cargo run -p patela-server -- run -vv
```
用于开发的 TPM 模拟,安装 [swtpm](https://github.com/stefanberger/swtpm)
要无需 root 权限访问 tpm 设备,你应该在 `/etc/udev/rules.d/` 中添加此 udev 规则,如 [参考文档](https://github.com/tpm2-software/tpm2-tss/blob/master/dist/tpm-udev.rules) 所示。
```
# tpm devices can only be accessed by the tss user but the tss
# group members can access tpmrm devices
KERNEL=="tpm[0-9]*", TAG+="systemd", MODE="0660", OWNER="wheel"
KERNEL=="tpmrm[0-9]*", TAG+="systemd", MODE="0660", GROUP="wheel"
KERNEL=="tcm[0-9]*", TAG+="systemd", MODE="0660", OWNER="wheel"
KERNEL=="tcmrm[0-9]*", TAG+="systemd", MODE="0660", GROUP="wheel"
```
并重新加载规则
```
udevadm control --reload-rules && udevadm trigger
```
```
export XDG_CONFIG_HOME=~/.config
```
首次设置
```
/usr/share/swtpm/swtpm-create-user-config-files
mkdir -p ${XDG_CONFIG_HOME}/patelatpm
swtpm_setup --tpm2 --tpmstate ${XDG_CONFIG_HOME}/patelatpm \
--create-ek-cert --create-platform-cert --lock-nvram
```
现在运行 tpm 模拟器
```
swtpm socket --tpm2 \
--server type=tcp,port=2321 \
--ctrl type=tcp,port=2322 \
--tpmstate dir=${XDG_CONFIG_HOME}/patelatpm \
--log file="swtpm.log" \
--log level=20 \
--flags not-need-init,startup-clear
```
```
export TPM2TOOLS_TCTI="swtpm:host=localhost,port=2321"
```
### 基本 CLI 操作
#### 服务器配置
**设置默认 Tor 配置:**
```
# 导入默认 torrc 文件
cargo run -p patela-server -- torrc import misc/default.torrc default
# 查看当前全局 Tor 配置
cargo run -p patela-server -- torrc get default
# 以 JSON 格式查看
cargo run -p patela-server -- torrc get default --json
```
**设置默认节点(网络)配置:**
```
# 设置全局网络配置(必填字段)
cargo run -p patela-server -- node set ipv4_gateway 10.10.10.1 default
cargo run -p patela-server -- node set ipv6_gateway fd00:1234:5678::1 default
# 设置可选字段
cargo run -p patela-server -- node set dns_server 10.10.10.2 default
cargo run -p patela-server -- node set interface_name eth0 default
# 查看当前全局节点配置
cargo run -p patela-server -- node get default
# 输出:
# 网络配置:
# IPv4 Gateway:10.10.10.1
# IPv6 Gateway:fd00:1234:5678::1
# DNS Server:10.10.10.2
# Interface Name:eth0
# 以 JSON 格式查看
cargo run -p patela-server -- node get default --json
# 移除可选字段(设置为 null)
cargo run -p patela-server -- node remove dns_server default
cargo run -p patela-server -- node remove interface_name default
```
**完整示例 - 设置新服务器:**
```
# 1. 设置默认 Tor 配置
cargo run -p patela-server -- torrc import misc/default.torrc default
# ✓ 全局默认配置导入成功
# 2. 设置默认网络配置
cargo run -p patela-server -- node set ipv4_gateway 10.10.10.1 default
# ✓ 全局默认 ipv4_gateway 设置为 10.10.10.1
cargo run -p patela-server -- node set ipv6_gateway fd00:1234:5678::1 default
# ✓ 全局默认 ipv6_gateway 设置为 fd00:1234:5678::1
# 3. 验证配置
cargo run -p patela-server -- node get default
# 网络配置:
# IPv4 Gateway:10.10.10.1
# IPv6 Gateway:fd00:1234:5678::1
cargo run -p patela-server -- torrc get default
# AvoidDiskWrites 1
# RelayBandwidthRate 40 MB
# RelayBandwidthBurst 80 MB
# ...
# 4. 启动服务器
set -a && source pippo.env && set +a
cargo run -p patela-server -- run -vvv
# 5. 当客户端连接时,检查待处理节点
cargo run -p patela-server -- list node
# ID | First Seen | Last Login | Enabled | EK Public (前 16 个字符)
# 1 | 2025-11-17 10:30:00 | 2025-11-17 10:30:00 | false | 0123456789abcdef...
# 6. 启用新节点
cargo run -p patela-server -- node enable 1
# ✓ 节点 1 启用成功
# 7. 查看所有中继
cargo run -p patela-server -- list relay
# ID | Node | Name | IPv4 | IPv6 | OR Port | Dir Port
# 1 | 1 | murazzano | 10.10.10.10 | fd00:1234:5678::100 | 9001 | 9030
# 2 | 1 | montebore | 10.10.10.11 | fd00:1234:5678::101 | 9001 | 9030
```
**设置节点特定配置:**
```
# 为特定节点覆盖 Tor 配置
cargo run -p patela-server -- torrc import custom-node.torrc node --id 1
# 为特定节点覆盖网络配置
cargo run -p patela-server -- node set ipv4_gateway 10.20.20.1 node --id 1
cargo run -p patela-server -- node set dns_server 10.20.20.2 node --id 1
# 查看特定节点配置
cargo run -p patela-server -- node get node --id 1
```
**设置中继特定配置:**
```
# 为特定中继覆盖 Tor 配置
cargo run -p patela-server -- torrc import custom-relay.torrc relay --id murazzano
```
#### 节点管理
**列出节点和中继:**
```
# 列出所有节点和中继
cargo run -p patela-server -- list all
# 仅列出节点
cargo run -p patela-server -- list node
# 仅列出中继
cargo run -p patela-server -- list relay
# 按名称筛选
cargo run -p patela-server -- list all murazzano
```
**启用/禁用节点:**
```
# 启用节点(允许身份验证和中继创建)
cargo run -p patela-server -- node enable 1
# 禁用节点(阻止身份验证)
cargo run -p patela-server -- node disable 1
```
#### 运行服务器
```
# 使用 pippo.env 中的环境变量运行
set -a && source pippo.env && set +a
cargo run -p patela-server -- run
# 使用详细日志运行
cargo run -p patela-server -- run -vvv
# 使用自定义选项运行
cargo run -p patela-server -- run \
--host 0.0.0.0 \
--port 8020 \
--ssl-cert-file certs/server.cert \
--ssl-key-file certs/server.key \
--biscuit-key
```
#### 客户端操作
```
# 运行客户端(连接到服务器,配置中继)
cargo run -p patela-client -- run --server https://server.example.com:8020
# 跳过网络设置(适用于测试)
cargo run -p patela-client -- run --server https://server.example.com:8020 --skip-net
# 跳过密钥恢复(全新开始)
cargo run -p patela-client -- run --server https://server.example.com:8020 --skip-restore
# TPM 操作
cargo run -p patela-client -- tpm attestate
cargo run -p patela-client -- tpm print-keys
cargo run -p patela-client -- tpm nv-read
cargo run -p patela-client -- tpm nv-write
# 网络操作
cargo run -p patela-client -- net list
```
测试 tpm 以进行证明
## 注意事项
### 认证(V2)
V2 使用基于 TPM 的证明代替 mTLS 证书作为节点身份:
**节点身份**:三个 TPM 值的组合:
- 认可密钥(EK)公钥部分
- 证明密钥(AK)公钥部分
- AK Name(AK 的加密名称)
**认证流程**:
1. 客户端从 TPM 加载 EK 和 AK
2. 客户端将公钥发送到服务器(`POST /public/auth`)
3. 服务器通过 `(ek_public, ak_public, ak_name)` 三元组匹配节点
4. 服务器使用 `make_credential` 创建证明挑战
5. 服务器将 Biscuit bearer token 作为挑战秘密加密
6. 客户端使用 `activate_credential` 解密(仅使用正确的 TPM 才可能)
7. 解密后的 token 成为 session bearer token
**TLS**:服务器仍使用 TLS(仅服务器端证书),但客户端认证通过 TPM 证明进行,而非客户端证书。
**手动批准**:新节点创建时 `enabled=0`,需要管理员通过 `patela enable ` 批准后才能进行认证。
## TPM
处理 tpm2 接口并非易事,幸运的是,rust 绑定的示例文档非常齐全,所有 patela 的代码只是两个示例的重构:
- [certify](https://github.com/parallaxsecond/rust-tss-esapi/blob/main/tss-esapi/examples/certify.rs)
用于与服务器的证明和注册
. [symmetric file encrypt decrypt](https://github.com/parallaxsecond/rust-tss-esapi/blob/main/tss-esapi/examples/symmetric_file_encrypt_decrypt.rs)
用于加密中继密钥以进行远程备份
## QEMU、Debian 和 部署
要运行 qemu/kvm,你需要一些配置:
- TPM 虚拟化/直通:如果你在 tpm 创建时遇到权限错误,请查看 `/var/lib/swtpm-localca/` 中的权限。它们应与 `/etc/libvirt/qemu.conf` 中的 `swtpm_{user, group}` 参数匹配
- 创建一个主网络并暴露 dhcp 服务器:你的服务器应该在此网络上可达
- 创建一个用于测试的第二“隔离”网络
- 使用 `virtiofs` 挂载工作目录,然后 `mount -t /{your mount name} /mnt`
我们部署在预构建的 debian 镜像上,但我们不做任何假设,你只需要一些依赖:
- `systemd`:处理中继生命周期
- `dhcp`:用于首次连接的客户端
- `libtss2-dev`:tpm 库
如果你需要为旧的 libc 版本或其他 exotic triplet 编译,你应该查看 [cargo zigbuild](https://github.com/rust-cross/cargo-zigbuild),它简直太棒了。在我的用例中,我想在 archlinux 笔记本电脑上构建 debug 版本并在 debian bookworm vm 中运行。这两个 glibc 不兼容,但使用 zig 你只需要运行:
```
cargo zigbuild --target x86_64-unknown-linux-gnu.2.41
```
要使用 qemu/libvirt 进行测试,你可以从 `misc/virsh.xml` 中的 virsh 示例开始,打开文件并将 `YOUR_PATH` 替换为有效的 debian/linux 内核镜像和 cpio,还有一个共享文件系统可以将代码目录挂载到 guest 内以便开发。此设置还假设有几个网络接口,一个用于 nat,另一个用于 ip 绑定。
一些有用的命令:
连接到控制台
```
virsh -c qemu:///system console patela
```
挂载主机文件系统
```
mount -t virtiofs /patela /mnt
```
清除持久设置中的 tpm
```
/mnt/target/x86_64-unknown-linux-gnu/debug/patela-client --tpm2 /dev/tpmrm0 tpm clean-persistent
```
运行 patela,服务器在主机上
```
/mnt/target/x86_64-unknown-linux-gnu/debug/patela-client --server https://10.10.10.1:8020 --tpm2 /dev/tpmrm0
```
如果你需要删除接口上的所有 ip 地址以便开发
```
ip addr flush
```
{ek_public, ak_public, ak_name} Server->>Database: get_or_create_node_by_ek() Database-->>Server: node (enabled=0 for new nodes) alt Node not enabled Server-->>Client: 401 Unauthorized
"Node not yet enabled" Note over Client: Poll every 3s for 15min
until admin runs: patela enable
(CPU cores, memory, network) Client->>Server: POST /private/specs + bearer token
{n_cpus, memory, cpu_name} Server->>Server: Calculate relay_count
min(memory/1GB, n_cpus) Server->>Database: Allocate cheese names
Allocate IPs (incremental) Server->>Database: Create relay records Database-->>Server: Success Server-->>Client: 200 OK Note over Client,Database: Configuration & Deployment Client->>Server: GET /private/config/node + bearer token Server->>Database: Fetch global_conf, node.tor_conf, relay.tor_conf Server->>Server: Resolve configuration hierarchy
(global → node → relay) Server-->>Client: Array of ResolvedRelayRecord
{name, ip_v4, ip_v6, or_port, dir_port, torrc} loop For each relay Client->>Client: Create system user _tor-{name} Client->>Client: Create /etc/tor/instances/{name}/ Client->>Client: Generate torrc from template end Client->>Server: GET /private/config/resolved/node + bearer token Server-->>Client: NodeConfig {network: {gateway_v4, gateway_v6, dns}} Client->>Client: Find network interface (starts with 'e', no IP) loop For each relay Client->>Client: rtnetlink: Add IP to interface Client->>Client: nftables: SNAT by relay UID
owner match → mark → source IP end loop For each relay Client->>Client: systemctl start tor@{name} end Client->>TPM: Store Tor relay keys in NV index Note over Client,TPM: V2: No remote backup,
keys stay in TPM only Note over Client,Database: Subsequent Boots Note over Server,Database: Server returns 200 OK (not 201)
Returns existing relay configs
No new IP/name allocation Note over Client,TPM: Client restores keys from TPM NV
Same configuration flow ``` ### 远程证明 V2 使用 `make_credential` / `activate_credential` 挑战-响应协议实现了基于 TPM 的远程证明: **工作原理**: 1. 客户端从 TPM 加载 EK(认可密钥)和 AK(证明密钥) 2. 客户端将公钥发送到服务器 3. 服务器使用 `make_credential` 创建一个加密到特定 TPM 的挑战 4. 只有具有匹配 EK 的 TPM 才能通过 `activate_credential` 解密 5. 这证明了客户端拥有特定的 TPM 硬件 **安全属性**: - 节点身份绑定到 TPM 硬件(EK + AK + AK Name) - 如果没有物理 TPM 访问权限,无法克隆 - 没有可窃取的共享密钥或证书 - 新节点需要手动管理员批准(`enabled` 标志) **与 V1 的比较**:V1 使用硬编码的客户端证书,这些证书可能会被窃取。V2 的 TPM 证明提供了无法从客户端二进制文件中提取的硬件绑定身份。 ## 快速开始 为了让 tpm 和 sqlite 正常工作,最好配置环境文件 ``` mv example.env .env ``` 生成服务器证书,如果您在不同的机器上运行,请添加网络地址或域名 ``` mkcert -install localhost 127.0.0.1 ::1 ``` 定位权威证书 ``` mkcert -CAROOT localhost 127.0.0.1 ::1 ``` 生成一个 biscuit 密钥对并复制私钥 `Private key: ed25519-private/
标签:actix-web, Biscuit, DHCP, iPXE, Rust, rustls, Session Token, SQLx, stboot, System Transparency, TLS, Tor, TPM, Web服务, 会话令牌, 受信任平台模块, 可信计算, 可视化界面, 安全启动, 客户端, 嵌入式, 幂等性, 开源, 无盘节点, 服务器, 硬件安全, 磁盘less, 网络安全, 网络安全, 网络引导, 网络流量审计, 远程认证, 通知系统, 防御工具, 隐私保护, 隐私保护, 非易失性存储