add crl data model

This commit is contained in:
yuyr 2026-01-27 10:33:31 +08:00
parent 177883c50c
commit 5e474fffd2
76 changed files with 882 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
target/
Cargo.lock

11
Cargo.toml Normal file
View File

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

View File

@ -0,0 +1,11 @@
# 单元测试
```
cargo test
# 查看输出
cargo test -- --nocapture
```

217
specs/00_common_types.md Normal file
View File

@ -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 RulesDER。RPKI Signed Object 与其 eContent 均要求 DER 编码。RFC 6488 §2RFC 9286 §4.2RFC 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 AccessAIA与 Subject Information AccessSIA的内层值都是一个 “AccessDescription 列表”。其 ASN.1 定义如下RFC 5280 §4.2.2.1RFC 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.1RFC 5280 §4.2.2.2RFC 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.5RFC 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.10RFC 6487 §4.8.11。
- `rdi: optional[AsIdentifierChoice]`
- RPKI profile **不支持** RDIMUST 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.3RFC 3779 §3.2.3.8。
## 0.5 常用 OID 表(最小集合)
> 说明:表中 “来源 RFC” 给出该 OID 在本模型中的直接规范引用点;在实现时也可以直接用点分十进制比对。
| OID | 符号名/含义 | 来源 RFC |
|---|---|---|
| `1.2.840.113549.1.7.2` | CMS SignedDataContentInfo.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.2RPKI 约束见 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.2RPKI 语义见 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-rpkiNotifyRRDP 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.13RPKI 约束见 RFC 6487 §4.8.6 |
| `2.5.29.32` | X.509 v3 扩展certificatePolicies | RFC 5280 §4.2.1.4RPKI 约束见 RFC 6487 §4.8.9;更新见 RFC 7318 §2 |
| `2.5.29.14` | X.509 v3 扩展subjectKeyIdentifier | RFC 5280 §4.2.1.2RPKI 约束见 RFC 6487 §4.8.2 |
| `2.5.29.35` | X.509 v3 扩展authorityKeyIdentifier证书/CRL | RFC 5280 §4.2.1.1证书RFC 5280 §5.2.1CRLRPKI 约束见 RFC 6487 §4.8.3 / RFC 9829 §3.1 |
| `2.5.29.15` | X.509 v3 扩展keyUsage | RFC 5280 §4.2.1.3RPKI 约束见 RFC 6487 §4.8.4 |
| `2.5.29.19` | X.509 v3 扩展basicConstraints | RFC 5280 §4.2.1.9RPKI 约束见 RFC 6487 §4.8.1 |
| `2.5.29.20` | CRL 扩展cRLNumber | RFC 5280 §5.2.3RPKI 约束更新见 RFC 9829 §3.1 |
| `1.3.6.1.5.5.7.14.2` | RPKI Certificate Policyid-cp-ipAddr-asNumber | RFC 6484 §1.2;证书中使用要求见 RFC 6487 §4.8.9 |
| `1.3.6.1.5.5.7.2.1` | Policy Qualifierid-qt-cps | RFC 5280 §4.2.1.4RPKI 限制见 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` 的字段。

141
specs/04_crl.md Normal file
View File

@ -0,0 +1,141 @@
# 04. CRLResource 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.1RFC 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.2RFC 5280 §5.1 的注释段落给出引用)。
### 4.2.2 CRL 扩展中常用内层结构ASN.1RFC 5280 §4.2.1.1RFC 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.5RFC 5280 §5.1.2.4。
因此在抽象模型中,将 `Time` 规范化为 “UTC 时间点 + 原始编码形态”:
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|---|---|---|---|---|
| `utc` | `UtcTime` | 规范化后的 UTC 时间点 | 从 `UTCTime`/`GeneralizedTime` 解码并转换为 UTC 时间点 | RFC 5280 §4.1.2.5.1RFC 5280 §4.1.2.5.2 |
| `encoding` | `enum{UTCTime, GeneralizedTime}` | 原始编码类型 | 需要保留编码形态以支持 profile 的编码约束校验 | RFC 5280 §5.1.2.4RFC 5280 §5.1.2.5 |
### 4.3.1 `RpkixCrl`
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|---|---|---|---|---|
| `raw_der` | `DerBytes` | CRL DER | 原样保留(建议) | RFC 6487 §5 |
| `version` | `int` | CRL 版本 | MUST 为 v2RP 不要求处理 v1 | RFC 6487 §5 |
| `issuer_dn` | `RpkixDistinguishedName` | CRL issuer | issuer DN 约束同证书 issuerCN/serialNumber 规则) | RFC 6487 §5引用 §4.4 |
| `signature_algorithm` | `Oid` | CRL 签名算法 | 必须为 `sha256WithRSAEncryption``1.2.840.113549.1.1.11` | RFC 6487 §5RFC 7935 §2引用 RFC 4055 |
| `this_update` | `Asn1TimeUtc` | 本次 CRL 生成时间 | X.509 `Time`UTCTime/GeneralizedTime解码为 UTC 时间点,并保留原始编码形态 | RFC 5280 §5.1.2.4RFC 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 §5RFC 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 处理 AKICRL 扩展仅允许 AKI 与 CRLNumber | RFC 9829 §3.1RFC 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 CRLRP 不要求处理 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.4RFC 5280 §5.1.2.5RFC 5280 §5.1.2.6(引用 §5.1.2.4)。
- 只允许两个 CRL 扩展AKI 与 CRLNumber除此之外不允许任何 CRL 扩展。RFC 9829 §3.1。
- RP 必须处理 AKICRLNumber 仅做 “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 anchorCRL 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`SKIOID `2.5.29.14`),则其值应与 CRL 的 `AuthorityKeyIdentifier.keyIdentifier`AKIOID `2.5.29.35`匹配。RFC 5280 §4.2.1.1RFC 5280 §4.2.1.2RFC 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.3RFC 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 §5RFC 7935 §2RFC 5280 §4.1.1.2。

