Compare commits
4 Commits
2fb36712e2
...
cc9f3f21de
| Author | SHA1 | Date | |
|---|---|---|---|
| cc9f3f21de | |||
| 56ae2ca4fc | |||
| bcd4829486 | |||
| 421847d329 |
@ -4,8 +4,12 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
der-parser = "10.0.0"
|
||||
der-parser = { version = "10.0.0", features = ["serialize"] }
|
||||
hex = "0.4.3"
|
||||
base64 = "0.22.1"
|
||||
sha2 = "0.10.8"
|
||||
thiserror = "2.0.18"
|
||||
time = "0.3.45"
|
||||
ring = "0.17.14"
|
||||
x509-parser = { version = "0.18.0", features = ["verify"] }
|
||||
url = "2.5.8"
|
||||
|
||||
17
README.md
17
README.md
@ -9,3 +9,20 @@ cargo test
|
||||
cargo test -- --nocapture
|
||||
```
|
||||
|
||||
# 覆盖率(cargo-llvm-cov)
|
||||
|
||||
安装工具:
|
||||
|
||||
```
|
||||
rustup component add llvm-tools-preview
|
||||
cargo install cargo-llvm-cov --locked
|
||||
```
|
||||
|
||||
统计行覆盖率并要求 >=90%:
|
||||
|
||||
```
|
||||
./scripts/coverage.sh
|
||||
# 或
|
||||
cargo llvm-cov --fail-under-lines 90
|
||||
```
|
||||
|
||||
|
||||
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.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.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.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 |
|
||||
|
||||
94
specs/01_tal.md
Normal file
94
specs/01_tal.md
Normal file
@ -0,0 +1,94 @@
|
||||
# 01. TAL(Trust Anchor Locator)
|
||||
|
||||
## 1.1 对象定位
|
||||
|
||||
TAL(Trust Anchor Locator)用于向 RP 提供:
|
||||
|
||||
1) 可检索“当前 TA 证书”的一个或多个 URI;以及
|
||||
2) 该 TA 证书的 `subjectPublicKeyInfo`(SPKI)期望值(用于绑定/防替换)。
|
||||
|
||||
RFC 8630 §2;RFC 8630 §2.3。
|
||||
|
||||
## 1.2 原始载体与编码
|
||||
|
||||
- 载体:文本文件(ASCII/UTF-8 兼容的行文本)。
|
||||
- 行结束:允许 `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 }
|
||||
```
|
||||
|
||||
其中 `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 解析规则(语义层)
|
||||
|
||||
输入:`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 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `raw` | `bytes` | TAL 原始文件字节 | 原样保留(可选但建议) | RFC 8630 §2.2 |
|
||||
| `comments` | `list[Utf8Text]` | 注释行(按出现顺序) | 每行以 `#` 开头;文本为 UTF-8;内容限制见 RFC 5198 §2 | 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`(可选细化)
|
||||
|
||||
> 若你的实现希望对 URI 做更强类型化,可在 `Tal.ta_uris` 上进一步拆分为 `TaUri` 结构。
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `uri` | `Uri` | 完整 URI 文本 | scheme 为 `rsync` 或 `https` | RFC 8630 §2.2 |
|
||||
| `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。
|
||||
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。
|
||||
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)。
|
||||
|
||||
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::prelude::FromDer;
|
||||
use x509_parser::prelude::X509Version;
|
||||
use x509_parser::revocation_list::CertificateRevocationList;
|
||||
use x509_parser::asn1_rs::Tag;
|
||||
use x509_parser::certificate::X509Certificate;
|
||||
use x509_parser::x509::SubjectPublicKeyInfo;
|
||||
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)]
|
||||
pub struct RevokedCert {
|
||||
pub serial_number: BigUnsigned,
|
||||
@ -73,52 +37,52 @@ pub struct RpkixCrl {
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
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),
|
||||
|
||||
#[error("trailing bytes after CRL DER: {0} bytes")]
|
||||
#[error("trailing bytes after CRL DER: {0} bytes (DER; RFC 5280 §5.1)")]
|
||||
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>),
|
||||
|
||||
#[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),
|
||||
|
||||
#[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,
|
||||
|
||||
#[error("CRL signatureAlgorithm must match TBSCertList.signature")]
|
||||
#[error("CRL signatureAlgorithm must match TBSCertList.signature (RFC 5280 §5.1)")]
|
||||
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),
|
||||
|
||||
#[error("unsupported CRL extension OID {0}")]
|
||||
#[error("unsupported CRL extension OID {0} (RFC 9829 §3.1)")]
|
||||
UnsupportedExtension(String),
|
||||
|
||||
#[error("duplicate CRL extension OID {0}")]
|
||||
#[error("duplicate CRL extension OID {0} (RFC 5280 §4.2; RFC 9829 §3.1)")]
|
||||
DuplicateExtension(String),
|
||||
|
||||
#[error("AuthorityKeyIdentifier must contain keyIdentifier")]
|
||||
#[error("AuthorityKeyIdentifier must contain keyIdentifier (RFC 5280 §5.2.1; RFC 9829 §3.1)")]
|
||||
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,
|
||||
|
||||
#[error("CRLNumber must be non-critical")]
|
||||
#[error("CRLNumber must be non-critical (RFC 9829 §3.1; RFC 5280 §5.2.3)")]
|
||||
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,
|
||||
|
||||
#[error("CRL entry extensions must not be present")]
|
||||
#[error("CRL entry extensions must not be present (RFC 6487 §5; RFC 5280 §5.1)")]
|
||||
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,
|
||||
|
||||
#[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 {
|
||||
field: &'static str,
|
||||
year: i32,
|
||||
@ -161,23 +125,23 @@ impl RpkixCrl {
|
||||
if !rc.extensions().is_empty() {
|
||||
return Err(CrlDecodeError::EntryExtensionsNotAllowed);
|
||||
}
|
||||
let revocation_date = asn1_time_to_model(rc.revocation_date);
|
||||
validate_time_encoding("revocationDate", &revocation_date)?;
|
||||
let revocation_date = crate::data_model::common::asn1_time_to_model(rc.revocation_date);
|
||||
validate_time_encoding_rfc5280("revocationDate", &revocation_date)?;
|
||||
Ok(RevokedCert {
|
||||
serial_number: biguint_to_big_unsigned(rc.serial()),
|
||||
serial_number: BigUnsigned::from_biguint(rc.serial()),
|
||||
revocation_date,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let this_update = asn1_time_to_model(crl.last_update());
|
||||
validate_time_encoding("thisUpdate", &this_update)?;
|
||||
let this_update = crate::data_model::common::asn1_time_to_model(crl.last_update());
|
||||
validate_time_encoding_rfc5280("thisUpdate", &this_update)?;
|
||||
|
||||
let next_update = crl
|
||||
.next_update()
|
||||
.map(asn1_time_to_model)
|
||||
.map(crate::data_model::common::asn1_time_to_model)
|
||||
.ok_or(CrlDecodeError::NextUpdateMissing)?;
|
||||
validate_time_encoding("nextUpdate", &next_update)?;
|
||||
validate_time_encoding_rfc5280("nextUpdate", &next_update)?;
|
||||
|
||||
Ok(RpkixCrl {
|
||||
raw_der: der.to_vec(),
|
||||
@ -267,53 +231,44 @@ impl RpkixCrl {
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CrlVerifyError {
|
||||
#[error("issuer certificate parse error: {0}")]
|
||||
#[error("issuer certificate parse error: {0} (RFC 5280 §4.1; RFC 6487 §4)")]
|
||||
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),
|
||||
|
||||
#[error("issuer SubjectPublicKeyInfo parse error: {0}")]
|
||||
#[error("issuer SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7)")]
|
||||
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),
|
||||
|
||||
#[error("CRL parse error: {0}")]
|
||||
#[error("CRL parse error: {0} (RFC 5280 §5.1; RFC 6487 §5)")]
|
||||
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),
|
||||
|
||||
#[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 {
|
||||
crl_issuer_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,
|
||||
|
||||
#[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,
|
||||
|
||||
#[error("CRL signature verification failed: {0}")]
|
||||
#[error("CRL signature verification failed: {0} (RFC 5280 §6.3.3(g); RFC 7935 §2)")]
|
||||
InvalidSignature(String),
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_time_encoding(field: &'static str, t: &Asn1TimeUtc) -> Result<(), CrlDecodeError> {
|
||||
fn validate_time_encoding_rfc5280(
|
||||
field: &'static str,
|
||||
t: &Asn1TimeUtc,
|
||||
) -> Result<(), CrlDecodeError> {
|
||||
let year = t.utc.year();
|
||||
let expected = if year <= 2049 {
|
||||
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> {
|
||||
match sig.parameters.as_ref() {
|
||||
None => Ok(()),
|
||||
Some(p) if p.tag() == Tag::Null => Ok(()),
|
||||
Some(_p) => Err(CrlDecodeError::InvalidSignatureAlgorithmParameters),
|
||||
if crate::data_model::common::algorithm_params_absent_or_null(sig) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(CrlDecodeError::InvalidSignatureAlgorithmParameters)
|
||||
}
|
||||
}
|
||||
|
||||
@ -367,7 +322,7 @@ fn parse_and_validate_extensions(exts: &[X509Extension<'_>]) -> Result<CrlExtens
|
||||
if n.bits() > 159 {
|
||||
return Err(CrlDecodeError::CrlNumberOutOfRange);
|
||||
}
|
||||
crl_number = Some(biguint_to_big_unsigned(&n));
|
||||
crl_number = Some(BigUnsigned::from_biguint(&n));
|
||||
}
|
||||
_ => 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>> {
|
||||
cert.extensions()
|
||||
.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,2 +1,10 @@
|
||||
pub mod common;
|
||||
pub mod crl;
|
||||
|
||||
pub mod rc;
|
||||
pub mod oid;
|
||||
pub mod signed_object;
|
||||
pub mod manifest;
|
||||
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";
|
||||
820
src/data_model/rc.rs
Normal file
820
src/data_model/rc.rs
Normal file
@ -0,0 +1,820 @@
|
||||
use der_parser::ber::{BerObjectContent, Class};
|
||||
use der_parser::der::{parse_der, DerObject, Tag};
|
||||
use der_parser::num_bigint::BigUint;
|
||||
use time::OffsetDateTime;
|
||||
use url::Url;
|
||||
use x509_parser::extensions::ParsedExtension;
|
||||
use x509_parser::prelude::{FromDer, X509Certificate, X509Extension, X509Version};
|
||||
|
||||
use crate::data_model::common::algorithm_params_absent_or_null;
|
||||
use crate::data_model::oid::{
|
||||
OID_AD_SIGNED_OBJECT, OID_AUTONOMOUS_SYS_IDS, OID_CP_IPADDR_ASNUMBER, OID_IP_ADDR_BLOCKS,
|
||||
OID_SHA256_WITH_RSA_ENCRYPTION, OID_SUBJECT_INFO_ACCESS, OID_SUBJECT_KEY_IDENTIFIER,
|
||||
};
|
||||
|
||||
/// Resource Certificate kind (semantic classification).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ResourceCertKind {
|
||||
Ca,
|
||||
Ee,
|
||||
}
|
||||
|
||||
/// A parsed RPKI Resource Certificate (RFC 6487) data model.
|
||||
///
|
||||
/// This module intentionally focuses on the semantics needed by Signed Object validation and
|
||||
/// object-specific EE certificate checks (MFT/ROA/ASPA), as described in
|
||||
/// `rpki/specs/03_resource_certificate_rc.md`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ResourceCertificate {
|
||||
pub raw_der: Vec<u8>,
|
||||
pub tbs: RpkixTbsCertificate,
|
||||
pub kind: ResourceCertKind,
|
||||
}
|
||||
|
||||
pub type ResourceCaCertificate = ResourceCertificate;
|
||||
pub type ResourceEeCertificate = ResourceCertificate;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RpkixTbsCertificate {
|
||||
pub version: u32,
|
||||
pub serial_number: BigUint,
|
||||
pub signature_algorithm: String,
|
||||
pub issuer_dn: String,
|
||||
pub subject_dn: String,
|
||||
pub validity_not_before: OffsetDateTime,
|
||||
pub validity_not_after: OffsetDateTime,
|
||||
/// DER encoding of SubjectPublicKeyInfo.
|
||||
pub subject_public_key_info: Vec<u8>,
|
||||
pub extensions: RcExtensions,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RcExtensions {
|
||||
pub basic_constraints_ca: bool,
|
||||
pub subject_key_identifier: Option<Vec<u8>>,
|
||||
pub subject_info_access: Option<SubjectInfoAccess>,
|
||||
pub certificate_policies_oid: Option<String>,
|
||||
|
||||
pub ip_resources: Option<IpResourceSet>,
|
||||
pub as_resources: Option<AsResourceSet>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum SubjectInfoAccess {
|
||||
Ca(SubjectInfoAccessCa),
|
||||
Ee(SubjectInfoAccessEe),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SubjectInfoAccessCa {
|
||||
pub access_descriptions: Vec<AccessDescription>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SubjectInfoAccessEe {
|
||||
pub signed_object_uris: Vec<Url>,
|
||||
/// The full list of access descriptions as carried in the SIA extension.
|
||||
pub access_descriptions: Vec<AccessDescription>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AccessDescription {
|
||||
pub access_method_oid: String,
|
||||
pub access_location: Url,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub enum Afi {
|
||||
Ipv4,
|
||||
Ipv6,
|
||||
}
|
||||
|
||||
impl Afi {
|
||||
pub fn ub(self) -> u16 {
|
||||
match self {
|
||||
Afi::Ipv4 => 32,
|
||||
Afi::Ipv6 => 128,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn octets_len(self) -> usize {
|
||||
match self {
|
||||
Afi::Ipv4 => 4,
|
||||
Afi::Ipv6 => 16,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct IpResourceSet {
|
||||
pub families: Vec<IpAddressFamily>,
|
||||
}
|
||||
|
||||
impl IpResourceSet {
|
||||
/// Decode the DER bytes carried inside the X.509 `extnValue` OCTET STRING for
|
||||
/// `id-pe-ipAddrBlocks` (RFC 3779 / RFC 6487).
|
||||
pub fn decode_extn_value(extn_value: &[u8]) -> Result<Self, IpResourceSetDecodeError> {
|
||||
parse_ip_addr_blocks(extn_value).map_err(|_| IpResourceSetDecodeError::InvalidEncoding)
|
||||
}
|
||||
|
||||
pub fn is_all_inherit(&self) -> bool {
|
||||
self.families
|
||||
.iter()
|
||||
.all(|f| matches!(f.choice, IpAddressChoice::Inherit))
|
||||
}
|
||||
|
||||
pub fn has_any_inherit(&self) -> bool {
|
||||
self.families
|
||||
.iter()
|
||||
.any(|f| matches!(f.choice, IpAddressChoice::Inherit))
|
||||
}
|
||||
|
||||
pub fn contains_prefix(&self, prefix: &IpPrefix) -> bool {
|
||||
self.families.iter().any(|fam| fam.contains_prefix(prefix))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum IpResourceSetDecodeError {
|
||||
#[error("invalid ipAddrBlocks encoding (RFC 3779 §2.2.3; RFC 6487 §4.8.10)")]
|
||||
InvalidEncoding,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct IpAddressFamily {
|
||||
pub afi: Afi,
|
||||
pub choice: IpAddressChoice,
|
||||
}
|
||||
|
||||
impl IpAddressFamily {
|
||||
pub fn contains_prefix(&self, prefix: &IpPrefix) -> bool {
|
||||
if self.afi != prefix.afi {
|
||||
return false;
|
||||
}
|
||||
match &self.choice {
|
||||
IpAddressChoice::Inherit => true,
|
||||
IpAddressChoice::AddressesOrRanges(items) => items.iter().any(|item| match item {
|
||||
IpAddressOrRange::Prefix(p) => prefix_covers(p, prefix),
|
||||
IpAddressOrRange::Range(r) => range_covers_prefix(self.afi, r, prefix),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum IpAddressChoice {
|
||||
Inherit,
|
||||
AddressesOrRanges(Vec<IpAddressOrRange>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum IpAddressOrRange {
|
||||
Prefix(IpPrefix),
|
||||
Range(IpAddressRange),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct IpAddressRange {
|
||||
pub min: Vec<u8>,
|
||||
pub max: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct IpPrefix {
|
||||
pub afi: Afi,
|
||||
pub prefix_len: u16,
|
||||
pub addr: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AsResourceSet {
|
||||
pub asnum: Option<AsIdentifierChoice>,
|
||||
pub rdi: Option<AsIdentifierChoice>,
|
||||
}
|
||||
|
||||
impl AsResourceSet {
|
||||
/// Decode the DER bytes carried inside the X.509 `extnValue` OCTET STRING for
|
||||
/// `id-pe-autonomousSysIds` (RFC 3779 / RFC 6487).
|
||||
pub fn decode_extn_value(extn_value: &[u8]) -> Result<Self, AsResourceSetDecodeError> {
|
||||
parse_as_identifiers(extn_value).map_err(|_| AsResourceSetDecodeError::InvalidEncoding)
|
||||
}
|
||||
|
||||
pub fn is_asnum_inherit(&self) -> bool {
|
||||
matches!(self.asnum, Some(AsIdentifierChoice::Inherit))
|
||||
}
|
||||
|
||||
pub fn has_any_range(&self) -> bool {
|
||||
self.asnum
|
||||
.as_ref()
|
||||
.map(|c| c.has_range())
|
||||
.unwrap_or(false)
|
||||
|| self.rdi.as_ref().map(|c| c.has_range()).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn asnum_single_id(&self) -> Option<u32> {
|
||||
match self.asnum.as_ref()? {
|
||||
AsIdentifierChoice::Inherit => None,
|
||||
AsIdentifierChoice::AsIdsOrRanges(items) => {
|
||||
if items.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
match &items[0] {
|
||||
AsIdOrRange::Id(v) => Some(*v),
|
||||
AsIdOrRange::Range { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AsResourceSetDecodeError {
|
||||
#[error("invalid autonomousSysIds encoding (RFC 3779 §3.2.3; RFC 6487 §4.8.11)")]
|
||||
InvalidEncoding,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum AsIdentifierChoice {
|
||||
Inherit,
|
||||
AsIdsOrRanges(Vec<AsIdOrRange>),
|
||||
}
|
||||
|
||||
impl AsIdentifierChoice {
|
||||
pub fn has_range(&self) -> bool {
|
||||
match self {
|
||||
AsIdentifierChoice::Inherit => false,
|
||||
AsIdentifierChoice::AsIdsOrRanges(items) => items.iter().any(|i| matches!(i, AsIdOrRange::Range { .. })),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum AsIdOrRange {
|
||||
Id(u32),
|
||||
Range { min: u32, max: u32 },
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ResourceCertificateError {
|
||||
#[error("X.509 parse error: {0} (RFC 5280 §4.1; RFC 6487 §4)")]
|
||||
Parse(String),
|
||||
|
||||
#[error("trailing bytes after certificate DER: {0} bytes (DER; RFC 5280 §4.1)")]
|
||||
TrailingBytes(usize),
|
||||
|
||||
#[error("certificate version must be v3 (RFC 5280 §4.1; RFC 6487 §4)")]
|
||||
InvalidVersion,
|
||||
|
||||
#[error("signatureAlgorithm does not match tbsCertificate.signature (RFC 5280 §4.1)")]
|
||||
SignatureAlgorithmMismatch,
|
||||
|
||||
#[error("unsupported signature algorithm (expected sha256WithRSAEncryption {OID_SHA256_WITH_RSA_ENCRYPTION}) (RFC 7935 §2; RFC 6487 §4)")]
|
||||
UnsupportedSignatureAlgorithm,
|
||||
|
||||
#[error("invalid signature algorithm parameters (RFC 5280 §4.1.1.2)")]
|
||||
InvalidSignatureAlgorithmParameters,
|
||||
|
||||
#[error("duplicate extension: {0} (RFC 5280 §4.2; RFC 6487 §4.8)")]
|
||||
DuplicateExtension(&'static str),
|
||||
|
||||
#[error("SubjectKeyIdentifier criticality must be non-critical (RFC 6487 §4.8.2)")]
|
||||
SkiCriticality,
|
||||
|
||||
#[error("SubjectInfoAccess criticality must be non-critical (RFC 6487 §4.8.8)")]
|
||||
SiaCriticality,
|
||||
|
||||
#[error("certificatePolicies criticality must be critical (RFC 6487 §4.8.9)")]
|
||||
CertificatePoliciesCriticality,
|
||||
|
||||
#[error("certificatePolicies must contain RPKI policy OID {OID_CP_IPADDR_ASNUMBER}, got {0} (RFC 6487 §4.8.9)")]
|
||||
InvalidCertificatePolicy(String),
|
||||
|
||||
#[error("SIA id-ad-signedObject accessLocation must be URI (RFC 6487 §4.8.8.2; RFC 5280 §4.2.2.2)")]
|
||||
SignedObjectSiaNotUri,
|
||||
|
||||
#[error("SIA id-ad-signedObject must include at least one rsync:// URI (RFC 6487 §4.8.8.2)")]
|
||||
SignedObjectSiaNoRsync,
|
||||
|
||||
#[error("invalid RFC 3779 IP resources extension (RFC 6487 §4.8.10; RFC 3779 §2.2)")]
|
||||
InvalidIpResources,
|
||||
|
||||
#[error("invalid RFC 3779 AS resources extension (RFC 6487 §4.8.11; RFC 3779 §3.2)")]
|
||||
InvalidAsResources,
|
||||
}
|
||||
|
||||
impl ResourceCertificate {
|
||||
pub fn from_der(der: &[u8]) -> Result<Self, ResourceCertificateError> {
|
||||
let (rem, cert) =
|
||||
X509Certificate::from_der(der).map_err(|e| ResourceCertificateError::Parse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(ResourceCertificateError::TrailingBytes(rem.len()));
|
||||
}
|
||||
|
||||
let version = match cert.version() {
|
||||
X509Version::V3 => 2u32,
|
||||
_ => return Err(ResourceCertificateError::InvalidVersion),
|
||||
};
|
||||
|
||||
let outer = &cert.signature_algorithm;
|
||||
let inner = &cert.tbs_certificate.signature;
|
||||
if outer.algorithm != inner.algorithm || outer.parameters != inner.parameters {
|
||||
return Err(ResourceCertificateError::SignatureAlgorithmMismatch);
|
||||
}
|
||||
if outer.algorithm.to_id_string() != OID_SHA256_WITH_RSA_ENCRYPTION {
|
||||
return Err(ResourceCertificateError::UnsupportedSignatureAlgorithm);
|
||||
}
|
||||
if !algorithm_params_absent_or_null(outer) {
|
||||
return Err(ResourceCertificateError::InvalidSignatureAlgorithmParameters);
|
||||
}
|
||||
|
||||
let validity_not_before = cert.validity().not_before.to_datetime();
|
||||
let validity_not_after = cert.validity().not_after.to_datetime();
|
||||
|
||||
let subject_public_key_info = cert.tbs_certificate.subject_pki.raw.to_vec();
|
||||
|
||||
let extensions = parse_extensions(cert.extensions())?;
|
||||
let kind = if extensions.basic_constraints_ca {
|
||||
ResourceCertKind::Ca
|
||||
} else {
|
||||
ResourceCertKind::Ee
|
||||
};
|
||||
|
||||
Ok(ResourceCertificate {
|
||||
raw_der: der.to_vec(),
|
||||
tbs: RpkixTbsCertificate {
|
||||
version,
|
||||
serial_number: cert.tbs_certificate.serial.clone(),
|
||||
signature_algorithm: outer.algorithm.to_id_string(),
|
||||
issuer_dn: cert.issuer().to_string(),
|
||||
subject_dn: cert.subject().to_string(),
|
||||
validity_not_before,
|
||||
validity_not_after,
|
||||
subject_public_key_info,
|
||||
extensions,
|
||||
},
|
||||
kind,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_extensions(exts: &[X509Extension<'_>]) -> Result<RcExtensions, ResourceCertificateError> {
|
||||
let mut basic_constraints_ca: Option<bool> = None;
|
||||
let mut ski: Option<Vec<u8>> = None;
|
||||
let mut sia: Option<SubjectInfoAccess> = None;
|
||||
let mut cert_policies_oid: Option<String> = None;
|
||||
|
||||
let mut ip_resources: Option<IpResourceSet> = None;
|
||||
let mut as_resources: Option<AsResourceSet> = None;
|
||||
|
||||
for ext in exts {
|
||||
let oid = ext.oid.to_id_string();
|
||||
match oid.as_str() {
|
||||
crate::data_model::oid::OID_BASIC_CONSTRAINTS => {
|
||||
if basic_constraints_ca.is_some() {
|
||||
return Err(ResourceCertificateError::DuplicateExtension("basicConstraints"));
|
||||
}
|
||||
let ParsedExtension::BasicConstraints(bc) = ext.parsed_extension() else {
|
||||
return Err(ResourceCertificateError::Parse("basicConstraints parse failed".into()));
|
||||
};
|
||||
basic_constraints_ca = Some(bc.ca);
|
||||
}
|
||||
OID_SUBJECT_KEY_IDENTIFIER => {
|
||||
if ski.is_some() {
|
||||
return Err(ResourceCertificateError::DuplicateExtension("subjectKeyIdentifier"));
|
||||
}
|
||||
if ext.critical {
|
||||
return Err(ResourceCertificateError::SkiCriticality);
|
||||
}
|
||||
let ParsedExtension::SubjectKeyIdentifier(s) = ext.parsed_extension() else {
|
||||
return Err(ResourceCertificateError::Parse("subjectKeyIdentifier parse failed".into()));
|
||||
};
|
||||
ski = Some(s.0.to_vec());
|
||||
}
|
||||
OID_SUBJECT_INFO_ACCESS => {
|
||||
if sia.is_some() {
|
||||
return Err(ResourceCertificateError::DuplicateExtension("subjectInfoAccess"));
|
||||
}
|
||||
if ext.critical {
|
||||
return Err(ResourceCertificateError::SiaCriticality);
|
||||
}
|
||||
let ParsedExtension::SubjectInfoAccess(s) = ext.parsed_extension() else {
|
||||
return Err(ResourceCertificateError::Parse("subjectInfoAccess parse failed".into()));
|
||||
};
|
||||
sia = Some(parse_sia(s.accessdescs.as_slice())?);
|
||||
}
|
||||
crate::data_model::oid::OID_CERTIFICATE_POLICIES => {
|
||||
if cert_policies_oid.is_some() {
|
||||
return Err(ResourceCertificateError::DuplicateExtension("certificatePolicies"));
|
||||
}
|
||||
if !ext.critical {
|
||||
return Err(ResourceCertificateError::CertificatePoliciesCriticality);
|
||||
}
|
||||
let ParsedExtension::CertificatePolicies(cp) = ext.parsed_extension() else {
|
||||
return Err(ResourceCertificateError::Parse("certificatePolicies parse failed".into()));
|
||||
};
|
||||
if cp.len() != 1 {
|
||||
return Err(ResourceCertificateError::InvalidCertificatePolicy(
|
||||
"expected exactly one policy".into(),
|
||||
));
|
||||
}
|
||||
let policy_oid = cp[0].policy_id.to_id_string();
|
||||
if policy_oid != OID_CP_IPADDR_ASNUMBER {
|
||||
return Err(ResourceCertificateError::InvalidCertificatePolicy(policy_oid));
|
||||
}
|
||||
cert_policies_oid = Some(OID_CP_IPADDR_ASNUMBER.to_string());
|
||||
}
|
||||
OID_IP_ADDR_BLOCKS => {
|
||||
if ip_resources.is_some() {
|
||||
return Err(ResourceCertificateError::DuplicateExtension("ipAddrBlocks"));
|
||||
}
|
||||
// Must be critical per RPKI profile; we only enforce when present.
|
||||
if !ext.critical {
|
||||
return Err(ResourceCertificateError::InvalidIpResources);
|
||||
}
|
||||
let parsed = IpResourceSet::decode_extn_value(ext.value)
|
||||
.map_err(|_e| ResourceCertificateError::InvalidIpResources)?;
|
||||
ip_resources = Some(parsed);
|
||||
}
|
||||
OID_AUTONOMOUS_SYS_IDS => {
|
||||
if as_resources.is_some() {
|
||||
return Err(ResourceCertificateError::DuplicateExtension("autonomousSysIds"));
|
||||
}
|
||||
if !ext.critical {
|
||||
return Err(ResourceCertificateError::InvalidAsResources);
|
||||
}
|
||||
let parsed = AsResourceSet::decode_extn_value(ext.value)
|
||||
.map_err(|_e| ResourceCertificateError::InvalidAsResources)?;
|
||||
as_resources = Some(parsed);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let basic_constraints_ca = basic_constraints_ca.unwrap_or(false);
|
||||
|
||||
Ok(RcExtensions {
|
||||
basic_constraints_ca,
|
||||
subject_key_identifier: ski,
|
||||
subject_info_access: sia,
|
||||
certificate_policies_oid: cert_policies_oid,
|
||||
ip_resources,
|
||||
as_resources,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_sia(access: &[x509_parser::extensions::AccessDescription<'_>]) -> Result<SubjectInfoAccess, ResourceCertificateError> {
|
||||
let mut all = Vec::with_capacity(access.len());
|
||||
let mut signed_object_uris: Vec<Url> = Vec::new();
|
||||
|
||||
for ad in access {
|
||||
let access_method_oid = ad.access_method.to_id_string();
|
||||
let uri = match &ad.access_location {
|
||||
x509_parser::extensions::GeneralName::URI(u) => u,
|
||||
_ => {
|
||||
if access_method_oid == OID_AD_SIGNED_OBJECT {
|
||||
return Err(ResourceCertificateError::SignedObjectSiaNotUri);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let url =
|
||||
Url::parse(uri).map_err(|_| ResourceCertificateError::Parse(format!("invalid URI: {uri}")))?;
|
||||
if access_method_oid == OID_AD_SIGNED_OBJECT {
|
||||
signed_object_uris.push(url.clone());
|
||||
}
|
||||
all.push(AccessDescription {
|
||||
access_method_oid,
|
||||
access_location: url,
|
||||
});
|
||||
}
|
||||
|
||||
if signed_object_uris.is_empty() {
|
||||
return Ok(SubjectInfoAccess::Ca(SubjectInfoAccessCa {
|
||||
access_descriptions: all,
|
||||
}));
|
||||
}
|
||||
|
||||
if !signed_object_uris.iter().any(|u| u.scheme() == "rsync") {
|
||||
return Err(ResourceCertificateError::SignedObjectSiaNoRsync);
|
||||
}
|
||||
|
||||
Ok(SubjectInfoAccess::Ee(SubjectInfoAccessEe {
|
||||
signed_object_uris,
|
||||
access_descriptions: all,
|
||||
}))
|
||||
}
|
||||
|
||||
fn parse_ip_addr_blocks(ext_value: &[u8]) -> Result<IpResourceSet, ()> {
|
||||
let (rem, obj) = parse_der(ext_value).map_err(|_| ())?;
|
||||
if !rem.is_empty() {
|
||||
return Err(());
|
||||
}
|
||||
let seq = obj.as_sequence().map_err(|_| ())?;
|
||||
let mut families = Vec::with_capacity(seq.len());
|
||||
for fam in seq {
|
||||
let fam_seq = fam.as_sequence().map_err(|_| ())?;
|
||||
if fam_seq.len() != 2 {
|
||||
return Err(());
|
||||
}
|
||||
let af_bytes = fam_seq[0].as_slice().map_err(|_| ())?;
|
||||
if af_bytes.len() != 2 {
|
||||
return Err(());
|
||||
}
|
||||
let afi = match af_bytes {
|
||||
[0x00, 0x01] => Afi::Ipv4,
|
||||
[0x00, 0x02] => Afi::Ipv6,
|
||||
_ => return Err(()),
|
||||
};
|
||||
|
||||
let choice = match &fam_seq[1].content {
|
||||
BerObjectContent::Null => IpAddressChoice::Inherit,
|
||||
BerObjectContent::Sequence(_) => {
|
||||
let items_seq = fam_seq[1].as_sequence().map_err(|_| ())?;
|
||||
let mut items = Vec::with_capacity(items_seq.len());
|
||||
for item in items_seq {
|
||||
items.push(parse_ip_address_or_range(afi, item)?);
|
||||
}
|
||||
IpAddressChoice::AddressesOrRanges(items)
|
||||
}
|
||||
_ => return Err(()),
|
||||
};
|
||||
families.push(IpAddressFamily { afi, choice });
|
||||
}
|
||||
Ok(IpResourceSet { families })
|
||||
}
|
||||
|
||||
fn parse_ip_address_or_range(afi: Afi, obj: &DerObject<'_>) -> Result<IpAddressOrRange, ()> {
|
||||
match &obj.content {
|
||||
BerObjectContent::BitString(_, _) => Ok(IpAddressOrRange::Prefix(parse_ip_prefix(afi, obj)?)),
|
||||
BerObjectContent::Sequence(_) => {
|
||||
let seq = obj.as_sequence().map_err(|_| ())?;
|
||||
if seq.len() != 2 {
|
||||
return Err(());
|
||||
}
|
||||
let min = parse_ip_address_bound(afi, &seq[0], false)?;
|
||||
let max = parse_ip_address_bound(afi, &seq[1], true)?;
|
||||
Ok(IpAddressOrRange::Range(IpAddressRange { min, max }))
|
||||
}
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_ip_prefix(afi: Afi, obj: &DerObject<'_>) -> Result<IpPrefix, ()> {
|
||||
let (unused_bits, bytes) = match &obj.content {
|
||||
BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()),
|
||||
_ => return Err(()),
|
||||
};
|
||||
if unused_bits > 7 {
|
||||
return Err(());
|
||||
}
|
||||
if !bytes.is_empty() && unused_bits != 0 {
|
||||
let mask = (1u8 << unused_bits) - 1;
|
||||
if (bytes[bytes.len() - 1] & mask) != 0 {
|
||||
return Err(());
|
||||
}
|
||||
} else if bytes.is_empty() && unused_bits != 0 {
|
||||
return Err(());
|
||||
}
|
||||
let prefix_len = (bytes.len() * 8)
|
||||
.checked_sub(unused_bits as usize)
|
||||
.ok_or(())? as u16;
|
||||
if prefix_len > afi.ub() {
|
||||
return Err(());
|
||||
}
|
||||
let addr = canonicalize_prefix_addr(afi, prefix_len, &bytes);
|
||||
Ok(IpPrefix {
|
||||
afi,
|
||||
prefix_len,
|
||||
addr,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse an RFC 3779 `IPAddress` BIT STRING into an address-like byte array.
|
||||
///
|
||||
/// When used as an `IPAddressRange` endpoint, RFC 3779 allows endpoints to be encoded with
|
||||
/// fewer than `ub` bits. In that case, the missing bits are interpreted as 0s for the lower
|
||||
/// bound and 1s for the upper bound. This is essential to correctly interpret ranges that
|
||||
/// are expressed on non-octet boundaries.
|
||||
fn parse_ip_address_bound(afi: Afi, obj: &DerObject<'_>, fill_remaining_ones: bool) -> Result<Vec<u8>, ()> {
|
||||
let (unused_bits, bytes) = match &obj.content {
|
||||
BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()),
|
||||
_ => return Err(()),
|
||||
};
|
||||
if unused_bits > 7 {
|
||||
return Err(());
|
||||
}
|
||||
if !bytes.is_empty() && unused_bits != 0 {
|
||||
let mask = (1u8 << unused_bits) - 1;
|
||||
if (bytes[bytes.len() - 1] & mask) != 0 {
|
||||
return Err(());
|
||||
}
|
||||
} else if bytes.is_empty() && unused_bits != 0 {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let bit_len: u16 = (bytes.len() * 8)
|
||||
.checked_sub(unused_bits as usize)
|
||||
.ok_or(())?
|
||||
.try_into()
|
||||
.map_err(|_| ())?;
|
||||
if bit_len > afi.ub() {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let mut out = vec![0u8; afi.octets_len()];
|
||||
let copy_len = bytes.len().min(out.len());
|
||||
out[..copy_len].copy_from_slice(&bytes[..copy_len]);
|
||||
|
||||
if fill_remaining_ones {
|
||||
if bit_len == 0 {
|
||||
for b in &mut out {
|
||||
*b = 0xFF;
|
||||
}
|
||||
return Ok(out);
|
||||
}
|
||||
|
||||
let last_bit = (bit_len - 1) as usize;
|
||||
let last_byte = last_bit / 8;
|
||||
let rem = (bit_len % 8) as u8;
|
||||
|
||||
if rem != 0 && last_byte < out.len() {
|
||||
// Set the (8-rem) trailing bits in the last byte to 1.
|
||||
let mask: u8 = (1u8 << (8 - rem)) - 1;
|
||||
out[last_byte] |= mask;
|
||||
}
|
||||
for b in out.iter_mut().skip(last_byte + 1) {
|
||||
*b = 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn parse_as_identifiers(ext_value: &[u8]) -> Result<AsResourceSet, ()> {
|
||||
let (rem, obj) = parse_der(ext_value).map_err(|_| ())?;
|
||||
if !rem.is_empty() {
|
||||
return Err(());
|
||||
}
|
||||
let seq = obj.as_sequence().map_err(|_| ())?;
|
||||
let mut asnum: Option<AsIdentifierChoice> = None;
|
||||
let mut rdi: Option<AsIdentifierChoice> = None;
|
||||
for item in seq {
|
||||
if item.class() != Class::ContextSpecific {
|
||||
return Err(());
|
||||
}
|
||||
match item.tag() {
|
||||
Tag(0) => {
|
||||
if asnum.is_some() {
|
||||
return Err(());
|
||||
}
|
||||
let inner = parse_explicit_inner(item)?;
|
||||
asnum = Some(parse_as_identifier_choice(&inner)?);
|
||||
}
|
||||
Tag(1) => {
|
||||
if rdi.is_some() {
|
||||
return Err(());
|
||||
}
|
||||
let inner = parse_explicit_inner(item)?;
|
||||
rdi = Some(parse_as_identifier_choice(&inner)?);
|
||||
}
|
||||
_ => return Err(()),
|
||||
}
|
||||
}
|
||||
Ok(AsResourceSet { asnum, rdi })
|
||||
}
|
||||
|
||||
fn parse_explicit_inner<'a>(obj: &'a DerObject<'a>) -> Result<DerObject<'a>, ()> {
|
||||
let inner_der = obj.as_slice().map_err(|_| ())?;
|
||||
let (rem, inner) = parse_der(inner_der).map_err(|_| ())?;
|
||||
if !rem.is_empty() {
|
||||
return Err(());
|
||||
}
|
||||
Ok(inner)
|
||||
}
|
||||
|
||||
fn parse_as_identifier_choice(obj: &DerObject<'_>) -> Result<AsIdentifierChoice, ()> {
|
||||
match &obj.content {
|
||||
BerObjectContent::Null => Ok(AsIdentifierChoice::Inherit),
|
||||
BerObjectContent::Sequence(_) => {
|
||||
let seq = obj.as_sequence().map_err(|_| ())?;
|
||||
let mut items = Vec::with_capacity(seq.len());
|
||||
for item in seq {
|
||||
items.push(parse_as_id_or_range(item)?);
|
||||
}
|
||||
Ok(AsIdentifierChoice::AsIdsOrRanges(items))
|
||||
}
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_as_id_or_range(obj: &DerObject<'_>) -> Result<AsIdOrRange, ()> {
|
||||
match &obj.content {
|
||||
BerObjectContent::Integer(_) => {
|
||||
let v = obj.as_u64().map_err(|_| ())?;
|
||||
if v > u32::MAX as u64 {
|
||||
return Err(());
|
||||
}
|
||||
Ok(AsIdOrRange::Id(v as u32))
|
||||
}
|
||||
BerObjectContent::Sequence(_) => {
|
||||
let seq = obj.as_sequence().map_err(|_| ())?;
|
||||
if seq.len() != 2 {
|
||||
return Err(());
|
||||
}
|
||||
let min = seq[0].as_u64().map_err(|_| ())?;
|
||||
let max = seq[1].as_u64().map_err(|_| ())?;
|
||||
if min > u32::MAX as u64 || max > u32::MAX as u64 || min > max {
|
||||
return Err(());
|
||||
}
|
||||
Ok(AsIdOrRange::Range {
|
||||
min: min as u32,
|
||||
max: max as u32,
|
||||
})
|
||||
}
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn canonicalize_prefix_addr(afi: Afi, prefix_len: u16, bytes: &[u8]) -> Vec<u8> {
|
||||
let full_len = afi.octets_len();
|
||||
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 && last_prefix_byte < addr.len() {
|
||||
let mask: u8 = 0xFF << (8 - rem);
|
||||
addr[last_prefix_byte] &= mask;
|
||||
}
|
||||
addr
|
||||
}
|
||||
|
||||
fn prefix_covers(resource: &IpPrefix, subject: &IpPrefix) -> bool {
|
||||
if resource.afi != subject.afi {
|
||||
return false;
|
||||
}
|
||||
if resource.prefix_len > subject.prefix_len {
|
||||
return false;
|
||||
}
|
||||
let n = resource.prefix_len as usize;
|
||||
let whole = n / 8;
|
||||
let rem = (n % 8) as u8;
|
||||
if resource.addr.len() != subject.addr.len() {
|
||||
return false;
|
||||
}
|
||||
if resource.addr[..whole] != subject.addr[..whole] {
|
||||
return false;
|
||||
}
|
||||
if rem == 0 {
|
||||
return true;
|
||||
}
|
||||
let mask = 0xFFu8 << (8 - rem);
|
||||
(resource.addr[whole] & mask) == (subject.addr[whole] & mask)
|
||||
}
|
||||
|
||||
fn prefix_range(afi: Afi, p: &IpPrefix) -> (u128, u128) {
|
||||
let mut base_bytes = [0u8; 16];
|
||||
match afi {
|
||||
Afi::Ipv4 => {
|
||||
base_bytes[12..].copy_from_slice(&p.addr[..4]);
|
||||
}
|
||||
Afi::Ipv6 => {
|
||||
base_bytes.copy_from_slice(&p.addr[..16]);
|
||||
}
|
||||
}
|
||||
let base = u128::from_be_bytes(base_bytes);
|
||||
let host_bits = (afi.ub() - p.prefix_len) as u32;
|
||||
if host_bits == 0 {
|
||||
return (base, base);
|
||||
}
|
||||
let mask = (1u128 << host_bits) - 1;
|
||||
(base, base | mask)
|
||||
}
|
||||
|
||||
fn range_covers_prefix(afi: Afi, r: &IpAddressRange, p: &IpPrefix) -> bool {
|
||||
let (p_min, p_max) = prefix_range(afi, p);
|
||||
let r_min = bytes_to_u128(afi, &r.min);
|
||||
let r_max = bytes_to_u128(afi, &r.max);
|
||||
r_min <= p_min && p_max <= r_max
|
||||
}
|
||||
|
||||
fn bytes_to_u128(afi: Afi, bytes: &[u8]) -> u128 {
|
||||
let mut out = [0u8; 16];
|
||||
match afi {
|
||||
Afi::Ipv4 => {
|
||||
let copy_len = bytes.len().min(4);
|
||||
out[12..12 + copy_len].copy_from_slice(&bytes[..copy_len]);
|
||||
}
|
||||
Afi::Ipv6 => {
|
||||
let copy_len = bytes.len().min(16);
|
||||
out[..copy_len].copy_from_slice(&bytes[..copy_len]);
|
||||
}
|
||||
}
|
||||
u128::from_be_bytes(out)
|
||||
}
|
||||
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..]
|
||||
}
|
||||
}
|
||||
188
src/data_model/ta.rs
Normal file
188
src/data_model/ta.rs
Normal file
@ -0,0 +1,188 @@
|
||||
use url::Url;
|
||||
use x509_parser::prelude::{FromDer, X509Certificate};
|
||||
|
||||
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)]
|
||||
pub struct TaCertificate {
|
||||
pub raw_der: Vec<u8>,
|
||||
pub rc_ca: ResourceCertificate,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TaCertificateError {
|
||||
#[error("TA certificate parse error: {0} (RFC 5280 §4.1; RFC 6487 §4; RFC 8630 §2.3)")]
|
||||
Parse(String),
|
||||
|
||||
#[error("trailing bytes after TA certificate DER: {0} bytes (DER; RFC 5280 §4.1; RFC 6487 §4)")]
|
||||
TrailingBytes(usize),
|
||||
|
||||
#[error("TA certificate must be a CA certificate (RFC 8630 §2.3; RFC 6487 §4.8.1)")]
|
||||
NotCa,
|
||||
|
||||
#[error("TA certificate must be self-signed (issuer DN must equal subject DN) (RFC 8630 §2.3; RFC 5280 §4.1.2.4)")]
|
||||
NotSelfSignedIssuerSubject,
|
||||
|
||||
#[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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
134
src/data_model/tal.rs
Normal file
134
src/data_model/tal.rs
Normal file
@ -0,0 +1,134 @@
|
||||
use base64::Engine;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Tal {
|
||||
pub raw: Vec<u8>,
|
||||
pub comments: Vec<String>,
|
||||
pub ta_uris: Vec<Url>,
|
||||
pub subject_public_key_info_der: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TalDecodeError {
|
||||
#[error("TAL must be valid UTF-8 (RFC 8630 §2.2)")]
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/E0AAy4jgV_BtY7GQELCxALV6Q5U.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/E0AAy4jgV_BtY7GQELCxALV6Q5U.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/IdgNYO8v_dEbdoPuHFTpsjA3l0U.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/IdgNYO8v_dEbdoPuHFTpsjA3l0U.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/UDn6-ZD0WTj18D8nHih3D--ZO_s.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/UDn6-ZD0WTj18D8nHih3D--ZO_s.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/Vvx1bC8uAflbQPltsuM_idhPJzQ.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/Vvx1bC8uAflbQPltsuM_idhPJzQ.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ZW5EIqvxKWSSAOsBmoFfKxIjbpI.cer
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ZW5EIqvxKWSSAOsBmoFfKxIjbpI.cer
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.mft
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.mft
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/fDvN8JDIeg8h7b5wWCFK_F7J_SY.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/fDvN8JDIeg8h7b5wWCFK_F7J_SY.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/fykdvBDNLvaFDLOeKcvquSaaNlM.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/fykdvBDNLvaFDLOeKcvquSaaNlM.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/lAN537Pqyt3IsX0kCwc4qs0Cejk.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/lAN537Pqyt3IsX0kCwc4qs0Cejk.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/laI1_vEYtlNzj-K0SXR_yGpd1TA.gbr
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/laI1_vEYtlNzj-K0SXR_yGpd1TA.gbr
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ovsCA/IOUcOeBGM_Tb4dwfvswY4bnNZYY.crl
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ovsCA/IOUcOeBGM_Tb4dwfvswY4bnNZYY.crl
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ovsCA/IOUcOeBGM_Tb4dwfvswY4bnNZYY.mft
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ovsCA/IOUcOeBGM_Tb4dwfvswY4bnNZYY.mft
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/xF8fZFbI8NRA4qk_N5CLpw6Nmj8.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/xF8fZFbI8NRA4qk_N5CLpw6Nmj8.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/y9dGCqfvXd6vKRjYx3GJikg6pSw.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/y9dGCqfvXd6vKRjYx3GJikg6pSw.roa
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nlrssf/cdFOuyVdwFjUv6WlHJP3P4MKuI8.crl
vendored
Normal file
BIN
tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nlrssf/cdFOuyVdwFjUv6WlHJP3P4MKuI8.crl
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nlrssf/cdFOuyVdwFjUv6WlHJP3P4MKuI8.mft
vendored
Normal file
BIN
tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nlrssf/cdFOuyVdwFjUv6WlHJP3P4MKuI8.mft
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/ta/afrinic-ta.cer
vendored
Normal file
BIN
tests/fixtures/ta/afrinic-ta.cer
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/ta/apnic-ta.cer
vendored
Normal file
BIN
tests/fixtures/ta/apnic-ta.cer
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/ta/arin-ta.cer
vendored
Normal file
BIN
tests/fixtures/ta/arin-ta.cer
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/ta/lacnic-ta.cer
vendored
Normal file
BIN
tests/fixtures/ta/lacnic-ta.cer
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/ta/ripe-ncc-ta.cer
vendored
Normal file
BIN
tests/fixtures/ta/ripe-ncc-ta.cer
vendored
Normal file
Binary file not shown.
10
tests/fixtures/tal/afrinic.tal
vendored
Normal file
10
tests/fixtures/tal/afrinic.tal
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
rsync://rpki.afrinic.net/repository/AfriNIC.cer
|
||||
https://rpki.afrinic.net/repository/AfriNIC.cer
|
||||
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxsAqAhWIO+ON2Ef9oRDM
|
||||
pKxv+AfmSLIdLWJtjrvUyDxJPBjgR+kVrOHUeTaujygFUp49tuN5H2C1rUuQavTH
|
||||
vve6xNF5fU3OkTcqEzMOZy+ctkbde2SRMVdvbO22+TH9gNhKDc9l7Vu01qU4LeJH
|
||||
k3X0f5uu5346YrGAOSv6AaYBXVgXxa0s9ZvgqFpim50pReQe/WI3QwFKNgpPzfQL
|
||||
6Y7fDPYdYaVOXPXSKtx7P4s4KLA/ZWmRL/bobw/i2fFviAGhDrjqqqum+/9w1hEl
|
||||
L/vqihVnV18saKTnLvkItA/Bf5i11Yhw2K7qv573YWxyuqCknO/iYLTR1DToBZcZ
|
||||
UQIDAQAB
|
||||
10
tests/fixtures/tal/apnic-rfc7730-https.tal
vendored
Normal file
10
tests/fixtures/tal/apnic-rfc7730-https.tal
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer
|
||||
rsync://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer
|
||||
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9RWSL61YAAYumEiU8z8
|
||||
qH2ETVIL01ilxZlzIL9JYSORMN5Cmtf8V2JblIealSqgOTGjvSjEsiV73s67zYQI
|
||||
7C/iSOb96uf3/s86NqbxDiFQGN8qG7RNcdgVuUlAidl8WxvLNI8VhqbAB5uSg/Mr
|
||||
LeSOvXRja041VptAxIhcGzDMvlAJRwkrYK/Mo8P4E2rSQgwqCgae0ebY1CsJ3Cjf
|
||||
i67C1nw7oXqJJovvXJ4apGmEv8az23OLC6Ki54Ul/E6xk227BFttqFV3YMtKx42H
|
||||
cCcDVZZy01n7JjzvO8ccaXmHIgR7utnqhBRNNq5Xc5ZhbkrUsNtiJmrZzVlgU6Ou
|
||||
0wIDAQAB
|
||||
19
tests/fixtures/tal/arin.tal
vendored
Normal file
19
tests/fixtures/tal/arin.tal
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
# THIS TRUST ANCHOR LOCATOR IS PROVIDED BY THE AMERICAN REGISTRY FOR
|
||||
# INTERNET NUMBERS (ARIN) "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
# IN NO EVENT SHALL ARIN BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS PUBLIC KEY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
rsync://rpki.arin.net/repository/arin-rpki-ta.cer
|
||||
https://rrdp.arin.net/arin-rpki-ta.cer
|
||||
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3lZPjbHvMRV5sDDqfLc/685th5FnreHMJjg8
|
||||
pEZUbG8Y8TQxSBsDebbsDpl3Ov3Cj1WtdrJ3CIfQODCPrrJdOBSrMATeUbPC+JlNf2SRP3UB+VJFgtTj
|
||||
0RN8cEYIuhBW5t6AxQbHhdNQH+A1F/OJdw0q9da2U29Lx85nfFxvnC1EpK9CbLJS4m37+RlpNbT1cba+
|
||||
b+loXpx0Qcb1C4UpJCGDy7uNf5w6/+l7RpATAHqqsX4qCtwwDYlbHzp2xk9owF3mkCxzl0HwncO+sEHH
|
||||
eaL3OjtwdIGrRGeHi2Mpt+mvWHhtQqVG+51MHTyg+nIjWFKKGx1Q9+KDx4wJStwveQIDAQAB
|
||||
4
tests/fixtures/tal/lacnic.tal
vendored
Normal file
4
tests/fixtures/tal/lacnic.tal
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
https://rrdp.lacnic.net/ta/rta-lacnic-rpki.cer
|
||||
rsync://repository.lacnic.net/rpki/lacnic/rta-lacnic-rpki.cer
|
||||
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqZEzhYK0+PtDOPfub/KRc3MeWx3neXx4/wbnJWGbNAtbYqXg3uU5J4HFzPgk/VIppgSKAhlO0H60DRP48by9gr5/yDHu2KXhOmnMg46sYsUIpfgtBS9+VtrqWziJfb+pkGtuOWeTnj6zBmBNZKK+5AlMCW1WPhrylIcB+XSZx8tk9GS/3SMQ+YfMVwwAyYjsex14Uzto4GjONALE5oh1M3+glRQduD6vzSwOD+WahMbc9vCOTED+2McLHRKgNaQf0YJ9a1jG9oJIvDkKXEqdfqDRktwyoD74cV57bW3tBAexB7GglITbInyQAsmdngtfg2LUMrcROHHP86QPZINjDQIDAQAB
|
||||
10
tests/fixtures/tal/ripe-ncc.tal
vendored
Normal file
10
tests/fixtures/tal/ripe-ncc.tal
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
https://rpki.ripe.net/ta/ripe-ncc-ta.cer
|
||||
rsync://rpki.ripe.net/ta/ripe-ncc-ta.cer
|
||||
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0URYSGqUz2myBsOzeW1j
|
||||
Q6NsxNvlLMyhWknvnl8NiBCs/T/S2XuNKQNZ+wBZxIgPPV2pFBFeQAvoH/WK83Hw
|
||||
A26V2siwm/MY2nKZ+Olw+wlpzlZ1p3Ipj2eNcKrmit8BwBC8xImzuCGaV0jkRB0G
|
||||
Z0hoH6Ml03umLprRsn6v0xOP0+l6Qc1ZHMFVFb385IQ7FQQTcVIxrdeMsoyJq9eM
|
||||
kE6DoclHhF/NlSllXubASQ9KUWqJ0+Ot3QCXr4LXECMfkpkVR2TZT+v5v658bHVs
|
||||
6ZxRD1b6Uk1uQKAyHUbn/tXvP8lrjAibGzVsXDT2L0x4Edx+QdixPgOji3gBMyL2
|
||||
VwIDAQAB
|
||||
25
tests/test_aspa_decode.rs
Normal file
25
tests/test_aspa_decode.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use rpki::data_model::aspa::{AspaDecodeError, AspaObject};
|
||||
|
||||
#[test]
|
||||
fn decode_aspa_fixture_smoke() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa",
|
||||
)
|
||||
.expect("read ASPA fixture");
|
||||
let aspa = AspaObject::decode_der(&der).expect("decode aspa");
|
||||
assert_eq!(aspa.econtent_type, rpki::data_model::oid::OID_CT_ASPA);
|
||||
assert_eq!(aspa.aspa.version, 1);
|
||||
assert_ne!(aspa.aspa.customer_as_id, 0);
|
||||
assert!(!aspa.aspa.provider_as_ids.is_empty());
|
||||
println!("{aspa:#?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rejects_non_aspa_econtent_type() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa",
|
||||
)
|
||||
.expect("read ROA fixture");
|
||||
let err = AspaObject::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::InvalidEContentType(_)));
|
||||
}
|
||||
111
tests/test_aspa_econtent_decode_errors.rs
Normal file
111
tests/test_aspa_econtent_decode_errors.rs
Normal file
@ -0,0 +1,111 @@
|
||||
use rpki::data_model::aspa::{AspaDecodeError, AspaEContent};
|
||||
|
||||
fn len_bytes(len: usize) -> Vec<u8> {
|
||||
if len < 128 {
|
||||
vec![len as u8]
|
||||
} else {
|
||||
let mut tmp = Vec::new();
|
||||
let mut n = len;
|
||||
while n > 0 {
|
||||
tmp.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
tmp.reverse();
|
||||
let mut out = vec![0x80 | (tmp.len() as u8)];
|
||||
out.extend(tmp);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
fn tlv(tag: u8, content: &[u8]) -> Vec<u8> {
|
||||
let mut out = vec![tag];
|
||||
out.extend(len_bytes(content.len()));
|
||||
out.extend_from_slice(content);
|
||||
out
|
||||
}
|
||||
|
||||
fn der_integer_u64(v: u64) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
let mut n = v;
|
||||
if n == 0 {
|
||||
bytes.push(0);
|
||||
} else {
|
||||
while n > 0 {
|
||||
bytes.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
bytes.reverse();
|
||||
if bytes[0] & 0x80 != 0 {
|
||||
bytes.insert(0, 0);
|
||||
}
|
||||
}
|
||||
tlv(0x02, &bytes)
|
||||
}
|
||||
|
||||
fn der_sequence(children: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
let mut content = Vec::new();
|
||||
for c in children {
|
||||
content.extend(c);
|
||||
}
|
||||
tlv(0x30, &content)
|
||||
}
|
||||
|
||||
fn cs_explicit(tag_no: u8, inner_der: Vec<u8>) -> Vec<u8> {
|
||||
tlv(0xA0 | (tag_no & 0x1F), &inner_der)
|
||||
}
|
||||
|
||||
fn aspa_attestation_explicit_version(version: u64, customer: u64, providers: Vec<u64>) -> Vec<u8> {
|
||||
let providers_der = der_sequence(providers.into_iter().map(der_integer_u64).collect());
|
||||
der_sequence(vec![
|
||||
cs_explicit(0, der_integer_u64(version)),
|
||||
der_integer_u64(customer),
|
||||
providers_der,
|
||||
])
|
||||
}
|
||||
|
||||
fn aspa_attestation_missing_version(customer: u64, providers: Vec<u64>) -> Vec<u8> {
|
||||
let providers_der = der_sequence(providers.into_iter().map(der_integer_u64).collect());
|
||||
der_sequence(vec![der_integer_u64(customer), providers_der])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_must_be_explicit_and_equal_to_one() {
|
||||
let der = aspa_attestation_missing_version(64496, vec![64497]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::InvalidAttestationSequence));
|
||||
|
||||
let der = aspa_attestation_explicit_version(0, 64496, vec![64497]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::VersionMustBeExplicitOne));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn customer_asid_out_of_range_is_rejected() {
|
||||
let der = aspa_attestation_explicit_version(1, (u32::MAX as u64) + 1, vec![64497]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::CustomerAsIdOutOfRange(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn providers_constraints_are_enforced() {
|
||||
// empty providers
|
||||
let der = aspa_attestation_explicit_version(1, 64496, vec![]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::EmptyProviders));
|
||||
|
||||
// not strictly increasing (duplicate)
|
||||
let der = aspa_attestation_explicit_version(1, 64496, vec![64497, 64497]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::ProvidersNotStrictlyIncreasing));
|
||||
|
||||
// not strictly increasing (descending)
|
||||
let der = aspa_attestation_explicit_version(1, 64496, vec![64500, 64497]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::ProvidersNotStrictlyIncreasing));
|
||||
|
||||
// contains customer
|
||||
let der = aspa_attestation_explicit_version(1, 64496, vec![64496, 64497]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::ProvidersContainCustomer(64496)));
|
||||
}
|
||||
|
||||
14
tests/test_aspa_embedded_ee_cert.rs
Normal file
14
tests/test_aspa_embedded_ee_cert.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use rpki::data_model::aspa::AspaObject;
|
||||
|
||||
#[test]
|
||||
fn aspa_embedded_ee_cert_resources_validate() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa",
|
||||
)
|
||||
.expect("read ASPA fixture");
|
||||
|
||||
let aspa = AspaObject::decode_der(&der).expect("decode aspa");
|
||||
aspa.validate_embedded_ee_cert()
|
||||
.expect("aspa EE cert resources must validate");
|
||||
}
|
||||
|
||||
150
tests/test_aspa_validate_ee_resources.rs
Normal file
150
tests/test_aspa_validate_ee_resources.rs
Normal file
@ -0,0 +1,150 @@
|
||||
use der_parser::num_bigint::BigUint;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use rpki::data_model::aspa::{AspaEContent, AspaValidateError};
|
||||
use rpki::data_model::rc::{
|
||||
AsIdentifierChoice, AsIdOrRange, AsResourceSet, IpResourceSet, RcExtensions, ResourceCertKind,
|
||||
ResourceCertificate, RpkixTbsCertificate, SubjectInfoAccess,
|
||||
};
|
||||
|
||||
fn dummy_ee(ip_resources: Option<IpResourceSet>, as_resources: Option<AsResourceSet>) -> ResourceCertificate {
|
||||
ResourceCertificate {
|
||||
raw_der: vec![],
|
||||
tbs: RpkixTbsCertificate {
|
||||
version: 2,
|
||||
serial_number: BigUint::from(1u8),
|
||||
signature_algorithm: "1.2.840.113549.1.1.11".to_string(),
|
||||
issuer_dn: "CN=issuer".to_string(),
|
||||
subject_dn: "CN=subject".to_string(),
|
||||
validity_not_before: OffsetDateTime::UNIX_EPOCH,
|
||||
validity_not_after: OffsetDateTime::UNIX_EPOCH,
|
||||
subject_public_key_info: vec![],
|
||||
extensions: RcExtensions {
|
||||
basic_constraints_ca: false,
|
||||
subject_key_identifier: Some(vec![0x01]),
|
||||
subject_info_access: Some(SubjectInfoAccess::Ca(
|
||||
rpki::data_model::rc::SubjectInfoAccessCa {
|
||||
access_descriptions: vec![],
|
||||
},
|
||||
)),
|
||||
certificate_policies_oid: None,
|
||||
ip_resources,
|
||||
as_resources,
|
||||
},
|
||||
},
|
||||
kind: ResourceCertKind::Ee,
|
||||
}
|
||||
}
|
||||
|
||||
fn test_aspa() -> AspaEContent {
|
||||
AspaEContent {
|
||||
version: 1,
|
||||
customer_as_id: 64496,
|
||||
provider_as_ids: vec![64497],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_accepts_when_customer_matches_ee_asid() {
|
||||
let aspa = test_aspa();
|
||||
let ee = dummy_ee(
|
||||
None,
|
||||
Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])),
|
||||
rdi: None,
|
||||
}),
|
||||
);
|
||||
aspa.validate_against_ee_cert(&ee)
|
||||
.expect("customer must match");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_missing_as_resources() {
|
||||
let aspa = test_aspa();
|
||||
let ee = dummy_ee(None, None);
|
||||
let err = aspa.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(err, AspaValidateError::EeAsResourcesMissing));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_as_resources_inherit_or_ranges() {
|
||||
let aspa = test_aspa();
|
||||
let ee = dummy_ee(
|
||||
None,
|
||||
Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::Inherit),
|
||||
rdi: None,
|
||||
}),
|
||||
);
|
||||
let err = aspa.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(err, AspaValidateError::EeAsResourcesInherit));
|
||||
|
||||
let ee = dummy_ee(
|
||||
None,
|
||||
Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range {
|
||||
min: 64496,
|
||||
max: 64497,
|
||||
}])),
|
||||
rdi: None,
|
||||
}),
|
||||
);
|
||||
let err = aspa.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(err, AspaValidateError::EeAsResourcesRangePresent));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_customer_mismatch() {
|
||||
let aspa = test_aspa();
|
||||
let ee = dummy_ee(
|
||||
None,
|
||||
Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64511)])),
|
||||
rdi: None,
|
||||
}),
|
||||
);
|
||||
let err = aspa.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(err, AspaValidateError::CustomerAsIdMismatch { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_ip_resources_present() {
|
||||
let aspa = test_aspa();
|
||||
let ee = dummy_ee(
|
||||
Some(IpResourceSet { families: vec![] }),
|
||||
Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])),
|
||||
rdi: None,
|
||||
}),
|
||||
);
|
||||
let err = aspa.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(err, AspaValidateError::EeIpResourcesPresent));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_rdi_present_or_not_single_id() {
|
||||
let aspa = test_aspa();
|
||||
let ee = dummy_ee(
|
||||
None,
|
||||
Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])),
|
||||
rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])),
|
||||
}),
|
||||
);
|
||||
let err = aspa.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(err, AspaValidateError::EeAsResourcesRdiPresent));
|
||||
|
||||
let ee = dummy_ee(
|
||||
None,
|
||||
Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
||||
AsIdOrRange::Id(64496),
|
||||
AsIdOrRange::Id(64497),
|
||||
])),
|
||||
rdi: None,
|
||||
}),
|
||||
);
|
||||
let err = aspa.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(err, AspaValidateError::EeAsResourcesNotSingleId));
|
||||
}
|
||||
|
||||
14
tests/test_aspa_verify.rs
Normal file
14
tests/test_aspa_verify.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use rpki::data_model::aspa::AspaObject;
|
||||
|
||||
#[test]
|
||||
fn verify_aspa_cms_signature_with_embedded_ee_cert() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa",
|
||||
)
|
||||
.expect("read ASPA fixture");
|
||||
let aspa = AspaObject::decode_der(&der).expect("decode aspa");
|
||||
aspa.signed_object
|
||||
.verify_signature()
|
||||
.expect("ASPA CMS signature should verify with embedded EE cert");
|
||||
}
|
||||
|
||||
93
tests/test_common.rs
Normal file
93
tests/test_common.rs
Normal file
@ -0,0 +1,93 @@
|
||||
use rpki::data_model::common::{
|
||||
algorithm_params_absent_or_null, Asn1TimeEncoding, Asn1TimeUtc, BigUnsigned,
|
||||
};
|
||||
use x509_parser::prelude::FromDer;
|
||||
use x509_parser::x509::AlgorithmIdentifier;
|
||||
use x509_parser::time::ASN1Time;
|
||||
|
||||
#[test]
|
||||
fn big_unsigned_helpers() {
|
||||
let n = BigUnsigned { bytes_be: vec![0] };
|
||||
assert_eq!(n.to_u64(), Some(0));
|
||||
assert_eq!(n.to_hex_upper(), "00");
|
||||
|
||||
let n = BigUnsigned {
|
||||
bytes_be: vec![0x01, 0x02, 0x03],
|
||||
};
|
||||
assert_eq!(n.to_u64(), Some(0x010203));
|
||||
assert_eq!(n.to_hex_upper(), "010203");
|
||||
|
||||
let n = BigUnsigned {
|
||||
bytes_be: vec![0; 9],
|
||||
};
|
||||
assert_eq!(n.to_u64(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_encoding_validation() {
|
||||
let t = Asn1TimeUtc {
|
||||
utc: time::OffsetDateTime::from_unix_timestamp(0).unwrap(),
|
||||
encoding: Asn1TimeEncoding::UtcTime,
|
||||
};
|
||||
t.validate_encoding_rfc5280("t").expect("utc ok");
|
||||
|
||||
let t = Asn1TimeUtc {
|
||||
utc: time::OffsetDateTime::parse("2050-01-01T00:00:00Z", &time::format_description::well_known::Rfc3339)
|
||||
.unwrap(),
|
||||
encoding: Asn1TimeEncoding::UtcTime,
|
||||
};
|
||||
assert!(t.validate_encoding_rfc5280("t").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn algorithm_params_absent_or_null_helper() {
|
||||
// AlgorithmIdentifier ::= SEQUENCE { algorithm OID, parameters ANY OPTIONAL }
|
||||
// Using sha256WithRSAEncryption with NULL parameters.
|
||||
let alg_null = hex::decode(
|
||||
"300D06092A864886F70D01010B0500",
|
||||
)
|
||||
.unwrap();
|
||||
let (_rem, id) = AlgorithmIdentifier::from_der(&alg_null).expect("parse AlgorithmIdentifier");
|
||||
assert!(algorithm_params_absent_or_null(&id));
|
||||
|
||||
// Same OID, but parameters = INTEGER 1 (invalid for our helper).
|
||||
let alg_int = hex::decode(
|
||||
"300E06092A864886F70D01010B020101",
|
||||
)
|
||||
.unwrap();
|
||||
let (_rem, id) = AlgorithmIdentifier::from_der(&alg_int).expect("parse AlgorithmIdentifier");
|
||||
assert!(!algorithm_params_absent_or_null(&id));
|
||||
|
||||
// parameters absent
|
||||
let oid = der_parser::Oid::from(&[1u64, 2, 3]).unwrap();
|
||||
let id = AlgorithmIdentifier::new(oid, None);
|
||||
assert!(algorithm_params_absent_or_null(&id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn asn1_time_to_model_helper_covers_branches() {
|
||||
let dt_utc = time::OffsetDateTime::parse(
|
||||
"2024-01-01T00:00:00Z",
|
||||
&time::format_description::well_known::Rfc3339,
|
||||
)
|
||||
.unwrap();
|
||||
let t = ASN1Time::new_utc(dt_utc);
|
||||
let m = rpki::data_model::common::asn1_time_to_model(t);
|
||||
assert_eq!(m.encoding, Asn1TimeEncoding::UtcTime);
|
||||
|
||||
let dt_gen = time::OffsetDateTime::parse(
|
||||
"2050-01-01T00:00:00Z",
|
||||
&time::format_description::well_known::Rfc3339,
|
||||
)
|
||||
.unwrap();
|
||||
let t = ASN1Time::new_generalized(dt_gen);
|
||||
let m = rpki::data_model::common::asn1_time_to_model(t);
|
||||
assert_eq!(m.encoding, Asn1TimeEncoding::GeneralizedTime);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn big_unsigned_from_biguint_zero_encodes_as_single_zero_byte() {
|
||||
let z = der_parser::num_bigint::BigUint::from(0u32);
|
||||
let n = BigUnsigned::from_biguint(&z);
|
||||
assert_eq!(n.bytes_be, vec![0]);
|
||||
}
|
||||
127
tests/test_crl_errors.rs
Normal file
127
tests/test_crl_errors.rs
Normal file
@ -0,0 +1,127 @@
|
||||
use rpki::data_model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl};
|
||||
use x509_parser::prelude::FromDer;
|
||||
use x509_parser::prelude::X509Certificate;
|
||||
|
||||
const TEST_NO_CRLSIGN_CERT_DER_B64: &str = "MIIDATCCAemgAwIBAgIUCyQLQJn92+gyAzvIz22q1F/97OMwDQYJKoZIhvcNAQELBQAwGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMB4XDTI2MDEyNzAzNTk1OVoXDTM2MDEyNTAzNTk1OVowGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/aoMU8J6cddkM2r6F2snd1rCdQPepgo2T2lrqWFcxnQJdcxBL1OYg3wFi95TJmZSeIHIOGauDaJ2abmjgyOUHOC4U68x66JRg4hLkmLxo1cf3uYHWl9Obph6g2qPRvN80ORq70JPuL6mAfUkNiO9hnwK6oQiTzc/rjCQGIFH8kTESBMXLfNCyUpGi+MNztYH6Ha6bKAQuXgd29OFwIkOlGQnYgGC2qBMvnp86eITvV1gTiuI8Ho9m9nZHCmaD7TylvkMDq8Hk5nkIpRcG0uO60SkR2BiMOYe/TNn5dTmHd6bsdbU2GOvgnq1SnqGq3FOWhKIe3ycUJde0uNfZOqRwIDAQABoz8wPTAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUFjyzfJCDNhFfKxVr06kjUkE23dMwDQYJKoZIhvcNAQELBQADggEBAK98n2gVlwKA3Ob1YeAm9f+8hm7pbvrt0tA8GW180CILjf09k7fKgiRlxqGdZ9ySXjU52+zCqu3MpBXVbI87ZC+zA6uK05n4y1F0n85MJ9hGR2UEiPcqou85X73LvioynnSOy/OV1PjKJXReUsqF3GgDtgcMyFssPJ9s/5DWuUCScUJY6pu0kuIGOLQ/oXUw4TvxUeyz73gOTiAJshVTQoLpHUhj0595S7lArjwi7oLI1b8m8guTknvhk0Sc3tJZmUqOcIvYIs0guHpaeC+sMoF4K+6UTrxxOBdX+fUEWNpUyYXWHjdZq25PbJdHwA/VAW2zYVojaVREligf0Qfo6F4=";
|
||||
|
||||
fn test_no_crlsign_cert_der() -> Vec<u8> {
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
general_purpose::STANDARD
|
||||
.decode(TEST_NO_CRLSIGN_CERT_DER_B64)
|
||||
.expect("decode base64 cert")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_errors_are_reported() {
|
||||
let crl_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
|
||||
)
|
||||
.expect("read CRL fixture");
|
||||
let issuer_cert_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
)
|
||||
.expect("read issuer cert");
|
||||
|
||||
let mut crl = RpkixCrl::decode_der(&crl_der).expect("decode");
|
||||
|
||||
// IssuerSubjectMismatch.
|
||||
crl.issuer_dn = "CN=Not The Issuer".to_string();
|
||||
let err = crl
|
||||
.verify_signature_with_issuer_certificate_der(&issuer_cert_der)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CrlVerifyError::IssuerSubjectMismatch { .. }));
|
||||
|
||||
// AkiSkiMismatch (force issuer dn to match, then change AKI).
|
||||
let (_rem, issuer_cert) = X509Certificate::from_der(&issuer_cert_der).unwrap();
|
||||
crl.issuer_dn = issuer_cert.subject().to_string();
|
||||
crl.extensions.authority_key_identifier = vec![0u8; 20];
|
||||
let err = crl
|
||||
.verify_signature_with_issuer_certificate_der(&issuer_cert_der)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CrlVerifyError::AkiSkiMismatch));
|
||||
|
||||
// IssuerKeyUsageMissingCrlSign (use a cert with KeyUsage present but without CRLSign).
|
||||
let no_crlsign_der = test_no_crlsign_cert_der();
|
||||
let (_rem, no_crlsign_cert) = X509Certificate::from_der(&no_crlsign_der).unwrap();
|
||||
crl.issuer_dn = no_crlsign_cert.subject().to_string();
|
||||
let err = crl
|
||||
.verify_signature_with_issuer_certificate_der(&no_crlsign_der)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CrlVerifyError::IssuerKeyUsageMissingCrlSign));
|
||||
|
||||
// InvalidSignature: verify cryptographically with the wrong SPKI.
|
||||
let no_crlsign_spki_der = no_crlsign_cert.public_key().raw.to_vec();
|
||||
let err = crl
|
||||
.verify_signature_with_issuer_spki_der(&no_crlsign_spki_der)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CrlVerifyError::InvalidSignature(_)));
|
||||
|
||||
// IssuerCertificateParse / trailing bytes.
|
||||
let err = crl
|
||||
.verify_signature_with_issuer_certificate_der(b"not a cert")
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CrlVerifyError::IssuerCertificateParse(_)));
|
||||
|
||||
let mut bad = issuer_cert_der.clone();
|
||||
bad.push(0);
|
||||
let err = crl
|
||||
.verify_signature_with_issuer_certificate_der(&bad)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CrlVerifyError::IssuerCertificateTrailingBytes(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_signature_with_spki_der_paths() {
|
||||
let crl_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
|
||||
)
|
||||
.expect("read CRL fixture");
|
||||
let issuer_cert_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
)
|
||||
.expect("read issuer cert");
|
||||
|
||||
let crl = RpkixCrl::decode_der(&crl_der).expect("decode");
|
||||
let (_rem, issuer_cert) = X509Certificate::from_der(&issuer_cert_der).unwrap();
|
||||
let spki_der = issuer_cert.public_key().raw.to_vec();
|
||||
|
||||
crl.verify_signature_with_issuer_spki_der(&spki_der)
|
||||
.expect("verify with spki der");
|
||||
|
||||
let mut bad = spki_der;
|
||||
bad.push(0);
|
||||
let err = crl.verify_signature_with_issuer_spki_der(&bad).unwrap_err();
|
||||
assert!(matches!(err, CrlVerifyError::IssuerSpkiTrailingBytes(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rejects_trailing_bytes() {
|
||||
let crl_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
|
||||
)
|
||||
.expect("read CRL fixture");
|
||||
let mut bad = crl_der.clone();
|
||||
bad.push(0);
|
||||
let err = RpkixCrl::decode_der(&bad).unwrap_err();
|
||||
assert!(matches!(err, CrlDecodeError::TrailingBytes(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_rejects_crl_with_trailing_bytes_in_raw_der() {
|
||||
let crl_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
|
||||
)
|
||||
.expect("read CRL fixture");
|
||||
let issuer_cert_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
)
|
||||
.expect("read issuer cert");
|
||||
|
||||
let mut crl = RpkixCrl::decode_der(&crl_der).expect("decode");
|
||||
crl.raw_der.push(0);
|
||||
let (_rem, issuer_cert) = X509Certificate::from_der(&issuer_cert_der).unwrap();
|
||||
let spki_der = issuer_cert.public_key().raw.to_vec();
|
||||
|
||||
let err = crl.verify_signature_with_issuer_spki_der(&spki_der).unwrap_err();
|
||||
assert!(matches!(err, CrlVerifyError::CrlTrailingBytes(1)));
|
||||
}
|
||||
35
tests/test_manifest_decode.rs
Normal file
35
tests/test_manifest_decode.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use rpki::data_model::manifest::{ManifestEContent, ManifestObject};
|
||||
|
||||
#[test]
|
||||
fn decode_manifest_fixture_smoke() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
)
|
||||
.expect("read MFT fixture");
|
||||
let mft = ManifestObject::decode_der(&der).expect("decode manifest object");
|
||||
|
||||
assert_eq!(mft.manifest.version, 0);
|
||||
assert_eq!(mft.manifest.file_hash_alg, rpki::data_model::oid::OID_SHA256);
|
||||
assert!(mft.manifest.next_update > mft.manifest.this_update);
|
||||
assert!(!mft.manifest.files.is_empty());
|
||||
// The manifest file MUST NOT be listed in its own fileList.
|
||||
assert!(mft
|
||||
.manifest
|
||||
.files
|
||||
.iter()
|
||||
.all(|f| !f.file_name.to_ascii_lowercase().ends_with(".mft")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_manifest_econtent_from_fixture_signed_object() {
|
||||
let so_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
)
|
||||
.expect("read MFT fixture");
|
||||
let so = rpki::data_model::signed_object::RpkiSignedObject::decode_der(&so_der)
|
||||
.expect("decode signed object");
|
||||
let e = ManifestEContent::decode_der(&so.signed_data.encap_content_info.econtent)
|
||||
.expect("decode manifest eContent");
|
||||
assert_eq!(e.version, 0);
|
||||
}
|
||||
|
||||
411
tests/test_manifest_decode_errors.rs
Normal file
411
tests/test_manifest_decode_errors.rs
Normal file
@ -0,0 +1,411 @@
|
||||
use rpki::data_model::common::BigUnsigned;
|
||||
use rpki::data_model::manifest::{ManifestDecodeError, ManifestEContent, ManifestObject};
|
||||
use rpki::data_model::signed_object::RpkiSignedObject;
|
||||
|
||||
fn len_bytes(len: usize) -> Vec<u8> {
|
||||
if len < 128 {
|
||||
vec![len as u8]
|
||||
} else {
|
||||
let mut tmp = Vec::new();
|
||||
let mut n = len;
|
||||
while n > 0 {
|
||||
tmp.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
tmp.reverse();
|
||||
let mut out = vec![0x80 | (tmp.len() as u8)];
|
||||
out.extend(tmp);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
fn tlv(tag: u8, content: &[u8]) -> Vec<u8> {
|
||||
let mut out = vec![tag];
|
||||
out.extend(len_bytes(content.len()));
|
||||
out.extend_from_slice(content);
|
||||
out
|
||||
}
|
||||
|
||||
fn der_integer_bytes(bytes: &[u8]) -> Vec<u8> {
|
||||
tlv(0x02, bytes)
|
||||
}
|
||||
|
||||
fn der_integer_u64(v: u64) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
let mut n = v;
|
||||
if n == 0 {
|
||||
bytes.push(0);
|
||||
} else {
|
||||
while n > 0 {
|
||||
bytes.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
bytes.reverse();
|
||||
if bytes[0] & 0x80 != 0 {
|
||||
bytes.insert(0, 0);
|
||||
}
|
||||
}
|
||||
der_integer_bytes(&bytes)
|
||||
}
|
||||
|
||||
fn der_oid(oid: &str) -> Vec<u8> {
|
||||
use std::str::FromStr;
|
||||
use der_parser::asn1_rs::ToDer;
|
||||
let oid = der_parser::Oid::from_str(oid).unwrap();
|
||||
oid.to_der_vec().unwrap()
|
||||
}
|
||||
|
||||
fn der_generalized_time_z(s: &str) -> Vec<u8> {
|
||||
tlv(0x18, s.as_bytes())
|
||||
}
|
||||
|
||||
fn der_ia5(s: &str) -> Vec<u8> {
|
||||
tlv(0x16, s.as_bytes())
|
||||
}
|
||||
|
||||
fn der_bit_string(unused: u8, bytes: &[u8]) -> Vec<u8> {
|
||||
let mut content = vec![unused];
|
||||
content.extend_from_slice(bytes);
|
||||
tlv(0x03, &content)
|
||||
}
|
||||
|
||||
fn der_sequence(children: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
let mut content = Vec::new();
|
||||
for c in children {
|
||||
content.extend(c);
|
||||
}
|
||||
tlv(0x30, &content)
|
||||
}
|
||||
|
||||
fn der_cs_explicit(tag_no: u8, inner_der: Vec<u8>) -> Vec<u8> {
|
||||
tlv(0xA0 | (tag_no & 0x1F), &inner_der)
|
||||
}
|
||||
|
||||
fn manifest_der(
|
||||
version: Option<u64>,
|
||||
manifest_number: Vec<u8>,
|
||||
this_update: Vec<u8>,
|
||||
next_update: Vec<u8>,
|
||||
file_hash_alg_oid: &str,
|
||||
file_list: Vec<Vec<u8>>,
|
||||
) -> Vec<u8> {
|
||||
let mut fields = Vec::new();
|
||||
if let Some(v) = version {
|
||||
fields.push(der_cs_explicit(0, der_integer_u64(v)));
|
||||
}
|
||||
fields.push(der_integer_bytes(&manifest_number));
|
||||
fields.push(this_update);
|
||||
fields.push(next_update);
|
||||
fields.push(der_oid(file_hash_alg_oid));
|
||||
fields.push(der_sequence(file_list));
|
||||
der_sequence(fields)
|
||||
}
|
||||
|
||||
fn file_and_hash(file: &str, unused: u8, hash: &[u8]) -> Vec<u8> {
|
||||
der_sequence(vec![der_ia5(file), der_bit_string(unused, hash)])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_econtent_version_must_be_zero_when_present() {
|
||||
let der = manifest_der(
|
||||
Some(1),
|
||||
vec![1],
|
||||
der_generalized_time_z("20260101000000Z"),
|
||||
der_generalized_time_z("20260102000000Z"),
|
||||
rpki::data_model::oid::OID_SHA256,
|
||||
vec![],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidManifestVersion(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_number_too_long_rejected() {
|
||||
let long = vec![1u8; 21];
|
||||
let der = manifest_der(
|
||||
Some(0),
|
||||
long,
|
||||
der_generalized_time_z("20260101000000Z"),
|
||||
der_generalized_time_z("20260102000000Z"),
|
||||
rpki::data_model::oid::OID_SHA256,
|
||||
vec![],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::ManifestNumberTooLong));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_number_negative_rejected() {
|
||||
// INTEGER -1 encoded as 0xFF
|
||||
let der = manifest_der(
|
||||
Some(0),
|
||||
vec![0xFF],
|
||||
der_generalized_time_z("20260101000000Z"),
|
||||
der_generalized_time_z("20260102000000Z"),
|
||||
rpki::data_model::oid::OID_SHA256,
|
||||
vec![],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidManifestNumber));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn this_update_and_next_update_must_be_generalized_time() {
|
||||
let der = manifest_der(
|
||||
Some(0),
|
||||
vec![1],
|
||||
tlv(0x17, b"240101000000Z"), // UTCTime
|
||||
der_generalized_time_z("20260102000000Z"),
|
||||
rpki::data_model::oid::OID_SHA256,
|
||||
vec![],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidThisUpdate));
|
||||
|
||||
let der = manifest_der(
|
||||
Some(0),
|
||||
vec![1],
|
||||
der_generalized_time_z("20260101000000Z"),
|
||||
tlv(0x17, b"240101000000Z"), // UTCTime
|
||||
rpki::data_model::oid::OID_SHA256,
|
||||
vec![],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidNextUpdate));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_update_must_be_later() {
|
||||
let der = manifest_der(
|
||||
Some(0),
|
||||
vec![1],
|
||||
der_generalized_time_z("20260102000000Z"),
|
||||
der_generalized_time_z("20260101000000Z"),
|
||||
rpki::data_model::oid::OID_SHA256,
|
||||
vec![],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::NextUpdateNotLater));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_hash_alg_must_be_sha256() {
|
||||
let der = manifest_der(
|
||||
Some(0),
|
||||
vec![1],
|
||||
der_generalized_time_z("20260101000000Z"),
|
||||
der_generalized_time_z("20260102000000Z"),
|
||||
"1.2.3.4",
|
||||
vec![],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidFileHashAlg(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_list_entry_validation() {
|
||||
let hash = vec![0u8; 32];
|
||||
// Invalid filename (bad char)
|
||||
let der = manifest_der(
|
||||
Some(0),
|
||||
vec![1],
|
||||
der_generalized_time_z("20260101000000Z"),
|
||||
der_generalized_time_z("20260102000000Z"),
|
||||
rpki::data_model::oid::OID_SHA256,
|
||||
vec![file_and_hash("bad!.roa", 0, &hash)],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidFileName(_)));
|
||||
|
||||
// Non-octet-aligned BIT STRING
|
||||
let der = manifest_der(
|
||||
Some(0),
|
||||
vec![1],
|
||||
der_generalized_time_z("20260101000000Z"),
|
||||
der_generalized_time_z("20260102000000Z"),
|
||||
rpki::data_model::oid::OID_SHA256,
|
||||
vec![file_and_hash("ok.roa", 1, &hash)],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::HashNotOctetAligned));
|
||||
|
||||
// Wrong hash length
|
||||
let der = manifest_der(
|
||||
Some(0),
|
||||
vec![1],
|
||||
der_generalized_time_z("20260101000000Z"),
|
||||
der_generalized_time_z("20260102000000Z"),
|
||||
rpki::data_model::oid::OID_SHA256,
|
||||
vec![file_and_hash("ok.roa", 0, &[0u8; 31])],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidHashLength(31)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_object_requires_correct_econtent_type() {
|
||||
let so_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
)
|
||||
.expect("read MFT fixture");
|
||||
let mut so = RpkiSignedObject::decode_der(&so_der).expect("decode signed object");
|
||||
so.signed_data.encap_content_info.econtent_type = "1.2.3.4".to_string();
|
||||
let err = ManifestObject::from_signed_object(so).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidEContentType(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_sequence_length_is_validated() {
|
||||
let der = der_sequence(vec![der_integer_u64(0)]);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestDecodeError::InvalidManifestSequenceLen(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_list_must_be_sequence_and_entry_shape_validated() {
|
||||
// Patch last byte to change SEQUENCE tag 0x30 to NULL tag 0x05 in the inner fileList.
|
||||
// Build a manifest with fileList = NULL explicitly.
|
||||
let der = {
|
||||
let mut fields = Vec::new();
|
||||
fields.push(der_cs_explicit(0, der_integer_u64(0)));
|
||||
fields.push(der_integer_u64(1));
|
||||
fields.push(der_generalized_time_z("20260101000000Z"));
|
||||
fields.push(der_generalized_time_z("20260102000000Z"));
|
||||
fields.push(der_oid(rpki::data_model::oid::OID_SHA256));
|
||||
fields.push(tlv(0x05, &[]));
|
||||
der_sequence(fields)
|
||||
};
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidFileList));
|
||||
|
||||
// FileAndHash not SEQUENCE of 2
|
||||
let der = manifest_der(
|
||||
Some(0),
|
||||
vec![1],
|
||||
der_generalized_time_z("20260101000000Z"),
|
||||
der_generalized_time_z("20260102000000Z"),
|
||||
rpki::data_model::oid::OID_SHA256,
|
||||
vec![der_sequence(vec![der_ia5("ok.roa")])],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidFileAndHash));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_tag_must_be_context_specific_0() {
|
||||
let der = {
|
||||
let mut fields = Vec::new();
|
||||
// Wrong tag number [1]
|
||||
fields.push(der_cs_explicit(1, der_integer_u64(0)));
|
||||
fields.push(der_integer_u64(1));
|
||||
fields.push(der_generalized_time_z("20260101000000Z"));
|
||||
fields.push(der_generalized_time_z("20260102000000Z"));
|
||||
fields.push(der_oid(rpki::data_model::oid::OID_SHA256));
|
||||
fields.push(der_sequence(vec![]));
|
||||
der_sequence(fields)
|
||||
};
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::Parse(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_manifest_number_helper_exercises_zero_encoding() {
|
||||
let z = der_parser::num_bigint::BigUint::from(0u32);
|
||||
let n = BigUnsigned::from_biguint(&z);
|
||||
assert_eq!(n.bytes_be, vec![0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_econtent_trailing_bytes_are_rejected() {
|
||||
let der = manifest_der(
|
||||
Some(0),
|
||||
vec![1],
|
||||
der_generalized_time_z("20260101000000Z"),
|
||||
der_generalized_time_z("20260102000000Z"),
|
||||
rpki::data_model::oid::OID_SHA256,
|
||||
vec![],
|
||||
);
|
||||
let mut bad = der.clone();
|
||||
bad.push(0);
|
||||
let err = ManifestEContent::decode_der(&bad).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::TrailingBytes(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_version_rejects_trailing_bytes_inside_explicit_tag() {
|
||||
let mut version_inner = der_integer_u64(0);
|
||||
version_inner.extend(tlv(0x05, &[]));
|
||||
|
||||
let der = {
|
||||
let mut fields = Vec::new();
|
||||
fields.push(der_cs_explicit(0, version_inner));
|
||||
fields.push(der_integer_u64(1));
|
||||
fields.push(der_generalized_time_z("20260101000000Z"));
|
||||
fields.push(der_generalized_time_z("20260102000000Z"));
|
||||
fields.push(der_oid(rpki::data_model::oid::OID_SHA256));
|
||||
fields.push(der_sequence(vec![]));
|
||||
der_sequence(fields)
|
||||
};
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::Parse(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_rejects_hash_with_wrong_type() {
|
||||
let hash = vec![0u8; 32];
|
||||
let entry = der_sequence(vec![der_ia5("ok.roa"), tlv(0x04, &hash)]); // OCTET STRING, not BIT STRING
|
||||
let der = manifest_der(
|
||||
Some(0),
|
||||
vec![1],
|
||||
der_generalized_time_z("20260101000000Z"),
|
||||
der_generalized_time_z("20260102000000Z"),
|
||||
rpki::data_model::oid::OID_SHA256,
|
||||
vec![entry],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidHashType));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_name_validation_branches_are_exercised() {
|
||||
let hash = vec![0u8; 32];
|
||||
let cases = [
|
||||
"noext", // missing '.'
|
||||
".roa", // empty base
|
||||
"a.roaa", // ext len != 3
|
||||
"a.r0a", // ext not alphabetic
|
||||
"a.txt", // ext not allowlisted
|
||||
];
|
||||
for name in cases {
|
||||
let der = manifest_der(
|
||||
Some(0),
|
||||
vec![1],
|
||||
der_generalized_time_z("20260101000000Z"),
|
||||
der_generalized_time_z("20260102000000Z"),
|
||||
rpki::data_model::oid::OID_SHA256,
|
||||
vec![file_and_hash(name, 0, &hash)],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidFileName(_)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iana_registry_filename_extensions_are_accepted() {
|
||||
let hash = vec![0u8; 32];
|
||||
for ext in rpki::data_model::common::IANA_RPKI_REPOSITORY_FILENAME_EXTENSIONS {
|
||||
let name = format!("ok.{ext}");
|
||||
let der = manifest_der(
|
||||
Some(0),
|
||||
vec![1],
|
||||
der_generalized_time_z("20260101000000Z"),
|
||||
der_generalized_time_z("20260102000000Z"),
|
||||
rpki::data_model::oid::OID_SHA256,
|
||||
vec![file_and_hash(&name, 0, &hash)],
|
||||
);
|
||||
ManifestEContent::decode_der(&der).expect("manifest should accept IANA extension");
|
||||
}
|
||||
}
|
||||
14
tests/test_manifest_embedded_ee_cert.rs
Normal file
14
tests/test_manifest_embedded_ee_cert.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use rpki::data_model::manifest::ManifestObject;
|
||||
|
||||
#[test]
|
||||
fn manifest_embedded_ee_cert_resources_validate() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
)
|
||||
.expect("read MFT fixture");
|
||||
|
||||
let mft = ManifestObject::decode_der(&der).expect("decode manifest");
|
||||
mft.validate_embedded_ee_cert()
|
||||
.expect("manifest EE cert resources must validate");
|
||||
}
|
||||
|
||||
498
tests/test_model_print_real_fixtures.rs
Normal file
498
tests/test_model_print_real_fixtures.rs
Normal file
@ -0,0 +1,498 @@
|
||||
use rpki::data_model::aspa::{AspaEContent, AspaObject};
|
||||
use rpki::data_model::crl::{CrlExtensions, RevokedCert, RpkixCrl};
|
||||
use rpki::data_model::manifest::{FileAndHash, ManifestEContent, ManifestObject};
|
||||
use rpki::data_model::rc::{
|
||||
AsResourceSet, IpResourceSet, RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate,
|
||||
SubjectInfoAccess,
|
||||
};
|
||||
use rpki::data_model::roa::{RoaEContent, RoaIpAddressFamily, RoaObject};
|
||||
use rpki::data_model::signed_object::{
|
||||
EncapsulatedContentInfo, ResourceEeCertificate, RpkiSignedObject, SignedAttrsProfiled, SignedDataProfiled,
|
||||
SignerInfoProfiled,
|
||||
};
|
||||
use rpki::data_model::ta::{TaCertificate, TrustAnchor};
|
||||
use rpki::data_model::tal::Tal;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct BytesFmt {
|
||||
len: usize,
|
||||
sha256_hex: String,
|
||||
head_hex: String,
|
||||
tail_hex: String,
|
||||
}
|
||||
|
||||
fn bytes_fmt(bytes: &[u8]) -> BytesFmt {
|
||||
let len = bytes.len();
|
||||
let sha = Sha256::digest(bytes);
|
||||
|
||||
let head_len = len.min(16);
|
||||
let tail_len = len.min(16);
|
||||
|
||||
let head = &bytes[..head_len];
|
||||
let tail = &bytes[len.saturating_sub(tail_len)..];
|
||||
|
||||
BytesFmt {
|
||||
len,
|
||||
sha256_hex: hex::encode(sha),
|
||||
head_hex: hex::encode(head),
|
||||
tail_hex: hex::encode(tail),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct TalPretty {
|
||||
raw: BytesFmt,
|
||||
comments: Vec<String>,
|
||||
ta_uris: Vec<String>,
|
||||
subject_public_key_info_der: BytesFmt,
|
||||
}
|
||||
|
||||
impl From<&Tal> for TalPretty {
|
||||
fn from(v: &Tal) -> Self {
|
||||
Self {
|
||||
raw: bytes_fmt(&v.raw),
|
||||
comments: v.comments.clone(),
|
||||
ta_uris: v.ta_uris.iter().map(|u| u.to_string()).collect(),
|
||||
subject_public_key_info_der: bytes_fmt(&v.subject_public_key_info_der),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct TaCertificatePretty {
|
||||
raw_der: BytesFmt,
|
||||
rc_ca: ResourceCertificatePretty,
|
||||
}
|
||||
|
||||
impl From<&TaCertificate> for TaCertificatePretty {
|
||||
fn from(v: &TaCertificate) -> Self {
|
||||
Self {
|
||||
raw_der: bytes_fmt(&v.raw_der),
|
||||
rc_ca: ResourceCertificatePretty::from(&v.rc_ca),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct TrustAnchorPretty {
|
||||
tal: TalPretty,
|
||||
ta_certificate: TaCertificatePretty,
|
||||
resolved_ta_uri: Option<String>,
|
||||
}
|
||||
|
||||
impl From<&TrustAnchor> for TrustAnchorPretty {
|
||||
fn from(v: &TrustAnchor) -> Self {
|
||||
Self {
|
||||
tal: TalPretty::from(&v.tal),
|
||||
ta_certificate: TaCertificatePretty::from(&v.ta_certificate),
|
||||
resolved_ta_uri: v.resolved_ta_uri.as_ref().map(|u| u.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct ResourceCertificatePretty {
|
||||
raw_der: BytesFmt,
|
||||
tbs: RpkixTbsCertificatePretty,
|
||||
kind: ResourceCertKind,
|
||||
}
|
||||
|
||||
impl From<&ResourceCertificate> for ResourceCertificatePretty {
|
||||
fn from(v: &ResourceCertificate) -> Self {
|
||||
Self {
|
||||
raw_der: bytes_fmt(&v.raw_der),
|
||||
tbs: RpkixTbsCertificatePretty::from(&v.tbs),
|
||||
kind: v.kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct RpkixTbsCertificatePretty {
|
||||
version: u32,
|
||||
serial_number: String,
|
||||
signature_algorithm: String,
|
||||
issuer_dn: String,
|
||||
subject_dn: String,
|
||||
validity_not_before: time::OffsetDateTime,
|
||||
validity_not_after: time::OffsetDateTime,
|
||||
subject_public_key_info: BytesFmt,
|
||||
extensions: RcExtensionsPretty,
|
||||
}
|
||||
|
||||
impl From<&RpkixTbsCertificate> for RpkixTbsCertificatePretty {
|
||||
fn from(v: &RpkixTbsCertificate) -> Self {
|
||||
Self {
|
||||
version: v.version,
|
||||
serial_number: hex::encode(v.serial_number.to_bytes_be()),
|
||||
signature_algorithm: v.signature_algorithm.clone(),
|
||||
issuer_dn: v.issuer_dn.clone(),
|
||||
subject_dn: v.subject_dn.clone(),
|
||||
validity_not_before: v.validity_not_before,
|
||||
validity_not_after: v.validity_not_after,
|
||||
subject_public_key_info: bytes_fmt(&v.subject_public_key_info),
|
||||
extensions: RcExtensionsPretty::from(&v.extensions),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct RcExtensionsPretty {
|
||||
basic_constraints_ca: bool,
|
||||
subject_key_identifier: Option<BytesFmt>,
|
||||
subject_info_access: Option<SubjectInfoAccess>,
|
||||
certificate_policies_oid: Option<String>,
|
||||
ip_resources: Option<IpResourceSet>,
|
||||
as_resources: Option<AsResourceSet>,
|
||||
}
|
||||
|
||||
impl From<&RcExtensions> for RcExtensionsPretty {
|
||||
fn from(v: &RcExtensions) -> Self {
|
||||
Self {
|
||||
basic_constraints_ca: v.basic_constraints_ca,
|
||||
subject_key_identifier: v.subject_key_identifier.as_ref().map(|b| bytes_fmt(b)),
|
||||
subject_info_access: v.subject_info_access.clone(),
|
||||
certificate_policies_oid: v.certificate_policies_oid.clone(),
|
||||
ip_resources: v.ip_resources.clone(),
|
||||
as_resources: v.as_resources.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct RpkiSignedObjectPretty {
|
||||
raw_der: BytesFmt,
|
||||
content_info_content_type: String,
|
||||
signed_data: SignedDataProfiledPretty,
|
||||
}
|
||||
|
||||
impl From<&RpkiSignedObject> for RpkiSignedObjectPretty {
|
||||
fn from(v: &RpkiSignedObject) -> Self {
|
||||
Self {
|
||||
raw_der: bytes_fmt(&v.raw_der),
|
||||
content_info_content_type: v.content_info_content_type.clone(),
|
||||
signed_data: SignedDataProfiledPretty::from(&v.signed_data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct SignedDataProfiledPretty {
|
||||
version: u32,
|
||||
digest_algorithms: Vec<String>,
|
||||
encap_content_info: EncapsulatedContentInfoPretty,
|
||||
certificates: Vec<ResourceEeCertificatePretty>,
|
||||
crls_present: bool,
|
||||
signer_infos: Vec<SignerInfoProfiledPretty>,
|
||||
}
|
||||
|
||||
impl From<&SignedDataProfiled> for SignedDataProfiledPretty {
|
||||
fn from(v: &SignedDataProfiled) -> Self {
|
||||
Self {
|
||||
version: v.version,
|
||||
digest_algorithms: v.digest_algorithms.clone(),
|
||||
encap_content_info: EncapsulatedContentInfoPretty::from(&v.encap_content_info),
|
||||
certificates: v
|
||||
.certificates
|
||||
.iter()
|
||||
.map(ResourceEeCertificatePretty::from)
|
||||
.collect(),
|
||||
crls_present: v.crls_present,
|
||||
signer_infos: v.signer_infos.iter().map(SignerInfoProfiledPretty::from).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct EncapsulatedContentInfoPretty {
|
||||
econtent_type: String,
|
||||
econtent: BytesFmt,
|
||||
}
|
||||
|
||||
impl From<&EncapsulatedContentInfo> for EncapsulatedContentInfoPretty {
|
||||
fn from(v: &EncapsulatedContentInfo) -> Self {
|
||||
Self {
|
||||
econtent_type: v.econtent_type.clone(),
|
||||
econtent: bytes_fmt(&v.econtent),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct ResourceEeCertificatePretty {
|
||||
raw_der: BytesFmt,
|
||||
subject_key_identifier: BytesFmt,
|
||||
spki_der: BytesFmt,
|
||||
sia_signed_object_uris: Vec<String>,
|
||||
resource_cert: ResourceCertificatePretty,
|
||||
}
|
||||
|
||||
impl From<&ResourceEeCertificate> for ResourceEeCertificatePretty {
|
||||
fn from(v: &ResourceEeCertificate) -> Self {
|
||||
Self {
|
||||
raw_der: bytes_fmt(&v.raw_der),
|
||||
subject_key_identifier: bytes_fmt(&v.subject_key_identifier),
|
||||
spki_der: bytes_fmt(&v.spki_der),
|
||||
sia_signed_object_uris: v.sia_signed_object_uris.clone(),
|
||||
resource_cert: ResourceCertificatePretty::from(&v.resource_cert),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct SignerInfoProfiledPretty {
|
||||
version: u32,
|
||||
sid_ski: BytesFmt,
|
||||
digest_algorithm: String,
|
||||
signature_algorithm: String,
|
||||
signed_attrs: SignedAttrsProfiledPretty,
|
||||
unsigned_attrs_present: bool,
|
||||
signature: BytesFmt,
|
||||
signed_attrs_der_for_signature: BytesFmt,
|
||||
}
|
||||
|
||||
impl From<&SignerInfoProfiled> for SignerInfoProfiledPretty {
|
||||
fn from(v: &SignerInfoProfiled) -> Self {
|
||||
Self {
|
||||
version: v.version,
|
||||
sid_ski: bytes_fmt(&v.sid_ski),
|
||||
digest_algorithm: v.digest_algorithm.clone(),
|
||||
signature_algorithm: v.signature_algorithm.clone(),
|
||||
signed_attrs: SignedAttrsProfiledPretty::from(&v.signed_attrs),
|
||||
unsigned_attrs_present: v.unsigned_attrs_present,
|
||||
signature: bytes_fmt(&v.signature),
|
||||
signed_attrs_der_for_signature: bytes_fmt(&v.signed_attrs_der_for_signature),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct SignedAttrsProfiledPretty {
|
||||
content_type: String,
|
||||
message_digest: BytesFmt,
|
||||
signing_time: rpki::data_model::common::Asn1TimeUtc,
|
||||
other_attrs_present: bool,
|
||||
}
|
||||
|
||||
impl From<&SignedAttrsProfiled> for SignedAttrsProfiledPretty {
|
||||
fn from(v: &SignedAttrsProfiled) -> Self {
|
||||
Self {
|
||||
content_type: v.content_type.clone(),
|
||||
message_digest: bytes_fmt(&v.message_digest),
|
||||
signing_time: v.signing_time,
|
||||
other_attrs_present: v.other_attrs_present,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct ManifestObjectPretty {
|
||||
signed_object: RpkiSignedObjectPretty,
|
||||
econtent_type: String,
|
||||
manifest: ManifestEContentPretty,
|
||||
}
|
||||
|
||||
impl From<&ManifestObject> for ManifestObjectPretty {
|
||||
fn from(v: &ManifestObject) -> Self {
|
||||
Self {
|
||||
signed_object: RpkiSignedObjectPretty::from(&v.signed_object),
|
||||
econtent_type: v.econtent_type.clone(),
|
||||
manifest: ManifestEContentPretty::from(&v.manifest),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct ManifestEContentPretty {
|
||||
version: u32,
|
||||
manifest_number: String,
|
||||
this_update: time::OffsetDateTime,
|
||||
next_update: time::OffsetDateTime,
|
||||
file_hash_alg: String,
|
||||
files: Vec<FileAndHashPretty>,
|
||||
}
|
||||
|
||||
impl From<&ManifestEContent> for ManifestEContentPretty {
|
||||
fn from(v: &ManifestEContent) -> Self {
|
||||
Self {
|
||||
version: v.version,
|
||||
manifest_number: v.manifest_number.to_hex_upper(),
|
||||
this_update: v.this_update,
|
||||
next_update: v.next_update,
|
||||
file_hash_alg: v.file_hash_alg.clone(),
|
||||
files: v.files.iter().map(FileAndHashPretty::from).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct FileAndHashPretty {
|
||||
file_name: String,
|
||||
hash_hex: String,
|
||||
}
|
||||
|
||||
impl From<&FileAndHash> for FileAndHashPretty {
|
||||
fn from(v: &FileAndHash) -> Self {
|
||||
Self {
|
||||
file_name: v.file_name.clone(),
|
||||
hash_hex: hex::encode(&v.hash_bytes),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct RoaObjectPretty {
|
||||
signed_object: RpkiSignedObjectPretty,
|
||||
econtent_type: String,
|
||||
roa: RoaEContentPretty,
|
||||
}
|
||||
|
||||
impl From<&RoaObject> for RoaObjectPretty {
|
||||
fn from(v: &RoaObject) -> Self {
|
||||
Self {
|
||||
signed_object: RpkiSignedObjectPretty::from(&v.signed_object),
|
||||
econtent_type: v.econtent_type.clone(),
|
||||
roa: RoaEContentPretty::from(&v.roa),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct RoaEContentPretty {
|
||||
version: u32,
|
||||
as_id: u32,
|
||||
ip_addr_blocks: Vec<RoaIpAddressFamily>,
|
||||
}
|
||||
|
||||
impl From<&RoaEContent> for RoaEContentPretty {
|
||||
fn from(v: &RoaEContent) -> Self {
|
||||
Self {
|
||||
version: v.version,
|
||||
as_id: v.as_id,
|
||||
ip_addr_blocks: v.ip_addr_blocks.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct AspaObjectPretty {
|
||||
signed_object: RpkiSignedObjectPretty,
|
||||
econtent_type: String,
|
||||
aspa: AspaEContent,
|
||||
}
|
||||
|
||||
impl From<&AspaObject> for AspaObjectPretty {
|
||||
fn from(v: &AspaObject) -> Self {
|
||||
Self {
|
||||
signed_object: RpkiSignedObjectPretty::from(&v.signed_object),
|
||||
econtent_type: v.econtent_type.clone(),
|
||||
aspa: v.aspa.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct RpkixCrlPretty {
|
||||
raw_der: BytesFmt,
|
||||
version: u32,
|
||||
issuer_dn: String,
|
||||
signature_algorithm_oid: String,
|
||||
this_update: rpki::data_model::common::Asn1TimeUtc,
|
||||
next_update: rpki::data_model::common::Asn1TimeUtc,
|
||||
revoked_certs: Vec<RevokedCert>,
|
||||
extensions: CrlExtensions,
|
||||
}
|
||||
|
||||
impl From<&RpkixCrl> for RpkixCrlPretty {
|
||||
fn from(v: &RpkixCrl) -> Self {
|
||||
Self {
|
||||
raw_der: bytes_fmt(&v.raw_der),
|
||||
version: v.version,
|
||||
issuer_dn: v.issuer_dn.clone(),
|
||||
signature_algorithm_oid: v.signature_algorithm_oid.clone(),
|
||||
this_update: v.this_update,
|
||||
next_update: v.next_update,
|
||||
revoked_certs: v.revoked_certs.clone(),
|
||||
extensions: v.extensions.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn print_all_models_from_real_fixtures() {
|
||||
// Note: run this test with `cargo test --test test_model_print_real_fixtures -- --nocapture`
|
||||
// to see the output.
|
||||
|
||||
let tal_path = "tests/fixtures/tal/ripe-ncc.tal";
|
||||
println!("== TAL / TA / TrustAnchor ==");
|
||||
println!("Fixture (TAL): {tal_path}");
|
||||
let tal_raw = std::fs::read(tal_path).expect("read TAL fixture");
|
||||
let tal = Tal::decode_bytes(&tal_raw).expect("decode TAL");
|
||||
|
||||
let ta_path = "tests/fixtures/ta/ripe-ncc-ta.cer";
|
||||
println!("Fixture (TA): {ta_path}");
|
||||
let ta_der = std::fs::read(ta_path).expect("read TA fixture");
|
||||
let ta = TaCertificate::from_der(&ta_der).expect("parse TA cert");
|
||||
|
||||
let resolved = tal
|
||||
.ta_uris
|
||||
.iter()
|
||||
.find(|u| u.scheme() == "https")
|
||||
.or_else(|| tal.ta_uris.first())
|
||||
.cloned()
|
||||
.expect("tal has at least one uri");
|
||||
let trust_anchor = TrustAnchor::bind(tal, &ta_der, Some(&resolved)).expect("bind trust anchor");
|
||||
println!("{:#?}", TalPretty::from(&trust_anchor.tal));
|
||||
println!("{:#?}", TaCertificatePretty::from(&ta));
|
||||
println!("{:#?}", TrustAnchorPretty::from(&trust_anchor));
|
||||
|
||||
println!();
|
||||
println!("== ResourceCertificate (example non-TA CA cert) ==");
|
||||
let ca_path =
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer";
|
||||
println!("Fixture (CA cert): {ca_path}");
|
||||
let ca_der = std::fs::read(ca_path).expect("read CA cert fixture");
|
||||
let ca_rc = ResourceCertificate::from_der(&ca_der).expect("parse CA resource certificate");
|
||||
println!("{:#?}", ResourceCertificatePretty::from(&ca_rc));
|
||||
|
||||
println!();
|
||||
println!("== Signed Object / Manifest ==");
|
||||
let mft_path =
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
|
||||
println!("Fixture (MFT): {mft_path}");
|
||||
let mft_der = std::fs::read(mft_path).expect("read MFT fixture");
|
||||
let mft_obj = ManifestObject::decode_der(&mft_der).expect("decode manifest object");
|
||||
println!("{:#?}", ManifestObjectPretty::from(&mft_obj));
|
||||
println!("Manifest.validate_embedded_ee_cert={:?}", mft_obj.validate_embedded_ee_cert());
|
||||
println!("Manifest.verify_signature={:?}", mft_obj.signed_object.verify_signature());
|
||||
|
||||
println!();
|
||||
println!("== Signed Object / ROA ==");
|
||||
let roa_path = "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa";
|
||||
println!("Fixture (ROA): {roa_path}");
|
||||
let roa_der = std::fs::read(roa_path).expect("read ROA fixture");
|
||||
let roa_obj = RoaObject::decode_der(&roa_der).expect("decode ROA object");
|
||||
println!("{:#?}", RoaObjectPretty::from(&roa_obj));
|
||||
println!("ROA.validate_embedded_ee_cert={:?}", roa_obj.validate_embedded_ee_cert());
|
||||
println!("ROA.verify_signature={:?}", roa_obj.signed_object.verify_signature());
|
||||
|
||||
println!();
|
||||
println!("== Signed Object / ASPA ==");
|
||||
let aspa_path =
|
||||
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa";
|
||||
println!("Fixture (ASPA): {aspa_path}");
|
||||
let aspa_der = std::fs::read(aspa_path).expect("read ASPA fixture");
|
||||
let aspa_obj = AspaObject::decode_der(&aspa_der).expect("decode ASPA object");
|
||||
println!("{:#?}", AspaObjectPretty::from(&aspa_obj));
|
||||
println!("ASPA.validate_embedded_ee_cert={:?}", aspa_obj.validate_embedded_ee_cert());
|
||||
println!("ASPA.verify_signature={:?}", aspa_obj.signed_object.verify_signature());
|
||||
|
||||
println!();
|
||||
println!("== CRL ==");
|
||||
let crl_path = "tests/fixtures/0099DEAB073EFD74C250C0A382B25012B5082AEE.crl";
|
||||
println!("Fixture (CRL): {crl_path}");
|
||||
let crl_der = std::fs::read(crl_path).expect("read CRL fixture");
|
||||
let crl = RpkixCrl::decode_der(&crl_der).expect("decode CRL");
|
||||
println!("{:#?}", RpkixCrlPretty::from(&crl));
|
||||
}
|
||||
93
tests/test_rc_from_der_errors.rs
Normal file
93
tests/test_rc_from_der_errors.rs
Normal file
@ -0,0 +1,93 @@
|
||||
use rpki::data_model::rc::{ResourceCertificate, ResourceCertificateError};
|
||||
|
||||
const TEST_NO_SIA_CERT_DER_B64: &str = "MIIDATCCAemgAwIBAgIUCyQLQJn92+gyAzvIz22q1F/97OMwDQYJKoZIhvcNAQELBQAwGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMB4XDTI2MDEyNzAzNTk1OVoXDTM2MDEyNTAzNTk1OVowGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/aoMU8J6cddkM2r6F2snd1rCdQPepgo2T2lrqWFcxnQJdcxBL1OYg3wFi95TJmZSeIHIOGauDaJ2abmjgyOUHOC4U68x66JRg4hLkmLxo1cf3uYHWl9Obph6g2qPRvN80ORq70JPuL6mAfUkNiO9hnwK6oQiTzc/rjCQGIFH8kTESBMXLfNCyUpGi+MNztYH6Ha6bKAQuXgd29OFwIkOlGQnYgGC2qBMvnp86eITvV1gTiuI8Ho9m9nZHCmaD7TylvkMDq8Hk5nkIpRcG0uO60SkR2BiMOYe/TNn5dTmHd6bsdbU2GOvgnq1SnqGq3FOWhKIe3ycUJde0uNfZOqRwIDAQABoz8wPTAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUFjyzfJCDNhFfKxVr06kjUkE23dMwDQYJKoZIhvcNAQELBQADggEBAK98n2gVlwKA3Ob1YeAm9f+8hm7pbvrt0tA8GW180CILjf09k7fKgiRlxqGdZ9ySXjU52+zCqu3MpBXVbI87ZC+zA6uK05n4y1F0n85MJ9hGR2UEiPcqou85X73LvioynnSOy/OV1PjKJXReUsqF3GgDtgcMyFssPJ9s/5DWuUCScUJY6pu0kuIGOLQ/oXUw4TvxUeyz73gOTiAJshVTQoLpHUhj0595S7lArjwi7oLI1b8m8guTknvhk0Sc3tJZmUqOcIvYIs0guHpaeC+sMoF4K+6UTrxxOBdX+fUEWNpUyYXWHjdZq25PbJdHwA/VAW2zYVojaVREligf0Qfo6F4=";
|
||||
|
||||
fn decode_b64(b64: &str) -> Vec<u8> {
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine as _;
|
||||
STANDARD.decode(b64).unwrap()
|
||||
}
|
||||
|
||||
fn replace_first(haystack: &mut [u8], needle: &[u8], replacement: &[u8]) -> bool {
|
||||
if needle.len() != replacement.len() {
|
||||
return false;
|
||||
}
|
||||
for i in 0..=haystack.len().saturating_sub(needle.len()) {
|
||||
if &haystack[i..i + needle.len()] == needle {
|
||||
haystack[i..i + needle.len()].copy_from_slice(replacement);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn replace_all(haystack: &mut [u8], needle: &[u8], replacement: &[u8]) -> usize {
|
||||
if needle.len() != replacement.len() {
|
||||
return 0;
|
||||
}
|
||||
let mut n = 0;
|
||||
let mut i = 0;
|
||||
while i + needle.len() <= haystack.len() {
|
||||
if &haystack[i..i + needle.len()] == needle {
|
||||
haystack[i..i + needle.len()].copy_from_slice(replacement);
|
||||
n += 1;
|
||||
i += needle.len();
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_bytes_after_cert_are_rejected() {
|
||||
let mut der = decode_b64(TEST_NO_SIA_CERT_DER_B64);
|
||||
der.push(0);
|
||||
let err = ResourceCertificate::from_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ResourceCertificateError::TrailingBytes(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_algorithm_mismatch_is_detected() {
|
||||
let mut der = decode_b64(TEST_NO_SIA_CERT_DER_B64);
|
||||
// DER encoding of sha256WithRSAEncryption OID:
|
||||
// 06 09 2A 86 48 86 F7 0D 01 01 0B
|
||||
let oid = [0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B];
|
||||
let mut patched = oid;
|
||||
patched[10] = 0x01; // rsaEncryption, same length encoding
|
||||
assert!(replace_first(&mut der, &oid, &patched));
|
||||
let err = ResourceCertificate::from_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ResourceCertificateError::SignatureAlgorithmMismatch));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_signature_algorithm_is_detected() {
|
||||
let mut der = decode_b64(TEST_NO_SIA_CERT_DER_B64);
|
||||
let oid = [0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B];
|
||||
let mut patched = oid;
|
||||
patched[10] = 0x01;
|
||||
let n = replace_all(&mut der, &oid, &patched);
|
||||
assert!(n >= 2, "expected to patch at least inner+outer AlgorithmIdentifier");
|
||||
let err = ResourceCertificate::from_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ResourceCertificateError::UnsupportedSignatureAlgorithm));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_signature_algorithm_parameters_are_detected() {
|
||||
let mut der = decode_b64(TEST_NO_SIA_CERT_DER_B64);
|
||||
// Replace NULL parameters (05 00) right after the sha256WithRSAEncryption OID with
|
||||
// an empty OCTET STRING (04 00) to keep the encoding length unchanged.
|
||||
let alg = [
|
||||
0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B, 0x05, 0x00,
|
||||
];
|
||||
let mut patched = alg;
|
||||
patched[11] = 0x04;
|
||||
let n = replace_all(&mut der, &alg, &patched);
|
||||
assert!(n >= 2, "expected to patch at least inner+outer AlgorithmIdentifier parameters");
|
||||
let err = ResourceCertificate::from_der(&der).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
ResourceCertificateError::InvalidSignatureAlgorithmParameters
|
||||
));
|
||||
}
|
||||
|
||||
43
tests/test_rc_from_der_fixtures.rs
Normal file
43
tests/test_rc_from_der_fixtures.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use rpki::data_model::oid::OID_CP_IPADDR_ASNUMBER;
|
||||
use rpki::data_model::rc::{ResourceCertKind, ResourceCertificate, SubjectInfoAccess};
|
||||
|
||||
#[test]
|
||||
fn resource_certificate_from_der_parses_ca_fixtures() {
|
||||
let fixtures = [
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer",
|
||||
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ZW5EIqvxKWSSAOsBmoFfKxIjbpI.cer",
|
||||
];
|
||||
|
||||
for path in fixtures {
|
||||
let der = std::fs::read(path).expect("read CA cert fixture");
|
||||
let rc = ResourceCertificate::from_der(&der).expect("parse CA cert fixture");
|
||||
|
||||
assert_eq!(rc.kind, ResourceCertKind::Ca, "fixture should be CA: {path}");
|
||||
assert_eq!(rc.tbs.version, 2, "X.509 v3 encoded as 2: {path}");
|
||||
|
||||
assert_eq!(
|
||||
rc.tbs.extensions.certificate_policies_oid.as_deref(),
|
||||
Some(OID_CP_IPADDR_ASNUMBER),
|
||||
"CA certificatePolicies OID: {path}"
|
||||
);
|
||||
|
||||
assert!(
|
||||
matches!(rc.tbs.extensions.subject_info_access, Some(SubjectInfoAccess::Ca(_))),
|
||||
"CA SIA should not contain signedObject accessMethod: {path}"
|
||||
);
|
||||
|
||||
assert!(rc.tbs.extensions.ip_resources.is_some(), "CA should have IP resources: {path}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resource_certificate_from_der_parses_as_resources_in_apnic_fixture() {
|
||||
let path =
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer";
|
||||
let der = std::fs::read(path).expect("read APNIC CA cert fixture");
|
||||
let rc = ResourceCertificate::from_der(&der).expect("parse APNIC CA cert fixture");
|
||||
|
||||
assert!(rc.tbs.extensions.as_resources.is_some(), "fixture should carry AS resources");
|
||||
}
|
||||
|
||||
93
tests/test_rc_helpers.rs
Normal file
93
tests/test_rc_helpers.rs
Normal file
@ -0,0 +1,93 @@
|
||||
use rpki::data_model::rc::{
|
||||
Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange,
|
||||
IpPrefix,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn ip_address_family_contains_prefix_afi_mismatch_is_false() {
|
||||
let fam = IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::Inherit,
|
||||
};
|
||||
let p = IpPrefix {
|
||||
afi: Afi::Ipv6,
|
||||
prefix_len: 0,
|
||||
addr: vec![0; 16],
|
||||
};
|
||||
assert!(!fam.contains_prefix(&p));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_address_family_inherit_covers_all_prefixes_of_same_afi() {
|
||||
let fam = IpAddressFamily {
|
||||
afi: Afi::Ipv6,
|
||||
choice: IpAddressChoice::Inherit,
|
||||
};
|
||||
let p = IpPrefix {
|
||||
afi: Afi::Ipv6,
|
||||
prefix_len: 32,
|
||||
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
};
|
||||
assert!(fam.contains_prefix(&p));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_resource_set_asnum_single_id_returns_expected_values() {
|
||||
let inherit = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::Inherit),
|
||||
rdi: None,
|
||||
};
|
||||
assert_eq!(inherit.asnum_single_id(), None);
|
||||
|
||||
let single_id = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64512)])),
|
||||
rdi: None,
|
||||
};
|
||||
assert_eq!(single_id.asnum_single_id(), Some(64512));
|
||||
|
||||
let single_range = AsResourceSet {
|
||||
asnum: None,
|
||||
rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range {
|
||||
min: 64512,
|
||||
max: 64520,
|
||||
}])),
|
||||
};
|
||||
assert_eq!(single_range.asnum_single_id(), None);
|
||||
|
||||
let multiple = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
||||
AsIdOrRange::Id(64512),
|
||||
AsIdOrRange::Id(64513),
|
||||
])),
|
||||
rdi: None,
|
||||
};
|
||||
assert_eq!(multiple.asnum_single_id(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_identifier_choice_has_range_detects_ranges() {
|
||||
assert!(!AsIdentifierChoice::Inherit.has_range());
|
||||
assert!(!AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64512)]).has_range());
|
||||
assert!(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range { min: 1, max: 2 }]).has_range());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_resource_contains_prefix_finds_matching_family() {
|
||||
let fam_v6 = IpAddressFamily {
|
||||
afi: Afi::Ipv6,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(IpPrefix {
|
||||
afi: Afi::Ipv6,
|
||||
prefix_len: 32,
|
||||
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
})]),
|
||||
};
|
||||
let set = rpki::data_model::rc::IpResourceSet { families: vec![fam_v6] };
|
||||
|
||||
let p = IpPrefix {
|
||||
afi: Afi::Ipv6,
|
||||
prefix_len: 48,
|
||||
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0x12, 0x34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
};
|
||||
assert!(set.contains_prefix(&p));
|
||||
}
|
||||
|
||||
58
tests/test_rc_ip_resource_range_coverage.rs
Normal file
58
tests/test_rc_ip_resource_range_coverage.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use rpki::data_model::rc::{
|
||||
Afi, IpAddressChoice, IpAddressFamily, IpAddressOrRange, IpAddressRange, IpPrefix, IpResourceSet,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn ip_resource_range_covers_prefix_ipv4() {
|
||||
let set = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Range(IpAddressRange {
|
||||
min: vec![10, 0, 0, 0],
|
||||
max: vec![10, 255, 255, 255],
|
||||
})]),
|
||||
}],
|
||||
};
|
||||
|
||||
let inside = IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 16,
|
||||
addr: vec![10, 1, 0, 0],
|
||||
};
|
||||
assert!(set.contains_prefix(&inside));
|
||||
|
||||
let outside = IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 16,
|
||||
addr: vec![11, 0, 0, 0],
|
||||
};
|
||||
assert!(!set.contains_prefix(&outside));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_resource_range_covers_prefix_ipv6() {
|
||||
let set = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv6,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Range(IpAddressRange {
|
||||
min: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
max: vec![0x20, 0x01, 0x0d, 0xb8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
|
||||
})]),
|
||||
}],
|
||||
};
|
||||
|
||||
let inside = IpPrefix {
|
||||
afi: Afi::Ipv6,
|
||||
prefix_len: 64,
|
||||
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
};
|
||||
assert!(set.contains_prefix(&inside));
|
||||
|
||||
let outside = IpPrefix {
|
||||
afi: Afi::Ipv6,
|
||||
prefix_len: 64,
|
||||
addr: vec![0x20, 0x01, 0x0d, 0xb9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
};
|
||||
assert!(!set.contains_prefix(&outside));
|
||||
}
|
||||
|
||||
135
tests/test_rc_resource_extensions_decode.rs
Normal file
135
tests/test_rc_resource_extensions_decode.rs
Normal file
@ -0,0 +1,135 @@
|
||||
use rpki::data_model::rc::{
|
||||
Afi, AsIdentifierChoice, AsIdOrRange, AsResourceSet, IpAddressChoice, IpAddressOrRange,
|
||||
IpResourceSet,
|
||||
};
|
||||
|
||||
fn len_bytes(len: usize) -> Vec<u8> {
|
||||
if len < 128 {
|
||||
vec![len as u8]
|
||||
} else {
|
||||
let mut tmp = Vec::new();
|
||||
let mut n = len;
|
||||
while n > 0 {
|
||||
tmp.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
tmp.reverse();
|
||||
let mut out = vec![0x80 | (tmp.len() as u8)];
|
||||
out.extend(tmp);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
fn tlv(tag: u8, content: &[u8]) -> Vec<u8> {
|
||||
let mut out = vec![tag];
|
||||
out.extend(len_bytes(content.len()));
|
||||
out.extend_from_slice(content);
|
||||
out
|
||||
}
|
||||
|
||||
fn der_sequence(children: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
let mut content = Vec::new();
|
||||
for c in children {
|
||||
content.extend(c);
|
||||
}
|
||||
tlv(0x30, &content)
|
||||
}
|
||||
|
||||
fn der_octet_string(bytes: &[u8]) -> Vec<u8> {
|
||||
tlv(0x04, bytes)
|
||||
}
|
||||
|
||||
fn der_null() -> Vec<u8> {
|
||||
vec![0x05, 0x00]
|
||||
}
|
||||
|
||||
fn der_integer_u64(v: u64) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
let mut n = v;
|
||||
if n == 0 {
|
||||
bytes.push(0);
|
||||
} else {
|
||||
while n > 0 {
|
||||
bytes.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
bytes.reverse();
|
||||
if bytes[0] & 0x80 != 0 {
|
||||
bytes.insert(0, 0);
|
||||
}
|
||||
}
|
||||
tlv(0x02, &bytes)
|
||||
}
|
||||
|
||||
fn der_bit_string(unused: u8, bytes: &[u8]) -> Vec<u8> {
|
||||
let mut content = vec![unused];
|
||||
content.extend_from_slice(bytes);
|
||||
tlv(0x03, &content)
|
||||
}
|
||||
|
||||
fn cs_cons(tag_no: u8, inner_der: Vec<u8>) -> Vec<u8> {
|
||||
tlv(0xA0 | (tag_no & 0x1F), &inner_der)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_addr_blocks_decode_handles_inherit_and_range_endpoint_fill() {
|
||||
// IPv6 family with one range:
|
||||
// - min: 240a:a000::/32 (encoded as 32 bits)
|
||||
// - max: 240a:a008::/31 (encoded as 31 bits, so fill-ones yields 240a:a009:ffff..)
|
||||
let address_family = der_octet_string(&[0x00, 0x02]);
|
||||
let min = der_bit_string(0, &[0x24, 0x0A, 0xA0, 0x00]);
|
||||
let max = der_bit_string(1, &[0x24, 0x0A, 0xA0, 0x08]);
|
||||
let range = der_sequence(vec![min, max]);
|
||||
let choice = der_sequence(vec![range]);
|
||||
let fam = der_sequence(vec![address_family, choice]);
|
||||
let ip_addr_blocks = der_sequence(vec![fam]);
|
||||
|
||||
let decoded = IpResourceSet::decode_extn_value(&ip_addr_blocks).expect("decode ipAddrBlocks");
|
||||
assert_eq!(decoded.families.len(), 1);
|
||||
let fam = &decoded.families[0];
|
||||
assert_eq!(fam.afi, Afi::Ipv6);
|
||||
let IpAddressChoice::AddressesOrRanges(items) = &fam.choice else {
|
||||
panic!("expected AddressesOrRanges");
|
||||
};
|
||||
let IpAddressOrRange::Range(r) = &items[0] else {
|
||||
panic!("expected Range");
|
||||
};
|
||||
assert_eq!(r.min[..4], [0x24, 0x0A, 0xA0, 0x00]);
|
||||
assert_eq!(r.max[..4], [0x24, 0x0A, 0xA0, 0x09]);
|
||||
assert!(r.max[4..].iter().all(|&b| b == 0xFF));
|
||||
|
||||
// Inherit branch.
|
||||
let fam_inherit = der_sequence(vec![der_octet_string(&[0x00, 0x01]), der_null()]);
|
||||
let decoded = IpResourceSet::decode_extn_value(&der_sequence(vec![fam_inherit])).expect("decode inherit");
|
||||
assert!(decoded.is_all_inherit());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autonomous_sys_ids_decode_handles_inherit_ids_and_ranges() {
|
||||
// ASIdentifiers with:
|
||||
// - asnum [0] EXPLICIT asIdsOrRanges { id 64496, range 64500..64510 }
|
||||
let as_id_or_ranges = der_sequence(vec![
|
||||
der_integer_u64(64496),
|
||||
der_sequence(vec![der_integer_u64(64500), der_integer_u64(64510)]),
|
||||
]);
|
||||
let asnum = cs_cons(0, as_id_or_ranges);
|
||||
let as_identifiers = der_sequence(vec![asnum]);
|
||||
|
||||
let decoded = AsResourceSet::decode_extn_value(&as_identifiers).expect("decode asIdentifiers");
|
||||
assert!(decoded.rdi.is_none());
|
||||
let Some(asnum) = decoded.asnum else {
|
||||
panic!("missing asnum");
|
||||
};
|
||||
let AsIdentifierChoice::AsIdsOrRanges(items) = asnum else {
|
||||
panic!("expected AsIdsOrRanges");
|
||||
};
|
||||
assert_eq!(items.len(), 2);
|
||||
assert!(matches!(items[0], AsIdOrRange::Id(64496)));
|
||||
assert!(matches!(items[1], AsIdOrRange::Range { min: 64500, max: 64510 }));
|
||||
|
||||
// asnum inherit.
|
||||
let asnum_inherit = cs_cons(0, der_null());
|
||||
let decoded = AsResourceSet::decode_extn_value(&der_sequence(vec![asnum_inherit])).expect("decode inherit");
|
||||
assert!(decoded.is_asnum_inherit());
|
||||
}
|
||||
|
||||
202
tests/test_rc_resource_extensions_decode_errors.rs
Normal file
202
tests/test_rc_resource_extensions_decode_errors.rs
Normal file
@ -0,0 +1,202 @@
|
||||
use rpki::data_model::rc::{AsResourceSet, IpResourceSet};
|
||||
|
||||
fn len_bytes(len: usize) -> Vec<u8> {
|
||||
if len < 128 {
|
||||
vec![len as u8]
|
||||
} else {
|
||||
let mut tmp = Vec::new();
|
||||
let mut n = len;
|
||||
while n > 0 {
|
||||
tmp.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
tmp.reverse();
|
||||
let mut out = vec![0x80 | (tmp.len() as u8)];
|
||||
out.extend(tmp);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
fn tlv(tag: u8, content: &[u8]) -> Vec<u8> {
|
||||
let mut out = vec![tag];
|
||||
out.extend(len_bytes(content.len()));
|
||||
out.extend_from_slice(content);
|
||||
out
|
||||
}
|
||||
|
||||
fn der_sequence(children: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
let mut content = Vec::new();
|
||||
for c in children {
|
||||
content.extend(c);
|
||||
}
|
||||
tlv(0x30, &content)
|
||||
}
|
||||
|
||||
fn der_octet_string(bytes: &[u8]) -> Vec<u8> {
|
||||
tlv(0x04, bytes)
|
||||
}
|
||||
|
||||
fn der_null() -> Vec<u8> {
|
||||
vec![0x05, 0x00]
|
||||
}
|
||||
|
||||
fn der_integer_bytes(bytes: &[u8]) -> Vec<u8> {
|
||||
tlv(0x02, bytes)
|
||||
}
|
||||
|
||||
fn der_integer_u64(v: u64) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
let mut n = v;
|
||||
if n == 0 {
|
||||
bytes.push(0);
|
||||
} else {
|
||||
while n > 0 {
|
||||
bytes.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
bytes.reverse();
|
||||
if bytes[0] & 0x80 != 0 {
|
||||
bytes.insert(0, 0);
|
||||
}
|
||||
}
|
||||
der_integer_bytes(&bytes)
|
||||
}
|
||||
|
||||
fn der_bit_string(unused: u8, bytes: &[u8]) -> Vec<u8> {
|
||||
let mut content = vec![unused];
|
||||
content.extend_from_slice(bytes);
|
||||
tlv(0x03, &content)
|
||||
}
|
||||
|
||||
fn cs_cons(tag_no: u8, inner_der: Vec<u8>) -> Vec<u8> {
|
||||
tlv(0xA0 | (tag_no & 0x1F), &inner_der)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_addr_blocks_decode_rejects_invalid_encodings() {
|
||||
// Not a SEQUENCE.
|
||||
assert!(IpResourceSet::decode_extn_value(&der_null()).is_err());
|
||||
|
||||
// IPAddressFamily wrong shape.
|
||||
let fam_wrong = der_sequence(vec![der_octet_string(&[0x00, 0x01])]); // missing choice
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam_wrong])).is_err());
|
||||
|
||||
// addressFamily wrong length.
|
||||
let fam_wrong = der_sequence(vec![
|
||||
der_octet_string(&[0x00]), // 1 byte
|
||||
der_null(),
|
||||
]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam_wrong])).is_err());
|
||||
|
||||
// unsupported AFI.
|
||||
let fam_wrong = der_sequence(vec![der_octet_string(&[0x00, 0x03]), der_null()]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam_wrong])).is_err());
|
||||
|
||||
// ipAddressChoice wrong type.
|
||||
let fam_wrong = der_sequence(vec![
|
||||
der_octet_string(&[0x00, 0x01]),
|
||||
der_integer_u64(1),
|
||||
]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam_wrong])).is_err());
|
||||
|
||||
// BitString with invalid unused-bits value (>7).
|
||||
let min = der_bit_string(0, &[0x0A, 0x00, 0x00, 0x00]);
|
||||
let max = der_bit_string(8, &[0x0A, 0xFF, 0xFF, 0xFF]); // invalid unused bits
|
||||
let range = der_sequence(vec![min, max]);
|
||||
let fam = der_sequence(vec![
|
||||
der_octet_string(&[0x00, 0x01]),
|
||||
der_sequence(vec![range]),
|
||||
]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).is_err());
|
||||
|
||||
// BitString with non-zero bits in the unused tail.
|
||||
let min = der_bit_string(0, &[0x0A, 0x00, 0x00, 0x00]);
|
||||
let max = der_bit_string(1, &[0x0A, 0x00, 0x00, 0x01]); // LSB set, but unused=1
|
||||
let range = der_sequence(vec![min, max]);
|
||||
let fam = der_sequence(vec![
|
||||
der_octet_string(&[0x00, 0x01]),
|
||||
der_sequence(vec![range]),
|
||||
]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).is_err());
|
||||
|
||||
// Prefix length out of range for IPv4 (40 bits).
|
||||
let prefix = der_bit_string(0, &[0x0A, 0x00, 0x00, 0x00, 0x00]); // 40 bits
|
||||
let fam = der_sequence(vec![
|
||||
der_octet_string(&[0x00, 0x01]),
|
||||
der_sequence(vec![prefix]),
|
||||
]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autonomous_sys_ids_decode_rejects_invalid_encodings() {
|
||||
// Not a SEQUENCE.
|
||||
assert!(AsResourceSet::decode_extn_value(&der_null()).is_err());
|
||||
|
||||
// Wrong tag class (expects [0]/[1] context-specific).
|
||||
let as_ids = der_sequence(vec![der_integer_u64(64496)]);
|
||||
assert!(AsResourceSet::decode_extn_value(&as_ids).is_err());
|
||||
|
||||
// Duplicate [0] tags.
|
||||
let a0 = cs_cons(0, der_null());
|
||||
let a0_dup = cs_cons(0, der_null());
|
||||
assert!(AsResourceSet::decode_extn_value(&der_sequence(vec![a0, a0_dup])).is_err());
|
||||
|
||||
// Out-of-range ASID (> u32::MAX).
|
||||
let too_big = der_integer_bytes(&[0x01, 0x00, 0x00, 0x00, 0x00]); // 2^32
|
||||
let asnum = cs_cons(0, der_sequence(vec![too_big]));
|
||||
assert!(AsResourceSet::decode_extn_value(&der_sequence(vec![asnum])).is_err());
|
||||
|
||||
// Range min > max.
|
||||
let bad_range = der_sequence(vec![der_integer_u64(64510), der_integer_u64(64500)]);
|
||||
let asnum = cs_cons(0, der_sequence(vec![bad_range]));
|
||||
assert!(AsResourceSet::decode_extn_value(&der_sequence(vec![asnum])).is_err());
|
||||
|
||||
// Unsupported element inside asIdsOrRanges.
|
||||
let asnum = cs_cons(0, der_sequence(vec![der_null()]));
|
||||
assert!(AsResourceSet::decode_extn_value(&der_sequence(vec![asnum])).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_addr_blocks_prefix_bitstring_unused_bits_checks_are_enforced() {
|
||||
// Prefix BitString with non-zero bits in the unused tail (parse_ip_prefix path).
|
||||
let prefix = der_bit_string(1, &[0x0A, 0x00, 0x00, 0x01]); // LSB set, but unused=1
|
||||
let fam = der_sequence(vec![
|
||||
der_octet_string(&[0x00, 0x01]),
|
||||
der_sequence(vec![prefix]),
|
||||
]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).is_err());
|
||||
|
||||
// Prefix BitString with empty bytes but unused_bits != 0 is invalid.
|
||||
let prefix = der_bit_string(1, &[]);
|
||||
let fam = der_sequence(vec![
|
||||
der_octet_string(&[0x00, 0x01]),
|
||||
der_sequence(vec![prefix]),
|
||||
]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_addr_blocks_range_upper_bound_can_fill_all_ones_when_bit_len_zero() {
|
||||
// A BIT STRING with 0 bits (content is only the "unused bits" count octet) is allowed.
|
||||
// For an IPAddressRange upper bound, RFC 3779 interprets missing bits as 1s.
|
||||
let min = der_bit_string(0, &[]);
|
||||
let max = der_bit_string(0, &[]);
|
||||
let range = der_sequence(vec![min, max]);
|
||||
let fam = der_sequence(vec![
|
||||
der_octet_string(&[0x00, 0x01]),
|
||||
der_sequence(vec![range]),
|
||||
]);
|
||||
|
||||
let set = IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).expect("decode range with 0-bit endpoints");
|
||||
let fam = set.families.iter().find(|f| f.afi == rpki::data_model::rc::Afi::Ipv4).unwrap();
|
||||
let rpki::data_model::rc::IpAddressChoice::AddressesOrRanges(items) = &fam.choice else {
|
||||
panic!("expected explicit addressesOrRanges");
|
||||
};
|
||||
assert_eq!(items.len(), 1);
|
||||
let rpki::data_model::rc::IpAddressOrRange::Range(r) = &items[0] else {
|
||||
panic!("expected a range");
|
||||
};
|
||||
assert_eq!(r.min, vec![0, 0, 0, 0]);
|
||||
assert_eq!(r.max, vec![0xFF, 0xFF, 0xFF, 0xFF]);
|
||||
}
|
||||
127
tests/test_roa_canonicalize.rs
Normal file
127
tests/test_roa_canonicalize.rs
Normal file
@ -0,0 +1,127 @@
|
||||
use rpki::data_model::roa::{IpPrefix, RoaAfi, RoaEContent};
|
||||
|
||||
fn len_bytes(len: usize) -> Vec<u8> {
|
||||
if len < 128 {
|
||||
vec![len as u8]
|
||||
} else {
|
||||
let mut tmp = Vec::new();
|
||||
let mut n = len;
|
||||
while n > 0 {
|
||||
tmp.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
tmp.reverse();
|
||||
let mut out = vec![0x80 | (tmp.len() as u8)];
|
||||
out.extend(tmp);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
fn tlv(tag: u8, content: &[u8]) -> Vec<u8> {
|
||||
let mut out = vec![tag];
|
||||
out.extend(len_bytes(content.len()));
|
||||
out.extend_from_slice(content);
|
||||
out
|
||||
}
|
||||
|
||||
fn der_integer_u64(v: u64) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
let mut n = v;
|
||||
if n == 0 {
|
||||
bytes.push(0);
|
||||
} else {
|
||||
while n > 0 {
|
||||
bytes.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
bytes.reverse();
|
||||
if bytes[0] & 0x80 != 0 {
|
||||
bytes.insert(0, 0);
|
||||
}
|
||||
}
|
||||
tlv(0x02, &bytes)
|
||||
}
|
||||
|
||||
fn der_octet_string(bytes: &[u8]) -> Vec<u8> {
|
||||
tlv(0x04, bytes)
|
||||
}
|
||||
|
||||
fn der_bit_string_from_prefix(prefix_addr: &[u8], prefix_len: u16) -> Vec<u8> {
|
||||
let byte_len = ((prefix_len as usize) + 7) / 8;
|
||||
let unused = (byte_len * 8) as u16 - prefix_len;
|
||||
let mut bytes = prefix_addr[..byte_len.min(prefix_addr.len())].to_vec();
|
||||
while bytes.len() < byte_len {
|
||||
bytes.push(0);
|
||||
}
|
||||
if !bytes.is_empty() && unused != 0 {
|
||||
let mask = (1u8 << (unused as u8)) - 1;
|
||||
let last = bytes.len() - 1;
|
||||
bytes[last] &= !mask;
|
||||
}
|
||||
let mut content = vec![unused as u8];
|
||||
content.extend(bytes);
|
||||
tlv(0x03, &content)
|
||||
}
|
||||
|
||||
fn der_sequence(children: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
let mut content = Vec::new();
|
||||
for c in children {
|
||||
content.extend(c);
|
||||
}
|
||||
tlv(0x30, &content)
|
||||
}
|
||||
|
||||
fn roa_ip_address(prefix_bs: Vec<u8>) -> Vec<u8> {
|
||||
der_sequence(vec![prefix_bs])
|
||||
}
|
||||
|
||||
fn roa_ip_family(afi: [u8; 2], addresses: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
der_sequence(vec![der_octet_string(&afi), der_sequence(addresses)])
|
||||
}
|
||||
|
||||
fn roa_attestation(as_id: u64, families: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
der_sequence(vec![der_integer_u64(as_id), der_sequence(families)])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_sorts_families_sorts_and_dedups_addresses() {
|
||||
// Build:
|
||||
// - families are in order: IPv6 then IPv4 (should become IPv4 then IPv6)
|
||||
// - IPv4 addresses contain a duplicate (should dedup)
|
||||
// - IPv4 /24 uses only 3 bytes, should be padded to 4 with host bits cleared
|
||||
let v6 = roa_ip_family(
|
||||
[0, 2],
|
||||
vec![roa_ip_address(der_bit_string_from_prefix(
|
||||
&[0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
32,
|
||||
))],
|
||||
);
|
||||
|
||||
let v4_prefix = der_bit_string_from_prefix(&[192, 0, 2, 255], 24);
|
||||
let v4 = roa_ip_family(
|
||||
[0, 1],
|
||||
vec![
|
||||
roa_ip_address(v4_prefix.clone()),
|
||||
roa_ip_address(v4_prefix), // duplicate
|
||||
],
|
||||
);
|
||||
|
||||
let der = roa_attestation(64496, vec![v6, v4]);
|
||||
let roa = RoaEContent::decode_der(&der).expect("decode roa econtent");
|
||||
|
||||
assert_eq!(roa.ip_addr_blocks.len(), 2);
|
||||
assert_eq!(roa.ip_addr_blocks[0].afi, RoaAfi::Ipv4);
|
||||
assert_eq!(roa.ip_addr_blocks[1].afi, RoaAfi::Ipv6);
|
||||
|
||||
let v4_fam = &roa.ip_addr_blocks[0];
|
||||
assert_eq!(v4_fam.addresses.len(), 1);
|
||||
assert_eq!(
|
||||
v4_fam.addresses[0].prefix,
|
||||
IpPrefix {
|
||||
afi: RoaAfi::Ipv4,
|
||||
prefix_len: 24,
|
||||
addr: vec![192, 0, 2, 0],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
33
tests/test_roa_decode.rs
Normal file
33
tests/test_roa_decode.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use rpki::data_model::roa::{RoaDecodeError, RoaObject};
|
||||
|
||||
#[test]
|
||||
fn decode_roa_fixture_smoke() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa",
|
||||
)
|
||||
.expect("read ROA fixture");
|
||||
let roa = RoaObject::decode_der(&der).expect("decode roa");
|
||||
assert_eq!(
|
||||
roa.econtent_type,
|
||||
rpki::data_model::oid::OID_CT_ROUTE_ORIGIN_AUTHZ
|
||||
);
|
||||
assert_eq!(roa.roa.version, 0);
|
||||
assert_eq!(roa.roa.as_id, 4538);
|
||||
assert!(!roa.roa.ip_addr_blocks.is_empty());
|
||||
assert!(roa
|
||||
.roa
|
||||
.ip_addr_blocks
|
||||
.iter()
|
||||
.all(|f| !f.addresses.is_empty()));
|
||||
println!("{roa:#?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rejects_non_roa_econtent_type() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
)
|
||||
.expect("read MFT fixture");
|
||||
let err = RoaObject::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidEContentType(_)));
|
||||
}
|
||||
205
tests/test_roa_econtent_decode_errors.rs
Normal file
205
tests/test_roa_econtent_decode_errors.rs
Normal file
@ -0,0 +1,205 @@
|
||||
use rpki::data_model::roa::{RoaDecodeError, RoaEContent};
|
||||
|
||||
fn len_bytes(len: usize) -> Vec<u8> {
|
||||
if len < 128 {
|
||||
vec![len as u8]
|
||||
} else {
|
||||
let mut tmp = Vec::new();
|
||||
let mut n = len;
|
||||
while n > 0 {
|
||||
tmp.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
tmp.reverse();
|
||||
let mut out = vec![0x80 | (tmp.len() as u8)];
|
||||
out.extend(tmp);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
fn tlv(tag: u8, content: &[u8]) -> Vec<u8> {
|
||||
let mut out = vec![tag];
|
||||
out.extend(len_bytes(content.len()));
|
||||
out.extend_from_slice(content);
|
||||
out
|
||||
}
|
||||
|
||||
fn der_integer_u64(v: u64) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
let mut n = v;
|
||||
if n == 0 {
|
||||
bytes.push(0);
|
||||
} else {
|
||||
while n > 0 {
|
||||
bytes.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
bytes.reverse();
|
||||
if bytes[0] & 0x80 != 0 {
|
||||
bytes.insert(0, 0);
|
||||
}
|
||||
}
|
||||
tlv(0x02, &bytes)
|
||||
}
|
||||
|
||||
fn der_octet_string(bytes: &[u8]) -> Vec<u8> {
|
||||
tlv(0x04, bytes)
|
||||
}
|
||||
|
||||
fn der_bit_string(unused: u8, bytes: &[u8]) -> Vec<u8> {
|
||||
let mut content = vec![unused];
|
||||
content.extend_from_slice(bytes);
|
||||
tlv(0x03, &content)
|
||||
}
|
||||
|
||||
fn der_sequence(children: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
let mut content = Vec::new();
|
||||
for c in children {
|
||||
content.extend(c);
|
||||
}
|
||||
tlv(0x30, &content)
|
||||
}
|
||||
|
||||
fn cs_explicit(tag_no: u8, inner_der: Vec<u8>) -> Vec<u8> {
|
||||
tlv(0xA0 | (tag_no & 0x1F), &inner_der)
|
||||
}
|
||||
|
||||
fn roa_ip_address(prefix_bs: Vec<u8>, max_len: Option<u64>) -> Vec<u8> {
|
||||
let mut fields = vec![prefix_bs];
|
||||
if let Some(m) = max_len {
|
||||
fields.push(der_integer_u64(m));
|
||||
}
|
||||
der_sequence(fields)
|
||||
}
|
||||
|
||||
fn roa_ip_family(afi: [u8; 2], addresses: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
der_sequence(vec![der_octet_string(&afi), der_sequence(addresses)])
|
||||
}
|
||||
|
||||
fn roa_attestation(version: Option<u64>, as_id: u64, families: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
let mut fields = Vec::new();
|
||||
if let Some(v) = version {
|
||||
fields.push(cs_explicit(0, der_integer_u64(v)));
|
||||
}
|
||||
fields.push(der_integer_u64(as_id));
|
||||
fields.push(der_sequence(families));
|
||||
der_sequence(fields)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_must_be_zero_when_present() {
|
||||
let der = roa_attestation(
|
||||
Some(1),
|
||||
64496,
|
||||
vec![roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)])],
|
||||
);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidVersion(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_id_out_of_range_is_rejected() {
|
||||
let der = roa_attestation(
|
||||
None,
|
||||
(u32::MAX as u64) + 1,
|
||||
vec![roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)])],
|
||||
);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::AsIdOutOfRange(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_addr_blocks_len_is_validated() {
|
||||
let der = roa_attestation(None, 64496, vec![]);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidIpAddrBlocksLen(0)));
|
||||
|
||||
let families = vec![
|
||||
roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)]),
|
||||
roa_ip_family([0, 2], vec![roa_ip_address(der_bit_string(0, &[]), None)]),
|
||||
roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)]),
|
||||
];
|
||||
let der = roa_attestation(None, 64496, families);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidIpAddrBlocksLen(3)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_afi_is_rejected() {
|
||||
let families = vec![
|
||||
roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)]),
|
||||
roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)]),
|
||||
];
|
||||
let der = roa_attestation(None, 64496, families);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::DuplicateAfi(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_afi_is_rejected() {
|
||||
let der = roa_attestation(
|
||||
None,
|
||||
64496,
|
||||
vec![roa_ip_family([0x12, 0x34], vec![roa_ip_address(der_bit_string(0, &[]), None)])],
|
||||
);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::UnsupportedAfi(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_address_list_is_rejected() {
|
||||
let der = roa_attestation(None, 64496, vec![roa_ip_family([0, 1], vec![])]);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::EmptyAddressList));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix_unused_bits_must_be_zeroed() {
|
||||
// prefix_len=1 => unused_bits=7, last byte must have lower 7 bits zero.
|
||||
// Use 0b1000_0001 which has a non-zero unused bit.
|
||||
let bs = der_bit_string(7, &[0b1000_0001]);
|
||||
let der = roa_attestation(
|
||||
None,
|
||||
64496,
|
||||
vec![roa_ip_family([0, 1], vec![roa_ip_address(bs, None)])],
|
||||
);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidPrefixUnusedBits));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix_len_out_of_range_is_rejected() {
|
||||
// IPv4 ub=32, encode 33 bits: 5 bytes with unused_bits=7 => 40-7=33.
|
||||
let bs = der_bit_string(7, &[0u8; 5]);
|
||||
let der = roa_attestation(
|
||||
None,
|
||||
64496,
|
||||
vec![roa_ip_family([0, 1], vec![roa_ip_address(bs, None)])],
|
||||
);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::PrefixLenOutOfRange { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_length_range_and_relation_are_validated() {
|
||||
// IPv4, prefix_len=8
|
||||
let bs = der_bit_string(0, &[0x0A]);
|
||||
// maxLength < prefix_len
|
||||
let der = roa_attestation(
|
||||
None,
|
||||
64496,
|
||||
vec![roa_ip_family([0, 1], vec![roa_ip_address(bs.clone(), Some(7))])],
|
||||
);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidMaxLength { .. }));
|
||||
|
||||
// maxLength > ub
|
||||
let der = roa_attestation(
|
||||
None,
|
||||
64496,
|
||||
vec![roa_ip_family([0, 1], vec![roa_ip_address(bs, Some(33))])],
|
||||
);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidMaxLength { .. }));
|
||||
}
|
||||
|
||||
13
tests/test_roa_embedded_ee_cert.rs
Normal file
13
tests/test_roa_embedded_ee_cert.rs
Normal file
@ -0,0 +1,13 @@
|
||||
use rpki::data_model::roa::RoaObject;
|
||||
|
||||
#[test]
|
||||
fn roa_embedded_ee_cert_resources_validate() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa",
|
||||
)
|
||||
.expect("read ROA fixture");
|
||||
|
||||
let roa = RoaObject::decode_der(&der).expect("decode roa");
|
||||
roa.validate_embedded_ee_cert()
|
||||
.expect("roa EE cert resources must validate");
|
||||
}
|
||||
176
tests/test_roa_validate_ee_resources.rs
Normal file
176
tests/test_roa_validate_ee_resources.rs
Normal file
@ -0,0 +1,176 @@
|
||||
use der_parser::num_bigint::BigUint;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use rpki::data_model::rc::{
|
||||
Afi, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange, IpPrefix, IpResourceSet,
|
||||
RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, SubjectInfoAccess,
|
||||
};
|
||||
use rpki::data_model::roa::{RoaAfi, RoaEContent, RoaIpAddress, RoaIpAddressFamily, RoaValidateError};
|
||||
|
||||
fn dummy_ee(ip_resources: Option<IpResourceSet>, as_resources: Option<AsResourceSet>) -> ResourceCertificate {
|
||||
ResourceCertificate {
|
||||
raw_der: vec![],
|
||||
tbs: RpkixTbsCertificate {
|
||||
version: 2,
|
||||
serial_number: BigUint::from(1u8),
|
||||
signature_algorithm: "1.2.840.113549.1.1.11".to_string(),
|
||||
issuer_dn: "CN=issuer".to_string(),
|
||||
subject_dn: "CN=subject".to_string(),
|
||||
validity_not_before: OffsetDateTime::UNIX_EPOCH,
|
||||
validity_not_after: OffsetDateTime::UNIX_EPOCH,
|
||||
subject_public_key_info: vec![],
|
||||
extensions: RcExtensions {
|
||||
basic_constraints_ca: false,
|
||||
subject_key_identifier: Some(vec![0x01]),
|
||||
subject_info_access: Some(SubjectInfoAccess::Ca(
|
||||
rpki::data_model::rc::SubjectInfoAccessCa {
|
||||
access_descriptions: vec![],
|
||||
},
|
||||
)),
|
||||
certificate_policies_oid: None,
|
||||
ip_resources,
|
||||
as_resources,
|
||||
},
|
||||
},
|
||||
kind: ResourceCertKind::Ee,
|
||||
}
|
||||
}
|
||||
|
||||
fn test_roa_single_v4_prefix() -> RoaEContent {
|
||||
RoaEContent {
|
||||
version: 0,
|
||||
as_id: 64496,
|
||||
ip_addr_blocks: vec![RoaIpAddressFamily {
|
||||
afi: RoaAfi::Ipv4,
|
||||
addresses: vec![RoaIpAddress {
|
||||
prefix: rpki::data_model::roa::IpPrefix {
|
||||
afi: RoaAfi::Ipv4,
|
||||
prefix_len: 8,
|
||||
addr: vec![10, 0, 0, 0],
|
||||
},
|
||||
max_length: Some(24),
|
||||
}],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_accepts_when_prefix_is_covered() {
|
||||
let roa = test_roa_single_v4_prefix();
|
||||
let ee = dummy_ee(
|
||||
Some(IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
||||
IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 0,
|
||||
addr: vec![0, 0, 0, 0],
|
||||
},
|
||||
)]),
|
||||
}],
|
||||
}),
|
||||
None,
|
||||
);
|
||||
roa.validate_against_ee_cert(&ee)
|
||||
.expect("prefix should be covered by 0/0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_when_as_resources_present() {
|
||||
let roa = test_roa_single_v4_prefix();
|
||||
let ee = dummy_ee(
|
||||
Some(IpResourceSet { families: vec![] }),
|
||||
Some(AsResourceSet {
|
||||
asnum: None,
|
||||
rdi: None,
|
||||
}),
|
||||
);
|
||||
let err = roa.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(err, RoaValidateError::EeAsResourcesPresent));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_when_ip_resources_missing() {
|
||||
let roa = test_roa_single_v4_prefix();
|
||||
let ee = dummy_ee(None, None);
|
||||
let err = roa.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(err, RoaValidateError::EeIpResourcesMissing));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_when_ip_resources_inherit() {
|
||||
let roa = test_roa_single_v4_prefix();
|
||||
let ee = dummy_ee(
|
||||
Some(IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::Inherit,
|
||||
}],
|
||||
}),
|
||||
None,
|
||||
);
|
||||
let err = roa.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(err, RoaValidateError::EeIpResourcesInherit));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_when_prefix_not_covered() {
|
||||
let roa = test_roa_single_v4_prefix();
|
||||
let ee = dummy_ee(
|
||||
Some(IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
||||
IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 24,
|
||||
addr: vec![192, 0, 2, 0],
|
||||
},
|
||||
)]),
|
||||
}],
|
||||
}),
|
||||
None,
|
||||
);
|
||||
let err = roa.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(err, RoaValidateError::PrefixNotInEeResources { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contains_prefix_handles_non_octet_boundary_prefix_len() {
|
||||
let roa = RoaEContent {
|
||||
version: 0,
|
||||
as_id: 64496,
|
||||
ip_addr_blocks: vec![RoaIpAddressFamily {
|
||||
afi: RoaAfi::Ipv4,
|
||||
addresses: vec![RoaIpAddress {
|
||||
prefix: rpki::data_model::roa::IpPrefix {
|
||||
afi: RoaAfi::Ipv4,
|
||||
prefix_len: 16,
|
||||
addr: vec![0b1010_0000, 0x12, 0, 0], // 160.18.0.0/16
|
||||
},
|
||||
max_length: None,
|
||||
}],
|
||||
}],
|
||||
};
|
||||
|
||||
let ee = dummy_ee(
|
||||
Some(IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
||||
IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 9,
|
||||
addr: vec![0b1010_0000, 0, 0, 0], // 160.0.0.0/9
|
||||
},
|
||||
)]),
|
||||
}],
|
||||
}),
|
||||
None,
|
||||
);
|
||||
|
||||
roa.validate_against_ee_cert(&ee)
|
||||
.expect("160.18.0.0/16 should be covered by 160.0.0.0/9");
|
||||
}
|
||||
|
||||
14
tests/test_roa_verify.rs
Normal file
14
tests/test_roa_verify.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use rpki::data_model::roa::RoaObject;
|
||||
|
||||
#[test]
|
||||
fn verify_roa_cms_signature_with_embedded_ee_cert() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa",
|
||||
)
|
||||
.expect("read ROA fixture");
|
||||
let roa = RoaObject::decode_der(&der).expect("decode roa");
|
||||
roa.signed_object
|
||||
.verify_signature()
|
||||
.expect("ROA CMS signature should verify with embedded EE cert");
|
||||
}
|
||||
|
||||
30
tests/test_signed_object_decode.rs
Normal file
30
tests/test_signed_object_decode.rs
Normal file
@ -0,0 +1,30 @@
|
||||
use rpki::data_model::oid::{OID_CT_RPKI_MANIFEST, OID_SHA256, OID_SIGNED_DATA};
|
||||
use rpki::data_model::signed_object::RpkiSignedObject;
|
||||
|
||||
#[test]
|
||||
fn decode_manifest_signed_object_smoke() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
)
|
||||
.expect("read MFT fixture");
|
||||
|
||||
let so = RpkiSignedObject::decode_der(&der).expect("decode signed object");
|
||||
|
||||
assert_eq!(so.content_info_content_type, OID_SIGNED_DATA);
|
||||
assert_eq!(so.signed_data.version, 3);
|
||||
assert_eq!(so.signed_data.digest_algorithms, vec![OID_SHA256.to_string()]);
|
||||
assert_eq!(
|
||||
so.signed_data.encap_content_info.econtent_type,
|
||||
OID_CT_RPKI_MANIFEST
|
||||
);
|
||||
assert_eq!(so.signed_data.certificates.len(), 1);
|
||||
assert!(!so.signed_data.certificates[0]
|
||||
.sia_signed_object_uris
|
||||
.is_empty());
|
||||
assert!(so.signed_data.certificates[0]
|
||||
.sia_signed_object_uris
|
||||
.iter()
|
||||
.any(|u| u.starts_with("rsync://")));
|
||||
assert_eq!(so.signed_data.signer_infos.len(), 1);
|
||||
println!("{so:#?}")
|
||||
}
|
||||
1523
tests/test_signed_object_decode_errors.rs
Normal file
1523
tests/test_signed_object_decode_errors.rs
Normal file
File diff suppressed because it is too large
Load Diff
164
tests/test_signed_object_verify.rs
Normal file
164
tests/test_signed_object_verify.rs
Normal file
@ -0,0 +1,164 @@
|
||||
use rpki::data_model::signed_object::{RpkiSignedObject, SignedObjectVerifyError};
|
||||
use x509_parser::prelude::FromDer;
|
||||
use x509_parser::prelude::X509Certificate;
|
||||
|
||||
fn len_bytes(len: usize) -> Vec<u8> {
|
||||
if len < 128 {
|
||||
vec![len as u8]
|
||||
} else {
|
||||
let mut tmp = Vec::new();
|
||||
let mut n = len;
|
||||
while n > 0 {
|
||||
tmp.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
tmp.reverse();
|
||||
let mut out = vec![0x80 | (tmp.len() as u8)];
|
||||
out.extend(tmp);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
fn tlv(tag: u8, content: &[u8]) -> Vec<u8> {
|
||||
let mut out = vec![tag];
|
||||
out.extend(len_bytes(content.len()));
|
||||
out.extend_from_slice(content);
|
||||
out
|
||||
}
|
||||
|
||||
fn der_integer_bytes(bytes: &[u8]) -> Vec<u8> {
|
||||
tlv(0x02, bytes)
|
||||
}
|
||||
|
||||
fn der_integer_u64(v: u64) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
let mut n = v;
|
||||
if n == 0 {
|
||||
bytes.push(0);
|
||||
} else {
|
||||
while n > 0 {
|
||||
bytes.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
bytes.reverse();
|
||||
if bytes[0] & 0x80 != 0 {
|
||||
bytes.insert(0, 0);
|
||||
}
|
||||
}
|
||||
der_integer_bytes(&bytes)
|
||||
}
|
||||
|
||||
fn der_null() -> Vec<u8> {
|
||||
vec![0x05, 0x00]
|
||||
}
|
||||
|
||||
fn der_oid(oid: &str) -> Vec<u8> {
|
||||
use std::str::FromStr;
|
||||
use der_parser::asn1_rs::ToDer;
|
||||
let oid = der_parser::Oid::from_str(oid).unwrap();
|
||||
oid.to_der_vec().unwrap()
|
||||
}
|
||||
|
||||
fn der_sequence(children: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
let mut content = Vec::new();
|
||||
for c in children {
|
||||
content.extend(c);
|
||||
}
|
||||
tlv(0x30, &content)
|
||||
}
|
||||
|
||||
fn der_bit_string(unused: u8, bytes: &[u8]) -> Vec<u8> {
|
||||
let mut content = vec![unused];
|
||||
content.extend_from_slice(bytes);
|
||||
tlv(0x03, &content)
|
||||
}
|
||||
|
||||
fn rsa_spki_der_with_modulus_bytes(modulus: &[u8], exponent: u64) -> Vec<u8> {
|
||||
// SubjectPublicKeyInfo for RSA public key:
|
||||
// SEQUENCE { algorithm AlgorithmIdentifier, subjectPublicKey BIT STRING }
|
||||
// subjectPublicKey contains RSAPublicKey DER.
|
||||
let alg = der_sequence(vec![
|
||||
der_oid(rpki::data_model::oid::OID_RSA_ENCRYPTION),
|
||||
der_null(),
|
||||
]);
|
||||
let rsa_pk = der_sequence(vec![der_integer_bytes(modulus), der_integer_u64(exponent)]);
|
||||
let spk = der_bit_string(0, &rsa_pk);
|
||||
der_sequence(vec![alg, spk])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_mft_cms_signature_with_embedded_ee_cert() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
)
|
||||
.expect("read MFT fixture");
|
||||
let so = RpkiSignedObject::decode_der(&der).expect("decode signed object");
|
||||
so.verify_signature()
|
||||
.expect("CMS signature should verify with embedded EE cert");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_fails_with_wrong_spki() {
|
||||
let mft_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
)
|
||||
.expect("read MFT fixture");
|
||||
let so = RpkiSignedObject::decode_der(&mft_der).expect("decode signed object");
|
||||
|
||||
let issuer_cert_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
)
|
||||
.expect("read issuer certificate fixture");
|
||||
let (_rem, issuer_cert) = X509Certificate::from_der(&issuer_cert_der).expect("parse cert");
|
||||
let wrong_spki_der = issuer_cert.public_key().raw.to_vec();
|
||||
|
||||
let err = so
|
||||
.verify_signature_with_ee_spki_der(&wrong_spki_der)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, SignedObjectVerifyError::InvalidSignature));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_fails_with_tampered_signature() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
)
|
||||
.expect("read MFT fixture");
|
||||
let mut so = RpkiSignedObject::decode_der(&der).expect("decode signed object");
|
||||
so.signed_data.signer_infos[0].signature[0] ^= 0x01;
|
||||
let err = so.verify_signature().unwrap_err();
|
||||
assert!(matches!(err, SignedObjectVerifyError::InvalidSignature));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_rejects_spki_der_with_trailing_bytes() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
)
|
||||
.expect("read MFT fixture");
|
||||
let so = RpkiSignedObject::decode_der(&der).expect("decode signed object");
|
||||
|
||||
let mut spki_der = so.signed_data.certificates[0].spki_der.clone();
|
||||
spki_der.push(0);
|
||||
let err = so
|
||||
.verify_signature_with_ee_spki_der(&spki_der)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, SignedObjectVerifyError::EeSpkiTrailingBytes(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_with_all_zero_modulus_exercises_strip_leading_zeros_fallback() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
)
|
||||
.expect("read MFT fixture");
|
||||
let so = RpkiSignedObject::decode_der(&der).expect("decode signed object");
|
||||
|
||||
// modulus INTEGER 0, exponent 65537 (valid exponent encoding); signature verification must fail
|
||||
// but the SPKI parsing path should succeed.
|
||||
let spki_der = rsa_spki_der_with_modulus_bytes(&[0x00], 65537);
|
||||
let err = so
|
||||
.verify_signature_with_ee_spki_der(&spki_der)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, SignedObjectVerifyError::InvalidSignature));
|
||||
}
|
||||
172
tests/test_ta_certificate.rs
Normal file
172
tests/test_ta_certificate.rs
Normal file
@ -0,0 +1,172 @@
|
||||
use der_parser::num_bigint::BigUint;
|
||||
use rpki::data_model::oid::OID_CP_IPADDR_ASNUMBER;
|
||||
use rpki::data_model::rc::{
|
||||
Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange,
|
||||
IpPrefix, RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate,
|
||||
};
|
||||
use rpki::data_model::ta::{TaCertificate, TaCertificateError};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
fn dummy_rc_ca(ext: RcExtensions) -> ResourceCertificate {
|
||||
let t = OffsetDateTime::from_unix_timestamp(0).unwrap();
|
||||
ResourceCertificate {
|
||||
raw_der: Vec::new(),
|
||||
kind: ResourceCertKind::Ca,
|
||||
tbs: RpkixTbsCertificate {
|
||||
version: 2,
|
||||
serial_number: BigUint::from(1u32),
|
||||
signature_algorithm: "1.2.840.113549.1.1.11".into(),
|
||||
issuer_dn: "CN=TA".into(),
|
||||
subject_dn: "CN=TA".into(),
|
||||
validity_not_before: t,
|
||||
validity_not_after: t,
|
||||
subject_public_key_info: Vec::new(),
|
||||
extensions: ext,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_certificate_from_der_parses_downloaded_fixtures() {
|
||||
let fixtures = [
|
||||
"tests/fixtures/ta/afrinic-ta.cer",
|
||||
"tests/fixtures/ta/apnic-ta.cer",
|
||||
"tests/fixtures/ta/arin-ta.cer",
|
||||
"tests/fixtures/ta/lacnic-ta.cer",
|
||||
"tests/fixtures/ta/ripe-ncc-ta.cer",
|
||||
];
|
||||
for path in fixtures {
|
||||
let der = std::fs::read(path).expect("read TA fixture");
|
||||
let ta = TaCertificate::from_der(&der).expect("parse TA fixture");
|
||||
assert_eq!(ta.rc_ca.kind, ResourceCertKind::Ca);
|
||||
assert_eq!(
|
||||
ta.rc_ca.tbs.extensions.certificate_policies_oid.as_deref(),
|
||||
Some(OID_CP_IPADDR_ASNUMBER)
|
||||
);
|
||||
assert!(ta.rc_ca.tbs.extensions.subject_key_identifier.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_certificate_rejects_non_self_signed_ca() {
|
||||
// This is a CA cert fixture, but not self-signed (issuer != subject).
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
)
|
||||
.expect("read CA cert fixture");
|
||||
assert!(matches!(
|
||||
TaCertificate::from_der(&der),
|
||||
Err(TaCertificateError::NotSelfSignedIssuerSubject)
|
||||
| Err(TaCertificateError::InvalidSelfSignature(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_constraints_require_policies_and_ski() {
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
basic_constraints_ca: true,
|
||||
subject_key_identifier: None,
|
||||
subject_info_access: None,
|
||||
certificate_policies_oid: None,
|
||||
ip_resources: Some(rpki::data_model::rc::IpResourceSet { families: vec![] }),
|
||||
as_resources: None,
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::MissingOrInvalidCertificatePolicies)
|
||||
));
|
||||
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
certificate_policies_oid: Some(OID_CP_IPADDR_ASNUMBER.to_string()),
|
||||
..rc.tbs.extensions.clone()
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::MissingSubjectKeyIdentifier)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_constraints_require_non_empty_resources_and_no_inherit() {
|
||||
// Missing both IP and AS resources.
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
basic_constraints_ca: true,
|
||||
subject_key_identifier: Some(vec![1]),
|
||||
subject_info_access: None,
|
||||
certificate_policies_oid: Some(OID_CP_IPADDR_ASNUMBER.to_string()),
|
||||
ip_resources: None,
|
||||
as_resources: None,
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::ResourcesMissing)
|
||||
));
|
||||
|
||||
// IP resources present but empty => resources empty.
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
ip_resources: Some(rpki::data_model::rc::IpResourceSet { families: vec![] }),
|
||||
..rc.tbs.extensions.clone()
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::ResourcesEmpty)
|
||||
));
|
||||
|
||||
// IP resources inherit is rejected.
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
ip_resources: Some(rpki::data_model::rc::IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::Inherit,
|
||||
}],
|
||||
}),
|
||||
..rc.tbs.extensions.clone()
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::IpResourcesInherit)
|
||||
));
|
||||
|
||||
// AS resources inherit is rejected.
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
ip_resources: None,
|
||||
as_resources: Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::Inherit),
|
||||
rdi: None,
|
||||
}),
|
||||
..rc.tbs.extensions.clone()
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::AsResourcesInherit)
|
||||
));
|
||||
|
||||
// Valid non-empty explicit IP resources => OK.
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
ip_resources: Some(rpki::data_model::rc::IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv6,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(IpPrefix {
|
||||
afi: Afi::Ipv6,
|
||||
prefix_len: 32,
|
||||
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
})]),
|
||||
}],
|
||||
}),
|
||||
as_resources: None,
|
||||
..rc.tbs.extensions.clone()
|
||||
});
|
||||
TaCertificate::validate_rc_constraints(&rc).expect("valid explicit resources");
|
||||
|
||||
// Valid non-empty explicit AS resources => OK.
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
ip_resources: None,
|
||||
as_resources: Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64512)])),
|
||||
rdi: None,
|
||||
}),
|
||||
..rc.tbs.extensions.clone()
|
||||
});
|
||||
TaCertificate::validate_rc_constraints(&rc).expect("valid explicit AS resources");
|
||||
}
|
||||
|
||||
31
tests/test_tal_decode.rs
Normal file
31
tests/test_tal_decode.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use der_parser::der::parse_der;
|
||||
use rpki::data_model::tal::Tal;
|
||||
|
||||
#[test]
|
||||
fn decode_tal_fixtures_smoke() {
|
||||
let fixtures = [
|
||||
"tests/fixtures/tal/afrinic.tal",
|
||||
"tests/fixtures/tal/apnic-rfc7730-https.tal",
|
||||
"tests/fixtures/tal/arin.tal",
|
||||
"tests/fixtures/tal/lacnic.tal",
|
||||
"tests/fixtures/tal/ripe-ncc.tal",
|
||||
];
|
||||
|
||||
for path in fixtures {
|
||||
let raw = std::fs::read(path).expect("read TAL fixture");
|
||||
let tal = Tal::decode_bytes(&raw).expect("decode TAL fixture");
|
||||
|
||||
assert!(!tal.ta_uris.is_empty(), "TA URI list must be non-empty: {path}");
|
||||
assert!(!tal.subject_public_key_info_der.is_empty(), "SPKI DER must be non-empty: {path}");
|
||||
|
||||
for u in &tal.ta_uris {
|
||||
assert!(matches!(u.scheme(), "rsync" | "https"), "scheme must be allowed: {u}");
|
||||
assert!(!u.path().ends_with('/'), "TA URI must not be a directory: {u}");
|
||||
}
|
||||
|
||||
// SPKI DER must be parseable as a DER object (typically a SEQUENCE).
|
||||
let (rem, _obj) = parse_der(&tal.subject_public_key_info_der).expect("parse spki DER");
|
||||
assert!(rem.is_empty(), "SPKI DER must not have trailing bytes: {path}");
|
||||
}
|
||||
}
|
||||
|
||||
71
tests/test_tal_decode_errors.rs
Normal file
71
tests/test_tal_decode_errors.rs
Normal file
@ -0,0 +1,71 @@
|
||||
use rpki::data_model::tal::{Tal, TalDecodeError};
|
||||
|
||||
fn mk_tal(uris: &[&str], b64_lines: &[&str]) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str("# comment\n");
|
||||
for u in uris {
|
||||
out.push_str(u);
|
||||
out.push('\n');
|
||||
}
|
||||
out.push('\n'); // separator
|
||||
for l in b64_lines {
|
||||
out.push_str(l);
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_missing_separator() {
|
||||
let s = "# c\nhttps://example.invalid/ta.cer\nAAAA\n";
|
||||
assert!(matches!(
|
||||
Tal::decode_bytes(s.as_bytes()),
|
||||
Err(TalDecodeError::MissingSeparatorEmptyLine)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_missing_uris() {
|
||||
let s = "# c\n\nAAAA\n";
|
||||
assert!(matches!(Tal::decode_bytes(s.as_bytes()), Err(TalDecodeError::MissingTaUris)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_unsupported_scheme() {
|
||||
let s = mk_tal(&["ftp://example.invalid/ta.cer"], &["AAAA"]);
|
||||
assert!(matches!(
|
||||
Tal::decode_bytes(s.as_bytes()),
|
||||
Err(TalDecodeError::UnsupportedUriScheme(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_directory_uri() {
|
||||
let s = mk_tal(&["https://example.invalid/dir/"], &["AAAA"]);
|
||||
assert!(matches!(
|
||||
Tal::decode_bytes(s.as_bytes()),
|
||||
Err(TalDecodeError::UriIsDirectory(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_comment_after_header() {
|
||||
let s = "# c\nhttps://example.invalid/ta.cer\n# late\n\nAAAA\n";
|
||||
assert!(matches!(Tal::decode_bytes(s.as_bytes()), Err(TalDecodeError::CommentAfterHeader)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_invalid_base64() {
|
||||
let s = mk_tal(&["https://example.invalid/ta.cer"], &["not-base64!!!"]);
|
||||
assert!(matches!(
|
||||
Tal::decode_bytes(s.as_bytes()),
|
||||
Err(TalDecodeError::SpkiBase64Decode)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_invalid_utf8() {
|
||||
let bytes = [0xFFu8, 0xFEu8];
|
||||
assert!(matches!(Tal::decode_bytes(&bytes), Err(TalDecodeError::InvalidUtf8)));
|
||||
}
|
||||
|
||||
62
tests/test_trust_anchor_bind.rs
Normal file
62
tests/test_trust_anchor_bind.rs
Normal file
@ -0,0 +1,62 @@
|
||||
use rpki::data_model::ta::{TrustAnchor, TrustAnchorError};
|
||||
use rpki::data_model::tal::Tal;
|
||||
use url::Url;
|
||||
|
||||
#[test]
|
||||
fn bind_trust_anchor_with_downloaded_fixtures_succeeds() {
|
||||
let cases = [
|
||||
("tests/fixtures/tal/afrinic.tal", "tests/fixtures/ta/afrinic-ta.cer"),
|
||||
(
|
||||
"tests/fixtures/tal/apnic-rfc7730-https.tal",
|
||||
"tests/fixtures/ta/apnic-ta.cer",
|
||||
),
|
||||
("tests/fixtures/tal/arin.tal", "tests/fixtures/ta/arin-ta.cer"),
|
||||
("tests/fixtures/tal/lacnic.tal", "tests/fixtures/ta/lacnic-ta.cer"),
|
||||
("tests/fixtures/tal/ripe-ncc.tal", "tests/fixtures/ta/ripe-ncc-ta.cer"),
|
||||
];
|
||||
|
||||
for (tal_path, ta_path) in cases {
|
||||
let tal_raw = std::fs::read(tal_path).expect("read TAL fixture");
|
||||
let tal = Tal::decode_bytes(&tal_raw).expect("decode TAL fixture");
|
||||
let ta_der = std::fs::read(ta_path).expect("read TA fixture");
|
||||
|
||||
TrustAnchor::bind(tal.clone(), &ta_der, None).expect("bind without resolved uri");
|
||||
|
||||
// Also exercise the resolved-uri-in-TAL check using one URI from the TAL list.
|
||||
let resolved = tal
|
||||
.ta_uris
|
||||
.iter()
|
||||
.find(|u| u.scheme() == "https")
|
||||
.or_else(|| tal.ta_uris.first())
|
||||
.expect("tal has ta uris");
|
||||
let resolved = resolved.clone();
|
||||
TrustAnchor::bind(tal, &ta_der, Some(&resolved)).expect("bind with resolved uri");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_rejects_spki_mismatch() {
|
||||
let tal_raw = std::fs::read("tests/fixtures/tal/ripe-ncc.tal").expect("read TAL fixture");
|
||||
let mut tal = Tal::decode_bytes(&tal_raw).expect("decode TAL fixture");
|
||||
let ta_der = std::fs::read("tests/fixtures/ta/ripe-ncc-ta.cer").expect("read TA fixture");
|
||||
|
||||
// Flip a byte in TAL SPKI to force mismatch.
|
||||
tal.subject_public_key_info_der[0] ^= 0x01;
|
||||
assert!(matches!(
|
||||
TrustAnchor::bind(tal, &ta_der, None),
|
||||
Err(TrustAnchorError::TalSpkiMismatch)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_rejects_resolved_uri_not_listed_in_tal() {
|
||||
let tal_raw = std::fs::read("tests/fixtures/tal/afrinic.tal").expect("read TAL fixture");
|
||||
let tal = Tal::decode_bytes(&tal_raw).expect("decode TAL fixture");
|
||||
let ta_der = std::fs::read("tests/fixtures/ta/afrinic-ta.cer").expect("read TA fixture");
|
||||
|
||||
let bad = Url::parse("https://example.invalid/not-in-tal.cer").unwrap();
|
||||
assert!(matches!(
|
||||
TrustAnchor::bind(tal, &ta_der, Some(&bad)),
|
||||
Err(TrustAnchorError::ResolvedUriNotInTal(_))
|
||||
));
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user