Compare commits

..

4 Commits

Author SHA1 Message Date
cc9f3f21de 增加rc,完成所有模型的解析,优化error code 的RFC引用 2026-02-03 16:50:52 +08:00
56ae2ca4fc 增加aspa对象解析 2026-02-02 15:42:30 +08:00
bcd4829486 add signed object, manifest impl. add coverage script 2026-02-02 15:42:01 +08:00
421847d329 增加RC数据结构和资源集合数据结构 (#1)
Co-authored-by: xiuting.xu <xiutingxt.xu@gmail.com>
Reviewed-on: #1
Reviewed-by: yuyr <yuyr@zgclab.edu.cn>
Co-authored-by: xuxt <xuxt@zgclab.edu.cn>
Co-committed-by: xuxt <xuxt@zgclab.edu.cn>
2026-02-02 15:37:05 +08:00
79 changed files with 5279 additions and 147 deletions

View File

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

View File

@ -182,6 +182,8 @@ RFC 引用RFC 5280 §4.2.2.1RFC 5280 §4.2.2.2RFC 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 §6RFC 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.2RPKI 约束见 RFC 6487 §4.8.8 |
| `1.3.6.1.5.5.7.48.2` | AIA accessMethod: id-ad-caIssuers | RFC 5280 §4.2.2.1 |

94
specs/01_tal.md Normal file
View File

@ -0,0 +1,94 @@
# 01. TALTrust Anchor Locator
## 1.1 对象定位
TALTrust Anchor Locator用于向 RP 提供:
1) 可检索“当前 TA 证书”的一个或多个 URI以及
2) 该 TA 证书的 `subjectPublicKeyInfo`SPKI期望值用于绑定/防替换)。
RFC 8630 §2RFC 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 SPKIBase64
- `subjectPublicKeyInfo` 以 DER 编码ASN.1)后,再 Base64 编码表示。RFC 8630 §2.2(第 4 点)。
- 为避免长行Base64 字符串中 **MAY** 插入换行。RFC 8630 §2.2。
- SPKI ASN.1 类型来自 X.509 / RFC 5280。RFC 8630 §2.2(第 4 点RFC 5280 §4.1.2.7。
#### 1.2.4.1 `SubjectPublicKeyInfo` 的 ASN.1 定义RFC 5280 §4.1
TAL 中携带的是一个 X.509 `SubjectPublicKeyInfo` 的 DER 字节串(再 Base64。其 ASN.1 定义如下RFC 5280 §4.1。
```asn1
SubjectPublicKeyInfo ::= SEQUENCE {
algorithm AlgorithmIdentifier,
subjectPublicKey BIT STRING }
```
其中 `algorithm`/`subjectPublicKey` 的取值受 RPKI 算法 profile 约束(例如 RSA 2048 + SHA-256 等SKI/AKI 计算仍用 SHA-1。RFC 5280 §4.1.2.7RFC 7935 §2-§3.1RFC 6487 §4.8.2-§4.8.3。
## 1.3 解析规则(语义层)
输入:`TalFileBytes: bytes`
解析步骤:
1) 按 `LF` / `CRLF` 识别行。RFC 8630 §2.2。
2) 从文件开头读取所有以 `#` 开头的行,作为 `comments`(保留去掉 `#` 后的 UTF-8 文本或保留原始行均可,但需保持 UTF-8。RFC 8630 §2.2。
3) 继续读取一行或多行非空行,作为 `ta_uris`保持顺序。RFC 8630 §2.2(第 2 点)。
4) 读取一个空行必须存在。RFC 8630 §2.2(第 3 点)。
5) 将剩余行拼接为 Base64 文本移除行分隔Base64 解码得到 `subject_public_key_info_der`。RFC 8630 §2.2(第 4 点)。
6) 可选:将 `subject_public_key_info_der` 解析为 X.509 `SubjectPublicKeyInfo` 结构(用于与 TA 证书比对。RFC 8630 §2.3RFC 5280 §4.1.2.7。
URI 解析与约束:
- `ta_uris[*]` 的 scheme **MUST**`rsync``https`。RFC 8630 §2.2。
- 每个 `ta_uri` **MUST** 指向“单个对象”,且 **MUST NOT** 指向目录或集合。RFC 8630 §2.3。
## 1.4 抽象数据模型(接口)
### 1.4.1 `Tal`
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|---|---|---|---|---|
| `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 解码所得 DERBase64 中可有换行 | 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。

View File

@ -0,0 +1,88 @@
# 02. TATrust Anchor自签名证书
## 2.1 对象定位
在 RP 侧“信任锚Trust Anchor, TA”以一个**自签名 CA 资源证书**体现,其可获取位置与期望公钥由 TAL 提供。RFC 8630 §2.3。
本文件描述两个紧密相关的数据对象:
1) `TaCertificate`TA 自签名资源证书本体X.509 DER
2) `TrustAnchor`:语义组合对象(`TAL` + `TaCertificate` 的绑定语义)
## 2.2 原始载体与编码
- 载体X.509 证书(通常以 `.cer` 存放于仓库,但文件扩展名不作为语义依据)。
- 编码DER。TA 证书必须符合 RPKI 资源证书 profile。RFC 8630 §2.3RFC 6487 §4。
### 2.2.1 X.509 Certificate 的 ASN.1 定义RFC 5280 §4.1TA 与 RC 共享)
TA 证书与普通资源证书RC在编码层面都是 X.509 `Certificate`DER。其 ASN.1 定义如下RFC 5280 §4.1。
```asn1
Certificate ::= SEQUENCE {
tbsCertificate TBSCertificate,
signatureAlgorithm AlgorithmIdentifier,
signatureValue BIT STRING }
```
其中 `tbsCertificate.extensions`v3 扩展)是 RPKI 语义的主要承载处IP/AS 资源扩展、SIA/AIA/CRLDP 等。RFC 5280 §4.1RPKI 对字段/扩展存在性与关键性约束见 RFC 6487 §4。
> 说明:更完整的 RC 编码层结构(包括 Extension 外层“extnValue 二次 DER 解码”的套娃方式)在 `03_resource_certificate_rc.md``00_common_types.md` 中给出。
## 2.3 TA 证书的 RPKI 语义约束(在 RC profile 基础上额外强调)
### 2.3.1 自签名与 profile
- TA URI 指向的对象 **MUST** 是一个**自签名 CA 证书**,并且 **MUST** 符合 RPKI 证书 profile。RFC 8630 §2.3RFC 6487 §4。
- 自签名证书在 RC profile 下的通用差异(例如 CRLDP/AIA 的省略规则、AKI 的规则)见 RFC 6487。RFC 6487 §4.8.3RFC 6487 §4.8.6RFC 6487 §4.8.7。
### 2.3.2 INRIP/AS 资源扩展)在 TA 上的额外约束
- TA 的 INR 扩展IP/AS 资源扩展RFC 3779**MUST** 是非空资源集合。RFC 8630 §2.3。
- TA 的 INR 扩展 **MUST NOT** 使用 `inherit` 形式。RFC 8630 §2.3。
- 说明:一般 RC profile 允许 `inherit`。RFC 6487 §4.8.10RFC 6487 §4.8.11RFC 3779 §2.2.3.5RFC 3779 §3.2.3.3。
### 2.3.3 TAL ↔ TA 公钥绑定
- 用于验证 TA 的公钥(来自 TAL 中的 SPKI**MUST** 与 TA 证书中的 `subjectPublicKeyInfo` 相同。RFC 8630 §2.3。
### 2.3.4 TA 稳定性语义(实现需建模为“约束/假设”,但不属于验证结果态)
- TA 公钥与 TAL 中公钥必须保持稳定(用于 RP 侧长期信任锚。RFC 8630 §2.3。
### 2.3.5 TA 与 CRL/Manifest 的关系(语义)
- RFC 8630 指出TA 为自签名证书,没有对应 CRL且不会被 manifest 列出TA 的获取/轮换由 TAL 控制。RFC 8630 §2.3。
> 注:这条更偏“发布/运维语义”,但对数据对象建模有影响:`TrustAnchor` 组合对象不应依赖 CRL/MFT 的存在。
## 2.4 抽象数据模型(接口)
### 2.4.1 `TaCertificate`
> 该对象在字段层面复用 `RC(CA)` 的语义模型(见 `03_resource_certificate_rc.md`),但增加 TA 特有约束。
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|---|---|---|---|---|
| `raw_der` | `DerBytes` | TA 证书 DER | X.509 DER证书 profile 约束见 RC 文档 | RFC 8630 §2.3RFC 6487 §4 |
| `rc_ca` | `ResourceCaCertificate` | 以 RC(CA) 语义解析出的字段集合 | 必须满足“自签名 CA”分支约束且 INR 必须非空且不允许 inherit | RFC 8630 §2.3RFC 6487 §4RFC 3779 §2/§3 |
### 2.4.2 `TrustAnchor`
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|---|---|---|---|---|
| `tal` | `Tal` | TAL 文件语义对象 | 见 `01_tal.md` | RFC 8630 §2.2 |
| `ta_certificate` | `TaCertificate` | TA 证书语义对象 | TA URI 指向的对象 | RFC 8630 §2.3 |
| `tal_spki_der` | `DerBytes` | 从 TAL 解析出的 SPKI DER | `tal.subject_public_key_info_der` | RFC 8630 §2.2 |
| `ta_spki_der` | `DerBytes` | 从 TA 证书抽取的 SPKI DER | `ta_certificate``subjectPublicKeyInfo` | RFC 8630 §2.3RFC 5280 §4.1.2.7 |
**绑定约束(字段级)**
- `tal_spki_der` 必须与 `ta_spki_der` 完全相等(字节层面的 DER 等价。RFC 8630 §2.3。
## 2.5 字段级约束清单(实现对照)
- TA URI 指向的对象必须是自签名 CA 证书,且符合 RPKI 证书 profile。RFC 8630 §2.3RFC 6487 §4。
- TA 的 INR 扩展必须非空,且不得使用 inherit。RFC 8630 §2.3。
- TAL 中 SPKI 必须与 TA 证书的 `subjectPublicKeyInfo` 匹配。RFC 8630 §2.3。
- TA 不依赖 CRL/MFT无对应 CRL且不被 manifest 列出。RFC 8630 §2.3。

View File

@ -0,0 +1,460 @@
# 03. RCResource Certificate资源证书CA/EE
## 3.1 对象定位
资源证书RC是 X.509 v3 证书,遵循 PKIX profileRFC 5280并受 RPKI profile 进一步约束。RFC 6487 §4。
RC 在 RPKI 中至少分为两类语义用途:
- `CA 证书`:签发下级证书/CRL并在 SIA 中声明发布点与 manifest。RFC 6487 §4.8.8.1。
- `EE 证书`:用于验证某个 RPKI Signed Object如 ROA/MFT在 SIA 中指向被验证对象。RFC 6487 §4.8.8.2。
## 3.2 原始载体与编码
- 载体X.509 证书。
- 编码DER。RFC 6487 §4“valid X.509 public key certificate consistent with RFC 5280” + RPKI 限制)。
### 3.2.1 X.509 v3 证书基本语法ASN.1RFC 5280 §4.1
资源证书在编码层面是 RFC 5280 定义的 X.509 v3 `Certificate`DER其中 `tbsCertificate` 携带主体字段与扩展集合(`Extensions`。RFC 5280 §4.1。
```asn1
Certificate ::= SEQUENCE {
tbsCertificate TBSCertificate,
signatureAlgorithm AlgorithmIdentifier,
signatureValue BIT STRING }
TBSCertificate ::= SEQUENCE {
version [0] EXPLICIT Version DEFAULT v1,
serialNumber CertificateSerialNumber,
signature AlgorithmIdentifier,
issuer Name,
validity Validity,
subject Name,
subjectPublicKeyInfo SubjectPublicKeyInfo,
issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
-- If present, version MUST be v2 or v3
subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
-- If present, version MUST be v2 or v3
extensions [3] EXPLICIT Extensions OPTIONAL
-- If present, version MUST be v3
}
Version ::= INTEGER { v1(0), v2(1), v3(2) }
CertificateSerialNumber ::= INTEGER
Validity ::= SEQUENCE {
notBefore Time,
notAfter Time }
Time ::= CHOICE {
utcTime UTCTime,
generalTime GeneralizedTime }
UniqueIdentifier ::= BIT STRING
SubjectPublicKeyInfo ::= SEQUENCE {
algorithm AlgorithmIdentifier,
subjectPublicKey BIT STRING }
Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension
Extension ::= SEQUENCE {
extnID OBJECT IDENTIFIER,
critical BOOLEAN DEFAULT FALSE,
extnValue OCTET STRING
-- contains the DER encoding of an ASN.1 value
-- corresponding to the extension type identified
-- by extnID
}
```
### 3.2.2 AlgorithmIdentifierASN.1RFC 5280 §4.1.1.2
```asn1
AlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL }
```
### 3.2.3 Name / DN 结构ASN.1RFC 5280 §4.1.2.4
```asn1
Name ::= CHOICE { -- only one possibility for now --
rdnSequence RDNSequence }
RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
RelativeDistinguishedName ::=
SET SIZE (1..MAX) OF AttributeTypeAndValue
AttributeTypeAndValue ::= SEQUENCE {
type AttributeType,
value AttributeValue }
AttributeType ::= OBJECT IDENTIFIER
AttributeValue ::= ANY -- DEFINED BY AttributeType
DirectoryString ::= CHOICE {
teletexString TeletexString (SIZE (1..MAX)),
printableString PrintableString (SIZE (1..MAX)),
universalString UniversalString (SIZE (1..MAX)),
utf8String UTF8String (SIZE (1..MAX)),
bmpString BMPString (SIZE (1..MAX)) }
```
### 3.2.4 GeneralNames / GeneralNameASN.1RFC 5280 §4.2.1.6
> 说明RPKI 的 AIA/SIA/CRLDP 等扩展通常把 URI 编码在 `uniformResourceIdentifier [6] IA5String` 分支中。RFC 5280 §4.2.1.6。
```asn1
GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
GeneralName ::= CHOICE {
otherName [0] OtherName,
rfc822Name [1] IA5String,
dNSName [2] IA5String,
x400Address [3] ORAddress,
directoryName [4] Name,
ediPartyName [5] EDIPartyName,
uniformResourceIdentifier [6] IA5String,
iPAddress [7] OCTET STRING,
registeredID [8] OBJECT IDENTIFIER }
OtherName ::= SEQUENCE {
type-id OBJECT IDENTIFIER,
value [0] EXPLICIT ANY DEFINED BY type-id }
EDIPartyName ::= SEQUENCE {
nameAssigner [0] DirectoryString OPTIONAL,
partyName [1] DirectoryString }
```
### 3.2.5 AIAAuthority Information AccessASN.1RFC 5280 §4.2.2.1
```asn1
id-pe-authorityInfoAccess OBJECT IDENTIFIER ::= { id-pe 1 }
AuthorityInfoAccessSyntax ::=
SEQUENCE SIZE (1..MAX) OF AccessDescription
AccessDescription ::= SEQUENCE {
accessMethod OBJECT IDENTIFIER,
accessLocation GeneralName }
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
id-ad-caIssuers OBJECT IDENTIFIER ::= { id-ad 2 }
```
### 3.2.6 SIASubject Information AccessASN.1RFC 5280 §4.2.2.2
```asn1
id-pe-subjectInfoAccess OBJECT IDENTIFIER ::= { id-pe 11 }
SubjectInfoAccessSyntax ::=
SEQUENCE SIZE (1..MAX) OF AccessDescription
AccessDescription ::= SEQUENCE {
accessMethod OBJECT IDENTIFIER,
accessLocation GeneralName }
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
id-ad-caRepository OBJECT IDENTIFIER ::= { id-ad 5 }
```
### 3.2.7 RPKI 在 SIA 中新增/使用的 accessMethod OIDRFC 6487 §4.8.8.1 / §4.8.8.2RFC 8182 §3.2
> 说明:下列 OID 用于 `AccessDescription.accessMethod`,并放在 SIA 的 `extnValue` 内层结构中(其外层 extnID 仍为 SIA`id-pe-subjectInfoAccess`。RFC 6487 §4.8.8RFC 8182 §3.2。
```asn1
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
id-ad-rpkiManifest OBJECT IDENTIFIER ::= { id-ad 10 } -- 1.3.6.1.5.5.7.48.10
id-ad-signedObject OBJECT IDENTIFIER ::= { id-ad 11 } -- 1.3.6.1.5.5.7.48.11
id-ad-rpkiNotify OBJECT IDENTIFIER ::= { id-ad 13 } -- 1.3.6.1.5.5.7.48.13
```
### 3.2.8 CRLDistributionPointsCRLDPASN.1RFC 5280 §4.2.1.13
```asn1
id-ce-cRLDistributionPoints OBJECT IDENTIFIER ::= { id-ce 31 }
CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint
DistributionPoint ::= SEQUENCE {
distributionPoint [0] DistributionPointName OPTIONAL,
reasons [1] ReasonFlags OPTIONAL,
cRLIssuer [2] GeneralNames OPTIONAL }
DistributionPointName ::= CHOICE {
fullName [0] GeneralNames,
nameRelativeToCRLIssuer [1] RelativeDistinguishedName }
ReasonFlags ::= BIT STRING {
unused (0),
keyCompromise (1),
cACompromise (2),
affiliationChanged (3),
superseded (4),
cessationOfOperation (5),
certificateHold (6),
privilegeWithdrawn (7),
aACompromise (8) }
```
### 3.2.9 Certificate PoliciesASN.1RFC 5280 §4.2.1.4
```asn1
id-ce-certificatePolicies OBJECT IDENTIFIER ::= { id-ce 32 }
anyPolicy OBJECT IDENTIFIER ::= { id-ce-certificatePolicies 0 }
certificatePolicies ::= SEQUENCE SIZE (1..MAX) OF PolicyInformation
PolicyInformation ::= SEQUENCE {
policyIdentifier CertPolicyId,
policyQualifiers SEQUENCE SIZE (1..MAX) OF
PolicyQualifierInfo OPTIONAL }
CertPolicyId ::= OBJECT IDENTIFIER
PolicyQualifierInfo ::= SEQUENCE {
policyQualifierId PolicyQualifierId,
qualifier ANY DEFINED BY policyQualifierId }
-- policyQualifierIds for Internet policy qualifiers
id-qt OBJECT IDENTIFIER ::= { id-pkix 2 }
id-qt-cps OBJECT IDENTIFIER ::= { id-qt 1 }
id-qt-unotice OBJECT IDENTIFIER ::= { id-qt 2 }
PolicyQualifierId ::= OBJECT IDENTIFIER ( id-qt-cps | id-qt-unotice )
Qualifier ::= CHOICE {
cPSuri CPSuri,
userNotice UserNotice }
CPSuri ::= IA5String
```
### 3.2.10 RFC 3779 IP/AS 资源扩展ASN.1RFC 3779 §2.2.1-§2.2.3RFC 3779 §3.2.1-§3.2.3
> 说明RFC 3779 给出两个扩展的 OID 与 ASN.1 语法;它们作为 X.509 v3 扩展出现在 `extensions` 中(外层 extnID 为下列 OID。RPKI profile 进一步约束 criticality/SAFI/RDI 等,见 RFC 6487 §4.8.10-§4.8.11。
```asn1
-- IP Address Delegation Extension
id-pe-ipAddrBlocks OBJECT IDENTIFIER ::= { id-pe 7 }
IPAddrBlocks ::= SEQUENCE OF IPAddressFamily
IPAddressFamily ::= SEQUENCE { -- AFI & optional SAFI --
addressFamily OCTET STRING (SIZE (2..3)),
ipAddressChoice IPAddressChoice }
IPAddressChoice ::= CHOICE {
inherit NULL, -- inherit from issuer --
addressesOrRanges SEQUENCE OF IPAddressOrRange }
IPAddressOrRange ::= CHOICE {
addressPrefix IPAddress,
addressRange IPAddressRange }
IPAddressRange ::= SEQUENCE {
min IPAddress,
max IPAddress }
IPAddress ::= BIT STRING
-- Autonomous System Identifier Delegation Extension
id-pe-autonomousSysIds OBJECT IDENTIFIER ::= { id-pe 8 }
ASIdentifiers ::= SEQUENCE {
asnum [0] EXPLICIT ASIdentifierChoice OPTIONAL,
rdi [1] EXPLICIT ASIdentifierChoice OPTIONAL}
ASIdentifierChoice ::= CHOICE {
inherit NULL, -- inherit from issuer --
asIdsOrRanges SEQUENCE OF ASIdOrRange }
ASIdOrRange ::= CHOICE {
id ASId,
range ASRange }
ASRange ::= SEQUENCE {
min ASId,
max ASId }
ASId ::= INTEGER
```
### 3.2.11 其它 RPKI profile 相关扩展的 ASN.1 定义RFC 5280 §4.2.1.1-§4.2.1.3RFC 5280 §4.2.1.9RFC 5280 §4.2.1.12
> 说明:这些是 RPKI 资源证书 profileRFC 6487 §4.8)所引用的通用 PKIX 扩展语法。RPKI 对其“必须/禁止/criticality/字段允许性”有额外限制(见本文件 3.3/3.4),但编码层的 ASN.1 类型来自 RFC 5280。
```asn1
id-ce-authorityKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 35 }
AuthorityKeyIdentifier ::= SEQUENCE {
keyIdentifier [0] KeyIdentifier OPTIONAL,
authorityCertIssuer [1] GeneralNames OPTIONAL,
authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL }
KeyIdentifier ::= OCTET STRING
id-ce-subjectKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 14 }
SubjectKeyIdentifier ::= KeyIdentifier
id-ce-keyUsage OBJECT IDENTIFIER ::= { id-ce 15 }
KeyUsage ::= BIT STRING {
digitalSignature (0),
nonRepudiation (1), -- recent editions of X.509 have
-- renamed this bit to contentCommitment
keyEncipherment (2),
dataEncipherment (3),
keyAgreement (4),
keyCertSign (5),
cRLSign (6),
encipherOnly (7),
decipherOnly (8) }
id-ce-basicConstraints OBJECT IDENTIFIER ::= { id-ce 19 }
BasicConstraints ::= SEQUENCE {
cA BOOLEAN DEFAULT FALSE,
pathLenConstraint INTEGER (0..MAX) OPTIONAL }
id-ce-extKeyUsage OBJECT IDENTIFIER ::= { id-ce 37 }
ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId
KeyPurposeId ::= OBJECT IDENTIFIER
```
## 3.3 抽象数据模型(接口)
> 说明:本模型面向“语义化解析产物”。实现可保留 `raw_der` 作为可追溯入口。
### 3.3.1 顶层联合类型:`ResourceCertificate`
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|---|---|---|---|---|
| `raw_der` | `DerBytes` | 证书 DER | 原样保留(建议) | RFC 6487 §4 |
| `tbs` | `RpkixTbsCertificate` | 证书语义字段(见下) | 仅允许 RFC 6487 允许的字段/扩展;其他字段 MUST NOT 出现 | RFC 6487 §4 |
| `kind` | `enum { ca, ee }` | 语义分类 | 来自 BasicConstraints + 用途约束 | RFC 6487 §4.8.1RFC 6487 §4.8.8 |
### 3.3.1.1 派生类型(用于字段类型标注)
为避免在其它对象文档里反复写“`ResourceCertificate``kind==...`”,这里定义两个派生/别名类型:
- `ResourceCaCertificate``ResourceCertificate``kind == ca`
- `ResourceEeCertificate``ResourceCertificate``kind == ee`
这些派生类型不引入新字段,只是对 `ResourceCertificate.kind` 的约束化视图。
### 3.3.2 `RpkixTbsCertificate`(语义字段集合)
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|---|---|---|---|---|
| `version` | `int` | X.509 版本 | MUST 为 v3字段值为 2 | RFC 6487 §4.1 |
| `serial_number` | `int` | 序列号 | 正整数;对每 CA 签发唯一 | RFC 6487 §4.2 |
| `signature_algorithm` | `Oid` | 证书签名算法 | 必须为 `sha256WithRSAEncryption``1.2.840.113549.1.1.11` | RFC 6487 §4.3RFC 7935 §2引用 RFC 4055 |
| `issuer_dn` | `RpkixDistinguishedName` | 颁发者 DN | 必含 1 个 CommonName可含 1 个 serialNumberCN 必须 PrintableString | RFC 6487 §4.4 |
| `subject_dn` | `RpkixDistinguishedName` | 主体 DN | 同 issuer 约束;且对同一 issuer 下“实体+公钥”唯一 | RFC 6487 §4.5 |
| `validity_not_before` | `UtcTime` | 有效期起 | X.509 `Time`UTCTime/GeneralizedTime解析为 UTC 时间点 | RFC 6487 §4.6.1RFC 5280 §4.1.2.5 |
| `validity_not_after` | `UtcTime` | 有效期止 | X.509 `Time`UTCTime/GeneralizedTime解析为 UTC 时间点 | RFC 6487 §4.6.2RFC 5280 §4.1.2.5 |
| `subject_public_key_info` | `DerBytes` | SPKI DER | 算法 profile 指定 | RFC 6487 §4.7RFC 7935 §3.1 |
| `extensions` | `RpkixExtensions` | 扩展集合 | 见下表criticality/存在性/内容受约束 | RFC 6487 §4.8 |
### 3.3.3 `RpkixDistinguishedName`RPKI profile 下的 DN 语义)
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|---|---|---|---|---|
| `common_name` | `string` | CommonName (CN) | MUST 存在且仅 1 个;类型为 PrintableString | RFC 6487 §4.4RFC 6487 §4.5 |
| `serial_number` | `optional[string]` | serialNumber | MAY 存在且仅 1 个 | RFC 6487 §4.4RFC 6487 §4.5 |
| `rfc4514` | `string` | DN 的 RFC4514 字符串表示 | 便于日志/索引(实现自选) | RFC 6487 §4.5(引用 RFC4514 |
### 3.3.4 `RpkixExtensions`(核心扩展语义)
> 表中 “存在性/criticality” 指 RPKI profile 下对该扩展的要求;实现应能区分 “字段缺失” 与 “字段存在但不符合约束”。
| 字段 | 类型 | 语义 | 存在性/criticality 与内容约束 | RFC 引用 |
|---|---|---|---|---|
| `basic_constraints` | `optional[BasicConstraints]` | CA 标志 | **extnID=`2.5.29.19`**CA 证书MUST present & criticalEEMUST NOT presentpathLen MUST NOT present | RFC 6487 §4.8.1RFC 5280 §4.2.1.9 |
| `subject_key_identifier` | `bytes` | SKI | **extnID=`2.5.29.14`**MUST present & non-critical值为 subjectPublicKey 的 DER bit string 的 SHA-1 哈希 | RFC 6487 §4.8.2(引用 RFC 5280 §4.2.1.2 |
| `authority_key_identifier` | `optional[AuthorityKeyIdentifier]` | AKI | **extnID=`2.5.29.35`**自签名MAY present 且可等于 SKI非自签名MUST presentauthorityCertIssuer/authorityCertSerialNumber MUST NOT presentnon-critical | RFC 6487 §4.8.3RFC 5280 §4.2.1.1 |
| `key_usage` | `KeyUsage` | KeyUsage | **extnID=`2.5.29.15`**MUST present & criticalCA`keyCertSign``cRLSign` 为 TRUEEE`digitalSignature` 为 TRUE | RFC 6487 §4.8.4RFC 5280 §4.2.1.3 |
| `extended_key_usage` | `optional[OidSet]` | EKU | **extnID=`2.5.29.37`**CAMUST NOT appear用于验证 RPKI 对象的 EEMUST NOT appear若出现不得标 critical | RFC 6487 §4.8.5RFC 5280 §4.2.1.12 |
| `crl_distribution_points` | `optional[CrlDistributionPoints]` | CRLDP | **extnID=`2.5.29.31`**自签名MUST be omitted非自签名MUST present & non-critical仅 1 个 DistributionPointfullName URI必须包含至少 1 个 `rsync://` | RFC 6487 §4.8.6RFC 5280 §4.2.1.13 |
| `authority_info_access` | `optional[AuthorityInfoAccess]` | AIA | **extnID=`1.3.6.1.5.5.7.1.1`**自签名MUST be omitted非自签名MUST present & non-critical必须含 accessMethod `id-ad-caIssuers`(**`1.3.6.1.5.5.7.48.2`**) 的 `rsync://` URI可含同对象其它 URI | RFC 6487 §4.8.7RFC 5280 §4.2.2.1 |
| `subject_info_access_ca` | `optional[SubjectInfoAccessCa]` | SIACA | **extnID=`1.3.6.1.5.5.7.1.11`**CAMUST present & non-critical必须含 accessMethod `id-ad-caRepository`(**`1.3.6.1.5.5.7.48.5`**)`rsync://` 目录 URI`id-ad-rpkiManifest`(**`1.3.6.1.5.5.7.48.10`**)`rsync://` 对象 URI若 CA 使用 RRDP还会包含 `id-ad-rpkiNotify`(**`1.3.6.1.5.5.7.48.13`**)HTTPS Notification URI | RFC 6487 §4.8.8.1RFC 5280 §4.2.2.2RFC 8182 §3.2 |
| `subject_info_access_ee` | `optional[SubjectInfoAccessEe]` | SIAEE | **extnID=`1.3.6.1.5.5.7.1.11`**EEMUST present & non-critical必须含 accessMethod `id-ad-signedObject`(**`1.3.6.1.5.5.7.48.11`**)URI **MUST include** `rsync://`EE 的 SIA 不允许其它 AccessMethods | RFC 6487 §4.8.8.2RFC 5280 §4.2.2.2 |
| `certificate_policies` | `CertificatePolicies` | 证书策略 | **extnID=`2.5.29.32`**MUST present & critical恰好 1 个 policy并允许 0 或 1 个 CPS qualifier若存在其 id 必为 `id-qt-cps`(**`1.3.6.1.5.5.7.2.1`**) | RFC 6487 §4.8.9RFC 7318 §2RFC 5280 §4.2.1.4 |
| `ip_resources` | `optional[IpResourceSet]` | IP 资源扩展 | **extnID=`1.3.6.1.5.5.7.1.7`**IP/AS 两者至少其一 MUST present若 present MUST be critical内容为 RFC 3779 语义;在公用互联网场景 SAFI MUST NOT 使用;且必须为非空或 inherit | RFC 6487 §4.8.10RFC 3779 §2.2.1RFC 3779 §2.2.2 |
| `as_resources` | `optional[AsResourceSet]` | AS 资源扩展 | **extnID=`1.3.6.1.5.5.7.1.8`**IP/AS 两者至少其一 MUST present若 present MUST be critical内容为 RFC 3779 语义RDI MUST NOT 使用;且必须为非空或 inherit | RFC 6487 §4.8.11RFC 3779 §3.2.1RFC 3779 §3.2.2 |
### 3.3.5 结构化子类型(建议)
#### `BasicConstraints`
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|---|---|---|---|---|
| `ca` | `bool` | 是否 CA | 由 issuer 决定;在 CA 证书中该扩展必须存在 | RFC 6487 §4.8.1 |
| `path_len_constraint` | `None` | pathLenConstraint | MUST NOT presentRPKI profile 不使用) | RFC 6487 §4.8.1 |
#### `AuthorityKeyIdentifier`
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|---|---|---|---|---|
| `key_identifier` | `bytes` | AKI.keyIdentifier | 使用 issuer 公钥的 SHA-1 哈希(按 RFC 5280 的定义) | RFC 6487 §4.8.3(引用 RFC 5280 §4.2.1.1 |
| `authority_cert_issuer` | `None` | authorityCertIssuer | MUST NOT present | RFC 6487 §4.8.3 |
| `authority_cert_serial_number` | `None` | authorityCertSerialNumber | MUST NOT present | RFC 6487 §4.8.3 |
#### `CrlDistributionPoints`
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|---|---|---|---|---|
| `distribution_point_uris` | `list[Uri]` | CRL 位置列表 | 仅 1 个 DistributionPoint必须包含至少 1 个 `rsync://` URI 指向该 issuer 最新 CRL可含其它 URI | RFC 6487 §4.8.6 |
#### `AuthorityInfoAccess`
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|---|---|---|---|---|
| `ca_issuers_uris` | `list[Uri]` | 上级 CA 证书位置 | accessMethod=`id-ad-caIssuers``1.3.6.1.5.5.7.48.2`);必含 `rsync://` URI可含同对象其它 URI | RFC 6487 §4.8.7RFC 5280 §4.2.2.1 |
#### `SubjectInfoAccessCa`
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|---|---|---|---|---|
| `ca_repository_uris` | `list[Uri]` | CA 发布点目录repository publication point | accessMethod=`id-ad-caRepository``1.3.6.1.5.5.7.48.5`);至少 1 个;必须包含 `rsync://`;也可包含其它机制(例如 `https://`)作为“同一目录”的替代访问方式;顺序表示 CA 偏好 | RFC 6487 §4.8.8.1RFC 5280 §4.2.2.2 |
| `rpki_manifest_uris` | `list[Uri]` | 当前 manifest 对象 URI | accessMethod=`id-ad-rpkiManifest``1.3.6.1.5.5.7.48.10`);至少 1 个;必须包含 `rsync://`;也可包含其它机制(例如 `https://`)作为“同一对象”的替代访问方式 | RFC 6487 §4.8.8.1RFC 5280 §4.2.2.2 |
| `rpki_notify_uris` | `optional[list[Uri]]` | RRDP NotificationUpdate Notification FileURI | accessMethod=`id-ad-rpkiNotify``1.3.6.1.5.5.7.48.13`);若存在则 accessLocation MUST 为 `https://` URI指向 RRDP Notification 文件 | RFC 8182 §3.2RFC 5280 §4.2.2.2 |
#### `SubjectInfoAccessEe`
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|---|---|---|---|---|
| `signed_object_uris` | `list[Uri]` | 被 EE 证书验证的签名对象位置 | accessMethod=`id-ad-signedObject``1.3.6.1.5.5.7.48.11`);必须包含 `rsync://`;其它 URI 可作为同对象替代机制EE SIA 不允许其它 AccessMethods | RFC 6487 §4.8.8.2RFC 5280 §4.2.2.2 |
#### `CertificatePolicies`
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|---|---|---|---|---|
| `policy_oid` | `Oid` | 唯一 policy OID | 恰好 1 个 policyRPKI CP 分配的 OID 为 `id-cp-ipAddr-asNumber``1.3.6.1.5.5.7.14.2` | RFC 6487 §4.8.9RFC 6484 §1.2 |
| `cps_uri` | `optional[Uri]` | CPS policy qualifier URI | MAY 存在且最多 1 个;若存在其 `policyQualifierId` 必为 `id-qt-cps`;对该 URI 不施加处理要求 | RFC 7318 §2RFC 5280 §4.2.1.4 |
## 3.4 字段级约束清单(实现对照)
- 仅允许 RFC 6487 §4 指定的字段/扩展;未列出字段 MUST NOT 出现。RFC 6487 §4。
- 证书版本必须为 v3。RFC 6487 §4.1。
- CA/EE 在 BasicConstraints 与 SIA 的约束不同。RFC 6487 §4.8.1RFC 6487 §4.8.8.1RFC 6487 §4.8.8.2。
- KeyUsageCA 仅 `keyCertSign`/`cRLSign`EE 仅 `digitalSignature`。RFC 6487 §4.8.4。
- CRLDP/AIA自签名必须省略非自签名必须存在并包含 `rsync://`。RFC 6487 §4.8.6RFC 6487 §4.8.7。
- IP/AS 资源扩展:两者至少其一存在;若存在必须 critical语义来自 RFC 3779在公用互联网场景 SAFI 与 RDI 均不得使用。RFC 6487 §4.8.10RFC 6487 §4.8.11RFC 3779 §2.2.3RFC 3779 §3.2.3。

