diff --git a/Cargo.toml b/Cargo.toml index 49f8bfa..1ceb1ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,11 @@ version = "0.1.0" edition = "2024" [dependencies] -der-parser = "10.0.0" +der-parser = { version = "10.0.0", features = ["serialize"] } hex = "0.4.3" +base64 = "0.22.1" +sha2 = "0.10.8" thiserror = "2.0.18" time = "0.3.45" +ring = "0.17.14" x509-parser = { version = "0.18.0", features = ["verify"] } diff --git a/README.md b/README.md index e89d904..c398ee1 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,20 @@ cargo test cargo test -- --nocapture ``` +# 覆盖率(cargo-llvm-cov) + +安装工具: + +``` +rustup component add llvm-tools-preview +cargo install cargo-llvm-cov --locked +``` + +统计行覆盖率并要求 >=90%: + +``` +./scripts/coverage.sh +# 或 +cargo llvm-cov --fail-under-lines 90 +``` + diff --git a/scripts/coverage.sh b/scripts/coverage.sh new file mode 100755 index 0000000..799772f --- /dev/null +++ b/scripts/coverage.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Requires: +# rustup component add llvm-tools-preview +# cargo install cargo-llvm-cov --locked + +run_out="$(mktemp)" +text_out="$(mktemp)" +html_out="$(mktemp)" + +cleanup() { + rm -f "$run_out" "$text_out" "$html_out" +} +trap cleanup EXIT + +# Preserve colored output even though we post-process output by running under a pseudo-TTY. +# We run tests only once, then generate both CLI text + HTML reports without rerunning tests. +set +e + +cargo llvm-cov clean --workspace >/dev/null 2>&1 + +# 1) Run tests once to collect coverage data (no report). +script -q -e -c "CARGO_TERM_COLOR=always cargo llvm-cov --no-report" "$run_out" >/dev/null 2>&1 +run_status="$?" + +# 2) CLI summary report + fail-under gate (no test rerun). +script -q -e -c "CARGO_TERM_COLOR=always cargo llvm-cov report --fail-under-lines 90" "$text_out" >/dev/null 2>&1 +text_status="$?" + +# 3) HTML report (no test rerun). +script -q -e -c "CARGO_TERM_COLOR=always cargo llvm-cov report --html" "$html_out" >/dev/null 2>&1 +html_status="$?" + +set -e + +strip_script_noise() { + tr -d '\r' | sed '/^Script \(started\|done\) on /d' +} + +strip_ansi_for_parse() { + awk ' + { + line = $0 + gsub(/\033\[[0-9;]*[A-Za-z]/, "", line) # CSI escapes + gsub(/\033\([A-Za-z]/, "", line) # charset escapes (e.g., ESC(B) + gsub(/\r/, "", line) + print line + } + ' +} + +cat "$run_out" | strip_script_noise +cat "$text_out" | strip_script_noise +cat "$html_out" | strip_script_noise + +cat "$run_out" | strip_ansi_for_parse | awk ' + BEGIN { + passed=0; failed=0; ignored=0; measured=0; filtered=0; + } + /^test result: / { + if (match($0, /([0-9]+) passed; ([0-9]+) failed; ([0-9]+) ignored; ([0-9]+) measured; ([0-9]+) filtered out;/, m)) { + passed += m[1]; failed += m[2]; ignored += m[3]; measured += m[4]; filtered += m[5]; + } + } + END { + executed = passed + failed; + total = passed + failed + ignored + measured; + printf("\nTEST SUMMARY (all suites): passed=%d failed=%d ignored=%d measured=%d filtered_out=%d executed=%d total=%d\n", + passed, failed, ignored, measured, filtered, executed, total); + } +' + +echo +echo "HTML report: target/llvm-cov/html/index.html" + +status="$text_status" +if [ "$run_status" -ne 0 ]; then status="$run_status"; fi +if [ "$html_status" -ne 0 ]; then status="$html_status"; fi +exit "$status" diff --git a/specs/05_signed_object_cms.md b/specs/05_signed_object_cms.md new file mode 100644 index 0000000..4447a0c --- /dev/null +++ b/specs/05_signed_object_cms.md @@ -0,0 +1,158 @@ +# 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)。 diff --git a/specs/06_manifest_mft.md b/specs/06_manifest_mft.md new file mode 100644 index 0000000..9c8d9b9 --- /dev/null +++ b/specs/06_manifest_mft.md @@ -0,0 +1,106 @@ +# 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(生成步骤)。 diff --git a/specs/07_roa.md b/specs/07_roa.md new file mode 100644 index 0000000..cecdd09 --- /dev/null +++ b/specs/07_roa.md @@ -0,0 +1,159 @@ +# 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。 diff --git a/src/data_model/common.rs b/src/data_model/common.rs new file mode 100644 index 0000000..6e0925b --- /dev/null +++ b/src/data_model/common.rs @@ -0,0 +1,108 @@ +use x509_parser::asn1_rs::Tag; +use x509_parser::x509::AlgorithmIdentifier; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Asn1TimeEncoding { + UtcTime, + GeneralizedTime, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Asn1TimeUtc { + pub utc: time::OffsetDateTime, + pub encoding: Asn1TimeEncoding, +} + +impl Asn1TimeUtc { + /// Validate Time encoding rules (RFC 5280): years 1950-2049 use UTCTime, + /// other years use GeneralizedTime. + pub fn validate_encoding_rfc5280( + &self, + field: &'static str, + ) -> Result<(), InvalidTimeEncodingError> { + let year = self.utc.year(); + let expected = if year <= 2049 { + Asn1TimeEncoding::UtcTime + } else { + Asn1TimeEncoding::GeneralizedTime + }; + if self.encoding != expected { + return Err(InvalidTimeEncodingError { + field, + year, + encoding: self.encoding, + }); + } + Ok(()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BigUnsigned { + /// Minimal big-endian bytes. For zero, this is `[0]`. + pub bytes_be: Vec, +} + +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 { + 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: +/// 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", +]; diff --git a/src/data_model/crl.rs b/src/data_model/crl.rs index 6d52d7f..afa9491 100644 --- a/src/data_model/crl.rs +++ b/src/data_model/crl.rs @@ -1,52 +1,16 @@ +pub use crate::data_model::common::{Asn1TimeEncoding, Asn1TimeUtc, BigUnsigned}; +use crate::data_model::oid::{ + OID_AUTHORITY_KEY_IDENTIFIER, OID_CRL_NUMBER, OID_SHA256_WITH_RSA_ENCRYPTION, + OID_SUBJECT_KEY_IDENTIFIER, +}; use x509_parser::extensions::{AuthorityKeyIdentifier, ParsedExtension, X509Extension}; use x509_parser::prelude::FromDer; use x509_parser::prelude::X509Version; use x509_parser::revocation_list::CertificateRevocationList; -use x509_parser::asn1_rs::Tag; use x509_parser::certificate::X509Certificate; use x509_parser::x509::SubjectPublicKeyInfo; use x509_parser::x509::AlgorithmIdentifier; -const OID_SHA256_WITH_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.11"; -const OID_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35"; -const OID_CRL_NUMBER: &str = "2.5.29.20"; -const OID_SUBJECT_KEY_IDENTIFIER: &str = "2.5.29.14"; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Asn1TimeEncoding { - UtcTime, - GeneralizedTime, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Asn1TimeUtc { - pub utc: time::OffsetDateTime, - pub encoding: Asn1TimeEncoding, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct BigUnsigned { - /// Minimal big-endian bytes. For zero, this is `[0]`. - pub bytes_be: Vec, -} - -impl BigUnsigned { - pub fn to_hex_upper(&self) -> String { - hex::encode_upper(&self.bytes_be) - } - - pub fn to_u64(&self) -> Option { - if self.bytes_be.len() > 8 { - return None; - } - let mut value: u64 = 0; - for &b in &self.bytes_be { - value = (value << 8) | (b as u64); - } - Some(value) - } -} - #[derive(Clone, Debug, PartialEq, Eq)] pub struct RevokedCert { pub serial_number: BigUnsigned, @@ -161,23 +125,23 @@ impl RpkixCrl { if !rc.extensions().is_empty() { return Err(CrlDecodeError::EntryExtensionsNotAllowed); } - let revocation_date = asn1_time_to_model(rc.revocation_date); - validate_time_encoding("revocationDate", &revocation_date)?; + let revocation_date = crate::data_model::common::asn1_time_to_model(rc.revocation_date); + validate_time_encoding_rfc5280("revocationDate", &revocation_date)?; Ok(RevokedCert { - serial_number: biguint_to_big_unsigned(rc.serial()), + serial_number: BigUnsigned::from_biguint(rc.serial()), revocation_date, }) }) .collect::, _>>()?; - let this_update = asn1_time_to_model(crl.last_update()); - validate_time_encoding("thisUpdate", &this_update)?; + let this_update = crate::data_model::common::asn1_time_to_model(crl.last_update()); + validate_time_encoding_rfc5280("thisUpdate", &this_update)?; let next_update = crl .next_update() - .map(asn1_time_to_model) + .map(crate::data_model::common::asn1_time_to_model) .ok_or(CrlDecodeError::NextUpdateMissing)?; - validate_time_encoding("nextUpdate", &next_update)?; + validate_time_encoding_rfc5280("nextUpdate", &next_update)?; Ok(RpkixCrl { raw_der: der.to_vec(), @@ -301,19 +265,10 @@ pub enum CrlVerifyError { InvalidSignature(String), } -fn asn1_time_to_model(t: x509_parser::time::ASN1Time) -> Asn1TimeUtc { - let encoding = if t.is_utctime() { - Asn1TimeEncoding::UtcTime - } else { - Asn1TimeEncoding::GeneralizedTime - }; - Asn1TimeUtc { - utc: t.to_datetime(), - encoding, - } -} - -fn validate_time_encoding(field: &'static str, t: &Asn1TimeUtc) -> Result<(), CrlDecodeError> { +fn validate_time_encoding_rfc5280( + field: &'static str, + t: &Asn1TimeUtc, +) -> Result<(), CrlDecodeError> { let year = t.utc.year(); let expected = if year <= 2049 { Asn1TimeEncoding::UtcTime @@ -331,10 +286,10 @@ fn validate_time_encoding(field: &'static str, t: &Asn1TimeUtc) -> Result<(), Cr } fn validate_sig_params(sig: &AlgorithmIdentifier<'_>) -> Result<(), CrlDecodeError> { - match sig.parameters.as_ref() { - None => Ok(()), - Some(p) if p.tag() == Tag::Null => Ok(()), - Some(_p) => Err(CrlDecodeError::InvalidSignatureAlgorithmParameters), + if crate::data_model::common::algorithm_params_absent_or_null(sig) { + Ok(()) + } else { + Err(CrlDecodeError::InvalidSignatureAlgorithmParameters) } } @@ -367,7 +322,7 @@ fn parse_and_validate_extensions(exts: &[X509Extension<'_>]) -> Result 159 { return Err(CrlDecodeError::CrlNumberOutOfRange); } - crl_number = Some(biguint_to_big_unsigned(&n)); + crl_number = Some(BigUnsigned::from_biguint(&n)); } _ => return Err(CrlDecodeError::UnsupportedExtension(oid)), } @@ -410,14 +365,6 @@ fn parse_crl_number(ext: &X509Extension<'_>) -> Result 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> { cert.extensions() .iter() diff --git a/src/data_model/manifest.rs b/src/data_model/manifest.rs new file mode 100644 index 0000000..0b1daaf --- /dev/null +++ b/src/data_model/manifest.rs @@ -0,0 +1,284 @@ +use crate::data_model::common::BigUnsigned; +use crate::data_model::oid::{OID_CT_RPKI_MANIFEST, OID_SHA256}; +use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError}; +use der_parser::ber::BerObjectContent; +use der_parser::der::{parse_der, DerObject, Tag}; +use time::OffsetDateTime; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ManifestObject { + pub signed_object: RpkiSignedObject, + pub econtent_type: String, + pub manifest: ManifestEContent, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ManifestEContent { + pub version: u32, + pub manifest_number: BigUnsigned, + pub this_update: OffsetDateTime, + pub next_update: OffsetDateTime, + pub file_hash_alg: String, + pub files: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FileAndHash { + pub file_name: String, + pub hash_bytes: Vec, +} + +#[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 { + let signed_object = RpkiSignedObject::decode_der(der)?; + Self::from_signed_object(signed_object) + } + + pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result { + 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 { + 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 { + 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 { + 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, 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), 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 { + let oid = obj + .as_oid() + .map_err(|e| ManifestDecodeError::Parse(e.to_string()))?; + Ok(oid.to_id_string()) +} diff --git a/src/data_model/mod.rs b/src/data_model/mod.rs index d20a38c..d75e3fc 100644 --- a/src/data_model/mod.rs +++ b/src/data_model/mod.rs @@ -1,2 +1,5 @@ +pub mod common; pub mod crl; - +pub mod oid; +pub mod signed_object; +pub mod manifest; diff --git a/src/data_model/oid.rs b/src/data_model/oid.rs new file mode 100644 index 0000000..b3eb09c --- /dev/null +++ b/src/data_model/oid.rs @@ -0,0 +1,20 @@ +pub const OID_SHA256: &str = "2.16.840.1.101.3.4.2.1"; + +pub const OID_SIGNED_DATA: &str = "1.2.840.113549.1.7.2"; + +pub const OID_CMS_ATTR_CONTENT_TYPE: &str = "1.2.840.113549.1.9.3"; +pub const OID_CMS_ATTR_MESSAGE_DIGEST: &str = "1.2.840.113549.1.9.4"; +pub const OID_CMS_ATTR_SIGNING_TIME: &str = "1.2.840.113549.1.9.5"; + +pub const OID_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.1"; +pub const OID_SHA256_WITH_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.11"; + +pub const OID_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35"; +pub const OID_CRL_NUMBER: &str = "2.5.29.20"; +pub const OID_SUBJECT_KEY_IDENTIFIER: &str = "2.5.29.14"; + +pub const OID_CT_RPKI_MANIFEST: &str = "1.2.840.113549.1.9.16.1.26"; + +// X.509 extensions / access methods (RFC 5280 / RFC 6487) +pub const OID_SUBJECT_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.11"; +pub const OID_AD_SIGNED_OBJECT: &str = "1.3.6.1.5.5.7.48.11"; diff --git a/src/data_model/signed_object.rs b/src/data_model/signed_object.rs new file mode 100644 index 0000000..e8ba5f8 --- /dev/null +++ b/src/data_model/signed_object.rs @@ -0,0 +1,771 @@ +use crate::data_model::common::{Asn1TimeEncoding, Asn1TimeUtc}; +use crate::data_model::oid::{ + OID_CMS_ATTR_CONTENT_TYPE, OID_CMS_ATTR_MESSAGE_DIGEST, OID_CMS_ATTR_SIGNING_TIME, + OID_RSA_ENCRYPTION, OID_SHA256, OID_SHA256_WITH_RSA_ENCRYPTION, OID_SIGNED_DATA, + OID_AD_SIGNED_OBJECT, OID_SUBJECT_INFO_ACCESS, OID_SUBJECT_KEY_IDENTIFIER, +}; +use der_parser::ber::Class; +use der_parser::der::{parse_der, DerObject, Tag}; +use sha2::{Digest, Sha256}; +use x509_parser::extensions::GeneralName; +use x509_parser::extensions::ParsedExtension; +use x509_parser::public_key::PublicKey; +use x509_parser::prelude::FromDer; +use x509_parser::x509::SubjectPublicKeyInfo; +use x509_parser::certificate::X509Certificate; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ResourceEeCertificate { + pub raw_der: Vec, + pub subject_key_identifier: Vec, + pub spki_der: Vec, + pub sia_signed_object_uris: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RpkiSignedObject { + pub raw_der: Vec, + 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, + pub encap_content_info: EncapsulatedContentInfo, + pub certificates: Vec, + pub crls_present: bool, + pub signer_infos: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EncapsulatedContentInfo { + pub econtent_type: String, + pub econtent: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SignerInfoProfiled { + pub version: u32, + pub sid_ski: Vec, + pub digest_algorithm: String, + pub signature_algorithm: String, + pub signed_attrs: SignedAttrsProfiled, + pub unsigned_attrs_present: bool, + pub signature: Vec, + pub signed_attrs_der_for_signature: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SignedAttrsProfiled { + pub content_type: String, + pub message_digest: Vec, + 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 { + 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 { + // 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 { + 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> = 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 { + 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, 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 { + 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, SignedObjectDecodeError> { + let mut sia: Option> = 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 = 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 { + 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 = None; + let mut signed_attrs_der_for_signature: Option> = 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 { + let mut content_type: Option = None; + let mut message_digest: Option> = None; + let mut signing_time: Option = 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 { + 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, 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 { + 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..] + } +} diff --git a/tests/test_common.rs b/tests/test_common.rs new file mode 100644 index 0000000..410f48c --- /dev/null +++ b/tests/test_common.rs @@ -0,0 +1,93 @@ +use rpki::data_model::common::{ + algorithm_params_absent_or_null, Asn1TimeEncoding, Asn1TimeUtc, BigUnsigned, +}; +use x509_parser::prelude::FromDer; +use x509_parser::x509::AlgorithmIdentifier; +use x509_parser::time::ASN1Time; + +#[test] +fn big_unsigned_helpers() { + let n = BigUnsigned { bytes_be: vec![0] }; + assert_eq!(n.to_u64(), Some(0)); + assert_eq!(n.to_hex_upper(), "00"); + + let n = BigUnsigned { + bytes_be: vec![0x01, 0x02, 0x03], + }; + assert_eq!(n.to_u64(), Some(0x010203)); + assert_eq!(n.to_hex_upper(), "010203"); + + let n = BigUnsigned { + bytes_be: vec![0; 9], + }; + assert_eq!(n.to_u64(), None); +} + +#[test] +fn time_encoding_validation() { + let t = Asn1TimeUtc { + utc: time::OffsetDateTime::from_unix_timestamp(0).unwrap(), + encoding: Asn1TimeEncoding::UtcTime, + }; + t.validate_encoding_rfc5280("t").expect("utc ok"); + + let t = Asn1TimeUtc { + utc: time::OffsetDateTime::parse("2050-01-01T00:00:00Z", &time::format_description::well_known::Rfc3339) + .unwrap(), + encoding: Asn1TimeEncoding::UtcTime, + }; + assert!(t.validate_encoding_rfc5280("t").is_err()); +} + +#[test] +fn algorithm_params_absent_or_null_helper() { + // AlgorithmIdentifier ::= SEQUENCE { algorithm OID, parameters ANY OPTIONAL } + // Using sha256WithRSAEncryption with NULL parameters. + let alg_null = hex::decode( + "300D06092A864886F70D01010B0500", + ) + .unwrap(); + let (_rem, id) = AlgorithmIdentifier::from_der(&alg_null).expect("parse AlgorithmIdentifier"); + assert!(algorithm_params_absent_or_null(&id)); + + // Same OID, but parameters = INTEGER 1 (invalid for our helper). + let alg_int = hex::decode( + "300E06092A864886F70D01010B020101", + ) + .unwrap(); + let (_rem, id) = AlgorithmIdentifier::from_der(&alg_int).expect("parse AlgorithmIdentifier"); + assert!(!algorithm_params_absent_or_null(&id)); + + // parameters absent + let oid = der_parser::Oid::from(&[1u64, 2, 3]).unwrap(); + let id = AlgorithmIdentifier::new(oid, None); + assert!(algorithm_params_absent_or_null(&id)); +} + +#[test] +fn asn1_time_to_model_helper_covers_branches() { + let dt_utc = time::OffsetDateTime::parse( + "2024-01-01T00:00:00Z", + &time::format_description::well_known::Rfc3339, + ) + .unwrap(); + let t = ASN1Time::new_utc(dt_utc); + let m = rpki::data_model::common::asn1_time_to_model(t); + assert_eq!(m.encoding, Asn1TimeEncoding::UtcTime); + + let dt_gen = time::OffsetDateTime::parse( + "2050-01-01T00:00:00Z", + &time::format_description::well_known::Rfc3339, + ) + .unwrap(); + let t = ASN1Time::new_generalized(dt_gen); + let m = rpki::data_model::common::asn1_time_to_model(t); + assert_eq!(m.encoding, Asn1TimeEncoding::GeneralizedTime); +} + +#[test] +fn big_unsigned_from_biguint_zero_encodes_as_single_zero_byte() { + let z = der_parser::num_bigint::BigUint::from(0u32); + let n = BigUnsigned::from_biguint(&z); + assert_eq!(n.bytes_be, vec![0]); +} diff --git a/tests/test_crl_errors.rs b/tests/test_crl_errors.rs new file mode 100644 index 0000000..139306c --- /dev/null +++ b/tests/test_crl_errors.rs @@ -0,0 +1,127 @@ +use rpki::data_model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl}; +use x509_parser::prelude::FromDer; +use x509_parser::prelude::X509Certificate; + +const TEST_NO_CRLSIGN_CERT_DER_B64: &str = "MIIDATCCAemgAwIBAgIUCyQLQJn92+gyAzvIz22q1F/97OMwDQYJKoZIhvcNAQELBQAwGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMB4XDTI2MDEyNzAzNTk1OVoXDTM2MDEyNTAzNTk1OVowGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/aoMU8J6cddkM2r6F2snd1rCdQPepgo2T2lrqWFcxnQJdcxBL1OYg3wFi95TJmZSeIHIOGauDaJ2abmjgyOUHOC4U68x66JRg4hLkmLxo1cf3uYHWl9Obph6g2qPRvN80ORq70JPuL6mAfUkNiO9hnwK6oQiTzc/rjCQGIFH8kTESBMXLfNCyUpGi+MNztYH6Ha6bKAQuXgd29OFwIkOlGQnYgGC2qBMvnp86eITvV1gTiuI8Ho9m9nZHCmaD7TylvkMDq8Hk5nkIpRcG0uO60SkR2BiMOYe/TNn5dTmHd6bsdbU2GOvgnq1SnqGq3FOWhKIe3ycUJde0uNfZOqRwIDAQABoz8wPTAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUFjyzfJCDNhFfKxVr06kjUkE23dMwDQYJKoZIhvcNAQELBQADggEBAK98n2gVlwKA3Ob1YeAm9f+8hm7pbvrt0tA8GW180CILjf09k7fKgiRlxqGdZ9ySXjU52+zCqu3MpBXVbI87ZC+zA6uK05n4y1F0n85MJ9hGR2UEiPcqou85X73LvioynnSOy/OV1PjKJXReUsqF3GgDtgcMyFssPJ9s/5DWuUCScUJY6pu0kuIGOLQ/oXUw4TvxUeyz73gOTiAJshVTQoLpHUhj0595S7lArjwi7oLI1b8m8guTknvhk0Sc3tJZmUqOcIvYIs0guHpaeC+sMoF4K+6UTrxxOBdX+fUEWNpUyYXWHjdZq25PbJdHwA/VAW2zYVojaVREligf0Qfo6F4="; + +fn test_no_crlsign_cert_der() -> Vec { + 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))); +} diff --git a/tests/test_manifest_decode.rs b/tests/test_manifest_decode.rs new file mode 100644 index 0000000..69aa298 --- /dev/null +++ b/tests/test_manifest_decode.rs @@ -0,0 +1,35 @@ +use rpki::data_model::manifest::{ManifestEContent, ManifestObject}; + +#[test] +fn decode_manifest_fixture_smoke() { + let der = std::fs::read( + "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", + ) + .expect("read MFT fixture"); + let mft = ManifestObject::decode_der(&der).expect("decode manifest object"); + + assert_eq!(mft.manifest.version, 0); + assert_eq!(mft.manifest.file_hash_alg, rpki::data_model::oid::OID_SHA256); + assert!(mft.manifest.next_update > mft.manifest.this_update); + assert!(!mft.manifest.files.is_empty()); + // The manifest file MUST NOT be listed in its own fileList. + assert!(mft + .manifest + .files + .iter() + .all(|f| !f.file_name.to_ascii_lowercase().ends_with(".mft"))); +} + +#[test] +fn decode_manifest_econtent_from_fixture_signed_object() { + let so_der = std::fs::read( + "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", + ) + .expect("read MFT fixture"); + let so = rpki::data_model::signed_object::RpkiSignedObject::decode_der(&so_der) + .expect("decode signed object"); + let e = ManifestEContent::decode_der(&so.signed_data.encap_content_info.econtent) + .expect("decode manifest eContent"); + assert_eq!(e.version, 0); +} + diff --git a/tests/test_manifest_decode_errors.rs b/tests/test_manifest_decode_errors.rs new file mode 100644 index 0000000..b21b9f3 --- /dev/null +++ b/tests/test_manifest_decode_errors.rs @@ -0,0 +1,411 @@ +use rpki::data_model::common::BigUnsigned; +use rpki::data_model::manifest::{ManifestDecodeError, ManifestEContent, ManifestObject}; +use rpki::data_model::signed_object::RpkiSignedObject; + +fn len_bytes(len: usize) -> Vec { + 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 { + let mut out = vec![tag]; + out.extend(len_bytes(content.len())); + out.extend_from_slice(content); + out +} + +fn der_integer_bytes(bytes: &[u8]) -> Vec { + tlv(0x02, bytes) +} + +fn der_integer_u64(v: u64) -> Vec { + 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 { + 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 { + tlv(0x18, s.as_bytes()) +} + +fn der_ia5(s: &str) -> Vec { + tlv(0x16, s.as_bytes()) +} + +fn der_bit_string(unused: u8, bytes: &[u8]) -> Vec { + let mut content = vec![unused]; + content.extend_from_slice(bytes); + tlv(0x03, &content) +} + +fn der_sequence(children: Vec>) -> Vec { + 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) -> Vec { + tlv(0xA0 | (tag_no & 0x1F), &inner_der) +} + +fn manifest_der( + version: Option, + manifest_number: Vec, + this_update: Vec, + next_update: Vec, + file_hash_alg_oid: &str, + file_list: Vec>, +) -> Vec { + 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 { + 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"); + } +} diff --git a/tests/test_signed_object_decode.rs b/tests/test_signed_object_decode.rs new file mode 100644 index 0000000..e5ec462 --- /dev/null +++ b/tests/test_signed_object_decode.rs @@ -0,0 +1,29 @@ +use rpki::data_model::oid::{OID_CT_RPKI_MANIFEST, OID_SHA256, OID_SIGNED_DATA}; +use rpki::data_model::signed_object::RpkiSignedObject; + +#[test] +fn decode_manifest_signed_object_smoke() { + let der = std::fs::read( + "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", + ) + .expect("read MFT fixture"); + + let so = RpkiSignedObject::decode_der(&der).expect("decode signed object"); + + assert_eq!(so.content_info_content_type, OID_SIGNED_DATA); + assert_eq!(so.signed_data.version, 3); + assert_eq!(so.signed_data.digest_algorithms, vec![OID_SHA256.to_string()]); + assert_eq!( + so.signed_data.encap_content_info.econtent_type, + OID_CT_RPKI_MANIFEST + ); + assert_eq!(so.signed_data.certificates.len(), 1); + assert!(!so.signed_data.certificates[0] + .sia_signed_object_uris + .is_empty()); + assert!(so.signed_data.certificates[0] + .sia_signed_object_uris + .iter() + .any(|u| u.starts_with("rsync://"))); + assert_eq!(so.signed_data.signer_infos.len(), 1); +} diff --git a/tests/test_signed_object_decode_errors.rs b/tests/test_signed_object_decode_errors.rs new file mode 100644 index 0000000..f2e0147 --- /dev/null +++ b/tests/test_signed_object_decode_errors.rs @@ -0,0 +1,1523 @@ +use rpki::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, +}; +use rpki::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError}; +use sha2::{Digest, Sha256}; +use x509_parser::extensions::ParsedExtension; +use x509_parser::prelude::FromDer; +use x509_parser::prelude::X509Certificate; + +const TEST_NO_SIA_CERT_DER_B64: &str = "MIIDATCCAemgAwIBAgIUCyQLQJn92+gyAzvIz22q1F/97OMwDQYJKoZIhvcNAQELBQAwGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMB4XDTI2MDEyNzAzNTk1OVoXDTM2MDEyNTAzNTk1OVowGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/aoMU8J6cddkM2r6F2snd1rCdQPepgo2T2lrqWFcxnQJdcxBL1OYg3wFi95TJmZSeIHIOGauDaJ2abmjgyOUHOC4U68x66JRg4hLkmLxo1cf3uYHWl9Obph6g2qPRvN80ORq70JPuL6mAfUkNiO9hnwK6oQiTzc/rjCQGIFH8kTESBMXLfNCyUpGi+MNztYH6Ha6bKAQuXgd29OFwIkOlGQnYgGC2qBMvnp86eITvV1gTiuI8Ho9m9nZHCmaD7TylvkMDq8Hk5nkIpRcG0uO60SkR2BiMOYe/TNn5dTmHd6bsdbU2GOvgnq1SnqGq3FOWhKIe3ycUJde0uNfZOqRwIDAQABoz8wPTAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUFjyzfJCDNhFfKxVr06kjUkE23dMwDQYJKoZIhvcNAQELBQADggEBAK98n2gVlwKA3Ob1YeAm9f+8hm7pbvrt0tA8GW180CILjf09k7fKgiRlxqGdZ9ySXjU52+zCqu3MpBXVbI87ZC+zA6uK05n4y1F0n85MJ9hGR2UEiPcqou85X73LvioynnSOy/OV1PjKJXReUsqF3GgDtgcMyFssPJ9s/5DWuUCScUJY6pu0kuIGOLQ/oXUw4TvxUeyz73gOTiAJshVTQoLpHUhj0595S7lArjwi7oLI1b8m8guTknvhk0Sc3tJZmUqOcIvYIs0guHpaeC+sMoF4K+6UTrxxOBdX+fUEWNpUyYXWHjdZq25PbJdHwA/VAW2zYVojaVREligf0Qfo6F4="; +const TEST_SIA_OTHER_CERT_DER_B64: &str = "MIIDMDCCAhigAwIBAgIUEqpG+JXMZKL3bEarJ9NzqmE0mbMwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwOVGVzdCBTSUEgb3RoZXIwHhcNMjYwMTI3MDY1MzA5WhcNMzYwMTI1MDY1MzA5WjAZMRcwFQYDVQQDDA5UZXN0IFNJQSBvdGhlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANheqbavt3/tlRUrGJrZQFHzNqqmjOjnugWqkaxXwKa9WSQUs+sWFpcsKzhLW0dTLyL/6SylCNumXSbNHv92oXj0NQMzD4yXzHb78QceEk+O6Rpwtmcts03vI96kUw/xoW94+A7P4imPYOIwVqMzt0qcSxEkeYxwnN/IA+nLvlDO0Uw+0ctxDwz1/EdhsVFr8WHY5pu0w9n9R7xRPTJoke0fn0Q1ptI07aBpMIVxHMfdEuS7Mabu07LLFc2AC1XxdCbuBpqAgLPgBbFe9w2edTUFCYzCTZ3KiezrpQea0sKElC27JnuJO4ySbrMTbXv/JxlnvRcro1fRjIB0Mv14ptMCAwEAAaNwMG4wCQYDVR0TBAIwADALBgNVHQ8EBAMCB4AwHQYDVR0OBBYEFLXYTYaN8zXjQ5wqrRUbvEKSD3wcMDUGCCsGAQUFBwELBCkwJzAlBggrBgEFBQcwBYYZcnN5bmM6Ly9leGFtcGxlLm5ldC9yZXBvLzANBgkqhkiG9w0BAQsFAAOCAQEAC0/lIV63dWy/jRfx2sYtCBV6ob+wTiOazSJL8s4lpIdZgmABw3YagjTwqHMQim/xVA8ecG9q8QVZ/AXYSnxtK6zTULXhAXDBdYHEUl/slDHOfk1Nvvd8t3qm7yk8wwP74xdqVk17NY5stcIyt5kf3w75adGy0we4jfGPNAKIpcRJpQSvLfhEsecnLzPPq3F4dLFFvyMNLT3rjFrJmxPviqBdt6Dm8l6MqbexxDeNTtqhq7JutBp1arzFzGicOzvG/CEskPKNPK7mIk3jdR9zfG+lygfoZe7cQEmvH3DtoUWVXoAYNALI5b3gE0CutR8x3M8h/jgRg4IutPafWqHXtw=="; +const TEST_SIA_HTTPS_CERT_DER_B64: &str = "MIIDODCCAiCgAwIBAgIUBp2fsJYhUBJk711xTOahGbDzwN8wDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwOVGVzdCBTSUEgaHR0cHMwHhcNMjYwMTI3MDY1MzA5WhcNMzYwMTI1MDY1MzA5WjAZMRcwFQYDVQQDDA5UZXN0IFNJQSBodHRwczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOMHIMaPf4Lr5DVDs1J3wPb6XL+b9EvmuVMo6JRhZHse3+Qx9ubOywowU+3NS50tgFe2H6Xm8XpeXMMhmWyKS/Z81J/6Sa9ZRI/vYLGOZmq1ORV44EDILoxxEazstD3LSgg5w28frAmY1wo0HvCX15Mp54nEwgjmwM9Vssg47iNz859grF/V3bRKlsnTwPhiJ79yt8etpNYZHCEc+h1PUilJh81NYPKnOGOUbJPVUkCF6MnLCM4WdYjKrnaGQzTAlDht15gcEyKmMEczsmU5TJe5ToJf+72bxOeGSvRbqS8iXvi/lZS0EcGVdpOc7oh6yHhC3EcZrHmvDHEeKtav18MCAwEAAaN4MHYwCQYDVR0TBAIwADALBgNVHQ8EBAMCB4AwHQYDVR0OBBYEFHXD07FAu+OLsSH95FGd2ONTX8pGMD0GCCsGAQUFBwELBDEwLzAtBggrBgEFBQcwC4YhaHR0cHM6Ly9leGFtcGxlLm5ldC9yZXBvL3Rlc3QubWZ0MA0GCSqGSIb3DQEBCwUAA4IBAQDG7Haq/PbaDKfnd7kKkvuB9GBkJXhYK8k5qVHoPS1CnWWCWRpVjMfOni6v3Ylxa1TUYmWEdjSeQYso5xzC/vWiWG1nnt8Pn2W34R6vlx/yN9mwBMr6QK+F3IrprTQEbxWY019GFwaFcCSptWff1/YckjeTZFg+eSkuWembXO4788Opqx9d1KPJSviTODK2B0S0U5UQCeRAFD7cuwa1zrZJ/Y5kktI4lChymMAHx3N3CeJC8CvDg6VRncKDNkw0dCzLrcwX/ufQ1BbR4pmiedK9SiBqGKZJRZwUEOSYBLf5uitN/mkmZCTWnQQSySQM+QlNjNRo/+6qiBKWpKpADZlI"; +const TEST_SIA_DNS_CERT_DER_B64: &str = "MIIDHjCCAgagAwIBAgIUJlS9d2BCJaamLyjYVTjvMNzWDxMwDQYJKoZIhvcNAQELBQAwFzEVMBMGA1UEAwwMVGVzdCBTSUEgZG5zMB4XDTI2MDEyNzA2NTMwOVoXDTM2MDEyNTA2NTMwOVowFzEVMBMGA1UEAwwMVGVzdCBTSUEgZG5zMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnKIddAQzQpDLbM+ZX3qR707L7CZvZ3MDGYN+tvuPXOfhATcOLtEaxu1dK4ZhV4Ou3ZqdxwYauyC+N4An0qCJW8mVr3zhbxathVGW7w4/S9pEV/+8dGW8ypOiqNixtmV++Ww54PguD6uxMk1S3IUOVTJY+QaetMy+SV9lCbOykZys17J56tMBmHRtuOxGPnaLtzZLddWqGhGFSDthSbKX4yToUIhTUl+wIRRYjBjnbGgzH5jV6eHUgrHRk+n567jNa9fe3cuRCGNBe6ny/8NPQnJEksWpA9lGfJDlEFsDIM9cXY78izr6i4JHeErwfusJiSchTT0ePhHXRAYMQoIqywIDAQABo2IwYDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUaY5MXriJ8s5VVa+s09HoSUloKBUwJwYIKwYBBQUHAQsEGzAZMBcGCCsGAQUFBzALggtleGFtcGxlLm5ldDANBgkqhkiG9w0BAQsFAAOCAQEAZSnkWxPTFWepHaK0XvAV145idY0ztEqY9BWUql2Ythzb4rjBAU1TfDRRklnnlE9o9/I6363ltaZBvj95e3CyTu4YGflxEpHsW+4aTth8ty1ee7YSqsdJ8gN08sroIpMTfr6tvWf65cVLSTkB4yP8cnNEM3zGr37zb32ChPXgUFwS9JFf3SMsXudZ4rHougE/PM4pQZvaOl3tFEzohV5MjA2VD38n3y6bVmx3i0Xqze7UZnl06aDKozzTXmFy/DoDRGG2pd2EjoC8gNAqIOL53uRz5nJlp8WEIBMe5Hmokrzv+zkAywVZZtYo1FvonOdg5etH94oMnZEtgV/OO9joRg=="; + +fn decode_b64(b64: &str) -> Vec { + use base64::{engine::general_purpose, Engine as _}; + general_purpose::STANDARD + .decode(b64) + .expect("decode base64 cert") +} + +fn test_no_sia_cert_der() -> Vec { + decode_b64(TEST_NO_SIA_CERT_DER_B64) +} + +fn test_sia_other_cert_der() -> Vec { + decode_b64(TEST_SIA_OTHER_CERT_DER_B64) +} + +fn test_sia_https_cert_der() -> Vec { + decode_b64(TEST_SIA_HTTPS_CERT_DER_B64) +} + +fn test_sia_dns_cert_der() -> Vec { + decode_b64(TEST_SIA_DNS_CERT_DER_B64) +} + +fn extract_ski_from_cert(cert_der: &[u8]) -> Vec { + let (_rem, cert) = X509Certificate::from_der(cert_der).expect("parse cert"); + cert.extensions() + .iter() + .find(|ext| ext.oid.to_id_string() == rpki::data_model::oid::OID_SUBJECT_KEY_IDENTIFIER) + .and_then(|ext| match ext.parsed_extension() { + ParsedExtension::SubjectKeyIdentifier(ki) => Some(ki.0.to_vec()), + _ => None, + }) + .expect("cert has SKI") +} + +fn len_bytes(len: usize) -> Vec { + 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 { + let mut out = vec![tag]; + out.extend(len_bytes(content.len())); + out.extend_from_slice(content); + out +} + +fn der_integer_u64(v: u64) -> Vec { + let mut bytes = Vec::new(); + let mut n = v; + if n == 0 { + bytes.push(0); + } else { + while n > 0 { + bytes.push((n & 0xFF) as u8); + n >>= 8; + } + bytes.reverse(); + if bytes[0] & 0x80 != 0 { + bytes.insert(0, 0); + } + } + tlv(0x02, &bytes) +} + +fn der_null() -> Vec { + vec![0x05, 0x00] +} + +fn der_octet_string(bytes: &[u8]) -> Vec { + tlv(0x04, bytes) +} + +fn der_oid(oid: &str) -> Vec { + 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 { + let mut content = Vec::new(); + for c in children { + content.extend(c); + } + tlv(0x30, &content) +} + +fn der_set(mut children: Vec>) -> Vec { + // DER requires SET elements to be sorted by their encoded form. + children.sort(); + let mut content = Vec::new(); + for c in children { + content.extend(c); + } + tlv(0x31, &content) +} + +fn cs_prim(tag_no: u8, content: &[u8]) -> Vec { + tlv(0x80 | (tag_no & 0x1F), content) +} + +fn cs_cons(tag_no: u8, content: &[u8]) -> Vec { + tlv(0xA0 | (tag_no & 0x1F), content) +} + +fn algorithm_id(oid: &str, params: Option>) -> Vec { + let mut items = vec![der_oid(oid)]; + if let Some(p) = params { + items.push(p); + } + der_sequence(items) +} + +fn cms_attribute(oid: &str, value_der: Vec) -> Vec { + der_sequence(vec![der_oid(oid), der_set(vec![value_der])]) +} + +fn cms_attribute_values(oid: &str, values_der: Vec>) -> Vec { + der_sequence(vec![der_oid(oid), der_set(values_der)]) +} + +fn signed_attrs_implicit( + content_type_oid: &str, + message_digest: &[u8], + signing_time_der: Vec, + extra_attrs: Vec>, +) -> Vec { + let mut attrs = vec![ + cms_attribute(OID_CMS_ATTR_CONTENT_TYPE, der_oid(content_type_oid)), + cms_attribute(OID_CMS_ATTR_MESSAGE_DIGEST, der_octet_string(message_digest)), + cms_attribute(OID_CMS_ATTR_SIGNING_TIME, signing_time_der), + ]; + attrs.extend(extra_attrs); + // Content of SET OF Attribute (tag is IMPLICIT in SignerInfo, so we only include the SET content). + let mut set_children = attrs; + set_children.sort(); + let mut content = Vec::new(); + for a in set_children { + content.extend(a); + } + cs_cons(0, &content) +} + +fn signed_attrs_raw(mut attrs: Vec>) -> Vec { + attrs.sort(); + let mut content = Vec::new(); + for a in attrs { + content.extend(a); + } + cs_cons(0, &content) +} + +fn signer_info( + sid_ski: Vec, + digest_oid: &str, + signed_attrs: Option>, + sig_alg_oid: &str, + sig_alg_params: Option>, + include_unsigned_attrs: bool, +) -> Vec { + let mut fields = Vec::new(); + fields.push(der_integer_u64(3)); + fields.push(cs_prim(0, &sid_ski)); + fields.push(algorithm_id(digest_oid, Some(der_null()))); + if let Some(sa) = signed_attrs { + fields.push(sa); + } + fields.push(algorithm_id(sig_alg_oid, sig_alg_params)); + fields.push(der_octet_string(b"sig")); + if include_unsigned_attrs { + fields.push(cs_cons(1, &der_set(vec![der_null()]))); + } + der_sequence(fields) +} + +fn signer_info_with_version( + version: u64, + sid_ski: Vec, + digest_oid: &str, + signed_attrs: Option>, + sig_alg_oid: &str, + sig_alg_params: Option>, + include_unsigned_attrs: bool, +) -> Vec { + let mut fields = Vec::new(); + fields.push(der_integer_u64(version)); + fields.push(cs_prim(0, &sid_ski)); + fields.push(algorithm_id(digest_oid, Some(der_null()))); + if let Some(sa) = signed_attrs { + fields.push(sa); + } + fields.push(algorithm_id(sig_alg_oid, sig_alg_params)); + fields.push(der_octet_string(b"sig")); + if include_unsigned_attrs { + fields.push(cs_cons(1, &der_set(vec![der_null()]))); + } + der_sequence(fields) +} + +fn signed_data( + version: u64, + digest_alg_ids: Vec>, + econtent_type_oid: &str, + econtent_bytes: Vec, + certificates_set_content: Option>, + include_crls: bool, + signer_infos: Vec>, +) -> Vec { + let mut fields = Vec::new(); + fields.push(der_integer_u64(version)); + fields.push(der_set(digest_alg_ids)); + let encap = der_sequence(vec![ + der_oid(econtent_type_oid), + cs_cons(0, &der_octet_string(&econtent_bytes)), + ]); + fields.push(encap); + + if let Some(cert_content) = certificates_set_content { + fields.push(cs_cons(0, &cert_content)); + } + if include_crls { + fields.push(cs_cons(1, &der_null())); + } + fields.push(der_set(signer_infos)); + der_sequence(fields) +} + +fn content_info_signed_data(signed_data_der: Vec) -> Vec { + der_sequence(vec![der_oid(OID_SIGNED_DATA), cs_cons(0, &signed_data_der)]) +} + +fn load_fixture_mft_ee_cert_and_ski() -> (Vec, Vec) { + let mft = 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(&mft) + .expect("decode MFT signed object"); + let ee = &so.signed_data.certificates[0]; + (ee.raw_der.clone(), ee.subject_key_identifier.clone()) +} + +#[test] +fn trailing_bytes_after_object() { + let der = std::fs::read( + "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", + ) + .expect("read MFT fixture"); + let mut bad = der.clone(); + bad.push(0); + let err = RpkiSignedObject::decode_der(&bad).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::TrailingBytes(1))); +} + +#[test] +fn content_info_must_have_two_elements() { + let ci = der_sequence(vec![der_oid(OID_SIGNED_DATA)]); + let err = RpkiSignedObject::decode_der(&ci).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::Parse(_))); +} + +#[test] +fn content_info_content_must_be_tag0_explicit() { + let sd = signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + b"e".to_vec(), + None, + false, + vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)], + ); + let ci = der_sequence(vec![der_oid(OID_SIGNED_DATA), cs_cons(1, &sd)]); + let err = RpkiSignedObject::decode_der(&ci).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::Parse(_))); +} + +#[test] +fn content_info_content_must_contain_only_one_inner_object() { + let sd = signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + b"e".to_vec(), + None, + false, + vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)], + ); + let mut inner = sd.clone(); + inner.extend(der_null()); + let ci = der_sequence(vec![der_oid(OID_SIGNED_DATA), cs_cons(0, &inner)]); + let err = RpkiSignedObject::decode_der(&ci).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::Parse(_))); +} + +#[test] +fn signed_data_must_have_expected_element_count() { + let sd = der_sequence(vec![ + der_integer_u64(3), + der_set(vec![algorithm_id(OID_SHA256, Some(der_null()))]), + der_sequence(vec![der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST)]), + ]); + let ci = content_info_signed_data(sd); + let err = RpkiSignedObject::decode_der(&ci).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::Parse(_))); +} + +#[test] +fn signer_infos_missing_is_rejected() { + let (cert_der, cert_ski) = load_fixture_mft_ee_cert_and_ski(); + let econtent = b"payload".to_vec(); + let digest = Sha256::digest(&econtent).to_vec(); + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + &digest, + tlv(0x17, b"240101000000Z"), + vec![], + ); + let si = signer_info( + cert_ski, + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let sd = der_sequence(vec![ + der_integer_u64(3), + der_set(vec![algorithm_id(OID_SHA256, Some(der_null()))]), + der_sequence(vec![ + der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST), + cs_cons(0, &der_octet_string(&econtent)), + ]), + cs_cons(0, &cert_der), + // no signerInfos SET + ]); + let _ = si; // ensure we don't accidentally depend on `si` here + let ci = content_info_signed_data(sd); + let err = RpkiSignedObject::decode_der(&ci).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::Parse(_))); +} + +#[test] +fn certificates_field_appears_more_than_once_is_rejected() { + let (cert_der, cert_ski) = load_fixture_mft_ee_cert_and_ski(); + let econtent = b"payload".to_vec(); + let digest = Sha256::digest(&econtent).to_vec(); + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + &digest, + tlv(0x17, b"240101000000Z"), + vec![], + ); + let si = signer_info( + cert_ski, + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let sd = der_sequence(vec![ + der_integer_u64(3), + der_set(vec![algorithm_id(OID_SHA256, Some(der_null()))]), + der_sequence(vec![ + der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST), + cs_cons(0, &der_octet_string(&econtent)), + ]), + cs_cons(0, &cert_der), + cs_cons(0, &cert_der), + der_set(vec![si]), + ]); + let ci = content_info_signed_data(sd); + let err = RpkiSignedObject::decode_der(&ci).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::Parse(_))); +} + +#[test] +fn signed_data_unexpected_field_is_rejected() { + let sd = der_sequence(vec![ + der_integer_u64(3), + der_set(vec![algorithm_id(OID_SHA256, Some(der_null()))]), + der_sequence(vec![ + der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST), + cs_cons(0, &der_octet_string(b"e")), + ]), + der_null(), + der_set(vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)]), + ]); + let ci = content_info_signed_data(sd); + let err = RpkiSignedObject::decode_der(&ci).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::Parse(_))); +} + +#[test] +fn encap_content_info_length_and_tags_are_validated() { + // EncapsulatedContentInfo with 3 elements. + let encap = der_sequence(vec![ + der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST), + cs_cons(0, &der_octet_string(b"e")), + der_null(), + ]); + let sd = der_sequence(vec![ + der_integer_u64(3), + der_set(vec![algorithm_id(OID_SHA256, Some(der_null()))]), + encap, + der_set(vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)]), + ]); + let ci = content_info_signed_data(sd); + let err = RpkiSignedObject::decode_der(&ci).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::Parse(_))); + + // eContent wrong tag number. + let encap = der_sequence(vec![ + der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST), + cs_cons(1, &der_octet_string(b"e")), + ]); + let sd = der_sequence(vec![ + der_integer_u64(3), + der_set(vec![algorithm_id(OID_SHA256, Some(der_null()))]), + encap, + der_set(vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)]), + ]); + let ci = content_info_signed_data(sd); + let err = RpkiSignedObject::decode_der(&ci).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::Parse(_))); + + // eContent contains trailing bytes inside the explicit wrapper. + let mut inner = der_octet_string(b"e"); + inner.push(0); + let encap = der_sequence(vec![ + der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST), + cs_cons(0, &inner), + ]); + let sd = der_sequence(vec![ + der_integer_u64(3), + der_set(vec![algorithm_id(OID_SHA256, Some(der_null()))]), + encap, + der_set(vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)]), + ]); + let ci = content_info_signed_data(sd); + let err = RpkiSignedObject::decode_der(&ci).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::Parse(_))); +} + +#[test] +fn empty_econtent_is_rejected() { + let (cert_der, cert_ski) = load_fixture_mft_ee_cert_and_ski(); + let econtent = Vec::new(); + let digest = Sha256::digest(&econtent).to_vec(); + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + &digest, + tlv(0x17, b"240101000000Z"), + vec![], + ); + let si = signer_info( + cert_ski, + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent, + Some(cert_der), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::EContentMissing)); +} + +#[test] +fn signer_info_sequence_len_and_version_are_validated() { + let (cert_der, cert_ski) = load_fixture_mft_ee_cert_and_ski(); + let econtent = b"payload".to_vec(); + let digest = Sha256::digest(&econtent).to_vec(); + + // Too-short SignerInfo SEQUENCE. + let si_short = der_sequence(vec![ + der_integer_u64(3), + cs_prim(0, &cert_ski), + algorithm_id(OID_SHA256, Some(der_null())), + algorithm_id(OID_RSA_ENCRYPTION, Some(der_null())), + ]); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent.clone(), + Some(cert_der.clone()), + false, + vec![si_short], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::Parse(_))); + + // Invalid SignerInfo.version. + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + &digest, + tlv(0x17, b"240101000000Z"), + vec![], + ); + let si = signer_info_with_version( + 1, + cert_ski, + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent, + Some(cert_der), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::InvalidSignerInfoVersion(1))); +} + +#[test] +fn signed_attrs_structure_and_presence_are_validated() { + let (cert_der, cert_ski) = load_fixture_mft_ee_cert_and_ski(); + let econtent = b"payload".to_vec(); + let digest = Sha256::digest(&econtent).to_vec(); + + // Attribute SEQUENCE must have 2 elements. + let bad_attr = der_sequence(vec![der_oid(OID_CMS_ATTR_CONTENT_TYPE)]); + let signed_attrs = signed_attrs_raw(vec![bad_attr]); + let si = signer_info( + cert_ski.clone(), + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent.clone(), + Some(cert_der.clone()), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::Parse(_))); + + // Missing content-type. + let signed_attrs = signed_attrs_raw(vec![ + cms_attribute(OID_CMS_ATTR_MESSAGE_DIGEST, der_octet_string(&digest)), + cms_attribute(OID_CMS_ATTR_SIGNING_TIME, tlv(0x17, b"240101000000Z")), + ]); + let si = signer_info( + cert_ski.clone(), + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent.clone(), + Some(cert_der.clone()), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::Parse(_))); + + // Missing message-digest. + let signed_attrs = signed_attrs_raw(vec![ + cms_attribute(OID_CMS_ATTR_CONTENT_TYPE, der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST)), + cms_attribute(OID_CMS_ATTR_SIGNING_TIME, tlv(0x17, b"240101000000Z")), + ]); + let si = signer_info( + cert_ski.clone(), + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent.clone(), + Some(cert_der.clone()), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::Parse(_))); + + // Missing signing-time. + let signed_attrs = signed_attrs_raw(vec![ + cms_attribute(OID_CMS_ATTR_CONTENT_TYPE, der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST)), + cms_attribute(OID_CMS_ATTR_MESSAGE_DIGEST, der_octet_string(&digest)), + ]); + let si = signer_info( + cert_ski, + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent, + Some(cert_der), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::Parse(_))); +} + +#[test] +fn algorithm_identifier_sequence_shape_is_validated() { + let sd = signed_data( + 3, + vec![der_sequence(vec![])], // invalid AlgorithmIdentifier + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + b"e".to_vec(), + None, + false, + vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)], + ); + let ci = content_info_signed_data(sd); + let err = RpkiSignedObject::decode_der(&ci).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::Parse(_))); +} + +#[test] +fn ee_certificate_missing_signed_object_sia_is_rejected() { + let cert_der = test_no_sia_cert_der(); + let cert_ski = extract_ski_from_cert(&cert_der); + + let econtent = b"payload".to_vec(); + let digest = Sha256::digest(&econtent).to_vec(); + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + &digest, + tlv(0x17, b"240101000000Z"), + vec![], + ); + let si = signer_info( + cert_ski, + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent, + Some(cert_der), + false, + vec![si], + )); + + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!( + err, + SignedObjectDecodeError::EeCertificateMissingSia + | SignedObjectDecodeError::EeCertificateMissingSignedObjectSia + )); +} + +#[test] +fn ee_certificate_sia_without_signed_object_access_method_is_rejected() { + let cert_der = test_sia_other_cert_der(); + let cert_ski = extract_ski_from_cert(&cert_der); + + let econtent = b"payload".to_vec(); + let digest = Sha256::digest(&econtent).to_vec(); + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + &digest, + tlv(0x17, b"240101000000Z"), + vec![], + ); + let si = signer_info( + cert_ski, + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent, + Some(cert_der), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!( + err, + SignedObjectDecodeError::EeCertificateMissingSignedObjectSia + )); +} + +#[test] +fn ee_certificate_signed_object_sia_must_be_uri() { + let cert_der = test_sia_dns_cert_der(); + let cert_ski = extract_ski_from_cert(&cert_der); + + let econtent = b"payload".to_vec(); + let digest = Sha256::digest(&econtent).to_vec(); + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + &digest, + tlv(0x17, b"240101000000Z"), + vec![], + ); + let si = signer_info( + cert_ski, + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent, + Some(cert_der), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!( + err, + SignedObjectDecodeError::EeCertificateSignedObjectSiaNotUri + )); +} + +#[test] +fn ee_certificate_signed_object_sia_requires_rsync_uri() { + let cert_der = test_sia_https_cert_der(); + let cert_ski = extract_ski_from_cert(&cert_der); + + let econtent = b"payload".to_vec(); + let digest = Sha256::digest(&econtent).to_vec(); + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + &digest, + tlv(0x17, b"240101000000Z"), + vec![], + ); + let si = signer_info( + cert_ski, + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent, + Some(cert_der), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!( + err, + SignedObjectDecodeError::EeCertificateSignedObjectSiaNoRsync + )); +} + +#[test] +fn signed_attrs_duplicate_content_type_is_rejected() { + let (cert_der, cert_ski) = load_fixture_mft_ee_cert_and_ski(); + let econtent = b"payload".to_vec(); + let digest = Sha256::digest(&econtent).to_vec(); + + let signed_attrs = signed_attrs_raw(vec![ + cms_attribute( + OID_CMS_ATTR_CONTENT_TYPE, + der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST), + ), + cms_attribute( + OID_CMS_ATTR_CONTENT_TYPE, + der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST), + ), + cms_attribute(OID_CMS_ATTR_MESSAGE_DIGEST, der_octet_string(&digest)), + cms_attribute(OID_CMS_ATTR_SIGNING_TIME, tlv(0x17, b"240101000000Z")), + ]); + let si = signer_info( + cert_ski, + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent, + Some(cert_der), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!( + err, + SignedObjectDecodeError::DuplicateSignedAttribute(ref oid) + if oid == OID_CMS_ATTR_CONTENT_TYPE + )); +} + +#[test] +fn signed_attrs_duplicate_signing_time_is_rejected() { + let (cert_der, cert_ski) = load_fixture_mft_ee_cert_and_ski(); + let econtent = b"payload".to_vec(); + let digest = Sha256::digest(&econtent).to_vec(); + + let signed_attrs = signed_attrs_raw(vec![ + cms_attribute( + OID_CMS_ATTR_CONTENT_TYPE, + der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST), + ), + cms_attribute(OID_CMS_ATTR_MESSAGE_DIGEST, der_octet_string(&digest)), + cms_attribute(OID_CMS_ATTR_SIGNING_TIME, tlv(0x17, b"240101000000Z")), + cms_attribute(OID_CMS_ATTR_SIGNING_TIME, tlv(0x17, b"240101000000Z")), + ]); + let si = signer_info( + cert_ski, + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent, + Some(cert_der), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!( + err, + SignedObjectDecodeError::DuplicateSignedAttribute(ref oid) + if oid == OID_CMS_ATTR_SIGNING_TIME + )); +} + +#[test] +fn invalid_content_info_content_type() { + let sd = der_sequence(vec![der_integer_u64(3), der_set(vec![]), der_sequence(vec![der_oid("1.2.3")]) , der_set(vec![])]); + let ci = der_sequence(vec![der_oid("1.2.3.4"), cs_cons(0, &sd)]); + let err = RpkiSignedObject::decode_der(&ci).unwrap_err(); + assert!(matches!( + err, + SignedObjectDecodeError::InvalidContentInfoContentType(_) + )); +} + +#[test] +fn invalid_signed_data_version() { + let so = content_info_signed_data(signed_data( + 4, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + b"e".to_vec(), + None, + false, + vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!( + err, + SignedObjectDecodeError::InvalidSignedDataVersion(4) + )); +} + +#[test] +fn invalid_digest_algorithms_count() { + let so = content_info_signed_data(signed_data( + 3, + vec![ + algorithm_id(OID_SHA256, Some(der_null())), + algorithm_id(OID_SHA256, Some(der_null())), + ], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + b"e".to_vec(), + None, + false, + vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!( + err, + SignedObjectDecodeError::InvalidDigestAlgorithmsCount(2) + )); +} + +#[test] +fn invalid_digest_algorithm_oid() { + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id("1.2.3.4", Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + b"e".to_vec(), + None, + false, + vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!( + err, + SignedObjectDecodeError::InvalidDigestAlgorithm(_) + )); +} + +#[test] +fn econtent_missing() { + let encap = der_sequence(vec![der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST)]); + let sd = der_sequence(vec![ + der_integer_u64(3), + der_set(vec![algorithm_id(OID_SHA256, Some(der_null()))]), + encap, + der_set(vec![]), + ]); + let so = der_sequence(vec![der_oid(OID_SIGNED_DATA), cs_cons(0, &sd)]); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::EContentMissing)); +} + +#[test] +fn crls_present_is_rejected() { + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + b"e".to_vec(), + None, + true, + vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::CrlsPresent)); +} + +#[test] +fn certificates_missing_is_rejected() { + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + b"e".to_vec(), + None, + false, + vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::CertificatesMissing)); +} + +#[test] +fn invalid_certificates_count_rejected() { + let (cert_der, cert_ski) = load_fixture_mft_ee_cert_and_ski(); + let econtent = b"payload".to_vec(); + let digest = Sha256::digest(&econtent).to_vec(); + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + &digest, + tlv(0x17, b"240101000000Z"), + vec![], + ); + let si = signer_info( + cert_ski, + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let mut certs = cert_der.clone(); + certs.extend_from_slice(&cert_der); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent, + Some(certs), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!( + err, + SignedObjectDecodeError::InvalidCertificatesCount(2) + )); +} + +#[test] +fn ee_certificate_parse_error_is_reported() { + let econtent = b"payload".to_vec(); + let digest = Sha256::digest(&econtent).to_vec(); + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + &digest, + tlv(0x17, b"240101000000Z"), + vec![], + ); + let si = signer_info( + vec![0x01, 0x02], + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + // CertificateSet contains a DER object, but it's not a certificate. + let certs = der_null(); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent, + Some(certs), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::EeCertificateParse(_))); +} + +#[test] +fn invalid_signer_infos_count_rejected() { + let (cert_der, cert_ski) = load_fixture_mft_ee_cert_and_ski(); + let econtent = b"payload".to_vec(); + let digest = Sha256::digest(&econtent).to_vec(); + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + &digest, + tlv(0x17, b"240101000000Z"), + vec![], + ); + let si1 = signer_info( + cert_ski.clone(), + OID_SHA256, + Some(signed_attrs.clone()), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let si2 = signer_info( + cert_ski, + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent, + Some(cert_der), + false, + vec![si1, si2], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!( + err, + SignedObjectDecodeError::InvalidSignerInfosCount(2) + )); +} + +#[test] +fn signer_info_errors_are_detected() { + let (cert_der, cert_ski) = load_fixture_mft_ee_cert_and_ski(); + let econtent = b"payload".to_vec(); + let digest = Sha256::digest(&econtent).to_vec(); + + // Missing signedAttrs. + let si_no_attrs = signer_info( + cert_ski.clone(), + OID_SHA256, + None, + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent.clone(), + Some(cert_der.clone()), + false, + vec![si_no_attrs], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::SignedAttrsMissing)); + + // Invalid signer identifier. + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + &digest, + tlv(0x17, b"240101000000Z"), + vec![], + ); + let si = der_sequence(vec![ + der_integer_u64(3), + der_null(), // not [0] SKI + algorithm_id(OID_SHA256, Some(der_null())), + signed_attrs, + algorithm_id(OID_RSA_ENCRYPTION, Some(der_null())), + der_octet_string(b"sig"), + ]); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent.clone(), + Some(cert_der.clone()), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::InvalidSignerIdentifier)); + + // Invalid digest algorithm. + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + &digest, + tlv(0x17, b"240101000000Z"), + vec![], + ); + let si = signer_info( + cert_ski.clone(), + "1.2.3.4", + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent.clone(), + Some(cert_der.clone()), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!( + err, + SignedObjectDecodeError::InvalidSignerInfoDigestAlgorithm(_) + )); + + // Invalid signature algorithm OID. + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + &digest, + tlv(0x17, b"240101000000Z"), + vec![], + ); + let si = signer_info( + cert_ski.clone(), + OID_SHA256, + Some(signed_attrs), + "1.2.3.4", + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent.clone(), + Some(cert_der.clone()), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!( + err, + SignedObjectDecodeError::InvalidSignatureAlgorithm(_) + )); + + // Invalid signing-time value. + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + &digest, + der_null(), + vec![], + ); + let si = signer_info( + cert_ski.clone(), + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent.clone(), + Some(cert_der.clone()), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!( + err, + SignedObjectDecodeError::InvalidSigningTimeValue + )); + + // signedAttrs has duplicate attribute. + let dup = cms_attribute(OID_CMS_ATTR_MESSAGE_DIGEST, der_octet_string(&digest)); + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + &digest, + tlv(0x17, b"240101000000Z"), + vec![dup], + ); + let si = signer_info( + cert_ski.clone(), + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent.clone(), + Some(cert_der.clone()), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!( + err, + SignedObjectDecodeError::DuplicateSignedAttribute(_) + )); + + // signedAttrs attrValues count != 1. + let bad_values = cms_attribute_values( + OID_CMS_ATTR_CONTENT_TYPE, + vec![ + der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST), + der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST), + ], + ); + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + &digest, + tlv(0x17, b"240101000000Z"), + vec![bad_values], + ); + let si = signer_info( + cert_ski.clone(), + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent.clone(), + Some(cert_der.clone()), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!( + err, + SignedObjectDecodeError::InvalidSignedAttributeValuesCount { .. } + )); + + // content-type mismatch. + let signed_attrs = signed_attrs_implicit( + "1.2.3.4", + &digest, + tlv(0x17, b"240101000000Z"), + vec![], + ); + let si = signer_info( + cert_ski.clone(), + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent.clone(), + Some(cert_der.clone()), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!( + err, + SignedObjectDecodeError::ContentTypeAttrMismatch { .. } + )); + + // message-digest mismatch. + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + b"wrong", + tlv(0x17, b"240101000000Z"), + vec![], + ); + let si = signer_info( + cert_ski.clone(), + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent.clone(), + Some(cert_der.clone()), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::MessageDigestMismatch)); + + // sid_ski mismatch. + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + &digest, + tlv(0x17, b"240101000000Z"), + vec![], + ); + let si = signer_info( + vec![0u8; 20], + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent.clone(), + Some(cert_der.clone()), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::SidSkiMismatch)); + + // Unsupported signedAttrs attribute. + let bad_attr = cms_attribute("1.2.3.4", der_null()); + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + &digest, + tlv(0x17, b"240101000000Z"), + vec![bad_attr], + ); + let si = signer_info( + cert_ski.clone(), + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent.clone(), + Some(cert_der.clone()), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!( + err, + SignedObjectDecodeError::UnsupportedSignedAttribute(_) + )); + + // signatureAlgorithm parameters invalid. + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + &digest, + tlv(0x18, b"20500101000000Z"), + vec![], + ); + let si = signer_info( + cert_ski.clone(), + OID_SHA256, + Some(signed_attrs), + OID_RSA_ENCRYPTION, + Some(der_integer_u64(1)), + false, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent.clone(), + Some(cert_der.clone()), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!( + err, + SignedObjectDecodeError::InvalidSignatureAlgorithmParameters + )); + + // unsignedAttrs present. + let signed_attrs = signed_attrs_implicit( + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + &digest, + tlv(0x17, b"240101000000Z"), + vec![], + ); + let si = signer_info( + cert_ski, + OID_SHA256, + Some(signed_attrs), + OID_SHA256_WITH_RSA_ENCRYPTION, + Some(der_null()), + true, + ); + let so = content_info_signed_data(signed_data( + 3, + vec![algorithm_id(OID_SHA256, Some(der_null()))], + rpki::data_model::oid::OID_CT_RPKI_MANIFEST, + econtent, + Some(cert_der), + false, + vec![si], + )); + let err = RpkiSignedObject::decode_der(&so).unwrap_err(); + assert!(matches!(err, SignedObjectDecodeError::UnsignedAttrsPresent)); +} diff --git a/tests/test_signed_object_verify.rs b/tests/test_signed_object_verify.rs new file mode 100644 index 0000000..b947d21 --- /dev/null +++ b/tests/test_signed_object_verify.rs @@ -0,0 +1,164 @@ +use rpki::data_model::signed_object::{RpkiSignedObject, SignedObjectVerifyError}; +use x509_parser::prelude::FromDer; +use x509_parser::prelude::X509Certificate; + +fn len_bytes(len: usize) -> Vec { + 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 { + let mut out = vec![tag]; + out.extend(len_bytes(content.len())); + out.extend_from_slice(content); + out +} + +fn der_integer_bytes(bytes: &[u8]) -> Vec { + tlv(0x02, bytes) +} + +fn der_integer_u64(v: u64) -> Vec { + 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 { + vec![0x05, 0x00] +} + +fn der_oid(oid: &str) -> Vec { + 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 { + let mut content = Vec::new(); + for c in children { + content.extend(c); + } + tlv(0x30, &content) +} + +fn der_bit_string(unused: u8, bytes: &[u8]) -> Vec { + 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 { + // 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)); +}