增加rc,完成所有模型的解析,优化error code 的RFC引用
This commit is contained in:
parent
56ae2ca4fc
commit
cc9f3f21de
104
specs/01_tal.md
104
specs/01_tal.md
@ -1,36 +1,94 @@
|
|||||||
# 01. Trust Anchor Locator (TAL)
|
# 01. TAL(Trust Anchor Locator)
|
||||||
|
|
||||||
## 1.1 对象定位
|
## 1.1 对象定位
|
||||||
TAL是一个数据格式/配置文件,目的是告诉RP信任锚的公钥是什么,以及相关对象可以从哪里获取。
|
|
||||||
|
|
||||||
## 1.2 数据格式 (RFC 8630 §2.2)
|
TAL(Trust Anchor Locator)用于向 RP 提供:
|
||||||
TAL是一个配置文件,格式定义如下:
|
|
||||||
```
|
1) 可检索“当前 TA 证书”的一个或多个 URI;以及
|
||||||
The TAL is an ordered sequence of:
|
2) 该 TA 证书的 `subjectPublicKeyInfo`(SPKI)期望值(用于绑定/防替换)。
|
||||||
1. an optional comment section consisting of one or more lines each starting with the "#" character, followed by human-readable informational UTF-8 text, conforming to the restrictions defined
|
|
||||||
in Section 2 of [RFC5198], and ending with a line break,
|
RFC 8630 §2;RFC 8630 §2.3。
|
||||||
2. a URI section that is comprised of one or more ordered lines, each containing a TA URI, and ending with a line break,
|
|
||||||
3. a line break, and
|
## 1.2 原始载体与编码
|
||||||
4. a subjectPublicKeyInfo [RFC5280] in DER format [X.509], encoded in base64 (see Section 4 of [RFC4648]). To avoid long lines,
|
|
||||||
line breaks MAY be inserted into the base64-encoded string.
|
- 载体:文本文件(ASCII/UTF-8 兼容的行文本)。
|
||||||
Note that line breaks in this file can use either "<CRLF>" or "<LF>".
|
- 行结束:允许 `CRLF` 或 `LF`。RFC 8630 §2.2。
|
||||||
|
- 结构:`[可选注释区] + URI 区 + 空行 + Base64(SPKI DER)`。RFC 8630 §2.2。
|
||||||
|
|
||||||
|
### 1.2.1 注释区
|
||||||
|
|
||||||
|
- 一行或多行,以 `#` 开头,后随人类可读 UTF-8 文本。RFC 8630 §2.2。
|
||||||
|
- 注释行文本需符合 RFC 5198 §2 的限制(RFC 8630 §2.2 引用)。
|
||||||
|
|
||||||
|
### 1.2.2 URI 区
|
||||||
|
|
||||||
|
- 一行或多行,每行一个 TA URI,按序排列。RFC 8630 §2.2。
|
||||||
|
- TA URI **MUST** 是 `rsync` 或 `https`。RFC 8630 §2.2。
|
||||||
|
|
||||||
|
### 1.2.3 空行分隔
|
||||||
|
|
||||||
|
- URI 区后必须有一个额外的换行(即空行),用于与 Base64 区分隔。RFC 8630 §2.2(第 3 点)。
|
||||||
|
|
||||||
|
### 1.2.4 SPKI(Base64)
|
||||||
|
|
||||||
|
- `subjectPublicKeyInfo` 以 DER 编码(ASN.1)后,再 Base64 编码表示。RFC 8630 §2.2(第 4 点)。
|
||||||
|
- 为避免长行,Base64 字符串中 **MAY** 插入换行。RFC 8630 §2.2。
|
||||||
|
- SPKI ASN.1 类型来自 X.509 / RFC 5280。RFC 8630 §2.2(第 4 点);RFC 5280 §4.1.2.7。
|
||||||
|
|
||||||
|
#### 1.2.4.1 `SubjectPublicKeyInfo` 的 ASN.1 定义(RFC 5280 §4.1)
|
||||||
|
|
||||||
|
TAL 中携带的是一个 X.509 `SubjectPublicKeyInfo` 的 DER 字节串(再 Base64)。其 ASN.1 定义如下:RFC 5280 §4.1。
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
SubjectPublicKeyInfo ::= SEQUENCE {
|
||||||
|
algorithm AlgorithmIdentifier,
|
||||||
|
subjectPublicKey BIT STRING }
|
||||||
```
|
```
|
||||||
|
|
||||||
## 1.3 抽象数据模型
|
其中 `algorithm`/`subjectPublicKey` 的取值受 RPKI 算法 profile 约束(例如 RSA 2048 + SHA-256 等;SKI/AKI 计算仍用 SHA-1)。RFC 5280 §4.1.2.7;RFC 7935 §2-§3.1;RFC 6487 §4.8.2-§4.8.3。
|
||||||
|
|
||||||
### 1.3.1 TAL
|
## 1.3 解析规则(语义层)
|
||||||
|
|
||||||
|
输入:`TalFileBytes: bytes`。
|
||||||
|
|
||||||
|
解析步骤:
|
||||||
|
|
||||||
|
1) 按 `LF` / `CRLF` 识别行。RFC 8630 §2.2。
|
||||||
|
2) 从文件开头读取所有以 `#` 开头的行,作为 `comments`(保留去掉 `#` 后的 UTF-8 文本或保留原始行均可,但需保持 UTF-8)。RFC 8630 §2.2。
|
||||||
|
3) 继续读取一行或多行非空行,作为 `ta_uris`(保持顺序)。RFC 8630 §2.2(第 2 点)。
|
||||||
|
4) 读取一个空行(必须存在)。RFC 8630 §2.2(第 3 点)。
|
||||||
|
5) 将剩余行拼接为 Base64 文本(移除行分隔),Base64 解码得到 `subject_public_key_info_der`。RFC 8630 §2.2(第 4 点)。
|
||||||
|
6) 可选:将 `subject_public_key_info_der` 解析为 X.509 `SubjectPublicKeyInfo` 结构(用于与 TA 证书比对)。RFC 8630 §2.3;RFC 5280 §4.1.2.7。
|
||||||
|
|
||||||
|
URI 解析与约束:
|
||||||
|
|
||||||
|
- `ta_uris[*]` 的 scheme **MUST** 为 `rsync` 或 `https`。RFC 8630 §2.2。
|
||||||
|
- 每个 `ta_uri` **MUST** 指向“单个对象”,且 **MUST NOT** 指向目录或集合。RFC 8630 §2.3。
|
||||||
|
|
||||||
|
## 1.4 抽象数据模型(接口)
|
||||||
|
|
||||||
|
### 1.4.1 `Tal`
|
||||||
|
|
||||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|----------|-------------|-------------------------|--------------------------------------------|---------------|
|
|---|---|---|---|---|
|
||||||
| uris | Vec<TalUri> | 指向TA的URI列表 | 允许rsync和https协议。 | RFC 8630 §2.1 |
|
| `raw` | `bytes` | TAL 原始文件字节 | 原样保留(可选但建议) | RFC 8630 §2.2 |
|
||||||
| comment | Vec<String> | 注释(可选) | | RFC 8630 §2.2 |
|
| `comments` | `list[Utf8Text]` | 注释行(按出现顺序) | 每行以 `#` 开头;文本为 UTF-8;内容限制见 RFC 5198 §2 | RFC 8630 §2.2 |
|
||||||
| spki_der | Vec<u8> | 原始的subjectPublicKeyInfo | x.509 SubjectPublicKeyInfo DER编码,再base64编码 | RFC 8630 §2.2 |
|
| `ta_uris` | `list[Uri]` | TA 证书位置列表 | 至少 1 个;按序;每个 scheme 必须是 `rsync` 或 `https` | RFC 8630 §2.2 |
|
||||||
|
| `subject_public_key_info_der` | `DerBytes` | TA 证书 SPKI 的期望 DER | Base64 解码所得 DER;Base64 中可有换行 | RFC 8630 §2.2 |
|
||||||
|
|
||||||
|
### 1.4.2 `TaUri`(可选细化)
|
||||||
|
|
||||||
### 1.3.2 TalUri
|
> 若你的实现希望对 URI 做更强类型化,可在 `Tal.ta_uris` 上进一步拆分为 `TaUri` 结构。
|
||||||
|
|
||||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|-------|--------|---------|---------|---------------|
|
|---|---|---|---|---|
|
||||||
| Rsync | String | rsync地址 | | RFC 8630 §2.1 |
|
| `uri` | `Uri` | 完整 URI 文本 | scheme 为 `rsync` 或 `https` | RFC 8630 §2.2 |
|
||||||
| Https | String | https地址 | | RFC 8630 §2.1 |
|
| `scheme` | `enum` | `rsync` / `https` | 从 `uri` 解析 | RFC 8630 §2.2 |
|
||||||
|
|
||||||
|
## 1.5 字段级约束清单(实现对照)
|
||||||
|
|
||||||
|
- TAL 由(可选)注释区 + URI 区 + 空行 + Base64(SPKI DER) 组成。RFC 8630 §2.2。
|
||||||
|
- URI 区至少 1 行,每行一个 TA URI,顺序有意义。RFC 8630 §2.2。
|
||||||
|
- TA URI 仅允许 `rsync` 或 `https`。RFC 8630 §2.2。
|
||||||
|
- Base64 区允许插入换行。RFC 8630 §2.2。
|
||||||
|
- 每个 TA URI 必须引用单个对象,不能指向目录/集合。RFC 8630 §2.3。
|
||||||
|
|||||||
121
specs/02_ta.md
121
specs/02_ta.md
@ -1,121 +0,0 @@
|
|||||||
# 02. Trust Anchor (TA)
|
|
||||||
|
|
||||||
## 2.1 对象定位
|
|
||||||
TA是一个自签名的CA证书。
|
|
||||||
|
|
||||||
## 2.2 原始载体与编码
|
|
||||||
|
|
||||||
- 载体:X.509 certificates.
|
|
||||||
- 编码:DER(遵循 RFC 5280 的 certificate 结构与字段语义,但受限于RFC 8630 §2.3)
|
|
||||||
|
|
||||||
|
|
||||||
## 2.3 抽象数据类型
|
|
||||||
|
|
||||||
### 2.3.1 TA
|
|
||||||
|
|
||||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
|
||||||
|-------------------|------------------|---------------|---------|---------------|
|
|
||||||
| name | String | 标识该TA,如apnic等 | | |
|
|
||||||
| cert_der | Vec<u8> | 原始DER内容 | | |
|
|
||||||
| cert | X509Certificate | 基础X509证书 | | RFC 5280 §4.1 |
|
|
||||||
| resource | ResourceSet | 资源集合 | | |
|
|
||||||
| publication_point | Uri | 获取该TA的URI | | |
|
|
||||||
|
|
||||||
### 2.3.2 ResourceSet
|
|
||||||
资源集合是来自RFC 3779的IP地址块(§2)和AS号段(§3),受约束于RFC 8630 §2.3
|
|
||||||
|
|
||||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
|
||||||
|------|----------------|--------|-------------|---------------------------|
|
|
||||||
| ips | IpResourceSet | IP地址集合 | 不能是inherit | RFC 3779 §2和RFC 8630 §2.3 |
|
|
||||||
| asns | AsnResourceSet | ASN集合 | 不能是inherit | RFC 3779 §3和RFC 8630 §2.3 |
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (### 2.3.3 IpResourceSet)
|
|
||||||
|
|
||||||
[//]: # (包括IPv4和IPv6的前缀表示)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
|
|
||||||
|
|
||||||
[//]: # (|----|------------------------|----------|-------------|--------------|)
|
|
||||||
|
|
||||||
[//]: # (| v4 | PrefixSet<Ipv4Prefix> | IPv4前缀集合 | | RFC 3779 §2 |)
|
|
||||||
|
|
||||||
[//]: # (| v6 | PrefixSet<Ipv6Prefix> | IPv6前缀集合 | | RFC 3779 §2 |)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (### 2.3.4 AsnResourceSet)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
|
|
||||||
|
|
||||||
[//]: # (|-------|--------------------|-------|-------------|-------------|)
|
|
||||||
|
|
||||||
[//]: # (| range | RangeSet<AsnBlock> | ASN集合 | | RFC 3779 §3 |)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (### 2.3.5 Ipv4Prefix)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
|
|
||||||
|
|
||||||
[//]: # (|------|-----|-----|---------|-------------|)
|
|
||||||
|
|
||||||
[//]: # (| addr | u32 | 地址 | | RFC 3779 §2 |)
|
|
||||||
|
|
||||||
[//]: # (| len | u8 | 长度 | 0-32 | RFC 3779 §2 |)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (### 2.3.6 Ipv6Prefix)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
|
|
||||||
|
|
||||||
[//]: # (|------|------|-----|---------|-------------|)
|
|
||||||
|
|
||||||
[//]: # (| addr | u128 | 地址 | | RFC 3779 §2 |)
|
|
||||||
|
|
||||||
[//]: # (| len | u8 | 长度 | 0-128 | RFC 3779 §2 |)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (### 2.3.7 AsnBlock)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
|
|
||||||
|
|
||||||
[//]: # (|----------|----------|-------|---------|--------------|)
|
|
||||||
|
|
||||||
[//]: # (| asn | Asn | ASN | | RFC 3779 §3 |)
|
|
||||||
|
|
||||||
[//]: # (| asnRange | AsnRange | ASN范围 | | RFC 3779 §3 |)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (### 2.3.8 Asn)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
|
|
||||||
|
|
||||||
[//]: # (|-----|-----|-----|---------|-------------|)
|
|
||||||
|
|
||||||
[//]: # (| asn | u32 | ASN | | RFC 3779 §3 |)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (### 2.3.8 AsnRange)
|
|
||||||
|
|
||||||
[//]: # ()
|
|
||||||
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
|
|
||||||
|
|
||||||
[//]: # (|-----|-----|-------|---------|--------------|)
|
|
||||||
|
|
||||||
[//]: # (| min | Asn | 最小ASN | | RFC 3779 §3 |)
|
|
||||||
|
|
||||||
[//]: # (| max | Asn | 最大ASN | | RFC 3779 §3 |)
|
|
||||||
|
|
||||||
# 2.4 TA校验流程(RFC 8630 §3)
|
|
||||||
1. 从TAL的URI列表中获取证书对象。(顺序访问,若前面失效,再访问后面的)
|
|
||||||
2. 验证证书格式,必须是当前、有效的自签名RPKI证书。
|
|
||||||
3. 验证公钥匹配。TAL中的SubjectPublicKeyInfo与下载证书的公钥一致。
|
|
||||||
4. 其他检查。
|
|
||||||
5. 更新本地存储库缓存。
|
|
||||||
88
specs/02_ta_certificate.md
Normal file
88
specs/02_ta_certificate.md
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# 02. TA(Trust Anchor)自签名证书
|
||||||
|
|
||||||
|
## 2.1 对象定位
|
||||||
|
|
||||||
|
在 RP 侧,“信任锚(Trust Anchor, TA)”以一个**自签名 CA 资源证书**体现,其可获取位置与期望公钥由 TAL 提供。RFC 8630 §2.3。
|
||||||
|
|
||||||
|
本文件描述两个紧密相关的数据对象:
|
||||||
|
|
||||||
|
1) `TaCertificate`:TA 自签名资源证书本体(X.509 DER)
|
||||||
|
2) `TrustAnchor`:语义组合对象(`TAL` + `TaCertificate` 的绑定语义)
|
||||||
|
|
||||||
|
## 2.2 原始载体与编码
|
||||||
|
|
||||||
|
- 载体:X.509 证书(通常以 `.cer` 存放于仓库,但文件扩展名不作为语义依据)。
|
||||||
|
- 编码:DER。TA 证书必须符合 RPKI 资源证书 profile。RFC 8630 §2.3;RFC 6487 §4。
|
||||||
|
|
||||||
|
### 2.2.1 X.509 Certificate 的 ASN.1 定义(RFC 5280 §4.1;TA 与 RC 共享)
|
||||||
|
|
||||||
|
TA 证书与普通资源证书(RC)在编码层面都是 X.509 `Certificate`(DER)。其 ASN.1 定义如下:RFC 5280 §4.1。
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
Certificate ::= SEQUENCE {
|
||||||
|
tbsCertificate TBSCertificate,
|
||||||
|
signatureAlgorithm AlgorithmIdentifier,
|
||||||
|
signatureValue BIT STRING }
|
||||||
|
```
|
||||||
|
|
||||||
|
其中 `tbsCertificate.extensions`(v3 扩展)是 RPKI 语义的主要承载处(IP/AS 资源扩展、SIA/AIA/CRLDP 等)。RFC 5280 §4.1;RPKI 对字段/扩展存在性与关键性约束见 RFC 6487 §4。
|
||||||
|
|
||||||
|
> 说明:更完整的 RC 编码层结构(包括 Extension 外层“extnValue 二次 DER 解码”的套娃方式)在 `03_resource_certificate_rc.md` 与 `00_common_types.md` 中给出。
|
||||||
|
|
||||||
|
## 2.3 TA 证书的 RPKI 语义约束(在 RC profile 基础上额外强调)
|
||||||
|
|
||||||
|
### 2.3.1 自签名与 profile
|
||||||
|
|
||||||
|
- TA URI 指向的对象 **MUST** 是一个**自签名 CA 证书**,并且 **MUST** 符合 RPKI 证书 profile。RFC 8630 §2.3;RFC 6487 §4。
|
||||||
|
- 自签名证书在 RC profile 下的通用差异(例如 CRLDP/AIA 的省略规则、AKI 的规则)见 RFC 6487。RFC 6487 §4.8.3;RFC 6487 §4.8.6;RFC 6487 §4.8.7。
|
||||||
|
|
||||||
|
### 2.3.2 INR(IP/AS 资源扩展)在 TA 上的额外约束
|
||||||
|
|
||||||
|
- TA 的 INR 扩展(IP/AS 资源扩展,RFC 3779)**MUST** 是非空资源集合。RFC 8630 §2.3。
|
||||||
|
- TA 的 INR 扩展 **MUST NOT** 使用 `inherit` 形式。RFC 8630 §2.3。
|
||||||
|
- 说明:一般 RC profile 允许 `inherit`。RFC 6487 §4.8.10;RFC 6487 §4.8.11;RFC 3779 §2.2.3.5;RFC 3779 §3.2.3.3。
|
||||||
|
|
||||||
|
### 2.3.3 TAL ↔ TA 公钥绑定
|
||||||
|
|
||||||
|
- 用于验证 TA 的公钥(来自 TAL 中的 SPKI)**MUST** 与 TA 证书中的 `subjectPublicKeyInfo` 相同。RFC 8630 §2.3。
|
||||||
|
|
||||||
|
### 2.3.4 TA 稳定性语义(实现需建模为“约束/假设”,但不属于验证结果态)
|
||||||
|
|
||||||
|
- TA 公钥与 TAL 中公钥必须保持稳定(用于 RP 侧长期信任锚)。RFC 8630 §2.3。
|
||||||
|
|
||||||
|
### 2.3.5 TA 与 CRL/Manifest 的关系(语义)
|
||||||
|
|
||||||
|
- RFC 8630 指出:TA 为自签名证书,没有对应 CRL,且不会被 manifest 列出;TA 的获取/轮换由 TAL 控制。RFC 8630 §2.3。
|
||||||
|
|
||||||
|
> 注:这条更偏“发布/运维语义”,但对数据对象建模有影响:`TrustAnchor` 组合对象不应依赖 CRL/MFT 的存在。
|
||||||
|
|
||||||
|
## 2.4 抽象数据模型(接口)
|
||||||
|
|
||||||
|
### 2.4.1 `TaCertificate`
|
||||||
|
|
||||||
|
> 该对象在字段层面复用 `RC(CA)` 的语义模型(见 `03_resource_certificate_rc.md`),但增加 TA 特有约束。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `raw_der` | `DerBytes` | TA 证书 DER | X.509 DER;证书 profile 约束见 RC 文档 | RFC 8630 §2.3;RFC 6487 §4 |
|
||||||
|
| `rc_ca` | `ResourceCaCertificate` | 以 RC(CA) 语义解析出的字段集合 | 必须满足“自签名 CA”分支约束;且 INR 必须非空且不允许 inherit | RFC 8630 §2.3;RFC 6487 §4;RFC 3779 §2/§3 |
|
||||||
|
|
||||||
|
### 2.4.2 `TrustAnchor`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `tal` | `Tal` | TAL 文件语义对象 | 见 `01_tal.md` | RFC 8630 §2.2 |
|
||||||
|
| `ta_certificate` | `TaCertificate` | TA 证书语义对象 | TA URI 指向的对象 | RFC 8630 §2.3 |
|
||||||
|
| `tal_spki_der` | `DerBytes` | 从 TAL 解析出的 SPKI DER | `tal.subject_public_key_info_der` | RFC 8630 §2.2 |
|
||||||
|
| `ta_spki_der` | `DerBytes` | 从 TA 证书抽取的 SPKI DER | `ta_certificate` 的 `subjectPublicKeyInfo` | RFC 8630 §2.3;RFC 5280 §4.1.2.7 |
|
||||||
|
|
||||||
|
**绑定约束(字段级)**
|
||||||
|
|
||||||
|
- `tal_spki_der` 必须与 `ta_spki_der` 完全相等(字节层面的 DER 等价)。RFC 8630 §2.3。
|
||||||
|
|
||||||
|
## 2.5 字段级约束清单(实现对照)
|
||||||
|
|
||||||
|
- TA URI 指向的对象必须是自签名 CA 证书,且符合 RPKI 证书 profile。RFC 8630 §2.3;RFC 6487 §4。
|
||||||
|
- TA 的 INR 扩展必须非空,且不得使用 inherit。RFC 8630 §2.3。
|
||||||
|
- TAL 中 SPKI 必须与 TA 证书的 `subjectPublicKeyInfo` 匹配。RFC 8630 §2.3。
|
||||||
|
- TA 不依赖 CRL/MFT(无对应 CRL,且不被 manifest 列出)。RFC 8630 §2.3。
|
||||||
314
specs/03_rc.md
314
specs/03_rc.md
@ -1,314 +0,0 @@
|
|||||||
# 03. RC (Resource Certifications)
|
|
||||||
|
|
||||||
## 3.1 对象定位
|
|
||||||
RC是资源证书,包括CA和EE
|
|
||||||
|
|
||||||
## 3.2 原始载体与编码
|
|
||||||
|
|
||||||
- 载体:X.509 certificates.
|
|
||||||
- 编码:DER(遵循 RFC 5280 的 Certificate 结构与字段语义,但受 RPKI profile 限制)RFC 6487 §4
|
|
||||||
|
|
||||||
### 3.2.1 基本语法(RFC 5280 §4,RFC 6487 )
|
|
||||||
|
|
||||||
RC是遵循RFC5280定义的X.509Certificate语法(RFC 5280 §4),并且符合RFC 6487 §4的约束。只选取RFC 6487 §4章节列出来的字段。(Unless specifically noted as being OPTIONAL, all the fields listed
|
|
||||||
here MUST be present, and any other fields MUST NOT appear in a
|
|
||||||
conforming resource certificate.)
|
|
||||||
|
|
||||||
```
|
|
||||||
Certificate ::= SEQUENCE {
|
|
||||||
tbsCertificate TBSCertificate,
|
|
||||||
signatureAlgorithm AlgorithmIdentifier,
|
|
||||||
signatureValue BIT STRING
|
|
||||||
}
|
|
||||||
|
|
||||||
TBSCertificate ::= SEQUENCE {
|
|
||||||
version [0] EXPLICIT Version MUST be v3,
|
|
||||||
serialNumber CertificateSerialNumber,
|
|
||||||
signature AlgorithmIdentifier,
|
|
||||||
issuer Name,
|
|
||||||
subject Name,
|
|
||||||
validity Validity,
|
|
||||||
subjectPublicKeyInfo SubjectPublicKeyInfo,
|
|
||||||
extensions [3] EXPLICIT Extensions OPTIONAL
|
|
||||||
-- If present, version MUST be v3
|
|
||||||
}
|
|
||||||
|
|
||||||
Version ::= INTEGER { v1(0), v2(1), v3(2) }
|
|
||||||
|
|
||||||
CertificateSerialNumber ::= INTEGER
|
|
||||||
|
|
||||||
Validity ::= SEQUENCE {
|
|
||||||
notBefore Time,
|
|
||||||
notAfter Time }
|
|
||||||
|
|
||||||
Time ::= CHOICE {
|
|
||||||
utcTime UTCTime,
|
|
||||||
generalTime GeneralizedTime }
|
|
||||||
|
|
||||||
UniqueIdentifier ::= BIT STRING
|
|
||||||
|
|
||||||
SubjectPublicKeyInfo ::= SEQUENCE {
|
|
||||||
algorithm AlgorithmIdentifier,
|
|
||||||
subjectPublicKey BIT STRING }
|
|
||||||
|
|
||||||
Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension
|
|
||||||
|
|
||||||
Extension ::= SEQUENCE {
|
|
||||||
extnID OBJECT IDENTIFIER,
|
|
||||||
critical BOOLEAN DEFAULT FALSE,
|
|
||||||
extnValue OCTET STRING
|
|
||||||
-- contains the DER encoding of an ASN.1 value
|
|
||||||
-- corresponding to the extension type identified
|
|
||||||
-- by extnID
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> 其中`Name` "a valid X.501 distinguished name"(RFC 6487 §4.4)
|
|
||||||
|
|
||||||
### 3.2.2 证书扩展字段 (RFC 6487 §4.8)
|
|
||||||
|
|
||||||
RC的证书扩展字段按照RFC 6487 §4.8的规定,有以下几个扩展:
|
|
||||||
|
|
||||||
- Basic Constraints
|
|
||||||
- Subject Key Identifier
|
|
||||||
- Authority Key Identifier
|
|
||||||
- Key Usage
|
|
||||||
- Extended Key Usage(CA证书,以及验证RPKI对象的EE证书不能出现该字段。非RPKI对象的EE可以出现EKU,但必须为non-critical)
|
|
||||||
- CRL Distribution Points
|
|
||||||
- Authority Information Access
|
|
||||||
- Subject Information Access
|
|
||||||
- SIA for CA Certificates
|
|
||||||
- SIA for EE Certificates
|
|
||||||
- Certificate Policies
|
|
||||||
- IP Resources
|
|
||||||
- AS Resources
|
|
||||||
|
|
||||||
```
|
|
||||||
# Basic Constraints
|
|
||||||
id-ce-basicConstraints OBJECT IDENTIFIER ::= { id-ce 19 }
|
|
||||||
|
|
||||||
BasicConstraints ::= SEQUENCE {
|
|
||||||
cA BOOLEAN DEFAULT FALSE }
|
|
||||||
|
|
||||||
|
|
||||||
# Subject Key Identifier
|
|
||||||
id-ce-subjectKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 14 }
|
|
||||||
|
|
||||||
SubjectKeyIdentifier ::= KeyIdentifier
|
|
||||||
|
|
||||||
KeyIdentifier ::= OCTET STRING
|
|
||||||
|
|
||||||
|
|
||||||
# Authority Key Identifier
|
|
||||||
id-ce-authorityKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 35 }
|
|
||||||
|
|
||||||
AuthorityKeyIdentifier ::= SEQUENCE {
|
|
||||||
keyIdentifier [0] KeyIdentifier OPTIONAL }
|
|
||||||
|
|
||||||
|
|
||||||
# Key Usage
|
|
||||||
id-ce-keyUsage OBJECT IDENTIFIER ::= { id-ce 15 }
|
|
||||||
|
|
||||||
KeyUsage ::= BIT STRING {
|
|
||||||
digitalSignature (0),
|
|
||||||
nonRepudiation (1), -- recent editions of X.509 have
|
|
||||||
-- renamed this bit to contentCommitment
|
|
||||||
keyEncipherment (2),
|
|
||||||
dataEncipherment (3),
|
|
||||||
keyAgreement (4),
|
|
||||||
keyCertSign (5),
|
|
||||||
cRLSign (6),
|
|
||||||
encipherOnly (7),
|
|
||||||
decipherOnly (8) }
|
|
||||||
|
|
||||||
|
|
||||||
# Extended Key Usage
|
|
||||||
id-ce-extKeyUsage OBJECT IDENTIFIER ::= { id-ce 37 }
|
|
||||||
|
|
||||||
ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId
|
|
||||||
|
|
||||||
KeyPurposeId ::= OBJECT IDENTIFIER
|
|
||||||
|
|
||||||
|
|
||||||
# CRL Distribution Points
|
|
||||||
id-ce-cRLDistributionPoints OBJECT IDENTIFIER ::= { id-ce 31 }
|
|
||||||
|
|
||||||
CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint
|
|
||||||
|
|
||||||
DistributionPoint ::= SEQUENCE {
|
|
||||||
distributionPoint [0] DistributionPointName OPTIONAL }
|
|
||||||
|
|
||||||
DistributionPointName ::= CHOICE {
|
|
||||||
fullName [0] GeneralNames }
|
|
||||||
|
|
||||||
|
|
||||||
## Authority Information Access
|
|
||||||
id-pe-authorityInfoAccess OBJECT IDENTIFIER ::= { id-pe 1 }
|
|
||||||
|
|
||||||
AuthorityInfoAccessSyntax ::=
|
|
||||||
SEQUENCE SIZE (1..MAX) OF AccessDescription
|
|
||||||
|
|
||||||
AccessDescription ::= SEQUENCE {
|
|
||||||
accessMethod OBJECT IDENTIFIER,
|
|
||||||
accessLocation GeneralName }
|
|
||||||
|
|
||||||
# AccessDescription
|
|
||||||
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
|
|
||||||
# CA 证书发布位置
|
|
||||||
id-ad-caIssuers OBJECT IDENTIFIER ::= { id-ad 2 }
|
|
||||||
# OCSP 服务地址
|
|
||||||
id-ad-ocsp OBJECT IDENTIFIER ::= { id-ad 1 }
|
|
||||||
|
|
||||||
|
|
||||||
# Subject Information Access
|
|
||||||
id-pe-subjectInfoAccess OBJECT IDENTIFIER ::= { id-pe 11 }
|
|
||||||
|
|
||||||
SubjectInfoAccessSyntax ::= SEQUENCE SIZE (1..MAX) OF AccessDescription
|
|
||||||
AccessDescription ::= SEQUENCE {
|
|
||||||
accessMethod OBJECT IDENTIFIER,
|
|
||||||
accessLocation GeneralName }
|
|
||||||
|
|
||||||
## Subject Information Access for CA (RFC 6487 §4.8.8.1)
|
|
||||||
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
|
|
||||||
id-ad-rpkiManifest OBJECT IDENTIFIER ::= { id-ad 10 }
|
|
||||||
|
|
||||||
必须存在一个accessMethod=id-ad-caRepository,accessLocation=rsyncURI。
|
|
||||||
必须存在一个accessMethod=id-ad-repiManifest, accessLocation=rsync URI,指向该CA的mft对象。
|
|
||||||
|
|
||||||
## Subject Information Access for EE (RFC 6487 §4.8.8.2)
|
|
||||||
id-ad-signedObject OBJECT IDENTIFIER ::= { id-ad 11 }
|
|
||||||
|
|
||||||
必须存在一个accessMethod=id-ad-signedObject, accessLocation=rsyncURI
|
|
||||||
不允许其他的accessMethod
|
|
||||||
|
|
||||||
|
|
||||||
# Certificate Policies
|
|
||||||
id-ce-certificatePolicies OBJECT IDENTIFIER ::= { id-ce 32 }
|
|
||||||
anyPolicy OBJECT IDENTIFIER ::= { id-ce-certificatePolicies 0 }
|
|
||||||
|
|
||||||
certificatePolicies ::= SEQUENCE SIZE (1..MAX) OF PolicyInformation
|
|
||||||
|
|
||||||
PolicyInformation ::= SEQUENCE {
|
|
||||||
policyIdentifier CertPolicyId,
|
|
||||||
policyQualifiers SEQUENCE SIZE (1..MAX) OF PolicyQualifierInfo OPTIONAL }
|
|
||||||
|
|
||||||
CertPolicyId ::= OBJECT IDENTIFIER
|
|
||||||
|
|
||||||
PolicyQualifierInfo ::= SEQUENCE {
|
|
||||||
policyQualifierId PolicyQualifierId,
|
|
||||||
qualifier ANY DEFINED BY policyQualifierId }
|
|
||||||
|
|
||||||
-- policyQualifierIds for Internet policy qualifiers
|
|
||||||
id-qt OBJECT IDENTIFIER ::= { id-pkix 2 }
|
|
||||||
id-qt-cps OBJECT IDENTIFIER ::= { id-qt 1 }
|
|
||||||
id-qt-unotice OBJECT IDENTIFIER ::= { id-qt 2 }
|
|
||||||
|
|
||||||
PolicyQualifierId ::= OBJECT IDENTIFIER ( id-qt-cps | id-qt-unotice )
|
|
||||||
|
|
||||||
Qualifier ::= CHOICE {
|
|
||||||
cPSuri CPSuri,
|
|
||||||
userNotice UserNotice }
|
|
||||||
|
|
||||||
CPSuri ::= IA5String
|
|
||||||
|
|
||||||
UserNotice ::= SEQUENCE {
|
|
||||||
noticeRef NoticeReference OPTIONAL,
|
|
||||||
explicitText DisplayText OPTIONAL }
|
|
||||||
|
|
||||||
NoticeReference ::= SEQUENCE {
|
|
||||||
organization DisplayText,
|
|
||||||
noticeNumbers SEQUENCE OF INTEGER }
|
|
||||||
|
|
||||||
DisplayText ::= CHOICE {
|
|
||||||
ia5String IA5String (SIZE (1..200)),
|
|
||||||
visibleString VisibleString (SIZE (1..200)),
|
|
||||||
bmpString BMPString (SIZE (1..200)),
|
|
||||||
utf8String UTF8String (SIZE (1..200)) }
|
|
||||||
|
|
||||||
|
|
||||||
# IP Resources
|
|
||||||
id-pe-ipAddrBlocks OBJECT IDENTIFIER ::= { id-pe 7 }
|
|
||||||
|
|
||||||
IPAddrBlocks ::= SEQUENCE OF IPAddressFamily
|
|
||||||
|
|
||||||
IPAddressFamily ::= SEQUENCE { -- AFI & optional SAFI --
|
|
||||||
addressFamily OCTET STRING (SIZE (2..3)),
|
|
||||||
ipAddressChoice IPAddressChoice }
|
|
||||||
|
|
||||||
IPAddressChoice ::= CHOICE {
|
|
||||||
inherit NULL, -- inherit from issuer --
|
|
||||||
addressesOrRanges SEQUENCE OF IPAddressOrRange }
|
|
||||||
|
|
||||||
IPAddressOrRange ::= CHOICE {
|
|
||||||
addressPrefix IPAddress,
|
|
||||||
addressRange IPAddressRange }
|
|
||||||
|
|
||||||
IPAddressRange ::= SEQUENCE {
|
|
||||||
min IPAddress,
|
|
||||||
max IPAddress }
|
|
||||||
|
|
||||||
IPAddress ::= BIT STRING
|
|
||||||
|
|
||||||
|
|
||||||
# AS Resources
|
|
||||||
id-pe-autonomousSysIds OBJECT IDENTIFIER ::= { id-pe 8 }
|
|
||||||
ASIdentifiers ::= SEQUENCE {
|
|
||||||
asnum [0] EXPLICIT ASIdentifierChoice OPTIONAL,
|
|
||||||
rdi [1] EXPLICIT ASIdentifierChoice OPTIONAL}
|
|
||||||
|
|
||||||
ASIdentifierChoice ::= CHOICE {
|
|
||||||
inherit NULL, -- inherit from issuer --
|
|
||||||
asIdsOrRanges SEQUENCE OF ASIdOrRange }
|
|
||||||
|
|
||||||
ASIdOrRange ::= CHOICE {
|
|
||||||
id ASId,
|
|
||||||
range ASRange }
|
|
||||||
|
|
||||||
ASRange ::= SEQUENCE {
|
|
||||||
min ASId,
|
|
||||||
max ASId }
|
|
||||||
|
|
||||||
ASId ::= INTEGER
|
|
||||||
```
|
|
||||||
|
|
||||||
# 3.3 抽象数据结构
|
|
||||||
采用X509 Certificate + Resource + 约束校验的方式组合
|
|
||||||
|
|
||||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
|
||||||
|----------|---------------------|----------|---------|---------------|
|
|
||||||
| cert_der | Vec<u8> | 证书原始数据 | | |
|
|
||||||
| cert | X509Certificate | 基础X509证书 | | RFC 5280 §4.1 |
|
|
||||||
| resource | ResourceSet | 资源集合 | | |
|
|
||||||
|
|
||||||
|
|
||||||
# 3.4 约束规则
|
|
||||||
|
|
||||||
## 3.4.1 Cert约束校验规则
|
|
||||||
RFC 6487中规定的证书的字段参见[3.2.1 ](#321-基本语法rfc-5280-4rfc-6487-)
|
|
||||||
-
|
|
||||||
|
|
||||||
| 字段 | 语义 | 约束/解析规则 | RFC 引用 |
|
|
||||||
|-----------|-------|----------------------------------------------|--------------|
|
|
||||||
| version | 证书版本 | 必须是v3(值为2) | RFC6487 §4.1 |
|
|
||||||
| serial | 证书编号 | 同一个CA签发的证书编号必须唯一 | RFC6487 §4.2 |
|
|
||||||
| validity | 证书有效期 | notBefore:时间不能早于证书的生成时间。若时间段大于上级证书的有效期,也是有效的 | RFC6487 §4.6 |
|
|
||||||
|
|
||||||
|
|
||||||
## 3.4.2 Cert Extentions中字段的约束校验规则
|
|
||||||
RFC 6487中规定的扩展字段参见[3.2.2 ](#322-证书扩展字段-rfc-6487-48)
|
|
||||||
|
|
||||||
| 字段 | critical | 语义 | 约束/解析规则 | RFC 引用 |
|
|
||||||
|----------------------------|----------|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|
|
|
||||||
| basicConstraints | Y | 证书类型 | CA证书:cA=TRUE; EE证书:cA=FALSE | RFC6487 §4.8.1 |
|
|
||||||
| subjectKeyIdentifier | N | 证书公钥 | SKI = SHA-1(DER-encoded SPKI bit string) | RFC6487 §4.8.2 |
|
|
||||||
| authorityKeyIdentifier | N | 父证书的公钥 | 字段只包含keyIdentifier,不能包含authorityCertIssuer和authorityCertSerialNumber;除了自签名CA外,其余证书必须出现。自签名CA若出现该字段,则等于SKI | RFC6487 §4.8.3 |
|
|
||||||
| keyUsage | Y | 证书公钥的用途权限 | CA证书:keyCertSign = TRUE, cRLSign = TRUE 其他都是FALSE。EE证书:digitalSignature = TRUE 其他都是FALSE | RFC6487 §4.8.4 |
|
|
||||||
| extendedKeyUsage | N | 扩展证书公钥的用途权限 | CA证书:不能出现EKU;验证 RPKI 对象的 EE 证书:不能出现EKU;非 RPKI 对象的 EE:可以出现EKU,但必须为non-critical. | RFC6487 §4.8.5 |
|
|
||||||
| cRLDistributionPoints | N | CRL的发布点位置 | 字段:distributionPoint,不能包含reasons、cRLIssuer。其中distributionPoint字段包含:fullName,不能包含nameRelativeToCRLIssuer。fullName的格式必须是URI。自签名证书禁止出现该字段。非自签名证书必须出现。一个CA只能有一个CRL。一个CRLDP只能包含一个distributionPoint。但一个distributionPoint字段中可以包含多于1个的URI,但必须包含rsync URI且必须是最新的。 | RFC6487 §4.8.6 |
|
|
||||||
| authorityInformationAccess | N | 签发者的发布点位置 | 除了自签名的CA,必须出现。自签名CA,禁止出现。推荐的URI访问方式是rsync,并且rsyncURI的话,必须指定accessMethod=id-ad-caIssuers | RFC6487 §4.8.7 |
|
|
||||||
| subjectInformationAccess | N | 发布点位置 | CA证书:必须存在。必须存在一个accessMethod=id-ad-caRepository,accessLocation=rsyncURI。必须存在一个accessMethod=id-ad-repiManifest,accessLocation=rsync URI,指向该CA的mft对象。 EE证书:必须存在。必须存在一个accessMethod=id-ad-signedObject,accessLocation=rsyncURI。不允许其他的accessMethod | RFC6487 §4.8.8 |
|
|
||||||
| certificatePolicies | Y | 证书策略 | 必须存在,并且只能存在一种策略:RFC 6484 — RPKI Certificate Policy (CP) | RFC6487 §4.8.9 |
|
|
||||||
| iPResources | Y | IP地址集合 | 所有的RPKI证书中必须包含IP Resources或者ASResources,或者两者都包含。 | RFC6487 §4.8.10 |
|
|
||||||
| aSResources | Y | ASN集合 | 所有的RPKI证书中必须包含IP Resources或者ASResources,或者两者都包含。 | RFC6487 §4.8.11 |
|
|
||||||
|
|
||||||
|
|
||||||
460
specs/03_resource_certificate_rc.md
Normal file
460
specs/03_resource_certificate_rc.md
Normal file
@ -0,0 +1,460 @@
|
|||||||
|
# 03. RC(Resource Certificate:资源证书,CA/EE)
|
||||||
|
|
||||||
|
## 3.1 对象定位
|
||||||
|
|
||||||
|
资源证书(RC)是 X.509 v3 证书,遵循 PKIX profile(RFC 5280),并受 RPKI profile 进一步约束。RFC 6487 §4。
|
||||||
|
|
||||||
|
RC 在 RPKI 中至少分为两类语义用途:
|
||||||
|
|
||||||
|
- `CA 证书`:签发下级证书/CRL,并在 SIA 中声明发布点与 manifest。RFC 6487 §4.8.8.1。
|
||||||
|
- `EE 证书`:用于验证某个 RPKI Signed Object(如 ROA/MFT),在 SIA 中指向被验证对象。RFC 6487 §4.8.8.2。
|
||||||
|
|
||||||
|
## 3.2 原始载体与编码
|
||||||
|
|
||||||
|
- 载体:X.509 证书。
|
||||||
|
- 编码:DER。RFC 6487 §4(“valid X.509 public key certificate consistent with RFC 5280” + RPKI 限制)。
|
||||||
|
|
||||||
|
### 3.2.1 X.509 v3 证书基本语法(ASN.1;RFC 5280 §4.1)
|
||||||
|
|
||||||
|
资源证书在编码层面是 RFC 5280 定义的 X.509 v3 `Certificate`(DER),其中 `tbsCertificate` 携带主体字段与扩展集合(`Extensions`)。RFC 5280 §4.1。
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
Certificate ::= SEQUENCE {
|
||||||
|
tbsCertificate TBSCertificate,
|
||||||
|
signatureAlgorithm AlgorithmIdentifier,
|
||||||
|
signatureValue BIT STRING }
|
||||||
|
|
||||||
|
TBSCertificate ::= SEQUENCE {
|
||||||
|
version [0] EXPLICIT Version DEFAULT v1,
|
||||||
|
serialNumber CertificateSerialNumber,
|
||||||
|
signature AlgorithmIdentifier,
|
||||||
|
issuer Name,
|
||||||
|
validity Validity,
|
||||||
|
subject Name,
|
||||||
|
subjectPublicKeyInfo SubjectPublicKeyInfo,
|
||||||
|
issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
|
||||||
|
-- If present, version MUST be v2 or v3
|
||||||
|
subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
|
||||||
|
-- If present, version MUST be v2 or v3
|
||||||
|
extensions [3] EXPLICIT Extensions OPTIONAL
|
||||||
|
-- If present, version MUST be v3
|
||||||
|
}
|
||||||
|
|
||||||
|
Version ::= INTEGER { v1(0), v2(1), v3(2) }
|
||||||
|
|
||||||
|
CertificateSerialNumber ::= INTEGER
|
||||||
|
|
||||||
|
Validity ::= SEQUENCE {
|
||||||
|
notBefore Time,
|
||||||
|
notAfter Time }
|
||||||
|
|
||||||
|
Time ::= CHOICE {
|
||||||
|
utcTime UTCTime,
|
||||||
|
generalTime GeneralizedTime }
|
||||||
|
|
||||||
|
UniqueIdentifier ::= BIT STRING
|
||||||
|
|
||||||
|
SubjectPublicKeyInfo ::= SEQUENCE {
|
||||||
|
algorithm AlgorithmIdentifier,
|
||||||
|
subjectPublicKey BIT STRING }
|
||||||
|
|
||||||
|
Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension
|
||||||
|
|
||||||
|
Extension ::= SEQUENCE {
|
||||||
|
extnID OBJECT IDENTIFIER,
|
||||||
|
critical BOOLEAN DEFAULT FALSE,
|
||||||
|
extnValue OCTET STRING
|
||||||
|
-- contains the DER encoding of an ASN.1 value
|
||||||
|
-- corresponding to the extension type identified
|
||||||
|
-- by extnID
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2.2 AlgorithmIdentifier(ASN.1;RFC 5280 §4.1.1.2)
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
AlgorithmIdentifier ::= SEQUENCE {
|
||||||
|
algorithm OBJECT IDENTIFIER,
|
||||||
|
parameters ANY DEFINED BY algorithm OPTIONAL }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2.3 Name / DN 结构(ASN.1;RFC 5280 §4.1.2.4)
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
Name ::= CHOICE { -- only one possibility for now --
|
||||||
|
rdnSequence RDNSequence }
|
||||||
|
|
||||||
|
RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
|
||||||
|
|
||||||
|
RelativeDistinguishedName ::=
|
||||||
|
SET SIZE (1..MAX) OF AttributeTypeAndValue
|
||||||
|
|
||||||
|
AttributeTypeAndValue ::= SEQUENCE {
|
||||||
|
type AttributeType,
|
||||||
|
value AttributeValue }
|
||||||
|
|
||||||
|
AttributeType ::= OBJECT IDENTIFIER
|
||||||
|
|
||||||
|
AttributeValue ::= ANY -- DEFINED BY AttributeType
|
||||||
|
|
||||||
|
DirectoryString ::= CHOICE {
|
||||||
|
teletexString TeletexString (SIZE (1..MAX)),
|
||||||
|
printableString PrintableString (SIZE (1..MAX)),
|
||||||
|
universalString UniversalString (SIZE (1..MAX)),
|
||||||
|
utf8String UTF8String (SIZE (1..MAX)),
|
||||||
|
bmpString BMPString (SIZE (1..MAX)) }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2.4 GeneralNames / GeneralName(ASN.1;RFC 5280 §4.2.1.6)
|
||||||
|
|
||||||
|
> 说明:RPKI 的 AIA/SIA/CRLDP 等扩展通常把 URI 编码在 `uniformResourceIdentifier [6] IA5String` 分支中。RFC 5280 §4.2.1.6。
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
|
||||||
|
|
||||||
|
GeneralName ::= CHOICE {
|
||||||
|
otherName [0] OtherName,
|
||||||
|
rfc822Name [1] IA5String,
|
||||||
|
dNSName [2] IA5String,
|
||||||
|
x400Address [3] ORAddress,
|
||||||
|
directoryName [4] Name,
|
||||||
|
ediPartyName [5] EDIPartyName,
|
||||||
|
uniformResourceIdentifier [6] IA5String,
|
||||||
|
iPAddress [7] OCTET STRING,
|
||||||
|
registeredID [8] OBJECT IDENTIFIER }
|
||||||
|
|
||||||
|
OtherName ::= SEQUENCE {
|
||||||
|
type-id OBJECT IDENTIFIER,
|
||||||
|
value [0] EXPLICIT ANY DEFINED BY type-id }
|
||||||
|
|
||||||
|
EDIPartyName ::= SEQUENCE {
|
||||||
|
nameAssigner [0] DirectoryString OPTIONAL,
|
||||||
|
partyName [1] DirectoryString }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2.5 AIA(Authority Information Access;ASN.1;RFC 5280 §4.2.2.1)
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
id-pe-authorityInfoAccess OBJECT IDENTIFIER ::= { id-pe 1 }
|
||||||
|
|
||||||
|
AuthorityInfoAccessSyntax ::=
|
||||||
|
SEQUENCE SIZE (1..MAX) OF AccessDescription
|
||||||
|
|
||||||
|
AccessDescription ::= SEQUENCE {
|
||||||
|
accessMethod OBJECT IDENTIFIER,
|
||||||
|
accessLocation GeneralName }
|
||||||
|
|
||||||
|
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
|
||||||
|
|
||||||
|
id-ad-caIssuers OBJECT IDENTIFIER ::= { id-ad 2 }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2.6 SIA(Subject Information Access;ASN.1;RFC 5280 §4.2.2.2)
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
id-pe-subjectInfoAccess OBJECT IDENTIFIER ::= { id-pe 11 }
|
||||||
|
|
||||||
|
SubjectInfoAccessSyntax ::=
|
||||||
|
SEQUENCE SIZE (1..MAX) OF AccessDescription
|
||||||
|
|
||||||
|
AccessDescription ::= SEQUENCE {
|
||||||
|
accessMethod OBJECT IDENTIFIER,
|
||||||
|
accessLocation GeneralName }
|
||||||
|
|
||||||
|
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
|
||||||
|
|
||||||
|
id-ad-caRepository OBJECT IDENTIFIER ::= { id-ad 5 }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2.7 RPKI 在 SIA 中新增/使用的 accessMethod OID(RFC 6487 §4.8.8.1 / §4.8.8.2;RFC 8182 §3.2)
|
||||||
|
|
||||||
|
> 说明:下列 OID 用于 `AccessDescription.accessMethod`,并放在 SIA 的 `extnValue` 内层结构中(其外层 extnID 仍为 SIA:`id-pe-subjectInfoAccess`)。RFC 6487 §4.8.8;RFC 8182 §3.2。
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
|
||||||
|
|
||||||
|
id-ad-rpkiManifest OBJECT IDENTIFIER ::= { id-ad 10 } -- 1.3.6.1.5.5.7.48.10
|
||||||
|
|
||||||
|
id-ad-signedObject OBJECT IDENTIFIER ::= { id-ad 11 } -- 1.3.6.1.5.5.7.48.11
|
||||||
|
|
||||||
|
id-ad-rpkiNotify OBJECT IDENTIFIER ::= { id-ad 13 } -- 1.3.6.1.5.5.7.48.13
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2.8 CRLDistributionPoints(CRLDP;ASN.1;RFC 5280 §4.2.1.13)
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
id-ce-cRLDistributionPoints OBJECT IDENTIFIER ::= { id-ce 31 }
|
||||||
|
|
||||||
|
CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint
|
||||||
|
|
||||||
|
DistributionPoint ::= SEQUENCE {
|
||||||
|
distributionPoint [0] DistributionPointName OPTIONAL,
|
||||||
|
reasons [1] ReasonFlags OPTIONAL,
|
||||||
|
cRLIssuer [2] GeneralNames OPTIONAL }
|
||||||
|
|
||||||
|
DistributionPointName ::= CHOICE {
|
||||||
|
fullName [0] GeneralNames,
|
||||||
|
nameRelativeToCRLIssuer [1] RelativeDistinguishedName }
|
||||||
|
|
||||||
|
ReasonFlags ::= BIT STRING {
|
||||||
|
unused (0),
|
||||||
|
keyCompromise (1),
|
||||||
|
cACompromise (2),
|
||||||
|
affiliationChanged (3),
|
||||||
|
superseded (4),
|
||||||
|
cessationOfOperation (5),
|
||||||
|
certificateHold (6),
|
||||||
|
privilegeWithdrawn (7),
|
||||||
|
aACompromise (8) }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2.9 Certificate Policies(ASN.1;RFC 5280 §4.2.1.4)
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
id-ce-certificatePolicies OBJECT IDENTIFIER ::= { id-ce 32 }
|
||||||
|
|
||||||
|
anyPolicy OBJECT IDENTIFIER ::= { id-ce-certificatePolicies 0 }
|
||||||
|
|
||||||
|
certificatePolicies ::= SEQUENCE SIZE (1..MAX) OF PolicyInformation
|
||||||
|
|
||||||
|
PolicyInformation ::= SEQUENCE {
|
||||||
|
policyIdentifier CertPolicyId,
|
||||||
|
policyQualifiers SEQUENCE SIZE (1..MAX) OF
|
||||||
|
PolicyQualifierInfo OPTIONAL }
|
||||||
|
|
||||||
|
CertPolicyId ::= OBJECT IDENTIFIER
|
||||||
|
|
||||||
|
PolicyQualifierInfo ::= SEQUENCE {
|
||||||
|
policyQualifierId PolicyQualifierId,
|
||||||
|
qualifier ANY DEFINED BY policyQualifierId }
|
||||||
|
|
||||||
|
-- policyQualifierIds for Internet policy qualifiers
|
||||||
|
|
||||||
|
id-qt OBJECT IDENTIFIER ::= { id-pkix 2 }
|
||||||
|
id-qt-cps OBJECT IDENTIFIER ::= { id-qt 1 }
|
||||||
|
id-qt-unotice OBJECT IDENTIFIER ::= { id-qt 2 }
|
||||||
|
|
||||||
|
PolicyQualifierId ::= OBJECT IDENTIFIER ( id-qt-cps | id-qt-unotice )
|
||||||
|
|
||||||
|
Qualifier ::= CHOICE {
|
||||||
|
cPSuri CPSuri,
|
||||||
|
userNotice UserNotice }
|
||||||
|
|
||||||
|
CPSuri ::= IA5String
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2.10 RFC 3779 IP/AS 资源扩展(ASN.1;RFC 3779 §2.2.1-§2.2.3;RFC 3779 §3.2.1-§3.2.3)
|
||||||
|
|
||||||
|
> 说明:RFC 3779 给出两个扩展的 OID 与 ASN.1 语法;它们作为 X.509 v3 扩展出现在 `extensions` 中(外层 extnID 为下列 OID)。RPKI profile 进一步约束 criticality/SAFI/RDI 等,见 RFC 6487 §4.8.10-§4.8.11。
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
-- IP Address Delegation Extension
|
||||||
|
id-pe-ipAddrBlocks OBJECT IDENTIFIER ::= { id-pe 7 }
|
||||||
|
|
||||||
|
IPAddrBlocks ::= SEQUENCE OF IPAddressFamily
|
||||||
|
|
||||||
|
IPAddressFamily ::= SEQUENCE { -- AFI & optional SAFI --
|
||||||
|
addressFamily OCTET STRING (SIZE (2..3)),
|
||||||
|
ipAddressChoice IPAddressChoice }
|
||||||
|
|
||||||
|
IPAddressChoice ::= CHOICE {
|
||||||
|
inherit NULL, -- inherit from issuer --
|
||||||
|
addressesOrRanges SEQUENCE OF IPAddressOrRange }
|
||||||
|
|
||||||
|
IPAddressOrRange ::= CHOICE {
|
||||||
|
addressPrefix IPAddress,
|
||||||
|
addressRange IPAddressRange }
|
||||||
|
|
||||||
|
IPAddressRange ::= SEQUENCE {
|
||||||
|
min IPAddress,
|
||||||
|
max IPAddress }
|
||||||
|
|
||||||
|
IPAddress ::= BIT STRING
|
||||||
|
|
||||||
|
-- Autonomous System Identifier Delegation Extension
|
||||||
|
id-pe-autonomousSysIds OBJECT IDENTIFIER ::= { id-pe 8 }
|
||||||
|
|
||||||
|
ASIdentifiers ::= SEQUENCE {
|
||||||
|
asnum [0] EXPLICIT ASIdentifierChoice OPTIONAL,
|
||||||
|
rdi [1] EXPLICIT ASIdentifierChoice OPTIONAL}
|
||||||
|
|
||||||
|
ASIdentifierChoice ::= CHOICE {
|
||||||
|
inherit NULL, -- inherit from issuer --
|
||||||
|
asIdsOrRanges SEQUENCE OF ASIdOrRange }
|
||||||
|
|
||||||
|
ASIdOrRange ::= CHOICE {
|
||||||
|
id ASId,
|
||||||
|
range ASRange }
|
||||||
|
|
||||||
|
ASRange ::= SEQUENCE {
|
||||||
|
min ASId,
|
||||||
|
max ASId }
|
||||||
|
|
||||||
|
ASId ::= INTEGER
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2.11 其它 RPKI profile 相关扩展的 ASN.1 定义(RFC 5280 §4.2.1.1-§4.2.1.3;RFC 5280 §4.2.1.9;RFC 5280 §4.2.1.12)
|
||||||
|
|
||||||
|
> 说明:这些是 RPKI 资源证书 profile(RFC 6487 §4.8)所引用的通用 PKIX 扩展语法。RPKI 对其“必须/禁止/criticality/字段允许性”有额外限制(见本文件 3.3/3.4),但编码层的 ASN.1 类型来自 RFC 5280。
|
||||||
|
|
||||||
|
```asn1
|
||||||
|
id-ce-authorityKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 35 }
|
||||||
|
|
||||||
|
AuthorityKeyIdentifier ::= SEQUENCE {
|
||||||
|
keyIdentifier [0] KeyIdentifier OPTIONAL,
|
||||||
|
authorityCertIssuer [1] GeneralNames OPTIONAL,
|
||||||
|
authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL }
|
||||||
|
|
||||||
|
KeyIdentifier ::= OCTET STRING
|
||||||
|
|
||||||
|
id-ce-subjectKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 14 }
|
||||||
|
|
||||||
|
SubjectKeyIdentifier ::= KeyIdentifier
|
||||||
|
|
||||||
|
id-ce-keyUsage OBJECT IDENTIFIER ::= { id-ce 15 }
|
||||||
|
|
||||||
|
KeyUsage ::= BIT STRING {
|
||||||
|
digitalSignature (0),
|
||||||
|
nonRepudiation (1), -- recent editions of X.509 have
|
||||||
|
-- renamed this bit to contentCommitment
|
||||||
|
keyEncipherment (2),
|
||||||
|
dataEncipherment (3),
|
||||||
|
keyAgreement (4),
|
||||||
|
keyCertSign (5),
|
||||||
|
cRLSign (6),
|
||||||
|
encipherOnly (7),
|
||||||
|
decipherOnly (8) }
|
||||||
|
|
||||||
|
id-ce-basicConstraints OBJECT IDENTIFIER ::= { id-ce 19 }
|
||||||
|
|
||||||
|
BasicConstraints ::= SEQUENCE {
|
||||||
|
cA BOOLEAN DEFAULT FALSE,
|
||||||
|
pathLenConstraint INTEGER (0..MAX) OPTIONAL }
|
||||||
|
|
||||||
|
id-ce-extKeyUsage OBJECT IDENTIFIER ::= { id-ce 37 }
|
||||||
|
|
||||||
|
ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId
|
||||||
|
|
||||||
|
KeyPurposeId ::= OBJECT IDENTIFIER
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.3 抽象数据模型(接口)
|
||||||
|
|
||||||
|
> 说明:本模型面向“语义化解析产物”。实现可保留 `raw_der` 作为可追溯入口。
|
||||||
|
|
||||||
|
### 3.3.1 顶层联合类型:`ResourceCertificate`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `raw_der` | `DerBytes` | 证书 DER | 原样保留(建议) | RFC 6487 §4 |
|
||||||
|
| `tbs` | `RpkixTbsCertificate` | 证书语义字段(见下) | 仅允许 RFC 6487 允许的字段/扩展;其他字段 MUST NOT 出现 | RFC 6487 §4 |
|
||||||
|
| `kind` | `enum { ca, ee }` | 语义分类 | 来自 BasicConstraints + 用途约束 | RFC 6487 §4.8.1;RFC 6487 §4.8.8 |
|
||||||
|
|
||||||
|
### 3.3.1.1 派生类型(用于字段类型标注)
|
||||||
|
|
||||||
|
为避免在其它对象文档里反复写“`ResourceCertificate` 且 `kind==...`”,这里定义两个派生/别名类型:
|
||||||
|
|
||||||
|
- `ResourceCaCertificate`:`ResourceCertificate` 且 `kind == ca`
|
||||||
|
- `ResourceEeCertificate`:`ResourceCertificate` 且 `kind == ee`
|
||||||
|
|
||||||
|
这些派生类型不引入新字段,只是对 `ResourceCertificate.kind` 的约束化视图。
|
||||||
|
|
||||||
|
### 3.3.2 `RpkixTbsCertificate`(语义字段集合)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `version` | `int` | X.509 版本 | MUST 为 v3(字段值为 2) | RFC 6487 §4.1 |
|
||||||
|
| `serial_number` | `int` | 序列号 | 正整数;对每 CA 签发唯一 | RFC 6487 §4.2 |
|
||||||
|
| `signature_algorithm` | `Oid` | 证书签名算法 | 必须为 `sha256WithRSAEncryption`(`1.2.840.113549.1.1.11`) | RFC 6487 §4.3;RFC 7935 §2(引用 RFC 4055) |
|
||||||
|
| `issuer_dn` | `RpkixDistinguishedName` | 颁发者 DN | 必含 1 个 CommonName;可含 1 个 serialNumber;CN 必须 PrintableString | RFC 6487 §4.4 |
|
||||||
|
| `subject_dn` | `RpkixDistinguishedName` | 主体 DN | 同 issuer 约束;且对同一 issuer 下“实体+公钥”唯一 | RFC 6487 §4.5 |
|
||||||
|
| `validity_not_before` | `UtcTime` | 有效期起 | X.509 `Time`(UTCTime/GeneralizedTime)解析为 UTC 时间点 | RFC 6487 §4.6.1;RFC 5280 §4.1.2.5 |
|
||||||
|
| `validity_not_after` | `UtcTime` | 有效期止 | X.509 `Time`(UTCTime/GeneralizedTime)解析为 UTC 时间点 | RFC 6487 §4.6.2;RFC 5280 §4.1.2.5 |
|
||||||
|
| `subject_public_key_info` | `DerBytes` | SPKI DER | 算法 profile 指定 | RFC 6487 §4.7;RFC 7935 §3.1 |
|
||||||
|
| `extensions` | `RpkixExtensions` | 扩展集合 | 见下表;criticality/存在性/内容受约束 | RFC 6487 §4.8 |
|
||||||
|
|
||||||
|
### 3.3.3 `RpkixDistinguishedName`(RPKI profile 下的 DN 语义)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `common_name` | `string` | CommonName (CN) | MUST 存在且仅 1 个;类型为 PrintableString | RFC 6487 §4.4;RFC 6487 §4.5 |
|
||||||
|
| `serial_number` | `optional[string]` | serialNumber | MAY 存在且仅 1 个 | RFC 6487 §4.4;RFC 6487 §4.5 |
|
||||||
|
| `rfc4514` | `string` | DN 的 RFC4514 字符串表示 | 便于日志/索引(实现自选) | RFC 6487 §4.5(引用 RFC4514) |
|
||||||
|
|
||||||
|
### 3.3.4 `RpkixExtensions`(核心扩展语义)
|
||||||
|
|
||||||
|
> 表中 “存在性/criticality” 指 RPKI profile 下对该扩展的要求;实现应能区分 “字段缺失” 与 “字段存在但不符合约束”。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 存在性/criticality 与内容约束 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `basic_constraints` | `optional[BasicConstraints]` | CA 标志 | **extnID=`2.5.29.19`**;CA 证书:MUST present & critical;EE:MUST NOT present;pathLen MUST NOT present | RFC 6487 §4.8.1;RFC 5280 §4.2.1.9 |
|
||||||
|
| `subject_key_identifier` | `bytes` | SKI | **extnID=`2.5.29.14`**;MUST present & non-critical;值为 subjectPublicKey 的 DER bit string 的 SHA-1 哈希 | RFC 6487 §4.8.2(引用 RFC 5280 §4.2.1.2) |
|
||||||
|
| `authority_key_identifier` | `optional[AuthorityKeyIdentifier]` | AKI | **extnID=`2.5.29.35`**;自签名:MAY present 且可等于 SKI;非自签名:MUST present;authorityCertIssuer/authorityCertSerialNumber MUST NOT present;non-critical | RFC 6487 §4.8.3;RFC 5280 §4.2.1.1 |
|
||||||
|
| `key_usage` | `KeyUsage` | KeyUsage | **extnID=`2.5.29.15`**;MUST present & critical;CA:仅 `keyCertSign` 与 `cRLSign` 为 TRUE;EE:仅 `digitalSignature` 为 TRUE | RFC 6487 §4.8.4;RFC 5280 §4.2.1.3 |
|
||||||
|
| `extended_key_usage` | `optional[OidSet]` | EKU | **extnID=`2.5.29.37`**;CA:MUST NOT appear;用于验证 RPKI 对象的 EE:MUST NOT appear;若出现不得标 critical | RFC 6487 §4.8.5;RFC 5280 §4.2.1.12 |
|
||||||
|
| `crl_distribution_points` | `optional[CrlDistributionPoints]` | CRLDP | **extnID=`2.5.29.31`**;自签名:MUST be omitted;非自签名:MUST present & non-critical;仅 1 个 DistributionPoint;fullName URI;必须包含至少 1 个 `rsync://` | RFC 6487 §4.8.6;RFC 5280 §4.2.1.13 |
|
||||||
|
| `authority_info_access` | `optional[AuthorityInfoAccess]` | AIA | **extnID=`1.3.6.1.5.5.7.1.1`**;自签名:MUST be omitted;非自签名:MUST present & non-critical;必须含 accessMethod `id-ad-caIssuers`(**`1.3.6.1.5.5.7.48.2`**) 的 `rsync://` URI;可含同对象其它 URI | RFC 6487 §4.8.7;RFC 5280 §4.2.2.1 |
|
||||||
|
| `subject_info_access_ca` | `optional[SubjectInfoAccessCa]` | SIA(CA) | **extnID=`1.3.6.1.5.5.7.1.11`**;CA:MUST present & non-critical;必须含 accessMethod `id-ad-caRepository`(**`1.3.6.1.5.5.7.48.5`**)(`rsync://` 目录 URI)与 `id-ad-rpkiManifest`(**`1.3.6.1.5.5.7.48.10`**)(`rsync://` 对象 URI);若 CA 使用 RRDP,还会包含 `id-ad-rpkiNotify`(**`1.3.6.1.5.5.7.48.13`**)(HTTPS Notification URI) | RFC 6487 §4.8.8.1;RFC 5280 §4.2.2.2;RFC 8182 §3.2 |
|
||||||
|
| `subject_info_access_ee` | `optional[SubjectInfoAccessEe]` | SIA(EE) | **extnID=`1.3.6.1.5.5.7.1.11`**;EE:MUST present & non-critical;必须含 accessMethod `id-ad-signedObject`(**`1.3.6.1.5.5.7.48.11`**);URI **MUST include** `rsync://`;EE 的 SIA 不允许其它 AccessMethods | RFC 6487 §4.8.8.2;RFC 5280 §4.2.2.2 |
|
||||||
|
| `certificate_policies` | `CertificatePolicies` | 证书策略 | **extnID=`2.5.29.32`**;MUST present & critical;恰好 1 个 policy;并允许 0 或 1 个 CPS qualifier(若存在其 id 必为 `id-qt-cps`(**`1.3.6.1.5.5.7.2.1`**)) | RFC 6487 §4.8.9;RFC 7318 §2;RFC 5280 §4.2.1.4 |
|
||||||
|
| `ip_resources` | `optional[IpResourceSet]` | IP 资源扩展 | **extnID=`1.3.6.1.5.5.7.1.7`**;IP/AS 两者至少其一 MUST present;若 present MUST be critical;内容为 RFC 3779 语义;在公用互联网场景 SAFI MUST NOT 使用;且必须为非空或 inherit | RFC 6487 §4.8.10;RFC 3779 §2.2.1;RFC 3779 §2.2.2 |
|
||||||
|
| `as_resources` | `optional[AsResourceSet]` | AS 资源扩展 | **extnID=`1.3.6.1.5.5.7.1.8`**;IP/AS 两者至少其一 MUST present;若 present MUST be critical;内容为 RFC 3779 语义;RDI MUST NOT 使用;且必须为非空或 inherit | RFC 6487 §4.8.11;RFC 3779 §3.2.1;RFC 3779 §3.2.2 |
|
||||||
|
|
||||||
|
### 3.3.5 结构化子类型(建议)
|
||||||
|
|
||||||
|
#### `BasicConstraints`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `ca` | `bool` | 是否 CA | 由 issuer 决定;在 CA 证书中该扩展必须存在 | RFC 6487 §4.8.1 |
|
||||||
|
| `path_len_constraint` | `None` | pathLenConstraint | MUST NOT present(RPKI profile 不使用) | RFC 6487 §4.8.1 |
|
||||||
|
|
||||||
|
#### `AuthorityKeyIdentifier`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `key_identifier` | `bytes` | AKI.keyIdentifier | 使用 issuer 公钥的 SHA-1 哈希(按 RFC 5280 的定义) | RFC 6487 §4.8.3(引用 RFC 5280 §4.2.1.1) |
|
||||||
|
| `authority_cert_issuer` | `None` | authorityCertIssuer | MUST NOT present | RFC 6487 §4.8.3 |
|
||||||
|
| `authority_cert_serial_number` | `None` | authorityCertSerialNumber | MUST NOT present | RFC 6487 §4.8.3 |
|
||||||
|
|
||||||
|
#### `CrlDistributionPoints`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `distribution_point_uris` | `list[Uri]` | CRL 位置列表 | 仅 1 个 DistributionPoint;必须包含至少 1 个 `rsync://` URI 指向该 issuer 最新 CRL;可含其它 URI | RFC 6487 §4.8.6 |
|
||||||
|
|
||||||
|
#### `AuthorityInfoAccess`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `ca_issuers_uris` | `list[Uri]` | 上级 CA 证书位置 | accessMethod=`id-ad-caIssuers`(`1.3.6.1.5.5.7.48.2`);必含 `rsync://` URI;可含同对象其它 URI | RFC 6487 §4.8.7;RFC 5280 §4.2.2.1 |
|
||||||
|
|
||||||
|
#### `SubjectInfoAccessCa`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `ca_repository_uris` | `list[Uri]` | CA 发布点目录(repository publication point) | accessMethod=`id-ad-caRepository`(`1.3.6.1.5.5.7.48.5`);至少 1 个;必须包含 `rsync://`;也可包含其它机制(例如 `https://`)作为“同一目录”的替代访问方式;顺序表示 CA 偏好 | RFC 6487 §4.8.8.1;RFC 5280 §4.2.2.2 |
|
||||||
|
| `rpki_manifest_uris` | `list[Uri]` | 当前 manifest 对象 URI | accessMethod=`id-ad-rpkiManifest`(`1.3.6.1.5.5.7.48.10`);至少 1 个;必须包含 `rsync://`;也可包含其它机制(例如 `https://`)作为“同一对象”的替代访问方式 | RFC 6487 §4.8.8.1;RFC 5280 §4.2.2.2 |
|
||||||
|
| `rpki_notify_uris` | `optional[list[Uri]]` | RRDP Notification(Update Notification File)URI | accessMethod=`id-ad-rpkiNotify`(`1.3.6.1.5.5.7.48.13`);若存在则 accessLocation MUST 为 `https://` URI,指向 RRDP Notification 文件 | RFC 8182 §3.2;RFC 5280 §4.2.2.2 |
|
||||||
|
|
||||||
|
#### `SubjectInfoAccessEe`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `signed_object_uris` | `list[Uri]` | 被 EE 证书验证的签名对象位置 | accessMethod=`id-ad-signedObject`(`1.3.6.1.5.5.7.48.11`);必须包含 `rsync://`;其它 URI 可作为同对象替代机制;EE SIA 不允许其它 AccessMethods | RFC 6487 §4.8.8.2;RFC 5280 §4.2.2.2 |
|
||||||
|
|
||||||
|
#### `CertificatePolicies`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `policy_oid` | `Oid` | 唯一 policy OID | 恰好 1 个 policy;RPKI CP 分配的 OID 为 `id-cp-ipAddr-asNumber`(`1.3.6.1.5.5.7.14.2`) | RFC 6487 §4.8.9;RFC 6484 §1.2 |
|
||||||
|
| `cps_uri` | `optional[Uri]` | CPS policy qualifier URI | MAY 存在且最多 1 个;若存在其 `policyQualifierId` 必为 `id-qt-cps`;对该 URI 不施加处理要求 | RFC 7318 §2;RFC 5280 §4.2.1.4 |
|
||||||
|
|
||||||
|
## 3.4 字段级约束清单(实现对照)
|
||||||
|
|
||||||
|
- 仅允许 RFC 6487 §4 指定的字段/扩展;未列出字段 MUST NOT 出现。RFC 6487 §4。
|
||||||
|
- 证书版本必须为 v3。RFC 6487 §4.1。
|
||||||
|
- CA/EE 在 BasicConstraints 与 SIA 的约束不同。RFC 6487 §4.8.1;RFC 6487 §4.8.8.1;RFC 6487 §4.8.8.2。
|
||||||
|
- KeyUsage:CA 仅 `keyCertSign`/`cRLSign`;EE 仅 `digitalSignature`。RFC 6487 §4.8.4。
|
||||||
|
- CRLDP/AIA:自签名必须省略;非自签名必须存在并包含 `rsync://`。RFC 6487 §4.8.6;RFC 6487 §4.8.7。
|
||||||
|
- IP/AS 资源扩展:两者至少其一存在;若存在必须 critical;语义来自 RFC 3779;在公用互联网场景 SAFI 与 RDI 均不得使用。RFC 6487 §4.8.10;RFC 6487 §4.8.11;RFC 3779 §2.2.3;RFC 3779 §3.2.3。
|
||||||
@ -1,4 +1,5 @@
|
|||||||
use crate::data_model::oid::OID_CT_ASPA;
|
use crate::data_model::oid::OID_CT_ASPA;
|
||||||
|
use crate::data_model::rc::ResourceCertificate;
|
||||||
use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError};
|
use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError};
|
||||||
use der_parser::ber::{Class};
|
use der_parser::ber::{Class};
|
||||||
use der_parser::der::{parse_der, DerObject, Tag};
|
use der_parser::der::{parse_der, DerObject, Tag};
|
||||||
@ -19,73 +20,64 @@ pub struct AspaEContent {
|
|||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum AspaDecodeError {
|
pub enum AspaDecodeError {
|
||||||
#[error("SignedObject decode error: {0}")]
|
#[error("SignedObject decode error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")]
|
||||||
SignedObjectDecode(#[from] SignedObjectDecodeError),
|
SignedObjectDecode(#[from] SignedObjectDecodeError),
|
||||||
|
|
||||||
#[error("ASPA eContentType must be {OID_CT_ASPA}, got {0}")]
|
#[error("ASPA eContentType must be {OID_CT_ASPA}, got {0} (draft-ietf-sidrops-aspa-profile-21 §2)")]
|
||||||
InvalidEContentType(String),
|
InvalidEContentType(String),
|
||||||
|
|
||||||
#[error("ASPA parse error: {0}")]
|
#[error("ASPA parse error: {0} (draft-ietf-sidrops-aspa-profile-21 §3; DER)")]
|
||||||
Parse(String),
|
Parse(String),
|
||||||
|
|
||||||
#[error("ASPA trailing bytes: {0} bytes")]
|
#[error("ASPA trailing bytes: {0} bytes (draft-ietf-sidrops-aspa-profile-21 §3; DER)")]
|
||||||
TrailingBytes(usize),
|
TrailingBytes(usize),
|
||||||
|
|
||||||
#[error("ASProviderAttestation must be a SEQUENCE of 3 elements")]
|
#[error("ASProviderAttestation must be a SEQUENCE of 3 elements (draft-ietf-sidrops-aspa-profile-21 §3)")]
|
||||||
InvalidAttestationSequence,
|
InvalidAttestationSequence,
|
||||||
|
|
||||||
#[error("ASPA version must be 1 and MUST be explicitly encoded")]
|
#[error("ASPA version must be 1 and MUST be explicitly encoded (draft-ietf-sidrops-aspa-profile-21 §3.1)")]
|
||||||
VersionMustBeExplicitOne,
|
VersionMustBeExplicitOne,
|
||||||
|
|
||||||
#[error("ASPA customerASID out of range (0..=4294967295), got {0}")]
|
#[error("ASPA customerASID out of range (0..=4294967295), got {0} (draft-ietf-sidrops-aspa-profile-21 §3.2)")]
|
||||||
CustomerAsIdOutOfRange(u64),
|
CustomerAsIdOutOfRange(u64),
|
||||||
|
|
||||||
#[error("ASPA providers must contain at least one ASID")]
|
#[error("ASPA providers must contain at least one ASID (draft-ietf-sidrops-aspa-profile-21 §3.3)")]
|
||||||
EmptyProviders,
|
EmptyProviders,
|
||||||
|
|
||||||
#[error("ASPA provider ASID out of range (0..=4294967295), got {0}")]
|
#[error("ASPA provider ASID out of range (0..=4294967295), got {0} (draft-ietf-sidrops-aspa-profile-21 §3.3)")]
|
||||||
ProviderAsIdOutOfRange(u64),
|
ProviderAsIdOutOfRange(u64),
|
||||||
|
|
||||||
#[error("ASPA providers must be in strictly increasing order")]
|
#[error("ASPA providers must be in strictly increasing order (draft-ietf-sidrops-aspa-profile-21 §3.3)")]
|
||||||
ProvidersNotStrictlyIncreasing,
|
ProvidersNotStrictlyIncreasing,
|
||||||
|
|
||||||
#[error("ASPA providers contains the customerASID ({0}) which is not allowed")]
|
#[error("ASPA providers contains the customerASID ({0}) which is not allowed (draft-ietf-sidrops-aspa-profile-21 §3.3)")]
|
||||||
ProvidersContainCustomer(u32),
|
ProvidersContainCustomer(u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum AspaValidateError {
|
pub enum AspaValidateError {
|
||||||
#[error("ASPA EE certificate must contain AS resources extension (RFC 3779)")]
|
#[error("ASPA EE certificate must contain AS resources extension (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2)")]
|
||||||
EeAsResourcesMissing,
|
EeAsResourcesMissing,
|
||||||
|
|
||||||
#[error("ASPA EE certificate AS resources must not use inherit")]
|
#[error("ASPA EE certificate AS resources must not use inherit (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.3)")]
|
||||||
EeAsResourcesInherit,
|
EeAsResourcesInherit,
|
||||||
|
|
||||||
#[error("ASPA EE certificate AS resources must not include ranges")]
|
#[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,
|
EeAsResourcesRangePresent,
|
||||||
|
|
||||||
#[error("ASPA customerASID ({customer_as_id}) does not match EE AS resources ({ee_as_id})")]
|
#[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 },
|
CustomerAsIdMismatch { customer_as_id: u32, ee_as_id: u32 },
|
||||||
|
|
||||||
#[error("ASPA EE certificate must not contain IP resources extension (RFC 3779)")]
|
#[error("ASPA EE certificate must not contain IP resources extension (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §2.2)")]
|
||||||
EeIpResourcesPresent,
|
EeIpResourcesPresent,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Minimal EE resource information required to validate ASPA payload semantics.
|
|
||||||
///
|
|
||||||
/// The caller is responsible for extracting this from the EE certificate (RFC 3779).
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct EeAspaResources {
|
|
||||||
/// The single ASID carried in EE's AS resources "id" element (profile requires exactly one).
|
|
||||||
pub as_id: Option<u32>,
|
|
||||||
/// Whether EE's AS resources uses inherit (must be false).
|
|
||||||
pub as_resources_inherit: bool,
|
|
||||||
/// Whether EE's AS resources includes any range elements (must be false).
|
|
||||||
pub as_resources_range_present: bool,
|
|
||||||
/// Whether EE certificate contains IP resources extension (must be false).
|
|
||||||
pub ip_resources_present: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AspaObject {
|
impl AspaObject {
|
||||||
pub fn decode_der(der: &[u8]) -> Result<Self, AspaDecodeError> {
|
pub fn decode_der(der: &[u8]) -> Result<Self, AspaDecodeError> {
|
||||||
let signed_object = RpkiSignedObject::decode_der(der)?;
|
let signed_object = RpkiSignedObject::decode_der(der)?;
|
||||||
@ -109,6 +101,12 @@ impl AspaObject {
|
|||||||
econtent_type: OID_CT_ASPA.to_string(),
|
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 {
|
impl AspaEContent {
|
||||||
@ -165,27 +163,45 @@ impl AspaEContent {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate ASPA payload against EE resources extracted by the caller.
|
/// Validate ASPA payload against the embedded EE resource certificate.
|
||||||
///
|
///
|
||||||
/// This implements the EE/payload semantic checks described in
|
/// This implements the EE/payload semantic checks described in
|
||||||
/// `draft-ietf-sidrops-aspa-profile-21` §4 (as summarized in `rpki/specs/08_aspa.md`).
|
/// `draft-ietf-sidrops-aspa-profile-21` §4 (as summarized in `rpki/specs/08_aspa.md`).
|
||||||
pub fn validate_against_ee_resources(&self, ee: &EeAspaResources) -> Result<(), AspaValidateError> {
|
pub fn validate_against_ee_cert(
|
||||||
if ee.ip_resources_present {
|
&self,
|
||||||
|
ee: &ResourceCertificate,
|
||||||
|
) -> Result<(), AspaValidateError> {
|
||||||
|
if ee.tbs.extensions.ip_resources.is_some() {
|
||||||
return Err(AspaValidateError::EeIpResourcesPresent);
|
return Err(AspaValidateError::EeIpResourcesPresent);
|
||||||
}
|
}
|
||||||
if ee.as_resources_inherit {
|
|
||||||
|
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);
|
return Err(AspaValidateError::EeAsResourcesInherit);
|
||||||
}
|
}
|
||||||
if ee.as_resources_range_present {
|
if asn.has_any_range() {
|
||||||
return Err(AspaValidateError::EeAsResourcesRangePresent);
|
return Err(AspaValidateError::EeAsResourcesRangePresent);
|
||||||
}
|
}
|
||||||
let ee_as_id = ee.as_id.ok_or(AspaValidateError::EeAsResourcesMissing)?;
|
|
||||||
|
let ee_as_id = asn
|
||||||
|
.asnum_single_id()
|
||||||
|
.ok_or(AspaValidateError::EeAsResourcesNotSingleId)?;
|
||||||
if ee_as_id != self.customer_as_id {
|
if ee_as_id != self.customer_as_id {
|
||||||
return Err(AspaValidateError::CustomerAsIdMismatch {
|
return Err(AspaValidateError::CustomerAsIdMismatch {
|
||||||
customer_as_id: self.customer_as_id,
|
customer_as_id: self.customer_as_id,
|
||||||
ee_as_id,
|
ee_as_id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,7 +69,7 @@ impl BigUnsigned {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
|
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
|
||||||
#[error("{field} time encoding invalid for year {year}: got {encoding:?}")]
|
#[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 struct InvalidTimeEncodingError {
|
||||||
pub field: &'static str,
|
pub field: &'static str,
|
||||||
pub year: i32,
|
pub year: i32,
|
||||||
|
|||||||
@ -37,52 +37,52 @@ pub struct RpkixCrl {
|
|||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum CrlDecodeError {
|
pub enum CrlDecodeError {
|
||||||
#[error("X.509 CRL parse error: {0}")]
|
#[error("X.509 CRL parse error: {0} (RFC 5280 §5.1; RFC 6487 §5)")]
|
||||||
Parse(String),
|
Parse(String),
|
||||||
|
|
||||||
#[error("trailing bytes after CRL DER: {0} bytes")]
|
#[error("trailing bytes after CRL DER: {0} bytes (DER; RFC 5280 §5.1)")]
|
||||||
TrailingBytes(usize),
|
TrailingBytes(usize),
|
||||||
|
|
||||||
#[error("CRL version must be v2, got {0:?}")]
|
#[error("CRL version must be v2, got {0:?} (RFC 5280 §5.1; RFC 6487 §5)")]
|
||||||
InvalidVersion(Option<u32>),
|
InvalidVersion(Option<u32>),
|
||||||
|
|
||||||
#[error("CRL signatureAlgorithm must be sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0}")]
|
#[error("CRL signatureAlgorithm must be sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0} (RFC 6487 §5; RFC 7935 §2)")]
|
||||||
InvalidSignatureAlgorithm(String),
|
InvalidSignatureAlgorithm(String),
|
||||||
|
|
||||||
#[error("CRL signature algorithm parameters must be absent or NULL")]
|
#[error("CRL signature algorithm parameters must be absent or NULL (RFC 5280 §4.1.1.2; RFC 7935 §2)")]
|
||||||
InvalidSignatureAlgorithmParameters,
|
InvalidSignatureAlgorithmParameters,
|
||||||
|
|
||||||
#[error("CRL signatureAlgorithm must match TBSCertList.signature")]
|
#[error("CRL signatureAlgorithm must match TBSCertList.signature (RFC 5280 §5.1)")]
|
||||||
SignatureAlgorithmMismatch,
|
SignatureAlgorithmMismatch,
|
||||||
|
|
||||||
#[error("CRL extensions must be exactly two (AKI + CRLNumber), got {0}")]
|
#[error("CRL extensions must be exactly two (AKI + CRLNumber), got {0} (RFC 9829 §3.1)")]
|
||||||
InvalidExtensionsCount(usize),
|
InvalidExtensionsCount(usize),
|
||||||
|
|
||||||
#[error("unsupported CRL extension OID {0}")]
|
#[error("unsupported CRL extension OID {0} (RFC 9829 §3.1)")]
|
||||||
UnsupportedExtension(String),
|
UnsupportedExtension(String),
|
||||||
|
|
||||||
#[error("duplicate CRL extension OID {0}")]
|
#[error("duplicate CRL extension OID {0} (RFC 5280 §4.2; RFC 9829 §3.1)")]
|
||||||
DuplicateExtension(String),
|
DuplicateExtension(String),
|
||||||
|
|
||||||
#[error("AuthorityKeyIdentifier must contain keyIdentifier")]
|
#[error("AuthorityKeyIdentifier must contain keyIdentifier (RFC 5280 §5.2.1; RFC 9829 §3.1)")]
|
||||||
AkiMissingKeyIdentifier,
|
AkiMissingKeyIdentifier,
|
||||||
|
|
||||||
#[error("AuthorityKeyIdentifier must not contain authorityCertIssuer or authorityCertSerialNumber")]
|
#[error("AuthorityKeyIdentifier must not contain authorityCertIssuer or authorityCertSerialNumber (RFC 5280 §5.2.1; RFC 9829 §3.1)")]
|
||||||
AkiHasOtherFields,
|
AkiHasOtherFields,
|
||||||
|
|
||||||
#[error("CRLNumber must be non-critical")]
|
#[error("CRLNumber must be non-critical (RFC 9829 §3.1; RFC 5280 §5.2.3)")]
|
||||||
CrlNumberCritical,
|
CrlNumberCritical,
|
||||||
|
|
||||||
#[error("CRLNumber out of range (must fit in 0..2^159-1)")]
|
#[error("CRLNumber out of range (must fit in 0..2^159-1) (RFC 9829 §3.1)")]
|
||||||
CrlNumberOutOfRange,
|
CrlNumberOutOfRange,
|
||||||
|
|
||||||
#[error("CRL entry extensions must not be present")]
|
#[error("CRL entry extensions must not be present (RFC 6487 §5; RFC 5280 §5.1)")]
|
||||||
EntryExtensionsNotAllowed,
|
EntryExtensionsNotAllowed,
|
||||||
|
|
||||||
#[error("CRL nextUpdate must be present (RFC 5280 §5.1.2.5)")]
|
#[error("CRL nextUpdate must be present (RFC 5280 §5.1.2.5; RFC 6487 §5)")]
|
||||||
NextUpdateMissing,
|
NextUpdateMissing,
|
||||||
|
|
||||||
#[error("{field} time encoding invalid for year {year}: got {encoding:?}")]
|
#[error("{field} time encoding invalid for year {year}: got {encoding:?} (RFC 5280 §5.1.2.4-§5.1.2.6)")]
|
||||||
InvalidTimeEncoding {
|
InvalidTimeEncoding {
|
||||||
field: &'static str,
|
field: &'static str,
|
||||||
year: i32,
|
year: i32,
|
||||||
@ -231,37 +231,37 @@ impl RpkixCrl {
|
|||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum CrlVerifyError {
|
pub enum CrlVerifyError {
|
||||||
#[error("issuer certificate parse error: {0}")]
|
#[error("issuer certificate parse error: {0} (RFC 5280 §4.1; RFC 6487 §4)")]
|
||||||
IssuerCertificateParse(String),
|
IssuerCertificateParse(String),
|
||||||
|
|
||||||
#[error("trailing bytes after issuer certificate DER: {0} bytes")]
|
#[error("trailing bytes after issuer certificate DER: {0} bytes (DER; RFC 5280 §4.1)")]
|
||||||
IssuerCertificateTrailingBytes(usize),
|
IssuerCertificateTrailingBytes(usize),
|
||||||
|
|
||||||
#[error("issuer SubjectPublicKeyInfo parse error: {0}")]
|
#[error("issuer SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7)")]
|
||||||
IssuerSpkiParse(String),
|
IssuerSpkiParse(String),
|
||||||
|
|
||||||
#[error("trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes")]
|
#[error("trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 5280 §4.1.2.7)")]
|
||||||
IssuerSpkiTrailingBytes(usize),
|
IssuerSpkiTrailingBytes(usize),
|
||||||
|
|
||||||
#[error("CRL parse error: {0}")]
|
#[error("CRL parse error: {0} (RFC 5280 §5.1; RFC 6487 §5)")]
|
||||||
CrlParse(String),
|
CrlParse(String),
|
||||||
|
|
||||||
#[error("trailing bytes after CRL DER: {0} bytes")]
|
#[error("trailing bytes after CRL DER: {0} bytes (DER; RFC 5280 §5.1)")]
|
||||||
CrlTrailingBytes(usize),
|
CrlTrailingBytes(usize),
|
||||||
|
|
||||||
#[error("CRL issuer DN does not match issuer certificate subject")]
|
#[error("CRL issuer DN does not match issuer certificate subject (RFC 5280 §5.1; RFC 5280 §6.3.3(b))")]
|
||||||
IssuerSubjectMismatch {
|
IssuerSubjectMismatch {
|
||||||
crl_issuer_dn: String,
|
crl_issuer_dn: String,
|
||||||
issuer_subject_dn: String,
|
issuer_subject_dn: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("issuer certificate keyUsage present but missing cRLSign")]
|
#[error("issuer certificate keyUsage present but missing cRLSign (RFC 5280 §4.2.1.3; RFC 5280 §6.3.3(f))")]
|
||||||
IssuerKeyUsageMissingCrlSign,
|
IssuerKeyUsageMissingCrlSign,
|
||||||
|
|
||||||
#[error("CRL AKI.keyIdentifier does not match issuer certificate SKI")]
|
#[error("CRL AKI.keyIdentifier does not match issuer certificate SKI (RFC 5280 §4.2.1.1; RFC 5280 §4.2.1.2; RFC 5280 §6.3.3(c)/(f))")]
|
||||||
AkiSkiMismatch,
|
AkiSkiMismatch,
|
||||||
|
|
||||||
#[error("CRL signature verification failed: {0}")]
|
#[error("CRL signature verification failed: {0} (RFC 5280 §6.3.3(g); RFC 7935 §2)")]
|
||||||
InvalidSignature(String),
|
InvalidSignature(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
use crate::data_model::common::BigUnsigned;
|
use crate::data_model::common::BigUnsigned;
|
||||||
use crate::data_model::oid::{OID_CT_RPKI_MANIFEST, OID_SHA256};
|
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 crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError};
|
||||||
use der_parser::ber::BerObjectContent;
|
use der_parser::ber::BerObjectContent;
|
||||||
use der_parser::der::{parse_der, DerObject, Tag};
|
use der_parser::der::{parse_der, DerObject, Tag};
|
||||||
@ -30,61 +31,76 @@ pub struct FileAndHash {
|
|||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum ManifestDecodeError {
|
pub enum ManifestDecodeError {
|
||||||
#[error("signed object decode error: {0}")]
|
#[error("signed object decode error: {0} (RFC 6488 §2-§3; RFC 9589 §4; RFC 9286 §4)")]
|
||||||
SignedObject(#[from] SignedObjectDecodeError),
|
SignedObject(#[from] SignedObjectDecodeError),
|
||||||
|
|
||||||
#[error("DER parse error: {0}")]
|
#[error("DER parse error: {0} (RFC 9286 §4.2; DER)")]
|
||||||
Parse(String),
|
Parse(String),
|
||||||
|
|
||||||
#[error("trailing bytes after DER object: {0} bytes")]
|
#[error("trailing bytes after DER object: {0} bytes (RFC 9286 §4.2; DER)")]
|
||||||
TrailingBytes(usize),
|
TrailingBytes(usize),
|
||||||
|
|
||||||
#[error("eContentType must be id-ct-rpkiManifest ({OID_CT_RPKI_MANIFEST}), got {0}")]
|
#[error("eContentType must be id-ct-rpkiManifest ({OID_CT_RPKI_MANIFEST}), got {0} (RFC 9286 §4.1; RFC 9286 §4.4(1))")]
|
||||||
InvalidEContentType(String),
|
InvalidEContentType(String),
|
||||||
|
|
||||||
#[error("Manifest must be a SEQUENCE of 5 or 6 elements, got {0}")]
|
#[error("Manifest must be a SEQUENCE of 5 or 6 elements, got {0} (RFC 9286 §4.2)")]
|
||||||
InvalidManifestSequenceLen(usize),
|
InvalidManifestSequenceLen(usize),
|
||||||
|
|
||||||
#[error("Manifest.version must be 0, got {0}")]
|
#[error("Manifest.version must be 0, got {0} (RFC 9286 §4.2.1)")]
|
||||||
InvalidManifestVersion(u64),
|
InvalidManifestVersion(u64),
|
||||||
|
|
||||||
#[error("Manifest.manifestNumber must be non-negative INTEGER")]
|
#[error("Manifest.manifestNumber must be non-negative INTEGER (RFC 9286 §4.2; RFC 9286 §4.2.1)")]
|
||||||
InvalidManifestNumber,
|
InvalidManifestNumber,
|
||||||
|
|
||||||
#[error("Manifest.manifestNumber longer than 20 octets")]
|
#[error("Manifest.manifestNumber longer than 20 octets (RFC 9286 §4.2.1)")]
|
||||||
ManifestNumberTooLong,
|
ManifestNumberTooLong,
|
||||||
|
|
||||||
#[error("Manifest.thisUpdate must be GeneralizedTime")]
|
#[error("Manifest.thisUpdate must be GeneralizedTime (RFC 9286 §4.2)")]
|
||||||
InvalidThisUpdate,
|
InvalidThisUpdate,
|
||||||
|
|
||||||
#[error("Manifest.nextUpdate must be GeneralizedTime")]
|
#[error("Manifest.nextUpdate must be GeneralizedTime (RFC 9286 §4.2)")]
|
||||||
InvalidNextUpdate,
|
InvalidNextUpdate,
|
||||||
|
|
||||||
#[error("Manifest.nextUpdate must be later than thisUpdate")]
|
#[error("Manifest.nextUpdate must be later than thisUpdate (RFC 9286 §4.2.1)")]
|
||||||
NextUpdateNotLater,
|
NextUpdateNotLater,
|
||||||
|
|
||||||
#[error("Manifest.fileHashAlg must be id-sha256 ({OID_SHA256}), got {0}")]
|
#[error("Manifest.fileHashAlg must be id-sha256 ({OID_SHA256}), got {0} (RFC 9286 §4.2.1; RFC 7935 §2)")]
|
||||||
InvalidFileHashAlg(String),
|
InvalidFileHashAlg(String),
|
||||||
|
|
||||||
#[error("Manifest.fileList must be a SEQUENCE")]
|
#[error("Manifest.fileList must be a SEQUENCE (RFC 9286 §4.2)")]
|
||||||
InvalidFileList,
|
InvalidFileList,
|
||||||
|
|
||||||
#[error("FileAndHash must be SEQUENCE of 2")]
|
#[error("FileAndHash must be SEQUENCE of 2 (RFC 9286 §4.2)")]
|
||||||
InvalidFileAndHash,
|
InvalidFileAndHash,
|
||||||
|
|
||||||
#[error("fileList file name invalid: {0}")]
|
#[error("fileList file name invalid: {0} (RFC 9286 §4.2.2)")]
|
||||||
InvalidFileName(String),
|
InvalidFileName(String),
|
||||||
|
|
||||||
#[error("fileList hash must be BIT STRING")]
|
#[error("fileList hash must be BIT STRING (RFC 9286 §4.2)")]
|
||||||
InvalidHashType,
|
InvalidHashType,
|
||||||
|
|
||||||
#[error("fileList hash BIT STRING must be octet-aligned (unused bits=0)")]
|
#[error("fileList hash BIT STRING must be octet-aligned (unused bits=0) (RFC 9286 §4.2.1; DER BIT STRING)")]
|
||||||
HashNotOctetAligned,
|
HashNotOctetAligned,
|
||||||
|
|
||||||
#[error("fileList hash length invalid for sha256: got {0} bytes")]
|
#[error("fileList hash length invalid for sha256: got {0} bytes (RFC 9286 §4.2.1; RFC 7935 §2)")]
|
||||||
InvalidHashLength(usize),
|
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 {
|
impl ManifestObject {
|
||||||
pub fn decode_der(der: &[u8]) -> Result<Self, ManifestDecodeError> {
|
pub fn decode_der(der: &[u8]) -> Result<Self, ManifestDecodeError> {
|
||||||
let signed_object = RpkiSignedObject::decode_der(der)?;
|
let signed_object = RpkiSignedObject::decode_der(der)?;
|
||||||
@ -107,6 +123,44 @@ impl ManifestObject {
|
|||||||
manifest,
|
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 {
|
impl ManifestEContent {
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod crl;
|
pub mod crl;
|
||||||
mod rc;
|
pub mod rc;
|
||||||
mod tal;
|
|
||||||
mod ta;
|
|
||||||
mod resources;
|
|
||||||
mod oids;
|
|
||||||
pub mod oid;
|
pub mod oid;
|
||||||
pub mod signed_object;
|
pub mod signed_object;
|
||||||
pub mod manifest;
|
pub mod manifest;
|
||||||
pub mod roa;
|
pub mod roa;
|
||||||
pub mod aspa;
|
pub mod aspa;
|
||||||
|
pub mod tal;
|
||||||
|
pub mod ta;
|
||||||
|
|||||||
@ -9,6 +9,14 @@ 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_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";
|
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_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35";
|
||||||
pub const OID_CRL_NUMBER: &str = "2.5.29.20";
|
pub const OID_CRL_NUMBER: &str = "2.5.29.20";
|
||||||
pub const OID_SUBJECT_KEY_IDENTIFIER: &str = "2.5.29.14";
|
pub const OID_SUBJECT_KEY_IDENTIFIER: &str = "2.5.29.14";
|
||||||
@ -20,3 +28,15 @@ pub const OID_CT_ASPA: &str = "1.2.840.113549.1.9.16.1.49";
|
|||||||
// X.509 extensions / access methods (RFC 5280 / RFC 6487)
|
// 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_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_SIGNED_OBJECT: &str = "1.3.6.1.5.5.7.48.11";
|
||||||
|
|
||||||
|
pub const OID_AD_CA_ISSUERS: &str = "1.3.6.1.5.5.7.48.2";
|
||||||
|
pub const OID_AD_CA_REPOSITORY: &str = "1.3.6.1.5.5.7.48.5";
|
||||||
|
pub const OID_AD_RPKI_MANIFEST: &str = "1.3.6.1.5.5.7.48.10";
|
||||||
|
pub const OID_AD_RPKI_NOTIFY: &str = "1.3.6.1.5.5.7.48.13";
|
||||||
|
|
||||||
|
// RFC 3779 resource extensions (RFC 6487 profile)
|
||||||
|
pub const OID_IP_ADDR_BLOCKS: &str = "1.3.6.1.5.5.7.1.7";
|
||||||
|
pub const OID_AUTONOMOUS_SYS_IDS: &str = "1.3.6.1.5.5.7.1.8";
|
||||||
|
|
||||||
|
// RPKI CP (RFC 6484 / RFC 6487)
|
||||||
|
pub const OID_CP_IPADDR_ASNUMBER: &str = "1.3.6.1.5.5.7.14.2";
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
pub const OID_BASIC_CONSTRAINTS: &str = "2.5.29.19";
|
|
||||||
pub const OID_SUBJECT_KEY_IDENTIFIER: &str = "2.5.29.14";
|
|
||||||
pub const OID_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35";
|
|
||||||
pub const OID_KEY_USAGE: &str = "2.5.29.15";
|
|
||||||
pub const OID_EXTENDED_KEY_USAGE: &str = "2.5.29.37";
|
|
||||||
pub const OID_CRL_DISTRIBUTION_POINTS: &str = "2.5.29.31";
|
|
||||||
pub const OID_AUTHORITY_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.1";
|
|
||||||
pub const OID_ACCESS_DESCRIPTION: &str = "1.3.6.1.5.5.7.48";
|
|
||||||
pub const OID_AD_CA_ISSUERS: &str = "1.3.6.1.5.5.7.48.2";
|
|
||||||
pub const OID_AD_OCSP: &str = "1.3.6.1.5.5.7.48.1";
|
|
||||||
pub const OID_SUBJECT_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.11";
|
|
||||||
pub const OID_CERTIFICATE_POLICIES: &str = "2.5.29.32";
|
|
||||||
pub const OID_SHA256_WITH_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.11";
|
|
||||||
1291
src/data_model/rc.rs
1291
src/data_model/rc.rs
File diff suppressed because it is too large
Load Diff
@ -1,90 +0,0 @@
|
|||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct ASIdentifiers {
|
|
||||||
pub asn: Vec<ASIdentifierChoice>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ASN
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum ASIdentifierChoice {
|
|
||||||
Inherit,
|
|
||||||
ASIDsOrRanges(Vec<ASIDOrRange>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum ASIDOrRange {
|
|
||||||
Id(Asn),
|
|
||||||
AsRange(ASRange),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct ASRange {
|
|
||||||
pub min: Asn,
|
|
||||||
pub max: Asn,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ASRange {
|
|
||||||
/// Creates a new AS number range from the smallest and largest number.
|
|
||||||
pub fn new(min: Asn, max: Asn) -> Self {
|
|
||||||
ASRange { min, max }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an AS block covering all ASNs.
|
|
||||||
pub fn all() -> ASRange {
|
|
||||||
ASRange::new(Asn::MIN, Asn::MAX)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the smallest AS number that is part of this range.
|
|
||||||
pub fn min(self) -> Asn {
|
|
||||||
self.min
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the largest AS number that is still part of this range.
|
|
||||||
pub fn max(self) -> Asn {
|
|
||||||
self.max
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the number of ASNs covered by this value.
|
|
||||||
pub fn asn_count(self) -> u32 {
|
|
||||||
u32::from(self.max) - u32::from(self.min) + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
|
||||||
pub struct Asn(u32);
|
|
||||||
|
|
||||||
impl Asn {
|
|
||||||
pub const MIN: Asn = Asn(u32::MIN);
|
|
||||||
pub const MAX: Asn = Asn(u32::MAX);
|
|
||||||
|
|
||||||
/// Creates an AS number from a `u32`.
|
|
||||||
pub fn from_u32(value: u32) -> Self {
|
|
||||||
Asn(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts an AS number into a `u32`.
|
|
||||||
pub fn into_u32(self) -> u32 {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts an AS number into a network-order byte array.
|
|
||||||
pub fn to_raw(self) -> [u8; 4] {
|
|
||||||
self.0.to_be_bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<u32> for Asn {
|
|
||||||
fn from(id: u32) -> Self {
|
|
||||||
Asn(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Asn> for u32 {
|
|
||||||
fn from(id: Asn) -> Self {
|
|
||||||
id.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct IPAddrBlocks {
|
|
||||||
ips: Vec<IPAddressFamily>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// IP Address Family
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct IPAddressFamily {
|
|
||||||
pub address_family: Afi,
|
|
||||||
pub ip_address_choice: IPAddressChoice,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum Afi {
|
|
||||||
Ipv4,
|
|
||||||
Ipv6,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum IPAddressChoice {
|
|
||||||
Inherit,
|
|
||||||
AddressOrRange(Vec<IPAddressOrRange>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum IPAddressOrRange {
|
|
||||||
AddressPrefix(IPAddressPrefix),
|
|
||||||
AddressRange(IPAddressRange),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct IPAddressPrefix {
|
|
||||||
pub address: IPAddress,
|
|
||||||
pub prefix_length: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct IPAddressRange {
|
|
||||||
pub min: IPAddress,
|
|
||||||
pub max: IPAddress,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
|
||||||
pub struct IPAddress(u128);
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
pub(crate) mod ip_resources;
|
|
||||||
pub(crate) mod as_resources;
|
|
||||||
pub mod resource;
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
use crate::data_model::resources::as_resources::ASIdentifiers;
|
|
||||||
use crate::data_model::resources::ip_resources::IPAddrBlocks;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct ResourceSet {
|
|
||||||
ip_addr_blocks: IPAddrBlocks,
|
|
||||||
as_identifiers: ASIdentifiers,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,4 +1,5 @@
|
|||||||
use crate::data_model::oid::OID_CT_ROUTE_ORIGIN_AUTHZ;
|
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 crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError};
|
||||||
use der_parser::ber::{BerObjectContent, Class};
|
use der_parser::ber::{BerObjectContent, Class};
|
||||||
use der_parser::der::{parse_der, DerObject, Tag};
|
use der_parser::der::{parse_der, DerObject, Tag};
|
||||||
@ -19,58 +20,58 @@ pub struct RoaEContent {
|
|||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum RoaDecodeError {
|
pub enum RoaDecodeError {
|
||||||
#[error("SignedObject decode error: {0}")]
|
#[error("SignedObject decode error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")]
|
||||||
SignedObjectDecode(#[from] SignedObjectDecodeError),
|
SignedObjectDecode(#[from] SignedObjectDecodeError),
|
||||||
|
|
||||||
#[error("ROA eContentType must be {OID_CT_ROUTE_ORIGIN_AUTHZ}, got {0}")]
|
#[error("ROA eContentType must be {OID_CT_ROUTE_ORIGIN_AUTHZ}, got {0} (RFC 9582 §3)")]
|
||||||
InvalidEContentType(String),
|
InvalidEContentType(String),
|
||||||
|
|
||||||
#[error("ROA parse error: {0}")]
|
#[error("ROA parse error: {0} (RFC 9582 §4; DER)")]
|
||||||
Parse(String),
|
Parse(String),
|
||||||
|
|
||||||
#[error("ROA trailing bytes: {0} bytes")]
|
#[error("ROA trailing bytes: {0} bytes (RFC 9582 §4; DER)")]
|
||||||
TrailingBytes(usize),
|
TrailingBytes(usize),
|
||||||
|
|
||||||
#[error("RouteOriginAttestation must be a SEQUENCE of 2 or 3 elements, got {0}")]
|
#[error("RouteOriginAttestation must be a SEQUENCE of 2 or 3 elements, got {0} (RFC 9582 §4)")]
|
||||||
InvalidAttestationSequenceLen(usize),
|
InvalidAttestationSequenceLen(usize),
|
||||||
|
|
||||||
#[error("ROA version must be 0, got {0}")]
|
#[error("ROA version must be 0, got {0} (RFC 9582 §4.1)")]
|
||||||
InvalidVersion(u64),
|
InvalidVersion(u64),
|
||||||
|
|
||||||
#[error("ROA asID out of range (0..=4294967295), got {0}")]
|
#[error("ROA asID out of range (0..=4294967295), got {0} (RFC 9582 §4.2)")]
|
||||||
AsIdOutOfRange(u64),
|
AsIdOutOfRange(u64),
|
||||||
|
|
||||||
#[error("ROA ipAddrBlocks must have length 1..2, got {0}")]
|
#[error("ROA ipAddrBlocks must have length 1..2, got {0} (RFC 9582 §4; RFC 9582 §4.3.1)")]
|
||||||
InvalidIpAddrBlocksLen(usize),
|
InvalidIpAddrBlocksLen(usize),
|
||||||
|
|
||||||
#[error("ROAIPAddressFamily must be a SEQUENCE of 2 elements")]
|
#[error("ROAIPAddressFamily must be a SEQUENCE of 2 elements (RFC 9582 §4.3.1)")]
|
||||||
InvalidIpAddressFamily,
|
InvalidIpAddressFamily,
|
||||||
|
|
||||||
#[error("ROA addressFamily must be an OCTET STRING of 2 bytes")]
|
#[error("ROA addressFamily must be an OCTET STRING of 2 bytes (RFC 9582 §4.3.1)")]
|
||||||
InvalidAddressFamily,
|
InvalidAddressFamily,
|
||||||
|
|
||||||
#[error("ROA addressFamily AFI not supported: {0:02X?}")]
|
#[error("ROA addressFamily AFI not supported: {0:02X?} (RFC 9582 §4.3.1)")]
|
||||||
UnsupportedAfi(Vec<u8>),
|
UnsupportedAfi(Vec<u8>),
|
||||||
|
|
||||||
#[error("ROA contains duplicate AFI {0:?}")]
|
#[error("ROA contains duplicate AFI {0:?} (RFC 9582 §4.3.1)")]
|
||||||
DuplicateAfi(RoaAfi),
|
DuplicateAfi(RoaAfi),
|
||||||
|
|
||||||
#[error("ROAAddresses must have at least one entry")]
|
#[error("ROAAddresses must have at least one entry (RFC 9582 §4.3.2)")]
|
||||||
EmptyAddressList,
|
EmptyAddressList,
|
||||||
|
|
||||||
#[error("ROAIPAddress must be a SEQUENCE of 1..2 elements")]
|
#[error("ROAIPAddress must be a SEQUENCE of 1..2 elements (RFC 9582 §4.3.2)")]
|
||||||
InvalidRoaIpAddress,
|
InvalidRoaIpAddress,
|
||||||
|
|
||||||
#[error("ROAIPAddress.address must be a BIT STRING")]
|
#[error("ROAIPAddress.address must be a BIT STRING (RFC 9582 §4.3.2.1; RFC 3779 §2.2.3.8)")]
|
||||||
InvalidPrefixBitString,
|
InvalidPrefixBitString,
|
||||||
|
|
||||||
#[error("ROAIPAddress.address has invalid unused bits encoding")]
|
#[error("ROAIPAddress.address has invalid unused bits encoding (RFC 9582 §4.3.2.1; RFC 3779 §2.2.3.8)")]
|
||||||
InvalidPrefixUnusedBits,
|
InvalidPrefixUnusedBits,
|
||||||
|
|
||||||
#[error("ROAIPAddress.address prefix length {prefix_len} out of range for {afi:?}")]
|
#[error("ROAIPAddress.address prefix length {prefix_len} out of range for {afi:?} (RFC 9582 §4.3.2.1)")]
|
||||||
PrefixLenOutOfRange { afi: RoaAfi, prefix_len: u16 },
|
PrefixLenOutOfRange { afi: RoaAfi, prefix_len: u16 },
|
||||||
|
|
||||||
#[error("ROAIPAddress.maxLength out of range for {afi:?}: prefix_len={prefix_len}, max_len={max_len}")]
|
#[error("ROAIPAddress.maxLength out of range for {afi:?}: prefix_len={prefix_len}, max_len={max_len} (RFC 9582 §4.3.2.2)")]
|
||||||
InvalidMaxLength {
|
InvalidMaxLength {
|
||||||
afi: RoaAfi,
|
afi: RoaAfi,
|
||||||
prefix_len: u16,
|
prefix_len: u16,
|
||||||
@ -83,10 +84,13 @@ pub enum RoaValidateError {
|
|||||||
#[error("ROA EE certificate must not contain AS resources extension (RFC 9582 §5)")]
|
#[error("ROA EE certificate must not contain AS resources extension (RFC 9582 §5)")]
|
||||||
EeAsResourcesPresent,
|
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)")]
|
#[error("ROA EE certificate IP resources must not use inherit (RFC 9582 §5)")]
|
||||||
EeIpResourcesInherit,
|
EeIpResourcesInherit,
|
||||||
|
|
||||||
#[error("ROA prefix not covered by EE certificate IP resources: {afi:?} {addr:?}/{prefix_len}")]
|
#[error("ROA prefix not covered by EE certificate IP resources: {afi:?} {addr:?}/{prefix_len} (RFC 9582 §5; RFC 3779 §2.3)")]
|
||||||
PrefixNotInEeResources {
|
PrefixNotInEeResources {
|
||||||
afi: RoaAfi,
|
afi: RoaAfi,
|
||||||
addr: Vec<u8>,
|
addr: Vec<u8>,
|
||||||
@ -94,26 +98,6 @@ pub enum RoaValidateError {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct IpResourceSet {
|
|
||||||
pub prefixes: Vec<IpPrefix>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IpResourceSet {
|
|
||||||
pub fn contains_prefix(&self, p: &IpPrefix) -> bool {
|
|
||||||
self.prefixes
|
|
||||||
.iter()
|
|
||||||
.any(|r| r.afi == p.afi && prefix_covers(r, p))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct EeResources {
|
|
||||||
pub ip_resources: IpResourceSet,
|
|
||||||
pub ip_resources_inherit: bool,
|
|
||||||
pub as_resources_present: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RoaObject {
|
impl RoaObject {
|
||||||
pub fn decode_der(der: &[u8]) -> Result<Self, RoaDecodeError> {
|
pub fn decode_der(der: &[u8]) -> Result<Self, RoaDecodeError> {
|
||||||
let signed_object = RpkiSignedObject::decode_der(der)?;
|
let signed_object = RpkiSignedObject::decode_der(der)?;
|
||||||
@ -137,6 +121,12 @@ impl RoaObject {
|
|||||||
econtent_type: OID_CT_ROUTE_ORIGIN_AUTHZ.to_string(),
|
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)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
@ -247,21 +237,30 @@ impl RoaEContent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate ROA payload against EE resources extracted by the caller.
|
/// Validate ROA payload against the embedded EE resource certificate (RFC 9582 §5).
|
||||||
///
|
///
|
||||||
/// This implements the ROA/EE semantic checks from RFC 9582 §5 that do not
|
/// This performs the EE/payload semantic checks that do not require certificate path
|
||||||
/// require certificate path validation.
|
/// validation.
|
||||||
pub fn validate_against_ee_resources(&self, ee: &EeResources) -> Result<(), RoaValidateError> {
|
pub fn validate_against_ee_cert(&self, ee: &ResourceCertificate) -> Result<(), RoaValidateError> {
|
||||||
if ee.as_resources_present {
|
if ee.tbs.extensions.as_resources.is_some() {
|
||||||
return Err(RoaValidateError::EeAsResourcesPresent);
|
return Err(RoaValidateError::EeAsResourcesPresent);
|
||||||
}
|
}
|
||||||
if ee.ip_resources_inherit {
|
|
||||||
|
let ip = ee
|
||||||
|
.tbs
|
||||||
|
.extensions
|
||||||
|
.ip_resources
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(RoaValidateError::EeIpResourcesMissing)?;
|
||||||
|
|
||||||
|
if ip.has_any_inherit() {
|
||||||
return Err(RoaValidateError::EeIpResourcesInherit);
|
return Err(RoaValidateError::EeIpResourcesInherit);
|
||||||
}
|
}
|
||||||
|
|
||||||
for fam in &self.ip_addr_blocks {
|
for fam in &self.ip_addr_blocks {
|
||||||
for entry in &fam.addresses {
|
for entry in &fam.addresses {
|
||||||
if !ee.ip_resources.contains_prefix(&entry.prefix) {
|
let rc_prefix = roa_prefix_to_rc(&entry.prefix);
|
||||||
|
if !ip.contains_prefix(&rc_prefix) {
|
||||||
return Err(RoaValidateError::PrefixNotInEeResources {
|
return Err(RoaValidateError::PrefixNotInEeResources {
|
||||||
afi: entry.prefix.afi,
|
afi: entry.prefix.afi,
|
||||||
addr: entry.prefix.addr.clone(),
|
addr: entry.prefix.addr.clone(),
|
||||||
@ -274,6 +273,18 @@ impl RoaEContent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
fn parse_ip_addr_blocks(obj: &DerObject<'_>) -> Result<Vec<RoaIpAddressFamily>, RoaDecodeError> {
|
||||||
let seq = obj
|
let seq = obj
|
||||||
.as_sequence()
|
.as_sequence()
|
||||||
@ -434,33 +445,3 @@ fn canonicalize_prefix_addr(afi: RoaAfi, prefix_len: u16, bytes: &[u8]) -> Vec<u
|
|||||||
}
|
}
|
||||||
addr
|
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 full_len = match resource.afi {
|
|
||||||
RoaAfi::Ipv4 => 4,
|
|
||||||
RoaAfi::Ipv6 => 16,
|
|
||||||
};
|
|
||||||
if resource.addr.len() != full_len || subject.addr.len() != full_len {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let n = resource.prefix_len as usize;
|
|
||||||
let whole = n / 8;
|
|
||||||
let rem = (n % 8) as u8;
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -2,17 +2,15 @@ use crate::data_model::common::{Asn1TimeEncoding, Asn1TimeUtc};
|
|||||||
use crate::data_model::oid::{
|
use crate::data_model::oid::{
|
||||||
OID_CMS_ATTR_CONTENT_TYPE, OID_CMS_ATTR_MESSAGE_DIGEST, OID_CMS_ATTR_SIGNING_TIME,
|
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_RSA_ENCRYPTION, OID_SHA256, OID_SHA256_WITH_RSA_ENCRYPTION, OID_SIGNED_DATA,
|
||||||
OID_AD_SIGNED_OBJECT, OID_SUBJECT_INFO_ACCESS, OID_SUBJECT_KEY_IDENTIFIER,
|
OID_AD_SIGNED_OBJECT, OID_SUBJECT_INFO_ACCESS,
|
||||||
};
|
};
|
||||||
|
use crate::data_model::rc::{ResourceCertificate, SubjectInfoAccess};
|
||||||
use der_parser::ber::Class;
|
use der_parser::ber::Class;
|
||||||
use der_parser::der::{parse_der, DerObject, Tag};
|
use der_parser::der::{parse_der, DerObject, Tag};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use x509_parser::extensions::GeneralName;
|
|
||||||
use x509_parser::extensions::ParsedExtension;
|
|
||||||
use x509_parser::public_key::PublicKey;
|
use x509_parser::public_key::PublicKey;
|
||||||
use x509_parser::prelude::FromDer;
|
use x509_parser::prelude::FromDer;
|
||||||
use x509_parser::x509::SubjectPublicKeyInfo;
|
use x509_parser::x509::SubjectPublicKeyInfo;
|
||||||
use x509_parser::certificate::X509Certificate;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct ResourceEeCertificate {
|
pub struct ResourceEeCertificate {
|
||||||
@ -20,6 +18,7 @@ pub struct ResourceEeCertificate {
|
|||||||
pub subject_key_identifier: Vec<u8>,
|
pub subject_key_identifier: Vec<u8>,
|
||||||
pub spki_der: Vec<u8>,
|
pub spki_der: Vec<u8>,
|
||||||
pub sia_signed_object_uris: Vec<String>,
|
pub sia_signed_object_uris: Vec<String>,
|
||||||
|
pub resource_cert: ResourceCertificate,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
@ -67,121 +66,121 @@ pub struct SignedAttrsProfiled {
|
|||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum SignedObjectDecodeError {
|
pub enum SignedObjectDecodeError {
|
||||||
#[error("DER parse error: {0}")]
|
#[error("DER parse error: {0} (RFC 6488 §2; RFC 6488 §3(1l); RFC 5652 §3/§5)")]
|
||||||
Parse(String),
|
Parse(String),
|
||||||
|
|
||||||
#[error("trailing bytes after DER object: {0} bytes")]
|
#[error("trailing bytes after DER object: {0} bytes (DER; RFC 6488 §3(1l))")]
|
||||||
TrailingBytes(usize),
|
TrailingBytes(usize),
|
||||||
|
|
||||||
#[error("ContentInfo.contentType must be SignedData ({OID_SIGNED_DATA}), got {0}")]
|
#[error("ContentInfo.contentType must be SignedData ({OID_SIGNED_DATA}), got {0} (RFC 6488 §3(1a); RFC 5652 §3)")]
|
||||||
InvalidContentInfoContentType(String),
|
InvalidContentInfoContentType(String),
|
||||||
|
|
||||||
#[error("SignedData.version must be 3, got {0}")]
|
#[error("SignedData.version must be 3, got {0} (RFC 6488 §2.1.1; RFC 6488 §3(1b); RFC 5652 §5.1)")]
|
||||||
InvalidSignedDataVersion(u64),
|
InvalidSignedDataVersion(u64),
|
||||||
|
|
||||||
#[error("SignedData.digestAlgorithms must contain exactly one AlgorithmIdentifier, got {0}")]
|
#[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),
|
InvalidDigestAlgorithmsCount(usize),
|
||||||
|
|
||||||
#[error("digest algorithm must be id-sha256 ({OID_SHA256}), got {0}")]
|
#[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),
|
InvalidDigestAlgorithm(String),
|
||||||
|
|
||||||
#[error("SignedData.certificates MUST be present")]
|
#[error("SignedData.certificates MUST be present (RFC 6488 §3(1c); RFC 5652 §5.1)")]
|
||||||
CertificatesMissing,
|
CertificatesMissing,
|
||||||
|
|
||||||
#[error("SignedData.certificates must contain exactly one EE certificate, got {0}")]
|
#[error("SignedData.certificates must contain exactly one EE certificate, got {0} (RFC 6488 §3(1c))")]
|
||||||
InvalidCertificatesCount(usize),
|
InvalidCertificatesCount(usize),
|
||||||
|
|
||||||
#[error("SignedData.crls MUST be omitted")]
|
#[error("SignedData.crls MUST be omitted (RFC 6488 §3(1d))")]
|
||||||
CrlsPresent,
|
CrlsPresent,
|
||||||
|
|
||||||
#[error("SignedData.signerInfos must contain exactly one SignerInfo, got {0}")]
|
#[error("SignedData.signerInfos must contain exactly one SignerInfo, got {0} (RFC 6488 §2.1; RFC 6488 §3(1e); RFC 5652 §5.1)")]
|
||||||
InvalidSignerInfosCount(usize),
|
InvalidSignerInfosCount(usize),
|
||||||
|
|
||||||
#[error("SignerInfo.version must be 3, got {0}")]
|
#[error("SignerInfo.version must be 3, got {0} (RFC 6488 §3(1e); RFC 5652 §5.3)")]
|
||||||
InvalidSignerInfoVersion(u64),
|
InvalidSignerInfoVersion(u64),
|
||||||
|
|
||||||
#[error("SignerInfo.sid must be subjectKeyIdentifier [0]")]
|
#[error("SignerInfo.sid must be subjectKeyIdentifier [0] (RFC 6488 §3(1c); RFC 5652 §5.3)")]
|
||||||
InvalidSignerIdentifier,
|
InvalidSignerIdentifier,
|
||||||
|
|
||||||
#[error("SignerInfo.digestAlgorithm must be id-sha256 ({OID_SHA256}), got {0}")]
|
#[error("SignerInfo.digestAlgorithm must be id-sha256 ({OID_SHA256}), got {0} (RFC 6488 §3(1j); RFC 7935 §2)")]
|
||||||
InvalidSignerInfoDigestAlgorithm(String),
|
InvalidSignerInfoDigestAlgorithm(String),
|
||||||
|
|
||||||
#[error("SignerInfo.signedAttrs MUST be present")]
|
#[error("SignerInfo.signedAttrs MUST be present (RFC 9589 §4; RFC 6488 §3(1f))")]
|
||||||
SignedAttrsMissing,
|
SignedAttrsMissing,
|
||||||
|
|
||||||
#[error("SignerInfo.unsignedAttrs MUST be omitted")]
|
#[error("SignerInfo.unsignedAttrs MUST be omitted (RFC 6488 §3(1i))")]
|
||||||
UnsignedAttrsPresent,
|
UnsignedAttrsPresent,
|
||||||
|
|
||||||
#[error(
|
#[error(
|
||||||
"SignerInfo.signatureAlgorithm must be rsaEncryption ({OID_RSA_ENCRYPTION}) or \
|
"SignerInfo.signatureAlgorithm must be rsaEncryption ({OID_RSA_ENCRYPTION}) or \
|
||||||
sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0}"
|
sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0} (RFC 6488 §3(1k); RFC 7935 §2)"
|
||||||
)]
|
)]
|
||||||
InvalidSignatureAlgorithm(String),
|
InvalidSignatureAlgorithm(String),
|
||||||
|
|
||||||
#[error("SignerInfo.signatureAlgorithm parameters must be absent or NULL")]
|
#[error("SignerInfo.signatureAlgorithm parameters must be absent or NULL (RFC 5280 §4.1.1.2; RFC 7935 §2)")]
|
||||||
InvalidSignatureAlgorithmParameters,
|
InvalidSignatureAlgorithmParameters,
|
||||||
|
|
||||||
#[error("signedAttrs contains unsupported attribute OID {0}")]
|
#[error("signedAttrs contains unsupported attribute OID {0} (RFC 9589 §4; RFC 6488 §2.1.6.4)")]
|
||||||
UnsupportedSignedAttribute(String),
|
UnsupportedSignedAttribute(String),
|
||||||
|
|
||||||
#[error("signedAttrs contains duplicate attribute OID {0}")]
|
#[error("signedAttrs contains duplicate attribute OID {0} (RFC 6488 §2.1.6.4; RFC 9589 §4)")]
|
||||||
DuplicateSignedAttribute(String),
|
DuplicateSignedAttribute(String),
|
||||||
|
|
||||||
#[error("signedAttrs attribute {oid} attrValues must contain exactly one value, got {count}")]
|
#[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 },
|
InvalidSignedAttributeValuesCount { oid: String, count: usize },
|
||||||
|
|
||||||
#[error("signedAttrs.content-type attrValues must equal eContentType ({econtent_type}), got {attr_content_type}")]
|
#[error("signedAttrs.content-type attrValues must equal eContentType ({econtent_type}), got {attr_content_type} (RFC 6488 §3(1h); RFC 9589 §4)")]
|
||||||
ContentTypeAttrMismatch {
|
ContentTypeAttrMismatch {
|
||||||
econtent_type: String,
|
econtent_type: String,
|
||||||
attr_content_type: String,
|
attr_content_type: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("EncapsulatedContentInfo.eContent MUST be present")]
|
#[error("EncapsulatedContentInfo.eContent MUST be present (RFC 6488 §2.1.3; RFC 5652 §5.2)")]
|
||||||
EContentMissing,
|
EContentMissing,
|
||||||
|
|
||||||
#[error("signedAttrs.message-digest does not match SHA-256(eContent)")]
|
#[error("signedAttrs.message-digest does not match SHA-256(eContent) (RFC 6488 §3(1f); RFC 5652 §11.2)")]
|
||||||
MessageDigestMismatch,
|
MessageDigestMismatch,
|
||||||
|
|
||||||
#[error("EE certificate parse error: {0}")]
|
#[error("EE certificate parse error: {0} (RFC 6488 §3(1c); RFC 6487 §4)")]
|
||||||
EeCertificateParse(String),
|
EeCertificateParse(String),
|
||||||
|
|
||||||
#[error("EE certificate missing SubjectKeyIdentifier extension")]
|
#[error("EE certificate missing SubjectKeyIdentifier extension (RFC 6488 §3(1c); RFC 6487 §4.8.2)")]
|
||||||
EeCertificateMissingSki,
|
EeCertificateMissingSki,
|
||||||
|
|
||||||
#[error("EE certificate missing SubjectInfoAccess extension ({OID_SUBJECT_INFO_ACCESS})")]
|
#[error("EE certificate missing SubjectInfoAccess extension ({OID_SUBJECT_INFO_ACCESS}) (RFC 6487 §4.8.8.2)")]
|
||||||
EeCertificateMissingSia,
|
EeCertificateMissingSia,
|
||||||
|
|
||||||
#[error("EE certificate SIA missing id-ad-signedObject access method ({OID_AD_SIGNED_OBJECT})")]
|
#[error("EE certificate SIA missing id-ad-signedObject access method ({OID_AD_SIGNED_OBJECT}) (RFC 6487 §4.8.8.2)")]
|
||||||
EeCertificateMissingSignedObjectSia,
|
EeCertificateMissingSignedObjectSia,
|
||||||
|
|
||||||
#[error("EE certificate SIA id-ad-signedObject accessLocation must be a URI")]
|
#[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,
|
EeCertificateSignedObjectSiaNotUri,
|
||||||
|
|
||||||
#[error("EE certificate SIA id-ad-signedObject must include at least one rsync:// URI")]
|
#[error("EE certificate SIA id-ad-signedObject must include at least one rsync:// URI (RFC 6487 §4.8.8.2)")]
|
||||||
EeCertificateSignedObjectSiaNoRsync,
|
EeCertificateSignedObjectSiaNoRsync,
|
||||||
|
|
||||||
#[error("SignerInfo.sid SKI does not match EE certificate SKI")]
|
#[error("SignerInfo.sid SKI does not match EE certificate SKI (RFC 6488 §3(1c); RFC 5652 §5.3)")]
|
||||||
SidSkiMismatch,
|
SidSkiMismatch,
|
||||||
|
|
||||||
#[error("invalid signing-time attribute value (expected UTCTime or GeneralizedTime)")]
|
#[error("invalid signing-time attribute value (expected UTCTime or GeneralizedTime) (RFC 5652 §11.3; RFC 9589 §4)")]
|
||||||
InvalidSigningTimeValue,
|
InvalidSigningTimeValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum SignedObjectVerifyError {
|
pub enum SignedObjectVerifyError {
|
||||||
#[error("EE SubjectPublicKeyInfo parse error: {0}")]
|
#[error("EE SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7)")]
|
||||||
EeSpkiParse(String),
|
EeSpkiParse(String),
|
||||||
|
|
||||||
#[error("trailing bytes after EE SubjectPublicKeyInfo DER: {0} bytes")]
|
#[error("trailing bytes after EE SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 5280 §4.1.2.7)")]
|
||||||
EeSpkiTrailingBytes(usize),
|
EeSpkiTrailingBytes(usize),
|
||||||
|
|
||||||
#[error("unsupported EE public key algorithm (only RSA supported in M3)")]
|
#[error("unsupported EE public key algorithm (only RSA supported in M3) (RFC 7935 §2)")]
|
||||||
UnsupportedEePublicKeyAlgorithm,
|
UnsupportedEePublicKeyAlgorithm,
|
||||||
|
|
||||||
#[error("EE RSA public exponent invalid")]
|
#[error("EE RSA public exponent invalid (RFC 8017 §A.1.1; RFC 7935 §2)")]
|
||||||
InvalidEeRsaExponent,
|
InvalidEeRsaExponent,
|
||||||
|
|
||||||
#[error("signature verification failed")]
|
#[error("signature verification failed (RFC 6488 §3(2)-(3); RFC 5652 §5.3; RFC 7935 §2)")]
|
||||||
InvalidSignature,
|
InvalidSignature,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -463,80 +462,60 @@ fn parse_certificate_set_implicit(obj: &DerObject<'_>) -> Result<Vec<ResourceEeC
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_ee_certificate(der: &[u8]) -> Result<ResourceEeCertificate, SignedObjectDecodeError> {
|
fn parse_ee_certificate(der: &[u8]) -> Result<ResourceEeCertificate, SignedObjectDecodeError> {
|
||||||
let (rem, cert) = X509Certificate::from_der(der)
|
let rc = match ResourceCertificate::from_der(der) {
|
||||||
.map_err(|e| SignedObjectDecodeError::EeCertificateParse(e.to_string()))?;
|
Ok(v) => v,
|
||||||
let _ = rem;
|
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 = cert
|
let ski = rc
|
||||||
.extensions()
|
.tbs
|
||||||
.iter()
|
.extensions
|
||||||
.find(|ext| ext.oid.to_id_string() == OID_SUBJECT_KEY_IDENTIFIER)
|
.subject_key_identifier
|
||||||
.and_then(|ext| match ext.parsed_extension() {
|
.clone()
|
||||||
ParsedExtension::SubjectKeyIdentifier(ki) => Some(ki.0.to_vec()),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.ok_or(SignedObjectDecodeError::EeCertificateMissingSki)?;
|
.ok_or(SignedObjectDecodeError::EeCertificateMissingSki)?;
|
||||||
|
|
||||||
let spki_der = cert.public_key().raw.to_vec();
|
let spki_der = rc.tbs.subject_public_key_info.clone();
|
||||||
|
|
||||||
let sia_signed_object_uris = parse_ee_sia_signed_object_uris(&cert)?;
|
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 {
|
Ok(ResourceEeCertificate {
|
||||||
raw_der: der.to_vec(),
|
raw_der: der.to_vec(),
|
||||||
subject_key_identifier: ski,
|
subject_key_identifier: ski,
|
||||||
spki_der,
|
spki_der,
|
||||||
sia_signed_object_uris,
|
sia_signed_object_uris: signed_object_uris,
|
||||||
|
resource_cert: rc,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_ee_sia_signed_object_uris(
|
|
||||||
cert: &X509Certificate<'_>,
|
|
||||||
) -> Result<Vec<String>, SignedObjectDecodeError> {
|
|
||||||
let mut sia: Option<x509_parser::extensions::SubjectInfoAccess<'_>> = None;
|
|
||||||
for ext in cert.extensions() {
|
|
||||||
if ext.oid.to_id_string() != OID_SUBJECT_INFO_ACCESS {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
match ext.parsed_extension() {
|
|
||||||
ParsedExtension::SubjectInfoAccess(s) => {
|
|
||||||
if sia.is_some() {
|
|
||||||
return Err(SignedObjectDecodeError::Parse(
|
|
||||||
"duplicate SubjectInfoAccess extensions".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
sia = Some(s.clone());
|
|
||||||
}
|
|
||||||
ParsedExtension::ParseError { error } => {
|
|
||||||
return Err(SignedObjectDecodeError::Parse(error.to_string()));
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Err(SignedObjectDecodeError::Parse(
|
|
||||||
"SubjectInfoAccess extension parse failed".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let sia = sia.ok_or(SignedObjectDecodeError::EeCertificateMissingSia)?;
|
|
||||||
let mut uris: Vec<String> = Vec::new();
|
|
||||||
for ad in sia.iter() {
|
|
||||||
if ad.access_method.to_id_string() != OID_AD_SIGNED_OBJECT {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
match &ad.access_location {
|
|
||||||
GeneralName::URI(u) => uris.push((*u).to_string()),
|
|
||||||
_ => return Err(SignedObjectDecodeError::EeCertificateSignedObjectSiaNotUri),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if uris.is_empty() {
|
|
||||||
return Err(SignedObjectDecodeError::EeCertificateMissingSignedObjectSia);
|
|
||||||
}
|
|
||||||
if !uris.iter().any(|u| u.starts_with("rsync://")) {
|
|
||||||
return Err(SignedObjectDecodeError::EeCertificateSignedObjectSiaNoRsync);
|
|
||||||
}
|
|
||||||
Ok(uris)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_signer_info(obj: &DerObject<'_>) -> Result<SignerInfoProfiled, SignedObjectDecodeError> {
|
fn parse_signer_info(obj: &DerObject<'_>) -> Result<SignerInfoProfiled, SignedObjectDecodeError> {
|
||||||
let seq = obj
|
let seq = obj
|
||||||
.as_sequence()
|
.as_sequence()
|
||||||
|
|||||||
@ -1,27 +1,188 @@
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
use x509_parser::prelude::*;
|
use x509_parser::prelude::{FromDer, X509Certificate};
|
||||||
|
|
||||||
use crate::data_model::resources::resource::ResourceSet;
|
use crate::data_model::oid::OID_CP_IPADDR_ASNUMBER;
|
||||||
|
use crate::data_model::rc::{AsIdentifierChoice, IpAddressChoice, ResourceCertKind, ResourceCertificate};
|
||||||
|
use crate::data_model::tal::Tal;
|
||||||
|
|
||||||
//
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
// #[derive(Debug, Clone)]
|
pub struct TaCertificate {
|
||||||
// pub struct TrustAnchorCert {
|
pub raw_der: Vec<u8>,
|
||||||
// /// 信任锚证书名称
|
pub rc_ca: ResourceCertificate,
|
||||||
// pub name: String,
|
}
|
||||||
//
|
|
||||||
// /// 证书原始DER内容
|
#[derive(Debug, thiserror::Error)]
|
||||||
// pub cert_der: Vec<u8>,
|
pub enum TaCertificateError {
|
||||||
//
|
#[error("TA certificate parse error: {0} (RFC 5280 §4.1; RFC 6487 §4; RFC 8630 §2.3)")]
|
||||||
// /// 证书
|
Parse(String),
|
||||||
// pub cert: X509Certificate<'static>,
|
|
||||||
//
|
#[error("trailing bytes after TA certificate DER: {0} bytes (DER; RFC 5280 §4.1; RFC 6487 §4)")]
|
||||||
// /// 资源集合
|
TrailingBytes(usize),
|
||||||
// pub resources: ResourceSet,
|
|
||||||
//
|
#[error("TA certificate must be a CA certificate (RFC 8630 §2.3; RFC 6487 §4.8.1)")]
|
||||||
// ///发布点
|
NotCa,
|
||||||
// pub publication_point: Url,
|
|
||||||
// }
|
#[error("TA certificate must be self-signed (issuer DN must equal subject DN) (RFC 8630 §2.3; RFC 5280 §4.1.2.4)")]
|
||||||
//
|
NotSelfSignedIssuerSubject,
|
||||||
// impl TrustAnchorCert {
|
|
||||||
//
|
#[error("TA certificate self-signature verification failed: {0} (RFC 8630 §2.3; RFC 5280 §6.1)")]
|
||||||
// }
|
InvalidSelfSignature(String),
|
||||||
|
|
||||||
|
#[error("TA certificate must contain certificatePolicies ipAddr-asNumber ({OID_CP_IPADDR_ASNUMBER}) (RFC 6487 §4.8.9; RFC 8630 §2.3)")]
|
||||||
|
MissingOrInvalidCertificatePolicies,
|
||||||
|
|
||||||
|
#[error("TA certificate must contain SubjectKeyIdentifier (RFC 6487 §4.8.2; RFC 8630 §2.3)")]
|
||||||
|
MissingSubjectKeyIdentifier,
|
||||||
|
|
||||||
|
#[error("TA certificate must contain at least one RFC 3779 resource extension (IP or AS) (RFC 6487 §4.8.10-§4.8.11; RFC 8630 §2.3)")]
|
||||||
|
ResourcesMissing,
|
||||||
|
|
||||||
|
#[error("TA certificate resources must be non-empty (RFC 8630 §2.3)")]
|
||||||
|
ResourcesEmpty,
|
||||||
|
|
||||||
|
#[error("TA certificate MUST NOT use inherit in IP resources (RFC 8630 §2.3; RFC 3779 §2.2.3.5)")]
|
||||||
|
IpResourcesInherit,
|
||||||
|
|
||||||
|
#[error("TA certificate MUST NOT use inherit in AS resources (RFC 8630 §2.3; RFC 3779 §3.2.3.3)")]
|
||||||
|
AsResourcesInherit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TaCertificate {
|
||||||
|
pub fn from_der(der: &[u8]) -> Result<Self, TaCertificateError> {
|
||||||
|
let rc_ca = ResourceCertificate::from_der(der).map_err(|e| TaCertificateError::Parse(e.to_string()))?;
|
||||||
|
if rc_ca.kind != ResourceCertKind::Ca {
|
||||||
|
return Err(TaCertificateError::NotCa);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strong self-signed check: issuer==subject AND signature verifies with its own SPKI.
|
||||||
|
let (rem, cert) =
|
||||||
|
X509Certificate::from_der(der).map_err(|e| TaCertificateError::Parse(e.to_string()))?;
|
||||||
|
if !rem.is_empty() {
|
||||||
|
return Err(TaCertificateError::TrailingBytes(rem.len()));
|
||||||
|
}
|
||||||
|
if cert.issuer().to_string() != cert.subject().to_string() {
|
||||||
|
return Err(TaCertificateError::NotSelfSignedIssuerSubject);
|
||||||
|
}
|
||||||
|
cert.verify_signature(None)
|
||||||
|
.map_err(|e| TaCertificateError::InvalidSelfSignature(e.to_string()))?;
|
||||||
|
|
||||||
|
Self::validate_rc_constraints(&rc_ca)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
raw_der: der.to_vec(),
|
||||||
|
rc_ca,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spki_der(&self) -> &[u8] {
|
||||||
|
&self.rc_ca.tbs.subject_public_key_info
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate TA-specific semantic constraints on a parsed Resource Certificate.
|
||||||
|
///
|
||||||
|
/// Note: this does not verify the X.509 signature; it is intended for higher-level logic and
|
||||||
|
/// for unit tests that exercise individual constraint branches.
|
||||||
|
pub fn validate_rc_constraints(rc_ca: &ResourceCertificate) -> Result<(), TaCertificateError> {
|
||||||
|
if rc_ca.kind != ResourceCertKind::Ca {
|
||||||
|
return Err(TaCertificateError::NotCa);
|
||||||
|
}
|
||||||
|
|
||||||
|
if rc_ca.tbs.extensions.certificate_policies_oid.as_deref() != Some(OID_CP_IPADDR_ASNUMBER) {
|
||||||
|
return Err(TaCertificateError::MissingOrInvalidCertificatePolicies);
|
||||||
|
}
|
||||||
|
if rc_ca.tbs.extensions.subject_key_identifier.is_none() {
|
||||||
|
return Err(TaCertificateError::MissingSubjectKeyIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ip = rc_ca.tbs.extensions.ip_resources.as_ref();
|
||||||
|
let asn = rc_ca.tbs.extensions.as_resources.as_ref();
|
||||||
|
if ip.is_none() && asn.is_none() {
|
||||||
|
return Err(TaCertificateError::ResourcesMissing);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut has_any_resource = false;
|
||||||
|
|
||||||
|
if let Some(ip) = ip {
|
||||||
|
if ip.has_any_inherit() {
|
||||||
|
return Err(TaCertificateError::IpResourcesInherit);
|
||||||
|
}
|
||||||
|
for fam in &ip.families {
|
||||||
|
match &fam.choice {
|
||||||
|
IpAddressChoice::Inherit => return Err(TaCertificateError::IpResourcesInherit),
|
||||||
|
IpAddressChoice::AddressesOrRanges(items) => {
|
||||||
|
if !items.is_empty() {
|
||||||
|
has_any_resource = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(asn) = asn {
|
||||||
|
if matches!(asn.asnum, Some(AsIdentifierChoice::Inherit))
|
||||||
|
|| matches!(asn.rdi, Some(AsIdentifierChoice::Inherit))
|
||||||
|
{
|
||||||
|
return Err(TaCertificateError::AsResourcesInherit);
|
||||||
|
}
|
||||||
|
if let Some(AsIdentifierChoice::AsIdsOrRanges(items)) = asn.asnum.as_ref() {
|
||||||
|
if !items.is_empty() {
|
||||||
|
has_any_resource = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(AsIdentifierChoice::AsIdsOrRanges(items)) = asn.rdi.as_ref() {
|
||||||
|
if !items.is_empty() {
|
||||||
|
has_any_resource = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !has_any_resource {
|
||||||
|
return Err(TaCertificateError::ResourcesEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct TrustAnchor {
|
||||||
|
pub tal: Tal,
|
||||||
|
pub ta_certificate: TaCertificate,
|
||||||
|
pub resolved_ta_uri: Option<Url>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum TrustAnchorError {
|
||||||
|
#[error("TA certificate error: {0} (RFC 8630 §2.3)")]
|
||||||
|
TaCertificate(#[from] TaCertificateError),
|
||||||
|
|
||||||
|
#[error("resolved TA URI not listed in TAL: {0} (RFC 8630 §2.2-§2.3)")]
|
||||||
|
ResolvedUriNotInTal(String),
|
||||||
|
|
||||||
|
#[error("TAL SPKI does not match TA certificate SubjectPublicKeyInfo (RFC 8630 §2.3; RFC 5280 §4.1.2.7)")]
|
||||||
|
TalSpkiMismatch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrustAnchor {
|
||||||
|
/// Bind a TAL and a downloaded TA certificate.
|
||||||
|
///
|
||||||
|
/// This does not download anything; it only validates the binding rules from RFC 8630 §2.3.
|
||||||
|
pub fn bind(tal: Tal, ta_der: &[u8], resolved_uri: Option<&Url>) -> Result<Self, TrustAnchorError> {
|
||||||
|
if let Some(u) = resolved_uri {
|
||||||
|
if !tal.ta_uris.iter().any(|x| x == u) {
|
||||||
|
return Err(TrustAnchorError::ResolvedUriNotInTal(u.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ta_certificate = TaCertificate::from_der(ta_der)?;
|
||||||
|
if tal.subject_public_key_info_der != ta_certificate.spki_der() {
|
||||||
|
return Err(TrustAnchorError::TalSpkiMismatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
tal,
|
||||||
|
ta_certificate,
|
||||||
|
resolved_ta_uri: resolved_uri.cloned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,18 +1,134 @@
|
|||||||
/// TAL Model
|
use base64::Engine;
|
||||||
#[derive(Clone, Debug)]
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct Tal {
|
pub struct Tal {
|
||||||
/// Optional human-readable comments
|
pub raw: Vec<u8>,
|
||||||
pub comments: Vec<String>,
|
pub comments: Vec<String>,
|
||||||
|
pub ta_uris: Vec<Url>,
|
||||||
/// Ordered list of URIs pointing to the TA certificate
|
pub subject_public_key_info_der: Vec<u8>,
|
||||||
pub uris: Vec<TalUri>,
|
|
||||||
|
|
||||||
/// SubjectPublicKeyInfo DER
|
|
||||||
pub spki_der: Vec<u8>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum TalUri {
|
pub enum TalDecodeError {
|
||||||
Rsync(String),
|
#[error("TAL must be valid UTF-8 (RFC 8630 §2.2)")]
|
||||||
Https(String),
|
InvalidUtf8,
|
||||||
|
|
||||||
|
#[error("TAL comments must appear only at the beginning (RFC 8630 §2.2)")]
|
||||||
|
CommentAfterHeader,
|
||||||
|
|
||||||
|
#[error("TAL must contain at least one TA URI line (RFC 8630 §2.2)")]
|
||||||
|
MissingTaUris,
|
||||||
|
|
||||||
|
#[error("TAL must contain an empty line separator between URI list and SPKI base64 (RFC 8630 §2.2)")]
|
||||||
|
MissingSeparatorEmptyLine,
|
||||||
|
|
||||||
|
#[error("TAL TA URI invalid: {0} (RFC 8630 §2.2)")]
|
||||||
|
InvalidUri(String),
|
||||||
|
|
||||||
|
#[error("TAL TA URI scheme must be rsync or https, got {0} (RFC 8630 §2.2)")]
|
||||||
|
UnsupportedUriScheme(String),
|
||||||
|
|
||||||
|
#[error("TAL TA URI must reference a single object (must not end with '/'): {0} (RFC 8630 §2.3)")]
|
||||||
|
UriIsDirectory(String),
|
||||||
|
|
||||||
|
#[error("TAL must contain base64-encoded SubjectPublicKeyInfo after the separator (RFC 8630 §2.2)")]
|
||||||
|
MissingSpki,
|
||||||
|
|
||||||
|
#[error("TAL SPKI base64 decode failed (RFC 8630 §2.2)")]
|
||||||
|
SpkiBase64Decode,
|
||||||
|
|
||||||
|
#[error("TAL SPKI DER is empty (RFC 8630 §2.2)")]
|
||||||
|
SpkiDerEmpty,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tal {
|
||||||
|
pub fn decode_bytes(input: &[u8]) -> Result<Self, TalDecodeError> {
|
||||||
|
let raw = input.to_vec();
|
||||||
|
let text = std::str::from_utf8(input).map_err(|_| TalDecodeError::InvalidUtf8)?;
|
||||||
|
|
||||||
|
let lines: Vec<&str> = text
|
||||||
|
.split('\n')
|
||||||
|
.map(|l| l.strip_suffix('\r').unwrap_or(l))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut idx = 0usize;
|
||||||
|
|
||||||
|
// 1) Leading comments.
|
||||||
|
let mut comments: Vec<String> = Vec::new();
|
||||||
|
while idx < lines.len() && lines[idx].starts_with('#') {
|
||||||
|
comments.push(lines[idx][1..].to_string());
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) URI list (one or more non-empty lines).
|
||||||
|
let mut ta_uris: Vec<Url> = Vec::new();
|
||||||
|
while idx < lines.len() {
|
||||||
|
let line = lines[idx].trim();
|
||||||
|
if line.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if line.starts_with('#') {
|
||||||
|
return Err(TalDecodeError::CommentAfterHeader);
|
||||||
|
}
|
||||||
|
let url = match Url::parse(line) {
|
||||||
|
Ok(u) => u,
|
||||||
|
Err(_) => {
|
||||||
|
if !ta_uris.is_empty() {
|
||||||
|
return Err(TalDecodeError::MissingSeparatorEmptyLine);
|
||||||
|
}
|
||||||
|
return Err(TalDecodeError::InvalidUri(line.to_string()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match url.scheme() {
|
||||||
|
"rsync" | "https" => {}
|
||||||
|
s => return Err(TalDecodeError::UnsupportedUriScheme(s.to_string())),
|
||||||
|
}
|
||||||
|
if url.path().ends_with('/') {
|
||||||
|
return Err(TalDecodeError::UriIsDirectory(line.to_string()));
|
||||||
|
}
|
||||||
|
if url.path_segments().and_then(|mut s| s.next_back()).unwrap_or("").is_empty() {
|
||||||
|
return Err(TalDecodeError::UriIsDirectory(line.to_string()));
|
||||||
|
}
|
||||||
|
ta_uris.push(url);
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ta_uris.is_empty() {
|
||||||
|
return Err(TalDecodeError::MissingTaUris);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Empty line separator (must exist).
|
||||||
|
if idx >= lines.len() || !lines[idx].trim().is_empty() {
|
||||||
|
return Err(TalDecodeError::MissingSeparatorEmptyLine);
|
||||||
|
}
|
||||||
|
idx += 1;
|
||||||
|
|
||||||
|
// 4) Base64(SPKI DER) remainder; allow line wrapping.
|
||||||
|
let mut b64 = String::new();
|
||||||
|
while idx < lines.len() {
|
||||||
|
let line = lines[idx].trim();
|
||||||
|
if !line.is_empty() {
|
||||||
|
b64.push_str(line);
|
||||||
|
}
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
if b64.is_empty() {
|
||||||
|
return Err(TalDecodeError::MissingSpki);
|
||||||
|
}
|
||||||
|
|
||||||
|
let spki_der = base64::engine::general_purpose::STANDARD
|
||||||
|
.decode(b64.as_bytes())
|
||||||
|
.map_err(|_| TalDecodeError::SpkiBase64Decode)?;
|
||||||
|
if spki_der.is_empty() {
|
||||||
|
return Err(TalDecodeError::SpkiDerEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
raw,
|
||||||
|
comments,
|
||||||
|
ta_uris,
|
||||||
|
subject_public_key_info_der: spki_der,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
||||||
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");
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,4 +1,40 @@
|
|||||||
use rpki::data_model::aspa::{AspaEContent, AspaValidateError, EeAspaResources};
|
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 {
|
fn test_aspa() -> AspaEContent {
|
||||||
AspaEContent {
|
AspaEContent {
|
||||||
@ -11,74 +47,104 @@ fn test_aspa() -> AspaEContent {
|
|||||||
#[test]
|
#[test]
|
||||||
fn validate_accepts_when_customer_matches_ee_asid() {
|
fn validate_accepts_when_customer_matches_ee_asid() {
|
||||||
let aspa = test_aspa();
|
let aspa = test_aspa();
|
||||||
let ee = EeAspaResources {
|
let ee = dummy_ee(
|
||||||
as_id: Some(64496),
|
None,
|
||||||
as_resources_inherit: false,
|
Some(AsResourceSet {
|
||||||
as_resources_range_present: false,
|
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])),
|
||||||
ip_resources_present: false,
|
rdi: None,
|
||||||
};
|
}),
|
||||||
aspa.validate_against_ee_resources(&ee)
|
);
|
||||||
|
aspa.validate_against_ee_cert(&ee)
|
||||||
.expect("customer must match");
|
.expect("customer must match");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn validate_rejects_missing_as_resources() {
|
fn validate_rejects_missing_as_resources() {
|
||||||
let aspa = test_aspa();
|
let aspa = test_aspa();
|
||||||
let ee = EeAspaResources {
|
let ee = dummy_ee(None, None);
|
||||||
as_id: None,
|
let err = aspa.validate_against_ee_cert(&ee).unwrap_err();
|
||||||
as_resources_inherit: false,
|
|
||||||
as_resources_range_present: false,
|
|
||||||
ip_resources_present: false,
|
|
||||||
};
|
|
||||||
let err = aspa.validate_against_ee_resources(&ee).unwrap_err();
|
|
||||||
assert!(matches!(err, AspaValidateError::EeAsResourcesMissing));
|
assert!(matches!(err, AspaValidateError::EeAsResourcesMissing));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn validate_rejects_as_resources_inherit_or_ranges() {
|
fn validate_rejects_as_resources_inherit_or_ranges() {
|
||||||
let aspa = test_aspa();
|
let aspa = test_aspa();
|
||||||
let ee = EeAspaResources {
|
let ee = dummy_ee(
|
||||||
as_id: Some(64496),
|
None,
|
||||||
as_resources_inherit: true,
|
Some(AsResourceSet {
|
||||||
as_resources_range_present: false,
|
asnum: Some(AsIdentifierChoice::Inherit),
|
||||||
ip_resources_present: false,
|
rdi: None,
|
||||||
};
|
}),
|
||||||
let err = aspa.validate_against_ee_resources(&ee).unwrap_err();
|
);
|
||||||
|
let err = aspa.validate_against_ee_cert(&ee).unwrap_err();
|
||||||
assert!(matches!(err, AspaValidateError::EeAsResourcesInherit));
|
assert!(matches!(err, AspaValidateError::EeAsResourcesInherit));
|
||||||
|
|
||||||
let ee = EeAspaResources {
|
let ee = dummy_ee(
|
||||||
as_id: Some(64496),
|
None,
|
||||||
as_resources_inherit: false,
|
Some(AsResourceSet {
|
||||||
as_resources_range_present: true,
|
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range {
|
||||||
ip_resources_present: false,
|
min: 64496,
|
||||||
};
|
max: 64497,
|
||||||
let err = aspa.validate_against_ee_resources(&ee).unwrap_err();
|
}])),
|
||||||
|
rdi: None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let err = aspa.validate_against_ee_cert(&ee).unwrap_err();
|
||||||
assert!(matches!(err, AspaValidateError::EeAsResourcesRangePresent));
|
assert!(matches!(err, AspaValidateError::EeAsResourcesRangePresent));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn validate_rejects_customer_mismatch() {
|
fn validate_rejects_customer_mismatch() {
|
||||||
let aspa = test_aspa();
|
let aspa = test_aspa();
|
||||||
let ee = EeAspaResources {
|
let ee = dummy_ee(
|
||||||
as_id: Some(64511),
|
None,
|
||||||
as_resources_inherit: false,
|
Some(AsResourceSet {
|
||||||
as_resources_range_present: false,
|
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64511)])),
|
||||||
ip_resources_present: false,
|
rdi: None,
|
||||||
};
|
}),
|
||||||
let err = aspa.validate_against_ee_resources(&ee).unwrap_err();
|
);
|
||||||
|
let err = aspa.validate_against_ee_cert(&ee).unwrap_err();
|
||||||
assert!(matches!(err, AspaValidateError::CustomerAsIdMismatch { .. }));
|
assert!(matches!(err, AspaValidateError::CustomerAsIdMismatch { .. }));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn validate_rejects_ip_resources_present() {
|
fn validate_rejects_ip_resources_present() {
|
||||||
let aspa = test_aspa();
|
let aspa = test_aspa();
|
||||||
let ee = EeAspaResources {
|
let ee = dummy_ee(
|
||||||
as_id: Some(64496),
|
Some(IpResourceSet { families: vec![] }),
|
||||||
as_resources_inherit: false,
|
Some(AsResourceSet {
|
||||||
as_resources_range_present: false,
|
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])),
|
||||||
ip_resources_present: true,
|
rdi: None,
|
||||||
};
|
}),
|
||||||
let err = aspa.validate_against_ee_resources(&ee).unwrap_err();
|
);
|
||||||
|
let err = aspa.validate_against_ee_cert(&ee).unwrap_err();
|
||||||
assert!(matches!(err, AspaValidateError::EeIpResourcesPresent));
|
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_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]);
|
||||||
|
}
|
||||||
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");
|
||||||
|
}
|
||||||
@ -1,7 +1,40 @@
|
|||||||
use rpki::data_model::roa::{
|
use der_parser::num_bigint::BigUint;
|
||||||
EeResources, IpPrefix, IpResourceSet, RoaAfi, RoaEContent, RoaIpAddress, RoaIpAddressFamily,
|
use time::OffsetDateTime;
|
||||||
RoaValidateError,
|
|
||||||
|
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 {
|
fn test_roa_single_v4_prefix() -> RoaEContent {
|
||||||
RoaEContent {
|
RoaEContent {
|
||||||
@ -10,7 +43,7 @@ fn test_roa_single_v4_prefix() -> RoaEContent {
|
|||||||
ip_addr_blocks: vec![RoaIpAddressFamily {
|
ip_addr_blocks: vec![RoaIpAddressFamily {
|
||||||
afi: RoaAfi::Ipv4,
|
afi: RoaAfi::Ipv4,
|
||||||
addresses: vec![RoaIpAddress {
|
addresses: vec![RoaIpAddress {
|
||||||
prefix: IpPrefix {
|
prefix: rpki::data_model::roa::IpPrefix {
|
||||||
afi: RoaAfi::Ipv4,
|
afi: RoaAfi::Ipv4,
|
||||||
prefix_len: 8,
|
prefix_len: 8,
|
||||||
addr: vec![10, 0, 0, 0],
|
addr: vec![10, 0, 0, 0],
|
||||||
@ -24,85 +57,120 @@ fn test_roa_single_v4_prefix() -> RoaEContent {
|
|||||||
#[test]
|
#[test]
|
||||||
fn validate_accepts_when_prefix_is_covered() {
|
fn validate_accepts_when_prefix_is_covered() {
|
||||||
let roa = test_roa_single_v4_prefix();
|
let roa = test_roa_single_v4_prefix();
|
||||||
let ee = EeResources {
|
let ee = dummy_ee(
|
||||||
ip_resources: IpResourceSet {
|
Some(IpResourceSet {
|
||||||
prefixes: vec![IpPrefix {
|
families: vec![IpAddressFamily {
|
||||||
afi: RoaAfi::Ipv4,
|
afi: Afi::Ipv4,
|
||||||
|
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
||||||
|
IpPrefix {
|
||||||
|
afi: Afi::Ipv4,
|
||||||
prefix_len: 0,
|
prefix_len: 0,
|
||||||
addr: vec![0, 0, 0, 0],
|
addr: vec![0, 0, 0, 0],
|
||||||
}],
|
|
||||||
},
|
},
|
||||||
ip_resources_inherit: false,
|
)]),
|
||||||
as_resources_present: false,
|
}],
|
||||||
};
|
}),
|
||||||
roa.validate_against_ee_resources(&ee)
|
None,
|
||||||
|
);
|
||||||
|
roa.validate_against_ee_cert(&ee)
|
||||||
.expect("prefix should be covered by 0/0");
|
.expect("prefix should be covered by 0/0");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn validate_rejects_when_as_resources_present() {
|
fn validate_rejects_when_as_resources_present() {
|
||||||
let roa = test_roa_single_v4_prefix();
|
let roa = test_roa_single_v4_prefix();
|
||||||
let ee = EeResources {
|
let ee = dummy_ee(
|
||||||
ip_resources: IpResourceSet { prefixes: vec![] },
|
Some(IpResourceSet { families: vec![] }),
|
||||||
ip_resources_inherit: false,
|
Some(AsResourceSet {
|
||||||
as_resources_present: true,
|
asnum: None,
|
||||||
};
|
rdi: None,
|
||||||
let err = roa.validate_against_ee_resources(&ee).unwrap_err();
|
}),
|
||||||
|
);
|
||||||
|
let err = roa.validate_against_ee_cert(&ee).unwrap_err();
|
||||||
assert!(matches!(err, RoaValidateError::EeAsResourcesPresent));
|
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]
|
#[test]
|
||||||
fn validate_rejects_when_ip_resources_inherit() {
|
fn validate_rejects_when_ip_resources_inherit() {
|
||||||
let roa = test_roa_single_v4_prefix();
|
let roa = test_roa_single_v4_prefix();
|
||||||
let ee = EeResources {
|
let ee = dummy_ee(
|
||||||
ip_resources: IpResourceSet { prefixes: vec![] },
|
Some(IpResourceSet {
|
||||||
ip_resources_inherit: true,
|
families: vec![IpAddressFamily {
|
||||||
as_resources_present: false,
|
afi: Afi::Ipv4,
|
||||||
};
|
choice: IpAddressChoice::Inherit,
|
||||||
let err = roa.validate_against_ee_resources(&ee).unwrap_err();
|
}],
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let err = roa.validate_against_ee_cert(&ee).unwrap_err();
|
||||||
assert!(matches!(err, RoaValidateError::EeIpResourcesInherit));
|
assert!(matches!(err, RoaValidateError::EeIpResourcesInherit));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn validate_rejects_when_prefix_not_covered() {
|
fn validate_rejects_when_prefix_not_covered() {
|
||||||
let roa = test_roa_single_v4_prefix();
|
let roa = test_roa_single_v4_prefix();
|
||||||
let ee = EeResources {
|
let ee = dummy_ee(
|
||||||
ip_resources: IpResourceSet {
|
Some(IpResourceSet {
|
||||||
prefixes: vec![IpPrefix {
|
families: vec![IpAddressFamily {
|
||||||
afi: RoaAfi::Ipv4,
|
afi: Afi::Ipv4,
|
||||||
|
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
||||||
|
IpPrefix {
|
||||||
|
afi: Afi::Ipv4,
|
||||||
prefix_len: 24,
|
prefix_len: 24,
|
||||||
addr: vec![192, 0, 2, 0],
|
addr: vec![192, 0, 2, 0],
|
||||||
}],
|
|
||||||
},
|
},
|
||||||
ip_resources_inherit: false,
|
)]),
|
||||||
as_resources_present: false,
|
}],
|
||||||
};
|
}),
|
||||||
let err = roa.validate_against_ee_resources(&ee).unwrap_err();
|
None,
|
||||||
|
);
|
||||||
|
let err = roa.validate_against_ee_cert(&ee).unwrap_err();
|
||||||
assert!(matches!(err, RoaValidateError::PrefixNotInEeResources { .. }));
|
assert!(matches!(err, RoaValidateError::PrefixNotInEeResources { .. }));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn contains_prefix_handles_non_octet_boundary_prefix_len() {
|
fn contains_prefix_handles_non_octet_boundary_prefix_len() {
|
||||||
let ee_set = IpResourceSet {
|
let roa = RoaEContent {
|
||||||
prefixes: vec![IpPrefix {
|
version: 0,
|
||||||
|
as_id: 64496,
|
||||||
|
ip_addr_blocks: vec![RoaIpAddressFamily {
|
||||||
afi: RoaAfi::Ipv4,
|
afi: RoaAfi::Ipv4,
|
||||||
prefix_len: 9,
|
addresses: vec![RoaIpAddress {
|
||||||
addr: vec![0b1010_0000, 0, 0, 0], // 160.0.0.0/9
|
prefix: rpki::data_model::roa::IpPrefix {
|
||||||
}],
|
|
||||||
};
|
|
||||||
|
|
||||||
let covered = IpPrefix {
|
|
||||||
afi: RoaAfi::Ipv4,
|
afi: RoaAfi::Ipv4,
|
||||||
prefix_len: 16,
|
prefix_len: 16,
|
||||||
addr: vec![0b1010_0000, 0x12, 0, 0], // 160.18.0.0/16
|
addr: vec![0b1010_0000, 0x12, 0, 0], // 160.18.0.0/16
|
||||||
|
},
|
||||||
|
max_length: None,
|
||||||
|
}],
|
||||||
|
}],
|
||||||
};
|
};
|
||||||
assert!(ee_set.contains_prefix(&covered));
|
|
||||||
|
|
||||||
let not_covered = IpPrefix {
|
let ee = dummy_ee(
|
||||||
afi: RoaAfi::Ipv4,
|
Some(IpResourceSet {
|
||||||
prefix_len: 16,
|
families: vec![IpAddressFamily {
|
||||||
addr: vec![0b1010_0001, 0x12, 0, 0], // 161.18.0.0/16
|
afi: Afi::Ipv4,
|
||||||
};
|
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
||||||
assert!(!ee_set.contains_prefix(¬_covered));
|
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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