Compare commits
3 Commits
dev_1.3_xu
...
cc9f3f21de
| Author | SHA1 | Date | |
|---|---|---|---|
| cc9f3f21de | |||
| 56ae2ca4fc | |||
| bcd4829486 |
@@ -1,9 +0,0 @@
|
|||||||
.git
|
|
||||||
.gitignore
|
|
||||||
.idea
|
|
||||||
target
|
|
||||||
rtr-db
|
|
||||||
tests
|
|
||||||
specs
|
|
||||||
scripts
|
|
||||||
README.md
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,5 +1,2 @@
|
|||||||
target/
|
target/
|
||||||
rtr-db/
|
Cargo.lock
|
||||||
.idea/
|
|
||||||
logs/
|
|
||||||
report/
|
|
||||||
|
|||||||
3528
Cargo.lock
generated
3528
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
35
Cargo.toml
35
Cargo.toml
@@ -1,40 +1,15 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rpki"
|
name = "rpki"
|
||||||
version = "1.3.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "rpki_rtr"
|
|
||||||
path = "src/main_rtr.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
der-parser = "10.0.0"
|
der-parser = { version = "10.0.0", features = ["serialize"] }
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
|
base64 = "0.22.1"
|
||||||
|
sha2 = "0.10.8"
|
||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
time = "0.3.45"
|
time = "0.3.45"
|
||||||
|
ring = "0.17.14"
|
||||||
x509-parser = { version = "0.18.0", features = ["verify"] }
|
x509-parser = { version = "0.18.0", features = ["verify"] }
|
||||||
url = "2.5.8"
|
url = "2.5.8"
|
||||||
asn1-rs = "0.7.1"
|
|
||||||
asn1-rs-derive = "0.6.0"
|
|
||||||
asn1 = "0.23.0"
|
|
||||||
arc-swap = "1.7.0"
|
|
||||||
chrono = { version = "0.4.44", features = ["serde"] }
|
|
||||||
chrono-tz = "0.10"
|
|
||||||
bytes = "1.11.1"
|
|
||||||
tokio = { version = "1.49.0", features = ["full"] }
|
|
||||||
rand = "0.10.0"
|
|
||||||
rocksdb = { version = "0.21.0", default-features = false }
|
|
||||||
serde = { version = "1", features = ["derive", "rc"] }
|
|
||||||
serde_json = "1"
|
|
||||||
base64 = "0.22"
|
|
||||||
anyhow = "1"
|
|
||||||
tracing = "0.1.44"
|
|
||||||
sha2 = "0.10"
|
|
||||||
tempfile = "3"
|
|
||||||
tokio-rustls = "0.26"
|
|
||||||
rustls = "0.23"
|
|
||||||
rustls-pemfile = "2"
|
|
||||||
rustls-pki-types = "1.14.0"
|
|
||||||
socket2 = "0.5"
|
|
||||||
russh = { version = "0.60.0", default-features = false, features = ["ring", "rsa"] }
|
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
|
||||||
|
|||||||
287
README.client.md
287
README.client.md
@@ -1,287 +0,0 @@
|
|||||||
# RTR Client 使用说明
|
|
||||||
|
|
||||||
本文说明仓库内三类 RTR Client 的用途、启动方式和验证命令:
|
|
||||||
|
|
||||||
- `deploy/client`: 本仓库自带的 `rtr_debug_client`,适合做 RTR 协议调试、连通性检查和单次查询。
|
|
||||||
- `deploy/frr`: FRR 作为黑盒 RTR Client,适合验证真实路由软件是否能消费 server 下发的 VRP。
|
|
||||||
- `deploy/bird`: BIRD 作为黑盒 RTR Client,适合验证 TCP 或 SSH transport 下的 RPKI/ROA/ASPA 数据导入。
|
|
||||||
|
|
||||||
以下命令默认从仓库根目录执行。
|
|
||||||
|
|
||||||
## 目录
|
|
||||||
|
|
||||||
- [前置条件](#前置条件)
|
|
||||||
- [rtr_debug_client](#rtr_debug_client)
|
|
||||||
- [Docker attach 交互命令](#docker-attach-交互命令)
|
|
||||||
- [TCP](#tcp)
|
|
||||||
- [TLS/mTLS](#tlsmtls)
|
|
||||||
- [SSH](#ssh)
|
|
||||||
- [多客户端压测](#多客户端压测)
|
|
||||||
- [FRR Client](#frr-client)
|
|
||||||
- [BIRD Client](#bird-client)
|
|
||||||
- [TCP](#tcp-1)
|
|
||||||
- [SSH](#ssh-1)
|
|
||||||
- [如何判断 Client 正常](#如何判断-client-正常)
|
|
||||||
|
|
||||||
## 前置条件
|
|
||||||
|
|
||||||
先启动 RTR Server。TCP 模式是最常用的本地验证方式:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./deploy/server/build-run.sh up --mode tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
如果要测试 TLS 或 SSH,需要先启动对应 server 模式,并准备匹配的证书或 SSH key:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./deploy/server/build-run.sh up --mode tls
|
|
||||||
./deploy/server/build-run.sh up --mode ssh
|
|
||||||
```
|
|
||||||
|
|
||||||
TLS/SSH 所需的本地开发证书和密钥可以通过脚本生成:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/generate-certs.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
已有 `certs/` 时使用 `--force` 覆盖。脚本生成的 `certs/tls` 和 `certs/ssh` 文件名与本仓库 compose 默认配置一致。
|
|
||||||
|
|
||||||
Client 连接地址不要求固定在某个 Docker 网络里。`deploy/client` 的 TCP/TLS/SSH 模式分别通过 `RPKI_RTR_TCP_SERVER_ADDR`、`RPKI_RTR_TLS_SERVER_ADDR`、`RPKI_RTR_SSH_SERVER_ADDR` 指定 server 地址。`deploy/bird` 通过 `RPKI_BIRD_RPKI_HOST`/`RPKI_BIRD_RPKI_PORT` 指定 TCP server 地址,通过 `RPKI_BIRD_SSH_RPKI_HOST`/`RPKI_RTR_SSH_PORT` 指定 SSH server 地址。示例 compose 里的默认值只是为了和本仓库 server compose 直接联调。
|
|
||||||
|
|
||||||
`deploy/frr` 示例默认连接 `127.0.0.1:323`,可以通过 `RPKI_FRR_RPKI_HOST`/`RPKI_FRR_RPKI_PORT` 覆盖。
|
|
||||||
|
|
||||||
## rtr_debug_client
|
|
||||||
|
|
||||||
`rtr_debug_client` 是本仓库提供的调试客户端。它会发起 RTR Reset Query 或 Serial Query,并打印 server 返回的 PDU 摘要。Docker 配置默认执行:
|
|
||||||
|
|
||||||
```text
|
|
||||||
<server_addr> <protocol_version> reset --summary-only
|
|
||||||
```
|
|
||||||
|
|
||||||
Docker 镜像的 `entrypoint.sh` 会把 stdout/stderr 重定向到 `logs/client/*.stdout.log` 和 `logs/client/*.stderr.log`。因此 `docker attach` 后可以输入交互命令,但命令响应和收到的 PDU 仍然主要从日志文件查看:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
tail -f logs/client/*.stdout.log
|
|
||||||
tail -f logs/client/*.stderr.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker attach 交互命令
|
|
||||||
|
|
||||||
如果 debug client 已经通过 compose 启动,可以 attach 到容器后输入命令:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker attach --sig-proxy=false <rtr-debug-client-container>
|
|
||||||
```
|
|
||||||
|
|
||||||
脱离 attach 会话时使用 `Ctrl-P` 后接 `Ctrl-Q`,避免 `Ctrl-C` 直接停止客户端进程。
|
|
||||||
|
|
||||||
可用交互命令如下:
|
|
||||||
|
|
||||||
| 命令 | 作用 |
|
|
||||||
| --- | --- |
|
|
||||||
| `help` | 显示可用命令列表。 |
|
|
||||||
| `state` | 打印当前客户端状态,包括 `session_id`、`serial`、`refresh/retry/expire`、输出模式、自动轮询状态等。 |
|
|
||||||
| `version` | 查看当前 RTR 协议版本。 |
|
|
||||||
| `version <n>` | 修改后续查询使用的 RTR 协议版本,例如 `version 2`。 |
|
|
||||||
| `reset` | 发送 Reset Query,通常用于让 server 重新下发完整数据集。 |
|
|
||||||
| `serial` | 使用当前已记录的 `session_id` 和 `serial` 发送 Serial Query。需要客户端已经从 EndOfData 中拿到状态。 |
|
|
||||||
| `serial <session_id> <serial>` | 显式指定参数发送 Serial Query,例如 `serial 31320 156`。 |
|
|
||||||
| `timeout` | 查看当前读取 PDU 的超时时间。 |
|
|
||||||
| `timeout <secs>` | 修改读取 PDU 的超时时间。超时只打印提示,连接保持打开。 |
|
|
||||||
| `poll` | 查看当前自动轮询间隔、来源、refresh hint、错误状态和暂停状态。 |
|
|
||||||
| `poll <secs>` | 修改自动轮询间隔。 |
|
|
||||||
| `poll pause` | 暂停自动轮询。 |
|
|
||||||
| `poll resume` | 恢复自动轮询。 |
|
|
||||||
| `output` | 查看当前输出模式,以及 summary 模式下跳过的 payload PDU 数量。 |
|
|
||||||
| `output verbose` | 打印所有收到的 PDU,包括 IPv4 Prefix、IPv6 Prefix、Router Key、ASPA 等 payload PDU。 |
|
|
||||||
| `output summary` | 只打印摘要,跳过 payload PDU 详情。compose 默认使用该模式。 |
|
|
||||||
| `quit` / `exit` | 退出客户端进程。 |
|
|
||||||
|
|
||||||
常用调试流程:
|
|
||||||
|
|
||||||
```text
|
|
||||||
output verbose
|
|
||||||
reset
|
|
||||||
state
|
|
||||||
serial
|
|
||||||
```
|
|
||||||
|
|
||||||
### TCP
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./deploy/client/build-run.sh up --mode tcp
|
|
||||||
./deploy/client/build-run.sh logs --mode tcp
|
|
||||||
./deploy/client/build-run.sh down --mode tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
默认连接 `rpki-rtr:323`,协议版本默认是 `2`。可以通过 `deploy/client/.env` 或环境变量覆盖:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
RPKI_RTR_TCP_SERVER_ADDR=10.0.0.12:323 \
|
|
||||||
RPKI_RTR_PROTOCOL_VERSION=2 \
|
|
||||||
./deploy/client/build-run.sh up --mode tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
### TLS/mTLS
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./deploy/client/build-run.sh up --mode tls
|
|
||||||
./deploy/client/build-run.sh logs --mode tls
|
|
||||||
./deploy/client/build-run.sh down --mode tls
|
|
||||||
```
|
|
||||||
|
|
||||||
TLS 模式默认读取 `certs/tls` 下的示例证书,并使用 `localhost` 作为 server name。常用变量:
|
|
||||||
|
|
||||||
| 变量 | 默认值 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `RPKI_RTR_TLS_SERVER_ADDR` | `rpki-rtr:324` | TLS RTR server 地址 |
|
|
||||||
| `RPKI_RTR_TLS_SERVER_NAME` | `localhost` | 必须匹配 server 证书 SAN dNSName |
|
|
||||||
| `RPKI_RTR_TLS_CA_CERT_PATH` | `/app/certs/client-ca.crt` | 容器内 CA 证书路径 |
|
|
||||||
| `RPKI_RTR_TLS_CLIENT_CERT_PATH` | `/app/certs/client-good.crt` | 容器内 client 证书路径 |
|
|
||||||
| `RPKI_RTR_TLS_CLIENT_KEY_PATH` | `/app/certs/client-good.key` | 容器内 client 私钥路径 |
|
|
||||||
| `RPKI_RTR_TLS_CERTS_HOST_DIR` | `../../certs/tls` | 宿主机 TLS 证书目录 |
|
|
||||||
|
|
||||||
### SSH
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./deploy/client/build-run.sh up --mode ssh
|
|
||||||
./deploy/client/build-run.sh logs --mode ssh
|
|
||||||
./deploy/client/build-run.sh down --mode ssh
|
|
||||||
```
|
|
||||||
|
|
||||||
SSH 模式默认连接 `rpki-rtr-ssh:22`,使用 key 认证,并校验 server public key。常用变量:
|
|
||||||
|
|
||||||
| 变量 | 默认值 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `RPKI_RTR_SSH_SERVER_ADDR` | `rpki-rtr:22` | SSH RTR server 地址 |
|
|
||||||
| `RPKI_RTR_SSH_USERNAME` | `rpki-rtr` | SSH 用户名 |
|
|
||||||
| `RPKI_RTR_CLIENT_KEYS_VOLUME` | `../../certs/ssh:/app/certs:ro` | 宿主机 SSH key 目录挂载 |
|
|
||||||
| `RPKI_RTR_CLIENT_KEY_PATH` | `/app/certs/rtr-client.key` | 容器内 client 私钥 |
|
|
||||||
| `RPKI_RTR_SSH_SERVER_PUBKEY_PATH` | `/app/certs/ssh_host_rsa_key.pub` | 容器内 server public key |
|
|
||||||
|
|
||||||
### 多客户端压测
|
|
||||||
|
|
||||||
`--clients multi` 会启动 5 个 TCP debug client,用于观察并发连接、client report 和 server 连接统计。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./deploy/client/build-run.sh up --mode tcp --clients multi
|
|
||||||
./deploy/client/build-run.sh logs --mode tcp --clients multi
|
|
||||||
./deploy/client/build-run.sh down --mode tcp --clients multi
|
|
||||||
```
|
|
||||||
|
|
||||||
## FRR Client
|
|
||||||
|
|
||||||
FRR 示例用于验证标准路由软件能否通过 RTR over TCP 从 server 获取前缀验证数据。
|
|
||||||
|
|
||||||
启动:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
默认连接 `127.0.0.1:323`。可以通过环境变量覆盖:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
RPKI_FRR_RPKI_HOST=10.0.0.12 \
|
|
||||||
RPKI_FRR_RPKI_PORT=323 \
|
|
||||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
验证连接和数据:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec -it frr-rpki-client vtysh -c "show rpki configuration"
|
|
||||||
docker exec -it frr-rpki-client vtysh -c "show rpki cache-connection"
|
|
||||||
docker exec -it frr-rpki-client vtysh -c "show rpki prefix-table"
|
|
||||||
```
|
|
||||||
|
|
||||||
停止:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
FRR 容器启动时会读取 `deploy/frr/frr.conf.template`,并生成 `/etc/frr/frr.conf`。模板中的 RTR cache 配置为:
|
|
||||||
|
|
||||||
```text
|
|
||||||
rpki cache tcp ${RPKI_FRR_RPKI_HOST} ${RPKI_FRR_RPKI_PORT} preference ${RPKI_FRR_RPKI_PREFERENCE}
|
|
||||||
```
|
|
||||||
|
|
||||||
如果需要调整 polling、timeout、BGP router-id 等其他 FRR 配置,可以修改 `deploy/frr/frr.conf.template`,或在 compose 中挂载自己的模板和 entrypoint。
|
|
||||||
|
|
||||||
## BIRD Client
|
|
||||||
|
|
||||||
BIRD 示例用于验证 BIRD 3.x 对 RTR v2、ROA 和 ASPA 的导入情况。容器启动后会周期性输出 RPKI 协议状态和表项摘要。
|
|
||||||
|
|
||||||
### TCP
|
|
||||||
|
|
||||||
启动:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose --env-file deploy/bird/.env -f deploy/bird/docker-compose.yml up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
查看日志:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs -f bird-rpki-client
|
|
||||||
```
|
|
||||||
|
|
||||||
停止:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose --env-file deploy/bird/.env -f deploy/bird/docker-compose.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
默认连接 `rpki-rtr:323`。可以通过环境变量覆盖:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
RPKI_BIRD_RPKI_HOST=10.0.0.12 \
|
|
||||||
RPKI_BIRD_RPKI_PORT=323 \
|
|
||||||
docker compose --env-file deploy/bird/.env -f deploy/bird/docker-compose.yml up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
### SSH
|
|
||||||
|
|
||||||
先启动 server SSH 模式:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./deploy/server/build-run.sh up --mode ssh
|
|
||||||
```
|
|
||||||
|
|
||||||
再启动 BIRD SSH client:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose --env-file deploy/bird/.env \
|
|
||||||
-f deploy/bird/docker-compose.yml \
|
|
||||||
-f deploy/bird/docker-compose.ssh.yml \
|
|
||||||
up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
查看日志:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs -f bird-rpki-client
|
|
||||||
```
|
|
||||||
|
|
||||||
SSH 模式默认读取 `certs/ssh` 目录下的 key,并使用 `bird.conf.ssh.template` 生成运行时配置。关键默认值:
|
|
||||||
|
|
||||||
| 变量 | 默认值 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `RPKI_BIRD_SSH_RPKI_HOST` | `rpki-rtr` | SSH RTR server 主机名 |
|
|
||||||
| `RPKI_RTR_SSH_PORT` | `22` | SSH RTR server 端口 |
|
|
||||||
| `RPKI_BIRD_SSH_CERTS_HOST_DIR` | `../../certs/ssh` | 宿主机 SSH 证书和 key 目录,复用 `rtr-client.key` |
|
|
||||||
|
|
||||||
## 如何判断 Client 正常
|
|
||||||
|
|
||||||
- `rtr_debug_client`: 日志中能看到 Reset Query 成功、EndOfData 和 payload 统计。
|
|
||||||
- FRR: `show rpki cache-connection` 显示已连接,`show rpki prefix-table` 有 VRP 条目。
|
|
||||||
- BIRD: 日志中 RPKI protocol 状态为 `up`,ROA/ASPA 表项摘要不为空。
|
|
||||||
|
|
||||||
如果 client 无法连接,优先检查:
|
|
||||||
|
|
||||||
- server 是否按对应 transport 启动。
|
|
||||||
- client 配置的 server 地址是否从当前容器或主机可达。
|
|
||||||
- TLS 的 server name 是否匹配证书 SAN。
|
|
||||||
- SSH 的用户名、client key 和 server public key 是否匹配。
|
|
||||||
- `RPKI_RTR_PROTOCOL_VERSION` 或 BIRD 配置中的 `min version`/`max version` 是否和 server 能力一致。
|
|
||||||
377
README.md
377
README.md
@@ -1,373 +1,28 @@
|
|||||||
# RPKI RTR Server
|
|
||||||
|
|
||||||
这是一个用 Rust 实现的 RPKI-to-Router (RTR) cache server。服务端从 CCR 文件中读取 RPKI 数据,生成 RTR cache snapshot 和 delta,并通过 RTR 协议提供给路由器或调试客户端。
|
|
||||||
|
|
||||||
当前代码重点覆盖:
|
# 单元测试
|
||||||
|
|
||||||
- RTR server:TCP 默认启用,可选启用 TLS/mTLS 和进程内 SSH transport。
|
```
|
||||||
- 数据输入:从 CCR 目录选择最新 `.ccr` 文件,解析 VRP 和 VAP/ASPA。
|
|
||||||
- 本地策略:可选加载 SLURM 文件,对 CCR 解析出的 payload 做过滤和本地断言。
|
|
||||||
- 持久化:使用 RocksDB 保存 RTR cache 状态。
|
|
||||||
- 运行报告:输出 source、client、runtime 三类 JSON report。
|
|
||||||
- 运行期管理:可选启用 HTTP Admin API,修改部分 runtime config、触发 source reload、管理 SLURM 文件。
|
|
||||||
- 调试工具:`rtr_debug_client` 支持 TCP、TLS 和 SSH 连接 RTR server。
|
|
||||||
|
|
||||||
## 目录
|
|
||||||
|
|
||||||
- [协议参考](#协议参考)
|
|
||||||
- [项目结构](#项目结构)
|
|
||||||
- [构建与测试](#构建与测试)
|
|
||||||
- [数据输入](#数据输入)
|
|
||||||
- [快速启动](#快速启动)
|
|
||||||
- [前置数据生成](#前置数据生成)
|
|
||||||
- [Docker 运行](#docker-运行)
|
|
||||||
- [跨架构构建](#跨架构构建)
|
|
||||||
- [传输模式](#传输模式)
|
|
||||||
- [主要环境变量](#主要环境变量)
|
|
||||||
- [运行报告](#运行报告)
|
|
||||||
- [Admin API](#admin-api)
|
|
||||||
- [调试客户端](#调试客户端)
|
|
||||||
- [部署入口](#部署入口)
|
|
||||||
- [开发说明](#开发说明)
|
|
||||||
|
|
||||||
## 协议参考
|
|
||||||
|
|
||||||
- RTR: [RFC 6810](https://www.rfc-editor.org/rfc/rfc6810.html), [RFC 8210](https://www.rfc-editor.org/rfc/rfc8210.html), [draft-ietf-sidrops-8210bis-26](https://www.ietf.org/archive/id/draft-ietf-sidrops-8210bis-26.html)
|
|
||||||
- SLURM: [RFC 8416](https://www.rfc-editor.org/rfc/rfc8416.html), [draft-ietf-sidrops-aspa-slurm-04](https://www.ietf.org/archive/id/draft-ietf-sidrops-aspa-slurm-04.html)(已过期,Expired 2026-05-20)
|
|
||||||
- CCR: [draft-ietf-sidrops-rpki-ccr](https://www.ietf.org/archive/id/draft-ietf-sidrops-rpki-ccr-02.html)
|
|
||||||
|
|
||||||
## 项目结构
|
|
||||||
|
|
||||||
| 路径 | 说明 |
|
|
||||||
| --- | --- |
|
|
||||||
| `src/main_rtr.rs` | RTR server 入口 |
|
|
||||||
| `src/rtr/` | RTR 协议、cache、store、server transport、admin、report |
|
|
||||||
| `src/source/` | CCR/SLURM source 加载流程 |
|
|
||||||
| `src/slurm/` | SLURM 文件解析、合并和策略应用 |
|
|
||||||
| `src/data_model/` | RPKI 相关数据结构 |
|
|
||||||
| `src/bin/rtr_debug_client/` | RTR 调试客户端 |
|
|
||||||
| `deploy/server/` | server Docker 镜像和 compose |
|
|
||||||
| `deploy/client/` | debug client Docker 镜像和 compose |
|
|
||||||
| `deploy/frr/`, `deploy/bird/` | FRR/BIRD 作为 RTR client 的测试部署 |
|
|
||||||
| `tests/` | 单元和集成测试 |
|
|
||||||
|
|
||||||
## 构建与测试
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo build --bin rpki_rtr
|
|
||||||
cargo build --bin rtr_debug_client
|
|
||||||
cargo test
|
cargo test
|
||||||
|
|
||||||
|
# 查看输出
|
||||||
|
cargo test -- --nocapture
|
||||||
```
|
```
|
||||||
|
|
||||||
服务端 binary 名称是 `rpki_rtr`:
|
# 覆盖率(cargo-llvm-cov)
|
||||||
|
|
||||||
```bash
|
安装工具:
|
||||||
cargo run --bin rpki_rtr
|
|
||||||
|
```
|
||||||
|
rustup component add llvm-tools-preview
|
||||||
|
cargo install cargo-llvm-cov --locked
|
||||||
```
|
```
|
||||||
|
|
||||||
默认配置会监听 `0.0.0.0:323`,从 `./data` 查找 CCR 文件,并使用 `./rtr-db` 保存 RocksDB 数据。启动前需要确保 CCR 目录里存在可解析的 `.ccr` 文件。
|
统计行覆盖率并要求 >=90%:
|
||||||
|
|
||||||
## 数据输入
|
```
|
||||||
|
./scripts/coverage.sh
|
||||||
服务端启动和刷新时会读取 `RPKI_RTR_CCR_DIR` 指向的目录:
|
# 或
|
||||||
|
cargo llvm-cov --fail-under-lines 90
|
||||||
1. 如果该目录下存在包含 `.ccr` 文件的子目录,先按子目录名排序选择最新子目录。
|
|
||||||
2. 在选中的目录中按文件名排序选择最新 `.ccr` 文件。
|
|
||||||
3. 从 CCR 中解析 VRP 和 VAP/ASPA。
|
|
||||||
4. 如果配置了 `RPKI_RTR_SLURM_DIR`,加载其中启用的 `*.slurm` 文件并应用本地策略。
|
|
||||||
|
|
||||||
## 快速启动
|
|
||||||
|
|
||||||
### 前置数据生成
|
|
||||||
|
|
||||||
TLS/mTLS 或 SSH transport 需要本地证书和密钥。开发和 Docker 联调可用脚本生成 compose 默认引用的最小文件集:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/10_generate-certs.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
如需覆盖已有 `certs/` 内容:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/10_generate-certs.sh --force
|
|
||||||
```
|
|
||||||
|
|
||||||
`10_generate-certs.sh` 常用参数:
|
|
||||||
|
|
||||||
| 参数 | 说明 |
|
|
||||||
| --- | --- |
|
|
||||||
| `--out-dir DIR` | 指定输出目录,默认生成到 `./certs`。 |
|
|
||||||
| `--server-dns NAME` | 指定 TLS server 证书的 dNSName SAN,默认 `localhost`。 |
|
|
||||||
| `--tls-only` | 只生成 `certs/tls` 下的 TLS 证书和私钥。 |
|
|
||||||
| `--ssh-only` | 只生成 `certs/ssh` 下的 SSH host key、client key 和 `authorized_keys`。 |
|
|
||||||
| `--force` | 覆盖已有文件。未指定时,如果目标文件已存在会直接报错退出。 |
|
|
||||||
| `-h`, `--help` | 查看脚本帮助。 |
|
|
||||||
|
|
||||||
TLS server name 默认是 `localhost`。如果需要其它 DNS 名称,需要同时设置 `scripts/10_generate-certs.sh --server-dns <name>` 和 client 的 `RPKI_RTR_TLS_SERVER_NAME=<name>`。
|
|
||||||
|
|
||||||
### Docker 运行
|
|
||||||
|
|
||||||
推荐使用 `deploy/server/build-run.sh` 和 `deploy/client/build-run.sh` 分别管理 server 与 debug client:
|
|
||||||
以下命令默认从仓库根目录执行。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod +x deploy/server/build-run.sh deploy/client/build-run.sh
|
|
||||||
./deploy/server/build-run.sh up --mode tcp
|
|
||||||
./deploy/client/build-run.sh up --mode tcp
|
|
||||||
./deploy/client/build-run.sh down
|
|
||||||
./deploy/server/build-run.sh down
|
|
||||||
```
|
|
||||||
|
|
||||||
脚本常用参数:
|
|
||||||
|
|
||||||
| 参数/变量 | server | client | 说明 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `--mode base|tcp|tls|ssh`, `MODE` | yes | yes | 选择 compose 模式,默认 `tcp`。 |
|
|
||||||
| `--clients single|multi`, `CLIENTS` | no | yes | 单 client 或 5 client,默认 `single`。 |
|
|
||||||
| `--no-build`, `BUILD=0` | yes | yes | 只运行已有镜像,不执行 compose build。 |
|
|
||||||
| `--server-image TAG`, `SERVER_IMAGE` | yes | no | server 镜像名;会传给 compose 的 `RPKI_RTR_SERVER_IMAGE`。 |
|
|
||||||
| `--client-image TAG`, `CLIENT_IMAGE` | no | yes | client 镜像名;会传给 compose 的 `RPKI_RTR_CLIENT_IMAGE`。 |
|
|
||||||
| `--platform VALUE`, `TARGET_PLATFORM` | yes | yes | `buildx` 目标平台,默认 `linux/arm64`。 |
|
|
||||||
| `--push`, `BUILDX_PUSH=1` | yes | yes | `buildx` 构建后推送到 registry;不设置时加载到本地 Docker。 |
|
|
||||||
| `--skip-base-pull`, `SKIP_BASE_IMAGE_PULL=1` | yes | yes | 跳过 buildx 前的基础镜像预拉取。 |
|
|
||||||
|
|
||||||
支持的 action 包括 `up`、`build`、`buildx`、`rebuild`、`down`/`stop`、`restart`、`logs`、`ps`。完整帮助见:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./deploy/server/build-run.sh --help
|
|
||||||
./deploy/client/build-run.sh --help
|
|
||||||
```
|
|
||||||
|
|
||||||
### 跨架构构建
|
|
||||||
示例:x86 开发机本地构建 ARM 镜像,打包后传到 ARM 部署机。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./deploy/server/build-run.sh buildx \
|
|
||||||
--platform linux/arm64 \
|
|
||||||
--server-image rpki-rtr:arm64
|
|
||||||
|
|
||||||
./deploy/client/build-run.sh buildx \
|
|
||||||
--platform linux/arm64 \
|
|
||||||
--client-image rpki-rtr-debug-client:arm64
|
|
||||||
|
|
||||||
mkdir -p dist/images
|
|
||||||
docker save rpki-rtr:arm64 -o dist/images/rpki-rtr-arm64.tar
|
|
||||||
docker save rpki-rtr-debug-client:arm64 -o dist/images/rpki-rtr-debug-client-arm64.tar
|
|
||||||
|
|
||||||
scp dist/images/*.tar user@arm-host:/tmp/
|
|
||||||
```
|
|
||||||
|
|
||||||
ARM 部署机加载镜像并运行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker load -i /tmp/rpki-rtr-arm64.tar
|
|
||||||
docker load -i /tmp/rpki-rtr-debug-client-arm64.tar
|
|
||||||
|
|
||||||
SERVER_IMAGE=rpki-rtr:arm64 \
|
|
||||||
BUILD=0 ./deploy/server/build-run.sh up --mode tcp
|
|
||||||
|
|
||||||
CLIENT_IMAGE=rpki-rtr-debug-client:arm64 \
|
|
||||||
BUILD=0 ./deploy/client/build-run.sh up --mode tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/server/docker-compose.yml up -d --build
|
|
||||||
docker compose -f deploy/server/docker-compose.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
TLS/mTLS 示例:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/server/docker-compose.tls.yml up -d --build
|
|
||||||
docker compose -f deploy/server/docker-compose.tls.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
## 传输模式
|
|
||||||
|
|
||||||
TCP 默认启用。TLS 和 SSH 是附加监听,不会替代 TCP。
|
|
||||||
|
|
||||||
| 模式 | 默认地址 | 启用方式 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| TCP | `0.0.0.0:323` | 默认启用 |
|
|
||||||
| TLS/mTLS | `0.0.0.0:324` | `RPKI_RTR_ENABLE_TLS=true` |
|
|
||||||
| SSH | `0.0.0.0:22` | `RPKI_RTR_ENABLE_SSH=true` |
|
|
||||||
|
|
||||||
TLS server 会加载服务端证书、私钥和 client CA。SSH server 使用 OpenSSH host key 和 authorized_keys 文件;`RPKI_RTR_SSH_AUTH_MODE` 支持 `key`、`password`、`both`。
|
|
||||||
|
|
||||||
## 主要环境变量
|
|
||||||
|
|
||||||
布尔值支持 `true/false`、`1/0`、`yes/no`、`on/off`。
|
|
||||||
|
|
||||||
| 变量 | 默认值 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `RPKI_RTR_ENABLE_TLS` | `false` | 是否启用 TLS/mTLS 监听 |
|
|
||||||
| `RPKI_RTR_ENABLE_SSH` | `false` | 是否启用 SSH 监听 |
|
|
||||||
| `RPKI_RTR_TCP_ADDR` | `0.0.0.0:323` | TCP 监听地址 |
|
|
||||||
| `RPKI_RTR_TLS_ADDR` | `0.0.0.0:324` | TLS 监听地址 |
|
|
||||||
| `RPKI_RTR_SSH_ADDR` | `0.0.0.0:22` | SSH 监听地址 |
|
|
||||||
| `RPKI_RTR_SSH_PORT` | 未设置 | 仅覆盖 `RPKI_RTR_SSH_ADDR` 中的端口 |
|
|
||||||
| `RPKI_RTR_DB_PATH` | `./rtr-db` | RocksDB 路径 |
|
|
||||||
| `RPKI_RTR_CCR_DIR` | `./data` | CCR 输入目录 |
|
|
||||||
| `RPKI_RTR_SLURM_DIR` | 未设置 | SLURM 目录;空值表示禁用 |
|
|
||||||
| `RPKI_RTR_REPORT_DIR` | `./report` | JSON report 输出目录 |
|
|
||||||
| `RPKI_RTR_MAX_DELTA` | `100` | 每个 RTR 版本保留的最大 delta 数,必须大于 0 |
|
|
||||||
| `RPKI_RTR_PRUNE_DELTA_BY_SNAPSHOT_SIZE` | `false` | 是否按 snapshot wire size 继续裁剪 delta window |
|
|
||||||
| `RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS` | `300` | source 刷新间隔,单位秒 |
|
|
||||||
| `RPKI_RTR_REFRESH_INTERVAL_SECS` | 未设置 | 旧变量名,仍兼容;建议使用 `RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS` |
|
|
||||||
| `RPKI_RTR_TIMING_REFRESH_SECS` | `3600` | RTR EndOfData `refresh` 字段 |
|
|
||||||
| `RPKI_RTR_TIMING_RETRY_SECS` | `600` | RTR EndOfData `retry` 字段 |
|
|
||||||
| `RPKI_RTR_TIMING_EXPIRE_SECS` | `7200` | RTR EndOfData `expire` 字段,必须大于 refresh 和 retry |
|
|
||||||
| `RPKI_RTR_MAX_CONNECTIONS` | `512` | 最大并发 RTR 连接数 |
|
|
||||||
| `RPKI_RTR_MAX_CONCURRENT_HANDSHAKES` | `128` | 最大并发握手数,不能大于最大连接数 |
|
|
||||||
| `RPKI_RTR_NOTIFY_QUEUE_SIZE` | `1024` | Serial Notify 广播队列大小 |
|
|
||||||
| `RPKI_RTR_TCP_KEEPALIVE_SECS` | `60` | TCP keepalive;设为 `0` 表示禁用 |
|
|
||||||
| `RPKI_RTR_WARN_INSECURE_TCP` | `true` | TCP 模式是否输出安全提示 |
|
|
||||||
| `RPKI_RTR_REQUIRE_TLS_SERVER_DNS_NAME_SAN` | `false` | 是否要求 TLS 服务端证书包含 dNSName SAN |
|
|
||||||
| `RPKI_RTR_ENFORCE_TLS_CLIENT_SAN_IP_MATCH` | `true` | 是否校验 TLS client 证书 SAN IP 与 peer IP 匹配 |
|
|
||||||
| `RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS` | `300` | runtime report 周期 |
|
|
||||||
| `RPKI_RTR_REPORT_HISTORY_LIMIT` | `10` | 每类 report 滚动保留数量 |
|
|
||||||
| `RPKI_RTR_TIMEZONE` | `Asia/Shanghai` | 日志和 report 使用的时区 |
|
|
||||||
| `RPKI_RTR_ADMIN_ADDR` | 未设置 | Admin API 监听地址;未设置时关闭 |
|
|
||||||
| `RPKI_RTR_ADMIN_TOKEN` | 未设置 | Admin API Bearer token;非 loopback 监听时必须设置 |
|
|
||||||
|
|
||||||
TLS 相关路径:
|
|
||||||
|
|
||||||
| 变量 | 默认值 |
|
|
||||||
| --- | --- |
|
|
||||||
| `RPKI_RTR_TLS_CERT_PATH` | `./certs/tls/server-dns.crt` |
|
|
||||||
| `RPKI_RTR_TLS_KEY_PATH` | `./certs/tls/server-dns.key` |
|
|
||||||
| `RPKI_RTR_TLS_CLIENT_CA_PATH` | `./certs/tls/client-ca.crt` |
|
|
||||||
|
|
||||||
SSH 相关配置:
|
|
||||||
|
|
||||||
| 变量 | 默认值 |
|
|
||||||
| --- | --- |
|
|
||||||
| `RPKI_RTR_SSH_HOST_KEY_PATH` | `./certs/ssh/ssh_host_rsa_key` |
|
|
||||||
| `RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH` | `./certs/ssh/rtr-authorized_keys` |
|
|
||||||
| `RPKI_RTR_SSH_USERNAME` | `rpki-rtr` |
|
|
||||||
| `RPKI_RTR_SSH_SUBSYSTEM_NAME` | `rpki-rtr` |
|
|
||||||
| `RPKI_RTR_SSH_AUTH_MODE` | `key` |
|
|
||||||
| `RPKI_RTR_SSH_PASSWORD` | 未设置 |
|
|
||||||
|
|
||||||
## 运行报告
|
|
||||||
|
|
||||||
服务会在 `RPKI_RTR_REPORT_DIR` 下写入 JSON report,并按 `RPKI_RTR_REPORT_HISTORY_LIMIT` 滚动保留:
|
|
||||||
|
|
||||||
- `rtr-source-*.json`:CCR/SLURM source、fingerprint、刷新状态、数据质量、cache 统计。
|
|
||||||
- `rtr-clients-*.json`:client 连接数和连接方式统计。
|
|
||||||
- `rtr-runtime-*.json`:进程状态、服务状态和当前生效的 runtime config。
|
|
||||||
|
|
||||||
## Admin API
|
|
||||||
|
|
||||||
Admin API 默认关闭。设置 `RPKI_RTR_ADMIN_ADDR` 后启用:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export RPKI_RTR_ADMIN_ADDR=127.0.0.1:8323
|
|
||||||
export RPKI_RTR_ADMIN_TOKEN=change-me
|
|
||||||
```
|
|
||||||
|
|
||||||
如果监听非 loopback 地址,例如 `0.0.0.0:8323`,必须设置 `RPKI_RTR_ADMIN_TOKEN`。设置 token 后,请求需要携带:
|
|
||||||
|
|
||||||
```http
|
|
||||||
Authorization: Bearer change-me
|
|
||||||
```
|
|
||||||
|
|
||||||
当前实现的主要接口:
|
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `GET` | `/admin/rtr/health` | 健康检查和能力开关 |
|
|
||||||
| `GET` | `/admin/rtr/config` | 查看 runtime config |
|
|
||||||
| `POST` | `/admin/rtr/config` | 修改部分 runtime config |
|
|
||||||
| `GET` | `/admin/rtr/logs/tail` | 读取 stdout/stderr 日志 tail |
|
|
||||||
| `POST` | `/admin/rtr/slurm/reload` | 触发 source reload |
|
|
||||||
| `GET` | `/admin/rtr/slurm/files` | 列出 SLURM 文件 |
|
|
||||||
| `POST` | `/admin/rtr/slurm/files` | 新增或覆盖 SLURM 文件 |
|
|
||||||
| `GET` | `/admin/rtr/slurm/files/{name}` | 读取 SLURM 文件 |
|
|
||||||
| `PUT` | `/admin/rtr/slurm/files/{name}` | 新增或覆盖指定 SLURM 文件 |
|
|
||||||
| `DELETE` | `/admin/rtr/slurm/files/{name}` | 删除 SLURM 文件 |
|
|
||||||
| `POST` | `/admin/rtr/slurm/files/{name}/enable` | 启用 `.slurm.disabled` 文件 |
|
|
||||||
| `POST` | `/admin/rtr/slurm/files/{name}/disable` | 禁用 `.slurm` 文件 |
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://127.0.0.1:8323/admin/rtr/config \
|
|
||||||
-H "Authorization: Bearer change-me"
|
|
||||||
|
|
||||||
curl -X POST http://127.0.0.1:8323/admin/rtr/config \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Bearer change-me" \
|
|
||||||
-d '{"max_delta": 6, "source_refresh_interval_seconds": 60}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## 调试客户端
|
|
||||||
|
|
||||||
`rtr_debug_client` 用于手动发起 Reset Query 或 Serial Query,并观察服务端返回的 PDU。
|
|
||||||
|
|
||||||
RTR Client 的 Docker 调试、FRR/BIRD 互通验证和常用排障步骤见 `README.client.md`。
|
|
||||||
|
|
||||||
TCP:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo run --bin rtr_debug_client -- 127.0.0.1:323 1 reset
|
|
||||||
cargo run --bin rtr_debug_client -- 127.0.0.1:323 1 serial 42 100
|
|
||||||
```
|
|
||||||
|
|
||||||
TLS:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo run --bin rtr_debug_client -- \
|
|
||||||
127.0.0.1:324 1 reset \
|
|
||||||
--tls \
|
|
||||||
--ca-cert certs/tls/client-ca.crt \
|
|
||||||
--server-name localhost \
|
|
||||||
--client-cert certs/tls/client-good.crt \
|
|
||||||
--client-key certs/tls/client-good.key
|
|
||||||
```
|
|
||||||
|
|
||||||
SSH:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo run --bin rtr_debug_client -- \
|
|
||||||
127.0.0.1:22 1 reset \
|
|
||||||
--ssh \
|
|
||||||
--ssh-user rpki-rtr \
|
|
||||||
--ssh-key certs/ssh/rtr-client.key \
|
|
||||||
--ssh-server-key certs/ssh/ssh_host_rsa_key.pub
|
|
||||||
```
|
|
||||||
|
|
||||||
SSH password auth 也已实现;SSH 模式下 `--ssh-key` 和 `--ssh-password` 二选一,同时必须通过 `--ssh-known-hosts` 或 `--ssh-server-key` 做 host key 校验。
|
|
||||||
|
|
||||||
更多参数见 `src/bin/rtr_debug_client/README.md`。
|
|
||||||
|
|
||||||
## 部署入口
|
|
||||||
|
|
||||||
| 用途 | Compose 文件 |
|
|
||||||
| --- | --- |
|
|
||||||
| Server | `deploy/server/docker-compose.yml` |
|
|
||||||
| Server TCP 示例 | `deploy/server/docker-compose.tcp.yml` |
|
|
||||||
| Server TLS 示例 | `deploy/server/docker-compose.tls.yml` |
|
|
||||||
| Server SSH 示例 | `deploy/server/docker-compose.ssh.yml` |
|
|
||||||
| Debug client | `deploy/client/docker-compose.yml` |
|
|
||||||
| Debug client 多实例 | `deploy/client/docker-compose.clients.yml` |
|
|
||||||
| FRR client | `deploy/frr/docker-compose.yml` |
|
|
||||||
| BIRD client | `deploy/bird/docker-compose.yml` |
|
|
||||||
|
|
||||||
RTR Client 统一使用说明见 `README.client.md`。
|
|
||||||
|
|
||||||
通用操作:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
COMPOSE_FILE=deploy/server/docker-compose.yml
|
|
||||||
docker compose -f "${COMPOSE_FILE}" up -d --build
|
|
||||||
docker compose -f "${COMPOSE_FILE}" ps
|
|
||||||
docker compose -f "${COMPOSE_FILE}" down
|
|
||||||
```
|
|
||||||
|
|
||||||
## 开发说明
|
|
||||||
|
|
||||||
- 代码中的 RTR cache 按协议版本维护状态;v0 只包含 Route Origin,v1 包含 Route Origin 和 Router Key,v2 包含 Route Origin、Router Key 和 ASPA。
|
|
||||||
- 当前 source pipeline 从 CCR 生成 Route Origin 和 ASPA,再可选应用 SLURM。
|
|
||||||
- `RtrStore::save_cache_state_versioned(...)` 是 cache 状态写入 RocksDB 的核心入口,相关边界测试在 `tests/test_store_boundary.rs`。
|
|
||||||
- Admin API 的日志 tail 依赖部署入口写出的 stdout/stderr 日志文件;容器入口脚本会把日志写到 `/app/logs`。
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"slurmVersion": 2,
|
|
||||||
"validationOutputFilters": {
|
|
||||||
"prefixFilters": [
|
|
||||||
{
|
|
||||||
"prefix": "24.0.0.0/8",
|
|
||||||
"comment": "Filter many VRPs in current CCR sample"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"bgpsecFilters": [],
|
|
||||||
"aspaFilters": [
|
|
||||||
{
|
|
||||||
"customerAsn": 80,
|
|
||||||
"comment": "Filter one ASPA known to exist in current CCR sample"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locallyAddedAssertions": {
|
|
||||||
"prefixAssertions": [],
|
|
||||||
"bgpsecAssertions": [],
|
|
||||||
"aspaAssertions": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Build-time image knob.
|
|
||||||
RPKI_BIRD_VERSION=3.2.1
|
|
||||||
|
|
||||||
# TCP mode target endpoint.
|
|
||||||
RPKI_BIRD_RPKI_HOST=rpki-rtr
|
|
||||||
RPKI_BIRD_RPKI_PORT=323
|
|
||||||
|
|
||||||
# SSH mode target endpoint.
|
|
||||||
RPKI_BIRD_SSH_RPKI_HOST=rpki-rtr
|
|
||||||
RPKI_RTR_SSH_PORT=22
|
|
||||||
|
|
||||||
# Config template paths in container.
|
|
||||||
RPKI_BIRD_CONFIG_TEMPLATE_PATH=/config/bird.conf.template
|
|
||||||
RPKI_BIRD_SSH_CONFIG_TEMPLATE_PATH=/config/bird.conf.ssh.template
|
|
||||||
|
|
||||||
# Observation and output knobs.
|
|
||||||
RPKI_BIRD_OBSERVE_PROTO=rpki_tcp
|
|
||||||
RPKI_BIRD_OBSERVE_MODE=interval
|
|
||||||
RPKI_BIRD_OBSERVE_DEBOUNCE_SECS=1
|
|
||||||
RPKI_BIRD_OBSERVE_INTERVAL=30
|
|
||||||
RPKI_BIRD_OBSERVE_ASPA_TABLE=rtr_aspa
|
|
||||||
RPKI_BIRD_OBSERVE_ROA4_TABLE=rtr_roa_v4
|
|
||||||
RPKI_BIRD_OBSERVE_ROA6_TABLE=rtr_roa_v6
|
|
||||||
RPKI_BIRD_OBSERVE_ASPA_COUNT=3
|
|
||||||
RPKI_BIRD_OBSERVE_ROA4_COUNT=3
|
|
||||||
RPKI_BIRD_OBSERVE_ROA6_COUNT=3
|
|
||||||
RPKI_BIRD_SHOW_ASPA=1
|
|
||||||
RPKI_BIRD_SHOW_ROA4=1
|
|
||||||
RPKI_BIRD_SHOW_ROA6=1
|
|
||||||
|
|
||||||
# Host volume mounts.
|
|
||||||
RPKI_BIRD_LOG_HOST_DIR=../../logs/bird
|
|
||||||
RPKI_BIRD_SSH_CERTS_HOST_DIR=../../certs/ssh
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
FROM debian:bookworm-slim AS builder
|
|
||||||
|
|
||||||
ARG BIRD_VERSION=3.2.1
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
build-essential \
|
|
||||||
bison \
|
|
||||||
flex \
|
|
||||||
m4 \
|
|
||||||
perl \
|
|
||||||
ca-certificates \
|
|
||||||
wget \
|
|
||||||
xz-utils \
|
|
||||||
libreadline-dev \
|
|
||||||
libncurses-dev \
|
|
||||||
libssh-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
|
|
||||||
RUN wget -O bird.tar.gz "https://bird.nic.cz/download/bird-${BIRD_VERSION}.tar.gz" \
|
|
||||||
&& tar -xzf bird.tar.gz \
|
|
||||||
&& mv "bird-${BIRD_VERSION}" bird
|
|
||||||
|
|
||||||
WORKDIR /build/bird
|
|
||||||
|
|
||||||
RUN ./configure \
|
|
||||||
--prefix=/usr \
|
|
||||||
--sysconfdir=/etc/bird \
|
|
||||||
--localstatedir=/run \
|
|
||||||
&& make -j"$(nproc)" \
|
|
||||||
&& make install
|
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
ca-certificates \
|
|
||||||
gettext-base \
|
|
||||||
tzdata \
|
|
||||||
netcat-openbsd \
|
|
||||||
libreadline8 \
|
|
||||||
libncurses6 \
|
|
||||||
libtinfo6 \
|
|
||||||
libssh-4 \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
COPY --from=builder /usr/sbin/bird /usr/sbin/bird
|
|
||||||
COPY --from=builder /usr/sbin/birdc /usr/sbin/birdc
|
|
||||||
COPY --from=builder /etc/bird /etc/bird
|
|
||||||
ENV TZ=Asia/Shanghai
|
|
||||||
|
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
|
||||||
RUN chmod +x /entrypoint.sh \
|
|
||||||
&& mkdir -p /run/bird
|
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
# BIRD Minimal RTR Client Config
|
|
||||||
|
|
||||||
This folder provides a minimal BIRD setup for black-box interop testing
|
|
||||||
against this repository's RTR server defaults.
|
|
||||||
|
|
||||||
Server defaults in this repo:
|
|
||||||
- TCP: `0.0.0.0:323`
|
|
||||||
- SSH: `0.0.0.0:22` (when enabled on server)
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
- `Dockerfile`: builds a minimal BIRD2 runtime image.
|
|
||||||
- `bird.conf.example`: sample `/etc/bird/bird.conf`.
|
|
||||||
- `bird.conf.ssh.example`: sample SSH transport `/etc/bird/bird.conf`.
|
|
||||||
- `entrypoint.sh`: starts BIRD in foreground mode.
|
|
||||||
- `docker-compose.yml`: one-click local TCP test client.
|
|
||||||
- `docker-compose.ssh.yml`: compose override for SSH transport.
|
|
||||||
|
|
||||||
By default, the container uses event-driven observation and prints snapshots
|
|
||||||
only when BIRD reports RPKI-related changes.
|
|
||||||
|
|
||||||
## Docker quick start
|
|
||||||
|
|
||||||
From repository root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/bird/docker-compose.yml up --build
|
|
||||||
```
|
|
||||||
|
|
||||||
Use another terminal to inspect:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs -f bird-rpki-client
|
|
||||||
```
|
|
||||||
|
|
||||||
If protocol state is `up`, the RTR client path is working.
|
|
||||||
|
|
||||||
Detached mode:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/bird/docker-compose.yml up -d --build
|
|
||||||
docker logs -f bird-rpki-client
|
|
||||||
```
|
|
||||||
|
|
||||||
Stop:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/bird/docker-compose.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
## SSH quick start
|
|
||||||
|
|
||||||
Start server in SSH mode first:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/server/docker-compose.ssh.yml up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
Start BIRD client with SSH override:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose \
|
|
||||||
-f deploy/bird/docker-compose.yml \
|
|
||||||
-f deploy/bird/docker-compose.ssh.yml \
|
|
||||||
up --build
|
|
||||||
```
|
|
||||||
|
|
||||||
In detached mode, observe with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs -f bird-rpki-client
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- This setup renders `bird.conf.template` at startup and uses `RPKI_HOST` / `RPKI_PORT`
|
|
||||||
for the RTR endpoint.
|
|
||||||
- `network_mode: host` expects your RTR server to be reachable at
|
|
||||||
`host.docker.internal:323` from the container.
|
|
||||||
- Observation is controlled by env vars:
|
|
||||||
`OBSERVE_MODE` (`event` by default, `interval` as fallback),
|
|
||||||
`OBSERVE_DEBOUNCE_SECS` (default `1`),
|
|
||||||
`OBSERVE_INTERVAL` (seconds, used when `OBSERVE_MODE=interval`),
|
|
||||||
and `OBSERVE_PROTO`.
|
|
||||||
- SSH mode mounts `../../certs/ssh` into `/config/ssh` and expects:
|
|
||||||
`rtr-client.key` and `ssh_host_rsa_key.pub`.
|
|
||||||
- Entrypoint auto-generates `/run/bird/known_hosts` from
|
|
||||||
`/config/ssh/ssh_host_rsa_key.pub` for BIRD SSH host-key verification.
|
|
||||||
- If your environment does not support Docker host networking, switch to a
|
|
||||||
bridge network and replace `remote` addresses accordingly.
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
# BIRD 最小化 RTR 客户端配置
|
|
||||||
|
|
||||||
本目录提供最小化 BIRD 配置,用于与本仓库 RTR Server 做黑盒互通测试。
|
|
||||||
|
|
||||||
本仓库默认 RTR 监听地址:
|
|
||||||
- TCP: `0.0.0.0:323`
|
|
||||||
- SSH: `0.0.0.0:22`(仅在 server SSH 模式启用时)
|
|
||||||
|
|
||||||
## 文件说明
|
|
||||||
|
|
||||||
- `Dockerfile`: 构建最小 BIRD 运行镜像(包含 SSH transport 支持)。
|
|
||||||
- `bird.conf.example`: `/etc/bird/bird.conf` 的 TCP 示例。
|
|
||||||
- `bird.conf.ssh.example`: `/etc/bird/bird.conf` 的 SSH transport 示例。
|
|
||||||
- `entrypoint.sh`: 前台启动 BIRD,并周期输出协议快照。
|
|
||||||
- `docker-compose.yml`: TCP 一键启动。
|
|
||||||
- `docker-compose.ssh.yml`: SSH 覆盖配置。
|
|
||||||
|
|
||||||
容器默认每 30 秒向日志输出一次 RPKI 协议状态快照。
|
|
||||||
|
|
||||||
## Docker 快速启动(TCP)
|
|
||||||
|
|
||||||
在仓库根目录执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/bird/docker-compose.yml up --build
|
|
||||||
```
|
|
||||||
|
|
||||||
另开终端查看日志:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs -f bird-rpki-client
|
|
||||||
```
|
|
||||||
|
|
||||||
如果协议状态显示 `up`,说明 RTR 客户端链路正常。
|
|
||||||
|
|
||||||
后台模式:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/bird/docker-compose.yml up -d --build
|
|
||||||
docker logs -f bird-rpki-client
|
|
||||||
```
|
|
||||||
|
|
||||||
停止:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/bird/docker-compose.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
## SSH 快速启动
|
|
||||||
|
|
||||||
先启动 server 的 SSH 模式:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/server/docker-compose.ssh.yml up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
再启动 BIRD SSH 客户端:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose \
|
|
||||||
-f deploy/bird/docker-compose.yml \
|
|
||||||
-f deploy/bird/docker-compose.ssh.yml \
|
|
||||||
up --build
|
|
||||||
```
|
|
||||||
|
|
||||||
查看日志:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs -f bird-rpki-client
|
|
||||||
```
|
|
||||||
|
|
||||||
## 说明
|
|
||||||
|
|
||||||
- 当前 compose 使用 `network_mode: host`,默认通过 `host.docker.internal` 访问 server。
|
|
||||||
- 观测频率由环境变量控制:`OBSERVE_INTERVAL`(秒,默认 `30`)和 `OBSERVE_PROTO`。
|
|
||||||
- SSH 模式会将 `../../certs/ssh` 挂载到容器 `/config/ssh`,并使用:
|
|
||||||
`rtr-client.key`、`ssh_host_rsa_key.pub`。
|
|
||||||
- 入口脚本会基于 `/config/ssh/ssh_host_rsa_key.pub` 自动生成
|
|
||||||
`/run/bird/known_hosts`,用于 BIRD 的 SSH 主机密钥校验。
|
|
||||||
- 如果你运行在 Docker Desktop(非 Linux 原生 host network 场景),建议改为自定义 bridge 网络并将 `remote` 地址改成可达的 server 容器名或宿主机地址。
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
log stderr all;
|
|
||||||
debug protocols all;
|
|
||||||
router id 192.0.2.2;
|
|
||||||
|
|
||||||
roa4 table rtr_roa_v4;
|
|
||||||
roa6 table rtr_roa_v6;
|
|
||||||
aspa table rtr_aspa;
|
|
||||||
|
|
||||||
protocol device {
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol rpki rpki_ssh {
|
|
||||||
roa4 { table rtr_roa_v4; };
|
|
||||||
roa6 { table rtr_roa_v6; };
|
|
||||||
aspa { table rtr_aspa; };
|
|
||||||
|
|
||||||
remote "host.docker.internal" port 22;
|
|
||||||
|
|
||||||
min version 2;
|
|
||||||
max version 2;
|
|
||||||
|
|
||||||
refresh 3600;
|
|
||||||
retry 600;
|
|
||||||
expire 7200;
|
|
||||||
|
|
||||||
transport ssh {
|
|
||||||
user "rpki-rtr";
|
|
||||||
bird private key "/config/ssh/rtr-client.key";
|
|
||||||
remote public key "/run/bird/known_hosts";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
log stderr all;
|
|
||||||
router id 192.0.2.2;
|
|
||||||
|
|
||||||
roa4 table rtr_roa_v4;
|
|
||||||
roa6 table rtr_roa_v6;
|
|
||||||
aspa table rtr_aspa;
|
|
||||||
|
|
||||||
protocol device {
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol rpki rpki_tcp {
|
|
||||||
roa4 { table rtr_roa_v4; };
|
|
||||||
roa6 { table rtr_roa_v6; };
|
|
||||||
aspa { table rtr_aspa; };
|
|
||||||
|
|
||||||
remote "host.docker.internal" port 323;
|
|
||||||
|
|
||||||
min version 2;
|
|
||||||
max version 2;
|
|
||||||
|
|
||||||
refresh 3600;
|
|
||||||
retry 600;
|
|
||||||
expire 7200;
|
|
||||||
|
|
||||||
transport tcp {
|
|
||||||
authentication none;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
log stderr all;
|
|
||||||
router id 192.0.2.2;
|
|
||||||
|
|
||||||
roa4 table rtr_roa_v4;
|
|
||||||
roa6 table rtr_roa_v6;
|
|
||||||
aspa table rtr_aspa;
|
|
||||||
|
|
||||||
protocol device {
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol rpki rpki_tcp {
|
|
||||||
roa4 { table rtr_roa_v4; };
|
|
||||||
roa6 { table rtr_roa_v6; };
|
|
||||||
aspa { table rtr_aspa; };
|
|
||||||
|
|
||||||
remote "host.docker.internal" port 323;
|
|
||||||
|
|
||||||
min version 2;
|
|
||||||
max version 2;
|
|
||||||
|
|
||||||
refresh 3600;
|
|
||||||
retry 600;
|
|
||||||
expire 7200;
|
|
||||||
|
|
||||||
transport tcp {
|
|
||||||
authentication none;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
log stderr all;
|
|
||||||
router id 192.0.2.2;
|
|
||||||
|
|
||||||
roa4 table rtr_roa_v4;
|
|
||||||
roa6 table rtr_roa_v6;
|
|
||||||
aspa table rtr_aspa;
|
|
||||||
|
|
||||||
protocol device {
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol rpki rpki_ssh {
|
|
||||||
roa4 { table rtr_roa_v4; };
|
|
||||||
roa6 { table rtr_roa_v6; };
|
|
||||||
aspa { table rtr_aspa; };
|
|
||||||
|
|
||||||
remote "host.docker.internal" port 22;
|
|
||||||
|
|
||||||
min version 2;
|
|
||||||
max version 2;
|
|
||||||
|
|
||||||
refresh 3600;
|
|
||||||
retry 600;
|
|
||||||
expire 7200;
|
|
||||||
|
|
||||||
transport ssh {
|
|
||||||
user "rpki-rtr";
|
|
||||||
bird private key "/config/ssh/rtr-client.key";
|
|
||||||
remote public key "/run/bird/known_hosts";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
log stderr all;
|
|
||||||
router id 192.0.2.2;
|
|
||||||
|
|
||||||
roa4 table rtr_roa_v4;
|
|
||||||
roa6 table rtr_roa_v6;
|
|
||||||
aspa table rtr_aspa;
|
|
||||||
|
|
||||||
protocol device {
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol rpki rpki_ssh {
|
|
||||||
roa4 { table rtr_roa_v4; };
|
|
||||||
roa6 { table rtr_roa_v6; };
|
|
||||||
aspa { table rtr_aspa; };
|
|
||||||
|
|
||||||
remote "${RPKI_HOST}" port ${RPKI_PORT};
|
|
||||||
|
|
||||||
min version 2;
|
|
||||||
max version 2;
|
|
||||||
|
|
||||||
refresh 3600;
|
|
||||||
retry 600;
|
|
||||||
expire 7200;
|
|
||||||
|
|
||||||
transport ssh {
|
|
||||||
user "rpki-rtr";
|
|
||||||
bird private key "/config/ssh/rtr-client.key";
|
|
||||||
remote public key "/run/bird/known_hosts";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
log stderr all;
|
|
||||||
router id 192.0.2.2;
|
|
||||||
|
|
||||||
roa4 table rtr_roa_v4;
|
|
||||||
roa6 table rtr_roa_v6;
|
|
||||||
aspa table rtr_aspa;
|
|
||||||
|
|
||||||
protocol device {
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol rpki rpki_tcp {
|
|
||||||
roa4 { table rtr_roa_v4; };
|
|
||||||
roa6 { table rtr_roa_v6; };
|
|
||||||
aspa { table rtr_aspa; };
|
|
||||||
|
|
||||||
remote "${RPKI_HOST}" port ${RPKI_PORT};
|
|
||||||
|
|
||||||
min version 2;
|
|
||||||
max version 2;
|
|
||||||
|
|
||||||
refresh 3600;
|
|
||||||
retry 600;
|
|
||||||
expire 7200;
|
|
||||||
|
|
||||||
transport tcp {
|
|
||||||
authentication none;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
services:
|
|
||||||
bird-rpki-client:
|
|
||||||
environment:
|
|
||||||
BIRD_CONFIG_TEMPLATE_PATH: "${RPKI_BIRD_SSH_CONFIG_TEMPLATE_PATH:-/config/bird.conf.ssh.template}"
|
|
||||||
RPKI_HOST: "${RPKI_BIRD_SSH_RPKI_HOST:-rpki-rtr}"
|
|
||||||
RPKI_PORT: "${RPKI_RTR_SSH_PORT:-22}"
|
|
||||||
OBSERVE_PROTO: "rpki_ssh"
|
|
||||||
volumes:
|
|
||||||
- ./bird.conf.ssh.template:/config/bird.conf.ssh.template:ro
|
|
||||||
- ${RPKI_BIRD_SSH_CERTS_HOST_DIR:-../../certs/ssh}:/config/ssh:ro
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
services:
|
|
||||||
bird-rpki-client:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
args:
|
|
||||||
BIRD_VERSION: "${RPKI_BIRD_VERSION:-3.2.1}"
|
|
||||||
container_name: bird-rpki-client
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
BIRD_CONFIG_TEMPLATE_PATH: "${RPKI_BIRD_CONFIG_TEMPLATE_PATH:-/config/bird.conf.template}"
|
|
||||||
|
|
||||||
RPKI_HOST: "${RPKI_BIRD_RPKI_HOST:-rpki-rtr}"
|
|
||||||
RPKI_PORT: "${RPKI_BIRD_RPKI_PORT:-323}"
|
|
||||||
|
|
||||||
OBSERVE_PROTO: "${RPKI_BIRD_OBSERVE_PROTO:-rpki_tcp}"
|
|
||||||
OBSERVE_MODE: "${RPKI_BIRD_OBSERVE_MODE:-interval}"
|
|
||||||
OBSERVE_DEBOUNCE_SECS: "${RPKI_BIRD_OBSERVE_DEBOUNCE_SECS:-1}"
|
|
||||||
OBSERVE_INTERVAL: "${RPKI_BIRD_OBSERVE_INTERVAL:-30}"
|
|
||||||
|
|
||||||
OBSERVE_ASPA_TABLE: "${RPKI_BIRD_OBSERVE_ASPA_TABLE:-rtr_aspa}"
|
|
||||||
OBSERVE_ROA4_TABLE: "${RPKI_BIRD_OBSERVE_ROA4_TABLE:-rtr_roa_v4}"
|
|
||||||
OBSERVE_ROA6_TABLE: "${RPKI_BIRD_OBSERVE_ROA6_TABLE:-rtr_roa_v6}"
|
|
||||||
|
|
||||||
OBSERVE_ASPA_COUNT: "${RPKI_BIRD_OBSERVE_ASPA_COUNT:-3}"
|
|
||||||
OBSERVE_ROA4_COUNT: "${RPKI_BIRD_OBSERVE_ROA4_COUNT:-3}"
|
|
||||||
OBSERVE_ROA6_COUNT: "${RPKI_BIRD_OBSERVE_ROA6_COUNT:-3}"
|
|
||||||
|
|
||||||
SHOW_ASPA: "${RPKI_BIRD_SHOW_ASPA:-1}"
|
|
||||||
SHOW_ROA4: "${RPKI_BIRD_SHOW_ROA4:-1}"
|
|
||||||
SHOW_ROA6: "${RPKI_BIRD_SHOW_ROA6:-1}"
|
|
||||||
volumes:
|
|
||||||
- ./bird.conf.template:/config/bird.conf.template:ro
|
|
||||||
- ${RPKI_BIRD_LOG_HOST_DIR:-../../logs/bird}:/app/logs
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
mkdir -p /run/bird
|
|
||||||
|
|
||||||
SOCK_PATH="/run/bird/bird.ctl"
|
|
||||||
PROTO="${OBSERVE_PROTO:-rpki_tcp}"
|
|
||||||
INTERVAL="${OBSERVE_INTERVAL:-30}"
|
|
||||||
MODE="${OBSERVE_MODE:-event}"
|
|
||||||
DEBOUNCE_SECS="${OBSERVE_DEBOUNCE_SECS:-1}"
|
|
||||||
RPKI_HOST="${RPKI_HOST:-host.docker.internal}"
|
|
||||||
RPKI_PORT="${RPKI_PORT:-323}"
|
|
||||||
|
|
||||||
BIRD_CONFIG_PATH="${BIRD_CONFIG_PATH:-/config/bird.conf}"
|
|
||||||
BIRD_CONFIG_TEMPLATE_PATH="${BIRD_CONFIG_TEMPLATE_PATH:-/config/bird.conf.template}"
|
|
||||||
|
|
||||||
ASPA_TABLE="${OBSERVE_ASPA_TABLE:-rtr_aspa}"
|
|
||||||
ROA4_TABLE="${OBSERVE_ROA4_TABLE:-rtr_roa_v4}"
|
|
||||||
ROA6_TABLE="${OBSERVE_ROA6_TABLE:-rtr_roa_v6}"
|
|
||||||
|
|
||||||
ASPA_COUNT="${OBSERVE_ASPA_COUNT:-3}"
|
|
||||||
ROA4_COUNT="${OBSERVE_ROA4_COUNT:-3}"
|
|
||||||
ROA6_COUNT="${OBSERVE_ROA6_COUNT:-3}"
|
|
||||||
|
|
||||||
SHOW_ASPA="${SHOW_ASPA:-1}"
|
|
||||||
SHOW_ROA4="${SHOW_ROA4:-1}"
|
|
||||||
SHOW_ROA6="${SHOW_ROA6:-1}"
|
|
||||||
SSH_HOST_PUBKEY_PATH="${SSH_HOST_PUBKEY_PATH:-/config/ssh/ssh_host_rsa_key.pub}"
|
|
||||||
SSH_KNOWN_HOSTS_PATH="${SSH_KNOWN_HOSTS_PATH:-/run/bird/known_hosts}"
|
|
||||||
|
|
||||||
LOG_DIR="${LOG_DIR:-/app/logs}"
|
|
||||||
LOG_NAME="${LOG_NAME:-${HOSTNAME:-bird-rpki-client}}"
|
|
||||||
STDOUT_LOG="${LOG_DIR}/${LOG_NAME}.stdout.log"
|
|
||||||
STDERR_LOG="${LOG_DIR}/${LOG_NAME}.stderr.log"
|
|
||||||
|
|
||||||
mkdir -p "$LOG_DIR"
|
|
||||||
exec >>"$STDOUT_LOG" 2>>"$STDERR_LOG"
|
|
||||||
|
|
||||||
if [ -r "$BIRD_CONFIG_TEMPLATE_PATH" ]; then
|
|
||||||
RENDERED_CONFIG_PATH="/run/bird/bird.generated.conf"
|
|
||||||
envsubst '${RPKI_HOST} ${RPKI_PORT}' < "$BIRD_CONFIG_TEMPLATE_PATH" > "$RENDERED_CONFIG_PATH"
|
|
||||||
BIRD_CONFIG_PATH="$RENDERED_CONFIG_PATH"
|
|
||||||
echo "[entrypoint] rendered config : $BIRD_CONFIG_TEMPLATE_PATH -> $BIRD_CONFIG_PATH"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ensure_ssh_known_hosts() {
|
|
||||||
if [ -s "$SSH_KNOWN_HOSTS_PATH" ]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -r "$SSH_HOST_PUBKEY_PATH" ]; then
|
|
||||||
echo "[entrypoint] WARNING: SSH host key file not found: $SSH_HOST_PUBKEY_PATH"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
set -- $(awk 'NF >= 2 { print $1, $2; exit }' "$SSH_HOST_PUBKEY_PATH")
|
|
||||||
if [ $# -ne 2 ]; then
|
|
||||||
echo "[entrypoint] WARNING: invalid SSH host key format in $SSH_HOST_PUBKEY_PATH"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
key_type="$1"
|
|
||||||
key_data="$2"
|
|
||||||
|
|
||||||
if echo "$key_type" | grep -q '^ssh-'; then
|
|
||||||
{
|
|
||||||
echo "$RPKI_HOST $key_type $key_data"
|
|
||||||
echo "[$RPKI_HOST]:$RPKI_PORT $key_type $key_data"
|
|
||||||
} > "$SSH_KNOWN_HOSTS_PATH"
|
|
||||||
else
|
|
||||||
cp "$SSH_HOST_PUBKEY_PATH" "$SSH_KNOWN_HOSTS_PATH"
|
|
||||||
fi
|
|
||||||
|
|
||||||
chmod 600 "$SSH_KNOWN_HOSTS_PATH" || true
|
|
||||||
echo "[entrypoint] generated known_hosts: $SSH_KNOWN_HOSTS_PATH"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_first_n_objects() {
|
|
||||||
table_name="$1"
|
|
||||||
max_objects="$2"
|
|
||||||
|
|
||||||
birdc -s "$SOCK_PATH" show route table "$table_name" all 2>/dev/null | awk -v max="$max_objects" '
|
|
||||||
BEGIN {
|
|
||||||
count = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 直接跳过空行
|
|
||||||
/^[[:space:]]*$/ {
|
|
||||||
next
|
|
||||||
}
|
|
||||||
|
|
||||||
# 保留 birdc 的表头
|
|
||||||
/^BIRD / {
|
|
||||||
print
|
|
||||||
next
|
|
||||||
}
|
|
||||||
|
|
||||||
/^Table / {
|
|
||||||
print
|
|
||||||
next
|
|
||||||
}
|
|
||||||
|
|
||||||
# 非缩进且不是表头,视为一个新对象的开始
|
|
||||||
/^[^[:space:]]/ {
|
|
||||||
count++
|
|
||||||
if (count > max) {
|
|
||||||
exit
|
|
||||||
}
|
|
||||||
print
|
|
||||||
next
|
|
||||||
}
|
|
||||||
|
|
||||||
# 缩进内容,只有在已进入前 max 个对象时才打印
|
|
||||||
{
|
|
||||||
if (count > 0 && count <= max) {
|
|
||||||
print
|
|
||||||
}
|
|
||||||
}
|
|
||||||
' || true
|
|
||||||
}
|
|
||||||
|
|
||||||
print_snapshot() {
|
|
||||||
echo "==== $(date +"%Y-%m-%dT%H:%M:%S%:z") RPKI snapshot ($PROTO) ===="
|
|
||||||
birdc -s "$SOCK_PATH" show protocols all "$PROTO" || true
|
|
||||||
|
|
||||||
if [ "$SHOW_ASPA" = "1" ]; then
|
|
||||||
echo "---- ASPA table ($ASPA_TABLE, first ${ASPA_COUNT} objects) ----"
|
|
||||||
print_first_n_objects "$ASPA_TABLE" "$ASPA_COUNT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$SHOW_ROA4" = "1" ]; then
|
|
||||||
echo "---- ROA4 table ($ROA4_TABLE, first ${ROA4_COUNT} objects) ----"
|
|
||||||
print_first_n_objects "$ROA4_TABLE" "$ROA4_COUNT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$SHOW_ROA6" = "1" ]; then
|
|
||||||
echo "---- ROA6 table ($ROA6_TABLE, first ${ROA6_COUNT} objects) ----"
|
|
||||||
print_first_n_objects "$ROA6_TABLE" "$ROA6_COUNT"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
is_rpki_related_event() {
|
|
||||||
line="$1"
|
|
||||||
echo "$line" | grep -Eiq "$PROTO|$ASPA_TABLE|$ROA4_TABLE|$ROA6_TABLE|rpki|rtr"
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "[entrypoint] starting bird"
|
|
||||||
echo "[entrypoint] config : $BIRD_CONFIG_PATH"
|
|
||||||
echo "[entrypoint] observe proto : $PROTO"
|
|
||||||
echo "[entrypoint] observe mode : $MODE"
|
|
||||||
echo "[entrypoint] observe interval : $INTERVAL (used when mode=interval)"
|
|
||||||
echo "[entrypoint] debounce secs : $DEBOUNCE_SECS (used when mode=event)"
|
|
||||||
echo "[entrypoint] target : $RPKI_HOST:$RPKI_PORT"
|
|
||||||
echo "[entrypoint] show aspa : $SHOW_ASPA ($ASPA_TABLE, first $ASPA_COUNT objects)"
|
|
||||||
echo "[entrypoint] show roa4 : $SHOW_ROA4 ($ROA4_TABLE, first $ROA4_COUNT objects)"
|
|
||||||
echo "[entrypoint] show roa6 : $SHOW_ROA6 ($ROA6_TABLE, first $ROA6_COUNT objects)"
|
|
||||||
|
|
||||||
if [ "$PROTO" = "rpki_ssh" ] || grep -q 'transport[[:space:]]\+ssh' "$BIRD_CONFIG_PATH"; then
|
|
||||||
ensure_ssh_known_hosts
|
|
||||||
fi
|
|
||||||
|
|
||||||
if nc -zvw3 "$RPKI_HOST" "$RPKI_PORT"; then
|
|
||||||
echo "[entrypoint] TCP connectivity to $RPKI_HOST:$RPKI_PORT OK"
|
|
||||||
else
|
|
||||||
echo "[entrypoint] WARNING: cannot connect to $RPKI_HOST:$RPKI_PORT before BIRD starts"
|
|
||||||
fi
|
|
||||||
|
|
||||||
bird -f -c "$BIRD_CONFIG_PATH" -s "$SOCK_PATH" &
|
|
||||||
BIRD_PID="$!"
|
|
||||||
|
|
||||||
sleep 1
|
|
||||||
|
|
||||||
case "$INTERVAL" in
|
|
||||||
''|*[!0-9]*)
|
|
||||||
INTERVAL=0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case "$MODE" in
|
|
||||||
event)
|
|
||||||
case "$DEBOUNCE_SECS" in
|
|
||||||
''|*[!0-9]*)
|
|
||||||
DEBOUNCE_SECS=1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "[entrypoint] waiting for BIRD monitor events and only printing on RPKI-related changes"
|
|
||||||
LAST_PRINT_TS=0
|
|
||||||
|
|
||||||
if birdc -s "$SOCK_PATH" monitor all 2>/dev/null | while IFS= read -r line; do
|
|
||||||
[ -n "$line" ] || continue
|
|
||||||
if ! is_rpki_related_event "$line"; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
now_ts="$(date +%s)"
|
|
||||||
if [ $((now_ts - LAST_PRINT_TS)) -lt "$DEBOUNCE_SECS" ]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
LAST_PRINT_TS="$now_ts"
|
|
||||||
echo "[monitor] $line"
|
|
||||||
print_snapshot
|
|
||||||
done; then
|
|
||||||
:
|
|
||||||
else
|
|
||||||
echo "[entrypoint] WARNING: birdc monitor failed, fallback to interval polling"
|
|
||||||
MODE="interval"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ "$MODE" = "interval" ] && [ "$INTERVAL" -gt 0 ]; then
|
|
||||||
while kill -0 "$BIRD_PID" 2>/dev/null; do
|
|
||||||
print_snapshot
|
|
||||||
sleep "$INTERVAL"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
wait "$BIRD_PID"
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Target RTR server addresses for client compose files
|
|
||||||
# build-run.sh auto-detects amd64/arm64 unless IMAGE_ARCH/RPKI_RTR_IMAGE_ARCH is set before running it.
|
|
||||||
# Direct docker compose uses RPKI_RTR_IMAGE_ARCH from this file; set it to arm64 on ARM hosts.
|
|
||||||
RPKI_RTR_CLIENT_IMAGE_BASE=rpki-rtr-debug-client
|
|
||||||
RPKI_RTR_CLIENT_IMAGE_TAG=v1.3
|
|
||||||
RPKI_RTR_IMAGE_ARCH=arm64
|
|
||||||
# RPKI_RTR_TARGET_PLATFORM=linux/amd64
|
|
||||||
# RPKI_RTR_CLIENT_IMAGE=rpki-rtr-debug-client-amd64:v1.3
|
|
||||||
|
|
||||||
# TCP example: 10.0.0.12:323
|
|
||||||
# TLS example: rpki.example.com:324
|
|
||||||
# SSH example: 10.0.0.12:22
|
|
||||||
RPKI_RTR_TCP_SERVER_ADDR=rpki-rtr:323
|
|
||||||
RPKI_RTR_TLS_SERVER_ADDR=rpki-rtr:324
|
|
||||||
RPKI_RTR_SSH_SERVER_ADDR=rpki-rtr:22
|
|
||||||
|
|
||||||
# RTR protocol version used as client command second argument (supported: 0,1,2)
|
|
||||||
RPKI_RTR_PROTOCOL_VERSION=2
|
|
||||||
|
|
||||||
# TLS server name used by --server-name in TLS mode
|
|
||||||
# Must match server certificate SAN dNSName.
|
|
||||||
RPKI_RTR_TLS_SERVER_NAME=localhost
|
|
||||||
RPKI_RTR_TLS_CA_CERT_PATH=/app/certs/client-ca.crt
|
|
||||||
RPKI_RTR_TLS_CLIENT_CERT_PATH=/app/certs/client-good.crt
|
|
||||||
RPKI_RTR_TLS_CLIENT_KEY_PATH=/app/certs/client-good.key
|
|
||||||
RPKI_RTR_TLS_CERTS_HOST_DIR=../../certs/tls
|
|
||||||
|
|
||||||
# Shared client logs mount on host.
|
|
||||||
RPKI_RTR_CLIENT_LOG_HOST_DIR=../../logs/client
|
|
||||||
|
|
||||||
# SSH mode examples:
|
|
||||||
# RPKI_RTR_SSH_SERVER_ADDR=10.0.0.12:2222
|
|
||||||
RPKI_RTR_CLIENT_KEYS_VOLUME=../../certs/ssh:/app/certs:ro
|
|
||||||
RPKI_RTR_CLIENT_KEY_PATH=/app/certs/rtr-client.key
|
|
||||||
RPKI_RTR_SSH_SERVER_PUBKEY_PATH=/app/certs/ssh_host_rsa_key.pub
|
|
||||||
RPKI_RTR_SSH_USERNAME=rpki-rtr
|
|
||||||
RPKI_RTR_SSH_PASSWORD=
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
FROM rust:1.89-bookworm AS builder
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends clang libclang-dev pkg-config \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
COPY Cargo.toml Cargo.lock ./
|
|
||||||
COPY src ./src
|
|
||||||
|
|
||||||
RUN cargo build --release --bin rtr_debug_client
|
|
||||||
|
|
||||||
FROM debian:bookworm-slim AS runtime
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends ca-certificates tzdata \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
ENV TZ=Asia/Shanghai
|
|
||||||
|
|
||||||
COPY --from=builder /build/target/release/rtr_debug_client /usr/local/bin/rtr_debug_client
|
|
||||||
COPY --chmod=755 deploy/client/entrypoint.sh /usr/local/bin/rtr-debug-client-entrypoint.sh
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/rtr-debug-client-entrypoint.sh"]
|
|
||||||
@@ -1,314 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)"
|
|
||||||
CALLER_TARGET_PLATFORM="${TARGET_PLATFORM:-${RPKI_RTR_TARGET_PLATFORM:-}}"
|
|
||||||
CALLER_IMAGE_ARCH="${IMAGE_ARCH:-${RPKI_RTR_IMAGE_ARCH:-}}"
|
|
||||||
CALLER_CLIENT_IMAGE="${CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE:-}}"
|
|
||||||
|
|
||||||
load_env_defaults() {
|
|
||||||
local line key value
|
|
||||||
[[ -f "${SCRIPT_DIR}/.env" ]] || return
|
|
||||||
|
|
||||||
while IFS= read -r line || [[ -n "${line}" ]]; do
|
|
||||||
line="${line%$'\r'}"
|
|
||||||
[[ -z "${line}" || "${line}" =~ ^[[:space:]]*# ]] && continue
|
|
||||||
[[ "${line}" == *"="* ]] || continue
|
|
||||||
|
|
||||||
key="${line%%=*}"
|
|
||||||
value="${line#*=}"
|
|
||||||
key="${key#"${key%%[![:space:]]*}"}"
|
|
||||||
key="${key%"${key##*[![:space:]]}"}"
|
|
||||||
[[ "${key}" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
|
|
||||||
|
|
||||||
if [[ -z "${!key+x}" ]]; then
|
|
||||||
export "${key}=${value}"
|
|
||||||
fi
|
|
||||||
done < "${SCRIPT_DIR}/.env"
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_image_arch() {
|
|
||||||
case "$(uname -m)" in
|
|
||||||
x86_64|amd64) printf '%s\n' amd64 ;;
|
|
||||||
aarch64|arm64) printf '%s\n' arm64 ;;
|
|
||||||
*) die "unsupported host architecture '$(uname -m)'; set RPKI_RTR_IMAGE_ARCH=amd64 or arm64" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
platform_for_image_arch() {
|
|
||||||
case "$1" in
|
|
||||||
amd64) printf '%s\n' linux/amd64 ;;
|
|
||||||
arm64) printf '%s\n' linux/arm64 ;;
|
|
||||||
*) die "unsupported image arch '$1', expected amd64 or arm64" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
image_arch_from_platform() {
|
|
||||||
case "$1" in
|
|
||||||
linux/amd64|amd64) printf '%s\n' amd64 ;;
|
|
||||||
linux/arm64|arm64) printf '%s\n' arm64 ;;
|
|
||||||
*) return 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
load_env_defaults
|
|
||||||
|
|
||||||
MODE="${MODE:-tcp}"
|
|
||||||
ACTION="up"
|
|
||||||
DETACH="${DETACH:-1}"
|
|
||||||
BUILD="${BUILD:-1}"
|
|
||||||
CLIENTS="${CLIENTS:-single}"
|
|
||||||
TARGET_PLATFORM="${CALLER_TARGET_PLATFORM}"
|
|
||||||
IMAGE_ARCH="${CALLER_IMAGE_ARCH}"
|
|
||||||
CLIENT_IMAGE="${CALLER_CLIENT_IMAGE}"
|
|
||||||
BUILDX_PUSH="${BUILDX_PUSH:-0}"
|
|
||||||
BUILDX_BUILDER="${BUILDX_BUILDER:-}"
|
|
||||||
SKIP_BASE_IMAGE_PULL="${SKIP_BASE_IMAGE_PULL:-0}"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage:
|
|
||||||
deploy/client/build-run.sh [action] [options]
|
|
||||||
|
|
||||||
Actions:
|
|
||||||
up Build and run debug client (default)
|
|
||||||
build Build debug client image
|
|
||||||
buildx Build debug client image with docker buildx
|
|
||||||
rebuild Rebuild image without cache, then run client
|
|
||||||
down|stop Stop client compose stack
|
|
||||||
restart Stop, then build and run client
|
|
||||||
logs Follow client logs
|
|
||||||
ps Show client compose status
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--mode MODE base|tcp|tls|ssh (default: tcp)
|
|
||||||
--clients MODE single|multi (default: single)
|
|
||||||
--platform VALUE Target platform for buildx (default: host architecture)
|
|
||||||
--image-arch VALUE
|
|
||||||
Image architecture suffix for default tag: amd64|arm64
|
|
||||||
--client-image TAG
|
|
||||||
Client image tag for buildx and compose
|
|
||||||
--push Push buildx image instead of loading it locally
|
|
||||||
--builder NAME buildx builder name
|
|
||||||
--skip-base-pull Do not pull Dockerfile base images before buildx
|
|
||||||
--foreground Run single client in foreground
|
|
||||||
--no-build Start without --build
|
|
||||||
-h|--help Show this help
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
echo "error: $*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
has_compose() {
|
|
||||||
docker compose version >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
has_buildx() {
|
|
||||||
docker buildx version >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
pull_base_images() {
|
|
||||||
[[ "${SKIP_BASE_IMAGE_PULL}" == "1" ]] && return
|
|
||||||
|
|
||||||
local platform image
|
|
||||||
local -a platforms images
|
|
||||||
IFS=',' read -r -a platforms <<< "${TARGET_PLATFORM}"
|
|
||||||
images=(rust:1.89-bookworm debian:bookworm-slim)
|
|
||||||
|
|
||||||
for platform in "${platforms[@]}"; do
|
|
||||||
for image in "${images[@]}"; do
|
|
||||||
docker pull --platform "${platform}" "${image}"
|
|
||||||
done
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
compose_files() {
|
|
||||||
if [[ "${CLIENTS}" == "multi" ]]; then
|
|
||||||
[[ "${MODE}" == "tcp" || "${MODE}" == "base" ]] || die "--clients multi is only defined for tcp/base mode"
|
|
||||||
printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.clients.yml"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "${MODE}" in
|
|
||||||
base) printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.yml" ;;
|
|
||||||
tcp) printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.tcp.yml" ;;
|
|
||||||
tls) printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.tls.yml" ;;
|
|
||||||
ssh) printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.ssh.yml" ;;
|
|
||||||
*) die "unsupported mode '${MODE}', expected base|tcp|tls|ssh" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
compose_args() {
|
|
||||||
local -a args files
|
|
||||||
args=()
|
|
||||||
[[ -f "${SCRIPT_DIR}/.env" ]] && args+=(--env-file "${SCRIPT_DIR}/.env")
|
|
||||||
mapfile -t files < <(compose_files)
|
|
||||||
args+=("${files[@]}")
|
|
||||||
printf '%s\n' "${args[@]}"
|
|
||||||
}
|
|
||||||
|
|
||||||
compose_run() {
|
|
||||||
local -a args
|
|
||||||
mapfile -t args < <(compose_args)
|
|
||||||
docker compose "${args[@]}" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
up_client() {
|
|
||||||
local -a up_args
|
|
||||||
up_args=(up)
|
|
||||||
[[ "${CLIENTS}" == "multi" || "${DETACH}" == "1" ]] && up_args+=(-d)
|
|
||||||
[[ "${BUILD}" == "1" && "${CLIENTS}" != "multi" ]] && up_args+=(--build)
|
|
||||||
compose_run "${up_args[@]}"
|
|
||||||
}
|
|
||||||
|
|
||||||
build_client() {
|
|
||||||
if [[ "${CLIENTS}" == "multi" ]]; then
|
|
||||||
docker build "$@" -f "${SCRIPT_DIR}/Dockerfile" -t "${CLIENT_IMAGE}" "${REPO_ROOT}"
|
|
||||||
else
|
|
||||||
compose_run build "$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
buildx_client() {
|
|
||||||
has_buildx || die "docker buildx is not available"
|
|
||||||
if [[ "${TARGET_PLATFORM}" == *,* && "${BUILDX_PUSH}" != "1" ]]; then
|
|
||||||
die "multi-platform buildx output requires --push"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local -a output_args builder_args
|
|
||||||
[[ "${BUILDX_PUSH}" == "1" ]] && output_args=(--push) || output_args=(--load)
|
|
||||||
[[ -n "${BUILDX_BUILDER}" ]] && builder_args=(--builder "${BUILDX_BUILDER}") || builder_args=()
|
|
||||||
|
|
||||||
pull_base_images
|
|
||||||
|
|
||||||
docker buildx build "${builder_args[@]}" \
|
|
||||||
--platform "${TARGET_PLATFORM}" \
|
|
||||||
-f "${SCRIPT_DIR}/Dockerfile" \
|
|
||||||
-t "${CLIENT_IMAGE}" \
|
|
||||||
"${output_args[@]}" \
|
|
||||||
"${REPO_ROOT}"
|
|
||||||
}
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
up|build|buildx|rebuild|down|stop|restart|logs|ps)
|
|
||||||
ACTION="$1"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--mode)
|
|
||||||
[[ $# -ge 2 ]] || die "--mode requires a value"
|
|
||||||
MODE="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--clients)
|
|
||||||
[[ $# -ge 2 ]] || die "--clients requires a value"
|
|
||||||
CLIENTS="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--platform)
|
|
||||||
[[ $# -ge 2 ]] || die "--platform requires a value"
|
|
||||||
TARGET_PLATFORM="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--image-arch)
|
|
||||||
[[ $# -ge 2 ]] || die "--image-arch requires a value"
|
|
||||||
IMAGE_ARCH="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--client-image)
|
|
||||||
[[ $# -ge 2 ]] || die "--client-image requires a value"
|
|
||||||
CLIENT_IMAGE="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--push)
|
|
||||||
BUILDX_PUSH=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--builder)
|
|
||||||
[[ $# -ge 2 ]] || die "--builder requires a value"
|
|
||||||
BUILDX_BUILDER="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--skip-base-pull)
|
|
||||||
SKIP_BASE_IMAGE_PULL=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--foreground)
|
|
||||||
DETACH=0
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--no-build)
|
|
||||||
BUILD=0
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "unknown argument '$1'"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -z "${TARGET_PLATFORM}" ]]; then
|
|
||||||
[[ -n "${IMAGE_ARCH}" ]] || IMAGE_ARCH="$(detect_image_arch)"
|
|
||||||
TARGET_PLATFORM="$(platform_for_image_arch "${IMAGE_ARCH}")"
|
|
||||||
elif [[ -z "${IMAGE_ARCH}" ]]; then
|
|
||||||
if [[ "${TARGET_PLATFORM}" != *,* ]] && IMAGE_ARCH="$(image_arch_from_platform "${TARGET_PLATFORM}")"; then
|
|
||||||
:
|
|
||||||
else
|
|
||||||
IMAGE_ARCH="$(detect_image_arch)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "${CLIENT_IMAGE}" ]]; then
|
|
||||||
if [[ "${TARGET_PLATFORM}" == *,* ]]; then
|
|
||||||
CLIENT_IMAGE="${RPKI_RTR_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}:${RPKI_RTR_CLIENT_IMAGE_TAG:-latest}"
|
|
||||||
else
|
|
||||||
CLIENT_IMAGE="${RPKI_RTR_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${IMAGE_ARCH}:${RPKI_RTR_CLIENT_IMAGE_TAG:-latest}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
[[ "${CLIENTS}" == "single" || "${CLIENTS}" == "multi" ]] || die "unsupported clients '${CLIENTS}', expected single|multi"
|
|
||||||
export RPKI_RTR_CLIENT_IMAGE="${CLIENT_IMAGE}"
|
|
||||||
if [[ "${ACTION}" == "buildx" ]]; then
|
|
||||||
has_buildx || die "docker buildx is not available"
|
|
||||||
else
|
|
||||||
has_compose || die "docker compose is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd "${REPO_ROOT}"
|
|
||||||
|
|
||||||
case "${ACTION}" in
|
|
||||||
up)
|
|
||||||
up_client
|
|
||||||
;;
|
|
||||||
build)
|
|
||||||
build_client
|
|
||||||
;;
|
|
||||||
buildx)
|
|
||||||
buildx_client
|
|
||||||
;;
|
|
||||||
rebuild)
|
|
||||||
build_client --no-cache
|
|
||||||
up_client
|
|
||||||
;;
|
|
||||||
down|stop)
|
|
||||||
compose_run down
|
|
||||||
;;
|
|
||||||
restart)
|
|
||||||
compose_run down
|
|
||||||
up_client
|
|
||||||
;;
|
|
||||||
logs)
|
|
||||||
compose_run logs -f
|
|
||||||
;;
|
|
||||||
ps)
|
|
||||||
compose_run ps
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
|
||||||
rtr-client-1:
|
|
||||||
image: ${RPKI_RTR_CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_CLIENT_IMAGE_TAG:-latest}}
|
|
||||||
command: ["${RPKI_RTR_TCP_SERVER_ADDR:-rpki-rtr:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
|
||||||
restart: no
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
rtr-client-2:
|
|
||||||
image: ${RPKI_RTR_CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_CLIENT_IMAGE_TAG:-latest}}
|
|
||||||
command: ["${RPKI_RTR_TCP_SERVER_ADDR:-rpki-rtr:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
|
||||||
restart: no
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
rtr-client-3:
|
|
||||||
image: ${RPKI_RTR_CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_CLIENT_IMAGE_TAG:-latest}}
|
|
||||||
command: ["${RPKI_RTR_TCP_SERVER_ADDR:-rpki-rtr:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
|
||||||
restart: no
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
rtr-client-4:
|
|
||||||
image: ${RPKI_RTR_CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_CLIENT_IMAGE_TAG:-latest}}
|
|
||||||
command: ["${RPKI_RTR_TCP_SERVER_ADDR:-rpki-rtr:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
|
||||||
restart: no
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
rtr-client-5:
|
|
||||||
image: ${RPKI_RTR_CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_CLIENT_IMAGE_TAG:-latest}}
|
|
||||||
command: ["${RPKI_RTR_TCP_SERVER_ADDR:-rpki-rtr:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
|
||||||
restart: no
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
|
||||||
rtr-debug-client:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: deploy/client/Dockerfile
|
|
||||||
image: ${RPKI_RTR_CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_CLIENT_IMAGE_TAG:-latest}}
|
|
||||||
command:
|
|
||||||
[
|
|
||||||
"${RPKI_RTR_SSH_SERVER_ADDR:-rpki-rtr:22}",
|
|
||||||
"${RPKI_RTR_PROTOCOL_VERSION:-2}",
|
|
||||||
"reset",
|
|
||||||
"--ssh",
|
|
||||||
"--ssh-user",
|
|
||||||
"${RPKI_RTR_SSH_USERNAME:-rpki-rtr}",
|
|
||||||
"--ssh-password",
|
|
||||||
"${RPKI_RTR_SSH_PASSWORD}",
|
|
||||||
"--ssh-server-key",
|
|
||||||
"${RPKI_RTR_SSH_SERVER_PUBKEY_PATH:-/app/certs/ssh_host_rsa_key.pub}",
|
|
||||||
"--keep-after-error",
|
|
||||||
"--summary-only"
|
|
||||||
]
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CLIENT_KEYS_VOLUME:-../../certs/ssh:/app/certs:ro}
|
|
||||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
|
||||||
restart: no
|
|
||||||
stdin_open: true
|
|
||||||
tty: true
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
|
||||||
rtr-debug-client:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: deploy/client/Dockerfile
|
|
||||||
image: ${RPKI_RTR_CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_CLIENT_IMAGE_TAG:-latest}}
|
|
||||||
command:
|
|
||||||
[
|
|
||||||
"${RPKI_RTR_SSH_SERVER_ADDR:-rpki-rtr:22}",
|
|
||||||
"${RPKI_RTR_PROTOCOL_VERSION:-2}",
|
|
||||||
"reset",
|
|
||||||
"--ssh",
|
|
||||||
"--ssh-user",
|
|
||||||
"${RPKI_RTR_SSH_USERNAME:-rpki-rtr}",
|
|
||||||
"--ssh-key",
|
|
||||||
"${RPKI_RTR_CLIENT_KEY_PATH:-/app/certs/rtr-client.key}",
|
|
||||||
"--ssh-server-key",
|
|
||||||
"${RPKI_RTR_SSH_SERVER_PUBKEY_PATH:-/app/certs/ssh_host_rsa_key.pub}",
|
|
||||||
"--keep-after-error",
|
|
||||||
"--summary-only"
|
|
||||||
]
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CLIENT_KEYS_VOLUME:-../../certs/ssh:/app/certs:ro}
|
|
||||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
|
||||||
restart: no
|
|
||||||
stdin_open: true
|
|
||||||
tty: true
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
services:
|
|
||||||
rtr-debug-client:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: deploy/client/Dockerfile
|
|
||||||
image: ${RPKI_RTR_CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_CLIENT_IMAGE_TAG:-latest}}
|
|
||||||
command: ["${RPKI_RTR_TCP_SERVER_ADDR:-rpki-rtr:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
|
||||||
restart: no
|
|
||||||
stdin_open: true
|
|
||||||
tty: true
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
|
||||||
rtr-debug-client:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: deploy/client/Dockerfile
|
|
||||||
image: ${RPKI_RTR_CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_CLIENT_IMAGE_TAG:-latest}}
|
|
||||||
command:
|
|
||||||
[
|
|
||||||
"${RPKI_RTR_TLS_SERVER_ADDR:-rpki-rtr:324}",
|
|
||||||
"${RPKI_RTR_PROTOCOL_VERSION:-2}",
|
|
||||||
"reset",
|
|
||||||
"--tls",
|
|
||||||
"--ca-cert",
|
|
||||||
"${RPKI_RTR_TLS_CA_CERT_PATH:-/app/certs/client-ca.crt}",
|
|
||||||
"--server-name",
|
|
||||||
"${RPKI_RTR_TLS_SERVER_NAME:-localhost}",
|
|
||||||
"--client-cert",
|
|
||||||
"${RPKI_RTR_TLS_CLIENT_CERT_PATH:-/app/certs/client-good.crt}",
|
|
||||||
"--client-key",
|
|
||||||
"${RPKI_RTR_TLS_CLIENT_KEY_PATH:-/app/certs/client-good.key}",
|
|
||||||
"--keep-after-error",
|
|
||||||
"--summary-only"
|
|
||||||
]
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_TLS_CERTS_HOST_DIR:-../../certs/tls}:/app/certs:ro
|
|
||||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
|
||||||
restart: no
|
|
||||||
stdin_open: true
|
|
||||||
tty: true
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
services:
|
|
||||||
rtr-debug-client:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: deploy/client/Dockerfile
|
|
||||||
image: ${RPKI_RTR_CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_CLIENT_IMAGE_TAG:-latest}}
|
|
||||||
command: ["${RPKI_RTR_TCP_SERVER_ADDR:-rpki-rtr:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
|
||||||
restart: no
|
|
||||||
stdin_open: true
|
|
||||||
tty: true
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
mkdir -p /app/logs
|
|
||||||
|
|
||||||
log_name="${HOSTNAME:-rtr-debug-client}"
|
|
||||||
stdout_log="/app/logs/${log_name}.stdout.log"
|
|
||||||
stderr_log="/app/logs/${log_name}.stderr.log"
|
|
||||||
|
|
||||||
exec /usr/local/bin/rtr_debug_client "$@" >>"$stdout_log" 2>>"$stderr_log"
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# RTR server endpoint used by deploy/frr/frr.conf.template.
|
|
||||||
RPKI_FRR_RPKI_HOST=127.0.0.1
|
|
||||||
RPKI_FRR_RPKI_PORT=323
|
|
||||||
RPKI_FRR_RPKI_PREFERENCE=1
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
# FRR Minimal RTR Client Config
|
|
||||||
|
|
||||||
中文文档: [README.zh.md](./README.zh.md)
|
|
||||||
|
|
||||||
This folder provides a minimal FRR setup for black-box interop testing
|
|
||||||
against this repository's RTR server defaults.
|
|
||||||
|
|
||||||
Server defaults in this repo:
|
|
||||||
- TCP: `0.0.0.0:323`
|
|
||||||
- TLS: `0.0.0.0:324`
|
|
||||||
|
|
||||||
Reference:
|
|
||||||
- `src/main.rs`
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
- `daemons.example`: sample `/etc/frr/daemons`
|
|
||||||
- `frr.conf.example`: static sample `/etc/frr/frr.conf`
|
|
||||||
- `frr.conf.template`: Docker template used to generate `/etc/frr/frr.conf`
|
|
||||||
- `entrypoint.sh`: renders `frr.conf.template` from environment variables before starting FRR
|
|
||||||
- `.env`: default Docker environment values
|
|
||||||
|
|
||||||
## How to apply on an FRR host
|
|
||||||
|
|
||||||
1. Copy `daemons.example` to `/etc/frr/daemons`.
|
|
||||||
2. Copy `frr.conf.example` to `/etc/frr/frr.conf`.
|
|
||||||
3. Restart FRR:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl restart frr
|
|
||||||
```
|
|
||||||
|
|
||||||
## Verify
|
|
||||||
|
|
||||||
```bash
|
|
||||||
vtysh -c "show rpki configuration"
|
|
||||||
vtysh -c "show rpki cache-server"
|
|
||||||
vtysh -c "show rpki cache-connection"
|
|
||||||
vtysh -c "show rpki prefix-table"
|
|
||||||
```
|
|
||||||
|
|
||||||
If `show rpki cache-connection` is connected and `show rpki prefix-table`
|
|
||||||
contains VRPs, the RTR client path is working.
|
|
||||||
|
|
||||||
## Docker quick start
|
|
||||||
|
|
||||||
From repository root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml up -d
|
|
||||||
docker exec -it frr-rpki-client vtysh -c "show rpki cache-connection"
|
|
||||||
docker exec -it frr-rpki-client vtysh -c "show rpki prefix-table"
|
|
||||||
```
|
|
||||||
|
|
||||||
The Docker example defaults to `127.0.0.1:323`. Override the RTR server
|
|
||||||
endpoint with environment variables:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
RPKI_FRR_RPKI_HOST=10.0.0.12 \
|
|
||||||
RPKI_FRR_RPKI_PORT=323 \
|
|
||||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Stop:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- This setup targets RTR over TCP (`rpki cache tcp`).
|
|
||||||
- Docker mode renders `frr.conf.template` into `/etc/frr/frr.conf` at startup.
|
|
||||||
- Keep protocol-level conformance checks in Rust tests and
|
|
||||||
`src/bin/rtr_debug_client`.
|
|
||||||
- `network_mode: host` is kept for the minimal FRR container example, but the
|
|
||||||
RTR server address is controlled by `RPKI_FRR_RPKI_HOST` and
|
|
||||||
`RPKI_FRR_RPKI_PORT`.
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
# FRR 最小化 RTR 客户端配置
|
|
||||||
|
|
||||||
本目录提供一个 FRR 最小配置,用于和本仓库 RTR Server 做黑盒互通测试。
|
|
||||||
|
|
||||||
本仓库默认 RTR 监听地址:
|
|
||||||
- TCP:`0.0.0.0:323`
|
|
||||||
- TLS:`0.0.0.0:324`
|
|
||||||
|
|
||||||
参考实现:
|
|
||||||
- `src/main.rs`
|
|
||||||
|
|
||||||
## 文件说明
|
|
||||||
|
|
||||||
- `daemons.example`:示例 `/etc/frr/daemons`
|
|
||||||
- `frr.conf.example`:静态示例 `/etc/frr/frr.conf`
|
|
||||||
- `frr.conf.template`:Docker 启动时生成 `/etc/frr/frr.conf` 的模板
|
|
||||||
- `entrypoint.sh`:启动 FRR 前根据环境变量渲染 `frr.conf.template`
|
|
||||||
- `.env`:Docker 示例使用的默认环境变量
|
|
||||||
|
|
||||||
## 在 FRR 主机上应用
|
|
||||||
|
|
||||||
1. 复制 `daemons.example` 到 `/etc/frr/daemons`
|
|
||||||
2. 复制 `frr.conf.example` 到 `/etc/frr/frr.conf`
|
|
||||||
3. 重启 FRR:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl restart frr
|
|
||||||
```
|
|
||||||
|
|
||||||
## 验证命令
|
|
||||||
|
|
||||||
```bash
|
|
||||||
vtysh -c "show rpki configuration"
|
|
||||||
vtysh -c "show rpki cache-server"
|
|
||||||
vtysh -c "show rpki cache-connection"
|
|
||||||
vtysh -c "show rpki prefix-table"
|
|
||||||
```
|
|
||||||
|
|
||||||
当 `show rpki cache-connection` 显示已连接,且 `show rpki prefix-table` 出现 VRP 时,说明 RTR 客户端链路工作正常。
|
|
||||||
|
|
||||||
## Docker 快速启动
|
|
||||||
|
|
||||||
在仓库根目录执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml up -d
|
|
||||||
docker exec -it frr-rpki-client vtysh -c "show rpki cache-connection"
|
|
||||||
docker exec -it frr-rpki-client vtysh -c "show rpki prefix-table"
|
|
||||||
```
|
|
||||||
|
|
||||||
Docker 示例默认连接 `127.0.0.1:323`。如果 RTR Server 在其他地址,可以通过环境变量覆盖:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
RPKI_FRR_RPKI_HOST=10.0.0.12 \
|
|
||||||
RPKI_FRR_RPKI_PORT=323 \
|
|
||||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
停止:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
## 可测试范围
|
|
||||||
|
|
||||||
- FRR 作为 RTR Client 与本仓库 Server 的 TCP 建链能力
|
|
||||||
- FRR 侧基础会话状态可见性(`cache-server` / `cache-connection`)
|
|
||||||
- VRP 下发与导入是否成功(`prefix-table` 是否有条目)
|
|
||||||
- 基于服务端数据变化触发的前缀表更新(可通过替换 `data` 后观察)
|
|
||||||
- 黑盒互通回归:用于确认“路由器客户端视角”功能可用
|
|
||||||
|
|
||||||
## 不覆盖范围
|
|
||||||
|
|
||||||
- 不替代协议级单元测试/集成测试(PDU 细节、异常路径、边界条件)
|
|
||||||
- 不替代 `src/bin/rtr_debug_client` 的逐报文调试能力
|
|
||||||
- 默认示例以 TCP 为主,TLS/mTLS 需按你的证书与 FRR 配置单独扩展
|
|
||||||
|
|
||||||
## 说明
|
|
||||||
|
|
||||||
- 当前示例主要针对 RTR over TCP(`rpki cache tcp`)
|
|
||||||
- Docker 模式会在启动时把 `frr.conf.template` 渲染为 `/etc/frr/frr.conf`
|
|
||||||
- `network_mode: host` 仍用于最小化 FRR 容器示例,但 RTR Server 地址由 `RPKI_FRR_RPKI_HOST` 和 `RPKI_FRR_RPKI_PORT` 控制
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Minimal FRR daemons config for RPKI testing
|
|
||||||
|
|
||||||
zebra=yes
|
|
||||||
bgpd=yes
|
|
||||||
|
|
||||||
# Enable bgpd RPKI module
|
|
||||||
bgpd_options=" -A 127.0.0.1 -M rpki"
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
services:
|
|
||||||
frr-rpki-client:
|
|
||||||
image: quay.io/frrouting/frr:10.2.2
|
|
||||||
container_name: frr-rpki-client
|
|
||||||
restart: unless-stopped
|
|
||||||
network_mode: host
|
|
||||||
privileged: true
|
|
||||||
environment:
|
|
||||||
RPKI_FRR_RPKI_HOST: "${RPKI_FRR_RPKI_HOST:-127.0.0.1}"
|
|
||||||
RPKI_FRR_RPKI_PORT: "${RPKI_FRR_RPKI_PORT:-323}"
|
|
||||||
RPKI_FRR_RPKI_PREFERENCE: "${RPKI_FRR_RPKI_PREFERENCE:-1}"
|
|
||||||
entrypoint: ["/bin/sh", "/config/entrypoint.sh"]
|
|
||||||
volumes:
|
|
||||||
- ./daemons.example:/etc/frr/daemons:ro
|
|
||||||
- ./frr.conf.template:/config/frr.conf.template:ro
|
|
||||||
- ./entrypoint.sh:/config/entrypoint.sh:ro
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
: "${RPKI_FRR_RPKI_HOST:=127.0.0.1}"
|
|
||||||
: "${RPKI_FRR_RPKI_PORT:=323}"
|
|
||||||
: "${RPKI_FRR_RPKI_PREFERENCE:=1}"
|
|
||||||
: "${RPKI_FRR_CONFIG_TEMPLATE_PATH:=/config/frr.conf.template}"
|
|
||||||
: "${RPKI_FRR_CONFIG_PATH:=/etc/frr/frr.conf}"
|
|
||||||
|
|
||||||
escape_sed() {
|
|
||||||
printf '%s' "$1" | sed 's/[&|\\]/\\&/g'
|
|
||||||
}
|
|
||||||
|
|
||||||
host="$(escape_sed "${RPKI_FRR_RPKI_HOST}")"
|
|
||||||
port="$(escape_sed "${RPKI_FRR_RPKI_PORT}")"
|
|
||||||
preference="$(escape_sed "${RPKI_FRR_RPKI_PREFERENCE}")"
|
|
||||||
|
|
||||||
sed \
|
|
||||||
-e "s|\${RPKI_FRR_RPKI_HOST}|${host}|g" \
|
|
||||||
-e "s|\${RPKI_FRR_RPKI_PORT}|${port}|g" \
|
|
||||||
-e "s|\${RPKI_FRR_RPKI_PREFERENCE}|${preference}|g" \
|
|
||||||
"${RPKI_FRR_CONFIG_TEMPLATE_PATH}" > "${RPKI_FRR_CONFIG_PATH}"
|
|
||||||
|
|
||||||
exec /usr/lib/frr/docker-start
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
frr version 10.2
|
|
||||||
frr defaults traditional
|
|
||||||
hostname rpki-lab
|
|
||||||
service integrated-vtysh-config
|
|
||||||
!
|
|
||||||
debug rpki
|
|
||||||
!
|
|
||||||
rpki
|
|
||||||
rpki polling_period 10
|
|
||||||
rpki timeout 10
|
|
||||||
rpki retry_interval 10
|
|
||||||
rpki expire_interval 7200
|
|
||||||
rpki cache tcp 127.0.0.1 323 preference 1
|
|
||||||
exit
|
|
||||||
!
|
|
||||||
router bgp 65001
|
|
||||||
bgp router-id 192.0.2.1
|
|
||||||
!
|
|
||||||
address-family ipv4 unicast
|
|
||||||
exit-address-family
|
|
||||||
!
|
|
||||||
line vty
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
frr version 10.2
|
|
||||||
frr defaults traditional
|
|
||||||
hostname rpki-lab
|
|
||||||
service integrated-vtysh-config
|
|
||||||
!
|
|
||||||
debug rpki
|
|
||||||
!
|
|
||||||
rpki
|
|
||||||
rpki polling_period 10
|
|
||||||
rpki timeout 10
|
|
||||||
rpki retry_interval 10
|
|
||||||
rpki expire_interval 7200
|
|
||||||
rpki cache tcp ${RPKI_FRR_RPKI_HOST} ${RPKI_FRR_RPKI_PORT} preference ${RPKI_FRR_RPKI_PREFERENCE}
|
|
||||||
exit
|
|
||||||
!
|
|
||||||
router bgp 65001
|
|
||||||
bgp router-id 192.0.2.1
|
|
||||||
!
|
|
||||||
address-family ipv4 unicast
|
|
||||||
exit-address-family
|
|
||||||
!
|
|
||||||
line vty
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
# Core runtime knobs.
|
|
||||||
# build-run.sh auto-detects amd64/arm64 unless IMAGE_ARCH/RPKI_RTR_IMAGE_ARCH is set before running it.
|
|
||||||
# Direct docker compose uses RPKI_RTR_IMAGE_ARCH from this file; set it to arm64 on ARM hosts.
|
|
||||||
RPKI_RTR_SERVER_IMAGE_BASE=rpki-rtr
|
|
||||||
RPKI_RTR_SERVER_IMAGE_TAG=v1.3
|
|
||||||
RPKI_RTR_IMAGE_ARCH=arm64
|
|
||||||
# RPKI_RTR_TARGET_PLATFORM=linux/amd64
|
|
||||||
# RPKI_RTR_SERVER_IMAGE=rpki-rtr-amd64:v1.3
|
|
||||||
|
|
||||||
# Data source directories on host.
|
|
||||||
RPKI_RTR_CCR_HOST_DIR=../../data
|
|
||||||
RPKI_RTR_SLURM_HOST_DIR=../../data
|
|
||||||
|
|
||||||
# In-container data source directories.
|
|
||||||
RPKI_RTR_CCR_DIR=/app/data
|
|
||||||
RPKI_RTR_SLURM_DIR=/app/slurm
|
|
||||||
RPKI_RTR_REPORT_HOST_DIR=../../report
|
|
||||||
|
|
||||||
# In-container runtime paths.
|
|
||||||
RPKI_RTR_DB_PATH=/app/rtr-db
|
|
||||||
RPKI_RTR_REPORT_DIR=/app/report
|
|
||||||
|
|
||||||
# Persistent directories on host.
|
|
||||||
RPKI_RTR_DB_HOST_DIR=../../rtr-db
|
|
||||||
RPKI_RTR_LOG_HOST_DIR=../../logs/server
|
|
||||||
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS=300
|
|
||||||
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS=300
|
|
||||||
RPKI_RTR_REPORT_HISTORY_LIMIT=10
|
|
||||||
RPKI_RTR_TIMEZONE=Asia/Shanghai
|
|
||||||
RPKI_RTR_MAX_DELTA=10
|
|
||||||
RPKI_RTR_MAX_CONNECTIONS=100000
|
|
||||||
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES=128
|
|
||||||
RPKI_RTR_ADMIN_ADDR=0.0.0.0:8323
|
|
||||||
RPKI_RTR_ADMIN_TOKEN=qwert
|
|
||||||
RUST_LOG=info
|
|
||||||
|
|
||||||
# TLS mode knobs.
|
|
||||||
RPKI_RTR_ENFORCE_TLS_CLIENT_SAN_IP_MATCH=false
|
|
||||||
RPKI_RTR_TLS_CERT_PATH=/app/certs/server-dns.crt
|
|
||||||
RPKI_RTR_TLS_KEY_PATH=/app/certs/server-dns.key
|
|
||||||
RPKI_RTR_TLS_CLIENT_CA_PATH=/app/certs/client-ca.crt
|
|
||||||
|
|
||||||
RPKI_RTR_TLS_CERTS_HOST_DIR=../../certs/tls
|
|
||||||
|
|
||||||
# SSH mode knobs.
|
|
||||||
RPKI_RTR_SSH_HOST_PORT=2222
|
|
||||||
RPKI_RTR_SSH_CONTAINER_PORT=22
|
|
||||||
RPKI_RTR_SSH_AUTH_MODE=key
|
|
||||||
RPKI_RTR_SSH_USERNAME=rpki-rtr
|
|
||||||
RPKI_RTR_SSH_SUBSYSTEM_NAME=rpki-rtr
|
|
||||||
RPKI_RTR_SSH_HOST_KEY_PATH=/app/certs/ssh_host_rsa_key
|
|
||||||
RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH=/app/certs/rtr-authorized_keys
|
|
||||||
|
|
||||||
RPKI_RTR_SSH_CERTS_HOST_DIR=../../certs/ssh
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
# Deployment (Docker Compose)
|
|
||||||
|
|
||||||
This project runs `src/main.rs` as a long-running server that:
|
|
||||||
|
|
||||||
1. loads latest `.ccr` from a configured directory,
|
|
||||||
2. applies optional SLURM filtering,
|
|
||||||
3. starts RTR server.
|
|
||||||
|
|
||||||
The container runs `rpki` directly as PID 1.
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
- `deploy/server/Dockerfile`
|
|
||||||
- `deploy/server/docker-compose.yml`
|
|
||||||
|
|
||||||
## Runtime Paths in Container
|
|
||||||
|
|
||||||
- CCR directory: `/app/data`
|
|
||||||
- RocksDB directory: `/app/rtr-db`
|
|
||||||
- SLURM directory: `/app/slurm`
|
|
||||||
- Report directory: `/app/report`
|
|
||||||
- TLS cert directory (optional): `/app/certs`
|
|
||||||
|
|
||||||
## Path Configuration via `.env`
|
|
||||||
|
|
||||||
- `RPKI_RTR_CCR_HOST_DIR`: host CCR directory mounted into container
|
|
||||||
- `RPKI_RTR_SLURM_HOST_DIR`: host SLURM directory mounted into container
|
|
||||||
- `RPKI_RTR_CCR_DIR`: in-container CCR directory path
|
|
||||||
- `RPKI_RTR_SLURM_DIR`: in-container SLURM directory path
|
|
||||||
- `RPKI_RTR_DB_HOST_DIR`: host RocksDB directory
|
|
||||||
- `RPKI_RTR_LOG_HOST_DIR`: host log directory
|
|
||||||
- `RPKI_RTR_REPORT_HOST_DIR`: host directory receiving split RTR JSON reports
|
|
||||||
- `RPKI_RTR_DB_PATH`: in-container RocksDB directory
|
|
||||||
- `RPKI_RTR_REPORT_DIR`: in-container report directory
|
|
||||||
|
|
||||||
## Runtime Configuration via `.env`
|
|
||||||
|
|
||||||
- Core: `RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS`, `RPKI_RTR_MAX_DELTA`, `RPKI_RTR_MAX_CONCURRENT_HANDSHAKES`, `RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS`, `RPKI_RTR_REPORT_HISTORY_LIMIT`, `RPKI_RTR_TIMEZONE`, `RPKI_RTR_ADMIN_ADDR`, `RPKI_RTR_ADMIN_TOKEN`, `RUST_LOG`
|
|
||||||
- TCP mode: `RPKI_RTR_MAX_CONNECTIONS`
|
|
||||||
- TLS mode: `RPKI_RTR_ENFORCE_TLS_CLIENT_SAN_IP_MATCH`, `RPKI_RTR_TLS_CERT_PATH`, `RPKI_RTR_TLS_KEY_PATH`, `RPKI_RTR_TLS_CLIENT_CA_PATH`, `RPKI_RTR_TLS_CERTS_HOST_DIR`
|
|
||||||
- SSH mode: `RPKI_RTR_SSH_HOST_PORT`, `RPKI_RTR_SSH_CONTAINER_PORT`, `RPKI_RTR_SSH_AUTH_MODE`, `RPKI_RTR_SSH_USERNAME`, `RPKI_RTR_SSH_SUBSYSTEM_NAME`, `RPKI_RTR_SSH_HOST_KEY_PATH`, `RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH`, `RPKI_RTR_SSH_CERTS_HOST_DIR`
|
|
||||||
|
|
||||||
## Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/server/docker-compose.yml up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Stop
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/server/docker-compose.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
## Logs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/server/docker-compose.yml logs -f rpki-rtr
|
|
||||||
```
|
|
||||||
|
|
||||||
The admin API can also stream the redirected log file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -N "http://127.0.0.1:8323/admin/rtr/logs/tail?stream=stdout&lines=200" \
|
|
||||||
-H "Authorization: Bearer $RPKI_RTR_ADMIN_TOKEN"
|
|
||||||
```
|
|
||||||
|
|
||||||
It reads `/app/logs/${HOSTNAME}.stdout.log` or `.stderr.log` by default. Set
|
|
||||||
`RPKI_RTR_LOG_DIR` and `RPKI_RTR_LOG_NAME` to override that lookup.
|
|
||||||
|
|
||||||
## Runtime Report
|
|
||||||
|
|
||||||
The server writes split JSON reports. Each report file uses a local-time
|
|
||||||
timestamp suffix and each category keeps `RPKI_RTR_REPORT_HISTORY_LIMIT` files,
|
|
||||||
defaulting to 10.
|
|
||||||
|
|
||||||
- `rtr-source-*.json`: CCR and SLURM source metadata, latest refresh status,
|
|
||||||
data quality counts, cache snapshot counts, and delta counts. Written on
|
|
||||||
startup and source refresh events.
|
|
||||||
- `rtr-clients-*.json`: active client connection counts and counts by transport
|
|
||||||
(`tcp`, `tls`, `ssh`). Written on startup, whenever the active connection
|
|
||||||
count changes.
|
|
||||||
- `rtr-runtime-*.json`: service start time, uptime, process RSS, and
|
|
||||||
non-sensitive runtime configuration. Written on startup and every
|
|
||||||
`RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS`, defaulting to 300 seconds.
|
|
||||||
|
|
||||||
Timestamps in logs and report JSON files use `RPKI_RTR_TIMEZONE`, which
|
|
||||||
defaults to `Asia/Shanghai`. Use IANA timezone names such as `Asia/Shanghai`,
|
|
||||||
`Europe/London`, `America/New_York`, or `UTC`; `Shanghai` is accepted as a
|
|
||||||
convenience alias for `Asia/Shanghai`.
|
|
||||||
|
|
||||||
## Runtime Admin Config
|
|
||||||
|
|
||||||
The admin endpoint is disabled by default. Set `RPKI_RTR_ADMIN_ADDR` to enable
|
|
||||||
`POST /admin/rtr/config`. If the address is not loopback, `RPKI_RTR_ADMIN_TOKEN`
|
|
||||||
must also be set and requests must include `Authorization: Bearer <token>`.
|
|
||||||
|
|
||||||
The endpoint accepts partial JSON updates. See `docs/rtr-admin-api.md` for the
|
|
||||||
complete request/response schema, examples, and runtime apply semantics.
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
FROM rust:1.89-bookworm AS builder
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
|
|
||||||
ENV CARGO_BUILD_JOBS=2
|
|
||||||
|
|
||||||
RUN set -eux; \
|
|
||||||
cat > /etc/apt/sources.list.d/debian.sources <<'EOF'
|
|
||||||
Types: deb
|
|
||||||
URIs: http://mirrors.tuna.tsinghua.edu.cn/debian
|
|
||||||
Suites: bookworm bookworm-updates
|
|
||||||
Components: main
|
|
||||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
|
||||||
|
|
||||||
Types: deb
|
|
||||||
URIs: http://mirrors.tuna.tsinghua.edu.cn/debian-security
|
|
||||||
Suites: bookworm-security
|
|
||||||
Components: main
|
|
||||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
|
||||||
EOF
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --fix-missing --no-install-recommends \
|
|
||||||
-o Acquire::Retries=10 \
|
|
||||||
-o Acquire::http::Timeout=60 \
|
|
||||||
build-essential \
|
|
||||||
cmake \
|
|
||||||
pkg-config \
|
|
||||||
clang \
|
|
||||||
libclang-dev \
|
|
||||||
libssl-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
COPY Cargo.toml Cargo.lock ./
|
|
||||||
COPY src ./src
|
|
||||||
|
|
||||||
RUN cargo build --release --bin rpki_rtr
|
|
||||||
|
|
||||||
FROM debian:bookworm-slim AS runtime
|
|
||||||
|
|
||||||
RUN set -eux; \
|
|
||||||
cat > /etc/apt/sources.list.d/debian.sources <<'EOF'
|
|
||||||
Types: deb
|
|
||||||
URIs: http://mirrors.tuna.tsinghua.edu.cn/debian
|
|
||||||
Suites: bookworm bookworm-updates
|
|
||||||
Components: main
|
|
||||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
|
||||||
|
|
||||||
Types: deb
|
|
||||||
URIs: http://mirrors.tuna.tsinghua.edu.cn/debian-security
|
|
||||||
Suites: bookworm-security
|
|
||||||
Components: main
|
|
||||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
|
||||||
EOF
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --fix-missing --no-install-recommends \
|
|
||||||
-o Acquire::Retries=10 \
|
|
||||||
-o Acquire::http::Timeout=60 \
|
|
||||||
ca-certificates \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY --from=builder /build/target/release/rpki_rtr /usr/local/bin/rpki_rtr
|
|
||||||
COPY --chmod=755 deploy/server/entrypoint.sh /usr/local/bin/rpki-rtr-entrypoint.sh
|
|
||||||
|
|
||||||
RUN mkdir -p /app/data /app/rtr-db /app/certs /app/slurm /app/logs /app/report
|
|
||||||
|
|
||||||
ENV RPKI_RTR_ENABLE_TLS=false \
|
|
||||||
RPKI_RTR_TCP_ADDR=0.0.0.0:323 \
|
|
||||||
RPKI_RTR_TLS_ADDR=0.0.0.0:324 \
|
|
||||||
RPKI_RTR_DB_PATH=/app/rtr-db \
|
|
||||||
RPKI_RTR_CCR_DIR=/app/data \
|
|
||||||
RPKI_RTR_SLURM_DIR=/app/slurm \
|
|
||||||
RPKI_RTR_REPORT_DIR=/app/report \
|
|
||||||
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS=300 \
|
|
||||||
RPKI_RTR_REPORT_HISTORY_LIMIT=10 \
|
|
||||||
RPKI_RTR_REFRESH_INTERVAL_SECS=300 \
|
|
||||||
RPKI_RTR_ADMIN_ADDR="" \
|
|
||||||
RPKI_RTR_ADMIN_TOKEN=""
|
|
||||||
|
|
||||||
EXPOSE 323 324
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/rpki-rtr-entrypoint.sh"]
|
|
||||||
@@ -1,292 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)"
|
|
||||||
CALLER_TARGET_PLATFORM="${TARGET_PLATFORM:-${RPKI_RTR_TARGET_PLATFORM:-}}"
|
|
||||||
CALLER_IMAGE_ARCH="${IMAGE_ARCH:-${RPKI_RTR_IMAGE_ARCH:-}}"
|
|
||||||
CALLER_SERVER_IMAGE="${SERVER_IMAGE:-${RPKI_RTR_SERVER_IMAGE:-}}"
|
|
||||||
|
|
||||||
load_env_defaults() {
|
|
||||||
local line key value
|
|
||||||
[[ -f "${SCRIPT_DIR}/.env" ]] || return
|
|
||||||
|
|
||||||
while IFS= read -r line || [[ -n "${line}" ]]; do
|
|
||||||
line="${line%$'\r'}"
|
|
||||||
[[ -z "${line}" || "${line}" =~ ^[[:space:]]*# ]] && continue
|
|
||||||
[[ "${line}" == *"="* ]] || continue
|
|
||||||
|
|
||||||
key="${line%%=*}"
|
|
||||||
value="${line#*=}"
|
|
||||||
key="${key#"${key%%[![:space:]]*}"}"
|
|
||||||
key="${key%"${key##*[![:space:]]}"}"
|
|
||||||
[[ "${key}" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
|
|
||||||
|
|
||||||
if [[ -z "${!key+x}" ]]; then
|
|
||||||
export "${key}=${value}"
|
|
||||||
fi
|
|
||||||
done < "${SCRIPT_DIR}/.env"
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_image_arch() {
|
|
||||||
case "$(uname -m)" in
|
|
||||||
x86_64|amd64) printf '%s\n' amd64 ;;
|
|
||||||
aarch64|arm64) printf '%s\n' arm64 ;;
|
|
||||||
*) die "unsupported host architecture '$(uname -m)'; set RPKI_RTR_IMAGE_ARCH=amd64 or arm64" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
platform_for_image_arch() {
|
|
||||||
case "$1" in
|
|
||||||
amd64) printf '%s\n' linux/amd64 ;;
|
|
||||||
arm64) printf '%s\n' linux/arm64 ;;
|
|
||||||
*) die "unsupported image arch '$1', expected amd64 or arm64" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
image_arch_from_platform() {
|
|
||||||
case "$1" in
|
|
||||||
linux/amd64|amd64) printf '%s\n' amd64 ;;
|
|
||||||
linux/arm64|arm64) printf '%s\n' arm64 ;;
|
|
||||||
*) return 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
load_env_defaults
|
|
||||||
|
|
||||||
MODE="${MODE:-tcp}"
|
|
||||||
ACTION="up"
|
|
||||||
DETACH="${DETACH:-1}"
|
|
||||||
BUILD="${BUILD:-1}"
|
|
||||||
TARGET_PLATFORM="${CALLER_TARGET_PLATFORM}"
|
|
||||||
IMAGE_ARCH="${CALLER_IMAGE_ARCH}"
|
|
||||||
SERVER_IMAGE="${CALLER_SERVER_IMAGE}"
|
|
||||||
BUILDX_PUSH="${BUILDX_PUSH:-0}"
|
|
||||||
BUILDX_BUILDER="${BUILDX_BUILDER:-}"
|
|
||||||
SKIP_BASE_IMAGE_PULL="${SKIP_BASE_IMAGE_PULL:-0}"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage:
|
|
||||||
deploy/server/build-run.sh [action] [options]
|
|
||||||
|
|
||||||
Actions:
|
|
||||||
up Build and start server (default)
|
|
||||||
build Build server image with docker compose
|
|
||||||
buildx Build server image with docker buildx
|
|
||||||
rebuild Rebuild image without cache, then start server
|
|
||||||
down|stop Stop server compose stack
|
|
||||||
restart Stop, then build and start server
|
|
||||||
logs Follow server logs
|
|
||||||
ps Show server compose status
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--mode MODE base|tcp|tls|ssh (default: tcp)
|
|
||||||
--platform VALUE Target platform for buildx (default: host architecture)
|
|
||||||
--image-arch VALUE
|
|
||||||
Image architecture suffix for default tag: amd64|arm64
|
|
||||||
--server-image TAG
|
|
||||||
Server image tag for buildx and compose
|
|
||||||
--push Push buildx image instead of loading it locally
|
|
||||||
--builder NAME buildx builder name
|
|
||||||
--skip-base-pull Do not pull Dockerfile base images before buildx
|
|
||||||
--foreground Run server in foreground
|
|
||||||
--no-build Start without --build
|
|
||||||
-h|--help Show this help
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
echo "error: $*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
has_compose() {
|
|
||||||
docker compose version >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
has_buildx() {
|
|
||||||
docker buildx version >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
pull_base_images() {
|
|
||||||
[[ "${SKIP_BASE_IMAGE_PULL}" == "1" ]] && return
|
|
||||||
|
|
||||||
local platform image
|
|
||||||
local -a platforms images
|
|
||||||
IFS=',' read -r -a platforms <<< "${TARGET_PLATFORM}"
|
|
||||||
images=(rust:1.89-bookworm debian:bookworm-slim)
|
|
||||||
|
|
||||||
for platform in "${platforms[@]}"; do
|
|
||||||
for image in "${images[@]}"; do
|
|
||||||
docker pull --platform "${platform}" "${image}"
|
|
||||||
done
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
compose_files() {
|
|
||||||
case "${MODE}" in
|
|
||||||
base) printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.yml" ;;
|
|
||||||
tcp) printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.tcp.yml" ;;
|
|
||||||
tls) printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.tls.yml" ;;
|
|
||||||
ssh) printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.ssh.yml" ;;
|
|
||||||
*) die "unsupported mode '${MODE}', expected base|tcp|tls|ssh" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
compose_args() {
|
|
||||||
local -a args files
|
|
||||||
args=()
|
|
||||||
[[ -f "${SCRIPT_DIR}/.env" ]] && args+=(--env-file "${SCRIPT_DIR}/.env")
|
|
||||||
mapfile -t files < <(compose_files)
|
|
||||||
args+=("${files[@]}")
|
|
||||||
printf '%s\n' "${args[@]}"
|
|
||||||
}
|
|
||||||
|
|
||||||
compose_run() {
|
|
||||||
local -a args
|
|
||||||
mapfile -t args < <(compose_args)
|
|
||||||
docker compose "${args[@]}" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
up_server() {
|
|
||||||
local -a up_args
|
|
||||||
up_args=(up)
|
|
||||||
[[ "${DETACH}" == "1" ]] && up_args+=(-d)
|
|
||||||
[[ "${BUILD}" == "1" ]] && up_args+=(--build)
|
|
||||||
compose_run "${up_args[@]}"
|
|
||||||
}
|
|
||||||
|
|
||||||
buildx_server() {
|
|
||||||
has_buildx || die "docker buildx is not available"
|
|
||||||
if [[ "${TARGET_PLATFORM}" == *,* && "${BUILDX_PUSH}" != "1" ]]; then
|
|
||||||
die "multi-platform buildx output requires --push"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local -a output_args builder_args
|
|
||||||
[[ "${BUILDX_PUSH}" == "1" ]] && output_args=(--push) || output_args=(--load)
|
|
||||||
[[ -n "${BUILDX_BUILDER}" ]] && builder_args=(--builder "${BUILDX_BUILDER}") || builder_args=()
|
|
||||||
|
|
||||||
pull_base_images
|
|
||||||
|
|
||||||
docker buildx build "${builder_args[@]}" \
|
|
||||||
--platform "${TARGET_PLATFORM}" \
|
|
||||||
-f "${SCRIPT_DIR}/Dockerfile" \
|
|
||||||
-t "${SERVER_IMAGE}" \
|
|
||||||
"${output_args[@]}" \
|
|
||||||
"${REPO_ROOT}"
|
|
||||||
}
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
up|build|buildx|rebuild|down|stop|restart|logs|ps)
|
|
||||||
ACTION="$1"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--mode)
|
|
||||||
[[ $# -ge 2 ]] || die "--mode requires a value"
|
|
||||||
MODE="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--platform)
|
|
||||||
[[ $# -ge 2 ]] || die "--platform requires a value"
|
|
||||||
TARGET_PLATFORM="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--image-arch)
|
|
||||||
[[ $# -ge 2 ]] || die "--image-arch requires a value"
|
|
||||||
IMAGE_ARCH="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--server-image)
|
|
||||||
[[ $# -ge 2 ]] || die "--server-image requires a value"
|
|
||||||
SERVER_IMAGE="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--push)
|
|
||||||
BUILDX_PUSH=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--builder)
|
|
||||||
[[ $# -ge 2 ]] || die "--builder requires a value"
|
|
||||||
BUILDX_BUILDER="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--skip-base-pull)
|
|
||||||
SKIP_BASE_IMAGE_PULL=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--foreground)
|
|
||||||
DETACH=0
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--no-build)
|
|
||||||
BUILD=0
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "unknown argument '$1'"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -z "${TARGET_PLATFORM}" ]]; then
|
|
||||||
[[ -n "${IMAGE_ARCH}" ]] || IMAGE_ARCH="$(detect_image_arch)"
|
|
||||||
TARGET_PLATFORM="$(platform_for_image_arch "${IMAGE_ARCH}")"
|
|
||||||
elif [[ -z "${IMAGE_ARCH}" ]]; then
|
|
||||||
if [[ "${TARGET_PLATFORM}" != *,* ]] && IMAGE_ARCH="$(image_arch_from_platform "${TARGET_PLATFORM}")"; then
|
|
||||||
:
|
|
||||||
else
|
|
||||||
IMAGE_ARCH="$(detect_image_arch)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "${SERVER_IMAGE}" ]]; then
|
|
||||||
if [[ "${TARGET_PLATFORM}" == *,* ]]; then
|
|
||||||
SERVER_IMAGE="${RPKI_RTR_SERVER_IMAGE_BASE:-rpki-rtr}:${RPKI_RTR_SERVER_IMAGE_TAG:-latest}"
|
|
||||||
else
|
|
||||||
SERVER_IMAGE="${RPKI_RTR_SERVER_IMAGE_BASE:-rpki-rtr}-${IMAGE_ARCH}:${RPKI_RTR_SERVER_IMAGE_TAG:-latest}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
export RPKI_RTR_SERVER_IMAGE="${SERVER_IMAGE}"
|
|
||||||
if [[ "${ACTION}" == "buildx" ]]; then
|
|
||||||
has_buildx || die "docker buildx is not available"
|
|
||||||
else
|
|
||||||
has_compose || die "docker compose is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd "${REPO_ROOT}"
|
|
||||||
|
|
||||||
case "${ACTION}" in
|
|
||||||
up)
|
|
||||||
up_server
|
|
||||||
;;
|
|
||||||
build)
|
|
||||||
compose_run build
|
|
||||||
;;
|
|
||||||
buildx)
|
|
||||||
buildx_server
|
|
||||||
;;
|
|
||||||
rebuild)
|
|
||||||
compose_run build --no-cache
|
|
||||||
up_server
|
|
||||||
;;
|
|
||||||
down|stop)
|
|
||||||
compose_run down
|
|
||||||
;;
|
|
||||||
restart)
|
|
||||||
compose_run down
|
|
||||||
up_server
|
|
||||||
;;
|
|
||||||
logs)
|
|
||||||
compose_run logs -f
|
|
||||||
;;
|
|
||||||
ps)
|
|
||||||
compose_run ps
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
|
||||||
rpki-rtr:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: deploy/server/Dockerfile
|
|
||||||
image: ${RPKI_RTR_SERVER_IMAGE:-${RPKI_RTR_SERVER_IMAGE_BASE:-rpki-rtr}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_SERVER_IMAGE_TAG:-latest}}
|
|
||||||
container_name: rpki-rtr-ssh
|
|
||||||
restart: no
|
|
||||||
ports:
|
|
||||||
- "323:323"
|
|
||||||
- "${RPKI_RTR_SSH_HOST_PORT:-2222}:${RPKI_RTR_SSH_CONTAINER_PORT:-22}"
|
|
||||||
- "8323:8323"
|
|
||||||
environment:
|
|
||||||
RPKI_RTR_ENABLE_TLS: "false"
|
|
||||||
RPKI_RTR_ENABLE_SSH: "true"
|
|
||||||
RPKI_RTR_TCP_ADDR: "0.0.0.0:323"
|
|
||||||
RPKI_RTR_SSH_ADDR: "0.0.0.0:${RPKI_RTR_SSH_CONTAINER_PORT:-22}"
|
|
||||||
RPKI_RTR_SSH_HOST_KEY_PATH: "${RPKI_RTR_SSH_HOST_KEY_PATH:-/app/certs/ssh_host_rsa_key}"
|
|
||||||
RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH: "${RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH:-/app/certs/rtr-authorized_keys}"
|
|
||||||
RPKI_RTR_SSH_USERNAME: "${RPKI_RTR_SSH_USERNAME:-rpki-rtr}"
|
|
||||||
RPKI_RTR_SSH_SUBSYSTEM_NAME: "${RPKI_RTR_SSH_SUBSYSTEM_NAME:-rpki-rtr}"
|
|
||||||
# SSH auth mode: key | password | both
|
|
||||||
RPKI_RTR_SSH_AUTH_MODE: "${RPKI_RTR_SSH_AUTH_MODE:-key}"
|
|
||||||
# Optional: enable password authentication in addition to publickey
|
|
||||||
# RPKI_RTR_SSH_PASSWORD: "test-password"
|
|
||||||
RPKI_RTR_DB_PATH: "${RPKI_RTR_DB_PATH:-/app/rtr-db}"
|
|
||||||
RPKI_RTR_CCR_DIR: "${RPKI_RTR_CCR_DIR:-/app/data}"
|
|
||||||
RPKI_RTR_SLURM_DIR: "${RPKI_RTR_SLURM_DIR:-/app/slurm}"
|
|
||||||
RPKI_RTR_REPORT_DIR: "${RPKI_RTR_REPORT_DIR:-/app/report}"
|
|
||||||
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS: "${RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS:-300}"
|
|
||||||
RPKI_RTR_REPORT_HISTORY_LIMIT: "${RPKI_RTR_REPORT_HISTORY_LIMIT:-10}"
|
|
||||||
RPKI_RTR_TIMEZONE: "${RPKI_RTR_TIMEZONE:-Asia/Shanghai}"
|
|
||||||
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "${RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS:-300}"
|
|
||||||
RPKI_RTR_MAX_DELTA: "${RPKI_RTR_MAX_DELTA:-10}"
|
|
||||||
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "${RPKI_RTR_MAX_CONCURRENT_HANDSHAKES:-128}"
|
|
||||||
RPKI_RTR_ADMIN_ADDR: "${RPKI_RTR_ADMIN_ADDR:-}"
|
|
||||||
RPKI_RTR_ADMIN_TOKEN: "${RPKI_RTR_ADMIN_TOKEN:-}"
|
|
||||||
RUST_LOG: "${RUST_LOG:-info}"
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CCR_HOST_DIR:-../../data}:${RPKI_RTR_CCR_DIR:-/app/data}:ro
|
|
||||||
- ${RPKI_RTR_DB_HOST_DIR:-../../rtr-db}:${RPKI_RTR_DB_PATH:-/app/rtr-db}
|
|
||||||
- ${RPKI_RTR_SLURM_HOST_DIR:-../../data}:${RPKI_RTR_SLURM_DIR:-/app/slurm}
|
|
||||||
- ${RPKI_RTR_SSH_CERTS_HOST_DIR:-../../certs/ssh}:/app/certs:ro
|
|
||||||
- ${RPKI_RTR_LOG_HOST_DIR:-../../logs/server}:/app/logs
|
|
||||||
- ${RPKI_RTR_REPORT_HOST_DIR:-../../report}:${RPKI_RTR_REPORT_DIR:-/app/report}
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
|
||||||
rpki-rtr:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: deploy/server/Dockerfile
|
|
||||||
image: ${RPKI_RTR_SERVER_IMAGE:-${RPKI_RTR_SERVER_IMAGE_BASE:-rpki-rtr}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_SERVER_IMAGE_TAG:-latest}}
|
|
||||||
container_name: rpki-rtr-tcp
|
|
||||||
restart: no
|
|
||||||
ports:
|
|
||||||
- "323:323"
|
|
||||||
- "8323:8323"
|
|
||||||
environment:
|
|
||||||
RPKI_RTR_ENABLE_TLS: "false"
|
|
||||||
RPKI_RTR_ENABLE_SSH: "false"
|
|
||||||
RPKI_RTR_TCP_ADDR: "0.0.0.0:323"
|
|
||||||
RPKI_RTR_DB_PATH: "${RPKI_RTR_DB_PATH:-/app/rtr-db}"
|
|
||||||
RPKI_RTR_CCR_DIR: "${RPKI_RTR_CCR_DIR:-/app/data}"
|
|
||||||
RPKI_RTR_SLURM_DIR: "${RPKI_RTR_SLURM_DIR:-/app/slurm}"
|
|
||||||
RPKI_RTR_REPORT_DIR: "${RPKI_RTR_REPORT_DIR:-/app/report}"
|
|
||||||
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS: "${RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS:-300}"
|
|
||||||
RPKI_RTR_REPORT_HISTORY_LIMIT: "${RPKI_RTR_REPORT_HISTORY_LIMIT:-10}"
|
|
||||||
RPKI_RTR_TIMEZONE: "${RPKI_RTR_TIMEZONE:-Asia/Shanghai}"
|
|
||||||
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "${RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS:-60}"
|
|
||||||
RPKI_RTR_MAX_DELTA: "${RPKI_RTR_MAX_DELTA:-10}"
|
|
||||||
RPKI_RTR_MAX_CONNECTIONS: "${RPKI_RTR_MAX_CONNECTIONS:-100000}"
|
|
||||||
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "${RPKI_RTR_MAX_CONCURRENT_HANDSHAKES:-128}"
|
|
||||||
RPKI_RTR_ADMIN_ADDR: "${RPKI_RTR_ADMIN_ADDR:-}"
|
|
||||||
RPKI_RTR_ADMIN_TOKEN: "${RPKI_RTR_ADMIN_TOKEN:-}"
|
|
||||||
RUST_LOG: "${RUST_LOG:-info}"
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CCR_HOST_DIR:-../../data}:${RPKI_RTR_CCR_DIR:-/app/data}:ro
|
|
||||||
- ${RPKI_RTR_DB_HOST_DIR:-../../rtr-db}:${RPKI_RTR_DB_PATH:-/app/rtr-db}
|
|
||||||
- ${RPKI_RTR_SLURM_HOST_DIR:-../../data}:${RPKI_RTR_SLURM_DIR:-/app/slurm}
|
|
||||||
- ${RPKI_RTR_LOG_HOST_DIR:-../../logs/server}:/app/logs
|
|
||||||
- ${RPKI_RTR_REPORT_HOST_DIR:-../../report}:${RPKI_RTR_REPORT_DIR:-/app/report}
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
services:
|
|
||||||
rpki-rtr:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: deploy/server/Dockerfile
|
|
||||||
image: ${RPKI_RTR_SERVER_IMAGE:-${RPKI_RTR_SERVER_IMAGE_BASE:-rpki-rtr}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_SERVER_IMAGE_TAG:-latest}}
|
|
||||||
container_name: rpki-rtr-tls
|
|
||||||
restart: no
|
|
||||||
ports:
|
|
||||||
# - "323:323"
|
|
||||||
- "324:324"
|
|
||||||
- "8323:8323"
|
|
||||||
environment:
|
|
||||||
RPKI_RTR_ENABLE_TLS: "true"
|
|
||||||
RPKI_RTR_ENABLE_SSH: "false"
|
|
||||||
# RPKI_RTR_TCP_ADDR: "0.0.0.0:323"
|
|
||||||
RPKI_RTR_TLS_ADDR: "0.0.0.0:324"
|
|
||||||
RPKI_RTR_TLS_CERT_PATH: "${RPKI_RTR_TLS_CERT_PATH:-/app/certs/server-dns.crt}"
|
|
||||||
RPKI_RTR_TLS_KEY_PATH: "${RPKI_RTR_TLS_KEY_PATH:-/app/certs/server-dns.key}"
|
|
||||||
RPKI_RTR_TLS_CLIENT_CA_PATH: "${RPKI_RTR_TLS_CLIENT_CA_PATH:-/app/certs/client-ca.crt}"
|
|
||||||
RPKI_RTR_ENFORCE_TLS_CLIENT_SAN_IP_MATCH: "${RPKI_RTR_ENFORCE_TLS_CLIENT_SAN_IP_MATCH:-false}"
|
|
||||||
RPKI_RTR_DB_PATH: "${RPKI_RTR_DB_PATH:-/app/rtr-db}"
|
|
||||||
RPKI_RTR_CCR_DIR: "${RPKI_RTR_CCR_DIR:-/app/data}"
|
|
||||||
RPKI_RTR_SLURM_DIR: "${RPKI_RTR_SLURM_DIR:-/app/slurm}"
|
|
||||||
RPKI_RTR_REPORT_DIR: "${RPKI_RTR_REPORT_DIR:-/app/report}"
|
|
||||||
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS: "${RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS:-300}"
|
|
||||||
RPKI_RTR_REPORT_HISTORY_LIMIT: "${RPKI_RTR_REPORT_HISTORY_LIMIT:-10}"
|
|
||||||
RPKI_RTR_TIMEZONE: "${RPKI_RTR_TIMEZONE:-Asia/Shanghai}"
|
|
||||||
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "${RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS:-300}"
|
|
||||||
RPKI_RTR_MAX_DELTA: "${RPKI_RTR_MAX_DELTA:-10}"
|
|
||||||
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "${RPKI_RTR_MAX_CONCURRENT_HANDSHAKES:-128}"
|
|
||||||
RPKI_RTR_ADMIN_ADDR: "${RPKI_RTR_ADMIN_ADDR:-}"
|
|
||||||
RPKI_RTR_ADMIN_TOKEN: "${RPKI_RTR_ADMIN_TOKEN:-}"
|
|
||||||
RUST_LOG: "${RUST_LOG:-info}"
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CCR_HOST_DIR:-../../data}:${RPKI_RTR_CCR_DIR:-/app/data}:ro
|
|
||||||
- ${RPKI_RTR_DB_HOST_DIR:-../../rtr-db}:${RPKI_RTR_DB_PATH:-/app/rtr-db}
|
|
||||||
- ${RPKI_RTR_SLURM_HOST_DIR:-../../data}:${RPKI_RTR_SLURM_DIR:-/app/slurm}
|
|
||||||
- ${RPKI_RTR_TLS_CERTS_HOST_DIR:-../../certs/tls}:/app/certs:ro
|
|
||||||
- ${RPKI_RTR_LOG_HOST_DIR:-../../logs/server}:/app/logs
|
|
||||||
- ${RPKI_RTR_REPORT_HOST_DIR:-../../report}:${RPKI_RTR_REPORT_DIR:-/app/report}
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
|
||||||
rpki-rtr:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: deploy/server/Dockerfile
|
|
||||||
image: ${RPKI_RTR_SERVER_IMAGE:-${RPKI_RTR_SERVER_IMAGE_BASE:-rpki-rtr}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_SERVER_IMAGE_TAG:-latest}}
|
|
||||||
container_name: rpki-rtr
|
|
||||||
restart: no
|
|
||||||
ports:
|
|
||||||
- "323:323"
|
|
||||||
- "324:324"
|
|
||||||
- "8323:8323"
|
|
||||||
# SSH mode example:
|
|
||||||
# - "22:22"
|
|
||||||
environment:
|
|
||||||
RPKI_RTR_ENABLE_TLS: "false"
|
|
||||||
RPKI_RTR_TCP_ADDR: "0.0.0.0:323"
|
|
||||||
RPKI_RTR_TLS_ADDR: "0.0.0.0:324"
|
|
||||||
RPKI_RTR_DB_PATH: "${RPKI_RTR_DB_PATH:-/app/rtr-db}"
|
|
||||||
RPKI_RTR_CCR_DIR: "${RPKI_RTR_CCR_DIR:-/app/data}"
|
|
||||||
RPKI_RTR_SLURM_DIR: "${RPKI_RTR_SLURM_DIR:-/app/slurm}"
|
|
||||||
RPKI_RTR_REPORT_DIR: "${RPKI_RTR_REPORT_DIR:-/app/report}"
|
|
||||||
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS: "${RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS:-300}"
|
|
||||||
RPKI_RTR_REPORT_HISTORY_LIMIT: "${RPKI_RTR_REPORT_HISTORY_LIMIT:-10}"
|
|
||||||
RPKI_RTR_TIMEZONE: "${RPKI_RTR_TIMEZONE:-Asia/Shanghai}"
|
|
||||||
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "${RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS:-300}"
|
|
||||||
RPKI_RTR_MAX_DELTA: "${RPKI_RTR_MAX_DELTA:-10}"
|
|
||||||
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "${RPKI_RTR_MAX_CONCURRENT_HANDSHAKES:-128}"
|
|
||||||
RPKI_RTR_ADMIN_ADDR: "${RPKI_RTR_ADMIN_ADDR:-}"
|
|
||||||
RPKI_RTR_ADMIN_TOKEN: "${RPKI_RTR_ADMIN_TOKEN:-}"
|
|
||||||
RUST_LOG: "${RUST_LOG:-info}"
|
|
||||||
# SSH mode example:
|
|
||||||
# RPKI_RTR_ENABLE_SSH: "true"
|
|
||||||
# RPKI_RTR_SSH_ADDR: "0.0.0.0:22"
|
|
||||||
# RPKI_RTR_SSH_PORT: "22"
|
|
||||||
# RPKI_RTR_SSH_HOST_KEY_PATH: "/app/certs/ssh_host_rsa_key"
|
|
||||||
# RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH: "/app/certs/rtr-authorized_keys"
|
|
||||||
# RPKI_RTR_SSH_USERNAME: "rpki-rtr"
|
|
||||||
# RPKI_RTR_SSH_SUBSYSTEM_NAME: "rpki-rtr"
|
|
||||||
# Optional: enable password auth in addition to publickey
|
|
||||||
# RPKI_RTR_SSH_PASSWORD: "test-password"
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CCR_HOST_DIR:-../../data}:${RPKI_RTR_CCR_DIR:-/app/data}:ro
|
|
||||||
- ${RPKI_RTR_DB_HOST_DIR:-../../rtr-db}:${RPKI_RTR_DB_PATH:-/app/rtr-db}
|
|
||||||
- ${RPKI_RTR_SLURM_HOST_DIR:-../../data}:${RPKI_RTR_SLURM_DIR:-/app/slurm}
|
|
||||||
- ${RPKI_RTR_LOG_HOST_DIR:-../../logs/server}:/app/logs
|
|
||||||
- ${RPKI_RTR_REPORT_HOST_DIR:-../../report}:${RPKI_RTR_REPORT_DIR:-/app/report}
|
|
||||||
# TLS mode example:
|
|
||||||
# - ../../certs/ssh:/app/certs:ro
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
mkdir -p /app/logs
|
|
||||||
|
|
||||||
log_name="${HOSTNAME:-rpki-rtr}"
|
|
||||||
stdout_log="/app/logs/${log_name}.stdout.log"
|
|
||||||
stderr_log="/app/logs/${log_name}.stderr.log"
|
|
||||||
|
|
||||||
exec /usr/local/bin/rpki_rtr "$@" >>"$stdout_log" 2>>"$stderr_log"
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
# RTR Admin API
|
|
||||||
|
|
||||||
本文档描述 RTR server 当前提供的 HTTP 管理接口。
|
|
||||||
|
|
||||||
## 启用方式
|
|
||||||
|
|
||||||
Admin API 默认关闭。设置 `RPKI_RTR_ADMIN_ADDR` 后启用:
|
|
||||||
|
|
||||||
```env
|
|
||||||
RPKI_RTR_ADMIN_ADDR=127.0.0.1:8323
|
|
||||||
RPKI_RTR_ADMIN_TOKEN=change-me
|
|
||||||
```
|
|
||||||
|
|
||||||
如果在 Docker 中需要从宿主机访问,容器内建议监听:
|
|
||||||
|
|
||||||
```env
|
|
||||||
RPKI_RTR_ADMIN_ADDR=0.0.0.0:8323
|
|
||||||
RPKI_RTR_ADMIN_TOKEN=change-me
|
|
||||||
```
|
|
||||||
|
|
||||||
并在 compose 中映射端口:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
ports:
|
|
||||||
- "8323:8323"
|
|
||||||
```
|
|
||||||
|
|
||||||
安全规则:
|
|
||||||
|
|
||||||
- `RPKI_RTR_ADMIN_ADDR` 为空时,不启动 Admin API。
|
|
||||||
- 非 loopback 地址,例如 `0.0.0.0:8323`,必须配置 `RPKI_RTR_ADMIN_TOKEN`,否则 admin server 会拒绝启动。
|
|
||||||
- 设置 token 后,请求必须带 `Authorization: Bearer <token>`。
|
|
||||||
|
|
||||||
## 通用 Headers
|
|
||||||
|
|
||||||
```http
|
|
||||||
Authorization: Bearer change-me
|
|
||||||
Content-Type: application/json
|
|
||||||
```
|
|
||||||
|
|
||||||
GET 请求不需要 `Content-Type`。
|
|
||||||
|
|
||||||
## GET /admin/rtr/health
|
|
||||||
|
|
||||||
用于检查 Admin API 是否可达,以及当前启用了哪些管理能力。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://127.0.0.1:8323/admin/rtr/health \
|
|
||||||
-H "Authorization: Bearer change-me"
|
|
||||||
```
|
|
||||||
|
|
||||||
响应示例:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "ok",
|
|
||||||
"config_api": true,
|
|
||||||
"source_reload_api": true,
|
|
||||||
"slurm_api": true,
|
|
||||||
"logs_api": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## GET /admin/rtr/logs/tail
|
|
||||||
|
|
||||||
实时读取 RTR 服务日志,效果类似 `tail -f`。默认读取 stdout 日志,先返回最近 200 行,然后持续输出新增内容。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -N "http://127.0.0.1:8323/admin/rtr/logs/tail?stream=stdout&lines=200&follow=true" \
|
|
||||||
-H "Authorization: Bearer change-me"
|
|
||||||
```
|
|
||||||
|
|
||||||
Query 参数:
|
|
||||||
|
|
||||||
| 参数 | 默认值 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `stream` | `stdout` | 可选 `stdout` 或 `stderr`。 |
|
|
||||||
| `lines` | `200` | 首次返回的历史行数,范围会限制在 `1..=5000`。 |
|
|
||||||
| `follow` | `true` | `true` 时保持连接并持续输出新增日志;`false` 时只返回当前 tail 内容。 |
|
|
||||||
|
|
||||||
日志文件路径按容器 entrypoint 的规则解析:
|
|
||||||
|
|
||||||
- 目录:`RPKI_RTR_LOG_DIR`,默认 `/app/logs`。
|
|
||||||
- 文件名前缀:`RPKI_RTR_LOG_NAME`,未设置时使用 `HOSTNAME`,再未设置时为 `rpki-rtr`。
|
|
||||||
- stdout 文件:`<name>.stdout.log`。
|
|
||||||
- stderr 文件:`<name>.stderr.log`。
|
|
||||||
|
|
||||||
## GET /admin/rtr/config
|
|
||||||
|
|
||||||
查询当前运行中的 runtime 配置。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://127.0.0.1:8323/admin/rtr/config \
|
|
||||||
-H "Authorization: Bearer change-me"
|
|
||||||
```
|
|
||||||
|
|
||||||
响应示例:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "ok",
|
|
||||||
"config": {
|
|
||||||
"max_delta": 10,
|
|
||||||
"prune_delta_by_snapshot_size": true,
|
|
||||||
"source_refresh_interval_seconds": 300,
|
|
||||||
"runtime_report_interval_seconds": 300,
|
|
||||||
"report_history_limit": 10,
|
|
||||||
"timezone": "Asia/Shanghai",
|
|
||||||
"timing": {
|
|
||||||
"refresh": 3600,
|
|
||||||
"retry": 600,
|
|
||||||
"expire": 7200
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## POST /admin/rtr/config
|
|
||||||
|
|
||||||
运行中动态修改 RTR server 的部分 runtime 配置。请求体是 JSON object,支持部分更新,只需要传要修改的字段。
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `max_delta` | integer | 每个 RTR 版本最多保留的 delta 条数,必须 `>= 1`。 |
|
|
||||||
| `prune_delta_by_snapshot_size` | boolean | 是否按 snapshot wire size 裁剪 delta window。 |
|
|
||||||
| `source_refresh_interval_seconds` | integer | CCR/SLURM source refresh 间隔,单位秒,必须 `>= 1`。 |
|
|
||||||
| `runtime_report_interval_seconds` | integer | `rtr-runtime-*.json` 周期写入间隔,单位秒,必须 `>= 1`。 |
|
|
||||||
| `report_history_limit` | integer | 每类 report 文件滚动保留数量,必须 `>= 1`。 |
|
|
||||||
| `timezone` | string | IANA 时区名,例如 `Asia/Shanghai`、`UTC`、`Europe/London`。 |
|
|
||||||
| `timing.refresh` | integer | RTR EndOfData `refresh`,单位秒。 |
|
|
||||||
| `timing.retry` | integer | RTR EndOfData `retry`,单位秒。 |
|
|
||||||
| `timing.expire` | integer | RTR EndOfData `expire`,必须大于 `refresh` 和 `retry`。 |
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://127.0.0.1:8323/admin/rtr/config \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Bearer change-me" \
|
|
||||||
-d '{"max_delta": 6}'
|
|
||||||
```
|
|
||||||
|
|
||||||
成功响应:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "ok",
|
|
||||||
"config": {
|
|
||||||
"max_delta": 6,
|
|
||||||
"prune_delta_by_snapshot_size": true,
|
|
||||||
"source_refresh_interval_seconds": 300,
|
|
||||||
"runtime_report_interval_seconds": 300,
|
|
||||||
"report_history_limit": 10,
|
|
||||||
"timezone": "Asia/Shanghai",
|
|
||||||
"timing": {
|
|
||||||
"refresh": 3600,
|
|
||||||
"retry": 600,
|
|
||||||
"expire": 7200
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## GET /admin/rtr/slurm/files
|
|
||||||
|
|
||||||
列出 `RPKI_RTR_SLURM_DIR` 下的 SLURM 文件。只返回合法的 `*.slurm` 和 `*.slurm.disabled` 文件,不返回 `.backup/`。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://127.0.0.1:8323/admin/rtr/slurm/files \
|
|
||||||
-H "Authorization: Bearer change-me"
|
|
||||||
```
|
|
||||||
|
|
||||||
响应示例:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "ok",
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"name": "local-policy.slurm",
|
|
||||||
"path": "/app/slurm/local-policy.slurm",
|
|
||||||
"enabled": true,
|
|
||||||
"size_bytes": 168,
|
|
||||||
"modified_unix_seconds": 1782100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "old-policy.slurm.disabled",
|
|
||||||
"path": "/app/slurm/old-policy.slurm.disabled",
|
|
||||||
"enabled": false,
|
|
||||||
"size_bytes": 168,
|
|
||||||
"modified_unix_seconds": 1782100100
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## GET /admin/rtr/slurm/files/{name}
|
|
||||||
|
|
||||||
读取单个 SLURM 文件,前端编辑页面可以直接使用这个接口加载内容。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://127.0.0.1:8323/admin/rtr/slurm/files/local-policy.slurm \
|
|
||||||
-H "Authorization: Bearer change-me"
|
|
||||||
```
|
|
||||||
|
|
||||||
响应示例:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "ok",
|
|
||||||
"file": {
|
|
||||||
"name": "local-policy.slurm",
|
|
||||||
"path": "/app/slurm/local-policy.slurm",
|
|
||||||
"enabled": true,
|
|
||||||
"size_bytes": 168,
|
|
||||||
"modified_unix_seconds": 1782100000,
|
|
||||||
"content": "{ \"slurmVersion\": 1, \"validationOutputFilters\": { \"prefixFilters\": [], \"bgpsecFilters\": [] }, \"locallyAddedAssertions\": { \"prefixAssertions\": [], \"bgpsecAssertions\": [] } }"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## POST /admin/rtr/slurm/files
|
|
||||||
|
|
||||||
新增或覆盖一个 SLURM 文件。文件名放在请求体中。
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "local-policy.slurm",
|
|
||||||
"content": "{ \"slurmVersion\": 1, \"validationOutputFilters\": { \"prefixFilters\": [], \"bgpsecFilters\": [] }, \"locallyAddedAssertions\": { \"prefixAssertions\": [], \"bgpsecAssertions\": [] } }",
|
|
||||||
"reload": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## PUT /admin/rtr/slurm/files/{name}
|
|
||||||
|
|
||||||
新增或覆盖指定 SLURM 文件。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X PUT http://127.0.0.1:8323/admin/rtr/slurm/files/local-policy.slurm \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Bearer change-me" \
|
|
||||||
-d '{
|
|
||||||
"content": "{ \"slurmVersion\": 1, \"validationOutputFilters\": { \"prefixFilters\": [], \"bgpsecFilters\": [] }, \"locallyAddedAssertions\": { \"prefixAssertions\": [], \"bgpsecAssertions\": [] } }",
|
|
||||||
"reload": true
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## DELETE /admin/rtr/slurm/files/{name}
|
|
||||||
|
|
||||||
删除指定 SLURM 文件。删除前会先备份。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X DELETE "http://127.0.0.1:8323/admin/rtr/slurm/files/local-policy.slurm?reload=true" \
|
|
||||||
-H "Authorization: Bearer change-me"
|
|
||||||
```
|
|
||||||
|
|
||||||
## POST /admin/rtr/slurm/files/{name}/disable
|
|
||||||
|
|
||||||
将启用文件改名为失效文件:
|
|
||||||
|
|
||||||
```text
|
|
||||||
local-policy.slurm -> local-policy.slurm.disabled
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST "http://127.0.0.1:8323/admin/rtr/slurm/files/local-policy.slurm/disable?reload=true" \
|
|
||||||
-H "Authorization: Bearer change-me"
|
|
||||||
```
|
|
||||||
|
|
||||||
## POST /admin/rtr/slurm/files/{name}/enable
|
|
||||||
|
|
||||||
将失效文件改名为启用文件:
|
|
||||||
|
|
||||||
```text
|
|
||||||
local-policy.slurm.disabled -> local-policy.slurm
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST "http://127.0.0.1:8323/admin/rtr/slurm/files/local-policy.slurm.disabled/enable?reload=true" \
|
|
||||||
-H "Authorization: Bearer change-me"
|
|
||||||
```
|
|
||||||
|
|
||||||
## POST /admin/rtr/slurm/reload
|
|
||||||
|
|
||||||
不修改文件,只立即触发一次 CCR/SLURM source reload。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://127.0.0.1:8323/admin/rtr/slurm/reload \
|
|
||||||
-H "Authorization: Bearer change-me"
|
|
||||||
```
|
|
||||||
|
|
||||||
## SLURM 文件规则
|
|
||||||
|
|
||||||
- `*.slurm`:启用,会被 source refresh 加载。
|
|
||||||
- `*.slurm.disabled`:失效,不会被加载。
|
|
||||||
- `.backup/`:接口自动生成的备份目录。
|
|
||||||
- 文件名只能是 basename,不能包含 `/`、`\` 或 `..`。
|
|
||||||
- 文件名只能包含 ASCII 字母、数字、`.`、`_`、`-`。
|
|
||||||
- 写入内容必须是合法 JSON,并且能被解析为合法 SLURM 文件。
|
|
||||||
- 单个请求体默认限制为 5 MiB。
|
|
||||||
- 覆盖、删除前会写入 `.backup/` 备份。
|
|
||||||
|
|
||||||
## 错误响应
|
|
||||||
|
|
||||||
| HTTP 状态码 | 含义 |
|
|
||||||
| --- | --- |
|
|
||||||
| `400 Bad Request` | JSON 非法、配置值非法、SLURM 文件名非法、目标文件不存在、reload 失败等。 |
|
|
||||||
| `401 Unauthorized` | 配置了 token,但请求缺少或使用了错误的 `Authorization`。 |
|
|
||||||
| `404 Not Found` | 路径或方法不支持。 |
|
|
||||||
| `413 Payload Too Large` | 请求体超过限制。 |
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
IMAGE_DIR="${ROOT_DIR}/images"
|
|
||||||
SERVER_ENV_FILE="${ROOT_DIR}/deploy/server/.env"
|
|
||||||
CONSOLE_ENV_FILE="${ROOT_DIR}/deploy/console/.env"
|
|
||||||
|
|
||||||
SERVER_TAR="${RPKI_RTR_SERVER_IMAGE_TAR:-}"
|
|
||||||
CLIENT_TAR="${RPKI_RTR_CLIENT_IMAGE_TAR:-}"
|
|
||||||
CONSOLE_TAR="${RPKI_RTR_CONSOLE_IMAGE_TAR:-}"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: scripts/00_0_install.sh [options]
|
|
||||||
|
|
||||||
Load offline Docker image tarballs from ./images, then run scripts/00_1_pre_check.sh.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--server-tar FILE Server image tarball. Env: RPKI_RTR_SERVER_IMAGE_TAR
|
|
||||||
--client-tar FILE Debug client image tarball. Env: RPKI_RTR_CLIENT_IMAGE_TAR
|
|
||||||
--console-tar FILE Console image tarball. Env: RPKI_RTR_CONSOLE_IMAGE_TAR
|
|
||||||
-h, --help Show this help.
|
|
||||||
|
|
||||||
Default tarballs under ./images:
|
|
||||||
server: rpki-rtr-*.tar, excluding rpki-rtr-debug-client-*.tar
|
|
||||||
client: rpki-rtr-debug-client-*.tar
|
|
||||||
console: rpki-console-ui-*.tar
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[00_install] %s\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
stage() {
|
|
||||||
printf '\n====== %s ======\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf '[00_install] ERROR: %s\n' "$*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
set_env_var() {
|
|
||||||
local env_file="$1"
|
|
||||||
local key="$2"
|
|
||||||
local value="$3"
|
|
||||||
local tmp_file
|
|
||||||
|
|
||||||
[ -f "${env_file}" ] || die "env file not found: ${env_file}"
|
|
||||||
tmp_file="${env_file}.tmp.$$"
|
|
||||||
|
|
||||||
awk -v key="${key}" -v value="${value}" '
|
|
||||||
BEGIN { done = 0 }
|
|
||||||
$0 ~ "^[[:space:]]*#?[[:space:]]*" key "=" {
|
|
||||||
if (!done) {
|
|
||||||
print key "=" value
|
|
||||||
done = 1
|
|
||||||
}
|
|
||||||
next
|
|
||||||
}
|
|
||||||
{ print }
|
|
||||||
END {
|
|
||||||
if (!done) {
|
|
||||||
print key "=" value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
' "${env_file}" >"${tmp_file}"
|
|
||||||
mv "${tmp_file}" "${env_file}"
|
|
||||||
}
|
|
||||||
|
|
||||||
while [ "$#" -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--server-tar)
|
|
||||||
[ "$#" -ge 2 ] || die "--server-tar requires a value"
|
|
||||||
SERVER_TAR="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--client-tar)
|
|
||||||
[ "$#" -ge 2 ] || die "--client-tar requires a value"
|
|
||||||
CLIENT_TAR="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--console-tar)
|
|
||||||
[ "$#" -ge 2 ] || die "--console-tar requires a value"
|
|
||||||
CONSOLE_TAR="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "unknown option: $1"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
resolve_tar() {
|
|
||||||
local path="$1"
|
|
||||||
|
|
||||||
if [ -f "${path}" ]; then
|
|
||||||
cd "$(dirname "${path}")" && printf '%s/%s\n' "$(pwd -P)" "$(basename "${path}")"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "${ROOT_DIR}/${path}" ]; then
|
|
||||||
cd "$(dirname "${ROOT_DIR}/${path}")" && printf '%s/%s\n' "$(pwd -P)" "$(basename "${path}")"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
load_image() {
|
|
||||||
local label="$1"
|
|
||||||
local tar_path="$2"
|
|
||||||
local output image
|
|
||||||
|
|
||||||
[ -f "${tar_path}" ] || die "${label} image tarball not found: ${tar_path}"
|
|
||||||
output="$(docker load -i "${tar_path}" 2>&1)" || {
|
|
||||||
printf '%s\n' "${output}" >&2
|
|
||||||
die "failed to load ${label} image from ${tar_path}"
|
|
||||||
}
|
|
||||||
printf '%s\n' "${output}" >&2
|
|
||||||
|
|
||||||
image="$(printf '%s\n' "${output}" | sed -n 's/^Loaded image: //p' | tail -n 1)"
|
|
||||||
[ -n "${image}" ] || die "cannot determine ${label} image name from docker load output"
|
|
||||||
printf '%s\n' "${image}"
|
|
||||||
}
|
|
||||||
|
|
||||||
find_single_tar() {
|
|
||||||
local label="$1"
|
|
||||||
local include_pattern="$2"
|
|
||||||
local exclude_pattern="${3:-}"
|
|
||||||
local matches=()
|
|
||||||
local file
|
|
||||||
|
|
||||||
while IFS= read -r -d '' file; do
|
|
||||||
matches+=("${file}")
|
|
||||||
done < <(
|
|
||||||
if [ -n "${exclude_pattern}" ]; then
|
|
||||||
find "${IMAGE_DIR}" -maxdepth 1 -type f -name "${include_pattern}" ! -name "${exclude_pattern}" -print0 | sort -z
|
|
||||||
else
|
|
||||||
find "${IMAGE_DIR}" -maxdepth 1 -type f -name "${include_pattern}" -print0 | sort -z
|
|
||||||
fi
|
|
||||||
)
|
|
||||||
|
|
||||||
case "${#matches[@]}" in
|
|
||||||
0)
|
|
||||||
die "no ${label} image tarball found under ${IMAGE_DIR} matching ${include_pattern}"
|
|
||||||
;;
|
|
||||||
1)
|
|
||||||
printf '%s\n' "${matches[0]}"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
printf '[00_install] ERROR: multiple %s image tarballs found:\n' "${label}" >&2
|
|
||||||
printf ' %s\n' "${matches[@]}" >&2
|
|
||||||
die "please specify --${label}-tar or RPKI_RTR_${label^^}_IMAGE_TAR"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
stage "Checking Docker"
|
|
||||||
command -v docker >/dev/null 2>&1 || die "docker command not found"
|
|
||||||
[ -d "${IMAGE_DIR}" ] || die "image directory not found: ${IMAGE_DIR}"
|
|
||||||
[ -f "${SERVER_ENV_FILE}" ] || die "server env file not found: ${SERVER_ENV_FILE}"
|
|
||||||
|
|
||||||
if [ -n "${SERVER_TAR}" ]; then
|
|
||||||
SERVER_TAR="$(resolve_tar "${SERVER_TAR}")" || die "server image tarball not found: ${SERVER_TAR}"
|
|
||||||
else
|
|
||||||
SERVER_TAR="$(find_single_tar server 'rpki-rtr-*.tar' 'rpki-rtr-debug-client-*.tar')"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "${CLIENT_TAR}" ]; then
|
|
||||||
CLIENT_TAR="$(resolve_tar "${CLIENT_TAR}")" || die "client image tarball not found: ${CLIENT_TAR}"
|
|
||||||
else
|
|
||||||
CLIENT_TAR="$(find_single_tar client 'rpki-rtr-debug-client-*.tar')"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "${CONSOLE_TAR}" ]; then
|
|
||||||
CONSOLE_TAR="$(resolve_tar "${CONSOLE_TAR}")" || die "console image tarball not found: ${CONSOLE_TAR}"
|
|
||||||
else
|
|
||||||
CONSOLE_TAR="$(find_single_tar console 'rpki-console-ui-*.tar')"
|
|
||||||
fi
|
|
||||||
|
|
||||||
stage "Loading Docker Images"
|
|
||||||
SERVER_IMAGE="$(load_image server "${SERVER_TAR}")"
|
|
||||||
CLIENT_IMAGE="$(load_image client "${CLIENT_TAR}")"
|
|
||||||
CONSOLE_IMAGE="$(load_image console "${CONSOLE_TAR}")"
|
|
||||||
|
|
||||||
stage "Updating Deploy Env"
|
|
||||||
set_env_var "${SERVER_ENV_FILE}" RPKI_RTR_SERVER_IMAGE "${SERVER_IMAGE}"
|
|
||||||
set_env_var "${CONSOLE_ENV_FILE}" RPKI_CONSOLE_UI_IMAGE "${CONSOLE_IMAGE}"
|
|
||||||
|
|
||||||
stage "Running Pre-Check"
|
|
||||||
RPKI_RTR_SERVER_IMAGE="${SERVER_IMAGE}" \
|
|
||||||
RPKI_RTR_CLIENT_IMAGE="${CLIENT_IMAGE}" \
|
|
||||||
bash "${SCRIPT_DIR}/00_1_pre_check.sh" || die "pre-check failed"
|
|
||||||
|
|
||||||
stage "Install Finished"
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
|
|
||||||
PRECHECK_WORK_DIR="${PRECHECK_WORK_DIR:-${ROOT_DIR}/.precheck}"
|
|
||||||
PRECHECK_ENV_FILE="${PRECHECK_ENV_FILE:-${PRECHECK_WORK_DIR}/precheck.env}"
|
|
||||||
PRECHECK_WAIT_SECS="${PRECHECK_WAIT_SECS:-60}"
|
|
||||||
KEEP_PRECHECK="${KEEP_PRECHECK:-0}"
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[00_1_pre_check] %s\n' "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
stage() {
|
|
||||||
printf '\n====== %s ======\n' "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf '[00_1_pre_check] ERROR: %s\n' "$*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
command -v docker >/dev/null 2>&1 || die "docker command not found"
|
|
||||||
if docker compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker compose)
|
|
||||||
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker-compose)
|
|
||||||
else
|
|
||||||
die "docker compose is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
stage "Preparing Pre-Check Data"
|
|
||||||
bash "${SCRIPT_DIR}/00_2_pre_check_prepare.sh" || die "prepare step failed"
|
|
||||||
|
|
||||||
set -a
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
. "${PRECHECK_ENV_FILE}"
|
|
||||||
set +a
|
|
||||||
|
|
||||||
ADMIN_PORT="${RPKI_RTR_ADMIN_ADDR##*:}"
|
|
||||||
ADMIN_TOKEN="${RPKI_RTR_ADMIN_TOKEN}"
|
|
||||||
|
|
||||||
server_compose() {
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${PRECHECK_ENV_FILE}" -p "${PRECHECK_PROJECT}" -f "${PRECHECK_SERVER_COMPOSE_FILE}" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
client_compose() {
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${PRECHECK_ENV_FILE}" -p "${PRECHECK_PROJECT}" -f "${PRECHECK_CLIENT_COMPOSE_FILE}" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
KEEP_PRECHECK="${KEEP_PRECHECK}" PRECHECK_WORK_DIR="${PRECHECK_WORK_DIR}" PRECHECK_ENV_FILE="${PRECHECK_ENV_FILE}" \
|
|
||||||
bash "${SCRIPT_DIR}/00_3_pre_check_cleanup.sh"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
http_get() {
|
|
||||||
local host="$1"
|
|
||||||
local port="$2"
|
|
||||||
local path="$3"
|
|
||||||
local token="$4"
|
|
||||||
|
|
||||||
exec 3<>"/dev/tcp/${host}/${port}" || return 1
|
|
||||||
printf 'GET %s HTTP/1.1\r\nHost: %s:%s\r\nAuthorization: Bearer %s\r\nConnection: close\r\n\r\n' \
|
|
||||||
"${path}" "${host}" "${port}" "${token}" >&3
|
|
||||||
cat <&3
|
|
||||||
exec 3>&-
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_admin_health() {
|
|
||||||
local deadline=$((SECONDS + PRECHECK_WAIT_SECS))
|
|
||||||
local response
|
|
||||||
|
|
||||||
log "waiting for server health"
|
|
||||||
while [ "${SECONDS}" -lt "${deadline}" ]; do
|
|
||||||
response="$(http_get 127.0.0.1 "${ADMIN_PORT}" /admin/rtr/health "${ADMIN_TOKEN}" 2>/dev/null || true)"
|
|
||||||
if printf '%s' "${response}" | grep -q '"status"[[:space:]]*:[[:space:]]*"ok"'; then
|
|
||||||
log "admin health check passed"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$(server_compose ps -q rpki-rtr | wc -l | tr -d ' ')" = "0" ]; then
|
|
||||||
server_compose logs rpki-rtr || true
|
|
||||||
die "server container is not running"
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
server_compose logs rpki-rtr || true
|
|
||||||
die "admin health check did not pass within ${PRECHECK_WAIT_SECS}s"
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_client_eod() {
|
|
||||||
local deadline=$((SECONDS + PRECHECK_WAIT_SECS))
|
|
||||||
local log_file
|
|
||||||
|
|
||||||
log "waiting for client response"
|
|
||||||
while [ "${SECONDS}" -lt "${deadline}" ]; do
|
|
||||||
for log_file in "${PRECHECK_WORK_DIR}"/logs/client/*.stdout.log; do
|
|
||||||
[ -f "${log_file}" ] || continue
|
|
||||||
if grep -q 'EndOfData:' "${log_file}" && grep -q 'summary[[:space:]]*:' "${log_file}"; then
|
|
||||||
log "client received EndOfData and payload summary"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if grep -qi 'Error Report' "${log_file}"; then
|
|
||||||
cat "${log_file}" >&2
|
|
||||||
die "client received RTR Error Report"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
find "${PRECHECK_WORK_DIR}/logs/client" -type f -maxdepth 1 -print -exec cat {} \; >&2 || true
|
|
||||||
die "client did not receive EndOfData and summary within ${PRECHECK_WAIT_SECS}s"
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_no_error_logs() {
|
|
||||||
local bad
|
|
||||||
log "checking logs"
|
|
||||||
bad="$(find "${PRECHECK_WORK_DIR}/logs" -type f -name '*.log' -print0 \
|
|
||||||
| xargs -0 grep -Ein 'panic|failed|RTR admin config server exited|Error Report' 2>/dev/null || true)"
|
|
||||||
if [ -n "${bad}" ]; then
|
|
||||||
printf '%s\n' "${bad}" >&2
|
|
||||||
die "error pattern found in pre-check logs"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_report_written() {
|
|
||||||
local deadline=$((SECONDS + PRECHECK_WAIT_SECS))
|
|
||||||
|
|
||||||
log "waiting for source report"
|
|
||||||
while [ "${SECONDS}" -lt "${deadline}" ]; do
|
|
||||||
if find "${PRECHECK_WORK_DIR}/report" -type f -name 'rtr-source-*.json' | grep -q .; then
|
|
||||||
log "source report written"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
die "server did not write rtr-source report within ${PRECHECK_WAIT_SECS}s"
|
|
||||||
}
|
|
||||||
|
|
||||||
stage "Starting RTR Server"
|
|
||||||
server_compose up -d --no-build rpki-rtr
|
|
||||||
wait_for_admin_health
|
|
||||||
assert_report_written
|
|
||||||
|
|
||||||
stage "Starting Debug Client"
|
|
||||||
client_compose up -d --no-build rtr-debug-client
|
|
||||||
wait_for_client_eod
|
|
||||||
|
|
||||||
assert_no_error_logs
|
|
||||||
stage "Pre-Check Passed"
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUT_PATH="${1:-}"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: scripts/00_2_generate_ccr.sh OUT.ccr
|
|
||||||
|
|
||||||
Generate a small deterministic CCR snapshot for pre-check.
|
|
||||||
This script writes a fixed base64-encoded CCR fixture and does not require Python.
|
|
||||||
The generated snapshot contains:
|
|
||||||
- VRP: 10.0.0.0/24 maxLen 24 AS65001
|
|
||||||
- VRP: 10.0.1.0/24 maxLen 24 AS65003
|
|
||||||
- VRP: 2001:db8:1::/48 maxLen 48 AS65002
|
|
||||||
- ASPA: customer AS65010 providers AS65011, AS65012, AS65013
|
|
||||||
- ASPA: customer AS65020 provider AS65021
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf '[00_2_generate_ccr] ERROR: %s\n' "$*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ "${OUT_PATH}" = "-h" ] || [ "${OUT_PATH}" = "--help" ]; then
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -n "${OUT_PATH}" ] || die "missing OUT.ccr"
|
|
||||||
mkdir -p "$(dirname "${OUT_PATH}")"
|
|
||||||
|
|
||||||
decode_base64() {
|
|
||||||
if command -v base64 >/dev/null 2>&1; then
|
|
||||||
base64 -d
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v openssl >/dev/null 2>&1; then
|
|
||||||
openssl base64 -d
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
die "base64 or openssl command is required to write CCR fixture"
|
|
||||||
}
|
|
||||||
|
|
||||||
cat <<'EOF' | decode_base64 >"${OUT_PATH}"
|
|
||||||
MIIBAwYLKoZIhvcNAQkQATaggfMwgfACAQAwDQYJYIZIAWUDBAIBBQAYDzIwMjYwNDAzMDAwMjAx
|
|
||||||
WqJ9MHswVzAaAgMA/ekwEzARBAIAATALMAkDBAAKAAACARgwGgIDAP3rMBMwEQQCAAEwCzAJAwQA
|
|
||||||
CgABAgEYMB0CAwD96jAWMBQEAgACMA4wDAMHACABDbgAAQIBMAQgsUgCHfy5AJcFp5RymOD0HuGO
|
|
||||||
wfhoWpll90ugYECGQPSjTDBKMCYwFgIDAP3yMA8CAwD98wIDAP30AgMA/fUwDAIDAP38MAUCAwD9
|
|
||||||
/QQgRPxVPOOUsUuuV4tZ8BG5PHNtKiaEr23Ird+yNxlWJ+w=
|
|
||||||
EOF
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
|
|
||||||
PRECHECK_WORK_DIR="${PRECHECK_WORK_DIR:-${ROOT_DIR}/.precheck}"
|
|
||||||
PRECHECK_PROJECT="${PRECHECK_PROJECT:-rpki-precheck}"
|
|
||||||
PRECHECK_ADMIN_TOKEN="${PRECHECK_ADMIN_TOKEN:-rpki-precheck-token}"
|
|
||||||
PRECHECK_PROTOCOL_VERSION="${PRECHECK_PROTOCOL_VERSION:-2}"
|
|
||||||
PRECHECK_SERVER_IMAGE="${PRECHECK_SERVER_IMAGE:-${RPKI_RTR_SERVER_IMAGE:-rpki-rtr-arm64:v1.2}}"
|
|
||||||
PRECHECK_CLIENT_IMAGE="${PRECHECK_CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client-arm64:v1.2}}"
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[00_1_pre_check_prepare] %s\n' "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf '[00_1_pre_check_prepare] ERROR: %s\n' "$*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
abs_dir() {
|
|
||||||
local path="$1"
|
|
||||||
mkdir -p "${path}"
|
|
||||||
cd "${path}" && pwd -P
|
|
||||||
}
|
|
||||||
|
|
||||||
[ -f "${SCRIPT_DIR}/00_2_generate_ccr.sh" ] || die "missing ${SCRIPT_DIR}/00_2_generate_ccr.sh"
|
|
||||||
|
|
||||||
rm -rf "${PRECHECK_WORK_DIR}"
|
|
||||||
mkdir -p \
|
|
||||||
"${PRECHECK_WORK_DIR}/ccr" \
|
|
||||||
"${PRECHECK_WORK_DIR}/slurm" \
|
|
||||||
"${PRECHECK_WORK_DIR}/db" \
|
|
||||||
"${PRECHECK_WORK_DIR}/logs/server" \
|
|
||||||
"${PRECHECK_WORK_DIR}/logs/client" \
|
|
||||||
"${PRECHECK_WORK_DIR}/report"
|
|
||||||
|
|
||||||
bash "${SCRIPT_DIR}/00_2_generate_ccr.sh" \
|
|
||||||
"${PRECHECK_WORK_DIR}/ccr/20260403T000201Z-precheck.ccr"
|
|
||||||
|
|
||||||
cat >"${PRECHECK_WORK_DIR}/slurm/precheck.slurm" <<'EOF'
|
|
||||||
{
|
|
||||||
"slurmVersion": 2,
|
|
||||||
"validationOutputFilters": {
|
|
||||||
"prefixFilters": [],
|
|
||||||
"bgpsecFilters": [],
|
|
||||||
"aspaFilters": []
|
|
||||||
},
|
|
||||||
"locallyAddedAssertions": {
|
|
||||||
"prefixAssertions": [
|
|
||||||
{
|
|
||||||
"prefix": "203.0.113.0/24",
|
|
||||||
"maxPrefixLength": 24,
|
|
||||||
"asn": 64496,
|
|
||||||
"comment": "pre-check local assertion"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"bgpsecAssertions": [],
|
|
||||||
"aspaAssertions": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
PRECHECK_WORK_DIR_ABS="$(abs_dir "${PRECHECK_WORK_DIR}")"
|
|
||||||
cat >"${PRECHECK_WORK_DIR}/precheck.env" <<EOF
|
|
||||||
PRECHECK_WORK_DIR=${PRECHECK_WORK_DIR_ABS}
|
|
||||||
PRECHECK_PROJECT=${PRECHECK_PROJECT}
|
|
||||||
RPKI_RTR_SERVER_IMAGE=${PRECHECK_SERVER_IMAGE}
|
|
||||||
RPKI_RTR_CLIENT_IMAGE=${PRECHECK_CLIENT_IMAGE}
|
|
||||||
PRECHECK_SERVER_COMPOSE_FILE=${ROOT_DIR}/deploy/server/docker-compose.tcp.yml
|
|
||||||
PRECHECK_CLIENT_COMPOSE_FILE=${ROOT_DIR}/deploy/client/docker-compose.tcp.yml
|
|
||||||
RPKI_RTR_CCR_HOST_DIR=${PRECHECK_WORK_DIR_ABS}/ccr
|
|
||||||
RPKI_RTR_SLURM_HOST_DIR=${PRECHECK_WORK_DIR_ABS}/slurm
|
|
||||||
RPKI_RTR_DB_HOST_DIR=${PRECHECK_WORK_DIR_ABS}/db
|
|
||||||
RPKI_RTR_LOG_HOST_DIR=${PRECHECK_WORK_DIR_ABS}/logs/server
|
|
||||||
RPKI_RTR_CLIENT_LOG_HOST_DIR=${PRECHECK_WORK_DIR_ABS}/logs/client
|
|
||||||
RPKI_RTR_REPORT_HOST_DIR=${PRECHECK_WORK_DIR_ABS}/report
|
|
||||||
RPKI_RTR_CCR_DIR=/app/data
|
|
||||||
RPKI_RTR_SLURM_DIR=/app/slurm
|
|
||||||
RPKI_RTR_DB_PATH=/app/rtr-db
|
|
||||||
RPKI_RTR_REPORT_DIR=/app/report
|
|
||||||
RPKI_RTR_ADMIN_ADDR=0.0.0.0:8323
|
|
||||||
RPKI_RTR_ADMIN_TOKEN=${PRECHECK_ADMIN_TOKEN}
|
|
||||||
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS=5
|
|
||||||
RPKI_RTR_REPORT_HISTORY_LIMIT=3
|
|
||||||
RPKI_RTR_TIMEZONE=Asia/Shanghai
|
|
||||||
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS=30
|
|
||||||
RPKI_RTR_MAX_DELTA=10
|
|
||||||
RPKI_RTR_MAX_CONNECTIONS=32
|
|
||||||
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES=8
|
|
||||||
RPKI_RTR_TCP_SERVER_ADDR=rpki-rtr:323
|
|
||||||
RPKI_RTR_PROTOCOL_VERSION=${PRECHECK_PROTOCOL_VERSION}
|
|
||||||
RUST_LOG=info
|
|
||||||
EOF
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
|
|
||||||
PRECHECK_WORK_DIR="${PRECHECK_WORK_DIR:-${ROOT_DIR}/.precheck}"
|
|
||||||
ENV_FILE="${PRECHECK_ENV_FILE:-${PRECHECK_WORK_DIR}/precheck.env}"
|
|
||||||
KEEP_PRECHECK="${KEEP_PRECHECK:-0}"
|
|
||||||
|
|
||||||
if [ -f "${ENV_FILE}" ]; then
|
|
||||||
set -a
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
. "${ENV_FILE}"
|
|
||||||
set +a
|
|
||||||
fi
|
|
||||||
|
|
||||||
PRECHECK_PROJECT="${PRECHECK_PROJECT:-rpki-precheck}"
|
|
||||||
PRECHECK_SERVER_COMPOSE_FILE="${PRECHECK_SERVER_COMPOSE_FILE:-${ROOT_DIR}/deploy/server/docker-compose.tcp.yml}"
|
|
||||||
PRECHECK_CLIENT_COMPOSE_FILE="${PRECHECK_CLIENT_COMPOSE_FILE:-${ROOT_DIR}/deploy/client/docker-compose.tcp.yml}"
|
|
||||||
COMPOSE_BIN=()
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[00_1_pre_check_cleanup] %s\n' "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
compose_run() {
|
|
||||||
local compose_file="$1"
|
|
||||||
shift
|
|
||||||
|
|
||||||
if [ "${#COMPOSE_BIN[@]}" -eq 0 ]; then
|
|
||||||
if docker compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker compose)
|
|
||||||
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker-compose)
|
|
||||||
else
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${ENV_FILE}" -p "${PRECHECK_PROJECT}" -f "${compose_file}" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
if command -v docker >/dev/null 2>&1; then
|
|
||||||
log "stopping pre-check client compose stack"
|
|
||||||
[ -f "${PRECHECK_CLIENT_COMPOSE_FILE}" ] && compose_run "${PRECHECK_CLIENT_COMPOSE_FILE}" down --remove-orphans >/dev/null 2>&1 || true
|
|
||||||
log "stopping pre-check server compose stack"
|
|
||||||
[ -f "${PRECHECK_SERVER_COMPOSE_FILE}" ] && compose_run "${PRECHECK_SERVER_COMPOSE_FILE}" down --remove-orphans >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${KEEP_PRECHECK}" = "1" ]; then
|
|
||||||
log "KEEP_PRECHECK=1, preserving ${PRECHECK_WORK_DIR}"
|
|
||||||
else
|
|
||||||
rm -rf "${PRECHECK_WORK_DIR}"
|
|
||||||
log "removed ${PRECHECK_WORK_DIR}"
|
|
||||||
fi
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
|
|
||||||
COMMON_ENV_FILE="${ROOT_DIR}/environment.server"
|
|
||||||
SERVER_ENV_FILE="${ROOT_DIR}/deploy/server/.env"
|
|
||||||
CONSOLE_ENV_FILE="${ROOT_DIR}/deploy/console/.env"
|
|
||||||
CONSOLE_COMPOSE_FILE="${ROOT_DIR}/deploy/console/docker-compose.yml"
|
|
||||||
MODE="${RPKI_RTR_SERVER_MODE:-tcp}"
|
|
||||||
COMPOSE_PROJECT="${RPKI_RTR_COMPOSE_PROJECT:-rpki-rtr}"
|
|
||||||
CONSOLE_COMPOSE_PROJECT="${RPKI_CONSOLE_COMPOSE_PROJECT:-rpki-console}"
|
|
||||||
WAIT_SECS="${RPKI_START_WAIT_SECS:-20}"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: scripts/01_start.sh [options]
|
|
||||||
|
|
||||||
Read root ./environment.server, update deploy/server/.env, then start the RTR server and console.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--env FILE Common env file. Default: ./environment.server
|
|
||||||
--mode MODE Server mode: tcp|tls|ssh|base. Default: tcp
|
|
||||||
--wait SECS Wait timeout for container running checks. Default: 20
|
|
||||||
-h, --help Show this help.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[01_start] %s\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
stage() {
|
|
||||||
printf '\n====== %s ======\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf '[01_start] ERROR: %s\n' "$*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
set_env_var() {
|
|
||||||
local env_file="$1"
|
|
||||||
local key="$2"
|
|
||||||
local value="$3"
|
|
||||||
local tmp_file
|
|
||||||
|
|
||||||
tmp_file="${env_file}.tmp.$$"
|
|
||||||
awk -v key="${key}" -v value="${value}" '
|
|
||||||
BEGIN { done = 0 }
|
|
||||||
$0 ~ "^[[:space:]]*#?[[:space:]]*" key "=" {
|
|
||||||
if (!done) {
|
|
||||||
print key "=" value
|
|
||||||
done = 1
|
|
||||||
}
|
|
||||||
next
|
|
||||||
}
|
|
||||||
{ print }
|
|
||||||
END {
|
|
||||||
if (!done) {
|
|
||||||
print key "=" value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
' "${env_file}" >"${tmp_file}"
|
|
||||||
mv "${tmp_file}" "${env_file}"
|
|
||||||
}
|
|
||||||
|
|
||||||
compose_file_for_mode() {
|
|
||||||
case "$1" in
|
|
||||||
tcp) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.tcp.yml" ;;
|
|
||||||
tls) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.tls.yml" ;;
|
|
||||||
ssh) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.ssh.yml" ;;
|
|
||||||
base) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.yml" ;;
|
|
||||||
*) die "unsupported mode '$1', expected tcp|tls|ssh|base" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
update_env_from_common() {
|
|
||||||
local target_env_file="$1"
|
|
||||||
local updated_count=0
|
|
||||||
local line key value
|
|
||||||
|
|
||||||
while IFS= read -r line || [ -n "${line}" ]; do
|
|
||||||
case "${line}" in
|
|
||||||
''|\#*) continue ;;
|
|
||||||
*=*)
|
|
||||||
key="${line%%=*}"
|
|
||||||
value="${line#*=}"
|
|
||||||
set_env_var "${target_env_file}" "${key}" "${value}"
|
|
||||||
updated_count=$((updated_count + 1))
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done <"${COMMON_ENV_FILE}"
|
|
||||||
|
|
||||||
printf '%s\n' "${updated_count}"
|
|
||||||
}
|
|
||||||
|
|
||||||
server_compose() {
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${SERVER_ENV_FILE}" -p "${COMPOSE_PROJECT}" -f "${SERVER_COMPOSE_FILE}" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
console_compose() {
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${CONSOLE_ENV_FILE}" -p "${CONSOLE_COMPOSE_PROJECT}" -f "${CONSOLE_COMPOSE_FILE}" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
container_running() {
|
|
||||||
local compose_kind="$1"
|
|
||||||
local service="$2"
|
|
||||||
local cid state
|
|
||||||
|
|
||||||
if [ "${compose_kind}" = "server" ]; then
|
|
||||||
cid="$(server_compose ps -q "${service}" 2>/dev/null || true)"
|
|
||||||
else
|
|
||||||
cid="$(console_compose ps -q "${service}" 2>/dev/null || true)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -n "${cid}" ] || return 1
|
|
||||||
state="$(docker inspect -f '{{.State.Running}}' "${cid}" 2>/dev/null || true)"
|
|
||||||
[ "${state}" = "true" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
show_service_debug() {
|
|
||||||
local compose_kind="$1"
|
|
||||||
local service="$2"
|
|
||||||
|
|
||||||
log "${service} is not running, current compose status:"
|
|
||||||
if [ "${compose_kind}" = "server" ]; then
|
|
||||||
server_compose ps "${service}" >&2 || true
|
|
||||||
log "${service} recent logs:"
|
|
||||||
server_compose logs --tail=80 "${service}" >&2 || true
|
|
||||||
else
|
|
||||||
console_compose ps "${service}" >&2 || true
|
|
||||||
log "${service} recent logs:"
|
|
||||||
console_compose logs --tail=80 "${service}" >&2 || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_running() {
|
|
||||||
local compose_kind="$1"
|
|
||||||
local service="$2"
|
|
||||||
local deadline=$((SECONDS + WAIT_SECS))
|
|
||||||
|
|
||||||
while [ "${SECONDS}" -lt "${deadline}" ]; do
|
|
||||||
if container_running "${compose_kind}" "${service}"; then
|
|
||||||
log "${service} is running"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
show_service_debug "${compose_kind}" "${service}"
|
|
||||||
die "${service} failed to reach running state after ${WAIT_SECS}s"
|
|
||||||
}
|
|
||||||
|
|
||||||
while [ "$#" -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--env)
|
|
||||||
[ "$#" -ge 2 ] || die "--env requires a value"
|
|
||||||
COMMON_ENV_FILE="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--mode)
|
|
||||||
[ "$#" -ge 2 ] || die "--mode requires a value"
|
|
||||||
MODE="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--wait)
|
|
||||||
[ "$#" -ge 2 ] || die "--wait requires a value"
|
|
||||||
WAIT_SECS="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "unknown option: $1"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
SERVER_COMPOSE_FILE="$(compose_file_for_mode "${MODE}")"
|
|
||||||
|
|
||||||
stage "Checking Docker"
|
|
||||||
command -v docker >/dev/null 2>&1 || die "docker command not found"
|
|
||||||
if docker compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker compose)
|
|
||||||
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker-compose)
|
|
||||||
else
|
|
||||||
die "docker compose is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -f "${SERVER_ENV_FILE}" ] || die "server env file not found: ${SERVER_ENV_FILE}"
|
|
||||||
[ -f "${SERVER_COMPOSE_FILE}" ] || die "server compose file not found: ${SERVER_COMPOSE_FILE}"
|
|
||||||
[ -f "${CONSOLE_ENV_FILE}" ] || die "console env file not found: ${CONSOLE_ENV_FILE}"
|
|
||||||
[ -f "${CONSOLE_COMPOSE_FILE}" ] || die "console compose file not found: ${CONSOLE_COMPOSE_FILE}"
|
|
||||||
|
|
||||||
if [ -f "${COMMON_ENV_FILE}" ]; then
|
|
||||||
stage "Reading Environment"
|
|
||||||
server_updated_count="$(update_env_from_common "${SERVER_ENV_FILE}")"
|
|
||||||
log "updated ${server_updated_count} server env variable(s)"
|
|
||||||
else
|
|
||||||
stage "Reading Environment"
|
|
||||||
log "environment.server file not found, skip env update"
|
|
||||||
fi
|
|
||||||
|
|
||||||
stage "Starting RTR Server"
|
|
||||||
log "starting docker compose service: rpki-rtr"
|
|
||||||
server_compose up -d --no-build rpki-rtr
|
|
||||||
log "waiting for rpki-rtr to be running"
|
|
||||||
wait_for_running server rpki-rtr
|
|
||||||
|
|
||||||
stage "Starting Console"
|
|
||||||
log "starting docker compose service: rpki-console-ui"
|
|
||||||
console_compose up -d --no-build rpki-console-ui
|
|
||||||
log "waiting for rpki-console-ui to be running"
|
|
||||||
wait_for_running console rpki-console-ui
|
|
||||||
|
|
||||||
stage "Start Finished"
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
|
|
||||||
SERVER_ENV_FILE="${ROOT_DIR}/deploy/server/.env"
|
|
||||||
CONSOLE_ENV_FILE="${ROOT_DIR}/deploy/console/.env"
|
|
||||||
SERVER_COMPOSE_FILE="${ROOT_DIR}/deploy/server/docker-compose.tcp.yml"
|
|
||||||
CONSOLE_COMPOSE_FILE="${ROOT_DIR}/deploy/console/docker-compose.yml"
|
|
||||||
SERVER_PROJECT="${RPKI_RTR_COMPOSE_PROJECT:-rpki-rtr}"
|
|
||||||
CONSOLE_PROJECT="${RPKI_CONSOLE_COMPOSE_PROJECT:-rpki-console}"
|
|
||||||
WAIT_SECS="${RPKI_SELF_CHECK_WAIT_SECS:-60}"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: scripts/02_self_check.sh [options]
|
|
||||||
|
|
||||||
Check whether the RTR server and console started by scripts/01_start.sh are healthy.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--wait SECS Wait timeout for each check. Default: 60
|
|
||||||
-h, --help Show this help.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[02_self_check] %s\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
stage() {
|
|
||||||
printf '\n====== %s ======\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf '[02_self_check] ERROR: %s\n' "$*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
while [ "$#" -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--wait)
|
|
||||||
[ "$#" -ge 2 ] || die "--wait requires a value"
|
|
||||||
WAIT_SECS="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "unknown option: $1"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
command -v docker >/dev/null 2>&1 || die "docker command not found"
|
|
||||||
if docker compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker compose)
|
|
||||||
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker-compose)
|
|
||||||
else
|
|
||||||
die "docker compose is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -f "${SERVER_ENV_FILE}" ] || die "server env file not found: ${SERVER_ENV_FILE}"
|
|
||||||
[ -f "${SERVER_COMPOSE_FILE}" ] || die "server compose file not found: ${SERVER_COMPOSE_FILE}"
|
|
||||||
[ -f "${CONSOLE_ENV_FILE}" ] || die "console env file not found: ${CONSOLE_ENV_FILE}"
|
|
||||||
[ -f "${CONSOLE_COMPOSE_FILE}" ] || die "console compose file not found: ${CONSOLE_COMPOSE_FILE}"
|
|
||||||
|
|
||||||
set -a
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
. "${SERVER_ENV_FILE}"
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
. "${CONSOLE_ENV_FILE}"
|
|
||||||
set +a
|
|
||||||
|
|
||||||
RTR_PORT="${RPKI_RTR_TCP_HOST_PORT:-323}"
|
|
||||||
ADMIN_PORT="${RPKI_RTR_ADMIN_ADDR##*:}"
|
|
||||||
ADMIN_PORT="${ADMIN_PORT:-8323}"
|
|
||||||
ADMIN_TOKEN="${RPKI_RTR_ADMIN_TOKEN:-}"
|
|
||||||
CONSOLE_PORT="${RPKI_CONSOLE_UI_PORT:-5173}"
|
|
||||||
|
|
||||||
server_compose() {
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${SERVER_ENV_FILE}" -p "${SERVER_PROJECT}" -f "${SERVER_COMPOSE_FILE}" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
console_compose() {
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${CONSOLE_ENV_FILE}" -p "${CONSOLE_PROJECT}" -f "${CONSOLE_COMPOSE_FILE}" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
container_running() {
|
|
||||||
local compose_kind="$1"
|
|
||||||
local service="$2"
|
|
||||||
local cid state
|
|
||||||
|
|
||||||
if [ "${compose_kind}" = "server" ]; then
|
|
||||||
cid="$(server_compose ps -q "${service}" 2>/dev/null || true)"
|
|
||||||
else
|
|
||||||
cid="$(console_compose ps -q "${service}" 2>/dev/null || true)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -n "${cid}" ] || return 1
|
|
||||||
state="$(docker inspect -f '{{.State.Running}}' "${cid}" 2>/dev/null || true)"
|
|
||||||
[ "${state}" = "true" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
tcp_open() {
|
|
||||||
local host="$1"
|
|
||||||
local port="$2"
|
|
||||||
|
|
||||||
if command -v nc >/dev/null 2>&1; then
|
|
||||||
nc -z "${host}" "${port}" >/dev/null 2>&1
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
(exec 3<>"/dev/tcp/${host}/${port}") >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
http_get() {
|
|
||||||
local url="$1"
|
|
||||||
local token="${2:-}"
|
|
||||||
|
|
||||||
if command -v curl >/dev/null 2>&1; then
|
|
||||||
if [ -n "${token}" ]; then
|
|
||||||
curl -fsS -H "Authorization: Bearer ${token}" "${url}"
|
|
||||||
else
|
|
||||||
curl -fsS "${url}"
|
|
||||||
fi
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
die "curl command is required for HTTP health checks"
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_until() {
|
|
||||||
local label="$1"
|
|
||||||
shift
|
|
||||||
local deadline=$((SECONDS + WAIT_SECS))
|
|
||||||
|
|
||||||
while [ "${SECONDS}" -lt "${deadline}" ]; do
|
|
||||||
if "$@"; then
|
|
||||||
log "${label}: ok"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
die "${label}: failed after ${WAIT_SECS}s"
|
|
||||||
}
|
|
||||||
|
|
||||||
show_command() {
|
|
||||||
printf '[02_self_check] check command: %s\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
check_admin_health() {
|
|
||||||
local body
|
|
||||||
body="$(http_get "http://127.0.0.1:${ADMIN_PORT}/admin/rtr/health" "${ADMIN_TOKEN}" 2>/dev/null || true)"
|
|
||||||
printf '%s' "${body}" | grep -q '"status"[[:space:]]*:[[:space:]]*"ok"'
|
|
||||||
}
|
|
||||||
|
|
||||||
check_console_http() {
|
|
||||||
http_get "http://127.0.0.1:${CONSOLE_PORT}/" >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
stage "Checking Containers"
|
|
||||||
show_command "${COMPOSE_BIN[*]} --env-file ${SERVER_ENV_FILE} -p ${SERVER_PROJECT} -f ${SERVER_COMPOSE_FILE} ps -q rpki-rtr && docker inspect"
|
|
||||||
wait_until "server container" container_running server rpki-rtr
|
|
||||||
show_command "${COMPOSE_BIN[*]} --env-file ${CONSOLE_ENV_FILE} -p ${CONSOLE_PROJECT} -f ${CONSOLE_COMPOSE_FILE} ps -q rpki-console-ui && docker inspect"
|
|
||||||
wait_until "console container" container_running console rpki-console-ui
|
|
||||||
|
|
||||||
stage "Checking Server"
|
|
||||||
show_command "tcp connect 127.0.0.1:${RTR_PORT}"
|
|
||||||
wait_until "RTR port ${RTR_PORT}" tcp_open 127.0.0.1 "${RTR_PORT}"
|
|
||||||
show_command "curl -fsS -H 'Authorization: Bearer <token>' http://127.0.0.1:${ADMIN_PORT}/admin/rtr/health"
|
|
||||||
wait_until "Admin health" check_admin_health
|
|
||||||
|
|
||||||
stage "Checking Console"
|
|
||||||
show_command "tcp connect 127.0.0.1:${CONSOLE_PORT}"
|
|
||||||
wait_until "Console port ${CONSOLE_PORT}" tcp_open 127.0.0.1 "${CONSOLE_PORT}"
|
|
||||||
show_command "curl -fsS http://127.0.0.1:${CONSOLE_PORT}/"
|
|
||||||
wait_until "Console HTTP" check_console_http
|
|
||||||
|
|
||||||
stage "Self-Check Passed"
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
|
|
||||||
SERVER_ENV_FILE="${ROOT_DIR}/deploy/server/.env"
|
|
||||||
CONSOLE_ENV_FILE="${ROOT_DIR}/deploy/console/.env"
|
|
||||||
MODE="${RPKI_RTR_SERVER_MODE:-tcp}"
|
|
||||||
SERVER_PROJECT="${RPKI_RTR_COMPOSE_PROJECT:-rpki-rtr}"
|
|
||||||
CONSOLE_PROJECT="${RPKI_CONSOLE_COMPOSE_PROJECT:-rpki-console}"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: scripts/03_shutdown.sh [options]
|
|
||||||
|
|
||||||
Stop services started by scripts/01_start.sh.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--mode MODE Server mode: tcp|tls|ssh|base. Default: tcp
|
|
||||||
-h, --help Show this help.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[03_shutdown] %s\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
stage() {
|
|
||||||
printf '\n====== %s ======\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf '[03_shutdown] ERROR: %s\n' "$*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
compose_file_for_mode() {
|
|
||||||
case "$1" in
|
|
||||||
tcp) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.tcp.yml" ;;
|
|
||||||
tls) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.tls.yml" ;;
|
|
||||||
ssh) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.ssh.yml" ;;
|
|
||||||
base) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.yml" ;;
|
|
||||||
*) die "unsupported mode '$1', expected tcp|tls|ssh|base" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
while [ "$#" -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--mode)
|
|
||||||
[ "$#" -ge 2 ] || die "--mode requires a value"
|
|
||||||
MODE="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "unknown option: $1"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
SERVER_COMPOSE_FILE="$(compose_file_for_mode "${MODE}")"
|
|
||||||
CONSOLE_COMPOSE_FILE="${ROOT_DIR}/deploy/console/docker-compose.yml"
|
|
||||||
|
|
||||||
stage "Checking Docker"
|
|
||||||
command -v docker >/dev/null 2>&1 || die "docker command not found"
|
|
||||||
if docker compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker compose)
|
|
||||||
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker-compose)
|
|
||||||
else
|
|
||||||
die "docker compose is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
stage "Stopping Console"
|
|
||||||
if [ -f "${CONSOLE_ENV_FILE}" ] && [ -f "${CONSOLE_COMPOSE_FILE}" ]; then
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${CONSOLE_ENV_FILE}" -p "${CONSOLE_PROJECT}" -f "${CONSOLE_COMPOSE_FILE}" down --remove-orphans
|
|
||||||
else
|
|
||||||
log "console compose/env not found, skip"
|
|
||||||
fi
|
|
||||||
|
|
||||||
stage "Stopping RTR Server"
|
|
||||||
if [ -f "${SERVER_ENV_FILE}" ] && [ -f "${SERVER_COMPOSE_FILE}" ]; then
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${SERVER_ENV_FILE}" -p "${SERVER_PROJECT}" -f "${SERVER_COMPOSE_FILE}" down --remove-orphans
|
|
||||||
else
|
|
||||||
log "server compose/env not found, skip"
|
|
||||||
fi
|
|
||||||
|
|
||||||
stage "Shutdown Finished"
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
SERVER_DIR="${ROOT_DIR}/deploy/server"
|
|
||||||
SERVER_ENV_FILE="${SERVER_DIR}/.env"
|
|
||||||
SERVER_COMPOSE_FILE="${SERVER_DIR}/docker-compose.tcp.yml"
|
|
||||||
COMPOSE_PROJECT="${RPKI_RTR_COMPOSE_PROJECT:-rpki-rtr}"
|
|
||||||
FORCE=0
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: scripts/04_cleanup.sh [options]
|
|
||||||
|
|
||||||
Remove server generated directories: logs, reports, and RTR database.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--force Clean even if the server container appears to be running.
|
|
||||||
-h, --help Show this help.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[04_cleanup] %s\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
stage() {
|
|
||||||
printf '\n====== %s ======\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf '[04_cleanup] ERROR: %s\n' "$*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
env_value() {
|
|
||||||
local key="$1"
|
|
||||||
local default_value="$2"
|
|
||||||
local line
|
|
||||||
|
|
||||||
line="$(grep -E "^[[:space:]]*${key}=" "${SERVER_ENV_FILE}" | tail -n 1 || true)"
|
|
||||||
if [ -n "${line}" ]; then
|
|
||||||
printf '%s\n' "${line#*=}"
|
|
||||||
else
|
|
||||||
printf '%s\n' "${default_value}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve_host_path() {
|
|
||||||
local raw_path="$1"
|
|
||||||
|
|
||||||
case "${raw_path}" in
|
|
||||||
/*|[A-Za-z]:/*|[A-Za-z]:\\*)
|
|
||||||
printf '%s\n' "${raw_path}"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
(cd "${SERVER_DIR}" && cd "${raw_path}" 2>/dev/null && pwd -P) || printf '%s/%s\n' "${SERVER_DIR}" "${raw_path}"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
compose_available() {
|
|
||||||
if docker compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker compose)
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker-compose)
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
server_running() {
|
|
||||||
local cid state
|
|
||||||
|
|
||||||
[ -f "${SERVER_COMPOSE_FILE}" ] || return 1
|
|
||||||
compose_available || return 1
|
|
||||||
|
|
||||||
cid="$("${COMPOSE_BIN[@]}" --env-file "${SERVER_ENV_FILE}" -p "${COMPOSE_PROJECT}" -f "${SERVER_COMPOSE_FILE}" ps -q rpki-rtr 2>/dev/null || true)"
|
|
||||||
[ -n "${cid}" ] || return 1
|
|
||||||
|
|
||||||
state="$(docker inspect -f '{{.State.Running}}' "${cid}" 2>/dev/null || true)"
|
|
||||||
[ "${state}" = "true" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
clean_dir() {
|
|
||||||
local label="$1"
|
|
||||||
local path="$2"
|
|
||||||
local root_with_slash
|
|
||||||
|
|
||||||
if [ ! -d "${path}" ]; then
|
|
||||||
log "${label}: ${path} not found, skip"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
root_with_slash="${ROOT_DIR}/"
|
|
||||||
case "${path}/" in
|
|
||||||
"${root_with_slash}"*) ;;
|
|
||||||
*)
|
|
||||||
log "ERROR: ${label} path is outside repo root, refuse to remove: ${path}"
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
log "${label}: removing ${path}"
|
|
||||||
if ! rm -rf "${path}"; then
|
|
||||||
log "ERROR: failed to remove ${label}: ${path}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
while [ "$#" -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--force)
|
|
||||||
FORCE=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "unknown option: $1"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
stage "Checking Server"
|
|
||||||
[ -f "${SERVER_ENV_FILE}" ] || die "server env file not found: ${SERVER_ENV_FILE}"
|
|
||||||
if [ "${FORCE}" -ne 1 ] && command -v docker >/dev/null 2>&1 && server_running; then
|
|
||||||
die "rpki-rtr is still running, run scripts/03_shutdown.sh first or use --force"
|
|
||||||
fi
|
|
||||||
|
|
||||||
LOG_DIR="${ROOT_DIR}/logs"
|
|
||||||
REPORT_DIR="$(resolve_host_path "$(env_value RPKI_RTR_REPORT_HOST_DIR '../../report')")"
|
|
||||||
DB_DIR="$(resolve_host_path "$(env_value RPKI_RTR_DB_HOST_DIR '../../rtr-db')")"
|
|
||||||
|
|
||||||
stage "Removing Server Files"
|
|
||||||
failed_count=0
|
|
||||||
clean_dir "logs" "${LOG_DIR}" || failed_count=$((failed_count + 1))
|
|
||||||
clean_dir "report" "${REPORT_DIR}" || failed_count=$((failed_count + 1))
|
|
||||||
clean_dir "rtr-db" "${DB_DIR}" || failed_count=$((failed_count + 1))
|
|
||||||
|
|
||||||
if [ "${failed_count}" -gt 0 ]; then
|
|
||||||
die "${failed_count} cleanup item(s) failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
stage "Cleanup Finished"
|
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
OUT_DIR="${ROOT_DIR}/certs"
|
|
||||||
TLS_DIR="${OUT_DIR}/tls"
|
|
||||||
SSH_DIR="${OUT_DIR}/ssh"
|
|
||||||
SERVER_DNS="localhost"
|
|
||||||
FORCE=0
|
|
||||||
DO_TLS=1
|
|
||||||
DO_SSH=1
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'USAGE'
|
|
||||||
Generate local development TLS certificates and SSH keys for RTR Docker deploys.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
scripts/10_generate-certs.sh [options]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--out-dir DIR Output directory, default: ./certs
|
|
||||||
--server-dns NAME TLS server dNSName SAN, default: localhost
|
|
||||||
--tls-only Generate only TLS files
|
|
||||||
--ssh-only Generate only SSH files
|
|
||||||
--force Overwrite existing files
|
|
||||||
-h, --help Show this help
|
|
||||||
|
|
||||||
Generated TLS files:
|
|
||||||
certs/tls/client-ca.crt
|
|
||||||
certs/tls/client-ca.key
|
|
||||||
certs/tls/server-dns.crt
|
|
||||||
certs/tls/server-dns.key
|
|
||||||
certs/tls/client-good.crt
|
|
||||||
certs/tls/client-good.key
|
|
||||||
|
|
||||||
Generated SSH files:
|
|
||||||
certs/ssh/ssh_host_rsa_key
|
|
||||||
certs/ssh/ssh_host_rsa_key.pub
|
|
||||||
certs/ssh/rtr-client.key
|
|
||||||
certs/ssh/rtr-client.key.pub
|
|
||||||
certs/ssh/rtr-authorized_keys
|
|
||||||
USAGE
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
echo "error: $*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
info() {
|
|
||||||
echo "[generate-certs] $*"
|
|
||||||
}
|
|
||||||
|
|
||||||
need_cmd() {
|
|
||||||
command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
run_quiet() {
|
|
||||||
local log_file
|
|
||||||
log_file="$(mktemp)"
|
|
||||||
if "$@" >"${log_file}" 2>&1; then
|
|
||||||
rm -f "${log_file}"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat "${log_file}" >&2
|
|
||||||
rm -f "${log_file}"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_abs_out_dir() {
|
|
||||||
case "${OUT_DIR}" in
|
|
||||||
/*) ;;
|
|
||||||
[A-Za-z]:*) ;;
|
|
||||||
*) OUT_DIR="${ROOT_DIR}/${OUT_DIR}" ;;
|
|
||||||
esac
|
|
||||||
TLS_DIR="${OUT_DIR}/tls"
|
|
||||||
SSH_DIR="${OUT_DIR}/ssh"
|
|
||||||
}
|
|
||||||
|
|
||||||
check_overwrite() {
|
|
||||||
local path="$1"
|
|
||||||
if [[ -e "${path}" && "${FORCE}" != "1" ]]; then
|
|
||||||
die "${path} already exists; rerun with --force to overwrite"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
write_tls_config_files() {
|
|
||||||
local tmp_dir="$1"
|
|
||||||
|
|
||||||
cat >"${tmp_dir}/ca.cnf" <<'EOF'
|
|
||||||
[ req ]
|
|
||||||
distinguished_name = req_distinguished_name
|
|
||||||
prompt = no
|
|
||||||
|
|
||||||
[ req_distinguished_name ]
|
|
||||||
CN = rpki-rtr-client-ca
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat >"${tmp_dir}/server.cnf" <<EOF
|
|
||||||
[ req ]
|
|
||||||
distinguished_name = req_distinguished_name
|
|
||||||
prompt = no
|
|
||||||
|
|
||||||
[ req_distinguished_name ]
|
|
||||||
CN = ${SERVER_DNS}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat >"${tmp_dir}/client.cnf" <<'EOF'
|
|
||||||
[ req ]
|
|
||||||
distinguished_name = req_distinguished_name
|
|
||||||
prompt = no
|
|
||||||
|
|
||||||
[ req_distinguished_name ]
|
|
||||||
CN = rpki-rtr-client
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat >"${tmp_dir}/server.ext" <<EOF
|
|
||||||
basicConstraints=CA:FALSE
|
|
||||||
keyUsage=digitalSignature,keyEncipherment
|
|
||||||
extendedKeyUsage=serverAuth
|
|
||||||
subjectAltName=DNS:${SERVER_DNS}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat >"${tmp_dir}/client.ext" <<'EOF'
|
|
||||||
basicConstraints=CA:FALSE
|
|
||||||
keyUsage=digitalSignature,keyEncipherment
|
|
||||||
extendedKeyUsage=clientAuth
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_tls() {
|
|
||||||
need_cmd openssl
|
|
||||||
mkdir -p "${TLS_DIR}"
|
|
||||||
|
|
||||||
info "generating TLS certificates in ${TLS_DIR}"
|
|
||||||
info "TLS server dNSName SAN: ${SERVER_DNS}"
|
|
||||||
|
|
||||||
local files=(
|
|
||||||
"${TLS_DIR}/client-ca.crt"
|
|
||||||
"${TLS_DIR}/client-ca.key"
|
|
||||||
"${TLS_DIR}/server-dns.crt"
|
|
||||||
"${TLS_DIR}/server-dns.key"
|
|
||||||
"${TLS_DIR}/client-good.crt"
|
|
||||||
"${TLS_DIR}/client-good.key"
|
|
||||||
)
|
|
||||||
for file in "${files[@]}"; do
|
|
||||||
check_overwrite "${file}"
|
|
||||||
done
|
|
||||||
|
|
||||||
local tmp_dir
|
|
||||||
tmp_dir="$(mktemp -d)"
|
|
||||||
write_tls_config_files "${tmp_dir}"
|
|
||||||
|
|
||||||
run_quiet openssl genrsa -out "${TLS_DIR}/client-ca.key" 2048
|
|
||||||
info "created TLS CA key: ${TLS_DIR}/client-ca.key"
|
|
||||||
run_quiet openssl req -x509 -new -nodes \
|
|
||||||
-config "${tmp_dir}/ca.cnf" \
|
|
||||||
-key "${TLS_DIR}/client-ca.key" \
|
|
||||||
-sha256 -days 3650 \
|
|
||||||
-out "${TLS_DIR}/client-ca.crt"
|
|
||||||
info "created TLS CA cert: ${TLS_DIR}/client-ca.crt"
|
|
||||||
|
|
||||||
run_quiet openssl genrsa -out "${TLS_DIR}/server-dns.key" 2048
|
|
||||||
info "created TLS server key: ${TLS_DIR}/server-dns.key"
|
|
||||||
run_quiet openssl req -new \
|
|
||||||
-config "${tmp_dir}/server.cnf" \
|
|
||||||
-key "${TLS_DIR}/server-dns.key" \
|
|
||||||
-out "${tmp_dir}/server-dns.csr"
|
|
||||||
run_quiet openssl x509 -req \
|
|
||||||
-in "${tmp_dir}/server-dns.csr" \
|
|
||||||
-CA "${TLS_DIR}/client-ca.crt" \
|
|
||||||
-CAkey "${TLS_DIR}/client-ca.key" \
|
|
||||||
-CAcreateserial \
|
|
||||||
-out "${TLS_DIR}/server-dns.crt" \
|
|
||||||
-days 3650 -sha256 \
|
|
||||||
-extfile "${tmp_dir}/server.ext"
|
|
||||||
info "created TLS server cert: ${TLS_DIR}/server-dns.crt"
|
|
||||||
|
|
||||||
run_quiet openssl genrsa -out "${TLS_DIR}/client-good.key" 2048
|
|
||||||
info "created TLS client key: ${TLS_DIR}/client-good.key"
|
|
||||||
run_quiet openssl req -new \
|
|
||||||
-config "${tmp_dir}/client.cnf" \
|
|
||||||
-key "${TLS_DIR}/client-good.key" \
|
|
||||||
-out "${tmp_dir}/client-good.csr"
|
|
||||||
run_quiet openssl x509 -req \
|
|
||||||
-in "${tmp_dir}/client-good.csr" \
|
|
||||||
-CA "${TLS_DIR}/client-ca.crt" \
|
|
||||||
-CAkey "${TLS_DIR}/client-ca.key" \
|
|
||||||
-CAcreateserial \
|
|
||||||
-out "${TLS_DIR}/client-good.crt" \
|
|
||||||
-days 3650 -sha256 \
|
|
||||||
-extfile "${tmp_dir}/client.ext"
|
|
||||||
info "created TLS client cert: ${TLS_DIR}/client-good.crt"
|
|
||||||
|
|
||||||
rm -f "${TLS_DIR}/client-ca.srl"
|
|
||||||
rm -rf "${tmp_dir}"
|
|
||||||
chmod 600 "${TLS_DIR}"/*.key
|
|
||||||
chmod 644 "${TLS_DIR}"/*.crt
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_ssh_key() {
|
|
||||||
local path="$1"
|
|
||||||
local comment="$2"
|
|
||||||
check_overwrite "${path}"
|
|
||||||
check_overwrite "${path}.pub"
|
|
||||||
rm -f "${path}" "${path}.pub"
|
|
||||||
run_quiet ssh-keygen -q -t rsa -b 3072 -m PEM -N "" -C "${comment}" -f "${path}"
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_ssh() {
|
|
||||||
need_cmd ssh-keygen
|
|
||||||
mkdir -p "${SSH_DIR}"
|
|
||||||
|
|
||||||
info "generating SSH keys in ${SSH_DIR}"
|
|
||||||
generate_ssh_key "${SSH_DIR}/ssh_host_rsa_key" "rpki-rtr-host"
|
|
||||||
info "created SSH host key: ${SSH_DIR}/ssh_host_rsa_key"
|
|
||||||
generate_ssh_key "${SSH_DIR}/rtr-client.key" "rpki-rtr-debug-client"
|
|
||||||
info "created SSH client key: ${SSH_DIR}/rtr-client.key"
|
|
||||||
|
|
||||||
check_overwrite "${SSH_DIR}/rtr-authorized_keys"
|
|
||||||
cp "${SSH_DIR}/rtr-client.key.pub" "${SSH_DIR}/rtr-authorized_keys"
|
|
||||||
info "created SSH authorized_keys: ${SSH_DIR}/rtr-authorized_keys"
|
|
||||||
|
|
||||||
chmod 600 \
|
|
||||||
"${SSH_DIR}/ssh_host_rsa_key" \
|
|
||||||
"${SSH_DIR}/rtr-client.key"
|
|
||||||
chmod 644 "${SSH_DIR}"/*.pub "${SSH_DIR}/rtr-authorized_keys"
|
|
||||||
}
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--out-dir)
|
|
||||||
[[ $# -ge 2 ]] || die "--out-dir requires a value"
|
|
||||||
OUT_DIR="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--server-dns)
|
|
||||||
[[ $# -ge 2 ]] || die "--server-dns requires a value"
|
|
||||||
SERVER_DNS="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--tls-only)
|
|
||||||
DO_TLS=1
|
|
||||||
DO_SSH=0
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--ssh-only)
|
|
||||||
DO_TLS=0
|
|
||||||
DO_SSH=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--force)
|
|
||||||
FORCE=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "unknown option: $1"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
ensure_abs_out_dir
|
|
||||||
|
|
||||||
if [[ "${DO_TLS}" == "1" ]]; then
|
|
||||||
generate_tls
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "${DO_SSH}" == "1" ]]; then
|
|
||||||
generate_ssh
|
|
||||||
fi
|
|
||||||
|
|
||||||
info "done"
|
|
||||||
info "generated files under ${OUT_DIR}"
|
|
||||||
|
|
||||||
if [[ "${DO_TLS}" == "1" ]]; then
|
|
||||||
cat <<EOF
|
|
||||||
|
|
||||||
TLS files:
|
|
||||||
${TLS_DIR}/client-ca.crt
|
|
||||||
${TLS_DIR}/server-dns.crt
|
|
||||||
${TLS_DIR}/server-dns.key
|
|
||||||
${TLS_DIR}/client-good.crt
|
|
||||||
${TLS_DIR}/client-good.key
|
|
||||||
${TLS_DIR}/client-ca.key (local signing key; not mounted by compose)
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "${DO_SSH}" == "1" ]]; then
|
|
||||||
cat <<EOF
|
|
||||||
|
|
||||||
SSH files:
|
|
||||||
${SSH_DIR}/ssh_host_rsa_key
|
|
||||||
${SSH_DIR}/ssh_host_rsa_key.pub
|
|
||||||
${SSH_DIR}/rtr-client.key
|
|
||||||
${SSH_DIR}/rtr-client.key.pub
|
|
||||||
${SSH_DIR}/rtr-authorized_keys
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
|
|
||||||
COMMON_ENV_FILE="${ROOT_DIR}/environment.client"
|
|
||||||
CLIENT_ENV_FILE="${ROOT_DIR}/deploy/client/.env"
|
|
||||||
MODE="${RPKI_RTR_CLIENT_MODE:-tcp}"
|
|
||||||
COMPOSE_PROJECT="${RPKI_RTR_CLIENT_COMPOSE_PROJECT:-rpki-rtr-client}"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: scripts/11_start_client.sh [options]
|
|
||||||
|
|
||||||
Read root ./environment.client, update deploy/client/.env, then start the RTR debug client.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--mode MODE Client mode: tcp|tls|ssh. Default: tcp
|
|
||||||
--env FILE Client common env file. Default: ./environment.client
|
|
||||||
-h, --help Show this help.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[11_start_client] %s\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
stage() {
|
|
||||||
printf '\n====== %s ======\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf '[11_start_client] ERROR: %s\n' "$*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
set_env_var() {
|
|
||||||
local env_file="$1"
|
|
||||||
local key="$2"
|
|
||||||
local value="$3"
|
|
||||||
local tmp_file
|
|
||||||
|
|
||||||
tmp_file="${env_file}.tmp.$$"
|
|
||||||
awk -v key="${key}" -v value="${value}" '
|
|
||||||
BEGIN { done = 0 }
|
|
||||||
$0 ~ "^[[:space:]]*#?[[:space:]]*" key "=" {
|
|
||||||
if (!done) {
|
|
||||||
print key "=" value
|
|
||||||
done = 1
|
|
||||||
}
|
|
||||||
next
|
|
||||||
}
|
|
||||||
{ print }
|
|
||||||
END {
|
|
||||||
if (!done) {
|
|
||||||
print key "=" value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
' "${env_file}" >"${tmp_file}"
|
|
||||||
mv "${tmp_file}" "${env_file}"
|
|
||||||
}
|
|
||||||
|
|
||||||
compose_file_for_mode() {
|
|
||||||
case "$1" in
|
|
||||||
tcp) printf '%s\n' "${ROOT_DIR}/deploy/client/docker-compose.tcp.yml" ;;
|
|
||||||
tls) printf '%s\n' "${ROOT_DIR}/deploy/client/docker-compose.tls.yml" ;;
|
|
||||||
ssh) printf '%s\n' "${ROOT_DIR}/deploy/client/docker-compose.ssh.yml" ;;
|
|
||||||
*) die "unsupported mode '$1', expected tcp|tls|ssh" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
while [ "$#" -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--mode)
|
|
||||||
[ "$#" -ge 2 ] || die "--mode requires a value"
|
|
||||||
MODE="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--env)
|
|
||||||
[ "$#" -ge 2 ] || die "--env requires a value"
|
|
||||||
COMMON_ENV_FILE="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "unknown option: $1"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
CLIENT_COMPOSE_FILE="$(compose_file_for_mode "${MODE}")"
|
|
||||||
|
|
||||||
stage "Checking Docker"
|
|
||||||
command -v docker >/dev/null 2>&1 || die "docker command not found"
|
|
||||||
if docker compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker compose)
|
|
||||||
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker-compose)
|
|
||||||
else
|
|
||||||
die "docker compose is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -f "${CLIENT_ENV_FILE}" ] || die "deploy client env file not found: ${CLIENT_ENV_FILE}"
|
|
||||||
[ -f "${CLIENT_COMPOSE_FILE}" ] || die "client compose file not found: ${CLIENT_COMPOSE_FILE}"
|
|
||||||
|
|
||||||
stage "Reading Client Environment"
|
|
||||||
if [ -f "${COMMON_ENV_FILE}" ]; then
|
|
||||||
updated_count=0
|
|
||||||
while IFS= read -r line || [ -n "${line}" ]; do
|
|
||||||
case "${line}" in
|
|
||||||
''|\#*) continue ;;
|
|
||||||
*=*)
|
|
||||||
key="${line%%=*}"
|
|
||||||
value="${line#*=}"
|
|
||||||
set_env_var "${CLIENT_ENV_FILE}" "${key}" "${value}"
|
|
||||||
updated_count=$((updated_count + 1))
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done <"${COMMON_ENV_FILE}"
|
|
||||||
log "updated ${updated_count} client env variable(s)"
|
|
||||||
else
|
|
||||||
log "environment.client file not found, skip env update"
|
|
||||||
fi
|
|
||||||
|
|
||||||
stage "Starting RTR Debug Client"
|
|
||||||
log "mode: ${MODE}"
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${CLIENT_ENV_FILE}" -p "${COMPOSE_PROJECT}" -f "${CLIENT_COMPOSE_FILE}" up -d --no-build rtr-debug-client
|
|
||||||
|
|
||||||
log "checking client compose status"
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${CLIENT_ENV_FILE}" -p "${COMPOSE_PROJECT}" -f "${CLIENT_COMPOSE_FILE}" ps rtr-debug-client
|
|
||||||
|
|
||||||
stage "Client Started"
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
|
|
||||||
CLIENT_ENV_FILE="${ROOT_DIR}/deploy/client/.env"
|
|
||||||
MODE="${RPKI_RTR_CLIENT_MODE:-tcp}"
|
|
||||||
COMPOSE_PROJECT="${RPKI_RTR_CLIENT_COMPOSE_PROJECT:-rpki-rtr-client}"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: scripts/12_shutdown_client.sh [options]
|
|
||||||
|
|
||||||
Stop the RTR debug client started by scripts/11_start_client.sh.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--mode MODE Client mode: tcp|tls|ssh. Default: tcp
|
|
||||||
-h, --help Show this help.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[12_shutdown_client] %s\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
stage() {
|
|
||||||
printf '\n====== %s ======\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf '[12_shutdown_client] ERROR: %s\n' "$*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
compose_file_for_mode() {
|
|
||||||
case "$1" in
|
|
||||||
tcp) printf '%s\n' "${ROOT_DIR}/deploy/client/docker-compose.tcp.yml" ;;
|
|
||||||
tls) printf '%s\n' "${ROOT_DIR}/deploy/client/docker-compose.tls.yml" ;;
|
|
||||||
ssh) printf '%s\n' "${ROOT_DIR}/deploy/client/docker-compose.ssh.yml" ;;
|
|
||||||
*) die "unsupported mode '$1', expected tcp|tls|ssh" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
while [ "$#" -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--mode)
|
|
||||||
[ "$#" -ge 2 ] || die "--mode requires a value"
|
|
||||||
MODE="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "unknown option: $1"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
CLIENT_COMPOSE_FILE="$(compose_file_for_mode "${MODE}")"
|
|
||||||
|
|
||||||
stage "Checking Docker"
|
|
||||||
command -v docker >/dev/null 2>&1 || die "docker command not found"
|
|
||||||
if docker compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker compose)
|
|
||||||
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker-compose)
|
|
||||||
else
|
|
||||||
die "docker compose is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
stage "Stopping RTR Debug Client"
|
|
||||||
log "mode: ${MODE}"
|
|
||||||
if [ -f "${CLIENT_ENV_FILE}" ] && [ -f "${CLIENT_COMPOSE_FILE}" ]; then
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${CLIENT_ENV_FILE}" -p "${COMPOSE_PROJECT}" -f "${CLIENT_COMPOSE_FILE}" down --remove-orphans
|
|
||||||
else
|
|
||||||
log "client compose/env not found, skip"
|
|
||||||
fi
|
|
||||||
|
|
||||||
stage "Client Shutdown Finished"
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
# 脚本说明
|
|
||||||
|
|
||||||
`scripts/` 目录用于离线安装、启动、健康检查、关闭和清理。
|
|
||||||
|
|
||||||
所有脚本建议在仓库根目录执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/00_0_install.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## 目录
|
|
||||||
|
|
||||||
- [推荐流程](#推荐流程)
|
|
||||||
- [环境变量文件](#环境变量文件)
|
|
||||||
- [00 安装](#00-安装)
|
|
||||||
- [00 Pre-Check](#00-pre-check)
|
|
||||||
- [01 启动 Server 和 Console](#01-启动-server-和-console)
|
|
||||||
- [02 自检](#02-自检)
|
|
||||||
- [03 关闭 Server 和 Console](#03-关闭-server-和-console)
|
|
||||||
- [04 清理](#04-清理)
|
|
||||||
- [10 生成证书](#10-生成证书)
|
|
||||||
- [11 启动 Client](#11-启动-client)
|
|
||||||
- [12 关闭 Client](#12-关闭-client)
|
|
||||||
- [Windows 路径注意事项](#windows-路径注意事项)
|
|
||||||
|
|
||||||
## 推荐流程
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. 加载镜像并执行 pre-check。
|
|
||||||
bash scripts/00_0_install.sh
|
|
||||||
|
|
||||||
# 2. 启动真实 server 和 console。
|
|
||||||
bash scripts/01_start.sh --mode tcp
|
|
||||||
|
|
||||||
# 3. 检查 server 和 console 是否正常。
|
|
||||||
bash scripts/02_self_check.sh
|
|
||||||
|
|
||||||
# 4. 关闭 server 和 console。
|
|
||||||
bash scripts/03_shutdown.sh --mode tcp
|
|
||||||
|
|
||||||
# 5. 清理 server 运行产生的文件。
|
|
||||||
bash scripts/04_cleanup.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
client 单独启动和关闭:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/11_start_client.sh --mode tcp
|
|
||||||
bash scripts/12_shutdown_client.sh --mode tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
支持的模式:
|
|
||||||
|
|
||||||
- server:`tcp`、`tls`、`ssh`、`base`
|
|
||||||
- client:`tcp`、`tls`、`ssh`
|
|
||||||
|
|
||||||
## 环境变量文件
|
|
||||||
|
|
||||||
`01_start.sh` 会读取根目录下的 `environment.server`。如果文件存在,会将其中所有 `KEY=VALUE` 同步到 `deploy/server/.env`。
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```env
|
|
||||||
RPKI_RTR_CCR_HOST_DIR=/opt/rpki-rtr/data
|
|
||||||
RPKI_RTR_SLURM_HOST_DIR=/opt/rpki-rtr/data
|
|
||||||
RPKI_RTR_DB_HOST_DIR=/opt/rpki-rtr/rtr-db
|
|
||||||
RPKI_RTR_LOG_HOST_DIR=/opt/rpki-rtr/logs/server
|
|
||||||
RPKI_RTR_REPORT_HOST_DIR=/opt/rpki-rtr/report
|
|
||||||
```
|
|
||||||
|
|
||||||
`11_start_client.sh` 会读取根目录下的 `environment.client`。如果文件存在,会将其中所有 `KEY=VALUE` 同步到 `deploy/client/.env`。
|
|
||||||
|
|
||||||
如果 `environment.server` 或 `environment.client` 不存在,对应脚本会跳过更新,直接使用已有的 deploy `.env` 文件。
|
|
||||||
|
|
||||||
## 00 安装
|
|
||||||
|
|
||||||
`00_0_install.sh` 用于从 `images/` 目录加载离线 Docker 镜像,更新 deploy `.env` 中的镜像名,然后执行 pre-check。
|
|
||||||
|
|
||||||
默认镜像包查找规则:
|
|
||||||
|
|
||||||
- server:`images/rpki-rtr-*.tar`,排除 `rpki-rtr-debug-client-*.tar`
|
|
||||||
- client:`images/rpki-rtr-debug-client-*.tar`
|
|
||||||
- console:`images/rpki-console-ui-*.tar`
|
|
||||||
|
|
||||||
也可以显式指定镜像包:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/00_0_install.sh \
|
|
||||||
--server-tar images/rpki-rtr-amd64-v1.2.tar \
|
|
||||||
--client-tar images/rpki-rtr-debug-client-amd64-v1.2.tar \
|
|
||||||
--console-tar images/rpki-console-ui-amd64-v1.0.tar
|
|
||||||
```
|
|
||||||
|
|
||||||
脚本会从 `docker load` 输出中解析真实镜像名,并写入:
|
|
||||||
|
|
||||||
- `deploy/server/.env` 的 `RPKI_RTR_SERVER_IMAGE`
|
|
||||||
- `deploy/console/.env` 的 `RPKI_CONSOLE_UI_IMAGE`
|
|
||||||
|
|
||||||
## 00 Pre-Check
|
|
||||||
|
|
||||||
`00_1_pre_check.sh` 会在 `.precheck/` 下准备隔离测试数据,使用仓库里的 Docker Compose 文件启动 server 和 debug client,然后检查:
|
|
||||||
|
|
||||||
- admin health 接口返回正常。
|
|
||||||
- server 写出了 source report。
|
|
||||||
- debug client 收到了 EndOfData 和 payload summary。
|
|
||||||
- pre-check 日志中没有 fatal 错误模式。
|
|
||||||
|
|
||||||
相关辅助脚本:
|
|
||||||
|
|
||||||
- `00_2_generate_ccr.sh`:生成固定的 CCR 测试文件。
|
|
||||||
- `00_2_pre_check_prepare.sh`:创建 `.precheck/` 数据、SLURM 文件和 env 文件。
|
|
||||||
- `00_3_pre_check_cleanup.sh`:停止 pre-check 容器并清理 `.precheck/`。
|
|
||||||
|
|
||||||
## 01 启动 Server 和 Console
|
|
||||||
|
|
||||||
`01_start.sh` 用于启动真实的 RTR server 和 console。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/01_start.sh --mode tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
启动后脚本会检查容器是否进入 `running` 状态。如果容器启动后退出,脚本会打印 compose 状态和最近日志,然后报错退出,不会继续打印 `Start Finished`。
|
|
||||||
|
|
||||||
常用参数:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/01_start.sh --mode tls
|
|
||||||
bash scripts/01_start.sh --mode ssh
|
|
||||||
bash scripts/01_start.sh --wait 60
|
|
||||||
bash scripts/01_start.sh --env ./environment.server
|
|
||||||
```
|
|
||||||
|
|
||||||
注意:console 不读取 `environment.server`,只使用 `deploy/console/.env`。
|
|
||||||
|
|
||||||
## 02 自检
|
|
||||||
|
|
||||||
`02_self_check.sh` 用于检查 `01_start.sh` 启动的 server 和 console 是否正常。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/02_self_check.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
检查内容包括:
|
|
||||||
|
|
||||||
- server 容器是否 running。
|
|
||||||
- console 容器是否 running。
|
|
||||||
- RTR TCP 端口是否可连接。
|
|
||||||
- admin health 接口是否健康。
|
|
||||||
- console 端口和 HTTP 根路径是否可访问。
|
|
||||||
|
|
||||||
如果启动较慢,可以增加等待时间:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/02_self_check.sh --wait 120
|
|
||||||
```
|
|
||||||
|
|
||||||
## 03 关闭 Server 和 Console
|
|
||||||
|
|
||||||
`03_shutdown.sh` 用于关闭 `01_start.sh` 启动的服务。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/03_shutdown.sh --mode tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
脚本会先关闭 console,再关闭 RTR server。
|
|
||||||
|
|
||||||
## 04 清理
|
|
||||||
|
|
||||||
`04_cleanup.sh` 用于删除 server 运行产生的目录:
|
|
||||||
|
|
||||||
- `logs/`
|
|
||||||
- `report/`
|
|
||||||
- `rtr-db/`
|
|
||||||
|
|
||||||
推荐先关闭服务,再清理:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/03_shutdown.sh
|
|
||||||
bash scripts/04_cleanup.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
清理前脚本会检查 `rpki-rtr` 是否仍在运行。如果仍在运行,会拒绝清理。只有明确需要强制清理时才使用:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/04_cleanup.sh --force
|
|
||||||
```
|
|
||||||
|
|
||||||
清理脚本会拒绝删除仓库根目录之外的路径。如果某一项清理失败,会继续清理后续项,最后返回失败。
|
|
||||||
|
|
||||||
## 10 生成证书
|
|
||||||
|
|
||||||
`10_generate_certs.sh` 用于生成 TLS/SSH 模式需要的本地证书材料(测试用)。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/10_generate_certs.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## 11 启动 Client
|
|
||||||
|
|
||||||
`11_start_client.sh` 用于启动 RTR debug client。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/11_start_client.sh --mode tcp
|
|
||||||
bash scripts/11_start_client.sh --mode tls
|
|
||||||
bash scripts/11_start_client.sh --mode ssh
|
|
||||||
```
|
|
||||||
|
|
||||||
如果 `environment.client` 存在,脚本会将其中所有 `KEY=VALUE` 同步到 `deploy/client/.env`;如果不存在,则直接使用已有的 `deploy/client/.env`。
|
|
||||||
|
|
||||||
## 12 关闭 Client
|
|
||||||
|
|
||||||
`12_shutdown_client.sh` 用于关闭 `11_start_client.sh` 启动的 client。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/12_shutdown_client.sh --mode tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
关闭时建议使用和启动时相同的 mode。
|
|
||||||
80
scripts/coverage.sh
Executable file
80
scripts/coverage.sh
Executable file
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Requires:
|
||||||
|
# rustup component add llvm-tools-preview
|
||||||
|
# cargo install cargo-llvm-cov --locked
|
||||||
|
|
||||||
|
run_out="$(mktemp)"
|
||||||
|
text_out="$(mktemp)"
|
||||||
|
html_out="$(mktemp)"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -f "$run_out" "$text_out" "$html_out"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Preserve colored output even though we post-process output by running under a pseudo-TTY.
|
||||||
|
# We run tests only once, then generate both CLI text + HTML reports without rerunning tests.
|
||||||
|
set +e
|
||||||
|
|
||||||
|
cargo llvm-cov clean --workspace >/dev/null 2>&1
|
||||||
|
|
||||||
|
# 1) Run tests once to collect coverage data (no report).
|
||||||
|
script -q -e -c "CARGO_TERM_COLOR=always cargo llvm-cov --no-report" "$run_out" >/dev/null 2>&1
|
||||||
|
run_status="$?"
|
||||||
|
|
||||||
|
# 2) CLI summary report + fail-under gate (no test rerun).
|
||||||
|
script -q -e -c "CARGO_TERM_COLOR=always cargo llvm-cov report --fail-under-lines 90" "$text_out" >/dev/null 2>&1
|
||||||
|
text_status="$?"
|
||||||
|
|
||||||
|
# 3) HTML report (no test rerun).
|
||||||
|
script -q -e -c "CARGO_TERM_COLOR=always cargo llvm-cov report --html" "$html_out" >/dev/null 2>&1
|
||||||
|
html_status="$?"
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
strip_script_noise() {
|
||||||
|
tr -d '\r' | sed '/^Script \(started\|done\) on /d'
|
||||||
|
}
|
||||||
|
|
||||||
|
strip_ansi_for_parse() {
|
||||||
|
awk '
|
||||||
|
{
|
||||||
|
line = $0
|
||||||
|
gsub(/\033\[[0-9;]*[A-Za-z]/, "", line) # CSI escapes
|
||||||
|
gsub(/\033\([A-Za-z]/, "", line) # charset escapes (e.g., ESC(B)
|
||||||
|
gsub(/\r/, "", line)
|
||||||
|
print line
|
||||||
|
}
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
cat "$run_out" | strip_script_noise
|
||||||
|
cat "$text_out" | strip_script_noise
|
||||||
|
cat "$html_out" | strip_script_noise
|
||||||
|
|
||||||
|
cat "$run_out" | strip_ansi_for_parse | awk '
|
||||||
|
BEGIN {
|
||||||
|
passed=0; failed=0; ignored=0; measured=0; filtered=0;
|
||||||
|
}
|
||||||
|
/^test result: / {
|
||||||
|
if (match($0, /([0-9]+) passed; ([0-9]+) failed; ([0-9]+) ignored; ([0-9]+) measured; ([0-9]+) filtered out;/, m)) {
|
||||||
|
passed += m[1]; failed += m[2]; ignored += m[3]; measured += m[4]; filtered += m[5];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
END {
|
||||||
|
executed = passed + failed;
|
||||||
|
total = passed + failed + ignored + measured;
|
||||||
|
printf("\nTEST SUMMARY (all suites): passed=%d failed=%d ignored=%d measured=%d filtered_out=%d executed=%d total=%d\n",
|
||||||
|
passed, failed, ignored, measured, filtered, executed, total);
|
||||||
|
}
|
||||||
|
'
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "HTML report: target/llvm-cov/html/index.html"
|
||||||
|
|
||||||
|
status="$text_status"
|
||||||
|
if [ "$run_status" -ne 0 ]; then status="$run_status"; fi
|
||||||
|
if [ "$html_status" -ne 0 ]; then status="$html_status"; fi
|
||||||
|
exit "$status"
|
||||||
@@ -182,6 +182,8 @@ RFC 引用:RFC 5280 §4.2.2.1;RFC 5280 §4.2.2.2;RFC 5280 §4.2.1.6。
|
|||||||
| `1.2.840.113549.1.9.5` | CMS signedAttrs: signing-time | RFC 9589 §4(更新 RFC 6488 §3(1f)/(1g)) |
|
| `1.2.840.113549.1.9.5` | CMS signedAttrs: signing-time | RFC 9589 §4(更新 RFC 6488 §3(1f)/(1g)) |
|
||||||
| `1.2.840.113549.1.9.16.1.24` | ROA eContentType: id-ct-routeOriginAuthz | RFC 9582 §3 |
|
| `1.2.840.113549.1.9.16.1.24` | ROA eContentType: id-ct-routeOriginAuthz | RFC 9582 §3 |
|
||||||
| `1.2.840.113549.1.9.16.1.26` | Manifest eContentType: id-ct-rpkiManifest | RFC 9286 §4.1 |
|
| `1.2.840.113549.1.9.16.1.26` | Manifest eContentType: id-ct-rpkiManifest | RFC 9286 §4.1 |
|
||||||
|
| `1.2.840.113549.1.9.16.1.35` | Ghostbusters eContentType: id-ct-rpkiGhostbusters | RFC 6493 §6;RFC 6493 §9.1 |
|
||||||
|
| `1.2.840.113549.1.9.16.1.49` | ASPA eContentType: id-ct-ASPA | `draft-ietf-sidrops-aspa-profile-21` §2 |
|
||||||
| `1.3.6.1.5.5.7.1.1` | X.509 v3 扩展:authorityInfoAccess | RFC 5280 §4.2.2.1 |
|
| `1.3.6.1.5.5.7.1.1` | X.509 v3 扩展:authorityInfoAccess | RFC 5280 §4.2.2.1 |
|
||||||
| `1.3.6.1.5.5.7.1.11` | X.509 v3 扩展:subjectInfoAccess | RFC 5280 §4.2.2.2;RPKI 约束见 RFC 6487 §4.8.8 |
|
| `1.3.6.1.5.5.7.1.11` | X.509 v3 扩展:subjectInfoAccess | RFC 5280 §4.2.2.2;RPKI 约束见 RFC 6487 §4.8.8 |
|
||||||
| `1.3.6.1.5.5.7.48.2` | AIA accessMethod: id-ad-caIssuers | RFC 5280 §4.2.2.1 |
|
| `1.3.6.1.5.5.7.48.2` | AIA accessMethod: id-ad-caIssuers | RFC 5280 §4.2.2.1 |
|
||||||
|
|||||||
104
specs/01_tal.md
104
specs/01_tal.md
@@ -1,36 +1,94 @@
|
|||||||
# 01. Trust Anchor Locator (TAL)
|
# 01. TAL(Trust Anchor Locator)
|
||||||
|
|
||||||
## 1.1 对象定位
|
## 1.1 对象定位
|
||||||
TAL是一个数据格式/配置文件,目的是告诉RP信任锚的公钥是什么,以及相关对象可以从哪里获取。
|
|
||||||
|
|
||||||
## 1.2 数据格式 (RFC 8630 §2.2)
|
TAL(Trust Anchor Locator)用于向 RP 提供:
|
||||||
TAL是一个配置文件,格式定义如下:
|
|
||||||
```
|
1) 可检索“当前 TA 证书”的一个或多个 URI;以及
|
||||||
The TAL is an ordered sequence of:
|
2) 该 TA 证书的 `subjectPublicKeyInfo`(SPKI)期望值(用于绑定/防替换)。
|
||||||
1. an optional comment section consisting of one or more lines each starting with the "#" character, followed by human-readable informational UTF-8 text, conforming to the restrictions defined
|
|
||||||
in Section 2 of [RFC5198], and ending with a line break,
|
RFC 8630 §2;RFC 8630 §2.3。
|
||||||
2. a URI section that is comprised of one or more ordered lines, each containing a TA URI, and ending with a line break,
|
|
||||||
3. a line break, and
|
## 1.2 原始载体与编码
|
||||||
4. a subjectPublicKeyInfo [RFC5280] in DER format [X.509], encoded in base64 (see Section 4 of [RFC4648]). To avoid long lines,
|
|
||||||
line breaks MAY be inserted into the base64-encoded string.
|
- 载体:文本文件(ASCII/UTF-8 兼容的行文本)。
|
||||||
Note that line breaks in this file can use either "<CRLF>" or "<LF>".
|
- 行结束:允许 `CRLF` 或 `LF`。RFC 8630 §2.2。
|
||||||
|
- 结构:`[可选注释区] + URI 区 + 空行 + Base64(SPKI DER)`。RFC 8630 §2.2。
|
||||||
|
|
||||||
|
### 1.2.1 注释区
|
||||||
|
|
||||||
|
- 一行或多行,以 `#` 开头,后随人类可读 UTF-8 文本。RFC 8630 §2.2。
|
||||||
|
- 注释行文本需符合 RFC 5198 §2 的限制(RFC 8630 §2.2 引用)。
|
||||||
|
|
||||||
|
### 1.2.2 URI 区
|
||||||
|
|
||||||
|
- 一行或多行,每行一个 TA URI,按序排列。RFC 8630 §2.2。
|
||||||
|
- TA URI **MUST** 是 `rsync` 或 `https`。RFC 8630 §2.2。
|
||||||
|
|
||||||
|
### 1.2.3 空行分隔
|
||||||
|
|
||||||
|
- URI 区后必须有一个额外的换行(即空行),用于与 Base64 区分隔。RFC 8630 §2.2(第 3 点)。
|
||||||
|
|
||||||
|
### 1.2.4 SPKI(Base64)
|
||||||
|
|
||||||
|
- `subjectPublicKeyInfo` 以 DER 编码(ASN.1)后,再 Base64 编码表示。RFC 8630 §2.2(第 4 点)。
|
||||||
|
- 为避免长行,Base64 字符串中 **MAY** 插入换行。RFC 8630 §2.2。
|
||||||
|
- SPKI ASN.1 类型来自 X.509 / RFC 5280。RFC 8630 §2.2(第 4 点);RFC 5280 §4.1.2.7。
|
||||||
|
|
||||||
|
#### 1.2.4.1 `SubjectPublicKeyInfo` 的 ASN.1 定义(RFC 5280 §4.1)
|
||||||
|
|
||||||
|
TAL 中携带的是一个 X.509 `SubjectPublicKeyInfo` 的 DER 字节串(再 Base64)。其 ASN.1 定义如下:RFC 5280 §4.1。
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
SubjectPublicKeyInfo ::= SEQUENCE {
|
||||||
|
algorithm AlgorithmIdentifier,
|
||||||
|
subjectPublicKey BIT STRING }
|
||||||
```
|
```
|
||||||
|
|
||||||
## 1.3 抽象数据模型
|
其中 `algorithm`/`subjectPublicKey` 的取值受 RPKI 算法 profile 约束(例如 RSA 2048 + SHA-256 等;SKI/AKI 计算仍用 SHA-1)。RFC 5280 §4.1.2.7;RFC 7935 §2-§3.1;RFC 6487 §4.8.2-§4.8.3。
|
||||||
|
|
||||||
### 1.3.1 TAL
|
## 1.3 解析规则(语义层)
|
||||||
|
|
||||||
|
输入:`TalFileBytes: bytes`。
|
||||||
|
|
||||||
|
解析步骤:
|
||||||
|
|
||||||
|
1) 按 `LF` / `CRLF` 识别行。RFC 8630 §2.2。
|
||||||
|
2) 从文件开头读取所有以 `#` 开头的行,作为 `comments`(保留去掉 `#` 后的 UTF-8 文本或保留原始行均可,但需保持 UTF-8)。RFC 8630 §2.2。
|
||||||
|
3) 继续读取一行或多行非空行,作为 `ta_uris`(保持顺序)。RFC 8630 §2.2(第 2 点)。
|
||||||
|
4) 读取一个空行(必须存在)。RFC 8630 §2.2(第 3 点)。
|
||||||
|
5) 将剩余行拼接为 Base64 文本(移除行分隔),Base64 解码得到 `subject_public_key_info_der`。RFC 8630 §2.2(第 4 点)。
|
||||||
|
6) 可选:将 `subject_public_key_info_der` 解析为 X.509 `SubjectPublicKeyInfo` 结构(用于与 TA 证书比对)。RFC 8630 §2.3;RFC 5280 §4.1.2.7。
|
||||||
|
|
||||||
|
URI 解析与约束:
|
||||||
|
|
||||||
|
- `ta_uris[*]` 的 scheme **MUST** 为 `rsync` 或 `https`。RFC 8630 §2.2。
|
||||||
|
- 每个 `ta_uri` **MUST** 指向“单个对象”,且 **MUST NOT** 指向目录或集合。RFC 8630 §2.3。
|
||||||
|
|
||||||
|
## 1.4 抽象数据模型(接口)
|
||||||
|
|
||||||
|
### 1.4.1 `Tal`
|
||||||
|
|
||||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|----------|-------------|-------------------------|--------------------------------------------|---------------|
|
|---|---|---|---|---|
|
||||||
| uris | Vec<TalUri> | 指向TA的URI列表 | 允许rsync和https协议。 | RFC 8630 §2.1 |
|
| `raw` | `bytes` | TAL 原始文件字节 | 原样保留(可选但建议) | RFC 8630 §2.2 |
|
||||||
| comment | Vec<String> | 注释(可选) | | RFC 8630 §2.2 |
|
| `comments` | `list[Utf8Text]` | 注释行(按出现顺序) | 每行以 `#` 开头;文本为 UTF-8;内容限制见 RFC 5198 §2 | RFC 8630 §2.2 |
|
||||||
| spki_der | Vec<u8> | 原始的subjectPublicKeyInfo | x.509 SubjectPublicKeyInfo DER编码,再base64编码 | RFC 8630 §2.2 |
|
| `ta_uris` | `list[Uri]` | TA 证书位置列表 | 至少 1 个;按序;每个 scheme 必须是 `rsync` 或 `https` | RFC 8630 §2.2 |
|
||||||
|
| `subject_public_key_info_der` | `DerBytes` | TA 证书 SPKI 的期望 DER | Base64 解码所得 DER;Base64 中可有换行 | RFC 8630 §2.2 |
|
||||||
|
|
||||||
|
### 1.4.2 `TaUri`(可选细化)
|
||||||
|
|
||||||
### 1.3.2 TalUri
|
> 若你的实现希望对 URI 做更强类型化,可在 `Tal.ta_uris` 上进一步拆分为 `TaUri` 结构。
|
||||||
|
|
||||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|-------|--------|---------|---------|---------------|
|
|---|---|---|---|---|
|
||||||
| Rsync | String | rsync地址 | | RFC 8630 §2.1 |
|
| `uri` | `Uri` | 完整 URI 文本 | scheme 为 `rsync` 或 `https` | RFC 8630 §2.2 |
|
||||||
| Https | String | https地址 | | RFC 8630 §2.1 |
|
| `scheme` | `enum` | `rsync` / `https` | 从 `uri` 解析 | RFC 8630 §2.2 |
|
||||||
|
|
||||||
|
## 1.5 字段级约束清单(实现对照)
|
||||||
|
|
||||||
|
- TAL 由(可选)注释区 + URI 区 + 空行 + Base64(SPKI DER) 组成。RFC 8630 §2.2。
|
||||||
|
- URI 区至少 1 行,每行一个 TA URI,顺序有意义。RFC 8630 §2.2。
|
||||||
|
- TA URI 仅允许 `rsync` 或 `https`。RFC 8630 §2.2。
|
||||||
|
- Base64 区允许插入换行。RFC 8630 §2.2。
|
||||||
|
- 每个 TA URI 必须引用单个对象,不能指向目录/集合。RFC 8630 §2.3。
|
||||||
|
|||||||
121
specs/02_ta.md
121
specs/02_ta.md
@@ -1,121 +0,0 @@
|
|||||||
# 02. Trust Anchor (TA)
|
|
||||||
|
|
||||||
## 2.1 对象定位
|
|
||||||
TA是一个自签名的CA证书。
|
|
||||||
|
|
||||||
## 2.2 原始载体与编码
|
|
||||||
|
|
||||||
- 载体:X.509 certificates.
|
|
||||||
- 编码:DER(遵循 RFC 5280 的 certificate 结构与字段语义,但受限于RFC 8630 §2.3)
|
|
||||||
|
|
||||||
|
|
||||||
## 2.3 抽象数据类型
|
|
||||||
|
|
||||||
### 2.3.1 TA
|
|
||||||
|
|
||||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
|
||||||
|-------------------|------------------|---------------|---------|---------------|
|
|
||||||
| name | String | 标识该TA,如apnic等 | | |
|
|
||||||
| cert_der | Vec<u8> | 原始DER内容 | | |
|
|
||||||
| cert | X509Certificate | 基础X509证书 | | RFC 5280 §4.1 |
|
|
||||||
| resource | ResourceSet | 资源集合 | | |
|
|
||||||
| publication_point | Uri | 获取该TA的URI | | |
|
|
||||||
|
|
||||||
### 2.3.2 ResourceSet
|
|
||||||
资源集合是来自RFC 3779的IP地址块(§2)和AS号段(§3),受约束于RFC 8630 §2.3
|
|
||||||
|
|
||||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
|
||||||
|------|----------------|--------|-------------|---------------------------|
|
|
||||||
| ips | IpResourceSet | IP地址集合 | 不能是inherit | RFC 3779 §2和RFC 8630 §2.3 |
|
|
||||||
| asns | AsnResourceSet | ASN集合 | 不能是inherit | RFC 3779 §3和RFC 8630 §2.3 |
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (### 2.3.3 IpResourceSet)
|
|
||||||
|
|
||||||
[//]: # (包括IPv4和IPv6的前缀表示)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
|
|
||||||
|
|
||||||
[//]: # (|----|------------------------|----------|-------------|--------------|)
|
|
||||||
|
|
||||||
[//]: # (| v4 | PrefixSet<Ipv4Prefix> | IPv4前缀集合 | | RFC 3779 §2 |)
|
|
||||||
|
|
||||||
[//]: # (| v6 | PrefixSet<Ipv6Prefix> | IPv6前缀集合 | | RFC 3779 §2 |)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (### 2.3.4 AsnResourceSet)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
|
|
||||||
|
|
||||||
[//]: # (|-------|--------------------|-------|-------------|-------------|)
|
|
||||||
|
|
||||||
[//]: # (| range | RangeSet<AsnBlock> | ASN集合 | | RFC 3779 §3 |)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (### 2.3.5 Ipv4Prefix)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
|
|
||||||
|
|
||||||
[//]: # (|------|-----|-----|---------|-------------|)
|
|
||||||
|
|
||||||
[//]: # (| addr | u32 | 地址 | | RFC 3779 §2 |)
|
|
||||||
|
|
||||||
[//]: # (| len | u8 | 长度 | 0-32 | RFC 3779 §2 |)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (### 2.3.6 Ipv6Prefix)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
|
|
||||||
|
|
||||||
[//]: # (|------|------|-----|---------|-------------|)
|
|
||||||
|
|
||||||
[//]: # (| addr | u128 | 地址 | | RFC 3779 §2 |)
|
|
||||||
|
|
||||||
[//]: # (| len | u8 | 长度 | 0-128 | RFC 3779 §2 |)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (### 2.3.7 AsnBlock)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
|
|
||||||
|
|
||||||
[//]: # (|----------|----------|-------|---------|--------------|)
|
|
||||||
|
|
||||||
[//]: # (| asn | Asn | ASN | | RFC 3779 §3 |)
|
|
||||||
|
|
||||||
[//]: # (| asnRange | AsnRange | ASN范围 | | RFC 3779 §3 |)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (### 2.3.8 Asn)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
|
|
||||||
|
|
||||||
[//]: # (|-----|-----|-----|---------|-------------|)
|
|
||||||
|
|
||||||
[//]: # (| asn | u32 | ASN | | RFC 3779 §3 |)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (### 2.3.8 AsnRange)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
|
|
||||||
|
|
||||||
[//]: # (|-----|-----|-------|---------|--------------|)
|
|
||||||
|
|
||||||
[//]: # (| min | Asn | 最小ASN | | RFC 3779 §3 |)
|
|
||||||
|
|
||||||
[//]: # (| max | Asn | 最大ASN | | RFC 3779 §3 |)
|
|
||||||
|
|
||||||
# 2.4 TA校验流程(RFC 8630 §3)
|
|
||||||
1. 从TAL的URI列表中获取证书对象。(顺序访问,若前面失效,再访问后面的)
|
|
||||||
2. 验证证书格式,必须是当前、有效的自签名RPKI证书。
|
|
||||||
3. 验证公钥匹配。TAL中的SubjectPublicKeyInfo与下载证书的公钥一致。
|
|
||||||
4. 其他检查。
|
|
||||||
5. 更新本地存储库缓存。
|
|
||||||
88
specs/02_ta_certificate.md
Normal file
88
specs/02_ta_certificate.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# 02. TA(Trust Anchor)自签名证书
|
||||||
|
|
||||||
|
## 2.1 对象定位
|
||||||
|
|
||||||
|
在 RP 侧,“信任锚(Trust Anchor, TA)”以一个**自签名 CA 资源证书**体现,其可获取位置与期望公钥由 TAL 提供。RFC 8630 §2.3。
|
||||||
|
|
||||||
|
本文件描述两个紧密相关的数据对象:
|
||||||
|
|
||||||
|
1) `TaCertificate`:TA 自签名资源证书本体(X.509 DER)
|
||||||
|
2) `TrustAnchor`:语义组合对象(`TAL` + `TaCertificate` 的绑定语义)
|
||||||
|
|
||||||
|
## 2.2 原始载体与编码
|
||||||
|
|
||||||
|
- 载体:X.509 证书(通常以 `.cer` 存放于仓库,但文件扩展名不作为语义依据)。
|
||||||
|
- 编码:DER。TA 证书必须符合 RPKI 资源证书 profile。RFC 8630 §2.3;RFC 6487 §4。
|
||||||
|
|
||||||
|
### 2.2.1 X.509 Certificate 的 ASN.1 定义(RFC 5280 §4.1;TA 与 RC 共享)
|
||||||
|
|
||||||
|
TA 证书与普通资源证书(RC)在编码层面都是 X.509 `Certificate`(DER)。其 ASN.1 定义如下:RFC 5280 §4.1。
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
Certificate ::= SEQUENCE {
|
||||||
|
tbsCertificate TBSCertificate,
|
||||||
|
signatureAlgorithm AlgorithmIdentifier,
|
||||||
|
signatureValue BIT STRING }
|
||||||
|
```
|
||||||
|
|
||||||
|
其中 `tbsCertificate.extensions`(v3 扩展)是 RPKI 语义的主要承载处(IP/AS 资源扩展、SIA/AIA/CRLDP 等)。RFC 5280 §4.1;RPKI 对字段/扩展存在性与关键性约束见 RFC 6487 §4。
|
||||||
|
|
||||||
|
> 说明:更完整的 RC 编码层结构(包括 Extension 外层“extnValue 二次 DER 解码”的套娃方式)在 `03_resource_certificate_rc.md` 与 `00_common_types.md` 中给出。
|
||||||
|
|
||||||
|
## 2.3 TA 证书的 RPKI 语义约束(在 RC profile 基础上额外强调)
|
||||||
|
|
||||||
|
### 2.3.1 自签名与 profile
|
||||||
|
|
||||||
|
- TA URI 指向的对象 **MUST** 是一个**自签名 CA 证书**,并且 **MUST** 符合 RPKI 证书 profile。RFC 8630 §2.3;RFC 6487 §4。
|
||||||
|
- 自签名证书在 RC profile 下的通用差异(例如 CRLDP/AIA 的省略规则、AKI 的规则)见 RFC 6487。RFC 6487 §4.8.3;RFC 6487 §4.8.6;RFC 6487 §4.8.7。
|
||||||
|
|
||||||
|
### 2.3.2 INR(IP/AS 资源扩展)在 TA 上的额外约束
|
||||||
|
|
||||||
|
- TA 的 INR 扩展(IP/AS 资源扩展,RFC 3779)**MUST** 是非空资源集合。RFC 8630 §2.3。
|
||||||
|
- TA 的 INR 扩展 **MUST NOT** 使用 `inherit` 形式。RFC 8630 §2.3。
|
||||||
|
- 说明:一般 RC profile 允许 `inherit`。RFC 6487 §4.8.10;RFC 6487 §4.8.11;RFC 3779 §2.2.3.5;RFC 3779 §3.2.3.3。
|
||||||
|
|
||||||
|
### 2.3.3 TAL ↔ TA 公钥绑定
|
||||||
|
|
||||||
|
- 用于验证 TA 的公钥(来自 TAL 中的 SPKI)**MUST** 与 TA 证书中的 `subjectPublicKeyInfo` 相同。RFC 8630 §2.3。
|
||||||
|
|
||||||
|
### 2.3.4 TA 稳定性语义(实现需建模为“约束/假设”,但不属于验证结果态)
|
||||||
|
|
||||||
|
- TA 公钥与 TAL 中公钥必须保持稳定(用于 RP 侧长期信任锚)。RFC 8630 §2.3。
|
||||||
|
|
||||||
|
### 2.3.5 TA 与 CRL/Manifest 的关系(语义)
|
||||||
|
|
||||||
|
- RFC 8630 指出:TA 为自签名证书,没有对应 CRL,且不会被 manifest 列出;TA 的获取/轮换由 TAL 控制。RFC 8630 §2.3。
|
||||||
|
|
||||||
|
> 注:这条更偏“发布/运维语义”,但对数据对象建模有影响:`TrustAnchor` 组合对象不应依赖 CRL/MFT 的存在。
|
||||||
|
|
||||||
|
## 2.4 抽象数据模型(接口)
|
||||||
|
|
||||||
|
### 2.4.1 `TaCertificate`
|
||||||
|
|
||||||
|
> 该对象在字段层面复用 `RC(CA)` 的语义模型(见 `03_resource_certificate_rc.md`),但增加 TA 特有约束。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `raw_der` | `DerBytes` | TA 证书 DER | X.509 DER;证书 profile 约束见 RC 文档 | RFC 8630 §2.3;RFC 6487 §4 |
|
||||||
|
| `rc_ca` | `ResourceCaCertificate` | 以 RC(CA) 语义解析出的字段集合 | 必须满足“自签名 CA”分支约束;且 INR 必须非空且不允许 inherit | RFC 8630 §2.3;RFC 6487 §4;RFC 3779 §2/§3 |
|
||||||
|
|
||||||
|
### 2.4.2 `TrustAnchor`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `tal` | `Tal` | TAL 文件语义对象 | 见 `01_tal.md` | RFC 8630 §2.2 |
|
||||||
|
| `ta_certificate` | `TaCertificate` | TA 证书语义对象 | TA URI 指向的对象 | RFC 8630 §2.3 |
|
||||||
|
| `tal_spki_der` | `DerBytes` | 从 TAL 解析出的 SPKI DER | `tal.subject_public_key_info_der` | RFC 8630 §2.2 |
|
||||||
|
| `ta_spki_der` | `DerBytes` | 从 TA 证书抽取的 SPKI DER | `ta_certificate` 的 `subjectPublicKeyInfo` | RFC 8630 §2.3;RFC 5280 §4.1.2.7 |
|
||||||
|
|
||||||
|
**绑定约束(字段级)**
|
||||||
|
|
||||||
|
- `tal_spki_der` 必须与 `ta_spki_der` 完全相等(字节层面的 DER 等价)。RFC 8630 §2.3。
|
||||||
|
|
||||||
|
## 2.5 字段级约束清单(实现对照)
|
||||||
|
|
||||||
|
- TA URI 指向的对象必须是自签名 CA 证书,且符合 RPKI 证书 profile。RFC 8630 §2.3;RFC 6487 §4。
|
||||||
|
- TA 的 INR 扩展必须非空,且不得使用 inherit。RFC 8630 §2.3。
|
||||||
|
- TAL 中 SPKI 必须与 TA 证书的 `subjectPublicKeyInfo` 匹配。RFC 8630 §2.3。
|
||||||
|
- TA 不依赖 CRL/MFT(无对应 CRL,且不被 manifest 列出)。RFC 8630 §2.3。
|
||||||
314
specs/03_rc.md
314
specs/03_rc.md
@@ -1,314 +0,0 @@
|
|||||||
# 03. RC (Resource Certifications)
|
|
||||||
|
|
||||||
## 3.1 对象定位
|
|
||||||
RC是资源证书,包括CA和EE
|
|
||||||
|
|
||||||
## 3.2 原始载体与编码
|
|
||||||
|
|
||||||
- 载体:X.509 certificates.
|
|
||||||
- 编码:DER(遵循 RFC 5280 的 Certificate 结构与字段语义,但受 RPKI profile 限制)RFC 6487 §4
|
|
||||||
|
|
||||||
### 3.2.1 基本语法(RFC 5280 §4,RFC 6487 )
|
|
||||||
|
|
||||||
RC是遵循RFC5280定义的X.509Certificate语法(RFC 5280 §4),并且符合RFC 6487 §4的约束。只选取RFC 6487 §4章节列出来的字段。(Unless specifically noted as being OPTIONAL, all the fields listed
|
|
||||||
here MUST be present, and any other fields MUST NOT appear in a
|
|
||||||
conforming resource certificate.)
|
|
||||||
|
|
||||||
```
|
|
||||||
Certificate ::= SEQUENCE {
|
|
||||||
tbsCertificate TBSCertificate,
|
|
||||||
signatureAlgorithm AlgorithmIdentifier,
|
|
||||||
signatureValue BIT STRING
|
|
||||||
}
|
|
||||||
|
|
||||||
TBSCertificate ::= SEQUENCE {
|
|
||||||
version [0] EXPLICIT Version MUST be v3,
|
|
||||||
serialNumber CertificateSerialNumber,
|
|
||||||
signature AlgorithmIdentifier,
|
|
||||||
issuer Name,
|
|
||||||
subject Name,
|
|
||||||
validity Validity,
|
|
||||||
subjectPublicKeyInfo SubjectPublicKeyInfo,
|
|
||||||
extensions [3] EXPLICIT Extensions OPTIONAL
|
|
||||||
-- If present, version MUST be v3
|
|
||||||
}
|
|
||||||
|
|
||||||
Version ::= INTEGER { v1(0), v2(1), v3(2) }
|
|
||||||
|
|
||||||
CertificateSerialNumber ::= INTEGER
|
|
||||||
|
|
||||||
Validity ::= SEQUENCE {
|
|
||||||
notBefore Time,
|
|
||||||
notAfter Time }
|
|
||||||
|
|
||||||
Time ::= CHOICE {
|
|
||||||
utcTime UTCTime,
|
|
||||||
generalTime GeneralizedTime }
|
|
||||||
|
|
||||||
UniqueIdentifier ::= BIT STRING
|
|
||||||
|
|
||||||
SubjectPublicKeyInfo ::= SEQUENCE {
|
|
||||||
algorithm AlgorithmIdentifier,
|
|
||||||
subjectPublicKey BIT STRING }
|
|
||||||
|
|
||||||
Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension
|
|
||||||
|
|
||||||
Extension ::= SEQUENCE {
|
|
||||||
extnID OBJECT IDENTIFIER,
|
|
||||||
critical BOOLEAN DEFAULT FALSE,
|
|
||||||
extnValue OCTET STRING
|
|
||||||
-- contains the DER encoding of an ASN.1 value
|
|
||||||
-- corresponding to the extension type identified
|
|
||||||
-- by extnID
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> 其中`Name` "a valid X.501 distinguished name"(RFC 6487 §4.4)
|
|
||||||
|
|
||||||
### 3.2.2 证书扩展字段 (RFC 6487 §4.8)
|
|
||||||
|
|
||||||
RC的证书扩展字段按照RFC 6487 §4.8的规定,有以下几个扩展:
|
|
||||||
|
|
||||||
- Basic Constraints
|
|
||||||
- Subject Key Identifier
|
|
||||||
- Authority Key Identifier
|
|
||||||
- Key Usage
|
|
||||||
- Extended Key Usage(CA证书,以及验证RPKI对象的EE证书不能出现该字段。非RPKI对象的EE可以出现EKU,但必须为non-critical)
|
|
||||||
- CRL Distribution Points
|
|
||||||
- Authority Information Access
|
|
||||||
- Subject Information Access
|
|
||||||
- SIA for CA Certificates
|
|
||||||
- SIA for EE Certificates
|
|
||||||
- Certificate Policies
|
|
||||||
- IP Resources
|
|
||||||
- AS Resources
|
|
||||||
|
|
||||||
```
|
|
||||||
# Basic Constraints
|
|
||||||
id-ce-basicConstraints OBJECT IDENTIFIER ::= { id-ce 19 }
|
|
||||||
|
|
||||||
BasicConstraints ::= SEQUENCE {
|
|
||||||
cA BOOLEAN DEFAULT FALSE }
|
|
||||||
|
|
||||||
|
|
||||||
# Subject Key Identifier
|
|
||||||
id-ce-subjectKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 14 }
|
|
||||||
|
|
||||||
SubjectKeyIdentifier ::= KeyIdentifier
|
|
||||||
|
|
||||||
KeyIdentifier ::= OCTET STRING
|
|
||||||
|
|
||||||
|
|
||||||
# Authority Key Identifier
|
|
||||||
id-ce-authorityKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 35 }
|
|
||||||
|
|
||||||
AuthorityKeyIdentifier ::= SEQUENCE {
|
|
||||||
keyIdentifier [0] KeyIdentifier OPTIONAL }
|
|
||||||
|
|
||||||
|
|
||||||
# Key Usage
|
|
||||||
id-ce-keyUsage OBJECT IDENTIFIER ::= { id-ce 15 }
|
|
||||||
|
|
||||||
KeyUsage ::= BIT STRING {
|
|
||||||
digitalSignature (0),
|
|
||||||
nonRepudiation (1), -- recent editions of X.509 have
|
|
||||||
-- renamed this bit to contentCommitment
|
|
||||||
keyEncipherment (2),
|
|
||||||
dataEncipherment (3),
|
|
||||||
keyAgreement (4),
|
|
||||||
keyCertSign (5),
|
|
||||||
cRLSign (6),
|
|
||||||
encipherOnly (7),
|
|
||||||
decipherOnly (8) }
|
|
||||||
|
|
||||||
|
|
||||||
# Extended Key Usage
|
|
||||||
id-ce-extKeyUsage OBJECT IDENTIFIER ::= { id-ce 37 }
|
|
||||||
|
|
||||||
ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId
|
|
||||||
|
|
||||||
KeyPurposeId ::= OBJECT IDENTIFIER
|
|
||||||
|
|
||||||
|
|
||||||
# CRL Distribution Points
|
|
||||||
id-ce-cRLDistributionPoints OBJECT IDENTIFIER ::= { id-ce 31 }
|
|
||||||
|
|
||||||
CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint
|
|
||||||
|
|
||||||
DistributionPoint ::= SEQUENCE {
|
|
||||||
distributionPoint [0] DistributionPointName OPTIONAL }
|
|
||||||
|
|
||||||
DistributionPointName ::= CHOICE {
|
|
||||||
fullName [0] GeneralNames }
|
|
||||||
|
|
||||||
|
|
||||||
## Authority Information Access
|
|
||||||
id-pe-authorityInfoAccess OBJECT IDENTIFIER ::= { id-pe 1 }
|
|
||||||
|
|
||||||
AuthorityInfoAccessSyntax ::=
|
|
||||||
SEQUENCE SIZE (1..MAX) OF AccessDescription
|
|
||||||
|
|
||||||
AccessDescription ::= SEQUENCE {
|
|
||||||
accessMethod OBJECT IDENTIFIER,
|
|
||||||
accessLocation GeneralName }
|
|
||||||
|
|
||||||
# AccessDescription
|
|
||||||
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
|
|
||||||
# CA 证书发布位置
|
|
||||||
id-ad-caIssuers OBJECT IDENTIFIER ::= { id-ad 2 }
|
|
||||||
# OCSP 服务地址
|
|
||||||
id-ad-ocsp OBJECT IDENTIFIER ::= { id-ad 1 }
|
|
||||||
|
|
||||||
|
|
||||||
# Subject Information Access
|
|
||||||
id-pe-subjectInfoAccess OBJECT IDENTIFIER ::= { id-pe 11 }
|
|
||||||
|
|
||||||
SubjectInfoAccessSyntax ::= SEQUENCE SIZE (1..MAX) OF AccessDescription
|
|
||||||
AccessDescription ::= SEQUENCE {
|
|
||||||
accessMethod OBJECT IDENTIFIER,
|
|
||||||
accessLocation GeneralName }
|
|
||||||
|
|
||||||
## Subject Information Access for CA (RFC 6487 §4.8.8.1)
|
|
||||||
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
|
|
||||||
id-ad-rpkiManifest OBJECT IDENTIFIER ::= { id-ad 10 }
|
|
||||||
|
|
||||||
必须存在一个accessMethod=id-ad-caRepository,accessLocation=rsyncURI。
|
|
||||||
必须存在一个accessMethod=id-ad-repiManifest, accessLocation=rsync URI,指向该CA的mft对象。
|
|
||||||
|
|
||||||
## Subject Information Access for EE (RFC 6487 §4.8.8.2)
|
|
||||||
id-ad-signedObject OBJECT IDENTIFIER ::= { id-ad 11 }
|
|
||||||
|
|
||||||
必须存在一个accessMethod=id-ad-signedObject, accessLocation=rsyncURI
|
|
||||||
不允许其他的accessMethod
|
|
||||||
|
|
||||||
|
|
||||||
# Certificate Policies
|
|
||||||
id-ce-certificatePolicies OBJECT IDENTIFIER ::= { id-ce 32 }
|
|
||||||
anyPolicy OBJECT IDENTIFIER ::= { id-ce-certificatePolicies 0 }
|
|
||||||
|
|
||||||
certificatePolicies ::= SEQUENCE SIZE (1..MAX) OF PolicyInformation
|
|
||||||
|
|
||||||
PolicyInformation ::= SEQUENCE {
|
|
||||||
policyIdentifier CertPolicyId,
|
|
||||||
policyQualifiers SEQUENCE SIZE (1..MAX) OF PolicyQualifierInfo OPTIONAL }
|
|
||||||
|
|
||||||
CertPolicyId ::= OBJECT IDENTIFIER
|
|
||||||
|
|
||||||
PolicyQualifierInfo ::= SEQUENCE {
|
|
||||||
policyQualifierId PolicyQualifierId,
|
|
||||||
qualifier ANY DEFINED BY policyQualifierId }
|
|
||||||
|
|
||||||
-- policyQualifierIds for Internet policy qualifiers
|
|
||||||
id-qt OBJECT IDENTIFIER ::= { id-pkix 2 }
|
|
||||||
id-qt-cps OBJECT IDENTIFIER ::= { id-qt 1 }
|
|
||||||
id-qt-unotice OBJECT IDENTIFIER ::= { id-qt 2 }
|
|
||||||
|
|
||||||
PolicyQualifierId ::= OBJECT IDENTIFIER ( id-qt-cps | id-qt-unotice )
|
|
||||||
|
|
||||||
Qualifier ::= CHOICE {
|
|
||||||
cPSuri CPSuri,
|
|
||||||
userNotice UserNotice }
|
|
||||||
|
|
||||||
CPSuri ::= IA5String
|
|
||||||
|
|
||||||
UserNotice ::= SEQUENCE {
|
|
||||||
noticeRef NoticeReference OPTIONAL,
|
|
||||||
explicitText DisplayText OPTIONAL }
|
|
||||||
|
|
||||||
NoticeReference ::= SEQUENCE {
|
|
||||||
organization DisplayText,
|
|
||||||
noticeNumbers SEQUENCE OF INTEGER }
|
|
||||||
|
|
||||||
DisplayText ::= CHOICE {
|
|
||||||
ia5String IA5String (SIZE (1..200)),
|
|
||||||
visibleString VisibleString (SIZE (1..200)),
|
|
||||||
bmpString BMPString (SIZE (1..200)),
|
|
||||||
utf8String UTF8String (SIZE (1..200)) }
|
|
||||||
|
|
||||||
|
|
||||||
# IP Resources
|
|
||||||
id-pe-ipAddrBlocks OBJECT IDENTIFIER ::= { id-pe 7 }
|
|
||||||
|
|
||||||
IPAddrBlocks ::= SEQUENCE OF IPAddressFamily
|
|
||||||
|
|
||||||
IPAddressFamily ::= SEQUENCE { -- AFI & optional SAFI --
|
|
||||||
addressFamily OCTET STRING (SIZE (2..3)),
|
|
||||||
ipAddressChoice IPAddressChoice }
|
|
||||||
|
|
||||||
IPAddressChoice ::= CHOICE {
|
|
||||||
inherit NULL, -- inherit from issuer --
|
|
||||||
addressesOrRanges SEQUENCE OF IPAddressOrRange }
|
|
||||||
|
|
||||||
IPAddressOrRange ::= CHOICE {
|
|
||||||
addressPrefix IPAddress,
|
|
||||||
addressRange IPAddressRange }
|
|
||||||
|
|
||||||
IPAddressRange ::= SEQUENCE {
|
|
||||||
min IPAddress,
|
|
||||||
max IPAddress }
|
|
||||||
|
|
||||||
IPAddress ::= BIT STRING
|
|
||||||
|
|
||||||
|
|
||||||
# AS Resources
|
|
||||||
id-pe-autonomousSysIds OBJECT IDENTIFIER ::= { id-pe 8 }
|
|
||||||
ASIdentifiers ::= SEQUENCE {
|
|
||||||
asnum [0] EXPLICIT ASIdentifierChoice OPTIONAL,
|
|
||||||
rdi [1] EXPLICIT ASIdentifierChoice OPTIONAL}
|
|
||||||
|
|
||||||
ASIdentifierChoice ::= CHOICE {
|
|
||||||
inherit NULL, -- inherit from issuer --
|
|
||||||
asIdsOrRanges SEQUENCE OF ASIdOrRange }
|
|
||||||
|
|
||||||
ASIdOrRange ::= CHOICE {
|
|
||||||
id ASId,
|
|
||||||
range ASRange }
|
|
||||||
|
|
||||||
ASRange ::= SEQUENCE {
|
|
||||||
min ASId,
|
|
||||||
max ASId }
|
|
||||||
|
|
||||||
ASId ::= INTEGER
|
|
||||||
```
|
|
||||||
|
|
||||||
# 3.3 抽象数据结构
|
|
||||||
采用X509 Certificate + Resource + 约束校验的方式组合
|
|
||||||
|
|
||||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
|
||||||
|----------|---------------------|----------|---------|---------------|
|
|
||||||
| cert_der | Vec<u8> | 证书原始数据 | | |
|
|
||||||
| cert | X509Certificate | 基础X509证书 | | RFC 5280 §4.1 |
|
|
||||||
| resource | ResourceSet | 资源集合 | | |
|
|
||||||
|
|
||||||
|
|
||||||
# 3.4 约束规则
|
|
||||||
|
|
||||||
## 3.4.1 Cert约束校验规则
|
|
||||||
RFC 6487中规定的证书的字段参见[3.2.1 ](#321-基本语法rfc-5280-4rfc-6487-)
|
|
||||||
-
|
|
||||||
|
|
||||||
| 字段 | 语义 | 约束/解析规则 | RFC 引用 |
|
|
||||||
|-----------|-------|----------------------------------------------|--------------|
|
|
||||||
| version | 证书版本 | 必须是v3(值为2) | RFC6487 §4.1 |
|
|
||||||
| serial | 证书编号 | 同一个CA签发的证书编号必须唯一 | RFC6487 §4.2 |
|
|
||||||
| validity | 证书有效期 | notBefore:时间不能早于证书的生成时间。若时间段大于上级证书的有效期,也是有效的 | RFC6487 §4.6 |
|
|
||||||
|
|
||||||
|
|
||||||
## 3.4.2 Cert Extentions中字段的约束校验规则
|
|
||||||
RFC 6487中规定的扩展字段参见[3.2.2 ](#322-证书扩展字段-rfc-6487-48)
|
|
||||||
|
|
||||||
| 字段 | critical | 语义 | 约束/解析规则 | RFC 引用 |
|
|
||||||
|----------------------------|----------|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|
|
|
||||||
| basicConstraints | Y | 证书类型 | CA证书:cA=TRUE; EE证书:cA=FALSE | RFC6487 §4.8.1 |
|
|
||||||
| subjectKeyIdentifier | N | 证书公钥 | SKI = SHA-1(DER-encoded SPKI bit string) | RFC6487 §4.8.2 |
|
|
||||||
| authorityKeyIdentifier | N | 父证书的公钥 | 字段只包含keyIdentifier,不能包含authorityCertIssuer和authorityCertSerialNumber;除了自签名CA外,其余证书必须出现。自签名CA若出现该字段,则等于SKI | RFC6487 §4.8.3 |
|
|
||||||
| keyUsage | Y | 证书公钥的用途权限 | CA证书:keyCertSign = TRUE, cRLSign = TRUE 其他都是FALSE。EE证书:digitalSignature = TRUE 其他都是FALSE | RFC6487 §4.8.4 |
|
|
||||||
| extendedKeyUsage | N | 扩展证书公钥的用途权限 | CA证书:不能出现EKU;验证 RPKI 对象的 EE 证书:不能出现EKU;非 RPKI 对象的 EE:可以出现EKU,但必须为non-critical. | RFC6487 §4.8.5 |
|
|
||||||
| cRLDistributionPoints | N | CRL的发布点位置 | 字段:distributionPoint,不能包含reasons、cRLIssuer。其中distributionPoint字段包含:fullName,不能包含nameRelativeToCRLIssuer。fullName的格式必须是URI。自签名证书禁止出现该字段。非自签名证书必须出现。一个CA只能有一个CRL。一个CRLDP只能包含一个distributionPoint。但一个distributionPoint字段中可以包含多于1个的URI,但必须包含rsync URI且必须是最新的。 | RFC6487 §4.8.6 |
|
|
||||||
| authorityInformationAccess | N | 签发者的发布点位置 | 除了自签名的CA,必须出现。自签名CA,禁止出现。推荐的URI访问方式是rsync,并且rsyncURI的话,必须指定accessMethod=id-ad-caIssuers | RFC6487 §4.8.7 |
|
|
||||||
| subjectInformationAccess | N | 发布点位置 | CA证书:必须存在。必须存在一个accessMethod=id-ad-caRepository,accessLocation=rsyncURI。必须存在一个accessMethod=id-ad-repiManifest,accessLocation=rsync URI,指向该CA的mft对象。 EE证书:必须存在。必须存在一个accessMethod=id-ad-signedObject,accessLocation=rsyncURI。不允许其他的accessMethod | RFC6487 §4.8.8 |
|
|
||||||
| certificatePolicies | Y | 证书策略 | 必须存在,并且只能存在一种策略:RFC 6484 — RPKI Certificate Policy (CP) | RFC6487 §4.8.9 |
|
|
||||||
| iPResources | Y | IP地址集合 | 所有的RPKI证书中必须包含IP Resources或者ASResources,或者两者都包含。 | RFC6487 §4.8.10 |
|
|
||||||
| aSResources | Y | ASN集合 | 所有的RPKI证书中必须包含IP Resources或者ASResources,或者两者都包含。 | RFC6487 §4.8.11 |
|
|
||||||
|
|
||||||
|
|
||||||
460
specs/03_resource_certificate_rc.md
Normal file
460
specs/03_resource_certificate_rc.md
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
# 03. RC(Resource Certificate:资源证书,CA/EE)
|
||||||
|
|
||||||
|
## 3.1 对象定位
|
||||||
|
|
||||||
|
资源证书(RC)是 X.509 v3 证书,遵循 PKIX profile(RFC 5280),并受 RPKI profile 进一步约束。RFC 6487 §4。
|
||||||
|
|
||||||
|
RC 在 RPKI 中至少分为两类语义用途:
|
||||||
|
|
||||||
|
- `CA 证书`:签发下级证书/CRL,并在 SIA 中声明发布点与 manifest。RFC 6487 §4.8.8.1。
|
||||||
|
- `EE 证书`:用于验证某个 RPKI Signed Object(如 ROA/MFT),在 SIA 中指向被验证对象。RFC 6487 §4.8.8.2。
|
||||||
|
|
||||||
|
## 3.2 原始载体与编码
|
||||||
|
|
||||||
|
- 载体:X.509 证书。
|
||||||
|
- 编码:DER。RFC 6487 §4(“valid X.509 public key certificate consistent with RFC 5280” + RPKI 限制)。
|
||||||
|
|
||||||
|
### 3.2.1 X.509 v3 证书基本语法(ASN.1;RFC 5280 §4.1)
|
||||||
|
|
||||||
|
资源证书在编码层面是 RFC 5280 定义的 X.509 v3 `Certificate`(DER),其中 `tbsCertificate` 携带主体字段与扩展集合(`Extensions`)。RFC 5280 §4.1。
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
Certificate ::= SEQUENCE {
|
||||||
|
tbsCertificate TBSCertificate,
|
||||||
|
signatureAlgorithm AlgorithmIdentifier,
|
||||||
|
signatureValue BIT STRING }
|
||||||
|
|
||||||
|
TBSCertificate ::= SEQUENCE {
|
||||||
|
version [0] EXPLICIT Version DEFAULT v1,
|
||||||
|
serialNumber CertificateSerialNumber,
|
||||||
|
signature AlgorithmIdentifier,
|
||||||
|
issuer Name,
|
||||||
|
validity Validity,
|
||||||
|
subject Name,
|
||||||
|
subjectPublicKeyInfo SubjectPublicKeyInfo,
|
||||||
|
issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
|
||||||
|
-- If present, version MUST be v2 or v3
|
||||||
|
subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
|
||||||
|
-- If present, version MUST be v2 or v3
|
||||||
|
extensions [3] EXPLICIT Extensions OPTIONAL
|
||||||
|
-- If present, version MUST be v3
|
||||||
|
}
|
||||||
|
|
||||||
|
Version ::= INTEGER { v1(0), v2(1), v3(2) }
|
||||||
|
|
||||||
|
CertificateSerialNumber ::= INTEGER
|
||||||
|
|
||||||
|
Validity ::= SEQUENCE {
|
||||||
|
notBefore Time,
|
||||||
|
notAfter Time }
|
||||||
|
|
||||||
|
Time ::= CHOICE {
|
||||||
|
utcTime UTCTime,
|
||||||
|
generalTime GeneralizedTime }
|
||||||
|
|
||||||
|
UniqueIdentifier ::= BIT STRING
|
||||||
|
|
||||||
|
SubjectPublicKeyInfo ::= SEQUENCE {
|
||||||
|
algorithm AlgorithmIdentifier,
|
||||||
|
subjectPublicKey BIT STRING }
|
||||||
|
|
||||||
|
Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension
|
||||||
|
|
||||||
|
Extension ::= SEQUENCE {
|
||||||
|
extnID OBJECT IDENTIFIER,
|
||||||
|
critical BOOLEAN DEFAULT FALSE,
|
||||||
|
extnValue OCTET STRING
|
||||||
|
-- contains the DER encoding of an ASN.1 value
|
||||||
|
-- corresponding to the extension type identified
|
||||||
|
-- by extnID
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2.2 AlgorithmIdentifier(ASN.1;RFC 5280 §4.1.1.2)
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
AlgorithmIdentifier ::= SEQUENCE {
|
||||||
|
algorithm OBJECT IDENTIFIER,
|
||||||
|
parameters ANY DEFINED BY algorithm OPTIONAL }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2.3 Name / DN 结构(ASN.1;RFC 5280 §4.1.2.4)
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
Name ::= CHOICE { -- only one possibility for now --
|
||||||
|
rdnSequence RDNSequence }
|
||||||
|
|
||||||
|
RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
|
||||||
|
|
||||||
|
RelativeDistinguishedName ::=
|
||||||
|
SET SIZE (1..MAX) OF AttributeTypeAndValue
|
||||||
|
|
||||||
|
AttributeTypeAndValue ::= SEQUENCE {
|
||||||
|
type AttributeType,
|
||||||
|
value AttributeValue }
|
||||||
|
|
||||||
|
AttributeType ::= OBJECT IDENTIFIER
|
||||||
|
|
||||||
|
AttributeValue ::= ANY -- DEFINED BY AttributeType
|
||||||
|
|
||||||
|
DirectoryString ::= CHOICE {
|
||||||
|
teletexString TeletexString (SIZE (1..MAX)),
|
||||||
|
printableString PrintableString (SIZE (1..MAX)),
|
||||||
|
universalString UniversalString (SIZE (1..MAX)),
|
||||||
|
utf8String UTF8String (SIZE (1..MAX)),
|
||||||
|
bmpString BMPString (SIZE (1..MAX)) }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2.4 GeneralNames / GeneralName(ASN.1;RFC 5280 §4.2.1.6)
|
||||||
|
|
||||||
|
> 说明:RPKI 的 AIA/SIA/CRLDP 等扩展通常把 URI 编码在 `uniformResourceIdentifier [6] IA5String` 分支中。RFC 5280 §4.2.1.6。
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
|
||||||
|
|
||||||
|
GeneralName ::= CHOICE {
|
||||||
|
otherName [0] OtherName,
|
||||||
|
rfc822Name [1] IA5String,
|
||||||
|
dNSName [2] IA5String,
|
||||||
|
x400Address [3] ORAddress,
|
||||||
|
directoryName [4] Name,
|
||||||
|
ediPartyName [5] EDIPartyName,
|
||||||
|
uniformResourceIdentifier [6] IA5String,
|
||||||
|
iPAddress [7] OCTET STRING,
|
||||||
|
registeredID [8] OBJECT IDENTIFIER }
|
||||||
|
|
||||||
|
OtherName ::= SEQUENCE {
|
||||||
|
type-id OBJECT IDENTIFIER,
|
||||||
|
value [0] EXPLICIT ANY DEFINED BY type-id }
|
||||||
|
|
||||||
|
EDIPartyName ::= SEQUENCE {
|
||||||
|
nameAssigner [0] DirectoryString OPTIONAL,
|
||||||
|
partyName [1] DirectoryString }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2.5 AIA(Authority Information Access;ASN.1;RFC 5280 §4.2.2.1)
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
id-pe-authorityInfoAccess OBJECT IDENTIFIER ::= { id-pe 1 }
|
||||||
|
|
||||||
|
AuthorityInfoAccessSyntax ::=
|
||||||
|
SEQUENCE SIZE (1..MAX) OF AccessDescription
|
||||||
|
|
||||||
|
AccessDescription ::= SEQUENCE {
|
||||||
|
accessMethod OBJECT IDENTIFIER,
|
||||||
|
accessLocation GeneralName }
|
||||||
|
|
||||||
|
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
|
||||||
|
|
||||||
|
id-ad-caIssuers OBJECT IDENTIFIER ::= { id-ad 2 }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2.6 SIA(Subject Information Access;ASN.1;RFC 5280 §4.2.2.2)
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
id-pe-subjectInfoAccess OBJECT IDENTIFIER ::= { id-pe 11 }
|
||||||
|
|
||||||
|
SubjectInfoAccessSyntax ::=
|
||||||
|
SEQUENCE SIZE (1..MAX) OF AccessDescription
|
||||||
|
|
||||||
|
AccessDescription ::= SEQUENCE {
|
||||||
|
accessMethod OBJECT IDENTIFIER,
|
||||||
|
accessLocation GeneralName }
|
||||||
|
|
||||||
|
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
|
||||||
|
|
||||||
|
id-ad-caRepository OBJECT IDENTIFIER ::= { id-ad 5 }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2.7 RPKI 在 SIA 中新增/使用的 accessMethod OID(RFC 6487 §4.8.8.1 / §4.8.8.2;RFC 8182 §3.2)
|
||||||
|
|
||||||
|
> 说明:下列 OID 用于 `AccessDescription.accessMethod`,并放在 SIA 的 `extnValue` 内层结构中(其外层 extnID 仍为 SIA:`id-pe-subjectInfoAccess`)。RFC 6487 §4.8.8;RFC 8182 §3.2。
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
|
||||||
|
|
||||||
|
id-ad-rpkiManifest OBJECT IDENTIFIER ::= { id-ad 10 } -- 1.3.6.1.5.5.7.48.10
|
||||||
|
|
||||||
|
id-ad-signedObject OBJECT IDENTIFIER ::= { id-ad 11 } -- 1.3.6.1.5.5.7.48.11
|
||||||
|
|
||||||
|
id-ad-rpkiNotify OBJECT IDENTIFIER ::= { id-ad 13 } -- 1.3.6.1.5.5.7.48.13
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2.8 CRLDistributionPoints(CRLDP;ASN.1;RFC 5280 §4.2.1.13)
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
id-ce-cRLDistributionPoints OBJECT IDENTIFIER ::= { id-ce 31 }
|
||||||
|
|
||||||
|
CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint
|
||||||
|
|
||||||
|
DistributionPoint ::= SEQUENCE {
|
||||||
|
distributionPoint [0] DistributionPointName OPTIONAL,
|
||||||
|
reasons [1] ReasonFlags OPTIONAL,
|
||||||
|
cRLIssuer [2] GeneralNames OPTIONAL }
|
||||||
|
|
||||||
|
DistributionPointName ::= CHOICE {
|
||||||
|
fullName [0] GeneralNames,
|
||||||
|
nameRelativeToCRLIssuer [1] RelativeDistinguishedName }
|
||||||
|
|
||||||
|
ReasonFlags ::= BIT STRING {
|
||||||
|
unused (0),
|
||||||
|
keyCompromise (1),
|
||||||
|
cACompromise (2),
|
||||||
|
affiliationChanged (3),
|
||||||
|
superseded (4),
|
||||||
|
cessationOfOperation (5),
|
||||||
|
certificateHold (6),
|
||||||
|
privilegeWithdrawn (7),
|
||||||
|
aACompromise (8) }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2.9 Certificate Policies(ASN.1;RFC 5280 §4.2.1.4)
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
id-ce-certificatePolicies OBJECT IDENTIFIER ::= { id-ce 32 }
|
||||||
|
|
||||||
|
anyPolicy OBJECT IDENTIFIER ::= { id-ce-certificatePolicies 0 }
|
||||||
|
|
||||||
|
certificatePolicies ::= SEQUENCE SIZE (1..MAX) OF PolicyInformation
|
||||||
|
|
||||||
|
PolicyInformation ::= SEQUENCE {
|
||||||
|
policyIdentifier CertPolicyId,
|
||||||
|
policyQualifiers SEQUENCE SIZE (1..MAX) OF
|
||||||
|
PolicyQualifierInfo OPTIONAL }
|
||||||
|
|
||||||
|
CertPolicyId ::= OBJECT IDENTIFIER
|
||||||
|
|
||||||
|
PolicyQualifierInfo ::= SEQUENCE {
|
||||||
|
policyQualifierId PolicyQualifierId,
|
||||||
|
qualifier ANY DEFINED BY policyQualifierId }
|
||||||
|
|
||||||
|
-- policyQualifierIds for Internet policy qualifiers
|
||||||
|
|
||||||
|
id-qt OBJECT IDENTIFIER ::= { id-pkix 2 }
|
||||||
|
id-qt-cps OBJECT IDENTIFIER ::= { id-qt 1 }
|
||||||
|
id-qt-unotice OBJECT IDENTIFIER ::= { id-qt 2 }
|
||||||
|
|
||||||
|
PolicyQualifierId ::= OBJECT IDENTIFIER ( id-qt-cps | id-qt-unotice )
|
||||||
|
|
||||||
|
Qualifier ::= CHOICE {
|
||||||
|
cPSuri CPSuri,
|
||||||
|
userNotice UserNotice }
|
||||||
|
|
||||||
|
CPSuri ::= IA5String
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2.10 RFC 3779 IP/AS 资源扩展(ASN.1;RFC 3779 §2.2.1-§2.2.3;RFC 3779 §3.2.1-§3.2.3)
|
||||||
|
|
||||||
|
> 说明:RFC 3779 给出两个扩展的 OID 与 ASN.1 语法;它们作为 X.509 v3 扩展出现在 `extensions` 中(外层 extnID 为下列 OID)。RPKI profile 进一步约束 criticality/SAFI/RDI 等,见 RFC 6487 §4.8.10-§4.8.11。
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
-- IP Address Delegation Extension
|
||||||
|
id-pe-ipAddrBlocks OBJECT IDENTIFIER ::= { id-pe 7 }
|
||||||
|
|
||||||
|
IPAddrBlocks ::= SEQUENCE OF IPAddressFamily
|
||||||
|
|
||||||
|
IPAddressFamily ::= SEQUENCE { -- AFI & optional SAFI --
|
||||||
|
addressFamily OCTET STRING (SIZE (2..3)),
|
||||||
|
ipAddressChoice IPAddressChoice }
|
||||||
|
|
||||||
|
IPAddressChoice ::= CHOICE {
|
||||||
|
inherit NULL, -- inherit from issuer --
|
||||||
|
addressesOrRanges SEQUENCE OF IPAddressOrRange }
|
||||||
|
|
||||||
|
IPAddressOrRange ::= CHOICE {
|
||||||
|
addressPrefix IPAddress,
|
||||||
|
addressRange IPAddressRange }
|
||||||
|
|
||||||
|
IPAddressRange ::= SEQUENCE {
|
||||||
|
min IPAddress,
|
||||||
|
max IPAddress }
|
||||||
|
|
||||||
|
IPAddress ::= BIT STRING
|
||||||
|
|
||||||
|
-- Autonomous System Identifier Delegation Extension
|
||||||
|
id-pe-autonomousSysIds OBJECT IDENTIFIER ::= { id-pe 8 }
|
||||||
|
|
||||||
|
ASIdentifiers ::= SEQUENCE {
|
||||||
|
asnum [0] EXPLICIT ASIdentifierChoice OPTIONAL,
|
||||||
|
rdi [1] EXPLICIT ASIdentifierChoice OPTIONAL}
|
||||||
|
|
||||||
|
ASIdentifierChoice ::= CHOICE {
|
||||||
|
inherit NULL, -- inherit from issuer --
|
||||||
|
asIdsOrRanges SEQUENCE OF ASIdOrRange }
|
||||||
|
|
||||||
|
ASIdOrRange ::= CHOICE {
|
||||||
|
id ASId,
|
||||||
|
range ASRange }
|
||||||
|
|
||||||
|
ASRange ::= SEQUENCE {
|
||||||
|
min ASId,
|
||||||
|
max ASId }
|
||||||
|
|
||||||
|
ASId ::= INTEGER
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2.11 其它 RPKI profile 相关扩展的 ASN.1 定义(RFC 5280 §4.2.1.1-§4.2.1.3;RFC 5280 §4.2.1.9;RFC 5280 §4.2.1.12)
|
||||||
|
|
||||||
|
> 说明:这些是 RPKI 资源证书 profile(RFC 6487 §4.8)所引用的通用 PKIX 扩展语法。RPKI 对其“必须/禁止/criticality/字段允许性”有额外限制(见本文件 3.3/3.4),但编码层的 ASN.1 类型来自 RFC 5280。
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
id-ce-authorityKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 35 }
|
||||||
|
|
||||||
|
AuthorityKeyIdentifier ::= SEQUENCE {
|
||||||
|
keyIdentifier [0] KeyIdentifier OPTIONAL,
|
||||||
|
authorityCertIssuer [1] GeneralNames OPTIONAL,
|
||||||
|
authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL }
|
||||||
|
|
||||||
|
KeyIdentifier ::= OCTET STRING
|
||||||
|
|
||||||
|
id-ce-subjectKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 14 }
|
||||||
|
|
||||||
|
SubjectKeyIdentifier ::= KeyIdentifier
|
||||||
|
|
||||||
|
id-ce-keyUsage OBJECT IDENTIFIER ::= { id-ce 15 }
|
||||||
|
|
||||||
|
KeyUsage ::= BIT STRING {
|
||||||
|
digitalSignature (0),
|
||||||
|
nonRepudiation (1), -- recent editions of X.509 have
|
||||||
|
-- renamed this bit to contentCommitment
|
||||||
|
keyEncipherment (2),
|
||||||
|
dataEncipherment (3),
|
||||||
|
keyAgreement (4),
|
||||||
|
keyCertSign (5),
|
||||||
|
cRLSign (6),
|
||||||
|
encipherOnly (7),
|
||||||
|
decipherOnly (8) }
|
||||||
|
|
||||||
|
id-ce-basicConstraints OBJECT IDENTIFIER ::= { id-ce 19 }
|
||||||
|
|
||||||
|
BasicConstraints ::= SEQUENCE {
|
||||||
|
cA BOOLEAN DEFAULT FALSE,
|
||||||
|
pathLenConstraint INTEGER (0..MAX) OPTIONAL }
|
||||||
|
|
||||||
|
id-ce-extKeyUsage OBJECT IDENTIFIER ::= { id-ce 37 }
|
||||||
|
|
||||||
|
ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId
|
||||||
|
|
||||||
|
KeyPurposeId ::= OBJECT IDENTIFIER
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.3 抽象数据模型(接口)
|
||||||
|
|
||||||
|
> 说明:本模型面向“语义化解析产物”。实现可保留 `raw_der` 作为可追溯入口。
|
||||||
|
|
||||||
|
### 3.3.1 顶层联合类型:`ResourceCertificate`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `raw_der` | `DerBytes` | 证书 DER | 原样保留(建议) | RFC 6487 §4 |
|
||||||
|
| `tbs` | `RpkixTbsCertificate` | 证书语义字段(见下) | 仅允许 RFC 6487 允许的字段/扩展;其他字段 MUST NOT 出现 | RFC 6487 §4 |
|
||||||
|
| `kind` | `enum { ca, ee }` | 语义分类 | 来自 BasicConstraints + 用途约束 | RFC 6487 §4.8.1;RFC 6487 §4.8.8 |
|
||||||
|
|
||||||
|
### 3.3.1.1 派生类型(用于字段类型标注)
|
||||||
|
|
||||||
|
为避免在其它对象文档里反复写“`ResourceCertificate` 且 `kind==...`”,这里定义两个派生/别名类型:
|
||||||
|
|
||||||
|
- `ResourceCaCertificate`:`ResourceCertificate` 且 `kind == ca`
|
||||||
|
- `ResourceEeCertificate`:`ResourceCertificate` 且 `kind == ee`
|
||||||
|
|
||||||
|
这些派生类型不引入新字段,只是对 `ResourceCertificate.kind` 的约束化视图。
|
||||||
|
|
||||||
|
### 3.3.2 `RpkixTbsCertificate`(语义字段集合)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `version` | `int` | X.509 版本 | MUST 为 v3(字段值为 2) | RFC 6487 §4.1 |
|
||||||
|
| `serial_number` | `int` | 序列号 | 正整数;对每 CA 签发唯一 | RFC 6487 §4.2 |
|
||||||
|
| `signature_algorithm` | `Oid` | 证书签名算法 | 必须为 `sha256WithRSAEncryption`(`1.2.840.113549.1.1.11`) | RFC 6487 §4.3;RFC 7935 §2(引用 RFC 4055) |
|
||||||
|
| `issuer_dn` | `RpkixDistinguishedName` | 颁发者 DN | 必含 1 个 CommonName;可含 1 个 serialNumber;CN 必须 PrintableString | RFC 6487 §4.4 |
|
||||||
|
| `subject_dn` | `RpkixDistinguishedName` | 主体 DN | 同 issuer 约束;且对同一 issuer 下“实体+公钥”唯一 | RFC 6487 §4.5 |
|
||||||
|
| `validity_not_before` | `UtcTime` | 有效期起 | X.509 `Time`(UTCTime/GeneralizedTime)解析为 UTC 时间点 | RFC 6487 §4.6.1;RFC 5280 §4.1.2.5 |
|
||||||
|
| `validity_not_after` | `UtcTime` | 有效期止 | X.509 `Time`(UTCTime/GeneralizedTime)解析为 UTC 时间点 | RFC 6487 §4.6.2;RFC 5280 §4.1.2.5 |
|
||||||
|
| `subject_public_key_info` | `DerBytes` | SPKI DER | 算法 profile 指定 | RFC 6487 §4.7;RFC 7935 §3.1 |
|
||||||
|
| `extensions` | `RpkixExtensions` | 扩展集合 | 见下表;criticality/存在性/内容受约束 | RFC 6487 §4.8 |
|
||||||
|
|
||||||
|
### 3.3.3 `RpkixDistinguishedName`(RPKI profile 下的 DN 语义)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `common_name` | `string` | CommonName (CN) | MUST 存在且仅 1 个;类型为 PrintableString | RFC 6487 §4.4;RFC 6487 §4.5 |
|
||||||
|
| `serial_number` | `optional[string]` | serialNumber | MAY 存在且仅 1 个 | RFC 6487 §4.4;RFC 6487 §4.5 |
|
||||||
|
| `rfc4514` | `string` | DN 的 RFC4514 字符串表示 | 便于日志/索引(实现自选) | RFC 6487 §4.5(引用 RFC4514) |
|
||||||
|
|
||||||
|
### 3.3.4 `RpkixExtensions`(核心扩展语义)
|
||||||
|
|
||||||
|
> 表中 “存在性/criticality” 指 RPKI profile 下对该扩展的要求;实现应能区分 “字段缺失” 与 “字段存在但不符合约束”。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 存在性/criticality 与内容约束 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `basic_constraints` | `optional[BasicConstraints]` | CA 标志 | **extnID=`2.5.29.19`**;CA 证书:MUST present & critical;EE:MUST NOT present;pathLen MUST NOT present | RFC 6487 §4.8.1;RFC 5280 §4.2.1.9 |
|
||||||
|
| `subject_key_identifier` | `bytes` | SKI | **extnID=`2.5.29.14`**;MUST present & non-critical;值为 subjectPublicKey 的 DER bit string 的 SHA-1 哈希 | RFC 6487 §4.8.2(引用 RFC 5280 §4.2.1.2) |
|
||||||
|
| `authority_key_identifier` | `optional[AuthorityKeyIdentifier]` | AKI | **extnID=`2.5.29.35`**;自签名:MAY present 且可等于 SKI;非自签名:MUST present;authorityCertIssuer/authorityCertSerialNumber MUST NOT present;non-critical | RFC 6487 §4.8.3;RFC 5280 §4.2.1.1 |
|
||||||
|
| `key_usage` | `KeyUsage` | KeyUsage | **extnID=`2.5.29.15`**;MUST present & critical;CA:仅 `keyCertSign` 与 `cRLSign` 为 TRUE;EE:仅 `digitalSignature` 为 TRUE | RFC 6487 §4.8.4;RFC 5280 §4.2.1.3 |
|
||||||
|
| `extended_key_usage` | `optional[OidSet]` | EKU | **extnID=`2.5.29.37`**;CA:MUST NOT appear;用于验证 RPKI 对象的 EE:MUST NOT appear;若出现不得标 critical | RFC 6487 §4.8.5;RFC 5280 §4.2.1.12 |
|
||||||
|
| `crl_distribution_points` | `optional[CrlDistributionPoints]` | CRLDP | **extnID=`2.5.29.31`**;自签名:MUST be omitted;非自签名:MUST present & non-critical;仅 1 个 DistributionPoint;fullName URI;必须包含至少 1 个 `rsync://` | RFC 6487 §4.8.6;RFC 5280 §4.2.1.13 |
|
||||||
|
| `authority_info_access` | `optional[AuthorityInfoAccess]` | AIA | **extnID=`1.3.6.1.5.5.7.1.1`**;自签名:MUST be omitted;非自签名:MUST present & non-critical;必须含 accessMethod `id-ad-caIssuers`(**`1.3.6.1.5.5.7.48.2`**) 的 `rsync://` URI;可含同对象其它 URI | RFC 6487 §4.8.7;RFC 5280 §4.2.2.1 |
|
||||||
|
| `subject_info_access_ca` | `optional[SubjectInfoAccessCa]` | SIA(CA) | **extnID=`1.3.6.1.5.5.7.1.11`**;CA:MUST present & non-critical;必须含 accessMethod `id-ad-caRepository`(**`1.3.6.1.5.5.7.48.5`**)(`rsync://` 目录 URI)与 `id-ad-rpkiManifest`(**`1.3.6.1.5.5.7.48.10`**)(`rsync://` 对象 URI);若 CA 使用 RRDP,还会包含 `id-ad-rpkiNotify`(**`1.3.6.1.5.5.7.48.13`**)(HTTPS Notification URI) | RFC 6487 §4.8.8.1;RFC 5280 §4.2.2.2;RFC 8182 §3.2 |
|
||||||
|
| `subject_info_access_ee` | `optional[SubjectInfoAccessEe]` | SIA(EE) | **extnID=`1.3.6.1.5.5.7.1.11`**;EE:MUST present & non-critical;必须含 accessMethod `id-ad-signedObject`(**`1.3.6.1.5.5.7.48.11`**);URI **MUST include** `rsync://`;EE 的 SIA 不允许其它 AccessMethods | RFC 6487 §4.8.8.2;RFC 5280 §4.2.2.2 |
|
||||||
|
| `certificate_policies` | `CertificatePolicies` | 证书策略 | **extnID=`2.5.29.32`**;MUST present & critical;恰好 1 个 policy;并允许 0 或 1 个 CPS qualifier(若存在其 id 必为 `id-qt-cps`(**`1.3.6.1.5.5.7.2.1`**)) | RFC 6487 §4.8.9;RFC 7318 §2;RFC 5280 §4.2.1.4 |
|
||||||
|
| `ip_resources` | `optional[IpResourceSet]` | IP 资源扩展 | **extnID=`1.3.6.1.5.5.7.1.7`**;IP/AS 两者至少其一 MUST present;若 present MUST be critical;内容为 RFC 3779 语义;在公用互联网场景 SAFI MUST NOT 使用;且必须为非空或 inherit | RFC 6487 §4.8.10;RFC 3779 §2.2.1;RFC 3779 §2.2.2 |
|
||||||
|
| `as_resources` | `optional[AsResourceSet]` | AS 资源扩展 | **extnID=`1.3.6.1.5.5.7.1.8`**;IP/AS 两者至少其一 MUST present;若 present MUST be critical;内容为 RFC 3779 语义;RDI MUST NOT 使用;且必须为非空或 inherit | RFC 6487 §4.8.11;RFC 3779 §3.2.1;RFC 3779 §3.2.2 |
|
||||||
|
|
||||||
|
### 3.3.5 结构化子类型(建议)
|
||||||
|
|
||||||
|
#### `BasicConstraints`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `ca` | `bool` | 是否 CA | 由 issuer 决定;在 CA 证书中该扩展必须存在 | RFC 6487 §4.8.1 |
|
||||||
|
| `path_len_constraint` | `None` | pathLenConstraint | MUST NOT present(RPKI profile 不使用) | RFC 6487 §4.8.1 |
|
||||||
|
|
||||||
|
#### `AuthorityKeyIdentifier`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `key_identifier` | `bytes` | AKI.keyIdentifier | 使用 issuer 公钥的 SHA-1 哈希(按 RFC 5280 的定义) | RFC 6487 §4.8.3(引用 RFC 5280 §4.2.1.1) |
|
||||||
|
| `authority_cert_issuer` | `None` | authorityCertIssuer | MUST NOT present | RFC 6487 §4.8.3 |
|
||||||
|
| `authority_cert_serial_number` | `None` | authorityCertSerialNumber | MUST NOT present | RFC 6487 §4.8.3 |
|
||||||
|
|
||||||
|
#### `CrlDistributionPoints`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `distribution_point_uris` | `list[Uri]` | CRL 位置列表 | 仅 1 个 DistributionPoint;必须包含至少 1 个 `rsync://` URI 指向该 issuer 最新 CRL;可含其它 URI | RFC 6487 §4.8.6 |
|
||||||
|
|
||||||
|
#### `AuthorityInfoAccess`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `ca_issuers_uris` | `list[Uri]` | 上级 CA 证书位置 | accessMethod=`id-ad-caIssuers`(`1.3.6.1.5.5.7.48.2`);必含 `rsync://` URI;可含同对象其它 URI | RFC 6487 §4.8.7;RFC 5280 §4.2.2.1 |
|
||||||
|
|
||||||
|
#### `SubjectInfoAccessCa`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `ca_repository_uris` | `list[Uri]` | CA 发布点目录(repository publication point) | accessMethod=`id-ad-caRepository`(`1.3.6.1.5.5.7.48.5`);至少 1 个;必须包含 `rsync://`;也可包含其它机制(例如 `https://`)作为“同一目录”的替代访问方式;顺序表示 CA 偏好 | RFC 6487 §4.8.8.1;RFC 5280 §4.2.2.2 |
|
||||||
|
| `rpki_manifest_uris` | `list[Uri]` | 当前 manifest 对象 URI | accessMethod=`id-ad-rpkiManifest`(`1.3.6.1.5.5.7.48.10`);至少 1 个;必须包含 `rsync://`;也可包含其它机制(例如 `https://`)作为“同一对象”的替代访问方式 | RFC 6487 §4.8.8.1;RFC 5280 §4.2.2.2 |
|
||||||
|
| `rpki_notify_uris` | `optional[list[Uri]]` | RRDP Notification(Update Notification File)URI | accessMethod=`id-ad-rpkiNotify`(`1.3.6.1.5.5.7.48.13`);若存在则 accessLocation MUST 为 `https://` URI,指向 RRDP Notification 文件 | RFC 8182 §3.2;RFC 5280 §4.2.2.2 |
|
||||||
|
|
||||||
|
#### `SubjectInfoAccessEe`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `signed_object_uris` | `list[Uri]` | 被 EE 证书验证的签名对象位置 | accessMethod=`id-ad-signedObject`(`1.3.6.1.5.5.7.48.11`);必须包含 `rsync://`;其它 URI 可作为同对象替代机制;EE SIA 不允许其它 AccessMethods | RFC 6487 §4.8.8.2;RFC 5280 §4.2.2.2 |
|
||||||
|
|
||||||
|
#### `CertificatePolicies`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `policy_oid` | `Oid` | 唯一 policy OID | 恰好 1 个 policy;RPKI CP 分配的 OID 为 `id-cp-ipAddr-asNumber`(`1.3.6.1.5.5.7.14.2`) | RFC 6487 §4.8.9;RFC 6484 §1.2 |
|
||||||
|
| `cps_uri` | `optional[Uri]` | CPS policy qualifier URI | MAY 存在且最多 1 个;若存在其 `policyQualifierId` 必为 `id-qt-cps`;对该 URI 不施加处理要求 | RFC 7318 §2;RFC 5280 §4.2.1.4 |
|
||||||
|
|
||||||
|
## 3.4 字段级约束清单(实现对照)
|
||||||
|
|
||||||
|
- 仅允许 RFC 6487 §4 指定的字段/扩展;未列出字段 MUST NOT 出现。RFC 6487 §4。
|
||||||
|
- 证书版本必须为 v3。RFC 6487 §4.1。
|
||||||
|
- CA/EE 在 BasicConstraints 与 SIA 的约束不同。RFC 6487 §4.8.1;RFC 6487 §4.8.8.1;RFC 6487 §4.8.8.2。
|
||||||
|
- KeyUsage:CA 仅 `keyCertSign`/`cRLSign`;EE 仅 `digitalSignature`。RFC 6487 §4.8.4。
|
||||||
|
- CRLDP/AIA:自签名必须省略;非自签名必须存在并包含 `rsync://`。RFC 6487 §4.8.6;RFC 6487 §4.8.7。
|
||||||
|
- IP/AS 资源扩展:两者至少其一存在;若存在必须 critical;语义来自 RFC 3779;在公用互联网场景 SAFI 与 RDI 均不得使用。RFC 6487 §4.8.10;RFC 6487 §4.8.11;RFC 3779 §2.2.3;RFC 3779 §3.2.3。
|
||||||
158
specs/05_signed_object_cms.md
Normal file
158
specs/05_signed_object_cms.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# 05. RPKI Signed Object(CMS SignedData 外壳)
|
||||||
|
|
||||||
|
## 5.1 对象定位
|
||||||
|
|
||||||
|
ROA、Manifest 等都属于 “RPKI Signed Object”,其外壳是 CMS SignedData,并受 RFC 6488 的 profile 约束;RFC 9589 进一步更新了 `signedAttrs` 的要求。RFC 6488 §2-§4;RFC 9589 §4。
|
||||||
|
|
||||||
|
本文件描述**通用外壳模型**(eContentType/eContent 由具体对象文档给出)。
|
||||||
|
|
||||||
|
## 5.2 原始载体与编码
|
||||||
|
|
||||||
|
- 载体:CMS `ContentInfo`,其中 `contentType` 为 SignedData。RFC 6488 §2;RFC 6488 §3(1a)。
|
||||||
|
- 编码:DER。RFC 6488 §2;RFC 6488 §3(1l)。
|
||||||
|
|
||||||
|
### 5.2.1 CMS 外壳:ContentInfo(ASN.1;RFC 5652 §3)
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
ContentInfo ::= SEQUENCE {
|
||||||
|
contentType ContentType,
|
||||||
|
content [0] EXPLICIT ANY DEFINED BY contentType }
|
||||||
|
|
||||||
|
ContentType ::= OBJECT IDENTIFIER
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2.2 CMS 外壳:SignedData(ASN.1;RFC 5652 §5.1)
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
id-signedData OBJECT IDENTIFIER ::= { iso(1) member-body(2)
|
||||||
|
us(840) rsadsi(113549) pkcs(1) pkcs7(7) 2 }
|
||||||
|
|
||||||
|
SignedData ::= SEQUENCE {
|
||||||
|
version CMSVersion,
|
||||||
|
digestAlgorithms DigestAlgorithmIdentifiers,
|
||||||
|
encapContentInfo EncapsulatedContentInfo,
|
||||||
|
certificates [0] IMPLICIT CertificateSet OPTIONAL,
|
||||||
|
crls [1] IMPLICIT RevocationInfoChoices OPTIONAL,
|
||||||
|
signerInfos SignerInfos }
|
||||||
|
|
||||||
|
DigestAlgorithmIdentifiers ::= SET OF DigestAlgorithmIdentifier
|
||||||
|
|
||||||
|
SignerInfos ::= SET OF SignerInfo
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2.3 CMS 外壳:EncapsulatedContentInfo(ASN.1;RFC 5652 §5.2)
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
EncapsulatedContentInfo ::= SEQUENCE {
|
||||||
|
eContentType ContentType,
|
||||||
|
eContent [0] EXPLICIT OCTET STRING OPTIONAL }
|
||||||
|
|
||||||
|
ContentType ::= OBJECT IDENTIFIER
|
||||||
|
```
|
||||||
|
|
||||||
|
> 注:CMS 允许 `eContent` 不一定 DER 编码(RFC 5652 §5.2);但 RPKI signed object profile 要求**整个对象 DER 编码**(RFC 6488 §2;RFC 6488 §3(1l)),且 eContent(payload)由对象规范定义并通常为 DER(如 ROA:RFC 9582 §4;Manifest:RFC 9286 §4.2)。
|
||||||
|
|
||||||
|
### 5.2.4 CMS 外壳:SignerInfo 与 Attribute(ASN.1;RFC 5652 §5.3)
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
SignerInfo ::= SEQUENCE {
|
||||||
|
version CMSVersion,
|
||||||
|
sid SignerIdentifier,
|
||||||
|
digestAlgorithm DigestAlgorithmIdentifier,
|
||||||
|
signedAttrs [0] IMPLICIT SignedAttributes OPTIONAL,
|
||||||
|
signatureAlgorithm SignatureAlgorithmIdentifier,
|
||||||
|
signature SignatureValue,
|
||||||
|
unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL }
|
||||||
|
|
||||||
|
SignerIdentifier ::= CHOICE {
|
||||||
|
issuerAndSerialNumber IssuerAndSerialNumber,
|
||||||
|
subjectKeyIdentifier [0] SubjectKeyIdentifier }
|
||||||
|
|
||||||
|
SignedAttributes ::= SET SIZE (1..MAX) OF Attribute
|
||||||
|
|
||||||
|
UnsignedAttributes ::= SET SIZE (1..MAX) OF Attribute
|
||||||
|
|
||||||
|
Attribute ::= SEQUENCE {
|
||||||
|
attrType OBJECT IDENTIFIER,
|
||||||
|
attrValues SET OF AttributeValue }
|
||||||
|
|
||||||
|
AttributeValue ::= ANY
|
||||||
|
|
||||||
|
SignatureValue ::= OCTET STRING
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2.5 RPKI 对 CMS 外壳字段的 profile 约束(RFC 6488 §2.1;RFC 6488 §3;更新:RFC 9589 §4)
|
||||||
|
|
||||||
|
> 说明:上面是 CMS 的通用 ASN.1;RPKI 进一步约束取值与允许出现的字段(例如 SignedData.version 必须为 3、crls 必须省略、signedAttrs 的内容限制等)。RFC 6488 §2-§3;RFC 9589 §4。
|
||||||
|
|
||||||
|
### 5.2.6 signedAttrs 中允许的属性与 attrType OID(RFC 6488 §2.1.6.4.1-§2.1.6.4.2;更新:RFC 9589 §4)
|
||||||
|
|
||||||
|
RPKI signed object profile 对 `SignerInfo.signedAttrs` 的 Attribute 集合施加限制(除 ASN.1 结构外,还包含“只允许哪些 attrType”的编码约束):
|
||||||
|
|
||||||
|
- `content-type`:attrType OID `1.2.840.113549.1.9.3`。RFC 6488 §2.1.6.4.1。
|
||||||
|
- `message-digest`:attrType OID `1.2.840.113549.1.9.4`。RFC 6488 §2.1.6.4.2。
|
||||||
|
- `signing-time`:attrType OID `1.2.840.113549.1.9.5`。RFC 9589 §4(更新 RFC 6488 的相关要求)。
|
||||||
|
|
||||||
|
并且:
|
||||||
|
|
||||||
|
- 每种属性在集合中只能出现一次;且 `attrValues` 虽然语法是 `SET OF`,但在 RPKI 中必须只含一个值。RFC 6488 §2.1.6.4。
|
||||||
|
|
||||||
|
## 5.3 抽象数据模型(接口)
|
||||||
|
|
||||||
|
### 5.3.1 `RpkiSignedObject`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `raw_der` | `DerBytes` | CMS DER | 原样保留(建议) | RFC 6488 §2;RFC 6488 §3(1l) |
|
||||||
|
| `content_info_content_type` | `Oid` | ContentInfo.contentType | MUST 为 SignedData:`1.2.840.113549.1.7.2` | RFC 6488 §3(1a) |
|
||||||
|
| `signed_data` | `SignedDataProfiled` | SignedData 语义字段 | 见下 | RFC 6488 §2.1;RFC 6488 §3 |
|
||||||
|
|
||||||
|
### 5.3.2 `SignedDataProfiled`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `version` | `int` | SignedData.version | MUST 为 3 | RFC 6488 §3(1b);RFC 6488 §2.1.1 |
|
||||||
|
| `digest_algorithms` | `list[Oid]` | SignedData.digestAlgorithms | MUST contain exactly one digest algorithm,且必须为 `id-sha256`(`2.16.840.1.101.3.4.2.1`) | RFC 6488 §2.1.2;RFC 7935 §2(引用 RFC 5754) |
|
||||||
|
| `encap_content_info` | `EncapsulatedContentInfo` | EncapsulatedContentInfo | 见下;eContentType 由具体对象定义 | RFC 6488 §2.1.3 |
|
||||||
|
| `certificates` | `list[ResourceEeCertificate]` | SignedData.certificates | MUST present;且仅包含 1 个 EE 证书;该 EE 的 SKI 必须匹配 SignerInfo.sid | RFC 6488 §3(1c) |
|
||||||
|
| `crls` | `None` | SignedData.crls | MUST be omitted | RFC 6488 §3(1d) |
|
||||||
|
| `signer_infos` | `list[SignerInfoProfiled]` | SignedData.signerInfos | MUST contain exactly one SignerInfo | RFC 6488 §2.1;RFC 6488 §2.1(SignerInfos 约束段落) |
|
||||||
|
|
||||||
|
### 5.3.3 `EncapsulatedContentInfo`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `econtent_type` | `Oid` | eContentType | MUST 与 signedAttrs.content-type 的 attrValues 一致;具体值由对象定义(如 ROA/MFT) | RFC 6488 §3(1h);RFC 6488 §2.1.3.1 |
|
||||||
|
| `econtent_der` | `DerBytes` | eContent(对象 payload) | DER 编码的对象特定 ASN.1(ROA/MFT 文档定义);在 CMS 中以 OCTET STRING 承载 | RFC 6488 §2.1.3;RFC 9286 §4.2;RFC 9582 §4 |
|
||||||
|
|
||||||
|
### 5.3.4 `SignerInfoProfiled`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `version` | `int` | SignerInfo.version | MUST 为 3 | RFC 6488 §3(1e) |
|
||||||
|
| `sid_ski` | `bytes` | sid(SubjectKeyIdentifier) | 必须与 EE 证书的 SKI 匹配 | RFC 6488 §3(1c) |
|
||||||
|
| `digest_algorithm` | `Oid` | SignerInfo.digestAlgorithm | 必须为 `id-sha256`(`2.16.840.1.101.3.4.2.1`) | RFC 6488 §3(1j);RFC 7935 §2(引用 RFC 5754) |
|
||||||
|
| `signature_algorithm` | `Oid` | SignerInfo.signatureAlgorithm | 生成时 MUST 为 `rsaEncryption`(`1.2.840.113549.1.1.1`);验证时实现必须接受 `rsaEncryption` 或 `sha256WithRSAEncryption`(`1.2.840.113549.1.1.11`) | RFC 6488 §3(1k);RFC 7935 §2 |
|
||||||
|
| `signed_attrs` | `SignedAttrsProfiled` | signedAttrs | MUST present;仅允许特定 3 个属性 | RFC 9589 §4(更新 RFC 6488 §3(1f)/(1g)) |
|
||||||
|
| `unsigned_attrs` | `None` | unsignedAttrs | MUST be omitted | RFC 6488 §3(1i) |
|
||||||
|
|
||||||
|
### 5.3.5 `SignedAttrsProfiled`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `content_type` | `Oid` | signedAttrs.content-type | attrType=`1.2.840.113549.1.9.3`;MUST present;attrValues 等于 eContentType | RFC 9589 §4;RFC 6488 §3(1h) |
|
||||||
|
| `message_digest` | `bytes` | signedAttrs.message-digest | attrType=`1.2.840.113549.1.9.4`;MUST present | RFC 9589 §4(更新 RFC 6488 §3(1f)) |
|
||||||
|
| `signing_time` | `UtcTime` | signedAttrs.signing-time | attrType=`1.2.840.113549.1.9.5`;MUST present(时间值正确性不用于安全假设) | RFC 9589 §4;RFC 9589 §5 |
|
||||||
|
| `other_attrs` | `None` | 其它 signed attributes | MUST NOT be included(binary-signing-time 也不允许) | RFC 9589 §4 |
|
||||||
|
|
||||||
|
## 5.4 字段级约束清单(实现对照)
|
||||||
|
|
||||||
|
- ContentInfo.contentType 必须为 SignedData(OID `1.2.840.113549.1.7.2`)。RFC 6488 §3(1a)。
|
||||||
|
- SignedData.version 必须为 3,且 SignerInfos 仅允许 1 个 SignerInfo。RFC 6488 §3(1b);RFC 6488 §2.1。
|
||||||
|
- SignedData.certificates 必须存在且仅含 1 个 EE 证书;该证书 SKI 必须匹配 SignerInfo.sid。RFC 6488 §3(1c)。
|
||||||
|
- SignedData.crls 必须省略。RFC 6488 §3(1d)。
|
||||||
|
- signedAttrs 必须存在,且仅允许 content-type/message-digest/signing-time;其它全部禁止。RFC 9589 §4。
|
||||||
|
- eContentType 必须与 content-type attribute 一致。RFC 6488 §3(1h)。
|
||||||
|
- unsignedAttrs 必须省略。RFC 6488 §3(1i)。
|
||||||
|
- digest/signature 算法必须符合算法 profile。RFC 6488 §3(1j)/(1k);RFC 7935 §2。
|
||||||
|
- 整个对象必须 DER 编码。RFC 6488 §3(1l)。
|
||||||
110
specs/06_manifest_mft.md
Normal file
110
specs/06_manifest_mft.md
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# 06. Manifest(MFT)
|
||||||
|
|
||||||
|
## 6.1 对象定位
|
||||||
|
|
||||||
|
Manifest 是 CA 发布点内对象的“清单”(文件名 + hash),用于 RP 侧检测删除/替换/回放等不一致情况。RFC 9286 §1;RFC 9286 §6。
|
||||||
|
|
||||||
|
Manifest 是一种 RPKI Signed Object:CMS 外壳遵循 RFC 6488/9589,eContent 遵循 RFC 9286。RFC 9286 §4;RFC 6488 §4;RFC 9589 §4。
|
||||||
|
|
||||||
|
## 6.2 原始载体与编码
|
||||||
|
|
||||||
|
- 外壳:CMS SignedData DER(见 `05_signed_object_cms.md`)。RFC 9286 §4;RFC 6488 §2。
|
||||||
|
- eContentType:`id-ct-rpkiManifest`,OID `1.2.840.113549.1.9.16.1.26`。RFC 9286 §4.1。
|
||||||
|
- eContent:DER 编码 ASN.1 `Manifest`。RFC 9286 §4.2。
|
||||||
|
|
||||||
|
### 6.2.1 eContentType 与 eContent 的 ASN.1 定义(RFC 9286 §4.1;RFC 9286 §4.2)
|
||||||
|
|
||||||
|
Manifest 是一种 RPKI signed object(CMS 外壳见 `05_signed_object_cms.md`)。其 `eContentType` 与 `eContent` 的 ASN.1 由 RFC 9286 明确定义。RFC 9286 §4。
|
||||||
|
|
||||||
|
**eContentType(OID)**:RFC 9286 §4.1。
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
id-smime OBJECT IDENTIFIER ::= { iso(1) member-body(2) us(840)
|
||||||
|
rsadsi(113549) pkcs(1) pkcs9(9) 16 }
|
||||||
|
|
||||||
|
id-ct OBJECT IDENTIFIER ::= { id-smime 1 }
|
||||||
|
|
||||||
|
id-ct-rpkiManifest OBJECT IDENTIFIER ::= { id-ct 26 }
|
||||||
|
```
|
||||||
|
|
||||||
|
**eContent(Manifest 结构)**:RFC 9286 §4.2。
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
Manifest ::= SEQUENCE {
|
||||||
|
version [0] INTEGER DEFAULT 0,
|
||||||
|
manifestNumber INTEGER (0..MAX),
|
||||||
|
thisUpdate GeneralizedTime,
|
||||||
|
nextUpdate GeneralizedTime,
|
||||||
|
fileHashAlg OBJECT IDENTIFIER,
|
||||||
|
fileList SEQUENCE SIZE (0..MAX) OF FileAndHash
|
||||||
|
}
|
||||||
|
|
||||||
|
FileAndHash ::= SEQUENCE {
|
||||||
|
file IA5String,
|
||||||
|
hash BIT STRING
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
解码要点:
|
||||||
|
|
||||||
|
- `fileHashAlg` 决定 `FileAndHash.hash` 的算法与输出长度(RPKI profile 要求 SHA-256)。RFC 9286 §4.2.1;RFC 7935 §2。
|
||||||
|
- `hash` 在 ASN.1 中是 BIT STRING,但 hash 输出是按字节的比特串,DER 编码时应为 “unused bits = 0” 的 octet-aligned BIT STRING(实现可据此做一致性检查)。RFC 9286 §4.2。
|
||||||
|
|
||||||
|
## 6.3 解析规则(eContent 语义层)
|
||||||
|
|
||||||
|
输入:`RpkiSignedObject`。
|
||||||
|
|
||||||
|
1) 先按通用 Signed Object 外壳解析得到 `encap_content_info.econtent_type` 与 `econtent_der`。RFC 6488 §3;RFC 9589 §4。
|
||||||
|
2) 要求 `econtent_type == 1.2.840.113549.1.9.16.1.26`。RFC 9286 §4.1;RFC 9286 §4.4(1)。
|
||||||
|
3) 将 `econtent_der` 以 DER 解析为 `Manifest` ASN.1。RFC 9286 §4.2。
|
||||||
|
4) 将 `fileList` 映射为语义字段 `files: list[FileAndHash]`,其中 `hash` 为 `fileHashAlg` 对应算法的输出字节序列。RFC 9286 §4.2.1(fileHashAlg/fileList 定义)。
|
||||||
|
|
||||||
|
## 6.4 抽象数据模型(接口)
|
||||||
|
|
||||||
|
### 6.4.1 `ManifestObject`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `signed_object` | `RpkiSignedObject` | CMS 外壳 | 外壳约束见 RFC 6488/9589 | RFC 9286 §4;RFC 6488 §3;RFC 9589 §4 |
|
||||||
|
| `econtent_type` | `Oid` | eContentType | 必须为 `1.2.840.113549.1.9.16.1.26` | RFC 9286 §4.1 |
|
||||||
|
| `manifest` | `ManifestEContent` | eContent 语义对象 | 见下 | RFC 9286 §4.2 |
|
||||||
|
|
||||||
|
### 6.4.2 `ManifestEContent`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `version` | `int` | Manifest.version | MUST 为 0 | RFC 9286 §4.2.1(version) |
|
||||||
|
| `manifest_number` | `int` | manifestNumber | 0..MAX;可达 20 octets;issuer 必须单调递增;RP 必须可处理至 20 octets | RFC 9286 §4.2;RFC 9286 §4.2.1(manifestNumber) |
|
||||||
|
| `this_update` | `UtcTime` | thisUpdate | 由 ASN.1 `GeneralizedTime` 解析为 UTC 时间点;且必须比先前生成的 manifest 更新 | RFC 9286 §4.2;RFC 9286 §4.2.1(thisUpdate) |
|
||||||
|
| `next_update` | `UtcTime` | nextUpdate | 由 ASN.1 `GeneralizedTime` 解析为 UTC 时间点;且必须晚于 thisUpdate | RFC 9286 §4.2;RFC 9286 §4.2.1(nextUpdate) |
|
||||||
|
| `file_hash_alg` | `Oid` | fileHashAlg | 必须为 `id-sha256`(`2.16.840.1.101.3.4.2.1`) | RFC 9286 §4.2.1(fileHashAlg);RFC 7935 §2(引用 RFC 5754) |
|
||||||
|
| `files` | `list[FileAndHash]` | fileList | `SEQUENCE SIZE (0..MAX)`;每项含文件名与 hash | RFC 9286 §4.2;RFC 9286 §4.2.1(fileList) |
|
||||||
|
|
||||||
|
### 6.4.3 `FileAndHash`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `file_name` | `string` | 文件名(不含路径) | 字符集限制:`[a-zA-Z0-9-_]+` + `.` + 三字母扩展;扩展必须在 IANA “RPKI Repository Name Schemes” 注册表中 | RFC 9286 §4.2.2 |
|
||||||
|
| `hash_bytes` | `bytes` | 文件内容 hash | 由 `file_hash_alg` 指定算法计算 | RFC 9286 §4.2.1(fileHashAlg/fileList) |
|
||||||
|
|
||||||
|
## 6.5 字段级约束清单(实现对照)
|
||||||
|
|
||||||
|
- eContentType 必须为 `id-ct-rpkiManifest`(OID `1.2.840.113549.1.9.16.1.26`)。RFC 9286 §4.1。
|
||||||
|
- eContent 必须 DER 编码且符合 `Manifest` ASN.1。RFC 9286 §4.2。
|
||||||
|
- `version` 必须为 0。RFC 9286 §4.2.1。
|
||||||
|
- `manifestNumber` 由 issuer 单调递增;RP 必须能处理至 20 octets;issuer 不得超过 20 octets。RFC 9286 §4.2.1。
|
||||||
|
- `nextUpdate` 必须晚于 `thisUpdate`。RFC 9286 §4.2.1。
|
||||||
|
- `fileHashAlg` 必须符合算法 profile(SHA-256)。RFC 9286 §4.2.1;RFC 7935 §2。
|
||||||
|
- `fileList` 中 `file` 名称字符集与扩展名受限;实现需按 RFC 限制解析并保留大小写语义。RFC 9286 §4.2.2。
|
||||||
|
|
||||||
|
## 6.6 与 EE 证书的语义约束(为后续验证准备)
|
||||||
|
|
||||||
|
Manifest 使用“one-time-use EE certificate”进行签名验证,规范对该 EE 证书的使用方式给出约束:
|
||||||
|
|
||||||
|
- Manifest 相关 EE 证书应为 one-time-use(每次新 manifest 生成新密钥对/新 EE)。RFC 9286 §4(Section 4 前导段落)。
|
||||||
|
- 用于签名/验证 manifest 的 EE 证书在 RFC 3779 的资源扩展中描述 INRs 时,**MUST** 使用 `inherit`(而不是显式列出资源集合)。RFC 9286 §5.1(生成步骤 2)。
|
||||||
|
- 若证书包含 **IP Address Delegation Extension**(IP prefix delegation):内容必须为 `inherit`(不得显式列 prefix/range)。RFC 9286 §5.1;RFC 3779。
|
||||||
|
- 若证书包含 **AS Identifier Delegation Extension**(ASN delegation):内容必须为 `inherit`(不得显式列 ASID/range)。RFC 9286 §5.1;RFC 3779。
|
||||||
|
- 另外按资源证书 profile:资源证书 **MUST** 至少包含上述两类扩展之一(也可两者都有),且这些扩展 **MUST** 标记为 critical。RFC 6487 §2。
|
||||||
|
- 用于验证 manifest 的 EE 证书 **MUST** 具有与 `thisUpdate..nextUpdate` 区间一致的有效期,以避免 CRL 无谓增长。RFC 9286 §4.2.1(manifestNumber 段落前的说明)。
|
||||||
|
- 替换 manifest 时,CA 必须撤销旧 manifest 对应 EE 证书;且若新 manifest 早于旧 manifest 的 nextUpdate 发行,则 CA **MUST** 同时发行新 CRL 撤销旧 manifest EE。RFC 9286 §4.2.1(nextUpdate 段落末);RFC 9286 §5.1(生成步骤)。
|
||||||
159
specs/07_roa.md
Normal file
159
specs/07_roa.md
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
# 07. ROA(Route Origin Authorization)
|
||||||
|
|
||||||
|
## 7.1 对象定位
|
||||||
|
|
||||||
|
ROA 是一种 RPKI Signed Object,用于声明“某 AS 被授权起源某些前缀”。RFC 9582 §1;RFC 9582 §4。
|
||||||
|
|
||||||
|
ROA 由 CMS 外壳 + ROA eContent 组成:
|
||||||
|
|
||||||
|
- 外壳:RFC 6488(更新:RFC 9589)
|
||||||
|
- eContent:RFC 9582
|
||||||
|
|
||||||
|
## 7.2 原始载体与编码
|
||||||
|
|
||||||
|
- 外壳:CMS SignedData DER(见 `05_signed_object_cms.md`)。RFC 9582 §1(引用 RFC 6488)。
|
||||||
|
- eContentType:`id-ct-routeOriginAuthz`,OID `1.2.840.113549.1.9.16.1.24`。RFC 9582 §3。
|
||||||
|
- eContent:DER 编码 ASN.1 `RouteOriginAttestation`。RFC 9582 §4。
|
||||||
|
|
||||||
|
### 7.2.1 eContentType 与 eContent 的 ASN.1 定义(RFC 9582 §3;RFC 9582 §4)
|
||||||
|
|
||||||
|
ROA 是一种 RPKI signed object(CMS 外壳见 `05_signed_object_cms.md`)。RFC 9582 定义了其 `eContentType` 以及 `eContent`(payload)的 ASN.1。RFC 9582 §3-§4。
|
||||||
|
|
||||||
|
**eContentType(OID)**:RFC 9582 §3。
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
id-ct-routeOriginAuthz OBJECT IDENTIFIER ::=
|
||||||
|
{ iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1)
|
||||||
|
pkcs-9(9) id-smime(16) id-ct(1) routeOriginAuthz(24) }
|
||||||
|
```
|
||||||
|
|
||||||
|
**eContent(ROA ASN.1 模块)**:RFC 9582 §4。
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
RPKI-ROA-2023
|
||||||
|
{ iso(1) member-body(2) us(840) rsadsi(113549)
|
||||||
|
pkcs(1) pkcs9(9) smime(16) mod(0)
|
||||||
|
id-mod-rpkiROA-2023(75) }
|
||||||
|
|
||||||
|
DEFINITIONS EXPLICIT TAGS ::=
|
||||||
|
BEGIN
|
||||||
|
|
||||||
|
IMPORTS
|
||||||
|
CONTENT-TYPE
|
||||||
|
FROM CryptographicMessageSyntax-2010 -- in [RFC6268]
|
||||||
|
{ iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1)
|
||||||
|
pkcs-9(9) smime(16) modules(0) id-mod-cms-2009(58) } ;
|
||||||
|
|
||||||
|
ct-routeOriginAttestation CONTENT-TYPE ::=
|
||||||
|
{ TYPE RouteOriginAttestation
|
||||||
|
IDENTIFIED BY id-ct-routeOriginAuthz }
|
||||||
|
|
||||||
|
id-ct-routeOriginAuthz OBJECT IDENTIFIER ::=
|
||||||
|
{ iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1)
|
||||||
|
pkcs-9(9) id-smime(16) id-ct(1) routeOriginAuthz(24) }
|
||||||
|
|
||||||
|
RouteOriginAttestation ::= SEQUENCE {
|
||||||
|
version [0] INTEGER DEFAULT 0,
|
||||||
|
asID ASID,
|
||||||
|
ipAddrBlocks SEQUENCE (SIZE(1..2)) OF ROAIPAddressFamily }
|
||||||
|
|
||||||
|
ASID ::= INTEGER (0..4294967295)
|
||||||
|
|
||||||
|
ROAIPAddressFamily ::= SEQUENCE {
|
||||||
|
addressFamily ADDRESS-FAMILY.&afi ({AddressFamilySet}),
|
||||||
|
addresses ADDRESS-FAMILY.&Addresses
|
||||||
|
({AddressFamilySet}{@addressFamily}) }
|
||||||
|
|
||||||
|
ADDRESS-FAMILY ::= CLASS {
|
||||||
|
&afi OCTET STRING (SIZE(2)) UNIQUE,
|
||||||
|
&Addresses
|
||||||
|
} WITH SYNTAX { AFI &afi ADDRESSES &Addresses }
|
||||||
|
|
||||||
|
AddressFamilySet ADDRESS-FAMILY ::=
|
||||||
|
{ addressFamilyIPv4 | addressFamilyIPv6 }
|
||||||
|
|
||||||
|
addressFamilyIPv4 ADDRESS-FAMILY ::=
|
||||||
|
{ AFI afi-IPv4 ADDRESSES ROAAddressesIPv4 }
|
||||||
|
addressFamilyIPv6 ADDRESS-FAMILY ::=
|
||||||
|
{ AFI afi-IPv6 ADDRESSES ROAAddressesIPv6 }
|
||||||
|
|
||||||
|
afi-IPv4 OCTET STRING ::= '0001'H
|
||||||
|
afi-IPv6 OCTET STRING ::= '0002'H
|
||||||
|
|
||||||
|
ROAAddressesIPv4 ::= SEQUENCE (SIZE(1..MAX)) OF ROAIPAddress{ub-IPv4}
|
||||||
|
ROAAddressesIPv6 ::= SEQUENCE (SIZE(1..MAX)) OF ROAIPAddress{ub-IPv6}
|
||||||
|
|
||||||
|
ub-IPv4 INTEGER ::= 32
|
||||||
|
ub-IPv6 INTEGER ::= 128
|
||||||
|
|
||||||
|
ROAIPAddress {INTEGER: ub} ::= SEQUENCE {
|
||||||
|
address BIT STRING (SIZE(0..ub)),
|
||||||
|
maxLength INTEGER (0..ub) OPTIONAL }
|
||||||
|
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
编码/解码要点(与上面 ASN.1 结构直接对应):
|
||||||
|
|
||||||
|
- `addressFamily` 仅允许 IPv4/IPv6 两种 AFI,并且每个 AFI 最多出现一次。RFC 9582 §4.3.1。
|
||||||
|
- `address` 是 BIT STRING 表示的前缀,语义与 RFC 3779 的 `IPAddress` 一致(按前缀长度截断,DER unused bits 置零)。RFC 9582 §4.3.2.1(引用 RFC 3779 §2.2.3.8)。
|
||||||
|
- `maxLength` 为可选字段,出现与否会影响语义与编码规范约束(例如等于前缀长时不建议编码)。RFC 9582 §4.3.2.2。
|
||||||
|
|
||||||
|
## 7.3 解析规则(eContent 语义层)
|
||||||
|
|
||||||
|
输入:`RpkiSignedObject`。
|
||||||
|
|
||||||
|
1) 解析 CMS 外壳,得到 `econtent_type` 与 `econtent_der`。RFC 6488 §3;RFC 9589 §4。
|
||||||
|
2) 要求 `econtent_type == 1.2.840.113549.1.9.16.1.24`。RFC 9582 §3。
|
||||||
|
3) 将 `econtent_der` 以 DER 解析为 `RouteOriginAttestation` ASN.1。RFC 9582 §4。
|
||||||
|
4) 将 `ipAddrBlocks` 解析为“前缀集合”的语义结构,并建议按 RFC 9582 给出的 canonicalization 过程做去重/排序/归一化(以便后续处理一致)。RFC 9582 §4.3.3。
|
||||||
|
|
||||||
|
## 7.4 抽象数据模型(接口)
|
||||||
|
|
||||||
|
### 7.4.1 `RoaObject`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `signed_object` | `RpkiSignedObject` | CMS 外壳 | 外壳约束见 RFC 6488/9589 | RFC 9582 §1;RFC 6488 §3;RFC 9589 §4 |
|
||||||
|
| `econtent_type` | `Oid` | eContentType | 必须为 `1.2.840.113549.1.9.16.1.24` | RFC 9582 §3 |
|
||||||
|
| `roa` | `RoaEContent` | eContent 语义对象 | 见下 | RFC 9582 §4 |
|
||||||
|
|
||||||
|
### 7.4.2 `RoaEContent`(RouteOriginAttestation)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `version` | `int` | version | MUST 为 0 | RFC 9582 §4.1 |
|
||||||
|
| `as_id` | `int` | asID | 0..4294967295 | RFC 9582 §4(ASID 定义);RFC 9582 §4.2 |
|
||||||
|
| `ip_addr_blocks` | `list[RoaIpAddressFamily]` | ipAddrBlocks | `SIZE(1..2)`;最多 IPv4/IPv6 各一个;建议 canonicalize | RFC 9582 §4;RFC 9582 §4.3.1;RFC 9582 §4.3.3 |
|
||||||
|
|
||||||
|
### 7.4.3 `RoaIpAddressFamily`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `afi` | `enum { ipv4, ipv6 }` | Address Family | MUST 为 IPv4(0001) 或 IPv6(0002) | RFC 9582 §4.3.1 |
|
||||||
|
| `addresses` | `list[RoaIpAddress]` | 前缀列表 | `SIZE(1..MAX)`;每项为前缀 + 可选 maxLength | RFC 9582 §4(ROAAddressesIPv4/IPv6);RFC 9582 §4.3.2 |
|
||||||
|
|
||||||
|
### 7.4.4 `RoaIpAddress`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `prefix` | `IpPrefix` | 前缀 | address 以 BIT STRING 表示前缀(同 RFC 3779 IPAddress 表示) | RFC 9582 §4.3.2.1(引用 RFC 3779 §2.2.3.8) |
|
||||||
|
| `max_length` | `optional[int]` | 最大允许前缀长 | 若存在:必须 `>= prefix_len` 且 `<= 32/128`;并且 `maxLength == prefix_len` 时 **SHOULD NOT** 编码(未来 RP 可能视为编码错误) | RFC 9582 §4.3.2.2 |
|
||||||
|
|
||||||
|
## 7.5 字段级约束清单(实现对照)
|
||||||
|
|
||||||
|
- eContentType 必须为 `id-ct-routeOriginAuthz`(OID `1.2.840.113549.1.9.16.1.24`),且该 OID 必须同时出现在 eContentType 与 signedAttrs.content-type。RFC 9582 §3(引用 RFC 6488)。
|
||||||
|
- eContent 必须 DER 编码并符合 `RouteOriginAttestation` ASN.1。RFC 9582 §4。
|
||||||
|
- `version` 必须为 0。RFC 9582 §4.1。
|
||||||
|
- `ipAddrBlocks` 长度为 1..2;每种 AFI 最多出现一次;仅支持 IPv4/IPv6。RFC 9582 §4;RFC 9582 §4.3.1。
|
||||||
|
- `maxLength` 若存在必须在范围内,且不应出现“等于前缀长”的冗余编码。RFC 9582 §4.3.2.2。
|
||||||
|
- 建议按 canonical form 归一化/排序以利一致处理。RFC 9582 §4.3.3。
|
||||||
|
|
||||||
|
## 7.6 与 EE 证书的语义约束(为后续验证准备)
|
||||||
|
|
||||||
|
ROA 的外壳包含一个 EE 证书,用于验证 ROA 签名;RFC 对该 EE 证书与 ROA payload 的匹配关系提出要求:
|
||||||
|
|
||||||
|
- ROA 的 EE 证书必须是有效的 RPKI EE 证书(路径从 TA 到 EE 可建立),并用于验证 CMS 签名。RFC 9582 §1(引用 RFC 6488);RFC 6488 §3(2)-(3)。
|
||||||
|
- ROA EE 证书中的 IP 资源扩展必须存在且不得使用 inherit。RFC 9582 §5。
|
||||||
|
- ROA EE 证书中 AS 资源扩展不得出现。RFC 9582 §5。
|
||||||
|
- ROA payload 中每个前缀必须包含在 EE 证书的 IP 资源集合内(资源包含语义来自 RFC 3779)。RFC 9582 §5;RFC 3779 §2.3。
|
||||||
100
specs/08_aspa.md
Normal file
100
specs/08_aspa.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# 08. ASPA(Autonomous System Provider Authorization)
|
||||||
|
|
||||||
|
## 8.1 对象定位
|
||||||
|
|
||||||
|
ASPA(Autonomous System Provider Authorization)是一种 RPKI Signed Object,用于由“客户 AS”(Customer AS, CAS)签名声明其上游“提供者 AS”(Provider AS, PAS)集合,以支持路由泄漏(route leak)检测/缓解。`draft-ietf-sidrops-aspa-profile-21`;`draft-ietf-sidrops-aspa-verification`。
|
||||||
|
|
||||||
|
ASPA 由 CMS 外壳 + ASPA eContent 组成:
|
||||||
|
|
||||||
|
- 外壳:RFC 6488(更新:RFC 9589)
|
||||||
|
- eContent:`draft-ietf-sidrops-aspa-profile-21`
|
||||||
|
|
||||||
|
## 8.2 原始载体与编码
|
||||||
|
|
||||||
|
- 外壳:CMS SignedData DER(见 `05_signed_object_cms.md`)。RFC 6488 §2-§3;RFC 9589 §4。
|
||||||
|
- eContentType:`id-ct-ASPA`,OID `1.2.840.113549.1.9.16.1.49`。`draft-ietf-sidrops-aspa-profile-21` §2。
|
||||||
|
- eContent:DER 编码 ASN.1 `ASProviderAttestation`。`draft-ietf-sidrops-aspa-profile-21` §3。
|
||||||
|
|
||||||
|
### 8.2.1 eContentType 与 eContent 的 ASN.1 定义(`draft-ietf-sidrops-aspa-profile-21` §2-§3)
|
||||||
|
|
||||||
|
ASPA 是一种 RPKI signed object(CMS 外壳见 `05_signed_object_cms.md`)。其 `eContentType` 与 `eContent`(payload)的 ASN.1 定义见 `draft-ietf-sidrops-aspa-profile-21` §2-§3。
|
||||||
|
|
||||||
|
**eContentType(OID)**:`draft-ietf-sidrops-aspa-profile-21` §2。
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
id-ct-ASPA OBJECT IDENTIFIER ::=
|
||||||
|
{ iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1)
|
||||||
|
pkcs-9(9) id-smime(16) id-ct(1) aspa(49) }
|
||||||
|
```
|
||||||
|
|
||||||
|
**eContent(ASPA ASN.1 模块)**:`draft-ietf-sidrops-aspa-profile-21` §3。
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
ASProviderAttestation ::= SEQUENCE {
|
||||||
|
version [0] INTEGER DEFAULT 0,
|
||||||
|
customerASID ASID,
|
||||||
|
providers ProviderASSet }
|
||||||
|
|
||||||
|
ProviderASSet ::= SEQUENCE (SIZE(1..MAX)) OF ASID
|
||||||
|
|
||||||
|
ASID ::= INTEGER (0..4294967295)
|
||||||
|
```
|
||||||
|
|
||||||
|
编码/解码要点(与上面 ASN.1 结构直接对应):
|
||||||
|
|
||||||
|
- `version`:规范要求 **MUST 为 1** 且 **MUST 显式编码**(不得依赖 DEFAULT 省略)。`draft-ietf-sidrops-aspa-profile-21` §3.1。
|
||||||
|
- `customerASID`:客户 AS 号(CAS)。`draft-ietf-sidrops-aspa-profile-21` §3.2。
|
||||||
|
- `providers`:授权的提供者 AS 集合(SPAS)。并对“自包含/排序/去重”施加额外约束(见下)。`draft-ietf-sidrops-aspa-profile-21` §3.3。
|
||||||
|
|
||||||
|
## 8.3 解析规则(eContent 语义层)
|
||||||
|
|
||||||
|
输入:`RpkiSignedObject`。
|
||||||
|
|
||||||
|
1) 解析 CMS 外壳,得到 `econtent_type` 与 `econtent_der`。RFC 6488 §3;RFC 9589 §4。
|
||||||
|
2) 要求 `econtent_type == 1.2.840.113549.1.9.16.1.49`。`draft-ietf-sidrops-aspa-profile-21` §2。
|
||||||
|
3) 将 `econtent_der` 以 DER 解析为 `ASProviderAttestation` ASN.1。`draft-ietf-sidrops-aspa-profile-21` §3。
|
||||||
|
4) 将 `providers` 映射为语义字段 `provider_as_ids: list[int]`,并对其执行“约束检查/(可选)归一化”。`draft-ietf-sidrops-aspa-profile-21` §3.3。
|
||||||
|
|
||||||
|
## 8.4 抽象数据模型(接口)
|
||||||
|
|
||||||
|
### 8.4.1 `AspaObject`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | 规范引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `signed_object` | `RpkiSignedObject` | CMS 外壳 | 外壳约束见 RFC 6488/9589 | RFC 6488 §3;RFC 9589 §4 |
|
||||||
|
| `econtent_type` | `Oid` | eContentType | 必须为 `1.2.840.113549.1.9.16.1.49` | `draft-ietf-sidrops-aspa-profile-21` §2 |
|
||||||
|
| `aspa` | `AspaEContent` | eContent 语义对象 | 见下 | `draft-ietf-sidrops-aspa-profile-21` §3 |
|
||||||
|
|
||||||
|
### 8.4.2 `AspaEContent`(ASProviderAttestation)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | 规范引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `version` | `int` | ASPA 版本 | MUST 为 `1` 且 MUST 显式编码(字段不可省略) | `draft-ietf-sidrops-aspa-profile-21` §3.1 |
|
||||||
|
| `customer_as_id` | `int` | Customer ASID | 0..4294967295 | `draft-ietf-sidrops-aspa-profile-21` §3.2(ASID 定义) |
|
||||||
|
| `provider_as_ids` | `list[int]` | Provider ASID 列表(SPAS) | 长度 `>= 1`;且满足“不得包含 customer、升序、去重” | `draft-ietf-sidrops-aspa-profile-21` §3.3 |
|
||||||
|
|
||||||
|
## 8.5 字段级约束清单(实现对照)
|
||||||
|
|
||||||
|
- eContentType 必须为 `id-ct-ASPA`(OID `1.2.840.113549.1.9.16.1.49`),且该 OID 必须同时出现在 eContentType 与 signedAttrs.content-type。`draft-ietf-sidrops-aspa-profile-21` §2(引用 RFC 6488)。
|
||||||
|
- eContent 必须 DER 编码并符合 `ASProviderAttestation` ASN.1。`draft-ietf-sidrops-aspa-profile-21` §3。
|
||||||
|
- `version` 必须为 1,且必须显式编码(缺失视为不合规)。`draft-ietf-sidrops-aspa-profile-21` §3.1。
|
||||||
|
- `providers`(`provider_as_ids`)必须满足:
|
||||||
|
- `customer_as_id` **MUST NOT** 出现在 `provider_as_ids` 中;
|
||||||
|
- `provider_as_ids` 必须按数值 **升序排序**;
|
||||||
|
- `provider_as_ids` 中每个 ASID 必须 **唯一**。`draft-ietf-sidrops-aspa-profile-21` §3.3。
|
||||||
|
|
||||||
|
## 8.6 与 EE 证书的语义约束(为后续验证准备)
|
||||||
|
|
||||||
|
ASPA 的外壳包含一个 EE 证书,用于验证 ASPA 签名;规范对该 EE 证书与 ASPA payload 的匹配关系提出要求:
|
||||||
|
|
||||||
|
- EE 证书必须包含 AS 资源扩展(Autonomous System Identifier Delegation Extension),且 `customer_as_id` 必须与该扩展中的 ASId 匹配。`draft-ietf-sidrops-aspa-profile-21` §4(引用 RFC 3779)。
|
||||||
|
- EE 证书的 AS 资源扩展 **必须**:
|
||||||
|
- 恰好包含 1 个 `id` 元素;
|
||||||
|
- **不得**包含 `inherit` 元素;
|
||||||
|
- **不得**包含 `range` 元素。`draft-ietf-sidrops-aspa-profile-21` §4(引用 RFC 3779 §3.2.3.3 / §3.2.3.6 / §3.2.3.7)。
|
||||||
|
- EE 证书 **不得**包含 IP 资源扩展(IP Address Delegation Extension)。`draft-ietf-sidrops-aspa-profile-21` §4(引用 RFC 3779)。
|
||||||
|
|
||||||
|
## 8.7 实现建议(非规范约束)
|
||||||
|
|
||||||
|
`draft-ietf-sidrops-aspa-profile-21` 给出了一条 RP 侧建议:实现可对单个 `customer_as_id` 的 `provider_as_ids` 数量施加上界(例如 4,000~10,000),超过阈值时建议将该 `customer_as_id` 的所有 ASPA 视为无效并记录错误日志。`draft-ietf-sidrops-aspa-profile-21` §6。
|
||||||
|
|
||||||
87
specs/09_ghostbusters_gbr.md
Normal file
87
specs/09_ghostbusters_gbr.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# 09. Ghostbusters Record(GBR)
|
||||||
|
|
||||||
|
## 9.1 对象定位
|
||||||
|
|
||||||
|
Ghostbusters Record(GBR)是一个可选的 RPKI Signed Object,用于承载“联系人信息”(人类可读的联系渠道),以便在证书过期、CRL 失效、密钥轮换等事件中能够联系到维护者。RFC 6493 §1。
|
||||||
|
|
||||||
|
GBR 由 CMS 外壳 + vCard 载荷组成:
|
||||||
|
|
||||||
|
- 外壳:RFC 6488(更新:RFC 9589)
|
||||||
|
- 载荷(payload):RFC 6493 定义的 vCard profile(基于 RFC 6350 vCard 4.0 的严格子集)。RFC 6493 §5。
|
||||||
|
|
||||||
|
## 9.2 原始载体与编码
|
||||||
|
|
||||||
|
- 外壳:CMS SignedData DER(见 `05_signed_object_cms.md`)。RFC 6493 §6(引用 RFC 6488)。
|
||||||
|
- eContentType:`id-ct-rpkiGhostbusters`,OID `1.2.840.113549.1.9.16.1.35`。RFC 6493 §6;RFC 6493 §9.1。
|
||||||
|
- eContent:一个 OCTET STRING,其 octets 是 vCard 文本(vCard 4.0,且受 RFC 6493 的 profile 约束)。RFC 6493 §5;RFC 6493 §6。
|
||||||
|
|
||||||
|
> 说明:与 ROA/MFT 这类“eContent 内部再 DER 解码为 ASN.1 结构”的对象不同,GBR 的 eContent 语义上就是“vCard 文本内容本身”(由 Signed Object Template 的 `eContent OCTET STRING` 承载)。RFC 6493 §6。
|
||||||
|
|
||||||
|
## 9.3 vCard profile(RFC 6493 §5)
|
||||||
|
|
||||||
|
GBR 的 vCard payload 是 RFC 6350 vCard 4.0 的严格子集,仅允许以下属性(properties):
|
||||||
|
|
||||||
|
- `BEGIN`:必须为第一行,值必须为 `BEGIN:VCARD`。RFC 6493 §5。
|
||||||
|
- `VERSION`:必须为第二行,值必须为 `VERSION:4.0`。RFC 6493 §5(引用 RFC 6350 §3.7.9)。
|
||||||
|
- `FN`:联系人姓名或角色名。RFC 6493 §5(引用 RFC 6350 §6.2.1)。
|
||||||
|
- `ORG`:组织信息(可选)。RFC 6493 §5(引用 RFC 6350 §6.6.4)。
|
||||||
|
- `ADR`:邮寄地址(可选)。RFC 6493 §5(引用 RFC 6350 §6.3)。
|
||||||
|
- `TEL`:语音/传真电话(可选)。RFC 6493 §5(引用 RFC 6350 §6.4.1)。
|
||||||
|
- `EMAIL`:邮箱(可选)。RFC 6493 §5(引用 RFC 6350 §6.4.2)。
|
||||||
|
- `END`:必须为最后一行,值必须为 `END:VCARD`。RFC 6493 §5。
|
||||||
|
|
||||||
|
额外约束:
|
||||||
|
|
||||||
|
- `BEGIN`、`VERSION`、`FN`、`END` 必须包含。RFC 6493 §5。
|
||||||
|
- 为保证可用性,`ADR`/`TEL`/`EMAIL` 三者中至少一个必须包含。RFC 6493 §5。
|
||||||
|
- 除上述属性外,**其他属性 MUST NOT** 出现。RFC 6493 §5。
|
||||||
|
|
||||||
|
## 9.4 解析规则(payload 语义层)
|
||||||
|
|
||||||
|
输入:`RpkiSignedObject`。
|
||||||
|
|
||||||
|
1) 解析 CMS 外壳,得到 `econtent_type` 与 `econtent_bytes`。RFC 6488 §3;RFC 9589 §4。
|
||||||
|
2) 要求 `econtent_type == 1.2.840.113549.1.9.16.1.35`。RFC 6493 §6。
|
||||||
|
3) 将 `econtent_bytes` 解析为 vCard 文本,并按 RFC 6493 §5 的 profile 校验(属性集合、必选项、行首/行尾约束)。RFC 6493 §5;RFC 6493 §7。
|
||||||
|
4) 通过校验后,将允许属性映射为 `GhostbustersVCard` 语义对象(见下)。
|
||||||
|
|
||||||
|
## 9.5 抽象数据模型(接口)
|
||||||
|
|
||||||
|
### 9.5.1 `GhostbustersObject`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `signed_object` | `RpkiSignedObject` | CMS 外壳 | 外壳约束见 RFC 6488/9589 | RFC 6493 §6;RFC 6488 §3;RFC 9589 §4 |
|
||||||
|
| `econtent_type` | `Oid` | eContentType | 必须为 `1.2.840.113549.1.9.16.1.35` | RFC 6493 §6 |
|
||||||
|
| `vcard` | `GhostbustersVCard` | vCard 语义对象 | 由 eContent 文本解析并校验 profile | RFC 6493 §5;RFC 6493 §7 |
|
||||||
|
|
||||||
|
### 9.5.2 `GhostbustersVCard`(vCard 4.0 profile)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `raw_text` | `string` | 原始 vCard 文本 | 由 eContent bytes 解码得到;用于保留原文/诊断 | RFC 6493 §5-§7 |
|
||||||
|
| `fn` | `string` | 联系人姓名/角色名 | `FN` 必须存在 | RFC 6493 §5 |
|
||||||
|
| `org` | `optional[string]` | 组织 | `ORG` 可选 | RFC 6493 §5 |
|
||||||
|
| `adrs` | `list[string]` | 邮寄地址(原始 ADR value) | 允许 0..N;至少满足“ADR/TEL/EMAIL 至少一项存在” | RFC 6493 §5 |
|
||||||
|
| `tels` | `list[Uri]` | 电话 URI(从 TEL 提取的 `tel:` 等 URI) | 允许 0..N;至少满足“ADR/TEL/EMAIL 至少一项存在” | RFC 6493 §5 |
|
||||||
|
| `emails` | `list[string]` | 邮箱地址 | 允许 0..N;至少满足“ADR/TEL/EMAIL 至少一项存在” | RFC 6493 §5 |
|
||||||
|
|
||||||
|
> 说明:RFC 6493 并未要求 RP 必须完整解析 vCard 参数(例如 `TYPE=WORK`、`VALUE=uri`),因此此处将 `ADR`/`TEL`/`EMAIL` 建模为“足以联络”的最小语义集合;实现可在保留 `raw_text` 的同时按 RFC 6350 扩展解析能力。
|
||||||
|
|
||||||
|
## 9.6 字段级约束清单(实现对照)
|
||||||
|
|
||||||
|
- eContentType 必须为 `id-ct-rpkiGhostbusters`(OID `1.2.840.113549.1.9.16.1.35`),且该 OID 必须同时出现在 eContentType 与 signedAttrs.content-type。RFC 6493 §6(引用 RFC 6488)。
|
||||||
|
- eContent 必须是 vCard 4.0 文本,且必须满足 RFC 6493 §5 的 profile:
|
||||||
|
- 第一行 `BEGIN:VCARD`;
|
||||||
|
- 第二行 `VERSION:4.0`;
|
||||||
|
- 末行 `END:VCARD`;
|
||||||
|
- 必须包含 `FN`;
|
||||||
|
- `ADR`/`TEL`/`EMAIL` 至少一个存在;
|
||||||
|
- 除允许集合外不得出现其他属性。RFC 6493 §5;RFC 6493 §7。
|
||||||
|
|
||||||
|
## 9.7 与 EE 证书的语义约束(为后续验证准备)
|
||||||
|
|
||||||
|
GBR 使用 CMS 外壳内的 EE 证书验证签名。RFC 6493 对该 EE 证书提出一条资源扩展约束:
|
||||||
|
|
||||||
|
- 用于验证 GBR 的 EE 证书在描述 Internet Number Resources 时,必须使用 `inherit`,而不是显式资源集合。RFC 6493 §6(引用 RFC 3779)。
|
||||||
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
# 10. SLURM(Simplified Local Internet Number Resource Management with the RPKI)
|
|
||||||
|
|
||||||
## 10.1 目标与范围
|
|
||||||
|
|
||||||
SLURM 用于让 RP(Relying Party)在本地对上游 RPKI 验证结果做“过滤”和“补充断言”,而不修改上游发布对象。
|
|
||||||
|
|
||||||
本文档基于:
|
|
||||||
- RFC 8416(SLURM v1,ROA/BGPsec)
|
|
||||||
- draft-ietf-sidrops-aspa-slurm-04(SLURM v2,新增 ASPA)
|
|
||||||
|
|
||||||
## 10.2 版本与顶层结构
|
|
||||||
|
|
||||||
### 10.2.1 SLURM v1(RFC 8416)
|
|
||||||
|
|
||||||
`slurmVersion` 必须为 `1`,且顶层 JSON 对象必须包含且仅包含以下成员:
|
|
||||||
- `slurmVersion`
|
|
||||||
- `validationOutputFilters`(必须包含 `prefixFilters`、`bgpsecFilters`)
|
|
||||||
- `locallyAddedAssertions`(必须包含 `prefixAssertions`、`bgpsecAssertions`)
|
|
||||||
|
|
||||||
空策略示例:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"slurmVersion": 1,
|
|
||||||
"validationOutputFilters": {
|
|
||||||
"prefixFilters": [],
|
|
||||||
"bgpsecFilters": []
|
|
||||||
},
|
|
||||||
"locallyAddedAssertions": {
|
|
||||||
"prefixAssertions": [],
|
|
||||||
"bgpsecAssertions": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 10.2.2 SLURM v2(draft-04)
|
|
||||||
|
|
||||||
`slurmVersion` 必须为 `2`,在 v1 基础上扩展 ASPA 两类成员:
|
|
||||||
- `validationOutputFilters.aspaFilters`
|
|
||||||
- `locallyAddedAssertions.aspaAssertions`
|
|
||||||
|
|
||||||
空策略示例:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"slurmVersion": 2,
|
|
||||||
"validationOutputFilters": {
|
|
||||||
"prefixFilters": [],
|
|
||||||
"bgpsecFilters": [],
|
|
||||||
"aspaFilters": []
|
|
||||||
},
|
|
||||||
"locallyAddedAssertions": {
|
|
||||||
"prefixAssertions": [],
|
|
||||||
"bgpsecAssertions": [],
|
|
||||||
"aspaAssertions": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 10.3 字段规范(RFC 8416)
|
|
||||||
|
|
||||||
### 10.3.1 `prefixFilters`
|
|
||||||
|
|
||||||
数组元素每项:
|
|
||||||
- 必须至少包含一个:`prefix` 或 `asn`
|
|
||||||
- 可选:`comment`
|
|
||||||
|
|
||||||
匹配规则:
|
|
||||||
- 若配置了 `prefix`:匹配“被该前缀覆盖(encompassed)”的 VRP 前缀
|
|
||||||
- 若配置了 `asn`:匹配该 ASN
|
|
||||||
- 同时配置时:两者都要匹配
|
|
||||||
|
|
||||||
### 10.3.2 `bgpsecFilters`
|
|
||||||
|
|
||||||
数组元素每项:
|
|
||||||
- 必须至少包含一个:`asn` 或 `SKI`
|
|
||||||
- 可选:`comment`
|
|
||||||
|
|
||||||
匹配规则:
|
|
||||||
- 按 `asn`/`SKI` 单独或联合匹配 Router Key(BGPsec)
|
|
||||||
|
|
||||||
### 10.3.3 `prefixAssertions`
|
|
||||||
|
|
||||||
数组元素每项:
|
|
||||||
- 必须:`prefix`、`asn`
|
|
||||||
- 可选:`maxPrefixLength`、`comment`
|
|
||||||
|
|
||||||
约束:
|
|
||||||
- 若给出 `maxPrefixLength`,应满足 `prefix 长度 <= maxPrefixLength <= 地址位宽(IPv4=32, IPv6=128)`
|
|
||||||
|
|
||||||
### 10.3.4 `bgpsecAssertions`
|
|
||||||
|
|
||||||
数组元素每项:
|
|
||||||
- 必须:`asn`、`SKI`、`routerPublicKey`
|
|
||||||
- 可选:`comment`
|
|
||||||
|
|
||||||
## 10.4 ASPA 扩展(draft-ietf-sidrops-aspa-slurm-04)
|
|
||||||
|
|
||||||
### 10.4.1 `aspaFilters`
|
|
||||||
|
|
||||||
数组元素每项:
|
|
||||||
- 必须:`customerAsn`
|
|
||||||
- 可选:`comment`
|
|
||||||
|
|
||||||
匹配规则:
|
|
||||||
- 当 VAP(Validated ASPA Payload)的 `customerAsn` 等于过滤器 `customerAsn` 时命中并移除。
|
|
||||||
|
|
||||||
### 10.4.2 `aspaAssertions`
|
|
||||||
|
|
||||||
数组元素每项:
|
|
||||||
- 必须:`customerAsn`
|
|
||||||
- 必须:`providerAsns`(ASN 数组)
|
|
||||||
- 可选:`comment`
|
|
||||||
|
|
||||||
关键约束(draft-04):
|
|
||||||
- `customerAsn` 不得出现在 `providerAsns` 中
|
|
||||||
- `providerAsns` 必须按升序排列
|
|
||||||
- `providerAsns` 里的 ASN 必须唯一(无重复)
|
|
||||||
|
|
||||||
语义补充(draft-04):
|
|
||||||
- `aspaAssertions` 仅用于“新增断言”,不构成隐式过滤(不会自动替代 `aspaFilters`)。
|
|
||||||
- 在 RTRv2 输出阶段,新增的 ASPA 断言应加入 ASPA PDU 集合,并做去重。
|
|
||||||
|
|
||||||
## 10.5 应用语义(RFC 8416 Section 4)
|
|
||||||
|
|
||||||
### 10.5.1 原子性
|
|
||||||
|
|
||||||
SLURM 应用必须是原子的:
|
|
||||||
- 要么完全不生效(等同未使用 SLURM)
|
|
||||||
- 要么完整按当前 SLURM 配置生效
|
|
||||||
|
|
||||||
### 10.5.2 处理顺序
|
|
||||||
|
|
||||||
在同一次计算中:
|
|
||||||
1. 先执行 `validationOutputFilters`(移除匹配验证结果)
|
|
||||||
2. 再追加 `locallyAddedAssertions`
|
|
||||||
|
|
||||||
### 10.5.3 多文件
|
|
||||||
|
|
||||||
实现可以支持多个 SLURM 文件并行使用(取并集),但在启用前应检查断言重叠冲突;若存在冲突,整组文件应被拒绝。
|
|
||||||
|
|
||||||
## 10.6 最小可用示例(SLURM v2)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"slurmVersion": 2,
|
|
||||||
"validationOutputFilters": {
|
|
||||||
"prefixFilters": [
|
|
||||||
{
|
|
||||||
"prefix": "203.0.113.0/24",
|
|
||||||
"comment": "Filter a broken VRP from upstream"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"bgpsecFilters": [],
|
|
||||||
"aspaFilters": [
|
|
||||||
{
|
|
||||||
"customerAsn": 64496,
|
|
||||||
"comment": "Filter one customer ASPA"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locallyAddedAssertions": {
|
|
||||||
"prefixAssertions": [
|
|
||||||
{
|
|
||||||
"asn": 64496,
|
|
||||||
"prefix": "203.0.113.0/24",
|
|
||||||
"maxPrefixLength": 24,
|
|
||||||
"comment": "Local business exception"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"bgpsecAssertions": [],
|
|
||||||
"aspaAssertions": [
|
|
||||||
{
|
|
||||||
"customerAsn": 64496,
|
|
||||||
"providerAsns": [64497, 64498],
|
|
||||||
"comment": "Local ASPA assertion"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 10.7 参考文献
|
|
||||||
|
|
||||||
- RFC 8416: https://www.rfc-editor.org/rfc/rfc8416.html
|
|
||||||
- draft-ietf-sidrops-aspa-slurm-04: https://www.ietf.org/archive/id/draft-ietf-sidrops-aspa-slurm-04.html
|
|
||||||
179
specs/11_rtr.md
179
specs/11_rtr.md
@@ -1,179 +0,0 @@
|
|||||||
# 11. RTR(RPKI to Router Protocol)
|
|
||||||
|
|
||||||
## 11.1 目标与文档范围
|
|
||||||
|
|
||||||
RTR 用于把 RP/Cache 已完成密码学验证的 RPKI 数据下发给路由器。
|
|
||||||
|
|
||||||
本文按以下规范整理:
|
|
||||||
- RFC 6810(RTR v0)
|
|
||||||
- RFC 8210(RTR v1,更新 RFC 6810)
|
|
||||||
- draft-ietf-sidrops-8210bis-26(RTR v2,草案)
|
|
||||||
|
|
||||||
## 11.2 协议演进
|
|
||||||
|
|
||||||
### 11.2.1 RFC 6810(v0)
|
|
||||||
|
|
||||||
- 只定义 Prefix Origin 相关 payload(IPv4/IPv6 Prefix PDU)。
|
|
||||||
- 主要 PDU:Serial Notify / Serial Query / Reset Query / Cache Response / Prefix / End of Data / Cache Reset / Error Report。
|
|
||||||
|
|
||||||
### 11.2.2 RFC 8210(v1)
|
|
||||||
|
|
||||||
在 v0 基础上新增/强化:
|
|
||||||
- 新增 `Router Key PDU`(PDU Type 9,v1 可用,v0 保留)。
|
|
||||||
- 强化协议版本协商与降级行为。
|
|
||||||
- `End of Data` 在 v1 中携带 `Refresh/Retry/Expire` 三个计时参数。
|
|
||||||
|
|
||||||
### 11.2.3 Version 2(草案)
|
|
||||||
|
|
||||||
在 v1 基础上新增/强化:
|
|
||||||
- 新增 `ASPA PDU`(PDU Type 11,仅 v2)。
|
|
||||||
- 新增 “Races, Ordering, and Transactions” 章节,要求缓存按规定顺序输出 payload 以降低路由器短暂误判。
|
|
||||||
- 协议版本提升到 `2`。
|
|
||||||
- 明确 PDU 最大长度上限为 64k(65535)。
|
|
||||||
|
|
||||||
## 11.3 PDU 与版本矩阵
|
|
||||||
|
|
||||||
PDU 类型(按规范注册表):
|
|
||||||
|
|
||||||
| PDU Type | 名称 | v0 (RFC6810) | v1 (RFC8210) | v2 (8210bis-26) |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| 0 | Serial Notify | 支持 | 支持 | 支持 |
|
|
||||||
| 1 | Serial Query | 支持 | 支持 | 支持 |
|
|
||||||
| 2 | Reset Query | 支持 | 支持 | 支持 |
|
|
||||||
| 3 | Cache Response | 支持 | 支持 | 支持 |
|
|
||||||
| 4 | IPv4 Prefix | 支持 | 支持 | 支持 |
|
|
||||||
| 6 | IPv6 Prefix | 支持 | 支持 | 支持 |
|
|
||||||
| 7 | End of Data | 支持 | 支持(含计时参数) | 支持 |
|
|
||||||
| 8 | Cache Reset | 支持 | 支持 | 支持 |
|
|
||||||
| 9 | Router Key | 保留 | 支持 | 支持 |
|
|
||||||
| 10 | Error Report | 支持 | 支持 | 支持 |
|
|
||||||
| 11 | ASPA | 保留 | 保留 | 支持 |
|
|
||||||
|
|
||||||
通用字段约束:
|
|
||||||
- `Protocol Version`:8-bit。
|
|
||||||
- `PDU Type`:8-bit。
|
|
||||||
- `Session ID`:16-bit。
|
|
||||||
- `Length`:32-bit。
|
|
||||||
- 保留位(zero/reserved)发送必须为 0,接收时按规范处理。
|
|
||||||
|
|
||||||
## 11.4 关键 PDU 语义
|
|
||||||
|
|
||||||
### 11.4.1 Serial Notify(Type 0)
|
|
||||||
|
|
||||||
- 由 Cache 主动发送,提示有新序列可拉取。
|
|
||||||
- 是少数可不由 Router 请求触发的消息。
|
|
||||||
|
|
||||||
### 11.4.2 Reset Query(Type 2)与 Cache Response(Type 3)
|
|
||||||
|
|
||||||
- Router 启动或失配时发 `Reset Query` 请求全量。
|
|
||||||
- Cache 回复 `Cache Response`,随后发送全量 payload,最后 `End of Data`。
|
|
||||||
|
|
||||||
### 11.4.3 Serial Query(Type 1)
|
|
||||||
|
|
||||||
- Router 持有上次 `Session ID + Serial` 时请求增量。
|
|
||||||
- Cache 若可提供增量:返回变化集。
|
|
||||||
- Cache 若无法从该 serial 补增量:返回 `Cache Reset`,要求 Router 走全量。
|
|
||||||
|
|
||||||
### 11.4.4 Prefix / Router Key / ASPA payload
|
|
||||||
|
|
||||||
- `IPv4 Prefix`(Type 4)/ `IPv6 Prefix`(Type 6):表示 VRP 的 announce/withdraw。
|
|
||||||
- `Router Key`(Type 9,v1+):表示 BGPsec Router Key 的 announce/withdraw。
|
|
||||||
- `ASPA`(Type 11,v2 草案):表示 ASPA 数据单元的 announce/withdraw。
|
|
||||||
|
|
||||||
语义要点(v1 / v2 草案):
|
|
||||||
- 对同一 payload 键(如 Prefix 四元组、Router Key 三元组、ASPA customer 键)应维护清晰的替换/撤销关系。
|
|
||||||
- Cache 负责把历史变化“合并简化”后再发给 Router,避免无意义抖动。
|
|
||||||
|
|
||||||
### 11.4.5 End of Data(Type 7)
|
|
||||||
|
|
||||||
- 标识一次响应结束,并给出当前 serial。
|
|
||||||
- v0:不含定时器字段。
|
|
||||||
- v1/v2:携带 `Refresh Interval`、`Retry Interval`、`Expire Interval`。
|
|
||||||
|
|
||||||
## 11.5 协议时序
|
|
||||||
|
|
||||||
### 11.5.1 初始同步(Full Sync)
|
|
||||||
|
|
||||||
1. Router 建连后发 `Reset Query`(带支持的协议版本)。
|
|
||||||
2. Cache 回 `Cache Response`。
|
|
||||||
3. Cache 按规范发送 payload 集合。
|
|
||||||
4. Cache 发 `End of Data` 收尾。
|
|
||||||
|
|
||||||
### 11.5.2 增量同步(Incremental Sync)
|
|
||||||
|
|
||||||
1. Router 发 `Serial Query(session_id, serial)`。
|
|
||||||
2. Cache 若可增量,返回变化并以 `End of Data` 收尾。
|
|
||||||
3. 若不可增量,返回 `Cache Reset`;Router 退回 Full Sync。
|
|
||||||
|
|
||||||
### 11.5.3 Cache Restart / Cache Shutdown(v2)
|
|
||||||
|
|
||||||
- `Cache Restart`:Cache 即将对已连接客户端不可用,但预计会在客户端达到 Expire Interval 前恢复;v2 session 应发送 `Error Report` code 12,随后关闭连接。
|
|
||||||
- `Cache Shutdown`:Cache 即将有意下线,并希望客户端 flush 从该 cache 学到的数据;v2 session 应发送 `Error Report` code 13,随后关闭连接。
|
|
||||||
- 这两个错误码只属于 v2。v0/v1 session 不得发送 code 12/13,只能关闭连接。
|
|
||||||
|
|
||||||
## 11.6 版本协商与降级
|
|
||||||
|
|
||||||
- Router 每次新连接必须由 `Reset Query` 或 `Serial Query` 启动,携带其协议版本。
|
|
||||||
- 双方在协商完成后,本连接内版本固定。
|
|
||||||
- 遇到不支持版本时,可按规范降级(例如 v1 对 v0、v2 对 v1/v0)或返回 `Unsupported Protocol Version` 后断开。
|
|
||||||
- 协商期若收到 `Serial Notify`,Router 应按规范兼容处理(通常忽略,待协商完成)。
|
|
||||||
|
|
||||||
## 11.7 计时器与失效(v1/v2)
|
|
||||||
|
|
||||||
`End of Data` 下发三个参数:
|
|
||||||
- `Refresh Interval`:多久后主动刷新。
|
|
||||||
- `Retry Interval`:失败后重试间隔。
|
|
||||||
- `Expire Interval`:本地数据最长可保留时长。
|
|
||||||
|
|
||||||
规范边界(RFC 8210):
|
|
||||||
- Refresh: 1 .. 86400(推荐 3600)
|
|
||||||
- Retry: 1 .. 7200(推荐 600)
|
|
||||||
- Expire: 600 .. 172800(推荐 7200)
|
|
||||||
- 且 `Expire` 必须大于 `Refresh` 和 `Retry`。
|
|
||||||
|
|
||||||
## 11.8 Version 2(草案)新增关注点
|
|
||||||
|
|
||||||
### 11.8.1 ASPA PDU
|
|
||||||
|
|
||||||
- 新增 ASPA 传输能力(Type 11)。
|
|
||||||
- 针对同一 customer ASN,Cache 需向 Router 提供一致且可替换的 ASPA 视图。
|
|
||||||
|
|
||||||
### 11.8.2 排序与事务
|
|
||||||
|
|
||||||
- 草案新增 race 条件说明(如前缀替换、撤销先后导致短暂误判)。
|
|
||||||
- 对 Cache 输出 payload 的顺序提出约束。
|
|
||||||
- 建议 Router 使用“事务式应用”(例如接收到完整响应后再切换生效)降低中间态影响。
|
|
||||||
|
|
||||||
### 11.8.3 Error Code 版本范围
|
|
||||||
|
|
||||||
- v0(RFC6810):0..7。
|
|
||||||
- v1(RFC8210):继承 v0,并新增 8 `Unexpected Protocol Version`。
|
|
||||||
- v2(8210bis-26):继承 v1,并新增 9 `ASPA Provider List Error`、10 `Transport Failure`、11 `Ordering Error`、12 `Cache Restart`、13 `Cache Shutdown`。
|
|
||||||
- 实现发送 Error Report 时,只能发送当前 session 协议版本定义过的错误码。
|
|
||||||
|
|
||||||
### 11.8.4 本实现的生命周期控制
|
|
||||||
|
|
||||||
- `RPKI_RTR_STOP_REASON=cache-restart|cache-shutdown|close-only` 控制进程收到 Ctrl-C 后的服务停止语义,默认 `cache-restart`。
|
|
||||||
- Admin API:
|
|
||||||
- `POST /admin/rtr/lifecycle/restart`:向 v2 established clients 发送 `Cache Restart`,然后走进程退出路径;实际重新拉起由 systemd/Docker/Kubernetes/Windows Service 等 supervisor 完成。若没有外部机制在客户端 `Expire Interval` 到期前恢复服务,不应使用该接口。
|
|
||||||
- `POST /admin/rtr/lifecycle/shutdown`:向 v2 established clients 发送 `Cache Shutdown`,然后退出进程。
|
|
||||||
- v0/v1 或尚未完成版本协商的连接不会收到 code 12/13。
|
|
||||||
|
|
||||||
## 11.9 传输与安全
|
|
||||||
|
|
||||||
规范定义可承载于多种传输:
|
|
||||||
- SSH
|
|
||||||
- TLS
|
|
||||||
- TCP MD5
|
|
||||||
- TCP-AO
|
|
||||||
|
|
||||||
安全原则:
|
|
||||||
- Router 与 Cache 之间必须建立可信关系。
|
|
||||||
- 需要完整性/机密性时优先使用具备认证与加密能力的传输。
|
|
||||||
- 若使用普通 TCP,部署上应限制在可信受控网络中。
|
|
||||||
|
|
||||||
## 11.10 参考文献
|
|
||||||
|
|
||||||
- RFC 6810: https://www.rfc-editor.org/rfc/rfc6810.html
|
|
||||||
- RFC 8210: https://www.rfc-editor.org/rfc/rfc8210.html
|
|
||||||
- draft-ietf-sidrops-8210bis-26: https://www.ietf.org/archive/id/draft-ietf-sidrops-8210bis-26.html
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,290 +0,0 @@
|
|||||||
use std::fs;
|
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
|
|
||||||
const CONTENT_TYPE_OID: &str = "1.2.840.113549.1.9.16.1.54";
|
|
||||||
const SHA256_OID: &str = "2.16.840.1.101.3.4.2.1";
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct Vrp {
|
|
||||||
addr: IpAddr,
|
|
||||||
prefix_len: u8,
|
|
||||||
max_len: u8,
|
|
||||||
asn: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct Vap {
|
|
||||||
customer_asn: u32,
|
|
||||||
providers: Vec<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
let out_dir = parse_out_dir_arg();
|
|
||||||
fs::create_dir_all(&out_dir)
|
|
||||||
.with_context(|| format!("failed to create output dir {}", out_dir.display()))?;
|
|
||||||
|
|
||||||
write_snapshot(
|
|
||||||
&out_dir.join("20260403T000001Z-mini-a.ccr"),
|
|
||||||
"20260403000001Z",
|
|
||||||
vec![
|
|
||||||
Vrp {
|
|
||||||
addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 0)),
|
|
||||||
prefix_len: 24,
|
|
||||||
max_len: 24,
|
|
||||||
asn: 65001,
|
|
||||||
},
|
|
||||||
Vrp {
|
|
||||||
addr: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 1, 0, 0, 0, 0, 0)),
|
|
||||||
prefix_len: 48,
|
|
||||||
max_len: 48,
|
|
||||||
asn: 65002,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
vec![Vap {
|
|
||||||
customer_asn: 65010,
|
|
||||||
providers: vec![65011, 65012],
|
|
||||||
}],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
write_snapshot(
|
|
||||||
&out_dir.join("20260403T000101Z-mini-b.ccr"),
|
|
||||||
"20260403000101Z",
|
|
||||||
vec![
|
|
||||||
Vrp {
|
|
||||||
addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 0)),
|
|
||||||
prefix_len: 24,
|
|
||||||
max_len: 24,
|
|
||||||
asn: 65001,
|
|
||||||
},
|
|
||||||
Vrp {
|
|
||||||
addr: IpAddr::V4(Ipv4Addr::new(10, 0, 1, 0)),
|
|
||||||
prefix_len: 24,
|
|
||||||
max_len: 24,
|
|
||||||
asn: 65003,
|
|
||||||
},
|
|
||||||
Vrp {
|
|
||||||
addr: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 1, 0, 0, 0, 0, 0)),
|
|
||||||
prefix_len: 48,
|
|
||||||
max_len: 48,
|
|
||||||
asn: 65002,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
vec![Vap {
|
|
||||||
customer_asn: 65010,
|
|
||||||
providers: vec![65011, 65012, 65013],
|
|
||||||
}],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
write_snapshot(
|
|
||||||
&out_dir.join("20260403T000201Z-mini-c.ccr"),
|
|
||||||
"20260403000201Z",
|
|
||||||
vec![
|
|
||||||
Vrp {
|
|
||||||
addr: IpAddr::V4(Ipv4Addr::new(10, 0, 1, 0)),
|
|
||||||
prefix_len: 24,
|
|
||||||
max_len: 24,
|
|
||||||
asn: 65003,
|
|
||||||
},
|
|
||||||
Vrp {
|
|
||||||
addr: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 2, 0, 0, 0, 0, 0)),
|
|
||||||
prefix_len: 48,
|
|
||||||
max_len: 48,
|
|
||||||
asn: 65004,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
vec![
|
|
||||||
Vap {
|
|
||||||
customer_asn: 65010,
|
|
||||||
providers: vec![65012, 65013],
|
|
||||||
},
|
|
||||||
Vap {
|
|
||||||
customer_asn: 65020,
|
|
||||||
providers: vec![65021],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
println!("generated CCR fixtures under {}", out_dir.display());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_out_dir_arg() -> PathBuf {
|
|
||||||
let mut args = std::env::args().skip(1);
|
|
||||||
let mut out_dir = PathBuf::from("data");
|
|
||||||
while let Some(arg) = args.next() {
|
|
||||||
if arg == "--out-dir" {
|
|
||||||
if let Some(v) = args.next() {
|
|
||||||
out_dir = PathBuf::from(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out_dir
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_snapshot(path: &Path, produced_at: &str, vrps: Vec<Vrp>, vaps: Vec<Vap>) -> Result<()> {
|
|
||||||
let bytes = encode_ccr_snapshot(produced_at, vrps, vaps);
|
|
||||||
fs::write(path, bytes).with_context(|| format!("failed to write {}", path.display()))?;
|
|
||||||
println!("wrote {}", path.display());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encode_ccr_snapshot(produced_at: &str, vrps: Vec<Vrp>, vaps: Vec<Vap>) -> Vec<u8> {
|
|
||||||
let vrp_sets = vrps
|
|
||||||
.into_iter()
|
|
||||||
.map(encode_roa_payload_set)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let vap_sets = vaps
|
|
||||||
.into_iter()
|
|
||||||
.map(encode_aspa_payload_set)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let vrp_set_seq = der_sequence(vrp_sets);
|
|
||||||
let vap_set_seq = der_sequence(vap_sets);
|
|
||||||
|
|
||||||
let vrp_hash = Sha256::digest(&vrp_set_seq).to_vec();
|
|
||||||
let vap_hash = Sha256::digest(&vap_set_seq).to_vec();
|
|
||||||
|
|
||||||
// draft-ietf-sidrops-rpki-ccr-02:
|
|
||||||
// ROAPayloadState/ASPAPayloadState include payload-set sequence + hash.
|
|
||||||
let vrp_state = der_sequence(vec![vrp_set_seq, der_octet_string(vrp_hash)]);
|
|
||||||
let vap_state = der_sequence(vec![vap_set_seq, der_octet_string(vap_hash)]);
|
|
||||||
|
|
||||||
// AlgorithmIdentifier for SHA-256.
|
|
||||||
let hash_alg = der_sequence(vec![der_oid(SHA256_OID), der_null()]);
|
|
||||||
|
|
||||||
let payload = der_sequence(vec![
|
|
||||||
der_integer(0),
|
|
||||||
hash_alg,
|
|
||||||
der_generalized_time(produced_at),
|
|
||||||
der_ctx(2, vrp_state),
|
|
||||||
der_ctx(3, vap_state),
|
|
||||||
]);
|
|
||||||
|
|
||||||
der_sequence(vec![der_oid(CONTENT_TYPE_OID), der_ctx(0, payload)])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encode_roa_payload_set(v: Vrp) -> Vec<u8> {
|
|
||||||
let (afi, addr_bytes) = match v.addr {
|
|
||||||
IpAddr::V4(ip) => ([0u8, 1u8].to_vec(), ip.octets().to_vec()),
|
|
||||||
IpAddr::V6(ip) => ([0u8, 2u8].to_vec(), ip.octets().to_vec()),
|
|
||||||
};
|
|
||||||
let bit_string = prefix_to_bit_string(&addr_bytes, v.prefix_len);
|
|
||||||
let roa_ip = der_sequence(vec![
|
|
||||||
der_bit_string(0, bit_string),
|
|
||||||
der_integer(u32::from(v.max_len)),
|
|
||||||
]);
|
|
||||||
let family = der_sequence(vec![der_octet_string(afi), der_sequence(vec![roa_ip])]);
|
|
||||||
der_sequence(vec![der_integer(v.asn), der_sequence(vec![family])])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encode_aspa_payload_set(v: Vap) -> Vec<u8> {
|
|
||||||
let providers = v.providers.into_iter().map(der_integer).collect::<Vec<_>>();
|
|
||||||
der_sequence(vec![der_integer(v.customer_asn), der_sequence(providers)])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prefix_to_bit_string(addr: &[u8], prefix_len: u8) -> Vec<u8> {
|
|
||||||
let byte_len = usize::from(prefix_len).div_ceil(8);
|
|
||||||
let mut out = addr[..byte_len].to_vec();
|
|
||||||
let rem = prefix_len % 8;
|
|
||||||
if rem != 0 {
|
|
||||||
let mask = 0xFFu8 << (8 - rem);
|
|
||||||
let last = out.len() - 1;
|
|
||||||
out[last] &= mask;
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
fn der_sequence(items: Vec<Vec<u8>>) -> Vec<u8> {
|
|
||||||
let content = items.concat();
|
|
||||||
der_tlv(0x30, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn der_integer(v: u32) -> Vec<u8> {
|
|
||||||
if v == 0 {
|
|
||||||
return der_tlv(0x02, vec![0]);
|
|
||||||
}
|
|
||||||
let mut bytes = v.to_be_bytes().to_vec();
|
|
||||||
while bytes.len() > 1 && bytes[0] == 0 {
|
|
||||||
bytes.remove(0);
|
|
||||||
}
|
|
||||||
if bytes[0] & 0x80 != 0 {
|
|
||||||
bytes.insert(0, 0);
|
|
||||||
}
|
|
||||||
der_tlv(0x02, bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn der_oid(oid: &str) -> Vec<u8> {
|
|
||||||
let parts = oid
|
|
||||||
.split('.')
|
|
||||||
.map(|s| s.parse::<u32>().unwrap())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
assert!(parts.len() >= 2);
|
|
||||||
let mut out = Vec::new();
|
|
||||||
out.push((parts[0] * 40 + parts[1]) as u8);
|
|
||||||
for &part in &parts[2..] {
|
|
||||||
out.extend(base128(part));
|
|
||||||
}
|
|
||||||
der_tlv(0x06, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn base128(mut n: u32) -> Vec<u8> {
|
|
||||||
let mut buf = vec![(n & 0x7F) as u8];
|
|
||||||
n >>= 7;
|
|
||||||
while n > 0 {
|
|
||||||
buf.push(((n & 0x7F) as u8) | 0x80);
|
|
||||||
n >>= 7;
|
|
||||||
}
|
|
||||||
buf.reverse();
|
|
||||||
buf
|
|
||||||
}
|
|
||||||
|
|
||||||
fn der_octet_string(bytes: Vec<u8>) -> Vec<u8> {
|
|
||||||
der_tlv(0x04, bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn der_null() -> Vec<u8> {
|
|
||||||
der_tlv(0x05, Vec::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn der_bit_string(unused_bits: u8, bytes: Vec<u8>) -> Vec<u8> {
|
|
||||||
let mut content = Vec::with_capacity(1 + bytes.len());
|
|
||||||
content.push(unused_bits);
|
|
||||||
content.extend(bytes);
|
|
||||||
der_tlv(0x03, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn der_generalized_time(v: &str) -> Vec<u8> {
|
|
||||||
der_tlv(0x18, v.as_bytes().to_vec())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn der_ctx(tag_no: u8, encoded_inner_der: Vec<u8>) -> Vec<u8> {
|
|
||||||
der_tlv(0xA0 + tag_no, encoded_inner_der)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn der_tlv(tag: u8, content: Vec<u8>) -> Vec<u8> {
|
|
||||||
let mut out = Vec::with_capacity(2 + content.len());
|
|
||||||
out.push(tag);
|
|
||||||
out.extend(der_len(content.len()));
|
|
||||||
out.extend(content);
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
fn der_len(len: usize) -> Vec<u8> {
|
|
||||||
if len < 128 {
|
|
||||||
return vec![len as u8];
|
|
||||||
}
|
|
||||||
let mut bytes = Vec::new();
|
|
||||||
let mut n = len;
|
|
||||||
while n > 0 {
|
|
||||||
bytes.push((n & 0xFF) as u8);
|
|
||||||
n >>= 8;
|
|
||||||
}
|
|
||||||
bytes.reverse();
|
|
||||||
let mut out = vec![0x80 | (bytes.len() as u8)];
|
|
||||||
out.extend(bytes);
|
|
||||||
out
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
# rtr_debug_client
|
|
||||||
|
|
||||||
`rtr_debug_client` 是用于 RTR 协议联调的命令行调试客户端,支持 `TCP`、`TLS`、`SSH` 三种传输。
|
|
||||||
|
|
||||||
它用于:
|
|
||||||
- 手动发送 `Reset Query`、`Serial Query`
|
|
||||||
- 持续接收并打印服务端 PDU
|
|
||||||
- 观察 `session_id`、`serial`、`EndOfData` timing hint、`ErrorReport` 等状态变化
|
|
||||||
|
|
||||||
## 构建
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo build --bin rtr_debug_client
|
|
||||||
```
|
|
||||||
|
|
||||||
## 基本用法
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run --bin rtr_debug_client -- <addr> <version> [reset|serial <session_id> <serial>] [options]
|
|
||||||
```
|
|
||||||
|
|
||||||
默认值:
|
|
||||||
- `addr`: `127.0.0.1:323`
|
|
||||||
- `version`: `1`
|
|
||||||
- `mode`: `reset`
|
|
||||||
- `--timeout`: `30`
|
|
||||||
- `--poll`: `600`
|
|
||||||
|
|
||||||
## TCP 用法
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run --bin rtr_debug_client -- 127.0.0.1:323 1 reset
|
|
||||||
```
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run --bin rtr_debug_client -- 127.0.0.1:323 1 serial 42 100
|
|
||||||
```
|
|
||||||
|
|
||||||
## TLS 用法
|
|
||||||
|
|
||||||
仅校验服务端证书:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run --bin rtr_debug_client -- \
|
|
||||||
127.0.0.1:324 1 reset \
|
|
||||||
--tls \
|
|
||||||
--ca-cert tests/fixtures/tls/client-ca.crt \
|
|
||||||
--server-name localhost
|
|
||||||
```
|
|
||||||
|
|
||||||
双向 TLS:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run --bin rtr_debug_client -- \
|
|
||||||
127.0.0.1:324 1 reset \
|
|
||||||
--tls \
|
|
||||||
--ca-cert tests/fixtures/tls/client-ca.crt \
|
|
||||||
--server-name localhost \
|
|
||||||
--client-cert tests/fixtures/tls/client-good.crt \
|
|
||||||
--client-key tests/fixtures/tls/client-good.key
|
|
||||||
```
|
|
||||||
|
|
||||||
## SSH 用法(按 draft-ietf-sidrops-8210bis-25)
|
|
||||||
|
|
||||||
`rtr_debug_client --ssh` 采用以下流程:
|
|
||||||
- SSHv2 连接
|
|
||||||
- `session` channel
|
|
||||||
- 请求 `subsystem`,默认 `rpki-rtr`
|
|
||||||
- 使用 `publickey` 认证
|
|
||||||
- 强制服务端 host key 校验(`known_hosts` 或 pinned server key 二选一)
|
|
||||||
|
|
||||||
### 1. 使用 known_hosts 校验服务端
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run --bin rtr_debug_client -- \
|
|
||||||
127.0.0.1:22 1 reset \
|
|
||||||
--ssh \
|
|
||||||
--ssh-user rpki-rtr \
|
|
||||||
--ssh-key certs/rtr-client.key \
|
|
||||||
--ssh-known-hosts certs/known_hosts
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 使用固定服务端公钥校验
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run --bin rtr_debug_client -- \
|
|
||||||
127.0.0.1:22 1 reset \
|
|
||||||
--ssh \
|
|
||||||
--ssh-user rpki-rtr \
|
|
||||||
--ssh-key certs/rtr-client.key \
|
|
||||||
--ssh-server-key certs/ssh_host_ed25519_key.pub
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 自定义 subsystem 名称
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run --bin rtr_debug_client -- \
|
|
||||||
127.0.0.1:22 1 reset \
|
|
||||||
--ssh \
|
|
||||||
--ssh-user rpki-rtr \
|
|
||||||
--ssh-key certs/rtr-client.key \
|
|
||||||
--ssh-known-hosts certs/known_hosts \
|
|
||||||
--ssh-subsystem rpki-rtr
|
|
||||||
```
|
|
||||||
|
|
||||||
## 参数说明
|
|
||||||
|
|
||||||
通用参数:
|
|
||||||
- `--timeout <secs>`:读取 PDU 超时时间(秒)
|
|
||||||
- `--poll <secs>`:默认自动轮询间隔(秒)
|
|
||||||
- `--keep-after-error`:兼容旧脚本的保留参数;默认已按 RFC/draft 处理 `ErrorReport`
|
|
||||||
- `--summary-only`:仅打印摘要,抑制 payload PDU 详细内容
|
|
||||||
|
|
||||||
TLS 参数:
|
|
||||||
- `--tls`
|
|
||||||
- `--ca-cert <path>`
|
|
||||||
- `--server-name <name>`
|
|
||||||
- `--client-cert <path>`
|
|
||||||
- `--client-key <path>`
|
|
||||||
|
|
||||||
SSH 参数:
|
|
||||||
- `--ssh`
|
|
||||||
- `--ssh-user <name>`
|
|
||||||
- `--ssh-key <path>`(OpenSSH 私钥)
|
|
||||||
- `--ssh-subsystem <name>`(默认 `rpki-rtr`)
|
|
||||||
- `--ssh-known-hosts <path>` 或 `--ssh-server-key <path>`(二选一,必须提供)
|
|
||||||
|
|
||||||
## SSH 连通性测试建议
|
|
||||||
|
|
||||||
如果你已经在 Docker 中启动了支持 SSH 的 RTR server,可按以下方式验证:
|
|
||||||
|
|
||||||
1. 先用 `ssh` 命令确认认证与 host key 配置正确(可连通)。
|
|
||||||
2. 再用 `rtr_debug_client --ssh` 发起连接并发送 `reset`。
|
|
||||||
3. 观察是否收到 `Cache Response` 和 `EndOfData`。
|
|
||||||
|
|
||||||
如果 `rtr_debug_client` 报 `failed to request SSH subsystem 'rpki-rtr'`,通常表示服务端未开启对应 subsystem 名称,或名称不一致。
|
|
||||||
|
|
||||||
## 运行时交互命令
|
|
||||||
|
|
||||||
客户端启动后可在标准输入中使用:
|
|
||||||
- `help`
|
|
||||||
- `state`
|
|
||||||
- `version` / `version <n>`
|
|
||||||
- `reset`
|
|
||||||
- `serial` / `serial <sid> <serial>`
|
|
||||||
- `timeout` / `timeout <secs>`
|
|
||||||
- `poll` / `poll <secs>` / `poll pause` / `poll resume`
|
|
||||||
- `output` / `output verbose` / `output summary`
|
|
||||||
- `quit`
|
|
||||||
|
|
||||||
## SSH Password Auth (Added)
|
|
||||||
|
|
||||||
`rtr_debug_client` now supports password auth in SSH mode.
|
|
||||||
|
|
||||||
- Use exactly one auth option in SSH mode:
|
|
||||||
- `--ssh-key <path>`
|
|
||||||
- `--ssh-password <value>`
|
|
||||||
- Host key verification is still required:
|
|
||||||
- `--ssh-known-hosts <path>` or `--ssh-server-key <path>`
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run --bin rtr_debug_client -- \
|
|
||||||
127.0.0.1:22 1 reset \
|
|
||||||
--ssh \
|
|
||||||
--ssh-user rpki-rtr \
|
|
||||||
--ssh-password 'your-password' \
|
|
||||||
--ssh-server-key certs/ssh_host_rsa_key.pub
|
|
||||||
```
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,404 +0,0 @@
|
|||||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
|
||||||
|
|
||||||
use crate::protocol::{
|
|
||||||
ASPA_FIXED_BODY_LEN, END_OF_DATA_V0_BODY_LEN, END_OF_DATA_V1_BODY_LEN, IPV4_PREFIX_BODY_LEN,
|
|
||||||
IPV6_PREFIX_BODY_LEN, PduHeader, PduType, ROUTER_KEY_FIXED_BODY_LEN, flag_meaning, hex_bytes,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn print_pdu(header: &PduHeader, body: &[u8]) {
|
|
||||||
println!("--------------------------------------------------");
|
|
||||||
println!("PDU: {}", header.pdu_type());
|
|
||||||
println!("version : {}", header.version);
|
|
||||||
println!("length : {}", header.length);
|
|
||||||
|
|
||||||
match header.pdu_type() {
|
|
||||||
PduType::CacheResponse => {
|
|
||||||
println!("session_id : {}", header.session_id());
|
|
||||||
}
|
|
||||||
PduType::CacheReset => {
|
|
||||||
println!("cache reset");
|
|
||||||
}
|
|
||||||
PduType::Ipv4Prefix => {
|
|
||||||
print_ipv4_prefix(header, body);
|
|
||||||
}
|
|
||||||
PduType::Ipv6Prefix => {
|
|
||||||
print_ipv6_prefix(header, body);
|
|
||||||
}
|
|
||||||
PduType::RouterKey => {
|
|
||||||
print_router_key(header, body);
|
|
||||||
}
|
|
||||||
PduType::EndOfData => {
|
|
||||||
print_end_of_data(header, body);
|
|
||||||
}
|
|
||||||
PduType::ErrorReport => {
|
|
||||||
print_error_report(header, body);
|
|
||||||
}
|
|
||||||
PduType::SerialNotify => {
|
|
||||||
print_serial_notify(header, body);
|
|
||||||
}
|
|
||||||
PduType::SerialQuery => {
|
|
||||||
print_serial_query(header, body);
|
|
||||||
}
|
|
||||||
PduType::Aspa => {
|
|
||||||
print_aspa(header, body);
|
|
||||||
}
|
|
||||||
PduType::ResetQuery => {
|
|
||||||
println!("reset query");
|
|
||||||
}
|
|
||||||
PduType::Unknown(_) => {
|
|
||||||
println!("field1 : {}", header.field1);
|
|
||||||
println!("body : {}", hex_bytes(body));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_ipv4_prefix(header: &PduHeader, body: &[u8]) {
|
|
||||||
if body.len() != IPV4_PREFIX_BODY_LEN {
|
|
||||||
println!("invalid IPv4 Prefix body length: {}", body.len());
|
|
||||||
println!("raw body: {}", hex_bytes(body));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let flags = body[0];
|
|
||||||
let prefix_len = body[1];
|
|
||||||
let max_len = body[2];
|
|
||||||
let zero = body[3];
|
|
||||||
let prefix = Ipv4Addr::new(body[4], body[5], body[6], body[7]);
|
|
||||||
let asn = u32::from_be_bytes([body[8], body[9], body[10], body[11]]);
|
|
||||||
|
|
||||||
println!("session_id : {}", header.session_id());
|
|
||||||
println!("flags : 0x{:02x} ({})", flags, flag_meaning(flags));
|
|
||||||
println!("prefix_len : {}", prefix_len);
|
|
||||||
println!("max_len : {}", max_len);
|
|
||||||
println!("zero : {}", zero);
|
|
||||||
println!("prefix : {}", prefix);
|
|
||||||
println!("asn : {}", asn);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_ipv6_prefix(header: &PduHeader, body: &[u8]) {
|
|
||||||
if body.len() != IPV6_PREFIX_BODY_LEN {
|
|
||||||
println!("invalid IPv6 Prefix body length: {}", body.len());
|
|
||||||
println!("raw body: {}", hex_bytes(body));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let flags = body[0];
|
|
||||||
let prefix_len = body[1];
|
|
||||||
let max_len = body[2];
|
|
||||||
let zero = body[3];
|
|
||||||
|
|
||||||
let mut addr = [0u8; 16];
|
|
||||||
addr.copy_from_slice(&body[4..20]);
|
|
||||||
let prefix = Ipv6Addr::from(addr);
|
|
||||||
|
|
||||||
let asn = u32::from_be_bytes([body[20], body[21], body[22], body[23]]);
|
|
||||||
|
|
||||||
println!("session_id : {}", header.session_id());
|
|
||||||
println!("flags : 0x{:02x} ({})", flags, flag_meaning(flags));
|
|
||||||
println!("prefix_len : {}", prefix_len);
|
|
||||||
println!("max_len : {}", max_len);
|
|
||||||
println!("zero : {}", zero);
|
|
||||||
println!("prefix : {}", prefix);
|
|
||||||
println!("asn : {}", asn);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_end_of_data(header: &PduHeader, body: &[u8]) {
|
|
||||||
println!("session_id : {}", header.session_id());
|
|
||||||
|
|
||||||
match body.len() {
|
|
||||||
END_OF_DATA_V0_BODY_LEN => {
|
|
||||||
let serial = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
|
|
||||||
println!("serial : {}", serial);
|
|
||||||
println!("variant : v0");
|
|
||||||
}
|
|
||||||
END_OF_DATA_V1_BODY_LEN => {
|
|
||||||
let serial = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
|
|
||||||
let refresh = u32::from_be_bytes([body[4], body[5], body[6], body[7]]);
|
|
||||||
let retry = u32::from_be_bytes([body[8], body[9], body[10], body[11]]);
|
|
||||||
let expire = u32::from_be_bytes([body[12], body[13], body[14], body[15]]);
|
|
||||||
println!("serial : {}", serial);
|
|
||||||
println!("refresh : {}", refresh);
|
|
||||||
println!("retry : {}", retry);
|
|
||||||
println!("expire : {}", expire);
|
|
||||||
println!("variant : v1/v2");
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
println!("invalid EndOfData body length: {}", body.len());
|
|
||||||
println!("raw body : {}", hex_bytes(body));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_error_report(header: &PduHeader, body: &[u8]) {
|
|
||||||
println!(
|
|
||||||
"error_code : {} ({})",
|
|
||||||
header.error_code(),
|
|
||||||
error_code_name(header.error_code())
|
|
||||||
);
|
|
||||||
|
|
||||||
if body.len() < 8 {
|
|
||||||
println!("invalid ErrorReport body length: {}", body.len());
|
|
||||||
println!("raw body : {}", hex_bytes(body));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let encapsulated_len = u32::from_be_bytes([body[0], body[1], body[2], body[3]]) as usize;
|
|
||||||
|
|
||||||
if body.len() < 4 + encapsulated_len + 4 {
|
|
||||||
println!("invalid ErrorReport: truncated encapsulated PDU");
|
|
||||||
println!("raw body : {}", hex_bytes(body));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let encapsulated = &body[4..4 + encapsulated_len];
|
|
||||||
|
|
||||||
let text_len_offset = 4 + encapsulated_len;
|
|
||||||
let text_len = u32::from_be_bytes([
|
|
||||||
body[text_len_offset],
|
|
||||||
body[text_len_offset + 1],
|
|
||||||
body[text_len_offset + 2],
|
|
||||||
body[text_len_offset + 3],
|
|
||||||
]) as usize;
|
|
||||||
|
|
||||||
if body.len() < text_len_offset + 4 + text_len {
|
|
||||||
println!("invalid ErrorReport: truncated text");
|
|
||||||
println!("raw body : {}", hex_bytes(body));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let text_bytes = &body[text_len_offset + 4..text_len_offset + 4 + text_len];
|
|
||||||
let text = String::from_utf8_lossy(text_bytes);
|
|
||||||
|
|
||||||
println!("encap_len : {}", encapsulated_len);
|
|
||||||
if let Some(encap_header) = parse_encapsulated_header(encapsulated) {
|
|
||||||
println!("encap_pdu_type : {}", encap_header.pdu_type());
|
|
||||||
println!("encap_version : {}", encap_header.version);
|
|
||||||
println!("encap_length : {}", encap_header.length);
|
|
||||||
match encap_header.pdu_type() {
|
|
||||||
PduType::ErrorReport => {
|
|
||||||
println!("encap_field1 : error_code={}", encap_header.error_code());
|
|
||||||
}
|
|
||||||
PduType::Unknown(_) => {
|
|
||||||
println!("encap_field1 : {}", encap_header.field1);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
println!("encap_field1 : session_id={}", encap_header.session_id());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if encapsulated_len > 0 {
|
|
||||||
println!("encap_header : <truncated or unavailable>");
|
|
||||||
}
|
|
||||||
println!("encap_pdu : {}", hex_bytes(encapsulated));
|
|
||||||
println!("text_len : {}", text_len);
|
|
||||||
println!("text_utf8 : {}", std::str::from_utf8(text_bytes).is_ok());
|
|
||||||
println!("text : {}", text);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_serial_notify(header: &PduHeader, body: &[u8]) {
|
|
||||||
if body.len() != 4 {
|
|
||||||
println!("invalid Serial Notify body length: {}", body.len());
|
|
||||||
println!("raw body : {}", hex_bytes(body));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let serial = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
|
|
||||||
println!("session_id : {}", header.session_id());
|
|
||||||
println!("serial : {}", serial);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_serial_query(header: &PduHeader, body: &[u8]) {
|
|
||||||
if body.len() != 4 {
|
|
||||||
println!("invalid Serial Query body length: {}", body.len());
|
|
||||||
println!("raw body : {}", hex_bytes(body));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let serial = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
|
|
||||||
println!("session_id : {}", header.session_id());
|
|
||||||
println!("serial : {}", serial);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_router_key(_header: &PduHeader, body: &[u8]) {
|
|
||||||
if body.len() < ROUTER_KEY_FIXED_BODY_LEN {
|
|
||||||
println!("invalid Router Key body length: {}", body.len());
|
|
||||||
println!("raw body : {}", hex_bytes(body));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let flags = _header.flags();
|
|
||||||
let zero = _header.zero();
|
|
||||||
let ski = &body[0..20];
|
|
||||||
let asn = u32::from_be_bytes([body[20], body[21], body[22], body[23]]);
|
|
||||||
let spki = &body[24..];
|
|
||||||
|
|
||||||
println!("flags : 0x{:02x} ({})", flags, flag_meaning(flags));
|
|
||||||
println!("zero : {}", zero);
|
|
||||||
println!("ski : {}", hex_bytes(ski));
|
|
||||||
println!("asn : {}", asn);
|
|
||||||
println!("spki_len : {}", spki.len());
|
|
||||||
println!("spki : {}", hex_bytes(spki));
|
|
||||||
|
|
||||||
if flags & !0x01 != 0 {
|
|
||||||
println!("warning : Router Key flags use reserved bits");
|
|
||||||
}
|
|
||||||
if zero != 0 {
|
|
||||||
println!("warning : Router Key reserved zero octet is non-zero");
|
|
||||||
}
|
|
||||||
if asn == 0 {
|
|
||||||
println!("warning : Router Key ASN is AS0");
|
|
||||||
}
|
|
||||||
if spki.is_empty() {
|
|
||||||
println!("warning : Router Key SPKI is empty");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn print_raw_pdu(header: &PduHeader, body: &[u8]) {
|
|
||||||
let mut raw = Vec::with_capacity(8 + body.len());
|
|
||||||
raw.push(header.version);
|
|
||||||
raw.push(header.pdu_type_raw);
|
|
||||||
raw.extend_from_slice(&header.field1.to_be_bytes());
|
|
||||||
raw.extend_from_slice(&header.length.to_be_bytes());
|
|
||||||
raw.extend_from_slice(body);
|
|
||||||
|
|
||||||
println!("--------------------------------------------------");
|
|
||||||
println!("[raw] pdu_type : {}", header.pdu_type());
|
|
||||||
println!("[raw] octets : {}", hex_bytes(&raw));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn error_code_name(code: u16) -> &'static str {
|
|
||||||
match code {
|
|
||||||
0 => "Corrupt Data",
|
|
||||||
1 => "Internal Error",
|
|
||||||
2 => "No Data Available",
|
|
||||||
3 => "Invalid Request",
|
|
||||||
4 => "Unsupported Protocol Version",
|
|
||||||
5 => "Unsupported PDU Type",
|
|
||||||
6 => "Withdrawal of Unknown Record",
|
|
||||||
7 => "Duplicate Announcement Received",
|
|
||||||
8 => "Unexpected Protocol Version",
|
|
||||||
9 => "ASPA Provider List Error",
|
|
||||||
10 => "Transport Failure",
|
|
||||||
11 => "Ordering Error",
|
|
||||||
12 => "Cache Restart",
|
|
||||||
13 => "Cache Shutdown",
|
|
||||||
_ => "Unknown Error Code",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_encapsulated_header(encapsulated: &[u8]) -> Option<PduHeader> {
|
|
||||||
if encapsulated.len() < 8 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut header = [0u8; 8];
|
|
||||||
header.copy_from_slice(&encapsulated[..8]);
|
|
||||||
Some(PduHeader::from_bytes(header))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_aspa(_header: &PduHeader, body: &[u8]) {
|
|
||||||
if body.len() < ASPA_FIXED_BODY_LEN {
|
|
||||||
println!("invalid ASPA body length: {}", body.len());
|
|
||||||
println!("raw body : {}", hex_bytes(body));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let flags = _header.flags();
|
|
||||||
let zero = _header.zero();
|
|
||||||
let customer_asn = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
|
|
||||||
|
|
||||||
println!("flags : 0x{:02x} ({})", flags, flag_meaning(flags));
|
|
||||||
println!("reserved : {}", zero);
|
|
||||||
println!("customer_as : {}", customer_asn);
|
|
||||||
|
|
||||||
let providers_raw = &body[4..];
|
|
||||||
if providers_raw.len() % 4 != 0 {
|
|
||||||
println!("invalid ASPA providers length: {}", providers_raw.len());
|
|
||||||
println!("providers : {}", hex_bytes(providers_raw));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut providers = Vec::new();
|
|
||||||
for chunk in providers_raw.chunks_exact(4) {
|
|
||||||
providers.push(u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("providers : {:?}", providers);
|
|
||||||
|
|
||||||
if flags & !0x01 != 0 {
|
|
||||||
println!("warning : ASPA flags use reserved bits");
|
|
||||||
}
|
|
||||||
if zero != 0 {
|
|
||||||
println!("warning : ASPA reserved zero octet is non-zero");
|
|
||||||
}
|
|
||||||
if customer_asn == 0 {
|
|
||||||
println!("warning : ASPA customer ASN is AS0");
|
|
||||||
}
|
|
||||||
|
|
||||||
let is_announcement = flags & 0x01 == 0x01;
|
|
||||||
if is_announcement {
|
|
||||||
if providers.is_empty() {
|
|
||||||
println!("warning : ASPA announcement has empty provider list");
|
|
||||||
}
|
|
||||||
} else if !providers.is_empty() {
|
|
||||||
println!("warning : ASPA withdrawal must not contain providers");
|
|
||||||
}
|
|
||||||
|
|
||||||
if providers.iter().any(|asn| *asn == 0) {
|
|
||||||
println!("warning : ASPA provider list contains AS0");
|
|
||||||
}
|
|
||||||
|
|
||||||
let strictly_increasing = providers.windows(2).all(|pair| pair[0] < pair[1]);
|
|
||||||
if !strictly_increasing && providers.len() > 1 {
|
|
||||||
println!("warning : ASPA providers are not in strictly increasing order");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_serial_notify_serial(body: &[u8]) -> Option<u32> {
|
|
||||||
if body.len() != 4 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(u32::from_be_bytes([body[0], body[1], body[2], body[3]]))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_end_of_data_serial(body: &[u8]) -> Option<u32> {
|
|
||||||
match body.len() {
|
|
||||||
4 | 16 => Some(u32::from_be_bytes([body[0], body[1], body[2], body[3]])),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct EndOfDataInfo {
|
|
||||||
pub serial: u32,
|
|
||||||
pub refresh: Option<u32>,
|
|
||||||
pub retry: Option<u32>,
|
|
||||||
pub expire: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_end_of_data_info(body: &[u8]) -> Option<EndOfDataInfo> {
|
|
||||||
match body.len() {
|
|
||||||
4 => {
|
|
||||||
let serial = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
|
|
||||||
Some(EndOfDataInfo {
|
|
||||||
serial,
|
|
||||||
refresh: None,
|
|
||||||
retry: None,
|
|
||||||
expire: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
16 => {
|
|
||||||
let serial = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
|
|
||||||
let refresh = u32::from_be_bytes([body[4], body[5], body[6], body[7]]);
|
|
||||||
let retry = u32::from_be_bytes([body[8], body[9], body[10], body[11]]);
|
|
||||||
let expire = u32::from_be_bytes([body[12], body[13], body[14], body[15]]);
|
|
||||||
Some(EndOfDataInfo {
|
|
||||||
serial,
|
|
||||||
refresh: Some(refresh),
|
|
||||||
retry: Some(retry),
|
|
||||||
expire: Some(expire),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
use std::fmt;
|
|
||||||
|
|
||||||
pub const HEADER_LEN: usize = 8;
|
|
||||||
pub const SERIAL_QUERY_LEN: usize = 12;
|
|
||||||
pub const MAX_PDU_LEN: u32 = 65_535;
|
|
||||||
|
|
||||||
pub const IPV4_PREFIX_BODY_LEN: usize = 12;
|
|
||||||
pub const IPV6_PREFIX_BODY_LEN: usize = 24;
|
|
||||||
pub const END_OF_DATA_V0_BODY_LEN: usize = 4;
|
|
||||||
pub const END_OF_DATA_V1_BODY_LEN: usize = 16;
|
|
||||||
pub const ROUTER_KEY_FIXED_BODY_LEN: usize = 24;
|
|
||||||
pub const ASPA_FIXED_BODY_LEN: usize = 4;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum QueryMode {
|
|
||||||
Reset,
|
|
||||||
Serial { session_id: u16, serial: u32 },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum PduType {
|
|
||||||
SerialNotify,
|
|
||||||
SerialQuery,
|
|
||||||
ResetQuery,
|
|
||||||
CacheResponse,
|
|
||||||
Ipv4Prefix,
|
|
||||||
Ipv6Prefix,
|
|
||||||
RouterKey,
|
|
||||||
EndOfData,
|
|
||||||
CacheReset,
|
|
||||||
ErrorReport,
|
|
||||||
Aspa,
|
|
||||||
Unknown(u8),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PduType {
|
|
||||||
pub fn code(self) -> u8 {
|
|
||||||
match self {
|
|
||||||
Self::SerialNotify => 0,
|
|
||||||
Self::SerialQuery => 1,
|
|
||||||
Self::ResetQuery => 2,
|
|
||||||
Self::CacheResponse => 3,
|
|
||||||
Self::Ipv4Prefix => 4,
|
|
||||||
Self::Ipv6Prefix => 6,
|
|
||||||
Self::RouterKey => 9,
|
|
||||||
Self::EndOfData => 7,
|
|
||||||
Self::CacheReset => 8,
|
|
||||||
Self::ErrorReport => 10,
|
|
||||||
Self::Aspa => 11,
|
|
||||||
Self::Unknown(v) => v,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn name(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::SerialNotify => "Serial Notify",
|
|
||||||
Self::SerialQuery => "Serial Query",
|
|
||||||
Self::ResetQuery => "Reset Query",
|
|
||||||
Self::CacheResponse => "Cache Response",
|
|
||||||
Self::Ipv4Prefix => "IPv4 Prefix",
|
|
||||||
Self::Ipv6Prefix => "IPv6 Prefix",
|
|
||||||
Self::RouterKey => "Router Key",
|
|
||||||
Self::EndOfData => "End of Data",
|
|
||||||
Self::CacheReset => "Cache Reset",
|
|
||||||
Self::ErrorReport => "Error Report",
|
|
||||||
Self::Aspa => "ASPA",
|
|
||||||
Self::Unknown(_) => "Unknown",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<u8> for PduType {
|
|
||||||
fn from(value: u8) -> Self {
|
|
||||||
match value {
|
|
||||||
0 => Self::SerialNotify,
|
|
||||||
1 => Self::SerialQuery,
|
|
||||||
2 => Self::ResetQuery,
|
|
||||||
3 => Self::CacheResponse,
|
|
||||||
4 => Self::Ipv4Prefix,
|
|
||||||
6 => Self::Ipv6Prefix,
|
|
||||||
9 => Self::RouterKey,
|
|
||||||
7 => Self::EndOfData,
|
|
||||||
8 => Self::CacheReset,
|
|
||||||
10 => Self::ErrorReport,
|
|
||||||
11 => Self::Aspa,
|
|
||||||
x => Self::Unknown(x),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for PduType {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Unknown(v) => write!(f, "{} ({})", self.name(), v),
|
|
||||||
_ => write!(f, "{} ({})", self.name(), self.code()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct PduHeader {
|
|
||||||
pub version: u8,
|
|
||||||
pub pdu_type_raw: u8,
|
|
||||||
pub field1: u16,
|
|
||||||
pub length: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PduHeader {
|
|
||||||
pub fn from_bytes(buf: [u8; HEADER_LEN]) -> Self {
|
|
||||||
Self {
|
|
||||||
version: buf[0],
|
|
||||||
pdu_type_raw: buf[1],
|
|
||||||
field1: u16::from_be_bytes([buf[2], buf[3]]),
|
|
||||||
length: u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pdu_type(&self) -> PduType {
|
|
||||||
self.pdu_type_raw.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn session_id(&self) -> u16 {
|
|
||||||
self.field1
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn error_code(&self) -> u16 {
|
|
||||||
self.field1
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn flags(&self) -> u8 {
|
|
||||||
self.field1.to_be_bytes()[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn zero(&self) -> u8 {
|
|
||||||
self.field1.to_be_bytes()[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct RawPdu {
|
|
||||||
pub header: PduHeader,
|
|
||||||
pub body: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn flag_meaning(flags: u8) -> &'static str {
|
|
||||||
if flags & 0x01 == 0x01 {
|
|
||||||
"announcement"
|
|
||||||
} else {
|
|
||||||
"withdrawal"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn hex_bytes(data: &[u8]) -> String {
|
|
||||||
if data.is_empty() {
|
|
||||||
return "<empty>".to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut out = String::with_capacity(data.len() * 3 - 1);
|
|
||||||
for (idx, b) in data.iter().enumerate() {
|
|
||||||
if idx > 0 {
|
|
||||||
out.push(' ');
|
|
||||||
}
|
|
||||||
use std::fmt::Write as _;
|
|
||||||
let _ = write!(out, "{:02x}", b);
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
use std::io;
|
|
||||||
|
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
|
||||||
|
|
||||||
use crate::protocol::{HEADER_LEN, MAX_PDU_LEN, PduHeader, PduType, RawPdu, SERIAL_QUERY_LEN};
|
|
||||||
|
|
||||||
pub async fn send_reset_query<S>(stream: &mut S, version: u8) -> io::Result<()>
|
|
||||||
where
|
|
||||||
S: AsyncWrite + Unpin,
|
|
||||||
{
|
|
||||||
let mut buf = [0u8; HEADER_LEN];
|
|
||||||
buf[0] = version;
|
|
||||||
buf[1] = PduType::ResetQuery.code();
|
|
||||||
buf[2..4].copy_from_slice(&0u16.to_be_bytes());
|
|
||||||
buf[4..8].copy_from_slice(&(HEADER_LEN as u32).to_be_bytes());
|
|
||||||
stream.write_all(&buf).await?;
|
|
||||||
stream.flush().await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_serial_query<S>(
|
|
||||||
stream: &mut S,
|
|
||||||
version: u8,
|
|
||||||
session_id: u16,
|
|
||||||
serial: u32,
|
|
||||||
) -> io::Result<()>
|
|
||||||
where
|
|
||||||
S: AsyncWrite + Unpin,
|
|
||||||
{
|
|
||||||
let mut buf = [0u8; SERIAL_QUERY_LEN];
|
|
||||||
buf[0] = version;
|
|
||||||
buf[1] = PduType::SerialQuery.code();
|
|
||||||
buf[2..4].copy_from_slice(&session_id.to_be_bytes());
|
|
||||||
buf[4..8].copy_from_slice(&(SERIAL_QUERY_LEN as u32).to_be_bytes());
|
|
||||||
buf[8..12].copy_from_slice(&serial.to_be_bytes());
|
|
||||||
stream.write_all(&buf).await?;
|
|
||||||
stream.flush().await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn read_header<S>(stream: &mut S) -> io::Result<PduHeader>
|
|
||||||
where
|
|
||||||
S: AsyncRead + Unpin,
|
|
||||||
{
|
|
||||||
let mut buf = [0u8; HEADER_LEN];
|
|
||||||
stream.read_exact(&mut buf).await?;
|
|
||||||
Ok(PduHeader::from_bytes(buf))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn read_pdu<S>(stream: &mut S) -> io::Result<RawPdu>
|
|
||||||
where
|
|
||||||
S: AsyncRead + Unpin,
|
|
||||||
{
|
|
||||||
let header = read_header(stream).await?;
|
|
||||||
|
|
||||||
if header.length < HEADER_LEN as u32 {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
format!("invalid PDU length {} < {}", header.length, HEADER_LEN),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if header.length > MAX_PDU_LEN {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
format!(
|
|
||||||
"PDU length {} exceeds max allowed {}",
|
|
||||||
header.length, MAX_PDU_LEN
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let body_len = header.length as usize - HEADER_LEN;
|
|
||||||
let mut body = vec![0u8; body_len];
|
|
||||||
stream.read_exact(&mut body).await?;
|
|
||||||
|
|
||||||
Ok(RawPdu { header, body })
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
use std::env;
|
|
||||||
use std::fs::File;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow};
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use rpki::rtr::payload::Payload;
|
|
||||||
use rpki::slurm::file::SlurmFile;
|
|
||||||
use rpki::source::ccr::{
|
|
||||||
find_latest_ccr_file, load_ccr_snapshot_from_file, snapshot_to_payloads_with_options,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct Cli {
|
|
||||||
ccr_path: PathBuf,
|
|
||||||
slurm_path: PathBuf,
|
|
||||||
dump_payloads: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
struct Output {
|
|
||||||
ccr_path: String,
|
|
||||||
slurm_path: String,
|
|
||||||
produced_at: Option<String>,
|
|
||||||
slurm_version: u32,
|
|
||||||
input_payload_count: usize,
|
|
||||||
input_vrp_count: usize,
|
|
||||||
input_vap_count: usize,
|
|
||||||
output_payload_count: usize,
|
|
||||||
output_vrp_count: usize,
|
|
||||||
output_vap_count: usize,
|
|
||||||
invalid_vrps: Vec<String>,
|
|
||||||
invalid_vaps: Vec<String>,
|
|
||||||
sample_output_aspa_customers: Vec<u32>,
|
|
||||||
payloads: Option<Vec<Payload>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
let cli = parse_args(env::args().skip(1))?;
|
|
||||||
|
|
||||||
let snapshot = load_ccr_snapshot_from_file(&cli.ccr_path)
|
|
||||||
.with_context(|| format!("failed to load CCR snapshot: {}", cli.ccr_path.display()))?;
|
|
||||||
let slurm = load_slurm(&cli.slurm_path)?;
|
|
||||||
|
|
||||||
let conversion = snapshot_to_payloads_with_options(&snapshot, true)?;
|
|
||||||
let payloads = slurm.apply(&conversion.payloads);
|
|
||||||
let (input_vrp_count, input_vap_count) = count_vrps_and_vaps(&conversion.payloads);
|
|
||||||
let (output_vrp_count, output_vap_count) = count_vrps_and_vaps(&payloads);
|
|
||||||
|
|
||||||
let output = Output {
|
|
||||||
ccr_path: cli.ccr_path.display().to_string(),
|
|
||||||
slurm_path: cli.slurm_path.display().to_string(),
|
|
||||||
produced_at: snapshot.produced_at.clone(),
|
|
||||||
slurm_version: slurm.version().as_u32(),
|
|
||||||
input_payload_count: conversion.payloads.len(),
|
|
||||||
input_vrp_count,
|
|
||||||
input_vap_count,
|
|
||||||
output_payload_count: payloads.len(),
|
|
||||||
output_vrp_count,
|
|
||||||
output_vap_count,
|
|
||||||
invalid_vrps: conversion.invalid_vrps,
|
|
||||||
invalid_vaps: conversion.invalid_vaps,
|
|
||||||
sample_output_aspa_customers: sample_aspa_customers(&payloads, 8),
|
|
||||||
payloads: cli.dump_payloads.then_some(payloads),
|
|
||||||
};
|
|
||||||
|
|
||||||
println!("{}", serde_json::to_string_pretty(&output)?);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_slurm(path: &PathBuf) -> Result<SlurmFile> {
|
|
||||||
let file = File::open(path)
|
|
||||||
.with_context(|| format!("failed to open SLURM file: {}", path.display()))?;
|
|
||||||
SlurmFile::from_reader(file)
|
|
||||||
.with_context(|| format!("failed to parse SLURM file: {}", path.display()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_args(args: impl Iterator<Item = String>) -> Result<Cli> {
|
|
||||||
let mut dump_payloads = false;
|
|
||||||
let mut positionals = Vec::new();
|
|
||||||
|
|
||||||
for arg in args {
|
|
||||||
match arg.as_str() {
|
|
||||||
"--dump-payloads" => dump_payloads = true,
|
|
||||||
"-h" | "--help" => {
|
|
||||||
print_help();
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
_ if arg.starts_with('-') => {
|
|
||||||
return Err(anyhow!("unknown option: {}", arg));
|
|
||||||
}
|
|
||||||
_ => positionals.push(arg),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if positionals.is_empty() {
|
|
||||||
return Ok(Cli {
|
|
||||||
ccr_path: find_latest_ccr_file("data")
|
|
||||||
.context("failed to find latest .ccr in ./data for default run")?,
|
|
||||||
slurm_path: PathBuf::from("data/example.slurm"),
|
|
||||||
dump_payloads,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if positionals.len() != 2 {
|
|
||||||
print_help();
|
|
||||||
return Err(anyhow!(
|
|
||||||
"expected: slurm_apply_client <snapshot.ccr> <policy.slurm>"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Cli {
|
|
||||||
ccr_path: PathBuf::from(&positionals[0]),
|
|
||||||
slurm_path: PathBuf::from(&positionals[1]),
|
|
||||||
dump_payloads,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_help() {
|
|
||||||
eprintln!(
|
|
||||||
"Usage: cargo run --bin slurm_apply_client -- [--dump-payloads] <snapshot.ccr> <policy.slurm>"
|
|
||||||
);
|
|
||||||
eprintln!();
|
|
||||||
eprintln!("Reads a CCR snapshot, converts it into payloads, applies SLURM, and prints JSON.");
|
|
||||||
eprintln!(
|
|
||||||
"When no arguments are provided, it defaults to the latest .ccr under ./data and ./data/example.slurm."
|
|
||||||
);
|
|
||||||
eprintln!("Use --dump-payloads to include the full payload list in the JSON output.");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn count_vrps_and_vaps(payloads: &[Payload]) -> (usize, usize) {
|
|
||||||
let mut vrps = 0;
|
|
||||||
let mut vaps = 0;
|
|
||||||
|
|
||||||
for payload in payloads {
|
|
||||||
match payload {
|
|
||||||
Payload::RouteOrigin(_) => vrps += 1,
|
|
||||||
Payload::Aspa(_) => vaps += 1,
|
|
||||||
Payload::RouterKey(_) => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(vrps, vaps)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sample_aspa_customers(payloads: &[Payload], limit: usize) -> Vec<u32> {
|
|
||||||
let mut customers = Vec::new();
|
|
||||||
|
|
||||||
for payload in payloads {
|
|
||||||
if let Payload::Aspa(aspa) = payload {
|
|
||||||
let customer = aspa.customer_asn().into_u32();
|
|
||||||
if !customers.contains(&customer) {
|
|
||||||
customers.push(customer);
|
|
||||||
if customers.len() == limit {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customers
|
|
||||||
}
|
|
||||||
239
src/data_model/aspa.rs
Normal file
239
src/data_model/aspa.rs
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
use crate::data_model::oid::OID_CT_ASPA;
|
||||||
|
use crate::data_model::rc::ResourceCertificate;
|
||||||
|
use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError};
|
||||||
|
use der_parser::ber::{Class};
|
||||||
|
use der_parser::der::{parse_der, DerObject, Tag};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct AspaObject {
|
||||||
|
pub signed_object: RpkiSignedObject,
|
||||||
|
pub econtent_type: String,
|
||||||
|
pub aspa: AspaEContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct AspaEContent {
|
||||||
|
pub version: u32,
|
||||||
|
pub customer_as_id: u32,
|
||||||
|
pub provider_as_ids: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum AspaDecodeError {
|
||||||
|
#[error("SignedObject decode error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")]
|
||||||
|
SignedObjectDecode(#[from] SignedObjectDecodeError),
|
||||||
|
|
||||||
|
#[error("ASPA eContentType must be {OID_CT_ASPA}, got {0} (draft-ietf-sidrops-aspa-profile-21 §2)")]
|
||||||
|
InvalidEContentType(String),
|
||||||
|
|
||||||
|
#[error("ASPA parse error: {0} (draft-ietf-sidrops-aspa-profile-21 §3; DER)")]
|
||||||
|
Parse(String),
|
||||||
|
|
||||||
|
#[error("ASPA trailing bytes: {0} bytes (draft-ietf-sidrops-aspa-profile-21 §3; DER)")]
|
||||||
|
TrailingBytes(usize),
|
||||||
|
|
||||||
|
#[error("ASProviderAttestation must be a SEQUENCE of 3 elements (draft-ietf-sidrops-aspa-profile-21 §3)")]
|
||||||
|
InvalidAttestationSequence,
|
||||||
|
|
||||||
|
#[error("ASPA version must be 1 and MUST be explicitly encoded (draft-ietf-sidrops-aspa-profile-21 §3.1)")]
|
||||||
|
VersionMustBeExplicitOne,
|
||||||
|
|
||||||
|
#[error("ASPA customerASID out of range (0..=4294967295), got {0} (draft-ietf-sidrops-aspa-profile-21 §3.2)")]
|
||||||
|
CustomerAsIdOutOfRange(u64),
|
||||||
|
|
||||||
|
#[error("ASPA providers must contain at least one ASID (draft-ietf-sidrops-aspa-profile-21 §3.3)")]
|
||||||
|
EmptyProviders,
|
||||||
|
|
||||||
|
#[error("ASPA provider ASID out of range (0..=4294967295), got {0} (draft-ietf-sidrops-aspa-profile-21 §3.3)")]
|
||||||
|
ProviderAsIdOutOfRange(u64),
|
||||||
|
|
||||||
|
#[error("ASPA providers must be in strictly increasing order (draft-ietf-sidrops-aspa-profile-21 §3.3)")]
|
||||||
|
ProvidersNotStrictlyIncreasing,
|
||||||
|
|
||||||
|
#[error("ASPA providers contains the customerASID ({0}) which is not allowed (draft-ietf-sidrops-aspa-profile-21 §3.3)")]
|
||||||
|
ProvidersContainCustomer(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum AspaValidateError {
|
||||||
|
#[error("ASPA EE certificate must contain AS resources extension (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2)")]
|
||||||
|
EeAsResourcesMissing,
|
||||||
|
|
||||||
|
#[error("ASPA EE certificate AS resources must not use inherit (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.3)")]
|
||||||
|
EeAsResourcesInherit,
|
||||||
|
|
||||||
|
#[error("ASPA EE certificate AS resources must not include ranges (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.6-§3.2.3.7)")]
|
||||||
|
EeAsResourcesRangePresent,
|
||||||
|
|
||||||
|
#[error("ASPA EE certificate AS resources must not include RDI (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.5; RFC 6487 §4.8.11)")]
|
||||||
|
EeAsResourcesRdiPresent,
|
||||||
|
|
||||||
|
#[error("ASPA EE certificate AS resources must contain exactly one ASID (id element) (draft-ietf-sidrops-aspa-profile-21 §4)")]
|
||||||
|
EeAsResourcesNotSingleId,
|
||||||
|
|
||||||
|
#[error("ASPA customerASID ({customer_as_id}) does not match EE AS resources ({ee_as_id}) (draft-ietf-sidrops-aspa-profile-21 §4)")]
|
||||||
|
CustomerAsIdMismatch { customer_as_id: u32, ee_as_id: u32 },
|
||||||
|
|
||||||
|
#[error("ASPA EE certificate must not contain IP resources extension (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §2.2)")]
|
||||||
|
EeIpResourcesPresent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AspaObject {
|
||||||
|
pub fn decode_der(der: &[u8]) -> Result<Self, AspaDecodeError> {
|
||||||
|
let signed_object = RpkiSignedObject::decode_der(der)?;
|
||||||
|
Self::from_signed_object(signed_object)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result<Self, AspaDecodeError> {
|
||||||
|
let econtent_type = signed_object
|
||||||
|
.signed_data
|
||||||
|
.encap_content_info
|
||||||
|
.econtent_type
|
||||||
|
.clone();
|
||||||
|
if econtent_type != OID_CT_ASPA {
|
||||||
|
return Err(AspaDecodeError::InvalidEContentType(econtent_type));
|
||||||
|
}
|
||||||
|
|
||||||
|
let aspa = AspaEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?;
|
||||||
|
Ok(Self {
|
||||||
|
aspa,
|
||||||
|
signed_object,
|
||||||
|
econtent_type: OID_CT_ASPA.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate this ASPA's embedded EE certificate resources.
|
||||||
|
pub fn validate_embedded_ee_cert(&self) -> Result<(), AspaValidateError> {
|
||||||
|
let ee = &self.signed_object.signed_data.certificates[0].resource_cert;
|
||||||
|
self.aspa.validate_against_ee_cert(ee)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AspaEContent {
|
||||||
|
/// Decode the DER-encoded ASProviderAttestation defined in draft-ietf-sidrops-aspa-profile-21 §3.
|
||||||
|
pub fn decode_der(der: &[u8]) -> Result<Self, AspaDecodeError> {
|
||||||
|
let (rem, obj) = parse_der(der).map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||||
|
if !rem.is_empty() {
|
||||||
|
return Err(AspaDecodeError::TrailingBytes(rem.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let seq = obj
|
||||||
|
.as_sequence()
|
||||||
|
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||||
|
if seq.len() != 3 {
|
||||||
|
return Err(AspaDecodeError::InvalidAttestationSequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
// version [0] EXPLICIT INTEGER MUST be present and MUST be 1.
|
||||||
|
let v_obj = &seq[0];
|
||||||
|
if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) {
|
||||||
|
return Err(AspaDecodeError::VersionMustBeExplicitOne);
|
||||||
|
}
|
||||||
|
let inner_der = v_obj
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||||
|
let (rem, inner) =
|
||||||
|
parse_der(inner_der).map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||||
|
if !rem.is_empty() {
|
||||||
|
return Err(AspaDecodeError::Parse(
|
||||||
|
"trailing bytes inside ASProviderAttestation.version".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let v = inner
|
||||||
|
.as_u64()
|
||||||
|
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||||
|
if v != 1 {
|
||||||
|
return Err(AspaDecodeError::VersionMustBeExplicitOne);
|
||||||
|
}
|
||||||
|
|
||||||
|
let customer_u64 = seq[1]
|
||||||
|
.as_u64()
|
||||||
|
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||||
|
if customer_u64 > u32::MAX as u64 {
|
||||||
|
return Err(AspaDecodeError::CustomerAsIdOutOfRange(customer_u64));
|
||||||
|
}
|
||||||
|
let customer_as_id = customer_u64 as u32;
|
||||||
|
|
||||||
|
let providers = parse_providers(&seq[2], customer_as_id)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
version: 1,
|
||||||
|
customer_as_id,
|
||||||
|
provider_as_ids: providers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate ASPA payload against the embedded EE resource certificate.
|
||||||
|
///
|
||||||
|
/// This implements the EE/payload semantic checks described in
|
||||||
|
/// `draft-ietf-sidrops-aspa-profile-21` §4 (as summarized in `rpki/specs/08_aspa.md`).
|
||||||
|
pub fn validate_against_ee_cert(
|
||||||
|
&self,
|
||||||
|
ee: &ResourceCertificate,
|
||||||
|
) -> Result<(), AspaValidateError> {
|
||||||
|
if ee.tbs.extensions.ip_resources.is_some() {
|
||||||
|
return Err(AspaValidateError::EeIpResourcesPresent);
|
||||||
|
}
|
||||||
|
|
||||||
|
let asn = ee
|
||||||
|
.tbs
|
||||||
|
.extensions
|
||||||
|
.as_resources
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(AspaValidateError::EeAsResourcesMissing)?;
|
||||||
|
|
||||||
|
if asn.rdi.is_some() {
|
||||||
|
return Err(AspaValidateError::EeAsResourcesRdiPresent);
|
||||||
|
}
|
||||||
|
if asn.is_asnum_inherit() {
|
||||||
|
return Err(AspaValidateError::EeAsResourcesInherit);
|
||||||
|
}
|
||||||
|
if asn.has_any_range() {
|
||||||
|
return Err(AspaValidateError::EeAsResourcesRangePresent);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ee_as_id = asn
|
||||||
|
.asnum_single_id()
|
||||||
|
.ok_or(AspaValidateError::EeAsResourcesNotSingleId)?;
|
||||||
|
if ee_as_id != self.customer_as_id {
|
||||||
|
return Err(AspaValidateError::CustomerAsIdMismatch {
|
||||||
|
customer_as_id: self.customer_as_id,
|
||||||
|
ee_as_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_providers(obj: &DerObject<'_>, customer_as_id: u32) -> Result<Vec<u32>, AspaDecodeError> {
|
||||||
|
let seq = obj
|
||||||
|
.as_sequence()
|
||||||
|
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||||
|
if seq.is_empty() {
|
||||||
|
return Err(AspaDecodeError::EmptyProviders);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out: Vec<u32> = Vec::with_capacity(seq.len());
|
||||||
|
let mut prev: Option<u32> = None;
|
||||||
|
for item in seq {
|
||||||
|
let v = item
|
||||||
|
.as_u64()
|
||||||
|
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||||
|
if v > u32::MAX as u64 {
|
||||||
|
return Err(AspaDecodeError::ProviderAsIdOutOfRange(v));
|
||||||
|
}
|
||||||
|
let asn = v as u32;
|
||||||
|
if asn == customer_as_id {
|
||||||
|
return Err(AspaDecodeError::ProvidersContainCustomer(customer_as_id));
|
||||||
|
}
|
||||||
|
if let Some(p) = prev {
|
||||||
|
if asn <= p {
|
||||||
|
return Err(AspaDecodeError::ProvidersNotStrictlyIncreasing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prev = Some(asn);
|
||||||
|
out.push(asn);
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
108
src/data_model/common.rs
Normal file
108
src/data_model/common.rs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
use x509_parser::asn1_rs::Tag;
|
||||||
|
use x509_parser::x509::AlgorithmIdentifier;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Asn1TimeEncoding {
|
||||||
|
UtcTime,
|
||||||
|
GeneralizedTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Asn1TimeUtc {
|
||||||
|
pub utc: time::OffsetDateTime,
|
||||||
|
pub encoding: Asn1TimeEncoding,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Asn1TimeUtc {
|
||||||
|
/// Validate Time encoding rules (RFC 5280): years 1950-2049 use UTCTime,
|
||||||
|
/// other years use GeneralizedTime.
|
||||||
|
pub fn validate_encoding_rfc5280(
|
||||||
|
&self,
|
||||||
|
field: &'static str,
|
||||||
|
) -> Result<(), InvalidTimeEncodingError> {
|
||||||
|
let year = self.utc.year();
|
||||||
|
let expected = if year <= 2049 {
|
||||||
|
Asn1TimeEncoding::UtcTime
|
||||||
|
} else {
|
||||||
|
Asn1TimeEncoding::GeneralizedTime
|
||||||
|
};
|
||||||
|
if self.encoding != expected {
|
||||||
|
return Err(InvalidTimeEncodingError {
|
||||||
|
field,
|
||||||
|
year,
|
||||||
|
encoding: self.encoding,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct BigUnsigned {
|
||||||
|
/// Minimal big-endian bytes. For zero, this is `[0]`.
|
||||||
|
pub bytes_be: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BigUnsigned {
|
||||||
|
pub fn from_biguint(n: &der_parser::num_bigint::BigUint) -> Self {
|
||||||
|
let mut bytes = n.to_bytes_be();
|
||||||
|
if bytes.is_empty() {
|
||||||
|
bytes.push(0);
|
||||||
|
}
|
||||||
|
Self { bytes_be: bytes }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_hex_upper(&self) -> String {
|
||||||
|
hex::encode_upper(&self.bytes_be)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_u64(&self) -> Option<u64> {
|
||||||
|
if self.bytes_be.len() > 8 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut value: u64 = 0;
|
||||||
|
for &b in &self.bytes_be {
|
||||||
|
value = (value << 8) | (b as u64);
|
||||||
|
}
|
||||||
|
Some(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
|
||||||
|
#[error("{field} time encoding invalid for year {year}: got {encoding:?} (RFC 5280 §4.1.2.5; RFC 5280 §5.1.2.4-§5.1.2.6)")]
|
||||||
|
pub struct InvalidTimeEncodingError {
|
||||||
|
pub field: &'static str,
|
||||||
|
pub year: i32,
|
||||||
|
pub encoding: Asn1TimeEncoding,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn asn1_time_to_model(t: x509_parser::time::ASN1Time) -> Asn1TimeUtc {
|
||||||
|
let encoding = if t.is_utctime() {
|
||||||
|
Asn1TimeEncoding::UtcTime
|
||||||
|
} else {
|
||||||
|
Asn1TimeEncoding::GeneralizedTime
|
||||||
|
};
|
||||||
|
Asn1TimeUtc {
|
||||||
|
utc: t.to_datetime(),
|
||||||
|
encoding,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn algorithm_params_absent_or_null(sig: &AlgorithmIdentifier<'_>) -> bool {
|
||||||
|
match sig.parameters.as_ref() {
|
||||||
|
None => true,
|
||||||
|
Some(p) if p.tag() == Tag::Null => true,
|
||||||
|
Some(_p) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filename extensions registered in IANA "RPKI Repository Name Schemes".
|
||||||
|
///
|
||||||
|
/// Source: <https://www.iana.org/assignments/rpki/rpki.xhtml>
|
||||||
|
/// Snapshot date: 2026-01-28.
|
||||||
|
///
|
||||||
|
/// Notes:
|
||||||
|
/// - Includes entries marked TEMPORARY/DEPRECATED by IANA (e.g., `asa`, `gbr`).
|
||||||
|
pub const IANA_RPKI_REPOSITORY_FILENAME_EXTENSIONS: &[&str] = &[
|
||||||
|
"asa", "cer", "crl", "gbr", "mft", "roa", "sig", "tak",
|
||||||
|
];
|
||||||
@@ -1,52 +1,16 @@
|
|||||||
|
pub use crate::data_model::common::{Asn1TimeEncoding, Asn1TimeUtc, BigUnsigned};
|
||||||
|
use crate::data_model::oid::{
|
||||||
|
OID_AUTHORITY_KEY_IDENTIFIER, OID_CRL_NUMBER, OID_SHA256_WITH_RSA_ENCRYPTION,
|
||||||
|
OID_SUBJECT_KEY_IDENTIFIER,
|
||||||
|
};
|
||||||
use x509_parser::extensions::{AuthorityKeyIdentifier, ParsedExtension, X509Extension};
|
use x509_parser::extensions::{AuthorityKeyIdentifier, ParsedExtension, X509Extension};
|
||||||
use x509_parser::prelude::FromDer;
|
use x509_parser::prelude::FromDer;
|
||||||
use x509_parser::prelude::X509Version;
|
use x509_parser::prelude::X509Version;
|
||||||
use x509_parser::revocation_list::CertificateRevocationList;
|
use x509_parser::revocation_list::CertificateRevocationList;
|
||||||
use x509_parser::asn1_rs::Tag;
|
|
||||||
use x509_parser::certificate::X509Certificate;
|
use x509_parser::certificate::X509Certificate;
|
||||||
use x509_parser::x509::SubjectPublicKeyInfo;
|
use x509_parser::x509::SubjectPublicKeyInfo;
|
||||||
use x509_parser::x509::AlgorithmIdentifier;
|
use x509_parser::x509::AlgorithmIdentifier;
|
||||||
|
|
||||||
const OID_SHA256_WITH_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.11";
|
|
||||||
const OID_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35";
|
|
||||||
const OID_CRL_NUMBER: &str = "2.5.29.20";
|
|
||||||
const OID_SUBJECT_KEY_IDENTIFIER: &str = "2.5.29.14";
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
pub enum Asn1TimeEncoding {
|
|
||||||
UtcTime,
|
|
||||||
GeneralizedTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
pub struct Asn1TimeUtc {
|
|
||||||
pub utc: time::OffsetDateTime,
|
|
||||||
pub encoding: Asn1TimeEncoding,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct BigUnsigned {
|
|
||||||
/// Minimal big-endian bytes. For zero, this is `[0]`.
|
|
||||||
pub bytes_be: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BigUnsigned {
|
|
||||||
pub fn to_hex_upper(&self) -> String {
|
|
||||||
hex::encode_upper(&self.bytes_be)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_u64(&self) -> Option<u64> {
|
|
||||||
if self.bytes_be.len() > 8 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut value: u64 = 0;
|
|
||||||
for &b in &self.bytes_be {
|
|
||||||
value = (value << 8) | (b as u64);
|
|
||||||
}
|
|
||||||
Some(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct RevokedCert {
|
pub struct RevokedCert {
|
||||||
pub serial_number: BigUnsigned,
|
pub serial_number: BigUnsigned,
|
||||||
@@ -73,52 +37,52 @@ pub struct RpkixCrl {
|
|||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum CrlDecodeError {
|
pub enum CrlDecodeError {
|
||||||
#[error("X.509 CRL parse error: {0}")]
|
#[error("X.509 CRL parse error: {0} (RFC 5280 §5.1; RFC 6487 §5)")]
|
||||||
Parse(String),
|
Parse(String),
|
||||||
|
|
||||||
#[error("trailing bytes after CRL DER: {0} bytes")]
|
#[error("trailing bytes after CRL DER: {0} bytes (DER; RFC 5280 §5.1)")]
|
||||||
TrailingBytes(usize),
|
TrailingBytes(usize),
|
||||||
|
|
||||||
#[error("CRL version must be v2, got {0:?}")]
|
#[error("CRL version must be v2, got {0:?} (RFC 5280 §5.1; RFC 6487 §5)")]
|
||||||
InvalidVersion(Option<u32>),
|
InvalidVersion(Option<u32>),
|
||||||
|
|
||||||
#[error("CRL signatureAlgorithm must be sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0}")]
|
#[error("CRL signatureAlgorithm must be sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0} (RFC 6487 §5; RFC 7935 §2)")]
|
||||||
InvalidSignatureAlgorithm(String),
|
InvalidSignatureAlgorithm(String),
|
||||||
|
|
||||||
#[error("CRL signature algorithm parameters must be absent or NULL")]
|
#[error("CRL signature algorithm parameters must be absent or NULL (RFC 5280 §4.1.1.2; RFC 7935 §2)")]
|
||||||
InvalidSignatureAlgorithmParameters,
|
InvalidSignatureAlgorithmParameters,
|
||||||
|
|
||||||
#[error("CRL signatureAlgorithm must match TBSCertList.signature")]
|
#[error("CRL signatureAlgorithm must match TBSCertList.signature (RFC 5280 §5.1)")]
|
||||||
SignatureAlgorithmMismatch,
|
SignatureAlgorithmMismatch,
|
||||||
|
|
||||||
#[error("CRL extensions must be exactly two (AKI + CRLNumber), got {0}")]
|
#[error("CRL extensions must be exactly two (AKI + CRLNumber), got {0} (RFC 9829 §3.1)")]
|
||||||
InvalidExtensionsCount(usize),
|
InvalidExtensionsCount(usize),
|
||||||
|
|
||||||
#[error("unsupported CRL extension OID {0}")]
|
#[error("unsupported CRL extension OID {0} (RFC 9829 §3.1)")]
|
||||||
UnsupportedExtension(String),
|
UnsupportedExtension(String),
|
||||||
|
|
||||||
#[error("duplicate CRL extension OID {0}")]
|
#[error("duplicate CRL extension OID {0} (RFC 5280 §4.2; RFC 9829 §3.1)")]
|
||||||
DuplicateExtension(String),
|
DuplicateExtension(String),
|
||||||
|
|
||||||
#[error("AuthorityKeyIdentifier must contain keyIdentifier")]
|
#[error("AuthorityKeyIdentifier must contain keyIdentifier (RFC 5280 §5.2.1; RFC 9829 §3.1)")]
|
||||||
AkiMissingKeyIdentifier,
|
AkiMissingKeyIdentifier,
|
||||||
|
|
||||||
#[error("AuthorityKeyIdentifier must not contain authorityCertIssuer or authorityCertSerialNumber")]
|
#[error("AuthorityKeyIdentifier must not contain authorityCertIssuer or authorityCertSerialNumber (RFC 5280 §5.2.1; RFC 9829 §3.1)")]
|
||||||
AkiHasOtherFields,
|
AkiHasOtherFields,
|
||||||
|
|
||||||
#[error("CRLNumber must be non-critical")]
|
#[error("CRLNumber must be non-critical (RFC 9829 §3.1; RFC 5280 §5.2.3)")]
|
||||||
CrlNumberCritical,
|
CrlNumberCritical,
|
||||||
|
|
||||||
#[error("CRLNumber out of range (must fit in 0..2^159-1)")]
|
#[error("CRLNumber out of range (must fit in 0..2^159-1) (RFC 9829 §3.1)")]
|
||||||
CrlNumberOutOfRange,
|
CrlNumberOutOfRange,
|
||||||
|
|
||||||
#[error("CRL entry extensions must not be present")]
|
#[error("CRL entry extensions must not be present (RFC 6487 §5; RFC 5280 §5.1)")]
|
||||||
EntryExtensionsNotAllowed,
|
EntryExtensionsNotAllowed,
|
||||||
|
|
||||||
#[error("CRL nextUpdate must be present (RFC 5280 §5.1.2.5)")]
|
#[error("CRL nextUpdate must be present (RFC 5280 §5.1.2.5; RFC 6487 §5)")]
|
||||||
NextUpdateMissing,
|
NextUpdateMissing,
|
||||||
|
|
||||||
#[error("{field} time encoding invalid for year {year}: got {encoding:?}")]
|
#[error("{field} time encoding invalid for year {year}: got {encoding:?} (RFC 5280 §5.1.2.4-§5.1.2.6)")]
|
||||||
InvalidTimeEncoding {
|
InvalidTimeEncoding {
|
||||||
field: &'static str,
|
field: &'static str,
|
||||||
year: i32,
|
year: i32,
|
||||||
@@ -161,23 +125,23 @@ impl RpkixCrl {
|
|||||||
if !rc.extensions().is_empty() {
|
if !rc.extensions().is_empty() {
|
||||||
return Err(CrlDecodeError::EntryExtensionsNotAllowed);
|
return Err(CrlDecodeError::EntryExtensionsNotAllowed);
|
||||||
}
|
}
|
||||||
let revocation_date = asn1_time_to_model(rc.revocation_date);
|
let revocation_date = crate::data_model::common::asn1_time_to_model(rc.revocation_date);
|
||||||
validate_time_encoding("revocationDate", &revocation_date)?;
|
validate_time_encoding_rfc5280("revocationDate", &revocation_date)?;
|
||||||
Ok(RevokedCert {
|
Ok(RevokedCert {
|
||||||
serial_number: biguint_to_big_unsigned(rc.serial()),
|
serial_number: BigUnsigned::from_biguint(rc.serial()),
|
||||||
revocation_date,
|
revocation_date,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
let this_update = asn1_time_to_model(crl.last_update());
|
let this_update = crate::data_model::common::asn1_time_to_model(crl.last_update());
|
||||||
validate_time_encoding("thisUpdate", &this_update)?;
|
validate_time_encoding_rfc5280("thisUpdate", &this_update)?;
|
||||||
|
|
||||||
let next_update = crl
|
let next_update = crl
|
||||||
.next_update()
|
.next_update()
|
||||||
.map(asn1_time_to_model)
|
.map(crate::data_model::common::asn1_time_to_model)
|
||||||
.ok_or(CrlDecodeError::NextUpdateMissing)?;
|
.ok_or(CrlDecodeError::NextUpdateMissing)?;
|
||||||
validate_time_encoding("nextUpdate", &next_update)?;
|
validate_time_encoding_rfc5280("nextUpdate", &next_update)?;
|
||||||
|
|
||||||
Ok(RpkixCrl {
|
Ok(RpkixCrl {
|
||||||
raw_der: der.to_vec(),
|
raw_der: der.to_vec(),
|
||||||
@@ -267,53 +231,44 @@ impl RpkixCrl {
|
|||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum CrlVerifyError {
|
pub enum CrlVerifyError {
|
||||||
#[error("issuer certificate parse error: {0}")]
|
#[error("issuer certificate parse error: {0} (RFC 5280 §4.1; RFC 6487 §4)")]
|
||||||
IssuerCertificateParse(String),
|
IssuerCertificateParse(String),
|
||||||
|
|
||||||
#[error("trailing bytes after issuer certificate DER: {0} bytes")]
|
#[error("trailing bytes after issuer certificate DER: {0} bytes (DER; RFC 5280 §4.1)")]
|
||||||
IssuerCertificateTrailingBytes(usize),
|
IssuerCertificateTrailingBytes(usize),
|
||||||
|
|
||||||
#[error("issuer SubjectPublicKeyInfo parse error: {0}")]
|
#[error("issuer SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7)")]
|
||||||
IssuerSpkiParse(String),
|
IssuerSpkiParse(String),
|
||||||
|
|
||||||
#[error("trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes")]
|
#[error("trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 5280 §4.1.2.7)")]
|
||||||
IssuerSpkiTrailingBytes(usize),
|
IssuerSpkiTrailingBytes(usize),
|
||||||
|
|
||||||
#[error("CRL parse error: {0}")]
|
#[error("CRL parse error: {0} (RFC 5280 §5.1; RFC 6487 §5)")]
|
||||||
CrlParse(String),
|
CrlParse(String),
|
||||||
|
|
||||||
#[error("trailing bytes after CRL DER: {0} bytes")]
|
#[error("trailing bytes after CRL DER: {0} bytes (DER; RFC 5280 §5.1)")]
|
||||||
CrlTrailingBytes(usize),
|
CrlTrailingBytes(usize),
|
||||||
|
|
||||||
#[error("CRL issuer DN does not match issuer certificate subject")]
|
#[error("CRL issuer DN does not match issuer certificate subject (RFC 5280 §5.1; RFC 5280 §6.3.3(b))")]
|
||||||
IssuerSubjectMismatch {
|
IssuerSubjectMismatch {
|
||||||
crl_issuer_dn: String,
|
crl_issuer_dn: String,
|
||||||
issuer_subject_dn: String,
|
issuer_subject_dn: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("issuer certificate keyUsage present but missing cRLSign")]
|
#[error("issuer certificate keyUsage present but missing cRLSign (RFC 5280 §4.2.1.3; RFC 5280 §6.3.3(f))")]
|
||||||
IssuerKeyUsageMissingCrlSign,
|
IssuerKeyUsageMissingCrlSign,
|
||||||
|
|
||||||
#[error("CRL AKI.keyIdentifier does not match issuer certificate SKI")]
|
#[error("CRL AKI.keyIdentifier does not match issuer certificate SKI (RFC 5280 §4.2.1.1; RFC 5280 §4.2.1.2; RFC 5280 §6.3.3(c)/(f))")]
|
||||||
AkiSkiMismatch,
|
AkiSkiMismatch,
|
||||||
|
|
||||||
#[error("CRL signature verification failed: {0}")]
|
#[error("CRL signature verification failed: {0} (RFC 5280 §6.3.3(g); RFC 7935 §2)")]
|
||||||
InvalidSignature(String),
|
InvalidSignature(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn asn1_time_to_model(t: x509_parser::time::ASN1Time) -> Asn1TimeUtc {
|
fn validate_time_encoding_rfc5280(
|
||||||
let encoding = if t.is_utctime() {
|
field: &'static str,
|
||||||
Asn1TimeEncoding::UtcTime
|
t: &Asn1TimeUtc,
|
||||||
} else {
|
) -> Result<(), CrlDecodeError> {
|
||||||
Asn1TimeEncoding::GeneralizedTime
|
|
||||||
};
|
|
||||||
Asn1TimeUtc {
|
|
||||||
utc: t.to_datetime(),
|
|
||||||
encoding,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_time_encoding(field: &'static str, t: &Asn1TimeUtc) -> Result<(), CrlDecodeError> {
|
|
||||||
let year = t.utc.year();
|
let year = t.utc.year();
|
||||||
let expected = if year <= 2049 {
|
let expected = if year <= 2049 {
|
||||||
Asn1TimeEncoding::UtcTime
|
Asn1TimeEncoding::UtcTime
|
||||||
@@ -331,10 +286,10 @@ fn validate_time_encoding(field: &'static str, t: &Asn1TimeUtc) -> Result<(), Cr
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn validate_sig_params(sig: &AlgorithmIdentifier<'_>) -> Result<(), CrlDecodeError> {
|
fn validate_sig_params(sig: &AlgorithmIdentifier<'_>) -> Result<(), CrlDecodeError> {
|
||||||
match sig.parameters.as_ref() {
|
if crate::data_model::common::algorithm_params_absent_or_null(sig) {
|
||||||
None => Ok(()),
|
Ok(())
|
||||||
Some(p) if p.tag() == Tag::Null => Ok(()),
|
} else {
|
||||||
Some(_p) => Err(CrlDecodeError::InvalidSignatureAlgorithmParameters),
|
Err(CrlDecodeError::InvalidSignatureAlgorithmParameters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,7 +322,7 @@ fn parse_and_validate_extensions(exts: &[X509Extension<'_>]) -> Result<CrlExtens
|
|||||||
if n.bits() > 159 {
|
if n.bits() > 159 {
|
||||||
return Err(CrlDecodeError::CrlNumberOutOfRange);
|
return Err(CrlDecodeError::CrlNumberOutOfRange);
|
||||||
}
|
}
|
||||||
crl_number = Some(biguint_to_big_unsigned(&n));
|
crl_number = Some(BigUnsigned::from_biguint(&n));
|
||||||
}
|
}
|
||||||
_ => return Err(CrlDecodeError::UnsupportedExtension(oid)),
|
_ => return Err(CrlDecodeError::UnsupportedExtension(oid)),
|
||||||
}
|
}
|
||||||
@@ -410,14 +365,6 @@ fn parse_crl_number(ext: &X509Extension<'_>) -> Result<der_parser::num_bigint::B
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn biguint_to_big_unsigned(n: &der_parser::num_bigint::BigUint) -> BigUnsigned {
|
|
||||||
let mut bytes = n.to_bytes_be();
|
|
||||||
if bytes.is_empty() {
|
|
||||||
bytes.push(0);
|
|
||||||
}
|
|
||||||
BigUnsigned { bytes_be: bytes }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_subject_key_identifier(cert: &X509Certificate<'_>) -> Option<Vec<u8>> {
|
fn get_subject_key_identifier(cert: &X509Certificate<'_>) -> Option<Vec<u8>> {
|
||||||
cert.extensions()
|
cert.extensions()
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
338
src/data_model/manifest.rs
Normal file
338
src/data_model/manifest.rs
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
use crate::data_model::common::BigUnsigned;
|
||||||
|
use crate::data_model::oid::{OID_CT_RPKI_MANIFEST, OID_SHA256};
|
||||||
|
use crate::data_model::rc::ResourceCertificate;
|
||||||
|
use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError};
|
||||||
|
use der_parser::ber::BerObjectContent;
|
||||||
|
use der_parser::der::{parse_der, DerObject, Tag};
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct ManifestObject {
|
||||||
|
pub signed_object: RpkiSignedObject,
|
||||||
|
pub econtent_type: String,
|
||||||
|
pub manifest: ManifestEContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct ManifestEContent {
|
||||||
|
pub version: u32,
|
||||||
|
pub manifest_number: BigUnsigned,
|
||||||
|
pub this_update: OffsetDateTime,
|
||||||
|
pub next_update: OffsetDateTime,
|
||||||
|
pub file_hash_alg: String,
|
||||||
|
pub files: Vec<FileAndHash>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct FileAndHash {
|
||||||
|
pub file_name: String,
|
||||||
|
pub hash_bytes: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ManifestDecodeError {
|
||||||
|
#[error("signed object decode error: {0} (RFC 6488 §2-§3; RFC 9589 §4; RFC 9286 §4)")]
|
||||||
|
SignedObject(#[from] SignedObjectDecodeError),
|
||||||
|
|
||||||
|
#[error("DER parse error: {0} (RFC 9286 §4.2; DER)")]
|
||||||
|
Parse(String),
|
||||||
|
|
||||||
|
#[error("trailing bytes after DER object: {0} bytes (RFC 9286 §4.2; DER)")]
|
||||||
|
TrailingBytes(usize),
|
||||||
|
|
||||||
|
#[error("eContentType must be id-ct-rpkiManifest ({OID_CT_RPKI_MANIFEST}), got {0} (RFC 9286 §4.1; RFC 9286 §4.4(1))")]
|
||||||
|
InvalidEContentType(String),
|
||||||
|
|
||||||
|
#[error("Manifest must be a SEQUENCE of 5 or 6 elements, got {0} (RFC 9286 §4.2)")]
|
||||||
|
InvalidManifestSequenceLen(usize),
|
||||||
|
|
||||||
|
#[error("Manifest.version must be 0, got {0} (RFC 9286 §4.2.1)")]
|
||||||
|
InvalidManifestVersion(u64),
|
||||||
|
|
||||||
|
#[error("Manifest.manifestNumber must be non-negative INTEGER (RFC 9286 §4.2; RFC 9286 §4.2.1)")]
|
||||||
|
InvalidManifestNumber,
|
||||||
|
|
||||||
|
#[error("Manifest.manifestNumber longer than 20 octets (RFC 9286 §4.2.1)")]
|
||||||
|
ManifestNumberTooLong,
|
||||||
|
|
||||||
|
#[error("Manifest.thisUpdate must be GeneralizedTime (RFC 9286 §4.2)")]
|
||||||
|
InvalidThisUpdate,
|
||||||
|
|
||||||
|
#[error("Manifest.nextUpdate must be GeneralizedTime (RFC 9286 §4.2)")]
|
||||||
|
InvalidNextUpdate,
|
||||||
|
|
||||||
|
#[error("Manifest.nextUpdate must be later than thisUpdate (RFC 9286 §4.2.1)")]
|
||||||
|
NextUpdateNotLater,
|
||||||
|
|
||||||
|
#[error("Manifest.fileHashAlg must be id-sha256 ({OID_SHA256}), got {0} (RFC 9286 §4.2.1; RFC 7935 §2)")]
|
||||||
|
InvalidFileHashAlg(String),
|
||||||
|
|
||||||
|
#[error("Manifest.fileList must be a SEQUENCE (RFC 9286 §4.2)")]
|
||||||
|
InvalidFileList,
|
||||||
|
|
||||||
|
#[error("FileAndHash must be SEQUENCE of 2 (RFC 9286 §4.2)")]
|
||||||
|
InvalidFileAndHash,
|
||||||
|
|
||||||
|
#[error("fileList file name invalid: {0} (RFC 9286 §4.2.2)")]
|
||||||
|
InvalidFileName(String),
|
||||||
|
|
||||||
|
#[error("fileList hash must be BIT STRING (RFC 9286 §4.2)")]
|
||||||
|
InvalidHashType,
|
||||||
|
|
||||||
|
#[error("fileList hash BIT STRING must be octet-aligned (unused bits=0) (RFC 9286 §4.2.1; DER BIT STRING)")]
|
||||||
|
HashNotOctetAligned,
|
||||||
|
|
||||||
|
#[error("fileList hash length invalid for sha256: got {0} bytes (RFC 9286 §4.2.1; RFC 7935 §2)")]
|
||||||
|
InvalidHashLength(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ManifestValidateError {
|
||||||
|
#[error("Manifest EE certificate MUST include at least one RFC 3779 resource extension (IP or AS) (RFC 6487 §4.8.10-§4.8.11; RFC 3779; RFC 9286 §5.1)")]
|
||||||
|
EeResourcesMissing,
|
||||||
|
|
||||||
|
#[error("Manifest EE certificate IP resources MUST use inherit only (RFC 9286 §5.1; RFC 3779 §2.2.3.5)")]
|
||||||
|
EeIpResourcesNotInherit,
|
||||||
|
|
||||||
|
#[error("Manifest EE certificate AS resources MUST use inherit only (RFC 9286 §5.1; RFC 3779 §3.2.3.3)")]
|
||||||
|
EeAsResourcesNotInherit,
|
||||||
|
|
||||||
|
#[error("Manifest EE certificate AS resources rdi MUST be absent (RFC 6487 §4.8.11; RFC 3779 §3.2.3.5)")]
|
||||||
|
EeAsResourcesRdiPresent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ManifestObject {
|
||||||
|
pub fn decode_der(der: &[u8]) -> Result<Self, ManifestDecodeError> {
|
||||||
|
let signed_object = RpkiSignedObject::decode_der(der)?;
|
||||||
|
Self::from_signed_object(signed_object)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result<Self, ManifestDecodeError> {
|
||||||
|
let econtent_type = signed_object
|
||||||
|
.signed_data
|
||||||
|
.encap_content_info
|
||||||
|
.econtent_type
|
||||||
|
.clone();
|
||||||
|
if econtent_type != OID_CT_RPKI_MANIFEST {
|
||||||
|
return Err(ManifestDecodeError::InvalidEContentType(econtent_type));
|
||||||
|
}
|
||||||
|
let manifest = ManifestEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?;
|
||||||
|
Ok(Self {
|
||||||
|
signed_object,
|
||||||
|
econtent_type: OID_CT_RPKI_MANIFEST.to_string(),
|
||||||
|
manifest,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate the embedded EE certificate resources against RFC 9286 §5.1.
|
||||||
|
///
|
||||||
|
/// This does **not** perform certificate path validation. It assumes `ee` is a parsed and
|
||||||
|
/// profile-validated RPKI EE resource certificate.
|
||||||
|
pub fn validate_against_ee_cert(
|
||||||
|
&self,
|
||||||
|
ee: &ResourceCertificate,
|
||||||
|
) -> Result<(), ManifestValidateError> {
|
||||||
|
let ip = ee.tbs.extensions.ip_resources.as_ref();
|
||||||
|
let asn = ee.tbs.extensions.as_resources.as_ref();
|
||||||
|
if ip.is_none() && asn.is_none() {
|
||||||
|
return Err(ManifestValidateError::EeResourcesMissing);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ip) = ip {
|
||||||
|
if !ip.is_all_inherit() {
|
||||||
|
return Err(ManifestValidateError::EeIpResourcesNotInherit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(asn) = asn {
|
||||||
|
if asn.rdi.is_some() {
|
||||||
|
return Err(ManifestValidateError::EeAsResourcesRdiPresent);
|
||||||
|
}
|
||||||
|
if !asn.is_asnum_inherit() {
|
||||||
|
return Err(ManifestValidateError::EeAsResourcesNotInherit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate this manifest's embedded EE certificate resources.
|
||||||
|
pub fn validate_embedded_ee_cert(&self) -> Result<(), ManifestValidateError> {
|
||||||
|
let ee = &self.signed_object.signed_data.certificates[0].resource_cert;
|
||||||
|
self.validate_against_ee_cert(ee)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ManifestEContent {
|
||||||
|
/// Decode the DER-encoded Manifest eContent defined in RFC 9286 §4.2.
|
||||||
|
pub fn decode_der(der: &[u8]) -> Result<Self, ManifestDecodeError> {
|
||||||
|
let (rem, obj) = parse_der(der).map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
|
||||||
|
if !rem.is_empty() {
|
||||||
|
return Err(ManifestDecodeError::TrailingBytes(rem.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let seq = obj
|
||||||
|
.as_sequence()
|
||||||
|
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
|
||||||
|
if seq.len() != 5 && seq.len() != 6 {
|
||||||
|
return Err(ManifestDecodeError::InvalidManifestSequenceLen(seq.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut idx = 0;
|
||||||
|
let mut version: u32 = 0;
|
||||||
|
if seq.len() == 6 {
|
||||||
|
let v_obj = &seq[0];
|
||||||
|
if v_obj.class() != der_parser::ber::Class::ContextSpecific || v_obj.tag() != Tag(0) {
|
||||||
|
return Err(ManifestDecodeError::Parse(
|
||||||
|
"Manifest.version must be [0] EXPLICIT INTEGER".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let inner_der = v_obj
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
|
||||||
|
let (rem, inner) =
|
||||||
|
parse_der(inner_der).map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
|
||||||
|
if !rem.is_empty() {
|
||||||
|
return Err(ManifestDecodeError::Parse(
|
||||||
|
"trailing bytes inside Manifest.version".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let v = inner
|
||||||
|
.as_u64()
|
||||||
|
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
|
||||||
|
if v != 0 {
|
||||||
|
return Err(ManifestDecodeError::InvalidManifestVersion(v));
|
||||||
|
}
|
||||||
|
version = 0;
|
||||||
|
idx = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let manifest_number = parse_manifest_number(&seq[idx])?;
|
||||||
|
idx += 1;
|
||||||
|
|
||||||
|
let this_update =
|
||||||
|
parse_generalized_time(&seq[idx], ManifestDecodeError::InvalidThisUpdate)?;
|
||||||
|
idx += 1;
|
||||||
|
let next_update =
|
||||||
|
parse_generalized_time(&seq[idx], ManifestDecodeError::InvalidNextUpdate)?;
|
||||||
|
idx += 1;
|
||||||
|
if next_update <= this_update {
|
||||||
|
return Err(ManifestDecodeError::NextUpdateNotLater);
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_hash_alg = oid_to_string(&seq[idx])?;
|
||||||
|
idx += 1;
|
||||||
|
if file_hash_alg != OID_SHA256 {
|
||||||
|
return Err(ManifestDecodeError::InvalidFileHashAlg(file_hash_alg));
|
||||||
|
}
|
||||||
|
|
||||||
|
let files = parse_file_list_sha256(&seq[idx])?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
version,
|
||||||
|
manifest_number,
|
||||||
|
this_update,
|
||||||
|
next_update,
|
||||||
|
file_hash_alg: OID_SHA256.to_string(),
|
||||||
|
files,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_manifest_number(obj: &DerObject<'_>) -> Result<BigUnsigned, ManifestDecodeError> {
|
||||||
|
let n = obj
|
||||||
|
.as_biguint()
|
||||||
|
.map_err(|_e| ManifestDecodeError::InvalidManifestNumber)?;
|
||||||
|
let out = BigUnsigned::from_biguint(&n);
|
||||||
|
if out.bytes_be.len() > 20 {
|
||||||
|
return Err(ManifestDecodeError::ManifestNumberTooLong);
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_generalized_time(
|
||||||
|
obj: &DerObject<'_>,
|
||||||
|
err: ManifestDecodeError,
|
||||||
|
) -> Result<OffsetDateTime, ManifestDecodeError> {
|
||||||
|
match &obj.content {
|
||||||
|
BerObjectContent::GeneralizedTime(dt) => dt
|
||||||
|
.to_datetime()
|
||||||
|
.map_err(|e| ManifestDecodeError::Parse(e.to_string())),
|
||||||
|
_ => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_file_list_sha256(obj: &DerObject<'_>) -> Result<Vec<FileAndHash>, ManifestDecodeError> {
|
||||||
|
let seq = obj
|
||||||
|
.as_sequence()
|
||||||
|
.map_err(|_e| ManifestDecodeError::InvalidFileList)?;
|
||||||
|
let mut out = Vec::with_capacity(seq.len());
|
||||||
|
for entry in seq {
|
||||||
|
let (file_name, hash_bytes) = parse_file_and_hash(entry)?;
|
||||||
|
validate_file_name(&file_name)?;
|
||||||
|
if hash_bytes.len() != 32 {
|
||||||
|
return Err(ManifestDecodeError::InvalidHashLength(hash_bytes.len()));
|
||||||
|
}
|
||||||
|
out.push(FileAndHash {
|
||||||
|
file_name,
|
||||||
|
hash_bytes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_file_and_hash(obj: &DerObject<'_>) -> Result<(String, Vec<u8>), ManifestDecodeError> {
|
||||||
|
let seq = obj
|
||||||
|
.as_sequence()
|
||||||
|
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
|
||||||
|
if seq.len() != 2 {
|
||||||
|
return Err(ManifestDecodeError::InvalidFileAndHash);
|
||||||
|
}
|
||||||
|
let file_name = seq[0]
|
||||||
|
.as_str()
|
||||||
|
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?
|
||||||
|
.to_string();
|
||||||
|
let (unused_bits, bits) = match &seq[1].content {
|
||||||
|
BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()),
|
||||||
|
_ => return Err(ManifestDecodeError::InvalidHashType),
|
||||||
|
};
|
||||||
|
if unused_bits != 0 {
|
||||||
|
return Err(ManifestDecodeError::HashNotOctetAligned);
|
||||||
|
}
|
||||||
|
Ok((file_name, bits))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_file_name(name: &str) -> Result<(), ManifestDecodeError> {
|
||||||
|
// RFC 9286 §4.2.2:
|
||||||
|
// 1+ chars from a-zA-Z0-9-_ , then '.', then 3-letter extension.
|
||||||
|
let Some((base, ext)) = name.rsplit_once('.') else {
|
||||||
|
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
|
||||||
|
};
|
||||||
|
if base.is_empty() || ext.len() != 3 {
|
||||||
|
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
|
||||||
|
}
|
||||||
|
if !base
|
||||||
|
.bytes()
|
||||||
|
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
|
||||||
|
{
|
||||||
|
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
|
||||||
|
}
|
||||||
|
if !ext.bytes().all(|b| b.is_ascii_alphabetic()) {
|
||||||
|
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
|
||||||
|
}
|
||||||
|
let ext_lower = ext.to_ascii_lowercase();
|
||||||
|
if !crate::data_model::common::IANA_RPKI_REPOSITORY_FILENAME_EXTENSIONS
|
||||||
|
.iter()
|
||||||
|
.any(|&e| e == ext_lower)
|
||||||
|
{
|
||||||
|
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oid_to_string(obj: &DerObject<'_>) -> Result<String, ManifestDecodeError> {
|
||||||
|
let oid = obj
|
||||||
|
.as_oid()
|
||||||
|
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
|
||||||
|
Ok(oid.to_id_string())
|
||||||
|
}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
|
pub mod common;
|
||||||
pub mod crl;
|
pub mod crl;
|
||||||
mod rc;
|
pub mod rc;
|
||||||
mod tal;
|
pub mod oid;
|
||||||
mod ta;
|
pub mod signed_object;
|
||||||
pub mod resources;
|
pub mod manifest;
|
||||||
mod oids;
|
pub mod roa;
|
||||||
|
pub mod aspa;
|
||||||
|
pub mod tal;
|
||||||
|
pub mod ta;
|
||||||
|
|||||||
42
src/data_model/oid.rs
Normal file
42
src/data_model/oid.rs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
pub const OID_SHA256: &str = "2.16.840.1.101.3.4.2.1";
|
||||||
|
|
||||||
|
pub const OID_SIGNED_DATA: &str = "1.2.840.113549.1.7.2";
|
||||||
|
|
||||||
|
pub const OID_CMS_ATTR_CONTENT_TYPE: &str = "1.2.840.113549.1.9.3";
|
||||||
|
pub const OID_CMS_ATTR_MESSAGE_DIGEST: &str = "1.2.840.113549.1.9.4";
|
||||||
|
pub const OID_CMS_ATTR_SIGNING_TIME: &str = "1.2.840.113549.1.9.5";
|
||||||
|
|
||||||
|
pub const OID_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.1";
|
||||||
|
pub const OID_SHA256_WITH_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.11";
|
||||||
|
|
||||||
|
// X.509 extensions (RFC 5280 / RFC 6487)
|
||||||
|
pub const OID_BASIC_CONSTRAINTS: &str = "2.5.29.19";
|
||||||
|
pub const OID_KEY_USAGE: &str = "2.5.29.15";
|
||||||
|
pub const OID_EXTENDED_KEY_USAGE: &str = "2.5.29.37";
|
||||||
|
pub const OID_CRL_DISTRIBUTION_POINTS: &str = "2.5.29.31";
|
||||||
|
pub const OID_AUTHORITY_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.1";
|
||||||
|
pub const OID_CERTIFICATE_POLICIES: &str = "2.5.29.32";
|
||||||
|
|
||||||
|
pub const OID_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35";
|
||||||
|
pub const OID_CRL_NUMBER: &str = "2.5.29.20";
|
||||||
|
pub const OID_SUBJECT_KEY_IDENTIFIER: &str = "2.5.29.14";
|
||||||
|
|
||||||
|
pub const OID_CT_RPKI_MANIFEST: &str = "1.2.840.113549.1.9.16.1.26";
|
||||||
|
pub const OID_CT_ROUTE_ORIGIN_AUTHZ: &str = "1.2.840.113549.1.9.16.1.24";
|
||||||
|
pub const OID_CT_ASPA: &str = "1.2.840.113549.1.9.16.1.49";
|
||||||
|
|
||||||
|
// X.509 extensions / access methods (RFC 5280 / RFC 6487)
|
||||||
|
pub const OID_SUBJECT_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.11";
|
||||||
|
pub const OID_AD_SIGNED_OBJECT: &str = "1.3.6.1.5.5.7.48.11";
|
||||||
|
|
||||||
|
pub const OID_AD_CA_ISSUERS: &str = "1.3.6.1.5.5.7.48.2";
|
||||||
|
pub const OID_AD_CA_REPOSITORY: &str = "1.3.6.1.5.5.7.48.5";
|
||||||
|
pub const OID_AD_RPKI_MANIFEST: &str = "1.3.6.1.5.5.7.48.10";
|
||||||
|
pub const OID_AD_RPKI_NOTIFY: &str = "1.3.6.1.5.5.7.48.13";
|
||||||
|
|
||||||
|
// RFC 3779 resource extensions (RFC 6487 profile)
|
||||||
|
pub const OID_IP_ADDR_BLOCKS: &str = "1.3.6.1.5.5.7.1.7";
|
||||||
|
pub const OID_AUTONOMOUS_SYS_IDS: &str = "1.3.6.1.5.5.7.1.8";
|
||||||
|
|
||||||
|
// RPKI CP (RFC 6484 / RFC 6487)
|
||||||
|
pub const OID_CP_IPADDR_ASNUMBER: &str = "1.3.6.1.5.5.7.14.2";
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
pub const OID_BASIC_CONSTRAINTS: &str = "2.5.29.19";
|
|
||||||
pub const OID_SUBJECT_KEY_IDENTIFIER: &str = "2.5.29.14";
|
|
||||||
pub const OID_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35";
|
|
||||||
pub const OID_KEY_USAGE: &str = "2.5.29.15";
|
|
||||||
pub const OID_EXTENDED_KEY_USAGE: &str = "2.5.29.37";
|
|
||||||
pub const OID_CRL_DISTRIBUTION_POINTS: &str = "2.5.29.31";
|
|
||||||
pub const OID_AUTHORITY_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.1";
|
|
||||||
pub const OID_ACCESS_DESCRIPTION: &str = "1.3.6.1.5.5.7.48";
|
|
||||||
pub const OID_AD_CA_ISSUERS: &str = "1.3.6.1.5.5.7.48.2";
|
|
||||||
pub const OID_AD_OCSP: &str = "1.3.6.1.5.5.7.48.1";
|
|
||||||
pub const OID_SUBJECT_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.11";
|
|
||||||
pub const OID_CERTIFICATE_POLICIES: &str = "2.5.29.32";
|
|
||||||
pub const OID_SHA256_WITH_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.11";
|
|
||||||
pub const OID_IP_ADDRESS_BLOCKS: &str = "1.3.6.1.5.5.7.1.7";
|
|
||||||
pub const OID_AS_IDENTIFIERS: &str = "1.3.6.1.5.5.7.1.8";
|
|
||||||
pub const OID_RPKI_CP: &str = "1.3.6.1.5.5.7.14.2";
|
|
||||||
1459
src/data_model/rc.rs
1459
src/data_model/rc.rs
File diff suppressed because it is too large
Load Diff
@@ -1,91 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct ASIdentifiers {
|
|
||||||
pub asn: Vec<ASIdentifierChoice>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ASN
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum ASIdentifierChoice {
|
|
||||||
Inherit,
|
|
||||||
ASIDsOrRanges(Vec<ASIDOrRange>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum ASIDOrRange {
|
|
||||||
Id(Asn),
|
|
||||||
AsRange(ASRange),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct ASRange {
|
|
||||||
pub min: Asn,
|
|
||||||
pub max: Asn,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ASRange {
|
|
||||||
/// Creates a new AS number range from the smallest and largest number.
|
|
||||||
pub fn new(min: Asn, max: Asn) -> Self {
|
|
||||||
ASRange { min, max }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an AS block covering all ASNs.
|
|
||||||
pub fn all() -> ASRange {
|
|
||||||
ASRange::new(Asn::MIN, Asn::MAX)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the smallest AS number that is part of this range.
|
|
||||||
pub fn min(self) -> Asn {
|
|
||||||
self.min
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the largest AS number that is still part of this range.
|
|
||||||
pub fn max(self) -> Asn {
|
|
||||||
self.max
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the number of ASNs covered by this value.
|
|
||||||
pub fn asn_count(self) -> u32 {
|
|
||||||
u32::from(self.max) - u32::from(self.min) + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Default, Serialize, Deserialize)]
|
|
||||||
pub struct Asn(u32);
|
|
||||||
|
|
||||||
impl Asn {
|
|
||||||
pub const MIN: Asn = Asn(u32::MIN);
|
|
||||||
pub const MAX: Asn = Asn(u32::MAX);
|
|
||||||
|
|
||||||
/// Creates an AS number from a `u32`.
|
|
||||||
pub fn from_u32(value: u32) -> Self {
|
|
||||||
Asn(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts an AS number into a `u32`.
|
|
||||||
pub fn into_u32(self) -> u32 {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts an AS number into a network-order byte array.
|
|
||||||
pub fn to_raw(self) -> [u8; 4] {
|
|
||||||
self.0.to_be_bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<u32> for Asn {
|
|
||||||
fn from(id: u32) -> Self {
|
|
||||||
Asn(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Asn> for u32 {
|
|
||||||
fn from(id: Asn) -> Self {
|
|
||||||
id.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct IPAddrBlocks {
|
|
||||||
pub ips: Vec<IPAddressFamily>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// IP Address Family
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct IPAddressFamily {
|
|
||||||
pub address_family: Afi,
|
|
||||||
pub ip_address_choice: IPAddressChoice,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
|
|
||||||
pub enum Afi {
|
|
||||||
Ipv4,
|
|
||||||
Ipv6,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub enum IPAddressChoice {
|
|
||||||
Inherit,
|
|
||||||
AddressOrRange(Vec<IPAddressOrRange>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub enum IPAddressOrRange {
|
|
||||||
AddressPrefix(IPAddressPrefix),
|
|
||||||
AddressRange(IPAddressRange),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
|
|
||||||
pub struct IPAddressPrefix {
|
|
||||||
pub address: IPAddress,
|
|
||||||
pub prefix_length: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IPAddressPrefix {
|
|
||||||
pub fn new(address: IPAddress, prefix_length: u8) -> Self {
|
|
||||||
Self {
|
|
||||||
address,
|
|
||||||
prefix_length,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_ipv4(&self) -> bool {
|
|
||||||
self.address.is_ipv4()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_ipv6(&self) -> bool {
|
|
||||||
self.address.is_ipv6()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn afi(&self) -> Afi {
|
|
||||||
self.address.afi()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn address(&self) -> IPAddress {
|
|
||||||
self.address
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prefix_length(&self) -> u8 {
|
|
||||||
self.prefix_length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
|
|
||||||
pub struct IPAddressRange {
|
|
||||||
pub min: IPAddress,
|
|
||||||
pub max: IPAddress,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IPAddressRange {
|
|
||||||
pub fn new(min: IPAddress, max: IPAddress) -> Self {
|
|
||||||
Self { min, max }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn min(&self) -> IPAddress {
|
|
||||||
self.min
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn max(&self) -> IPAddress {
|
|
||||||
self.max
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
|
|
||||||
pub enum IPAddress {
|
|
||||||
V4(Ipv4Addr),
|
|
||||||
V6(Ipv6Addr),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IPAddress {
|
|
||||||
pub fn from_ipv4(addr: Ipv4Addr) -> Self {
|
|
||||||
Self::V4(addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_ipv6(addr: Ipv6Addr) -> Self {
|
|
||||||
Self::V6(addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_ipv4(self) -> Option<Ipv4Addr> {
|
|
||||||
match self {
|
|
||||||
Self::V4(addr) => Some(addr),
|
|
||||||
Self::V6(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_ipv6(self) -> Option<Ipv6Addr> {
|
|
||||||
match self {
|
|
||||||
Self::V4(_) => None,
|
|
||||||
Self::V6(addr) => Some(addr),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_ipv4(self) -> bool {
|
|
||||||
matches!(self, Self::V4(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_ipv6(self) -> bool {
|
|
||||||
matches!(self, Self::V6(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn afi(self) -> Afi {
|
|
||||||
match self {
|
|
||||||
Self::V4(_) => Afi::Ipv4,
|
|
||||||
Self::V6(_) => Afi::Ipv6,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the numeric address value.
|
|
||||||
///
|
|
||||||
/// For IPv4, this is the 32-bit address widened to u128.
|
|
||||||
/// For IPv6, this is the full 128-bit address.
|
|
||||||
pub fn as_u128(self) -> u128 {
|
|
||||||
match self {
|
|
||||||
Self::V4(addr) => u32::from(addr) as u128,
|
|
||||||
Self::V6(addr) => u128::from(addr),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_v4_u32(self) -> Option<u32> {
|
|
||||||
match self {
|
|
||||||
Self::V4(addr) => Some(u32::from(addr)),
|
|
||||||
Self::V6(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_v6_u128(self) -> Option<u128> {
|
|
||||||
match self {
|
|
||||||
Self::V4(_) => None,
|
|
||||||
Self::V6(addr) => Some(u128::from(addr)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
pub mod ip_resources;
|
|
||||||
pub mod as_resources;
|
|
||||||
pub mod resource;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
use crate::data_model::resources::as_resources::ASIdentifiers;
|
|
||||||
use crate::data_model::resources::ip_resources::IPAddrBlocks;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct ResourceSet {
|
|
||||||
ip_addr_blocks: IPAddrBlocks,
|
|
||||||
as_identifiers: ASIdentifiers,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
447
src/data_model/roa.rs
Normal file
447
src/data_model/roa.rs
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
use crate::data_model::oid::OID_CT_ROUTE_ORIGIN_AUTHZ;
|
||||||
|
use crate::data_model::rc::{Afi as RcAfi, IpPrefix as RcIpPrefix, ResourceCertificate};
|
||||||
|
use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError};
|
||||||
|
use der_parser::ber::{BerObjectContent, Class};
|
||||||
|
use der_parser::der::{parse_der, DerObject, Tag};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct RoaObject {
|
||||||
|
pub signed_object: RpkiSignedObject,
|
||||||
|
pub econtent_type: String,
|
||||||
|
pub roa: RoaEContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct RoaEContent {
|
||||||
|
pub version: u32,
|
||||||
|
pub as_id: u32,
|
||||||
|
pub ip_addr_blocks: Vec<RoaIpAddressFamily>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum RoaDecodeError {
|
||||||
|
#[error("SignedObject decode error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")]
|
||||||
|
SignedObjectDecode(#[from] SignedObjectDecodeError),
|
||||||
|
|
||||||
|
#[error("ROA eContentType must be {OID_CT_ROUTE_ORIGIN_AUTHZ}, got {0} (RFC 9582 §3)")]
|
||||||
|
InvalidEContentType(String),
|
||||||
|
|
||||||
|
#[error("ROA parse error: {0} (RFC 9582 §4; DER)")]
|
||||||
|
Parse(String),
|
||||||
|
|
||||||
|
#[error("ROA trailing bytes: {0} bytes (RFC 9582 §4; DER)")]
|
||||||
|
TrailingBytes(usize),
|
||||||
|
|
||||||
|
#[error("RouteOriginAttestation must be a SEQUENCE of 2 or 3 elements, got {0} (RFC 9582 §4)")]
|
||||||
|
InvalidAttestationSequenceLen(usize),
|
||||||
|
|
||||||
|
#[error("ROA version must be 0, got {0} (RFC 9582 §4.1)")]
|
||||||
|
InvalidVersion(u64),
|
||||||
|
|
||||||
|
#[error("ROA asID out of range (0..=4294967295), got {0} (RFC 9582 §4.2)")]
|
||||||
|
AsIdOutOfRange(u64),
|
||||||
|
|
||||||
|
#[error("ROA ipAddrBlocks must have length 1..2, got {0} (RFC 9582 §4; RFC 9582 §4.3.1)")]
|
||||||
|
InvalidIpAddrBlocksLen(usize),
|
||||||
|
|
||||||
|
#[error("ROAIPAddressFamily must be a SEQUENCE of 2 elements (RFC 9582 §4.3.1)")]
|
||||||
|
InvalidIpAddressFamily,
|
||||||
|
|
||||||
|
#[error("ROA addressFamily must be an OCTET STRING of 2 bytes (RFC 9582 §4.3.1)")]
|
||||||
|
InvalidAddressFamily,
|
||||||
|
|
||||||
|
#[error("ROA addressFamily AFI not supported: {0:02X?} (RFC 9582 §4.3.1)")]
|
||||||
|
UnsupportedAfi(Vec<u8>),
|
||||||
|
|
||||||
|
#[error("ROA contains duplicate AFI {0:?} (RFC 9582 §4.3.1)")]
|
||||||
|
DuplicateAfi(RoaAfi),
|
||||||
|
|
||||||
|
#[error("ROAAddresses must have at least one entry (RFC 9582 §4.3.2)")]
|
||||||
|
EmptyAddressList,
|
||||||
|
|
||||||
|
#[error("ROAIPAddress must be a SEQUENCE of 1..2 elements (RFC 9582 §4.3.2)")]
|
||||||
|
InvalidRoaIpAddress,
|
||||||
|
|
||||||
|
#[error("ROAIPAddress.address must be a BIT STRING (RFC 9582 §4.3.2.1; RFC 3779 §2.2.3.8)")]
|
||||||
|
InvalidPrefixBitString,
|
||||||
|
|
||||||
|
#[error("ROAIPAddress.address has invalid unused bits encoding (RFC 9582 §4.3.2.1; RFC 3779 §2.2.3.8)")]
|
||||||
|
InvalidPrefixUnusedBits,
|
||||||
|
|
||||||
|
#[error("ROAIPAddress.address prefix length {prefix_len} out of range for {afi:?} (RFC 9582 §4.3.2.1)")]
|
||||||
|
PrefixLenOutOfRange { afi: RoaAfi, prefix_len: u16 },
|
||||||
|
|
||||||
|
#[error("ROAIPAddress.maxLength out of range for {afi:?}: prefix_len={prefix_len}, max_len={max_len} (RFC 9582 §4.3.2.2)")]
|
||||||
|
InvalidMaxLength {
|
||||||
|
afi: RoaAfi,
|
||||||
|
prefix_len: u16,
|
||||||
|
max_len: u16,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum RoaValidateError {
|
||||||
|
#[error("ROA EE certificate must not contain AS resources extension (RFC 9582 §5)")]
|
||||||
|
EeAsResourcesPresent,
|
||||||
|
|
||||||
|
#[error("ROA EE certificate must contain IP resources extension (RFC 9582 §5)")]
|
||||||
|
EeIpResourcesMissing,
|
||||||
|
|
||||||
|
#[error("ROA EE certificate IP resources must not use inherit (RFC 9582 §5)")]
|
||||||
|
EeIpResourcesInherit,
|
||||||
|
|
||||||
|
#[error("ROA prefix not covered by EE certificate IP resources: {afi:?} {addr:?}/{prefix_len} (RFC 9582 §5; RFC 3779 §2.3)")]
|
||||||
|
PrefixNotInEeResources {
|
||||||
|
afi: RoaAfi,
|
||||||
|
addr: Vec<u8>,
|
||||||
|
prefix_len: u16,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RoaObject {
|
||||||
|
pub fn decode_der(der: &[u8]) -> Result<Self, RoaDecodeError> {
|
||||||
|
let signed_object = RpkiSignedObject::decode_der(der)?;
|
||||||
|
Self::from_signed_object(signed_object)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result<Self, RoaDecodeError> {
|
||||||
|
let econtent_type = signed_object
|
||||||
|
.signed_data
|
||||||
|
.encap_content_info
|
||||||
|
.econtent_type
|
||||||
|
.clone();
|
||||||
|
if econtent_type != OID_CT_ROUTE_ORIGIN_AUTHZ {
|
||||||
|
return Err(RoaDecodeError::InvalidEContentType(econtent_type));
|
||||||
|
}
|
||||||
|
|
||||||
|
let roa = RoaEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?;
|
||||||
|
Ok(Self {
|
||||||
|
roa,
|
||||||
|
signed_object,
|
||||||
|
econtent_type: OID_CT_ROUTE_ORIGIN_AUTHZ.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate this ROA's embedded EE certificate resources.
|
||||||
|
pub fn validate_embedded_ee_cert(&self) -> Result<(), RoaValidateError> {
|
||||||
|
let ee = &self.signed_object.signed_data.certificates[0].resource_cert;
|
||||||
|
self.roa.validate_against_ee_cert(ee)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
pub enum RoaAfi {
|
||||||
|
Ipv4,
|
||||||
|
Ipv6,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RoaAfi {
|
||||||
|
fn ub(self) -> u16 {
|
||||||
|
match self {
|
||||||
|
RoaAfi::Ipv4 => 32,
|
||||||
|
RoaAfi::Ipv6 => 128,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct RoaIpAddressFamily {
|
||||||
|
pub afi: RoaAfi,
|
||||||
|
pub addresses: Vec<RoaIpAddress>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct RoaIpAddress {
|
||||||
|
pub prefix: IpPrefix,
|
||||||
|
pub max_length: Option<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct IpPrefix {
|
||||||
|
pub afi: RoaAfi,
|
||||||
|
/// Prefix length in bits.
|
||||||
|
pub prefix_len: u16,
|
||||||
|
/// Network order address bytes (IPv4 4 bytes / IPv6 16 bytes), with host bits cleared.
|
||||||
|
pub addr: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RoaEContent {
|
||||||
|
/// Decode the DER-encoded RouteOriginAttestation defined in RFC 9582 §4.
|
||||||
|
pub fn decode_der(der: &[u8]) -> Result<Self, RoaDecodeError> {
|
||||||
|
let (rem, obj) = parse_der(der).map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||||
|
if !rem.is_empty() {
|
||||||
|
return Err(RoaDecodeError::TrailingBytes(rem.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let seq = obj
|
||||||
|
.as_sequence()
|
||||||
|
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||||
|
if seq.len() != 2 && seq.len() != 3 {
|
||||||
|
return Err(RoaDecodeError::InvalidAttestationSequenceLen(seq.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut idx = 0;
|
||||||
|
let mut version: u32 = 0;
|
||||||
|
if seq.len() == 3 {
|
||||||
|
let v_obj = &seq[0];
|
||||||
|
if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) {
|
||||||
|
return Err(RoaDecodeError::Parse(
|
||||||
|
"RouteOriginAttestation.version must be [0] EXPLICIT INTEGER".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let inner_der = v_obj
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||||
|
let (rem, inner) =
|
||||||
|
parse_der(inner_der).map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||||
|
if !rem.is_empty() {
|
||||||
|
return Err(RoaDecodeError::Parse(
|
||||||
|
"trailing bytes inside RouteOriginAttestation.version".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let v = inner
|
||||||
|
.as_u64()
|
||||||
|
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||||
|
if v != 0 {
|
||||||
|
return Err(RoaDecodeError::InvalidVersion(v));
|
||||||
|
}
|
||||||
|
version = 0;
|
||||||
|
idx = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let as_id_u64 = seq[idx]
|
||||||
|
.as_u64()
|
||||||
|
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||||
|
if as_id_u64 > u32::MAX as u64 {
|
||||||
|
return Err(RoaDecodeError::AsIdOutOfRange(as_id_u64));
|
||||||
|
}
|
||||||
|
let as_id = as_id_u64 as u32;
|
||||||
|
idx += 1;
|
||||||
|
|
||||||
|
let ip_addr_blocks = parse_ip_addr_blocks(&seq[idx])?;
|
||||||
|
|
||||||
|
let mut out = Self {
|
||||||
|
version,
|
||||||
|
as_id,
|
||||||
|
ip_addr_blocks,
|
||||||
|
};
|
||||||
|
out.canonicalize();
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn canonicalize(&mut self) {
|
||||||
|
self.ip_addr_blocks.sort_by_key(|f| f.afi);
|
||||||
|
for fam in &mut self.ip_addr_blocks {
|
||||||
|
fam.addresses.sort();
|
||||||
|
fam.addresses.dedup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate ROA payload against the embedded EE resource certificate (RFC 9582 §5).
|
||||||
|
///
|
||||||
|
/// This performs the EE/payload semantic checks that do not require certificate path
|
||||||
|
/// validation.
|
||||||
|
pub fn validate_against_ee_cert(&self, ee: &ResourceCertificate) -> Result<(), RoaValidateError> {
|
||||||
|
if ee.tbs.extensions.as_resources.is_some() {
|
||||||
|
return Err(RoaValidateError::EeAsResourcesPresent);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ip = ee
|
||||||
|
.tbs
|
||||||
|
.extensions
|
||||||
|
.ip_resources
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(RoaValidateError::EeIpResourcesMissing)?;
|
||||||
|
|
||||||
|
if ip.has_any_inherit() {
|
||||||
|
return Err(RoaValidateError::EeIpResourcesInherit);
|
||||||
|
}
|
||||||
|
|
||||||
|
for fam in &self.ip_addr_blocks {
|
||||||
|
for entry in &fam.addresses {
|
||||||
|
let rc_prefix = roa_prefix_to_rc(&entry.prefix);
|
||||||
|
if !ip.contains_prefix(&rc_prefix) {
|
||||||
|
return Err(RoaValidateError::PrefixNotInEeResources {
|
||||||
|
afi: entry.prefix.afi,
|
||||||
|
addr: entry.prefix.addr.clone(),
|
||||||
|
prefix_len: entry.prefix.prefix_len,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn roa_prefix_to_rc(p: &IpPrefix) -> RcIpPrefix {
|
||||||
|
let afi = match p.afi {
|
||||||
|
RoaAfi::Ipv4 => RcAfi::Ipv4,
|
||||||
|
RoaAfi::Ipv6 => RcAfi::Ipv6,
|
||||||
|
};
|
||||||
|
RcIpPrefix {
|
||||||
|
afi,
|
||||||
|
prefix_len: p.prefix_len,
|
||||||
|
addr: p.addr.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_ip_addr_blocks(obj: &DerObject<'_>) -> Result<Vec<RoaIpAddressFamily>, RoaDecodeError> {
|
||||||
|
let seq = obj
|
||||||
|
.as_sequence()
|
||||||
|
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||||
|
if seq.is_empty() || seq.len() > 2 {
|
||||||
|
return Err(RoaDecodeError::InvalidIpAddrBlocksLen(seq.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out: Vec<RoaIpAddressFamily> = Vec::new();
|
||||||
|
for fam in seq {
|
||||||
|
let family = parse_ip_address_family(fam)?;
|
||||||
|
if out.iter().any(|f| f.afi == family.afi) {
|
||||||
|
return Err(RoaDecodeError::DuplicateAfi(family.afi));
|
||||||
|
}
|
||||||
|
out.push(family);
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_ip_address_family(obj: &DerObject<'_>) -> Result<RoaIpAddressFamily, RoaDecodeError> {
|
||||||
|
let seq = obj
|
||||||
|
.as_sequence()
|
||||||
|
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||||
|
if seq.len() != 2 {
|
||||||
|
return Err(RoaDecodeError::InvalidIpAddressFamily);
|
||||||
|
}
|
||||||
|
|
||||||
|
let afi = parse_afi(&seq[0])?;
|
||||||
|
let addresses = parse_roa_addresses(afi, &seq[1])?;
|
||||||
|
if addresses.is_empty() {
|
||||||
|
return Err(RoaDecodeError::EmptyAddressList);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(RoaIpAddressFamily { afi, addresses })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_afi(obj: &DerObject<'_>) -> Result<RoaAfi, RoaDecodeError> {
|
||||||
|
let bytes = obj
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|_e| RoaDecodeError::InvalidAddressFamily)?;
|
||||||
|
if bytes.len() != 2 {
|
||||||
|
return Err(RoaDecodeError::InvalidAddressFamily);
|
||||||
|
}
|
||||||
|
match bytes {
|
||||||
|
[0x00, 0x01] => Ok(RoaAfi::Ipv4),
|
||||||
|
[0x00, 0x02] => Ok(RoaAfi::Ipv6),
|
||||||
|
_ => Err(RoaDecodeError::UnsupportedAfi(bytes.to_vec())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_roa_addresses(afi: RoaAfi, obj: &DerObject<'_>) -> Result<Vec<RoaIpAddress>, RoaDecodeError> {
|
||||||
|
let seq = obj
|
||||||
|
.as_sequence()
|
||||||
|
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||||
|
let mut out = Vec::with_capacity(seq.len());
|
||||||
|
for entry in seq {
|
||||||
|
out.push(parse_roa_ip_address(afi, entry)?);
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_roa_ip_address(afi: RoaAfi, obj: &DerObject<'_>) -> Result<RoaIpAddress, RoaDecodeError> {
|
||||||
|
let seq = obj
|
||||||
|
.as_sequence()
|
||||||
|
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||||
|
if seq.is_empty() || seq.len() > 2 {
|
||||||
|
return Err(RoaDecodeError::InvalidRoaIpAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefix = parse_prefix_bits(afi, &seq[0])?;
|
||||||
|
let max_length = match seq.get(1) {
|
||||||
|
None => None,
|
||||||
|
Some(m) => {
|
||||||
|
let v = m
|
||||||
|
.as_u64()
|
||||||
|
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||||
|
let max_len: u16 = v
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_e| RoaDecodeError::InvalidMaxLength {
|
||||||
|
afi,
|
||||||
|
prefix_len: prefix.prefix_len,
|
||||||
|
max_len: u16::MAX,
|
||||||
|
})?;
|
||||||
|
Some(max_len)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(max_len) = max_length {
|
||||||
|
let ub = afi.ub();
|
||||||
|
if max_len > ub || max_len < prefix.prefix_len {
|
||||||
|
return Err(RoaDecodeError::InvalidMaxLength {
|
||||||
|
afi,
|
||||||
|
prefix_len: prefix.prefix_len,
|
||||||
|
max_len,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(RoaIpAddress { prefix, max_length })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_prefix_bits(afi: RoaAfi, obj: &DerObject<'_>) -> Result<IpPrefix, RoaDecodeError> {
|
||||||
|
let (unused_bits, bytes) = match &obj.content {
|
||||||
|
BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()),
|
||||||
|
_ => return Err(RoaDecodeError::InvalidPrefixBitString),
|
||||||
|
};
|
||||||
|
|
||||||
|
if unused_bits > 7 {
|
||||||
|
return Err(RoaDecodeError::InvalidPrefixUnusedBits);
|
||||||
|
}
|
||||||
|
if bytes.is_empty() {
|
||||||
|
if unused_bits != 0 {
|
||||||
|
return Err(RoaDecodeError::InvalidPrefixUnusedBits);
|
||||||
|
}
|
||||||
|
} else if unused_bits != 0 {
|
||||||
|
let mask = (1u8 << unused_bits) - 1;
|
||||||
|
if (bytes[bytes.len() - 1] & mask) != 0 {
|
||||||
|
return Err(RoaDecodeError::InvalidPrefixUnusedBits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefix_len = (bytes.len() * 8)
|
||||||
|
.checked_sub(unused_bits as usize)
|
||||||
|
.ok_or(RoaDecodeError::InvalidPrefixUnusedBits)? as u16;
|
||||||
|
if prefix_len > afi.ub() {
|
||||||
|
return Err(RoaDecodeError::PrefixLenOutOfRange { afi, prefix_len });
|
||||||
|
}
|
||||||
|
|
||||||
|
let addr = canonicalize_prefix_addr(afi, prefix_len, &bytes);
|
||||||
|
Ok(IpPrefix {
|
||||||
|
afi,
|
||||||
|
prefix_len,
|
||||||
|
addr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn canonicalize_prefix_addr(afi: RoaAfi, prefix_len: u16, bytes: &[u8]) -> Vec<u8> {
|
||||||
|
let full_len = match afi {
|
||||||
|
RoaAfi::Ipv4 => 4,
|
||||||
|
RoaAfi::Ipv6 => 16,
|
||||||
|
};
|
||||||
|
let mut addr = vec![0u8; full_len];
|
||||||
|
let copy_len = bytes.len().min(full_len);
|
||||||
|
addr[..copy_len].copy_from_slice(&bytes[..copy_len]);
|
||||||
|
|
||||||
|
if prefix_len == 0 {
|
||||||
|
return addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
let last_prefix_bit = (prefix_len - 1) as usize;
|
||||||
|
let last_prefix_byte = last_prefix_bit / 8;
|
||||||
|
let rem = (prefix_len % 8) as u8;
|
||||||
|
if rem != 0 {
|
||||||
|
let mask: u8 = 0xFF << (8 - rem);
|
||||||
|
if last_prefix_byte < addr.len() {
|
||||||
|
addr[last_prefix_byte] &= mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addr
|
||||||
|
}
|
||||||
750
src/data_model/signed_object.rs
Normal file
750
src/data_model/signed_object.rs
Normal file
@@ -0,0 +1,750 @@
|
|||||||
|
use crate::data_model::common::{Asn1TimeEncoding, Asn1TimeUtc};
|
||||||
|
use crate::data_model::oid::{
|
||||||
|
OID_CMS_ATTR_CONTENT_TYPE, OID_CMS_ATTR_MESSAGE_DIGEST, OID_CMS_ATTR_SIGNING_TIME,
|
||||||
|
OID_RSA_ENCRYPTION, OID_SHA256, OID_SHA256_WITH_RSA_ENCRYPTION, OID_SIGNED_DATA,
|
||||||
|
OID_AD_SIGNED_OBJECT, OID_SUBJECT_INFO_ACCESS,
|
||||||
|
};
|
||||||
|
use crate::data_model::rc::{ResourceCertificate, SubjectInfoAccess};
|
||||||
|
use der_parser::ber::Class;
|
||||||
|
use der_parser::der::{parse_der, DerObject, Tag};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use x509_parser::public_key::PublicKey;
|
||||||
|
use x509_parser::prelude::FromDer;
|
||||||
|
use x509_parser::x509::SubjectPublicKeyInfo;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct ResourceEeCertificate {
|
||||||
|
pub raw_der: Vec<u8>,
|
||||||
|
pub subject_key_identifier: Vec<u8>,
|
||||||
|
pub spki_der: Vec<u8>,
|
||||||
|
pub sia_signed_object_uris: Vec<String>,
|
||||||
|
pub resource_cert: ResourceCertificate,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct RpkiSignedObject {
|
||||||
|
pub raw_der: Vec<u8>,
|
||||||
|
pub content_info_content_type: String,
|
||||||
|
pub signed_data: SignedDataProfiled,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct SignedDataProfiled {
|
||||||
|
pub version: u32,
|
||||||
|
pub digest_algorithms: Vec<String>,
|
||||||
|
pub encap_content_info: EncapsulatedContentInfo,
|
||||||
|
pub certificates: Vec<ResourceEeCertificate>,
|
||||||
|
pub crls_present: bool,
|
||||||
|
pub signer_infos: Vec<SignerInfoProfiled>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct EncapsulatedContentInfo {
|
||||||
|
pub econtent_type: String,
|
||||||
|
pub econtent: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct SignerInfoProfiled {
|
||||||
|
pub version: u32,
|
||||||
|
pub sid_ski: Vec<u8>,
|
||||||
|
pub digest_algorithm: String,
|
||||||
|
pub signature_algorithm: String,
|
||||||
|
pub signed_attrs: SignedAttrsProfiled,
|
||||||
|
pub unsigned_attrs_present: bool,
|
||||||
|
pub signature: Vec<u8>,
|
||||||
|
pub signed_attrs_der_for_signature: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct SignedAttrsProfiled {
|
||||||
|
pub content_type: String,
|
||||||
|
pub message_digest: Vec<u8>,
|
||||||
|
pub signing_time: Asn1TimeUtc,
|
||||||
|
pub other_attrs_present: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum SignedObjectDecodeError {
|
||||||
|
#[error("DER parse error: {0} (RFC 6488 §2; RFC 6488 §3(1l); RFC 5652 §3/§5)")]
|
||||||
|
Parse(String),
|
||||||
|
|
||||||
|
#[error("trailing bytes after DER object: {0} bytes (DER; RFC 6488 §3(1l))")]
|
||||||
|
TrailingBytes(usize),
|
||||||
|
|
||||||
|
#[error("ContentInfo.contentType must be SignedData ({OID_SIGNED_DATA}), got {0} (RFC 6488 §3(1a); RFC 5652 §3)")]
|
||||||
|
InvalidContentInfoContentType(String),
|
||||||
|
|
||||||
|
#[error("SignedData.version must be 3, got {0} (RFC 6488 §2.1.1; RFC 6488 §3(1b); RFC 5652 §5.1)")]
|
||||||
|
InvalidSignedDataVersion(u64),
|
||||||
|
|
||||||
|
#[error("SignedData.digestAlgorithms must contain exactly one AlgorithmIdentifier, got {0} (RFC 6488 §2.1.2; RFC 6488 §3(1b); RFC 5652 §5.1)")]
|
||||||
|
InvalidDigestAlgorithmsCount(usize),
|
||||||
|
|
||||||
|
#[error("digest algorithm must be id-sha256 ({OID_SHA256}), got {0} (RFC 6488 §2.1.2; RFC 6488 §3(1b); RFC 7935 §2)")]
|
||||||
|
InvalidDigestAlgorithm(String),
|
||||||
|
|
||||||
|
#[error("SignedData.certificates MUST be present (RFC 6488 §3(1c); RFC 5652 §5.1)")]
|
||||||
|
CertificatesMissing,
|
||||||
|
|
||||||
|
#[error("SignedData.certificates must contain exactly one EE certificate, got {0} (RFC 6488 §3(1c))")]
|
||||||
|
InvalidCertificatesCount(usize),
|
||||||
|
|
||||||
|
#[error("SignedData.crls MUST be omitted (RFC 6488 §3(1d))")]
|
||||||
|
CrlsPresent,
|
||||||
|
|
||||||
|
#[error("SignedData.signerInfos must contain exactly one SignerInfo, got {0} (RFC 6488 §2.1; RFC 6488 §3(1e); RFC 5652 §5.1)")]
|
||||||
|
InvalidSignerInfosCount(usize),
|
||||||
|
|
||||||
|
#[error("SignerInfo.version must be 3, got {0} (RFC 6488 §3(1e); RFC 5652 §5.3)")]
|
||||||
|
InvalidSignerInfoVersion(u64),
|
||||||
|
|
||||||
|
#[error("SignerInfo.sid must be subjectKeyIdentifier [0] (RFC 6488 §3(1c); RFC 5652 §5.3)")]
|
||||||
|
InvalidSignerIdentifier,
|
||||||
|
|
||||||
|
#[error("SignerInfo.digestAlgorithm must be id-sha256 ({OID_SHA256}), got {0} (RFC 6488 §3(1j); RFC 7935 §2)")]
|
||||||
|
InvalidSignerInfoDigestAlgorithm(String),
|
||||||
|
|
||||||
|
#[error("SignerInfo.signedAttrs MUST be present (RFC 9589 §4; RFC 6488 §3(1f))")]
|
||||||
|
SignedAttrsMissing,
|
||||||
|
|
||||||
|
#[error("SignerInfo.unsignedAttrs MUST be omitted (RFC 6488 §3(1i))")]
|
||||||
|
UnsignedAttrsPresent,
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"SignerInfo.signatureAlgorithm must be rsaEncryption ({OID_RSA_ENCRYPTION}) or \
|
||||||
|
sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0} (RFC 6488 §3(1k); RFC 7935 §2)"
|
||||||
|
)]
|
||||||
|
InvalidSignatureAlgorithm(String),
|
||||||
|
|
||||||
|
#[error("SignerInfo.signatureAlgorithm parameters must be absent or NULL (RFC 5280 §4.1.1.2; RFC 7935 §2)")]
|
||||||
|
InvalidSignatureAlgorithmParameters,
|
||||||
|
|
||||||
|
#[error("signedAttrs contains unsupported attribute OID {0} (RFC 9589 §4; RFC 6488 §2.1.6.4)")]
|
||||||
|
UnsupportedSignedAttribute(String),
|
||||||
|
|
||||||
|
#[error("signedAttrs contains duplicate attribute OID {0} (RFC 6488 §2.1.6.4; RFC 9589 §4)")]
|
||||||
|
DuplicateSignedAttribute(String),
|
||||||
|
|
||||||
|
#[error("signedAttrs attribute {oid} attrValues must contain exactly one value, got {count} (RFC 6488 §2.1.6.4; RFC 5652 §5.3)")]
|
||||||
|
InvalidSignedAttributeValuesCount { oid: String, count: usize },
|
||||||
|
|
||||||
|
#[error("signedAttrs.content-type attrValues must equal eContentType ({econtent_type}), got {attr_content_type} (RFC 6488 §3(1h); RFC 9589 §4)")]
|
||||||
|
ContentTypeAttrMismatch {
|
||||||
|
econtent_type: String,
|
||||||
|
attr_content_type: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("EncapsulatedContentInfo.eContent MUST be present (RFC 6488 §2.1.3; RFC 5652 §5.2)")]
|
||||||
|
EContentMissing,
|
||||||
|
|
||||||
|
#[error("signedAttrs.message-digest does not match SHA-256(eContent) (RFC 6488 §3(1f); RFC 5652 §11.2)")]
|
||||||
|
MessageDigestMismatch,
|
||||||
|
|
||||||
|
#[error("EE certificate parse error: {0} (RFC 6488 §3(1c); RFC 6487 §4)")]
|
||||||
|
EeCertificateParse(String),
|
||||||
|
|
||||||
|
#[error("EE certificate missing SubjectKeyIdentifier extension (RFC 6488 §3(1c); RFC 6487 §4.8.2)")]
|
||||||
|
EeCertificateMissingSki,
|
||||||
|
|
||||||
|
#[error("EE certificate missing SubjectInfoAccess extension ({OID_SUBJECT_INFO_ACCESS}) (RFC 6487 §4.8.8.2)")]
|
||||||
|
EeCertificateMissingSia,
|
||||||
|
|
||||||
|
#[error("EE certificate SIA missing id-ad-signedObject access method ({OID_AD_SIGNED_OBJECT}) (RFC 6487 §4.8.8.2)")]
|
||||||
|
EeCertificateMissingSignedObjectSia,
|
||||||
|
|
||||||
|
#[error("EE certificate SIA id-ad-signedObject accessLocation must be a URI (RFC 6487 §4.8.8.2; RFC 5280 §4.2.2.2)")]
|
||||||
|
EeCertificateSignedObjectSiaNotUri,
|
||||||
|
|
||||||
|
#[error("EE certificate SIA id-ad-signedObject must include at least one rsync:// URI (RFC 6487 §4.8.8.2)")]
|
||||||
|
EeCertificateSignedObjectSiaNoRsync,
|
||||||
|
|
||||||
|
#[error("SignerInfo.sid SKI does not match EE certificate SKI (RFC 6488 §3(1c); RFC 5652 §5.3)")]
|
||||||
|
SidSkiMismatch,
|
||||||
|
|
||||||
|
#[error("invalid signing-time attribute value (expected UTCTime or GeneralizedTime) (RFC 5652 §11.3; RFC 9589 §4)")]
|
||||||
|
InvalidSigningTimeValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum SignedObjectVerifyError {
|
||||||
|
#[error("EE SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7)")]
|
||||||
|
EeSpkiParse(String),
|
||||||
|
|
||||||
|
#[error("trailing bytes after EE SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 5280 §4.1.2.7)")]
|
||||||
|
EeSpkiTrailingBytes(usize),
|
||||||
|
|
||||||
|
#[error("unsupported EE public key algorithm (only RSA supported in M3) (RFC 7935 §2)")]
|
||||||
|
UnsupportedEePublicKeyAlgorithm,
|
||||||
|
|
||||||
|
#[error("EE RSA public exponent invalid (RFC 8017 §A.1.1; RFC 7935 §2)")]
|
||||||
|
InvalidEeRsaExponent,
|
||||||
|
|
||||||
|
#[error("signature verification failed (RFC 6488 §3(2)-(3); RFC 5652 §5.3; RFC 7935 §2)")]
|
||||||
|
InvalidSignature,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RpkiSignedObject {
|
||||||
|
/// Decode a DER-encoded RPKI Signed Object (CMS ContentInfo wrapping SignedData) and enforce
|
||||||
|
/// the profile constraints from RFC 6488 §2-§3 and RFC 9589 §4.
|
||||||
|
pub fn decode_der(der: &[u8]) -> Result<Self, SignedObjectDecodeError> {
|
||||||
|
let (rem, obj) = parse_der(der).map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||||
|
if !rem.is_empty() {
|
||||||
|
return Err(SignedObjectDecodeError::TrailingBytes(rem.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let content_info_seq = obj
|
||||||
|
.as_sequence()
|
||||||
|
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||||
|
if content_info_seq.len() != 2 {
|
||||||
|
return Err(SignedObjectDecodeError::Parse(
|
||||||
|
"ContentInfo must be a SEQUENCE of 2 elements".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let content_type = oid_to_string(&content_info_seq[0])?;
|
||||||
|
if content_type != OID_SIGNED_DATA {
|
||||||
|
return Err(SignedObjectDecodeError::InvalidContentInfoContentType(
|
||||||
|
content_type,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let signed_data = parse_signed_data_from_contentinfo(&content_info_seq[1])?;
|
||||||
|
|
||||||
|
Ok(RpkiSignedObject {
|
||||||
|
raw_der: der.to_vec(),
|
||||||
|
content_info_content_type: OID_SIGNED_DATA.to_string(),
|
||||||
|
signed_data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify the CMS signature using the embedded EE certificate public key.
|
||||||
|
pub fn verify_signature(&self) -> Result<(), SignedObjectVerifyError> {
|
||||||
|
let ee = &self.signed_data.certificates[0];
|
||||||
|
self.verify_signature_with_ee_spki_der(&ee.spki_der)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify the CMS signature using a DER-encoded SubjectPublicKeyInfo.
|
||||||
|
pub fn verify_signature_with_ee_spki_der(
|
||||||
|
&self,
|
||||||
|
ee_spki_der: &[u8],
|
||||||
|
) -> Result<(), SignedObjectVerifyError> {
|
||||||
|
let (rem, spki) = SubjectPublicKeyInfo::from_der(ee_spki_der)
|
||||||
|
.map_err(|e| SignedObjectVerifyError::EeSpkiParse(e.to_string()))?;
|
||||||
|
if !rem.is_empty() {
|
||||||
|
return Err(SignedObjectVerifyError::EeSpkiTrailingBytes(rem.len()));
|
||||||
|
}
|
||||||
|
self.verify_signature_with_ee_spki(&spki)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify the CMS signature using a parsed SubjectPublicKeyInfo.
|
||||||
|
pub fn verify_signature_with_ee_spki(
|
||||||
|
&self,
|
||||||
|
ee_spki: &SubjectPublicKeyInfo<'_>,
|
||||||
|
) -> Result<(), SignedObjectVerifyError> {
|
||||||
|
let pk = ee_spki
|
||||||
|
.parsed()
|
||||||
|
.map_err(|_e| SignedObjectVerifyError::UnsupportedEePublicKeyAlgorithm)?;
|
||||||
|
let (n, e) = match pk {
|
||||||
|
PublicKey::RSA(rsa) => {
|
||||||
|
let n = strip_leading_zeros(rsa.modulus).to_vec();
|
||||||
|
let e = strip_leading_zeros(rsa.exponent).to_vec();
|
||||||
|
let _exp = rsa
|
||||||
|
.try_exponent()
|
||||||
|
.map_err(|_e| SignedObjectVerifyError::InvalidEeRsaExponent)?;
|
||||||
|
(n, e)
|
||||||
|
}
|
||||||
|
_ => return Err(SignedObjectVerifyError::UnsupportedEePublicKeyAlgorithm),
|
||||||
|
};
|
||||||
|
|
||||||
|
let signer = &self.signed_data.signer_infos[0];
|
||||||
|
// The message to be verified is the DER encoding of SignedAttributes (SET OF Attribute).
|
||||||
|
let msg = &signer.signed_attrs_der_for_signature;
|
||||||
|
|
||||||
|
let pk = ring::signature::RsaPublicKeyComponents { n, e };
|
||||||
|
pk.verify(
|
||||||
|
&ring::signature::RSA_PKCS1_2048_8192_SHA256,
|
||||||
|
msg,
|
||||||
|
&signer.signature,
|
||||||
|
)
|
||||||
|
.map_err(|_e| SignedObjectVerifyError::InvalidSignature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_signed_data_from_contentinfo(
|
||||||
|
obj: &DerObject<'_>,
|
||||||
|
) -> Result<SignedDataProfiled, SignedObjectDecodeError> {
|
||||||
|
// ContentInfo.content is `[0] EXPLICIT`, but `der-parser` will represent unknown tagged
|
||||||
|
// objects as `Unknown(Any)`. For EXPLICIT tags, the content octets are the full encoding of
|
||||||
|
// the inner object, so we parse it from the object's slice.
|
||||||
|
if obj.class() != Class::ContextSpecific || obj.tag() != Tag(0) {
|
||||||
|
return Err(SignedObjectDecodeError::Parse(
|
||||||
|
"ContentInfo.content must be [0] EXPLICIT".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let inner_der = obj
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||||
|
let (rem, inner_obj) =
|
||||||
|
parse_der(inner_der).map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||||
|
if !rem.is_empty() {
|
||||||
|
return Err(SignedObjectDecodeError::Parse(
|
||||||
|
"trailing bytes inside ContentInfo.content".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
parse_signed_data(&inner_obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_signed_data(obj: &DerObject<'_>) -> Result<SignedDataProfiled, SignedObjectDecodeError> {
|
||||||
|
let seq = obj
|
||||||
|
.as_sequence()
|
||||||
|
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||||
|
if seq.len() < 4 || seq.len() > 6 {
|
||||||
|
return Err(SignedObjectDecodeError::Parse(
|
||||||
|
"SignedData must be a SEQUENCE of 4..6 elements".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let version = seq[0]
|
||||||
|
.as_u64()
|
||||||
|
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||||
|
if version != 3 {
|
||||||
|
return Err(SignedObjectDecodeError::InvalidSignedDataVersion(version));
|
||||||
|
}
|
||||||
|
|
||||||
|
let digest_set = seq[1]
|
||||||
|
.as_set()
|
||||||
|
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||||
|
if digest_set.len() != 1 {
|
||||||
|
return Err(SignedObjectDecodeError::InvalidDigestAlgorithmsCount(
|
||||||
|
digest_set.len(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let (digest_oid, _params_ok) = parse_algorithm_identifier(&digest_set[0])?;
|
||||||
|
if digest_oid != OID_SHA256 {
|
||||||
|
return Err(SignedObjectDecodeError::InvalidDigestAlgorithm(digest_oid));
|
||||||
|
}
|
||||||
|
let digest_algorithms = vec![OID_SHA256.to_string()];
|
||||||
|
|
||||||
|
let encap_content_info = parse_encapsulated_content_info(&seq[2])?;
|
||||||
|
|
||||||
|
let mut certificates: Option<Vec<ResourceEeCertificate>> = None;
|
||||||
|
let mut crls_present = false;
|
||||||
|
let mut signer_infos_obj: Option<&DerObject<'_>> = None;
|
||||||
|
|
||||||
|
for item in &seq[3..] {
|
||||||
|
if item.class() == Class::ContextSpecific && item.tag() == Tag(0) {
|
||||||
|
if certificates.is_some() {
|
||||||
|
return Err(SignedObjectDecodeError::Parse(
|
||||||
|
"SignedData.certificates appears more than once".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
certificates = Some(parse_certificate_set_implicit(item)?);
|
||||||
|
} else if item.class() == Class::ContextSpecific && item.tag() == Tag(1) {
|
||||||
|
crls_present = true;
|
||||||
|
} else if item.class() == Class::Universal && item.tag() == Tag::Set {
|
||||||
|
signer_infos_obj = Some(item);
|
||||||
|
} else {
|
||||||
|
return Err(SignedObjectDecodeError::Parse(
|
||||||
|
"unexpected field in SignedData".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if crls_present {
|
||||||
|
return Err(SignedObjectDecodeError::CrlsPresent);
|
||||||
|
}
|
||||||
|
let certificates = certificates.ok_or(SignedObjectDecodeError::CertificatesMissing)?;
|
||||||
|
if certificates.len() != 1 {
|
||||||
|
return Err(SignedObjectDecodeError::InvalidCertificatesCount(
|
||||||
|
certificates.len(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let signer_infos_obj = signer_infos_obj.ok_or_else(|| {
|
||||||
|
SignedObjectDecodeError::Parse("SignedData.signerInfos missing".into())
|
||||||
|
})?;
|
||||||
|
let signer_infos_set = signer_infos_obj
|
||||||
|
.as_set()
|
||||||
|
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||||
|
if signer_infos_set.len() != 1 {
|
||||||
|
return Err(SignedObjectDecodeError::InvalidSignerInfosCount(
|
||||||
|
signer_infos_set.len(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let signer_info = parse_signer_info(&signer_infos_set[0])?;
|
||||||
|
if signer_info.sid_ski != certificates[0].subject_key_identifier {
|
||||||
|
return Err(SignedObjectDecodeError::SidSkiMismatch);
|
||||||
|
}
|
||||||
|
if signer_info.signed_attrs.content_type != encap_content_info.econtent_type {
|
||||||
|
return Err(SignedObjectDecodeError::ContentTypeAttrMismatch {
|
||||||
|
econtent_type: encap_content_info.econtent_type.clone(),
|
||||||
|
attr_content_type: signer_info.signed_attrs.content_type.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let computed = Sha256::digest(&encap_content_info.econtent).to_vec();
|
||||||
|
if computed != signer_info.signed_attrs.message_digest {
|
||||||
|
return Err(SignedObjectDecodeError::MessageDigestMismatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SignedDataProfiled {
|
||||||
|
version: 3,
|
||||||
|
digest_algorithms,
|
||||||
|
encap_content_info,
|
||||||
|
certificates,
|
||||||
|
crls_present,
|
||||||
|
signer_infos: vec![signer_info],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_encapsulated_content_info(obj: &DerObject<'_>) -> Result<EncapsulatedContentInfo, SignedObjectDecodeError> {
|
||||||
|
let seq = obj
|
||||||
|
.as_sequence()
|
||||||
|
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||||
|
if seq.len() == 1 {
|
||||||
|
return Err(SignedObjectDecodeError::EContentMissing);
|
||||||
|
}
|
||||||
|
if seq.len() != 2 {
|
||||||
|
return Err(SignedObjectDecodeError::Parse(
|
||||||
|
"EncapsulatedContentInfo must be SEQUENCE of 1..2".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let econtent_type = oid_to_string(&seq[0])?;
|
||||||
|
|
||||||
|
let econtent_tagged = &seq[1];
|
||||||
|
if econtent_tagged.class() != Class::ContextSpecific || econtent_tagged.tag() != Tag(0) {
|
||||||
|
return Err(SignedObjectDecodeError::Parse(
|
||||||
|
"EncapsulatedContentInfo.eContent must be [0] EXPLICIT".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let inner_der = econtent_tagged
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?
|
||||||
|
;
|
||||||
|
let (rem, inner_obj) =
|
||||||
|
parse_der(inner_der).map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||||
|
if !rem.is_empty() {
|
||||||
|
return Err(SignedObjectDecodeError::Parse(
|
||||||
|
"trailing bytes inside EncapsulatedContentInfo.eContent".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let econtent = inner_obj
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?
|
||||||
|
.to_vec();
|
||||||
|
if econtent.is_empty() {
|
||||||
|
return Err(SignedObjectDecodeError::EContentMissing);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(EncapsulatedContentInfo {
|
||||||
|
econtent_type,
|
||||||
|
econtent,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_certificate_set_implicit(obj: &DerObject<'_>) -> Result<Vec<ResourceEeCertificate>, SignedObjectDecodeError> {
|
||||||
|
let content = obj
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||||
|
let mut input = content;
|
||||||
|
let mut certs = Vec::new();
|
||||||
|
while !input.is_empty() {
|
||||||
|
let (rem, _any_obj) =
|
||||||
|
parse_der(input).map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||||
|
let consumed = input.len() - rem.len();
|
||||||
|
let der = &input[..consumed];
|
||||||
|
certs.push(parse_ee_certificate(der)?);
|
||||||
|
input = rem;
|
||||||
|
}
|
||||||
|
Ok(certs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_ee_certificate(der: &[u8]) -> Result<ResourceEeCertificate, SignedObjectDecodeError> {
|
||||||
|
let rc = match ResourceCertificate::from_der(der) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
return match e {
|
||||||
|
crate::data_model::rc::ResourceCertificateError::SignedObjectSiaNotUri => {
|
||||||
|
Err(SignedObjectDecodeError::EeCertificateSignedObjectSiaNotUri)
|
||||||
|
}
|
||||||
|
crate::data_model::rc::ResourceCertificateError::SignedObjectSiaNoRsync => {
|
||||||
|
Err(SignedObjectDecodeError::EeCertificateSignedObjectSiaNoRsync)
|
||||||
|
}
|
||||||
|
_ => Err(SignedObjectDecodeError::EeCertificateParse(e.to_string())),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let ski = rc
|
||||||
|
.tbs
|
||||||
|
.extensions
|
||||||
|
.subject_key_identifier
|
||||||
|
.clone()
|
||||||
|
.ok_or(SignedObjectDecodeError::EeCertificateMissingSki)?;
|
||||||
|
|
||||||
|
let spki_der = rc.tbs.subject_public_key_info.clone();
|
||||||
|
|
||||||
|
let sia = rc
|
||||||
|
.tbs
|
||||||
|
.extensions
|
||||||
|
.subject_info_access
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(SignedObjectDecodeError::EeCertificateMissingSia)?;
|
||||||
|
let signed_object_uris: Vec<String> = match sia {
|
||||||
|
SubjectInfoAccess::Ee(ee) => ee
|
||||||
|
.signed_object_uris
|
||||||
|
.iter()
|
||||||
|
.map(|u| u.as_str().to_string())
|
||||||
|
.collect(),
|
||||||
|
SubjectInfoAccess::Ca(_ca) => Vec::new(),
|
||||||
|
};
|
||||||
|
if signed_object_uris.is_empty() {
|
||||||
|
return Err(SignedObjectDecodeError::EeCertificateMissingSignedObjectSia);
|
||||||
|
}
|
||||||
|
if !signed_object_uris.iter().any(|u| u.starts_with("rsync://")) {
|
||||||
|
return Err(SignedObjectDecodeError::EeCertificateSignedObjectSiaNoRsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ResourceEeCertificate {
|
||||||
|
raw_der: der.to_vec(),
|
||||||
|
subject_key_identifier: ski,
|
||||||
|
spki_der,
|
||||||
|
sia_signed_object_uris: signed_object_uris,
|
||||||
|
resource_cert: rc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_signer_info(obj: &DerObject<'_>) -> Result<SignerInfoProfiled, SignedObjectDecodeError> {
|
||||||
|
let seq = obj
|
||||||
|
.as_sequence()
|
||||||
|
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||||
|
if seq.len() < 5 || seq.len() > 7 {
|
||||||
|
return Err(SignedObjectDecodeError::Parse(
|
||||||
|
"SignerInfo must be a SEQUENCE of 5..7 elements".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let version = seq[0]
|
||||||
|
.as_u64()
|
||||||
|
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||||
|
if version != 3 {
|
||||||
|
return Err(SignedObjectDecodeError::InvalidSignerInfoVersion(version));
|
||||||
|
}
|
||||||
|
|
||||||
|
let sid = &seq[1];
|
||||||
|
if sid.class() != Class::ContextSpecific || sid.tag() != Tag(0) {
|
||||||
|
return Err(SignedObjectDecodeError::InvalidSignerIdentifier);
|
||||||
|
}
|
||||||
|
let sid_ski = sid
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?
|
||||||
|
.to_vec();
|
||||||
|
|
||||||
|
let (digest_algorithm, _params_ok) = parse_algorithm_identifier(&seq[2])?;
|
||||||
|
if digest_algorithm != OID_SHA256 {
|
||||||
|
return Err(SignedObjectDecodeError::InvalidSignerInfoDigestAlgorithm(
|
||||||
|
digest_algorithm,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut idx = 3;
|
||||||
|
let mut signed_attrs: Option<SignedAttrsProfiled> = None;
|
||||||
|
let mut signed_attrs_der_for_signature: Option<Vec<u8>> = None;
|
||||||
|
|
||||||
|
if seq[idx].class() == Class::ContextSpecific && seq[idx].tag() == Tag(0) {
|
||||||
|
let signed_attrs_obj = &seq[idx];
|
||||||
|
let signed_attrs_content = signed_attrs_obj
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||||
|
signed_attrs = Some(parse_signed_attrs_implicit(signed_attrs_content)?);
|
||||||
|
signed_attrs_der_for_signature = Some(make_signed_attrs_der_for_signature(signed_attrs_obj)?);
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let signed_attrs = signed_attrs.ok_or(SignedObjectDecodeError::SignedAttrsMissing)?;
|
||||||
|
let signed_attrs_der_for_signature =
|
||||||
|
signed_attrs_der_for_signature.ok_or(SignedObjectDecodeError::SignedAttrsMissing)?;
|
||||||
|
|
||||||
|
let (signature_algorithm, params_ok) = parse_algorithm_identifier(&seq[idx])?;
|
||||||
|
if !params_ok {
|
||||||
|
return Err(SignedObjectDecodeError::InvalidSignatureAlgorithmParameters);
|
||||||
|
}
|
||||||
|
if signature_algorithm != OID_RSA_ENCRYPTION
|
||||||
|
&& signature_algorithm != OID_SHA256_WITH_RSA_ENCRYPTION
|
||||||
|
{
|
||||||
|
return Err(SignedObjectDecodeError::InvalidSignatureAlgorithm(
|
||||||
|
signature_algorithm,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
idx += 1;
|
||||||
|
|
||||||
|
let signature = seq[idx]
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?
|
||||||
|
.to_vec();
|
||||||
|
idx += 1;
|
||||||
|
|
||||||
|
let unsigned_attrs_present = seq.get(idx).is_some();
|
||||||
|
if unsigned_attrs_present {
|
||||||
|
return Err(SignedObjectDecodeError::UnsignedAttrsPresent);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SignerInfoProfiled {
|
||||||
|
version: 3,
|
||||||
|
sid_ski,
|
||||||
|
digest_algorithm: OID_SHA256.to_string(),
|
||||||
|
signature_algorithm,
|
||||||
|
signed_attrs,
|
||||||
|
unsigned_attrs_present,
|
||||||
|
signature,
|
||||||
|
signed_attrs_der_for_signature,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_signed_attrs_implicit(input: &[u8]) -> Result<SignedAttrsProfiled, SignedObjectDecodeError> {
|
||||||
|
let mut content_type: Option<String> = None;
|
||||||
|
let mut message_digest: Option<Vec<u8>> = None;
|
||||||
|
let mut signing_time: Option<Asn1TimeUtc> = None;
|
||||||
|
|
||||||
|
let mut remaining = input;
|
||||||
|
while !remaining.is_empty() {
|
||||||
|
let (rem, attr_obj) =
|
||||||
|
parse_der(remaining).map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||||
|
remaining = rem;
|
||||||
|
|
||||||
|
let attr_seq = attr_obj
|
||||||
|
.as_sequence()
|
||||||
|
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||||
|
if attr_seq.len() != 2 {
|
||||||
|
return Err(SignedObjectDecodeError::Parse(
|
||||||
|
"Attribute must be SEQUENCE of 2".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let oid = oid_to_string(&attr_seq[0])?;
|
||||||
|
let values_set = attr_seq[1]
|
||||||
|
.as_set()
|
||||||
|
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||||
|
if values_set.len() != 1 {
|
||||||
|
return Err(SignedObjectDecodeError::InvalidSignedAttributeValuesCount {
|
||||||
|
oid,
|
||||||
|
count: values_set.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
match oid.as_str() {
|
||||||
|
OID_CMS_ATTR_CONTENT_TYPE => {
|
||||||
|
if content_type.is_some() {
|
||||||
|
return Err(SignedObjectDecodeError::DuplicateSignedAttribute(oid));
|
||||||
|
}
|
||||||
|
let v = oid_to_string(&values_set[0])?;
|
||||||
|
content_type = Some(v);
|
||||||
|
}
|
||||||
|
OID_CMS_ATTR_MESSAGE_DIGEST => {
|
||||||
|
if message_digest.is_some() {
|
||||||
|
return Err(SignedObjectDecodeError::DuplicateSignedAttribute(oid));
|
||||||
|
}
|
||||||
|
let v = values_set[0]
|
||||||
|
.as_slice()
|
||||||
|
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?
|
||||||
|
.to_vec();
|
||||||
|
message_digest = Some(v);
|
||||||
|
}
|
||||||
|
OID_CMS_ATTR_SIGNING_TIME => {
|
||||||
|
if signing_time.is_some() {
|
||||||
|
return Err(SignedObjectDecodeError::DuplicateSignedAttribute(oid));
|
||||||
|
}
|
||||||
|
signing_time = Some(parse_signing_time_value(&values_set[0])?);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(SignedObjectDecodeError::UnsupportedSignedAttribute(oid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SignedAttrsProfiled {
|
||||||
|
content_type: content_type.ok_or_else(|| {
|
||||||
|
SignedObjectDecodeError::Parse("missing signedAttrs content-type".into())
|
||||||
|
})?,
|
||||||
|
message_digest: message_digest.ok_or_else(|| {
|
||||||
|
SignedObjectDecodeError::Parse("missing signedAttrs message-digest".into())
|
||||||
|
})?,
|
||||||
|
signing_time: signing_time.ok_or_else(|| {
|
||||||
|
SignedObjectDecodeError::Parse("missing signedAttrs signing-time".into())
|
||||||
|
})?,
|
||||||
|
other_attrs_present: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_signing_time_value(obj: &DerObject<'_>) -> Result<Asn1TimeUtc, SignedObjectDecodeError> {
|
||||||
|
match &obj.content {
|
||||||
|
der_parser::ber::BerObjectContent::UTCTime(dt) => Ok(Asn1TimeUtc {
|
||||||
|
utc: dt.to_datetime().map_err(|_| SignedObjectDecodeError::InvalidSigningTimeValue)?,
|
||||||
|
encoding: Asn1TimeEncoding::UtcTime,
|
||||||
|
}),
|
||||||
|
der_parser::ber::BerObjectContent::GeneralizedTime(dt) => Ok(Asn1TimeUtc {
|
||||||
|
utc: dt.to_datetime().map_err(|_| SignedObjectDecodeError::InvalidSigningTimeValue)?,
|
||||||
|
encoding: Asn1TimeEncoding::GeneralizedTime,
|
||||||
|
}),
|
||||||
|
_ => Err(SignedObjectDecodeError::InvalidSigningTimeValue),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_signed_attrs_der_for_signature(obj: &DerObject<'_>) -> Result<Vec<u8>, SignedObjectDecodeError> {
|
||||||
|
// We need the DER encoding of SignedAttributes (SET OF Attribute) as signature input.
|
||||||
|
// The SignedAttributes field in SignerInfo is `[0] IMPLICIT`, so the on-wire bytes start with
|
||||||
|
// a context-specific constructed tag (0xA0 for tag 0). For signature verification, this tag
|
||||||
|
// is replaced with the universal SET tag (0x31), leaving length+content unchanged.
|
||||||
|
//
|
||||||
|
let mut cs_der = obj
|
||||||
|
.to_vec()
|
||||||
|
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||||
|
if cs_der.is_empty() {
|
||||||
|
return Err(SignedObjectDecodeError::Parse(
|
||||||
|
"signedAttrs encoding is empty".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// The first byte should be the context-specific tag (0xA0) for [0] constructed.
|
||||||
|
// Replace it with universal SET (0x31) for signature input.
|
||||||
|
cs_der[0] = 0x31;
|
||||||
|
Ok(cs_der)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oid_to_string(obj: &DerObject<'_>) -> Result<String, SignedObjectDecodeError> {
|
||||||
|
let oid = obj
|
||||||
|
.as_oid()
|
||||||
|
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||||
|
Ok(oid.to_id_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_algorithm_identifier(
|
||||||
|
obj: &DerObject<'_>,
|
||||||
|
) -> Result<(String, bool), SignedObjectDecodeError> {
|
||||||
|
let seq = obj
|
||||||
|
.as_sequence()
|
||||||
|
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||||
|
if seq.is_empty() || seq.len() > 2 {
|
||||||
|
return Err(SignedObjectDecodeError::Parse(
|
||||||
|
"AlgorithmIdentifier must be SEQUENCE of 1..2".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let oid = oid_to_string(&seq[0])?;
|
||||||
|
let params_ok = match seq.get(1) {
|
||||||
|
None => true,
|
||||||
|
Some(p) => matches!(p.content, der_parser::ber::BerObjectContent::Null),
|
||||||
|
};
|
||||||
|
Ok((oid, params_ok))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strip_leading_zeros(bytes: &[u8]) -> &[u8] {
|
||||||
|
let mut idx = 0;
|
||||||
|
while idx < bytes.len() && bytes[idx] == 0 {
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
if idx == bytes.len() {
|
||||||
|
&bytes[bytes.len() - 1..]
|
||||||
|
} else {
|
||||||
|
&bytes[idx..]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,188 @@
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
use x509_parser::prelude::*;
|
use x509_parser::prelude::{FromDer, X509Certificate};
|
||||||
|
|
||||||
use crate::data_model::resources::resource::ResourceSet;
|
use crate::data_model::oid::OID_CP_IPADDR_ASNUMBER;
|
||||||
|
use crate::data_model::rc::{AsIdentifierChoice, IpAddressChoice, ResourceCertKind, ResourceCertificate};
|
||||||
|
use crate::data_model::tal::Tal;
|
||||||
|
|
||||||
//
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
// #[derive(Debug, Clone)]
|
pub struct TaCertificate {
|
||||||
// pub struct TrustAnchorCert {
|
pub raw_der: Vec<u8>,
|
||||||
// /// 信任锚证书名称
|
pub rc_ca: ResourceCertificate,
|
||||||
// pub name: String,
|
}
|
||||||
//
|
|
||||||
// /// 证书原始DER内容
|
#[derive(Debug, thiserror::Error)]
|
||||||
// pub cert_der: Vec<u8>,
|
pub enum TaCertificateError {
|
||||||
//
|
#[error("TA certificate parse error: {0} (RFC 5280 §4.1; RFC 6487 §4; RFC 8630 §2.3)")]
|
||||||
// /// 证书
|
Parse(String),
|
||||||
// pub cert: X509Certificate<'static>,
|
|
||||||
//
|
#[error("trailing bytes after TA certificate DER: {0} bytes (DER; RFC 5280 §4.1; RFC 6487 §4)")]
|
||||||
// /// 资源集合
|
TrailingBytes(usize),
|
||||||
// pub resources: ResourceSet,
|
|
||||||
//
|
#[error("TA certificate must be a CA certificate (RFC 8630 §2.3; RFC 6487 §4.8.1)")]
|
||||||
// ///发布点
|
NotCa,
|
||||||
// pub publication_point: Url,
|
|
||||||
// }
|
#[error("TA certificate must be self-signed (issuer DN must equal subject DN) (RFC 8630 §2.3; RFC 5280 §4.1.2.4)")]
|
||||||
//
|
NotSelfSignedIssuerSubject,
|
||||||
// impl TrustAnchorCert {
|
|
||||||
//
|
#[error("TA certificate self-signature verification failed: {0} (RFC 8630 §2.3; RFC 5280 §6.1)")]
|
||||||
// }
|
InvalidSelfSignature(String),
|
||||||
|
|
||||||
|
#[error("TA certificate must contain certificatePolicies ipAddr-asNumber ({OID_CP_IPADDR_ASNUMBER}) (RFC 6487 §4.8.9; RFC 8630 §2.3)")]
|
||||||
|
MissingOrInvalidCertificatePolicies,
|
||||||
|
|
||||||
|
#[error("TA certificate must contain SubjectKeyIdentifier (RFC 6487 §4.8.2; RFC 8630 §2.3)")]
|
||||||
|
MissingSubjectKeyIdentifier,
|
||||||
|
|
||||||
|
#[error("TA certificate must contain at least one RFC 3779 resource extension (IP or AS) (RFC 6487 §4.8.10-§4.8.11; RFC 8630 §2.3)")]
|
||||||
|
ResourcesMissing,
|
||||||
|
|
||||||
|
#[error("TA certificate resources must be non-empty (RFC 8630 §2.3)")]
|
||||||
|
ResourcesEmpty,
|
||||||
|
|
||||||
|
#[error("TA certificate MUST NOT use inherit in IP resources (RFC 8630 §2.3; RFC 3779 §2.2.3.5)")]
|
||||||
|
IpResourcesInherit,
|
||||||
|
|
||||||
|
#[error("TA certificate MUST NOT use inherit in AS resources (RFC 8630 §2.3; RFC 3779 §3.2.3.3)")]
|
||||||
|
AsResourcesInherit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TaCertificate {
|
||||||
|
pub fn from_der(der: &[u8]) -> Result<Self, TaCertificateError> {
|
||||||
|
let rc_ca = ResourceCertificate::from_der(der).map_err(|e| TaCertificateError::Parse(e.to_string()))?;
|
||||||
|
if rc_ca.kind != ResourceCertKind::Ca {
|
||||||
|
return Err(TaCertificateError::NotCa);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strong self-signed check: issuer==subject AND signature verifies with its own SPKI.
|
||||||
|
let (rem, cert) =
|
||||||
|
X509Certificate::from_der(der).map_err(|e| TaCertificateError::Parse(e.to_string()))?;
|
||||||
|
if !rem.is_empty() {
|
||||||
|
return Err(TaCertificateError::TrailingBytes(rem.len()));
|
||||||
|
}
|
||||||
|
if cert.issuer().to_string() != cert.subject().to_string() {
|
||||||
|
return Err(TaCertificateError::NotSelfSignedIssuerSubject);
|
||||||
|
}
|
||||||
|
cert.verify_signature(None)
|
||||||
|
.map_err(|e| TaCertificateError::InvalidSelfSignature(e.to_string()))?;
|
||||||
|
|
||||||
|
Self::validate_rc_constraints(&rc_ca)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
raw_der: der.to_vec(),
|
||||||
|
rc_ca,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spki_der(&self) -> &[u8] {
|
||||||
|
&self.rc_ca.tbs.subject_public_key_info
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate TA-specific semantic constraints on a parsed Resource Certificate.
|
||||||
|
///
|
||||||
|
/// Note: this does not verify the X.509 signature; it is intended for higher-level logic and
|
||||||
|
/// for unit tests that exercise individual constraint branches.
|
||||||
|
pub fn validate_rc_constraints(rc_ca: &ResourceCertificate) -> Result<(), TaCertificateError> {
|
||||||
|
if rc_ca.kind != ResourceCertKind::Ca {
|
||||||
|
return Err(TaCertificateError::NotCa);
|
||||||
|
}
|
||||||
|
|
||||||
|
if rc_ca.tbs.extensions.certificate_policies_oid.as_deref() != Some(OID_CP_IPADDR_ASNUMBER) {
|
||||||
|
return Err(TaCertificateError::MissingOrInvalidCertificatePolicies);
|
||||||
|
}
|
||||||
|
if rc_ca.tbs.extensions.subject_key_identifier.is_none() {
|
||||||
|
return Err(TaCertificateError::MissingSubjectKeyIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ip = rc_ca.tbs.extensions.ip_resources.as_ref();
|
||||||
|
let asn = rc_ca.tbs.extensions.as_resources.as_ref();
|
||||||
|
if ip.is_none() && asn.is_none() {
|
||||||
|
return Err(TaCertificateError::ResourcesMissing);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut has_any_resource = false;
|
||||||
|
|
||||||
|
if let Some(ip) = ip {
|
||||||
|
if ip.has_any_inherit() {
|
||||||
|
return Err(TaCertificateError::IpResourcesInherit);
|
||||||
|
}
|
||||||
|
for fam in &ip.families {
|
||||||
|
match &fam.choice {
|
||||||
|
IpAddressChoice::Inherit => return Err(TaCertificateError::IpResourcesInherit),
|
||||||
|
IpAddressChoice::AddressesOrRanges(items) => {
|
||||||
|
if !items.is_empty() {
|
||||||
|
has_any_resource = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(asn) = asn {
|
||||||
|
if matches!(asn.asnum, Some(AsIdentifierChoice::Inherit))
|
||||||
|
|| matches!(asn.rdi, Some(AsIdentifierChoice::Inherit))
|
||||||
|
{
|
||||||
|
return Err(TaCertificateError::AsResourcesInherit);
|
||||||
|
}
|
||||||
|
if let Some(AsIdentifierChoice::AsIdsOrRanges(items)) = asn.asnum.as_ref() {
|
||||||
|
if !items.is_empty() {
|
||||||
|
has_any_resource = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(AsIdentifierChoice::AsIdsOrRanges(items)) = asn.rdi.as_ref() {
|
||||||
|
if !items.is_empty() {
|
||||||
|
has_any_resource = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !has_any_resource {
|
||||||
|
return Err(TaCertificateError::ResourcesEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct TrustAnchor {
|
||||||
|
pub tal: Tal,
|
||||||
|
pub ta_certificate: TaCertificate,
|
||||||
|
pub resolved_ta_uri: Option<Url>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum TrustAnchorError {
|
||||||
|
#[error("TA certificate error: {0} (RFC 8630 §2.3)")]
|
||||||
|
TaCertificate(#[from] TaCertificateError),
|
||||||
|
|
||||||
|
#[error("resolved TA URI not listed in TAL: {0} (RFC 8630 §2.2-§2.3)")]
|
||||||
|
ResolvedUriNotInTal(String),
|
||||||
|
|
||||||
|
#[error("TAL SPKI does not match TA certificate SubjectPublicKeyInfo (RFC 8630 §2.3; RFC 5280 §4.1.2.7)")]
|
||||||
|
TalSpkiMismatch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrustAnchor {
|
||||||
|
/// Bind a TAL and a downloaded TA certificate.
|
||||||
|
///
|
||||||
|
/// This does not download anything; it only validates the binding rules from RFC 8630 §2.3.
|
||||||
|
pub fn bind(tal: Tal, ta_der: &[u8], resolved_uri: Option<&Url>) -> Result<Self, TrustAnchorError> {
|
||||||
|
if let Some(u) = resolved_uri {
|
||||||
|
if !tal.ta_uris.iter().any(|x| x == u) {
|
||||||
|
return Err(TrustAnchorError::ResolvedUriNotInTal(u.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ta_certificate = TaCertificate::from_der(ta_der)?;
|
||||||
|
if tal.subject_public_key_info_der != ta_certificate.spki_der() {
|
||||||
|
return Err(TrustAnchorError::TalSpkiMismatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
tal,
|
||||||
|
ta_certificate,
|
||||||
|
resolved_ta_uri: resolved_uri.cloned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,18 +1,134 @@
|
|||||||
/// TAL Model
|
use base64::Engine;
|
||||||
#[derive(Clone, Debug)]
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct Tal {
|
pub struct Tal {
|
||||||
/// Optional human-readable comments
|
pub raw: Vec<u8>,
|
||||||
pub comments: Vec<String>,
|
pub comments: Vec<String>,
|
||||||
|
pub ta_uris: Vec<Url>,
|
||||||
/// Ordered list of URIs pointing to the TA certificate
|
pub subject_public_key_info_der: Vec<u8>,
|
||||||
pub uris: Vec<TalUri>,
|
|
||||||
|
|
||||||
/// SubjectPublicKeyInfo DER
|
|
||||||
pub spki_der: Vec<u8>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum TalUri {
|
pub enum TalDecodeError {
|
||||||
Rsync(String),
|
#[error("TAL must be valid UTF-8 (RFC 8630 §2.2)")]
|
||||||
Https(String),
|
InvalidUtf8,
|
||||||
|
|
||||||
|
#[error("TAL comments must appear only at the beginning (RFC 8630 §2.2)")]
|
||||||
|
CommentAfterHeader,
|
||||||
|
|
||||||
|
#[error("TAL must contain at least one TA URI line (RFC 8630 §2.2)")]
|
||||||
|
MissingTaUris,
|
||||||
|
|
||||||
|
#[error("TAL must contain an empty line separator between URI list and SPKI base64 (RFC 8630 §2.2)")]
|
||||||
|
MissingSeparatorEmptyLine,
|
||||||
|
|
||||||
|
#[error("TAL TA URI invalid: {0} (RFC 8630 §2.2)")]
|
||||||
|
InvalidUri(String),
|
||||||
|
|
||||||
|
#[error("TAL TA URI scheme must be rsync or https, got {0} (RFC 8630 §2.2)")]
|
||||||
|
UnsupportedUriScheme(String),
|
||||||
|
|
||||||
|
#[error("TAL TA URI must reference a single object (must not end with '/'): {0} (RFC 8630 §2.3)")]
|
||||||
|
UriIsDirectory(String),
|
||||||
|
|
||||||
|
#[error("TAL must contain base64-encoded SubjectPublicKeyInfo after the separator (RFC 8630 §2.2)")]
|
||||||
|
MissingSpki,
|
||||||
|
|
||||||
|
#[error("TAL SPKI base64 decode failed (RFC 8630 §2.2)")]
|
||||||
|
SpkiBase64Decode,
|
||||||
|
|
||||||
|
#[error("TAL SPKI DER is empty (RFC 8630 §2.2)")]
|
||||||
|
SpkiDerEmpty,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tal {
|
||||||
|
pub fn decode_bytes(input: &[u8]) -> Result<Self, TalDecodeError> {
|
||||||
|
let raw = input.to_vec();
|
||||||
|
let text = std::str::from_utf8(input).map_err(|_| TalDecodeError::InvalidUtf8)?;
|
||||||
|
|
||||||
|
let lines: Vec<&str> = text
|
||||||
|
.split('\n')
|
||||||
|
.map(|l| l.strip_suffix('\r').unwrap_or(l))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut idx = 0usize;
|
||||||
|
|
||||||
|
// 1) Leading comments.
|
||||||
|
let mut comments: Vec<String> = Vec::new();
|
||||||
|
while idx < lines.len() && lines[idx].starts_with('#') {
|
||||||
|
comments.push(lines[idx][1..].to_string());
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) URI list (one or more non-empty lines).
|
||||||
|
let mut ta_uris: Vec<Url> = Vec::new();
|
||||||
|
while idx < lines.len() {
|
||||||
|
let line = lines[idx].trim();
|
||||||
|
if line.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if line.starts_with('#') {
|
||||||
|
return Err(TalDecodeError::CommentAfterHeader);
|
||||||
|
}
|
||||||
|
let url = match Url::parse(line) {
|
||||||
|
Ok(u) => u,
|
||||||
|
Err(_) => {
|
||||||
|
if !ta_uris.is_empty() {
|
||||||
|
return Err(TalDecodeError::MissingSeparatorEmptyLine);
|
||||||
|
}
|
||||||
|
return Err(TalDecodeError::InvalidUri(line.to_string()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match url.scheme() {
|
||||||
|
"rsync" | "https" => {}
|
||||||
|
s => return Err(TalDecodeError::UnsupportedUriScheme(s.to_string())),
|
||||||
|
}
|
||||||
|
if url.path().ends_with('/') {
|
||||||
|
return Err(TalDecodeError::UriIsDirectory(line.to_string()));
|
||||||
|
}
|
||||||
|
if url.path_segments().and_then(|mut s| s.next_back()).unwrap_or("").is_empty() {
|
||||||
|
return Err(TalDecodeError::UriIsDirectory(line.to_string()));
|
||||||
|
}
|
||||||
|
ta_uris.push(url);
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ta_uris.is_empty() {
|
||||||
|
return Err(TalDecodeError::MissingTaUris);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Empty line separator (must exist).
|
||||||
|
if idx >= lines.len() || !lines[idx].trim().is_empty() {
|
||||||
|
return Err(TalDecodeError::MissingSeparatorEmptyLine);
|
||||||
|
}
|
||||||
|
idx += 1;
|
||||||
|
|
||||||
|
// 4) Base64(SPKI DER) remainder; allow line wrapping.
|
||||||
|
let mut b64 = String::new();
|
||||||
|
while idx < lines.len() {
|
||||||
|
let line = lines[idx].trim();
|
||||||
|
if !line.is_empty() {
|
||||||
|
b64.push_str(line);
|
||||||
|
}
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
if b64.is_empty() {
|
||||||
|
return Err(TalDecodeError::MissingSpki);
|
||||||
|
}
|
||||||
|
|
||||||
|
let spki_der = base64::engine::general_purpose::STANDARD
|
||||||
|
.decode(b64.as_bytes())
|
||||||
|
.map_err(|_| TalDecodeError::SpkiBase64Decode)?;
|
||||||
|
if spki_der.is_empty() {
|
||||||
|
return Err(TalDecodeError::SpkiDerEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
raw,
|
||||||
|
comments,
|
||||||
|
ta_uris,
|
||||||
|
subject_public_key_info_der: spki_der,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1 @@
|
|||||||
pub mod data_model;
|
pub mod data_model;
|
||||||
pub mod slurm;
|
|
||||||
pub mod rtr;
|
|
||||||
pub mod source;
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user