429
src/data_model/crl.rs Normal file
View File

@ -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<u8>,
}
impl BigUnsigned {
pub fn to_hex_upper(&self) -> String {
hex::encode_upper(&self.bytes_be)
}
pub fn to_u64(&self) -> Option<u64> {
if self.bytes_be.len() > 8 {
return None;
}
let mut value: u64 = 0;
for &b in &self.bytes_be {
value = (value << 8) | (b as u64);
}
Some(value)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RevokedCert {
pub serial_number: BigUnsigned,
pub revocation_date: Asn1TimeUtc,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CrlExtensions {
pub authority_key_identifier: Vec<u8>,
pub crl_number: BigUnsigned,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RpkixCrl {
pub raw_der: Vec<u8>,
pub version: u32,
pub issuer_dn: String,
pub signature_algorithm_oid: String,
pub this_update: Asn1TimeUtc,
pub next_update: Asn1TimeUtc,
pub revoked_certs: Vec<RevokedCert>,
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<u32>),
#[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<Self, CrlDecodeError> {
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::<Result<Vec<_>, _>>()?;
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<CrlExtensions, CrlDecodeError> {
if exts.len() != 2 {
return Err(CrlDecodeError::InvalidExtensionsCount(exts.len()));
}
let mut authority_key_identifier: Option<Vec<u8>> = None;
let mut crl_number: Option<BigUnsigned> = 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<Vec<u8>, 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<der_parser::num_bigint::BigUint, CrlDecodeError> {
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<Vec<u8>> {
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,
})
}

2
src/data_model/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod crl;

1
src/lib.rs Normal file
View File

@ -0,0 +1 @@
pub mod data_model;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

68
tests/test_crl_decode.rs Normal file
View File

@ -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:#?}");
}