From 54ae9cb946ede9a75bc6593cc25f0c20744b57e4 Mon Sep 17 00:00:00 2001 From: "Jeromy Statia (from Dev Box)" Date: Mon, 19 Jan 2026 10:38:27 -0800 Subject: [PATCH 01/29] Add native Rust COSE validation workspace --- .gitignore | 14 + native/rust/.gitignore | 14 + native/rust/Cargo.lock | 1226 +++++++++++++ native/rust/Cargo.toml | 43 + native/rust/README.md | 14 + native/rust/collect-coverage.ps1 | 124 ++ native/rust/collect-native-coverage.ps1 | 1 + native/rust/cose_sign1_validation/Cargo.toml | 25 + native/rust/cose_sign1_validation/README.md | 25 + .../examples/detached_payload_provider.rs | 100 ++ .../examples/validate_smoke.rs | 91 + native/rust/cose_sign1_validation/src/cose.rs | 124 ++ native/rust/cose_sign1_validation/src/lib.rs | 32 + .../src/message_fact_producer.rs | 523 ++++++ .../src/message_facts.rs | 463 +++++ .../cose_sign1_validation/src/trust_packs.rs | 121 ++ .../src/trust_plan_builder.rs | 211 +++ .../cose_sign1_validation/src/validator.rs | 1563 +++++++++++++++++ .../testdata/v1/UnitTestPayload.json | 1 + .../testdata/v1/UnitTestSignatureWithCRL.cose | Bin 0 -> 5788 bytes .../tests/cose_decode.rs | 131 ++ .../tests/counter_signature_parity.rs | 355 ++++ .../tests/detached_streaming.rs | 167 ++ .../tests/message_fact_producer_more.rs | 521 ++++++ .../tests/real_v1_cose_files.rs | 106 ++ .../tests/real_world_trust_plans.rs | 851 +++++++++ .../tests/v2_validator_parity.rs | 394 +++++ .../tests/validator_more_coverage.rs | 1217 +++++++++++++ .../tests/validator_pipeline_tests.rs | 518 ++++++ .../tests/validator_smoke.rs | 150 ++ .../Cargo.toml | 18 + .../README.md | 9 + .../examples/akv_kid_allowed.rs | 88 + .../src/facts.rs | 62 + .../src/fluent_ext.rs | 55 + .../src/lib.rs | 6 + .../src/pack.rs | 227 +++ .../tests/akv_kid.rs | 466 +++++ .../Cargo.toml | 19 + .../README.md | 9 + .../examples/x5chain_identity.rs | 78 + .../src/facts.rs | 298 ++++ .../src/fluent_ext.rs | 400 +++++ .../src/lib.rs | 7 + .../src/pack.rs | 669 +++++++ .../src/signing_key_resolver.rs | 243 +++ .../testdata/v1/1ts-statement.scitt | Bin 0 -> 6281 bytes .../testdata/v1/2ts-statement.scitt | Bin 0 -> 6906 bytes .../testdata/v1/UnitTestPayload.json | 1 + .../testdata/v1/UnitTestSignatureWithCRL.cose | Bin 0 -> 5788 bytes .../tests/cert_fact_sets.rs | 176 ++ .../tests/chain_trust_more_coverage.rs | 133 ++ .../tests/counter_signature_x5chain.rs | 166 ++ .../tests/generated_cert_extensions.rs | 706 ++++++++ .../tests/real_v1_cert_facts.rs | 335 ++++ .../tests/x5chain_identity.rs | 87 + .../cose_sign1_validation_demo/Cargo.toml | 11 + .../cose_sign1_validation_demo/src/main.rs | 171 ++ .../Cargo.toml | 22 + .../README.md | 9 + .../examples/debug_verify_scitt.rs | 201 +++ .../examples/mst_receipt_present.rs | 77 + .../src/facts.rs | 202 +++ .../src/fluent_ext.rs | 166 ++ .../src/lib.rs | 7 + .../src/pack.rs | 336 ++++ .../src/receipt_verify.rs | 769 ++++++++ ...cp.confidential-ledger.azure.com.jwks.json | 1 + .../tests/mst_receipts.rs | 431 +++++ .../cose_sign1_validation_trust/Cargo.toml | 18 + .../cose_sign1_validation_trust/README.md | 16 + .../examples/trust_plan_minimal.rs | 78 + .../cose_sign1_validation_trust/src/audit.rs | 44 + .../src/cose_sign1.rs | 256 +++ .../src/decision.rs | 38 + .../cose_sign1_validation_trust/src/error.rs | 16 + .../src/evaluation_options.rs | 29 + .../src/fact_properties.rs | 42 + .../cose_sign1_validation_trust/src/facts.rs | 354 ++++ .../cose_sign1_validation_trust/src/field.rs | 27 + .../cose_sign1_validation_trust/src/fluent.rs | 926 ++++++++++ .../cose_sign1_validation_trust/src/ids.rs | 49 + .../cose_sign1_validation_trust/src/lib.rs | 21 + .../cose_sign1_validation_trust/src/plan.rs | 123 ++ .../cose_sign1_validation_trust/src/policy.rs | 62 + .../cose_sign1_validation_trust/src/rules.rs | 730 ++++++++ .../src/subject.rs | 65 + .../tests/compiled_plan_semantics.rs | 115 ++ .../tests/cose_header_parsing_tests.rs | 153 ++ .../tests/declarative_predicates.rs | 103 ++ .../tests/ids_subject_decision_tests.rs | 97 + .../tests/rules_policy_audit_tests.rs | 412 +++++ native/rust/docs/README.md | 24 + native/rust/docs/azure-key-vault-pack.md | 21 + native/rust/docs/certificate-pack.md | 35 + native/rust/docs/demo-exe.md | 25 + native/rust/docs/detached-payloads.md | 37 + native/rust/docs/extension-points.md | 58 + native/rust/docs/getting-started.md | 44 + native/rust/docs/transparent-mst-pack.md | 16 + native/rust/docs/troubleshooting.md | 29 + native/rust/docs/trust-model.md | 36 + native/rust/docs/trust-subjects.md | 33 + native/rust/docs/validator-architecture.md | 49 + native/rust/scripts/check_repo_rules.py | 89 + 105 files changed, 20056 insertions(+) create mode 100644 native/rust/.gitignore create mode 100644 native/rust/Cargo.lock create mode 100644 native/rust/Cargo.toml create mode 100644 native/rust/README.md create mode 100644 native/rust/collect-coverage.ps1 create mode 100644 native/rust/collect-native-coverage.ps1 create mode 100644 native/rust/cose_sign1_validation/Cargo.toml create mode 100644 native/rust/cose_sign1_validation/README.md create mode 100644 native/rust/cose_sign1_validation/examples/detached_payload_provider.rs create mode 100644 native/rust/cose_sign1_validation/examples/validate_smoke.rs create mode 100644 native/rust/cose_sign1_validation/src/cose.rs create mode 100644 native/rust/cose_sign1_validation/src/lib.rs create mode 100644 native/rust/cose_sign1_validation/src/message_fact_producer.rs create mode 100644 native/rust/cose_sign1_validation/src/message_facts.rs create mode 100644 native/rust/cose_sign1_validation/src/trust_packs.rs create mode 100644 native/rust/cose_sign1_validation/src/trust_plan_builder.rs create mode 100644 native/rust/cose_sign1_validation/src/validator.rs create mode 100644 native/rust/cose_sign1_validation/testdata/v1/UnitTestPayload.json create mode 100644 native/rust/cose_sign1_validation/testdata/v1/UnitTestSignatureWithCRL.cose create mode 100644 native/rust/cose_sign1_validation/tests/cose_decode.rs create mode 100644 native/rust/cose_sign1_validation/tests/counter_signature_parity.rs create mode 100644 native/rust/cose_sign1_validation/tests/detached_streaming.rs create mode 100644 native/rust/cose_sign1_validation/tests/message_fact_producer_more.rs create mode 100644 native/rust/cose_sign1_validation/tests/real_v1_cose_files.rs create mode 100644 native/rust/cose_sign1_validation/tests/real_world_trust_plans.rs create mode 100644 native/rust/cose_sign1_validation/tests/v2_validator_parity.rs create mode 100644 native/rust/cose_sign1_validation/tests/validator_more_coverage.rs create mode 100644 native/rust/cose_sign1_validation/tests/validator_pipeline_tests.rs create mode 100644 native/rust/cose_sign1_validation/tests/validator_smoke.rs create mode 100644 native/rust/cose_sign1_validation_azure_key_vault/Cargo.toml create mode 100644 native/rust/cose_sign1_validation_azure_key_vault/README.md create mode 100644 native/rust/cose_sign1_validation_azure_key_vault/examples/akv_kid_allowed.rs create mode 100644 native/rust/cose_sign1_validation_azure_key_vault/src/facts.rs create mode 100644 native/rust/cose_sign1_validation_azure_key_vault/src/fluent_ext.rs create mode 100644 native/rust/cose_sign1_validation_azure_key_vault/src/lib.rs create mode 100644 native/rust/cose_sign1_validation_azure_key_vault/src/pack.rs create mode 100644 native/rust/cose_sign1_validation_azure_key_vault/tests/akv_kid.rs create mode 100644 native/rust/cose_sign1_validation_certificates/Cargo.toml create mode 100644 native/rust/cose_sign1_validation_certificates/README.md create mode 100644 native/rust/cose_sign1_validation_certificates/examples/x5chain_identity.rs create mode 100644 native/rust/cose_sign1_validation_certificates/src/facts.rs create mode 100644 native/rust/cose_sign1_validation_certificates/src/fluent_ext.rs create mode 100644 native/rust/cose_sign1_validation_certificates/src/lib.rs create mode 100644 native/rust/cose_sign1_validation_certificates/src/pack.rs create mode 100644 native/rust/cose_sign1_validation_certificates/src/signing_key_resolver.rs create mode 100644 native/rust/cose_sign1_validation_certificates/testdata/v1/1ts-statement.scitt create mode 100644 native/rust/cose_sign1_validation_certificates/testdata/v1/2ts-statement.scitt create mode 100644 native/rust/cose_sign1_validation_certificates/testdata/v1/UnitTestPayload.json create mode 100644 native/rust/cose_sign1_validation_certificates/testdata/v1/UnitTestSignatureWithCRL.cose create mode 100644 native/rust/cose_sign1_validation_certificates/tests/cert_fact_sets.rs create mode 100644 native/rust/cose_sign1_validation_certificates/tests/chain_trust_more_coverage.rs create mode 100644 native/rust/cose_sign1_validation_certificates/tests/counter_signature_x5chain.rs create mode 100644 native/rust/cose_sign1_validation_certificates/tests/generated_cert_extensions.rs create mode 100644 native/rust/cose_sign1_validation_certificates/tests/real_v1_cert_facts.rs create mode 100644 native/rust/cose_sign1_validation_certificates/tests/x5chain_identity.rs create mode 100644 native/rust/cose_sign1_validation_demo/Cargo.toml create mode 100644 native/rust/cose_sign1_validation_demo/src/main.rs create mode 100644 native/rust/cose_sign1_validation_transparent_mst/Cargo.toml create mode 100644 native/rust/cose_sign1_validation_transparent_mst/README.md create mode 100644 native/rust/cose_sign1_validation_transparent_mst/examples/debug_verify_scitt.rs create mode 100644 native/rust/cose_sign1_validation_transparent_mst/examples/mst_receipt_present.rs create mode 100644 native/rust/cose_sign1_validation_transparent_mst/src/facts.rs create mode 100644 native/rust/cose_sign1_validation_transparent_mst/src/fluent_ext.rs create mode 100644 native/rust/cose_sign1_validation_transparent_mst/src/lib.rs create mode 100644 native/rust/cose_sign1_validation_transparent_mst/src/pack.rs create mode 100644 native/rust/cose_sign1_validation_transparent_mst/src/receipt_verify.rs create mode 100644 native/rust/cose_sign1_validation_transparent_mst/testdata/esrp-cts-cp.confidential-ledger.azure.com.jwks.json create mode 100644 native/rust/cose_sign1_validation_transparent_mst/tests/mst_receipts.rs create mode 100644 native/rust/cose_sign1_validation_trust/Cargo.toml create mode 100644 native/rust/cose_sign1_validation_trust/README.md create mode 100644 native/rust/cose_sign1_validation_trust/examples/trust_plan_minimal.rs create mode 100644 native/rust/cose_sign1_validation_trust/src/audit.rs create mode 100644 native/rust/cose_sign1_validation_trust/src/cose_sign1.rs create mode 100644 native/rust/cose_sign1_validation_trust/src/decision.rs create mode 100644 native/rust/cose_sign1_validation_trust/src/error.rs create mode 100644 native/rust/cose_sign1_validation_trust/src/evaluation_options.rs create mode 100644 native/rust/cose_sign1_validation_trust/src/fact_properties.rs create mode 100644 native/rust/cose_sign1_validation_trust/src/facts.rs create mode 100644 native/rust/cose_sign1_validation_trust/src/field.rs create mode 100644 native/rust/cose_sign1_validation_trust/src/fluent.rs create mode 100644 native/rust/cose_sign1_validation_trust/src/ids.rs create mode 100644 native/rust/cose_sign1_validation_trust/src/lib.rs create mode 100644 native/rust/cose_sign1_validation_trust/src/plan.rs create mode 100644 native/rust/cose_sign1_validation_trust/src/policy.rs create mode 100644 native/rust/cose_sign1_validation_trust/src/rules.rs create mode 100644 native/rust/cose_sign1_validation_trust/src/subject.rs create mode 100644 native/rust/cose_sign1_validation_trust/tests/compiled_plan_semantics.rs create mode 100644 native/rust/cose_sign1_validation_trust/tests/cose_header_parsing_tests.rs create mode 100644 native/rust/cose_sign1_validation_trust/tests/declarative_predicates.rs create mode 100644 native/rust/cose_sign1_validation_trust/tests/ids_subject_decision_tests.rs create mode 100644 native/rust/cose_sign1_validation_trust/tests/rules_policy_audit_tests.rs create mode 100644 native/rust/docs/README.md create mode 100644 native/rust/docs/azure-key-vault-pack.md create mode 100644 native/rust/docs/certificate-pack.md create mode 100644 native/rust/docs/demo-exe.md create mode 100644 native/rust/docs/detached-payloads.md create mode 100644 native/rust/docs/extension-points.md create mode 100644 native/rust/docs/getting-started.md create mode 100644 native/rust/docs/transparent-mst-pack.md create mode 100644 native/rust/docs/troubleshooting.md create mode 100644 native/rust/docs/trust-model.md create mode 100644 native/rust/docs/trust-subjects.md create mode 100644 native/rust/docs/validator-architecture.md create mode 100644 native/rust/scripts/check_repo_rules.py diff --git a/.gitignore b/.gitignore index 74c870b1..92abf99d 100644 --- a/.gitignore +++ b/.gitignore @@ -366,3 +366,17 @@ FodyWeavers.xsd # Visual Studio live unit testing configuration files. *.lutconfig + +# --- Rust (Cargo) --- +# Cargo build artifacts (repo-wide; native/rust also has its own .gitignore) +**/target/ + +# Rustfmt / editor backups +**/*.rs.bk + +# LLVM/coverage/profiling artifacts (can be emitted outside target) +**/*.profraw +**/*.profdata +lcov.info +tarpaulin-report.html + diff --git a/native/rust/.gitignore b/native/rust/.gitignore new file mode 100644 index 00000000..aaf28d87 --- /dev/null +++ b/native/rust/.gitignore @@ -0,0 +1,14 @@ +# Rust build outputs +/target/ + +# Coverage outputs +/coverage/ + +# LLVM/coverage/profiling artifacts +*.profraw +*.profdata +lcov.info +tarpaulin-report.html + +# Editor +/.vscode/ diff --git a/native/rust/Cargo.lock b/native/rust/Cargo.lock new file mode 100644 index 00000000..30362c40 --- /dev/null +++ b/native/rust/Cargo.lock @@ -0,0 +1,1226 @@ +# 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 = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[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 = "cc" +version = "1.2.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cose_sign1_validation" +version = "0.1.0" +dependencies = [ + "anyhow", + "cose_sign1_validation_azure_key_vault", + "cose_sign1_validation_certificates", + "cose_sign1_validation_transparent_mst", + "cose_sign1_validation_trust", + "once_cell", + "regex", + "thiserror 2.0.17", + "tinycbor", + "x509-parser", +] + +[[package]] +name = "cose_sign1_validation_azure_key_vault" +version = "0.1.0" +dependencies = [ + "cose_sign1_validation", + "cose_sign1_validation_trust", + "once_cell", + "regex", + "tinycbor", + "url", +] + +[[package]] +name = "cose_sign1_validation_certificates" +version = "0.1.0" +dependencies = [ + "cose_sign1_validation", + "cose_sign1_validation_trust", + "hex", + "rcgen", + "ring", + "sha1", + "thiserror 2.0.17", + "tinycbor", + "x509-parser", +] + +[[package]] +name = "cose_sign1_validation_demo" +version = "0.1.0" +dependencies = [ + "anyhow", + "cose_sign1_validation", + "cose_sign1_validation_trust", +] + +[[package]] +name = "cose_sign1_validation_transparent_mst" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "cose_sign1_validation", + "cose_sign1_validation_trust", + "hex", + "once_cell", + "ring", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.17", + "tinycbor", + "ureq", + "url", +] + +[[package]] +name = "cose_sign1_validation_trust" +version = "0.1.0" +dependencies = [ + "anyhow", + "hex", + "once_cell", + "parking_lot", + "regex", + "sha2", + "thiserror 2.0.17", + "tinycbor", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +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 = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[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 = "embedded-io" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" + +[[package]] +name = "find-msvc-tools" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[package]] +name = "flate2" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[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 = "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", + "libc", + "wasi", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[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 = "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 = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[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 = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[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-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[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-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "log", + "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 = [ + "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 = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[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 = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[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.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[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 = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" + +[[package]] +name = "time-macros" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinycbor" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5194b017318f43f24dd6ee6dd777b1f7fc2540dc9ac0721f970eba2245df3f2" +dependencies = [ + "embedded-io", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "url", + "webpki-roots 0.26.11", +] + +[[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 = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.5", +] + +[[package]] +name = "webpki-roots" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +dependencies = [ + "rustls-pki-types", +] + +[[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", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[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 = "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" + +[[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 = "zmij" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f63c051f4fe3c1509da62131a678643c5b6fbdc9273b2b79d4378ebda003d2" diff --git a/native/rust/Cargo.toml b/native/rust/Cargo.toml new file mode 100644 index 00000000..f4517df7 --- /dev/null +++ b/native/rust/Cargo.toml @@ -0,0 +1,43 @@ +[workspace] +resolver = "2" +members = [ + "cose_sign1_validation", + "cose_sign1_validation_trust", + "cose_sign1_validation_certificates", + "cose_sign1_validation_transparent_mst", + "cose_sign1_validation_azure_key_vault", + "cose_sign1_validation_demo", +] + +[workspace.package] +edition = "2021" +license = "MIT" + +[workspace.dependencies] +anyhow = "1" +thiserror = "2" +sha2 = "0.10" +ring = "0.17" +hex = "0.4" +sha1 = "0.10" +time = { version = "0.3", features = ["macros"] } + +# JSON + base64url (for MST JWKS parsing) +serde = { version = "1", features = ["derive"] } +serde_json = "1" +base64 = "0.22" + +# X.509 parsing +x509-parser = "0.16" + +# Prefer tinycbor (per requirements) +tinycbor = { version = "0.10", features = ["alloc", "std"] } + +# Concurrency + plumbing +once_cell = "1" +parking_lot = "0.12" +regex = "1" +url = "2" + +# HTTP client (used for optional online JWKS retrieval) +ureq = { version = "2", features = ["tls"] } diff --git a/native/rust/README.md b/native/rust/README.md new file mode 100644 index 00000000..b2886cbf --- /dev/null +++ b/native/rust/README.md @@ -0,0 +1,14 @@ +# native/rust + +Rust port of the V2 trust/validation framework (mirrors `V2/CoseSign1.Validation`). + +Docs live in `native/rust/docs/`: +- `native/rust/docs/README.md` + +Workspace crates: +- `cose_sign1_validation_trust`: facts/rules/plan/audit/subject IDs. +- `cose_sign1_validation`: COSE_Sign1-centric facade (parsing + validation entrypoints). + +Try it: +- `cargo test --workspace` +- `cargo run -p cose_sign1_validation_demo -- --help` diff --git a/native/rust/collect-coverage.ps1 b/native/rust/collect-coverage.ps1 new file mode 100644 index 00000000..1225ad86 --- /dev/null +++ b/native/rust/collect-coverage.ps1 @@ -0,0 +1,124 @@ +param( + [int]$FailUnderLines = 95, + [string]$OutputDir = "coverage", + [switch]$NoHtml, + [switch]$NoClean +) + +$ErrorActionPreference = "Stop" + +$here = Split-Path -Parent $MyInvocation.MyCommand.Path + +function Assert-NoTestsInSrc { + param( + [Parameter(Mandatory = $true)][string]$Root + ) + + $patterns = @( + '#\[cfg\(test\)\]', + '#\[test\]', + '^\s*mod\s+tests\b' + ) + + $srcFiles = Get-ChildItem -Path $Root -Recurse -File -Filter '*.rs' | + Where-Object { + $_.FullName -match '(\\|/)src(\\|/)' -and + $_.FullName -notmatch '(\\|/)target(\\|/)' -and + $_.FullName -notmatch '(\\|/)tests(\\|/)' + } + + $violations = @() + foreach ($file in $srcFiles) { + foreach ($pattern in $patterns) { + $matches = Select-String -Path $file.FullName -Pattern $pattern -AllMatches -CaseSensitive:$false -ErrorAction SilentlyContinue + if ($matches) { + $violations += $matches + } + } + } + + if ($violations.Count -gt 0) { + Write-Host "ERROR: Test code detected under src/. Move tests to the crate's tests/ folder." -ForegroundColor Red + $violations | + Select-Object -First 50 | + ForEach-Object { Write-Host (" {0}:{1}: {2}" -f $_.Path, $_.LineNumber, $_.Line.Trim()) -ForegroundColor Red } + throw "No-tests-in-src gate failed. Found $($violations.Count) matches." + } +} + +function Invoke-Checked { + param( + [Parameter(Mandatory = $true)][string]$Command, + [Parameter(Mandatory = $true)][scriptblock]$Run + ) + + & $Run | Out-Host + if ($LASTEXITCODE -ne 0) { + throw "$Command failed with exit code $LASTEXITCODE" + } +} + +# Exclude non-production code from coverage accounting: +# - tests/ and examples/ directories +# - build artifacts +# - the demo executable crate (not production) +# Note: cargo-llvm-cov expects a Rust-style regex over file paths. Use `\\` to match a single +# Windows path separator in the regex, and keep the PowerShell string itself single-quoted. +$ignoreFilenameRegex = '(^|\\|/)(tests|examples)(\\|/)|(^|\\|/)target(\\|/)|(^|\\|/)cose_sign1_validation_demo(\\|/)' + +Push-Location $here +try { + Assert-NoTestsInSrc -Root $here + + if (-not $NoClean) { + if (Test-Path $OutputDir) { + Remove-Item -Recurse -Force $OutputDir + } + } + New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null + + Invoke-Checked -Command "rustup component add llvm-tools-preview" -Run { + rustup component add llvm-tools-preview + } + + $llvmCov = Get-Command cargo-llvm-cov -ErrorAction SilentlyContinue + if (-not $llvmCov) { + Write-Host "Installing cargo-llvm-cov..." -ForegroundColor Yellow + Invoke-Checked -Command "cargo install cargo-llvm-cov --locked" -Run { + cargo install cargo-llvm-cov --locked + } + } + + $baseArgs = @( + "--workspace", + "--fail-under-lines", "$FailUnderLines", + "--ignore-filename-regex", $ignoreFilenameRegex + ) + + $summaryArgs = @( + "--workspace", + "--ignore-filename-regex", $ignoreFilenameRegex, + "--summary-only" + ) + + try { + if (-not $NoHtml) { + Invoke-Checked -Command "cargo llvm-cov (html report)" -Run { + cargo llvm-cov @baseArgs --html --output-dir $OutputDir + } + } + + Invoke-Checked -Command "cargo llvm-cov (lcov report)" -Run { + cargo llvm-cov @baseArgs --lcov --output-path (Join-Path $OutputDir "lcov.info") + } + } catch { + Write-Host "Coverage gate failed; current production-code summary:" -ForegroundColor Yellow + & cargo llvm-cov @summaryArgs | Out-Host + throw + } + + Write-Host "OK: Rust production line coverage >= $FailUnderLines%" -ForegroundColor Green + Write-Host "Artifacts: $(Join-Path $here $OutputDir)" -ForegroundColor Green +} finally { + Pop-Location +} \ No newline at end of file diff --git a/native/rust/collect-native-coverage.ps1 b/native/rust/collect-native-coverage.ps1 new file mode 100644 index 00000000..88030af1 --- /dev/null +++ b/native/rust/collect-native-coverage.ps1 @@ -0,0 +1 @@ +param( [string]$BuildDir = "out-vs18", [string]$Configuration = "Release", [string]$VcpkgRoot = $env:VCPKG_ROOT, [string]$OpenCppCoveragePath = $env:OPENCPPCOVERAGE_PATH, [string]$Generator = "", [string]$Architecture = "x64", [switch]$AutoInstallTools)$ErrorActionPreference = "Stop"$here = Split-Path -Parent $MyInvocation.MyCommand.Path$buildPath = Join-Path $here $BuildDirif (-not $VcpkgRoot) { throw "VCPKG_ROOT is not set. Set it to your vcpkg installation root (e.g. C:\\vcpkg)."}function Resolve-ExePath { param( [Parameter(Mandatory = $true)][string]$Name, [string[]]$FallbackPaths ) $cmd = Get-Command $Name -ErrorAction SilentlyContinue if ($cmd -and $cmd.Source -and (Test-Path $cmd.Source)) { return $cmd.Source } foreach ($p in ($FallbackPaths | Where-Object { $_ })) { if (Test-Path $p) { return $p } } return $null}function Resolve-OpenCppCoverage { if ($OpenCppCoveragePath) { if (-not (Test-Path $OpenCppCoveragePath)) { throw "OpenCppCoveragePath was provided but does not exist: $OpenCppCoveragePath" } return $OpenCppCoveragePath } $occ = Resolve-ExePath -Name "OpenCppCoverage" -FallbackPaths @( "C:\\Program Files\\OpenCppCoverage\\OpenCppCoverage.exe", "C:\\Program Files (x86)\\OpenCppCoverage\\OpenCppCoverage.exe", (Join-Path $here "_tools\\OpenCppCoverage\\OpenCppCoverage.exe"), (Join-Path $here "..\\..\\..\\_tmp\\tools\\OpenCppCoverage\\OpenCppCoverage.exe") ) if ($occ) { return $occ } if (-not $AutoInstallTools) { return $null } Write-Host "OpenCppCoverage not found. Attempting to install..." -ForegroundColor Yellow $winget = Resolve-ExePath -Name "winget" -FallbackPaths @() if ($winget) { try { & $winget install -e --id OpenCppCoverage.OpenCppCoverage --accept-source-agreements --accept-package-agreements --silent | Out-Host } catch { Write-Warning "winget install failed: $($_.Exception.Message)" } } $occ = Resolve-ExePath -Name "OpenCppCoverage" -FallbackPaths @( "C:\\Program Files\\OpenCppCoverage\\OpenCppCoverage.exe", "C:\\Program Files (x86)\\OpenCppCoverage\\OpenCppCoverage.exe" ) if ($occ) { return $occ } $choco = Resolve-ExePath -Name "choco" -FallbackPaths @() if ($choco) { try { & $choco install opencppcoverage -y --no-progress | Out-Host } catch { Write-Warning "choco install failed: $($_.Exception.Message)" } } return (Resolve-ExePath -Name "OpenCppCoverage" -FallbackPaths @( "C:\\Program Files\\OpenCppCoverage\\OpenCppCoverage.exe", "C:\\Program Files (x86)\\OpenCppCoverage\\OpenCppCoverage.exe" ))}function Resolve-VsCMakeBinDir { $vswhere = Resolve-ExePath -Name "vswhere" -FallbackPaths @( "${env:ProgramFiles(x86)}\\Microsoft Visual Studio\\Installer\\vswhere.exe", "${env:ProgramFiles}\\Microsoft Visual Studio\\Installer\\vswhere.exe" ) if (-not $vswhere) { return $null } $installPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath if ($LASTEXITCODE -ne 0 -or -not $installPath) { return $null } $installPath = $installPath.Trim() if (-not $installPath) { return $null } $cmakeBin = Join-Path $installPath "Common7\\IDE\\CommonExtensions\\Microsoft\\CMake\\CMake\\bin" if (Test-Path $cmakeBin) { return $cmakeBin } return $null}function Resolve-VsGenerator { param( [string]$Explicit ) if ($Explicit) { return $Explicit } $vswhere = Resolve-ExePath -Name "vswhere" -FallbackPaths @( "${env:ProgramFiles(x86)}\\Microsoft Visual Studio\\Installer\\vswhere.exe", "${env:ProgramFiles}\\Microsoft Visual Studio\\Installer\\vswhere.exe" ) if (-not $vswhere) { # Default to the most common currently-supported VS generator. return "Visual Studio 17 2022" } $ver = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationVersion if ($LASTEXITCODE -ne 0 -or -not $ver) { return "Visual Studio 17 2022" } $major = ($ver.Trim() -split '\.')[0] switch ($major) { "17" { return "Visual Studio 17 2022" } "18" { return "Visual Studio 18 2026" } default { return "Visual Studio 17 2022" } }}$cmake = Resolve-ExePath -Name "cmake" -FallbackPaths @()$ctest = Resolve-ExePath -Name "ctest" -FallbackPaths @()if (-not $cmake -or -not $ctest) { $vsCmakeBin = Resolve-VsCMakeBinDir if ($vsCmakeBin) { if (-not $cmake) { $candidate = Join-Path $vsCmakeBin "cmake.exe" if (Test-Path $candidate) { $cmake = $candidate } } if (-not $ctest) { $candidate = Join-Path $vsCmakeBin "ctest.exe" if (Test-Path $candidate) { $ctest = $candidate } } }}if (-not (Test-Path $cmake)) { throw "cmake.exe not found. Install CMake or the Visual Studio CMake tools."}if (-not (Test-Path $ctest)) { throw "ctest.exe not found. Install CMake or the Visual Studio CMake tools."}$occExe = Resolve-OpenCppCoverageif (-not $occExe) { throw "OpenCppCoverage not found. Re-run with -AutoInstallTools, or install it manually (e.g. winget install OpenCppCoverage.OpenCppCoverage), or set OPENCPPCOVERAGE_PATH."}& $cmake -S $here -B $buildPath -G (Resolve-VsGenerator -Explicit $Generator) -A $Architecture -DCMAKE_TOOLCHAIN_FILE="$VcpkgRoot\\scripts\\buildsystems\\vcpkg.cmake"& $cmake --build $buildPath --config $Configuration -j& $ctest --test-dir $buildPath -C $Configuration --output-on-failure$exe = Join-Path $buildPath "$Configuration\\cosesign1_native_tests.exe"if (-not (Test-Path $exe)) { throw "Test executable not found: $exe"}$headerSources = Join-Path $buildPath "vcpkg_installed\\x64-windows\\include\\cosesign1"if (-not (Test-Path $headerSources)) { Write-Warning "Header include dir not found at expected location: $headerSources"}$coverageOut = Join-Path $buildPath "coverage"New-Item -ItemType Directory -Force -Path $coverageOut | Out-Null$occArgs = @( "--quiet", "--sources=$headerSources", "--export_type=html:$coverageOut", "--", $exe)& $occExe @occArgsWrite-Output "Coverage HTML written to: $coverageOut" \ No newline at end of file diff --git a/native/rust/cose_sign1_validation/Cargo.toml b/native/rust/cose_sign1_validation/Cargo.toml new file mode 100644 index 00000000..56d7c0e7 --- /dev/null +++ b/native/rust/cose_sign1_validation/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "cose_sign1_validation" +version = "0.1.0" +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +thiserror.workspace = true + +once_cell.workspace = true +regex.workspace = true + +tinycbor.workspace = true + +cose_sign1_validation_trust = { path = "../cose_sign1_validation_trust" } + +[dev-dependencies] +anyhow.workspace = true + +x509-parser.workspace = true + +cose_sign1_validation_transparent_mst = { path = "../cose_sign1_validation_transparent_mst" } +cose_sign1_validation_certificates = { path = "../cose_sign1_validation_certificates" } +cose_sign1_validation_azure_key_vault = { path = "../cose_sign1_validation_azure_key_vault" } diff --git a/native/rust/cose_sign1_validation/README.md b/native/rust/cose_sign1_validation/README.md new file mode 100644 index 00000000..4dcd69c1 --- /dev/null +++ b/native/rust/cose_sign1_validation/README.md @@ -0,0 +1,25 @@ +# cose_sign1_validation + +COSE_Sign1-focused staged validator. + +## What it does + +- Parses COSE_Sign1 CBOR and orchestrates validation stages: + - key material resolution + - trust evaluation + - signature verification + - post-signature policy +- Supports detached payload verification (bytes or provider) +- Provides extension traits for: + - signing key resolution (`SigningKeyResolver` / `SigningKey`) + - counter-signature discovery (`CounterSignatureResolver` / `CounterSignature`) + - post-signature validation (`PostSignatureValidator`) + +## Examples + +Run: + +- `cargo run -p cose_sign1_validation --example validate_smoke` +- `cargo run -p cose_sign1_validation --example detached_payload_provider` + +For the bigger picture docs, see `native/rust/docs/README.md`. diff --git a/native/rust/cose_sign1_validation/examples/detached_payload_provider.rs b/native/rust/cose_sign1_validation/examples/detached_payload_provider.rs new file mode 100644 index 00000000..e95fcb31 --- /dev/null +++ b/native/rust/cose_sign1_validation/examples/detached_payload_provider.rs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use cose_sign1_validation::{ + CoseSign1TrustPack, CoseSign1ValidationOptions, CoseSign1Validator, DetachedPayload, + DetachedPayloadFnProvider, SigningKey, SigningKeyResolutionResult, SigningKeyResolver, + SimpleTrustPack, TrustPlanBuilder, +}; +use std::io::Cursor; +use std::sync::Arc; +use tinycbor::{Encode, Encoder}; + +struct AcceptAllSigningKey; + +impl SigningKey for AcceptAllSigningKey { + fn key_type(&self) -> &'static str { + "AcceptAllSigningKey" + } + + fn verify(&self, _alg: i64, _sig_structure: &[u8], _signature: &[u8]) -> Result { + Ok(true) + } +} + +struct ExampleSigningKeyResolver; + +impl SigningKeyResolver for ExampleSigningKeyResolver { + fn resolve( + &self, + _message: &cose_sign1_validation::CoseSign1<'_>, + _options: &CoseSign1ValidationOptions, + ) -> SigningKeyResolutionResult { + SigningKeyResolutionResult::success(Arc::new(AcceptAllSigningKey)) + } +} + +fn build_minimal_cose_sign1_with_detached_payload() -> Vec { + let mut buf = vec![0u8; 1024]; + let buf_len = buf.len(); + let mut enc = Encoder(buf.as_mut_slice()); + + enc.array(4).unwrap(); + + // protected header: bstr(CBOR map {1: -7}) (alg = ES256) + let mut hdr_buf = vec![0u8; 64]; + let hdr_len = hdr_buf.len(); + let mut hdr_enc = Encoder(hdr_buf.as_mut_slice()); + hdr_enc.map(1).unwrap(); + (1i64).encode(&mut hdr_enc).unwrap(); + (-7i64).encode(&mut hdr_enc).unwrap(); + let used_hdr = hdr_len - hdr_enc.0.len(); + let protected_bytes = &hdr_buf[..used_hdr]; + protected_bytes.encode(&mut enc).unwrap(); + + // unprotected header: empty map + enc.map(0).unwrap(); + + // payload: nil (detached) + Option::<&[u8]>::None.encode(&mut enc).unwrap(); + + // signature: arbitrary bstr + b"sig".as_slice().encode(&mut enc).unwrap(); + + let used = buf_len - enc.0.len(); + buf.truncate(used); + buf +} + +fn main() { + let payload: Arc<[u8]> = Arc::from(b"this is the detached payload".as_slice()); + + // Provider opens a fresh reader each time. + let provider = DetachedPayloadFnProvider::new({ + let payload = payload.clone(); + move || Ok(Box::new(Cursor::new(payload.to_vec())) as Box) + }) + .with_len_hint(payload.len() as u64); + + let trust_packs: Vec> = vec![Arc::new( + SimpleTrustPack::no_facts("example_signing_key") + .with_signing_key_resolver(Arc::new(ExampleSigningKeyResolver)), + )]; + + let bundled = TrustPlanBuilder::new(trust_packs) + .for_message(|m| m.allow_all()) + .compile() + .unwrap(); + + let validator = CoseSign1Validator::new(bundled).with_options(|o| { + o.detached_payload = Some(DetachedPayload::Provider(Arc::new(provider))); + }); + + let cose = build_minimal_cose_sign1_with_detached_payload(); + let result = validator + .validate_bytes(Arc::from(cose.into_boxed_slice())) + .expect("validation failed"); + + assert!(result.overall.is_valid()); + println!("OK: detached payload verified (example signing key accepted signature)"); +} diff --git a/native/rust/cose_sign1_validation/examples/validate_smoke.rs b/native/rust/cose_sign1_validation/examples/validate_smoke.rs new file mode 100644 index 00000000..1aa1624f --- /dev/null +++ b/native/rust/cose_sign1_validation/examples/validate_smoke.rs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use cose_sign1_validation::{ + CoseSign1TrustPack, CoseSign1ValidationOptions, CoseSign1Validator, SigningKey, + SigningKeyResolutionResult, SigningKeyResolver, SimpleTrustPack, TrustPlanBuilder, +}; +use std::sync::Arc; +use tinycbor::{Encode, Encoder}; + +struct AcceptAllSigningKey; + +impl SigningKey for AcceptAllSigningKey { + fn key_type(&self) -> &'static str { + "AcceptAllSigningKey" + } + + fn verify(&self, _alg: i64, _sig_structure: &[u8], _signature: &[u8]) -> Result { + Ok(true) + } +} + +struct ExampleSigningKeyResolver; + +impl SigningKeyResolver for ExampleSigningKeyResolver { + fn resolve( + &self, + _message: &cose_sign1_validation::CoseSign1<'_>, + _options: &CoseSign1ValidationOptions, + ) -> SigningKeyResolutionResult { + SigningKeyResolutionResult::success(Arc::new(AcceptAllSigningKey)) + } +} + +fn build_minimal_cose_sign1_with_embedded_payload(payload: &[u8]) -> Vec { + let mut buf = vec![0u8; 1024]; + let buf_len = buf.len(); + let mut enc = Encoder(buf.as_mut_slice()); + + enc.array(4).unwrap(); + + // protected header: bstr(CBOR map {1: -7}) (alg = ES256) + let mut hdr_buf = vec![0u8; 64]; + let hdr_len = hdr_buf.len(); + let mut hdr_enc = Encoder(hdr_buf.as_mut_slice()); + hdr_enc.map(1).unwrap(); + (1i64).encode(&mut hdr_enc).unwrap(); + (-7i64).encode(&mut hdr_enc).unwrap(); + let used_hdr = hdr_len - hdr_enc.0.len(); + let protected_bytes = &hdr_buf[..used_hdr]; + protected_bytes.encode(&mut enc).unwrap(); + + // unprotected header: empty map + enc.map(0).unwrap(); + + // payload: embedded bstr + payload.encode(&mut enc).unwrap(); + + // signature: arbitrary bstr (the example signing key accepts all) + b"sig".as_slice().encode(&mut enc).unwrap(); + + let used = buf_len - enc.0.len(); + buf.truncate(used); + buf +} + +fn main() { + let cose = build_minimal_cose_sign1_with_embedded_payload(b"hello"); + + let trust_packs: Vec> = vec![Arc::new( + SimpleTrustPack::no_facts("example_signing_key") + .with_signing_key_resolver(Arc::new(ExampleSigningKeyResolver)), + )]; + + // For a smoke example, trust everything. + let bundled = TrustPlanBuilder::new(trust_packs) + .for_message(|m| m.allow_all()) + .compile() + .unwrap(); + + let validator = CoseSign1Validator::new(bundled); + + let result = validator + .validate_bytes(Arc::from(cose.into_boxed_slice())) + .expect("validation failed"); + + println!("resolution: {:?}", result.resolution.kind); + println!("trust: {:?}", result.trust.kind); + println!("signature: {:?}", result.signature.kind); + println!("overall: {:?}", result.overall.kind); +} diff --git a/native/rust/cose_sign1_validation/src/cose.rs b/native/rust/cose_sign1_validation/src/cose.rs new file mode 100644 index 00000000..23d47a10 --- /dev/null +++ b/native/rust/cose_sign1_validation/src/cose.rs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use tinycbor::Decoder; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CoseSign1<'a> { + pub protected_header: &'a [u8], + pub unprotected_header: tinycbor::Any<'a>, + pub payload: Option<&'a [u8]>, + pub signature: &'a [u8], +} + +#[derive(Debug, thiserror::Error)] +pub enum CoseDecodeError { + #[error("CBOR decode failed: {0}")] + Cbor(String), + + #[error("COSE_Sign1 must be an array(4)")] + NotSign1, +} + +impl CoseDecodeError { + fn cbor(e: E) -> Self { + Self::Cbor(e.to_string()) + } +} + +impl<'a> CoseSign1<'a> { + pub fn from_cbor(cbor: &'a [u8]) -> Result { + // Some encoders wrap COSE_Sign1 in the standard CBOR tag 18. + // Accept (and strip) an initial tag(18) if present. + let cbor = match decode_cose_sign1_tag_prefix(cbor) { + Ok(Some(rest)) => rest, + Ok(None) => cbor, + Err(e) => return Err(CoseDecodeError::Cbor(e)), + }; + + let mut d = Decoder(cbor); + // COSE_Sign1 = [ protected : bstr, unprotected : map, payload : bstr / nil, signature : bstr ] + // We accept both definite-length bstr and store the raw bytes slice as returned by tinycbor. + // For unprotected header we keep original encoding using Any. + let mut array = d.array_visitor().map_err(CoseDecodeError::cbor)?; + + let protected_header = array + .visit::<&[u8]>() + .ok_or(CoseDecodeError::NotSign1)? + .map_err(CoseDecodeError::cbor)?; + + let unprotected_header = array + .visit::>() + .ok_or(CoseDecodeError::NotSign1)? + .map_err(CoseDecodeError::cbor)?; + + let payload = array + .visit::>() + .ok_or(CoseDecodeError::NotSign1)? + .map_err(CoseDecodeError::cbor)?; + + let signature = array + .visit::<&[u8]>() + .ok_or(CoseDecodeError::NotSign1)? + .map_err(CoseDecodeError::cbor)?; + + // Ensure there are no extra array items. + if array.visit::>().is_some() { + return Err(CoseDecodeError::NotSign1); + } + + Ok(Self { + protected_header, + unprotected_header, + payload, + signature, + }) + } +} + +fn decode_cose_sign1_tag_prefix(input: &[u8]) -> Result, String> { + let first = match input.first() { + Some(b) => *b, + None => return Ok(None), + }; + + let major = first >> 5; + let ai = first & 0x1f; + if major != 6 { + return Ok(None); + } + + let (tag, used) = decode_cbor_uint_value(ai, &input[1..]) + .ok_or_else(|| "invalid CBOR tag encoding".to_string())?; + let consumed = 1 + used; + if tag != 18 { + return Err(format!( + "unexpected CBOR tag {tag} (expected 18 for COSE_Sign1)" + )); + } + + Ok(input.get(consumed..)) +} + +fn decode_cbor_uint_value(ai: u8, rest: &[u8]) -> Option<(u64, usize)> { + match ai { + 0..=23 => Some((ai as u64, 0)), + 24 => Some((u64::from(*rest.first()?), 1)), + 25 => { + let b = rest.get(0..2)?; + Some((u16::from_be_bytes([b[0], b[1]]) as u64, 2)) + } + 26 => { + let b = rest.get(0..4)?; + Some((u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as u64, 4)) + } + 27 => { + let b = rest.get(0..8)?; + Some(( + u64::from_be_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]), + 8, + )) + } + _ => None, + } +} diff --git a/native/rust/cose_sign1_validation/src/lib.rs b/native/rust/cose_sign1_validation/src/lib.rs new file mode 100644 index 00000000..30db110f --- /dev/null +++ b/native/rust/cose_sign1_validation/src/lib.rs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +pub mod cose; + +pub mod message_fact_producer; +pub mod message_facts; +pub mod trust_plan_builder; +pub mod trust_packs; +pub mod validator; + +pub use cose::CoseSign1; +pub use message_fact_producer::CoseSign1MessageFactProducer; +pub use message_facts::{ + CborValueReader, ContentTypeFact, CoseSign1MessageBytesFact, CoseSign1MessagePartsFact, + CounterSignatureEnvelopeIntegrityFact, CounterSignatureSigningKeySubjectFact, + CounterSignatureSubjectFact, CwtClaimScalar, CwtClaimsFact, CwtClaimsPresentFact, + DetachedPayloadPresentFact, PrimarySigningKeySubjectFact, UnknownCounterSignatureBytesFact, +}; +pub use trust_plan_builder::{ + CoseSign1CompiledTrustPlan, OnEmptyBehavior, TrustPlanBuilder, TrustPlanCompileError, +}; +pub use trust_packs::CoseSign1TrustPack; +pub use trust_packs::{NoopTrustFactProducer, SimpleTrustPack}; +pub use validator::{ + CoseSign1MessageValidator, CoseSign1ValidationError, CoseSign1ValidationOptions, + CoseSign1ValidationResult, CoseSign1Validator, CoseSign1ValidatorInit, CounterSignature, + CounterSignatureResolutionResult, CounterSignatureResolver, DetachedPayload, + DetachedPayloadFnProvider, DetachedPayloadProvider, PostSignatureValidationContext, + PostSignatureValidator, SigningKey, SigningKeyResolutionResult, SigningKeyResolver, + ValidationFailure, ValidationResult, ValidationResultKind, +}; diff --git a/native/rust/cose_sign1_validation/src/message_fact_producer.rs b/native/rust/cose_sign1_validation/src/message_fact_producer.rs new file mode 100644 index 00000000..33ec46b9 --- /dev/null +++ b/native/rust/cose_sign1_validation/src/message_fact_producer.rs @@ -0,0 +1,523 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::cose::CoseSign1; +use crate::message_facts::{ + ContentTypeFact, CoseSign1MessageBytesFact, CoseSign1MessagePartsFact, CwtClaimScalar, + CwtClaimsFact, CwtClaimsPresentFact, CounterSignatureSigningKeySubjectFact, + CounterSignatureSubjectFact, DetachedPayloadPresentFact, PrimarySigningKeySubjectFact, + UnknownCounterSignatureBytesFact, +}; +use crate::validator::CounterSignatureResolver; +use cose_sign1_validation_trust::error::TrustError; +use cose_sign1_validation_trust::facts::{FactKey, TrustFactContext, TrustFactProducer}; +use cose_sign1_validation_trust::ids::sha256_of_bytes; +use cose_sign1_validation_trust::subject::TrustSubject; +use once_cell::sync::Lazy; +use regex::Regex; +use std::collections::BTreeMap; +use std::collections::HashSet; +use std::sync::Arc; + +/// Produces basic "message facts" from the COSE_Sign1 bytes in the engine context. +/// +/// This mirrors the V2 pattern where fact producers can access the message, but keeps +/// everything as owned bytes so facts are cacheable without lifetimes. +#[derive(Default, Clone)] +pub struct CoseSign1MessageFactProducer { + counter_signature_resolvers: Vec>, +} + +impl CoseSign1MessageFactProducer { + pub fn new() -> Self { + Self::default() + } + + pub fn with_counter_signature_resolvers( + mut self, + resolvers: Vec>, + ) -> Self { + self.counter_signature_resolvers = resolvers; + self + } +} + +impl TrustFactProducer for CoseSign1MessageFactProducer { + fn name(&self) -> &'static str { + "cose_sign1_validation::CoseSign1MessageFactProducer" + } + + fn produce(&self, ctx: &mut TrustFactContext<'_>) -> Result<(), TrustError> { + // V2 parity: core message facts only apply to the Message subject. + if ctx.subject().kind != "Message" { + for k in self.provides() { + ctx.mark_produced(*k); + } + return Ok(()); + } + + let bytes = match ctx.cose_sign1_bytes() { + Some(b) => b, + None => { + ctx.mark_missing::("MissingMessage"); + ctx.mark_missing::("MissingMessage"); + ctx.mark_missing::("MissingMessage"); + ctx.mark_missing::("MissingMessage"); + ctx.mark_missing::("MissingMessage"); + ctx.mark_missing::("MissingMessage"); + ctx.mark_missing::("MissingMessage"); + ctx.mark_missing::("MissingMessage"); + ctx.mark_missing::("MissingMessage"); + ctx.mark_missing::("MissingMessage"); + + for k in self.provides() { + ctx.mark_produced(*k); + } + return Ok(()); + } + }; + + // Always produce bytes fact. + ctx.observe(CoseSign1MessageBytesFact { + bytes: Arc::from(bytes), + })?; + + // Produce parts/content-type/detached/countersignature facts. + // Prefer the already-parsed message from the engine context. + if let Some(pm) = ctx.cose_sign1_message() { + ctx.observe(CoseSign1MessagePartsFact { + protected_header: Arc::new(pm.protected_header_bytes.as_ref().to_vec()), + unprotected_header: Arc::new(pm.unprotected_header_bytes.as_ref().to_vec()), + payload: pm.payload.as_ref().map(|p| Arc::new(p.as_ref().to_vec())), + signature: Arc::new(pm.signature.as_ref().to_vec()), + })?; + + ctx.observe(DetachedPayloadPresentFact { + present: pm.payload.is_none(), + })?; + + if let Some(ct) = resolve_content_type_from_parsed(pm) { + ctx.observe(ContentTypeFact { content_type: ct })?; + } + + produce_cwt_claims_facts(ctx, pm)?; + + // V2 parity: provide a derived subject for the primary signing key. + ctx.observe(PrimarySigningKeySubjectFact { + subject: TrustSubject::primary_signing_key(ctx.subject()), + })?; + + // V2 parity: counter-signatures are resolver-driven. + self.produce_counter_signature_facts(ctx, pm)?; + } else { + let msg = match CoseSign1::from_cbor(bytes) { + Ok(m) => m, + Err(e) => { + ctx.mark_error::(format!("cose_decode_failed: {e}")); + for k in self.provides() { + ctx.mark_produced(*k); + } + return Ok(()); + } + }; + + ctx.observe(CoseSign1MessagePartsFact { + protected_header: Arc::new(msg.protected_header.to_vec()), + unprotected_header: Arc::new(msg.unprotected_header.as_ref().to_vec()), + payload: msg.payload.map(|p| Arc::new(p.to_vec())), + signature: Arc::new(msg.signature.to_vec()), + })?; + + ctx.observe(DetachedPayloadPresentFact { + present: msg.payload.is_none(), + })?; + + if let Ok(pm) = cose_sign1_validation_trust::CoseSign1ParsedMessage::from_parts( + msg.protected_header, + msg.unprotected_header.as_ref(), + msg.payload, + msg.signature, + ) { + if let Some(ct) = resolve_content_type_from_parsed(&pm) { + ctx.observe(ContentTypeFact { content_type: ct })?; + } + + produce_cwt_claims_facts(ctx, &pm)?; + + // V2 parity: provide a derived subject for the primary signing key. + ctx.observe(PrimarySigningKeySubjectFact { + subject: TrustSubject::primary_signing_key(ctx.subject()), + })?; + + // V2 parity: counter-signatures are resolver-driven. + self.produce_counter_signature_facts(ctx, &pm)?; + } + } + + for k in self.provides() { + ctx.mark_produced(*k); + } + Ok(()) + } + + fn provides(&self) -> &'static [FactKey] { + static PROVIDED: Lazy<[FactKey; 10]> = Lazy::new(|| { + [ + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + FactKey::of::(), + ] + }); + &*PROVIDED + } +} + +fn produce_cwt_claims_facts( + ctx: &TrustFactContext<'_>, + pm: &cose_sign1_validation_trust::CoseSign1ParsedMessage, +) -> Result<(), TrustError> { + // COSE header parameter label 15 = CWT Claims. + const CWT_CLAIMS: i64 = 15; + + let raw = pm + .protected_header + .get(CWT_CLAIMS) + .or_else(|| pm.unprotected_header.get(CWT_CLAIMS)) + .cloned(); + + let Some(raw) = raw else { + ctx.observe(CwtClaimsPresentFact { present: false })?; + return Ok(()); + }; + + ctx.observe(CwtClaimsPresentFact { present: true })?; + + // We expect a CBOR map. The header map parser stores non-scalar values as `Other`. + let value_bytes: Arc<[u8]> = match raw { + cose_sign1_validation_trust::CoseHeaderValue::Other(b) => b, + // Unexpected shape: treat as present but unparseable. + _ => { + ctx.mark_error::("CwtClaimsValueNotMap".to_string()); + return Ok(()); + } + }; + + let mut d = tinycbor::Decoder(value_bytes.as_ref()); + let mut map = d + .map_visitor() + .map_err(|e| TrustError::FactProduction(format!("cwt_claims_map_decode_failed: {e}")))?; + + let mut scalar_claims: BTreeMap = BTreeMap::new(); + let mut raw_claims: BTreeMap> = BTreeMap::new(); + let mut raw_claims_text: BTreeMap> = BTreeMap::new(); + + // Standard CWT claim labels (RFC 8392): + // 1=iss, 2=sub, 3=aud, 4=exp, 5=nbf, 6=iat, 7=cti + let mut iss: Option = None; + let mut sub: Option = None; + let mut aud: Option = None; + let mut exp: Option = None; + let mut nbf: Option = None; + let mut iat: Option = None; + + while let Some(entry) = map.visit::, tinycbor::Any<'_>>() { + let (key_any, value_any) = entry + .map_err(|e| TrustError::FactProduction(format!("cwt_claim_entry_decode_failed: {e}")))?; + + let key_bytes = key_any.as_ref(); + let value_bytes = value_any.as_ref(); + + // CWT standard claim keys are typically integers (RFC 8392), but some profiles may + // emit text keys. Handle both. + let key_i64 = decode_cbor_i64_one(key_bytes); + let key_text = decode_cbor_text_one(key_bytes); + + // Try scalar value types. + let value_str = + ::decode(&mut tinycbor::Decoder(value_bytes)).ok(); + let value_i64 = decode_cbor_i64_one(value_bytes); + let value_bool = match value_bytes { + [0xF4] => Some(false), + [0xF5] => Some(true), + _ => None, + }; + + // Preserve raw bytes for both numeric and text keys. + if let Some(k) = key_i64 { + raw_claims.insert(k, Arc::from(value_bytes.to_vec().into_boxed_slice())); + + // Store numeric-keyed scalar claims. + if let Some(s) = &value_str { + scalar_claims.insert(k, CwtClaimScalar::Str(s.clone())); + } else if let Some(n) = value_i64 { + scalar_claims.insert(k, CwtClaimScalar::I64(n)); + } else if let Some(b) = value_bool { + scalar_claims.insert(k, CwtClaimScalar::Bool(b)); + } + + match (k, &value_str, value_i64) { + (1, Some(s), _) => iss = Some(s.clone()), + (2, Some(s), _) => sub = Some(s.clone()), + (3, Some(s), _) => aud = Some(s.clone()), + (4, _, Some(n)) => exp = Some(n), + (5, _, Some(n)) => nbf = Some(n), + (6, _, Some(n)) => iat = Some(n), + _ => {} + } + + continue; + } + + // Store a few well-known text-keyed claims as first-class fields. + if let Some(k) = key_text.as_deref() { + raw_claims_text.insert(k.to_string(), Arc::from(value_bytes.to_vec().into_boxed_slice())); + + match (k, &value_str, value_i64) { + ("iss", Some(s), _) => iss = Some(s.clone()), + ("sub", Some(s), _) => sub = Some(s.clone()), + ("aud", Some(s), _) => aud = Some(s.clone()), + ("exp", _, Some(n)) => exp = Some(n), + ("nbf", _, Some(n)) => nbf = Some(n), + ("iat", _, Some(n)) => iat = Some(n), + _ => {} + } + } + } + + ctx.observe(CwtClaimsFact { + scalar_claims, + raw_claims, + raw_claims_text, + iss, + sub, + aud, + exp, + nbf, + iat, + })?; + + Ok(()) +} + +fn decode_cbor_text_one(bytes: &[u8]) -> Option { + let mut d = tinycbor::Decoder(bytes); + let Ok(s) = ::decode(&mut d) else { + return None; + }; + // Ensure full consumption (avoid partial decodes on trailing bytes). + if d.0.is_empty() { + Some(s) + } else { + None + } +} + +fn decode_cbor_i64_one(bytes: &[u8]) -> Option { + let (n, used) = decode_cbor_i64(bytes)?; + if used == bytes.len() { + Some(n) + } else { + None + } +} + +fn decode_cbor_i64(bytes: &[u8]) -> Option<(i64, usize)> { + let first = *bytes.first()?; + let major = first >> 5; + let ai = first & 0x1f; + + let (unsigned, used) = decode_cbor_uint_value(ai, &bytes[1..])?; + + match major { + 0 => i64::try_from(unsigned).ok().map(|v| (v, 1 + used)), + 1 => { + // Negative integer is encoded as -1 - n. + let n = i64::try_from(unsigned).ok()?; + Some((-1 - n, 1 + used)) + } + _ => None, + } +} + +fn decode_cbor_uint_value(ai: u8, rest: &[u8]) -> Option<(u64, usize)> { + match ai { + 0..=23 => Some((ai as u64, 0)), + 24 => Some((u64::from(*rest.first()?), 1)), + 25 => { + let b = rest.get(0..2)?; + Some((u16::from_be_bytes([b[0], b[1]]) as u64, 2)) + } + 26 => { + let b = rest.get(0..4)?; + Some((u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as u64, 4)) + } + 27 => { + let b = rest.get(0..8)?; + Some(( + u64::from_be_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]), + 8, + )) + } + _ => None, + } +} + +impl CoseSign1MessageFactProducer { + fn produce_counter_signature_facts( + &self, + ctx: &TrustFactContext<'_>, + pm: &cose_sign1_validation_trust::CoseSign1ParsedMessage, + ) -> Result<(), TrustError> { + if self.counter_signature_resolvers.is_empty() { + // No resolver-driven discovery configured. + // Treat this as Available(empty) rather than Missing so that other producers + // may contribute counter-signature subjects. + return Ok(()); + } + + let mut subjects = Vec::new(); + let mut signing_key_subjects = Vec::new(); + let mut unknowns = Vec::new(); + let mut seen_ids: HashSet = HashSet::new(); + let mut any_success = false; + let mut failure_reasons: Vec = Vec::new(); + + for resolver in &self.counter_signature_resolvers { + let result = resolver.resolve(pm); + + if !result.is_success { + let mut reason = format!("ProducerFailed:{}", resolver.name()); + if let Some(msg) = result.error_message { + if !msg.trim().is_empty() { + reason = format!("{reason}:{msg}"); + } + } + failure_reasons.push(reason); + continue; + } + + any_success = true; + + for cs in result.counter_signatures { + let raw = cs.raw_counter_signature_bytes(); + let is_protected_header = cs.is_protected_header(); + + let subject = TrustSubject::counter_signature(ctx.subject(), raw.as_ref()); + let signing_key_subject = TrustSubject::counter_signature_signing_key(&subject); + signing_key_subjects.push(CounterSignatureSigningKeySubjectFact { + subject: signing_key_subject, + is_protected_header, + }); + + subjects.push(CounterSignatureSubjectFact { + subject, + is_protected_header, + }); + + let counter_signature_id = sha256_of_bytes(raw.as_ref()); + if seen_ids.insert(counter_signature_id) { + unknowns.push(UnknownCounterSignatureBytesFact { + counter_signature_id, + raw_counter_signature_bytes: raw, + }); + } + } + } + + for f in subjects { + ctx.observe(f)?; + } + for f in signing_key_subjects { + ctx.observe(f)?; + } + for f in unknowns { + ctx.observe(f)?; + } + + if !any_success && !failure_reasons.is_empty() { + // If we had resolvers but none succeeded, surface a Missing reason like V2. + ctx.mark_missing::(failure_reasons.join(" | ")); + ctx.mark_missing::(failure_reasons.join(" | ")); + ctx.mark_missing::(failure_reasons.join(" | ")); + } + + Ok(()) + } +} + +fn resolve_content_type_from_parsed( + pm: &cose_sign1_validation_trust::CoseSign1ParsedMessage, +) -> Option { + // Mirrors V2 CoseSign1MessageExtensions.TryGetContentType. + // Header labels: + // - 3 = content-type + // - 258 = CoseHashEnvelope payload hash alg (signature format marker) + // - 259 = CoseHashEnvelope preimage content type + const CONTENT_TYPE: i64 = 3; + const PAYLOAD_HASH_ALG: i64 = 258; + const PREIMAGE_CONTENT_TYPE: i64 = 259; + + let has_envelope_marker = pm.protected_header.get(PAYLOAD_HASH_ALG).is_some(); + + let raw_ct = get_text_or_utf8_bytes(&pm.protected_header, CONTENT_TYPE) + .or_else(|| get_text_or_utf8_bytes(&pm.unprotected_header, CONTENT_TYPE)); + + if has_envelope_marker { + if let Some(ct) = get_text_or_utf8_bytes(&pm.protected_header, PREIMAGE_CONTENT_TYPE) + .or_else(|| get_text_or_utf8_bytes(&pm.unprotected_header, PREIMAGE_CONTENT_TYPE)) + { + return Some(ct); + } + + if let Some(i) = pm + .protected_header + .get_i64(PREIMAGE_CONTENT_TYPE) + .or_else(|| pm.unprotected_header.get_i64(PREIMAGE_CONTENT_TYPE)) + { + return Some(format!("coap/{i}")); + } + + return None; + } + + let ct = raw_ct?; + + static COSE_HASH_V: Lazy = Lazy::new(|| Regex::new("(?i)\\+cose-hash-v").unwrap()); + static HASH_LEGACY: Lazy = Lazy::new(|| Regex::new("(?i)\\+hash-([\\w_]+)").unwrap()); + + if COSE_HASH_V.is_match(&ct) { + let stripped = COSE_HASH_V.replace_all(&ct, ""); + let stripped = stripped.trim(); + return (!stripped.is_empty()).then(|| stripped.to_string()); + } + + if HASH_LEGACY.is_match(&ct) { + let stripped = HASH_LEGACY.replace_all(&ct, ""); + let stripped = stripped.trim(); + return (!stripped.is_empty()).then(|| stripped.to_string()); + } + + Some(ct) +} + +fn get_text_or_utf8_bytes( + map: &cose_sign1_validation_trust::CoseHeaderMap, + label: i64, +) -> Option { + if let Some(s) = map.get_text(label) { + if !s.trim().is_empty() { + return Some(s.to_string()); + } + } + + let b = map.get(label).and_then(|v| v.as_bytes())?; + let s = std::str::from_utf8(b).ok()?; + (!s.trim().is_empty()).then(|| s.to_string()) +} diff --git a/native/rust/cose_sign1_validation/src/message_facts.rs b/native/rust/cose_sign1_validation/src/message_facts.rs new file mode 100644 index 00000000..d76a834d --- /dev/null +++ b/native/rust/cose_sign1_validation/src/message_facts.rs @@ -0,0 +1,463 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::sync::Arc; +use cose_sign1_validation_trust::fact_properties::{FactProperties, FactValue}; +use std::borrow::Cow; +use std::collections::BTreeMap; + +/// An opaque, borrow-based reader over a CBOR-encoded value. +/// +/// This is intended for custom policy predicates that need to inspect a claim value +/// without the library interpreting its schema. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CborValueReader<'a> { + bytes: &'a [u8], +} + +impl<'a> CborValueReader<'a> { + pub fn new(bytes: &'a [u8]) -> Self { + Self { bytes } + } + + pub fn bytes(&self) -> &'a [u8] { + self.bytes + } + + /// Best-effort decode helper for callers who want a typed view. + /// + /// Note: this does not enforce full consumption of the input. + pub fn decode>(&self) -> Option { + let mut d = tinycbor::Decoder(self.bytes); + T::decode(&mut d).ok() + } +} + +/// Parsed, owned view of a COSE_Sign1 message. +/// +/// This is intentionally "boring" and ownership-heavy so it can be stored as a trust fact +/// without lifetimes. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CoseSign1MessagePartsFact { + pub protected_header: Arc>, + pub unprotected_header: Arc>, + pub payload: Option>>, + pub signature: Arc>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CoseSign1MessageBytesFact { + pub bytes: Arc<[u8]>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DetachedPayloadPresentFact { + pub present: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ContentTypeFact { + pub content_type: String, +} + +/// Indicates whether the COSE header parameter for CWT Claims (label 15) is present. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CwtClaimsPresentFact { + pub present: bool, +} + +/// Parsed view of a CWT Claims map from the COSE header parameter (label 15). +/// +/// This exposes common standard claims as optional fields, and also preserves any scalar +/// (string/int/bool) claim values in `scalar_claims` keyed by claim label. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CwtClaimsFact { + pub scalar_claims: BTreeMap, + + /// Raw CBOR bytes for each numeric claim label. + pub raw_claims: BTreeMap>, + + /// Raw CBOR bytes for each text claim key. + pub raw_claims_text: BTreeMap>, + + pub iss: Option, + pub sub: Option, + pub aud: Option, + pub exp: Option, + pub nbf: Option, + pub iat: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CwtClaimScalar { + Str(String), + I64(i64), + Bool(bool), +} + +impl CwtClaimsFact { + pub fn claim_value_i64(&self, label: i64) -> Option> { + self.raw_claims + .get(&label) + .map(|b| CborValueReader::new(b.as_ref())) + } + + pub fn claim_value_text(&self, key: &str) -> Option> { + self.raw_claims_text + .get(key) + .map(|b| CborValueReader::new(b.as_ref())) + } +} + +/// Field-name constants for declarative trust policies. +pub mod fields { + pub mod detached_payload_present { + pub const PRESENT: &str = "present"; + } + + pub mod content_type { + pub const CONTENT_TYPE: &str = "content_type"; + } + + pub mod cwt_claims_present { + pub const PRESENT: &str = "present"; + } + + pub mod cwt_claims { + pub const ISS: &str = "iss"; + pub const SUB: &str = "sub"; + pub const AUD: &str = "aud"; + pub const EXP: &str = "exp"; + pub const NBF: &str = "nbf"; + pub const IAT: &str = "iat"; + + /// Scalar claim values can also be addressed by numeric label. + /// + /// Format: `claim_