diff --git a/specs/01_tal.md b/specs/01_tal.md index 1502de1..3d99073 100644 --- a/specs/01_tal.md +++ b/specs/01_tal.md @@ -1,36 +1,94 @@ -# 01. Trust Anchor Locator (TAL) +# 01. TAL(Trust Anchor Locator) ## 1.1 对象定位 -TAL是一个数据格式/配置文件,目的是告诉RP信任锚的公钥是什么,以及相关对象可以从哪里获取。 -## 1.2 数据格式 (RFC 8630 §2.2) -TAL是一个配置文件,格式定义如下: -``` -The TAL is an ordered sequence of: -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, -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 -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. -Note that line breaks in this file can use either "" or "". +TAL(Trust Anchor Locator)用于向 RP 提供: + +1) 可检索“当前 TA 证书”的一个或多个 URI;以及 +2) 该 TA 证书的 `subjectPublicKeyInfo`(SPKI)期望值(用于绑定/防替换)。 + +RFC 8630 §2;RFC 8630 §2.3。 + +## 1.2 原始载体与编码 + +- 载体:文本文件(ASCII/UTF-8 兼容的行文本)。 +- 行结束:允许 `CRLF` 或 `LF`。RFC 8630 §2.2。 +- 结构:`[可选注释区] + URI 区 + 空行 + Base64(SPKI DER)`。RFC 8630 §2.2。 + +### 1.2.1 注释区 + +- 一行或多行,以 `#` 开头,后随人类可读 UTF-8 文本。RFC 8630 §2.2。 +- 注释行文本需符合 RFC 5198 §2 的限制(RFC 8630 §2.2 引用)。 + +### 1.2.2 URI 区 + +- 一行或多行,每行一个 TA URI,按序排列。RFC 8630 §2.2。 +- TA URI **MUST** 是 `rsync` 或 `https`。RFC 8630 §2.2。 + +### 1.2.3 空行分隔 + +- URI 区后必须有一个额外的换行(即空行),用于与 Base64 区分隔。RFC 8630 §2.2(第 3 点)。 + +### 1.2.4 SPKI(Base64) + +- `subjectPublicKeyInfo` 以 DER 编码(ASN.1)后,再 Base64 编码表示。RFC 8630 §2.2(第 4 点)。 +- 为避免长行,Base64 字符串中 **MAY** 插入换行。RFC 8630 §2.2。 +- SPKI ASN.1 类型来自 X.509 / RFC 5280。RFC 8630 §2.2(第 4 点);RFC 5280 §4.1.2.7。 + +#### 1.2.4.1 `SubjectPublicKeyInfo` 的 ASN.1 定义(RFC 5280 §4.1) + +TAL 中携带的是一个 X.509 `SubjectPublicKeyInfo` 的 DER 字节串(再 Base64)。其 ASN.1 定义如下:RFC 5280 §4.1。 + +```asn1 +SubjectPublicKeyInfo ::= SEQUENCE { + algorithm AlgorithmIdentifier, + subjectPublicKey BIT STRING } ``` -## 1.3 抽象数据模型 +其中 `algorithm`/`subjectPublicKey` 的取值受 RPKI 算法 profile 约束(例如 RSA 2048 + SHA-256 等;SKI/AKI 计算仍用 SHA-1)。RFC 5280 §4.1.2.7;RFC 7935 §2-§3.1;RFC 6487 §4.8.2-§4.8.3。 -### 1.3.1 TAL +## 1.3 解析规则(语义层) -| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 | -|----------|-------------|-------------------------|--------------------------------------------|---------------| -| uris | Vec | 指向TA的URI列表 | 允许rsync和https协议。 | RFC 8630 §2.1 | -| comment | Vec | 注释(可选) | | RFC 8630 §2.2 | -| spki_der | Vec | 原始的subjectPublicKeyInfo | x.509 SubjectPublicKeyInfo DER编码,再base64编码 | RFC 8630 §2.2 | +输入:`TalFileBytes: bytes`。 +解析步骤: -### 1.3.2 TalUri +1) 按 `LF` / `CRLF` 识别行。RFC 8630 §2.2。 +2) 从文件开头读取所有以 `#` 开头的行,作为 `comments`(保留去掉 `#` 后的 UTF-8 文本或保留原始行均可,但需保持 UTF-8)。RFC 8630 §2.2。 +3) 继续读取一行或多行非空行,作为 `ta_uris`(保持顺序)。RFC 8630 §2.2(第 2 点)。 +4) 读取一个空行(必须存在)。RFC 8630 §2.2(第 3 点)。 +5) 将剩余行拼接为 Base64 文本(移除行分隔),Base64 解码得到 `subject_public_key_info_der`。RFC 8630 §2.2(第 4 点)。 +6) 可选:将 `subject_public_key_info_der` 解析为 X.509 `SubjectPublicKeyInfo` 结构(用于与 TA 证书比对)。RFC 8630 §2.3;RFC 5280 §4.1.2.7。 -| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 | -|-------|--------|---------|---------|---------------| -| Rsync | String | rsync地址 | | RFC 8630 §2.1 | -| Https | String | https地址 | | RFC 8630 §2.1 | +URI 解析与约束: +- `ta_uris[*]` 的 scheme **MUST** 为 `rsync` 或 `https`。RFC 8630 §2.2。 +- 每个 `ta_uri` **MUST** 指向“单个对象”,且 **MUST NOT** 指向目录或集合。RFC 8630 §2.3。 + +## 1.4 抽象数据模型(接口) + +### 1.4.1 `Tal` + +| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 | +|---|---|---|---|---| +| `raw` | `bytes` | TAL 原始文件字节 | 原样保留(可选但建议) | RFC 8630 §2.2 | +| `comments` | `list[Utf8Text]` | 注释行(按出现顺序) | 每行以 `#` 开头;文本为 UTF-8;内容限制见 RFC 5198 §2 | RFC 8630 §2.2 | +| `ta_uris` | `list[Uri]` | TA 证书位置列表 | 至少 1 个;按序;每个 scheme 必须是 `rsync` 或 `https` | RFC 8630 §2.2 | +| `subject_public_key_info_der` | `DerBytes` | TA 证书 SPKI 的期望 DER | Base64 解码所得 DER;Base64 中可有换行 | RFC 8630 §2.2 | + +### 1.4.2 `TaUri`(可选细化) + +> 若你的实现希望对 URI 做更强类型化,可在 `Tal.ta_uris` 上进一步拆分为 `TaUri` 结构。 + +| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 | +|---|---|---|---|---| +| `uri` | `Uri` | 完整 URI 文本 | scheme 为 `rsync` 或 `https` | RFC 8630 §2.2 | +| `scheme` | `enum` | `rsync` / `https` | 从 `uri` 解析 | RFC 8630 §2.2 | + +## 1.5 字段级约束清单(实现对照) + +- TAL 由(可选)注释区 + URI 区 + 空行 + Base64(SPKI DER) 组成。RFC 8630 §2.2。 +- URI 区至少 1 行,每行一个 TA URI,顺序有意义。RFC 8630 §2.2。 +- TA URI 仅允许 `rsync` 或 `https`。RFC 8630 §2.2。 +- Base64 区允许插入换行。RFC 8630 §2.2。 +- 每个 TA URI 必须引用单个对象,不能指向目录/集合。RFC 8630 §2.3。 diff --git a/specs/02_ta.md b/specs/02_ta.md deleted file mode 100644 index b01d664..0000000 --- a/specs/02_ta.md +++ /dev/null @@ -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 | 原始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 | IPv4前缀集合 | | RFC 3779 §2 |) - -[//]: # (| v6 | PrefixSet | IPv6前缀集合 | | RFC 3779 §2 |) - -[//]: # () -[//]: # (### 2.3.4 AsnResourceSet) - -[//]: # () -[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |) - -[//]: # (|-------|--------------------|-------|-------------|-------------|) - -[//]: # (| range | RangeSet | 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. 更新本地存储库缓存。 diff --git a/specs/02_ta_certificate.md b/specs/02_ta_certificate.md new file mode 100644 index 0000000..0fc39a0 --- /dev/null +++ b/specs/02_ta_certificate.md @@ -0,0 +1,88 @@ +# 02. TA(Trust Anchor)自签名证书 + +## 2.1 对象定位 + +在 RP 侧,“信任锚(Trust Anchor, TA)”以一个**自签名 CA 资源证书**体现,其可获取位置与期望公钥由 TAL 提供。RFC 8630 §2.3。 + +本文件描述两个紧密相关的数据对象: + +1) `TaCertificate`:TA 自签名资源证书本体(X.509 DER) +2) `TrustAnchor`:语义组合对象(`TAL` + `TaCertificate` 的绑定语义) + +## 2.2 原始载体与编码 + +- 载体:X.509 证书(通常以 `.cer` 存放于仓库,但文件扩展名不作为语义依据)。 +- 编码:DER。TA 证书必须符合 RPKI 资源证书 profile。RFC 8630 §2.3;RFC 6487 §4。 + +### 2.2.1 X.509 Certificate 的 ASN.1 定义(RFC 5280 §4.1;TA 与 RC 共享) + +TA 证书与普通资源证书(RC)在编码层面都是 X.509 `Certificate`(DER)。其 ASN.1 定义如下:RFC 5280 §4.1。 + +```asn1 +Certificate ::= SEQUENCE { + tbsCertificate TBSCertificate, + signatureAlgorithm AlgorithmIdentifier, + signatureValue BIT STRING } +``` + +其中 `tbsCertificate.extensions`(v3 扩展)是 RPKI 语义的主要承载处(IP/AS 资源扩展、SIA/AIA/CRLDP 等)。RFC 5280 §4.1;RPKI 对字段/扩展存在性与关键性约束见 RFC 6487 §4。 + +> 说明:更完整的 RC 编码层结构(包括 Extension 外层“extnValue 二次 DER 解码”的套娃方式)在 `03_resource_certificate_rc.md` 与 `00_common_types.md` 中给出。 + +## 2.3 TA 证书的 RPKI 语义约束(在 RC profile 基础上额外强调) + +### 2.3.1 自签名与 profile + +- TA URI 指向的对象 **MUST** 是一个**自签名 CA 证书**,并且 **MUST** 符合 RPKI 证书 profile。RFC 8630 §2.3;RFC 6487 §4。 +- 自签名证书在 RC profile 下的通用差异(例如 CRLDP/AIA 的省略规则、AKI 的规则)见 RFC 6487。RFC 6487 §4.8.3;RFC 6487 §4.8.6;RFC 6487 §4.8.7。 + +### 2.3.2 INR(IP/AS 资源扩展)在 TA 上的额外约束 + +- TA 的 INR 扩展(IP/AS 资源扩展,RFC 3779)**MUST** 是非空资源集合。RFC 8630 §2.3。 +- TA 的 INR 扩展 **MUST NOT** 使用 `inherit` 形式。RFC 8630 §2.3。 + - 说明:一般 RC profile 允许 `inherit`。RFC 6487 §4.8.10;RFC 6487 §4.8.11;RFC 3779 §2.2.3.5;RFC 3779 §3.2.3.3。 + +### 2.3.3 TAL ↔ TA 公钥绑定 + +- 用于验证 TA 的公钥(来自 TAL 中的 SPKI)**MUST** 与 TA 证书中的 `subjectPublicKeyInfo` 相同。RFC 8630 §2.3。 + +### 2.3.4 TA 稳定性语义(实现需建模为“约束/假设”,但不属于验证结果态) + +- TA 公钥与 TAL 中公钥必须保持稳定(用于 RP 侧长期信任锚)。RFC 8630 §2.3。 + +### 2.3.5 TA 与 CRL/Manifest 的关系(语义) + +- RFC 8630 指出:TA 为自签名证书,没有对应 CRL,且不会被 manifest 列出;TA 的获取/轮换由 TAL 控制。RFC 8630 §2.3。 + +> 注:这条更偏“发布/运维语义”,但对数据对象建模有影响:`TrustAnchor` 组合对象不应依赖 CRL/MFT 的存在。 + +## 2.4 抽象数据模型(接口) + +### 2.4.1 `TaCertificate` + +> 该对象在字段层面复用 `RC(CA)` 的语义模型(见 `03_resource_certificate_rc.md`),但增加 TA 特有约束。 + +| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 | +|---|---|---|---|---| +| `raw_der` | `DerBytes` | TA 证书 DER | X.509 DER;证书 profile 约束见 RC 文档 | RFC 8630 §2.3;RFC 6487 §4 | +| `rc_ca` | `ResourceCaCertificate` | 以 RC(CA) 语义解析出的字段集合 | 必须满足“自签名 CA”分支约束;且 INR 必须非空且不允许 inherit | RFC 8630 §2.3;RFC 6487 §4;RFC 3779 §2/§3 | + +### 2.4.2 `TrustAnchor` + +| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 | +|---|---|---|---|---| +| `tal` | `Tal` | TAL 文件语义对象 | 见 `01_tal.md` | RFC 8630 §2.2 | +| `ta_certificate` | `TaCertificate` | TA 证书语义对象 | TA URI 指向的对象 | RFC 8630 §2.3 | +| `tal_spki_der` | `DerBytes` | 从 TAL 解析出的 SPKI DER | `tal.subject_public_key_info_der` | RFC 8630 §2.2 | +| `ta_spki_der` | `DerBytes` | 从 TA 证书抽取的 SPKI DER | `ta_certificate` 的 `subjectPublicKeyInfo` | RFC 8630 §2.3;RFC 5280 §4.1.2.7 | + +**绑定约束(字段级)** + +- `tal_spki_der` 必须与 `ta_spki_der` 完全相等(字节层面的 DER 等价)。RFC 8630 §2.3。 + +## 2.5 字段级约束清单(实现对照) + +- TA URI 指向的对象必须是自签名 CA 证书,且符合 RPKI 证书 profile。RFC 8630 §2.3;RFC 6487 §4。 +- TA 的 INR 扩展必须非空,且不得使用 inherit。RFC 8630 §2.3。 +- TAL 中 SPKI 必须与 TA 证书的 `subjectPublicKeyInfo` 匹配。RFC 8630 §2.3。 +- TA 不依赖 CRL/MFT(无对应 CRL,且不被 manifest 列出)。RFC 8630 §2.3。 diff --git a/specs/03_rc.md b/specs/03_rc.md deleted file mode 100644 index 9b03541..0000000 --- a/specs/03_rc.md +++ /dev/null @@ -1,314 +0,0 @@ -# 03. RC (Resource Certifications) - -## 3.1 对象定位 -RC是资源证书,包括CA和EE - -## 3.2 原始载体与编码 - -- 载体:X.509 certificates. -- 编码:DER(遵循 RFC 5280 的 Certificate 结构与字段语义,但受 RPKI profile 限制)RFC 6487 §4 - -### 3.2.1 基本语法(RFC 5280 §4,RFC 6487 ) - -RC是遵循RFC5280定义的X.509Certificate语法(RFC 5280 §4),并且符合RFC 6487 §4的约束。只选取RFC 6487 §4章节列出来的字段。(Unless specifically noted as being OPTIONAL, all the fields listed -here MUST be present, and any other fields MUST NOT appear in a -conforming resource certificate.) - -``` -Certificate ::= SEQUENCE { - tbsCertificate TBSCertificate, - signatureAlgorithm AlgorithmIdentifier, - signatureValue BIT STRING -} - -TBSCertificate ::= SEQUENCE { - version [0] EXPLICIT Version MUST be v3, - serialNumber CertificateSerialNumber, - signature AlgorithmIdentifier, - issuer Name, - subject Name, - validity Validity, - subjectPublicKeyInfo SubjectPublicKeyInfo, - extensions [3] EXPLICIT Extensions OPTIONAL - -- If present, version MUST be v3 - } - -Version ::= INTEGER { v1(0), v2(1), v3(2) } - -CertificateSerialNumber ::= INTEGER - -Validity ::= SEQUENCE { - notBefore Time, - notAfter Time } - -Time ::= CHOICE { - utcTime UTCTime, - generalTime GeneralizedTime } - -UniqueIdentifier ::= BIT STRING - -SubjectPublicKeyInfo ::= SEQUENCE { - algorithm AlgorithmIdentifier, - subjectPublicKey BIT STRING } - -Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension - -Extension ::= SEQUENCE { - extnID OBJECT IDENTIFIER, - critical BOOLEAN DEFAULT FALSE, - extnValue OCTET STRING - -- contains the DER encoding of an ASN.1 value - -- corresponding to the extension type identified - -- by extnID -} -``` - -> 其中`Name` "a valid X.501 distinguished name"(RFC 6487 §4.4) - -### 3.2.2 证书扩展字段 (RFC 6487 §4.8) - -RC的证书扩展字段按照RFC 6487 §4.8的规定,有以下几个扩展: - -- Basic Constraints -- Subject Key Identifier -- Authority Key Identifier -- Key Usage -- Extended Key Usage(CA证书,以及验证RPKI对象的EE证书不能出现该字段。非RPKI对象的EE可以出现EKU,但必须为non-critical) -- CRL Distribution Points -- Authority Information Access -- Subject Information Access - - SIA for CA Certificates - - SIA for EE Certificates -- Certificate Policies -- IP Resources -- AS Resources - -``` -# Basic Constraints -id-ce-basicConstraints OBJECT IDENTIFIER ::= { id-ce 19 } - -BasicConstraints ::= SEQUENCE { - cA BOOLEAN DEFAULT FALSE } - - -# Subject Key Identifier -id-ce-subjectKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 14 } - -SubjectKeyIdentifier ::= KeyIdentifier - -KeyIdentifier ::= OCTET STRING - - -# Authority Key Identifier -id-ce-authorityKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 35 } - -AuthorityKeyIdentifier ::= SEQUENCE { - keyIdentifier [0] KeyIdentifier OPTIONAL } - - -# Key Usage -id-ce-keyUsage OBJECT IDENTIFIER ::= { id-ce 15 } - -KeyUsage ::= BIT STRING { - digitalSignature (0), - nonRepudiation (1), -- recent editions of X.509 have - -- renamed this bit to contentCommitment - keyEncipherment (2), - dataEncipherment (3), - keyAgreement (4), - keyCertSign (5), - cRLSign (6), - encipherOnly (7), - decipherOnly (8) } - - -# Extended Key Usage -id-ce-extKeyUsage OBJECT IDENTIFIER ::= { id-ce 37 } - -ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId - -KeyPurposeId ::= OBJECT IDENTIFIER - - -# CRL Distribution Points -id-ce-cRLDistributionPoints OBJECT IDENTIFIER ::= { id-ce 31 } - -CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint - -DistributionPoint ::= SEQUENCE { - distributionPoint [0] DistributionPointName OPTIONAL } - -DistributionPointName ::= CHOICE { - fullName [0] GeneralNames } - - -## Authority Information Access -id-pe-authorityInfoAccess OBJECT IDENTIFIER ::= { id-pe 1 } - -AuthorityInfoAccessSyntax ::= - SEQUENCE SIZE (1..MAX) OF AccessDescription - -AccessDescription ::= SEQUENCE { - accessMethod OBJECT IDENTIFIER, - accessLocation GeneralName } - -# AccessDescription -id-ad OBJECT IDENTIFIER ::= { id-pkix 48 } -# CA 证书发布位置 -id-ad-caIssuers OBJECT IDENTIFIER ::= { id-ad 2 } -# OCSP 服务地址 -id-ad-ocsp OBJECT IDENTIFIER ::= { id-ad 1 } - - -# Subject Information Access -id-pe-subjectInfoAccess OBJECT IDENTIFIER ::= { id-pe 11 } - -SubjectInfoAccessSyntax ::= SEQUENCE SIZE (1..MAX) OF AccessDescription -AccessDescription ::= SEQUENCE { - accessMethod OBJECT IDENTIFIER, - accessLocation GeneralName } - -## Subject Information Access for CA (RFC 6487 §4.8.8.1) -id-ad OBJECT IDENTIFIER ::= { id-pkix 48 } -id-ad-rpkiManifest OBJECT IDENTIFIER ::= { id-ad 10 } - -必须存在一个accessMethod=id-ad-caRepository,accessLocation=rsyncURI。 -必须存在一个accessMethod=id-ad-repiManifest, accessLocation=rsync URI,指向该CA的mft对象。 - -## Subject Information Access for EE (RFC 6487 §4.8.8.2) -id-ad-signedObject OBJECT IDENTIFIER ::= { id-ad 11 } - -必须存在一个accessMethod=id-ad-signedObject, accessLocation=rsyncURI -不允许其他的accessMethod - - -# Certificate Policies -id-ce-certificatePolicies OBJECT IDENTIFIER ::= { id-ce 32 } -anyPolicy OBJECT IDENTIFIER ::= { id-ce-certificatePolicies 0 } - -certificatePolicies ::= SEQUENCE SIZE (1..MAX) OF PolicyInformation - -PolicyInformation ::= SEQUENCE { - policyIdentifier CertPolicyId, - policyQualifiers SEQUENCE SIZE (1..MAX) OF PolicyQualifierInfo OPTIONAL } - -CertPolicyId ::= OBJECT IDENTIFIER - -PolicyQualifierInfo ::= SEQUENCE { - policyQualifierId PolicyQualifierId, - qualifier ANY DEFINED BY policyQualifierId } - --- policyQualifierIds for Internet policy qualifiers -id-qt OBJECT IDENTIFIER ::= { id-pkix 2 } -id-qt-cps OBJECT IDENTIFIER ::= { id-qt 1 } -id-qt-unotice OBJECT IDENTIFIER ::= { id-qt 2 } - -PolicyQualifierId ::= OBJECT IDENTIFIER ( id-qt-cps | id-qt-unotice ) - -Qualifier ::= CHOICE { - cPSuri CPSuri, - userNotice UserNotice } - -CPSuri ::= IA5String - -UserNotice ::= SEQUENCE { - noticeRef NoticeReference OPTIONAL, - explicitText DisplayText OPTIONAL } - -NoticeReference ::= SEQUENCE { - organization DisplayText, - noticeNumbers SEQUENCE OF INTEGER } - -DisplayText ::= CHOICE { - ia5String IA5String (SIZE (1..200)), - visibleString VisibleString (SIZE (1..200)), - bmpString BMPString (SIZE (1..200)), - utf8String UTF8String (SIZE (1..200)) } - - -# IP Resources -id-pe-ipAddrBlocks OBJECT IDENTIFIER ::= { id-pe 7 } - -IPAddrBlocks ::= SEQUENCE OF IPAddressFamily - -IPAddressFamily ::= SEQUENCE { -- AFI & optional SAFI -- - addressFamily OCTET STRING (SIZE (2..3)), - ipAddressChoice IPAddressChoice } - -IPAddressChoice ::= CHOICE { - inherit NULL, -- inherit from issuer -- - addressesOrRanges SEQUENCE OF IPAddressOrRange } - -IPAddressOrRange ::= CHOICE { - addressPrefix IPAddress, - addressRange IPAddressRange } - -IPAddressRange ::= SEQUENCE { - min IPAddress, - max IPAddress } - -IPAddress ::= BIT STRING - - -# AS Resources -id-pe-autonomousSysIds OBJECT IDENTIFIER ::= { id-pe 8 } -ASIdentifiers ::= SEQUENCE { - asnum [0] EXPLICIT ASIdentifierChoice OPTIONAL, - rdi [1] EXPLICIT ASIdentifierChoice OPTIONAL} - -ASIdentifierChoice ::= CHOICE { - inherit NULL, -- inherit from issuer -- - asIdsOrRanges SEQUENCE OF ASIdOrRange } - -ASIdOrRange ::= CHOICE { - id ASId, - range ASRange } - -ASRange ::= SEQUENCE { - min ASId, - max ASId } - -ASId ::= INTEGER -``` - -# 3.3 抽象数据结构 -采用X509 Certificate + Resource + 约束校验的方式组合 - -| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 | -|----------|---------------------|----------|---------|---------------| -| cert_der | Vec | 证书原始数据 | | | -| cert | X509Certificate | 基础X509证书 | | RFC 5280 §4.1 | -| resource | ResourceSet | 资源集合 | | | - - -# 3.4 约束规则 - -## 3.4.1 Cert约束校验规则 -RFC 6487中规定的证书的字段参见[3.2.1 ](#321-基本语法rfc-5280-4rfc-6487-) -- - -| 字段 | 语义 | 约束/解析规则 | RFC 引用 | -|-----------|-------|----------------------------------------------|--------------| -| version | 证书版本 | 必须是v3(值为2) | RFC6487 §4.1 | -| serial | 证书编号 | 同一个CA签发的证书编号必须唯一 | RFC6487 §4.2 | -| validity | 证书有效期 | notBefore:时间不能早于证书的生成时间。若时间段大于上级证书的有效期,也是有效的 | RFC6487 §4.6 | - - -## 3.4.2 Cert Extentions中字段的约束校验规则 -RFC 6487中规定的扩展字段参见[3.2.2 ](#322-证书扩展字段-rfc-6487-48) - -| 字段 | critical | 语义 | 约束/解析规则 | RFC 引用 | -|----------------------------|----------|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------| -| basicConstraints | Y | 证书类型 | CA证书:cA=TRUE; EE证书:cA=FALSE | RFC6487 §4.8.1 | -| subjectKeyIdentifier | N | 证书公钥 | SKI = SHA-1(DER-encoded SPKI bit string) | RFC6487 §4.8.2 | -| authorityKeyIdentifier | N | 父证书的公钥 | 字段只包含keyIdentifier,不能包含authorityCertIssuer和authorityCertSerialNumber;除了自签名CA外,其余证书必须出现。自签名CA若出现该字段,则等于SKI | RFC6487 §4.8.3 | -| keyUsage | Y | 证书公钥的用途权限 | CA证书:keyCertSign = TRUE, cRLSign = TRUE 其他都是FALSE。EE证书:digitalSignature = TRUE 其他都是FALSE | RFC6487 §4.8.4 | -| extendedKeyUsage | N | 扩展证书公钥的用途权限 | CA证书:不能出现EKU;验证 RPKI 对象的 EE 证书:不能出现EKU;非 RPKI 对象的 EE:可以出现EKU,但必须为non-critical. | RFC6487 §4.8.5 | -| cRLDistributionPoints | N | CRL的发布点位置 | 字段:distributionPoint,不能包含reasons、cRLIssuer。其中distributionPoint字段包含:fullName,不能包含nameRelativeToCRLIssuer。fullName的格式必须是URI。自签名证书禁止出现该字段。非自签名证书必须出现。一个CA只能有一个CRL。一个CRLDP只能包含一个distributionPoint。但一个distributionPoint字段中可以包含多于1个的URI,但必须包含rsync URI且必须是最新的。 | RFC6487 §4.8.6 | -| authorityInformationAccess | N | 签发者的发布点位置 | 除了自签名的CA,必须出现。自签名CA,禁止出现。推荐的URI访问方式是rsync,并且rsyncURI的话,必须指定accessMethod=id-ad-caIssuers | RFC6487 §4.8.7 | -| subjectInformationAccess | N | 发布点位置 | CA证书:必须存在。必须存在一个accessMethod=id-ad-caRepository,accessLocation=rsyncURI。必须存在一个accessMethod=id-ad-repiManifest,accessLocation=rsync URI,指向该CA的mft对象。 EE证书:必须存在。必须存在一个accessMethod=id-ad-signedObject,accessLocation=rsyncURI。不允许其他的accessMethod | RFC6487 §4.8.8 | -| certificatePolicies | Y | 证书策略 | 必须存在,并且只能存在一种策略:RFC 6484 — RPKI Certificate Policy (CP) | RFC6487 §4.8.9 | -| iPResources | Y | IP地址集合 | 所有的RPKI证书中必须包含IP Resources或者ASResources,或者两者都包含。 | RFC6487 §4.8.10 | -| aSResources | Y | ASN集合 | 所有的RPKI证书中必须包含IP Resources或者ASResources,或者两者都包含。 | RFC6487 §4.8.11 | - - diff --git a/specs/03_resource_certificate_rc.md b/specs/03_resource_certificate_rc.md new file mode 100644 index 0000000..6fc4d59 --- /dev/null +++ b/specs/03_resource_certificate_rc.md @@ -0,0 +1,460 @@ +# 03. RC(Resource Certificate:资源证书,CA/EE) + +## 3.1 对象定位 + +资源证书(RC)是 X.509 v3 证书,遵循 PKIX profile(RFC 5280),并受 RPKI profile 进一步约束。RFC 6487 §4。 + +RC 在 RPKI 中至少分为两类语义用途: + +- `CA 证书`:签发下级证书/CRL,并在 SIA 中声明发布点与 manifest。RFC 6487 §4.8.8.1。 +- `EE 证书`:用于验证某个 RPKI Signed Object(如 ROA/MFT),在 SIA 中指向被验证对象。RFC 6487 §4.8.8.2。 + +## 3.2 原始载体与编码 + +- 载体:X.509 证书。 +- 编码:DER。RFC 6487 §4(“valid X.509 public key certificate consistent with RFC 5280” + RPKI 限制)。 + +### 3.2.1 X.509 v3 证书基本语法(ASN.1;RFC 5280 §4.1) + +资源证书在编码层面是 RFC 5280 定义的 X.509 v3 `Certificate`(DER),其中 `tbsCertificate` 携带主体字段与扩展集合(`Extensions`)。RFC 5280 §4.1。 + +```asn1 +Certificate ::= SEQUENCE { + tbsCertificate TBSCertificate, + signatureAlgorithm AlgorithmIdentifier, + signatureValue BIT STRING } + +TBSCertificate ::= SEQUENCE { + version [0] EXPLICIT Version DEFAULT v1, + serialNumber CertificateSerialNumber, + signature AlgorithmIdentifier, + issuer Name, + validity Validity, + subject Name, + subjectPublicKeyInfo SubjectPublicKeyInfo, + issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL, + -- If present, version MUST be v2 or v3 + subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL, + -- If present, version MUST be v2 or v3 + extensions [3] EXPLICIT Extensions OPTIONAL + -- If present, version MUST be v3 + } + +Version ::= INTEGER { v1(0), v2(1), v3(2) } + +CertificateSerialNumber ::= INTEGER + +Validity ::= SEQUENCE { + notBefore Time, + notAfter Time } + +Time ::= CHOICE { + utcTime UTCTime, + generalTime GeneralizedTime } + +UniqueIdentifier ::= BIT STRING + +SubjectPublicKeyInfo ::= SEQUENCE { + algorithm AlgorithmIdentifier, + subjectPublicKey BIT STRING } + +Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension + +Extension ::= SEQUENCE { + extnID OBJECT IDENTIFIER, + critical BOOLEAN DEFAULT FALSE, + extnValue OCTET STRING + -- contains the DER encoding of an ASN.1 value + -- corresponding to the extension type identified + -- by extnID + } +``` + +### 3.2.2 AlgorithmIdentifier(ASN.1;RFC 5280 §4.1.1.2) + +```asn1 +AlgorithmIdentifier ::= SEQUENCE { + algorithm OBJECT IDENTIFIER, + parameters ANY DEFINED BY algorithm OPTIONAL } +``` + +### 3.2.3 Name / DN 结构(ASN.1;RFC 5280 §4.1.2.4) + +```asn1 +Name ::= CHOICE { -- only one possibility for now -- + rdnSequence RDNSequence } + +RDNSequence ::= SEQUENCE OF RelativeDistinguishedName + +RelativeDistinguishedName ::= + SET SIZE (1..MAX) OF AttributeTypeAndValue + +AttributeTypeAndValue ::= SEQUENCE { + type AttributeType, + value AttributeValue } + +AttributeType ::= OBJECT IDENTIFIER + +AttributeValue ::= ANY -- DEFINED BY AttributeType + +DirectoryString ::= CHOICE { + teletexString TeletexString (SIZE (1..MAX)), + printableString PrintableString (SIZE (1..MAX)), + universalString UniversalString (SIZE (1..MAX)), + utf8String UTF8String (SIZE (1..MAX)), + bmpString BMPString (SIZE (1..MAX)) } +``` + +### 3.2.4 GeneralNames / GeneralName(ASN.1;RFC 5280 §4.2.1.6) + +> 说明:RPKI 的 AIA/SIA/CRLDP 等扩展通常把 URI 编码在 `uniformResourceIdentifier [6] IA5String` 分支中。RFC 5280 §4.2.1.6。 + +```asn1 +GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName + +GeneralName ::= CHOICE { + otherName [0] OtherName, + rfc822Name [1] IA5String, + dNSName [2] IA5String, + x400Address [3] ORAddress, + directoryName [4] Name, + ediPartyName [5] EDIPartyName, + uniformResourceIdentifier [6] IA5String, + iPAddress [7] OCTET STRING, + registeredID [8] OBJECT IDENTIFIER } + +OtherName ::= SEQUENCE { + type-id OBJECT IDENTIFIER, + value [0] EXPLICIT ANY DEFINED BY type-id } + +EDIPartyName ::= SEQUENCE { + nameAssigner [0] DirectoryString OPTIONAL, + partyName [1] DirectoryString } +``` + +### 3.2.5 AIA(Authority Information Access;ASN.1;RFC 5280 §4.2.2.1) + +```asn1 +id-pe-authorityInfoAccess OBJECT IDENTIFIER ::= { id-pe 1 } + +AuthorityInfoAccessSyntax ::= + SEQUENCE SIZE (1..MAX) OF AccessDescription + +AccessDescription ::= SEQUENCE { + accessMethod OBJECT IDENTIFIER, + accessLocation GeneralName } + +id-ad OBJECT IDENTIFIER ::= { id-pkix 48 } + +id-ad-caIssuers OBJECT IDENTIFIER ::= { id-ad 2 } +``` + +### 3.2.6 SIA(Subject Information Access;ASN.1;RFC 5280 §4.2.2.2) + +```asn1 +id-pe-subjectInfoAccess OBJECT IDENTIFIER ::= { id-pe 11 } + +SubjectInfoAccessSyntax ::= + SEQUENCE SIZE (1..MAX) OF AccessDescription + +AccessDescription ::= SEQUENCE { + accessMethod OBJECT IDENTIFIER, + accessLocation GeneralName } + +id-ad OBJECT IDENTIFIER ::= { id-pkix 48 } + +id-ad-caRepository OBJECT IDENTIFIER ::= { id-ad 5 } +``` + +### 3.2.7 RPKI 在 SIA 中新增/使用的 accessMethod OID(RFC 6487 §4.8.8.1 / §4.8.8.2;RFC 8182 §3.2) + +> 说明:下列 OID 用于 `AccessDescription.accessMethod`,并放在 SIA 的 `extnValue` 内层结构中(其外层 extnID 仍为 SIA:`id-pe-subjectInfoAccess`)。RFC 6487 §4.8.8;RFC 8182 §3.2。 + +```asn1 +id-ad OBJECT IDENTIFIER ::= { id-pkix 48 } + +id-ad-rpkiManifest OBJECT IDENTIFIER ::= { id-ad 10 } -- 1.3.6.1.5.5.7.48.10 + +id-ad-signedObject OBJECT IDENTIFIER ::= { id-ad 11 } -- 1.3.6.1.5.5.7.48.11 + +id-ad-rpkiNotify OBJECT IDENTIFIER ::= { id-ad 13 } -- 1.3.6.1.5.5.7.48.13 +``` + +### 3.2.8 CRLDistributionPoints(CRLDP;ASN.1;RFC 5280 §4.2.1.13) + +```asn1 +id-ce-cRLDistributionPoints OBJECT IDENTIFIER ::= { id-ce 31 } + +CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint + +DistributionPoint ::= SEQUENCE { + distributionPoint [0] DistributionPointName OPTIONAL, + reasons [1] ReasonFlags OPTIONAL, + cRLIssuer [2] GeneralNames OPTIONAL } + +DistributionPointName ::= CHOICE { + fullName [0] GeneralNames, + nameRelativeToCRLIssuer [1] RelativeDistinguishedName } + +ReasonFlags ::= BIT STRING { + unused (0), + keyCompromise (1), + cACompromise (2), + affiliationChanged (3), + superseded (4), + cessationOfOperation (5), + certificateHold (6), + privilegeWithdrawn (7), + aACompromise (8) } +``` + +### 3.2.9 Certificate Policies(ASN.1;RFC 5280 §4.2.1.4) + +```asn1 +id-ce-certificatePolicies OBJECT IDENTIFIER ::= { id-ce 32 } + +anyPolicy OBJECT IDENTIFIER ::= { id-ce-certificatePolicies 0 } + +certificatePolicies ::= SEQUENCE SIZE (1..MAX) OF PolicyInformation + +PolicyInformation ::= SEQUENCE { + policyIdentifier CertPolicyId, + policyQualifiers SEQUENCE SIZE (1..MAX) OF + PolicyQualifierInfo OPTIONAL } + +CertPolicyId ::= OBJECT IDENTIFIER + +PolicyQualifierInfo ::= SEQUENCE { + policyQualifierId PolicyQualifierId, + qualifier ANY DEFINED BY policyQualifierId } + +-- policyQualifierIds for Internet policy qualifiers + +id-qt OBJECT IDENTIFIER ::= { id-pkix 2 } +id-qt-cps OBJECT IDENTIFIER ::= { id-qt 1 } +id-qt-unotice OBJECT IDENTIFIER ::= { id-qt 2 } + +PolicyQualifierId ::= OBJECT IDENTIFIER ( id-qt-cps | id-qt-unotice ) + +Qualifier ::= CHOICE { + cPSuri CPSuri, + userNotice UserNotice } + +CPSuri ::= IA5String +``` + +### 3.2.10 RFC 3779 IP/AS 资源扩展(ASN.1;RFC 3779 §2.2.1-§2.2.3;RFC 3779 §3.2.1-§3.2.3) + +> 说明:RFC 3779 给出两个扩展的 OID 与 ASN.1 语法;它们作为 X.509 v3 扩展出现在 `extensions` 中(外层 extnID 为下列 OID)。RPKI profile 进一步约束 criticality/SAFI/RDI 等,见 RFC 6487 §4.8.10-§4.8.11。 + +```asn1 +-- IP Address Delegation Extension +id-pe-ipAddrBlocks OBJECT IDENTIFIER ::= { id-pe 7 } + +IPAddrBlocks ::= SEQUENCE OF IPAddressFamily + +IPAddressFamily ::= SEQUENCE { -- AFI & optional SAFI -- + addressFamily OCTET STRING (SIZE (2..3)), + ipAddressChoice IPAddressChoice } + +IPAddressChoice ::= CHOICE { + inherit NULL, -- inherit from issuer -- + addressesOrRanges SEQUENCE OF IPAddressOrRange } + +IPAddressOrRange ::= CHOICE { + addressPrefix IPAddress, + addressRange IPAddressRange } + +IPAddressRange ::= SEQUENCE { + min IPAddress, + max IPAddress } + +IPAddress ::= BIT STRING + +-- Autonomous System Identifier Delegation Extension +id-pe-autonomousSysIds OBJECT IDENTIFIER ::= { id-pe 8 } + +ASIdentifiers ::= SEQUENCE { + asnum [0] EXPLICIT ASIdentifierChoice OPTIONAL, + rdi [1] EXPLICIT ASIdentifierChoice OPTIONAL} + +ASIdentifierChoice ::= CHOICE { + inherit NULL, -- inherit from issuer -- + asIdsOrRanges SEQUENCE OF ASIdOrRange } + +ASIdOrRange ::= CHOICE { + id ASId, + range ASRange } + +ASRange ::= SEQUENCE { + min ASId, + max ASId } + +ASId ::= INTEGER +``` + +### 3.2.11 其它 RPKI profile 相关扩展的 ASN.1 定义(RFC 5280 §4.2.1.1-§4.2.1.3;RFC 5280 §4.2.1.9;RFC 5280 §4.2.1.12) + +> 说明:这些是 RPKI 资源证书 profile(RFC 6487 §4.8)所引用的通用 PKIX 扩展语法。RPKI 对其“必须/禁止/criticality/字段允许性”有额外限制(见本文件 3.3/3.4),但编码层的 ASN.1 类型来自 RFC 5280。 + +```asn1 +id-ce-authorityKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 35 } + +AuthorityKeyIdentifier ::= SEQUENCE { + keyIdentifier [0] KeyIdentifier OPTIONAL, + authorityCertIssuer [1] GeneralNames OPTIONAL, + authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL } + +KeyIdentifier ::= OCTET STRING + +id-ce-subjectKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 14 } + +SubjectKeyIdentifier ::= KeyIdentifier + +id-ce-keyUsage OBJECT IDENTIFIER ::= { id-ce 15 } + +KeyUsage ::= BIT STRING { + digitalSignature (0), + nonRepudiation (1), -- recent editions of X.509 have + -- renamed this bit to contentCommitment + keyEncipherment (2), + dataEncipherment (3), + keyAgreement (4), + keyCertSign (5), + cRLSign (6), + encipherOnly (7), + decipherOnly (8) } + +id-ce-basicConstraints OBJECT IDENTIFIER ::= { id-ce 19 } + +BasicConstraints ::= SEQUENCE { + cA BOOLEAN DEFAULT FALSE, + pathLenConstraint INTEGER (0..MAX) OPTIONAL } + +id-ce-extKeyUsage OBJECT IDENTIFIER ::= { id-ce 37 } + +ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId + +KeyPurposeId ::= OBJECT IDENTIFIER +``` + +## 3.3 抽象数据模型(接口) + +> 说明:本模型面向“语义化解析产物”。实现可保留 `raw_der` 作为可追溯入口。 + +### 3.3.1 顶层联合类型:`ResourceCertificate` + +| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 | +|---|---|---|---|---| +| `raw_der` | `DerBytes` | 证书 DER | 原样保留(建议) | RFC 6487 §4 | +| `tbs` | `RpkixTbsCertificate` | 证书语义字段(见下) | 仅允许 RFC 6487 允许的字段/扩展;其他字段 MUST NOT 出现 | RFC 6487 §4 | +| `kind` | `enum { ca, ee }` | 语义分类 | 来自 BasicConstraints + 用途约束 | RFC 6487 §4.8.1;RFC 6487 §4.8.8 | + +### 3.3.1.1 派生类型(用于字段类型标注) + +为避免在其它对象文档里反复写“`ResourceCertificate` 且 `kind==...`”,这里定义两个派生/别名类型: + +- `ResourceCaCertificate`:`ResourceCertificate` 且 `kind == ca` +- `ResourceEeCertificate`:`ResourceCertificate` 且 `kind == ee` + +这些派生类型不引入新字段,只是对 `ResourceCertificate.kind` 的约束化视图。 + +### 3.3.2 `RpkixTbsCertificate`(语义字段集合) + +| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 | +|---|---|---|---|---| +| `version` | `int` | X.509 版本 | MUST 为 v3(字段值为 2) | RFC 6487 §4.1 | +| `serial_number` | `int` | 序列号 | 正整数;对每 CA 签发唯一 | RFC 6487 §4.2 | +| `signature_algorithm` | `Oid` | 证书签名算法 | 必须为 `sha256WithRSAEncryption`(`1.2.840.113549.1.1.11`) | RFC 6487 §4.3;RFC 7935 §2(引用 RFC 4055) | +| `issuer_dn` | `RpkixDistinguishedName` | 颁发者 DN | 必含 1 个 CommonName;可含 1 个 serialNumber;CN 必须 PrintableString | RFC 6487 §4.4 | +| `subject_dn` | `RpkixDistinguishedName` | 主体 DN | 同 issuer 约束;且对同一 issuer 下“实体+公钥”唯一 | RFC 6487 §4.5 | +| `validity_not_before` | `UtcTime` | 有效期起 | X.509 `Time`(UTCTime/GeneralizedTime)解析为 UTC 时间点 | RFC 6487 §4.6.1;RFC 5280 §4.1.2.5 | +| `validity_not_after` | `UtcTime` | 有效期止 | X.509 `Time`(UTCTime/GeneralizedTime)解析为 UTC 时间点 | RFC 6487 §4.6.2;RFC 5280 §4.1.2.5 | +| `subject_public_key_info` | `DerBytes` | SPKI DER | 算法 profile 指定 | RFC 6487 §4.7;RFC 7935 §3.1 | +| `extensions` | `RpkixExtensions` | 扩展集合 | 见下表;criticality/存在性/内容受约束 | RFC 6487 §4.8 | + +### 3.3.3 `RpkixDistinguishedName`(RPKI profile 下的 DN 语义) + +| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 | +|---|---|---|---|---| +| `common_name` | `string` | CommonName (CN) | MUST 存在且仅 1 个;类型为 PrintableString | RFC 6487 §4.4;RFC 6487 §4.5 | +| `serial_number` | `optional[string]` | serialNumber | MAY 存在且仅 1 个 | RFC 6487 §4.4;RFC 6487 §4.5 | +| `rfc4514` | `string` | DN 的 RFC4514 字符串表示 | 便于日志/索引(实现自选) | RFC 6487 §4.5(引用 RFC4514) | + +### 3.3.4 `RpkixExtensions`(核心扩展语义) + +> 表中 “存在性/criticality” 指 RPKI profile 下对该扩展的要求;实现应能区分 “字段缺失” 与 “字段存在但不符合约束”。 + +| 字段 | 类型 | 语义 | 存在性/criticality 与内容约束 | RFC 引用 | +|---|---|---|---|---| +| `basic_constraints` | `optional[BasicConstraints]` | CA 标志 | **extnID=`2.5.29.19`**;CA 证书:MUST present & critical;EE:MUST NOT present;pathLen MUST NOT present | RFC 6487 §4.8.1;RFC 5280 §4.2.1.9 | +| `subject_key_identifier` | `bytes` | SKI | **extnID=`2.5.29.14`**;MUST present & non-critical;值为 subjectPublicKey 的 DER bit string 的 SHA-1 哈希 | RFC 6487 §4.8.2(引用 RFC 5280 §4.2.1.2) | +| `authority_key_identifier` | `optional[AuthorityKeyIdentifier]` | AKI | **extnID=`2.5.29.35`**;自签名:MAY present 且可等于 SKI;非自签名:MUST present;authorityCertIssuer/authorityCertSerialNumber MUST NOT present;non-critical | RFC 6487 §4.8.3;RFC 5280 §4.2.1.1 | +| `key_usage` | `KeyUsage` | KeyUsage | **extnID=`2.5.29.15`**;MUST present & critical;CA:仅 `keyCertSign` 与 `cRLSign` 为 TRUE;EE:仅 `digitalSignature` 为 TRUE | RFC 6487 §4.8.4;RFC 5280 §4.2.1.3 | +| `extended_key_usage` | `optional[OidSet]` | EKU | **extnID=`2.5.29.37`**;CA:MUST NOT appear;用于验证 RPKI 对象的 EE:MUST NOT appear;若出现不得标 critical | RFC 6487 §4.8.5;RFC 5280 §4.2.1.12 | +| `crl_distribution_points` | `optional[CrlDistributionPoints]` | CRLDP | **extnID=`2.5.29.31`**;自签名:MUST be omitted;非自签名:MUST present & non-critical;仅 1 个 DistributionPoint;fullName URI;必须包含至少 1 个 `rsync://` | RFC 6487 §4.8.6;RFC 5280 §4.2.1.13 | +| `authority_info_access` | `optional[AuthorityInfoAccess]` | AIA | **extnID=`1.3.6.1.5.5.7.1.1`**;自签名:MUST be omitted;非自签名:MUST present & non-critical;必须含 accessMethod `id-ad-caIssuers`(**`1.3.6.1.5.5.7.48.2`**) 的 `rsync://` URI;可含同对象其它 URI | RFC 6487 §4.8.7;RFC 5280 §4.2.2.1 | +| `subject_info_access_ca` | `optional[SubjectInfoAccessCa]` | SIA(CA) | **extnID=`1.3.6.1.5.5.7.1.11`**;CA:MUST present & non-critical;必须含 accessMethod `id-ad-caRepository`(**`1.3.6.1.5.5.7.48.5`**)(`rsync://` 目录 URI)与 `id-ad-rpkiManifest`(**`1.3.6.1.5.5.7.48.10`**)(`rsync://` 对象 URI);若 CA 使用 RRDP,还会包含 `id-ad-rpkiNotify`(**`1.3.6.1.5.5.7.48.13`**)(HTTPS Notification URI) | RFC 6487 §4.8.8.1;RFC 5280 §4.2.2.2;RFC 8182 §3.2 | +| `subject_info_access_ee` | `optional[SubjectInfoAccessEe]` | SIA(EE) | **extnID=`1.3.6.1.5.5.7.1.11`**;EE:MUST present & non-critical;必须含 accessMethod `id-ad-signedObject`(**`1.3.6.1.5.5.7.48.11`**);URI **MUST include** `rsync://`;EE 的 SIA 不允许其它 AccessMethods | RFC 6487 §4.8.8.2;RFC 5280 §4.2.2.2 | +| `certificate_policies` | `CertificatePolicies` | 证书策略 | **extnID=`2.5.29.32`**;MUST present & critical;恰好 1 个 policy;并允许 0 或 1 个 CPS qualifier(若存在其 id 必为 `id-qt-cps`(**`1.3.6.1.5.5.7.2.1`**)) | RFC 6487 §4.8.9;RFC 7318 §2;RFC 5280 §4.2.1.4 | +| `ip_resources` | `optional[IpResourceSet]` | IP 资源扩展 | **extnID=`1.3.6.1.5.5.7.1.7`**;IP/AS 两者至少其一 MUST present;若 present MUST be critical;内容为 RFC 3779 语义;在公用互联网场景 SAFI MUST NOT 使用;且必须为非空或 inherit | RFC 6487 §4.8.10;RFC 3779 §2.2.1;RFC 3779 §2.2.2 | +| `as_resources` | `optional[AsResourceSet]` | AS 资源扩展 | **extnID=`1.3.6.1.5.5.7.1.8`**;IP/AS 两者至少其一 MUST present;若 present MUST be critical;内容为 RFC 3779 语义;RDI MUST NOT 使用;且必须为非空或 inherit | RFC 6487 §4.8.11;RFC 3779 §3.2.1;RFC 3779 §3.2.2 | + +### 3.3.5 结构化子类型(建议) + +#### `BasicConstraints` + +| 字段 | 类型 | 语义 | 约束 | RFC 引用 | +|---|---|---|---|---| +| `ca` | `bool` | 是否 CA | 由 issuer 决定;在 CA 证书中该扩展必须存在 | RFC 6487 §4.8.1 | +| `path_len_constraint` | `None` | pathLenConstraint | MUST NOT present(RPKI profile 不使用) | RFC 6487 §4.8.1 | + +#### `AuthorityKeyIdentifier` + +| 字段 | 类型 | 语义 | 约束 | RFC 引用 | +|---|---|---|---|---| +| `key_identifier` | `bytes` | AKI.keyIdentifier | 使用 issuer 公钥的 SHA-1 哈希(按 RFC 5280 的定义) | RFC 6487 §4.8.3(引用 RFC 5280 §4.2.1.1) | +| `authority_cert_issuer` | `None` | authorityCertIssuer | MUST NOT present | RFC 6487 §4.8.3 | +| `authority_cert_serial_number` | `None` | authorityCertSerialNumber | MUST NOT present | RFC 6487 §4.8.3 | + +#### `CrlDistributionPoints` + +| 字段 | 类型 | 语义 | 约束 | RFC 引用 | +|---|---|---|---|---| +| `distribution_point_uris` | `list[Uri]` | CRL 位置列表 | 仅 1 个 DistributionPoint;必须包含至少 1 个 `rsync://` URI 指向该 issuer 最新 CRL;可含其它 URI | RFC 6487 §4.8.6 | + +#### `AuthorityInfoAccess` + +| 字段 | 类型 | 语义 | 约束 | RFC 引用 | +|---|---|---|---|---| +| `ca_issuers_uris` | `list[Uri]` | 上级 CA 证书位置 | accessMethod=`id-ad-caIssuers`(`1.3.6.1.5.5.7.48.2`);必含 `rsync://` URI;可含同对象其它 URI | RFC 6487 §4.8.7;RFC 5280 §4.2.2.1 | + +#### `SubjectInfoAccessCa` + +| 字段 | 类型 | 语义 | 约束 | RFC 引用 | +|---|---|---|---|---| +| `ca_repository_uris` | `list[Uri]` | CA 发布点目录(repository publication point) | accessMethod=`id-ad-caRepository`(`1.3.6.1.5.5.7.48.5`);至少 1 个;必须包含 `rsync://`;也可包含其它机制(例如 `https://`)作为“同一目录”的替代访问方式;顺序表示 CA 偏好 | RFC 6487 §4.8.8.1;RFC 5280 §4.2.2.2 | +| `rpki_manifest_uris` | `list[Uri]` | 当前 manifest 对象 URI | accessMethod=`id-ad-rpkiManifest`(`1.3.6.1.5.5.7.48.10`);至少 1 个;必须包含 `rsync://`;也可包含其它机制(例如 `https://`)作为“同一对象”的替代访问方式 | RFC 6487 §4.8.8.1;RFC 5280 §4.2.2.2 | +| `rpki_notify_uris` | `optional[list[Uri]]` | RRDP Notification(Update Notification File)URI | accessMethod=`id-ad-rpkiNotify`(`1.3.6.1.5.5.7.48.13`);若存在则 accessLocation MUST 为 `https://` URI,指向 RRDP Notification 文件 | RFC 8182 §3.2;RFC 5280 §4.2.2.2 | + +#### `SubjectInfoAccessEe` + +| 字段 | 类型 | 语义 | 约束 | RFC 引用 | +|---|---|---|---|---| +| `signed_object_uris` | `list[Uri]` | 被 EE 证书验证的签名对象位置 | accessMethod=`id-ad-signedObject`(`1.3.6.1.5.5.7.48.11`);必须包含 `rsync://`;其它 URI 可作为同对象替代机制;EE SIA 不允许其它 AccessMethods | RFC 6487 §4.8.8.2;RFC 5280 §4.2.2.2 | + +#### `CertificatePolicies` + +| 字段 | 类型 | 语义 | 约束 | RFC 引用 | +|---|---|---|---|---| +| `policy_oid` | `Oid` | 唯一 policy OID | 恰好 1 个 policy;RPKI CP 分配的 OID 为 `id-cp-ipAddr-asNumber`(`1.3.6.1.5.5.7.14.2`) | RFC 6487 §4.8.9;RFC 6484 §1.2 | +| `cps_uri` | `optional[Uri]` | CPS policy qualifier URI | MAY 存在且最多 1 个;若存在其 `policyQualifierId` 必为 `id-qt-cps`;对该 URI 不施加处理要求 | RFC 7318 §2;RFC 5280 §4.2.1.4 | + +## 3.4 字段级约束清单(实现对照) + +- 仅允许 RFC 6487 §4 指定的字段/扩展;未列出字段 MUST NOT 出现。RFC 6487 §4。 +- 证书版本必须为 v3。RFC 6487 §4.1。 +- CA/EE 在 BasicConstraints 与 SIA 的约束不同。RFC 6487 §4.8.1;RFC 6487 §4.8.8.1;RFC 6487 §4.8.8.2。 +- KeyUsage:CA 仅 `keyCertSign`/`cRLSign`;EE 仅 `digitalSignature`。RFC 6487 §4.8.4。 +- CRLDP/AIA:自签名必须省略;非自签名必须存在并包含 `rsync://`。RFC 6487 §4.8.6;RFC 6487 §4.8.7。 +- IP/AS 资源扩展:两者至少其一存在;若存在必须 critical;语义来自 RFC 3779;在公用互联网场景 SAFI 与 RDI 均不得使用。RFC 6487 §4.8.10;RFC 6487 §4.8.11;RFC 3779 §2.2.3;RFC 3779 §3.2.3。 diff --git a/src/data_model/aspa.rs b/src/data_model/aspa.rs index 22ac46c..e7c1590 100644 --- a/src/data_model/aspa.rs +++ b/src/data_model/aspa.rs @@ -1,4 +1,5 @@ use crate::data_model::oid::OID_CT_ASPA; +use crate::data_model::rc::ResourceCertificate; use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError}; use der_parser::ber::{Class}; use der_parser::der::{parse_der, DerObject, Tag}; @@ -19,73 +20,64 @@ pub struct AspaEContent { #[derive(Debug, thiserror::Error)] pub enum AspaDecodeError { - #[error("SignedObject decode error: {0}")] + #[error("SignedObject decode error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")] SignedObjectDecode(#[from] SignedObjectDecodeError), - #[error("ASPA eContentType must be {OID_CT_ASPA}, got {0}")] + #[error("ASPA eContentType must be {OID_CT_ASPA}, got {0} (draft-ietf-sidrops-aspa-profile-21 §2)")] InvalidEContentType(String), - #[error("ASPA parse error: {0}")] + #[error("ASPA parse error: {0} (draft-ietf-sidrops-aspa-profile-21 §3; DER)")] Parse(String), - #[error("ASPA trailing bytes: {0} bytes")] + #[error("ASPA trailing bytes: {0} bytes (draft-ietf-sidrops-aspa-profile-21 §3; DER)")] TrailingBytes(usize), - #[error("ASProviderAttestation must be a SEQUENCE of 3 elements")] + #[error("ASProviderAttestation must be a SEQUENCE of 3 elements (draft-ietf-sidrops-aspa-profile-21 §3)")] InvalidAttestationSequence, - #[error("ASPA version must be 1 and MUST be explicitly encoded")] + #[error("ASPA version must be 1 and MUST be explicitly encoded (draft-ietf-sidrops-aspa-profile-21 §3.1)")] VersionMustBeExplicitOne, - #[error("ASPA customerASID out of range (0..=4294967295), got {0}")] + #[error("ASPA customerASID out of range (0..=4294967295), got {0} (draft-ietf-sidrops-aspa-profile-21 §3.2)")] CustomerAsIdOutOfRange(u64), - #[error("ASPA providers must contain at least one ASID")] + #[error("ASPA providers must contain at least one ASID (draft-ietf-sidrops-aspa-profile-21 §3.3)")] EmptyProviders, - #[error("ASPA provider ASID out of range (0..=4294967295), got {0}")] + #[error("ASPA provider ASID out of range (0..=4294967295), got {0} (draft-ietf-sidrops-aspa-profile-21 §3.3)")] ProviderAsIdOutOfRange(u64), - #[error("ASPA providers must be in strictly increasing order")] + #[error("ASPA providers must be in strictly increasing order (draft-ietf-sidrops-aspa-profile-21 §3.3)")] ProvidersNotStrictlyIncreasing, - #[error("ASPA providers contains the customerASID ({0}) which is not allowed")] + #[error("ASPA providers contains the customerASID ({0}) which is not allowed (draft-ietf-sidrops-aspa-profile-21 §3.3)")] ProvidersContainCustomer(u32), } #[derive(Debug, thiserror::Error)] pub enum AspaValidateError { - #[error("ASPA EE certificate must contain AS resources extension (RFC 3779)")] + #[error("ASPA EE certificate must contain AS resources extension (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2)")] EeAsResourcesMissing, - #[error("ASPA EE certificate AS resources must not use inherit")] + #[error("ASPA EE certificate AS resources must not use inherit (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.3)")] EeAsResourcesInherit, - #[error("ASPA EE certificate AS resources must not include ranges")] + #[error("ASPA EE certificate AS resources must not include ranges (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.6-§3.2.3.7)")] EeAsResourcesRangePresent, - #[error("ASPA 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 }, - #[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, } -/// 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, - /// 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 { pub fn decode_der(der: &[u8]) -> Result { let signed_object = RpkiSignedObject::decode_der(der)?; @@ -109,6 +101,12 @@ impl AspaObject { 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 { @@ -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 /// `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> { - if ee.ip_resources_present { + pub fn validate_against_ee_cert( + &self, + ee: &ResourceCertificate, + ) -> Result<(), AspaValidateError> { + if ee.tbs.extensions.ip_resources.is_some() { 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); } - if ee.as_resources_range_present { + if asn.has_any_range() { 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 { return Err(AspaValidateError::CustomerAsIdMismatch { customer_as_id: self.customer_as_id, ee_as_id, }); } + Ok(()) } } diff --git a/src/data_model/common.rs b/src/data_model/common.rs index 6e0925b..cc47015 100644 --- a/src/data_model/common.rs +++ b/src/data_model/common.rs @@ -69,7 +69,7 @@ impl BigUnsigned { } #[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 field: &'static str, pub year: i32, diff --git a/src/data_model/crl.rs b/src/data_model/crl.rs index afa9491..1c5010e 100644 --- a/src/data_model/crl.rs +++ b/src/data_model/crl.rs @@ -37,52 +37,52 @@ pub struct RpkixCrl { #[derive(Debug, thiserror::Error)] pub enum CrlDecodeError { - #[error("X.509 CRL parse error: {0}")] + #[error("X.509 CRL parse error: {0} (RFC 5280 §5.1; RFC 6487 §5)")] Parse(String), - #[error("trailing bytes after CRL DER: {0} bytes")] + #[error("trailing bytes after CRL DER: {0} bytes (DER; RFC 5280 §5.1)")] TrailingBytes(usize), - #[error("CRL version must be v2, got {0:?}")] + #[error("CRL version must be v2, got {0:?} (RFC 5280 §5.1; RFC 6487 §5)")] InvalidVersion(Option), - #[error("CRL signatureAlgorithm must be sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0}")] + #[error("CRL signatureAlgorithm must be sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0} (RFC 6487 §5; RFC 7935 §2)")] InvalidSignatureAlgorithm(String), - #[error("CRL signature algorithm parameters must be absent or NULL")] + #[error("CRL signature algorithm parameters must be absent or NULL (RFC 5280 §4.1.1.2; RFC 7935 §2)")] InvalidSignatureAlgorithmParameters, - #[error("CRL signatureAlgorithm must match TBSCertList.signature")] + #[error("CRL signatureAlgorithm must match TBSCertList.signature (RFC 5280 §5.1)")] SignatureAlgorithmMismatch, - #[error("CRL extensions must be exactly two (AKI + CRLNumber), got {0}")] + #[error("CRL extensions must be exactly two (AKI + CRLNumber), got {0} (RFC 9829 §3.1)")] InvalidExtensionsCount(usize), - #[error("unsupported CRL extension OID {0}")] + #[error("unsupported CRL extension OID {0} (RFC 9829 §3.1)")] UnsupportedExtension(String), - #[error("duplicate CRL extension OID {0}")] + #[error("duplicate CRL extension OID {0} (RFC 5280 §4.2; RFC 9829 §3.1)")] DuplicateExtension(String), - #[error("AuthorityKeyIdentifier must contain keyIdentifier")] + #[error("AuthorityKeyIdentifier must contain keyIdentifier (RFC 5280 §5.2.1; RFC 9829 §3.1)")] AkiMissingKeyIdentifier, - #[error("AuthorityKeyIdentifier must not contain authorityCertIssuer or authorityCertSerialNumber")] + #[error("AuthorityKeyIdentifier must not contain authorityCertIssuer or authorityCertSerialNumber (RFC 5280 §5.2.1; RFC 9829 §3.1)")] AkiHasOtherFields, - #[error("CRLNumber must be non-critical")] + #[error("CRLNumber must be non-critical (RFC 9829 §3.1; RFC 5280 §5.2.3)")] CrlNumberCritical, - #[error("CRLNumber out of range (must fit in 0..2^159-1)")] + #[error("CRLNumber out of range (must fit in 0..2^159-1) (RFC 9829 §3.1)")] CrlNumberOutOfRange, - #[error("CRL entry extensions must not be present")] + #[error("CRL entry extensions must not be present (RFC 6487 §5; RFC 5280 §5.1)")] EntryExtensionsNotAllowed, - #[error("CRL nextUpdate must be present (RFC 5280 §5.1.2.5)")] + #[error("CRL nextUpdate must be present (RFC 5280 §5.1.2.5; RFC 6487 §5)")] NextUpdateMissing, - #[error("{field} time encoding invalid for year {year}: got {encoding:?}")] + #[error("{field} time encoding invalid for year {year}: got {encoding:?} (RFC 5280 §5.1.2.4-§5.1.2.6)")] InvalidTimeEncoding { field: &'static str, year: i32, @@ -231,37 +231,37 @@ impl RpkixCrl { #[derive(Debug, thiserror::Error)] pub enum CrlVerifyError { - #[error("issuer certificate parse error: {0}")] + #[error("issuer certificate parse error: {0} (RFC 5280 §4.1; RFC 6487 §4)")] IssuerCertificateParse(String), - #[error("trailing bytes after issuer certificate DER: {0} bytes")] + #[error("trailing bytes after issuer certificate DER: {0} bytes (DER; RFC 5280 §4.1)")] IssuerCertificateTrailingBytes(usize), - #[error("issuer SubjectPublicKeyInfo parse error: {0}")] + #[error("issuer SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7)")] IssuerSpkiParse(String), - #[error("trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes")] + #[error("trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 5280 §4.1.2.7)")] IssuerSpkiTrailingBytes(usize), - #[error("CRL parse error: {0}")] + #[error("CRL parse error: {0} (RFC 5280 §5.1; RFC 6487 §5)")] CrlParse(String), - #[error("trailing bytes after CRL DER: {0} bytes")] + #[error("trailing bytes after CRL DER: {0} bytes (DER; RFC 5280 §5.1)")] CrlTrailingBytes(usize), - #[error("CRL issuer DN does not match issuer certificate subject")] + #[error("CRL issuer DN does not match issuer certificate subject (RFC 5280 §5.1; RFC 5280 §6.3.3(b))")] IssuerSubjectMismatch { crl_issuer_dn: String, issuer_subject_dn: String, }, - #[error("issuer certificate keyUsage present but missing cRLSign")] + #[error("issuer certificate keyUsage present but missing cRLSign (RFC 5280 §4.2.1.3; RFC 5280 §6.3.3(f))")] IssuerKeyUsageMissingCrlSign, - #[error("CRL AKI.keyIdentifier does not match issuer certificate SKI")] + #[error("CRL AKI.keyIdentifier does not match issuer certificate SKI (RFC 5280 §4.2.1.1; RFC 5280 §4.2.1.2; RFC 5280 §6.3.3(c)/(f))")] AkiSkiMismatch, - #[error("CRL signature verification failed: {0}")] + #[error("CRL signature verification failed: {0} (RFC 5280 §6.3.3(g); RFC 7935 §2)")] InvalidSignature(String), } diff --git a/src/data_model/manifest.rs b/src/data_model/manifest.rs index 0b1daaf..2cb8c09 100644 --- a/src/data_model/manifest.rs +++ b/src/data_model/manifest.rs @@ -1,5 +1,6 @@ use crate::data_model::common::BigUnsigned; use crate::data_model::oid::{OID_CT_RPKI_MANIFEST, OID_SHA256}; +use crate::data_model::rc::ResourceCertificate; use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError}; use der_parser::ber::BerObjectContent; use der_parser::der::{parse_der, DerObject, Tag}; @@ -30,61 +31,76 @@ pub struct FileAndHash { #[derive(Debug, thiserror::Error)] 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), - #[error("DER parse error: {0}")] + #[error("DER parse error: {0} (RFC 9286 §4.2; DER)")] 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), - #[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), - #[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), - #[error("Manifest.version must be 0, got {0}")] + #[error("Manifest.version must be 0, got {0} (RFC 9286 §4.2.1)")] 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, - #[error("Manifest.manifestNumber longer than 20 octets")] + #[error("Manifest.manifestNumber longer than 20 octets (RFC 9286 §4.2.1)")] ManifestNumberTooLong, - #[error("Manifest.thisUpdate must be GeneralizedTime")] + #[error("Manifest.thisUpdate must be GeneralizedTime (RFC 9286 §4.2)")] InvalidThisUpdate, - #[error("Manifest.nextUpdate must be GeneralizedTime")] + #[error("Manifest.nextUpdate must be GeneralizedTime (RFC 9286 §4.2)")] InvalidNextUpdate, - #[error("Manifest.nextUpdate must be later than thisUpdate")] + #[error("Manifest.nextUpdate must be later than thisUpdate (RFC 9286 §4.2.1)")] NextUpdateNotLater, - #[error("Manifest.fileHashAlg must be id-sha256 ({OID_SHA256}), got {0}")] + #[error("Manifest.fileHashAlg must be id-sha256 ({OID_SHA256}), got {0} (RFC 9286 §4.2.1; RFC 7935 §2)")] InvalidFileHashAlg(String), - #[error("Manifest.fileList must be a SEQUENCE")] + #[error("Manifest.fileList must be a SEQUENCE (RFC 9286 §4.2)")] InvalidFileList, - #[error("FileAndHash must be SEQUENCE of 2")] + #[error("FileAndHash must be SEQUENCE of 2 (RFC 9286 §4.2)")] InvalidFileAndHash, - #[error("fileList file name invalid: {0}")] + #[error("fileList file name invalid: {0} (RFC 9286 §4.2.2)")] InvalidFileName(String), - #[error("fileList hash must be BIT STRING")] + #[error("fileList hash must be BIT STRING (RFC 9286 §4.2)")] InvalidHashType, - #[error("fileList hash BIT STRING must be octet-aligned (unused bits=0)")] + #[error("fileList hash BIT STRING must be octet-aligned (unused bits=0) (RFC 9286 §4.2.1; DER BIT STRING)")] HashNotOctetAligned, - #[error("fileList hash length invalid for sha256: got {0} bytes")] + #[error("fileList hash length invalid for sha256: got {0} bytes (RFC 9286 §4.2.1; RFC 7935 §2)")] InvalidHashLength(usize), } +#[derive(Debug, thiserror::Error)] +pub enum ManifestValidateError { + #[error("Manifest EE certificate MUST include at least one RFC 3779 resource extension (IP or AS) (RFC 6487 §4.8.10-§4.8.11; RFC 3779; RFC 9286 §5.1)")] + EeResourcesMissing, + + #[error("Manifest EE certificate IP resources MUST use inherit only (RFC 9286 §5.1; RFC 3779 §2.2.3.5)")] + EeIpResourcesNotInherit, + + #[error("Manifest EE certificate AS resources MUST use inherit only (RFC 9286 §5.1; RFC 3779 §3.2.3.3)")] + EeAsResourcesNotInherit, + + #[error("Manifest EE certificate AS resources rdi MUST be absent (RFC 6487 §4.8.11; RFC 3779 §3.2.3.5)")] + EeAsResourcesRdiPresent, +} + impl ManifestObject { pub fn decode_der(der: &[u8]) -> Result { let signed_object = RpkiSignedObject::decode_der(der)?; @@ -107,6 +123,44 @@ impl ManifestObject { 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 { diff --git a/src/data_model/mod.rs b/src/data_model/mod.rs index e7e1e4d..bd487da 100644 --- a/src/data_model/mod.rs +++ b/src/data_model/mod.rs @@ -1,12 +1,10 @@ pub mod common; pub mod crl; -mod rc; -mod tal; -mod ta; -mod resources; -mod oids; +pub mod rc; pub mod oid; pub mod signed_object; pub mod manifest; pub mod roa; pub mod aspa; +pub mod tal; +pub mod ta; diff --git a/src/data_model/oid.rs b/src/data_model/oid.rs index cd1025f..aa84359 100644 --- a/src/data_model/oid.rs +++ b/src/data_model/oid.rs @@ -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_SHA256_WITH_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.11"; +// X.509 extensions (RFC 5280 / RFC 6487) +pub const OID_BASIC_CONSTRAINTS: &str = "2.5.29.19"; +pub const OID_KEY_USAGE: &str = "2.5.29.15"; +pub const OID_EXTENDED_KEY_USAGE: &str = "2.5.29.37"; +pub const OID_CRL_DISTRIBUTION_POINTS: &str = "2.5.29.31"; +pub const OID_AUTHORITY_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.1"; +pub const OID_CERTIFICATE_POLICIES: &str = "2.5.29.32"; + pub const OID_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35"; pub const OID_CRL_NUMBER: &str = "2.5.29.20"; pub const OID_SUBJECT_KEY_IDENTIFIER: &str = "2.5.29.14"; @@ -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) pub const OID_SUBJECT_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.11"; pub const OID_AD_SIGNED_OBJECT: &str = "1.3.6.1.5.5.7.48.11"; + +pub const OID_AD_CA_ISSUERS: &str = "1.3.6.1.5.5.7.48.2"; +pub const OID_AD_CA_REPOSITORY: &str = "1.3.6.1.5.5.7.48.5"; +pub const OID_AD_RPKI_MANIFEST: &str = "1.3.6.1.5.5.7.48.10"; +pub const OID_AD_RPKI_NOTIFY: &str = "1.3.6.1.5.5.7.48.13"; + +// RFC 3779 resource extensions (RFC 6487 profile) +pub const OID_IP_ADDR_BLOCKS: &str = "1.3.6.1.5.5.7.1.7"; +pub const OID_AUTONOMOUS_SYS_IDS: &str = "1.3.6.1.5.5.7.1.8"; + +// RPKI CP (RFC 6484 / RFC 6487) +pub const OID_CP_IPADDR_ASNUMBER: &str = "1.3.6.1.5.5.7.14.2"; diff --git a/src/data_model/oids.rs b/src/data_model/oids.rs deleted file mode 100644 index 08cd5b4..0000000 --- a/src/data_model/oids.rs +++ /dev/null @@ -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"; diff --git a/src/data_model/rc.rs b/src/data_model/rc.rs index cc9e224..8b7d986 100644 --- a/src/data_model/rc.rs +++ b/src/data_model/rc.rs @@ -1,21 +1,80 @@ -use der_parser::asn1_rs::Tag; +use der_parser::ber::{BerObjectContent, Class}; +use der_parser::der::{parse_der, DerObject, Tag}; use der_parser::num_bigint::BigUint; -use url::Url; use time::OffsetDateTime; -use x509_parser::x509::AlgorithmIdentifier; -use x509_parser::prelude::{Validity, KeyUsage, X509Certificate, FromDer, - X509Version, X509Extension, ParsedExtension, - CRLDistributionPoints, DistributionPointName, GeneralName}; -use crate::data_model::crl::CrlDecodeError; -use crate::data_model::resources::ip_resources::IPAddrBlocks; -use crate::data_model::resources::as_resources::ASIdentifiers; -use crate::data_model::oids; +use url::Url; +use x509_parser::extensions::ParsedExtension; +use x509_parser::prelude::{FromDer, X509Certificate, X509Extension, X509Version}; +use crate::data_model::common::algorithm_params_absent_or_null; +use crate::data_model::oid::{ + OID_AD_SIGNED_OBJECT, OID_AUTONOMOUS_SYS_IDS, OID_CP_IPADDR_ASNUMBER, OID_IP_ADDR_BLOCKS, + OID_SHA256_WITH_RSA_ENCRYPTION, OID_SUBJECT_INFO_ACCESS, OID_SUBJECT_KEY_IDENTIFIER, +}; + +/// Resource Certificate kind (semantic classification). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ResourceCertKind { + Ca, + Ee, +} + +/// A parsed RPKI Resource Certificate (RFC 6487) data model. +/// +/// This module intentionally focuses on the semantics needed by Signed Object validation and +/// object-specific EE certificate checks (MFT/ROA/ASPA), as described in +/// `rpki/specs/03_resource_certificate_rc.md`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ResourceCertificate { + pub raw_der: Vec, + pub tbs: RpkixTbsCertificate, + pub kind: ResourceCertKind, +} + +pub type ResourceCaCertificate = ResourceCertificate; +pub type ResourceEeCertificate = ResourceCertificate; #[derive(Clone, Debug, PartialEq, Eq)] -pub struct SubjectPublicKeyInfo { - pub algorithm_oid: String, - pub subject_public_key: u8, +pub struct RpkixTbsCertificate { + pub version: u32, + pub serial_number: BigUint, + pub signature_algorithm: String, + pub issuer_dn: String, + pub subject_dn: String, + pub validity_not_before: OffsetDateTime, + pub validity_not_after: OffsetDateTime, + /// DER encoding of SubjectPublicKeyInfo. + pub subject_public_key_info: Vec, + pub extensions: RcExtensions, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RcExtensions { + pub basic_constraints_ca: bool, + pub subject_key_identifier: Option>, + pub subject_info_access: Option, + pub certificate_policies_oid: Option, + + pub ip_resources: Option, + pub as_resources: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SubjectInfoAccess { + Ca(SubjectInfoAccessCa), + Ee(SubjectInfoAccessEe), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SubjectInfoAccessCa { + pub access_descriptions: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SubjectInfoAccessEe { + pub signed_object_uris: Vec, + /// The full list of access descriptions as carried in the SIA extension. + pub access_descriptions: Vec, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -24,496 +83,738 @@ pub struct AccessDescription { pub access_location: Url, } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PolicyInformation { - pub policy_oid: String, +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum Afi { + Ipv4, + Ipv6, +} + +impl Afi { + pub fn ub(self) -> u16 { + match self { + Afi::Ipv4 => 32, + Afi::Ipv6 => 128, + } + } + + pub fn octets_len(self) -> usize { + match self { + Afi::Ipv4 => 4, + Afi::Ipv6 => 16, + } + } } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RcExtension { - pub basic_constraints: bool, - pub subject_key_identifier: u8, - pub authority_key_identifier: u8, - pub key_usage: KeyUsage, - pub extended_key_usage_oid: u8, - pub crl_distribution_points: Vec, - pub authority_info_access: Vec, - pub subject_info_access: Vec, - pub certificate_policies: Vec, - pub ip_resource: IPAddrBlocks, - pub as_resource: ASIdentifiers, - +pub struct IpResourceSet { + pub families: Vec, } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ResourceCert { - /// 证书原始DER内容 - pub cert_der: Vec, +impl IpResourceSet { + /// Decode the DER bytes carried inside the X.509 `extnValue` OCTET STRING for + /// `id-pe-ipAddrBlocks` (RFC 3779 / RFC 6487). + pub fn decode_extn_value(extn_value: &[u8]) -> Result { + parse_ip_addr_blocks(extn_value).map_err(|_| IpResourceSetDecodeError::InvalidEncoding) + } - /// 基本证书信息 - pub version: u32, - pub serial_number: BigUint, - pub signature_algorithm_oid: String, - pub issuer_dn: String, - pub subject_dn: String, - pub validity: Validity, - pub subject_public_key_info: SubjectPublicKeyInfo, - pub extensions: RcExtension, + pub fn is_all_inherit(&self) -> bool { + self.families + .iter() + .all(|f| matches!(f.choice, IpAddressChoice::Inherit)) + } + + pub fn has_any_inherit(&self) -> bool { + self.families + .iter() + .any(|f| matches!(f.choice, IpAddressChoice::Inherit)) + } + + pub fn contains_prefix(&self, prefix: &IpPrefix) -> bool { + self.families.iter().any(|fam| fam.contains_prefix(prefix)) + } } - #[derive(Debug, thiserror::Error)] -pub enum ResourceCertError { - #[error("X.509 parse resource cert error: {0}")] - ParseCert(String), - - #[error("trailing bytes after CRL DER: {0} bytes")] - TrailingBytes(usize), - - #[error("invalid version {0}")] - InvalidVersion(u32), - - #[error("signatureAlgorithm does not match tbsCertificate.signature")] - SignatureAlgorithmMismatch, - - #[error("unsupported signature algorithm")] - UnsupportedSignatureAlgorithm, - - #[error("invalid Cert signature algorithm parameters")] - InvalidSignatureParameters, - - #[error("invalid Cert validity range")] - InvalidValidityRange, - - #[error("Cert not yet valid")] - NotYetValid, - - #[error("expired")] - Expired, - - #[error("Critical error, {0} should be {1}")] - CriticalError(String, String), - - #[error("Duplicate Extension: {0}")] - DuplicateExtension(String), - - #[error("AKI missing keyIdentifier")] - AkiMissingKeyIdentifier, - - #[error("Unexpected parameter: {0}")] - UnexceptedParameter(String), - - #[error("Missing parameter: {0}")] - MissingParameter(String), - - #[error("CRL DP invalid distributionPointName: {0}")] - CrlDpInvalidDistributionPointName(String), - - #[error("CRL DP unexpected distributionPointType: {0}")] - CrlDpUnexpectedDistributionPointType(String), - - #[error("invalid URI: {0}")] - InvalidUri(String), - - #[error("Unsupported General Name in {0}")] - UnsupportedGeneralName(String), - - #[error("Unsupported CRL Distribution Point")] - UnsupportedCrlDistributionPoint, - - #[error("Invalid Access Location Type")] - InvalidAccessLocationType, - - #[error("Empty AuthorityInfoAccess!")] - EmptyAuthorityInfoAccess, - +pub enum IpResourceSetDecodeError { + #[error("invalid ipAddrBlocks encoding (RFC 3779 §2.2.3; RFC 6487 §4.8.10)")] + InvalidEncoding, } -// impl ResourceCert{ -// pub fn from_der(cert_der: &[u8]) -> Result { -// let (rem, x509_rc) = X509Certificate::from_der(cert_der) -// .map_err(|e| ResourceCertError::ParseCert(e.to_string()))?; -// -// if !rem.is_empty() { -// return Err(ResourceCertError::TrailingBytes(rem.len())); -// } -// -// // 校验 -// parse_and_validate_cert(x509_rc) -// } -// -// -// -// } -// -// fn parse_and_validate_cert(x509_rc: X509Certificate) -> Result { -// ///逐个校验RC的内容, 如果有任何一个校验失败, 则返回错误 -// -// // 1. 版本号必须是V3 -// let version = match x509_rc.version() { -// X509Version::V3 => X509Version::V3, -// v => { -// return Err(ResourceCertError::InvalidVersion(v.0)); -// } -// }; -// -// // 2.校验签名算法 -// // 2.1. 校验外层的签名算法与里层的一致 -// let outer = &x509_rc.signature_algorithm; -// let inner = &x509_rc.tbs_certificate.signature; -// -// if outer.algorithm != inner.algorithm || outer.parameters != inner.parameters { -// return Err(ResourceCertError::SignatureAlgorithmMismatch); -// } -// //2.2 RPKI的签名算法必须是rsaWithSHA256 -// let signature_algorithm = &x509_rc.signature_algorithm; -// if signature_algorithm.algorithm.to_id_string() != oids::OID_SHA256_WITH_RSA_ENCRYPTION { -// return Err(ResourceCertError::UnsupportedSignatureAlgorithm); -// } -// validate_sig_params(signature_algorithm)?; -// -// // 3. 校验Validity -// let validity = x509_rc.validity(); -// validate_validity(validity, OffsetDateTime::now_utc())?; -// -// // 4. SubjectPublicKeyInfo -// let subject_public_key_info = x509_rc.tbs_certificate.subject_pki; -// -// let extensions = parse_and_validate_extensions(x509_rc.extensions())?; -// -// Ok(ResourceCert { -// cert_der: x509_rc.to_der().to_vec(), -// version: version.0, -// serial_number: x509_rc.serial(), -// signature_algorithm_oid: signature_algorithm.algorithm.to_id_string(), -// issuer_dn: x509_rc.issuer().to_string(), -// subject_dn: x509_rc.subject().to_string(), -// validity, -// subject_public_key_info: SubjectPublicKeyInfo { -// // algorithm_oid: x509_rc.tbs_certificate.subject_pki.algorithm.algorithm.to_id_string(), -// // subject_public_key: x509_rc.tbs_certificate.subject_pki.subject_public_key.unused_bits, -// }, -// extensions, -// }) -// -// -// } -// -// fn validate_sig_params(sig: &AlgorithmIdentifier<'_>) -> Result<(), CrlDecodeError> { -// match sig.parameters.as_ref() { -// None => Ok(()), -// Some(p) if p.tag() == Tag::Null => Ok(()), -// Some(_p) => Err(CrlDecodeError::InvalidSignatureAlgorithmParameters), -// } -// } -// -// fn validate_validity( -// validity: &Validity, -// now: OffsetDateTime, -// ) -> Result<(), ResourceCertError> { -// let not_before = validity.not_before.to_datetime(); -// let not_after = validity.not_after.to_datetime(); -// -// if not_after < not_before { -// return Err(ResourceCertError::InvalidValidityRange); -// } -// -// if now < not_before { -// return Err(ResourceCertError::NotYetValid); -// } -// -// if now > not_after { -// return Err(ResourceCertError::Expired); -// } -// -// Ok(()) -// } -// -// -// pub fn parse_and_validate_extensions( -// exts: &[X509Extension<'_>], -// ) -> Result { -// let mut basic_constraints = None; -// let mut ip_addr_blocks = None; -// let mut as_identifiers = None; -// let mut ski = None; -// let mut aki = None; -// let mut crl_dp = None; -// let mut aia = None; -// let mut sia = None; -// let mut key_usage = None; -// let mut extended_key_usage = None; -// let mut certificate_policies = None; -// -// for ext in exts { -// let oid = ext.oid.to_id_string(); -// let critical = ext.critical; -// match oid.as_str() { -// oids::OID_BASIC_CONSTRAINTS => { -// if basic_constraints.is_some() { -// return Err(ResourceCertError::DuplicateExtension("basicConstraints".into())); -// } -// if !critical { -// return Err(ResourceCertError::CriticalError("basicConstraints".into(), "critical".into())); -// } -// let bc = parse_basic_constraints(ext)?; -// basic_constraints = Some(bc); -// } -// oids::OID_SUBJECT_KEY_IDENTIFIER => { -// if ski.is_some() { -// return Err(ResourceCertError::DuplicateExtension("subjectKeyIdentifier".into())); -// } -// if critical { -// return Err(ResourceCertError::CriticalError("subjectKeyIdentifier".into(), "non-critical".into())); -// } -// let s = parse_subject_key_identifier(ext)?; -// ski = Some(s); -// } -// oids::OID_AUTHORITY_KEY_IDENTIFIER => { -// if aki.is_some() { -// return Err(ResourceCertError::DuplicateExtension("authorityKeyIdentifier".into())); -// } -// if critical { -// return Err(ResourceCertError::CriticalError("authorityKeyIdentifier".into(), "non-critical".into())); -// } -// let a = parse_authority_key_identifier(ext)?; -// aki = Some(a); -// } -// oids::OID_KEY_USAGE => { -// if key_usage.is_some() { -// return Err(ResourceCertError::DuplicateExtension("keyUsage".into())); -// } -// if !critical { -// return Err(ResourceCertError::CriticalError("keyUsage".into(), "critical".into())); -// } -// let ku = parse_key_usage(ext)?; -// key_usage = Some(ku); -// } -// oids::OID_EXTENDED_KEY_USAGE => { -// if extended_key_usage.is_some() { -// return Err(ResourceCertError::DuplicateExtension("extendedKeyUsage".into())); -// } -// if critical { -// return Err(ResourceCertError::CriticalError("extendedKeyUsage".into(), "non-critical".into())); -// } -// let eku = oids::OID_EXTENDED_KEY_USAGE; -// } -// oids::OID_CRL_DISTRIBUTION_POINTS => { -// if crl_dp.is_some() { -// return Err(ResourceCertError::DuplicateExtension("crlDistributionPoints".into())); -// } -// if critical { -// return Err(ResourceCertError::CriticalError("crlDistributionPoints".into(), "non-critical".into())); -// } -// let cdp = parse_crl_distribution_points(ext)?; -// crl_dp = Some(cdp); -// } -// oids::OID_AUTHORITY_INFO_ACCESS => { -// if aia.is_some() { -// return Err(ResourceCertError::DuplicateExtension("authorityInfoAccess".into())); -// } -// if critical { -// return Err(ResourceCertError::CriticalError("authorityInfoAccess".into(), "non-critical".into())); -// } -// let p_aia = parse_authority_info_access(ext)?; -// aia = Some(p_aia); -// } -// oids::OID_SUBJECT_INFO_ACCESS => { -// if sia.is_some() { -// return Err(ResourceCertError::DuplicateExtension("subjectInfoAccess".into())); -// } -// if critical { -// return Err(ResourceCertError::CriticalError("subjectInfoAccess".into(), "non-critical".into())); -// } -// let p_sia = parse_subject_info_access(ext)?; -// sia = Some(p_sia); -// } -// oids::OID_CERTIFICATE_POLICIES => { -// if certificate_policies.is_some() { -// return Err(ResourceCertError::DuplicateExtension("certificatePolicies".into())); -// } -// if !critical { -// return Err(ResourceCertError::CriticalError("certificatePolicies".into(), "critical".into())); -// } -// let p_cp = parse_certificate_policies(ext)?; -// certificate_policies = Some(p_cp); -// } -// } -// -// -// } -// Ok(RcExtension { -// basic_constraints, -// ip_addr_blocks, -// as_identifiers, -// subject_key_id: ski, -// authority_key_id: aki, -// crl_distribution_points: crl_dp, -// authority_info_access: aia, -// }) -// } -// -// fn parse_basic_constraints(ext: &X509Extension<'_>) -> Result { -// let ParsedExtension::BasicConstraints(bc) = ext.parsed_extension() else { -// return Err(ResourceCertError::ParseCert("basicConstraints parse failed".into())); -// }; -// Ok(bc.ca) -// } -// -// fn parse_subject_key_identifier(ext: &X509Extension<'_>) -> Result, ResourceCertError> { -// let ParsedExtension::SubjectKeyIdentifier(s) = ext.parsed_extension() else { -// return Err(ResourceCertError::ParseCert("subjectKeyIdentifier parse failed".into())); -// }; -// Ok(s.0.to_vec()) -// } -// -// fn parse_authority_key_identifier(ext: &X509Extension<'_>) -> Result, ResourceCertError> { -// let ParsedExtension::AuthorityKeyIdentifier(aki) = ext.parsed_extension() else { -// return Err(ResourceCertError::ParseCert("authorityKeyIdentifier parse failed".into())); -// }; -// let key_id = aki -// .key_identifier -// .as_ref() -// .ok_or(ResourceCertError::MissingParameter("key_identifier".into()))?; -// -// if aki.authority_cert_issuer.is_some() { -// return Err(ResourceCertError::UnexceptedParameter("authority_cert_issuer".into())); -// } -// if aki.authority_cert_serial.is_some() { -// return Err(ResourceCertError::UnexceptedParameter("authority_cert_serial".into())); -// } -// -// -// Ok(key_id.0.to_vec()) -// } -// -// fn parse_key_usage(ext: &X509Extension<'_>) -> Result { -// let ParsedExtension::KeyUsage(ku) = ext.parsed_extension() else { -// return Err(ResourceCertError::ParseCert("keyUsage parse failed".into())); -// }; -// Ok(ku.clone()) -// } -// -// fn parse_crl_distribution_points(ext: &X509Extension<'_>) -> Result, ResourceCertError> { -// let ParsedExtension::CRLDistributionPoints(cdp) = ext.parsed_extension() else { -// return Err(ResourceCertError::ParseCert("crlDistributionPoints parse failed".into())); -// }; -// let mut urls = Vec::new(); -// for point in cdp.points.iter() { -// if point.reasons.is_some() { -// return Err(ResourceCertError::UnexceptedParameter("reasons".into())); -// } -// if point.crl_issuer.is_some() { -// return Err(ResourceCertError::UnexceptedParameter("crl_issuer".into())); -// } -// -// let dp_name = point.distribution_point.as_ref() -// .ok_or(ResourceCertError::MissingParameter("distribution_point".into()))?; -// match dp_name { -// DistributionPointName::FullName(names) => { -// for name in names { -// match name { -// GeneralName::URI(uri) => { -// let url = Url::parse(uri) -// .map_err(|_| ResourceCertError::InvalidUri(uri.to_string()))?; -// urls.push(url); -// } -// _ => { -// return Err(ResourceCertError::UnsupportedGeneralName("distribution_point".into())); -// } -// } -// } -// -// } -// DistributionPointName::NameRelativeToCRLIssuer(_) => { -// return Err(ResourceCertError::UnsupportedCrlDistributionPoint); -// } -// } -// } -// if urls.is_empty() { -// return Err(ResourceCertError::MissingParameter("distribution_point".into())); -// } -// Ok(urls) -// } -// -// fn parse_authority_info_access( -// ext: &X509Extension<'_>, -// ) -> Result, ResourceCertError> { -// let ParsedExtension::AuthorityInfoAccess(aia) = ext.parsed_extension() else { -// return Err(ResourceCertError::ParseCert( -// "authorityInfoAccess parse failed".into(), -// )); -// }; -// -// let mut access_descriptions = Vec::new(); -// -// for access in &aia.accessdescs { -// let access_method_oid = access.access_method.to_id_string(); -// -// let uri = match &access.access_location { -// GeneralName::URI(uri) => uri, -// _ => { -// return Err(ResourceCertError::InvalidAccessLocationType); -// } -// }; -// -// let url = Url::parse(uri) -// .map_err(|_| ResourceCertError::InvalidUri(uri.to_string()))?; -// -// access_descriptions.push(AccessDescription { -// access_method_oid, -// access_location: url, -// }); -// } -// -// if access_descriptions.is_empty() { -// return Err(ResourceCertError::EmptyAuthorityInfoAccess); -// } -// -// Ok(access_descriptions) -// } -// -// fn parse_subject_info_access(ext: &X509Extension<'_>) -> Result, ResourceCertError> { -// let ParsedExtension::SubjectInfoAccess(sia) = ext.parsed_extension() else { -// return Err(ResourceCertError::ParseCert( -// "subjectInfoAccess parse failed".into(), -// )); -// }; -// let mut access_descriptions = Vec::new(); -// -// for access in &sia.accessdescs { -// let access_method_oid = access.access_method.to_id_string(); -// -// // accessLocation: MUST be URI in RPKI -// let uri = match &access.access_location { -// GeneralName::URI(uri) => uri, -// _ => { -// return Err(ResourceCertError::InvalidAccessLocationType); -// } -// }; -// -// let url = Url::parse(uri) -// .map_err(|_| ResourceCertError::InvalidUri(uri.to_string()))?; -// -// access_descriptions.push(AccessDescription { -// access_method_oid, -// access_location: url, -// }); -// } -// -// if access_descriptions.is_empty() { -// return Err(ResourceCertError::EmptyAuthorityInfoAccess); -// } -// -// Ok(access_descriptions) -// } -// -// fn parse_certificate_policies(ext: &X509Extension<'_>) -> Result, ResourceCertError> { -// let ParsedExtension::CertificatePolicies(cp) = ext.parsed_extension() else { -// return Err(ResourceCertError::ParseCert( -// "certificatePolicies parse failed".into(), -// )); -// }; -// let mut policies = Vec::new(); -// -// } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct IpAddressFamily { + pub afi: Afi, + pub choice: IpAddressChoice, +} + +impl IpAddressFamily { + pub fn contains_prefix(&self, prefix: &IpPrefix) -> bool { + if self.afi != prefix.afi { + return false; + } + match &self.choice { + IpAddressChoice::Inherit => true, + IpAddressChoice::AddressesOrRanges(items) => items.iter().any(|item| match item { + IpAddressOrRange::Prefix(p) => prefix_covers(p, prefix), + IpAddressOrRange::Range(r) => range_covers_prefix(self.afi, r, prefix), + }), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum IpAddressChoice { + Inherit, + AddressesOrRanges(Vec), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum IpAddressOrRange { + Prefix(IpPrefix), + Range(IpAddressRange), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct IpAddressRange { + pub min: Vec, + pub max: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct IpPrefix { + pub afi: Afi, + pub prefix_len: u16, + pub addr: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AsResourceSet { + pub asnum: Option, + pub rdi: Option, +} + +impl AsResourceSet { + /// Decode the DER bytes carried inside the X.509 `extnValue` OCTET STRING for + /// `id-pe-autonomousSysIds` (RFC 3779 / RFC 6487). + pub fn decode_extn_value(extn_value: &[u8]) -> Result { + parse_as_identifiers(extn_value).map_err(|_| AsResourceSetDecodeError::InvalidEncoding) + } + + pub fn is_asnum_inherit(&self) -> bool { + matches!(self.asnum, Some(AsIdentifierChoice::Inherit)) + } + + pub fn has_any_range(&self) -> bool { + self.asnum + .as_ref() + .map(|c| c.has_range()) + .unwrap_or(false) + || self.rdi.as_ref().map(|c| c.has_range()).unwrap_or(false) + } + + pub fn asnum_single_id(&self) -> Option { + match self.asnum.as_ref()? { + AsIdentifierChoice::Inherit => None, + AsIdentifierChoice::AsIdsOrRanges(items) => { + if items.len() != 1 { + return None; + } + match &items[0] { + AsIdOrRange::Id(v) => Some(*v), + AsIdOrRange::Range { .. } => None, + } + } + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum AsResourceSetDecodeError { + #[error("invalid autonomousSysIds encoding (RFC 3779 §3.2.3; RFC 6487 §4.8.11)")] + InvalidEncoding, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum AsIdentifierChoice { + Inherit, + AsIdsOrRanges(Vec), +} + +impl AsIdentifierChoice { + pub fn has_range(&self) -> bool { + match self { + AsIdentifierChoice::Inherit => false, + AsIdentifierChoice::AsIdsOrRanges(items) => items.iter().any(|i| matches!(i, AsIdOrRange::Range { .. })), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum AsIdOrRange { + Id(u32), + Range { min: u32, max: u32 }, +} + +#[derive(Debug, thiserror::Error)] +pub enum ResourceCertificateError { + #[error("X.509 parse error: {0} (RFC 5280 §4.1; RFC 6487 §4)")] + Parse(String), + + #[error("trailing bytes after certificate DER: {0} bytes (DER; RFC 5280 §4.1)")] + TrailingBytes(usize), + + #[error("certificate version must be v3 (RFC 5280 §4.1; RFC 6487 §4)")] + InvalidVersion, + + #[error("signatureAlgorithm does not match tbsCertificate.signature (RFC 5280 §4.1)")] + SignatureAlgorithmMismatch, + + #[error("unsupported signature algorithm (expected sha256WithRSAEncryption {OID_SHA256_WITH_RSA_ENCRYPTION}) (RFC 7935 §2; RFC 6487 §4)")] + UnsupportedSignatureAlgorithm, + + #[error("invalid signature algorithm parameters (RFC 5280 §4.1.1.2)")] + InvalidSignatureAlgorithmParameters, + + #[error("duplicate extension: {0} (RFC 5280 §4.2; RFC 6487 §4.8)")] + DuplicateExtension(&'static str), + + #[error("SubjectKeyIdentifier criticality must be non-critical (RFC 6487 §4.8.2)")] + SkiCriticality, + + #[error("SubjectInfoAccess criticality must be non-critical (RFC 6487 §4.8.8)")] + SiaCriticality, + + #[error("certificatePolicies criticality must be critical (RFC 6487 §4.8.9)")] + CertificatePoliciesCriticality, + + #[error("certificatePolicies must contain RPKI policy OID {OID_CP_IPADDR_ASNUMBER}, got {0} (RFC 6487 §4.8.9)")] + InvalidCertificatePolicy(String), + + #[error("SIA id-ad-signedObject accessLocation must be URI (RFC 6487 §4.8.8.2; RFC 5280 §4.2.2.2)")] + SignedObjectSiaNotUri, + + #[error("SIA id-ad-signedObject must include at least one rsync:// URI (RFC 6487 §4.8.8.2)")] + SignedObjectSiaNoRsync, + + #[error("invalid RFC 3779 IP resources extension (RFC 6487 §4.8.10; RFC 3779 §2.2)")] + InvalidIpResources, + + #[error("invalid RFC 3779 AS resources extension (RFC 6487 §4.8.11; RFC 3779 §3.2)")] + InvalidAsResources, +} + +impl ResourceCertificate { + pub fn from_der(der: &[u8]) -> Result { + let (rem, cert) = + X509Certificate::from_der(der).map_err(|e| ResourceCertificateError::Parse(e.to_string()))?; + if !rem.is_empty() { + return Err(ResourceCertificateError::TrailingBytes(rem.len())); + } + + let version = match cert.version() { + X509Version::V3 => 2u32, + _ => return Err(ResourceCertificateError::InvalidVersion), + }; + + let outer = &cert.signature_algorithm; + let inner = &cert.tbs_certificate.signature; + if outer.algorithm != inner.algorithm || outer.parameters != inner.parameters { + return Err(ResourceCertificateError::SignatureAlgorithmMismatch); + } + if outer.algorithm.to_id_string() != OID_SHA256_WITH_RSA_ENCRYPTION { + return Err(ResourceCertificateError::UnsupportedSignatureAlgorithm); + } + if !algorithm_params_absent_or_null(outer) { + return Err(ResourceCertificateError::InvalidSignatureAlgorithmParameters); + } + + let validity_not_before = cert.validity().not_before.to_datetime(); + let validity_not_after = cert.validity().not_after.to_datetime(); + + let subject_public_key_info = cert.tbs_certificate.subject_pki.raw.to_vec(); + + let extensions = parse_extensions(cert.extensions())?; + let kind = if extensions.basic_constraints_ca { + ResourceCertKind::Ca + } else { + ResourceCertKind::Ee + }; + + Ok(ResourceCertificate { + raw_der: der.to_vec(), + tbs: RpkixTbsCertificate { + version, + serial_number: cert.tbs_certificate.serial.clone(), + signature_algorithm: outer.algorithm.to_id_string(), + issuer_dn: cert.issuer().to_string(), + subject_dn: cert.subject().to_string(), + validity_not_before, + validity_not_after, + subject_public_key_info, + extensions, + }, + kind, + }) + } +} + +fn parse_extensions(exts: &[X509Extension<'_>]) -> Result { + let mut basic_constraints_ca: Option = None; + let mut ski: Option> = None; + let mut sia: Option = None; + let mut cert_policies_oid: Option = None; + + let mut ip_resources: Option = None; + let mut as_resources: Option = None; + + for ext in exts { + let oid = ext.oid.to_id_string(); + match oid.as_str() { + crate::data_model::oid::OID_BASIC_CONSTRAINTS => { + if basic_constraints_ca.is_some() { + return Err(ResourceCertificateError::DuplicateExtension("basicConstraints")); + } + let ParsedExtension::BasicConstraints(bc) = ext.parsed_extension() else { + return Err(ResourceCertificateError::Parse("basicConstraints parse failed".into())); + }; + basic_constraints_ca = Some(bc.ca); + } + OID_SUBJECT_KEY_IDENTIFIER => { + if ski.is_some() { + return Err(ResourceCertificateError::DuplicateExtension("subjectKeyIdentifier")); + } + if ext.critical { + return Err(ResourceCertificateError::SkiCriticality); + } + let ParsedExtension::SubjectKeyIdentifier(s) = ext.parsed_extension() else { + return Err(ResourceCertificateError::Parse("subjectKeyIdentifier parse failed".into())); + }; + ski = Some(s.0.to_vec()); + } + OID_SUBJECT_INFO_ACCESS => { + if sia.is_some() { + return Err(ResourceCertificateError::DuplicateExtension("subjectInfoAccess")); + } + if ext.critical { + return Err(ResourceCertificateError::SiaCriticality); + } + let ParsedExtension::SubjectInfoAccess(s) = ext.parsed_extension() else { + return Err(ResourceCertificateError::Parse("subjectInfoAccess parse failed".into())); + }; + sia = Some(parse_sia(s.accessdescs.as_slice())?); + } + crate::data_model::oid::OID_CERTIFICATE_POLICIES => { + if cert_policies_oid.is_some() { + return Err(ResourceCertificateError::DuplicateExtension("certificatePolicies")); + } + if !ext.critical { + return Err(ResourceCertificateError::CertificatePoliciesCriticality); + } + let ParsedExtension::CertificatePolicies(cp) = ext.parsed_extension() else { + return Err(ResourceCertificateError::Parse("certificatePolicies parse failed".into())); + }; + if cp.len() != 1 { + return Err(ResourceCertificateError::InvalidCertificatePolicy( + "expected exactly one policy".into(), + )); + } + let policy_oid = cp[0].policy_id.to_id_string(); + if policy_oid != OID_CP_IPADDR_ASNUMBER { + return Err(ResourceCertificateError::InvalidCertificatePolicy(policy_oid)); + } + cert_policies_oid = Some(OID_CP_IPADDR_ASNUMBER.to_string()); + } + OID_IP_ADDR_BLOCKS => { + if ip_resources.is_some() { + return Err(ResourceCertificateError::DuplicateExtension("ipAddrBlocks")); + } + // Must be critical per RPKI profile; we only enforce when present. + if !ext.critical { + return Err(ResourceCertificateError::InvalidIpResources); + } + let parsed = IpResourceSet::decode_extn_value(ext.value) + .map_err(|_e| ResourceCertificateError::InvalidIpResources)?; + ip_resources = Some(parsed); + } + OID_AUTONOMOUS_SYS_IDS => { + if as_resources.is_some() { + return Err(ResourceCertificateError::DuplicateExtension("autonomousSysIds")); + } + if !ext.critical { + return Err(ResourceCertificateError::InvalidAsResources); + } + let parsed = AsResourceSet::decode_extn_value(ext.value) + .map_err(|_e| ResourceCertificateError::InvalidAsResources)?; + as_resources = Some(parsed); + } + _ => {} + } + } + + let basic_constraints_ca = basic_constraints_ca.unwrap_or(false); + + Ok(RcExtensions { + basic_constraints_ca, + subject_key_identifier: ski, + subject_info_access: sia, + certificate_policies_oid: cert_policies_oid, + ip_resources, + as_resources, + }) +} + +fn parse_sia(access: &[x509_parser::extensions::AccessDescription<'_>]) -> Result { + let mut all = Vec::with_capacity(access.len()); + let mut signed_object_uris: Vec = Vec::new(); + + for ad in access { + let access_method_oid = ad.access_method.to_id_string(); + let uri = match &ad.access_location { + x509_parser::extensions::GeneralName::URI(u) => u, + _ => { + if access_method_oid == OID_AD_SIGNED_OBJECT { + return Err(ResourceCertificateError::SignedObjectSiaNotUri); + } + continue; + } + }; + let url = + Url::parse(uri).map_err(|_| ResourceCertificateError::Parse(format!("invalid URI: {uri}")))?; + if access_method_oid == OID_AD_SIGNED_OBJECT { + signed_object_uris.push(url.clone()); + } + all.push(AccessDescription { + access_method_oid, + access_location: url, + }); + } + + if signed_object_uris.is_empty() { + return Ok(SubjectInfoAccess::Ca(SubjectInfoAccessCa { + access_descriptions: all, + })); + } + + if !signed_object_uris.iter().any(|u| u.scheme() == "rsync") { + return Err(ResourceCertificateError::SignedObjectSiaNoRsync); + } + + Ok(SubjectInfoAccess::Ee(SubjectInfoAccessEe { + signed_object_uris, + access_descriptions: all, + })) +} + +fn parse_ip_addr_blocks(ext_value: &[u8]) -> Result { + let (rem, obj) = parse_der(ext_value).map_err(|_| ())?; + if !rem.is_empty() { + return Err(()); + } + let seq = obj.as_sequence().map_err(|_| ())?; + let mut families = Vec::with_capacity(seq.len()); + for fam in seq { + let fam_seq = fam.as_sequence().map_err(|_| ())?; + if fam_seq.len() != 2 { + return Err(()); + } + let af_bytes = fam_seq[0].as_slice().map_err(|_| ())?; + if af_bytes.len() != 2 { + return Err(()); + } + let afi = match af_bytes { + [0x00, 0x01] => Afi::Ipv4, + [0x00, 0x02] => Afi::Ipv6, + _ => return Err(()), + }; + + let choice = match &fam_seq[1].content { + BerObjectContent::Null => IpAddressChoice::Inherit, + BerObjectContent::Sequence(_) => { + let items_seq = fam_seq[1].as_sequence().map_err(|_| ())?; + let mut items = Vec::with_capacity(items_seq.len()); + for item in items_seq { + items.push(parse_ip_address_or_range(afi, item)?); + } + IpAddressChoice::AddressesOrRanges(items) + } + _ => return Err(()), + }; + families.push(IpAddressFamily { afi, choice }); + } + Ok(IpResourceSet { families }) +} + +fn parse_ip_address_or_range(afi: Afi, obj: &DerObject<'_>) -> Result { + match &obj.content { + BerObjectContent::BitString(_, _) => Ok(IpAddressOrRange::Prefix(parse_ip_prefix(afi, obj)?)), + BerObjectContent::Sequence(_) => { + let seq = obj.as_sequence().map_err(|_| ())?; + if seq.len() != 2 { + return Err(()); + } + let min = parse_ip_address_bound(afi, &seq[0], false)?; + let max = parse_ip_address_bound(afi, &seq[1], true)?; + Ok(IpAddressOrRange::Range(IpAddressRange { min, max })) + } + _ => Err(()), + } +} + +fn parse_ip_prefix(afi: Afi, obj: &DerObject<'_>) -> Result { + let (unused_bits, bytes) = match &obj.content { + BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()), + _ => return Err(()), + }; + if unused_bits > 7 { + return Err(()); + } + if !bytes.is_empty() && unused_bits != 0 { + let mask = (1u8 << unused_bits) - 1; + if (bytes[bytes.len() - 1] & mask) != 0 { + return Err(()); + } + } else if bytes.is_empty() && unused_bits != 0 { + return Err(()); + } + let prefix_len = (bytes.len() * 8) + .checked_sub(unused_bits as usize) + .ok_or(())? as u16; + if prefix_len > afi.ub() { + return Err(()); + } + let addr = canonicalize_prefix_addr(afi, prefix_len, &bytes); + Ok(IpPrefix { + afi, + prefix_len, + addr, + }) +} + +/// Parse an RFC 3779 `IPAddress` BIT STRING into an address-like byte array. +/// +/// When used as an `IPAddressRange` endpoint, RFC 3779 allows endpoints to be encoded with +/// fewer than `ub` bits. In that case, the missing bits are interpreted as 0s for the lower +/// bound and 1s for the upper bound. This is essential to correctly interpret ranges that +/// are expressed on non-octet boundaries. +fn parse_ip_address_bound(afi: Afi, obj: &DerObject<'_>, fill_remaining_ones: bool) -> Result, ()> { + let (unused_bits, bytes) = match &obj.content { + BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()), + _ => return Err(()), + }; + if unused_bits > 7 { + return Err(()); + } + if !bytes.is_empty() && unused_bits != 0 { + let mask = (1u8 << unused_bits) - 1; + if (bytes[bytes.len() - 1] & mask) != 0 { + return Err(()); + } + } else if bytes.is_empty() && unused_bits != 0 { + return Err(()); + } + + let bit_len: u16 = (bytes.len() * 8) + .checked_sub(unused_bits as usize) + .ok_or(())? + .try_into() + .map_err(|_| ())?; + if bit_len > afi.ub() { + return Err(()); + } + + let mut out = vec![0u8; afi.octets_len()]; + let copy_len = bytes.len().min(out.len()); + out[..copy_len].copy_from_slice(&bytes[..copy_len]); + + if fill_remaining_ones { + if bit_len == 0 { + for b in &mut out { + *b = 0xFF; + } + return Ok(out); + } + + let last_bit = (bit_len - 1) as usize; + let last_byte = last_bit / 8; + let rem = (bit_len % 8) as u8; + + if rem != 0 && last_byte < out.len() { + // Set the (8-rem) trailing bits in the last byte to 1. + let mask: u8 = (1u8 << (8 - rem)) - 1; + out[last_byte] |= mask; + } + for b in out.iter_mut().skip(last_byte + 1) { + *b = 0xFF; + } + } + + Ok(out) +} + +fn parse_as_identifiers(ext_value: &[u8]) -> Result { + let (rem, obj) = parse_der(ext_value).map_err(|_| ())?; + if !rem.is_empty() { + return Err(()); + } + let seq = obj.as_sequence().map_err(|_| ())?; + let mut asnum: Option = None; + let mut rdi: Option = None; + for item in seq { + if item.class() != Class::ContextSpecific { + return Err(()); + } + match item.tag() { + Tag(0) => { + if asnum.is_some() { + return Err(()); + } + let inner = parse_explicit_inner(item)?; + asnum = Some(parse_as_identifier_choice(&inner)?); + } + Tag(1) => { + if rdi.is_some() { + return Err(()); + } + let inner = parse_explicit_inner(item)?; + rdi = Some(parse_as_identifier_choice(&inner)?); + } + _ => return Err(()), + } + } + Ok(AsResourceSet { asnum, rdi }) +} + +fn parse_explicit_inner<'a>(obj: &'a DerObject<'a>) -> Result, ()> { + let inner_der = obj.as_slice().map_err(|_| ())?; + let (rem, inner) = parse_der(inner_der).map_err(|_| ())?; + if !rem.is_empty() { + return Err(()); + } + Ok(inner) +} + +fn parse_as_identifier_choice(obj: &DerObject<'_>) -> Result { + match &obj.content { + BerObjectContent::Null => Ok(AsIdentifierChoice::Inherit), + BerObjectContent::Sequence(_) => { + let seq = obj.as_sequence().map_err(|_| ())?; + let mut items = Vec::with_capacity(seq.len()); + for item in seq { + items.push(parse_as_id_or_range(item)?); + } + Ok(AsIdentifierChoice::AsIdsOrRanges(items)) + } + _ => Err(()), + } +} + +fn parse_as_id_or_range(obj: &DerObject<'_>) -> Result { + match &obj.content { + BerObjectContent::Integer(_) => { + let v = obj.as_u64().map_err(|_| ())?; + if v > u32::MAX as u64 { + return Err(()); + } + Ok(AsIdOrRange::Id(v as u32)) + } + BerObjectContent::Sequence(_) => { + let seq = obj.as_sequence().map_err(|_| ())?; + if seq.len() != 2 { + return Err(()); + } + let min = seq[0].as_u64().map_err(|_| ())?; + let max = seq[1].as_u64().map_err(|_| ())?; + if min > u32::MAX as u64 || max > u32::MAX as u64 || min > max { + return Err(()); + } + Ok(AsIdOrRange::Range { + min: min as u32, + max: max as u32, + }) + } + _ => Err(()), + } +} + +fn canonicalize_prefix_addr(afi: Afi, prefix_len: u16, bytes: &[u8]) -> Vec { + let full_len = afi.octets_len(); + let mut addr = vec![0u8; full_len]; + let copy_len = bytes.len().min(full_len); + addr[..copy_len].copy_from_slice(&bytes[..copy_len]); + + if prefix_len == 0 { + return addr; + } + + let last_prefix_bit = (prefix_len - 1) as usize; + let last_prefix_byte = last_prefix_bit / 8; + let rem = (prefix_len % 8) as u8; + if rem != 0 && last_prefix_byte < addr.len() { + let mask: u8 = 0xFF << (8 - rem); + addr[last_prefix_byte] &= mask; + } + addr +} + +fn prefix_covers(resource: &IpPrefix, subject: &IpPrefix) -> bool { + if resource.afi != subject.afi { + return false; + } + if resource.prefix_len > subject.prefix_len { + return false; + } + let n = resource.prefix_len as usize; + let whole = n / 8; + let rem = (n % 8) as u8; + if resource.addr.len() != subject.addr.len() { + return false; + } + if resource.addr[..whole] != subject.addr[..whole] { + return false; + } + if rem == 0 { + return true; + } + let mask = 0xFFu8 << (8 - rem); + (resource.addr[whole] & mask) == (subject.addr[whole] & mask) +} + +fn prefix_range(afi: Afi, p: &IpPrefix) -> (u128, u128) { + let mut base_bytes = [0u8; 16]; + match afi { + Afi::Ipv4 => { + base_bytes[12..].copy_from_slice(&p.addr[..4]); + } + Afi::Ipv6 => { + base_bytes.copy_from_slice(&p.addr[..16]); + } + } + let base = u128::from_be_bytes(base_bytes); + let host_bits = (afi.ub() - p.prefix_len) as u32; + if host_bits == 0 { + return (base, base); + } + let mask = (1u128 << host_bits) - 1; + (base, base | mask) +} + +fn range_covers_prefix(afi: Afi, r: &IpAddressRange, p: &IpPrefix) -> bool { + let (p_min, p_max) = prefix_range(afi, p); + let r_min = bytes_to_u128(afi, &r.min); + let r_max = bytes_to_u128(afi, &r.max); + r_min <= p_min && p_max <= r_max +} + +fn bytes_to_u128(afi: Afi, bytes: &[u8]) -> u128 { + let mut out = [0u8; 16]; + match afi { + Afi::Ipv4 => { + let copy_len = bytes.len().min(4); + out[12..12 + copy_len].copy_from_slice(&bytes[..copy_len]); + } + Afi::Ipv6 => { + let copy_len = bytes.len().min(16); + out[..copy_len].copy_from_slice(&bytes[..copy_len]); + } + } + u128::from_be_bytes(out) +} diff --git a/src/data_model/resources/as_resources.rs b/src/data_model/resources/as_resources.rs deleted file mode 100644 index 13eb095..0000000 --- a/src/data_model/resources/as_resources.rs +++ /dev/null @@ -1,90 +0,0 @@ - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ASIdentifiers { - pub asn: Vec -} - - - - -// ASN -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ASIdentifierChoice { - Inherit, - ASIDsOrRanges(Vec), -} - -#[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 for Asn { - fn from(id: u32) -> Self { - Asn(id) - } -} - -impl From for u32 { - fn from(id: Asn) -> Self { - id.0 - } -} \ No newline at end of file diff --git a/src/data_model/resources/ip_resources.rs b/src/data_model/resources/ip_resources.rs deleted file mode 100644 index 9590bf8..0000000 --- a/src/data_model/resources/ip_resources.rs +++ /dev/null @@ -1,46 +0,0 @@ - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct IPAddrBlocks { - ips: Vec -} - - -// 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), -} - -#[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); \ No newline at end of file diff --git a/src/data_model/resources/mod.rs b/src/data_model/resources/mod.rs deleted file mode 100644 index 39284d6..0000000 --- a/src/data_model/resources/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub(crate) mod ip_resources; -pub(crate) mod as_resources; -pub mod resource; \ No newline at end of file diff --git a/src/data_model/resources/resource.rs b/src/data_model/resources/resource.rs deleted file mode 100644 index 98b442c..0000000 --- a/src/data_model/resources/resource.rs +++ /dev/null @@ -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, -} - - diff --git a/src/data_model/roa.rs b/src/data_model/roa.rs index b69192d..e806d85 100644 --- a/src/data_model/roa.rs +++ b/src/data_model/roa.rs @@ -1,4 +1,5 @@ use crate::data_model::oid::OID_CT_ROUTE_ORIGIN_AUTHZ; +use crate::data_model::rc::{Afi as RcAfi, IpPrefix as RcIpPrefix, ResourceCertificate}; use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError}; use der_parser::ber::{BerObjectContent, Class}; use der_parser::der::{parse_der, DerObject, Tag}; @@ -19,58 +20,58 @@ pub struct RoaEContent { #[derive(Debug, thiserror::Error)] pub enum RoaDecodeError { - #[error("SignedObject decode error: {0}")] + #[error("SignedObject decode error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")] SignedObjectDecode(#[from] SignedObjectDecodeError), - #[error("ROA eContentType must be {OID_CT_ROUTE_ORIGIN_AUTHZ}, got {0}")] + #[error("ROA eContentType must be {OID_CT_ROUTE_ORIGIN_AUTHZ}, got {0} (RFC 9582 §3)")] InvalidEContentType(String), - #[error("ROA parse error: {0}")] + #[error("ROA parse error: {0} (RFC 9582 §4; DER)")] Parse(String), - #[error("ROA trailing bytes: {0} bytes")] + #[error("ROA trailing bytes: {0} bytes (RFC 9582 §4; DER)")] TrailingBytes(usize), - #[error("RouteOriginAttestation must be a SEQUENCE of 2 or 3 elements, got {0}")] + #[error("RouteOriginAttestation must be a SEQUENCE of 2 or 3 elements, got {0} (RFC 9582 §4)")] InvalidAttestationSequenceLen(usize), - #[error("ROA version must be 0, got {0}")] + #[error("ROA version must be 0, got {0} (RFC 9582 §4.1)")] 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), - #[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), - #[error("ROAIPAddressFamily must be a SEQUENCE of 2 elements")] + #[error("ROAIPAddressFamily must be a SEQUENCE of 2 elements (RFC 9582 §4.3.1)")] InvalidIpAddressFamily, - #[error("ROA addressFamily must be an OCTET STRING of 2 bytes")] + #[error("ROA addressFamily must be an OCTET STRING of 2 bytes (RFC 9582 §4.3.1)")] InvalidAddressFamily, - #[error("ROA addressFamily AFI not supported: {0:02X?}")] + #[error("ROA addressFamily AFI not supported: {0:02X?} (RFC 9582 §4.3.1)")] UnsupportedAfi(Vec), - #[error("ROA contains duplicate AFI {0:?}")] + #[error("ROA contains duplicate AFI {0:?} (RFC 9582 §4.3.1)")] DuplicateAfi(RoaAfi), - #[error("ROAAddresses must have at least one entry")] + #[error("ROAAddresses must have at least one entry (RFC 9582 §4.3.2)")] 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, - #[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, - #[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, - #[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 }, - #[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 { afi: RoaAfi, prefix_len: u16, @@ -83,10 +84,13 @@ pub enum RoaValidateError { #[error("ROA EE certificate must not contain AS resources extension (RFC 9582 §5)")] EeAsResourcesPresent, + #[error("ROA EE certificate must contain IP resources extension (RFC 9582 §5)")] + EeIpResourcesMissing, + #[error("ROA EE certificate IP resources must not use inherit (RFC 9582 §5)")] EeIpResourcesInherit, - #[error("ROA prefix not covered by EE certificate IP resources: {afi:?} {addr:?}/{prefix_len}")] + #[error("ROA prefix not covered by EE certificate IP resources: {afi:?} {addr:?}/{prefix_len} (RFC 9582 §5; RFC 3779 §2.3)")] PrefixNotInEeResources { afi: RoaAfi, addr: Vec, @@ -94,26 +98,6 @@ pub enum RoaValidateError { }, } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct IpResourceSet { - pub prefixes: Vec, -} - -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 { pub fn decode_der(der: &[u8]) -> Result { let signed_object = RpkiSignedObject::decode_der(der)?; @@ -137,6 +121,12 @@ impl RoaObject { 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)] @@ -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 - /// require certificate path validation. - pub fn validate_against_ee_resources(&self, ee: &EeResources) -> Result<(), RoaValidateError> { - if ee.as_resources_present { + /// This performs the EE/payload semantic checks that do not require certificate path + /// validation. + pub fn validate_against_ee_cert(&self, ee: &ResourceCertificate) -> Result<(), RoaValidateError> { + if ee.tbs.extensions.as_resources.is_some() { return Err(RoaValidateError::EeAsResourcesPresent); } - 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); } for fam in &self.ip_addr_blocks { 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 { afi: entry.prefix.afi, 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, RoaDecodeError> { let seq = obj .as_sequence() @@ -434,33 +445,3 @@ fn canonicalize_prefix_addr(afi: RoaAfi, prefix_len: u16, bytes: &[u8]) -> Vec 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) -} diff --git a/src/data_model/signed_object.rs b/src/data_model/signed_object.rs index e8ba5f8..61bee43 100644 --- a/src/data_model/signed_object.rs +++ b/src/data_model/signed_object.rs @@ -2,17 +2,15 @@ use crate::data_model::common::{Asn1TimeEncoding, Asn1TimeUtc}; use crate::data_model::oid::{ OID_CMS_ATTR_CONTENT_TYPE, OID_CMS_ATTR_MESSAGE_DIGEST, OID_CMS_ATTR_SIGNING_TIME, OID_RSA_ENCRYPTION, OID_SHA256, OID_SHA256_WITH_RSA_ENCRYPTION, OID_SIGNED_DATA, - OID_AD_SIGNED_OBJECT, OID_SUBJECT_INFO_ACCESS, 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::der::{parse_der, DerObject, Tag}; use sha2::{Digest, Sha256}; -use x509_parser::extensions::GeneralName; -use x509_parser::extensions::ParsedExtension; use x509_parser::public_key::PublicKey; use x509_parser::prelude::FromDer; use x509_parser::x509::SubjectPublicKeyInfo; -use x509_parser::certificate::X509Certificate; #[derive(Clone, Debug, PartialEq, Eq)] pub struct ResourceEeCertificate { @@ -20,6 +18,7 @@ pub struct ResourceEeCertificate { pub subject_key_identifier: Vec, pub spki_der: Vec, pub sia_signed_object_uris: Vec, + pub resource_cert: ResourceCertificate, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -67,121 +66,121 @@ pub struct SignedAttrsProfiled { #[derive(Debug, thiserror::Error)] 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), - #[error("trailing bytes after DER object: {0} bytes")] + #[error("trailing bytes after DER object: {0} bytes (DER; RFC 6488 §3(1l))")] TrailingBytes(usize), - #[error("ContentInfo.contentType must be SignedData ({OID_SIGNED_DATA}), got {0}")] + #[error("ContentInfo.contentType must be SignedData ({OID_SIGNED_DATA}), got {0} (RFC 6488 §3(1a); RFC 5652 §3)")] InvalidContentInfoContentType(String), - #[error("SignedData.version must be 3, got {0}")] + #[error("SignedData.version must be 3, got {0} (RFC 6488 §2.1.1; RFC 6488 §3(1b); RFC 5652 §5.1)")] InvalidSignedDataVersion(u64), - #[error("SignedData.digestAlgorithms must contain exactly one AlgorithmIdentifier, got {0}")] + #[error("SignedData.digestAlgorithms must contain exactly one AlgorithmIdentifier, got {0} (RFC 6488 §2.1.2; RFC 6488 §3(1b); RFC 5652 §5.1)")] InvalidDigestAlgorithmsCount(usize), - #[error("digest algorithm must be id-sha256 ({OID_SHA256}), got {0}")] + #[error("digest algorithm must be id-sha256 ({OID_SHA256}), got {0} (RFC 6488 §2.1.2; RFC 6488 §3(1b); RFC 7935 §2)")] InvalidDigestAlgorithm(String), - #[error("SignedData.certificates MUST be present")] + #[error("SignedData.certificates MUST be present (RFC 6488 §3(1c); RFC 5652 §5.1)")] CertificatesMissing, - #[error("SignedData.certificates must contain exactly one EE certificate, got {0}")] + #[error("SignedData.certificates must contain exactly one EE certificate, got {0} (RFC 6488 §3(1c))")] InvalidCertificatesCount(usize), - #[error("SignedData.crls MUST be omitted")] + #[error("SignedData.crls MUST be omitted (RFC 6488 §3(1d))")] 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), - #[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), - #[error("SignerInfo.sid must be subjectKeyIdentifier [0]")] + #[error("SignerInfo.sid must be subjectKeyIdentifier [0] (RFC 6488 §3(1c); RFC 5652 §5.3)")] InvalidSignerIdentifier, - #[error("SignerInfo.digestAlgorithm must be id-sha256 ({OID_SHA256}), got {0}")] + #[error("SignerInfo.digestAlgorithm must be id-sha256 ({OID_SHA256}), got {0} (RFC 6488 §3(1j); RFC 7935 §2)")] InvalidSignerInfoDigestAlgorithm(String), - #[error("SignerInfo.signedAttrs MUST be present")] + #[error("SignerInfo.signedAttrs MUST be present (RFC 9589 §4; RFC 6488 §3(1f))")] SignedAttrsMissing, - #[error("SignerInfo.unsignedAttrs MUST be omitted")] + #[error("SignerInfo.unsignedAttrs MUST be omitted (RFC 6488 §3(1i))")] UnsignedAttrsPresent, #[error( "SignerInfo.signatureAlgorithm must be rsaEncryption ({OID_RSA_ENCRYPTION}) or \ -sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0}" +sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0} (RFC 6488 §3(1k); RFC 7935 §2)" )] InvalidSignatureAlgorithm(String), - #[error("SignerInfo.signatureAlgorithm parameters must be absent or NULL")] + #[error("SignerInfo.signatureAlgorithm parameters must be absent or NULL (RFC 5280 §4.1.1.2; RFC 7935 §2)")] InvalidSignatureAlgorithmParameters, - #[error("signedAttrs contains unsupported attribute OID {0}")] + #[error("signedAttrs contains unsupported attribute OID {0} (RFC 9589 §4; RFC 6488 §2.1.6.4)")] UnsupportedSignedAttribute(String), - #[error("signedAttrs contains duplicate attribute OID {0}")] + #[error("signedAttrs contains duplicate attribute OID {0} (RFC 6488 §2.1.6.4; RFC 9589 §4)")] DuplicateSignedAttribute(String), - #[error("signedAttrs attribute {oid} attrValues must contain exactly one value, got {count}")] + #[error("signedAttrs attribute {oid} attrValues must contain exactly one value, got {count} (RFC 6488 §2.1.6.4; RFC 5652 §5.3)")] InvalidSignedAttributeValuesCount { oid: String, count: usize }, - #[error("signedAttrs.content-type attrValues must equal eContentType ({econtent_type}), got {attr_content_type}")] + #[error("signedAttrs.content-type attrValues must equal eContentType ({econtent_type}), got {attr_content_type} (RFC 6488 §3(1h); RFC 9589 §4)")] ContentTypeAttrMismatch { econtent_type: String, attr_content_type: String, }, - #[error("EncapsulatedContentInfo.eContent MUST be present")] + #[error("EncapsulatedContentInfo.eContent MUST be present (RFC 6488 §2.1.3; RFC 5652 §5.2)")] EContentMissing, - #[error("signedAttrs.message-digest does not match SHA-256(eContent)")] + #[error("signedAttrs.message-digest does not match SHA-256(eContent) (RFC 6488 §3(1f); RFC 5652 §11.2)")] MessageDigestMismatch, - #[error("EE certificate parse error: {0}")] + #[error("EE certificate parse error: {0} (RFC 6488 §3(1c); RFC 6487 §4)")] EeCertificateParse(String), - #[error("EE certificate missing SubjectKeyIdentifier extension")] + #[error("EE certificate missing SubjectKeyIdentifier extension (RFC 6488 §3(1c); RFC 6487 §4.8.2)")] EeCertificateMissingSki, - #[error("EE certificate missing SubjectInfoAccess extension ({OID_SUBJECT_INFO_ACCESS})")] + #[error("EE certificate missing SubjectInfoAccess extension ({OID_SUBJECT_INFO_ACCESS}) (RFC 6487 §4.8.8.2)")] EeCertificateMissingSia, - #[error("EE certificate SIA missing id-ad-signedObject access method ({OID_AD_SIGNED_OBJECT})")] + #[error("EE certificate SIA missing id-ad-signedObject access method ({OID_AD_SIGNED_OBJECT}) (RFC 6487 §4.8.8.2)")] EeCertificateMissingSignedObjectSia, - #[error("EE certificate SIA id-ad-signedObject accessLocation must be a URI")] + #[error("EE certificate SIA id-ad-signedObject accessLocation must be a URI (RFC 6487 §4.8.8.2; RFC 5280 §4.2.2.2)")] EeCertificateSignedObjectSiaNotUri, - #[error("EE certificate SIA id-ad-signedObject must include at least one rsync:// URI")] + #[error("EE certificate SIA id-ad-signedObject must include at least one rsync:// URI (RFC 6487 §4.8.8.2)")] EeCertificateSignedObjectSiaNoRsync, - #[error("SignerInfo.sid SKI does not match EE certificate SKI")] + #[error("SignerInfo.sid SKI does not match EE certificate SKI (RFC 6488 §3(1c); RFC 5652 §5.3)")] SidSkiMismatch, - #[error("invalid signing-time attribute value (expected UTCTime or GeneralizedTime)")] + #[error("invalid signing-time attribute value (expected UTCTime or GeneralizedTime) (RFC 5652 §11.3; RFC 9589 §4)")] InvalidSigningTimeValue, } #[derive(Debug, thiserror::Error)] pub enum SignedObjectVerifyError { - #[error("EE SubjectPublicKeyInfo parse error: {0}")] + #[error("EE SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7)")] 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), - #[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, - #[error("EE RSA public exponent invalid")] + #[error("EE RSA public exponent invalid (RFC 8017 §A.1.1; RFC 7935 §2)")] InvalidEeRsaExponent, - #[error("signature verification failed")] + #[error("signature verification failed (RFC 6488 §3(2)-(3); RFC 5652 §5.3; RFC 7935 §2)")] InvalidSignature, } @@ -463,80 +462,60 @@ fn parse_certificate_set_implicit(obj: &DerObject<'_>) -> Result Result { - let (rem, cert) = X509Certificate::from_der(der) - .map_err(|e| SignedObjectDecodeError::EeCertificateParse(e.to_string()))?; - let _ = rem; + let rc = match ResourceCertificate::from_der(der) { + Ok(v) => v, + Err(e) => { + return match e { + crate::data_model::rc::ResourceCertificateError::SignedObjectSiaNotUri => { + Err(SignedObjectDecodeError::EeCertificateSignedObjectSiaNotUri) + } + crate::data_model::rc::ResourceCertificateError::SignedObjectSiaNoRsync => { + Err(SignedObjectDecodeError::EeCertificateSignedObjectSiaNoRsync) + } + _ => Err(SignedObjectDecodeError::EeCertificateParse(e.to_string())), + }; + } + }; - let ski = cert - .extensions() - .iter() - .find(|ext| ext.oid.to_id_string() == OID_SUBJECT_KEY_IDENTIFIER) - .and_then(|ext| match ext.parsed_extension() { - ParsedExtension::SubjectKeyIdentifier(ki) => Some(ki.0.to_vec()), - _ => None, - }) + let ski = rc + .tbs + .extensions + .subject_key_identifier + .clone() .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 = match sia { + SubjectInfoAccess::Ee(ee) => ee + .signed_object_uris + .iter() + .map(|u| u.as_str().to_string()) + .collect(), + SubjectInfoAccess::Ca(_ca) => Vec::new(), + }; + if signed_object_uris.is_empty() { + return Err(SignedObjectDecodeError::EeCertificateMissingSignedObjectSia); + } + if !signed_object_uris.iter().any(|u| u.starts_with("rsync://")) { + return Err(SignedObjectDecodeError::EeCertificateSignedObjectSiaNoRsync); + } Ok(ResourceEeCertificate { raw_der: der.to_vec(), subject_key_identifier: ski, spki_der, - sia_signed_object_uris, + sia_signed_object_uris: signed_object_uris, + resource_cert: rc, }) } -fn parse_ee_sia_signed_object_uris( - cert: &X509Certificate<'_>, -) -> Result, SignedObjectDecodeError> { - let mut sia: Option> = 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 = 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 { let seq = obj .as_sequence() diff --git a/src/data_model/ta.rs b/src/data_model/ta.rs index e2f2507..b262a09 100644 --- a/src/data_model/ta.rs +++ b/src/data_model/ta.rs @@ -1,27 +1,188 @@ 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(Debug, Clone)] -// pub struct TrustAnchorCert { -// /// 信任锚证书名称 -// pub name: String, -// -// /// 证书原始DER内容 -// pub cert_der: Vec, -// -// /// 证书 -// pub cert: X509Certificate<'static>, -// -// /// 资源集合 -// pub resources: ResourceSet, -// -// ///发布点 -// pub publication_point: Url, -// } -// -// impl TrustAnchorCert { -// -// } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TaCertificate { + pub raw_der: Vec, + pub rc_ca: ResourceCertificate, +} + +#[derive(Debug, thiserror::Error)] +pub enum TaCertificateError { + #[error("TA certificate parse error: {0} (RFC 5280 §4.1; RFC 6487 §4; RFC 8630 §2.3)")] + Parse(String), + + #[error("trailing bytes after TA certificate DER: {0} bytes (DER; RFC 5280 §4.1; RFC 6487 §4)")] + TrailingBytes(usize), + + #[error("TA certificate must be a CA certificate (RFC 8630 §2.3; RFC 6487 §4.8.1)")] + NotCa, + + #[error("TA certificate must be self-signed (issuer DN must equal subject DN) (RFC 8630 §2.3; RFC 5280 §4.1.2.4)")] + NotSelfSignedIssuerSubject, + + #[error("TA certificate self-signature verification failed: {0} (RFC 8630 §2.3; RFC 5280 §6.1)")] + InvalidSelfSignature(String), + + #[error("TA certificate must contain certificatePolicies ipAddr-asNumber ({OID_CP_IPADDR_ASNUMBER}) (RFC 6487 §4.8.9; RFC 8630 §2.3)")] + MissingOrInvalidCertificatePolicies, + + #[error("TA certificate must contain SubjectKeyIdentifier (RFC 6487 §4.8.2; RFC 8630 §2.3)")] + MissingSubjectKeyIdentifier, + + #[error("TA certificate must contain at least one RFC 3779 resource extension (IP or AS) (RFC 6487 §4.8.10-§4.8.11; RFC 8630 §2.3)")] + ResourcesMissing, + + #[error("TA certificate resources must be non-empty (RFC 8630 §2.3)")] + ResourcesEmpty, + + #[error("TA certificate MUST NOT use inherit in IP resources (RFC 8630 §2.3; RFC 3779 §2.2.3.5)")] + IpResourcesInherit, + + #[error("TA certificate MUST NOT use inherit in AS resources (RFC 8630 §2.3; RFC 3779 §3.2.3.3)")] + AsResourcesInherit, +} + +impl TaCertificate { + pub fn from_der(der: &[u8]) -> Result { + 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, +} + +#[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 { + 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(), + }) + } +} diff --git a/src/data_model/tal.rs b/src/data_model/tal.rs index 5dfd02d..1c0256b 100644 --- a/src/data_model/tal.rs +++ b/src/data_model/tal.rs @@ -1,18 +1,134 @@ -/// TAL Model -#[derive(Clone, Debug)] +use base64::Engine; +use url::Url; + +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Tal { - /// Optional human-readable comments + pub raw: Vec, pub comments: Vec, - - /// Ordered list of URIs pointing to the TA certificate - pub uris: Vec, - - /// SubjectPublicKeyInfo DER - pub spki_der: Vec, + pub ta_uris: Vec, + pub subject_public_key_info_der: Vec, } -#[derive(Debug, Clone)] -pub enum TalUri { - Rsync(String), - Https(String), +#[derive(Debug, thiserror::Error)] +pub enum TalDecodeError { + #[error("TAL must be valid UTF-8 (RFC 8630 §2.2)")] + InvalidUtf8, + + #[error("TAL comments must appear only at the beginning (RFC 8630 §2.2)")] + CommentAfterHeader, + + #[error("TAL must contain at least one TA URI line (RFC 8630 §2.2)")] + MissingTaUris, + + #[error("TAL must contain an empty line separator between URI list and SPKI base64 (RFC 8630 §2.2)")] + MissingSeparatorEmptyLine, + + #[error("TAL TA URI invalid: {0} (RFC 8630 §2.2)")] + InvalidUri(String), + + #[error("TAL TA URI scheme must be rsync or https, got {0} (RFC 8630 §2.2)")] + UnsupportedUriScheme(String), + + #[error("TAL TA URI must reference a single object (must not end with '/'): {0} (RFC 8630 §2.3)")] + UriIsDirectory(String), + + #[error("TAL must contain base64-encoded SubjectPublicKeyInfo after the separator (RFC 8630 §2.2)")] + MissingSpki, + + #[error("TAL SPKI base64 decode failed (RFC 8630 §2.2)")] + SpkiBase64Decode, + + #[error("TAL SPKI DER is empty (RFC 8630 §2.2)")] + SpkiDerEmpty, +} + +impl Tal { + pub fn decode_bytes(input: &[u8]) -> Result { + 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 = 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 = 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, + }) + } } diff --git a/tests/fixtures/ta/afrinic-ta.cer b/tests/fixtures/ta/afrinic-ta.cer new file mode 100644 index 0000000..734586b Binary files /dev/null and b/tests/fixtures/ta/afrinic-ta.cer differ diff --git a/tests/fixtures/ta/apnic-ta.cer b/tests/fixtures/ta/apnic-ta.cer new file mode 100644 index 0000000..83f98ce Binary files /dev/null and b/tests/fixtures/ta/apnic-ta.cer differ diff --git a/tests/fixtures/ta/arin-ta.cer b/tests/fixtures/ta/arin-ta.cer new file mode 100644 index 0000000..573af6b Binary files /dev/null and b/tests/fixtures/ta/arin-ta.cer differ diff --git a/tests/fixtures/ta/lacnic-ta.cer b/tests/fixtures/ta/lacnic-ta.cer new file mode 100644 index 0000000..d86e36f Binary files /dev/null and b/tests/fixtures/ta/lacnic-ta.cer differ diff --git a/tests/fixtures/ta/ripe-ncc-ta.cer b/tests/fixtures/ta/ripe-ncc-ta.cer new file mode 100644 index 0000000..ad42dfd Binary files /dev/null and b/tests/fixtures/ta/ripe-ncc-ta.cer differ diff --git a/tests/fixtures/tal/afrinic.tal b/tests/fixtures/tal/afrinic.tal new file mode 100644 index 0000000..5ff40bf --- /dev/null +++ b/tests/fixtures/tal/afrinic.tal @@ -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 diff --git a/tests/fixtures/tal/apnic-rfc7730-https.tal b/tests/fixtures/tal/apnic-rfc7730-https.tal new file mode 100644 index 0000000..803eb20 --- /dev/null +++ b/tests/fixtures/tal/apnic-rfc7730-https.tal @@ -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 diff --git a/tests/fixtures/tal/arin.tal b/tests/fixtures/tal/arin.tal new file mode 100644 index 0000000..559099b --- /dev/null +++ b/tests/fixtures/tal/arin.tal @@ -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 diff --git a/tests/fixtures/tal/lacnic.tal b/tests/fixtures/tal/lacnic.tal new file mode 100644 index 0000000..f81af21 --- /dev/null +++ b/tests/fixtures/tal/lacnic.tal @@ -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 \ No newline at end of file diff --git a/tests/fixtures/tal/ripe-ncc.tal b/tests/fixtures/tal/ripe-ncc.tal new file mode 100644 index 0000000..80002d9 --- /dev/null +++ b/tests/fixtures/tal/ripe-ncc.tal @@ -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 diff --git a/tests/test_aspa_embedded_ee_cert.rs b/tests/test_aspa_embedded_ee_cert.rs new file mode 100644 index 0000000..cdbe72e --- /dev/null +++ b/tests/test_aspa_embedded_ee_cert.rs @@ -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"); +} + diff --git a/tests/test_aspa_validate_ee_resources.rs b/tests/test_aspa_validate_ee_resources.rs index 6e8c2a2..1ff1f01 100644 --- a/tests/test_aspa_validate_ee_resources.rs +++ b/tests/test_aspa_validate_ee_resources.rs @@ -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, as_resources: Option) -> ResourceCertificate { + ResourceCertificate { + raw_der: vec![], + tbs: RpkixTbsCertificate { + version: 2, + serial_number: BigUint::from(1u8), + signature_algorithm: "1.2.840.113549.1.1.11".to_string(), + issuer_dn: "CN=issuer".to_string(), + subject_dn: "CN=subject".to_string(), + validity_not_before: OffsetDateTime::UNIX_EPOCH, + validity_not_after: OffsetDateTime::UNIX_EPOCH, + subject_public_key_info: vec![], + extensions: RcExtensions { + basic_constraints_ca: false, + subject_key_identifier: Some(vec![0x01]), + subject_info_access: Some(SubjectInfoAccess::Ca( + rpki::data_model::rc::SubjectInfoAccessCa { + access_descriptions: vec![], + }, + )), + certificate_policies_oid: None, + ip_resources, + as_resources, + }, + }, + kind: ResourceCertKind::Ee, + } +} fn test_aspa() -> AspaEContent { AspaEContent { @@ -11,74 +47,104 @@ fn test_aspa() -> AspaEContent { #[test] fn validate_accepts_when_customer_matches_ee_asid() { let aspa = test_aspa(); - let ee = EeAspaResources { - as_id: Some(64496), - as_resources_inherit: false, - as_resources_range_present: false, - ip_resources_present: false, - }; - aspa.validate_against_ee_resources(&ee) + let ee = dummy_ee( + None, + Some(AsResourceSet { + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])), + rdi: None, + }), + ); + aspa.validate_against_ee_cert(&ee) .expect("customer must match"); } #[test] fn validate_rejects_missing_as_resources() { let aspa = test_aspa(); - let ee = EeAspaResources { - as_id: None, - as_resources_inherit: false, - as_resources_range_present: false, - ip_resources_present: false, - }; - let err = aspa.validate_against_ee_resources(&ee).unwrap_err(); + let ee = dummy_ee(None, None); + let err = aspa.validate_against_ee_cert(&ee).unwrap_err(); assert!(matches!(err, AspaValidateError::EeAsResourcesMissing)); } #[test] fn validate_rejects_as_resources_inherit_or_ranges() { let aspa = test_aspa(); - let ee = EeAspaResources { - as_id: Some(64496), - as_resources_inherit: true, - as_resources_range_present: false, - ip_resources_present: false, - }; - let err = aspa.validate_against_ee_resources(&ee).unwrap_err(); + let ee = dummy_ee( + None, + Some(AsResourceSet { + asnum: Some(AsIdentifierChoice::Inherit), + rdi: None, + }), + ); + let err = aspa.validate_against_ee_cert(&ee).unwrap_err(); assert!(matches!(err, AspaValidateError::EeAsResourcesInherit)); - let ee = EeAspaResources { - as_id: Some(64496), - as_resources_inherit: false, - as_resources_range_present: true, - ip_resources_present: false, - }; - let err = aspa.validate_against_ee_resources(&ee).unwrap_err(); + let ee = dummy_ee( + None, + Some(AsResourceSet { + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range { + min: 64496, + max: 64497, + }])), + rdi: None, + }), + ); + let err = aspa.validate_against_ee_cert(&ee).unwrap_err(); assert!(matches!(err, AspaValidateError::EeAsResourcesRangePresent)); } #[test] fn validate_rejects_customer_mismatch() { let aspa = test_aspa(); - let ee = EeAspaResources { - as_id: Some(64511), - as_resources_inherit: false, - as_resources_range_present: false, - ip_resources_present: false, - }; - let err = aspa.validate_against_ee_resources(&ee).unwrap_err(); + let ee = dummy_ee( + None, + Some(AsResourceSet { + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64511)])), + rdi: None, + }), + ); + let err = aspa.validate_against_ee_cert(&ee).unwrap_err(); assert!(matches!(err, AspaValidateError::CustomerAsIdMismatch { .. })); } #[test] fn validate_rejects_ip_resources_present() { let aspa = test_aspa(); - let ee = EeAspaResources { - as_id: Some(64496), - as_resources_inherit: false, - as_resources_range_present: false, - ip_resources_present: true, - }; - let err = aspa.validate_against_ee_resources(&ee).unwrap_err(); + let ee = dummy_ee( + Some(IpResourceSet { families: vec![] }), + Some(AsResourceSet { + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])), + rdi: None, + }), + ); + let err = aspa.validate_against_ee_cert(&ee).unwrap_err(); assert!(matches!(err, AspaValidateError::EeIpResourcesPresent)); } +#[test] +fn validate_rejects_rdi_present_or_not_single_id() { + let aspa = test_aspa(); + let ee = dummy_ee( + None, + Some(AsResourceSet { + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])), + rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])), + }), + ); + let err = aspa.validate_against_ee_cert(&ee).unwrap_err(); + assert!(matches!(err, AspaValidateError::EeAsResourcesRdiPresent)); + + let ee = dummy_ee( + None, + Some(AsResourceSet { + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![ + AsIdOrRange::Id(64496), + AsIdOrRange::Id(64497), + ])), + rdi: None, + }), + ); + let err = aspa.validate_against_ee_cert(&ee).unwrap_err(); + assert!(matches!(err, AspaValidateError::EeAsResourcesNotSingleId)); +} + diff --git a/tests/test_manifest_embedded_ee_cert.rs b/tests/test_manifest_embedded_ee_cert.rs new file mode 100644 index 0000000..89aa1a0 --- /dev/null +++ b/tests/test_manifest_embedded_ee_cert.rs @@ -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"); +} + diff --git a/tests/test_model_print_real_fixtures.rs b/tests/test_model_print_real_fixtures.rs new file mode 100644 index 0000000..d3cf451 --- /dev/null +++ b/tests/test_model_print_real_fixtures.rs @@ -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, + ta_uris: Vec, + 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, +} + +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, + subject_info_access: Option, + certificate_policies_oid: Option, + ip_resources: Option, + as_resources: Option, +} + +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, + encap_content_info: EncapsulatedContentInfoPretty, + certificates: Vec, + crls_present: bool, + signer_infos: Vec, +} + +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, + 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, +} + +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, +} + +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, + 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)); +} diff --git a/tests/test_rc_from_der_errors.rs b/tests/test_rc_from_der_errors.rs new file mode 100644 index 0000000..b65e7d4 --- /dev/null +++ b/tests/test_rc_from_der_errors.rs @@ -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 { + 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 + )); +} + diff --git a/tests/test_rc_from_der_fixtures.rs b/tests/test_rc_from_der_fixtures.rs new file mode 100644 index 0000000..3ef7fb0 --- /dev/null +++ b/tests/test_rc_from_der_fixtures.rs @@ -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"); +} + diff --git a/tests/test_rc_helpers.rs b/tests/test_rc_helpers.rs new file mode 100644 index 0000000..d6d4659 --- /dev/null +++ b/tests/test_rc_helpers.rs @@ -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)); +} + diff --git a/tests/test_rc_ip_resource_range_coverage.rs b/tests/test_rc_ip_resource_range_coverage.rs new file mode 100644 index 0000000..4f8b58a --- /dev/null +++ b/tests/test_rc_ip_resource_range_coverage.rs @@ -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)); +} + diff --git a/tests/test_rc_resource_extensions_decode.rs b/tests/test_rc_resource_extensions_decode.rs new file mode 100644 index 0000000..57ac45a --- /dev/null +++ b/tests/test_rc_resource_extensions_decode.rs @@ -0,0 +1,135 @@ +use rpki::data_model::rc::{ + Afi, AsIdentifierChoice, AsIdOrRange, AsResourceSet, IpAddressChoice, IpAddressOrRange, + IpResourceSet, +}; + +fn len_bytes(len: usize) -> Vec { + 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 { + let mut out = vec![tag]; + out.extend(len_bytes(content.len())); + out.extend_from_slice(content); + out +} + +fn der_sequence(children: Vec>) -> Vec { + let mut content = Vec::new(); + for c in children { + content.extend(c); + } + tlv(0x30, &content) +} + +fn der_octet_string(bytes: &[u8]) -> Vec { + tlv(0x04, bytes) +} + +fn der_null() -> Vec { + vec![0x05, 0x00] +} + +fn der_integer_u64(v: u64) -> Vec { + 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 { + let mut content = vec![unused]; + content.extend_from_slice(bytes); + tlv(0x03, &content) +} + +fn cs_cons(tag_no: u8, inner_der: Vec) -> Vec { + 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()); +} + diff --git a/tests/test_rc_resource_extensions_decode_errors.rs b/tests/test_rc_resource_extensions_decode_errors.rs new file mode 100644 index 0000000..c91957c --- /dev/null +++ b/tests/test_rc_resource_extensions_decode_errors.rs @@ -0,0 +1,202 @@ +use rpki::data_model::rc::{AsResourceSet, IpResourceSet}; + +fn len_bytes(len: usize) -> Vec { + 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 { + let mut out = vec![tag]; + out.extend(len_bytes(content.len())); + out.extend_from_slice(content); + out +} + +fn der_sequence(children: Vec>) -> Vec { + let mut content = Vec::new(); + for c in children { + content.extend(c); + } + tlv(0x30, &content) +} + +fn der_octet_string(bytes: &[u8]) -> Vec { + tlv(0x04, bytes) +} + +fn der_null() -> Vec { + vec![0x05, 0x00] +} + +fn der_integer_bytes(bytes: &[u8]) -> Vec { + tlv(0x02, bytes) +} + +fn der_integer_u64(v: u64) -> Vec { + 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 { + let mut content = vec![unused]; + content.extend_from_slice(bytes); + tlv(0x03, &content) +} + +fn cs_cons(tag_no: u8, inner_der: Vec) -> Vec { + 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]); +} diff --git a/tests/test_roa_embedded_ee_cert.rs b/tests/test_roa_embedded_ee_cert.rs new file mode 100644 index 0000000..3b09791 --- /dev/null +++ b/tests/test_roa_embedded_ee_cert.rs @@ -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"); +} diff --git a/tests/test_roa_validate_ee_resources.rs b/tests/test_roa_validate_ee_resources.rs index ec66153..27fdf1b 100644 --- a/tests/test_roa_validate_ee_resources.rs +++ b/tests/test_roa_validate_ee_resources.rs @@ -1,7 +1,40 @@ -use rpki::data_model::roa::{ - EeResources, IpPrefix, IpResourceSet, RoaAfi, RoaEContent, RoaIpAddress, RoaIpAddressFamily, - RoaValidateError, +use der_parser::num_bigint::BigUint; +use time::OffsetDateTime; + +use rpki::data_model::rc::{ + Afi, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange, IpPrefix, IpResourceSet, + RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, SubjectInfoAccess, }; +use rpki::data_model::roa::{RoaAfi, RoaEContent, RoaIpAddress, RoaIpAddressFamily, RoaValidateError}; + +fn dummy_ee(ip_resources: Option, as_resources: Option) -> ResourceCertificate { + ResourceCertificate { + raw_der: vec![], + tbs: RpkixTbsCertificate { + version: 2, + serial_number: BigUint::from(1u8), + signature_algorithm: "1.2.840.113549.1.1.11".to_string(), + issuer_dn: "CN=issuer".to_string(), + subject_dn: "CN=subject".to_string(), + validity_not_before: OffsetDateTime::UNIX_EPOCH, + validity_not_after: OffsetDateTime::UNIX_EPOCH, + subject_public_key_info: vec![], + extensions: RcExtensions { + basic_constraints_ca: false, + subject_key_identifier: Some(vec![0x01]), + subject_info_access: Some(SubjectInfoAccess::Ca( + rpki::data_model::rc::SubjectInfoAccessCa { + access_descriptions: vec![], + }, + )), + certificate_policies_oid: None, + ip_resources, + as_resources, + }, + }, + kind: ResourceCertKind::Ee, + } +} fn test_roa_single_v4_prefix() -> RoaEContent { RoaEContent { @@ -10,7 +43,7 @@ fn test_roa_single_v4_prefix() -> RoaEContent { ip_addr_blocks: vec![RoaIpAddressFamily { afi: RoaAfi::Ipv4, addresses: vec![RoaIpAddress { - prefix: IpPrefix { + prefix: rpki::data_model::roa::IpPrefix { afi: RoaAfi::Ipv4, prefix_len: 8, addr: vec![10, 0, 0, 0], @@ -24,85 +57,120 @@ fn test_roa_single_v4_prefix() -> RoaEContent { #[test] fn validate_accepts_when_prefix_is_covered() { let roa = test_roa_single_v4_prefix(); - let ee = EeResources { - ip_resources: IpResourceSet { - prefixes: vec![IpPrefix { - afi: RoaAfi::Ipv4, - prefix_len: 0, - addr: vec![0, 0, 0, 0], + let ee = dummy_ee( + Some(IpResourceSet { + families: vec![IpAddressFamily { + afi: Afi::Ipv4, + choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix( + IpPrefix { + afi: Afi::Ipv4, + prefix_len: 0, + addr: vec![0, 0, 0, 0], + }, + )]), }], - }, - 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"); } #[test] fn validate_rejects_when_as_resources_present() { let roa = test_roa_single_v4_prefix(); - let ee = EeResources { - ip_resources: IpResourceSet { prefixes: vec![] }, - ip_resources_inherit: false, - as_resources_present: true, - }; - let err = roa.validate_against_ee_resources(&ee).unwrap_err(); + let ee = dummy_ee( + Some(IpResourceSet { families: vec![] }), + Some(AsResourceSet { + asnum: None, + rdi: None, + }), + ); + let err = roa.validate_against_ee_cert(&ee).unwrap_err(); assert!(matches!(err, RoaValidateError::EeAsResourcesPresent)); } +#[test] +fn validate_rejects_when_ip_resources_missing() { + let roa = test_roa_single_v4_prefix(); + let ee = dummy_ee(None, None); + let err = roa.validate_against_ee_cert(&ee).unwrap_err(); + assert!(matches!(err, RoaValidateError::EeIpResourcesMissing)); +} + #[test] fn validate_rejects_when_ip_resources_inherit() { let roa = test_roa_single_v4_prefix(); - let ee = EeResources { - ip_resources: IpResourceSet { prefixes: vec![] }, - ip_resources_inherit: true, - as_resources_present: false, - }; - let err = roa.validate_against_ee_resources(&ee).unwrap_err(); + let ee = dummy_ee( + Some(IpResourceSet { + families: vec![IpAddressFamily { + afi: Afi::Ipv4, + choice: IpAddressChoice::Inherit, + }], + }), + None, + ); + let err = roa.validate_against_ee_cert(&ee).unwrap_err(); assert!(matches!(err, RoaValidateError::EeIpResourcesInherit)); } #[test] fn validate_rejects_when_prefix_not_covered() { let roa = test_roa_single_v4_prefix(); - let ee = EeResources { - ip_resources: IpResourceSet { - prefixes: vec![IpPrefix { - afi: RoaAfi::Ipv4, - prefix_len: 24, - addr: vec![192, 0, 2, 0], + let ee = dummy_ee( + Some(IpResourceSet { + families: vec![IpAddressFamily { + afi: Afi::Ipv4, + choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix( + IpPrefix { + afi: Afi::Ipv4, + prefix_len: 24, + addr: vec![192, 0, 2, 0], + }, + )]), }], - }, - 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 { .. })); } #[test] fn contains_prefix_handles_non_octet_boundary_prefix_len() { - let ee_set = IpResourceSet { - prefixes: vec![IpPrefix { + let roa = RoaEContent { + version: 0, + as_id: 64496, + ip_addr_blocks: vec![RoaIpAddressFamily { afi: RoaAfi::Ipv4, - prefix_len: 9, - addr: vec![0b1010_0000, 0, 0, 0], // 160.0.0.0/9 + addresses: vec![RoaIpAddress { + prefix: rpki::data_model::roa::IpPrefix { + afi: RoaAfi::Ipv4, + prefix_len: 16, + addr: vec![0b1010_0000, 0x12, 0, 0], // 160.18.0.0/16 + }, + max_length: None, + }], }], }; - let covered = IpPrefix { - afi: RoaAfi::Ipv4, - prefix_len: 16, - addr: vec![0b1010_0000, 0x12, 0, 0], // 160.18.0.0/16 - }; - assert!(ee_set.contains_prefix(&covered)); + let ee = dummy_ee( + Some(IpResourceSet { + families: vec![IpAddressFamily { + afi: Afi::Ipv4, + choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix( + IpPrefix { + afi: Afi::Ipv4, + prefix_len: 9, + addr: vec![0b1010_0000, 0, 0, 0], // 160.0.0.0/9 + }, + )]), + }], + }), + None, + ); - let not_covered = IpPrefix { - afi: RoaAfi::Ipv4, - prefix_len: 16, - addr: vec![0b1010_0001, 0x12, 0, 0], // 161.18.0.0/16 - }; - assert!(!ee_set.contains_prefix(¬_covered)); + roa.validate_against_ee_cert(&ee) + .expect("160.18.0.0/16 should be covered by 160.0.0.0/9"); } diff --git a/tests/test_ta_certificate.rs b/tests/test_ta_certificate.rs new file mode 100644 index 0000000..156147c --- /dev/null +++ b/tests/test_ta_certificate.rs @@ -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"); +} + diff --git a/tests/test_tal_decode.rs b/tests/test_tal_decode.rs new file mode 100644 index 0000000..b862dc1 --- /dev/null +++ b/tests/test_tal_decode.rs @@ -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}"); + } +} + diff --git a/tests/test_tal_decode_errors.rs b/tests/test_tal_decode_errors.rs new file mode 100644 index 0000000..b898072 --- /dev/null +++ b/tests/test_tal_decode_errors.rs @@ -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))); +} + diff --git a/tests/test_trust_anchor_bind.rs b/tests/test_trust_anchor_bind.rs new file mode 100644 index 0000000..3079ad8 --- /dev/null +++ b/tests/test_trust_anchor_bind.rs @@ -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(_)) + )); +}