Compare commits
4 Commits
2fb36712e2
...
cc9f3f21de
| Author | SHA1 | Date | |
|---|---|---|---|
| cc9f3f21de | |||
| 56ae2ca4fc | |||
| bcd4829486 | |||
| 421847d329 |
@ -12,3 +12,4 @@ thiserror = "2.0.18"
|
||||
time = "0.3.45"
|
||||
ring = "0.17.14"
|
||||
x509-parser = { version = "0.18.0", features = ["verify"] }
|
||||
url = "2.5.8"
|
||||
|
||||
@ -182,6 +182,8 @@ RFC 引用:RFC 5280 §4.2.2.1;RFC 5280 §4.2.2.2;RFC 5280 §4.2.1.6。
|
||||
| `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.2.840.113549.1.9.16.1.35` | Ghostbusters eContentType: id-ct-rpkiGhostbusters | RFC 6493 §6;RFC 6493 §9.1 |
|
||||
| `1.2.840.113549.1.9.16.1.49` | ASPA eContentType: id-ct-ASPA | `draft-ietf-sidrops-aspa-profile-21` §2 |
|
||||
| `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 |
|
||||
|
||||
94
specs/01_tal.md
Normal file
94
specs/01_tal.md
Normal file
@ -0,0 +1,94 @@
|
||||
# 01. TAL(Trust Anchor Locator)
|
||||
|
||||
## 1.1 对象定位
|
||||
|
||||
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 }
|
||||
```
|
||||
|
||||
其中 `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 解析规则(语义层)
|
||||
|
||||
输入:`TalFileBytes: bytes`。
|
||||
|
||||
解析步骤:
|
||||
|
||||
1) 按 `LF` / `CRLF` 识别行。RFC 8630 §2.2。
|
||||
2) 从文件开头读取所有以 `#` 开头的行,作为 `comments`(保留去掉 `#` 后的 UTF-8 文本或保留原始行均可,但需保持 UTF-8)。RFC 8630 §2.2。
|
||||
3) 继续读取一行或多行非空行,作为 `ta_uris`(保持顺序)。RFC 8630 §2.2(第 2 点)。
|
||||
4) 读取一个空行(必须存在)。RFC 8630 §2.2(第 3 点)。
|
||||
5) 将剩余行拼接为 Base64 文本(移除行分隔),Base64 解码得到 `subject_public_key_info_der`。RFC 8630 §2.2(第 4 点)。
|
||||
6) 可选:将 `subject_public_key_info_der` 解析为 X.509 `SubjectPublicKeyInfo` 结构(用于与 TA 证书比对)。RFC 8630 §2.3;RFC 5280 §4.1.2.7。
|
||||
|
||||
URI 解析与约束:
|
||||
|
||||
- `ta_uris[*]` 的 scheme **MUST** 为 `rsync` 或 `https`。RFC 8630 §2.2。
|
||||
- 每个 `ta_uri` **MUST** 指向“单个对象”,且 **MUST NOT** 指向目录或集合。RFC 8630 §2.3。
|
||||
|
||||
## 1.4 抽象数据模型(接口)
|
||||
|
||||
### 1.4.1 `Tal`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `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。
|
||||
88
specs/02_ta_certificate.md
Normal file
88
specs/02_ta_certificate.md
Normal file
@ -0,0 +1,88 @@
|
||||
# 02. TA(Trust Anchor)自签名证书
|
||||
|
||||
## 2.1 对象定位
|
||||
|
||||
在 RP 侧,“信任锚(Trust Anchor, TA)”以一个**自签名 CA 资源证书**体现,其可获取位置与期望公钥由 TAL 提供。RFC 8630 §2.3。
|
||||
|
||||
本文件描述两个紧密相关的数据对象:
|
||||
|
||||
1) `TaCertificate`:TA 自签名资源证书本体(X.509 DER)
|
||||
2) `TrustAnchor`:语义组合对象(`TAL` + `TaCertificate` 的绑定语义)
|
||||
|
||||
## 2.2 原始载体与编码
|
||||
|
||||
- 载体:X.509 证书(通常以 `.cer` 存放于仓库,但文件扩展名不作为语义依据)。
|
||||
- 编码:DER。TA 证书必须符合 RPKI 资源证书 profile。RFC 8630 §2.3;RFC 6487 §4。
|
||||
|
||||
### 2.2.1 X.509 Certificate 的 ASN.1 定义(RFC 5280 §4.1;TA 与 RC 共享)
|
||||
|
||||
TA 证书与普通资源证书(RC)在编码层面都是 X.509 `Certificate`(DER)。其 ASN.1 定义如下:RFC 5280 §4.1。
|
||||
|
||||
```asn1
|
||||
Certificate ::= SEQUENCE {
|
||||
tbsCertificate TBSCertificate,
|
||||
signatureAlgorithm AlgorithmIdentifier,
|
||||
signatureValue BIT STRING }
|
||||
```
|
||||
|
||||
其中 `tbsCertificate.extensions`(v3 扩展)是 RPKI 语义的主要承载处(IP/AS 资源扩展、SIA/AIA/CRLDP 等)。RFC 5280 §4.1;RPKI 对字段/扩展存在性与关键性约束见 RFC 6487 §4。
|
||||
|
||||
> 说明:更完整的 RC 编码层结构(包括 Extension 外层“extnValue 二次 DER 解码”的套娃方式)在 `03_resource_certificate_rc.md` 与 `00_common_types.md` 中给出。
|
||||
|
||||
## 2.3 TA 证书的 RPKI 语义约束(在 RC profile 基础上额外强调)
|
||||
|
||||
### 2.3.1 自签名与 profile
|
||||
|
||||
- TA URI 指向的对象 **MUST** 是一个**自签名 CA 证书**,并且 **MUST** 符合 RPKI 证书 profile。RFC 8630 §2.3;RFC 6487 §4。
|
||||
- 自签名证书在 RC profile 下的通用差异(例如 CRLDP/AIA 的省略规则、AKI 的规则)见 RFC 6487。RFC 6487 §4.8.3;RFC 6487 §4.8.6;RFC 6487 §4.8.7。
|
||||
|
||||
### 2.3.2 INR(IP/AS 资源扩展)在 TA 上的额外约束
|
||||
|
||||
- TA 的 INR 扩展(IP/AS 资源扩展,RFC 3779)**MUST** 是非空资源集合。RFC 8630 §2.3。
|
||||
- TA 的 INR 扩展 **MUST NOT** 使用 `inherit` 形式。RFC 8630 §2.3。
|
||||
- 说明:一般 RC profile 允许 `inherit`。RFC 6487 §4.8.10;RFC 6487 §4.8.11;RFC 3779 §2.2.3.5;RFC 3779 §3.2.3.3。
|
||||
|
||||
### 2.3.3 TAL ↔ TA 公钥绑定
|
||||
|
||||
- 用于验证 TA 的公钥(来自 TAL 中的 SPKI)**MUST** 与 TA 证书中的 `subjectPublicKeyInfo` 相同。RFC 8630 §2.3。
|
||||
|
||||
### 2.3.4 TA 稳定性语义(实现需建模为“约束/假设”,但不属于验证结果态)
|
||||
|
||||
- TA 公钥与 TAL 中公钥必须保持稳定(用于 RP 侧长期信任锚)。RFC 8630 §2.3。
|
||||
|
||||
### 2.3.5 TA 与 CRL/Manifest 的关系(语义)
|
||||
|
||||
- RFC 8630 指出:TA 为自签名证书,没有对应 CRL,且不会被 manifest 列出;TA 的获取/轮换由 TAL 控制。RFC 8630 §2.3。
|
||||
|
||||
> 注:这条更偏“发布/运维语义”,但对数据对象建模有影响:`TrustAnchor` 组合对象不应依赖 CRL/MFT 的存在。
|
||||
|
||||
## 2.4 抽象数据模型(接口)
|
||||
|
||||
### 2.4.1 `TaCertificate`
|
||||
|
||||
> 该对象在字段层面复用 `RC(CA)` 的语义模型(见 `03_resource_certificate_rc.md`),但增加 TA 特有约束。
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `raw_der` | `DerBytes` | TA 证书 DER | X.509 DER;证书 profile 约束见 RC 文档 | RFC 8630 §2.3;RFC 6487 §4 |
|
||||
| `rc_ca` | `ResourceCaCertificate` | 以 RC(CA) 语义解析出的字段集合 | 必须满足“自签名 CA”分支约束;且 INR 必须非空且不允许 inherit | RFC 8630 §2.3;RFC 6487 §4;RFC 3779 §2/§3 |
|
||||
|
||||
### 2.4.2 `TrustAnchor`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `tal` | `Tal` | TAL 文件语义对象 | 见 `01_tal.md` | RFC 8630 §2.2 |
|
||||
| `ta_certificate` | `TaCertificate` | TA 证书语义对象 | TA URI 指向的对象 | RFC 8630 §2.3 |
|
||||
| `tal_spki_der` | `DerBytes` | 从 TAL 解析出的 SPKI DER | `tal.subject_public_key_info_der` | RFC 8630 §2.2 |
|
||||
| `ta_spki_der` | `DerBytes` | 从 TA 证书抽取的 SPKI DER | `ta_certificate` 的 `subjectPublicKeyInfo` | RFC 8630 §2.3;RFC 5280 §4.1.2.7 |
|
||||
|
||||
**绑定约束(字段级)**
|
||||
|
||||
- `tal_spki_der` 必须与 `ta_spki_der` 完全相等(字节层面的 DER 等价)。RFC 8630 §2.3。
|
||||
|
||||
## 2.5 字段级约束清单(实现对照)
|
||||
|
||||
- TA URI 指向的对象必须是自签名 CA 证书,且符合 RPKI 证书 profile。RFC 8630 §2.3;RFC 6487 §4。
|
||||
- TA 的 INR 扩展必须非空,且不得使用 inherit。RFC 8630 §2.3。
|
||||
- TAL 中 SPKI 必须与 TA 证书的 `subjectPublicKeyInfo` 匹配。RFC 8630 §2.3。
|
||||
- TA 不依赖 CRL/MFT(无对应 CRL,且不被 manifest 列出)。RFC 8630 §2.3。
|
||||
460
specs/03_resource_certificate_rc.md
Normal file
460
specs/03_resource_certificate_rc.md
Normal file
@ -0,0 +1,460 @@
|
||||
# 03. RC(Resource Certificate:资源证书,CA/EE)
|
||||
|
||||
## 3.1 对象定位
|
||||
|
||||
资源证书(RC)是 X.509 v3 证书,遵循 PKIX profile(RFC 5280),并受 RPKI profile 进一步约束。RFC 6487 §4。
|
||||
|
||||
RC 在 RPKI 中至少分为两类语义用途:
|
||||
|
||||
- `CA 证书`:签发下级证书/CRL,并在 SIA 中声明发布点与 manifest。RFC 6487 §4.8.8.1。
|
||||
- `EE 证书`:用于验证某个 RPKI Signed Object(如 ROA/MFT),在 SIA 中指向被验证对象。RFC 6487 §4.8.8.2。
|
||||
|
||||
## 3.2 原始载体与编码
|
||||
|
||||
- 载体:X.509 证书。
|
||||
- 编码:DER。RFC 6487 §4(“valid X.509 public key certificate consistent with RFC 5280” + RPKI 限制)。
|
||||
|
||||
### 3.2.1 X.509 v3 证书基本语法(ASN.1;RFC 5280 §4.1)
|
||||
|
||||
资源证书在编码层面是 RFC 5280 定义的 X.509 v3 `Certificate`(DER),其中 `tbsCertificate` 携带主体字段与扩展集合(`Extensions`)。RFC 5280 §4.1。
|
||||
|
||||
```asn1
|
||||
Certificate ::= SEQUENCE {
|
||||
tbsCertificate TBSCertificate,
|
||||
signatureAlgorithm AlgorithmIdentifier,
|
||||
signatureValue BIT STRING }
|
||||
|
||||
TBSCertificate ::= SEQUENCE {
|
||||
version [0] EXPLICIT Version DEFAULT v1,
|
||||
serialNumber CertificateSerialNumber,
|
||||
signature AlgorithmIdentifier,
|
||||
issuer Name,
|
||||
validity Validity,
|
||||
subject Name,
|
||||
subjectPublicKeyInfo SubjectPublicKeyInfo,
|
||||
issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
|
||||
-- If present, version MUST be v2 or v3
|
||||
subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
|
||||
-- If present, version MUST be v2 or v3
|
||||
extensions [3] EXPLICIT Extensions OPTIONAL
|
||||
-- If present, version MUST be v3
|
||||
}
|
||||
|
||||
Version ::= INTEGER { v1(0), v2(1), v3(2) }
|
||||
|
||||
CertificateSerialNumber ::= INTEGER
|
||||
|
||||
Validity ::= SEQUENCE {
|
||||
notBefore Time,
|
||||
notAfter Time }
|
||||
|
||||
Time ::= CHOICE {
|
||||
utcTime UTCTime,
|
||||
generalTime GeneralizedTime }
|
||||
|
||||
UniqueIdentifier ::= BIT STRING
|
||||
|
||||
SubjectPublicKeyInfo ::= SEQUENCE {
|
||||
algorithm AlgorithmIdentifier,
|
||||
subjectPublicKey BIT STRING }
|
||||
|
||||
Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension
|
||||
|
||||
Extension ::= SEQUENCE {
|
||||
extnID OBJECT IDENTIFIER,
|
||||
critical BOOLEAN DEFAULT FALSE,
|
||||
extnValue OCTET STRING
|
||||
-- contains the DER encoding of an ASN.1 value
|
||||
-- corresponding to the extension type identified
|
||||
-- by extnID
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2.2 AlgorithmIdentifier(ASN.1;RFC 5280 §4.1.1.2)
|
||||
|
||||
```asn1
|
||||
AlgorithmIdentifier ::= SEQUENCE {
|
||||
algorithm OBJECT IDENTIFIER,
|
||||
parameters ANY DEFINED BY algorithm OPTIONAL }
|
||||
```
|
||||
|
||||
### 3.2.3 Name / DN 结构(ASN.1;RFC 5280 §4.1.2.4)
|
||||
|
||||
```asn1
|
||||
Name ::= CHOICE { -- only one possibility for now --
|
||||
rdnSequence RDNSequence }
|
||||
|
||||
RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
|
||||
|
||||
RelativeDistinguishedName ::=
|
||||
SET SIZE (1..MAX) OF AttributeTypeAndValue
|
||||
|
||||
AttributeTypeAndValue ::= SEQUENCE {
|
||||
type AttributeType,
|
||||
value AttributeValue }
|
||||
|
||||
AttributeType ::= OBJECT IDENTIFIER
|
||||
|
||||
AttributeValue ::= ANY -- DEFINED BY AttributeType
|
||||
|
||||
DirectoryString ::= CHOICE {
|
||||
teletexString TeletexString (SIZE (1..MAX)),
|
||||
printableString PrintableString (SIZE (1..MAX)),
|
||||
universalString UniversalString (SIZE (1..MAX)),
|
||||
utf8String UTF8String (SIZE (1..MAX)),
|
||||
bmpString BMPString (SIZE (1..MAX)) }
|
||||
```
|
||||
|
||||
### 3.2.4 GeneralNames / GeneralName(ASN.1;RFC 5280 §4.2.1.6)
|
||||
|
||||
> 说明:RPKI 的 AIA/SIA/CRLDP 等扩展通常把 URI 编码在 `uniformResourceIdentifier [6] IA5String` 分支中。RFC 5280 §4.2.1.6。
|
||||
|
||||
```asn1
|
||||
GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
|
||||
|
||||
GeneralName ::= CHOICE {
|
||||
otherName [0] OtherName,
|
||||
rfc822Name [1] IA5String,
|
||||
dNSName [2] IA5String,
|
||||
x400Address [3] ORAddress,
|
||||
directoryName [4] Name,
|
||||
ediPartyName [5] EDIPartyName,
|
||||
uniformResourceIdentifier [6] IA5String,
|
||||
iPAddress [7] OCTET STRING,
|
||||
registeredID [8] OBJECT IDENTIFIER }
|
||||
|
||||
OtherName ::= SEQUENCE {
|
||||
type-id OBJECT IDENTIFIER,
|
||||
value [0] EXPLICIT ANY DEFINED BY type-id }
|
||||
|
||||
EDIPartyName ::= SEQUENCE {
|
||||
nameAssigner [0] DirectoryString OPTIONAL,
|
||||
partyName [1] DirectoryString }
|
||||
```
|
||||
|
||||
### 3.2.5 AIA(Authority Information Access;ASN.1;RFC 5280 §4.2.2.1)
|
||||
|
||||
```asn1
|
||||
id-pe-authorityInfoAccess OBJECT IDENTIFIER ::= { id-pe 1 }
|
||||
|
||||
AuthorityInfoAccessSyntax ::=
|
||||
SEQUENCE SIZE (1..MAX) OF AccessDescription
|
||||
|
||||
AccessDescription ::= SEQUENCE {
|
||||
accessMethod OBJECT IDENTIFIER,
|
||||
accessLocation GeneralName }
|
||||
|
||||
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
|
||||
|
||||
id-ad-caIssuers OBJECT IDENTIFIER ::= { id-ad 2 }
|
||||
```
|
||||
|
||||
### 3.2.6 SIA(Subject Information Access;ASN.1;RFC 5280 §4.2.2.2)
|
||||
|
||||
```asn1
|
||||
id-pe-subjectInfoAccess OBJECT IDENTIFIER ::= { id-pe 11 }
|
||||
|
||||
SubjectInfoAccessSyntax ::=
|
||||
SEQUENCE SIZE (1..MAX) OF AccessDescription
|
||||
|
||||
AccessDescription ::= SEQUENCE {
|
||||
accessMethod OBJECT IDENTIFIER,
|
||||
accessLocation GeneralName }
|
||||
|
||||
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
|
||||
|
||||
id-ad-caRepository OBJECT IDENTIFIER ::= { id-ad 5 }
|
||||
```
|
||||
|
||||
### 3.2.7 RPKI 在 SIA 中新增/使用的 accessMethod OID(RFC 6487 §4.8.8.1 / §4.8.8.2;RFC 8182 §3.2)
|
||||
|
||||
> 说明:下列 OID 用于 `AccessDescription.accessMethod`,并放在 SIA 的 `extnValue` 内层结构中(其外层 extnID 仍为 SIA:`id-pe-subjectInfoAccess`)。RFC 6487 §4.8.8;RFC 8182 §3.2。
|
||||
|
||||
```asn1
|
||||
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
|
||||
|
||||
id-ad-rpkiManifest OBJECT IDENTIFIER ::= { id-ad 10 } -- 1.3.6.1.5.5.7.48.10
|
||||
|
||||
id-ad-signedObject OBJECT IDENTIFIER ::= { id-ad 11 } -- 1.3.6.1.5.5.7.48.11
|
||||
|
||||
id-ad-rpkiNotify OBJECT IDENTIFIER ::= { id-ad 13 } -- 1.3.6.1.5.5.7.48.13
|
||||
```
|
||||
|
||||
### 3.2.8 CRLDistributionPoints(CRLDP;ASN.1;RFC 5280 §4.2.1.13)
|
||||
|
||||
```asn1
|
||||
id-ce-cRLDistributionPoints OBJECT IDENTIFIER ::= { id-ce 31 }
|
||||
|
||||
CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint
|
||||
|
||||
DistributionPoint ::= SEQUENCE {
|
||||
distributionPoint [0] DistributionPointName OPTIONAL,
|
||||
reasons [1] ReasonFlags OPTIONAL,
|
||||
cRLIssuer [2] GeneralNames OPTIONAL }
|
||||
|
||||
DistributionPointName ::= CHOICE {
|
||||
fullName [0] GeneralNames,
|
||||
nameRelativeToCRLIssuer [1] RelativeDistinguishedName }
|
||||
|
||||
ReasonFlags ::= BIT STRING {
|
||||
unused (0),
|
||||
keyCompromise (1),
|
||||
cACompromise (2),
|
||||
affiliationChanged (3),
|
||||
superseded (4),
|
||||
cessationOfOperation (5),
|
||||
certificateHold (6),
|
||||
privilegeWithdrawn (7),
|
||||
aACompromise (8) }
|
||||
```
|
||||
|
||||
### 3.2.9 Certificate Policies(ASN.1;RFC 5280 §4.2.1.4)
|
||||
|
||||
```asn1
|
||||
id-ce-certificatePolicies OBJECT IDENTIFIER ::= { id-ce 32 }
|
||||
|
||||
anyPolicy OBJECT IDENTIFIER ::= { id-ce-certificatePolicies 0 }
|
||||
|
||||
certificatePolicies ::= SEQUENCE SIZE (1..MAX) OF PolicyInformation
|
||||
|
||||
PolicyInformation ::= SEQUENCE {
|
||||
policyIdentifier CertPolicyId,
|
||||
policyQualifiers SEQUENCE SIZE (1..MAX) OF
|
||||
PolicyQualifierInfo OPTIONAL }
|
||||
|
||||
CertPolicyId ::= OBJECT IDENTIFIER
|
||||
|
||||
PolicyQualifierInfo ::= SEQUENCE {
|
||||
policyQualifierId PolicyQualifierId,
|
||||
qualifier ANY DEFINED BY policyQualifierId }
|
||||
|
||||
-- policyQualifierIds for Internet policy qualifiers
|
||||
|
||||
id-qt OBJECT IDENTIFIER ::= { id-pkix 2 }
|
||||
id-qt-cps OBJECT IDENTIFIER ::= { id-qt 1 }
|
||||
id-qt-unotice OBJECT IDENTIFIER ::= { id-qt 2 }
|
||||
|
||||
PolicyQualifierId ::= OBJECT IDENTIFIER ( id-qt-cps | id-qt-unotice )
|
||||
|
||||
Qualifier ::= CHOICE {
|
||||
cPSuri CPSuri,
|
||||
userNotice UserNotice }
|
||||
|
||||
CPSuri ::= IA5String
|
||||
```
|
||||
|
||||
### 3.2.10 RFC 3779 IP/AS 资源扩展(ASN.1;RFC 3779 §2.2.1-§2.2.3;RFC 3779 §3.2.1-§3.2.3)
|
||||
|
||||
> 说明:RFC 3779 给出两个扩展的 OID 与 ASN.1 语法;它们作为 X.509 v3 扩展出现在 `extensions` 中(外层 extnID 为下列 OID)。RPKI profile 进一步约束 criticality/SAFI/RDI 等,见 RFC 6487 §4.8.10-§4.8.11。
|
||||
|
||||
```asn1
|
||||
-- IP Address Delegation Extension
|
||||
id-pe-ipAddrBlocks OBJECT IDENTIFIER ::= { id-pe 7 }
|
||||
|
||||
IPAddrBlocks ::= SEQUENCE OF IPAddressFamily
|
||||
|
||||
IPAddressFamily ::= SEQUENCE { -- AFI & optional SAFI --
|
||||
addressFamily OCTET STRING (SIZE (2..3)),
|
||||
ipAddressChoice IPAddressChoice }
|
||||
|
||||
IPAddressChoice ::= CHOICE {
|
||||
inherit NULL, -- inherit from issuer --
|
||||
addressesOrRanges SEQUENCE OF IPAddressOrRange }
|
||||
|
||||
IPAddressOrRange ::= CHOICE {
|
||||
addressPrefix IPAddress,
|
||||
addressRange IPAddressRange }
|
||||
|
||||
IPAddressRange ::= SEQUENCE {
|
||||
min IPAddress,
|
||||
max IPAddress }
|
||||
|
||||
IPAddress ::= BIT STRING
|
||||
|
||||
-- Autonomous System Identifier Delegation Extension
|
||||
id-pe-autonomousSysIds OBJECT IDENTIFIER ::= { id-pe 8 }
|
||||
|
||||
ASIdentifiers ::= SEQUENCE {
|
||||
asnum [0] EXPLICIT ASIdentifierChoice OPTIONAL,
|
||||
rdi [1] EXPLICIT ASIdentifierChoice OPTIONAL}
|
||||
|
||||
ASIdentifierChoice ::= CHOICE {
|
||||
inherit NULL, -- inherit from issuer --
|
||||
asIdsOrRanges SEQUENCE OF ASIdOrRange }
|
||||
|
||||
ASIdOrRange ::= CHOICE {
|
||||
id ASId,
|
||||
range ASRange }
|
||||
|
||||
ASRange ::= SEQUENCE {
|
||||
min ASId,
|
||||
max ASId }
|
||||
|
||||
ASId ::= INTEGER
|
||||
```
|
||||
|
||||
### 3.2.11 其它 RPKI profile 相关扩展的 ASN.1 定义(RFC 5280 §4.2.1.1-§4.2.1.3;RFC 5280 §4.2.1.9;RFC 5280 §4.2.1.12)
|
||||
|
||||
> 说明:这些是 RPKI 资源证书 profile(RFC 6487 §4.8)所引用的通用 PKIX 扩展语法。RPKI 对其“必须/禁止/criticality/字段允许性”有额外限制(见本文件 3.3/3.4),但编码层的 ASN.1 类型来自 RFC 5280。
|
||||
|
||||
```asn1
|
||||
id-ce-authorityKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 35 }
|
||||
|
||||
AuthorityKeyIdentifier ::= SEQUENCE {
|
||||
keyIdentifier [0] KeyIdentifier OPTIONAL,
|
||||
authorityCertIssuer [1] GeneralNames OPTIONAL,
|
||||
authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL }
|
||||
|
||||
KeyIdentifier ::= OCTET STRING
|
||||
|
||||
id-ce-subjectKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 14 }
|
||||
|
||||
SubjectKeyIdentifier ::= KeyIdentifier
|
||||
|
||||
id-ce-keyUsage OBJECT IDENTIFIER ::= { id-ce 15 }
|
||||
|
||||
KeyUsage ::= BIT STRING {
|
||||
digitalSignature (0),
|
||||
nonRepudiation (1), -- recent editions of X.509 have
|
||||
-- renamed this bit to contentCommitment
|
||||
keyEncipherment (2),
|
||||
dataEncipherment (3),
|
||||
keyAgreement (4),
|
||||
keyCertSign (5),
|
||||
cRLSign (6),
|
||||
encipherOnly (7),
|
||||
decipherOnly (8) }
|
||||
|
||||
id-ce-basicConstraints OBJECT IDENTIFIER ::= { id-ce 19 }
|
||||
|
||||
BasicConstraints ::= SEQUENCE {
|
||||
cA BOOLEAN DEFAULT FALSE,
|
||||
pathLenConstraint INTEGER (0..MAX) OPTIONAL }
|
||||
|
||||
id-ce-extKeyUsage OBJECT IDENTIFIER ::= { id-ce 37 }
|
||||
|
||||
ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId
|
||||
|
||||
KeyPurposeId ::= OBJECT IDENTIFIER
|
||||
```
|
||||
|
||||
## 3.3 抽象数据模型(接口)
|
||||
|
||||
> 说明:本模型面向“语义化解析产物”。实现可保留 `raw_der` 作为可追溯入口。
|
||||
|
||||
### 3.3.1 顶层联合类型:`ResourceCertificate`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `raw_der` | `DerBytes` | 证书 DER | 原样保留(建议) | RFC 6487 §4 |
|
||||
| `tbs` | `RpkixTbsCertificate` | 证书语义字段(见下) | 仅允许 RFC 6487 允许的字段/扩展;其他字段 MUST NOT 出现 | RFC 6487 §4 |
|
||||
| `kind` | `enum { ca, ee }` | 语义分类 | 来自 BasicConstraints + 用途约束 | RFC 6487 §4.8.1;RFC 6487 §4.8.8 |
|
||||
|
||||
### 3.3.1.1 派生类型(用于字段类型标注)
|
||||
|
||||
为避免在其它对象文档里反复写“`ResourceCertificate` 且 `kind==...`”,这里定义两个派生/别名类型:
|
||||
|
||||
- `ResourceCaCertificate`:`ResourceCertificate` 且 `kind == ca`
|
||||
- `ResourceEeCertificate`:`ResourceCertificate` 且 `kind == ee`
|
||||
|
||||
这些派生类型不引入新字段,只是对 `ResourceCertificate.kind` 的约束化视图。
|
||||
|
||||
### 3.3.2 `RpkixTbsCertificate`(语义字段集合)
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `version` | `int` | X.509 版本 | MUST 为 v3(字段值为 2) | RFC 6487 §4.1 |
|
||||
| `serial_number` | `int` | 序列号 | 正整数;对每 CA 签发唯一 | RFC 6487 §4.2 |
|
||||
| `signature_algorithm` | `Oid` | 证书签名算法 | 必须为 `sha256WithRSAEncryption`(`1.2.840.113549.1.1.11`) | RFC 6487 §4.3;RFC 7935 §2(引用 RFC 4055) |
|
||||
| `issuer_dn` | `RpkixDistinguishedName` | 颁发者 DN | 必含 1 个 CommonName;可含 1 个 serialNumber;CN 必须 PrintableString | RFC 6487 §4.4 |
|
||||
| `subject_dn` | `RpkixDistinguishedName` | 主体 DN | 同 issuer 约束;且对同一 issuer 下“实体+公钥”唯一 | RFC 6487 §4.5 |
|
||||
| `validity_not_before` | `UtcTime` | 有效期起 | X.509 `Time`(UTCTime/GeneralizedTime)解析为 UTC 时间点 | RFC 6487 §4.6.1;RFC 5280 §4.1.2.5 |
|
||||
| `validity_not_after` | `UtcTime` | 有效期止 | X.509 `Time`(UTCTime/GeneralizedTime)解析为 UTC 时间点 | RFC 6487 §4.6.2;RFC 5280 §4.1.2.5 |
|
||||
| `subject_public_key_info` | `DerBytes` | SPKI DER | 算法 profile 指定 | RFC 6487 §4.7;RFC 7935 §3.1 |
|
||||
| `extensions` | `RpkixExtensions` | 扩展集合 | 见下表;criticality/存在性/内容受约束 | RFC 6487 §4.8 |
|
||||
|
||||
### 3.3.3 `RpkixDistinguishedName`(RPKI profile 下的 DN 语义)
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `common_name` | `string` | CommonName (CN) | MUST 存在且仅 1 个;类型为 PrintableString | RFC 6487 §4.4;RFC 6487 §4.5 |
|
||||
| `serial_number` | `optional[string]` | serialNumber | MAY 存在且仅 1 个 | RFC 6487 §4.4;RFC 6487 §4.5 |
|
||||
| `rfc4514` | `string` | DN 的 RFC4514 字符串表示 | 便于日志/索引(实现自选) | RFC 6487 §4.5(引用 RFC4514) |
|
||||
|
||||
### 3.3.4 `RpkixExtensions`(核心扩展语义)
|
||||
|
||||
> 表中 “存在性/criticality” 指 RPKI profile 下对该扩展的要求;实现应能区分 “字段缺失” 与 “字段存在但不符合约束”。
|
||||
|
||||
| 字段 | 类型 | 语义 | 存在性/criticality 与内容约束 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `basic_constraints` | `optional[BasicConstraints]` | CA 标志 | **extnID=`2.5.29.19`**;CA 证书:MUST present & critical;EE:MUST NOT present;pathLen MUST NOT present | RFC 6487 §4.8.1;RFC 5280 §4.2.1.9 |
|
||||
| `subject_key_identifier` | `bytes` | SKI | **extnID=`2.5.29.14`**;MUST present & non-critical;值为 subjectPublicKey 的 DER bit string 的 SHA-1 哈希 | RFC 6487 §4.8.2(引用 RFC 5280 §4.2.1.2) |
|
||||
| `authority_key_identifier` | `optional[AuthorityKeyIdentifier]` | AKI | **extnID=`2.5.29.35`**;自签名:MAY present 且可等于 SKI;非自签名:MUST present;authorityCertIssuer/authorityCertSerialNumber MUST NOT present;non-critical | RFC 6487 §4.8.3;RFC 5280 §4.2.1.1 |
|
||||
| `key_usage` | `KeyUsage` | KeyUsage | **extnID=`2.5.29.15`**;MUST present & critical;CA:仅 `keyCertSign` 与 `cRLSign` 为 TRUE;EE:仅 `digitalSignature` 为 TRUE | RFC 6487 §4.8.4;RFC 5280 §4.2.1.3 |
|
||||
| `extended_key_usage` | `optional[OidSet]` | EKU | **extnID=`2.5.29.37`**;CA:MUST NOT appear;用于验证 RPKI 对象的 EE:MUST NOT appear;若出现不得标 critical | RFC 6487 §4.8.5;RFC 5280 §4.2.1.12 |
|
||||
| `crl_distribution_points` | `optional[CrlDistributionPoints]` | CRLDP | **extnID=`2.5.29.31`**;自签名:MUST be omitted;非自签名:MUST present & non-critical;仅 1 个 DistributionPoint;fullName URI;必须包含至少 1 个 `rsync://` | RFC 6487 §4.8.6;RFC 5280 §4.2.1.13 |
|
||||
| `authority_info_access` | `optional[AuthorityInfoAccess]` | AIA | **extnID=`1.3.6.1.5.5.7.1.1`**;自签名:MUST be omitted;非自签名:MUST present & non-critical;必须含 accessMethod `id-ad-caIssuers`(**`1.3.6.1.5.5.7.48.2`**) 的 `rsync://` URI;可含同对象其它 URI | RFC 6487 §4.8.7;RFC 5280 §4.2.2.1 |
|
||||
| `subject_info_access_ca` | `optional[SubjectInfoAccessCa]` | SIA(CA) | **extnID=`1.3.6.1.5.5.7.1.11`**;CA:MUST present & non-critical;必须含 accessMethod `id-ad-caRepository`(**`1.3.6.1.5.5.7.48.5`**)(`rsync://` 目录 URI)与 `id-ad-rpkiManifest`(**`1.3.6.1.5.5.7.48.10`**)(`rsync://` 对象 URI);若 CA 使用 RRDP,还会包含 `id-ad-rpkiNotify`(**`1.3.6.1.5.5.7.48.13`**)(HTTPS Notification URI) | RFC 6487 §4.8.8.1;RFC 5280 §4.2.2.2;RFC 8182 §3.2 |
|
||||
| `subject_info_access_ee` | `optional[SubjectInfoAccessEe]` | SIA(EE) | **extnID=`1.3.6.1.5.5.7.1.11`**;EE:MUST present & non-critical;必须含 accessMethod `id-ad-signedObject`(**`1.3.6.1.5.5.7.48.11`**);URI **MUST include** `rsync://`;EE 的 SIA 不允许其它 AccessMethods | RFC 6487 §4.8.8.2;RFC 5280 §4.2.2.2 |
|
||||
| `certificate_policies` | `CertificatePolicies` | 证书策略 | **extnID=`2.5.29.32`**;MUST present & critical;恰好 1 个 policy;并允许 0 或 1 个 CPS qualifier(若存在其 id 必为 `id-qt-cps`(**`1.3.6.1.5.5.7.2.1`**)) | RFC 6487 §4.8.9;RFC 7318 §2;RFC 5280 §4.2.1.4 |
|
||||
| `ip_resources` | `optional[IpResourceSet]` | IP 资源扩展 | **extnID=`1.3.6.1.5.5.7.1.7`**;IP/AS 两者至少其一 MUST present;若 present MUST be critical;内容为 RFC 3779 语义;在公用互联网场景 SAFI MUST NOT 使用;且必须为非空或 inherit | RFC 6487 §4.8.10;RFC 3779 §2.2.1;RFC 3779 §2.2.2 |
|
||||
| `as_resources` | `optional[AsResourceSet]` | AS 资源扩展 | **extnID=`1.3.6.1.5.5.7.1.8`**;IP/AS 两者至少其一 MUST present;若 present MUST be critical;内容为 RFC 3779 语义;RDI MUST NOT 使用;且必须为非空或 inherit | RFC 6487 §4.8.11;RFC 3779 §3.2.1;RFC 3779 §3.2.2 |
|
||||
|
||||
### 3.3.5 结构化子类型(建议)
|
||||
|
||||
#### `BasicConstraints`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `ca` | `bool` | 是否 CA | 由 issuer 决定;在 CA 证书中该扩展必须存在 | RFC 6487 §4.8.1 |
|
||||
| `path_len_constraint` | `None` | pathLenConstraint | MUST NOT present(RPKI profile 不使用) | RFC 6487 §4.8.1 |
|
||||
|
||||
#### `AuthorityKeyIdentifier`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `key_identifier` | `bytes` | AKI.keyIdentifier | 使用 issuer 公钥的 SHA-1 哈希(按 RFC 5280 的定义) | RFC 6487 §4.8.3(引用 RFC 5280 §4.2.1.1) |
|
||||
| `authority_cert_issuer` | `None` | authorityCertIssuer | MUST NOT present | RFC 6487 §4.8.3 |
|
||||
| `authority_cert_serial_number` | `None` | authorityCertSerialNumber | MUST NOT present | RFC 6487 §4.8.3 |
|
||||
|
||||
#### `CrlDistributionPoints`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `distribution_point_uris` | `list[Uri]` | CRL 位置列表 | 仅 1 个 DistributionPoint;必须包含至少 1 个 `rsync://` URI 指向该 issuer 最新 CRL;可含其它 URI | RFC 6487 §4.8.6 |
|
||||
|
||||
#### `AuthorityInfoAccess`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `ca_issuers_uris` | `list[Uri]` | 上级 CA 证书位置 | accessMethod=`id-ad-caIssuers`(`1.3.6.1.5.5.7.48.2`);必含 `rsync://` URI;可含同对象其它 URI | RFC 6487 §4.8.7;RFC 5280 §4.2.2.1 |
|
||||
|
||||
#### `SubjectInfoAccessCa`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `ca_repository_uris` | `list[Uri]` | CA 发布点目录(repository publication point) | accessMethod=`id-ad-caRepository`(`1.3.6.1.5.5.7.48.5`);至少 1 个;必须包含 `rsync://`;也可包含其它机制(例如 `https://`)作为“同一目录”的替代访问方式;顺序表示 CA 偏好 | RFC 6487 §4.8.8.1;RFC 5280 §4.2.2.2 |
|
||||
| `rpki_manifest_uris` | `list[Uri]` | 当前 manifest 对象 URI | accessMethod=`id-ad-rpkiManifest`(`1.3.6.1.5.5.7.48.10`);至少 1 个;必须包含 `rsync://`;也可包含其它机制(例如 `https://`)作为“同一对象”的替代访问方式 | RFC 6487 §4.8.8.1;RFC 5280 §4.2.2.2 |
|
||||
| `rpki_notify_uris` | `optional[list[Uri]]` | RRDP Notification(Update Notification File)URI | accessMethod=`id-ad-rpkiNotify`(`1.3.6.1.5.5.7.48.13`);若存在则 accessLocation MUST 为 `https://` URI,指向 RRDP Notification 文件 | RFC 8182 §3.2;RFC 5280 §4.2.2.2 |
|
||||
|
||||
#### `SubjectInfoAccessEe`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `signed_object_uris` | `list[Uri]` | 被 EE 证书验证的签名对象位置 | accessMethod=`id-ad-signedObject`(`1.3.6.1.5.5.7.48.11`);必须包含 `rsync://`;其它 URI 可作为同对象替代机制;EE SIA 不允许其它 AccessMethods | RFC 6487 §4.8.8.2;RFC 5280 §4.2.2.2 |
|
||||
|
||||
#### `CertificatePolicies`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `policy_oid` | `Oid` | 唯一 policy OID | 恰好 1 个 policy;RPKI CP 分配的 OID 为 `id-cp-ipAddr-asNumber`(`1.3.6.1.5.5.7.14.2`) | RFC 6487 §4.8.9;RFC 6484 §1.2 |
|
||||
| `cps_uri` | `optional[Uri]` | CPS policy qualifier URI | MAY 存在且最多 1 个;若存在其 `policyQualifierId` 必为 `id-qt-cps`;对该 URI 不施加处理要求 | RFC 7318 §2;RFC 5280 §4.2.1.4 |
|
||||
|
||||
## 3.4 字段级约束清单(实现对照)
|
||||
|
||||
- 仅允许 RFC 6487 §4 指定的字段/扩展;未列出字段 MUST NOT 出现。RFC 6487 §4。
|
||||
- 证书版本必须为 v3。RFC 6487 §4.1。
|
||||
- CA/EE 在 BasicConstraints 与 SIA 的约束不同。RFC 6487 §4.8.1;RFC 6487 §4.8.8.1;RFC 6487 §4.8.8.2。
|
||||
- KeyUsage:CA 仅 `keyCertSign`/`cRLSign`;EE 仅 `digitalSignature`。RFC 6487 §4.8.4。
|
||||
- CRLDP/AIA:自签名必须省略;非自签名必须存在并包含 `rsync://`。RFC 6487 §4.8.6;RFC 6487 §4.8.7。
|
||||
- IP/AS 资源扩展:两者至少其一存在;若存在必须 critical;语义来自 RFC 3779;在公用互联网场景 SAFI 与 RDI 均不得使用。RFC 6487 §4.8.10;RFC 6487 §4.8.11;RFC 3779 §2.2.3;RFC 3779 §3.2.3。
|
||||
@ -102,5 +102,9 @@ FileAndHash ::= SEQUENCE {
|
||||
Manifest 使用“one-time-use EE certificate”进行签名验证,规范对该 EE 证书的使用方式给出约束:
|
||||
|
||||
- Manifest 相关 EE 证书应为 one-time-use(每次新 manifest 生成新密钥对/新 EE)。RFC 9286 §4(Section 4 前导段落)。
|
||||
- 用于签名/验证 manifest 的 EE 证书在 RFC 3779 的资源扩展中描述 INRs 时,**MUST** 使用 `inherit`(而不是显式列出资源集合)。RFC 9286 §5.1(生成步骤 2)。
|
||||
- 若证书包含 **IP Address Delegation Extension**(IP prefix delegation):内容必须为 `inherit`(不得显式列 prefix/range)。RFC 9286 §5.1;RFC 3779。
|
||||
- 若证书包含 **AS Identifier Delegation Extension**(ASN delegation):内容必须为 `inherit`(不得显式列 ASID/range)。RFC 9286 §5.1;RFC 3779。
|
||||
- 另外按资源证书 profile:资源证书 **MUST** 至少包含上述两类扩展之一(也可两者都有),且这些扩展 **MUST** 标记为 critical。RFC 6487 §2。
|
||||
- 用于验证 manifest 的 EE 证书 **MUST** 具有与 `thisUpdate..nextUpdate` 区间一致的有效期,以避免 CRL 无谓增长。RFC 9286 §4.2.1(manifestNumber 段落前的说明)。
|
||||
- 替换 manifest 时,CA 必须撤销旧 manifest 对应 EE 证书;且若新 manifest 早于旧 manifest 的 nextUpdate 发行,则 CA **MUST** 同时发行新 CRL 撤销旧 manifest EE。RFC 9286 §4.2.1(nextUpdate 段落末);RFC 9286 §5.1(生成步骤)。
|
||||
|
||||
100
specs/08_aspa.md
Normal file
100
specs/08_aspa.md
Normal file
@ -0,0 +1,100 @@
|
||||
# 08. ASPA(Autonomous System Provider Authorization)
|
||||
|
||||
## 8.1 对象定位
|
||||
|
||||
ASPA(Autonomous System Provider Authorization)是一种 RPKI Signed Object,用于由“客户 AS”(Customer AS, CAS)签名声明其上游“提供者 AS”(Provider AS, PAS)集合,以支持路由泄漏(route leak)检测/缓解。`draft-ietf-sidrops-aspa-profile-21`;`draft-ietf-sidrops-aspa-verification`。
|
||||
|
||||
ASPA 由 CMS 外壳 + ASPA eContent 组成:
|
||||
|
||||
- 外壳:RFC 6488(更新:RFC 9589)
|
||||
- eContent:`draft-ietf-sidrops-aspa-profile-21`
|
||||
|
||||
## 8.2 原始载体与编码
|
||||
|
||||
- 外壳:CMS SignedData DER(见 `05_signed_object_cms.md`)。RFC 6488 §2-§3;RFC 9589 §4。
|
||||
- eContentType:`id-ct-ASPA`,OID `1.2.840.113549.1.9.16.1.49`。`draft-ietf-sidrops-aspa-profile-21` §2。
|
||||
- eContent:DER 编码 ASN.1 `ASProviderAttestation`。`draft-ietf-sidrops-aspa-profile-21` §3。
|
||||
|
||||
### 8.2.1 eContentType 与 eContent 的 ASN.1 定义(`draft-ietf-sidrops-aspa-profile-21` §2-§3)
|
||||
|
||||
ASPA 是一种 RPKI signed object(CMS 外壳见 `05_signed_object_cms.md`)。其 `eContentType` 与 `eContent`(payload)的 ASN.1 定义见 `draft-ietf-sidrops-aspa-profile-21` §2-§3。
|
||||
|
||||
**eContentType(OID)**:`draft-ietf-sidrops-aspa-profile-21` §2。
|
||||
|
||||
```asn1
|
||||
id-ct-ASPA OBJECT IDENTIFIER ::=
|
||||
{ iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1)
|
||||
pkcs-9(9) id-smime(16) id-ct(1) aspa(49) }
|
||||
```
|
||||
|
||||
**eContent(ASPA ASN.1 模块)**:`draft-ietf-sidrops-aspa-profile-21` §3。
|
||||
|
||||
```asn1
|
||||
ASProviderAttestation ::= SEQUENCE {
|
||||
version [0] INTEGER DEFAULT 0,
|
||||
customerASID ASID,
|
||||
providers ProviderASSet }
|
||||
|
||||
ProviderASSet ::= SEQUENCE (SIZE(1..MAX)) OF ASID
|
||||
|
||||
ASID ::= INTEGER (0..4294967295)
|
||||
```
|
||||
|
||||
编码/解码要点(与上面 ASN.1 结构直接对应):
|
||||
|
||||
- `version`:规范要求 **MUST 为 1** 且 **MUST 显式编码**(不得依赖 DEFAULT 省略)。`draft-ietf-sidrops-aspa-profile-21` §3.1。
|
||||
- `customerASID`:客户 AS 号(CAS)。`draft-ietf-sidrops-aspa-profile-21` §3.2。
|
||||
- `providers`:授权的提供者 AS 集合(SPAS)。并对“自包含/排序/去重”施加额外约束(见下)。`draft-ietf-sidrops-aspa-profile-21` §3.3。
|
||||
|
||||
## 8.3 解析规则(eContent 语义层)
|
||||
|
||||
输入:`RpkiSignedObject`。
|
||||
|
||||
1) 解析 CMS 外壳,得到 `econtent_type` 与 `econtent_der`。RFC 6488 §3;RFC 9589 §4。
|
||||
2) 要求 `econtent_type == 1.2.840.113549.1.9.16.1.49`。`draft-ietf-sidrops-aspa-profile-21` §2。
|
||||
3) 将 `econtent_der` 以 DER 解析为 `ASProviderAttestation` ASN.1。`draft-ietf-sidrops-aspa-profile-21` §3。
|
||||
4) 将 `providers` 映射为语义字段 `provider_as_ids: list[int]`,并对其执行“约束检查/(可选)归一化”。`draft-ietf-sidrops-aspa-profile-21` §3.3。
|
||||
|
||||
## 8.4 抽象数据模型(接口)
|
||||
|
||||
### 8.4.1 `AspaObject`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | 规范引用 |
|
||||
|---|---|---|---|---|
|
||||
| `signed_object` | `RpkiSignedObject` | CMS 外壳 | 外壳约束见 RFC 6488/9589 | RFC 6488 §3;RFC 9589 §4 |
|
||||
| `econtent_type` | `Oid` | eContentType | 必须为 `1.2.840.113549.1.9.16.1.49` | `draft-ietf-sidrops-aspa-profile-21` §2 |
|
||||
| `aspa` | `AspaEContent` | eContent 语义对象 | 见下 | `draft-ietf-sidrops-aspa-profile-21` §3 |
|
||||
|
||||
### 8.4.2 `AspaEContent`(ASProviderAttestation)
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | 规范引用 |
|
||||
|---|---|---|---|---|
|
||||
| `version` | `int` | ASPA 版本 | MUST 为 `1` 且 MUST 显式编码(字段不可省略) | `draft-ietf-sidrops-aspa-profile-21` §3.1 |
|
||||
| `customer_as_id` | `int` | Customer ASID | 0..4294967295 | `draft-ietf-sidrops-aspa-profile-21` §3.2(ASID 定义) |
|
||||
| `provider_as_ids` | `list[int]` | Provider ASID 列表(SPAS) | 长度 `>= 1`;且满足“不得包含 customer、升序、去重” | `draft-ietf-sidrops-aspa-profile-21` §3.3 |
|
||||
|
||||
## 8.5 字段级约束清单(实现对照)
|
||||
|
||||
- eContentType 必须为 `id-ct-ASPA`(OID `1.2.840.113549.1.9.16.1.49`),且该 OID 必须同时出现在 eContentType 与 signedAttrs.content-type。`draft-ietf-sidrops-aspa-profile-21` §2(引用 RFC 6488)。
|
||||
- eContent 必须 DER 编码并符合 `ASProviderAttestation` ASN.1。`draft-ietf-sidrops-aspa-profile-21` §3。
|
||||
- `version` 必须为 1,且必须显式编码(缺失视为不合规)。`draft-ietf-sidrops-aspa-profile-21` §3.1。
|
||||
- `providers`(`provider_as_ids`)必须满足:
|
||||
- `customer_as_id` **MUST NOT** 出现在 `provider_as_ids` 中;
|
||||
- `provider_as_ids` 必须按数值 **升序排序**;
|
||||
- `provider_as_ids` 中每个 ASID 必须 **唯一**。`draft-ietf-sidrops-aspa-profile-21` §3.3。
|
||||
|
||||
## 8.6 与 EE 证书的语义约束(为后续验证准备)
|
||||
|
||||
ASPA 的外壳包含一个 EE 证书,用于验证 ASPA 签名;规范对该 EE 证书与 ASPA payload 的匹配关系提出要求:
|
||||
|
||||
- EE 证书必须包含 AS 资源扩展(Autonomous System Identifier Delegation Extension),且 `customer_as_id` 必须与该扩展中的 ASId 匹配。`draft-ietf-sidrops-aspa-profile-21` §4(引用 RFC 3779)。
|
||||
- EE 证书的 AS 资源扩展 **必须**:
|
||||
- 恰好包含 1 个 `id` 元素;
|
||||
- **不得**包含 `inherit` 元素;
|
||||
- **不得**包含 `range` 元素。`draft-ietf-sidrops-aspa-profile-21` §4(引用 RFC 3779 §3.2.3.3 / §3.2.3.6 / §3.2.3.7)。
|
||||
- EE 证书 **不得**包含 IP 资源扩展(IP Address Delegation Extension)。`draft-ietf-sidrops-aspa-profile-21` §4(引用 RFC 3779)。
|
||||
|
||||
## 8.7 实现建议(非规范约束)
|
||||
|
||||
`draft-ietf-sidrops-aspa-profile-21` 给出了一条 RP 侧建议:实现可对单个 `customer_as_id` 的 `provider_as_ids` 数量施加上界(例如 4,000~10,000),超过阈值时建议将该 `customer_as_id` 的所有 ASPA 视为无效并记录错误日志。`draft-ietf-sidrops-aspa-profile-21` §6。
|
||||
|
||||
87
specs/09_ghostbusters_gbr.md
Normal file
87
specs/09_ghostbusters_gbr.md
Normal file
@ -0,0 +1,87 @@
|
||||
# 09. Ghostbusters Record(GBR)
|
||||
|
||||
## 9.1 对象定位
|
||||
|
||||
Ghostbusters Record(GBR)是一个可选的 RPKI Signed Object,用于承载“联系人信息”(人类可读的联系渠道),以便在证书过期、CRL 失效、密钥轮换等事件中能够联系到维护者。RFC 6493 §1。
|
||||
|
||||
GBR 由 CMS 外壳 + vCard 载荷组成:
|
||||
|
||||
- 外壳:RFC 6488(更新:RFC 9589)
|
||||
- 载荷(payload):RFC 6493 定义的 vCard profile(基于 RFC 6350 vCard 4.0 的严格子集)。RFC 6493 §5。
|
||||
|
||||
## 9.2 原始载体与编码
|
||||
|
||||
- 外壳:CMS SignedData DER(见 `05_signed_object_cms.md`)。RFC 6493 §6(引用 RFC 6488)。
|
||||
- eContentType:`id-ct-rpkiGhostbusters`,OID `1.2.840.113549.1.9.16.1.35`。RFC 6493 §6;RFC 6493 §9.1。
|
||||
- eContent:一个 OCTET STRING,其 octets 是 vCard 文本(vCard 4.0,且受 RFC 6493 的 profile 约束)。RFC 6493 §5;RFC 6493 §6。
|
||||
|
||||
> 说明:与 ROA/MFT 这类“eContent 内部再 DER 解码为 ASN.1 结构”的对象不同,GBR 的 eContent 语义上就是“vCard 文本内容本身”(由 Signed Object Template 的 `eContent OCTET STRING` 承载)。RFC 6493 §6。
|
||||
|
||||
## 9.3 vCard profile(RFC 6493 §5)
|
||||
|
||||
GBR 的 vCard payload 是 RFC 6350 vCard 4.0 的严格子集,仅允许以下属性(properties):
|
||||
|
||||
- `BEGIN`:必须为第一行,值必须为 `BEGIN:VCARD`。RFC 6493 §5。
|
||||
- `VERSION`:必须为第二行,值必须为 `VERSION:4.0`。RFC 6493 §5(引用 RFC 6350 §3.7.9)。
|
||||
- `FN`:联系人姓名或角色名。RFC 6493 §5(引用 RFC 6350 §6.2.1)。
|
||||
- `ORG`:组织信息(可选)。RFC 6493 §5(引用 RFC 6350 §6.6.4)。
|
||||
- `ADR`:邮寄地址(可选)。RFC 6493 §5(引用 RFC 6350 §6.3)。
|
||||
- `TEL`:语音/传真电话(可选)。RFC 6493 §5(引用 RFC 6350 §6.4.1)。
|
||||
- `EMAIL`:邮箱(可选)。RFC 6493 §5(引用 RFC 6350 §6.4.2)。
|
||||
- `END`:必须为最后一行,值必须为 `END:VCARD`。RFC 6493 §5。
|
||||
|
||||
额外约束:
|
||||
|
||||
- `BEGIN`、`VERSION`、`FN`、`END` 必须包含。RFC 6493 §5。
|
||||
- 为保证可用性,`ADR`/`TEL`/`EMAIL` 三者中至少一个必须包含。RFC 6493 §5。
|
||||
- 除上述属性外,**其他属性 MUST NOT** 出现。RFC 6493 §5。
|
||||
|
||||
## 9.4 解析规则(payload 语义层)
|
||||
|
||||
输入:`RpkiSignedObject`。
|
||||
|
||||
1) 解析 CMS 外壳,得到 `econtent_type` 与 `econtent_bytes`。RFC 6488 §3;RFC 9589 §4。
|
||||
2) 要求 `econtent_type == 1.2.840.113549.1.9.16.1.35`。RFC 6493 §6。
|
||||
3) 将 `econtent_bytes` 解析为 vCard 文本,并按 RFC 6493 §5 的 profile 校验(属性集合、必选项、行首/行尾约束)。RFC 6493 §5;RFC 6493 §7。
|
||||
4) 通过校验后,将允许属性映射为 `GhostbustersVCard` 语义对象(见下)。
|
||||
|
||||
## 9.5 抽象数据模型(接口)
|
||||
|
||||
### 9.5.1 `GhostbustersObject`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `signed_object` | `RpkiSignedObject` | CMS 外壳 | 外壳约束见 RFC 6488/9589 | RFC 6493 §6;RFC 6488 §3;RFC 9589 §4 |
|
||||
| `econtent_type` | `Oid` | eContentType | 必须为 `1.2.840.113549.1.9.16.1.35` | RFC 6493 §6 |
|
||||
| `vcard` | `GhostbustersVCard` | vCard 语义对象 | 由 eContent 文本解析并校验 profile | RFC 6493 §5;RFC 6493 §7 |
|
||||
|
||||
### 9.5.2 `GhostbustersVCard`(vCard 4.0 profile)
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `raw_text` | `string` | 原始 vCard 文本 | 由 eContent bytes 解码得到;用于保留原文/诊断 | RFC 6493 §5-§7 |
|
||||
| `fn` | `string` | 联系人姓名/角色名 | `FN` 必须存在 | RFC 6493 §5 |
|
||||
| `org` | `optional[string]` | 组织 | `ORG` 可选 | RFC 6493 §5 |
|
||||
| `adrs` | `list[string]` | 邮寄地址(原始 ADR value) | 允许 0..N;至少满足“ADR/TEL/EMAIL 至少一项存在” | RFC 6493 §5 |
|
||||
| `tels` | `list[Uri]` | 电话 URI(从 TEL 提取的 `tel:` 等 URI) | 允许 0..N;至少满足“ADR/TEL/EMAIL 至少一项存在” | RFC 6493 §5 |
|
||||
| `emails` | `list[string]` | 邮箱地址 | 允许 0..N;至少满足“ADR/TEL/EMAIL 至少一项存在” | RFC 6493 §5 |
|
||||
|
||||
> 说明:RFC 6493 并未要求 RP 必须完整解析 vCard 参数(例如 `TYPE=WORK`、`VALUE=uri`),因此此处将 `ADR`/`TEL`/`EMAIL` 建模为“足以联络”的最小语义集合;实现可在保留 `raw_text` 的同时按 RFC 6350 扩展解析能力。
|
||||
|
||||
## 9.6 字段级约束清单(实现对照)
|
||||
|
||||
- eContentType 必须为 `id-ct-rpkiGhostbusters`(OID `1.2.840.113549.1.9.16.1.35`),且该 OID 必须同时出现在 eContentType 与 signedAttrs.content-type。RFC 6493 §6(引用 RFC 6488)。
|
||||
- eContent 必须是 vCard 4.0 文本,且必须满足 RFC 6493 §5 的 profile:
|
||||
- 第一行 `BEGIN:VCARD`;
|
||||
- 第二行 `VERSION:4.0`;
|
||||
- 末行 `END:VCARD`;
|
||||
- 必须包含 `FN`;
|
||||
- `ADR`/`TEL`/`EMAIL` 至少一个存在;
|
||||
- 除允许集合外不得出现其他属性。RFC 6493 §5;RFC 6493 §7。
|
||||
|
||||
## 9.7 与 EE 证书的语义约束(为后续验证准备)
|
||||
|
||||
GBR 使用 CMS 外壳内的 EE 证书验证签名。RFC 6493 对该 EE 证书提出一条资源扩展约束:
|
||||
|
||||
- 用于验证 GBR 的 EE 证书在描述 Internet Number Resources 时,必须使用 `inherit`,而不是显式资源集合。RFC 6493 §6(引用 RFC 3779)。
|
||||
|
||||
239
src/data_model/aspa.rs
Normal file
239
src/data_model/aspa.rs
Normal file
@ -0,0 +1,239 @@
|
||||
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};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AspaObject {
|
||||
pub signed_object: RpkiSignedObject,
|
||||
pub econtent_type: String,
|
||||
pub aspa: AspaEContent,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AspaEContent {
|
||||
pub version: u32,
|
||||
pub customer_as_id: u32,
|
||||
pub provider_as_ids: Vec<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AspaDecodeError {
|
||||
#[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} (draft-ietf-sidrops-aspa-profile-21 §2)")]
|
||||
InvalidEContentType(String),
|
||||
|
||||
#[error("ASPA parse error: {0} (draft-ietf-sidrops-aspa-profile-21 §3; DER)")]
|
||||
Parse(String),
|
||||
|
||||
#[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 (draft-ietf-sidrops-aspa-profile-21 §3)")]
|
||||
InvalidAttestationSequence,
|
||||
|
||||
#[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} (draft-ietf-sidrops-aspa-profile-21 §3.2)")]
|
||||
CustomerAsIdOutOfRange(u64),
|
||||
|
||||
#[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} (draft-ietf-sidrops-aspa-profile-21 §3.3)")]
|
||||
ProviderAsIdOutOfRange(u64),
|
||||
|
||||
#[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 (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 (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2)")]
|
||||
EeAsResourcesMissing,
|
||||
|
||||
#[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 (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.6-§3.2.3.7)")]
|
||||
EeAsResourcesRangePresent,
|
||||
|
||||
#[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 (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §2.2)")]
|
||||
EeIpResourcesPresent,
|
||||
}
|
||||
|
||||
impl AspaObject {
|
||||
pub fn decode_der(der: &[u8]) -> Result<Self, AspaDecodeError> {
|
||||
let signed_object = RpkiSignedObject::decode_der(der)?;
|
||||
Self::from_signed_object(signed_object)
|
||||
}
|
||||
|
||||
pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result<Self, AspaDecodeError> {
|
||||
let econtent_type = signed_object
|
||||
.signed_data
|
||||
.encap_content_info
|
||||
.econtent_type
|
||||
.clone();
|
||||
if econtent_type != OID_CT_ASPA {
|
||||
return Err(AspaDecodeError::InvalidEContentType(econtent_type));
|
||||
}
|
||||
|
||||
let aspa = AspaEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?;
|
||||
Ok(Self {
|
||||
aspa,
|
||||
signed_object,
|
||||
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 {
|
||||
/// Decode the DER-encoded ASProviderAttestation defined in draft-ietf-sidrops-aspa-profile-21 §3.
|
||||
pub fn decode_der(der: &[u8]) -> Result<Self, AspaDecodeError> {
|
||||
let (rem, obj) = parse_der(der).map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(AspaDecodeError::TrailingBytes(rem.len()));
|
||||
}
|
||||
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||
if seq.len() != 3 {
|
||||
return Err(AspaDecodeError::InvalidAttestationSequence);
|
||||
}
|
||||
|
||||
// version [0] EXPLICIT INTEGER MUST be present and MUST be 1.
|
||||
let v_obj = &seq[0];
|
||||
if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) {
|
||||
return Err(AspaDecodeError::VersionMustBeExplicitOne);
|
||||
}
|
||||
let inner_der = v_obj
|
||||
.as_slice()
|
||||
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||
let (rem, inner) =
|
||||
parse_der(inner_der).map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(AspaDecodeError::Parse(
|
||||
"trailing bytes inside ASProviderAttestation.version".into(),
|
||||
));
|
||||
}
|
||||
let v = inner
|
||||
.as_u64()
|
||||
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||
if v != 1 {
|
||||
return Err(AspaDecodeError::VersionMustBeExplicitOne);
|
||||
}
|
||||
|
||||
let customer_u64 = seq[1]
|
||||
.as_u64()
|
||||
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||
if customer_u64 > u32::MAX as u64 {
|
||||
return Err(AspaDecodeError::CustomerAsIdOutOfRange(customer_u64));
|
||||
}
|
||||
let customer_as_id = customer_u64 as u32;
|
||||
|
||||
let providers = parse_providers(&seq[2], customer_as_id)?;
|
||||
|
||||
Ok(Self {
|
||||
version: 1,
|
||||
customer_as_id,
|
||||
provider_as_ids: providers,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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_cert(
|
||||
&self,
|
||||
ee: &ResourceCertificate,
|
||||
) -> Result<(), AspaValidateError> {
|
||||
if ee.tbs.extensions.ip_resources.is_some() {
|
||||
return Err(AspaValidateError::EeIpResourcesPresent);
|
||||
}
|
||||
|
||||
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 asn.has_any_range() {
|
||||
return Err(AspaValidateError::EeAsResourcesRangePresent);
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_providers(obj: &DerObject<'_>, customer_as_id: u32) -> Result<Vec<u32>, AspaDecodeError> {
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||
if seq.is_empty() {
|
||||
return Err(AspaDecodeError::EmptyProviders);
|
||||
}
|
||||
|
||||
let mut out: Vec<u32> = Vec::with_capacity(seq.len());
|
||||
let mut prev: Option<u32> = None;
|
||||
for item in seq {
|
||||
let v = item
|
||||
.as_u64()
|
||||
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||
if v > u32::MAX as u64 {
|
||||
return Err(AspaDecodeError::ProviderAsIdOutOfRange(v));
|
||||
}
|
||||
let asn = v as u32;
|
||||
if asn == customer_as_id {
|
||||
return Err(AspaDecodeError::ProvidersContainCustomer(customer_as_id));
|
||||
}
|
||||
if let Some(p) = prev {
|
||||
if asn <= p {
|
||||
return Err(AspaDecodeError::ProvidersNotStrictlyIncreasing);
|
||||
}
|
||||
}
|
||||
prev = Some(asn);
|
||||
out.push(asn);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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<u32>),
|
||||
|
||||
#[error("CRL signatureAlgorithm must be sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0}")]
|
||||
#[error("CRL signatureAlgorithm must be sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0} (RFC 6487 §5; RFC 7935 §2)")]
|
||||
InvalidSignatureAlgorithm(String),
|
||||
|
||||
#[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),
|
||||
}
|
||||
|
||||
|
||||
@ -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<Self, ManifestDecodeError> {
|
||||
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 {
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
pub mod common;
|
||||
pub mod crl;
|
||||
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;
|
||||
|
||||
@ -9,12 +9,34 @@ 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";
|
||||
|
||||
pub const OID_CT_RPKI_MANIFEST: &str = "1.2.840.113549.1.9.16.1.26";
|
||||
pub const OID_CT_ROUTE_ORIGIN_AUTHZ: &str = "1.2.840.113549.1.9.16.1.24";
|
||||
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";
|
||||
|
||||
820
src/data_model/rc.rs
Normal file
820
src/data_model/rc.rs
Normal file
@ -0,0 +1,820 @@
|
||||
use der_parser::ber::{BerObjectContent, Class};
|
||||
use der_parser::der::{parse_der, DerObject, Tag};
|
||||
use der_parser::num_bigint::BigUint;
|
||||
use time::OffsetDateTime;
|
||||
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<u8>,
|
||||
pub tbs: RpkixTbsCertificate,
|
||||
pub kind: ResourceCertKind,
|
||||
}
|
||||
|
||||
pub type ResourceCaCertificate = ResourceCertificate;
|
||||
pub type ResourceEeCertificate = ResourceCertificate;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
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<u8>,
|
||||
pub extensions: RcExtensions,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RcExtensions {
|
||||
pub basic_constraints_ca: bool,
|
||||
pub subject_key_identifier: Option<Vec<u8>>,
|
||||
pub subject_info_access: Option<SubjectInfoAccess>,
|
||||
pub certificate_policies_oid: Option<String>,
|
||||
|
||||
pub ip_resources: Option<IpResourceSet>,
|
||||
pub as_resources: Option<AsResourceSet>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum SubjectInfoAccess {
|
||||
Ca(SubjectInfoAccessCa),
|
||||
Ee(SubjectInfoAccessEe),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SubjectInfoAccessCa {
|
||||
pub access_descriptions: Vec<AccessDescription>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SubjectInfoAccessEe {
|
||||
pub signed_object_uris: Vec<Url>,
|
||||
/// The full list of access descriptions as carried in the SIA extension.
|
||||
pub access_descriptions: Vec<AccessDescription>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AccessDescription {
|
||||
pub access_method_oid: String,
|
||||
pub access_location: Url,
|
||||
}
|
||||
|
||||
#[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 IpResourceSet {
|
||||
pub families: Vec<IpAddressFamily>,
|
||||
}
|
||||
|
||||
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<Self, IpResourceSetDecodeError> {
|
||||
parse_ip_addr_blocks(extn_value).map_err(|_| IpResourceSetDecodeError::InvalidEncoding)
|
||||
}
|
||||
|
||||
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 IpResourceSetDecodeError {
|
||||
#[error("invalid ipAddrBlocks encoding (RFC 3779 §2.2.3; RFC 6487 §4.8.10)")]
|
||||
InvalidEncoding,
|
||||
}
|
||||
|
||||
#[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<IpAddressOrRange>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum IpAddressOrRange {
|
||||
Prefix(IpPrefix),
|
||||
Range(IpAddressRange),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct IpAddressRange {
|
||||
pub min: Vec<u8>,
|
||||
pub max: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct IpPrefix {
|
||||
pub afi: Afi,
|
||||
pub prefix_len: u16,
|
||||
pub addr: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AsResourceSet {
|
||||
pub asnum: Option<AsIdentifierChoice>,
|
||||
pub rdi: Option<AsIdentifierChoice>,
|
||||
}
|
||||
|
||||
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<Self, AsResourceSetDecodeError> {
|
||||
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<u32> {
|
||||
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<AsIdOrRange>),
|
||||
}
|
||||
|
||||
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<Self, ResourceCertificateError> {
|
||||
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<RcExtensions, ResourceCertificateError> {
|
||||
let mut basic_constraints_ca: Option<bool> = None;
|
||||
let mut ski: Option<Vec<u8>> = None;
|
||||
let mut sia: Option<SubjectInfoAccess> = None;
|
||||
let mut cert_policies_oid: Option<String> = None;
|
||||
|
||||
let mut ip_resources: Option<IpResourceSet> = None;
|
||||
let mut as_resources: Option<AsResourceSet> = 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<SubjectInfoAccess, ResourceCertificateError> {
|
||||
let mut all = Vec::with_capacity(access.len());
|
||||
let mut signed_object_uris: Vec<Url> = 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<IpResourceSet, ()> {
|
||||
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<IpAddressOrRange, ()> {
|
||||
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<IpPrefix, ()> {
|
||||
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<Vec<u8>, ()> {
|
||||
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<AsResourceSet, ()> {
|
||||
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<AsIdentifierChoice> = None;
|
||||
let mut rdi: Option<AsIdentifierChoice> = 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<DerObject<'a>, ()> {
|
||||
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<AsIdentifierChoice, ()> {
|
||||
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<AsIdOrRange, ()> {
|
||||
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<u8> {
|
||||
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)
|
||||
}
|
||||
447
src/data_model/roa.rs
Normal file
447
src/data_model/roa.rs
Normal file
@ -0,0 +1,447 @@
|
||||
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};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RoaObject {
|
||||
pub signed_object: RpkiSignedObject,
|
||||
pub econtent_type: String,
|
||||
pub roa: RoaEContent,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RoaEContent {
|
||||
pub version: u32,
|
||||
pub as_id: u32,
|
||||
pub ip_addr_blocks: Vec<RoaIpAddressFamily>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RoaDecodeError {
|
||||
#[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} (RFC 9582 §3)")]
|
||||
InvalidEContentType(String),
|
||||
|
||||
#[error("ROA parse error: {0} (RFC 9582 §4; DER)")]
|
||||
Parse(String),
|
||||
|
||||
#[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} (RFC 9582 §4)")]
|
||||
InvalidAttestationSequenceLen(usize),
|
||||
|
||||
#[error("ROA version must be 0, got {0} (RFC 9582 §4.1)")]
|
||||
InvalidVersion(u64),
|
||||
|
||||
#[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} (RFC 9582 §4; RFC 9582 §4.3.1)")]
|
||||
InvalidIpAddrBlocksLen(usize),
|
||||
|
||||
#[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 (RFC 9582 §4.3.1)")]
|
||||
InvalidAddressFamily,
|
||||
|
||||
#[error("ROA addressFamily AFI not supported: {0:02X?} (RFC 9582 §4.3.1)")]
|
||||
UnsupportedAfi(Vec<u8>),
|
||||
|
||||
#[error("ROA contains duplicate AFI {0:?} (RFC 9582 §4.3.1)")]
|
||||
DuplicateAfi(RoaAfi),
|
||||
|
||||
#[error("ROAAddresses must have at least one entry (RFC 9582 §4.3.2)")]
|
||||
EmptyAddressList,
|
||||
|
||||
#[error("ROAIPAddress must be a SEQUENCE of 1..2 elements (RFC 9582 §4.3.2)")]
|
||||
InvalidRoaIpAddress,
|
||||
|
||||
#[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 (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:?} (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} (RFC 9582 §4.3.2.2)")]
|
||||
InvalidMaxLength {
|
||||
afi: RoaAfi,
|
||||
prefix_len: u16,
|
||||
max_len: u16,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
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} (RFC 9582 §5; RFC 3779 §2.3)")]
|
||||
PrefixNotInEeResources {
|
||||
afi: RoaAfi,
|
||||
addr: Vec<u8>,
|
||||
prefix_len: u16,
|
||||
},
|
||||
}
|
||||
|
||||
impl RoaObject {
|
||||
pub fn decode_der(der: &[u8]) -> Result<Self, RoaDecodeError> {
|
||||
let signed_object = RpkiSignedObject::decode_der(der)?;
|
||||
Self::from_signed_object(signed_object)
|
||||
}
|
||||
|
||||
pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result<Self, RoaDecodeError> {
|
||||
let econtent_type = signed_object
|
||||
.signed_data
|
||||
.encap_content_info
|
||||
.econtent_type
|
||||
.clone();
|
||||
if econtent_type != OID_CT_ROUTE_ORIGIN_AUTHZ {
|
||||
return Err(RoaDecodeError::InvalidEContentType(econtent_type));
|
||||
}
|
||||
|
||||
let roa = RoaEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?;
|
||||
Ok(Self {
|
||||
roa,
|
||||
signed_object,
|
||||
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)]
|
||||
pub enum RoaAfi {
|
||||
Ipv4,
|
||||
Ipv6,
|
||||
}
|
||||
|
||||
impl RoaAfi {
|
||||
fn ub(self) -> u16 {
|
||||
match self {
|
||||
RoaAfi::Ipv4 => 32,
|
||||
RoaAfi::Ipv6 => 128,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RoaIpAddressFamily {
|
||||
pub afi: RoaAfi,
|
||||
pub addresses: Vec<RoaIpAddress>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct RoaIpAddress {
|
||||
pub prefix: IpPrefix,
|
||||
pub max_length: Option<u16>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct IpPrefix {
|
||||
pub afi: RoaAfi,
|
||||
/// Prefix length in bits.
|
||||
pub prefix_len: u16,
|
||||
/// Network order address bytes (IPv4 4 bytes / IPv6 16 bytes), with host bits cleared.
|
||||
pub addr: Vec<u8>,
|
||||
}
|
||||
|
||||
impl RoaEContent {
|
||||
/// Decode the DER-encoded RouteOriginAttestation defined in RFC 9582 §4.
|
||||
pub fn decode_der(der: &[u8]) -> Result<Self, RoaDecodeError> {
|
||||
let (rem, obj) = parse_der(der).map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(RoaDecodeError::TrailingBytes(rem.len()));
|
||||
}
|
||||
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
if seq.len() != 2 && seq.len() != 3 {
|
||||
return Err(RoaDecodeError::InvalidAttestationSequenceLen(seq.len()));
|
||||
}
|
||||
|
||||
let mut idx = 0;
|
||||
let mut version: u32 = 0;
|
||||
if seq.len() == 3 {
|
||||
let v_obj = &seq[0];
|
||||
if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) {
|
||||
return Err(RoaDecodeError::Parse(
|
||||
"RouteOriginAttestation.version must be [0] EXPLICIT INTEGER".into(),
|
||||
));
|
||||
}
|
||||
let inner_der = v_obj
|
||||
.as_slice()
|
||||
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
let (rem, inner) =
|
||||
parse_der(inner_der).map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(RoaDecodeError::Parse(
|
||||
"trailing bytes inside RouteOriginAttestation.version".into(),
|
||||
));
|
||||
}
|
||||
let v = inner
|
||||
.as_u64()
|
||||
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
if v != 0 {
|
||||
return Err(RoaDecodeError::InvalidVersion(v));
|
||||
}
|
||||
version = 0;
|
||||
idx = 1;
|
||||
}
|
||||
|
||||
let as_id_u64 = seq[idx]
|
||||
.as_u64()
|
||||
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
if as_id_u64 > u32::MAX as u64 {
|
||||
return Err(RoaDecodeError::AsIdOutOfRange(as_id_u64));
|
||||
}
|
||||
let as_id = as_id_u64 as u32;
|
||||
idx += 1;
|
||||
|
||||
let ip_addr_blocks = parse_ip_addr_blocks(&seq[idx])?;
|
||||
|
||||
let mut out = Self {
|
||||
version,
|
||||
as_id,
|
||||
ip_addr_blocks,
|
||||
};
|
||||
out.canonicalize();
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn canonicalize(&mut self) {
|
||||
self.ip_addr_blocks.sort_by_key(|f| f.afi);
|
||||
for fam in &mut self.ip_addr_blocks {
|
||||
fam.addresses.sort();
|
||||
fam.addresses.dedup();
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate ROA payload against the embedded EE resource certificate (RFC 9582 §5).
|
||||
///
|
||||
/// 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);
|
||||
}
|
||||
|
||||
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 {
|
||||
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(),
|
||||
prefix_len: entry.prefix.prefix_len,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn roa_prefix_to_rc(p: &IpPrefix) -> RcIpPrefix {
|
||||
let afi = match p.afi {
|
||||
RoaAfi::Ipv4 => RcAfi::Ipv4,
|
||||
RoaAfi::Ipv6 => RcAfi::Ipv6,
|
||||
};
|
||||
RcIpPrefix {
|
||||
afi,
|
||||
prefix_len: p.prefix_len,
|
||||
addr: p.addr.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_ip_addr_blocks(obj: &DerObject<'_>) -> Result<Vec<RoaIpAddressFamily>, RoaDecodeError> {
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
if seq.is_empty() || seq.len() > 2 {
|
||||
return Err(RoaDecodeError::InvalidIpAddrBlocksLen(seq.len()));
|
||||
}
|
||||
|
||||
let mut out: Vec<RoaIpAddressFamily> = Vec::new();
|
||||
for fam in seq {
|
||||
let family = parse_ip_address_family(fam)?;
|
||||
if out.iter().any(|f| f.afi == family.afi) {
|
||||
return Err(RoaDecodeError::DuplicateAfi(family.afi));
|
||||
}
|
||||
out.push(family);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn parse_ip_address_family(obj: &DerObject<'_>) -> Result<RoaIpAddressFamily, RoaDecodeError> {
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
if seq.len() != 2 {
|
||||
return Err(RoaDecodeError::InvalidIpAddressFamily);
|
||||
}
|
||||
|
||||
let afi = parse_afi(&seq[0])?;
|
||||
let addresses = parse_roa_addresses(afi, &seq[1])?;
|
||||
if addresses.is_empty() {
|
||||
return Err(RoaDecodeError::EmptyAddressList);
|
||||
}
|
||||
|
||||
Ok(RoaIpAddressFamily { afi, addresses })
|
||||
}
|
||||
|
||||
fn parse_afi(obj: &DerObject<'_>) -> Result<RoaAfi, RoaDecodeError> {
|
||||
let bytes = obj
|
||||
.as_slice()
|
||||
.map_err(|_e| RoaDecodeError::InvalidAddressFamily)?;
|
||||
if bytes.len() != 2 {
|
||||
return Err(RoaDecodeError::InvalidAddressFamily);
|
||||
}
|
||||
match bytes {
|
||||
[0x00, 0x01] => Ok(RoaAfi::Ipv4),
|
||||
[0x00, 0x02] => Ok(RoaAfi::Ipv6),
|
||||
_ => Err(RoaDecodeError::UnsupportedAfi(bytes.to_vec())),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_roa_addresses(afi: RoaAfi, obj: &DerObject<'_>) -> Result<Vec<RoaIpAddress>, RoaDecodeError> {
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
let mut out = Vec::with_capacity(seq.len());
|
||||
for entry in seq {
|
||||
out.push(parse_roa_ip_address(afi, entry)?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn parse_roa_ip_address(afi: RoaAfi, obj: &DerObject<'_>) -> Result<RoaIpAddress, RoaDecodeError> {
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
if seq.is_empty() || seq.len() > 2 {
|
||||
return Err(RoaDecodeError::InvalidRoaIpAddress);
|
||||
}
|
||||
|
||||
let prefix = parse_prefix_bits(afi, &seq[0])?;
|
||||
let max_length = match seq.get(1) {
|
||||
None => None,
|
||||
Some(m) => {
|
||||
let v = m
|
||||
.as_u64()
|
||||
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
let max_len: u16 = v
|
||||
.try_into()
|
||||
.map_err(|_e| RoaDecodeError::InvalidMaxLength {
|
||||
afi,
|
||||
prefix_len: prefix.prefix_len,
|
||||
max_len: u16::MAX,
|
||||
})?;
|
||||
Some(max_len)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(max_len) = max_length {
|
||||
let ub = afi.ub();
|
||||
if max_len > ub || max_len < prefix.prefix_len {
|
||||
return Err(RoaDecodeError::InvalidMaxLength {
|
||||
afi,
|
||||
prefix_len: prefix.prefix_len,
|
||||
max_len,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(RoaIpAddress { prefix, max_length })
|
||||
}
|
||||
|
||||
fn parse_prefix_bits(afi: RoaAfi, obj: &DerObject<'_>) -> Result<IpPrefix, RoaDecodeError> {
|
||||
let (unused_bits, bytes) = match &obj.content {
|
||||
BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()),
|
||||
_ => return Err(RoaDecodeError::InvalidPrefixBitString),
|
||||
};
|
||||
|
||||
if unused_bits > 7 {
|
||||
return Err(RoaDecodeError::InvalidPrefixUnusedBits);
|
||||
}
|
||||
if bytes.is_empty() {
|
||||
if unused_bits != 0 {
|
||||
return Err(RoaDecodeError::InvalidPrefixUnusedBits);
|
||||
}
|
||||
} else if unused_bits != 0 {
|
||||
let mask = (1u8 << unused_bits) - 1;
|
||||
if (bytes[bytes.len() - 1] & mask) != 0 {
|
||||
return Err(RoaDecodeError::InvalidPrefixUnusedBits);
|
||||
}
|
||||
}
|
||||
|
||||
let prefix_len = (bytes.len() * 8)
|
||||
.checked_sub(unused_bits as usize)
|
||||
.ok_or(RoaDecodeError::InvalidPrefixUnusedBits)? as u16;
|
||||
if prefix_len > afi.ub() {
|
||||
return Err(RoaDecodeError::PrefixLenOutOfRange { afi, prefix_len });
|
||||
}
|
||||
|
||||
let addr = canonicalize_prefix_addr(afi, prefix_len, &bytes);
|
||||
Ok(IpPrefix {
|
||||
afi,
|
||||
prefix_len,
|
||||
addr,
|
||||
})
|
||||
}
|
||||
|
||||
fn canonicalize_prefix_addr(afi: RoaAfi, prefix_len: u16, bytes: &[u8]) -> Vec<u8> {
|
||||
let full_len = match afi {
|
||||
RoaAfi::Ipv4 => 4,
|
||||
RoaAfi::Ipv6 => 16,
|
||||
};
|
||||
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 {
|
||||
let mask: u8 = 0xFF << (8 - rem);
|
||||
if last_prefix_byte < addr.len() {
|
||||
addr[last_prefix_byte] &= mask;
|
||||
}
|
||||
}
|
||||
addr
|
||||
}
|
||||
@ -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<u8>,
|
||||
pub spki_der: Vec<u8>,
|
||||
pub sia_signed_object_uris: Vec<String>,
|
||||
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<Vec<ResourceEeC
|
||||
}
|
||||
|
||||
fn parse_ee_certificate(der: &[u8]) -> Result<ResourceEeCertificate, SignedObjectDecodeError> {
|
||||
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<String> = match sia {
|
||||
SubjectInfoAccess::Ee(ee) => ee
|
||||
.signed_object_uris
|
||||
.iter()
|
||||
.map(|u| u.as_str().to_string())
|
||||
.collect(),
|
||||
SubjectInfoAccess::Ca(_ca) => Vec::new(),
|
||||
};
|
||||
if signed_object_uris.is_empty() {
|
||||
return Err(SignedObjectDecodeError::EeCertificateMissingSignedObjectSia);
|
||||
}
|
||||
if !signed_object_uris.iter().any(|u| u.starts_with("rsync://")) {
|
||||
return Err(SignedObjectDecodeError::EeCertificateSignedObjectSiaNoRsync);
|
||||
}
|
||||
|
||||
Ok(ResourceEeCertificate {
|
||||
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<Vec<String>, SignedObjectDecodeError> {
|
||||
let mut sia: Option<x509_parser::extensions::SubjectInfoAccess<'_>> = None;
|
||||
for ext in cert.extensions() {
|
||||
if ext.oid.to_id_string() != OID_SUBJECT_INFO_ACCESS {
|
||||
continue;
|
||||
}
|
||||
match ext.parsed_extension() {
|
||||
ParsedExtension::SubjectInfoAccess(s) => {
|
||||
if sia.is_some() {
|
||||
return Err(SignedObjectDecodeError::Parse(
|
||||
"duplicate SubjectInfoAccess extensions".into(),
|
||||
));
|
||||
}
|
||||
sia = Some(s.clone());
|
||||
}
|
||||
ParsedExtension::ParseError { error } => {
|
||||
return Err(SignedObjectDecodeError::Parse(error.to_string()));
|
||||
}
|
||||
_ => {
|
||||
return Err(SignedObjectDecodeError::Parse(
|
||||
"SubjectInfoAccess extension parse failed".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sia = sia.ok_or(SignedObjectDecodeError::EeCertificateMissingSia)?;
|
||||
let mut uris: Vec<String> = Vec::new();
|
||||
for ad in sia.iter() {
|
||||
if ad.access_method.to_id_string() != OID_AD_SIGNED_OBJECT {
|
||||
continue;
|
||||
}
|
||||
match &ad.access_location {
|
||||
GeneralName::URI(u) => uris.push((*u).to_string()),
|
||||
_ => return Err(SignedObjectDecodeError::EeCertificateSignedObjectSiaNotUri),
|
||||
}
|
||||
}
|
||||
if uris.is_empty() {
|
||||
return Err(SignedObjectDecodeError::EeCertificateMissingSignedObjectSia);
|
||||
}
|
||||
if !uris.iter().any(|u| u.starts_with("rsync://")) {
|
||||
return Err(SignedObjectDecodeError::EeCertificateSignedObjectSiaNoRsync);
|
||||
}
|
||||
Ok(uris)
|
||||
}
|
||||
|
||||
fn parse_signer_info(obj: &DerObject<'_>) -> Result<SignerInfoProfiled, SignedObjectDecodeError> {
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
|
||||
188
src/data_model/ta.rs
Normal file
188
src/data_model/ta.rs
Normal file
@ -0,0 +1,188 @@
|
||||
use url::Url;
|
||||
use x509_parser::prelude::{FromDer, X509Certificate};
|
||||
|
||||
use crate::data_model::oid::OID_CP_IPADDR_ASNUMBER;
|
||||
use crate::data_model::rc::{AsIdentifierChoice, IpAddressChoice, ResourceCertKind, ResourceCertificate};
|
||||
use crate::data_model::tal::Tal;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TaCertificate {
|
||||
pub raw_der: Vec<u8>,
|
||||
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<Self, TaCertificateError> {
|
||||
let rc_ca = ResourceCertificate::from_der(der).map_err(|e| TaCertificateError::Parse(e.to_string()))?;
|
||||
if rc_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(TaCertificateError::NotCa);
|
||||
}
|
||||
|
||||
// Strong self-signed check: issuer==subject AND signature verifies with its own SPKI.
|
||||
let (rem, cert) =
|
||||
X509Certificate::from_der(der).map_err(|e| TaCertificateError::Parse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(TaCertificateError::TrailingBytes(rem.len()));
|
||||
}
|
||||
if cert.issuer().to_string() != cert.subject().to_string() {
|
||||
return Err(TaCertificateError::NotSelfSignedIssuerSubject);
|
||||
}
|
||||
cert.verify_signature(None)
|
||||
.map_err(|e| TaCertificateError::InvalidSelfSignature(e.to_string()))?;
|
||||
|
||||
Self::validate_rc_constraints(&rc_ca)?;
|
||||
|
||||
Ok(Self {
|
||||
raw_der: der.to_vec(),
|
||||
rc_ca,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn spki_der(&self) -> &[u8] {
|
||||
&self.rc_ca.tbs.subject_public_key_info
|
||||
}
|
||||
|
||||
/// Validate TA-specific semantic constraints on a parsed Resource Certificate.
|
||||
///
|
||||
/// Note: this does not verify the X.509 signature; it is intended for higher-level logic and
|
||||
/// for unit tests that exercise individual constraint branches.
|
||||
pub fn validate_rc_constraints(rc_ca: &ResourceCertificate) -> Result<(), TaCertificateError> {
|
||||
if rc_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(TaCertificateError::NotCa);
|
||||
}
|
||||
|
||||
if rc_ca.tbs.extensions.certificate_policies_oid.as_deref() != Some(OID_CP_IPADDR_ASNUMBER) {
|
||||
return Err(TaCertificateError::MissingOrInvalidCertificatePolicies);
|
||||
}
|
||||
if rc_ca.tbs.extensions.subject_key_identifier.is_none() {
|
||||
return Err(TaCertificateError::MissingSubjectKeyIdentifier);
|
||||
}
|
||||
|
||||
let ip = rc_ca.tbs.extensions.ip_resources.as_ref();
|
||||
let asn = rc_ca.tbs.extensions.as_resources.as_ref();
|
||||
if ip.is_none() && asn.is_none() {
|
||||
return Err(TaCertificateError::ResourcesMissing);
|
||||
}
|
||||
|
||||
let mut has_any_resource = false;
|
||||
|
||||
if let Some(ip) = ip {
|
||||
if ip.has_any_inherit() {
|
||||
return Err(TaCertificateError::IpResourcesInherit);
|
||||
}
|
||||
for fam in &ip.families {
|
||||
match &fam.choice {
|
||||
IpAddressChoice::Inherit => return Err(TaCertificateError::IpResourcesInherit),
|
||||
IpAddressChoice::AddressesOrRanges(items) => {
|
||||
if !items.is_empty() {
|
||||
has_any_resource = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(asn) = asn {
|
||||
if matches!(asn.asnum, Some(AsIdentifierChoice::Inherit))
|
||||
|| matches!(asn.rdi, Some(AsIdentifierChoice::Inherit))
|
||||
{
|
||||
return Err(TaCertificateError::AsResourcesInherit);
|
||||
}
|
||||
if let Some(AsIdentifierChoice::AsIdsOrRanges(items)) = asn.asnum.as_ref() {
|
||||
if !items.is_empty() {
|
||||
has_any_resource = true;
|
||||
}
|
||||
}
|
||||
if let Some(AsIdentifierChoice::AsIdsOrRanges(items)) = asn.rdi.as_ref() {
|
||||
if !items.is_empty() {
|
||||
has_any_resource = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !has_any_resource {
|
||||
return Err(TaCertificateError::ResourcesEmpty);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TrustAnchor {
|
||||
pub tal: Tal,
|
||||
pub ta_certificate: TaCertificate,
|
||||
pub resolved_ta_uri: Option<Url>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TrustAnchorError {
|
||||
#[error("TA certificate error: {0} (RFC 8630 §2.3)")]
|
||||
TaCertificate(#[from] TaCertificateError),
|
||||
|
||||
#[error("resolved TA URI not listed in TAL: {0} (RFC 8630 §2.2-§2.3)")]
|
||||
ResolvedUriNotInTal(String),
|
||||
|
||||
#[error("TAL SPKI does not match TA certificate SubjectPublicKeyInfo (RFC 8630 §2.3; RFC 5280 §4.1.2.7)")]
|
||||
TalSpkiMismatch,
|
||||
}
|
||||
|
||||
impl TrustAnchor {
|
||||
/// Bind a TAL and a downloaded TA certificate.
|
||||
///
|
||||
/// This does not download anything; it only validates the binding rules from RFC 8630 §2.3.
|
||||
pub fn bind(tal: Tal, ta_der: &[u8], resolved_uri: Option<&Url>) -> Result<Self, TrustAnchorError> {
|
||||
if let Some(u) = resolved_uri {
|
||||
if !tal.ta_uris.iter().any(|x| x == u) {
|
||||
return Err(TrustAnchorError::ResolvedUriNotInTal(u.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
let ta_certificate = TaCertificate::from_der(ta_der)?;
|
||||
if tal.subject_public_key_info_der != ta_certificate.spki_der() {
|
||||
return Err(TrustAnchorError::TalSpkiMismatch);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
tal,
|
||||
ta_certificate,
|
||||
resolved_ta_uri: resolved_uri.cloned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
134
src/data_model/tal.rs
Normal file
134
src/data_model/tal.rs
Normal file
@ -0,0 +1,134 @@
|
||||
use base64::Engine;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Tal {
|
||||
pub raw: Vec<u8>,
|
||||
pub comments: Vec<String>,
|
||||
pub ta_uris: Vec<Url>,
|
||||
pub subject_public_key_info_der: Vec<u8>,
|
||||
}
|
||||
|
||||
#[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<Self, TalDecodeError> {
|
||||
let raw = input.to_vec();
|
||||
let text = std::str::from_utf8(input).map_err(|_| TalDecodeError::InvalidUtf8)?;
|
||||
|
||||
let lines: Vec<&str> = text
|
||||
.split('\n')
|
||||
.map(|l| l.strip_suffix('\r').unwrap_or(l))
|
||||
.collect();
|
||||
|
||||
let mut idx = 0usize;
|
||||
|
||||
// 1) Leading comments.
|
||||
let mut comments: Vec<String> = Vec::new();
|
||||
while idx < lines.len() && lines[idx].starts_with('#') {
|
||||
comments.push(lines[idx][1..].to_string());
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
// 2) URI list (one or more non-empty lines).
|
||||
let mut ta_uris: Vec<Url> = Vec::new();
|
||||
while idx < lines.len() {
|
||||
let line = lines[idx].trim();
|
||||
if line.is_empty() {
|
||||
break;
|
||||
}
|
||||
if line.starts_with('#') {
|
||||
return Err(TalDecodeError::CommentAfterHeader);
|
||||
}
|
||||
let url = match Url::parse(line) {
|
||||
Ok(u) => u,
|
||||
Err(_) => {
|
||||
if !ta_uris.is_empty() {
|
||||
return Err(TalDecodeError::MissingSeparatorEmptyLine);
|
||||
}
|
||||
return Err(TalDecodeError::InvalidUri(line.to_string()));
|
||||
}
|
||||
};
|
||||
match url.scheme() {
|
||||
"rsync" | "https" => {}
|
||||
s => return Err(TalDecodeError::UnsupportedUriScheme(s.to_string())),
|
||||
}
|
||||
if url.path().ends_with('/') {
|
||||
return Err(TalDecodeError::UriIsDirectory(line.to_string()));
|
||||
}
|
||||
if url.path_segments().and_then(|mut s| s.next_back()).unwrap_or("").is_empty() {
|
||||
return Err(TalDecodeError::UriIsDirectory(line.to_string()));
|
||||
}
|
||||
ta_uris.push(url);
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
if ta_uris.is_empty() {
|
||||
return Err(TalDecodeError::MissingTaUris);
|
||||
}
|
||||
|
||||
// 3) Empty line separator (must exist).
|
||||
if idx >= lines.len() || !lines[idx].trim().is_empty() {
|
||||
return Err(TalDecodeError::MissingSeparatorEmptyLine);
|
||||
}
|
||||
idx += 1;
|
||||
|
||||
// 4) Base64(SPKI DER) remainder; allow line wrapping.
|
||||
let mut b64 = String::new();
|
||||
while idx < lines.len() {
|
||||
let line = lines[idx].trim();
|
||||
if !line.is_empty() {
|
||||
b64.push_str(line);
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
if b64.is_empty() {
|
||||
return Err(TalDecodeError::MissingSpki);
|
||||
}
|
||||
|
||||
let spki_der = base64::engine::general_purpose::STANDARD
|
||||
.decode(b64.as_bytes())
|
||||
.map_err(|_| TalDecodeError::SpkiBase64Decode)?;
|
||||
if spki_der.is_empty() {
|
||||
return Err(TalDecodeError::SpkiDerEmpty);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
raw,
|
||||
comments,
|
||||
ta_uris,
|
||||
subject_public_key_info_der: spki_der,
|
||||
})
|
||||
}
|
||||
}
|
||||
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/E0AAy4jgV_BtY7GQELCxALV6Q5U.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/E0AAy4jgV_BtY7GQELCxALV6Q5U.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/IdgNYO8v_dEbdoPuHFTpsjA3l0U.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/IdgNYO8v_dEbdoPuHFTpsjA3l0U.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/UDn6-ZD0WTj18D8nHih3D--ZO_s.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/UDn6-ZD0WTj18D8nHih3D--ZO_s.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/Vvx1bC8uAflbQPltsuM_idhPJzQ.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/Vvx1bC8uAflbQPltsuM_idhPJzQ.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ZW5EIqvxKWSSAOsBmoFfKxIjbpI.cer
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ZW5EIqvxKWSSAOsBmoFfKxIjbpI.cer
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.mft
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.mft
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/fDvN8JDIeg8h7b5wWCFK_F7J_SY.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/fDvN8JDIeg8h7b5wWCFK_F7J_SY.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/fykdvBDNLvaFDLOeKcvquSaaNlM.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/fykdvBDNLvaFDLOeKcvquSaaNlM.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/lAN537Pqyt3IsX0kCwc4qs0Cejk.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/lAN537Pqyt3IsX0kCwc4qs0Cejk.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/laI1_vEYtlNzj-K0SXR_yGpd1TA.gbr
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/laI1_vEYtlNzj-K0SXR_yGpd1TA.gbr
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ovsCA/IOUcOeBGM_Tb4dwfvswY4bnNZYY.crl
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ovsCA/IOUcOeBGM_Tb4dwfvswY4bnNZYY.crl
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ovsCA/IOUcOeBGM_Tb4dwfvswY4bnNZYY.mft
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ovsCA/IOUcOeBGM_Tb4dwfvswY4bnNZYY.mft
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/xF8fZFbI8NRA4qk_N5CLpw6Nmj8.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/xF8fZFbI8NRA4qk_N5CLpw6Nmj8.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/y9dGCqfvXd6vKRjYx3GJikg6pSw.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/y9dGCqfvXd6vKRjYx3GJikg6pSw.roa
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nlrssf/cdFOuyVdwFjUv6WlHJP3P4MKuI8.crl
vendored
Normal file
BIN
tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nlrssf/cdFOuyVdwFjUv6WlHJP3P4MKuI8.crl
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nlrssf/cdFOuyVdwFjUv6WlHJP3P4MKuI8.mft
vendored
Normal file
BIN
tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nlrssf/cdFOuyVdwFjUv6WlHJP3P4MKuI8.mft
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/ta/afrinic-ta.cer
vendored
Normal file
BIN
tests/fixtures/ta/afrinic-ta.cer
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/ta/apnic-ta.cer
vendored
Normal file
BIN
tests/fixtures/ta/apnic-ta.cer
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/ta/arin-ta.cer
vendored
Normal file
BIN
tests/fixtures/ta/arin-ta.cer
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/ta/lacnic-ta.cer
vendored
Normal file
BIN
tests/fixtures/ta/lacnic-ta.cer
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/ta/ripe-ncc-ta.cer
vendored
Normal file
BIN
tests/fixtures/ta/ripe-ncc-ta.cer
vendored
Normal file
Binary file not shown.
10
tests/fixtures/tal/afrinic.tal
vendored
Normal file
10
tests/fixtures/tal/afrinic.tal
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
rsync://rpki.afrinic.net/repository/AfriNIC.cer
|
||||
https://rpki.afrinic.net/repository/AfriNIC.cer
|
||||
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxsAqAhWIO+ON2Ef9oRDM
|
||||
pKxv+AfmSLIdLWJtjrvUyDxJPBjgR+kVrOHUeTaujygFUp49tuN5H2C1rUuQavTH
|
||||
vve6xNF5fU3OkTcqEzMOZy+ctkbde2SRMVdvbO22+TH9gNhKDc9l7Vu01qU4LeJH
|
||||
k3X0f5uu5346YrGAOSv6AaYBXVgXxa0s9ZvgqFpim50pReQe/WI3QwFKNgpPzfQL
|
||||
6Y7fDPYdYaVOXPXSKtx7P4s4KLA/ZWmRL/bobw/i2fFviAGhDrjqqqum+/9w1hEl
|
||||
L/vqihVnV18saKTnLvkItA/Bf5i11Yhw2K7qv573YWxyuqCknO/iYLTR1DToBZcZ
|
||||
UQIDAQAB
|
||||
10
tests/fixtures/tal/apnic-rfc7730-https.tal
vendored
Normal file
10
tests/fixtures/tal/apnic-rfc7730-https.tal
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer
|
||||
rsync://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer
|
||||
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9RWSL61YAAYumEiU8z8
|
||||
qH2ETVIL01ilxZlzIL9JYSORMN5Cmtf8V2JblIealSqgOTGjvSjEsiV73s67zYQI
|
||||
7C/iSOb96uf3/s86NqbxDiFQGN8qG7RNcdgVuUlAidl8WxvLNI8VhqbAB5uSg/Mr
|
||||
LeSOvXRja041VptAxIhcGzDMvlAJRwkrYK/Mo8P4E2rSQgwqCgae0ebY1CsJ3Cjf
|
||||
i67C1nw7oXqJJovvXJ4apGmEv8az23OLC6Ki54Ul/E6xk227BFttqFV3YMtKx42H
|
||||
cCcDVZZy01n7JjzvO8ccaXmHIgR7utnqhBRNNq5Xc5ZhbkrUsNtiJmrZzVlgU6Ou
|
||||
0wIDAQAB
|
||||
19
tests/fixtures/tal/arin.tal
vendored
Normal file
19
tests/fixtures/tal/arin.tal
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
# THIS TRUST ANCHOR LOCATOR IS PROVIDED BY THE AMERICAN REGISTRY FOR
|
||||
# INTERNET NUMBERS (ARIN) "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
# IN NO EVENT SHALL ARIN BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS PUBLIC KEY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
rsync://rpki.arin.net/repository/arin-rpki-ta.cer
|
||||
https://rrdp.arin.net/arin-rpki-ta.cer
|
||||
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3lZPjbHvMRV5sDDqfLc/685th5FnreHMJjg8
|
||||
pEZUbG8Y8TQxSBsDebbsDpl3Ov3Cj1WtdrJ3CIfQODCPrrJdOBSrMATeUbPC+JlNf2SRP3UB+VJFgtTj
|
||||
0RN8cEYIuhBW5t6AxQbHhdNQH+A1F/OJdw0q9da2U29Lx85nfFxvnC1EpK9CbLJS4m37+RlpNbT1cba+
|
||||
b+loXpx0Qcb1C4UpJCGDy7uNf5w6/+l7RpATAHqqsX4qCtwwDYlbHzp2xk9owF3mkCxzl0HwncO+sEHH
|
||||
eaL3OjtwdIGrRGeHi2Mpt+mvWHhtQqVG+51MHTyg+nIjWFKKGx1Q9+KDx4wJStwveQIDAQAB
|
||||
4
tests/fixtures/tal/lacnic.tal
vendored
Normal file
4
tests/fixtures/tal/lacnic.tal
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
https://rrdp.lacnic.net/ta/rta-lacnic-rpki.cer
|
||||
rsync://repository.lacnic.net/rpki/lacnic/rta-lacnic-rpki.cer
|
||||
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqZEzhYK0+PtDOPfub/KRc3MeWx3neXx4/wbnJWGbNAtbYqXg3uU5J4HFzPgk/VIppgSKAhlO0H60DRP48by9gr5/yDHu2KXhOmnMg46sYsUIpfgtBS9+VtrqWziJfb+pkGtuOWeTnj6zBmBNZKK+5AlMCW1WPhrylIcB+XSZx8tk9GS/3SMQ+YfMVwwAyYjsex14Uzto4GjONALE5oh1M3+glRQduD6vzSwOD+WahMbc9vCOTED+2McLHRKgNaQf0YJ9a1jG9oJIvDkKXEqdfqDRktwyoD74cV57bW3tBAexB7GglITbInyQAsmdngtfg2LUMrcROHHP86QPZINjDQIDAQAB
|
||||
10
tests/fixtures/tal/ripe-ncc.tal
vendored
Normal file
10
tests/fixtures/tal/ripe-ncc.tal
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
https://rpki.ripe.net/ta/ripe-ncc-ta.cer
|
||||
rsync://rpki.ripe.net/ta/ripe-ncc-ta.cer
|
||||
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0URYSGqUz2myBsOzeW1j
|
||||
Q6NsxNvlLMyhWknvnl8NiBCs/T/S2XuNKQNZ+wBZxIgPPV2pFBFeQAvoH/WK83Hw
|
||||
A26V2siwm/MY2nKZ+Olw+wlpzlZ1p3Ipj2eNcKrmit8BwBC8xImzuCGaV0jkRB0G
|
||||
Z0hoH6Ml03umLprRsn6v0xOP0+l6Qc1ZHMFVFb385IQ7FQQTcVIxrdeMsoyJq9eM
|
||||
kE6DoclHhF/NlSllXubASQ9KUWqJ0+Ot3QCXr4LXECMfkpkVR2TZT+v5v658bHVs
|
||||
6ZxRD1b6Uk1uQKAyHUbn/tXvP8lrjAibGzVsXDT2L0x4Edx+QdixPgOji3gBMyL2
|
||||
VwIDAQAB
|
||||
25
tests/test_aspa_decode.rs
Normal file
25
tests/test_aspa_decode.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use rpki::data_model::aspa::{AspaDecodeError, AspaObject};
|
||||
|
||||
#[test]
|
||||
fn decode_aspa_fixture_smoke() {
|
||||
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");
|
||||
assert_eq!(aspa.econtent_type, rpki::data_model::oid::OID_CT_ASPA);
|
||||
assert_eq!(aspa.aspa.version, 1);
|
||||
assert_ne!(aspa.aspa.customer_as_id, 0);
|
||||
assert!(!aspa.aspa.provider_as_ids.is_empty());
|
||||
println!("{aspa:#?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rejects_non_aspa_econtent_type() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa",
|
||||
)
|
||||
.expect("read ROA fixture");
|
||||
let err = AspaObject::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::InvalidEContentType(_)));
|
||||
}
|
||||
111
tests/test_aspa_econtent_decode_errors.rs
Normal file
111
tests/test_aspa_econtent_decode_errors.rs
Normal file
@ -0,0 +1,111 @@
|
||||
use rpki::data_model::aspa::{AspaDecodeError, AspaEContent};
|
||||
|
||||
fn len_bytes(len: usize) -> Vec<u8> {
|
||||
if len < 128 {
|
||||
vec![len as u8]
|
||||
} else {
|
||||
let mut tmp = Vec::new();
|
||||
let mut n = len;
|
||||
while n > 0 {
|
||||
tmp.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
tmp.reverse();
|
||||
let mut out = vec![0x80 | (tmp.len() as u8)];
|
||||
out.extend(tmp);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
fn tlv(tag: u8, content: &[u8]) -> Vec<u8> {
|
||||
let mut out = vec![tag];
|
||||
out.extend(len_bytes(content.len()));
|
||||
out.extend_from_slice(content);
|
||||
out
|
||||
}
|
||||
|
||||
fn der_integer_u64(v: u64) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
let mut n = v;
|
||||
if n == 0 {
|
||||
bytes.push(0);
|
||||
} else {
|
||||
while n > 0 {
|
||||
bytes.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
bytes.reverse();
|
||||
if bytes[0] & 0x80 != 0 {
|
||||
bytes.insert(0, 0);
|
||||
}
|
||||
}
|
||||
tlv(0x02, &bytes)
|
||||
}
|
||||
|
||||
fn der_sequence(children: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
let mut content = Vec::new();
|
||||
for c in children {
|
||||
content.extend(c);
|
||||
}
|
||||
tlv(0x30, &content)
|
||||
}
|
||||
|
||||
fn cs_explicit(tag_no: u8, inner_der: Vec<u8>) -> Vec<u8> {
|
||||
tlv(0xA0 | (tag_no & 0x1F), &inner_der)
|
||||
}
|
||||
|
||||
fn aspa_attestation_explicit_version(version: u64, customer: u64, providers: Vec<u64>) -> Vec<u8> {
|
||||
let providers_der = der_sequence(providers.into_iter().map(der_integer_u64).collect());
|
||||
der_sequence(vec![
|
||||
cs_explicit(0, der_integer_u64(version)),
|
||||
der_integer_u64(customer),
|
||||
providers_der,
|
||||
])
|
||||
}
|
||||
|
||||
fn aspa_attestation_missing_version(customer: u64, providers: Vec<u64>) -> Vec<u8> {
|
||||
let providers_der = der_sequence(providers.into_iter().map(der_integer_u64).collect());
|
||||
der_sequence(vec![der_integer_u64(customer), providers_der])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_must_be_explicit_and_equal_to_one() {
|
||||
let der = aspa_attestation_missing_version(64496, vec![64497]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::InvalidAttestationSequence));
|
||||
|
||||
let der = aspa_attestation_explicit_version(0, 64496, vec![64497]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::VersionMustBeExplicitOne));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn customer_asid_out_of_range_is_rejected() {
|
||||
let der = aspa_attestation_explicit_version(1, (u32::MAX as u64) + 1, vec![64497]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::CustomerAsIdOutOfRange(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn providers_constraints_are_enforced() {
|
||||
// empty providers
|
||||
let der = aspa_attestation_explicit_version(1, 64496, vec![]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::EmptyProviders));
|
||||
|
||||
// not strictly increasing (duplicate)
|
||||
let der = aspa_attestation_explicit_version(1, 64496, vec![64497, 64497]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::ProvidersNotStrictlyIncreasing));
|
||||
|
||||
// not strictly increasing (descending)
|
||||
let der = aspa_attestation_explicit_version(1, 64496, vec![64500, 64497]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::ProvidersNotStrictlyIncreasing));
|
||||
|
||||
// contains customer
|
||||
let der = aspa_attestation_explicit_version(1, 64496, vec![64496, 64497]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::ProvidersContainCustomer(64496)));
|
||||
}
|
||||
|
||||
14
tests/test_aspa_embedded_ee_cert.rs
Normal file
14
tests/test_aspa_embedded_ee_cert.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use rpki::data_model::aspa::AspaObject;
|
||||
|
||||
#[test]
|
||||
fn aspa_embedded_ee_cert_resources_validate() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa",
|
||||
)
|
||||
.expect("read ASPA fixture");
|
||||
|
||||
let aspa = AspaObject::decode_der(&der).expect("decode aspa");
|
||||
aspa.validate_embedded_ee_cert()
|
||||
.expect("aspa EE cert resources must validate");
|
||||
}
|
||||
|
||||
150
tests/test_aspa_validate_ee_resources.rs
Normal file
150
tests/test_aspa_validate_ee_resources.rs
Normal file
@ -0,0 +1,150 @@
|
||||
use der_parser::num_bigint::BigUint;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use rpki::data_model::aspa::{AspaEContent, AspaValidateError};
|
||||
use rpki::data_model::rc::{
|
||||
AsIdentifierChoice, AsIdOrRange, AsResourceSet, IpResourceSet, RcExtensions, ResourceCertKind,
|
||||
ResourceCertificate, RpkixTbsCertificate, SubjectInfoAccess,
|
||||
};
|
||||
|
||||
fn dummy_ee(ip_resources: Option<IpResourceSet>, as_resources: Option<AsResourceSet>) -> ResourceCertificate {
|
||||
ResourceCertificate {
|
||||
raw_der: vec![],
|
||||
tbs: RpkixTbsCertificate {
|
||||
version: 2,
|
||||
serial_number: BigUint::from(1u8),
|
||||
signature_algorithm: "1.2.840.113549.1.1.11".to_string(),
|
||||
issuer_dn: "CN=issuer".to_string(),
|
||||
subject_dn: "CN=subject".to_string(),
|
||||
validity_not_before: OffsetDateTime::UNIX_EPOCH,
|
||||
validity_not_after: OffsetDateTime::UNIX_EPOCH,
|
||||
subject_public_key_info: vec![],
|
||||
extensions: RcExtensions {
|
||||
basic_constraints_ca: false,
|
||||
subject_key_identifier: Some(vec![0x01]),
|
||||
subject_info_access: Some(SubjectInfoAccess::Ca(
|
||||
rpki::data_model::rc::SubjectInfoAccessCa {
|
||||
access_descriptions: vec![],
|
||||
},
|
||||
)),
|
||||
certificate_policies_oid: None,
|
||||
ip_resources,
|
||||
as_resources,
|
||||
},
|
||||
},
|
||||
kind: ResourceCertKind::Ee,
|
||||
}
|
||||
}
|
||||
|
||||
fn test_aspa() -> AspaEContent {
|
||||
AspaEContent {
|
||||
version: 1,
|
||||
customer_as_id: 64496,
|
||||
provider_as_ids: vec![64497],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_accepts_when_customer_matches_ee_asid() {
|
||||
let aspa = test_aspa();
|
||||
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 = 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 = 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 = 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 = 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 = 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));
|
||||
}
|
||||
|
||||
14
tests/test_aspa_verify.rs
Normal file
14
tests/test_aspa_verify.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use rpki::data_model::aspa::AspaObject;
|
||||
|
||||
#[test]
|
||||
fn verify_aspa_cms_signature_with_embedded_ee_cert() {
|
||||
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.signed_object
|
||||
.verify_signature()
|
||||
.expect("ASPA CMS signature should verify with embedded EE cert");
|
||||
}
|
||||
|
||||
14
tests/test_manifest_embedded_ee_cert.rs
Normal file
14
tests/test_manifest_embedded_ee_cert.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use rpki::data_model::manifest::ManifestObject;
|
||||
|
||||
#[test]
|
||||
fn manifest_embedded_ee_cert_resources_validate() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
)
|
||||
.expect("read MFT fixture");
|
||||
|
||||
let mft = ManifestObject::decode_der(&der).expect("decode manifest");
|
||||
mft.validate_embedded_ee_cert()
|
||||
.expect("manifest EE cert resources must validate");
|
||||
}
|
||||
|
||||
498
tests/test_model_print_real_fixtures.rs
Normal file
498
tests/test_model_print_real_fixtures.rs
Normal file
@ -0,0 +1,498 @@
|
||||
use rpki::data_model::aspa::{AspaEContent, AspaObject};
|
||||
use rpki::data_model::crl::{CrlExtensions, RevokedCert, RpkixCrl};
|
||||
use rpki::data_model::manifest::{FileAndHash, ManifestEContent, ManifestObject};
|
||||
use rpki::data_model::rc::{
|
||||
AsResourceSet, IpResourceSet, RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate,
|
||||
SubjectInfoAccess,
|
||||
};
|
||||
use rpki::data_model::roa::{RoaEContent, RoaIpAddressFamily, RoaObject};
|
||||
use rpki::data_model::signed_object::{
|
||||
EncapsulatedContentInfo, ResourceEeCertificate, RpkiSignedObject, SignedAttrsProfiled, SignedDataProfiled,
|
||||
SignerInfoProfiled,
|
||||
};
|
||||
use rpki::data_model::ta::{TaCertificate, TrustAnchor};
|
||||
use rpki::data_model::tal::Tal;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct BytesFmt {
|
||||
len: usize,
|
||||
sha256_hex: String,
|
||||
head_hex: String,
|
||||
tail_hex: String,
|
||||
}
|
||||
|
||||
fn bytes_fmt(bytes: &[u8]) -> BytesFmt {
|
||||
let len = bytes.len();
|
||||
let sha = Sha256::digest(bytes);
|
||||
|
||||
let head_len = len.min(16);
|
||||
let tail_len = len.min(16);
|
||||
|
||||
let head = &bytes[..head_len];
|
||||
let tail = &bytes[len.saturating_sub(tail_len)..];
|
||||
|
||||
BytesFmt {
|
||||
len,
|
||||
sha256_hex: hex::encode(sha),
|
||||
head_hex: hex::encode(head),
|
||||
tail_hex: hex::encode(tail),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct TalPretty {
|
||||
raw: BytesFmt,
|
||||
comments: Vec<String>,
|
||||
ta_uris: Vec<String>,
|
||||
subject_public_key_info_der: BytesFmt,
|
||||
}
|
||||
|
||||
impl From<&Tal> for TalPretty {
|
||||
fn from(v: &Tal) -> Self {
|
||||
Self {
|
||||
raw: bytes_fmt(&v.raw),
|
||||
comments: v.comments.clone(),
|
||||
ta_uris: v.ta_uris.iter().map(|u| u.to_string()).collect(),
|
||||
subject_public_key_info_der: bytes_fmt(&v.subject_public_key_info_der),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct TaCertificatePretty {
|
||||
raw_der: BytesFmt,
|
||||
rc_ca: ResourceCertificatePretty,
|
||||
}
|
||||
|
||||
impl From<&TaCertificate> for TaCertificatePretty {
|
||||
fn from(v: &TaCertificate) -> Self {
|
||||
Self {
|
||||
raw_der: bytes_fmt(&v.raw_der),
|
||||
rc_ca: ResourceCertificatePretty::from(&v.rc_ca),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct TrustAnchorPretty {
|
||||
tal: TalPretty,
|
||||
ta_certificate: TaCertificatePretty,
|
||||
resolved_ta_uri: Option<String>,
|
||||
}
|
||||
|
||||
impl From<&TrustAnchor> for TrustAnchorPretty {
|
||||
fn from(v: &TrustAnchor) -> Self {
|
||||
Self {
|
||||
tal: TalPretty::from(&v.tal),
|
||||
ta_certificate: TaCertificatePretty::from(&v.ta_certificate),
|
||||
resolved_ta_uri: v.resolved_ta_uri.as_ref().map(|u| u.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct ResourceCertificatePretty {
|
||||
raw_der: BytesFmt,
|
||||
tbs: RpkixTbsCertificatePretty,
|
||||
kind: ResourceCertKind,
|
||||
}
|
||||
|
||||
impl From<&ResourceCertificate> for ResourceCertificatePretty {
|
||||
fn from(v: &ResourceCertificate) -> Self {
|
||||
Self {
|
||||
raw_der: bytes_fmt(&v.raw_der),
|
||||
tbs: RpkixTbsCertificatePretty::from(&v.tbs),
|
||||
kind: v.kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct RpkixTbsCertificatePretty {
|
||||
version: u32,
|
||||
serial_number: String,
|
||||
signature_algorithm: String,
|
||||
issuer_dn: String,
|
||||
subject_dn: String,
|
||||
validity_not_before: time::OffsetDateTime,
|
||||
validity_not_after: time::OffsetDateTime,
|
||||
subject_public_key_info: BytesFmt,
|
||||
extensions: RcExtensionsPretty,
|
||||
}
|
||||
|
||||
impl From<&RpkixTbsCertificate> for RpkixTbsCertificatePretty {
|
||||
fn from(v: &RpkixTbsCertificate) -> Self {
|
||||
Self {
|
||||
version: v.version,
|
||||
serial_number: hex::encode(v.serial_number.to_bytes_be()),
|
||||
signature_algorithm: v.signature_algorithm.clone(),
|
||||
issuer_dn: v.issuer_dn.clone(),
|
||||
subject_dn: v.subject_dn.clone(),
|
||||
validity_not_before: v.validity_not_before,
|
||||
validity_not_after: v.validity_not_after,
|
||||
subject_public_key_info: bytes_fmt(&v.subject_public_key_info),
|
||||
extensions: RcExtensionsPretty::from(&v.extensions),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct RcExtensionsPretty {
|
||||
basic_constraints_ca: bool,
|
||||
subject_key_identifier: Option<BytesFmt>,
|
||||
subject_info_access: Option<SubjectInfoAccess>,
|
||||
certificate_policies_oid: Option<String>,
|
||||
ip_resources: Option<IpResourceSet>,
|
||||
as_resources: Option<AsResourceSet>,
|
||||
}
|
||||
|
||||
impl From<&RcExtensions> for RcExtensionsPretty {
|
||||
fn from(v: &RcExtensions) -> Self {
|
||||
Self {
|
||||
basic_constraints_ca: v.basic_constraints_ca,
|
||||
subject_key_identifier: v.subject_key_identifier.as_ref().map(|b| bytes_fmt(b)),
|
||||
subject_info_access: v.subject_info_access.clone(),
|
||||
certificate_policies_oid: v.certificate_policies_oid.clone(),
|
||||
ip_resources: v.ip_resources.clone(),
|
||||
as_resources: v.as_resources.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct RpkiSignedObjectPretty {
|
||||
raw_der: BytesFmt,
|
||||
content_info_content_type: String,
|
||||
signed_data: SignedDataProfiledPretty,
|
||||
}
|
||||
|
||||
impl From<&RpkiSignedObject> for RpkiSignedObjectPretty {
|
||||
fn from(v: &RpkiSignedObject) -> Self {
|
||||
Self {
|
||||
raw_der: bytes_fmt(&v.raw_der),
|
||||
content_info_content_type: v.content_info_content_type.clone(),
|
||||
signed_data: SignedDataProfiledPretty::from(&v.signed_data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct SignedDataProfiledPretty {
|
||||
version: u32,
|
||||
digest_algorithms: Vec<String>,
|
||||
encap_content_info: EncapsulatedContentInfoPretty,
|
||||
certificates: Vec<ResourceEeCertificatePretty>,
|
||||
crls_present: bool,
|
||||
signer_infos: Vec<SignerInfoProfiledPretty>,
|
||||
}
|
||||
|
||||
impl From<&SignedDataProfiled> for SignedDataProfiledPretty {
|
||||
fn from(v: &SignedDataProfiled) -> Self {
|
||||
Self {
|
||||
version: v.version,
|
||||
digest_algorithms: v.digest_algorithms.clone(),
|
||||
encap_content_info: EncapsulatedContentInfoPretty::from(&v.encap_content_info),
|
||||
certificates: v
|
||||
.certificates
|
||||
.iter()
|
||||
.map(ResourceEeCertificatePretty::from)
|
||||
.collect(),
|
||||
crls_present: v.crls_present,
|
||||
signer_infos: v.signer_infos.iter().map(SignerInfoProfiledPretty::from).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct EncapsulatedContentInfoPretty {
|
||||
econtent_type: String,
|
||||
econtent: BytesFmt,
|
||||
}
|
||||
|
||||
impl From<&EncapsulatedContentInfo> for EncapsulatedContentInfoPretty {
|
||||
fn from(v: &EncapsulatedContentInfo) -> Self {
|
||||
Self {
|
||||
econtent_type: v.econtent_type.clone(),
|
||||
econtent: bytes_fmt(&v.econtent),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct ResourceEeCertificatePretty {
|
||||
raw_der: BytesFmt,
|
||||
subject_key_identifier: BytesFmt,
|
||||
spki_der: BytesFmt,
|
||||
sia_signed_object_uris: Vec<String>,
|
||||
resource_cert: ResourceCertificatePretty,
|
||||
}
|
||||
|
||||
impl From<&ResourceEeCertificate> for ResourceEeCertificatePretty {
|
||||
fn from(v: &ResourceEeCertificate) -> Self {
|
||||
Self {
|
||||
raw_der: bytes_fmt(&v.raw_der),
|
||||
subject_key_identifier: bytes_fmt(&v.subject_key_identifier),
|
||||
spki_der: bytes_fmt(&v.spki_der),
|
||||
sia_signed_object_uris: v.sia_signed_object_uris.clone(),
|
||||
resource_cert: ResourceCertificatePretty::from(&v.resource_cert),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct SignerInfoProfiledPretty {
|
||||
version: u32,
|
||||
sid_ski: BytesFmt,
|
||||
digest_algorithm: String,
|
||||
signature_algorithm: String,
|
||||
signed_attrs: SignedAttrsProfiledPretty,
|
||||
unsigned_attrs_present: bool,
|
||||
signature: BytesFmt,
|
||||
signed_attrs_der_for_signature: BytesFmt,
|
||||
}
|
||||
|
||||
impl From<&SignerInfoProfiled> for SignerInfoProfiledPretty {
|
||||
fn from(v: &SignerInfoProfiled) -> Self {
|
||||
Self {
|
||||
version: v.version,
|
||||
sid_ski: bytes_fmt(&v.sid_ski),
|
||||
digest_algorithm: v.digest_algorithm.clone(),
|
||||
signature_algorithm: v.signature_algorithm.clone(),
|
||||
signed_attrs: SignedAttrsProfiledPretty::from(&v.signed_attrs),
|
||||
unsigned_attrs_present: v.unsigned_attrs_present,
|
||||
signature: bytes_fmt(&v.signature),
|
||||
signed_attrs_der_for_signature: bytes_fmt(&v.signed_attrs_der_for_signature),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct SignedAttrsProfiledPretty {
|
||||
content_type: String,
|
||||
message_digest: BytesFmt,
|
||||
signing_time: rpki::data_model::common::Asn1TimeUtc,
|
||||
other_attrs_present: bool,
|
||||
}
|
||||
|
||||
impl From<&SignedAttrsProfiled> for SignedAttrsProfiledPretty {
|
||||
fn from(v: &SignedAttrsProfiled) -> Self {
|
||||
Self {
|
||||
content_type: v.content_type.clone(),
|
||||
message_digest: bytes_fmt(&v.message_digest),
|
||||
signing_time: v.signing_time,
|
||||
other_attrs_present: v.other_attrs_present,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct ManifestObjectPretty {
|
||||
signed_object: RpkiSignedObjectPretty,
|
||||
econtent_type: String,
|
||||
manifest: ManifestEContentPretty,
|
||||
}
|
||||
|
||||
impl From<&ManifestObject> for ManifestObjectPretty {
|
||||
fn from(v: &ManifestObject) -> Self {
|
||||
Self {
|
||||
signed_object: RpkiSignedObjectPretty::from(&v.signed_object),
|
||||
econtent_type: v.econtent_type.clone(),
|
||||
manifest: ManifestEContentPretty::from(&v.manifest),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct ManifestEContentPretty {
|
||||
version: u32,
|
||||
manifest_number: String,
|
||||
this_update: time::OffsetDateTime,
|
||||
next_update: time::OffsetDateTime,
|
||||
file_hash_alg: String,
|
||||
files: Vec<FileAndHashPretty>,
|
||||
}
|
||||
|
||||
impl From<&ManifestEContent> for ManifestEContentPretty {
|
||||
fn from(v: &ManifestEContent) -> Self {
|
||||
Self {
|
||||
version: v.version,
|
||||
manifest_number: v.manifest_number.to_hex_upper(),
|
||||
this_update: v.this_update,
|
||||
next_update: v.next_update,
|
||||
file_hash_alg: v.file_hash_alg.clone(),
|
||||
files: v.files.iter().map(FileAndHashPretty::from).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct FileAndHashPretty {
|
||||
file_name: String,
|
||||
hash_hex: String,
|
||||
}
|
||||
|
||||
impl From<&FileAndHash> for FileAndHashPretty {
|
||||
fn from(v: &FileAndHash) -> Self {
|
||||
Self {
|
||||
file_name: v.file_name.clone(),
|
||||
hash_hex: hex::encode(&v.hash_bytes),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct RoaObjectPretty {
|
||||
signed_object: RpkiSignedObjectPretty,
|
||||
econtent_type: String,
|
||||
roa: RoaEContentPretty,
|
||||
}
|
||||
|
||||
impl From<&RoaObject> for RoaObjectPretty {
|
||||
fn from(v: &RoaObject) -> Self {
|
||||
Self {
|
||||
signed_object: RpkiSignedObjectPretty::from(&v.signed_object),
|
||||
econtent_type: v.econtent_type.clone(),
|
||||
roa: RoaEContentPretty::from(&v.roa),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct RoaEContentPretty {
|
||||
version: u32,
|
||||
as_id: u32,
|
||||
ip_addr_blocks: Vec<RoaIpAddressFamily>,
|
||||
}
|
||||
|
||||
impl From<&RoaEContent> for RoaEContentPretty {
|
||||
fn from(v: &RoaEContent) -> Self {
|
||||
Self {
|
||||
version: v.version,
|
||||
as_id: v.as_id,
|
||||
ip_addr_blocks: v.ip_addr_blocks.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct AspaObjectPretty {
|
||||
signed_object: RpkiSignedObjectPretty,
|
||||
econtent_type: String,
|
||||
aspa: AspaEContent,
|
||||
}
|
||||
|
||||
impl From<&AspaObject> for AspaObjectPretty {
|
||||
fn from(v: &AspaObject) -> Self {
|
||||
Self {
|
||||
signed_object: RpkiSignedObjectPretty::from(&v.signed_object),
|
||||
econtent_type: v.econtent_type.clone(),
|
||||
aspa: v.aspa.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct RpkixCrlPretty {
|
||||
raw_der: BytesFmt,
|
||||
version: u32,
|
||||
issuer_dn: String,
|
||||
signature_algorithm_oid: String,
|
||||
this_update: rpki::data_model::common::Asn1TimeUtc,
|
||||
next_update: rpki::data_model::common::Asn1TimeUtc,
|
||||
revoked_certs: Vec<RevokedCert>,
|
||||
extensions: CrlExtensions,
|
||||
}
|
||||
|
||||
impl From<&RpkixCrl> for RpkixCrlPretty {
|
||||
fn from(v: &RpkixCrl) -> Self {
|
||||
Self {
|
||||
raw_der: bytes_fmt(&v.raw_der),
|
||||
version: v.version,
|
||||
issuer_dn: v.issuer_dn.clone(),
|
||||
signature_algorithm_oid: v.signature_algorithm_oid.clone(),
|
||||
this_update: v.this_update,
|
||||
next_update: v.next_update,
|
||||
revoked_certs: v.revoked_certs.clone(),
|
||||
extensions: v.extensions.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn print_all_models_from_real_fixtures() {
|
||||
// Note: run this test with `cargo test --test test_model_print_real_fixtures -- --nocapture`
|
||||
// to see the output.
|
||||
|
||||
let tal_path = "tests/fixtures/tal/ripe-ncc.tal";
|
||||
println!("== TAL / TA / TrustAnchor ==");
|
||||
println!("Fixture (TAL): {tal_path}");
|
||||
let tal_raw = std::fs::read(tal_path).expect("read TAL fixture");
|
||||
let tal = Tal::decode_bytes(&tal_raw).expect("decode TAL");
|
||||
|
||||
let ta_path = "tests/fixtures/ta/ripe-ncc-ta.cer";
|
||||
println!("Fixture (TA): {ta_path}");
|
||||
let ta_der = std::fs::read(ta_path).expect("read TA fixture");
|
||||
let ta = TaCertificate::from_der(&ta_der).expect("parse TA cert");
|
||||
|
||||
let resolved = tal
|
||||
.ta_uris
|
||||
.iter()
|
||||
.find(|u| u.scheme() == "https")
|
||||
.or_else(|| tal.ta_uris.first())
|
||||
.cloned()
|
||||
.expect("tal has at least one uri");
|
||||
let trust_anchor = TrustAnchor::bind(tal, &ta_der, Some(&resolved)).expect("bind trust anchor");
|
||||
println!("{:#?}", TalPretty::from(&trust_anchor.tal));
|
||||
println!("{:#?}", TaCertificatePretty::from(&ta));
|
||||
println!("{:#?}", TrustAnchorPretty::from(&trust_anchor));
|
||||
|
||||
println!();
|
||||
println!("== ResourceCertificate (example non-TA CA cert) ==");
|
||||
let ca_path =
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer";
|
||||
println!("Fixture (CA cert): {ca_path}");
|
||||
let ca_der = std::fs::read(ca_path).expect("read CA cert fixture");
|
||||
let ca_rc = ResourceCertificate::from_der(&ca_der).expect("parse CA resource certificate");
|
||||
println!("{:#?}", ResourceCertificatePretty::from(&ca_rc));
|
||||
|
||||
println!();
|
||||
println!("== Signed Object / Manifest ==");
|
||||
let mft_path =
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
|
||||
println!("Fixture (MFT): {mft_path}");
|
||||
let mft_der = std::fs::read(mft_path).expect("read MFT fixture");
|
||||
let mft_obj = ManifestObject::decode_der(&mft_der).expect("decode manifest object");
|
||||
println!("{:#?}", ManifestObjectPretty::from(&mft_obj));
|
||||
println!("Manifest.validate_embedded_ee_cert={:?}", mft_obj.validate_embedded_ee_cert());
|
||||
println!("Manifest.verify_signature={:?}", mft_obj.signed_object.verify_signature());
|
||||
|
||||
println!();
|
||||
println!("== Signed Object / ROA ==");
|
||||
let roa_path = "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa";
|
||||
println!("Fixture (ROA): {roa_path}");
|
||||
let roa_der = std::fs::read(roa_path).expect("read ROA fixture");
|
||||
let roa_obj = RoaObject::decode_der(&roa_der).expect("decode ROA object");
|
||||
println!("{:#?}", RoaObjectPretty::from(&roa_obj));
|
||||
println!("ROA.validate_embedded_ee_cert={:?}", roa_obj.validate_embedded_ee_cert());
|
||||
println!("ROA.verify_signature={:?}", roa_obj.signed_object.verify_signature());
|
||||
|
||||
println!();
|
||||
println!("== Signed Object / ASPA ==");
|
||||
let aspa_path =
|
||||
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa";
|
||||
println!("Fixture (ASPA): {aspa_path}");
|
||||
let aspa_der = std::fs::read(aspa_path).expect("read ASPA fixture");
|
||||
let aspa_obj = AspaObject::decode_der(&aspa_der).expect("decode ASPA object");
|
||||
println!("{:#?}", AspaObjectPretty::from(&aspa_obj));
|
||||
println!("ASPA.validate_embedded_ee_cert={:?}", aspa_obj.validate_embedded_ee_cert());
|
||||
println!("ASPA.verify_signature={:?}", aspa_obj.signed_object.verify_signature());
|
||||
|
||||
println!();
|
||||
println!("== CRL ==");
|
||||
let crl_path = "tests/fixtures/0099DEAB073EFD74C250C0A382B25012B5082AEE.crl";
|
||||
println!("Fixture (CRL): {crl_path}");
|
||||
let crl_der = std::fs::read(crl_path).expect("read CRL fixture");
|
||||
let crl = RpkixCrl::decode_der(&crl_der).expect("decode CRL");
|
||||
println!("{:#?}", RpkixCrlPretty::from(&crl));
|
||||
}
|
||||
93
tests/test_rc_from_der_errors.rs
Normal file
93
tests/test_rc_from_der_errors.rs
Normal file
@ -0,0 +1,93 @@
|
||||
use rpki::data_model::rc::{ResourceCertificate, ResourceCertificateError};
|
||||
|
||||
const TEST_NO_SIA_CERT_DER_B64: &str = "MIIDATCCAemgAwIBAgIUCyQLQJn92+gyAzvIz22q1F/97OMwDQYJKoZIhvcNAQELBQAwGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMB4XDTI2MDEyNzAzNTk1OVoXDTM2MDEyNTAzNTk1OVowGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/aoMU8J6cddkM2r6F2snd1rCdQPepgo2T2lrqWFcxnQJdcxBL1OYg3wFi95TJmZSeIHIOGauDaJ2abmjgyOUHOC4U68x66JRg4hLkmLxo1cf3uYHWl9Obph6g2qPRvN80ORq70JPuL6mAfUkNiO9hnwK6oQiTzc/rjCQGIFH8kTESBMXLfNCyUpGi+MNztYH6Ha6bKAQuXgd29OFwIkOlGQnYgGC2qBMvnp86eITvV1gTiuI8Ho9m9nZHCmaD7TylvkMDq8Hk5nkIpRcG0uO60SkR2BiMOYe/TNn5dTmHd6bsdbU2GOvgnq1SnqGq3FOWhKIe3ycUJde0uNfZOqRwIDAQABoz8wPTAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUFjyzfJCDNhFfKxVr06kjUkE23dMwDQYJKoZIhvcNAQELBQADggEBAK98n2gVlwKA3Ob1YeAm9f+8hm7pbvrt0tA8GW180CILjf09k7fKgiRlxqGdZ9ySXjU52+zCqu3MpBXVbI87ZC+zA6uK05n4y1F0n85MJ9hGR2UEiPcqou85X73LvioynnSOy/OV1PjKJXReUsqF3GgDtgcMyFssPJ9s/5DWuUCScUJY6pu0kuIGOLQ/oXUw4TvxUeyz73gOTiAJshVTQoLpHUhj0595S7lArjwi7oLI1b8m8guTknvhk0Sc3tJZmUqOcIvYIs0guHpaeC+sMoF4K+6UTrxxOBdX+fUEWNpUyYXWHjdZq25PbJdHwA/VAW2zYVojaVREligf0Qfo6F4=";
|
||||
|
||||
fn decode_b64(b64: &str) -> Vec<u8> {
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine as _;
|
||||
STANDARD.decode(b64).unwrap()
|
||||
}
|
||||
|
||||
fn replace_first(haystack: &mut [u8], needle: &[u8], replacement: &[u8]) -> bool {
|
||||
if needle.len() != replacement.len() {
|
||||
return false;
|
||||
}
|
||||
for i in 0..=haystack.len().saturating_sub(needle.len()) {
|
||||
if &haystack[i..i + needle.len()] == needle {
|
||||
haystack[i..i + needle.len()].copy_from_slice(replacement);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn replace_all(haystack: &mut [u8], needle: &[u8], replacement: &[u8]) -> usize {
|
||||
if needle.len() != replacement.len() {
|
||||
return 0;
|
||||
}
|
||||
let mut n = 0;
|
||||
let mut i = 0;
|
||||
while i + needle.len() <= haystack.len() {
|
||||
if &haystack[i..i + needle.len()] == needle {
|
||||
haystack[i..i + needle.len()].copy_from_slice(replacement);
|
||||
n += 1;
|
||||
i += needle.len();
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_bytes_after_cert_are_rejected() {
|
||||
let mut der = decode_b64(TEST_NO_SIA_CERT_DER_B64);
|
||||
der.push(0);
|
||||
let err = ResourceCertificate::from_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ResourceCertificateError::TrailingBytes(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_algorithm_mismatch_is_detected() {
|
||||
let mut der = decode_b64(TEST_NO_SIA_CERT_DER_B64);
|
||||
// DER encoding of sha256WithRSAEncryption OID:
|
||||
// 06 09 2A 86 48 86 F7 0D 01 01 0B
|
||||
let oid = [0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B];
|
||||
let mut patched = oid;
|
||||
patched[10] = 0x01; // rsaEncryption, same length encoding
|
||||
assert!(replace_first(&mut der, &oid, &patched));
|
||||
let err = ResourceCertificate::from_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ResourceCertificateError::SignatureAlgorithmMismatch));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_signature_algorithm_is_detected() {
|
||||
let mut der = decode_b64(TEST_NO_SIA_CERT_DER_B64);
|
||||
let oid = [0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B];
|
||||
let mut patched = oid;
|
||||
patched[10] = 0x01;
|
||||
let n = replace_all(&mut der, &oid, &patched);
|
||||
assert!(n >= 2, "expected to patch at least inner+outer AlgorithmIdentifier");
|
||||
let err = ResourceCertificate::from_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ResourceCertificateError::UnsupportedSignatureAlgorithm));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_signature_algorithm_parameters_are_detected() {
|
||||
let mut der = decode_b64(TEST_NO_SIA_CERT_DER_B64);
|
||||
// Replace NULL parameters (05 00) right after the sha256WithRSAEncryption OID with
|
||||
// an empty OCTET STRING (04 00) to keep the encoding length unchanged.
|
||||
let alg = [
|
||||
0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B, 0x05, 0x00,
|
||||
];
|
||||
let mut patched = alg;
|
||||
patched[11] = 0x04;
|
||||
let n = replace_all(&mut der, &alg, &patched);
|
||||
assert!(n >= 2, "expected to patch at least inner+outer AlgorithmIdentifier parameters");
|
||||
let err = ResourceCertificate::from_der(&der).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
ResourceCertificateError::InvalidSignatureAlgorithmParameters
|
||||
));
|
||||
}
|
||||
|
||||
43
tests/test_rc_from_der_fixtures.rs
Normal file
43
tests/test_rc_from_der_fixtures.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use rpki::data_model::oid::OID_CP_IPADDR_ASNUMBER;
|
||||
use rpki::data_model::rc::{ResourceCertKind, ResourceCertificate, SubjectInfoAccess};
|
||||
|
||||
#[test]
|
||||
fn resource_certificate_from_der_parses_ca_fixtures() {
|
||||
let fixtures = [
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer",
|
||||
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ZW5EIqvxKWSSAOsBmoFfKxIjbpI.cer",
|
||||
];
|
||||
|
||||
for path in fixtures {
|
||||
let der = std::fs::read(path).expect("read CA cert fixture");
|
||||
let rc = ResourceCertificate::from_der(&der).expect("parse CA cert fixture");
|
||||
|
||||
assert_eq!(rc.kind, ResourceCertKind::Ca, "fixture should be CA: {path}");
|
||||
assert_eq!(rc.tbs.version, 2, "X.509 v3 encoded as 2: {path}");
|
||||
|
||||
assert_eq!(
|
||||
rc.tbs.extensions.certificate_policies_oid.as_deref(),
|
||||
Some(OID_CP_IPADDR_ASNUMBER),
|
||||
"CA certificatePolicies OID: {path}"
|
||||
);
|
||||
|
||||
assert!(
|
||||
matches!(rc.tbs.extensions.subject_info_access, Some(SubjectInfoAccess::Ca(_))),
|
||||
"CA SIA should not contain signedObject accessMethod: {path}"
|
||||
);
|
||||
|
||||
assert!(rc.tbs.extensions.ip_resources.is_some(), "CA should have IP resources: {path}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resource_certificate_from_der_parses_as_resources_in_apnic_fixture() {
|
||||
let path =
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer";
|
||||
let der = std::fs::read(path).expect("read APNIC CA cert fixture");
|
||||
let rc = ResourceCertificate::from_der(&der).expect("parse APNIC CA cert fixture");
|
||||
|
||||
assert!(rc.tbs.extensions.as_resources.is_some(), "fixture should carry AS resources");
|
||||
}
|
||||
|
||||
93
tests/test_rc_helpers.rs
Normal file
93
tests/test_rc_helpers.rs
Normal file
@ -0,0 +1,93 @@
|
||||
use rpki::data_model::rc::{
|
||||
Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange,
|
||||
IpPrefix,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn ip_address_family_contains_prefix_afi_mismatch_is_false() {
|
||||
let fam = IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::Inherit,
|
||||
};
|
||||
let p = IpPrefix {
|
||||
afi: Afi::Ipv6,
|
||||
prefix_len: 0,
|
||||
addr: vec![0; 16],
|
||||
};
|
||||
assert!(!fam.contains_prefix(&p));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_address_family_inherit_covers_all_prefixes_of_same_afi() {
|
||||
let fam = IpAddressFamily {
|
||||
afi: Afi::Ipv6,
|
||||
choice: IpAddressChoice::Inherit,
|
||||
};
|
||||
let p = IpPrefix {
|
||||
afi: Afi::Ipv6,
|
||||
prefix_len: 32,
|
||||
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
};
|
||||
assert!(fam.contains_prefix(&p));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_resource_set_asnum_single_id_returns_expected_values() {
|
||||
let inherit = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::Inherit),
|
||||
rdi: None,
|
||||
};
|
||||
assert_eq!(inherit.asnum_single_id(), None);
|
||||
|
||||
let single_id = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64512)])),
|
||||
rdi: None,
|
||||
};
|
||||
assert_eq!(single_id.asnum_single_id(), Some(64512));
|
||||
|
||||
let single_range = AsResourceSet {
|
||||
asnum: None,
|
||||
rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range {
|
||||
min: 64512,
|
||||
max: 64520,
|
||||
}])),
|
||||
};
|
||||
assert_eq!(single_range.asnum_single_id(), None);
|
||||
|
||||
let multiple = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
||||
AsIdOrRange::Id(64512),
|
||||
AsIdOrRange::Id(64513),
|
||||
])),
|
||||
rdi: None,
|
||||
};
|
||||
assert_eq!(multiple.asnum_single_id(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_identifier_choice_has_range_detects_ranges() {
|
||||
assert!(!AsIdentifierChoice::Inherit.has_range());
|
||||
assert!(!AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64512)]).has_range());
|
||||
assert!(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range { min: 1, max: 2 }]).has_range());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_resource_contains_prefix_finds_matching_family() {
|
||||
let fam_v6 = IpAddressFamily {
|
||||
afi: Afi::Ipv6,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(IpPrefix {
|
||||
afi: Afi::Ipv6,
|
||||
prefix_len: 32,
|
||||
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
})]),
|
||||
};
|
||||
let set = rpki::data_model::rc::IpResourceSet { families: vec![fam_v6] };
|
||||
|
||||
let p = IpPrefix {
|
||||
afi: Afi::Ipv6,
|
||||
prefix_len: 48,
|
||||
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0x12, 0x34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
};
|
||||
assert!(set.contains_prefix(&p));
|
||||
}
|
||||
|
||||
58
tests/test_rc_ip_resource_range_coverage.rs
Normal file
58
tests/test_rc_ip_resource_range_coverage.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use rpki::data_model::rc::{
|
||||
Afi, IpAddressChoice, IpAddressFamily, IpAddressOrRange, IpAddressRange, IpPrefix, IpResourceSet,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn ip_resource_range_covers_prefix_ipv4() {
|
||||
let set = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Range(IpAddressRange {
|
||||
min: vec![10, 0, 0, 0],
|
||||
max: vec![10, 255, 255, 255],
|
||||
})]),
|
||||
}],
|
||||
};
|
||||
|
||||
let inside = IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 16,
|
||||
addr: vec![10, 1, 0, 0],
|
||||
};
|
||||
assert!(set.contains_prefix(&inside));
|
||||
|
||||
let outside = IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 16,
|
||||
addr: vec![11, 0, 0, 0],
|
||||
};
|
||||
assert!(!set.contains_prefix(&outside));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_resource_range_covers_prefix_ipv6() {
|
||||
let set = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv6,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Range(IpAddressRange {
|
||||
min: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
max: vec![0x20, 0x01, 0x0d, 0xb8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
|
||||
})]),
|
||||
}],
|
||||
};
|
||||
|
||||
let inside = IpPrefix {
|
||||
afi: Afi::Ipv6,
|
||||
prefix_len: 64,
|
||||
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
};
|
||||
assert!(set.contains_prefix(&inside));
|
||||
|
||||
let outside = IpPrefix {
|
||||
afi: Afi::Ipv6,
|
||||
prefix_len: 64,
|
||||
addr: vec![0x20, 0x01, 0x0d, 0xb9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
};
|
||||
assert!(!set.contains_prefix(&outside));
|
||||
}
|
||||
|
||||
135
tests/test_rc_resource_extensions_decode.rs
Normal file
135
tests/test_rc_resource_extensions_decode.rs
Normal file
@ -0,0 +1,135 @@
|
||||
use rpki::data_model::rc::{
|
||||
Afi, AsIdentifierChoice, AsIdOrRange, AsResourceSet, IpAddressChoice, IpAddressOrRange,
|
||||
IpResourceSet,
|
||||
};
|
||||
|
||||
fn len_bytes(len: usize) -> Vec<u8> {
|
||||
if len < 128 {
|
||||
vec![len as u8]
|
||||
} else {
|
||||
let mut tmp = Vec::new();
|
||||
let mut n = len;
|
||||
while n > 0 {
|
||||
tmp.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
tmp.reverse();
|
||||
let mut out = vec![0x80 | (tmp.len() as u8)];
|
||||
out.extend(tmp);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
fn tlv(tag: u8, content: &[u8]) -> Vec<u8> {
|
||||
let mut out = vec![tag];
|
||||
out.extend(len_bytes(content.len()));
|
||||
out.extend_from_slice(content);
|
||||
out
|
||||
}
|
||||
|
||||
fn der_sequence(children: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
let mut content = Vec::new();
|
||||
for c in children {
|
||||
content.extend(c);
|
||||
}
|
||||
tlv(0x30, &content)
|
||||
}
|
||||
|
||||
fn der_octet_string(bytes: &[u8]) -> Vec<u8> {
|
||||
tlv(0x04, bytes)
|
||||
}
|
||||
|
||||
fn der_null() -> Vec<u8> {
|
||||
vec![0x05, 0x00]
|
||||
}
|
||||
|
||||
fn der_integer_u64(v: u64) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
let mut n = v;
|
||||
if n == 0 {
|
||||
bytes.push(0);
|
||||
} else {
|
||||
while n > 0 {
|
||||
bytes.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
bytes.reverse();
|
||||
if bytes[0] & 0x80 != 0 {
|
||||
bytes.insert(0, 0);
|
||||
}
|
||||
}
|
||||
tlv(0x02, &bytes)
|
||||
}
|
||||
|
||||
fn der_bit_string(unused: u8, bytes: &[u8]) -> Vec<u8> {
|
||||
let mut content = vec![unused];
|
||||
content.extend_from_slice(bytes);
|
||||
tlv(0x03, &content)
|
||||
}
|
||||
|
||||
fn cs_cons(tag_no: u8, inner_der: Vec<u8>) -> Vec<u8> {
|
||||
tlv(0xA0 | (tag_no & 0x1F), &inner_der)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_addr_blocks_decode_handles_inherit_and_range_endpoint_fill() {
|
||||
// IPv6 family with one range:
|
||||
// - min: 240a:a000::/32 (encoded as 32 bits)
|
||||
// - max: 240a:a008::/31 (encoded as 31 bits, so fill-ones yields 240a:a009:ffff..)
|
||||
let address_family = der_octet_string(&[0x00, 0x02]);
|
||||
let min = der_bit_string(0, &[0x24, 0x0A, 0xA0, 0x00]);
|
||||
let max = der_bit_string(1, &[0x24, 0x0A, 0xA0, 0x08]);
|
||||
let range = der_sequence(vec![min, max]);
|
||||
let choice = der_sequence(vec![range]);
|
||||
let fam = der_sequence(vec![address_family, choice]);
|
||||
let ip_addr_blocks = der_sequence(vec![fam]);
|
||||
|
||||
let decoded = IpResourceSet::decode_extn_value(&ip_addr_blocks).expect("decode ipAddrBlocks");
|
||||
assert_eq!(decoded.families.len(), 1);
|
||||
let fam = &decoded.families[0];
|
||||
assert_eq!(fam.afi, Afi::Ipv6);
|
||||
let IpAddressChoice::AddressesOrRanges(items) = &fam.choice else {
|
||||
panic!("expected AddressesOrRanges");
|
||||
};
|
||||
let IpAddressOrRange::Range(r) = &items[0] else {
|
||||
panic!("expected Range");
|
||||
};
|
||||
assert_eq!(r.min[..4], [0x24, 0x0A, 0xA0, 0x00]);
|
||||
assert_eq!(r.max[..4], [0x24, 0x0A, 0xA0, 0x09]);
|
||||
assert!(r.max[4..].iter().all(|&b| b == 0xFF));
|
||||
|
||||
// Inherit branch.
|
||||
let fam_inherit = der_sequence(vec![der_octet_string(&[0x00, 0x01]), der_null()]);
|
||||
let decoded = IpResourceSet::decode_extn_value(&der_sequence(vec![fam_inherit])).expect("decode inherit");
|
||||
assert!(decoded.is_all_inherit());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autonomous_sys_ids_decode_handles_inherit_ids_and_ranges() {
|
||||
// ASIdentifiers with:
|
||||
// - asnum [0] EXPLICIT asIdsOrRanges { id 64496, range 64500..64510 }
|
||||
let as_id_or_ranges = der_sequence(vec![
|
||||
der_integer_u64(64496),
|
||||
der_sequence(vec![der_integer_u64(64500), der_integer_u64(64510)]),
|
||||
]);
|
||||
let asnum = cs_cons(0, as_id_or_ranges);
|
||||
let as_identifiers = der_sequence(vec![asnum]);
|
||||
|
||||
let decoded = AsResourceSet::decode_extn_value(&as_identifiers).expect("decode asIdentifiers");
|
||||
assert!(decoded.rdi.is_none());
|
||||
let Some(asnum) = decoded.asnum else {
|
||||
panic!("missing asnum");
|
||||
};
|
||||
let AsIdentifierChoice::AsIdsOrRanges(items) = asnum else {
|
||||
panic!("expected AsIdsOrRanges");
|
||||
};
|
||||
assert_eq!(items.len(), 2);
|
||||
assert!(matches!(items[0], AsIdOrRange::Id(64496)));
|
||||
assert!(matches!(items[1], AsIdOrRange::Range { min: 64500, max: 64510 }));
|
||||
|
||||
// asnum inherit.
|
||||
let asnum_inherit = cs_cons(0, der_null());
|
||||
let decoded = AsResourceSet::decode_extn_value(&der_sequence(vec![asnum_inherit])).expect("decode inherit");
|
||||
assert!(decoded.is_asnum_inherit());
|
||||
}
|
||||
|
||||
202
tests/test_rc_resource_extensions_decode_errors.rs
Normal file
202
tests/test_rc_resource_extensions_decode_errors.rs
Normal file
@ -0,0 +1,202 @@
|
||||
use rpki::data_model::rc::{AsResourceSet, IpResourceSet};
|
||||
|
||||
fn len_bytes(len: usize) -> Vec<u8> {
|
||||
if len < 128 {
|
||||
vec![len as u8]
|
||||
} else {
|
||||
let mut tmp = Vec::new();
|
||||
let mut n = len;
|
||||
while n > 0 {
|
||||
tmp.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
tmp.reverse();
|
||||
let mut out = vec![0x80 | (tmp.len() as u8)];
|
||||
out.extend(tmp);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
fn tlv(tag: u8, content: &[u8]) -> Vec<u8> {
|
||||
let mut out = vec![tag];
|
||||
out.extend(len_bytes(content.len()));
|
||||
out.extend_from_slice(content);
|
||||
out
|
||||
}
|
||||
|
||||
fn der_sequence(children: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
let mut content = Vec::new();
|
||||
for c in children {
|
||||
content.extend(c);
|
||||
}
|
||||
tlv(0x30, &content)
|
||||
}
|
||||
|
||||
fn der_octet_string(bytes: &[u8]) -> Vec<u8> {
|
||||
tlv(0x04, bytes)
|
||||
}
|
||||
|
||||
fn der_null() -> Vec<u8> {
|
||||
vec![0x05, 0x00]
|
||||
}
|
||||
|
||||
fn der_integer_bytes(bytes: &[u8]) -> Vec<u8> {
|
||||
tlv(0x02, bytes)
|
||||
}
|
||||
|
||||
fn der_integer_u64(v: u64) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
let mut n = v;
|
||||
if n == 0 {
|
||||
bytes.push(0);
|
||||
} else {
|
||||
while n > 0 {
|
||||
bytes.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
bytes.reverse();
|
||||
if bytes[0] & 0x80 != 0 {
|
||||
bytes.insert(0, 0);
|
||||
}
|
||||
}
|
||||
der_integer_bytes(&bytes)
|
||||
}
|
||||
|
||||
fn der_bit_string(unused: u8, bytes: &[u8]) -> Vec<u8> {
|
||||
let mut content = vec![unused];
|
||||
content.extend_from_slice(bytes);
|
||||
tlv(0x03, &content)
|
||||
}
|
||||
|
||||
fn cs_cons(tag_no: u8, inner_der: Vec<u8>) -> Vec<u8> {
|
||||
tlv(0xA0 | (tag_no & 0x1F), &inner_der)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_addr_blocks_decode_rejects_invalid_encodings() {
|
||||
// Not a SEQUENCE.
|
||||
assert!(IpResourceSet::decode_extn_value(&der_null()).is_err());
|
||||
|
||||
// IPAddressFamily wrong shape.
|
||||
let fam_wrong = der_sequence(vec![der_octet_string(&[0x00, 0x01])]); // missing choice
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam_wrong])).is_err());
|
||||
|
||||
// addressFamily wrong length.
|
||||
let fam_wrong = der_sequence(vec![
|
||||
der_octet_string(&[0x00]), // 1 byte
|
||||
der_null(),
|
||||
]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam_wrong])).is_err());
|
||||
|
||||
// unsupported AFI.
|
||||
let fam_wrong = der_sequence(vec![der_octet_string(&[0x00, 0x03]), der_null()]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam_wrong])).is_err());
|
||||
|
||||
// ipAddressChoice wrong type.
|
||||
let fam_wrong = der_sequence(vec![
|
||||
der_octet_string(&[0x00, 0x01]),
|
||||
der_integer_u64(1),
|
||||
]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam_wrong])).is_err());
|
||||
|
||||
// BitString with invalid unused-bits value (>7).
|
||||
let min = der_bit_string(0, &[0x0A, 0x00, 0x00, 0x00]);
|
||||
let max = der_bit_string(8, &[0x0A, 0xFF, 0xFF, 0xFF]); // invalid unused bits
|
||||
let range = der_sequence(vec![min, max]);
|
||||
let fam = der_sequence(vec![
|
||||
der_octet_string(&[0x00, 0x01]),
|
||||
der_sequence(vec![range]),
|
||||
]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).is_err());
|
||||
|
||||
// BitString with non-zero bits in the unused tail.
|
||||
let min = der_bit_string(0, &[0x0A, 0x00, 0x00, 0x00]);
|
||||
let max = der_bit_string(1, &[0x0A, 0x00, 0x00, 0x01]); // LSB set, but unused=1
|
||||
let range = der_sequence(vec![min, max]);
|
||||
let fam = der_sequence(vec![
|
||||
der_octet_string(&[0x00, 0x01]),
|
||||
der_sequence(vec![range]),
|
||||
]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).is_err());
|
||||
|
||||
// Prefix length out of range for IPv4 (40 bits).
|
||||
let prefix = der_bit_string(0, &[0x0A, 0x00, 0x00, 0x00, 0x00]); // 40 bits
|
||||
let fam = der_sequence(vec![
|
||||
der_octet_string(&[0x00, 0x01]),
|
||||
der_sequence(vec![prefix]),
|
||||
]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autonomous_sys_ids_decode_rejects_invalid_encodings() {
|
||||
// Not a SEQUENCE.
|
||||
assert!(AsResourceSet::decode_extn_value(&der_null()).is_err());
|
||||
|
||||
// Wrong tag class (expects [0]/[1] context-specific).
|
||||
let as_ids = der_sequence(vec![der_integer_u64(64496)]);
|
||||
assert!(AsResourceSet::decode_extn_value(&as_ids).is_err());
|
||||
|
||||
// Duplicate [0] tags.
|
||||
let a0 = cs_cons(0, der_null());
|
||||
let a0_dup = cs_cons(0, der_null());
|
||||
assert!(AsResourceSet::decode_extn_value(&der_sequence(vec![a0, a0_dup])).is_err());
|
||||
|
||||
// Out-of-range ASID (> u32::MAX).
|
||||
let too_big = der_integer_bytes(&[0x01, 0x00, 0x00, 0x00, 0x00]); // 2^32
|
||||
let asnum = cs_cons(0, der_sequence(vec![too_big]));
|
||||
assert!(AsResourceSet::decode_extn_value(&der_sequence(vec![asnum])).is_err());
|
||||
|
||||
// Range min > max.
|
||||
let bad_range = der_sequence(vec![der_integer_u64(64510), der_integer_u64(64500)]);
|
||||
let asnum = cs_cons(0, der_sequence(vec![bad_range]));
|
||||
assert!(AsResourceSet::decode_extn_value(&der_sequence(vec![asnum])).is_err());
|
||||
|
||||
// Unsupported element inside asIdsOrRanges.
|
||||
let asnum = cs_cons(0, der_sequence(vec![der_null()]));
|
||||
assert!(AsResourceSet::decode_extn_value(&der_sequence(vec![asnum])).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_addr_blocks_prefix_bitstring_unused_bits_checks_are_enforced() {
|
||||
// Prefix BitString with non-zero bits in the unused tail (parse_ip_prefix path).
|
||||
let prefix = der_bit_string(1, &[0x0A, 0x00, 0x00, 0x01]); // LSB set, but unused=1
|
||||
let fam = der_sequence(vec![
|
||||
der_octet_string(&[0x00, 0x01]),
|
||||
der_sequence(vec![prefix]),
|
||||
]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).is_err());
|
||||
|
||||
// Prefix BitString with empty bytes but unused_bits != 0 is invalid.
|
||||
let prefix = der_bit_string(1, &[]);
|
||||
let fam = der_sequence(vec![
|
||||
der_octet_string(&[0x00, 0x01]),
|
||||
der_sequence(vec![prefix]),
|
||||
]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_addr_blocks_range_upper_bound_can_fill_all_ones_when_bit_len_zero() {
|
||||
// A BIT STRING with 0 bits (content is only the "unused bits" count octet) is allowed.
|
||||
// For an IPAddressRange upper bound, RFC 3779 interprets missing bits as 1s.
|
||||
let min = der_bit_string(0, &[]);
|
||||
let max = der_bit_string(0, &[]);
|
||||
let range = der_sequence(vec![min, max]);
|
||||
let fam = der_sequence(vec![
|
||||
der_octet_string(&[0x00, 0x01]),
|
||||
der_sequence(vec![range]),
|
||||
]);
|
||||
|
||||
let set = IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).expect("decode range with 0-bit endpoints");
|
||||
let fam = set.families.iter().find(|f| f.afi == rpki::data_model::rc::Afi::Ipv4).unwrap();
|
||||
let rpki::data_model::rc::IpAddressChoice::AddressesOrRanges(items) = &fam.choice else {
|
||||
panic!("expected explicit addressesOrRanges");
|
||||
};
|
||||
assert_eq!(items.len(), 1);
|
||||
let rpki::data_model::rc::IpAddressOrRange::Range(r) = &items[0] else {
|
||||
panic!("expected a range");
|
||||
};
|
||||
assert_eq!(r.min, vec![0, 0, 0, 0]);
|
||||
assert_eq!(r.max, vec![0xFF, 0xFF, 0xFF, 0xFF]);
|
||||
}
|
||||
127
tests/test_roa_canonicalize.rs
Normal file
127
tests/test_roa_canonicalize.rs
Normal file
@ -0,0 +1,127 @@
|
||||
use rpki::data_model::roa::{IpPrefix, RoaAfi, RoaEContent};
|
||||
|
||||
fn len_bytes(len: usize) -> Vec<u8> {
|
||||
if len < 128 {
|
||||
vec![len as u8]
|
||||
} else {
|
||||
let mut tmp = Vec::new();
|
||||
let mut n = len;
|
||||
while n > 0 {
|
||||
tmp.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
tmp.reverse();
|
||||
let mut out = vec![0x80 | (tmp.len() as u8)];
|
||||
out.extend(tmp);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
fn tlv(tag: u8, content: &[u8]) -> Vec<u8> {
|
||||
let mut out = vec![tag];
|
||||
out.extend(len_bytes(content.len()));
|
||||
out.extend_from_slice(content);
|
||||
out
|
||||
}
|
||||
|
||||
fn der_integer_u64(v: u64) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
let mut n = v;
|
||||
if n == 0 {
|
||||
bytes.push(0);
|
||||
} else {
|
||||
while n > 0 {
|
||||
bytes.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
bytes.reverse();
|
||||
if bytes[0] & 0x80 != 0 {
|
||||
bytes.insert(0, 0);
|
||||
}
|
||||
}
|
||||
tlv(0x02, &bytes)
|
||||
}
|
||||
|
||||
fn der_octet_string(bytes: &[u8]) -> Vec<u8> {
|
||||
tlv(0x04, bytes)
|
||||
}
|
||||
|
||||
fn der_bit_string_from_prefix(prefix_addr: &[u8], prefix_len: u16) -> Vec<u8> {
|
||||
let byte_len = ((prefix_len as usize) + 7) / 8;
|
||||
let unused = (byte_len * 8) as u16 - prefix_len;
|
||||
let mut bytes = prefix_addr[..byte_len.min(prefix_addr.len())].to_vec();
|
||||
while bytes.len() < byte_len {
|
||||
bytes.push(0);
|
||||
}
|
||||
if !bytes.is_empty() && unused != 0 {
|
||||
let mask = (1u8 << (unused as u8)) - 1;
|
||||
let last = bytes.len() - 1;
|
||||
bytes[last] &= !mask;
|
||||
}
|
||||
let mut content = vec![unused as u8];
|
||||
content.extend(bytes);
|
||||
tlv(0x03, &content)
|
||||
}
|
||||
|
||||
fn der_sequence(children: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
let mut content = Vec::new();
|
||||
for c in children {
|
||||
content.extend(c);
|
||||
}
|
||||
tlv(0x30, &content)
|
||||
}
|
||||
|
||||
fn roa_ip_address(prefix_bs: Vec<u8>) -> Vec<u8> {
|
||||
der_sequence(vec![prefix_bs])
|
||||
}
|
||||
|
||||
fn roa_ip_family(afi: [u8; 2], addresses: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
der_sequence(vec![der_octet_string(&afi), der_sequence(addresses)])
|
||||
}
|
||||
|
||||
fn roa_attestation(as_id: u64, families: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
der_sequence(vec![der_integer_u64(as_id), der_sequence(families)])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_sorts_families_sorts_and_dedups_addresses() {
|
||||
// Build:
|
||||
// - families are in order: IPv6 then IPv4 (should become IPv4 then IPv6)
|
||||
// - IPv4 addresses contain a duplicate (should dedup)
|
||||
// - IPv4 /24 uses only 3 bytes, should be padded to 4 with host bits cleared
|
||||
let v6 = roa_ip_family(
|
||||
[0, 2],
|
||||
vec![roa_ip_address(der_bit_string_from_prefix(
|
||||
&[0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
32,
|
||||
))],
|
||||
);
|
||||
|
||||
let v4_prefix = der_bit_string_from_prefix(&[192, 0, 2, 255], 24);
|
||||
let v4 = roa_ip_family(
|
||||
[0, 1],
|
||||
vec![
|
||||
roa_ip_address(v4_prefix.clone()),
|
||||
roa_ip_address(v4_prefix), // duplicate
|
||||
],
|
||||
);
|
||||
|
||||
let der = roa_attestation(64496, vec![v6, v4]);
|
||||
let roa = RoaEContent::decode_der(&der).expect("decode roa econtent");
|
||||
|
||||
assert_eq!(roa.ip_addr_blocks.len(), 2);
|
||||
assert_eq!(roa.ip_addr_blocks[0].afi, RoaAfi::Ipv4);
|
||||
assert_eq!(roa.ip_addr_blocks[1].afi, RoaAfi::Ipv6);
|
||||
|
||||
let v4_fam = &roa.ip_addr_blocks[0];
|
||||
assert_eq!(v4_fam.addresses.len(), 1);
|
||||
assert_eq!(
|
||||
v4_fam.addresses[0].prefix,
|
||||
IpPrefix {
|
||||
afi: RoaAfi::Ipv4,
|
||||
prefix_len: 24,
|
||||
addr: vec![192, 0, 2, 0],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
33
tests/test_roa_decode.rs
Normal file
33
tests/test_roa_decode.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use rpki::data_model::roa::{RoaDecodeError, RoaObject};
|
||||
|
||||
#[test]
|
||||
fn decode_roa_fixture_smoke() {
|
||||
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");
|
||||
assert_eq!(
|
||||
roa.econtent_type,
|
||||
rpki::data_model::oid::OID_CT_ROUTE_ORIGIN_AUTHZ
|
||||
);
|
||||
assert_eq!(roa.roa.version, 0);
|
||||
assert_eq!(roa.roa.as_id, 4538);
|
||||
assert!(!roa.roa.ip_addr_blocks.is_empty());
|
||||
assert!(roa
|
||||
.roa
|
||||
.ip_addr_blocks
|
||||
.iter()
|
||||
.all(|f| !f.addresses.is_empty()));
|
||||
println!("{roa:#?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rejects_non_roa_econtent_type() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
)
|
||||
.expect("read MFT fixture");
|
||||
let err = RoaObject::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidEContentType(_)));
|
||||
}
|
||||
205
tests/test_roa_econtent_decode_errors.rs
Normal file
205
tests/test_roa_econtent_decode_errors.rs
Normal file
@ -0,0 +1,205 @@
|
||||
use rpki::data_model::roa::{RoaDecodeError, RoaEContent};
|
||||
|
||||
fn len_bytes(len: usize) -> Vec<u8> {
|
||||
if len < 128 {
|
||||
vec![len as u8]
|
||||
} else {
|
||||
let mut tmp = Vec::new();
|
||||
let mut n = len;
|
||||
while n > 0 {
|
||||
tmp.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
tmp.reverse();
|
||||
let mut out = vec![0x80 | (tmp.len() as u8)];
|
||||
out.extend(tmp);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
fn tlv(tag: u8, content: &[u8]) -> Vec<u8> {
|
||||
let mut out = vec![tag];
|
||||
out.extend(len_bytes(content.len()));
|
||||
out.extend_from_slice(content);
|
||||
out
|
||||
}
|
||||
|
||||
fn der_integer_u64(v: u64) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
let mut n = v;
|
||||
if n == 0 {
|
||||
bytes.push(0);
|
||||
} else {
|
||||
while n > 0 {
|
||||
bytes.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
bytes.reverse();
|
||||
if bytes[0] & 0x80 != 0 {
|
||||
bytes.insert(0, 0);
|
||||
}
|
||||
}
|
||||
tlv(0x02, &bytes)
|
||||
}
|
||||
|
||||
fn der_octet_string(bytes: &[u8]) -> Vec<u8> {
|
||||
tlv(0x04, bytes)
|
||||
}
|
||||
|
||||
fn der_bit_string(unused: u8, bytes: &[u8]) -> Vec<u8> {
|
||||
let mut content = vec![unused];
|
||||
content.extend_from_slice(bytes);
|
||||
tlv(0x03, &content)
|
||||
}
|
||||
|
||||
fn der_sequence(children: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
let mut content = Vec::new();
|
||||
for c in children {
|
||||
content.extend(c);
|
||||
}
|
||||
tlv(0x30, &content)
|
||||
}
|
||||
|
||||
fn cs_explicit(tag_no: u8, inner_der: Vec<u8>) -> Vec<u8> {
|
||||
tlv(0xA0 | (tag_no & 0x1F), &inner_der)
|
||||
}
|
||||
|
||||
fn roa_ip_address(prefix_bs: Vec<u8>, max_len: Option<u64>) -> Vec<u8> {
|
||||
let mut fields = vec![prefix_bs];
|
||||
if let Some(m) = max_len {
|
||||
fields.push(der_integer_u64(m));
|
||||
}
|
||||
der_sequence(fields)
|
||||
}
|
||||
|
||||
fn roa_ip_family(afi: [u8; 2], addresses: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
der_sequence(vec![der_octet_string(&afi), der_sequence(addresses)])
|
||||
}
|
||||
|
||||
fn roa_attestation(version: Option<u64>, as_id: u64, families: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
let mut fields = Vec::new();
|
||||
if let Some(v) = version {
|
||||
fields.push(cs_explicit(0, der_integer_u64(v)));
|
||||
}
|
||||
fields.push(der_integer_u64(as_id));
|
||||
fields.push(der_sequence(families));
|
||||
der_sequence(fields)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_must_be_zero_when_present() {
|
||||
let der = roa_attestation(
|
||||
Some(1),
|
||||
64496,
|
||||
vec![roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)])],
|
||||
);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidVersion(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_id_out_of_range_is_rejected() {
|
||||
let der = roa_attestation(
|
||||
None,
|
||||
(u32::MAX as u64) + 1,
|
||||
vec![roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)])],
|
||||
);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::AsIdOutOfRange(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_addr_blocks_len_is_validated() {
|
||||
let der = roa_attestation(None, 64496, vec![]);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidIpAddrBlocksLen(0)));
|
||||
|
||||
let families = vec![
|
||||
roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)]),
|
||||
roa_ip_family([0, 2], vec![roa_ip_address(der_bit_string(0, &[]), None)]),
|
||||
roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)]),
|
||||
];
|
||||
let der = roa_attestation(None, 64496, families);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidIpAddrBlocksLen(3)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_afi_is_rejected() {
|
||||
let families = vec![
|
||||
roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)]),
|
||||
roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)]),
|
||||
];
|
||||
let der = roa_attestation(None, 64496, families);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::DuplicateAfi(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_afi_is_rejected() {
|
||||
let der = roa_attestation(
|
||||
None,
|
||||
64496,
|
||||
vec![roa_ip_family([0x12, 0x34], vec![roa_ip_address(der_bit_string(0, &[]), None)])],
|
||||
);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::UnsupportedAfi(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_address_list_is_rejected() {
|
||||
let der = roa_attestation(None, 64496, vec![roa_ip_family([0, 1], vec![])]);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::EmptyAddressList));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix_unused_bits_must_be_zeroed() {
|
||||
// prefix_len=1 => unused_bits=7, last byte must have lower 7 bits zero.
|
||||
// Use 0b1000_0001 which has a non-zero unused bit.
|
||||
let bs = der_bit_string(7, &[0b1000_0001]);
|
||||
let der = roa_attestation(
|
||||
None,
|
||||
64496,
|
||||
vec![roa_ip_family([0, 1], vec![roa_ip_address(bs, None)])],
|
||||
);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidPrefixUnusedBits));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix_len_out_of_range_is_rejected() {
|
||||
// IPv4 ub=32, encode 33 bits: 5 bytes with unused_bits=7 => 40-7=33.
|
||||
let bs = der_bit_string(7, &[0u8; 5]);
|
||||
let der = roa_attestation(
|
||||
None,
|
||||
64496,
|
||||
vec![roa_ip_family([0, 1], vec![roa_ip_address(bs, None)])],
|
||||
);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::PrefixLenOutOfRange { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_length_range_and_relation_are_validated() {
|
||||
// IPv4, prefix_len=8
|
||||
let bs = der_bit_string(0, &[0x0A]);
|
||||
// maxLength < prefix_len
|
||||
let der = roa_attestation(
|
||||
None,
|
||||
64496,
|
||||
vec![roa_ip_family([0, 1], vec![roa_ip_address(bs.clone(), Some(7))])],
|
||||
);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidMaxLength { .. }));
|
||||
|
||||
// maxLength > ub
|
||||
let der = roa_attestation(
|
||||
None,
|
||||
64496,
|
||||
vec![roa_ip_family([0, 1], vec![roa_ip_address(bs, Some(33))])],
|
||||
);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidMaxLength { .. }));
|
||||
}
|
||||
|
||||
13
tests/test_roa_embedded_ee_cert.rs
Normal file
13
tests/test_roa_embedded_ee_cert.rs
Normal file
@ -0,0 +1,13 @@
|
||||
use rpki::data_model::roa::RoaObject;
|
||||
|
||||
#[test]
|
||||
fn roa_embedded_ee_cert_resources_validate() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa",
|
||||
)
|
||||
.expect("read ROA fixture");
|
||||
|
||||
let roa = RoaObject::decode_der(&der).expect("decode roa");
|
||||
roa.validate_embedded_ee_cert()
|
||||
.expect("roa EE cert resources must validate");
|
||||
}
|
||||
176
tests/test_roa_validate_ee_resources.rs
Normal file
176
tests/test_roa_validate_ee_resources.rs
Normal file
@ -0,0 +1,176 @@
|
||||
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<IpResourceSet>, as_resources: Option<AsResourceSet>) -> ResourceCertificate {
|
||||
ResourceCertificate {
|
||||
raw_der: vec![],
|
||||
tbs: RpkixTbsCertificate {
|
||||
version: 2,
|
||||
serial_number: BigUint::from(1u8),
|
||||
signature_algorithm: "1.2.840.113549.1.1.11".to_string(),
|
||||
issuer_dn: "CN=issuer".to_string(),
|
||||
subject_dn: "CN=subject".to_string(),
|
||||
validity_not_before: OffsetDateTime::UNIX_EPOCH,
|
||||
validity_not_after: OffsetDateTime::UNIX_EPOCH,
|
||||
subject_public_key_info: vec![],
|
||||
extensions: RcExtensions {
|
||||
basic_constraints_ca: false,
|
||||
subject_key_identifier: Some(vec![0x01]),
|
||||
subject_info_access: Some(SubjectInfoAccess::Ca(
|
||||
rpki::data_model::rc::SubjectInfoAccessCa {
|
||||
access_descriptions: vec![],
|
||||
},
|
||||
)),
|
||||
certificate_policies_oid: None,
|
||||
ip_resources,
|
||||
as_resources,
|
||||
},
|
||||
},
|
||||
kind: ResourceCertKind::Ee,
|
||||
}
|
||||
}
|
||||
|
||||
fn test_roa_single_v4_prefix() -> RoaEContent {
|
||||
RoaEContent {
|
||||
version: 0,
|
||||
as_id: 64496,
|
||||
ip_addr_blocks: vec![RoaIpAddressFamily {
|
||||
afi: RoaAfi::Ipv4,
|
||||
addresses: vec![RoaIpAddress {
|
||||
prefix: rpki::data_model::roa::IpPrefix {
|
||||
afi: RoaAfi::Ipv4,
|
||||
prefix_len: 8,
|
||||
addr: vec![10, 0, 0, 0],
|
||||
},
|
||||
max_length: Some(24),
|
||||
}],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_accepts_when_prefix_is_covered() {
|
||||
let roa = test_roa_single_v4_prefix();
|
||||
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],
|
||||
},
|
||||
)]),
|
||||
}],
|
||||
}),
|
||||
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 = 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 = 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 = 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],
|
||||
},
|
||||
)]),
|
||||
}],
|
||||
}),
|
||||
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 roa = RoaEContent {
|
||||
version: 0,
|
||||
as_id: 64496,
|
||||
ip_addr_blocks: vec![RoaIpAddressFamily {
|
||||
afi: RoaAfi::Ipv4,
|
||||
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 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,
|
||||
);
|
||||
|
||||
roa.validate_against_ee_cert(&ee)
|
||||
.expect("160.18.0.0/16 should be covered by 160.0.0.0/9");
|
||||
}
|
||||
|
||||
14
tests/test_roa_verify.rs
Normal file
14
tests/test_roa_verify.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use rpki::data_model::roa::RoaObject;
|
||||
|
||||
#[test]
|
||||
fn verify_roa_cms_signature_with_embedded_ee_cert() {
|
||||
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.signed_object
|
||||
.verify_signature()
|
||||
.expect("ROA CMS signature should verify with embedded EE cert");
|
||||
}
|
||||
|
||||
@ -26,4 +26,5 @@ fn decode_manifest_signed_object_smoke() {
|
||||
.iter()
|
||||
.any(|u| u.starts_with("rsync://")));
|
||||
assert_eq!(so.signed_data.signer_infos.len(), 1);
|
||||
println!("{so:#?}")
|
||||
}
|
||||
|
||||
172
tests/test_ta_certificate.rs
Normal file
172
tests/test_ta_certificate.rs
Normal file
@ -0,0 +1,172 @@
|
||||
use der_parser::num_bigint::BigUint;
|
||||
use rpki::data_model::oid::OID_CP_IPADDR_ASNUMBER;
|
||||
use rpki::data_model::rc::{
|
||||
Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange,
|
||||
IpPrefix, RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate,
|
||||
};
|
||||
use rpki::data_model::ta::{TaCertificate, TaCertificateError};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
fn dummy_rc_ca(ext: RcExtensions) -> ResourceCertificate {
|
||||
let t = OffsetDateTime::from_unix_timestamp(0).unwrap();
|
||||
ResourceCertificate {
|
||||
raw_der: Vec::new(),
|
||||
kind: ResourceCertKind::Ca,
|
||||
tbs: RpkixTbsCertificate {
|
||||
version: 2,
|
||||
serial_number: BigUint::from(1u32),
|
||||
signature_algorithm: "1.2.840.113549.1.1.11".into(),
|
||||
issuer_dn: "CN=TA".into(),
|
||||
subject_dn: "CN=TA".into(),
|
||||
validity_not_before: t,
|
||||
validity_not_after: t,
|
||||
subject_public_key_info: Vec::new(),
|
||||
extensions: ext,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_certificate_from_der_parses_downloaded_fixtures() {
|
||||
let fixtures = [
|
||||
"tests/fixtures/ta/afrinic-ta.cer",
|
||||
"tests/fixtures/ta/apnic-ta.cer",
|
||||
"tests/fixtures/ta/arin-ta.cer",
|
||||
"tests/fixtures/ta/lacnic-ta.cer",
|
||||
"tests/fixtures/ta/ripe-ncc-ta.cer",
|
||||
];
|
||||
for path in fixtures {
|
||||
let der = std::fs::read(path).expect("read TA fixture");
|
||||
let ta = TaCertificate::from_der(&der).expect("parse TA fixture");
|
||||
assert_eq!(ta.rc_ca.kind, ResourceCertKind::Ca);
|
||||
assert_eq!(
|
||||
ta.rc_ca.tbs.extensions.certificate_policies_oid.as_deref(),
|
||||
Some(OID_CP_IPADDR_ASNUMBER)
|
||||
);
|
||||
assert!(ta.rc_ca.tbs.extensions.subject_key_identifier.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_certificate_rejects_non_self_signed_ca() {
|
||||
// This is a CA cert fixture, but not self-signed (issuer != subject).
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
)
|
||||
.expect("read CA cert fixture");
|
||||
assert!(matches!(
|
||||
TaCertificate::from_der(&der),
|
||||
Err(TaCertificateError::NotSelfSignedIssuerSubject)
|
||||
| Err(TaCertificateError::InvalidSelfSignature(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_constraints_require_policies_and_ski() {
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
basic_constraints_ca: true,
|
||||
subject_key_identifier: None,
|
||||
subject_info_access: None,
|
||||
certificate_policies_oid: None,
|
||||
ip_resources: Some(rpki::data_model::rc::IpResourceSet { families: vec![] }),
|
||||
as_resources: None,
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::MissingOrInvalidCertificatePolicies)
|
||||
));
|
||||
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
certificate_policies_oid: Some(OID_CP_IPADDR_ASNUMBER.to_string()),
|
||||
..rc.tbs.extensions.clone()
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::MissingSubjectKeyIdentifier)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_constraints_require_non_empty_resources_and_no_inherit() {
|
||||
// Missing both IP and AS resources.
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
basic_constraints_ca: true,
|
||||
subject_key_identifier: Some(vec![1]),
|
||||
subject_info_access: None,
|
||||
certificate_policies_oid: Some(OID_CP_IPADDR_ASNUMBER.to_string()),
|
||||
ip_resources: None,
|
||||
as_resources: None,
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::ResourcesMissing)
|
||||
));
|
||||
|
||||
// IP resources present but empty => resources empty.
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
ip_resources: Some(rpki::data_model::rc::IpResourceSet { families: vec![] }),
|
||||
..rc.tbs.extensions.clone()
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::ResourcesEmpty)
|
||||
));
|
||||
|
||||
// IP resources inherit is rejected.
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
ip_resources: Some(rpki::data_model::rc::IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::Inherit,
|
||||
}],
|
||||
}),
|
||||
..rc.tbs.extensions.clone()
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::IpResourcesInherit)
|
||||
));
|
||||
|
||||
// AS resources inherit is rejected.
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
ip_resources: None,
|
||||
as_resources: Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::Inherit),
|
||||
rdi: None,
|
||||
}),
|
||||
..rc.tbs.extensions.clone()
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::AsResourcesInherit)
|
||||
));
|
||||
|
||||
// Valid non-empty explicit IP resources => OK.
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
ip_resources: Some(rpki::data_model::rc::IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv6,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(IpPrefix {
|
||||
afi: Afi::Ipv6,
|
||||
prefix_len: 32,
|
||||
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
})]),
|
||||
}],
|
||||
}),
|
||||
as_resources: None,
|
||||
..rc.tbs.extensions.clone()
|
||||
});
|
||||
TaCertificate::validate_rc_constraints(&rc).expect("valid explicit resources");
|
||||
|
||||
// Valid non-empty explicit AS resources => OK.
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
ip_resources: None,
|
||||
as_resources: Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64512)])),
|
||||
rdi: None,
|
||||
}),
|
||||
..rc.tbs.extensions.clone()
|
||||
});
|
||||
TaCertificate::validate_rc_constraints(&rc).expect("valid explicit AS resources");
|
||||
}
|
||||
|
||||
31
tests/test_tal_decode.rs
Normal file
31
tests/test_tal_decode.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use der_parser::der::parse_der;
|
||||
use rpki::data_model::tal::Tal;
|
||||
|
||||
#[test]
|
||||
fn decode_tal_fixtures_smoke() {
|
||||
let fixtures = [
|
||||
"tests/fixtures/tal/afrinic.tal",
|
||||
"tests/fixtures/tal/apnic-rfc7730-https.tal",
|
||||
"tests/fixtures/tal/arin.tal",
|
||||
"tests/fixtures/tal/lacnic.tal",
|
||||
"tests/fixtures/tal/ripe-ncc.tal",
|
||||
];
|
||||
|
||||
for path in fixtures {
|
||||
let raw = std::fs::read(path).expect("read TAL fixture");
|
||||
let tal = Tal::decode_bytes(&raw).expect("decode TAL fixture");
|
||||
|
||||
assert!(!tal.ta_uris.is_empty(), "TA URI list must be non-empty: {path}");
|
||||
assert!(!tal.subject_public_key_info_der.is_empty(), "SPKI DER must be non-empty: {path}");
|
||||
|
||||
for u in &tal.ta_uris {
|
||||
assert!(matches!(u.scheme(), "rsync" | "https"), "scheme must be allowed: {u}");
|
||||
assert!(!u.path().ends_with('/'), "TA URI must not be a directory: {u}");
|
||||
}
|
||||
|
||||
// SPKI DER must be parseable as a DER object (typically a SEQUENCE).
|
||||
let (rem, _obj) = parse_der(&tal.subject_public_key_info_der).expect("parse spki DER");
|
||||
assert!(rem.is_empty(), "SPKI DER must not have trailing bytes: {path}");
|
||||
}
|
||||
}
|
||||
|
||||
71
tests/test_tal_decode_errors.rs
Normal file
71
tests/test_tal_decode_errors.rs
Normal file
@ -0,0 +1,71 @@
|
||||
use rpki::data_model::tal::{Tal, TalDecodeError};
|
||||
|
||||
fn mk_tal(uris: &[&str], b64_lines: &[&str]) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str("# comment\n");
|
||||
for u in uris {
|
||||
out.push_str(u);
|
||||
out.push('\n');
|
||||
}
|
||||
out.push('\n'); // separator
|
||||
for l in b64_lines {
|
||||
out.push_str(l);
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_missing_separator() {
|
||||
let s = "# c\nhttps://example.invalid/ta.cer\nAAAA\n";
|
||||
assert!(matches!(
|
||||
Tal::decode_bytes(s.as_bytes()),
|
||||
Err(TalDecodeError::MissingSeparatorEmptyLine)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_missing_uris() {
|
||||
let s = "# c\n\nAAAA\n";
|
||||
assert!(matches!(Tal::decode_bytes(s.as_bytes()), Err(TalDecodeError::MissingTaUris)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_unsupported_scheme() {
|
||||
let s = mk_tal(&["ftp://example.invalid/ta.cer"], &["AAAA"]);
|
||||
assert!(matches!(
|
||||
Tal::decode_bytes(s.as_bytes()),
|
||||
Err(TalDecodeError::UnsupportedUriScheme(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_directory_uri() {
|
||||
let s = mk_tal(&["https://example.invalid/dir/"], &["AAAA"]);
|
||||
assert!(matches!(
|
||||
Tal::decode_bytes(s.as_bytes()),
|
||||
Err(TalDecodeError::UriIsDirectory(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_comment_after_header() {
|
||||
let s = "# c\nhttps://example.invalid/ta.cer\n# late\n\nAAAA\n";
|
||||
assert!(matches!(Tal::decode_bytes(s.as_bytes()), Err(TalDecodeError::CommentAfterHeader)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_invalid_base64() {
|
||||
let s = mk_tal(&["https://example.invalid/ta.cer"], &["not-base64!!!"]);
|
||||
assert!(matches!(
|
||||
Tal::decode_bytes(s.as_bytes()),
|
||||
Err(TalDecodeError::SpkiBase64Decode)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_invalid_utf8() {
|
||||
let bytes = [0xFFu8, 0xFEu8];
|
||||
assert!(matches!(Tal::decode_bytes(&bytes), Err(TalDecodeError::InvalidUtf8)));
|
||||
}
|
||||
|
||||
62
tests/test_trust_anchor_bind.rs
Normal file
62
tests/test_trust_anchor_bind.rs
Normal file
@ -0,0 +1,62 @@
|
||||
use rpki::data_model::ta::{TrustAnchor, TrustAnchorError};
|
||||
use rpki::data_model::tal::Tal;
|
||||
use url::Url;
|
||||
|
||||
#[test]
|
||||
fn bind_trust_anchor_with_downloaded_fixtures_succeeds() {
|
||||
let cases = [
|
||||
("tests/fixtures/tal/afrinic.tal", "tests/fixtures/ta/afrinic-ta.cer"),
|
||||
(
|
||||
"tests/fixtures/tal/apnic-rfc7730-https.tal",
|
||||
"tests/fixtures/ta/apnic-ta.cer",
|
||||
),
|
||||
("tests/fixtures/tal/arin.tal", "tests/fixtures/ta/arin-ta.cer"),
|
||||
("tests/fixtures/tal/lacnic.tal", "tests/fixtures/ta/lacnic-ta.cer"),
|
||||
("tests/fixtures/tal/ripe-ncc.tal", "tests/fixtures/ta/ripe-ncc-ta.cer"),
|
||||
];
|
||||
|
||||
for (tal_path, ta_path) in cases {
|
||||
let tal_raw = std::fs::read(tal_path).expect("read TAL fixture");
|
||||
let tal = Tal::decode_bytes(&tal_raw).expect("decode TAL fixture");
|
||||
let ta_der = std::fs::read(ta_path).expect("read TA fixture");
|
||||
|
||||
TrustAnchor::bind(tal.clone(), &ta_der, None).expect("bind without resolved uri");
|
||||
|
||||
// Also exercise the resolved-uri-in-TAL check using one URI from the TAL list.
|
||||
let resolved = tal
|
||||
.ta_uris
|
||||
.iter()
|
||||
.find(|u| u.scheme() == "https")
|
||||
.or_else(|| tal.ta_uris.first())
|
||||
.expect("tal has ta uris");
|
||||
let resolved = resolved.clone();
|
||||
TrustAnchor::bind(tal, &ta_der, Some(&resolved)).expect("bind with resolved uri");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_rejects_spki_mismatch() {
|
||||
let tal_raw = std::fs::read("tests/fixtures/tal/ripe-ncc.tal").expect("read TAL fixture");
|
||||
let mut tal = Tal::decode_bytes(&tal_raw).expect("decode TAL fixture");
|
||||
let ta_der = std::fs::read("tests/fixtures/ta/ripe-ncc-ta.cer").expect("read TA fixture");
|
||||
|
||||
// Flip a byte in TAL SPKI to force mismatch.
|
||||
tal.subject_public_key_info_der[0] ^= 0x01;
|
||||
assert!(matches!(
|
||||
TrustAnchor::bind(tal, &ta_der, None),
|
||||
Err(TrustAnchorError::TalSpkiMismatch)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_rejects_resolved_uri_not_listed_in_tal() {
|
||||
let tal_raw = std::fs::read("tests/fixtures/tal/afrinic.tal").expect("read TAL fixture");
|
||||
let tal = Tal::decode_bytes(&tal_raw).expect("decode TAL fixture");
|
||||
let ta_der = std::fs::read("tests/fixtures/ta/afrinic-ta.cer").expect("read TA fixture");
|
||||
|
||||
let bad = Url::parse("https://example.invalid/not-in-tal.cer").unwrap();
|
||||
assert!(matches!(
|
||||
TrustAnchor::bind(tal, &ta_der, Some(&bad)),
|
||||
Err(TrustAnchorError::ResolvedUriNotInTal(_))
|
||||
));
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user