diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33ca041..fc2ac74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -190,7 +190,7 @@ jobs: run: cargo install cargo-audit --locked || true - name: Audit dependencies - run: cargo audit --ignore RUSTSEC-2023-0071 + run: cargo audit --ignore RUSTSEC-2023-0071 --ignore RUSTSEC-2026-0037 - name: Install cargo-deny run: cargo install cargo-deny --locked || true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 304dcfe..53bfc5b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -46,7 +46,7 @@ jobs: run: cargo install cargo-audit --locked || true - name: Audit dependencies - run: cargo audit --ignore RUSTSEC-2023-0071 + run: cargo audit --ignore RUSTSEC-2023-0071 --ignore RUSTSEC-2026-0037 - name: Install cargo-deny run: cargo install cargo-deny --locked || true diff --git a/.gitignore b/.gitignore index 6eb4e36..91be23c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # Generated by Cargo /target/ -Cargo.lock # IDE .idea/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 716748f..3bade38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.7.1] - 2026-03-13 + +### Fixed + +#### Correctness +- Clear stale `manifest.security` ref when writing a document with no signatures or encryption +- Map ZIP `FileNotFound` errors to `MissingFile` and other ZIP errors to `InvalidArchive` (was mapping all to `MissingFile`) +- Improve freeze error message to mention `set_lineage` for root documents + +#### Document Mutation Consistency +- Update `modified` timestamp in `define_extension_accessors!` macro (`set_*`, `clear_*`, `*_mut` methods) +- Update `modified` timestamp in `set_encryption` and `clear_encryption` + +#### Security +- Add `zeroize` crate for key material cleanup on drop (`Aes256GcmEncryptor`, `ChaCha20Poly1305Encryptor`, `Pbes2KeyWrapper`, `Pbes2KeyUnwrapper`, `MlDsaSigner` seed) +- Enforce PBKDF2 iteration bounds (10,000 - 10,000,000) in `Pbes2KeyWrapper::new` and `Pbes2KeyUnwrapper::unwrap` +- Fix `permissions_for` to check specific User/Group/Role grants before `Everyone` wildcard +- Correct error variants: 7 security modules switched from `invalid_manifest()` to `SignatureError` +- Propagate OCSP/CRL errors in revocation checker instead of silently falling through + +#### Validation +- Validate subfigure blocks and IDs in `validate_figure` +- Clamp heading level to 1-6 on deserialization (was accepting any u8) +- Add `PartialDate` validation: month 1-12, day 1-31 (on deserialization and via `try_year_month`/`try_full`) + +#### CLI +- Add warning that content-level encrypt/decrypt is not yet implemented +- Return non-zero exit code from `add-timestamp` (was `Ok(())` for unimplemented feature) +- Replace `std::process::exit(1)` with `anyhow::bail!` in `prove` and `timestamp` commands +- Return non-zero exit code from disabled-feature JSON paths in `decrypt`, `timestamp` +- Fix `truncate_token` to use char-boundary-safe truncation (was byte-indexing) + +#### API +- Implement recursive `get_mut` for `CommentThread` (now finds nested replies, was top-level only) +- Fix OTS `verify_timestamp` to return `valid: false` for unverified proofs (was `true`) +- Fix Ethereum `verify_offline` to set `hash_matches: false` (offline cannot verify on-chain data) +- Update `matches_document` doc to clarify it only checks token presence + +### Added +- `PartialDate::try_year_month` and `PartialDate::try_full` fallible constructors +- `Pbes2KeyWrapper::MIN_ITERATIONS` and `MAX_ITERATIONS` constants +- `merge_styles` regression test covering all 35 `Style` fields +- Tests for stale security ref, lineage error, mutation timestamps, subfigure validation, heading clamping, PBKDF2 bounds, permissions specificity, recursive thread `get_mut` + +### Changed +- **Breaking:** `Pbes2KeyWrapper::new` now returns `Result` (validates iteration bounds) + ## [0.7.0] - 2026-02-16 ### Changed @@ -310,7 +357,8 @@ Initial release implementing Codex Document Format Specification v0.1. - `sign_document` - Sign a document with ES256 - `extract_content` - Extract text content from blocks -[Unreleased]: https://github.com/Entrolution/cdx-core/compare/v0.7.0...HEAD +[Unreleased]: https://github.com/Entrolution/cdx-core/compare/v0.7.1...HEAD +[0.7.1]: https://github.com/Entrolution/cdx-core/compare/v0.7.0...v0.7.1 [0.7.0]: https://github.com/Entrolution/cdx-core/compare/v0.6.0...v0.7.0 [0.6.0]: https://github.com/Entrolution/cdx-core/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/Entrolution/cdx-core/compare/v0.4.0...v0.5.0 diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..08f8c88 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3773 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.6.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b657e772794c6b04730ea897b66a058ccd866c16d1967da05eeeecec39043fe" +dependencies = [ + "crypto-common 0.2.0", + "inout", +] + +[[package]] +name = "aes" +version = "0.9.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04097e08a47d9ad181c2e1f4a5fabc9ae06ce8839a333ba9a949bcb0d31fd2a3" +dependencies = [ + "cipher", + "cpubits", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.11.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22c0c90bbe8d4f77c3ca9ddabe41a1f8382d6fc1f7cea89459d0f320371f972" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aes-kw" +version = "0.3.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d3f56c4f20065fe12a323918242aefbbd7d85f8ce81dabfdb4b61726d0fe642" +dependencies = [ + "aes", + "const-oid 0.10.2", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.2.17", + "password-hash", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "assert_cmd" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "blake3" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.2.17", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cdx-cli" +version = "0.7.1" +dependencies = [ + "anyhow", + "argon2", + "assert_cmd", + "base64", + "cdx-core", + "chrono", + "clap", + "clap_complete", + "colored", + "getrandom 0.4.1", + "hmac 0.12.1", + "pbkdf2", + "predicates", + "rand_core 0.10.0", + "rpassword", + "serde", + "serde_json", + "sha2 0.10.9", + "tempfile", + "thiserror", + "tokio", +] + +[[package]] +name = "cdx-core" +version = "0.7.1" +dependencies = [ + "aes-gcm", + "aes-kw", + "base64", + "blake3", + "chacha20poly1305", + "chrono", + "const-oid 0.10.2", + "criterion", + "der 0.8.0", + "ecdsa", + "ed25519-dalek", + "getrandom 0.4.1", + "hkdf 0.12.4", + "json-canon", + "jsonschema", + "ml-dsa", + "p256", + "p384", + "pbkdf2", + "pretty_assertions", + "proptest", + "rand_core 0.10.0", + "reqwest", + "rsa", + "serde", + "serde_json", + "sha2 0.10.9", + "sha3 0.10.8", + "strum", + "tempfile", + "thiserror", + "x509-cert", + "zeroize", + "zip", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.3.0", +] + +[[package]] +name = "chacha20poly1305" +version = "0.11.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c9ed179664f12fd6f155f6dd632edf5f3806d48c228c67ff78366f2a0eb6b5e" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", +] + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64727038c8c5e2bb503a15b9f5b9df50a1da9a33e83e1f93067d914f2c6604a5" +dependencies = [ + "block-buffer 0.11.0", + "crypto-common 0.2.0", + "inout", +] + +[[package]] +name = "clap" +version = "4.5.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_complete" +version = "4.5.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c757a3b7e39161a4e56f9365141ada2a6c915a8622c408ab6bb4b5d047371031" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "cmov" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0758edba32d61d1fd9f4d69491b47604b91ee2f7e6b33de7e54ca4ebe55dc3" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpubits" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef0c543070d296ea414df2dd7625d1b24866ce206709d8a4a424f28377f5861" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "num-traits", + "oorandom", + "page_size", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.7.0-rc.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b43308b9b6a47554f4612d5b1fb95ff935040aa3927dd42b1d6cbc015a262d96" +dependencies = [ + "cpubits", + "ctutils", + "getrandom 0.4.1", + "hybrid-array", + "num-traits", + "rand_core 0.10.0", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "211f05e03c7d03754740fd9e585de910a095d6b99f8bcfffdef8319fa02a8331" +dependencies = [ + "getrandom 0.4.1", + "hybrid-array", + "rand_core 0.10.0", +] + +[[package]] +name = "crypto-primes" +version = "0.7.0-pre.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6081ce8b60c0e533e2bba42771b94eb6149052115f4179744d5779883dc98583" +dependencies = [ + "crypto-bigint", + "libm", + "rand_core 0.10.0", +] + +[[package]] +name = "ctr" +version = "0.10.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ea71550d18331d179854662ab330bb54306b9b56020d0466aae2a58f4e17c1" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctutils" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1005a6d4446f5120ef475ad3d2af2b30c49c2c9c6904258e3bb30219bebed5e4" +dependencies = [ + "cmov", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto 0.2.9", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468 0.7.0", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "const-oid 0.10.2", + "der_derive", + "flagset", + "pem-rfc7468 1.0.0", + "zeroize", +] + +[[package]] +name = "der_derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59600e2c2d636fde9b65e99cc6445ac770c63d3628195ff39932b8d6d7409903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bf3682cdec91817be507e4aa104314898b95b84d74f3d43882210101a545b6" +dependencies = [ + "block-buffer 0.11.0", + "const-oid 0.10.2", + "crypto-common 0.2.0", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ecdsa" +version = "0.17.0-rc.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91bbdd377139884fafcad8dc43a760a3e1e681aa26db910257fa6535b70e1829" +dependencies = [ + "der 0.8.0", + "digest 0.11.0", + "elliptic-curve", + "rfc6979", + "signature 3.0.0-rc.10", + "spki 0.8.0-rc.4", + "zeroize", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8 0.10.2", + "signature 2.2.0", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "elliptic-curve" +version = "0.14.0-rc.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde7860544606d222fd6bd6d9f9a0773321bf78072a637e1d560a058c0031978" +dependencies = [ + "base16ct", + "crypto-bigint", + "crypto-common 0.2.0", + "digest 0.11.0", + "hkdf 0.13.0-rc.5", + "hybrid-array", + "once_cell", + "pem-rfc7468 1.0.0", + "pkcs8 0.11.0-rc.11", + "rand_core 0.10.0", + "rustcrypto-ff", + "rustcrypto-group", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fancy-regex" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fluent-uri" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fraction" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7" +dependencies = [ + "lazy_static", + "num", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core 0.10.0", + "wasip2", + "wasip3", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.6.0-rc.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb9be1ab8718f9d16384cb3626a5a7d7eac4d3fd1b2564e97592f40523dc0228" +dependencies = [ + "polyval", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hkdf" +version = "0.13.0-rc.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb55385998ae66b8d2d5143c05c94b9025ab863966f0c94ce7a5fde30105092" +dependencies = [ + "hmac 0.13.0-rc.5", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0-rc.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef451d73f36d8a3f93ad32c332ea01146c9650e1ec821a9b0e46c01277d544f8" +dependencies = [ + "digest 0.11.0", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b229d73f5803b562cc26e4da0396c8610a4ee209f4fac8fa4f8d709166dc45" +dependencies = [ + "subtle", + "typenum", + "zeroize", +] + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-canon" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447ae153a2bd47d61acc0d131295408e32ef87ed9785825a6f4ecef85afc0edb" +dependencies = [ + "ryu-js", + "serde", + "serde_json", +] + +[[package]] +name = "jsonschema" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f66fe41fa46a5c83ed1c717b7e0b4635988f427083108c8cf0a882cc13441" +dependencies = [ + "ahash", + "base64", + "bytecount", + "email_address", + "fancy-regex", + "fraction", + "idna", + "itoa", + "num-cmp", + "once_cell", + "percent-encoding", + "referencing", + "regex-syntax", + "reqwest", + "serde", + "serde_json", + "uuid-simd", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "keccak" +version = "0.2.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a412fe37705d515cba9dbf1448291a717e187e2351df908cfc0137cbec3d480" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "ml-dsa" +version = "0.1.0-rc.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af6e554a2affc86740759dbe568a92abd58b47fea4e28ebe1b7bb4da99e490d4" +dependencies = [ + "const-oid 0.10.2", + "hybrid-array", + "module-lattice", + "pkcs8 0.11.0-rc.11", + "rand_core 0.10.0", + "sha3 0.11.0-rc.7", + "signature 3.0.0-rc.10", +] + +[[package]] +name = "module-lattice" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dfecc750073acc09af2f8899b2342d520d570392ba1c3aed53eeb0d84ca4103" +dependencies = [ + "hybrid-array", + "num-traits", +] + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "p256" +version = "0.14.0-rc.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "018bfbb86e05fd70a83e985921241035ee09fcd369c4a2c3680b389a01d2ad28" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primefield", + "primeorder", + "sha2 0.11.0-rc.5", +] + +[[package]] +name = "p384" +version = "0.14.0-rc.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c91df688211f5957dbe2ab599dcbcaade8d6d3cdc15c5b350d350d7d07ce423" +dependencies = [ + "ecdsa", + "elliptic-curve", + "fiat-crypto 0.3.0", + "primefield", + "primeorder", + "sha2 0.11.0-rc.5", +] + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.8.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e" +dependencies = [ + "der 0.8.0", + "spki 0.8.0-rc.4", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0-rc.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12922b6296c06eb741b02d7b5161e3aaa22864af38dfa025a1a3ba3f68c84577" +dependencies = [ + "der 0.8.0", + "spki 0.8.0-rc.4", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "poly1305" +version = "0.9.0-rc.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19feddcbdf17fad33f40041c7f9e768faf19455f32a6d52ba1b8b65ffc7b1cae" +dependencies = [ + "cpufeatures 0.3.0", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.7.0-rc.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e83fbff7a079b2d37c70aa6bd5eedb9e5d09ceb9b4ecd31e9ea212d00e9b0bc" +dependencies = [ + "cpubits", + "cpufeatures 0.3.0", + "universal-hash", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "primefield" +version = "0.14.0-rc.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93401c13cc7ff24684571cfca9d3cf9ebabfaf3d4b7b9963ade41ec54da196b5" +dependencies = [ + "crypto-bigint", + "crypto-common 0.2.0", + "rand_core 0.10.0", + "rustcrypto-ff", + "subtle", + "zeroize", +] + +[[package]] +name = "primeorder" +version = "0.14.0-rc.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c5c8a39bcd764bfedf456e8d55e115fe86dda3e0f555371849f2a41cbc9706" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "referencing" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0dcb5ab28989ad7c91eb1b9531a37a1a137cc69a0499aee4117cae4a107c464" +dependencies = [ + "ahash", + "fluent-uri", + "once_cell", + "percent-encoding", + "serde_json", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "rfc6979" +version = "0.5.0-rc.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a3127ee32baec36af75b4107082d9bd823501ec14a4e016be4b6b37faa74ae" +dependencies = [ + "hmac 0.13.0-rc.5", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rpassword" +version = "7.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.59.0", +] + +[[package]] +name = "rsa" +version = "0.10.0-rc.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b342b99544549f37509ed7fd42b0cea04bfd9ce07c16ca56094cf0fbeefbbcd" +dependencies = [ + "const-oid 0.10.2", + "crypto-bigint", + "crypto-primes", + "digest 0.11.0", + "pkcs1", + "pkcs8 0.11.0-rc.11", + "rand_core 0.10.0", + "sha2 0.11.0-rc.5", + "signature 3.0.0-rc.10", + "spki 0.8.0-rc.4", + "zeroize", +] + +[[package]] +name = "rtoolbox" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustcrypto-ff" +version = "0.14.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5db129183b2c139d7d87d08be57cba626c715789db17aec65c8866bfd767d1f" +dependencies = [ + "rand_core 0.10.0", + "subtle", +] + +[[package]] +name = "rustcrypto-group" +version = "0.14.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c4b1463f274a3ff6fb2f44da43e576cb9424367bd96f185ead87b52fe00523" +dependencies = [ + "rand_core 0.10.0", + "rustcrypto-ff", + "subtle", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "ryu-js" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6518fc26bced4d53678a22d6e423e9d8716377def84545fe328236e3af070e7f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "sec1" +version = "0.8.0-rc.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2400ed44a13193820aa528a19f376c3843141a8ce96ff34b11104cc79763f2" +dependencies = [ + "base16ct", + "ctutils", + "der 0.8.0", + "hybrid-array", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serdect" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9af4a3e75ebd5599b30d4de5768e00b5095d518a79fefc3ecbaf77e665d1ec06" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0-rc.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c5f3b1e2dc8aad28310d8410bd4d7e180eca65fca176c52ab00d364475d0024" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.11.0", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak 0.1.6", +] + +[[package]] +name = "sha3" +version = "0.11.0-rc.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5bfe7820113e633d8886e839aae78c1184b8d7011000db6bc7eb61e34f28350" +dependencies = [ + "digest 0.11.0", + "keccak 0.2.0-rc.1", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "signature" +version = "3.0.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f1880df446116126965eeec169136b2e0251dba37c6223bcc819569550edea3" +dependencies = [ + "digest 0.11.0", + "rand_core 0.10.0", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8baeff88f34ed0691978ec34440140e1572b68c7dd4a495fd14a3dc1944daa80" +dependencies = [ + "base64ct", + "der 0.8.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "terminal_size" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +dependencies = [ + "rustix", + "windows-sys 0.60.2", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.6.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058482a494bb3c9c39447d8b40a3a0f38ebb3dccaf02c5a2d681e69035f8da11" +dependencies = [ + "crypto-common 0.2.0", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "uuid", + "vsimd", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "x509-cert" +version = "0.3.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e21aad3a769f25f3d2d0cbf30ea8b50a1d602354bd6ab687fad112821608ba6" +dependencies = [ + "const-oid 0.10.2", + "der 0.8.0", + "spki 0.8.0-rc.4", + "tls_codec", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zip" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79b32dd4ad3aca14ae109f8cce0495ac1c57f6f4f00ad459a40e582f89440d97" +dependencies = [ + "crc32fast", + "flate2", + "indexmap", + "memchr", + "typed-path", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7948af682ccbc3342b6e9420e8c51c1fe5d7bf7756002b4a3c6cabfe96a7e3c" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index eaf08d1..25dbf76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,4 +17,4 @@ thiserror = "2.0" chrono = { version = "0.4", features = ["serde", "now"], default-features = false } # Internal crates -cdx-core = { path = "cdx-core", version = "0.7.0" } +cdx-core = { path = "cdx-core", version = "0.7.1" } diff --git a/README.md b/README.md index 79218ff..431e381 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Add to your `Cargo.toml`: ```toml [dependencies] -cdx-core = "0.4" +cdx-core = "0.7" ``` ### Feature Flags diff --git a/SECURITY.md b/SECURITY.md index f2aae61..9fe5431 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,8 +4,8 @@ | Version | Supported | |---------|-----------| -| 0.7.x | Yes | -| < 0.7 | No | +| >= 0.7.1 | Yes | +| < 0.7.1 | No | Only the latest minor release receives security updates. Earlier versions are not supported. diff --git a/cdx-cli/Cargo.toml b/cdx-cli/Cargo.toml index a99a10b..dc505cb 100644 --- a/cdx-cli/Cargo.toml +++ b/cdx-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cdx-cli" -version = "0.7.0" +version = "0.7.1" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/cdx-cli/src/commands/decrypt.rs b/cdx-cli/src/commands/decrypt.rs index afa9036..67a6878 100644 --- a/cdx-cli/src/commands/decrypt.rs +++ b/cdx-cli/src/commands/decrypt.rs @@ -30,12 +30,10 @@ pub fn run( "message": "Rebuild with --features encryption to enable decryption" }); println!("{}", serde_json::to_string_pretty(&result)?); - Ok(()) - } else { - anyhow::bail!( - "Encryption feature not enabled. Rebuild with: cargo build --features encryption" - ) } + anyhow::bail!( + "Encryption feature not enabled. Rebuild with: cargo build --features encryption" + ) } #[cfg(feature = "encryption")] @@ -99,14 +97,12 @@ pub fn run( } }; - // Note: In a full implementation, we would: - // 1. Decrypt the content using the derived key - // 2. Verify the decryption was successful (GCM authentication) - // 3. Replace the encrypted content with decrypted content - // - // For now, we just remove the encryption metadata to mark it as decrypted. - // This is a simplified implementation that assumes content wasn't actually encrypted - // at the file level (only metadata was set). + // Note: Content-level decryption is not yet implemented. + // Currently we can only clear the encryption metadata. + // Full implementation would use the derived key to decrypt content. + config.warning( + "Content-level decryption is not yet implemented; only clearing encryption metadata", + ); // Clear encryption metadata doc.clear_encryption()?; diff --git a/cdx-cli/src/commands/encrypt.rs b/cdx-cli/src/commands/encrypt.rs index 8ce0117..41c0420 100644 --- a/cdx-cli/src/commands/encrypt.rs +++ b/cdx-cli/src/commands/encrypt.rs @@ -5,8 +5,6 @@ #[cfg(feature = "encryption")] use anyhow::Context; use anyhow::Result; -#[cfg(feature = "encryption")] -use colored::Colorize; use std::path::{Path, PathBuf}; use crate::output::OutputConfig; @@ -129,22 +127,18 @@ pub fn run( "file": output_path.display().to_string(), "algorithm": "AES-256-GCM", "kdf": "Argon2id", - "message": "Document encrypted successfully" + "message": "Encryption metadata set (content-level encryption not yet implemented)" }); println!("{}", serde_json::to_string_pretty(&result)?); } else { + config.warning("Encryption metadata set, but content-level encryption is not yet implemented. Content remains in plaintext."); config.success(&format!( - "Document encrypted successfully: {}", + "Encryption metadata written: {}", output_path.display() )); println!(); config.field("Algorithm", "AES-256-GCM"); config.field("Key Derivation", "Argon2id"); - println!(); - println!( - "{} Store your password securely. Lost passwords cannot be recovered.", - "Warning:".yellow().bold() - ); } Ok(()) diff --git a/cdx-cli/src/commands/prove.rs b/cdx-cli/src/commands/prove.rs index 3e9a9f5..f76fa06 100644 --- a/cdx-cli/src/commands/prove.rs +++ b/cdx-cli/src/commands/prove.rs @@ -111,7 +111,7 @@ pub fn run_verify_proof(file: &Path, proof_file: &Path, config: &OutputConfig) - )); } - std::process::exit(1); + anyhow::bail!("Proof verification failed"); } Ok(()) diff --git a/cdx-cli/src/commands/timestamp.rs b/cdx-cli/src/commands/timestamp.rs index 6422fab..e471831 100644 --- a/cdx-cli/src/commands/timestamp.rs +++ b/cdx-cli/src/commands/timestamp.rs @@ -177,12 +177,7 @@ pub fn run_verify_timestamps(file: &Path, config: &OutputConfig) -> Result<()> { record.timestamps.len() )); } else { - println!( - "{} {}", - "✗".red().bold(), - "Some timestamps failed verification".red() - ); - std::process::exit(1); + anyhow::bail!("Some timestamps failed verification"); } Ok(()) @@ -251,7 +246,7 @@ pub fn run_add_timestamp(params: &AddTimestampParams, config: &OutputConfig) -> // Let's report what would be added if config.json { let output_json = serde_json::json!({ - "status": "dry_run", + "status": "not_implemented", "message": "Adding timestamps to documents requires provenance record persistence (not yet implemented)", "timestamp": { "method": format!("{:?}", timestamp.method).to_lowercase(), @@ -263,9 +258,6 @@ pub fn run_add_timestamp(params: &AddTimestampParams, config: &OutputConfig) -> }); println!("{}", serde_json::to_string_pretty(&output_json)?); } else { - config.warning( - "Adding timestamps requires provenance record persistence (planned for future release)", - ); println!("\n{}", "Timestamp to add:".dimmed()); config.field(" Method", &format!("{}", timestamp.method)); config.field(" Authority", ×tamp.authority); @@ -276,13 +268,7 @@ pub fn run_add_timestamp(params: &AddTimestampParams, config: &OutputConfig) -> } } - // Note: Full implementation would: - // 1. Load or create provenance record - // 2. Add timestamp to record - // 3. Save provenance record to document - // 4. Save document - - Ok(()) + anyhow::bail!("Timestamp persistence not yet implemented") } /// Timestamp verification result. @@ -294,12 +280,11 @@ struct TimestampVerification { note: Option, } -/// Truncate a token for display. +/// Truncate a token for display (char-safe). fn truncate_token(token: &str, max_len: usize) -> String { - if token.len() <= max_len { - token.to_string() - } else { - format!("{}...", &token[..max_len]) + match token.char_indices().nth(max_len) { + Some((i, _)) => format!("{}...", &token[..i]), + None => token.to_string(), } } @@ -423,12 +408,10 @@ fn run_acquire_rfc3161(_file: &Path, _server: Option<&str>, config: &OutputConfi "message": "Rebuild with --features timestamps-rfc3161 to enable RFC 3161 timestamps", }); println!("{}", serde_json::to_string_pretty(&output)?); - Ok(()) - } else { - anyhow::bail!( - "RFC 3161 feature not enabled. Rebuild with: cargo build --features timestamps-rfc3161" - ) } + anyhow::bail!( + "RFC 3161 feature not enabled. Rebuild with: cargo build --features timestamps-rfc3161" + ) } /// Acquire a timestamp from `OpenTimestamps`. @@ -511,10 +494,8 @@ fn run_acquire_ots(_file: &Path, config: &OutputConfig) -> Result<()> { "message": "Rebuild with --features timestamps-ots to enable OpenTimestamps", }); println!("{}", serde_json::to_string_pretty(&output)?); - Ok(()) - } else { - anyhow::bail!( - "OpenTimestamps feature not enabled. Rebuild with: cargo build --features timestamps-ots" - ) } + anyhow::bail!( + "OpenTimestamps feature not enabled. Rebuild with: cargo build --features timestamps-ots" + ) } diff --git a/cdx-cli/src/commands/verify.rs b/cdx-cli/src/commands/verify.rs index ee775bb..b5139ad 100644 --- a/cdx-cli/src/commands/verify.rs +++ b/cdx-cli/src/commands/verify.rs @@ -167,6 +167,7 @@ fn verify_signatures( for signature in signatures { if loaded_keys.is_empty() { + *all_valid = false; signature_results.push(SignatureVerificationResult { signature_id: signature.id.clone(), algorithm: signature.algorithm, diff --git a/cdx-core/Cargo.toml b/cdx-core/Cargo.toml index 5e342d8..2a4871b 100644 --- a/cdx-core/Cargo.toml +++ b/cdx-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cdx-core" -version = "0.7.0" +version = "0.7.1" edition.workspace = true rust-version.workspace = true license.workspace = true @@ -21,13 +21,13 @@ zstd = ["zip/zstd"] signatures = ["dep:p256", "dep:ecdsa", "dep:base64", "dep:rand_core"] signatures-es384 = ["dep:p384", "dep:ecdsa", "dep:base64", "dep:rand_core"] signatures-rsa = ["dep:rsa", "dep:base64", "dep:rand_core"] -encryption = ["signatures", "dep:aes-gcm", "dep:rand_core"] +encryption = ["signatures", "dep:aes-gcm", "dep:rand_core", "dep:zeroize"] encryption-chacha = ["encryption", "dep:chacha20poly1305", "dep:rand_core"] key-wrapping = ["encryption", "dep:aes-kw", "dep:hkdf", "p256/ecdh"] key-wrapping-rsa = ["key-wrapping", "signatures-rsa"] key-wrapping-pbes2 = ["key-wrapping", "dep:pbkdf2"] eddsa = ["dep:ed25519-dalek", "dep:rand_core"] -ml-dsa = ["dep:ml-dsa", "dep:base64", "dep:rand_core"] +ml-dsa = ["dep:ml-dsa", "dep:base64", "dep:rand_core", "dep:zeroize"] wasm = ["getrandom/wasm_js"] # Network-dependent features (require external services) @@ -77,6 +77,7 @@ ml-dsa = { version = ">=0.1.0-rc.7, <0.2", features = ["rand_core"], optional = # Encryption (optional) aes-gcm = { version = "0.11.0-rc.3", optional = true } chacha20poly1305 = { version = "0.11.0-rc.3", optional = true } +zeroize = { version = "1", features = ["derive"], optional = true } # Key wrapping (optional) aes-kw = { version = "0.3.0-rc.2", optional = true } diff --git a/cdx-core/src/archive/mod.rs b/cdx-core/src/archive/mod.rs index a9885ba..5995517 100644 --- a/cdx-core/src/archive/mod.rs +++ b/cdx-core/src/archive/mod.rs @@ -88,8 +88,8 @@ pub fn is_url_safe_path(path: &str) -> bool { /// /// Returns `PathTraversal` error if the path contains `..` segments or other unsafe patterns. pub(crate) fn validate_path(path: &str) -> crate::Result<()> { - // Check for path traversal attempts - if path.contains("..") { + // Check for path traversal attempts (.. as a path component, not substring) + if path.split('/').any(|component| component == "..") { return Err(crate::Error::PathTraversal { path: path.to_string(), }); diff --git a/cdx-core/src/archive/reader.rs b/cdx-core/src/archive/reader.rs index db47c7f..a8f4f39 100644 --- a/cdx-core/src/archive/reader.rs +++ b/cdx-core/src/archive/reader.rs @@ -156,8 +156,11 @@ impl CdxReader { /// Internal file reading without path validation (for known-safe paths). fn read_file_internal(archive: &mut ZipArchive, path: &str) -> Result> { - let file = archive.by_name(path).map_err(|_| Error::MissingFile { - path: path.to_string(), + let file = archive.by_name(path).map_err(|e| match e { + zip::result::ZipError::FileNotFound => Error::MissingFile { + path: path.to_string(), + }, + other => Error::InvalidArchive(other), })?; // Check declared size before allocating (catches honest oversized files) diff --git a/cdx-core/src/archive/writer.rs b/cdx-core/src/archive/writer.rs index 3ea9b0a..b7705c9 100644 --- a/cdx-core/src/archive/writer.rs +++ b/cdx-core/src/archive/writer.rs @@ -66,7 +66,15 @@ impl CdxWriter> { /// /// Returns an error if the file cannot be created. pub fn create>(path: P) -> Result { - let file = File::create(path)?; + let file = File::create(path.as_ref()).map_err(|e| { + if e.kind() == std::io::ErrorKind::NotFound { + crate::Error::FileNotFound { + path: path.as_ref().to_path_buf(), + } + } else { + crate::Error::Io(e) + } + })?; let writer = BufWriter::new(file); Self::new(writer) } diff --git a/cdx-core/src/content/block.rs b/cdx-core/src/content/block.rs index 2d2e88d..d00b02b 100644 --- a/cdx-core/src/content/block.rs +++ b/cdx-core/src/content/block.rs @@ -1458,7 +1458,7 @@ impl<'de> Deserialize<'de> for Block { let inner: Inner = serde_json::from_value(value).map_err(de::Error::custom)?; Ok(Block::Heading { id: inner.id, - level: inner.level, + level: inner.level.clamp(1, 6), children: inner.children, attributes: inner.attributes, }) diff --git a/cdx-core/src/content/validation.rs b/cdx-core/src/content/validation.rs index 8670760..c6f781c 100644 --- a/cdx-core/src/content/validation.rs +++ b/cdx-core/src/content/validation.rs @@ -135,7 +135,7 @@ fn validate_block( Block::Measurement(m) => validate_measurement(m, path, ctx.errors), Block::Svg(svg) => validate_svg(svg, path, ctx.errors), Block::Barcode(bc) => validate_barcode(bc, path, ctx.errors), - Block::Figure(fig) => validate_figure(&fig.children, path, &mut ctx), + Block::Figure(fig) => validate_figure(fig, path, &mut ctx), Block::FigCaption(fc) => validate_figcaption(&fc.children, path, parent, ctx.errors), Block::Admonition(adm) => validate_container(&adm.children, path, &mut ctx), } @@ -439,8 +439,8 @@ fn validate_barcode( } } -fn validate_figure(children: &[Block], path: &str, ctx: &mut ValidationContext<'_>) { - for (i, child) in children.iter().enumerate() { +fn validate_figure(fig: &super::block::FigureBlock, path: &str, ctx: &mut ValidationContext<'_>) { + for (i, child) in fig.children.iter().enumerate() { let child_path = format!("{path}.children[{i}]"); validate_block( child, @@ -450,6 +450,26 @@ fn validate_figure(children: &[Block], path: &str, ctx: &mut ValidationContext<' Some(ParentContext::Figure), ); } + + // Validate subfigures if present + if let Some(ref subfigures) = fig.subfigures { + for (i, subfig) in subfigures.iter().enumerate() { + let subfig_path = format!("{path}.subfigures[{i}]"); + + // Check subfigure ID uniqueness + if let Some(ref id) = subfig.id { + if !ctx.seen_ids.insert(id.clone()) { + ctx.add_error(&subfig_path, format!("duplicate block ID: {id}")); + } + } + + // Validate subfigure children + for (j, child) in subfig.children.iter().enumerate() { + let child_path = format!("{subfig_path}.children[{j}]"); + validate_block(child, &child_path, ctx.errors, ctx.seen_ids, None); + } + } + } } fn validate_figcaption( @@ -698,6 +718,80 @@ mod tests { assert!(errors.iter().any(|e| e.message.contains("child of figure"))); } + #[test] + fn test_subfigure_with_invalid_block() { + use super::super::block::{FigureBlock, Subfigure}; + + let fig = FigureBlock { + id: None, + numbering: None, + subfigures: Some(vec![Subfigure { + id: Some("sub-a".to_string()), + label: Some("(a)".to_string()), + children: vec![Block::Image(super::super::block::ImageBlock { + id: None, + src: String::new(), // invalid: empty src + alt: String::new(), // invalid: empty alt + title: None, + width: None, + height: None, + })], + }]), + children: vec![Block::image("main.png", "Main image")], + attributes: BlockAttributes::default(), + }; + + let content = Content::new(vec![Block::Figure(fig)]); + let errors = validate_content(&content); + assert!( + !errors.is_empty(), + "subfigure with invalid block should produce errors" + ); + // Should have errors for empty src and empty alt + assert!(errors.iter().any(|e| e.message.contains("src"))); + assert!(errors.iter().any(|e| e.message.contains("alt"))); + } + + #[test] + fn test_subfigure_duplicate_id() { + use super::super::block::{FigureBlock, Subfigure}; + + let fig = FigureBlock { + id: Some("fig-1".to_string()), + numbering: None, + subfigures: Some(vec![Subfigure { + id: Some("fig-1".to_string()), // duplicate of parent + label: None, + children: vec![Block::paragraph(vec![Text::plain("subfig")])], + }]), + children: vec![Block::paragraph(vec![Text::plain("content")])], + attributes: BlockAttributes::default(), + }; + + let content = Content::new(vec![Block::Figure(fig)]); + let errors = validate_content(&content); + assert!(errors.iter().any(|e| e.message.contains("duplicate"))); + } + + #[test] + fn test_heading_level_clamped_on_deser() { + let json = r#"{"type":"heading","level":0,"children":[{"value":"Zero"}]}"#; + let block: Block = serde_json::from_str(json).unwrap(); + if let Block::Heading { level, .. } = block { + assert_eq!(level, 1, "level 0 should be clamped to 1"); + } else { + panic!("Expected Heading"); + } + + let json = r#"{"type":"heading","level":99,"children":[{"value":"High"}]}"#; + let block: Block = serde_json::from_str(json).unwrap(); + if let Block::Heading { level, .. } = block { + assert_eq!(level, 6, "level 99 should be clamped to 6"); + } else { + panic!("Expected Heading"); + } + } + #[test] fn test_measurement_empty_display() { let content = Content::new(vec![Block::Measurement( diff --git a/cdx-core/src/document/io.rs b/cdx-core/src/document/io.rs index 2f301c5..328cfdb 100644 --- a/cdx-core/src/document/io.rs +++ b/cdx-core/src/document/io.rs @@ -217,6 +217,8 @@ impl Document { signatures: signatures_ref, encryption: encryption_ref, }); + } else { + manifest.security = None; } } diff --git a/cdx-core/src/document/mod.rs b/cdx-core/src/document/mod.rs index 6103dce..850eac3 100644 --- a/cdx-core/src/document/mod.rs +++ b/cdx-core/src/document/mod.rs @@ -97,6 +97,7 @@ macro_rules! define_extension_accessors { #[doc = concat!("Get a mutable reference to the ", $label, ".\n\n# Errors\n\nReturns an error if the document is in an immutable state.")] pub fn $field_mut(&mut self) -> Result> { self.require_mutable(concat!("modify ", $label))?; + self.manifest.modified = chrono::Utc::now(); Ok(self.$field.as_mut()) } @@ -110,6 +111,7 @@ macro_rules! define_extension_accessors { pub fn $set(&mut self, value: $type) -> Result<()> { self.require_mutable(concat!("set ", $label))?; self.$field = Some(value); + self.manifest.modified = chrono::Utc::now(); Ok(()) } @@ -117,6 +119,7 @@ macro_rules! define_extension_accessors { pub fn $clear(&mut self) -> Result<()> { self.require_mutable(concat!("remove ", $label))?; self.$field = None; + self.manifest.modified = chrono::Utc::now(); Ok(()) } }; @@ -185,6 +188,10 @@ impl Document { pub fn content_mut(&mut self) -> Result<&mut Content> { self.require_mutable("modify content")?; self.manifest.modified = Utc::now(); + // Reset document ID so freeze() recomputes it from the modified content + if !self.manifest.id.is_pending() { + self.manifest.id = DocumentId::pending(); + } Ok(&mut self.content) } @@ -202,6 +209,10 @@ impl Document { pub fn dublin_core_mut(&mut self) -> Result<&mut DublinCore> { self.require_mutable("modify Dublin Core metadata")?; self.manifest.modified = Utc::now(); + // Reset document ID so freeze() recomputes it from the modified metadata + if !self.manifest.id.is_pending() { + self.manifest.id = DocumentId::pending(); + } Ok(&mut self.dublin_core) } diff --git a/cdx-core/src/document/security.rs b/cdx-core/src/document/security.rs index 1d249a1..0dad518 100644 --- a/cdx-core/src/document/security.rs +++ b/cdx-core/src/document/security.rs @@ -116,6 +116,7 @@ impl Document { pub fn set_encryption(&mut self, metadata: EncryptionMetadata) -> Result<()> { self.require_mutable("set encryption")?; self.encryption_metadata = Some(metadata); + self.manifest.modified = chrono::Utc::now(); Ok(()) } @@ -128,6 +129,7 @@ impl Document { pub fn clear_encryption(&mut self) -> Result<()> { self.require_mutable("remove encryption")?; self.encryption_metadata = None; + self.manifest.modified = chrono::Utc::now(); Ok(()) } } diff --git a/cdx-core/src/document/state.rs b/cdx-core/src/document/state.rs index 0a526ac..4ba5d1d 100644 --- a/cdx-core/src/document/state.rs +++ b/cdx-core/src/document/state.rs @@ -67,7 +67,8 @@ impl Document { if self.manifest.lineage.is_none() { return Err(crate::Error::StateRequirementNotMet { state: DocumentState::Frozen, - requirement: "lineage information".to_string(), + requirement: "lineage information (call set_lineage for root documents)" + .to_string(), }); } @@ -206,7 +207,12 @@ impl Document { self.require_mutable("modify lineage")?; let lineage = if let Some(parent_id) = parent { - Lineage::from_parent(parent_id, None).with_note(note.unwrap_or_default()) + let mut l = Lineage::from_parent(parent_id, None); + l.version = Some(version); + if let Some(n) = note { + l = l.with_note(n); + } + l } else { let mut l = Lineage::root(); l.version = Some(version); diff --git a/cdx-core/src/document/tests.rs b/cdx-core/src/document/tests.rs index c20cea1..13a568b 100644 --- a/cdx-core/src/document/tests.rs +++ b/cdx-core/src/document/tests.rs @@ -478,4 +478,153 @@ mod tests { assert_eq!(doc.comments().unwrap().comments.len(), 2); } + + #[cfg(any(feature = "signatures", feature = "encryption"))] + #[test] + fn test_write_to_clears_stale_security_ref() { + use crate::manifest::SecurityRef; + + // Create a document and add a signature + let mut doc = Document::builder() + .title("Security Ref Test") + .creator("Author") + .add_paragraph("Content") + .build() + .unwrap(); + + // Manually set a security reference as if it had signatures before + doc.manifest_mut().security = Some(SecurityRef { + signatures: Some("security/signatures.json".to_string()), + encryption: None, + }); + + // Save and reload — signature_file is None so the security ref should be cleared + let bytes = doc.to_bytes().unwrap(); + let loaded = Document::from_bytes(bytes).unwrap(); + assert!( + loaded.manifest().security.is_none(), + "security ref should be None when no signatures or encryption exist" + ); + } + + #[test] + fn test_root_document_can_freeze_after_set_lineage() { + let mut doc = Document::builder() + .title("Root Doc") + .creator("Author") + .add_paragraph("Content") + .build() + .unwrap(); + + // Set root lineage + doc.set_lineage(None, 1, None).unwrap(); + assert!(doc.manifest().lineage.is_some()); + } + + #[cfg(feature = "signatures")] + #[test] + fn test_freeze_lineage_error_message_mentions_set_lineage() { + let mut doc = Document::builder() + .title("Lineage Error Test") + .creator("Author") + .add_paragraph("Content") + .build() + .unwrap(); + + doc.submit_for_review().unwrap(); + + // Add a dummy signature so the signature check passes + #[cfg(feature = "signatures")] + { + use crate::security::{Signature, SignatureAlgorithm, SignerInfo}; + let sig = Signature::new( + "sig-1", + SignatureAlgorithm::ES256, + SignerInfo::new("test@example.com"), + "dummysig", + ); + doc.add_signature(sig).unwrap(); + } + + let err = doc.freeze().unwrap_err(); + let msg = err.to_string(); + assert!( + msg.contains("set_lineage"), + "error message should mention set_lineage, got: {msg}" + ); + } + + // Phase 2: Document mutation consistency tests + + #[test] + fn test_set_extension_updates_modified() { + use crate::extensions::Bibliography; + + let mut doc = Document::builder() + .title("Extension Modified Test") + .creator("Author") + .add_paragraph("Content") + .build() + .unwrap(); + + let before = doc.manifest().modified; + std::thread::sleep(std::time::Duration::from_millis(10)); + + doc.set_bibliography(Bibliography::default()).unwrap(); + assert!( + doc.manifest().modified > before, + "modified timestamp should update after set_bibliography" + ); + } + + #[test] + fn test_clear_extension_updates_modified() { + use crate::extensions::Bibliography; + + let mut doc = Document::builder() + .title("Clear Extension Modified Test") + .creator("Author") + .add_paragraph("Content") + .build() + .unwrap(); + + doc.set_bibliography(Bibliography::default()).unwrap(); + let before = doc.manifest().modified; + std::thread::sleep(std::time::Duration::from_millis(10)); + + doc.clear_bibliography().unwrap(); + assert!( + doc.manifest().modified > before, + "modified timestamp should update after clear_bibliography" + ); + } + + #[cfg(feature = "encryption")] + #[test] + fn test_set_encryption_updates_modified() { + use crate::security::{EncryptionAlgorithm, EncryptionMetadata}; + + let mut doc = Document::builder() + .title("Encryption Modified Test") + .creator("Author") + .add_paragraph("Content") + .build() + .unwrap(); + + let before = doc.manifest().modified; + std::thread::sleep(std::time::Duration::from_millis(10)); + + let meta = EncryptionMetadata { + algorithm: EncryptionAlgorithm::Aes256Gcm, + kdf: None, + wrapped_key: None, + key_management: None, + recipients: Vec::new(), + }; + doc.set_encryption(meta).unwrap(); + assert!( + doc.manifest().modified > before, + "modified timestamp should update after set_encryption" + ); + } } diff --git a/cdx-core/src/error.rs b/cdx-core/src/error.rs index 191b1e8..b96069e 100644 --- a/cdx-core/src/error.rs +++ b/cdx-core/src/error.rs @@ -192,6 +192,13 @@ pub(crate) fn invalid_manifest(reason: impl Into) -> Error { } } +/// Create an [`Error::SignatureError`] with a formatted reason. +pub(crate) fn signature_error(reason: impl Into) -> Error { + Error::SignatureError { + reason: reason.into(), + } +} + /// Create an [`Error::EncryptionError`] with a formatted reason. pub(crate) fn encryption_error(reason: impl Into) -> Error { Error::EncryptionError { diff --git a/cdx-core/src/extensions/collaboration/thread.rs b/cdx-core/src/extensions/collaboration/thread.rs index e77b9c5..84716a7 100644 --- a/cdx-core/src/extensions/collaboration/thread.rs +++ b/cdx-core/src/extensions/collaboration/thread.rs @@ -84,10 +84,63 @@ impl CommentThread { None } - /// Find a mutable comment. + /// Find a mutable comment, including in nested replies. fn find_comment_mut(&mut self, id: &str) -> Option<&mut Comment> { - // Note: Can't recurse into replies with mutable reference easily - // This is a limitation of the current implementation - self.comments.iter_mut().find(|comment| comment.id == id) + Self::find_comment_mut_recursive(&mut self.comments, id) + } + + /// Recursively search for a mutable comment by ID. + fn find_comment_mut_recursive<'a>( + comments: &'a mut [Comment], + id: &str, + ) -> Option<&'a mut Comment> { + for comment in comments { + if comment.id == id { + return Some(comment); + } + if let Some(found) = Self::find_comment_mut_recursive(&mut comment.replies, id) { + return Some(found); + } + } + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::extensions::collaboration::Collaborator; + + #[test] + fn test_get_mut_finds_reply() { + let mut thread = CommentThread::new(); + let author = Collaborator::new("Alice"); + + let mut parent = Comment::new("c1", "block-1", author.clone(), "Parent comment"); + let reply = Comment::new("reply-1", "block-1", author, "Reply to parent"); + parent.replies.push(reply); + + thread.add(parent); + + // get_mut should find the nested reply + assert!( + thread.get_mut("reply-1").is_some(), + "get_mut should find nested replies" + ); + + // Mutate the reply + if let Some(reply) = thread.get_mut("reply-1") { + reply.resolved = true; + } + + // Verify mutation persisted + let reply = thread.get("reply-1").unwrap(); + assert!(reply.resolved); + } + + #[test] + fn test_get_mut_returns_none_for_missing() { + let mut thread = CommentThread::new(); + assert!(thread.get_mut("nonexistent").is_none()); } } diff --git a/cdx-core/src/extensions/semantic/bibliography.rs b/cdx-core/src/extensions/semantic/bibliography.rs index 7ec5092..a0f09bc 100644 --- a/cdx-core/src/extensions/semantic/bibliography.rs +++ b/cdx-core/src/extensions/semantic/bibliography.rs @@ -391,7 +391,7 @@ impl Author { } /// A partial date (year, year-month, or full date). -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct PartialDate { /// Year. @@ -423,6 +423,9 @@ impl PartialDate { } /// Create a year-month date. + /// + /// Note: `month` should be in the range 1-12. Use [`Self::try_year_month`] + /// for validated construction. #[must_use] pub const fn year_month(year: i32, month: u8) -> Self { Self { @@ -433,7 +436,22 @@ impl PartialDate { } } + /// Create a year-month date with validation. + /// + /// # Errors + /// + /// Returns an error if month is not in the range 1-12. + pub fn try_year_month(year: i32, month: u8) -> Result { + if !(1..=12).contains(&month) { + return Err(format!("month must be 1-12, got {month}")); + } + Ok(Self::year_month(year, month)) + } + /// Create a full date. + /// + /// Note: `month` should be 1-12 and `day` should be 1-31. + /// Use [`Self::try_full`] for validated construction. #[must_use] pub const fn full(year: i32, month: u8, day: u8) -> Self { Self { @@ -444,6 +462,21 @@ impl PartialDate { } } + /// Create a full date with validation. + /// + /// # Errors + /// + /// Returns an error if month is not 1-12 or day is not 1-31. + pub fn try_full(year: i32, month: u8, day: u8) -> Result { + if !(1..=12).contains(&month) { + return Err(format!("month must be 1-12, got {month}")); + } + if !(1..=31).contains(&day) { + return Err(format!("day must be 1-31, got {day}")); + } + Ok(Self::full(year, month, day)) + } + /// Create a seasonal date. #[must_use] pub fn seasonal(year: i32, season: impl Into) -> Self { @@ -456,6 +489,46 @@ impl PartialDate { } } +impl<'de> Deserialize<'de> for PartialDate { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct Raw { + year: i32, + #[serde(default)] + month: Option, + #[serde(default)] + day: Option, + #[serde(default)] + season: Option, + } + let raw = Raw::deserialize(deserializer)?; + if let Some(m) = raw.month { + if !(1..=12).contains(&m) { + return Err(serde::de::Error::custom(format!( + "month must be 1-12, got {m}" + ))); + } + } + if let Some(d) = raw.day { + if !(1..=31).contains(&d) { + return Err(serde::de::Error::custom(format!( + "day must be 1-31, got {d}" + ))); + } + } + Ok(PartialDate { + year: raw.year, + month: raw.month, + day: raw.day, + season: raw.season, + }) + } +} + impl std::fmt::Display for PartialDate { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Some(season) = &self.season { @@ -468,3 +541,55 @@ impl std::fmt::Display for PartialDate { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_try_year_month_valid() { + assert!(PartialDate::try_year_month(2024, 1).is_ok()); + assert!(PartialDate::try_year_month(2024, 12).is_ok()); + } + + #[test] + fn test_try_year_month_invalid() { + assert!(PartialDate::try_year_month(2024, 0).is_err()); + assert!(PartialDate::try_year_month(2024, 13).is_err()); + } + + #[test] + fn test_try_full_valid() { + assert!(PartialDate::try_full(2024, 6, 15).is_ok()); + } + + #[test] + fn test_try_full_invalid() { + assert!(PartialDate::try_full(2024, 0, 15).is_err()); + assert!(PartialDate::try_full(2024, 6, 0).is_err()); + assert!(PartialDate::try_full(2024, 6, 32).is_err()); + } + + #[test] + fn test_partial_date_deser_rejects_invalid_month() { + let json = r#"{"year":2024,"month":13}"#; + let result: Result = serde_json::from_str(json); + assert!(result.is_err()); + } + + #[test] + fn test_partial_date_deser_rejects_invalid_day() { + let json = r#"{"year":2024,"month":6,"day":32}"#; + let result: Result = serde_json::from_str(json); + assert!(result.is_err()); + } + + #[test] + fn test_partial_date_deser_accepts_valid() { + let json = r#"{"year":2024,"month":6,"day":15}"#; + let result: PartialDate = serde_json::from_str(json).unwrap(); + assert_eq!(result.year, 2024); + assert_eq!(result.month, Some(6)); + assert_eq!(result.day, Some(15)); + } +} diff --git a/cdx-core/src/presentation/responsive.rs b/cdx-core/src/presentation/responsive.rs index 5c14b96..9f91ed4 100644 --- a/cdx-core/src/presentation/responsive.rs +++ b/cdx-core/src/presentation/responsive.rs @@ -270,6 +270,7 @@ impl ResponsiveStyle { } /// Merge source style into destination, source values take precedence. +#[allow(clippy::too_many_lines)] fn merge_styles(dest: &mut Style, source: &Style) { if source.font_family.is_some() { dest.font_family.clone_from(&source.font_family); @@ -358,6 +359,34 @@ fn merge_styles(dest: &mut Style, source: &Style) { if source.extends.is_some() { dest.extends.clone_from(&source.extends); } + if source.writing_mode.is_some() { + dest.writing_mode.clone_from(&source.writing_mode); + } + if source.z_index.is_some() { + dest.z_index = source.z_index; + } + if source.background_image.is_some() { + dest.background_image.clone_from(&source.background_image); + } + if source.background_size.is_some() { + dest.background_size.clone_from(&source.background_size); + } + if source.background_position.is_some() { + dest.background_position + .clone_from(&source.background_position); + } + if source.background_repeat.is_some() { + dest.background_repeat.clone_from(&source.background_repeat); + } + if source.opacity.is_some() { + dest.opacity = source.opacity; + } + if source.border_radius.is_some() { + dest.border_radius.clone_from(&source.border_radius); + } + if source.box_shadow.is_some() { + dest.box_shadow.clone_from(&source.box_shadow); + } } #[cfg(test)] @@ -506,6 +535,97 @@ mod tests { assert!(h1_style.breakpoints.contains_key("mobile")); } + #[test] + fn test_merge_styles_all_fields() { + use crate::presentation::style::{Color, WritingMode}; + + // Create a source style with every field set to a non-default value + let source = Style { + font_family: Some("serif".to_string()), + font_size: Some(CssValue::String("18px".to_string())), + font_weight: Some(FontWeight::Number(700)), + font_style: Some("italic".to_string()), + line_height: Some(CssValue::Number(1.8)), + letter_spacing: Some(CssValue::String("0.05em".to_string())), + text_align: Some(crate::presentation::style::TextAlign::Center), + text_decoration: Some("underline".to_string()), + text_transform: Some("uppercase".to_string()), + color: Some(Color::hex("#ff0000".to_string())), + margin_top: Some(CssValue::String("10px".to_string())), + margin_right: Some(CssValue::String("11px".to_string())), + margin_bottom: Some(CssValue::String("12px".to_string())), + margin_left: Some(CssValue::String("13px".to_string())), + padding_top: Some(CssValue::String("14px".to_string())), + padding_right: Some(CssValue::String("15px".to_string())), + padding_bottom: Some(CssValue::String("16px".to_string())), + padding_left: Some(CssValue::String("17px".to_string())), + border_width: Some(CssValue::String("2px".to_string())), + border_style: Some("solid".to_string()), + border_color: Some(Color::hex("#000".to_string())), + background_color: Some(Color::hex("#fff".to_string())), + width: Some(CssValue::String("100%".to_string())), + height: Some(CssValue::String("auto".to_string())), + max_width: Some(CssValue::String("800px".to_string())), + max_height: Some(CssValue::String("600px".to_string())), + page_break_before: Some("always".to_string()), + page_break_after: Some("avoid".to_string()), + extends: Some("base".to_string()), + writing_mode: Some(WritingMode::VerticalRl), + z_index: Some(42), + background_image: Some("url(bg.png)".to_string()), + background_size: Some("cover".to_string()), + background_position: Some("center".to_string()), + background_repeat: Some("no-repeat".to_string()), + opacity: Some(0.9), + border_radius: Some(CssValue::String("8px".to_string())), + box_shadow: Some("0 2px 4px rgba(0,0,0,0.2)".to_string()), + }; + + // Merge into a default (empty) style + let mut dest = Style::default(); + merge_styles(&mut dest, &source); + + // Assert every field was transferred + assert_eq!(dest.font_family, source.font_family); + assert_eq!(dest.font_size, source.font_size); + assert_eq!(dest.font_weight, source.font_weight); + assert_eq!(dest.font_style, source.font_style); + assert_eq!(dest.line_height, source.line_height); + assert_eq!(dest.letter_spacing, source.letter_spacing); + assert_eq!(dest.text_align, source.text_align); + assert_eq!(dest.text_decoration, source.text_decoration); + assert_eq!(dest.text_transform, source.text_transform); + assert_eq!(dest.color, source.color); + assert_eq!(dest.margin_top, source.margin_top); + assert_eq!(dest.margin_right, source.margin_right); + assert_eq!(dest.margin_bottom, source.margin_bottom); + assert_eq!(dest.margin_left, source.margin_left); + assert_eq!(dest.padding_top, source.padding_top); + assert_eq!(dest.padding_right, source.padding_right); + assert_eq!(dest.padding_bottom, source.padding_bottom); + assert_eq!(dest.padding_left, source.padding_left); + assert_eq!(dest.border_width, source.border_width); + assert_eq!(dest.border_style, source.border_style); + assert_eq!(dest.border_color, source.border_color); + assert_eq!(dest.background_color, source.background_color); + assert_eq!(dest.width, source.width); + assert_eq!(dest.height, source.height); + assert_eq!(dest.max_width, source.max_width); + assert_eq!(dest.max_height, source.max_height); + assert_eq!(dest.page_break_before, source.page_break_before); + assert_eq!(dest.page_break_after, source.page_break_after); + assert_eq!(dest.extends, source.extends); + assert_eq!(dest.writing_mode, source.writing_mode); + assert_eq!(dest.z_index, source.z_index); + assert_eq!(dest.background_image, source.background_image); + assert_eq!(dest.background_size, source.background_size); + assert_eq!(dest.background_position, source.background_position); + assert_eq!(dest.background_repeat, source.background_repeat); + assert_eq!(dest.opacity, source.opacity); + assert_eq!(dest.border_radius, source.border_radius); + assert_eq!(dest.box_shadow, source.box_shadow); + } + #[test] fn test_round_trip() { let base = Style { diff --git a/cdx-core/src/provenance/ethereum.rs b/cdx-core/src/provenance/ethereum.rs index b275d25..9c6b485 100644 --- a/cdx-core/src/provenance/ethereum.rs +++ b/cdx-core/src/provenance/ethereum.rs @@ -408,7 +408,10 @@ pub fn verify_offline( if let (Some(block_num), Some(block_ts)) = (timestamp.block_number, timestamp.block_timestamp) { - return EthereumVerification::success(block_num, confirmations, block_ts); + let mut result = EthereumVerification::success(block_num, confirmations, block_ts); + // Offline verification cannot verify hash matches — requires on-chain lookup + result.hash_matches = false; + return result; } } else { return EthereumVerification::failure(format!( @@ -516,6 +519,8 @@ mod tests { let result = verify_offline(×tamp, &config); assert!(result.verified); + // Offline verification cannot verify hash matches + assert!(!result.hash_matches); } #[test] diff --git a/cdx-core/src/provenance/ots.rs b/cdx-core/src/provenance/ots.rs index 28ea182..3158ea1 100644 --- a/cdx-core/src/provenance/ots.rs +++ b/cdx-core/src/provenance/ots.rs @@ -364,9 +364,10 @@ impl OtsClient { let _ = document_id; Ok(TimestampVerification { - valid: true, + valid: false, status: VerificationStatus::Pending, - message: "Timestamp proof present (full verification requires upgrade)".to_string(), + message: "Timestamp proof present but unverified (full verification requires upgrade)" + .to_string(), }) } } @@ -599,7 +600,7 @@ mod tests { TimestampRecord::open_timestamps(Utc::now(), BASE64.encode(b"some proof data")); let result = client.verify_timestamp(×tamp, &doc_id).unwrap(); - assert!(result.valid); + assert!(!result.valid); assert_eq!(result.status, VerificationStatus::Pending); } } diff --git a/cdx-core/src/provenance/record.rs b/cdx-core/src/provenance/record.rs index db7d7a5..c40ca88 100644 --- a/cdx-core/src/provenance/record.rs +++ b/cdx-core/src/provenance/record.rs @@ -277,13 +277,15 @@ impl TimestampRecord { } } - /// Verify that this timestamp matches a document ID. + /// Check whether this timestamp record has a non-empty token. /// - /// Note: This only checks the token format, not cryptographic validity. - /// Full verification requires the timestamp authority's certificate or blockchain access. + /// This only validates that a token is present — it does **not** verify + /// that the token corresponds to the given `document_id`. Full verification + /// requires protocol-specific checks (RFC 3161 / OTS / blockchain). #[must_use] pub fn matches_document(&self, _document_id: &DocumentId) -> bool { - // Basic validation - token should be non-empty + // TODO: Implement protocol-specific document ID matching. + // For now, only check that a token exists. !self.token.is_empty() } } diff --git a/cdx-core/src/security/access_control.rs b/cdx-core/src/security/access_control.rs index 3b7cecd..9dc00e2 100644 --- a/cdx-core/src/security/access_control.rs +++ b/cdx-core/src/security/access_control.rs @@ -56,13 +56,20 @@ impl AccessControl { /// Get effective permissions for a principal. /// - /// Searches for matching grants and returns the most specific permissions, - /// or the default permissions if no grants match. + /// Checks specific grants (User/Group/Role) first, then falls back to + /// `Everyone` grants, then the default permissions. #[must_use] pub fn permissions_for(&self, principal: &Principal) -> &Permissions { - // Find the most specific matching grant + // Specific matches first (skip Everyone) for grant in &self.grants { - if grant.principal.matches(principal) { + if !matches!(grant.principal, Principal::Everyone) && grant.principal.matches(principal) + { + return &grant.permissions; + } + } + // Then wildcard + for grant in &self.grants { + if matches!(grant.principal, Principal::Everyone) { return &grant.permissions; } } @@ -453,4 +460,30 @@ mod tests { assert!(perms.edit); assert!(!perms.sign); } + + #[test] + fn test_permissions_for_specificity() { + // Everyone → read_only, but admin user → full_access + // Admin should get full_access even though Everyone appears first + let ac = AccessControl::deny_all() + .with_grant(PermissionGrant::new( + Principal::Everyone, + Permissions::read_only(), + )) + .with_grant(PermissionGrant::full_access_for_user("admin@example.com")); + + let admin = Principal::user("admin@example.com"); + let perms = ac.permissions_for(&admin); + assert!( + perms.edit, + "admin should get full_access, not read_only from Everyone" + ); + assert!(perms.sign); + + // Regular user should still get Everyone's read_only + let user = Principal::user("user@example.com"); + let perms = ac.permissions_for(&user); + assert!(perms.view); + assert!(!perms.edit); + } } diff --git a/cdx-core/src/security/annotations.rs b/cdx-core/src/security/annotations.rs index 40579ed..ce62db2 100644 --- a/cdx-core/src/security/annotations.rs +++ b/cdx-core/src/security/annotations.rs @@ -228,12 +228,9 @@ impl AnnotationType { } } -/// Get current timestamp in ISO 8601 format. +/// Get current timestamp in ISO 8601 / RFC 3339 format. fn chrono_now() -> String { - // Use a simple RFC 3339 format - // In production, this would use chrono or time crate - // For now, return a placeholder that tests can override - "2025-01-01T00:00:00Z".to_string() + chrono::Utc::now().to_rfc3339() } #[cfg(test)] diff --git a/cdx-core/src/security/eddsa.rs b/cdx-core/src/security/eddsa.rs index 4e667dc..4426db5 100644 --- a/cdx-core/src/security/eddsa.rs +++ b/cdx-core/src/security/eddsa.rs @@ -2,7 +2,7 @@ //! EdDSA (Ed25519) signature implementation. -use crate::error::invalid_manifest; +use crate::error::signature_error; use crate::{DocumentId, Result}; use super::signature::{Signature, SignatureAlgorithm, SignatureVerification, SignerInfo}; @@ -26,7 +26,7 @@ impl EddsaSigner { use ed25519_dalek::pkcs8::DecodePrivateKey; let signing_key = ed25519_dalek::SigningKey::from_pkcs8_pem(pem) - .map_err(|e| invalid_manifest(format!("Failed to parse EdDSA private key PEM: {e}")))?; + .map_err(|e| signature_error(format!("Failed to parse EdDSA private key PEM: {e}")))?; Ok(Self { signing_key, @@ -46,12 +46,12 @@ impl EddsaSigner { let mut key_bytes = [0u8; 32]; getrandom::fill(&mut key_bytes) - .map_err(|e| invalid_manifest(format!("System RNG failed: {e}")))?; + .map_err(|e| signature_error(format!("System RNG failed: {e}")))?; let signing_key = ed25519_dalek::SigningKey::from_bytes(&key_bytes); let verifying_key = signing_key.verifying_key(); let public_key_pem = verifying_key .to_public_key_pem(LineEnding::LF) - .map_err(|e| invalid_manifest(format!("Failed to encode EdDSA public key: {e}")))?; + .map_err(|e| signature_error(format!("Failed to encode EdDSA public key: {e}")))?; Ok(( Self { @@ -73,7 +73,7 @@ impl EddsaSigner { self.signing_key .verifying_key() .to_public_key_pem(LineEnding::LF) - .map_err(|e| invalid_manifest(format!("Failed to encode EdDSA public key: {e}"))) + .map_err(|e| signature_error(format!("Failed to encode EdDSA public key: {e}"))) } } @@ -135,7 +135,7 @@ impl EddsaVerifier { use ed25519_dalek::pkcs8::DecodePublicKey; let verifying_key = ed25519_dalek::VerifyingKey::from_public_key_pem(pem) - .map_err(|e| invalid_manifest(format!("Failed to parse EdDSA public key PEM: {e}")))?; + .map_err(|e| signature_error(format!("Failed to parse EdDSA public key PEM: {e}")))?; Ok(Self { verifying_key }) } @@ -164,7 +164,7 @@ impl Verifier for EddsaVerifier { // Decode signature from base64 let sig_bytes = base64::engine::general_purpose::STANDARD .decode(&signature.value) - .map_err(|e| invalid_manifest(format!("Failed to decode signature: {e}")))?; + .map_err(|e| signature_error(format!("Failed to decode signature: {e}")))?; // Parse signature let sig_array: [u8; 64] = diff --git a/cdx-core/src/security/encryption.rs b/cdx-core/src/security/encryption.rs index c288700..093645e 100644 --- a/cdx-core/src/security/encryption.rs +++ b/cdx-core/src/security/encryption.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; -use crate::error::{encryption_error, invalid_manifest}; +use crate::error::encryption_error; use crate::Result; /// Encryption algorithm enumeration. @@ -155,6 +155,7 @@ pub struct EncryptedData { /// AES-256-GCM encryptor. #[cfg(feature = "encryption")] +#[derive(zeroize::ZeroizeOnDrop)] pub struct Aes256GcmEncryptor { key: [u8; 32], } @@ -169,7 +170,7 @@ impl Aes256GcmEncryptor { /// Returns an error if the key is not 32 bytes. pub fn new(key: &[u8]) -> Result { let key: [u8; 32] = key.try_into().map_err(|_| { - invalid_manifest(format!( + encryption_error(format!( "Invalid key length: expected 32 bytes, got {}", key.len() )) @@ -214,12 +215,12 @@ impl Aes256GcmEncryptor { }; let cipher = Aes256Gcm::new_from_slice(&self.key) - .map_err(|e| invalid_manifest(format!("Failed to create cipher: {e}")))?; + .map_err(|e| encryption_error(format!("Failed to create cipher: {e}")))?; let nonce_obj = Nonce::from(*nonce); let ciphertext = cipher .encrypt(&nonce_obj, plaintext) - .map_err(|e| invalid_manifest(format!("Encryption failed: {e}")))?; + .map_err(|e| encryption_error(format!("Encryption failed: {e}")))?; // GCM appends the tag to the ciphertext let tag_start = ciphertext.len().saturating_sub(16); @@ -244,24 +245,25 @@ impl Aes256GcmEncryptor { }; let nonce: [u8; 12] = nonce.try_into().map_err(|_| { - invalid_manifest(format!( + encryption_error(format!( "Invalid nonce length: expected 12 bytes, got {}", nonce.len() )) })?; let cipher = Aes256Gcm::new_from_slice(&self.key) - .map_err(|e| invalid_manifest(format!("Failed to create cipher: {e}")))?; + .map_err(|e| encryption_error(format!("Failed to create cipher: {e}")))?; let nonce_obj = Nonce::from(nonce); cipher .decrypt(&nonce_obj, ciphertext) - .map_err(|e| invalid_manifest(format!("Decryption failed: {e}"))) + .map_err(|e| encryption_error(format!("Decryption failed: {e}"))) } } /// ChaCha20-Poly1305 encryptor. #[cfg(feature = "encryption-chacha")] +#[derive(zeroize::ZeroizeOnDrop)] pub struct ChaCha20Poly1305Encryptor { key: [u8; 32], } @@ -276,7 +278,7 @@ impl ChaCha20Poly1305Encryptor { /// Returns an error if the key is not 32 bytes. pub fn new(key: &[u8]) -> Result { let key: [u8; 32] = key.try_into().map_err(|_| { - invalid_manifest(format!( + encryption_error(format!( "Invalid key length: expected 32 bytes, got {}", key.len() )) @@ -321,12 +323,12 @@ impl ChaCha20Poly1305Encryptor { }; let cipher = ChaCha20Poly1305::new_from_slice(&self.key) - .map_err(|e| invalid_manifest(format!("Failed to create cipher: {e}")))?; + .map_err(|e| encryption_error(format!("Failed to create cipher: {e}")))?; let nonce_obj = Nonce::from(*nonce); let ciphertext = cipher .encrypt(&nonce_obj, plaintext) - .map_err(|e| invalid_manifest(format!("Encryption failed: {e}")))?; + .map_err(|e| encryption_error(format!("Encryption failed: {e}")))?; // Poly1305 appends the tag to the ciphertext (16 bytes) let tag_start = ciphertext.len().saturating_sub(16); @@ -351,19 +353,19 @@ impl ChaCha20Poly1305Encryptor { }; let nonce: [u8; 12] = nonce.try_into().map_err(|_| { - invalid_manifest(format!( + encryption_error(format!( "Invalid nonce length: expected 12 bytes, got {}", nonce.len() )) })?; let cipher = ChaCha20Poly1305::new_from_slice(&self.key) - .map_err(|e| invalid_manifest(format!("Failed to create cipher: {e}")))?; + .map_err(|e| encryption_error(format!("Failed to create cipher: {e}")))?; let nonce_obj = Nonce::from(nonce); cipher .decrypt(&nonce_obj, ciphertext) - .map_err(|e| invalid_manifest(format!("Decryption failed: {e}"))) + .map_err(|e| encryption_error(format!("Decryption failed: {e}"))) } } @@ -620,7 +622,7 @@ pub struct Pbes2WrappedKeyData { /// then wraps the content encryption key with AES Key Wrap (RFC 3394). #[cfg(feature = "key-wrapping-pbes2")] pub struct Pbes2KeyWrapper { - password: Vec, + password: zeroize::Zeroizing>, iterations: u32, } @@ -629,13 +631,30 @@ impl Pbes2KeyWrapper { /// Default PBKDF2 iteration count (600,000). pub const DEFAULT_ITERATIONS: u32 = 600_000; + /// Minimum allowed PBKDF2 iteration count. + pub const MIN_ITERATIONS: u32 = 10_000; + + /// Maximum allowed PBKDF2 iteration count. + pub const MAX_ITERATIONS: u32 = 10_000_000; + /// Create a key wrapper with the given password and iteration count. - #[must_use] - pub fn new(password: impl AsRef<[u8]>, iterations: u32) -> Self { - Self { - password: password.as_ref().to_vec(), - iterations, + /// + /// # Errors + /// + /// Returns an error if `iterations` is outside the allowed range + /// (`MIN_ITERATIONS..=MAX_ITERATIONS`). + pub fn new(password: impl AsRef<[u8]>, iterations: u32) -> Result { + if !(Self::MIN_ITERATIONS..=Self::MAX_ITERATIONS).contains(&iterations) { + return Err(encryption_error(format!( + "PBKDF2 iterations must be between {} and {}, got {iterations}", + Self::MIN_ITERATIONS, + Self::MAX_ITERATIONS + ))); } + Ok(Self { + password: zeroize::Zeroizing::new(password.as_ref().to_vec()), + iterations, + }) } /// Wrap a content encryption key. @@ -678,7 +697,7 @@ impl Pbes2KeyWrapper { /// in the wrapped data, then unwraps the content encryption key. #[cfg(feature = "key-wrapping-pbes2")] pub struct Pbes2KeyUnwrapper { - password: Vec, + password: zeroize::Zeroizing>, } #[cfg(feature = "key-wrapping-pbes2")] @@ -687,7 +706,7 @@ impl Pbes2KeyUnwrapper { #[must_use] pub fn new(password: impl AsRef<[u8]>) -> Self { Self { - password: password.as_ref().to_vec(), + password: zeroize::Zeroizing::new(password.as_ref().to_vec()), } } @@ -698,10 +717,23 @@ impl Pbes2KeyUnwrapper { /// /// # Errors /// - /// Returns an error if the password is wrong or the wrapped data is tampered. + /// Returns an error if the password is wrong, the wrapped data is tampered, + /// or the iteration count is outside the allowed range. pub fn unwrap(&self, data: &Pbes2WrappedKeyData) -> Result> { use aes_kw::{cipher::KeyInit, KwAes256}; + // Validate iteration count before doing expensive KDF work + if !(Pbes2KeyWrapper::MIN_ITERATIONS..=Pbes2KeyWrapper::MAX_ITERATIONS) + .contains(&data.iterations) + { + return Err(encryption_error(format!( + "PBKDF2 iterations must be between {} and {}, got {}", + Pbes2KeyWrapper::MIN_ITERATIONS, + Pbes2KeyWrapper::MAX_ITERATIONS, + data.iterations + ))); + } + // Derive KEK via PBKDF2-HMAC-SHA256 (same params as wrap) let mut kek_bytes = [0u8; 32]; pbkdf2::pbkdf2_hmac::( @@ -1306,7 +1338,7 @@ mod pbes2_tests { let password = b"correct horse battery staple"; let content_key = Aes256GcmEncryptor::generate_key(); - let wrapper = Pbes2KeyWrapper::new(password, Pbes2KeyWrapper::DEFAULT_ITERATIONS); + let wrapper = Pbes2KeyWrapper::new(password, Pbes2KeyWrapper::DEFAULT_ITERATIONS).unwrap(); let wrapped = wrapper.wrap(&content_key).unwrap(); assert_eq!(wrapped.wrapped_key.len(), content_key.len() + 8); @@ -1323,7 +1355,8 @@ mod pbes2_tests { fn test_pbes2_wrong_password_fails() { let content_key = Aes256GcmEncryptor::generate_key(); - let wrapper = Pbes2KeyWrapper::new(b"correct password", 1000); + let wrapper = + Pbes2KeyWrapper::new(b"correct password", Pbes2KeyWrapper::MIN_ITERATIONS).unwrap(); let wrapped = wrapper.wrap(&content_key).unwrap(); let unwrapper = Pbes2KeyUnwrapper::new(b"wrong password"); @@ -1336,7 +1369,7 @@ mod pbes2_tests { let password = b"my password"; let content_key = Aes256GcmEncryptor::generate_key(); - let wrapper = Pbes2KeyWrapper::new(password, 1000); + let wrapper = Pbes2KeyWrapper::new(password, Pbes2KeyWrapper::MIN_ITERATIONS).unwrap(); let mut wrapped = wrapper.wrap(&content_key).unwrap(); // Tamper with the salt @@ -1354,9 +1387,9 @@ mod pbes2_tests { let password = b"shared password"; let content_key = Aes256GcmEncryptor::generate_key(); - // Wrap with different iteration counts - for &iterations in &[1000u32, 10_000, 100_000] { - let wrapper = Pbes2KeyWrapper::new(password, iterations); + // Wrap with different valid iteration counts + for &iterations in &[10_000u32, 100_000, 1_000_000] { + let wrapper = Pbes2KeyWrapper::new(password, iterations).unwrap(); let wrapped = wrapper.wrap(&content_key).unwrap(); assert_eq!(wrapped.iterations, iterations); @@ -1366,6 +1399,35 @@ mod pbes2_tests { } } + #[test] + fn test_pbes2_iteration_bounds() { + // Below minimum + assert!(Pbes2KeyWrapper::new(b"password", 0).is_err()); + assert!(Pbes2KeyWrapper::new(b"password", 1).is_err()); + assert!(Pbes2KeyWrapper::new(b"password", 9_999).is_err()); + + // At minimum + assert!(Pbes2KeyWrapper::new(b"password", 10_000).is_ok()); + + // At maximum + assert!(Pbes2KeyWrapper::new(b"password", 10_000_000).is_ok()); + + // Above maximum + assert!(Pbes2KeyWrapper::new(b"password", 10_000_001).is_err()); + assert!(Pbes2KeyWrapper::new(b"password", u32::MAX).is_err()); + } + + #[test] + fn test_pbes2_unwrap_rejects_bad_iterations() { + let unwrapper = Pbes2KeyUnwrapper::new(b"password"); + let data = Pbes2WrappedKeyData { + wrapped_key: vec![0u8; 40], + salt: vec![0u8; 16], + iterations: 0, + }; + assert!(unwrapper.unwrap(&data).is_err()); + } + #[test] fn test_pbes2_integration_encrypt_wrap_unwrap_decrypt() { let password = b"document encryption password"; @@ -1377,7 +1439,7 @@ mod pbes2_tests { let encrypted = encryptor.encrypt(plaintext).unwrap(); // Wrap the content key with password - let wrapper = Pbes2KeyWrapper::new(password, 1000); + let wrapper = Pbes2KeyWrapper::new(password, Pbes2KeyWrapper::MIN_ITERATIONS).unwrap(); let wrapped = wrapper.wrap(&content_key).unwrap(); // Unwrap and decrypt diff --git a/cdx-core/src/security/es384.rs b/cdx-core/src/security/es384.rs index 8014d0a..82144d3 100644 --- a/cdx-core/src/security/es384.rs +++ b/cdx-core/src/security/es384.rs @@ -2,7 +2,7 @@ //! //! This module provides ES384 signing and verification using the NIST P-384 curve. -use crate::error::invalid_manifest; +use crate::error::signature_error; use crate::{DocumentId, Result}; use super::signature::{Signature, SignatureAlgorithm, SignatureVerification, SignerInfo}; @@ -24,7 +24,7 @@ impl Es384Signer { use p384::pkcs8::DecodePrivateKey; let signing_key = p384::ecdsa::SigningKey::from_pkcs8_pem(pem) - .map_err(|e| invalid_manifest(format!("Failed to parse P-384 private key PEM: {e}")))?; + .map_err(|e| signature_error(format!("Failed to parse P-384 private key PEM: {e}")))?; Ok(Self { signing_key, @@ -47,7 +47,7 @@ impl Es384Signer { let verifying_key = signing_key.verifying_key(); let public_key_pem = verifying_key .to_public_key_pem(p384::pkcs8::LineEnding::LF) - .map_err(|e| invalid_manifest(format!("Failed to encode P-384 public key: {e}")))?; + .map_err(|e| signature_error(format!("Failed to encode P-384 public key: {e}")))?; Ok(( Self { @@ -69,7 +69,7 @@ impl Es384Signer { self.signing_key .verifying_key() .to_public_key_pem(p384::pkcs8::LineEnding::LF) - .map_err(|e| invalid_manifest(format!("Failed to encode P-384 public key: {e}"))) + .map_err(|e| signature_error(format!("Failed to encode P-384 public key: {e}"))) } } @@ -128,7 +128,7 @@ impl Es384Verifier { use p384::pkcs8::DecodePublicKey; let verifying_key = p384::ecdsa::VerifyingKey::from_public_key_pem(pem) - .map_err(|e| invalid_manifest(format!("Failed to parse P-384 public key PEM: {e}")))?; + .map_err(|e| signature_error(format!("Failed to parse P-384 public key PEM: {e}")))?; Ok(Self { verifying_key }) } @@ -156,11 +156,11 @@ impl Verifier for Es384Verifier { // Decode signature from base64 let sig_bytes = base64::engine::general_purpose::STANDARD .decode(&signature.value) - .map_err(|e| invalid_manifest(format!("Failed to decode signature: {e}")))?; + .map_err(|e| signature_error(format!("Failed to decode signature: {e}")))?; // Parse signature let ecdsa_sig = p384::ecdsa::Signature::from_slice(&sig_bytes) - .map_err(|e| invalid_manifest(format!("Invalid ES384 signature format: {e}")))?; + .map_err(|e| signature_error(format!("Invalid ES384 signature format: {e}")))?; // Verify match self.verifying_key.verify(document_id.digest(), &ecdsa_sig) { diff --git a/cdx-core/src/security/ml_dsa.rs b/cdx-core/src/security/ml_dsa.rs index 91340ce..0cca5b8 100644 --- a/cdx-core/src/security/ml_dsa.rs +++ b/cdx-core/src/security/ml_dsa.rs @@ -10,7 +10,7 @@ //! Post-quantum cryptography is still maturing. While ML-DSA-65 is //! standardized by NIST, implementations should be considered experimental. -use crate::error::invalid_manifest; +use crate::error::signature_error; use crate::{DocumentId, Result}; use super::signature::{Signature, SignatureAlgorithm, SignatureVerification, SignerInfo}; @@ -27,7 +27,7 @@ use super::signer::{Signer, Verifier}; #[cfg(feature = "ml-dsa")] pub struct MlDsaSigner { signing_key: ml_dsa::SigningKey, - seed: [u8; 32], + seed: zeroize::Zeroizing<[u8; 32]>, signer_info: SignerInfo, } @@ -49,7 +49,7 @@ impl MlDsaSigner { use ml_dsa::KeyGen; let seed: [u8; 32] = seed_bytes.try_into().map_err(|_| { - invalid_manifest(format!( + signature_error(format!( "Invalid ML-DSA-65 seed length: expected 32, got {}", seed_bytes.len() )) @@ -59,7 +59,7 @@ impl MlDsaSigner { Ok(Self { signing_key: kp.signing_key().clone(), - seed, + seed: zeroize::Zeroizing::new(seed), signer_info, }) } @@ -82,7 +82,7 @@ impl MlDsaSigner { Ok(( Self { signing_key: kp.signing_key().clone(), - seed, + seed: zeroize::Zeroizing::new(seed), signer_info, }, public_key_bytes, @@ -167,7 +167,7 @@ impl MlDsaVerifier { pub fn from_bytes(public_key_bytes: &[u8]) -> Result { let verifying_key = ml_dsa::VerifyingKey::decode(public_key_bytes.try_into().map_err(|_| { - invalid_manifest(format!( + signature_error(format!( "Invalid ML-DSA-65 public key length: got {}", public_key_bytes.len() )) @@ -200,12 +200,12 @@ impl Verifier for MlDsaVerifier { // Decode signature from base64 let sig_bytes = base64::engine::general_purpose::STANDARD .decode(&signature.value) - .map_err(|e| invalid_manifest(format!("Failed to decode signature: {e}")))?; + .map_err(|e| signature_error(format!("Failed to decode signature: {e}")))?; // Parse ML-DSA signature let ml_sig = ml_dsa::Signature::::try_from(sig_bytes.as_slice()).map_err(|_| { - invalid_manifest(format!( + signature_error(format!( "Invalid ML-DSA-65 signature length: got {}", sig_bytes.len() )) diff --git a/cdx-core/src/security/revocation.rs b/cdx-core/src/security/revocation.rs index f4bb90f..5188991 100644 --- a/cdx-core/src/security/revocation.rs +++ b/cdx-core/src/security/revocation.rs @@ -526,29 +526,76 @@ impl RevocationChecker { ) -> Result { if self.config.prefer_ocsp { // Try OCSP first - if let Some(issuer) = issuer_der { + let ocsp_err = if let Some(issuer) = issuer_der { match self.check_ocsp(cert_der, issuer).await { Ok(result) if !result.status.is_error() => return Ok(result), - _ => {} // Fall through to CRL + Ok(result) => Some(format!("OCSP returned error: {}", result.status)), + Err(e) => Some(format!("OCSP check failed: {e}")), } - } + } else { + None + }; // Fall back to CRL - self.check_crl(cert_der).await - } else { - // Try CRL first match self.check_crl(cert_der).await { Ok(result) if !result.status.is_error() => Ok(result), - _ => { - // Fall back to OCSP - if let Some(issuer) = issuer_der { - self.check_ocsp(cert_der, issuer).await - } else { - Err(crate::Error::InvalidCertificate { - reason: "CRL check failed and no issuer provided for OCSP".to_string(), - }) - } + Ok(result) => Err(crate::Error::InvalidCertificate { + reason: format!( + "CRL returned error: {}{}", + result.status, + ocsp_err + .as_ref() + .map_or(String::new(), |e| format!("; prior {e}")) + ), + }), + Err(crl_err) => Err(crate::Error::InvalidCertificate { + reason: format!( + "CRL check failed: {crl_err}{}", + ocsp_err + .as_ref() + .map_or(String::new(), |e| format!("; prior {e}")) + ), + }), + } + } else { + // Try CRL first + let crl_err = match self.check_crl(cert_der).await { + Ok(result) if !result.status.is_error() => return Ok(result), + Ok(result) => Some(format!("CRL returned error: {}", result.status)), + Err(e) => Some(format!("CRL check failed: {e}")), + }; + + // Fall back to OCSP + if let Some(issuer) = issuer_der { + match self.check_ocsp(cert_der, issuer).await { + Ok(result) if !result.status.is_error() => Ok(result), + Ok(result) => Err(crate::Error::InvalidCertificate { + reason: format!( + "OCSP returned error: {}{}", + result.status, + crl_err + .as_ref() + .map_or(String::new(), |e| format!("; prior {e}")) + ), + }), + Err(ocsp_err) => Err(crate::Error::InvalidCertificate { + reason: format!( + "OCSP check failed: {ocsp_err}{}", + crl_err + .as_ref() + .map_or(String::new(), |e| format!("; prior {e}")) + ), + }), } + } else { + Err(crate::Error::InvalidCertificate { + reason: format!( + "No issuer provided for OCSP fallback{}", + crl_err + .as_ref() + .map_or(String::new(), |e| format!("; prior {e}")) + ), + }) } } } @@ -656,33 +703,17 @@ fn build_ocsp_request( } #[cfg(feature = "ocsp")] -fn parse_ocsp_response(response: &[u8]) -> RevocationStatus { - // Parse OCSP response +fn parse_ocsp_response(_response: &[u8]) -> RevocationStatus { + // OCSP response parsing is not yet implemented. // A full implementation would: // 1. Check the response status (successful, malformed, etc.) // 2. Verify the response signature // 3. Extract the certificate status - - // Check for basic response structure - if response.is_empty() { - return RevocationStatus::Error { - message: "Empty OCSP response".to_string(), - }; - } - - // OCSP response status is the first byte after the sequence tag - // 0 = successful, 1 = malformed, 2 = internal error, etc. - if response.len() > 2 { - // This is a simplified check - full parsing would use proper ASN.1 - // Look for common success indicators - if response.contains(&0x00) { - // Likely successful response - would need full parsing to determine status - return RevocationStatus::Unknown; - } - } - + // + // Return Error rather than Unknown so callers know the check was not performed, + // rather than incorrectly treating an unparsed response as indeterminate. RevocationStatus::Error { - message: "Failed to parse OCSP response".to_string(), + message: "OCSP response parsing not yet implemented".to_string(), } } diff --git a/cdx-core/src/security/rsa_pss.rs b/cdx-core/src/security/rsa_pss.rs index 4e35563..23f1718 100644 --- a/cdx-core/src/security/rsa_pss.rs +++ b/cdx-core/src/security/rsa_pss.rs @@ -3,7 +3,7 @@ //! This module provides PS256 signing and verification using RSA with PSS padding //! and SHA-256. -use crate::error::invalid_manifest; +use crate::error::signature_error; use crate::{DocumentId, Result}; use super::signature::{Signature, SignatureAlgorithm, SignatureVerification, SignerInfo}; @@ -25,7 +25,7 @@ impl Ps256Signer { use rsa::pkcs8::DecodePrivateKey; let signing_key = rsa::RsaPrivateKey::from_pkcs8_pem(pem) - .map_err(|e| invalid_manifest(format!("Failed to parse RSA private key PEM: {e}")))?; + .map_err(|e| signature_error(format!("Failed to parse RSA private key PEM: {e}")))?; Ok(Self { signing_key, @@ -46,12 +46,12 @@ impl Ps256Signer { let signing_key = rsa::RsaPrivateKey::new(&mut rand_core::UnwrapErr(getrandom::SysRng), bits) - .map_err(|e| invalid_manifest(format!("Failed to generate RSA key: {e}")))?; + .map_err(|e| signature_error(format!("Failed to generate RSA key: {e}")))?; let public_key = signing_key.to_public_key(); let public_key_pem = public_key .to_public_key_pem(rsa::pkcs8::LineEnding::LF) - .map_err(|e| invalid_manifest(format!("Failed to encode RSA public key: {e}")))?; + .map_err(|e| signature_error(format!("Failed to encode RSA public key: {e}")))?; Ok(( Self { @@ -84,7 +84,7 @@ impl Ps256Signer { self.signing_key .to_public_key() .to_public_key_pem(rsa::pkcs8::LineEnding::LF) - .map_err(|e| invalid_manifest(format!("Failed to encode RSA public key: {e}"))) + .map_err(|e| signature_error(format!("Failed to encode RSA public key: {e}"))) } } @@ -150,7 +150,7 @@ impl Ps256Verifier { use rsa::pkcs8::DecodePublicKey; let verifying_key = rsa::RsaPublicKey::from_public_key_pem(pem) - .map_err(|e| invalid_manifest(format!("Failed to parse RSA public key PEM: {e}")))?; + .map_err(|e| signature_error(format!("Failed to parse RSA public key PEM: {e}")))?; Ok(Self { verifying_key }) } @@ -179,14 +179,14 @@ impl Verifier for Ps256Verifier { // Decode signature from base64 let sig_bytes = base64::engine::general_purpose::STANDARD .decode(&signature.value) - .map_err(|e| invalid_manifest(format!("Failed to decode signature: {e}")))?; + .map_err(|e| signature_error(format!("Failed to decode signature: {e}")))?; // Create PSS verifying key let verifying_key = VerifyingKey::::new(self.verifying_key.clone()); // Parse signature let rsa_sig = rsa::pss::Signature::try_from(sig_bytes.as_slice()) - .map_err(|e| invalid_manifest(format!("Invalid PS256 signature format: {e}")))?; + .map_err(|e| signature_error(format!("Invalid PS256 signature format: {e}")))?; // Verify match verifying_key.verify(document_id.digest(), &rsa_sig) { diff --git a/cdx-core/src/security/signer.rs b/cdx-core/src/security/signer.rs index bbc8fc8..5d560ce 100644 --- a/cdx-core/src/security/signer.rs +++ b/cdx-core/src/security/signer.rs @@ -1,6 +1,6 @@ //! Signer and verifier traits and implementations. -use crate::error::invalid_manifest; +use crate::error::signature_error; use crate::{DocumentId, Result}; use super::signature::{Signature, SignatureAlgorithm, SignatureVerification, SignerInfo}; @@ -53,7 +53,7 @@ impl EcdsaSigner { use p256::pkcs8::DecodePrivateKey; let signing_key = p256::ecdsa::SigningKey::from_pkcs8_pem(pem) - .map_err(|e| invalid_manifest(format!("Failed to parse private key PEM: {e}")))?; + .map_err(|e| signature_error(format!("Failed to parse private key PEM: {e}")))?; Ok(Self { signing_key, @@ -76,7 +76,7 @@ impl EcdsaSigner { let verifying_key = signing_key.verifying_key(); let public_key_pem = verifying_key .to_public_key_pem(p256::pkcs8::LineEnding::LF) - .map_err(|e| invalid_manifest(format!("Failed to encode public key: {e}")))?; + .map_err(|e| signature_error(format!("Failed to encode public key: {e}")))?; Ok(( Self { @@ -98,7 +98,7 @@ impl EcdsaSigner { self.signing_key .verifying_key() .to_public_key_pem(p256::pkcs8::LineEnding::LF) - .map_err(|e| invalid_manifest(format!("Failed to encode public key: {e}"))) + .map_err(|e| signature_error(format!("Failed to encode public key: {e}"))) } } @@ -160,7 +160,7 @@ impl EcdsaVerifier { use p256::pkcs8::DecodePublicKey; let verifying_key = p256::ecdsa::VerifyingKey::from_public_key_pem(pem) - .map_err(|e| invalid_manifest(format!("Failed to parse public key PEM: {e}")))?; + .map_err(|e| signature_error(format!("Failed to parse public key PEM: {e}")))?; Ok(Self { verifying_key }) } @@ -189,11 +189,11 @@ impl Verifier for EcdsaVerifier { // Decode signature from base64 let sig_bytes = base64::engine::general_purpose::STANDARD .decode(&signature.value) - .map_err(|e| invalid_manifest(format!("Failed to decode signature: {e}")))?; + .map_err(|e| signature_error(format!("Failed to decode signature: {e}")))?; // Parse signature let ecdsa_sig = p256::ecdsa::Signature::from_slice(&sig_bytes) - .map_err(|e| invalid_manifest(format!("Invalid signature format: {e}")))?; + .map_err(|e| signature_error(format!("Invalid signature format: {e}")))?; // Verify match self.verifying_key.verify(document_id.digest(), &ecdsa_sig) { diff --git a/cdx-core/src/security/webauthn.rs b/cdx-core/src/security/webauthn.rs index fa853ba..9618781 100644 --- a/cdx-core/src/security/webauthn.rs +++ b/cdx-core/src/security/webauthn.rs @@ -31,7 +31,7 @@ use sha2::{Digest, Sha256}; use super::signature::{Signature, SignatureVerification, WebAuthnSignature}; use super::Verifier; -use crate::error::invalid_manifest; +use crate::error::signature_error; use crate::{DocumentId, Result}; /// WebAuthn client data structure. @@ -85,7 +85,7 @@ impl WebAuthnVerifier { /// Returns an error if the public key cannot be parsed. pub fn new(expected_origin: impl Into, public_key: &[u8]) -> Result { let verifying_key = VerifyingKey::from_sec1_bytes(public_key) - .map_err(|e| invalid_manifest(format!("Invalid WebAuthn public key: {e}")))?; + .map_err(|e| signature_error(format!("Invalid WebAuthn public key: {e}")))?; Ok(Self { expected_origin: expected_origin.into(), @@ -103,7 +103,7 @@ impl WebAuthnVerifier { use p256::pkcs8::DecodePublicKey; let verifying_key = VerifyingKey::from_public_key_pem(pem) - .map_err(|e| invalid_manifest(format!("Invalid WebAuthn public key PEM: {e}")))?; + .map_err(|e| signature_error(format!("Invalid WebAuthn public key PEM: {e}")))?; Ok(Self { expected_origin: expected_origin.into(), @@ -122,6 +122,7 @@ impl WebAuthnVerifier { /// Verify a WebAuthn signature. fn verify_webauthn( &self, + signature_id: &str, document_id: &DocumentId, webauthn: &WebAuthnSignature, ) -> Result { @@ -130,28 +131,31 @@ impl WebAuthnVerifier { // Decode credential ID let credential_id = engine .decode(&webauthn.credential_id) - .map_err(|e| invalid_manifest(format!("Invalid credential ID base64: {e}")))?; + .map_err(|e| signature_error(format!("Invalid credential ID base64: {e}")))?; // Verify credential ID if expected if let Some(ref expected) = self.expected_credential_id { if &credential_id != expected { - return Ok(SignatureVerification::invalid("", "Credential ID mismatch")); + return Ok(SignatureVerification::invalid( + signature_id, + "Credential ID mismatch", + )); } } // Decode client data JSON let client_data_bytes = engine .decode(&webauthn.client_data_json) - .map_err(|e| invalid_manifest(format!("Invalid clientDataJSON base64: {e}")))?; + .map_err(|e| signature_error(format!("Invalid clientDataJSON base64: {e}")))?; // Parse client data let client_data: ClientData = serde_json::from_slice(&client_data_bytes) - .map_err(|e| invalid_manifest(format!("Invalid clientDataJSON: {e}")))?; + .map_err(|e| signature_error(format!("Invalid clientDataJSON: {e}")))?; // Verify type if client_data.type_ != "webauthn.get" { return Ok(SignatureVerification::invalid( - "", + signature_id, format!( "Invalid type: expected 'webauthn.get', got '{}'", client_data.type_ @@ -162,7 +166,7 @@ impl WebAuthnVerifier { // Verify origin if client_data.origin != self.expected_origin { return Ok(SignatureVerification::invalid( - "", + signature_id, format!( "Origin mismatch: expected '{}', got '{}'", self.expected_origin, client_data.origin @@ -174,11 +178,11 @@ impl WebAuthnVerifier { // WebAuthn uses base64url encoding for the challenge let challenge_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD .decode(&client_data.challenge) - .map_err(|e| invalid_manifest(format!("Invalid challenge base64url: {e}")))?; + .map_err(|e| signature_error(format!("Invalid challenge base64url: {e}")))?; if challenge_bytes != document_id.digest() { return Ok(SignatureVerification::invalid( - "", + signature_id, "Challenge does not match document ID", )); } @@ -186,21 +190,36 @@ impl WebAuthnVerifier { // Decode authenticator data let authenticator_data = engine .decode(&webauthn.authenticator_data) - .map_err(|e| invalid_manifest(format!("Invalid authenticatorData base64: {e}")))?; + .map_err(|e| signature_error(format!("Invalid authenticatorData base64: {e}")))?; // Verify authenticator data is at least 37 bytes (RP ID hash + flags + counter) if authenticator_data.len() < 37 { return Ok(SignatureVerification::invalid( - "", + signature_id, "Authenticator data too short", )); } + // Verify RP ID hash (first 32 bytes of authenticator data) + // Per WebAuthn spec section 7.2 step 13: rpIdHash must equal SHA-256(rp_id) + let expected_rp_id = self + .expected_origin + .strip_prefix("https://") + .or_else(|| self.expected_origin.strip_prefix("http://")) + .unwrap_or(&self.expected_origin); + let expected_rp_id_hash = Sha256::digest(expected_rp_id.as_bytes()); + if authenticator_data[..32] != expected_rp_id_hash[..] { + return Ok(SignatureVerification::invalid( + signature_id, + "RP ID hash mismatch in authenticator data", + )); + } + // Check user presence flag (bit 0 of flags byte at offset 32) let flags = authenticator_data[32]; if flags & 0x01 == 0 { return Ok(SignatureVerification::invalid( - "", + signature_id, "User presence flag not set", )); } @@ -208,11 +227,11 @@ impl WebAuthnVerifier { // Decode signature let signature_bytes = engine .decode(&webauthn.signature) - .map_err(|e| invalid_manifest(format!("Invalid signature base64: {e}")))?; + .map_err(|e| signature_error(format!("Invalid signature base64: {e}")))?; // Parse the signature (DER-encoded ECDSA signature) let signature = p256::ecdsa::DerSignature::from_bytes(&signature_bytes) - .map_err(|e| invalid_manifest(format!("Invalid ECDSA signature: {e}")))?; + .map_err(|e| signature_error(format!("Invalid ECDSA signature: {e}")))?; // Compute the signed data: authenticator_data || SHA-256(clientDataJSON) let client_data_hash = Sha256::digest(&client_data_bytes); @@ -221,9 +240,9 @@ impl WebAuthnVerifier { // Verify the signature match self.verifying_key.verify(&signed_data, &signature) { - Ok(()) => Ok(SignatureVerification::valid("")), + Ok(()) => Ok(SignatureVerification::valid(signature_id)), Err(e) => Ok(SignatureVerification::invalid( - "", + signature_id, format!("Signature verification failed: {e}"), )), } @@ -245,12 +264,7 @@ impl Verifier for WebAuthnVerifier { }; // Verify the WebAuthn assertion - let mut result = self.verify_webauthn(document_id, webauthn)?; - - // Update the signature ID in the result - result.signature_id.clone_from(&signature.id); - - Ok(result) + self.verify_webauthn(&signature.id, document_id, webauthn) } } diff --git a/cdx-core/src/validation/mod.rs b/cdx-core/src/validation/mod.rs index a479a81..0a733ed 100644 --- a/cdx-core/src/validation/mod.rs +++ b/cdx-core/src/validation/mod.rs @@ -161,9 +161,16 @@ enum SchemaType { /// Internal validation function. fn validate_json(json: &str, schema_type: SchemaType) -> ValidationResult { // Parse the JSON + let type_name = match schema_type { + SchemaType::Manifest => "manifest", + SchemaType::Content => "content", + SchemaType::DublinCore => "Dublin Core metadata", + SchemaType::BlockIndex => "block index", + SchemaType::Signatures => "signatures", + }; let value: serde_json::Value = - serde_json::from_str(json).map_err(|e| crate::Error::InvalidManifest { - reason: format!("Invalid JSON: {e}"), + serde_json::from_str(json).map_err(|e| crate::Error::ValidationFailed { + reason: format!("Invalid {type_name} JSON: {e}"), })?; // Get the schema for this type diff --git a/cdx-core/tests/integration.rs b/cdx-core/tests/integration.rs index 36b58b3..2664e55 100644 --- a/cdx-core/tests/integration.rs +++ b/cdx-core/tests/integration.rs @@ -1642,6 +1642,7 @@ mod ots_tests { } /// Test timestamp verification with valid proof data. + /// Note: valid is false because no actual verification occurs; only structure checks. #[test] fn test_verify_valid_proof_structure() { let client = OtsClient::new(); @@ -1650,7 +1651,7 @@ mod ots_tests { let timestamp = TimestampRecord::open_timestamps(Utc::now(), proof_data); let result = client.verify_timestamp(×tamp, &doc_id).unwrap(); - assert!(result.valid); + assert!(!result.valid); assert_eq!(result.status, VerificationStatus::Pending); } diff --git a/deny.toml b/deny.toml index e657874..d38c8a7 100644 --- a/deny.toml +++ b/deny.toml @@ -5,7 +5,11 @@ all-features = true [advisories] db-path = "~/.cargo/advisory-db" db-urls = ["https://github.com/rustsec/advisory-db"] -yanked = "deny" +# RustCrypto RC ecosystem has transiently yanked digest/cipher/crypto-common 0.11.x. +# Non-yanked replacements (0.11.2+) break hkdf/universal-hash/ctr. +# Downgrade to "warn" until the RC ecosystem stabilizes. +# Last reviewed: 2026-03-13 +yanked = "warn" # rsa has a timing side-channel (Marvin Attack) with no fixed version available (incl. 0.10-rc). # signatures-rsa is opt-in (not in default features). Review periodically.