Compare commits

..

1 Commits

Author SHA1 Message Date
2fb36712e2 add signed object, manifest impl. add coverage script 2026-01-29 10:35:02 +08:00
30 changed files with 4114 additions and 1278 deletions

View File

@ -4,9 +4,11 @@ version = "0.1.0"
edition = "2024"
[dependencies]
der-parser = "10.0.0"
der-parser = { version = "10.0.0", features = ["serialize"] }
hex = "0.4.3"
base64 = "0.22.1"
sha2 = "0.10.8"
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

@ -9,3 +9,20 @@ cargo test
cargo test -- --nocapture
```
# 覆盖率cargo-llvm-cov
安装工具:
```
rustup component add llvm-tools-preview
cargo install cargo-llvm-cov --locked
```
统计行覆盖率并要求 >=90%
```
./scripts/coverage.sh
# 或
cargo llvm-cov --fail-under-lines 90
```

80
scripts/coverage.sh Executable file
View File

@ -0,0 +1,80 @@
#!/usr/bin/env bash
set -euo pipefail
# Requires:
# rustup component add llvm-tools-preview
# cargo install cargo-llvm-cov --locked
run_out="$(mktemp)"
text_out="$(mktemp)"
html_out="$(mktemp)"
cleanup() {
rm -f "$run_out" "$text_out" "$html_out"
}
trap cleanup EXIT
# Preserve colored output even though we post-process output by running under a pseudo-TTY.
# We run tests only once, then generate both CLI text + HTML reports without rerunning tests.
set +e
cargo llvm-cov clean --workspace >/dev/null 2>&1
# 1) Run tests once to collect coverage data (no report).
script -q -e -c "CARGO_TERM_COLOR=always cargo llvm-cov --no-report" "$run_out" >/dev/null 2>&1
run_status="$?"
# 2) CLI summary report + fail-under gate (no test rerun).
script -q -e -c "CARGO_TERM_COLOR=always cargo llvm-cov report --fail-under-lines 90" "$text_out" >/dev/null 2>&1
text_status="$?"
# 3) HTML report (no test rerun).
script -q -e -c "CARGO_TERM_COLOR=always cargo llvm-cov report --html" "$html_out" >/dev/null 2>&1
html_status="$?"
set -e
strip_script_noise() {
tr -d '\r' | sed '/^Script \(started\|done\) on /d'
}
strip_ansi_for_parse() {
awk '
{
line = $0
gsub(/\033\[[0-9;]*[A-Za-z]/, "", line) # CSI escapes
gsub(/\033\([A-Za-z]/, "", line) # charset escapes (e.g., ESC(B)
gsub(/\r/, "", line)
print line
}
'
}
cat "$run_out" | strip_script_noise
cat "$text_out" | strip_script_noise
cat "$html_out" | strip_script_noise
cat "$run_out" | strip_ansi_for_parse | awk '
BEGIN {
passed=0; failed=0; ignored=0; measured=0; filtered=0;
}
/^test result: / {
if (match($0, /([0-9]+) passed; ([0-9]+) failed; ([0-9]+) ignored; ([0-9]+) measured; ([0-9]+) filtered out;/, m)) {
passed += m[1]; failed += m[2]; ignored += m[3]; measured += m[4]; filtered += m[5];
}
}
END {
executed = passed + failed;
total = passed + failed + ignored + measured;
printf("\nTEST SUMMARY (all suites): passed=%d failed=%d ignored=%d measured=%d filtered_out=%d executed=%d total=%d\n",
passed, failed, ignored, measured, filtered, executed, total);
}
'
echo
echo "HTML report: target/llvm-cov/html/index.html"
status="$text_status"
if [ "$run_status" -ne 0 ]; then status="$run_status"; fi
if [ "$html_status" -ne 0 ]; then status="$html_status"; fi
exit "$status"

View File

@ -1,36 +0,0 @@
# 01. Trust Anchor Locator (TAL)
## 1.1 对象定位
TAL是一个数据格式/配置文件目的是告诉RP信任锚的公钥是什么以及相关对象可以从哪里获取。
## 1.2 数据格式 RFC 8630 §2.2
TAL是一个配置文件格式定义如下
```
The TAL is an ordered sequence of:
1. an optional comment section consisting of one or more lines each starting with the "#" character, followed by human-readable informational UTF-8 text, conforming to the restrictions defined
in Section 2 of [RFC5198], and ending with a line break,
2. a URI section that is comprised of one or more ordered lines, each containing a TA URI, and ending with a line break,
3. a line break, and
4. a subjectPublicKeyInfo [RFC5280] in DER format [X.509], encoded in base64 (see Section 4 of [RFC4648]). To avoid long lines,
line breaks MAY be inserted into the base64-encoded string.
Note that line breaks in this file can use either "<CRLF>" or "<LF>".
```
## 1.3 抽象数据模型
### 1.3.1 TAL
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|----------|-------------|-------------------------|--------------------------------------------|---------------|
| uris | Vec<TalUri> | 指向TA的URI列表 | 允许rsync和https协议。 | RFC 8630 §2.1 |
| comment | Vec<String> | 注释(可选) | | RFC 8630 §2.2 |
| spki_der | Vec<u8> | 原始的subjectPublicKeyInfo | x.509 SubjectPublicKeyInfo DER编码再base64编码 | RFC 8630 §2.2 |
### 1.3.2 TalUri
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|-------|--------|---------|---------|---------------|
| Rsync | String | rsync地址 | | RFC 8630 §2.1 |
| Https | String | https地址 | | RFC 8630 §2.1 |

View File

@ -1,121 +0,0 @@
# 02. Trust Anchor (TA)
## 2.1 对象定位
TA是一个自签名的CA证书。
## 2.2 原始载体与编码
- 载体X.509 certificates.
- 编码DER遵循 RFC 5280 的 certificate 结构与字段语义但受限于RFC 8630 §2.3
## 2.3 抽象数据类型
### 2.3.1 TA
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|-------------------|------------------|---------------|---------|---------------|
| name | String | 标识该TA如apnic等 | | |
| cert_der | Vec<u8> | 原始DER内容 | | |
| cert | X509Certificate | 基础X509证书 | | RFC 5280 §4.1 |
| resource | ResourceSet | 资源集合 | | |
| publication_point | Uri | 获取该TA的URI | | |
### 2.3.2 ResourceSet
资源集合是来自RFC 3779的IP地址块§2和AS号段§3)受约束于RFC 8630 §2.3
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|------|----------------|--------|-------------|---------------------------|
| ips | IpResourceSet | IP地址集合 | 不能是inherit | RFC 3779 §2和RFC 8630 §2.3 |
| asns | AsnResourceSet | ASN集合 | 不能是inherit | RFC 3779 §3和RFC 8630 §2.3 |
[//]: # ()
[//]: # (### 2.3.3 IpResourceSet)
[//]: # (包括IPv4和IPv6的前缀表示)
[//]: # ()
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
[//]: # (|----|------------------------|----------|-------------|--------------|)
[//]: # (| v4 | PrefixSet<Ipv4Prefix> | IPv4前缀集合 | | RFC 3779 §2 |)
[//]: # (| v6 | PrefixSet<Ipv6Prefix> | IPv6前缀集合 | | RFC 3779 §2 |)
[//]: # ()
[//]: # (### 2.3.4 AsnResourceSet)
[//]: # ()
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
[//]: # (|-------|--------------------|-------|-------------|-------------|)
[//]: # (| range | RangeSet<AsnBlock> | ASN集合 | | RFC 3779 §3 |)
[//]: # ()
[//]: # (### 2.3.5 Ipv4Prefix)
[//]: # ()
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
[//]: # (|------|-----|-----|---------|-------------|)
[//]: # (| addr | u32 | 地址 | | RFC 3779 §2 |)
[//]: # (| len | u8 | 长度 | 0-32 | RFC 3779 §2 |)
[//]: # ()
[//]: # ()
[//]: # (### 2.3.6 Ipv6Prefix)
[//]: # ()
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
[//]: # (|------|------|-----|---------|-------------|)
[//]: # (| addr | u128 | 地址 | | RFC 3779 §2 |)
[//]: # (| len | u8 | 长度 | 0-128 | RFC 3779 §2 |)
[//]: # ()
[//]: # (### 2.3.7 AsnBlock)
[//]: # ()
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
[//]: # (|----------|----------|-------|---------|--------------|)
[//]: # (| asn | Asn | ASN | | RFC 3779 §3 |)
[//]: # (| asnRange | AsnRange | ASN范围 | | RFC 3779 §3 |)
[//]: # ()
[//]: # ()
[//]: # (### 2.3.8 Asn)
[//]: # ()
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
[//]: # (|-----|-----|-----|---------|-------------|)
[//]: # (| asn | u32 | ASN | | RFC 3779 §3 |)
[//]: # ()
[//]: # (### 2.3.8 AsnRange)
[//]: # ()
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
[//]: # (|-----|-----|-------|---------|--------------|)
[//]: # (| min | Asn | 最小ASN | | RFC 3779 §3 |)
[//]: # (| max | Asn | 最大ASN | | RFC 3779 §3 |)
# 2.4 TA校验流程RFC 8630 §3
1. 从TAL的URI列表中获取证书对象。顺序访问若前面失效再访问后面的
2. 验证证书格式必须是当前、有效的自签名RPKI证书。
3. 验证公钥匹配。TAL中的SubjectPublicKeyInfo与下载证书的公钥一致。
4. 其他检查。
5. 更新本地存储库缓存。

View File

@ -1,314 +0,0 @@
# 03. RC (Resource Certifications)
## 3.1 对象定位
RC是资源证书包括CA和EE
## 3.2 原始载体与编码
- 载体X.509 certificates.
- 编码DER遵循 RFC 5280 的 Certificate 结构与字段语义,但受 RPKI profile 限制RFC 6487 §4
### 3.2.1 基本语法RFC 5280 §4RFC 6487
RC是遵循RFC5280定义的X.509Certificate语法(RFC 5280 §4)并且符合RFC 6487 §4的约束。只选取RFC 6487 §4章节列出来的字段。Unless specifically noted as being OPTIONAL, all the fields listed
here MUST be present, and any other fields MUST NOT appear in a
conforming resource certificate.
```
Certificate ::= SEQUENCE {
tbsCertificate TBSCertificate,
signatureAlgorithm AlgorithmIdentifier,
signatureValue BIT STRING
}
TBSCertificate ::= SEQUENCE {
version [0] EXPLICIT Version MUST be v3,
serialNumber CertificateSerialNumber,
signature AlgorithmIdentifier,
issuer Name,
subject Name,
validity Validity,
subjectPublicKeyInfo SubjectPublicKeyInfo,
extensions [3] EXPLICIT Extensions OPTIONAL
-- If present, version MUST be v3
}
Version ::= INTEGER { v1(0), v2(1), v3(2) }
CertificateSerialNumber ::= INTEGER
Validity ::= SEQUENCE {
notBefore Time,
notAfter Time }
Time ::= CHOICE {
utcTime UTCTime,
generalTime GeneralizedTime }
UniqueIdentifier ::= BIT STRING
SubjectPublicKeyInfo ::= SEQUENCE {
algorithm AlgorithmIdentifier,
subjectPublicKey BIT STRING }
Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension
Extension ::= SEQUENCE {
extnID OBJECT IDENTIFIER,
critical BOOLEAN DEFAULT FALSE,
extnValue OCTET STRING
-- contains the DER encoding of an ASN.1 value
-- corresponding to the extension type identified
-- by extnID
}
```
> 其中`Name` "a valid X.501 distinguished name"(RFC 6487 §4.4)
### 3.2.2 证书扩展字段 RFC 6487 §4.8)
RC的证书扩展字段按照RFC 6487 §4.8的规定,有以下几个扩展:
- Basic Constraints
- Subject Key Identifier
- Authority Key Identifier
- Key Usage
- Extended Key Usage(CA证书以及验证RPKI对象的EE证书不能出现该字段。非RPKI对象的EE可以出现EKU但必须为non-critical)
- CRL Distribution Points
- Authority Information Access
- Subject Information Access
- SIA for CA Certificates
- SIA for EE Certificates
- Certificate Policies
- IP Resources
- AS Resources
```
# Basic Constraints
id-ce-basicConstraints OBJECT IDENTIFIER ::= { id-ce 19 }
BasicConstraints ::= SEQUENCE {
cA BOOLEAN DEFAULT FALSE }
# Subject Key Identifier
id-ce-subjectKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 14 }
SubjectKeyIdentifier ::= KeyIdentifier
KeyIdentifier ::= OCTET STRING
# Authority Key Identifier
id-ce-authorityKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 35 }
AuthorityKeyIdentifier ::= SEQUENCE {
keyIdentifier [0] KeyIdentifier OPTIONAL }
# Key Usage
id-ce-keyUsage OBJECT IDENTIFIER ::= { id-ce 15 }
KeyUsage ::= BIT STRING {
digitalSignature (0),
nonRepudiation (1), -- recent editions of X.509 have
-- renamed this bit to contentCommitment
keyEncipherment (2),
dataEncipherment (3),
keyAgreement (4),
keyCertSign (5),
cRLSign (6),
encipherOnly (7),
decipherOnly (8) }
# Extended Key Usage
id-ce-extKeyUsage OBJECT IDENTIFIER ::= { id-ce 37 }
ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId
KeyPurposeId ::= OBJECT IDENTIFIER
# CRL Distribution Points
id-ce-cRLDistributionPoints OBJECT IDENTIFIER ::= { id-ce 31 }
CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint
DistributionPoint ::= SEQUENCE {
distributionPoint [0] DistributionPointName OPTIONAL }
DistributionPointName ::= CHOICE {
fullName [0] GeneralNames }
## Authority Information Access
id-pe-authorityInfoAccess OBJECT IDENTIFIER ::= { id-pe 1 }
AuthorityInfoAccessSyntax ::=
SEQUENCE SIZE (1..MAX) OF AccessDescription
AccessDescription ::= SEQUENCE {
accessMethod OBJECT IDENTIFIER,
accessLocation GeneralName }
# AccessDescription
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
# CA 证书发布位置
id-ad-caIssuers OBJECT IDENTIFIER ::= { id-ad 2 }
# OCSP 服务地址
id-ad-ocsp OBJECT IDENTIFIER ::= { id-ad 1 }
# Subject Information Access
id-pe-subjectInfoAccess OBJECT IDENTIFIER ::= { id-pe 11 }
SubjectInfoAccessSyntax ::= SEQUENCE SIZE (1..MAX) OF AccessDescription
AccessDescription ::= SEQUENCE {
accessMethod OBJECT IDENTIFIER,
accessLocation GeneralName }
## Subject Information Access for CA (RFC 6487 §4.8.8.1)
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
id-ad-rpkiManifest OBJECT IDENTIFIER ::= { id-ad 10 }
必须存在一个accessMethod=id-ad-caRepositoryaccessLocation=rsyncURI。
必须存在一个accessMethod=id-ad-repiManifest, accessLocation=rsync URI指向该CA的mft对象。
## Subject Information Access for EE (RFC 6487 §4.8.8.2)
id-ad-signedObject OBJECT IDENTIFIER ::= { id-ad 11 }
必须存在一个accessMethod=id-ad-signedObject, accessLocation=rsyncURI
不允许其他的accessMethod
# Certificate Policies
id-ce-certificatePolicies OBJECT IDENTIFIER ::= { id-ce 32 }
anyPolicy OBJECT IDENTIFIER ::= { id-ce-certificatePolicies 0 }
certificatePolicies ::= SEQUENCE SIZE (1..MAX) OF PolicyInformation
PolicyInformation ::= SEQUENCE {
policyIdentifier CertPolicyId,
policyQualifiers SEQUENCE SIZE (1..MAX) OF PolicyQualifierInfo OPTIONAL }
CertPolicyId ::= OBJECT IDENTIFIER
PolicyQualifierInfo ::= SEQUENCE {
policyQualifierId PolicyQualifierId,
qualifier ANY DEFINED BY policyQualifierId }
-- policyQualifierIds for Internet policy qualifiers
id-qt OBJECT IDENTIFIER ::= { id-pkix 2 }
id-qt-cps OBJECT IDENTIFIER ::= { id-qt 1 }
id-qt-unotice OBJECT IDENTIFIER ::= { id-qt 2 }
PolicyQualifierId ::= OBJECT IDENTIFIER ( id-qt-cps | id-qt-unotice )
Qualifier ::= CHOICE {
cPSuri CPSuri,
userNotice UserNotice }
CPSuri ::= IA5String
UserNotice ::= SEQUENCE {
noticeRef NoticeReference OPTIONAL,
explicitText DisplayText OPTIONAL }
NoticeReference ::= SEQUENCE {
organization DisplayText,
noticeNumbers SEQUENCE OF INTEGER }
DisplayText ::= CHOICE {
ia5String IA5String (SIZE (1..200)),
visibleString VisibleString (SIZE (1..200)),
bmpString BMPString (SIZE (1..200)),
utf8String UTF8String (SIZE (1..200)) }
# IP Resources
id-pe-ipAddrBlocks OBJECT IDENTIFIER ::= { id-pe 7 }
IPAddrBlocks ::= SEQUENCE OF IPAddressFamily
IPAddressFamily ::= SEQUENCE { -- AFI & optional SAFI --
addressFamily OCTET STRING (SIZE (2..3)),
ipAddressChoice IPAddressChoice }
IPAddressChoice ::= CHOICE {
inherit NULL, -- inherit from issuer --
addressesOrRanges SEQUENCE OF IPAddressOrRange }
IPAddressOrRange ::= CHOICE {
addressPrefix IPAddress,
addressRange IPAddressRange }
IPAddressRange ::= SEQUENCE {
min IPAddress,
max IPAddress }
IPAddress ::= BIT STRING
# AS Resources
id-pe-autonomousSysIds OBJECT IDENTIFIER ::= { id-pe 8 }
ASIdentifiers ::= SEQUENCE {
asnum [0] EXPLICIT ASIdentifierChoice OPTIONAL,
rdi [1] EXPLICIT ASIdentifierChoice OPTIONAL}
ASIdentifierChoice ::= CHOICE {
inherit NULL, -- inherit from issuer --
asIdsOrRanges SEQUENCE OF ASIdOrRange }
ASIdOrRange ::= CHOICE {
id ASId,
range ASRange }
ASRange ::= SEQUENCE {
min ASId,
max ASId }
ASId ::= INTEGER
```
# 3.3 抽象数据结构
采用X509 Certificate + Resource + 约束校验的方式组合
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|----------|---------------------|----------|---------|---------------|
| cert_der | Vec<u8> | 证书原始数据 | | |
| cert | X509Certificate | 基础X509证书 | | RFC 5280 §4.1 |
| resource | ResourceSet | 资源集合 | | |
# 3.4 约束规则
## 3.4.1 Cert约束校验规则
RFC 6487中规定的证书的字段参见[3.2.1 ](#321-基本语法rfc-5280-4rfc-6487-)
-
| 字段 | 语义 | 约束/解析规则 | RFC 引用 |
|-----------|-------|----------------------------------------------|--------------|
| version | 证书版本 | 必须是v3(值为2 | RFC6487 §4.1 |
| serial | 证书编号 | 同一个CA签发的证书编号必须唯一 | RFC6487 §4.2 |
| validity | 证书有效期 | notBefore时间不能早于证书的生成时间。若时间段大于上级证书的有效期也是有效的 | RFC6487 §4.6 |
## 3.4.2 Cert Extentions中字段的约束校验规则
RFC 6487中规定的扩展字段参见[3.2.2 ](#322-证书扩展字段-rfc-6487-48)
| 字段 | critical | 语义 | 约束/解析规则 | RFC 引用 |
|----------------------------|----------|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|
| basicConstraints | Y | 证书类型 | CA证书cA=TRUE; EE证书cA=FALSE | RFC6487 §4.8.1 |
| subjectKeyIdentifier | N | 证书公钥 | SKI = SHA-1(DER-encoded SPKI bit string) | RFC6487 §4.8.2 |
| authorityKeyIdentifier | N | 父证书的公钥 | 字段只包含keyIdentifier不能包含authorityCertIssuer和authorityCertSerialNumber除了自签名CA外其余证书必须出现。自签名CA若出现该字段则等于SKI | RFC6487 §4.8.3 |
| keyUsage | Y | 证书公钥的用途权限 | CA证书keyCertSign = TRUE, cRLSign = TRUE 其他都是FALSE。EE证书digitalSignature = TRUE 其他都是FALSE | RFC6487 §4.8.4 |
| extendedKeyUsage | N | 扩展证书公钥的用途权限 | CA证书不能出现EKU验证 RPKI 对象的 EE 证书不能出现EKU非 RPKI 对象的 EE可以出现EKU但必须为non-critical. | RFC6487 §4.8.5 |
| cRLDistributionPoints | N | CRL的发布点位置 | 字段distributionPoint不能包含reasons、cRLIssuer。其中distributionPoint字段包含fullName不能包含nameRelativeToCRLIssuer。fullName的格式必须是URI。自签名证书禁止出现该字段。非自签名证书必须出现。一个CA只能有一个CRL。一个CRLDP只能包含一个distributionPoint。但一个distributionPoint字段中可以包含多于1个的URI但必须包含rsync URI且必须是最新的。 | RFC6487 §4.8.6 |
| authorityInformationAccess | N | 签发者的发布点位置 | 除了自签名的CA必须出现。自签名CA禁止出现。推荐的URI访问方式是rsync并且rsyncURI的话必须指定accessMethod=id-ad-caIssuers | RFC6487 §4.8.7 |
| subjectInformationAccess | N | 发布点位置 | CA证书必须存在。必须存在一个accessMethod=id-ad-caRepositoryaccessLocation=rsyncURI。必须存在一个accessMethod=id-ad-repiManifest,accessLocation=rsync URI指向该CA的mft对象。 EE证书必须存在。必须存在一个accessMethod=id-ad-signedObject,accessLocation=rsyncURI。不允许其他的accessMethod | RFC6487 §4.8.8 |
| certificatePolicies | Y | 证书策略 | 必须存在并且只能存在一种策略RFC 6484 — RPKI Certificate Policy (CP) | RFC6487 §4.8.9 |
| iPResources | Y | IP地址集合 | 所有的RPKI证书中必须包含IP Resources或者ASResources或者两者都包含。 | RFC6487 §4.8.10 |
| aSResources | Y | ASN集合 | 所有的RPKI证书中必须包含IP Resources或者ASResources或者两者都包含。 | RFC6487 §4.8.11 |

View File

@ -0,0 +1,158 @@
# 05. RPKI Signed ObjectCMS SignedData 外壳)
## 5.1 对象定位
ROA、Manifest 等都属于 “RPKI Signed Object”其外壳是 CMS SignedData并受 RFC 6488 的 profile 约束RFC 9589 进一步更新了 `signedAttrs` 的要求。RFC 6488 §2-§4RFC 9589 §4。
本文件描述**通用外壳模型**eContentType/eContent 由具体对象文档给出)。
## 5.2 原始载体与编码
- 载体CMS `ContentInfo`,其中 `contentType` 为 SignedData。RFC 6488 §2RFC 6488 §3(1a)。
- 编码DER。RFC 6488 §2RFC 6488 §3(1l)。
### 5.2.1 CMS 外壳ContentInfoASN.1RFC 5652 §3
```asn1
ContentInfo ::= SEQUENCE {
contentType ContentType,
content [0] EXPLICIT ANY DEFINED BY contentType }
ContentType ::= OBJECT IDENTIFIER
```
### 5.2.2 CMS 外壳SignedDataASN.1RFC 5652 §5.1
```asn1
id-signedData OBJECT IDENTIFIER ::= { iso(1) member-body(2)
us(840) rsadsi(113549) pkcs(1) pkcs7(7) 2 }
SignedData ::= SEQUENCE {
version CMSVersion,
digestAlgorithms DigestAlgorithmIdentifiers,
encapContentInfo EncapsulatedContentInfo,
certificates [0] IMPLICIT CertificateSet OPTIONAL,
crls [1] IMPLICIT RevocationInfoChoices OPTIONAL,
signerInfos SignerInfos }
DigestAlgorithmIdentifiers ::= SET OF DigestAlgorithmIdentifier
SignerInfos ::= SET OF SignerInfo
```
### 5.2.3 CMS 外壳EncapsulatedContentInfoASN.1RFC 5652 §5.2
```asn1
EncapsulatedContentInfo ::= SEQUENCE {
eContentType ContentType,
eContent [0] EXPLICIT OCTET STRING OPTIONAL }
ContentType ::= OBJECT IDENTIFIER
```
> 注CMS 允许 `eContent` 不一定 DER 编码RFC 5652 §5.2);但 RPKI signed object profile 要求**整个对象 DER 编码**RFC 6488 §2RFC 6488 §3(1l)),且 eContentpayload由对象规范定义并通常为 DER如 ROARFC 9582 §4ManifestRFC 9286 §4.2)。
### 5.2.4 CMS 外壳SignerInfo 与 AttributeASN.1RFC 5652 §5.3
```asn1
SignerInfo ::= SEQUENCE {
version CMSVersion,
sid SignerIdentifier,
digestAlgorithm DigestAlgorithmIdentifier,
signedAttrs [0] IMPLICIT SignedAttributes OPTIONAL,
signatureAlgorithm SignatureAlgorithmIdentifier,
signature SignatureValue,
unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL }
SignerIdentifier ::= CHOICE {
issuerAndSerialNumber IssuerAndSerialNumber,
subjectKeyIdentifier [0] SubjectKeyIdentifier }
SignedAttributes ::= SET SIZE (1..MAX) OF Attribute
UnsignedAttributes ::= SET SIZE (1..MAX) OF Attribute
Attribute ::= SEQUENCE {
attrType OBJECT IDENTIFIER,
attrValues SET OF AttributeValue }
AttributeValue ::= ANY
SignatureValue ::= OCTET STRING
```
### 5.2.5 RPKI 对 CMS 外壳字段的 profile 约束RFC 6488 §2.1RFC 6488 §3更新RFC 9589 §4
> 说明:上面是 CMS 的通用 ASN.1RPKI 进一步约束取值与允许出现的字段(例如 SignedData.version 必须为 3、crls 必须省略、signedAttrs 的内容限制等。RFC 6488 §2-§3RFC 9589 §4。
### 5.2.6 signedAttrs 中允许的属性与 attrType OIDRFC 6488 §2.1.6.4.1-§2.1.6.4.2更新RFC 9589 §4
RPKI signed object profile 对 `SignerInfo.signedAttrs` 的 Attribute 集合施加限制(除 ASN.1 结构外,还包含“只允许哪些 attrType”的编码约束
- `content-type`attrType OID `1.2.840.113549.1.9.3`。RFC 6488 §2.1.6.4.1。
- `message-digest`attrType OID `1.2.840.113549.1.9.4`。RFC 6488 §2.1.6.4.2。
- `signing-time`attrType OID `1.2.840.113549.1.9.5`。RFC 9589 §4更新 RFC 6488 的相关要求)。
并且:
- 每种属性在集合中只能出现一次;且 `attrValues` 虽然语法是 `SET OF`,但在 RPKI 中必须只含一个值。RFC 6488 §2.1.6.4。
## 5.3 抽象数据模型(接口)
### 5.3.1 `RpkiSignedObject`
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|---|---|---|---|---|
| `raw_der` | `DerBytes` | CMS DER | 原样保留(建议) | RFC 6488 §2RFC 6488 §3(1l) |
| `content_info_content_type` | `Oid` | ContentInfo.contentType | MUST 为 SignedData`1.2.840.113549.1.7.2` | RFC 6488 §3(1a) |
| `signed_data` | `SignedDataProfiled` | SignedData 语义字段 | 见下 | RFC 6488 §2.1RFC 6488 §3 |
### 5.3.2 `SignedDataProfiled`
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|---|---|---|---|---|
| `version` | `int` | SignedData.version | MUST 为 3 | RFC 6488 §3(1b)RFC 6488 §2.1.1 |
| `digest_algorithms` | `list[Oid]` | SignedData.digestAlgorithms | MUST contain exactly one digest algorithm且必须为 `id-sha256``2.16.840.1.101.3.4.2.1` | RFC 6488 §2.1.2RFC 7935 §2引用 RFC 5754 |
| `encap_content_info` | `EncapsulatedContentInfo` | EncapsulatedContentInfo | 见下eContentType 由具体对象定义 | RFC 6488 §2.1.3 |
| `certificates` | `list[ResourceEeCertificate]` | SignedData.certificates | MUST present且仅包含 1 个 EE 证书;该 EE 的 SKI 必须匹配 SignerInfo.sid | RFC 6488 §3(1c) |
| `crls` | `None` | SignedData.crls | MUST be omitted | RFC 6488 §3(1d) |
| `signer_infos` | `list[SignerInfoProfiled]` | SignedData.signerInfos | MUST contain exactly one SignerInfo | RFC 6488 §2.1RFC 6488 §2.1SignerInfos 约束段落) |
### 5.3.3 `EncapsulatedContentInfo`
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|---|---|---|---|---|
| `econtent_type` | `Oid` | eContentType | MUST 与 signedAttrs.content-type 的 attrValues 一致;具体值由对象定义(如 ROA/MFT | RFC 6488 §3(1h)RFC 6488 §2.1.3.1 |
| `econtent_der` | `DerBytes` | eContent对象 payload | DER 编码的对象特定 ASN.1ROA/MFT 文档定义);在 CMS 中以 OCTET STRING 承载 | RFC 6488 §2.1.3RFC 9286 §4.2RFC 9582 §4 |
### 5.3.4 `SignerInfoProfiled`
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|---|---|---|---|---|
| `version` | `int` | SignerInfo.version | MUST 为 3 | RFC 6488 §3(1e) |
| `sid_ski` | `bytes` | sidSubjectKeyIdentifier | 必须与 EE 证书的 SKI 匹配 | RFC 6488 §3(1c) |
| `digest_algorithm` | `Oid` | SignerInfo.digestAlgorithm | 必须为 `id-sha256``2.16.840.1.101.3.4.2.1` | RFC 6488 §3(1j)RFC 7935 §2引用 RFC 5754 |
| `signature_algorithm` | `Oid` | SignerInfo.signatureAlgorithm | 生成时 MUST 为 `rsaEncryption``1.2.840.113549.1.1.1`);验证时实现必须接受 `rsaEncryption``sha256WithRSAEncryption``1.2.840.113549.1.1.11` | RFC 6488 §3(1k)RFC 7935 §2 |
| `signed_attrs` | `SignedAttrsProfiled` | signedAttrs | MUST present仅允许特定 3 个属性 | RFC 9589 §4更新 RFC 6488 §3(1f)/(1g) |
| `unsigned_attrs` | `None` | unsignedAttrs | MUST be omitted | RFC 6488 §3(1i) |
### 5.3.5 `SignedAttrsProfiled`
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|---|---|---|---|---|
| `content_type` | `Oid` | signedAttrs.content-type | attrType=`1.2.840.113549.1.9.3`MUST presentattrValues 等于 eContentType | RFC 9589 §4RFC 6488 §3(1h) |
| `message_digest` | `bytes` | signedAttrs.message-digest | attrType=`1.2.840.113549.1.9.4`MUST present | RFC 9589 §4更新 RFC 6488 §3(1f) |
| `signing_time` | `UtcTime` | signedAttrs.signing-time | attrType=`1.2.840.113549.1.9.5`MUST present时间值正确性不用于安全假设 | RFC 9589 §4RFC 9589 §5 |
| `other_attrs` | `None` | 其它 signed attributes | MUST NOT be includedbinary-signing-time 也不允许) | RFC 9589 §4 |
## 5.4 字段级约束清单(实现对照)
- ContentInfo.contentType 必须为 SignedDataOID `1.2.840.113549.1.7.2`。RFC 6488 §3(1a)。
- SignedData.version 必须为 3且 SignerInfos 仅允许 1 个 SignerInfo。RFC 6488 §3(1b)RFC 6488 §2.1。
- SignedData.certificates 必须存在且仅含 1 个 EE 证书;该证书 SKI 必须匹配 SignerInfo.sid。RFC 6488 §3(1c)。
- SignedData.crls 必须省略。RFC 6488 §3(1d)。
- signedAttrs 必须存在,且仅允许 content-type/message-digest/signing-time其它全部禁止。RFC 9589 §4。
- eContentType 必须与 content-type attribute 一致。RFC 6488 §3(1h)。
- unsignedAttrs 必须省略。RFC 6488 §3(1i)。
- digest/signature 算法必须符合算法 profile。RFC 6488 §3(1j)/(1k)RFC 7935 §2。
- 整个对象必须 DER 编码。RFC 6488 §3(1l)。

106
specs/06_manifest_mft.md Normal file
View File

@ -0,0 +1,106 @@
# 06. ManifestMFT
## 6.1 对象定位
Manifest 是 CA 发布点内对象的“清单”(文件名 + hash用于 RP 侧检测删除/替换/回放等不一致情况。RFC 9286 §1RFC 9286 §6。
Manifest 是一种 RPKI Signed ObjectCMS 外壳遵循 RFC 6488/9589eContent 遵循 RFC 9286。RFC 9286 §4RFC 6488 §4RFC 9589 §4。
## 6.2 原始载体与编码
- 外壳CMS SignedData DER`05_signed_object_cms.md`。RFC 9286 §4RFC 6488 §2。
- eContentType`id-ct-rpkiManifest`OID `1.2.840.113549.1.9.16.1.26`。RFC 9286 §4.1。
- eContentDER 编码 ASN.1 `Manifest`。RFC 9286 §4.2。
### 6.2.1 eContentType 与 eContent 的 ASN.1 定义RFC 9286 §4.1RFC 9286 §4.2
Manifest 是一种 RPKI signed objectCMS 外壳见 `05_signed_object_cms.md`)。其 `eContentType``eContent` 的 ASN.1 由 RFC 9286 明确定义。RFC 9286 §4。
**eContentTypeOID**RFC 9286 §4.1。
```asn1
id-smime OBJECT IDENTIFIER ::= { iso(1) member-body(2) us(840)
rsadsi(113549) pkcs(1) pkcs9(9) 16 }
id-ct OBJECT IDENTIFIER ::= { id-smime 1 }
id-ct-rpkiManifest OBJECT IDENTIFIER ::= { id-ct 26 }
```
**eContentManifest 结构)**RFC 9286 §4.2。
```asn1
Manifest ::= SEQUENCE {
version [0] INTEGER DEFAULT 0,
manifestNumber INTEGER (0..MAX),
thisUpdate GeneralizedTime,
nextUpdate GeneralizedTime,
fileHashAlg OBJECT IDENTIFIER,
fileList SEQUENCE SIZE (0..MAX) OF FileAndHash
}
FileAndHash ::= SEQUENCE {
file IA5String,
hash BIT STRING
}
```
解码要点:
- `fileHashAlg` 决定 `FileAndHash.hash` 的算法与输出长度RPKI profile 要求 SHA-256。RFC 9286 §4.2.1RFC 7935 §2。
- `hash` 在 ASN.1 中是 BIT STRING但 hash 输出是按字节的比特串DER 编码时应为 “unused bits = 0” 的 octet-aligned BIT STRING实现可据此做一致性检查。RFC 9286 §4.2。
## 6.3 解析规则eContent 语义层)
输入:`RpkiSignedObject`
1) 先按通用 Signed Object 外壳解析得到 `encap_content_info.econtent_type``econtent_der`。RFC 6488 §3RFC 9589 §4。
2) 要求 `econtent_type == 1.2.840.113549.1.9.16.1.26`。RFC 9286 §4.1RFC 9286 §4.4(1)。
3) 将 `econtent_der` 以 DER 解析为 `Manifest` ASN.1。RFC 9286 §4.2。
4) 将 `fileList` 映射为语义字段 `files: list[FileAndHash]`,其中 `hash``fileHashAlg` 对应算法的输出字节序列。RFC 9286 §4.2.1fileHashAlg/fileList 定义)。
## 6.4 抽象数据模型(接口)
### 6.4.1 `ManifestObject`
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|---|---|---|---|---|
| `signed_object` | `RpkiSignedObject` | CMS 外壳 | 外壳约束见 RFC 6488/9589 | RFC 9286 §4RFC 6488 §3RFC 9589 §4 |
| `econtent_type` | `Oid` | eContentType | 必须为 `1.2.840.113549.1.9.16.1.26` | RFC 9286 §4.1 |
| `manifest` | `ManifestEContent` | eContent 语义对象 | 见下 | RFC 9286 §4.2 |
### 6.4.2 `ManifestEContent`
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|---|---|---|---|---|
| `version` | `int` | Manifest.version | MUST 为 0 | RFC 9286 §4.2.1version |
| `manifest_number` | `int` | manifestNumber | 0..MAX可达 20 octetsissuer 必须单调递增RP 必须可处理至 20 octets | RFC 9286 §4.2RFC 9286 §4.2.1manifestNumber |
| `this_update` | `UtcTime` | thisUpdate | 由 ASN.1 `GeneralizedTime` 解析为 UTC 时间点;且必须比先前生成的 manifest 更新 | RFC 9286 §4.2RFC 9286 §4.2.1thisUpdate |
| `next_update` | `UtcTime` | nextUpdate | 由 ASN.1 `GeneralizedTime` 解析为 UTC 时间点;且必须晚于 thisUpdate | RFC 9286 §4.2RFC 9286 §4.2.1nextUpdate |
| `file_hash_alg` | `Oid` | fileHashAlg | 必须为 `id-sha256``2.16.840.1.101.3.4.2.1` | RFC 9286 §4.2.1fileHashAlgRFC 7935 §2引用 RFC 5754 |
| `files` | `list[FileAndHash]` | fileList | `SEQUENCE SIZE (0..MAX)`;每项含文件名与 hash | RFC 9286 §4.2RFC 9286 §4.2.1fileList |
### 6.4.3 `FileAndHash`
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|---|---|---|---|---|
| `file_name` | `string` | 文件名(不含路径) | 字符集限制:`[a-zA-Z0-9-_]+` + `.` + 三字母扩展;扩展必须在 IANA “RPKI Repository Name Schemes” 注册表中 | RFC 9286 §4.2.2 |
| `hash_bytes` | `bytes` | 文件内容 hash | 由 `file_hash_alg` 指定算法计算 | RFC 9286 §4.2.1fileHashAlg/fileList |
## 6.5 字段级约束清单(实现对照)
- eContentType 必须为 `id-ct-rpkiManifest`OID `1.2.840.113549.1.9.16.1.26`。RFC 9286 §4.1。
- eContent 必须 DER 编码且符合 `Manifest` ASN.1。RFC 9286 §4.2。
- `version` 必须为 0。RFC 9286 §4.2.1。
- `manifestNumber` 由 issuer 单调递增RP 必须能处理至 20 octetsissuer 不得超过 20 octets。RFC 9286 §4.2.1。
- `nextUpdate` 必须晚于 `thisUpdate`。RFC 9286 §4.2.1。
- `fileHashAlg` 必须符合算法 profileSHA-256。RFC 9286 §4.2.1RFC 7935 §2。
- `fileList``file` 名称字符集与扩展名受限;实现需按 RFC 限制解析并保留大小写语义。RFC 9286 §4.2.2。
## 6.6 与 EE 证书的语义约束(为后续验证准备)
Manifest 使用“one-time-use EE certificate”进行签名验证规范对该 EE 证书的使用方式给出约束:
- Manifest 相关 EE 证书应为 one-time-use每次新 manifest 生成新密钥对/新 EE。RFC 9286 §4Section 4 前导段落)。
- 用于验证 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(生成步骤)。

159
specs/07_roa.md Normal file
View File

@ -0,0 +1,159 @@
# 07. ROARoute Origin Authorization
## 7.1 对象定位
ROA 是一种 RPKI Signed Object用于声明“某 AS 被授权起源某些前缀”。RFC 9582 §1RFC 9582 §4。
ROA 由 CMS 外壳 + ROA eContent 组成:
- 外壳RFC 6488更新RFC 9589
- eContentRFC 9582
## 7.2 原始载体与编码
- 外壳CMS SignedData DER`05_signed_object_cms.md`。RFC 9582 §1引用 RFC 6488
- eContentType`id-ct-routeOriginAuthz`OID `1.2.840.113549.1.9.16.1.24`。RFC 9582 §3。
- eContentDER 编码 ASN.1 `RouteOriginAttestation`。RFC 9582 §4。
### 7.2.1 eContentType 与 eContent 的 ASN.1 定义RFC 9582 §3RFC 9582 §4
ROA 是一种 RPKI signed objectCMS 外壳见 `05_signed_object_cms.md`。RFC 9582 定义了其 `eContentType` 以及 `eContent`payload的 ASN.1。RFC 9582 §3-§4。
**eContentTypeOID**RFC 9582 §3。
```asn1
id-ct-routeOriginAuthz OBJECT IDENTIFIER ::=
{ iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1)
pkcs-9(9) id-smime(16) id-ct(1) routeOriginAuthz(24) }
```
**eContentROA ASN.1 模块)**RFC 9582 §4。
```asn1
RPKI-ROA-2023
{ iso(1) member-body(2) us(840) rsadsi(113549)
pkcs(1) pkcs9(9) smime(16) mod(0)
id-mod-rpkiROA-2023(75) }
DEFINITIONS EXPLICIT TAGS ::=
BEGIN
IMPORTS
CONTENT-TYPE
FROM CryptographicMessageSyntax-2010 -- in [RFC6268]
{ iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1)
pkcs-9(9) smime(16) modules(0) id-mod-cms-2009(58) } ;
ct-routeOriginAttestation CONTENT-TYPE ::=
{ TYPE RouteOriginAttestation
IDENTIFIED BY id-ct-routeOriginAuthz }
id-ct-routeOriginAuthz OBJECT IDENTIFIER ::=
{ iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1)
pkcs-9(9) id-smime(16) id-ct(1) routeOriginAuthz(24) }
RouteOriginAttestation ::= SEQUENCE {
version [0] INTEGER DEFAULT 0,
asID ASID,
ipAddrBlocks SEQUENCE (SIZE(1..2)) OF ROAIPAddressFamily }
ASID ::= INTEGER (0..4294967295)
ROAIPAddressFamily ::= SEQUENCE {
addressFamily ADDRESS-FAMILY.&afi ({AddressFamilySet}),
addresses ADDRESS-FAMILY.&Addresses
({AddressFamilySet}{@addressFamily}) }
ADDRESS-FAMILY ::= CLASS {
&afi OCTET STRING (SIZE(2)) UNIQUE,
&Addresses
} WITH SYNTAX { AFI &afi ADDRESSES &Addresses }
AddressFamilySet ADDRESS-FAMILY ::=
{ addressFamilyIPv4 | addressFamilyIPv6 }
addressFamilyIPv4 ADDRESS-FAMILY ::=
{ AFI afi-IPv4 ADDRESSES ROAAddressesIPv4 }
addressFamilyIPv6 ADDRESS-FAMILY ::=
{ AFI afi-IPv6 ADDRESSES ROAAddressesIPv6 }
afi-IPv4 OCTET STRING ::= '0001'H
afi-IPv6 OCTET STRING ::= '0002'H
ROAAddressesIPv4 ::= SEQUENCE (SIZE(1..MAX)) OF ROAIPAddress{ub-IPv4}
ROAAddressesIPv6 ::= SEQUENCE (SIZE(1..MAX)) OF ROAIPAddress{ub-IPv6}
ub-IPv4 INTEGER ::= 32
ub-IPv6 INTEGER ::= 128
ROAIPAddress {INTEGER: ub} ::= SEQUENCE {
address BIT STRING (SIZE(0..ub)),
maxLength INTEGER (0..ub) OPTIONAL }
END
```
编码/解码要点(与上面 ASN.1 结构直接对应):
- `addressFamily` 仅允许 IPv4/IPv6 两种 AFI并且每个 AFI 最多出现一次。RFC 9582 §4.3.1。
- `address` 是 BIT STRING 表示的前缀,语义与 RFC 3779 的 `IPAddress` 一致按前缀长度截断DER unused bits 置零。RFC 9582 §4.3.2.1(引用 RFC 3779 §2.2.3.8)。
- `maxLength` 为可选字段出现与否会影响语义与编码规范约束例如等于前缀长时不建议编码。RFC 9582 §4.3.2.2。
## 7.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.24`。RFC 9582 §3。
3) 将 `econtent_der` 以 DER 解析为 `RouteOriginAttestation` ASN.1。RFC 9582 §4。
4) 将 `ipAddrBlocks` 解析为“前缀集合”的语义结构,并建议按 RFC 9582 给出的 canonicalization 过程做去重/排序/归一化以便后续处理一致。RFC 9582 §4.3.3。
## 7.4 抽象数据模型(接口)
### 7.4.1 `RoaObject`
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|---|---|---|---|---|
| `signed_object` | `RpkiSignedObject` | CMS 外壳 | 外壳约束见 RFC 6488/9589 | RFC 9582 §1RFC 6488 §3RFC 9589 §4 |
| `econtent_type` | `Oid` | eContentType | 必须为 `1.2.840.113549.1.9.16.1.24` | RFC 9582 §3 |
| `roa` | `RoaEContent` | eContent 语义对象 | 见下 | RFC 9582 §4 |
### 7.4.2 `RoaEContent`RouteOriginAttestation
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|---|---|---|---|---|
| `version` | `int` | version | MUST 为 0 | RFC 9582 §4.1 |
| `as_id` | `int` | asID | 0..4294967295 | RFC 9582 §4ASID 定义RFC 9582 §4.2 |
| `ip_addr_blocks` | `list[RoaIpAddressFamily]` | ipAddrBlocks | `SIZE(1..2)`;最多 IPv4/IPv6 各一个;建议 canonicalize | RFC 9582 §4RFC 9582 §4.3.1RFC 9582 §4.3.3 |
### 7.4.3 `RoaIpAddressFamily`
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|---|---|---|---|---|
| `afi` | `enum { ipv4, ipv6 }` | Address Family | MUST 为 IPv4(0001) 或 IPv6(0002) | RFC 9582 §4.3.1 |
| `addresses` | `list[RoaIpAddress]` | 前缀列表 | `SIZE(1..MAX)`;每项为前缀 + 可选 maxLength | RFC 9582 §4ROAAddressesIPv4/IPv6RFC 9582 §4.3.2 |
### 7.4.4 `RoaIpAddress`
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|---|---|---|---|---|
| `prefix` | `IpPrefix` | 前缀 | address 以 BIT STRING 表示前缀(同 RFC 3779 IPAddress 表示) | RFC 9582 §4.3.2.1(引用 RFC 3779 §2.2.3.8 |
| `max_length` | `optional[int]` | 最大允许前缀长 | 若存在:必须 `>= prefix_len``<= 32/128`;并且 `maxLength == prefix_len`**SHOULD NOT** 编码(未来 RP 可能视为编码错误) | RFC 9582 §4.3.2.2 |
## 7.5 字段级约束清单(实现对照)
- eContentType 必须为 `id-ct-routeOriginAuthz`OID `1.2.840.113549.1.9.16.1.24`),且该 OID 必须同时出现在 eContentType 与 signedAttrs.content-type。RFC 9582 §3引用 RFC 6488
- eContent 必须 DER 编码并符合 `RouteOriginAttestation` ASN.1。RFC 9582 §4。
- `version` 必须为 0。RFC 9582 §4.1。
- `ipAddrBlocks` 长度为 1..2;每种 AFI 最多出现一次;仅支持 IPv4/IPv6。RFC 9582 §4RFC 9582 §4.3.1。
- `maxLength` 若存在必须在范围内且不应出现“等于前缀长”的冗余编码。RFC 9582 §4.3.2.2。
- 建议按 canonical form 归一化/排序以利一致处理。RFC 9582 §4.3.3。
## 7.6 与 EE 证书的语义约束(为后续验证准备)
ROA 的外壳包含一个 EE 证书,用于验证 ROA 签名RFC 对该 EE 证书与 ROA payload 的匹配关系提出要求:
- ROA 的 EE 证书必须是有效的 RPKI EE 证书(路径从 TA 到 EE 可建立),并用于验证 CMS 签名。RFC 9582 §1引用 RFC 6488RFC 6488 §3(2)-(3)。
- ROA EE 证书中的 IP 资源扩展必须存在且不得使用 inherit。RFC 9582 §5。
- ROA EE 证书中 AS 资源扩展不得出现。RFC 9582 §5。
- ROA payload 中每个前缀必须包含在 EE 证书的 IP 资源集合内(资源包含语义来自 RFC 3779。RFC 9582 §5RFC 3779 §2.3。

108
src/data_model/common.rs Normal file
View File

@ -0,0 +1,108 @@
use x509_parser::asn1_rs::Tag;
use x509_parser::x509::AlgorithmIdentifier;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Asn1TimeEncoding {
UtcTime,
GeneralizedTime,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Asn1TimeUtc {
pub utc: time::OffsetDateTime,
pub encoding: Asn1TimeEncoding,
}
impl Asn1TimeUtc {
/// Validate Time encoding rules (RFC 5280): years 1950-2049 use UTCTime,
/// other years use GeneralizedTime.
pub fn validate_encoding_rfc5280(
&self,
field: &'static str,
) -> Result<(), InvalidTimeEncodingError> {
let year = self.utc.year();
let expected = if year <= 2049 {
Asn1TimeEncoding::UtcTime
} else {
Asn1TimeEncoding::GeneralizedTime
};
if self.encoding != expected {
return Err(InvalidTimeEncodingError {
field,
year,
encoding: self.encoding,
});
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigUnsigned {
/// Minimal big-endian bytes. For zero, this is `[0]`.
pub bytes_be: Vec<u8>,
}
impl BigUnsigned {
pub fn from_biguint(n: &der_parser::num_bigint::BigUint) -> Self {
let mut bytes = n.to_bytes_be();
if bytes.is_empty() {
bytes.push(0);
}
Self { bytes_be: bytes }
}
pub fn to_hex_upper(&self) -> String {
hex::encode_upper(&self.bytes_be)
}
pub fn to_u64(&self) -> Option<u64> {
if self.bytes_be.len() > 8 {
return None;
}
let mut value: u64 = 0;
for &b in &self.bytes_be {
value = (value << 8) | (b as u64);
}
Some(value)
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("{field} time encoding invalid for year {year}: got {encoding:?}")]
pub struct InvalidTimeEncodingError {
pub field: &'static str,
pub year: i32,
pub encoding: Asn1TimeEncoding,
}
pub fn asn1_time_to_model(t: x509_parser::time::ASN1Time) -> Asn1TimeUtc {
let encoding = if t.is_utctime() {
Asn1TimeEncoding::UtcTime
} else {
Asn1TimeEncoding::GeneralizedTime
};
Asn1TimeUtc {
utc: t.to_datetime(),
encoding,
}
}
pub fn algorithm_params_absent_or_null(sig: &AlgorithmIdentifier<'_>) -> bool {
match sig.parameters.as_ref() {
None => true,
Some(p) if p.tag() == Tag::Null => true,
Some(_p) => false,
}
}
/// Filename extensions registered in IANA "RPKI Repository Name Schemes".
///
/// Source: <https://www.iana.org/assignments/rpki/rpki.xhtml>
/// Snapshot date: 2026-01-28.
///
/// Notes:
/// - Includes entries marked TEMPORARY/DEPRECATED by IANA (e.g., `asa`, `gbr`).
pub const IANA_RPKI_REPOSITORY_FILENAME_EXTENSIONS: &[&str] = &[
"asa", "cer", "crl", "gbr", "mft", "roa", "sig", "tak",
];

View File

@ -1,52 +1,16 @@
pub use crate::data_model::common::{Asn1TimeEncoding, Asn1TimeUtc, BigUnsigned};
use crate::data_model::oid::{
OID_AUTHORITY_KEY_IDENTIFIER, OID_CRL_NUMBER, OID_SHA256_WITH_RSA_ENCRYPTION,
OID_SUBJECT_KEY_IDENTIFIER,
};
use x509_parser::extensions::{AuthorityKeyIdentifier, ParsedExtension, X509Extension};
use x509_parser::prelude::FromDer;
use x509_parser::prelude::X509Version;
use x509_parser::revocation_list::CertificateRevocationList;
use x509_parser::asn1_rs::Tag;
use x509_parser::certificate::X509Certificate;
use x509_parser::x509::SubjectPublicKeyInfo;
use x509_parser::x509::AlgorithmIdentifier;
const OID_SHA256_WITH_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.11";
const OID_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35";
const OID_CRL_NUMBER: &str = "2.5.29.20";
const OID_SUBJECT_KEY_IDENTIFIER: &str = "2.5.29.14";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Asn1TimeEncoding {
UtcTime,
GeneralizedTime,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Asn1TimeUtc {
pub utc: time::OffsetDateTime,
pub encoding: Asn1TimeEncoding,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigUnsigned {
/// Minimal big-endian bytes. For zero, this is `[0]`.
pub bytes_be: Vec<u8>,
}
impl BigUnsigned {
pub fn to_hex_upper(&self) -> String {
hex::encode_upper(&self.bytes_be)
}
pub fn to_u64(&self) -> Option<u64> {
if self.bytes_be.len() > 8 {
return None;
}
let mut value: u64 = 0;
for &b in &self.bytes_be {
value = (value << 8) | (b as u64);
}
Some(value)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RevokedCert {
pub serial_number: BigUnsigned,
@ -161,23 +125,23 @@ impl RpkixCrl {
if !rc.extensions().is_empty() {
return Err(CrlDecodeError::EntryExtensionsNotAllowed);
}
let revocation_date = asn1_time_to_model(rc.revocation_date);
validate_time_encoding("revocationDate", &revocation_date)?;
let revocation_date = crate::data_model::common::asn1_time_to_model(rc.revocation_date);
validate_time_encoding_rfc5280("revocationDate", &revocation_date)?;
Ok(RevokedCert {
serial_number: biguint_to_big_unsigned(rc.serial()),
serial_number: BigUnsigned::from_biguint(rc.serial()),
revocation_date,
})
})
.collect::<Result<Vec<_>, _>>()?;
let this_update = asn1_time_to_model(crl.last_update());
validate_time_encoding("thisUpdate", &this_update)?;
let this_update = crate::data_model::common::asn1_time_to_model(crl.last_update());
validate_time_encoding_rfc5280("thisUpdate", &this_update)?;
let next_update = crl
.next_update()
.map(asn1_time_to_model)
.map(crate::data_model::common::asn1_time_to_model)
.ok_or(CrlDecodeError::NextUpdateMissing)?;
validate_time_encoding("nextUpdate", &next_update)?;
validate_time_encoding_rfc5280("nextUpdate", &next_update)?;
Ok(RpkixCrl {
raw_der: der.to_vec(),
@ -301,19 +265,10 @@ pub enum CrlVerifyError {
InvalidSignature(String),
}
fn asn1_time_to_model(t: x509_parser::time::ASN1Time) -> Asn1TimeUtc {
let encoding = if t.is_utctime() {
Asn1TimeEncoding::UtcTime
} else {
Asn1TimeEncoding::GeneralizedTime
};
Asn1TimeUtc {
utc: t.to_datetime(),
encoding,
}
}
fn validate_time_encoding(field: &'static str, t: &Asn1TimeUtc) -> Result<(), CrlDecodeError> {
fn validate_time_encoding_rfc5280(
field: &'static str,
t: &Asn1TimeUtc,
) -> Result<(), CrlDecodeError> {
let year = t.utc.year();
let expected = if year <= 2049 {
Asn1TimeEncoding::UtcTime
@ -331,10 +286,10 @@ fn validate_time_encoding(field: &'static str, t: &Asn1TimeUtc) -> Result<(), Cr
}
fn validate_sig_params(sig: &AlgorithmIdentifier<'_>) -> Result<(), CrlDecodeError> {
match sig.parameters.as_ref() {
None => Ok(()),
Some(p) if p.tag() == Tag::Null => Ok(()),
Some(_p) => Err(CrlDecodeError::InvalidSignatureAlgorithmParameters),
if crate::data_model::common::algorithm_params_absent_or_null(sig) {
Ok(())
} else {
Err(CrlDecodeError::InvalidSignatureAlgorithmParameters)
}
}
@ -367,7 +322,7 @@ fn parse_and_validate_extensions(exts: &[X509Extension<'_>]) -> Result<CrlExtens
if n.bits() > 159 {
return Err(CrlDecodeError::CrlNumberOutOfRange);
}
crl_number = Some(biguint_to_big_unsigned(&n));
crl_number = Some(BigUnsigned::from_biguint(&n));
}
_ => return Err(CrlDecodeError::UnsupportedExtension(oid)),
}
@ -410,14 +365,6 @@ fn parse_crl_number(ext: &X509Extension<'_>) -> Result<der_parser::num_bigint::B
}
}
fn biguint_to_big_unsigned(n: &der_parser::num_bigint::BigUint) -> BigUnsigned {
let mut bytes = n.to_bytes_be();
if bytes.is_empty() {
bytes.push(0);
}
BigUnsigned { bytes_be: bytes }
}
fn get_subject_key_identifier(cert: &X509Certificate<'_>) -> Option<Vec<u8>> {
cert.extensions()
.iter()

284
src/data_model/manifest.rs Normal file
View File

@ -0,0 +1,284 @@
use crate::data_model::common::BigUnsigned;
use crate::data_model::oid::{OID_CT_RPKI_MANIFEST, OID_SHA256};
use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError};
use der_parser::ber::BerObjectContent;
use der_parser::der::{parse_der, DerObject, Tag};
use time::OffsetDateTime;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ManifestObject {
pub signed_object: RpkiSignedObject,
pub econtent_type: String,
pub manifest: ManifestEContent,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ManifestEContent {
pub version: u32,
pub manifest_number: BigUnsigned,
pub this_update: OffsetDateTime,
pub next_update: OffsetDateTime,
pub file_hash_alg: String,
pub files: Vec<FileAndHash>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FileAndHash {
pub file_name: String,
pub hash_bytes: Vec<u8>,
}
#[derive(Debug, thiserror::Error)]
pub enum ManifestDecodeError {
#[error("signed object decode error: {0}")]
SignedObject(#[from] SignedObjectDecodeError),
#[error("DER parse error: {0}")]
Parse(String),
#[error("trailing bytes after DER object: {0} bytes")]
TrailingBytes(usize),
#[error("eContentType must be id-ct-rpkiManifest ({OID_CT_RPKI_MANIFEST}), got {0}")]
InvalidEContentType(String),
#[error("Manifest must be a SEQUENCE of 5 or 6 elements, got {0}")]
InvalidManifestSequenceLen(usize),
#[error("Manifest.version must be 0, got {0}")]
InvalidManifestVersion(u64),
#[error("Manifest.manifestNumber must be non-negative INTEGER")]
InvalidManifestNumber,
#[error("Manifest.manifestNumber longer than 20 octets")]
ManifestNumberTooLong,
#[error("Manifest.thisUpdate must be GeneralizedTime")]
InvalidThisUpdate,
#[error("Manifest.nextUpdate must be GeneralizedTime")]
InvalidNextUpdate,
#[error("Manifest.nextUpdate must be later than thisUpdate")]
NextUpdateNotLater,
#[error("Manifest.fileHashAlg must be id-sha256 ({OID_SHA256}), got {0}")]
InvalidFileHashAlg(String),
#[error("Manifest.fileList must be a SEQUENCE")]
InvalidFileList,
#[error("FileAndHash must be SEQUENCE of 2")]
InvalidFileAndHash,
#[error("fileList file name invalid: {0}")]
InvalidFileName(String),
#[error("fileList hash must be BIT STRING")]
InvalidHashType,
#[error("fileList hash BIT STRING must be octet-aligned (unused bits=0)")]
HashNotOctetAligned,
#[error("fileList hash length invalid for sha256: got {0} bytes")]
InvalidHashLength(usize),
}
impl ManifestObject {
pub fn decode_der(der: &[u8]) -> Result<Self, ManifestDecodeError> {
let signed_object = RpkiSignedObject::decode_der(der)?;
Self::from_signed_object(signed_object)
}
pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result<Self, ManifestDecodeError> {
let econtent_type = signed_object
.signed_data
.encap_content_info
.econtent_type
.clone();
if econtent_type != OID_CT_RPKI_MANIFEST {
return Err(ManifestDecodeError::InvalidEContentType(econtent_type));
}
let manifest = ManifestEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?;
Ok(Self {
signed_object,
econtent_type: OID_CT_RPKI_MANIFEST.to_string(),
manifest,
})
}
}
impl ManifestEContent {
/// Decode the DER-encoded Manifest eContent defined in RFC 9286 §4.2.
pub fn decode_der(der: &[u8]) -> Result<Self, ManifestDecodeError> {
let (rem, obj) = parse_der(der).map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
if !rem.is_empty() {
return Err(ManifestDecodeError::TrailingBytes(rem.len()));
}
let seq = obj
.as_sequence()
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
if seq.len() != 5 && seq.len() != 6 {
return Err(ManifestDecodeError::InvalidManifestSequenceLen(seq.len()));
}
let mut idx = 0;
let mut version: u32 = 0;
if seq.len() == 6 {
let v_obj = &seq[0];
if v_obj.class() != der_parser::ber::Class::ContextSpecific || v_obj.tag() != Tag(0) {
return Err(ManifestDecodeError::Parse(
"Manifest.version must be [0] EXPLICIT INTEGER".into(),
));
}
let inner_der = v_obj
.as_slice()
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
let (rem, inner) =
parse_der(inner_der).map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
if !rem.is_empty() {
return Err(ManifestDecodeError::Parse(
"trailing bytes inside Manifest.version".into(),
));
}
let v = inner
.as_u64()
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
if v != 0 {
return Err(ManifestDecodeError::InvalidManifestVersion(v));
}
version = 0;
idx = 1;
}
let manifest_number = parse_manifest_number(&seq[idx])?;
idx += 1;
let this_update =
parse_generalized_time(&seq[idx], ManifestDecodeError::InvalidThisUpdate)?;
idx += 1;
let next_update =
parse_generalized_time(&seq[idx], ManifestDecodeError::InvalidNextUpdate)?;
idx += 1;
if next_update <= this_update {
return Err(ManifestDecodeError::NextUpdateNotLater);
}
let file_hash_alg = oid_to_string(&seq[idx])?;
idx += 1;
if file_hash_alg != OID_SHA256 {
return Err(ManifestDecodeError::InvalidFileHashAlg(file_hash_alg));
}
let files = parse_file_list_sha256(&seq[idx])?;
Ok(Self {
version,
manifest_number,
this_update,
next_update,
file_hash_alg: OID_SHA256.to_string(),
files,
})
}
}
fn parse_manifest_number(obj: &DerObject<'_>) -> Result<BigUnsigned, ManifestDecodeError> {
let n = obj
.as_biguint()
.map_err(|_e| ManifestDecodeError::InvalidManifestNumber)?;
let out = BigUnsigned::from_biguint(&n);
if out.bytes_be.len() > 20 {
return Err(ManifestDecodeError::ManifestNumberTooLong);
}
Ok(out)
}
fn parse_generalized_time(
obj: &DerObject<'_>,
err: ManifestDecodeError,
) -> Result<OffsetDateTime, ManifestDecodeError> {
match &obj.content {
BerObjectContent::GeneralizedTime(dt) => dt
.to_datetime()
.map_err(|e| ManifestDecodeError::Parse(e.to_string())),
_ => Err(err),
}
}
fn parse_file_list_sha256(obj: &DerObject<'_>) -> Result<Vec<FileAndHash>, ManifestDecodeError> {
let seq = obj
.as_sequence()
.map_err(|_e| ManifestDecodeError::InvalidFileList)?;
let mut out = Vec::with_capacity(seq.len());
for entry in seq {
let (file_name, hash_bytes) = parse_file_and_hash(entry)?;
validate_file_name(&file_name)?;
if hash_bytes.len() != 32 {
return Err(ManifestDecodeError::InvalidHashLength(hash_bytes.len()));
}
out.push(FileAndHash {
file_name,
hash_bytes,
});
}
Ok(out)
}
fn parse_file_and_hash(obj: &DerObject<'_>) -> Result<(String, Vec<u8>), ManifestDecodeError> {
let seq = obj
.as_sequence()
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
if seq.len() != 2 {
return Err(ManifestDecodeError::InvalidFileAndHash);
}
let file_name = seq[0]
.as_str()
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?
.to_string();
let (unused_bits, bits) = match &seq[1].content {
BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()),
_ => return Err(ManifestDecodeError::InvalidHashType),
};
if unused_bits != 0 {
return Err(ManifestDecodeError::HashNotOctetAligned);
}
Ok((file_name, bits))
}
fn validate_file_name(name: &str) -> Result<(), ManifestDecodeError> {
// RFC 9286 §4.2.2:
// 1+ chars from a-zA-Z0-9-_ , then '.', then 3-letter extension.
let Some((base, ext)) = name.rsplit_once('.') else {
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
};
if base.is_empty() || ext.len() != 3 {
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
}
if !base
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
{
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
}
if !ext.bytes().all(|b| b.is_ascii_alphabetic()) {
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
}
let ext_lower = ext.to_ascii_lowercase();
if !crate::data_model::common::IANA_RPKI_REPOSITORY_FILENAME_EXTENSIONS
.iter()
.any(|&e| e == ext_lower)
{
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
}
Ok(())
}
fn oid_to_string(obj: &DerObject<'_>) -> Result<String, ManifestDecodeError> {
let oid = obj
.as_oid()
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
Ok(oid.to_id_string())
}

View File

@ -1,6 +1,5 @@
pub mod common;
pub mod crl;
mod rc;
mod tal;
mod ta;
mod resources;
mod oids;
pub mod oid;
pub mod signed_object;
pub mod manifest;

20
src/data_model/oid.rs Normal file
View File

@ -0,0 +1,20 @@
pub const OID_SHA256: &str = "2.16.840.1.101.3.4.2.1";
pub const OID_SIGNED_DATA: &str = "1.2.840.113549.1.7.2";
pub const OID_CMS_ATTR_CONTENT_TYPE: &str = "1.2.840.113549.1.9.3";
pub const OID_CMS_ATTR_MESSAGE_DIGEST: &str = "1.2.840.113549.1.9.4";
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";
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";
// 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";

View File

@ -1,13 +0,0 @@
pub const OID_BASIC_CONSTRAINTS: &str = "2.5.29.19";
pub const OID_SUBJECT_KEY_IDENTIFIER: &str = "2.5.29.14";
pub const OID_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35";
pub const OID_KEY_USAGE: &str = "2.5.29.15";
pub const OID_EXTENDED_KEY_USAGE: &str = "2.5.29.37";
pub const OID_CRL_DISTRIBUTION_POINTS: &str = "2.5.29.31";
pub const OID_AUTHORITY_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.1";
pub const OID_ACCESS_DESCRIPTION: &str = "1.3.6.1.5.5.7.48";
pub const OID_AD_CA_ISSUERS: &str = "1.3.6.1.5.5.7.48.2";
pub const OID_AD_OCSP: &str = "1.3.6.1.5.5.7.48.1";
pub const OID_SUBJECT_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.11";
pub const OID_CERTIFICATE_POLICIES: &str = "2.5.29.32";
pub const OID_SHA256_WITH_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.11";

View File

@ -1,519 +0,0 @@
use der_parser::asn1_rs::Tag;
use der_parser::num_bigint::BigUint;
use url::Url;
use time::OffsetDateTime;
use x509_parser::x509::AlgorithmIdentifier;
use x509_parser::prelude::{Validity, KeyUsage, X509Certificate, FromDer,
X509Version, X509Extension, ParsedExtension,
CRLDistributionPoints, DistributionPointName, GeneralName};
use crate::data_model::crl::CrlDecodeError;
use crate::data_model::resources::ip_resources::IPAddrBlocks;
use crate::data_model::resources::as_resources::ASIdentifiers;
use crate::data_model::oids;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SubjectPublicKeyInfo {
pub algorithm_oid: String,
pub subject_public_key: u8,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AccessDescription {
pub access_method_oid: String,
pub access_location: Url,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PolicyInformation {
pub policy_oid: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RcExtension {
pub basic_constraints: bool,
pub subject_key_identifier: u8,
pub authority_key_identifier: u8,
pub key_usage: KeyUsage,
pub extended_key_usage_oid: u8,
pub crl_distribution_points: Vec<Url>,
pub authority_info_access: Vec<AccessDescription>,
pub subject_info_access: Vec<AccessDescription>,
pub certificate_policies: Vec<PolicyInformation>,
pub ip_resource: IPAddrBlocks,
pub as_resource: ASIdentifiers,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResourceCert {
/// 证书原始DER内容
pub cert_der: Vec<u8>,
/// 基本证书信息
pub version: u32,
pub serial_number: BigUint,
pub signature_algorithm_oid: String,
pub issuer_dn: String,
pub subject_dn: String,
pub validity: Validity,
pub subject_public_key_info: SubjectPublicKeyInfo,
pub extensions: RcExtension,
}
#[derive(Debug, thiserror::Error)]
pub enum ResourceCertError {
#[error("X.509 parse resource cert error: {0}")]
ParseCert(String),
#[error("trailing bytes after CRL DER: {0} bytes")]
TrailingBytes(usize),
#[error("invalid version {0}")]
InvalidVersion(u32),
#[error("signatureAlgorithm does not match tbsCertificate.signature")]
SignatureAlgorithmMismatch,
#[error("unsupported signature algorithm")]
UnsupportedSignatureAlgorithm,
#[error("invalid Cert signature algorithm parameters")]
InvalidSignatureParameters,
#[error("invalid Cert validity range")]
InvalidValidityRange,
#[error("Cert not yet valid")]
NotYetValid,
#[error("expired")]
Expired,
#[error("Critical error, {0} should be {1}")]
CriticalError(String, String),
#[error("Duplicate Extension: {0}")]
DuplicateExtension(String),
#[error("AKI missing keyIdentifier")]
AkiMissingKeyIdentifier,
#[error("Unexpected parameter: {0}")]
UnexceptedParameter(String),
#[error("Missing parameter: {0}")]
MissingParameter(String),
#[error("CRL DP invalid distributionPointName: {0}")]
CrlDpInvalidDistributionPointName(String),
#[error("CRL DP unexpected distributionPointType: {0}")]
CrlDpUnexpectedDistributionPointType(String),
#[error("invalid URI: {0}")]
InvalidUri(String),
#[error("Unsupported General Name in {0}")]
UnsupportedGeneralName(String),
#[error("Unsupported CRL Distribution Point")]
UnsupportedCrlDistributionPoint,
#[error("Invalid Access Location Type")]
InvalidAccessLocationType,
#[error("Empty AuthorityInfoAccess!")]
EmptyAuthorityInfoAccess,
}
// impl ResourceCert{
// pub fn from_der(cert_der: &[u8]) -> Result<Self, ResourceCertError> {
// let (rem, x509_rc) = X509Certificate::from_der(cert_der)
// .map_err(|e| ResourceCertError::ParseCert(e.to_string()))?;
//
// if !rem.is_empty() {
// return Err(ResourceCertError::TrailingBytes(rem.len()));
// }
//
// // 校验
// parse_and_validate_cert(x509_rc)
// }
//
//
//
// }
//
// fn parse_and_validate_cert(x509_rc: X509Certificate) -> Result<ResourceCert, ResourceCertError> {
// ///逐个校验RC的内容, 如果有任何一个校验失败, 则返回错误
//
// // 1. 版本号必须是V3
// let version = match x509_rc.version() {
// X509Version::V3 => X509Version::V3,
// v => {
// return Err(ResourceCertError::InvalidVersion(v.0));
// }
// };
//
// // 2.校验签名算法
// // 2.1. 校验外层的签名算法与里层的一致
// let outer = &x509_rc.signature_algorithm;
// let inner = &x509_rc.tbs_certificate.signature;
//
// if outer.algorithm != inner.algorithm || outer.parameters != inner.parameters {
// return Err(ResourceCertError::SignatureAlgorithmMismatch);
// }
// //2.2 RPKI的签名算法必须是rsaWithSHA256
// let signature_algorithm = &x509_rc.signature_algorithm;
// if signature_algorithm.algorithm.to_id_string() != oids::OID_SHA256_WITH_RSA_ENCRYPTION {
// return Err(ResourceCertError::UnsupportedSignatureAlgorithm);
// }
// validate_sig_params(signature_algorithm)?;
//
// // 3. 校验Validity
// let validity = x509_rc.validity();
// validate_validity(validity, OffsetDateTime::now_utc())?;
//
// // 4. SubjectPublicKeyInfo
// let subject_public_key_info = x509_rc.tbs_certificate.subject_pki;
//
// let extensions = parse_and_validate_extensions(x509_rc.extensions())?;
//
// Ok(ResourceCert {
// cert_der: x509_rc.to_der().to_vec(),
// version: version.0,
// serial_number: x509_rc.serial(),
// signature_algorithm_oid: signature_algorithm.algorithm.to_id_string(),
// issuer_dn: x509_rc.issuer().to_string(),
// subject_dn: x509_rc.subject().to_string(),
// validity,
// subject_public_key_info: SubjectPublicKeyInfo {
// // algorithm_oid: x509_rc.tbs_certificate.subject_pki.algorithm.algorithm.to_id_string(),
// // subject_public_key: x509_rc.tbs_certificate.subject_pki.subject_public_key.unused_bits,
// },
// extensions,
// })
//
//
// }
//
// fn validate_sig_params(sig: &AlgorithmIdentifier<'_>) -> Result<(), CrlDecodeError> {
// match sig.parameters.as_ref() {
// None => Ok(()),
// Some(p) if p.tag() == Tag::Null => Ok(()),
// Some(_p) => Err(CrlDecodeError::InvalidSignatureAlgorithmParameters),
// }
// }
//
// fn validate_validity(
// validity: &Validity,
// now: OffsetDateTime,
// ) -> Result<(), ResourceCertError> {
// let not_before = validity.not_before.to_datetime();
// let not_after = validity.not_after.to_datetime();
//
// if not_after < not_before {
// return Err(ResourceCertError::InvalidValidityRange);
// }
//
// if now < not_before {
// return Err(ResourceCertError::NotYetValid);
// }
//
// if now > not_after {
// return Err(ResourceCertError::Expired);
// }
//
// Ok(())
// }
//
//
// pub fn parse_and_validate_extensions(
// exts: &[X509Extension<'_>],
// ) -> Result<RcExtension, ResourceCertError> {
// let mut basic_constraints = None;
// let mut ip_addr_blocks = None;
// let mut as_identifiers = None;
// let mut ski = None;
// let mut aki = None;
// let mut crl_dp = None;
// let mut aia = None;
// let mut sia = None;
// let mut key_usage = None;
// let mut extended_key_usage = None;
// let mut certificate_policies = None;
//
// for ext in exts {
// let oid = ext.oid.to_id_string();
// let critical = ext.critical;
// match oid.as_str() {
// oids::OID_BASIC_CONSTRAINTS => {
// if basic_constraints.is_some() {
// return Err(ResourceCertError::DuplicateExtension("basicConstraints".into()));
// }
// if !critical {
// return Err(ResourceCertError::CriticalError("basicConstraints".into(), "critical".into()));
// }
// let bc = parse_basic_constraints(ext)?;
// basic_constraints = Some(bc);
// }
// oids::OID_SUBJECT_KEY_IDENTIFIER => {
// if ski.is_some() {
// return Err(ResourceCertError::DuplicateExtension("subjectKeyIdentifier".into()));
// }
// if critical {
// return Err(ResourceCertError::CriticalError("subjectKeyIdentifier".into(), "non-critical".into()));
// }
// let s = parse_subject_key_identifier(ext)?;
// ski = Some(s);
// }
// oids::OID_AUTHORITY_KEY_IDENTIFIER => {
// if aki.is_some() {
// return Err(ResourceCertError::DuplicateExtension("authorityKeyIdentifier".into()));
// }
// if critical {
// return Err(ResourceCertError::CriticalError("authorityKeyIdentifier".into(), "non-critical".into()));
// }
// let a = parse_authority_key_identifier(ext)?;
// aki = Some(a);
// }
// oids::OID_KEY_USAGE => {
// if key_usage.is_some() {
// return Err(ResourceCertError::DuplicateExtension("keyUsage".into()));
// }
// if !critical {
// return Err(ResourceCertError::CriticalError("keyUsage".into(), "critical".into()));
// }
// let ku = parse_key_usage(ext)?;
// key_usage = Some(ku);
// }
// oids::OID_EXTENDED_KEY_USAGE => {
// if extended_key_usage.is_some() {
// return Err(ResourceCertError::DuplicateExtension("extendedKeyUsage".into()));
// }
// if critical {
// return Err(ResourceCertError::CriticalError("extendedKeyUsage".into(), "non-critical".into()));
// }
// let eku = oids::OID_EXTENDED_KEY_USAGE;
// }
// oids::OID_CRL_DISTRIBUTION_POINTS => {
// if crl_dp.is_some() {
// return Err(ResourceCertError::DuplicateExtension("crlDistributionPoints".into()));
// }
// if critical {
// return Err(ResourceCertError::CriticalError("crlDistributionPoints".into(), "non-critical".into()));
// }
// let cdp = parse_crl_distribution_points(ext)?;
// crl_dp = Some(cdp);
// }
// oids::OID_AUTHORITY_INFO_ACCESS => {
// if aia.is_some() {
// return Err(ResourceCertError::DuplicateExtension("authorityInfoAccess".into()));
// }
// if critical {
// return Err(ResourceCertError::CriticalError("authorityInfoAccess".into(), "non-critical".into()));
// }
// let p_aia = parse_authority_info_access(ext)?;
// aia = Some(p_aia);
// }
// oids::OID_SUBJECT_INFO_ACCESS => {
// if sia.is_some() {
// return Err(ResourceCertError::DuplicateExtension("subjectInfoAccess".into()));
// }
// if critical {
// return Err(ResourceCertError::CriticalError("subjectInfoAccess".into(), "non-critical".into()));
// }
// let p_sia = parse_subject_info_access(ext)?;
// sia = Some(p_sia);
// }
// oids::OID_CERTIFICATE_POLICIES => {
// if certificate_policies.is_some() {
// return Err(ResourceCertError::DuplicateExtension("certificatePolicies".into()));
// }
// if !critical {
// return Err(ResourceCertError::CriticalError("certificatePolicies".into(), "critical".into()));
// }
// let p_cp = parse_certificate_policies(ext)?;
// certificate_policies = Some(p_cp);
// }
// }
//
//
// }
// Ok(RcExtension {
// basic_constraints,
// ip_addr_blocks,
// as_identifiers,
// subject_key_id: ski,
// authority_key_id: aki,
// crl_distribution_points: crl_dp,
// authority_info_access: aia,
// })
// }
//
// fn parse_basic_constraints(ext: &X509Extension<'_>) -> Result<bool, ResourceCertError> {
// let ParsedExtension::BasicConstraints(bc) = ext.parsed_extension() else {
// return Err(ResourceCertError::ParseCert("basicConstraints parse failed".into()));
// };
// Ok(bc.ca)
// }
//
// fn parse_subject_key_identifier(ext: &X509Extension<'_>) -> Result<Vec<u8>, ResourceCertError> {
// let ParsedExtension::SubjectKeyIdentifier(s) = ext.parsed_extension() else {
// return Err(ResourceCertError::ParseCert("subjectKeyIdentifier parse failed".into()));
// };
// Ok(s.0.to_vec())
// }
//
// fn parse_authority_key_identifier(ext: &X509Extension<'_>) -> Result<Vec<u8>, ResourceCertError> {
// let ParsedExtension::AuthorityKeyIdentifier(aki) = ext.parsed_extension() else {
// return Err(ResourceCertError::ParseCert("authorityKeyIdentifier parse failed".into()));
// };
// let key_id = aki
// .key_identifier
// .as_ref()
// .ok_or(ResourceCertError::MissingParameter("key_identifier".into()))?;
//
// if aki.authority_cert_issuer.is_some() {
// return Err(ResourceCertError::UnexceptedParameter("authority_cert_issuer".into()));
// }
// if aki.authority_cert_serial.is_some() {
// return Err(ResourceCertError::UnexceptedParameter("authority_cert_serial".into()));
// }
//
//
// Ok(key_id.0.to_vec())
// }
//
// fn parse_key_usage(ext: &X509Extension<'_>) -> Result<KeyUsage, ResourceCertError> {
// let ParsedExtension::KeyUsage(ku) = ext.parsed_extension() else {
// return Err(ResourceCertError::ParseCert("keyUsage parse failed".into()));
// };
// Ok(ku.clone())
// }
//
// fn parse_crl_distribution_points(ext: &X509Extension<'_>) -> Result<Vec<Url>, ResourceCertError> {
// let ParsedExtension::CRLDistributionPoints(cdp) = ext.parsed_extension() else {
// return Err(ResourceCertError::ParseCert("crlDistributionPoints parse failed".into()));
// };
// let mut urls = Vec::new();
// for point in cdp.points.iter() {
// if point.reasons.is_some() {
// return Err(ResourceCertError::UnexceptedParameter("reasons".into()));
// }
// if point.crl_issuer.is_some() {
// return Err(ResourceCertError::UnexceptedParameter("crl_issuer".into()));
// }
//
// let dp_name = point.distribution_point.as_ref()
// .ok_or(ResourceCertError::MissingParameter("distribution_point".into()))?;
// match dp_name {
// DistributionPointName::FullName(names) => {
// for name in names {
// match name {
// GeneralName::URI(uri) => {
// let url = Url::parse(uri)
// .map_err(|_| ResourceCertError::InvalidUri(uri.to_string()))?;
// urls.push(url);
// }
// _ => {
// return Err(ResourceCertError::UnsupportedGeneralName("distribution_point".into()));
// }
// }
// }
//
// }
// DistributionPointName::NameRelativeToCRLIssuer(_) => {
// return Err(ResourceCertError::UnsupportedCrlDistributionPoint);
// }
// }
// }
// if urls.is_empty() {
// return Err(ResourceCertError::MissingParameter("distribution_point".into()));
// }
// Ok(urls)
// }
//
// fn parse_authority_info_access(
// ext: &X509Extension<'_>,
// ) -> Result<Vec<AccessDescription>, ResourceCertError> {
// let ParsedExtension::AuthorityInfoAccess(aia) = ext.parsed_extension() else {
// return Err(ResourceCertError::ParseCert(
// "authorityInfoAccess parse failed".into(),
// ));
// };
//
// let mut access_descriptions = Vec::new();
//
// for access in &aia.accessdescs {
// let access_method_oid = access.access_method.to_id_string();
//
// let uri = match &access.access_location {
// GeneralName::URI(uri) => uri,
// _ => {
// return Err(ResourceCertError::InvalidAccessLocationType);
// }
// };
//
// let url = Url::parse(uri)
// .map_err(|_| ResourceCertError::InvalidUri(uri.to_string()))?;
//
// access_descriptions.push(AccessDescription {
// access_method_oid,
// access_location: url,
// });
// }
//
// if access_descriptions.is_empty() {
// return Err(ResourceCertError::EmptyAuthorityInfoAccess);
// }
//
// Ok(access_descriptions)
// }
//
// fn parse_subject_info_access(ext: &X509Extension<'_>) -> Result<Vec<AccessDescription>, ResourceCertError> {
// let ParsedExtension::SubjectInfoAccess(sia) = ext.parsed_extension() else {
// return Err(ResourceCertError::ParseCert(
// "subjectInfoAccess parse failed".into(),
// ));
// };
// let mut access_descriptions = Vec::new();
//
// for access in &sia.accessdescs {
// let access_method_oid = access.access_method.to_id_string();
//
// // accessLocation: MUST be URI in RPKI
// let uri = match &access.access_location {
// GeneralName::URI(uri) => uri,
// _ => {
// return Err(ResourceCertError::InvalidAccessLocationType);
// }
// };
//
// let url = Url::parse(uri)
// .map_err(|_| ResourceCertError::InvalidUri(uri.to_string()))?;
//
// access_descriptions.push(AccessDescription {
// access_method_oid,
// access_location: url,
// });
// }
//
// if access_descriptions.is_empty() {
// return Err(ResourceCertError::EmptyAuthorityInfoAccess);
// }
//
// Ok(access_descriptions)
// }
//
// fn parse_certificate_policies(ext: &X509Extension<'_>) -> Result<Vec<PolicyInformation>, ResourceCertError> {
// let ParsedExtension::CertificatePolicies(cp) = ext.parsed_extension() else {
// return Err(ResourceCertError::ParseCert(
// "certificatePolicies parse failed".into(),
// ));
// };
// let mut policies = Vec::new();
//
// }

View File

@ -1,90 +0,0 @@
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ASIdentifiers {
pub asn: Vec<ASIdentifierChoice>
}
// ASN
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ASIdentifierChoice {
Inherit,
ASIDsOrRanges(Vec<ASIDOrRange>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ASIDOrRange {
Id(Asn),
AsRange(ASRange),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ASRange {
pub min: Asn,
pub max: Asn,
}
impl ASRange {
/// Creates a new AS number range from the smallest and largest number.
pub fn new(min: Asn, max: Asn) -> Self {
ASRange { min, max }
}
/// Returns an AS block covering all ASNs.
pub fn all() -> ASRange {
ASRange::new(Asn::MIN, Asn::MAX)
}
/// Returns the smallest AS number that is part of this range.
pub fn min(self) -> Asn {
self.min
}
/// Returns the largest AS number that is still part of this range.
pub fn max(self) -> Asn {
self.max
}
/// Returns the number of ASNs covered by this value.
pub fn asn_count(self) -> u32 {
u32::from(self.max) - u32::from(self.min) + 1
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Asn(u32);
impl Asn {
pub const MIN: Asn = Asn(u32::MIN);
pub const MAX: Asn = Asn(u32::MAX);
/// Creates an AS number from a `u32`.
pub fn from_u32(value: u32) -> Self {
Asn(value)
}
/// Converts an AS number into a `u32`.
pub fn into_u32(self) -> u32 {
self.0
}
/// Converts an AS number into a network-order byte array.
pub fn to_raw(self) -> [u8; 4] {
self.0.to_be_bytes()
}
}
impl From<u32> for Asn {
fn from(id: u32) -> Self {
Asn(id)
}
}
impl From<Asn> for u32 {
fn from(id: Asn) -> Self {
id.0
}
}

View File

@ -1,46 +0,0 @@
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IPAddrBlocks {
ips: Vec<IPAddressFamily>
}
// IP Address Family
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IPAddressFamily {
pub address_family: Afi,
pub ip_address_choice: IPAddressChoice,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Afi {
Ipv4,
Ipv6,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IPAddressChoice {
Inherit,
AddressOrRange(Vec<IPAddressOrRange>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IPAddressOrRange {
AddressPrefix(IPAddressPrefix),
AddressRange(IPAddressRange),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IPAddressPrefix {
pub address: IPAddress,
pub prefix_length: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IPAddressRange {
pub min: IPAddress,
pub max: IPAddress,
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct IPAddress(u128);

View File

@ -1,3 +0,0 @@
pub(crate) mod ip_resources;
pub(crate) mod as_resources;
pub mod resource;

View File

@ -1,10 +0,0 @@
use crate::data_model::resources::as_resources::ASIdentifiers;
use crate::data_model::resources::ip_resources::IPAddrBlocks;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResourceSet {
ip_addr_blocks: IPAddrBlocks,
as_identifiers: ASIdentifiers,
}

View File

@ -0,0 +1,771 @@
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,
};
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 {
pub raw_der: Vec<u8>,
pub subject_key_identifier: Vec<u8>,
pub spki_der: Vec<u8>,
pub sia_signed_object_uris: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RpkiSignedObject {
pub raw_der: Vec<u8>,
pub content_info_content_type: String,
pub signed_data: SignedDataProfiled,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignedDataProfiled {
pub version: u32,
pub digest_algorithms: Vec<String>,
pub encap_content_info: EncapsulatedContentInfo,
pub certificates: Vec<ResourceEeCertificate>,
pub crls_present: bool,
pub signer_infos: Vec<SignerInfoProfiled>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EncapsulatedContentInfo {
pub econtent_type: String,
pub econtent: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignerInfoProfiled {
pub version: u32,
pub sid_ski: Vec<u8>,
pub digest_algorithm: String,
pub signature_algorithm: String,
pub signed_attrs: SignedAttrsProfiled,
pub unsigned_attrs_present: bool,
pub signature: Vec<u8>,
pub signed_attrs_der_for_signature: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignedAttrsProfiled {
pub content_type: String,
pub message_digest: Vec<u8>,
pub signing_time: Asn1TimeUtc,
pub other_attrs_present: bool,
}
#[derive(Debug, thiserror::Error)]
pub enum SignedObjectDecodeError {
#[error("DER parse error: {0}")]
Parse(String),
#[error("trailing bytes after DER object: {0} bytes")]
TrailingBytes(usize),
#[error("ContentInfo.contentType must be SignedData ({OID_SIGNED_DATA}), got {0}")]
InvalidContentInfoContentType(String),
#[error("SignedData.version must be 3, got {0}")]
InvalidSignedDataVersion(u64),
#[error("SignedData.digestAlgorithms must contain exactly one AlgorithmIdentifier, got {0}")]
InvalidDigestAlgorithmsCount(usize),
#[error("digest algorithm must be id-sha256 ({OID_SHA256}), got {0}")]
InvalidDigestAlgorithm(String),
#[error("SignedData.certificates MUST be present")]
CertificatesMissing,
#[error("SignedData.certificates must contain exactly one EE certificate, got {0}")]
InvalidCertificatesCount(usize),
#[error("SignedData.crls MUST be omitted")]
CrlsPresent,
#[error("SignedData.signerInfos must contain exactly one SignerInfo, got {0}")]
InvalidSignerInfosCount(usize),
#[error("SignerInfo.version must be 3, got {0}")]
InvalidSignerInfoVersion(u64),
#[error("SignerInfo.sid must be subjectKeyIdentifier [0]")]
InvalidSignerIdentifier,
#[error("SignerInfo.digestAlgorithm must be id-sha256 ({OID_SHA256}), got {0}")]
InvalidSignerInfoDigestAlgorithm(String),
#[error("SignerInfo.signedAttrs MUST be present")]
SignedAttrsMissing,
#[error("SignerInfo.unsignedAttrs MUST be omitted")]
UnsignedAttrsPresent,
#[error(
"SignerInfo.signatureAlgorithm must be rsaEncryption ({OID_RSA_ENCRYPTION}) or \
sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0}"
)]
InvalidSignatureAlgorithm(String),
#[error("SignerInfo.signatureAlgorithm parameters must be absent or NULL")]
InvalidSignatureAlgorithmParameters,
#[error("signedAttrs contains unsupported attribute OID {0}")]
UnsupportedSignedAttribute(String),
#[error("signedAttrs contains duplicate attribute OID {0}")]
DuplicateSignedAttribute(String),
#[error("signedAttrs attribute {oid} attrValues must contain exactly one value, got {count}")]
InvalidSignedAttributeValuesCount { oid: String, count: usize },
#[error("signedAttrs.content-type attrValues must equal eContentType ({econtent_type}), got {attr_content_type}")]
ContentTypeAttrMismatch {
econtent_type: String,
attr_content_type: String,
},
#[error("EncapsulatedContentInfo.eContent MUST be present")]
EContentMissing,
#[error("signedAttrs.message-digest does not match SHA-256(eContent)")]
MessageDigestMismatch,
#[error("EE certificate parse error: {0}")]
EeCertificateParse(String),
#[error("EE certificate missing SubjectKeyIdentifier extension")]
EeCertificateMissingSki,
#[error("EE certificate missing SubjectInfoAccess extension ({OID_SUBJECT_INFO_ACCESS})")]
EeCertificateMissingSia,
#[error("EE certificate SIA missing id-ad-signedObject access method ({OID_AD_SIGNED_OBJECT})")]
EeCertificateMissingSignedObjectSia,
#[error("EE certificate SIA id-ad-signedObject accessLocation must be a URI")]
EeCertificateSignedObjectSiaNotUri,
#[error("EE certificate SIA id-ad-signedObject must include at least one rsync:// URI")]
EeCertificateSignedObjectSiaNoRsync,
#[error("SignerInfo.sid SKI does not match EE certificate SKI")]
SidSkiMismatch,
#[error("invalid signing-time attribute value (expected UTCTime or GeneralizedTime)")]
InvalidSigningTimeValue,
}
#[derive(Debug, thiserror::Error)]
pub enum SignedObjectVerifyError {
#[error("EE SubjectPublicKeyInfo parse error: {0}")]
EeSpkiParse(String),
#[error("trailing bytes after EE SubjectPublicKeyInfo DER: {0} bytes")]
EeSpkiTrailingBytes(usize),
#[error("unsupported EE public key algorithm (only RSA supported in M3)")]
UnsupportedEePublicKeyAlgorithm,
#[error("EE RSA public exponent invalid")]
InvalidEeRsaExponent,
#[error("signature verification failed")]
InvalidSignature,
}
impl RpkiSignedObject {
/// Decode a DER-encoded RPKI Signed Object (CMS ContentInfo wrapping SignedData) and enforce
/// the profile constraints from RFC 6488 §2-§3 and RFC 9589 §4.
pub fn decode_der(der: &[u8]) -> Result<Self, SignedObjectDecodeError> {
let (rem, obj) = parse_der(der).map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
if !rem.is_empty() {
return Err(SignedObjectDecodeError::TrailingBytes(rem.len()));
}
let content_info_seq = obj
.as_sequence()
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
if content_info_seq.len() != 2 {
return Err(SignedObjectDecodeError::Parse(
"ContentInfo must be a SEQUENCE of 2 elements".into(),
));
}
let content_type = oid_to_string(&content_info_seq[0])?;
if content_type != OID_SIGNED_DATA {
return Err(SignedObjectDecodeError::InvalidContentInfoContentType(
content_type,
));
}
let signed_data = parse_signed_data_from_contentinfo(&content_info_seq[1])?;
Ok(RpkiSignedObject {
raw_der: der.to_vec(),
content_info_content_type: OID_SIGNED_DATA.to_string(),
signed_data,
})
}
/// Verify the CMS signature using the embedded EE certificate public key.
pub fn verify_signature(&self) -> Result<(), SignedObjectVerifyError> {
let ee = &self.signed_data.certificates[0];
self.verify_signature_with_ee_spki_der(&ee.spki_der)
}
/// Verify the CMS signature using a DER-encoded SubjectPublicKeyInfo.
pub fn verify_signature_with_ee_spki_der(
&self,
ee_spki_der: &[u8],
) -> Result<(), SignedObjectVerifyError> {
let (rem, spki) = SubjectPublicKeyInfo::from_der(ee_spki_der)
.map_err(|e| SignedObjectVerifyError::EeSpkiParse(e.to_string()))?;
if !rem.is_empty() {
return Err(SignedObjectVerifyError::EeSpkiTrailingBytes(rem.len()));
}
self.verify_signature_with_ee_spki(&spki)
}
/// Verify the CMS signature using a parsed SubjectPublicKeyInfo.
pub fn verify_signature_with_ee_spki(
&self,
ee_spki: &SubjectPublicKeyInfo<'_>,
) -> Result<(), SignedObjectVerifyError> {
let pk = ee_spki
.parsed()
.map_err(|_e| SignedObjectVerifyError::UnsupportedEePublicKeyAlgorithm)?;
let (n, e) = match pk {
PublicKey::RSA(rsa) => {
let n = strip_leading_zeros(rsa.modulus).to_vec();
let e = strip_leading_zeros(rsa.exponent).to_vec();
let _exp = rsa
.try_exponent()
.map_err(|_e| SignedObjectVerifyError::InvalidEeRsaExponent)?;
(n, e)
}
_ => return Err(SignedObjectVerifyError::UnsupportedEePublicKeyAlgorithm),
};
let signer = &self.signed_data.signer_infos[0];
// The message to be verified is the DER encoding of SignedAttributes (SET OF Attribute).
let msg = &signer.signed_attrs_der_for_signature;
let pk = ring::signature::RsaPublicKeyComponents { n, e };
pk.verify(
&ring::signature::RSA_PKCS1_2048_8192_SHA256,
msg,
&signer.signature,
)
.map_err(|_e| SignedObjectVerifyError::InvalidSignature)
}
}
fn parse_signed_data_from_contentinfo(
obj: &DerObject<'_>,
) -> Result<SignedDataProfiled, SignedObjectDecodeError> {
// ContentInfo.content is `[0] EXPLICIT`, but `der-parser` will represent unknown tagged
// objects as `Unknown(Any)`. For EXPLICIT tags, the content octets are the full encoding of
// the inner object, so we parse it from the object's slice.
if obj.class() != Class::ContextSpecific || obj.tag() != Tag(0) {
return Err(SignedObjectDecodeError::Parse(
"ContentInfo.content must be [0] EXPLICIT".into(),
));
}
let inner_der = obj
.as_slice()
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
let (rem, inner_obj) =
parse_der(inner_der).map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
if !rem.is_empty() {
return Err(SignedObjectDecodeError::Parse(
"trailing bytes inside ContentInfo.content".into(),
));
}
parse_signed_data(&inner_obj)
}
fn parse_signed_data(obj: &DerObject<'_>) -> Result<SignedDataProfiled, SignedObjectDecodeError> {
let seq = obj
.as_sequence()
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
if seq.len() < 4 || seq.len() > 6 {
return Err(SignedObjectDecodeError::Parse(
"SignedData must be a SEQUENCE of 4..6 elements".into(),
));
}
let version = seq[0]
.as_u64()
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
if version != 3 {
return Err(SignedObjectDecodeError::InvalidSignedDataVersion(version));
}
let digest_set = seq[1]
.as_set()
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
if digest_set.len() != 1 {
return Err(SignedObjectDecodeError::InvalidDigestAlgorithmsCount(
digest_set.len(),
));
}
let (digest_oid, _params_ok) = parse_algorithm_identifier(&digest_set[0])?;
if digest_oid != OID_SHA256 {
return Err(SignedObjectDecodeError::InvalidDigestAlgorithm(digest_oid));
}
let digest_algorithms = vec![OID_SHA256.to_string()];
let encap_content_info = parse_encapsulated_content_info(&seq[2])?;
let mut certificates: Option<Vec<ResourceEeCertificate>> = None;
let mut crls_present = false;
let mut signer_infos_obj: Option<&DerObject<'_>> = None;
for item in &seq[3..] {
if item.class() == Class::ContextSpecific && item.tag() == Tag(0) {
if certificates.is_some() {
return Err(SignedObjectDecodeError::Parse(
"SignedData.certificates appears more than once".into(),
));
}
certificates = Some(parse_certificate_set_implicit(item)?);
} else if item.class() == Class::ContextSpecific && item.tag() == Tag(1) {
crls_present = true;
} else if item.class() == Class::Universal && item.tag() == Tag::Set {
signer_infos_obj = Some(item);
} else {
return Err(SignedObjectDecodeError::Parse(
"unexpected field in SignedData".into(),
));
}
}
if crls_present {
return Err(SignedObjectDecodeError::CrlsPresent);
}
let certificates = certificates.ok_or(SignedObjectDecodeError::CertificatesMissing)?;
if certificates.len() != 1 {
return Err(SignedObjectDecodeError::InvalidCertificatesCount(
certificates.len(),
));
}
let signer_infos_obj = signer_infos_obj.ok_or_else(|| {
SignedObjectDecodeError::Parse("SignedData.signerInfos missing".into())
})?;
let signer_infos_set = signer_infos_obj
.as_set()
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
if signer_infos_set.len() != 1 {
return Err(SignedObjectDecodeError::InvalidSignerInfosCount(
signer_infos_set.len(),
));
}
let signer_info = parse_signer_info(&signer_infos_set[0])?;
if signer_info.sid_ski != certificates[0].subject_key_identifier {
return Err(SignedObjectDecodeError::SidSkiMismatch);
}
if signer_info.signed_attrs.content_type != encap_content_info.econtent_type {
return Err(SignedObjectDecodeError::ContentTypeAttrMismatch {
econtent_type: encap_content_info.econtent_type.clone(),
attr_content_type: signer_info.signed_attrs.content_type.clone(),
});
}
let computed = Sha256::digest(&encap_content_info.econtent).to_vec();
if computed != signer_info.signed_attrs.message_digest {
return Err(SignedObjectDecodeError::MessageDigestMismatch);
}
Ok(SignedDataProfiled {
version: 3,
digest_algorithms,
encap_content_info,
certificates,
crls_present,
signer_infos: vec![signer_info],
})
}
fn parse_encapsulated_content_info(obj: &DerObject<'_>) -> Result<EncapsulatedContentInfo, SignedObjectDecodeError> {
let seq = obj
.as_sequence()
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
if seq.len() == 1 {
return Err(SignedObjectDecodeError::EContentMissing);
}
if seq.len() != 2 {
return Err(SignedObjectDecodeError::Parse(
"EncapsulatedContentInfo must be SEQUENCE of 1..2".into(),
));
}
let econtent_type = oid_to_string(&seq[0])?;
let econtent_tagged = &seq[1];
if econtent_tagged.class() != Class::ContextSpecific || econtent_tagged.tag() != Tag(0) {
return Err(SignedObjectDecodeError::Parse(
"EncapsulatedContentInfo.eContent must be [0] EXPLICIT".into(),
));
}
let inner_der = econtent_tagged
.as_slice()
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?
;
let (rem, inner_obj) =
parse_der(inner_der).map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
if !rem.is_empty() {
return Err(SignedObjectDecodeError::Parse(
"trailing bytes inside EncapsulatedContentInfo.eContent".into(),
));
}
let econtent = inner_obj
.as_slice()
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?
.to_vec();
if econtent.is_empty() {
return Err(SignedObjectDecodeError::EContentMissing);
}
Ok(EncapsulatedContentInfo {
econtent_type,
econtent,
})
}
fn parse_certificate_set_implicit(obj: &DerObject<'_>) -> Result<Vec<ResourceEeCertificate>, SignedObjectDecodeError> {
let content = obj
.as_slice()
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
let mut input = content;
let mut certs = Vec::new();
while !input.is_empty() {
let (rem, _any_obj) =
parse_der(input).map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
let consumed = input.len() - rem.len();
let der = &input[..consumed];
certs.push(parse_ee_certificate(der)?);
input = rem;
}
Ok(certs)
}
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 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,
})
.ok_or(SignedObjectDecodeError::EeCertificateMissingSki)?;
let spki_der = cert.public_key().raw.to_vec();
let sia_signed_object_uris = parse_ee_sia_signed_object_uris(&cert)?;
Ok(ResourceEeCertificate {
raw_der: der.to_vec(),
subject_key_identifier: ski,
spki_der,
sia_signed_object_uris,
})
}
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()
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
if seq.len() < 5 || seq.len() > 7 {
return Err(SignedObjectDecodeError::Parse(
"SignerInfo must be a SEQUENCE of 5..7 elements".into(),
));
}
let version = seq[0]
.as_u64()
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
if version != 3 {
return Err(SignedObjectDecodeError::InvalidSignerInfoVersion(version));
}
let sid = &seq[1];
if sid.class() != Class::ContextSpecific || sid.tag() != Tag(0) {
return Err(SignedObjectDecodeError::InvalidSignerIdentifier);
}
let sid_ski = sid
.as_slice()
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?
.to_vec();
let (digest_algorithm, _params_ok) = parse_algorithm_identifier(&seq[2])?;
if digest_algorithm != OID_SHA256 {
return Err(SignedObjectDecodeError::InvalidSignerInfoDigestAlgorithm(
digest_algorithm,
));
}
let mut idx = 3;
let mut signed_attrs: Option<SignedAttrsProfiled> = None;
let mut signed_attrs_der_for_signature: Option<Vec<u8>> = None;
if seq[idx].class() == Class::ContextSpecific && seq[idx].tag() == Tag(0) {
let signed_attrs_obj = &seq[idx];
let signed_attrs_content = signed_attrs_obj
.as_slice()
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
signed_attrs = Some(parse_signed_attrs_implicit(signed_attrs_content)?);
signed_attrs_der_for_signature = Some(make_signed_attrs_der_for_signature(signed_attrs_obj)?);
idx += 1;
}
let signed_attrs = signed_attrs.ok_or(SignedObjectDecodeError::SignedAttrsMissing)?;
let signed_attrs_der_for_signature =
signed_attrs_der_for_signature.ok_or(SignedObjectDecodeError::SignedAttrsMissing)?;
let (signature_algorithm, params_ok) = parse_algorithm_identifier(&seq[idx])?;
if !params_ok {
return Err(SignedObjectDecodeError::InvalidSignatureAlgorithmParameters);
}
if signature_algorithm != OID_RSA_ENCRYPTION
&& signature_algorithm != OID_SHA256_WITH_RSA_ENCRYPTION
{
return Err(SignedObjectDecodeError::InvalidSignatureAlgorithm(
signature_algorithm,
));
}
idx += 1;
let signature = seq[idx]
.as_slice()
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?
.to_vec();
idx += 1;
let unsigned_attrs_present = seq.get(idx).is_some();
if unsigned_attrs_present {
return Err(SignedObjectDecodeError::UnsignedAttrsPresent);
}
Ok(SignerInfoProfiled {
version: 3,
sid_ski,
digest_algorithm: OID_SHA256.to_string(),
signature_algorithm,
signed_attrs,
unsigned_attrs_present,
signature,
signed_attrs_der_for_signature,
})
}
fn parse_signed_attrs_implicit(input: &[u8]) -> Result<SignedAttrsProfiled, SignedObjectDecodeError> {
let mut content_type: Option<String> = None;
let mut message_digest: Option<Vec<u8>> = None;
let mut signing_time: Option<Asn1TimeUtc> = None;
let mut remaining = input;
while !remaining.is_empty() {
let (rem, attr_obj) =
parse_der(remaining).map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
remaining = rem;
let attr_seq = attr_obj
.as_sequence()
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
if attr_seq.len() != 2 {
return Err(SignedObjectDecodeError::Parse(
"Attribute must be SEQUENCE of 2".into(),
));
}
let oid = oid_to_string(&attr_seq[0])?;
let values_set = attr_seq[1]
.as_set()
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
if values_set.len() != 1 {
return Err(SignedObjectDecodeError::InvalidSignedAttributeValuesCount {
oid,
count: values_set.len(),
});
}
match oid.as_str() {
OID_CMS_ATTR_CONTENT_TYPE => {
if content_type.is_some() {
return Err(SignedObjectDecodeError::DuplicateSignedAttribute(oid));
}
let v = oid_to_string(&values_set[0])?;
content_type = Some(v);
}
OID_CMS_ATTR_MESSAGE_DIGEST => {
if message_digest.is_some() {
return Err(SignedObjectDecodeError::DuplicateSignedAttribute(oid));
}
let v = values_set[0]
.as_slice()
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?
.to_vec();
message_digest = Some(v);
}
OID_CMS_ATTR_SIGNING_TIME => {
if signing_time.is_some() {
return Err(SignedObjectDecodeError::DuplicateSignedAttribute(oid));
}
signing_time = Some(parse_signing_time_value(&values_set[0])?);
}
_ => {
return Err(SignedObjectDecodeError::UnsupportedSignedAttribute(oid));
}
}
}
Ok(SignedAttrsProfiled {
content_type: content_type.ok_or_else(|| {
SignedObjectDecodeError::Parse("missing signedAttrs content-type".into())
})?,
message_digest: message_digest.ok_or_else(|| {
SignedObjectDecodeError::Parse("missing signedAttrs message-digest".into())
})?,
signing_time: signing_time.ok_or_else(|| {
SignedObjectDecodeError::Parse("missing signedAttrs signing-time".into())
})?,
other_attrs_present: false,
})
}
fn parse_signing_time_value(obj: &DerObject<'_>) -> Result<Asn1TimeUtc, SignedObjectDecodeError> {
match &obj.content {
der_parser::ber::BerObjectContent::UTCTime(dt) => Ok(Asn1TimeUtc {
utc: dt.to_datetime().map_err(|_| SignedObjectDecodeError::InvalidSigningTimeValue)?,
encoding: Asn1TimeEncoding::UtcTime,
}),
der_parser::ber::BerObjectContent::GeneralizedTime(dt) => Ok(Asn1TimeUtc {
utc: dt.to_datetime().map_err(|_| SignedObjectDecodeError::InvalidSigningTimeValue)?,
encoding: Asn1TimeEncoding::GeneralizedTime,
}),
_ => Err(SignedObjectDecodeError::InvalidSigningTimeValue),
}
}
fn make_signed_attrs_der_for_signature(obj: &DerObject<'_>) -> Result<Vec<u8>, SignedObjectDecodeError> {
// We need the DER encoding of SignedAttributes (SET OF Attribute) as signature input.
// The SignedAttributes field in SignerInfo is `[0] IMPLICIT`, so the on-wire bytes start with
// a context-specific constructed tag (0xA0 for tag 0). For signature verification, this tag
// is replaced with the universal SET tag (0x31), leaving length+content unchanged.
//
let mut cs_der = obj
.to_vec()
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
if cs_der.is_empty() {
return Err(SignedObjectDecodeError::Parse(
"signedAttrs encoding is empty".into(),
));
}
// The first byte should be the context-specific tag (0xA0) for [0] constructed.
// Replace it with universal SET (0x31) for signature input.
cs_der[0] = 0x31;
Ok(cs_der)
}
fn oid_to_string(obj: &DerObject<'_>) -> Result<String, SignedObjectDecodeError> {
let oid = obj
.as_oid()
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
Ok(oid.to_id_string())
}
fn parse_algorithm_identifier(
obj: &DerObject<'_>,
) -> Result<(String, bool), SignedObjectDecodeError> {
let seq = obj
.as_sequence()
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
if seq.is_empty() || seq.len() > 2 {
return Err(SignedObjectDecodeError::Parse(
"AlgorithmIdentifier must be SEQUENCE of 1..2".into(),
));
}
let oid = oid_to_string(&seq[0])?;
let params_ok = match seq.get(1) {
None => true,
Some(p) => matches!(p.content, der_parser::ber::BerObjectContent::Null),
};
Ok((oid, params_ok))
}
fn strip_leading_zeros(bytes: &[u8]) -> &[u8] {
let mut idx = 0;
while idx < bytes.len() && bytes[idx] == 0 {
idx += 1;
}
if idx == bytes.len() {
&bytes[bytes.len() - 1..]
} else {
&bytes[idx..]
}
}

View File

@ -1,27 +0,0 @@
use url::Url;
use x509_parser::prelude::*;
use crate::data_model::resources::resource::ResourceSet;
//
// #[derive(Debug, Clone)]
// pub struct TrustAnchorCert {
// /// 信任锚证书名称
// pub name: String,
//
// /// 证书原始DER内容
// pub cert_der: Vec<u8>,
//
// /// 证书
// pub cert: X509Certificate<'static>,
//
// /// 资源集合
// pub resources: ResourceSet,
//
// ///发布点
// pub publication_point: Url,
// }
//
// impl TrustAnchorCert {
//
// }

View File

@ -1,18 +0,0 @@
/// TAL Model
#[derive(Clone, Debug)]
pub struct Tal {
/// Optional human-readable comments
pub comments: Vec<String>,
/// Ordered list of URIs pointing to the TA certificate
pub uris: Vec<TalUri>,
/// SubjectPublicKeyInfo DER
pub spki_der: Vec<u8>,
}
#[derive(Debug, Clone)]
pub enum TalUri {
Rsync(String),
Https(String),
}

93
tests/test_common.rs Normal file
View File

@ -0,0 +1,93 @@
use rpki::data_model::common::{
algorithm_params_absent_or_null, Asn1TimeEncoding, Asn1TimeUtc, BigUnsigned,
};
use x509_parser::prelude::FromDer;
use x509_parser::x509::AlgorithmIdentifier;
use x509_parser::time::ASN1Time;
#[test]
fn big_unsigned_helpers() {
let n = BigUnsigned { bytes_be: vec![0] };
assert_eq!(n.to_u64(), Some(0));
assert_eq!(n.to_hex_upper(), "00");
let n = BigUnsigned {
bytes_be: vec![0x01, 0x02, 0x03],
};
assert_eq!(n.to_u64(), Some(0x010203));
assert_eq!(n.to_hex_upper(), "010203");
let n = BigUnsigned {
bytes_be: vec![0; 9],
};
assert_eq!(n.to_u64(), None);
}
#[test]
fn time_encoding_validation() {
let t = Asn1TimeUtc {
utc: time::OffsetDateTime::from_unix_timestamp(0).unwrap(),
encoding: Asn1TimeEncoding::UtcTime,
};
t.validate_encoding_rfc5280("t").expect("utc ok");
let t = Asn1TimeUtc {
utc: time::OffsetDateTime::parse("2050-01-01T00:00:00Z", &time::format_description::well_known::Rfc3339)
.unwrap(),
encoding: Asn1TimeEncoding::UtcTime,
};
assert!(t.validate_encoding_rfc5280("t").is_err());
}
#[test]
fn algorithm_params_absent_or_null_helper() {
// AlgorithmIdentifier ::= SEQUENCE { algorithm OID, parameters ANY OPTIONAL }
// Using sha256WithRSAEncryption with NULL parameters.
let alg_null = hex::decode(
"300D06092A864886F70D01010B0500",
)
.unwrap();
let (_rem, id) = AlgorithmIdentifier::from_der(&alg_null).expect("parse AlgorithmIdentifier");
assert!(algorithm_params_absent_or_null(&id));
// Same OID, but parameters = INTEGER 1 (invalid for our helper).
let alg_int = hex::decode(
"300E06092A864886F70D01010B020101",
)
.unwrap();
let (_rem, id) = AlgorithmIdentifier::from_der(&alg_int).expect("parse AlgorithmIdentifier");
assert!(!algorithm_params_absent_or_null(&id));
// parameters absent
let oid = der_parser::Oid::from(&[1u64, 2, 3]).unwrap();
let id = AlgorithmIdentifier::new(oid, None);
assert!(algorithm_params_absent_or_null(&id));
}
#[test]
fn asn1_time_to_model_helper_covers_branches() {
let dt_utc = time::OffsetDateTime::parse(
"2024-01-01T00:00:00Z",
&time::format_description::well_known::Rfc3339,
)
.unwrap();
let t = ASN1Time::new_utc(dt_utc);
let m = rpki::data_model::common::asn1_time_to_model(t);
assert_eq!(m.encoding, Asn1TimeEncoding::UtcTime);
let dt_gen = time::OffsetDateTime::parse(
"2050-01-01T00:00:00Z",
&time::format_description::well_known::Rfc3339,
)
.unwrap();
let t = ASN1Time::new_generalized(dt_gen);
let m = rpki::data_model::common::asn1_time_to_model(t);
assert_eq!(m.encoding, Asn1TimeEncoding::GeneralizedTime);
}
#[test]
fn big_unsigned_from_biguint_zero_encodes_as_single_zero_byte() {
let z = der_parser::num_bigint::BigUint::from(0u32);
let n = BigUnsigned::from_biguint(&z);
assert_eq!(n.bytes_be, vec![0]);
}

127
tests/test_crl_errors.rs Normal file
View File

@ -0,0 +1,127 @@
use rpki::data_model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl};
use x509_parser::prelude::FromDer;
use x509_parser::prelude::X509Certificate;
const TEST_NO_CRLSIGN_CERT_DER_B64: &str = "MIIDATCCAemgAwIBAgIUCyQLQJn92+gyAzvIz22q1F/97OMwDQYJKoZIhvcNAQELBQAwGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMB4XDTI2MDEyNzAzNTk1OVoXDTM2MDEyNTAzNTk1OVowGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/aoMU8J6cddkM2r6F2snd1rCdQPepgo2T2lrqWFcxnQJdcxBL1OYg3wFi95TJmZSeIHIOGauDaJ2abmjgyOUHOC4U68x66JRg4hLkmLxo1cf3uYHWl9Obph6g2qPRvN80ORq70JPuL6mAfUkNiO9hnwK6oQiTzc/rjCQGIFH8kTESBMXLfNCyUpGi+MNztYH6Ha6bKAQuXgd29OFwIkOlGQnYgGC2qBMvnp86eITvV1gTiuI8Ho9m9nZHCmaD7TylvkMDq8Hk5nkIpRcG0uO60SkR2BiMOYe/TNn5dTmHd6bsdbU2GOvgnq1SnqGq3FOWhKIe3ycUJde0uNfZOqRwIDAQABoz8wPTAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUFjyzfJCDNhFfKxVr06kjUkE23dMwDQYJKoZIhvcNAQELBQADggEBAK98n2gVlwKA3Ob1YeAm9f+8hm7pbvrt0tA8GW180CILjf09k7fKgiRlxqGdZ9ySXjU52+zCqu3MpBXVbI87ZC+zA6uK05n4y1F0n85MJ9hGR2UEiPcqou85X73LvioynnSOy/OV1PjKJXReUsqF3GgDtgcMyFssPJ9s/5DWuUCScUJY6pu0kuIGOLQ/oXUw4TvxUeyz73gOTiAJshVTQoLpHUhj0595S7lArjwi7oLI1b8m8guTknvhk0Sc3tJZmUqOcIvYIs0guHpaeC+sMoF4K+6UTrxxOBdX+fUEWNpUyYXWHjdZq25PbJdHwA/VAW2zYVojaVREligf0Qfo6F4=";
fn test_no_crlsign_cert_der() -> Vec<u8> {
use base64::{engine::general_purpose, Engine as _};
general_purpose::STANDARD
.decode(TEST_NO_CRLSIGN_CERT_DER_B64)
.expect("decode base64 cert")
}
#[test]
fn verify_errors_are_reported() {
let crl_der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
)
.expect("read CRL fixture");
let issuer_cert_der = std::fs::read(
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
)
.expect("read issuer cert");
let mut crl = RpkixCrl::decode_der(&crl_der).expect("decode");
// IssuerSubjectMismatch.
crl.issuer_dn = "CN=Not The Issuer".to_string();
let err = crl
.verify_signature_with_issuer_certificate_der(&issuer_cert_der)
.unwrap_err();
assert!(matches!(err, CrlVerifyError::IssuerSubjectMismatch { .. }));
// AkiSkiMismatch (force issuer dn to match, then change AKI).
let (_rem, issuer_cert) = X509Certificate::from_der(&issuer_cert_der).unwrap();
crl.issuer_dn = issuer_cert.subject().to_string();
crl.extensions.authority_key_identifier = vec![0u8; 20];
let err = crl
.verify_signature_with_issuer_certificate_der(&issuer_cert_der)
.unwrap_err();
assert!(matches!(err, CrlVerifyError::AkiSkiMismatch));
// IssuerKeyUsageMissingCrlSign (use a cert with KeyUsage present but without CRLSign).
let no_crlsign_der = test_no_crlsign_cert_der();
let (_rem, no_crlsign_cert) = X509Certificate::from_der(&no_crlsign_der).unwrap();
crl.issuer_dn = no_crlsign_cert.subject().to_string();
let err = crl
.verify_signature_with_issuer_certificate_der(&no_crlsign_der)
.unwrap_err();
assert!(matches!(err, CrlVerifyError::IssuerKeyUsageMissingCrlSign));
// InvalidSignature: verify cryptographically with the wrong SPKI.
let no_crlsign_spki_der = no_crlsign_cert.public_key().raw.to_vec();
let err = crl
.verify_signature_with_issuer_spki_der(&no_crlsign_spki_der)
.unwrap_err();
assert!(matches!(err, CrlVerifyError::InvalidSignature(_)));
// IssuerCertificateParse / trailing bytes.
let err = crl
.verify_signature_with_issuer_certificate_der(b"not a cert")
.unwrap_err();
assert!(matches!(err, CrlVerifyError::IssuerCertificateParse(_)));
let mut bad = issuer_cert_der.clone();
bad.push(0);
let err = crl
.verify_signature_with_issuer_certificate_der(&bad)
.unwrap_err();
assert!(matches!(err, CrlVerifyError::IssuerCertificateTrailingBytes(1)));
}
#[test]
fn verify_signature_with_spki_der_paths() {
let crl_der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
)
.expect("read CRL fixture");
let issuer_cert_der = std::fs::read(
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
)
.expect("read issuer cert");
let crl = RpkixCrl::decode_der(&crl_der).expect("decode");
let (_rem, issuer_cert) = X509Certificate::from_der(&issuer_cert_der).unwrap();
let spki_der = issuer_cert.public_key().raw.to_vec();
crl.verify_signature_with_issuer_spki_der(&spki_der)
.expect("verify with spki der");
let mut bad = spki_der;
bad.push(0);
let err = crl.verify_signature_with_issuer_spki_der(&bad).unwrap_err();
assert!(matches!(err, CrlVerifyError::IssuerSpkiTrailingBytes(1)));
}
#[test]
fn decode_rejects_trailing_bytes() {
let crl_der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
)
.expect("read CRL fixture");
let mut bad = crl_der.clone();
bad.push(0);
let err = RpkixCrl::decode_der(&bad).unwrap_err();
assert!(matches!(err, CrlDecodeError::TrailingBytes(1)));
}
#[test]
fn verify_rejects_crl_with_trailing_bytes_in_raw_der() {
let crl_der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
)
.expect("read CRL fixture");
let issuer_cert_der = std::fs::read(
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
)
.expect("read issuer cert");
let mut crl = RpkixCrl::decode_der(&crl_der).expect("decode");
crl.raw_der.push(0);
let (_rem, issuer_cert) = X509Certificate::from_der(&issuer_cert_der).unwrap();
let spki_der = issuer_cert.public_key().raw.to_vec();
let err = crl.verify_signature_with_issuer_spki_der(&spki_der).unwrap_err();
assert!(matches!(err, CrlVerifyError::CrlTrailingBytes(1)));
}

View File

@ -0,0 +1,35 @@
use rpki::data_model::manifest::{ManifestEContent, ManifestObject};
#[test]
fn decode_manifest_fixture_smoke() {
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 object");
assert_eq!(mft.manifest.version, 0);
assert_eq!(mft.manifest.file_hash_alg, rpki::data_model::oid::OID_SHA256);
assert!(mft.manifest.next_update > mft.manifest.this_update);
assert!(!mft.manifest.files.is_empty());
// The manifest file MUST NOT be listed in its own fileList.
assert!(mft
.manifest
.files
.iter()
.all(|f| !f.file_name.to_ascii_lowercase().ends_with(".mft")));
}
#[test]
fn decode_manifest_econtent_from_fixture_signed_object() {
let so_der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
)
.expect("read MFT fixture");
let so = rpki::data_model::signed_object::RpkiSignedObject::decode_der(&so_der)
.expect("decode signed object");
let e = ManifestEContent::decode_der(&so.signed_data.encap_content_info.econtent)
.expect("decode manifest eContent");
assert_eq!(e.version, 0);
}

View File

@ -0,0 +1,411 @@
use rpki::data_model::common::BigUnsigned;
use rpki::data_model::manifest::{ManifestDecodeError, ManifestEContent, ManifestObject};
use rpki::data_model::signed_object::RpkiSignedObject;
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_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_oid(oid: &str) -> Vec<u8> {
use std::str::FromStr;
use der_parser::asn1_rs::ToDer;
let oid = der_parser::Oid::from_str(oid).unwrap();
oid.to_der_vec().unwrap()
}
fn der_generalized_time_z(s: &str) -> Vec<u8> {
tlv(0x18, s.as_bytes())
}
fn der_ia5(s: &str) -> Vec<u8> {
tlv(0x16, s.as_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 der_cs_explicit(tag_no: u8, inner_der: Vec<u8>) -> Vec<u8> {
tlv(0xA0 | (tag_no & 0x1F), &inner_der)
}
fn manifest_der(
version: Option<u64>,
manifest_number: Vec<u8>,
this_update: Vec<u8>,
next_update: Vec<u8>,
file_hash_alg_oid: &str,
file_list: Vec<Vec<u8>>,
) -> Vec<u8> {
let mut fields = Vec::new();
if let Some(v) = version {
fields.push(der_cs_explicit(0, der_integer_u64(v)));
}
fields.push(der_integer_bytes(&manifest_number));
fields.push(this_update);
fields.push(next_update);
fields.push(der_oid(file_hash_alg_oid));
fields.push(der_sequence(file_list));
der_sequence(fields)
}
fn file_and_hash(file: &str, unused: u8, hash: &[u8]) -> Vec<u8> {
der_sequence(vec![der_ia5(file), der_bit_string(unused, hash)])
}
#[test]
fn manifest_econtent_version_must_be_zero_when_present() {
let der = manifest_der(
Some(1),
vec![1],
der_generalized_time_z("20260101000000Z"),
der_generalized_time_z("20260102000000Z"),
rpki::data_model::oid::OID_SHA256,
vec![],
);
let err = ManifestEContent::decode_der(&der).unwrap_err();
assert!(matches!(err, ManifestDecodeError::InvalidManifestVersion(1)));
}
#[test]
fn manifest_number_too_long_rejected() {
let long = vec![1u8; 21];
let der = manifest_der(
Some(0),
long,
der_generalized_time_z("20260101000000Z"),
der_generalized_time_z("20260102000000Z"),
rpki::data_model::oid::OID_SHA256,
vec![],
);
let err = ManifestEContent::decode_der(&der).unwrap_err();
assert!(matches!(err, ManifestDecodeError::ManifestNumberTooLong));
}
#[test]
fn manifest_number_negative_rejected() {
// INTEGER -1 encoded as 0xFF
let der = manifest_der(
Some(0),
vec![0xFF],
der_generalized_time_z("20260101000000Z"),
der_generalized_time_z("20260102000000Z"),
rpki::data_model::oid::OID_SHA256,
vec![],
);
let err = ManifestEContent::decode_der(&der).unwrap_err();
assert!(matches!(err, ManifestDecodeError::InvalidManifestNumber));
}
#[test]
fn this_update_and_next_update_must_be_generalized_time() {
let der = manifest_der(
Some(0),
vec![1],
tlv(0x17, b"240101000000Z"), // UTCTime
der_generalized_time_z("20260102000000Z"),
rpki::data_model::oid::OID_SHA256,
vec![],
);
let err = ManifestEContent::decode_der(&der).unwrap_err();
assert!(matches!(err, ManifestDecodeError::InvalidThisUpdate));
let der = manifest_der(
Some(0),
vec![1],
der_generalized_time_z("20260101000000Z"),
tlv(0x17, b"240101000000Z"), // UTCTime
rpki::data_model::oid::OID_SHA256,
vec![],
);
let err = ManifestEContent::decode_der(&der).unwrap_err();
assert!(matches!(err, ManifestDecodeError::InvalidNextUpdate));
}
#[test]
fn next_update_must_be_later() {
let der = manifest_der(
Some(0),
vec![1],
der_generalized_time_z("20260102000000Z"),
der_generalized_time_z("20260101000000Z"),
rpki::data_model::oid::OID_SHA256,
vec![],
);
let err = ManifestEContent::decode_der(&der).unwrap_err();
assert!(matches!(err, ManifestDecodeError::NextUpdateNotLater));
}
#[test]
fn file_hash_alg_must_be_sha256() {
let der = manifest_der(
Some(0),
vec![1],
der_generalized_time_z("20260101000000Z"),
der_generalized_time_z("20260102000000Z"),
"1.2.3.4",
vec![],
);
let err = ManifestEContent::decode_der(&der).unwrap_err();
assert!(matches!(err, ManifestDecodeError::InvalidFileHashAlg(_)));
}
#[test]
fn file_list_entry_validation() {
let hash = vec![0u8; 32];
// Invalid filename (bad char)
let der = manifest_der(
Some(0),
vec![1],
der_generalized_time_z("20260101000000Z"),
der_generalized_time_z("20260102000000Z"),
rpki::data_model::oid::OID_SHA256,
vec![file_and_hash("bad!.roa", 0, &hash)],
);
let err = ManifestEContent::decode_der(&der).unwrap_err();
assert!(matches!(err, ManifestDecodeError::InvalidFileName(_)));
// Non-octet-aligned BIT STRING
let der = manifest_der(
Some(0),
vec![1],
der_generalized_time_z("20260101000000Z"),
der_generalized_time_z("20260102000000Z"),
rpki::data_model::oid::OID_SHA256,
vec![file_and_hash("ok.roa", 1, &hash)],
);
let err = ManifestEContent::decode_der(&der).unwrap_err();
assert!(matches!(err, ManifestDecodeError::HashNotOctetAligned));
// Wrong hash length
let der = manifest_der(
Some(0),
vec![1],
der_generalized_time_z("20260101000000Z"),
der_generalized_time_z("20260102000000Z"),
rpki::data_model::oid::OID_SHA256,
vec![file_and_hash("ok.roa", 0, &[0u8; 31])],
);
let err = ManifestEContent::decode_der(&der).unwrap_err();
assert!(matches!(err, ManifestDecodeError::InvalidHashLength(31)));
}
#[test]
fn manifest_object_requires_correct_econtent_type() {
let so_der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
)
.expect("read MFT fixture");
let mut so = RpkiSignedObject::decode_der(&so_der).expect("decode signed object");
so.signed_data.encap_content_info.econtent_type = "1.2.3.4".to_string();
let err = ManifestObject::from_signed_object(so).unwrap_err();
assert!(matches!(err, ManifestDecodeError::InvalidEContentType(_)));
}
#[test]
fn manifest_sequence_length_is_validated() {
let der = der_sequence(vec![der_integer_u64(0)]);
let err = ManifestEContent::decode_der(&der).unwrap_err();
assert!(matches!(
err,
ManifestDecodeError::InvalidManifestSequenceLen(_)
));
}
#[test]
fn file_list_must_be_sequence_and_entry_shape_validated() {
// Patch last byte to change SEQUENCE tag 0x30 to NULL tag 0x05 in the inner fileList.
// Build a manifest with fileList = NULL explicitly.
let der = {
let mut fields = Vec::new();
fields.push(der_cs_explicit(0, der_integer_u64(0)));
fields.push(der_integer_u64(1));
fields.push(der_generalized_time_z("20260101000000Z"));
fields.push(der_generalized_time_z("20260102000000Z"));
fields.push(der_oid(rpki::data_model::oid::OID_SHA256));
fields.push(tlv(0x05, &[]));
der_sequence(fields)
};
let err = ManifestEContent::decode_der(&der).unwrap_err();
assert!(matches!(err, ManifestDecodeError::InvalidFileList));
// FileAndHash not SEQUENCE of 2
let der = manifest_der(
Some(0),
vec![1],
der_generalized_time_z("20260101000000Z"),
der_generalized_time_z("20260102000000Z"),
rpki::data_model::oid::OID_SHA256,
vec![der_sequence(vec![der_ia5("ok.roa")])],
);
let err = ManifestEContent::decode_der(&der).unwrap_err();
assert!(matches!(err, ManifestDecodeError::InvalidFileAndHash));
}
#[test]
fn version_tag_must_be_context_specific_0() {
let der = {
let mut fields = Vec::new();
// Wrong tag number [1]
fields.push(der_cs_explicit(1, der_integer_u64(0)));
fields.push(der_integer_u64(1));
fields.push(der_generalized_time_z("20260101000000Z"));
fields.push(der_generalized_time_z("20260102000000Z"));
fields.push(der_oid(rpki::data_model::oid::OID_SHA256));
fields.push(der_sequence(vec![]));
der_sequence(fields)
};
let err = ManifestEContent::decode_der(&der).unwrap_err();
assert!(matches!(err, ManifestDecodeError::Parse(_)));
}
#[test]
fn parse_manifest_number_helper_exercises_zero_encoding() {
let z = der_parser::num_bigint::BigUint::from(0u32);
let n = BigUnsigned::from_biguint(&z);
assert_eq!(n.bytes_be, vec![0]);
}
#[test]
fn manifest_econtent_trailing_bytes_are_rejected() {
let der = manifest_der(
Some(0),
vec![1],
der_generalized_time_z("20260101000000Z"),
der_generalized_time_z("20260102000000Z"),
rpki::data_model::oid::OID_SHA256,
vec![],
);
let mut bad = der.clone();
bad.push(0);
let err = ManifestEContent::decode_der(&bad).unwrap_err();
assert!(matches!(err, ManifestDecodeError::TrailingBytes(1)));
}
#[test]
fn manifest_version_rejects_trailing_bytes_inside_explicit_tag() {
let mut version_inner = der_integer_u64(0);
version_inner.extend(tlv(0x05, &[]));
let der = {
let mut fields = Vec::new();
fields.push(der_cs_explicit(0, version_inner));
fields.push(der_integer_u64(1));
fields.push(der_generalized_time_z("20260101000000Z"));
fields.push(der_generalized_time_z("20260102000000Z"));
fields.push(der_oid(rpki::data_model::oid::OID_SHA256));
fields.push(der_sequence(vec![]));
der_sequence(fields)
};
let err = ManifestEContent::decode_der(&der).unwrap_err();
assert!(matches!(err, ManifestDecodeError::Parse(_)));
}
#[test]
fn manifest_rejects_hash_with_wrong_type() {
let hash = vec![0u8; 32];
let entry = der_sequence(vec![der_ia5("ok.roa"), tlv(0x04, &hash)]); // OCTET STRING, not BIT STRING
let der = manifest_der(
Some(0),
vec![1],
der_generalized_time_z("20260101000000Z"),
der_generalized_time_z("20260102000000Z"),
rpki::data_model::oid::OID_SHA256,
vec![entry],
);
let err = ManifestEContent::decode_der(&der).unwrap_err();
assert!(matches!(err, ManifestDecodeError::InvalidHashType));
}
#[test]
fn file_name_validation_branches_are_exercised() {
let hash = vec![0u8; 32];
let cases = [
"noext", // missing '.'
".roa", // empty base
"a.roaa", // ext len != 3
"a.r0a", // ext not alphabetic
"a.txt", // ext not allowlisted
];
for name in cases {
let der = manifest_der(
Some(0),
vec![1],
der_generalized_time_z("20260101000000Z"),
der_generalized_time_z("20260102000000Z"),
rpki::data_model::oid::OID_SHA256,
vec![file_and_hash(name, 0, &hash)],
);
let err = ManifestEContent::decode_der(&der).unwrap_err();
assert!(matches!(err, ManifestDecodeError::InvalidFileName(_)));
}
}
#[test]
fn iana_registry_filename_extensions_are_accepted() {
let hash = vec![0u8; 32];
for ext in rpki::data_model::common::IANA_RPKI_REPOSITORY_FILENAME_EXTENSIONS {
let name = format!("ok.{ext}");
let der = manifest_der(
Some(0),
vec![1],
der_generalized_time_z("20260101000000Z"),
der_generalized_time_z("20260102000000Z"),
rpki::data_model::oid::OID_SHA256,
vec![file_and_hash(&name, 0, &hash)],
);
ManifestEContent::decode_der(&der).expect("manifest should accept IANA extension");
}
}

View File

@ -0,0 +1,29 @@
use rpki::data_model::oid::{OID_CT_RPKI_MANIFEST, OID_SHA256, OID_SIGNED_DATA};
use rpki::data_model::signed_object::RpkiSignedObject;
#[test]
fn decode_manifest_signed_object_smoke() {
let der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
)
.expect("read MFT fixture");
let so = RpkiSignedObject::decode_der(&der).expect("decode signed object");
assert_eq!(so.content_info_content_type, OID_SIGNED_DATA);
assert_eq!(so.signed_data.version, 3);
assert_eq!(so.signed_data.digest_algorithms, vec![OID_SHA256.to_string()]);
assert_eq!(
so.signed_data.encap_content_info.econtent_type,
OID_CT_RPKI_MANIFEST
);
assert_eq!(so.signed_data.certificates.len(), 1);
assert!(!so.signed_data.certificates[0]
.sia_signed_object_uris
.is_empty());
assert!(so.signed_data.certificates[0]
.sia_signed_object_uris
.iter()
.any(|u| u.starts_with("rsync://")));
assert_eq!(so.signed_data.signer_infos.len(), 1);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,164 @@
use rpki::data_model::signed_object::{RpkiSignedObject, SignedObjectVerifyError};
use x509_parser::prelude::FromDer;
use x509_parser::prelude::X509Certificate;
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_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_null() -> Vec<u8> {
vec![0x05, 0x00]
}
fn der_oid(oid: &str) -> Vec<u8> {
use std::str::FromStr;
use der_parser::asn1_rs::ToDer;
let oid = der_parser::Oid::from_str(oid).unwrap();
oid.to_der_vec().unwrap()
}
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_bit_string(unused: u8, bytes: &[u8]) -> Vec<u8> {
let mut content = vec![unused];
content.extend_from_slice(bytes);
tlv(0x03, &content)
}
fn rsa_spki_der_with_modulus_bytes(modulus: &[u8], exponent: u64) -> Vec<u8> {
// SubjectPublicKeyInfo for RSA public key:
// SEQUENCE { algorithm AlgorithmIdentifier, subjectPublicKey BIT STRING }
// subjectPublicKey contains RSAPublicKey DER.
let alg = der_sequence(vec![
der_oid(rpki::data_model::oid::OID_RSA_ENCRYPTION),
der_null(),
]);
let rsa_pk = der_sequence(vec![der_integer_bytes(modulus), der_integer_u64(exponent)]);
let spk = der_bit_string(0, &rsa_pk);
der_sequence(vec![alg, spk])
}
#[test]
fn verify_mft_cms_signature_with_embedded_ee_cert() {
let der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
)
.expect("read MFT fixture");
let so = RpkiSignedObject::decode_der(&der).expect("decode signed object");
so.verify_signature()
.expect("CMS signature should verify with embedded EE cert");
}
#[test]
fn verify_fails_with_wrong_spki() {
let mft_der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
)
.expect("read MFT fixture");
let so = RpkiSignedObject::decode_der(&mft_der).expect("decode signed object");
let issuer_cert_der = std::fs::read(
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
)
.expect("read issuer certificate fixture");
let (_rem, issuer_cert) = X509Certificate::from_der(&issuer_cert_der).expect("parse cert");
let wrong_spki_der = issuer_cert.public_key().raw.to_vec();
let err = so
.verify_signature_with_ee_spki_der(&wrong_spki_der)
.unwrap_err();
assert!(matches!(err, SignedObjectVerifyError::InvalidSignature));
}
#[test]
fn verify_fails_with_tampered_signature() {
let der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
)
.expect("read MFT fixture");
let mut so = RpkiSignedObject::decode_der(&der).expect("decode signed object");
so.signed_data.signer_infos[0].signature[0] ^= 0x01;
let err = so.verify_signature().unwrap_err();
assert!(matches!(err, SignedObjectVerifyError::InvalidSignature));
}
#[test]
fn verify_rejects_spki_der_with_trailing_bytes() {
let der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
)
.expect("read MFT fixture");
let so = RpkiSignedObject::decode_der(&der).expect("decode signed object");
let mut spki_der = so.signed_data.certificates[0].spki_der.clone();
spki_der.push(0);
let err = so
.verify_signature_with_ee_spki_der(&spki_der)
.unwrap_err();
assert!(matches!(err, SignedObjectVerifyError::EeSpkiTrailingBytes(1)));
}
#[test]
fn verify_with_all_zero_modulus_exercises_strip_leading_zeros_fallback() {
let der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
)
.expect("read MFT fixture");
let so = RpkiSignedObject::decode_der(&der).expect("decode signed object");
// modulus INTEGER 0, exponent 65537 (valid exponent encoding); signature verification must fail
// but the SPKI parsing path should succeed.
let spki_der = rsa_spki_der_with_modulus_bytes(&[0x00], 65537);
let err = so
.verify_signature_with_ee_spki_der(&spki_der)
.unwrap_err();
assert!(matches!(err, SignedObjectVerifyError::InvalidSignature));
}