Compare commits
1 Commits
dev_1.0.0_
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 421847d329 |
@ -4,11 +4,9 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
der-parser = { version = "10.0.0", features = ["serialize"] }
|
der-parser = "10.0.0"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
base64 = "0.22.1"
|
|
||||||
sha2 = "0.10.8"
|
|
||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
time = "0.3.45"
|
time = "0.3.45"
|
||||||
ring = "0.17.14"
|
|
||||||
x509-parser = { version = "0.18.0", features = ["verify"] }
|
x509-parser = { version = "0.18.0", features = ["verify"] }
|
||||||
|
url = "2.5.8"
|
||||||
|
|||||||
17
README.md
17
README.md
@ -9,20 +9,3 @@ cargo test
|
|||||||
cargo test -- --nocapture
|
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
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|||||||
@ -1,80 +0,0 @@
|
|||||||
#!/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"
|
|
||||||
36
specs/01_tal.md
Normal file
36
specs/01_tal.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# 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 |
|
||||||
|
|
||||||
121
specs/02_ta.md
Normal file
121
specs/02_ta.md
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
# 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. 更新本地存储库缓存。
|
||||||
314
specs/03_rc.md
Normal file
314
specs/03_rc.md
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
# 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 §4,RFC 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-caRepository,accessLocation=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-caRepository,accessLocation=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 |
|
||||||
|
|
||||||
|
|
||||||
@ -1,158 +0,0 @@
|
|||||||
# 05. RPKI Signed Object(CMS SignedData 外壳)
|
|
||||||
|
|
||||||
## 5.1 对象定位
|
|
||||||
|
|
||||||
ROA、Manifest 等都属于 “RPKI Signed Object”,其外壳是 CMS SignedData,并受 RFC 6488 的 profile 约束;RFC 9589 进一步更新了 `signedAttrs` 的要求。RFC 6488 §2-§4;RFC 9589 §4。
|
|
||||||
|
|
||||||
本文件描述**通用外壳模型**(eContentType/eContent 由具体对象文档给出)。
|
|
||||||
|
|
||||||
## 5.2 原始载体与编码
|
|
||||||
|
|
||||||
- 载体:CMS `ContentInfo`,其中 `contentType` 为 SignedData。RFC 6488 §2;RFC 6488 §3(1a)。
|
|
||||||
- 编码:DER。RFC 6488 §2;RFC 6488 §3(1l)。
|
|
||||||
|
|
||||||
### 5.2.1 CMS 外壳:ContentInfo(ASN.1;RFC 5652 §3)
|
|
||||||
|
|
||||||
```asn1
|
|
||||||
ContentInfo ::= SEQUENCE {
|
|
||||||
contentType ContentType,
|
|
||||||
content [0] EXPLICIT ANY DEFINED BY contentType }
|
|
||||||
|
|
||||||
ContentType ::= OBJECT IDENTIFIER
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2.2 CMS 外壳:SignedData(ASN.1;RFC 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 外壳:EncapsulatedContentInfo(ASN.1;RFC 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 §2;RFC 6488 §3(1l)),且 eContent(payload)由对象规范定义并通常为 DER(如 ROA:RFC 9582 §4;Manifest:RFC 9286 §4.2)。
|
|
||||||
|
|
||||||
### 5.2.4 CMS 外壳:SignerInfo 与 Attribute(ASN.1;RFC 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.1;RFC 6488 §3;更新:RFC 9589 §4)
|
|
||||||
|
|
||||||
> 说明:上面是 CMS 的通用 ASN.1;RPKI 进一步约束取值与允许出现的字段(例如 SignedData.version 必须为 3、crls 必须省略、signedAttrs 的内容限制等)。RFC 6488 §2-§3;RFC 9589 §4。
|
|
||||||
|
|
||||||
### 5.2.6 signedAttrs 中允许的属性与 attrType OID(RFC 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 §2;RFC 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.1;RFC 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.2;RFC 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.1;RFC 6488 §2.1(SignerInfos 约束段落) |
|
|
||||||
|
|
||||||
### 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.1(ROA/MFT 文档定义);在 CMS 中以 OCTET STRING 承载 | RFC 6488 §2.1.3;RFC 9286 §4.2;RFC 9582 §4 |
|
|
||||||
|
|
||||||
### 5.3.4 `SignerInfoProfiled`
|
|
||||||
|
|
||||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| `version` | `int` | SignerInfo.version | MUST 为 3 | RFC 6488 §3(1e) |
|
|
||||||
| `sid_ski` | `bytes` | sid(SubjectKeyIdentifier) | 必须与 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 present;attrValues 等于 eContentType | RFC 9589 §4;RFC 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 §4;RFC 9589 §5 |
|
|
||||||
| `other_attrs` | `None` | 其它 signed attributes | MUST NOT be included(binary-signing-time 也不允许) | RFC 9589 §4 |
|
|
||||||
|
|
||||||
## 5.4 字段级约束清单(实现对照)
|
|
||||||
|
|
||||||
- ContentInfo.contentType 必须为 SignedData(OID `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)。
|
|
||||||
@ -1,106 +0,0 @@
|
|||||||
# 06. Manifest(MFT)
|
|
||||||
|
|
||||||
## 6.1 对象定位
|
|
||||||
|
|
||||||
Manifest 是 CA 发布点内对象的“清单”(文件名 + hash),用于 RP 侧检测删除/替换/回放等不一致情况。RFC 9286 §1;RFC 9286 §6。
|
|
||||||
|
|
||||||
Manifest 是一种 RPKI Signed Object:CMS 外壳遵循 RFC 6488/9589,eContent 遵循 RFC 9286。RFC 9286 §4;RFC 6488 §4;RFC 9589 §4。
|
|
||||||
|
|
||||||
## 6.2 原始载体与编码
|
|
||||||
|
|
||||||
- 外壳:CMS SignedData DER(见 `05_signed_object_cms.md`)。RFC 9286 §4;RFC 6488 §2。
|
|
||||||
- eContentType:`id-ct-rpkiManifest`,OID `1.2.840.113549.1.9.16.1.26`。RFC 9286 §4.1。
|
|
||||||
- eContent:DER 编码 ASN.1 `Manifest`。RFC 9286 §4.2。
|
|
||||||
|
|
||||||
### 6.2.1 eContentType 与 eContent 的 ASN.1 定义(RFC 9286 §4.1;RFC 9286 §4.2)
|
|
||||||
|
|
||||||
Manifest 是一种 RPKI signed object(CMS 外壳见 `05_signed_object_cms.md`)。其 `eContentType` 与 `eContent` 的 ASN.1 由 RFC 9286 明确定义。RFC 9286 §4。
|
|
||||||
|
|
||||||
**eContentType(OID)**: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 }
|
|
||||||
```
|
|
||||||
|
|
||||||
**eContent(Manifest 结构)**: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.1;RFC 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 §3;RFC 9589 §4。
|
|
||||||
2) 要求 `econtent_type == 1.2.840.113549.1.9.16.1.26`。RFC 9286 §4.1;RFC 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.1(fileHashAlg/fileList 定义)。
|
|
||||||
|
|
||||||
## 6.4 抽象数据模型(接口)
|
|
||||||
|
|
||||||
### 6.4.1 `ManifestObject`
|
|
||||||
|
|
||||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| `signed_object` | `RpkiSignedObject` | CMS 外壳 | 外壳约束见 RFC 6488/9589 | RFC 9286 §4;RFC 6488 §3;RFC 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.1(version) |
|
|
||||||
| `manifest_number` | `int` | manifestNumber | 0..MAX;可达 20 octets;issuer 必须单调递增;RP 必须可处理至 20 octets | RFC 9286 §4.2;RFC 9286 §4.2.1(manifestNumber) |
|
|
||||||
| `this_update` | `UtcTime` | thisUpdate | 由 ASN.1 `GeneralizedTime` 解析为 UTC 时间点;且必须比先前生成的 manifest 更新 | RFC 9286 §4.2;RFC 9286 §4.2.1(thisUpdate) |
|
|
||||||
| `next_update` | `UtcTime` | nextUpdate | 由 ASN.1 `GeneralizedTime` 解析为 UTC 时间点;且必须晚于 thisUpdate | RFC 9286 §4.2;RFC 9286 §4.2.1(nextUpdate) |
|
|
||||||
| `file_hash_alg` | `Oid` | fileHashAlg | 必须为 `id-sha256`(`2.16.840.1.101.3.4.2.1`) | RFC 9286 §4.2.1(fileHashAlg);RFC 7935 §2(引用 RFC 5754) |
|
|
||||||
| `files` | `list[FileAndHash]` | fileList | `SEQUENCE SIZE (0..MAX)`;每项含文件名与 hash | RFC 9286 §4.2;RFC 9286 §4.2.1(fileList) |
|
|
||||||
|
|
||||||
### 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.1(fileHashAlg/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 octets;issuer 不得超过 20 octets。RFC 9286 §4.2.1。
|
|
||||||
- `nextUpdate` 必须晚于 `thisUpdate`。RFC 9286 §4.2.1。
|
|
||||||
- `fileHashAlg` 必须符合算法 profile(SHA-256)。RFC 9286 §4.2.1;RFC 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 §4(Section 4 前导段落)。
|
|
||||||
- 用于验证 manifest 的 EE 证书 **MUST** 具有与 `thisUpdate..nextUpdate` 区间一致的有效期,以避免 CRL 无谓增长。RFC 9286 §4.2.1(manifestNumber 段落前的说明)。
|
|
||||||
- 替换 manifest 时,CA 必须撤销旧 manifest 对应 EE 证书;且若新 manifest 早于旧 manifest 的 nextUpdate 发行,则 CA **MUST** 同时发行新 CRL 撤销旧 manifest EE。RFC 9286 §4.2.1(nextUpdate 段落末);RFC 9286 §5.1(生成步骤)。
|
|
||||||
159
specs/07_roa.md
159
specs/07_roa.md
@ -1,159 +0,0 @@
|
|||||||
# 07. ROA(Route Origin Authorization)
|
|
||||||
|
|
||||||
## 7.1 对象定位
|
|
||||||
|
|
||||||
ROA 是一种 RPKI Signed Object,用于声明“某 AS 被授权起源某些前缀”。RFC 9582 §1;RFC 9582 §4。
|
|
||||||
|
|
||||||
ROA 由 CMS 外壳 + ROA eContent 组成:
|
|
||||||
|
|
||||||
- 外壳:RFC 6488(更新:RFC 9589)
|
|
||||||
- eContent:RFC 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。
|
|
||||||
- eContent:DER 编码 ASN.1 `RouteOriginAttestation`。RFC 9582 §4。
|
|
||||||
|
|
||||||
### 7.2.1 eContentType 与 eContent 的 ASN.1 定义(RFC 9582 §3;RFC 9582 §4)
|
|
||||||
|
|
||||||
ROA 是一种 RPKI signed object(CMS 外壳见 `05_signed_object_cms.md`)。RFC 9582 定义了其 `eContentType` 以及 `eContent`(payload)的 ASN.1。RFC 9582 §3-§4。
|
|
||||||
|
|
||||||
**eContentType(OID)**: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) }
|
|
||||||
```
|
|
||||||
|
|
||||||
**eContent(ROA 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 §3;RFC 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 §1;RFC 6488 §3;RFC 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 §4(ASID 定义);RFC 9582 §4.2 |
|
|
||||||
| `ip_addr_blocks` | `list[RoaIpAddressFamily]` | ipAddrBlocks | `SIZE(1..2)`;最多 IPv4/IPv6 各一个;建议 canonicalize | RFC 9582 §4;RFC 9582 §4.3.1;RFC 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 §4(ROAAddressesIPv4/IPv6);RFC 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 §4;RFC 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 6488);RFC 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 §5;RFC 3779 §2.3。
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
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",
|
|
||||||
];
|
|
||||||
@ -1,16 +1,52 @@
|
|||||||
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::extensions::{AuthorityKeyIdentifier, ParsedExtension, X509Extension};
|
||||||
use x509_parser::prelude::FromDer;
|
use x509_parser::prelude::FromDer;
|
||||||
use x509_parser::prelude::X509Version;
|
use x509_parser::prelude::X509Version;
|
||||||
use x509_parser::revocation_list::CertificateRevocationList;
|
use x509_parser::revocation_list::CertificateRevocationList;
|
||||||
|
use x509_parser::asn1_rs::Tag;
|
||||||
use x509_parser::certificate::X509Certificate;
|
use x509_parser::certificate::X509Certificate;
|
||||||
use x509_parser::x509::SubjectPublicKeyInfo;
|
use x509_parser::x509::SubjectPublicKeyInfo;
|
||||||
use x509_parser::x509::AlgorithmIdentifier;
|
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)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct RevokedCert {
|
pub struct RevokedCert {
|
||||||
pub serial_number: BigUnsigned,
|
pub serial_number: BigUnsigned,
|
||||||
@ -125,23 +161,23 @@ impl RpkixCrl {
|
|||||||
if !rc.extensions().is_empty() {
|
if !rc.extensions().is_empty() {
|
||||||
return Err(CrlDecodeError::EntryExtensionsNotAllowed);
|
return Err(CrlDecodeError::EntryExtensionsNotAllowed);
|
||||||
}
|
}
|
||||||
let revocation_date = crate::data_model::common::asn1_time_to_model(rc.revocation_date);
|
let revocation_date = asn1_time_to_model(rc.revocation_date);
|
||||||
validate_time_encoding_rfc5280("revocationDate", &revocation_date)?;
|
validate_time_encoding("revocationDate", &revocation_date)?;
|
||||||
Ok(RevokedCert {
|
Ok(RevokedCert {
|
||||||
serial_number: BigUnsigned::from_biguint(rc.serial()),
|
serial_number: biguint_to_big_unsigned(rc.serial()),
|
||||||
revocation_date,
|
revocation_date,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
let this_update = crate::data_model::common::asn1_time_to_model(crl.last_update());
|
let this_update = asn1_time_to_model(crl.last_update());
|
||||||
validate_time_encoding_rfc5280("thisUpdate", &this_update)?;
|
validate_time_encoding("thisUpdate", &this_update)?;
|
||||||
|
|
||||||
let next_update = crl
|
let next_update = crl
|
||||||
.next_update()
|
.next_update()
|
||||||
.map(crate::data_model::common::asn1_time_to_model)
|
.map(asn1_time_to_model)
|
||||||
.ok_or(CrlDecodeError::NextUpdateMissing)?;
|
.ok_or(CrlDecodeError::NextUpdateMissing)?;
|
||||||
validate_time_encoding_rfc5280("nextUpdate", &next_update)?;
|
validate_time_encoding("nextUpdate", &next_update)?;
|
||||||
|
|
||||||
Ok(RpkixCrl {
|
Ok(RpkixCrl {
|
||||||
raw_der: der.to_vec(),
|
raw_der: der.to_vec(),
|
||||||
@ -265,10 +301,19 @@ pub enum CrlVerifyError {
|
|||||||
InvalidSignature(String),
|
InvalidSignature(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_time_encoding_rfc5280(
|
fn asn1_time_to_model(t: x509_parser::time::ASN1Time) -> Asn1TimeUtc {
|
||||||
field: &'static str,
|
let encoding = if t.is_utctime() {
|
||||||
t: &Asn1TimeUtc,
|
Asn1TimeEncoding::UtcTime
|
||||||
) -> Result<(), CrlDecodeError> {
|
} else {
|
||||||
|
Asn1TimeEncoding::GeneralizedTime
|
||||||
|
};
|
||||||
|
Asn1TimeUtc {
|
||||||
|
utc: t.to_datetime(),
|
||||||
|
encoding,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_time_encoding(field: &'static str, t: &Asn1TimeUtc) -> Result<(), CrlDecodeError> {
|
||||||
let year = t.utc.year();
|
let year = t.utc.year();
|
||||||
let expected = if year <= 2049 {
|
let expected = if year <= 2049 {
|
||||||
Asn1TimeEncoding::UtcTime
|
Asn1TimeEncoding::UtcTime
|
||||||
@ -286,10 +331,10 @@ fn validate_time_encoding_rfc5280(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn validate_sig_params(sig: &AlgorithmIdentifier<'_>) -> Result<(), CrlDecodeError> {
|
fn validate_sig_params(sig: &AlgorithmIdentifier<'_>) -> Result<(), CrlDecodeError> {
|
||||||
if crate::data_model::common::algorithm_params_absent_or_null(sig) {
|
match sig.parameters.as_ref() {
|
||||||
Ok(())
|
None => Ok(()),
|
||||||
} else {
|
Some(p) if p.tag() == Tag::Null => Ok(()),
|
||||||
Err(CrlDecodeError::InvalidSignatureAlgorithmParameters)
|
Some(_p) => Err(CrlDecodeError::InvalidSignatureAlgorithmParameters),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -322,7 +367,7 @@ fn parse_and_validate_extensions(exts: &[X509Extension<'_>]) -> Result<CrlExtens
|
|||||||
if n.bits() > 159 {
|
if n.bits() > 159 {
|
||||||
return Err(CrlDecodeError::CrlNumberOutOfRange);
|
return Err(CrlDecodeError::CrlNumberOutOfRange);
|
||||||
}
|
}
|
||||||
crl_number = Some(BigUnsigned::from_biguint(&n));
|
crl_number = Some(biguint_to_big_unsigned(&n));
|
||||||
}
|
}
|
||||||
_ => return Err(CrlDecodeError::UnsupportedExtension(oid)),
|
_ => return Err(CrlDecodeError::UnsupportedExtension(oid)),
|
||||||
}
|
}
|
||||||
@ -365,6 +410,14 @@ 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>> {
|
fn get_subject_key_identifier(cert: &X509Certificate<'_>) -> Option<Vec<u8>> {
|
||||||
cert.extensions()
|
cert.extensions()
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@ -1,284 +0,0 @@
|
|||||||
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())
|
|
||||||
}
|
|
||||||
@ -1,5 +1,6 @@
|
|||||||
pub mod common;
|
|
||||||
pub mod crl;
|
pub mod crl;
|
||||||
pub mod oid;
|
mod rc;
|
||||||
pub mod signed_object;
|
mod tal;
|
||||||
pub mod manifest;
|
mod ta;
|
||||||
|
mod resources;
|
||||||
|
mod oids;
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
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";
|
|
||||||
13
src/data_model/oids.rs
Normal file
13
src/data_model/oids.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
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";
|
||||||
519
src/data_model/rc.rs
Normal file
519
src/data_model/rc.rs
Normal file
@ -0,0 +1,519 @@
|
|||||||
|
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();
|
||||||
|
//
|
||||||
|
// }
|
||||||
90
src/data_model/resources/as_resources.rs
Normal file
90
src/data_model/resources/as_resources.rs
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
|
||||||
|
#[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
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/data_model/resources/ip_resources.rs
Normal file
46
src/data_model/resources/ip_resources.rs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
#[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);
|
||||||
3
src/data_model/resources/mod.rs
Normal file
3
src/data_model/resources/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub(crate) mod ip_resources;
|
||||||
|
pub(crate) mod as_resources;
|
||||||
|
pub mod resource;
|
||||||
10
src/data_model/resources/resource.rs
Normal file
10
src/data_model/resources/resource.rs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1,771 +0,0 @@
|
|||||||
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..]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
27
src/data_model/ta.rs
Normal file
27
src/data_model/ta.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
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 {
|
||||||
|
//
|
||||||
|
// }
|
||||||
18
src/data_model/tal.rs
Normal file
18
src/data_model/tal.rs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/// 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),
|
||||||
|
}
|
||||||
@ -1,93 +0,0 @@
|
|||||||
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]);
|
|
||||||
}
|
|
||||||
@ -1,127 +0,0 @@
|
|||||||
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)));
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,411 +0,0 @@
|
|||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
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
@ -1,164 +0,0 @@
|
|||||||
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));
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user