diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c4ceb8f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,248 @@ +# ====================================================== +# Continuous Integration: making sure the codebase works +# ====================================================== +# +# This workflow tests modifications to 'domain-kmip', ensuring that +# 'domain-kmip' can be used by others successfully. It verifies certain aspects +# of the codebase, such as the formatting and feature flag combinations, and +# runs the full test suite. It runs on Ubuntu, Mac OS, and Windows. + +name: CI + +# When the workflow runs +# --------------------- +on: + # Execute when a pull request is (re-) opened or its head changes (e.g. new + # commits are added or the commit history is rewritten) ... but only if + # build-related files change. + pull_request: + paths: + - '**.rs' + - 'Cargo.toml' + - 'Cargo.lock' + - '.github/workflows/ci.yml' + + # If a pull request is merged, at least one commit is added to the target + # branch. If the target is another pull request, it will be caught by the + # above event. We miss PRs that merge to a non-PR branch, except for the + # 'main' branch. + + # Execute when a commit is pushed to 'main' (including merged PRs) or to a + # release tag ... but only if build-related files change. + push: + branches: + - 'main' + - 'releases/**' + paths: + - '**.rs' + - 'Cargo.toml' + - 'Cargo.lock' + - '.github/workflows/ci.yml' + + # Rebuild 'main' every week. This will account for changes to dependencies + # and to Rust, either of which can trigger new failures. Rust releases are + # every 6 weeks, on a Thursday; this event runs every Friday. + schedule: + - cron: '0 10 * * FRI' + +# Jobs +# ---------------------------------------------------------------------------- +jobs: + + # Check Formatting + # ---------------- + # + # NOTE: This job is run even if no '.rs' files have changed. Inserting such + # a check would require using a separate workflow file or using third-party + # actions. Most commits do change '.rs' files, and 'cargo-fmt' is pretty + # fast, so optimizing this is not necessary. + check-fmt: + name: Check formatting + runs-on: ubuntu-latest + steps: + + # Load the repository. + - name: Checkout repository + uses: actions/checkout@v4 + + # Set up the Rust toolchain. + - name: Set up Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + components: rustfmt + cache: false + + # Do the actual formatting check. + - name: Check formatting + run: cargo fmt --all -- --check + + # Determine MSRV + # -------------- + # + # The MSRV needs to be determined as we will test 'domain-kmip' against the + # Rust compiler at that version. + determine-msrv: + name: Determine MSRV + runs-on: ubuntu-latest + outputs: + msrv: ${{ steps.determine-msrv.outputs.msrv }} + steps: + + # Load the repository. + - name: Checkout repository + uses: actions/checkout@v4 + + # Determine the MSRV. + - name: Determine MSRV + id: determine-msrv + run: | + msrv=`cargo metadata --no-deps --format-version 1 | jq -r '.packages[]|select(.name=="domain-kmip")|.rust_version'` + echo "msrv=$msrv" >> "$GITHUB_OUTPUT" + + # Check Feature Flags + # ------------------- + # + # Rust does not provide any way to check that all possible feature flag + # combinations will succeed, so we need to try them manually here. We will + # assume this choice is not influenced by the OS or Rust version. + check-feature-flags: + name: Check feature flags + runs-on: ubuntu-latest + steps: + + # Load the repository. + - name: Checkout repository + uses: actions/checkout@v4 + + # Set up the Rust toolchain. + - name: Set up Rust + id: setup-rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: false + + # Do the actual feature flag checks. + - name: Check empty feature set + run: cargo check --all-targets --no-default-features + + # Check the required feature flags for every example. + - name: Check required features of examples + run: | + # Scrape crate metadata and construct the right 'check' commands. + # Cargo deosn't have an option to select the right features for us. + # See: https://github.com/rust-lang/cargo/issues/4663 + cargo metadata --no-deps --format-version 1 \ + | jq -r '.packages[].targets[]|select(.kind|any(.=="example"))|{name,features:(.["required-features"]|join(","))}|"\(.name) \(.features)"' \ + | while read -r name features; do + cargo check --example=$name --no-default-features --features=$features + done + + # Check Minimal Versions + # ---------------------- + # + # Ensure that 'domain-kmip' compiles with the oldest compatible versions of all + # packages, even those 'domain-kmip' depends upon indirectly. + check-minimal-versions: + name: Check minimal versions + runs-on: ubuntu-latest + env: + RUSTFLAGS: "-D warnings" + steps: + + # Load the repository. + - name: Checkout repository + uses: actions/checkout@v4 + + # Set up the Rust toolchain. + - name: Set up Rust nightly + id: setup-rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: nightly + cache: false + + # Lock all dependencies to their minimal versions. + - name: Lock dependencies to minimal versions + run: cargo +nightly update -Z minimal-versions + + # Check that 'domain-kmip' compiles. + - name: Check + run: cargo check --all-targets --all-features --locked + + # Clippy + # ------ + # + # We run Clippy separately, and only on nightly Rust because it offers a + # superset of the lints. + # + # 'cargo clippy' and 'cargo build' can share some state for fast execution, + # but it's faster to execute them in parallel than to establish an ordering + # between them. + clippy: + name: Clippy + runs-on: ubuntu-latest + env: + RUSTFLAGS: "-D warnings" + steps: + + # Load the repository. + - name: Checkout repository + uses: actions/checkout@v4 + + # Set up the Rust toolchain. + - name: Set up Rust nightly + id: setup-rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: nightly + components: clippy + cache: false + + # Do the actually Clippy run. + - name: Check Clippy + run: cargo +nightly clippy --all-targets --all-features + + # Test + # ---- + # + # Ensure that 'domain-kmip' compiles and its test suite passes, on a large + # number of operating systems and Rust versions. + test: + name: Test + needs: determine-msrv + strategy: + matrix: + os: [ubuntu-latest, macOS-latest, windows-latest] + rust: ["${{ needs.determine-msrv.outputs.msrv }}", stable, nightly] + runs-on: ${{ matrix.os }} + env: + RUSTFLAGS: "-D warnings" + steps: + + # Load the repository. + - name: Checkout repository + uses: actions/checkout@v4 + + # Prepare the environment on Windows + - name: Prepare Windows environment + if: matrix.os == 'windows-latest' + shell: bash + run: | + # See + echo "CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER=rust-lld" >> "$GITHUB_ENV" + + # Set up the Rust toolchain. + - name: Set up Rust ${{ matrix.rust }} + id: setup-rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + cache: false + + # Build and run the test suite. + - name: Test + run: cargo test --all-targets --all-features + +# TODO: Use 'cargo-semver-checks' on releases. diff --git a/Cargo.lock b/Cargo.lock index 04da1b6..8368cce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,37 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "bcder" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7c42c9913f68cf9390a225e81ad56a5c515347287eb98baa710090ca1de86d" +dependencies = [ + "bytes", + "smallvec", +] + [[package]] name = "bitflags" version = "2.10.0" @@ -26,6 +51,16 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "cc" +version = "1.2.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -41,6 +76,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "domain" version = "0.11.1" @@ -52,6 +98,8 @@ dependencies = [ "hashbrown", "octseq", "rand", + "ring", + "secrecy", "time", ] @@ -59,8 +107,14 @@ dependencies = [ name = "domain-kmip" version = "0.0.1" dependencies = [ + "bcder", "domain", "kmip-protocol", + "lazy_static", + "tracing", + "tracing-subscriber", + "url", + "uuid", ] [[package]] @@ -95,6 +149,21 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[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 = "getrandom" version = "0.2.16" @@ -106,6 +175,18 @@ dependencies = [ "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 = "hashbrown" version = "0.16.1" @@ -121,6 +202,118 @@ 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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[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 = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "kmip-protocol" version = "0.5.0" @@ -135,11 +328,14 @@ dependencies = [ "maybe-async", "r2d2", "rustc_version", + "rustls", + "rustls-pemfile", "serde", "serde_bytes", "serde_derive", "tracing", "trait-set", + "webpki-roots", ] [[package]] @@ -156,12 +352,24 @@ dependencies = [ "trait-set", ] +[[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.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[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" @@ -177,6 +385,15 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "maybe-async" version = "0.2.10" @@ -188,6 +405,21 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -232,12 +464,27 @@ dependencies = [ "windows-link", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "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" @@ -271,6 +518,12 @@ 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 = "r2d2" version = "0.8.10" @@ -309,7 +562,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.16", ] [[package]] @@ -321,6 +574,37 @@ dependencies = [ "bitflags", ] +[[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.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -330,6 +614,56 @@ dependencies = [ "semver", ] +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" +dependencies = [ + "base64", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "scheduled-thread-pool" version = "0.2.7" @@ -345,6 +679,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + [[package]] name = "semver" version = "1.0.27" @@ -358,6 +701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -390,12 +734,39 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -418,6 +789,26 @@ dependencies = [ "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 2.0.111", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.44" @@ -437,6 +828,16 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tracing" version = "0.1.41" @@ -467,6 +868,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -486,18 +917,239 @@ 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 = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +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 = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[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.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.111", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +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-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "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.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[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 2.0.111", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.30" @@ -517,3 +1169,63 @@ dependencies = [ "quote", "syn 2.0.111", ] + +[[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 2.0.111", + "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 2.0.111", +] diff --git a/Cargo.toml b/Cargo.toml index 62e6d63..f082dcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ version = "0.0.1" edition = "2024" # The MSRV is at least 4 versions behind stable (about half a year). -rust-version = "1.87.0" +rust-version = "1.85.0" license = "BSD-3-Clause" readme = "README.md" @@ -23,6 +23,8 @@ repository = "https://github.com/nlnetlabs/domain-kmip/" # TODO: Convert to a regular 'version' spec. git = "https://github.com/NLnetLabs/domain.git" branch = "main" +# TODO: Find a way to enable the 'crypto' module without Ring or OpenSSL. +features = ["ring", "unstable-crypto-sign"] # This crate needs some way to interact with KMIP servers. This requires # connecting to them using an appropriate transport protocol and understanding @@ -35,3 +37,50 @@ package = "kmip-protocol" # TODO: Convert to a regular 'version' spec. git = "https://github.com/NLnetLabs/kmip-protocol.git" branch = "next" +# TODO: Support asynchronous operation? +features = ["tls-with-rustls"] + +# KMIP specifies the format for cryptographic data, often relying on DER +# based formats. DNS sometimes uses different formats, thereby requiring the +# re-encoding of these signatures. +# +# - 'bcder' provides a simple DER encoder and decoder. It is maintained by us +# (NLnet Labs). +# +# TODO: Perhaps try hard-coding the relevant DER decoding? +# TODO: Should 'kmip-protocol' provide this decoding support for us? +[dependencies.bcder] +version = "0.7.0" + +# This crate expresses KMIP key information as a URL for simplicity. It needs +# some way to conformantly serialize and deserialize URLs. +# +# - 'url' is a popular Rust library for handling URLs. +# +# TODO: Maintenance status? +[dependencies.url] +version = "2.5.4" + +# TODO: Does this crate really need tracing? Or is it an artefact of the +# need for quick debugging? +[dependencies.tracing] +version = "0.1.40" + +# TODO: Rely on 'kmip-protocol' to help us generate unique batch item IDs. +# Each KMIP connection could hold a 64-bit counter to assure uniqueness. +[dependencies.uuid] +version = "1.18.0" +features = ["v4"] + + +[dev-dependencies] + +# TODO: See '[dependencies.tracing]'. +[dev-dependencies.tracing-subscriber] +version = "0.3.19" +features = ["env-filter"] + +# NOTE: 'tracing-subscriber' transitively introduces 'lazy_static' with a low +# version specifier, resulting in compilation failures. +[dev-dependencies.lazy_static] +version = "1.5.0" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..df99c69 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 80 diff --git a/src/lib.rs b/src/lib.rs index 35dd44a..ebe80c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,1884 @@ //! KMIP HSM signing support for [`domain`]. -pub use domain; +use core::{fmt, str::FromStr}; + +use std::{ + string::{String, ToString}, + vec::Vec, +}; + +use bcder::{BitString, ConstOid, Oid}; +use kmip::{ + client::pool::SyncConnPool, + types::{ + common::{KeyFormatType, KeyMaterial, TransparentRSAPublicKey}, + response::ManagedObject, + }, +}; +use tracing::{debug, error}; +use url::Url; + +use domain::{ + base::iana::SecurityAlgorithm, crypto::common::rsa_encode, rdata::Dnskey, + utils::base16, +}; + +pub use kmip::client::{ClientCertificate, ConnectionSettings}; + +/// Dependency re-exports +pub mod dep { + pub use domain; + pub use kmip; +} + +//------------ Constants ----------------------------------------------------- + +/// [RFC 4055](https://tools.ietf.org/html/rfc4055) `rsaEncryption` +/// +/// Identifies an RSA public key with no limitation to either RSASSA-PSS or +/// RSAES-OEAP. +pub const RSA_ENCRYPTION_OID: ConstOid = + Oid(&[42, 134, 72, 134, 247, 13, 1, 1, 1]); + +/// [RFC 5480](https://tools.ietf.org/html/rfc5480) `ecPublicKey`. +/// +/// Identifies public keys for elliptic curve cryptography. +pub const EC_PUBLIC_KEY_OID: ConstOid = Oid(&[42, 134, 72, 206, 61, 2, 1]); + +/// [RFC 5480](https://tools.ietf.org/html/rfc5480) `secp256r1`. +/// +/// Identifies the P-256 curve for elliptic curve cryptography. +pub const SECP256R1_OID: ConstOid = Oid(&[42, 134, 72, 206, 61, 3, 1, 7]); + +//------------ KeyUrl -------------------------------------------------------- + +/// A URL that represents a key stored in a KMIP compatible HSM. +/// +/// The URL structure is: +/// +/// ```text +/// kmip:///keys/?algorithm=&flags= +/// ```` +/// +/// The algorithm and flags must be stored in the URL because they are DNSSEC +/// specific and not properties of the key itself and thus not known to or +/// stored by the HSM. +/// +/// While algorithm may seem to be something known to and stored by the HSM, +/// DNSSEC complicates that by aliasing multiple algorithm numbers to the +/// same cryptographic algorithm, and we need to know when using the key which +/// _DNSSEC_ algorithm number to use. +/// +/// The `server_id` could be the actual address of the target, but does not have +/// to be. There are multiple reasons for this: +/// +/// - In a highly available clustered deployment across multiple subnets +/// it could be that the clustered HSM is available to the clustered +/// application via different names/IP addresses in different subnets of +/// the deployment. Using an abstract server_id which is mapped via local +/// configuration in the subnet to the correct hostname/FQDN/IP address +/// for that subnet allows the correct target address to be determined at +/// the point of access. +/// - Using the actual hostname/FQDN/IP address may make it confusing for +/// an operator trying to understand where the key is actually stored. +/// This can happen for example if the product name for the HSM is say +/// Fortanix DSM, while the domain name used to access the HSM might be +/// eu.smartkey.io, which having no mention of the name Fortanix in the +/// FQDN is not immediately obvious that it has any relationship with +/// Fortanix. +/// - If the same HSM is used for different use cases via use of HSM +/// partitions, referring to the HSM by its address may not make it clear +/// which partition is being used, so using a more meaningful name like +/// 'testing' or such could make it clearer where the key is actually +/// being stored. +/// - Storing the username and password in the key URL will cause many +/// copies of those credentials to be stored, one per key, which is harder +/// to secure than if they are only in a single location and looked up on +/// actual access. +/// - Storing the username and password in the key URL would cause the URL +/// to become unusable if the credentials were rotated even though the +/// location at which the key is stored has not changed. +/// - Even if the FQDN, port number, username and password are all correct, +/// there may need to be more settings specified in order to connect to +/// the HSM some of which would not fit easily into a URL such as TLS +/// client certficate details and whether or not to require the server +/// TLS certificate to be valid (which can be inconvenient in test setups +/// using self-signed certificates). +/// +/// Thus an abstract `server_id` is stored in the key URL and it is the +/// responsibility of the user of the key URL to map the server id to the full +/// set of settings required to successfully connect to the HSM to make use of +/// the key. +pub struct KeyUrl { + /// The original URL from which this KeyUrl was parsed. + url: Url, + + /// The KMIP server ID. Produced by the application. + server_id: String, + + /// The KMIP key ID. Produced by the KMIP server. + key_id: String, + + /// The DNSSEC algorithm this key is to be used for. + algorithm: SecurityAlgorithm, + + /// The DNSSEC flags that apply to this key. + flags: u16, +} + +//--- Accessors + +impl KeyUrl { + /// The KMIP server ID. + pub fn server_id(&self) -> &str { + &self.server_id + } + + /// The KMIP key ID. + pub fn key_id(&self) -> &str { + &self.key_id + } + + /// The DNSSEC algorithm identifier for the key. + pub fn algorithm(&self) -> SecurityAlgorithm { + self.algorithm + } + + /// The DNSSEC flags for the key. + pub fn flags(&self) -> u16 { + self.flags + } +} + +//--- impl Deref + +impl std::ops::Deref for KeyUrl { + type Target = Url; + + fn deref(&self) -> &Self::Target { + &self.url + } +} + +//--- Conversions + +impl From for Url { + fn from(key_url: KeyUrl) -> Self { + key_url.url + } +} + +impl TryFrom for KeyUrl { + type Error = String; + + fn try_from(url: Url) -> Result { + let server_id = url + .host_str() + .ok_or(format!("Key URL lacks hostname component: {url}"))? + .to_string(); + + let url_path = url.path().to_string(); + let key_id = url_path + .strip_prefix("/keys/") + .ok_or(format!("Key URL lacks /keys/ path component: {url}"))?; + + let key_id = key_id.to_string(); + let mut flags = None; + let mut algorithm = None; + for (k, v) in url.query_pairs() { + match &*k { + "flags" => { + flags = Some(v.parse::().map_err(|err| { + format!("Key URL flags value is invalid: {err}") + })?) + } + "algorithm" => { + algorithm = Some(SecurityAlgorithm::from_str(&v).map_err( + |err| { + format!("Key URL algorithm value is invalid: {err}") + }, + )?) + } + unknown => Err(format!( + "Key URL contains unknown query parameter: {unknown}" + ))?, + } + } + let algorithm = algorithm + .ok_or(format!("Key URL lacks algorithm query parameter: {url}"))?; + let flags = flags + .ok_or(format!("Key URL lacks flags query parameter: {url}"))?; + + Ok(Self { + url, + server_id, + key_id, + algorithm, + flags, + }) + } +} + +//--- impl Display + +impl fmt::Display for KeyUrl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.url.fmt(f) + } +} + +//------------ PublicKey ----------------------------------------------------- + +/// A public key retrieved from a KMIP server. +pub struct PublicKey { + /// The DNSSEC algorithm for use with this public key. + algorithm: SecurityAlgorithm, + + /// The public key octets. + public_key: Vec, +} + +impl PublicKey { + /// Create a public key from a key stored on a KMIP server. + /// + /// The public key details will be retrieved from the KMIP server. + /// + /// The DNSSEC algorithm is needed in order for [`Self::dnskey()`] to + /// generate a [`Dnskey`] and must match the cryptographic algorithm of + /// the key stored on the KMIP server. + /// + /// Note: This function will block while awaiting the response from the + /// KMIP server. + /// + /// If the KMIP operation fails an error or the response cannot be parsed + /// an error will be returned. + /// + /// If the cryptographic algorithm of the retrieved key does not match + /// the given DNSSEC algorithm an error will be returned. + pub fn for_key_id_and_dnssec_algorithm( + public_key_id: &str, + algorithm: SecurityAlgorithm, + conn_pool: SyncConnPool, + ) -> Result { + // https://datatracker.ietf.org/doc/html/rfc5702#section-2 + // Use of SHA-2 Algorithms with RSA in DNSKEY and RRSIG Resource + // Records for DNSSEC + // + // 2. DNSKEY Resource Records + // "The format of the DNSKEY RR can be found in [RFC4034]. [RFC3110] + // describes the use of RSA/SHA-1 for DNSSEC signatures." + // | + // | + // v + // https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.4 + // Resource Records for the DNS Security Extensions + // 2. The DNSKEY Resource Record + // 2.1.4. The Public Key Field + // "The Public Key Field holds the public key material. The + // format depends on the algorithm of the key being stored and is + // described in separate documents." + // | + // | + // v + // https://datatracker.ietf.org/doc/html/rfc3110#section-2 + // RSA/SHA-1 SIGs and RSA KEYs in the Domain Name System (DNS) + // 2. RSA Public KEY Resource Records + // "... The structure of the algorithm specific portion of the RDATA + // part of such RRs is as shown below. + // + // Field Size + // ----- ---- + // exponent length 1 or 3 octets (see text) + // exponent as specified by length field + // modulus remaining space + // + // For interoperability, the exponent and modulus are each limited to + // 4096 bits in length. The public key exponent is a variable length + // unsigned integer. Its length in octets is represented as one octet + // if it is in the range of 1 to 255 and by a zero octet followed by + // a two octet unsigned length if it is longer than 255 bytes. The + // public key modulus field is a multiprecision unsigned integer. The + // length of the modulus can be determined from the RDLENGTH and the + // preceding RDATA fields including the exponent. Leading zero octets + // are prohibited in the exponent and modulus. + + let client = conn_pool + .get() + .inspect_err(|err| error!("{err}")) + .map_err(|err| { + kmip::client::Error::ServerError(format!( + "Error while attempting to acquire KMIP connection from pool: {err}" + )) + })?; + + // Note: OpenDNSSEC queries the public key ID, _unless_ it was + // configured not the public key in the HSM (by setting CKA_TOKEN + // false) in which case there is no public key and so it uses the + // private key object handle instead. + let res = client + .get_key(public_key_id) + .inspect_err(|err| error!("{err}"))?; + let ManagedObject::PublicKey(public_key) = res.cryptographic_object + else { + return Err(kmip::client::Error::DeserializeError(format!( + "Fetched KMIP object was expected to be a PublicKey but was instead: {}", + res.cryptographic_object + )))?; + }; + + // https://docs.oasis-open.org/kmip/ug/v1.2/cn01/kmip-ug-v1.2-cn01.html#_Toc407027125 + // "“Raw” key format is intended to be applied to symmetric keys + // and not asymmetric keys" + // + // As we deal in asymmetric keys (RSA, ECDSA), not symmetric keys, + // we should not encounter public_key.key_block.key_format_type + // == KeyFormatType::Raw. However, Fortanix DSM returns + // KeyFormatType::Raw when fetching key data for an ECDSA public key. + + match public_key.key_block.key_value.key_material { + KeyMaterial::Bytes(bytes) => { + debug!( + "Cryptographic Algorithm: {:?}", + public_key.key_block.cryptographic_algorithm + ); + debug!( + "Cryptographic Length: {:?}", + public_key.key_block.cryptographic_length + ); + debug!( + "Key Format Type: {:?}", + public_key.key_block.key_format_type + ); + debug!( + "Key Compression Type: {:?}", + public_key.key_block.key_compression_type + ); + debug!("Key bytes as hex: {}", base16::encode_display(&bytes)); + + match (algorithm, public_key.key_block.key_format_type) { + ( + SecurityAlgorithm::RSAMD5 + | SecurityAlgorithm::RSASHA1 + | SecurityAlgorithm::RSASHA1_NSEC3_SHA1 + | SecurityAlgorithm::RSASHA256 + | SecurityAlgorithm::RSASHA512, + KeyFormatType::PKCS1, + ) => Self::parse_rsa_from_pkcs1(algorithm, &bytes), + + ( + SecurityAlgorithm::RSAMD5 + | SecurityAlgorithm::RSASHA1 + | SecurityAlgorithm::RSASHA1_NSEC3_SHA1 + | SecurityAlgorithm::RSASHA256 + | SecurityAlgorithm::RSASHA512, + KeyFormatType::Raw, + ) => Self::parse_rsa_from_raw(algorithm, &bytes), + + ( + SecurityAlgorithm::ECDSAP256SHA256, + KeyFormatType::Raw, + ) => Self::parse_ecdsa_from_raw(algorithm, &bytes), + + (expected, key_format_type) => { + let alg = public_key + .key_block + .cryptographic_algorithm + .map(|a| a.to_string()) + .unwrap_or("unknown algorithm".to_string()); + let len = public_key + .key_block + .cryptographic_length + .map(|l| l.to_string()) + .unwrap_or("unknown length".to_string()); + let actual = + format!("{alg} ({len}) as {key_format_type}"); + Err(PublicKeyError::AlgorithmMismatch { + expected, + actual, + }) + } + } + } + + KeyMaterial::TransparentRSAPublicKey( + // Nameshed-HSM-Relay + TransparentRSAPublicKey { + modulus, + public_exponent, + }, + ) => Ok(Self { + algorithm, + public_key: rsa_encode(&public_exponent, &modulus), + }), + + mat => Err(kmip::client::Error::DeserializeError(format!( + "Fetched KMIP object has unsupported key material type: {mat}" + )) + .into()), + } + } + + /// Create a public key from a key stored on a KMIP server. + /// + /// This is a thin wrapper around + /// [`Self::for_key_id_and_dnssec_algorithm`]. + pub fn for_key_url( + public_key_url: KeyUrl, + conn_pool: SyncConnPool, + ) -> Result { + Self::for_key_id_and_dnssec_algorithm( + public_key_url.key_id(), + public_key_url.algorithm(), + conn_pool, + ) + } + + /// The DNSSEC algorithm of the key. + pub fn algorithm(&self) -> SecurityAlgorithm { + self.algorithm + } + + /// Generate a DNSKEY RR or this public key. + pub fn dnskey(&self, flags: u16) -> Dnskey> { + // SAFETY: The key came from a KMIP server and was validated to have + // the expected length when the KMIP server response was parsed by + // fetch_public_key(). + Dnskey::new(flags, 3, self.algorithm, self.public_key.clone()).unwrap() + } +} + +impl PublicKey { + /// Parse an RSA key encoded in the PKCS#1 format. + /// + /// # Panics + /// + /// Panics if the specified DNS security algorithm for the key does not use + /// RSA. + fn parse_rsa_from_pkcs1( + algorithm: SecurityAlgorithm, + bytes: &[u8], + ) -> Result { + // Ensure the specified algorithm uses RSA. + assert!(matches!( + algorithm, + SecurityAlgorithm::RSAMD5 + | SecurityAlgorithm::RSASHA1 + | SecurityAlgorithm::RSASHA1_NSEC3_SHA1 + | SecurityAlgorithm::RSASHA256 + | SecurityAlgorithm::RSASHA512 + )); + + // PyKMIP outputs PKCS#1 ASN.1 DER encoded RSA public key data like so: + // RSAPublicKey::=SEQUENCE{ + // modulus INTEGER, -- n + // publicExponent INTEGER -- e } + + // TODO: Decode this manually, to avoid the 'bcder' dependency? + let (modulus, public_exponent) = bcder::Mode::Der + .decode(bytes, |cons| { + cons.take_sequence(|cons| { + let modulus = bcder::Unsigned::take_from(cons)?; + let public_exponent = bcder::Unsigned::take_from(cons)?; + Ok((modulus, public_exponent)) + }) + }) + .map_err(|err| { + kmip::client::Error::DeserializeError(format!( + "Unable to parse DER encoded PKCS#1 RSAPublicKey: {err}" + )) + })?; + + let public_key = domain::crypto::common::rsa_encode( + public_exponent.as_slice(), + modulus.as_slice(), + ); + + Ok(Self { + algorithm, + public_key, + }) + } + + /// Parse an RSA key encoded in the KMIP "raw" format convention. + /// + /// # Panics + /// + /// Panics if the specified DNS security algorithm for the key does not use + /// RSA. + fn parse_rsa_from_raw( + algorithm: SecurityAlgorithm, + bytes: &[u8], + ) -> Result { + // Ensure the specified algorithm uses RSA. + assert!(matches!( + algorithm, + SecurityAlgorithm::RSAMD5 + | SecurityAlgorithm::RSASHA1 + | SecurityAlgorithm::RSASHA1_NSEC3_SHA1 + | SecurityAlgorithm::RSASHA256 + | SecurityAlgorithm::RSASHA512 + )); + + // For an RSA key Fortanix DSM supplies: (from https://asn1js.eu/) + // SubjectPublicKeyInfo SEQUENCE (2 elem) + // algorithm AlgorithmIdentifier SEQUENCE (2 elem) + // algorithm OBJECT IDENTIFIER 1.2.840.113549.1.1.1 rsaEncryption (PKCS #1) + // parameter ANY NULL + // subjectPublicKey BIT STRING (2160 bit) 001100001000001000000001000010100000001010000010000000010000000100000… + // SEQUENCE (2 elem) + // INTEGER (2048 bit) 229677698057230630160769379936346719377896297586216888467726484346678… + // INTEGER 65537 + + // TODO: Decode this manually, to avoid the 'bcder' dependency? + let (modulus, public_exponent) = + bcder::Mode::Der + .decode(bytes, |cons| { + cons.take_sequence(|cons| { + cons.take_sequence(|cons| { + let algorithm = Oid::take_from(cons)?; + if algorithm != RSA_ENCRYPTION_OID { + return Err(cons.content_err("Only SubjectPublicKeyInfo with algorithm rsaEncryption is supported")); + } + // Ignore the parameters. + Ok(()) + })?; + cons.take_sequence(|cons| { + let modulus = bcder::Unsigned::take_from(cons)?; + let public_exponent = bcder::Unsigned::take_from(cons)?; + Ok((modulus, public_exponent)) + }) + }) + }) + .map_err(|err| { + kmip::client::Error::DeserializeError(format!( + "Unable to parse raw RSASHA256 SubjectPublicKeyInfo: {err}" + )) + })?; + + let public_key = domain::crypto::common::rsa_encode( + public_exponent.as_slice(), + modulus.as_slice(), + ); + + Ok(Self { + algorithm, + public_key, + }) + } + + /// Parse an ECDSA key encoded in the KMIP "raw" format convention. + /// + /// # Panics + /// + /// Panics if the specified DNS security algorithm for the key does not use + /// RSA. + fn parse_ecdsa_from_raw( + algorithm: SecurityAlgorithm, + bytes: &[u8], + ) -> Result { + // Ensure the specified algorithm uses ECDSA. + // TODO: Support ECDSAP384SHA384. + assert!(matches!(algorithm, SecurityAlgorithm::ECDSAP256SHA256)); + + // For an ECDSA key Fortanix DSM supplies: (from https://asn1js.eu/) + // SubjectPublicKeyInfo SEQUENCE @0+89 (constructed): (2 elem) + // algorithm AlgorithmIdentifier SEQUENCE @2+19 (constructed): (2 elem) + // algorithm OBJECT_IDENTIFIER @4+7: 1.2.840.10045.2.1|ecPublicKey|ANSI X9.62 public key type + // parameters ANY OBJECT_IDENTIFIER @13+8: 1.2.840.10045.3.1.7|prime256v1|ANSI X9.62 named elliptic curve + // subjectPublicKey BIT_STRING @23+66: (520 bit) + // + // From: https://www.rfc-editor.org/rfc/rfc5480.html#section-2.1.1 + // The parameter for id-ecPublicKey is as follows and MUST always be + // present: + // + // ECParameters ::= CHOICE { + // namedCurve OBJECT IDENTIFIER + // -- implicitCurve NULL + // -- specifiedCurve SpecifiedECDomain + // } + // -- implicitCurve and specifiedCurve MUST NOT be used in PKIX. + // -- Details for SpecifiedECDomain can be found in [X9.62]. + // -- Any future additions to this CHOICE should be coordinated + // -- with ANSI X9. + let bits = bcder::Mode::Der + .decode(bytes, |cons| { + cons.take_sequence(|cons| { + cons.take_sequence(|cons| { + let algorithm = Oid::take_from(cons)?; + if algorithm != EC_PUBLIC_KEY_OID { + Err(cons.content_err("Only SubjectPublicKeyInfo with algorithm id-ecPublicKey is supported")) + } else { + let named_curve = Oid::take_from(cons)?; + if named_curve != SECP256R1_OID { + return Err(cons.content_err("Only SubjectPublicKeyInfo with namedCurve secp256r1 is supported")); + } + Ok(()) + } + })?; + let bits = BitString::take_from(cons)?; + Ok(bits) + }) + }).map_err(|err| kmip::client::Error::DeserializeError(format!("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo: {err}")))?; + + // https://www.rfc-editor.org/rfc/rfc5480#section-2.2 + // "The subjectPublicKey from SubjectPublicKeyInfo + // is the ECC public key. ECC public keys have the + // following syntax: + // + // ECPoint ::= OCTET STRING + // ... + // The first octet of the OCTET STRING indicates + // whether the key is compressed or uncompressed. + // The uncompressed form is indicated by 0x04 and + // the compressed form is indicated by either 0x02 + // or 0x03 (see 2.3.3 in [SEC1]). The public key + // MUST be rejected if any other value is included + // in the first octet." + let Some(octets) = bits.octet_slice() else { + return Err(kmip::client::Error::DeserializeError("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: missing octets".into()))?; + }; + + // Expect octet string to be [, + // <32-byte X value>, <32-byte Y value>]. + if octets.len() != 65 { + return Err(kmip::client::Error::DeserializeError(format!( + "Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: expected [, <32-byte X value>, <32-byte Y value>]: {} ({} bytes)", + base16::encode_display(octets), + octets.len() + )))?; + } + + // Note: OpenDNSSEC doesn't support the compressed + // form either. + let compression_flag = octets[0]; + if compression_flag != 0x04 { + return Err(kmip::client::Error::DeserializeError(format!( + "Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: unknown compression flag {compression_flag:?}" + )))?; + } + + // Expect octet string to be X | Y (| denotes + // concatenation) where X and Y are each 32 bytes + // (because P-256 uses 256 bit values and 256 bits are + // 32 bytes). Skip the compression flag. + let public_key = octets[1..].to_vec(); + + Ok(Self { + algorithm, + public_key, + }) + } +} + +//============ sign ========================================================== + +/// Submodule for private keys and signing. +pub mod sign { + use std::boxed::Box; + use std::string::{String, ToString}; + use std::time::SystemTime; + use std::vec::Vec; + + use kmip::client::pool::SyncConnPool; + use kmip::types::common::{ + CryptographicAlgorithm, CryptographicParameters, + CryptographicUsageMask, Data, DigitalSignatureAlgorithm, + HashingAlgorithm, PaddingMethod, UniqueBatchItemID, UniqueIdentifier, + }; + use kmip::types::request::{ + self, BatchItem, CommonTemplateAttribute, PrivateKeyTemplateAttribute, + PublicKeyTemplateAttribute, RequestPayload, + }; + use kmip::types::response::{ + CreateKeyPairResponsePayload, ResponsePayload, + }; + use tracing::{debug, error, trace}; + use url::Url; + use uuid::Uuid; + + use domain::base::iana::SecurityAlgorithm; + use domain::crypto::common::DigestType; + use domain::crypto::sign::{GenerateParams, SignError, SignRaw, Signature}; + use domain::rdata::Dnskey; + use domain::utils::base16; + + use super::{ + DestroyError, GenerateError, KeyUrl, KeyUrlParseError, PublicKey, + }; + + //----------- KeyPair ---------------------------------------------------- + + /// A reference to a key pair stored in an [OASIS KMIP] compliant HSM + /// server. + /// + /// Allows operations to be performed on and using the key pair. + /// + /// Operations common to key pairs irrespective of the underlying crypto + /// backend are offered via the [`SignRaw`] trait impl. + /// + /// Operations specifc to KMIP key pairs are offered via methods specific + /// to this type, e.g. batching support via [`Self::sign_raw_enqueue()`] + /// and [`Self::sign_raw_submit_queue()`]. + /// + /// See [`Self::from_metadata()`] and [`Self::from_urls()`] to construct + /// a [`KeyPair`] from individual public and private KMIP keys. + /// + /// To generate a KMIP key pair see [`generate()`]. + /// + /// To destroy individual KMIP keys see [`destroy()`]. + /// + /// [OASIS KMIP]: https://www.oasis-open.org/committees/tc_home.php?wg_abbrev=kmip + #[derive(Clone, Debug)] + pub struct KeyPair { + /// The algorithm used by the key. + algorithm: SecurityAlgorithm, + + /// The KMIP ID of the private key. + private_key_id: String, + + /// The KMIP ID of the public key. + public_key_id: String, + + /// The connection pool for connecting to the KMIP server. + // TODO: Should this be T that impl's a Connection trait, why should + // it know that it's a pool rather than a single connection? + conn_pool: SyncConnPool, + + /// Cached DNSKEY RR for the public key. + dnskey: Dnskey>, + + /// Flags from [`Dnskey`]. + flags: u16, + } + + //--- Constructors + + impl KeyPair { + /// Construct a reference to a KMIP HSM held key pair using key + /// metadata. + pub fn from_metadata( + algorithm: SecurityAlgorithm, + flags: u16, + private_key_id: &str, + public_key_id: &str, + conn_pool: SyncConnPool, + ) -> Result { + let dnskey = PublicKey::for_key_id_and_dnssec_algorithm( + public_key_id, + algorithm, + conn_pool.clone(), + ) + .map_err(|err| GenerateError::Kmip(err.to_string()))? + .dnskey(flags); + + Ok(Self { + algorithm, + private_key_id: private_key_id.to_string(), + public_key_id: public_key_id.to_string(), + conn_pool, + flags, + dnskey, + }) + } + + /// Construct a reference to a KMIP HSM held key pair using key URLs. + pub fn from_urls( + priv_key_url: KeyUrl, + pub_key_url: KeyUrl, + conn_pool: SyncConnPool, + ) -> Result { + if priv_key_url.algorithm() != pub_key_url.algorithm() { + Err(GenerateError::Kmip(format!( + "Private and public key URLs have different algorithms: {} vs {}", + priv_key_url.algorithm(), + pub_key_url.algorithm() + ))) + } else if priv_key_url.flags() != pub_key_url.flags() { + Err(GenerateError::Kmip(format!( + "Private and public key URLs have different flags: {} vs {}", + priv_key_url.flags(), + pub_key_url.flags() + ))) + } else if priv_key_url.server_id() != pub_key_url.server_id() { + Err(GenerateError::Kmip(format!( + "Private and public key URLs have different server IDs: {} vs {}", + priv_key_url.server_id(), + pub_key_url.server_id() + ))) + } else if priv_key_url.server_id() != conn_pool.server_id() { + Err(GenerateError::Kmip(format!( + "Key URLs have different server ID to the KMIP connection pool: {} vs {}", + priv_key_url.server_id(), + conn_pool.server_id() + ))) + } else { + Self::from_metadata( + priv_key_url.algorithm(), + priv_key_url.flags(), + priv_key_url.key_id(), + pub_key_url.key_id(), + conn_pool, + ) + } + } + } + + //--- Accessors + + impl KeyPair { + /// Get the KMIP HSM ID for the private half of this key pair. + pub fn private_key_id(&self) -> &str { + &self.private_key_id + } + + /// Get the KMIP HSM ID for the public half of this key pair. + pub fn public_key_id(&self) -> &str { + &self.public_key_id + } + + /// Get a KMIP URL for the private half of this key pair. + pub fn private_key_url(&self) -> Url { + // + self.mk_key_url(&self.private_key_id).unwrap() + } + + /// Get a KMIP URL for the public half of this key pair. + pub fn public_key_url(&self) -> Url { + self.mk_key_url(&self.public_key_id).unwrap() + } + + /// Get a reference to the KMIP HSM connection pool for this key pair. + pub fn conn_pool(&self) -> &SyncConnPool { + &self.conn_pool + } + } + + //--- Operations + + impl KeyPair { + /// Enqueue a KMIP signing operation using this key pair on the given + /// data. + /// + /// Like [`SignRaw::sign_raw()`] but deferred until + /// [`KeyPair::sign_raw_submit_queue()`] is called. + pub fn sign_raw_enqueue( + &self, + queue: &mut SignQueue, + data: &[u8], + ) -> Result, SignError> { + let request = self.sign_pre(data)?; + let operation = request.operation(); + let batch_item_id = + UniqueBatchItemID(Uuid::new_v4().into_bytes().to_vec()); + let batch_item = BatchItem(operation, Some(batch_item_id), request); + queue.0.push(batch_item); + Ok(None) + } + + /// Submit the given signing queue as a batch to the KMIP HSM. + // + // TODO: Should the queue store the KMIP connection pool reference and + // should submit() be a method on the queue? + // TODO: What happens if the same queue is used with + // sign_raw_enqueue() but with keys that are held by different KMIP + // HSMs and thus have different KMIP connection pools? + pub fn sign_raw_submit_queue( + &self, + queue: &mut SignQueue, + ) -> Result, SignError> { + // Execute the request and capture the response. + let client = self.conn_pool.get().map_err(|err| { + error!("Error while obtaining KMIP pool connection: {err}"); + SignError + })?; + + // Drain the queue. + let q_size = queue.0.capacity(); + let mut empty = Vec::with_capacity(q_size); + std::mem::swap(&mut queue.0, &mut empty); + let queue = empty; + + // This will block which could be problematic if executed from an + // async task handler thread as it will block execution of other + // tasks while waiting for the remote KMIP server to respond. + let res = client.do_requests(queue).map_err(|err| { + error!("Error while sending KMIP request: {err}"); + SignError + })?; + + let mut sigs = Vec::with_capacity(q_size); + for res in res { + let res = res.map_err(|err| { + error!("{err}"); + SignError + })?; + let sig = self.sign_post(res.payload.unwrap())?; + sigs.push(sig); + } + + Ok(sigs) + } + } + + //--- Internal details + + impl KeyPair { + /// Make a KMIP URL for this key using the given KMIP ID. + fn mk_key_url(&self, key_id: &str) -> Result { + // We have to store the algorithm in the URL because the DNSSEC + // algorithm (e.g. 5 and 7) don't necessarily correspond to the + // cryptographic algorithm of the key known to the HSM. And we + // have to store the flags in the URL because these are not known + // to the HSM, they say someting about the use to which the key + // will be put of which the HSM is unaware. + let url = format!( + "kmip://{}/keys/{}?algorithm={}&flags={}", + self.conn_pool.server_id(), + key_id, + self.algorithm, + self.flags + ); + + let url = Url::parse(&url).map_err(|err| { + KeyUrlParseError(format!("unable to parse {url} as URL: {err}")) + })?; + + Ok(url) + } + + /// Prepare a KMIP signing operation request to sign the given data + /// using this key pair. + fn sign_pre(&self, data: &[u8]) -> Result { + let (crypto_alg, hashing_alg, _digest_type) = match self.algorithm { + SecurityAlgorithm::RSASHA256 => ( + CryptographicAlgorithm::RSA, + HashingAlgorithm::SHA256, + DigestType::Sha256, + ), + SecurityAlgorithm::ECDSAP256SHA256 => ( + CryptographicAlgorithm::ECDSA, + HashingAlgorithm::SHA256, + DigestType::Sha256, + ), + alg => { + error!("Algorithm not supported for KMIP signing: {alg}"); + return Err(SignError); + } + }; + let mut cryptographic_parameters = + CryptographicParameters::default() + .with_hashing_algorithm(hashing_alg) + .with_cryptographic_algorithm(crypto_alg); + if self.algorithm == SecurityAlgorithm::RSASHA256 { + cryptographic_parameters = cryptographic_parameters + .with_padding_method(PaddingMethod::PKCS1_v1_5); + } + let request = RequestPayload::Sign( + Some(UniqueIdentifier(self.private_key_id.clone())), + Some(cryptographic_parameters), + Data(data.as_ref().to_vec()), + ); + Ok(request) + } + + /// Process a KMIP HSM signing operation response for this key pair. + fn sign_post( + &self, + res: ResponsePayload, + ) -> Result { + trace!("Checking sign payload"); + let ResponsePayload::Sign(signed) = res else { + unreachable!(); + }; + + trace!( + "Algorithm: {}, Signature Data: {}", + self.algorithm, + base16::encode_display(&signed.signature_data) + ); + match (self.algorithm, signed.signature_data.len()) { + (SecurityAlgorithm::RSASHA256, _) => Ok(Signature::RsaSha256( + signed.signature_data.into_boxed_slice(), + )), + + (SecurityAlgorithm::ECDSAP256SHA256, _) => { + let signature = Self::parse_ecdsa_sig_from_x962( + &signed.signature_data, + )?; + Ok(Signature::EcdsaP256Sha256(signature)) + } + + // TODO + //(SecurityAlgorithm::ECDSAP384SHA384, 96) => {}, + //(SecurityAlgorithm::ED25519, 64) => {}, + //(SecurityAlgorithm::ED448, 114) => {}, + (alg, sig_len) => { + error!( + "KMIP signature algorithm not supported or signature length incorrect: {sig_len} byte {alg} signature (0x{})", + base16::encode_display(&signed.signature_data) + ); + Err(SignError) + } + } + } + + /// Parse an ECDSA signature from the X9.62 ASN.1 DER format. + pub(crate) fn parse_ecdsa_sig_from_x962( + bytes: &[u8], + ) -> Result, SignError> { + // ECDSA signature received from Fortanix DSM, decoded + // using this command: + // + // $ echo '' | xxd -r -p | dumpasn1 - + // 0 69: SEQUENCE { + // 2 33: INTEGER + // : 00 C6 A7 D1 2E A1 0C B4 96 BD D9 A5 48 2C 9B F4 + // : 0C EC 9F FC EF 1A 0D 59 BB B9 24 F3 FE DA DC F8 + // : 9E + // 37 32: INTEGER + // : 4B A7 22 69 F2 F8 65 88 63 D0 25 D3 A9 D5 92 4F + // : A2 21 BD 59 CD 27 60 6D 16 C3 79 EF B4 0A CA 33 + // : } + // + // Where the two integer values are known as 'r' and 's'. + let (r, s) = bcder::Mode::Der + .decode(bytes, |cons| { + cons.take_sequence(|cons| { + let r = bcder::Unsigned::take_from(cons)?; + let s = bcder::Unsigned::take_from(cons)?; + Ok((r, s)) + }) + }) + .map_err(|err| { + error!( + "Unable to parse DER encoded PKCS#1 RSAPublicKey: {err}" + ); + SignError + })?; + let (mut r, mut s) = (r.as_slice(), s.as_slice()); + + // In DER, there can be at most one leading zero byte, + // because the high bit might be set and that would + // otherwise indicate a negative integer. Strip it. + for x in [&mut r, &mut s] { + *x = match *x { + [0, 0x80..=0xFF, ..] => &x[1..], + // Badly formatted signature. + [0, _, ..] => { + error!("Leading zeros in ECDSA signature integer"); + return Err(SignError); + } + x => x, + }; + + if x.len() > 32 { + error!("Overly long ECDSA signature integer"); + return Err(SignError); + } + } + + let mut signature = Box::new([0u8; 64]); + signature[32 - r.len()..32].copy_from_slice(r); + signature[64 - r.len()..64].copy_from_slice(s); + Ok(signature) + } + } + + //----------- SignQueue -------------------------------------------------- + + /// A queue of KMIP signing operations pending batch submission. + #[derive(Debug, Default)] + pub struct SignQueue(Vec); + + impl SignQueue { + /// Constructs a new empty signing queue. + pub fn new() -> Self { + Self(vec![]) + } + } + + impl SignRaw for KeyPair { + fn algorithm(&self) -> SecurityAlgorithm { + self.algorithm + } + + fn dnskey(&self) -> Dnskey> { + self.dnskey.clone() + } + + fn sign_raw(&self, data: &[u8]) -> Result { + let request = self.sign_pre(data)?; + + // Execute the request and capture the response. + let client = self.conn_pool.get().map_err(|err| { + error!("Error while obtaining KMIP pool connection: {err}"); + SignError + })?; + + // This will block which could be problematic if executed from an + // async task handler thread as it will block execution of other + // tasks while waiting for the remote KMIP server to respond. + let res = client.do_request(request).map_err(|err| { + error!("Error while sending KMIP request: {err}"); + SignError + })?; + + self.sign_post(res) + } + } + + //----------- generate() ------------------------------------------------- + + /// Generate a new key pair for a given algorithm using a specified HSM. + pub fn generate( + public_key_name: String, + private_key_name: String, + params: GenerateParams, + // TODO: Is this enough? Or do we need to take SecurityAlgorithm as + // input instead of GenerateParams to ensure we don't lose distinctions + // like 5 vs 7 which are both RSASHA1? + flags: u16, + conn_pool: SyncConnPool, + ) -> Result { + let algorithm = params.algorithm(); + + let client = conn_pool.get().map_err(|err| { + GenerateError::Kmip(format!( + "Key generation failed: Cannot connect to KMIP server {}: {err}", + conn_pool.server_id() + )) + })?; + + // TODO: Determine this on first use of the HSM? + // PyKMIP doesn't support ActivationDate. + // Fortanix DSM does support it and creates the key in an activated + // state but still returns a (harmless?) error: + // Server error: Operation CreateKeyPair failed: Input field `state` + // is not coherent with provided activation/deactivation dates + let activate_on_create = false; + + let use_cryptographic_params = false; + + // Note: Strictly speaking KMIP requires that each key, including + // public and private "halves" of the same key "pair", have a unique + // name within the HSM namespace. We don't enforce that here, e.g. + // maybe you know that your backend is actually a KMIP to PKCS#11 + // gateway and PKCS#11 doesn't have the same restriction and you + // want keys to be named as you are used to with your PKCS#11 HSM. We + // also don't intefere with names by making them unique as that would + // change any max name length calculations performed by the caller + // to avoid known issues with backend name limitations for their + // particular HSM (the PKCS#11 and KMIP specifications are silent on + // name limits but implementations definitely have limits, and not all + // the same). + + let mut common_attrs = vec![]; + let priv_key_attrs = vec![ + // Krill supplies a name at creation time. Do we need to? + // Note: Fortanix DSM requires a name for at least the private + // key. + request::Attribute::Name(private_key_name), + request::Attribute::CryptographicUsageMask( + CryptographicUsageMask::Sign, + ), + ]; + let pub_key_attrs = vec![ + // Krill supplies a name at creation time. Do we need to? + // Note: Fortanix DSM requires a name for at least the private + // key. + request::Attribute::Name(public_key_name), + // Krill does verification, do we need to? ODS doesn't. + // Note: PyKMIP requires a Cryptographic Usage Mask for the public + // key. + request::Attribute::CryptographicUsageMask( + CryptographicUsageMask::Verify, + ), + ]; + + // PyKMIP doesn't support CryptographicParameters so we cannot supply + // HashingAlgorithm. It also doesn't support the Hash operation. + // How do we specify SHA256 hashing? Do we have to do it ourselves + // post-signing? Can we just specify the hashing to do when invoking + // the Sign operation? + // Fortanix DSM also doesn't support Cryptographic Parameters: + // Server error: Operation CreateKeyPair failed: Don't have handling + // for attribute Cryptographic Parameters + + // PyKMIP doesn't support Attribute::ActivationDate. For HSMs that + // don't support it we have to do a separate Activate operation after + // creating the key pair. + // Fortanix DSM does support ActivationDate. + + match params { + GenerateParams::RsaSha256 { bits } => { + // RFC 8624 3.1 DNSSEC Signing: MUST + // https://docs.oasis-open.org/kmip/spec/v1.2/os/kmip-spec-v1.2-os.html#_Toc395776503 + // "For RSA, Cryptographic Length corresponds to the bit + // length of the Modulus" + + // https://www.rfc-editor.org/rfc/rfc5702.html#section-2.1 + // 2.1. RSA/SHA-256 DNSKEY Resource Records + // "For interoperability, as in [RFC3110], the key size of + // RSA/SHA-256 keys MUST NOT be less than 512 bits and MUST + // NOT be more than 4096 bits." + if !(512..=4096).contains(&bits) { + return Err(GenerateError::UnsupportedAlgorithm); + } + + if use_cryptographic_params { + common_attrs.push(request::Attribute::CryptographicParameters( + CryptographicParameters::default().with_digital_signature_algorithm( + DigitalSignatureAlgorithm::SHA256WithRSAEncryption_PKCS1_v1_5, + ), + )) + } else { + common_attrs.push( + request::Attribute::CryptographicAlgorithm( + CryptographicAlgorithm::RSA, + ), + ); + common_attrs.push(request::Attribute::CryptographicLength( + bits.try_into().unwrap(), + )); + } + } + GenerateParams::RsaSha512 { .. } => { + return Err(GenerateError::UnsupportedAlgorithm); + } + GenerateParams::EcdsaP256Sha256 => { + // PyKMIP doesn't support ECDSA: + // "Operation CreateKeyPair failed: The cryptographic + // algorithm (CryptographicAlgorithm.ECDSA) is not a + // supported asymmetric key algorithm." + + if use_cryptographic_params { + common_attrs.push( + request::Attribute::CryptographicParameters( + CryptographicParameters::default() + .with_digital_signature_algorithm( + DigitalSignatureAlgorithm::ECDSAWithSHA256, + ), + ), + ) + } else { + // RFC 8624 3.1 DNSSEC Signing: MUST + // https://docs.oasis-open.org/kmip/spec/v1.2/os/kmip-spec-v1.2-os.html#_Toc395776503 + // "For ECDSA, ECDH, and ECMQV algorithms, Cryptographic + // Length corresponds to the bit length of parameter + // Q." + common_attrs.push( + request::Attribute::CryptographicAlgorithm( + CryptographicAlgorithm::ECDSA, + ), + ); + // ODS doesn't tell PKCS#11 a Q length. I have no idea + // what value we should put here, but as Q length is + // optional let's try not passing it. + // Note: PyKMIP requires a length: use 256 from P-256? + // Note: Fortanix also requires a length and gives error + // "missing required field `elliptic_curve` in request + // body" if cryptographic length is not specified, and + // a value of 256 works fine while a value of 255 causes + // error "Unsupported length for ECC key". When using 256 + // the Fortanix UI shows the key as type EC with curve + // NistP256 so that seems good. + common_attrs + .push(request::Attribute::CryptographicLength(256)); + } + } + GenerateParams::EcdsaP384Sha384 => { + // RFC 8624 3.1 DNSSEC Signing: MAY + // TODO + return Err(GenerateError::UnsupportedAlgorithm); + } + GenerateParams::Ed25519 => { + // RFC 8624 3.1 DNSSEC Signing: RECOMMENDED + // TODO + return Err(GenerateError::UnsupportedAlgorithm); + } + GenerateParams::Ed448 => { + // RFC 8624 3.1 DNSSEC Signing: MAY + // TODO + return Err(GenerateError::UnsupportedAlgorithm); + } + }; + + if activate_on_create { + // https://docs.oasis-open.org/kmip/testcases/v1.1/kmip-testcases-v1.1.html + // shows an example including an Activation Date value of 2 noted + // as meaning Thu Jan 01 01:00:02 CET 1970. i.e. the activation + // date should be a UNIX epoch timestamp. + let time_now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + + common_attrs.push(request::Attribute::ActivationDate(time_now)); + } + + let request = RequestPayload::CreateKeyPair( + Some(CommonTemplateAttribute::new(common_attrs)), + Some(PrivateKeyTemplateAttribute::new(priv_key_attrs)), + Some(PublicKeyTemplateAttribute::new(pub_key_attrs)), + ); + + // Execute the request and capture the response + let response = client.do_request(request).map_err(|err| { + error!("KMIP request failed: {err}"); + debug!( + "KMIP last request: {}", + client.last_req_diag_str().unwrap_or_default() + ); + debug!( + "KMIP last response: {}", + client.last_res_diag_str().unwrap_or_default() + ); + GenerateError::Kmip(err.to_string()) + })?; + trace!("Key generation operation complete"); + + // Drop the KMIP client so that it will be returned to the pool and + // thus be available below when KeyPair::new() is invoked and tries to + // fetch the details needed to determine the DNSKEY RR. + drop(client); + + // Process the successful response + let ResponsePayload::CreateKeyPair(payload) = response else { + error!("KMIP request failed: Wrong response type received!"); + return Err(GenerateError::Kmip( + "Unable to parse KMIP response: payload should be CreateKeyPair".to_string(), + )); + }; + + let CreateKeyPairResponsePayload { + private_key_unique_identifier, + public_key_unique_identifier, + } = payload; + + trace!("Creating KeyPair with DNSKEY"); + + let key_pair = KeyPair::from_metadata( + algorithm, + flags, + private_key_unique_identifier.as_str(), + public_key_unique_identifier.as_str(), + conn_pool.clone(), + ) + .map_err(|err| GenerateError::Kmip(err.to_string()))?; + + // Activate the key if not already, otherwise it cannot be used for + // signing. + if !activate_on_create { + let client = conn_pool.get().map_err(|err| { + GenerateError::Kmip(format!( + "Key generation failed: Cannot connect to KMIP server {}: {err}", + conn_pool.server_id() + )) + })?; + let request = + RequestPayload::Activate(Some(private_key_unique_identifier)); + + // Execute the request and capture the response + trace!("Activating KMIP key..."); + let response = client.do_request(request).map_err(|err| { + eprintln!("KMIP activate private key request failed: {err}"); + eprintln!( + "KMIP last request: {}", + client.last_req_diag_str().unwrap_or_default() + ); + eprintln!( + "KMIP last response: {}", + client.last_res_diag_str().unwrap_or_default() + ); + GenerateError::Kmip(err.to_string()) + })?; + trace!("Activate operation complete"); + + // Process the successful response + let ResponsePayload::Activate(_) = response else { + error!("KMIP request failed: Wrong response type received!"); + return Err(GenerateError::Kmip( + "Unable to parse KMIP response: payload should be Activate" + .to_string(), + )); + }; + } + + Ok(key_pair) + } + + //----------- destroy() -------------------------------------------------- + + /// Destroy a KMIP key by ID using a given KMIP server connection pool. + /// + /// As a KMIP key cannot be destroyed if it is active, this function first + /// attempts to revoke the key and then destroy it. + pub fn destroy( + key_id: &str, + conn_pool: SyncConnPool, + ) -> Result<(), DestroyError> { + let client = conn_pool.get().map_err(|err| { + DestroyError::Kmip(format!( + "Key destruction failed: Cannot connect to KMIP server {}: {err}", + conn_pool.server_id() + )) + })?; + + client + .revoke_key(key_id) + .map_err(|err| DestroyError::Kmip(err.to_string()))?; + client + .destroy_key(key_id) + .map_err(|err| DestroyError::Kmip(err.to_string())) + } +} + +//============ Error Types =================================================== + +//----------- GenerateError -------------------------------------------------- + +/// An error occurred while generating a key pair with a KMIP server. +#[derive(Clone, Debug)] +pub enum GenerateError { + /// The requested algorithm is not supported. + UnsupportedAlgorithm, + + /// A problem occurred while communicating with the KMIP server. + Kmip(String), +} + +//--- Formatting + +impl fmt::Display for GenerateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnsupportedAlgorithm => { + write!(f, "algorithm not supported") + } + Self::Kmip(err) => { + write!( + f, + "a problem occurred while communicating with the KMIP server: {err}" + ) + } + } + } +} + +//--- impl Error + +impl std::error::Error for GenerateError {} + +//------------ DestroyError -------------------------------------------------- + +/// An error occurred while destroying a key using KMIP. +#[derive(Clone, Debug)] +pub enum DestroyError { + /// A problem occurred while communicating with the KMIP server. + Kmip(String), +} + +//--- Formatting + +impl fmt::Display for DestroyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Kmip(err) => { + write!( + f, + "a problem occurred while communicating with the KMIP server: {err}" + ) + } + } + } +} + +//--- Error + +impl std::error::Error for DestroyError {} + +//------------ KeyUrlError --------------------------------------------------- + +/// An error occurred while parsing a KMIP key URL. +#[derive(Clone, Debug)] +pub struct KeyUrlParseError(String); + +//--- Formatting + +impl fmt::Display for KeyUrlParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "invalid key URL: {}", self.0) + } +} + +//--- impl Error + +impl std::error::Error for KeyUrlParseError {} + +//--- Conversions + +impl From for KeyUrlParseError { + fn from(err: String) -> Self { + KeyUrlParseError(err) + } +} + +//------------ PublicKeyError ------------------------------------------------ + +/// An error occurred while retrieving a KMIP public key. +#[derive(Clone, Debug)] +pub enum PublicKeyError { + /// The cryptographic algorithm of the KMIP key does not match the + /// specified DNSSEC algorithm. + AlgorithmMismatch { + /// The DNSSEC algorithm that was expected. + expected: SecurityAlgorithm, + + /// The type of key data received from the KMIP server. + actual: String, + }, + + /// A problem occurred while communicating with the KMIP server. + Kmip(String), +} + +//--- Formatting + +impl fmt::Display for PublicKeyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::AlgorithmMismatch { expected, actual } => { + write!( + f, + "algorithm mismatch: expected {expected} but found {actual}" + ) + } + Self::Kmip(err) => { + write!( + f, + "a problem occurred while communicating with the KMIP server: {err}" + ) + } + } + } +} + +//--- impl Error + +impl std::error::Error for PublicKeyError {} + +//--- Conversions + +impl From for PublicKeyError { + fn from(err: kmip::client::Error) -> Self { + PublicKeyError::Kmip(err.to_string()) + } +} + +//============ Testing ======================================================= + +#[cfg(test)] +mod tests { + use core::time::Duration; + + use std::fs::File; + use std::io::{BufReader, Read}; + use std::string::ToString; + use std::time::SystemTime; + use std::vec::Vec; + + use domain::base::iana::SecurityAlgorithm; + use kmip::client::ConnectionSettings; + use kmip::client::pool::ConnectionManager; + + use domain::crypto::sign::SignRaw; + + use crate::sign::KeyPair; + + use super::{PublicKey, sign::generate}; + + fn init_logging() { + use tracing_subscriber::EnvFilter; + + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .with_thread_ids(true) + .without_time() + // Useful sometimes: + // .with_span_events(tracing_subscriber::fmt::format::FmtSpan::NEW) + .init(); + } + + /// Test [`PublicKey::parse_rsa_from_pkcs1()`]. + #[test] + fn parse_rsa_key_from_pkcs1() { + // TODO: Find real-world samples. + let bytes = [48, 6, 2, 1, 127, 2, 1, 42]; + let key = PublicKey::parse_rsa_from_pkcs1( + SecurityAlgorithm::RSASHA256, + &bytes, + ) + .unwrap(); + assert_eq!(key.algorithm, SecurityAlgorithm::RSASHA256); + assert_eq!(key.public_key, [1, 42, 127]); + } + + /// Test [`PublicKey::parse_rsa_from_raw()`]. + #[test] + fn parse_rsa_key_from_raw() { + // TODO: Find real-world samples. + let bytes = [ + 48, 21, 48, 11, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 48, 6, 2, + 1, 127, 2, 1, 42, + ]; + let key = + PublicKey::parse_rsa_from_raw(SecurityAlgorithm::RSASHA256, &bytes) + .unwrap(); + assert_eq!(key.algorithm, SecurityAlgorithm::RSASHA256); + assert_eq!(key.public_key, [1, 42, 127]); + } + + /// Test [`PublicKey::parse_ecdsa_from_raw()`]. + #[test] + fn parse_ecdsa_key_from_raw() { + // TODO: Find real-world samples. + let bytes = [ + 48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, + 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + let key = PublicKey::parse_ecdsa_from_raw( + SecurityAlgorithm::ECDSAP256SHA256, + &bytes, + ) + .unwrap(); + assert_eq!(key.algorithm, SecurityAlgorithm::ECDSAP256SHA256); + assert_eq!( + key.public_key, + [ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0 + ] + ); + } + + /// Test [`KeyPair::parse_ecdsa_sig_from_x962()`]. + #[test] + fn parse_ecdsa_sig_from_x962() { + // TODO: Find real-world samples. + let bytes = [48, 6, 2, 1, 21, 2, 1, 47]; + let signature = KeyPair::parse_ecdsa_sig_from_x962(&bytes).unwrap(); + assert_eq!( + *signature, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 47 + ] + ); + } + + #[test] + #[ignore = "Requires running PyKMIP"] + fn pykmip_connect() { + init_logging(); + let mut cert_bytes = Vec::new(); + let file = File::open( + "/home/ximon/docker_data/pykmip/pykmip-data/selfsigned.crt", + ) + .unwrap(); + let mut reader = BufReader::new(file); + reader.read_to_end(&mut cert_bytes).unwrap(); + + let mut key_bytes = Vec::new(); + let file = File::open( + "/home/ximon/docker_data/pykmip/pykmip-data/selfsigned.key", + ) + .unwrap(); + let mut reader = BufReader::new(file); + reader.read_to_end(&mut key_bytes).unwrap(); + + let conn_settings = ConnectionSettings { + host: "localhost".to_string(), + port: 5696, + insecure: true, + client_cert: Some(kmip::client::ClientCertificate::SeparatePem { + cert_bytes, + key_bytes, + }), + ..Default::default() + }; + + eprintln!("Creating pool..."); + let pool = ConnectionManager::create_connection_pool( + "Test server".to_string(), + conn_settings.into(), + 16384, + Some(Duration::from_secs(60)), + Some(Duration::from_secs(60)), + ) + .unwrap(); + + eprintln!("Connecting..."); + let client = pool.get().unwrap(); + + eprintln!("Connected"); + let res = client.query(); + dbg!(&res); + res.unwrap(); + + let pub_key_name = format!( + "{}", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + ); + let pri_key_name = format!( + "{}", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + ); + let res = generate( + pub_key_name, + pri_key_name, + domain::crypto::sign::GenerateParams::RsaSha256 { bits: 2048 }, + // crate::crypto::sign::GenerateParams::EcdsaP256Sha256, + 256, + pool, + ); + dbg!(&res); + let key = res.unwrap(); + + eprintln!("DNSKEY: {}", key.dnskey()); + } + + #[test] + #[ignore = "Requires Fortanix credentials"] + fn fortanix_dsm_test() { + // Note: keyls fails against Fortanix DSM for some reason with error: + // Error: Server error: Operation Locate failed: expected + // AttributeValue, got ObjectType, Diagnostics: + // req: 78[77[69[6Ai6Bi]0C[23[24e1:25[99tA1t]]]0Di]0F[5Ce8:79[08[0At57e4:]]]], + // resp: 7B[7A[69[6Ai6Bi]92d0Di]0F[5Ce8:7Fe1:7Ee100:7Dt]] + + init_logging(); + + // conn_settings.host = "eu.smartkey.io".to_string(); + // conn_settings.port = 5696; + // conn_settings.username = Some(env!("FORTANIX_USER").to_string()); + // conn_settings.password = Some(env!("FORTANIX_PASS").to_string()); + + let conn_settings = ConnectionSettings { + host: "127.0.0.1".to_string(), + port: 5696, + insecure: true, + connect_timeout: Some(Duration::from_secs(3)), + read_timeout: Some(Duration::from_secs(30)), + write_timeout: Some(Duration::from_secs(3)), + ..Default::default() + }; + + eprintln!("Creating pool..."); + let pool = ConnectionManager::create_connection_pool( + "Test server".to_string(), + conn_settings.into(), + 16384, + Some(Duration::from_secs(60)), + Some(Duration::from_secs(60)), + ) + .unwrap(); + + eprintln!("Connecting..."); + let client = pool.get().unwrap(); + + eprintln!("Connected"); + // let res = client.query(); + // dbg!(&res); + // res.unwrap(); + + let pub_key_name = format!( + "{}", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + ); + let pri_key_name = format!( + "{}", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + ); + let res = generate( + pub_key_name, + pri_key_name, + domain::crypto::sign::GenerateParams::RsaSha256 { bits: 1024 }, + // crate::crypto::sign::GenerateParams::EcdsaP256Sha256, + 256, + pool, + ); + let key = res.unwrap(); + eprintln!("Generated public key with id: {}", key.public_key_id()); + eprintln!("Generated private key with id: {}", key.private_key_id()); + + // sleep(Duration::from_secs(5)); + + eprintln!("DNSKEY: {}", key.dnskey()); + + client.activate_key(key.public_key_id()).unwrap(); + + // Fortanix: Activating the public key also activates the private key. + // Attempting to then activate the private key fails as it is already + // active. Yet signing fails with "Object is not yet active"... + // client.activate_key(key.private_key_id()).unwrap(); + + // // This works round the not yet active yet error. + // sleep(Duration::from_secs(5)); + + // let request = RequestPayload::Sign( + // Some(UniqueIdentifier(key.private_key_id().to_string())), + // // While the KMIP 1.2 spec says crypto parameters are optional and + // // if not specified those of the key will be used, Fortanix + // // complains about "No cryptographic parameters specified" if this + // // is None, and "Must specicify HashingAlgorithm" if that is not + // // specified. + // Some( + // CryptographicParameters::default() + // // .with_padding_method(PaddingMethod::) + // .with_hashing_algorithm(HashingAlgorithm::SHA256) + // .with_cryptographic_algorithm( + // CryptographicAlgorithm::RSA, + // //CryptographicAlgorithm::ECDSA, + // ), + // ), + // Data("Message for ECDSA signing".as_bytes().to_vec()), + // ); + + // // Execute the request and capture the response + // let res = client.do_request(request).unwrap(); + + // dbg!(&res); + + // let ResponsePayload::Sign(signed) = res else { + // unreachable!(); + // }; + + // // let signature = + // // openssl::ecdsa::EcdsaSig::from_der(&signed.signature_data) + // // .unwrap(); + + // // dbg!(signature.r().to_vec_padded(32)); + // // dbg!(signature.s().to_vec_padded(32)); + + // // dbg!(response); + } +}