增加rc,完成所有模型的解析,优化error code 的RFC引用

This commit is contained in:
yuyr 2026-02-03 16:50:52 +08:00
parent 56ae2ca4fc
commit cc9f3f21de
47 changed files with 3837 additions and 1516 deletions

View File

@ -1,36 +1,94 @@
# 01. Trust Anchor Locator (TAL) # 01. TALTrust Anchor Locator
## 1.1 对象定位 ## 1.1 对象定位
TAL是一个数据格式/配置文件目的是告诉RP信任锚的公钥是什么以及相关对象可以从哪里获取。
## 1.2 数据格式 RFC 8630 §2.2 TALTrust 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 §2RFC 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 SPKIBase64
- `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.7RFC 7935 §2-§3.1RFC 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.3RFC 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 解码所得 DERBase64 中可有换行 | 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。

View File

@ -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. 更新本地存储库缓存。

View File

@ -0,0 +1,88 @@
# 02. TATrust 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.3RFC 6487 §4。
### 2.2.1 X.509 Certificate 的 ASN.1 定义RFC 5280 §4.1TA 与 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.1RPKI 对字段/扩展存在性与关键性约束见 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.3RFC 6487 §4。
- 自签名证书在 RC profile 下的通用差异(例如 CRLDP/AIA 的省略规则、AKI 的规则)见 RFC 6487。RFC 6487 §4.8.3RFC 6487 §4.8.6RFC 6487 §4.8.7。
### 2.3.2 INRIP/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.10RFC 6487 §4.8.11RFC 3779 §2.2.3.5RFC 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.3RFC 6487 §4 |
| `rc_ca` | `ResourceCaCertificate` | 以 RC(CA) 语义解析出的字段集合 | 必须满足“自签名 CA”分支约束且 INR 必须非空且不允许 inherit | RFC 8630 §2.3RFC 6487 §4RFC 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.3RFC 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.3RFC 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。

View File

@ -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 §4RFC 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-caRepositoryaccessLocation=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-caRepositoryaccessLocation=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 |

View File

@ -0,0 +1,460 @@
# 03. RCResource Certificate资源证书CA/EE
## 3.1 对象定位
资源证书RC是 X.509 v3 证书,遵循 PKIX profileRFC 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.1RFC 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 AlgorithmIdentifierASN.1RFC 5280 §4.1.1.2
```asn1
AlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL }
```
### 3.2.3 Name / DN 结构ASN.1RFC 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 / GeneralNameASN.1RFC 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 AIAAuthority Information AccessASN.1RFC 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 SIASubject Information AccessASN.1RFC 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 OIDRFC 6487 §4.8.8.1 / §4.8.8.2RFC 8182 §3.2
> 说明:下列 OID 用于 `AccessDescription.accessMethod`,并放在 SIA 的 `extnValue` 内层结构中(其外层 extnID 仍为 SIA`id-pe-subjectInfoAccess`。RFC 6487 §4.8.8RFC 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 CRLDistributionPointsCRLDPASN.1RFC 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 PoliciesASN.1RFC 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.1RFC 3779 §2.2.1-§2.2.3RFC 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.3RFC 5280 §4.2.1.9RFC 5280 §4.2.1.12
> 说明:这些是 RPKI 资源证书 profileRFC 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.1RFC 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.3RFC 7935 §2引用 RFC 4055 |
| `issuer_dn` | `RpkixDistinguishedName` | 颁发者 DN | 必含 1 个 CommonName可含 1 个 serialNumberCN 必须 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.1RFC 5280 §4.1.2.5 |
| `validity_not_after` | `UtcTime` | 有效期止 | X.509 `Time`UTCTime/GeneralizedTime解析为 UTC 时间点 | RFC 6487 §4.6.2RFC 5280 §4.1.2.5 |
| `subject_public_key_info` | `DerBytes` | SPKI DER | 算法 profile 指定 | RFC 6487 §4.7RFC 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.4RFC 6487 §4.5 |
| `serial_number` | `optional[string]` | serialNumber | MAY 存在且仅 1 个 | RFC 6487 §4.4RFC 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 & criticalEEMUST NOT presentpathLen MUST NOT present | RFC 6487 §4.8.1RFC 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 presentauthorityCertIssuer/authorityCertSerialNumber MUST NOT presentnon-critical | RFC 6487 §4.8.3RFC 5280 §4.2.1.1 |
| `key_usage` | `KeyUsage` | KeyUsage | **extnID=`2.5.29.15`**MUST present & criticalCA`keyCertSign``cRLSign` 为 TRUEEE`digitalSignature` 为 TRUE | RFC 6487 §4.8.4RFC 5280 §4.2.1.3 |
| `extended_key_usage` | `optional[OidSet]` | EKU | **extnID=`2.5.29.37`**CAMUST NOT appear用于验证 RPKI 对象的 EEMUST NOT appear若出现不得标 critical | RFC 6487 §4.8.5RFC 5280 §4.2.1.12 |
| `crl_distribution_points` | `optional[CrlDistributionPoints]` | CRLDP | **extnID=`2.5.29.31`**自签名MUST be omitted非自签名MUST present & non-critical仅 1 个 DistributionPointfullName URI必须包含至少 1 个 `rsync://` | RFC 6487 §4.8.6RFC 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.7RFC 5280 §4.2.2.1 |
| `subject_info_access_ca` | `optional[SubjectInfoAccessCa]` | SIACA | **extnID=`1.3.6.1.5.5.7.1.11`**CAMUST 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.1RFC 5280 §4.2.2.2RFC 8182 §3.2 |
| `subject_info_access_ee` | `optional[SubjectInfoAccessEe]` | SIAEE | **extnID=`1.3.6.1.5.5.7.1.11`**EEMUST 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.2RFC 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.9RFC 7318 §2RFC 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.10RFC 3779 §2.2.1RFC 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.11RFC 3779 §3.2.1RFC 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 presentRPKI 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.7RFC 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.1RFC 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.1RFC 5280 §4.2.2.2 |
| `rpki_notify_uris` | `optional[list[Uri]]` | RRDP NotificationUpdate Notification FileURI | accessMethod=`id-ad-rpkiNotify``1.3.6.1.5.5.7.48.13`);若存在则 accessLocation MUST 为 `https://` URI指向 RRDP Notification 文件 | RFC 8182 §3.2RFC 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.2RFC 5280 §4.2.2.2 |
#### `CertificatePolicies`
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|---|---|---|---|---|
| `policy_oid` | `Oid` | 唯一 policy OID | 恰好 1 个 policyRPKI CP 分配的 OID 为 `id-cp-ipAddr-asNumber``1.3.6.1.5.5.7.14.2` | RFC 6487 §4.8.9RFC 6484 §1.2 |
| `cps_uri` | `optional[Uri]` | CPS policy qualifier URI | MAY 存在且最多 1 个;若存在其 `policyQualifierId` 必为 `id-qt-cps`;对该 URI 不施加处理要求 | RFC 7318 §2RFC 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.1RFC 6487 §4.8.8.1RFC 6487 §4.8.8.2。
- KeyUsageCA 仅 `keyCertSign`/`cRLSign`EE 仅 `digitalSignature`。RFC 6487 §4.8.4。
- CRLDP/AIA自签名必须省略非自签名必须存在并包含 `rsync://`。RFC 6487 §4.8.6RFC 6487 §4.8.7。
- IP/AS 资源扩展:两者至少其一存在;若存在必须 critical语义来自 RFC 3779在公用互联网场景 SAFI 与 RDI 均不得使用。RFC 6487 §4.8.10RFC 6487 §4.8.11RFC 3779 §2.2.3RFC 3779 §3.2.3。

View File

@ -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(())
} }
} }

View File

@ -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,

View File

@ -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),
} }

View File

@ -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 {

View File

@ -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;

View File

@ -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";

View File

@ -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";

File diff suppressed because it is too large Load Diff

View File

@ -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
}
}

View File

@ -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);

View File

@ -1,3 +0,0 @@
pub(crate) mod ip_resources;
pub(crate) mod as_resources;
pub mod resource;

View File

@ -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,
}

View File

@ -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)
}

View File

@ -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()

View File

@ -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(),
})
}
}

View File

@ -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

Binary file not shown.

BIN
tests/fixtures/ta/apnic-ta.cer vendored Normal file

Binary file not shown.

BIN
tests/fixtures/ta/arin-ta.cer vendored Normal file

Binary file not shown.

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

Binary file not shown.

10
tests/fixtures/tal/afrinic.tal vendored Normal file
View 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

View 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
View 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
View 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
View 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

View 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");
}

View File

@ -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));
}

View 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");
}

View 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));
}

View 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
));
}

View 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
View 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));
}

View 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));
}

View 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());
}

View 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]);
}

View 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");
}

View File

@ -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(&not_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");
} }

View 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
View 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}");
}
}

View 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)));
}

View 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(_))
));
}