diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c96eb1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..49f8bfa --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "rpki" +version = "0.1.0" +edition = "2024" + +[dependencies] +der-parser = "10.0.0" +hex = "0.4.3" +thiserror = "2.0.18" +time = "0.3.45" +x509-parser = { version = "0.18.0", features = ["verify"] } diff --git a/README.md b/README.md index e69de29..e89d904 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,11 @@ + + +# 单元测试 + +``` +cargo test + +# 查看输出 +cargo test -- --nocapture +``` + diff --git a/specs/00_common_types.md b/specs/00_common_types.md new file mode 100644 index 0000000..cabf293 --- /dev/null +++ b/specs/00_common_types.md @@ -0,0 +1,217 @@ +# 00. 公共类型、编码约定与 OID 表 + +本文件定义各数据对象文档共用的类型与记号。 + +## 0.1 规范关键字 + +文中 “MUST/SHOULD/MAY/MUST NOT/SHOULD NOT” 语义遵循 RFC 2119 / RFC 8174。 + +## 0.2 编码/序列化约定 + +### 0.2.1 DER + +- `DER`:ASN.1 Distinguished Encoding Rules(DER)。RPKI Signed Object 与其 eContent 均要求 DER 编码。RFC 6488 §2;RFC 9286 §4.2;RFC 9582 §4。 + +### 0.2.2 X.509 v3 扩展(Extension)编码模板 + +> 用途:解释证书/CRL里“按 OID 挂扩展”的通用编码方式。该模板来自 RFC 5280,对 RPKI 来说是“所有 extnID 扩展”的外层载体。 + +证书与 CRL 的扩展都使用同一个外层结构(DER 编码)。其 ASN.1 定义如下:RFC 5280 §4.1。 + +```asn1 +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 + } +``` + +解码要点: + +- 先按 `extnID` 选择“要用哪一种内层 ASN.1 结构”。 +- 再把 `extnValue` 的 octets 作为 DER 进行**二次解码**,得到该扩展的内层值(例如 SIA/AIA/CRLDP/3779 资源扩展等)。 + +编码要点: + +- 先把内层值按其 ASN.1 结构 DER 编码,得到 `inner_der`; +- 再写入 `extnValue = OCTET STRING(inner_der)`。 + +RFC 引用:RFC 5280 §4.1(证书扩展位置);RFC 5280 §4.2(扩展定义与语义)。 + +### 0.2.3 AIA/SIA 的内层容器结构(AccessDescription + GeneralName URI) + +Authority Information Access(AIA)与 Subject Information Access(SIA)的内层值都是一个 “AccessDescription 列表”。其 ASN.1 定义如下:RFC 5280 §4.2.2.1;RFC 5280 §4.2.2.2。 + +```asn1 +AuthorityInfoAccessSyntax ::= + SEQUENCE SIZE (1..MAX) OF AccessDescription + +SubjectInfoAccessSyntax ::= + SEQUENCE SIZE (1..MAX) OF AccessDescription + +AccessDescription ::= SEQUENCE { + accessMethod OBJECT IDENTIFIER, + accessLocation GeneralName } +``` + +其中 `accessLocation` 的 `GeneralName`(URI 用 `uniformResourceIdentifier` 分支)定义如下:RFC 5280 §4.2.1.6。 + +```asn1 +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 } +``` + +解码要点: + +- 先按 `extnID` 识别是 AIA(`1.3.6.1.5.5.7.1.1`) 或 SIA(`1.3.6.1.5.5.7.1.11`); +- 再把 `extnValue` 解码成 “SEQUENCE OF AccessDescription”; +- 每条 `AccessDescription` 用 `accessMethod` 的 OID 区分语义; +- URI 通常出现在 `GeneralName.uniformResourceIdentifier`(IA5String)中。 + +RFC 引用:RFC 5280 §4.2.2.1;RFC 5280 §4.2.2.2;RFC 5280 §4.2.1.6。 + +### 0.2.2 Base64(仅 TAL) + +- `Base64`:TAL 中的 `subjectPublicKeyInfo` 以 DER 字节序列经 Base64 编码表示;允许插入换行。RFC 8630 §2.2。 + +### 0.2.4 换行 + +- `LF` 或 `CRLF`:TAL 文件允许使用 `\n` 或 `\r\n`。RFC 8630 §2.2。 + +## 0.3 基本类型(抽象模型层) + +> 说明:这些类型用于描述“语义对象”,不等同于 ASN.1 具体类型,但会给出从 ASN.1/文本到该类型的解析规则。 + +- `DerBytes`: `bytes`,承载一个 DER 编码对象的原始字节(入口)。 +- `Oid`: `string`(点分十进制),例如 `1.2.840.113549.1.9.16.1.26`。 +- `Uri`: `string`,URI 文本;其语法来自 RFC 3986,但在 RPKI profile 下会被进一步限制(见各对象文档)。 +- `Utf8Text`: `string`,UTF-8 文本(例如 TAL 注释行)。RFC 8630 §2.2(引用 RFC 5198 §2 的限制)。 +- `UtcTime`: `string`,承载“UTC 时间点”的语义值;来源可能是 X.509 `Time`(UTCTime/GeneralizedTime)或 ASN.1 `GeneralizedTime`(如 Manifest)。RFC 5280 §4.1.2.5;RFC 9286 §4.2。 +- `HexBytes`: `bytes` 的十六进制展示形态(仅文档说明用,不建议作为接口字段)。 + +## 0.4 RPKI 资源集合语义(高层表示) + +### 0.4.1 IP 资源集合(语义模型) + +用于表达 RFC 3779 的 `IPAddrBlocks`(以及 ROA 的“仅前缀”子集)。 + +#### 类型:`IpResourceSet` + +- `families: list[IpFamilyResources]` + +#### 类型:`IpFamilyResources` + +- `afi: enum { ipv4, ipv6 }` +- `safi: optional[int]` + - 在公用互联网资源证书语义中,SAFI **MUST NOT** 使用。RFC 6487 §4.8.10。 +- `choice: enum { inherit, explicit }` +- `explicit_resources: optional[list[IpRangeOrPrefix]]` + +#### 类型:`IpRangeOrPrefix` + +- `kind: enum { prefix, range }` +- `prefix: optional[IpPrefix]` +- `range: optional[IpRange]` + +#### 类型:`IpPrefix` + +- `address: bytes`(IPv4 4 字节或 IPv6 16 字节的网络序) +- `prefix_len: int` + +#### 类型:`IpRange` + +- `min: bytes` +- `max: bytes` + +解析与约束要点(实现应在解析阶段保留必要信息供后续验证使用): + +- RFC 3779 `IPAddress`/`IPAddressRange` 以 `BIT STRING` 编码前缀/范围,并对排序/去重/规范编码给出要求。RFC 3779 §2.2.3.6-§2.2.3.9。 +- RFC 3779 `inherit` 语义表示资源集合继承自签发者。RFC 3779 §2.2.3.5。 + +### 0.4.2 AS 资源集合(语义模型) + +用于表达 RFC 3779 的 `ASIdentifiers`。 + +#### 类型:`AsResourceSet` + +- `asnum: optional[AsIdentifierChoice]` + - 在 RPKI profile 中,`asnum` 与 `rdi` 不能都缺省到导致“无 AS 资源扩展”的语义;但 RC 层面允许 “仅 IP” 或 “仅 AS” 或两者都存在。RFC 6487 §4.8.10;RFC 6487 §4.8.11。 +- `rdi: optional[AsIdentifierChoice]` + - RPKI profile **不支持** RDI,MUST NOT 使用。RFC 6487 §4.8.11。 + +#### 类型:`AsIdentifierChoice` + +- `choice: enum { inherit, explicit }` +- `explicit_ranges: optional[list[AsRangeOrId]]` + +#### 类型:`AsRangeOrId` + +- `kind: enum { id, range }` +- `id: optional[int]`(0..4294967295) +- `range: optional[AsRange]` + +#### 类型:`AsRange` + +- `min: int` +- `max: int` + +解析与约束要点: + +- `inherit` 语义与排序/范围编码规则来自 RFC 3779。RFC 3779 §3.2.3.3;RFC 3779 §3.2.3.8。 + +## 0.5 常用 OID 表(最小集合) + +> 说明:表中 “来源 RFC” 给出该 OID 在本模型中的直接规范引用点;在实现时也可以直接用点分十进制比对。 + +| OID | 符号名/含义 | 来源 RFC | +|---|---|---| +| `1.2.840.113549.1.7.2` | CMS SignedData(ContentInfo.contentType) | RFC 6488 §3(1a) | +| `1.2.840.113549.1.9.3` | CMS signedAttrs: content-type | RFC 6488 §3(1f) | +| `1.2.840.113549.1.9.4` | CMS signedAttrs: message-digest | RFC 6488 §3(1f) | +| `1.2.840.113549.1.9.5` | CMS signedAttrs: signing-time | RFC 9589 §4(更新 RFC 6488 §3(1f)/(1g)) | +| `1.2.840.113549.1.9.16.1.24` | ROA eContentType: id-ct-routeOriginAuthz | RFC 9582 §3 | +| `1.2.840.113549.1.9.16.1.26` | Manifest eContentType: id-ct-rpkiManifest | RFC 9286 §4.1 | +| `1.3.6.1.5.5.7.1.1` | X.509 v3 扩展:authorityInfoAccess | RFC 5280 §4.2.2.1 | +| `1.3.6.1.5.5.7.1.11` | X.509 v3 扩展:subjectInfoAccess | RFC 5280 §4.2.2.2;RPKI 约束见 RFC 6487 §4.8.8 | +| `1.3.6.1.5.5.7.48.2` | AIA accessMethod: id-ad-caIssuers | RFC 5280 §4.2.2.1 | +| `1.3.6.1.5.5.7.48.5` | SIA accessMethod: id-ad-caRepository | RFC 5280 §4.2.2.2;RPKI 语义见 RFC 6481 §2.2;约束见 RFC 6487 §4.8.8.1 | +| `1.3.6.1.5.5.7.48.10` | SIA accessMethod: id-ad-rpkiManifest | RFC 6487 §4.8.8.1 | +| `1.3.6.1.5.5.7.48.11` | SIA accessMethod: id-ad-signedObject | RFC 6487 §4.8.8.2 | +| `1.3.6.1.5.5.7.48.13` | SIA accessMethod: id-ad-rpkiNotify(RRDP Notification) | RFC 8182 §3.2 | +| `1.3.6.1.5.5.7.1.7` | RFC 3779 IP 资源扩展:id-pe-ipAddrBlocks | RFC 3779 §2.2.1 | +| `1.3.6.1.5.5.7.1.8` | RFC 3779 AS 资源扩展:id-pe-autonomousSysIds | RFC 3779 §3.2.1 | +| `2.5.29.31` | X.509 v3 扩展:cRLDistributionPoints | RFC 5280 §4.2.1.13;RPKI 约束见 RFC 6487 §4.8.6 | +| `2.5.29.32` | X.509 v3 扩展:certificatePolicies | RFC 5280 §4.2.1.4;RPKI 约束见 RFC 6487 §4.8.9;更新见 RFC 7318 §2 | +| `2.5.29.14` | X.509 v3 扩展:subjectKeyIdentifier | RFC 5280 §4.2.1.2;RPKI 约束见 RFC 6487 §4.8.2 | +| `2.5.29.35` | X.509 v3 扩展:authorityKeyIdentifier(证书/CRL) | RFC 5280 §4.2.1.1(证书);RFC 5280 §5.2.1(CRL);RPKI 约束见 RFC 6487 §4.8.3 / RFC 9829 §3.1 | +| `2.5.29.15` | X.509 v3 扩展:keyUsage | RFC 5280 §4.2.1.3;RPKI 约束见 RFC 6487 §4.8.4 | +| `2.5.29.19` | X.509 v3 扩展:basicConstraints | RFC 5280 §4.2.1.9;RPKI 约束见 RFC 6487 §4.8.1 | +| `2.5.29.20` | CRL 扩展:cRLNumber | RFC 5280 §5.2.3;RPKI 约束更新见 RFC 9829 §3.1 | +| `1.3.6.1.5.5.7.14.2` | RPKI Certificate Policy:id-cp-ipAddr-asNumber | RFC 6484 §1.2;证书中使用要求见 RFC 6487 §4.8.9 | +| `1.3.6.1.5.5.7.2.1` | Policy Qualifier:id-qt-cps | RFC 5280 §4.2.1.4;RPKI 限制见 RFC 7318 §2 | +| `2.16.840.1.101.3.4.2.1` | `id-sha256`(SHA-256 摘要算法 OID) | RFC 7935 §2(引用 RFC 5754) | +| `1.2.840.113549.1.1.1` | `rsaEncryption`(CMS SignerInfo.signatureAlgorithm 生成时使用) | RFC 7935 §2(引用 RFC 3370) | +| `1.2.840.113549.1.1.11` | `sha256WithRSAEncryption`(证书/CRL 签名算法;CMS 验证兼容) | RFC 7935 §2(引用 RFC 4055) | + +## 0.6 `None` 类型字段的含义(文档约定) + +在本目录的“抽象数据模型(接口)”字段表里,如果某字段的类型标为 `None`,表示: + +- 该字段/子字段在通用 ASN.1 结构里**可能存在**(或是可选字段/可扩展集合),但在 RPKI profile 下被明确规定 **MUST NOT be present / MUST be omitted**; +- 因此它**不应作为正常语义数据对象的可用字段**出现; +- 实现可以选择两种方式之一: + 1) **不在对象结构中建模该字段**,仅在“约束清单”里写明禁止出现;解析时若遇到该字段则直接报“profile 违规”;或 + 2) 为了便于输出结构化诊断,把它保留成“永远为 None 的占位字段”,并规定:若从 DER 中解析到该字段,则对象不符合 profile。 + +注意:像 CMS `signedAttrs` 中的 “other attributes MUST NOT be included” 这类规则,本质是“集合不得含额外成员”。文档里用 `other_attrs: None` 只是为了把“禁止项”放进同一张字段表,便于实现逐条对照;它不代表 ASN.1 里真的有一个名为 `other_attrs` 的字段。 diff --git a/specs/04_crl.md b/specs/04_crl.md new file mode 100644 index 0000000..8b861d3 --- /dev/null +++ b/specs/04_crl.md @@ -0,0 +1,141 @@ +# 04. CRL(Resource Certificate Revocation List) + +## 4.1 对象定位 + +RPKI CA 必须发布符合 profile 的 CRL,用于声明其签发且未过期证书中的撤销集合。RFC 6487 §5。 + +RFC 9829 更新了 RFC 6487 对 CRL Number 以及 “current CRL” 识别的处理规则。RFC 9829 §3。 + +## 4.2 原始载体与编码 + +- 载体:X.509 CRL。 +- 编码:DER(遵循 RFC 5280 的 CRL 结构与字段语义,但受 RPKI profile 限制)。RFC 6487 §5(“consistent with RFC 5280”)。 + +### 4.2.1 X.509 v2 CRL 基本语法(ASN.1;RFC 5280 §5.1) + +CRL 在编码层面是 RFC 5280 定义的 `CertificateList`(DER)。RFC 5280 §5.1。 + +```asn1 +CertificateList ::= SEQUENCE { + tbsCertList TBSCertList, + signatureAlgorithm AlgorithmIdentifier, + signatureValue BIT STRING } + +TBSCertList ::= SEQUENCE { + version Version OPTIONAL, + -- if present, MUST be v2 + signature AlgorithmIdentifier, + issuer Name, + thisUpdate Time, + nextUpdate Time OPTIONAL, + revokedCertificates SEQUENCE OF SEQUENCE { + userCertificate CertificateSerialNumber, + revocationDate Time, + crlEntryExtensions Extensions OPTIONAL + -- if present, version MUST be v2 + } OPTIONAL, + crlExtensions [0] EXPLICIT Extensions OPTIONAL + -- if present, version MUST be v2 + } +``` + +> 注:`Version`/`Time`/`CertificateSerialNumber`/`Extensions` 的定义在 RFC 5280 §4.1;`AlgorithmIdentifier` 的定义在 RFC 5280 §4.1.1.2(RFC 5280 §5.1 的注释段落给出引用)。 + +### 4.2.2 CRL 扩展中常用内层结构(ASN.1;RFC 5280 §4.2.1.1;RFC 5280 §5.2.3) + +RPKI profile(经 RFC 9829 更新)要求 CRL **仅允许**两个扩展:AKI 与 CRL Number。RFC 9829 §3.1。 + +```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-cRLNumber OBJECT IDENTIFIER ::= { id-ce 20 } + +CRLNumber ::= INTEGER (0..MAX) +``` + + +## 4.3 抽象数据模型(接口) + +### 4.3.0 `Asn1TimeUtc`(X.509 `Time` 的抽象) + +X.509 中的 `Time` 是一个 CHOICE,可用 `UTCTime` 或 `GeneralizedTime` 编码;但在语义层面都表达一个 UTC 时间点。RFC 5280 §4.1.2.5;RFC 5280 §5.1.2.4。 + +因此在抽象模型中,将 `Time` 规范化为 “UTC 时间点 + 原始编码形态”: + +| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 | +|---|---|---|---|---| +| `utc` | `UtcTime` | 规范化后的 UTC 时间点 | 从 `UTCTime`/`GeneralizedTime` 解码并转换为 UTC 时间点 | RFC 5280 §4.1.2.5.1;RFC 5280 §4.1.2.5.2 | +| `encoding` | `enum{UTCTime, GeneralizedTime}` | 原始编码类型 | 需要保留编码形态以支持 profile 的编码约束校验 | RFC 5280 §5.1.2.4;RFC 5280 §5.1.2.5 | + +### 4.3.1 `RpkixCrl` + +| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 | +|---|---|---|---|---| +| `raw_der` | `DerBytes` | CRL DER | 原样保留(建议) | RFC 6487 §5 | +| `version` | `int` | CRL 版本 | MUST 为 v2;RP 不要求处理 v1 | RFC 6487 §5 | +| `issuer_dn` | `RpkixDistinguishedName` | CRL issuer | issuer DN 约束同证书 issuer(CN/serialNumber 规则) | RFC 6487 §5(引用 §4.4) | +| `signature_algorithm` | `Oid` | CRL 签名算法 | 必须为 `sha256WithRSAEncryption`(`1.2.840.113549.1.1.11`) | RFC 6487 §5;RFC 7935 §2(引用 RFC 4055) | +| `this_update` | `Asn1TimeUtc` | 本次 CRL 生成时间 | X.509 `Time`(UTCTime/GeneralizedTime)解码为 UTC 时间点,并保留原始编码形态 | RFC 5280 §5.1.2.4;RFC 5280 §4.1.2.5 | +| `next_update` | `Asn1TimeUtc` | 下次 CRL 计划时间 | **MUST present**(尽管 ASN.1 标注 OPTIONAL);解码同 `this_update` | RFC 5280 §5.1.2.5 | +| `revoked_certs` | `list[RevokedCert]` | 撤销条目 | 仅包含未过期且已撤销证书 | RFC 6487 §5 | +| `extensions` | `CrlExtensions` | CRL 扩展 | **仅允许** AKI 与 CRL Number 两个扩展 | RFC 9829 §3.1(更新 RFC 6487 §5) | + +### 4.3.2 `RevokedCert` + +| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 | +|---|---|---|---|---| +| `serial_number` | `int` | 被撤销证书序列号 | 必须存在 | RFC 6487 §5 | +| `revocation_date` | `Asn1TimeUtc` | 撤销时间 | X.509 `Time` 解码为 UTC 时间点,并保留原始编码形态;revocationDate 的表达规则同 `thisUpdate` | RFC 6487 §5;RFC 5280 §5.1.2.6(引用 §5.1.2.4) | +| `entry_extensions` | `None` | 条目扩展 | CRL entry extensions MUST NOT present | RFC 6487 §5 | + +### 4.3.3 `CrlExtensions` + +| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 | +|---|---|---|---|---| +| `authority_key_identifier` | `bytes` | AKI.keyIdentifier | **extnID=`2.5.29.35`**;RP MUST 处理 AKI;CRL 扩展仅允许 AKI 与 CRLNumber | RFC 9829 §3.1;RFC 5280 §5.2.1 | +| `crl_number` | `int` | CRLNumber | **extnID=`2.5.29.20`**;RP 必须忽略其排序语义;仅检查 non-critical 且数值范围:0..2^159-1 | RFC 9829 §3.1(引用 RFC 5280 §5.2.3) | + +## 4.4 字段级约束清单(实现对照) + +- CA 必须签发 version 2 CRL;RP 不要求处理 v1。RFC 6487 §5。 +- CRL 不得包含 Indirect CRL 或 Delta CRL。RFC 6487 §5。 +- CRL 范围必须覆盖该 CA 签发的全部证书。RFC 6487 §5。 +- `nextUpdate` MUST present(尽管 ASN.1 标注 OPTIONAL)。RFC 5280 §5.1.2.5。 +- 时间编码规则(`thisUpdate`/`nextUpdate`/`revocationDate`):2049(含)及之前必须用 `UTCTime`;2050(含)及之后必须用 `GeneralizedTime`;应用必须能处理两者。RFC 5280 §5.1.2.4;RFC 5280 §5.1.2.5;RFC 5280 §5.1.2.6(引用 §5.1.2.4)。 +- 只允许两个 CRL 扩展:AKI 与 CRLNumber;除此之外不允许任何 CRL 扩展。RFC 9829 §3.1。 +- RP 必须处理 AKI;CRLNumber 仅做 “non-critical + 数值范围” 检查并忽略其它语义。RFC 9829 §3.1。 +- 每个撤销条目仅允许 Serial Number 与 Revocation Date;不允许条目扩展。RFC 6487 §5。 + +## 4.5 CRL 验签与绑定校验(验证阶段) + +> 本节描述“基于已解析的数据对象对 CRL 做签名校验(cryptographic signature validation)”所需输入与处理步骤。其定位属于验证阶段(而非纯解码阶段)。 + +### 4.5.1 验签所需输入 + +- **CRL 对象本身**:包含 `tbsCertList`(待签名数据)、`signatureAlgorithm`、`signatureValue`。RFC 5280 §5.1。 +- **CRL issuer 的 CA 证书(或其公钥)**:用于提供验签公钥(`SubjectPublicKeyInfo`)。RFC 5280 §6.3.3 (f)-(g)。 +- (链路上下文)用于验证 issuer CA 证书链的同一信任锚(trust anchor):CRL issuer 的证书链必须与目标证书使用同一信任锚。RFC 5280 §6.3.3 (f)。 + +### 4.5.2 绑定与一致性校验(推荐最小集合) + +在执行签名验签之前,RP 通常应进行下列绑定校验以确保“用的是正确的 issuer 证书/公钥”: + +1. **Issuer DN 匹配**:`CRL.issuer_dn` 必须等于 issuer CA 证书的 `subject`。RFC 5280 §5.1(`issuer` 字段);RFC 5280 §6.3.3 (b)(“verify that the CRL issuer matches the certificate issuer” 的一般化要求)。 +2. **AKI ↔ SKI 绑定**:若 issuer CA 证书包含 `SubjectKeyIdentifier`(SKI,OID `2.5.29.14`),则其值应与 CRL 的 `AuthorityKeyIdentifier.keyIdentifier`(AKI,OID `2.5.29.35`)匹配。RFC 5280 §4.2.1.1;RFC 5280 §4.2.1.2;RFC 5280 §6.3.3 (c)(3)(delta 与 complete CRL 的 AKI 匹配规则;在非 delta 场景下同样用于选择正确的签发者公钥)。 +3. **KeyUsage 约束**:若 issuer CA 证书存在 `KeyUsage` 扩展(OID `2.5.29.15`),则必须包含 `cRLSign` 位。RFC 5280 §4.2.1.3;RFC 5280 §6.3.3 (f)。 + +> 注:SKI 的生成算法在 RFC 5280 中不强制限定(不同 CA 可能采用不同方式生成 keyIdentifier);因此一般做“字节值匹配”,而不对 SKI 值做“从公钥推导再比对”的强校验。 + +### 4.5.3 签名验签 + +完成上述绑定后,使用 issuer CA 证书提供的公钥对 CRL 签名进行验签: + +- 使用 issuer 的 `SubjectPublicKeyInfo` 验证 `signatureValue` 是对 `tbsCertList` 的正确签名。RFC 5280 §6.3.3 (g)。 +- RPKI profile 限定签名算法为 `sha256WithRSAEncryption`(OID `1.2.840.113549.1.1.11`),并要求算法参数编码符合 X.509/PKIX 约束(常见为 absent 或 NULL)。RFC 6487 §5;RFC 7935 §2;RFC 5280 §4.1.1.2。 diff --git a/src/data_model/crl.rs b/src/data_model/crl.rs new file mode 100644 index 0000000..6d52d7f --- /dev/null +++ b/src/data_model/crl.rs @@ -0,0 +1,429 @@ +use x509_parser::extensions::{AuthorityKeyIdentifier, ParsedExtension, X509Extension}; +use x509_parser::prelude::FromDer; +use x509_parser::prelude::X509Version; +use x509_parser::revocation_list::CertificateRevocationList; +use x509_parser::asn1_rs::Tag; +use x509_parser::certificate::X509Certificate; +use x509_parser::x509::SubjectPublicKeyInfo; +use x509_parser::x509::AlgorithmIdentifier; + +const OID_SHA256_WITH_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.11"; +const OID_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35"; +const OID_CRL_NUMBER: &str = "2.5.29.20"; +const OID_SUBJECT_KEY_IDENTIFIER: &str = "2.5.29.14"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Asn1TimeEncoding { + UtcTime, + GeneralizedTime, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Asn1TimeUtc { + pub utc: time::OffsetDateTime, + pub encoding: Asn1TimeEncoding, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BigUnsigned { + /// Minimal big-endian bytes. For zero, this is `[0]`. + pub bytes_be: Vec, +} + +impl BigUnsigned { + pub fn to_hex_upper(&self) -> String { + hex::encode_upper(&self.bytes_be) + } + + pub fn to_u64(&self) -> Option { + if self.bytes_be.len() > 8 { + return None; + } + let mut value: u64 = 0; + for &b in &self.bytes_be { + value = (value << 8) | (b as u64); + } + Some(value) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RevokedCert { + pub serial_number: BigUnsigned, + pub revocation_date: Asn1TimeUtc, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CrlExtensions { + pub authority_key_identifier: Vec, + pub crl_number: BigUnsigned, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RpkixCrl { + pub raw_der: Vec, + pub version: u32, + pub issuer_dn: String, + pub signature_algorithm_oid: String, + pub this_update: Asn1TimeUtc, + pub next_update: Asn1TimeUtc, + pub revoked_certs: Vec, + pub extensions: CrlExtensions, +} + +#[derive(Debug, thiserror::Error)] +pub enum CrlDecodeError { + #[error("X.509 CRL parse error: {0}")] + Parse(String), + + #[error("trailing bytes after CRL DER: {0} bytes")] + TrailingBytes(usize), + + #[error("CRL version must be v2, got {0:?}")] + InvalidVersion(Option), + + #[error("CRL signatureAlgorithm must be sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0}")] + InvalidSignatureAlgorithm(String), + + #[error("CRL signature algorithm parameters must be absent or NULL")] + InvalidSignatureAlgorithmParameters, + + #[error("CRL signatureAlgorithm must match TBSCertList.signature")] + SignatureAlgorithmMismatch, + + #[error("CRL extensions must be exactly two (AKI + CRLNumber), got {0}")] + InvalidExtensionsCount(usize), + + #[error("unsupported CRL extension OID {0}")] + UnsupportedExtension(String), + + #[error("duplicate CRL extension OID {0}")] + DuplicateExtension(String), + + #[error("AuthorityKeyIdentifier must contain keyIdentifier")] + AkiMissingKeyIdentifier, + + #[error("AuthorityKeyIdentifier must not contain authorityCertIssuer or authorityCertSerialNumber")] + AkiHasOtherFields, + + #[error("CRLNumber must be non-critical")] + CrlNumberCritical, + + #[error("CRLNumber out of range (must fit in 0..2^159-1)")] + CrlNumberOutOfRange, + + #[error("CRL entry extensions must not be present")] + EntryExtensionsNotAllowed, + + #[error("CRL nextUpdate must be present (RFC 5280 §5.1.2.5)")] + NextUpdateMissing, + + #[error("{field} time encoding invalid for year {year}: got {encoding:?}")] + InvalidTimeEncoding { + field: &'static str, + year: i32, + encoding: Asn1TimeEncoding, + }, +} + +impl RpkixCrl { + /// Decode a DER-encoded X.509 v2 CRL and enforce the RPKI profile constraints from + /// `specs/prepare/data_models/04_crl.md` (RFC 6487 §5; RFC 9829 §3.1; RFC 5280 §5.1). + pub fn decode_der(der: &[u8]) -> Result { + let (rem, crl) = CertificateRevocationList::from_der(der) + .map_err(|e| CrlDecodeError::Parse(e.to_string()))?; + if !rem.is_empty() { + return Err(CrlDecodeError::TrailingBytes(rem.len())); + } + + let version = match crl.version() { + Some(X509Version::V2) => 2, + Some(v) => return Err(CrlDecodeError::InvalidVersion(Some(v.0))), + None => return Err(CrlDecodeError::InvalidVersion(None)), + }; + + let sig_oid = crl.signature_algorithm.algorithm.to_id_string(); + let tbs_sig_oid = crl.tbs_cert_list.signature.algorithm.to_id_string(); + if sig_oid != tbs_sig_oid { + return Err(CrlDecodeError::SignatureAlgorithmMismatch); + } + if sig_oid != OID_SHA256_WITH_RSA_ENCRYPTION { + return Err(CrlDecodeError::InvalidSignatureAlgorithm(sig_oid)); + } + validate_sig_params(&crl.signature_algorithm)?; + validate_sig_params(&crl.tbs_cert_list.signature)?; + + let extensions = parse_and_validate_extensions(crl.extensions())?; + + let revoked_certs = crl + .iter_revoked_certificates() + .map(|rc| { + if !rc.extensions().is_empty() { + return Err(CrlDecodeError::EntryExtensionsNotAllowed); + } + let revocation_date = asn1_time_to_model(rc.revocation_date); + validate_time_encoding("revocationDate", &revocation_date)?; + Ok(RevokedCert { + serial_number: biguint_to_big_unsigned(rc.serial()), + revocation_date, + }) + }) + .collect::, _>>()?; + + let this_update = asn1_time_to_model(crl.last_update()); + validate_time_encoding("thisUpdate", &this_update)?; + + let next_update = crl + .next_update() + .map(asn1_time_to_model) + .ok_or(CrlDecodeError::NextUpdateMissing)?; + validate_time_encoding("nextUpdate", &next_update)?; + + Ok(RpkixCrl { + raw_der: der.to_vec(), + version, + issuer_dn: crl.issuer().to_string(), + signature_algorithm_oid: OID_SHA256_WITH_RSA_ENCRYPTION.to_string(), + this_update, + next_update, + revoked_certs, + extensions, + }) + } + + /// Verify the cryptographic signature on this CRL using the issuer certificate. + /// + /// Signature verification needs the issuer public key (RFC 5280 §6.3.3 (f)-(g)). + /// In RPKI practice, this public key is obtained from the CRL issuer CA certificate + /// (and that certificate must already be validated up to the same trust anchor). + /// + /// This helper also performs common binding checks: + /// - CRL `issuer_dn` must equal issuer certificate `subject` + /// - if issuer KeyUsage is present, require `cRLSign` + /// - if issuer SKI is present, require it matches CRL AKI.keyIdentifier + pub fn verify_signature_with_issuer_certificate_der( + &self, + issuer_cert_der: &[u8], + ) -> Result<(), CrlVerifyError> { + let (rem, issuer_cert) = X509Certificate::from_der(issuer_cert_der) + .map_err(|e| CrlVerifyError::IssuerCertificateParse(e.to_string()))?; + if !rem.is_empty() { + return Err(CrlVerifyError::IssuerCertificateTrailingBytes(rem.len())); + } + + let subject_dn = issuer_cert.subject().to_string(); + if subject_dn != self.issuer_dn { + return Err(CrlVerifyError::IssuerSubjectMismatch { + crl_issuer_dn: self.issuer_dn.clone(), + issuer_subject_dn: subject_dn, + }); + } + + if let Some(ku) = issuer_cert + .key_usage() + .map_err(|e| CrlVerifyError::IssuerCertificateParse(e.to_string()))? + { + if !ku.value.crl_sign() { + return Err(CrlVerifyError::IssuerKeyUsageMissingCrlSign); + } + } + + if let Some(issuer_ski) = get_subject_key_identifier(&issuer_cert) { + if issuer_ski != self.extensions.authority_key_identifier { + return Err(CrlVerifyError::AkiSkiMismatch); + } + } + + self.verify_signature_with_issuer_spki(issuer_cert.public_key()) + } + + /// Verify the cryptographic signature on this CRL using the issuer SubjectPublicKeyInfo. + pub fn verify_signature_with_issuer_spki( + &self, + issuer_spki: &SubjectPublicKeyInfo<'_>, + ) -> Result<(), CrlVerifyError> { + let (rem, crl) = CertificateRevocationList::from_der(&self.raw_der) + .map_err(|e| CrlVerifyError::CrlParse(e.to_string()))?; + if !rem.is_empty() { + return Err(CrlVerifyError::CrlTrailingBytes(rem.len())); + } + crl.verify_signature(issuer_spki) + .map_err(|e| CrlVerifyError::InvalidSignature(e.to_string())) + } + + /// Verify the cryptographic signature on this CRL using a DER-encoded SubjectPublicKeyInfo. + pub fn verify_signature_with_issuer_spki_der( + &self, + issuer_spki_der: &[u8], + ) -> Result<(), CrlVerifyError> { + let (rem, spki) = SubjectPublicKeyInfo::from_der(issuer_spki_der) + .map_err(|e| CrlVerifyError::IssuerSpkiParse(e.to_string()))?; + if !rem.is_empty() { + return Err(CrlVerifyError::IssuerSpkiTrailingBytes(rem.len())); + } + self.verify_signature_with_issuer_spki(&spki) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum CrlVerifyError { + #[error("issuer certificate parse error: {0}")] + IssuerCertificateParse(String), + + #[error("trailing bytes after issuer certificate DER: {0} bytes")] + IssuerCertificateTrailingBytes(usize), + + #[error("issuer SubjectPublicKeyInfo parse error: {0}")] + IssuerSpkiParse(String), + + #[error("trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes")] + IssuerSpkiTrailingBytes(usize), + + #[error("CRL parse error: {0}")] + CrlParse(String), + + #[error("trailing bytes after CRL DER: {0} bytes")] + CrlTrailingBytes(usize), + + #[error("CRL issuer DN does not match issuer certificate subject")] + IssuerSubjectMismatch { + crl_issuer_dn: String, + issuer_subject_dn: String, + }, + + #[error("issuer certificate keyUsage present but missing cRLSign")] + IssuerKeyUsageMissingCrlSign, + + #[error("CRL AKI.keyIdentifier does not match issuer certificate SKI")] + AkiSkiMismatch, + + #[error("CRL signature verification failed: {0}")] + InvalidSignature(String), +} + +fn asn1_time_to_model(t: x509_parser::time::ASN1Time) -> Asn1TimeUtc { + let encoding = if t.is_utctime() { + Asn1TimeEncoding::UtcTime + } else { + Asn1TimeEncoding::GeneralizedTime + }; + Asn1TimeUtc { + utc: t.to_datetime(), + encoding, + } +} + +fn validate_time_encoding(field: &'static str, t: &Asn1TimeUtc) -> Result<(), CrlDecodeError> { + let year = t.utc.year(); + let expected = if year <= 2049 { + Asn1TimeEncoding::UtcTime + } else { + Asn1TimeEncoding::GeneralizedTime + }; + if t.encoding != expected { + return Err(CrlDecodeError::InvalidTimeEncoding { + field, + year, + encoding: t.encoding, + }); + } + Ok(()) +} + +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 parse_and_validate_extensions(exts: &[X509Extension<'_>]) -> Result { + if exts.len() != 2 { + return Err(CrlDecodeError::InvalidExtensionsCount(exts.len())); + } + + let mut authority_key_identifier: Option> = None; + let mut crl_number: Option = None; + + for ext in exts { + let oid = ext.oid.to_id_string(); + match oid.as_str() { + OID_AUTHORITY_KEY_IDENTIFIER => { + if authority_key_identifier.is_some() { + return Err(CrlDecodeError::DuplicateExtension(oid)); + } + let aki = parse_aki(ext)?; + authority_key_identifier = Some(aki); + } + OID_CRL_NUMBER => { + if crl_number.is_some() { + return Err(CrlDecodeError::DuplicateExtension(oid)); + } + if ext.critical { + return Err(CrlDecodeError::CrlNumberCritical); + } + let n = parse_crl_number(ext)?; + if n.bits() > 159 { + return Err(CrlDecodeError::CrlNumberOutOfRange); + } + crl_number = Some(biguint_to_big_unsigned(&n)); + } + _ => return Err(CrlDecodeError::UnsupportedExtension(oid)), + } + } + + Ok(CrlExtensions { + authority_key_identifier: authority_key_identifier.unwrap(), + crl_number: crl_number.unwrap(), + }) +} + +fn parse_aki(ext: &X509Extension<'_>) -> Result, CrlDecodeError> { + let ParsedExtension::AuthorityKeyIdentifier(aki) = ext.parsed_extension() else { + return Err(CrlDecodeError::Parse("AKI extension parse failed".into())); + }; + validate_aki_profile(aki)?; + Ok(aki + .key_identifier + .as_ref() + .ok_or(CrlDecodeError::AkiMissingKeyIdentifier)? + .0 + .to_vec()) +} + +fn validate_aki_profile(aki: &AuthorityKeyIdentifier<'_>) -> Result<(), CrlDecodeError> { + if aki.key_identifier.is_none() { + return Err(CrlDecodeError::AkiMissingKeyIdentifier); + } + if aki.authority_cert_issuer.is_some() || aki.authority_cert_serial.is_some() { + return Err(CrlDecodeError::AkiHasOtherFields); + } + Ok(()) +} + +fn parse_crl_number(ext: &X509Extension<'_>) -> Result { + match ext.parsed_extension() { + ParsedExtension::CRLNumber(n) => Ok(n.clone()), + ParsedExtension::ParseError { error } => Err(CrlDecodeError::Parse(error.to_string())), + _ => Err(CrlDecodeError::Parse("CRLNumber extension parse failed".into())), + } +} + +fn biguint_to_big_unsigned(n: &der_parser::num_bigint::BigUint) -> BigUnsigned { + let mut bytes = n.to_bytes_be(); + if bytes.is_empty() { + bytes.push(0); + } + BigUnsigned { bytes_be: bytes } +} + +fn get_subject_key_identifier(cert: &X509Certificate<'_>) -> Option> { + 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, + }) +} diff --git a/src/data_model/mod.rs b/src/data_model/mod.rs new file mode 100644 index 0000000..d20a38c --- /dev/null +++ b/src/data_model/mod.rs @@ -0,0 +1,2 @@ +pub mod crl; + diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c0dd03b --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod data_model; diff --git a/tests/fixtures/0099DEAB073EFD74C250C0A382B25012B5082AEE.crl b/tests/fixtures/0099DEAB073EFD74C250C0A382B25012B5082AEE.crl new file mode 100644 index 0000000..e5b446a Binary files /dev/null and b/tests/fixtures/0099DEAB073EFD74C250C0A382B25012B5082AEE.crl differ diff --git a/tests/fixtures/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl b/tests/fixtures/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl new file mode 100644 index 0000000..6b488ad Binary files /dev/null and b/tests/fixtures/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl differ diff --git a/tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer b/tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer new file mode 100644 index 0000000..c776da5 Binary files /dev/null and b/tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl new file mode 100644 index 0000000..6b488ad Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft new file mode 100644 index 0000000..51cbb26 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142067.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142067.roa new file mode 100644 index 0000000..013a577 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142067.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142068.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142068.roa new file mode 100644 index 0000000..ef5fe69 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142068.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142069.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142069.roa new file mode 100644 index 0000000..abccbe2 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142069.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142070.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142070.roa new file mode 100644 index 0000000..30d1fd1 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142070.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142071.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142071.roa new file mode 100644 index 0000000..b3a9878 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142071.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142072.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142072.roa new file mode 100644 index 0000000..7a447f7 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142072.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142073.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142073.roa new file mode 100644 index 0000000..928dd08 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142073.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142074.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142074.roa new file mode 100644 index 0000000..abf8125 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142074.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142075.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142075.roa new file mode 100644 index 0000000..755181c Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142075.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142076.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142076.roa new file mode 100644 index 0000000..8aef0a5 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142076.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142077.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142077.roa new file mode 100644 index 0000000..73ef213 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142077.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142078.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142078.roa new file mode 100644 index 0000000..ab5ea8d Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142078.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142079.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142079.roa new file mode 100644 index 0000000..c86db63 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142079.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142080.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142080.roa new file mode 100644 index 0000000..c8cb6ec Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142080.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142081.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142081.roa new file mode 100644 index 0000000..c0c0f82 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142081.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142082.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142082.roa new file mode 100644 index 0000000..10f411d Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142082.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142083.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142083.roa new file mode 100644 index 0000000..da4b57c Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142083.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142084.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142084.roa new file mode 100644 index 0000000..069ccd7 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142084.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142085.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142085.roa new file mode 100644 index 0000000..bad4a99 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142085.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142086.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142086.roa new file mode 100644 index 0000000..52a6532 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142086.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142087.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142087.roa new file mode 100644 index 0000000..5816498 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142087.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142088.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142088.roa new file mode 100644 index 0000000..5548b91 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142088.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142089.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142089.roa new file mode 100644 index 0000000..fb9e080 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142089.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142090.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142090.roa new file mode 100644 index 0000000..87b4a0a Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142090.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142091.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142091.roa new file mode 100644 index 0000000..d5d7e5f Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142091.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142092.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142092.roa new file mode 100644 index 0000000..1429773 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142092.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142093.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142093.roa new file mode 100644 index 0000000..b51b082 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142093.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142094.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142094.roa new file mode 100644 index 0000000..07e1da2 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142094.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142095.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142095.roa new file mode 100644 index 0000000..d401660 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142095.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142096.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142096.roa new file mode 100644 index 0000000..f50bb33 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142096.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142097.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142097.roa new file mode 100644 index 0000000..e697357 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142097.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142098.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142098.roa new file mode 100644 index 0000000..e7e6a19 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142098.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142099.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142099.roa new file mode 100644 index 0000000..47ceb6b Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142099.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142100.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142100.roa new file mode 100644 index 0000000..2fe1220 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142100.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142101.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142101.roa new file mode 100644 index 0000000..8d5a596 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142101.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142102.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142102.roa new file mode 100644 index 0000000..1bcf013 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142102.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142103.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142103.roa new file mode 100644 index 0000000..f29251a Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142103.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142104.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142104.roa new file mode 100644 index 0000000..106402a Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142104.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142105.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142105.roa new file mode 100644 index 0000000..8397586 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142105.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142106.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142106.roa new file mode 100644 index 0000000..e1b7960 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142106.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142650.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142650.roa new file mode 100644 index 0000000..1e69010 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142650.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142651.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142651.roa new file mode 100644 index 0000000..824c1de Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142651.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142652.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142652.roa new file mode 100644 index 0000000..6c9c2e8 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142652.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142653.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142653.roa new file mode 100644 index 0000000..c79d7f5 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142653.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142654.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142654.roa new file mode 100644 index 0000000..ff5b171 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142654.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142655.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142655.roa new file mode 100644 index 0000000..d2952b8 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142655.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142656.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142656.roa new file mode 100644 index 0000000..7c832a7 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142656.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142657.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142657.roa new file mode 100644 index 0000000..071ef55 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142657.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142658.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142658.roa new file mode 100644 index 0000000..564add2 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142658.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142659.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142659.roa new file mode 100644 index 0000000..5f96ec2 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142659.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144698.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144698.roa new file mode 100644 index 0000000..7ae6e02 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144698.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144699.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144699.roa new file mode 100644 index 0000000..6d8af43 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144699.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144700.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144700.roa new file mode 100644 index 0000000..5561289 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144700.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144701.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144701.roa new file mode 100644 index 0000000..3d9aff8 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144701.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144702.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144702.roa new file mode 100644 index 0000000..097885b Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144702.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144703.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144703.roa new file mode 100644 index 0000000..00e9899 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144703.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144704.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144704.roa new file mode 100644 index 0000000..87de888 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144704.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144705.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144705.roa new file mode 100644 index 0000000..f9733d6 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144705.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144706.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144706.roa new file mode 100644 index 0000000..0a99610 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144706.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144707.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144707.roa new file mode 100644 index 0000000..57dba74 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS144707.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS23910.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS23910.roa new file mode 100644 index 0000000..85f592e Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS23910.roa differ diff --git a/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa new file mode 100644 index 0000000..846e777 Binary files /dev/null and b/tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa differ diff --git a/tests/test_crl_decode.rs b/tests/test_crl_decode.rs new file mode 100644 index 0000000..f5055ff --- /dev/null +++ b/tests/test_crl_decode.rs @@ -0,0 +1,68 @@ +use std::path::PathBuf; + +use rpki::data_model::crl::RpkixCrl; +use rpki::data_model::crl::Asn1TimeEncoding; + +#[test] +fn decode_and_validate_crl_fixture() { + let path = PathBuf::from("tests/fixtures/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl"); + let der = std::fs::read(&path).expect("read CRL fixture"); + + let crl = RpkixCrl::decode_der(&der).expect("decode CRL"); + + assert_eq!(crl.version, 2); + assert_eq!(crl.signature_algorithm_oid, "1.2.840.113549.1.1.11"); + assert_eq!(crl.this_update.encoding, Asn1TimeEncoding::UtcTime); + assert_eq!(crl.next_update.encoding, Asn1TimeEncoding::UtcTime); + assert_eq!( + hex::encode_upper(&crl.extensions.authority_key_identifier), + "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA" + ); + assert_eq!(crl.extensions.crl_number.bytes_be, vec![12]); + assert!(crl.revoked_certs.is_empty()); + + println!("{crl:#?}"); +} + +#[test] +fn crl_signature_verification_succeeds_with_issuer_cert() { + let crl_der = std::fs::read( + "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl", + ) + .expect("read CRL fixture"); + let issuer_cert_der = std::fs::read( + "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer", + ) + .expect("read issuer certificate fixture"); + + let crl = RpkixCrl::decode_der(&crl_der).expect("decode CRL"); + crl.verify_signature_with_issuer_certificate_der(&issuer_cert_der) + .expect("CRL signature must verify with issuer certificate"); +} + +#[test] +fn decode_crl_with_revoked_entries() { + let der = + std::fs::read("tests/fixtures/0099DEAB073EFD74C250C0A382B25012B5082AEE.crl") + .expect("read CRL fixture with revoked entries"); + + let crl = RpkixCrl::decode_der(&der).expect("decode CRL"); + + assert_eq!(crl.revoked_certs.len(), 21); + for entry in &crl.revoked_certs { + assert!(!entry.serial_number.bytes_be.is_empty()); + // 0 should be encoded as [0], otherwise no leading zero bytes. + if entry.serial_number.bytes_be.len() > 1 { + assert_ne!(entry.serial_number.bytes_be[0], 0); + } + let year = entry.revocation_date.utc.year(); + let expected = if year <= 2049 { + Asn1TimeEncoding::UtcTime + } else { + Asn1TimeEncoding::GeneralizedTime + }; + assert_eq!(entry.revocation_date.encoding, expected); + } + + println!("{crl:#?}"); +}