diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 5ba318e5..5b23e18e 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -77,6 +77,37 @@ jobs: - name: List working directory run: ${{ matrix.dir_command }} + native-coverage-windows: + name: native-coverage-windows-asan + if: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }} + runs-on: windows-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Build Rust FFI libraries (release) + shell: pwsh + run: | + cargo build --manifest-path native/rust/Cargo.toml --release -p cose_sign1_validation_ffi -p cose_sign1_validation_ffi_certificates -p cose_sign1_validation_ffi_mst -p cose_sign1_validation_ffi_akv -p cose_sign1_validation_ffi_trust + + - name: Install OpenCppCoverage + shell: pwsh + run: | + choco install opencppcoverage -y --no-progress + + - name: Native C coverage (Debug + ASAN, 95% gate) + shell: pwsh + run: | + ./native/c/collect-coverage.ps1 -Configuration Debug -MinimumLineCoveragePercent 95 + + - name: Native C++ coverage (Debug + ASAN, 95% gate) + shell: pwsh + run: | + ./native/c_pp/collect-coverage.ps1 -Configuration Debug -MinimumLineCoveragePercent 95 + # Create a changelog that includes all the PRs merged since the last release. # If it's not a pull request, skip to the build job. create_changelog: diff --git a/.gitignore b/.gitignore index 74c870b1..3e6b06e8 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,11 @@ bld/ [Ll]og/ [Ll]ogs/ +# CMake build directories +build/ +build-*/ +cmake-build-*/ + # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot @@ -149,6 +154,11 @@ coverage*.json coverage*.xml coverage*.info +# Coverage output directories (native scripts, OpenCppCoverage, etc.) +coverage/ +coverage-*/ +coverage*/ + # Visual Studio code coverage results *.coverage *.coveragexml @@ -198,6 +208,26 @@ PublishScripts/ *.nupkg # NuGet Symbol Packages *.snupkg + +# vcpkg artifacts (manifest mode / local dev) +vcpkg_installed/ +vcpkg/downloads/ +vcpkg/buildtrees/ +vcpkg/packages/ + +# vcpkg artifacts that may appear under subfolders +native/**/vcpkg_installed/ +native/**/vcpkg/downloads/ +native/**/vcpkg/buildtrees/ +native/**/vcpkg/packages/ + +# Native (C/C++) CMake build outputs +native/**/build/ +native/**/CMakeFiles/ +native/**/CMakeCache.txt +native/**/cmake_install.cmake +native/**/CTestTestfile.cmake +native/**/Testing/ # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. @@ -366,3 +396,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/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 00000000..5c473b12 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,22 @@ +{ + "version": 4, + "configurations": [ + { + "name": "windows-msvc-x64 (native packs)", + "intelliSenseMode": "windows-msvc-x64", + "cStandard": "c11", + "cppStandard": "c++17", + "includePath": [ + "${workspaceFolder}/native/c/include", + "${workspaceFolder}/native/c_pp/include", + "${workspaceFolder}/native/c_pp/../c/include" + ], + "defines": [ + "COSE_HAS_CERTIFICATES_PACK", + "COSE_HAS_MST_PACK", + "COSE_HAS_AKV_PACK", + "COSE_HAS_TRUST_PACK" + ] + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..e3e1bfcf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + // This repo contains multiple independent CMake projects under native/. + // The C/C++ extension may not automatically pick up per-target CMake definitions, + // so we provide a workspace-wide IntelliSense config in .vscode/c_cpp_properties.json + // (including COSE_HAS_*_PACK defines) to prevent pack-gated code from being greyed out. + "C_Cpp.default.intelliSenseMode": "windows-msvc-x64", + "C_Cpp.default.cppStandard": "c++17", + "C_Cpp.default.cStandard": "c11" +} diff --git a/native/.gitignore b/native/.gitignore new file mode 100644 index 00000000..729824f3 --- /dev/null +++ b/native/.gitignore @@ -0,0 +1,31 @@ +# CMake build directories +build/ +Build/ +out/ +build-*/ +cmake-build-*/ + +# Rust build artifacts +target/ + +# Visual Studio +.vs/ +*.user +*.suo +*.vcxproj.user + +# Test outputs +Testing/ + +# Coverage reports +coverage/ +coverage-*/ +coverage*/ +*.profraw +*.profdata + +# vcpkg artifacts (when using vcpkg from this folder) +vcpkg_installed/ +vcpkg/downloads/ +vcpkg/buildtrees/ +vcpkg/packages/ diff --git a/native/ARCHITECTURE.md b/native/ARCHITECTURE.md new file mode 100644 index 00000000..205b89d6 --- /dev/null +++ b/native/ARCHITECTURE.md @@ -0,0 +1,399 @@ +# Native Architecture + +This document moved to [native/docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). +# Native FFI Architecture + +This document describes the complete architecture of the native (C/C++) projections for the COSE Sign1 validation library. + +## Overview + +The native projections provide three layers of abstraction: +1. **Rust FFI Layer**: C ABI exports from Rust using `extern "C"` +2. **C Projection**: Direct C API wrapping the FFI layer +3. **C++ Projection**: RAII wrappers providing modern C++ idioms + +All three layers follow a **per-pack modular architecture**, allowing consumers to include and link only the functionality they need. + +## Per-Pack Modularity + +The library is organized into packs, each providing specific validation functionality: + +- **Base**: Core validator, builder, result types (required) +- **Certificates Pack**: X.509 certificate validation +- **MST Pack**: Merkle Sealed Transparency receipt verification +- **AKV Pack**: Azure Key Vault KID validation +- **Trust Pack**: Trust policy authoring (future milestone) + +Each pack is: +- A separate Rust FFI crate (staticlib/cdylib) +- A separate C header file +- A separate C++ header file +- An optional CMake target +- An optional vcpkg feature (future) + +## Layer 1: Rust FFI + +### Directory Structure +``` +native/rust/ +├── cose_sign1_validation_ffi/ # Base FFI (required) +│ ├── Cargo.toml # crate-type = ["cdylib", "staticlib", "rlib"] +│ └── src/ +│ ├── lib.rs # Core types, builder, validator +│ ├── error.rs # Panic catching, thread-local errors +│ └── version.rs # ABI versioning +├── cose_sign1_validation_ffi_certificates/ # Certificates pack FFI +│ ├── Cargo.toml # crate-type = ["staticlib", "cdylib"] +│ └── src/ +│ ├── lib.rs # Pack registration function +│ └── options.rs # C ABI options struct +├── cose_sign1_validation_ffi_mst/ # MST pack FFI +├── cose_sign1_validation_ffi_akv/ # AKV pack FFI +└── cose_sign1_validation_ffi_trust/ # Trust pack FFI (placeholder) +``` + +### Build Artifacts +- **Windows**: `*.dll` + `*.dll.lib` (import library) +- **Linux**: `*.so` +- **macOS**: `*.dylib` + +Static libraries (`.lib`/`.a`) also available for all packs. + +### C ABI Types +```c +// Opaque handles +typedef struct cose_validator_builder_t cose_validator_builder_t; +typedef struct cose_validator_t cose_validator_t; +typedef struct cose_validation_result_t cose_validation_result_t; + +// Status codes +typedef enum { + COSE_OK = 0, + COSE_ERR = 1, + COSE_PANIC = 2, + COSE_INVALID_ARG = 3 +} cose_status_t; + +// Pack options (one struct per pack) +typedef struct cose_certificate_trust_options_t { /* ... */ } cose_certificate_trust_options_t; +typedef struct cose_mst_trust_options_t { /* ... */ } cose_mst_trust_options_t; +typedef struct cose_akv_trust_options_t { /* ... */ } cose_akv_trust_options_t; +``` + +### Key Functions (Base) +```c +cose_validator_builder_t* cose_validator_builder_new(void); +void cose_validator_builder_free(cose_validator_builder_t*); +cose_status_t cose_validator_builder_build(cose_validator_builder_t*, cose_validator_t**); +cose_status_t cose_validator_validate_bytes(cose_validator_t*, const uint8_t*, size_t, + const uint8_t*, size_t, cose_validation_result_t**); +``` + +### Key Functions (Per-Pack) +```c +// Certificates pack +cose_status_t cose_validator_builder_with_certificates_pack(cose_validator_builder_t*); +cose_status_t cose_validator_builder_with_certificates_pack_ex(cose_validator_builder_t*, + cose_certificate_trust_options_t*); + +// MST pack +cose_status_t cose_validator_builder_with_mst_pack(cose_validator_builder_t*); +cose_status_t cose_validator_builder_with_mst_pack_ex(cose_validator_builder_t*, + cose_mst_trust_options_t*); + +// AKV pack +cose_status_t cose_validator_builder_with_akv_pack(cose_validator_builder_t*); +cose_status_t cose_validator_builder_with_akv_pack_ex(cose_validator_builder_t*, + cose_akv_trust_options_t*); +``` + +## Layer 2: C Projection + +### Directory Structure +``` +native/c/ +├── CMakeLists.txt # Build system with conditional pack linking +├── README.md # C API documentation +├── include/cose/ +│ ├── cose_sign1.h # Base API (required) +│ ├── cose_certificates.h # Certificates pack API +│ ├── cose_mst.h # MST pack API +│ └── cose_azure_key_vault.h # AKV pack API +└── tests/ + ├── CMakeLists.txt + └── smoke_test.c # Basic validation test +``` + +### CMake Configuration +```cmake +find_library(COSE_FFI_BASE_LIB cose_sign1_validation_ffi REQUIRED) +find_library(COSE_FFI_CERTIFICATES_LIB cose_sign1_validation_ffi_certificates) +find_library(COSE_FFI_MST_LIB cose_sign1_validation_ffi_mst) +find_library(COSE_FFI_AKV_LIB cose_sign1_validation_ffi_akv) + +if(COSE_FFI_CERTIFICATES_LIB) + target_link_libraries(cose_sign1 PUBLIC ${COSE_FFI_CERTIFICATES_LIB}) + target_compile_definitions(cose_sign1 PUBLIC COSE_HAS_CERTIFICATES_PACK) +endif() +# ... similar for MST and AKV +``` + +### Header Organization +Each pack header: +1. Includes `cose_sign1.h` (base types) +2. Declares pack-specific options struct +3. Declares pack registration functions +4. Protected by include guards +5. Uses `extern "C"` for C++ compatibility + +### Usage Example (C) +```c +#include +#include + +cose_validator_builder_t* builder = cose_validator_builder_new(); +cose_validator_builder_with_certificates_pack(builder); + +cose_validator_t* validator; +if (cose_validator_builder_build(builder, &validator) != COSE_OK) { + fprintf(stderr, "Build failed: %s\n", cose_last_error_message_utf8()); + cose_validator_builder_free(builder); + return 1; +} + +cose_validation_result_t* result; +cose_validator_validate_bytes(validator, cose_bytes, cose_len, NULL, 0, &result); + +if (cose_validation_result_ok(result)) { + printf("Valid!\n"); +} else { + char* msg = cose_validation_result_failure_message(result); + printf("Invalid: %s\n", msg); + cose_string_free(msg); +} + +cose_validation_result_free(result); +cose_validator_free(validator); +cose_validator_builder_free(builder); +``` + +## Layer 3: C++ Projection + +### Directory Structure +``` +native/c_pp/ +├── CMakeLists.txt # Interface library with conditional pack linking +├── README.md # C++ API documentation +├── include/cose/ +│ ├── validator.hpp # Base RAII types (required) +│ ├── certificates.hpp # Certificates pack RAII +│ ├── mst.hpp # MST pack RAII +│ ├── azure_key_vault.hpp # AKV pack RAII +│ └── cose.hpp # Convenience header (includes all) +└── tests/ + ├── CMakeLists.txt + └── smoke_test.cpp # RAII validation test +``` + +### RAII Design Principles +- **Non-copyable**: Copy constructors deleted +- **Movable**: Move constructors/assignment enabled +- **Exception-based**: Errors throw `cose::cose_error` +- **Automatic cleanup**: Destructors call FFI free functions +- **Modern C++17**: Uses `std::vector`, `std::string`, structured bindings + +### Key Classes + +#### Base (validator.hpp) +```cpp +namespace cose { + // Exception type + class cose_error : public std::runtime_error { /* ... */ }; + + // RAII wrapper for validation result + class ValidationResult { + cose_validation_result_t* handle; + public: + ValidationResult(cose_validation_result_t*); + ~ValidationResult(); + bool Ok() const; + std::string FailureMessage() const; + }; + + // RAII wrapper for validator + class Validator { + cose_validator_t* handle; + public: + Validator(cose_validator_t*); + ~Validator(); + ValidationResult Validate(const std::vector& cose_bytes, + const std::vector& detached_payload = {}); + }; + + // Fluent builder base class + class ValidatorBuilder { + protected: + cose_validator_builder_t* handle; + public: + ValidatorBuilder(); + virtual ~ValidatorBuilder(); + Validator Build(); + }; +} +``` + +#### Per-Pack Extensions (certificates.hpp, mst.hpp, azure_key_vault.hpp) +```cpp +namespace cose { + // Options use C++ types + struct CertificateOptions { + bool trust_embedded_chain_as_trusted = false; + bool identity_pinning_enabled = false; + std::vector allowed_thumbprints; + std::vector pqc_algorithm_oids; + }; + + // Builder extends base class + class ValidatorBuilderWithCertificates : public ValidatorBuilder { + public: + ValidatorBuilderWithCertificates& WithCertificates(); + ValidatorBuilderWithCertificates& WithCertificates(const CertificateOptions& options); + }; +} +``` + +### Usage Example (C++) +```cpp +#include + +try { + // Fluent builder with pack + auto validator = cose::ValidatorBuilderWithCertificates() + .WithCertificates() + .Build(); + + std::vector cose_bytes = /* ... */; + auto result = validator.Validate(cose_bytes); + + if (result.Ok()) { + std::cout << "Valid!\n"; + } else { + std::cout << "Invalid: " << result.FailureMessage() << "\n"; + } + + // RAII cleanup happens automatically +} catch (const cose::cose_error& e) { + std::cerr << "Error: " << e.what() << "\n"; + return 1; +} +``` + +## Build System Integration + +### CMake Workflow +1. Build Rust FFI libraries: `cargo build --release --workspace` +2. Configure C projection: `cmake -B build -S native/c -DBUILD_TESTING=ON` +3. Build C projection: `cmake --build build --config Release` +4. Configure C++ projection: `cmake -B build -S native/c_pp -DBUILD_TESTING=ON` +5. Build C++ projection: `cmake --build build --config Release` +6. Run tests: `ctest -C Release` (requires Rust DLLs in PATH) + +### vcpkg (Overlay Port) +```json +{ + "name": "cosesign1-validation-native", + "version-string": "0.1.0", + "description": "C and C++ projections for COSE_Sign1 validation (Rust FFI-backed)", + "supports": "windows | linux | osx", + "default-features": ["certificates", "cpp"], + "features": { + "cpp": { + "description": "Install C++ projection headers + CMake target" + }, + "certificates": { + "description": "Build/install X.509 certificates pack FFI and enable COSE_HAS_CERTIFICATES_PACK" + }, + "mst": { + "description": "Build/install MST pack FFI and enable COSE_HAS_MST_PACK" + }, + "akv": { + "description": "Build/install Azure Key Vault pack FFI and enable COSE_HAS_AKV_PACK" + }, + "trust": { + "description": "Build/install trust-policy pack FFI and enable COSE_HAS_TRUST_PACK" + } + } +} +``` + +## Error Handling + +### Rust FFI Layer +- All public functions wrapped in `with_catch_unwind()` +- Panics converted to `COSE_PANIC` status code +- Error messages stored thread-locally +- Retrieved via `cose_last_error_message_utf8()` + +### C Projection +- Check status codes after every call +- Use `cose_last_error_message_utf8()` for details +- Manually free all returned strings with `cose_string_free()` + +### C++ Projection +- Exceptions thrown for all errors +- `cose::cose_error` includes detailed message +- RAII ensures cleanup even during exception unwinding +- No manual resource management needed + +## Testing Strategy + +### Smoke Tests (Current) +- **C**: Builder creation, pack registration, validator build +- **C++**: RAII wrappers, fluent API, exception handling, all packs + +### Future Integration Tests +- Real COSE Sign1 message validation +- Certificate chain validation scenarios +- MST receipt verification with mock receipts +- AKV KID validation with pattern matching +- Trust policy evaluation +- Negative test cases (invalid signatures, expired certs, etc.) + +### Coverage Testing +- **Rust**: `cargo-llvm-cov` with 95% target (already achieved) +- **C**: OpenCppCoverage (Windows) or gcov (Linux) +- **C++**: OpenCppCoverage (Windows) or gcov (Linux) + +## Documentation + +Each layer provides: +- **README.md**: Usage guide with examples +- **API reference**: Inline comments in headers +- **Architecture guide**: This document +- **Progress log**: [FFI_PROJECTIONS_PROGRESS.md](FFI_PROJECTIONS_PROGRESS.md) + +## Future Work + +### Milestone M3: Trust Policy Authoring +- Expose trust policy DSL to C/C++ +- `TrustPlanBuilder` FFI +- C and C++ wrappers for policy construction +- Default trust plans + +### Milestone M4: Comprehensive Testing +- Integration tests with real COSE messages +- Certificate validation test suite +- MST verification test suite +- Performance benchmarks + +### Milestone M5: Packaging +- vcpkg port with per-pack features +- CMake find_package support +- Conan package (optional) +- Documentation site + +### Milestone M6: Coverage & CI +- OpenCppCoverage scripts for C/C++ +- GitHub Actions workflow for native builds +- Coverage reporting and enforcement +- Cross-platform testing (Windows, Linux, macOS) diff --git a/native/FFI_PROJECTIONS_PROGRESS.md b/native/FFI_PROJECTIONS_PROGRESS.md new file mode 100644 index 00000000..ff765133 --- /dev/null +++ b/native/FFI_PROJECTIONS_PROGRESS.md @@ -0,0 +1,3 @@ +# FFI Projections Progress Log + +This document moved to [native/docs/FFI_PROJECTIONS_PROGRESS.md](docs/FFI_PROJECTIONS_PROGRESS.md). diff --git a/native/c/CMakeLists.txt b/native/c/CMakeLists.txt new file mode 100644 index 00000000..60383510 --- /dev/null +++ b/native/c/CMakeLists.txt @@ -0,0 +1,150 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cmake_minimum_required(VERSION 3.20) + +project(cose_sign1_c + VERSION 0.1.0 + DESCRIPTION "C projection for COSE Sign1 validation" + LANGUAGES C CXX +) + +# Standard CMake testing option (BUILD_TESTING) + CTest integration. +include(CTest) + +# C standard +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +# C++ is only used for optional GoogleTest-based tests (C API is still C). +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +option(COSE_ENABLE_ASAN "Enable AddressSanitizer for native builds" OFF) + +if(COSE_ENABLE_ASAN) + if(MSVC) + add_compile_options(/fsanitize=address) + if(CMAKE_VERSION VERSION_LESS "3.21") + message(WARNING "COSE_ENABLE_ASAN is ON. On Windows, CMake 3.21+ is recommended so post-build steps can copy runtime DLL dependencies.") + endif() + elseif(CMAKE_C_COMPILER_ID MATCHES "Clang|GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + add_compile_options(-fsanitize=address -fno-omit-frame-pointer) + add_link_options(-fsanitize=address) + endif() +endif() + +# Find Rust FFI libraries +# These should be built first with: cargo build --release --workspace +set(RUST_FFI_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../rust/target/release") + +# Base FFI library (required) +find_library(COSE_FFI_BASE_LIB + NAMES cose_sign1_validation_ffi + PATHS ${RUST_FFI_DIR} + REQUIRED +) + +# Pack FFI libraries (optional) +find_library(COSE_FFI_CERTIFICATES_LIB + NAMES cose_sign1_validation_ffi_certificates + PATHS ${RUST_FFI_DIR} +) + +find_library(COSE_FFI_MST_LIB + NAMES cose_sign1_validation_ffi_mst + PATHS ${RUST_FFI_DIR} +) + +find_library(COSE_FFI_AKV_LIB + NAMES cose_sign1_validation_ffi_akv + PATHS ${RUST_FFI_DIR} +) + +find_library(COSE_FFI_TRUST_LIB + NAMES cose_sign1_validation_ffi_trust + PATHS ${RUST_FFI_DIR} +) + +# Create interface library for headers +add_library(cose_headers INTERFACE) +target_include_directories(cose_headers INTERFACE + $ + $ +) + +# Main library - just provides the Rust FFI libraries as an importable target +add_library(cose_sign1 INTERFACE) +target_link_libraries(cose_sign1 INTERFACE + cose_headers + ${COSE_FFI_BASE_LIB} +) + +# Link standard system libraries required by Rust +if(WIN32) + target_link_libraries(cose_sign1 INTERFACE + ws2_32 + advapi32 + userenv + bcrypt + ntdll + ) +elseif(UNIX) + target_link_libraries(cose_sign1 INTERFACE + pthread + dl + m + ) +endif() + +# Optional pack libraries +if(COSE_FFI_CERTIFICATES_LIB) + message(STATUS "Found certificates pack: ${COSE_FFI_CERTIFICATES_LIB}") + target_link_libraries(cose_sign1 INTERFACE ${COSE_FFI_CERTIFICATES_LIB}) + target_compile_definitions(cose_sign1 INTERFACE COSE_HAS_CERTIFICATES_PACK) +endif() + +if(COSE_FFI_MST_LIB) + message(STATUS "Found MST pack: ${COSE_FFI_MST_LIB}") + target_link_libraries(cose_sign1 INTERFACE ${COSE_FFI_MST_LIB}) + target_compile_definitions(cose_sign1 INTERFACE COSE_HAS_MST_PACK) +endif() + +if(COSE_FFI_AKV_LIB) + message(STATUS "Found AKV pack: ${COSE_FFI_AKV_LIB}") + target_link_libraries(cose_sign1 INTERFACE ${COSE_FFI_AKV_LIB}) + target_compile_definitions(cose_sign1 INTERFACE COSE_HAS_AKV_PACK) +endif() + +if(COSE_FFI_TRUST_LIB) + message(STATUS "Found trust pack: ${COSE_FFI_TRUST_LIB}") + target_link_libraries(cose_sign1 INTERFACE ${COSE_FFI_TRUST_LIB}) + target_compile_definitions(cose_sign1 INTERFACE COSE_HAS_TRUST_PACK) +endif() + +# Enable testing +if(BUILD_TESTING) + add_subdirectory(tests) +endif() + +add_subdirectory(examples) + +# Installation rules +install(DIRECTORY include/cose + DESTINATION include + FILES_MATCHING PATTERN "*.h" +) + +install(TARGETS cose_sign1 cose_headers + EXPORT cose_sign1_targets + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib + RUNTIME DESTINATION bin + INCLUDES DESTINATION include +) + +install(EXPORT cose_sign1_targets + FILE cose_sign1-targets.cmake + NAMESPACE cose:: + DESTINATION lib/cmake/cose_sign1 +) diff --git a/native/c/README.md b/native/c/README.md new file mode 100644 index 00000000..46d898e2 --- /dev/null +++ b/native/c/README.md @@ -0,0 +1,270 @@ +# COSE Sign1 C API + +C projection for the COSE Sign1 validation library. + +## Prerequisites + +- CMake 3.20 or later +- C11-capable compiler (MSVC, GCC, Clang) +- Rust toolchain (to build the underlying FFI libraries) + +## Building + +### 1. Build the Rust FFI libraries + +```bash +cd ../rust +cargo build --release --workspace +``` + +This will produce the FFI libraries in `../rust/target/release/`. + +### 2. Configure and build the C projection + +```bash +mkdir build +cd build +cmake .. -DBUILD_TESTING=ON +cmake --build . --config Release +``` + +### 3. Run tests + +```bash +ctest -C Release +``` + +## Coverage (Windows) + +Coverage for the C projection is collected with OpenCppCoverage. + +```powershell +./collect-coverage.ps1 -Configuration Debug -MinimumLineCoveragePercent 95 +``` + +Note: on Windows, `Debug` tends to produce the most reliable line-coverage measurement under OpenCppCoverage (especially when ASAN is enabled). + +Outputs HTML to [native/c/coverage/index.html](coverage/index.html). + +## Usage Example + +## Compilable example programs + +This repo ships a real, buildable C example you can use as a starting point: + +- [native/c/examples/trust_policy_example.c](examples/trust_policy_example.c) + +Build it (after building the Rust FFI libs): + +```bash +cd native/c +cmake -S . -B build -DBUILD_TESTING=ON +cmake --build build --config Release --target cose_trust_policy_example +``` + +Run it: + +```bash +native/c/build/examples/Release/cose_trust_policy_example.exe path/to/message.cose [path/to/detached_payload.bin] +``` + +### Detailed end-to-end example (custom trust policy + feedback) + +This example shows how to: +- Configure a validator builder (optionally adding packs) +- Author a custom trust policy with message-scope and pack-specific helpers +- Compile the policy into a bundled trust plan and attach it to the validator +- Validate bytes and print user-friendly feedback + +If you build via this repo’s CMake, the optional packs are exposed via `COSE_HAS__PACK`. + +```c +#include +#include +#include +#include +#include + +#include +#include +#include + +static void print_last_error_and_free(void) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "%s\n", err ? err : "(no error message)"); + if (err) cose_string_free(err); +} + +#define COSE_CHECK(call) \ + do { \ + cose_status_t _st = (call); \ + if (_st != COSE_OK) { \ + fprintf(stderr, "FAILED: %s\n", #call); \ + print_last_error_and_free(); \ + goto cleanup; \ + } \ + } while (0) + +int main(void) { + cose_validator_builder_t* builder = NULL; + cose_trust_policy_builder_t* policy = NULL; + cose_compiled_trust_plan_t* plan = NULL; + cose_validator_t* validator = NULL; + cose_validation_result_t* result = NULL; + + // 1) Create builder + COSE_CHECK(cose_validator_builder_new(&builder)); + + // 2) Add packs you intend to rely on in policy. +#ifdef COSE_HAS_CERTIFICATES_PACK + COSE_CHECK(cose_validator_builder_with_certificates_pack(builder)); +#endif +#ifdef COSE_HAS_MST_PACK + COSE_CHECK(cose_validator_builder_with_mst_pack(builder)); +#endif +#ifdef COSE_HAS_AKV_PACK + COSE_CHECK(cose_validator_builder_with_akv_pack(builder)); +#endif + + // 3) Build a custom trust policy (starts empty) + COSE_CHECK(cose_trust_policy_builder_new_from_validator_builder(builder, &policy)); + + // Message-scope rules + COSE_CHECK(cose_trust_policy_builder_require_content_type_non_empty(policy)); + COSE_CHECK(cose_trust_policy_builder_require_detached_payload_absent(policy)); + COSE_CHECK(cose_trust_policy_builder_require_cwt_claims_present(policy)); + + // Pack-specific trust-policy helpers +#ifdef COSE_HAS_CERTIFICATES_PACK + COSE_CHECK(cose_certificates_trust_policy_builder_require_x509_chain_trusted(policy)); + COSE_CHECK(cose_certificates_trust_policy_builder_require_signing_certificate_present(policy)); + COSE_CHECK(cose_certificates_trust_policy_builder_require_signing_certificate_thumbprint_present(policy)); +#endif + +#ifdef COSE_HAS_MST_PACK + // Require at least one MST receipt on counter-signatures. + COSE_CHECK(cose_mst_trust_policy_builder_require_receipt_present(policy)); +#endif + +#ifdef COSE_HAS_AKV_PACK + COSE_CHECK(cose_akv_trust_policy_builder_require_azure_key_vault_kid_allowed(policy)); +#endif + + // 4) Compile the policy into a bundled plan and attach it + COSE_CHECK(cose_trust_policy_builder_compile(policy, &plan)); + COSE_CHECK(cose_validator_builder_with_compiled_trust_plan(builder, plan)); + + // 5) Build validator + COSE_CHECK(cose_validator_builder_build(builder, &validator)); + + // 6) Validate bytes + // NOTE: Replace these with your actual bytes. + const uint8_t* cose_bytes = NULL; + size_t cose_bytes_len = 0; + + if (cose_bytes == NULL || cose_bytes_len == 0) { + fprintf(stderr, "Provide COSE_Sign1 bytes before calling validate.\n"); + goto cleanup; + } + + COSE_CHECK(cose_validator_validate_bytes( + validator, + cose_bytes, + cose_bytes_len, + NULL, + 0, + &result + )); + + { + bool ok = false; + COSE_CHECK(cose_validation_result_is_success(result, &ok)); + if (ok) { + printf("Validation successful\n"); + } else { + char* msg = cose_validation_result_failure_message_utf8(result); + printf("Validation failed: %s\n", msg ? msg : "(no message)"); + if (msg) cose_string_free(msg); + } + } + +cleanup: + if (result) cose_validation_result_free(result); + if (validator) cose_validator_free(validator); + if (plan) cose_compiled_trust_plan_free(plan); + if (policy) cose_trust_policy_builder_free(policy); + if (builder) cose_validator_builder_free(builder); + + return 0; +} +``` + +## Available Pack Headers + +- `` - Base validation API (required) +- `` - X.509 certificate validation pack +- `` - Microsoft Secure Transparency receipt verification pack +- `` - Azure Key Vault KID validation pack + +## Pack Options + +Each pack supports two functions: +- `cose_validator_builder_with__pack()` - Use default (secure) options +- `cose_validator_builder_with__pack_ex()` - Use custom options + +### Certificates Pack Options + +```c +cose_certificate_trust_options_t opts = { + .trust_embedded_chain_as_trusted = true, // For testing/pinned roots + .identity_pinning_enabled = true, + .allowed_thumbprints = (const char*[]){ + "ABCD1234...", + NULL // NULL-terminated + }, + .pqc_algorithm_oids = NULL // No custom PQC OIDs +}; +cose_validator_builder_with_certificates_pack_ex(builder, &opts); +``` + +### MST Pack Options + +```c +cose_mst_trust_options_t opts = { + .allow_network = false, + .offline_jwks_json = "{...}", // JWKS JSON string + .jwks_api_version = NULL +}; +cose_validator_builder_with_mst_pack_ex(builder, &opts); +``` + +### Azure Key Vault Pack Options + +```c +cose_akv_trust_options_t opts = { + .require_azure_key_vault_kid = true, + .allowed_kid_patterns = (const char*[]){ + "https://*.vault.azure.net/keys/*", + "https://*.managedhsm.azure.net/keys/*", + NULL + } +}; +cose_validator_builder_with_akv_pack_ex(builder, &opts); +``` + +## Error Handling + +All functions return `cose_status_t`: +- `COSE_OK` - Success +- `COSE_ERR` - Error (retrieve message with `cose_last_error_message_utf8()`) +- `COSE_PANIC` - Rust panic (should not occur in normal usage) +- `COSE_INVALID_ARG` - Invalid argument (e.g., null pointer) + +Error messages are thread-local. Always call `cose_string_free()` on strings returned by the library. + +## Memory Management + +- All `*_new()` functions allocate handles that must be freed with corresponding `*_free()` functions +- `*_free()` functions accept NULL pointers (no-op) +- Strings returned by the library must be freed with `cose_string_free()` +- String arrays in option structs are NOT owned by the library (caller retains ownership) diff --git a/native/c/collect-coverage.ps1 b/native/c/collect-coverage.ps1 new file mode 100644 index 00000000..848eb2a0 --- /dev/null +++ b/native/c/collect-coverage.ps1 @@ -0,0 +1,490 @@ +[CmdletBinding()] +param( + [ValidateSet('Debug', 'Release', 'RelWithDebInfo')] + [string]$Configuration = 'RelWithDebInfo', + + [string]$BuildDir = (Join-Path $PSScriptRoot 'build'), + [string]$ReportDir = (Join-Path $PSScriptRoot 'coverage'), + + # Compile and run tests under AddressSanitizer (ASAN) to catch memory errors. + # On MSVC this enables /fsanitize=address. + [switch]$EnableAsan = $true, + + # Optional: use vcpkg toolchain so GoogleTest can be found and the CTest + # suite runs gtest-discovered tests. + [string]$VcpkgRoot = 'C:\vcpkg', + [string]$VcpkgTriplet = 'x64-windows', + [switch]$UseVcpkg = $true, + [switch]$EnsureGTest = $true, + + # If set, fail fast when OpenCppCoverage isn't available. + # Otherwise, run tests via CTest and skip coverage generation. + [switch]$RequireCoverageTool, + + # Minimum overall line coverage percentage required for the C projection test suite. + # Set to 0 to disable coverage gating (tests will still run). + [ValidateRange(0, 100)] + [int]$MinimumLineCoveragePercent = 95, + + [switch]$NoBuild +) + +$ErrorActionPreference = 'Stop' + +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 Get-VsInstallationPath { + $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 + } + + $vsPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + if ($LASTEXITCODE -ne 0 -or -not $vsPath) { + $vsPath = & $vswhere -latest -products * -property installationPath + } + + if (-not $vsPath) { + return $null + } + + $vsPath = ($vsPath | Select-Object -First 1).Trim() + if (-not $vsPath) { + return $null + } + + if (-not (Test-Path $vsPath)) { + return $null + } + + return $vsPath +} + +function Add-VsAsanRuntimeToPath { + if (-not ($env:OS -eq 'Windows_NT')) { + return + } + + $vsPath = Get-VsInstallationPath + if (-not $vsPath) { + return + } + + # On MSVC, /fsanitize=address depends on clang ASAN runtime DLLs that ship with VS. + # If they're not on PATH, Windows shows modal popup dialogs and tests fail with 0xc0000135. + $candidateDirs = @() + + $msvcToolsRoot = Join-Path $vsPath 'VC\Tools\MSVC' + if (Test-Path $msvcToolsRoot) { + $latestMsvc = Get-ChildItem -Path $msvcToolsRoot -Directory -ErrorAction SilentlyContinue | + Sort-Object Name -Descending | + Select-Object -First 1 + if ($latestMsvc) { + $candidateDirs += (Join-Path $latestMsvc.FullName 'bin\Hostx64\x64') + $candidateDirs += (Join-Path $latestMsvc.FullName 'bin\Hostx64\x86') + } + } + + $llvmRoot = Join-Path $vsPath 'VC\Tools\Llvm' + if (Test-Path $llvmRoot) { + $candidateDirs += (Join-Path $llvmRoot 'x64\bin') + $clangLibRoot = Join-Path $llvmRoot 'x64\lib\clang' + if (Test-Path $clangLibRoot) { + $latestClang = Get-ChildItem -Path $clangLibRoot -Directory -ErrorAction SilentlyContinue | + Sort-Object Name -Descending | + Select-Object -First 1 + if ($latestClang) { + $candidateDirs += (Join-Path $latestClang.FullName 'lib\windows') + } + } + } + + $asanDllName = 'clang_rt.asan_dynamic-x86_64.dll' + foreach ($dir in ($candidateDirs | Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique)) { + if (Test-Path (Join-Path $dir $asanDllName)) { + if ($env:PATH -notlike "${dir}*") { + $env:PATH = "${dir};$env:PATH" + Write-Host "Using ASAN runtime from: $dir" -ForegroundColor Yellow + } + return + } + } +} + +function Find-VsCMakeBin { + function Probe-VsRootForCMakeBin([string]$vsRoot) { + if (-not $vsRoot -or -not (Test-Path $vsRoot)) { + return $null + } + + # Typical layout: + # \\\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe + $years = Get-ChildItem -Path $vsRoot -Directory -ErrorAction SilentlyContinue + foreach ($year in $years) { + $editions = Get-ChildItem -Path $year.FullName -Directory -ErrorAction SilentlyContinue + foreach ($edition in $editions) { + $cmakeBin = Join-Path $edition.FullName 'Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin' + if (Test-Path (Join-Path $cmakeBin 'cmake.exe')) { + return $cmakeBin + } + + $cmakeExtensionRoot = Join-Path $edition.FullName 'Common7\IDE\CommonExtensions\Microsoft\CMake' + if (Test-Path $cmakeExtensionRoot) { + $found = Get-ChildItem -Path $cmakeExtensionRoot -Recurse -File -Filter 'cmake.exe' -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ($found) { + return (Split-Path -Parent $found.FullName) + } + } + } + } + return $null + } + + $vswhere = Resolve-ExePath -Name 'vswhere' -FallbackPaths @( + "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe", + "${env:ProgramFiles}\Microsoft Visual Studio\Installer\vswhere.exe" + ) + + if ($vswhere) { + $vsPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + if ($LASTEXITCODE -ne 0 -or -not $vsPath) { + $vsPath = & $vswhere -latest -products * -property installationPath + } + if ($vsPath) { + $vsPath = ($vsPath | Select-Object -First 1).Trim() + if ($vsPath) { + $cmakeBin = Join-Path $vsPath 'Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin' + if (Test-Path (Join-Path $cmakeBin 'cmake.exe')) { + return $cmakeBin + } + + $cmakeExtensionRoot = Join-Path $vsPath 'Common7\IDE\CommonExtensions\Microsoft\CMake' + if (Test-Path $cmakeExtensionRoot) { + $found = Get-ChildItem -Path $cmakeExtensionRoot -Recurse -File -Filter 'cmake.exe' -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ($found) { + return (Split-Path -Parent $found.FullName) + } + } + } + } + } + + # Final fallback: probe common Visual Studio roots when vswhere is missing/unavailable. + $roots = @( + (Join-Path $env:ProgramFiles 'Microsoft Visual Studio'), + (Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio') + ) + foreach ($r in ($roots | Where-Object { $_ })) { + $bin = Probe-VsRootForCMakeBin -vsRoot $r + if ($bin) { + return $bin + } + } + + return $null +} + +function Get-NormalizedPath([string]$Path) { + return [System.IO.Path]::GetFullPath($Path) +} + +function Get-CoberturaLineCoverage([string]$CoberturaPath) { + if (-not (Test-Path $CoberturaPath)) { + throw "Cobertura report not found: $CoberturaPath" + } + + [xml]$xml = Get-Content -LiteralPath $CoberturaPath + $root = $xml.SelectSingleNode('/coverage') + if (-not $root) { + throw "Invalid Cobertura report (missing root): $CoberturaPath" + } + + # OpenCppCoverage's Cobertura export can include the same source file multiple + # times (e.g., once per module/test executable). The root totals may + # therefore double-count "lines-valid" and under-report the union coverage. + # Aggregate coverage by (filename, line number) and take the max hits. + $fileToLineHits = @{} + $classNodes = $xml.SelectNodes('//class[@filename]') + foreach ($classNode in $classNodes) { + $filename = $classNode.GetAttribute('filename') + if (-not $filename) { + continue + } + + if (-not $fileToLineHits.ContainsKey($filename)) { + $fileToLineHits[$filename] = @{} + } + + $lineNodes = $classNode.SelectNodes('lines/line[@number and @hits]') + foreach ($lineNode in $lineNodes) { + $lineNumber = [int]$lineNode.GetAttribute('number') + $hits = [int]$lineNode.GetAttribute('hits') + $lineHitsForFile = $fileToLineHits[$filename] + + if ($lineHitsForFile.ContainsKey($lineNumber)) { + if ($hits -gt $lineHitsForFile[$lineNumber]) { + $lineHitsForFile[$lineNumber] = $hits + } + } else { + $lineHitsForFile[$lineNumber] = $hits + } + } + } + + $dedupedValid = 0 + $dedupedCovered = 0 + foreach ($filename in $fileToLineHits.Keys) { + foreach ($lineNumber in $fileToLineHits[$filename].Keys) { + $dedupedValid += 1 + if ($fileToLineHits[$filename][$lineNumber] -gt 0) { + $dedupedCovered += 1 + } + } + } + + $dedupedPercent = 0.0 + if ($dedupedValid -gt 0) { + $dedupedPercent = ($dedupedCovered / [double]$dedupedValid) * 100.0 + } + + # Keep root totals for diagnostics/fallback. + $rootLinesValid = [int]$root.GetAttribute('lines-valid') + $rootLinesCovered = [int]$root.GetAttribute('lines-covered') + $rootLineRateAttr = $root.GetAttribute('line-rate') + $rootPercent = 0.0 + if ($rootLinesValid -gt 0) { + $rootPercent = ($rootLinesCovered / [double]$rootLinesValid) * 100.0 + } elseif ($rootLineRateAttr) { + $rootPercent = ([double]$rootLineRateAttr) * 100.0 + } + + # If the deduped aggregation produced no data (e.g., missing entries), + # fall back to root totals so we still surface something useful. + if ($dedupedValid -le 0 -and $rootLinesValid -gt 0) { + $dedupedValid = $rootLinesValid + $dedupedCovered = $rootLinesCovered + $dedupedPercent = $rootPercent + } + + return [pscustomobject]@{ + LinesValid = $dedupedValid + LinesCovered = $dedupedCovered + Percent = $dedupedPercent + + RootLinesValid = $rootLinesValid + RootLinesCovered = $rootLinesCovered + RootPercent = $rootPercent + FileCount = $fileToLineHits.Count + } +} + +function Assert-Tooling { + $openCpp = Get-Command 'OpenCppCoverage.exe' -ErrorAction SilentlyContinue + if (-not $openCpp) { + $candidates = @( + $env:OPENCPPCOVERAGE_PATH, + 'C:\\Program Files\\OpenCppCoverage\\OpenCppCoverage.exe', + 'C:\\Program Files (x86)\\OpenCppCoverage\\OpenCppCoverage.exe' + ) + foreach ($candidate in $candidates) { + if ($candidate -and (Test-Path $candidate)) { + $openCpp = [pscustomobject]@{ Source = $candidate } + break + } + } + } + if (-not $openCpp -and $RequireCoverageTool) { + throw "OpenCppCoverage.exe not found on PATH. Install OpenCppCoverage and ensure it's available in PATH, or omit -RequireCoverageTool to run tests without coverage. See: https://github.com/OpenCppCoverage/OpenCppCoverage" + } + + $cmakeExe = (Get-Command 'cmake.exe' -ErrorAction SilentlyContinue).Source + $ctestExe = (Get-Command 'ctest.exe' -ErrorAction SilentlyContinue).Source + + if ((-not $cmakeExe) -or (-not $ctestExe)) { + if ($env:OS -eq 'Windows_NT') { + $vsCmakeBin = Find-VsCMakeBin + if ($vsCmakeBin) { + # Prefer using the VS-bundled CMake/CTest, and ensure child processes can find them. + if ($env:PATH -notlike "${vsCmakeBin}*") { + $env:PATH = "${vsCmakeBin};$env:PATH" + } + + if (-not $cmakeExe) { + $candidate = (Join-Path $vsCmakeBin 'cmake.exe') + if (Test-Path $candidate) { $cmakeExe = $candidate } + } + if (-not $ctestExe) { + $candidate = (Join-Path $vsCmakeBin 'ctest.exe') + if (Test-Path $candidate) { $ctestExe = $candidate } + } + } + } + } + + if (-not $cmakeExe) { + throw 'cmake.exe not found on PATH (and no Visual Studio-bundled CMake was found).' + } + if (-not $ctestExe) { + throw 'ctest.exe not found on PATH (and no Visual Studio-bundled CTest was found).' + } + + $vcpkgExe = Join-Path $VcpkgRoot 'vcpkg.exe' + if ($UseVcpkg -or $EnsureGTest) { + if (-not (Test-Path $vcpkgExe)) { + throw "vcpkg.exe not found at $vcpkgExe" + } + + $toolchain = Join-Path $VcpkgRoot 'scripts\buildsystems\vcpkg.cmake' + if (-not (Test-Path $toolchain)) { + throw "vcpkg toolchain not found at $toolchain" + } + } + + return @{ + OpenCppCoverage = if ($openCpp) { $openCpp.Source } else { $null } + CMake = $cmakeExe + CTest = $ctestExe + } +} + +$tools = Assert-Tooling +$openCppCoverageExe = $tools.OpenCppCoverage +$cmakeExe = $tools.CMake +$ctestExe = $tools.CTest + +if ($MinimumLineCoveragePercent -gt 0) { + $RequireCoverageTool = $true +} + +# If the caller didn't explicitly override BuildDir/ReportDir, use ASAN-specific defaults. +if ($EnableAsan) { + if (-not $PSBoundParameters.ContainsKey('BuildDir')) { + $BuildDir = (Join-Path $PSScriptRoot 'build-asan') + } + if (-not $PSBoundParameters.ContainsKey('ReportDir')) { + $ReportDir = (Join-Path $PSScriptRoot 'coverage-asan') + } + + # Leak detection is generally not supported/usable on Windows; keep it off to reduce noise. + $env:ASAN_OPTIONS = 'detect_leaks=0,halt_on_error=1' + + Add-VsAsanRuntimeToPath +} + +if (-not $NoBuild) { + if ($EnsureGTest) { + $vcpkgExe = Join-Path $VcpkgRoot 'vcpkg.exe' + & $vcpkgExe install "gtest:$VcpkgTriplet" + if ($LASTEXITCODE -ne 0) { + throw "vcpkg failed to install gtest:$VcpkgTriplet" + } + $UseVcpkg = $true + } + + $cmakeArgs = @('-S', $PSScriptRoot, '-B', $BuildDir, '-DBUILD_TESTING=ON') + if ($EnableAsan) { + $cmakeArgs += '-DCOSE_ENABLE_ASAN=ON' + } + if ($UseVcpkg) { + $toolchain = Join-Path $VcpkgRoot 'scripts\buildsystems\vcpkg.cmake' + $cmakeArgs += "-DCMAKE_TOOLCHAIN_FILE=$toolchain" + $cmakeArgs += "-DVCPKG_TARGET_TRIPLET=$VcpkgTriplet" + $cmakeArgs += "-DVCPKG_APPLOCAL_DEPS=OFF" + } + + & $cmakeExe @cmakeArgs + & $cmakeExe --build $BuildDir --config $Configuration +} + +if (-not (Test-Path $BuildDir)) { + throw "Build directory not found: $BuildDir. Build first (or pass -BuildDir pointing to an existing build)." +} + +New-Item -ItemType Directory -Force -Path $ReportDir | Out-Null + +$sourcesList = @( + # The C projection is mostly ABI declarations in headers; measurable lines + # are primarily in the test suite that exercises the API. + (Get-NormalizedPath (Join-Path $PSScriptRoot 'include')), + (Get-NormalizedPath (Join-Path $PSScriptRoot 'tests')) +) + +$excludeList = @( + (Get-NormalizedPath $BuildDir), + (Get-NormalizedPath (Join-Path $PSScriptRoot '..\\rust\\target')) +) + +if ($openCppCoverageExe) { + $coberturaPath = (Join-Path $ReportDir 'cobertura.xml') + + $openCppArgs = @() + foreach($s in $sourcesList) { $openCppArgs += '--sources'; $openCppArgs += $s } + foreach($e in $excludeList) { $openCppArgs += '--excluded_sources'; $openCppArgs += $e } + $openCppArgs += '--export_type' + $openCppArgs += ("html:" + $ReportDir) + $openCppArgs += '--export_type' + $openCppArgs += ("cobertura:" + $coberturaPath) + + # CTest spawns test executables; we must enable child-process coverage. + $openCppArgs += '--cover_children' + + $openCppArgs += '--quiet' + $openCppArgs += '--' + + & $openCppCoverageExe @openCppArgs $ctestExe --test-dir $BuildDir -C $Configuration --output-on-failure + + if ($LASTEXITCODE -ne 0) { + throw "OpenCppCoverage failed with exit code $LASTEXITCODE" + } + + $coverage = Get-CoberturaLineCoverage $coberturaPath + $pct = [Math]::Round([double]$coverage.Percent, 2) + Write-Host "Line coverage (C projection suite): ${pct}% ($($coverage.LinesCovered)/$($coverage.LinesValid))" + + if (($null -ne $coverage.RootLinesValid) -and ($coverage.RootLinesValid -gt 0)) { + $rootPct = [Math]::Round([double]$coverage.RootPercent, 2) + Write-Host "(Cobertura root totals: ${rootPct}% ($($coverage.RootLinesCovered)/$($coverage.RootLinesValid)))" + } + + if ($MinimumLineCoveragePercent -gt 0) { + if ($coverage.LinesValid -le 0) { + throw "No coverable lines were detected by OpenCppCoverage (lines-valid=0); cannot enforce $MinimumLineCoveragePercent% gate." + } + + if ($coverage.Percent -lt $MinimumLineCoveragePercent) { + throw "Line coverage ${pct}% is below required ${MinimumLineCoveragePercent}%." + } + } +} else { + Write-Warning "OpenCppCoverage.exe not found; running tests without coverage." + & $ctestExe --test-dir $BuildDir -C $Configuration --output-on-failure + if ($LASTEXITCODE -ne 0) { + throw "CTest failed with exit code $LASTEXITCODE" + } +} + +Write-Host "Coverage report: $(Join-Path $ReportDir 'index.html')" diff --git a/native/c/docs/01-consume-vcpkg.md b/native/c/docs/01-consume-vcpkg.md new file mode 100644 index 00000000..7a0d6e04 --- /dev/null +++ b/native/c/docs/01-consume-vcpkg.md @@ -0,0 +1,36 @@ +# Consume via vcpkg (C) + +This projection ships as a single vcpkg port that installs headers + a CMake package. + +## Install + +Using the repo’s overlay port: + +```powershell +vcpkg install cosesign1-validation-native[certificates,mst,akv,trust] --overlay-ports=/native/vcpkg_ports +``` + +Notes: + +- Default features are `cpp` and `certificates`. If you’re consuming only the C projection, you can disable defaults: + +```powershell +vcpkg install cosesign1-validation-native[certificates,mst,akv,trust] --no-default-features --overlay-ports=/native/vcpkg_ports +``` + +## CMake usage + +```cmake +find_package(cose_sign1_validation CONFIG REQUIRED) + +target_link_libraries(your_target PRIVATE cosesign1_validation_native::cose_sign1) +``` + +## Feature → header mapping + +- `certificates` → `` and `COSE_HAS_CERTIFICATES_PACK` +- `mst` → `` and `COSE_HAS_MST_PACK` +- `akv` → `` and `COSE_HAS_AKV_PACK` +- `trust` → `` and `COSE_HAS_TRUST_PACK` + +When consuming via vcpkg/CMake, the `COSE_HAS_*` macros are set for you based on enabled features. diff --git a/native/c/docs/02-core-api.md b/native/c/docs/02-core-api.md new file mode 100644 index 00000000..3cbd185f --- /dev/null +++ b/native/c/docs/02-core-api.md @@ -0,0 +1,51 @@ +# Core API (C) + +The base validation API is in ``. + +## Basic flow + +1) Create a builder +2) Optionally enable packs on the builder +3) Build a validator +4) Validate bytes +5) Inspect the result + +## Minimal example + +```c +#include +#include + +int validate(const unsigned char* msg, size_t msg_len) { + cose_validator_builder_t* builder = NULL; + cose_validator_t* validator = NULL; + cose_validation_result_t* result = NULL; + + if (cose_validator_builder_new(&builder) != COSE_OK) return 1; + + if (cose_validator_builder_build(builder, &validator) != COSE_OK) { + cose_validator_builder_free(builder); + return 1; + } + + // Builder can be freed after build. + cose_validator_builder_free(builder); + + if (cose_validator_validate_bytes(validator, msg, msg_len, NULL, 0, &result) != COSE_OK) { + cose_validator_free(validator); + return 1; + } + + bool ok = false; + (void)cose_validation_result_is_success(result, &ok); + + cose_validation_result_free(result); + cose_validator_free(validator); + + return ok ? 0 : 2; +} +``` + +## Detached payload + +`cose_validator_validate_bytes` accepts an optional detached payload buffer. Pass `NULL, 0` for embedded payload. diff --git a/native/c/docs/03-errors.md b/native/c/docs/03-errors.md new file mode 100644 index 00000000..a25f48b9 --- /dev/null +++ b/native/c/docs/03-errors.md @@ -0,0 +1,39 @@ +# Errors (C) + +## Status codes + +Most APIs return `cose_status_t`: + +- `COSE_OK`: success +- `COSE_ERR`: failure; call `cose_last_error_message_utf8()` to get details +- `COSE_PANIC`: Rust panic crossed the FFI boundary +- `COSE_INVALID_ARG`: null pointer / invalid argument + +## Getting the last error + +`cose_last_error_message_utf8()` returns a newly allocated UTF-8 string for the current thread. + +```c +char* msg = cose_last_error_message_utf8(); +if (msg) { + // log msg + cose_string_free(msg); +} +``` + +You can clear it with `cose_last_error_clear()`. + +## Validation failures vs call failures + +- A call failure (e.g., invalid input buffer) returns a non-`COSE_OK` status. +- A validation failure still returns `COSE_OK`, but `cose_validation_result_is_success(..., &ok)` will set `ok=false`. + +To get a human-readable validation failure reason: + +```c +char* failure = cose_validation_result_failure_message_utf8(result); +if (failure) { + // log failure + cose_string_free(failure); +} +``` diff --git a/native/c/docs/04-packs.md b/native/c/docs/04-packs.md new file mode 100644 index 00000000..e6d690f9 --- /dev/null +++ b/native/c/docs/04-packs.md @@ -0,0 +1,32 @@ +# Packs (C) + +Packs are optional “trust evidence” providers (certificates, MST receipts, AKV KID rules, trust plan composition helpers). + +Enable packs on a `cose_validator_builder_t*` before building the validator. + +## Certificates pack + +Header: `` + +- `cose_validator_builder_with_certificates_pack(builder)` +- `cose_validator_builder_with_certificates_pack_ex(builder, &options)` + +## MST pack + +Header: `` + +- `cose_validator_builder_with_mst_pack(builder)` +- `cose_validator_builder_with_mst_pack_ex(builder, &options)` + +## Azure Key Vault pack + +Header: `` + +- `cose_validator_builder_with_akv_pack(builder)` +- `cose_validator_builder_with_akv_pack_ex(builder, &options)` + +## Trust pack + +Header: `` + +The trust pack provides the trust-plan/policy authoring surface and compiled trust plan attachment. diff --git a/native/c/docs/05-trust-plans.md b/native/c/docs/05-trust-plans.md new file mode 100644 index 00000000..ab5d73f6 --- /dev/null +++ b/native/c/docs/05-trust-plans.md @@ -0,0 +1,35 @@ +# Trust plans and policies (C) + +The trust authoring surface is in ``. + +There are two related concepts: + +- **Trust policy**: a minimal fluent surface for message-scope requirements, compiled into a bundled plan. +- **Trust plan builder**: selects pack default plans and composes them (OR/AND), also able to compile allow-all/deny-all. + +## Attach a compiled plan to a validator + +A compiled plan can be attached to the validator builder, overriding the default behavior. + +High level: + +1) Start with `cose_validator_builder_t*` +2) Create a plan/policy builder from it +3) Compile into `cose_compiled_trust_plan_t*` +4) Attach with `cose_validator_builder_with_compiled_trust_plan` + +Key APIs: + +- Policies: + - `cose_trust_policy_builder_new_from_validator_builder` + - `cose_trust_policy_builder_require_*` + - `cose_trust_policy_builder_compile` + +- Plan builder: + - `cose_trust_plan_builder_new_from_validator_builder` + - `cose_trust_plan_builder_add_all_pack_default_plans` + - `cose_trust_plan_builder_compile_or` / `..._compile_and` + - `cose_trust_plan_builder_compile_allow_all` / `..._compile_deny_all` + +- Attach: + - `cose_validator_builder_with_compiled_trust_plan` diff --git a/native/c/docs/README.md b/native/c/docs/README.md new file mode 100644 index 00000000..1690a651 --- /dev/null +++ b/native/c/docs/README.md @@ -0,0 +1,19 @@ +# Native C docs + +Start here: + +- [Consume via vcpkg](01-consume-vcpkg.md) +- [Core API](02-core-api.md) +- [Packs](04-packs.md) +- [Trust plans and policies](05-trust-plans.md) +- [Errors](03-errors.md) + +Cross-cutting: + +- Testing/coverage/ASAN: see [native/docs/06-testing-coverage-asan.md](../../docs/06-testing-coverage-asan.md) + +## Repo quick links + +- Headers: [native/c/include/cose/](../include/cose/) +- Examples: [native/c/examples/](../examples/) +- Tests: [native/c/tests/](../tests/) diff --git a/native/c/examples/CMakeLists.txt b/native/c/examples/CMakeLists.txt new file mode 100644 index 00000000..7847f911 --- /dev/null +++ b/native/c/examples/CMakeLists.txt @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Examples are optional and primarily for developer guidance. +option(COSE_BUILD_EXAMPLES "Build C projection examples" ON) + +if(NOT COSE_BUILD_EXAMPLES) + return() +endif() + +if(NOT COSE_FFI_TRUST_LIB) + message(STATUS "Skipping C examples: trust pack not found (cose_sign1_validation_ffi_trust)") + return() +endif() + +add_executable(cose_trust_policy_example + trust_policy_example.c +) + +target_link_libraries(cose_trust_policy_example PRIVATE + cose_sign1 +) diff --git a/native/c/examples/trust_policy_example.c b/native/c/examples/trust_policy_example.c new file mode 100644 index 00000000..0d946789 --- /dev/null +++ b/native/c/examples/trust_policy_example.c @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include +#include + +#ifdef COSE_HAS_CERTIFICATES_PACK +#include +#endif + +#ifdef COSE_HAS_MST_PACK +#include +#endif + +#ifdef COSE_HAS_AKV_PACK +#include +#endif + +#include +#include +#include +#include + +static void print_last_error_and_free(void) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "%s\n", err ? err : "(no error message)"); + if (err) cose_string_free(err); +} + +static bool read_file_bytes(const char* path, uint8_t** out_bytes, size_t* out_len) { + *out_bytes = NULL; + *out_len = 0; + + FILE* f = NULL; +#if defined(_MSC_VER) + if (fopen_s(&f, path, "rb") != 0) { + return false; + } +#else + f = fopen(path, "rb"); + if (!f) { + return false; + } +#endif + + if (fseek(f, 0, SEEK_END) != 0) { + fclose(f); + return false; + } + + long size = ftell(f); + if (size < 0) { + fclose(f); + return false; + } + + if (fseek(f, 0, SEEK_SET) != 0) { + fclose(f); + return false; + } + + uint8_t* buf = (uint8_t*)malloc((size_t)size); + if (!buf) { + fclose(f); + return false; + } + + size_t read = fread(buf, 1, (size_t)size, f); + fclose(f); + + if (read != (size_t)size) { + free(buf); + return false; + } + + *out_bytes = buf; + *out_len = (size_t)size; + return true; +} + +#define COSE_CHECK(call) \ + do { \ + cose_status_t _st = (call); \ + if (_st != COSE_OK) { \ + fprintf(stderr, "FAILED: %s\n", #call); \ + print_last_error_and_free(); \ + goto cleanup; \ + } \ + } while (0) + +static void usage(const char* argv0) { + fprintf(stderr, + "Usage:\n" + " %s [detached_payload.bin]\n\n" + "Notes:\n" + "- This example builds a custom trust policy, compiles it to a bundled plan, and attaches it\n" + " to the validator builder before validating the message.\n", + argv0); +} + +int main(int argc, char** argv) { + if (argc < 2) { + usage(argv[0]); + return 2; + } + + const char* cose_path = argv[1]; + const char* payload_path = (argc >= 3) ? argv[2] : NULL; + + uint8_t* cose_bytes = NULL; + size_t cose_len = 0; + + uint8_t* payload_bytes = NULL; + size_t payload_len = 0; + + cose_validator_builder_t* builder = NULL; + cose_trust_policy_builder_t* policy = NULL; + cose_compiled_trust_plan_t* plan = NULL; + cose_validator_t* validator = NULL; + cose_validation_result_t* result = NULL; + + if (!read_file_bytes(cose_path, &cose_bytes, &cose_len)) { + fprintf(stderr, "Failed to read COSE file: %s\n", cose_path); + return 2; + } + + if (payload_path) { + if (!read_file_bytes(payload_path, &payload_bytes, &payload_len)) { + fprintf(stderr, "Failed to read detached payload file: %s\n", payload_path); + free(cose_bytes); + return 2; + } + } + + // 1) Builder + packs + COSE_CHECK(cose_validator_builder_new(&builder)); + +#ifdef COSE_HAS_CERTIFICATES_PACK + COSE_CHECK(cose_validator_builder_with_certificates_pack(builder)); +#endif +#ifdef COSE_HAS_MST_PACK + COSE_CHECK(cose_validator_builder_with_mst_pack(builder)); +#endif +#ifdef COSE_HAS_AKV_PACK + COSE_CHECK(cose_validator_builder_with_akv_pack(builder)); +#endif + + // 2) Custom trust policy bound to builder's packs + COSE_CHECK(cose_trust_policy_builder_new_from_validator_builder(builder, &policy)); + + // Message-scope requirements (safe to rely on trust pack being present) + if (payload_path) { + COSE_CHECK(cose_trust_policy_builder_require_detached_payload_present(policy)); + } else { + COSE_CHECK(cose_trust_policy_builder_require_detached_payload_absent(policy)); + } + +#ifdef COSE_HAS_CERTIFICATES_PACK + // Signing-key scope requirements (certificates pack) + COSE_CHECK(cose_trust_policy_builder_and(policy)); + COSE_CHECK(cose_certificates_trust_policy_builder_require_x509_chain_trusted(policy)); + COSE_CHECK(cose_certificates_trust_policy_builder_require_signing_certificate_present(policy)); + COSE_CHECK(cose_certificates_trust_policy_builder_require_signing_certificate_thumbprint_present(policy)); +#endif + +#ifdef COSE_HAS_MST_PACK + COSE_CHECK(cose_trust_policy_builder_and(policy)); + COSE_CHECK(cose_mst_trust_policy_builder_require_receipt_present(policy)); +#endif + +#ifdef COSE_HAS_AKV_PACK + COSE_CHECK(cose_trust_policy_builder_and(policy)); + COSE_CHECK(cose_akv_trust_policy_builder_require_azure_key_vault_kid_allowed(policy)); +#endif + + // 3) Compile + attach + COSE_CHECK(cose_trust_policy_builder_compile(policy, &plan)); + COSE_CHECK(cose_validator_builder_with_compiled_trust_plan(builder, plan)); + + // 4) Build validator + COSE_CHECK(cose_validator_builder_build(builder, &validator)); + + // 5) Validate + COSE_CHECK(cose_validator_validate_bytes( + validator, + cose_bytes, + cose_len, + payload_bytes, + payload_len, + &result + )); + + { + bool ok = false; + COSE_CHECK(cose_validation_result_is_success(result, &ok)); + if (ok) { + printf("Validation successful\n"); + } else { + char* msg = cose_validation_result_failure_message_utf8(result); + printf("Validation failed: %s\n", msg ? msg : "(no message)"); + if (msg) cose_string_free(msg); + } + } + +cleanup: + if (result) cose_validation_result_free(result); + if (validator) cose_validator_free(validator); + if (plan) cose_compiled_trust_plan_free(plan); + if (policy) cose_trust_policy_builder_free(policy); + if (builder) cose_validator_builder_free(builder); + + free(payload_bytes); + free(cose_bytes); + + return 0; +} diff --git a/native/c/include/cose/cose_azure_key_vault.h b/native/c/include/cose/cose_azure_key_vault.h new file mode 100644 index 00000000..30defa4c --- /dev/null +++ b/native/c/include/cose/cose_azure_key_vault.h @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * @file cose_azure_key_vault.h + * @brief Azure Key Vault KID validation pack for COSE Sign1 + */ + +#ifndef COSE_AZURE_KEY_VAULT_H +#define COSE_AZURE_KEY_VAULT_H + +#include "cose_sign1.h" +#include "cose_trust.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Options for Azure Key Vault KID validation + */ +typedef struct { + /** If true, require the KID to look like an Azure Key Vault identifier */ + bool require_azure_key_vault_kid; + + /** NULL-terminated array of allowed KID pattern strings (supports wildcards * and ?). + * NULL means use default patterns (*.vault.azure.net/keys/*, *.managedhsm.azure.net/keys/*). */ + const char* const* allowed_kid_patterns; +} cose_akv_trust_options_t; + +/** + * @brief Add Azure Key Vault KID validation pack with default options + * + * Default options (secure-by-default): + * - require_azure_key_vault_kid: true + * - allowed_kid_patterns: + * - https://*.vault.azure.net/keys/* + * - https://*.managedhsm.azure.net/keys/* + * + * @param builder Validator builder handle + * @return COSE_OK on success, error code otherwise + */ +cose_status_t cose_validator_builder_with_akv_pack( + cose_validator_builder_t* builder +); + +/** + * @brief Add Azure Key Vault KID validation pack with custom options + * + * @param builder Validator builder handle + * @param options Options structure (NULL for defaults) + * @return COSE_OK on success, error code otherwise + */ +cose_status_t cose_validator_builder_with_akv_pack_ex( + cose_validator_builder_t* builder, + const cose_akv_trust_options_t* options +); + +/** + * @brief Trust-policy helper: require that the message `kid` looks like an Azure Key Vault key identifier. + * + * This API is provided by the AKV pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_akv_trust_policy_builder_require_azure_key_vault_kid( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Trust-policy helper: require that the message `kid` does not look like an Azure Key Vault key identifier. + * + * This API is provided by the AKV pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_akv_trust_policy_builder_require_not_azure_key_vault_kid( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Trust-policy helper: require that the message `kid` is allowlisted by the AKV pack configuration. + * + * This API is provided by the AKV pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_akv_trust_policy_builder_require_azure_key_vault_kid_allowed( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Trust-policy helper: require that the message `kid` is not allowlisted by the AKV pack configuration. + * + * This API is provided by the AKV pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_akv_trust_policy_builder_require_azure_key_vault_kid_not_allowed( + cose_trust_policy_builder_t* policy_builder +); + +#ifdef __cplusplus +} +#endif + +#endif // COSE_AZURE_KEY_VAULT_H diff --git a/native/c/include/cose/cose_certificates.h b/native/c/include/cose/cose_certificates.h new file mode 100644 index 00000000..988eaf7c --- /dev/null +++ b/native/c/include/cose/cose_certificates.h @@ -0,0 +1,377 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * @file cose_certificates.h + * @brief X.509 certificate validation pack for COSE Sign1 + */ + +#ifndef COSE_CERTIFICATES_H +#define COSE_CERTIFICATES_H + +#include "cose_sign1.h" +#include "cose_trust.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Options for X.509 certificate validation + */ +typedef struct { + /** If true, treat well-formed embedded x5chain as trusted (for tests/pinned roots) */ + bool trust_embedded_chain_as_trusted; + + /** If true, enable identity pinning based on allowed_thumbprints */ + bool identity_pinning_enabled; + + /** NULL-terminated array of allowed certificate thumbprints (case/whitespace insensitive). + * NULL means no thumbprint filtering. */ + const char* const* allowed_thumbprints; + + /** NULL-terminated array of PQC algorithm OID strings. + * NULL means no custom PQC OIDs. */ + const char* const* pqc_algorithm_oids; +} cose_certificate_trust_options_t; + +/** + * @brief Add X.509 certificate validation pack with default options + * + * Default options: + * - trust_embedded_chain_as_trusted: false + * - identity_pinning_enabled: false + * - No thumbprint filtering + * - No custom PQC OIDs + * + * @param builder Validator builder handle + * @return COSE_OK on success, error code otherwise + */ +cose_status_t cose_validator_builder_with_certificates_pack( + cose_validator_builder_t* builder +); + +/** + * @brief Add X.509 certificate validation pack with custom options + * + * @param builder Validator builder handle + * @param options Options structure (NULL for defaults) + * @return COSE_OK on success, error code otherwise + */ +cose_status_t cose_validator_builder_with_certificates_pack_ex( + cose_validator_builder_t* builder, + const cose_certificate_trust_options_t* options +); + +/** + * @brief Trust-policy helper: require that the X.509 chain is trusted. + * + * This API is provided by the certificates pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_certificates_trust_policy_builder_require_x509_chain_trusted( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Trust-policy helper: require that the X.509 chain is not trusted. + * + * This API is provided by the certificates pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_certificates_trust_policy_builder_require_x509_chain_not_trusted( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Trust-policy helper: require that the X.509 chain could be built (pack observed at least one element). + * + * This API is provided by the certificates pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_certificates_trust_policy_builder_require_x509_chain_built( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Trust-policy helper: require that the X.509 chain could not be built. + * + * This API is provided by the certificates pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_certificates_trust_policy_builder_require_x509_chain_not_built( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Trust-policy helper: require that the X.509 chain element count equals `expected`. + * + * This API is provided by the certificates pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_certificates_trust_policy_builder_require_x509_chain_element_count_eq( + cose_trust_policy_builder_t* policy_builder, + size_t expected +); + +/** + * @brief Trust-policy helper: require that the X.509 chain status flags equal `expected`. + * + * This API is provided by the certificates pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_certificates_trust_policy_builder_require_x509_chain_status_flags_eq( + cose_trust_policy_builder_t* policy_builder, + uint32_t expected +); + +/** + * @brief Trust-policy helper: require that the leaf chain element (index 0) has a non-empty thumbprint. + */ +cose_status_t cose_certificates_trust_policy_builder_require_leaf_chain_thumbprint_present( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Trust-policy helper: require that a signing certificate identity fact is present. + */ +cose_status_t cose_certificates_trust_policy_builder_require_signing_certificate_present( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Trust-policy helper: pin the leaf certificate subject name (chain element index 0). + */ +cose_status_t cose_certificates_trust_policy_builder_require_leaf_subject_eq( + cose_trust_policy_builder_t* policy_builder, + const char* subject_utf8 +); + +/** + * @brief Trust-policy helper: pin the issuer certificate subject name (chain element index 1). + */ +cose_status_t cose_certificates_trust_policy_builder_require_issuer_subject_eq( + cose_trust_policy_builder_t* policy_builder, + const char* subject_utf8 +); + +/** + * @brief Trust-policy helper: require that the signing certificate subject/issuer matches the leaf chain element. + */ +cose_status_t cose_certificates_trust_policy_builder_require_signing_certificate_subject_issuer_matches_leaf_chain_element( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Trust-policy helper: if the issuer element (index 1) is missing, allow; otherwise require issuer chaining. + */ +cose_status_t cose_certificates_trust_policy_builder_require_leaf_issuer_is_next_chain_subject_optional( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Trust-policy helper: require the leaf signing certificate thumbprint to equal the provided value. + */ +cose_status_t cose_certificates_trust_policy_builder_require_signing_certificate_thumbprint_eq( + cose_trust_policy_builder_t* policy_builder, + const char* thumbprint_utf8 +); + +/** + * @brief Trust-policy helper: require that the leaf signing certificate thumbprint is present and non-empty. + */ +cose_status_t cose_certificates_trust_policy_builder_require_signing_certificate_thumbprint_present( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Trust-policy helper: require the leaf signing certificate subject to equal the provided value. + */ +cose_status_t cose_certificates_trust_policy_builder_require_signing_certificate_subject_eq( + cose_trust_policy_builder_t* policy_builder, + const char* subject_utf8 +); + +/** + * @brief Trust-policy helper: require the leaf signing certificate issuer to equal the provided value. + */ +cose_status_t cose_certificates_trust_policy_builder_require_signing_certificate_issuer_eq( + cose_trust_policy_builder_t* policy_builder, + const char* issuer_utf8 +); + +/** + * @brief Trust-policy helper: require the leaf signing certificate serial number to equal the provided value. + */ +cose_status_t cose_certificates_trust_policy_builder_require_signing_certificate_serial_number_eq( + cose_trust_policy_builder_t* policy_builder, + const char* serial_number_utf8 +); + +/** + * @brief Trust-policy helper: require that the signing certificate is expired at or before `now_unix_seconds`. + */ +cose_status_t cose_certificates_trust_policy_builder_require_signing_certificate_expired_at_or_before( + cose_trust_policy_builder_t* policy_builder, + int64_t now_unix_seconds +); + +/** + * @brief Trust-policy helper: require that the leaf signing certificate is valid at `now_unix_seconds`. + */ +cose_status_t cose_certificates_trust_policy_builder_require_signing_certificate_valid_at( + cose_trust_policy_builder_t* policy_builder, + int64_t now_unix_seconds +); + +/** + * @brief Trust-policy helper: require signing certificate not-before <= `max_unix_seconds`. + */ +cose_status_t cose_certificates_trust_policy_builder_require_signing_certificate_not_before_le( + cose_trust_policy_builder_t* policy_builder, + int64_t max_unix_seconds +); + +/** + * @brief Trust-policy helper: require signing certificate not-before >= `min_unix_seconds`. + */ +cose_status_t cose_certificates_trust_policy_builder_require_signing_certificate_not_before_ge( + cose_trust_policy_builder_t* policy_builder, + int64_t min_unix_seconds +); + +/** + * @brief Trust-policy helper: require signing certificate not-after <= `max_unix_seconds`. + */ +cose_status_t cose_certificates_trust_policy_builder_require_signing_certificate_not_after_le( + cose_trust_policy_builder_t* policy_builder, + int64_t max_unix_seconds +); + +/** + * @brief Trust-policy helper: require signing certificate not-after >= `min_unix_seconds`. + */ +cose_status_t cose_certificates_trust_policy_builder_require_signing_certificate_not_after_ge( + cose_trust_policy_builder_t* policy_builder, + int64_t min_unix_seconds +); + +/** + * @brief Trust-policy helper: require that the X.509 chain element at `index` has subject equal to the provided value. + */ +cose_status_t cose_certificates_trust_policy_builder_require_chain_element_subject_eq( + cose_trust_policy_builder_t* policy_builder, + size_t index, + const char* subject_utf8 +); + +/** + * @brief Trust-policy helper: require that the X.509 chain element at `index` has issuer equal to the provided value. + */ +cose_status_t cose_certificates_trust_policy_builder_require_chain_element_issuer_eq( + cose_trust_policy_builder_t* policy_builder, + size_t index, + const char* issuer_utf8 +); + +/** + * @brief Trust-policy helper: require that the X.509 chain element at `index` has thumbprint equal to the provided value. + */ +cose_status_t cose_certificates_trust_policy_builder_require_chain_element_thumbprint_eq( + cose_trust_policy_builder_t* policy_builder, + size_t index, + const char* thumbprint_utf8 +); + +/** + * @brief Trust-policy helper: require that the X.509 chain element at `index` has a non-empty thumbprint. + */ +cose_status_t cose_certificates_trust_policy_builder_require_chain_element_thumbprint_present( + cose_trust_policy_builder_t* policy_builder, + size_t index +); + +/** + * @brief Trust-policy helper: require that the X.509 chain element at `index` is valid at `now_unix_seconds`. + */ +cose_status_t cose_certificates_trust_policy_builder_require_chain_element_valid_at( + cose_trust_policy_builder_t* policy_builder, + size_t index, + int64_t now_unix_seconds +); + +/** + * @brief Trust-policy helper: require chain element not-before <= `max_unix_seconds`. + */ +cose_status_t cose_certificates_trust_policy_builder_require_chain_element_not_before_le( + cose_trust_policy_builder_t* policy_builder, + size_t index, + int64_t max_unix_seconds +); + +/** + * @brief Trust-policy helper: require chain element not-before >= `min_unix_seconds`. + */ +cose_status_t cose_certificates_trust_policy_builder_require_chain_element_not_before_ge( + cose_trust_policy_builder_t* policy_builder, + size_t index, + int64_t min_unix_seconds +); + +/** + * @brief Trust-policy helper: require chain element not-after <= `max_unix_seconds`. + */ +cose_status_t cose_certificates_trust_policy_builder_require_chain_element_not_after_le( + cose_trust_policy_builder_t* policy_builder, + size_t index, + int64_t max_unix_seconds +); + +/** + * @brief Trust-policy helper: require chain element not-after >= `min_unix_seconds`. + */ +cose_status_t cose_certificates_trust_policy_builder_require_chain_element_not_after_ge( + cose_trust_policy_builder_t* policy_builder, + size_t index, + int64_t min_unix_seconds +); + +/** + * @brief Trust-policy helper: deny if a PQC algorithm is explicitly detected; allow if missing. + */ +cose_status_t cose_certificates_trust_policy_builder_require_not_pqc_algorithm_or_missing( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Trust-policy helper: require that the X.509 public key algorithm fact has thumbprint equal to the provided value. + */ +cose_status_t cose_certificates_trust_policy_builder_require_x509_public_key_algorithm_thumbprint_eq( + cose_trust_policy_builder_t* policy_builder, + const char* thumbprint_utf8 +); + +/** + * @brief Trust-policy helper: require that the X.509 public key algorithm OID equals the provided value. + */ +cose_status_t cose_certificates_trust_policy_builder_require_x509_public_key_algorithm_oid_eq( + cose_trust_policy_builder_t* policy_builder, + const char* oid_utf8 +); + +/** + * @brief Trust-policy helper: require that the X.509 public key algorithm is flagged as PQC. + */ +cose_status_t cose_certificates_trust_policy_builder_require_x509_public_key_algorithm_is_pqc( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Trust-policy helper: require that the X.509 public key algorithm is not flagged as PQC. + */ +cose_status_t cose_certificates_trust_policy_builder_require_x509_public_key_algorithm_is_not_pqc( + cose_trust_policy_builder_t* policy_builder +); + +#ifdef __cplusplus +} +#endif + +#endif // COSE_CERTIFICATES_H diff --git a/native/c/include/cose/cose_mst.h b/native/c/include/cose/cose_mst.h new file mode 100644 index 00000000..7a568141 --- /dev/null +++ b/native/c/include/cose/cose_mst.h @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * @file cose_mst.h + * @brief Microsoft Secure Transparency (MST) receipt verification pack for COSE Sign1 + */ + +#ifndef COSE_MST_H +#define COSE_MST_H + +#include "cose_sign1.h" +#include "cose_trust.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Options for MST receipt verification + */ +typedef struct { + /** If true, allow network fetching of JWKS when offline keys are missing */ + bool allow_network; + + /** Offline JWKS JSON string (NULL means no offline JWKS). Not owned by this struct. */ + const char* offline_jwks_json; + + /** Optional api-version for CodeTransparency /jwks endpoint (NULL means no api-version) */ + const char* jwks_api_version; +} cose_mst_trust_options_t; + +/** + * @brief Add MST receipt verification pack with default options (online mode) + * + * Default options: + * - allow_network: true + * - No offline JWKS + * - No api-version + * + * @param builder Validator builder handle + * @return COSE_OK on success, error code otherwise + */ +cose_status_t cose_validator_builder_with_mst_pack( + cose_validator_builder_t* builder +); + +/** + * @brief Add MST receipt verification pack with custom options + * + * @param builder Validator builder handle + * @param options Options structure (NULL for defaults) + * @return COSE_OK on success, error code otherwise + */ +cose_status_t cose_validator_builder_with_mst_pack_ex( + cose_validator_builder_t* builder, + const cose_mst_trust_options_t* options +); + +/** + * @brief Trust-policy helper: require that an MST receipt is present on at least one counter-signature. + * + * This API is provided by the MST pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_mst_trust_policy_builder_require_receipt_present( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Trust-policy helper: require that an MST receipt is not present on at least one counter-signature. + * + * This API is provided by the MST pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_mst_trust_policy_builder_require_receipt_not_present( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Trust-policy helper: require that the MST receipt signature verified. + * + * This API is provided by the MST pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_mst_trust_policy_builder_require_receipt_signature_verified( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Trust-policy helper: require that the MST receipt signature did not verify. + * + * This API is provided by the MST pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_mst_trust_policy_builder_require_receipt_signature_not_verified( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Trust-policy helper: require that the MST receipt issuer contains the provided substring. + * + * This API is provided by the MST pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_mst_trust_policy_builder_require_receipt_issuer_contains( + cose_trust_policy_builder_t* policy_builder, + const char* needle_utf8 +); + +/** + * @brief Trust-policy helper: require that the MST receipt issuer equals the provided value. + * + * This API is provided by the MST pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_mst_trust_policy_builder_require_receipt_issuer_eq( + cose_trust_policy_builder_t* policy_builder, + const char* issuer_utf8 +); + +/** + * @brief Trust-policy helper: require that the MST receipt key id (kid) equals the provided value. + * + * This API is provided by the MST pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_mst_trust_policy_builder_require_receipt_kid_eq( + cose_trust_policy_builder_t* policy_builder, + const char* kid_utf8 +); + +/** + * @brief Trust-policy helper: require that the MST receipt key id (kid) contains the provided substring. + * + * This API is provided by the MST pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_mst_trust_policy_builder_require_receipt_kid_contains( + cose_trust_policy_builder_t* policy_builder, + const char* needle_utf8 +); + +/** + * @brief Trust-policy helper: require that the MST receipt is trusted. + * + * This API is provided by the MST pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_mst_trust_policy_builder_require_receipt_trusted( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Trust-policy helper: require that the MST receipt is not trusted. + * + * This API is provided by the MST pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_mst_trust_policy_builder_require_receipt_not_trusted( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Trust-policy helper: require that the MST receipt is trusted and the issuer contains the provided substring. + * + * This API is provided by the MST pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_mst_trust_policy_builder_require_receipt_trusted_from_issuer_contains( + cose_trust_policy_builder_t* policy_builder, + const char* needle_utf8 +); + +/** + * @brief Trust-policy helper: require that the MST receipt statement SHA-256 equals the provided hex string. + * + * This API is provided by the MST pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_mst_trust_policy_builder_require_receipt_statement_sha256_eq( + cose_trust_policy_builder_t* policy_builder, + const char* sha256_hex_utf8 +); + +/** + * @brief Trust-policy helper: require that the MST receipt statement coverage equals the provided value. + * + * This API is provided by the MST pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_mst_trust_policy_builder_require_receipt_statement_coverage_eq( + cose_trust_policy_builder_t* policy_builder, + const char* coverage_utf8 +); + +/** + * @brief Trust-policy helper: require that the MST receipt statement coverage contains the provided substring. + * + * This API is provided by the MST pack FFI library and extends `cose_trust_policy_builder_t`. + */ +cose_status_t cose_mst_trust_policy_builder_require_receipt_statement_coverage_contains( + cose_trust_policy_builder_t* policy_builder, + const char* needle_utf8 +); + +#ifdef __cplusplus +} +#endif + +#endif // COSE_MST_H diff --git a/native/c/include/cose/cose_sign1.h b/native/c/include/cose/cose_sign1.h new file mode 100644 index 00000000..4d180424 --- /dev/null +++ b/native/c/include/cose/cose_sign1.h @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * @file cose_sign1.h + * @brief C API for COSE Sign1 validation + * + * This header provides the base validation API. To use specific trust packs, + * include the corresponding pack header (cose_certificates.h, cose_mst.h, etc.) + */ + +#ifndef COSE_SIGN1_H +#define COSE_SIGN1_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// ABI version for compatibility checking +#define COSE_ABI_VERSION 1 + +/** + * @brief Status codes returned by COSE API functions + */ +typedef enum { + COSE_OK = 0, ///< Operation succeeded + COSE_ERR = 1, ///< Operation failed (check cose_last_error_message_utf8) + COSE_PANIC = 2, ///< Rust panic occurred (should not happen in normal usage) + COSE_INVALID_ARG = 3 ///< Invalid argument passed (e.g., null pointer) +} cose_status_t; + +/** + * @brief Opaque handle to a validator builder + */ +typedef struct cose_validator_builder_t cose_validator_builder_t; + +/** + * @brief Opaque handle to a validator + */ +typedef struct cose_validator_t cose_validator_t; + +/** + * @brief Opaque handle to a validation result + */ +typedef struct cose_validation_result_t cose_validation_result_t; + +/** + * @brief Get the ABI version of this library + * @return ABI version number (currently 1) + */ +unsigned int cose_ffi_abi_version(void); + +/** + * @brief Get the last error message for the current thread + * + * This function returns a newly-allocated UTF-8 string containing the last error + * message. The caller must free it using cose_string_free(). + * + * @return Newly-allocated error message string, or NULL if no error + */ +char* cose_last_error_message_utf8(void); + +/** + * @brief Clear the last error message for the current thread + */ +void cose_last_error_clear(void); + +/** + * @brief Free a string returned by this library + * @param s String to free (can be NULL) + */ +void cose_string_free(char* s); + +/** + * @brief Create a new validator builder + * @param out Output parameter for the builder handle + * @return COSE_OK on success, error code otherwise + */ +cose_status_t cose_validator_builder_new(cose_validator_builder_t** out); + +/** + * @brief Free a validator builder + * @param builder Builder to free (can be NULL) + */ +void cose_validator_builder_free(cose_validator_builder_t* builder); + +/** + * @brief Build a validator from the builder + * @param builder Builder handle + * @param out Output parameter for the validator handle + * @return COSE_OK on success, error code otherwise + */ +cose_status_t cose_validator_builder_build( + cose_validator_builder_t* builder, + cose_validator_t** out +); + +/** + * @brief Free a validator + * @param validator Validator to free (can be NULL) + */ +void cose_validator_free(cose_validator_t* validator); + +/** + * @brief Validate COSE Sign1 bytes + * + * @param validator Validator handle + * @param cose_bytes COSE Sign1 message bytes + * @param cose_bytes_len Length of cose_bytes + * @param detached_payload Detached payload bytes (NULL if embedded) + * @param detached_payload_len Length of detached_payload (0 if embedded) + * @param out_result Output parameter for the validation result + * @return COSE_OK on success, error code otherwise + */ +cose_status_t cose_validator_validate_bytes( + const cose_validator_t* validator, + const unsigned char* cose_bytes, + size_t cose_bytes_len, + const unsigned char* detached_payload, + size_t detached_payload_len, + cose_validation_result_t** out_result +); + +/** + * @brief Free a validation result + * @param result Result to free (can be NULL) + */ +void cose_validation_result_free(cose_validation_result_t* result); + +/** + * @brief Check if validation was successful + * @param result Validation result handle + * @param out_ok Output parameter for success status + * @return COSE_OK on success, error code otherwise + */ +cose_status_t cose_validation_result_is_success( + const cose_validation_result_t* result, + bool* out_ok +); + +/** + * @brief Get failure message from validation result + * + * Returns NULL if validation succeeded. The caller must free the returned + * string using cose_string_free(). + * + * @param result Validation result handle + * @return Newly-allocated failure message, or NULL if validation succeeded + */ +char* cose_validation_result_failure_message_utf8( + const cose_validation_result_t* result +); + +#ifdef __cplusplus +} +#endif + +#endif // COSE_SIGN1_H diff --git a/native/c/include/cose/cose_trust.h b/native/c/include/cose/cose_trust.h new file mode 100644 index 00000000..fcde51ae --- /dev/null +++ b/native/c/include/cose/cose_trust.h @@ -0,0 +1,448 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#ifndef COSE_TRUST_H +#define COSE_TRUST_H + +/** + * @file cose_trust.h + * @brief C API for trust-plan authoring (bundled compiled trust plans) + */ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Opaque handle for building a trust plan. +typedef struct cose_trust_plan_builder_t cose_trust_plan_builder_t; + +// Opaque handle for building a custom trust policy (minimal fluent surface). +typedef struct cose_trust_policy_builder_t cose_trust_policy_builder_t; + +// Opaque handle for a bundled compiled trust plan. +typedef struct cose_compiled_trust_plan_t cose_compiled_trust_plan_t; + +/** + * @brief Create a trust policy builder bound to the packs currently configured on a validator builder. + * + * This builder starts empty and lets callers express a minimal set of message-scope requirements. + */ +cose_status_t cose_trust_policy_builder_new_from_validator_builder( + const cose_validator_builder_t* builder, + cose_trust_policy_builder_t** out_policy_builder +); + +/** + * @brief Free a trust policy builder. + */ +void cose_trust_policy_builder_free(cose_trust_policy_builder_t* policy_builder); + +/** + * @brief Set the next composition operator to AND. + */ +cose_status_t cose_trust_policy_builder_and(cose_trust_policy_builder_t* policy_builder); + +/** + * @brief Set the next composition operator to OR. + */ +cose_status_t cose_trust_policy_builder_or(cose_trust_policy_builder_t* policy_builder); + +/** + * @brief Require Content-Type to be present and non-empty. + */ +cose_status_t cose_trust_policy_builder_require_content_type_non_empty( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Require Content-Type to equal the provided value. + */ +cose_status_t cose_trust_policy_builder_require_content_type_eq( + cose_trust_policy_builder_t* policy_builder, + const char* content_type_utf8 +); + +/** + * @brief Require a detached payload to be present. + */ +cose_status_t cose_trust_policy_builder_require_detached_payload_present( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Require a detached payload to be absent. + */ +cose_status_t cose_trust_policy_builder_require_detached_payload_absent( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief If a counter-signature verifier produced envelope-integrity evidence, require that it + * indicates the COSE_Sign1 Sig_structure is intact. + * + * If the evidence is missing, this requirement is treated as trusted. + */ +cose_status_t cose_trust_policy_builder_require_counter_signature_envelope_sig_structure_intact_or_missing( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Require CWT claims (header parameter label 15) to be present. + */ +cose_status_t cose_trust_policy_builder_require_cwt_claims_present( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Require CWT claims (header parameter label 15) to be absent. + */ +cose_status_t cose_trust_policy_builder_require_cwt_claims_absent( + cose_trust_policy_builder_t* policy_builder +); + +/** + * @brief Require that CWT `iss` (issuer) equals the provided value. + */ +cose_status_t cose_trust_policy_builder_require_cwt_iss_eq( + cose_trust_policy_builder_t* policy_builder, + const char* iss_utf8 +); + +/** + * @brief Require that CWT `sub` (subject) equals the provided value. + */ +cose_status_t cose_trust_policy_builder_require_cwt_sub_eq( + cose_trust_policy_builder_t* policy_builder, + const char* sub_utf8 +); + +/** + * @brief Require that CWT `aud` (audience) equals the provided value. + */ +cose_status_t cose_trust_policy_builder_require_cwt_aud_eq( + cose_trust_policy_builder_t* policy_builder, + const char* aud_utf8 +); + +/** + * @brief Require that a numeric-label CWT claim is present. + */ +cose_status_t cose_trust_policy_builder_require_cwt_claim_label_present( + cose_trust_policy_builder_t* policy_builder, + int64_t label +); + +/** + * @brief Require that a text-key CWT claim is present. + */ +cose_status_t cose_trust_policy_builder_require_cwt_claim_text_present( + cose_trust_policy_builder_t* policy_builder, + const char* key_utf8 +); + +/** + * @brief Require that a numeric-label CWT claim decodes to an int64 and equals the provided value. + */ +cose_status_t cose_trust_policy_builder_require_cwt_claim_label_i64_eq( + cose_trust_policy_builder_t* policy_builder, + int64_t label, + int64_t value +); + +/** + * @brief Require that a numeric-label CWT claim decodes to a bool and equals the provided value. + */ +cose_status_t cose_trust_policy_builder_require_cwt_claim_label_bool_eq( + cose_trust_policy_builder_t* policy_builder, + int64_t label, + bool value +); + +/** + * @brief Require that a numeric-label CWT claim decodes to an int64 and is >= the provided value. + */ +cose_status_t cose_trust_policy_builder_require_cwt_claim_label_i64_ge( + cose_trust_policy_builder_t* policy_builder, + int64_t label, + int64_t min +); + +/** + * @brief Require that a numeric-label CWT claim decodes to an int64 and is <= the provided value. + */ +cose_status_t cose_trust_policy_builder_require_cwt_claim_label_i64_le( + cose_trust_policy_builder_t* policy_builder, + int64_t label, + int64_t max +); + +/** + * @brief Require that a text-key CWT claim decodes to a string and equals the provided value. + */ +cose_status_t cose_trust_policy_builder_require_cwt_claim_text_str_eq( + cose_trust_policy_builder_t* policy_builder, + const char* key_utf8, + const char* value_utf8 +); + +/** + * @brief Require that a numeric-label CWT claim decodes to a string and equals the provided value. + */ +cose_status_t cose_trust_policy_builder_require_cwt_claim_label_str_eq( + cose_trust_policy_builder_t* policy_builder, + int64_t label, + const char* value_utf8 +); + +/** + * @brief Require that a numeric-label CWT claim decodes to a string and starts with the prefix. + */ +cose_status_t cose_trust_policy_builder_require_cwt_claim_label_str_starts_with( + cose_trust_policy_builder_t* policy_builder, + int64_t label, + const char* prefix_utf8 +); + +/** + * @brief Require that a text-key CWT claim decodes to a string and starts with the prefix. + */ +cose_status_t cose_trust_policy_builder_require_cwt_claim_text_str_starts_with( + cose_trust_policy_builder_t* policy_builder, + const char* key_utf8, + const char* prefix_utf8 +); + +/** + * @brief Require that a numeric-label CWT claim decodes to a string and contains the needle. + */ +cose_status_t cose_trust_policy_builder_require_cwt_claim_label_str_contains( + cose_trust_policy_builder_t* policy_builder, + int64_t label, + const char* needle_utf8 +); + +/** + * @brief Require that a text-key CWT claim decodes to a string and contains the needle. + */ +cose_status_t cose_trust_policy_builder_require_cwt_claim_text_str_contains( + cose_trust_policy_builder_t* policy_builder, + const char* key_utf8, + const char* needle_utf8 +); + +/** + * @brief Require that a text-key CWT claim decodes to a bool and equals the provided value. + */ +cose_status_t cose_trust_policy_builder_require_cwt_claim_text_bool_eq( + cose_trust_policy_builder_t* policy_builder, + const char* key_utf8, + bool value +); + +/** + * @brief Require that a text-key CWT claim decodes to an int64 and is >= the provided value. + */ +cose_status_t cose_trust_policy_builder_require_cwt_claim_text_i64_ge( + cose_trust_policy_builder_t* policy_builder, + const char* key_utf8, + int64_t min +); + +/** + * @brief Require that a text-key CWT claim decodes to an int64 and is <= the provided value. + */ +cose_status_t cose_trust_policy_builder_require_cwt_claim_text_i64_le( + cose_trust_policy_builder_t* policy_builder, + const char* key_utf8, + int64_t max +); + +/** + * @brief Require that a text-key CWT claim decodes to an int64 and equals the provided value. + */ +cose_status_t cose_trust_policy_builder_require_cwt_claim_text_i64_eq( + cose_trust_policy_builder_t* policy_builder, + const char* key_utf8, + int64_t value +); + +/** + * @brief Require that CWT `exp` (expiration time) is >= the provided value. + */ +cose_status_t cose_trust_policy_builder_require_cwt_exp_ge( + cose_trust_policy_builder_t* policy_builder, + int64_t min +); + +/** + * @brief Require that CWT `exp` (expiration time) is <= the provided value. + */ +cose_status_t cose_trust_policy_builder_require_cwt_exp_le( + cose_trust_policy_builder_t* policy_builder, + int64_t max +); + +/** + * @brief Require that CWT `nbf` (not before) is >= the provided value. + */ +cose_status_t cose_trust_policy_builder_require_cwt_nbf_ge( + cose_trust_policy_builder_t* policy_builder, + int64_t min +); + +/** + * @brief Require that CWT `nbf` (not before) is <= the provided value. + */ +cose_status_t cose_trust_policy_builder_require_cwt_nbf_le( + cose_trust_policy_builder_t* policy_builder, + int64_t max +); + +/** + * @brief Require that CWT `iat` (issued at) is >= the provided value. + */ +cose_status_t cose_trust_policy_builder_require_cwt_iat_ge( + cose_trust_policy_builder_t* policy_builder, + int64_t min +); + +/** + * @brief Require that CWT `iat` (issued at) is <= the provided value. + */ +cose_status_t cose_trust_policy_builder_require_cwt_iat_le( + cose_trust_policy_builder_t* policy_builder, + int64_t max +); + +/** + * @brief Compile this policy into a bundled compiled trust plan. + */ +cose_status_t cose_trust_policy_builder_compile( + cose_trust_policy_builder_t* policy_builder, + cose_compiled_trust_plan_t** out_plan +); + +/** + * @brief Create a trust plan builder bound to the packs currently configured on a validator builder. + * + * The pack list is used to (a) discover pack default trust plans and (b) validate that a compiled + * plan can be satisfied by the configured packs. + */ +cose_status_t cose_trust_plan_builder_new_from_validator_builder( + const cose_validator_builder_t* builder, + cose_trust_plan_builder_t** out_plan_builder +); + +/** + * @brief Free a trust plan builder. + */ +void cose_trust_plan_builder_free(cose_trust_plan_builder_t* plan_builder); + +/** + * @brief Select all configured packs' default trust plans. + * + * Packs that do not provide a default plan are ignored. + */ +cose_status_t cose_trust_plan_builder_add_all_pack_default_plans( + cose_trust_plan_builder_t* plan_builder +); + +/** + * @brief Select a specific pack's default trust plan by pack name. + * + * @param pack_name_utf8 Pack name (must match CoseSign1TrustPack::name()) + */ +cose_status_t cose_trust_plan_builder_add_pack_default_plan_by_name( + cose_trust_plan_builder_t* plan_builder, + const char* pack_name_utf8 +); + +/** + * @brief Get the number of configured packs captured on this plan builder. + */ +cose_status_t cose_trust_plan_builder_pack_count( + const cose_trust_plan_builder_t* plan_builder, + size_t* out_count +); + +/** + * @brief Get the pack name at `index`. + * + * Ownership: caller must free via `cose_string_free`. + */ +char* cose_trust_plan_builder_pack_name_utf8( + const cose_trust_plan_builder_t* plan_builder, + size_t index +); + +/** + * @brief Returns whether the pack at `index` provides a default trust plan. + */ +cose_status_t cose_trust_plan_builder_pack_has_default_plan( + const cose_trust_plan_builder_t* plan_builder, + size_t index, + bool* out_has_default +); + +/** + * @brief Clear any selected plans on this builder. + */ +cose_status_t cose_trust_plan_builder_clear_selected_plans( + cose_trust_plan_builder_t* plan_builder +); + +/** + * @brief Compile the selected plans as an OR-composed bundled plan. + */ +cose_status_t cose_trust_plan_builder_compile_or( + cose_trust_plan_builder_t* plan_builder, + cose_compiled_trust_plan_t** out_plan +); + +/** + * @brief Compile the selected plans as an AND-composed bundled plan. + */ +cose_status_t cose_trust_plan_builder_compile_and( + cose_trust_plan_builder_t* plan_builder, + cose_compiled_trust_plan_t** out_plan +); + +/** + * @brief Compile an allow-all bundled plan. + */ +cose_status_t cose_trust_plan_builder_compile_allow_all( + cose_trust_plan_builder_t* plan_builder, + cose_compiled_trust_plan_t** out_plan +); + +/** + * @brief Compile a deny-all bundled plan. + */ +cose_status_t cose_trust_plan_builder_compile_deny_all( + cose_trust_plan_builder_t* plan_builder, + cose_compiled_trust_plan_t** out_plan +); + +/** + * @brief Free a bundled compiled trust plan. + */ +void cose_compiled_trust_plan_free(cose_compiled_trust_plan_t* plan); + +/** + * @brief Attach a bundled compiled trust plan to a validator builder. + * + * Once set, the eventual validator uses the bundled plan rather than OR-composing pack default plans. + */ +cose_status_t cose_validator_builder_with_compiled_trust_plan( + cose_validator_builder_t* builder, + const cose_compiled_trust_plan_t* plan +); + +#ifdef __cplusplus +} +#endif + +#endif // COSE_TRUST_H diff --git a/native/c/tests/CMakeLists.txt b/native/c/tests/CMakeLists.txt new file mode 100644 index 00000000..328d470e --- /dev/null +++ b/native/c/tests/CMakeLists.txt @@ -0,0 +1,128 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + + +# Prefer GoogleTest (via vcpkg) when available; otherwise fall back to the +# custom-runner executables so the repo still builds without extra deps. +find_package(GTest CONFIG QUIET) + +if (GTest_FOUND) + include(GoogleTest) + + function(cose_copy_rust_dlls target_name) + if(NOT WIN32) + return() + endif() + + set(_rust_dlls "") + foreach(_libvar IN ITEMS COSE_FFI_BASE_LIB COSE_FFI_CERTIFICATES_LIB COSE_FFI_MST_LIB COSE_FFI_AKV_LIB COSE_FFI_TRUST_LIB) + if(DEFINED ${_libvar} AND ${_libvar}) + set(_import_lib "${${_libvar}}") + if(_import_lib MATCHES "\\.dll\\.lib$") + string(REPLACE ".dll.lib" ".dll" _dll "${_import_lib}") + list(APPEND _rust_dlls "${_dll}") + endif() + endif() + endforeach() + + list(REMOVE_DUPLICATES _rust_dlls) + foreach(_dll IN LISTS _rust_dlls) + if(EXISTS "${_dll}") + add_custom_command( + TARGET ${target_name} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_dll}" $ + ) + endif() + endforeach() + + # Also copy MSVC runtime + other dynamic deps when available. + # This avoids failures on environments without global VC redistributables. + if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.21") + add_custom_command( + TARGET ${target_name} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ + COMMAND_EXPAND_LISTS + ) + endif() + + # MSVC ASAN uses an additional runtime DLL that is not always present on PATH. + # Copy it next to the executable to avoid 0xc0000135 during gtest discovery. + if(MSVC AND COSE_ENABLE_ASAN) + get_filename_component(_cl_dir "${CMAKE_CXX_COMPILER}" DIRECTORY) + foreach(_asan_name IN ITEMS + clang_rt.asan_dynamic-x86_64.dll + clang_rt.asan_dynamic-i386.dll + clang_rt.asan_dynamic-aarch64.dll + ) + set(_asan_dll "${_cl_dir}/${_asan_name}") + if(EXISTS "${_asan_dll}") + add_custom_command( + TARGET ${target_name} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_asan_dll}" $ + ) + endif() + endforeach() + endif() + endfunction() + + add_executable(smoke_test smoke_test_gtest.cpp) + target_link_libraries(smoke_test PRIVATE cose_sign1 GTest::gtest_main) + cose_copy_rust_dlls(smoke_test) + gtest_discover_tests(smoke_test DISCOVERY_MODE PRE_TEST DISCOVERY_TIMEOUT 30) + + if (COSE_FFI_TRUST_LIB) + add_executable(real_world_trust_plans_test real_world_trust_plans_gtest.cpp) + target_link_libraries(real_world_trust_plans_test PRIVATE cose_sign1 GTest::gtest_main) + cose_copy_rust_dlls(real_world_trust_plans_test) + + get_filename_component(COSE_REPO_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../.." ABSOLUTE) + set(COSE_TESTDATA_V1_DIR "${COSE_REPO_ROOT}/native/rust/cose_sign1_validation_certificates/testdata/v1") + set(COSE_MST_JWKS_PATH "${COSE_REPO_ROOT}/native/rust/cose_sign1_validation_transparent_mst/testdata/esrp-cts-cp.confidential-ledger.azure.com.jwks.json") + + target_compile_definitions(real_world_trust_plans_test PRIVATE + COSE_TESTDATA_V1_DIR="${COSE_TESTDATA_V1_DIR}" + COSE_MST_JWKS_PATH="${COSE_MST_JWKS_PATH}" + ) + + gtest_discover_tests(real_world_trust_plans_test DISCOVERY_MODE PRE_TEST DISCOVERY_TIMEOUT 30) + endif() +else() + # Basic smoke test for C API + add_executable(smoke_test smoke_test.c) + target_link_libraries(smoke_test PRIVATE cose_sign1) + add_test(NAME smoke_test COMMAND smoke_test) + + if (COSE_FFI_TRUST_LIB) + add_executable(real_world_trust_plans_test real_world_trust_plans_test.c) + target_link_libraries(real_world_trust_plans_test PRIVATE cose_sign1) + + get_filename_component(COSE_REPO_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../.." ABSOLUTE) + set(COSE_TESTDATA_V1_DIR "${COSE_REPO_ROOT}/native/rust/cose_sign1_validation_certificates/testdata/v1") + set(COSE_MST_JWKS_PATH "${COSE_REPO_ROOT}/native/rust/cose_sign1_validation_transparent_mst/testdata/esrp-cts-cp.confidential-ledger.azure.com.jwks.json") + + target_compile_definitions(real_world_trust_plans_test PRIVATE + COSE_TESTDATA_V1_DIR="${COSE_TESTDATA_V1_DIR}" + COSE_MST_JWKS_PATH="${COSE_MST_JWKS_PATH}" + ) + + add_test(NAME real_world_trust_plans_test COMMAND real_world_trust_plans_test) + + set(COSE_REAL_WORLD_TEST_NAMES + compile_fails_when_required_pack_missing + compile_succeeds_when_required_pack_present + real_v1_policy_can_gate_on_certificate_facts + real_scitt_policy_can_require_cwt_claims_and_mst_receipt_trusted_from_issuer + real_v1_policy_can_validate_with_mst_only_by_bypassing_primary_signature + ) + + foreach(tname IN LISTS COSE_REAL_WORLD_TEST_NAMES) + add_test( + NAME real_world_trust_plans_test.${tname} + COMMAND real_world_trust_plans_test --test ${tname} + ) + endforeach() + endif() +endif() diff --git a/native/c/tests/real_world_trust_plans_gtest.cpp b/native/c/tests/real_world_trust_plans_gtest.cpp new file mode 100644 index 00000000..28026989 --- /dev/null +++ b/native/c/tests/real_world_trust_plans_gtest.cpp @@ -0,0 +1,367 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include + +extern "C" { +#include +#include + +#ifdef COSE_HAS_CERTIFICATES_PACK +#include +#endif + +#ifdef COSE_HAS_MST_PACK +#include +#endif +} + +#include +#include +#include +#include +#include + +#ifndef COSE_TESTDATA_V1_DIR +#define COSE_TESTDATA_V1_DIR "" +#endif + +#ifndef COSE_MST_JWKS_PATH +#define COSE_MST_JWKS_PATH "" +#endif + +static std::string take_last_error() { + char* err = cose_last_error_message_utf8(); + std::string out = err ? err : "(no error message)"; + if (err) cose_string_free(err); + return out; +} + +static void assert_ok(cose_status_t st, const char* call) { + ASSERT_EQ(st, COSE_OK) << call << ": " << take_last_error(); +} + +static void assert_not_ok(cose_status_t st, const char* call) { + ASSERT_NE(st, COSE_OK) << "expected failure for " << call; +} + +static std::vector read_file_bytes(const std::string& path) { + std::ifstream f(path, std::ios::binary); + if (!f) { + throw std::runtime_error("failed to open file: " + path); + } + + f.seekg(0, std::ios::end); + auto size = f.tellg(); + if (size < 0) { + throw std::runtime_error("failed to stat file: " + path); + } + + f.seekg(0, std::ios::beg); + std::vector out(static_cast(size)); + if (!out.empty()) { + f.read(reinterpret_cast(out.data()), static_cast(out.size())); + if (!f) { + throw std::runtime_error("failed to read file: " + path); + } + } + + return out; +} + +static std::string join_path2(const std::string& a, const std::string& b) { + if (a.empty()) return b; + const char last = a.back(); + if (last == '/' || last == '\\') return a + b; + return a + "/" + b; +} + +TEST(RealWorldTrustPlansC, CoverageHelpers) { + // Cover the "no error" branch. + cose_last_error_clear(); + EXPECT_EQ(take_last_error(), "(no error message)"); + + // Cover join_path2 branches. + EXPECT_EQ(join_path2("", "b"), "b"); + EXPECT_EQ(join_path2("a/", "b"), "a/b"); + EXPECT_EQ(join_path2("a\\", "b"), "a\\b"); + EXPECT_EQ(join_path2("a", "b"), "a/b"); + + // Cover read_file_bytes error path. + EXPECT_THROW((void)read_file_bytes("this_file_should_not_exist_12345.bin"), std::runtime_error); + + // Cover read_file_bytes success path. + const char* temp = std::getenv("TEMP"); + std::string tmp_dir = temp ? temp : "."; + std::string tmp_path = join_path2(tmp_dir, "cose_native_tmp_file.bin"); + { + std::ofstream out(tmp_path, std::ios::binary | std::ios::trunc); + ASSERT_TRUE(out.good()); + const unsigned char bytes[3] = { 1, 2, 3 }; + out.write(reinterpret_cast(bytes), 3); + ASSERT_TRUE(out.good()); + } + + auto got = read_file_bytes(tmp_path); + EXPECT_EQ(got.size(), 3u); + EXPECT_EQ(got[0], 1); + EXPECT_EQ(got[1], 2); + EXPECT_EQ(got[2], 3); + + (void)std::remove(tmp_path.c_str()); +} + +TEST(RealWorldTrustPlansC, CompileFailsWhenRequiredPackMissing) { +#ifndef COSE_HAS_TRUST_PACK + GTEST_SKIP() << "trust pack not available"; +#else +#ifndef COSE_HAS_CERTIFICATES_PACK + GTEST_SKIP() << "COSE_HAS_CERTIFICATES_PACK not enabled"; +#else + cose_validator_builder_t* builder = nullptr; + cose_trust_policy_builder_t* policy = nullptr; + cose_compiled_trust_plan_t* plan = nullptr; + + assert_ok(cose_validator_builder_new(&builder), "cose_validator_builder_new"); + assert_ok( + cose_trust_policy_builder_new_from_validator_builder(builder, &policy), + "cose_trust_policy_builder_new_from_validator_builder"); + + // Certificates pack is linked, but NOT configured on the builder. + // Compiling should fail because no pack will produce the fact. + assert_ok( + cose_certificates_trust_policy_builder_require_x509_chain_trusted(policy), + "cose_certificates_trust_policy_builder_require_x509_chain_trusted"); + + cose_status_t st = cose_trust_policy_builder_compile(policy, &plan); + assert_not_ok(st, "cose_trust_policy_builder_compile"); + + cose_trust_policy_builder_free(policy); + cose_validator_builder_free(builder); +#endif +#endif +} + +TEST(RealWorldTrustPlansC, CompileSucceedsWhenRequiredPackPresent) { +#ifndef COSE_HAS_TRUST_PACK + GTEST_SKIP() << "trust pack not available"; +#else +#ifndef COSE_HAS_CERTIFICATES_PACK + GTEST_SKIP() << "COSE_HAS_CERTIFICATES_PACK not enabled"; +#else + cose_validator_builder_t* builder = nullptr; + cose_trust_policy_builder_t* policy = nullptr; + cose_compiled_trust_plan_t* plan = nullptr; + cose_validator_t* validator = nullptr; + + assert_ok(cose_validator_builder_new(&builder), "cose_validator_builder_new"); + assert_ok(cose_validator_builder_with_certificates_pack(builder), "cose_validator_builder_with_certificates_pack"); + + assert_ok( + cose_trust_policy_builder_new_from_validator_builder(builder, &policy), + "cose_trust_policy_builder_new_from_validator_builder"); + + assert_ok( + cose_certificates_trust_policy_builder_require_x509_chain_trusted(policy), + "cose_certificates_trust_policy_builder_require_x509_chain_trusted"); + + assert_ok(cose_trust_policy_builder_compile(policy, &plan), "cose_trust_policy_builder_compile"); + assert_ok( + cose_validator_builder_with_compiled_trust_plan(builder, plan), + "cose_validator_builder_with_compiled_trust_plan"); + + assert_ok(cose_validator_builder_build(builder, &validator), "cose_validator_builder_build"); + + cose_validator_free(validator); + cose_compiled_trust_plan_free(plan); + cose_trust_policy_builder_free(policy); + cose_validator_builder_free(builder); +#endif +#endif +} + +TEST(RealWorldTrustPlansC, RealV1PolicyCanGateOnCertificateFacts) { +#ifndef COSE_HAS_TRUST_PACK + GTEST_SKIP() << "trust pack not available"; +#else +#ifndef COSE_HAS_CERTIFICATES_PACK + GTEST_SKIP() << "COSE_HAS_CERTIFICATES_PACK not enabled"; +#else + cose_validator_builder_t* builder = nullptr; + cose_trust_policy_builder_t* policy = nullptr; + cose_compiled_trust_plan_t* plan = nullptr; + + assert_ok(cose_validator_builder_new(&builder), "cose_validator_builder_new"); + assert_ok(cose_validator_builder_with_certificates_pack(builder), "cose_validator_builder_with_certificates_pack"); + + assert_ok( + cose_trust_policy_builder_new_from_validator_builder(builder, &policy), + "cose_trust_policy_builder_new_from_validator_builder"); + + assert_ok( + cose_certificates_trust_policy_builder_require_signing_certificate_present(policy), + "cose_certificates_trust_policy_builder_require_signing_certificate_present"); + + assert_ok(cose_trust_policy_builder_and(policy), "cose_trust_policy_builder_and"); + + assert_ok( + cose_certificates_trust_policy_builder_require_not_pqc_algorithm_or_missing(policy), + "cose_certificates_trust_policy_builder_require_not_pqc_algorithm_or_missing"); + + assert_ok(cose_trust_policy_builder_compile(policy, &plan), "cose_trust_policy_builder_compile"); + + cose_compiled_trust_plan_free(plan); + cose_trust_policy_builder_free(policy); + cose_validator_builder_free(builder); +#endif +#endif +} + +TEST(RealWorldTrustPlansC, RealScittPolicyCanRequireCwtClaimsAndMstReceiptTrustedFromIssuer) { +#ifndef COSE_HAS_TRUST_PACK + GTEST_SKIP() << "trust pack not available"; +#else +#ifndef COSE_HAS_MST_PACK + GTEST_SKIP() << "COSE_HAS_MST_PACK not enabled"; +#else + if (std::string(COSE_MST_JWKS_PATH).empty()) { + FAIL() << "COSE_MST_JWKS_PATH not set"; + } + + cose_validator_builder_t* builder = nullptr; + cose_trust_policy_builder_t* policy = nullptr; + cose_compiled_trust_plan_t* plan = nullptr; + + assert_ok(cose_validator_builder_new(&builder), "cose_validator_builder_new"); + + const auto jwks_json = read_file_bytes(COSE_MST_JWKS_PATH); + std::string jwks_str(reinterpret_cast(jwks_json.data()), jwks_json.size()); + + cose_mst_trust_options_t mst_opts; + mst_opts.allow_network = false; + mst_opts.offline_jwks_json = jwks_str.c_str(); + mst_opts.jwks_api_version = nullptr; + + assert_ok( + cose_validator_builder_with_mst_pack_ex(builder, &mst_opts), + "cose_validator_builder_with_mst_pack_ex"); + +#ifdef COSE_HAS_CERTIFICATES_PACK + cose_certificate_trust_options_t cert_opts; + cert_opts.trust_embedded_chain_as_trusted = true; + cert_opts.identity_pinning_enabled = false; + cert_opts.allowed_thumbprints = nullptr; + cert_opts.pqc_algorithm_oids = nullptr; + + assert_ok( + cose_validator_builder_with_certificates_pack_ex(builder, &cert_opts), + "cose_validator_builder_with_certificates_pack_ex"); +#endif + + assert_ok( + cose_trust_policy_builder_new_from_validator_builder(builder, &policy), + "cose_trust_policy_builder_new_from_validator_builder"); + + assert_ok( + cose_trust_policy_builder_require_cwt_claims_present(policy), + "cose_trust_policy_builder_require_cwt_claims_present"); + + assert_ok(cose_trust_policy_builder_and(policy), "cose_trust_policy_builder_and"); + + assert_ok( + cose_mst_trust_policy_builder_require_receipt_trusted_from_issuer_contains( + policy, + "confidential-ledger.azure.com"), + "cose_mst_trust_policy_builder_require_receipt_trusted_from_issuer_contains"); + + assert_ok(cose_trust_policy_builder_compile(policy, &plan), "cose_trust_policy_builder_compile"); + + cose_compiled_trust_plan_free(plan); + cose_trust_policy_builder_free(policy); + cose_validator_builder_free(builder); +#endif +#endif +} + +TEST(RealWorldTrustPlansC, RealV1PolicyCanValidateWithMstOnlyBypassingPrimarySignature) { +#ifndef COSE_HAS_TRUST_PACK + GTEST_SKIP() << "trust pack not available"; +#else +#ifndef COSE_HAS_MST_PACK + GTEST_SKIP() << "COSE_HAS_MST_PACK not enabled"; +#else + if (std::string(COSE_TESTDATA_V1_DIR).empty()) { + FAIL() << "COSE_TESTDATA_V1_DIR not set"; + } + + if (std::string(COSE_MST_JWKS_PATH).empty()) { + FAIL() << "COSE_MST_JWKS_PATH not set"; + } + + cose_validator_builder_t* builder = nullptr; + cose_trust_plan_builder_t* plan_builder = nullptr; + cose_compiled_trust_plan_t* plan = nullptr; + cose_validator_t* validator = nullptr; + cose_validation_result_t* result = nullptr; + + assert_ok(cose_validator_builder_new(&builder), "cose_validator_builder_new"); + + const auto jwks_json = read_file_bytes(COSE_MST_JWKS_PATH); + std::string jwks_str(reinterpret_cast(jwks_json.data()), jwks_json.size()); + + cose_mst_trust_options_t mst_opts; + mst_opts.allow_network = false; + mst_opts.offline_jwks_json = jwks_str.c_str(); + mst_opts.jwks_api_version = nullptr; + + assert_ok( + cose_validator_builder_with_mst_pack_ex(builder, &mst_opts), + "cose_validator_builder_with_mst_pack_ex"); + + assert_ok( + cose_trust_plan_builder_new_from_validator_builder(builder, &plan_builder), + "cose_trust_plan_builder_new_from_validator_builder"); + + assert_ok( + cose_trust_plan_builder_add_all_pack_default_plans(plan_builder), + "cose_trust_plan_builder_add_all_pack_default_plans"); + + assert_ok( + cose_trust_plan_builder_compile_and(plan_builder, &plan), + "cose_trust_plan_builder_compile_and"); + + assert_ok( + cose_validator_builder_with_compiled_trust_plan(builder, plan), + "cose_validator_builder_with_compiled_trust_plan"); + + assert_ok(cose_validator_builder_build(builder, &validator), "cose_validator_builder_build"); + + for (const auto* file : {"2ts-statement.scitt", "1ts-statement.scitt"}) { + const auto path = join_path2(COSE_TESTDATA_V1_DIR, file); + const auto cose_bytes = read_file_bytes(path); + + assert_ok( + cose_validator_validate_bytes( + validator, + cose_bytes.data(), + cose_bytes.size(), + nullptr, + 0, + &result), + "cose_validator_validate_bytes"); + + bool ok = false; + assert_ok(cose_validation_result_is_success(result, &ok), "cose_validation_result_is_success"); + ASSERT_TRUE(ok) << "expected success for " << file; + + cose_validation_result_free(result); + result = nullptr; + } + + cose_validator_free(validator); + cose_compiled_trust_plan_free(plan); + cose_trust_plan_builder_free(plan_builder); + cose_validator_builder_free(builder); +#endif +#endif +} diff --git a/native/c/tests/real_world_trust_plans_test.c b/native/c/tests/real_world_trust_plans_test.c new file mode 100644 index 00000000..2a5337e7 --- /dev/null +++ b/native/c/tests/real_world_trust_plans_test.c @@ -0,0 +1,511 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include +#include + +#ifdef COSE_HAS_CERTIFICATES_PACK +#include +#endif + +#ifdef COSE_HAS_MST_PACK +#include +#endif + +#include +#include +#include +#include +#include + +#ifndef COSE_TESTDATA_V1_DIR +#define COSE_TESTDATA_V1_DIR "" +#endif + +#ifndef COSE_MST_JWKS_PATH +#define COSE_MST_JWKS_PATH "" +#endif + +static void fail(const char* msg) { + fprintf(stderr, "FAIL: %s\n", msg); + exit(1); +} + +static void assert_status_ok(cose_status_t st, const char* call) { + if (st == COSE_OK) return; + + fprintf(stderr, "FAILED: %s\n", call); + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "%s\n", err ? err : "(no error message)"); + if (err) cose_string_free(err); + exit(1); +} + +static void assert_status_not_ok(cose_status_t st, const char* call) { + if (st != COSE_OK) return; + + fprintf(stderr, "EXPECTED FAILURE but got COSE_OK: %s\n", call); + exit(1); +} + +static bool read_file_bytes(const char* path, uint8_t** out_bytes, size_t* out_len) { + *out_bytes = NULL; + *out_len = 0; + + FILE* f = NULL; +#if defined(_MSC_VER) + if (fopen_s(&f, path, "rb") != 0) { + return false; + } +#else + f = fopen(path, "rb"); + if (!f) { + return false; + } +#endif + + if (fseek(f, 0, SEEK_END) != 0) { + fclose(f); + return false; + } + + long size = ftell(f); + if (size < 0) { + fclose(f); + return false; + } + + if (fseek(f, 0, SEEK_SET) != 0) { + fclose(f); + return false; + } + + uint8_t* buf = (uint8_t*)malloc((size_t)size); + if (!buf) { + fclose(f); + return false; + } + + size_t read = fread(buf, 1, (size_t)size, f); + fclose(f); + + if (read != (size_t)size) { + free(buf); + return false; + } + + *out_bytes = buf; + *out_len = (size_t)size; + return true; +} + +static char* join_path2(const char* a, const char* b) { + size_t alen = strlen(a); + size_t blen = strlen(b); + + const bool need_sep = (alen > 0 && a[alen - 1] != '/' && a[alen - 1] != '\\'); + size_t len = alen + (need_sep ? 1 : 0) + blen + 1; + + char* out = (char*)malloc(len); + if (!out) return NULL; + + memcpy(out, a, alen); + size_t pos = alen; + if (need_sep) { + out[pos++] = '/'; + } + memcpy(out + pos, b, blen); + out[pos + blen] = 0; + return out; +} + +static void test_compile_fails_when_required_pack_missing(void) { +#ifndef COSE_HAS_CERTIFICATES_PACK + printf("SKIP: %s (COSE_HAS_CERTIFICATES_PACK not enabled)\n", __func__); + return; +#else + cose_validator_builder_t* builder = NULL; + cose_trust_policy_builder_t* policy = NULL; + cose_compiled_trust_plan_t* plan = NULL; + + assert_status_ok(cose_validator_builder_new(&builder), "cose_validator_builder_new"); + assert_status_ok( + cose_trust_policy_builder_new_from_validator_builder(builder, &policy), + "cose_trust_policy_builder_new_from_validator_builder" + ); + + // Certificates pack is linked, but NOT configured on the builder. + // The require-call succeeds, but compiling should fail because no pack will produce the fact. + assert_status_ok( + cose_certificates_trust_policy_builder_require_x509_chain_trusted(policy), + "cose_certificates_trust_policy_builder_require_x509_chain_trusted" + ); + + cose_status_t st = cose_trust_policy_builder_compile(policy, &plan); + assert_status_not_ok(st, "cose_trust_policy_builder_compile"); + + cose_trust_policy_builder_free(policy); + cose_validator_builder_free(builder); +#endif +} + +static void test_compile_succeeds_when_required_pack_present(void) { +#ifndef COSE_HAS_CERTIFICATES_PACK + printf("SKIP: %s (COSE_HAS_CERTIFICATES_PACK not enabled)\n", __func__); + return; +#else + cose_validator_builder_t* builder = NULL; + cose_trust_policy_builder_t* policy = NULL; + cose_compiled_trust_plan_t* plan = NULL; + cose_validator_t* validator = NULL; + + assert_status_ok(cose_validator_builder_new(&builder), "cose_validator_builder_new"); + assert_status_ok( + cose_validator_builder_with_certificates_pack(builder), + "cose_validator_builder_with_certificates_pack" + ); + + assert_status_ok( + cose_trust_policy_builder_new_from_validator_builder(builder, &policy), + "cose_trust_policy_builder_new_from_validator_builder" + ); + + assert_status_ok( + cose_certificates_trust_policy_builder_require_x509_chain_trusted(policy), + "cose_certificates_trust_policy_builder_require_x509_chain_trusted" + ); + + assert_status_ok( + cose_trust_policy_builder_compile(policy, &plan), + "cose_trust_policy_builder_compile" + ); + + assert_status_ok( + cose_validator_builder_with_compiled_trust_plan(builder, plan), + "cose_validator_builder_with_compiled_trust_plan" + ); + + assert_status_ok( + cose_validator_builder_build(builder, &validator), + "cose_validator_builder_build" + ); + + cose_validator_free(validator); + cose_compiled_trust_plan_free(plan); + cose_trust_policy_builder_free(policy); + cose_validator_builder_free(builder); +#endif +} + +static void test_real_v1_policy_can_gate_on_certificate_facts(void) { +#ifndef COSE_HAS_CERTIFICATES_PACK + printf("SKIP: %s (COSE_HAS_CERTIFICATES_PACK not enabled)\n", __func__); + return; +#else + cose_validator_builder_t* builder = NULL; + cose_trust_policy_builder_t* policy = NULL; + cose_compiled_trust_plan_t* plan = NULL; + + assert_status_ok(cose_validator_builder_new(&builder), "cose_validator_builder_new"); + assert_status_ok( + cose_validator_builder_with_certificates_pack(builder), + "cose_validator_builder_with_certificates_pack" + ); + + assert_status_ok( + cose_trust_policy_builder_new_from_validator_builder(builder, &policy), + "cose_trust_policy_builder_new_from_validator_builder" + ); + + // Roughly matches: require_signing_certificate_present AND require_not_pqc_algorithm_or_missing + assert_status_ok( + cose_certificates_trust_policy_builder_require_signing_certificate_present(policy), + "cose_certificates_trust_policy_builder_require_signing_certificate_present" + ); + assert_status_ok(cose_trust_policy_builder_and(policy), "cose_trust_policy_builder_and"); + assert_status_ok( + cose_certificates_trust_policy_builder_require_not_pqc_algorithm_or_missing(policy), + "cose_certificates_trust_policy_builder_require_not_pqc_algorithm_or_missing" + ); + + assert_status_ok( + cose_trust_policy_builder_compile(policy, &plan), + "cose_trust_policy_builder_compile" + ); + + cose_compiled_trust_plan_free(plan); + cose_trust_policy_builder_free(policy); + cose_validator_builder_free(builder); +#endif +} + +static void test_real_scitt_policy_can_require_cwt_claims_and_mst_receipt_trusted_from_issuer(void) { +#ifndef COSE_HAS_MST_PACK + printf("SKIP: %s (COSE_HAS_MST_PACK not enabled)\n", __func__); + return; +#else + // Build/compile a policy that mirrors the Rust real-world policy shape (using only projected helpers). + // Note: end-to-end validation of the SCITT vectors requires counter-signature-driven primary-signature bypass, + // which is driven by the MST pack default trust plan; see the separate validation test below. + cose_validator_builder_t* builder = NULL; + cose_trust_policy_builder_t* policy = NULL; + cose_compiled_trust_plan_t* plan = NULL; + + uint8_t* jwks_bytes = NULL; + size_t jwks_len = 0; + + assert_status_ok(cose_validator_builder_new(&builder), "cose_validator_builder_new"); + + // MST offline JWKS (deterministic) + if (COSE_MST_JWKS_PATH[0] == 0) { + fail("COSE_MST_JWKS_PATH not set"); + } + if (!read_file_bytes(COSE_MST_JWKS_PATH, &jwks_bytes, &jwks_len)) { + fail("failed to read MST JWKS json"); + } + + // Ensure null-terminated JSON string + char* jwks_json = (char*)malloc(jwks_len + 1); + if (!jwks_json) { + fail("out of memory"); + } + memcpy(jwks_json, jwks_bytes, jwks_len); + jwks_json[jwks_len] = 0; + + cose_mst_trust_options_t mst_opts; + mst_opts.allow_network = false; + mst_opts.offline_jwks_json = jwks_json; + mst_opts.jwks_api_version = NULL; + + assert_status_ok( + cose_validator_builder_with_mst_pack_ex(builder, &mst_opts), + "cose_validator_builder_with_mst_pack_ex" + ); + +#ifdef COSE_HAS_CERTIFICATES_PACK + // Mirror Rust tests: include certificates pack too. + cose_certificate_trust_options_t cert_opts; + cert_opts.trust_embedded_chain_as_trusted = true; + cert_opts.identity_pinning_enabled = false; + cert_opts.allowed_thumbprints = NULL; + cert_opts.pqc_algorithm_oids = NULL; + + assert_status_ok( + cose_validator_builder_with_certificates_pack_ex(builder, &cert_opts), + "cose_validator_builder_with_certificates_pack_ex" + ); +#endif + + assert_status_ok( + cose_trust_policy_builder_new_from_validator_builder(builder, &policy), + "cose_trust_policy_builder_new_from_validator_builder" + ); + + assert_status_ok( + cose_trust_policy_builder_require_cwt_claims_present(policy), + "cose_trust_policy_builder_require_cwt_claims_present" + ); + + assert_status_ok(cose_trust_policy_builder_and(policy), "cose_trust_policy_builder_and"); + assert_status_ok( + cose_mst_trust_policy_builder_require_receipt_trusted_from_issuer_contains( + policy, + "confidential-ledger.azure.com" + ), + "cose_mst_trust_policy_builder_require_receipt_trusted_from_issuer_contains" + ); + + assert_status_ok( + cose_trust_policy_builder_compile(policy, &plan), + "cose_trust_policy_builder_compile" + ); + + cose_compiled_trust_plan_free(plan); + cose_trust_policy_builder_free(policy); + cose_validator_builder_free(builder); + + free(jwks_json); + free(jwks_bytes); +#endif +} + +static void test_real_v1_policy_can_validate_with_mst_only_by_bypassing_primary_signature(void) { +#ifndef COSE_HAS_MST_PACK + printf("SKIP: %s (COSE_HAS_MST_PACK not enabled)\n", __func__); + return; +#else + cose_validator_builder_t* builder = NULL; + cose_trust_plan_builder_t* plan_builder = NULL; + cose_compiled_trust_plan_t* plan = NULL; + cose_validator_t* validator = NULL; + cose_validation_result_t* result = NULL; + + uint8_t* cose_bytes = NULL; + size_t cose_len = 0; + + uint8_t* jwks_bytes = NULL; + size_t jwks_len = 0; + + assert_status_ok(cose_validator_builder_new(&builder), "cose_validator_builder_new"); + + if (!read_file_bytes(COSE_MST_JWKS_PATH, &jwks_bytes, &jwks_len)) { + fail("failed to read MST JWKS json"); + } + + char* jwks_json = (char*)malloc(jwks_len + 1); + if (!jwks_json) { + fail("out of memory"); + } + memcpy(jwks_json, jwks_bytes, jwks_len); + jwks_json[jwks_len] = 0; + + cose_mst_trust_options_t mst_opts; + mst_opts.allow_network = false; + mst_opts.offline_jwks_json = jwks_json; + mst_opts.jwks_api_version = NULL; + + assert_status_ok( + cose_validator_builder_with_mst_pack_ex(builder, &mst_opts), + "cose_validator_builder_with_mst_pack_ex" + ); + + // Use the MST pack default trust plan; this is the native analogue to Rust's TrustPlanBuilder MST-only policy, + // and is expected to enable bypassing unsupported primary signature algorithms when countersignature evidence exists. + assert_status_ok( + cose_trust_plan_builder_new_from_validator_builder(builder, &plan_builder), + "cose_trust_plan_builder_new_from_validator_builder" + ); + assert_status_ok( + cose_trust_plan_builder_add_all_pack_default_plans(plan_builder), + "cose_trust_plan_builder_add_all_pack_default_plans" + ); + assert_status_ok( + cose_trust_plan_builder_compile_and(plan_builder, &plan), + "cose_trust_plan_builder_compile_and" + ); + + assert_status_ok( + cose_validator_builder_with_compiled_trust_plan(builder, plan), + "cose_validator_builder_with_compiled_trust_plan" + ); + assert_status_ok( + cose_validator_builder_build(builder, &validator), + "cose_validator_builder_build" + ); + + // Validate both v1 SCITT vectors. + const char* files[] = {"2ts-statement.scitt", "1ts-statement.scitt"}; + for (size_t i = 0; i < 2; i++) { + char* path = join_path2(COSE_TESTDATA_V1_DIR, files[i]); + if (!path) { + fail("out of memory"); + } + if (!read_file_bytes(path, &cose_bytes, &cose_len)) { + fprintf(stderr, "Failed to read test vector: %s\n", path); + fail("missing test vector"); + } + + assert_status_ok( + cose_validator_validate_bytes(validator, cose_bytes, cose_len, NULL, 0, &result), + "cose_validator_validate_bytes" + ); + + bool ok = false; + assert_status_ok(cose_validation_result_is_success(result, &ok), "cose_validation_result_is_success"); + if (!ok) { + char* msg = cose_validation_result_failure_message_utf8(result); + fprintf(stderr, "expected success but validation failed for %s: %s\n", files[i], msg ? msg : "(no message)"); + if (msg) cose_string_free(msg); + exit(1); + } + + cose_validation_result_free(result); + result = NULL; + free(cose_bytes); + cose_bytes = NULL; + free(path); + } + + cose_validator_free(validator); + cose_compiled_trust_plan_free(plan); + cose_trust_plan_builder_free(plan_builder); + cose_validator_builder_free(builder); + + free(jwks_json); + free(jwks_bytes); +#endif +} + +typedef void (*test_fn_t)(void); + +typedef struct test_case_t { + const char* name; + test_fn_t fn; +} test_case_t; + +static const test_case_t g_tests[] = { + {"compile_fails_when_required_pack_missing", test_compile_fails_when_required_pack_missing}, + {"compile_succeeds_when_required_pack_present", test_compile_succeeds_when_required_pack_present}, + {"real_v1_policy_can_gate_on_certificate_facts", test_real_v1_policy_can_gate_on_certificate_facts}, + {"real_scitt_policy_can_require_cwt_claims_and_mst_receipt_trusted_from_issuer", test_real_scitt_policy_can_require_cwt_claims_and_mst_receipt_trusted_from_issuer}, + {"real_v1_policy_can_validate_with_mst_only_by_bypassing_primary_signature", test_real_v1_policy_can_validate_with_mst_only_by_bypassing_primary_signature}, +}; + +static void usage(const char* argv0) { + fprintf(stderr, + "Usage:\n" + " %s [--list] [--test ]\n", + argv0); +} + +static void list_tests(void) { + for (size_t i = 0; i < (sizeof(g_tests) / sizeof(g_tests[0])); i++) { + printf("%s\n", g_tests[i].name); + } +} + +static int run_one(const char* name) { + for (size_t i = 0; i < (sizeof(g_tests) / sizeof(g_tests[0])); i++) { + if (strcmp(g_tests[i].name, name) == 0) { + printf("RUN: %s\n", g_tests[i].name); + g_tests[i].fn(); + printf("PASS: %s\n", g_tests[i].name); + return 0; + } + } + fprintf(stderr, "Unknown test: %s\n", name); + return 2; +} + +int main(int argc, char** argv) { +#ifndef COSE_HAS_TRUST_PACK + // If trust pack isn't present, this test target should ideally be skipped at build time, + // but keep a safe runtime no-op. + printf("Skipping: trust pack not available\n"); + return 0; +#else + if (argc == 2 && strcmp(argv[1], "--list") == 0) { + list_tests(); + return 0; + } + + if (argc == 3 && strcmp(argv[1], "--test") == 0) { + return run_one(argv[2]); + } + + if (argc != 1) { + usage(argv[0]); + return 2; + } + + for (size_t i = 0; i < (sizeof(g_tests) / sizeof(g_tests[0])); i++) { + int rc = run_one(g_tests[i].name); + if (rc != 0) { + return rc; + } + } + + printf("OK\n"); + return 0; +#endif +} diff --git a/native/c/tests/smoke_test.c b/native/c/tests/smoke_test.c new file mode 100644 index 00000000..c223316f --- /dev/null +++ b/native/c/tests/smoke_test.c @@ -0,0 +1,934 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include +#include +#include +#include +#include +#include +#include + +int main(void) { + printf("COSE C API Smoke Test\n"); + printf("ABI Version: %u\n", cose_ffi_abi_version()); + + // Create builder + cose_validator_builder_t* builder = NULL; + cose_status_t status = cose_validator_builder_new(&builder); + if (status != COSE_OK) { + fprintf(stderr, "Failed to create builder: %d\n", status); + char* err = cose_last_error_message_utf8(); + if (err) { + fprintf(stderr, "Error: %s\n", err); + cose_string_free(err); + } + return 1; + } + printf("✓ Builder created\n"); + +#ifdef COSE_HAS_CERTIFICATES_PACK + // Add certificates pack + status = cose_validator_builder_with_certificates_pack(builder); + if (status != COSE_OK) { + fprintf(stderr, "Failed to add certificates pack: %d\n", status); + char* err = cose_last_error_message_utf8(); + if (err) { + fprintf(stderr, "Error: %s\n", err); + cose_string_free(err); + } + cose_validator_builder_free(builder); + return 1; + } + printf("✓ Certificates pack added\n"); +#endif + +#ifdef COSE_HAS_MST_PACK + // Add MST pack (so MST receipt facts can be produced during validation) + status = cose_validator_builder_with_mst_pack(builder); + if (status != COSE_OK) { + fprintf(stderr, "Failed to add MST pack: %d\n", status); + char* err = cose_last_error_message_utf8(); + if (err) { + fprintf(stderr, "Error: %s\n", err); + cose_string_free(err); + } + cose_validator_builder_free(builder); + return 1; + } + printf("✓ MST pack added\n"); +#endif + +#ifdef COSE_HAS_AKV_PACK + // Add AKV pack (so AKV facts can be produced during validation) + status = cose_validator_builder_with_akv_pack(builder); + if (status != COSE_OK) { + fprintf(stderr, "Failed to add AKV pack: %d\n", status); + char* err = cose_last_error_message_utf8(); + if (err) { + fprintf(stderr, "Error: %s\n", err); + cose_string_free(err); + } + cose_validator_builder_free(builder); + return 1; + } + printf("✓ AKV pack added\n"); +#endif +#ifdef COSE_HAS_TRUST_PACK + // Trust-plan authoring: build a bundled plan from pack defaults and attach it. + { + cose_trust_plan_builder_t* plan_builder = NULL; + status = cose_trust_plan_builder_new_from_validator_builder(builder, &plan_builder); + if (status != COSE_OK || !plan_builder) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to create trust plan builder: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_validator_builder_free(builder); + return 1; + } + + // Pack enumeration helpers (for diagnostics / UI use-cases). + { + size_t pack_count = 0; + status = cose_trust_plan_builder_pack_count(plan_builder, &pack_count); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to get pack count: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_plan_builder_free(plan_builder); + cose_validator_builder_free(builder); + return 1; + } + + for (size_t i = 0; i < pack_count; i++) { + char* name = cose_trust_plan_builder_pack_name_utf8(plan_builder, i); + if (!name) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to get pack name: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_plan_builder_free(plan_builder); + cose_validator_builder_free(builder); + return 1; + } + + bool has_default = false; + status = cose_trust_plan_builder_pack_has_default_plan(plan_builder, i, &has_default); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to query pack default plan: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_string_free(name); + cose_trust_plan_builder_free(plan_builder); + cose_validator_builder_free(builder); + return 1; + } + + printf(" - Pack[%zu] %s (default plan: %s)\n", i, name, has_default ? "yes" : "no"); + cose_string_free(name); + } + } + + status = cose_trust_plan_builder_add_all_pack_default_plans(plan_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add default plans: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_plan_builder_free(plan_builder); + cose_validator_builder_free(builder); + return 1; + } + + cose_compiled_trust_plan_t* plan = NULL; + status = cose_trust_plan_builder_compile_or(plan_builder, &plan); + cose_trust_plan_builder_free(plan_builder); + if (status != COSE_OK || !plan) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to compile trust plan: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_validator_builder_with_compiled_trust_plan(builder, plan); + cose_compiled_trust_plan_free(plan); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to attach trust plan: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_validator_builder_free(builder); + return 1; + } + + printf("✓ Compiled trust plan attached\n"); + } + + // Trust-policy authoring: compile a small custom policy and attach it (overrides prior plan). + { + cose_trust_policy_builder_t* policy_builder = NULL; + status = cose_trust_policy_builder_new_from_validator_builder(builder, &policy_builder); + if (status != COSE_OK || !policy_builder) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to create trust policy builder: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_trust_policy_builder_require_detached_payload_absent(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add policy rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + +#ifdef COSE_HAS_CERTIFICATES_PACK + // Pack-specific trust-policy helpers (certificates / X.509 predicates) + status = cose_certificates_trust_policy_builder_require_x509_chain_trusted(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add x509-chain-trusted rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_x509_chain_built(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add x509-chain-built rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_x509_chain_element_count_eq(policy_builder, 1); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add x509-chain-element-count rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_x509_chain_status_flags_eq(policy_builder, 0); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add x509-chain-status-flags rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_leaf_chain_thumbprint_present(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add leaf-thumbprint-present rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_signing_certificate_present(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add signing-cert-present rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_leaf_subject_eq(policy_builder, "CN=example"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add leaf-subject-eq rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_issuer_subject_eq(policy_builder, "CN=issuer.example"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add issuer-subject-eq rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_signing_certificate_subject_issuer_matches_leaf_chain_element(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add signing-cert-matches-leaf rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_leaf_issuer_is_next_chain_subject_optional(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add issuer-chaining-optional rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_signing_certificate_thumbprint_eq(policy_builder, "ABCD1234"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add signing-cert-thumbprint-eq rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_signing_certificate_thumbprint_present(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add signing-cert-thumbprint-present rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_signing_certificate_subject_eq(policy_builder, "CN=example"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add signing-cert-subject-eq rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_signing_certificate_issuer_eq(policy_builder, "CN=issuer.example"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add signing-cert-issuer-eq rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_signing_certificate_serial_number_eq(policy_builder, "01"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add signing-cert-serial-number-eq rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_signing_certificate_expired_at_or_before(policy_builder, 0); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add signing-cert-expired rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_signing_certificate_valid_at(policy_builder, (int64_t)0); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add signing-cert-valid-at rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_signing_certificate_not_before_le(policy_builder, (int64_t)0); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add signing-cert-not-before-le rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_signing_certificate_not_before_ge(policy_builder, (int64_t)0); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add signing-cert-not-before-ge rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_signing_certificate_not_after_le(policy_builder, (int64_t)0); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add signing-cert-not-after-le rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_signing_certificate_not_after_ge(policy_builder, (int64_t)0); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add signing-cert-not-after-ge rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_chain_element_subject_eq(policy_builder, (size_t)0, "CN=example"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add chain-element[0]-subject-eq rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_chain_element_issuer_eq(policy_builder, (size_t)0, "CN=issuer.example"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add chain-element[0]-issuer-eq rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_chain_element_thumbprint_present(policy_builder, (size_t)0); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add chain-element[0]-thumbprint-present rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_chain_element_thumbprint_eq(policy_builder, (size_t)0, "ABCD1234"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add chain-element[0]-thumbprint-eq rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_chain_element_valid_at(policy_builder, (size_t)0, (int64_t)0); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add chain-element[0]-valid-at rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_chain_element_not_before_le(policy_builder, (size_t)0, (int64_t)0); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add chain-element[0]-not-before-le rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_chain_element_not_before_ge(policy_builder, (size_t)0, (int64_t)0); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add chain-element[0]-not-before-ge rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_chain_element_not_after_le(policy_builder, (size_t)0, (int64_t)0); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add chain-element[0]-not-after-le rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_chain_element_not_after_ge(policy_builder, (size_t)0, (int64_t)0); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add chain-element[0]-not-after-ge rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_not_pqc_algorithm_or_missing(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add not-pqc-or-missing rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_x509_public_key_algorithm_thumbprint_eq(policy_builder, "ABCD1234"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add x509-public-key-algorithm-thumbprint-eq rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_x509_public_key_algorithm_oid_eq(policy_builder, "1.2.840.113549.1.1.1"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add x509-public-key-algorithm-oid-eq rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_certificates_trust_policy_builder_require_x509_public_key_algorithm_is_not_pqc(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add x509-public-key-algorithm-not-pqc rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } +#endif + +#ifdef COSE_HAS_MST_PACK + // Pack-specific trust-policy helpers (MST receipt predicates) + status = cose_mst_trust_policy_builder_require_receipt_present(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add MST receipt-present rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_mst_trust_policy_builder_require_receipt_not_present(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add MST receipt-not-present rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_mst_trust_policy_builder_require_receipt_signature_verified(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add MST receipt-signature-verified rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_mst_trust_policy_builder_require_receipt_signature_not_verified(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add MST receipt-signature-not-verified rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_mst_trust_policy_builder_require_receipt_issuer_contains(policy_builder, "microsoft"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add MST receipt-issuer-contains rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_mst_trust_policy_builder_require_receipt_issuer_eq(policy_builder, "issuer.example"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add MST receipt-issuer-eq rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_mst_trust_policy_builder_require_receipt_kid_eq(policy_builder, "kid.example"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add MST receipt-kid-eq rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_mst_trust_policy_builder_require_receipt_kid_contains(policy_builder, "kid"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add MST receipt-kid-contains rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_mst_trust_policy_builder_require_receipt_trusted(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add MST receipt-trusted rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_mst_trust_policy_builder_require_receipt_not_trusted(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add MST receipt-not-trusted rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_mst_trust_policy_builder_require_receipt_trusted_from_issuer_contains(policy_builder, "microsoft"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add MST receipt-trusted-from-issuer-contains rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_mst_trust_policy_builder_require_receipt_statement_sha256_eq( + policy_builder, + "0000000000000000000000000000000000000000000000000000000000000000"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add MST receipt-statement-sha256-eq rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_mst_trust_policy_builder_require_receipt_statement_coverage_eq(policy_builder, "coverage.example"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add MST receipt-statement-coverage-eq rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_mst_trust_policy_builder_require_receipt_statement_coverage_contains(policy_builder, "example"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add MST receipt-statement-coverage-contains rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } +#endif + + status = cose_trust_policy_builder_require_cwt_claims_present(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add CWT claims-present rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_trust_policy_builder_require_cwt_iss_eq(policy_builder, "issuer.example"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add CWT iss-eq rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_trust_policy_builder_require_cwt_claim_label_present(policy_builder, (int64_t)6); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add CWT claim label-present rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_trust_policy_builder_require_cwt_claim_label_i64_ge(policy_builder, (int64_t)6, (int64_t)123); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add CWT claim label i64-ge rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_trust_policy_builder_require_cwt_claim_label_bool_eq(policy_builder, (int64_t)6, true); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add CWT claim label bool-eq rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_trust_policy_builder_require_cwt_claim_text_str_eq(policy_builder, "nonce", "abc"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add CWT claim text str-eq rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_trust_policy_builder_require_cwt_claim_text_str_starts_with(policy_builder, "nonce", "a"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add CWT claim text starts-with rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_trust_policy_builder_require_cwt_claim_text_str_contains(policy_builder, "nonce", "b"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add CWT claim text contains rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + +#ifdef COSE_HAS_AKV_PACK + // Pack-specific policy helpers (AKV) + status = cose_akv_trust_policy_builder_require_azure_key_vault_kid(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add AKV kid-detected rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_akv_trust_policy_builder_require_not_azure_key_vault_kid(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add AKV kid-not-detected rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_akv_trust_policy_builder_require_azure_key_vault_kid_allowed(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add AKV kid-allowed rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_akv_trust_policy_builder_require_azure_key_vault_kid_not_allowed(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add AKV kid-not-allowed rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } +#endif + + status = cose_trust_policy_builder_require_cwt_claim_label_str_starts_with(policy_builder, (int64_t)1000, "a"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add CWT claim label starts-with rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_trust_policy_builder_require_cwt_claim_label_str_contains(policy_builder, (int64_t)1000, "b"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add CWT claim label contains rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_trust_policy_builder_require_cwt_claim_label_str_eq(policy_builder, (int64_t)1000, "exact.example"); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add CWT claim label str-eq rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_trust_policy_builder_require_cwt_claim_text_i64_le(policy_builder, "nonce", (int64_t)0); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add CWT claim text i64-le rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_trust_policy_builder_require_cwt_claim_text_i64_eq(policy_builder, "nonce", (int64_t)0); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add CWT claim text i64-eq rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_trust_policy_builder_require_cwt_claim_text_bool_eq(policy_builder, "nonce", true); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add CWT claim text bool-eq rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_trust_policy_builder_require_cwt_exp_ge(policy_builder, 0); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add CWT exp-ge rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_trust_policy_builder_require_cwt_iat_le(policy_builder, 0); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add CWT iat-le rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_trust_policy_builder_require_counter_signature_envelope_sig_structure_intact_or_missing(policy_builder); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to add counter-signature envelope-integrity rule: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_trust_policy_builder_free(policy_builder); + cose_validator_builder_free(builder); + return 1; + } + + cose_compiled_trust_plan_t* plan = NULL; + status = cose_trust_policy_builder_compile(policy_builder, &plan); + cose_trust_policy_builder_free(policy_builder); + if (status != COSE_OK || !plan) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to compile trust policy: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_validator_builder_free(builder); + return 1; + } + + status = cose_validator_builder_with_compiled_trust_plan(builder, plan); + cose_compiled_trust_plan_free(plan); + if (status != COSE_OK) { + char* err = cose_last_error_message_utf8(); + fprintf(stderr, "Failed to attach trust policy: %s\n", err ? err : "(no error)"); + if (err) cose_string_free(err); + cose_validator_builder_free(builder); + return 1; + } + + printf("✓ Custom trust policy compiled and attached\n"); + } +#endif + + // Build validator + cose_validator_t* validator = NULL; + status = cose_validator_builder_build(builder, &validator); + if (status != COSE_OK) { + fprintf(stderr, "Failed to build validator: %d\n", status); + char* err = cose_last_error_message_utf8(); + if (err) { + fprintf(stderr, "Error: %s\n", err); + cose_string_free(err); + } + cose_validator_builder_free(builder); + return 1; + } + printf("✓ Validator built\n"); + + // Cleanup + cose_validator_free(validator); + cose_validator_builder_free(builder); + + printf("\n✅ All smoke tests passed\n"); + return 0; +} diff --git a/native/c/tests/smoke_test_gtest.cpp b/native/c/tests/smoke_test_gtest.cpp new file mode 100644 index 00000000..373b9813 --- /dev/null +++ b/native/c/tests/smoke_test_gtest.cpp @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include + +extern "C" { +#include +#include +#include +#include +#include +} + +#include + +static std::string take_last_error() { + char* err = cose_last_error_message_utf8(); + std::string out = err ? err : "(no error message)"; + if (err) cose_string_free(err); + return out; +} + +static void assert_ok(cose_status_t st, const char* call) { + ASSERT_EQ(st, COSE_OK) << call << ": " << take_last_error(); +} + +TEST(SmokeC, TakeLastErrorReturnsString) { + // Ensure the helper itself is covered even when assertions pass. + const auto s = take_last_error(); + EXPECT_FALSE(s.empty()); +} + +TEST(SmokeC, AbiVersionAvailable) { + EXPECT_GT(cose_ffi_abi_version(), 0u); +} + +TEST(SmokeC, BuilderCreatesAndBuilds) { + cose_validator_builder_t* builder = nullptr; + cose_validator_t* validator = nullptr; + + assert_ok(cose_validator_builder_new(&builder), "cose_validator_builder_new"); + +#ifdef COSE_HAS_CERTIFICATES_PACK + assert_ok(cose_validator_builder_with_certificates_pack(builder), "cose_validator_builder_with_certificates_pack"); +#endif + +#ifdef COSE_HAS_MST_PACK + assert_ok(cose_validator_builder_with_mst_pack(builder), "cose_validator_builder_with_mst_pack"); +#endif + +#ifdef COSE_HAS_AKV_PACK + assert_ok(cose_validator_builder_with_akv_pack(builder), "cose_validator_builder_with_akv_pack"); +#endif + +#ifdef COSE_HAS_TRUST_PACK + // Attach a bundled plan from pack defaults. + { + cose_trust_plan_builder_t* plan_builder = nullptr; + cose_compiled_trust_plan_t* plan = nullptr; + + assert_ok( + cose_trust_plan_builder_new_from_validator_builder(builder, &plan_builder), + "cose_trust_plan_builder_new_from_validator_builder"); + + assert_ok( + cose_trust_plan_builder_add_all_pack_default_plans(plan_builder), + "cose_trust_plan_builder_add_all_pack_default_plans"); + + assert_ok(cose_trust_plan_builder_compile_or(plan_builder, &plan), "cose_trust_plan_builder_compile_or"); + assert_ok( + cose_validator_builder_with_compiled_trust_plan(builder, plan), + "cose_validator_builder_with_compiled_trust_plan"); + + cose_compiled_trust_plan_free(plan); + cose_trust_plan_builder_free(plan_builder); + } +#endif + + assert_ok(cose_validator_builder_build(builder, &validator), "cose_validator_builder_build"); + + cose_validator_free(validator); + cose_validator_builder_free(builder); +} diff --git a/native/c_pp/CMakeLists.txt b/native/c_pp/CMakeLists.txt new file mode 100644 index 00000000..19de6d72 --- /dev/null +++ b/native/c_pp/CMakeLists.txt @@ -0,0 +1,152 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cmake_minimum_required(VERSION 3.20) + +project(cose_sign1_cpp + VERSION 0.1.0 + DESCRIPTION "C++ projection for COSE Sign1 validation" + LANGUAGES CXX +) + +# Standard CMake testing option (BUILD_TESTING) + CTest integration. +include(CTest) + +# C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +option(COSE_ENABLE_ASAN "Enable AddressSanitizer for native builds" OFF) + +if(COSE_ENABLE_ASAN) + if(MSVC) + add_compile_options(/fsanitize=address) + if(CMAKE_VERSION VERSION_LESS "3.21") + message(WARNING "COSE_ENABLE_ASAN is ON. On Windows, CMake 3.21+ is recommended so post-build steps can copy runtime DLL dependencies.") + endif() + elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + add_compile_options(-fsanitize=address -fno-omit-frame-pointer) + add_link_options(-fsanitize=address) + endif() +endif() + +# Find the C projection (headers and libraries) +set(C_PROJECTION_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../c") +set(RUST_FFI_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../rust/target/release") + +# Find C headers +if(NOT EXISTS "${C_PROJECTION_DIR}/include") + message(FATAL_ERROR "C projection headers not found at ${C_PROJECTION_DIR}/include") +endif() + +# Find Rust FFI libraries +find_library(COSE_FFI_BASE_LIB + NAMES cose_sign1_validation_ffi + PATHS ${RUST_FFI_DIR} + REQUIRED +) + +# Pack FFI libraries (optional) +find_library(COSE_FFI_CERTIFICATES_LIB + NAMES cose_sign1_validation_ffi_certificates + PATHS ${RUST_FFI_DIR} +) + +find_library(COSE_FFI_MST_LIB + NAMES cose_sign1_validation_ffi_mst + PATHS ${RUST_FFI_DIR} +) + +find_library(COSE_FFI_AKV_LIB + NAMES cose_sign1_validation_ffi_akv + PATHS ${RUST_FFI_DIR} +) + +find_library(COSE_FFI_TRUST_LIB + NAMES cose_sign1_validation_ffi_trust + PATHS ${RUST_FFI_DIR} +) + +# Create interface library for C++ headers +add_library(cose_cpp_headers INTERFACE) +target_include_directories(cose_cpp_headers INTERFACE + $ + $ + $ +) + +# Main C++ library - header-only wrappers around C API +add_library(cose_sign1_cpp INTERFACE) +target_link_libraries(cose_sign1_cpp INTERFACE + cose_cpp_headers + ${COSE_FFI_BASE_LIB} +) + +# Link standard system libraries required by Rust +if(WIN32) + target_link_libraries(cose_sign1_cpp INTERFACE + ws2_32 + advapi32 + userenv + bcrypt + ntdll + ) +elseif(UNIX) + target_link_libraries(cose_sign1_cpp INTERFACE + pthread + dl + m + ) +endif() + +# Optional pack libraries +if(COSE_FFI_CERTIFICATES_LIB) + message(STATUS "Found certificates pack: ${COSE_FFI_CERTIFICATES_LIB}") + target_link_libraries(cose_sign1_cpp INTERFACE ${COSE_FFI_CERTIFICATES_LIB}) + target_compile_definitions(cose_sign1_cpp INTERFACE COSE_HAS_CERTIFICATES_PACK) +endif() + +if(COSE_FFI_MST_LIB) + message(STATUS "Found MST pack: ${COSE_FFI_MST_LIB}") + target_link_libraries(cose_sign1_cpp INTERFACE ${COSE_FFI_MST_LIB}) + target_compile_definitions(cose_sign1_cpp INTERFACE COSE_HAS_MST_PACK) +endif() + +if(COSE_FFI_AKV_LIB) + message(STATUS "Found AKV pack: ${COSE_FFI_AKV_LIB}") + target_link_libraries(cose_sign1_cpp INTERFACE ${COSE_FFI_AKV_LIB}) + target_compile_definitions(cose_sign1_cpp INTERFACE COSE_HAS_AKV_PACK) +endif() + +if(COSE_FFI_TRUST_LIB) + message(STATUS "Found trust pack: ${COSE_FFI_TRUST_LIB}") + target_link_libraries(cose_sign1_cpp INTERFACE ${COSE_FFI_TRUST_LIB}) + target_compile_definitions(cose_sign1_cpp INTERFACE COSE_HAS_TRUST_PACK) +endif() + +# Enable testing +if(BUILD_TESTING) + add_subdirectory(tests) +endif() + +add_subdirectory(examples) + +# Installation rules +install(DIRECTORY include/cose + DESTINATION include + FILES_MATCHING PATTERN "*.hpp" +) + +install(TARGETS cose_sign1_cpp cose_cpp_headers + EXPORT cose_sign1_cpp_targets + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib + RUNTIME DESTINATION bin + INCLUDES DESTINATION include +) + +install(EXPORT cose_sign1_cpp_targets + FILE cose_sign1_cpp-targets.cmake + NAMESPACE cose:: + DESTINATION lib/cmake/cose_sign1_cpp +) diff --git a/native/c_pp/README.md b/native/c_pp/README.md new file mode 100644 index 00000000..43ba48fc --- /dev/null +++ b/native/c_pp/README.md @@ -0,0 +1,279 @@ +# COSE Sign1 C++ API + +Modern C++ (C++17) projection for the COSE Sign1 validation library with RAII wrappers and fluent builder pattern. + +## Prerequisites + +- CMake 3.20 or later +- C++17-capable compiler (MSVC 2017+, GCC 7+, Clang 5+) +- Rust toolchain (to build the underlying FFI libraries) + +## Building + +### 1. Build the Rust FFI libraries + +```bash +cd ../rust +cargo build --release --workspace +``` + +### 2. Configure and build the C++ projection + +```bash +mkdir build +cd build +cmake .. -DBUILD_TESTING=ON +cmake --build . --config Release +``` + +### 3. Run tests + +```bash +ctest -C Release +``` + +## Coverage (Windows) + +Coverage for the C++ projection is collected with OpenCppCoverage. + +```powershell +./collect-coverage.ps1 -Configuration Debug -MinimumLineCoveragePercent 95 +``` + +Note: on Windows, `Debug` tends to produce the most reliable line-coverage measurement under OpenCppCoverage (especially when ASAN is enabled). + +Outputs HTML to [native/c_pp/coverage/index.html](coverage/index.html). + +## Usage Example + +## Compilable example programs + +This repo ships a real, buildable C++ example you can use as a starting point: + +- [native/c_pp/examples/trust_policy_example.cpp](examples/trust_policy_example.cpp) + +Build it (after building the Rust FFI libs): + +```bash +cd native/c_pp +cmake -S . -B build -DBUILD_TESTING=ON +cmake --build build --config Release --target cose_trust_policy_example_cpp +``` + +Run it: + +```bash +native/c_pp/build/examples/Release/cose_trust_policy_example_cpp.exe path/to/message.cose [path/to/detached_payload.bin] +``` + +### Basic validation with certificates pack + +```cpp +#include + +int main() { + try { + // Build validator with certificates pack + auto validator = cose::ValidatorBuilderWithCertificates() + .WithCertificates() + .Build(); + + // Validate COSE Sign1 message + std::vector cose_bytes = /* ... */; + auto result = validator.Validate(cose_bytes); + + if (result.Ok()) { + std::cout << "✓ Validation successful\n"; + } else { + std::cout << "✗ Validation failed: " + << result.FailureMessage() << "\n"; + } + + } catch (const cose::cose_error& e) { + std::cerr << "Error: " << e.what() << "\n"; + return 1; + } + + return 0; +} +``` + +### Detailed end-to-end example (custom trust policy + feedback) + +This example shows how to author a custom trust policy (message-scope + pack-specific rules), compile it into a bundled plan, attach it to the validator builder, and then validate bytes with a user-friendly failure message. + +```cpp +#include +#include + +#include +#include +#include + +int main() { + try { + // 1) Configure builder + packs you intend to rely on + cose::ValidatorBuilderWithCertificates builder; + builder.WithCertificates(); + + // 2) Build a custom trust policy bound to the builder's configured packs + cose::TrustPolicyBuilder policy(builder); + + // Message-scope requirements + policy + .RequireContentTypeNonEmpty() + .And() + .RequireDetachedPayloadAbsent() + .And() + .RequireCwtClaimsPresent(); + + // Pack-specific trust-policy helpers (certificates pack) + cose::RequireX509ChainTrusted(policy); + cose::RequireSigningCertificatePresent(policy); + cose::RequireSigningCertificateThumbprintPresent(policy); + + // 3) Compile policy into a bundled plan and attach it + auto plan = policy.Compile(); + cose::WithCompiledTrustPlan(builder, plan); + + // 4) Build validator + auto validator = builder.Build(); + + // 5) Validate bytes + std::vector cose_bytes = /* ... */; + if (cose_bytes.empty()) { + std::cerr << "Provide COSE_Sign1 bytes before calling Validate().\n"; + return 1; + } + + auto result = validator.Validate(cose_bytes); + if (result.Ok()) { + std::cout << "Validation successful\n"; + return 0; + } + + std::cout << "Validation failed: " << result.FailureMessage() << "\n"; + return 2; + } catch (const cose::cose_error& e) { + std::cerr << "Error: " << e.what() << "\n"; + return 3; + } +} +``` + +### Using custom options + +```cpp +#include + +// Certificate options +cose::CertificateOptions cert_opts; +cert_opts.trust_embedded_chain_as_trusted = true; +cert_opts.identity_pinning_enabled = true; +cert_opts.allowed_thumbprints = { + "ABCD1234...", + "5678EFGH..." +}; + +auto validator = cose::ValidatorBuilderWithCertificates() + .WithCertificates(cert_opts) + .Build(); +``` + +### Multiple packs (requires separate includes) + +```cpp +// Note: This requires a more complex inheritance structure +// For now, use individual pack builder classes +// Future: implement a unified builder that composes all packs +``` + +## Per-Pack Headers + +The C++ projection follows the per-pack modular design: + +- `` - Base validator and builder (required) +- `` - X.509 certificate pack wrappers +- `` - MST receipt verification pack wrappers +- `` - Azure Key Vault KID validation pack wrappers +- `` - Convenience header (includes all available packs) + +Include only the headers you need. Each pack header provides: +- An options struct (e.g., `CertificateOptions`) +- A builder extension class (e.g., `ValidatorBuilderWithCertificates`) +- Pack-specific methods (e.g., `.WithCertificates()`) + +## Pack Options + +### Certificates Pack + +```cpp +cose::CertificateOptions opts; +opts.trust_embedded_chain_as_trusted = true; // For testing/pinned roots +opts.identity_pinning_enabled = true; +opts.allowed_thumbprints = {"ABCD...", "1234..."}; +opts.pqc_algorithm_oids = {"1.2.3.4.5"}; + +builder.WithCertificates(opts); +``` + +### MST Pack + +```cpp +cose::MstOptions opts; +opts.allow_network = false; +opts.offline_jwks_json = R"({"keys":[...]})"; +opts.jwks_api_version = "2024-01-01"; + +builder.WithMst(opts); +``` + +### Azure Key Vault Pack + +```cpp +cose::AzureKeyVaultOptions opts; +opts.require_azure_key_vault_kid = true; +opts.allowed_kid_patterns = { + "https://*.vault.azure.net/keys/*", + "https://*.managedhsm.azure.net/keys/*" +}; + +builder.WithAzureKeyVault(opts); +``` + +## RAII and Exception Safety + +All C++ wrappers use RAII for automatic resource management: +- No manual `free()` calls needed +- Resources cleaned up automatically via destructors +- Move semantics supported for efficient transfers +- Copy constructors deleted (move-only types) + +Errors are reported via `cose::cose_error` exceptions that include detailed error messages from the underlying FFI layer. + +## Design Principles + +- **Header-only wrappers**: All C++ code is in headers, no separate `.cpp` compilation needed +- **Zero-cost abstraction**: Minimal overhead over C API +- **Modern C++**: Uses C++17 features (structured bindings, if-init, etc.) +- **Per-pack modularity**: Include and link only what you need +- **Exception-based error handling**: Natural C++ idiom +- **RAII resource management**: No manual cleanup required + +## Comparison with C API + +| Feature | C API | C++ API | +|---------|-------|---------| +| Resource management | Manual `*_free()` | Automatic RAII | +| Error handling | Status codes + `cose_last_error_message_utf8()` | Exceptions with messages | +| Builder pattern | Function calls | Fluent method chaining | +| String handling | `char*` + manual free | `std::string` | +| Binary data | `uint8_t*` + length | `std::vector` | +| Options | C structs with pointers | C++ structs with STL containers | + +## Memory Safety + +- All heap allocations managed by RAII +- No raw pointers in public API (except internally for FFI) +- Move semantics prevent double-free +- Deleted copy constructors prevent accidental copying of resources diff --git a/native/c_pp/collect-coverage.ps1 b/native/c_pp/collect-coverage.ps1 new file mode 100644 index 00000000..57d6c394 --- /dev/null +++ b/native/c_pp/collect-coverage.ps1 @@ -0,0 +1,486 @@ +[CmdletBinding()] +param( + [ValidateSet('Debug', 'Release', 'RelWithDebInfo')] + [string]$Configuration = 'RelWithDebInfo', + + [string]$BuildDir = (Join-Path $PSScriptRoot 'build'), + [string]$ReportDir = (Join-Path $PSScriptRoot 'coverage'), + + # Compile and run tests under AddressSanitizer (ASAN) to catch memory errors. + # On MSVC this enables /fsanitize=address. + [switch]$EnableAsan = $true, + + # Optional: use vcpkg toolchain so GoogleTest can be found and the CTest + # suite runs gtest-discovered tests. + [string]$VcpkgRoot = 'C:\vcpkg', + [string]$VcpkgTriplet = 'x64-windows', + [switch]$UseVcpkg = $true, + [switch]$EnsureGTest = $true, + + # If set, fail fast when OpenCppCoverage isn't available. + # Otherwise, run tests via CTest and skip coverage generation. + [switch]$RequireCoverageTool, + + # Minimum overall line coverage percentage required for production/header code. + # Set to 0 to disable coverage gating (tests will still run). + [ValidateRange(0, 100)] + [int]$MinimumLineCoveragePercent = 95, + + [switch]$NoBuild +) + +$ErrorActionPreference = 'Stop' + +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 Get-VsInstallationPath { + $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 + } + + $vsPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + if ($LASTEXITCODE -ne 0 -or -not $vsPath) { + $vsPath = & $vswhere -latest -products * -property installationPath + } + + if (-not $vsPath) { + return $null + } + + $vsPath = ($vsPath | Select-Object -First 1).Trim() + if (-not $vsPath) { + return $null + } + + if (-not (Test-Path $vsPath)) { + return $null + } + + return $vsPath +} + +function Add-VsAsanRuntimeToPath { + if (-not ($env:OS -eq 'Windows_NT')) { + return + } + + $vsPath = Get-VsInstallationPath + if (-not $vsPath) { + return + } + + # On MSVC, /fsanitize=address depends on clang ASAN runtime DLLs that ship with VS. + # If they're not on PATH, Windows shows modal popup dialogs and tests fail with 0xc0000135. + $candidateDirs = @() + + $msvcToolsRoot = Join-Path $vsPath 'VC\Tools\MSVC' + if (Test-Path $msvcToolsRoot) { + $latestMsvc = Get-ChildItem -Path $msvcToolsRoot -Directory -ErrorAction SilentlyContinue | + Sort-Object Name -Descending | + Select-Object -First 1 + if ($latestMsvc) { + $candidateDirs += (Join-Path $latestMsvc.FullName 'bin\Hostx64\x64') + $candidateDirs += (Join-Path $latestMsvc.FullName 'bin\Hostx64\x86') + } + } + + $llvmRoot = Join-Path $vsPath 'VC\Tools\Llvm' + if (Test-Path $llvmRoot) { + $candidateDirs += (Join-Path $llvmRoot 'x64\bin') + $clangLibRoot = Join-Path $llvmRoot 'x64\lib\clang' + if (Test-Path $clangLibRoot) { + $latestClang = Get-ChildItem -Path $clangLibRoot -Directory -ErrorAction SilentlyContinue | + Sort-Object Name -Descending | + Select-Object -First 1 + if ($latestClang) { + $candidateDirs += (Join-Path $latestClang.FullName 'lib\windows') + } + } + } + + $asanDllName = 'clang_rt.asan_dynamic-x86_64.dll' + foreach ($dir in ($candidateDirs | Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique)) { + if (Test-Path (Join-Path $dir $asanDllName)) { + if ($env:PATH -notlike "${dir}*") { + $env:PATH = "${dir};$env:PATH" + Write-Host "Using ASAN runtime from: $dir" -ForegroundColor Yellow + } + return + } + } +} + +function Find-VsCMakeBin { + function Probe-VsRootForCMakeBin([string]$vsRoot) { + if (-not $vsRoot -or -not (Test-Path $vsRoot)) { + return $null + } + + $years = Get-ChildItem -Path $vsRoot -Directory -ErrorAction SilentlyContinue + foreach ($year in $years) { + $editions = Get-ChildItem -Path $year.FullName -Directory -ErrorAction SilentlyContinue + foreach ($edition in $editions) { + $cmakeBin = Join-Path $edition.FullName 'Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin' + if (Test-Path (Join-Path $cmakeBin 'cmake.exe')) { + return $cmakeBin + } + + $cmakeExtensionRoot = Join-Path $edition.FullName 'Common7\IDE\CommonExtensions\Microsoft\CMake' + if (Test-Path $cmakeExtensionRoot) { + $found = Get-ChildItem -Path $cmakeExtensionRoot -Recurse -File -Filter 'cmake.exe' -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ($found) { + return (Split-Path -Parent $found.FullName) + } + } + } + } + + return $null + } + + $vswhere = Resolve-ExePath -Name 'vswhere' -FallbackPaths @( + "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe", + "${env:ProgramFiles}\Microsoft Visual Studio\Installer\vswhere.exe" + ) + + if ($vswhere) { + $vsPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + if ($LASTEXITCODE -ne 0 -or -not $vsPath) { + $vsPath = & $vswhere -latest -products * -property installationPath + } + + if ($vsPath) { + $vsPath = ($vsPath | Select-Object -First 1).Trim() + if ($vsPath) { + $cmakeBin = Join-Path $vsPath 'Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin' + if (Test-Path (Join-Path $cmakeBin 'cmake.exe')) { + return $cmakeBin + } + + $cmakeExtensionRoot = Join-Path $vsPath 'Common7\IDE\CommonExtensions\Microsoft\CMake' + if (Test-Path $cmakeExtensionRoot) { + $found = Get-ChildItem -Path $cmakeExtensionRoot -Recurse -File -Filter 'cmake.exe' -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ($found) { + return (Split-Path -Parent $found.FullName) + } + } + } + } + } + + $roots = @( + (Join-Path $env:ProgramFiles 'Microsoft Visual Studio'), + (Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio') + ) + foreach ($r in ($roots | Where-Object { $_ })) { + $bin = Probe-VsRootForCMakeBin -vsRoot $r + if ($bin) { + return $bin + } + } + + return $null +} + +function Get-NormalizedPath([string]$Path) { + return [System.IO.Path]::GetFullPath($Path) +} + +function Get-CoberturaLineCoverage([string]$CoberturaPath) { + if (-not (Test-Path $CoberturaPath)) { + throw "Cobertura report not found: $CoberturaPath" + } + + [xml]$xml = Get-Content -LiteralPath $CoberturaPath + $root = $xml.SelectSingleNode('/coverage') + if (-not $root) { + throw "Invalid Cobertura report (missing root): $CoberturaPath" + } + + # OpenCppCoverage's Cobertura export can include the same source file multiple + # times (e.g., once per module/test executable). The root totals may + # therefore double-count "lines-valid" and under-report the union coverage. + # Aggregate coverage by (filename, line number) and take the max hits. + $fileToLineHits = @{} + $classNodes = $xml.SelectNodes('//class[@filename]') + foreach ($classNode in $classNodes) { + $filename = $classNode.GetAttribute('filename') + if (-not $filename) { + continue + } + + if (-not $fileToLineHits.ContainsKey($filename)) { + $fileToLineHits[$filename] = @{} + } + + $lineNodes = $classNode.SelectNodes('lines/line[@number and @hits]') + foreach ($lineNode in $lineNodes) { + $lineNumber = [int]$lineNode.GetAttribute('number') + $hits = [int]$lineNode.GetAttribute('hits') + $lineHitsForFile = $fileToLineHits[$filename] + + if ($lineHitsForFile.ContainsKey($lineNumber)) { + if ($hits -gt $lineHitsForFile[$lineNumber]) { + $lineHitsForFile[$lineNumber] = $hits + } + } else { + $lineHitsForFile[$lineNumber] = $hits + } + } + } + + $dedupedValid = 0 + $dedupedCovered = 0 + foreach ($filename in $fileToLineHits.Keys) { + foreach ($lineNumber in $fileToLineHits[$filename].Keys) { + $dedupedValid += 1 + if ($fileToLineHits[$filename][$lineNumber] -gt 0) { + $dedupedCovered += 1 + } + } + } + + $dedupedPercent = 0.0 + if ($dedupedValid -gt 0) { + $dedupedPercent = ($dedupedCovered / [double]$dedupedValid) * 100.0 + } + + # Keep root totals for diagnostics/fallback. + $rootLinesValid = [int]$root.GetAttribute('lines-valid') + $rootLinesCovered = [int]$root.GetAttribute('lines-covered') + $rootLineRateAttr = $root.GetAttribute('line-rate') + $rootPercent = 0.0 + if ($rootLinesValid -gt 0) { + $rootPercent = ($rootLinesCovered / [double]$rootLinesValid) * 100.0 + } elseif ($rootLineRateAttr) { + $rootPercent = ([double]$rootLineRateAttr) * 100.0 + } + + # If the deduped aggregation produced no data (e.g., missing entries), + # fall back to root totals so we still surface something useful. + if ($dedupedValid -le 0 -and $rootLinesValid -gt 0) { + $dedupedValid = $rootLinesValid + $dedupedCovered = $rootLinesCovered + $dedupedPercent = $rootPercent + } + + return [pscustomobject]@{ + LinesValid = $dedupedValid + LinesCovered = $dedupedCovered + Percent = $dedupedPercent + + RootLinesValid = $rootLinesValid + RootLinesCovered = $rootLinesCovered + RootPercent = $rootPercent + FileCount = $fileToLineHits.Count + } +} + +function Assert-Tooling { + $openCpp = Get-Command 'OpenCppCoverage.exe' -ErrorAction SilentlyContinue + if (-not $openCpp) { + $candidates = @( + $env:OPENCPPCOVERAGE_PATH, + 'C:\\Program Files\\OpenCppCoverage\\OpenCppCoverage.exe', + 'C:\\Program Files (x86)\\OpenCppCoverage\\OpenCppCoverage.exe' + ) + foreach ($candidate in $candidates) { + if ($candidate -and (Test-Path $candidate)) { + $openCpp = [pscustomobject]@{ Source = $candidate } + break + } + } + } + if (-not $openCpp -and $RequireCoverageTool) { + throw "OpenCppCoverage.exe not found on PATH. Install OpenCppCoverage and ensure it's available in PATH, or omit -RequireCoverageTool to run tests without coverage. See: https://github.com/OpenCppCoverage/OpenCppCoverage" + } + + $cmakeExe = (Get-Command 'cmake.exe' -ErrorAction SilentlyContinue).Source + $ctestExe = (Get-Command 'ctest.exe' -ErrorAction SilentlyContinue).Source + + if ((-not $cmakeExe) -or (-not $ctestExe)) { + if ($env:OS -eq 'Windows_NT') { + $vsCmakeBin = Find-VsCMakeBin + if ($vsCmakeBin) { + if ($env:PATH -notlike "${vsCmakeBin}*") { + $env:PATH = "${vsCmakeBin};$env:PATH" + } + + if (-not $cmakeExe) { + $candidate = (Join-Path $vsCmakeBin 'cmake.exe') + if (Test-Path $candidate) { $cmakeExe = $candidate } + } + if (-not $ctestExe) { + $candidate = (Join-Path $vsCmakeBin 'ctest.exe') + if (Test-Path $candidate) { $ctestExe = $candidate } + } + } + } + } + + if (-not $cmakeExe) { + throw 'cmake.exe not found on PATH (and no Visual Studio-bundled CMake was found).' + } + if (-not $ctestExe) { + throw 'ctest.exe not found on PATH (and no Visual Studio-bundled CTest was found).' + } + + $vcpkgExe = Join-Path $VcpkgRoot 'vcpkg.exe' + if ($UseVcpkg -or $EnsureGTest) { + if (-not (Test-Path $vcpkgExe)) { + throw "vcpkg.exe not found at $vcpkgExe" + } + + $toolchain = Join-Path $VcpkgRoot 'scripts\buildsystems\vcpkg.cmake' + if (-not (Test-Path $toolchain)) { + throw "vcpkg toolchain not found at $toolchain" + } + } + + return @{ + OpenCppCoverage = if ($openCpp) { $openCpp.Source } else { $null } + CMake = $cmakeExe + CTest = $ctestExe + } +} + +$tools = Assert-Tooling +$openCppCoverageExe = $tools.OpenCppCoverage +$cmakeExe = $tools.CMake +$ctestExe = $tools.CTest + +if ($MinimumLineCoveragePercent -gt 0) { + $RequireCoverageTool = $true +} + +# If the caller didn't explicitly override BuildDir/ReportDir, use ASAN-specific defaults. +if ($EnableAsan) { + if (-not $PSBoundParameters.ContainsKey('BuildDir')) { + $BuildDir = (Join-Path $PSScriptRoot 'build-asan') + } + if (-not $PSBoundParameters.ContainsKey('ReportDir')) { + $ReportDir = (Join-Path $PSScriptRoot 'coverage-asan') + } + + # Leak detection is generally not supported/usable on Windows; keep it off to reduce noise. + $env:ASAN_OPTIONS = 'detect_leaks=0,halt_on_error=1' + + Add-VsAsanRuntimeToPath +} + +if (-not $NoBuild) { + if ($EnsureGTest) { + $vcpkgExe = Join-Path $VcpkgRoot 'vcpkg.exe' + & $vcpkgExe install "gtest:$VcpkgTriplet" + if ($LASTEXITCODE -ne 0) { + throw "vcpkg failed to install gtest:$VcpkgTriplet" + } + $UseVcpkg = $true + } + + $cmakeArgs = @('-S', $PSScriptRoot, '-B', $BuildDir, '-DBUILD_TESTING=ON') + if ($EnableAsan) { + $cmakeArgs += '-DCOSE_ENABLE_ASAN=ON' + } + if ($UseVcpkg) { + $toolchain = Join-Path $VcpkgRoot 'scripts\buildsystems\vcpkg.cmake' + $cmakeArgs += "-DCMAKE_TOOLCHAIN_FILE=$toolchain" + $cmakeArgs += "-DVCPKG_TARGET_TRIPLET=$VcpkgTriplet" + $cmakeArgs += "-DVCPKG_APPLOCAL_DEPS=OFF" + } + + & $cmakeExe @cmakeArgs + & $cmakeExe --build $BuildDir --config $Configuration +} + +if (-not (Test-Path $BuildDir)) { + throw "Build directory not found: $BuildDir. Build first (or pass -BuildDir pointing to an existing build)." +} + +New-Item -ItemType Directory -Force -Path $ReportDir | Out-Null + +$sourcesList = @( + # Production/header code is primarily in include/ + (Get-NormalizedPath (Join-Path $PSScriptRoot 'include')) +) + +$excludeList = @( + (Get-NormalizedPath $BuildDir), + (Get-NormalizedPath (Join-Path $PSScriptRoot '..\\rust\\target')) +) + +if ($openCppCoverageExe) { + $coberturaPath = (Join-Path $ReportDir 'cobertura.xml') + + $openCppArgs = @() + foreach($s in $sourcesList) { $openCppArgs += '--sources'; $openCppArgs += $s } + foreach($e in $excludeList) { $openCppArgs += '--excluded_sources'; $openCppArgs += $e } + $openCppArgs += '--export_type' + $openCppArgs += ("html:" + $ReportDir) + $openCppArgs += '--export_type' + $openCppArgs += ("cobertura:" + $coberturaPath) + + # CTest spawns test executables; we must enable child-process coverage. + $openCppArgs += '--cover_children' + + $openCppArgs += '--quiet' + $openCppArgs += '--' + + & $openCppCoverageExe @openCppArgs $ctestExe --test-dir $BuildDir -C $Configuration --output-on-failure + + if ($LASTEXITCODE -ne 0) { + throw "OpenCppCoverage failed with exit code $LASTEXITCODE" + } + + $coverage = Get-CoberturaLineCoverage $coberturaPath + $pct = [Math]::Round([double]$coverage.Percent, 2) + Write-Host "Line coverage (production/header): ${pct}% ($($coverage.LinesCovered)/$($coverage.LinesValid))" + + if (($null -ne $coverage.RootLinesValid) -and ($coverage.RootLinesValid -gt 0)) { + $rootPct = [Math]::Round([double]$coverage.RootPercent, 2) + Write-Host "(Cobertura root totals: ${rootPct}% ($($coverage.RootLinesCovered)/$($coverage.RootLinesValid)))" + } + + if ($MinimumLineCoveragePercent -gt 0) { + if ($coverage.LinesValid -le 0) { + throw "No coverable production/header lines were detected by OpenCppCoverage (lines-valid=0); cannot enforce $MinimumLineCoveragePercent% gate." + } + + if ($coverage.Percent -lt $MinimumLineCoveragePercent) { + throw "Line coverage ${pct}% is below required ${MinimumLineCoveragePercent}%." + } + } +} else { + Write-Warning "OpenCppCoverage.exe not found; running tests without coverage." + & $ctestExe --test-dir $BuildDir -C $Configuration --output-on-failure + if ($LASTEXITCODE -ne 0) { + throw "CTest failed with exit code $LASTEXITCODE" + } +} + +Write-Host "Coverage report: $(Join-Path $ReportDir 'index.html')" diff --git a/native/c_pp/docs/01-consume-vcpkg.md b/native/c_pp/docs/01-consume-vcpkg.md new file mode 100644 index 00000000..1b3396af --- /dev/null +++ b/native/c_pp/docs/01-consume-vcpkg.md @@ -0,0 +1,31 @@ +# Consume via vcpkg (C++) + +The C++ projection is delivered by the same vcpkg port as the C projection. + +## Install + +```powershell +vcpkg install cosesign1-validation-native[cpp,certificates,mst,akv,trust] --overlay-ports=/native/vcpkg_ports +``` + +Notes: + +- Default features are `cpp` and `certificates`. + +## CMake usage + +```cmake +find_package(cose_sign1_validation CONFIG REQUIRED) + +target_link_libraries(your_target PRIVATE cosesign1_validation_native::cose_sign1_cpp) +``` + +## Headers + +- Convenience include-all: `` +- Core API: `` +- Optional packs (enabled by vcpkg features): + - `` (`COSE_HAS_CERTIFICATES_PACK`) + - `` (`COSE_HAS_MST_PACK`) + - `` (`COSE_HAS_AKV_PACK`) + - `` (`COSE_HAS_TRUST_PACK`) diff --git a/native/c_pp/docs/02-core-api.md b/native/c_pp/docs/02-core-api.md new file mode 100644 index 00000000..c15abfca --- /dev/null +++ b/native/c_pp/docs/02-core-api.md @@ -0,0 +1,38 @@ +# Core API (C++) + +The core C++ surface is in ``. + +## Types + +- `cose::ValidatorBuilder`: constructs a validator; owns a `cose_validator_builder_t*` +- `cose::Validator`: validates COSE_Sign1 bytes +- `cose::ValidationResult`: reports success/failure + provides a failure message +- `cose::cose_error`: thrown when a C API call returns a non-`COSE_OK` status + +## Minimal example + +```cpp +#include +#include + +bool validate(const std::vector& msg) +{ + auto validator = cose::ValidatorBuilder().Build(); + auto result = validator.Validate(msg); + + if (!result.Ok()) { + // result.FailureMessage() contains a human-readable reason + return false; + } + + return true; +} +``` + +## Detached payload + +Use the second parameter of `Validator::Validate`: + +```cpp +auto result = validator.Validate(cose_bytes, detached_payload); +``` diff --git a/native/c_pp/docs/03-errors.md b/native/c_pp/docs/03-errors.md new file mode 100644 index 00000000..1b0c41aa --- /dev/null +++ b/native/c_pp/docs/03-errors.md @@ -0,0 +1,13 @@ +# Errors (C++) + +The C++ wrapper throws `cose::cose_error` when a C API call fails. + +Under the hood it reads the thread-local last error message from the C API: + +- `cose_last_error_message_utf8()` +- `cose_string_free()` + +Validation failures are not thrown; they are represented by `cose::ValidationResult`: + +- `result.Ok()` returns `false` +- `result.FailureMessage()` returns a message string diff --git a/native/c_pp/docs/04-packs.md b/native/c_pp/docs/04-packs.md new file mode 100644 index 00000000..e557b63d --- /dev/null +++ b/native/c_pp/docs/04-packs.md @@ -0,0 +1,14 @@ +# Packs (C++) + +The convenience header `` includes the core validator API plus any enabled pack headers. + +Packs are enabled via vcpkg features and appear as: + +- `COSE_HAS_CERTIFICATES_PACK` → `` +- `COSE_HAS_MST_PACK` → `` +- `COSE_HAS_AKV_PACK` → `` +- `COSE_HAS_TRUST_PACK` → `` + +Most pack APIs extend the builder surface via helper functions/classes. + +If you’re authoring/attaching compiled trust plans, start with `` and the underlying C trust APIs in ``. diff --git a/native/c_pp/docs/README.md b/native/c_pp/docs/README.md new file mode 100644 index 00000000..34d5d1ad --- /dev/null +++ b/native/c_pp/docs/README.md @@ -0,0 +1,18 @@ +# Native C++ docs + +Start here: + +- [Consume via vcpkg](01-consume-vcpkg.md) +- [Core API](02-core-api.md) +- [Packs](04-packs.md) +- [Errors](03-errors.md) + +Cross-cutting: + +- Testing/coverage/ASAN: see [native/docs/06-testing-coverage-asan.md](../../docs/06-testing-coverage-asan.md) + +## Repo quick links + +- Headers: [native/c_pp/include/](../include/) +- Examples: [native/c_pp/examples/](../examples/) +- Tests: [native/c_pp/tests/](../tests/) diff --git a/native/c_pp/examples/CMakeLists.txt b/native/c_pp/examples/CMakeLists.txt new file mode 100644 index 00000000..0ee4efa7 --- /dev/null +++ b/native/c_pp/examples/CMakeLists.txt @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Examples are optional and primarily for developer guidance. +option(COSE_CPP_BUILD_EXAMPLES "Build C++ projection examples" ON) + +if(NOT COSE_CPP_BUILD_EXAMPLES) + return() +endif() + +if(NOT COSE_FFI_TRUST_LIB) + message(STATUS "Skipping C++ examples: trust pack not found (cose_sign1_validation_ffi_trust)") + return() +endif() + +add_executable(cose_trust_policy_example_cpp + trust_policy_example.cpp +) + +target_link_libraries(cose_trust_policy_example_cpp PRIVATE + cose_sign1_cpp +) diff --git a/native/c_pp/examples/trust_policy_example.cpp b/native/c_pp/examples/trust_policy_example.cpp new file mode 100644 index 00000000..8b3b167c --- /dev/null +++ b/native/c_pp/examples/trust_policy_example.cpp @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include + +#ifdef COSE_HAS_CERTIFICATES_PACK +#include +#endif + +#include +#include +#include +#include +#include + +static bool read_file_bytes(const std::string& path, std::vector& out) { + std::ifstream f(path, std::ios::binary); + if (!f) { + return false; + } + + f.seekg(0, std::ios::end); + std::streamoff size = f.tellg(); + if (size < 0) { + return false; + } + + f.seekg(0, std::ios::beg); + out.resize(static_cast(size)); + if (!out.empty()) { + f.read(reinterpret_cast(out.data()), static_cast(out.size())); + if (!f) { + return false; + } + } + + return true; +} + +static void usage(const char* argv0) { + std::cerr + << "Usage:\n" + << " " << argv0 << " [detached_payload.bin]\n\n" + << "Notes:\n" + << "- Builds a custom trust policy, compiles it to a bundled plan, attaches it to the builder,\n" + << " then validates the message and prints a failure message.\n"; +} + +int main(int argc, char** argv) { + if (argc < 2) { + usage(argv[0]); + return 2; + } + + const std::string cose_path = argv[1]; + const bool has_payload = (argc >= 3); + const std::string payload_path = has_payload ? argv[2] : std::string(); + + std::vector cose_bytes; + std::vector payload_bytes; + + if (!read_file_bytes(cose_path, cose_bytes)) { + std::cerr << "Failed to read COSE file: " << cose_path << "\n"; + return 2; + } + + if (has_payload) { + if (!read_file_bytes(payload_path, payload_bytes)) { + std::cerr << "Failed to read detached payload file: " << payload_path << "\n"; + return 2; + } + } + + try { +#ifdef COSE_HAS_CERTIFICATES_PACK + // 1) Builder + packs + cose::ValidatorBuilderWithCertificates builder; + builder.WithCertificates(); +#else + cose::ValidatorBuilder builder; +#endif + + // 2) Custom trust policy bound to builder's configured packs + cose::TrustPolicyBuilder policy(builder); + + if (has_payload) { + policy.RequireDetachedPayloadPresent(); + } else { + policy.RequireDetachedPayloadAbsent(); + } + +#ifdef COSE_HAS_CERTIFICATES_PACK + // Pack-specific trust-policy helpers (certificates pack) + policy.And(); + cose::RequireX509ChainTrusted(policy); + cose::RequireSigningCertificatePresent(policy); + cose::RequireSigningCertificateThumbprintPresent(policy); +#endif + + // 3) Compile + attach + auto plan = policy.Compile(); + cose::WithCompiledTrustPlan(builder, plan); + + // 4) Build validator + auto validator = builder.Build(); + + // 5) Validate + auto result = validator.Validate(cose_bytes, payload_bytes); + if (result.Ok()) { + std::cout << "Validation successful\n"; + return 0; + } + + std::cout << "Validation failed: " << result.FailureMessage() << "\n"; + return 1; + } catch (const cose::cose_error& e) { + std::cerr << "Error: " << e.what() << "\n"; + return 3; + } +} diff --git a/native/c_pp/include/cose/azure_key_vault.hpp b/native/c_pp/include/cose/azure_key_vault.hpp new file mode 100644 index 00000000..c0815e28 --- /dev/null +++ b/native/c_pp/include/cose/azure_key_vault.hpp @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * @file azure_key_vault.hpp + * @brief C++ wrappers for Azure Key Vault KID validation pack + */ + +#ifndef COSE_AZURE_KEY_VAULT_HPP +#define COSE_AZURE_KEY_VAULT_HPP + +#include +#include +#include +#include +#include + +namespace cose { + +/** + * @brief Options for Azure Key Vault KID validation + */ +struct AzureKeyVaultOptions { + /** If true, require the KID to look like an Azure Key Vault identifier */ + bool require_azure_key_vault_kid = true; + + /** Allowed KID pattern strings (supports wildcards * and ?). + * Empty vector means use defaults (*.vault.azure.net/keys/*, *.managedhsm.azure.net/keys/*) */ + std::vector allowed_kid_patterns; +}; + +/** + * @brief ValidatorBuilder extension for Azure Key Vault pack + */ +class ValidatorBuilderWithAzureKeyVault : public ValidatorBuilder { +public: + ValidatorBuilderWithAzureKeyVault() = default; + + /** + * @brief Add Azure Key Vault KID validation pack with default options + * @return Reference to this builder for chaining + */ + ValidatorBuilderWithAzureKeyVault& WithAzureKeyVault() { + CheckBuilder(); + detail::ThrowIfNotOk(cose_validator_builder_with_akv_pack(builder_)); + return *this; + } + + /** + * @brief Add Azure Key Vault KID validation pack with custom options + * @param options Azure Key Vault validation options + * @return Reference to this builder for chaining + */ + ValidatorBuilderWithAzureKeyVault& WithAzureKeyVault(const AzureKeyVaultOptions& options) { + CheckBuilder(); + + // Convert C++ strings to C string array + std::vector patterns_ptrs; + for (const auto& s : options.allowed_kid_patterns) { + patterns_ptrs.push_back(s.c_str()); + } + patterns_ptrs.push_back(nullptr); // NULL-terminated + + cose_akv_trust_options_t c_opts = { + options.require_azure_key_vault_kid, + options.allowed_kid_patterns.empty() ? nullptr : patterns_ptrs.data() + }; + + detail::ThrowIfNotOk(cose_validator_builder_with_akv_pack_ex(builder_, &c_opts)); + + return *this; + } +}; + +/** + * @brief Trust-policy helper: require that the message `kid` looks like an Azure Key Vault key identifier. + */ +inline TrustPolicyBuilder& RequireAzureKeyVaultKid(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk( + cose_akv_trust_policy_builder_require_azure_key_vault_kid(policy.native_handle()) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the message `kid` does not look like an Azure Key Vault key identifier. + */ +inline TrustPolicyBuilder& RequireNotAzureKeyVaultKid(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk( + cose_akv_trust_policy_builder_require_not_azure_key_vault_kid(policy.native_handle()) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the message `kid` is allowlisted by the AKV pack configuration. + */ +inline TrustPolicyBuilder& RequireAzureKeyVaultKidAllowed(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk( + cose_akv_trust_policy_builder_require_azure_key_vault_kid_allowed(policy.native_handle()) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the message `kid` is not allowlisted by the AKV pack configuration. + */ +inline TrustPolicyBuilder& RequireAzureKeyVaultKidNotAllowed(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk( + cose_akv_trust_policy_builder_require_azure_key_vault_kid_not_allowed(policy.native_handle()) + ); + return policy; +} + +} // namespace cose + +#endif // COSE_AZURE_KEY_VAULT_HPP diff --git a/native/c_pp/include/cose/certificates.hpp b/native/c_pp/include/cose/certificates.hpp new file mode 100644 index 00000000..46b23a52 --- /dev/null +++ b/native/c_pp/include/cose/certificates.hpp @@ -0,0 +1,585 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * @file certificates.hpp + * @brief C++ wrappers for X.509 certificate validation pack + */ + +#ifndef COSE_CERTIFICATES_HPP +#define COSE_CERTIFICATES_HPP + +#include +#include +#include +#include +#include + +namespace cose { + +/** + * @brief Options for X.509 certificate validation + */ +struct CertificateOptions { + /** If true, treat well-formed embedded x5chain as trusted (for tests/pinned roots) */ + bool trust_embedded_chain_as_trusted = false; + + /** If true, enable identity pinning based on allowed_thumbprints */ + bool identity_pinning_enabled = false; + + /** Allowed certificate thumbprints (case/whitespace insensitive) */ + std::vector allowed_thumbprints; + + /** PQC algorithm OID strings */ + std::vector pqc_algorithm_oids; +}; + +/** + * @brief ValidatorBuilder extension for certificates pack + */ +class ValidatorBuilderWithCertificates : public ValidatorBuilder { +public: + ValidatorBuilderWithCertificates() = default; + + /** + * @brief Add X.509 certificate validation pack with default options + * @return Reference to this builder for chaining + */ + ValidatorBuilderWithCertificates& WithCertificates() { + CheckBuilder(); + detail::ThrowIfNotOk(cose_validator_builder_with_certificates_pack(builder_)); + return *this; + } + + /** + * @brief Add X.509 certificate validation pack with custom options + * @param options Certificate validation options + * @return Reference to this builder for chaining + */ + ValidatorBuilderWithCertificates& WithCertificates(const CertificateOptions& options) { + CheckBuilder(); + + // Convert C++ strings to C string arrays + std::vector thumbprints_ptrs; + for (const auto& s : options.allowed_thumbprints) { + thumbprints_ptrs.push_back(s.c_str()); + } + thumbprints_ptrs.push_back(nullptr); // NULL-terminated + + std::vector oids_ptrs; + for (const auto& s : options.pqc_algorithm_oids) { + oids_ptrs.push_back(s.c_str()); + } + oids_ptrs.push_back(nullptr); // NULL-terminated + + cose_certificate_trust_options_t c_opts = { + options.trust_embedded_chain_as_trusted, + options.identity_pinning_enabled, + options.allowed_thumbprints.empty() ? nullptr : thumbprints_ptrs.data(), + options.pqc_algorithm_oids.empty() ? nullptr : oids_ptrs.data() + }; + + detail::ThrowIfNotOk(cose_validator_builder_with_certificates_pack_ex(builder_, &c_opts)); + + return *this; + } +}; + +/** + * @brief Trust-policy helper: require that the X.509 chain is trusted. + */ +inline TrustPolicyBuilder& RequireX509ChainTrusted(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_x509_chain_trusted(policy.native_handle()) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the X.509 chain is not trusted. + */ +inline TrustPolicyBuilder& RequireX509ChainNotTrusted(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_x509_chain_not_trusted(policy.native_handle()) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the X.509 chain could be built (pack observed at least one element). + */ +inline TrustPolicyBuilder& RequireX509ChainBuilt(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_x509_chain_built(policy.native_handle()) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the X.509 chain could not be built. + */ +inline TrustPolicyBuilder& RequireX509ChainNotBuilt(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_x509_chain_not_built(policy.native_handle()) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the X.509 chain element count equals `expected`. + */ +inline TrustPolicyBuilder& RequireX509ChainElementCountEq(TrustPolicyBuilder& policy, size_t expected) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_x509_chain_element_count_eq( + policy.native_handle(), + expected + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the X.509 chain status flags equal `expected`. + */ +inline TrustPolicyBuilder& RequireX509ChainStatusFlagsEq(TrustPolicyBuilder& policy, uint32_t expected) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_x509_chain_status_flags_eq( + policy.native_handle(), + expected + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the leaf chain element (index 0) has a non-empty thumbprint. + */ +inline TrustPolicyBuilder& RequireLeafChainThumbprintPresent(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_leaf_chain_thumbprint_present(policy.native_handle()) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that a signing certificate identity fact is present. + */ +inline TrustPolicyBuilder& RequireSigningCertificatePresent(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_signing_certificate_present(policy.native_handle()) + ); + return policy; +} + +/** + * @brief Trust-policy helper: pin the leaf certificate subject name (chain element index 0). + */ +inline TrustPolicyBuilder& RequireLeafSubjectEq(TrustPolicyBuilder& policy, const std::string& subject) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_leaf_subject_eq( + policy.native_handle(), + subject.c_str() + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: pin the issuer certificate subject name (chain element index 1). + */ +inline TrustPolicyBuilder& RequireIssuerSubjectEq(TrustPolicyBuilder& policy, const std::string& subject) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_issuer_subject_eq( + policy.native_handle(), + subject.c_str() + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the signing certificate subject/issuer matches the leaf chain element. + */ +inline TrustPolicyBuilder& RequireSigningCertificateSubjectIssuerMatchesLeafChainElement(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_signing_certificate_subject_issuer_matches_leaf_chain_element( + policy.native_handle() + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: if the issuer element (index 1) is missing, allow; otherwise require issuer chaining. + */ +inline TrustPolicyBuilder& RequireLeafIssuerIsNextChainSubjectOptional(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_leaf_issuer_is_next_chain_subject_optional( + policy.native_handle() + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require the leaf signing certificate thumbprint to equal the provided value. + */ +inline TrustPolicyBuilder& RequireSigningCertificateThumbprintEq(TrustPolicyBuilder& policy, const std::string& thumbprint) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_signing_certificate_thumbprint_eq( + policy.native_handle(), + thumbprint.c_str() + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the leaf signing certificate thumbprint is present and non-empty. + */ +inline TrustPolicyBuilder& RequireSigningCertificateThumbprintPresent(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_signing_certificate_thumbprint_present(policy.native_handle()) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require the leaf signing certificate subject to equal the provided value. + */ +inline TrustPolicyBuilder& RequireSigningCertificateSubjectEq(TrustPolicyBuilder& policy, const std::string& subject) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_signing_certificate_subject_eq( + policy.native_handle(), + subject.c_str() + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require the leaf signing certificate issuer to equal the provided value. + */ +inline TrustPolicyBuilder& RequireSigningCertificateIssuerEq(TrustPolicyBuilder& policy, const std::string& issuer) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_signing_certificate_issuer_eq( + policy.native_handle(), + issuer.c_str() + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require the leaf signing certificate serial number to equal the provided value. + */ +inline TrustPolicyBuilder& RequireSigningCertificateSerialNumberEq( + TrustPolicyBuilder& policy, + const std::string& serial_number +) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_signing_certificate_serial_number_eq( + policy.native_handle(), + serial_number.c_str() + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the signing certificate is expired at or before `now_unix_seconds`. + */ +inline TrustPolicyBuilder& RequireSigningCertificateExpiredAtOrBefore(TrustPolicyBuilder& policy, int64_t now_unix_seconds) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_signing_certificate_expired_at_or_before( + policy.native_handle(), + now_unix_seconds + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the leaf signing certificate is valid at `now_unix_seconds`. + */ +inline TrustPolicyBuilder& RequireSigningCertificateValidAt(TrustPolicyBuilder& policy, int64_t now_unix_seconds) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_signing_certificate_valid_at( + policy.native_handle(), + now_unix_seconds + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require signing certificate not-before <= `max_unix_seconds`. + */ +inline TrustPolicyBuilder& RequireSigningCertificateNotBeforeLe(TrustPolicyBuilder& policy, int64_t max_unix_seconds) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_signing_certificate_not_before_le( + policy.native_handle(), + max_unix_seconds + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require signing certificate not-before >= `min_unix_seconds`. + */ +inline TrustPolicyBuilder& RequireSigningCertificateNotBeforeGe(TrustPolicyBuilder& policy, int64_t min_unix_seconds) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_signing_certificate_not_before_ge( + policy.native_handle(), + min_unix_seconds + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require signing certificate not-after <= `max_unix_seconds`. + */ +inline TrustPolicyBuilder& RequireSigningCertificateNotAfterLe(TrustPolicyBuilder& policy, int64_t max_unix_seconds) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_signing_certificate_not_after_le( + policy.native_handle(), + max_unix_seconds + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require signing certificate not-after >= `min_unix_seconds`. + */ +inline TrustPolicyBuilder& RequireSigningCertificateNotAfterGe(TrustPolicyBuilder& policy, int64_t min_unix_seconds) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_signing_certificate_not_after_ge( + policy.native_handle(), + min_unix_seconds + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the X.509 chain element at `index` has subject equal to the provided value. + */ +inline TrustPolicyBuilder& RequireChainElementSubjectEq( + TrustPolicyBuilder& policy, + size_t index, + const std::string& subject +) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_chain_element_subject_eq( + policy.native_handle(), + index, + subject.c_str() + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the X.509 chain element at `index` has issuer equal to the provided value. + */ +inline TrustPolicyBuilder& RequireChainElementIssuerEq( + TrustPolicyBuilder& policy, + size_t index, + const std::string& issuer +) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_chain_element_issuer_eq( + policy.native_handle(), + index, + issuer.c_str() + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the X.509 chain element at `index` has thumbprint equal to the provided value. + */ +inline TrustPolicyBuilder& RequireChainElementThumbprintEq( + TrustPolicyBuilder& policy, + size_t index, + const std::string& thumbprint +) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_chain_element_thumbprint_eq( + policy.native_handle(), + index, + thumbprint.c_str() + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the X.509 chain element at `index` has a non-empty thumbprint. + */ +inline TrustPolicyBuilder& RequireChainElementThumbprintPresent( + TrustPolicyBuilder& policy, + size_t index +) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_chain_element_thumbprint_present( + policy.native_handle(), + index + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the X.509 chain element at `index` is valid at `now_unix_seconds`. + */ +inline TrustPolicyBuilder& RequireChainElementValidAt( + TrustPolicyBuilder& policy, + size_t index, + int64_t now_unix_seconds +) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_chain_element_valid_at( + policy.native_handle(), + index, + now_unix_seconds + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require chain element not-before <= `max_unix_seconds`. + */ +inline TrustPolicyBuilder& RequireChainElementNotBeforeLe( + TrustPolicyBuilder& policy, + size_t index, + int64_t max_unix_seconds +) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_chain_element_not_before_le( + policy.native_handle(), + index, + max_unix_seconds + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require chain element not-before >= `min_unix_seconds`. + */ +inline TrustPolicyBuilder& RequireChainElementNotBeforeGe( + TrustPolicyBuilder& policy, + size_t index, + int64_t min_unix_seconds +) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_chain_element_not_before_ge( + policy.native_handle(), + index, + min_unix_seconds + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require chain element not-after <= `max_unix_seconds`. + */ +inline TrustPolicyBuilder& RequireChainElementNotAfterLe( + TrustPolicyBuilder& policy, + size_t index, + int64_t max_unix_seconds +) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_chain_element_not_after_le( + policy.native_handle(), + index, + max_unix_seconds + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require chain element not-after >= `min_unix_seconds`. + */ +inline TrustPolicyBuilder& RequireChainElementNotAfterGe( + TrustPolicyBuilder& policy, + size_t index, + int64_t min_unix_seconds +) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_chain_element_not_after_ge( + policy.native_handle(), + index, + min_unix_seconds + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: deny if a PQC algorithm is explicitly detected; allow if missing. + */ +inline TrustPolicyBuilder& RequireNotPqcAlgorithmOrMissing(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_not_pqc_algorithm_or_missing(policy.native_handle()) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the X.509 public key algorithm fact has thumbprint equal to the provided value. + */ +inline TrustPolicyBuilder& RequireX509PublicKeyAlgorithmThumbprintEq(TrustPolicyBuilder& policy, const std::string& thumbprint) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_x509_public_key_algorithm_thumbprint_eq( + policy.native_handle(), + thumbprint.c_str() + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the X.509 public key algorithm OID equals the provided value. + */ +inline TrustPolicyBuilder& RequireX509PublicKeyAlgorithmOidEq(TrustPolicyBuilder& policy, const std::string& oid) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_x509_public_key_algorithm_oid_eq( + policy.native_handle(), + oid.c_str() + ) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the X.509 public key algorithm is flagged as PQC. + */ +inline TrustPolicyBuilder& RequireX509PublicKeyAlgorithmIsPqc(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_x509_public_key_algorithm_is_pqc(policy.native_handle()) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the X.509 public key algorithm is not flagged as PQC. + */ +inline TrustPolicyBuilder& RequireX509PublicKeyAlgorithmIsNotPqc(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk( + cose_certificates_trust_policy_builder_require_x509_public_key_algorithm_is_not_pqc(policy.native_handle()) + ); + return policy; +} + +} // namespace cose + +#endif // COSE_CERTIFICATES_HPP diff --git a/native/c_pp/include/cose/cose.hpp b/native/c_pp/include/cose/cose.hpp new file mode 100644 index 00000000..9d209cd2 --- /dev/null +++ b/native/c_pp/include/cose/cose.hpp @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * @file cose.hpp + * @brief Convenience header that includes all COSE C++ wrappers + * + * This header includes all available pack extensions. If you want to use only + * specific packs, include the individual headers instead. + */ + +#ifndef COSE_HPP +#define COSE_HPP + +#include + +// Optional pack headers - include only if the corresponding FFI library is available +#ifdef COSE_HAS_CERTIFICATES_PACK +#include +#endif + +#ifdef COSE_HAS_MST_PACK +#include +#endif + +#ifdef COSE_HAS_AKV_PACK +#include +#endif + +#ifdef COSE_HAS_TRUST_PACK +#include +#endif + +#endif // COSE_HPP diff --git a/native/c_pp/include/cose/mst.hpp b/native/c_pp/include/cose/mst.hpp new file mode 100644 index 00000000..83b5e466 --- /dev/null +++ b/native/c_pp/include/cose/mst.hpp @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * @file mst.hpp + * @brief C++ wrappers for MST receipt verification pack + */ + +#ifndef COSE_MST_HPP +#define COSE_MST_HPP + +#include +#include +#include +#include + +namespace cose { + +/** + * @brief Options for MST receipt verification + */ +struct MstOptions { + /** If true, allow network fetching of JWKS when offline keys are missing */ + bool allow_network = true; + + /** Offline JWKS JSON string (empty means no offline JWKS) */ + std::string offline_jwks_json; + + /** Optional api-version for CodeTransparency /jwks endpoint (empty means no api-version) */ + std::string jwks_api_version; +}; + +/** + * @brief ValidatorBuilder extension for MST pack + */ +class ValidatorBuilderWithMst : public ValidatorBuilder { +public: + ValidatorBuilderWithMst() = default; + + /** + * @brief Add MST receipt verification pack with default options (online mode) + * @return Reference to this builder for chaining + */ + ValidatorBuilderWithMst& WithMst() { + CheckBuilder(); + detail::ThrowIfNotOk(cose_validator_builder_with_mst_pack(builder_)); + return *this; + } + + /** + * @brief Add MST receipt verification pack with custom options + * @param options MST verification options + * @return Reference to this builder for chaining + */ + ValidatorBuilderWithMst& WithMst(const MstOptions& options) { + CheckBuilder(); + + cose_mst_trust_options_t c_opts = { + options.allow_network, + options.offline_jwks_json.empty() ? nullptr : options.offline_jwks_json.c_str(), + options.jwks_api_version.empty() ? nullptr : options.jwks_api_version.c_str() + }; + + detail::ThrowIfNotOk(cose_validator_builder_with_mst_pack_ex(builder_, &c_opts)); + + return *this; + } +}; + +/** + * @brief Trust-policy helper: require that an MST receipt is present on at least one counter-signature. + */ +inline TrustPolicyBuilder& RequireMstReceiptPresent(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk( + cose_mst_trust_policy_builder_require_receipt_present(policy.native_handle()) + ); + return policy; +} + +inline TrustPolicyBuilder& RequireMstReceiptNotPresent(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk( + cose_mst_trust_policy_builder_require_receipt_not_present(policy.native_handle()) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the MST receipt signature verified. + */ +inline TrustPolicyBuilder& RequireMstReceiptSignatureVerified(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk( + cose_mst_trust_policy_builder_require_receipt_signature_verified(policy.native_handle()) + ); + return policy; +} + +inline TrustPolicyBuilder& RequireMstReceiptSignatureNotVerified(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk( + cose_mst_trust_policy_builder_require_receipt_signature_not_verified(policy.native_handle()) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the MST receipt issuer contains the provided substring. + */ +inline TrustPolicyBuilder& RequireMstReceiptIssuerContains(TrustPolicyBuilder& policy, const std::string& needle) { + detail::ThrowIfNotOk( + cose_mst_trust_policy_builder_require_receipt_issuer_contains( + policy.native_handle(), + needle.c_str() + ) + ); + return policy; +} + +inline TrustPolicyBuilder& RequireMstReceiptIssuerEq(TrustPolicyBuilder& policy, const std::string& issuer) { + detail::ThrowIfNotOk( + cose_mst_trust_policy_builder_require_receipt_issuer_eq(policy.native_handle(), issuer.c_str()) + ); + return policy; +} + +/** + * @brief Trust-policy helper: require that the MST receipt key id (kid) equals the provided value. + */ +inline TrustPolicyBuilder& RequireMstReceiptKidEq(TrustPolicyBuilder& policy, const std::string& kid) { + detail::ThrowIfNotOk( + cose_mst_trust_policy_builder_require_receipt_kid_eq(policy.native_handle(), kid.c_str()) + ); + return policy; +} + +inline TrustPolicyBuilder& RequireMstReceiptKidContains(TrustPolicyBuilder& policy, const std::string& needle) { + detail::ThrowIfNotOk( + cose_mst_trust_policy_builder_require_receipt_kid_contains(policy.native_handle(), needle.c_str()) + ); + return policy; +} + +inline TrustPolicyBuilder& RequireMstReceiptTrusted(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk(cose_mst_trust_policy_builder_require_receipt_trusted(policy.native_handle())); + return policy; +} + +inline TrustPolicyBuilder& RequireMstReceiptNotTrusted(TrustPolicyBuilder& policy) { + detail::ThrowIfNotOk(cose_mst_trust_policy_builder_require_receipt_not_trusted(policy.native_handle())); + return policy; +} + +inline TrustPolicyBuilder& RequireMstReceiptTrustedFromIssuerContains(TrustPolicyBuilder& policy, const std::string& needle) { + detail::ThrowIfNotOk( + cose_mst_trust_policy_builder_require_receipt_trusted_from_issuer_contains( + policy.native_handle(), + needle.c_str() + ) + ); + return policy; +} + +inline TrustPolicyBuilder& RequireMstReceiptStatementSha256Eq(TrustPolicyBuilder& policy, const std::string& sha256Hex) { + detail::ThrowIfNotOk( + cose_mst_trust_policy_builder_require_receipt_statement_sha256_eq( + policy.native_handle(), + sha256Hex.c_str() + ) + ); + return policy; +} + +inline TrustPolicyBuilder& RequireMstReceiptStatementCoverageEq(TrustPolicyBuilder& policy, const std::string& coverage) { + detail::ThrowIfNotOk( + cose_mst_trust_policy_builder_require_receipt_statement_coverage_eq( + policy.native_handle(), + coverage.c_str() + ) + ); + return policy; +} + +inline TrustPolicyBuilder& RequireMstReceiptStatementCoverageContains(TrustPolicyBuilder& policy, const std::string& needle) { + detail::ThrowIfNotOk( + cose_mst_trust_policy_builder_require_receipt_statement_coverage_contains( + policy.native_handle(), + needle.c_str() + ) + ); + return policy; +} + +} // namespace cose + +#endif // COSE_MST_HPP diff --git a/native/c_pp/include/cose/trust.hpp b/native/c_pp/include/cose/trust.hpp new file mode 100644 index 00000000..af6b7ba5 --- /dev/null +++ b/native/c_pp/include/cose/trust.hpp @@ -0,0 +1,510 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * @file trust.hpp + * @brief C++ RAII wrappers for trust-plan authoring (Trust pack) + */ + +#ifndef COSE_TRUST_HPP +#define COSE_TRUST_HPP + +#include +#include + +#include +#include +#include +#include + +namespace cose { + +class CompiledTrustPlan { +public: + explicit CompiledTrustPlan(cose_compiled_trust_plan_t* plan) : plan_(plan) { + if (!plan_) { + throw cose_error("Null compiled trust plan"); + } + } + + ~CompiledTrustPlan() { + if (plan_) { + cose_compiled_trust_plan_free(plan_); + } + } + + CompiledTrustPlan(const CompiledTrustPlan&) = delete; + CompiledTrustPlan& operator=(const CompiledTrustPlan&) = delete; + + CompiledTrustPlan(CompiledTrustPlan&& other) noexcept : plan_(other.plan_) { + other.plan_ = nullptr; + } + + CompiledTrustPlan& operator=(CompiledTrustPlan&& other) noexcept { + if (this != &other) { + if (plan_) { + cose_compiled_trust_plan_free(plan_); + } + plan_ = other.plan_; + other.plan_ = nullptr; + } + return *this; + } + + const cose_compiled_trust_plan_t* native_handle() const { + return plan_; + } + +private: + cose_compiled_trust_plan_t* plan_; + + friend class TrustPlanBuilder; +}; + +class TrustPlanBuilder { +public: + explicit TrustPlanBuilder(const ValidatorBuilder& validator_builder) { + cose_status_t status = cose_trust_plan_builder_new_from_validator_builder( + validator_builder.native_handle(), + &builder_ + ); + detail::ThrowIfNotOkOrNull(status, builder_); + } + + ~TrustPlanBuilder() { + if (builder_) { + cose_trust_plan_builder_free(builder_); + } + } + + TrustPlanBuilder(const TrustPlanBuilder&) = delete; + TrustPlanBuilder& operator=(const TrustPlanBuilder&) = delete; + + TrustPlanBuilder(TrustPlanBuilder&& other) noexcept : builder_(other.builder_) { + other.builder_ = nullptr; + } + + TrustPlanBuilder& operator=(TrustPlanBuilder&& other) noexcept { + if (this != &other) { + if (builder_) { + cose_trust_plan_builder_free(builder_); + } + builder_ = other.builder_; + other.builder_ = nullptr; + } + return *this; + } + + TrustPlanBuilder& AddAllPackDefaultPlans() { + CheckBuilder(); + detail::ThrowIfNotOk(cose_trust_plan_builder_add_all_pack_default_plans(builder_)); + return *this; + } + + TrustPlanBuilder& AddPackDefaultPlanByName(const std::string& pack_name) { + CheckBuilder(); + cose_status_t status = cose_trust_plan_builder_add_pack_default_plan_by_name( + builder_, + pack_name.c_str() + ); + detail::ThrowIfNotOk(status); + return *this; + } + + size_t PackCount() const { + CheckBuilder(); + size_t count = 0; + detail::ThrowIfNotOk(cose_trust_plan_builder_pack_count(builder_, &count)); + return count; + } + + std::string PackName(size_t index) const { + CheckBuilder(); + char* s = cose_trust_plan_builder_pack_name_utf8(builder_, index); + if (!s) { + throw cose_error(COSE_ERR); + } + std::string out(s); + cose_string_free(s); + return out; + } + + bool PackHasDefaultPlan(size_t index) const { + CheckBuilder(); + bool has_default = false; + detail::ThrowIfNotOk(cose_trust_plan_builder_pack_has_default_plan(builder_, index, &has_default)); + return has_default; + } + + TrustPlanBuilder& ClearSelectedPlans() { + CheckBuilder(); + detail::ThrowIfNotOk(cose_trust_plan_builder_clear_selected_plans(builder_)); + return *this; + } + + CompiledTrustPlan CompileOr() { + CheckBuilder(); + cose_compiled_trust_plan_t* out = nullptr; + cose_status_t status = cose_trust_plan_builder_compile_or(builder_, &out); + detail::ThrowIfNotOkOrNull(status, out); + return CompiledTrustPlan(out); + } + + CompiledTrustPlan CompileAnd() { + CheckBuilder(); + cose_compiled_trust_plan_t* out = nullptr; + cose_status_t status = cose_trust_plan_builder_compile_and(builder_, &out); + detail::ThrowIfNotOkOrNull(status, out); + return CompiledTrustPlan(out); + } + + CompiledTrustPlan CompileAllowAll() { + CheckBuilder(); + cose_compiled_trust_plan_t* out = nullptr; + cose_status_t status = cose_trust_plan_builder_compile_allow_all(builder_, &out); + detail::ThrowIfNotOkOrNull(status, out); + return CompiledTrustPlan(out); + } + + CompiledTrustPlan CompileDenyAll() { + CheckBuilder(); + cose_compiled_trust_plan_t* out = nullptr; + cose_status_t status = cose_trust_plan_builder_compile_deny_all(builder_, &out); + detail::ThrowIfNotOkOrNull(status, out); + return CompiledTrustPlan(out); + } + +private: + cose_trust_plan_builder_t* builder_ = nullptr; + + void CheckBuilder() const { + if (!builder_) { + throw cose_error("TrustPlanBuilder already consumed or invalid"); + } + } +}; + +class TrustPolicyBuilder { +public: + explicit TrustPolicyBuilder(const ValidatorBuilder& validator_builder) { + cose_status_t status = cose_trust_policy_builder_new_from_validator_builder( + validator_builder.native_handle(), + &builder_ + ); + detail::ThrowIfNotOkOrNull(status, builder_); + } + + ~TrustPolicyBuilder() { + if (builder_) { + cose_trust_policy_builder_free(builder_); + } + } + + TrustPolicyBuilder(const TrustPolicyBuilder&) = delete; + TrustPolicyBuilder& operator=(const TrustPolicyBuilder&) = delete; + + TrustPolicyBuilder(TrustPolicyBuilder&& other) noexcept : builder_(other.builder_) { + other.builder_ = nullptr; + } + + TrustPolicyBuilder& operator=(TrustPolicyBuilder&& other) noexcept { + if (this != &other) { + if (builder_) { + cose_trust_policy_builder_free(builder_); + } + builder_ = other.builder_; + other.builder_ = nullptr; + } + return *this; + } + + /** + * @brief Expose the underlying C policy-builder handle for optional pack projections. + */ + cose_trust_policy_builder_t* native_handle() const { + return builder_; + } + + TrustPolicyBuilder& And() { + CheckBuilder(); + detail::ThrowIfNotOk(cose_trust_policy_builder_and(builder_)); + return *this; + } + + TrustPolicyBuilder& Or() { + CheckBuilder(); + detail::ThrowIfNotOk(cose_trust_policy_builder_or(builder_)); + return *this; + } + + TrustPolicyBuilder& RequireContentTypeNonEmpty() { + CheckBuilder(); + detail::ThrowIfNotOk(cose_trust_policy_builder_require_content_type_non_empty(builder_)); + return *this; + } + + TrustPolicyBuilder& RequireContentTypeEq(const std::string& content_type) { + CheckBuilder(); + cose_status_t status = cose_trust_policy_builder_require_content_type_eq( + builder_, + content_type.c_str() + ); + detail::ThrowIfNotOk(status); + return *this; + } + + TrustPolicyBuilder& RequireDetachedPayloadPresent() { + CheckBuilder(); + detail::ThrowIfNotOk(cose_trust_policy_builder_require_detached_payload_present(builder_)); + return *this; + } + + TrustPolicyBuilder& RequireDetachedPayloadAbsent() { + CheckBuilder(); + detail::ThrowIfNotOk(cose_trust_policy_builder_require_detached_payload_absent(builder_)); + return *this; + } + + TrustPolicyBuilder& RequireCounterSignatureEnvelopeSigStructureIntactOrMissing() { + CheckBuilder(); + detail::ThrowIfNotOk( + cose_trust_policy_builder_require_counter_signature_envelope_sig_structure_intact_or_missing( + builder_ + ) + ); + return *this; + } + + TrustPolicyBuilder& RequireCwtClaimsPresent() { + CheckBuilder(); + detail::ThrowIfNotOk(cose_trust_policy_builder_require_cwt_claims_present(builder_)); + return *this; + } + + TrustPolicyBuilder& RequireCwtClaimsAbsent() { + CheckBuilder(); + detail::ThrowIfNotOk(cose_trust_policy_builder_require_cwt_claims_absent(builder_)); + return *this; + } + + TrustPolicyBuilder& RequireCwtIssEq(const std::string& iss) { + CheckBuilder(); + detail::ThrowIfNotOk(cose_trust_policy_builder_require_cwt_iss_eq(builder_, iss.c_str())); + return *this; + } + + TrustPolicyBuilder& RequireCwtSubEq(const std::string& sub) { + CheckBuilder(); + detail::ThrowIfNotOk(cose_trust_policy_builder_require_cwt_sub_eq(builder_, sub.c_str())); + return *this; + } + + TrustPolicyBuilder& RequireCwtAudEq(const std::string& aud) { + CheckBuilder(); + detail::ThrowIfNotOk(cose_trust_policy_builder_require_cwt_aud_eq(builder_, aud.c_str())); + return *this; + } + + TrustPolicyBuilder& RequireCwtClaimLabelPresent(int64_t label) { + CheckBuilder(); + detail::ThrowIfNotOk( + cose_trust_policy_builder_require_cwt_claim_label_present(builder_, label) + ); + return *this; + } + + TrustPolicyBuilder& RequireCwtClaimTextPresent(const std::string& key) { + CheckBuilder(); + detail::ThrowIfNotOk( + cose_trust_policy_builder_require_cwt_claim_text_present(builder_, key.c_str()) + ); + return *this; + } + + TrustPolicyBuilder& RequireCwtClaimLabelI64Eq(int64_t label, int64_t value) { + CheckBuilder(); + detail::ThrowIfNotOk( + cose_trust_policy_builder_require_cwt_claim_label_i64_eq(builder_, label, value) + ); + return *this; + } + + TrustPolicyBuilder& RequireCwtClaimLabelBoolEq(int64_t label, bool value) { + CheckBuilder(); + detail::ThrowIfNotOk( + cose_trust_policy_builder_require_cwt_claim_label_bool_eq(builder_, label, value) + ); + return *this; + } + + TrustPolicyBuilder& RequireCwtClaimLabelI64Ge(int64_t label, int64_t min) { + CheckBuilder(); + detail::ThrowIfNotOk( + cose_trust_policy_builder_require_cwt_claim_label_i64_ge(builder_, label, min) + ); + return *this; + } + + TrustPolicyBuilder& RequireCwtClaimLabelI64Le(int64_t label, int64_t max) { + CheckBuilder(); + detail::ThrowIfNotOk( + cose_trust_policy_builder_require_cwt_claim_label_i64_le(builder_, label, max) + ); + return *this; + } + + TrustPolicyBuilder& RequireCwtClaimTextStrEq(const std::string& key, const std::string& value) { + CheckBuilder(); + detail::ThrowIfNotOk( + cose_trust_policy_builder_require_cwt_claim_text_str_eq(builder_, key.c_str(), value.c_str()) + ); + return *this; + } + + TrustPolicyBuilder& RequireCwtClaimLabelStrEq(int64_t label, const std::string& value) { + CheckBuilder(); + detail::ThrowIfNotOk( + cose_trust_policy_builder_require_cwt_claim_label_str_eq(builder_, label, value.c_str()) + ); + return *this; + } + + TrustPolicyBuilder& RequireCwtClaimLabelStrStartsWith(int64_t label, const std::string& prefix) { + CheckBuilder(); + detail::ThrowIfNotOk( + cose_trust_policy_builder_require_cwt_claim_label_str_starts_with(builder_, label, prefix.c_str()) + ); + return *this; + } + + TrustPolicyBuilder& RequireCwtClaimTextStrStartsWith( + const std::string& key, + const std::string& prefix + ) { + CheckBuilder(); + detail::ThrowIfNotOk( + cose_trust_policy_builder_require_cwt_claim_text_str_starts_with(builder_, key.c_str(), prefix.c_str()) + ); + return *this; + } + + TrustPolicyBuilder& RequireCwtClaimLabelStrContains(int64_t label, const std::string& needle) { + CheckBuilder(); + detail::ThrowIfNotOk( + cose_trust_policy_builder_require_cwt_claim_label_str_contains(builder_, label, needle.c_str()) + ); + return *this; + } + + TrustPolicyBuilder& RequireCwtClaimTextStrContains( + const std::string& key, + const std::string& needle + ) { + CheckBuilder(); + detail::ThrowIfNotOk( + cose_trust_policy_builder_require_cwt_claim_text_str_contains(builder_, key.c_str(), needle.c_str()) + ); + return *this; + } + + TrustPolicyBuilder& RequireCwtClaimTextBoolEq(const std::string& key, bool value) { + CheckBuilder(); + detail::ThrowIfNotOk( + cose_trust_policy_builder_require_cwt_claim_text_bool_eq(builder_, key.c_str(), value) + ); + return *this; + } + + TrustPolicyBuilder& RequireCwtClaimTextI64Eq(const std::string& key, int64_t value) { + CheckBuilder(); + detail::ThrowIfNotOk( + cose_trust_policy_builder_require_cwt_claim_text_i64_eq(builder_, key.c_str(), value) + ); + return *this; + } + + TrustPolicyBuilder& RequireCwtClaimTextI64Ge(const std::string& key, int64_t min) { + CheckBuilder(); + detail::ThrowIfNotOk( + cose_trust_policy_builder_require_cwt_claim_text_i64_ge(builder_, key.c_str(), min) + ); + return *this; + } + + TrustPolicyBuilder& RequireCwtClaimTextI64Le(const std::string& key, int64_t max) { + CheckBuilder(); + detail::ThrowIfNotOk( + cose_trust_policy_builder_require_cwt_claim_text_i64_le(builder_, key.c_str(), max) + ); + return *this; + } + + TrustPolicyBuilder& RequireCwtExpGe(int64_t min) { + CheckBuilder(); + detail::ThrowIfNotOk(cose_trust_policy_builder_require_cwt_exp_ge(builder_, min)); + return *this; + } + + TrustPolicyBuilder& RequireCwtExpLe(int64_t max) { + CheckBuilder(); + detail::ThrowIfNotOk(cose_trust_policy_builder_require_cwt_exp_le(builder_, max)); + return *this; + } + + TrustPolicyBuilder& RequireCwtNbfGe(int64_t min) { + CheckBuilder(); + detail::ThrowIfNotOk(cose_trust_policy_builder_require_cwt_nbf_ge(builder_, min)); + return *this; + } + + TrustPolicyBuilder& RequireCwtNbfLe(int64_t max) { + CheckBuilder(); + detail::ThrowIfNotOk(cose_trust_policy_builder_require_cwt_nbf_le(builder_, max)); + return *this; + } + + TrustPolicyBuilder& RequireCwtIatGe(int64_t min) { + CheckBuilder(); + detail::ThrowIfNotOk(cose_trust_policy_builder_require_cwt_iat_ge(builder_, min)); + return *this; + } + + TrustPolicyBuilder& RequireCwtIatLe(int64_t max) { + CheckBuilder(); + detail::ThrowIfNotOk(cose_trust_policy_builder_require_cwt_iat_le(builder_, max)); + return *this; + } + + CompiledTrustPlan Compile() { + CheckBuilder(); + cose_compiled_trust_plan_t* out = nullptr; + cose_status_t status = cose_trust_policy_builder_compile(builder_, &out); + detail::ThrowIfNotOkOrNull(status, out); + return CompiledTrustPlan(out); + } + +private: + cose_trust_policy_builder_t* builder_ = nullptr; + + void CheckBuilder() const { + if (!builder_) { + throw cose_error("TrustPolicyBuilder already consumed or invalid"); + } + } +}; + +inline ValidatorBuilder& WithCompiledTrustPlan( + ValidatorBuilder& builder, + const CompiledTrustPlan& plan +) { + cose_status_t status = cose_validator_builder_with_compiled_trust_plan( + builder.native_handle(), + plan.native_handle() + ); + detail::ThrowIfNotOk(status); + return builder; +} + +} // namespace cose + +#endif // COSE_TRUST_HPP diff --git a/native/c_pp/include/cose/validator.hpp b/native/c_pp/include/cose/validator.hpp new file mode 100644 index 00000000..7b8c0682 --- /dev/null +++ b/native/c_pp/include/cose/validator.hpp @@ -0,0 +1,294 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * @file validator.hpp + * @brief C++ RAII wrappers for COSE Sign1 validation + */ + +#ifndef COSE_VALIDATOR_HPP +#define COSE_VALIDATOR_HPP + +#include +#include +#include +#include +#include + +namespace cose { + +/** + * @brief Exception thrown by COSE validation operations + */ +class cose_error : public std::runtime_error { +public: + explicit cose_error(const std::string& msg) : std::runtime_error(msg) {} + explicit cose_error(cose_status_t status) + : std::runtime_error(get_error_message(status)) {} + +private: + static std::string get_error_message(cose_status_t status) { + char* msg = cose_last_error_message_utf8(); + if (msg) { + std::string result(msg); + cose_string_free(msg); + return result; + } + return "COSE error (status=" + std::to_string(static_cast(status)) + ")"; + } +}; + +namespace detail { + +inline void ThrowIfNotOk(cose_status_t status) { + if (status != COSE_OK) { + throw cose_error(status); + } +} + +template +inline void ThrowIfNotOkOrNull(cose_status_t status, T* ptr) { + if (status != COSE_OK || !ptr) { + throw cose_error(status); + } +} + +} // namespace detail + +/** + * @brief RAII wrapper for validation result + */ +class ValidationResult { +public: + explicit ValidationResult(cose_validation_result_t* result) : result_(result) { + if (!result_) { + throw cose_error("Null validation result"); + } + } + + ~ValidationResult() { + if (result_) { + cose_validation_result_free(result_); + } + } + + // Non-copyable + ValidationResult(const ValidationResult&) = delete; + ValidationResult& operator=(const ValidationResult&) = delete; + + // Movable + ValidationResult(ValidationResult&& other) noexcept : result_(other.result_) { + other.result_ = nullptr; + } + + ValidationResult& operator=(ValidationResult&& other) noexcept { + if (this != &other) { + if (result_) { + cose_validation_result_free(result_); + } + result_ = other.result_; + other.result_ = nullptr; + } + return *this; + } + + /** + * @brief Check if validation was successful + * @return true if validation succeeded, false otherwise + */ + bool Ok() const { + bool ok = false; + cose_status_t status = cose_validation_result_is_success(result_, &ok); + if (status != COSE_OK) { + throw cose_error(status); + } + return ok; + } + + /** + * @brief Get failure message if validation failed + * @return Failure message string, or empty string if validation succeeded + */ + std::string FailureMessage() const { + char* msg = cose_validation_result_failure_message_utf8(result_); + if (msg) { + std::string result(msg); + cose_string_free(msg); + return result; + } + return std::string(); + } + +private: + cose_validation_result_t* result_; +}; + +/** + * @brief RAII wrapper for validator + */ +class Validator { +public: + explicit Validator(cose_validator_t* validator) : validator_(validator) { + if (!validator_) { + throw cose_error("Null validator"); + } + } + + ~Validator() { + if (validator_) { + cose_validator_free(validator_); + } + } + + // Non-copyable + Validator(const Validator&) = delete; + Validator& operator=(const Validator&) = delete; + + // Movable + Validator(Validator&& other) noexcept : validator_(other.validator_) { + other.validator_ = nullptr; + } + + Validator& operator=(Validator&& other) noexcept { + if (this != &other) { + if (validator_) { + cose_validator_free(validator_); + } + validator_ = other.validator_; + other.validator_ = nullptr; + } + return *this; + } + + /** + * @brief Validate COSE Sign1 message bytes + * + * @param cose_bytes COSE Sign1 message bytes + * @param detached_payload Optional detached payload bytes (empty for embedded payload) + * @return ValidationResult object + */ + ValidationResult Validate( + const std::vector& cose_bytes, + const std::vector& detached_payload = {} + ) const { + cose_validation_result_t* result = nullptr; + + const uint8_t* detached_ptr = detached_payload.empty() ? nullptr : detached_payload.data(); + size_t detached_len = detached_payload.size(); + + cose_status_t status = cose_validator_validate_bytes( + validator_, + cose_bytes.data(), + cose_bytes.size(), + detached_ptr, + detached_len, + &result + ); + + if (status != COSE_OK) { + throw cose_error(status); + } + + return ValidationResult(result); + } + +private: + cose_validator_t* validator_; + + friend class ValidatorBuilder; +}; + +/** + * @brief Fluent builder for Validator + * + * Example usage: + * @code + * auto validator = ValidatorBuilder() + * .WithCertificates() + * .WithMst() + * .Build(); + * auto result = validator.Validate(cose_bytes); + * if (result.Ok()) { + * // Validation successful + * } + * @endcode + */ +class ValidatorBuilder { +public: + ValidatorBuilder() { + cose_status_t status = cose_validator_builder_new(&builder_); + if (status != COSE_OK || !builder_) { + throw cose_error(status); + } + } + + ~ValidatorBuilder() { + if (builder_) { + cose_validator_builder_free(builder_); + } + } + + // Non-copyable + ValidatorBuilder(const ValidatorBuilder&) = delete; + ValidatorBuilder& operator=(const ValidatorBuilder&) = delete; + + // Movable + ValidatorBuilder(ValidatorBuilder&& other) noexcept : builder_(other.builder_) { + other.builder_ = nullptr; + } + + ValidatorBuilder& operator=(ValidatorBuilder&& other) noexcept { + if (this != &other) { + if (builder_) { + cose_validator_builder_free(builder_); + } + builder_ = other.builder_; + other.builder_ = nullptr; + } + return *this; + } + + /** + * @brief Build the validator + * @return Validator object + * @throws cose_error if build fails + */ + Validator Build() { + if (!builder_) { + throw cose_error("Builder already consumed"); + } + + cose_validator_t* validator = nullptr; + cose_status_t status = cose_validator_builder_build(builder_, &validator); + + // Builder is consumed, prevent double-free + builder_ = nullptr; + + if (status != COSE_OK || !validator) { + throw cose_error(status); + } + + return Validator(validator); + } + + /** + * @brief Expose the underlying C builder handle for advanced / optional pack projections. + */ + cose_validator_builder_t* native_handle() const { + return builder_; + } + +protected: + cose_validator_builder_t* builder_; + + // Helper for pack methods to check builder validity + void CheckBuilder() const { + if (!builder_) { + throw cose_error("Builder already consumed or invalid"); + } + } +}; + +} // namespace cose + +#endif // COSE_VALIDATOR_HPP diff --git a/native/c_pp/tests/CMakeLists.txt b/native/c_pp/tests/CMakeLists.txt new file mode 100644 index 00000000..98ee7f4f --- /dev/null +++ b/native/c_pp/tests/CMakeLists.txt @@ -0,0 +1,132 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Prefer GoogleTest (via vcpkg) when available; otherwise fall back to the +# custom-runner executables so the repo still builds without extra deps. +find_package(GTest CONFIG QUIET) + +if (GTest_FOUND) + include(GoogleTest) + + function(cose_copy_rust_dlls target_name) + if(NOT WIN32) + return() + endif() + + set(_rust_dlls "") + foreach(_libvar IN ITEMS COSE_FFI_BASE_LIB COSE_FFI_CERTIFICATES_LIB COSE_FFI_MST_LIB COSE_FFI_AKV_LIB COSE_FFI_TRUST_LIB) + if(DEFINED ${_libvar} AND ${_libvar}) + set(_import_lib "${${_libvar}}") + if(_import_lib MATCHES "\\.dll\\.lib$") + string(REPLACE ".dll.lib" ".dll" _dll "${_import_lib}") + list(APPEND _rust_dlls "${_dll}") + endif() + endif() + endforeach() + + list(REMOVE_DUPLICATES _rust_dlls) + foreach(_dll IN LISTS _rust_dlls) + if(EXISTS "${_dll}") + add_custom_command( + TARGET ${target_name} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_dll}" $ + ) + endif() + endforeach() + + # Also copy MSVC runtime + other dynamic deps when available. + # This avoids failures on environments without global VC redistributables. + if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.21") + add_custom_command( + TARGET ${target_name} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ + COMMAND_EXPAND_LISTS + ) + endif() + + # MSVC ASAN uses an additional runtime DLL that is not always present on PATH. + # Copy it next to the executable to avoid 0xc0000135 during gtest discovery. + if(MSVC AND COSE_ENABLE_ASAN) + get_filename_component(_cl_dir "${CMAKE_CXX_COMPILER}" DIRECTORY) + foreach(_asan_name IN ITEMS + clang_rt.asan_dynamic-x86_64.dll + clang_rt.asan_dynamic-i386.dll + clang_rt.asan_dynamic-aarch64.dll + ) + set(_asan_dll "${_cl_dir}/${_asan_name}") + if(EXISTS "${_asan_dll}") + add_custom_command( + TARGET ${target_name} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_asan_dll}" $ + ) + endif() + endforeach() + endif() + endfunction() + + add_executable(smoke_test_cpp smoke_test_gtest.cpp) + target_link_libraries(smoke_test_cpp PRIVATE cose_sign1_cpp GTest::gtest_main) + cose_copy_rust_dlls(smoke_test_cpp) + gtest_discover_tests(smoke_test_cpp DISCOVERY_MODE PRE_TEST DISCOVERY_TIMEOUT 30) + + add_executable(coverage_surface_cpp coverage_surface_gtest.cpp) + target_link_libraries(coverage_surface_cpp PRIVATE cose_sign1_cpp GTest::gtest_main) + cose_copy_rust_dlls(coverage_surface_cpp) + gtest_discover_tests(coverage_surface_cpp DISCOVERY_MODE PRE_TEST DISCOVERY_TIMEOUT 30) + + if (COSE_FFI_TRUST_LIB) + add_executable(real_world_trust_plans_test_cpp real_world_trust_plans_gtest.cpp) + target_link_libraries(real_world_trust_plans_test_cpp PRIVATE cose_sign1_cpp GTest::gtest_main) + cose_copy_rust_dlls(real_world_trust_plans_test_cpp) + + get_filename_component(COSE_REPO_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../.." ABSOLUTE) + set(COSE_TESTDATA_V1_DIR "${COSE_REPO_ROOT}/native/rust/cose_sign1_validation_certificates/testdata/v1") + set(COSE_MST_JWKS_PATH "${COSE_REPO_ROOT}/native/rust/cose_sign1_validation_transparent_mst/testdata/esrp-cts-cp.confidential-ledger.azure.com.jwks.json") + + target_compile_definitions(real_world_trust_plans_test_cpp PRIVATE + COSE_TESTDATA_V1_DIR="${COSE_TESTDATA_V1_DIR}" + COSE_MST_JWKS_PATH="${COSE_MST_JWKS_PATH}" + ) + + gtest_discover_tests(real_world_trust_plans_test_cpp DISCOVERY_MODE PRE_TEST DISCOVERY_TIMEOUT 30) + endif() +else() + # Basic smoke test for C++ API + add_executable(smoke_test_cpp smoke_test.cpp) + target_link_libraries(smoke_test_cpp PRIVATE cose_sign1_cpp) + add_test(NAME smoke_test_cpp COMMAND smoke_test_cpp) + + if (COSE_FFI_TRUST_LIB) + add_executable(real_world_trust_plans_test_cpp real_world_trust_plans_test.cpp) + target_link_libraries(real_world_trust_plans_test_cpp PRIVATE cose_sign1_cpp) + + get_filename_component(COSE_REPO_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../.." ABSOLUTE) + set(COSE_TESTDATA_V1_DIR "${COSE_REPO_ROOT}/native/rust/cose_sign1_validation_certificates/testdata/v1") + set(COSE_MST_JWKS_PATH "${COSE_REPO_ROOT}/native/rust/cose_sign1_validation_transparent_mst/testdata/esrp-cts-cp.confidential-ledger.azure.com.jwks.json") + + target_compile_definitions(real_world_trust_plans_test_cpp PRIVATE + COSE_TESTDATA_V1_DIR="${COSE_TESTDATA_V1_DIR}" + COSE_MST_JWKS_PATH="${COSE_MST_JWKS_PATH}" + ) + + add_test(NAME real_world_trust_plans_test_cpp COMMAND real_world_trust_plans_test_cpp) + + set(COSE_REAL_WORLD_TEST_NAMES + compile_fails_when_required_pack_missing + compile_succeeds_when_required_pack_present + real_v1_policy_can_gate_on_certificate_facts + real_scitt_policy_can_require_cwt_claims_and_mst_receipt_trusted_from_issuer + real_v1_policy_can_validate_with_mst_only_by_bypassing_primary_signature + ) + + foreach(tname IN LISTS COSE_REAL_WORLD_TEST_NAMES) + add_test( + NAME real_world_trust_plans_test_cpp.${tname} + COMMAND real_world_trust_plans_test_cpp --test ${tname} + ) + endforeach() + endif() +endif() diff --git a/native/c_pp/tests/coverage_surface_gtest.cpp b/native/c_pp/tests/coverage_surface_gtest.cpp new file mode 100644 index 00000000..58034cf4 --- /dev/null +++ b/native/c_pp/tests/coverage_surface_gtest.cpp @@ -0,0 +1,247 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include + +#include + +#include +#include +#include +#include + +TEST(CoverageSurface, TrustAndCoreBuilders) { + // Cover CompiledTrustPlan null-guard. + EXPECT_THROW((void)cose::CompiledTrustPlan(nullptr), cose::cose_error); + + // Cover ValidatorBuilder move ops and the "consumed" error path. + cose::ValidatorBuilder b1; + cose::ValidatorBuilder b2(std::move(b1)); + cose::ValidatorBuilder b3; + b3 = std::move(b2); + + EXPECT_THROW((void)b1.Build(), cose::cose_error); + + // Exercise TrustPolicyBuilder surface. + cose::TrustPolicyBuilder p(b3); + p.And() + .RequireContentTypeNonEmpty() + .RequireContentTypeEq("application/cose") + .Or() + .RequireDetachedPayloadPresent() + .RequireDetachedPayloadAbsent() + .RequireCounterSignatureEnvelopeSigStructureIntactOrMissing() + .RequireCwtClaimsPresent() + .RequireCwtClaimsAbsent() + .RequireCwtIssEq("issuer") + .RequireCwtSubEq("subject") + .RequireCwtAudEq("aud") + .RequireCwtClaimLabelPresent(1) + .RequireCwtClaimTextPresent("k") + .RequireCwtClaimLabelI64Eq(2, 42) + .RequireCwtClaimLabelBoolEq(3, true) + .RequireCwtClaimLabelI64Ge(4, 0) + .RequireCwtClaimLabelI64Le(5, 100) + .RequireCwtClaimTextStrEq("k2", "v2") + .RequireCwtClaimLabelStrEq(6, "v") + .RequireCwtClaimLabelStrStartsWith(7, "pre") + .RequireCwtClaimTextStrStartsWith("k3", "pre") + .RequireCwtClaimLabelStrContains(8, "needle") + .RequireCwtClaimTextStrContains("k4", "needle") + .RequireCwtClaimTextBoolEq("k5", false) + .RequireCwtClaimTextI64Eq("k6", -1) + .RequireCwtClaimTextI64Ge("k7", 0) + .RequireCwtClaimTextI64Le("k8", 123) + .RequireCwtExpGe(0) + .RequireCwtExpLe(4102444800) // 2100-01-01 + .RequireCwtNbfGe(0) + .RequireCwtNbfLe(4102444800) + .RequireCwtIatGe(0) + .RequireCwtIatLe(4102444800); + + // Exercise TrustPlanBuilder surface. + cose::TrustPlanBuilder plan_builder(b3); + EXPECT_NO_THROW((void)plan_builder.AddAllPackDefaultPlans()); + + const size_t pack_count = plan_builder.PackCount(); + // Cover PackName failure path (out-of-range index). + EXPECT_THROW((void)plan_builder.PackName(pack_count), cose::cose_error); + + for (size_t i = 0; i < pack_count; ++i) { + const auto name = plan_builder.PackName(i); + (void)plan_builder.PackHasDefaultPlan(i); + if (plan_builder.PackHasDefaultPlan(i)) { + EXPECT_NO_THROW((void)plan_builder.AddPackDefaultPlanByName(name)); + } + } + + EXPECT_NO_THROW((void)plan_builder.ClearSelectedPlans()); + + // Cover compile helpers that should not depend on selected plans. + auto allow_all = plan_builder.CompileAllowAll(); + auto deny_all = plan_builder.CompileDenyAll(); + + // Cover CompiledTrustPlan move operations. + cose::CompiledTrustPlan moved_plan(std::move(deny_all)); + deny_all = std::move(moved_plan); + + // Cover CompiledTrustPlan move-assignment branch where the destination already owns a plan. + auto allow_all2 = plan_builder.CompileAllowAll(); + auto deny_all2 = plan_builder.CompileDenyAll(); + allow_all2 = std::move(deny_all2); + + // Cover TrustPlanBuilder move-assignment branch where the destination already owns a builder. + cose::TrustPlanBuilder tb1(b3); + cose::TrustPlanBuilder tb2(b3); + tb1 = std::move(tb2); + EXPECT_NO_THROW((void)tb1.PackCount()); + EXPECT_THROW((void)tb2.PackCount(), cose::cose_error); + + cose::ValidatorBuilder b4; + EXPECT_NO_THROW((void)cose::WithCompiledTrustPlan(b4, allow_all)); + + // Cover WithCompiledTrustPlan error path by using a moved-from builder handle. + cose::ValidatorBuilder moved_from; + cose::ValidatorBuilder moved_to(std::move(moved_from)); + (void)moved_to; + EXPECT_THROW((void)cose::WithCompiledTrustPlan(moved_from, allow_all), cose::cose_error); + + // Cover CheckBuilder() failure on TrustPolicyBuilder. + cose::TrustPolicyBuilder p2(std::move(p)); + EXPECT_THROW((void)p.And(), cose::cose_error); + + // Use p2 so it stays alive and is destroyed cleanly. + EXPECT_NO_THROW((void)p2.Compile()); +} + +TEST(CoverageSurface, ThrowsWhenValidatorBuilderConsumed) { + // Ensure ThrowIfNotOkOrNull is covered for constructors that wrap a C "new" API. + cose::ValidatorBuilder b; + auto validator = b.Build(); + (void)validator; + + EXPECT_THROW((void)cose::TrustPlanBuilder(b), cose::cose_error); + EXPECT_THROW((void)cose::TrustPolicyBuilder(b), cose::cose_error); +} + +#ifdef COSE_HAS_CERTIFICATES_PACK +TEST(CoverageSurface, CertificatesPackAndPolicyHelpers) { + cose::ValidatorBuilderWithCertificates b; + + cose::CertificateOptions opts; + opts.trust_embedded_chain_as_trusted = true; + opts.identity_pinning_enabled = true; + opts.allowed_thumbprints = {"aa", "bb"}; + opts.pqc_algorithm_oids = {"1.2.3.4"}; + + EXPECT_NO_THROW((void)b.WithCertificates()); + EXPECT_NO_THROW((void)b.WithCertificates(opts)); + + cose::TrustPolicyBuilder policy(b); + + // Exercise all certificates trust-policy helpers. + cose::RequireX509ChainTrusted(policy); + cose::RequireX509ChainNotTrusted(policy); + cose::RequireX509ChainBuilt(policy); + cose::RequireX509ChainNotBuilt(policy); + cose::RequireX509ChainElementCountEq(policy, 2); + cose::RequireX509ChainStatusFlagsEq(policy, 0); + cose::RequireLeafChainThumbprintPresent(policy); + cose::RequireSigningCertificatePresent(policy); + cose::RequireLeafSubjectEq(policy, "CN=leaf"); + cose::RequireIssuerSubjectEq(policy, "CN=issuer"); + cose::RequireSigningCertificateSubjectIssuerMatchesLeafChainElement(policy); + cose::RequireLeafIssuerIsNextChainSubjectOptional(policy); + cose::RequireSigningCertificateThumbprintEq(policy, "00"); + cose::RequireSigningCertificateThumbprintPresent(policy); + cose::RequireSigningCertificateSubjectEq(policy, "CN=leaf"); + cose::RequireSigningCertificateIssuerEq(policy, "CN=issuer"); + cose::RequireSigningCertificateSerialNumberEq(policy, "01"); + cose::RequireSigningCertificateExpiredAtOrBefore(policy, 0); + cose::RequireSigningCertificateValidAt(policy, 0); + cose::RequireSigningCertificateNotBeforeLe(policy, 0); + cose::RequireSigningCertificateNotBeforeGe(policy, 0); + cose::RequireSigningCertificateNotAfterLe(policy, 0); + cose::RequireSigningCertificateNotAfterGe(policy, 0); + cose::RequireChainElementSubjectEq(policy, 0, "CN=leaf"); + cose::RequireChainElementIssuerEq(policy, 0, "CN=issuer"); + cose::RequireChainElementThumbprintEq(policy, 0, "00"); + cose::RequireChainElementThumbprintPresent(policy, 0); + cose::RequireChainElementValidAt(policy, 0, 0); + cose::RequireChainElementNotBeforeLe(policy, 0, 0); + cose::RequireChainElementNotBeforeGe(policy, 0, 0); + cose::RequireChainElementNotAfterLe(policy, 0, 0); + cose::RequireChainElementNotAfterGe(policy, 0, 0); + cose::RequireNotPqcAlgorithmOrMissing(policy); + cose::RequireX509PublicKeyAlgorithmThumbprintEq(policy, "00"); + cose::RequireX509PublicKeyAlgorithmOidEq(policy, "1.2.3.4"); + cose::RequireX509PublicKeyAlgorithmIsPqc(policy); + cose::RequireX509PublicKeyAlgorithmIsNotPqc(policy); + + // Cover the error branch in helper functions by calling them on a moved-from builder. + cose::TrustPolicyBuilder policy2(std::move(policy)); + EXPECT_THROW((void)cose::RequireX509ChainTrusted(policy), cose::cose_error); + + // Keep policy2 alive for cleanup. + EXPECT_NO_THROW((void)policy2.Compile()); +} +#endif + +#ifdef COSE_HAS_MST_PACK +TEST(CoverageSurface, MstPackAndPolicyHelpers) { + cose::ValidatorBuilderWithMst b; + + cose::MstOptions opts; + opts.allow_network = false; + opts.offline_jwks_json = "{\"keys\":[]}"; + opts.jwks_api_version = "2023-01-01"; + + EXPECT_NO_THROW((void)b.WithMst()); + EXPECT_NO_THROW((void)b.WithMst(opts)); + + cose::TrustPolicyBuilder policy(b); + + cose::RequireMstReceiptPresent(policy); + cose::RequireMstReceiptNotPresent(policy); + cose::RequireMstReceiptSignatureVerified(policy); + cose::RequireMstReceiptSignatureNotVerified(policy); + cose::RequireMstReceiptIssuerContains(policy, "issuer"); + cose::RequireMstReceiptIssuerEq(policy, "issuer"); + cose::RequireMstReceiptKidEq(policy, "kid"); + cose::RequireMstReceiptKidContains(policy, "kid"); + cose::RequireMstReceiptTrusted(policy); + cose::RequireMstReceiptNotTrusted(policy); + cose::RequireMstReceiptTrustedFromIssuerContains(policy, "issuer"); + cose::RequireMstReceiptStatementSha256Eq(policy, "00"); + cose::RequireMstReceiptStatementCoverageEq(policy, "coverage"); + cose::RequireMstReceiptStatementCoverageContains(policy, "cov"); + + cose::TrustPolicyBuilder policy2(std::move(policy)); + EXPECT_THROW((void)cose::RequireMstReceiptPresent(policy), cose::cose_error); + EXPECT_NO_THROW((void)policy2.Compile()); +} +#endif + +#ifdef COSE_HAS_AKV_PACK +TEST(CoverageSurface, AkvPackAndPolicyHelpers) { + cose::ValidatorBuilderWithAzureKeyVault b; + + cose::AzureKeyVaultOptions opts; + opts.require_azure_key_vault_kid = true; + opts.allowed_kid_patterns = {"*.vault.azure.net/keys/*"}; + + EXPECT_NO_THROW((void)b.WithAzureKeyVault()); + EXPECT_NO_THROW((void)b.WithAzureKeyVault(opts)); + + cose::TrustPolicyBuilder policy(b); + + cose::RequireAzureKeyVaultKid(policy); + cose::RequireNotAzureKeyVaultKid(policy); + cose::RequireAzureKeyVaultKidAllowed(policy); + cose::RequireAzureKeyVaultKidNotAllowed(policy); + + cose::TrustPolicyBuilder policy2(std::move(policy)); + EXPECT_THROW((void)cose::RequireAzureKeyVaultKid(policy), cose::cose_error); + EXPECT_NO_THROW((void)policy2.Compile()); +} +#endif diff --git a/native/c_pp/tests/real_world_trust_plans_gtest.cpp b/native/c_pp/tests/real_world_trust_plans_gtest.cpp new file mode 100644 index 00000000..186d1b86 --- /dev/null +++ b/native/c_pp/tests/real_world_trust_plans_gtest.cpp @@ -0,0 +1,221 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include + +#include + +#ifdef COSE_HAS_CERTIFICATES_PACK +#include +#endif + +#ifdef COSE_HAS_MST_PACK +#include +#endif + +#include +#include +#include +#include +#include + +#ifndef COSE_TESTDATA_V1_DIR +#define COSE_TESTDATA_V1_DIR "" +#endif + +#ifndef COSE_MST_JWKS_PATH +#define COSE_MST_JWKS_PATH "" +#endif + +static std::vector read_file_bytes(const std::string& path) { + std::ifstream f(path, std::ios::binary); + if (!f) { + throw std::runtime_error("failed to open file: " + path); + } + + f.seekg(0, std::ios::end); + auto size = f.tellg(); + if (size < 0) { + throw std::runtime_error("failed to stat file: " + path); + } + + f.seekg(0, std::ios::beg); + std::vector out(static_cast(size)); + if (!out.empty()) { + f.read(reinterpret_cast(out.data()), static_cast(out.size())); + if (!f) { + throw std::runtime_error("failed to read file: " + path); + } + } + + return out; +} + +static std::string join_path2(const std::string& a, const std::string& b) { + if (a.empty()) return b; + const char last = a.back(); + if (last == '/' || last == '\\') return a + b; + return a + "/" + b; +} + +TEST(RealWorldTrustPlans, CompileFailsWhenRequiredPackMissing) { +#ifndef COSE_HAS_TRUST_PACK + GTEST_SKIP() << "trust pack not available"; +#else +#ifndef COSE_HAS_CERTIFICATES_PACK + GTEST_SKIP() << "COSE_HAS_CERTIFICATES_PACK not enabled"; +#else + // Certificates pack is linked, but NOT configured on the builder. + // Requiring a certificates-only fact should fail. + cose::ValidatorBuilder builder; + cose::TrustPolicyBuilder policy(builder); + + try { + cose::RequireX509ChainTrusted(policy); + (void)policy.Compile(); + FAIL() << "expected policy.Compile() to throw"; + } catch (const cose::cose_error&) { + SUCCEED(); + } +#endif +#endif +} + +TEST(RealWorldTrustPlans, CompileSucceedsWhenRequiredPackPresent) { +#ifndef COSE_HAS_TRUST_PACK + GTEST_SKIP() << "trust pack not available"; +#else +#ifndef COSE_HAS_CERTIFICATES_PACK + GTEST_SKIP() << "COSE_HAS_CERTIFICATES_PACK not enabled"; +#else + cose::ValidatorBuilder builder; + + ASSERT_EQ(cose_validator_builder_with_certificates_pack(builder.native_handle()), COSE_OK); + + cose::TrustPolicyBuilder policy(builder); + cose::RequireX509ChainTrusted(policy); + + auto plan = policy.Compile(); + cose::WithCompiledTrustPlan(builder, plan); + + auto validator = builder.Build(); + (void)validator; +#endif +#endif +} + +TEST(RealWorldTrustPlans, RealV1PolicyCanGateOnCertificateFacts) { +#ifndef COSE_HAS_TRUST_PACK + GTEST_SKIP() << "trust pack not available"; +#else +#ifndef COSE_HAS_CERTIFICATES_PACK + GTEST_SKIP() << "COSE_HAS_CERTIFICATES_PACK not enabled"; +#else + cose::ValidatorBuilder builder; + ASSERT_EQ(cose_validator_builder_with_certificates_pack(builder.native_handle()), COSE_OK); + + cose::TrustPolicyBuilder policy(builder); + cose::RequireSigningCertificatePresent(policy); + policy.And(); + cose::RequireNotPqcAlgorithmOrMissing(policy); + + auto plan = policy.Compile(); + (void)plan; +#endif +#endif +} + +TEST(RealWorldTrustPlans, RealScittPolicyCanRequireCwtClaimsAndMstReceiptTrustedFromIssuer) { +#ifndef COSE_HAS_TRUST_PACK + GTEST_SKIP() << "trust pack not available"; +#else +#ifndef COSE_HAS_MST_PACK + GTEST_SKIP() << "COSE_HAS_MST_PACK not enabled"; +#else + cose::ValidatorBuilder builder; + + if (std::string(COSE_MST_JWKS_PATH).empty()) { + FAIL() << "COSE_MST_JWKS_PATH not set"; + } + + const auto jwks_json = read_file_bytes(COSE_MST_JWKS_PATH); + const std::string jwks_str(reinterpret_cast(jwks_json.data()), jwks_json.size()); + + { + cose_mst_trust_options_t opts; + opts.allow_network = false; + opts.offline_jwks_json = jwks_str.c_str(); + opts.jwks_api_version = nullptr; + + ASSERT_EQ(cose_validator_builder_with_mst_pack_ex(builder.native_handle(), &opts), COSE_OK); + } + +#ifdef COSE_HAS_CERTIFICATES_PACK + { + cose_certificate_trust_options_t cert_opts; + cert_opts.trust_embedded_chain_as_trusted = true; + cert_opts.identity_pinning_enabled = false; + cert_opts.allowed_thumbprints = nullptr; + cert_opts.pqc_algorithm_oids = nullptr; + + ASSERT_EQ(cose_validator_builder_with_certificates_pack_ex(builder.native_handle(), &cert_opts), COSE_OK); + } +#endif + + cose::TrustPolicyBuilder policy(builder); + policy.RequireCwtClaimsPresent(); + policy.And(); + cose::RequireMstReceiptTrustedFromIssuerContains(policy, "confidential-ledger.azure.com"); + + (void)policy.Compile(); +#endif +#endif +} + +TEST(RealWorldTrustPlans, RealV1PolicyCanValidateWithMstOnlyBypassingPrimarySignature) { +#ifndef COSE_HAS_TRUST_PACK + GTEST_SKIP() << "trust pack not available"; +#else +#ifndef COSE_HAS_MST_PACK + GTEST_SKIP() << "COSE_HAS_MST_PACK not enabled"; +#else + if (std::string(COSE_TESTDATA_V1_DIR).empty()) { + FAIL() << "COSE_TESTDATA_V1_DIR not set"; + } + + if (std::string(COSE_MST_JWKS_PATH).empty()) { + FAIL() << "COSE_MST_JWKS_PATH not set"; + } + + cose::ValidatorBuilder builder; + + const auto jwks_json = read_file_bytes(COSE_MST_JWKS_PATH); + const std::string jwks_str(reinterpret_cast(jwks_json.data()), jwks_json.size()); + + { + cose_mst_trust_options_t opts; + opts.allow_network = false; + opts.offline_jwks_json = jwks_str.c_str(); + opts.jwks_api_version = nullptr; + + ASSERT_EQ(cose_validator_builder_with_mst_pack_ex(builder.native_handle(), &opts), COSE_OK); + } + + // Use the MST pack default trust plan. + cose::TrustPlanBuilder plan_builder(builder); + plan_builder.AddAllPackDefaultPlans(); + auto plan = plan_builder.CompileAnd(); + cose::WithCompiledTrustPlan(builder, plan); + + auto validator = builder.Build(); + + for (const auto* file : {"2ts-statement.scitt", "1ts-statement.scitt"}) { + const auto path = join_path2(COSE_TESTDATA_V1_DIR, file); + const auto cose_bytes = read_file_bytes(path); + auto result = validator.Validate(cose_bytes); + ASSERT_TRUE(result.Ok()) << "expected success for " << file << ", got failure: " + << result.FailureMessage(); + } +#endif +#endif +} diff --git a/native/c_pp/tests/real_world_trust_plans_test.cpp b/native/c_pp/tests/real_world_trust_plans_test.cpp new file mode 100644 index 00000000..e7c7c906 --- /dev/null +++ b/native/c_pp/tests/real_world_trust_plans_test.cpp @@ -0,0 +1,300 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include + +#ifdef COSE_HAS_CERTIFICATES_PACK +#include +#endif + +#ifdef COSE_HAS_MST_PACK +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#ifndef COSE_TESTDATA_V1_DIR +#define COSE_TESTDATA_V1_DIR "" +#endif + +#ifndef COSE_MST_JWKS_PATH +#define COSE_MST_JWKS_PATH "" +#endif + +static std::vector read_file_bytes(const std::string& path) { + std::ifstream f(path, std::ios::binary); + if (!f) { + throw std::runtime_error("failed to open file: " + path); + } + + f.seekg(0, std::ios::end); + auto size = f.tellg(); + if (size < 0) { + throw std::runtime_error("failed to stat file: " + path); + } + + f.seekg(0, std::ios::beg); + std::vector out(static_cast(size)); + if (!out.empty()) { + f.read(reinterpret_cast(out.data()), static_cast(out.size())); + if (!f) { + throw std::runtime_error("failed to read file: " + path); + } + } + + return out; +} + +static std::string join_path2(const std::string& a, const std::string& b) { + if (a.empty()) return b; + const char last = a.back(); + if (last == '/' || last == '\\') return a + b; + return a + "/" + b; +} + +static void test_compile_fails_when_required_pack_missing() { +#ifndef COSE_HAS_CERTIFICATES_PACK + std::cout << "SKIP: " << __func__ << " (COSE_HAS_CERTIFICATES_PACK not enabled)\n"; + return; +#else + // Certificates pack is linked, but NOT configured on the builder. + // Requiring a certificates-only fact should fail. + cose::ValidatorBuilder builder; + cose::TrustPolicyBuilder policy(builder); + + try { + cose::RequireX509ChainTrusted(policy); + (void)policy.Compile(); + throw std::runtime_error("expected policy.Compile() to throw"); + } catch (const cose::cose_error&) { + // ok + } +#endif +} + +static void test_compile_succeeds_when_required_pack_present() { +#ifndef COSE_HAS_CERTIFICATES_PACK + std::cout << "SKIP: " << __func__ << " (COSE_HAS_CERTIFICATES_PACK not enabled)\n"; + return; +#else + cose::ValidatorBuilder builder; + // Add cert pack to builder using the pack's C API. + if (cose_validator_builder_with_certificates_pack(builder.native_handle()) != COSE_OK) { + throw cose::cose_error(COSE_ERR); + } + + cose::TrustPolicyBuilder policy(builder); + cose::RequireX509ChainTrusted(policy); + + auto plan = policy.Compile(); + cose::WithCompiledTrustPlan(builder, plan); + + auto validator = builder.Build(); + (void)validator; +#endif +} + +static void test_real_v1_policy_can_gate_on_certificate_facts() { +#ifndef COSE_HAS_CERTIFICATES_PACK + std::cout << "SKIP: " << __func__ << " (COSE_HAS_CERTIFICATES_PACK not enabled)\n"; + return; +#else + cose::ValidatorBuilder builder; + if (cose_validator_builder_with_certificates_pack(builder.native_handle()) != COSE_OK) { + throw cose::cose_error(COSE_ERR); + } + + cose::TrustPolicyBuilder policy(builder); + cose::RequireSigningCertificatePresent(policy); + policy.And(); + cose::RequireNotPqcAlgorithmOrMissing(policy); + + auto plan = policy.Compile(); + (void)plan; +#endif +} + +static void test_real_scitt_policy_can_require_cwt_claims_and_mst_receipt_trusted_from_issuer() { +#ifndef COSE_HAS_MST_PACK + std::cout << "SKIP: " << __func__ << " (COSE_HAS_MST_PACK not enabled)\n"; + return; +#else + cose::ValidatorBuilder builder; + + if (std::string(COSE_MST_JWKS_PATH).empty()) { + throw std::runtime_error("COSE_MST_JWKS_PATH not set"); + } + + const auto jwks_json = read_file_bytes(COSE_MST_JWKS_PATH); + const std::string jwks_str(reinterpret_cast(jwks_json.data()), jwks_json.size()); + + cose::MstOptions mst; + mst.allow_network = false; + mst.offline_jwks_json = jwks_str; + + // Add packs using the C API; avoids needing a multi-pack C++ builder. + { + cose_mst_trust_options_t opts; + opts.allow_network = mst.allow_network; + opts.offline_jwks_json = mst.offline_jwks_json.c_str(); + opts.jwks_api_version = nullptr; + + if (cose_validator_builder_with_mst_pack_ex(builder.native_handle(), &opts) != COSE_OK) { + throw cose::cose_error(COSE_ERR); + } + } + +#ifdef COSE_HAS_CERTIFICATES_PACK + { + cose_certificate_trust_options_t cert_opts; + cert_opts.trust_embedded_chain_as_trusted = true; + cert_opts.identity_pinning_enabled = false; + cert_opts.allowed_thumbprints = nullptr; + cert_opts.pqc_algorithm_oids = nullptr; + + if (cose_validator_builder_with_certificates_pack_ex(builder.native_handle(), &cert_opts) != COSE_OK) { + throw cose::cose_error(COSE_ERR); + } + } +#endif + + cose::TrustPolicyBuilder policy(builder); + policy.RequireCwtClaimsPresent(); + policy.And(); + cose::RequireMstReceiptTrustedFromIssuerContains(policy, "confidential-ledger.azure.com"); + + // This is a policy-shape compilation test (projected helpers exist and compile). + (void)policy.Compile(); +#endif +} + +static void test_real_v1_policy_can_validate_with_mst_only_by_bypassing_primary_signature() { +#ifndef COSE_HAS_MST_PACK + std::cout << "SKIP: " << __func__ << " (COSE_HAS_MST_PACK not enabled)\n"; + return; +#else + cose::ValidatorBuilder builder; + + const auto jwks_json = read_file_bytes(COSE_MST_JWKS_PATH); + const std::string jwks_str(reinterpret_cast(jwks_json.data()), jwks_json.size()); + + { + cose_mst_trust_options_t opts; + opts.allow_network = false; + opts.offline_jwks_json = jwks_str.c_str(); + opts.jwks_api_version = nullptr; + + if (cose_validator_builder_with_mst_pack_ex(builder.native_handle(), &opts) != COSE_OK) { + throw cose::cose_error(COSE_ERR); + } + } + + // Use the MST pack default trust plan (native analogue to Rust's TrustPlanBuilder MST-only test). + cose::TrustPlanBuilder plan_builder(builder); + plan_builder.AddAllPackDefaultPlans(); + auto plan = plan_builder.CompileAnd(); + cose::WithCompiledTrustPlan(builder, plan); + + auto validator = builder.Build(); + + for (const auto* file : {"2ts-statement.scitt", "1ts-statement.scitt"}) { + const auto path = join_path2(COSE_TESTDATA_V1_DIR, file); + const auto cose_bytes = read_file_bytes(path); + auto result = validator.Validate(cose_bytes); + if (!result.Ok()) { + throw std::runtime_error( + std::string("expected success for ") + file + ", got failure: " + result.FailureMessage() + ); + } + } +#endif +} + +using test_fn_t = void (*)(); + +struct test_case_t { + const char* name; + test_fn_t fn; +}; + +static const test_case_t g_tests[] = { + {"compile_fails_when_required_pack_missing", test_compile_fails_when_required_pack_missing}, + {"compile_succeeds_when_required_pack_present", test_compile_succeeds_when_required_pack_present}, + {"real_v1_policy_can_gate_on_certificate_facts", test_real_v1_policy_can_gate_on_certificate_facts}, + {"real_scitt_policy_can_require_cwt_claims_and_mst_receipt_trusted_from_issuer", test_real_scitt_policy_can_require_cwt_claims_and_mst_receipt_trusted_from_issuer}, + {"real_v1_policy_can_validate_with_mst_only_by_bypassing_primary_signature", test_real_v1_policy_can_validate_with_mst_only_by_bypassing_primary_signature}, +}; + +static void usage(const char* argv0) { + std::cerr << "Usage:\n"; + std::cerr << " " << argv0 << " [--list] [--test ]\n"; +} + +static void list_tests() { + for (const auto& t : g_tests) { + std::cout << t.name << "\n"; + } +} + +static int run_one(const std::string& name) { + for (const auto& t : g_tests) { + if (name == t.name) { + std::cout << "RUN: " << t.name << "\n"; + t.fn(); + std::cout << "PASS: " << t.name << "\n"; + return 0; + } + } + + std::cerr << "Unknown test: " << name << "\n"; + return 2; +} + +int main(int argc, char** argv) { +#ifndef COSE_HAS_TRUST_PACK + std::cout << "Skipping: trust pack not available\n"; + return 0; +#else + try { + // Minimal subtest runner so CTest can show 1 result per test function. + // - no args: run all tests + // - --list: list tests + // - --test : run one test + if (argc == 2 && std::string(argv[1]) == "--list") { + list_tests(); + return 0; + } + + if (argc == 3 && std::string(argv[1]) == "--test") { + return run_one(argv[2]); + } + + if (argc != 1) { + usage(argv[0]); + return 2; + } + + for (const auto& t : g_tests) { + const int rc = run_one(t.name); + if (rc != 0) { + return rc; + } + } + + std::cout << "OK\n"; + return 0; + } catch (const cose::cose_error& e) { + std::cerr << "cose_error: " << e.what() << "\n"; + return 1; + } catch (const std::exception& e) { + std::cerr << "std::exception: " << e.what() << "\n"; + return 1; + } +#endif +} diff --git a/native/c_pp/tests/smoke_test.cpp b/native/c_pp/tests/smoke_test.cpp new file mode 100644 index 00000000..ed6d9856 --- /dev/null +++ b/native/c_pp/tests/smoke_test.cpp @@ -0,0 +1,240 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include +#include +#include + +int main() { + try { + std::cout << "COSE C++ API Smoke Test\n"; + std::cout << "ABI Version: " << cose_ffi_abi_version() << "\n"; + + // Test 1: Basic builder + { + auto builder = cose::ValidatorBuilder(); + auto validator = builder.Build(); + std::cout << "✓ Basic validator built\n"; + } + +#ifdef COSE_HAS_CERTIFICATES_PACK + // Test 2: Builder with certificates pack (default options) + { + auto builder = cose::ValidatorBuilderWithCertificates(); + builder.WithCertificates(); + auto validator = builder.Build(); + std::cout << "✓ Validator with certificates pack built\n"; + } + + // Test 3: Builder with custom certificate options + { + cose::CertificateOptions opts; + opts.trust_embedded_chain_as_trusted = true; + opts.allowed_thumbprints = {"ABCD1234"}; + + auto builder = cose::ValidatorBuilderWithCertificates(); + builder.WithCertificates(opts); + auto validator = builder.Build(); + std::cout << "✓ Validator with custom certificate options built\n"; + } +#endif + +#ifdef COSE_HAS_MST_PACK + // Test 4: Builder with MST pack + { + auto builder = cose::ValidatorBuilderWithMst(); + builder.WithMst(); + auto validator = builder.Build(); + std::cout << "✓ Validator with MST pack built\n"; + } + + // Test 5: Builder with custom MST options + { + cose::MstOptions opts; + opts.allow_network = false; + opts.offline_jwks_json = R"({"keys":[]})"; + + auto builder = cose::ValidatorBuilderWithMst(); + builder.WithMst(opts); + auto validator = builder.Build(); + std::cout << "✓ Validator with custom MST options built\n"; + } +#endif + +#ifdef COSE_HAS_AKV_PACK + // Test 6: Builder with AKV pack + { + auto builder = cose::ValidatorBuilderWithAzureKeyVault(); + builder.WithAzureKeyVault(); + auto validator = builder.Build(); + std::cout << "✓ Validator with AKV pack built\n"; + } +#endif + +#ifdef COSE_HAS_TRUST_PACK + // Test 7: Compile and attach a bundled trust plan + { +#ifdef COSE_HAS_CERTIFICATES_PACK + auto builder = cose::ValidatorBuilderWithCertificates(); + builder.WithCertificates(); +#else + auto builder = cose::ValidatorBuilder(); +#endif + + auto tp = cose::TrustPlanBuilder(builder); + auto plan = tp.AddAllPackDefaultPlans().CompileOr(); + cose::WithCompiledTrustPlan(builder, plan); + + auto validator = builder.Build(); + (void)validator; + std::cout << "✓ Bundled trust plan compiled and attached\n"; + } + + // Test 8: AllowAll/DenyAll plan compilation (no attach) + { + auto builder = cose::ValidatorBuilder(); + auto tp = cose::TrustPlanBuilder(builder); + + auto allow_all = tp.CompileAllowAll(); + (void)allow_all; + + auto deny_all = tp.CompileDenyAll(); + (void)deny_all; + + std::cout << "✓ AllowAll/DenyAll plans compiled\n"; + } + + // Test 9: Compile and attach a custom trust policy (message-scope requirements) + { + auto builder = cose::ValidatorBuilder(); + +#ifdef COSE_HAS_CERTIFICATES_PACK + { + cose_status_t status = cose_validator_builder_with_certificates_pack(builder.native_handle()); + if (status != COSE_OK) { + throw cose::cose_error(status); + } + } +#endif + +#ifdef COSE_HAS_MST_PACK + { + cose_status_t status = cose_validator_builder_with_mst_pack(builder.native_handle()); + if (status != COSE_OK) { + throw cose::cose_error(status); + } + } +#endif + +#ifdef COSE_HAS_AKV_PACK + { + cose_status_t status = cose_validator_builder_with_akv_pack(builder.native_handle()); + if (status != COSE_OK) { + throw cose::cose_error(status); + } + } +#endif + auto policy = cose::TrustPolicyBuilder(builder); + +#ifdef COSE_HAS_CERTIFICATES_PACK + cose::RequireX509ChainTrusted(policy); + cose::RequireX509ChainBuilt(policy); + cose::RequireX509ChainElementCountEq(policy, 1); + cose::RequireX509ChainStatusFlagsEq(policy, 0); + cose::RequireLeafChainThumbprintPresent(policy); + cose::RequireSigningCertificatePresent(policy); + cose::RequireLeafSubjectEq(policy, "CN=example"); + cose::RequireIssuerSubjectEq(policy, "CN=issuer.example"); + cose::RequireSigningCertificateSubjectIssuerMatchesLeafChainElement(policy); + cose::RequireLeafIssuerIsNextChainSubjectOptional(policy); + cose::RequireSigningCertificateThumbprintEq(policy, "ABCD1234"); + cose::RequireSigningCertificateThumbprintPresent(policy); + cose::RequireSigningCertificateSubjectEq(policy, "CN=example"); + cose::RequireSigningCertificateIssuerEq(policy, "CN=issuer.example"); + cose::RequireSigningCertificateSerialNumberEq(policy, "01"); + cose::RequireSigningCertificateValidAt(policy, 0); + cose::RequireSigningCertificateExpiredAtOrBefore(policy, 0); + cose::RequireSigningCertificateNotBeforeLe(policy, 0); + cose::RequireSigningCertificateNotBeforeGe(policy, 0); + cose::RequireSigningCertificateNotAfterLe(policy, 0); + cose::RequireSigningCertificateNotAfterGe(policy, 0); + cose::RequireChainElementSubjectEq(policy, 0, "CN=example"); + cose::RequireChainElementIssuerEq(policy, 0, "CN=issuer.example"); + cose::RequireChainElementThumbprintPresent(policy, 0); + cose::RequireChainElementThumbprintEq(policy, 0, "ABCD1234"); + cose::RequireChainElementValidAt(policy, 0, 0); + cose::RequireChainElementNotBeforeLe(policy, 0, 0); + cose::RequireChainElementNotBeforeGe(policy, 0, 0); + cose::RequireChainElementNotAfterLe(policy, 0, 0); + cose::RequireChainElementNotAfterGe(policy, 0, 0); + cose::RequireNotPqcAlgorithmOrMissing(policy); + cose::RequireX509PublicKeyAlgorithmThumbprintEq(policy, "ABCD1234"); + cose::RequireX509PublicKeyAlgorithmOidEq(policy, "1.2.840.113549.1.1.1"); + cose::RequireX509PublicKeyAlgorithmIsNotPqc(policy); +#endif + +#ifdef COSE_HAS_MST_PACK + cose::RequireMstReceiptPresent(policy); + cose::RequireMstReceiptNotPresent(policy); + cose::RequireMstReceiptSignatureVerified(policy); + cose::RequireMstReceiptSignatureNotVerified(policy); + cose::RequireMstReceiptIssuerContains(policy, "microsoft"); + cose::RequireMstReceiptIssuerEq(policy, "issuer.example"); + cose::RequireMstReceiptKidEq(policy, "kid.example"); + cose::RequireMstReceiptKidContains(policy, "kid"); + cose::RequireMstReceiptTrusted(policy); + cose::RequireMstReceiptNotTrusted(policy); + cose::RequireMstReceiptTrustedFromIssuerContains(policy, "microsoft"); + cose::RequireMstReceiptStatementSha256Eq( + policy, + "0000000000000000000000000000000000000000000000000000000000000000"); + cose::RequireMstReceiptStatementCoverageEq(policy, "coverage.example"); + cose::RequireMstReceiptStatementCoverageContains(policy, "example"); +#endif + + #ifdef COSE_HAS_AKV_PACK + cose::RequireAzureKeyVaultKid(policy); + cose::RequireAzureKeyVaultKidAllowed(policy); + cose::RequireNotAzureKeyVaultKid(policy); + cose::RequireAzureKeyVaultKidNotAllowed(policy); + #endif + + auto plan = policy + .RequireDetachedPayloadAbsent() + .RequireCwtClaimsPresent() + .RequireCwtIssEq("issuer.example") + .RequireCwtClaimLabelPresent(6) + .RequireCwtClaimLabelI64Ge(6, 123) + .RequireCwtClaimLabelBoolEq(6, true) + .RequireCwtClaimTextStrEq("nonce", "abc") + .RequireCwtClaimTextStrStartsWith("nonce", "a") + .RequireCwtClaimTextStrContains("nonce", "b") + .RequireCwtClaimLabelStrStartsWith(1000, "a") + .RequireCwtClaimLabelStrContains(1000, "b") + .RequireCwtClaimLabelStrEq(1000, "exact.example") + .RequireCwtClaimTextI64Le("nonce", 0) + .RequireCwtClaimTextI64Eq("nonce", 0) + .RequireCwtClaimTextBoolEq("nonce", true) + .RequireCwtExpGe(0) + .RequireCwtIatLe(0) + .RequireCounterSignatureEnvelopeSigStructureIntactOrMissing() + .Compile(); + cose::WithCompiledTrustPlan(builder, plan); + + auto validator = builder.Build(); + (void)validator; + std::cout << "✓ Custom trust policy compiled and attached\n"; + } +#endif + + std::cout << "\n✅ All C++ smoke tests passed\n"; + return 0; + + } catch (const cose::cose_error& e) { + std::cerr << "COSE error: " << e.what() << "\n"; + return 1; + } catch (const std::exception& e) { + std::cerr << "Exception: " << e.what() << "\n"; + return 1; + } +} diff --git a/native/c_pp/tests/smoke_test_gtest.cpp b/native/c_pp/tests/smoke_test_gtest.cpp new file mode 100644 index 00000000..f355da7c --- /dev/null +++ b/native/c_pp/tests/smoke_test_gtest.cpp @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include + +#include + +TEST(Smoke, AbiVersionAvailable) { + EXPECT_GT(cose_ffi_abi_version(), 0u); +} + +TEST(Smoke, BasicValidatorBuilds) { + auto builder = cose::ValidatorBuilder(); + auto validator = builder.Build(); + (void)validator; +} + +#ifdef COSE_HAS_CERTIFICATES_PACK +TEST(Smoke, CertificatesPackBuildsDefault) { + auto builder = cose::ValidatorBuilderWithCertificates(); + builder.WithCertificates(); + auto validator = builder.Build(); + (void)validator; +} + +TEST(Smoke, CertificatesPackBuildsCustomOptions) { + cose::CertificateOptions opts; + opts.trust_embedded_chain_as_trusted = true; + opts.allowed_thumbprints = {"ABCD1234"}; + + auto builder = cose::ValidatorBuilderWithCertificates(); + builder.WithCertificates(opts); + auto validator = builder.Build(); + (void)validator; +} +#endif + +#ifdef COSE_HAS_MST_PACK +TEST(Smoke, MstPackBuildsDefault) { + auto builder = cose::ValidatorBuilderWithMst(); + builder.WithMst(); + auto validator = builder.Build(); + (void)validator; +} + +TEST(Smoke, MstPackBuildsCustomOptions) { + cose::MstOptions opts; + opts.allow_network = false; + opts.offline_jwks_json = R"({"keys":[]})"; + + auto builder = cose::ValidatorBuilderWithMst(); + builder.WithMst(opts); + auto validator = builder.Build(); + (void)validator; +} +#endif + +#ifdef COSE_HAS_AKV_PACK +TEST(Smoke, AkvPackBuildsDefault) { + auto builder = cose::ValidatorBuilderWithAzureKeyVault(); + builder.WithAzureKeyVault(); + auto validator = builder.Build(); + (void)validator; +} +#endif + +#ifdef COSE_HAS_TRUST_PACK +TEST(Smoke, BundledTrustPlanCompilesAndAttaches) { +#ifdef COSE_HAS_CERTIFICATES_PACK + auto builder = cose::ValidatorBuilderWithCertificates(); + builder.WithCertificates(); +#else + auto builder = cose::ValidatorBuilder(); +#endif + + auto tp = cose::TrustPlanBuilder(builder); + auto plan = tp.AddAllPackDefaultPlans().CompileOr(); + cose::WithCompiledTrustPlan(builder, plan); + + auto validator = builder.Build(); + (void)validator; +} + +TEST(Smoke, AllowAllAndDenyAllPlansCompile) { + auto builder = cose::ValidatorBuilder(); + auto tp = cose::TrustPlanBuilder(builder); + + auto allow_all = tp.CompileAllowAll(); + (void)allow_all; + + auto deny_all = tp.CompileDenyAll(); + (void)deny_all; +} + +TEST(Smoke, CustomTrustPolicyCompilesAndAttaches) { + auto builder = cose::ValidatorBuilder(); + +#ifdef COSE_HAS_CERTIFICATES_PACK + ASSERT_EQ(cose_validator_builder_with_certificates_pack(builder.native_handle()), COSE_OK); +#endif +#ifdef COSE_HAS_MST_PACK + ASSERT_EQ(cose_validator_builder_with_mst_pack(builder.native_handle()), COSE_OK); +#endif +#ifdef COSE_HAS_AKV_PACK + ASSERT_EQ(cose_validator_builder_with_akv_pack(builder.native_handle()), COSE_OK); +#endif + + auto policy = cose::TrustPolicyBuilder(builder); + +#ifdef COSE_HAS_CERTIFICATES_PACK + cose::RequireX509ChainTrusted(policy); + cose::RequireX509ChainBuilt(policy); + cose::RequireX509ChainElementCountEq(policy, 1); + cose::RequireX509ChainStatusFlagsEq(policy, 0); + cose::RequireLeafChainThumbprintPresent(policy); + cose::RequireSigningCertificatePresent(policy); + cose::RequireLeafSubjectEq(policy, "CN=example"); + cose::RequireIssuerSubjectEq(policy, "CN=issuer.example"); + cose::RequireSigningCertificateSubjectIssuerMatchesLeafChainElement(policy); + cose::RequireLeafIssuerIsNextChainSubjectOptional(policy); + cose::RequireSigningCertificateThumbprintEq(policy, "ABCD1234"); + cose::RequireSigningCertificateThumbprintPresent(policy); + cose::RequireSigningCertificateSubjectEq(policy, "CN=example"); + cose::RequireSigningCertificateIssuerEq(policy, "CN=issuer.example"); + cose::RequireSigningCertificateSerialNumberEq(policy, "01"); + cose::RequireSigningCertificateValidAt(policy, 0); + cose::RequireSigningCertificateExpiredAtOrBefore(policy, 0); + cose::RequireSigningCertificateNotBeforeLe(policy, 0); + cose::RequireSigningCertificateNotBeforeGe(policy, 0); + cose::RequireSigningCertificateNotAfterLe(policy, 0); + cose::RequireSigningCertificateNotAfterGe(policy, 0); + cose::RequireChainElementSubjectEq(policy, 0, "CN=example"); + cose::RequireChainElementIssuerEq(policy, 0, "CN=issuer.example"); + cose::RequireChainElementThumbprintPresent(policy, 0); + cose::RequireChainElementThumbprintEq(policy, 0, "ABCD1234"); + cose::RequireChainElementValidAt(policy, 0, 0); + cose::RequireChainElementNotBeforeLe(policy, 0, 0); + cose::RequireChainElementNotBeforeGe(policy, 0, 0); + cose::RequireChainElementNotAfterLe(policy, 0, 0); + cose::RequireChainElementNotAfterGe(policy, 0, 0); + cose::RequireNotPqcAlgorithmOrMissing(policy); + cose::RequireX509PublicKeyAlgorithmThumbprintEq(policy, "ABCD1234"); + cose::RequireX509PublicKeyAlgorithmOidEq(policy, "1.2.840.113549.1.1.1"); + cose::RequireX509PublicKeyAlgorithmIsNotPqc(policy); +#endif + +#ifdef COSE_HAS_MST_PACK + cose::RequireMstReceiptPresent(policy); + cose::RequireMstReceiptNotPresent(policy); + cose::RequireMstReceiptSignatureVerified(policy); + cose::RequireMstReceiptSignatureNotVerified(policy); + cose::RequireMstReceiptIssuerContains(policy, "microsoft"); + cose::RequireMstReceiptIssuerEq(policy, "issuer.example"); + cose::RequireMstReceiptKidEq(policy, "kid.example"); + cose::RequireMstReceiptKidContains(policy, "kid"); + cose::RequireMstReceiptTrusted(policy); + cose::RequireMstReceiptNotTrusted(policy); + cose::RequireMstReceiptTrustedFromIssuerContains(policy, "microsoft"); + cose::RequireMstReceiptStatementSha256Eq( + policy, + "0000000000000000000000000000000000000000000000000000000000000000"); + cose::RequireMstReceiptStatementCoverageEq(policy, "coverage.example"); + cose::RequireMstReceiptStatementCoverageContains(policy, "example"); +#endif + +#ifdef COSE_HAS_AKV_PACK + cose::RequireAzureKeyVaultKid(policy); + cose::RequireAzureKeyVaultKidAllowed(policy); + cose::RequireNotAzureKeyVaultKid(policy); + cose::RequireAzureKeyVaultKidNotAllowed(policy); +#endif + + auto plan = policy + .RequireDetachedPayloadAbsent() + .RequireCwtClaimsPresent() + .RequireCwtIssEq("issuer.example") + .RequireCwtClaimLabelPresent(6) + .RequireCwtClaimLabelI64Ge(6, 123) + .RequireCwtClaimLabelBoolEq(6, true) + .RequireCwtClaimTextStrEq("nonce", "abc") + .RequireCwtClaimTextStrStartsWith("nonce", "a") + .RequireCwtClaimTextStrContains("nonce", "b") + .RequireCwtClaimLabelStrStartsWith(1000, "a") + .RequireCwtClaimLabelStrContains(1000, "b") + .RequireCwtClaimLabelStrEq(1000, "exact.example") + .RequireCwtClaimTextI64Le("nonce", 0) + .RequireCwtClaimTextI64Eq("nonce", 0) + .RequireCwtClaimTextBoolEq("nonce", true) + .RequireCwtExpGe(0) + .RequireCwtIatLe(0) + .RequireCounterSignatureEnvelopeSigStructureIntactOrMissing() + .Compile(); + + cose::WithCompiledTrustPlan(builder, plan); + + auto validator = builder.Build(); + (void)validator; +} +#endif diff --git a/native/collect-coverage-asan.ps1 b/native/collect-coverage-asan.ps1 new file mode 100644 index 00000000..9a6ed0c0 --- /dev/null +++ b/native/collect-coverage-asan.ps1 @@ -0,0 +1,154 @@ +[CmdletBinding()] +param( + [ValidateSet('Debug', 'Release', 'RelWithDebInfo')] + [string]$Configuration = 'Debug', + + [ValidateRange(0, 100)] + [int]$MinimumLineCoveragePercent = 95, + + # Build the Rust FFI DLLs first (required for native C/C++ tests). + [switch]$BuildRust = $true +) + +$ErrorActionPreference = 'Stop' + +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 Get-VsInstallationPath { + $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 + } + + $vsPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + if ($LASTEXITCODE -ne 0 -or -not $vsPath) { + $vsPath = & $vswhere -latest -products * -property installationPath + } + + if (-not $vsPath) { + return $null + } + + $vsPath = ($vsPath | Select-Object -First 1).Trim() + if (-not $vsPath) { + return $null + } + + if (-not (Test-Path $vsPath)) { + return $null + } + + return $vsPath +} + +function Add-VsAsanRuntimeToPath { + if (-not ($env:OS -eq 'Windows_NT')) { + return + } + + $vsPath = Get-VsInstallationPath + if (-not $vsPath) { + return + } + + # On MSVC, /fsanitize=address depends on clang ASAN runtime DLLs that ship with VS. + # If they're not on PATH, Windows shows modal popup dialogs and tests fail with 0xc0000135. + $candidateDirs = @() + + $msvcToolsRoot = Join-Path $vsPath 'VC\Tools\MSVC' + if (Test-Path $msvcToolsRoot) { + $latestMsvc = Get-ChildItem -Path $msvcToolsRoot -Directory -ErrorAction SilentlyContinue | + Sort-Object Name -Descending | + Select-Object -First 1 + if ($latestMsvc) { + $candidateDirs += (Join-Path $latestMsvc.FullName 'bin\Hostx64\x64') + $candidateDirs += (Join-Path $latestMsvc.FullName 'bin\Hostx64\x86') + } + } + + $llvmRoot = Join-Path $vsPath 'VC\Tools\Llvm' + if (Test-Path $llvmRoot) { + $candidateDirs += (Join-Path $llvmRoot 'x64\bin') + $clangLibRoot = Join-Path $llvmRoot 'x64\lib\clang' + if (Test-Path $clangLibRoot) { + $latestClang = Get-ChildItem -Path $clangLibRoot -Directory -ErrorAction SilentlyContinue | + Sort-Object Name -Descending | + Select-Object -First 1 + if ($latestClang) { + $candidateDirs += (Join-Path $latestClang.FullName 'lib\windows') + } + } + } + + $asanDllName = 'clang_rt.asan_dynamic-x86_64.dll' + foreach ($dir in ($candidateDirs | Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique)) { + if (Test-Path (Join-Path $dir $asanDllName)) { + if ($env:PATH -notlike "${dir}*") { + $env:PATH = "${dir};$env:PATH" + Write-Host "Using ASAN runtime from: $dir" -ForegroundColor Yellow + } + return + } + } +} + +$repoRoot = Split-Path -Parent $PSScriptRoot + +# Ensure ASAN runtime is available for all phases, including Rust-dependency C code. +Add-VsAsanRuntimeToPath + +# When running under the ASAN pipeline, also build any C/C++ code compiled by Rust crates +# (e.g., PQClean via pqcrypto-*) with AddressSanitizer enabled. This helps catch memory +# issues inside those vendored C implementations. +$prevCFlags = ${env:CFLAGS_x86_64-pc-windows-msvc} +$prevCxxFlags = ${env:CXXFLAGS_x86_64-pc-windows-msvc} +${env:CFLAGS_x86_64-pc-windows-msvc} = '/fsanitize=address' +${env:CXXFLAGS_x86_64-pc-windows-msvc} = '/fsanitize=address' + +try { + if ($BuildRust) { + Push-Location (Join-Path $PSScriptRoot 'rust') + try { + cargo build --release -p cose_sign1_validation_ffi -p cose_sign1_validation_ffi_certificates -p cose_sign1_validation_ffi_mst -p cose_sign1_validation_ffi_akv -p cose_sign1_validation_ffi_trust + + # Explicitly compile the PQClean-backed PQC implementation under ASAN, even though it's + # feature-gated and not built by default. + # This keeps the default coverage gates unchanged while still ensuring PQClean C is + # ASAN-instrumented in the ASAN pipeline. + cargo build --release -p cose_sign1_validation_certificates --features pqc-mldsa + } finally { + Pop-Location + } + } + + & (Join-Path $PSScriptRoot 'rust\collect-coverage.ps1') -FailUnderLines $MinimumLineCoveragePercent + & (Join-Path $PSScriptRoot 'c\collect-coverage.ps1') -Configuration $Configuration -MinimumLineCoveragePercent $MinimumLineCoveragePercent + & (Join-Path $PSScriptRoot 'c_pp\collect-coverage.ps1') -Configuration $Configuration -MinimumLineCoveragePercent $MinimumLineCoveragePercent +} finally { + ${env:CFLAGS_x86_64-pc-windows-msvc} = $prevCFlags + ${env:CXXFLAGS_x86_64-pc-windows-msvc} = $prevCxxFlags +} + +Write-Host "Native C + C++ coverage gates passed (Configuration=$Configuration, MinimumLineCoveragePercent=$MinimumLineCoveragePercent)." \ No newline at end of file diff --git a/native/docs/01-overview.md b/native/docs/01-overview.md new file mode 100644 index 00000000..10df20ff --- /dev/null +++ b/native/docs/01-overview.md @@ -0,0 +1,54 @@ +# Overview: repo layout and mental model + +## Mental model + +- **Rust is the implementation**. +- **Native projections are thin**: + - **C**: ABI-stable function surface + pack feature macros + - **C++**: header-only RAII wrappers + fluent builders +- **Everything is shipped through one vcpkg port**: + - The port builds the Rust FFI static libraries using `cargo`. + - The port installs C/C++ headers. + - The port provides CMake targets you link against. + +## Repository layout (native) + +- `native/rust/` + - Rust workspace (implementation + FFI crates) +- `native/c/` + - C projection headers + native tests + CMake build +- `native/c_pp/` + - C++ projection headers + native tests + CMake build +- `native/vcpkg_ports/cosesign1-validation-native/` + - Overlay port used to build/install everything via vcpkg + +## Packs (optional features) + +The native surface is modular: optional packs contribute additional validation facts and policy helpers. + +Current packs: + +- `certificates` (X.509) +- `mst` (Microsoft's Signing Transparency) +- `akv` (Azure Key Vault) +- `trust` (trust-policy / trust-plan authoring) + +On the C side these are exposed by compile definitions: + +- `COSE_HAS_CERTIFICATES_PACK` +- `COSE_HAS_MST_PACK` +- `COSE_HAS_AKV_PACK` +- `COSE_HAS_TRUST_PACK` + +When consuming via vcpkg+CMake, those definitions are applied automatically when the corresponding pack libs are present. + +## How the vcpkg port works + +The overlay port: + +- builds selected Rust FFI crates in both `debug` and `release` profiles +- installs the resulting **static libraries** into the vcpkg installed tree +- installs the C headers (and optionally the C++ headers) +- provides a CMake config package named `cose_sign1_validation` + +See [vcpkg consumption](03-vcpkg.md) for copy/paste usage. diff --git a/native/docs/02-rust-ffi.md b/native/docs/02-rust-ffi.md new file mode 100644 index 00000000..88990789 --- /dev/null +++ b/native/docs/02-rust-ffi.md @@ -0,0 +1,41 @@ +# Rust workspace + FFI crates + +## What lives where + +- `native/rust/` is a Cargo workspace. +- The “core” implementation crates are the source of truth. +- The `*_ffi*` crates build the C ABI boundary and are what native code links to. + +## Key crates (conceptual) + +- Base FFI crate: `cose_sign1_validation_ffi` +- Optional FFI crates (pinned behind vcpkg features): + - `cose_sign1_validation_ffi_certificates` + - `cose_sign1_validation_ffi_mst` + - `cose_sign1_validation_ffi_akv` + - `cose_sign1_validation_ffi_trust` + +## Build the Rust artifacts locally + +From repo root: + +```powershell +cd native/rust +cargo build --release --workspace +``` + +This produces libraries under: + +- `native/rust/target/release/` (release) +- `native/rust/target/debug/` (debug) + +## Why vcpkg is the recommended native entry point + +You *can* build Rust first and then build `native/c` or `native/c_pp` directly, but the recommended consumption story is: + +- use `vcpkg` to build/install the Rust FFI artifacts +- link to a single CMake package (`cose_sign1_validation`) and its targets + +This makes consuming apps reproducible and avoids custom ad-hoc “copy the right libs” steps. + +See [vcpkg consumption](03-vcpkg.md). diff --git a/native/docs/03-vcpkg.md b/native/docs/03-vcpkg.md new file mode 100644 index 00000000..924ac69f --- /dev/null +++ b/native/docs/03-vcpkg.md @@ -0,0 +1,77 @@ +# vcpkg: single-port native consumption + +## The port + +- vcpkg port name: `cosesign1-validation-native` +- CMake package name: `cose_sign1_validation` +- CMake targets: + - `cosesign1_validation_native::cose_sign1` (C) + - `cosesign1_validation_native::cose_sign1_cpp` (C++) when feature `cpp` is enabled + +The port is implemented as an **overlay port** in this repo at: + +- `native/vcpkg_ports/cosesign1-validation-native/` + +## Features (configuration options) + +Feature | Purpose | C compile define +---|---|--- +`cpp` | Install C++ projection headers + CMake target | (n/a) +`certificates` | Enable X.509 pack | `COSE_HAS_CERTIFICATES_PACK` +`mst` | Enable MST pack | `COSE_HAS_MST_PACK` +`akv` | Enable AKV pack | `COSE_HAS_AKV_PACK` +`trust` | Enable trust-policy/trust-plan pack | `COSE_HAS_TRUST_PACK` + +Defaults: `cpp, certificates`. + +## Install with overlay ports + +Assuming you have a vcpkg checkout at `C:\vcpkg` and this repo at `C:\src\repos\CoseSignTool`: + +```powershell +C:\vcpkg\vcpkg install cosesign1-validation-native[cpp,certificates,mst,akv,trust] --overlay-ports=C:\src\repos\CoseSignTool\native\vcpkg_ports +``` + +Notes: + +- The port runs `cargo build` internally. Ensure Rust is installed and on PATH. +- The port is **static-only** (it installs static libraries). + +## Use from CMake (toolchain) + +Configure your project with the vcpkg toolchain file: + +```powershell +cmake -S . -B out -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake +``` + +In your `CMakeLists.txt`: + +```cmake +find_package(cose_sign1_validation CONFIG REQUIRED) + +# C API +target_link_libraries(your_target PRIVATE cosesign1_validation_native::cose_sign1) + +# C++ API (requires feature "cpp") +target_link_libraries(your_cpp_target PRIVATE cosesign1_validation_native::cose_sign1_cpp) +``` + +The port’s config file also links required platform libs (e.g., Windows system libs) for the C target. + +## What gets installed + +- C headers under `include/cose/…` +- C++ headers under `include/cose/…` (when `cpp` is enabled) +- Rust FFI static libraries under `lib/` and `debug/lib/` + +## Development workflow tips + +- If you’re iterating on the port, prefer `--editable` workflows by pointing vcpkg at this repo and using overlay ports. +- If a vcpkg install seems stale, use: + +```powershell +C:\vcpkg\vcpkg remove cosesign1-validation-native +``` + +or bump the port version for internal testing. diff --git a/native/docs/04-c-projection.md b/native/docs/04-c-projection.md new file mode 100644 index 00000000..691cabfa --- /dev/null +++ b/native/docs/04-c-projection.md @@ -0,0 +1,56 @@ +# C projection + +For the full C developer guide (including vcpkg consumption and trust plans), see [native/c/docs/README.md](../c/docs/README.md). + +## Audience + +You want a stable C API you can call from C/C++ (or other languages that can call a C ABI). + +## API surface + +- Headers live in [native/c/include/cose/](../c/include/cose/). +- The core header is: + - `` +- Pack headers (optional): + - `` + - `` + - `` + - `` + +When consuming via vcpkg, the correct pack macros are defined automatically. + +## Quickstart (CMake + vcpkg) + +1) Install: + +```powershell +vcpkg install cosesign1-validation-native[certificates,mst,akv,trust] --overlay-ports=/native/vcpkg_ports +``` + +2) Link: + +```cmake +find_package(cose_sign1_validation CONFIG REQUIRED) + +target_link_libraries(your_target PRIVATE cosesign1_validation_native::cose_sign1) +``` + +## Error handling + +- Most APIs return a status code. +- When a call fails, you can fetch a human-readable error message: + +```c +char* msg = cose_last_error_message_utf8(); +// use msg +cose_string_free(msg); +``` + +Always free returned strings with `cose_string_free`. + +## Testing and examples + +- Examples: [native/c/examples/](../c/examples/) +- Tests: [native/c/tests/](../c/tests/) + +See [testing + ASAN + coverage](06-testing-coverage-asan.md). diff --git a/native/docs/05-cpp-projection.md b/native/docs/05-cpp-projection.md new file mode 100644 index 00000000..ae402d63 --- /dev/null +++ b/native/docs/05-cpp-projection.md @@ -0,0 +1,31 @@ +# C++ projection + +For the full C++ developer guide (including vcpkg consumption), see [native/c_pp/docs/README.md](../c_pp/docs/README.md). + +## Audience + +You want an ergonomic C++ wrapper over the C ABI with RAII and modern types. + +## API surface + +- Headers live in [native/c_pp/include/](../c_pp/include/). +- The library wraps the C projection and stays ABI-stable by delegating to the C ABI. + +Consume via vcpkg: + +```cmake +find_package(cose_sign1_validation CONFIG REQUIRED) + +target_link_libraries(your_target PRIVATE cosesign1_validation_native::cose_sign1_cpp) +``` + +## Notes on exceptions / error model + +The C++ API typically reports failures via return objects (and may throw only for programmer errors, depending on the wrapper). Follow the header docs for each type. + +## Testing and examples + +- Examples: [native/c_pp/examples/](../c_pp/examples/) +- Tests: [native/c_pp/tests/](../c_pp/tests/) + +See [testing + ASAN + coverage](06-testing-coverage-asan.md). diff --git a/native/docs/06-testing-coverage-asan.md b/native/docs/06-testing-coverage-asan.md new file mode 100644 index 00000000..37af246d --- /dev/null +++ b/native/docs/06-testing-coverage-asan.md @@ -0,0 +1,61 @@ +# Testing, coverage, and ASAN (Windows) + +This repo supports running native tests under MSVC AddressSanitizer (ASAN) and collecting line coverage on Windows using OpenCppCoverage. + +## Prerequisites + +- Visual Studio 2022 with C++ workload +- CMake + Ninja (or VS generator) +- Rust toolchain (for building the Rust FFI static libs) +- OpenCppCoverage + +## One-command runner + +From repo root: + +```powershell +./native/collect-coverage-asan.ps1 -Configuration Debug -MinimumLineCoveragePercent 95 +``` + +This: + +- builds required Rust FFI crates +- runs [native/c/collect-coverage.ps1](../c/collect-coverage.ps1) (C projection) +- runs [native/c_pp/collect-coverage.ps1](../c_pp/collect-coverage.ps1) (C++ projection) +- fails if either projection is < 95% **union** line coverage + +Runner script: [native/collect-coverage-asan.ps1](../collect-coverage-asan.ps1) + +It also builds any Rust dependencies that compile native C/C++ code with ASAN enabled (e.g., PQClean-backed PQC implementations used by feature-gated crates). + +## Why Debug? + +For header-heavy C++ wrappers, Debug tends to produce more reliable line mapping for OpenCppCoverage than optimized configurations. + +You still get ASAN’s memory checking in Debug. + +## Coverage output + +Each language script emits: + +- HTML report +- Cobertura XML + +The scripts compute a deduplicated union metric across all files by `(filename, lineNumber)` taking the maximum hit count. + +## Common failures + +### Missing ASAN runtime DLLs + +If tests fail to start with `0xc0000135` (or you see modal “missing DLL” popups), ASAN runtime DLLs are not being found. + +The scripts attempt to locate the Visual Studio ASAN runtime (e.g. `clang_rt.asan_dynamic-x86_64.dll`) and prepend its directory to `PATH` before running tests. + +If that detection fails: + +- ensure Visual Studio 2022 is installed with the C++ workload, or +- manually add the VS ASAN runtime directory to `PATH`. + +### Coverage is 0% + +Ensure the OpenCppCoverage command is invoked with child-process coverage enabled (CTest spawns test processes). The scripts already pass `--cover_children`. diff --git a/native/docs/07-troubleshooting.md b/native/docs/07-troubleshooting.md new file mode 100644 index 00000000..bef9592b --- /dev/null +++ b/native/docs/07-troubleshooting.md @@ -0,0 +1,33 @@ +# Troubleshooting + +## vcpkg can’t find the port + +This repo ships an overlay port under [native/vcpkg_ports](../vcpkg_ports). + +Example: + +```powershell +vcpkg install cosesign1-validation-native --overlay-ports=/native/vcpkg_ports +``` + +## Rust target mismatch + +The vcpkg port maps the vcpkg triplet to a Rust target triple. If you use a custom triplet, ensure the port knows how to map it (see [native/vcpkg_ports/cosesign1-validation-native/portfile.cmake](../vcpkg_ports/cosesign1-validation-native/portfile.cmake)). + +## Linker errors about CRT mismatch + +The port enforces static linkage on the vcpkg side. Ensure your consuming project uses a compatible runtime library selection. + +## OpenCppCoverage not found + +The coverage scripts try: + +- `OPENCPPCOVERAGE_PATH` +- `OpenCppCoverage.exe` on `PATH` +- common install locations + +Install via Chocolatey: + +```powershell +choco install opencppcoverage +``` diff --git a/native/docs/ARCHITECTURE.md b/native/docs/ARCHITECTURE.md new file mode 100644 index 00000000..235bdd3a --- /dev/null +++ b/native/docs/ARCHITECTURE.md @@ -0,0 +1,396 @@ +# Native FFI Architecture + +This document describes the complete architecture of the native (C/C++) projections for the COSE Sign1 validation library. + +## Overview + +The native projections provide three layers of abstraction: +1. **Rust FFI Layer**: C ABI exports from Rust using `extern "C"` +2. **C Projection**: Direct C API wrapping the FFI layer +3. **C++ Projection**: RAII wrappers providing modern C++ idioms + +All three layers follow a **per-pack modular architecture**, allowing consumers to include and link only the functionality they need. + +## Per-Pack Modularity + +The library is organized into packs, each providing specific validation functionality: + +- **Base**: Core validator, builder, result types (required) +- **Certificates Pack**: X.509 certificate validation +- **MST Pack**: Merkle Sealed Transparency receipt verification +- **AKV Pack**: Azure Key Vault KID validation +- **Trust Pack**: Trust policy authoring (future milestone) + +Each pack is: +- A separate Rust FFI crate (staticlib/cdylib) +- A separate C header file +- A separate C++ header file +- An optional CMake target +- An optional vcpkg feature (future) + +## Layer 1: Rust FFI + +### Directory Structure +``` +native/rust/ +├── cose_sign1_validation_ffi/ # Base FFI (required) +│ ├── Cargo.toml # crate-type = ["cdylib", "staticlib", "rlib"] +│ └── src/ +│ ├── lib.rs # Core types, builder, validator +│ ├── error.rs # Panic catching, thread-local errors +│ └── version.rs # ABI versioning +├── cose_sign1_validation_ffi_certificates/ # Certificates pack FFI +│ ├── Cargo.toml # crate-type = ["staticlib", "cdylib"] +│ └── src/ +│ ├── lib.rs # Pack registration function +│ └── options.rs # C ABI options struct +├── cose_sign1_validation_ffi_mst/ # MST pack FFI +├── cose_sign1_validation_ffi_akv/ # AKV pack FFI +└── cose_sign1_validation_ffi_trust/ # Trust pack FFI (placeholder) +``` + +### Build Artifacts +- **Windows**: `*.dll` + `*.dll.lib` (import library) +- **Linux**: `*.so` +- **macOS**: `*.dylib` + +Static libraries (`.lib`/`.a`) also available for all packs. + +### C ABI Types +```c +// Opaque handles +typedef struct cose_validator_builder_t cose_validator_builder_t; +typedef struct cose_validator_t cose_validator_t; +typedef struct cose_validation_result_t cose_validation_result_t; + +// Status codes +typedef enum { + COSE_OK = 0, + COSE_ERR = 1, + COSE_PANIC = 2, + COSE_INVALID_ARG = 3 +} cose_status_t; + +// Pack options (one struct per pack) +typedef struct cose_certificate_trust_options_t { /* ... */ } cose_certificate_trust_options_t; +typedef struct cose_mst_trust_options_t { /* ... */ } cose_mst_trust_options_t; +typedef struct cose_akv_trust_options_t { /* ... */ } cose_akv_trust_options_t; +``` + +### Key Functions (Base) +```c +cose_validator_builder_t* cose_validator_builder_new(void); +void cose_validator_builder_free(cose_validator_builder_t*); +cose_status_t cose_validator_builder_build(cose_validator_builder_t*, cose_validator_t**); +cose_status_t cose_validator_validate_bytes(cose_validator_t*, const uint8_t*, size_t, + const uint8_t*, size_t, cose_validation_result_t**); +``` + +### Key Functions (Per-Pack) +```c +// Certificates pack +cose_status_t cose_validator_builder_with_certificates_pack(cose_validator_builder_t*); +cose_status_t cose_validator_builder_with_certificates_pack_ex(cose_validator_builder_t*, + cose_certificate_trust_options_t*); + +// MST pack +cose_status_t cose_validator_builder_with_mst_pack(cose_validator_builder_t*); +cose_status_t cose_validator_builder_with_mst_pack_ex(cose_validator_builder_t*, + cose_mst_trust_options_t*); + +// AKV pack +cose_status_t cose_validator_builder_with_akv_pack(cose_validator_builder_t*); +cose_status_t cose_validator_builder_with_akv_pack_ex(cose_validator_builder_t*, + cose_akv_trust_options_t*); +``` + +## Layer 2: C Projection + +### Directory Structure +``` +native/c/ +├── CMakeLists.txt # Build system with conditional pack linking +├── README.md # C API documentation +├── include/cose/ +│ ├── cose_sign1.h # Base API (required) +│ ├── cose_certificates.h # Certificates pack API +│ ├── cose_mst.h # MST pack API +│ └── cose_azure_key_vault.h # AKV pack API +└── tests/ + ├── CMakeLists.txt + └── smoke_test.c # Basic validation test +``` + +### CMake Configuration +```cmake +find_library(COSE_FFI_BASE_LIB cose_sign1_validation_ffi REQUIRED) +find_library(COSE_FFI_CERTIFICATES_LIB cose_sign1_validation_ffi_certificates) +find_library(COSE_FFI_MST_LIB cose_sign1_validation_ffi_mst) +find_library(COSE_FFI_AKV_LIB cose_sign1_validation_ffi_akv) + +if(COSE_FFI_CERTIFICATES_LIB) + target_link_libraries(cose_sign1 PUBLIC ${COSE_FFI_CERTIFICATES_LIB}) + target_compile_definitions(cose_sign1 PUBLIC COSE_HAS_CERTIFICATES_PACK) +endif() +# ... similar for MST and AKV +``` + +### Header Organization +Each pack header: +1. Includes `cose_sign1.h` (base types) +2. Declares pack-specific options struct +3. Declares pack registration functions +4. Protected by include guards +5. Uses `extern "C"` for C++ compatibility + +### Usage Example (C) +```c +#include +#include + +cose_validator_builder_t* builder = cose_validator_builder_new(); +cose_validator_builder_with_certificates_pack(builder); + +cose_validator_t* validator; +if (cose_validator_builder_build(builder, &validator) != COSE_OK) { + fprintf(stderr, "Build failed: %s\n", cose_last_error_message_utf8()); + cose_validator_builder_free(builder); + return 1; +} + +cose_validation_result_t* result; +cose_validator_validate_bytes(validator, cose_bytes, cose_len, NULL, 0, &result); + +if (cose_validation_result_ok(result)) { + printf("Valid!\n"); +} else { + char* msg = cose_validation_result_failure_message(result); + printf("Invalid: %s\n", msg); + cose_string_free(msg); +} + +cose_validation_result_free(result); +cose_validator_free(validator); +cose_validator_builder_free(builder); +``` + +## Layer 3: C++ Projection + +### Directory Structure +``` +native/c_pp/ +├── CMakeLists.txt # Interface library with conditional pack linking +├── README.md # C++ API documentation +├── include/cose/ +│ ├── validator.hpp # Base RAII types (required) +│ ├── certificates.hpp # Certificates pack RAII +│ ├── mst.hpp # MST pack RAII +│ ├── azure_key_vault.hpp # AKV pack RAII +│ └── cose.hpp # Convenience header (includes all) +└── tests/ + ├── CMakeLists.txt + └── smoke_test.cpp # RAII validation test +``` + +### RAII Design Principles +- **Non-copyable**: Copy constructors deleted +- **Movable**: Move constructors/assignment enabled +- **Exception-based**: Errors throw `cose::cose_error` +- **Automatic cleanup**: Destructors call FFI free functions +- **Modern C++17**: Uses `std::vector`, `std::string`, structured bindings + +### Key Classes + +#### Base (validator.hpp) +```cpp +namespace cose { + // Exception type + class cose_error : public std::runtime_error { /* ... */ }; + + // RAII wrapper for validation result + class ValidationResult { + cose_validation_result_t* handle; + public: + ValidationResult(cose_validation_result_t*); + ~ValidationResult(); + bool Ok() const; + std::string FailureMessage() const; + }; + + // RAII wrapper for validator + class Validator { + cose_validator_t* handle; + public: + Validator(cose_validator_t*); + ~Validator(); + ValidationResult Validate(const std::vector& cose_bytes, + const std::vector& detached_payload = {}); + }; + + // Fluent builder base class + class ValidatorBuilder { + protected: + cose_validator_builder_t* handle; + public: + ValidatorBuilder(); + virtual ~ValidatorBuilder(); + Validator Build(); + }; +} +``` + +#### Per-Pack Extensions (certificates.hpp, mst.hpp, azure_key_vault.hpp) +```cpp +namespace cose { + // Options use C++ types + struct CertificateOptions { + bool trust_embedded_chain_as_trusted = false; + bool identity_pinning_enabled = false; + std::vector allowed_thumbprints; + std::vector pqc_algorithm_oids; + }; + + // Builder extends base class + class ValidatorBuilderWithCertificates : public ValidatorBuilder { + public: + ValidatorBuilderWithCertificates& WithCertificates(); + ValidatorBuilderWithCertificates& WithCertificates(const CertificateOptions& options); + }; +} +``` + +### Usage Example (C++) +```cpp +#include + +try { + // Fluent builder with pack + auto validator = cose::ValidatorBuilderWithCertificates() + .WithCertificates() + .Build(); + + std::vector cose_bytes = /* ... */; + auto result = validator.Validate(cose_bytes); + + if (result.Ok()) { + std::cout << "Valid!\n"; + } else { + std::cout << "Invalid: " << result.FailureMessage() << "\n"; + } + + // RAII cleanup happens automatically +} catch (const cose::cose_error& e) { + std::cerr << "Error: " << e.what() << "\n"; + return 1; +} +``` + +## Build System Integration + +### CMake Workflow +1. Build Rust FFI libraries: `cargo build --release --workspace` +2. Configure C projection: `cmake -B build -S native/c -DBUILD_TESTING=ON` +3. Build C projection: `cmake --build build --config Release` +4. Configure C++ projection: `cmake -B build -S native/c_pp -DBUILD_TESTING=ON` +5. Build C++ projection: `cmake --build build --config Release` +6. Run tests: `ctest -C Release` (requires Rust DLLs in PATH) + +### vcpkg (Overlay Port) +```json +{ + "name": "cosesign1-validation-native", + "version-string": "0.1.0", + "description": "C and C++ projections for COSE_Sign1 validation (Rust FFI-backed)", + "supports": "windows | linux | osx", + "default-features": ["certificates", "cpp"], + "features": { + "cpp": { + "description": "Install C++ projection headers + CMake target" + }, + "certificates": { + "description": "Build/install X.509 certificates pack FFI and enable COSE_HAS_CERTIFICATES_PACK" + }, + "mst": { + "description": "Build/install MST pack FFI and enable COSE_HAS_MST_PACK" + }, + "akv": { + "description": "Build/install Azure Key Vault pack FFI and enable COSE_HAS_AKV_PACK" + }, + "trust": { + "description": "Build/install trust-policy pack FFI and enable COSE_HAS_TRUST_PACK" + } + } +} +``` + +## Error Handling + +### Rust FFI Layer +- All public functions wrapped in `with_catch_unwind()` +- Panics converted to `COSE_PANIC` status code +- Error messages stored thread-locally +- Retrieved via `cose_last_error_message_utf8()` + +### C Projection +- Check status codes after every call +- Use `cose_last_error_message_utf8()` for details +- Manually free all returned strings with `cose_string_free()` + +### C++ Projection +- Exceptions thrown for all errors +- `cose::cose_error` includes detailed message +- RAII ensures cleanup even during exception unwinding +- No manual resource management needed + +## Testing Strategy + +### Smoke Tests (Current) +- **C**: Builder creation, pack registration, validator build +- **C++**: RAII wrappers, fluent API, exception handling, all packs + +### Future Integration Tests +- Real COSE Sign1 message validation +- Certificate chain validation scenarios +- MST receipt verification with mock receipts +- AKV KID validation with pattern matching +- Trust policy evaluation +- Negative test cases (invalid signatures, expired certs, etc.) + +### Coverage Testing +- **Rust**: `cargo-llvm-cov` with 95% target (already achieved) +- **C**: OpenCppCoverage (Windows) or gcov (Linux) +- **C++**: OpenCppCoverage (Windows) or gcov (Linux) + +## Documentation + +Each layer provides: +- **README.md**: Usage guide with examples +- **API reference**: Inline comments in headers +- **Architecture guide**: This document +- **Progress log**: [FFI_PROJECTIONS_PROGRESS.md](FFI_PROJECTIONS_PROGRESS.md) + +## Future Work + +### Milestone M3: Trust Policy Authoring +- Expose trust policy DSL to C/C++ +- `TrustPlanBuilder` FFI +- C and C++ wrappers for policy construction +- Default trust plans + +### Milestone M4: Comprehensive Testing +- Integration tests with real COSE messages +- Certificate validation test suite +- MST verification test suite +- Performance benchmarks + +### Milestone M5: Packaging +- vcpkg port with per-pack features +- CMake find_package support +- Conan package (optional) +- Documentation site + +### Milestone M6: Coverage & CI +- OpenCppCoverage scripts for C/C++ +- GitHub Actions workflow for native builds +- Coverage reporting and enforcement +- Cross-platform testing (Windows, Linux, macOS) diff --git a/native/docs/README.md b/native/docs/README.md new file mode 100644 index 00000000..362835f4 --- /dev/null +++ b/native/docs/README.md @@ -0,0 +1,50 @@ +# Native development (Rust-first, C/C++ projections via vcpkg) + +This folder is the entry point for native developers. + +## Rust-first documentation + +The Rust implementation is the **source of truth**. If you are trying to understand behavior, APIs, +or extension points, prefer the Rust docs first: + +- Rust workspace docs: [native/rust/docs/README.md](../rust/docs/README.md) +- Crate README surfaces under [native/rust/](../rust/) (each crate has a `README.md`) +- Runnable examples live under each crate’s `examples/` folder + +This `native/docs/` folder focuses on how Rust is packaged and consumed from native code. + +## What you get + +- A Rust implementation of COSE_Sign1 validation (source of truth) +- C and C++ projections (headers + CMake targets) backed by Rust FFI libraries +- A single vcpkg port (`cosesign1-validation-native`) that builds the Rust FFI and installs the C/C++ projections + +## Start here + +### Consuming from C/C++ (recommended path) + +If you want to consume this from a native app/library, start with: + +- [vcpkg + CMake consumption](03-vcpkg.md) + +Then jump to the projection that matches your integration: + +- [C projection guide](04-c-projection.md) +- [C++ projection guide](05-cpp-projection.md) + +Those guides include the expected include/link model and small end-to-end examples. + +### Developing in this repo (Rust + projections) + +If you want to modify the Rust validator and/or projections: + +- [Architecture + repo layout](01-overview.md) +- [Rust workspace + FFI crates](02-rust-ffi.md) + +### Quality & safety workflows + +- [Testing, ASAN, and coverage](06-testing-coverage-asan.md) + +### Troubleshooting + +- [Troubleshooting](07-troubleshooting.md) 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..ea6a49dd --- /dev/null +++ b/native/rust/Cargo.lock @@ -0,0 +1,1456 @@ +# 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 = "cborrs" +version = "0.1.0" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "cc" +version = "1.2.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cose-openssl" +version = "0.1.0" +dependencies = [ + "cborrs", + "hex", + "openssl-sys", +] + +[[package]] +name = "cose-openssl-ffi" +version = "0.1.0" +dependencies = [ + "cose-openssl", + "hex", +] + +[[package]] +name = "cose_sign1_validation" +version = "0.1.0" +dependencies = [ + "anyhow", + "cose_sign1_validation_azure_key_vault", + "cose_sign1_validation_certificates", + "cose_sign1_validation_test_utils", + "cose_sign1_validation_transparent_mst", + "cose_sign1_validation_trust", + "once_cell", + "regex", + "sha1", + "sha2", + "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", + "pqcrypto-mldsa", + "pqcrypto-traits", + "rcgen", + "ring", + "sha1", + "thiserror 2.0.17", + "tinycbor", + "x509-parser", +] + +[[package]] +name = "cose_sign1_validation_demo" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "cose_sign1_validation", + "cose_sign1_validation_certificates", + "cose_sign1_validation_trust", + "hex", + "rcgen", + "ring", + "sha1", + "tinycbor", + "x509-parser", +] + +[[package]] +name = "cose_sign1_validation_ffi" +version = "0.1.0" +dependencies = [ + "anyhow", + "cose_sign1_validation", + "libc", + "once_cell", + "thiserror 2.0.17", +] + +[[package]] +name = "cose_sign1_validation_ffi_akv" +version = "0.1.0" +dependencies = [ + "anyhow", + "cose_sign1_validation", + "cose_sign1_validation_azure_key_vault", + "cose_sign1_validation_ffi", + "cose_sign1_validation_ffi_trust", + "libc", +] + +[[package]] +name = "cose_sign1_validation_ffi_certificates" +version = "0.1.0" +dependencies = [ + "anyhow", + "cose_sign1_validation", + "cose_sign1_validation_certificates", + "cose_sign1_validation_ffi", + "cose_sign1_validation_ffi_trust", + "libc", +] + +[[package]] +name = "cose_sign1_validation_ffi_mst" +version = "0.1.0" +dependencies = [ + "anyhow", + "cose_sign1_validation", + "cose_sign1_validation_ffi", + "cose_sign1_validation_ffi_trust", + "cose_sign1_validation_transparent_mst", + "libc", +] + +[[package]] +name = "cose_sign1_validation_ffi_trust" +version = "0.1.0" +dependencies = [ + "anyhow", + "cose_sign1_validation", + "cose_sign1_validation_ffi", + "cose_sign1_validation_test_utils", + "cose_sign1_validation_trust", + "libc", + "tinycbor", +] + +[[package]] +name = "cose_sign1_validation_test_utils" +version = "0.1.0" +dependencies = [ + "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 = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[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 = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[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 = "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 = "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 = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[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 = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[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 = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[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 = "pqcrypto-internals" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a326caf27cbf2ac291ca7fd56300497ba9e76a8cc6a7d95b7a18b57f22b61d" +dependencies = [ + "cc", + "dunce", + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "pqcrypto-mldsa" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f812cd126a2582599478a434fea75937b4b05d234c64a49e0cea129e130528" +dependencies = [ + "cc", + "glob", + "libc", + "paste", + "pqcrypto-internals", + "pqcrypto-traits", +] + +[[package]] +name = "pqcrypto-traits" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e851c7654eed9e68d7d27164c454961a616cf8c203d500607ef22c737b51bb" + +[[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 = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[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 0.2.17", + "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 = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8af07e3652e13c8528c167c11f9298817906e0316e061abddc5dbcb7effa2641" +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 = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[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 = "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 = "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 = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[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..805d6542 --- /dev/null +++ b/native/rust/Cargo.toml @@ -0,0 +1,64 @@ +[workspace] +resolver = "2" + +members = [ + "cose_openssl/cose_openssl", + "cose_openssl/cose_openssl_ffi", + "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_ffi", + "cose_sign1_validation_ffi_certificates", + "cose_sign1_validation_ffi_mst", + "cose_sign1_validation_ffi_akv", + "cose_sign1_validation_ffi_trust", + "cose_sign1_validation_test_utils", + "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" + +# PQC signatures (FIPS 204 ML-DSA) +pqcrypto-mldsa = { version = "0.1.2", default-features = false, features = ["std"] } +pqcrypto-traits = { version = "0.3.5", default-features = false, features = ["std"] } + +# 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"] } + +# OpenSSL FFI bindings (used by cose-openssl) +openssl-sys = "0.9" + +# use by cose-openssl. The everparse repo has multiple crates named "cborrs"; +# patch to pick nondet. +[patch."https://github.com/project-everest/everparse"] +cborrs = { path = "cose_openssl/.patched/everparse/src/cbor/pulse/nondet/rust" } diff --git a/native/rust/README.md b/native/rust/README.md new file mode 100644 index 00000000..7f2151cf --- /dev/null +++ b/native/rust/README.md @@ -0,0 +1,18 @@ +# native/rust + +Rust port of the V2 trust/validation framework (mirrors `V2/CoseSign1.Validation`). + +Docs live in [native/rust/docs/](docs/): +- [native/rust/docs/README.md](docs/README.md) + +Workspace crates: +- `cose_sign1_validation_trust`: trust engine (facts/rules/compiled plans/audit/subject IDs). +- `cose_sign1_validation`: fluent-first validator facade (trust pack wiring + validation pipeline). +- `cose_sign1_validation_certificates`: X.509 `x5chain` parsing + signature verification via leaf cert public key. +- `cose_sign1_validation_transparent_mst`: Transparent MST receipt parsing + verification. +- `cose_sign1_validation_azure_key_vault`: Azure Key Vault `kid` pattern detection/allow-listing. +- `cose_sign1_validation_demo`: runnable demo (`selftest` + `validate`). + +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..16669b81 --- /dev/null +++ b/native/rust/collect-coverage.ps1 @@ -0,0 +1,315 @@ +param( + [int]$FailUnderLines = 95, + [string]$OutputDir = "coverage", + [switch]$NoHtml, + [switch]$NoClean, + [switch]$AbiParityCheckOnly +) + +$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" + } +} + +function Remove-LlvmCovNoise { + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)][object]$Item + ) + + process { + $line = $null + if ($Item -is [System.Management.Automation.ErrorRecord]) { + $line = $Item.Exception.Message + } else { + $line = $Item.ToString() + } + + if ([string]::IsNullOrWhiteSpace($line)) { + return + } + + # llvm-profdata/llvm-cov can emit a deterministic warning in multi-crate coverage runs: + # "warning: functions have mismatched data" + # This message is noisy and doesn't affect the repo's coverage gates. + if ($line -notmatch 'functions have mismatched data') { + $line + } + } +} + +function Assert-FluentHelpersProjectedToFfi { + param( + [Parameter(Mandatory = $true)][string]$Root + ) + + # Fluent helper surfaces that should be projected to the Rust FFI layer. + # Note: This is intentionally scoped to callback-free `require_*` helpers. + $fluentFiles = @( + (Join-Path $Root 'cose_sign1_validation\src\message_facts.rs'), + (Join-Path $Root 'cose_sign1_validation_certificates\src\fluent_ext.rs'), + (Join-Path $Root 'cose_sign1_validation_transparent_mst\src\fluent_ext.rs'), + (Join-Path $Root 'cose_sign1_validation_azure_key_vault\src\fluent_ext.rs') + ) + + foreach ($p in $fluentFiles) { + if (-not (Test-Path $p)) { + throw "ABI parity gate: expected fluent file not found: $p" + } + } + + # Rust-only helpers that intentionally cannot/should not be projected across the C ABI. + # These rely on passing closures/callbacks. + $excluded = @( + 'require_cwt_claim' + , 'require_kid_allowed' + , 'require_trusted' + ) + + $requireMethods = @() + foreach ($p in $fluentFiles) { + $matches = Select-String -Path $p -Pattern '\bfn\s+(require_[A-Za-z0-9_]+)\b' -AllMatches + foreach ($m in $matches) { + foreach ($mm in $m.Matches) { + $name = $mm.Groups[1].Value + if ($excluded -notcontains $name) { + $requireMethods += $name + } + } + } + } + + $requireMethods = $requireMethods | Sort-Object -Unique + + $ffiFiles = Get-ChildItem -Path $Root -Recurse -File -Filter 'lib.rs' | + Where-Object { + $_.FullName -match '(\\|/)cose_sign1_validation_ffi' -and + $_.FullName -match '(\\|/)src(\\|/)' -and + $_.FullName -notmatch '(\\|/)target(\\|/)' + } + + if ($ffiFiles.Count -eq 0) { + throw "ABI parity gate: no Rust FFI lib.rs files found under $Root" + } + + $missing = @() + foreach ($name in $requireMethods) { + $escaped = [regex]::Escape($name) + # Use alphanumeric boundaries (not \b) so we still match snake_case substrings inside + # exported names like `cose_*_require_xxx(...)`. + $pattern = "(?&1 | Remove-LlvmCovNoise + } finally { + $ErrorActionPreference = $prevEap + } + } + } + + Invoke-Checked -Command "cargo llvm-cov report (lcov)" -Run { + $prevEap = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + cargo llvm-cov report @reportArgs --lcov --output-path (Join-Path $OutputDir "lcov.info") *>&1 | Remove-LlvmCovNoise + } finally { + $ErrorActionPreference = $prevEap + } + } + } catch { + Write-Host "Coverage gate failed; current production-code summary:" -ForegroundColor Yellow + try { + $prevEap = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + & cargo llvm-cov report --lcov @summaryReportArgs *>&1 | Remove-LlvmCovNoise | Out-Host + } finally { + $ErrorActionPreference = $prevEap + } + } catch { + & cargo llvm-cov @summaryArgs | Out-Host + } + throw + } finally { + $env:CARGO_INCREMENTAL = $prevCargoIncremental + $env:CARGO_LLVM_COV_TARGET_DIR = $prevLlvmCovTargetDir + $env:CARGO_LLVM_COV_BUILD_DIR = $prevLlvmCovBuildDir + $env:LLVM_PROFILE_FILE = $prevLlvmProfileFile + $env:CARGO_TARGET_DIR = $prevCargoTargetDir + } + + 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_openssl/.patched/everparse b/native/rust/cose_openssl/.patched/everparse new file mode 160000 index 00000000..a17b4739 --- /dev/null +++ b/native/rust/cose_openssl/.patched/everparse @@ -0,0 +1 @@ +Subproject commit a17b47390dabb112abbc07736945c6ac427664ee diff --git a/native/rust/cose_openssl/cose_openssl/Cargo.toml b/native/rust/cose_openssl/cose_openssl/Cargo.toml new file mode 100644 index 00000000..233187cd --- /dev/null +++ b/native/rust/cose_openssl/cose_openssl/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "cose-openssl" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["lib"] + +[features] +pqc = [] + +[lints.rust] +warnings = "deny" + +[dependencies] +hex = "0.4" +openssl-sys.workspace = true +cborrs = { git = "https://github.com/project-everest/everparse", tag = "v2026.02.04" } diff --git a/native/rust/cose_openssl/cose_openssl/src/cose.rs b/native/rust/cose_openssl/cose_openssl/src/cose.rs new file mode 100644 index 00000000..1cef34fb --- /dev/null +++ b/native/rust/cose_openssl/cose_openssl/src/cose.rs @@ -0,0 +1,403 @@ +use crate::ossl_wrappers::{ + EvpKey, KeyType, WhichEC, ecdsa_der_to_fixed, ecdsa_fixed_to_der, +}; +use cborrs::cbornondet::*; + +#[cfg(feature = "pqc")] +use crate::ossl_wrappers::WhichMLDSA; + +const COSE_SIGN1_TAG: u64 = 18; +const COSE_HEADER_ALG: u64 = 1; +const SIG_STRUCTURE1_CONTEXT: &str = "Signature1"; +const CBOR_SIMPLE_VALUE_NULL: u8 = 22; + +fn cbor_serialize(item: CborNondet) -> Result, String> { + let sz = cbor_nondet_size(item, usize::MAX) + .ok_or("Failed to estimate CBOR serialization size")?; + let mut buf = vec![0u8; sz]; + + let written = cbor_nondet_serialize(item, &mut buf) + .ok_or("Failed to serialize CBOR item")?; + + if sz != written { + return Err(format!( + "Failed to serialize CBOR, written {written} != expected {sz}" + )); + } + + Ok(buf) +} + +fn cose_alg(key: &EvpKey) -> Result<(CborNondetIntKind, u64), String> { + // EverCBOR starts counting negs from -1, so Neg 7 is -8, for instance. + // Therefore, substract 1 from the absolute value before convertion. + match &key.typ { + KeyType::EC(WhichEC::P256) => Ok((CborNondetIntKind::NegInt64, 7 - 1)), // ES256 = -7 + KeyType::EC(WhichEC::P384) => Ok((CborNondetIntKind::NegInt64, 35 - 1)), // ES384 = -35 + KeyType::EC(WhichEC::P521) => Ok((CborNondetIntKind::NegInt64, 36 - 1)), // ES512 = -36 + #[cfg(feature = "pqc")] + KeyType::MLDSA(which) => match which { + WhichMLDSA::P44 => Ok((CborNondetIntKind::NegInt64, 48 - 1)), + WhichMLDSA::P65 => Ok((CborNondetIntKind::NegInt64, 49 - 1)), + WhichMLDSA::P87 => Ok((CborNondetIntKind::NegInt64, 50 - 1)), + }, + } +} + +/// Parse a COSE_Sign1 envelope and return (phdr, payload, signature). +fn parse_cose_sign1<'a>( + envelope: &'a [u8], +) -> Result<(CborNondet<'a>, CborNondet<'a>, CborNondet<'a>), String> { + let (tag, _) = cbor_nondet_parse(None, false, envelope) + .ok_or("Failed to parse COSE envelope")?; + + let rest = match cbor_nondet_destruct(tag) { + CborNondetView::Tagged { tag, payload } if tag == COSE_SIGN1_TAG => { + Ok(payload) + } + CborNondetView::Tagged { tag, .. } => Err(format!( + "Wrong COSE tag: expected {COSE_SIGN1_TAG}, got {tag}" + )), + _ => Err("Expected COSE_Sign1 tagged item".to_string()), + }?; + + let arr = match cbor_nondet_destruct(rest) { + CborNondetView::Array { _0 } => Ok(_0), + _ => Err("Expected COSE_Sign1 array inside tag".to_string()), + }?; + + if cbor_nondet_get_array_length(arr) != 4 { + return Err("COSE_Sign1 array length is not 4".to_string()); + } + + let phdr = cbor_nondet_get_array_item(arr, 0) + .ok_or("Failed to get protected header from COSE array")?; + let payload = cbor_nondet_get_array_item(arr, 2) + .ok_or("Failed to get payload from COSE array")?; + let signature = cbor_nondet_get_array_item(arr, 3) + .ok_or("Failed to get signature from COSE array")?; + + Ok((phdr, payload, signature)) +} + +/// Insert alg(1), return error if already exists. +fn insert_alg(key: &EvpKey, phdr: &[u8]) -> Result, String> { + let (parsed, _) = cbor_nondet_parse(None, false, phdr) + .ok_or("Failed to parse protected header map")?; + + let entries = match cbor_nondet_destruct(parsed) { + CborNondetView::Map { _0 } => Ok(_0), + _ => Err("Protected header is not a CBOR map".to_string()), + }?; + + let alg_label = + cbor_nondet_mk_int64(CborNondetIntKind::UInt64, COSE_HEADER_ALG); + if cbor_nondet_map_get(entries, alg_label).is_some() { + return Err("Algorithm already set in protected header".to_string()); + } + + // Insert alg(1) to the beginning. + let (kind, val) = cose_alg(key)?; + let mut map = Vec::::new(); + map.push(cbor_nondet_mk_map_entry( + cbor_nondet_mk_int64(CborNondetIntKind::UInt64, COSE_HEADER_ALG), + cbor_nondet_mk_int64(kind, val), + )); + + for entry in entries { + map.push(entry); + } + + let map = cbor_nondet_mk_map(&mut map) + .ok_or("Failed to build protected header map")?; + + cbor_serialize(map) +} + +/// To-be-signed (TBS). +/// https://www.rfc-editor.org/rfc/rfc9052.html#section-4.4. +fn sig_structure(phdr: &[u8], payload: &[u8]) -> Result, String> { + let items = [ + cbor_nondet_mk_text_string(SIG_STRUCTURE1_CONTEXT) + .ok_or("Failed to make Sig_structure context string")?, + cbor_nondet_mk_byte_string(phdr) + .ok_or("Failed to make protected header byte string")?, + cbor_nondet_mk_byte_string(&[]) + .ok_or("Failed to make external AAD byte string")?, + cbor_nondet_mk_byte_string(payload) + .ok_or("Failed to make payload byte string")?, + ]; + let arr = + cbor_nondet_mk_array(&items).ok_or("Failed to build TBS array")?; + + cbor_serialize(arr) +} + +/// Produce a COSE_Sign1 envelope. +pub fn cose_sign1( + key: &EvpKey, + phdr: &[u8], + uhdr: &[u8], + payload: &[u8], + detached: bool, +) -> Result, String> { + let phdr_bytes = insert_alg(key, phdr)?; + let tbs = sig_structure(&phdr_bytes, payload)?; + let sig = crate::sign::sign(key, &tbs)?; + + // ECDSA: convert from DER to COSE fixed-size (r || s) per RFC 9053 s2.1. + let sig = match &key.typ { + KeyType::EC(_) => ecdsa_der_to_fixed(&sig, key.ec_field_size()?)?, + #[cfg(feature = "pqc")] + KeyType::MLDSA(_) => sig, + }; + + let payload_item = if detached { + cbor_nondet_mk_simple_value(CBOR_SIMPLE_VALUE_NULL) + .ok_or("Failed to make CBOR null for detached payload")? + } else { + cbor_nondet_mk_byte_string(payload) + .ok_or("Failed to make payload byte string")? + }; + + // Parse uhdr so we can embed it as-is. + let (uhdr_item, _) = cbor_nondet_parse(None, false, uhdr) + .ok_or("Failed to parse unprotected header")?; + + let arr = [ + cbor_nondet_mk_byte_string(&phdr_bytes) + .ok_or("Failed to make protected header byte string")?, + uhdr_item, + payload_item, + cbor_nondet_mk_byte_string(&sig) + .ok_or("Failed to make signature byte string")?, + ]; + + let inner = + cbor_nondet_mk_array(&arr).ok_or("Failed to build COSE_Sign1 array")?; + let tagged = cbor_nondet_mk_tagged(COSE_SIGN1_TAG, &inner); + + cbor_serialize(tagged) +} + +/// Check that the algorithm encoded in the phdr matches the key type. +fn check_phdr_alg(key: &EvpKey, phdr_bytes: &[u8]) -> Result<(), String> { + let (parsed, _) = cbor_nondet_parse(None, false, phdr_bytes) + .ok_or("Failed to parse protected header for algorithm check")?; + let entries = match cbor_nondet_destruct(parsed) { + CborNondetView::Map { _0 } => Ok(_0), + _ => Err("Protected header is not a CBOR map".to_string()), + }?; + + let alg_label = + cbor_nondet_mk_int64(CborNondetIntKind::UInt64, COSE_HEADER_ALG); + let alg_item = cbor_nondet_map_get(entries, alg_label) + .ok_or("Algorithm not found in protected header")?; + + let (phdr_kind, phdr_val) = + match cbor_nondet_destruct(alg_item) { + CborNondetView::Int64 { kind, value } => Ok((kind, value)), + _ => Err("Algorithm value in protected header is not an integer" + .to_string()), + }?; + + let (key_kind, key_val) = cose_alg(key)?; + if phdr_kind != key_kind || phdr_val != key_val { + return Err( + "Algorithm mismatch between protected header and key".to_string() + ); + } + Ok(()) +} + +/// Verify a COSE_Sign1 envelope. If `payload` is `Some`, it is used +/// as the detached payload; otherwise the embedded payload is used. +pub fn cose_verify1( + key: &EvpKey, + envelope: &[u8], + payload: Option<&[u8]>, +) -> Result { + let (cose_phdr, cose_payload, cose_sig) = parse_cose_sign1(envelope)?; + + let phdr_bytes = match cbor_nondet_destruct(cose_phdr) { + CborNondetView::ByteString { payload } => Ok(payload.to_vec()), + _ => Err("Protected header is not a byte string".to_string()), + }?; + + check_phdr_alg(key, &phdr_bytes)?; + + let actual_payload = match payload { + Some(p) => p.to_vec(), + None => match cbor_nondet_destruct(cose_payload) { + CborNondetView::ByteString { payload } => Ok(payload.to_vec()), + _ => Err("Embedded payload is not a byte string".to_string()), + }?, + }; + + let sig = match cbor_nondet_destruct(cose_sig) { + CborNondetView::ByteString { payload } => Ok(payload.to_vec()), + _ => Err("Signature is not a byte string".to_string()), + }?; + + // ECDSA: convert from COSE fixed-size (r || s) to DER for OpenSSL. + let sig = match &key.typ { + KeyType::EC(_) => ecdsa_fixed_to_der(&sig, key.ec_field_size()?)?, + #[cfg(feature = "pqc")] + KeyType::MLDSA(_) => sig, + }; + + let tbs = sig_structure(&phdr_bytes, &actual_payload)?; + crate::verify::verify(key, &sig, &tbs) +} + +#[cfg(test)] +mod tests { + use super::*; + use hex; + + const TEST_PHDR: &str = "A319018B020FA3061A698B72820173736572766963652E6578616D706C652E636F6D02706C65646765722E7369676E6174757265666363662E7631A1647478696465322E313334"; + + #[test] + fn test_parse_cose() { + let in_str = "d284588da50138220458406661363331386532666561643537313035326231383230393236653865653531313030623630633161383239393362333031353133383561623334343237303019018b020fa3061a698b72820173736572766963652e6578616d706c652e636f6d02706c65646765722e7369676e6174757265666363662e7631a1647478696465322e313334a119018ca12081590100a2018358204208b5b5378c253f49641ab2edb58b557c75cdbb85ae9327930362c84ebba694784963653a322e3133333a3066646666336265663338346237383231316363336434306463363333663336383364353963643930303864613037653030623266356464323734613365633758200000000000000000000000000000000000000000000000000000000000000000028382f5582081980abb4e161b2f3d306c185ef9f7ce84cf5a3b0c8978da82e049d761adfd0082f55820610e8b89721667f99305e7ce4befe0b3b393821a3f72713f89961ebc7e81de6382f55820cbe0d3307b00aa9f324e29c8fb26508404af81044c7adcd4f5b41043d92aff23f6586005784bfccce87452a35a0cd14df5ed8a38c8937f63fb6b522fb94a1551c0e061893bb35fba1fa6fea322b080a14c0894c3864bf4e76df04ffb0f7c350366f91c0d522652d8fa3ebad6ba0270b48e43a065312c759d8bc9a413d4270d5ba86182"; + let v = hex::decode(in_str).unwrap(); + let (_phdr, _payload, _sig) = parse_cose_sign1(&v).unwrap(); + } + + #[test] + fn test_insert_alg() { + let key = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); + let phdr = hex::decode(TEST_PHDR).unwrap(); + let phdr = insert_alg(&key, &phdr).unwrap(); + + // Parse result and verify alg is present. + let (parsed, _) = cbor_nondet_parse(None, false, &phdr).unwrap(); + let entries = match cbor_nondet_destruct(parsed) { + CborNondetView::Map { _0 } => _0, + _ => panic!("Expected map"), + }; + + // Check alg. + let alg_label = + cbor_nondet_mk_int64(CborNondetIntKind::UInt64, COSE_HEADER_ALG); + let alg_item = cbor_nondet_map_get(entries, alg_label) + .expect("Algorithm not found in protected header"); + let (kind, val) = match cbor_nondet_destruct(alg_item) { + CborNondetView::Int64 { kind, value } => (kind, value), + _ => panic!("Algorithm value is not an integer"), + }; + let (expected_kind, expected_val) = cose_alg(&key).unwrap(); + assert!(kind == expected_kind); + assert!(val == expected_val); + + // Inserting again must fail. + assert!(insert_alg(&key, &phdr).is_err()); + } + + fn sign_verify_cose(key_type: KeyType) { + let key = EvpKey::new(key_type).unwrap(); + let phdr = hex::decode(TEST_PHDR).unwrap(); + let uhdr = b"\xa0"; // empty map + let payload = b"Good boy..."; + + let envelope = cose_sign1(&key, &phdr, uhdr, payload, false).unwrap(); + assert!(cose_verify1(&key, &envelope, None).unwrap()); + } + + #[test] + fn cose_ec_p256() { + sign_verify_cose(KeyType::EC(WhichEC::P256)); + } + + #[test] + fn cose_ec_p384() { + sign_verify_cose(KeyType::EC(WhichEC::P384)); + } + + #[test] + fn cose_ec_p521() { + sign_verify_cose(KeyType::EC(WhichEC::P521)); + } + + #[test] + fn cose_detached_payload() { + let key = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); + let phdr = hex::decode(TEST_PHDR).unwrap(); + let uhdr = b"\xa0"; // empty map + let payload = b"Good boy..."; + + let envelope = cose_sign1(&key, &phdr, uhdr, payload, true).unwrap(); + + // Verify with the detached payload supplied externally. + assert!(cose_verify1(&key, &envelope, Some(payload)).unwrap()); + + // Verify without supplying the payload must fail. + assert!(cose_verify1(&key, &envelope, None).is_err()); + } + + #[test] + fn cose_with_der_imported_key() { + // Create key pair + let original_key = EvpKey::new(KeyType::EC(WhichEC::P384)).unwrap(); + + // Export private key to DER and reimport for signing + let priv_der = original_key.to_der_private().unwrap(); + let signing_key = EvpKey::from_der_private(&priv_der).unwrap(); + + // Export public key DER and reimport for verification + let pub_der = original_key.to_der_public().unwrap(); + let verification_key = EvpKey::from_der_public(&pub_der).unwrap(); + + let phdr = hex::decode(TEST_PHDR).unwrap(); + let uhdr = b"\xa0"; + let payload = b"test with DER-imported key"; + + // Sign with DER-reimported private key + let envelope = + cose_sign1(&signing_key, &phdr, uhdr, payload, false).unwrap(); + // Verify with DER-imported public key + assert!(cose_verify1(&verification_key, &envelope, None).unwrap()); + } + + #[cfg(feature = "pqc")] + mod pqc_tests { + use super::*; + #[test] + fn cose_mldsa44() { + sign_verify_cose(KeyType::MLDSA(WhichMLDSA::P44)); + } + #[test] + fn cose_mldsa65() { + sign_verify_cose(KeyType::MLDSA(WhichMLDSA::P65)); + } + #[test] + fn cose_mldsa87() { + sign_verify_cose(KeyType::MLDSA(WhichMLDSA::P87)); + } + + #[test] + fn cose_mldsa_with_der_imported_key() { + // Create ML-DSA key pair + let original_key = + EvpKey::new(KeyType::MLDSA(WhichMLDSA::P65)).unwrap(); + + // Export private key to DER and reimport for signing + let priv_der = original_key.to_der_private().unwrap(); + let signing_key = EvpKey::from_der_private(&priv_der).unwrap(); + + // Export public key DER and reimport for verification + let pub_der = original_key.to_der_public().unwrap(); + let verification_key = EvpKey::from_der_public(&pub_der).unwrap(); + + let phdr = hex::decode(TEST_PHDR).unwrap(); + let uhdr = b"\xa0"; + let payload = b"ML-DSA with DER-imported key"; + + // Sign with DER-reimported private key + let envelope = + cose_sign1(&signing_key, &phdr, uhdr, payload, false).unwrap(); + // Verify with DER-imported public key + assert!(cose_verify1(&verification_key, &envelope, None).unwrap()); + } + } +} diff --git a/native/rust/cose_openssl/cose_openssl/src/lib.rs b/native/rust/cose_openssl/cose_openssl/src/lib.rs new file mode 100644 index 00000000..dad23cee --- /dev/null +++ b/native/rust/cose_openssl/cose_openssl/src/lib.rs @@ -0,0 +1,10 @@ +mod cose; +mod ossl_wrappers; +mod sign; +mod verify; + +pub use cose::{cose_sign1, cose_verify1}; +pub use ossl_wrappers::{EvpKey, KeyType, WhichEC}; + +#[cfg(feature = "pqc")] +pub use ossl_wrappers::WhichMLDSA; diff --git a/native/rust/cose_openssl/cose_openssl/src/ossl_wrappers.rs b/native/rust/cose_openssl/cose_openssl/src/ossl_wrappers.rs new file mode 100644 index 00000000..768eb450 --- /dev/null +++ b/native/rust/cose_openssl/cose_openssl/src/ossl_wrappers.rs @@ -0,0 +1,649 @@ +use openssl_sys as ossl; +use std::ffi::CString; +use std::marker::PhantomData; +use std::ptr; + +// Not exposed by openssl-sys 0.9, but available at link time (OpenSSL 3.0+). +unsafe extern "C" { + fn EVP_PKEY_is_a( + pkey: *const ossl::EVP_PKEY, + name: *const std::ffi::c_char, + ) -> std::ffi::c_int; + + fn EVP_PKEY_get_group_name( + pkey: *const ossl::EVP_PKEY, + name: *mut std::ffi::c_char, + name_sz: usize, + gname_len: *mut usize, + ) -> std::ffi::c_int; +} + +#[cfg(feature = "pqc")] +#[derive(Debug)] +pub enum WhichMLDSA { + P44, + P65, + P87, +} + +#[cfg(feature = "pqc")] +impl WhichMLDSA { + fn openssl_str(&self) -> &'static str { + match self { + WhichMLDSA::P44 => "ML-DSA-44", + WhichMLDSA::P65 => "ML-DSA-65", + WhichMLDSA::P87 => "ML-DSA-87", + } + } +} + +#[derive(Debug)] +pub enum WhichEC { + P256, + P384, + P521, +} + +impl WhichEC { + fn openssl_str(&self) -> &'static str { + match self { + WhichEC::P256 => "P-256", + WhichEC::P384 => "P-384", + WhichEC::P521 => "P-521", + } + } + + fn openssl_group_name(&self) -> &'static str { + match self { + WhichEC::P256 => "prime256v1", + WhichEC::P384 => "secp384r1", + WhichEC::P521 => "secp521r1", + } + } +} + +#[derive(Debug)] +pub enum KeyType { + #[cfg(feature = "pqc")] + MLDSA(WhichMLDSA), + EC(WhichEC), +} + +#[derive(Debug)] +pub struct EvpKey { + pub key: *mut ossl::EVP_PKEY, + pub typ: KeyType, +} + +impl EvpKey { + pub fn new(typ: KeyType) -> Result { + unsafe { + let key = match &typ { + #[cfg(feature = "pqc")] + KeyType::MLDSA(which) => { + let alg = CString::new(which.openssl_str()).unwrap(); + ossl::EVP_PKEY_Q_keygen( + ptr::null_mut(), + ptr::null_mut(), + alg.as_ptr(), + ) + } + KeyType::EC(which) => { + let crv = CString::new(which.openssl_str()).unwrap(); + let alg = CString::new("EC").unwrap(); + ossl::EVP_PKEY_Q_keygen( + ptr::null_mut(), + ptr::null_mut(), + alg.as_ptr(), + crv.as_ptr(), + ) + } + }; + + if key.is_null() { + return Err("Failed to create signing key".to_string()); + } + + Ok(EvpKey { key, typ }) + } + } + + /// Create an `EvpKey` from a DER-encoded SubjectPublicKeyInfo. + /// Automatically detects key type (EC curve or ML-DSA variant). + pub fn from_der_public(der: &[u8]) -> Result { + let key = unsafe { + let mut ptr = der.as_ptr(); + let key = + ossl::d2i_PUBKEY(ptr::null_mut(), &mut ptr, der.len() as i64); + if key.is_null() { + return Err("Failed to parse DER public key".to_string()); + } + key + }; + + let typ = match Self::detect_key_type_raw(key) { + Ok(t) => t, + Err(e) => { + unsafe { + ossl::EVP_PKEY_free(key); + } + return Err(e); + } + }; + + Ok(EvpKey { key, typ }) + } + + /// Create an `EvpKey` from a DER-encoded private key + /// (PKCS#8 or traditional format). + /// Automatically detects key type (EC curve or ML-DSA variant). + pub fn from_der_private(der: &[u8]) -> Result { + let key = unsafe { + let mut ptr = der.as_ptr(); + let key = ossl::d2i_AutoPrivateKey( + ptr::null_mut(), + &mut ptr, + der.len() as i64, + ); + if key.is_null() { + return Err("Failed to parse DER private key".to_string()); + } + key + }; + + let typ = match Self::detect_key_type_raw(key) { + Ok(t) => t, + Err(e) => { + unsafe { + ossl::EVP_PKEY_free(key); + } + return Err(e); + } + }; + + Ok(EvpKey { key, typ }) + } + + fn detect_key_type_raw( + pkey: *mut ossl::EVP_PKEY, + ) -> Result { + unsafe { + // EC: check algorithm, then match curve by group name. + let ec = CString::new("EC").unwrap(); + if EVP_PKEY_is_a(pkey as *const _, ec.as_ptr()) == 1 { + let mut buf = [0u8; 64]; + let mut len: usize = 0; + if EVP_PKEY_get_group_name( + pkey as *const _, + buf.as_mut_ptr() as *mut std::ffi::c_char, + buf.len(), + &mut len, + ) != 1 + { + return Err("Failed to get EC group name".to_string()); + } + let group = std::str::from_utf8(&buf[..len]) + .map_err(|_| "EC group name is not UTF-8".to_string())?; + + for variant in [WhichEC::P256, WhichEC::P384, WhichEC::P521] { + if group == variant.openssl_group_name() { + return Ok(KeyType::EC(variant)); + } + } + return Err(format!("Unsupported EC curve: {}", group)); + } + + // ML-DSA: each variant has its own algorithm name. + #[cfg(feature = "pqc")] + for variant in [WhichMLDSA::P44, WhichMLDSA::P65, WhichMLDSA::P87] { + let cname = CString::new(variant.openssl_str()).unwrap(); + if EVP_PKEY_is_a(pkey as *const _, cname.as_ptr()) == 1 { + return Ok(KeyType::MLDSA(variant)); + } + } + + Err("Unsupported key type".to_string()) + } + } + + /// Export the public key as DER-encoded SubjectPublicKeyInfo. + pub fn to_der_public(&self) -> Result, String> { + unsafe { + let mut der_ptr: *mut u8 = ptr::null_mut(); + let len = ossl::i2d_PUBKEY(self.key, &mut der_ptr); + + if len <= 0 || der_ptr.is_null() { + return Err(format!( + "Failed to encode public key to DER (rc={})", + len + )); + } + + // Copy the DER data into a Vec and free the OpenSSL-allocated memory + let der_slice = std::slice::from_raw_parts(der_ptr, len as usize); + let der = der_slice.to_vec(); + ossl::CRYPTO_free( + der_ptr as *mut std::ffi::c_void, + concat!(file!(), "\0").as_ptr() as *const i8, + line!() as i32, + ); + + Ok(der) + } + } + + /// Export the private key as DER-encoded traditional format. + pub fn to_der_private(&self) -> Result, String> { + unsafe { + let mut der_ptr: *mut u8 = ptr::null_mut(); + let len = ossl::i2d_PrivateKey(self.key, &mut der_ptr); + + if len <= 0 || der_ptr.is_null() { + return Err(format!( + "Failed to encode private key to DER (rc={})", + len + )); + } + + let der_slice = std::slice::from_raw_parts(der_ptr, len as usize); + let der = der_slice.to_vec(); + ossl::CRYPTO_free( + der_ptr as *mut std::ffi::c_void, + concat!(file!(), "\0").as_ptr() as *const i8, + line!() as i32, + ); + + Ok(der) + } + } + + /// Compute the EC field-element byte size from the key's bit size. + /// Returns an error if the key is not an EC key. + pub fn ec_field_size(&self) -> Result { + if !matches!(self.typ, KeyType::EC(_)) { + return Err("ec_field_size called on a non-EC key".to_string()); + } + unsafe { + let bits = ossl::EVP_PKEY_bits(self.key); + if bits <= 0 { + return Err("EVP_PKEY_bits failed".to_string()); + } + Ok(((bits + 7) / 8) as usize) + } + } + + /// Return the OpenSSL digest matching the key's COSE algorithm. + /// Returns null for algorithms that do not use a separate digest + /// (e.g. ML-DSA). + pub fn digest(&self) -> *const ossl::EVP_MD { + unsafe { + match &self.typ { + KeyType::EC(WhichEC::P256) => ossl::EVP_sha256(), + KeyType::EC(WhichEC::P384) => ossl::EVP_sha384(), + KeyType::EC(WhichEC::P521) => ossl::EVP_sha512(), + #[cfg(feature = "pqc")] + KeyType::MLDSA(_) => ptr::null(), + } + } + } +} + +impl Drop for EvpKey { + fn drop(&mut self) { + unsafe { + if !self.key.is_null() { + ossl::EVP_PKEY_free(self.key); + } + } + } +} + +// --------------------------------------------------------------------------- +// ECDSA signature format conversion (DER <-> IEEE P1363 fixed-size) +// using OpenSSL's ECDSA_SIG API. +// +// OpenSSL produces/consumes DER-encoded ECDSA signatures: +// SEQUENCE { INTEGER r, INTEGER s } +// +// COSE (RFC 9053) requires the fixed-size (r || s) representation. +// --------------------------------------------------------------------------- + +/// Convert a DER-encoded ECDSA signature to fixed-size (r || s). +pub fn ecdsa_der_to_fixed( + der: &[u8], + field_size: usize, +) -> Result, String> { + unsafe { + let mut p = der.as_ptr(); + let sig = ossl::d2i_ECDSA_SIG( + ptr::null_mut(), + &mut p, + der.len() as std::ffi::c_long, + ); + if sig.is_null() { + return Err("Failed to parse DER ECDSA signature".to_string()); + } + + let mut r: *const ossl::BIGNUM = ptr::null(); + let mut s: *const ossl::BIGNUM = ptr::null(); + ossl::ECDSA_SIG_get0(sig, &mut r, &mut s); + + let mut fixed = vec![0u8; field_size * 2]; + let rc_r = ossl::BN_bn2binpad( + r, + fixed.as_mut_ptr(), + field_size as std::ffi::c_int, + ); + let rc_s = ossl::BN_bn2binpad( + s, + fixed[field_size..].as_mut_ptr(), + field_size as std::ffi::c_int, + ); + ossl::ECDSA_SIG_free(sig); + + if rc_r != field_size as std::ffi::c_int + || rc_s != field_size as std::ffi::c_int + { + return Err("BN_bn2binpad failed for ECDSA r or s".to_string()); + } + + Ok(fixed) + } +} + +/// Convert a fixed-size (r || s) ECDSA signature to DER. +pub fn ecdsa_fixed_to_der( + fixed: &[u8], + field_size: usize, +) -> Result, String> { + if fixed.len() != field_size * 2 { + return Err(format!( + "Expected {} byte ECDSA signature, got {}", + field_size * 2, + fixed.len() + )); + } + + unsafe { + let r = ossl::BN_bin2bn( + fixed.as_ptr(), + field_size as std::ffi::c_int, + ptr::null_mut(), + ); + if r.is_null() { + return Err("BN_bin2bn failed for ECDSA r".to_string()); + } + + let s = ossl::BN_bin2bn( + fixed[field_size..].as_ptr(), + field_size as std::ffi::c_int, + ptr::null_mut(), + ); + if s.is_null() { + ossl::BN_free(r); + return Err("BN_bin2bn failed for ECDSA s".to_string()); + } + + let sig = ossl::ECDSA_SIG_new(); + if sig.is_null() { + ossl::BN_free(r); + ossl::BN_free(s); + return Err("ECDSA_SIG_new failed".to_string()); + } + + // ECDSA_SIG_set0 takes ownership of r and s on success. + if ossl::ECDSA_SIG_set0(sig, r, s) != 1 { + ossl::ECDSA_SIG_free(sig); + // set0 did not take ownership, so free r/s. + ossl::BN_free(r); + ossl::BN_free(s); + return Err("ECDSA_SIG_set0 failed".to_string()); + } + + let mut out_ptr: *mut u8 = ptr::null_mut(); + let len = ossl::i2d_ECDSA_SIG(sig, &mut out_ptr); + ossl::ECDSA_SIG_free(sig); + + if len <= 0 || out_ptr.is_null() { + return Err("i2d_ECDSA_SIG failed".to_string()); + } + + let der = std::slice::from_raw_parts(out_ptr, len as usize).to_vec(); + ossl::CRYPTO_free( + out_ptr as *mut std::ffi::c_void, + concat!(file!(), "\0").as_ptr() as *const i8, + line!() as i32, + ); + + Ok(der) + } +} + +#[derive(Debug)] +pub struct EvpMdContext { + op: PhantomData, + pub ctx: *mut ossl::EVP_MD_CTX, +} + +pub struct SignOp; +pub struct VerifyOp; + +pub trait ContextInit { + fn init( + ctx: *mut ossl::EVP_MD_CTX, + md: *const ossl::EVP_MD, + key: *mut ossl::EVP_PKEY, + ) -> Result<(), i32>; + fn purpose() -> &'static str; +} + +impl ContextInit for SignOp { + fn init( + ctx: *mut ossl::EVP_MD_CTX, + md: *const ossl::EVP_MD, + key: *mut ossl::EVP_PKEY, + ) -> Result<(), i32> { + unsafe { + let rc = ossl::EVP_DigestSignInit( + ctx, + ptr::null_mut(), + md, + ptr::null_mut(), + key, + ); + match rc { + 1 => Ok(()), + err => Err(err), + } + } + } + fn purpose() -> &'static str { + "Sign" + } +} + +impl ContextInit for VerifyOp { + fn init( + ctx: *mut ossl::EVP_MD_CTX, + md: *const ossl::EVP_MD, + key: *mut ossl::EVP_PKEY, + ) -> Result<(), i32> { + unsafe { + let rc = ossl::EVP_DigestVerifyInit( + ctx, + ptr::null_mut(), + md, + ptr::null_mut(), + key, + ); + match rc { + 1 => Ok(()), + err => Err(err), + } + } + } + fn purpose() -> &'static str { + "Verify" + } +} + +impl EvpMdContext { + pub fn new(key: &EvpKey) -> Result { + unsafe { + let ctx = ossl::EVP_MD_CTX_new(); + if ctx.is_null() { + return Err(format!( + "Failed to create ctx for: {}", + T::purpose() + )); + } + if let Err(err) = T::init(ctx, key.digest(), key.key) { + ossl::EVP_MD_CTX_free(ctx); + return Err(format!( + "Failed to init context for {} with err {}", + T::purpose(), + err + )); + } + Ok(EvpMdContext { + op: PhantomData, + ctx, + }) + } + } +} + +impl Drop for EvpMdContext { + fn drop(&mut self) { + unsafe { + if !self.ctx.is_null() { + ossl::EVP_MD_CTX_free(self.ctx); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + #[cfg(feature = "pqc")] + fn create_ml_dsa_keys() { + assert!(EvpKey::new(KeyType::MLDSA(WhichMLDSA::P44)).is_ok()); + assert!(EvpKey::new(KeyType::MLDSA(WhichMLDSA::P65)).is_ok()); + assert!(EvpKey::new(KeyType::MLDSA(WhichMLDSA::P87)).is_ok()); + } + + #[test] + fn create_ec_keys() { + assert!(EvpKey::new(KeyType::EC(WhichEC::P256)).is_ok()); + assert!(EvpKey::new(KeyType::EC(WhichEC::P384)).is_ok()); + assert!(EvpKey::new(KeyType::EC(WhichEC::P521)).is_ok()); + } + + #[test] + fn ec_key_from_der_roundtrip() { + for which in [WhichEC::P256, WhichEC::P384, WhichEC::P521] { + let key = EvpKey::new(KeyType::EC(which)).unwrap(); + let der = key.to_der_public().unwrap(); + let imported = EvpKey::from_der_public(&der).unwrap(); + assert!( + matches!(imported.typ, KeyType::EC(_)), + "Expected EC key type" + ); + + // Verify the reimported key exports the same DER + let der2 = imported.to_der_public().unwrap(); + assert_eq!(der, der2); + } + } + + #[test] + fn ec_key_from_der_p256() { + let key = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); + let der = key.to_der_public().unwrap(); + let imported = EvpKey::from_der_public(&der).unwrap(); + + assert!(matches!(imported.typ, KeyType::EC(WhichEC::P256))); + } + + #[test] + fn from_der_rejects_garbage() { + assert!(EvpKey::from_der_public(&[0xde, 0xad, 0xbe, 0xef]).is_err()); + } + + #[test] + fn from_der_private_rejects_garbage() { + assert!(EvpKey::from_der_private(&[0xde, 0xad, 0xbe, 0xef]).is_err()); + } + + #[test] + fn ec_key_private_der_roundtrip() { + for which in [WhichEC::P256, WhichEC::P384, WhichEC::P521] { + let key = EvpKey::new(KeyType::EC(which)).unwrap(); + let priv_der = key.to_der_private().unwrap(); + let imported = EvpKey::from_der_private(&priv_der).unwrap(); + assert!( + matches!(imported.typ, KeyType::EC(_)), + "Expected EC key type" + ); + + // Private key re-export must be identical. + let priv_der2 = imported.to_der_private().unwrap(); + assert_eq!(priv_der, priv_der2); + + // Public key extracted from the reimported private key must + // match the original. + let pub1 = key.to_der_public().unwrap(); + let pub2 = imported.to_der_public().unwrap(); + assert_eq!(pub1, pub2); + } + } + + #[test] + #[cfg(feature = "pqc")] + fn ml_dsa_key_from_der_roundtrip() { + for which in [WhichMLDSA::P44, WhichMLDSA::P65, WhichMLDSA::P87] { + let key = EvpKey::new(KeyType::MLDSA(which)).unwrap(); + let der = key.to_der_public().unwrap(); + let imported = EvpKey::from_der_public(&der).unwrap(); + assert!( + matches!(imported.typ, KeyType::MLDSA(_)), + "Expected ML-DSA key type" + ); + let der2 = imported.to_der_public().unwrap(); + assert_eq!(der, der2); + } + } + + #[test] + #[cfg(feature = "pqc")] + fn ml_dsa_key_private_der_roundtrip() { + for which in [WhichMLDSA::P44, WhichMLDSA::P65, WhichMLDSA::P87] { + let key = EvpKey::new(KeyType::MLDSA(which)).unwrap(); + let priv_der = key.to_der_private().unwrap(); + let imported = EvpKey::from_der_private(&priv_der).unwrap(); + assert!( + matches!(imported.typ, KeyType::MLDSA(_)), + "Expected ML-DSA key type" + ); + + // Private key re-export must be identical. + let priv_der2 = imported.to_der_private().unwrap(); + assert_eq!(priv_der, priv_der2); + + let pub1 = key.to_der_public().unwrap(); + let pub2 = imported.to_der_public().unwrap(); + assert_eq!(pub1, pub2); + } + } + + #[test] + #[ignore] + fn intentional_leak_for_sanitizer_validation() { + // This test intentionally leaks memory to verify sanitizers + // detect it if not ignored. + let key = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); + std::mem::forget(key); + } +} diff --git a/native/rust/cose_openssl/cose_openssl/src/sign.rs b/native/rust/cose_openssl/cose_openssl/src/sign.rs new file mode 100644 index 00000000..b107fb9d --- /dev/null +++ b/native/rust/cose_openssl/cose_openssl/src/sign.rs @@ -0,0 +1,40 @@ +use crate::ossl_wrappers::{EvpKey, EvpMdContext, SignOp}; + +use openssl_sys as ossl; +use std::ptr; + +pub fn sign(key: &EvpKey, msg: &[u8]) -> Result, String> { + unsafe { + let ctx = EvpMdContext::::new(key)?; + + let mut sig_size: usize = 0; + let res = ossl::EVP_DigestSign( + ctx.ctx, + ptr::null_mut(), + &mut sig_size, + msg.as_ptr(), + msg.len(), + ); + if res != 1 { + return Err(format!("Failed to signature size, err: {}", res)); + } + + let mut sig = vec![0u8; sig_size]; + let res = ossl::EVP_DigestSign( + ctx.ctx, + sig.as_mut_ptr(), + &mut sig_size, + msg.as_ptr(), + msg.len(), + ); + if res != 1 { + return Err(format!("Failed to sign, err: {}", res)); + } + + // Not always fixed, e.g. for EC keys. More on this here: + // https://docs.openssl.org/3.0/man3/EVP_DigestSignInit/#description. + sig.truncate(sig_size); + + Ok(sig) + } +} diff --git a/native/rust/cose_openssl/cose_openssl/src/verify.rs b/native/rust/cose_openssl/cose_openssl/src/verify.rs new file mode 100644 index 00000000..f89a352e --- /dev/null +++ b/native/rust/cose_openssl/cose_openssl/src/verify.rs @@ -0,0 +1,23 @@ +use crate::ossl_wrappers::{EvpKey, EvpMdContext, VerifyOp}; + +use openssl_sys as ossl; + +pub fn verify(key: &EvpKey, sig: &[u8], msg: &[u8]) -> Result { + unsafe { + let ctx = EvpMdContext::::new(key)?; + + let res = ossl::EVP_DigestVerify( + ctx.ctx, + sig.as_ptr(), + sig.len(), + msg.as_ptr(), + msg.len(), + ); + + match res { + 1 => Ok(true), + 0 => Ok(false), + err => Err(format!("Failed to verify signature, err: {}", err)), + } + } +} diff --git a/native/rust/cose_openssl/cose_openssl_ffi/Cargo.toml b/native/rust/cose_openssl/cose_openssl_ffi/Cargo.toml new file mode 100644 index 00000000..95502eb2 --- /dev/null +++ b/native/rust/cose_openssl/cose_openssl_ffi/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "cose-openssl-ffi" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["staticlib"] + +[features] +pqc = ["cose-openssl/pqc"] + +[lints.rust] +warnings = "deny" + +[dependencies] +cose-openssl = { path = "../cose_openssl" } + +[dev-dependencies] +hex = "0.4" diff --git a/native/rust/cose_openssl/cose_openssl_ffi/src/ffi/helpers.rs b/native/rust/cose_openssl/cose_openssl_ffi/src/ffi/helpers.rs new file mode 100644 index 00000000..2bf1b41c --- /dev/null +++ b/native/rust/cose_openssl/cose_openssl_ffi/src/ffi/helpers.rs @@ -0,0 +1,70 @@ +use cose_openssl::EvpKey; +use cose_openssl::cose_sign1; +use std::slice; + +/// Raw pointer + length to slice. Returns an empty slice for null/zero-length. +pub unsafe fn as_slice<'a>(ptr: *const u8, len: usize) -> &'a [u8] { + if ptr.is_null() || len == 0 { + &[] + } else { + unsafe { slice::from_raw_parts(ptr, len) } + } +} + +/// Write a `Vec` result into caller-supplied output pointers. +pub unsafe fn write_output( + v: Vec, + out_ptr: *mut *mut u8, + out_len: *mut usize, +) { + let b = v.into_boxed_slice(); + let len = b.len(); + let ptr = Box::into_raw(b) as *mut u8; + unsafe { + *out_len = len; + *out_ptr = ptr; + } +} + +/// Shared implementation for `cose_sign` and `cose_sign_detached`. +pub unsafe fn sign_inner( + phdr_ptr: *const u8, + phdr_len: usize, + uhdr_ptr: *const u8, + uhdr_len: usize, + payload_ptr: *const u8, + payload_len: usize, + key_der_ptr: *const u8, + key_der_len: usize, + out_ptr: *mut *mut u8, + out_len: *mut usize, + detached: bool, +) -> i32 { + unsafe { + if out_ptr.is_null() || out_len.is_null() { + return -1; + } + + let key = match EvpKey::from_der_private(as_slice( + key_der_ptr, + key_der_len, + )) { + Ok(k) => k, + Err(_) => return -1, + }; + + match cose_sign1( + &key, + as_slice(phdr_ptr, phdr_len), + as_slice(uhdr_ptr, uhdr_len), + as_slice(payload_ptr, payload_len), + detached, + ) { + Ok(envelope) => { + write_output(envelope, out_ptr, out_len); + 0 + } + Err(_) => -1, + } + } +} diff --git a/native/rust/cose_openssl/cose_openssl_ffi/src/ffi/mod.rs b/native/rust/cose_openssl/cose_openssl_ffi/src/ffi/mod.rs new file mode 100644 index 00000000..85da5bda --- /dev/null +++ b/native/rust/cose_openssl/cose_openssl_ffi/src/ffi/mod.rs @@ -0,0 +1,404 @@ +mod helpers; + +use cose_openssl::EvpKey; +use cose_openssl::cose_verify1; +use helpers::*; + +/// Free a buffer previously returned by `cose_sign` / `cose_sign_detached`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn cose_free(ptr: *mut u8, len: usize) { + if !ptr.is_null() && len > 0 { + unsafe { + drop(Vec::from_raw_parts(ptr, len, len)); + } + } +} + +/// Sign with embedded payload. +/// +/// Produces a complete COSE_Sign1 envelope (tag 18). +/// +/// * `phdr` - serialised CBOR map (protected header, **without** alg). +/// * `uhdr` - serialised CBOR map (unprotected header). +/// * `payload` - raw payload bytes. +/// * `key_der` - DER-encoded private key. +/// * `out_ptr` / `out_len` - on success, receives the COSE_Sign1 bytes +/// (caller must free with `cose_free`). +/// +/// Returns 0 on success, -1 on error. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn cose_sign( + phdr_ptr: *const u8, + phdr_len: usize, + uhdr_ptr: *const u8, + uhdr_len: usize, + payload_ptr: *const u8, + payload_len: usize, + key_der_ptr: *const u8, + key_der_len: usize, + out_ptr: *mut *mut u8, + out_len: *mut usize, +) -> i32 { + unsafe { + sign_inner( + phdr_ptr, + phdr_len, + uhdr_ptr, + uhdr_len, + payload_ptr, + payload_len, + key_der_ptr, + key_der_len, + out_ptr, + out_len, + false, + ) + } +} + +/// Sign with detached payload. +/// +/// Same as `cose_sign` but the COSE_Sign1 envelope carries a CBOR null +/// instead of the payload (the payload must be supplied separately at +/// verification time). +/// +/// Returns 0 on success, -1 on error. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn cose_sign_detached( + phdr_ptr: *const u8, + phdr_len: usize, + uhdr_ptr: *const u8, + uhdr_len: usize, + payload_ptr: *const u8, + payload_len: usize, + key_der_ptr: *const u8, + key_der_len: usize, + out_ptr: *mut *mut u8, + out_len: *mut usize, +) -> i32 { + unsafe { + sign_inner( + phdr_ptr, + phdr_len, + uhdr_ptr, + uhdr_len, + payload_ptr, + payload_len, + key_der_ptr, + key_der_len, + out_ptr, + out_len, + true, + ) + } +} + +/// Verify a COSE_Sign1 envelope with embedded payload. +/// +/// * `envelope` - the full COSE_Sign1 bytes. +/// * `key_der` - DER-encoded public key (SubjectPublicKeyInfo). +/// +/// Returns 1 if the signature is valid, 0 if invalid, -1 on error. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn cose_verify( + envelope_ptr: *const u8, + envelope_len: usize, + key_der_ptr: *const u8, + key_der_len: usize, +) -> i32 { + unsafe { + let key = + match EvpKey::from_der_public(as_slice(key_der_ptr, key_der_len)) { + Ok(k) => k, + Err(_) => return -1, + }; + + match cose_verify1(&key, as_slice(envelope_ptr, envelope_len), None) { + Ok(true) => 1, + Ok(false) => 0, + Err(_) => -1, + } + } +} + +/// Verify a COSE_Sign1 envelope with a detached payload. +/// +/// * `envelope` - the COSE_Sign1 bytes (payload slot is CBOR null). +/// * `payload` - the detached payload bytes. +/// * `key_der` - DER-encoded public key (SubjectPublicKeyInfo). +/// +/// Returns 1 if the signature is valid, 0 if invalid, -1 on error. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn cose_verify_detached( + envelope_ptr: *const u8, + envelope_len: usize, + payload_ptr: *const u8, + payload_len: usize, + key_der_ptr: *const u8, + key_der_len: usize, +) -> i32 { + unsafe { + let key = + match EvpKey::from_der_public(as_slice(key_der_ptr, key_der_len)) { + Ok(k) => k, + Err(_) => return -1, + }; + + match cose_verify1( + &key, + as_slice(envelope_ptr, envelope_len), + Some(as_slice(payload_ptr, payload_len)), + ) { + Ok(true) => 1, + Ok(false) => 0, + Err(_) => -1, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cose_openssl::{KeyType, WhichEC}; + use std::ptr; + + const TEST_PHDR: &str = "A319018B020FA3061A698B72820173736572766963652E6578616D706C652E636F6D02706C65646765722E7369676E6174757265666363662E7631A1647478696465322E313334"; + + /// Helper: generate a key pair, return (priv_der, pub_der). + fn make_key_pair(typ: KeyType) -> (Vec, Vec) { + let key = EvpKey::new(typ).unwrap(); + let priv_der = key.to_der_private().unwrap(); + let pub_der = key.to_der_public().unwrap(); + (priv_der, pub_der) + } + + #[test] + fn ffi_sign_verify_ec() { + let (priv_der, pub_der) = make_key_pair(KeyType::EC(WhichEC::P256)); + let phdr = hex::decode(TEST_PHDR).unwrap(); + let uhdr = b"\xa0"; + let payload = b"ffi roundtrip"; + + let mut out_ptr: *mut u8 = ptr::null_mut(); + let mut out_len: usize = 0; + + let rc = unsafe { + cose_sign( + phdr.as_ptr(), + phdr.len(), + uhdr.as_ptr(), + uhdr.len(), + payload.as_ptr(), + payload.len(), + priv_der.as_ptr(), + priv_der.len(), + &mut out_ptr, + &mut out_len, + ) + }; + assert_eq!(rc, 0); + assert!(!out_ptr.is_null()); + assert!(out_len > 0); + + let rc = unsafe { + cose_verify(out_ptr, out_len, pub_der.as_ptr(), pub_der.len()) + }; + assert_eq!(rc, 1); + + unsafe { cose_free(out_ptr, out_len) }; + } + + #[test] + fn ffi_sign_detached_verify_detached_ec() { + let (priv_der, pub_der) = make_key_pair(KeyType::EC(WhichEC::P384)); + let phdr = hex::decode(TEST_PHDR).unwrap(); + let uhdr = b"\xa0"; + let payload = b"detached ffi"; + + let mut out_ptr: *mut u8 = ptr::null_mut(); + let mut out_len: usize = 0; + + let rc = unsafe { + cose_sign_detached( + phdr.as_ptr(), + phdr.len(), + uhdr.as_ptr(), + uhdr.len(), + payload.as_ptr(), + payload.len(), + priv_der.as_ptr(), + priv_der.len(), + &mut out_ptr, + &mut out_len, + ) + }; + assert_eq!(rc, 0); + + // Verify with detached payload. + let rc = unsafe { + cose_verify_detached( + out_ptr, + out_len, + payload.as_ptr(), + payload.len(), + pub_der.as_ptr(), + pub_der.len(), + ) + }; + assert_eq!(rc, 1); + + // Non-detached verify must fail (payload slot is null). + let rc = unsafe { + cose_verify(out_ptr, out_len, pub_der.as_ptr(), pub_der.len()) + }; + assert_eq!(rc, -1); + + unsafe { cose_free(out_ptr, out_len) }; + } + + #[test] + fn ffi_verify_wrong_key_returns_zero() { + let (priv_der, _) = make_key_pair(KeyType::EC(WhichEC::P256)); + let (_, other_pub) = make_key_pair(KeyType::EC(WhichEC::P256)); + let phdr = hex::decode(TEST_PHDR).unwrap(); + let uhdr = b"\xa0"; + let payload = b"wrong key"; + + let mut out_ptr: *mut u8 = ptr::null_mut(); + let mut out_len: usize = 0; + + let rc = unsafe { + cose_sign( + phdr.as_ptr(), + phdr.len(), + uhdr.as_ptr(), + uhdr.len(), + payload.as_ptr(), + payload.len(), + priv_der.as_ptr(), + priv_der.len(), + &mut out_ptr, + &mut out_len, + ) + }; + assert_eq!(rc, 0); + + // Verify with a different public key -- signature invalid. + let rc = unsafe { + cose_verify(out_ptr, out_len, other_pub.as_ptr(), other_pub.len()) + }; + assert_eq!(rc, 0); + + unsafe { cose_free(out_ptr, out_len) }; + } + + #[test] + fn ffi_sign_bad_key_returns_error() { + let phdr = hex::decode(TEST_PHDR).unwrap(); + let uhdr = b"\xa0"; + let payload = b"bad key"; + let garbage_key = [0xde, 0xad, 0xbe, 0xef]; + + let mut out_ptr: *mut u8 = ptr::null_mut(); + let mut out_len: usize = 0; + + let rc = unsafe { + cose_sign( + phdr.as_ptr(), + phdr.len(), + uhdr.as_ptr(), + uhdr.len(), + payload.as_ptr(), + payload.len(), + garbage_key.as_ptr(), + garbage_key.len(), + &mut out_ptr, + &mut out_len, + ) + }; + assert_eq!(rc, -1); + } + + #[cfg(feature = "pqc")] + mod pqc_tests { + use super::*; + use cose_openssl::WhichMLDSA; + + #[test] + fn ffi_sign_verify_mldsa() { + let (priv_der, pub_der) = + make_key_pair(KeyType::MLDSA(WhichMLDSA::P65)); + let phdr = hex::decode(TEST_PHDR).unwrap(); + let uhdr = b"\xa0"; + let payload = b"mldsa ffi roundtrip"; + + let mut out_ptr: *mut u8 = ptr::null_mut(); + let mut out_len: usize = 0; + + let rc = unsafe { + cose_sign( + phdr.as_ptr(), + phdr.len(), + uhdr.as_ptr(), + uhdr.len(), + payload.as_ptr(), + payload.len(), + priv_der.as_ptr(), + priv_der.len(), + &mut out_ptr, + &mut out_len, + ) + }; + assert_eq!(rc, 0); + + let rc = unsafe { + cose_verify(out_ptr, out_len, pub_der.as_ptr(), pub_der.len()) + }; + assert_eq!(rc, 1); + + unsafe { cose_free(out_ptr, out_len) }; + } + + #[test] + fn ffi_sign_detached_verify_detached_mldsa() { + let (priv_der, pub_der) = + make_key_pair(KeyType::MLDSA(WhichMLDSA::P44)); + let phdr = hex::decode(TEST_PHDR).unwrap(); + let uhdr = b"\xa0"; + let payload = b"mldsa detached ffi"; + + let mut out_ptr: *mut u8 = ptr::null_mut(); + let mut out_len: usize = 0; + + let rc = unsafe { + cose_sign_detached( + phdr.as_ptr(), + phdr.len(), + uhdr.as_ptr(), + uhdr.len(), + payload.as_ptr(), + payload.len(), + priv_der.as_ptr(), + priv_der.len(), + &mut out_ptr, + &mut out_len, + ) + }; + assert_eq!(rc, 0); + + let rc = unsafe { + cose_verify_detached( + out_ptr, + out_len, + payload.as_ptr(), + payload.len(), + pub_der.as_ptr(), + pub_der.len(), + ) + }; + assert_eq!(rc, 1); + + unsafe { cose_free(out_ptr, out_len) }; + } + } +} diff --git a/native/rust/cose_openssl/cose_openssl_ffi/src/lib.rs b/native/rust/cose_openssl/cose_openssl_ffi/src/lib.rs new file mode 100644 index 00000000..9078be84 --- /dev/null +++ b/native/rust/cose_openssl/cose_openssl_ffi/src/lib.rs @@ -0,0 +1,4 @@ +mod ffi; +pub use ffi::{ + cose_free, cose_sign, cose_sign_detached, cose_verify, cose_verify_detached, +}; diff --git a/native/rust/cose_openssl/cpp/cose_openssl_ffi.h b/native/rust/cose_openssl/cpp/cose_openssl_ffi.h new file mode 100644 index 00000000..419fcce0 --- /dev/null +++ b/native/rust/cose_openssl/cpp/cose_openssl_ffi.h @@ -0,0 +1,86 @@ +// C header for the cose-openssl-ffi static library. + +#ifndef COSE_OPENSSL_FFI_H +#define COSE_OPENSSL_FFI_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Free a buffer previously returned by cose_sign / cose_sign_detached. + */ +void cose_free(uint8_t *ptr, size_t len); + +/** + * Sign with embedded payload. + * + * Produces a complete COSE_Sign1 envelope (tag 18). + * + * @param phdr Serialised CBOR map (protected header, without alg). + * @param phdr_len Length of phdr. + * @param uhdr Serialised CBOR map (unprotected header). + * @param uhdr_len Length of uhdr. + * @param payload Raw payload bytes. + * @param payload_len Length of payload. + * @param key_der DER-encoded private key. + * @param key_der_len Length of key_der. + * @param out_ptr On success, receives a pointer to the COSE_Sign1 bytes + * (caller must free with cose_free). + * @param out_len On success, receives the length of the output. + * @return 0 on success, -1 on error. + */ +int32_t cose_sign(const uint8_t *phdr, size_t phdr_len, const uint8_t *uhdr, + size_t uhdr_len, const uint8_t *payload, size_t payload_len, + const uint8_t *key_der, size_t key_der_len, uint8_t **out_ptr, + size_t *out_len); + +/** + * Sign with detached payload. + * + * Same as cose_sign but the COSE_Sign1 envelope carries a CBOR null instead + * of the payload. + * + * @return 0 on success, -1 on error. + */ +int32_t cose_sign_detached(const uint8_t *phdr, size_t phdr_len, + const uint8_t *uhdr, size_t uhdr_len, + const uint8_t *payload, size_t payload_len, + const uint8_t *key_der, size_t key_der_len, + uint8_t **out_ptr, size_t *out_len); + +/** + * Verify a COSE_Sign1 envelope with embedded payload. + * + * @param envelope Full COSE_Sign1 bytes. + * @param envelope_len Length of envelope. + * @param key_der DER-encoded public key (SubjectPublicKeyInfo). + * @param key_der_len Length of key_der. + * @return 1 if valid, 0 if invalid, -1 on error. + */ +int32_t cose_verify(const uint8_t *envelope, size_t envelope_len, + const uint8_t *key_der, size_t key_der_len); + +/** + * Verify a COSE_Sign1 envelope with detached payload. + * + * @param envelope COSE_Sign1 bytes (payload slot is CBOR null). + * @param envelope_len Length of envelope. + * @param payload Detached payload bytes. + * @param payload_len Length of payload. + * @param key_der DER-encoded public key (SubjectPublicKeyInfo). + * @param key_der_len Length of key_der. + * @return 1 if valid, 0 if invalid, -1 on error. + */ +int32_t cose_verify_detached(const uint8_t *envelope, size_t envelope_len, + const uint8_t *payload, size_t payload_len, + const uint8_t *key_der, size_t key_der_len); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* COSE_OPENSSL_FFI_H */ diff --git a/native/rust/cose_sign1_validation/Cargo.toml b/native/rust/cose_sign1_validation/Cargo.toml new file mode 100644 index 00000000..cd798028 --- /dev/null +++ b/native/rust/cose_sign1_validation/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "cose_sign1_validation" +version = "0.1.0" +edition.workspace = true +license.workspace = true + +[lib] +test = false + +[dependencies] +anyhow.workspace = true +thiserror.workspace = true + +sha1.workspace = true +sha2.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" } +cose_sign1_validation_test_utils = { path = "../cose_sign1_validation_test_utils" } diff --git a/native/rust/cose_sign1_validation/README.md b/native/rust/cose_sign1_validation/README.md new file mode 100644 index 00000000..d09ca564 --- /dev/null +++ b/native/rust/cose_sign1_validation/README.md @@ -0,0 +1,34 @@ +# 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 +- The post-signature stage includes a built-in validator for indirect signature formats (e.g. `+cose-hash-v` / hash envelopes) when detached payload verification is used. +- 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`) + +## Recommended API + +For new integrations, treat the fluent surface as the primary entrypoint: + +- `use cose_sign1_validation::fluent::*;` + +This keeps policy authoring and validation setup on the same, cohesive API. + +## 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](../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..b7c04011 --- /dev/null +++ b/native/rust/cose_sign1_validation/examples/detached_payload_provider.rs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use cose_sign1_validation::fluent::*; + +fn main() { + use std::io::Cursor; + + let testdata_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("testdata") + .join("v1"); + + let cose_bytes = std::fs::read(testdata_dir.join("UnitTestSignatureWithCRL.cose")) + .expect("read cose testdata"); + let payload_bytes = + std::fs::read(testdata_dir.join("UnitTestPayload.json")).expect("read payload testdata"); + + let payload: std::sync::Arc<[u8]> = std::sync::Arc::from(payload_bytes.into_boxed_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 cert_pack = std::sync::Arc::new( + cose_sign1_validation_certificates::pack::X509CertificateTrustPack::new( + cose_sign1_validation_certificates::pack::CertificateTrustOptions { + trust_embedded_chain_as_trusted: true, + ..Default::default() + }, + ), + ); + let trust_packs: Vec> = vec![cert_pack]; + + let validator = CoseSign1Validator::new(trust_packs).with_options(|o| { + o.detached_payload = Some(DetachedPayload::Provider(std::sync::Arc::new(provider))); + o.certificate_header_location = cose_sign1_validation_trust::CoseHeaderLocation::Any; + o.trust_evaluation_options.bypass_trust = true; + }); + + let result = validator + .validate_bytes(std::sync::Arc::from(cose_bytes.into_boxed_slice())) + .expect("validation failed"); + + assert!( + result.signature.is_valid(), + "signature invalid: {:#?}", + result.signature + ); + println!("OK: detached payload verified (provider)"); +} diff --git a/native/rust/cose_sign1_validation/examples/validate_custom_policy.rs b/native/rust/cose_sign1_validation/examples/validate_custom_policy.rs new file mode 100644 index 00000000..af829d78 --- /dev/null +++ b/native/rust/cose_sign1_validation/examples/validate_custom_policy.rs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::sync::Arc; + +use cose_sign1_validation::fluent::*; +use cose_sign1_validation_certificates::fluent_ext::PrimarySigningKeyScopeRulesExt; +use cose_sign1_validation_certificates::pack::{CertificateTrustOptions, X509CertificateTrustPack}; +use cose_sign1_validation_trust::CoseHeaderLocation; + +fn main() { + // This example demonstrates a "real" integration shape: + // - choose packs + // - compile an explicit trust plan (policy) + // - configure detached payload + // - validate and print feedback + + let args: Vec = std::env::args().collect(); + + // Usage: + // validate_custom_policy [detached_payload.bin] + // If no args are supplied, fall back to an in-repo test vector (may fail depending on algorithms). + let (cose_bytes, payload_bytes) = if args.len() >= 2 { + let cose_path = &args[1]; + let payload_path = args.get(2); + let cose = std::fs::read(cose_path).expect("read cose file"); + let payload = payload_path.map(|p| std::fs::read(p).expect("read payload file")); + (cose, payload) + } else { + let testdata_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("testdata") + .join("v1"); + + let cose = std::fs::read(testdata_dir.join("UnitTestSignatureWithCRL.cose")) + .expect("read cose testdata"); + let payload = + std::fs::read(testdata_dir.join("UnitTestPayload.json")).expect("read payload testdata"); + (cose, Some(payload)) + }; + + // 1) Packs + let cert_pack = Arc::new(X509CertificateTrustPack::new(CertificateTrustOptions { + // Deterministic for examples/tests: treat embedded x5chain as trusted. + // In production, configure trust roots / revocation rather than enabling this. + trust_embedded_chain_as_trusted: true, + ..Default::default() + })); + + let trust_packs: Vec> = vec![cert_pack]; + + // 2) Custom plan + let plan = TrustPlanBuilder::new(trust_packs).for_primary_signing_key(|key| { + key.require_x509_chain_trusted() + .and() + .require_signing_certificate_present() + .and() + .require_leaf_chain_thumbprint_present() + }) + .compile() + .expect("plan compile"); + + // 3) Validator + detached payload configuration + let validator = CoseSign1Validator::new(plan).with_options(|o| { + if let Some(payload_bytes) = payload_bytes.clone() { + o.detached_payload = Some(DetachedPayload::bytes(Arc::from( + payload_bytes.into_boxed_slice(), + ))); + } + o.certificate_header_location = CoseHeaderLocation::Any; + }); + + // 4) Validate + let result = validator + .validate_bytes(Arc::from(cose_bytes.into_boxed_slice())) + .expect("validation pipeline error"); + + println!("resolution: {:?}", result.resolution.kind); + println!("trust: {:?}", result.trust.kind); + println!("signature: {:?}", result.signature.kind); + println!("post_signature_policy: {:?}", result.post_signature_policy.kind); + println!("overall: {:?}", result.overall.kind); + + if result.overall.is_valid() { + println!("Validation successful"); + return; + } + + let stages = [ + ("resolution", &result.resolution), + ("trust", &result.trust), + ("signature", &result.signature), + ("post_signature_policy", &result.post_signature_policy), + ("overall", &result.overall), + ]; + + for (name, stage) in stages { + if stage.failures.is_empty() { + continue; + } + + eprintln!("{name} failures:"); + for failure in &stage.failures { + eprintln!("- {}", failure.message); + } + } +} 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..cb981e7d --- /dev/null +++ b/native/rust/cose_sign1_validation/examples/validate_smoke.rs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use cose_sign1_validation::fluent::*; + +fn main() { + // This example demonstrates the recommended integration pattern: + // - use the fluent API surface (`cose_sign1_validation::fluent::*`) + // - wire one or more trust packs (here: the certificates pack) + // - optionally bypass trust while still verifying the cryptographic signature + + let testdata_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("testdata") + .join("v1"); + + // Real COSE + payload test vector. + let cose_bytes = std::fs::read(testdata_dir.join("UnitTestSignatureWithCRL.cose")) + .expect("read cose testdata"); + let payload_bytes = + std::fs::read(testdata_dir.join("UnitTestPayload.json")).expect("read payload testdata"); + + let cert_pack = std::sync::Arc::new( + cose_sign1_validation_certificates::pack::X509CertificateTrustPack::new( + cose_sign1_validation_certificates::pack::CertificateTrustOptions { + // Deterministic for a local example: treat embedded x5chain as trusted. + trust_embedded_chain_as_trusted: true, + ..Default::default() + }, + ), + ); + + let trust_packs: Vec> = vec![cert_pack]; + + let validator = CoseSign1Validator::new(trust_packs).with_options(|o| { + o.detached_payload = Some(DetachedPayload::bytes(std::sync::Arc::from( + payload_bytes.into_boxed_slice(), + ))); + o.certificate_header_location = cose_sign1_validation_trust::CoseHeaderLocation::Any; + + // Trust is often environment-dependent (roots/CRLs/OCSP). For a smoke example, + // keep trust bypassed but still verify the signature. + o.trust_evaluation_options.bypass_trust = true; + }); + + let result = validator + .validate_bytes(std::sync::Arc::from(cose_bytes.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..b3042d38 --- /dev/null +++ b/native/rust/cose_sign1_validation/src/cose.rs @@ -0,0 +1,140 @@ +// 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 { + /// Helper to normalize CBOR/COSE decoding failures into `CoseDecodeError::Cbor`. + /// + /// This is used to avoid leaking the underlying error type across this crate boundary. + fn cbor(e: E) -> Self { + Self::Cbor(e.to_string()) + } +} + +impl<'a> CoseSign1<'a> { + /// Decode a COSE_Sign1 message from CBOR. + /// + /// Accepts (and strips) an optional leading CBOR tag 18, which some encoders wrap around + /// the COSE_Sign1 structure. + 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, + }) + } +} + +/// If `input` begins with CBOR tag 18, returns a slice starting after the tag. +/// +/// Returns: +/// - `Ok(Some(rest))` if tag 18 is present and decoded successfully. +/// - `Ok(None)` if no CBOR tag is present (caller should treat `input` as the message). +/// - `Err(...)` if a tag is present but is not tag 18, or the tag encoding is invalid. +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..)) +} + +/// Decode the unsigned integer value for a CBOR additional-information (AI) field. +/// +/// The returned tuple is `(value, bytes_consumed_from_rest)`. +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/fluent.rs b/native/rust/cose_sign1_validation/src/fluent.rs new file mode 100644 index 00000000..79e7d326 --- /dev/null +++ b/native/rust/cose_sign1_validation/src/fluent.rs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Fluent-first API surface. +//! +//! This module is the intended "customer" entrypoint for policy authoring and validation. +//! It re-exports the handful of types needed to: +//! - build a trust policy (`TrustPlanBuilder`) +//! - compile/bundle it (`CoseSign1CompiledTrustPlan`) +//! - run validation (`CoseSign1Validator`) +//! +//! Pack-specific fluent extensions live in their respective crates, for example: +//! - `cose_sign1_validation_transparent_mst::fluent_ext::*` +//! - `cose_sign1_validation_certificates::fluent_ext::*` +//! - `cose_sign1_validation_azure_key_vault::fluent_ext::*` + +use std::sync::Arc; + +// Core validation entrypoints +pub use crate::validator::{ + CoseSign1ValidationError, CoseSign1ValidationOptions, CoseSign1ValidationResult, + CoseSign1Validator, CounterSignature, CounterSignatureResolutionResult, + CounterSignatureResolver, DetachedPayload, DetachedPayloadFnProvider, DetachedPayloadProvider, + PostSignatureValidationContext, PostSignatureValidator, SigningKey, SigningKeyResolutionResult, + SigningKeyResolver, ValidationFailure, ValidationResult, ValidationResultKind, +}; + +// Message representation +pub use crate::cose::{CoseDecodeError, CoseSign1}; + +// Message fact producer (useful for tests and custom pack authors) +pub use crate::message_fact_producer::CoseSign1MessageFactProducer; + +// Trust-pack plumbing +pub use crate::trust_packs::CoseSign1TrustPack; + +// Trust-plan authoring (CoseSign1 wrapper) +pub use crate::trust_plan_builder::{ + CoseSign1CompiledTrustPlan, OnEmptyBehavior, TrustPlanBuilder, TrustPlanCompileError, +}; + +// Trust DSL building blocks (needed for extension traits and advanced policies) +pub use cose_sign1_validation_trust::fluent::{ + MessageScope, PrimarySigningKeyScope, ScopeRules, SubjectsFromFactsScope, Where, +}; + +// Built-in message-scope fluent extensions +pub use crate::message_facts::fluent_ext::*; + +// Common fact types used for scoping and advanced inspection. +pub use crate::message_facts::{ + CborValueReader, ContentTypeFact, CoseSign1MessageBytesFact, CoseSign1MessagePartsFact, + CounterSignatureEnvelopeIntegrityFact, CounterSignatureSigningKeySubjectFact, + CounterSignatureSubjectFact, CwtClaimsFact, CwtClaimsPresentFact, DetachedPayloadPresentFact, + PrimarySigningKeySubjectFact, UnknownCounterSignatureBytesFact, + CwtClaimScalar, +}; + +/// Build a [`CoseSign1Validator`] from trust packs and a fluent policy closure. +/// +/// This is the most compact "customer path": you provide the packs and express policy in the +/// closure; we compile and bundle the plan and return a ready-to-use validator. +pub fn build_validator_with_policy( + trust_packs: Vec>, + policy: impl FnOnce(TrustPlanBuilder) -> TrustPlanBuilder, +) -> Result { + let plan = policy(TrustPlanBuilder::new(trust_packs)).compile()?; + Ok(CoseSign1Validator::new(plan)) +} diff --git a/native/rust/cose_sign1_validation/src/indirect_signature.rs b/native/rust/cose_sign1_validation/src/indirect_signature.rs new file mode 100644 index 00000000..da9563b6 --- /dev/null +++ b/native/rust/cose_sign1_validation/src/indirect_signature.rs @@ -0,0 +1,404 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::validator::{PostSignatureValidationContext, PostSignatureValidator, ValidationResult}; +use cose_sign1_validation_trust::{CoseHeaderLocation, CoseHeaderMap}; +use once_cell::sync::Lazy; +use regex::Regex; +use sha1::Digest as _; +use std::io::Read; + +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()); + +const VALIDATOR_NAME: &str = "Indirect Signature Content Validation"; + +const COSE_HEADER_LABEL_ALG: i64 = 1; +const COSE_HEADER_LABEL_CONTENT_TYPE: i64 = 3; + +// COSE Hash Envelope header labels. +const COSE_HASH_ENVELOPE_PAYLOAD_HASH_ALG: i64 = 258; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum IndirectSignatureKind { + LegacyHashExtension, + CoseHashV, + CoseHashEnvelope, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum HashAlgorithm { + Sha256, + Sha384, + Sha512, + Sha1, +} + +impl HashAlgorithm { + fn name(&self) -> &'static str { + match self { + Self::Sha256 => "SHA256", + Self::Sha384 => "SHA384", + Self::Sha512 => "SHA512", + Self::Sha1 => "SHA1", + } + } +} + +fn cose_hash_alg_from_cose_alg_value(value: i64) -> Option { + // COSE hash algorithm IDs (IANA): + // -16 SHA-256, -43 SHA-384, -44 SHA-512 + // (We also accept SHA-1 (-14) for legacy compatibility.) + match value { + -16 => Some(HashAlgorithm::Sha256), + -43 => Some(HashAlgorithm::Sha384), + -44 => Some(HashAlgorithm::Sha512), + -14 => Some(HashAlgorithm::Sha1), + _ => None, + } +} + +fn legacy_hash_alg_from_name(name: &str) -> Option { + let upper = name.trim().to_ascii_uppercase(); + match upper.as_str() { + "SHA256" => Some(HashAlgorithm::Sha256), + "SHA384" => Some(HashAlgorithm::Sha384), + "SHA512" => Some(HashAlgorithm::Sha512), + "SHA1" => Some(HashAlgorithm::Sha1), + _ => None, + } +} + +fn header_text_or_utf8_bytes(map: &CoseHeaderMap, label: i64) -> Option { + let v = map.get(label)?; + if let Some(s) = v.as_text() { + return Some(s.to_string()); + } + if let Some(b) = v.as_bytes() { + return std::str::from_utf8(b).ok().map(|s| s.to_string()); + } + None +} + +fn read_unprotected_header_map(message: &crate::cose::CoseSign1<'_>) -> Result { + CoseHeaderMap::from_cbor_map_bytes(message.unprotected_header.as_ref()) +} + +fn read_protected_header_map(message: &crate::cose::CoseSign1<'_>) -> Result { + CoseHeaderMap::from_cbor_map_bytes(message.protected_header) +} + +fn detect_indirect_signature_kind(protected: &CoseHeaderMap, content_type: Option<&str>) -> Option { + if protected.get(COSE_HASH_ENVELOPE_PAYLOAD_HASH_ALG).is_some() { + return Some(IndirectSignatureKind::CoseHashEnvelope); + } + + let ct = content_type?; + + if COSE_HASH_V.is_match(ct) { + return Some(IndirectSignatureKind::CoseHashV); + } + + if HASH_LEGACY.is_match(ct) { + return Some(IndirectSignatureKind::LegacyHashExtension); + } + + None +} + +fn compute_hash_bytes(alg: HashAlgorithm, data: &[u8]) -> Vec { + match alg { + HashAlgorithm::Sha256 => sha2::Sha256::digest(data).to_vec(), + HashAlgorithm::Sha384 => sha2::Sha384::digest(data).to_vec(), + HashAlgorithm::Sha512 => sha2::Sha512::digest(data).to_vec(), + HashAlgorithm::Sha1 => sha1::Sha1::digest(data).to_vec(), + } +} + +fn compute_hash_reader(alg: HashAlgorithm, mut reader: impl Read) -> Result, String> { + let mut buf = [0u8; 64 * 1024]; + match alg { + HashAlgorithm::Sha256 => { + let mut hasher = sha2::Sha256::new(); + loop { + let read = reader + .read(&mut buf) + .map_err(|e| format!("detached_payload_read_failed: {e}"))?; + if read == 0 { + break; + } + hasher.update(&buf[..read]); + } + Ok(hasher.finalize().to_vec()) + } + HashAlgorithm::Sha384 => { + let mut hasher = sha2::Sha384::new(); + loop { + let read = reader + .read(&mut buf) + .map_err(|e| format!("detached_payload_read_failed: {e}"))?; + if read == 0 { + break; + } + hasher.update(&buf[..read]); + } + Ok(hasher.finalize().to_vec()) + } + HashAlgorithm::Sha512 => { + let mut hasher = sha2::Sha512::new(); + loop { + let read = reader + .read(&mut buf) + .map_err(|e| format!("detached_payload_read_failed: {e}"))?; + if read == 0 { + break; + } + hasher.update(&buf[..read]); + } + Ok(hasher.finalize().to_vec()) + } + HashAlgorithm::Sha1 => { + let mut hasher = sha1::Sha1::new(); + loop { + let read = reader + .read(&mut buf) + .map_err(|e| format!("detached_payload_read_failed: {e}"))?; + if read == 0 { + break; + } + hasher.update(&buf[..read]); + } + Ok(hasher.finalize().to_vec()) + } + } +} + +fn compute_hash_from_detached_payload( + alg: HashAlgorithm, + payload: &crate::validator::DetachedPayload, +) -> Result, String> { + match payload { + crate::validator::DetachedPayload::Bytes(b) => { + if b.is_empty() { + return Err("detached payload was empty".to_string()); + } + Ok(compute_hash_bytes(alg, b.as_ref())) + } + crate::validator::DetachedPayload::Provider(p) => { + let reader = p.open()?; + compute_hash_reader(alg, reader) + } + } +} + +fn parse_cose_hash_v(payload: &[u8]) -> Result<(HashAlgorithm, Vec), String> { + let mut d = tinycbor::Decoder(payload); + let mut array = d + .array_visitor() + .map_err(|e| format!("invalid COSE_Hash_V: {e}"))?; + + let alg = array + .visit::() + .ok_or_else(|| "invalid COSE_Hash_V: missing alg".to_string()) + .map_err(|e| format!("invalid COSE_Hash_V: {e}"))?; + let alg = alg.map_err(|e| format!("invalid COSE_Hash_V: {e}"))?; + + let hash_bytes = array + .visit::<&[u8]>() + .ok_or_else(|| "invalid COSE_Hash_V: missing hash".to_string()) + .map_err(|e| format!("invalid COSE_Hash_V: {e}"))?; + let hash_bytes = hash_bytes.map_err(|e| format!("invalid COSE_Hash_V: {e}"))?; + + let alg = cose_hash_alg_from_cose_alg_value(alg) + .ok_or_else(|| format!("unsupported COSE_Hash_V algorithm {alg}"))?; + + if hash_bytes.is_empty() { + return Err("invalid COSE_Hash_V: empty hash".to_string()); + } + + Ok((alg, hash_bytes.to_vec())) +} + +pub struct IndirectSignaturePostSignatureValidator; + +impl IndirectSignaturePostSignatureValidator { + pub fn new() -> Self { + Self + } +} + +impl PostSignatureValidator for IndirectSignaturePostSignatureValidator { + fn validate(&self, context: &PostSignatureValidationContext<'_>) -> ValidationResult { + let Some(detached_payload) = context.options.detached_payload.as_ref() else { + // Treat this as "signature-only verification". + return ValidationResult::not_applicable( + VALIDATOR_NAME, + Some("No detached payload provided (signature-only verification)"), + ); + }; + + let protected = match read_protected_header_map(context.message) { + Ok(m) => m, + Err(e) => { + return ValidationResult::failure_message( + VALIDATOR_NAME, + format!("protected_header_decode_failed: {e}"), + Some("INDIRECT_SIGNATURE_HEADER_DECODE_FAILED"), + ) + } + }; + + let mut content_type = header_text_or_utf8_bytes(&protected, COSE_HEADER_LABEL_CONTENT_TYPE); + let mut kind = detect_indirect_signature_kind(&protected, content_type.as_deref()); + + // Some producers may place Content-Type in the unprotected header. Only consult + // unprotected headers when the caller's configuration allows it. + if context.options.certificate_header_location == CoseHeaderLocation::Any + && kind.is_none() + && content_type.is_none() + { + if let Ok(unprotected) = read_unprotected_header_map(context.message) { + content_type = header_text_or_utf8_bytes(&unprotected, COSE_HEADER_LABEL_CONTENT_TYPE); + kind = detect_indirect_signature_kind(&protected, content_type.as_deref()); + } + } + + let kind = match kind { + Some(k) => k, + None => { + return ValidationResult::not_applicable(VALIDATOR_NAME, Some("Not an indirect signature")) + } + }; + + // Validate minimal envelope rules when detected (matches V1 expectations). + if kind == IndirectSignatureKind::CoseHashEnvelope { + match read_unprotected_header_map(context.message) { + Ok(unprotected) => { + if unprotected.get(COSE_HASH_ENVELOPE_PAYLOAD_HASH_ALG).is_some() { + return ValidationResult::failure_message( + VALIDATOR_NAME, + "CoseHashEnvelope payload-hash-alg (258) must not be present in unprotected headers", + Some("INDIRECT_SIGNATURE_INVALID_HEADERS"), + ); + } + } + Err(e) => { + return ValidationResult::failure_message( + VALIDATOR_NAME, + format!("unprotected_header_decode_failed: {e}"), + Some("INDIRECT_SIGNATURE_HEADER_DECODE_FAILED"), + ) + } + } + } + + let Some(payload) = context.message.payload else { + return ValidationResult::failure_message( + VALIDATOR_NAME, + "Indirect signature validation requires an embedded payload", + Some("INDIRECT_SIGNATURE_MISSING_HASH"), + ); + }; + + // Determine the hash algorithm and the stored expected hash. + let (alg, expected_hash, format_name) = match kind { + IndirectSignatureKind::LegacyHashExtension => { + let ct = content_type.unwrap_or_default(); + let caps = HASH_LEGACY + .captures(&ct) + .and_then(|c| c.get(1).map(|m| m.as_str().to_string())); + + let Some(alg_name) = caps else { + return ValidationResult::failure_message( + VALIDATOR_NAME, + "Indirect signature content-type did not contain a +hash-* extension", + Some("INDIRECT_SIGNATURE_UNSUPPORTED_FORMAT"), + ); + }; + + let Some(alg) = legacy_hash_alg_from_name(&alg_name) else { + return ValidationResult::failure_message( + VALIDATOR_NAME, + format!("Unsupported legacy hash algorithm '{alg_name}'"), + Some("INDIRECT_SIGNATURE_UNSUPPORTED_ALGORITHM"), + ); + }; + + (alg, payload.to_vec(), "Legacy+hash-*") + } + IndirectSignatureKind::CoseHashV => match parse_cose_hash_v(payload) { + Ok((alg, hash)) => (alg, hash, "COSE_Hash_V"), + Err(e) => { + return ValidationResult::failure_message( + VALIDATOR_NAME, + e, + Some("INDIRECT_SIGNATURE_INVALID_COSE_HASH_V"), + ) + } + }, + IndirectSignatureKind::CoseHashEnvelope => { + let Some(alg_raw) = protected.get_i64(COSE_HASH_ENVELOPE_PAYLOAD_HASH_ALG) else { + return ValidationResult::failure_message( + VALIDATOR_NAME, + "CoseHashEnvelope payload-hash-alg (258) missing from protected headers", + Some("INDIRECT_SIGNATURE_INVALID_HEADERS"), + ); + }; + + let Some(alg) = cose_hash_alg_from_cose_alg_value(alg_raw) else { + return ValidationResult::failure_message( + VALIDATOR_NAME, + format!("Unsupported CoseHashEnvelope hash algorithm {alg_raw}"), + Some("INDIRECT_SIGNATURE_UNSUPPORTED_ALGORITHM"), + ); + }; + + (alg, payload.to_vec(), "CoseHashEnvelope") + } + }; + + // Compute the artifact hash and compare. + let actual_hash = match compute_hash_from_detached_payload(alg, detached_payload) { + Ok(v) => v, + Err(e) => { + return ValidationResult::failure_message( + VALIDATOR_NAME, + e, + Some("INDIRECT_SIGNATURE_PAYLOAD_READ_FAILED"), + ) + } + }; + + if actual_hash == expected_hash { + let mut metadata = std::collections::BTreeMap::new(); + metadata.insert("IndirectSignature.Format".to_string(), format_name.to_string()); + metadata.insert("IndirectSignature.HashAlgorithm".to_string(), alg.name().to_string()); + ValidationResult::success(VALIDATOR_NAME, Some(metadata)) + } else { + ValidationResult::failure_message( + VALIDATOR_NAME, + format!( + "Indirect signature content did not match ({format_name}, {})", + alg.name() + ), + Some("INDIRECT_SIGNATURE_CONTENT_MISMATCH"), + ) + } + } + + fn validate_async<'a>( + &'a self, + context: &'a PostSignatureValidationContext<'a>, + ) -> crate::validator::BoxFuture<'a, ValidationResult> { + // Implementation is synchronous (hashing is done with a blocking reader). + Box::pin(async move { self.validate(context) }) + } +} + +// Ensure the core header label constants stay in sync with COSE conventions. +// This is compile-time only (no runtime code, so it doesn't affect coverage). +const _: () = { + let _ = COSE_HEADER_LABEL_ALG; + let _ = COSE_HEADER_LABEL_CONTENT_TYPE; +}; diff --git a/native/rust/cose_sign1_validation/src/internal.rs b/native/rust/cose_sign1_validation/src/internal.rs new file mode 100644 index 00000000..c262f6b2 --- /dev/null +++ b/native/rust/cose_sign1_validation/src/internal.rs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Legacy/advanced API surface. +//! +//! This module is intentionally hidden from generated docs. +//! +//! Most consumers should use `cose_sign1_validation::fluent`. + +// Keep the internal module paths available under `internal::*` for: +// - tests +// - deep debugging +// - advanced integrations + +pub mod cose { + pub use crate::cose::*; +} + +pub use crate::cose::CoseSign1; + +pub use crate::message_fact_producer::CoseSign1MessageFactProducer; + +pub use crate::message_facts::{ + CborValueReader, ContentTypeFact, CoseSign1MessageBytesFact, CoseSign1MessagePartsFact, + CounterSignatureEnvelopeIntegrityFact, CounterSignatureSigningKeySubjectFact, + CounterSignatureSubjectFact, CwtClaimScalar, CwtClaimsFact, CwtClaimsPresentFact, + DetachedPayloadPresentFact, PrimarySigningKeySubjectFact, UnknownCounterSignatureBytesFact, +}; + +pub use crate::trust_plan_builder::{ + CoseSign1CompiledTrustPlan, OnEmptyBehavior, TrustPlanBuilder, TrustPlanCompileError, +}; + +pub use crate::trust_packs::CoseSign1TrustPack; + +pub use crate::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/lib.rs b/native/rust/cose_sign1_validation/src/lib.rs new file mode 100644 index 00000000..c80dfc39 --- /dev/null +++ b/native/rust/cose_sign1_validation/src/lib.rs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! COSE_Sign1 validation entrypoint. +//! +//! This crate provides the primary validation API for COSE_Sign1 messages. +//! New integrations should start with the fluent surface in [`fluent`], which +//! wires together: +//! - COSE parsing +//! - Signature verification via trust packs +//! - Trust evaluation via the `cose_sign1_validation_trust` engine +//! +//! For advanced/legacy scenarios, lower-level APIs exist under [`internal`], but +//! the fluent surface is the intended stable integration point. + +/// Fluent-first API entrypoint. +/// +/// New integrations should prefer importing from `cose_sign1_validation::fluent`. +pub mod fluent; + +/// Legacy/advanced surface (intentionally hidden from docs). +#[doc(hidden)] +pub mod internal; + +mod cose; + +mod message_fact_producer; +mod message_facts; +mod trust_packs; +mod trust_plan_builder; +mod validator; + +mod indirect_signature; 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..5f1677bc --- /dev/null +++ b/native/rust/cose_sign1_validation/src/message_fact_producer.rs @@ -0,0 +1,573 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::cose::CoseSign1; +use crate::message_facts::{ + ContentTypeFact, CoseSign1MessageBytesFact, CoseSign1MessagePartsFact, + CounterSignatureSigningKeySubjectFact, CounterSignatureSubjectFact, CwtClaimScalar, + CwtClaimsFact, CwtClaimsPresentFact, 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 { + /// Create a producer with default settings. + /// + /// By default, no counter-signature resolvers are configured; counter-signature discovery is + /// therefore a no-op. + pub fn new() -> Self { + Self::default() + } + + /// Attach counter-signature resolvers used to discover counter-signatures from message parts. + /// + /// These resolvers are only consulted when producing facts for the `Message` subject. + pub fn with_counter_signature_resolvers( + mut self, + resolvers: Vec>, + ) -> Self { + self.counter_signature_resolvers = resolvers; + self + } +} + +impl TrustFactProducer for CoseSign1MessageFactProducer { + /// A stable name used in diagnostics and audit trails. + fn name(&self) -> &'static str { + "cose_sign1_validation::CoseSign1MessageFactProducer" + } + + /// Produce message-derived facts for the current subject. + /// + /// This producer is intentionally conservative: + /// - It only produces facts for the `Message` subject kind. + /// - It prefers the parsed message already available in the engine context. + /// - When parsing fails, it marks appropriate facts as `Error`/`Missing` rather than panicking. + 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() { + let protected_header = Arc::new(pm.protected_header_bytes.as_ref().to_vec()); + let unprotected_header = Arc::new(pm.unprotected_header_bytes.as_ref().to_vec()); + let payload = pm.payload.as_ref().map(|p| Arc::new(p.as_ref().to_vec())); + let signature = Arc::new(pm.signature.as_ref().to_vec()); + + ctx.observe(CoseSign1MessagePartsFact { + protected_header, + unprotected_header, + payload, + signature, + })?; + + 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(()); + } + }; + + let protected_header = Arc::new(msg.protected_header.to_vec()); + let unprotected_header = Arc::new(msg.unprotected_header.as_ref().to_vec()); + let payload = msg.payload.map(|p| Arc::new(p.to_vec())); + let signature = Arc::new(msg.signature.to_vec()); + + ctx.observe(CoseSign1MessagePartsFact { + protected_header, + unprotected_header, + payload, + signature, + })?; + + 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(()) + } + + /// Declare the set of facts this producer can produce. + 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 + } +} + +/// Decode and emit CWT-claims facts from the message headers. +/// +/// The claim set is carried in COSE header parameter label `15` and is expected to be a CBOR map. +/// For convenience, a subset of well-known claims are also exposed as typed fields (e.g. `iss`). +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(()) +} + +/// Decode a single CBOR text string from `bytes`. +/// +/// Returns `None` if decoding fails. +fn decode_cbor_text_one(bytes: &[u8]) -> Option { + let mut d = tinycbor::Decoder(bytes); + ::decode(&mut d).ok() +} + +/// Decode a single CBOR integer (major type 0/1) from `bytes`. +/// +/// Returns `None` if decoding fails. +fn decode_cbor_i64_one(bytes: &[u8]) -> Option { + decode_cbor_i64(bytes).map(|(n, _used)| n) +} + +/// Decode a CBOR integer (major type 0/1) and return the value plus bytes consumed. +/// +/// This is a small, allocation-free helper used when parsing CBOR map keys/values that may be +/// embedded in COSE headers. +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, + } +} + +/// Decode the unsigned-integer argument for a CBOR additional information (AI) value. +/// +/// Returns `(value, bytes_consumed_from_rest)`. +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 { + /// Produce counter-signature-derived subjects and unknown raw bytes facts. + /// + /// This uses the configured [`CounterSignatureResolver`]s to discover counter-signatures from + /// the message, then derives stable trust subjects: + /// - `CounterSignature` subjects are derived from the message subject and raw bytes. + /// - `CounterSignatureSigningKey` subjects are derived from each counter-signature subject. + /// + /// When resolvers are configured but all fail, the relevant fact keys are marked as `Missing` + /// with aggregated failure reasons (mirrors the V2 behavior). + 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(()) + } +} + +/// Resolve a user-friendly content type string from COSE headers. +/// +/// This mirrors the V2 behavior and supports the `CoseHashEnvelope` marker semantics, where the +/// preimage content type (label `259`) is preferred. +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) +} + +/// Read a header value as either a text string or UTF-8 bytes. +/// +/// Some producers encode string-ish values as CBOR bstr containing UTF-8 bytes; this helper +/// provides a tolerant accessor. +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..89b696c8 --- /dev/null +++ b/native/rust/cose_sign1_validation/src/message_facts.rs @@ -0,0 +1,529 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use cose_sign1_validation_trust::fact_properties::{FactProperties, FactValue}; +use std::borrow::Cow; +use std::collections::BTreeMap; +use std::sync::Arc; + +/// 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> { + /// Wrap raw CBOR bytes in a lightweight reader. + /// + /// The bytes are not validated on construction. + pub fn new(bytes: &'a [u8]) -> Self { + Self { bytes } + } + + /// Return the underlying CBOR 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 { + /// Return a borrow-based view of the raw CBOR bytes for a numeric claim label. + /// + /// This allows predicates to decode (or inspect) claim values without this crate + /// interpreting the claim schema. + pub fn claim_value_i64(&self, label: i64) -> Option> { + self.raw_claims + .get(&label) + .map(|b| CborValueReader::new(b.as_ref())) + } + + /// Return a borrow-based view of the raw CBOR bytes for a text claim key. + /// + /// This mirrors `claim_value_i64`, but for non-standard claims that use string keys. + 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_