View File

@ -102,5 +102,9 @@ FileAndHash ::= SEQUENCE {
Manifest 使用“one-time-use EE certificate”进行签名验证规范对该 EE 证书的使用方式给出约束:
- Manifest 相关 EE 证书应为 one-time-use每次新 manifest 生成新密钥对/新 EE。RFC 9286 §4Section 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.1RFC 3779。
- 若证书包含 **AS Identifier Delegation Extension**ASN delegation内容必须为 `inherit`(不得显式列 ASID/range。RFC 9286 §5.1RFC 3779。
- 另外按资源证书 profile资源证书 **MUST** 至少包含上述两类扩展之一(也可两者都有),且这些扩展 **MUST** 标记为 critical。RFC 6487 §2。
- 用于验证 manifest 的 EE 证书 **MUST** 具有与 `thisUpdate..nextUpdate` 区间一致的有效期,以避免 CRL 无谓增长。RFC 9286 §4.2.1manifestNumber 段落前的说明)。
- 替换 manifest 时CA 必须撤销旧 manifest 对应 EE 证书;且若新 manifest 早于旧 manifest 的 nextUpdate 发行,则 CA **MUST** 同时发行新 CRL 撤销旧 manifest EE。RFC 9286 §4.2.1nextUpdate 段落末RFC 9286 §5.1(生成步骤)。

100
specs/08_aspa.md Normal file
View File

@ -0,0 +1,100 @@
# 08. ASPAAutonomous System Provider Authorization
## 8.1 对象定位
ASPAAutonomous 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-§3RFC 9589 §4。
- eContentType`id-ct-ASPA`OID `1.2.840.113549.1.9.16.1.49``draft-ietf-sidrops-aspa-profile-21` §2。
- eContentDER 编码 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 objectCMS 外壳见 `05_signed_object_cms.md`)。其 `eContentType``eContent`payload的 ASN.1 定义见 `draft-ietf-sidrops-aspa-profile-21` §2-§3。
**eContentTypeOID**`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) }
```
**eContentASPA 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 §3RFC 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 §3RFC 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.2ASID 定义) |
| `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。

View File

@ -0,0 +1,87 @@
# 09. Ghostbusters RecordGBR
## 9.1 对象定位
Ghostbusters RecordGBR是一个可选的 RPKI Signed Object用于承载“联系人信息”人类可读的联系渠道以便在证书过期、CRL 失效、密钥轮换等事件中能够联系到维护者。RFC 6493 §1。
GBR 由 CMS 外壳 + vCard 载荷组成:
- 外壳RFC 6488更新RFC 9589
- 载荷payloadRFC 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 §6RFC 6493 §9.1。
- eContent一个 OCTET STRING其 octets 是 vCard 文本vCard 4.0,且受 RFC 6493 的 profile 约束。RFC 6493 §5RFC 6493 §6。
> 说明:与 ROA/MFT 这类“eContent 内部再 DER 解码为 ASN.1 结构”的对象不同GBR 的 eContent 语义上就是“vCard 文本内容本身”(由 Signed Object Template 的 `eContent OCTET STRING` 承载。RFC 6493 §6。
## 9.3 vCard profileRFC 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 §3RFC 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 §5RFC 6493 §7。
4) 通过校验后,将允许属性映射为 `GhostbustersVCard` 语义对象(见下)。
## 9.5 抽象数据模型(接口)
### 9.5.1 `GhostbustersObject`
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|---|---|---|---|---|
| `signed_object` | `RpkiSignedObject` | CMS 外壳 | 外壳约束见 RFC 6488/9589 | RFC 6493 §6RFC 6488 §3RFC 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 §5RFC 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 §5RFC 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
View 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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View 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
View 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/ta/afrinic-ta.cer vendored Normal file

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

BIN
tests/fixtures/ta/ripe-ncc-ta.cer vendored Normal file

Binary file not shown.

10
tests/fixtures/tal/afrinic.tal vendored Normal file
View File

@ -0,0 +1,10 @@
rsync://rpki.afrinic.net/repository/AfriNIC.cer
https://rpki.afrinic.net/repository/AfriNIC.cer
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxsAqAhWIO+ON2Ef9oRDM
pKxv+AfmSLIdLWJtjrvUyDxJPBjgR+kVrOHUeTaujygFUp49tuN5H2C1rUuQavTH
vve6xNF5fU3OkTcqEzMOZy+ctkbde2SRMVdvbO22+TH9gNhKDc9l7Vu01qU4LeJH
k3X0f5uu5346YrGAOSv6AaYBXVgXxa0s9ZvgqFpim50pReQe/WI3QwFKNgpPzfQL
6Y7fDPYdYaVOXPXSKtx7P4s4KLA/ZWmRL/bobw/i2fFviAGhDrjqqqum+/9w1hEl
L/vqihVnV18saKTnLvkItA/Bf5i11Yhw2K7qv573YWxyuqCknO/iYLTR1DToBZcZ
UQIDAQAB

View File

@ -0,0 +1,10 @@
https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer
rsync://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9RWSL61YAAYumEiU8z8
qH2ETVIL01ilxZlzIL9JYSORMN5Cmtf8V2JblIealSqgOTGjvSjEsiV73s67zYQI
7C/iSOb96uf3/s86NqbxDiFQGN8qG7RNcdgVuUlAidl8WxvLNI8VhqbAB5uSg/Mr
LeSOvXRja041VptAxIhcGzDMvlAJRwkrYK/Mo8P4E2rSQgwqCgae0ebY1CsJ3Cjf
i67C1nw7oXqJJovvXJ4apGmEv8az23OLC6Ki54Ul/E6xk227BFttqFV3YMtKx42H
cCcDVZZy01n7JjzvO8ccaXmHIgR7utnqhBRNNq5Xc5ZhbkrUsNtiJmrZzVlgU6Ou
0wIDAQAB

19
tests/fixtures/tal/arin.tal vendored Normal file
View File

@ -0,0 +1,19 @@
# THIS TRUST ANCHOR LOCATOR IS PROVIDED BY THE AMERICAN REGISTRY FOR
# INTERNET NUMBERS (ARIN) "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL ARIN BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS PUBLIC KEY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
rsync://rpki.arin.net/repository/arin-rpki-ta.cer
https://rrdp.arin.net/arin-rpki-ta.cer
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3lZPjbHvMRV5sDDqfLc/685th5FnreHMJjg8
pEZUbG8Y8TQxSBsDebbsDpl3Ov3Cj1WtdrJ3CIfQODCPrrJdOBSrMATeUbPC+JlNf2SRP3UB+VJFgtTj
0RN8cEYIuhBW5t6AxQbHhdNQH+A1F/OJdw0q9da2U29Lx85nfFxvnC1EpK9CbLJS4m37+RlpNbT1cba+
b+loXpx0Qcb1C4UpJCGDy7uNf5w6/+l7RpATAHqqsX4qCtwwDYlbHzp2xk9owF3mkCxzl0HwncO+sEHH
eaL3OjtwdIGrRGeHi2Mpt+mvWHhtQqVG+51MHTyg+nIjWFKKGx1Q9+KDx4wJStwveQIDAQAB

4
tests/fixtures/tal/lacnic.tal vendored Normal file
View File

@ -0,0 +1,4 @@
https://rrdp.lacnic.net/ta/rta-lacnic-rpki.cer
rsync://repository.lacnic.net/rpki/lacnic/rta-lacnic-rpki.cer
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqZEzhYK0+PtDOPfub/KRc3MeWx3neXx4/wbnJWGbNAtbYqXg3uU5J4HFzPgk/VIppgSKAhlO0H60DRP48by9gr5/yDHu2KXhOmnMg46sYsUIpfgtBS9+VtrqWziJfb+pkGtuOWeTnj6zBmBNZKK+5AlMCW1WPhrylIcB+XSZx8tk9GS/3SMQ+YfMVwwAyYjsex14Uzto4GjONALE5oh1M3+glRQduD6vzSwOD+WahMbc9vCOTED+2McLHRKgNaQf0YJ9a1jG9oJIvDkKXEqdfqDRktwyoD74cV57bW3tBAexB7GglITbInyQAsmdngtfg2LUMrcROHHP86QPZINjDQIDAQAB

10
tests/fixtures/tal/ripe-ncc.tal vendored Normal file
View File

@ -0,0 +1,10 @@
https://rpki.ripe.net/ta/ripe-ncc-ta.cer
rsync://rpki.ripe.net/ta/ripe-ncc-ta.cer
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0URYSGqUz2myBsOzeW1j
Q6NsxNvlLMyhWknvnl8NiBCs/T/S2XuNKQNZ+wBZxIgPPV2pFBFeQAvoH/WK83Hw
A26V2siwm/MY2nKZ+Olw+wlpzlZ1p3Ipj2eNcKrmit8BwBC8xImzuCGaV0jkRB0G
Z0hoH6Ml03umLprRsn6v0xOP0+l6Qc1ZHMFVFb385IQ7FQQTcVIxrdeMsoyJq9eM
kE6DoclHhF/NlSllXubASQ9KUWqJ0+Ot3QCXr4LXECMfkpkVR2TZT+v5v658bHVs
6ZxRD1b6Uk1uQKAyHUbn/tXvP8lrjAibGzVsXDT2L0x4Edx+QdixPgOji3gBMyL2
VwIDAQAB

25
tests/test_aspa_decode.rs Normal file
View 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(_)));
}

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

View File

@ -0,0 +1,14 @@
use rpki::data_model::aspa::AspaObject;
#[test]
fn aspa_embedded_ee_cert_resources_validate() {
let der = std::fs::read(
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa",
)
.expect("read ASPA fixture");
let aspa = AspaObject::decode_der(&der).expect("decode aspa");
aspa.validate_embedded_ee_cert()
.expect("aspa EE cert resources must validate");
}

View File

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

View File

@ -0,0 +1,14 @@
use rpki::data_model::manifest::ManifestObject;
#[test]
fn manifest_embedded_ee_cert_resources_validate() {
let der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
)
.expect("read MFT fixture");
let mft = ManifestObject::decode_der(&der).expect("decode manifest");
mft.validate_embedded_ee_cert()
.expect("manifest EE cert resources must validate");
}

View File

@ -0,0 +1,498 @@
use rpki::data_model::aspa::{AspaEContent, AspaObject};
use rpki::data_model::crl::{CrlExtensions, RevokedCert, RpkixCrl};
use rpki::data_model::manifest::{FileAndHash, ManifestEContent, ManifestObject};
use rpki::data_model::rc::{
AsResourceSet, IpResourceSet, RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate,
SubjectInfoAccess,
};
use rpki::data_model::roa::{RoaEContent, RoaIpAddressFamily, RoaObject};
use rpki::data_model::signed_object::{
EncapsulatedContentInfo, ResourceEeCertificate, RpkiSignedObject, SignedAttrsProfiled, SignedDataProfiled,
SignerInfoProfiled,
};
use rpki::data_model::ta::{TaCertificate, TrustAnchor};
use rpki::data_model::tal::Tal;
use sha2::{Digest, Sha256};
#[derive(Clone, Debug, PartialEq, Eq)]
struct BytesFmt {
len: usize,
sha256_hex: String,
head_hex: String,
tail_hex: String,
}
fn bytes_fmt(bytes: &[u8]) -> BytesFmt {
let len = bytes.len();
let sha = Sha256::digest(bytes);
let head_len = len.min(16);
let tail_len = len.min(16);
let head = &bytes[..head_len];
let tail = &bytes[len.saturating_sub(tail_len)..];
BytesFmt {
len,
sha256_hex: hex::encode(sha),
head_hex: hex::encode(head),
tail_hex: hex::encode(tail),
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct TalPretty {
raw: BytesFmt,
comments: Vec<String>,
ta_uris: Vec<String>,
subject_public_key_info_der: BytesFmt,
}
impl From<&Tal> for TalPretty {
fn from(v: &Tal) -> Self {
Self {
raw: bytes_fmt(&v.raw),
comments: v.comments.clone(),
ta_uris: v.ta_uris.iter().map(|u| u.to_string()).collect(),
subject_public_key_info_der: bytes_fmt(&v.subject_public_key_info_der),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct TaCertificatePretty {
raw_der: BytesFmt,
rc_ca: ResourceCertificatePretty,
}
impl From<&TaCertificate> for TaCertificatePretty {
fn from(v: &TaCertificate) -> Self {
Self {
raw_der: bytes_fmt(&v.raw_der),
rc_ca: ResourceCertificatePretty::from(&v.rc_ca),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct TrustAnchorPretty {
tal: TalPretty,
ta_certificate: TaCertificatePretty,
resolved_ta_uri: Option<String>,
}
impl From<&TrustAnchor> for TrustAnchorPretty {
fn from(v: &TrustAnchor) -> Self {
Self {
tal: TalPretty::from(&v.tal),
ta_certificate: TaCertificatePretty::from(&v.ta_certificate),
resolved_ta_uri: v.resolved_ta_uri.as_ref().map(|u| u.to_string()),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct ResourceCertificatePretty {
raw_der: BytesFmt,
tbs: RpkixTbsCertificatePretty,
kind: ResourceCertKind,
}
impl From<&ResourceCertificate> for ResourceCertificatePretty {
fn from(v: &ResourceCertificate) -> Self {
Self {
raw_der: bytes_fmt(&v.raw_der),
tbs: RpkixTbsCertificatePretty::from(&v.tbs),
kind: v.kind,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct RpkixTbsCertificatePretty {
version: u32,
serial_number: String,
signature_algorithm: String,
issuer_dn: String,
subject_dn: String,
validity_not_before: time::OffsetDateTime,
validity_not_after: time::OffsetDateTime,
subject_public_key_info: BytesFmt,
extensions: RcExtensionsPretty,
}
impl From<&RpkixTbsCertificate> for RpkixTbsCertificatePretty {
fn from(v: &RpkixTbsCertificate) -> Self {
Self {
version: v.version,
serial_number: hex::encode(v.serial_number.to_bytes_be()),
signature_algorithm: v.signature_algorithm.clone(),
issuer_dn: v.issuer_dn.clone(),
subject_dn: v.subject_dn.clone(),
validity_not_before: v.validity_not_before,
validity_not_after: v.validity_not_after,
subject_public_key_info: bytes_fmt(&v.subject_public_key_info),
extensions: RcExtensionsPretty::from(&v.extensions),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct RcExtensionsPretty {
basic_constraints_ca: bool,
subject_key_identifier: Option<BytesFmt>,
subject_info_access: Option<SubjectInfoAccess>,
certificate_policies_oid: Option<String>,
ip_resources: Option<IpResourceSet>,
as_resources: Option<AsResourceSet>,
}
impl From<&RcExtensions> for RcExtensionsPretty {
fn from(v: &RcExtensions) -> Self {
Self {
basic_constraints_ca: v.basic_constraints_ca,
subject_key_identifier: v.subject_key_identifier.as_ref().map(|b| bytes_fmt(b)),
subject_info_access: v.subject_info_access.clone(),
certificate_policies_oid: v.certificate_policies_oid.clone(),
ip_resources: v.ip_resources.clone(),
as_resources: v.as_resources.clone(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct RpkiSignedObjectPretty {
raw_der: BytesFmt,
content_info_content_type: String,
signed_data: SignedDataProfiledPretty,
}
impl From<&RpkiSignedObject> for RpkiSignedObjectPretty {
fn from(v: &RpkiSignedObject) -> Self {
Self {
raw_der: bytes_fmt(&v.raw_der),
content_info_content_type: v.content_info_content_type.clone(),
signed_data: SignedDataProfiledPretty::from(&v.signed_data),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct SignedDataProfiledPretty {
version: u32,
digest_algorithms: Vec<String>,
encap_content_info: EncapsulatedContentInfoPretty,
certificates: Vec<ResourceEeCertificatePretty>,
crls_present: bool,
signer_infos: Vec<SignerInfoProfiledPretty>,
}
impl From<&SignedDataProfiled> for SignedDataProfiledPretty {
fn from(v: &SignedDataProfiled) -> Self {
Self {
version: v.version,
digest_algorithms: v.digest_algorithms.clone(),
encap_content_info: EncapsulatedContentInfoPretty::from(&v.encap_content_info),
certificates: v
.certificates
.iter()
.map(ResourceEeCertificatePretty::from)
.collect(),
crls_present: v.crls_present,
signer_infos: v.signer_infos.iter().map(SignerInfoProfiledPretty::from).collect(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct EncapsulatedContentInfoPretty {
econtent_type: String,
econtent: BytesFmt,
}
impl From<&EncapsulatedContentInfo> for EncapsulatedContentInfoPretty {
fn from(v: &EncapsulatedContentInfo) -> Self {
Self {
econtent_type: v.econtent_type.clone(),
econtent: bytes_fmt(&v.econtent),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct ResourceEeCertificatePretty {
raw_der: BytesFmt,
subject_key_identifier: BytesFmt,
spki_der: BytesFmt,
sia_signed_object_uris: Vec<String>,
resource_cert: ResourceCertificatePretty,
}
impl From<&ResourceEeCertificate> for ResourceEeCertificatePretty {
fn from(v: &ResourceEeCertificate) -> Self {
Self {
raw_der: bytes_fmt(&v.raw_der),
subject_key_identifier: bytes_fmt(&v.subject_key_identifier),
spki_der: bytes_fmt(&v.spki_der),
sia_signed_object_uris: v.sia_signed_object_uris.clone(),
resource_cert: ResourceCertificatePretty::from(&v.resource_cert),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct SignerInfoProfiledPretty {
version: u32,
sid_ski: BytesFmt,
digest_algorithm: String,
signature_algorithm: String,
signed_attrs: SignedAttrsProfiledPretty,
unsigned_attrs_present: bool,
signature: BytesFmt,
signed_attrs_der_for_signature: BytesFmt,
}
impl From<&SignerInfoProfiled> for SignerInfoProfiledPretty {
fn from(v: &SignerInfoProfiled) -> Self {
Self {
version: v.version,
sid_ski: bytes_fmt(&v.sid_ski),
digest_algorithm: v.digest_algorithm.clone(),
signature_algorithm: v.signature_algorithm.clone(),
signed_attrs: SignedAttrsProfiledPretty::from(&v.signed_attrs),
unsigned_attrs_present: v.unsigned_attrs_present,
signature: bytes_fmt(&v.signature),
signed_attrs_der_for_signature: bytes_fmt(&v.signed_attrs_der_for_signature),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct SignedAttrsProfiledPretty {
content_type: String,
message_digest: BytesFmt,
signing_time: rpki::data_model::common::Asn1TimeUtc,
other_attrs_present: bool,
}
impl From<&SignedAttrsProfiled> for SignedAttrsProfiledPretty {
fn from(v: &SignedAttrsProfiled) -> Self {
Self {
content_type: v.content_type.clone(),
message_digest: bytes_fmt(&v.message_digest),
signing_time: v.signing_time,
other_attrs_present: v.other_attrs_present,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct ManifestObjectPretty {
signed_object: RpkiSignedObjectPretty,
econtent_type: String,
manifest: ManifestEContentPretty,
}
impl From<&ManifestObject> for ManifestObjectPretty {
fn from(v: &ManifestObject) -> Self {
Self {
signed_object: RpkiSignedObjectPretty::from(&v.signed_object),
econtent_type: v.econtent_type.clone(),
manifest: ManifestEContentPretty::from(&v.manifest),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct ManifestEContentPretty {
version: u32,
manifest_number: String,
this_update: time::OffsetDateTime,
next_update: time::OffsetDateTime,
file_hash_alg: String,
files: Vec<FileAndHashPretty>,
}
impl From<&ManifestEContent> for ManifestEContentPretty {
fn from(v: &ManifestEContent) -> Self {
Self {
version: v.version,
manifest_number: v.manifest_number.to_hex_upper(),
this_update: v.this_update,
next_update: v.next_update,
file_hash_alg: v.file_hash_alg.clone(),
files: v.files.iter().map(FileAndHashPretty::from).collect(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct FileAndHashPretty {
file_name: String,
hash_hex: String,
}
impl From<&FileAndHash> for FileAndHashPretty {
fn from(v: &FileAndHash) -> Self {
Self {
file_name: v.file_name.clone(),
hash_hex: hex::encode(&v.hash_bytes),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct RoaObjectPretty {
signed_object: RpkiSignedObjectPretty,
econtent_type: String,
roa: RoaEContentPretty,
}
impl From<&RoaObject> for RoaObjectPretty {
fn from(v: &RoaObject) -> Self {
Self {
signed_object: RpkiSignedObjectPretty::from(&v.signed_object),
econtent_type: v.econtent_type.clone(),
roa: RoaEContentPretty::from(&v.roa),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct RoaEContentPretty {
version: u32,
as_id: u32,
ip_addr_blocks: Vec<RoaIpAddressFamily>,
}
impl From<&RoaEContent> for RoaEContentPretty {
fn from(v: &RoaEContent) -> Self {
Self {
version: v.version,
as_id: v.as_id,
ip_addr_blocks: v.ip_addr_blocks.clone(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct AspaObjectPretty {
signed_object: RpkiSignedObjectPretty,
econtent_type: String,
aspa: AspaEContent,
}
impl From<&AspaObject> for AspaObjectPretty {
fn from(v: &AspaObject) -> Self {
Self {
signed_object: RpkiSignedObjectPretty::from(&v.signed_object),
econtent_type: v.econtent_type.clone(),
aspa: v.aspa.clone(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct RpkixCrlPretty {
raw_der: BytesFmt,
version: u32,
issuer_dn: String,
signature_algorithm_oid: String,
this_update: rpki::data_model::common::Asn1TimeUtc,
next_update: rpki::data_model::common::Asn1TimeUtc,
revoked_certs: Vec<RevokedCert>,
extensions: CrlExtensions,
}
impl From<&RpkixCrl> for RpkixCrlPretty {
fn from(v: &RpkixCrl) -> Self {
Self {
raw_der: bytes_fmt(&v.raw_der),
version: v.version,
issuer_dn: v.issuer_dn.clone(),
signature_algorithm_oid: v.signature_algorithm_oid.clone(),
this_update: v.this_update,
next_update: v.next_update,
revoked_certs: v.revoked_certs.clone(),
extensions: v.extensions.clone(),
}
}
}
#[test]
fn print_all_models_from_real_fixtures() {
// Note: run this test with `cargo test --test test_model_print_real_fixtures -- --nocapture`
// to see the output.
let tal_path = "tests/fixtures/tal/ripe-ncc.tal";
println!("== TAL / TA / TrustAnchor ==");
println!("Fixture (TAL): {tal_path}");
let tal_raw = std::fs::read(tal_path).expect("read TAL fixture");
let tal = Tal::decode_bytes(&tal_raw).expect("decode TAL");
let ta_path = "tests/fixtures/ta/ripe-ncc-ta.cer";
println!("Fixture (TA): {ta_path}");
let ta_der = std::fs::read(ta_path).expect("read TA fixture");
let ta = TaCertificate::from_der(&ta_der).expect("parse TA cert");
let resolved = tal
.ta_uris
.iter()
.find(|u| u.scheme() == "https")
.or_else(|| tal.ta_uris.first())
.cloned()
.expect("tal has at least one uri");
let trust_anchor = TrustAnchor::bind(tal, &ta_der, Some(&resolved)).expect("bind trust anchor");
println!("{:#?}", TalPretty::from(&trust_anchor.tal));
println!("{:#?}", TaCertificatePretty::from(&ta));
println!("{:#?}", TrustAnchorPretty::from(&trust_anchor));
println!();
println!("== ResourceCertificate (example non-TA CA cert) ==");
let ca_path =
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer";
println!("Fixture (CA cert): {ca_path}");
let ca_der = std::fs::read(ca_path).expect("read CA cert fixture");
let ca_rc = ResourceCertificate::from_der(&ca_der).expect("parse CA resource certificate");
println!("{:#?}", ResourceCertificatePretty::from(&ca_rc));
println!();
println!("== Signed Object / Manifest ==");
let mft_path =
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
println!("Fixture (MFT): {mft_path}");
let mft_der = std::fs::read(mft_path).expect("read MFT fixture");
let mft_obj = ManifestObject::decode_der(&mft_der).expect("decode manifest object");
println!("{:#?}", ManifestObjectPretty::from(&mft_obj));
println!("Manifest.validate_embedded_ee_cert={:?}", mft_obj.validate_embedded_ee_cert());
println!("Manifest.verify_signature={:?}", mft_obj.signed_object.verify_signature());
println!();
println!("== Signed Object / ROA ==");
let roa_path = "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa";
println!("Fixture (ROA): {roa_path}");
let roa_der = std::fs::read(roa_path).expect("read ROA fixture");
let roa_obj = RoaObject::decode_der(&roa_der).expect("decode ROA object");
println!("{:#?}", RoaObjectPretty::from(&roa_obj));
println!("ROA.validate_embedded_ee_cert={:?}", roa_obj.validate_embedded_ee_cert());
println!("ROA.verify_signature={:?}", roa_obj.signed_object.verify_signature());
println!();
println!("== Signed Object / ASPA ==");
let aspa_path =
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa";
println!("Fixture (ASPA): {aspa_path}");
let aspa_der = std::fs::read(aspa_path).expect("read ASPA fixture");
let aspa_obj = AspaObject::decode_der(&aspa_der).expect("decode ASPA object");
println!("{:#?}", AspaObjectPretty::from(&aspa_obj));
println!("ASPA.validate_embedded_ee_cert={:?}", aspa_obj.validate_embedded_ee_cert());
println!("ASPA.verify_signature={:?}", aspa_obj.signed_object.verify_signature());
println!();
println!("== CRL ==");
let crl_path = "tests/fixtures/0099DEAB073EFD74C250C0A382B25012B5082AEE.crl";
println!("Fixture (CRL): {crl_path}");
let crl_der = std::fs::read(crl_path).expect("read CRL fixture");
let crl = RpkixCrl::decode_der(&crl_der).expect("decode CRL");
println!("{:#?}", RpkixCrlPretty::from(&crl));
}

View File

@ -0,0 +1,93 @@
use rpki::data_model::rc::{ResourceCertificate, ResourceCertificateError};
const TEST_NO_SIA_CERT_DER_B64: &str = "MIIDATCCAemgAwIBAgIUCyQLQJn92+gyAzvIz22q1F/97OMwDQYJKoZIhvcNAQELBQAwGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMB4XDTI2MDEyNzAzNTk1OVoXDTM2MDEyNTAzNTk1OVowGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/aoMU8J6cddkM2r6F2snd1rCdQPepgo2T2lrqWFcxnQJdcxBL1OYg3wFi95TJmZSeIHIOGauDaJ2abmjgyOUHOC4U68x66JRg4hLkmLxo1cf3uYHWl9Obph6g2qPRvN80ORq70JPuL6mAfUkNiO9hnwK6oQiTzc/rjCQGIFH8kTESBMXLfNCyUpGi+MNztYH6Ha6bKAQuXgd29OFwIkOlGQnYgGC2qBMvnp86eITvV1gTiuI8Ho9m9nZHCmaD7TylvkMDq8Hk5nkIpRcG0uO60SkR2BiMOYe/TNn5dTmHd6bsdbU2GOvgnq1SnqGq3FOWhKIe3ycUJde0uNfZOqRwIDAQABoz8wPTAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUFjyzfJCDNhFfKxVr06kjUkE23dMwDQYJKoZIhvcNAQELBQADggEBAK98n2gVlwKA3Ob1YeAm9f+8hm7pbvrt0tA8GW180CILjf09k7fKgiRlxqGdZ9ySXjU52+zCqu3MpBXVbI87ZC+zA6uK05n4y1F0n85MJ9hGR2UEiPcqou85X73LvioynnSOy/OV1PjKJXReUsqF3GgDtgcMyFssPJ9s/5DWuUCScUJY6pu0kuIGOLQ/oXUw4TvxUeyz73gOTiAJshVTQoLpHUhj0595S7lArjwi7oLI1b8m8guTknvhk0Sc3tJZmUqOcIvYIs0guHpaeC+sMoF4K+6UTrxxOBdX+fUEWNpUyYXWHjdZq25PbJdHwA/VAW2zYVojaVREligf0Qfo6F4=";
fn decode_b64(b64: &str) -> Vec<u8> {
use base64::engine::general_purpose::STANDARD;
use base64::Engine as _;
STANDARD.decode(b64).unwrap()
}
fn replace_first(haystack: &mut [u8], needle: &[u8], replacement: &[u8]) -> bool {
if needle.len() != replacement.len() {
return false;
}
for i in 0..=haystack.len().saturating_sub(needle.len()) {
if &haystack[i..i + needle.len()] == needle {
haystack[i..i + needle.len()].copy_from_slice(replacement);
return true;
}
}
false
}
fn replace_all(haystack: &mut [u8], needle: &[u8], replacement: &[u8]) -> usize {
if needle.len() != replacement.len() {
return 0;
}
let mut n = 0;
let mut i = 0;
while i + needle.len() <= haystack.len() {
if &haystack[i..i + needle.len()] == needle {
haystack[i..i + needle.len()].copy_from_slice(replacement);
n += 1;
i += needle.len();
} else {
i += 1;
}
}
n
}
#[test]
fn trailing_bytes_after_cert_are_rejected() {
let mut der = decode_b64(TEST_NO_SIA_CERT_DER_B64);
der.push(0);
let err = ResourceCertificate::from_der(&der).unwrap_err();
assert!(matches!(err, ResourceCertificateError::TrailingBytes(1)));
}
#[test]
fn signature_algorithm_mismatch_is_detected() {
let mut der = decode_b64(TEST_NO_SIA_CERT_DER_B64);
// DER encoding of sha256WithRSAEncryption OID:
// 06 09 2A 86 48 86 F7 0D 01 01 0B
let oid = [0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B];
let mut patched = oid;
patched[10] = 0x01; // rsaEncryption, same length encoding
assert!(replace_first(&mut der, &oid, &patched));
let err = ResourceCertificate::from_der(&der).unwrap_err();
assert!(matches!(err, ResourceCertificateError::SignatureAlgorithmMismatch));
}
#[test]
fn unsupported_signature_algorithm_is_detected() {
let mut der = decode_b64(TEST_NO_SIA_CERT_DER_B64);
let oid = [0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B];
let mut patched = oid;
patched[10] = 0x01;
let n = replace_all(&mut der, &oid, &patched);
assert!(n >= 2, "expected to patch at least inner+outer AlgorithmIdentifier");
let err = ResourceCertificate::from_der(&der).unwrap_err();
assert!(matches!(err, ResourceCertificateError::UnsupportedSignatureAlgorithm));
}
#[test]
fn invalid_signature_algorithm_parameters_are_detected() {
let mut der = decode_b64(TEST_NO_SIA_CERT_DER_B64);
// Replace NULL parameters (05 00) right after the sha256WithRSAEncryption OID with
// an empty OCTET STRING (04 00) to keep the encoding length unchanged.
let alg = [
0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B, 0x05, 0x00,
];
let mut patched = alg;
patched[11] = 0x04;
let n = replace_all(&mut der, &alg, &patched);
assert!(n >= 2, "expected to patch at least inner+outer AlgorithmIdentifier parameters");
let err = ResourceCertificate::from_der(&der).unwrap_err();
assert!(matches!(
err,
ResourceCertificateError::InvalidSignatureAlgorithmParameters
));
}

View File

@ -0,0 +1,43 @@
use rpki::data_model::oid::OID_CP_IPADDR_ASNUMBER;
use rpki::data_model::rc::{ResourceCertKind, ResourceCertificate, SubjectInfoAccess};
#[test]
fn resource_certificate_from_der_parses_ca_fixtures() {
let fixtures = [
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer",
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ZW5EIqvxKWSSAOsBmoFfKxIjbpI.cer",
];
for path in fixtures {
let der = std::fs::read(path).expect("read CA cert fixture");
let rc = ResourceCertificate::from_der(&der).expect("parse CA cert fixture");
assert_eq!(rc.kind, ResourceCertKind::Ca, "fixture should be CA: {path}");
assert_eq!(rc.tbs.version, 2, "X.509 v3 encoded as 2: {path}");
assert_eq!(
rc.tbs.extensions.certificate_policies_oid.as_deref(),
Some(OID_CP_IPADDR_ASNUMBER),
"CA certificatePolicies OID: {path}"
);
assert!(
matches!(rc.tbs.extensions.subject_info_access, Some(SubjectInfoAccess::Ca(_))),
"CA SIA should not contain signedObject accessMethod: {path}"
);
assert!(rc.tbs.extensions.ip_resources.is_some(), "CA should have IP resources: {path}");
}
}
#[test]
fn resource_certificate_from_der_parses_as_resources_in_apnic_fixture() {
let path =
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer";
let der = std::fs::read(path).expect("read APNIC CA cert fixture");
let rc = ResourceCertificate::from_der(&der).expect("parse APNIC CA cert fixture");
assert!(rc.tbs.extensions.as_resources.is_some(), "fixture should carry AS resources");
}

93
tests/test_rc_helpers.rs Normal file
View File

@ -0,0 +1,93 @@
use rpki::data_model::rc::{
Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange,
IpPrefix,
};
#[test]
fn ip_address_family_contains_prefix_afi_mismatch_is_false() {
let fam = IpAddressFamily {
afi: Afi::Ipv4,
choice: IpAddressChoice::Inherit,
};
let p = IpPrefix {
afi: Afi::Ipv6,
prefix_len: 0,
addr: vec![0; 16],
};
assert!(!fam.contains_prefix(&p));
}
#[test]
fn ip_address_family_inherit_covers_all_prefixes_of_same_afi() {
let fam = IpAddressFamily {
afi: Afi::Ipv6,
choice: IpAddressChoice::Inherit,
};
let p = IpPrefix {
afi: Afi::Ipv6,
prefix_len: 32,
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
};
assert!(fam.contains_prefix(&p));
}
#[test]
fn as_resource_set_asnum_single_id_returns_expected_values() {
let inherit = AsResourceSet {
asnum: Some(AsIdentifierChoice::Inherit),
rdi: None,
};
assert_eq!(inherit.asnum_single_id(), None);
let single_id = AsResourceSet {
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64512)])),
rdi: None,
};
assert_eq!(single_id.asnum_single_id(), Some(64512));
let single_range = AsResourceSet {
asnum: None,
rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range {
min: 64512,
max: 64520,
}])),
};
assert_eq!(single_range.asnum_single_id(), None);
let multiple = AsResourceSet {
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
AsIdOrRange::Id(64512),
AsIdOrRange::Id(64513),
])),
rdi: None,
};
assert_eq!(multiple.asnum_single_id(), None);
}
#[test]
fn as_identifier_choice_has_range_detects_ranges() {
assert!(!AsIdentifierChoice::Inherit.has_range());
assert!(!AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64512)]).has_range());
assert!(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range { min: 1, max: 2 }]).has_range());
}
#[test]
fn ip_resource_contains_prefix_finds_matching_family() {
let fam_v6 = IpAddressFamily {
afi: Afi::Ipv6,
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(IpPrefix {
afi: Afi::Ipv6,
prefix_len: 32,
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
})]),
};
let set = rpki::data_model::rc::IpResourceSet { families: vec![fam_v6] };
let p = IpPrefix {
afi: Afi::Ipv6,
prefix_len: 48,
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0x12, 0x34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
};
assert!(set.contains_prefix(&p));
}

View File

@ -0,0 +1,58 @@
use rpki::data_model::rc::{
Afi, IpAddressChoice, IpAddressFamily, IpAddressOrRange, IpAddressRange, IpPrefix, IpResourceSet,
};
#[test]
fn ip_resource_range_covers_prefix_ipv4() {
let set = IpResourceSet {
families: vec![IpAddressFamily {
afi: Afi::Ipv4,
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Range(IpAddressRange {
min: vec![10, 0, 0, 0],
max: vec![10, 255, 255, 255],
})]),
}],
};
let inside = IpPrefix {
afi: Afi::Ipv4,
prefix_len: 16,
addr: vec![10, 1, 0, 0],
};
assert!(set.contains_prefix(&inside));
let outside = IpPrefix {
afi: Afi::Ipv4,
prefix_len: 16,
addr: vec![11, 0, 0, 0],
};
assert!(!set.contains_prefix(&outside));
}
#[test]
fn ip_resource_range_covers_prefix_ipv6() {
let set = IpResourceSet {
families: vec![IpAddressFamily {
afi: Afi::Ipv6,
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Range(IpAddressRange {
min: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
max: vec![0x20, 0x01, 0x0d, 0xb8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
})]),
}],
};
let inside = IpPrefix {
afi: Afi::Ipv6,
prefix_len: 64,
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
};
assert!(set.contains_prefix(&inside));
let outside = IpPrefix {
afi: Afi::Ipv6,
prefix_len: 64,
addr: vec![0x20, 0x01, 0x0d, 0xb9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
};
assert!(!set.contains_prefix(&outside));
}

View File

@ -0,0 +1,135 @@
use rpki::data_model::rc::{
Afi, AsIdentifierChoice, AsIdOrRange, AsResourceSet, IpAddressChoice, IpAddressOrRange,
IpResourceSet,
};
fn len_bytes(len: usize) -> Vec<u8> {
if len < 128 {
vec![len as u8]
} else {
let mut tmp = Vec::new();
let mut n = len;
while n > 0 {
tmp.push((n & 0xFF) as u8);
n >>= 8;
}
tmp.reverse();
let mut out = vec![0x80 | (tmp.len() as u8)];
out.extend(tmp);
out
}
}
fn tlv(tag: u8, content: &[u8]) -> Vec<u8> {
let mut out = vec![tag];
out.extend(len_bytes(content.len()));
out.extend_from_slice(content);
out
}
fn der_sequence(children: Vec<Vec<u8>>) -> Vec<u8> {
let mut content = Vec::new();
for c in children {
content.extend(c);
}
tlv(0x30, &content)
}
fn der_octet_string(bytes: &[u8]) -> Vec<u8> {
tlv(0x04, bytes)
}
fn der_null() -> Vec<u8> {
vec![0x05, 0x00]
}
fn der_integer_u64(v: u64) -> Vec<u8> {
let mut bytes = Vec::new();
let mut n = v;
if n == 0 {
bytes.push(0);
} else {
while n > 0 {
bytes.push((n & 0xFF) as u8);
n >>= 8;
}
bytes.reverse();
if bytes[0] & 0x80 != 0 {
bytes.insert(0, 0);
}
}
tlv(0x02, &bytes)
}
fn der_bit_string(unused: u8, bytes: &[u8]) -> Vec<u8> {
let mut content = vec![unused];
content.extend_from_slice(bytes);
tlv(0x03, &content)
}
fn cs_cons(tag_no: u8, inner_der: Vec<u8>) -> Vec<u8> {
tlv(0xA0 | (tag_no & 0x1F), &inner_der)
}
#[test]
fn ip_addr_blocks_decode_handles_inherit_and_range_endpoint_fill() {
// IPv6 family with one range:
// - min: 240a:a000::/32 (encoded as 32 bits)
// - max: 240a:a008::/31 (encoded as 31 bits, so fill-ones yields 240a:a009:ffff..)
let address_family = der_octet_string(&[0x00, 0x02]);
let min = der_bit_string(0, &[0x24, 0x0A, 0xA0, 0x00]);
let max = der_bit_string(1, &[0x24, 0x0A, 0xA0, 0x08]);
let range = der_sequence(vec![min, max]);
let choice = der_sequence(vec![range]);
let fam = der_sequence(vec![address_family, choice]);
let ip_addr_blocks = der_sequence(vec![fam]);
let decoded = IpResourceSet::decode_extn_value(&ip_addr_blocks).expect("decode ipAddrBlocks");
assert_eq!(decoded.families.len(), 1);
let fam = &decoded.families[0];
assert_eq!(fam.afi, Afi::Ipv6);
let IpAddressChoice::AddressesOrRanges(items) = &fam.choice else {
panic!("expected AddressesOrRanges");
};
let IpAddressOrRange::Range(r) = &items[0] else {
panic!("expected Range");
};
assert_eq!(r.min[..4], [0x24, 0x0A, 0xA0, 0x00]);
assert_eq!(r.max[..4], [0x24, 0x0A, 0xA0, 0x09]);
assert!(r.max[4..].iter().all(|&b| b == 0xFF));
// Inherit branch.
let fam_inherit = der_sequence(vec![der_octet_string(&[0x00, 0x01]), der_null()]);
let decoded = IpResourceSet::decode_extn_value(&der_sequence(vec![fam_inherit])).expect("decode inherit");
assert!(decoded.is_all_inherit());
}
#[test]
fn autonomous_sys_ids_decode_handles_inherit_ids_and_ranges() {
// ASIdentifiers with:
// - asnum [0] EXPLICIT asIdsOrRanges { id 64496, range 64500..64510 }
let as_id_or_ranges = der_sequence(vec![
der_integer_u64(64496),
der_sequence(vec![der_integer_u64(64500), der_integer_u64(64510)]),
]);
let asnum = cs_cons(0, as_id_or_ranges);
let as_identifiers = der_sequence(vec![asnum]);
let decoded = AsResourceSet::decode_extn_value(&as_identifiers).expect("decode asIdentifiers");
assert!(decoded.rdi.is_none());
let Some(asnum) = decoded.asnum else {
panic!("missing asnum");
};
let AsIdentifierChoice::AsIdsOrRanges(items) = asnum else {
panic!("expected AsIdsOrRanges");
};
assert_eq!(items.len(), 2);
assert!(matches!(items[0], AsIdOrRange::Id(64496)));
assert!(matches!(items[1], AsIdOrRange::Range { min: 64500, max: 64510 }));
// asnum inherit.
let asnum_inherit = cs_cons(0, der_null());
let decoded = AsResourceSet::decode_extn_value(&der_sequence(vec![asnum_inherit])).expect("decode inherit");
assert!(decoded.is_asnum_inherit());
}

View File

@ -0,0 +1,202 @@
use rpki::data_model::rc::{AsResourceSet, IpResourceSet};
fn len_bytes(len: usize) -> Vec<u8> {
if len < 128 {
vec![len as u8]
} else {
let mut tmp = Vec::new();
let mut n = len;
while n > 0 {
tmp.push((n & 0xFF) as u8);
n >>= 8;
}
tmp.reverse();
let mut out = vec![0x80 | (tmp.len() as u8)];
out.extend(tmp);
out
}
}
fn tlv(tag: u8, content: &[u8]) -> Vec<u8> {
let mut out = vec![tag];
out.extend(len_bytes(content.len()));
out.extend_from_slice(content);
out
}
fn der_sequence(children: Vec<Vec<u8>>) -> Vec<u8> {
let mut content = Vec::new();
for c in children {
content.extend(c);
}
tlv(0x30, &content)
}
fn der_octet_string(bytes: &[u8]) -> Vec<u8> {
tlv(0x04, bytes)
}
fn der_null() -> Vec<u8> {
vec![0x05, 0x00]
}
fn der_integer_bytes(bytes: &[u8]) -> Vec<u8> {
tlv(0x02, bytes)
}
fn der_integer_u64(v: u64) -> Vec<u8> {
let mut bytes = Vec::new();
let mut n = v;
if n == 0 {
bytes.push(0);
} else {
while n > 0 {
bytes.push((n & 0xFF) as u8);
n >>= 8;
}
bytes.reverse();
if bytes[0] & 0x80 != 0 {
bytes.insert(0, 0);
}
}
der_integer_bytes(&bytes)
}
fn der_bit_string(unused: u8, bytes: &[u8]) -> Vec<u8> {
let mut content = vec![unused];
content.extend_from_slice(bytes);
tlv(0x03, &content)
}
fn cs_cons(tag_no: u8, inner_der: Vec<u8>) -> Vec<u8> {
tlv(0xA0 | (tag_no & 0x1F), &inner_der)
}
#[test]
fn ip_addr_blocks_decode_rejects_invalid_encodings() {
// Not a SEQUENCE.
assert!(IpResourceSet::decode_extn_value(&der_null()).is_err());
// IPAddressFamily wrong shape.
let fam_wrong = der_sequence(vec![der_octet_string(&[0x00, 0x01])]); // missing choice
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam_wrong])).is_err());
// addressFamily wrong length.
let fam_wrong = der_sequence(vec![
der_octet_string(&[0x00]), // 1 byte
der_null(),
]);
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam_wrong])).is_err());
// unsupported AFI.
let fam_wrong = der_sequence(vec![der_octet_string(&[0x00, 0x03]), der_null()]);
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam_wrong])).is_err());
// ipAddressChoice wrong type.
let fam_wrong = der_sequence(vec![
der_octet_string(&[0x00, 0x01]),
der_integer_u64(1),
]);
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam_wrong])).is_err());
// BitString with invalid unused-bits value (>7).
let min = der_bit_string(0, &[0x0A, 0x00, 0x00, 0x00]);
let max = der_bit_string(8, &[0x0A, 0xFF, 0xFF, 0xFF]); // invalid unused bits
let range = der_sequence(vec![min, max]);
let fam = der_sequence(vec![
der_octet_string(&[0x00, 0x01]),
der_sequence(vec![range]),
]);
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).is_err());
// BitString with non-zero bits in the unused tail.
let min = der_bit_string(0, &[0x0A, 0x00, 0x00, 0x00]);
let max = der_bit_string(1, &[0x0A, 0x00, 0x00, 0x01]); // LSB set, but unused=1
let range = der_sequence(vec![min, max]);
let fam = der_sequence(vec![
der_octet_string(&[0x00, 0x01]),
der_sequence(vec![range]),
]);
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).is_err());
// Prefix length out of range for IPv4 (40 bits).
let prefix = der_bit_string(0, &[0x0A, 0x00, 0x00, 0x00, 0x00]); // 40 bits
let fam = der_sequence(vec![
der_octet_string(&[0x00, 0x01]),
der_sequence(vec![prefix]),
]);
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).is_err());
}
#[test]
fn autonomous_sys_ids_decode_rejects_invalid_encodings() {
// Not a SEQUENCE.
assert!(AsResourceSet::decode_extn_value(&der_null()).is_err());
// Wrong tag class (expects [0]/[1] context-specific).
let as_ids = der_sequence(vec![der_integer_u64(64496)]);
assert!(AsResourceSet::decode_extn_value(&as_ids).is_err());
// Duplicate [0] tags.
let a0 = cs_cons(0, der_null());
let a0_dup = cs_cons(0, der_null());
assert!(AsResourceSet::decode_extn_value(&der_sequence(vec![a0, a0_dup])).is_err());
// Out-of-range ASID (> u32::MAX).
let too_big = der_integer_bytes(&[0x01, 0x00, 0x00, 0x00, 0x00]); // 2^32
let asnum = cs_cons(0, der_sequence(vec![too_big]));
assert!(AsResourceSet::decode_extn_value(&der_sequence(vec![asnum])).is_err());
// Range min > max.
let bad_range = der_sequence(vec![der_integer_u64(64510), der_integer_u64(64500)]);
let asnum = cs_cons(0, der_sequence(vec![bad_range]));
assert!(AsResourceSet::decode_extn_value(&der_sequence(vec![asnum])).is_err());
// Unsupported element inside asIdsOrRanges.
let asnum = cs_cons(0, der_sequence(vec![der_null()]));
assert!(AsResourceSet::decode_extn_value(&der_sequence(vec![asnum])).is_err());
}
#[test]
fn ip_addr_blocks_prefix_bitstring_unused_bits_checks_are_enforced() {
// Prefix BitString with non-zero bits in the unused tail (parse_ip_prefix path).
let prefix = der_bit_string(1, &[0x0A, 0x00, 0x00, 0x01]); // LSB set, but unused=1
let fam = der_sequence(vec![
der_octet_string(&[0x00, 0x01]),
der_sequence(vec![prefix]),
]);
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).is_err());
// Prefix BitString with empty bytes but unused_bits != 0 is invalid.
let prefix = der_bit_string(1, &[]);
let fam = der_sequence(vec![
der_octet_string(&[0x00, 0x01]),
der_sequence(vec![prefix]),
]);
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).is_err());
}
#[test]
fn ip_addr_blocks_range_upper_bound_can_fill_all_ones_when_bit_len_zero() {
// A BIT STRING with 0 bits (content is only the "unused bits" count octet) is allowed.
// For an IPAddressRange upper bound, RFC 3779 interprets missing bits as 1s.
let min = der_bit_string(0, &[]);
let max = der_bit_string(0, &[]);
let range = der_sequence(vec![min, max]);
let fam = der_sequence(vec![
der_octet_string(&[0x00, 0x01]),
der_sequence(vec![range]),
]);
let set = IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).expect("decode range with 0-bit endpoints");
let fam = set.families.iter().find(|f| f.afi == rpki::data_model::rc::Afi::Ipv4).unwrap();
let rpki::data_model::rc::IpAddressChoice::AddressesOrRanges(items) = &fam.choice else {
panic!("expected explicit addressesOrRanges");
};
assert_eq!(items.len(), 1);
let rpki::data_model::rc::IpAddressOrRange::Range(r) = &items[0] else {
panic!("expected a range");
};
assert_eq!(r.min, vec![0, 0, 0, 0]);
assert_eq!(r.max, vec![0xFF, 0xFF, 0xFF, 0xFF]);
}

View File

@ -0,0 +1,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
View 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(_)));
}

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

View File

@ -0,0 +1,13 @@
use rpki::data_model::roa::RoaObject;
#[test]
fn roa_embedded_ee_cert_resources_validate() {
let der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa",
)
.expect("read ROA fixture");
let roa = RoaObject::decode_der(&der).expect("decode roa");
roa.validate_embedded_ee_cert()
.expect("roa EE cert resources must validate");
}

View File

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

View File

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

View File

@ -0,0 +1,172 @@
use der_parser::num_bigint::BigUint;
use rpki::data_model::oid::OID_CP_IPADDR_ASNUMBER;
use rpki::data_model::rc::{
Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange,
IpPrefix, RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate,
};
use rpki::data_model::ta::{TaCertificate, TaCertificateError};
use time::OffsetDateTime;
fn dummy_rc_ca(ext: RcExtensions) -> ResourceCertificate {
let t = OffsetDateTime::from_unix_timestamp(0).unwrap();
ResourceCertificate {
raw_der: Vec::new(),
kind: ResourceCertKind::Ca,
tbs: RpkixTbsCertificate {
version: 2,
serial_number: BigUint::from(1u32),
signature_algorithm: "1.2.840.113549.1.1.11".into(),
issuer_dn: "CN=TA".into(),
subject_dn: "CN=TA".into(),
validity_not_before: t,
validity_not_after: t,
subject_public_key_info: Vec::new(),
extensions: ext,
},
}
}
#[test]
fn ta_certificate_from_der_parses_downloaded_fixtures() {
let fixtures = [
"tests/fixtures/ta/afrinic-ta.cer",
"tests/fixtures/ta/apnic-ta.cer",
"tests/fixtures/ta/arin-ta.cer",
"tests/fixtures/ta/lacnic-ta.cer",
"tests/fixtures/ta/ripe-ncc-ta.cer",
];
for path in fixtures {
let der = std::fs::read(path).expect("read TA fixture");
let ta = TaCertificate::from_der(&der).expect("parse TA fixture");
assert_eq!(ta.rc_ca.kind, ResourceCertKind::Ca);
assert_eq!(
ta.rc_ca.tbs.extensions.certificate_policies_oid.as_deref(),
Some(OID_CP_IPADDR_ASNUMBER)
);
assert!(ta.rc_ca.tbs.extensions.subject_key_identifier.is_some());
}
}
#[test]
fn ta_certificate_rejects_non_self_signed_ca() {
// This is a CA cert fixture, but not self-signed (issuer != subject).
let der = std::fs::read(
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
)
.expect("read CA cert fixture");
assert!(matches!(
TaCertificate::from_der(&der),
Err(TaCertificateError::NotSelfSignedIssuerSubject)
| Err(TaCertificateError::InvalidSelfSignature(_))
));
}
#[test]
fn ta_constraints_require_policies_and_ski() {
let rc = dummy_rc_ca(RcExtensions {
basic_constraints_ca: true,
subject_key_identifier: None,
subject_info_access: None,
certificate_policies_oid: None,
ip_resources: Some(rpki::data_model::rc::IpResourceSet { families: vec![] }),
as_resources: None,
});
assert!(matches!(
TaCertificate::validate_rc_constraints(&rc),
Err(TaCertificateError::MissingOrInvalidCertificatePolicies)
));
let rc = dummy_rc_ca(RcExtensions {
certificate_policies_oid: Some(OID_CP_IPADDR_ASNUMBER.to_string()),
..rc.tbs.extensions.clone()
});
assert!(matches!(
TaCertificate::validate_rc_constraints(&rc),
Err(TaCertificateError::MissingSubjectKeyIdentifier)
));
}
#[test]
fn ta_constraints_require_non_empty_resources_and_no_inherit() {
// Missing both IP and AS resources.
let rc = dummy_rc_ca(RcExtensions {
basic_constraints_ca: true,
subject_key_identifier: Some(vec![1]),
subject_info_access: None,
certificate_policies_oid: Some(OID_CP_IPADDR_ASNUMBER.to_string()),
ip_resources: None,
as_resources: None,
});
assert!(matches!(
TaCertificate::validate_rc_constraints(&rc),
Err(TaCertificateError::ResourcesMissing)
));
// IP resources present but empty => resources empty.
let rc = dummy_rc_ca(RcExtensions {
ip_resources: Some(rpki::data_model::rc::IpResourceSet { families: vec![] }),
..rc.tbs.extensions.clone()
});
assert!(matches!(
TaCertificate::validate_rc_constraints(&rc),
Err(TaCertificateError::ResourcesEmpty)
));
// IP resources inherit is rejected.
let rc = dummy_rc_ca(RcExtensions {
ip_resources: Some(rpki::data_model::rc::IpResourceSet {
families: vec![IpAddressFamily {
afi: Afi::Ipv4,
choice: IpAddressChoice::Inherit,
}],
}),
..rc.tbs.extensions.clone()
});
assert!(matches!(
TaCertificate::validate_rc_constraints(&rc),
Err(TaCertificateError::IpResourcesInherit)
));
// AS resources inherit is rejected.
let rc = dummy_rc_ca(RcExtensions {
ip_resources: None,
as_resources: Some(AsResourceSet {
asnum: Some(AsIdentifierChoice::Inherit),
rdi: None,
}),
..rc.tbs.extensions.clone()
});
assert!(matches!(
TaCertificate::validate_rc_constraints(&rc),
Err(TaCertificateError::AsResourcesInherit)
));
// Valid non-empty explicit IP resources => OK.
let rc = dummy_rc_ca(RcExtensions {
ip_resources: Some(rpki::data_model::rc::IpResourceSet {
families: vec![IpAddressFamily {
afi: Afi::Ipv6,
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(IpPrefix {
afi: Afi::Ipv6,
prefix_len: 32,
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
})]),
}],
}),
as_resources: None,
..rc.tbs.extensions.clone()
});
TaCertificate::validate_rc_constraints(&rc).expect("valid explicit resources");
// Valid non-empty explicit AS resources => OK.
let rc = dummy_rc_ca(RcExtensions {
ip_resources: None,
as_resources: Some(AsResourceSet {
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64512)])),
rdi: None,
}),
..rc.tbs.extensions.clone()
});
TaCertificate::validate_rc_constraints(&rc).expect("valid explicit AS resources");
}

31
tests/test_tal_decode.rs Normal file
View File

@ -0,0 +1,31 @@
use der_parser::der::parse_der;
use rpki::data_model::tal::Tal;
#[test]
fn decode_tal_fixtures_smoke() {
let fixtures = [
"tests/fixtures/tal/afrinic.tal",
"tests/fixtures/tal/apnic-rfc7730-https.tal",
"tests/fixtures/tal/arin.tal",
"tests/fixtures/tal/lacnic.tal",
"tests/fixtures/tal/ripe-ncc.tal",
];
for path in fixtures {
let raw = std::fs::read(path).expect("read TAL fixture");
let tal = Tal::decode_bytes(&raw).expect("decode TAL fixture");
assert!(!tal.ta_uris.is_empty(), "TA URI list must be non-empty: {path}");
assert!(!tal.subject_public_key_info_der.is_empty(), "SPKI DER must be non-empty: {path}");
for u in &tal.ta_uris {
assert!(matches!(u.scheme(), "rsync" | "https"), "scheme must be allowed: {u}");
assert!(!u.path().ends_with('/'), "TA URI must not be a directory: {u}");
}
// SPKI DER must be parseable as a DER object (typically a SEQUENCE).
let (rem, _obj) = parse_der(&tal.subject_public_key_info_der).expect("parse spki DER");
assert!(rem.is_empty(), "SPKI DER must not have trailing bytes: {path}");
}
}

View File

@ -0,0 +1,71 @@
use rpki::data_model::tal::{Tal, TalDecodeError};
fn mk_tal(uris: &[&str], b64_lines: &[&str]) -> String {
let mut out = String::new();
out.push_str("# comment\n");
for u in uris {
out.push_str(u);
out.push('\n');
}
out.push('\n'); // separator
for l in b64_lines {
out.push_str(l);
out.push('\n');
}
out
}
#[test]
fn tal_rejects_missing_separator() {
let s = "# c\nhttps://example.invalid/ta.cer\nAAAA\n";
assert!(matches!(
Tal::decode_bytes(s.as_bytes()),
Err(TalDecodeError::MissingSeparatorEmptyLine)
));
}
#[test]
fn tal_rejects_missing_uris() {
let s = "# c\n\nAAAA\n";
assert!(matches!(Tal::decode_bytes(s.as_bytes()), Err(TalDecodeError::MissingTaUris)));
}
#[test]
fn tal_rejects_unsupported_scheme() {
let s = mk_tal(&["ftp://example.invalid/ta.cer"], &["AAAA"]);
assert!(matches!(
Tal::decode_bytes(s.as_bytes()),
Err(TalDecodeError::UnsupportedUriScheme(_))
));
}
#[test]
fn tal_rejects_directory_uri() {
let s = mk_tal(&["https://example.invalid/dir/"], &["AAAA"]);
assert!(matches!(
Tal::decode_bytes(s.as_bytes()),
Err(TalDecodeError::UriIsDirectory(_))
));
}
#[test]
fn tal_rejects_comment_after_header() {
let s = "# c\nhttps://example.invalid/ta.cer\n# late\n\nAAAA\n";
assert!(matches!(Tal::decode_bytes(s.as_bytes()), Err(TalDecodeError::CommentAfterHeader)));
}
#[test]
fn tal_rejects_invalid_base64() {
let s = mk_tal(&["https://example.invalid/ta.cer"], &["not-base64!!!"]);
assert!(matches!(
Tal::decode_bytes(s.as_bytes()),
Err(TalDecodeError::SpkiBase64Decode)
));
}
#[test]
fn tal_rejects_invalid_utf8() {
let bytes = [0xFFu8, 0xFEu8];
assert!(matches!(Tal::decode_bytes(&bytes), Err(TalDecodeError::InvalidUtf8)));
}

View File

@ -0,0 +1,62 @@
use rpki::data_model::ta::{TrustAnchor, TrustAnchorError};
use rpki::data_model::tal::Tal;
use url::Url;
#[test]
fn bind_trust_anchor_with_downloaded_fixtures_succeeds() {
let cases = [
("tests/fixtures/tal/afrinic.tal", "tests/fixtures/ta/afrinic-ta.cer"),
(
"tests/fixtures/tal/apnic-rfc7730-https.tal",
"tests/fixtures/ta/apnic-ta.cer",
),
("tests/fixtures/tal/arin.tal", "tests/fixtures/ta/arin-ta.cer"),
("tests/fixtures/tal/lacnic.tal", "tests/fixtures/ta/lacnic-ta.cer"),
("tests/fixtures/tal/ripe-ncc.tal", "tests/fixtures/ta/ripe-ncc-ta.cer"),
];
for (tal_path, ta_path) in cases {
let tal_raw = std::fs::read(tal_path).expect("read TAL fixture");
let tal = Tal::decode_bytes(&tal_raw).expect("decode TAL fixture");
let ta_der = std::fs::read(ta_path).expect("read TA fixture");
TrustAnchor::bind(tal.clone(), &ta_der, None).expect("bind without resolved uri");
// Also exercise the resolved-uri-in-TAL check using one URI from the TAL list.
let resolved = tal
.ta_uris
.iter()
.find(|u| u.scheme() == "https")
.or_else(|| tal.ta_uris.first())
.expect("tal has ta uris");
let resolved = resolved.clone();
TrustAnchor::bind(tal, &ta_der, Some(&resolved)).expect("bind with resolved uri");
}
}
#[test]
fn bind_rejects_spki_mismatch() {
let tal_raw = std::fs::read("tests/fixtures/tal/ripe-ncc.tal").expect("read TAL fixture");
let mut tal = Tal::decode_bytes(&tal_raw).expect("decode TAL fixture");
let ta_der = std::fs::read("tests/fixtures/ta/ripe-ncc-ta.cer").expect("read TA fixture");
// Flip a byte in TAL SPKI to force mismatch.
tal.subject_public_key_info_der[0] ^= 0x01;
assert!(matches!(
TrustAnchor::bind(tal, &ta_der, None),
Err(TrustAnchorError::TalSpkiMismatch)
));
}
#[test]
fn bind_rejects_resolved_uri_not_listed_in_tal() {
let tal_raw = std::fs::read("tests/fixtures/tal/afrinic.tal").expect("read TAL fixture");
let tal = Tal::decode_bytes(&tal_raw).expect("decode TAL fixture");
let ta_der = std::fs::read("tests/fixtures/ta/afrinic-ta.cer").expect("read TA fixture");
let bad = Url::parse("https://example.invalid/not-in-tal.cer").unwrap();
assert!(matches!(
TrustAnchor::bind(tal, &ta_der, Some(&bad)),
Err(TrustAnchorError::ResolvedUriNotInTal(_))
));
}