Harden first-release validation and dependency hygiene
This commit is contained in:
parent
277cbca878
commit
591bed90a8
@ -4,6 +4,7 @@ target
|
|||||||
out
|
out
|
||||||
state
|
state
|
||||||
tests
|
tests
|
||||||
|
tools
|
||||||
docs
|
docs
|
||||||
!docs/third-party-licenses.txt
|
!docs/third-party-licenses.txt
|
||||||
docker
|
docker
|
||||||
|
|||||||
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
@ -7,26 +7,46 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
rust:
|
rust:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
- uses: dtolnay/rust-toolchain@6d653ac52d5f748757700b3b0ec16abf11083d92
|
- uses: dtolnay/rust-toolchain@6d653ac52d5f748757700b3b0ec16abf11083d92
|
||||||
with:
|
with:
|
||||||
toolchain: 1.92.0
|
toolchain: 1.92.0
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
- run: sudo apt-get update && sudo apt-get install -y --no-install-recommends build-essential clang libclang-dev pkg-config python3-cryptography openssl
|
- run: sudo apt-get update && sudo apt-get install -y --no-install-recommends build-essential clang libclang-dev pkg-config python3-cryptography openssl ripgrep curl rsync time
|
||||||
- run: cargo fmt --all --check
|
- run: python3 tools/check_format.py
|
||||||
|
- run: python3 -m unittest discover -s tools -p 'test_*.py'
|
||||||
- run: cargo check --locked --no-default-features
|
- run: cargo check --locked --no-default-features
|
||||||
- run: cargo test --locked
|
- run: cargo test --locked
|
||||||
- run: cargo clippy --locked --all-targets -- -D warnings
|
- run: cargo clippy --locked --all-targets -- -D warnings
|
||||||
- run: cargo build --locked --release
|
- run: cargo build --locked --release
|
||||||
|
- run: python3 tools/dependency_notices.py --check
|
||||||
docker:
|
docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
- run: sudo apt-get update && sudo apt-get install -y --no-install-recommends python3-cryptography openssl
|
- run: sudo apt-get update && sudo apt-get install -y --no-install-recommends python3-cryptography openssl ripgrep
|
||||||
- run: docker build -f docker/Dockerfile -t panda-rpki:ci .
|
- run: docker build -f docker/Dockerfile -t panda-rpki:ci .
|
||||||
- run: docker run --rm panda-rpki:ci --help
|
- run: docker run --rm panda-rpki:ci --help
|
||||||
- run: bash tests/synthetic_docker_e2e.sh
|
- run: bash tests/synthetic_docker_e2e.sh
|
||||||
|
env:
|
||||||
|
PANDA_RPKI_TEST_IMAGE: panda-rpki:ci
|
||||||
|
audit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
|
- uses: dtolnay/rust-toolchain@6d653ac52d5f748757700b3b0ec16abf11083d92
|
||||||
|
with:
|
||||||
|
toolchain: 1.92.0
|
||||||
|
- run: cargo install cargo-audit --locked --version 0.22.0
|
||||||
|
- run: cargo audit --json
|
||||||
|
|||||||
@ -12,3 +12,7 @@ Documentation and code comments use English. Do not commit generated repository
|
|||||||
objects, keys, state, logs or build outputs. Keep third-party attribution intact.
|
objects, keys, state, logs or build outputs. Keep third-party attribution intact.
|
||||||
Submit only code you are entitled to contribute under the [project license](LICENSE)
|
Submit only code you are entitled to contribute under the [project license](LICENSE)
|
||||||
and identify externally sourced code and its license in the pull request.
|
and identify externally sourced code and its license in the pull request.
|
||||||
|
|
||||||
|
Use the [security policy](SECURITY.md) for suspected vulnerabilities, not public
|
||||||
|
issues. Changes to Cargo.lock must update and check dependency notices using
|
||||||
|
the [dependency maintenance workflow](docs/dependencies.md).
|
||||||
|
|||||||
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -1054,9 +1054,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.37.5"
|
version = "0.41.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
|
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -25,7 +25,7 @@ toml = "0.8.20"
|
|||||||
rocksdb = { version = "0.22.0", default-features = false, features = ["lz4"] }
|
rocksdb = { version = "0.22.0", default-features = false, features = ["lz4"] }
|
||||||
serde_cbor = "0.11.2"
|
serde_cbor = "0.11.2"
|
||||||
roxmltree = "0.20.0"
|
roxmltree = "0.20.0"
|
||||||
quick-xml = "0.37.2"
|
quick-xml = "0.41.0"
|
||||||
uuid = { version = "1.7.0", features = ["v4"] }
|
uuid = { version = "1.7.0", features = ["v4"] }
|
||||||
reqwest = { version = "0.12.12", default-features = false, features = ["blocking", "rustls-tls", "gzip", "brotli", "deflate"] }
|
reqwest = { version = "0.12.12", default-features = false, features = ["blocking", "rustls-tls", "gzip", "brotli", "deflate"] }
|
||||||
tempfile = "3.16.0"
|
tempfile = "3.16.0"
|
||||||
|
|||||||
@ -4,6 +4,10 @@ Panda RPKI is an RPKI relying party written in Rust. It synchronizes repositorie
|
|||||||
over RRDP, validates signed objects, and exports routing data as CSV and Canonical
|
over RRDP, validates signed objects, and exports routing data as CSV and Canonical
|
||||||
Cache Representation (CCR). Run a single cycle or use the daemon for continuous operation.
|
Cache Representation (CCR). Run a single cycle or use the daemon for continuous operation.
|
||||||
|
|
||||||
|
Version 0.1.0 is an unreleased candidate, validated on Linux x86-64. Other
|
||||||
|
platforms are not yet verified. Review the [standards and limitations](docs/conformance-matrix.md)
|
||||||
|
and [security policy](SECURITY.md) before evaluating it for routing use.
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
Requirements: Linux, Docker, and a TAL with its matching DER trust-anchor
|
Requirements: Linux, Docker, and a TAL with its matching DER trust-anchor
|
||||||
@ -52,6 +56,7 @@ matching TA certificates. No RTR server is included. See the
|
|||||||
- [Docker and Compose](docs/docker.md): single-anchor, all-five and daemon deployments.
|
- [Docker and Compose](docs/docker.md): single-anchor, all-five and daemon deployments.
|
||||||
- [Development and testing](docs/development.md).
|
- [Development and testing](docs/development.md).
|
||||||
- [Contributing](CONTRIBUTING.md) and [changelog](CHANGELOG.md).
|
- [Contributing](CONTRIBUTING.md) and [changelog](CHANGELOG.md).
|
||||||
|
- [Release checklist](docs/releasing.md).
|
||||||
|
|
||||||
## Build from source
|
## Build from source
|
||||||
|
|
||||||
|
|||||||
19
SECURITY.md
Normal file
19
SECURITY.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Security policy
|
||||||
|
|
||||||
|
## Reporting vulnerabilities
|
||||||
|
|
||||||
|
Do not report suspected vulnerabilities, exploit inputs, or sensitive logs in
|
||||||
|
public issues or pull requests. The planned reporting channel is GitHub Private
|
||||||
|
Vulnerability Reporting on the official public repository.
|
||||||
|
|
||||||
|
That repository and reporting channel are not available yet. Public release is
|
||||||
|
blocked until maintainers configure and test the private reporting channel and
|
||||||
|
replace this paragraph with its actual link. No response-time commitment is
|
||||||
|
currently offered. Do not assume that a public issue is a private channel.
|
||||||
|
|
||||||
|
## Supported versions
|
||||||
|
|
||||||
|
Version 0.1.0 is an unreleased candidate. There is no supported production
|
||||||
|
release yet. Dependency scans and regression tests are release checks, not a
|
||||||
|
guarantee that the validator is free of vulnerabilities. Validate suitability
|
||||||
|
for your deployment before using its results for routing decisions.
|
||||||
@ -126,7 +126,7 @@ No production trust-anchor or repository data is included in this source tree.
|
|||||||
| potential_utf | 0.1.6 | Unicode-3.0 |
|
| potential_utf | 0.1.6 | Unicode-3.0 |
|
||||||
| powerfmt | 0.2.0 | MIT OR Apache-2.0 |
|
| powerfmt | 0.2.0 | MIT OR Apache-2.0 |
|
||||||
| proc-macro2 | 1.0.107 | MIT OR Apache-2.0 |
|
| proc-macro2 | 1.0.107 | MIT OR Apache-2.0 |
|
||||||
| quick-xml | 0.37.5 | MIT |
|
| quick-xml | 0.41.0 | MIT |
|
||||||
| quinn | 0.11.11 | MIT OR Apache-2.0 |
|
| quinn | 0.11.11 | MIT OR Apache-2.0 |
|
||||||
| quinn-proto | 0.11.17 | MIT OR Apache-2.0 |
|
| quinn-proto | 0.11.17 | MIT OR Apache-2.0 |
|
||||||
| quinn-udp | 0.5.15 | MIT OR Apache-2.0 |
|
| quinn-udp | 0.5.15 | MIT OR Apache-2.0 |
|
||||||
|
|||||||
@ -4,16 +4,28 @@
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| TAL syntax and TA public-key binding | RFC 8630 §2 | `src/validation/from_tal.rs`, `src/model/ta.rs` | parser and synthetic integration tests |
|
| TAL syntax and TA public-key binding | RFC 8630 §2 | `src/validation/from_tal.rs`, `src/model/ta.rs` | parser and synthetic integration tests |
|
||||||
| TA and issued CA profile/signature/resource checks | RFC 6487 §4 | `src/validation/`, `src/model/` | profile unit tests and synthetic integration tests |
|
| TA and issued CA profile/signature/resource checks | RFC 6487 §4 | `src/validation/`, `src/model/` | profile unit tests and synthetic integration tests |
|
||||||
| Manifest, manifest file hashes, CRL freshness, and ROA checks | RFC 6486, RFC 6487, RFC 6482 | `src/validation/`, `src/model/` | RRDP unit tests and self-contained `tests/synthetic_docker_e2e.sh` |
|
| Manifest profile, file hashes, and freshness checks | RFC 9286 §§4, 6 | `src/model/manifest.rs`, `src/validation/manifest.rs` | manifest profile and validation unit tests |
|
||||||
|
| CRL profile and freshness checks | RFC 6487 §5 | `src/model/crl.rs`, `src/validation/` | CRL profile and validation tests |
|
||||||
|
| ROA profile and payload checks | RFC 9582 §§3–4 | `src/model/roa.rs`, `src/validation/` | ROA profile tests and synthetic CSV assertions |
|
||||||
|
| ASPA profile and provider-list checks | `draft-ietf-sidrops-aspa-profile-21` §§2–4 | `src/model/aspa.rs`, `src/validation/` | ASPA profile tests and synthetic VAP assertions |
|
||||||
|
| BGPsec router-certificate profile checks | RFC 8209 §3 | `src/model/router_cert.rs`, `src/validation/` | router-certificate profile tests |
|
||||||
|
| CMS signed-object profile | RFC 6488 §§2–3; RFC 9589 §4 | `src/model/signed_object.rs` | signed-object profile tests |
|
||||||
| RRDP notification, snapshot, delta/replace/withdraw and fallback | RFC 8182 §§3.4–3.5 | `src/repository/sync/rrdp.rs`, `src/repository/sync/repo.rs` | snapshot/delta state-restart unit tests and synthetic Docker tests |
|
| RRDP notification, snapshot, delta/replace/withdraw and fallback | RFC 8182 §§3.4–3.5 | `src/repository/sync/rrdp.rs`, `src/repository/sync/repo.rs` | snapshot/delta state-restart unit tests and synthetic Docker tests |
|
||||||
| RRDP direct-reference and redirect origin checks | RFC 9674 §3.2 | `src/repository/fetch/http.rs`, `src/repository/sync/rrdp.rs` | cross-origin unit tests |
|
| RRDP direct-reference and redirect origin checks | RFC 9674 §3.2 | `src/repository/fetch/http.rs`, `src/repository/sync/rrdp.rs` | cross-origin unit tests |
|
||||||
| Repeated TAL/TA pairing and stable aggregate output | RFC 8630 §2; RFC 6813 data model | `src/cli/mod.rs`, `src/cli/validate.rs`, `src/runtime/` | CLI parser checks; locked multi-TAL E2E |
|
| Repeated TAL/TA pairing and aggregate output | RFC 8630 §2 for each TAL; aggregation is an implementation choice | `src/cli/mod.rs`, `src/cli/validate.rs`, `src/runtime/` | CLI parser checks and multi-TAL integration evidence |
|
||||||
| Bounded repository and object workers (default 8 each, shared across TALs) | operational support for the standard validation pipeline | `src/scheduler/`, `src/runtime/` | worker-pool unit tests |
|
| Bounded repository and object workers (default 8 each, shared across TALs) | operational support for the standard validation pipeline | `src/scheduler/`, `src/runtime/` | worker-pool unit tests |
|
||||||
| Resumable RRDP protocol state | RFC 8182 §3.4 | `src/repository/storage/`, `src/repository/sync/rrdp.rs` | atomic state index and restart delta test |
|
| Resumable RRDP protocol state | RFC 8182 §3.4 | `src/repository/storage/`, `src/repository/sync/rrdp.rs` | atomic state index and restart delta test |
|
||||||
| CCR manifest, ROA, ASPA, trust-anchor and router-key states | `draft-ietf-sidrops-rpki-ccr-11` §§2–4 | `src/ccr/` | encode/decode tests and integration artifacts |
|
| CCR manifest, ROA, ASPA, trust-anchor and router-key states | `draft-ietf-sidrops-rpki-ccr-11` §§2–4 | `src/ccr/` | encode/decode tests and integration artifacts |
|
||||||
| TA Constraints rule normalization and EE resource checks | `draft-ietf-sidrops-constraining-rpki-trust-anchors-01` §§3–4 | `src/ta_constraints.rs`, `src/validation/` | parser/normalization tests; constrained validation E2E |
|
| TA Constraints rule normalization and EE resource checks | `draft-ietf-sidrops-constraining-rpki-trust-anchors-01` §§3–4 | `src/ta_constraints.rs`, `src/validation/` | parser/normalization tests; constrained validation E2E |
|
||||||
| VRP CSV output | RFC 6810 data semantics | `src/cli/validate.rs`, `src/runtime/` | self-contained Docker E2E CSV/summary assertions |
|
| VRP CSV output | RFC 6810 data semantics | `src/cli/validate.rs`, `src/runtime/` | self-contained Docker E2E CSV/summary assertions |
|
||||||
|
|
||||||
|
References identify the implemented profiles, not a certification of complete
|
||||||
|
RFC compliance. Draft revisions are intentionally explicit and are not claims
|
||||||
|
of compatibility with later revisions. The CLI uses HTTPS RRDP, requires local
|
||||||
|
TAL/TA pairs, and does not provide RTR or automatic TA-certificate refresh.
|
||||||
|
CCR tests establish encoding and payload consistency, not independent
|
||||||
|
revalidation of the entire trust chain by another relying party.
|
||||||
|
|
||||||
The worker pool and interval index are implementation mechanisms, not new wire
|
The worker pool and interval index are implementation mechanisms, not new wire
|
||||||
protocols; their tests must also prove deterministic output and unchanged
|
protocols; their tests must also prove deterministic output and unchanged
|
||||||
validation decisions. Only the table entries above are in v0.1.0 scope.
|
validation decisions. Only the table entries above are in v0.1.0 scope.
|
||||||
|
|||||||
56
docs/dependencies.md
Normal file
56
docs/dependencies.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Dependency maintenance
|
||||||
|
|
||||||
|
Use the committed Cargo.lock. Do not run broad dependency updates as part of
|
||||||
|
unrelated changes. Review runtime compatibility and licensing for each update.
|
||||||
|
|
||||||
|
## Security audit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo install cargo-audit --locked --version 0.22.0
|
||||||
|
cargo audit --json
|
||||||
|
```
|
||||||
|
|
||||||
|
The audit fetches the RustSec advisory database and needs network access. Record
|
||||||
|
the tool version and database revision with release evidence. Database download
|
||||||
|
failure is not a clean scan. Review informational warnings as well as errors;
|
||||||
|
unresolved affected vulnerabilities block release. Cargo auditing does not scan
|
||||||
|
Debian packages or fully establish the security of native bundled code.
|
||||||
|
|
||||||
|
### Reviewed maintenance warning
|
||||||
|
|
||||||
|
`serde_cbor 0.11.2` has the informational unmaintained advisory
|
||||||
|
[RUSTSEC-2021-0127](https://rustsec.org/advisories/RUSTSEC-2021-0127.html).
|
||||||
|
It remains used for persisted repository metadata. Replacing it needs a separate
|
||||||
|
storage-compatibility review, not an untested codec substitution during release
|
||||||
|
cleanup. The warning remains visible in audit output; no advisory is ignored.
|
||||||
|
Operators should protect the state directory from untrusted modification.
|
||||||
|
|
||||||
|
## License notices
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tools/dependency_notices.py --check
|
||||||
|
# After a reviewed Cargo.lock change:
|
||||||
|
python3 tools/dependency_notices.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Requirements: Python 3, Cargo, curl, and HTTPS access to the Cargo registry and
|
||||||
|
GitHub's raw file service. The tool reads the entire locked dependency graph,
|
||||||
|
including build-time and other-platform dependencies, and preserves upstream
|
||||||
|
license and notice text. `--check` does not modify tracked files.
|
||||||
|
|
||||||
|
Some package archives omit their license file. `tools/license_supplements.json`
|
||||||
|
records exact upstream commit URLs and SHA-256 checksums, derived from the
|
||||||
|
package's `.cargo_vcs_info.json`. Version changes need a new source review;
|
||||||
|
missing texts or checksum mismatches fail rather than silently omit attribution.
|
||||||
|
Do not edit original copyright or license wording to match project formatting.
|
||||||
|
|
||||||
|
## Coverage
|
||||||
|
|
||||||
|
With cargo-llvm-cov and the toolchain's llvm-tools-preview component installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo llvm-cov --locked --all-targets --json --output-path target/coverage.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Record actual line coverage and tool versions. The release target is 90%.
|
||||||
|
Do not exclude production modules or change the denominator to meet the target.
|
||||||
@ -1,10 +1,12 @@
|
|||||||
# Development and testing
|
# Development and testing
|
||||||
|
|
||||||
Use Rust 1.92 or newer on Linux and the [native dependencies](getting-started.md).
|
Use Rust 1.92 or newer on Linux and the [native dependencies](getting-started.md).
|
||||||
Tests additionally need Python 3, Python `cryptography`, and OpenSSL's CLI.
|
Tests additionally need Python 3, Python `cryptography`, OpenSSL's CLI, and
|
||||||
|
`ripgrep` (`rg`). Install them with `sudo apt-get install python3-cryptography openssl ripgrep`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo fmt --all --check
|
python3 tools/check_format.py
|
||||||
|
python3 -m unittest discover -s tools -p 'test_*.py'
|
||||||
cargo check --locked --no-default-features
|
cargo check --locked --no-default-features
|
||||||
cargo test --locked
|
cargo test --locked
|
||||||
cargo clippy --locked --all-targets -- -D warnings
|
cargo clippy --locked --all-targets -- -D warnings
|
||||||
@ -31,3 +33,10 @@ Generated keys and outputs are temporary and unsuitable as production inputs.
|
|||||||
Internal Rust modules are not a stable library API. The supported interface is
|
Internal Rust modules are not a stable library API. The supported interface is
|
||||||
the CLI and documented output files. Add behavior tests and update documentation
|
the CLI and documented output files. Add behavior tests and update documentation
|
||||||
when changing these interfaces.
|
when changing these interfaces.
|
||||||
|
|
||||||
|
The format checker also checks files included through `include!`, which are not
|
||||||
|
all traversed by `cargo fmt`. To format them, run `rustfmt --edition 2024` on
|
||||||
|
the reported files, then rerun the checker. Preserve upstream license texts.
|
||||||
|
|
||||||
|
For dependency and release checks see [Releasing](releasing.md) and
|
||||||
|
[Dependency maintenance](dependencies.md).
|
||||||
|
|||||||
37
docs/releasing.md
Normal file
37
docs/releasing.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Release checklist
|
||||||
|
|
||||||
|
This checklist does not publish packages or images automatically. Keep
|
||||||
|
`publish = false` unless a separate crates.io publication decision is made.
|
||||||
|
|
||||||
|
## Before the first public release
|
||||||
|
|
||||||
|
- Confirm rights to publish all project code and preserve upstream attribution.
|
||||||
|
- Establish the official public repository and enable and test GitHub Private
|
||||||
|
Vulnerability Reporting. Update [SECURITY.md](../SECURITY.md) with the real link.
|
||||||
|
- Require CI checks on the protected main branch. Verify an actual successful
|
||||||
|
run on the hosting platform; local tests do not prove hosted CI is enabled.
|
||||||
|
- Run the [development checks](development.md), dependency audit, license checks,
|
||||||
|
and coverage measurement from a clean candidate checkout. The line-coverage
|
||||||
|
target is 90%; record any shortfall as an unresolved release item, without
|
||||||
|
excluding production files to improve the number.
|
||||||
|
- Review every security advisory. Fix affected dependencies with minimal
|
||||||
|
compatible changes, or block release. Do not silently suppress advisories.
|
||||||
|
- Confirm no credentials, production inputs, repository state or internal
|
||||||
|
verification data are tracked. Check documentation commands and links.
|
||||||
|
|
||||||
|
## Tag and artifacts
|
||||||
|
|
||||||
|
After the checks pass, replace `Unreleased` with the release date, review the
|
||||||
|
version and known limitations, and tag the exact tested commit. Rebuild source,
|
||||||
|
binary and container artifacts from that commit. Publish SHA-256 checksums,
|
||||||
|
build/toolchain details, dependency inventory (SBOM), and source revision with
|
||||||
|
the artifacts. Include project and third-party licenses in binary distributions.
|
||||||
|
|
||||||
|
Only Linux x86-64 has been validated for this candidate. Do not label other
|
||||||
|
architectures or operating systems supported without testing them. Pin and
|
||||||
|
record container base-image digests for the release build; mutable tags alone
|
||||||
|
are not sufficient to reproduce an image.
|
||||||
|
|
||||||
|
If runtime code or dependencies change, repeat end-to-end snapshot/delta and
|
||||||
|
artifact verification. Documentation-only changes do not establish new runtime
|
||||||
|
performance measurements; retain the revision associated with earlier results.
|
||||||
@ -22157,7 +22157,7 @@ DEALINGS IN THE SOFTWARE.
|
|||||||
|
|
||||||
|
|
||||||
========================================================================
|
========================================================================
|
||||||
quick-xml 0.37.5 — LICENSE-MIT.md
|
quick-xml 0.41.0 — LICENSE-MIT.md
|
||||||
========================================================================
|
========================================================================
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
|||||||
@ -102,7 +102,8 @@ impl ResourceCertificate {
|
|||||||
return Err(ResourceCertificateProfileError::CertificatePoliciesTooManyQualifiers);
|
return Err(ResourceCertificateProfileError::CertificatePoliciesTooManyQualifiers);
|
||||||
}
|
}
|
||||||
if let Some(qualifier_oid) = policies.qualifier_oids.first()
|
if let Some(qualifier_oid) = policies.qualifier_oids.first()
|
||||||
&& qualifier_oid != OID_QT_CPS {
|
&& qualifier_oid != OID_QT_CPS
|
||||||
|
{
|
||||||
return Err(
|
return Err(
|
||||||
ResourceCertificateProfileError::CertificatePoliciesInvalidQualifier(
|
ResourceCertificateProfileError::CertificatePoliciesInvalidQualifier(
|
||||||
qualifier_oid.clone(),
|
qualifier_oid.clone(),
|
||||||
|
|||||||
@ -115,7 +115,8 @@ impl RcExtensionsParsed {
|
|||||||
if is_self_signed {
|
if is_self_signed {
|
||||||
if let (Some(keyid), Some(ski)) =
|
if let (Some(keyid), Some(ski)) =
|
||||||
(keyid.as_ref(), subject_key_identifier.as_ref())
|
(keyid.as_ref(), subject_key_identifier.as_ref())
|
||||||
&& keyid != ski {
|
&& keyid != ski
|
||||||
|
{
|
||||||
return Err(ResourceCertificateProfileError::AkiSelfSignedNotEqualSki);
|
return Err(ResourceCertificateProfileError::AkiSelfSignedNotEqualSki);
|
||||||
}
|
}
|
||||||
} else if keyid.is_none() {
|
} else if keyid.is_none() {
|
||||||
|
|||||||
@ -54,10 +54,7 @@ impl RpkiSignedObject {
|
|||||||
pub fn verify_signature(&self) -> Result<(), SignedObjectVerifyError> {
|
pub fn verify_signature(&self) -> Result<(), SignedObjectVerifyError> {
|
||||||
let ee = &self.signed_data.certificates[0];
|
let ee = &self.signed_data.certificates[0];
|
||||||
|
|
||||||
self.verify_signature_with_rsa_components(
|
self.verify_signature_with_rsa_components(&ee.rsa_public_modulus, &ee.rsa_public_exponent)
|
||||||
&ee.rsa_public_modulus,
|
|
||||||
&ee.rsa_public_exponent,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify the CMS signature using a DER-encoded SubjectPublicKeyInfo.
|
/// Verify the CMS signature using a DER-encoded SubjectPublicKeyInfo.
|
||||||
|
|||||||
@ -7,9 +7,15 @@ pub enum StorageError {
|
|||||||
#[error("missing column family: {0}")]
|
#[error("missing column family: {0}")]
|
||||||
MissingColumnFamily(&'static str),
|
MissingColumnFamily(&'static str),
|
||||||
#[error("cbor codec error for {entity}: {detail}")]
|
#[error("cbor codec error for {entity}: {detail}")]
|
||||||
Codec { entity: &'static str, detail: String },
|
Codec {
|
||||||
|
entity: &'static str,
|
||||||
|
detail: String,
|
||||||
|
},
|
||||||
#[error("invalid {entity}: {detail}")]
|
#[error("invalid {entity}: {detail}")]
|
||||||
InvalidData { entity: &'static str, detail: String },
|
InvalidData {
|
||||||
|
entity: &'static str,
|
||||||
|
detail: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type StorageResult<T> = Result<T, StorageError>;
|
pub type StorageResult<T> = Result<T, StorageError>;
|
||||||
@ -52,9 +58,13 @@ impl RepositoryViewEntry {
|
|||||||
}
|
}
|
||||||
match self.state {
|
match self.state {
|
||||||
RepositoryViewState::Present | RepositoryViewState::Replaced => {
|
RepositoryViewState::Present | RepositoryViewState::Replaced => {
|
||||||
let hash = self.current_hash.as_deref().ok_or(StorageError::InvalidData {
|
let hash = self
|
||||||
|
.current_hash
|
||||||
|
.as_deref()
|
||||||
|
.ok_or(StorageError::InvalidData {
|
||||||
entity: "repository_view",
|
entity: "repository_view",
|
||||||
detail: "current_hash is required when state is present or replaced".to_string(),
|
detail: "current_hash is required when state is present or replaced"
|
||||||
|
.to_string(),
|
||||||
})?;
|
})?;
|
||||||
validate_sha256_hex("repository_view.current_hash", hash)?;
|
validate_sha256_hex("repository_view.current_hash", hash)?;
|
||||||
}
|
}
|
||||||
@ -79,23 +89,38 @@ pub struct RawByHashEntry {
|
|||||||
|
|
||||||
impl RawByHashEntry {
|
impl RawByHashEntry {
|
||||||
pub fn from_bytes(sha256_hex: impl Into<String>, bytes: Vec<u8>) -> Self {
|
pub fn from_bytes(sha256_hex: impl Into<String>, bytes: Vec<u8>) -> Self {
|
||||||
Self { sha256_hex: sha256_hex.into(), bytes, origin_uris: Vec::new(), object_type: None, encoding: None }
|
Self {
|
||||||
|
sha256_hex: sha256_hex.into(),
|
||||||
|
bytes,
|
||||||
|
origin_uris: Vec::new(),
|
||||||
|
object_type: None,
|
||||||
|
encoding: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_internal(&self) -> StorageResult<()> {
|
pub fn validate_internal(&self) -> StorageResult<()> {
|
||||||
validate_sha256_hex("raw_by_hash.sha256_hex", &self.sha256_hex)?;
|
validate_sha256_hex("raw_by_hash.sha256_hex", &self.sha256_hex)?;
|
||||||
if self.bytes.is_empty() {
|
if self.bytes.is_empty() {
|
||||||
return Err(StorageError::InvalidData { entity: "raw_by_hash", detail: "bytes must not be empty".to_string() });
|
return Err(StorageError::InvalidData {
|
||||||
|
entity: "raw_by_hash",
|
||||||
|
detail: "bytes must not be empty".to_string(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let computed = hex::encode(compute_sha256_32(&self.bytes));
|
let computed = hex::encode(compute_sha256_32(&self.bytes));
|
||||||
if computed != self.sha256_hex.to_ascii_lowercase() {
|
if computed != self.sha256_hex.to_ascii_lowercase() {
|
||||||
return Err(StorageError::InvalidData { entity: "raw_by_hash", detail: "sha256_hex does not match bytes".to_string() });
|
return Err(StorageError::InvalidData {
|
||||||
|
entity: "raw_by_hash",
|
||||||
|
detail: "sha256_hex does not match bytes".to_string(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let mut seen = HashSet::with_capacity(self.origin_uris.len());
|
let mut seen = HashSet::with_capacity(self.origin_uris.len());
|
||||||
for uri in &self.origin_uris {
|
for uri in &self.origin_uris {
|
||||||
validate_non_empty("raw_by_hash.origin_uris[]", uri)?;
|
validate_non_empty("raw_by_hash.origin_uris[]", uri)?;
|
||||||
if !seen.insert(uri.as_str()) {
|
if !seen.insert(uri.as_str()) {
|
||||||
return Err(StorageError::InvalidData { entity: "raw_by_hash", detail: format!("duplicate origin URI: {uri}") });
|
return Err(StorageError::InvalidData {
|
||||||
|
entity: "raw_by_hash",
|
||||||
|
detail: format!("duplicate origin URI: {uri}"),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -118,11 +143,24 @@ pub struct ValidatedManifestMeta {
|
|||||||
|
|
||||||
impl ValidatedManifestMeta {
|
impl ValidatedManifestMeta {
|
||||||
pub fn validate_internal(&self) -> StorageResult<()> {
|
pub fn validate_internal(&self) -> StorageResult<()> {
|
||||||
validate_manifest_number_be("validated_manifest_meta.validated_manifest_number", &self.validated_manifest_number)?;
|
validate_manifest_number_be(
|
||||||
let this_update = parse_time("validated_manifest_meta.validated_manifest_this_update", &self.validated_manifest_this_update)?;
|
"validated_manifest_meta.validated_manifest_number",
|
||||||
let next_update = parse_time("validated_manifest_meta.validated_manifest_next_update", &self.validated_manifest_next_update)?;
|
&self.validated_manifest_number,
|
||||||
|
)?;
|
||||||
|
let this_update = parse_time(
|
||||||
|
"validated_manifest_meta.validated_manifest_this_update",
|
||||||
|
&self.validated_manifest_this_update,
|
||||||
|
)?;
|
||||||
|
let next_update = parse_time(
|
||||||
|
"validated_manifest_meta.validated_manifest_next_update",
|
||||||
|
&self.validated_manifest_next_update,
|
||||||
|
)?;
|
||||||
if next_update < this_update {
|
if next_update < this_update {
|
||||||
return Err(StorageError::InvalidData { entity: "validated_manifest_meta", detail: "validated_manifest_next_update must be >= validated_manifest_this_update".to_string() });
|
return Err(StorageError::InvalidData {
|
||||||
|
entity: "validated_manifest_meta",
|
||||||
|
detail: "validated_manifest_next_update must be >= validated_manifest_this_update"
|
||||||
|
.to_string(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -141,11 +179,26 @@ pub struct ManifestAntiRollbackMeta {
|
|||||||
|
|
||||||
impl ManifestAntiRollbackMeta {
|
impl ManifestAntiRollbackMeta {
|
||||||
pub fn validate_internal(&self) -> StorageResult<()> {
|
pub fn validate_internal(&self) -> StorageResult<()> {
|
||||||
validate_non_empty("manifest_anti_rollback.manifest_rsync_uri", &self.manifest_rsync_uri)?;
|
validate_non_empty(
|
||||||
validate_manifest_number_be("manifest_anti_rollback.manifest_number_be", &self.manifest_number_be)?;
|
"manifest_anti_rollback.manifest_rsync_uri",
|
||||||
parse_time("manifest_anti_rollback.manifest_this_update", &self.manifest_this_update)?;
|
&self.manifest_rsync_uri,
|
||||||
validate_sha256_digest_bytes("manifest_anti_rollback.manifest_sha256", &self.manifest_sha256)?;
|
)?;
|
||||||
parse_time("manifest_anti_rollback.updated_at_validation_time", &self.updated_at_validation_time)?;
|
validate_manifest_number_be(
|
||||||
|
"manifest_anti_rollback.manifest_number_be",
|
||||||
|
&self.manifest_number_be,
|
||||||
|
)?;
|
||||||
|
parse_time(
|
||||||
|
"manifest_anti_rollback.manifest_this_update",
|
||||||
|
&self.manifest_this_update,
|
||||||
|
)?;
|
||||||
|
validate_sha256_digest_bytes(
|
||||||
|
"manifest_anti_rollback.manifest_sha256",
|
||||||
|
&self.manifest_sha256,
|
||||||
|
)?;
|
||||||
|
parse_time(
|
||||||
|
"manifest_anti_rollback.updated_at_validation_time",
|
||||||
|
&self.updated_at_validation_time,
|
||||||
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,7 +68,10 @@ impl RrdpSourceMemberRecord {
|
|||||||
&self.last_confirmed_session_id,
|
&self.last_confirmed_session_id,
|
||||||
)?;
|
)?;
|
||||||
if self.present {
|
if self.present {
|
||||||
let hash = self.current_hash.as_deref().ok_or(StorageError::InvalidData {
|
let hash = self
|
||||||
|
.current_hash
|
||||||
|
.as_deref()
|
||||||
|
.ok_or(StorageError::InvalidData {
|
||||||
entity: "rrdp_source_member",
|
entity: "rrdp_source_member",
|
||||||
detail: "current_hash is required when present=true".to_string(),
|
detail: "current_hash is required when present=true".to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
@ -19,12 +19,7 @@ impl RocksStore {
|
|||||||
let mut options = Options::default();
|
let mut options = Options::default();
|
||||||
configure_work_db_options(&mut options);
|
configure_work_db_options(&mut options);
|
||||||
let descriptors = column_family_descriptors();
|
let descriptors = column_family_descriptors();
|
||||||
let db = DB::open_cf_descriptors_read_only(
|
let db = DB::open_cf_descriptors_read_only(&options, source, descriptors, false)
|
||||||
&options,
|
|
||||||
source,
|
|
||||||
descriptors,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.map_err(|error| StorageError::RocksDb(error.to_string()))?;
|
.map_err(|error| StorageError::RocksDb(error.to_string()))?;
|
||||||
let checkpoint =
|
let checkpoint =
|
||||||
Checkpoint::new(&db).map_err(|error| StorageError::RocksDb(error.to_string()))?;
|
Checkpoint::new(&db).map_err(|error| StorageError::RocksDb(error.to_string()))?;
|
||||||
@ -41,11 +36,7 @@ impl RocksStore {
|
|||||||
configure_work_db_options(&mut base_opts);
|
configure_work_db_options(&mut base_opts);
|
||||||
let descriptors = column_family_descriptors();
|
let descriptors = column_family_descriptors();
|
||||||
|
|
||||||
let db = DB::open_cf_descriptors(
|
let db = DB::open_cf_descriptors(&base_opts, path, descriptors)
|
||||||
&base_opts,
|
|
||||||
path,
|
|
||||||
descriptors,
|
|
||||||
)
|
|
||||||
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@ -124,7 +115,6 @@ impl RocksStore {
|
|||||||
.cf_handle(name)
|
.cf_handle(name)
|
||||||
.ok_or(StorageError::MissingColumnFamily(name))
|
.ok_or(StorageError::MissingColumnFamily(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reject_unsupported_column_families(path: &Path) -> StorageResult<()> {
|
fn reject_unsupported_column_families(path: &Path) -> StorageResult<()> {
|
||||||
|
|||||||
@ -20,7 +20,10 @@ impl RocksStore {
|
|||||||
Ok(Some(metadata))
|
Ok(Some(metadata))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn put_manifest_anti_rollback_meta(&self, metadata: &ManifestAntiRollbackMeta) -> StorageResult<()> {
|
pub fn put_manifest_anti_rollback_meta(
|
||||||
|
&self,
|
||||||
|
metadata: &ManifestAntiRollbackMeta,
|
||||||
|
) -> StorageResult<()> {
|
||||||
metadata.validate_internal()?;
|
metadata.validate_internal()?;
|
||||||
let cf = self.cf(CF_MANIFEST_ANTI_ROLLBACK)?;
|
let cf = self.cf(CF_MANIFEST_ANTI_ROLLBACK)?;
|
||||||
let key = manifest_anti_rollback_key(&metadata.manifest_rsync_uri);
|
let key = manifest_anti_rollback_key(&metadata.manifest_rsync_uri);
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
// Repository-view and raw-object/blob storage operations.
|
// Repository-view and raw-object/blob storage operations.
|
||||||
|
|
||||||
impl RocksStore {
|
impl RocksStore {
|
||||||
|
|
||||||
pub fn put_repository_view_entry(&self, entry: &RepositoryViewEntry) -> StorageResult<()> {
|
pub fn put_repository_view_entry(&self, entry: &RepositoryViewEntry) -> StorageResult<()> {
|
||||||
entry.validate_internal()?;
|
entry.validate_internal()?;
|
||||||
let cf = self.cf(CF_REPOSITORY_VIEW)?;
|
let cf = self.cf(CF_REPOSITORY_VIEW)?;
|
||||||
@ -359,5 +358,4 @@ impl RocksStore {
|
|||||||
}
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,72 +6,144 @@ impl RocksStore {
|
|||||||
let cf = self.cf(CF_RRDP_SOURCE)?;
|
let cf = self.cf(CF_RRDP_SOURCE)?;
|
||||||
let key = rrdp_source_key(&record.notify_uri);
|
let key = rrdp_source_key(&record.notify_uri);
|
||||||
let value = encode_cbor(record, "rrdp_source")?;
|
let value = encode_cbor(record, "rrdp_source")?;
|
||||||
self.db.put_cf(cf, key.as_bytes(), value).map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
self.db
|
||||||
|
.put_cf(cf, key.as_bytes(), value)
|
||||||
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rrdp_source_record(&self, notify_uri: &str) -> StorageResult<Option<RrdpSourceRecord>> {
|
pub fn get_rrdp_source_record(
|
||||||
|
&self,
|
||||||
|
notify_uri: &str,
|
||||||
|
) -> StorageResult<Option<RrdpSourceRecord>> {
|
||||||
let cf = self.cf(CF_RRDP_SOURCE)?;
|
let cf = self.cf(CF_RRDP_SOURCE)?;
|
||||||
let key = rrdp_source_key(notify_uri);
|
let key = rrdp_source_key(notify_uri);
|
||||||
let Some(bytes) = self.db.get_cf(cf, key.as_bytes()).map_err(|e| StorageError::RocksDb(e.to_string()))? else { return Ok(None) };
|
let Some(bytes) = self
|
||||||
|
.db
|
||||||
|
.get_cf(cf, key.as_bytes())
|
||||||
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
let record = decode_cbor::<RrdpSourceRecord>(&bytes, "rrdp_source")?;
|
let record = decode_cbor::<RrdpSourceRecord>(&bytes, "rrdp_source")?;
|
||||||
record.validate_internal()?;
|
record.validate_internal()?;
|
||||||
Ok(Some(record))
|
Ok(Some(record))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn put_rrdp_source_member_record(&self, record: &RrdpSourceMemberRecord) -> StorageResult<()> {
|
pub fn put_rrdp_source_member_record(
|
||||||
|
&self,
|
||||||
|
record: &RrdpSourceMemberRecord,
|
||||||
|
) -> StorageResult<()> {
|
||||||
record.validate_internal()?;
|
record.validate_internal()?;
|
||||||
let cf = self.cf(CF_RRDP_SOURCE_MEMBER)?;
|
let cf = self.cf(CF_RRDP_SOURCE_MEMBER)?;
|
||||||
let key = rrdp_source_member_key(&record.notify_uri, &record.rsync_uri);
|
let key = rrdp_source_member_key(&record.notify_uri, &record.rsync_uri);
|
||||||
let value = encode_cbor(record, "rrdp_source_member")?;
|
let value = encode_cbor(record, "rrdp_source_member")?;
|
||||||
self.db.put_cf(cf, key.as_bytes(), value).map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
self.db
|
||||||
|
.put_cf(cf, key.as_bytes(), value)
|
||||||
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rrdp_source_member_record(&self, notify_uri: &str, rsync_uri: &str) -> StorageResult<Option<RrdpSourceMemberRecord>> {
|
pub fn get_rrdp_source_member_record(
|
||||||
|
&self,
|
||||||
|
notify_uri: &str,
|
||||||
|
rsync_uri: &str,
|
||||||
|
) -> StorageResult<Option<RrdpSourceMemberRecord>> {
|
||||||
let cf = self.cf(CF_RRDP_SOURCE_MEMBER)?;
|
let cf = self.cf(CF_RRDP_SOURCE_MEMBER)?;
|
||||||
let key = rrdp_source_member_key(notify_uri, rsync_uri);
|
let key = rrdp_source_member_key(notify_uri, rsync_uri);
|
||||||
let Some(bytes) = self.db.get_cf(cf, key.as_bytes()).map_err(|e| StorageError::RocksDb(e.to_string()))? else { return Ok(None) };
|
let Some(bytes) = self
|
||||||
|
.db
|
||||||
|
.get_cf(cf, key.as_bytes())
|
||||||
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
let record = decode_cbor::<RrdpSourceMemberRecord>(&bytes, "rrdp_source_member")?;
|
let record = decode_cbor::<RrdpSourceMemberRecord>(&bytes, "rrdp_source_member")?;
|
||||||
record.validate_internal()?;
|
record.validate_internal()?;
|
||||||
Ok(Some(record))
|
Ok(Some(record))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_rrdp_source_member_records(&self, notify_uri: &str) -> StorageResult<Vec<RrdpSourceMemberRecord>> {
|
pub fn list_rrdp_source_member_records(
|
||||||
|
&self,
|
||||||
|
notify_uri: &str,
|
||||||
|
) -> StorageResult<Vec<RrdpSourceMemberRecord>> {
|
||||||
let cf = self.cf(CF_RRDP_SOURCE_MEMBER)?;
|
let cf = self.cf(CF_RRDP_SOURCE_MEMBER)?;
|
||||||
let prefix = rrdp_source_member_prefix(notify_uri);
|
let prefix = rrdp_source_member_prefix(notify_uri);
|
||||||
let mode = IteratorMode::From(prefix.as_bytes(), Direction::Forward);
|
let mode = IteratorMode::From(prefix.as_bytes(), Direction::Forward);
|
||||||
self.db.iterator_cf(cf, mode).take_while(|res| match res { Ok((key, _)) => key.starts_with(prefix.as_bytes()), Err(_) => false }).map(|res| {
|
self.db
|
||||||
|
.iterator_cf(cf, mode)
|
||||||
|
.take_while(|res| match res {
|
||||||
|
Ok((key, _)) => key.starts_with(prefix.as_bytes()),
|
||||||
|
Err(_) => false,
|
||||||
|
})
|
||||||
|
.map(|res| {
|
||||||
let (_key, value) = res.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
let (_key, value) = res.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
||||||
let record = decode_cbor::<RrdpSourceMemberRecord>(&value, "rrdp_source_member")?;
|
let record = decode_cbor::<RrdpSourceMemberRecord>(&value, "rrdp_source_member")?;
|
||||||
record.validate_internal()?;
|
record.validate_internal()?;
|
||||||
Ok(record)
|
Ok(record)
|
||||||
}).collect()
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_current_rrdp_source_members(&self, notify_uri: &str) -> StorageResult<Vec<RrdpSourceMemberRecord>> {
|
pub fn list_current_rrdp_source_members(
|
||||||
|
&self,
|
||||||
|
notify_uri: &str,
|
||||||
|
) -> StorageResult<Vec<RrdpSourceMemberRecord>> {
|
||||||
let mut records = self.list_rrdp_source_member_records(notify_uri)?;
|
let mut records = self.list_rrdp_source_member_records(notify_uri)?;
|
||||||
records.retain(|record| record.present);
|
records.retain(|record| record.present);
|
||||||
records.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri));
|
records.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri));
|
||||||
Ok(records)
|
Ok(records)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_current_rrdp_source_member(&self, notify_uri: &str, rsync_uri: &str) -> StorageResult<bool> {
|
pub fn is_current_rrdp_source_member(
|
||||||
Ok(matches!(self.get_rrdp_source_member_record(notify_uri, rsync_uri)?, Some(record) if record.present))
|
&self,
|
||||||
|
notify_uri: &str,
|
||||||
|
rsync_uri: &str,
|
||||||
|
) -> StorageResult<bool> {
|
||||||
|
Ok(
|
||||||
|
matches!(self.get_rrdp_source_member_record(notify_uri, rsync_uri)?, Some(record) if record.present),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_current_object_bytes_by_uri(&self, rsync_uri: &str) -> StorageResult<Option<Vec<u8>>> {
|
pub fn load_current_object_bytes_by_uri(
|
||||||
Ok(self.load_current_object_with_hash_by_uri(rsync_uri)?.map(|object| object.bytes))
|
&self,
|
||||||
|
rsync_uri: &str,
|
||||||
|
) -> StorageResult<Option<Vec<u8>>> {
|
||||||
|
Ok(self
|
||||||
|
.load_current_object_with_hash_by_uri(rsync_uri)?
|
||||||
|
.map(|object| object.bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_current_object_with_hash_by_uri(&self, rsync_uri: &str) -> StorageResult<Option<CurrentObjectWithHash>> {
|
pub fn load_current_object_with_hash_by_uri(
|
||||||
let Some(view) = self.get_repository_view_entry(rsync_uri)? else { return Ok(None) };
|
&self,
|
||||||
|
rsync_uri: &str,
|
||||||
|
) -> StorageResult<Option<CurrentObjectWithHash>> {
|
||||||
|
let Some(view) = self.get_repository_view_entry(rsync_uri)? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
match view.state {
|
match view.state {
|
||||||
RepositoryViewState::Withdrawn => Ok(None),
|
RepositoryViewState::Withdrawn => Ok(None),
|
||||||
RepositoryViewState::Present | RepositoryViewState::Replaced => {
|
RepositoryViewState::Present | RepositoryViewState::Replaced => {
|
||||||
let hash = view.current_hash.as_deref().ok_or(StorageError::InvalidData { entity: "repository_view", detail: format!("current_hash missing for current object URI: {rsync_uri}") })?;
|
let hash = view
|
||||||
let bytes = self.get_blob_bytes(hash)?.ok_or(StorageError::InvalidData { entity: "repository_view", detail: format!("blob bytes missing for current object URI: {rsync_uri} (hash={hash})") })?;
|
.current_hash
|
||||||
Ok(Some(CurrentObjectWithHash { current_hash_hex: hash.to_ascii_lowercase(), current_hash: decode_sha256_hex_32("repository_view.current_hash", hash)?, bytes }))
|
.as_deref()
|
||||||
|
.ok_or(StorageError::InvalidData {
|
||||||
|
entity: "repository_view",
|
||||||
|
detail: format!("current_hash missing for current object URI: {rsync_uri}"),
|
||||||
|
})?;
|
||||||
|
let bytes = self
|
||||||
|
.get_blob_bytes(hash)?
|
||||||
|
.ok_or(StorageError::InvalidData {
|
||||||
|
entity: "repository_view",
|
||||||
|
detail: format!(
|
||||||
|
"blob bytes missing for current object URI: {rsync_uri} (hash={hash})"
|
||||||
|
),
|
||||||
|
})?;
|
||||||
|
Ok(Some(CurrentObjectWithHash {
|
||||||
|
current_hash_hex: hash.to_ascii_lowercase(),
|
||||||
|
current_hash: decode_sha256_hex_32("repository_view.current_hash", hash)?,
|
||||||
|
bytes,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,14 +153,25 @@ impl RocksStore {
|
|||||||
let cf = self.cf(CF_RRDP_URI_OWNER)?;
|
let cf = self.cf(CF_RRDP_URI_OWNER)?;
|
||||||
let key = rrdp_uri_owner_key(&record.rsync_uri);
|
let key = rrdp_uri_owner_key(&record.rsync_uri);
|
||||||
let value = encode_cbor(record, "rrdp_uri_owner")?;
|
let value = encode_cbor(record, "rrdp_uri_owner")?;
|
||||||
self.db.put_cf(cf, key.as_bytes(), value).map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
self.db
|
||||||
|
.put_cf(cf, key.as_bytes(), value)
|
||||||
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rrdp_uri_owner_record(&self, rsync_uri: &str) -> StorageResult<Option<RrdpUriOwnerRecord>> {
|
pub fn get_rrdp_uri_owner_record(
|
||||||
|
&self,
|
||||||
|
rsync_uri: &str,
|
||||||
|
) -> StorageResult<Option<RrdpUriOwnerRecord>> {
|
||||||
let cf = self.cf(CF_RRDP_URI_OWNER)?;
|
let cf = self.cf(CF_RRDP_URI_OWNER)?;
|
||||||
let key = rrdp_uri_owner_key(rsync_uri);
|
let key = rrdp_uri_owner_key(rsync_uri);
|
||||||
let Some(bytes) = self.db.get_cf(cf, key.as_bytes()).map_err(|e| StorageError::RocksDb(e.to_string()))? else { return Ok(None) };
|
let Some(bytes) = self
|
||||||
|
.db
|
||||||
|
.get_cf(cf, key.as_bytes())
|
||||||
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
let record = decode_cbor::<RrdpUriOwnerRecord>(&bytes, "rrdp_uri_owner")?;
|
let record = decode_cbor::<RrdpUriOwnerRecord>(&bytes, "rrdp_uri_owner")?;
|
||||||
record.validate_internal()?;
|
record.validate_internal()?;
|
||||||
Ok(Some(record))
|
Ok(Some(record))
|
||||||
@ -97,12 +180,16 @@ impl RocksStore {
|
|||||||
pub fn delete_rrdp_uri_owner_record(&self, rsync_uri: &str) -> StorageResult<()> {
|
pub fn delete_rrdp_uri_owner_record(&self, rsync_uri: &str) -> StorageResult<()> {
|
||||||
let cf = self.cf(CF_RRDP_URI_OWNER)?;
|
let cf = self.cf(CF_RRDP_URI_OWNER)?;
|
||||||
let key = rrdp_uri_owner_key(rsync_uri);
|
let key = rrdp_uri_owner_key(rsync_uri);
|
||||||
self.db.delete_cf(cf, key.as_bytes()).map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
self.db
|
||||||
|
.delete_cf(cf, key.as_bytes())
|
||||||
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_batch(&self, batch: WriteBatch) -> StorageResult<()> {
|
pub fn write_batch(&self, batch: WriteBatch) -> StorageResult<()> {
|
||||||
self.db.write(batch).map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
self.db
|
||||||
|
.write(batch)
|
||||||
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,9 @@ use crate::output::analysis::timing::{TimingHandle, TimingMeta};
|
|||||||
use crate::repository::fetch::rsync::LocalDirRsyncFetcher;
|
use crate::repository::fetch::rsync::LocalDirRsyncFetcher;
|
||||||
use crate::repository::storage::RepositoryViewState;
|
use crate::repository::storage::RepositoryViewState;
|
||||||
use crate::repository::sync::rrdp::Fetcher as HttpFetcher;
|
use crate::repository::sync::rrdp::Fetcher as HttpFetcher;
|
||||||
use crate::repository::sync::store_projection::{build_repository_view_present_entry, compute_sha256_hex};
|
use crate::repository::sync::store_projection::{
|
||||||
|
build_repository_view_present_entry, compute_sha256_hex,
|
||||||
|
};
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
@ -252,7 +254,10 @@ fn rsync_second_sync_marks_missing_repository_view_entries_withdrawn() {
|
|||||||
.get_repository_view_entry("rsync://example.test/repo/c.crl")
|
.get_repository_view_entry("rsync://example.test/repo/c.crl")
|
||||||
.expect("get added repo view")
|
.expect("get added repo view")
|
||||||
.expect("added entry exists");
|
.expect("added entry exists");
|
||||||
assert_eq!(added.state, crate::repository::storage::RepositoryViewState::Present);
|
assert_eq!(
|
||||||
|
added.state,
|
||||||
|
crate::repository::storage::RepositoryViewState::Present
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -340,7 +340,8 @@ pub(crate) fn load_rrdp_local_state(
|
|||||||
if let Some(record) = store
|
if let Some(record) = store
|
||||||
.get_rrdp_source_record(notification_uri)
|
.get_rrdp_source_record(notification_uri)
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
&& let (Some(session_id), Some(serial)) = (record.last_session_id, record.last_serial) {
|
&& let (Some(session_id), Some(serial)) = (record.last_session_id, record.last_serial)
|
||||||
|
{
|
||||||
return Ok(Some(RrdpState { session_id, serial }));
|
return Ok(Some(RrdpState { session_id, serial }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -617,7 +618,8 @@ pub fn parse_delta_file(xml: &[u8]) -> Result<DeltaFile, RrdpError> {
|
|||||||
let hash_sha256 = parse_sha256_hex_delta_withdraw(hash)?;
|
let hash_sha256 = parse_sha256_hex_delta_withdraw(hash)?;
|
||||||
|
|
||||||
if let Some(s) = collect_element_text(&child)
|
if let Some(s) = collect_element_text(&child)
|
||||||
&& !strip_all_ascii_whitespace(&s).is_empty() {
|
&& !strip_all_ascii_whitespace(&s).is_empty()
|
||||||
|
{
|
||||||
return Err(RrdpError::DeltaWithdrawUnexpectedContent);
|
return Err(RrdpError::DeltaWithdrawUnexpectedContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,20 @@ use super::{
|
|||||||
RrdpResourceKind, RrdpSyncError, parse_u64_str, strip_all_ascii_whitespace,
|
RrdpResourceKind, RrdpSyncError, parse_u64_str, strip_all_ascii_whitespace,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn decode_attribute(
|
||||||
|
attr: &quick_xml::events::attributes::Attribute<'_>,
|
||||||
|
decoder: quick_xml::encoding::Decoder,
|
||||||
|
) -> Result<String, RrdpError> {
|
||||||
|
// Preserve the existing decode + unescape behavior without adopting the
|
||||||
|
// newer API's additional XML attribute whitespace normalization.
|
||||||
|
let decoded = decoder
|
||||||
|
.decode(attr.value.as_ref())
|
||||||
|
.map_err(|e| RrdpError::Xml(e.to_string()))?;
|
||||||
|
quick_xml::escape::unescape(&decoded)
|
||||||
|
.map(|value| value.into_owned())
|
||||||
|
.map_err(|e| RrdpError::Xml(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(super) fn apply_snapshot(
|
pub(super) fn apply_snapshot(
|
||||||
store: &RocksStore,
|
store: &RocksStore,
|
||||||
@ -91,10 +105,7 @@ pub(super) fn apply_snapshot_from_bufread<R: BufRead>(
|
|||||||
Err(e) => return Err(RrdpError::Xml(e.to_string()).into()),
|
Err(e) => return Err(RrdpError::Xml(e.to_string()).into()),
|
||||||
};
|
};
|
||||||
let key = attr.key.as_ref();
|
let key = attr.key.as_ref();
|
||||||
let value = attr
|
let value = decode_attribute(&attr, reader.decoder())?;
|
||||||
.decode_and_unescape_value(reader.decoder())
|
|
||||||
.map_err(|e| RrdpError::Xml(e.to_string()))?
|
|
||||||
.into_owned();
|
|
||||||
match key {
|
match key {
|
||||||
b"xmlns" => xmlns = value,
|
b"xmlns" => xmlns = value,
|
||||||
b"version" => version = value,
|
b"version" => version = value,
|
||||||
@ -136,11 +147,7 @@ pub(super) fn apply_snapshot_from_bufread<R: BufRead>(
|
|||||||
Err(e) => return Err(RrdpError::Xml(e.to_string()).into()),
|
Err(e) => return Err(RrdpError::Xml(e.to_string()).into()),
|
||||||
};
|
};
|
||||||
if attr.key.as_ref() == b"uri" {
|
if attr.key.as_ref() == b"uri" {
|
||||||
uri = Some(
|
uri = Some(decode_attribute(&attr, reader.decoder())?);
|
||||||
attr.decode_and_unescape_value(reader.decoder())
|
|
||||||
.map_err(|e| RrdpError::Xml(e.to_string()))?
|
|
||||||
.into_owned(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let uri = uri.ok_or(RrdpError::PublishUriMissing)?;
|
let uri = uri.ok_or(RrdpError::PublishUriMissing)?;
|
||||||
@ -186,6 +193,17 @@ pub(super) fn apply_snapshot_from_bufread<R: BufRead>(
|
|||||||
current_publish_text.push_str(&text);
|
current_publish_text.push_str(&text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(Event::GeneralRef(_)) => {
|
||||||
|
// Earlier reader versions left references inside the raw text,
|
||||||
|
// where base64 decoding rejected them. Do not silently drop a
|
||||||
|
// reference now that the reader emits a separate event.
|
||||||
|
if in_publish && publish_nested_depth == 0 {
|
||||||
|
return Err(RrdpError::PublishBase64(
|
||||||
|
"entity references are not valid base64 text".into(),
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(Event::CData(e)) => {
|
Ok(Event::CData(e)) => {
|
||||||
if in_publish && publish_nested_depth == 0 {
|
if in_publish && publish_nested_depth == 0 {
|
||||||
let text = reader
|
let text = reader
|
||||||
@ -452,3 +470,28 @@ pub(super) fn fetch_snapshot_into_tempfile(
|
|||||||
.map_err(|e| RrdpSyncError::Fetch(format!("tempfile rewind failed: {e}")))?;
|
.map_err(|e| RrdpSyncError::Fetch(format!("tempfile rewind failed: {e}")))?;
|
||||||
Ok((tmp, bytes_written))
|
Ok((tmp, bytes_written))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod attribute_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn attribute_decoding_preserves_whitespace_and_unescapes() {
|
||||||
|
let reader = Reader::from_str("");
|
||||||
|
let attr = quick_xml::events::attributes::Attribute::from((
|
||||||
|
b"uri".as_slice(),
|
||||||
|
b"a\tb&c".as_slice(),
|
||||||
|
));
|
||||||
|
assert_eq!(decode_attribute(&attr, reader.decoder()).unwrap(), "a\tb&c");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn attribute_decoding_rejects_unknown_entities() {
|
||||||
|
let reader = Reader::from_str("");
|
||||||
|
let attr = quick_xml::events::attributes::Attribute::from((
|
||||||
|
b"uri".as_slice(),
|
||||||
|
b"a&unknown;".as_slice(),
|
||||||
|
));
|
||||||
|
assert!(decode_attribute(&attr, reader.decoder()).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -99,12 +99,18 @@ fn apply_delta_applies_publish_replace_and_withdraw_with_membership_checks() {
|
|||||||
.get_repository_view_entry("rsync://example.net/repo/a.mft")
|
.get_repository_view_entry("rsync://example.net/repo/a.mft")
|
||||||
.expect("get a view")
|
.expect("get a view")
|
||||||
.expect("a view exists");
|
.expect("a view exists");
|
||||||
assert_eq!(a_view.state, crate::repository::storage::RepositoryViewState::Withdrawn);
|
assert_eq!(
|
||||||
|
a_view.state,
|
||||||
|
crate::repository::storage::RepositoryViewState::Withdrawn
|
||||||
|
);
|
||||||
let b_view = store
|
let b_view = store
|
||||||
.get_repository_view_entry("rsync://example.net/repo/b.roa")
|
.get_repository_view_entry("rsync://example.net/repo/b.roa")
|
||||||
.expect("get b view")
|
.expect("get b view")
|
||||||
.expect("b view exists");
|
.expect("b view exists");
|
||||||
assert_eq!(b_view.state, crate::repository::storage::RepositoryViewState::Present);
|
assert_eq!(
|
||||||
|
b_view.state,
|
||||||
|
crate::repository::storage::RepositoryViewState::Present
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
b_view.current_hash.as_deref(),
|
b_view.current_hash.as_deref(),
|
||||||
Some(hex::encode(sha2::Sha256::digest(b"b2")).as_str())
|
Some(hex::encode(sha2::Sha256::digest(b"b2")).as_str())
|
||||||
|
|||||||
@ -1,5 +1,30 @@
|
|||||||
// RRDP test group: sync.
|
// RRDP test group: sync.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn snapshot_entity_reference_is_not_silently_dropped_from_base64() {
|
||||||
|
for content in ["YQ==&", "YQ==", "YQ==&unknown;"] {
|
||||||
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
let store = RocksStore::open(temp.path()).unwrap();
|
||||||
|
let sid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
|
||||||
|
let xml = format!(
|
||||||
|
"<snapshot xmlns=\"{RRDP_XMLNS}\" version=\"1\" session_id=\"{sid}\" serial=\"1\"><publish uri=\"rsync://example.net/repo/a.roa\">{content}</publish></snapshot>"
|
||||||
|
);
|
||||||
|
let error = super::snapshot_apply::apply_snapshot(
|
||||||
|
&store,
|
||||||
|
"https://example.net/notification.xml",
|
||||||
|
None,
|
||||||
|
xml.as_bytes(),
|
||||||
|
sid,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
.expect_err("an entity must not disappear and turn invalid base64 into valid data");
|
||||||
|
assert!(matches!(
|
||||||
|
error,
|
||||||
|
RrdpSyncError::Rrdp(RrdpError::PublishBase64(_))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sync_from_notification_snapshot_rejects_cross_source_owner_conflict() {
|
fn sync_from_notification_snapshot_rejects_cross_source_owner_conflict() {
|
||||||
let tmp = tempfile::tempdir().expect("tempdir");
|
let tmp = tempfile::tempdir().expect("tempdir");
|
||||||
@ -177,7 +202,10 @@ fn sync_from_notification_snapshot_applies_snapshot_and_stores_state() {
|
|||||||
.get_repository_view_entry("rsync://example.net/repo/a.mft")
|
.get_repository_view_entry("rsync://example.net/repo/a.mft")
|
||||||
.expect("get repository view")
|
.expect("get repository view")
|
||||||
.expect("repository view exists");
|
.expect("repository view exists");
|
||||||
assert_eq!(view.state, crate::repository::storage::RepositoryViewState::Present);
|
assert_eq!(
|
||||||
|
view.state,
|
||||||
|
crate::repository::storage::RepositoryViewState::Present
|
||||||
|
);
|
||||||
assert_eq!(view.repository_source.as_deref(), Some(notif_uri));
|
assert_eq!(view.repository_source.as_deref(), Some(notif_uri));
|
||||||
|
|
||||||
let current_bytes = store
|
let current_bytes = store
|
||||||
@ -202,7 +230,10 @@ fn sync_from_notification_snapshot_applies_snapshot_and_stores_state() {
|
|||||||
.expect("get owner")
|
.expect("get owner")
|
||||||
.expect("owner exists");
|
.expect("owner exists");
|
||||||
assert_eq!(owner.notify_uri, notif_uri);
|
assert_eq!(owner.notify_uri, notif_uri);
|
||||||
assert_eq!(owner.owner_state, crate::repository::storage::RrdpUriOwnerState::Active);
|
assert_eq!(
|
||||||
|
owner.owner_state,
|
||||||
|
crate::repository::storage::RrdpUriOwnerState::Active
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -26,17 +26,25 @@ fn unique_rrdp_repos_from_publication_points(
|
|||||||
|
|
||||||
fn print_summary_from_shared(validation_time: time::OffsetDateTime, shared: &PostValidationShared) {
|
fn print_summary_from_shared(validation_time: time::OffsetDateTime, shared: &PostValidationShared) {
|
||||||
let warning_count = shared.tree_warnings.len()
|
let warning_count = shared.tree_warnings.len()
|
||||||
+ shared.publication_points.iter().map(|pp| pp.warnings.len()).sum::<usize>();
|
+ shared
|
||||||
crate::logging::emit(crate::logging::Level::Info, "validation_summary", || serde_json::json!({
|
.publication_points
|
||||||
|
.iter()
|
||||||
|
.map(|pp| pp.warnings.len())
|
||||||
|
.sum::<usize>();
|
||||||
|
crate::logging::emit(crate::logging::Level::Info, "validation_summary", || {
|
||||||
|
serde_json::json!({
|
||||||
"validation_time": validation_time.unix_timestamp(),
|
"validation_time": validation_time.unix_timestamp(),
|
||||||
"publication_points": shared.instances_processed, "failed": shared.instances_failed,
|
"publication_points": shared.instances_processed, "failed": shared.instances_failed,
|
||||||
"repositories": unique_rrdp_repos_from_publication_points(&shared.publication_points),
|
"repositories": unique_rrdp_repos_from_publication_points(&shared.publication_points),
|
||||||
"vrps": shared.vrps.len(), "aspas": shared.aspas.len(), "warnings": warning_count,
|
"vrps": shared.vrps.len(), "aspas": shared.aspas.len(), "warnings": warning_count,
|
||||||
}));
|
})
|
||||||
|
});
|
||||||
if warning_count > 0 {
|
if warning_count > 0 {
|
||||||
crate::logging::emit(crate::logging::Level::Warn, "validation_warnings", || serde_json::json!({
|
crate::logging::emit(crate::logging::Level::Warn, "validation_warnings", || {
|
||||||
|
serde_json::json!({
|
||||||
"count": warning_count, "failed_publication_points": shared.instances_failed,
|
"count": warning_count, "failed_publication_points": shared.instances_failed,
|
||||||
}));
|
})
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,7 +105,6 @@ impl PostValidationShared {
|
|||||||
ccr_accumulator,
|
ccr_accumulator,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|||||||
@ -18,15 +18,31 @@ where
|
|||||||
// One scheduler entry preserves explicit TAL identities for one or many roots.
|
// One scheduler entry preserves explicit TAL identities for one or many roots.
|
||||||
if let Some(t) = timing {
|
if let Some(t) = timing {
|
||||||
run_tree_from_multiple_tals_parallel_phase2_audit_with_timing(
|
run_tree_from_multiple_tals_parallel_phase2_audit_with_timing(
|
||||||
store, policy, args.tal_inputs.clone(), http, rsync, validation_time,
|
store,
|
||||||
config, args.parallel_phase1_config.clone(), args.parallel_phase2_config.clone(),
|
policy,
|
||||||
collect_current_repo_objects, t,
|
args.tal_inputs.clone(),
|
||||||
|
http,
|
||||||
|
rsync,
|
||||||
|
validation_time,
|
||||||
|
config,
|
||||||
|
args.parallel_phase1_config.clone(),
|
||||||
|
args.parallel_phase2_config.clone(),
|
||||||
|
collect_current_repo_objects,
|
||||||
|
t,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
run_tree_from_multiple_tals_parallel_phase2_audit(
|
run_tree_from_multiple_tals_parallel_phase2_audit(
|
||||||
store, policy, args.tal_inputs.clone(), http, rsync, validation_time,
|
store,
|
||||||
config, args.parallel_phase1_config.clone(), args.parallel_phase2_config.clone(),
|
policy,
|
||||||
|
args.tal_inputs.clone(),
|
||||||
|
http,
|
||||||
|
rsync,
|
||||||
|
validation_time,
|
||||||
|
config,
|
||||||
|
args.parallel_phase1_config.clone(),
|
||||||
|
args.parallel_phase2_config.clone(),
|
||||||
collect_current_repo_objects,
|
collect_current_repo_objects,
|
||||||
)
|
)
|
||||||
}.map_err(|error| error.to_string())
|
}
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,11 @@ pub(crate) fn run_config(args: RunConfig) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
policy.ta_constraints = args.ta_constraints.clone();
|
policy.ta_constraints = args.ta_constraints.clone();
|
||||||
for warning in policy.ta_constraints.configuration_warnings() {
|
for warning in policy.ta_constraints.configuration_warnings() {
|
||||||
crate::logging::emit(crate::logging::Level::Warn, "constraint_warning", || serde_json::json!({"reason": warning}));
|
crate::logging::emit(
|
||||||
|
crate::logging::Level::Warn,
|
||||||
|
"constraint_warning",
|
||||||
|
|| serde_json::json!({"reason": warning}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let validation_time = args
|
let validation_time = args
|
||||||
.validation_time
|
.validation_time
|
||||||
@ -45,8 +49,7 @@ pub(crate) fn run_config(args: RunConfig) -> Result<(), String> {
|
|||||||
let config = TreeRunConfig {
|
let config = TreeRunConfig {
|
||||||
max_depth: Some(args.max_ca_depth),
|
max_depth: Some(args.max_ca_depth),
|
||||||
max_instances: args.max_instances,
|
max_instances: args.max_instances,
|
||||||
compact_audit: args.skip_report_build
|
compact_audit: args.skip_report_build && args.report_json_path.is_none(),
|
||||||
&& args.report_json_path.is_none(),
|
|
||||||
build_ccr_accumulator: args.ccr_out_path.is_some(),
|
build_ccr_accumulator: args.ccr_out_path.is_some(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -157,7 +160,11 @@ pub(crate) fn run_config(args: RunConfig) -> Result<(), String> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let validation_ms = validation_started.elapsed().as_millis() as u64;
|
let validation_ms = validation_started.elapsed().as_millis() as u64;
|
||||||
crate::logging::emit(crate::logging::Level::Info, "validation_phase_completed", || serde_json::json!({"elapsed_ms": validation_ms}));
|
crate::logging::emit(
|
||||||
|
crate::logging::Level::Info,
|
||||||
|
"validation_phase_completed",
|
||||||
|
|| serde_json::json!({"elapsed_ms": validation_ms}),
|
||||||
|
);
|
||||||
let shared = PostValidationShared::from_run_output(out);
|
let shared = PostValidationShared::from_run_output(out);
|
||||||
record_memory_checkpoint(
|
record_memory_checkpoint(
|
||||||
&mut memory_checkpoints,
|
&mut memory_checkpoints,
|
||||||
@ -222,8 +229,7 @@ pub(crate) fn run_config(args: RunConfig) -> Result<(), String> {
|
|||||||
.compare_view_trust_anchor
|
.compare_view_trust_anchor
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.unwrap_or("unknown");
|
.unwrap_or("unknown");
|
||||||
let (report_result, ccr_result, compare_view_result) =
|
let (report_result, ccr_result, compare_view_result) = std::thread::scope(|scope| {
|
||||||
std::thread::scope(|scope| {
|
|
||||||
// Reborrow `shared` as a plain reference so the scoped output tasks
|
// Reborrow `shared` as a plain reference so the scoped output tasks
|
||||||
// capture the reference instead of moving fields out of the owned value.
|
// capture the reference instead of moving fields out of the owned value.
|
||||||
let shared = &shared;
|
let shared = &shared;
|
||||||
@ -240,13 +246,8 @@ pub(crate) fn run_config(args: RunConfig) -> Result<(), String> {
|
|||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
let ccr_handle = scope.spawn(|| {
|
let ccr_handle =
|
||||||
run_ccr_task(
|
scope.spawn(|| run_ccr_task(shared, args.ccr_out_path.as_deref(), ccr_produced_at));
|
||||||
shared,
|
|
||||||
args.ccr_out_path.as_deref(),
|
|
||||||
ccr_produced_at,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let compare_view_handle = scope.spawn(|| {
|
let compare_view_handle = scope.spawn(|| {
|
||||||
run_compare_view_task(
|
run_compare_view_task(
|
||||||
shared,
|
shared,
|
||||||
@ -374,9 +375,7 @@ pub(crate) fn run_config(args: RunConfig) -> Result<(), String> {
|
|||||||
// the transport/validation stages rather than by the compact publication
|
// the transport/validation stages rather than by the compact publication
|
||||||
// point audit, so the final summary can use those authoritative counts.
|
// point audit, so the final summary can use those authoritative counts.
|
||||||
if let Some(path) = args.summary_out_path.as_deref() {
|
if let Some(path) = args.summary_out_path.as_deref() {
|
||||||
let analysis_counts = timing
|
let analysis_counts = timing.as_ref().map(|(_, handle)| handle.counts_snapshot());
|
||||||
.as_ref()
|
|
||||||
.map(|(_, handle)| handle.counts_snapshot());
|
|
||||||
write_summary(
|
write_summary(
|
||||||
path,
|
path,
|
||||||
&shared,
|
&shared,
|
||||||
@ -458,9 +457,7 @@ fn write_summary(
|
|||||||
summary.synchronized_objects += point.objects.len();
|
summary.synchronized_objects += point.objects.len();
|
||||||
match point.repo_sync_phase.as_deref() {
|
match point.repo_sync_phase.as_deref() {
|
||||||
Some(phase) if phase.contains("fallback") => summary.rrdp_snapshot_fallbacks += 1,
|
Some(phase) if phase.contains("fallback") => summary.rrdp_snapshot_fallbacks += 1,
|
||||||
Some(phase) if phase.contains("snapshot") => {
|
Some(phase) if phase.contains("snapshot") => summary.rrdp_snapshot_repositories += 1,
|
||||||
summary.rrdp_snapshot_repositories += 1
|
|
||||||
}
|
|
||||||
Some(phase) if phase.contains("delta") => summary.rrdp_delta_repositories += 1,
|
Some(phase) if phase.contains("delta") => summary.rrdp_delta_repositories += 1,
|
||||||
Some(phase) if phase.contains("noop") => summary.rrdp_noop_repositories += 1,
|
Some(phase) if phase.contains("noop") => summary.rrdp_noop_repositories += 1,
|
||||||
_ => {}
|
_ => {}
|
||||||
@ -469,13 +466,25 @@ fn write_summary(
|
|||||||
let accepted = object.result == AuditObjectResult::Ok;
|
let accepted = object.result == AuditObjectResult::Ok;
|
||||||
match &object.kind {
|
match &object.kind {
|
||||||
AuditObjectKind::Manifest => {
|
AuditObjectKind::Manifest => {
|
||||||
if accepted { summary.validated_manifests += 1; } else { summary.rejected_manifests += 1; }
|
if accepted {
|
||||||
|
summary.validated_manifests += 1;
|
||||||
|
} else {
|
||||||
|
summary.rejected_manifests += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AuditObjectKind::Crl => {
|
AuditObjectKind::Crl => {
|
||||||
if accepted { summary.validated_crls += 1; } else { summary.rejected_crls += 1; }
|
if accepted {
|
||||||
|
summary.validated_crls += 1;
|
||||||
|
} else {
|
||||||
|
summary.rejected_crls += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AuditObjectKind::Roa => {
|
AuditObjectKind::Roa => {
|
||||||
if accepted { summary.validated_roas += 1; } else { summary.rejected_roas += 1; }
|
if accepted {
|
||||||
|
summary.validated_roas += 1;
|
||||||
|
} else {
|
||||||
|
summary.rejected_roas += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@ -522,7 +531,9 @@ fn write_summary(
|
|||||||
.and_then(|counts| {
|
.and_then(|counts| {
|
||||||
let snapshot = counts.get("rrdp_snapshot_objects_applied_total").copied();
|
let snapshot = counts.get("rrdp_snapshot_objects_applied_total").copied();
|
||||||
let delta = counts.get("rrdp_delta_ops_applied_total").copied();
|
let delta = counts.get("rrdp_delta_ops_applied_total").copied();
|
||||||
snapshot.zip(delta).map(|(snapshot, delta)| snapshot + delta)
|
snapshot
|
||||||
|
.zip(delta)
|
||||||
|
.map(|(snapshot, delta)| snapshot + delta)
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
shared
|
shared
|
||||||
@ -538,8 +549,12 @@ fn write_summary(
|
|||||||
summary.synchronized_repositories = notification_uris.len();
|
summary.synchronized_repositories = notification_uris.len();
|
||||||
|
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
std::fs::create_dir_all(parent)
|
std::fs::create_dir_all(parent).map_err(|error| {
|
||||||
.map_err(|error| format!("create Panda RPKI summary parent {}: {error}", parent.display()))?;
|
format!(
|
||||||
|
"create Panda RPKI summary parent {}: {error}",
|
||||||
|
parent.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
let bytes = serde_json::to_vec_pretty(&summary)
|
let bytes = serde_json::to_vec_pretty(&summary)
|
||||||
.map_err(|error| format!("serialize Panda RPKI summary: {error}"))?;
|
.map_err(|error| format!("serialize Panda RPKI summary: {error}"))?;
|
||||||
|
|||||||
@ -64,5 +64,4 @@ impl<E: RepoTransportExecutor> RepoSyncRuntime for Phase1RepoSyncRuntime<E> {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,5 +74,4 @@ pub trait RepoSyncRuntime: Send + Sync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn reset_run_state(&self) -> Result<(), String>;
|
fn reset_run_state(&self) -> Result<(), String>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -92,7 +92,8 @@ impl TransportStateTables {
|
|||||||
return match record.state {
|
return match record.state {
|
||||||
RepoRuntimeState::WaitingRrdp => {
|
RepoRuntimeState::WaitingRrdp => {
|
||||||
if let Some(key) = record.rrdp_notification_key.as_ref()
|
if let Some(key) = record.rrdp_notification_key.as_ref()
|
||||||
&& let Some(entry) = self.rrdp_inflight.get_mut(key) {
|
&& let Some(entry) = self.rrdp_inflight.get_mut(key)
|
||||||
|
{
|
||||||
entry.waiting_requesters.push(requester);
|
entry.waiting_requesters.push(requester);
|
||||||
}
|
}
|
||||||
TransportRequestAction::Waiting {
|
TransportRequestAction::Waiting {
|
||||||
@ -129,7 +130,8 @@ impl TransportStateTables {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if sync_preference == SyncPreference::RrdpThenRsync
|
if sync_preference == SyncPreference::RrdpThenRsync
|
||||||
&& let Some(notification_uri) = identity.notification_uri.clone() {
|
&& let Some(notification_uri) = identity.notification_uri.clone()
|
||||||
|
{
|
||||||
if let Some(entry) = self.rrdp_inflight.get_mut(¬ification_uri) {
|
if let Some(entry) = self.rrdp_inflight.get_mut(¬ification_uri) {
|
||||||
if let Some(result) = entry.last_result.clone() {
|
if let Some(result) = entry.last_result.clone() {
|
||||||
return match result.result {
|
return match result.result {
|
||||||
@ -496,7 +498,8 @@ impl TransportStateTables {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
if let Some(failure_scope_uri) = record.rsync_failure_scope_key.as_ref()
|
if let Some(failure_scope_uri) = record.rsync_failure_scope_key.as_ref()
|
||||||
&& !rsync_failure_scope_reachable.contains(failure_scope_uri) {
|
&& !rsync_failure_scope_reachable.contains(failure_scope_uri)
|
||||||
|
{
|
||||||
rsync_failure_probe_inflight.insert(failure_scope_uri.clone(), rsync_scope_uri);
|
rsync_failure_probe_inflight.insert(failure_scope_uri.clone(), rsync_scope_uri);
|
||||||
}
|
}
|
||||||
record.state = RepoRuntimeState::WaitingRsync;
|
record.state = RepoRuntimeState::WaitingRsync;
|
||||||
@ -686,7 +689,8 @@ impl TransportStateTables {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(failure_scope_uri) = result.rsync_failure_scope_uri.as_ref()
|
if let Some(failure_scope_uri) = result.rsync_failure_scope_uri.as_ref()
|
||||||
&& reusable_failure_scope.as_deref() != Some(failure_scope_uri.as_str()) {
|
&& reusable_failure_scope.as_deref() != Some(failure_scope_uri.as_str())
|
||||||
|
{
|
||||||
let mut follow_up_tasks = Vec::new();
|
let mut follow_up_tasks = Vec::new();
|
||||||
for record in self.runtime_records.values_mut() {
|
for record in self.runtime_records.values_mut() {
|
||||||
if record.rsync_scope_key != *rsync_scope_uri
|
if record.rsync_scope_key != *rsync_scope_uri
|
||||||
|
|||||||
@ -59,7 +59,9 @@ impl<H: Fetcher + 'static> RepoTransportExecutor for LiveRrdpTransportExecutor<H
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
.expect("rrdp transport requires notification uri");
|
.expect("rrdp transport requires notification uri");
|
||||||
let sync_result = if task.retry_short_timeout {
|
let sync_result = if task.retry_short_timeout {
|
||||||
crate::repository::fetch::http::with_scoped_http_timeout_override(RETRY_SHORT_TIMEOUT, || {
|
crate::repository::fetch::http::with_scoped_http_timeout_override(
|
||||||
|
RETRY_SHORT_TIMEOUT,
|
||||||
|
|| {
|
||||||
run_rrdp_transport(
|
run_rrdp_transport(
|
||||||
self.store.as_ref(),
|
self.store.as_ref(),
|
||||||
notification_uri,
|
notification_uri,
|
||||||
@ -68,7 +70,8 @@ impl<H: Fetcher + 'static> RepoTransportExecutor for LiveRrdpTransportExecutor<H
|
|||||||
self.timing.as_ref(),
|
self.timing.as_ref(),
|
||||||
self.download_log.as_ref(),
|
self.download_log.as_ref(),
|
||||||
)
|
)
|
||||||
})
|
},
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
run_rrdp_transport(
|
run_rrdp_transport(
|
||||||
self.store.as_ref(),
|
self.store.as_ref(),
|
||||||
@ -93,8 +96,7 @@ impl<H: Fetcher + 'static> RepoTransportExecutor for LiveRrdpTransportExecutor<H
|
|||||||
warnings: Vec::new(),
|
warnings: Vec::new(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => RepoTransportResultEnvelope {
|
||||||
RepoTransportResultEnvelope {
|
|
||||||
dedup_key: task.dedup_key,
|
dedup_key: task.dedup_key,
|
||||||
rsync_failure_scope_uri: task.rsync_failure_scope_uri,
|
rsync_failure_scope_uri: task.rsync_failure_scope_uri,
|
||||||
repo_identity: task.repo_identity,
|
repo_identity: task.repo_identity,
|
||||||
@ -107,8 +109,7 @@ impl<H: Fetcher + 'static> RepoTransportExecutor for LiveRrdpTransportExecutor<H
|
|||||||
warnings: Vec::new(),
|
warnings: Vec::new(),
|
||||||
error_class: RepoTransportErrorClass::Unknown,
|
error_class: RepoTransportErrorClass::Unknown,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -187,8 +188,7 @@ impl<R: RsyncFetcher + 'static> RepoTransportExecutor for LiveRsyncTransportExec
|
|||||||
warnings: Vec::new(),
|
warnings: Vec::new(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => RepoTransportResultEnvelope {
|
||||||
RepoTransportResultEnvelope {
|
|
||||||
dedup_key: task.dedup_key,
|
dedup_key: task.dedup_key,
|
||||||
rsync_failure_scope_uri: task.rsync_failure_scope_uri,
|
rsync_failure_scope_uri: task.rsync_failure_scope_uri,
|
||||||
repo_identity: task.repo_identity,
|
repo_identity: task.repo_identity,
|
||||||
@ -201,8 +201,7 @@ impl<R: RsyncFetcher + 'static> RepoTransportExecutor for LiveRsyncTransportExec
|
|||||||
warnings: Vec::new(),
|
warnings: Vec::new(),
|
||||||
error_class: RepoTransportErrorClass::Unknown,
|
error_class: RepoTransportErrorClass::Unknown,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -100,7 +100,8 @@ impl<E: RepoSyncExecutor> RepoWorkerPool<E> {
|
|||||||
let mut first_err: Option<String> = None;
|
let mut first_err: Option<String> = None;
|
||||||
for handle in self.workers.drain(..) {
|
for handle in self.workers.drain(..) {
|
||||||
if let Err(e) = handle.join()
|
if let Err(e) = handle.join()
|
||||||
&& first_err.is_none() {
|
&& first_err.is_none()
|
||||||
|
{
|
||||||
first_err = Some(format!("join repo worker failed: {e:?}"));
|
first_err = Some(format!("join repo worker failed: {e:?}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -187,7 +188,8 @@ impl<E: RepoTransportExecutor> RepoTransportWorkerPool<E> {
|
|||||||
let mut first_err: Option<String> = None;
|
let mut first_err: Option<String> = None;
|
||||||
for handle in self.workers.drain(..) {
|
for handle in self.workers.drain(..) {
|
||||||
if let Err(e) = handle.join()
|
if let Err(e) = handle.join()
|
||||||
&& first_err.is_none() {
|
&& first_err.is_none()
|
||||||
|
{
|
||||||
first_err = Some(format!("join repo transport worker failed: {e:?}"));
|
first_err = Some(format!("join repo transport worker failed: {e:?}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
// Interval indexes and trust-anchor resource constraint evaluation.
|
// Interval indexes and trust-anchor resource constraint evaluation.
|
||||||
|
|
||||||
|
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
@ -215,7 +214,6 @@ impl TaConstraintsByTal {
|
|||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn adjacent_constraints_path(source: &TalSource) -> Option<PathBuf> {
|
fn adjacent_constraints_path(source: &TalSource) -> Option<PathBuf> {
|
||||||
|
|||||||
@ -24,7 +24,6 @@ fn verify_child_signature(
|
|||||||
child: &X509Certificate<'_>,
|
child: &X509Certificate<'_>,
|
||||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||||
) -> Result<(), CaPathError> {
|
) -> Result<(), CaPathError> {
|
||||||
|
|
||||||
child
|
child
|
||||||
.verify_signature(Some(issuer_spki))
|
.verify_signature(Some(issuer_spki))
|
||||||
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))
|
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))
|
||||||
|
|||||||
@ -3,10 +3,7 @@
|
|||||||
fn ip_resources_by_afi_items(
|
fn ip_resources_by_afi_items(
|
||||||
set: &IpResourceSet,
|
set: &IpResourceSet,
|
||||||
) -> Result<
|
) -> Result<
|
||||||
std::collections::BTreeMap<
|
std::collections::BTreeMap<crate::model::rc::Afi, Vec<crate::model::rc::IpAddressOrRange>>,
|
||||||
crate::model::rc::Afi,
|
|
||||||
Vec<crate::model::rc::IpAddressOrRange>,
|
|
||||||
>,
|
|
||||||
CaPathError,
|
CaPathError,
|
||||||
> {
|
> {
|
||||||
let mut m: std::collections::BTreeMap<
|
let mut m: std::collections::BTreeMap<
|
||||||
@ -96,9 +93,7 @@ fn ip_items_to_merged_intervals(
|
|||||||
let mut intervals = Vec::new();
|
let mut intervals = Vec::new();
|
||||||
for item in items {
|
for item in items {
|
||||||
match item {
|
match item {
|
||||||
crate::model::rc::IpAddressOrRange::Prefix(p) => {
|
crate::model::rc::IpAddressOrRange::Prefix(p) => intervals.push(prefix_to_range(p)),
|
||||||
intervals.push(prefix_to_range(p))
|
|
||||||
}
|
|
||||||
crate::model::rc::IpAddressOrRange::Range(r) => {
|
crate::model::rc::IpAddressOrRange::Range(r) => {
|
||||||
intervals.push((r.min.clone(), r.max.clone()))
|
intervals.push((r.min.clone(), r.max.clone()))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,8 +65,8 @@ fn validate_manifest_embedded_ee_cert_path(
|
|||||||
if !rem.is_empty() {
|
if !rem.is_empty() {
|
||||||
return Err(CertPathError::IssuerSpkiTrailingBytes(rem.len()).into());
|
return Err(CertPathError::IssuerSpkiTrailingBytes(rem.len()).into());
|
||||||
}
|
}
|
||||||
let issuer_crl = crate::model::crl::RpkixCrl::decode_der(crl_bytes)
|
let issuer_crl =
|
||||||
.map_err(CertPathError::from)?;
|
crate::model::crl::RpkixCrl::decode_der(crl_bytes).map_err(CertPathError::from)?;
|
||||||
let revoked_serials = issuer_crl
|
let revoked_serials = issuer_crl
|
||||||
.revoked_certs
|
.revoked_certs
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@ -502,9 +502,7 @@ pub(crate) fn try_build_fresh_publication_point_with_timing(
|
|||||||
// - If manifestNumber is higher, require thisUpdate to be more recent than the previously
|
// - If manifestNumber is higher, require thisUpdate to be more recent than the previously
|
||||||
// validated thisUpdate.
|
// validated thisUpdate.
|
||||||
let anti_rollback_started = std::time::Instant::now();
|
let anti_rollback_started = std::time::Instant::now();
|
||||||
if let Some(old_meta) = store
|
if let Some(old_meta) = store.get_manifest_anti_rollback_meta(manifest_rsync_uri)? {
|
||||||
.get_manifest_anti_rollback_meta(manifest_rsync_uri)?
|
|
||||||
{
|
|
||||||
timing.anti_rollback_meta_hit = true;
|
timing.anti_rollback_meta_hit = true;
|
||||||
if old_meta.manifest_rsync_uri == manifest_rsync_uri {
|
if old_meta.manifest_rsync_uri == manifest_rsync_uri {
|
||||||
let new_num = manifest.manifest.manifest_number.bytes_be.as_slice();
|
let new_num = manifest.manifest.manifest_number.bytes_be.as_slice();
|
||||||
|
|||||||
@ -16,17 +16,47 @@ fn process_roa_with_issuer(
|
|||||||
ta_constraints: Option<&crate::ta_constraints::TaConstraints>,
|
ta_constraints: Option<&crate::ta_constraints::TaConstraints>,
|
||||||
) -> Result<Vec<Vrp>, ObjectValidateError> {
|
) -> Result<Vec<Vrp>, ObjectValidateError> {
|
||||||
let roa = {
|
let roa = {
|
||||||
let _span = timing.as_ref().map(|t| t.span_phase("objects_roa_decode_and_validate_total"));
|
let _span = timing
|
||||||
RoaObject::decode_der_with_strict_options(file.bytes().map_err(ObjectValidateError::BytesLoad)?, strict_cms_der, strict_name)?
|
.as_ref()
|
||||||
|
.map(|t| t.span_phase("objects_roa_decode_and_validate_total"));
|
||||||
|
RoaObject::decode_der_with_strict_options(
|
||||||
|
file.bytes().map_err(ObjectValidateError::BytesLoad)?,
|
||||||
|
strict_cms_der,
|
||||||
|
strict_name,
|
||||||
|
)?
|
||||||
};
|
};
|
||||||
roa.validate_embedded_ee_cert()?;
|
roa.validate_embedded_ee_cert()?;
|
||||||
roa.signed_object.verify()?;
|
roa.signed_object.verify()?;
|
||||||
let ee = &roa.signed_object.signed_data.certificates[0];
|
let ee = &roa.signed_object.signed_data.certificates[0];
|
||||||
let crl_uri = choose_crl_uri_for_certificate(ee.resource_cert.tbs.extensions.crl_distribution_points_uris.as_ref(), crl_states)?;
|
let crl_uri = choose_crl_uri_for_certificate(
|
||||||
|
ee.resource_cert
|
||||||
|
.tbs
|
||||||
|
.extensions
|
||||||
|
.crl_distribution_points_uris
|
||||||
|
.as_ref(),
|
||||||
|
crl_states,
|
||||||
|
)?;
|
||||||
let verified_crl = ensure_issuer_crl_verified(crl_uri, crl_states, issuer_ca_der)?;
|
let verified_crl = ensure_issuer_crl_verified(crl_uri, crl_states, issuer_ca_der)?;
|
||||||
validate_signed_object_ee_cert_path_fast(ee, issuer_ca, issuer_spki, &verified_crl.crl, &verified_crl.revoked_serials, issuer_ca_rsync_uri, Some(crl_uri), validation_time)?;
|
validate_signed_object_ee_cert_path_fast(
|
||||||
let ee_vrs = validate_ee_resources_for_mode(&ee.resource_cert, issuer_effective_ip, issuer_effective_as, issuer_resources_index, resource_validation_mode)?;
|
ee,
|
||||||
if let Some(constraints) = ta_constraints { constraints.validate_ee_certificate(&ee.resource_cert)?; }
|
issuer_ca,
|
||||||
|
issuer_spki,
|
||||||
|
&verified_crl.crl,
|
||||||
|
&verified_crl.revoked_serials,
|
||||||
|
issuer_ca_rsync_uri,
|
||||||
|
Some(crl_uri),
|
||||||
|
validation_time,
|
||||||
|
)?;
|
||||||
|
let ee_vrs = validate_ee_resources_for_mode(
|
||||||
|
&ee.resource_cert,
|
||||||
|
issuer_effective_ip,
|
||||||
|
issuer_effective_as,
|
||||||
|
issuer_resources_index,
|
||||||
|
resource_validation_mode,
|
||||||
|
)?;
|
||||||
|
if let Some(constraints) = ta_constraints {
|
||||||
|
constraints.validate_ee_certificate(&ee.resource_cert)?;
|
||||||
|
}
|
||||||
roa_to_vrps_with_vrs(&roa, ee_vrs.ip.as_ref())
|
roa_to_vrps_with_vrs(&roa, ee_vrs.ip.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,21 +78,52 @@ fn process_roa_with_issuer_parallel(
|
|||||||
ta_constraints: Option<&crate::ta_constraints::TaConstraints>,
|
ta_constraints: Option<&crate::ta_constraints::TaConstraints>,
|
||||||
) -> Result<Vec<Vrp>, ObjectValidateError> {
|
) -> Result<Vec<Vrp>, ObjectValidateError> {
|
||||||
let roa = {
|
let roa = {
|
||||||
let _span = timing.as_ref().map(|t| t.span_phase("objects_roa_decode_and_validate_total"));
|
let _span = timing
|
||||||
RoaObject::decode_der_with_strict_options(file.bytes().map_err(ObjectValidateError::BytesLoad)?, strict_cms_der, strict_name)?
|
.as_ref()
|
||||||
|
.map(|t| t.span_phase("objects_roa_decode_and_validate_total"));
|
||||||
|
RoaObject::decode_der_with_strict_options(
|
||||||
|
file.bytes().map_err(ObjectValidateError::BytesLoad)?,
|
||||||
|
strict_cms_der,
|
||||||
|
strict_name,
|
||||||
|
)?
|
||||||
};
|
};
|
||||||
roa.validate_embedded_ee_cert()?;
|
roa.validate_embedded_ee_cert()?;
|
||||||
roa.signed_object.verify()?;
|
roa.signed_object.verify()?;
|
||||||
let ee = &roa.signed_object.signed_data.certificates[0];
|
let ee = &roa.signed_object.signed_data.certificates[0];
|
||||||
let (crl_uri, verified_crl) = {
|
let (crl_uri, verified_crl) = {
|
||||||
let mut states = crl_states.lock().expect("parallel issuer CRL state lock");
|
let mut states = crl_states.lock().expect("parallel issuer CRL state lock");
|
||||||
let uri = choose_crl_uri_for_certificate(ee.resource_cert.tbs.extensions.crl_distribution_points_uris.as_ref(), &states)?.to_string();
|
let uri = choose_crl_uri_for_certificate(
|
||||||
|
ee.resource_cert
|
||||||
|
.tbs
|
||||||
|
.extensions
|
||||||
|
.crl_distribution_points_uris
|
||||||
|
.as_ref(),
|
||||||
|
&states,
|
||||||
|
)?
|
||||||
|
.to_string();
|
||||||
let value = ensure_issuer_crl_verified(&uri, &mut states, issuer_ca_der)?;
|
let value = ensure_issuer_crl_verified(&uri, &mut states, issuer_ca_der)?;
|
||||||
(uri, value)
|
(uri, value)
|
||||||
};
|
};
|
||||||
validate_signed_object_ee_cert_path_fast(ee, issuer_ca, issuer_spki, &verified_crl.crl, &verified_crl.revoked_serials, issuer_ca_rsync_uri, Some(crl_uri.as_str()), validation_time)?;
|
validate_signed_object_ee_cert_path_fast(
|
||||||
let ee_vrs = validate_ee_resources_for_mode(&ee.resource_cert, issuer_effective_ip, issuer_effective_as, issuer_resources_index, resource_validation_mode)?;
|
ee,
|
||||||
if let Some(constraints) = ta_constraints { constraints.validate_ee_certificate(&ee.resource_cert)?; }
|
issuer_ca,
|
||||||
|
issuer_spki,
|
||||||
|
&verified_crl.crl,
|
||||||
|
&verified_crl.revoked_serials,
|
||||||
|
issuer_ca_rsync_uri,
|
||||||
|
Some(crl_uri.as_str()),
|
||||||
|
validation_time,
|
||||||
|
)?;
|
||||||
|
let ee_vrs = validate_ee_resources_for_mode(
|
||||||
|
&ee.resource_cert,
|
||||||
|
issuer_effective_ip,
|
||||||
|
issuer_effective_as,
|
||||||
|
issuer_resources_index,
|
||||||
|
resource_validation_mode,
|
||||||
|
)?;
|
||||||
|
if let Some(constraints) = ta_constraints {
|
||||||
|
constraints.validate_ee_certificate(&ee.resource_cert)?;
|
||||||
|
}
|
||||||
roa_to_vrps_with_vrs(&roa, ee_vrs.ip.as_ref())
|
roa_to_vrps_with_vrs(&roa, ee_vrs.ip.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,17 +145,50 @@ fn process_aspa_with_issuer(
|
|||||||
ta_constraints: Option<&crate::ta_constraints::TaConstraints>,
|
ta_constraints: Option<&crate::ta_constraints::TaConstraints>,
|
||||||
) -> Result<AspaAttestation, ObjectValidateError> {
|
) -> Result<AspaAttestation, ObjectValidateError> {
|
||||||
let aspa = {
|
let aspa = {
|
||||||
let _span = timing.as_ref().map(|t| t.span_phase("objects_aspa_decode_and_validate_total"));
|
let _span = timing
|
||||||
AspaObject::decode_der_with_strict_options(file.bytes().map_err(ObjectValidateError::BytesLoad)?, strict_cms_der, strict_name)?
|
.as_ref()
|
||||||
|
.map(|t| t.span_phase("objects_aspa_decode_and_validate_total"));
|
||||||
|
AspaObject::decode_der_with_strict_options(
|
||||||
|
file.bytes().map_err(ObjectValidateError::BytesLoad)?,
|
||||||
|
strict_cms_der,
|
||||||
|
strict_name,
|
||||||
|
)?
|
||||||
};
|
};
|
||||||
aspa.validate_embedded_ee_cert()?;
|
aspa.validate_embedded_ee_cert()?;
|
||||||
aspa.signed_object.verify()?;
|
aspa.signed_object.verify()?;
|
||||||
let ee = &aspa.signed_object.signed_data.certificates[0];
|
let ee = &aspa.signed_object.signed_data.certificates[0];
|
||||||
let crl_uri = choose_crl_uri_for_certificate(ee.resource_cert.tbs.extensions.crl_distribution_points_uris.as_ref(), crl_states)?;
|
let crl_uri = choose_crl_uri_for_certificate(
|
||||||
|
ee.resource_cert
|
||||||
|
.tbs
|
||||||
|
.extensions
|
||||||
|
.crl_distribution_points_uris
|
||||||
|
.as_ref(),
|
||||||
|
crl_states,
|
||||||
|
)?;
|
||||||
let verified_crl = ensure_issuer_crl_verified(crl_uri, crl_states, issuer_ca_der)?;
|
let verified_crl = ensure_issuer_crl_verified(crl_uri, crl_states, issuer_ca_der)?;
|
||||||
validate_signed_object_ee_cert_path_fast(ee, issuer_ca, issuer_spki, &verified_crl.crl, &verified_crl.revoked_serials, issuer_ca_rsync_uri, Some(crl_uri), validation_time)?;
|
validate_signed_object_ee_cert_path_fast(
|
||||||
let ee_vrs = validate_ee_resources_for_mode(&ee.resource_cert, issuer_effective_ip, issuer_effective_as, issuer_resources_index, resource_validation_mode)?;
|
ee,
|
||||||
if let Some(constraints) = ta_constraints { constraints.validate_ee_certificate(&ee.resource_cert)?; }
|
issuer_ca,
|
||||||
|
issuer_spki,
|
||||||
|
&verified_crl.crl,
|
||||||
|
&verified_crl.revoked_serials,
|
||||||
|
issuer_ca_rsync_uri,
|
||||||
|
Some(crl_uri),
|
||||||
|
validation_time,
|
||||||
|
)?;
|
||||||
|
let ee_vrs = validate_ee_resources_for_mode(
|
||||||
|
&ee.resource_cert,
|
||||||
|
issuer_effective_ip,
|
||||||
|
issuer_effective_as,
|
||||||
|
issuer_resources_index,
|
||||||
|
resource_validation_mode,
|
||||||
|
)?;
|
||||||
|
if let Some(constraints) = ta_constraints {
|
||||||
|
constraints.validate_ee_certificate(&ee.resource_cert)?;
|
||||||
|
}
|
||||||
validate_aspa_customer_in_vrs(&aspa, ee_vrs.asn.as_ref())?;
|
validate_aspa_customer_in_vrs(&aspa, ee_vrs.asn.as_ref())?;
|
||||||
Ok(AspaAttestation { customer_as_id: aspa.aspa.customer_as_id, provider_as_ids: aspa.aspa.provider_as_ids.clone() })
|
Ok(AspaAttestation {
|
||||||
|
customer_as_id: aspa.aspa.customer_as_id,
|
||||||
|
provider_as_ids: aspa.aspa.provider_as_ids.clone(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,21 +2,33 @@
|
|||||||
|
|
||||||
const RFC_NONE: &[RfcRef] = &[];
|
const RFC_NONE: &[RfcRef] = &[];
|
||||||
const RFC_CRLDP: &[RfcRef] = &[RfcRef("RFC 6487 §4.8.6")];
|
const RFC_CRLDP: &[RfcRef] = &[RfcRef("RFC 6487 §4.8.6")];
|
||||||
const RFC_CRLDP_AND_LOCKED_PACK: &[RfcRef] = &[RfcRef("RFC 6487 §4.8.6"), RfcRef("RFC 9286 §4.2.1")];
|
const RFC_CRLDP_AND_LOCKED_PACK: &[RfcRef] =
|
||||||
|
&[RfcRef("RFC 6487 §4.8.6"), RfcRef("RFC 9286 §4.2.1")];
|
||||||
|
|
||||||
fn ber_compatible_cms_warning(der: &[u8], rsync_uri: &str, object_kind: &str) -> Option<Warning> {
|
fn ber_compatible_cms_warning(der: &[u8], rsync_uri: &str, object_kind: &str) -> Option<Warning> {
|
||||||
let strict_error = RpkiSignedObject::strict_cms_der_error(der)?;
|
let strict_error = RpkiSignedObject::strict_cms_der_error(der)?;
|
||||||
Some(Warning::new(format!("accepted BER-compatible CMS encoding for {object_kind}: {rsync_uri}: {strict_error}"))
|
Some(
|
||||||
|
Warning::new(format!(
|
||||||
|
"accepted BER-compatible CMS encoding for {object_kind}: {rsync_uri}: {strict_error}"
|
||||||
|
))
|
||||||
.with_category(WarningCategory::BerCompatibleCmsEncoding)
|
.with_category(WarningCategory::BerCompatibleCmsEncoding)
|
||||||
.with_rfc_refs(&[RfcRef("X.690 §10"), RfcRef("RFC 6488 §2")])
|
.with_rfc_refs(&[RfcRef("X.690 §10"), RfcRef("RFC 6488 §2")])
|
||||||
.with_context(rsync_uri))
|
.with_context(rsync_uri),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
fn ber_compatible_cms_warning_for_file(file: &PackFile, object_kind: &str) -> Option<Warning> {
|
fn ber_compatible_cms_warning_for_file(file: &PackFile, object_kind: &str) -> Option<Warning> {
|
||||||
ber_compatible_cms_warning(file.bytes().ok()?, &file.rsync_uri, object_kind)
|
ber_compatible_cms_warning(file.bytes().ok()?, &file.rsync_uri, object_kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode_resource_certificate_with_policy(der: &[u8], policy: &Policy) -> Result<ResourceCertificate, crate::model::rc::ResourceCertificateDecodeError> {
|
fn decode_resource_certificate_with_policy(
|
||||||
if policy.strict.name { ResourceCertificate::decode_der_with_strict_name(der) } else { ResourceCertificate::decode_der(der) }
|
der: &[u8],
|
||||||
|
policy: &Policy,
|
||||||
|
) -> Result<ResourceCertificate, crate::model::rc::ResourceCertificateDecodeError> {
|
||||||
|
if policy.strict.name {
|
||||||
|
ResourceCertificate::decode_der_with_strict_name(der)
|
||||||
|
} else {
|
||||||
|
ResourceCertificate::decode_der(der)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@ -39,13 +51,24 @@ pub(crate) struct IssuerResourcesIndex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn extra_rfc_refs_for_crl_selection(error: &ObjectValidateError) -> &'static [RfcRef] {
|
fn extra_rfc_refs_for_crl_selection(error: &ObjectValidateError) -> &'static [RfcRef] {
|
||||||
match error { ObjectValidateError::MissingCrlDpUris => RFC_CRLDP, ObjectValidateError::CrlNotFound(_) => RFC_CRLDP_AND_LOCKED_PACK, _ => RFC_NONE }
|
match error {
|
||||||
|
ObjectValidateError::MissingCrlDpUris => RFC_CRLDP,
|
||||||
|
ObjectValidateError::CrlNotFound(_) => RFC_CRLDP_AND_LOCKED_PACK,
|
||||||
|
_ => RFC_NONE,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct Vrp { pub asn: u32, pub prefix: IpPrefix, pub max_length: u16 }
|
pub struct Vrp {
|
||||||
|
pub asn: u32,
|
||||||
|
pub prefix: IpPrefix,
|
||||||
|
pub max_length: u16,
|
||||||
|
}
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct AspaAttestation { pub customer_as_id: u32, pub provider_as_ids: Vec<u32> }
|
pub struct AspaAttestation {
|
||||||
|
pub customer_as_id: u32,
|
||||||
|
pub provider_as_ids: Vec<u32>,
|
||||||
|
}
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct RouterKeyPayload {
|
pub struct RouterKeyPayload {
|
||||||
pub as_id: u32,
|
pub as_id: u32,
|
||||||
@ -76,7 +99,9 @@ pub struct ObjectsStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct RoaTaskOk { pub(crate) vrps: Vec<Vrp> }
|
pub(crate) struct RoaTaskOk {
|
||||||
|
pub(crate) vrps: Vec<Vrp>,
|
||||||
|
}
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct RoaTaskResult {
|
pub(crate) struct RoaTaskResult {
|
||||||
pub(crate) publication_point_id: u64,
|
pub(crate) publication_point_id: u64,
|
||||||
|
|||||||
@ -88,11 +88,9 @@ pub(crate) fn prepare_publication_point_for_parallel_roa_and_ta_constraints<
|
|||||||
);
|
);
|
||||||
return ParallelObjectsPrepare::Complete(empty(stats, warnings));
|
return ParallelObjectsPrepare::Complete(empty(stats, warnings));
|
||||||
}
|
}
|
||||||
if let Some(warning) = ber_compatible_cms_warning(
|
if let Some(warning) =
|
||||||
publication_point.manifest_bytes(),
|
ber_compatible_cms_warning(publication_point.manifest_bytes(), manifest_uri, "manifest")
|
||||||
manifest_uri,
|
{
|
||||||
"manifest",
|
|
||||||
) {
|
|
||||||
warnings.push(warning);
|
warnings.push(warning);
|
||||||
}
|
}
|
||||||
let issuer_ca = match decode_resource_certificate_with_policy(issuer_ca_der, policy) {
|
let issuer_ca = match decode_resource_certificate_with_policy(issuer_ca_der, policy) {
|
||||||
@ -138,20 +136,15 @@ pub(crate) fn prepare_publication_point_for_parallel_roa_and_ta_constraints<
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|file| file.rsync_uri.ends_with(".crl"))
|
.filter(|file| file.rsync_uri.ends_with(".crl"))
|
||||||
.filter_map(|file| {
|
.filter_map(|file| {
|
||||||
file.bytes_cloned().ok().map(|bytes| {
|
file.bytes_cloned()
|
||||||
(
|
.ok()
|
||||||
file.rsync_uri.clone(),
|
.map(|bytes| (file.rsync_uri.clone(), IssuerCrlState::Pending { bytes }))
|
||||||
IssuerCrlState::Pending { bytes },
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
if crl_states.is_empty() && (stats.roa_total > 0 || stats.aspa_total > 0) {
|
if crl_states.is_empty() && (stats.roa_total > 0 || stats.aspa_total > 0) {
|
||||||
stats.publication_point_dropped = true;
|
stats.publication_point_dropped = true;
|
||||||
warnings.push(
|
warnings.push(
|
||||||
Warning::new(
|
Warning::new("dropping publication point: no CRL files in validated publication point")
|
||||||
"dropping publication point: no CRL files in validated publication point",
|
|
||||||
)
|
|
||||||
.with_rfc_refs(&[RfcRef("RFC 6487 §4.8.6"), RfcRef("RFC 9286 §7")])
|
.with_rfc_refs(&[RfcRef("RFC 6487 §4.8.6"), RfcRef("RFC 9286 §7")])
|
||||||
.with_context(manifest_uri),
|
.with_context(manifest_uri),
|
||||||
);
|
);
|
||||||
@ -217,9 +210,7 @@ pub(crate) fn reduce_parallel_roa_stage(
|
|||||||
for (index, file) in shared.locked_files.iter().enumerate() {
|
for (index, file) in shared.locked_files.iter().enumerate() {
|
||||||
if file.rsync_uri.ends_with(".roa") {
|
if file.rsync_uri.ends_with(".roa") {
|
||||||
let result = match result_iter.peek() {
|
let result = match result_iter.peek() {
|
||||||
Some(result) if result.index == index => {
|
Some(result) if result.index == index => result_iter.next().expect("peeked result"),
|
||||||
result_iter.next().expect("peeked result")
|
|
||||||
}
|
|
||||||
Some(result) => {
|
Some(result) => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"unexpected ROA task result index {} while reducing {}",
|
"unexpected ROA task result index {} while reducing {}",
|
||||||
@ -254,10 +245,7 @@ pub(crate) fn reduce_parallel_roa_stage(
|
|||||||
let mut refs = vec![RfcRef("RFC 6488 §3"), RfcRef("RFC 9582 §4-§5")];
|
let mut refs = vec![RfcRef("RFC 6488 §3"), RfcRef("RFC 9582 §4-§5")];
|
||||||
refs.extend_from_slice(extra_rfc_refs_for_crl_selection(&error));
|
refs.extend_from_slice(extra_rfc_refs_for_crl_selection(&error));
|
||||||
warnings.push(
|
warnings.push(
|
||||||
Warning::new(format!(
|
Warning::new(format!("dropping invalid ROA: {}: {error}", file.rsync_uri))
|
||||||
"dropping invalid ROA: {}: {error}",
|
|
||||||
file.rsync_uri
|
|
||||||
))
|
|
||||||
.with_rfc_refs(&refs)
|
.with_rfc_refs(&refs)
|
||||||
.with_context(&file.rsync_uri),
|
.with_context(&file.rsync_uri),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -37,8 +37,8 @@ fn ensure_issuer_crl_verified(
|
|||||||
IssuerCrlState::Verified(v) => Ok(Arc::clone(v)),
|
IssuerCrlState::Verified(v) => Ok(Arc::clone(v)),
|
||||||
IssuerCrlState::Pending { bytes } => {
|
IssuerCrlState::Pending { bytes } => {
|
||||||
let der = std::mem::take(bytes);
|
let der = std::mem::take(bytes);
|
||||||
let crl = crate::model::crl::RpkixCrl::decode_der(&der)
|
let crl =
|
||||||
.map_err(CertPathError::CrlDecode)?;
|
crate::model::crl::RpkixCrl::decode_der(&der).map_err(CertPathError::CrlDecode)?;
|
||||||
crl.verify_signature_with_issuer_certificate_der(issuer_ca_der)
|
crl.verify_signature_with_issuer_certificate_der(issuer_ca_der)
|
||||||
.map_err(CertPathError::CrlVerify)?;
|
.map_err(CertPathError::CrlVerify)?;
|
||||||
|
|
||||||
@ -227,10 +227,7 @@ fn validate_aspa_customer_in_vrs(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_resource_set_contains_asn(
|
fn as_resource_set_contains_asn(resources: &crate::model::rc::AsResourceSet, asn: u32) -> bool {
|
||||||
resources: &crate::model::rc::AsResourceSet,
|
|
||||||
asn: u32,
|
|
||||||
) -> bool {
|
|
||||||
let Some(choice) = resources.asnum.as_ref() else {
|
let Some(choice) = resources.asnum.as_ref() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@ -605,11 +602,13 @@ fn build_issuer_resources_index(
|
|||||||
|
|
||||||
if let Some(asr) = issuer_effective_as {
|
if let Some(asr) = issuer_effective_as {
|
||||||
if let Some(choice) = asr.asnum.as_ref()
|
if let Some(choice) = asr.asnum.as_ref()
|
||||||
&& !matches!(choice, AsIdentifierChoice::Inherit) {
|
&& !matches!(choice, AsIdentifierChoice::Inherit)
|
||||||
|
{
|
||||||
idx.asnum = Some(as_choice_to_merged_intervals(choice));
|
idx.asnum = Some(as_choice_to_merged_intervals(choice));
|
||||||
}
|
}
|
||||||
if let Some(choice) = asr.rdi.as_ref()
|
if let Some(choice) = asr.rdi.as_ref()
|
||||||
&& !matches!(choice, AsIdentifierChoice::Inherit) {
|
&& !matches!(choice, AsIdentifierChoice::Inherit)
|
||||||
|
{
|
||||||
idx.rdi = Some(as_choice_to_merged_intervals(choice));
|
idx.rdi = Some(as_choice_to_merged_intervals(choice));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,8 +9,15 @@ pub fn process_publication_point_for_issuer<P: PublicationPointData>(
|
|||||||
timing: Option<&TimingHandle>,
|
timing: Option<&TimingHandle>,
|
||||||
) -> ObjectsOutput {
|
) -> ObjectsOutput {
|
||||||
process_publication_point_for_issuer_with_ta_constraints(
|
process_publication_point_for_issuer_with_ta_constraints(
|
||||||
publication_point, policy, issuer_ca_der, issuer_ca_rsync_uri,
|
publication_point,
|
||||||
issuer_effective_ip, issuer_effective_as, validation_time, timing, None,
|
policy,
|
||||||
|
issuer_ca_der,
|
||||||
|
issuer_ca_rsync_uri,
|
||||||
|
issuer_effective_ip,
|
||||||
|
issuer_effective_as,
|
||||||
|
validation_time,
|
||||||
|
timing,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,30 +35,58 @@ pub fn process_publication_point_for_issuer_with_ta_constraints<P: PublicationPo
|
|||||||
let manifest_uri = publication_point.manifest_rsync_uri();
|
let manifest_uri = publication_point.manifest_rsync_uri();
|
||||||
let files = publication_point.files();
|
let files = publication_point.files();
|
||||||
let mut stats = ObjectsStats {
|
let mut stats = ObjectsStats {
|
||||||
roa_total: files.iter().filter(|f| f.rsync_uri.ends_with(".roa")).count(),
|
roa_total: files
|
||||||
aspa_total: files.iter().filter(|f| f.rsync_uri.ends_with(".asa")).count(),
|
.iter()
|
||||||
|
.filter(|f| f.rsync_uri.ends_with(".roa"))
|
||||||
|
.count(),
|
||||||
|
aspa_total: files
|
||||||
|
.iter()
|
||||||
|
.filter(|f| f.rsync_uri.ends_with(".asa"))
|
||||||
|
.count(),
|
||||||
..ObjectsStats::default()
|
..ObjectsStats::default()
|
||||||
};
|
};
|
||||||
let mut warnings = Vec::new();
|
let mut warnings = Vec::new();
|
||||||
let mut audit = Vec::new();
|
let mut audit = Vec::new();
|
||||||
let empty = |stats: ObjectsStats, warnings: Vec<Warning>, audit: Vec<ObjectAuditEntry>| ObjectsOutput {
|
let empty =
|
||||||
vrps: Vec::new(), aspas: Vec::new(), router_keys: Vec::new(), warnings, stats, audit,
|
|stats: ObjectsStats, warnings: Vec<Warning>, audit: Vec<ObjectAuditEntry>| ObjectsOutput {
|
||||||
|
vrps: Vec::new(),
|
||||||
|
aspas: Vec::new(),
|
||||||
|
router_keys: Vec::new(),
|
||||||
|
warnings,
|
||||||
|
stats,
|
||||||
|
audit,
|
||||||
};
|
};
|
||||||
if let Err(error) = ManifestObject::decode_der_with_strict_options(
|
if let Err(error) = ManifestObject::decode_der_with_strict_options(
|
||||||
publication_point.manifest_bytes(), policy.strict.cms_der, policy.strict.name,
|
publication_point.manifest_bytes(),
|
||||||
|
policy.strict.cms_der,
|
||||||
|
policy.strict.name,
|
||||||
) {
|
) {
|
||||||
stats.publication_point_dropped = true;
|
stats.publication_point_dropped = true;
|
||||||
warnings.push(Warning::new(format!("dropping publication point: manifest decode failed: {error}"))
|
warnings.push(
|
||||||
.with_rfc_refs(&[RfcRef("RFC 9286 §4"), RfcRef("RFC 9286 §6.6")]).with_context(manifest_uri));
|
Warning::new(format!(
|
||||||
|
"dropping publication point: manifest decode failed: {error}"
|
||||||
|
))
|
||||||
|
.with_rfc_refs(&[RfcRef("RFC 9286 §4"), RfcRef("RFC 9286 §6.6")])
|
||||||
|
.with_context(manifest_uri),
|
||||||
|
);
|
||||||
return empty(stats, warnings, audit);
|
return empty(stats, warnings, audit);
|
||||||
}
|
}
|
||||||
if let Some(warning) = ber_compatible_cms_warning(publication_point.manifest_bytes(), manifest_uri, "manifest") { warnings.push(warning); }
|
if let Some(warning) =
|
||||||
|
ber_compatible_cms_warning(publication_point.manifest_bytes(), manifest_uri, "manifest")
|
||||||
|
{
|
||||||
|
warnings.push(warning);
|
||||||
|
}
|
||||||
let issuer_ca = match decode_resource_certificate_with_policy(issuer_ca_der, policy) {
|
let issuer_ca = match decode_resource_certificate_with_policy(issuer_ca_der, policy) {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
stats.publication_point_dropped = true;
|
stats.publication_point_dropped = true;
|
||||||
warnings.push(Warning::new(format!("dropping publication point: issuer CA decode failed: {error}"))
|
warnings.push(
|
||||||
.with_rfc_refs(&[RfcRef("RFC 6487 §7.2"), RfcRef("RFC 5280 §6.1")]).with_context(manifest_uri));
|
Warning::new(format!(
|
||||||
|
"dropping publication point: issuer CA decode failed: {error}"
|
||||||
|
))
|
||||||
|
.with_rfc_refs(&[RfcRef("RFC 6487 §7.2"), RfcRef("RFC 5280 §6.1")])
|
||||||
|
.with_context(manifest_uri),
|
||||||
|
);
|
||||||
return empty(stats, warnings, audit);
|
return empty(stats, warnings, audit);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -59,59 +94,205 @@ pub fn process_publication_point_for_issuer_with_ta_constraints<P: PublicationPo
|
|||||||
Ok(([], spki)) => spki,
|
Ok(([], spki)) => spki,
|
||||||
Ok((remaining, _)) => {
|
Ok((remaining, _)) => {
|
||||||
stats.publication_point_dropped = true;
|
stats.publication_point_dropped = true;
|
||||||
warnings.push(Warning::new(format!("dropping publication point: issuer SPKI has {} trailing bytes", remaining.len())).with_context(manifest_uri));
|
warnings.push(
|
||||||
|
Warning::new(format!(
|
||||||
|
"dropping publication point: issuer SPKI has {} trailing bytes",
|
||||||
|
remaining.len()
|
||||||
|
))
|
||||||
|
.with_context(manifest_uri),
|
||||||
|
);
|
||||||
return empty(stats, warnings, audit);
|
return empty(stats, warnings, audit);
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
stats.publication_point_dropped = true;
|
stats.publication_point_dropped = true;
|
||||||
warnings.push(Warning::new(format!("dropping publication point: issuer SPKI parse failed: {error}")).with_context(manifest_uri));
|
warnings.push(
|
||||||
|
Warning::new(format!(
|
||||||
|
"dropping publication point: issuer SPKI parse failed: {error}"
|
||||||
|
))
|
||||||
|
.with_context(manifest_uri),
|
||||||
|
);
|
||||||
return empty(stats, warnings, audit);
|
return empty(stats, warnings, audit);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut crl_states: std::collections::HashMap<String, IssuerCrlState> = match files.iter().filter(|f| f.rsync_uri.ends_with(".crl")).map(|file| {
|
let mut crl_states: std::collections::HashMap<String, IssuerCrlState> = match files
|
||||||
Ok((file.rsync_uri.clone(), IssuerCrlState::Pending { bytes: file.bytes_cloned().map_err(|e| format!("CRL bytes load failed: {e}"))? }))
|
.iter()
|
||||||
}).collect::<Result<_, String>>() { Ok(value) => value, Err(error) => { stats.publication_point_dropped = true; warnings.push(Warning::new(error).with_context(manifest_uri)); return empty(stats, warnings, audit); } };
|
.filter(|f| f.rsync_uri.ends_with(".crl"))
|
||||||
let issuer_resources_index = build_issuer_resources_index(issuer_effective_ip, issuer_effective_as);
|
.map(|file| {
|
||||||
|
Ok((
|
||||||
|
file.rsync_uri.clone(),
|
||||||
|
IssuerCrlState::Pending {
|
||||||
|
bytes: file
|
||||||
|
.bytes_cloned()
|
||||||
|
.map_err(|e| format!("CRL bytes load failed: {e}"))?,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<Result<_, String>>()
|
||||||
|
{
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => {
|
||||||
|
stats.publication_point_dropped = true;
|
||||||
|
warnings.push(Warning::new(error).with_context(manifest_uri));
|
||||||
|
return empty(stats, warnings, audit);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let issuer_resources_index =
|
||||||
|
build_issuer_resources_index(issuer_effective_ip, issuer_effective_as);
|
||||||
if crl_states.is_empty() && (stats.roa_total > 0 || stats.aspa_total > 0) {
|
if crl_states.is_empty() && (stats.roa_total > 0 || stats.aspa_total > 0) {
|
||||||
stats.publication_point_dropped = true;
|
stats.publication_point_dropped = true;
|
||||||
warnings.push(Warning::new("dropping publication point: no CRL files in validated publication point").with_rfc_refs(&[RfcRef("RFC 6487 §4.8.6"), RfcRef("RFC 9286 §7")]).with_context(manifest_uri));
|
warnings.push(
|
||||||
|
Warning::new("dropping publication point: no CRL files in validated publication point")
|
||||||
|
.with_rfc_refs(&[RfcRef("RFC 6487 §4.8.6"), RfcRef("RFC 9286 §7")])
|
||||||
|
.with_context(manifest_uri),
|
||||||
|
);
|
||||||
return empty(stats, warnings, audit);
|
return empty(stats, warnings, audit);
|
||||||
}
|
}
|
||||||
let mut vrps = Vec::new();
|
let mut vrps = Vec::new();
|
||||||
let mut aspas = Vec::new();
|
let mut aspas = Vec::new();
|
||||||
for (index, file) in files.iter().enumerate() {
|
for (index, file) in files.iter().enumerate() {
|
||||||
if file.rsync_uri.ends_with(".roa") {
|
if file.rsync_uri.ends_with(".roa") {
|
||||||
let result = process_roa_with_issuer(file, issuer_ca_der, &issuer_ca, &issuer_spki, issuer_ca_rsync_uri, &mut crl_states, &issuer_resources_index, issuer_effective_ip, issuer_effective_as, validation_time, timing, policy.strict.cms_der, policy.strict.name, policy.resource_validation_mode, ta_constraints);
|
let result = process_roa_with_issuer(
|
||||||
|
file,
|
||||||
|
issuer_ca_der,
|
||||||
|
&issuer_ca,
|
||||||
|
&issuer_spki,
|
||||||
|
issuer_ca_rsync_uri,
|
||||||
|
&mut crl_states,
|
||||||
|
&issuer_resources_index,
|
||||||
|
issuer_effective_ip,
|
||||||
|
issuer_effective_as,
|
||||||
|
validation_time,
|
||||||
|
timing,
|
||||||
|
policy.strict.cms_der,
|
||||||
|
policy.strict.name,
|
||||||
|
policy.resource_validation_mode,
|
||||||
|
ta_constraints,
|
||||||
|
);
|
||||||
match result {
|
match result {
|
||||||
Ok(mut values) => {
|
Ok(mut values) => {
|
||||||
stats.roa_ok += 1;
|
stats.roa_ok += 1;
|
||||||
vrps.append(&mut values);
|
vrps.append(&mut values);
|
||||||
audit.push(ObjectAuditEntry { rsync_uri: file.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&file.sha256), kind: AuditObjectKind::Roa, result: AuditObjectResult::Ok, detail: None });
|
audit.push(ObjectAuditEntry {
|
||||||
if let Some(warning) = ber_compatible_cms_warning_for_file(file, "ROA") { warnings.push(warning); }
|
rsync_uri: file.rsync_uri.clone(),
|
||||||
|
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||||
|
kind: AuditObjectKind::Roa,
|
||||||
|
result: AuditObjectResult::Ok,
|
||||||
|
detail: None,
|
||||||
|
});
|
||||||
|
if let Some(warning) = ber_compatible_cms_warning_for_file(file, "ROA") {
|
||||||
|
warnings.push(warning);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(error) => match policy.signed_object_failure_policy {
|
Err(error) => match policy.signed_object_failure_policy {
|
||||||
SignedObjectFailurePolicy::DropObject => {
|
SignedObjectFailurePolicy::DropObject => {
|
||||||
audit.push(ObjectAuditEntry { rsync_uri: file.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&file.sha256), kind: AuditObjectKind::Roa, result: AuditObjectResult::Error, detail: Some(error.to_string()) });
|
audit.push(ObjectAuditEntry {
|
||||||
let mut refs = vec![RfcRef("RFC 6488 §3"), RfcRef("RFC 9582 §4-§5")]; refs.extend_from_slice(extra_rfc_refs_for_crl_selection(&error));
|
rsync_uri: file.rsync_uri.clone(),
|
||||||
warnings.push(Warning::new(format!("dropping invalid ROA: {}: {error}", file.rsync_uri)).with_rfc_refs(&refs).with_context(&file.rsync_uri));
|
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||||
|
kind: AuditObjectKind::Roa,
|
||||||
|
result: AuditObjectResult::Error,
|
||||||
|
detail: Some(error.to_string()),
|
||||||
|
});
|
||||||
|
let mut refs = vec![RfcRef("RFC 6488 §3"), RfcRef("RFC 9582 §4-§5")];
|
||||||
|
refs.extend_from_slice(extra_rfc_refs_for_crl_selection(&error));
|
||||||
|
warnings.push(
|
||||||
|
Warning::new(format!(
|
||||||
|
"dropping invalid ROA: {}: {error}",
|
||||||
|
file.rsync_uri
|
||||||
|
))
|
||||||
|
.with_rfc_refs(&refs)
|
||||||
|
.with_context(&file.rsync_uri),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
SignedObjectFailurePolicy::DropPublicationPoint => {
|
SignedObjectFailurePolicy::DropPublicationPoint => {
|
||||||
stats.publication_point_dropped = true;
|
stats.publication_point_dropped = true;
|
||||||
warnings.push(Warning::new(format!("dropping publication point due to invalid ROA: {}: {error}", file.rsync_uri)).with_context(manifest_uri));
|
warnings.push(
|
||||||
for later in files.iter().skip(index + 1) { if later.rsync_uri.ends_with(".roa") || later.rsync_uri.ends_with(".asa") { audit.push(ObjectAuditEntry { rsync_uri: later.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&later.sha256), kind: if later.rsync_uri.ends_with(".roa") { AuditObjectKind::Roa } else { AuditObjectKind::Aspa }, result: AuditObjectResult::Skipped, detail: Some("skipped due to signed_object_failure_policy=drop_publication_point".to_string()) }); } }
|
Warning::new(format!(
|
||||||
|
"dropping publication point due to invalid ROA: {}: {error}",
|
||||||
|
file.rsync_uri
|
||||||
|
))
|
||||||
|
.with_context(manifest_uri),
|
||||||
|
);
|
||||||
|
for later in files.iter().skip(index + 1) {
|
||||||
|
if later.rsync_uri.ends_with(".roa")
|
||||||
|
|| later.rsync_uri.ends_with(".asa")
|
||||||
|
{
|
||||||
|
audit.push(ObjectAuditEntry { rsync_uri: later.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&later.sha256), kind: if later.rsync_uri.ends_with(".roa") { AuditObjectKind::Roa } else { AuditObjectKind::Aspa }, result: AuditObjectResult::Skipped, detail: Some("skipped due to signed_object_failure_policy=drop_publication_point".to_string()) });
|
||||||
|
}
|
||||||
|
}
|
||||||
return empty(stats, warnings, audit);
|
return empty(stats, warnings, audit);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else if file.rsync_uri.ends_with(".asa") {
|
} else if file.rsync_uri.ends_with(".asa") {
|
||||||
match process_aspa_with_issuer(file, issuer_ca_der, &issuer_ca, &issuer_spki, issuer_ca_rsync_uri, &mut crl_states, &issuer_resources_index, issuer_effective_ip, issuer_effective_as, validation_time, timing, policy.strict.cms_der, policy.strict.name, policy.resource_validation_mode, ta_constraints) {
|
match process_aspa_with_issuer(
|
||||||
Ok(attestation) => { stats.aspa_ok += 1; aspas.push(attestation); audit.push(ObjectAuditEntry { rsync_uri: file.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&file.sha256), kind: AuditObjectKind::Aspa, result: AuditObjectResult::Ok, detail: None }); if let Some(warning) = ber_compatible_cms_warning_for_file(file, "ASPA") { warnings.push(warning); } }
|
file,
|
||||||
|
issuer_ca_der,
|
||||||
|
&issuer_ca,
|
||||||
|
&issuer_spki,
|
||||||
|
issuer_ca_rsync_uri,
|
||||||
|
&mut crl_states,
|
||||||
|
&issuer_resources_index,
|
||||||
|
issuer_effective_ip,
|
||||||
|
issuer_effective_as,
|
||||||
|
validation_time,
|
||||||
|
timing,
|
||||||
|
policy.strict.cms_der,
|
||||||
|
policy.strict.name,
|
||||||
|
policy.resource_validation_mode,
|
||||||
|
ta_constraints,
|
||||||
|
) {
|
||||||
|
Ok(attestation) => {
|
||||||
|
stats.aspa_ok += 1;
|
||||||
|
aspas.push(attestation);
|
||||||
|
audit.push(ObjectAuditEntry {
|
||||||
|
rsync_uri: file.rsync_uri.clone(),
|
||||||
|
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||||
|
kind: AuditObjectKind::Aspa,
|
||||||
|
result: AuditObjectResult::Ok,
|
||||||
|
detail: None,
|
||||||
|
});
|
||||||
|
if let Some(warning) = ber_compatible_cms_warning_for_file(file, "ASPA") {
|
||||||
|
warnings.push(warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(error) => match policy.signed_object_failure_policy {
|
Err(error) => match policy.signed_object_failure_policy {
|
||||||
SignedObjectFailurePolicy::DropObject => { audit.push(ObjectAuditEntry { rsync_uri: file.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&file.sha256), kind: AuditObjectKind::Aspa, result: AuditObjectResult::Error, detail: Some(error.to_string()) }); warnings.push(Warning::new(format!("dropping invalid ASPA: {}: {error}", file.rsync_uri)).with_context(&file.rsync_uri)); }
|
SignedObjectFailurePolicy::DropObject => {
|
||||||
SignedObjectFailurePolicy::DropPublicationPoint => { stats.publication_point_dropped = true; warnings.push(Warning::new(format!("dropping publication point due to invalid ASPA: {}: {error}", file.rsync_uri)).with_context(manifest_uri)); return empty(stats, warnings, audit); }
|
audit.push(ObjectAuditEntry {
|
||||||
|
rsync_uri: file.rsync_uri.clone(),
|
||||||
|
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||||
|
kind: AuditObjectKind::Aspa,
|
||||||
|
result: AuditObjectResult::Error,
|
||||||
|
detail: Some(error.to_string()),
|
||||||
|
});
|
||||||
|
warnings.push(
|
||||||
|
Warning::new(format!(
|
||||||
|
"dropping invalid ASPA: {}: {error}",
|
||||||
|
file.rsync_uri
|
||||||
|
))
|
||||||
|
.with_context(&file.rsync_uri),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
SignedObjectFailurePolicy::DropPublicationPoint => {
|
||||||
|
stats.publication_point_dropped = true;
|
||||||
|
warnings.push(
|
||||||
|
Warning::new(format!(
|
||||||
|
"dropping publication point due to invalid ASPA: {}: {error}",
|
||||||
|
file.rsync_uri
|
||||||
|
))
|
||||||
|
.with_context(manifest_uri),
|
||||||
|
);
|
||||||
|
return empty(stats, warnings, audit);
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ObjectsOutput {
|
||||||
|
vrps,
|
||||||
|
aspas,
|
||||||
|
router_keys: Vec::new(),
|
||||||
|
warnings,
|
||||||
|
stats,
|
||||||
|
audit,
|
||||||
}
|
}
|
||||||
ObjectsOutput { vrps, aspas, router_keys: Vec::new(), warnings, stats, audit }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -187,8 +187,8 @@ fn root_discovery_from_tal_input(
|
|||||||
let tal_bytes = std::fs::read(path).map_err(|e| {
|
let tal_bytes = std::fs::read(path).map_err(|e| {
|
||||||
FromTalError::TalFetch(format!("read TAL file failed: {}: {e}", path.display()))
|
FromTalError::TalFetch(format!("read TAL file failed: {}: {e}", path.display()))
|
||||||
})?;
|
})?;
|
||||||
let tal = crate::model::tal::Tal::decode_bytes(&tal_bytes)
|
let tal =
|
||||||
.map_err(FromTalError::from)?;
|
crate::model::tal::Tal::decode_bytes(&tal_bytes).map_err(FromTalError::from)?;
|
||||||
if strict_name {
|
if strict_name {
|
||||||
discover_root_ca_instance_from_tal_with_fetchers_strict_name(
|
discover_root_ca_instance_from_tal_with_fetchers_strict_name(
|
||||||
http_fetcher,
|
http_fetcher,
|
||||||
|
|||||||
@ -243,7 +243,8 @@ fn compute_ready_publication_point_stage(
|
|||||||
.ta_constraints
|
.ta_constraints
|
||||||
.shared_for_tal(&ready.node.handle.tal_id);
|
.shared_for_tal(&ready.node.handle.tal_id);
|
||||||
if ta_constraints.is_some()
|
if ta_constraints.is_some()
|
||||||
&& let Some(timing) = runner.timing.as_ref() {
|
&& let Some(timing) = runner.timing.as_ref()
|
||||||
|
{
|
||||||
timing.record_count("ta_constraints_parallel_publication_points", 1);
|
timing.record_count("ta_constraints_parallel_publication_points", 1);
|
||||||
}
|
}
|
||||||
match prepare_publication_point_for_parallel_roa_and_ta_constraints(
|
match prepare_publication_point_for_parallel_roa_and_ta_constraints(
|
||||||
|
|||||||
@ -102,7 +102,6 @@ enum StageOutcome {
|
|||||||
Fresh(Box<StagedOutcome>),
|
Fresh(Box<StagedOutcome>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct FreshErrorOutcome {
|
struct FreshErrorOutcome {
|
||||||
ready: ReadyCaInstance,
|
ready: ReadyCaInstance,
|
||||||
publication_point_started: Instant,
|
publication_point_started: Instant,
|
||||||
|
|||||||
@ -86,7 +86,10 @@ fn build_publication_point_audit_from_snapshot(
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut warnings = runner_warnings.iter().map(AuditWarning::from).collect::<Vec<_>>();
|
let mut warnings = runner_warnings
|
||||||
|
.iter()
|
||||||
|
.map(AuditWarning::from)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
warnings.extend(objects.warnings.iter().map(AuditWarning::from));
|
warnings.extend(objects.warnings.iter().map(AuditWarning::from));
|
||||||
PublicationPointAudit {
|
PublicationPointAudit {
|
||||||
node_id: None,
|
node_id: None,
|
||||||
@ -119,7 +122,10 @@ fn build_publication_point_audit_from_failed_fetch(
|
|||||||
runner_warnings: &[Warning],
|
runner_warnings: &[Warning],
|
||||||
fresh_error: &ManifestFreshError,
|
fresh_error: &ManifestFreshError,
|
||||||
) -> PublicationPointAudit {
|
) -> PublicationPointAudit {
|
||||||
let mut warnings = runner_warnings.iter().map(AuditWarning::from).collect::<Vec<_>>();
|
let mut warnings = runner_warnings
|
||||||
|
.iter()
|
||||||
|
.map(AuditWarning::from)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
warnings.push(AuditWarning::from(
|
warnings.push(AuditWarning::from(
|
||||||
&Warning::new(fresh_error.to_string()).with_context(&ca.manifest_rsync_uri),
|
&Warning::new(fresh_error.to_string()).with_context(&ca.manifest_rsync_uri),
|
||||||
));
|
));
|
||||||
|
|||||||
@ -12,15 +12,25 @@ fn discover_children_from_fresh_snapshot_with_audit_with_issuer_der<P: Publicati
|
|||||||
) -> Result<ChildDiscoveryOutput, String> {
|
) -> Result<ChildDiscoveryOutput, String> {
|
||||||
let locked_files = publication_point.files();
|
let locked_files = publication_point.files();
|
||||||
let issuer_ca = match crate::model::rc::ResourceCertificate::decode_der(issuer_ca_der) {
|
let issuer_ca = match crate::model::rc::ResourceCertificate::decode_der(issuer_ca_der) {
|
||||||
Ok(ca) => match ca.validate_rfc6487_profile(crate::model::rc::ResourceCertificateRole::Ca) {
|
Ok(ca) => {
|
||||||
|
match ca.validate_rfc6487_profile(crate::model::rc::ResourceCertificateRole::Ca) {
|
||||||
Ok(()) => Some(ca),
|
Ok(()) => Some(ca),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
crate::logging::emit(crate::logging::Level::Debug, "issuer_ca_profile_error", || serde_json::json!({"error": error.to_string()}));
|
crate::logging::emit(
|
||||||
|
crate::logging::Level::Debug,
|
||||||
|
"issuer_ca_profile_error",
|
||||||
|
|| serde_json::json!({"error": error.to_string()}),
|
||||||
|
);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
crate::logging::emit(crate::logging::Level::Debug, "issuer_ca_decode_error", || serde_json::json!({"error": error.to_string()}));
|
crate::logging::emit(
|
||||||
|
crate::logging::Level::Debug,
|
||||||
|
"issuer_ca_decode_error",
|
||||||
|
|| serde_json::json!({"error": error.to_string()}),
|
||||||
|
);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -36,10 +46,7 @@ fn discover_children_from_fresh_snapshot_with_audit_with_issuer_der<P: Publicati
|
|||||||
let bytes = file
|
let bytes = file
|
||||||
.bytes_cloned()
|
.bytes_cloned()
|
||||||
.map_err(|error| format!("snapshot CRL bytes load failed: {error}"))?;
|
.map_err(|error| format!("snapshot CRL bytes load failed: {error}"))?;
|
||||||
Ok((
|
Ok((file.rsync_uri.clone(), IssuerCrlState::Pending { bytes }))
|
||||||
file.rsync_uri.clone(),
|
|
||||||
IssuerCrlState::Pending { bytes },
|
|
||||||
))
|
|
||||||
})
|
})
|
||||||
.collect::<Result<_, String>>()?;
|
.collect::<Result<_, String>>()?;
|
||||||
|
|
||||||
@ -63,7 +70,10 @@ fn discover_children_from_fresh_snapshot_with_audit_with_issuer_der<P: Publicati
|
|||||||
let mut child_load_nanos = 0u64;
|
let mut child_load_nanos = 0u64;
|
||||||
let mut child_load_count = 0u64;
|
let mut child_load_count = 0u64;
|
||||||
|
|
||||||
for file in locked_files.iter().filter(|file| file.rsync_uri.ends_with(".cer")) {
|
for file in locked_files
|
||||||
|
.iter()
|
||||||
|
.filter(|file| file.rsync_uri.ends_with(".cer"))
|
||||||
|
{
|
||||||
let child_der = load_child_certificate_der_for_discovery(
|
let child_der = load_child_certificate_der_for_discovery(
|
||||||
file,
|
file,
|
||||||
&mut child_load_nanos,
|
&mut child_load_nanos,
|
||||||
@ -104,7 +114,8 @@ fn discover_children_from_fresh_snapshot_with_audit_with_issuer_der<P: Publicati
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let (Some(issuer_ca), Some(issuer_spki)) = (issuer_ca.as_ref(), issuer_spki.as_ref()) else {
|
let (Some(issuer_ca), Some(issuer_spki)) = (issuer_ca.as_ref(), issuer_spki.as_ref())
|
||||||
|
else {
|
||||||
ca_error = ca_error.saturating_add(1);
|
ca_error = ca_error.saturating_add(1);
|
||||||
audits.push(ObjectAuditEntry {
|
audits.push(ObjectAuditEntry {
|
||||||
rsync_uri: file.rsync_uri.clone(),
|
rsync_uri: file.rsync_uri.clone(),
|
||||||
@ -139,7 +150,8 @@ fn discover_children_from_fresh_snapshot_with_audit_with_issuer_der<P: Publicati
|
|||||||
&mut crl_states,
|
&mut crl_states,
|
||||||
issuer_ca_der,
|
issuer_ca_der,
|
||||||
) {
|
) {
|
||||||
Ok(verified_crl) => BgpsecRouterCertificate::validate_path_with_prevalidated_issuer(
|
Ok(verified_crl) => {
|
||||||
|
BgpsecRouterCertificate::validate_path_with_prevalidated_issuer(
|
||||||
child_der,
|
child_der,
|
||||||
issuer_ca,
|
issuer_ca,
|
||||||
issuer_spki,
|
issuer_spki,
|
||||||
@ -148,7 +160,8 @@ fn discover_children_from_fresh_snapshot_with_audit_with_issuer_der<P: Publicati
|
|||||||
issuer.ca_certificate_rsync_uri.as_deref(),
|
issuer.ca_certificate_rsync_uri.as_deref(),
|
||||||
Some(issuer_crl_uri.as_str()),
|
Some(issuer_crl_uri.as_str()),
|
||||||
validation_time,
|
validation_time,
|
||||||
),
|
)
|
||||||
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
router_error = router_error.saturating_add(1);
|
router_error = router_error.saturating_add(1);
|
||||||
audits.push(ObjectAuditEntry {
|
audits.push(ObjectAuditEntry {
|
||||||
@ -156,7 +169,9 @@ fn discover_children_from_fresh_snapshot_with_audit_with_issuer_der<P: Publicati
|
|||||||
sha256_hex: sha256_hex_from_32(&file.sha256),
|
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||||
kind: AuditObjectKind::RouterCertificate,
|
kind: AuditObjectKind::RouterCertificate,
|
||||||
result: AuditObjectResult::Error,
|
result: AuditObjectResult::Error,
|
||||||
detail: Some(format!("router certificate issuer CRL validation failed: {error}")),
|
detail: Some(format!(
|
||||||
|
"router certificate issuer CRL validation failed: {error}"
|
||||||
|
)),
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -164,14 +179,18 @@ fn discover_children_from_fresh_snapshot_with_audit_with_issuer_der<P: Publicati
|
|||||||
match router_result {
|
match router_result {
|
||||||
Ok(router) => {
|
Ok(router) => {
|
||||||
if let Some(constraints) = policy.ta_constraints.for_tal(&issuer.tal_id)
|
if let Some(constraints) = policy.ta_constraints.for_tal(&issuer.tal_id)
|
||||||
&& let Err(error) = constraints.validate_ee_certificate(&router.resource_cert) {
|
&& let Err(error) =
|
||||||
|
constraints.validate_ee_certificate(&router.resource_cert)
|
||||||
|
{
|
||||||
router_error = router_error.saturating_add(1);
|
router_error = router_error.saturating_add(1);
|
||||||
audits.push(ObjectAuditEntry {
|
audits.push(ObjectAuditEntry {
|
||||||
rsync_uri: file.rsync_uri.clone(),
|
rsync_uri: file.rsync_uri.clone(),
|
||||||
sha256_hex: sha256_hex_from_32(&file.sha256),
|
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||||
kind: AuditObjectKind::RouterCertificate,
|
kind: AuditObjectKind::RouterCertificate,
|
||||||
result: AuditObjectResult::Error,
|
result: AuditObjectResult::Error,
|
||||||
detail: Some(format!("router certificate violates TA constraints: {error}")),
|
detail: Some(format!(
|
||||||
|
"router certificate violates TA constraints: {error}"
|
||||||
|
)),
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -194,7 +213,9 @@ fn discover_children_from_fresh_snapshot_with_audit_with_issuer_der<P: Publicati
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let source_hash = sha256_hex_from_32(&file.sha256);
|
let source_hash = sha256_hex_from_32(&file.sha256);
|
||||||
let until = PackTime::from_utc_offset_datetime(router.resource_cert.tbs.validity_not_after);
|
let until = PackTime::from_utc_offset_datetime(
|
||||||
|
router.resource_cert.tbs.validity_not_after,
|
||||||
|
);
|
||||||
for as_id in asns {
|
for as_id in asns {
|
||||||
router_keys.push(RouterKeyPayload {
|
router_keys.push(RouterKeyPayload {
|
||||||
as_id,
|
as_id,
|
||||||
@ -212,7 +233,9 @@ fn discover_children_from_fresh_snapshot_with_audit_with_issuer_der<P: Publicati
|
|||||||
sha256_hex: source_hash,
|
sha256_hex: source_hash,
|
||||||
kind: AuditObjectKind::RouterCertificate,
|
kind: AuditObjectKind::RouterCertificate,
|
||||||
result: AuditObjectResult::Ok,
|
result: AuditObjectResult::Ok,
|
||||||
detail: Some("validated BGPsec router certificate (RFC 8209)".to_string()),
|
detail: Some(
|
||||||
|
"validated BGPsec router certificate (RFC 8209)".to_string(),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(error) if is_non_router_certificate(&error) => {
|
Err(error) if is_non_router_certificate(&error) => {
|
||||||
@ -221,7 +244,10 @@ fn discover_children_from_fresh_snapshot_with_audit_with_issuer_der<P: Publicati
|
|||||||
sha256_hex: sha256_hex_from_32(&file.sha256),
|
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||||
kind: AuditObjectKind::Certificate,
|
kind: AuditObjectKind::Certificate,
|
||||||
result: AuditObjectResult::Skipped,
|
result: AuditObjectResult::Skipped,
|
||||||
detail: Some("certificate is neither a CA nor a BGPsec router certificate".to_string()),
|
detail: Some(
|
||||||
|
"certificate is neither a CA nor a BGPsec router certificate"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@ -271,13 +297,14 @@ fn discover_children_from_fresh_snapshot_with_audit_with_issuer_der<P: Publicati
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
uri_nanos = uri_nanos.saturating_add(
|
uri_nanos = uri_nanos
|
||||||
uri_started
|
.saturating_add(uri_started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64);
|
||||||
.elapsed()
|
let child_ski = validated
|
||||||
.as_nanos()
|
.child_ca
|
||||||
.min(u128::from(u64::MAX)) as u64,
|
.tbs
|
||||||
);
|
.extensions
|
||||||
let child_ski = validated.child_ca.tbs.extensions.subject_key_identifier.clone();
|
.subject_key_identifier
|
||||||
|
.clone();
|
||||||
let child_hash = sha256_hex_from_32(&file.sha256);
|
let child_hash = sha256_hex_from_32(&file.sha256);
|
||||||
children.push(DiscoveredChildCaInstance {
|
children.push(DiscoveredChildCaInstance {
|
||||||
handle: CaInstanceHandle {
|
handle: CaInstanceHandle {
|
||||||
@ -298,7 +325,9 @@ fn discover_children_from_fresh_snapshot_with_audit_with_issuer_der<P: Publicati
|
|||||||
child_ca_certificate_rsync_uri: file.rsync_uri.clone(),
|
child_ca_certificate_rsync_uri: file.rsync_uri.clone(),
|
||||||
child_ca_certificate_sha256_hex: child_hash.clone(),
|
child_ca_certificate_sha256_hex: child_hash.clone(),
|
||||||
},
|
},
|
||||||
child_entry_projection: child_ski.as_ref().map(|ski| DiscoveredChildEntryProjection {
|
child_entry_projection: child_ski
|
||||||
|
.as_ref()
|
||||||
|
.map(|ski| DiscoveredChildEntryProjection {
|
||||||
child_ski: hex::encode(ski),
|
child_ski: hex::encode(ski),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@ -313,7 +342,13 @@ fn discover_children_from_fresh_snapshot_with_audit_with_issuer_der<P: Publicati
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(timing) = timing {
|
if let Some(timing) = timing {
|
||||||
timing.record_count("child_cer_seen", locked_files.iter().filter(|file| file.rsync_uri.ends_with(".cer")).count() as u64);
|
timing.record_count(
|
||||||
|
"child_cer_seen",
|
||||||
|
locked_files
|
||||||
|
.iter()
|
||||||
|
.filter(|file| file.rsync_uri.ends_with(".cer"))
|
||||||
|
.count() as u64,
|
||||||
|
);
|
||||||
timing.record_count("child_ca_ok", ca_ok);
|
timing.record_count("child_ca_ok", ca_ok);
|
||||||
timing.record_count("child_ca_error", ca_error);
|
timing.record_count("child_ca_error", ca_error);
|
||||||
timing.record_count("child_router_ok", router_ok);
|
timing.record_count("child_router_ok", router_ok);
|
||||||
@ -325,5 +360,9 @@ fn discover_children_from_fresh_snapshot_with_audit_with_issuer_der<P: Publicati
|
|||||||
timing.record_phase_nanos("child_certificate_der_load_total", child_load_nanos);
|
timing.record_phase_nanos("child_certificate_der_load_total", child_load_nanos);
|
||||||
timing.record_count("child_certificate_der_load_count", child_load_count);
|
timing.record_count("child_certificate_der_load_count", child_load_count);
|
||||||
}
|
}
|
||||||
Ok(ChildDiscoveryOutput { children, audits, router_keys })
|
Ok(ChildDiscoveryOutput {
|
||||||
|
children,
|
||||||
|
audits,
|
||||||
|
router_keys,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,6 @@ enum IssuerCrlState {
|
|||||||
Verified(VerifiedIssuerCrl),
|
Verified(VerifiedIssuerCrl),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn load_child_certificate_der_for_discovery<'a>(
|
fn load_child_certificate_der_for_discovery<'a>(
|
||||||
file: &'a PackFile,
|
file: &'a PackFile,
|
||||||
elapsed_nanos: &mut u64,
|
elapsed_nanos: &mut u64,
|
||||||
@ -32,12 +31,8 @@ fn load_child_certificate_der_for_discovery<'a>(
|
|||||||
let bytes = file
|
let bytes = file
|
||||||
.bytes()
|
.bytes()
|
||||||
.map_err(|e| format!("child certificate bytes load failed: {e}"))?;
|
.map_err(|e| format!("child certificate bytes load failed: {e}"))?;
|
||||||
*elapsed_nanos = elapsed_nanos.saturating_add(
|
*elapsed_nanos =
|
||||||
started
|
elapsed_nanos.saturating_add(started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64);
|
||||||
.elapsed()
|
|
||||||
.as_nanos()
|
|
||||||
.min(u128::from(u64::MAX)) as u64,
|
|
||||||
);
|
|
||||||
*count = count.saturating_add(1);
|
*count = count.saturating_add(1);
|
||||||
Ok(bytes)
|
Ok(bytes)
|
||||||
}
|
}
|
||||||
@ -51,10 +46,7 @@ fn ca_certificate_der_for_validation<'a>(
|
|||||||
let was_lazy = ca.ca_certificate_sha256_hex().is_some();
|
let was_lazy = ca.ca_certificate_sha256_hex().is_some();
|
||||||
let der = ca.ca_certificate_der(store)?;
|
let der = ca.ca_certificate_der(store)?;
|
||||||
if was_lazy {
|
if was_lazy {
|
||||||
let elapsed = started
|
let elapsed = started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64;
|
||||||
.elapsed()
|
|
||||||
.as_nanos()
|
|
||||||
.min(u128::from(u64::MAX)) as u64;
|
|
||||||
if let Some(timing) = timing {
|
if let Some(timing) = timing {
|
||||||
timing.record_count("ca_certificate_lazy_load_count", 1);
|
timing.record_count("ca_certificate_lazy_load_count", 1);
|
||||||
timing.record_count("ca_certificate_lazy_load_bytes", der.len() as u64);
|
timing.record_count("ca_certificate_lazy_load_bytes", der.len() as u64);
|
||||||
@ -86,7 +78,11 @@ fn select_issuer_crl_uri_for_child<'a>(
|
|||||||
}
|
}
|
||||||
Err(format!(
|
Err(format!(
|
||||||
"CRL referenced by child certificate CRLDistributionPoints not found in publication point snapshot: {} (RFC 6487 §4.8.6; RFC 9286 §4.2.1)",
|
"CRL referenced by child certificate CRLDistributionPoints not found in publication point snapshot: {} (RFC 6487 §4.8.6; RFC 9286 §4.2.1)",
|
||||||
crldp_uris.iter().map(|uri| uri.as_str()).collect::<Vec<_>>().join(", ")
|
crldp_uris
|
||||||
|
.iter()
|
||||||
|
.map(|uri| uri.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +100,8 @@ fn ensure_issuer_crl_verified<'a>(
|
|||||||
let der = std::mem::take(bytes);
|
let der = std::mem::take(bytes);
|
||||||
let crl = crate::model::crl::RpkixCrl::decode_der(&der)?;
|
let crl = crate::model::crl::RpkixCrl::decode_der(&der)?;
|
||||||
crl.verify_signature_with_issuer_certificate_der(issuer_ca_der)?;
|
crl.verify_signature_with_issuer_certificate_der(issuer_ca_der)?;
|
||||||
let mut revoked_serials = std::collections::HashSet::with_capacity(crl.revoked_certs.len());
|
let mut revoked_serials =
|
||||||
|
std::collections::HashSet::with_capacity(crl.revoked_certs.len());
|
||||||
for revoked in &crl.revoked_certs {
|
for revoked in &crl.revoked_certs {
|
||||||
revoked_serials.insert(revoked.serial_number.bytes_be.clone());
|
revoked_serials.insert(revoked.serial_number.bytes_be.clone());
|
||||||
}
|
}
|
||||||
@ -131,22 +128,40 @@ fn router_asns_for_resource_mode(
|
|||||||
let contains = |asn: u32| {
|
let contains = |asn: u32| {
|
||||||
resources.asnum.as_ref().is_some_and(|choice| match choice {
|
resources.asnum.as_ref().is_some_and(|choice| match choice {
|
||||||
crate::model::rc::AsIdentifierChoice::Inherit => false,
|
crate::model::rc::AsIdentifierChoice::Inherit => false,
|
||||||
crate::model::rc::AsIdentifierChoice::AsIdsOrRanges(items) => items.iter().any(|item| match item {
|
crate::model::rc::AsIdentifierChoice::AsIdsOrRanges(items) => {
|
||||||
|
items.iter().any(|item| match item {
|
||||||
crate::model::rc::AsIdOrRange::Id(id) => *id == asn,
|
crate::model::rc::AsIdOrRange::Id(id) => *id == asn,
|
||||||
crate::model::rc::AsIdOrRange::Range { min, max } => *min <= asn && asn <= *max,
|
crate::model::rc::AsIdOrRange::Range { min, max } => *min <= asn && asn <= *max,
|
||||||
}),
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
match mode {
|
match mode {
|
||||||
ResourceValidationMode::Rfc6487 => {
|
ResourceValidationMode::Rfc6487 => {
|
||||||
let outside = router_asns.iter().copied().filter(|asn| !contains(*asn)).collect::<Vec<_>>();
|
let outside = router_asns
|
||||||
if outside.is_empty() { Ok(router_asns.to_vec()) } else {
|
.iter()
|
||||||
Err(format!("router AS resources are not a subset of issuer effective AS resources: {outside:?}"))
|
.copied()
|
||||||
|
.filter(|asn| !contains(*asn))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if outside.is_empty() {
|
||||||
|
Ok(router_asns.to_vec())
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"router AS resources are not a subset of issuer effective AS resources: {outside:?}"
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ResourceValidationMode::ValidationUpdate03 => {
|
ResourceValidationMode::ValidationUpdate03 => {
|
||||||
let filtered = router_asns.iter().copied().filter(|asn| contains(*asn)).collect::<Vec<_>>();
|
let filtered = router_asns
|
||||||
if filtered.is_empty() { Err("router AS resources have empty validated resource set".to_string()) } else { Ok(filtered) }
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|asn| contains(*asn))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if filtered.is_empty() {
|
||||||
|
Err("router AS resources have empty validated resource set".to_string())
|
||||||
|
} else {
|
||||||
|
Ok(filtered)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -232,16 +232,26 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
|
|||||||
let mut ccr_append_ms = 0;
|
let mut ccr_append_ms = 0;
|
||||||
if self.ccr_accumulator.is_some() {
|
if self.ccr_accumulator.is_some() {
|
||||||
let ccr_projection_build_started = std::time::Instant::now();
|
let ccr_projection_build_started = std::time::Instant::now();
|
||||||
let subordinate_skis = discovered_children.iter().map(|child| {
|
let subordinate_skis = discovered_children
|
||||||
|
.iter()
|
||||||
|
.map(|child| {
|
||||||
if let Some(projection) = &child.child_entry_projection {
|
if let Some(projection) = &child.child_entry_projection {
|
||||||
return hex::decode(&projection.child_ski).map_err(|error| error.to_string());
|
return hex::decode(&projection.child_ski)
|
||||||
|
.map_err(|error| error.to_string());
|
||||||
}
|
}
|
||||||
let der = child.handle.ca_certificate_der(self.store)?;
|
let der = child.handle.ca_certificate_der(self.store)?;
|
||||||
let certificate = ResourceCertificate::decode_der(der.as_ref()).map_err(|error| error.to_string())?;
|
let certificate = ResourceCertificate::decode_der(der.as_ref())
|
||||||
certificate.tbs.extensions.subject_key_identifier.clone()
|
.map_err(|error| error.to_string())?;
|
||||||
|
certificate
|
||||||
|
.tbs
|
||||||
|
.extensions
|
||||||
|
.subject_key_identifier
|
||||||
|
.clone()
|
||||||
.ok_or_else(|| "child certificate missing SubjectKeyIdentifier".to_string())
|
.ok_or_else(|| "child certificate missing SubjectKeyIdentifier".to_string())
|
||||||
}).collect::<Result<Vec<_>, String>>()?;
|
})
|
||||||
let ccr_manifest_projection = crate::ccr::projection::from_snapshot(ca, &pack, subordinate_skis)?;
|
.collect::<Result<Vec<_>, String>>()?;
|
||||||
|
let ccr_manifest_projection =
|
||||||
|
crate::ccr::projection::from_snapshot(ca, &pack, subordinate_skis)?;
|
||||||
ccr_projection_build_ms = ccr_projection_build_started.elapsed().as_millis() as u64;
|
ccr_projection_build_ms = ccr_projection_build_started.elapsed().as_millis() as u64;
|
||||||
let ccr_append_started = std::time::Instant::now();
|
let ccr_append_started = std::time::Instant::now();
|
||||||
self.append_ccr_manifest_projection(&ccr_manifest_projection)?;
|
self.append_ccr_manifest_projection(&ccr_manifest_projection)?;
|
||||||
@ -296,15 +306,18 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
|
|||||||
|
|
||||||
// Commit protocol freshness only after successful publication-point
|
// Commit protocol freshness only after successful publication-point
|
||||||
// validation/finalization, independently of optional CCR output.
|
// validation/finalization, independently of optional CCR output.
|
||||||
self.store.put_manifest_anti_rollback_meta(
|
self.store
|
||||||
|
.put_manifest_anti_rollback_meta(
|
||||||
&crate::repository::storage::ManifestAntiRollbackMeta {
|
&crate::repository::storage::ManifestAntiRollbackMeta {
|
||||||
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
|
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
|
||||||
manifest_number_be: pack.manifest_number_be.clone(),
|
manifest_number_be: pack.manifest_number_be.clone(),
|
||||||
manifest_this_update: pack.this_update.clone(),
|
manifest_this_update: pack.this_update.clone(),
|
||||||
manifest_sha256: <sha2::Sha256 as sha2::Digest>::digest(&pack.manifest_bytes).to_vec(),
|
manifest_sha256: <sha2::Sha256 as sha2::Digest>::digest(&pack.manifest_bytes)
|
||||||
|
.to_vec(),
|
||||||
updated_at_validation_time: pack.verified_at.clone(),
|
updated_at_validation_time: pack.verified_at.clone(),
|
||||||
},
|
},
|
||||||
).map_err(|error| format!("persist manifest freshness failed: {error}"))?;
|
)
|
||||||
|
.map_err(|error| format!("persist manifest freshness failed: {error}"))?;
|
||||||
|
|
||||||
Ok(FreshPublicationPointFinalizeOutput {
|
Ok(FreshPublicationPointFinalizeOutput {
|
||||||
result: PublicationPointRunResult {
|
result: PublicationPointRunResult {
|
||||||
|
|||||||
@ -99,7 +99,10 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
|
|||||||
Err(stage_error) => {
|
Err(stage_error) => {
|
||||||
let mut warnings = Vec::new();
|
let mut warnings = Vec::new();
|
||||||
warnings.push(
|
warnings.push(
|
||||||
Warning::new(format!("publication point processing failed: {}", stage_error.error))
|
Warning::new(format!(
|
||||||
|
"publication point processing failed: {}",
|
||||||
|
stage_error.error
|
||||||
|
))
|
||||||
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
|
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
|
||||||
.with_context(&ca.manifest_rsync_uri),
|
.with_context(&ca.manifest_rsync_uri),
|
||||||
);
|
);
|
||||||
@ -186,8 +189,14 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
let object_ms = object_started.elapsed().as_millis() as u64;
|
let object_ms = object_started.elapsed().as_millis() as u64;
|
||||||
self.record_publication_point_step_ms(&ca.manifest_rsync_uri, "objects_processing", object_ms);
|
self.record_publication_point_step_ms(
|
||||||
objects.router_keys.extend(stage.discovered_router_keys.clone());
|
&ca.manifest_rsync_uri,
|
||||||
|
"objects_processing",
|
||||||
|
object_ms,
|
||||||
|
);
|
||||||
|
objects
|
||||||
|
.router_keys
|
||||||
|
.extend(stage.discovered_router_keys.clone());
|
||||||
|
|
||||||
let finalized = self.finalize_fresh_publication_point_from_reducer(
|
let finalized = self.finalize_fresh_publication_point_from_reducer(
|
||||||
ca,
|
ca,
|
||||||
|
|||||||
@ -19,5 +19,4 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
64
tests/cli_errors.rs
Normal file
64
tests/cli_errors.rs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn command() -> Command {
|
||||||
|
let mut cmd = Command::new(env!("CARGO_BIN_EXE_panda-rpki"));
|
||||||
|
cmd.env_remove("PANDA_RPKI_LOG_LEVEL")
|
||||||
|
.env_remove("PANDA_RPKI_LOG_FORMAT");
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn help_is_successful_and_goes_to_stdout() {
|
||||||
|
for args in [vec![], vec!["--help"], vec!["-h"]] {
|
||||||
|
let result = command().args(args).output().unwrap();
|
||||||
|
assert!(result.status.success());
|
||||||
|
assert!(String::from_utf8_lossy(&result.stdout).contains("panda-rpki"));
|
||||||
|
assert!(result.stderr.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_command_returns_two_without_stdout() {
|
||||||
|
let result = command().arg("not-a-command").output().unwrap();
|
||||||
|
assert_eq!(result.status.code(), Some(2));
|
||||||
|
assert!(result.stdout.is_empty());
|
||||||
|
assert!(String::from_utf8_lossy(&result.stderr).contains("unknown command"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn malformed_validation_arguments_do_not_create_outputs() {
|
||||||
|
let directory = tempfile::tempdir().unwrap();
|
||||||
|
for args in [
|
||||||
|
vec!["validate", "--out", "output"],
|
||||||
|
vec!["validate", "--unknown", "value"],
|
||||||
|
vec!["validate", "--out"],
|
||||||
|
] {
|
||||||
|
let result = command()
|
||||||
|
.current_dir(directory.path())
|
||||||
|
.args(args)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(result.status.code(), Some(2));
|
||||||
|
assert!(result.stdout.is_empty());
|
||||||
|
assert_eq!(std::fs::read_dir(directory.path()).unwrap().count(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_daemon_state_root_fails_without_creating_state() {
|
||||||
|
let directory = tempfile::tempdir().unwrap();
|
||||||
|
let result = command()
|
||||||
|
.current_dir(directory.path())
|
||||||
|
.args([
|
||||||
|
"daemon",
|
||||||
|
"--",
|
||||||
|
"--tal",
|
||||||
|
"missing.tal",
|
||||||
|
"--ta",
|
||||||
|
"missing.cer",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(result.status.code(), Some(2));
|
||||||
|
assert_eq!(std::fs::read_dir(directory.path()).unwrap().count(), 0);
|
||||||
|
}
|
||||||
@ -38,7 +38,12 @@ for _ in $(seq 1 50); do
|
|||||||
done
|
done
|
||||||
rg -qx 'ready' "$work_root/server.log"
|
rg -qx 'ready' "$work_root/server.log"
|
||||||
|
|
||||||
docker build -f "$repo_root/docker/Dockerfile" -t panda-rpki:synthetic-e2e "$repo_root"
|
image="${PANDA_RPKI_TEST_IMAGE:-panda-rpki:synthetic-e2e}"
|
||||||
|
if [[ -z "${PANDA_RPKI_TEST_IMAGE:-}" ]]; then
|
||||||
|
docker build -f "$repo_root/docker/Dockerfile" -t "$image" "$repo_root"
|
||||||
|
else
|
||||||
|
docker image inspect "$image" >/dev/null
|
||||||
|
fi
|
||||||
run_validator() {
|
run_validator() {
|
||||||
local out="$1"
|
local out="$1"
|
||||||
local mode="$2"
|
local mode="$2"
|
||||||
@ -46,7 +51,7 @@ run_validator() {
|
|||||||
-v "$fixture:/fixture:ro" \
|
-v "$fixture:/fixture:ro" \
|
||||||
-v "$work_root/state:/state" \
|
-v "$work_root/state:/state" \
|
||||||
-v "$out:/output" \
|
-v "$out:/output" \
|
||||||
panda-rpki:synthetic-e2e validate \
|
"$image" validate \
|
||||||
--tal /fixture/tal/custom.tal --ta /fixture/ta/custom-ta.cer \
|
--tal /fixture/tal/custom.tal --ta /fixture/ta/custom-ta.cer \
|
||||||
--http-root-cert /fixture/certs/rrdp-ca.pem \
|
--http-root-cert /fixture/certs/rrdp-ca.pem \
|
||||||
--http-timeout-secs 5 \
|
--http-timeout-secs 5 \
|
||||||
@ -94,7 +99,7 @@ daemon_container="panda-rpki-daemon-$(basename "$work_root")"
|
|||||||
docker run --rm --name "$daemon_container" --network host --read-only --tmpfs /tmp \
|
docker run --rm --name "$daemon_container" --network host --read-only --tmpfs /tmp \
|
||||||
--user "$(id -u):$(id -g)" \
|
--user "$(id -u):$(id -g)" \
|
||||||
-v "$fixture:/fixture:ro" -v "$work_root/daemon:/data" \
|
-v "$fixture:/fixture:ro" -v "$work_root/daemon:/data" \
|
||||||
panda-rpki:synthetic-e2e daemon \
|
"$image" daemon \
|
||||||
--state-root /data --max-runs 3 --interval-secs 2 --retain-runs 3 -- \
|
--state-root /data --max-runs 3 --interval-secs 2 --retain-runs 3 -- \
|
||||||
--tal /fixture/tal/custom.tal --ta /fixture/ta/custom-ta.cer \
|
--tal /fixture/tal/custom.tal --ta /fixture/ta/custom-ta.cer \
|
||||||
--http-root-cert /fixture/certs/rrdp-ca.pem --log-format json \
|
--http-root-cert /fixture/certs/rrdp-ca.pem --log-format json \
|
||||||
|
|||||||
9
tools/check_format.py
Normal file
9
tools/check_format.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
"""Check every Rust source file, including include! fragments."""
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
root = Path(__file__).resolve().parents[1]
|
||||||
|
files = sorted(p for folder in ("src", "tests") for p in (root / folder).rglob("*.rs"))
|
||||||
|
subprocess.run(["cargo", "fmt", "--all", "--check"], cwd=root, check=True)
|
||||||
|
subprocess.run(["rustfmt", "--check", "--edition", "2024", *map(str, files)],
|
||||||
|
cwd=root, check=True)
|
||||||
86
tools/dependency_notices.py
Normal file
86
tools/dependency_notices.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
"""Reproduce notices from locked Cargo archives and pinned upstream supplements."""
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
HEADER = """# Third-party notices
|
||||||
|
|
||||||
|
Panda RPKI is distributed under BSD-3-Clause; dependencies retain their own
|
||||||
|
licenses. The table below records the locked Cargo dependency graph, including
|
||||||
|
build-time and platform-specific packages that may not be linked on Linux.
|
||||||
|
Upstream license expressions are reproduced as declared by each package.
|
||||||
|
|
||||||
|
The accompanying [license texts](docs/third-party-licenses.txt) retain notices
|
||||||
|
from package archives, including bundled native code. These files must accompany
|
||||||
|
binary distributions. Container OS package notices remain under
|
||||||
|
`/usr/share/doc/` and `/usr/share/common-licenses/` in the Debian image.
|
||||||
|
|
||||||
|
Python cryptography and OpenSSL are external test tools, not bundled source.
|
||||||
|
Consult their installed license notices when distributing a test environment.
|
||||||
|
No production trust-anchor or repository data is included in this source tree.
|
||||||
|
|
||||||
|
| Package | Locked version | Declared license |
|
||||||
|
| --- | --- | --- |
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def generate():
|
||||||
|
metadata = json.loads(subprocess.check_output(
|
||||||
|
["cargo", "metadata", "--locked", "--format-version", "1"], cwd=ROOT))
|
||||||
|
supplements = json.loads((ROOT / "tools/license_supplements.json").read_text())
|
||||||
|
rows, texts = [], []
|
||||||
|
for package in sorted(metadata["packages"], key=lambda p: (p["name"], p["version"])):
|
||||||
|
if package["id"] in metadata["workspace_members"]:
|
||||||
|
continue
|
||||||
|
name, version = package["name"], package["version"]
|
||||||
|
source = Path(package["manifest_path"]).parent
|
||||||
|
candidates = sorted(p for p in source.rglob("*") if p.is_file()
|
||||||
|
and (p.name.upper().startswith(("LICENSE", "LICENCE", "COPYING", "COPYRIGHT", "NOTICE", "AUTHORS"))
|
||||||
|
or "LICENSES" in p.relative_to(source).parts)
|
||||||
|
and p.stat().st_size < 500_000)
|
||||||
|
contents = []
|
||||||
|
for path in candidates:
|
||||||
|
try:
|
||||||
|
contents.append((str(path.relative_to(source)), path.read_text(encoding="utf-8")))
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
continue
|
||||||
|
if not contents:
|
||||||
|
entry = supplements.get(f"{name}@{version}")
|
||||||
|
if entry is None:
|
||||||
|
raise RuntimeError(f"Missing license texts: {name}@{version}; review upstream sources")
|
||||||
|
data = subprocess.check_output([
|
||||||
|
"curl", "--fail", "--silent", "--show-error", "--location",
|
||||||
|
"--proto", "=https", "--proto-redir", "=https", "--connect-timeout", "15",
|
||||||
|
"--max-time", "120", entry["url"]])
|
||||||
|
if hashlib.sha256(data).hexdigest() != entry["sha256"]:
|
||||||
|
raise RuntimeError(f"Supplement checksum mismatch: {name}@{version}")
|
||||||
|
contents.append(("LICENSE (upstream package revision)", data.decode("utf-8")))
|
||||||
|
rows.append(f"| {name} | {version} | {package['license'] or 'See upstream license'} |")
|
||||||
|
for label, content in contents:
|
||||||
|
texts.append(f"\n{'=' * 72}\n{name} {version} — {label}\n{'=' * 72}\n{content}\n")
|
||||||
|
return {
|
||||||
|
ROOT / "THIRD_PARTY_NOTICES.md": HEADER + "\n".join(rows) + "\n",
|
||||||
|
ROOT / "docs/third-party-licenses.txt":
|
||||||
|
"Third-party license texts from locked package archives.\n" + "".join(texts),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--check", action="store_true", help="Fail if tracked notices differ; do not write files")
|
||||||
|
args = parser.parse_args()
|
||||||
|
outputs = generate() # Resolve everything before writing any output.
|
||||||
|
for path, content in outputs.items():
|
||||||
|
if args.check:
|
||||||
|
if not path.exists() or path.read_bytes() != content.encode("utf-8"):
|
||||||
|
raise SystemExit(f"Outdated notices: {path.name}; run tools/dependency_notices.py")
|
||||||
|
else:
|
||||||
|
path.write_bytes(content.encode("utf-8"))
|
||||||
|
print("Dependency notices are current." if args.check else "Dependency notices generated.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
14
tools/license_supplements.json
Normal file
14
tools/license_supplements.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"alloc-stdlib@0.2.4": {
|
||||||
|
"url": "https://raw.githubusercontent.com/dropbox/rust-alloc-no-stdlib/ae42d22078b98549e987d2f03d12df7b984fde47/LICENSE",
|
||||||
|
"sha256": "c0c56f26d9c051cac4d200c34c84e7ae9aaa853e01a982a1df08b09931e518ae"
|
||||||
|
},
|
||||||
|
"asn1-rs-impl@0.2.0": {
|
||||||
|
"url": "https://raw.githubusercontent.com/rusticata/asn1-rs/a20e5f7319c896737ad0f2557037817b91ad854f/LICENSE-MIT",
|
||||||
|
"sha256": "a5c61b93b6ee1d104af9920cf020ff3c7efe818e31fe562c72261847a728f513"
|
||||||
|
},
|
||||||
|
"cookie-factory@0.3.3": {
|
||||||
|
"url": "https://raw.githubusercontent.com/rust-bakery/cookie-factory/d36b805dbd7dd65f2df947235c5bcc573afe2c76/LICENSES/MIT.txt",
|
||||||
|
"sha256": "d09216dc1ea5f273997667935a8d6514d80f00b89fb6954ecc3a43e0a6e360de"
|
||||||
|
}
|
||||||
|
}
|
||||||
59
tools/test_dependency_notices.py
Normal file
59
tools/test_dependency_notices.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
"""Offline regression checks for release-maintenance failure handling."""
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import dependency_notices as notices
|
||||||
|
|
||||||
|
|
||||||
|
class NoticeTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.temp = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(self.temp.cleanup)
|
||||||
|
self.root = Path(self.temp.name)
|
||||||
|
(self.root / "tools").mkdir()
|
||||||
|
(self.root / "package").mkdir()
|
||||||
|
(self.root / "tools/license_supplements.json").write_text("{}")
|
||||||
|
self.metadata = json.dumps({"workspace_members": [], "packages": [{
|
||||||
|
"id": "example", "name": "example", "version": "1.0.0", "license": "MIT",
|
||||||
|
"manifest_path": str(self.root / "package/Cargo.toml"),
|
||||||
|
}]}).encode()
|
||||||
|
self.patch = patch.object(notices, "ROOT", self.root)
|
||||||
|
self.patch.start()
|
||||||
|
self.addCleanup(self.patch.stop)
|
||||||
|
|
||||||
|
def test_missing_license_fails(self):
|
||||||
|
with patch.object(notices.subprocess, "check_output", return_value=self.metadata):
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "Missing license"):
|
||||||
|
notices.generate()
|
||||||
|
|
||||||
|
def test_license_text_is_preserved(self):
|
||||||
|
original = "Copyright Example \n\nMIT license text\n"
|
||||||
|
(self.root / "package/LICENSE").write_text(original)
|
||||||
|
with patch.object(notices.subprocess, "check_output", return_value=self.metadata):
|
||||||
|
outputs = notices.generate()
|
||||||
|
self.assertIn(original, outputs[self.root / "docs/third-party-licenses.txt"])
|
||||||
|
self.assertFalse((self.root / "THIRD_PARTY_NOTICES.md").exists())
|
||||||
|
|
||||||
|
def test_supplement_checksum_mismatch_fails(self):
|
||||||
|
(self.root / "tools/license_supplements.json").write_text(json.dumps({
|
||||||
|
"example@1.0.0": {"url": "https://example.invalid/LICENSE", "sha256": "0" * 64}
|
||||||
|
}))
|
||||||
|
with patch.object(notices.subprocess, "check_output", side_effect=[self.metadata, b"bad"]):
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "checksum mismatch"):
|
||||||
|
notices.generate()
|
||||||
|
|
||||||
|
def test_check_does_not_overwrite_outdated_file(self):
|
||||||
|
path = self.root / "THIRD_PARTY_NOTICES.md"
|
||||||
|
path.write_text("old")
|
||||||
|
with patch.object(notices, "generate", return_value={path: "new"}):
|
||||||
|
with patch("sys.argv", ["dependency_notices.py", "--check"]):
|
||||||
|
with self.assertRaisesRegex(SystemExit, "Outdated notices"):
|
||||||
|
notices.main()
|
||||||
|
self.assertEqual(path.read_text(), "old")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
x
Reference in New Issue
Block a user