diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..6e599d3 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +# +# SPDX-License-Identifier: Apache-2.0 + +[alias] +check-wasm = "check --workspace --target wasm32-unknown-unknown --no-default-features --features wasm" diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9d56117 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +# +# SPDX-License-Identifier: Apache-2.0 + +version: 2 +updates: + - package-ecosystem: cargo + directory: / + schedule: + interval: weekly + day: monday + time: "04:00" + timezone: Europe/Copenhagen + open-pull-requests-limit: 10 + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + time: "04:00" + timezone: Europe/Copenhagen + open-pull-requests-limit: 10 diff --git a/.github/workflows/crates-package-preflight.yml b/.github/workflows/crates-package-preflight.yml new file mode 100644 index 0000000..9a7ca1b --- /dev/null +++ b/.github/workflows/crates-package-preflight.yml @@ -0,0 +1,194 @@ +# SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +# +# SPDX-License-Identifier: Apache-2.0 + +name: Crates Package Preflight + +run-name: Crates package preflight ${{ inputs.version }} @ ${{ github.sha }} + +on: + workflow_dispatch: + inputs: + version: + description: Release version without a leading v + required: true + type: string + default: 0.2.0 + +concurrency: + group: crates-package-preflight-${{ inputs.version }}-${{ github.sha }} + cancel-in-progress: false + +permissions: + contents: read + +env: + BUFFA_VERSION: 0.9.0 + BUF_VERSION: 1.71.0 + CARGO_AUDIT_VERSION: 0.22.2 + CARGO_CHECK_EXTERNAL_TYPES_VERSION: 0.5.0 + CARGO_DENY_VERSION: 0.20.2 + CARGO_FUZZ_VERSION: 0.13.2 + CARGO_NEXTEST_VERSION: 0.9.140 + EXTERNAL_TYPES_NIGHTLY: nightly-2026-03-20 + FUZZ_NIGHTLY: nightly-2026-07-01 + WASM_BINDGEN_CLI_VERSION: 0.2.126 + WASM_PACK_VERSION: 0.15.0 + BUF_LINUX_X86_64_SHA256: d3de2838c68a5759ca276884254bc70df4e4ad185d6ed5f65f327b6ce6363eab + +jobs: + verify-source-sha: + name: Verify release source + runs-on: ubuntu-24.04 + outputs: + release_sha: ${{ steps.verify-source.outputs.release_sha }} + release_version: ${{ steps.verify-source.outputs.release_version }} + steps: + - name: Checkout requested release commit + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + persist-credentials: false + + - name: Install Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + + - name: Match workflow head, current main, and crate versions + id: verify-source + env: + RELEASE_SHA: ${{ github.sha }} + RELEASE_VERSION: ${{ inputs.version }} + RELEASE_SOURCE_WRITE_GITHUB_OUTPUT: '1' + run: node scripts/verify_release_source.mjs + + crates-package: + name: Review crates.io packages + needs: verify-source-sha + runs-on: ubuntu-24.04 + steps: + - name: Checkout certified release commit + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ needs.verify-source-sha.outputs.release_sha }} + fetch-depth: 0 + persist-credentials: false + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master + with: + toolchain: 1.96.0 + components: rustfmt, clippy + targets: wasm32-unknown-unknown + + - name: Cache Cargo + uses: Swatinem/rust-cache@23869a5bd66c73db3c0ac40331f3206eb23791dc # v2.9.1 + + - name: Install Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + + - name: Install buf + shell: bash + run: | + set -euo pipefail + install_dir="$RUNNER_TEMP/buf/bin" + mkdir -p "$install_dir" + curl --fail-with-body --location --proto '=https' --tlsv1.2 \ + --retry 5 --retry-all-errors \ + --output "$install_dir/buf" \ + "https://github.com/bufbuild/buf/releases/download/v${BUF_VERSION}/buf-Linux-x86_64" + printf '%s %s\n' "$BUF_LINUX_X86_64_SHA256" "$install_dir/buf" \ + | sha256sum --check --strict + chmod 0755 "$install_dir/buf" + printf '%s\n' "$install_dir" >> "$GITHUB_PATH" + "$install_dir/buf" --version + + - name: Install pinned Buffa generators + run: | + cargo install protoc-gen-buffa --version "$BUFFA_VERSION" --locked + cargo install protoc-gen-buffa-packaging --version "$BUFFA_VERSION" --locked + + - name: Install nextest + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + with: + tool: cargo-nextest@${{ env.CARGO_NEXTEST_VERSION }} + + - name: Install external-type checker + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + with: + tool: cargo-check-external-types@${{ env.CARGO_CHECK_EXTERNAL_TYPES_VERSION }} + + - name: Install external-type checker toolchain + run: rustup toolchain install "$EXTERNAL_TYPES_NIGHTLY" --profile minimal + + - name: Install fuzz toolchain and runner + run: | + rustup toolchain install "$FUZZ_NIGHTLY" --profile minimal + cargo install cargo-fuzz --version "$CARGO_FUZZ_VERSION" --locked + + - name: Install wasm-pack + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + with: + tool: wasm-pack@${{ env.WASM_PACK_VERSION }} + + - name: Install wasm-bindgen CLI + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + with: + tool: wasm-bindgen-cli@${{ env.WASM_BINDGEN_CLI_VERSION }} + + - name: Install cargo-deny + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + with: + tool: cargo-deny@${{ env.CARGO_DENY_VERSION }} + + - name: Install cargo-audit + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + with: + tool: cargo-audit@${{ env.CARGO_AUDIT_VERSION }} + + - name: Check release version and publish order + env: + RELEASE_VERSION: ${{ needs.verify-source-sha.outputs.release_version }} + run: node scripts/publish_crates_in_order.mjs order + + - name: Run pinned release-readiness gates + env: + RELEASE_VERSION: ${{ needs.verify-source-sha.outputs.release_version }} + run: node scripts/run_pinned_release_readiness.mjs --release-packages + + - name: Inspect normalized crate tarballs and dry-run publication + env: + RELEASE_VERSION: ${{ needs.verify-source-sha.outputs.release_version }} + run: node scripts/publish_crates_in_order.mjs inspect + + - name: Write reviewed package attestation + env: + RELEASE_SHA: ${{ needs.verify-source-sha.outputs.release_sha }} + RELEASE_VERSION: ${{ needs.verify-source-sha.outputs.release_version }} + run: node scripts/write_release_attestation.mjs + + - name: Upload reviewed package attestation + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: reallyme-cose-crates-preflight-${{ inputs.version }}-${{ github.sha }} + path: release-attestation/crates-preflight.json + if-no-files-found: error + retention-days: 30 + + - name: Summarize reviewed release + env: + RELEASE_SHA: ${{ needs.verify-source-sha.outputs.release_sha }} + RELEASE_VERSION: ${{ needs.verify-source-sha.outputs.release_version }} + run: | + { + echo '## Crates package preflight passed' + echo + echo "- Version: \`${RELEASE_VERSION}\`" + echo "- Commit: \`${RELEASE_SHA}\`" + echo "- Publish order: \`reallyme-cose-proto\`, then \`reallyme-cose\`" + echo "- Attestation run ID: \`${GITHUB_RUN_ID}\`" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/crates-release.yml b/.github/workflows/crates-release.yml index 5e6ea0f..ea2030c 100644 --- a/.github/workflows/crates-release.yml +++ b/.github/workflows/crates-release.yml @@ -4,49 +4,104 @@ name: Crates.io Release +run-name: Crates.io release ${{ inputs.version }} @ ${{ inputs.release_sha }} + on: workflow_dispatch: inputs: - publish: - description: Publish reallyme-cose to crates.io after the dry run succeeds + preflight_run_id: + description: Successful Crates Package Preflight run ID that reviewed this release + required: true + type: string + release_sha: + description: Exact current main commit SHA certified by the preflight run required: true - type: boolean - default: false + type: string + version: + description: Exact crate version certified by the preflight run + required: true + type: string + default: 0.2.0 + +concurrency: + group: crates-release-${{ inputs.version }}-${{ inputs.release_sha }} + cancel-in-progress: false permissions: + actions: read contents: read jobs: - dry-run: - name: crates.io publish preflight - runs-on: ubuntu-latest - permissions: - contents: read + verify-preflight: + name: Verify reviewed package preflight + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-24.04 + outputs: + release_sha: ${{ steps.verify-attestation.outputs.release_sha }} + release_version: ${{ steps.verify-attestation.outputs.release_version }} steps: - - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + - name: Checkout certified release commit + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ inputs.release_sha }} + fetch-depth: 1 + persist-credentials: false - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master + - name: Install Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - toolchain: 1.96.0 + node-version: '24' - - name: Cache Cargo - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + - name: Download reviewed package attestation + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: reallyme-cose-crates-preflight-${{ inputs.version }}-${{ inputs.release_sha }} + path: release-attestation + github-token: ${{ github.token }} + repository: ${{ github.repository }} + run-id: ${{ inputs.preflight_run_id }} + + - name: Verify preflight conclusion, commit, and reviewed inputs + id: verify-attestation + env: + GH_TOKEN: ${{ github.token }} + PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }} + RELEASE_SHA: ${{ inputs.release_sha }} + RELEASE_VERSION: ${{ inputs.version }} + RELEASE_ATTESTATION_WRITE_GITHUB_OUTPUT: '1' + run: node scripts/verify_release_attestation.mjs - - name: Inspect publishable crate tarballs - run: node scripts/publish_crates_in_order.mjs inspect + - name: Summarize release authorization + env: + PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }} + RELEASE_SHA: ${{ inputs.release_sha }} + RELEASE_VERSION: ${{ inputs.version }} + run: | + { + echo '## Reviewed preflight accepted' + echo + echo "- Version: \`${RELEASE_VERSION}\`" + echo "- Commit: \`${RELEASE_SHA}\`" + echo "- Preflight run ID: \`${PREFLIGHT_RUN_ID}\`" + } >> "$GITHUB_STEP_SUMMARY" publish: - name: publish reallyme-cose - needs: dry-run - if: inputs.publish == true - runs-on: ubuntu-latest + name: Publish reallyme-cose crates + needs: verify-preflight + runs-on: ubuntu-24.04 + environment: crates-io-release permissions: contents: read + env: + RELEASE_SHA: ${{ needs.verify-preflight.outputs.release_sha }} + RELEASE_VERSION: ${{ needs.verify-preflight.outputs.release_version }} steps: - - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + - name: Checkout certified release commit without credentials + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ needs.verify-preflight.outputs.release_sha }} + fetch-depth: 0 + persist-credentials: false - name: Install Rust toolchain uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master @@ -54,9 +109,70 @@ jobs: toolchain: 1.96.0 - name: Cache Cargo - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + uses: Swatinem/rust-cache@23869a5bd66c73db3c0ac40331f3206eb23791dc # v2.9.1 - - name: Publish reallyme-cose - run: node scripts/publish_crates_in_order.mjs publish + - name: Install Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + + - name: Revalidate current main immediately before publication + run: node scripts/verify_release_source.mjs + + - name: Publish crates in reviewed dependency order env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: node scripts/publish_crates_in_order.mjs publish + + - name: Summarize crates.io publication + run: | + { + echo '## crates.io publication completed' + echo + echo "- [reallyme-cose-proto ${RELEASE_VERSION}](https://crates.io/crates/reallyme-cose-proto/${RELEASE_VERSION})" + echo "- [reallyme-cose ${RELEASE_VERSION}](https://crates.io/crates/reallyme-cose/${RELEASE_VERSION})" + } >> "$GITHUB_STEP_SUMMARY" + + finalize: + name: Tag and create GitHub release + needs: [verify-preflight, publish] + runs-on: ubuntu-24.04 + permissions: + contents: write + env: + RELEASE_SHA: ${{ needs.verify-preflight.outputs.release_sha }} + RELEASE_VERSION: ${{ needs.verify-preflight.outputs.release_version }} + steps: + - name: Create release tag through the GitHub API + id: release-tag + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + tag="reallyme-cose-v${RELEASE_VERSION}" + existing_commit="$(gh api "repos/$GITHUB_REPOSITORY/commits/$tag" --jq '.sha' 2>/dev/null || true)" + if [[ -n "$existing_commit" && "$existing_commit" != "$RELEASE_SHA" ]]; then + echo "$tag already exists at a different commit" >&2 + exit 1 + fi + if [[ -z "$existing_commit" ]]; then + gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \ + --field ref="refs/tags/$tag" \ + --field sha="$RELEASE_SHA" > /dev/null + fi + printf 'tag=%s\n' "$tag" >> "$GITHUB_OUTPUT" + + - name: Create GitHub release + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ steps.release-tag.outputs.tag }} + run: | + set -euo pipefail + if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then + exit 0 + fi + gh release create "$RELEASE_TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --verify-tag \ + --title "$RELEASE_TAG" \ + --generate-notes diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 380ca84..29fbc3f 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -11,7 +11,7 @@ on: - "docs/**" - "LICENSE" - "NOTICE" - - ".github/renovate.json" + - ".github/dependabot.yml" push: branches: - main @@ -20,7 +20,7 @@ on: - "docs/**" - "LICENSE" - "NOTICE" - - ".github/renovate.json" + - ".github/dependabot.yml" schedule: - cron: "41 3 * * 1" workflow_dispatch: @@ -28,31 +28,42 @@ on: permissions: contents: read +env: + CARGO_FUZZ_VERSION: 0.13.2 + NIGHTLY_TOOLCHAIN: nightly-2026-07-01 + FUZZ_MAX_TOTAL_TIME_SECONDS: 900 + jobs: build: name: fuzz target build - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install Rust nightly uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: nightly + toolchain: ${{ env.NIGHTLY_TOOLCHAIN }} - name: Cache Cargo - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + uses: Swatinem/rust-cache@23869a5bd66c73db3c0ac40331f3206eb23791dc # v2.9.1 - name: Install cargo-fuzz - run: cargo install cargo-fuzz --locked + run: cargo install cargo-fuzz --version "$CARGO_FUZZ_VERSION" --locked + + - name: Verify fuzz lockfile is current + run: cargo metadata --manifest-path fuzz/Cargo.toml --locked --no-deps - name: Build fuzz targets - run: cargo +nightly fuzz build + run: cargo +nightly-2026-07-01 fuzz build + + - name: Verify fuzz build did not rewrite the lockfile + run: git diff --exit-code -- fuzz/Cargo.lock scheduled: name: scheduled fuzz (${{ matrix.target }}) - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' strategy: fail-fast: false @@ -61,27 +72,40 @@ jobs: - cose_sign1 - cose_key - multikey_to_cose + - wire + - cose_encrypt steps: - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install Rust nightly uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: nightly + toolchain: ${{ env.NIGHTLY_TOOLCHAIN }} - name: Cache Cargo - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + uses: Swatinem/rust-cache@23869a5bd66c73db3c0ac40331f3206eb23791dc # v2.9.1 - name: Install cargo-fuzz - run: cargo install cargo-fuzz --locked + run: cargo install cargo-fuzz --version "$CARGO_FUZZ_VERSION" --locked + + - name: Verify fuzz lockfile is current + run: cargo metadata --manifest-path fuzz/Cargo.toml --locked --no-deps + + - name: Restore and persist fuzz corpus + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: fuzz/corpus/${{ matrix.target }} + key: fuzz-corpus-v1-${{ runner.os }}-${{ matrix.target }}-${{ github.run_id }} + restore-keys: | + fuzz-corpus-v1-${{ runner.os }}-${{ matrix.target }}- - name: Run time-boxed fuzz target - run: cargo +nightly fuzz run ${{ matrix.target }} -- -max_total_time=300 -rss_limit_mb=4096 + run: cargo +nightly-2026-07-01 fuzz run ${{ matrix.target }} -- -max_total_time="$FUZZ_MAX_TOTAL_TIME_SECONDS" -rss_limit_mb=4096 - name: Upload crash artifacts if: failure() - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: fuzz-artifacts-${{ matrix.target }} path: fuzz/artifacts/${{ matrix.target }} diff --git a/.github/workflows/protobuf-ci.yml b/.github/workflows/protobuf-ci.yml new file mode 100644 index 0000000..a605293 --- /dev/null +++ b/.github/workflows/protobuf-ci.yml @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +# +# SPDX-License-Identifier: Apache-2.0 + +name: Protobuf Checks + +on: + workflow_dispatch: + pull_request: + paths: + - ".github/workflows/protobuf-ci.yml" + - "buf.yaml" + - "buf.gen.yaml" + - "crates/proto/**" + - "scripts/check_release_readiness.mjs" + - "scripts/release-readiness/core.mjs" + - "scripts/harden-generated-cose-proto.mjs" + push: + branches: + - main + paths: + - ".github/workflows/protobuf-ci.yml" + - "buf.yaml" + - "buf.gen.yaml" + - "crates/proto/**" + - "scripts/check_release_readiness.mjs" + - "scripts/release-readiness/core.mjs" + - "scripts/harden-generated-cose-proto.mjs" + +permissions: + contents: read + +env: + BUFFA_VERSION: 0.9.0 + BUF_VERSION: 1.71.0 + BUF_LINUX_X86_64_SHA256: d3de2838c68a5759ca276884254bc70df4e4ad185d6ed5f65f327b6ce6363eab + +jobs: + protobuf: + name: lint, generated freshness + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master + with: + toolchain: 1.96.0 + components: rustfmt + + - name: Install Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + + - name: Cache Cargo + uses: Swatinem/rust-cache@23869a5bd66c73db3c0ac40331f3206eb23791dc # v2.9.1 + + - name: Install buf + shell: bash + run: | + set -euo pipefail + install_dir="$RUNNER_TEMP/buf/bin" + mkdir -p "$install_dir" + curl --fail-with-body --location --proto '=https' --tlsv1.2 \ + --retry 5 --retry-all-errors \ + --output "$install_dir/buf" \ + "https://github.com/bufbuild/buf/releases/download/v${BUF_VERSION}/buf-Linux-x86_64" + printf '%s %s\n' "$BUF_LINUX_X86_64_SHA256" "$install_dir/buf" \ + | sha256sum --check --strict + chmod 0755 "$install_dir/buf" + printf '%s\n' "$install_dir" >> "$GITHUB_PATH" + "$install_dir/buf" --version + + - name: Install pinned Buffa generators + run: | + cargo install protoc-gen-buffa --version "$BUFFA_VERSION" --locked + cargo install protoc-gen-buffa-packaging --version "$BUFFA_VERSION" --locked + + - name: Checkout release-readiness runner + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: reallyme/release-readiness + ref: f27973caf9d3a12847cac4032c361f5f553c97e9 + path: .release-readiness + persist-credentials: false + + - name: Check release readiness generated freshness + run: node .release-readiness/scripts/run-consumer-check.mjs --generated-freshness diff --git a/.github/workflows/release-preflight.yml b/.github/workflows/release-preflight.yml deleted file mode 100644 index d6e98ec..0000000 --- a/.github/workflows/release-preflight.yml +++ /dev/null @@ -1,48 +0,0 @@ -# SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved -# -# SPDX-License-Identifier: Apache-2.0 - -name: Release Preflight - -on: - workflow_dispatch: - -permissions: - contents: read - -jobs: - rust-crate: - name: rust crate metadata - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master - with: - toolchain: 1.96.0 - components: rustfmt, clippy - targets: wasm32-unknown-unknown - - - name: Cache Cargo - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - - - name: Install Node - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 - with: - node-version: '24' - - - name: Install nextest - uses: taiki-e/install-action@c61310c7f42caf5b425e349b6742d93ca197327c # nextest - - - name: Install cargo-deny - uses: taiki-e/install-action@c61310c7f42caf5b425e349b6742d93ca197327c # cargo-deny - with: - tool: cargo-deny - - - name: Check release readiness contract - run: node scripts/check_release_readiness.mjs - - - name: Inspect crate tarball and dry-run publish - run: node scripts/publish_crates_in_order.mjs inspect diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index d14ebba..51b72d2 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -12,7 +12,7 @@ on: - "docs/**" - "LICENSE" - "NOTICE" - - ".github/renovate.json" + - ".github/dependabot.yml" push: branches: - main @@ -21,18 +21,27 @@ on: - "docs/**" - "LICENSE" - "NOTICE" - - ".github/renovate.json" + - ".github/dependabot.yml" permissions: contents: read +env: + CARGO_AUDIT_VERSION: 0.22.2 + CARGO_CHECK_EXTERNAL_TYPES_VERSION: 0.5.0 + CARGO_DENY_VERSION: 0.20.2 + CARGO_NEXTEST_VERSION: 0.9.140 + EXTERNAL_TYPES_NIGHTLY: nightly-2026-03-20 + WASM_BINDGEN_CLI_VERSION: 0.2.126 + WASM_PACK_VERSION: 0.15.0 + jobs: rust: name: fmt, lint, test, feature lanes - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master @@ -42,71 +51,128 @@ jobs: targets: wasm32-unknown-unknown - name: Cache Cargo - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + uses: Swatinem/rust-cache@23869a5bd66c73db3c0ac40331f3206eb23791dc # v2.9.1 - name: Install nextest - uses: taiki-e/install-action@c61310c7f42caf5b425e349b6742d93ca197327c # nextest + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + with: + tool: cargo-nextest@${{ env.CARGO_NEXTEST_VERSION }} - name: Install cargo-deny - uses: taiki-e/install-action@c61310c7f42caf5b425e349b6742d93ca197327c # cargo-deny + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 with: - tool: cargo-deny + tool: cargo-deny@${{ env.CARGO_DENY_VERSION }} - name: Install cargo-audit - uses: taiki-e/install-action@c61310c7f42caf5b425e349b6742d93ca197327c # cargo-audit + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + with: + tool: cargo-audit@${{ env.CARGO_AUDIT_VERSION }} + + - name: Install external-type checker + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 with: - tool: cargo-audit + tool: cargo-check-external-types@${{ env.CARGO_CHECK_EXTERNAL_TYPES_VERSION }} + + - name: Install external-type checker toolchain + run: rustup toolchain install "$EXTERNAL_TYPES_NIGHTLY" --profile minimal - name: Format run: cargo fmt --check - name: Check all features - run: cargo check --workspace --all-features + run: cargo check --locked --workspace --all-features - name: Check warnings as errors - run: cargo check --workspace --all-features + run: cargo check --locked --workspace --all-features env: RUSTFLAGS: -Dwarnings - name: Check structure-only lane - run: cargo check --workspace --no-default-features + run: cargo check --locked --workspace --no-default-features env: RUSTFLAGS: -Dwarnings - name: Clippy - run: cargo clippy --workspace --all-targets --all-features -- -D warnings + run: cargo clippy --locked --workspace --all-targets --all-features -- -D warnings + + - name: Check public external-type provenance + run: cargo +"${EXTERNAL_TYPES_NIGHTLY}" check-external-types --manifest-path crates/cose/Cargo.toml --all-features - name: Format vector audit tool run: cargo fmt --manifest-path tools/vector-audit/Cargo.toml --check - name: Clippy vector audit tool - run: cargo clippy --manifest-path tools/vector-audit/Cargo.toml --all-targets -- -D warnings + run: cargo clippy --locked --manifest-path tools/vector-audit/Cargo.toml --all-targets -- -D warnings + + - name: Format vector golden generator + run: cargo fmt --manifest-path tools/vector-goldens/Cargo.toml --check + + - name: Clippy vector golden generator + run: cargo clippy --locked --manifest-path tools/vector-goldens/Cargo.toml --all-targets -- -D warnings - name: Test native lane - run: cargo nextest run --workspace --no-default-features --features native + run: cargo nextest run --locked --workspace --no-default-features --features native - name: Test all features - run: cargo nextest run --workspace --all-features + run: cargo nextest run --locked --workspace --all-features + + - name: Test release profile + run: cargo nextest run --release --locked --workspace --all-features - name: Independent vector audit - run: cargo run --manifest-path tools/vector-audit/Cargo.toml -- . + run: cargo run --locked --manifest-path tools/vector-audit/Cargo.toml --bin reallyme-cose-vector-audit -- . - name: Doc build - run: cargo doc --workspace --no-deps --all-features + run: cargo doc --locked --workspace --no-deps --all-features env: RUSTDOCFLAGS: -D warnings - name: Doctests - run: cargo test --workspace --all-features --doc + run: cargo test --locked --workspace --all-features --doc - name: Check native lane - run: cargo check --workspace --no-default-features --features native + run: cargo check --locked --workspace --no-default-features --features native + + - name: Check native wire lane + run: cargo check --locked --workspace --no-default-features --features native,wire - name: Check wasm32 lane - run: cargo check --workspace --no-default-features --features wasm --target wasm32-unknown-unknown + run: cargo check --locked --workspace --no-default-features --features wasm --target wasm32-unknown-unknown + + - name: Check wasm32 wire lane + run: cargo check --locked --workspace --no-default-features --features wasm,wire --target wasm32-unknown-unknown - name: Dependency policy run: cargo deny check - name: Security advisory audit run: cargo audit + + - name: Install Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + + - name: Install wasm-pack + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + with: + tool: wasm-pack@${{ env.WASM_PACK_VERSION }} + + - name: Install wasm-bindgen CLI + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + with: + tool: wasm-bindgen-cli@${{ env.WASM_BINDGEN_CLI_VERSION }} + + - name: Test COSE_Sign1 in the wasm runtime + run: wasm-pack test --node crates/cose --no-default-features --features wasm --test test_wasm_sign1 + + - name: Checkout release-readiness runner + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: reallyme/release-readiness + ref: f27973caf9d3a12847cac4032c361f5f553c97e9 + path: .release-readiness + persist-credentials: false + + - name: Release readiness + run: node .release-readiness/scripts/run-consumer-check.mjs --policy-only diff --git a/.gitignore b/.gitignore index 5a2e00b..db4a81a 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ target/ /tarpaulin-report.html /crypto-lane-logs/ /.cargo-release/ +/.release-readiness/ proptest-regressions/ # Generated Rust/source artifacts @@ -45,10 +46,8 @@ proptest-regressions/ **/generated/** **/src/generated **/src/generated/** -!crates/proto/crypto/src/generated/ -!crates/proto/crypto/src/generated/** -!packages/ts/src/proto/generated/ -!packages/ts/src/proto/generated/** +!crates/proto/src/generated/ +!crates/proto/src/generated/** **/src/pb **/src/pb/** **/src/prost diff --git a/Cargo.lock b/Cargo.lock index 18368c8..bb37bbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,7 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common 0.1.7", - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -57,6 +57,7 @@ dependencies = [ "ctr 0.10.1", "ghash", "subtle", + "zeroize", ] [[package]] @@ -84,6 +85,42 @@ dependencies = [ "zeroize", ] +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + +[[package]] +name = "allocation-counter" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beb9e990c0a33699f1984d85a6abead615ccc72dd8130bf3e15dcabe2ca149c9" + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + [[package]] name = "argon2" version = "0.5.3" @@ -109,6 +146,17 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -206,7 +254,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -228,12 +276,42 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "buffa" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97ce1f34252b8e2516042e1002d56c160908a7ff8a1c9d47e97836558d1c83a" +dependencies = [ + "base64", + "bytes", + "foldhash", + "hashbrown 0.15.5", + "once_cell", + "rustversion", + "serde", + "serde_json", + "smoothutf8", + "thiserror", +] + [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.67" @@ -259,7 +337,6 @@ dependencies = [ "cfg-if", "cipher 0.5.2", "cpufeatures 0.3.0", - "rand_core 0.10.1", "zeroize", ] @@ -335,6 +412,31 @@ dependencies = [ "inout 0.2.2", ] +[[package]] +name = "clap" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "cmov" version = "0.5.4" @@ -393,6 +495,66 @@ dependencies = [ "libc", ] +[[package]] +name = "criterion" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "num-traits", + "oorandom", + "page_size", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + [[package]] name = "crunchy" version = "0.2.4" @@ -422,7 +584,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "generic-array", + "generic-array 0.14.7", "rand_core 0.6.4", "typenum", ] @@ -477,7 +639,6 @@ dependencies = [ "curve25519-dalek-derive", "digest 0.11.3", "fiat-crypto", - "rand_core 0.10.1", "rustc_version", "subtle", "zeroize", @@ -491,7 +652,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -517,7 +678,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" dependencies = [ "data-encoding", - "syn", + "syn 2.0.119", ] [[package]] @@ -587,13 +748,30 @@ checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core 0.10.1", "sha2", - "signature", "subtle", "zeroize", ] +[[package]] +name = "ed448-goldilocks" +version = "0.14.0-pre.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b805154de2e68f59874ec217ca36790dcffe500cd872c60fe509d28d0814a74d" +dependencies = [ + "elliptic-curve", + "hash2curve", + "rand_core 0.10.1", + "shake", + "subtle", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + [[package]] name = "elliptic-curve" version = "0.14.1" @@ -644,6 +822,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "futures-core" version = "0.3.32" @@ -678,6 +862,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "generic-array" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab4e5aa225bc56696909483320f0ff9b600f1a971b52e07a17d70f3d9b43254b" +dependencies = [ + "rustversion", + "typenum", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -711,7 +905,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" dependencies = [ - "polyval 0.7.2", + "polyval 0.7.3", ] [[package]] @@ -736,6 +930,26 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hash2curve" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1eaf40612d7d854743e7189228a6d528f0f6e8502cf6a0cb831d28a218b7f3f6" +dependencies = [ + "digest 0.11.3", + "elliptic-curve", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", + "serde", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -771,12 +985,17 @@ dependencies = [ "chacha20poly1305", "hkdf", "hybrid-array", + "ml-kem", "p256", "p384", "p521", "rand_core 0.10.1", "sha2", + "sha3 0.12.0", + "shake", "subtle", + "turboshake", + "x-wing", "x25519-dalek", "zeroize", ] @@ -800,7 +1019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", ] [[package]] @@ -809,7 +1028,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -821,6 +1040,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -853,6 +1081,15 @@ dependencies = [ "wnaf", ] +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + [[package]] name = "keccak" version = "0.2.0" @@ -879,6 +1116,12 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "match-lookup" version = "0.1.2" @@ -887,7 +1130,7 @@ checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -896,6 +1139,16 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + [[package]] name = "ml-dsa" version = "0.1.1" @@ -996,10 +1249,19 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1007,6 +1269,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1015,6 +1278,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -1062,6 +1331,16 @@ dependencies = [ "sha2", ] +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "password-hash" version = "0.5.0" @@ -1108,6 +1387,34 @@ dependencies = [ "spki", ] +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "poly1305" version = "0.9.1" @@ -1132,9 +1439,9 @@ dependencies = [ [[package]] name = "polyval" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b20f20e954175de5f463f67781b35583397d916b1d148738923711b2ad16bee8" +checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" dependencies = [ "cpubits", "cpufeatures 0.3.0", @@ -1201,17 +1508,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "rand" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" -dependencies = [ - "chacha20", - "getrandom 0.4.3", - "rand_core 0.10.1", -] - [[package]] name = "rand_core" version = "0.6.4" @@ -1227,24 +1523,45 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "reallyme-codec" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc0e852dfef578e3b314e890bac9aee6da6f23122abcf5eb2da9185f9ad40711" +checksum = "fd3a3125c574e35d31a7abbdac1fb1c66788f54e84e8d757fb1d941a7cd46573" dependencies = [ "reallyme-codec-base64url", "reallyme-codec-cbor", "reallyme-codec-multibase", "reallyme-codec-multicodec", "reallyme-codec-multikey", + "thiserror", ] [[package]] name = "reallyme-codec-base64url" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c565a3783ab52fb27bf9c0c55a0dea1b8e1828f94d5874cce5d110774a6f259e" +checksum = "318534e19a178ea727b2e141fde87e892c3b338d4b8a52323b29bd3a5f50cb94" dependencies = [ "base64", "thiserror", @@ -1252,82 +1569,108 @@ dependencies = [ [[package]] name = "reallyme-codec-cbor" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29088f56a703311c3b984df9e71888cdecdf310440a7a3d63e5beaab48f77db1" +checksum = "0fe95bd99c7a82d1661e131c17bdb7b757cab05969d32452e971e0d6afeaa569" dependencies = [ "cid", "multihash", "multihash-codetable", "sha2", "thiserror", + "zeroize", ] [[package]] name = "reallyme-codec-jcs" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134d7a029ae431f762034d37c5c5ea3e34eb49417b8cf06de321d85ab619474f" +checksum = "be98f72844bae8c270002805b7e727782f3903a9b244b81bffbc154582422a92" dependencies = [ + "itoa", "ryu-js", + "serde", "serde_json", "thiserror", + "zeroize", ] [[package]] name = "reallyme-codec-multibase" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c866573200c88d9858a527c2764e7786494101dd9e91ffae0ac5043a098b992c" +checksum = "060dbf70afe2e22822c726dc38d4efb24654cca7c02b134624a11777cda966f8" dependencies = [ + "base64", "bs58", "reallyme-codec-base64url", "thiserror", + "zeroize", ] [[package]] name = "reallyme-codec-multicodec" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb0f85ffa7e2b2a44a8529169a9085e392dd9a71d747ed0c81e2c7f0f895160" +checksum = "378589a32bb420707728dc36b693e15aaf34a0baa48cf91f50ec8c15c6b1c6f9" [[package]] name = "reallyme-codec-multikey" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e955e390ddddd6e54ac4f79edee3c74e2122de2cba5fbf24daabd90626c3cd0" +checksum = "fdce86dc938b3de36f8fd6f7213dd4127cbeb47018263dbbfdeab78adb60165e" dependencies = [ "reallyme-codec-multibase", "reallyme-codec-multicodec", "thiserror", ] +[[package]] +name = "reallyme-codec-pem" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32bcc148863f3d8d9e347f858e1bbf8f9df6154bab85b46da7da850e747f3633" +dependencies = [ + "base64", + "thiserror", + "zeroize", +] + [[package]] name = "reallyme-cose" -version = "0.1.2" +version = "0.2.0" dependencies = [ + "allocation-counter", + "buffa", "ciborium", "coset", - "getrandom 0.2.17", + "criterion", "reallyme-codec", + "reallyme-cose-proto", "reallyme-crypto", "serde", "serde_json", "thiserror", + "wasm-bindgen-test", + "zeroize", +] + +[[package]] +name = "reallyme-cose-proto" +version = "0.2.0" +dependencies = [ + "buffa", + "serde", + "serde_json", "zeroize", ] [[package]] name = "reallyme-crypto" -version = "0.1.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3fd5abf6acbc465ed1dea322b4e984935b6ea5179882c1516d4df471845765" +checksum = "45d9a5ffa141792f779bd2c72a458493c9762b4405c851783b30bdfb0b02c50a" dependencies = [ - "reallyme-codec", - "reallyme-codec-base64url", - "reallyme-codec-multibase", - "reallyme-codec-multicodec", - "reallyme-codec-multikey", "reallyme-crypto-aes-kw", "reallyme-crypto-aes256-gcm", "reallyme-crypto-aes256-gcm-siv", @@ -1344,6 +1687,7 @@ dependencies = [ "reallyme-crypto-hpke", "reallyme-crypto-jwk", "reallyme-crypto-jwk-multikey", + "reallyme-crypto-kmac", "reallyme-crypto-ml-dsa-44", "reallyme-crypto-ml-dsa-65", "reallyme-crypto-ml-dsa-87", @@ -1364,13 +1708,15 @@ dependencies = [ "reallyme-crypto-slh-dsa", "reallyme-crypto-x-wing", "reallyme-crypto-x25519", -] + "thiserror", + "zeroize", +] [[package]] name = "reallyme-crypto-aes-kw" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928dcd3dddcd6a670bbf909f4c2844b68686d3be20b729036e59648b4ffc7bd4" +checksum = "794039d2d9307db507c80a4aebc14a9d63983dd4d1ab19fbdc5e251813f1af00" dependencies = [ "aes-kw", "reallyme-crypto-core", @@ -1379,23 +1725,21 @@ dependencies = [ [[package]] name = "reallyme-crypto-aes256-gcm" -version = "0.1.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac30c7be3e8b0ce2dc957f6fb1f0bb540e0767fa5e700838bc52e8c0778a7a1" +checksum = "a4773577e0b2a26043edf808d2b62196be88217bc52041e8f242d908982b71ad" dependencies = [ "aes 0.9.1", "aes-gcm", - "js-sys", "reallyme-crypto-core", - "wasm-bindgen", "zeroize", ] [[package]] name = "reallyme-crypto-aes256-gcm-siv" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb36dec471f98a8b0d06a7ec4583f86fbb0a25e745f887241b46368842a12ccb" +checksum = "a1173283e822a88ea578f1062db24629cc5defcade0450891a6fa8037a3d83dc" dependencies = [ "aes 0.9.1", "aes-gcm-siv", @@ -1407,9 +1751,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-argon2id" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c91ccc30c5935c957930e9e9cea83030d602d7faa510daf68f9a146f7cc595" +checksum = "7eb67943cea5496fd2a81115502ef48682f26a47eb4b0939b99c9801061c684d" dependencies = [ "argon2", "getrandom 0.2.17", @@ -1420,9 +1764,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-chacha20-poly1305" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d411ad6b2a300593a155506db3a90c64f5aa6aa88862dacd955ec770929ee48" +checksum = "397b467db25574881c0ec4513796387ff17948537e73542a8a9e58ce7ee6fedb" dependencies = [ "chacha20poly1305", "getrandom 0.4.3", @@ -1432,9 +1776,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-concat-kdf" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed1bb24de4db0e753f2f8fc204bf0832366a6f02b392a2df753e5a5cb5e6c5e" +checksum = "ba8f36144263a30c7049c10440ad4afc9216ce3ece7e008c162df3bfb95576ea" dependencies = [ "reallyme-crypto-core", "sha2", @@ -1443,9 +1787,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-constant-time" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cac2c9e26184225fc19bbffd1ba6cf95f2acf12c46e7ca65285e680c4f01a04" +checksum = "b7e0cfe324ddafb08c87644649afb2811eeefb3923038e61bab6616e2ff9b5de" dependencies = [ "reallyme-crypto-core", "subtle", @@ -1453,37 +1797,35 @@ dependencies = [ [[package]] name = "reallyme-crypto-core" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d5ef8109af8ad6268bd322c4f3adb15f6d05b209907ebf1b49fc2b11e7dd576" +checksum = "42c236066be2f755014b40169975d9bbaf60422a0f7a5b5aded35414e8f06d10" dependencies = [ "thiserror", ] [[package]] name = "reallyme-crypto-csprng" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e020f333ba4455be39accc598d94f5cd97d5e47650b9013605209849ca08f751" +checksum = "40be6cbcead368a4a06394fe9f2e1bcd8ac5ca14b21683b1d045f2caf45b1334" dependencies = [ "getrandom 0.4.3", "reallyme-crypto-core", + "secrecy", + "subtle", "zeroize", ] [[package]] name = "reallyme-crypto-dispatch" -version = "0.1.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0cfc9872b24807a41b07ec60171bf447a4ececee85c44ace79c4684a23895d5" +checksum = "78dae1ca708a04480036b33236157eb4f1bcbf12d904167dba7eb2308d4b776e" dependencies = [ "reallyme-codec-multikey", - "reallyme-crypto-aes256-gcm", - "reallyme-crypto-aes256-gcm-siv", - "reallyme-crypto-chacha20-poly1305", "reallyme-crypto-core", "reallyme-crypto-ed25519", - "reallyme-crypto-hmac", "reallyme-crypto-ml-dsa-44", "reallyme-crypto-ml-dsa-65", "reallyme-crypto-ml-dsa-87", @@ -1494,10 +1836,6 @@ dependencies = [ "reallyme-crypto-p384", "reallyme-crypto-p521", "reallyme-crypto-secp256k1", - "reallyme-crypto-sha2", - "reallyme-crypto-sha2-256", - "reallyme-crypto-sha3", - "reallyme-crypto-sha3-256", "reallyme-crypto-x-wing", "reallyme-crypto-x25519", "thiserror", @@ -1506,23 +1844,21 @@ dependencies = [ [[package]] name = "reallyme-crypto-ed25519" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d883ff8b61c068473bb5d55b804acef208e6d192b49ef4254698e62e1691c23" +checksum = "95077fb769929eba118ede31dce55c6cd934853c9dde6c27f4cc49b1e2139819" dependencies = [ "ed25519-dalek", - "js-sys", - "rand", "reallyme-crypto-core", - "wasm-bindgen", + "reallyme-crypto-csprng", "zeroize", ] [[package]] name = "reallyme-crypto-hkdf" -version = "0.1.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13609c03b4b4903a6e3136e9b038e8e045afef7b4dbdaac85c409a593b44141" +checksum = "7d54ecb927a9a43e27daa67beeffa8701b932cdcbe841a7ab9ef37d7a39ddef5" dependencies = [ "getrandom 0.4.3", "hkdf", @@ -1533,33 +1869,43 @@ dependencies = [ [[package]] name = "reallyme-crypto-hmac" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da57df3ddafc6f4f5d0c5639d444d85afaaf23799d829bf4fcf4d6a87929b5e" +checksum = "a0aaf6d39535bc72eee2e8a62496ba290bb2744a1183758ea54d90cfd5aae45d" dependencies = [ "hmac", "reallyme-crypto-core", "sha2", + "subtle", "zeroize", ] [[package]] name = "reallyme-crypto-hpke" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ff2171ffe2d0dbdc68cdfea36c19bb492a59a26f18ea40e8a02ab7e7e258b03" +checksum = "dccae392c132845e0981901573a45f488986c7d64a67f639dca2e76a99dc16ab" dependencies = [ "getrandom 0.4.3", + "hkdf", + "hmac", "hpke", + "k256", + "ml-kem", + "reallyme-crypto-secp256k1", + "reallyme-crypto-x448", + "sha2", + "shake", + "subtle", "thiserror", "zeroize", ] [[package]] name = "reallyme-crypto-jwk" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "070552a8399ab0b99f94983e3114a6915e99c8ea428ca055e1f1e3a3fa6bea4c" +checksum = "9e7d1a5142875750d976d85ca15a991277f71872fdfb745b93fec4ca05a2c70e" dependencies = [ "reallyme-codec-base64url", "reallyme-codec-jcs", @@ -1572,116 +1918,119 @@ dependencies = [ [[package]] name = "reallyme-crypto-jwk-multikey" -version = "0.1.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "370c45001edac77c48d1c1e461f65351d6e2f77da7e012b8efef6a3d6d8afa8a" +checksum = "729352adf82fb545895c1e470c0e0097a3732deaa5521efe1d69fa7840a31b6c" dependencies = [ - "reallyme-codec-base64url", "reallyme-codec-multikey", "reallyme-crypto-jwk", - "reallyme-crypto-p256", - "reallyme-crypto-secp256k1", "thiserror", ] +[[package]] +name = "reallyme-crypto-kmac" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de63e0cddc6584af726031d7bf388b3e3f25b289cdf735a58bbfc200293ac19f" +dependencies = [ + "reallyme-crypto-core", + "sha3 0.10.9", + "sha3-kmac", + "zeroize", +] + [[package]] name = "reallyme-crypto-ml-dsa-44" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4b38079a6c2792f4ff47970c424b737d1da6e12ea44250b275d35d7212c473" +checksum = "b8d7fc3b252f9a64d11aaa0de176da4083bd49ae9d184685f2a0f8e036ae44a0" dependencies = [ - "js-sys", + "getrandom 0.4.3", "ml-dsa", "reallyme-crypto-core", - "wasm-bindgen", "zeroize", ] [[package]] name = "reallyme-crypto-ml-dsa-65" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bacb74964487ee8b3827a16b9addc015f77ddcfc668f0503590eff495facca3d" +checksum = "8c93d1edf2cb86ad0788a606a4b627abe8e464b57c087386b337c897e33567ec" dependencies = [ - "js-sys", + "getrandom 0.4.3", "ml-dsa", "reallyme-crypto-core", - "wasm-bindgen", "zeroize", ] [[package]] name = "reallyme-crypto-ml-dsa-87" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02abac85c5ac62191cad0047eb3472ebcf1c64bb57b26341aa8a490ab344a25a" +checksum = "d7ad5379ce0187227d9b217b2daa17ddf08ae68e8005fbaa7f1ff09fdfb3184a" dependencies = [ - "js-sys", + "getrandom 0.4.3", "ml-dsa", "reallyme-crypto-core", - "wasm-bindgen", "zeroize", ] [[package]] name = "reallyme-crypto-ml-kem-1024" -version = "0.1.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9ff387f359bae250b8648f6bcde1178e5393190ca8ae9e85c8dfc8341f6b2a8" +checksum = "0ca3a794d0b03989f2580741be6a1f3335b0924a9a7eeb5b0bdf88bb90f099f5" dependencies = [ - "js-sys", + "getrandom 0.4.3", "ml-kem", "reallyme-crypto-core", - "wasm-bindgen", "zeroize", ] [[package]] name = "reallyme-crypto-ml-kem-512" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a056e9f2c9cda6d545f43659bcc3b2b3f436615615d2ecbfe25a275a69b8f53" +checksum = "96bbf7b6c6bd64aeae0e8d089de5379389d36d7b8a39117cd872565f85b5c01f" dependencies = [ - "js-sys", + "getrandom 0.4.3", "ml-kem", "reallyme-crypto-core", - "wasm-bindgen", "zeroize", ] [[package]] name = "reallyme-crypto-ml-kem-768" -version = "0.1.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8621d4867f75f3ccd4357a9e925d24e2bce08a3c839d816b4440dd53c20464f" +checksum = "ac3904cc39544ec8338088aad25a3f29f0b4a8fb1e8ef3b52258f2d556600349" dependencies = [ - "js-sys", + "getrandom 0.4.3", "ml-kem", "reallyme-crypto-core", - "wasm-bindgen", "zeroize", ] [[package]] name = "reallyme-crypto-p256" -version = "0.1.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c21cf8a253d4e2020863daf9be79f38da21b2e58ceb7a5fe0639a5bea019ae50" +checksum = "bca665ee00d699117c6d8391b0c7ce31a8012cecf383e7a6f01f5b44f7bd8b7d" dependencies = [ "ecdsa", - "js-sys", + "getrandom 0.4.3", "p256", + "reallyme-codec-pem", "reallyme-crypto-core", "sha2", - "wasm-bindgen", "zeroize", ] [[package]] name = "reallyme-crypto-p384" -version = "0.1.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbd69fca5d6ae4ab79d534d53ae41b067f140c0f77a4aca972ef9cc29c1bba12" +checksum = "99b6593952b57c2716db21938d91caf10caf568dde51172a87405ed88af8b214" dependencies = [ "ecdsa", "getrandom 0.4.3", @@ -1693,9 +2042,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-p521" -version = "0.1.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82485fa6dce4ad90814b44a6cee1a20c1e2bb7de508b501e5625f1ed4b229927" +checksum = "e8f11378a9ae7b9c531028c14c8442a7c67969ff3601dfde2f3f35ce43f5b723" dependencies = [ "ecdsa", "getrandom 0.4.3", @@ -1707,9 +2056,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-pbkdf2" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5301281d36317efea00938252b2b204d9eb0bef83f9ff92bd55577f16e7f6a" +checksum = "d67cebf1c2301170a70124f89fb0d60b98da3d27697aad020164b9e2700a1f1f" dependencies = [ "pbkdf2", "reallyme-crypto-core", @@ -1719,9 +2068,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-rsa" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891830f7e286097f07e0c972e47f96e883228d072dc1e2ee082c643e77d668df" +checksum = "d9fb73e3090d6c3e1382d85369f4b5deaf3f76741fd76ea4b90cb41b7075ea48" dependencies = [ "const-oid", "crypto-bigint", @@ -1730,28 +2079,28 @@ dependencies = [ "sha2", "spki", "subtle", + "zeroize", ] [[package]] name = "reallyme-crypto-secp256k1" -version = "0.1.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a041131148618cbab8ea7f109640e1d0ea509c7ec7961130b2c86851f43719a7" +checksum = "2307cc673c30ed5e427d25d0d9f7a9e7059451c42e8ee4c4b7931f3a8209c196" dependencies = [ "ecdsa", - "js-sys", + "getrandom 0.4.3", "k256", "reallyme-crypto-core", "sha2", - "wasm-bindgen", "zeroize", ] [[package]] name = "reallyme-crypto-sha2" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd040b02e8af8fa810bcb2f35925c9668b2926900c90692a12724fbf1722d76" +checksum = "d117b4a909bfa43fe547139dbfffd0ef268fac2ba01c3d4406b32d3251fa5bad" dependencies = [ "sha2", "zeroize", @@ -1759,9 +2108,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-sha2-256" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147ffd55087e2a5d0800ab27617b60b438a177797dc651bcdc42b7972c57b466" +checksum = "792c68d08ffadc5d3ab40a907cd6ff5e20afe009ee48861e060b191fbce12aa4" dependencies = [ "getrandom 0.4.3", "sha2", @@ -1770,9 +2119,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-sha3" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab06737e2d3034877dedd7474d07bedae04c1ce7db5581947ef45cdb5e973561" +checksum = "9ac37c76780e55a44cab08859137fb66b2887706d80c0f265539880b87d1baba" dependencies = [ "sha3 0.12.0", "shake", @@ -1781,9 +2130,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-sha3-256" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3250f76ab4bc293f3955343c7cc9d0bcb4a2632cf47316b019800cfc567f9345" +checksum = "aea609279b35ce35662cd19e90b08e8034ab8afb00f972410f12317638b0fa7a" dependencies = [ "getrandom 0.4.3", "sha3 0.12.0", @@ -1792,9 +2141,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-signer" -version = "0.1.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa77ebd4d7d7eea73141a6553309192378b093b4205993cdcd575759d55cd3b" +checksum = "b614ca54b7ca240ece7d5ffbf37671295940c6cf0f18675cedbba7bfb4eadf75" dependencies = [ "reallyme-crypto-core", "reallyme-crypto-dispatch", @@ -1805,23 +2154,21 @@ dependencies = [ [[package]] name = "reallyme-crypto-slh-dsa" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27774407e958ba6694d418c36ef091faf92d7d7dcc211ad75968011a0f8161c5" +checksum = "16775d5196c265f82c8e718fe9f8669e4d2f4013b3bbd40770d096e4afc92e67" dependencies = [ - "js-sys", - "rand", "reallyme-crypto-core", + "reallyme-crypto-csprng", "slh-dsa", - "wasm-bindgen", "zeroize", ] [[package]] name = "reallyme-crypto-x-wing" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c103b9e9c0d810be2138c64d92b9786bb625efa89ffcf8716bdaf8832b38336a" +checksum = "c517f135ea508527124290830409229d3882723a4b4d67f9124178f6385a6f02" dependencies = [ "getrandom 0.4.3", "ml-kem", @@ -1834,17 +2181,57 @@ dependencies = [ [[package]] name = "reallyme-crypto-x25519" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd9c65919f60ff1f394c20238c81fc982ec6761fbab018b0858a890c6b5437b" +checksum = "0deb382108befcdbd8dfcf806d304126b6627c699479e8accff8d73c8da60494" dependencies = [ - "js-sys", + "getrandom 0.4.3", "reallyme-crypto-core", - "wasm-bindgen", "x25519-dalek", "zeroize", ] +[[package]] +name = "reallyme-crypto-x448" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "592808e870dedc4c352c8d28cf7d6b433c93b524c4dbbb071ad3ceac289dd21c" +dependencies = [ + "getrandom 0.4.3", + "reallyme-crypto-core", + "x448", + "zeroize", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + [[package]] name = "rfc6979" version = "0.6.0" @@ -1885,6 +2272,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04d056b875a9d2e6cb9a61d127afee9ac5999b9f87bcb32079d1318e505be714" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "sec1" version = "0.8.1" @@ -1941,7 +2337,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1989,6 +2385,17 @@ dependencies = [ "digest 0.11.3", ] +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak 0.1.6", + "zeroize", +] + [[package]] name = "sha3" version = "0.11.0" @@ -1996,7 +2403,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" dependencies = [ "digest 0.11.3", - "keccak", + "keccak 0.2.0", ] [[package]] @@ -2006,10 +2413,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" dependencies = [ "digest 0.11.3", - "keccak", + "keccak 0.2.0", "sponge-cursor", ] +[[package]] +name = "sha3-kmac" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b6b86fb906e40929519c1a38dc349a7f5595778cc00cc20b55eec34fddeb9ec" +dependencies = [ + "generic-array 1.4.4", + "sha3 0.10.9", + "sha3-utils", +] + +[[package]] +name = "sha3-utils" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08e0bf98cc082cbe077f06a707c94ca15796d046cc4cd2593167b5730a6727be" +dependencies = [ + "zerocopy", +] + [[package]] name = "shake" version = "0.1.0" @@ -2017,7 +2444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" dependencies = [ "digest 0.11.3", - "keccak", + "keccak 0.2.0", "sponge-cursor", ] @@ -2037,6 +2464,12 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.12" @@ -2063,6 +2496,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "smoothutf8" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4ec95892483d6d94284caccfddb9b77111a4dcac33ed25d624248def0fa5f1" +dependencies = [ + "simdutf8", +] + [[package]] name = "spki" version = "0.8.0" @@ -2090,9 +2532,20 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -2107,7 +2560,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2127,7 +2580,17 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", ] [[package]] @@ -2156,9 +2619,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.12+spec-1.1.0" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap", "toml_datetime", @@ -2175,6 +2638,17 @@ dependencies = [ "winnow", ] +[[package]] +name = "turboshake" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f892c6904b0bd5a9241eac848347abcbbbd2b9f3892bad23ac3e6efab8d6f06a" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", + "sponge-cursor", +] + [[package]] name = "typenum" version = "1.20.1" @@ -2219,6 +2693,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2238,6 +2722,16 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.126" @@ -2257,7 +2751,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] @@ -2270,11 +2764,106 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-bindgen-test" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0d555ca874445df8d314f94f5c948a4e74e5418f332c89f660a3d8310a96f4" +dependencies = [ + "async-trait", + "cast", + "js-sys", + "libm", + "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94eb68555b95bcea5e8cf4abe280b529049479fa995bfc23734af96a6aedc120" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31d56021e873866c968588ed85ccdf56db5c426e44afdb4618c39895104b920" + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] @@ -2290,6 +2879,19 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "x-wing" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b51507b887016c3925c84591108dadb8c099f776eb04fec6a60ae3519fee856f" +dependencies = [ + "kem", + "ml-kem", + "sha3 0.12.0", + "shake", + "x25519-dalek", +] + [[package]] name = "x25519-dalek" version = "3.0.0" @@ -2302,6 +2904,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "x448" +version = "0.14.0-pre.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c166d06b9fa4328c890d46bda797269fb6aeca96393aeaad0e74b4b11550e06" +dependencies = [ + "ed448-goldilocks", + "zeroize", +] + [[package]] name = "zerocopy" version = "0.8.54" @@ -2319,7 +2931,7 @@ checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2339,11 +2951,11 @@ checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "zmij" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd2f034a4bebf216c9e4b7083603e024cf930873fd67830cfb083c9fa33129d9" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 4804cb7..cf66af0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,69 +2,47 @@ # # SPDX-License-Identifier: Apache-2.0 -[package] -name = "reallyme-cose" -version = "0.1.2" -description = "Audit-facing COSE helpers for ReallyMe identity software." +[workspace] +resolver = "2" +default-members = ["crates/cose"] +members = ["crates/cose", "crates/proto"] +exclude = ["fuzz", "tools/vector-audit", "tools/vector-goldens"] + +[workspace.package] edition = "2021" rust-version = "1.96" license = "Apache-2.0" repository = "https://github.com/reallyme/cose" -keywords = ["cose", "cbor", "signing", "identity", "credentials"] -categories = ["cryptography", "encoding"] -publish = true -include = [ - "/src/**/*.rs", - "/tests/**/*.rs", - "/conformance/**/*.json", - "/proto/**/*.proto", - "/buf.yaml", - "/Cargo.toml", - "/README.md", - "/LICENSE", - "/NOTICE", -] - -[features] -default = ["native"] -native = ["cose-crypto", "reallyme-crypto/native"] -wasm = ["cose-crypto", "reallyme-crypto/wasm"] -cose-crypto = [ - "reallyme-crypto/dispatch", - "reallyme-crypto/ed25519", - "reallyme-crypto/p256", - "reallyme-crypto/p384", - "reallyme-crypto/p521", - "reallyme-crypto/secp256k1", - "reallyme-crypto/sha2", - "reallyme-crypto/x25519", -] +publish = false -[dependencies] +# One reviewed version requirement governs each external dependency across the +# workspace. Crate-local manifests retain only feature and optionality policy. +[workspace.dependencies] +allocation-counter = "0.8.1" +buffa = { version = "0.9.0", features = ["json"] } ciborium = "0.2" -coset = "0.4.0" -reallyme-codec = { version = "0.1.1", default-features = false, features = ["cbor", "multikey"] } -reallyme-crypto = { version = "0.1.6", default-features = false } -thiserror = "2.0" -zeroize = "1.8" - -[dev-dependencies] +coset = "0.4.2" +criterion = "0.8.2" +reallyme-codec = { version = "0.2.0", default-features = false, features = ["base64url", "cbor", "multikey"] } +reallyme-cose-proto = { version = "0.2.0", path = "crates/proto", default-features = false } +reallyme-crypto = { version = "0.3.0", default-features = false } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" - -[target.'cfg(target_arch = "wasm32")'.dependencies] -getrandom02 = { package = "getrandom", version = "0.2.17", features = ["js"] } +thiserror = "2.0" +wasm-bindgen-test = "0.3.76" +zeroize = "1.8" [profile.release] lto = true codegen-units = 1 panic = "abort" +overflow-checks = true -[lints.rust] +[workspace.lints.rust] unsafe_code = "deny" missing_docs = "warn" -[lints.clippy] +[workspace.lints.clippy] dbg_macro = "deny" expect_used = "deny" large_include_file = "deny" @@ -76,3 +54,4 @@ unimplemented = "deny" unreachable = "deny" unwrap_used = "deny" wildcard_imports = "deny" +missing_errors_doc = "deny" diff --git a/README.md b/README.md index 860eb3b..4b41fc5 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,34 @@ With `default-features = false`, the crate still builds the COSE_Key, Multikey, policy, algorithm-mapping, and CBOR limit helpers, but does not export crypto-backed signing or verification APIs. +Enable the `wire` feature only for protobuf operation adapters: + +```toml +reallyme-cose = { version = "0.2.0", features = ["wire"] } +``` + +When default features are disabled, pair `wire` with an explicit runtime lane, +for example `features = ["native", "wire"]` for native Rust or +`features = ["wasm", "wire"]` for `wasm32-unknown-unknown`. +The `wire` feature is intentionally non-additive: enabling `wire` alone fails +closed because operation adapters must never compile without a selected runtime. + +## Workspace Layout + +The repository follows the same workspace conventions as ReallyMe Crypto and +ReallyMe Codec: + +```text +crates/ +├── cose/ public COSE facade, semantic operations, tests, and benchmarks +└── proto/ protobuf schema and generated Buffa boundary types +docs/ versioned scope and performance records +fuzz/ cargo-fuzz targets and corpora +scripts/ generation, policy, and release-readiness automation +tools/ independent vector audit and golden-vector generation +vectors/ portable classical and post-quantum conformance vectors +``` + ## Standards Profile This crate implements a focused COSE profile over: @@ -39,6 +67,8 @@ This crate implements a focused COSE profile over: COSE_Key structure, key identifiers, and CBOR encoding restrictions. - [RFC 9053](https://www.rfc-editor.org/rfc/rfc9053.html), for initial COSE algorithm identifiers, key types, curve identifiers, and key-parameter labels. +- [RFC 9964](https://www.rfc-editor.org/rfc/rfc9964.html), for ML-DSA algorithm + registrations and AKP COSE_Key encoding. `reallyme-cose` handles COSE_Key construction and parsing, supported alg/kty/crv mapping, key identifier policy, deterministic CBOR boundary checks, @@ -46,6 +76,13 @@ and the structural COSE_Sign1 parsing needed by mdoc, attestation, credential, and wallet code. Signing, verification, hashing, and key generation remain in `reallyme-crypto`. +COSE_Key bytes used for deterministic `kid` derivation follow RFC 8949 core +deterministic map ordering. Verification intentionally also accepts other map +orderings permitted by RFC 9052: the received protected-header byte string is +authenticated exactly as received and is never decoded and re-encoded before +signature or AEAD verification. Duplicate labels and other profile violations +still fail closed. + ## Quick Start ```rust @@ -53,22 +90,21 @@ use reallyme_cose::{cose_sign1, cose_verify1_with_policy, Algorithm, CoseError, use reallyme_crypto::dispatch::generate_keypair; fn sign_and_verify() -> Result<(), CoseError> { - let (public_key, private_key) = generate_keypair(Algorithm::Ed25519)?; + let (public_key, private_key) = generate_keypair(Algorithm::Ed25519) + .map_err(|_| CoseError::Crypto)?; let kid = b"example-key"; let cose_bytes = cose_sign1(Algorithm::Ed25519, b"payload", &private_key, Some(kid))?; - let policy = CosePolicy { - require_kid: true, - allowed_algs: vec![Algorithm::Ed25519], - ..Default::default() - }; - - let verified = cose_verify1_with_policy(&cose_bytes, &policy, |requested_kid| { - (requested_kid == kid).then(|| public_key.clone()) + let policy = CosePolicy::new() + .with_require_kid(true) + .allow_algorithm(Algorithm::Ed25519); + + let verified = cose_verify1_with_policy(&cose_bytes, &policy, |algorithm, requested_kid| { + (algorithm == Algorithm::Ed25519 && requested_kid == kid).then(|| public_key.clone()) })?; - assert_eq!(verified.payload, b"payload"); + assert_eq!(verified.payload.as_slice(), b"payload"); assert_eq!(verified.alg, Algorithm::Ed25519); - assert_eq!(verified.kid, kid); + assert_eq!(verified.kid.as_slice(), kid); Ok(()) } ``` @@ -89,10 +125,7 @@ let cose = cose_sign1_with_options( b"payload", &private_key, Some(b"example-key"), - CoseSign1EncodeOptions { - tag: true, - max_cose_sign1_bytes: 512 * 1024, - }, + CoseSign1EncodeOptions::tagged().with_max_cose_sign1_bytes(512 * 1024), )?; ``` @@ -104,25 +137,57 @@ and return verified metadata: ```rust use reallyme_cose::{cose_verify1_with_policy, Algorithm, CosePolicy}; -let policy = CosePolicy { - require_kid: true, - allowed_algs: vec![Algorithm::P256], - max_cose_sign1_bytes: 512 * 1024, - ..Default::default() -}; +let policy = CosePolicy::new() + .with_require_kid(true) + .allow_algorithm(Algorithm::P256) + .with_max_cose_sign1_bytes(512 * 1024); -let verified = cose_verify1_with_policy(&cose, &policy, |kid| resolve_public_key(kid))?; +let verified = cose_verify1_with_policy(&cose, &policy, |algorithm, kid| { + resolve_public_key(algorithm, kid) +})?; assert_eq!(verified.alg, Algorithm::P256); ``` +### Non-exportable signing keys + +Applications that keep private keys in a secure enclave, keystore, HSM, or +another provider-owned boundary implement `CoseSigner` and call +`cose_sign1_with_signer` or `cose_sign1_detached_with_signer`. The provider +receives the exact, bounded COSE `Sig_structure`; private key bytes never enter +the COSE API. Provider failures use the fixed `CoseSignerError` enum, and the +COSE layer validates and normalizes the returned signature before encoding it. + +The provider declares one algorithm through `CoseSigner::algorithm`. ECDSA +providers return their native DER signature; providers for Ed25519 and ML-DSA +return fixed-width signature bytes. Providers must return signatures in a +`Zeroizing>` owner and must not include key handles, platform exception +text, or user data in errors. + +### Sensitive result ownership + +Native APIs retain wipe-on-drop ownership at the public boundary: +`derive_kid_from_cose_key_public` returns `Zeroizing>`, and +`cose_key_to_multikey` returns `Zeroizing`. Keep those owners intact +instead of converting them into ordinary `Vec` or `String` values. `CoseKey` +is also a wipe-on-drop owner and is marked `must_use`. + +ML-KEM request values are non-exhaustive and constructed with +`CoseMlKemEncryptRequest::new` or `CoseMlKemDecryptRequest::new`; their fields +are deliberately not public struct-literal surface. Use +`with_supp_priv_info` when replacing the optional private KDF context. + ## Supported Features - COSE_Sign1 with attached payloads. - COSE_Sign1 with detached payloads. +- Explicit external AAD for attached and detached COSE_Sign1 signing and + verification. Verification fails authentication when the supplied AAD does + not exactly match the bytes used for signing. - COSE_Sign1 signing can emit either untagged messages or messages carrying the registered COSE_Sign1 root tag (18) through `cose_sign1_tagged`, `cose_sign1_detached_tagged`, or `CoseSign1EncodeOptions`. -- Ed25519, P-256, P-384, P-521, and secp256k1 signing. +- Ed25519, P-256, P-384, P-521, secp256k1, ML-DSA-44, ML-DSA-65, and + ML-DSA-87 signing using their current IANA COSE registrations. - ECDSA signatures use the fixed-width `r || s` encoding required by RFC 9053; DER-encoded ECDSA signatures are rejected. - Verification accepts untagged COSE_Sign1 input and input carrying the @@ -136,38 +201,218 @@ assert_eq!(verified.alg, Algorithm::P256); - Verification binds the protected header bytes exactly as received, per RFC 9052 §4.4. - COSE_Key public/private construction and extraction for supported signing keys. -- Private COSE_Key extraction returns a zeroizing buffer. Because - `coset::CoseKey` stores private parameters in ordinary CBOR value buffers, - keep private COSE_Key values short-lived and extract them only at backend - boundaries. + Private construction requires the corresponding public key and validates the + pair before the key can enter the typed API. +- One-recipient `COSE_Encrypt` and `COSE_Recipient` processing for + ML-KEM-512, ML-KEM-768, and ML-KEM-1024, with direct KMAC256 derivation or + the fixed ML-KEM-512+A128KW, ML-KEM-768+A192KW, and + ML-KEM-1024+A256KW key-wrap profiles. +- AES-128-GCM, AES-192-GCM, and AES-256-GCM protected content algorithms for + every supported ML-KEM recipient profile. +- Guarded COSE_Key, COSE_Sign1, and COSE_Encrypt parsers retain the decoded + CBOR tree under a recursive wipe owner until every semantic field has moved + into its profile-specific wipe-on-drop owner. Rejected parses therefore wipe + partially constructed payload, ciphertext, identifier, header, and key + buffers. The public `CoseKey` owner also wipes private parameters, + identifiers, base-IV material, and rejected text/byte extension owners. + Private-key extraction and verified payloads use zeroizing return buffers. - COSE_Key public conversion for X25519 key-agreement keys. -- COSE_Key to Multikey and Multikey to COSE_Key conversion. -- `kid = SHA-256(canonical public-only COSE_Key)` derivation. -- Portable vectors in `conformance/vectors/cose-sign1.json` and - `conformance/vectors/cose-key.json`. ECDSA vectors are the cross-lane - contract for fixed-width `r || s` signatures; Swift, Kotlin, TypeScript, and - other SDK lanes must reject DER signatures at the COSE boundary. - -## Independent Vector Audit +- COSE_Key to Multikey and Multikey to COSE_Key conversion, including the + ReallyMe ML-KEM-512/768/1024 AKP profiles. ML-KEM uses the registered draft + Multicodec names `mlkem-512-pub`, `mlkem-768-pub`, and `mlkem-1024-pub` while + retaining ReallyMe private-use COSE algorithm identifiers until IANA assigns + final values. +- `kid = SHA-256(canonical algorithm-bound public-only COSE_Key)` derivation. +- Portable classical and PQ vectors in `vectors/cose-sign1*.json` + and `vectors/cose-key*.json`. The PQ suites cover ML-DSA-44/65/87 + Sign1 plus ML-DSA and ML-KEM AKP COSE_Key/Multikey round trips. ECDSA vectors + remain the cross-lane contract for fixed-width `r || s` signatures. Any + Swift, Kotlin, TypeScript, and other SDK implementations must reject DER signatures + at the COSE boundary. + +## Protobuf Wire Boundary + +The native Rust API remains the primary SDK surface. For service, FFI, generated +SDK, and conformance boundaries, `reallyme-cose` also exposes a generated +protobuf operation lane: request bytes in, result-envelope bytes out, with +structured `CoseError` protobuf bytes on failure. + +The checked-in schema lives at +`crates/proto/proto/reallyme/cose/v1/cose.proto`. Generated Buffa Rust +types live in the low-level `reallyme-cose-proto` workspace crate, under +`crates/proto`; that crate is published only so the optional `wire` feature +can remain publishable while generated code and its lint posture stay out of the +handwritten native crate. After editing the schema, run `buf generate` and keep the +generated files checked in. COSE does not currently publish Swift, Kotlin, or +TypeScript packages from this repository. The schema's Swift metadata allows +generated Swift protobuf consumers to use the same wire contract independently. + +The wire lane has two layers: + +- `reallyme-cose-proto` owns generated request, result, and error message + types. It is not the normal SDK surface. Its generated Rust is hardened after + `buf generate` so request, result, key, payload, AAD, and `kid` byte fields + are redacted from `Debug`, zeroized by `clear`, and zeroized on drop. Buffa + requires generated messages to remain `Clone`; each clone is therefore an + explicit transient owner with the same wipe-on-drop behavior. +- `reallyme-cose::wire` owns executable adapters behind the opt-in `wire` + feature. Use `wire::execute_operation_proto` for protobuf request bytes or + `wire::execute_operation_proto_json` for generated ProtoJSON requests. Both return a + binary `CoseOperationResponseV2`, whose outcome is either a structured error + or an exact operation-specific result variant. Rust-side adapters should use + `wire::decode_operation_response_for_request`; it rejects a result variant + that does not match the submitted request. The earlier status-plus-opaque- + payload compatibility envelope has been removed. Operation-specific dispatch + remains private so the executable boundary has one request and one response + model. + +Executable response bytes are stored in zeroizing owners by the Rust wire +adapter. Generated ProtoJSON uses protobuf enum names +and standard protobuf base64 bytes. Serialization returns a caller-owned JSON +`String` that the library cannot wipe; callers handling private keys, payloads, +AAD, or identifiers should place it in a zeroizing owner and release it as soon +as the transport write completes. Likewise, generated `PartialEq` is a schema +convenience and is not a constant-time secret comparison primitive. + +ProtoJSON is provided only by the generated Buffa protobuf types. The native +Rust SDK does not define a second ad hoc JSON representation for COSE requests +or results. `buf.gen.yaml` enables both borrowed Buffa views and strict +generated ProtoJSON. The post-generation hardening pass redacts sensitive byte +fields in both owned-message and borrowed-view `Debug` implementations; owned +messages additionally zeroize sensitive buffers on `clear` and drop. Borrowed +views cannot erase caller-owned input and therefore do not claim zeroization. +The executable binary protobuf decoder rejects unknown fields, matching strict +ProtoJSON semantics and preventing opaque length-delimited values from being +retained in generated unknown-field storage. Direct users of sensitive +generated messages also receive recursive unknown-field wiping on `clear` and +drop. + +## ReallyMe ML-KEM COSE Profile + +The current IANA COSE algorithm registry does not yet assign ML-KEM recipient +algorithm identifiers. To provide interoperable COSE objects now without +occupying or guessing future standards-track values, ReallyMe emits stable +private-use identifiers below `-65536`: + +| COSE algorithm | ReallyMe identifier | +| --- | ---: | +| ML-KEM-512 direct | `-65537` | +| ML-KEM-768 direct | `-65538` | +| ML-KEM-1024 direct | `-65539` | +| ML-KEM-512+A128KW | `-65540` | +| ML-KEM-768+A192KW | `-65541` | +| ML-KEM-1024+A256KW | `-65542` | +| `ek` recipient header label | `-65543` | + +These are COSE integer identifiers and are deliberately independent from the +protobuf enum numbers. The content algorithm is authenticated in the +`COSE_Encrypt` protected header. The exact direct or key-wrap ML-KEM profile and +the mandatory recipient `kid` are authenticated in the `COSE_Recipient` +protected header. The `ek` header carries the ML-KEM ciphertext in the +recipient unprotected map because its integrity is established by successful +decapsulation, KDF binding, and content or key-wrap authentication. + +For DID and Multikey integration, public ML-KEM AKP keys use the draft +Multicodec names `mlkem-512-pub`, `mlkem-768-pub`, and `mlkem-1024-pub`. +Decoding those Multikeys binds them to the corresponding ReallyMe private-use +COSE algorithm identifier; it never guesses a future IANA value. When final +IANA identifiers are assigned, transitional decoding must accept the private +and final identifiers explicitly while encoding policy selects one deliberately. + +The profile follows the current COSE ML-KEM draft construction: + +- ML-KEM uses AKP COSE keys. Private import/export uses the 64-octet FIPS 203 + seed `d || z`, and private/public construction cryptographically validates + the supplied pair. +- The mandatory recipient `kid` is exactly + `SHA-256(canonical public-only algorithm-bound COSE_Key)`. Encryption rejects + a caller-supplied identifier that does not identify the recipient public key. + Decryption deterministically derives the public key from the private + `d || z` seed and rejects a protected `kid` bound to any other key before + decapsulation. Arbitrary routing labels are not valid recipient identifiers + in this profile. +- KMAC256 derives from the ML-KEM shared secret. Its message is the CBOR KDF + context `[AlgorithmID, SuppPubInfo, ?SuppPrivInfo]`, its customization string + is empty, and `SuppPubInfo` binds the output length and exact encoded + recipient protected header. +- Direct mode derives the AES-GCM content-encryption key and encodes a null + recipient ciphertext. +- Key-wrap mode derives only the fixed AES-KW key-encryption key for the KEM + strength, wraps a fresh random content-encryption key, and carries that + wrapped key as recipient ciphertext. +- The ML-KEM parameter set and AES-GCM content cipher are independent protocol + choices. Mixed combinations remain interoperable, but their combined + security is limited by the weaker component. Applications seeking aligned + strength should pair ML-KEM-512 with AES-128-GCM, ML-KEM-768 with + AES-192-GCM, and ML-KEM-1024 with AES-256-GCM. +- External AAD and optional `SuppPrivInfo` must match exactly at decryption. + AES-KW integrity failures remain distinct from content authentication + failures. + +This initial profile intentionally supports exactly one recipient. Decoding +accepts tagged or untagged `COSE_Encrypt`; encoding emits the registered +`COSE_Encrypt` tag. AKP keys use the base direct identifier in their `alg` +parameter because the same key may be selected by either recipient mode; the +recipient protected `alg` remains the authoritative operation profile. + +When IANA assigns final identifiers, ReallyMe will add explicit transitional +decoding for the private-use and final identifiers. Existing private-use +objects will not be silently reinterpreted, and emitted identifiers will change +only in a documented profile/version transition. + +## COSE-Layer Vector Audit `tools/vector-audit` is a standalone, unpublished Cargo binary that audits the committed vector JSON with RustCrypto, `ciborium`, and `bs58`. It does not depend on `reallyme-cose`, `reallyme-crypto`, or `reallyme-codec`, which keeps -the vector checks independent from the implementation under test. +the CBOR, COSE, KDF, key-wrap, and Multikey checks independent from production. +It uses the same direct RustCrypto primitive families as production, so +primitive ACVP assurance remains the responsibility of the pinned +`reallyme-crypto` dependency. In addition to COSE_Sign1 and COSE_Key fixtures, +the tool independently parses the ReallyMe +ML-KEM `COSE_Encrypt` maps, repeats deterministic encapsulation and +decapsulation, reconstructs the KMAC256 context, performs RFC 3394 AES-KW +unwrap where applicable, and authenticates the AES-GCM ciphertext. It also +derives every PQ public key directly from its seed, verifies ML-DSA Sign1 +signatures through `ml-dsa`, and checks the AKP parameters and Multicodec prefix +without linking the production COSE, Crypto, or Codec crates. + +The classical Sign1 suite also contains a fixed Ed25519 signature produced by +Node's OpenSSL-backed implementation over RFC 8032 key material. Suite counts +and exact SHA-256 digests are bound in `vectors/manifest.json`. + +`tools/vector-goldens` is the separate maintenance generator for deterministic +ReallyMe fixtures. Its fixed ML-KEM seeds, encapsulation randomness, CEKs, and +nonces are test inputs only; the production encryption API continues to use +operating-system entropy. Regenerated bytes must pass the independent audit +before they are accepted. + +```sh +cargo run --manifest-path tools/vector-goldens/Cargo.toml +cargo run --manifest-path tools/vector-audit/Cargo.toml --bin reallyme-cose-vector-audit -- . +``` ## Unsupported COSE Surface The following structures and features are not implemented: - COSE_Mac0, COSE_Mac, and MAC verification. -- COSE_Encrypt0, COSE_Encrypt, and recipient processing. +- COSE_Encrypt0. +- Multi-recipient COSE_Encrypt. - COSE_Sign multi-signer structures. - Countersignatures. - Critical protected headers. +- COSE_Sign1 content-type and extension headers, because the verified-result + API does not expose processing results for those semantics. - Integrity-sensitive fields in unprotected headers, including `alg` and `kid`. - Indefinite-length CBOR at public byte boundaries. +- Floating-point COSE_Key extension values. They are rejected rather than + accepted without full RFC 8949 preferred-width and canonical-NaN validation. - Unexpected CBOR tags except a root COSE_Sign1 tag when decoding Sign1 input. -- Algorithms outside the explicit mapping above. +- X-Wing recipient processing. X-Wing-768 remains represented in the protobuf + algorithm enum so the wire contract can add an explicit profile without + renumbering; attempts to use it in an unsupported operation fail closed + without provider or algorithm fallback. Unsupported inputs fail closed with typed `CoseError` variants. Error messages do not include payloads, keys, raw CBOR, or resolver-provided material. @@ -179,15 +424,51 @@ decoding: - COSE_Sign1 input: 65,536 bytes. - COSE_Key input: 16,384 bytes. +- COSE_Encrypt input: 1,114,112 bytes. - Detached payload input: 1,048,576 bytes. The default attached COSE_Sign1 limit is 65,536 bytes. Consumers that need larger attached reports can opt into a higher limit with -`CoseSign1EncodeOptions::max_cose_sign1_bytes` when signing and -`CosePolicy::max_cose_sign1_bytes` when verifying. Large application payloads +`CoseSign1EncodeOptions::with_max_cose_sign1_bytes` when signing and +`CosePolicy::with_max_cose_sign1_bytes` when verifying. Large application payloads should still prefer detached signing and enforce transport or application-level limits before calling this crate. +`CosePolicy::allowed_algorithms()` is intentionally fail-open only when empty: an empty +allow-list means "any algorithm implemented by this crate and accepted by the +COSE_Key/key resolver path." Set at least one allowed algorithm for verifier +surfaces that know their credential suite. `CosePolicy::require_kid()` is the +separate protected-header presence gate; it does not imply an algorithm +allow-list. Policy and signing-option structs are constructed with builders +rather than public fields so new policy controls can be added without breaking +downstream callers. + +Every native Sign1 key resolver receives `(expected_algorithm, protected_kid)`. +Resolvers must use that tuple as the key-store lookup identity and return only +a public key registered for both values; resolving by `kid` alone discards the +algorithm-binding guarantee. The protobuf lane expresses the same restriction +through its algorithm allow-list and trusted `expected_kid` input. + +The protobuf operation lane is capped independently at 2 MiB for request +messages and caller-supplied per-operation COSE/payload limits. Native Rust APIs +may opt into larger local limits directly; protobuf callers cannot raise their +parse policy beyond the message envelope cap. + +## 0.2.0 Platform Scope + +The `0.2.0` release is intentionally Rust and protobuf only. Its publishable +artifacts are `reallyme-cose-proto` and `reallyme-cose`; the `native` and `wasm` +features are Rust runtime lanes, not platform SDK packages. + +The `0.2.0` distribution does not include Swift, Android/Kotlin, Kotlin/JVM, +native C/JNI, or TypeScript/WASM npm packages. Those package formats are not +part of this release's compatibility or support contract. + +The protobuf `swift_prefix` option is generation metadata, not a published +Swift package. Likewise, `wasm32-unknown-unknown` is a Rust compilation target, +not an npm package. The exact artifact scope is recorded in +[`docs/platform-scope-0.2.0.json`](docs/platform-scope-0.2.0.json). + ## Development Checks Run the full release gate before publishing: @@ -197,19 +478,53 @@ cargo fmt --check cargo check --workspace --all-features RUSTFLAGS=-Dwarnings cargo check --workspace --all-features RUSTFLAGS=-Dwarnings cargo check --workspace --no-default-features +RUSTFLAGS=-Dwarnings cargo check --workspace --no-default-features --features native,wire cargo clippy --workspace --all-targets --all-features -- -D warnings +buf lint +buf generate +cargo fmt --package reallyme-cose-proto --check cargo fmt --manifest-path tools/vector-audit/Cargo.toml --check cargo clippy --manifest-path tools/vector-audit/Cargo.toml --all-targets -- -D warnings +cargo fmt --manifest-path tools/vector-goldens/Cargo.toml --check +cargo clippy --manifest-path tools/vector-goldens/Cargo.toml --all-targets -- -D warnings cargo test --workspace --all-features -cargo run --manifest-path tools/vector-audit/Cargo.toml -- . +cargo bench --bench operation_performance --all-features +cargo run --manifest-path tools/vector-audit/Cargo.toml --bin reallyme-cose-vector-audit -- . cargo nextest run --workspace --no-default-features --features native cargo check --workspace --no-default-features --features native -cargo check --workspace --target wasm32-unknown-unknown --no-default-features --features wasm +cargo check-wasm cargo deny check cargo audit +node --test scripts/release-readiness/operation-contract-routing.test.mjs node scripts/check_release_readiness.mjs ``` +Release readiness requires crates.io dependencies for the published ReallyMe +foundational crates: `reallyme-crypto` `^0.3.0` and `reallyme-codec` `^0.2.0`. +Local `../crypto` or `../codec` path dependencies are not accepted for release. + +Release readiness structurally inspects all 15 executable operation routes. +After masking Rust comments and literals, it requires one dispatcher branch, +one selected semantic execution, one family result conversion, and one central +failure mapper per route. It also rejects native convenience paths that bypass +the semantic facade and adapters that add direct error classification, +transport codecs, generated result construction, or native convenience facades. +It also caps hand-written Rust modules at 500 lines, rejects substantive inline +test modules, and enforces provider, ownership, concurrency, and performance +controls. The benchmark asserts named peak-allocation ceilings; the host +measurements are recorded in +[`docs/performance-baseline-0.2.0.md`](docs/performance-baseline-0.2.0.md). + +The wasm lane must be checked against `wasm32-unknown-unknown`. A host-target +`cargo check --workspace --no-default-features --features wasm` is intentionally +not the supported command because `reallyme-crypto` exposes its wasm provider +exports only when compiling for `wasm32`. Use `cargo check-wasm`, or run the +equivalent explicit command: + +```sh +cargo check --workspace --target wasm32-unknown-unknown --no-default-features --features wasm +``` + ## License Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE). diff --git a/SECURITY.md b/SECURITY.md index 19db665..fe4b1cb 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,10 +18,11 @@ third-party deployments. ## Supported Versions -Only the latest published version of `reallyme-cose` receives security fixes. +Only the latest published versions of `reallyme-cose` and +`reallyme-cose-proto` receive security fixes. ## Scope -This policy covers the `reallyme-cose` crate in this repository. Issues in -`reallyme-crypto`, `reallyme-codec`, or other dependencies should be reported -to their respective repositories. +This policy covers the `reallyme-cose` and `reallyme-cose-proto` crates in this +repository. Issues in `reallyme-crypto`, `reallyme-codec`, or other dependencies +should be reported to their respective repositories. diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..dd3a9e4 --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +# +# SPDX-License-Identifier: Apache-2.0 + +version: v2 +plugins: + - local: protoc-gen-buffa + out: crates/proto/src/generated/buffa + opt: + - views=true + - json=true + - local: protoc-gen-buffa-packaging + out: crates/proto/src/generated/buffa + strategy: all diff --git a/buf.yaml b/buf.yaml index 97e9a74..478a1ba 100644 --- a/buf.yaml +++ b/buf.yaml @@ -4,7 +4,7 @@ version: v2 modules: - - path: proto + - path: crates/proto/proto lint: use: - STANDARD diff --git a/conformance/vectors/manifest.json b/conformance/vectors/manifest.json deleted file mode 100644 index 7c82e16..0000000 --- a/conformance/vectors/manifest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "schema": "reallyme.cose.conformance.vector_manifest.v1", - "suites": [ - { - "id": "cose-sign1", - "source": "ReallyMe deterministic fixture", - "path": "cose-sign1.json", - "case_count": 23 - }, - { - "id": "cose-key", - "source": "ReallyMe generated public-key fixture", - "path": "cose-key.json", - "case_count": 6 - } - ] -} diff --git a/crates/cose/Cargo.toml b/crates/cose/Cargo.toml new file mode 100644 index 0000000..686c8b5 --- /dev/null +++ b/crates/cose/Cargo.toml @@ -0,0 +1,89 @@ +# SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "reallyme-cose" +version = "0.2.0" +description = "Strict COSE helpers for ReallyMe identity software." +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +keywords = ["cose", "cbor", "signing", "identity", "credentials"] +categories = ["cryptography", "encoding"] +publish = true +readme = "README.md" +include = ["/src/**/*.rs", "/Cargo.toml", "/README.md", "/LICENSE", "/NOTICE"] + +[package.metadata.cargo_check_external_types] +# These dependencies deliberately define the cryptographic selector, secret +# owner, and generated wire contract exposed by the supported public API. +allowed_external_types = [ + "crypto_core::algorithm::Algorithm", + "reallyme_cose_proto::*", + "zeroize::ZeroizeOnDrop", + "zeroize::Zeroizing", +] + +[features] +default = ["native"] +native = ["cose-crypto", "reallyme-crypto/native"] +wasm = ["cose-crypto", "reallyme-crypto/wasm"] +wire = [ + "dep:buffa", + "dep:reallyme-cose-proto", + "dep:serde", + "dep:serde_json", +] +cose-crypto = [ + "reallyme-crypto/dispatch", + "reallyme-crypto/aes", + "reallyme-crypto/aes-kw", + "reallyme-crypto/csprng", + "reallyme-crypto/constant-time", + "reallyme-crypto/ed25519", + "reallyme-crypto/kmac", + "reallyme-crypto/ml-kem-512", + "reallyme-crypto/ml-kem-768", + "reallyme-crypto/ml-kem-1024", + "reallyme-crypto/p256", + "reallyme-crypto/p384", + "reallyme-crypto/p521", + "reallyme-crypto/secp256k1", + "reallyme-crypto/ml-dsa-44", + "reallyme-crypto/ml-dsa-65", + "reallyme-crypto/ml-dsa-87", + "reallyme-crypto/sha2", + "reallyme-crypto/x25519", +] + +[dependencies] +buffa = { workspace = true, optional = true } +ciborium.workspace = true +coset.workspace = true +reallyme-codec.workspace = true +reallyme-cose-proto = { workspace = true, features = ["generated"], optional = true } +reallyme-crypto.workspace = true +serde = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } +thiserror.workspace = true +zeroize.workspace = true + +[dev-dependencies] +serde.workspace = true +serde_json.workspace = true + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +allocation-counter.workspace = true +criterion.workspace = true + +[[bench]] +name = "operation_performance" +harness = false + +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wasm-bindgen-test.workspace = true + +[lints] +workspace = true diff --git a/crates/cose/LICENSE b/crates/cose/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/crates/cose/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/crates/cose/NOTICE b/crates/cose/NOTICE new file mode 100644 index 0000000..5ccbe49 --- /dev/null +++ b/crates/cose/NOTICE @@ -0,0 +1,20 @@ +SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved + +SPDX-License-Identifier: Apache-2.0 + +ReallyMe Crypto +Copyright © 2026 ReallyMe LLC. All rights reserved. + +This product includes software developed by ReallyMe LLC and distributed under +the Apache License, Version 2.0. + +ReallyMe Crypto wraps third-party cryptographic implementations and platform +providers. Dependency license terms are recorded in the package manifests and +enforced by the repository dependency policy. Current provider families include +RustCrypto crates, ed25519-dalek, x25519-dalek, ml-kem, ml-dsa, BouncyCastle, +Bitcoin Core libsecp256k1 through reallyme/CSecp256k1 (Swift) and ACINQ +secp256k1-kmp (Kotlin/JVM), CryptoKit, JCA/JCE, @noble/curves, and +@noble/hashes. + +This NOTICE file is informational and does not modify the license terms in +LICENSE or any third-party dependency license. diff --git a/crates/cose/README.md b/crates/cose/README.md new file mode 100644 index 0000000..c78e24e --- /dev/null +++ b/crates/cose/README.md @@ -0,0 +1,536 @@ + + +# reallyme-cose + +[![Rust CI](https://github.com/reallyme/cose/actions/workflows/rust-ci.yml/badge.svg)](https://github.com/reallyme/cose/actions/workflows/rust-ci.yml) +[![reallyme-cose](https://img.shields.io/crates/v/reallyme-cose?label=reallyme-cose&color=2563eb)](https://crates.io/crates/reallyme-cose) +[![Security Policy](https://img.shields.io/badge/security-policy-0f766e)](https://github.com/reallyme/cose/blob/main/SECURITY.md) +[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE) + +`reallyme-cose` is a focused COSE layer for identity systems that need +COSE_Sign1, COSE_Key, deterministic `kid` derivation, and Multikey interop +without broad COSE structure or algorithm negotiation. It builds on +`reallyme-crypto` and `reallyme-codec` to provide strict protected-header +validation, explicit algorithm/key binding, typed non-PII errors, deterministic +CBOR checks, resource limits, and portable conformance vectors for SDK and +protocol implementations. + +## Install + +```sh +cargo add reallyme-cose +``` + +Default features enable COSE signing and verification through `reallyme-crypto`. +With `default-features = false`, the crate still builds the COSE_Key, Multikey, +policy, algorithm-mapping, and CBOR limit helpers, but does not export +crypto-backed signing or verification APIs. + +Enable the `wire` feature only for protobuf operation adapters: + +```toml +reallyme-cose = { version = "0.2.0", features = ["wire"] } +``` + +When default features are disabled, pair `wire` with an explicit runtime lane, +for example `features = ["native", "wire"]` for native Rust or +`features = ["wasm", "wire"]` for `wasm32-unknown-unknown`. +The `wire` feature is intentionally non-additive: enabling `wire` alone fails +closed because operation adapters must never compile without a selected runtime. + +## Workspace Layout + +The repository follows the same workspace conventions as ReallyMe Crypto and +ReallyMe Codec: + +```text +crates/ +├── cose/ public COSE facade, semantic operations, tests, and benchmarks +└── proto/ protobuf schema and generated Buffa boundary types +docs/ versioned scope and performance records +fuzz/ cargo-fuzz targets and corpora +scripts/ generation, policy, and release-readiness automation +tools/ independent vector audit and golden-vector generation +vectors/ portable classical and post-quantum conformance vectors +``` + +## Standards Profile + +This crate implements a focused COSE profile over: + +- [RFC 9052](https://www.rfc-editor.org/rfc/rfc9052.html), for COSE structures, + protected and unprotected header maps, COSE_Sign1, Sig_structure construction, + COSE_Key structure, key identifiers, and CBOR encoding restrictions. +- [RFC 9053](https://www.rfc-editor.org/rfc/rfc9053.html), for initial COSE + algorithm identifiers, key types, curve identifiers, and key-parameter labels. +- [RFC 9964](https://www.rfc-editor.org/rfc/rfc9964.html), for ML-DSA algorithm + registrations and AKP COSE_Key encoding. + +`reallyme-cose` handles COSE_Key construction and parsing, supported +alg/kty/crv mapping, key identifier policy, deterministic CBOR boundary checks, +and the structural COSE_Sign1 parsing needed by mdoc, attestation, credential, +and wallet code. Signing, verification, hashing, and key generation remain in +`reallyme-crypto`. + +COSE_Key bytes used for deterministic `kid` derivation follow RFC 8949 core +deterministic map ordering. Verification intentionally also accepts other map +orderings permitted by RFC 9052: the received protected-header byte string is +authenticated exactly as received and is never decoded and re-encoded before +signature or AEAD verification. Duplicate labels and other profile violations +still fail closed. + +## Quick Start + +```rust +use reallyme_cose::{cose_sign1, cose_verify1_with_policy, Algorithm, CoseError, CosePolicy}; +use reallyme_crypto::dispatch::generate_keypair; + +fn sign_and_verify() -> Result<(), CoseError> { + let (public_key, private_key) = generate_keypair(Algorithm::Ed25519) + .map_err(|_| CoseError::Crypto)?; + let kid = b"example-key"; + + let cose_bytes = cose_sign1(Algorithm::Ed25519, b"payload", &private_key, Some(kid))?; + let policy = CosePolicy::new() + .with_require_kid(true) + .allow_algorithm(Algorithm::Ed25519); + + let verified = cose_verify1_with_policy(&cose_bytes, &policy, |algorithm, requested_kid| { + (algorithm == Algorithm::Ed25519 && requested_kid == kid).then(|| public_key.clone()) + })?; + assert_eq!(verified.payload.as_slice(), b"payload"); + assert_eq!(verified.alg, Algorithm::Ed25519); + assert_eq!(verified.kid.as_slice(), kid); + Ok(()) +} +``` + +The same example is compile-checked as the crate-level doc example. + +## Signing And Verification Options + +COSE_Sign1 encoders emit untagged messages by default. Use the tagged helpers +or `CoseSign1EncodeOptions` when an integration expects the registered +COSE_Sign1 root tag (18): + +```rust +use reallyme_cose::{cose_sign1_with_options, Algorithm, CoseSign1EncodeOptions}; + +let cose = cose_sign1_with_options( + Algorithm::Ed25519, + b"payload", + &private_key, + Some(b"example-key"), + CoseSign1EncodeOptions::tagged().with_max_cose_sign1_bytes(512 * 1024), +)?; +``` + +Policy-aware verification keeps common platform requirements at the byte API +boundary. `cose_verify1_with_policy` and `cose_verify1_detached_with_policy` +can require `kid`, restrict accepted algorithms, raise or lower byte limits, +and return verified metadata: + +```rust +use reallyme_cose::{cose_verify1_with_policy, Algorithm, CosePolicy}; + +let policy = CosePolicy::new() + .with_require_kid(true) + .allow_algorithm(Algorithm::P256) + .with_max_cose_sign1_bytes(512 * 1024); + +let verified = cose_verify1_with_policy(&cose, &policy, |algorithm, kid| { + resolve_public_key(algorithm, kid) +})?; +assert_eq!(verified.alg, Algorithm::P256); +``` + +### Non-exportable signing keys + +Applications that keep private keys in a secure enclave, keystore, HSM, or +another provider-owned boundary implement `CoseSigner` and call +`cose_sign1_with_signer` or `cose_sign1_detached_with_signer`. The provider +receives the exact, bounded COSE `Sig_structure`; private key bytes never enter +the COSE API. Provider failures use the fixed `CoseSignerError` enum, and the +COSE layer validates and normalizes the returned signature before encoding it. + +The provider declares one algorithm through `CoseSigner::algorithm`. ECDSA +providers return their native DER signature; providers for Ed25519 and ML-DSA +return fixed-width signature bytes. Providers must return signatures in a +`Zeroizing>` owner and must not include key handles, platform exception +text, or user data in errors. + +### Sensitive result ownership + +Native APIs retain wipe-on-drop ownership at the public boundary: +`derive_kid_from_cose_key_public` returns `Zeroizing>`, and +`cose_key_to_multikey` returns `Zeroizing`. Keep those owners intact +instead of converting them into ordinary `Vec` or `String` values. `CoseKey` +is also a wipe-on-drop owner and is marked `must_use`. + +ML-KEM request values are non-exhaustive and constructed with +`CoseMlKemEncryptRequest::new` or `CoseMlKemDecryptRequest::new`; their fields +are deliberately not public struct-literal surface. Use +`with_supp_priv_info` when replacing the optional private KDF context. + +## Supported Features + +- COSE_Sign1 with attached payloads. +- COSE_Sign1 with detached payloads. +- Explicit external AAD for attached and detached COSE_Sign1 signing and + verification. Verification fails authentication when the supplied AAD does + not exactly match the bytes used for signing. +- COSE_Sign1 signing can emit either untagged messages or messages carrying the + registered COSE_Sign1 root tag (18) through `cose_sign1_tagged`, + `cose_sign1_detached_tagged`, or `CoseSign1EncodeOptions`. +- Ed25519, P-256, P-384, P-521, secp256k1, ML-DSA-44, ML-DSA-65, and + ML-DSA-87 signing using their current IANA COSE registrations. +- ECDSA signatures use the fixed-width `r || s` encoding required by + RFC 9053; DER-encoded ECDSA signatures are rejected. +- Verification accepts untagged COSE_Sign1 input and input carrying the + registered COSE_Sign1 tag (18). +- `cose_verify1_with_policy` and `cose_verify1_detached_with_policy` enforce + `CosePolicy` at the byte API boundary, including `kid` requirements, + algorithm allow-lists, and configurable byte limits. +- `cose_verify1_with_metadata` and policy-aware verification return verified + payload, algorithm, and `kid` metadata so callers do not need to reparse COSE + after successful verification. +- Verification binds the protected header bytes exactly as received, per + RFC 9052 §4.4. +- COSE_Key public/private construction and extraction for supported signing keys. + Private construction requires the corresponding public key and validates the + pair before the key can enter the typed API. +- One-recipient `COSE_Encrypt` and `COSE_Recipient` processing for + ML-KEM-512, ML-KEM-768, and ML-KEM-1024, with direct KMAC256 derivation or + the fixed ML-KEM-512+A128KW, ML-KEM-768+A192KW, and + ML-KEM-1024+A256KW key-wrap profiles. +- AES-128-GCM, AES-192-GCM, and AES-256-GCM protected content algorithms for + every supported ML-KEM recipient profile. +- Guarded COSE_Key, COSE_Sign1, and COSE_Encrypt parsers retain the decoded + CBOR tree under a recursive wipe owner until every semantic field has moved + into its profile-specific wipe-on-drop owner. Rejected parses therefore wipe + partially constructed payload, ciphertext, identifier, header, and key + buffers. The public `CoseKey` owner also wipes private parameters, + identifiers, base-IV material, and rejected text/byte extension owners. + Private-key extraction and verified payloads use zeroizing return buffers. +- COSE_Key public conversion for X25519 key-agreement keys. +- COSE_Key to Multikey and Multikey to COSE_Key conversion, including the + ReallyMe ML-KEM-512/768/1024 AKP profiles. ML-KEM uses the registered draft + Multicodec names `mlkem-512-pub`, `mlkem-768-pub`, and `mlkem-1024-pub` while + retaining ReallyMe private-use COSE algorithm identifiers until IANA assigns + final values. +- `kid = SHA-256(canonical algorithm-bound public-only COSE_Key)` derivation. +- Portable classical and PQ vectors in `vectors/cose-sign1*.json` + and `vectors/cose-key*.json`. The PQ suites cover ML-DSA-44/65/87 + Sign1 plus ML-DSA and ML-KEM AKP COSE_Key/Multikey round trips. ECDSA vectors + remain the cross-lane contract for fixed-width `r || s` signatures. Any + Swift, Kotlin, TypeScript, and other SDK implementations must reject DER signatures + at the COSE boundary. + +## Protobuf Wire Boundary + +The native Rust API remains the primary SDK surface. For service, FFI, generated +SDK, and conformance boundaries, `reallyme-cose` also exposes a generated +protobuf operation lane: request bytes in, result-envelope bytes out, with +structured `CoseError` protobuf bytes on failure. + +The checked-in schema lives at +`crates/proto/proto/reallyme/cose/v1/cose.proto`. Generated Buffa Rust +types live in the low-level `reallyme-cose-proto` workspace crate, under +`crates/proto`; that crate is published only so the optional `wire` feature +can remain publishable while generated code and its lint posture stay out of the +handwritten native crate. After editing the schema, run `buf generate` and keep the +generated files checked in. COSE does not currently publish Swift, Kotlin, or +TypeScript packages from this repository. The schema's Swift metadata allows +generated Swift protobuf consumers to use the same wire contract independently. + +The wire lane has two layers: + +- `reallyme-cose-proto` owns generated request, result, and error message + types. It is not the normal SDK surface. Its generated Rust is hardened after + `buf generate` so request, result, key, payload, AAD, and `kid` byte fields + are redacted from `Debug`, zeroized by `clear`, and zeroized on drop. Buffa + requires generated messages to remain `Clone`; each clone is therefore an + explicit transient owner with the same wipe-on-drop behavior. +- `reallyme-cose::wire` owns executable adapters behind the opt-in `wire` + feature. Use `wire::execute_operation_proto` for protobuf request bytes or + `wire::execute_operation_proto_json` for generated ProtoJSON requests. Both return a + binary `CoseOperationResponseV2`, whose outcome is either a structured error + or an exact operation-specific result variant. Rust-side adapters should use + `wire::decode_operation_response_for_request`; it rejects a result variant + that does not match the submitted request. The earlier status-plus-opaque- + payload compatibility envelope has been removed. Operation-specific dispatch + remains private so the executable boundary has one request and one response + model. + +Executable response bytes are stored in zeroizing owners by the Rust wire +adapter. Generated ProtoJSON uses protobuf enum names +and standard protobuf base64 bytes. Serialization returns a caller-owned JSON +`String` that the library cannot wipe; callers handling private keys, payloads, +AAD, or identifiers should place it in a zeroizing owner and release it as soon +as the transport write completes. Likewise, generated `PartialEq` is a schema +convenience and is not a constant-time secret comparison primitive. + +ProtoJSON is provided only by the generated Buffa protobuf types. The native +Rust SDK does not define a second ad hoc JSON representation for COSE requests +or results. `buf.gen.yaml` enables both borrowed Buffa views and strict +generated ProtoJSON. The post-generation hardening pass redacts sensitive byte +fields in both owned-message and borrowed-view `Debug` implementations; owned +messages additionally zeroize sensitive buffers on `clear` and drop. Borrowed +views cannot erase caller-owned input and therefore do not claim zeroization. +The executable binary protobuf decoder rejects unknown fields, matching strict +ProtoJSON semantics and preventing opaque length-delimited values from being +retained in generated unknown-field storage. Direct users of sensitive +generated messages also receive recursive unknown-field wiping on `clear` and +drop. + +## ReallyMe ML-KEM COSE Profile + +The current IANA COSE algorithm registry does not yet assign ML-KEM recipient +algorithm identifiers. To provide interoperable COSE objects now without +occupying or guessing future standards-track values, ReallyMe emits stable +private-use identifiers below `-65536`: + +| COSE algorithm | ReallyMe identifier | +| --- | ---: | +| ML-KEM-512 direct | `-65537` | +| ML-KEM-768 direct | `-65538` | +| ML-KEM-1024 direct | `-65539` | +| ML-KEM-512+A128KW | `-65540` | +| ML-KEM-768+A192KW | `-65541` | +| ML-KEM-1024+A256KW | `-65542` | +| `ek` recipient header label | `-65543` | + +These are COSE integer identifiers and are deliberately independent from the +protobuf enum numbers. The content algorithm is authenticated in the +`COSE_Encrypt` protected header. The exact direct or key-wrap ML-KEM profile and +the mandatory recipient `kid` are authenticated in the `COSE_Recipient` +protected header. The `ek` header carries the ML-KEM ciphertext in the +recipient unprotected map because its integrity is established by successful +decapsulation, KDF binding, and content or key-wrap authentication. + +For DID and Multikey integration, public ML-KEM AKP keys use the draft +Multicodec names `mlkem-512-pub`, `mlkem-768-pub`, and `mlkem-1024-pub`. +Decoding those Multikeys binds them to the corresponding ReallyMe private-use +COSE algorithm identifier; it never guesses a future IANA value. When final +IANA identifiers are assigned, transitional decoding must accept the private +and final identifiers explicitly while encoding policy selects one deliberately. + +The profile follows the current COSE ML-KEM draft construction: + +- ML-KEM uses AKP COSE keys. Private import/export uses the 64-octet FIPS 203 + seed `d || z`, and private/public construction cryptographically validates + the supplied pair. +- The mandatory recipient `kid` is exactly + `SHA-256(canonical public-only algorithm-bound COSE_Key)`. Encryption rejects + a caller-supplied identifier that does not identify the recipient public key. + Decryption deterministically derives the public key from the private + `d || z` seed and rejects a protected `kid` bound to any other key before + decapsulation. Arbitrary routing labels are not valid recipient identifiers + in this profile. +- KMAC256 derives from the ML-KEM shared secret. Its message is the CBOR KDF + context `[AlgorithmID, SuppPubInfo, ?SuppPrivInfo]`, its customization string + is empty, and `SuppPubInfo` binds the output length and exact encoded + recipient protected header. +- Direct mode derives the AES-GCM content-encryption key and encodes a null + recipient ciphertext. +- Key-wrap mode derives only the fixed AES-KW key-encryption key for the KEM + strength, wraps a fresh random content-encryption key, and carries that + wrapped key as recipient ciphertext. +- The ML-KEM parameter set and AES-GCM content cipher are independent protocol + choices. Mixed combinations remain interoperable, but their combined + security is limited by the weaker component. Applications seeking aligned + strength should pair ML-KEM-512 with AES-128-GCM, ML-KEM-768 with + AES-192-GCM, and ML-KEM-1024 with AES-256-GCM. +- External AAD and optional `SuppPrivInfo` must match exactly at decryption. + AES-KW integrity failures remain distinct from content authentication + failures. + +This initial profile intentionally supports exactly one recipient. Decoding +accepts tagged or untagged `COSE_Encrypt`; encoding emits the registered +`COSE_Encrypt` tag. AKP keys use the base direct identifier in their `alg` +parameter because the same key may be selected by either recipient mode; the +recipient protected `alg` remains the authoritative operation profile. + +When IANA assigns final identifiers, ReallyMe will add explicit transitional +decoding for the private-use and final identifiers. Existing private-use +objects will not be silently reinterpreted, and emitted identifiers will change +only in a documented profile/version transition. + +## COSE-Layer Vector Audit + +`tools/vector-audit` is a standalone, unpublished Cargo binary that audits the +committed vector JSON with RustCrypto, `ciborium`, and `bs58`. It does not +depend on `reallyme-cose`, `reallyme-crypto`, or `reallyme-codec`, which keeps +the CBOR, COSE, KDF, key-wrap, and Multikey checks independent from production. +It uses the same direct RustCrypto primitive families as production, so +primitive ACVP assurance remains the responsibility of the pinned +`reallyme-crypto` dependency. In addition to COSE_Sign1 and COSE_Key fixtures, +the tool independently parses the ReallyMe +ML-KEM `COSE_Encrypt` maps, repeats deterministic encapsulation and +decapsulation, reconstructs the KMAC256 context, performs RFC 3394 AES-KW +unwrap where applicable, and authenticates the AES-GCM ciphertext. It also +derives every PQ public key directly from its seed, verifies ML-DSA Sign1 +signatures through `ml-dsa`, and checks the AKP parameters and Multicodec prefix +without linking the production COSE, Crypto, or Codec crates. + +The classical Sign1 suite also contains a fixed Ed25519 signature produced by +Node's OpenSSL-backed implementation over RFC 8032 key material. Suite counts +and exact SHA-256 digests are bound in `vectors/manifest.json`. + +`tools/vector-goldens` is the separate maintenance generator for deterministic +ReallyMe fixtures. Its fixed ML-KEM seeds, encapsulation randomness, CEKs, and +nonces are test inputs only; the production encryption API continues to use +operating-system entropy. Regenerated bytes must pass the independent audit +before they are accepted. + +```sh +cargo run --manifest-path tools/vector-goldens/Cargo.toml +cargo run --manifest-path tools/vector-audit/Cargo.toml --bin reallyme-cose-vector-audit -- . +``` + +## Unsupported COSE Surface + +The following structures and features are not implemented: + +- COSE_Mac0, COSE_Mac, and MAC verification. +- COSE_Encrypt0. +- Multi-recipient COSE_Encrypt. +- COSE_Sign multi-signer structures. +- Countersignatures. +- Critical protected headers. +- COSE_Sign1 content-type and extension headers, because the verified-result + API does not expose processing results for those semantics. +- Integrity-sensitive fields in unprotected headers, including `alg` and `kid`. +- Indefinite-length CBOR at public byte boundaries. +- Floating-point COSE_Key extension values. They are rejected rather than + accepted without full RFC 8949 preferred-width and canonical-NaN validation. +- Unexpected CBOR tags except a root COSE_Sign1 tag when decoding Sign1 input. +- X-Wing recipient processing. X-Wing-768 remains represented in the protobuf + algorithm enum so the wire contract can add an explicit profile without + renumbering; attempts to use it in an unsupported operation fail closed + without provider or algorithm fallback. + +Unsupported inputs fail closed with typed `CoseError` variants. Error messages +do not include payloads, keys, raw CBOR, or resolver-provided material. + +## Resource Limits + +Public byte-boundary APIs enforce deterministic limits before COSE structure +decoding: + +- COSE_Sign1 input: 65,536 bytes. +- COSE_Key input: 16,384 bytes. +- COSE_Encrypt input: 1,114,112 bytes. +- Detached payload input: 1,048,576 bytes. + +The default attached COSE_Sign1 limit is 65,536 bytes. Consumers that need +larger attached reports can opt into a higher limit with +`CoseSign1EncodeOptions::with_max_cose_sign1_bytes` when signing and +`CosePolicy::with_max_cose_sign1_bytes` when verifying. Large application payloads +should still prefer detached signing and enforce transport or application-level +limits before calling this crate. + +`CosePolicy::allowed_algorithms()` is intentionally fail-open only when empty: an empty +allow-list means "any algorithm implemented by this crate and accepted by the +COSE_Key/key resolver path." Set at least one allowed algorithm for verifier +surfaces that know their credential suite. `CosePolicy::require_kid()` is the +separate protected-header presence gate; it does not imply an algorithm +allow-list. Policy and signing-option structs are constructed with builders +rather than public fields so new policy controls can be added without breaking +downstream callers. + +Every native Sign1 key resolver receives `(expected_algorithm, protected_kid)`. +Resolvers must use that tuple as the key-store lookup identity and return only +a public key registered for both values; resolving by `kid` alone discards the +algorithm-binding guarantee. The protobuf lane expresses the same restriction +through its algorithm allow-list and trusted `expected_kid` input. + +The protobuf operation lane is capped independently at 2 MiB for request +messages and caller-supplied per-operation COSE/payload limits. Native Rust APIs +may opt into larger local limits directly; protobuf callers cannot raise their +parse policy beyond the message envelope cap. + +## 0.2.0 Platform Scope + +The `0.2.0` release is intentionally Rust and protobuf only. Its publishable +artifacts are `reallyme-cose-proto` and `reallyme-cose`; the `native` and `wasm` +features are Rust runtime lanes, not platform SDK packages. + +The `0.2.0` distribution does not include Swift, Android/Kotlin, Kotlin/JVM, +native C/JNI, or TypeScript/WASM npm packages. Those package formats are not +part of this release's compatibility or support contract. + +The protobuf `swift_prefix` option is generation metadata, not a published +Swift package. Likewise, `wasm32-unknown-unknown` is a Rust compilation target, +not an npm package. The exact artifact scope is recorded in +[`docs/platform-scope-0.2.0.json`](https://github.com/reallyme/cose/blob/main/docs/platform-scope-0.2.0.json). + +## Development Checks + +Run the full release gate before publishing: + +```sh +cargo fmt --check +cargo check --workspace --all-features +RUSTFLAGS=-Dwarnings cargo check --workspace --all-features +RUSTFLAGS=-Dwarnings cargo check --workspace --no-default-features +RUSTFLAGS=-Dwarnings cargo check --workspace --no-default-features --features native,wire +cargo clippy --workspace --all-targets --all-features -- -D warnings +buf lint +buf generate +cargo fmt --package reallyme-cose-proto --check +cargo fmt --manifest-path tools/vector-audit/Cargo.toml --check +cargo clippy --manifest-path tools/vector-audit/Cargo.toml --all-targets -- -D warnings +cargo fmt --manifest-path tools/vector-goldens/Cargo.toml --check +cargo clippy --manifest-path tools/vector-goldens/Cargo.toml --all-targets -- -D warnings +cargo test --workspace --all-features +cargo bench --bench operation_performance --all-features +cargo run --manifest-path tools/vector-audit/Cargo.toml --bin reallyme-cose-vector-audit -- . +cargo nextest run --workspace --no-default-features --features native +cargo check --workspace --no-default-features --features native +cargo check-wasm +cargo deny check +cargo audit +node --test scripts/release-readiness/operation-contract-routing.test.mjs +node scripts/check_release_readiness.mjs +``` + +Release readiness requires crates.io dependencies for the published ReallyMe +foundational crates: `reallyme-crypto` `^0.3.0` and `reallyme-codec` `^0.2.0`. +Local `../crypto` or `../codec` path dependencies are not accepted for release. + +Release readiness structurally inspects all 15 executable operation routes. +After masking Rust comments and literals, it requires one dispatcher branch, +one selected semantic execution, one family result conversion, and one central +failure mapper per route. It also rejects native convenience paths that bypass +the semantic facade and adapters that add direct error classification, +transport codecs, generated result construction, or native convenience facades. +It also caps hand-written Rust modules at 500 lines, rejects substantive inline +test modules, and enforces provider, ownership, concurrency, and performance +controls. The benchmark asserts named peak-allocation ceilings; the host +measurements are recorded in +[`docs/performance-baseline-0.2.0.md`](https://github.com/reallyme/cose/blob/main/docs/performance-baseline-0.2.0.md). + +The wasm lane must be checked against `wasm32-unknown-unknown`. A host-target +`cargo check --workspace --no-default-features --features wasm` is intentionally +not the supported command because `reallyme-crypto` exposes its wasm provider +exports only when compiling for `wasm32`. Use `cargo check-wasm`, or run the +equivalent explicit command: + +```sh +cargo check --workspace --target wasm32-unknown-unknown --no-default-features --features wasm +``` + +## License + +Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE). + +## Copyright and Trademarks + +Copyright © 2026 by ReallyMe LLC. + +ReallyMe® is a registered trademark of ReallyMe LLC. diff --git a/crates/cose/benches/operation_performance.rs b/crates/cose/benches/operation_performance.rs new file mode 100644 index 0000000..cb61697 --- /dev/null +++ b/crates/cose/benches/operation_performance.rs @@ -0,0 +1,301 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Throughput and peak-allocation baselines for the COSE operation families. + +use std::hint::black_box; +use std::time::Duration; + +use allocation_counter::measure; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use reallyme_cose::{ + cose_decrypt_ml_kem, cose_encrypt_ml_kem_direct, cose_key_from_public_bytes, + cose_key_from_slice, cose_key_to_multikey, cose_key_to_vec, cose_sign1, cose_sign1_detached, + cose_verify1_detached_with_policy, cose_verify1_with_policy, derive_kid_from_cose_key_public, + Algorithm, CoseContentEncryptionAlgorithm, CoseMlKemAlgorithm, CoseMlKemDecryptRequest, + CoseMlKemEncryptRequest, CosePolicy, +}; +use reallyme_crypto::dispatch::generate_keypair; +use zeroize::Zeroizing; + +const REALISTIC_PAYLOAD_BYTES: usize = 4_096; +const MAX_DETACHED_PAYLOAD_BYTES: usize = 1_048_576; +const SIGN1_PEAK_ALLOCATION_LIMIT: u64 = 4 * 1_048_576; +const KEY_PEAK_ALLOCATION_LIMIT: u64 = 1_048_576; +const MULTIKEY_PEAK_ALLOCATION_LIMIT: u64 = 1_048_576; +const DECRYPT_PEAK_ALLOCATION_LIMIT: u64 = 12 * 1_048_576; +const BENCHMARK_SAMPLE_SIZE: usize = 10; + +struct SignFixture { + cose: Zeroizing>, + payload: Zeroizing>, + public_key: Zeroizing>, + detached: bool, +} + +struct DecryptFixture { + cose_encrypt: Zeroizing>, + private_key: Zeroizing>, + kid: Zeroizing>, +} + +fn benchmark_operations(c: &mut Criterion) { + let realistic_sign = sign_fixture(REALISTIC_PAYLOAD_BYTES, false); + let maximum_sign = sign_fixture(MAX_DETACHED_PAYLOAD_BYTES, true); + let (realistic_key, maximum_key) = key_fixtures(); + let realistic_decrypt = decrypt_fixture(CoseMlKemAlgorithm::MlKem512, REALISTIC_PAYLOAD_BYTES); + let maximum_decrypt = + decrypt_fixture(CoseMlKemAlgorithm::MlKem1024, MAX_DETACHED_PAYLOAD_BYTES); + + assert_peak_allocation( + "sign1_verify_realistic", + SIGN1_PEAK_ALLOCATION_LIMIT, + || verify_sign(&realistic_sign), + ); + assert_peak_allocation("sign1_verify_maximum", SIGN1_PEAK_ALLOCATION_LIMIT, || { + verify_sign(&maximum_sign) + }); + assert_peak_allocation( + "cose_key_parse_realistic", + KEY_PEAK_ALLOCATION_LIMIT, + || cose_key_from_slice(&realistic_key), + ); + assert_peak_allocation( + "cose_key_parse_maximum_profile", + KEY_PEAK_ALLOCATION_LIMIT, + || cose_key_from_slice(&maximum_key), + ); + let maximum_key_owner = + cose_key_from_slice(&maximum_key).unwrap_or_else(|error| benchmark_setup_failed(error)); + assert_peak_allocation("multikey_realistic", MULTIKEY_PEAK_ALLOCATION_LIMIT, || { + let key = cose_key_from_slice(&realistic_key) + .unwrap_or_else(|error| benchmark_setup_failed(error)); + cose_key_to_multikey(&key) + }); + assert_peak_allocation( + "multikey_maximum_profile", + MULTIKEY_PEAK_ALLOCATION_LIMIT, + || cose_key_to_multikey(&maximum_key_owner), + ); + assert_peak_allocation("decrypt_realistic", DECRYPT_PEAK_ALLOCATION_LIMIT, || { + decrypt(&realistic_decrypt) + }); + assert_peak_allocation("decrypt_maximum", DECRYPT_PEAK_ALLOCATION_LIMIT, || { + decrypt(&maximum_decrypt) + }); + + let mut sign_group = c.benchmark_group("sign1_verify"); + bench_sign(&mut sign_group, "realistic", &realistic_sign); + bench_sign(&mut sign_group, "maximum_detached", &maximum_sign); + sign_group.finish(); + + let mut key_group = c.benchmark_group("cose_key_parse"); + key_group.throughput(Throughput::Bytes( + realistic_key.len().try_into().unwrap_or(u64::MAX), + )); + key_group.bench_with_input( + BenchmarkId::new("realistic", realistic_key.len()), + &realistic_key, + |b, input| { + b.iter(|| black_box(cose_key_from_slice(black_box(input)))); + }, + ); + key_group.throughput(Throughput::Bytes( + maximum_key.len().try_into().unwrap_or(u64::MAX), + )); + key_group.bench_with_input( + BenchmarkId::new("maximum_profile", maximum_key.len()), + &maximum_key, + |b, input| { + b.iter(|| black_box(cose_key_from_slice(black_box(input)))); + }, + ); + key_group.finish(); + + let realistic_key_owner = + cose_key_from_slice(&realistic_key).unwrap_or_else(|error| benchmark_setup_failed(error)); + let mut multikey_group = c.benchmark_group("multikey_conversion"); + multikey_group.bench_function("realistic", |b| { + b.iter(|| black_box(cose_key_to_multikey(black_box(&realistic_key_owner)))); + }); + multikey_group.bench_function("maximum_profile", |b| { + b.iter(|| black_box(cose_key_to_multikey(black_box(&maximum_key_owner)))); + }); + multikey_group.finish(); + + let mut decrypt_group = c.benchmark_group("cose_encrypt_decrypt"); + bench_decrypt(&mut decrypt_group, "realistic", &realistic_decrypt); + bench_decrypt(&mut decrypt_group, "maximum", &maximum_decrypt); + decrypt_group.finish(); +} + +fn bench_sign( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + name: &str, + fixture: &SignFixture, +) { + group.throughput(Throughput::Bytes( + fixture.payload.len().try_into().unwrap_or(u64::MAX), + )); + group.bench_function(name, |b| { + b.iter(|| { + verify_sign(black_box(fixture)); + black_box(()) + }) + }); +} + +fn bench_decrypt( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, + name: &str, + fixture: &DecryptFixture, +) { + group.throughput(Throughput::Bytes( + fixture.cose_encrypt.len().try_into().unwrap_or(u64::MAX), + )); + group.bench_function(name, |b| { + b.iter(|| { + decrypt(black_box(fixture)); + black_box(()) + }) + }); +} + +#[allow(clippy::print_stderr)] +fn assert_peak_allocation(name: &str, limit: u64, operation: impl FnOnce() -> T) { + let info = measure(|| drop(black_box(operation()))); + assert!( + info.bytes_max <= limit, + "{name} exceeded its reviewed peak allocation limit" + ); + eprintln!( + "allocation {name}: peak={} total={} count={}", + info.bytes_max, info.bytes_total, info.count_total + ); +} + +fn sign_fixture(payload_len: usize, detached: bool) -> SignFixture { + let (public_key, private_key) = + generate_keypair(Algorithm::Ed25519).unwrap_or_else(|error| benchmark_setup_failed(error)); + let payload = Zeroizing::new(vec![0x5a; payload_len]); + let cose = if detached { + cose_sign1_detached( + Algorithm::Ed25519, + &payload, + &private_key, + Some(b"benchmark-key"), + ) + } else { + cose_sign1( + Algorithm::Ed25519, + &payload, + &private_key, + Some(b"benchmark-key"), + ) + } + .unwrap_or_else(|error| benchmark_setup_failed(error)); + SignFixture { + cose, + payload, + public_key: Zeroizing::new(public_key), + detached, + } +} + +fn verify_sign(fixture: &SignFixture) { + let policy = CosePolicy::new().allow_algorithm(Algorithm::Ed25519); + let result = if fixture.detached { + cose_verify1_detached_with_policy(&fixture.cose, &fixture.payload, &policy, |_, _| { + Some(fixture.public_key.to_vec()) + }) + .map(|_| ()) + } else { + cose_verify1_with_policy(&fixture.cose, &policy, |_, _| { + Some(fixture.public_key.to_vec()) + }) + .map(|_| ()) + }; + if let Err(error) = result { + benchmark_setup_failed(error); + } +} + +fn key_fixtures() -> (Zeroizing>, Zeroizing>) { + let (ed_public, _) = + generate_keypair(Algorithm::Ed25519).unwrap_or_else(|error| benchmark_setup_failed(error)); + let ed_key = cose_key_from_public_bytes(Algorithm::Ed25519, &ed_public) + .unwrap_or_else(|error| benchmark_setup_failed(error)); + let realistic = cose_key_to_vec(&ed_key).unwrap_or_else(|error| benchmark_setup_failed(error)); + + let (ml_kem_public, _) = reallyme_crypto::ml_kem_1024::generate_ml_kem_1024_keypair() + .unwrap_or_else(|error| benchmark_setup_failed(error)); + let ml_kem_key = cose_key_from_public_bytes(Algorithm::MlKem1024, &ml_kem_public) + .unwrap_or_else(|error| benchmark_setup_failed(error)); + let maximum = + cose_key_to_vec(&ml_kem_key).unwrap_or_else(|error| benchmark_setup_failed(error)); + (realistic, maximum) +} + +fn decrypt_fixture(algorithm: CoseMlKemAlgorithm, plaintext_len: usize) -> DecryptFixture { + let crypto_algorithm = match algorithm { + CoseMlKemAlgorithm::MlKem512 => Algorithm::MlKem512, + CoseMlKemAlgorithm::MlKem768 => Algorithm::MlKem768, + CoseMlKemAlgorithm::MlKem1024 => Algorithm::MlKem1024, + _ => benchmark_setup_failed("unrecognized ML-KEM benchmark algorithm"), + }; + let (public_key, private_key) = + generate_keypair(crypto_algorithm).unwrap_or_else(|error| benchmark_setup_failed(error)); + let key = cose_key_from_public_bytes(crypto_algorithm, &public_key) + .unwrap_or_else(|error| benchmark_setup_failed(error)); + let kid = + derive_kid_from_cose_key_public(&key).unwrap_or_else(|error| benchmark_setup_failed(error)); + let plaintext = Zeroizing::new(vec![0xa5; plaintext_len]); + let request = CoseMlKemEncryptRequest::new( + algorithm, + CoseContentEncryptionAlgorithm::Aes256Gcm, + &public_key, + &kid, + &plaintext, + None, + ); + let cose_encrypt = + cose_encrypt_ml_kem_direct(&request).unwrap_or_else(|error| benchmark_setup_failed(error)); + DecryptFixture { + cose_encrypt, + private_key, + kid, + } +} + +fn decrypt(fixture: &DecryptFixture) { + let request = CoseMlKemDecryptRequest::new( + &fixture.cose_encrypt, + &fixture.private_key, + &fixture.kid, + None, + ); + if let Err(error) = cose_decrypt_ml_kem(&request) { + benchmark_setup_failed(error); + } +} + +#[allow(clippy::print_stderr)] +fn benchmark_setup_failed(error: impl core::fmt::Debug) -> ! { + eprintln!("benchmark fixture or operation failed: {error:?}"); + std::process::exit(1) +} + +fn configured_criterion() -> Criterion { + Criterion::default() + .sample_size(BENCHMARK_SAMPLE_SIZE) + .warm_up_time(Duration::from_millis(500)) + .measurement_time(Duration::from_secs(1)) +} + +criterion_group! { + name = benches; + config = configured_criterion(); + targets = benchmark_operations +} +criterion_main!(benches); diff --git a/crates/cose/src/algorithm/map_from_cose.rs b/crates/cose/src/algorithm/map_from_cose.rs new file mode 100644 index 0000000..44d0ccc --- /dev/null +++ b/crates/cose/src/algorithm/map_from_cose.rs @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 +use crate::CoseError; +use coset::iana; +use reallyme_crypto::core::Algorithm; + +/// Strict internal mapping from a parsed COSE algorithm to the public selector. +/// +/// # Errors +/// +/// Returns [`CoseError::UnsupportedAlgorithm`] when the COSE algorithm is not +/// one of the registered algorithms implemented by this crate. +pub(crate) fn algorithm_from_cose_alg(alg: &coset::Algorithm) -> Result { + match alg { + coset::Algorithm::Assigned(iana::Algorithm::Ed25519) => Ok(Algorithm::Ed25519), + + coset::Algorithm::Assigned(iana::Algorithm::ESP256) => Ok(Algorithm::P256), + + coset::Algorithm::Assigned(iana::Algorithm::ESP384) => Ok(Algorithm::P384), + + coset::Algorithm::Assigned(iana::Algorithm::ESP512) => Ok(Algorithm::P521), + + coset::Algorithm::Assigned(iana::Algorithm::ES256K) => Ok(Algorithm::Secp256k1), + + coset::Algorithm::Assigned(iana::Algorithm::ML_DSA_44) => Ok(Algorithm::MlDsa44), + + coset::Algorithm::Assigned(iana::Algorithm::ML_DSA_65) => Ok(Algorithm::MlDsa65), + + coset::Algorithm::Assigned(iana::Algorithm::ML_DSA_87) => Ok(Algorithm::MlDsa87), + + _ => Err(CoseError::UnsupportedAlgorithm), + } +} diff --git a/crates/cose/src/algorithm/ml_kem.rs b/crates/cose/src/algorithm/ml_kem.rs new file mode 100644 index 0000000..0401450 --- /dev/null +++ b/crates/cose/src/algorithm/ml_kem.rs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +/// ReallyMe private-use COSE algorithm identifier for direct ML-KEM-512. +pub const REALLYME_COSE_ALG_ML_KEM_512: i64 = -65_537; +/// ReallyMe private-use COSE algorithm identifier for direct ML-KEM-768. +pub const REALLYME_COSE_ALG_ML_KEM_768: i64 = -65_538; +/// ReallyMe private-use COSE algorithm identifier for direct ML-KEM-1024. +pub const REALLYME_COSE_ALG_ML_KEM_1024: i64 = -65_539; +/// ReallyMe private-use COSE algorithm identifier for ML-KEM-512+A128KW. +pub const REALLYME_COSE_ALG_ML_KEM_512_A128KW: i64 = -65_540; +/// ReallyMe private-use COSE algorithm identifier for ML-KEM-768+A192KW. +pub const REALLYME_COSE_ALG_ML_KEM_768_A192KW: i64 = -65_541; +/// ReallyMe private-use COSE algorithm identifier for ML-KEM-1024+A256KW. +pub const REALLYME_COSE_ALG_ML_KEM_1024_A256KW: i64 = -65_542; + +/// ReallyMe private-use COSE header label carrying the ML-KEM ciphertext. +/// +/// The active COSE-HPKE draft assumes `-4` for `ek`, but that allocation is +/// not final. ReallyMe uses a private-use label so emitted objects cannot be +/// silently reinterpreted if the final IANA assignment differs. +pub const REALLYME_COSE_HEADER_EK: i64 = -65_543; diff --git a/crates/cose/src/algorithm/mod.rs b/crates/cose/src/algorithm/mod.rs new file mode 100644 index 0000000..48d9a4e --- /dev/null +++ b/crates/cose/src/algorithm/mod.rs @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! COSE algorithm mapping helpers. + +#[cfg(feature = "cose-crypto")] +mod map_from_cose; +mod ml_kem; + +#[cfg(feature = "cose-crypto")] +pub(crate) use map_from_cose::algorithm_from_cose_alg; +pub use ml_kem::{ + REALLYME_COSE_ALG_ML_KEM_1024, REALLYME_COSE_ALG_ML_KEM_1024_A256KW, + REALLYME_COSE_ALG_ML_KEM_512, REALLYME_COSE_ALG_ML_KEM_512_A128KW, + REALLYME_COSE_ALG_ML_KEM_768, REALLYME_COSE_ALG_ML_KEM_768_A192KW, REALLYME_COSE_HEADER_EK, +}; diff --git a/crates/cose/src/encode_cbor.rs b/crates/cose/src/encode_cbor.rs new file mode 100644 index 0000000..b10571b --- /dev/null +++ b/crates/cose/src/encode_cbor.rs @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Fixed-allocation encoding for sensitive CBOR values. + +use std::io::Cursor; + +use ciborium::value::Value; +#[cfg(feature = "cose-crypto")] +use coset::AsCborValue; +use zeroize::Zeroizing; + +use crate::zeroize_coset::SensitiveCborValue; +use crate::CoseError; + +const MAX_CBOR_HEAD_BYTES: usize = 9; + +/// Encode a sensitive CBOR tree without permitting output-buffer growth. +/// +/// A checked recursive upper bound sizes the sole output allocation. Encoding +/// then writes into a fixed-size slice, so a serializer or dependency change +/// cannot leave stale sensitive prefixes in a freed allocation. The CBOR tree +/// is recursively wiped on every exit path. +pub(crate) fn encode_cbor_value(value: Value) -> Result>, CoseError> { + let sensitive = SensitiveCborValue::from_value(value); + let capacity = encoded_upper_bound(sensitive.value())?; + + let mut encoded = Zeroizing::new(Vec::new()); + encoded + .try_reserve_exact(capacity) + .map_err(|_| CoseError::ResourceLimitExceeded)?; + encoded.resize(capacity, 0); + + let written = { + let mut writer = Cursor::new(encoded.as_mut_slice()); + ciborium::ser::into_writer(sensitive.value(), &mut writer).map_err(|_| CoseError::Cbor)?; + usize::try_from(writer.position()).map_err(|_| CoseError::ResourceLimitExceeded)? + }; + if written > capacity { + return Err(CoseError::Cbor); + } + encoded.truncate(written); + + Ok(encoded) +} + +fn encoded_upper_bound(value: &Value) -> Result { + match value { + Value::Bytes(bytes) => upper_bound_for_bytes(bytes.len()), + Value::Text(text) => upper_bound_for_bytes(text.len()), + Value::Array(values) => { + let mut size = MAX_CBOR_HEAD_BYTES; + for value in values { + size = size + .checked_add(encoded_upper_bound(value)?) + .ok_or(CoseError::ResourceLimitExceeded)?; + } + Ok(size) + } + Value::Map(entries) => { + let mut size = MAX_CBOR_HEAD_BYTES; + for (key, value) in entries { + size = size + .checked_add(encoded_upper_bound(key)?) + .ok_or(CoseError::ResourceLimitExceeded)?; + size = size + .checked_add(encoded_upper_bound(value)?) + .ok_or(CoseError::ResourceLimitExceeded)?; + } + Ok(size) + } + Value::Tag(_, tagged) => MAX_CBOR_HEAD_BYTES + .checked_add(encoded_upper_bound(tagged)?) + .ok_or(CoseError::ResourceLimitExceeded), + Value::Integer(_) | Value::Float(_) => Ok(MAX_CBOR_HEAD_BYTES), + Value::Bool(_) | Value::Null => Ok(1), + // `ciborium::Value` is non-exhaustive. Dependency upgrades must review + // the maximum encoding size of new variants before they are accepted. + _ => Err(CoseError::Cbor), + } +} + +fn upper_bound_for_bytes(length: usize) -> Result { + MAX_CBOR_HEAD_BYTES + .checked_add(length) + .ok_or(CoseError::ResourceLimitExceeded) +} + +/// Return the authenticated protected-header bytes under a zeroizing owner. +#[cfg(feature = "cose-crypto")] +pub(crate) fn encode_protected_header( + protected: &coset::ProtectedHeader, +) -> Result>, CoseError> { + if let Some(original) = &protected.original_data { + return Ok(Zeroizing::new(original.clone())); + } + + // Converting explicitly keeps the cloned header under a recursive wipe + // owner. `CborSerializable::to_vec` would drop its intermediate tree + // without clearing identifiers or extension bytes. + let value = protected + .header + .clone() + .to_cbor_value() + .map_err(|_| CoseError::Cbor)?; + encode_cbor_value(value) +} + +#[cfg(all(test, not(target_arch = "wasm32")))] +#[path = "encode_cbor_tests.rs"] +mod tests; diff --git a/crates/cose/src/encode_cbor_tests.rs b/crates/cose/src/encode_cbor_tests.rs new file mode 100644 index 0000000..e0d4099 --- /dev/null +++ b/crates/cose/src/encode_cbor_tests.rs @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use allocation_counter::measure; +use ciborium::value::Value; + +use super::encode_cbor_value; +use crate::CoseError; + +#[test] +fn sensitive_cbor_output_uses_exactly_one_allocation() -> Result<(), CoseError> { + let value = Value::Array(vec![ + Value::Bytes(vec![0x41; 4_096]), + Value::Bytes(vec![0x53; 4_627]), + ]); + let mut result = None; + + // Initialize the counter's thread-local state before measuring the encoder. + let _ = measure(|| {}); + let allocations = measure(|| { + result = Some(encode_cbor_value(value)); + }); + let encoded = result.ok_or(CoseError::Cbor)??; + let capacity_u64 = + u64::try_from(encoded.capacity()).map_err(|_| CoseError::ResourceLimitExceeded)?; + + // The input tree is consumed and freed inside the measured closure, so net + // allocation fields include those deallocations. Total allocations still + // isolates the property under test: only the final output buffer allocates. + assert_eq!(allocations.count_total, 1); + assert_eq!(allocations.count_max, 1); + assert_eq!(allocations.bytes_total, capacity_u64); + Ok(()) +} diff --git a/crates/cose/src/encrypt/codec.rs b/crates/cose/src/encrypt/codec.rs new file mode 100644 index 0000000..03f9f85 --- /dev/null +++ b/crates/cose/src/encrypt/codec.rs @@ -0,0 +1,373 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use ciborium::value::Value; +use coset::{ + iana, AsCborValue, CoseEncrypt, CoseRecipient, Header, Label, ProtectedHeader, + RegisteredLabelWithPrivate, +}; +use zeroize::Zeroizing; + +use crate::algorithm::REALLYME_COSE_HEADER_EK; +use crate::encode_cbor::encode_cbor_value; +use crate::limits::MAX_COSE_ENCRYPT_BYTES; +use crate::zeroize_coset::{zeroize_cose_encrypt, SensitiveCborValue}; +use crate::CoseError; + +use super::profile::ML_KEM_KID_LENGTH; + +pub(crate) const COSE_ENCRYPT_TAG: u64 = 96; +pub(crate) const MAX_PLAINTEXT_BYTES: usize = 1_048_576; +pub(crate) const MAX_EXTERNAL_AAD_BYTES: usize = 1_048_576; +pub(crate) const MAX_KID_BYTES: usize = 1_024; +pub(crate) const MAX_SUPP_PRIV_INFO_BYTES: usize = 65_536; +pub(crate) const AES_GCM_NONCE_LENGTH: usize = 12; +pub(crate) const AES_GCM_TAG_LENGTH: usize = 16; + +pub(crate) fn protected_header( + algorithm: RegisteredLabelWithPrivate, + kid: Option<&[u8]>, +) -> ProtectedHeader { + ProtectedHeader { + original_data: None, + header: Header { + alg: Some(algorithm), + key_id: kid.map(<[u8]>::to_vec).unwrap_or_default(), + ..Header::default() + }, + } +} + +pub(crate) fn recipient_unprotected(encapsulated_key: Vec) -> Header { + Header { + rest: vec![( + Label::Int(REALLYME_COSE_HEADER_EK), + Value::Bytes(encapsulated_key), + )], + ..Header::default() + } +} + +pub(crate) fn body_unprotected(iv: &[u8]) -> Header { + Header { + iv: iv.to_vec(), + ..Header::default() + } +} + +pub(crate) fn encode(cose: CoseEncrypt) -> Result>, CoseError> { + let mut value = cose.to_cbor_value().map_err(|_| CoseError::Cbor)?; + value = Value::Tag(COSE_ENCRYPT_TAG, Box::new(value)); + let encoded = encode_cbor_value(value)?; + if encoded.len() > MAX_COSE_ENCRYPT_BYTES { + return Err(CoseError::ResourceLimitExceeded); + } + Ok(encoded) +} + +pub(crate) fn decode(bytes: &[u8]) -> Result { + let decoded = SensitiveCborValue::decode_cose_encrypt(bytes)?; + let body = match decoded.value() { + Value::Tag(COSE_ENCRYPT_TAG, body) => body.as_ref(), + value => value, + }; + let items = match body { + Value::Array(items) if items.len() == 4 => items, + _ => return Err(CoseError::InvalidFormat), + }; + + // Establish the recursive wipe owner before any payload, ciphertext, + // identifier, or protected-header bytes are cloned out of the decoded + // tree. Rejected semantic parses therefore clear both owners. + let mut sensitive = SensitiveCoseEncrypt { + inner: CoseEncrypt::default(), + }; + decode_protected_header( + &items[0], + &mut sensitive.inner.protected, + EncryptHeaderBucket::BodyProtected, + )?; + decode_encrypt_header( + &items[1], + &mut sensitive.inner.unprotected, + EncryptHeaderBucket::BodyUnprotected, + )?; + sensitive.inner.ciphertext = decode_optional_bytes(&items[2])?; + + let recipients = match &items[3] { + Value::Array(recipients) if recipients.len() == 1 => recipients, + _ => return Err(CoseError::InvalidRecipient), + }; + let recipient_items = match &recipients[0] { + Value::Array(items) if items.len() == 3 => items, + _ => return Err(CoseError::InvalidRecipient), + }; + sensitive.inner.recipients.push(CoseRecipient::default()); + let recipient = sensitive + .inner + .recipients + .first_mut() + .ok_or(CoseError::InvalidRecipient)?; + decode_protected_header( + &recipient_items[0], + &mut recipient.protected, + EncryptHeaderBucket::RecipientProtected, + )?; + decode_encrypt_header( + &recipient_items[1], + &mut recipient.unprotected, + EncryptHeaderBucket::RecipientUnprotected, + )?; + recipient.ciphertext = decode_optional_bytes(&recipient_items[2])?; + + Ok(sensitive) +} + +#[derive(Clone, Copy)] +enum EncryptHeaderBucket { + BodyProtected, + BodyUnprotected, + RecipientProtected, + RecipientUnprotected, +} + +fn decode_protected_header( + value: &Value, + protected: &mut ProtectedHeader, + bucket: EncryptHeaderBucket, +) -> Result<(), CoseError> { + let bytes = match value { + Value::Bytes(bytes) => bytes, + _ => return Err(CoseError::InvalidFormat), + }; + protected.original_data = Some(bytes.clone()); + if bytes.is_empty() { + return Ok(()); + } + + let decoded = SensitiveCborValue::decode_protected_header(bytes)?; + decode_encrypt_header(decoded.value(), &mut protected.header, bucket) +} + +fn decode_encrypt_header( + value: &Value, + header: &mut Header, + bucket: EncryptHeaderBucket, +) -> Result<(), CoseError> { + let entries = match value { + Value::Map(entries) => entries, + _ => return Err(CoseError::InvalidFormat), + }; + let mut saw_algorithm = false; + let mut saw_kid = false; + let mut saw_iv = false; + let mut saw_encapsulated_key = false; + + for (label, value) in entries { + let label = match label { + Value::Integer(integer) => { + i64::try_from(*integer).map_err(|_| CoseError::InvalidFormat)? + } + _ => return Err(header_bucket_error(bucket)), + }; + + if label == iana::HeaderParameter::Alg as i64 { + if saw_algorithm { + return Err(CoseError::DuplicateMapLabel); + } + saw_algorithm = true; + if matches!( + bucket, + EncryptHeaderBucket::BodyUnprotected | EncryptHeaderBucket::RecipientUnprotected + ) { + return Err(CoseError::UnprotectedHeaderNotAllowed); + } + header.alg = Some(parse_header_algorithm(value)?); + } else if label == iana::HeaderParameter::Kid as i64 { + if saw_kid { + return Err(CoseError::DuplicateMapLabel); + } + saw_kid = true; + if !matches!(bucket, EncryptHeaderBucket::RecipientProtected) { + return Err(header_bucket_error(bucket)); + } + header.key_id = match value { + Value::Bytes(kid) if !kid.is_empty() => kid.clone(), + _ => return Err(CoseError::InvalidRecipient), + }; + } else if label == iana::HeaderParameter::Iv as i64 { + if saw_iv { + return Err(CoseError::DuplicateMapLabel); + } + saw_iv = true; + if !matches!(bucket, EncryptHeaderBucket::BodyUnprotected) { + return Err(header_bucket_error(bucket)); + } + header.iv = match value { + Value::Bytes(iv) if !iv.is_empty() => iv.clone(), + _ => return Err(CoseError::InvalidIv), + }; + } else if label == REALLYME_COSE_HEADER_EK { + if saw_encapsulated_key { + return Err(CoseError::DuplicateMapLabel); + } + saw_encapsulated_key = true; + if !matches!(bucket, EncryptHeaderBucket::RecipientUnprotected) { + return Err(header_bucket_error(bucket)); + } + let encapsulated_key = match value { + Value::Bytes(encapsulated_key) => encapsulated_key.clone(), + _ => return Err(CoseError::InvalidEncapsulatedKey), + }; + header.rest.push(( + Label::Int(REALLYME_COSE_HEADER_EK), + Value::Bytes(encapsulated_key), + )); + } else { + return Err(header_bucket_error(bucket)); + } + } + Ok(()) +} + +fn parse_header_algorithm( + value: &Value, +) -> Result, CoseError> { + match value { + Value::Integer(integer) => { + RegisteredLabelWithPrivate::from_cbor_value(Value::Integer(*integer)) + .map_err(|_| CoseError::InvalidFormat) + } + Value::Text(text) => Ok(RegisteredLabelWithPrivate::Text(text.clone())), + _ => Err(CoseError::InvalidFormat), + } +} + +fn decode_optional_bytes(value: &Value) -> Result>, CoseError> { + match value { + Value::Bytes(bytes) => Ok(Some(bytes.clone())), + Value::Null => Ok(None), + _ => Err(CoseError::InvalidFormat), + } +} + +const fn header_bucket_error(bucket: EncryptHeaderBucket) -> CoseError { + match bucket { + EncryptHeaderBucket::BodyProtected | EncryptHeaderBucket::BodyUnprotected => { + CoseError::InvalidFormat + } + EncryptHeaderBucket::RecipientProtected | EncryptHeaderBucket::RecipientUnprotected => { + CoseError::InvalidRecipient + } + } +} + +pub(crate) fn validate_structure(cose: &CoseEncrypt) -> Result<(), CoseError> { + if cose.recipients.len() != 1 { + return Err(CoseError::InvalidRecipient); + } + if cose.ciphertext.is_none() { + return Err(CoseError::MissingCiphertext); + } + if cose.unprotected.alg.is_some() || cose.protected.header.alg.is_none() { + return Err(CoseError::UnprotectedHeaderNotAllowed); + } + if cose.unprotected.iv.len() != AES_GCM_NONCE_LENGTH + || !cose.protected.header.iv.is_empty() + || !cose.unprotected.partial_iv.is_empty() + || !cose.protected.header.partial_iv.is_empty() + { + return Err(CoseError::InvalidIv); + } + if !cose.protected.header.key_id.is_empty() + || !cose.unprotected.key_id.is_empty() + || !cose.protected.header.rest.is_empty() + || !cose.unprotected.rest.is_empty() + || !cose.protected.header.crit.is_empty() + || !cose.unprotected.crit.is_empty() + || has_unsupported_profile_headers(&cose.protected.header) + || has_unsupported_profile_headers(&cose.unprotected) + { + return Err(CoseError::InvalidFormat); + } + + let recipient = cose.recipients.first().ok_or(CoseError::InvalidRecipient)?; + validate_recipient(recipient) +} + +fn validate_recipient(recipient: &CoseRecipient) -> Result<(), CoseError> { + if !recipient.recipients.is_empty() { + return Err(CoseError::InvalidRecipient); + } + if recipient.protected.header.alg.is_none() || recipient.unprotected.alg.is_some() { + return Err(CoseError::UnprotectedHeaderNotAllowed); + } + if recipient.protected.header.key_id.len() != ML_KEM_KID_LENGTH + || !recipient.unprotected.key_id.is_empty() + || !recipient.protected.header.iv.is_empty() + || !recipient.unprotected.iv.is_empty() + || !recipient.protected.header.partial_iv.is_empty() + || !recipient.unprotected.partial_iv.is_empty() + || !recipient.protected.header.crit.is_empty() + || !recipient.unprotected.crit.is_empty() + || !recipient.protected.header.rest.is_empty() + || has_unsupported_profile_headers(&recipient.protected.header) + || has_unsupported_profile_headers(&recipient.unprotected) + { + return Err(CoseError::InvalidRecipient); + } + + let mut encapsulated_key_count = 0usize; + for (label, value) in &recipient.unprotected.rest { + if *label != Label::Int(REALLYME_COSE_HEADER_EK) { + return Err(CoseError::InvalidRecipient); + } + if !matches!(value, Value::Bytes(_)) { + return Err(CoseError::InvalidEncapsulatedKey); + } + encapsulated_key_count = encapsulated_key_count + .checked_add(1) + .ok_or(CoseError::ResourceLimitExceeded)?; + } + if encapsulated_key_count != 1 { + return Err(CoseError::MissingEncapsulatedKey); + } + Ok(()) +} + +fn has_unsupported_profile_headers(header: &Header) -> bool { + // The ReallyMe ML-KEM profile does not define content-type processing or + // counter-signature semantics. Accepting either would let callers treat a + // successfully decrypted object as fully processed while authenticated or + // unauthenticated header requirements were silently ignored. + header.content_type.is_some() || !header.counter_signatures.is_empty() +} + +pub(crate) fn encapsulated_key(recipient: &CoseRecipient) -> Result<&[u8], CoseError> { + recipient + .unprotected + .rest + .iter() + .find_map(|(label, value)| { + if *label == Label::Int(REALLYME_COSE_HEADER_EK) { + match value { + Value::Bytes(bytes) => Some(bytes.as_slice()), + _ => None, + } + } else { + None + } + }) + .ok_or(CoseError::MissingEncapsulatedKey) +} + +pub(crate) struct SensitiveCoseEncrypt { + pub(crate) inner: CoseEncrypt, +} + +impl Drop for SensitiveCoseEncrypt { + fn drop(&mut self) { + zeroize_cose_encrypt(&mut self.inner); + } +} diff --git a/crates/cose/src/encrypt/create.rs b/crates/cose/src/encrypt/create.rs new file mode 100644 index 0000000..2917eb2 --- /dev/null +++ b/crates/cose/src/encrypt/create.rs @@ -0,0 +1,331 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use coset::{CoseEncrypt, CoseRecipient, RegisteredLabelWithPrivate}; +use reallyme_crypto::aes::{ + encrypt, encrypt_aes128_gcm, encrypt_aes192_gcm, Aes128GcmEncryptRequest, Aes128GcmKey, + Aes128GcmNonce, Aes192GcmEncryptRequest, Aes192GcmKey, Aes192GcmNonce, Aes256GcmKey, + Aes256GcmNonce, EncryptRequest, +}; +use reallyme_crypto::aes_kw::{ + wrap_key_aes128, wrap_key_aes192, wrap_key_aes256, Aes128KwKek, Aes192KwKek, Aes256KwKek, +}; +use reallyme_crypto::core::RngOutputKind; +use reallyme_crypto::csprng::{generate_aead_nonce_12, OsSecureRandom, SecureRandom}; +use zeroize::{Zeroize, Zeroizing}; + +use crate::encode_cbor::encode_protected_header; +use crate::failure::CoseFailure; +use crate::key::derive_kid_from_ml_kem_public_key; +use crate::CoseError; + +use super::codec::{ + body_unprotected, encode, protected_header, recipient_unprotected, AES_GCM_NONCE_LENGTH, + MAX_EXTERNAL_AAD_BYTES, MAX_KID_BYTES, MAX_PLAINTEXT_BYTES, MAX_SUPP_PRIV_INFO_BYTES, +}; +use super::kdf::{derive_key, enc_structure}; +use super::profile::{content_algorithm_profile, suite_for, MlKemSuite, ML_KEM_KID_LENGTH}; +use super::types::{ + CoseContentEncryptionAlgorithm, CoseMlKemAlgorithm, CoseMlKemEncryptInput, + CoseMlKemEncryptRequest, CoseMlKemMode, +}; + +#[must_use] +pub(crate) struct CoseMlKemEncryptOutput { + cose_encrypt: Zeroizing>, +} + +impl CoseMlKemEncryptOutput { + pub(crate) fn into_zeroizing(self) -> Zeroizing> { + self.cose_encrypt + } +} + +/// Encrypts using the direct ReallyMe ML-KEM COSE profile with empty external AAD. +/// +/// # Errors +/// +/// Returns [`CoseError`] for unsupported algorithms, invalid keys or identifiers, +/// oversized inputs, entropy failure, cryptographic failure, or encoding failure. +pub fn cose_encrypt_ml_kem_direct( + request: &CoseMlKemEncryptRequest<'_>, +) -> Result>, CoseError> { + cose_encrypt_ml_kem_direct_with_external_aad(request, &[]) +} + +/// Encrypts using the direct ReallyMe ML-KEM COSE profile and external AAD. +/// +/// # Errors +/// +/// Returns [`CoseError`] for unsupported algorithms, invalid keys or identifiers, +/// oversized inputs, entropy failure, cryptographic failure, or encoding failure. +pub fn cose_encrypt_ml_kem_direct_with_external_aad( + request: &CoseMlKemEncryptRequest<'_>, + external_aad: &[u8], +) -> Result>, CoseError> { + encrypt_cose_ml_kem_direct(CoseMlKemEncryptInput::new(request, external_aad)) + .map(CoseMlKemEncryptOutput::into_zeroizing) + .map_err(CoseFailure::into_native_error) +} + +/// Encrypts using the AES-KW ReallyMe ML-KEM COSE profile with empty external AAD. +/// +/// # Errors +/// +/// Returns [`CoseError`] for unsupported algorithms, invalid keys or identifiers, +/// oversized inputs, entropy failure, cryptographic failure, or encoding failure. +pub fn cose_encrypt_ml_kem_key_wrap( + request: &CoseMlKemEncryptRequest<'_>, +) -> Result>, CoseError> { + cose_encrypt_ml_kem_key_wrap_with_external_aad(request, &[]) +} + +/// Encrypts using the AES-KW ReallyMe ML-KEM COSE profile and external AAD. +/// +/// # Errors +/// +/// Returns [`CoseError`] for unsupported algorithms, invalid keys or identifiers, +/// oversized inputs, entropy failure, cryptographic failure, or encoding failure. +pub fn cose_encrypt_ml_kem_key_wrap_with_external_aad( + request: &CoseMlKemEncryptRequest<'_>, + external_aad: &[u8], +) -> Result>, CoseError> { + encrypt_cose_ml_kem_key_wrap(CoseMlKemEncryptInput::new(request, external_aad)) + .map(CoseMlKemEncryptOutput::into_zeroizing) + .map_err(CoseFailure::into_native_error) +} + +pub(crate) fn encrypt_cose_ml_kem_direct( + input: CoseMlKemEncryptInput<'_, '_>, +) -> Result { + encrypt_ml_kem(input.request, CoseMlKemMode::Direct, input.external_aad) + .map(|cose_encrypt| CoseMlKemEncryptOutput { cose_encrypt }) + .map_err(CoseFailure::from_encrypt_error) +} + +pub(crate) fn encrypt_cose_ml_kem_key_wrap( + input: CoseMlKemEncryptInput<'_, '_>, +) -> Result { + encrypt_ml_kem(input.request, CoseMlKemMode::KeyWrap, input.external_aad) + .map(|cose_encrypt| CoseMlKemEncryptOutput { cose_encrypt }) + .map_err(CoseFailure::from_encrypt_error) +} + +fn encrypt_ml_kem( + request: &CoseMlKemEncryptRequest<'_>, + mode: CoseMlKemMode, + external_aad: &[u8], +) -> Result>, CoseError> { + validate_request(request, external_aad)?; + let suite = suite_for(request.kem_algorithm, mode)?; + if request.recipient_public_key.len() != suite.public_key_length { + return Err(CoseError::InvalidKeyMaterial); + } + let (encapsulated_key, shared_secret) = encapsulate(suite.kem, request.recipient_public_key)?; + if encapsulated_key.len() != suite.encapsulated_key_length { + return Err(CoseError::Crypto); + } + let expected_kid = Zeroizing::new(derive_kid_from_ml_kem_public_key( + suite.kem.crypto_algorithm(), + request.recipient_public_key, + )?); + if !reallyme_crypto::operations::constant_time::equal( + expected_kid.as_ref(), + request.recipient_kid, + ) { + return Err(CoseError::KidMismatch); + } + + let recipient_protected = protected_header( + RegisteredLabelWithPrivate::PrivateUse(suite.cose_algorithm), + Some(request.recipient_kid), + ); + let recipient_protected_bytes = encode_protected_header(&recipient_protected)?; + let (content_cose_algorithm, _, content_key_length) = + content_algorithm_profile(request.content_algorithm); + + let mut rng = OsSecureRandom; + let (content_key, recipient_ciphertext) = match suite.mode { + CoseMlKemMode::Direct => ( + derive_key( + &shared_secret, + content_cose_algorithm as i64, + content_key_length, + &recipient_protected_bytes, + request.supp_priv_info, + )?, + None, + ), + CoseMlKemMode::KeyWrap => { + let kek_length = suite.kek_length.ok_or(CoseError::UnsupportedAlgorithm)?; + let algorithm_id = suite + .key_wrap_algorithm_id + .ok_or(CoseError::UnsupportedAlgorithm)?; + let kek = derive_key( + &shared_secret, + algorithm_id, + kek_length, + &recipient_protected_bytes, + request.supp_priv_info, + )?; + let mut content_key = Zeroizing::new(vec![0u8; content_key_length]); + rng.fill_secure(&mut content_key, RngOutputKind::Generic) + .map_err(|_| CoseError::Crypto)?; + let wrapped = wrap_content_key(&suite, &kek, &content_key)?; + (content_key, Some(wrapped)) + } + }; + + let nonce = generate_aead_nonce_12(&mut rng).map_err(|_| CoseError::Crypto)?; + let body_protected = protected_header( + RegisteredLabelWithPrivate::Assigned(content_cose_algorithm), + None, + ); + let body_protected_bytes = encode_protected_header(&body_protected)?; + let aad = enc_structure(&body_protected_bytes, external_aad)?; + let ciphertext = encrypt_content( + request.content_algorithm, + &content_key, + nonce.as_bytes(), + &aad, + request.plaintext, + )?; + + let recipient = CoseRecipient { + protected: recipient_protected, + unprotected: recipient_unprotected(encapsulated_key), + ciphertext: recipient_ciphertext, + recipients: Vec::new(), + }; + let cose = CoseEncrypt { + protected: body_protected, + unprotected: body_unprotected(nonce.as_bytes()), + ciphertext: Some(ciphertext), + recipients: vec![recipient], + }; + encode(cose) +} + +fn validate_request( + request: &CoseMlKemEncryptRequest<'_>, + external_aad: &[u8], +) -> Result<(), CoseError> { + if request.plaintext.len() > MAX_PLAINTEXT_BYTES + || external_aad.len() > MAX_EXTERNAL_AAD_BYTES + || request + .supp_priv_info + .is_some_and(|value| value.len() > MAX_SUPP_PRIV_INFO_BYTES) + { + return Err(CoseError::ResourceLimitExceeded); + } + if request.recipient_kid.is_empty() { + return Err(CoseError::MissingKid); + } + if request.recipient_kid.len() > MAX_KID_BYTES { + return Err(CoseError::ResourceLimitExceeded); + } + if request.recipient_kid.len() != ML_KEM_KID_LENGTH { + return Err(CoseError::KidMismatch); + } + Ok(()) +} + +fn encapsulate( + algorithm: CoseMlKemAlgorithm, + public_key: &[u8], +) -> Result<(Vec, Zeroizing>), CoseError> { + match algorithm { + CoseMlKemAlgorithm::MlKem512 => { + reallyme_crypto::ml_kem_512::ml_kem_512_encapsulate(public_key) + } + CoseMlKemAlgorithm::MlKem768 => { + reallyme_crypto::ml_kem_768::ml_kem_768_encapsulate(public_key) + } + CoseMlKemAlgorithm::MlKem1024 => { + reallyme_crypto::ml_kem_1024::ml_kem_1024_encapsulate(public_key) + } + } + .map_err(|error| match error { + reallyme_crypto::core::CryptoError::InvalidKey => CoseError::InvalidKeyMaterial, + _ => CoseError::Crypto, + }) +} + +fn wrap_content_key( + suite: &MlKemSuite, + kek: &[u8], + content_key: &[u8], +) -> Result, CoseError> { + let wrapped = match suite.kem { + CoseMlKemAlgorithm::MlKem512 => { + let key = Aes128KwKek::from_slice(kek).map_err(|_| CoseError::Crypto)?; + wrap_key_aes128(&key, content_key) + } + CoseMlKemAlgorithm::MlKem768 => { + let key = Aes192KwKek::from_slice(kek).map_err(|_| CoseError::Crypto)?; + wrap_key_aes192(&key, content_key) + } + CoseMlKemAlgorithm::MlKem1024 => { + let key = Aes256KwKek::from_slice(kek).map_err(|_| CoseError::Crypto)?; + wrap_key_aes256(&key, content_key) + } + } + .map_err(|_| CoseError::Crypto)?; + Ok(wrapped.into_vec()) +} + +fn encrypt_content( + algorithm: CoseContentEncryptionAlgorithm, + key: &[u8], + nonce: &[u8], + aad: &[u8], + plaintext: &[u8], +) -> Result, CoseError> { + if nonce.len() != AES_GCM_NONCE_LENGTH { + return Err(CoseError::InvalidIv); + } + let result = match algorithm { + CoseContentEncryptionAlgorithm::Aes128Gcm => { + let key = Aes128GcmKey::from_slice(key).map_err(|_| CoseError::Crypto)?; + let nonce = Aes128GcmNonce::from_slice(nonce).map_err(|_| CoseError::InvalidIv)?; + encrypt_aes128_gcm(&Aes128GcmEncryptRequest { + key: &key, + nonce, + aad, + plaintext, + }) + } + CoseContentEncryptionAlgorithm::Aes192Gcm => { + let key = Aes192GcmKey::from_slice(key).map_err(|_| CoseError::Crypto)?; + let nonce = Aes192GcmNonce::from_slice(nonce).map_err(|_| CoseError::InvalidIv)?; + encrypt_aes192_gcm(&Aes192GcmEncryptRequest { + key: &key, + nonce, + aad, + plaintext, + }) + } + CoseContentEncryptionAlgorithm::Aes256Gcm => { + let key = Aes256GcmKey::from_slice(key).map_err(|_| CoseError::Crypto)?; + let nonce = Aes256GcmNonce::from_slice(nonce).map_err(|_| CoseError::InvalidIv)?; + encrypt(&EncryptRequest { + key: &key, + nonce, + aad, + plaintext, + }) + } + } + .map_err(|_| CoseError::Crypto)?; + let mut ciphertext = result.into_vec(); + let expected_length = plaintext + .len() + .checked_add(super::codec::AES_GCM_TAG_LENGTH) + .ok_or(CoseError::ResourceLimitExceeded)?; + if ciphertext.len() != expected_length { + ciphertext.zeroize(); + return Err(CoseError::Crypto); + } + Ok(ciphertext) +} diff --git a/crates/cose/src/encrypt/decrypt.rs b/crates/cose/src/encrypt/decrypt.rs new file mode 100644 index 0000000..ca026cc --- /dev/null +++ b/crates/cose/src/encrypt/decrypt.rs @@ -0,0 +1,324 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use reallyme_crypto::aes::{ + decrypt, decrypt_aes128_gcm, decrypt_aes192_gcm, Aes128GcmDecryptRequest, Aes128GcmKey, + Aes128GcmNonce, Aes192GcmDecryptRequest, Aes192GcmKey, Aes192GcmNonce, Aes256GcmKey, + Aes256GcmNonce, CiphertextWithTag, DecryptRequest, +}; +use reallyme_crypto::aes_kw::{ + unwrap_key_aes128, unwrap_key_aes192, unwrap_key_aes256, Aes128KwKek, Aes192KwKek, Aes256KwKek, +}; +use zeroize::Zeroizing; + +use crate::encode_cbor::encode_protected_header; +use crate::error::{decrypt_error_from_crypto_error, key_unwrap_error_from_crypto_error}; +use crate::failure::CoseFailure; +use crate::key::derive_kid_from_ml_kem_public_key; +use crate::CoseError; + +use super::codec::{ + decode, encapsulated_key, validate_structure, AES_GCM_TAG_LENGTH, MAX_EXTERNAL_AAD_BYTES, + MAX_KID_BYTES, MAX_PLAINTEXT_BYTES, MAX_SUPP_PRIV_INFO_BYTES, +}; +use super::kdf::{derive_key, enc_structure}; +use super::profile::{ + content_algorithm_from_cose, content_algorithm_profile, suite_from_cose_algorithm, MlKemSuite, + ML_KEM_KID_LENGTH, ML_KEM_PRIVATE_SEED_LENGTH, +}; +use super::types::{ + CoseContentEncryptionAlgorithm, CoseMlKemAlgorithm, CoseMlKemDecryptInput, + CoseMlKemDecryptRequest, CoseMlKemMode, DecryptedCoseEncrypt, +}; + +/// Decrypts the ReallyMe ML-KEM COSE profile with empty external AAD. +/// +/// # Errors +/// +/// Returns [`CoseError`] for malformed or oversized COSE, unsupported or +/// misplaced algorithms, key identifier mismatch, invalid key material, +/// AES-KW integrity failure, or content authentication failure. +pub fn cose_decrypt_ml_kem( + request: &CoseMlKemDecryptRequest<'_>, +) -> Result { + cose_decrypt_ml_kem_with_external_aad(request, &[]) +} + +/// Decrypts the ReallyMe ML-KEM COSE profile with caller-supplied external AAD. +/// +/// # Errors +/// +/// Returns [`CoseError`] for malformed or oversized COSE, unsupported or +/// misplaced algorithms, key identifier mismatch, invalid key material, +/// AES-KW integrity failure, or content authentication failure. +pub fn cose_decrypt_ml_kem_with_external_aad( + request: &CoseMlKemDecryptRequest<'_>, + external_aad: &[u8], +) -> Result { + decrypt_cose_ml_kem(CoseMlKemDecryptInput::new(request, external_aad)) + .map_err(CoseFailure::into_native_error) +} + +pub(crate) fn decrypt_cose_ml_kem( + input: CoseMlKemDecryptInput<'_, '_>, +) -> Result { + decrypt_ml_kem(input.request, input.external_aad).map_err(CoseFailure::from_encrypt_error) +} + +fn decrypt_ml_kem( + request: &CoseMlKemDecryptRequest<'_>, + external_aad: &[u8], +) -> Result { + validate_request(request, external_aad)?; + let sensitive = decode(request.cose_encrypt)?; + let cose = &sensitive.inner; + validate_structure(cose)?; + + let body_algorithm = cose + .protected + .header + .alg + .as_ref() + .ok_or(CoseError::UnsupportedAlgorithm)?; + let content_algorithm = content_algorithm_from_cose(body_algorithm)?; + let (content_cose_algorithm, _, content_key_length) = + content_algorithm_profile(content_algorithm); + let recipient = cose.recipients.first().ok_or(CoseError::InvalidRecipient)?; + let recipient_algorithm = recipient + .protected + .header + .alg + .as_ref() + .ok_or(CoseError::UnsupportedAlgorithm)?; + let suite = suite_from_cose_algorithm(recipient_algorithm)?; + + if !reallyme_crypto::operations::constant_time::equal( + &recipient.protected.header.key_id, + request.expected_recipient_kid, + ) { + return Err(CoseError::KidMismatch); + } + let recipient_public_key = + derive_public_key_from_seed(suite.kem, request.recipient_private_key)?; + let derived_kid = Zeroizing::new(derive_kid_from_ml_kem_public_key( + suite.kem.crypto_algorithm(), + &recipient_public_key, + )?); + if !reallyme_crypto::operations::constant_time::equal( + derived_kid.as_ref(), + &recipient.protected.header.key_id, + ) { + return Err(CoseError::KidMismatch); + } + match suite.mode { + CoseMlKemMode::Direct if recipient.ciphertext.is_some() => { + return Err(CoseError::InvalidRecipient); + } + CoseMlKemMode::KeyWrap if recipient.ciphertext.is_none() => { + return Err(CoseError::MissingCiphertext); + } + CoseMlKemMode::Direct | CoseMlKemMode::KeyWrap => {} + } + + let encapsulated_key = encapsulated_key(recipient)?; + if encapsulated_key.len() != suite.encapsulated_key_length { + return Err(CoseError::InvalidEncapsulatedKey); + } + let shared_secret = decapsulate(suite.kem, encapsulated_key, request.recipient_private_key)?; + let recipient_protected = encode_protected_header(&recipient.protected)?; + let content_key = match suite.mode { + CoseMlKemMode::Direct => derive_key( + &shared_secret, + content_cose_algorithm as i64, + content_key_length, + &recipient_protected, + request.supp_priv_info, + )?, + CoseMlKemMode::KeyWrap => { + let kek_length = suite.kek_length.ok_or(CoseError::UnsupportedAlgorithm)?; + let algorithm_id = suite + .key_wrap_algorithm_id + .ok_or(CoseError::UnsupportedAlgorithm)?; + let kek = derive_key( + &shared_secret, + algorithm_id, + kek_length, + &recipient_protected, + request.supp_priv_info, + )?; + let wrapped = recipient + .ciphertext + .as_deref() + .ok_or(CoseError::MissingCiphertext)?; + unwrap_content_key(&suite, &kek, wrapped, content_key_length)? + } + }; + + let body_protected = encode_protected_header(&cose.protected)?; + let aad = enc_structure(&body_protected, external_aad)?; + let ciphertext = cose + .ciphertext + .as_deref() + .ok_or(CoseError::MissingCiphertext)?; + let plaintext = decrypt_content( + content_algorithm, + &content_key, + &cose.unprotected.iv, + &aad, + ciphertext, + )?; + if plaintext.len() > MAX_PLAINTEXT_BYTES { + return Err(CoseError::ResourceLimitExceeded); + } + + Ok(DecryptedCoseEncrypt { + plaintext, + content_algorithm, + kem_algorithm: suite.kem, + mode: suite.mode, + kid: Zeroizing::new(recipient.protected.header.key_id.clone()), + profile: suite.profile, + }) +} + +fn validate_request( + request: &CoseMlKemDecryptRequest<'_>, + external_aad: &[u8], +) -> Result<(), CoseError> { + if request.recipient_private_key.len() != ML_KEM_PRIVATE_SEED_LENGTH { + return Err(CoseError::InvalidKeyMaterial); + } + if request.expected_recipient_kid.is_empty() { + return Err(CoseError::MissingKid); + } + if request.expected_recipient_kid.len() > MAX_KID_BYTES + || external_aad.len() > MAX_EXTERNAL_AAD_BYTES + || request + .supp_priv_info + .is_some_and(|value| value.len() > MAX_SUPP_PRIV_INFO_BYTES) + { + return Err(CoseError::ResourceLimitExceeded); + } + if request.expected_recipient_kid.len() != ML_KEM_KID_LENGTH { + return Err(CoseError::KidMismatch); + } + Ok(()) +} + +fn derive_public_key_from_seed( + algorithm: CoseMlKemAlgorithm, + private_key: &[u8], +) -> Result, CoseError> { + let seed = <&[u8; ML_KEM_PRIVATE_SEED_LENGTH]>::try_from(private_key) + .map_err(|_| CoseError::InvalidKeyMaterial)?; + let (public_key, derived_private_key) = match algorithm { + CoseMlKemAlgorithm::MlKem512 => { + reallyme_crypto::ml_kem_512::generate_ml_kem_512_keypair_from_seed(seed) + } + CoseMlKemAlgorithm::MlKem768 => { + reallyme_crypto::ml_kem_768::generate_ml_kem_768_keypair_from_seed(seed) + } + CoseMlKemAlgorithm::MlKem1024 => { + reallyme_crypto::ml_kem_1024::generate_ml_kem_1024_keypair_from_seed(seed) + } + } + .map_err(|_| CoseError::Crypto)?; + drop(derived_private_key); + Ok(public_key) +} + +fn decapsulate( + algorithm: CoseMlKemAlgorithm, + ciphertext: &[u8], + private_key: &[u8], +) -> Result>, CoseError> { + match algorithm { + CoseMlKemAlgorithm::MlKem512 => { + reallyme_crypto::ml_kem_512::ml_kem_512_decapsulate(ciphertext, private_key) + } + CoseMlKemAlgorithm::MlKem768 => { + reallyme_crypto::ml_kem_768::ml_kem_768_decapsulate(ciphertext, private_key) + } + CoseMlKemAlgorithm::MlKem1024 => { + reallyme_crypto::ml_kem_1024::ml_kem_1024_decapsulate(ciphertext, private_key) + } + } + .map_err(|_| CoseError::Crypto) +} + +fn unwrap_content_key( + suite: &MlKemSuite, + kek: &[u8], + wrapped: &[u8], + expected_length: usize, +) -> Result>, CoseError> { + let unwrapped = match suite.kem { + CoseMlKemAlgorithm::MlKem512 => { + let key = Aes128KwKek::from_slice(kek).map_err(|_| CoseError::Crypto)?; + unwrap_key_aes128(&key, wrapped) + } + CoseMlKemAlgorithm::MlKem768 => { + let key = Aes192KwKek::from_slice(kek).map_err(|_| CoseError::Crypto)?; + unwrap_key_aes192(&key, wrapped) + } + CoseMlKemAlgorithm::MlKem1024 => { + let key = Aes256KwKek::from_slice(kek).map_err(|_| CoseError::Crypto)?; + unwrap_key_aes256(&key, wrapped) + } + } + .map_err(key_unwrap_error_from_crypto_error)?; + if unwrapped.len() != expected_length { + return Err(CoseError::InvalidRecipient); + } + Ok(unwrapped.into_zeroizing()) +} + +fn decrypt_content( + algorithm: CoseContentEncryptionAlgorithm, + key: &[u8], + nonce: &[u8], + aad: &[u8], + ciphertext: &[u8], +) -> Result>, CoseError> { + if ciphertext.len() < AES_GCM_TAG_LENGTH { + return Err(CoseError::InvalidFormat); + } + let ciphertext = + CiphertextWithTag::from_vec(ciphertext.to_vec()).map_err(|_| CoseError::InvalidFormat)?; + let result = match algorithm { + CoseContentEncryptionAlgorithm::Aes128Gcm => { + let key = Aes128GcmKey::from_slice(key).map_err(|_| CoseError::Crypto)?; + let nonce = Aes128GcmNonce::from_slice(nonce).map_err(|_| CoseError::InvalidIv)?; + decrypt_aes128_gcm(&Aes128GcmDecryptRequest { + key: &key, + nonce, + aad, + ciphertext: &ciphertext, + }) + } + CoseContentEncryptionAlgorithm::Aes192Gcm => { + let key = Aes192GcmKey::from_slice(key).map_err(|_| CoseError::Crypto)?; + let nonce = Aes192GcmNonce::from_slice(nonce).map_err(|_| CoseError::InvalidIv)?; + decrypt_aes192_gcm(&Aes192GcmDecryptRequest { + key: &key, + nonce, + aad, + ciphertext: &ciphertext, + }) + } + CoseContentEncryptionAlgorithm::Aes256Gcm => { + let key = Aes256GcmKey::from_slice(key).map_err(|_| CoseError::Crypto)?; + let nonce = Aes256GcmNonce::from_slice(nonce).map_err(|_| CoseError::InvalidIv)?; + decrypt(&DecryptRequest { + key: &key, + nonce, + aad, + ciphertext: &ciphertext, + }) + } + }; + result + .map(Zeroizing::new) + .map_err(decrypt_error_from_crypto_error) +} diff --git a/crates/cose/src/encrypt/kdf.rs b/crates/cose/src/encrypt/kdf.rs new file mode 100644 index 0000000..b7da404 --- /dev/null +++ b/crates/cose/src/encrypt/kdf.rs @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use ciborium::value::{Integer, Value}; +use reallyme_crypto::kmac::{derive_kmac256, Kmac256Key}; +use zeroize::Zeroizing; + +use crate::encode_cbor::encode_cbor_value; +use crate::CoseError; + +pub(crate) fn derive_key( + shared_secret: &[u8], + algorithm_id: i64, + output_length: usize, + recipient_protected: &[u8], + supp_priv_info: Option<&[u8]>, +) -> Result>, CoseError> { + let output_bits = output_length + .checked_mul(8) + .ok_or(CoseError::ResourceLimitExceeded)?; + let output_bits = u64::try_from(output_bits).map_err(|_| CoseError::ResourceLimitExceeded)?; + + // draft-ietf-jose-pqc-kem-06 deliberately removes PartyUInfo and + // PartyVInfo. Retaining only AlgorithmID, SuppPubInfo, and optional + // SuppPrivInfo avoids inventing identities while still binding the + // derived key to the next-layer algorithm and recipient protected header. + let supp_pub_info = Value::Array(vec![ + Value::Integer(Integer::from(output_bits)), + Value::Bytes(recipient_protected.to_vec()), + ]); + let mut context_items = vec![Value::Integer(Integer::from(algorithm_id)), supp_pub_info]; + if let Some(supp_priv_info) = supp_priv_info { + context_items.push(Value::Bytes(supp_priv_info.to_vec())); + } + + let context = encode_cbor_value(Value::Array(context_items))?; + + let key = Kmac256Key::from_slice(shared_secret).map_err(|_| CoseError::Crypto)?; + let derived = + derive_kmac256(&key, &context, &[], output_length).map_err(|_| CoseError::Crypto)?; + Ok(Zeroizing::new(derived.as_bytes().to_vec())) +} + +pub(crate) fn enc_structure( + body_protected: &[u8], + external_aad: &[u8], +) -> Result>, CoseError> { + let value = Value::Array(vec![ + Value::Text("Encrypt".to_owned()), + Value::Bytes(body_protected.to_vec()), + Value::Bytes(external_aad.to_vec()), + ]); + encode_cbor_value(value) +} diff --git a/crates/cose/src/encrypt/mod.rs b/crates/cose/src/encrypt/mod.rs new file mode 100644 index 0000000..e0fda6a --- /dev/null +++ b/crates/cose/src/encrypt/mod.rs @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! ReallyMe ML-KEM profiles for `COSE_Encrypt` and `COSE_Recipient`. + +mod codec; +pub(crate) mod create; +pub(crate) mod decrypt; +mod kdf; +mod profile; +pub(crate) mod types; + +pub use crate::algorithm::{ + REALLYME_COSE_ALG_ML_KEM_1024, REALLYME_COSE_ALG_ML_KEM_1024_A256KW, + REALLYME_COSE_ALG_ML_KEM_512, REALLYME_COSE_ALG_ML_KEM_512_A128KW, + REALLYME_COSE_ALG_ML_KEM_768, REALLYME_COSE_ALG_ML_KEM_768_A192KW, REALLYME_COSE_HEADER_EK, +}; +pub use create::{ + cose_encrypt_ml_kem_direct, cose_encrypt_ml_kem_direct_with_external_aad, + cose_encrypt_ml_kem_key_wrap, cose_encrypt_ml_kem_key_wrap_with_external_aad, +}; +pub use decrypt::{cose_decrypt_ml_kem, cose_decrypt_ml_kem_with_external_aad}; +pub use types::{ + CoseContentEncryptionAlgorithm, CoseMlKemAlgorithm, CoseMlKemDecryptRequest, + CoseMlKemEncryptRequest, CoseMlKemMode, CoseMlKemProfile, DecryptedCoseEncrypt, +}; diff --git a/crates/cose/src/encrypt/profile.rs b/crates/cose/src/encrypt/profile.rs new file mode 100644 index 0000000..30f206c --- /dev/null +++ b/crates/cose/src/encrypt/profile.rs @@ -0,0 +1,171 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use coset::{iana, RegisteredLabelWithPrivate}; +use reallyme_crypto::core::AeadAlgorithm; + +use crate::algorithm::{ + REALLYME_COSE_ALG_ML_KEM_1024, REALLYME_COSE_ALG_ML_KEM_1024_A256KW, + REALLYME_COSE_ALG_ML_KEM_512, REALLYME_COSE_ALG_ML_KEM_512_A128KW, + REALLYME_COSE_ALG_ML_KEM_768, REALLYME_COSE_ALG_ML_KEM_768_A192KW, +}; +use crate::CoseError; + +use super::types::{ + CoseContentEncryptionAlgorithm, CoseMlKemAlgorithm, CoseMlKemMode, CoseMlKemProfile, +}; + +pub(crate) const ML_KEM_PRIVATE_SEED_LENGTH: usize = 64; +pub(crate) const ML_KEM_KID_LENGTH: usize = 32; +pub(crate) const ML_KEM_512_PUBLIC_KEY_LENGTH: usize = 800; +pub(crate) const ML_KEM_768_PUBLIC_KEY_LENGTH: usize = 1_184; +pub(crate) const ML_KEM_1024_PUBLIC_KEY_LENGTH: usize = 1_568; +pub(crate) const ML_KEM_512_CIPHERTEXT_LENGTH: usize = 768; +pub(crate) const ML_KEM_768_CIPHERTEXT_LENGTH: usize = 1_088; +pub(crate) const ML_KEM_1024_CIPHERTEXT_LENGTH: usize = 1_568; + +#[derive(Clone, Copy)] +pub(crate) struct MlKemSuite { + pub(crate) profile: CoseMlKemProfile, + pub(crate) mode: CoseMlKemMode, + pub(crate) kem: CoseMlKemAlgorithm, + pub(crate) cose_algorithm: i64, + pub(crate) kek_length: Option, + pub(crate) key_wrap_algorithm_id: Option, + pub(crate) public_key_length: usize, + pub(crate) encapsulated_key_length: usize, +} + +pub(crate) fn suite_for( + kem: CoseMlKemAlgorithm, + mode: CoseMlKemMode, +) -> Result { + let ( + cose_algorithm, + kek_length, + key_wrap_algorithm_id, + public_key_length, + encapsulated_key_length, + ) = match (kem, mode) { + (CoseMlKemAlgorithm::MlKem512, CoseMlKemMode::Direct) => ( + REALLYME_COSE_ALG_ML_KEM_512, + None, + None, + ML_KEM_512_PUBLIC_KEY_LENGTH, + ML_KEM_512_CIPHERTEXT_LENGTH, + ), + (CoseMlKemAlgorithm::MlKem768, CoseMlKemMode::Direct) => ( + REALLYME_COSE_ALG_ML_KEM_768, + None, + None, + ML_KEM_768_PUBLIC_KEY_LENGTH, + ML_KEM_768_CIPHERTEXT_LENGTH, + ), + (CoseMlKemAlgorithm::MlKem1024, CoseMlKemMode::Direct) => ( + REALLYME_COSE_ALG_ML_KEM_1024, + None, + None, + ML_KEM_1024_PUBLIC_KEY_LENGTH, + ML_KEM_1024_CIPHERTEXT_LENGTH, + ), + (CoseMlKemAlgorithm::MlKem512, CoseMlKemMode::KeyWrap) => ( + REALLYME_COSE_ALG_ML_KEM_512_A128KW, + Some(16), + Some(iana::Algorithm::A128KW as i64), + ML_KEM_512_PUBLIC_KEY_LENGTH, + ML_KEM_512_CIPHERTEXT_LENGTH, + ), + (CoseMlKemAlgorithm::MlKem768, CoseMlKemMode::KeyWrap) => ( + REALLYME_COSE_ALG_ML_KEM_768_A192KW, + Some(24), + Some(iana::Algorithm::A192KW as i64), + ML_KEM_768_PUBLIC_KEY_LENGTH, + ML_KEM_768_CIPHERTEXT_LENGTH, + ), + (CoseMlKemAlgorithm::MlKem1024, CoseMlKemMode::KeyWrap) => ( + REALLYME_COSE_ALG_ML_KEM_1024_A256KW, + Some(32), + Some(iana::Algorithm::A256KW as i64), + ML_KEM_1024_PUBLIC_KEY_LENGTH, + ML_KEM_1024_CIPHERTEXT_LENGTH, + ), + }; + + Ok(MlKemSuite { + profile: CoseMlKemProfile::ReallyMeV1, + mode, + kem, + cose_algorithm, + kek_length, + key_wrap_algorithm_id, + public_key_length, + encapsulated_key_length, + }) +} + +pub(crate) fn suite_from_cose_algorithm( + algorithm: &RegisteredLabelWithPrivate, +) -> Result { + let value = match algorithm { + RegisteredLabelWithPrivate::PrivateUse(value) => *value, + RegisteredLabelWithPrivate::Assigned(_) | RegisteredLabelWithPrivate::Text(_) => { + return Err(CoseError::UnsupportedAlgorithm); + } + }; + + match value { + REALLYME_COSE_ALG_ML_KEM_512 => { + suite_for(CoseMlKemAlgorithm::MlKem512, CoseMlKemMode::Direct) + } + REALLYME_COSE_ALG_ML_KEM_768 => { + suite_for(CoseMlKemAlgorithm::MlKem768, CoseMlKemMode::Direct) + } + REALLYME_COSE_ALG_ML_KEM_1024 => { + suite_for(CoseMlKemAlgorithm::MlKem1024, CoseMlKemMode::Direct) + } + REALLYME_COSE_ALG_ML_KEM_512_A128KW => { + suite_for(CoseMlKemAlgorithm::MlKem512, CoseMlKemMode::KeyWrap) + } + REALLYME_COSE_ALG_ML_KEM_768_A192KW => { + suite_for(CoseMlKemAlgorithm::MlKem768, CoseMlKemMode::KeyWrap) + } + REALLYME_COSE_ALG_ML_KEM_1024_A256KW => { + suite_for(CoseMlKemAlgorithm::MlKem1024, CoseMlKemMode::KeyWrap) + } + _ => Err(CoseError::UnsupportedAlgorithm), + } +} + +pub(crate) fn content_algorithm_profile( + algorithm: CoseContentEncryptionAlgorithm, +) -> (iana::Algorithm, AeadAlgorithm, usize) { + match algorithm { + CoseContentEncryptionAlgorithm::Aes128Gcm => { + (iana::Algorithm::A128GCM, AeadAlgorithm::Aes128Gcm, 16) + } + CoseContentEncryptionAlgorithm::Aes192Gcm => { + (iana::Algorithm::A192GCM, AeadAlgorithm::Aes192Gcm, 24) + } + CoseContentEncryptionAlgorithm::Aes256Gcm => { + (iana::Algorithm::A256GCM, AeadAlgorithm::Aes256Gcm, 32) + } + } +} + +pub(crate) fn content_algorithm_from_cose( + algorithm: &RegisteredLabelWithPrivate, +) -> Result { + match algorithm { + RegisteredLabelWithPrivate::Assigned(iana::Algorithm::A128GCM) => { + Ok(CoseContentEncryptionAlgorithm::Aes128Gcm) + } + RegisteredLabelWithPrivate::Assigned(iana::Algorithm::A192GCM) => { + Ok(CoseContentEncryptionAlgorithm::Aes192Gcm) + } + RegisteredLabelWithPrivate::Assigned(iana::Algorithm::A256GCM) => { + Ok(CoseContentEncryptionAlgorithm::Aes256Gcm) + } + _ => Err(CoseError::UnsupportedAlgorithm), + } +} diff --git a/crates/cose/src/encrypt/types.rs b/crates/cose/src/encrypt/types.rs new file mode 100644 index 0000000..e95d3b1 --- /dev/null +++ b/crates/cose/src/encrypt/types.rs @@ -0,0 +1,206 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use reallyme_crypto::core::Algorithm; +use zeroize::Zeroizing; + +/// ML-KEM parameter sets supported by the ReallyMe COSE profile. +/// +/// This profile-specific selector prevents unrelated crypto algorithms from +/// entering the native encryption API and defers conversion to the broader +/// crypto dispatch enum until the primitive boundary. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CoseMlKemAlgorithm { + /// ML-KEM-512. + MlKem512, + /// ML-KEM-768. + MlKem768, + /// ML-KEM-1024. + MlKem1024, +} + +impl CoseMlKemAlgorithm { + pub(crate) const fn crypto_algorithm(self) -> Algorithm { + match self { + Self::MlKem512 => Algorithm::MlKem512, + Self::MlKem768 => Algorithm::MlKem768, + Self::MlKem1024 => Algorithm::MlKem1024, + } + } +} + +/// Inputs for creating one-recipient ReallyMe ML-KEM `COSE_Encrypt`. +/// +/// The recipient `kid` is mandatory in the ReallyMe profile and is placed in +/// the protected recipient header. This binds key selection to the same bytes +/// that are fed into the COSE KDF context. +#[must_use] +#[non_exhaustive] +pub struct CoseMlKemEncryptRequest<'a> { + /// ML-KEM-512, ML-KEM-768, or ML-KEM-1024. + pub(super) kem_algorithm: CoseMlKemAlgorithm, + /// AES-GCM algorithm used for the content layer. + pub(super) content_algorithm: CoseContentEncryptionAlgorithm, + /// Raw FIPS 203 ML-KEM encapsulation public key. + pub(super) recipient_public_key: &'a [u8], + /// SHA-256 thumbprint of the canonical public COSE_Key for this recipient. + pub(super) recipient_kid: &'a [u8], + /// Plaintext to encrypt. + pub(super) plaintext: &'a [u8], + /// Optional mutually known private KDF context agreed out of band. + pub(super) supp_priv_info: Option<&'a [u8]>, +} + +impl<'a> CoseMlKemEncryptRequest<'a> { + /// Construct a complete bounded ML-KEM encryption request. + pub const fn new( + kem_algorithm: CoseMlKemAlgorithm, + content_algorithm: CoseContentEncryptionAlgorithm, + recipient_public_key: &'a [u8], + recipient_kid: &'a [u8], + plaintext: &'a [u8], + supp_priv_info: Option<&'a [u8]>, + ) -> Self { + Self { + kem_algorithm, + content_algorithm, + recipient_public_key, + recipient_kid, + plaintext, + supp_priv_info, + } + } + + /// Replace the optional mutually known private KDF context. + pub const fn with_supp_priv_info(mut self, supp_priv_info: Option<&'a [u8]>) -> Self { + self.supp_priv_info = supp_priv_info; + self + } +} + +/// Inputs for decrypting one-recipient ReallyMe ML-KEM `COSE_Encrypt`. +#[must_use] +#[non_exhaustive] +pub struct CoseMlKemDecryptRequest<'a> { + /// Tagged or untagged encoded `COSE_Encrypt`. + pub(super) cose_encrypt: &'a [u8], + /// Raw 64-octet FIPS 203 ML-KEM seed `d || z`. + pub(super) recipient_private_key: &'a [u8], + /// Expected canonical public COSE_Key thumbprint for the selected private key. + pub(super) expected_recipient_kid: &'a [u8], + /// Optional mutually known private KDF context agreed out of band. + pub(super) supp_priv_info: Option<&'a [u8]>, +} + +impl<'a> CoseMlKemDecryptRequest<'a> { + /// Construct a complete bounded ML-KEM decryption request. + pub const fn new( + cose_encrypt: &'a [u8], + recipient_private_key: &'a [u8], + expected_recipient_kid: &'a [u8], + supp_priv_info: Option<&'a [u8]>, + ) -> Self { + Self { + cose_encrypt, + recipient_private_key, + expected_recipient_kid, + supp_priv_info, + } + } + + /// Replace the optional mutually known private KDF context. + pub const fn with_supp_priv_info(mut self, supp_priv_info: Option<&'a [u8]>) -> Self { + self.supp_priv_info = supp_priv_info; + self + } +} + +pub(crate) struct CoseMlKemEncryptInput<'request, 'data> { + pub(super) request: &'request CoseMlKemEncryptRequest<'data>, + pub(super) external_aad: &'request [u8], +} + +impl<'request, 'data> CoseMlKemEncryptInput<'request, 'data> { + pub(crate) const fn new( + request: &'request CoseMlKemEncryptRequest<'data>, + external_aad: &'request [u8], + ) -> Self { + Self { + request, + external_aad, + } + } +} + +pub(crate) struct CoseMlKemDecryptInput<'request, 'data> { + pub(super) request: &'request CoseMlKemDecryptRequest<'data>, + pub(super) external_aad: &'request [u8], +} + +impl<'request, 'data> CoseMlKemDecryptInput<'request, 'data> { + pub(crate) const fn new( + request: &'request CoseMlKemDecryptRequest<'data>, + external_aad: &'request [u8], + ) -> Self { + Self { + request, + external_aad, + } + } +} + +/// Content-encryption algorithms supported by the ReallyMe ML-KEM COSE profile. +/// +/// The content cipher is selected independently from the ML-KEM parameter set. +/// Applications seeking aligned strength should pair ML-KEM-512/768/1024 with +/// AES-128/192/256-GCM respectively; a mixed pairing has the strength of its +/// weaker component. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CoseContentEncryptionAlgorithm { + /// AES-128-GCM. + Aes128Gcm, + /// AES-192-GCM. + Aes192Gcm, + /// AES-256-GCM. + Aes256Gcm, +} + +/// ML-KEM key-distribution mode used by a `COSE_Recipient`. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CoseMlKemMode { + /// KMAC256 derives the content-encryption key directly. + Direct, + /// KMAC256 derives an AES-KW key-encryption key that unwraps the CEK. + KeyWrap, +} + +/// Identifier namespace used by a decoded ML-KEM COSE object. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CoseMlKemProfile { + /// Stable ReallyMe private-use identifiers for the pre-IANA profile. + ReallyMeV1, +} + +/// Authenticated plaintext and recipient metadata from `COSE_Encrypt`. +#[must_use] +#[non_exhaustive] +pub struct DecryptedCoseEncrypt { + /// Authenticated plaintext, zeroized on drop. + pub plaintext: Zeroizing>, + /// Content-encryption algorithm from the protected body header. + pub content_algorithm: CoseContentEncryptionAlgorithm, + /// ML-KEM algorithm from the protected recipient header. + pub kem_algorithm: CoseMlKemAlgorithm, + /// Direct or AES-KW recipient mode. + pub mode: CoseMlKemMode, + /// Protected recipient key identifier, zeroized on drop because application + /// key identifiers can contain privacy-sensitive routing metadata. + pub kid: Zeroizing>, + /// Identifier namespace decoded from the recipient algorithm. + pub profile: CoseMlKemProfile, +} diff --git a/crates/cose/src/error.rs b/crates/cose/src/error.rs new file mode 100644 index 0000000..86f6710 --- /dev/null +++ b/crates/cose/src/error.rs @@ -0,0 +1,252 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use thiserror::Error; + +#[cfg(feature = "cose-crypto")] +use reallyme_crypto::core::{ + AeadFailureKind, CryptoError, KeyWrapFailureKind, SignatureFailureKind, +}; +#[cfg(feature = "cose-crypto")] +use reallyme_crypto::dispatch::AlgorithmError; + +/// Error type for COSE encoding, signing, verification, key, and policy operations. +#[non_exhaustive] +#[derive(Debug, Error, PartialEq, Eq)] +pub enum CoseError { + /// CBOR serialization or parsing failed. + #[error("cbor encoding/decoding error")] + Cbor, + + /// The requested algorithm is not supported by the current COSE mapping. + #[error("unsupported algorithm")] + UnsupportedAlgorithm, + + /// A COSE_Sign1 object did not contain an attached payload where one was required. + #[error("missing payload")] + MissingPayload, + + /// Signature verification failed for otherwise well-formed signature bytes. + #[error("invalid signature")] + InvalidSignature, + + /// Signature bytes were not encoded according to the selected COSE + /// algorithm's wire format. + #[error("invalid signature encoding")] + InvalidSignatureEncoding, + + /// The configured cryptographic backend rejected the operation. + #[error("crypto error")] + Crypto, + + /// The requested cryptographic provider is unavailable in this runtime. + #[error("cryptographic provider unavailable")] + ProviderUnavailable, + + /// A Multikey value was malformed or could not be converted safely. + #[error("invalid multikey")] + InvalidMultikey, + + /// Required public or private key bytes were absent. + #[error("missing key material")] + MissingKeyMaterial, + + /// Key bytes were present but did not match the expected COSE key shape. + #[error("invalid key material")] + InvalidKeyMaterial, + + /// A policy required `kid` / key_id but the COSE object did not provide one. + #[error("missing kid")] + MissingKid, + + /// A `kid` was present, but the caller's resolver did not return a key. + #[error("key not resolved")] + KeyNotResolved, + + /// The COSE object is structurally invalid for the requested operation. + #[error("invalid COSE format")] + InvalidFormat, + + /// Encoded input exceeded the crate's deterministic resource limits. + #[error("resource limit exceeded")] + ResourceLimitExceeded, + + /// Encoded input used indefinite-length or otherwise non-canonical CBOR. + #[error("non-canonical CBOR")] + NonCanonicalCbor, + + /// Encoded input used CBOR tags outside this crate's supported profile. + #[error("unexpected CBOR tag")] + UnexpectedCborTag, + + /// Encoded input repeated a CBOR map label where uniqueness is required. + #[error("duplicate CBOR map label")] + DuplicateMapLabel, + + /// A critical protected header was present but is not supported. + #[error("unsupported critical header")] + UnsupportedCriticalHeader, + + /// An unprotected header carried fields that must be integrity protected. + #[error("unprotected header not allowed")] + UnprotectedHeaderNotAllowed, + + /// Private key material was required but absent. + #[error("missing private key material")] + MissingPrivateKey, + + /// A COSE encryption object did not contain an attached ciphertext. + #[error("missing ciphertext")] + MissingCiphertext, + + /// A COSE encryption object used an invalid or misplaced IV. + #[error("invalid IV")] + InvalidIv, + + /// A COSE encryption object did not contain exactly one supported recipient. + #[error("invalid recipient")] + InvalidRecipient, + + /// The recipient did not contain the ML-KEM encapsulated key header. + #[error("missing encapsulated key")] + MissingEncapsulatedKey, + + /// The ML-KEM encapsulated key was malformed for the selected parameter set. + #[error("invalid encapsulated key")] + InvalidEncapsulatedKey, + + /// The authenticated ciphertext, external AAD, or derived key did not verify. + #[error("authentication failed")] + AuthenticationFailed, + + /// AES Key Wrap integrity verification failed for the recipient CEK. + #[error("key unwrap failed")] + KeyUnwrapFailed, + + /// A required key identifier was present but did not match the selected key. + #[error("kid mismatch")] + KidMismatch, +} + +#[cfg(feature = "cose-crypto")] +pub(crate) fn sign_error_from_algorithm_error(error: AlgorithmError) -> CoseError { + match error { + AlgorithmError::UnsupportedAlgorithm(_) + | AlgorithmError::UnsupportedAeadAlgorithm(_) + | AlgorithmError::UnsupportedHashAlgorithm(_) + | AlgorithmError::UnsupportedMacAlgorithm(_) => CoseError::UnsupportedAlgorithm, + AlgorithmError::InvalidKey(_) | AlgorithmError::Crypto(CryptoError::InvalidKey) => { + CoseError::InvalidKeyMaterial + } + AlgorithmError::SignatureInvalid(_) => CoseError::InvalidSignature, + AlgorithmError::Crypto(CryptoError::Signature { kind, .. }) => { + sign_error_from_signature_failure(kind) + } + AlgorithmError::Crypto(_) => CoseError::Crypto, + // Crypto marks this enum non-exhaustive so providers can add precise + // failure modes. Unknown future cases must fail closed without leaking + // backend detail through COSE's public error boundary. + _ => CoseError::Crypto, + } +} + +#[cfg(feature = "cose-crypto")] +pub(crate) fn verify_error_from_algorithm_error(error: AlgorithmError) -> CoseError { + match error { + AlgorithmError::UnsupportedAlgorithm(_) + | AlgorithmError::UnsupportedAeadAlgorithm(_) + | AlgorithmError::UnsupportedHashAlgorithm(_) + | AlgorithmError::UnsupportedMacAlgorithm(_) => CoseError::UnsupportedAlgorithm, + AlgorithmError::InvalidKey(_) | AlgorithmError::Crypto(CryptoError::InvalidKey) => { + CoseError::InvalidKeyMaterial + } + AlgorithmError::SignatureInvalid(_) => CoseError::InvalidSignature, + AlgorithmError::Crypto(CryptoError::Signature { kind, .. }) => { + verify_error_from_signature_failure(kind) + } + AlgorithmError::Crypto(_) => CoseError::Crypto, + // Crypto marks this enum non-exhaustive so providers can add precise + // failure modes. Unknown future cases must fail closed without leaking + // backend detail through COSE's public error boundary. + _ => CoseError::Crypto, + } +} + +#[cfg(feature = "cose-crypto")] +pub(crate) fn decrypt_error_from_crypto_error(error: CryptoError) -> CoseError { + match error { + CryptoError::AeadDecrypt { kind, .. } => match kind { + AeadFailureKind::AuthenticationFailed => CoseError::AuthenticationFailed, + AeadFailureKind::ShortCiphertext => CoseError::InvalidFormat, + AeadFailureKind::LengthOverflow => CoseError::ResourceLimitExceeded, + AeadFailureKind::InvalidKeyMaterial + | AeadFailureKind::InvalidOutputLength + | AeadFailureKind::BackendFailure => CoseError::Crypto, + // New backend failure classes must not be reported as caller + // authentication failures without an explicit COSE decision. + _ => CoseError::Crypto, + }, + CryptoError::InvalidCiphertextLength { .. } => CoseError::InvalidFormat, + CryptoError::InvalidKey => CoseError::Crypto, + _ => CoseError::Crypto, + } +} + +#[cfg(feature = "cose-crypto")] +pub(crate) fn key_unwrap_error_from_crypto_error(error: CryptoError) -> CoseError { + match error { + CryptoError::KeyWrap { kind, .. } => match kind { + KeyWrapFailureKind::IntegrityCheckFailed => CoseError::KeyUnwrapFailed, + KeyWrapFailureKind::InvalidWrappedLength => CoseError::InvalidRecipient, + KeyWrapFailureKind::LengthOverflow => CoseError::ResourceLimitExceeded, + KeyWrapFailureKind::InvalidKekLength + | KeyWrapFailureKind::InvalidPlaintextLength + | KeyWrapFailureKind::BackendFailure => CoseError::Crypto, + // Unknown future key-wrap failures are operational until COSE + // deliberately assigns a narrower public meaning. + _ => CoseError::Crypto, + }, + _ => CoseError::Crypto, + } +} + +#[cfg(feature = "cose-crypto")] +fn sign_error_from_signature_failure(kind: SignatureFailureKind) -> CoseError { + match kind { + SignatureFailureKind::InvalidPrivateKey + | SignatureFailureKind::InvalidPublicKey + | SignatureFailureKind::InvalidMessage + | SignatureFailureKind::SecureEnclaveRejectedKey => CoseError::InvalidKeyMaterial, + SignatureFailureKind::InvalidSignature => CoseError::InvalidSignature, + SignatureFailureKind::SecureEnclaveUnavailable => CoseError::ProviderUnavailable, + SignatureFailureKind::BackendFailure | SignatureFailureKind::KeyGenerationFailed => { + CoseError::Crypto + } + // Future provider/backend signature failures are operational crypto + // failures unless COSE deliberately classifies them more narrowly. + _ => CoseError::Crypto, + } +} + +#[cfg(feature = "cose-crypto")] +fn verify_error_from_signature_failure(kind: SignatureFailureKind) -> CoseError { + match kind { + SignatureFailureKind::InvalidPublicKey + | SignatureFailureKind::InvalidPrivateKey + | SignatureFailureKind::InvalidMessage + | SignatureFailureKind::SecureEnclaveRejectedKey => CoseError::InvalidKeyMaterial, + SignatureFailureKind::InvalidSignature => CoseError::InvalidSignature, + SignatureFailureKind::SecureEnclaveUnavailable => CoseError::ProviderUnavailable, + SignatureFailureKind::BackendFailure | SignatureFailureKind::KeyGenerationFailed => { + CoseError::Crypto + } + // Future provider/backend signature failures are operational crypto + // failures unless COSE deliberately classifies them more narrowly. + _ => CoseError::Crypto, + } +} + +#[cfg(all(test, feature = "cose-crypto"))] +#[path = "error_tests.rs"] +mod tests; diff --git a/crates/cose/src/error_tests.rs b/crates/cose/src/error_tests.rs new file mode 100644 index 0000000..a951b6c --- /dev/null +++ b/crates/cose/src/error_tests.rs @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use reallyme_crypto::core::{ + AeadBackend, AeadFailureKind, CryptoError, KeyWrapAlgorithm, KeyWrapFailureKind, + KeyWrapOperation, +}; + +use super::{decrypt_error_from_crypto_error, key_unwrap_error_from_crypto_error, CoseError}; + +#[test] +fn decrypt_error_mapping_preserves_authentication_and_backend_semantics() { + let authentication = CryptoError::AeadDecrypt { + backend: AeadBackend::Native, + kind: AeadFailureKind::AuthenticationFailed, + }; + let backend = CryptoError::AeadDecrypt { + backend: AeadBackend::Native, + kind: AeadFailureKind::BackendFailure, + }; + + assert_eq!( + decrypt_error_from_crypto_error(authentication), + CoseError::AuthenticationFailed, + ); + assert_eq!(decrypt_error_from_crypto_error(backend), CoseError::Crypto); +} + +#[test] +fn key_unwrap_error_mapping_separates_shape_integrity_and_backend_failures() { + let error = |kind| CryptoError::KeyWrap { + algorithm: KeyWrapAlgorithm::Aes256Kw, + operation: KeyWrapOperation::Unwrap, + kind, + }; + + assert_eq!( + key_unwrap_error_from_crypto_error(error(KeyWrapFailureKind::IntegrityCheckFailed)), + CoseError::KeyUnwrapFailed, + ); + assert_eq!( + key_unwrap_error_from_crypto_error(error(KeyWrapFailureKind::InvalidWrappedLength)), + CoseError::InvalidRecipient, + ); + assert_eq!( + key_unwrap_error_from_crypto_error(error(KeyWrapFailureKind::BackendFailure)), + CoseError::Crypto, + ); +} diff --git a/crates/cose/src/failure.rs b/crates/cose/src/failure.rs new file mode 100644 index 0000000..7837a4a --- /dev/null +++ b/crates/cose/src/failure.rs @@ -0,0 +1,257 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Canonical internal COSE failure classification. + +use crate::CoseError; + +/// Component responsible for a failed COSE operation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum CoseFailureOrigin { + /// Caller-controlled input or COSE semantic validation failed. + Caller, + /// Algorithm selection or provider availability failed. + Provider, + /// Cryptographic backend or internal execution failed. + Backend, +} + +/// Structured external error branch selected for a failure. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum CoseFailureBranch { + /// COSE input, structure, key material, or authentication semantics failed. + Primitive, + /// Provider selection or availability failed. + Provider, + /// Backend execution failed without caller-safe detail. + Backend, +} + +/// Exact, allocation-free semantic reason retained across COSE boundaries. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum CoseFailureReason { + CommonCbor, + CommonUnsupportedAlgorithm, + Sign1MissingPayload, + Sign1InvalidSignature, + Sign1InvalidSignatureEncoding, + CommonCryptoFailed, + ProviderUnavailable, + MultikeyInvalidMultikey, + KeyMissingKeyMaterial, + KeyInvalidKeyMaterial, + Sign1MissingKid, + Sign1KeyNotResolved, + CommonInvalidFormat, + CommonResourceLimitExceeded, + CommonNonCanonicalCbor, + CommonUnexpectedCborTag, + CommonDuplicateMapLabel, + Sign1UnsupportedCriticalHeader, + Sign1UnprotectedHeaderNotAllowed, + Sign1MissingPrivateKey, + #[cfg(feature = "wire")] + Sign1KidKeyMismatch, + EncryptMissingCiphertext, + EncryptInvalidIv, + EncryptInvalidRecipient, + EncryptMissingEncapsulatedKey, + EncryptInvalidEncapsulatedKey, + EncryptAuthenticationFailed, + EncryptKeyUnwrapFailed, + EncryptKidMismatch, + #[cfg(feature = "cose-crypto")] + EncryptMissingKid, + #[cfg(feature = "cose-crypto")] + EncryptUnprotectedHeaderNotAllowed, +} + +/// Canonical internal failure shared by semantic functions and adapters. +/// +/// Fields remain private so invalid origin/branch combinations cannot be +/// assembled outside the canonical constructors. The type carries only fixed +/// enums and therefore cannot retain secrets, PII, raw input, or backend text. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct CoseFailure { + origin: CoseFailureOrigin, + branch: CoseFailureBranch, + reason: CoseFailureReason, +} + +impl CoseFailure { + #[cfg(any(feature = "wire", test))] + pub(crate) const fn origin(&self) -> CoseFailureOrigin { + self.origin + } + + #[cfg(any(feature = "wire", test))] + pub(crate) const fn branch(&self) -> CoseFailureBranch { + self.branch + } + + #[cfg(any(feature = "wire", test))] + pub(crate) const fn reason(&self) -> CoseFailureReason { + self.reason + } + + /// Converts a semantic failure to the existing public native error. + /// + /// Every current semantic reason is safe for native exposure because the + /// public error variants contain no dynamic context. Boundary-only reasons + /// such as malformed protobuf are introduced separately by adapters and + /// are not represented as semantic reasons. + pub(crate) const fn into_native_error(self) -> CoseError { + match self.reason { + CoseFailureReason::CommonCbor => CoseError::Cbor, + CoseFailureReason::CommonUnsupportedAlgorithm => CoseError::UnsupportedAlgorithm, + CoseFailureReason::Sign1MissingPayload => CoseError::MissingPayload, + CoseFailureReason::Sign1InvalidSignature => CoseError::InvalidSignature, + CoseFailureReason::Sign1InvalidSignatureEncoding => CoseError::InvalidSignatureEncoding, + CoseFailureReason::CommonCryptoFailed => CoseError::Crypto, + CoseFailureReason::ProviderUnavailable => CoseError::ProviderUnavailable, + CoseFailureReason::MultikeyInvalidMultikey => CoseError::InvalidMultikey, + CoseFailureReason::KeyMissingKeyMaterial => CoseError::MissingKeyMaterial, + CoseFailureReason::KeyInvalidKeyMaterial => CoseError::InvalidKeyMaterial, + CoseFailureReason::Sign1MissingKid => CoseError::MissingKid, + CoseFailureReason::Sign1KeyNotResolved => CoseError::KeyNotResolved, + CoseFailureReason::CommonInvalidFormat => CoseError::InvalidFormat, + CoseFailureReason::CommonResourceLimitExceeded => CoseError::ResourceLimitExceeded, + CoseFailureReason::CommonNonCanonicalCbor => CoseError::NonCanonicalCbor, + CoseFailureReason::CommonUnexpectedCborTag => CoseError::UnexpectedCborTag, + CoseFailureReason::CommonDuplicateMapLabel => CoseError::DuplicateMapLabel, + CoseFailureReason::Sign1UnsupportedCriticalHeader => { + CoseError::UnsupportedCriticalHeader + } + CoseFailureReason::Sign1UnprotectedHeaderNotAllowed => { + CoseError::UnprotectedHeaderNotAllowed + } + CoseFailureReason::Sign1MissingPrivateKey => CoseError::MissingPrivateKey, + // The native resolver API has no separate expected-kid input. This + // semantic reason is therefore reachable only from structured + // adapters and degrades to the stable unresolved-key variant + // if a future native adapter elects to expose the same operation. + #[cfg(feature = "wire")] + CoseFailureReason::Sign1KidKeyMismatch => CoseError::KeyNotResolved, + CoseFailureReason::EncryptMissingCiphertext => CoseError::MissingCiphertext, + CoseFailureReason::EncryptInvalidIv => CoseError::InvalidIv, + CoseFailureReason::EncryptInvalidRecipient => CoseError::InvalidRecipient, + CoseFailureReason::EncryptMissingEncapsulatedKey => CoseError::MissingEncapsulatedKey, + CoseFailureReason::EncryptInvalidEncapsulatedKey => CoseError::InvalidEncapsulatedKey, + CoseFailureReason::EncryptAuthenticationFailed => CoseError::AuthenticationFailed, + CoseFailureReason::EncryptKeyUnwrapFailed => CoseError::KeyUnwrapFailed, + CoseFailureReason::EncryptKidMismatch => CoseError::KidMismatch, + #[cfg(feature = "cose-crypto")] + CoseFailureReason::EncryptMissingKid => CoseError::MissingKid, + #[cfg(feature = "cose-crypto")] + CoseFailureReason::EncryptUnprotectedHeaderNotAllowed => { + CoseError::UnprotectedHeaderNotAllowed + } + } + } + + const fn caller(reason: CoseFailureReason) -> Self { + Self { + origin: CoseFailureOrigin::Caller, + branch: CoseFailureBranch::Primitive, + reason, + } + } + + const fn provider(reason: CoseFailureReason) -> Self { + Self { + origin: CoseFailureOrigin::Provider, + branch: CoseFailureBranch::Provider, + reason, + } + } + + const fn backend(reason: CoseFailureReason) -> Self { + Self { + origin: CoseFailureOrigin::Backend, + branch: CoseFailureBranch::Backend, + reason, + } + } + + #[cfg(feature = "wire")] + pub(crate) const fn sign1_kid_key_mismatch() -> Self { + Self::caller(CoseFailureReason::Sign1KidKeyMismatch) + } + + #[cfg(feature = "cose-crypto")] + pub(crate) fn from_encrypt_error(error: CoseError) -> Self { + match error { + CoseError::MissingKid => Self::caller(CoseFailureReason::EncryptMissingKid), + CoseError::UnprotectedHeaderNotAllowed => { + Self::caller(CoseFailureReason::EncryptUnprotectedHeaderNotAllowed) + } + other => Self::from(other), + } + } +} + +impl From for CoseFailure { + fn from(error: CoseError) -> Self { + match error { + CoseError::Cbor => Self::caller(CoseFailureReason::CommonCbor), + CoseError::UnsupportedAlgorithm => { + Self::provider(CoseFailureReason::CommonUnsupportedAlgorithm) + } + CoseError::MissingPayload => Self::caller(CoseFailureReason::Sign1MissingPayload), + CoseError::InvalidSignature => Self::caller(CoseFailureReason::Sign1InvalidSignature), + CoseError::InvalidSignatureEncoding => { + Self::caller(CoseFailureReason::Sign1InvalidSignatureEncoding) + } + CoseError::Crypto => Self::backend(CoseFailureReason::CommonCryptoFailed), + CoseError::ProviderUnavailable => { + Self::provider(CoseFailureReason::ProviderUnavailable) + } + CoseError::InvalidMultikey => Self::caller(CoseFailureReason::MultikeyInvalidMultikey), + CoseError::MissingKeyMaterial => Self::caller(CoseFailureReason::KeyMissingKeyMaterial), + CoseError::InvalidKeyMaterial => Self::caller(CoseFailureReason::KeyInvalidKeyMaterial), + CoseError::MissingKid => Self::caller(CoseFailureReason::Sign1MissingKid), + CoseError::KeyNotResolved => Self::caller(CoseFailureReason::Sign1KeyNotResolved), + CoseError::InvalidFormat => Self::caller(CoseFailureReason::CommonInvalidFormat), + CoseError::ResourceLimitExceeded => { + Self::caller(CoseFailureReason::CommonResourceLimitExceeded) + } + CoseError::NonCanonicalCbor => Self::caller(CoseFailureReason::CommonNonCanonicalCbor), + CoseError::UnexpectedCborTag => { + Self::caller(CoseFailureReason::CommonUnexpectedCborTag) + } + CoseError::DuplicateMapLabel => { + Self::caller(CoseFailureReason::CommonDuplicateMapLabel) + } + CoseError::UnsupportedCriticalHeader => { + Self::caller(CoseFailureReason::Sign1UnsupportedCriticalHeader) + } + CoseError::UnprotectedHeaderNotAllowed => { + Self::caller(CoseFailureReason::Sign1UnprotectedHeaderNotAllowed) + } + CoseError::MissingPrivateKey => Self::caller(CoseFailureReason::Sign1MissingPrivateKey), + CoseError::MissingCiphertext => { + Self::caller(CoseFailureReason::EncryptMissingCiphertext) + } + CoseError::InvalidIv => Self::caller(CoseFailureReason::EncryptInvalidIv), + CoseError::InvalidRecipient => Self::caller(CoseFailureReason::EncryptInvalidRecipient), + CoseError::MissingEncapsulatedKey => { + Self::caller(CoseFailureReason::EncryptMissingEncapsulatedKey) + } + CoseError::InvalidEncapsulatedKey => { + Self::caller(CoseFailureReason::EncryptInvalidEncapsulatedKey) + } + CoseError::AuthenticationFailed => { + Self::caller(CoseFailureReason::EncryptAuthenticationFailed) + } + CoseError::KeyUnwrapFailed => Self::caller(CoseFailureReason::EncryptKeyUnwrapFailed), + CoseError::KidMismatch => Self::caller(CoseFailureReason::EncryptKidMismatch), + } + } +} + +impl From for CoseError { + fn from(failure: CoseFailure) -> Self { + failure.into_native_error() + } +} diff --git a/crates/cose/src/failure_tests.rs b/crates/cose/src/failure_tests.rs new file mode 100644 index 0000000..8751b19 --- /dev/null +++ b/crates/cose/src/failure_tests.rs @@ -0,0 +1,198 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use crate::failure::{CoseFailure, CoseFailureBranch, CoseFailureOrigin, CoseFailureReason}; +use crate::CoseError; + +#[test] +fn every_native_error_has_a_lossless_canonical_failure() { + for (error, origin, branch, reason) in native_error_cases() { + let failure = CoseFailure::from(error); + + assert_eq!(failure.origin(), origin); + assert_eq!(failure.branch(), branch); + assert_eq!(failure.reason(), reason); + + let remapped = CoseFailure::from(failure.into_native_error()); + assert_eq!(remapped, failure); + } +} + +fn native_error_cases() -> Vec<( + CoseError, + CoseFailureOrigin, + CoseFailureBranch, + CoseFailureReason, +)> { + use CoseError as Error; + use CoseFailureBranch::{Backend as BackendBranch, Primitive, Provider as ProviderBranch}; + use CoseFailureOrigin::{Backend, Caller, Provider}; + use CoseFailureReason as Reason; + + vec![ + (Error::Cbor, Caller, Primitive, Reason::CommonCbor), + ( + Error::UnsupportedAlgorithm, + Provider, + ProviderBranch, + Reason::CommonUnsupportedAlgorithm, + ), + ( + Error::MissingPayload, + Caller, + Primitive, + Reason::Sign1MissingPayload, + ), + ( + Error::InvalidSignature, + Caller, + Primitive, + Reason::Sign1InvalidSignature, + ), + ( + Error::InvalidSignatureEncoding, + Caller, + Primitive, + Reason::Sign1InvalidSignatureEncoding, + ), + ( + Error::Crypto, + Backend, + BackendBranch, + Reason::CommonCryptoFailed, + ), + ( + Error::ProviderUnavailable, + Provider, + ProviderBranch, + Reason::ProviderUnavailable, + ), + ( + Error::InvalidMultikey, + Caller, + Primitive, + Reason::MultikeyInvalidMultikey, + ), + ( + Error::MissingKeyMaterial, + Caller, + Primitive, + Reason::KeyMissingKeyMaterial, + ), + ( + Error::InvalidKeyMaterial, + Caller, + Primitive, + Reason::KeyInvalidKeyMaterial, + ), + ( + Error::MissingKid, + Caller, + Primitive, + Reason::Sign1MissingKid, + ), + ( + Error::KeyNotResolved, + Caller, + Primitive, + Reason::Sign1KeyNotResolved, + ), + ( + Error::InvalidFormat, + Caller, + Primitive, + Reason::CommonInvalidFormat, + ), + ( + Error::ResourceLimitExceeded, + Caller, + Primitive, + Reason::CommonResourceLimitExceeded, + ), + ( + Error::NonCanonicalCbor, + Caller, + Primitive, + Reason::CommonNonCanonicalCbor, + ), + ( + Error::UnexpectedCborTag, + Caller, + Primitive, + Reason::CommonUnexpectedCborTag, + ), + ( + Error::DuplicateMapLabel, + Caller, + Primitive, + Reason::CommonDuplicateMapLabel, + ), + ( + Error::UnsupportedCriticalHeader, + Caller, + Primitive, + Reason::Sign1UnsupportedCriticalHeader, + ), + ( + Error::UnprotectedHeaderNotAllowed, + Caller, + Primitive, + Reason::Sign1UnprotectedHeaderNotAllowed, + ), + ( + Error::MissingPrivateKey, + Caller, + Primitive, + Reason::Sign1MissingPrivateKey, + ), + ( + Error::MissingCiphertext, + Caller, + Primitive, + Reason::EncryptMissingCiphertext, + ), + ( + Error::InvalidIv, + Caller, + Primitive, + Reason::EncryptInvalidIv, + ), + ( + Error::InvalidRecipient, + Caller, + Primitive, + Reason::EncryptInvalidRecipient, + ), + ( + Error::MissingEncapsulatedKey, + Caller, + Primitive, + Reason::EncryptMissingEncapsulatedKey, + ), + ( + Error::InvalidEncapsulatedKey, + Caller, + Primitive, + Reason::EncryptInvalidEncapsulatedKey, + ), + ( + Error::AuthenticationFailed, + Caller, + Primitive, + Reason::EncryptAuthenticationFailed, + ), + ( + Error::KeyUnwrapFailed, + Caller, + Primitive, + Reason::EncryptKeyUnwrapFailed, + ), + ( + Error::KidMismatch, + Caller, + Primitive, + Reason::EncryptKidMismatch, + ), + ] +} diff --git a/crates/cose/src/key/akp.rs b/crates/cose/src/key/akp.rs new file mode 100644 index 0000000..1296b34 --- /dev/null +++ b/crates/cose/src/key/akp.rs @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Additional-key-type COSE_Key profiles for ML-DSA and ML-KEM. + +use ciborium::value::Value; +use coset::{iana, CoseKeyBuilder, MlDsaVariant, RegisteredLabelWithPrivate}; +use reallyme_crypto::core::Algorithm; + +use crate::algorithm::{ + REALLYME_COSE_ALG_ML_KEM_1024, REALLYME_COSE_ALG_ML_KEM_512, REALLYME_COSE_ALG_ML_KEM_768, +}; +use crate::CoseError; + +const ML_DSA_PRIVATE_SEED_BYTES: usize = 32; +const ML_DSA_44_PUBLIC_KEY_BYTES: usize = 1_312; +const ML_DSA_65_PUBLIC_KEY_BYTES: usize = 1_952; +const ML_DSA_87_PUBLIC_KEY_BYTES: usize = 2_592; +const ML_KEM_PRIVATE_SEED_BYTES: usize = 64; +const ML_KEM_512_PUBLIC_KEY_BYTES: usize = 800; +const ML_KEM_768_PUBLIC_KEY_BYTES: usize = 1_184; +const ML_KEM_1024_PUBLIC_KEY_BYTES: usize = 1_568; + +#[derive(Clone, Copy)] +pub(crate) struct AkpProfile { + pub(crate) algorithm: Algorithm, + pub(crate) public_key_len: usize, + pub(crate) private_key_len: usize, + pub(crate) is_signature: bool, +} + +pub(crate) fn akp_profile(algorithm: Algorithm) -> Result { + match algorithm { + Algorithm::MlDsa44 => Ok(AkpProfile { + algorithm, + public_key_len: ML_DSA_44_PUBLIC_KEY_BYTES, + private_key_len: ML_DSA_PRIVATE_SEED_BYTES, + is_signature: true, + }), + Algorithm::MlDsa65 => Ok(AkpProfile { + algorithm, + public_key_len: ML_DSA_65_PUBLIC_KEY_BYTES, + private_key_len: ML_DSA_PRIVATE_SEED_BYTES, + is_signature: true, + }), + Algorithm::MlDsa87 => Ok(AkpProfile { + algorithm, + public_key_len: ML_DSA_87_PUBLIC_KEY_BYTES, + private_key_len: ML_DSA_PRIVATE_SEED_BYTES, + is_signature: true, + }), + Algorithm::MlKem512 => Ok(AkpProfile { + algorithm, + public_key_len: ML_KEM_512_PUBLIC_KEY_BYTES, + private_key_len: ML_KEM_PRIVATE_SEED_BYTES, + is_signature: false, + }), + Algorithm::MlKem768 => Ok(AkpProfile { + algorithm, + public_key_len: ML_KEM_768_PUBLIC_KEY_BYTES, + private_key_len: ML_KEM_PRIVATE_SEED_BYTES, + is_signature: false, + }), + Algorithm::MlKem1024 => Ok(AkpProfile { + algorithm, + public_key_len: ML_KEM_1024_PUBLIC_KEY_BYTES, + private_key_len: ML_KEM_PRIVATE_SEED_BYTES, + is_signature: false, + }), + _ => Err(CoseError::UnsupportedAlgorithm), + } +} + +pub(crate) fn akp_profile_from_cose_algorithm( + algorithm: &RegisteredLabelWithPrivate, +) -> Result { + match algorithm { + RegisteredLabelWithPrivate::Assigned(iana::Algorithm::ML_DSA_44) => { + akp_profile(Algorithm::MlDsa44) + } + RegisteredLabelWithPrivate::Assigned(iana::Algorithm::ML_DSA_65) => { + akp_profile(Algorithm::MlDsa65) + } + RegisteredLabelWithPrivate::Assigned(iana::Algorithm::ML_DSA_87) => { + akp_profile(Algorithm::MlDsa87) + } + RegisteredLabelWithPrivate::PrivateUse(REALLYME_COSE_ALG_ML_KEM_512) => { + akp_profile(Algorithm::MlKem512) + } + RegisteredLabelWithPrivate::PrivateUse(REALLYME_COSE_ALG_ML_KEM_768) => { + akp_profile(Algorithm::MlKem768) + } + RegisteredLabelWithPrivate::PrivateUse(REALLYME_COSE_ALG_ML_KEM_1024) => { + akp_profile(Algorithm::MlKem1024) + } + _ => Err(CoseError::UnsupportedAlgorithm), + } +} + +pub(crate) const fn algorithm_for_akp_profile(profile: AkpProfile) -> Algorithm { + profile.algorithm +} + +pub(crate) fn akp_key( + algorithm: Algorithm, + public_key: &[u8], + private_key: Option<&[u8]>, +) -> Result { + if matches!( + algorithm, + Algorithm::MlDsa44 | Algorithm::MlDsa65 | Algorithm::MlDsa87 + ) { + let mut builder = + CoseKeyBuilder::new_mldsa_pub_key(ml_dsa_variant(algorithm)?, public_key.to_vec()); + if let Some(private_key) = private_key { + builder = builder.param( + iana::AkpKeyParameter::Priv as i64, + Value::Bytes(private_key.to_vec()), + ); + } + return Ok(builder.build()); + } + + let mut builder = CoseKeyBuilder::new_okp_key() + .key_type(iana::KeyType::AKP) + .param( + iana::AkpKeyParameter::Pub as i64, + Value::Bytes(public_key.to_vec()), + ); + if let Some(private_key) = private_key { + builder = builder.param( + iana::AkpKeyParameter::Priv as i64, + Value::Bytes(private_key.to_vec()), + ); + } + let mut key = builder.build(); + key.alg = Some(RegisteredLabelWithPrivate::PrivateUse( + ml_kem_cose_algorithm(algorithm)?, + )); + Ok(key) +} + +fn ml_dsa_variant(algorithm: Algorithm) -> Result { + match algorithm { + Algorithm::MlDsa44 => Ok(MlDsaVariant::MlDsa44), + Algorithm::MlDsa65 => Ok(MlDsaVariant::MlDsa65), + Algorithm::MlDsa87 => Ok(MlDsaVariant::MlDsa87), + _ => Err(CoseError::UnsupportedAlgorithm), + } +} + +fn ml_kem_cose_algorithm(algorithm: Algorithm) -> Result { + match algorithm { + Algorithm::MlKem512 => Ok(REALLYME_COSE_ALG_ML_KEM_512), + Algorithm::MlKem768 => Ok(REALLYME_COSE_ALG_ML_KEM_768), + Algorithm::MlKem1024 => Ok(REALLYME_COSE_ALG_ML_KEM_1024), + _ => Err(CoseError::UnsupportedAlgorithm), + } +} diff --git a/crates/cose/src/key/convert.rs b/crates/cose/src/key/convert.rs new file mode 100644 index 0000000..b8cd30b --- /dev/null +++ b/crates/cose/src/key/convert.rs @@ -0,0 +1,441 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Raw-byte construction, extraction, and canonical encoding for COSE_Key. + +use ciborium::value::Value; +use coset::{iana, AsCborValue, CoseKeyBuilder}; +use reallyme_crypto::core::Algorithm; +use zeroize::Zeroizing; + +use crate::encode_cbor::encode_cbor_value; +use crate::failure::CoseFailure; +use crate::limits::validate_cose_key_bytes; +use crate::{CoseError, CoseKey}; + +use super::akp::{akp_key, akp_profile, algorithm_for_akp_profile}; +use super::ec::{ + algorithm_for_ec2_profile, canonical_ec_public_key, ec2_profile, ec2_public_bytes_from_key, + ec2_public_key_builder, +}; +use super::profile::{ + get_param_bytes, validate_cose_key_profile, KeyProfile, ED25519_PUBLIC_KEY_BYTES, + ED25519_SECRET_KEY_BYTES, X25519_PUBLIC_KEY_BYTES, +}; +use super::validate_material::{validate_private_public_pair, validate_public_key}; + +pub(crate) struct CoseKeyFromPublicBytesInput<'a> { + algorithm: Algorithm, + public_key: &'a [u8], +} + +impl<'a> CoseKeyFromPublicBytesInput<'a> { + pub(crate) const fn new(algorithm: Algorithm, public_key: &'a [u8]) -> Self { + Self { + algorithm, + public_key, + } + } +} + +pub(crate) struct CoseKeyFromPrivateBytesInput<'a> { + algorithm: Algorithm, + private_key: &'a [u8], + public_key: Option<&'a [u8]>, +} + +impl<'a> CoseKeyFromPrivateBytesInput<'a> { + pub(crate) const fn new( + algorithm: Algorithm, + private_key: &'a [u8], + public_key: Option<&'a [u8]>, + ) -> Self { + Self { + algorithm, + private_key, + public_key, + } + } +} + +#[must_use] +pub(crate) struct CoseKeyOwnerOutput { + key: CoseKey, +} + +impl CoseKeyOwnerOutput { + pub(crate) fn into_key(self) -> CoseKey { + self.key + } +} + +pub(crate) struct CoseKeyRefInput<'a> { + key: &'a CoseKey, +} + +impl<'a> CoseKeyRefInput<'a> { + pub(crate) const fn new(key: &'a CoseKey) -> Self { + Self { key } + } + + pub(crate) const fn key(&self) -> &CoseKey { + self.key + } +} + +#[must_use] +pub(crate) struct CoseKeyBytesOutput { + bytes: Zeroizing>, +} + +impl CoseKeyBytesOutput { + pub(crate) fn into_zeroizing(self) -> Zeroizing> { + self.bytes + } + + pub(crate) fn into_vec(mut self) -> Vec { + // The established native public-key API returns `Vec`. This is the + // single deliberate escape from the semantic wipe owner; adapters keep + // the zeroizing form and never call this compatibility conversion. + core::mem::take(&mut self.bytes) + } +} + +pub(crate) fn construct_cose_key_from_public( + input: CoseKeyFromPublicBytesInput<'_>, +) -> Result { + construct_cose_key_from_public_impl(input.algorithm, input.public_key) + .map(|key| CoseKeyOwnerOutput { key }) + .map_err(CoseFailure::from) +} + +pub(crate) fn construct_cose_key_from_private( + input: CoseKeyFromPrivateBytesInput<'_>, +) -> Result { + construct_cose_key_from_private_impl(input.algorithm, input.private_key, input.public_key) + .map(|key| CoseKeyOwnerOutput { key }) + .map_err(CoseFailure::from) +} + +pub(crate) fn encode_cose_key( + input: CoseKeyRefInput<'_>, +) -> Result { + encode_cose_key_impl(input.key) + .map(|bytes| CoseKeyBytesOutput { bytes }) + .map_err(CoseFailure::from) +} + +pub(crate) fn extract_cose_key_public( + input: CoseKeyRefInput<'_>, +) -> Result { + extract_cose_key_public_impl(input.key) + .map(Zeroizing::new) + .map(|bytes| CoseKeyBytesOutput { bytes }) + .map_err(CoseFailure::from) +} + +pub(crate) fn extract_cose_key_private( + input: CoseKeyRefInput<'_>, +) -> Result { + extract_cose_key_private_impl(input.key) + .map(|bytes| CoseKeyBytesOutput { bytes }) + .map_err(CoseFailure::from) +} + +/// Encode a COSE_Key to canonical CBOR bytes. +/// +/// The returned buffer zeroizes on drop because a validated [`CoseKey`] may +/// contain private parameters. +/// +/// # Errors +/// +/// Returns [`CoseError`] when the key profile or material is invalid, or when +/// canonical CBOR serialization or post-serialization validation fails. +pub fn cose_key_to_vec(key: &CoseKey) -> Result>, CoseError> { + encode_cose_key(CoseKeyRefInput::new(key)) + .map(CoseKeyBytesOutput::into_zeroizing) + .map_err(CoseFailure::into_native_error) +} + +fn encode_cose_key_impl(key: &CoseKey) -> Result>, CoseError> { + validate_cose_key_profile(key)?; + encode_cose_key_value(key) +} + +#[cfg(feature = "cose-crypto")] +pub(crate) fn canonical_ml_kem_public_key_bytes( + algorithm: Algorithm, + public_key: &[u8], +) -> Result>, CoseError> { + let profile = akp_profile(algorithm)?; + if profile.is_signature || public_key.len() != profile.public_key_len { + return Err(CoseError::InvalidKeyMaterial); + } + + let key = CoseKey::new(akp_key(algorithm, public_key, None)?); + encode_cose_key_value(&key) +} + +fn encode_cose_key_value(key: &CoseKey) -> Result>, CoseError> { + let value = key + .inner() + .clone() + .to_cbor_value() + .map_err(|_| CoseError::Cbor)?; + let encoded = encode_cbor_value(value)?; + validate_cose_key_bytes(&encoded)?; + Ok(encoded) +} + +/// Build a COSE_Key from raw public key bytes. +/// +/// # Errors +/// +/// Returns [`CoseError`] when the algorithm lacks a supported COSE_Key mapping +/// or the public key has an invalid length, encoding, point, or backend shape. +pub fn cose_key_from_public_bytes( + algorithm: Algorithm, + public_key: &[u8], +) -> Result { + construct_cose_key_from_public(CoseKeyFromPublicBytesInput::new(algorithm, public_key)) + .map(CoseKeyOwnerOutput::into_key) + .map_err(CoseFailure::into_native_error) +} + +fn construct_cose_key_from_public_impl( + algorithm: Algorithm, + public_key: &[u8], +) -> Result { + match algorithm { + Algorithm::Ed25519 => { + if public_key.len() != ED25519_PUBLIC_KEY_BYTES { + return Err(CoseError::InvalidKeyMaterial); + } + validate_public_key(algorithm, public_key)?; + Ok(CoseKey::new( + CoseKeyBuilder::new_okp_key() + .param( + iana::OkpKeyParameter::Crv as i64, + Value::Integer((iana::EllipticCurve::Ed25519 as i64).into()), + ) + .param( + iana::OkpKeyParameter::X as i64, + Value::Bytes(public_key.to_vec()), + ) + .algorithm(iana::Algorithm::Ed25519) + .build(), + )) + } + Algorithm::X25519 => { + if public_key.len() != X25519_PUBLIC_KEY_BYTES { + return Err(CoseError::InvalidKeyMaterial); + } + validate_public_key(algorithm, public_key)?; + Ok(CoseKey::new( + CoseKeyBuilder::new_okp_key() + .param( + iana::OkpKeyParameter::Crv as i64, + Value::Integer((iana::EllipticCurve::X25519 as i64).into()), + ) + .param( + iana::OkpKeyParameter::X as i64, + Value::Bytes(public_key.to_vec()), + ) + .build(), + )) + } + Algorithm::P256 | Algorithm::P384 | Algorithm::P521 | Algorithm::Secp256k1 => { + let profile = ec2_profile(algorithm)?; + let canonical = canonical_ec_public_key(profile, public_key)?; + Ok(CoseKey::new( + ec2_public_key_builder(profile, &canonical)? + .algorithm(profile.alg) + .build(), + )) + } + Algorithm::MlDsa44 + | Algorithm::MlDsa65 + | Algorithm::MlDsa87 + | Algorithm::MlKem512 + | Algorithm::MlKem768 + | Algorithm::MlKem1024 => { + let profile = akp_profile(algorithm)?; + if public_key.len() != profile.public_key_len { + return Err(CoseError::InvalidKeyMaterial); + } + validate_public_key(algorithm, public_key)?; + Ok(CoseKey::new(akp_key(algorithm, public_key, None)?)) + } + _ => Err(CoseError::UnsupportedAlgorithm), + } +} + +/// Extract raw public key bytes from a COSE_Key. +/// +/// # Errors +/// +/// Returns [`CoseError`] when the key profile, algorithm, parameters, lengths, +/// curve point, or public key material is missing or invalid. +pub fn cose_key_to_public_bytes(key: &CoseKey) -> Result, CoseError> { + extract_cose_key_public(CoseKeyRefInput::new(key)) + .map(CoseKeyBytesOutput::into_vec) + .map_err(CoseFailure::into_native_error) +} + +fn extract_cose_key_public_impl(key: &CoseKey) -> Result, CoseError> { + match validate_cose_key_profile(key)? { + KeyProfile::Okp(profile) => { + let public_key = get_param_bytes(key.inner(), iana::OkpKeyParameter::X as i64) + .ok_or(CoseError::MissingKeyMaterial)?; + if public_key.len() != profile.coordinate_len { + return Err(CoseError::InvalidKeyMaterial); + } + let algorithm = if profile.alg == Some(iana::Algorithm::Ed25519) { + Algorithm::Ed25519 + } else { + Algorithm::X25519 + }; + validate_public_key(algorithm, public_key)?; + Ok(public_key.to_vec()) + } + KeyProfile::Ec2(profile) => { + let public_key = ec2_public_bytes_from_key(key.inner(), profile)?; + validate_public_key(algorithm_for_ec2_profile(profile)?, &public_key)?; + Ok(public_key) + } + KeyProfile::Akp(profile) => { + let public_key = get_param_bytes(key.inner(), iana::AkpKeyParameter::Pub as i64) + .ok_or(CoseError::MissingKeyMaterial)?; + if public_key.len() != profile.public_key_len { + return Err(CoseError::InvalidKeyMaterial); + } + validate_public_key(algorithm_for_akp_profile(profile), public_key)?; + Ok(public_key.to_vec()) + } + } +} + +/// Build a COSE_Key from raw private key bytes and its public binding. +/// +/// # Errors +/// +/// Returns [`CoseError`] when private or public material is missing, malformed, +/// unsupported, or not bound to the supplied private key. +pub fn cose_key_from_private_bytes( + algorithm: Algorithm, + private_key: &[u8], + public_key: Option<&[u8]>, +) -> Result { + construct_cose_key_from_private(CoseKeyFromPrivateBytesInput::new( + algorithm, + private_key, + public_key, + )) + .map(CoseKeyOwnerOutput::into_key) + .map_err(CoseFailure::into_native_error) +} + +fn construct_cose_key_from_private_impl( + algorithm: Algorithm, + private_key: &[u8], + public_key: Option<&[u8]>, +) -> Result { + if private_key.is_empty() { + return Err(CoseError::MissingPrivateKey); + } + + match algorithm { + Algorithm::Ed25519 => { + if private_key.len() != ED25519_SECRET_KEY_BYTES { + return Err(CoseError::InvalidKeyMaterial); + } + let public_key = public_key.ok_or(CoseError::MissingKeyMaterial)?; + if public_key.len() != ED25519_PUBLIC_KEY_BYTES { + return Err(CoseError::InvalidKeyMaterial); + } + validate_private_public_pair(algorithm, private_key, public_key)?; + + // RFC 8949 bytewise key order places -2 (`x`) before -4 (`d`). + // Building in that order keeps the accepted in-memory key directly + // serializable by the canonical encoder. + let key = CoseKeyBuilder::new_okp_key() + .param( + iana::OkpKeyParameter::Crv as i64, + Value::Integer((iana::EllipticCurve::Ed25519 as i64).into()), + ) + .param( + iana::OkpKeyParameter::X as i64, + Value::Bytes(public_key.to_vec()), + ) + .param( + iana::OkpKeyParameter::D as i64, + Value::Bytes(private_key.to_vec()), + ) + .algorithm(iana::Algorithm::Ed25519) + .build(); + Ok(CoseKey::new(key)) + } + Algorithm::P256 | Algorithm::P384 | Algorithm::P521 | Algorithm::Secp256k1 => { + let profile = ec2_profile(algorithm)?; + if private_key.len() != profile.coordinate_len { + return Err(CoseError::InvalidKeyMaterial); + } + let public_key = public_key.ok_or(CoseError::MissingKeyMaterial)?; + let canonical = canonical_ec_public_key(profile, public_key)?; + validate_private_public_pair(algorithm, private_key, &canonical)?; + let key = ec2_public_key_builder(profile, &canonical)? + .param( + iana::Ec2KeyParameter::D as i64, + Value::Bytes(private_key.to_vec()), + ) + .algorithm(profile.alg) + .build(); + Ok(CoseKey::new(key)) + } + Algorithm::MlDsa44 + | Algorithm::MlDsa65 + | Algorithm::MlDsa87 + | Algorithm::MlKem512 + | Algorithm::MlKem768 + | Algorithm::MlKem1024 => { + let profile = akp_profile(algorithm)?; + let public_key = public_key.ok_or(CoseError::MissingKeyMaterial)?; + if private_key.len() != profile.private_key_len + || public_key.len() != profile.public_key_len + { + return Err(CoseError::InvalidKeyMaterial); + } + validate_private_public_pair(algorithm, private_key, public_key)?; + Ok(CoseKey::new(akp_key( + algorithm, + public_key, + Some(private_key), + )?)) + } + _ => Err(CoseError::UnsupportedAlgorithm), + } +} + +/// Extract raw private key bytes from a COSE_Key. +/// +/// # Errors +/// +/// Returns [`CoseError`] when the profile is invalid or private material is +/// absent. +pub fn cose_key_to_private_bytes(key: &CoseKey) -> Result>, CoseError> { + extract_cose_key_private(CoseKeyRefInput::new(key)) + .map(CoseKeyBytesOutput::into_zeroizing) + .map_err(CoseFailure::into_native_error) +} + +fn extract_cose_key_private_impl(key: &CoseKey) -> Result>, CoseError> { + let private_label = match validate_cose_key_profile(key)? { + KeyProfile::Okp(_) => iana::OkpKeyParameter::D as i64, + KeyProfile::Ec2(_) => iana::Ec2KeyParameter::D as i64, + KeyProfile::Akp(_) => iana::AkpKeyParameter::Priv as i64, + }; + let private_key = + get_param_bytes(key.inner(), private_label).ok_or(CoseError::MissingKeyMaterial)?; + Ok(Zeroizing::new(private_key.to_vec())) +} diff --git a/crates/cose/src/key/derive_kid.rs b/crates/cose/src/key/derive_kid.rs new file mode 100644 index 0000000..ce8185f --- /dev/null +++ b/crates/cose/src/key/derive_kid.rs @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use crate::failure::CoseFailure; +#[cfg(feature = "cose-crypto")] +use crate::key::convert::canonical_ml_kem_public_key_bytes; +use crate::key::convert::{ + construct_cose_key_from_public, encode_cose_key, extract_cose_key_public, + CoseKeyFromPublicBytesInput, CoseKeyRefInput, +}; +use crate::key::profile::algorithm_for_cose_key; +use crate::{CoseError, CoseKey}; +use reallyme_codec::cbor::sha2_256_content_hash; +#[cfg(feature = "cose-crypto")] +use zeroize::Zeroize; +use zeroize::Zeroizing; + +#[must_use] +pub(crate) struct CoseKeyKidOutput { + kid: Zeroizing>, +} + +impl CoseKeyKidOutput { + pub(crate) fn into_zeroizing(self) -> Zeroizing> { + self.kid + } +} + +pub(crate) fn derive_cose_key_public_kid( + input: CoseKeyRefInput<'_>, +) -> Result { + let algorithm = algorithm_for_cose_key(input.key()).map_err(CoseFailure::from)?; + let public_bytes = extract_cose_key_public(input)?.into_zeroizing(); + let public_key = + construct_cose_key_from_public(CoseKeyFromPublicBytesInput::new(algorithm, &public_bytes))? + .into_key(); + let canonical = encode_cose_key(CoseKeyRefInput::new(&public_key))?.into_zeroizing(); + Ok(CoseKeyKidOutput { + kid: Zeroizing::new(sha2_256_content_hash(&canonical).to_vec()), + }) +} + +/// Derive `kid = SHA-256(canonical public COSE_Key)`. +/// +/// The public key is validated and normalized before encoding, so compressed, +/// raw-coordinate, and uncompressed SEC1 inputs for the same EC point produce +/// one identifier. The canonical public key includes its algorithm binding; +/// the same bytes under a different COSE algorithm therefore cannot collide. +/// +/// # Errors +/// +/// Returns [`CoseError`] when the key profile, public material, algorithm +/// binding, point encoding, or canonical CBOR encoding is invalid. +pub fn derive_kid_from_cose_key_public(key: &CoseKey) -> Result>, CoseError> { + derive_cose_key_public_kid(CoseKeyRefInput::new(key)) + .map(CoseKeyKidOutput::into_zeroizing) + .map_err(CoseFailure::into_native_error) +} + +#[cfg(feature = "cose-crypto")] +pub(crate) fn derive_kid_from_ml_kem_public_key( + algorithm: reallyme_crypto::core::Algorithm, + public_key: &[u8], +) -> Result<[u8; 32], CoseError> { + let mut canonical = canonical_ml_kem_public_key_bytes(algorithm, public_key)?; + let kid = sha2_256_content_hash(&canonical); + canonical.zeroize(); + Ok(kid) +} diff --git a/crates/cose/src/key/ec.rs b/crates/cose/src/key/ec.rs new file mode 100644 index 0000000..9497733 --- /dev/null +++ b/crates/cose/src/key/ec.rs @@ -0,0 +1,272 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Elliptic-curve COSE_Key profile and point conversion helpers. + +use coset::{iana, CoseKeyBuilder}; +use reallyme_crypto::core::Algorithm; + +use crate::CoseError; + +use super::profile::{get_param_bytes, get_param_value}; +use super::validate_material::validate_public_key; + +const COMPRESSED_POINT_PREFIX_BYTES: usize = 1; +const COMPRESSED_POINT_EVEN_PREFIX: u8 = 0x02; +const COMPRESSED_POINT_ODD_PREFIX: u8 = 0x03; +const UNCOMPRESSED_POINT_PREFIX: u8 = 0x04; + +pub(crate) const P256_COORDINATE_BYTES: usize = 32; +pub(crate) const P384_COORDINATE_BYTES: usize = 48; +pub(crate) const P521_COORDINATE_BYTES: usize = 66; + +#[derive(Clone, Copy)] +pub(crate) struct Ec2Profile { + pub(crate) curve: iana::EllipticCurve, + pub(crate) alg: iana::Algorithm, + pub(crate) coordinate_len: usize, +} + +pub(crate) fn ec2_profile(algorithm: Algorithm) -> Result { + match algorithm { + Algorithm::P256 => Ok(Ec2Profile { + curve: iana::EllipticCurve::P_256, + alg: iana::Algorithm::ESP256, + coordinate_len: P256_COORDINATE_BYTES, + }), + Algorithm::P384 => Ok(Ec2Profile { + curve: iana::EllipticCurve::P_384, + alg: iana::Algorithm::ESP384, + coordinate_len: P384_COORDINATE_BYTES, + }), + Algorithm::P521 => Ok(Ec2Profile { + curve: iana::EllipticCurve::P_521, + alg: iana::Algorithm::ESP512, + coordinate_len: P521_COORDINATE_BYTES, + }), + Algorithm::Secp256k1 => Ok(Ec2Profile { + curve: iana::EllipticCurve::Secp256k1, + alg: iana::Algorithm::ES256K, + coordinate_len: P256_COORDINATE_BYTES, + }), + _ => Err(CoseError::UnsupportedAlgorithm), + } +} + +pub(crate) fn ec2_profile_from_curve(curve: i64) -> Result { + if curve == iana::EllipticCurve::P_256 as i64 { + return ec2_profile(Algorithm::P256); + } + if curve == iana::EllipticCurve::P_384 as i64 { + return ec2_profile(Algorithm::P384); + } + if curve == iana::EllipticCurve::P_521 as i64 { + return ec2_profile(Algorithm::P521); + } + if curve == iana::EllipticCurve::Secp256k1 as i64 { + return ec2_profile(Algorithm::Secp256k1); + } + Err(CoseError::UnsupportedAlgorithm) +} + +pub(crate) fn ec2_public_key_builder( + profile: Ec2Profile, + public_key: &[u8], +) -> Result { + let compressed_len = compressed_point_len(profile)?; + let raw_len = raw_point_len(profile)?; + let uncompressed_len = raw_len + .checked_add(COMPRESSED_POINT_PREFIX_BYTES) + .ok_or(CoseError::InvalidFormat)?; + + if public_key.len() == compressed_len { + let prefix = public_key + .first() + .copied() + .ok_or(CoseError::InvalidKeyMaterial)?; + if matches!( + prefix, + COMPRESSED_POINT_EVEN_PREFIX | COMPRESSED_POINT_ODD_PREFIX + ) { + let x = public_key + .get(COMPRESSED_POINT_PREFIX_BYTES..compressed_len) + .ok_or(CoseError::InvalidKeyMaterial)? + .to_vec(); + return Ok(CoseKeyBuilder::new_ec2_pub_key_y_sign( + profile.curve, + x, + prefix == COMPRESSED_POINT_ODD_PREFIX, + )); + } + } + + if public_key.len() == raw_len { + let x = public_key + .get(..profile.coordinate_len) + .ok_or(CoseError::InvalidKeyMaterial)? + .to_vec(); + let y = public_key + .get(profile.coordinate_len..raw_len) + .ok_or(CoseError::InvalidKeyMaterial)? + .to_vec(); + return Ok(CoseKeyBuilder::new_ec2_pub_key(profile.curve, x, y)); + } + + if public_key.len() == uncompressed_len + && public_key.first().copied() == Some(UNCOMPRESSED_POINT_PREFIX) + { + let x_start = COMPRESSED_POINT_PREFIX_BYTES; + let y_start = x_start + .checked_add(profile.coordinate_len) + .ok_or(CoseError::InvalidFormat)?; + let x = public_key + .get(x_start..y_start) + .ok_or(CoseError::InvalidKeyMaterial)? + .to_vec(); + let y = public_key + .get(y_start..uncompressed_len) + .ok_or(CoseError::InvalidKeyMaterial)? + .to_vec(); + return Ok(CoseKeyBuilder::new_ec2_pub_key(profile.curve, x, y)); + } + + Err(CoseError::InvalidKeyMaterial) +} + +pub(crate) fn canonical_ec_public_key( + profile: Ec2Profile, + public_key: &[u8], +) -> Result, CoseError> { + let compressed_len = compressed_point_len(profile)?; + let raw_len = raw_point_len(profile)?; + let uncompressed_len = raw_len + .checked_add(COMPRESSED_POINT_PREFIX_BYTES) + .ok_or(CoseError::InvalidFormat)?; + + if public_key.len() == compressed_len + && matches!( + public_key.first().copied(), + Some(COMPRESSED_POINT_EVEN_PREFIX | COMPRESSED_POINT_ODD_PREFIX) + ) + { + validate_supplied_point(profile, public_key, raw_len, uncompressed_len)?; + return Ok(public_key.to_vec()); + } + + let (x, y) = if public_key.len() == raw_len { + ( + public_key + .get(..profile.coordinate_len) + .ok_or(CoseError::InvalidKeyMaterial)?, + public_key + .get(profile.coordinate_len..raw_len) + .ok_or(CoseError::InvalidKeyMaterial)?, + ) + } else if public_key.len() == uncompressed_len + && public_key.first().copied() == Some(UNCOMPRESSED_POINT_PREFIX) + { + let x_start = COMPRESSED_POINT_PREFIX_BYTES; + let y_start = x_start + .checked_add(profile.coordinate_len) + .ok_or(CoseError::InvalidFormat)?; + ( + public_key + .get(x_start..y_start) + .ok_or(CoseError::InvalidKeyMaterial)?, + public_key + .get(y_start..uncompressed_len) + .ok_or(CoseError::InvalidKeyMaterial)?, + ) + } else { + return Err(CoseError::InvalidKeyMaterial); + }; + + // Compression must never repair an invalid caller-supplied y-coordinate. + // Validate the complete SEC1 point first; only then is it safe to retain + // the y parity and discard the remaining coordinate bytes. + validate_supplied_point(profile, public_key, raw_len, uncompressed_len)?; + + let y_last = y.last().copied().ok_or(CoseError::InvalidKeyMaterial)?; + let mut canonical = Vec::with_capacity(compressed_len); + canonical.push(if y_last & 1 == 1 { + COMPRESSED_POINT_ODD_PREFIX + } else { + COMPRESSED_POINT_EVEN_PREFIX + }); + canonical.extend_from_slice(x); + Ok(canonical) +} + +fn validate_supplied_point( + profile: Ec2Profile, + public_key: &[u8], + raw_len: usize, + uncompressed_len: usize, +) -> Result<(), CoseError> { + let algorithm = algorithm_for_ec2_profile(profile)?; + if public_key.len() != raw_len { + return validate_public_key(algorithm, public_key); + } + + let mut sec1 = Vec::new(); + sec1.try_reserve_exact(uncompressed_len) + .map_err(|_| CoseError::ResourceLimitExceeded)?; + sec1.push(UNCOMPRESSED_POINT_PREFIX); + sec1.extend_from_slice(public_key); + validate_public_key(algorithm, &sec1) +} + +pub(crate) fn ec2_public_bytes_from_key( + key: &coset::CoseKey, + profile: Ec2Profile, +) -> Result, CoseError> { + let x = get_param_bytes(key, iana::Ec2KeyParameter::X as i64) + .ok_or(CoseError::MissingKeyMaterial)?; + let y = get_param_value(key, iana::Ec2KeyParameter::Y as i64) + .ok_or(CoseError::MissingKeyMaterial)?; + let mut public_key = Vec::with_capacity(compressed_point_len(profile)?); + if let Some(y_sign) = y.as_bool() { + public_key.push(if y_sign { + COMPRESSED_POINT_ODD_PREFIX + } else { + COMPRESSED_POINT_EVEN_PREFIX + }); + public_key.extend_from_slice(x); + return Ok(public_key); + } + + let y = y.as_bytes().ok_or(CoseError::InvalidFormat)?; + let y_last = y.last().copied().ok_or(CoseError::InvalidKeyMaterial)?; + public_key.push(if y_last & 1 == 1 { + COMPRESSED_POINT_ODD_PREFIX + } else { + COMPRESSED_POINT_EVEN_PREFIX + }); + public_key.extend_from_slice(x); + Ok(public_key) +} + +pub(crate) fn algorithm_for_ec2_profile(profile: Ec2Profile) -> Result { + match profile.curve { + iana::EllipticCurve::P_256 => Ok(Algorithm::P256), + iana::EllipticCurve::P_384 => Ok(Algorithm::P384), + iana::EllipticCurve::P_521 => Ok(Algorithm::P521), + iana::EllipticCurve::Secp256k1 => Ok(Algorithm::Secp256k1), + _ => Err(CoseError::UnsupportedAlgorithm), + } +} + +fn compressed_point_len(profile: Ec2Profile) -> Result { + profile + .coordinate_len + .checked_add(COMPRESSED_POINT_PREFIX_BYTES) + .ok_or(CoseError::InvalidFormat) +} + +fn raw_point_len(profile: Ec2Profile) -> Result { + profile + .coordinate_len + .checked_mul(2) + .ok_or(CoseError::InvalidFormat) +} diff --git a/src/key/map_algorithm.rs b/crates/cose/src/key/map_algorithm.rs similarity index 54% rename from src/key/map_algorithm.rs rename to crates/cose/src/key/map_algorithm.rs index ec890fe..7207c6c 100644 --- a/src/key/map_algorithm.rs +++ b/crates/cose/src/key/map_algorithm.rs @@ -9,11 +9,14 @@ use crate::CoseError; pub(crate) fn alg_to_cose(alg: Algorithm) -> Result { match alg { - Algorithm::Ed25519 => Ok(iana::Algorithm::EdDSA), - Algorithm::P256 => Ok(iana::Algorithm::ES256), - Algorithm::P384 => Ok(iana::Algorithm::ES384), - Algorithm::P521 => Ok(iana::Algorithm::ES512), + Algorithm::Ed25519 => Ok(iana::Algorithm::Ed25519), + Algorithm::P256 => Ok(iana::Algorithm::ESP256), + Algorithm::P384 => Ok(iana::Algorithm::ESP384), + Algorithm::P521 => Ok(iana::Algorithm::ESP512), Algorithm::Secp256k1 => Ok(iana::Algorithm::ES256K), + Algorithm::MlDsa44 => Ok(iana::Algorithm::ML_DSA_44), + Algorithm::MlDsa65 => Ok(iana::Algorithm::ML_DSA_65), + Algorithm::MlDsa87 => Ok(iana::Algorithm::ML_DSA_87), _ => Err(CoseError::UnsupportedAlgorithm), } } @@ -22,11 +25,14 @@ pub(crate) fn cose_to_alg( alg: &RegisteredLabelWithPrivate, ) -> Result { match alg { - RegisteredLabelWithPrivate::Assigned(iana::Algorithm::EdDSA) => Ok(Algorithm::Ed25519), - RegisteredLabelWithPrivate::Assigned(iana::Algorithm::ES256) => Ok(Algorithm::P256), - RegisteredLabelWithPrivate::Assigned(iana::Algorithm::ES384) => Ok(Algorithm::P384), - RegisteredLabelWithPrivate::Assigned(iana::Algorithm::ES512) => Ok(Algorithm::P521), + RegisteredLabelWithPrivate::Assigned(iana::Algorithm::Ed25519) => Ok(Algorithm::Ed25519), + RegisteredLabelWithPrivate::Assigned(iana::Algorithm::ESP256) => Ok(Algorithm::P256), + RegisteredLabelWithPrivate::Assigned(iana::Algorithm::ESP384) => Ok(Algorithm::P384), + RegisteredLabelWithPrivate::Assigned(iana::Algorithm::ESP512) => Ok(Algorithm::P521), RegisteredLabelWithPrivate::Assigned(iana::Algorithm::ES256K) => Ok(Algorithm::Secp256k1), + RegisteredLabelWithPrivate::Assigned(iana::Algorithm::ML_DSA_44) => Ok(Algorithm::MlDsa44), + RegisteredLabelWithPrivate::Assigned(iana::Algorithm::ML_DSA_65) => Ok(Algorithm::MlDsa65), + RegisteredLabelWithPrivate::Assigned(iana::Algorithm::ML_DSA_87) => Ok(Algorithm::MlDsa87), _ => Err(CoseError::UnsupportedAlgorithm), } } diff --git a/crates/cose/src/key/mod.rs b/crates/cose/src/key/mod.rs new file mode 100644 index 0000000..177d330 --- /dev/null +++ b/crates/cose/src/key/mod.rs @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! COSE_Key encoding, extraction, and key identifier derivation. + +mod akp; +pub(crate) mod convert; +pub(crate) mod derive_kid; +pub(crate) mod ec; +#[cfg(feature = "cose-crypto")] +pub(crate) mod map_algorithm; +mod owned; +mod parse; +pub(crate) mod profile; +#[cfg(feature = "cose-crypto")] +mod reject_weak_public_key; +mod validate_material; + +pub use convert::{ + cose_key_from_private_bytes, cose_key_from_public_bytes, cose_key_to_private_bytes, + cose_key_to_public_bytes, cose_key_to_vec, +}; +pub use derive_kid::derive_kid_from_cose_key_public; +#[cfg(feature = "cose-crypto")] +pub(crate) use derive_kid::derive_kid_from_ml_kem_public_key; +pub use owned::CoseKey; +pub use parse::cose_key_from_slice; +#[cfg(feature = "wire")] +pub(crate) use parse::{parse_cose_key, CoseKeyParseInput, CoseKeyParseOutput}; + +#[cfg(all(test, feature = "cose-crypto"))] +mod parse_differential_tests; +#[cfg(test)] +mod parse_tests; diff --git a/crates/cose/src/key/owned.rs b/crates/cose/src/key/owned.rs new file mode 100644 index 0000000..4096fe5 --- /dev/null +++ b/crates/cose/src/key/owned.rs @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use coset::{Label, RegisteredLabel, RegisteredLabelWithPrivate}; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +use crate::zeroize_coset::zeroize_value; + +/// Owned COSE key whose byte-valued fields are wiped on drop. +/// +/// `coset::CoseKey` intentionally models the wire format and stores parameters +/// in ordinary vectors. This wrapper supplies the ownership boundary required +/// by the SDK: private parameters, identifiers, and base-IV material do not +/// remain in allocator-owned buffers after the key is dropped. +/// +/// The type deliberately does not implement `Clone`, `Debug`, serialization, +/// or expose the underlying mutable key. Those capabilities could duplicate or +/// disclose private parameters without an auditable lifetime. +#[must_use] +pub struct CoseKey { + inner: coset::CoseKey, +} + +impl CoseKey { + pub(crate) fn new(inner: coset::CoseKey) -> Self { + Self { inner } + } + + pub(crate) fn inner(&self) -> &coset::CoseKey { + &self.inner + } + + pub(crate) fn inner_mut(&mut self) -> &mut coset::CoseKey { + &mut self.inner + } +} + +impl Drop for CoseKey { + fn drop(&mut self) { + zeroize_cose_key(&mut self.inner); + } +} + +impl ZeroizeOnDrop for CoseKey {} + +fn zeroize_cose_key(key: &mut coset::CoseKey) { + if let RegisteredLabel::Text(text) = &mut key.kty { + text.zeroize(); + } + key.key_id.zeroize(); + if let Some(RegisteredLabelWithPrivate::Text(text)) = &mut key.alg { + text.zeroize(); + } + for mut operation in core::mem::take(&mut key.key_ops) { + if let RegisteredLabel::Text(text) = &mut operation { + text.zeroize(); + } + } + key.base_iv.zeroize(); + for (label, value) in &mut key.params { + if let Label::Text(text) = label { + text.zeroize(); + } + zeroize_value(value); + } +} + +#[cfg(test)] +#[path = "owned_tests.rs"] +mod tests; diff --git a/crates/cose/src/key/owned_tests.rs b/crates/cose/src/key/owned_tests.rs new file mode 100644 index 0000000..7d72f69 --- /dev/null +++ b/crates/cose/src/key/owned_tests.rs @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use ciborium::value::Value; +use coset::{Label, RegisteredLabel, RegisteredLabelWithPrivate}; + +use super::zeroize_cose_key; + +#[test] +fn rejected_text_labels_and_owned_values_are_wiped() { + let mut key = coset::CoseKey { + kty: RegisteredLabel::Text("sensitive-kty".to_owned()), + key_id: b"sensitive-kid".to_vec(), + alg: Some(RegisteredLabelWithPrivate::Text("sensitive-alg".to_owned())), + key_ops: [RegisteredLabel::Text("sensitive-operation".to_owned())] + .into_iter() + .collect(), + base_iv: b"sensitive-base-iv".to_vec(), + params: vec![( + Label::Text("sensitive-label".to_owned()), + Value::Bytes(b"sensitive-value".to_vec()), + )], + }; + + zeroize_cose_key(&mut key); + + assert!(matches!(key.kty, RegisteredLabel::Text(ref value) if value.is_empty())); + assert!(key.key_id.iter().all(|byte| *byte == 0)); + assert!( + matches!(key.alg, Some(RegisteredLabelWithPrivate::Text(ref value)) if value.is_empty()) + ); + assert!(key.key_ops.is_empty()); + assert!(key.base_iv.iter().all(|byte| *byte == 0)); + assert!(matches!( + key.params.as_slice(), + [(Label::Text(label), Value::Bytes(value))] + if label.is_empty() && value.iter().all(|byte| *byte == 0) + )); +} diff --git a/crates/cose/src/key/parse.rs b/crates/cose/src/key/parse.rs new file mode 100644 index 0000000..e3333ed --- /dev/null +++ b/crates/cose/src/key/parse.rs @@ -0,0 +1,175 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Operation-specific COSE_Key parsing semantics. + +use ciborium::value::Value; +use coset::{iana, AsCborValue, Label, RegisteredLabel, RegisteredLabelWithPrivate}; +use zeroize::Zeroize; + +use crate::failure::CoseFailure; +use crate::zeroize_coset::SensitiveCborValue; +use crate::{CoseError, CoseKey}; + +use super::profile::validate_parsed_cose_key; + +/// Borrowed domain input for the COSE_Key parse operation. +pub(crate) struct CoseKeyParseInput<'a> { + encoded: &'a [u8], +} + +impl<'a> CoseKeyParseInput<'a> { + pub(crate) const fn new(encoded: &'a [u8]) -> Self { + Self { encoded } + } +} + +/// Owning domain output for the COSE_Key parse operation. +/// +/// Keeping the key behind an operation-specific owner prevents adapters from +/// treating arbitrary result bytes as an interchangeable semantic result. +#[must_use] +pub(crate) struct CoseKeyParseOutput { + key: CoseKey, +} + +impl CoseKeyParseOutput { + pub(crate) fn into_key(self) -> CoseKey { + self.key + } +} + +/// Parses and validates one canonical, untagged COSE_Key. +/// +/// This function accepts only domain input. Transport decoding, generated +/// protobuf ownership, and result serialization remain adapter concerns. +pub(crate) fn parse_cose_key( + input: CoseKeyParseInput<'_>, +) -> Result { + let key = decode_owned_cose_key(input.encoded).map_err(CoseFailure::from)?; + validate_parsed_cose_key(&key).map_err(CoseFailure::from)?; + Ok(CoseKeyParseOutput { key }) +} + +/// Decode a COSE_Key from canonical, untagged CBOR bytes. +/// +/// # Errors +/// +/// Returns [`CoseError`] when the CBOR is malformed, non-canonical, oversized, +/// tagged, duplicated, or does not satisfy the supported COSE_Key profiles. +pub fn cose_key_from_slice(bytes: &[u8]) -> Result { + parse_cose_key(CoseKeyParseInput::new(bytes)) + .map(CoseKeyParseOutput::into_key) + .map_err(CoseFailure::into_native_error) +} + +fn decode_owned_cose_key(bytes: &[u8]) -> Result { + let mut decoded = SensitiveCborValue::decode_cose_key(bytes)?; + let entries = match decoded.value_mut() { + Value::Map(entries) => entries, + _ => return Err(CoseError::Cbor), + }; + + // Establish the destination wipe owner before moving any decoded field. + // Moved values leave empty or null placeholders in the original CBOR tree, + // so both the remaining tree and a partially assembled key are cleared + // when any subsequent label or value validation fails. + let mut key = CoseKey::new(coset::CoseKey::default()); + for (raw_label, raw_value) in entries { + let label = parse_cose_key_label(raw_label)?; + match label { + Label::Int(value) if value == iana::KeyParameter::Kty as i64 => { + key.inner_mut().kty = parse_key_type(raw_value)?; + } + Label::Int(value) if value == iana::KeyParameter::Kid as i64 => { + key.inner_mut().key_id = parse_nonempty_bytes(raw_value)?; + } + Label::Int(value) if value == iana::KeyParameter::Alg as i64 => { + key.inner_mut().alg = Some(parse_key_algorithm(raw_value)?); + } + Label::Int(value) if value == iana::KeyParameter::KeyOps as i64 => { + parse_key_operations(raw_value, key.inner_mut())?; + } + Label::Int(value) if value == iana::KeyParameter::BaseIv as i64 => { + key.inner_mut().base_iv = parse_nonempty_bytes(raw_value)?; + } + parameter_label => key + .inner_mut() + .params + .push((parameter_label, take_cbor_value(raw_value))), + } + } + + if key.inner().kty == RegisteredLabel::Assigned(iana::KeyType::Reserved) { + return Err(CoseError::InvalidFormat); + } + Ok(key) +} + +fn parse_cose_key_label(value: &mut Value) -> Result { + match value { + Value::Integer(integer) => i64::try_from(*integer) + .map(Label::Int) + .map_err(|_| CoseError::Cbor), + Value::Text(text) => Ok(Label::Text(core::mem::take(text))), + _ => Err(CoseError::Cbor), + } +} + +fn parse_key_type(value: &mut Value) -> Result, CoseError> { + match value { + Value::Integer(integer) => { + RegisteredLabel::from_cbor_value(Value::Integer(*integer)).map_err(|_| CoseError::Cbor) + } + Value::Text(text) => Ok(RegisteredLabel::Text(core::mem::take(text))), + _ => Err(CoseError::Cbor), + } +} + +fn parse_key_algorithm( + value: &mut Value, +) -> Result, CoseError> { + match value { + Value::Integer(integer) => { + RegisteredLabelWithPrivate::from_cbor_value(Value::Integer(*integer)) + .map_err(|_| CoseError::Cbor) + } + Value::Text(text) => Ok(RegisteredLabelWithPrivate::Text(core::mem::take(text))), + _ => Err(CoseError::Cbor), + } +} + +fn parse_key_operations(value: &mut Value, key: &mut coset::CoseKey) -> Result<(), CoseError> { + let operations = match value { + Value::Array(operations) if !operations.is_empty() => operations, + _ => return Err(CoseError::Cbor), + }; + for value in operations { + let mut operation = match value { + Value::Integer(integer) => RegisteredLabel::from_cbor_value(Value::Integer(*integer)) + .map_err(|_| CoseError::Cbor)?, + Value::Text(text) => RegisteredLabel::Text(core::mem::take(text)), + _ => return Err(CoseError::Cbor), + }; + if key.key_ops.contains(&operation) { + if let RegisteredLabel::Text(text) = &mut operation { + text.zeroize(); + } + return Err(CoseError::Cbor); + } + key.key_ops.insert(operation); + } + Ok(()) +} + +fn parse_nonempty_bytes(value: &mut Value) -> Result, CoseError> { + match value { + Value::Bytes(bytes) if !bytes.is_empty() => Ok(core::mem::take(bytes)), + _ => Err(CoseError::Cbor), + } +} + +fn take_cbor_value(value: &mut Value) -> Value { + core::mem::replace(value, Value::Null) +} diff --git a/crates/cose/src/key/parse_differential_tests.rs b/crates/cose/src/key/parse_differential_tests.rs new file mode 100644 index 0000000..2d7a4e8 --- /dev/null +++ b/crates/cose/src/key/parse_differential_tests.rs @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +#![allow(clippy::expect_used)] + +use reallyme_crypto::dispatch::generate_keypair; +use zeroize::Zeroizing; + +use crate::Algorithm; + +use super::convert::{cose_key_from_private_bytes, cose_key_from_public_bytes, cose_key_to_vec}; +use super::parse::{cose_key_from_slice, parse_cose_key, CoseKeyParseInput}; + +#[test] +fn native_facade_and_semantic_parse_match_for_all_pilot_owner_classes() { + let (classical_public, classical_private) = + generate_keypair(Algorithm::Ed25519).expect("Ed25519 fixture generation must succeed"); + let (pq_public, pq_private) = + generate_keypair(Algorithm::MlKem768).expect("ML-KEM-768 fixture generation must succeed"); + + let fixtures = [ + encode_public(Algorithm::Ed25519, &classical_public), + encode_ed25519_private(&classical_private, &classical_public), + encode_public(Algorithm::MlKem768, &pq_public), + encode_private(Algorithm::MlKem768, &pq_private, &pq_public), + ]; + + for encoded in fixtures { + let native = cose_key_from_slice(&encoded).expect("native fixture parse must succeed"); + let semantic = parse_cose_key(CoseKeyParseInput::new(&encoded)) + .expect("semantic fixture parse must succeed") + .into_key(); + + assert_eq!( + cose_key_to_vec(&native) + .expect("native result must encode") + .as_slice(), + encoded.as_slice(), + ); + assert_eq!( + cose_key_to_vec(&semantic) + .expect("semantic result must encode") + .as_slice(), + encoded.as_slice(), + ); + } +} + +fn encode_public(algorithm: Algorithm, public_key: &[u8]) -> Zeroizing> { + let key = cose_key_from_public_bytes(algorithm, public_key) + .expect("public fixture COSE_Key must build"); + cose_key_to_vec(&key).expect("public fixture COSE_Key must encode") +} + +fn encode_private( + algorithm: Algorithm, + private_key: &[u8], + public_key: &[u8], +) -> Zeroizing> { + let key = cose_key_from_private_bytes(algorithm, private_key, Some(public_key)) + .expect("private fixture COSE_Key must build"); + cose_key_to_vec(&key).expect("private fixture COSE_Key must encode") +} + +fn encode_ed25519_private(private_key: &[u8], public_key: &[u8]) -> Zeroizing> { + // The fixture is independently assembled in RFC 8949 bytewise key order: + // kty, alg, crv, x, d. It tests parsing without depending on the separate + // private-key construction path owned by the key-family boundary. + let mut encoded = Zeroizing::new(vec![ + 0xa5, 0x01, 0x01, 0x03, 0x32, 0x20, 0x06, 0x21, 0x58, 0x20, + ]); + encoded.extend_from_slice(public_key); + encoded.extend_from_slice(&[0x23, 0x58, 0x20]); + encoded.extend_from_slice(private_key); + encoded +} diff --git a/crates/cose/src/key/parse_tests.rs b/crates/cose/src/key/parse_tests.rs new file mode 100644 index 0000000..f350c01 --- /dev/null +++ b/crates/cose/src/key/parse_tests.rs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +#![allow(clippy::panic)] + +use crate::failure::{CoseFailureBranch, CoseFailureOrigin, CoseFailureReason}; +use crate::limits::MAX_COSE_KEY_BYTES; +use crate::{Algorithm, CoseError}; + +use super::convert::{cose_key_from_public_bytes, cose_key_to_vec}; +use super::parse::{parse_cose_key, CoseKeyParseInput, CoseKeyParseOutput}; + +const RFC_8032_ED25519_PUBLIC_KEY: [u8; 32] = [ + 0xd7, 0x5a, 0x98, 0x01, 0x82, 0xb1, 0x0a, 0xb7, 0xd5, 0x4b, 0xfe, 0xd3, 0xc9, 0x64, 0x07, 0x3a, + 0x0e, 0xe1, 0x72, 0xf3, 0xda, 0xa6, 0x23, 0x25, 0xaf, 0x02, 0x1a, 0x68, 0xf7, 0x07, 0x51, 0x1a, +]; + +#[test] +fn semantic_parse_preserves_canonical_key_bytes() -> Result<(), CoseError> { + let key = cose_key_from_public_bytes(Algorithm::Ed25519, &RFC_8032_ED25519_PUBLIC_KEY)?; + let encoded = cose_key_to_vec(&key)?; + + let parsed = parse_cose_key(CoseKeyParseInput::new(&encoded))?; + let reencoded = reencode(parsed)?; + + assert_eq!(reencoded.as_slice(), encoded.as_slice()); + Ok(()) +} + +#[test] +fn semantic_parse_rejects_empty_input_with_typed_error() { + let failure = parse_cose_key(CoseKeyParseInput::new(&[])).err(); + + assert_eq!( + failure.map(|value| (value.origin(), value.branch(), value.reason())), + Some(( + CoseFailureOrigin::Caller, + CoseFailureBranch::Primitive, + CoseFailureReason::CommonCbor, + )) + ); +} + +#[test] +fn semantic_parse_rejects_oversized_input_before_decode() { + let oversized_len = match MAX_COSE_KEY_BYTES.checked_add(1) { + Some(value) => value, + None => panic!("COSE_Key test length overflowed"), + }; + let oversized = vec![0_u8; oversized_len]; + + let failure = parse_cose_key(CoseKeyParseInput::new(&oversized)).err(); + + assert_eq!( + failure.map(|value| (value.origin(), value.branch(), value.reason())), + Some(( + CoseFailureOrigin::Caller, + CoseFailureBranch::Primitive, + CoseFailureReason::CommonResourceLimitExceeded, + )) + ); +} + +fn reencode(output: CoseKeyParseOutput) -> Result>, CoseError> { + cose_key_to_vec(&output.into_key()) +} diff --git a/crates/cose/src/key/profile.rs b/crates/cose/src/key/profile.rs new file mode 100644 index 0000000..665314e --- /dev/null +++ b/crates/cose/src/key/profile.rs @@ -0,0 +1,263 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Supported COSE_Key profile validation and parameter access. + +use ciborium::value::Value; +use coset::{iana, Label, RegisteredLabel, RegisteredLabelWithPrivate}; +use reallyme_crypto::core::Algorithm; + +use crate::{CoseError, CoseKey}; + +use super::akp::{akp_profile_from_cose_algorithm, algorithm_for_akp_profile, AkpProfile}; +use super::ec::{ + algorithm_for_ec2_profile, ec2_profile_from_curve, ec2_public_bytes_from_key, Ec2Profile, +}; +use super::validate_material::{validate_private_public_pair, validate_public_key}; + +pub(crate) const ED25519_PUBLIC_KEY_BYTES: usize = 32; +pub(crate) const ED25519_SECRET_KEY_BYTES: usize = 32; +pub(crate) const X25519_PUBLIC_KEY_BYTES: usize = 32; + +#[derive(Clone, Copy)] +pub(crate) enum KeyProfile { + Okp(OkpProfile), + Ec2(Ec2Profile), + Akp(AkpProfile), +} + +#[derive(Clone, Copy)] +pub(crate) struct OkpProfile { + pub(crate) alg: Option, + pub(crate) coordinate_len: usize, +} + +pub(crate) fn validate_cose_key_profile(key: &CoseKey) -> Result { + let key = key.inner(); + match key.kty { + RegisteredLabel::Assigned(iana::KeyType::OKP) => validate_okp_profile(key), + RegisteredLabel::Assigned(iana::KeyType::EC2) => validate_ec2_profile(key), + RegisteredLabel::Assigned(iana::KeyType::AKP) => validate_akp_profile(key), + _ => Err(CoseError::UnsupportedAlgorithm), + } +} + +pub(crate) fn validate_parsed_cose_key(key: &CoseKey) -> Result<(), CoseError> { + validate_cose_key_profile(key).map(|_| ()) +} + +pub(crate) fn algorithm_for_cose_key(key: &CoseKey) -> Result { + match validate_cose_key_profile(key)? { + KeyProfile::Okp(profile) => { + if profile.alg == Some(iana::Algorithm::Ed25519) { + Ok(Algorithm::Ed25519) + } else { + Ok(Algorithm::X25519) + } + } + KeyProfile::Ec2(profile) => algorithm_for_ec2_profile(profile), + KeyProfile::Akp(profile) => Ok(algorithm_for_akp_profile(profile)), + } +} + +fn validate_okp_profile(key: &coset::CoseKey) -> Result { + let curve = + get_param_i64(key, iana::OkpKeyParameter::Crv as i64).ok_or(CoseError::InvalidFormat)?; + let profile = okp_profile(curve)?; + validate_key_algorithm(key, profile.alg)?; + validate_optional_param_len(key, iana::OkpKeyParameter::X as i64, profile.coordinate_len)?; + validate_optional_param_len(key, iana::OkpKeyParameter::D as i64, profile.coordinate_len)?; + + let public_key = get_param_bytes(key, iana::OkpKeyParameter::X as i64); + let private_key = get_param_bytes(key, iana::OkpKeyParameter::D as i64); + if public_key.is_none() && private_key.is_none() { + return Err(CoseError::MissingKeyMaterial); + } + if private_key.is_some() && public_key.is_none() { + // Private keys need an auditable public binding so every accepted key + // can be validated identically after construction or parsing. + return Err(CoseError::MissingKeyMaterial); + } + + let is_signature = profile.alg == Some(iana::Algorithm::Ed25519); + if private_key.is_some() && !is_signature { + // X25519 remains public-only until private/public derivation is + // implemented consistently across every runtime provider. + return Err(CoseError::UnsupportedAlgorithm); + } + validate_key_operations(key, private_key.is_some(), is_signature)?; + + if let Some(public_key) = public_key { + let algorithm = if is_signature { + Algorithm::Ed25519 + } else { + Algorithm::X25519 + }; + validate_public_key(algorithm, public_key)?; + if let Some(private_key) = private_key { + validate_private_public_pair(algorithm, private_key, public_key)?; + } + } + + Ok(KeyProfile::Okp(profile)) +} + +fn validate_ec2_profile(key: &coset::CoseKey) -> Result { + let curve = + get_param_i64(key, iana::Ec2KeyParameter::Crv as i64).ok_or(CoseError::InvalidFormat)?; + let profile = ec2_profile_from_curve(curve)?; + validate_key_algorithm(key, Some(profile.alg))?; + validate_optional_param_len(key, iana::Ec2KeyParameter::X as i64, profile.coordinate_len)?; + validate_optional_param_len(key, iana::Ec2KeyParameter::D as i64, profile.coordinate_len)?; + + if let Some(y) = get_param_value(key, iana::Ec2KeyParameter::Y as i64) { + if y.as_bool().is_none() { + let y_bytes = y.as_bytes().ok_or(CoseError::InvalidFormat)?; + if y_bytes.len() != profile.coordinate_len { + return Err(CoseError::InvalidKeyMaterial); + } + } + } + + let private_key = get_param_bytes(key, iana::Ec2KeyParameter::D as i64); + let has_public_point = get_param_bytes(key, iana::Ec2KeyParameter::X as i64).is_some() + && get_param_value(key, iana::Ec2KeyParameter::Y as i64).is_some(); + if private_key.is_none() && !has_public_point { + return Err(CoseError::MissingKeyMaterial); + } + if private_key.is_some() && !has_public_point { + // A private scalar without its public point cannot be checked for + // scalar validity or private/public binding. + return Err(CoseError::MissingKeyMaterial); + } + + validate_key_operations(key, private_key.is_some(), true)?; + if has_public_point { + let algorithm = algorithm_for_ec2_profile(profile)?; + let public_key = ec2_public_bytes_from_key(key, profile)?; + validate_public_key(algorithm, &public_key)?; + if let Some(private_key) = private_key { + validate_private_public_pair(algorithm, private_key, &public_key)?; + } + } + + Ok(KeyProfile::Ec2(profile)) +} + +fn validate_akp_profile(key: &coset::CoseKey) -> Result { + let algorithm = key.alg.as_ref().ok_or(CoseError::UnsupportedAlgorithm)?; + let profile = akp_profile_from_cose_algorithm(algorithm)?; + validate_optional_param_len( + key, + iana::AkpKeyParameter::Pub as i64, + profile.public_key_len, + )?; + validate_optional_param_len( + key, + iana::AkpKeyParameter::Priv as i64, + profile.private_key_len, + )?; + let public_key = get_param_bytes(key, iana::AkpKeyParameter::Pub as i64) + .ok_or(CoseError::MissingKeyMaterial)?; + let private_key = get_param_bytes(key, iana::AkpKeyParameter::Priv as i64); + validate_key_operations(key, private_key.is_some(), profile.is_signature)?; + let algorithm = algorithm_for_akp_profile(profile); + validate_public_key(algorithm, public_key)?; + if let Some(private_key) = private_key { + validate_private_public_pair(algorithm, private_key, public_key)?; + } + Ok(KeyProfile::Akp(profile)) +} + +fn okp_profile(curve: i64) -> Result { + if curve == iana::EllipticCurve::Ed25519 as i64 { + return Ok(OkpProfile { + alg: Some(iana::Algorithm::Ed25519), + coordinate_len: ED25519_PUBLIC_KEY_BYTES, + }); + } + if curve == iana::EllipticCurve::X25519 as i64 { + return Ok(OkpProfile { + alg: None, + coordinate_len: X25519_PUBLIC_KEY_BYTES, + }); + } + Err(CoseError::UnsupportedAlgorithm) +} + +fn validate_key_algorithm( + key: &coset::CoseKey, + expected: Option, +) -> Result<(), CoseError> { + match (&key.alg, expected) { + (None, _) => Ok(()), + (Some(RegisteredLabelWithPrivate::Assigned(actual)), Some(expected_alg)) + if *actual == expected_alg => + { + Ok(()) + } + _ => Err(CoseError::UnsupportedAlgorithm), + } +} + +fn validate_optional_param_len( + key: &coset::CoseKey, + label: i64, + expected_len: usize, +) -> Result<(), CoseError> { + if let Some(bytes) = get_param_bytes(key, label) { + if bytes.len() != expected_len { + return Err(CoseError::InvalidKeyMaterial); + } + } + Ok(()) +} + +fn validate_key_operations( + key: &coset::CoseKey, + has_private_key: bool, + is_signature_key: bool, +) -> Result<(), CoseError> { + for operation in &key.key_ops { + let allowed = if is_signature_key { + matches!( + operation, + RegisteredLabel::Assigned(iana::KeyOperation::Verify) + | RegisteredLabel::Assigned(iana::KeyOperation::Sign) if has_private_key + ) || matches!( + operation, + RegisteredLabel::Assigned(iana::KeyOperation::Verify) + ) + } else { + matches!( + operation, + RegisteredLabel::Assigned(iana::KeyOperation::DeriveKey) + | RegisteredLabel::Assigned(iana::KeyOperation::DeriveBits) + ) + }; + if !allowed { + return Err(CoseError::InvalidKeyMaterial); + } + } + Ok(()) +} + +pub(crate) fn get_param_value(key: &coset::CoseKey, label: i64) -> Option<&Value> { + key.params + .iter() + .find(|(candidate, _)| *candidate == Label::Int(label)) + .map(|(_, value)| value) +} + +pub(crate) fn get_param_bytes(key: &coset::CoseKey, label: i64) -> Option<&[u8]> { + get_param_value(key, label) + .and_then(Value::as_bytes) + .map(Vec::as_slice) +} + +fn get_param_i64(key: &coset::CoseKey, label: i64) -> Option { + get_param_value(key, label) + .and_then(Value::as_integer) + .and_then(|value| value.try_into().ok()) +} diff --git a/crates/cose/src/key/reject_weak_public_key.rs b/crates/cose/src/key/reject_weak_public_key.rs new file mode 100644 index 0000000..5e65183 --- /dev/null +++ b/crates/cose/src/key/reject_weak_public_key.rs @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Rejects public-key encodings that cannot provide signature security. + +use reallyme_crypto::core::Algorithm; + +use crate::CoseError; + +const ED25519_PUBLIC_KEY_BYTES: usize = 32; + +// C2SP's CCTV Ed25519 corpus identifies these canonical low-order points and +// non-canonical aliases. Keeping the policy at the COSE boundary prevents a +// backend parser's permissiveness from silently changing key acceptance. +const ED25519_LOW_ORDER_ENCODINGS: [[u8; ED25519_PUBLIC_KEY_BYTES]; 14] = [ + [0x00; ED25519_PUBLIC_KEY_BYTES], + [ + 0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x7f, + ], + [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x80, + ], + [ + 0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, + ], + [ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + ], + [ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x80, + ], + [ + 0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x7f, + ], + [ + 0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, + ], + [ + 0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4, 0x89, 0xf2, 0xef, 0x98, + 0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, 0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, + 0xfc, 0x05, + ], + [ + 0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4, 0x89, 0xf2, 0xef, 0x98, + 0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, 0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, + 0xfc, 0x85, + ], + [ + 0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, 0x76, 0x0d, 0x10, 0x67, + 0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, + 0x03, 0x7a, + ], + [ + 0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, 0x76, 0x0d, 0x10, 0x67, + 0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, + 0x03, 0xfa, + ], + [ + 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x7f, + ], + [ + 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, + ], +]; + +pub(super) fn reject_weak_public_key( + algorithm: Algorithm, + public_key: &[u8], +) -> Result<(), CoseError> { + if algorithm != Algorithm::Ed25519 { + return Ok(()); + } + + let public_key = <&[u8; ED25519_PUBLIC_KEY_BYTES]>::try_from(public_key) + .map_err(|_| CoseError::InvalidKeyMaterial)?; + if ED25519_LOW_ORDER_ENCODINGS.contains(public_key) { + return Err(CoseError::InvalidKeyMaterial); + } + + Ok(()) +} diff --git a/crates/cose/src/key/validate_material.rs b/crates/cose/src/key/validate_material.rs new file mode 100644 index 0000000..5769a13 --- /dev/null +++ b/crates/cose/src/key/validate_material.rs @@ -0,0 +1,187 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Backend-assisted validation for raw public and private key material. + +use reallyme_crypto::core::Algorithm; +#[cfg(feature = "cose-crypto")] +use reallyme_crypto::core::CryptoError; +#[cfg(feature = "cose-crypto")] +use reallyme_crypto::dispatch::{derive_shared_secret, sign, verify}; +#[cfg(feature = "cose-crypto")] +use zeroize::{Zeroize, Zeroizing}; + +#[cfg(feature = "cose-crypto")] +use crate::error::{sign_error_from_algorithm_error, verify_error_from_algorithm_error}; +use crate::CoseError; + +#[cfg(feature = "cose-crypto")] +use super::reject_weak_public_key::reject_weak_public_key; + +#[cfg(feature = "cose-crypto")] +const KEY_VALIDATION_MESSAGE: &[u8] = b"ReallyMe COSE key validation v1"; +#[cfg(feature = "cose-crypto")] +const ECDSA_VALIDATION_SIGNATURE_DER: &[u8] = &[0x30, 0x06, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01]; +#[cfg(feature = "cose-crypto")] +const SECP256K1_VALIDATION_SIGNATURE_BYTES: usize = 64; +#[cfg(feature = "cose-crypto")] +const ED25519_VALIDATION_SIGNATURE_BYTES: usize = 64; +#[cfg(feature = "cose-crypto")] +const ML_DSA_44_SIGNATURE_BYTES: usize = 2_420; +#[cfg(feature = "cose-crypto")] +const ML_DSA_65_SIGNATURE_BYTES: usize = 3_309; +#[cfg(feature = "cose-crypto")] +const ML_DSA_87_SIGNATURE_BYTES: usize = 4_627; +#[cfg(feature = "cose-crypto")] +const X25519_PUBLIC_KEY_BYTES: usize = 32; +#[cfg(feature = "cose-crypto")] +const X25519_VALIDATION_SECRET: [u8; X25519_PUBLIC_KEY_BYTES] = [0x42; X25519_PUBLIC_KEY_BYTES]; + +#[cfg(feature = "cose-crypto")] +pub(crate) fn validate_public_key( + algorithm: Algorithm, + public_key: &[u8], +) -> Result<(), CoseError> { + reject_weak_public_key(algorithm, public_key)?; + + if algorithm == Algorithm::X25519 { + return match derive_shared_secret(algorithm, &X25519_VALIDATION_SECRET, public_key) { + Ok(mut shared_secret) => { + shared_secret.zeroize(); + Ok(()) + } + Err(error) => match sign_error_from_algorithm_error(error) { + CoseError::ProviderUnavailable => Err(CoseError::ProviderUnavailable), + CoseError::UnsupportedAlgorithm => Err(CoseError::UnsupportedAlgorithm), + _ => Err(CoseError::InvalidKeyMaterial), + }, + }; + } + + if matches!( + algorithm, + Algorithm::MlKem512 | Algorithm::MlKem768 | Algorithm::MlKem1024 + ) { + let (_, shared_secret) = ml_kem_encapsulate(algorithm, public_key)?; + drop(shared_secret); + return Ok(()); + } + + let mut signature = match algorithm { + Algorithm::Ed25519 => Zeroizing::new(vec![0u8; ED25519_VALIDATION_SIGNATURE_BYTES]), + Algorithm::P256 | Algorithm::P384 | Algorithm::P521 => { + Zeroizing::new(ECDSA_VALIDATION_SIGNATURE_DER.to_vec()) + } + // The secp256k1 backend accepts compact r||s, unlike the NIST-curve + // backends' DER input. A correctly shaped dummy reaches public-key + // parsing before failing signature validation. + Algorithm::Secp256k1 => Zeroizing::new(vec![0u8; SECP256K1_VALIDATION_SIGNATURE_BYTES]), + Algorithm::MlDsa44 => Zeroizing::new(vec![0u8; ML_DSA_44_SIGNATURE_BYTES]), + Algorithm::MlDsa65 => Zeroizing::new(vec![0u8; ML_DSA_65_SIGNATURE_BYTES]), + Algorithm::MlDsa87 => Zeroizing::new(vec![0u8; ML_DSA_87_SIGNATURE_BYTES]), + _ => return Err(CoseError::UnsupportedAlgorithm), + }; + let result = verify(algorithm, public_key, KEY_VALIDATION_MESSAGE, &signature); + signature.zeroize(); + match result { + Ok(()) => Ok(()), + Err(error) => match verify_error_from_algorithm_error(error) { + CoseError::InvalidSignature => Ok(()), + other => Err(other), + }, + } +} + +#[cfg(not(feature = "cose-crypto"))] +pub(crate) fn validate_public_key(_: Algorithm, _: &[u8]) -> Result<(), CoseError> { + Err(CoseError::UnsupportedAlgorithm) +} + +#[cfg(feature = "cose-crypto")] +pub(crate) fn validate_private_public_pair( + algorithm: Algorithm, + private_key: &[u8], + public_key: &[u8], +) -> Result<(), CoseError> { + if matches!( + algorithm, + Algorithm::MlKem512 | Algorithm::MlKem768 | Algorithm::MlKem1024 + ) { + let (ciphertext, encapsulated_secret) = ml_kem_encapsulate(algorithm, public_key)?; + let decapsulated_secret = ml_kem_decapsulate(algorithm, &ciphertext, private_key)?; + if reallyme_crypto::operations::constant_time::equal( + &encapsulated_secret, + &decapsulated_secret, + ) { + return Ok(()); + } + return Err(CoseError::InvalidKeyMaterial); + } + + let mut signature = Zeroizing::new( + sign(algorithm, private_key, KEY_VALIDATION_MESSAGE) + .map_err(sign_error_from_algorithm_error)?, + ); + let verification = verify(algorithm, public_key, KEY_VALIDATION_MESSAGE, &signature) + .map_err(verify_error_from_algorithm_error); + signature.zeroize(); + match verification { + Err(CoseError::InvalidSignature) => Err(CoseError::InvalidKeyMaterial), + result => result, + } +} + +#[cfg(not(feature = "cose-crypto"))] +pub(crate) fn validate_private_public_pair( + _: Algorithm, + _: &[u8], + _: &[u8], +) -> Result<(), CoseError> { + Err(CoseError::UnsupportedAlgorithm) +} + +#[cfg(feature = "cose-crypto")] +fn ml_kem_encapsulate( + algorithm: Algorithm, + public_key: &[u8], +) -> Result<(Vec, Zeroizing>), CoseError> { + match algorithm { + Algorithm::MlKem512 => reallyme_crypto::ml_kem_512::ml_kem_512_encapsulate(public_key), + Algorithm::MlKem768 => reallyme_crypto::ml_kem_768::ml_kem_768_encapsulate(public_key), + Algorithm::MlKem1024 => reallyme_crypto::ml_kem_1024::ml_kem_1024_encapsulate(public_key), + _ => return Err(CoseError::UnsupportedAlgorithm), + } + .map_err(ml_kem_key_operation_error) +} + +#[cfg(feature = "cose-crypto")] +fn ml_kem_decapsulate( + algorithm: Algorithm, + ciphertext: &[u8], + private_key: &[u8], +) -> Result>, CoseError> { + match algorithm { + Algorithm::MlKem512 => { + reallyme_crypto::ml_kem_512::ml_kem_512_decapsulate(ciphertext, private_key) + } + Algorithm::MlKem768 => { + reallyme_crypto::ml_kem_768::ml_kem_768_decapsulate(ciphertext, private_key) + } + Algorithm::MlKem1024 => { + reallyme_crypto::ml_kem_1024::ml_kem_1024_decapsulate(ciphertext, private_key) + } + _ => return Err(CoseError::UnsupportedAlgorithm), + } + .map_err(ml_kem_key_operation_error) +} + +#[cfg(feature = "cose-crypto")] +fn ml_kem_key_operation_error(error: CryptoError) -> CoseError { + match error { + CryptoError::InvalidKey | CryptoError::InvalidCiphertextLength { .. } => { + CoseError::InvalidKeyMaterial + } + _ => CoseError::Crypto, + } +} diff --git a/crates/cose/src/lib.rs b/crates/cose/src/lib.rs new file mode 100644 index 0000000..28912f1 --- /dev/null +++ b/crates/cose/src/lib.rs @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! COSE helpers for ReallyMe identity software. +//! +//! The public surface covers COSE_Sign1, COSE_Key, and the ReallyMe ML-KEM +//! profile for COSE_Encrypt. Other COSE message families fail closed instead +//! of being partially interpreted. +//! +//! # Example +//! +//! ``` +//! use reallyme_cose::{cose_sign1, cose_verify1_with_policy, Algorithm, CoseError, CosePolicy}; +//! use reallyme_crypto::dispatch::generate_keypair; +//! +//! fn sign_and_verify() -> Result<(), CoseError> { +//! let (public_key, private_key) = generate_keypair(Algorithm::Ed25519) +//! .map_err(|_| CoseError::Crypto)?; +//! let kid = b"example-key"; +//! +//! let cose_bytes = cose_sign1(Algorithm::Ed25519, b"payload", &private_key, Some(kid))?; +//! let policy = CosePolicy::new() +//! .with_require_kid(true) +//! .allow_algorithm(Algorithm::Ed25519); +//! +//! let verified = cose_verify1_with_policy(&cose_bytes, &policy, |algorithm, requested_kid| { +//! (algorithm == Algorithm::Ed25519 && requested_kid == kid).then(|| public_key.clone()) +//! })?; +//! assert_eq!(verified.payload.as_slice(), b"payload"); +//! assert_eq!(verified.alg, Algorithm::Ed25519); +//! assert_eq!(verified.kid.as_slice(), kid); +//! Ok(()) +//! } +//! # fn main() -> Result<(), CoseError> { sign_and_verify() } +//! ``` + +#[cfg(all(feature = "wire", not(any(feature = "native", feature = "wasm"))))] +compile_error!( + "reallyme-cose `wire` requires a runtime lane: enable feature `native` for Rust crypto or `wasm` for wasm32-unknown-unknown" +); + +/// Crypto algorithm selector used by the COSE public API. +/// +/// Consumers should import this re-export instead of depending directly on +/// `reallyme-crypto`; that keeps the algorithm type identical to the one used +/// by `reallyme-cose`. +pub use reallyme_crypto::core::Algorithm; + +/// COSE algorithm mapping helpers. +pub mod algorithm; +/// Typed COSE errors. +pub mod error; +pub use error::CoseError; + +mod encode_cbor; +mod failure; + +#[cfg(test)] +mod failure_tests; + +/// Resource limits shared by COSE byte-boundary APIs. +pub mod limits; + +mod zeroize_coset; + +// --- COSE_Encrypt --- +#[cfg(feature = "cose-crypto")] +pub mod encrypt; +#[cfg(feature = "cose-crypto")] +pub use encrypt::{ + cose_decrypt_ml_kem, cose_decrypt_ml_kem_with_external_aad, cose_encrypt_ml_kem_direct, + cose_encrypt_ml_kem_direct_with_external_aad, cose_encrypt_ml_kem_key_wrap, + cose_encrypt_ml_kem_key_wrap_with_external_aad, CoseContentEncryptionAlgorithm, + CoseMlKemAlgorithm, CoseMlKemDecryptRequest, CoseMlKemEncryptRequest, CoseMlKemMode, + CoseMlKemProfile, DecryptedCoseEncrypt, REALLYME_COSE_ALG_ML_KEM_1024, + REALLYME_COSE_ALG_ML_KEM_1024_A256KW, REALLYME_COSE_ALG_ML_KEM_512, + REALLYME_COSE_ALG_ML_KEM_512_A128KW, REALLYME_COSE_ALG_ML_KEM_768, + REALLYME_COSE_ALG_ML_KEM_768_A192KW, REALLYME_COSE_HEADER_EK, +}; + +// --- COSE_Sign1 --- +pub mod sign1; +#[cfg(feature = "cose-crypto")] +pub use sign1::{ + cose_sign1, cose_sign1_detached, cose_sign1_detached_tagged, cose_sign1_detached_with_options, + cose_sign1_detached_with_options_and_external_aad, cose_sign1_detached_with_signer, + cose_sign1_tagged, cose_sign1_with_options, cose_sign1_with_options_and_external_aad, + cose_sign1_with_signer, cose_verify1, cose_verify1_detached, + cose_verify1_detached_with_metadata, cose_verify1_detached_with_policy, + cose_verify1_detached_with_policy_and_external_aad, cose_verify1_with_metadata, + cose_verify1_with_policy, cose_verify1_with_policy_and_external_aad, CoseSign1EncodeOptions, + CoseSigner, CoseSignerError, VerifiedCoseSign1, VerifiedDetachedCoseSign1, +}; + +/// COSE semantic policy enforcement. +pub mod policy; +pub use policy::CosePolicy; + +// --- COSE_Key --- +pub mod key; +pub use key::{ + cose_key_from_private_bytes, cose_key_from_public_bytes, cose_key_from_slice, + cose_key_to_private_bytes, cose_key_to_public_bytes, cose_key_to_vec, + derive_kid_from_cose_key_public, CoseKey, +}; + +/// COSE_Key and Multikey conversion helpers. +pub mod multikey; +pub use multikey::{cose_key_to_multikey, multikey_to_cose_key}; + +#[cfg(feature = "wire")] +mod operation_contract; + +/// Protobuf-ready request, result, and error adapters. +#[cfg(feature = "wire")] +pub mod wire; diff --git a/crates/cose/src/limits/mod.rs b/crates/cose/src/limits/mod.rs new file mode 100644 index 0000000..4a7a402 --- /dev/null +++ b/crates/cose/src/limits/mod.rs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Resource limits shared by COSE byte-boundary APIs. + +mod profile; +mod validate; + +#[cfg(test)] +mod validate_tests; + +pub use profile::{ + MAX_COSE_ENCRYPT_BYTES, MAX_COSE_KEY_BYTES, MAX_COSE_SIGN1_BYTES, MAX_DETACHED_PAYLOAD_BYTES, +}; + +pub(crate) use profile::validate_cose_key_bytes; + +#[cfg(feature = "cose-crypto")] +pub(crate) use profile::{ + validate_cose_encrypt_bytes, validate_cose_sign1_bytes_with_limit, validate_detached_payload, + validate_detached_payload_with_limit, validate_protected_header_bytes, +}; diff --git a/crates/cose/src/limits/profile.rs b/crates/cose/src/limits/profile.rs new file mode 100644 index 0000000..3db8114 --- /dev/null +++ b/crates/cose/src/limits/profile.rs @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Public COSE profile limits and entrypoint-specific validation. + +use crate::CoseError; + +use super::validate::{validate_cbor_bytes, CborItemRole}; + +/// Maximum accepted encoded COSE_Sign1 size. +pub const MAX_COSE_SIGN1_BYTES: usize = 65_536; + +/// Maximum accepted encoded COSE_Key size. +pub const MAX_COSE_KEY_BYTES: usize = 16_384; + +/// Maximum accepted encoded `COSE_Encrypt` size. +pub const MAX_COSE_ENCRYPT_BYTES: usize = 1_114_112; + +/// Maximum accepted detached payload size for signing and verification. +pub const MAX_DETACHED_PAYLOAD_BYTES: usize = 1_048_576; + +#[cfg(feature = "cose-crypto")] +pub(crate) fn validate_cose_sign1_bytes_with_limit( + bytes: &[u8], + max_len: usize, +) -> Result<(), CoseError> { + validate_cbor_bytes(bytes, max_len, CborItemRole::CoseSign1Top) +} + +#[cfg(feature = "cose-crypto")] +pub(crate) fn validate_cose_encrypt_bytes(bytes: &[u8]) -> Result<(), CoseError> { + validate_cbor_bytes(bytes, MAX_COSE_ENCRYPT_BYTES, CborItemRole::CoseEncryptTop) +} + +pub(crate) fn validate_cose_key_bytes(bytes: &[u8]) -> Result<(), CoseError> { + validate_cbor_bytes(bytes, MAX_COSE_KEY_BYTES, CborItemRole::CoseKeyTop) +} + +#[cfg(feature = "cose-crypto")] +pub(crate) fn validate_detached_payload(payload: &[u8]) -> Result<(), CoseError> { + validate_detached_payload_with_limit(payload, MAX_DETACHED_PAYLOAD_BYTES) +} + +#[cfg(feature = "cose-crypto")] +pub(crate) fn validate_detached_payload_with_limit( + payload: &[u8], + max_len: usize, +) -> Result<(), CoseError> { + if payload.len() > max_len { + return Err(CoseError::ResourceLimitExceeded); + } + Ok(()) +} + +#[cfg(feature = "cose-crypto")] +pub(crate) fn validate_protected_header_bytes(bytes: &[u8]) -> Result<(), CoseError> { + if bytes.is_empty() { + return Ok(()); + } + validate_cbor_bytes(bytes, bytes.len(), CborItemRole::ProtectedHeaderMap) +} diff --git a/crates/cose/src/limits/validate.rs b/crates/cose/src/limits/validate.rs new file mode 100644 index 0000000..1ce69db --- /dev/null +++ b/crates/cose/src/limits/validate.rs @@ -0,0 +1,455 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Resource limits and deterministic CBOR boundary checks. + +use std::collections::HashSet; + +#[cfg(feature = "cose-crypto")] +use super::profile::validate_protected_header_bytes; +use crate::CoseError; + +const CBOR_INDEFINITE_ADDITIONAL_INFO: u8 = 0x1f; +const CBOR_ADDITIONAL_INFO_MASK: u8 = 0x1f; +const CBOR_MAJOR_TYPE_SHIFT: u8 = 5; +const CBOR_UINT_MAJOR: u8 = 0; +const CBOR_NEGATIVE_INT_MAJOR: u8 = 1; +const CBOR_BYTES_MAJOR: u8 = 2; +const CBOR_TEXT_MAJOR: u8 = 3; +const CBOR_ARRAY_MAJOR: u8 = 4; +const CBOR_MAP_MAJOR: u8 = 5; +const CBOR_TAG_MAJOR: u8 = 6; +const CBOR_SIMPLE_MAJOR: u8 = 7; +const CBOR_ONE_BYTE_LENGTH: u8 = 24; +const CBOR_TWO_BYTE_LENGTH: u8 = 25; +const CBOR_FOUR_BYTE_LENGTH: u8 = 26; +const CBOR_EIGHT_BYTE_LENGTH: u8 = 27; +#[cfg(feature = "cose-crypto")] +const COSE_SIGN1_TAG: usize = 18; +#[cfg(feature = "cose-crypto")] +const COSE_ENCRYPT_TAG: usize = 96; +const MAX_CBOR_DEPTH: usize = 32; +const MAX_CBOR_COLLECTION_ITEMS: usize = 1_024; +#[cfg(feature = "cose-crypto")] +const MAX_COSE_HEADER_PARAMETERS: usize = 32; + +pub(super) fn validate_cbor_bytes( + bytes: &[u8], + max_len: usize, + role: CborItemRole, +) -> Result<(), CoseError> { + if bytes.is_empty() { + return Err(CoseError::Cbor); + } + + if bytes.len() > max_len { + return Err(CoseError::ResourceLimitExceeded); + } + + let parsed_len = parse_cbor_item(bytes, 0, 0, role)?; + if parsed_len == bytes.len() { + Ok(()) + } else { + Err(CoseError::Cbor) + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(super) enum CborItemRole { + Normal, + CoseKeyTop, + #[cfg(feature = "cose-crypto")] + CoseSign1Top, + #[cfg(feature = "cose-crypto")] + CoseSign1Body, + #[cfg(feature = "cose-crypto")] + CoseEncryptTop, + #[cfg(feature = "cose-crypto")] + CoseEncryptBody, + #[cfg(feature = "cose-crypto")] + CoseRecipientList, + #[cfg(feature = "cose-crypto")] + CoseRecipient, + #[cfg(feature = "cose-crypto")] + ProtectedHeaderMap, + #[cfg(feature = "cose-crypto")] + HeaderMap, +} + +fn parse_cbor_item( + bytes: &[u8], + offset: usize, + depth: usize, + role: CborItemRole, +) -> Result { + if depth > MAX_CBOR_DEPTH { + return Err(CoseError::ResourceLimitExceeded); + } + + let first = *bytes.get(offset).ok_or(CoseError::Cbor)?; + let major = first >> CBOR_MAJOR_TYPE_SHIFT; + let additional = first & CBOR_ADDITIONAL_INFO_MASK; + let value_start = offset + .checked_add(1) + .ok_or(CoseError::ResourceLimitExceeded)?; + + if additional == CBOR_INDEFINITE_ADDITIONAL_INFO { + return match major { + CBOR_BYTES_MAJOR | CBOR_TEXT_MAJOR | CBOR_ARRAY_MAJOR | CBOR_MAP_MAJOR => { + Err(CoseError::NonCanonicalCbor) + } + _ => Err(CoseError::Cbor), + }; + } + + #[cfg(feature = "cose-crypto")] + if role == CborItemRole::ProtectedHeaderMap && major != CBOR_MAP_MAJOR { + return Err(CoseError::InvalidFormat); + } + #[cfg(feature = "cose-crypto")] + if matches!( + role, + CborItemRole::CoseEncryptBody + | CborItemRole::CoseRecipientList + | CborItemRole::CoseRecipient + ) && major != CBOR_ARRAY_MAJOR + { + return Err(CoseError::InvalidFormat); + } + #[cfg(feature = "cose-crypto")] + if role == CborItemRole::CoseEncryptTop && major != CBOR_ARRAY_MAJOR && major != CBOR_TAG_MAJOR + { + return Err(CoseError::InvalidFormat); + } + + match major { + CBOR_UINT_MAJOR | CBOR_NEGATIVE_INT_MAJOR => { + read_argument(bytes, value_start, additional).map(|(_, next_offset)| next_offset) + } + CBOR_BYTES_MAJOR => { + let (len, data_offset) = read_argument(bytes, value_start, additional)?; + data_offset + .checked_add(len) + .filter(|end| *end <= bytes.len()) + .ok_or(CoseError::Cbor) + } + CBOR_TEXT_MAJOR => { + let (len, data_offset) = read_argument(bytes, value_start, additional)?; + let end = data_offset + .checked_add(len) + .filter(|end| *end <= bytes.len()) + .ok_or(CoseError::Cbor)?; + let text = bytes.get(data_offset..end).ok_or(CoseError::Cbor)?; + core::str::from_utf8(text).map_err(|_| CoseError::Cbor)?; + Ok(end) + } + CBOR_ARRAY_MAJOR => { + let (len, mut next_offset) = read_argument(bytes, value_start, additional)?; + validate_collection_length(role, CBOR_ARRAY_MAJOR, len)?; + let remaining = bytes + .len() + .checked_sub(next_offset) + .ok_or(CoseError::Cbor)?; + if len > remaining { + return Err(CoseError::Cbor); + } + for index in 0..len { + let child_depth = next_depth(depth)?; + if protected_header_first_item(role) && index == 0 { + if let Some(end) = + parse_protected_header_bstr_if_present(bytes, next_offset, child_depth)? + { + next_offset = end; + continue; + } + } + let child_role = array_child_role(role, index); + next_offset = parse_cbor_item(bytes, next_offset, child_depth, child_role)?; + } + Ok(next_offset) + } + CBOR_MAP_MAJOR => { + let (len, mut next_offset) = read_argument(bytes, value_start, additional)?; + validate_collection_length(role, CBOR_MAP_MAJOR, len)?; + let remaining = bytes + .len() + .checked_sub(next_offset) + .ok_or(CoseError::Cbor)?; + if len > remaining / 2 { + return Err(CoseError::Cbor); + } + let mut keys = HashSet::new(); + keys.try_reserve(len) + .map_err(|_| CoseError::ResourceLimitExceeded)?; + let mut previous_key: Option<&[u8]> = None; + for _ in 0..len { + let child_depth = next_depth(depth)?; + let key_start = next_offset; + let key_end = + parse_cbor_item(bytes, next_offset, child_depth, CborItemRole::Normal)?; + let key = bytes.get(key_start..key_end).ok_or(CoseError::Cbor)?; + // Container-valued keys can cause their encoded bytes to be + // visited once per enclosing map. The global byte, item, and + // depth limits deliberately bound that duplicate-key work. + if !keys.insert(key) { + return Err(CoseError::DuplicateMapLabel); + } + if role == CborItemRole::CoseKeyTop + && previous_key.is_some_and(|previous| { + deterministic_key_order(previous, key) != core::cmp::Ordering::Less + }) + { + return Err(CoseError::NonCanonicalCbor); + } + previous_key = Some(key); + next_offset = key_end; + next_offset = + parse_cbor_item(bytes, next_offset, child_depth, CborItemRole::Normal)?; + } + Ok(next_offset) + } + CBOR_TAG_MAJOR => { + let (tag, next_offset) = read_argument(bytes, value_start, additional)?; + let child_role = tag_child_role(role, tag)?; + parse_cbor_item(bytes, next_offset, next_depth(depth)?, child_role) + } + CBOR_SIMPLE_MAJOR => parse_simple(bytes, value_start, additional), + _ => Err(CoseError::Cbor), + } +} + +fn read_argument(bytes: &[u8], offset: usize, additional: u8) -> Result<(usize, usize), CoseError> { + match additional { + value if value < CBOR_ONE_BYTE_LENGTH => Ok((usize::from(value), offset)), + CBOR_ONE_BYTE_LENGTH => { + let value = *bytes.get(offset).ok_or(CoseError::Cbor)?; + if value < CBOR_ONE_BYTE_LENGTH { + return Err(CoseError::NonCanonicalCbor); + } + Ok(( + usize::from(value), + offset + .checked_add(1) + .ok_or(CoseError::ResourceLimitExceeded)?, + )) + } + CBOR_TWO_BYTE_LENGTH => { + let end = offset + .checked_add(2) + .ok_or(CoseError::ResourceLimitExceeded)?; + let data = bytes.get(offset..end).ok_or(CoseError::Cbor)?; + let value = u16::from_be_bytes([data[0], data[1]]); + if value <= u16::from(u8::MAX) { + return Err(CoseError::NonCanonicalCbor); + } + Ok((usize::from(value), end)) + } + CBOR_FOUR_BYTE_LENGTH => { + let end = offset + .checked_add(4) + .ok_or(CoseError::ResourceLimitExceeded)?; + let data = bytes.get(offset..end).ok_or(CoseError::Cbor)?; + let value = u32::from_be_bytes([data[0], data[1], data[2], data[3]]); + if value <= u32::from(u16::MAX) { + return Err(CoseError::NonCanonicalCbor); + } + let len = usize::try_from(value).map_err(|_| CoseError::ResourceLimitExceeded)?; + Ok((len, end)) + } + CBOR_EIGHT_BYTE_LENGTH => { + let end = offset + .checked_add(8) + .ok_or(CoseError::ResourceLimitExceeded)?; + let data = bytes.get(offset..end).ok_or(CoseError::Cbor)?; + let value = u64::from_be_bytes([ + data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], + ]); + if value <= u64::from(u32::MAX) { + return Err(CoseError::NonCanonicalCbor); + } + let len = usize::try_from(value).map_err(|_| CoseError::ResourceLimitExceeded)?; + Ok((len, end)) + } + _ => Err(CoseError::Cbor), + } +} + +fn parse_simple(bytes: &[u8], offset: usize, additional: u8) -> Result { + match additional { + value if value < CBOR_ONE_BYTE_LENGTH => Ok(offset), + CBOR_ONE_BYTE_LENGTH => { + let value = *bytes.get(offset).ok_or(CoseError::Cbor)?; + if value < CBOR_ONE_BYTE_LENGTH { + return Err(CoseError::NonCanonicalCbor); + } + offset + .checked_add(1) + .filter(|end| *end <= bytes.len()) + .ok_or(CoseError::Cbor) + } + CBOR_TWO_BYTE_LENGTH | CBOR_FOUR_BYTE_LENGTH | CBOR_EIGHT_BYTE_LENGTH => { + // COSE profiles do not require floating-point extension values. + // Rejecting them avoids accepting encodings without enforcing the + // RFC 8949 preferred-width and canonical-NaN requirements. + Err(CoseError::NonCanonicalCbor) + } + _ => Err(CoseError::Cbor), + } +} + +fn deterministic_key_order(left: &[u8], right: &[u8]) -> core::cmp::Ordering { + // RFC 8949 core deterministic encoding sorts map keys bytewise by their + // deterministic encodings. Length-first ordering is the distinct legacy + // alternative in RFC 8949 Section 4.2.3 and must not be used for the core + // deterministic profile that stabilizes COSE_Key-derived identifiers. + left.cmp(right) +} + +fn validate_collection_length(role: CborItemRole, major: u8, len: usize) -> Result<(), CoseError> { + if len > MAX_CBOR_COLLECTION_ITEMS { + return Err(CoseError::ResourceLimitExceeded); + } + + #[cfg(not(feature = "cose-crypto"))] + let _ = (role, major); + + #[cfg(feature = "cose-crypto")] + { + if matches!( + role, + CborItemRole::ProtectedHeaderMap | CborItemRole::HeaderMap + ) && major == CBOR_MAP_MAJOR + && len > MAX_COSE_HEADER_PARAMETERS + { + return Err(CoseError::ResourceLimitExceeded); + } + if matches!( + role, + CborItemRole::CoseSign1Top | CborItemRole::CoseSign1Body + ) && (major != CBOR_ARRAY_MAJOR || len != 4) + { + return Err(CoseError::InvalidFormat); + } + if matches!( + role, + CborItemRole::CoseEncryptTop | CborItemRole::CoseEncryptBody + ) && (major != CBOR_ARRAY_MAJOR || len != 4) + { + return Err(CoseError::InvalidFormat); + } + if role == CborItemRole::CoseRecipientList && (major != CBOR_ARRAY_MAJOR || len != 1) { + return Err(CoseError::InvalidRecipient); + } + if role == CborItemRole::CoseRecipient && (major != CBOR_ARRAY_MAJOR || len != 3) { + return Err(CoseError::InvalidRecipient); + } + } + + Ok(()) +} + +fn tag_child_role(role: CborItemRole, tag: usize) -> Result { + #[cfg(not(feature = "cose-crypto"))] + let _ = tag; + + match role { + #[cfg(feature = "cose-crypto")] + CborItemRole::CoseSign1Top if tag == COSE_SIGN1_TAG => Ok(CborItemRole::CoseSign1Body), + #[cfg(feature = "cose-crypto")] + CborItemRole::CoseEncryptTop if tag == COSE_ENCRYPT_TAG => { + Ok(CborItemRole::CoseEncryptBody) + } + _ => Err(CoseError::UnexpectedCborTag), + } +} + +fn protected_header_first_item(role: CborItemRole) -> bool { + match role { + #[cfg(feature = "cose-crypto")] + CborItemRole::CoseSign1Top + | CborItemRole::CoseSign1Body + | CborItemRole::CoseEncryptTop + | CborItemRole::CoseEncryptBody + | CborItemRole::CoseRecipient => true, + #[cfg(feature = "cose-crypto")] + CborItemRole::CoseRecipientList + | CborItemRole::ProtectedHeaderMap + | CborItemRole::HeaderMap => false, + CborItemRole::Normal | CborItemRole::CoseKeyTop => false, + } +} + +fn array_child_role(role: CborItemRole, index: usize) -> CborItemRole { + #[cfg(not(feature = "cose-crypto"))] + let _ = index; + + match role { + CborItemRole::CoseKeyTop => CborItemRole::Normal, + #[cfg(feature = "cose-crypto")] + CborItemRole::CoseEncryptTop | CborItemRole::CoseEncryptBody if index == 3 => { + CborItemRole::CoseRecipientList + } + #[cfg(feature = "cose-crypto")] + CborItemRole::CoseSign1Top + | CborItemRole::CoseSign1Body + | CborItemRole::CoseEncryptTop + | CborItemRole::CoseEncryptBody + | CborItemRole::CoseRecipient + if index == 1 => + { + CborItemRole::HeaderMap + } + #[cfg(feature = "cose-crypto")] + CborItemRole::CoseRecipientList => CborItemRole::CoseRecipient, + _ => CborItemRole::Normal, + } +} + +#[cfg(feature = "cose-crypto")] +fn parse_protected_header_bstr_if_present( + bytes: &[u8], + offset: usize, + depth: usize, +) -> Result, CoseError> { + if depth > MAX_CBOR_DEPTH { + return Err(CoseError::ResourceLimitExceeded); + } + + let first = *bytes.get(offset).ok_or(CoseError::Cbor)?; + let major = first >> CBOR_MAJOR_TYPE_SHIFT; + if major != CBOR_BYTES_MAJOR { + return Ok(None); + } + + let additional = first & CBOR_ADDITIONAL_INFO_MASK; + let value_start = offset + .checked_add(1) + .ok_or(CoseError::ResourceLimitExceeded)?; + if additional == CBOR_INDEFINITE_ADDITIONAL_INFO { + return Err(CoseError::NonCanonicalCbor); + } + + let (len, data_offset) = read_argument(bytes, value_start, additional)?; + let end = data_offset + .checked_add(len) + .filter(|candidate| *candidate <= bytes.len()) + .ok_or(CoseError::Cbor)?; + let protected = bytes.get(data_offset..end).ok_or(CoseError::Cbor)?; + validate_protected_header_bytes(protected)?; + + Ok(Some(end)) +} + +#[cfg(not(feature = "cose-crypto"))] +fn parse_protected_header_bstr_if_present( + _bytes: &[u8], + _offset: usize, + _depth: usize, +) -> Result, CoseError> { + Ok(None) +} + +fn next_depth(depth: usize) -> Result { + depth.checked_add(1).ok_or(CoseError::ResourceLimitExceeded) +} diff --git a/crates/cose/src/limits/validate_tests.rs b/crates/cose/src/limits/validate_tests.rs new file mode 100644 index 0000000..fe88579 --- /dev/null +++ b/crates/cose/src/limits/validate_tests.rs @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use super::validate::{validate_cbor_bytes, CborItemRole}; +use crate::CoseError; + +const LARGE_MAP_ENTRIES: u64 = 1_024; + +#[test] +fn large_map_with_unique_raw_keys_validates_linearly() { + let cbor = integer_key_map(LARGE_MAP_ENTRIES, None); + assert_eq!( + validate_cbor_bytes(&cbor, cbor.len(), CborItemRole::Normal), + Ok(()), + ); +} + +#[test] +fn large_map_with_late_duplicate_raw_key_is_rejected() { + let unique_entries = LARGE_MAP_ENTRIES.saturating_sub(1); + let duplicate_key = unique_entries.saturating_sub(1); + let cbor = integer_key_map(unique_entries, Some(duplicate_key)); + assert_eq!( + validate_cbor_bytes(&cbor, cbor.len(), CborItemRole::Normal), + Err(CoseError::DuplicateMapLabel), + ); +} + +#[test] +fn impossible_map_length_is_rejected_before_key_tracking_allocation() { + let mut cbor = vec![0xbb]; + cbor.extend_from_slice(&u64::MAX.to_be_bytes()); + assert!(matches!( + validate_cbor_bytes(&cbor, cbor.len(), CborItemRole::Normal), + Err(CoseError::Cbor | CoseError::ResourceLimitExceeded), + )); +} + +#[test] +fn collection_over_limit_is_rejected_before_key_tracking_allocation() { + let cbor = integer_key_map(LARGE_MAP_ENTRIES + 1, None); + assert_eq!( + validate_cbor_bytes(&cbor, cbor.len(), CborItemRole::Normal), + Err(CoseError::ResourceLimitExceeded), + ); +} + +#[test] +fn overlong_simple_value_is_non_canonical() { + assert_eq!( + validate_cbor_bytes(&[0xf8, 0x00], 2, CborItemRole::Normal), + Err(CoseError::NonCanonicalCbor), + ); +} + +#[test] +fn floating_point_extension_values_are_rejected() { + let encodings: [&[u8]; 3] = [ + &[0xf9, 0x3c, 0x00], + &[0xfa, 0x3f, 0x80, 0x00, 0x00], + &[0xfb, 0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + ]; + for encoding in encodings { + assert_eq!( + validate_cbor_bytes(encoding, encoding.len(), CborItemRole::Normal), + Err(CoseError::NonCanonicalCbor), + ); + } +} + +#[test] +fn invalid_utf8_is_rejected_before_sensitive_tree_decode() { + // The first map value models a private parameter that a tree decoder would + // allocate before reaching the malformed text label. Pre-validation must + // reject the label so no partially decoded sensitive tree can be dropped + // through ciborium's ordinary, non-zeroizing error path. + let cbor = [0xa2, 0x23, 0x41, 0xaa, 0x61, 0xff, 0x00]; + assert_eq!( + validate_cbor_bytes(&cbor, cbor.len(), CborItemRole::CoseKeyTop), + Err(CoseError::Cbor), + ); +} + +#[test] +fn cose_key_map_uses_core_deterministic_bytewise_key_order() { + // {24: null, -1: null}. The encoded key 0x18 0x18 sorts before 0x20 + // under RFC 8949 core deterministic bytewise ordering. + let cbor = [0xa2, 0x18, 0x18, 0xf6, 0x20, 0xf6]; + assert_eq!( + validate_cbor_bytes(&cbor, cbor.len(), CborItemRole::CoseKeyTop), + Ok(()), + ); +} + +#[test] +fn cose_key_map_rejects_length_first_order_that_violates_core_deterministic_order() { + let cbor = [0xa2, 0x20, 0xf6, 0x18, 0x18, 0xf6]; + assert_eq!( + validate_cbor_bytes(&cbor, cbor.len(), CborItemRole::CoseKeyTop), + Err(CoseError::NonCanonicalCbor), + ); +} + +fn integer_key_map(unique_entries: u64, duplicate_key: Option) -> Vec { + let total_entries = unique_entries + u64::from(duplicate_key.is_some()); + let mut cbor = Vec::new(); + append_major(&mut cbor, 5, total_entries); + for key in 0..unique_entries { + append_major(&mut cbor, 0, key); + cbor.push(0xf6); + } + if let Some(key) = duplicate_key { + append_major(&mut cbor, 0, key); + cbor.push(0xf6); + } + cbor +} + +fn append_major(output: &mut Vec, major: u8, value: u64) { + let major_bits = major << 5; + match value { + 0..=23 => output.push(major_bits | u8::try_from(value).unwrap_or(0)), + 24..=0xff => { + output.push(major_bits | 24); + output.push(u8::try_from(value).unwrap_or(0)); + } + 0x100..=0xffff => { + output.push(major_bits | 25); + output.extend_from_slice(&u16::try_from(value).unwrap_or(0).to_be_bytes()); + } + 0x1_0000..=0xffff_ffff => { + output.push(major_bits | 26); + output.extend_from_slice(&u32::try_from(value).unwrap_or(0).to_be_bytes()); + } + _ => { + output.push(major_bits | 27); + output.extend_from_slice(&value.to_be_bytes()); + } + } +} diff --git a/crates/cose/src/multikey/convert.rs b/crates/cose/src/multikey/convert.rs new file mode 100644 index 0000000..4cbf487 --- /dev/null +++ b/crates/cose/src/multikey/convert.rs @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use reallyme_crypto::core::Algorithm; + +use reallyme_codec::multikey::{encode_multikey, parse_multikey}; +use zeroize::Zeroizing; + +use crate::failure::CoseFailure; +use crate::key::convert::{ + construct_cose_key_from_public, extract_cose_key_public, CoseKeyFromPublicBytesInput, + CoseKeyOwnerOutput, CoseKeyRefInput, +}; +use crate::key::profile::algorithm_for_cose_key; +use crate::{CoseError, CoseKey}; + +pub(crate) struct MultikeyInput<'a> { + multikey: &'a str, +} + +impl<'a> MultikeyInput<'a> { + pub(crate) const fn new(multikey: &'a str) -> Self { + Self { multikey } + } +} + +#[must_use] +pub(crate) struct CoseMultikeyOutput { + multikey: Zeroizing, +} + +impl CoseMultikeyOutput { + pub(crate) fn into_zeroizing(self) -> Zeroizing { + self.multikey + } +} + +pub(crate) fn convert_cose_key_to_multikey( + input: CoseKeyRefInput<'_>, +) -> Result { + let algorithm = algorithm_for_cose_key(input.key()).map_err(CoseFailure::from)?; + let public_key = extract_cose_key_public(input)?.into_zeroizing(); + let codec_name = codec_name_for_algorithm(algorithm).map_err(CoseFailure::from)?; + encode_multikey(codec_name, &public_key) + .map(Zeroizing::new) + .map(|multikey| CoseMultikeyOutput { multikey }) + .map_err(|_| CoseFailure::from(CoseError::InvalidMultikey)) +} + +pub(crate) fn convert_multikey_to_cose_key( + input: MultikeyInput<'_>, +) -> Result { + let mut parsed = parse_multikey(input.multikey) + .map_err(|_| CoseFailure::from(CoseError::InvalidMultikey))?; + let algorithm = algorithm_for_codec_name(parsed.codec_name).map_err(CoseFailure::from)?; + let public_key = Zeroizing::new(core::mem::take(&mut parsed.public_key)); + construct_cose_key_from_public(CoseKeyFromPublicBytesInput::new(algorithm, &public_key)) +} + +/// Convert COSE_Key (public) → multikey string. +/// +/// # Errors +/// +/// Returns [`CoseError`] when the COSE_Key profile or public material is +/// invalid, the algorithm has no supported multikey mapping, or encoding fails. +pub fn cose_key_to_multikey(key: &CoseKey) -> Result, CoseError> { + convert_cose_key_to_multikey(CoseKeyRefInput::new(key)) + .map(CoseMultikeyOutput::into_zeroizing) + .map_err(CoseFailure::into_native_error) +} + +fn codec_name_for_algorithm(algorithm: Algorithm) -> Result<&'static str, CoseError> { + let codec_name = match algorithm { + Algorithm::Ed25519 => "ed25519-pub", + Algorithm::X25519 => "x25519-pub", + Algorithm::P256 => "p256-pub", + Algorithm::P384 => "p384-pub", + Algorithm::P521 => "p521-pub", + Algorithm::Secp256k1 => "secp256k1-pub", + Algorithm::MlDsa44 => "mldsa-44-pub", + Algorithm::MlDsa65 => "mldsa-65-pub", + Algorithm::MlDsa87 => "mldsa-87-pub", + Algorithm::MlKem512 => "mlkem-512-pub", + Algorithm::MlKem768 => "mlkem-768-pub", + Algorithm::MlKem1024 => "mlkem-1024-pub", + Algorithm::SlhDsaSha2_128s | Algorithm::XWing768 => { + return Err(CoseError::UnsupportedAlgorithm); + } + }; + Ok(codec_name) +} + +/// Convert multikey string → COSE_Key (public only). +/// +/// # Errors +/// +/// Returns [`CoseError`] when the multikey is malformed, its codec has no +/// supported COSE_Key mapping, or its public key material is invalid. +pub fn multikey_to_cose_key(multikey: &str) -> Result { + convert_multikey_to_cose_key(MultikeyInput::new(multikey)) + .map(CoseKeyOwnerOutput::into_key) + .map_err(CoseFailure::into_native_error) +} + +fn algorithm_for_codec_name(codec_name: &str) -> Result { + let algorithm = match codec_name { + "ed25519-pub" => Algorithm::Ed25519, + "x25519-pub" => Algorithm::X25519, + "p256-pub" => Algorithm::P256, + "p384-pub" => Algorithm::P384, + "p521-pub" => Algorithm::P521, + "secp256k1-pub" => Algorithm::Secp256k1, + "mldsa-44-pub" => Algorithm::MlDsa44, + "mldsa-65-pub" => Algorithm::MlDsa65, + "mldsa-87-pub" => Algorithm::MlDsa87, + "mlkem-512-pub" => Algorithm::MlKem512, + "mlkem-768-pub" => Algorithm::MlKem768, + "mlkem-1024-pub" => Algorithm::MlKem1024, + // The multikey codec is valid, but this bridge does not currently map + // it into a deliberate COSE_Key representation. + _ => return Err(CoseError::UnsupportedAlgorithm), + }; + Ok(algorithm) +} diff --git a/src/multikey/mod.rs b/crates/cose/src/multikey/mod.rs similarity index 90% rename from src/multikey/mod.rs rename to crates/cose/src/multikey/mod.rs index 36c1087..6dc13e7 100644 --- a/src/multikey/mod.rs +++ b/crates/cose/src/multikey/mod.rs @@ -4,6 +4,6 @@ //! COSE_Key and Multikey conversion helpers. -mod convert; +pub(crate) mod convert; pub use convert::{cose_key_to_multikey, multikey_to_cose_key}; diff --git a/crates/cose/src/operation_contract/encrypt/create.rs b/crates/cose/src/operation_contract/encrypt/create.rs new file mode 100644 index 0000000..65a37ae --- /dev/null +++ b/crates/cose/src/operation_contract/encrypt/create.rs @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Generated-contract adapters for direct and AES-KW ML-KEM encryption. + +use zeroize::Zeroizing; + +use crate::encrypt::create::{ + encrypt_cose_ml_kem_direct, encrypt_cose_ml_kem_key_wrap, CoseMlKemEncryptOutput, +}; +use crate::encrypt::types::{CoseMlKemEncryptInput, CoseMlKemEncryptRequest}; +use crate::failure::CoseFailure; +use crate::operation_contract::encrypt::result; +use crate::operation_contract::input::{content_algorithm_from_proto, ml_kem_algorithm_from_proto}; +use crate::operation_contract::map_failure::boundary_error_from_failure; +use crate::wire::{ + CoseMlKemEncryptRequest as ProtoEncryptRequest, CoseOperationResult, CoseWireResult, +}; + +pub(crate) fn direct_result(request: ProtoEncryptRequest) -> CoseWireResult { + encrypt_result( + request, + encrypt_cose_ml_kem_direct, + result::encrypted_direct, + ) +} + +pub(crate) fn key_wrap_result(request: ProtoEncryptRequest) -> CoseWireResult { + encrypt_result( + request, + encrypt_cose_ml_kem_key_wrap, + result::encrypted_key_wrap, + ) +} + +fn encrypt_result( + mut request: ProtoEncryptRequest, + operation: for<'request, 'aad> fn( + CoseMlKemEncryptInput<'request, 'aad>, + ) -> Result, + convert_result: fn(CoseMlKemEncryptOutput) -> CoseOperationResult, +) -> CoseWireResult { + let recipient_public_key = Zeroizing::new(core::mem::take(&mut request.recipient_public_key)); + let recipient_kid = Zeroizing::new(core::mem::take(&mut request.recipient_kid)); + let plaintext = Zeroizing::new(core::mem::take(&mut request.plaintext)); + let external_aad = Zeroizing::new(core::mem::take(&mut request.external_aad)); + let supp_priv_info = Zeroizing::new(core::mem::take(&mut request.supp_priv_info)); + let native_request = CoseMlKemEncryptRequest::new( + ml_kem_algorithm_from_proto(request.kem_algorithm)?, + content_algorithm_from_proto(request.content_algorithm)?, + &recipient_public_key, + &recipient_kid, + &plaintext, + request + .has_supp_priv_info + .then_some(supp_priv_info.as_slice()), + ); + let input = CoseMlKemEncryptInput::new(&native_request, &external_aad); + let output = operation(input).map_err(boundary_error_from_failure)?; + Ok(convert_result(output)) +} diff --git a/crates/cose/src/operation_contract/encrypt/decrypt.rs b/crates/cose/src/operation_contract/encrypt/decrypt.rs new file mode 100644 index 0000000..62e0fbf --- /dev/null +++ b/crates/cose/src/operation_contract/encrypt/decrypt.rs @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Generated-contract adapter for ML-KEM decryption. + +use zeroize::Zeroizing; + +use crate::encrypt::decrypt::decrypt_cose_ml_kem; +use crate::encrypt::types::{CoseMlKemDecryptInput, CoseMlKemDecryptRequest}; +use crate::operation_contract::encrypt::result; +use crate::operation_contract::map_failure::boundary_error_from_failure; +use crate::wire::{ + CoseMlKemDecryptRequest as ProtoDecryptRequest, CoseOperationResult, CoseWireResult, +}; + +pub(crate) fn result(mut request: ProtoDecryptRequest) -> CoseWireResult { + let cose_encrypt = Zeroizing::new(core::mem::take(&mut request.cose_encrypt)); + let recipient_private_key = Zeroizing::new(core::mem::take(&mut request.recipient_private_key)); + let expected_recipient_kid = + Zeroizing::new(core::mem::take(&mut request.expected_recipient_kid)); + let external_aad = Zeroizing::new(core::mem::take(&mut request.external_aad)); + let supp_priv_info = Zeroizing::new(core::mem::take(&mut request.supp_priv_info)); + let native_request = CoseMlKemDecryptRequest::new( + &cose_encrypt, + &recipient_private_key, + &expected_recipient_kid, + request + .has_supp_priv_info + .then_some(supp_priv_info.as_slice()), + ); + let decrypted = decrypt_cose_ml_kem(CoseMlKemDecryptInput::new(&native_request, &external_aad)) + .map_err(boundary_error_from_failure)?; + Ok(result::decrypted(decrypted)) +} diff --git a/crates/cose/src/operation_contract/encrypt/mod.rs b/crates/cose/src/operation_contract/encrypt/mod.rs new file mode 100644 index 0000000..c985ac6 --- /dev/null +++ b/crates/cose/src/operation_contract/encrypt/mod.rs @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Generated-contract adapters for ML-KEM COSE_Encrypt operations. + +pub(crate) mod create; +pub(crate) mod decrypt; +pub(crate) mod result; diff --git a/crates/cose/src/operation_contract/encrypt/result.rs b/crates/cose/src/operation_contract/encrypt/result.rs new file mode 100644 index 0000000..5b53fbb --- /dev/null +++ b/crates/cose/src/operation_contract/encrypt/result.rs @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Generated-result conversion for ML-KEM COSE_Encrypt operations. + +use buffa::EnumValue; + +use crate::encrypt::create::CoseMlKemEncryptOutput; +use crate::encrypt::types::{ + CoseContentEncryptionAlgorithm as NativeContentAlgorithm, CoseMlKemAlgorithm as NativeKem, + CoseMlKemMode as NativeMode, DecryptedCoseEncrypt, +}; +use crate::wire::{ + cose_operation_result::Result as OperationResultBranch, CoseContentEncryptionAlgorithm, + CoseKemAlgorithm, CoseMlKemDecryptResult, CoseMlKemEncryptResult, CoseMlKemMode, + CoseOperationResult, +}; + +pub(crate) fn encrypted_direct(output: CoseMlKemEncryptOutput) -> CoseOperationResult { + operation_result(OperationResultBranch::MlKemEncryptDirect(Box::new( + encrypt_message(output), + ))) +} + +pub(crate) fn encrypted_key_wrap(output: CoseMlKemEncryptOutput) -> CoseOperationResult { + operation_result(OperationResultBranch::MlKemEncryptKeyWrap(Box::new( + encrypt_message(output), + ))) +} + +pub(crate) fn decrypted(output: DecryptedCoseEncrypt) -> CoseOperationResult { + let mut plaintext = output.plaintext; + let mut recipient_kid = output.kid; + operation_result(OperationResultBranch::MlKemDecrypt(Box::new( + CoseMlKemDecryptResult { + plaintext: core::mem::take(&mut *plaintext), + content_algorithm: EnumValue::from(content_algorithm_to_proto( + output.content_algorithm, + )), + kem_algorithm: EnumValue::from(kem_algorithm_to_proto(output.kem_algorithm)), + mode: EnumValue::from(mode_to_proto(output.mode)), + recipient_kid: core::mem::take(&mut *recipient_kid), + __buffa_unknown_fields: Default::default(), + }, + ))) +} + +fn encrypt_message(output: CoseMlKemEncryptOutput) -> CoseMlKemEncryptResult { + let mut cose_encrypt = output.into_zeroizing(); + CoseMlKemEncryptResult { + cose_encrypt: core::mem::take(&mut *cose_encrypt), + __buffa_unknown_fields: Default::default(), + } +} + +fn operation_result(result: OperationResultBranch) -> CoseOperationResult { + CoseOperationResult { + result: Some(result), + __buffa_unknown_fields: Default::default(), + } +} + +const fn kem_algorithm_to_proto(algorithm: NativeKem) -> CoseKemAlgorithm { + match algorithm { + NativeKem::MlKem512 => CoseKemAlgorithm::MlKem512, + NativeKem::MlKem768 => CoseKemAlgorithm::MlKem768, + NativeKem::MlKem1024 => CoseKemAlgorithm::MlKem1024, + } +} + +const fn content_algorithm_to_proto( + algorithm: NativeContentAlgorithm, +) -> CoseContentEncryptionAlgorithm { + match algorithm { + NativeContentAlgorithm::Aes128Gcm => CoseContentEncryptionAlgorithm::Aes128Gcm, + NativeContentAlgorithm::Aes192Gcm => CoseContentEncryptionAlgorithm::Aes192Gcm, + NativeContentAlgorithm::Aes256Gcm => CoseContentEncryptionAlgorithm::Aes256Gcm, + } +} + +const fn mode_to_proto(mode: NativeMode) -> CoseMlKemMode { + match mode { + NativeMode::Direct => CoseMlKemMode::Direct, + NativeMode::KeyWrap => CoseMlKemMode::KeyWrap, + } +} diff --git a/crates/cose/src/operation_contract/execute.rs b/crates/cose/src/operation_contract/execute.rs new file mode 100644 index 0000000..8d685e7 --- /dev/null +++ b/crates/cose/src/operation_contract/execute.rs @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Executable binary protobuf and generated ProtoJSON operation boundary. + +use zeroize::Zeroizing; + +use crate::wire::{ + cose_operation_request, decode_json, decode_protobuf, CoseErrorReason, CoseOperationRequest, + CoseOperationResult, CoseWireError, CoseWireResult, +}; + +pub(crate) fn execute_proto(request_bytes: &[u8]) -> Zeroizing> { + let response = super::response_v2::from_result( + decode_protobuf(request_bytes).and_then(dispatch_operation), + ); + super::response_v2::encode_or_error(&response) +} + +pub(crate) fn execute_proto_json(request_json: &str) -> Zeroizing> { + let response = super::response_v2::from_result( + decode_json(request_json.as_bytes()).and_then(dispatch_operation), + ); + super::response_v2::encode_or_error(&response) +} + +fn dispatch_operation(mut request: CoseOperationRequest) -> CoseWireResult { + // The generated owner implements Drop so retained unknown fields are + // recursively wiped. Taking the oneof preserves that owner for its Drop + // path while transferring the selected request into typed dispatch. + let Some(operation) = request.operation.take() else { + return Err(CoseWireError::primitive_internal( + CoseErrorReason::CommonInvalidParameter, + )); + }; + + match operation { + cose_operation_request::Operation::Sign1Create(request) => { + super::sign1::create::attached_result(*request) + } + cose_operation_request::Operation::Sign1CreateDetached(request) => { + super::sign1::create::detached_result(*request) + } + cose_operation_request::Operation::Sign1Verify(request) => { + super::sign1::verify::attached_result(*request) + } + cose_operation_request::Operation::Sign1VerifyDetached(request) => { + super::sign1::verify::detached_result(*request) + } + cose_operation_request::Operation::KeyFromPublicBytes(request) => { + super::key::convert::from_public_bytes_result(*request) + } + cose_operation_request::Operation::KeyFromPrivateBytes(request) => { + super::key::convert::from_private_bytes_result(*request) + } + cose_operation_request::Operation::KeyParse(request) => super::key::parse::result(*request), + cose_operation_request::Operation::KeyToPublicBytes(request) => { + super::key::convert::to_public_bytes_result(*request) + } + cose_operation_request::Operation::KeyToPrivateBytes(request) => { + super::key::convert::to_private_bytes_result(*request) + } + cose_operation_request::Operation::KeyDerivePublicKid(request) => { + super::key::convert::derive_public_kid_result(*request) + } + cose_operation_request::Operation::KeyToMultikey(request) => { + super::key::convert::to_multikey_result(*request) + } + cose_operation_request::Operation::MultikeyToCoseKey(request) => { + super::key::convert::multikey_to_key_result(*request) + } + cose_operation_request::Operation::MlKemEncryptDirect(request) => { + super::encrypt::create::direct_result(*request) + } + cose_operation_request::Operation::MlKemEncryptKeyWrap(request) => { + super::encrypt::create::key_wrap_result(*request) + } + cose_operation_request::Operation::MlKemDecrypt(request) => { + super::encrypt::decrypt::result(*request) + } + } +} diff --git a/crates/cose/src/operation_contract/input.rs b/crates/cose/src/operation_contract/input.rs new file mode 100644 index 0000000..e408885 --- /dev/null +++ b/crates/cose/src/operation_contract/input.rs @@ -0,0 +1,182 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Conversion of generated request values into native domain inputs. + +use buffa::EnumValue; +use reallyme_cose_proto::generated::proto::reallyme::cose::v1::__buffa::oneof::cose_algorithm_identifier::Algorithm as CoseAlgorithmIdentifierBranch; +use reallyme_crypto::core::Algorithm; + +use crate::limits::{MAX_COSE_SIGN1_BYTES, MAX_DETACHED_PAYLOAD_BYTES}; +use crate::wire::{ + CoseAlgorithmIdentifier, CoseContentEncryptionAlgorithm, CoseErrorReason, CoseKemAlgorithm, + CoseKeyAgreementAlgorithm, CoseSign1Options, CoseSignatureAlgorithm, CoseWireError, + CoseWireResult, MAX_COSE_PROTO_MESSAGE_BYTES, +}; +use crate::{ + CoseContentEncryptionAlgorithm as NativeCoseContentEncryptionAlgorithm, + CoseMlKemAlgorithm as NativeCoseMlKemAlgorithm, CosePolicy, CoseSign1EncodeOptions, +}; + +pub(crate) fn encode_options_from_proto( + options: Option<&CoseSign1Options>, +) -> CoseWireResult { + let Some(options) = options else { + return Ok(CoseSign1EncodeOptions::default()); + }; + Ok(CoseSign1EncodeOptions::new() + .with_tag(options.tag) + .with_max_cose_sign1_bytes(optional_limit_to_usize( + options.max_cose_sign1_bytes, + MAX_COSE_SIGN1_BYTES, + )?)) +} + +pub(crate) fn policy_from_parts( + max_cose_sign1_bytes: u64, + max_detached_payload_bytes: u64, + require_kid: bool, + allowed_algorithms: &[EnumValue], +) -> CoseWireResult { + let mut allowed = Vec::with_capacity(allowed_algorithms.len()); + for candidate in allowed_algorithms { + allowed.push(signature_algorithm_from_proto(*candidate)?); + } + Ok(CosePolicy::new() + .with_require_kid(require_kid) + .with_allowed_algorithms(allowed) + .with_max_cose_sign1_bytes(optional_limit_to_usize( + max_cose_sign1_bytes, + MAX_COSE_SIGN1_BYTES, + )?) + .with_max_detached_payload_bytes(optional_limit_to_usize( + max_detached_payload_bytes, + MAX_DETACHED_PAYLOAD_BYTES, + )?)) +} + +fn optional_limit_to_usize(value: u64, default: usize) -> CoseWireResult { + if value == 0 { + return Ok(default); + } + let limit = usize::try_from(value) + .map_err(|_| CoseWireError::primitive_internal(CoseErrorReason::CommonInvalidLength))?; + if limit > MAX_COSE_PROTO_MESSAGE_BYTES { + return Err(CoseWireError::primitive_internal( + CoseErrorReason::CommonResourceLimitExceeded, + )); + } + Ok(limit) +} + +pub(crate) fn signature_algorithm_from_proto( + value: EnumValue, +) -> CoseWireResult { + let algorithm = value.as_known().ok_or(CoseWireError::primitive_internal( + CoseErrorReason::CommonInvalidParameter, + ))?; + match algorithm { + CoseSignatureAlgorithm::Ed25519 => Ok(Algorithm::Ed25519), + CoseSignatureAlgorithm::EcdsaP256Sha256 => Ok(Algorithm::P256), + CoseSignatureAlgorithm::EcdsaP384Sha384 => Ok(Algorithm::P384), + CoseSignatureAlgorithm::EcdsaP521Sha512 => Ok(Algorithm::P521), + CoseSignatureAlgorithm::EcdsaSecp256k1Sha256 => Ok(Algorithm::Secp256k1), + CoseSignatureAlgorithm::MlDsa44 => Ok(Algorithm::MlDsa44), + CoseSignatureAlgorithm::MlDsa65 => Ok(Algorithm::MlDsa65), + CoseSignatureAlgorithm::MlDsa87 => Ok(Algorithm::MlDsa87), + CoseSignatureAlgorithm::Unspecified => Err(CoseWireError::primitive_internal( + CoseErrorReason::CommonInvalidParameter, + )), + } +} + +pub(crate) fn algorithm_identifier_from_proto( + identifier: Option<&CoseAlgorithmIdentifier>, +) -> CoseWireResult { + let identifier = identifier.ok_or(CoseWireError::primitive_internal( + CoseErrorReason::CommonInvalidParameter, + ))?; + match identifier.algorithm.as_ref() { + Some(CoseAlgorithmIdentifierBranch::Signature(value)) => { + signature_algorithm_from_proto(*value) + } + Some(CoseAlgorithmIdentifierBranch::KeyAgreement(value)) => { + key_agreement_algorithm_from_proto(*value) + } + Some(CoseAlgorithmIdentifierBranch::Kem(value)) => kem_algorithm_from_proto(*value), + None => Err(CoseWireError::primitive_internal( + CoseErrorReason::CommonInvalidParameter, + )), + } +} + +fn key_agreement_algorithm_from_proto( + value: EnumValue, +) -> CoseWireResult { + let algorithm = value.as_known().ok_or(CoseWireError::primitive_internal( + CoseErrorReason::CommonInvalidParameter, + ))?; + match algorithm { + CoseKeyAgreementAlgorithm::X25519 => Ok(Algorithm::X25519), + CoseKeyAgreementAlgorithm::Unspecified => Err(CoseWireError::primitive_internal( + CoseErrorReason::CommonInvalidParameter, + )), + } +} + +fn kem_algorithm_from_proto(value: EnumValue) -> CoseWireResult { + let algorithm = value.as_known().ok_or(CoseWireError::primitive_internal( + CoseErrorReason::CommonInvalidParameter, + ))?; + match algorithm { + CoseKemAlgorithm::MlKem512 => Ok(Algorithm::MlKem512), + CoseKemAlgorithm::MlKem768 => Ok(Algorithm::MlKem768), + CoseKemAlgorithm::MlKem1024 => Ok(Algorithm::MlKem1024), + CoseKemAlgorithm::XWing768 => Ok(Algorithm::XWing768), + CoseKemAlgorithm::Unspecified => Err(CoseWireError::primitive_internal( + CoseErrorReason::CommonInvalidParameter, + )), + } +} + +pub(crate) fn ml_kem_algorithm_from_proto( + value: EnumValue, +) -> CoseWireResult { + let algorithm = value.as_known().ok_or(CoseWireError::primitive_internal( + CoseErrorReason::CommonInvalidParameter, + ))?; + match algorithm { + CoseKemAlgorithm::MlKem512 => Ok(NativeCoseMlKemAlgorithm::MlKem512), + CoseKemAlgorithm::MlKem768 => Ok(NativeCoseMlKemAlgorithm::MlKem768), + CoseKemAlgorithm::MlKem1024 => Ok(NativeCoseMlKemAlgorithm::MlKem1024), + CoseKemAlgorithm::Unspecified => Err(CoseWireError::primitive_internal( + CoseErrorReason::CommonInvalidParameter, + )), + _ => Err(CoseWireError::provider_internal( + CoseErrorReason::CommonUnsupportedAlgorithm, + )), + } +} + +pub(crate) fn content_algorithm_from_proto( + value: EnumValue, +) -> CoseWireResult { + let algorithm = value.as_known().ok_or(CoseWireError::primitive_internal( + CoseErrorReason::CommonInvalidParameter, + ))?; + match algorithm { + CoseContentEncryptionAlgorithm::Aes128Gcm => { + Ok(NativeCoseContentEncryptionAlgorithm::Aes128Gcm) + } + CoseContentEncryptionAlgorithm::Aes192Gcm => { + Ok(NativeCoseContentEncryptionAlgorithm::Aes192Gcm) + } + CoseContentEncryptionAlgorithm::Aes256Gcm => { + Ok(NativeCoseContentEncryptionAlgorithm::Aes256Gcm) + } + CoseContentEncryptionAlgorithm::Unspecified => Err(CoseWireError::primitive_internal( + CoseErrorReason::CommonInvalidParameter, + )), + } +} diff --git a/crates/cose/src/operation_contract/key/convert.rs b/crates/cose/src/operation_contract/key/convert.rs new file mode 100644 index 0000000..9efa7e4 --- /dev/null +++ b/crates/cose/src/operation_contract/key/convert.rs @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Generated-contract adapters for remaining COSE_Key and Multikey operations. + +use zeroize::{Zeroize, Zeroizing}; + +use crate::key::convert::{ + construct_cose_key_from_private, construct_cose_key_from_public, extract_cose_key_private, + extract_cose_key_public, CoseKeyFromPrivateBytesInput, CoseKeyFromPublicBytesInput, + CoseKeyRefInput, +}; +use crate::key::derive_kid::derive_cose_key_public_kid; +use crate::key::{parse_cose_key, CoseKeyParseInput, CoseKeyParseOutput}; +use crate::multikey::convert::{ + convert_cose_key_to_multikey, convert_multikey_to_cose_key, MultikeyInput, +}; +use crate::operation_contract::input::algorithm_identifier_from_proto; +use crate::operation_contract::key::result; +use crate::operation_contract::map_failure::boundary_error_from_failure; +use crate::wire::{ + CoseKeyBytesRequest, CoseKeyFromPrivateBytesRequest, CoseKeyFromPublicBytesRequest, + CoseMultikeyToCoseKeyRequest, CoseOperationResult, CoseWireResult, +}; + +pub(crate) fn from_public_bytes_result( + mut request: CoseKeyFromPublicBytesRequest, +) -> CoseWireResult { + let algorithm = algorithm_identifier_from_proto(request.algorithm.as_option())?; + let public_key = Zeroizing::new(core::mem::take(&mut request.public_key)); + let output = + construct_cose_key_from_public(CoseKeyFromPublicBytesInput::new(algorithm, &public_key)) + .map_err(boundary_error_from_failure)?; + result::from_public_key(output) +} + +pub(crate) fn from_private_bytes_result( + mut request: CoseKeyFromPrivateBytesRequest, +) -> CoseWireResult { + let algorithm = match algorithm_identifier_from_proto(request.algorithm.as_option()) { + Ok(algorithm) => algorithm, + Err(error) => { + request.private_key.zeroize(); + request.public_key.zeroize(); + return Err(error); + } + }; + let private_key = Zeroizing::new(core::mem::take(&mut request.private_key)); + let public_key = Zeroizing::new(core::mem::take(&mut request.public_key)); + let public_key = request.has_public_key.then_some(public_key.as_slice()); + let output = construct_cose_key_from_private(CoseKeyFromPrivateBytesInput::new( + algorithm, + &private_key, + public_key, + )) + .map_err(boundary_error_from_failure)?; + result::from_private_key(output) +} + +pub(crate) fn to_public_bytes_result( + request: CoseKeyBytesRequest, +) -> CoseWireResult { + let key = parse_request_key(request)?; + let output = + extract_cose_key_public(CoseKeyRefInput::new(&key)).map_err(boundary_error_from_failure)?; + Ok(result::public_key_bytes(output)) +} + +pub(crate) fn to_private_bytes_result( + request: CoseKeyBytesRequest, +) -> CoseWireResult { + let key = parse_request_key(request)?; + let output = extract_cose_key_private(CoseKeyRefInput::new(&key)) + .map_err(boundary_error_from_failure)?; + Ok(result::private_key_bytes(output)) +} + +pub(crate) fn derive_public_kid_result( + request: CoseKeyBytesRequest, +) -> CoseWireResult { + let key = parse_request_key(request)?; + let output = derive_cose_key_public_kid(CoseKeyRefInput::new(&key)) + .map_err(boundary_error_from_failure)?; + Ok(result::key_identifier(output)) +} + +pub(crate) fn to_multikey_result( + request: CoseKeyBytesRequest, +) -> CoseWireResult { + let key = parse_request_key(request)?; + let output = convert_cose_key_to_multikey(CoseKeyRefInput::new(&key)) + .map_err(boundary_error_from_failure)?; + Ok(result::multikey(output)) +} + +pub(crate) fn multikey_to_key_result( + mut request: CoseMultikeyToCoseKeyRequest, +) -> CoseWireResult { + let multikey = Zeroizing::new(core::mem::take(&mut request.multikey)); + let output = convert_multikey_to_cose_key(MultikeyInput::new(&multikey)) + .map_err(boundary_error_from_failure)?; + result::from_multikey_key(output) +} + +fn parse_request_key(mut request: CoseKeyBytesRequest) -> CoseWireResult { + let encoded_key = Zeroizing::new(core::mem::take(&mut request.cose_key)); + parse_cose_key(CoseKeyParseInput::new(&encoded_key)) + .map(CoseKeyParseOutput::into_key) + .map_err(boundary_error_from_failure) +} diff --git a/crates/cose/src/operation_contract/key/mod.rs b/crates/cose/src/operation_contract/key/mod.rs new file mode 100644 index 0000000..0c9f783 --- /dev/null +++ b/crates/cose/src/operation_contract/key/mod.rs @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Generated-contract adapters for COSE_Key operations. + +pub(crate) mod convert; +pub(crate) mod parse; +pub(crate) mod result; diff --git a/crates/cose/src/operation_contract/key/parse.rs b/crates/cose/src/operation_contract/key/parse.rs new file mode 100644 index 0000000..a2be9f8 --- /dev/null +++ b/crates/cose/src/operation_contract/key/parse.rs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Protobuf adapter for the canonical COSE_Key parse operation. + +use zeroize::{Zeroize, Zeroizing}; + +use crate::key::{parse_cose_key, CoseKeyParseInput}; +use crate::operation_contract::key::result; +use crate::operation_contract::map_failure::boundary_error_from_failure; +use crate::wire::{CoseKeyBytesRequest, CoseOperationResult, CoseWireResult}; + +pub(crate) fn result(mut request: CoseKeyBytesRequest) -> CoseWireResult { + // Transfer generated storage immediately into a zeroizing owner. The + // semantic input borrows this allocation and cannot outlive it. + let encoded_key = Zeroizing::new(core::mem::take(&mut request.cose_key)); + request.cose_key.zeroize(); + + let output = parse_cose_key(CoseKeyParseInput::new(&encoded_key)) + .map_err(boundary_error_from_failure)?; + result::parsed_key(output) +} diff --git a/crates/cose/src/operation_contract/key/result.rs b/crates/cose/src/operation_contract/key/result.rs new file mode 100644 index 0000000..6eb7f24 --- /dev/null +++ b/crates/cose/src/operation_contract/key/result.rs @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Generated-result conversion for COSE_Key and Multikey operations. + +use zeroize::Zeroizing; + +use crate::key::convert::{ + encode_cose_key, CoseKeyBytesOutput, CoseKeyOwnerOutput, CoseKeyRefInput, +}; +use crate::key::derive_kid::CoseKeyKidOutput; +use crate::key::CoseKeyParseOutput; +use crate::multikey::convert::CoseMultikeyOutput; +use crate::operation_contract::map_failure::boundary_error_from_failure; +use crate::wire::{ + cose_operation_result::Result as OperationResultBranch, CoseKeyBytesResult, CoseMultikeyResult, + CoseOperationResult, CoseWireResult, +}; + +pub(crate) fn from_public_key(output: CoseKeyOwnerOutput) -> CoseWireResult { + let message = encode_key(output.into_key())?; + Ok(operation_result(OperationResultBranch::KeyFromPublicBytes( + Box::new(message), + ))) +} + +pub(crate) fn from_private_key(output: CoseKeyOwnerOutput) -> CoseWireResult { + let message = encode_key(output.into_key())?; + Ok(operation_result( + OperationResultBranch::KeyFromPrivateBytes(Box::new(message)), + )) +} + +pub(crate) fn parsed_key(output: CoseKeyParseOutput) -> CoseWireResult { + let message = encode_key(output.into_key())?; + Ok(operation_result(OperationResultBranch::KeyParse(Box::new( + message, + )))) +} + +pub(crate) fn public_key_bytes(output: CoseKeyBytesOutput) -> CoseOperationResult { + operation_result(OperationResultBranch::KeyToPublicBytes(Box::new( + key_bytes_message(output.into_zeroizing()), + ))) +} + +pub(crate) fn private_key_bytes(output: CoseKeyBytesOutput) -> CoseOperationResult { + operation_result(OperationResultBranch::KeyToPrivateBytes(Box::new( + key_bytes_message(output.into_zeroizing()), + ))) +} + +pub(crate) fn key_identifier(output: CoseKeyKidOutput) -> CoseOperationResult { + operation_result(OperationResultBranch::KeyDerivePublicKid(Box::new( + key_bytes_message(output.into_zeroizing()), + ))) +} + +pub(crate) fn multikey(output: CoseMultikeyOutput) -> CoseOperationResult { + let mut value = output.into_zeroizing(); + operation_result(OperationResultBranch::KeyToMultikey(Box::new( + CoseMultikeyResult { + multikey: core::mem::take(&mut *value), + __buffa_unknown_fields: Default::default(), + }, + ))) +} + +pub(crate) fn from_multikey_key(output: CoseKeyOwnerOutput) -> CoseWireResult { + let message = encode_key(output.into_key())?; + Ok(operation_result(OperationResultBranch::MultikeyToCoseKey( + Box::new(message), + ))) +} + +fn encode_key(key: crate::CoseKey) -> CoseWireResult { + let output = + encode_cose_key(CoseKeyRefInput::new(&key)).map_err(boundary_error_from_failure)?; + Ok(key_bytes_message(output.into_zeroizing())) +} + +fn key_bytes_message(mut bytes: Zeroizing>) -> CoseKeyBytesResult { + CoseKeyBytesResult { + key_bytes: core::mem::take(&mut *bytes), + __buffa_unknown_fields: Default::default(), + } +} + +fn operation_result(result: OperationResultBranch) -> CoseOperationResult { + CoseOperationResult { + result: Some(result), + __buffa_unknown_fields: Default::default(), + } +} diff --git a/crates/cose/src/operation_contract/map_failure.rs b/crates/cose/src/operation_contract/map_failure.rs new file mode 100644 index 0000000..1a4e75d --- /dev/null +++ b/crates/cose/src/operation_contract/map_failure.rs @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Canonical semantic-failure mapping for protobuf adapters. + +use crate::failure::{CoseFailure, CoseFailureBranch, CoseFailureOrigin, CoseFailureReason}; + +use crate::wire::{CoseErrorReason, CoseWireError}; + +pub(crate) fn boundary_error_from_failure(failure: CoseFailure) -> CoseWireError { + let reason = reason_from_failure(failure.reason()); + match (failure.origin(), failure.branch()) { + (CoseFailureOrigin::Caller, CoseFailureBranch::Primitive) => { + CoseWireError::primitive_internal(reason) + } + (CoseFailureOrigin::Provider, CoseFailureBranch::Provider) => { + CoseWireError::provider_internal(reason) + } + (CoseFailureOrigin::Backend, CoseFailureBranch::Backend) => { + CoseWireError::backend_internal(reason) + } + // `CoseFailure` fields are private and its constructors preserve these + // pairings. This defensive branch still fails closed if that invariant + // is ever broken by an internal implementation change. + _ => CoseWireError::backend_internal(CoseErrorReason::BackendInternal), + } +} + +const fn reason_from_failure(reason: CoseFailureReason) -> CoseErrorReason { + match reason { + CoseFailureReason::CommonCbor => CoseErrorReason::CommonCbor, + CoseFailureReason::CommonUnsupportedAlgorithm => { + CoseErrorReason::CommonUnsupportedAlgorithm + } + CoseFailureReason::Sign1MissingPayload => CoseErrorReason::Sign1MissingPayload, + CoseFailureReason::Sign1InvalidSignature => CoseErrorReason::Sign1InvalidSignature, + CoseFailureReason::Sign1InvalidSignatureEncoding => { + CoseErrorReason::Sign1InvalidSignatureEncoding + } + CoseFailureReason::CommonCryptoFailed => CoseErrorReason::CommonCryptoFailed, + CoseFailureReason::ProviderUnavailable => CoseErrorReason::ProviderUnavailable, + CoseFailureReason::MultikeyInvalidMultikey => CoseErrorReason::MultikeyInvalidMultikey, + CoseFailureReason::KeyMissingKeyMaterial => CoseErrorReason::KeyMissingKeyMaterial, + CoseFailureReason::KeyInvalidKeyMaterial => CoseErrorReason::KeyInvalidKeyMaterial, + CoseFailureReason::Sign1MissingKid => CoseErrorReason::Sign1MissingKid, + CoseFailureReason::Sign1KeyNotResolved => CoseErrorReason::Sign1KeyNotResolved, + CoseFailureReason::CommonInvalidFormat => CoseErrorReason::CommonInvalidFormat, + CoseFailureReason::CommonResourceLimitExceeded => { + CoseErrorReason::CommonResourceLimitExceeded + } + CoseFailureReason::CommonNonCanonicalCbor => CoseErrorReason::CommonNonCanonicalCbor, + CoseFailureReason::CommonUnexpectedCborTag => CoseErrorReason::CommonUnexpectedCborTag, + CoseFailureReason::CommonDuplicateMapLabel => CoseErrorReason::CommonDuplicateMapLabel, + CoseFailureReason::Sign1UnsupportedCriticalHeader => { + CoseErrorReason::Sign1UnsupportedCriticalHeader + } + CoseFailureReason::Sign1UnprotectedHeaderNotAllowed => { + CoseErrorReason::Sign1UnprotectedHeaderNotAllowed + } + CoseFailureReason::Sign1MissingPrivateKey => CoseErrorReason::Sign1MissingPrivateKey, + CoseFailureReason::Sign1KidKeyMismatch => CoseErrorReason::Sign1KidKeyMismatch, + CoseFailureReason::EncryptMissingCiphertext => CoseErrorReason::EncryptMissingCiphertext, + CoseFailureReason::EncryptInvalidIv => CoseErrorReason::EncryptInvalidIv, + CoseFailureReason::EncryptInvalidRecipient => CoseErrorReason::EncryptInvalidRecipient, + CoseFailureReason::EncryptMissingEncapsulatedKey => { + CoseErrorReason::EncryptMissingEncapsulatedKey + } + CoseFailureReason::EncryptInvalidEncapsulatedKey => { + CoseErrorReason::EncryptInvalidEncapsulatedKey + } + CoseFailureReason::EncryptAuthenticationFailed => { + CoseErrorReason::EncryptAuthenticationFailed + } + CoseFailureReason::EncryptKeyUnwrapFailed => CoseErrorReason::EncryptKeyUnwrapFailed, + CoseFailureReason::EncryptKidMismatch => CoseErrorReason::EncryptKidMismatch, + #[cfg(feature = "cose-crypto")] + CoseFailureReason::EncryptMissingKid => CoseErrorReason::EncryptMissingKid, + #[cfg(feature = "cose-crypto")] + CoseFailureReason::EncryptUnprotectedHeaderNotAllowed => { + CoseErrorReason::EncryptUnprotectedHeaderNotAllowed + } + } +} diff --git a/crates/cose/src/operation_contract/map_failure_tests.rs b/crates/cose/src/operation_contract/map_failure_tests.rs new file mode 100644 index 0000000..460f106 --- /dev/null +++ b/crates/cose/src/operation_contract/map_failure_tests.rs @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use crate::failure::CoseFailure; +use crate::CoseError; + +use crate::wire::{CoseErrorReason, CoseWireErrorBranch}; + +use super::map_failure::boundary_error_from_failure; + +#[test] +fn canonical_failures_preserve_exact_wire_branch_and_reason() { + use CoseError as Native; + use CoseErrorReason as Reason; + use CoseWireErrorBranch::{Backend, Primitive, Provider}; + + let cases = [ + (Native::Cbor, Primitive, Reason::CommonCbor), + ( + Native::UnsupportedAlgorithm, + Provider, + Reason::CommonUnsupportedAlgorithm, + ), + ( + Native::MissingPayload, + Primitive, + Reason::Sign1MissingPayload, + ), + ( + Native::InvalidSignature, + Primitive, + Reason::Sign1InvalidSignature, + ), + ( + Native::InvalidSignatureEncoding, + Primitive, + Reason::Sign1InvalidSignatureEncoding, + ), + (Native::Crypto, Backend, Reason::CommonCryptoFailed), + ( + Native::ProviderUnavailable, + Provider, + Reason::ProviderUnavailable, + ), + ( + Native::InvalidMultikey, + Primitive, + Reason::MultikeyInvalidMultikey, + ), + ( + Native::MissingKeyMaterial, + Primitive, + Reason::KeyMissingKeyMaterial, + ), + ( + Native::InvalidKeyMaterial, + Primitive, + Reason::KeyInvalidKeyMaterial, + ), + (Native::MissingKid, Primitive, Reason::Sign1MissingKid), + ( + Native::KeyNotResolved, + Primitive, + Reason::Sign1KeyNotResolved, + ), + ( + Native::InvalidFormat, + Primitive, + Reason::CommonInvalidFormat, + ), + ( + Native::ResourceLimitExceeded, + Primitive, + Reason::CommonResourceLimitExceeded, + ), + ( + Native::NonCanonicalCbor, + Primitive, + Reason::CommonNonCanonicalCbor, + ), + ( + Native::UnexpectedCborTag, + Primitive, + Reason::CommonUnexpectedCborTag, + ), + ( + Native::DuplicateMapLabel, + Primitive, + Reason::CommonDuplicateMapLabel, + ), + ( + Native::UnsupportedCriticalHeader, + Primitive, + Reason::Sign1UnsupportedCriticalHeader, + ), + ( + Native::UnprotectedHeaderNotAllowed, + Primitive, + Reason::Sign1UnprotectedHeaderNotAllowed, + ), + ( + Native::MissingPrivateKey, + Primitive, + Reason::Sign1MissingPrivateKey, + ), + ( + Native::MissingCiphertext, + Primitive, + Reason::EncryptMissingCiphertext, + ), + (Native::InvalidIv, Primitive, Reason::EncryptInvalidIv), + ( + Native::InvalidRecipient, + Primitive, + Reason::EncryptInvalidRecipient, + ), + ( + Native::MissingEncapsulatedKey, + Primitive, + Reason::EncryptMissingEncapsulatedKey, + ), + ( + Native::InvalidEncapsulatedKey, + Primitive, + Reason::EncryptInvalidEncapsulatedKey, + ), + ( + Native::AuthenticationFailed, + Primitive, + Reason::EncryptAuthenticationFailed, + ), + ( + Native::KeyUnwrapFailed, + Primitive, + Reason::EncryptKeyUnwrapFailed, + ), + (Native::KidMismatch, Primitive, Reason::EncryptKidMismatch), + ]; + + for (native, expected_branch, expected_reason) in cases { + let wire = boundary_error_from_failure(CoseFailure::from(native)); + assert_eq!(wire.branch(), expected_branch); + assert_eq!(wire.reason(), expected_reason); + } +} diff --git a/crates/cose/src/operation_contract/mod.rs b/crates/cose/src/operation_contract/mod.rs new file mode 100644 index 0000000..12de028 --- /dev/null +++ b/crates/cose/src/operation_contract/mod.rs @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Executable adapters for the generated COSE operation contract. + +pub(crate) mod encrypt; +pub(crate) mod execute; +pub(crate) mod input; +pub(crate) mod key; +mod map_failure; +pub(crate) mod response_v2; +pub(crate) mod sign1; + +#[cfg(test)] +mod map_failure_tests; diff --git a/crates/cose/src/operation_contract/response_v2.rs b/crates/cose/src/operation_contract/response_v2.rs new file mode 100644 index 0000000..0da6b59 --- /dev/null +++ b/crates/cose/src/operation_contract/response_v2.rs @@ -0,0 +1,234 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Generated operation-response construction and validation. + +use buffa::EnumValue; +use zeroize::Zeroizing; + +use crate::wire::{ + cose_error, cose_operation_request, decode_protobuf_with_limit, encode_protobuf, + validate_cose_error_proto_wire, CoseContentEncryptionAlgorithm, CoseErrorReason, + CoseKemAlgorithm, CoseMlKemDecryptResult, CoseMlKemMode, CoseOperationOutcomeV2, + CoseOperationRequest, CoseOperationResponseV2, CoseOperationResult, CoseOperationResultBranch, + CoseSignatureAlgorithm, CoseWireError, CoseWireResult, MAX_COSE_PROTO_MESSAGE_BYTES, +}; + +const MAX_RESPONSE_OVERHEAD_BYTES: usize = 32; + +pub(crate) fn from_result(result: CoseWireResult) -> CoseOperationResponseV2 { + let outcome = match result { + Ok(result) => CoseOperationOutcomeV2::Result(Box::new(result)), + Err(error) => CoseOperationOutcomeV2::Error(Box::new(cose_error(error))), + }; + CoseOperationResponseV2 { + outcome: Some(outcome), + __buffa_unknown_fields: Default::default(), + } +} + +pub(crate) fn from_error(error: CoseWireError) -> CoseOperationResponseV2 { + from_result(Err(error)) +} + +pub(crate) fn encode_or_error(response: &CoseOperationResponseV2) -> Zeroizing> { + let encoded = encode_protobuf(response); + let maximum = match maximum_response_bytes() { + Ok(maximum) => maximum, + Err(error) => return encode_protobuf(&from_result(Err(error))), + }; + if encoded.len() > maximum { + return encode_protobuf(&from_result(Err(CoseWireError::backend_internal( + CoseErrorReason::CommonResourceLimitExceeded, + )))); + } + encoded +} + +pub(crate) fn decode_for_request( + request: &CoseOperationRequest, + bytes: &[u8], +) -> Result { + let response = decode(bytes)?; + validate_for_request(request, &response).map_err(from_error)?; + Ok(response) +} + +pub(crate) fn decode(bytes: &[u8]) -> Result { + let maximum = maximum_response_bytes().map_err(from_error)?; + if bytes.len() > maximum { + return Err(from_result(Err(CoseWireError::backend_internal( + CoseErrorReason::CommonResourceLimitExceeded, + )))); + } + + let response = decode_protobuf_with_limit::(bytes, maximum) + .map_err(|error| from_result(Err(response_boundary_error(error))))?; + validate(&response).map_err(from_error)?; + Ok(response) +} + +fn validate(response: &CoseOperationResponseV2) -> CoseWireResult<()> { + match response.outcome.as_ref() { + Some(CoseOperationOutcomeV2::Result(result)) => validate_result(result), + Some(CoseOperationOutcomeV2::Error(error)) => { + validate_cose_error_proto_wire(error).map_err(|_| malformed_response_error()) + } + None => Err(malformed_response_error()), + } +} + +fn validate_for_request( + request: &CoseOperationRequest, + response: &CoseOperationResponseV2, +) -> CoseWireResult<()> { + match response.outcome.as_ref() { + Some(CoseOperationOutcomeV2::Result(result)) => { + if result_matches_request(request, result) { + Ok(()) + } else { + Err(malformed_response_error()) + } + } + Some(CoseOperationOutcomeV2::Error(_)) => Ok(()), + None => Err(malformed_response_error()), + } +} + +fn validate_result(result: &CoseOperationResult) -> CoseWireResult<()> { + let valid = match result.result.as_ref() { + Some( + CoseOperationResultBranch::Sign1Create(_) + | CoseOperationResultBranch::Sign1CreateDetached(_) + | CoseOperationResultBranch::KeyFromPublicBytes(_) + | CoseOperationResultBranch::KeyFromPrivateBytes(_) + | CoseOperationResultBranch::KeyParse(_) + | CoseOperationResultBranch::KeyToPublicBytes(_) + | CoseOperationResultBranch::KeyToPrivateBytes(_) + | CoseOperationResultBranch::KeyDerivePublicKid(_) + | CoseOperationResultBranch::KeyToMultikey(_) + | CoseOperationResultBranch::MultikeyToCoseKey(_) + | CoseOperationResultBranch::MlKemEncryptDirect(_) + | CoseOperationResultBranch::MlKemEncryptKeyWrap(_), + ) => true, + Some( + CoseOperationResultBranch::Sign1Verify(message) + | CoseOperationResultBranch::Sign1VerifyDetached(message), + ) => signature_algorithm_is_valid(message.algorithm), + Some(CoseOperationResultBranch::MlKemDecrypt(message)) => { + decrypt_metadata_is_valid(message) + } + None => false, + }; + if valid { + Ok(()) + } else { + Err(malformed_response_error()) + } +} + +fn result_matches_request(request: &CoseOperationRequest, result: &CoseOperationResult) -> bool { + matches!( + (request.operation.as_ref(), result.result.as_ref()), + ( + Some(cose_operation_request::Operation::Sign1Create(_)), + Some(CoseOperationResultBranch::Sign1Create(_)), + ) | ( + Some(cose_operation_request::Operation::Sign1CreateDetached(_)), + Some(CoseOperationResultBranch::Sign1CreateDetached(_)), + ) | ( + Some(cose_operation_request::Operation::Sign1Verify(_)), + Some(CoseOperationResultBranch::Sign1Verify(_)), + ) | ( + Some(cose_operation_request::Operation::Sign1VerifyDetached(_)), + Some(CoseOperationResultBranch::Sign1VerifyDetached(_)), + ) | ( + Some(cose_operation_request::Operation::KeyFromPublicBytes(_)), + Some(CoseOperationResultBranch::KeyFromPublicBytes(_)), + ) | ( + Some(cose_operation_request::Operation::KeyFromPrivateBytes(_)), + Some(CoseOperationResultBranch::KeyFromPrivateBytes(_)), + ) | ( + Some(cose_operation_request::Operation::KeyParse(_)), + Some(CoseOperationResultBranch::KeyParse(_)), + ) | ( + Some(cose_operation_request::Operation::KeyToPublicBytes(_)), + Some(CoseOperationResultBranch::KeyToPublicBytes(_)), + ) | ( + Some(cose_operation_request::Operation::KeyToPrivateBytes(_)), + Some(CoseOperationResultBranch::KeyToPrivateBytes(_)), + ) | ( + Some(cose_operation_request::Operation::KeyDerivePublicKid(_)), + Some(CoseOperationResultBranch::KeyDerivePublicKid(_)), + ) | ( + Some(cose_operation_request::Operation::KeyToMultikey(_)), + Some(CoseOperationResultBranch::KeyToMultikey(_)), + ) | ( + Some(cose_operation_request::Operation::MultikeyToCoseKey(_)), + Some(CoseOperationResultBranch::MultikeyToCoseKey(_)), + ) | ( + Some(cose_operation_request::Operation::MlKemEncryptDirect(_)), + Some(CoseOperationResultBranch::MlKemEncryptDirect(_)), + ) | ( + Some(cose_operation_request::Operation::MlKemEncryptKeyWrap(_)), + Some(CoseOperationResultBranch::MlKemEncryptKeyWrap(_)), + ) | ( + Some(cose_operation_request::Operation::MlKemDecrypt(_)), + Some(CoseOperationResultBranch::MlKemDecrypt(_)), + ) + ) +} + +fn signature_algorithm_is_valid(value: EnumValue) -> bool { + matches!( + value.as_known(), + Some( + CoseSignatureAlgorithm::Ed25519 + | CoseSignatureAlgorithm::EcdsaP256Sha256 + | CoseSignatureAlgorithm::EcdsaP384Sha384 + | CoseSignatureAlgorithm::EcdsaP521Sha512 + | CoseSignatureAlgorithm::EcdsaSecp256k1Sha256 + | CoseSignatureAlgorithm::MlDsa44 + | CoseSignatureAlgorithm::MlDsa65 + | CoseSignatureAlgorithm::MlDsa87 + ) + ) +} + +fn decrypt_metadata_is_valid(result: &CoseMlKemDecryptResult) -> bool { + matches!( + result.content_algorithm.as_known(), + Some( + CoseContentEncryptionAlgorithm::Aes128Gcm + | CoseContentEncryptionAlgorithm::Aes192Gcm + | CoseContentEncryptionAlgorithm::Aes256Gcm + ) + ) && matches!( + result.kem_algorithm.as_known(), + Some(CoseKemAlgorithm::MlKem512 | CoseKemAlgorithm::MlKem768 | CoseKemAlgorithm::MlKem1024) + ) && matches!( + result.mode.as_known(), + Some(CoseMlKemMode::Direct | CoseMlKemMode::KeyWrap) + ) +} + +fn maximum_response_bytes() -> CoseWireResult { + MAX_COSE_PROTO_MESSAGE_BYTES + .checked_add(MAX_RESPONSE_OVERHEAD_BYTES) + .ok_or(CoseWireError::backend_internal( + CoseErrorReason::BackendInternal, + )) +} + +const fn response_boundary_error(error: CoseWireError) -> CoseWireError { + if matches!(error.reason(), CoseErrorReason::CommonResourceLimitExceeded) { + CoseWireError::backend_internal(CoseErrorReason::CommonResourceLimitExceeded) + } else { + malformed_response_error() + } +} + +const fn malformed_response_error() -> CoseWireError { + CoseWireError::backend_internal(CoseErrorReason::BackendInternal) +} diff --git a/crates/cose/src/operation_contract/sign1/create.rs b/crates/cose/src/operation_contract/sign1/create.rs new file mode 100644 index 0000000..77233bd --- /dev/null +++ b/crates/cose/src/operation_contract/sign1/create.rs @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Generated-contract adapters for attached and detached COSE_Sign1 creation. + +use zeroize::Zeroizing; + +use crate::operation_contract::input::{encode_options_from_proto, signature_algorithm_from_proto}; +use crate::operation_contract::map_failure::boundary_error_from_failure; +use crate::operation_contract::sign1::result; +use crate::sign1::sign::{create_cose_sign1, create_detached_cose_sign1}; +use crate::sign1::types::CoseSign1CreateInput; +use crate::wire::{ + CoseOperationResult, CoseSign1CreateDetachedRequest, CoseSign1CreateRequest, CoseWireResult, +}; + +pub(crate) fn attached_result( + mut request: CoseSign1CreateRequest, +) -> CoseWireResult { + let payload = Zeroizing::new(core::mem::take(&mut request.payload)); + let private_key = Zeroizing::new(core::mem::take(&mut request.private_key)); + let kid = Zeroizing::new(core::mem::take(&mut request.kid)); + let external_aad = Zeroizing::new(core::mem::take(&mut request.external_aad)); + let algorithm = signature_algorithm_from_proto(request.algorithm)?; + let options = encode_options_from_proto(request.options.as_option())?; + let kid = request.has_kid.then_some(kid.as_slice()); + let result = create_cose_sign1(CoseSign1CreateInput::new( + algorithm, + &payload, + &private_key, + kid, + &external_aad, + options, + )) + .map_err(boundary_error_from_failure)?; + Ok(result::created_attached(result)) +} + +pub(crate) fn detached_result( + mut request: CoseSign1CreateDetachedRequest, +) -> CoseWireResult { + let payload = Zeroizing::new(core::mem::take(&mut request.payload)); + let private_key = Zeroizing::new(core::mem::take(&mut request.private_key)); + let kid = Zeroizing::new(core::mem::take(&mut request.kid)); + let external_aad = Zeroizing::new(core::mem::take(&mut request.external_aad)); + let algorithm = signature_algorithm_from_proto(request.algorithm)?; + let options = encode_options_from_proto(request.options.as_option())?; + let kid = request.has_kid.then_some(kid.as_slice()); + let result = create_detached_cose_sign1(CoseSign1CreateInput::new( + algorithm, + &payload, + &private_key, + kid, + &external_aad, + options, + )) + .map_err(boundary_error_from_failure)?; + Ok(result::created_detached(result)) +} diff --git a/crates/cose/src/operation_contract/sign1/mod.rs b/crates/cose/src/operation_contract/sign1/mod.rs new file mode 100644 index 0000000..2f3570c --- /dev/null +++ b/crates/cose/src/operation_contract/sign1/mod.rs @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Generated-contract adapters for COSE_Sign1 operations. + +pub(crate) mod create; +pub(crate) mod result; +pub(crate) mod verify; diff --git a/crates/cose/src/operation_contract/sign1/result.rs b/crates/cose/src/operation_contract/sign1/result.rs new file mode 100644 index 0000000..137c536 --- /dev/null +++ b/crates/cose/src/operation_contract/sign1/result.rs @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Generated-result conversion for COSE_Sign1 operations. + +use buffa::EnumValue; +use reallyme_crypto::core::Algorithm; +use zeroize::Zeroizing; + +use crate::sign1::sign::CoseSign1CreateOutput; +use crate::sign1::verify::{VerifiedCoseSign1, VerifiedDetachedCoseSign1}; +use crate::wire::{ + cose_operation_result::Result as OperationResultBranch, CoseErrorReason, CoseOperationResult, + CoseSign1CreateResult, CoseSign1VerifyResult, CoseSignatureAlgorithm, CoseWireError, + CoseWireResult, +}; + +pub(crate) fn created_attached(output: CoseSign1CreateOutput) -> CoseOperationResult { + operation_result(OperationResultBranch::Sign1Create(Box::new( + create_message(output), + ))) +} + +pub(crate) fn created_detached(output: CoseSign1CreateOutput) -> CoseOperationResult { + operation_result(OperationResultBranch::Sign1CreateDetached(Box::new( + create_message(output), + ))) +} + +pub(crate) fn verified_attached(output: VerifiedCoseSign1) -> CoseWireResult { + let message = verify_message(output.payload, output.alg, output.kid)?; + Ok(operation_result(OperationResultBranch::Sign1Verify( + Box::new(message), + ))) +} + +pub(crate) fn verified_detached( + output: VerifiedDetachedCoseSign1, +) -> CoseWireResult { + let message = verify_message(Zeroizing::new(Vec::new()), output.alg, output.kid)?; + Ok(operation_result( + OperationResultBranch::Sign1VerifyDetached(Box::new(message)), + )) +} + +fn create_message(output: CoseSign1CreateOutput) -> CoseSign1CreateResult { + let mut cose_sign1 = output.into_zeroizing(); + CoseSign1CreateResult { + cose_sign1: core::mem::take(&mut *cose_sign1), + __buffa_unknown_fields: Default::default(), + } +} + +fn verify_message( + mut payload: Zeroizing>, + algorithm: Algorithm, + mut kid: Zeroizing>, +) -> CoseWireResult { + Ok(CoseSign1VerifyResult { + payload: core::mem::take(&mut *payload), + algorithm: EnumValue::from(signature_algorithm_to_proto(algorithm)?), + kid: core::mem::take(&mut *kid), + __buffa_unknown_fields: Default::default(), + }) +} + +fn operation_result(result: OperationResultBranch) -> CoseOperationResult { + CoseOperationResult { + result: Some(result), + __buffa_unknown_fields: Default::default(), + } +} + +fn signature_algorithm_to_proto(algorithm: Algorithm) -> CoseWireResult { + match algorithm { + Algorithm::Ed25519 => Ok(CoseSignatureAlgorithm::Ed25519), + Algorithm::P256 => Ok(CoseSignatureAlgorithm::EcdsaP256Sha256), + Algorithm::P384 => Ok(CoseSignatureAlgorithm::EcdsaP384Sha384), + Algorithm::P521 => Ok(CoseSignatureAlgorithm::EcdsaP521Sha512), + Algorithm::Secp256k1 => Ok(CoseSignatureAlgorithm::EcdsaSecp256k1Sha256), + Algorithm::MlDsa44 => Ok(CoseSignatureAlgorithm::MlDsa44), + Algorithm::MlDsa65 => Ok(CoseSignatureAlgorithm::MlDsa65), + Algorithm::MlDsa87 => Ok(CoseSignatureAlgorithm::MlDsa87), + Algorithm::X25519 + | Algorithm::MlKem512 + | Algorithm::MlKem768 + | Algorithm::MlKem1024 + | Algorithm::SlhDsaSha2_128s + | Algorithm::XWing768 => Err(CoseWireError::provider_internal( + CoseErrorReason::CommonUnsupportedAlgorithm, + )), + } +} diff --git a/crates/cose/src/operation_contract/sign1/verify.rs b/crates/cose/src/operation_contract/sign1/verify.rs new file mode 100644 index 0000000..f5d6820 --- /dev/null +++ b/crates/cose/src/operation_contract/sign1/verify.rs @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Generated-contract adapters for attached and detached COSE_Sign1 verification. + +use zeroize::Zeroizing; + +use crate::operation_contract::input::policy_from_parts; +use crate::operation_contract::map_failure::boundary_error_from_failure; +use crate::operation_contract::sign1::result; +use crate::sign1::types::{ + CoseSign1DetachedVerifyInput, CoseSign1KeyResolution, CoseSign1VerifyInput, +}; +use crate::sign1::verify::{verify_cose_sign1, verify_detached_cose_sign1}; +use crate::wire::{ + CoseOperationResult, CoseSign1VerifyDetachedRequest, CoseSign1VerifyRequest, CoseWireResult, +}; + +pub(crate) fn attached_result( + mut request: CoseSign1VerifyRequest, +) -> CoseWireResult { + let cose_sign1 = Zeroizing::new(core::mem::take(&mut request.cose_sign1)); + let public_key = Zeroizing::new(core::mem::take(&mut request.public_key)); + let external_aad = Zeroizing::new(core::mem::take(&mut request.external_aad)); + let expected_kid = Zeroizing::new(core::mem::take(&mut request.expected_kid)); + let policy = policy_from_parts( + request.max_cose_sign1_bytes, + request.max_detached_payload_bytes, + request.require_kid, + &request.allowed_algorithms, + )?; + let verified = verify_cose_sign1( + CoseSign1VerifyInput::new(&cose_sign1, &external_aad, &policy), + |algorithm, protected_kid| { + resolve_request_key(algorithm, protected_kid, &expected_kid, public_key) + }, + ) + .map_err(boundary_error_from_failure)?; + result::verified_attached(verified) +} + +pub(crate) fn detached_result( + mut request: CoseSign1VerifyDetachedRequest, +) -> CoseWireResult { + let cose_sign1 = Zeroizing::new(core::mem::take(&mut request.cose_sign1)); + let payload = Zeroizing::new(core::mem::take(&mut request.payload)); + let public_key = Zeroizing::new(core::mem::take(&mut request.public_key)); + let external_aad = Zeroizing::new(core::mem::take(&mut request.external_aad)); + let expected_kid = Zeroizing::new(core::mem::take(&mut request.expected_kid)); + let policy = policy_from_parts( + request.max_cose_sign1_bytes, + request.max_detached_payload_bytes, + request.require_kid, + &request.allowed_algorithms, + )?; + let verified = verify_detached_cose_sign1( + CoseSign1DetachedVerifyInput::new(&cose_sign1, &payload, &external_aad, &policy), + |algorithm, protected_kid| { + resolve_request_key(algorithm, protected_kid, &expected_kid, public_key) + }, + ) + .map_err(boundary_error_from_failure)?; + result::verified_detached(verified) +} + +fn resolve_request_key( + _expected_algorithm: crate::Algorithm, + protected_kid: &[u8], + expected_kid: &[u8], + public_key: Zeroizing>, +) -> CoseSign1KeyResolution { + if expected_kid.is_empty() + || reallyme_crypto::operations::constant_time::equal(protected_kid, expected_kid) + { + CoseSign1KeyResolution::Resolved(public_key) + } else { + CoseSign1KeyResolution::KidMismatch + } +} diff --git a/src/policy/mod.rs b/crates/cose/src/policy/mod.rs similarity index 60% rename from src/policy/mod.rs rename to crates/cose/src/policy/mod.rs index 5474e97..69caadf 100644 --- a/src/policy/mod.rs +++ b/crates/cose/src/policy/mod.rs @@ -6,4 +6,6 @@ mod validate; -pub use validate::{validate_cose_sign1_policy, CosePolicy}; +#[cfg(feature = "cose-crypto")] +pub(crate) use validate::validate_cose_sign1_policy; +pub use validate::CosePolicy; diff --git a/crates/cose/src/policy/validate.rs b/crates/cose/src/policy/validate.rs new file mode 100644 index 0000000..9c085b0 --- /dev/null +++ b/crates/cose/src/policy/validate.rs @@ -0,0 +1,143 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 +#[cfg(feature = "cose-crypto")] +use crate::algorithm::algorithm_from_cose_alg; +#[cfg(feature = "cose-crypto")] +use coset::CoseSign1; +use reallyme_crypto::core::Algorithm; + +use crate::limits::{MAX_COSE_SIGN1_BYTES, MAX_DETACHED_PAYLOAD_BYTES}; +#[cfg(feature = "cose-crypto")] +use crate::CoseError; + +/// Verification policy for COSE_Sign1 byte-boundary APIs. +#[must_use] +#[derive(Debug, Clone)] +pub struct CosePolicy { + /// Require a `kid` / key_id in protected header. + require_kid: bool, + + /// Allowed algorithms. Empty means any algorithm supported by this crate. + allowed_algs: Vec, + + /// Maximum accepted encoded COSE_Sign1 bytes at public verification APIs. + max_cose_sign1_bytes: usize, + + /// Maximum accepted detached payload bytes at detached verification APIs. + max_detached_payload_bytes: usize, +} + +impl Default for CosePolicy { + fn default() -> Self { + Self { + require_kid: false, + allowed_algs: Vec::new(), + max_cose_sign1_bytes: MAX_COSE_SIGN1_BYTES, + max_detached_payload_bytes: MAX_DETACHED_PAYLOAD_BYTES, + } + } +} + +impl CosePolicy { + /// Construct the default verification policy. + pub fn new() -> Self { + Self::default() + } + + /// Return whether protected-header `kid` is required. + #[must_use] + pub fn require_kid(&self) -> bool { + self.require_kid + } + + /// Return the configured algorithm allow-list. + /// + /// An empty list means any algorithm supported by this crate is accepted. + #[must_use] + pub fn allowed_algorithms(&self) -> &[Algorithm] { + &self.allowed_algs + } + + /// Return the maximum accepted encoded COSE_Sign1 size. + #[must_use] + pub fn max_cose_sign1_bytes(&self) -> usize { + self.max_cose_sign1_bytes + } + + /// Return the maximum accepted detached payload size. + #[must_use] + pub fn max_detached_payload_bytes(&self) -> usize { + self.max_detached_payload_bytes + } + + /// Configure whether protected-header `kid` is required. + pub fn with_require_kid(mut self, require_kid: bool) -> Self { + self.require_kid = require_kid; + self + } + + /// Replace the algorithm allow-list. + /// + /// Pass an empty iterator to accept any algorithm supported by this crate. + pub fn with_allowed_algorithms( + mut self, + allowed_algorithms: impl IntoIterator, + ) -> Self { + self.allowed_algs = allowed_algorithms.into_iter().collect(); + self + } + + /// Add one algorithm to the allow-list. + pub fn allow_algorithm(mut self, algorithm: Algorithm) -> Self { + self.allowed_algs.push(algorithm); + self + } + + /// Configure the maximum accepted encoded COSE_Sign1 size. + pub fn with_max_cose_sign1_bytes(mut self, max_cose_sign1_bytes: usize) -> Self { + self.max_cose_sign1_bytes = max_cose_sign1_bytes; + self + } + + /// Configure the maximum accepted detached payload size. + pub fn with_max_detached_payload_bytes(mut self, max_detached_payload_bytes: usize) -> Self { + self.max_detached_payload_bytes = max_detached_payload_bytes; + self + } +} + +/// Validate COSE_Sign1 header policy without performing cryptographic verification. +/// +/// # Errors +/// +/// Returns [`CoseError`] when required protected headers are missing, an +/// integrity-sensitive header is unprotected, or the algorithm is disallowed. +#[cfg(feature = "cose-crypto")] +pub(crate) fn validate_cose_sign1_policy( + cose: &CoseSign1, + policy: &CosePolicy, +) -> Result<(), CoseError> { + // --- kid requirement --- + if policy.require_kid() && cose.protected.header.key_id.is_empty() { + return Err(CoseError::MissingKid); + } + + // --- algorithm allow-list --- + if !policy.allowed_algorithms().is_empty() { + let cose_alg = cose + .protected + .header + .alg + .as_ref() + .ok_or(CoseError::UnsupportedAlgorithm)?; + + let alg = algorithm_from_cose_alg(cose_alg)?; + + if !policy.allowed_algorithms().contains(&alg) { + return Err(CoseError::UnsupportedAlgorithm); + } + } + + Ok(()) +} diff --git a/crates/cose/src/sign1/build_sig_structure.rs b/crates/cose/src/sign1/build_sig_structure.rs new file mode 100644 index 0000000..e9f4e04 --- /dev/null +++ b/crates/cose/src/sign1/build_sig_structure.rs @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use zeroize::Zeroizing; + +use crate::CoseError; + +const CBOR_DIRECT_ARGUMENT_MAX: usize = 23; +const CBOR_ONE_BYTE_ARGUMENT_MAX: usize = 0xff; +const CBOR_TWO_BYTE_ARGUMENT_MAX: usize = 0xffff; +const CBOR_FOUR_BYTE_ARGUMENT_MAX: usize = 0xffff_ffff; + +/// Build Sig_structure bytes per RFC 9052 §4.4. +pub(crate) fn build_sig_structure( + protected_bytes: &[u8], + external_aad: &[u8], + payload: &[u8], +) -> Result>, CoseError> { + const ARRAY_OF_FOUR: u8 = 0x84; + const SIGNATURE1_TEXT: &[u8] = b"Signature1"; + const SIGNATURE1_TEXT_HEADER: u8 = 0x6a; + + let capacity = 1usize + .checked_add(1) + .and_then(|size| size.checked_add(SIGNATURE1_TEXT.len())) + .and_then(|size| checked_bstr_size(size, protected_bytes.len())) + .and_then(|size| checked_bstr_size(size, external_aad.len())) + .and_then(|size| checked_bstr_size(size, payload.len())) + .ok_or(CoseError::ResourceLimitExceeded)?; + let mut encoded = Zeroizing::new(Vec::with_capacity(capacity)); + encoded.push(ARRAY_OF_FOUR); + encoded.push(SIGNATURE1_TEXT_HEADER); + encoded.extend_from_slice(SIGNATURE1_TEXT); + append_bstr(&mut encoded, protected_bytes)?; + append_bstr(&mut encoded, external_aad)?; + append_bstr(&mut encoded, payload)?; + Ok(encoded) +} + +fn checked_bstr_size(current: usize, payload_len: usize) -> Option { + current + .checked_add(cbor_length_header_size(payload_len))? + .checked_add(payload_len) +} + +const fn cbor_length_header_size(length: usize) -> usize { + if length <= CBOR_DIRECT_ARGUMENT_MAX { + 1 + } else if length <= CBOR_ONE_BYTE_ARGUMENT_MAX { + 2 + } else if length <= CBOR_TWO_BYTE_ARGUMENT_MAX { + 3 + } else if length <= CBOR_FOUR_BYTE_ARGUMENT_MAX { + 5 + } else { + 9 + } +} + +#[cfg(test)] +#[path = "build_sig_structure_tests.rs"] +mod tests; + +fn append_bstr(output: &mut Vec, bytes: &[u8]) -> Result<(), CoseError> { + const BSTR_MAJOR: u8 = 0x40; + match bytes.len() { + 0..=23 => { + let length = u8::try_from(bytes.len()).map_err(|_| CoseError::ResourceLimitExceeded)?; + output.push(BSTR_MAJOR | length); + } + 24..=0xff => { + output.push(BSTR_MAJOR | 24); + output.push(u8::try_from(bytes.len()).map_err(|_| CoseError::ResourceLimitExceeded)?); + } + 0x100..=0xffff => { + output.push(BSTR_MAJOR | 25); + output.extend_from_slice( + &u16::try_from(bytes.len()) + .map_err(|_| CoseError::ResourceLimitExceeded)? + .to_be_bytes(), + ); + } + 0x1_0000..=0xffff_ffff => { + output.push(BSTR_MAJOR | 26); + output.extend_from_slice( + &u32::try_from(bytes.len()) + .map_err(|_| CoseError::ResourceLimitExceeded)? + .to_be_bytes(), + ); + } + _ => { + output.push(BSTR_MAJOR | 27); + output.extend_from_slice( + &u64::try_from(bytes.len()) + .map_err(|_| CoseError::ResourceLimitExceeded)? + .to_be_bytes(), + ); + } + } + output.extend_from_slice(bytes); + Ok(()) +} diff --git a/crates/cose/src/sign1/build_sig_structure_tests.rs b/crates/cose/src/sign1/build_sig_structure_tests.rs new file mode 100644 index 0000000..bb47a80 --- /dev/null +++ b/crates/cose/src/sign1/build_sig_structure_tests.rs @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use super::build_sig_structure; +use crate::limits::MAX_DETACHED_PAYLOAD_BYTES; +use crate::CoseError; + +#[test] +fn encodes_the_canonical_cose_signature1_structure() -> Result<(), CoseError> { + let encoded = build_sig_structure(&[0xa1, 0x01, 0x27], &[0x01], b"payload")?; + + assert_eq!( + encoded.as_slice(), + [ + 0x84, 0x6a, b'S', b'i', b'g', b'n', b'a', b't', b'u', b'r', b'e', b'1', 0x43, 0xa1, + 0x01, 0x27, 0x41, 0x01, 0x47, b'p', b'a', b'y', b'l', b'o', b'a', b'd', + ] + ); + Ok(()) +} + +#[test] +fn accepts_the_documented_maximum_payload() -> Result<(), CoseError> { + let payload = vec![0x5a; MAX_DETACHED_PAYLOAD_BYTES]; + let encoded = build_sig_structure(&[], &[], &payload)?; + + let payload_header_offset = 14; + assert_eq!( + &encoded[payload_header_offset..payload_header_offset + 5], + &[0x5a, 0x00, 0x10, 0x00, 0x00] + ); + assert_eq!(encoded.len(), payload.len() + payload_header_offset + 5); + Ok(()) +} diff --git a/src/sign1/convert_ecdsa_signature.rs b/crates/cose/src/sign1/convert_signature.rs similarity index 64% rename from src/sign1/convert_ecdsa_signature.rs rename to crates/cose/src/sign1/convert_signature.rs index 9c55409..fc02c5d 100644 --- a/src/sign1/convert_ecdsa_signature.rs +++ b/crates/cose/src/sign1/convert_signature.rs @@ -2,17 +2,20 @@ // // SPDX-License-Identifier: Apache-2.0 -//! ECDSA signature encoding conversion between COSE and backend formats. +//! Signature validation and encoding conversion at the COSE boundary. //! //! COSE (RFC 9053 §2.1) encodes ECDSA signatures as the fixed-width //! concatenation `r || s`, each padded to the curve coordinate width. The //! `reallyme-crypto` NIST-curve backends produce and consume ASN.1 DER //! `ECDSA-Sig-Value` encodings, so this module converts strictly between the //! two forms at the COSE byte boundary and fails closed on malformed input. +//! Ed25519 and ML-DSA are already encoded in their COSE wire forms, but their +//! exact widths are enforced here so a permissive backend cannot accept a +//! truncated signature, a valid prefix, or trailing bytes. use reallyme_crypto::core::Algorithm; -use crate::key::convert::{P256_COORDINATE_BYTES, P384_COORDINATE_BYTES, P521_COORDINATE_BYTES}; +use crate::key::ec::{P256_COORDINATE_BYTES, P384_COORDINATE_BYTES, P521_COORDINATE_BYTES}; use crate::CoseError; const DER_SEQUENCE_TAG: u8 = 0x30; @@ -21,6 +24,10 @@ const DER_LONG_FORM_LENGTH_ONE_BYTE: u8 = 0x81; const DER_SHORT_FORM_LENGTH_MAX: usize = 0x7f; const DER_LONG_FORM_LENGTH_MAX: usize = 0xff; const SIGN_BIT_MASK: u8 = 0x80; +const ED25519_SIGNATURE_BYTES: usize = 64; +const ML_DSA_44_SIGNATURE_BYTES: usize = 2_420; +const ML_DSA_65_SIGNATURE_BYTES: usize = 3_309; +const ML_DSA_87_SIGNATURE_BYTES: usize = 4_627; /// Coordinate width for algorithms whose backend signature encoding is DER. /// @@ -35,14 +42,36 @@ fn der_backed_coordinate_len(alg: Algorithm) -> Option { } } +/// Exact width for algorithms whose backend already uses the COSE encoding. +fn direct_signature_len(alg: Algorithm) -> Option { + match alg { + Algorithm::Ed25519 => Some(ED25519_SIGNATURE_BYTES), + Algorithm::MlDsa44 => Some(ML_DSA_44_SIGNATURE_BYTES), + Algorithm::MlDsa65 => Some(ML_DSA_65_SIGNATURE_BYTES), + Algorithm::MlDsa87 => Some(ML_DSA_87_SIGNATURE_BYTES), + _ => None, + } +} + +fn validate_direct_signature_len(alg: Algorithm, signature: &[u8]) -> Result<(), CoseError> { + if direct_signature_len(alg).is_some_and(|expected| signature.len() != expected) { + return Err(CoseError::InvalidSignatureEncoding); + } + Ok(()) +} + /// Convert a backend-produced signature into the COSE wire encoding. pub(crate) fn cose_signature_from_backend( alg: Algorithm, signature: Vec, ) -> Result, CoseError> { match der_backed_coordinate_len(alg) { - Some(coordinate_len) => der_to_fixed_width(&signature, coordinate_len), - None => Ok(signature), + Some(coordinate_len) => der_to_fixed_width(&signature, coordinate_len) + .map_err(|_| CoseError::InvalidSignatureEncoding), + None => { + validate_direct_signature_len(alg, &signature)?; + Ok(signature) + } } } @@ -52,15 +81,19 @@ pub(crate) fn backend_signature_from_cose( signature: &[u8], ) -> Result, CoseError> { match der_backed_coordinate_len(alg) { - Some(coordinate_len) => fixed_width_to_der(signature, coordinate_len), - None => Ok(signature.to_vec()), + Some(coordinate_len) => fixed_width_to_der(signature, coordinate_len) + .map_err(|_| CoseError::InvalidSignatureEncoding), + None => { + validate_direct_signature_len(alg, signature)?; + Ok(signature.to_vec()) + } } } fn der_to_fixed_width(der: &[u8], coordinate_len: usize) -> Result, CoseError> { - let sequence_tag = *der.first().ok_or(CoseError::InvalidSignature)?; + let sequence_tag = *der.first().ok_or(CoseError::InvalidSignatureEncoding)?; if sequence_tag != DER_SEQUENCE_TAG { - return Err(CoseError::InvalidSignature); + return Err(CoseError::InvalidSignatureEncoding); } let (content_len, content_start) = read_der_length(der, 1)?; @@ -239,114 +272,5 @@ fn write_der_integer(der: &mut Vec, value: &[u8]) -> Result<(), CoseError> { } #[cfg(test)] -mod tests { - #![allow(clippy::unwrap_used)] - - use super::{backend_signature_from_cose, cose_signature_from_backend}; - use crate::CoseError; - use reallyme_crypto::core::Algorithm; - - const P256_SIGNATURE_LEN: usize = 64; - const P521_SIGNATURE_LEN: usize = 132; - - fn sample_fixed(len: usize) -> Vec { - (0..len) - .map(|index| u8::try_from(index % 251).unwrap().wrapping_add(1)) - .collect() - } - - #[test] - fn fixed_roundtrips_through_der() { - for (alg, len) in [ - (Algorithm::P256, P256_SIGNATURE_LEN), - (Algorithm::P384, 96), - (Algorithm::P521, P521_SIGNATURE_LEN), - ] { - let fixed = sample_fixed(len); - let der = backend_signature_from_cose(alg, &fixed).unwrap(); - let back = cose_signature_from_backend(alg, der).unwrap(); - assert_eq!(back, fixed); - } - } - - #[test] - fn high_bit_scalars_gain_der_sign_padding_and_roundtrip() { - let mut fixed = vec![0xff_u8; P256_SIGNATURE_LEN]; - fixed[0] = 0x80; - fixed[32] = 0x80; - let der = backend_signature_from_cose(Algorithm::P256, &fixed).unwrap(); - assert_eq!(der[4], 0); - let back = cose_signature_from_backend(Algorithm::P256, der).unwrap(); - assert_eq!(back, fixed); - } - - #[test] - fn short_scalars_left_pad_to_coordinate_width() { - let mut fixed = vec![0_u8; P256_SIGNATURE_LEN]; - fixed[31] = 0x01; - fixed[63] = 0x02; - let der = backend_signature_from_cose(Algorithm::P256, &fixed).unwrap(); - let back = cose_signature_from_backend(Algorithm::P256, der).unwrap(); - assert_eq!(back, fixed); - } - - #[test] - fn p521_uses_long_form_der_length() { - let fixed = sample_fixed(P521_SIGNATURE_LEN); - let der = backend_signature_from_cose(Algorithm::P521, &fixed).unwrap(); - assert_eq!(der[1], 0x81); - let back = cose_signature_from_backend(Algorithm::P521, der).unwrap(); - assert_eq!(back, fixed); - } - - #[test] - fn wrong_length_fixed_signature_is_rejected() { - let fixed = sample_fixed(P256_SIGNATURE_LEN - 1); - let err = backend_signature_from_cose(Algorithm::P256, &fixed).unwrap_err(); - assert_eq!(err, CoseError::InvalidSignature); - } - - #[test] - fn zero_scalars_are_rejected() { - let fixed = vec![0_u8; P256_SIGNATURE_LEN]; - let err = backend_signature_from_cose(Algorithm::P256, &fixed).unwrap_err(); - assert_eq!(err, CoseError::InvalidSignature); - } - - #[test] - fn der_with_trailing_bytes_is_rejected() { - let fixed = sample_fixed(P256_SIGNATURE_LEN); - let mut der = backend_signature_from_cose(Algorithm::P256, &fixed).unwrap(); - der.push(0); - let err = cose_signature_from_backend(Algorithm::P256, der).unwrap_err(); - assert_eq!(err, CoseError::InvalidSignature); - } - - #[test] - fn negative_der_integer_is_rejected() { - // SEQUENCE { INTEGER 0x80 (negative), INTEGER 0x01 } - let der = vec![0x30, 0x06, 0x02, 0x01, 0x80, 0x02, 0x01, 0x01]; - let err = cose_signature_from_backend(Algorithm::P256, der).unwrap_err(); - assert_eq!(err, CoseError::InvalidSignature); - } - - #[test] - fn non_minimal_der_integer_is_rejected() { - // SEQUENCE { INTEGER 0x00 0x01 (non-minimal), INTEGER 0x01 } - let der = vec![0x30, 0x07, 0x02, 0x02, 0x00, 0x01, 0x02, 0x01, 0x01]; - let err = cose_signature_from_backend(Algorithm::P256, der).unwrap_err(); - assert_eq!(err, CoseError::InvalidSignature); - } - - #[test] - fn ed25519_and_secp256k1_pass_through_unchanged() { - let fixed = sample_fixed(64); - for alg in [Algorithm::Ed25519, Algorithm::Secp256k1] { - assert_eq!( - cose_signature_from_backend(alg, fixed.clone()).unwrap(), - fixed - ); - assert_eq!(backend_signature_from_cose(alg, &fixed).unwrap(), fixed); - } - } -} +#[path = "convert_signature_tests.rs"] +mod tests; diff --git a/crates/cose/src/sign1/convert_signature_tests.rs b/crates/cose/src/sign1/convert_signature_tests.rs new file mode 100644 index 0000000..47b1559 --- /dev/null +++ b/crates/cose/src/sign1/convert_signature_tests.rs @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +#![allow(clippy::unwrap_used)] + +use super::{backend_signature_from_cose, cose_signature_from_backend}; +use crate::CoseError; +use reallyme_crypto::core::Algorithm; + +const P256_SIGNATURE_LEN: usize = 64; +const P521_SIGNATURE_LEN: usize = 132; + +fn sample_fixed(len: usize) -> Vec { + (0..len) + .map(|index| u8::try_from(index % 251).unwrap().wrapping_add(1)) + .collect() +} + +#[test] +fn fixed_roundtrips_through_der() { + for (alg, len) in [ + (Algorithm::P256, P256_SIGNATURE_LEN), + (Algorithm::P384, 96), + (Algorithm::P521, P521_SIGNATURE_LEN), + ] { + let fixed = sample_fixed(len); + let der = backend_signature_from_cose(alg, &fixed).unwrap(); + let back = cose_signature_from_backend(alg, der).unwrap(); + assert_eq!(back, fixed); + } +} + +#[test] +fn high_bit_scalars_gain_der_sign_padding_and_roundtrip() { + let mut fixed = vec![0xff_u8; P256_SIGNATURE_LEN]; + fixed[0] = 0x80; + fixed[32] = 0x80; + let der = backend_signature_from_cose(Algorithm::P256, &fixed).unwrap(); + assert_eq!(der[4], 0); + let back = cose_signature_from_backend(Algorithm::P256, der).unwrap(); + assert_eq!(back, fixed); +} + +#[test] +fn short_scalars_left_pad_to_coordinate_width() { + let mut fixed = vec![0_u8; P256_SIGNATURE_LEN]; + fixed[31] = 0x01; + fixed[63] = 0x02; + let der = backend_signature_from_cose(Algorithm::P256, &fixed).unwrap(); + let back = cose_signature_from_backend(Algorithm::P256, der).unwrap(); + assert_eq!(back, fixed); +} + +#[test] +fn p521_uses_long_form_der_length() { + let fixed = sample_fixed(P521_SIGNATURE_LEN); + let der = backend_signature_from_cose(Algorithm::P521, &fixed).unwrap(); + assert_eq!(der[1], 0x81); + let back = cose_signature_from_backend(Algorithm::P521, der).unwrap(); + assert_eq!(back, fixed); +} + +#[test] +fn wrong_length_fixed_signature_is_rejected() { + let fixed = sample_fixed(P256_SIGNATURE_LEN - 1); + let err = backend_signature_from_cose(Algorithm::P256, &fixed).unwrap_err(); + assert_eq!(err, CoseError::InvalidSignatureEncoding); +} + +#[test] +fn zero_scalars_are_rejected() { + let fixed = vec![0_u8; P256_SIGNATURE_LEN]; + let err = backend_signature_from_cose(Algorithm::P256, &fixed).unwrap_err(); + assert_eq!(err, CoseError::InvalidSignatureEncoding); +} + +#[test] +fn der_with_trailing_bytes_is_rejected() { + let fixed = sample_fixed(P256_SIGNATURE_LEN); + let mut der = backend_signature_from_cose(Algorithm::P256, &fixed).unwrap(); + der.push(0); + let err = cose_signature_from_backend(Algorithm::P256, der).unwrap_err(); + assert_eq!(err, CoseError::InvalidSignatureEncoding); +} + +#[test] +fn negative_der_integer_is_rejected() { + let der = vec![0x30, 0x06, 0x02, 0x01, 0x80, 0x02, 0x01, 0x01]; + let err = cose_signature_from_backend(Algorithm::P256, der).unwrap_err(); + assert_eq!(err, CoseError::InvalidSignatureEncoding); +} + +#[test] +fn non_minimal_der_integer_is_rejected() { + let der = vec![0x30, 0x07, 0x02, 0x02, 0x00, 0x01, 0x02, 0x01, 0x01]; + let err = cose_signature_from_backend(Algorithm::P256, der).unwrap_err(); + assert_eq!(err, CoseError::InvalidSignatureEncoding); +} + +#[test] +fn direct_signature_encodings_pass_through_unchanged() { + let fixed = sample_fixed(64); + assert_eq!( + cose_signature_from_backend(Algorithm::Ed25519, fixed.clone()).unwrap(), + fixed + ); + assert_eq!( + backend_signature_from_cose(Algorithm::Ed25519, &fixed).unwrap(), + fixed + ); +} + +#[test] +fn direct_signature_encodings_reject_wrong_widths() { + for (alg, expected_len) in [ + (Algorithm::Ed25519, 64_usize), + (Algorithm::MlDsa44, 2_420), + (Algorithm::MlDsa65, 3_309), + (Algorithm::MlDsa87, 4_627), + ] { + for invalid_len in [expected_len - 1, expected_len + 1] { + let signature = vec![1_u8; invalid_len]; + assert_eq!( + backend_signature_from_cose(alg, &signature), + Err(CoseError::InvalidSignatureEncoding), + ); + assert_eq!( + cose_signature_from_backend(alg, signature), + Err(CoseError::InvalidSignatureEncoding), + ); + } + } +} + +#[test] +fn secp256k1_backend_encoding_remains_opaque() { + let signature = sample_fixed(64); + assert_eq!( + cose_signature_from_backend(Algorithm::Secp256k1, signature.clone()).unwrap(), + signature + ); + assert_eq!( + backend_signature_from_cose(Algorithm::Secp256k1, &signature).unwrap(), + signature + ); +} diff --git a/crates/cose/src/sign1/decode.rs b/crates/cose/src/sign1/decode.rs new file mode 100644 index 0000000..30c39c0 --- /dev/null +++ b/crates/cose/src/sign1/decode.rs @@ -0,0 +1,192 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Bounded COSE_Sign1 decoding and structural validation. + +use ciborium::value::Value; +use coset::{iana, AsCborValue, CoseSign1, Header, ProtectedHeader, RegisteredLabelWithPrivate}; + +use crate::limits::validate_protected_header_bytes; +use crate::zeroize_coset::{zeroize_cose_sign1, SensitiveCborValue}; +use crate::CoseError; + +/// Decode untagged COSE_Sign1 bytes, or bytes carrying the registered +/// COSE_Sign1 tag (18) already allowed by the byte-boundary tag policy. +pub(super) fn decode_cose_sign1( + cose_bytes: &[u8], + max_len: usize, +) -> Result { + let decoded = SensitiveCborValue::decode_cose_sign1(cose_bytes, max_len)?; + let body = match decoded.value() { + Value::Tag(18, body) => body.as_ref(), + value => value, + }; + let items = match body { + Value::Array(items) => items, + _ => return Err(CoseError::InvalidFormat), + }; + let [protected_value, unprotected_value, payload_value, signature_value] = items.as_slice() + else { + return Err(CoseError::InvalidFormat); + }; + + // Establish the recursive wipe owner before cloning any semantic field. + // Every rejected header or field type below therefore clears both the + // original CBOR tree and the partially constructed COSE object. + let mut cose = SensitiveCoseSign1::new(CoseSign1::default()); + decode_protected_header(protected_value, &mut cose.inner_mut().protected)?; + decode_sign1_header( + unprotected_value, + &mut cose.inner_mut().unprotected, + Sign1HeaderBucket::Unprotected, + )?; + cose.inner_mut().payload = match payload_value { + Value::Bytes(payload) => Some(payload.clone()), + Value::Null => None, + _ => return Err(CoseError::InvalidFormat), + }; + cose.inner_mut().signature = match signature_value { + Value::Bytes(signature) => signature.clone(), + _ => return Err(CoseError::InvalidSignatureEncoding), + }; + Ok(cose) +} + +#[derive(Clone, Copy)] +enum Sign1HeaderBucket { + Protected, + Unprotected, +} + +fn decode_protected_header( + value: &Value, + protected: &mut ProtectedHeader, +) -> Result<(), CoseError> { + let bytes = match value { + Value::Bytes(bytes) => bytes, + _ => return Err(CoseError::InvalidFormat), + }; + protected.original_data = Some(bytes.clone()); + if bytes.is_empty() { + return Ok(()); + } + + let decoded = SensitiveCborValue::decode_protected_header(bytes)?; + decode_sign1_header( + decoded.value(), + &mut protected.header, + Sign1HeaderBucket::Protected, + ) +} + +fn decode_sign1_header( + value: &Value, + header: &mut Header, + bucket: Sign1HeaderBucket, +) -> Result<(), CoseError> { + let entries = match value { + Value::Map(entries) => entries, + _ => return Err(CoseError::InvalidFormat), + }; + let mut saw_algorithm = false; + let mut saw_kid = false; + + for (label, value) in entries { + let label = match label { + Value::Integer(integer) => { + i64::try_from(*integer).map_err(|_| CoseError::InvalidFormat)? + } + Value::Text(_) => return Err(CoseError::InvalidFormat), + _ => return Err(CoseError::InvalidFormat), + }; + if label == iana::HeaderParameter::Alg as i64 { + if saw_algorithm { + return Err(CoseError::DuplicateMapLabel); + } + saw_algorithm = true; + if matches!(bucket, Sign1HeaderBucket::Unprotected) { + return Err(CoseError::UnprotectedHeaderNotAllowed); + } + header.alg = Some(parse_header_algorithm(value)?); + } else if label == iana::HeaderParameter::Kid as i64 { + if saw_kid { + return Err(CoseError::DuplicateMapLabel); + } + saw_kid = true; + if matches!(bucket, Sign1HeaderBucket::Unprotected) { + return Err(CoseError::UnprotectedHeaderNotAllowed); + } + header.key_id = match value { + Value::Bytes(kid) if !kid.is_empty() => kid.clone(), + _ => return Err(CoseError::InvalidFormat), + }; + } else if label == iana::HeaderParameter::Crit as i64 { + return Err(CoseError::UnsupportedCriticalHeader); + } else { + // This SDK does not expose processing results for content-type, + // countersignatures, extension headers, or application-specific + // unprotected metadata. Accepting them would imply semantics that + // the returned verified result cannot represent. + return Err(CoseError::InvalidFormat); + } + } + Ok(()) +} + +fn parse_header_algorithm( + value: &Value, +) -> Result, CoseError> { + match value { + Value::Integer(integer) => { + RegisteredLabelWithPrivate::from_cbor_value(Value::Integer(*integer)) + .map_err(|_| CoseError::InvalidFormat) + } + Value::Text(text) => Ok(RegisteredLabelWithPrivate::Text(text.clone())), + _ => Err(CoseError::InvalidFormat), + } +} + +pub(super) struct SensitiveCoseSign1 { + inner: CoseSign1, +} + +impl SensitiveCoseSign1 { + fn new(inner: CoseSign1) -> Self { + Self { inner } + } + + pub(super) fn inner(&self) -> &CoseSign1 { + &self.inner + } + + pub(super) fn inner_mut(&mut self) -> &mut CoseSign1 { + &mut self.inner + } +} + +impl Drop for SensitiveCoseSign1 { + fn drop(&mut self) { + zeroize_cose_sign1(&mut self.inner); + } +} + +pub(super) fn validate_cose_sign1_structure(cose: &CoseSign1) -> Result<(), CoseError> { + if let Some(protected_bytes) = &cose.protected.original_data { + validate_protected_header_bytes(protected_bytes)?; + } + + if !cose.protected.header.crit.is_empty() { + return Err(CoseError::UnsupportedCriticalHeader); + } + + if cose.unprotected.alg.is_some() || !cose.unprotected.key_id.is_empty() { + return Err(CoseError::UnprotectedHeaderNotAllowed); + } + + if !cose.unprotected.crit.is_empty() { + return Err(CoseError::UnsupportedCriticalHeader); + } + + Ok(()) +} diff --git a/crates/cose/src/sign1/mod.rs b/crates/cose/src/sign1/mod.rs new file mode 100644 index 0000000..fe1b638 --- /dev/null +++ b/crates/cose/src/sign1/mod.rs @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! COSE_Sign1 signing and verification. + +#[cfg(feature = "cose-crypto")] +mod build_sig_structure; +#[cfg(feature = "cose-crypto")] +mod convert_signature; +#[cfg(feature = "cose-crypto")] +mod decode; +#[cfg(feature = "cose-crypto")] +mod provider; +#[cfg(feature = "cose-crypto")] +pub(crate) mod sign; +#[cfg(feature = "cose-crypto")] +pub(crate) mod types; +#[cfg(feature = "cose-crypto")] +pub(crate) mod verify; + +#[cfg(feature = "cose-crypto")] +pub use provider::{CoseSigner, CoseSignerError}; +#[cfg(feature = "cose-crypto")] +pub use sign::{ + cose_sign1, cose_sign1_detached, cose_sign1_detached_tagged, cose_sign1_detached_with_options, + cose_sign1_detached_with_options_and_external_aad, cose_sign1_detached_with_signer, + cose_sign1_tagged, cose_sign1_with_options, cose_sign1_with_options_and_external_aad, + cose_sign1_with_signer, CoseSign1EncodeOptions, +}; +#[cfg(feature = "cose-crypto")] +pub use verify::{ + cose_verify1, cose_verify1_detached, cose_verify1_detached_with_metadata, + cose_verify1_detached_with_policy, cose_verify1_detached_with_policy_and_external_aad, + cose_verify1_with_metadata, cose_verify1_with_policy, + cose_verify1_with_policy_and_external_aad, VerifiedCoseSign1, VerifiedDetachedCoseSign1, +}; diff --git a/crates/cose/src/sign1/provider.rs b/crates/cose/src/sign1/provider.rs new file mode 100644 index 0000000..48e888e --- /dev/null +++ b/crates/cose/src/sign1/provider.rs @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Typed signing-provider boundary for non-exportable platform keys. + +use reallyme_crypto::core::Algorithm; +use thiserror::Error; +use zeroize::Zeroizing; + +use crate::CoseError; + +/// Stable failures returned by an application or platform signing provider. +/// +/// Variants deliberately carry no backend text, key handle, payload, or other +/// dynamic context so they remain safe to propagate through audit-facing APIs. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)] +pub enum CoseSignerError { + /// The provider does not implement the requested signature algorithm. + #[error("signing algorithm unsupported by provider")] + UnsupportedAlgorithm, + /// The provider rejected or could not resolve its configured key handle. + #[error("signing key unavailable or invalid")] + InvalidKey, + /// The provider, hardware, or required user-presence mechanism is unavailable. + #[error("signing provider unavailable")] + Unavailable, + /// Signing failed without caller-safe diagnostic detail. + #[error("signing backend failure")] + Backend, +} + +impl From for CoseError { + fn from(error: CoseSignerError) -> Self { + match error { + CoseSignerError::UnsupportedAlgorithm => Self::UnsupportedAlgorithm, + CoseSignerError::InvalidKey => Self::InvalidKeyMaterial, + CoseSignerError::Unavailable => Self::ProviderUnavailable, + CoseSignerError::Backend => Self::Crypto, + } + } +} + +/// Provider for signing with an application-owned or non-exportable key. +/// +/// The provider receives the complete COSE `Sig_structure` and returns the +/// native signature representation expected for [`Self::algorithm`]: DER for +/// NIST ECDSA algorithms and fixed-width bytes for the other supported +/// algorithms. The COSE layer validates and converts that result before it is +/// encoded, so providers do not implement COSE or CBOR semantics. +pub trait CoseSigner { + /// Algorithm bound to the provider's key handle. + fn algorithm(&self) -> Algorithm; + + /// Sign an exact COSE `Sig_structure` without exporting private key bytes. + /// + /// # Errors + /// + /// Returns a fixed [`CoseSignerError`] when the key, provider, algorithm, + /// or backend cannot complete the operation. + fn sign(&self, sig_structure: &[u8]) -> Result>, CoseSignerError>; +} diff --git a/crates/cose/src/sign1/sign.rs b/crates/cose/src/sign1/sign.rs new file mode 100644 index 0000000..bd757a2 --- /dev/null +++ b/crates/cose/src/sign1/sign.rs @@ -0,0 +1,416 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use coset::{AsCborValue, CoseSign1, Header, ProtectedHeader, RegisteredLabelWithPrivate}; +use reallyme_crypto::core::Algorithm; +use reallyme_crypto::dispatch::sign; + +use crate::failure::CoseFailure; +use crate::{ + encode_cbor::{encode_cbor_value, encode_protected_header}, + error::sign_error_from_algorithm_error, + key::map_algorithm::alg_to_cose, + CoseError, +}; + +use super::build_sig_structure::build_sig_structure; +use super::convert_signature::cose_signature_from_backend; +use crate::limits::{ + validate_cose_sign1_bytes_with_limit, validate_detached_payload, MAX_COSE_SIGN1_BYTES, +}; +use zeroize::Zeroizing; + +use super::provider::CoseSigner; +use super::types::{CoseSign1CreateInput, CoseSign1SigningSource}; + +#[must_use] +pub(crate) struct CoseSign1CreateOutput { + cose_sign1: Zeroizing>, +} + +impl CoseSign1CreateOutput { + pub(crate) fn into_zeroizing(self) -> Zeroizing> { + self.cose_sign1 + } +} + +#[derive(Clone, Copy)] +enum Sign1PayloadMode { + Attached, + Detached, +} + +/// Encoding controls for COSE_Sign1 signing APIs. +#[must_use] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct CoseSign1EncodeOptions { + /// Emit the registered COSE_Sign1 root tag (18). + tag: bool, + + /// Maximum encoded COSE_Sign1 size accepted after signing. + max_cose_sign1_bytes: usize, +} + +impl Default for CoseSign1EncodeOptions { + fn default() -> Self { + Self::new() + } +} + +impl CoseSign1EncodeOptions { + /// Construct the default untagged encoding options. + pub const fn new() -> Self { + Self { + tag: false, + max_cose_sign1_bytes: MAX_COSE_SIGN1_BYTES, + } + } + + /// Construct options that emit the registered COSE_Sign1 root tag (18). + pub const fn tagged() -> Self { + Self { + tag: true, + max_cose_sign1_bytes: MAX_COSE_SIGN1_BYTES, + } + } + + /// Return whether the encoded COSE_Sign1 root tag (18) is emitted. + #[must_use] + pub const fn tag(&self) -> bool { + self.tag + } + + /// Return the maximum encoded COSE_Sign1 size accepted after signing. + #[must_use] + pub const fn max_cose_sign1_bytes(&self) -> usize { + self.max_cose_sign1_bytes + } + + /// Configure whether the registered COSE_Sign1 root tag (18) is emitted. + pub const fn with_tag(mut self, tag: bool) -> Self { + self.tag = tag; + self + } + + /// Configure the maximum encoded COSE_Sign1 size accepted after signing. + pub const fn with_max_cose_sign1_bytes(mut self, max_cose_sign1_bytes: usize) -> Self { + self.max_cose_sign1_bytes = max_cose_sign1_bytes; + self + } +} + +/// Create COSE_Sign1 with an attached payload. +/// +/// # Errors +/// +/// Returns [`CoseError`] when the algorithm, key, input, signature encoding, +/// or encoded output violates the supported COSE profile or resource limits. +pub fn cose_sign1( + alg: Algorithm, + payload: &[u8], + private_key: &[u8], + kid: Option<&[u8]>, +) -> Result>, CoseError> { + cose_sign1_with_options( + alg, + payload, + private_key, + kid, + CoseSign1EncodeOptions::default(), + ) +} + +/// Create tagged COSE_Sign1 with an attached payload. +/// +/// # Errors +/// +/// Returns [`CoseError`] when the algorithm, key, input, signature encoding, +/// or encoded output violates the supported COSE profile or resource limits. +pub fn cose_sign1_tagged( + alg: Algorithm, + payload: &[u8], + private_key: &[u8], + kid: Option<&[u8]>, +) -> Result>, CoseError> { + cose_sign1_with_options( + alg, + payload, + private_key, + kid, + CoseSign1EncodeOptions::tagged(), + ) +} + +/// Create COSE_Sign1 with an attached payload and explicit encoding options. +/// +/// # Errors +/// +/// Returns [`CoseError`] when the algorithm, key, input, signature encoding, +/// encoding options, or encoded output violates the supported COSE profile. +pub fn cose_sign1_with_options( + alg: Algorithm, + payload: &[u8], + private_key: &[u8], + kid: Option<&[u8]>, + options: CoseSign1EncodeOptions, +) -> Result>, CoseError> { + cose_sign1_with_options_and_external_aad(alg, payload, private_key, kid, &[], options) +} + +/// Create COSE_Sign1 with an attached payload, external AAD, and explicit +/// encoding options. +/// +/// # Errors +/// +/// Returns [`CoseError`] when an input, key, algorithm, signature, encoding, +/// external-AAD limit, or output-size policy is invalid. +pub fn cose_sign1_with_options_and_external_aad( + alg: Algorithm, + payload: &[u8], + private_key: &[u8], + kid: Option<&[u8]>, + external_aad: &[u8], + options: CoseSign1EncodeOptions, +) -> Result>, CoseError> { + create_cose_sign1(CoseSign1CreateInput::new( + alg, + payload, + private_key, + kid, + external_aad, + options, + )) + .map(CoseSign1CreateOutput::into_zeroizing) + .map_err(CoseFailure::into_native_error) +} + +/// Create COSE_Sign1 with a detached payload. +/// +/// # Errors +/// +/// Returns [`CoseError`] when the algorithm, key, detached payload, signature +/// encoding, or encoded output violates the supported COSE profile. +pub fn cose_sign1_detached( + alg: Algorithm, + payload: &[u8], + private_key: &[u8], + kid: Option<&[u8]>, +) -> Result>, CoseError> { + cose_sign1_detached_with_options( + alg, + payload, + private_key, + kid, + CoseSign1EncodeOptions::default(), + ) +} + +/// Create tagged COSE_Sign1 with a detached payload. +/// +/// # Errors +/// +/// Returns [`CoseError`] when the algorithm, key, detached payload, signature +/// encoding, or encoded output violates the supported COSE profile. +pub fn cose_sign1_detached_tagged( + alg: Algorithm, + payload: &[u8], + private_key: &[u8], + kid: Option<&[u8]>, +) -> Result>, CoseError> { + cose_sign1_detached_with_options( + alg, + payload, + private_key, + kid, + CoseSign1EncodeOptions::tagged(), + ) +} + +/// Create COSE_Sign1 with a detached payload and explicit encoding options. +/// +/// # Errors +/// +/// Returns [`CoseError`] when the algorithm, key, detached payload, signature +/// encoding, encoding options, or output violates the supported COSE profile. +pub fn cose_sign1_detached_with_options( + alg: Algorithm, + payload: &[u8], + private_key: &[u8], + kid: Option<&[u8]>, + options: CoseSign1EncodeOptions, +) -> Result>, CoseError> { + cose_sign1_detached_with_options_and_external_aad(alg, payload, private_key, kid, &[], options) +} + +/// Create detached COSE_Sign1 with external AAD and explicit encoding options. +/// +/// # Errors +/// +/// Returns [`CoseError`] when an input, key, algorithm, signature, encoding, +/// external-AAD limit, or output-size policy is invalid. +pub fn cose_sign1_detached_with_options_and_external_aad( + alg: Algorithm, + payload: &[u8], + private_key: &[u8], + kid: Option<&[u8]>, + external_aad: &[u8], + options: CoseSign1EncodeOptions, +) -> Result>, CoseError> { + create_detached_cose_sign1(CoseSign1CreateInput::new( + alg, + payload, + private_key, + kid, + external_aad, + options, + )) + .map(CoseSign1CreateOutput::into_zeroizing) + .map_err(CoseFailure::into_native_error) +} + +/// Create an attached COSE_Sign1 with a non-exportable provider-owned key. +/// +/// # Errors +/// +/// Returns [`CoseError`] when input validation, provider signing, signature +/// normalization, or bounded COSE encoding fails. +pub fn cose_sign1_with_signer( + signer: &dyn CoseSigner, + payload: &[u8], + kid: Option<&[u8]>, + external_aad: &[u8], + options: CoseSign1EncodeOptions, +) -> Result>, CoseError> { + create_cose_sign1(CoseSign1CreateInput::with_signer( + signer, + payload, + kid, + external_aad, + options, + )) + .map(CoseSign1CreateOutput::into_zeroizing) + .map_err(CoseFailure::into_native_error) +} + +/// Create a detached COSE_Sign1 with a non-exportable provider-owned key. +/// +/// # Errors +/// +/// Returns [`CoseError`] when input validation, provider signing, signature +/// normalization, or bounded COSE encoding fails. +pub fn cose_sign1_detached_with_signer( + signer: &dyn CoseSigner, + payload: &[u8], + kid: Option<&[u8]>, + external_aad: &[u8], + options: CoseSign1EncodeOptions, +) -> Result>, CoseError> { + create_detached_cose_sign1(CoseSign1CreateInput::with_signer( + signer, + payload, + kid, + external_aad, + options, + )) + .map(CoseSign1CreateOutput::into_zeroizing) + .map_err(CoseFailure::into_native_error) +} + +pub(crate) fn create_cose_sign1( + input: CoseSign1CreateInput<'_>, +) -> Result { + create_cose_sign1_impl(input, Sign1PayloadMode::Attached) + .map(|cose_sign1| CoseSign1CreateOutput { cose_sign1 }) + .map_err(CoseFailure::from) +} + +pub(crate) fn create_detached_cose_sign1( + input: CoseSign1CreateInput<'_>, +) -> Result { + create_cose_sign1_impl(input, Sign1PayloadMode::Detached) + .map(|cose_sign1| CoseSign1CreateOutput { cose_sign1 }) + .map_err(CoseFailure::from) +} + +fn create_cose_sign1_impl( + input: CoseSign1CreateInput<'_>, + payload_mode: Sign1PayloadMode, +) -> Result>, CoseError> { + validate_detached_payload(input.payload)?; + validate_detached_payload(input.external_aad)?; + let protected = build_protected_header(input.algorithm, input.kid)?; + let signature = sign_payload( + input.algorithm, + input.signing_source, + &protected, + input.external_aad, + input.payload, + )?; + let payload = match payload_mode { + Sign1PayloadMode::Attached => Some(input.payload.to_vec()), + Sign1PayloadMode::Detached => None, + }; + encode_cose_sign1( + CoseSign1 { + protected, + unprotected: Header::default(), + payload, + signature, + }, + input.options, + ) +} + +fn build_protected_header( + alg: Algorithm, + kid: Option<&[u8]>, +) -> Result { + let cose_alg = alg_to_cose(alg)?; + let header = Header { + alg: Some(RegisteredLabelWithPrivate::Assigned(cose_alg)), + key_id: kid.map(<[u8]>::to_vec).unwrap_or_default(), + ..Default::default() + }; + + Ok(ProtectedHeader { + header, + original_data: None, + }) +} + +fn sign_payload( + alg: Algorithm, + signing_source: CoseSign1SigningSource<'_>, + protected: &ProtectedHeader, + external_aad: &[u8], + payload: &[u8], +) -> Result, CoseError> { + let protected_bytes = encode_protected_header(protected)?; + let to_sign = build_sig_structure(&protected_bytes, external_aad, payload)?; + + let mut backend_signature = match signing_source { + CoseSign1SigningSource::PrivateKey(private_key) => Zeroizing::new( + sign(alg, private_key, &to_sign).map_err(sign_error_from_algorithm_error)?, + ), + CoseSign1SigningSource::Provider(provider) => { + provider.sign(&to_sign).map_err(CoseError::from)? + } + }; + let signature = core::mem::take(&mut *backend_signature); + cose_signature_from_backend(alg, signature) +} + +fn encode_cose_sign1( + cose: CoseSign1, + options: CoseSign1EncodeOptions, +) -> Result>, CoseError> { + let mut value = cose.to_cbor_value().map_err(|_| CoseError::Cbor)?; + if options.tag() { + value = ciborium::value::Value::Tag(18, Box::new(value)); + } + let encoded = encode_cbor_value(value)?; + + validate_cose_sign1_bytes_with_limit(&encoded, options.max_cose_sign1_bytes())?; + Ok(encoded) +} diff --git a/crates/cose/src/sign1/types.rs b/crates/cose/src/sign1/types.rs new file mode 100644 index 0000000..a23462b --- /dev/null +++ b/crates/cose/src/sign1/types.rs @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Operation-specific semantic inputs for COSE_Sign1. + +use reallyme_crypto::core::Algorithm; +use zeroize::Zeroizing; + +use crate::policy::CosePolicy; + +use super::provider::CoseSigner; +use super::sign::CoseSign1EncodeOptions; + +pub(super) enum CoseSign1SigningSource<'a> { + PrivateKey(&'a [u8]), + Provider(&'a dyn CoseSigner), +} + +pub(crate) struct CoseSign1CreateInput<'a> { + pub(super) algorithm: Algorithm, + pub(super) payload: &'a [u8], + pub(super) signing_source: CoseSign1SigningSource<'a>, + pub(super) kid: Option<&'a [u8]>, + pub(super) external_aad: &'a [u8], + pub(super) options: CoseSign1EncodeOptions, +} + +impl<'a> CoseSign1CreateInput<'a> { + pub(crate) const fn new( + algorithm: Algorithm, + payload: &'a [u8], + private_key: &'a [u8], + kid: Option<&'a [u8]>, + external_aad: &'a [u8], + options: CoseSign1EncodeOptions, + ) -> Self { + Self { + algorithm, + payload, + signing_source: CoseSign1SigningSource::PrivateKey(private_key), + kid, + external_aad, + options, + } + } + + pub(crate) fn with_signer( + signer: &'a dyn CoseSigner, + payload: &'a [u8], + kid: Option<&'a [u8]>, + external_aad: &'a [u8], + options: CoseSign1EncodeOptions, + ) -> Self { + Self { + algorithm: signer.algorithm(), + payload, + signing_source: CoseSign1SigningSource::Provider(signer), + kid, + external_aad, + options, + } + } +} + +pub(crate) struct CoseSign1VerifyInput<'a> { + pub(super) cose_sign1: &'a [u8], + pub(super) external_aad: &'a [u8], + pub(super) policy: &'a CosePolicy, +} + +impl<'a> CoseSign1VerifyInput<'a> { + pub(crate) const fn new( + cose_sign1: &'a [u8], + external_aad: &'a [u8], + policy: &'a CosePolicy, + ) -> Self { + Self { + cose_sign1, + external_aad, + policy, + } + } +} + +pub(crate) struct CoseSign1DetachedVerifyInput<'a> { + pub(super) cose_sign1: &'a [u8], + pub(super) payload: &'a [u8], + pub(super) external_aad: &'a [u8], + pub(super) policy: &'a CosePolicy, +} + +impl<'a> CoseSign1DetachedVerifyInput<'a> { + pub(crate) const fn new( + cose_sign1: &'a [u8], + payload: &'a [u8], + external_aad: &'a [u8], + policy: &'a CosePolicy, + ) -> Self { + Self { + cose_sign1, + payload, + external_aad, + policy, + } + } +} + +/// Deliberately typed key-resolution outcome used by verification semantics. +/// +/// A resolver receives both the protected-header algorithm and `kid`, then +/// returns an owned zeroizing key so semantic verification never has to clone +/// public-key storage supplied by a generated boundary. +pub(crate) enum CoseSign1KeyResolution { + Resolved(Zeroizing>), + NotResolved, + #[cfg(feature = "wire")] + KidMismatch, +} diff --git a/crates/cose/src/sign1/verify.rs b/crates/cose/src/sign1/verify.rs new file mode 100644 index 0000000..fd71124 --- /dev/null +++ b/crates/cose/src/sign1/verify.rs @@ -0,0 +1,305 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use coset::CoseSign1; +use reallyme_crypto::core::Algorithm; +use reallyme_crypto::dispatch::verify; + +use crate::encode_cbor::encode_protected_header; +use crate::error::verify_error_from_algorithm_error; +use crate::failure::CoseFailure; +use crate::policy::{validate_cose_sign1_policy, CosePolicy}; +use crate::{key::map_algorithm::cose_to_alg, CoseError}; + +use super::build_sig_structure::build_sig_structure; +use super::convert_signature::backend_signature_from_cose; +use super::decode::{decode_cose_sign1, validate_cose_sign1_structure}; +use super::types::{CoseSign1DetachedVerifyInput, CoseSign1KeyResolution, CoseSign1VerifyInput}; +use crate::limits::validate_detached_payload_with_limit; +use zeroize::Zeroizing; + +/// Verified COSE_Sign1 attached payload and protected-header metadata. +#[must_use] +#[non_exhaustive] +pub struct VerifiedCoseSign1 { + /// Verified attached payload bytes. + pub payload: Zeroizing>, + + /// Verified protected-header algorithm. + pub alg: Algorithm, + + /// Verified protected-header key identifier. + pub kid: Zeroizing>, +} + +/// Verified COSE_Sign1 protected-header metadata for detached payloads. +#[must_use] +#[non_exhaustive] +pub struct VerifiedDetachedCoseSign1 { + /// Verified protected-header algorithm. + pub alg: Algorithm, + + /// Verified protected-header key identifier. + pub kid: Zeroizing>, +} + +/// Verify COSE_Sign1 with an attached payload. +/// +/// # Errors +/// +/// Returns [`CoseError`] for malformed or oversized COSE, unsupported or +/// disallowed algorithms, unresolved keys, invalid keys, or invalid signatures. +pub fn cose_verify1( + cose_bytes: &[u8], + public_key_resolver: impl Fn(Algorithm, &[u8]) -> Option>, +) -> Result>, CoseError> { + let verified = + cose_verify1_with_policy(cose_bytes, &CosePolicy::default(), public_key_resolver)?; + Ok(verified.payload) +} + +/// Verify COSE_Sign1 with an attached payload and return verified metadata. +/// +/// # Errors +/// +/// Returns [`CoseError`] for malformed or oversized COSE, unsupported or +/// disallowed algorithms, unresolved keys, invalid keys, or invalid signatures. +pub fn cose_verify1_with_metadata( + cose_bytes: &[u8], + public_key_resolver: impl Fn(Algorithm, &[u8]) -> Option>, +) -> Result { + cose_verify1_with_policy(cose_bytes, &CosePolicy::default(), public_key_resolver) +} + +/// Verify COSE_Sign1 with an attached payload under an explicit policy. +/// +/// # Errors +/// +/// Returns [`CoseError`] when decoding, policy validation, key resolution, key +/// validation, signature decoding, or signature verification fails. +pub fn cose_verify1_with_policy( + cose_bytes: &[u8], + policy: &CosePolicy, + public_key_resolver: impl Fn(Algorithm, &[u8]) -> Option>, +) -> Result { + cose_verify1_with_policy_and_external_aad(cose_bytes, &[], policy, public_key_resolver) +} + +/// Verify an attached COSE_Sign1 with explicit external AAD and policy. +/// +/// # Errors +/// +/// Returns [`CoseError`] when the COSE structure, external AAD, policy, +/// resolver result, key material, signature encoding, or signature is invalid. +pub fn cose_verify1_with_policy_and_external_aad( + cose_bytes: &[u8], + external_aad: &[u8], + policy: &CosePolicy, + public_key_resolver: impl Fn(Algorithm, &[u8]) -> Option>, +) -> Result { + verify_cose_sign1( + CoseSign1VerifyInput::new(cose_bytes, external_aad, policy), + |algorithm, kid| match public_key_resolver(algorithm, kid) { + Some(public_key) => CoseSign1KeyResolution::Resolved(Zeroizing::new(public_key)), + None => CoseSign1KeyResolution::NotResolved, + }, + ) + .map_err(CoseFailure::into_native_error) +} + +/// Verify COSE_Sign1 with a detached payload. +/// +/// # Errors +/// +/// Returns [`CoseError`] for malformed or oversized COSE, an attached payload, +/// unsupported algorithms, unresolved keys, invalid keys, or invalid signatures. +pub fn cose_verify1_detached( + cose_bytes: &[u8], + payload: &[u8], + public_key_resolver: impl Fn(Algorithm, &[u8]) -> Option>, +) -> Result<(), CoseError> { + cose_verify1_detached_with_policy( + cose_bytes, + payload, + &CosePolicy::default(), + public_key_resolver, + ) + .map(|_| ()) +} + +/// Verify COSE_Sign1 with a detached payload and return verified metadata. +/// +/// # Errors +/// +/// Returns [`CoseError`] for malformed or oversized COSE, an attached payload, +/// unsupported algorithms, unresolved keys, invalid keys, or invalid signatures. +pub fn cose_verify1_detached_with_metadata( + cose_bytes: &[u8], + payload: &[u8], + public_key_resolver: impl Fn(Algorithm, &[u8]) -> Option>, +) -> Result { + cose_verify1_detached_with_policy( + cose_bytes, + payload, + &CosePolicy::default(), + public_key_resolver, + ) +} + +/// Verify COSE_Sign1 with a detached payload under an explicit policy. +/// +/// # Errors +/// +/// Returns [`CoseError`] when decoding, policy validation, payload validation, +/// key resolution, key validation, or signature verification fails. +pub fn cose_verify1_detached_with_policy( + cose_bytes: &[u8], + payload: &[u8], + policy: &CosePolicy, + public_key_resolver: impl Fn(Algorithm, &[u8]) -> Option>, +) -> Result { + cose_verify1_detached_with_policy_and_external_aad( + cose_bytes, + payload, + &[], + policy, + public_key_resolver, + ) +} + +/// Verify a detached COSE_Sign1 with explicit external AAD and policy. +/// +/// # Errors +/// +/// Returns [`CoseError`] when the COSE structure, detached payload, external +/// AAD, policy, resolver result, key material, or signature is invalid. +pub fn cose_verify1_detached_with_policy_and_external_aad( + cose_bytes: &[u8], + payload: &[u8], + external_aad: &[u8], + policy: &CosePolicy, + public_key_resolver: impl Fn(Algorithm, &[u8]) -> Option>, +) -> Result { + verify_detached_cose_sign1( + CoseSign1DetachedVerifyInput::new(cose_bytes, payload, external_aad, policy), + |algorithm, kid| match public_key_resolver(algorithm, kid) { + Some(public_key) => CoseSign1KeyResolution::Resolved(Zeroizing::new(public_key)), + None => CoseSign1KeyResolution::NotResolved, + }, + ) + .map_err(CoseFailure::into_native_error) +} + +pub(crate) fn verify_cose_sign1( + input: CoseSign1VerifyInput<'_>, + public_key_resolver: impl FnOnce(Algorithm, &[u8]) -> CoseSign1KeyResolution, +) -> Result { + validate_detached_payload_with_limit( + input.external_aad, + input.policy.max_detached_payload_bytes(), + )?; + let mut cose = decode_cose_sign1(input.cose_sign1, input.policy.max_cose_sign1_bytes())?; + let payload = cose + .inner() + .payload + .as_ref() + .ok_or(CoseError::MissingPayload)?; + let metadata = verify_cose_signature( + cose.inner(), + input.external_aad, + payload, + input.policy, + public_key_resolver, + )?; + let payload = cose + .inner_mut() + .payload + .take() + .ok_or(CoseError::MissingPayload)?; + + Ok(VerifiedCoseSign1 { + payload: Zeroizing::new(payload), + alg: metadata.alg, + kid: metadata.kid, + }) +} + +pub(crate) fn verify_detached_cose_sign1( + input: CoseSign1DetachedVerifyInput<'_>, + public_key_resolver: impl FnOnce(Algorithm, &[u8]) -> CoseSign1KeyResolution, +) -> Result { + validate_detached_payload_with_limit(input.payload, input.policy.max_detached_payload_bytes())?; + validate_detached_payload_with_limit( + input.external_aad, + input.policy.max_detached_payload_bytes(), + )?; + let cose = decode_cose_sign1(input.cose_sign1, input.policy.max_cose_sign1_bytes())?; + if cose.inner().payload.is_some() { + return Err(CoseFailure::from(CoseError::InvalidFormat)); + } + + verify_cose_signature( + cose.inner(), + input.external_aad, + input.payload, + input.policy, + public_key_resolver, + ) +} + +fn verify_cose_signature( + cose: &CoseSign1, + external_aad: &[u8], + payload: &[u8], + policy: &CosePolicy, + public_key_resolver: impl FnOnce(Algorithm, &[u8]) -> CoseSign1KeyResolution, +) -> Result { + validate_cose_sign1_structure(cose)?; + validate_cose_sign1_policy(cose, policy)?; + + let cose_alg = cose + .protected + .header + .alg + .as_ref() + .ok_or(CoseError::UnsupportedAlgorithm)?; + let alg = cose_to_alg(cose_alg)?; + + let kid: &[u8] = &cose.protected.header.key_id; + // Key stores must resolve the algorithm and identifier as one tuple. This + // prevents a shared kid from selecting bytes belonging to another key + // family and keeps that invariant independent of backend key-shape checks. + let public_key = match public_key_resolver(alg, kid) { + CoseSign1KeyResolution::Resolved(public_key) => public_key, + CoseSign1KeyResolution::NotResolved => { + return Err(CoseFailure::from(key_resolution_error(kid))); + } + #[cfg(feature = "wire")] + CoseSign1KeyResolution::KidMismatch => { + return Err(CoseFailure::sign1_kid_key_mismatch()); + } + }; + + // RFC 9052 §4.4: the Sig_structure must carry the protected header bstr + // exactly as received, not a re-encoding of the parsed header. + let protected_bytes = encode_protected_header(&cose.protected)?; + let to_verify = build_sig_structure(&protected_bytes, external_aad, payload)?; + let backend_signature = backend_signature_from_cose(alg, &cose.signature)?; + + verify(alg, &public_key, &to_verify, &backend_signature) + .map_err(verify_error_from_algorithm_error)?; + + Ok(VerifiedDetachedCoseSign1 { + alg, + kid: Zeroizing::new(kid.to_vec()), + }) +} + +fn key_resolution_error(kid: &[u8]) -> CoseError { + if kid.is_empty() { + CoseError::MissingKid + } else { + CoseError::KeyNotResolved + } +} diff --git a/crates/cose/src/wire.rs b/crates/cose/src/wire.rs new file mode 100644 index 0000000..df83059 --- /dev/null +++ b/crates/cose/src/wire.rs @@ -0,0 +1,331 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Protobuf-ready COSE operation request and response adapters. +//! +//! This module deliberately does not run a service. It owns the stable binary +//! messages a Connect or FFI wrapper can forward without losing the exact +//! operation result or structured COSE error branch and reason code. + +use buffa::{DecodeOptions, EnumValue, Message}; +use reallyme_cose_proto::generated::proto::reallyme::cose::v1::__buffa::oneof::cose_error::Error as CoseErrorBranchProto; +pub(crate) use reallyme_cose_proto::generated::proto::reallyme::cose::v1::__buffa::oneof::cose_operation_response_v2::Outcome as CoseOperationOutcomeV2; +pub(crate) use reallyme_cose_proto::generated::proto::reallyme::cose::v1::__buffa::oneof::cose_operation_result::Result as CoseOperationResultBranch; +use thiserror::Error; +use zeroize::Zeroizing; + +/// Re-export of the generated protobuf boundary. +pub mod proto { + pub use reallyme_cose_proto::generated::proto; + pub use reallyme_cose_proto::generated::COSE_PROTO_PACKAGE; +} + +pub use reallyme_cose_proto::generated::proto::reallyme::cose::v1::{ + __buffa::oneof::cose_algorithm_identifier, __buffa::oneof::cose_error as cose_error_proto, + __buffa::oneof::cose_operation_request, __buffa::oneof::cose_operation_response_v2, + __buffa::oneof::cose_operation_result, CoseAlgorithmIdentifier, CoseBackendError, + CoseContentEncryptionAlgorithm, CoseError as CoseErrorProto, CoseErrorReason, CoseKemAlgorithm, + CoseKeyAgreementAlgorithm, CoseKeyBytesRequest, CoseKeyBytesResult, + CoseKeyFromPrivateBytesRequest, CoseKeyFromPublicBytesRequest, CoseMlKemDecryptRequest, + CoseMlKemDecryptResult, CoseMlKemEncryptRequest, CoseMlKemEncryptResult, CoseMlKemMode, + CoseMultikeyResult, CoseMultikeyToCoseKeyRequest, CoseOperationRequest, + CoseOperationResponseV2, CoseOperationResult, CosePrimitiveError, CoseProviderError, + CoseSign1CreateDetachedRequest, CoseSign1CreateRequest, CoseSign1CreateResult, + CoseSign1Options, CoseSign1VerifyDetachedRequest, CoseSign1VerifyRequest, + CoseSign1VerifyResult, CoseSignatureAlgorithm, +}; + +/// Maximum accepted protobuf message size at the COSE wire boundary. +/// +/// The cap covers the largest supported detached-payload operation plus COSE +/// object and key material overhead. It is deliberately checked before protobuf +/// decode so hostile length-delimited fields cannot force unbounded allocation. +pub const MAX_COSE_PROTO_MESSAGE_BYTES: usize = 2_097_152; + +/// Maximum caller-supplied COSE byte limit accepted by protobuf operations. +/// +/// Native Rust APIs can opt into larger local policies directly. The protobuf +/// lane is intentionally capped to its message envelope so untrusted service, +/// FFI, and generated-SDK callers cannot widen boundary parsing beyond the +/// bytes this adapter already agreed to decode. +const COSE_PROTO_RECURSION_LIMIT: u32 = 64; +const COSE_PROTO_UNKNOWN_FIELD_LIMIT: usize = 0; + +/// Maximum accepted generated ProtoJSON request size at the COSE wire boundary. +/// +/// This leaves room for base64 expansion of the largest accepted protobuf +/// request plus generated field names and enum strings. +pub const MAX_COSE_PROTO_JSON_BYTES: usize = 3_145_728; + +pub(crate) fn encode_protobuf(message: &M) -> Zeroizing> { + Zeroizing::new(message.encode_to_vec()) +} + +/// Decodes a bounded protobuf message from untrusted bytes. +/// +/// # Errors +/// +/// Returns [`CoseWireError`] when the input exceeds the message limit or is +/// malformed, recursively excessive, or otherwise rejected by Buffa. +pub(crate) fn decode_protobuf(bytes: &[u8]) -> CoseWireResult +where + M: Message, +{ + decode_protobuf_with_limit(bytes, MAX_COSE_PROTO_MESSAGE_BYTES) +} + +pub(crate) fn decode_protobuf_with_limit(bytes: &[u8], max_bytes: usize) -> CoseWireResult +where + M: Message, +{ + if bytes.len() > max_bytes { + return Err(CoseWireError::primitive_internal( + CoseErrorReason::CommonResourceLimitExceeded, + )); + } + + DecodeOptions::new() + .with_recursion_limit(COSE_PROTO_RECURSION_LIMIT) + .with_max_message_size(max_bytes) + // Executable crypto requests use an exact schema contract. Rejecting + // unknown fields keeps binary protobuf semantics aligned with strict + // ProtoJSON and prevents arbitrary length-delimited unknown values + // from being retained in non-zeroizing generated storage. + .with_unknown_field_limit(COSE_PROTO_UNKNOWN_FIELD_LIMIT) + .decode_from_slice(bytes) + .map_err(|_| CoseWireError::primitive_internal(CoseErrorReason::CommonMalformedProtobuf)) +} + +pub(crate) fn decode_json( + bytes: &[u8], +) -> CoseWireResult { + if bytes.len() > MAX_COSE_PROTO_JSON_BYTES { + return Err(CoseWireError::primitive_internal( + CoseErrorReason::CommonResourceLimitExceeded, + )); + } + + let message = serde_json::from_slice(bytes) + .map_err(|_| CoseWireError::primitive_internal(CoseErrorReason::CommonMalformedJson))?; + let encoded = encode_protobuf(&message); + if encoded.len() > MAX_COSE_PROTO_MESSAGE_BYTES { + return Err(CoseWireError::primitive_internal( + CoseErrorReason::CommonResourceLimitExceeded, + )); + } + Ok(message) +} + +/// Decode a structured COSE error protobuf message. +/// +/// # Errors +/// +/// Returns a generated error response when the protobuf is malformed or its +/// branch and reason do not form a valid COSE error. +pub fn decode_cose_error(bytes: &[u8]) -> Result { + let error = decode_protobuf::(bytes) + .map_err(crate::operation_contract::response_v2::from_error)?; + validate_cose_error_proto_wire(&error) + .map_err(crate::operation_contract::response_v2::from_error)?; + Ok(error) +} + +/// Internal COSE wire-boundary error branch plus stable reason. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum CoseWireErrorBranch { + Primitive, + Provider, + Backend, +} + +/// Internal error preserving both the protobuf branch and exact reason. +#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)] +#[error("COSE wire boundary error")] +pub(crate) struct CoseWireError { + branch: CoseWireErrorBranch, + reason: CoseErrorReason, +} + +impl CoseWireError { + pub(crate) const fn branch(self) -> CoseWireErrorBranch { + self.branch + } + + pub(crate) const fn reason(self) -> CoseErrorReason { + self.reason + } + + pub(crate) const fn primitive_internal(reason: CoseErrorReason) -> Self { + Self { + branch: CoseWireErrorBranch::Primitive, + reason, + } + } + + pub(crate) const fn provider_internal(reason: CoseErrorReason) -> Self { + Self { + branch: CoseWireErrorBranch::Provider, + reason, + } + } + + pub(crate) const fn backend_internal(reason: CoseErrorReason) -> Self { + Self { + branch: CoseWireErrorBranch::Backend, + reason, + } + } +} + +pub(crate) type CoseWireResult = Result; + +/// Builds the structured `CoseError` protobuf message for a boundary error. +pub(crate) fn cose_error(error: CoseWireError) -> CoseErrorProto { + let reason = EnumValue::from(error.reason()); + let branch = match error.branch() { + CoseWireErrorBranch::Primitive => { + CoseErrorBranchProto::Primitive(Box::new(CosePrimitiveError { + reason, + __buffa_unknown_fields: Default::default(), + })) + } + CoseWireErrorBranch::Provider => { + CoseErrorBranchProto::Provider(Box::new(CoseProviderError { + reason, + __buffa_unknown_fields: Default::default(), + })) + } + CoseWireErrorBranch::Backend => CoseErrorBranchProto::Backend(Box::new(CoseBackendError { + reason, + __buffa_unknown_fields: Default::default(), + })), + }; + + CoseErrorProto { + error: Some(branch), + __buffa_unknown_fields: Default::default(), + } +} + +/// Decodes and executes the binary protobuf COSE operation entrypoint. +/// +/// Native Rust SDK callers should keep using the ergonomic Rust APIs; service, +/// FFI, and generated-SDK wrappers can use this lane when they need one +/// mechanically testable protobuf boundary. +pub fn execute_operation_proto(request_bytes: &[u8]) -> Zeroizing> { + crate::operation_contract::execute::execute_proto(request_bytes) +} + +/// Decode and execute a generated ProtoJSON request using the fully +/// discriminated version-two binary response contract. +pub fn execute_operation_proto_json(request_json: &str) -> Zeroizing> { + crate::operation_contract::execute::execute_proto_json(request_json) +} + +/// Decode and validate an operation response. +/// +/// Successful responses must carry the exact result oneof branch paired with +/// the request operation. Invalid, oversized, or mismatched responses return a +/// generated version-two backend error response rather than a generic Rust +/// error or a caller-input classification. +/// +/// # Errors +/// +/// Returns a generated [`CoseOperationResponseV2`] error outcome when the +/// response is malformed, oversized, lacks an outcome, contains invalid result +/// metadata, or does not match the request operation. +pub fn decode_operation_response( + bytes: &[u8], +) -> Result { + crate::operation_contract::response_v2::decode(bytes) +} + +/// Decode and validate a response for the request that produced it. +/// +/// # Errors +/// +/// Returns a generated [`CoseOperationResponseV2`] error outcome when the +/// response is invalid or its result branch does not match the request. +pub fn decode_operation_response_for_request( + request: &CoseOperationRequest, + bytes: &[u8], +) -> Result { + crate::operation_contract::response_v2::decode_for_request(request, bytes) +} + +pub(crate) fn validate_cose_error_proto_wire(error: &CoseErrorProto) -> CoseWireResult<()> { + let (branch, reason) = match error.error.as_ref() { + Some(CoseErrorBranchProto::Primitive(error)) => { + (CoseWireErrorBranch::Primitive, error.reason) + } + Some(CoseErrorBranchProto::Provider(error)) => { + (CoseWireErrorBranch::Provider, error.reason) + } + Some(CoseErrorBranchProto::Backend(error)) => (CoseWireErrorBranch::Backend, error.reason), + None => { + return Err(malformed_error_envelope_error()); + } + }; + + match reason.as_known() { + Some(CoseErrorReason::Unspecified) | None => Err(malformed_error_envelope_error()), + Some(reason) if reason_is_valid_for_branch(branch, reason) => Ok(()), + Some(_) => Err(malformed_error_envelope_error()), + } +} + +fn reason_is_valid_for_branch(branch: CoseWireErrorBranch, reason: CoseErrorReason) -> bool { + match branch { + CoseWireErrorBranch::Primitive => matches!( + reason, + CoseErrorReason::CommonCbor + | CoseErrorReason::CommonInvalidFormat + | CoseErrorReason::CommonResourceLimitExceeded + | CoseErrorReason::CommonNonCanonicalCbor + | CoseErrorReason::CommonUnexpectedCborTag + | CoseErrorReason::CommonDuplicateMapLabel + | CoseErrorReason::CommonMalformedProtobuf + | CoseErrorReason::CommonMalformedJson + | CoseErrorReason::CommonInvalidParameter + | CoseErrorReason::CommonInvalidLength + | CoseErrorReason::CommonInvalidEncoding + | CoseErrorReason::Sign1MissingPayload + | CoseErrorReason::Sign1MissingKid + | CoseErrorReason::Sign1KeyNotResolved + | CoseErrorReason::Sign1UnsupportedCriticalHeader + | CoseErrorReason::Sign1UnprotectedHeaderNotAllowed + | CoseErrorReason::Sign1InvalidSignature + | CoseErrorReason::Sign1InvalidSignatureEncoding + | CoseErrorReason::Sign1KidKeyMismatch + | CoseErrorReason::Sign1MissingPrivateKey + | CoseErrorReason::KeyMissingKeyMaterial + | CoseErrorReason::KeyInvalidKeyMaterial + | CoseErrorReason::MultikeyInvalidMultikey + | CoseErrorReason::EncryptMissingCiphertext + | CoseErrorReason::EncryptInvalidIv + | CoseErrorReason::EncryptInvalidRecipient + | CoseErrorReason::EncryptMissingEncapsulatedKey + | CoseErrorReason::EncryptInvalidEncapsulatedKey + | CoseErrorReason::EncryptAuthenticationFailed + | CoseErrorReason::EncryptKeyUnwrapFailed + | CoseErrorReason::EncryptKidMismatch + | CoseErrorReason::EncryptMissingKid + | CoseErrorReason::EncryptUnprotectedHeaderNotAllowed + ), + CoseWireErrorBranch::Provider => matches!( + reason, + CoseErrorReason::CommonUnsupportedAlgorithm | CoseErrorReason::ProviderUnavailable + ), + CoseWireErrorBranch::Backend => matches!( + reason, + CoseErrorReason::CommonResourceLimitExceeded + | CoseErrorReason::CommonCryptoFailed + | CoseErrorReason::BackendInternal + ), + } +} + +const fn malformed_error_envelope_error() -> CoseWireError { + CoseWireError::primitive_internal(CoseErrorReason::CommonMalformedProtobuf) +} diff --git a/crates/cose/src/zeroize_coset.rs b/crates/cose/src/zeroize_coset.rs new file mode 100644 index 0000000..72f2f43 --- /dev/null +++ b/crates/cose/src/zeroize_coset.rs @@ -0,0 +1,194 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Recursive wiping for decoded `coset` owners. +//! +//! `coset` correctly models COSE wire objects, but its decoded buffers are +//! ordinary vectors and strings. Profile bounds are enforced before tree +//! decoding; semantic validation still happens afterward, so rejected objects +//! can contain sensitive caller-controlled bytes. These helpers wipe the entire +//! decoded owner, not only the fields used on the success path. + +use ciborium::value::Value; +#[cfg(feature = "cose-crypto")] +use coset::{ + CoseEncrypt, CoseRecipient, CoseSign1, CoseSignature, Header, Label, ProtectedHeader, + RegisteredLabel, RegisteredLabelWithPrivate, +}; +use zeroize::Zeroize; + +use crate::limits::validate_cose_key_bytes; +#[cfg(feature = "cose-crypto")] +use crate::limits::{ + validate_cose_encrypt_bytes, validate_cose_sign1_bytes_with_limit, + validate_protected_header_bytes, +}; +use crate::CoseError; + +/// Decoded CBOR tree that recursively wipes owned byte and text values. +/// +/// Keeping the original decoded tree alive while semantic fields are copied +/// into a profile-specific wipe-on-drop owner prevents rejected parses from +/// abandoning partially decoded payloads, keys, claims, or ciphertexts in +/// ordinary allocator-owned buffers. +pub(crate) struct SensitiveCborValue { + value: Value, +} + +impl SensitiveCborValue { + pub(crate) const fn from_value(value: Value) -> Self { + Self { value } + } + + pub(crate) fn decode_cose_key(bytes: &[u8]) -> Result { + validate_cose_key_bytes(bytes)?; + Self::decode_validated(bytes) + } + + #[cfg(feature = "cose-crypto")] + pub(crate) fn decode_cose_sign1(bytes: &[u8], max_len: usize) -> Result { + validate_cose_sign1_bytes_with_limit(bytes, max_len)?; + Self::decode_validated(bytes) + } + + #[cfg(feature = "cose-crypto")] + pub(crate) fn decode_cose_encrypt(bytes: &[u8]) -> Result { + validate_cose_encrypt_bytes(bytes)?; + Self::decode_validated(bytes) + } + + #[cfg(feature = "cose-crypto")] + pub(crate) fn decode_protected_header(bytes: &[u8]) -> Result { + validate_protected_header_bytes(bytes)?; + Self::decode_validated(bytes) + } + + fn decode_validated(bytes: &[u8]) -> Result { + let mut reader = bytes; + let value = ciborium::de::from_reader(&mut reader).map_err(|_| CoseError::Cbor)?; + if !reader.is_empty() { + let mut value = value; + zeroize_value(&mut value); + return Err(CoseError::Cbor); + } + Ok(Self { value }) + } + + pub(crate) fn value_mut(&mut self) -> &mut Value { + &mut self.value + } + + pub(crate) fn value(&self) -> &Value { + &self.value + } +} + +impl Drop for SensitiveCborValue { + fn drop(&mut self) { + zeroize_value(&mut self.value); + } +} + +#[cfg(feature = "cose-crypto")] +pub(crate) fn zeroize_cose_encrypt(cose: &mut CoseEncrypt) { + zeroize_protected_header(&mut cose.protected); + zeroize_header(&mut cose.unprotected); + if let Some(ciphertext) = &mut cose.ciphertext { + ciphertext.zeroize(); + } + for recipient in &mut cose.recipients { + zeroize_recipient(recipient); + } +} + +#[cfg(feature = "cose-crypto")] +pub(crate) fn zeroize_cose_sign1(cose: &mut CoseSign1) { + zeroize_protected_header(&mut cose.protected); + zeroize_header(&mut cose.unprotected); + if let Some(payload) = &mut cose.payload { + payload.zeroize(); + } + cose.signature.zeroize(); +} + +#[cfg(feature = "cose-crypto")] +fn zeroize_recipient(recipient: &mut CoseRecipient) { + zeroize_protected_header(&mut recipient.protected); + zeroize_header(&mut recipient.unprotected); + if let Some(ciphertext) = &mut recipient.ciphertext { + ciphertext.zeroize(); + } + for child in &mut recipient.recipients { + zeroize_recipient(child); + } +} + +#[cfg(feature = "cose-crypto")] +fn zeroize_protected_header(protected: &mut ProtectedHeader) { + if let Some(original) = &mut protected.original_data { + original.zeroize(); + } + zeroize_header(&mut protected.header); +} + +#[cfg(feature = "cose-crypto")] +fn zeroize_header(header: &mut Header) { + if let Some(RegisteredLabelWithPrivate::Text(text)) = &mut header.alg { + text.zeroize(); + } + for label in &mut header.crit { + if let RegisteredLabelWithPrivate::Text(text) = label { + text.zeroize(); + } + } + if let Some(RegisteredLabel::Text(text)) = &mut header.content_type { + text.zeroize(); + } + header.key_id.zeroize(); + header.iv.zeroize(); + header.partial_iv.zeroize(); + for signature in &mut header.counter_signatures { + zeroize_signature(signature); + } + for (label, value) in &mut header.rest { + if let Label::Text(text) = label { + text.zeroize(); + } + zeroize_value(value); + } +} + +#[cfg(feature = "cose-crypto")] +fn zeroize_signature(signature: &mut CoseSignature) { + zeroize_protected_header(&mut signature.protected); + zeroize_header(&mut signature.unprotected); + signature.signature.zeroize(); +} + +pub(crate) fn zeroize_value(value: &mut Value) { + match value { + Value::Bytes(bytes) => bytes.zeroize(), + Value::Text(text) => text.zeroize(), + Value::Array(values) => { + for value in values { + zeroize_value(value); + } + } + Value::Map(entries) => { + for (key, value) in entries { + zeroize_value(key); + zeroize_value(value); + } + } + Value::Tag(_, tagged) => zeroize_value(tagged), + Value::Integer(_) | Value::Float(_) | Value::Bool(_) | Value::Null => {} + // `ciborium::Value` is non-exhaustive. Dependency upgrades must review + // any new variants here before release; unknown variants cannot be + // safely assumed to be buffer-free. + _ => {} + } +} + +#[cfg(test)] +mod decode_sensitive_tests; diff --git a/crates/cose/src/zeroize_coset/decode_sensitive_tests.rs b/crates/cose/src/zeroize_coset/decode_sensitive_tests.rs new file mode 100644 index 0000000..47e6a5d --- /dev/null +++ b/crates/cose/src/zeroize_coset/decode_sensitive_tests.rs @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use crate::CoseError; + +use super::SensitiveCborValue; + +const HOSTILE_NESTING_DEPTH: usize = 40; + +#[test] +fn decode_cose_key_rejects_excessive_nesting_before_tree_decode() { + let mut encoded = Vec::with_capacity(HOSTILE_NESTING_DEPTH.saturating_add(1)); + encoded.extend(core::iter::repeat_n(0x81, HOSTILE_NESTING_DEPTH)); + encoded.push(0xf6); + + assert!(matches!( + SensitiveCborValue::decode_cose_key(&encoded), + Err(CoseError::ResourceLimitExceeded), + )); +} diff --git a/tests/cose.rs b/crates/cose/tests/cose.rs similarity index 57% rename from tests/cose.rs rename to crates/cose/tests/cose.rs index c2d60c1..fb039ef 100644 --- a/tests/cose.rs +++ b/crates/cose/tests/cose.rs @@ -6,6 +6,9 @@ #[path = "cose_suite/support.rs"] mod support; +#[cfg(feature = "cose-crypto")] +#[path = "cose_suite/concurrency_tests.rs"] +mod concurrency_tests; #[path = "cose_suite/conformance_vector_tests.rs"] mod conformance_vector_tests; #[path = "cose_suite/cose_key_tests.rs"] @@ -16,20 +19,42 @@ mod cose_private_key_tests; mod detached_reject_wrong_payload_tests; #[path = "cose_suite/detached_roundtrip_tests.rs"] mod detached_roundtrip_tests; +#[cfg(all(feature = "cose-crypto", feature = "wire"))] +#[path = "cose_suite/encrypt_semantic_tests.rs"] +mod encrypt_semantic_tests; +#[path = "cose_suite/external_aad_tests.rs"] +mod external_aad_tests; #[path = "cose_suite/interop_verify_tests.rs"] mod interop_verify_tests; +#[cfg(feature = "wire")] +#[path = "cose_suite/key_parse_adapter_tests.rs"] +mod key_parse_adapter_tests; +#[cfg(all(feature = "cose-crypto", feature = "wire"))] +#[path = "cose_suite/key_parse_differential_tests.rs"] +mod key_parse_differential_tests; +#[cfg(all(feature = "cose-crypto", feature = "wire"))] +#[path = "cose_suite/operation_response_v2_tests.rs"] +mod operation_response_v2_tests; + +#[cfg(all(feature = "cose-crypto", feature = "wire"))] +#[path = "cose_suite/key_family_semantic_tests.rs"] +mod key_family_semantic_tests; #[path = "cose_suite/kid_derive_tests.rs"] mod kid_derive_tests; #[path = "cose_suite/kid_tests.rs"] mod kid_tests; #[path = "cose_suite/malicious_cbor_tests.rs"] mod malicious_cbor_tests; +#[path = "cose_suite/ml_kem_encrypt_tests.rs"] +mod ml_kem_encrypt_tests; #[path = "cose_suite/multikey_tests.rs"] mod multikey_tests; #[path = "cose_suite/multikey_to_cose_tests.rs"] mod multikey_to_cose_tests; #[path = "cose_suite/platform_consumer_tests.rs"] mod platform_consumer_tests; +#[path = "cose_suite/platform_signer_tests.rs"] +mod platform_signer_tests; #[path = "cose_suite/rfc9052_9053_profile_tests.rs"] mod rfc9052_9053_profile_tests; #[path = "cose_suite/roundtrip_eddsa_tests.rs"] @@ -38,7 +63,13 @@ mod roundtrip_eddsa_tests; mod roundtrip_es256k_tests; #[path = "cose_suite/roundtrip_p256_tests.rs"] mod roundtrip_p256_tests; +#[cfg(all(feature = "cose-crypto", feature = "wire"))] +#[path = "cose_suite/sign1_semantic_tests.rs"] +mod sign1_semantic_tests; #[path = "cose_suite/signing_algorithm_coverage_tests.rs"] mod signing_algorithm_coverage_tests; #[path = "cose_suite/tamper_reject_tests.rs"] mod tamper_reject_tests; +#[cfg(feature = "wire")] +#[path = "cose_suite/wire_tests.rs"] +mod wire_tests; diff --git a/crates/cose/tests/cose_suite/concurrency_tests.rs b/crates/cose/tests/cose_suite/concurrency_tests.rs new file mode 100644 index 0000000..4f9a706 --- /dev/null +++ b/crates/cose/tests/cose_suite/concurrency_tests.rs @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use reallyme_cose::{ + cose_decrypt_ml_kem, cose_encrypt_ml_kem_direct, cose_key_from_public_bytes, + cose_key_from_slice, cose_key_to_multikey, cose_key_to_vec, cose_sign1, + cose_verify1_with_policy, derive_kid_from_cose_key_public, Algorithm, + CoseContentEncryptionAlgorithm, CoseMlKemAlgorithm, CoseMlKemDecryptRequest, + CoseMlKemEncryptRequest, CosePolicy, +}; +use reallyme_crypto::dispatch::generate_keypair; +use zeroize::Zeroizing; + +const CONCURRENT_WORKERS: usize = 4; +const PAYLOAD: &[u8] = b"concurrent semantic operation payload"; + +#[test] +fn operation_families_are_deterministic_and_independently_owned_under_concurrency() { + let (signing_public_key, signing_private_key) = + generate_keypair(Algorithm::Ed25519).expect("Ed25519 fixture generation must succeed"); + let signing_key = cose_key_from_public_bytes(Algorithm::Ed25519, &signing_public_key) + .expect("Ed25519 COSE_Key construction must succeed"); + let encoded_signing_key = + cose_key_to_vec(&signing_key).expect("Ed25519 COSE_Key encoding must succeed"); + + let (kem_public_key, kem_private_key) = + generate_keypair(Algorithm::MlKem512).expect("ML-KEM-512 fixture generation must succeed"); + let kem_key = cose_key_from_public_bytes(Algorithm::MlKem512, &kem_public_key) + .expect("ML-KEM COSE_Key construction must succeed"); + let kem_kid = + derive_kid_from_cose_key_public(&kem_key).expect("ML-KEM kid derivation must succeed"); + let policy = CosePolicy::new().allow_algorithm(Algorithm::Ed25519); + + let outputs = std::thread::scope(|scope| { + let mut handles = Vec::with_capacity(CONCURRENT_WORKERS); + for _ in 0..CONCURRENT_WORKERS { + handles.push(scope.spawn(|| { + let cose_sign1 = cose_sign1( + Algorithm::Ed25519, + PAYLOAD, + &signing_private_key, + Some(b"concurrent-key"), + ) + .expect("concurrent signing must succeed"); + let verified = cose_verify1_with_policy(&cose_sign1, &policy, |_, _| { + Some(signing_public_key.to_vec()) + }) + .expect("concurrent verification must succeed"); + assert_eq!(verified.payload.as_slice(), PAYLOAD); + + let parsed = cose_key_from_slice(&encoded_signing_key) + .expect("concurrent COSE_Key parsing must succeed"); + let multikey = cose_key_to_multikey(&parsed) + .expect("concurrent Multikey conversion must succeed"); + + let encrypt_request = CoseMlKemEncryptRequest::new( + CoseMlKemAlgorithm::MlKem512, + CoseContentEncryptionAlgorithm::Aes128Gcm, + &kem_public_key, + &kem_kid, + PAYLOAD, + None, + ); + let cose_encrypt = cose_encrypt_ml_kem_direct(&encrypt_request) + .expect("concurrent encryption must succeed"); + let decrypt_request = + CoseMlKemDecryptRequest::new(&cose_encrypt, &kem_private_key, &kem_kid, None); + let decrypted = cose_decrypt_ml_kem(&decrypt_request) + .expect("concurrent decryption must succeed"); + + (cose_sign1, multikey, decrypted.plaintext, decrypted.kid) + })); + } + handles + .into_iter() + .map(|handle| handle.join().expect("concurrent operation must not panic")) + .collect::>() + }); + + let (expected_sign1, expected_multikey, _, _) = outputs + .first() + .expect("the fixed worker count must produce at least one output"); + for (cose_sign1, multikey, plaintext, kid) in &outputs { + assert_eq!(cose_sign1.as_slice(), expected_sign1.as_slice()); + assert_eq!(multikey.as_str(), expected_multikey.as_str()); + assert_eq!(plaintext.as_slice(), PAYLOAD); + assert_eq!(kid.as_slice(), kem_kid.as_slice()); + } + + // Keep the owner types visible in this test so a future return-type + // regression to unmanaged buffers fails compilation. + let _: &Zeroizing> = expected_sign1; + let _: &Zeroizing = expected_multikey; +} diff --git a/crates/cose/tests/cose_suite/conformance_vector_tests.rs b/crates/cose/tests/cose_suite/conformance_vector_tests.rs new file mode 100644 index 0000000..2e6cd09 --- /dev/null +++ b/crates/cose/tests/cose_suite/conformance_vector_tests.rs @@ -0,0 +1,473 @@ +#![allow(missing_docs, clippy::expect_used, clippy::panic, clippy::unwrap_used)] +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use reallyme_cose::{ + cose_decrypt_ml_kem_with_external_aad, cose_key_from_public_bytes, cose_key_from_slice, + cose_key_to_multikey, cose_key_to_public_bytes, cose_key_to_vec, cose_verify1, + cose_verify1_detached, derive_kid_from_cose_key_public, multikey_to_cose_key, Algorithm, + CoseContentEncryptionAlgorithm, CoseError, CoseMlKemAlgorithm, CoseMlKemDecryptRequest, + CoseMlKemMode, +}; +use serde::Deserialize; + +const COSE_SIGN1_VECTORS: &str = include_str!("../../../../vectors/cose-sign1.json"); +const COSE_KEY_VECTORS: &str = include_str!("../../../../vectors/cose-key.json"); +const COSE_PQ_SIGN1_VECTORS: &str = include_str!("../../../../vectors/cose-sign1-pq.json"); +const COSE_PQ_KEY_VECTORS: &str = include_str!("../../../../vectors/cose-key-pq.json"); +const COSE_ML_KEM_ENCRYPT_VECTORS: &str = + include_str!("../../../../vectors/cose-encrypt-ml-kem.json"); +const VECTOR_MANIFEST: &str = include_str!("../../../../vectors/manifest.json"); + +#[derive(Debug, Deserialize)] +struct Manifest { + suites: Vec, +} + +#[derive(Debug, Deserialize)] +struct ManifestSuite { + id: String, + case_count: usize, +} + +#[test] +fn manifest_case_counts_match_suites() { + let manifest: Manifest = serde_json::from_str(VECTOR_MANIFEST).expect("manifest must parse"); + let sign1: CoseSign1Suite = + serde_json::from_str(COSE_SIGN1_VECTORS).expect("COSE_Sign1 vectors must parse"); + let key: CoseKeySuite = + serde_json::from_str(COSE_KEY_VECTORS).expect("COSE_Key vectors must parse"); + let pq_sign1: CoseSign1Suite = + serde_json::from_str(COSE_PQ_SIGN1_VECTORS).expect("PQ COSE_Sign1 vectors must parse"); + let pq_key: CoseKeySuite = + serde_json::from_str(COSE_PQ_KEY_VECTORS).expect("PQ COSE_Key vectors must parse"); + let encrypt: CoseMlKemEncryptSuite = serde_json::from_str(COSE_ML_KEM_ENCRYPT_VECTORS) + .expect("ML-KEM COSE_Encrypt vectors must parse"); + + for suite in manifest.suites { + let actual = match suite.id.as_str() { + "cose-sign1" => sign1.cases.len(), + "cose-key" => key.cases.len(), + "cose-sign1-pq" => pq_sign1.cases.len(), + "cose-key-pq" => pq_key.cases.len(), + "cose-encrypt-ml-kem" => encrypt.cases.len(), + other => panic!("unknown manifest suite: {other}"), + }; + assert_eq!(suite.case_count, actual, "case count for {}", suite.id); + } +} + +#[derive(Debug, Deserialize)] +struct CoseSign1Suite { + cases: Vec, +} + +#[derive(Debug, Deserialize)] +struct CoseSign1Case { + id: String, + operation: String, + algorithm: String, + kid_hex: String, + public_key_hex: String, + payload_hex: String, + cose_sign1_hex: String, + expected_error: Option, +} + +#[derive(Debug, Deserialize)] +struct CoseKeySuite { + cases: Vec, +} + +#[derive(Debug, Deserialize)] +struct CoseKeyCase { + id: String, + algorithm: String, + public_key_hex: String, + cose_key_hex: String, + multikey: String, +} + +#[derive(Debug, Deserialize)] +struct CoseMlKemEncryptSuite { + cases: Vec, +} + +#[derive(Debug, Deserialize)] +struct CoseMlKemEncryptCase { + id: String, + kem_algorithm: String, + mode: String, + content_algorithm: String, + private_key_seed_hex: String, + public_key_hex: String, + recipient_kid_hex: String, + plaintext_hex: String, + external_aad_hex: String, + supp_priv_info_hex: String, + cose_encrypt_hex: String, +} + +#[test] +fn portable_cose_sign1_vectors_verify() { + let suite: CoseSign1Suite = + serde_json::from_str(COSE_SIGN1_VECTORS).expect("COSE_Sign1 vectors must parse"); + + for case in suite.cases { + let kid = decode_hex(&case.kid_hex); + let public_key = decode_hex(&case.public_key_hex); + let payload = decode_hex(&case.payload_hex); + let cose = decode_hex(&case.cose_sign1_hex); + + let result = match case.operation.as_str() { + "verify_attached" => cose_verify1(&cose, |_, requested_kid| { + resolve_expected_kid(requested_kid, &kid, &public_key) + }) + .map(|verified_payload| { + assert_eq!( + verified_payload.as_slice(), + payload.as_slice(), + "{}", + case.id + ); + }), + "verify_detached" => cose_verify1_detached(&cose, &payload, |_, requested_kid| { + resolve_expected_kid(requested_kid, &kid, &public_key) + }), + _ => panic!("unknown vector operation: {}", case.operation), + }; + + // The algorithm field is asserted to be a supported name; the + // algorithm actually used comes from the message's protected header. + let _ = algorithm_from_name(&case.algorithm); + assert_vector_result(&case.id, result, case.expected_error.as_deref()); + } +} + +#[test] +fn portable_pq_cose_sign1_vectors_verify() { + let suite: CoseSign1Suite = + serde_json::from_str(COSE_PQ_SIGN1_VECTORS).expect("PQ COSE_Sign1 vectors must parse"); + verify_sign1_suite(suite); +} + +fn verify_sign1_suite(suite: CoseSign1Suite) { + for case in suite.cases { + let kid = decode_hex(&case.kid_hex); + let public_key = decode_hex(&case.public_key_hex); + let payload = decode_hex(&case.payload_hex); + let cose = decode_hex(&case.cose_sign1_hex); + + let result = match case.operation.as_str() { + "verify_attached" => cose_verify1(&cose, |_, requested_kid| { + resolve_expected_kid(requested_kid, &kid, &public_key) + }) + .map(|verified_payload| { + assert_eq!( + verified_payload.as_slice(), + payload.as_slice(), + "{}", + case.id + ); + }), + "verify_detached" => cose_verify1_detached(&cose, &payload, |_, requested_kid| { + resolve_expected_kid(requested_kid, &kid, &public_key) + }), + _ => panic!("unknown vector operation: {}", case.operation), + }; + + let _ = algorithm_from_name(&case.algorithm); + assert_vector_result(&case.id, result, case.expected_error.as_deref()); + } +} + +#[test] +fn portable_cose_key_vectors_roundtrip() { + let suite: CoseKeySuite = + serde_json::from_str(COSE_KEY_VECTORS).expect("COSE_Key vectors must parse"); + + for case in suite.cases { + let algorithm = algorithm_from_name(&case.algorithm); + let public_key = decode_hex(&case.public_key_hex); + let cose_key_bytes = decode_hex(&case.cose_key_hex); + + let decoded_key = cose_key_from_slice(&cose_key_bytes).expect("COSE_Key must decode"); + assert_eq!( + cose_key_to_public_bytes(&decoded_key).expect("public key must extract"), + public_key, + "{}", + case.id + ); + assert_eq!( + cose_key_to_vec(&decoded_key) + .expect("COSE_Key must re-encode") + .as_slice(), + cose_key_bytes.as_slice(), + "{}", + case.id + ); + assert_eq!( + cose_key_to_multikey(&decoded_key) + .expect("multikey must encode") + .as_str(), + case.multikey.as_str(), + "{}", + case.id + ); + + let rebuilt_key = + cose_key_from_public_bytes(algorithm, &public_key).expect("COSE_Key must rebuild"); + assert_eq!( + cose_key_to_vec(&rebuilt_key) + .expect("rebuilt COSE_Key must encode") + .as_slice(), + cose_key_bytes.as_slice(), + "{}", + case.id + ); + } +} + +#[test] +fn portable_pq_cose_key_and_multikey_vectors_roundtrip() { + let suite: CoseKeySuite = + serde_json::from_str(COSE_PQ_KEY_VECTORS).expect("PQ COSE_Key vectors must parse"); + + for case in suite.cases { + let algorithm = algorithm_from_name(&case.algorithm); + let public_key = decode_hex(&case.public_key_hex); + let cose_key_bytes = decode_hex(&case.cose_key_hex); + let decoded_key = cose_key_from_slice(&cose_key_bytes).expect("PQ COSE_Key must decode"); + + assert_eq!( + cose_key_to_public_bytes(&decoded_key).expect("PQ public key must extract"), + public_key, + "{}", + case.id + ); + assert_eq!( + cose_key_to_multikey(&decoded_key) + .expect("PQ multikey must encode") + .as_str(), + case.multikey.as_str(), + "{}", + case.id + ); + let from_multikey = + multikey_to_cose_key(&case.multikey).expect("PQ Multikey must convert to COSE_Key"); + assert_eq!( + cose_key_to_vec(&from_multikey) + .expect("PQ Multikey-derived COSE_Key must encode") + .as_slice(), + cose_key_bytes.as_slice(), + "{}", + case.id + ); + let rebuilt = + cose_key_from_public_bytes(algorithm, &public_key).expect("PQ COSE_Key must rebuild"); + assert_eq!( + cose_key_to_vec(&rebuilt) + .expect("PQ COSE_Key must encode") + .as_slice(), + cose_key_bytes.as_slice(), + "{}", + case.id + ); + } +} + +#[test] +fn portable_cose_key_vectors_roundtrip_reallyme_codec() { + // Exercise the multikey strings directly at the reallyme-codec layer, + // independent of this crate's COSE_Key conversion functions. + use reallyme_codec::multikey::{encode_multikey, parse_multikey}; + + let suite: CoseKeySuite = + serde_json::from_str(COSE_KEY_VECTORS).expect("COSE_Key vectors must parse"); + + for case in suite.cases { + let public_key = decode_hex(&case.public_key_hex); + let codec_name = multikey_codec_name(&case.algorithm); + + assert_eq!( + encode_multikey(codec_name, &public_key).expect("codec multikey must encode"), + case.multikey, + "{}", + case.id + ); + + let parsed = parse_multikey(&case.multikey).expect("codec multikey must parse"); + assert_eq!(parsed.codec_name, codec_name, "{}", case.id); + assert_eq!(parsed.public_key, public_key, "{}", case.id); + } +} + +#[test] +fn deterministic_ml_kem_cose_encrypt_vectors_decrypt_and_preserve_metadata() { + let suite: CoseMlKemEncryptSuite = serde_json::from_str(COSE_ML_KEM_ENCRYPT_VECTORS) + .expect("ML-KEM COSE_Encrypt vectors must parse"); + + for case in suite.cases { + let (crypto_algorithm, kem_algorithm) = ml_kem_algorithm_from_name(&case.kem_algorithm); + let content_algorithm = content_algorithm_from_name(&case.content_algorithm); + let mode = ml_kem_mode_from_name(&case.mode); + let private_key = decode_hex(&case.private_key_seed_hex); + let public_key = decode_hex(&case.public_key_hex); + let kid = decode_hex(&case.recipient_kid_hex); + let plaintext = decode_hex(&case.plaintext_hex); + let external_aad = decode_hex(&case.external_aad_hex); + let supp_priv_info = decode_hex(&case.supp_priv_info_hex); + let cose_encrypt = decode_hex(&case.cose_encrypt_hex); + + let public_cose_key = cose_key_from_public_bytes(crypto_algorithm, &public_key) + .expect("vector public key must form a COSE_Key"); + assert_eq!( + derive_kid_from_cose_key_public(&public_cose_key) + .expect("vector public COSE_Key must derive a kid") + .as_slice(), + kid.as_slice(), + "{}", + case.id + ); + + let decrypted = cose_decrypt_ml_kem_with_external_aad( + &CoseMlKemDecryptRequest::new(&cose_encrypt, &private_key, &kid, Some(&supp_priv_info)), + &external_aad, + ) + .expect("committed ML-KEM COSE_Encrypt vector must decrypt"); + + assert_eq!(decrypted.plaintext.as_slice(), plaintext, "{}", case.id); + assert_eq!(decrypted.kem_algorithm, kem_algorithm, "{}", case.id); + assert_eq!( + decrypted.content_algorithm, content_algorithm, + "{}", + case.id + ); + assert_eq!(decrypted.mode, mode, "{}", case.id); + assert_eq!(decrypted.kid.as_slice(), kid, "{}", case.id); + } +} + +fn multikey_codec_name(algorithm: &str) -> &'static str { + match algorithm { + "Ed25519" => "ed25519-pub", + "X25519" => "x25519-pub", + "P256" => "p256-pub", + "P384" => "p384-pub", + "P521" => "p521-pub", + "Secp256k1" => "secp256k1-pub", + other => panic!("unsupported algorithm name: {other}"), + } +} + +fn ml_kem_algorithm_from_name(name: &str) -> (Algorithm, CoseMlKemAlgorithm) { + match name { + "ML-KEM-512" => (Algorithm::MlKem512, CoseMlKemAlgorithm::MlKem512), + "ML-KEM-768" => (Algorithm::MlKem768, CoseMlKemAlgorithm::MlKem768), + "ML-KEM-1024" => (Algorithm::MlKem1024, CoseMlKemAlgorithm::MlKem1024), + other => panic!("unsupported ML-KEM algorithm name: {other}"), + } +} + +fn content_algorithm_from_name(name: &str) -> CoseContentEncryptionAlgorithm { + match name { + "A128GCM" => CoseContentEncryptionAlgorithm::Aes128Gcm, + "A192GCM" => CoseContentEncryptionAlgorithm::Aes192Gcm, + "A256GCM" => CoseContentEncryptionAlgorithm::Aes256Gcm, + other => panic!("unsupported content algorithm name: {other}"), + } +} + +fn ml_kem_mode_from_name(name: &str) -> CoseMlKemMode { + match name { + "direct" => CoseMlKemMode::Direct, + "key_wrap" => CoseMlKemMode::KeyWrap, + other => panic!("unsupported ML-KEM mode name: {other}"), + } +} + +fn resolve_expected_kid( + requested_kid: &[u8], + expected_kid: &[u8], + public_key: &[u8], +) -> Option> { + if requested_kid == expected_kid { + Some(public_key.to_vec()) + } else { + None + } +} + +fn assert_vector_result(id: &str, result: Result<(), CoseError>, expected_error: Option<&str>) { + match expected_error { + Some(name) => { + assert_eq!( + result.expect_err(id), + cose_error_from_name(id, name), + "{id}" + ); + } + None => result.expect(id), + } +} + +fn cose_error_from_name(id: &str, name: &str) -> CoseError { + match name { + "InvalidSignature" => CoseError::InvalidSignature, + "InvalidSignatureEncoding" => CoseError::InvalidSignatureEncoding, + "MissingKid" => CoseError::MissingKid, + "KeyNotResolved" => CoseError::KeyNotResolved, + "MissingPayload" => CoseError::MissingPayload, + "InvalidFormat" => CoseError::InvalidFormat, + "UnsupportedAlgorithm" => CoseError::UnsupportedAlgorithm, + "UnsupportedCriticalHeader" => CoseError::UnsupportedCriticalHeader, + "UnprotectedHeaderNotAllowed" => CoseError::UnprotectedHeaderNotAllowed, + "ResourceLimitExceeded" => CoseError::ResourceLimitExceeded, + "NonCanonicalCbor" => CoseError::NonCanonicalCbor, + "UnexpectedCborTag" => CoseError::UnexpectedCborTag, + "Cbor" => CoseError::Cbor, + other => panic!("unsupported expected error in vector {id}: {other}"), + } +} + +fn algorithm_from_name(name: &str) -> Algorithm { + match name { + "Ed25519" => Algorithm::Ed25519, + "P256" => Algorithm::P256, + "P384" => Algorithm::P384, + "P521" => Algorithm::P521, + "Secp256k1" => Algorithm::Secp256k1, + "X25519" => Algorithm::X25519, + "ML-DSA-44" => Algorithm::MlDsa44, + "ML-DSA-65" => Algorithm::MlDsa65, + "ML-DSA-87" => Algorithm::MlDsa87, + "ML-KEM-512" => Algorithm::MlKem512, + "ML-KEM-768" => Algorithm::MlKem768, + "ML-KEM-1024" => Algorithm::MlKem1024, + _ => panic!("unsupported algorithm name: {name}"), + } +} + +fn decode_hex(input: &str) -> Vec { + assert_eq!(input.len() % 2, 0, "hex input must have an even length"); + + input + .as_bytes() + .chunks_exact(2) + .map(|chunk| { + let high = decode_hex_nibble(chunk[0]); + let low = decode_hex_nibble(chunk[1]); + (high << 4) | low + }) + .collect() +} + +fn decode_hex_nibble(byte: u8) -> u8 { + match byte { + b'0'..=b'9' => byte - b'0', + b'a'..=b'f' => byte - b'a' + 10, + b'A'..=b'F' => byte - b'A' + 10, + _ => panic!("invalid hex character"), + } +} diff --git a/crates/cose/tests/cose_suite/cose_key_tests.rs b/crates/cose/tests/cose_suite/cose_key_tests.rs new file mode 100644 index 0000000..3fcd56b --- /dev/null +++ b/crates/cose/tests/cose_suite/cose_key_tests.rs @@ -0,0 +1,276 @@ +#![allow(missing_docs, clippy::expect_used, clippy::unwrap_used)] +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use reallyme_cose::{cose_key_from_public_bytes, cose_key_to_public_bytes, Algorithm, CoseError}; + +use super::support::{gen_ed25519, gen_p256, gen_p384, gen_p521, gen_secp256k1}; + +#[test] +fn cose_key_ed25519_roundtrip() { + let k = gen_ed25519(); + + let cose_key = cose_key_from_public_bytes(k.alg, &k.public).unwrap(); + + let out = cose_key_to_public_bytes(&cose_key).unwrap(); + + assert_eq!(out, k.public); +} + +#[test] +fn cose_key_p256_roundtrip() { + let k = gen_p256(); + + let cose_key = cose_key_from_public_bytes(k.alg, &k.public).unwrap(); + + let out = cose_key_to_public_bytes(&cose_key).unwrap(); + + assert_eq!(out, k.public); +} + +#[test] +fn cose_key_p384_roundtrip() { + let k = gen_p384(); + + let cose_key = cose_key_from_public_bytes(k.alg, &k.public).unwrap(); + + let out = cose_key_to_public_bytes(&cose_key).unwrap(); + + assert_eq!(out, k.public); +} + +#[test] +fn cose_key_p521_roundtrip() { + let k = gen_p521(); + + let cose_key = cose_key_from_public_bytes(k.alg, &k.public).unwrap(); + + let out = cose_key_to_public_bytes(&cose_key).unwrap(); + + assert_eq!(out, k.public); +} + +#[test] +fn cose_key_secp256k1_roundtrip() { + let k = gen_secp256k1(); + + let cose_key = cose_key_from_public_bytes(k.alg, &k.public).unwrap(); + + let out = cose_key_to_public_bytes(&cose_key).unwrap(); + + assert_eq!(out, k.public); +} + +#[test] +fn cose_key_rejects_invalid_ec_length() { + let bad = vec![0u8; 10]; + + let res = cose_key_from_public_bytes(Algorithm::P256, &bad); + + assert!(res.is_err()); +} + +#[test] +fn cose_key_rejects_off_curve_raw_and_uncompressed_points() { + for key in [gen_p256(), gen_p384(), gen_p521(), gen_secp256k1()] { + let uncompressed = + uncompressed_public_key(&key).expect("fixture must use a supported EC algorithm"); + let raw = uncompressed + .get(1..) + .expect("fixed SEC1 fixture must contain a prefix"); + let _ = cose_key_from_public_bytes(key.alg, raw) + .expect("valid raw point must remain supported"); + let _ = cose_key_from_public_bytes(key.alg, &uncompressed) + .expect("valid uncompressed point must remain supported"); + + let mut invalid_uncompressed = uncompressed; + let last = invalid_uncompressed + .last_mut() + .expect("fixed SEC1 fixture must contain coordinates"); + *last = last.wrapping_add(2); + let invalid_raw = invalid_uncompressed + .get(1..) + .expect("fixed SEC1 fixture must contain a prefix"); + + assert_eq!( + cose_key_from_public_bytes(key.alg, invalid_raw).err(), + Some(CoseError::InvalidKeyMaterial), + "{:?}", + key.alg, + ); + assert_eq!( + cose_key_from_public_bytes(key.alg, &invalid_uncompressed).err(), + Some(CoseError::InvalidKeyMaterial), + "{:?}", + key.alg, + ); + } +} + +fn uncompressed_public_key(key: &super::support::TestKey) -> Option> { + match key.alg { + Algorithm::P256 => Some( + reallyme_crypto::p256::decompress_public_key(&key.public) + .expect("P-256 fixture must decompress"), + ), + Algorithm::P384 => Some( + reallyme_crypto::p384::decompress_p384(&key.public) + .expect("P-384 fixture must decompress"), + ), + Algorithm::P521 => Some( + reallyme_crypto::p521::decompress_p521(&key.public) + .expect("P-521 fixture must decompress"), + ), + Algorithm::Secp256k1 => { + let (x, y) = reallyme_crypto::secp256k1::decompress_public_key(&key.public) + .expect("secp256k1 fixture must decompress"); + let mut uncompressed = Vec::with_capacity(65); + uncompressed.push(0x04); + uncompressed.extend_from_slice(&x); + uncompressed.extend_from_slice(&y); + Some(uncompressed) + } + _ => None, + } +} + +#[test] +fn cose_key_rejects_wrong_length_ml_kem_public_key() { + let k = gen_ed25519(); + + let res = cose_key_from_public_bytes(Algorithm::MlKem1024, &k.public); + + assert!(res.is_err()); +} + +#[test] +fn cose_key_ml_kem_public_roundtrips() { + let keypairs = [ + ( + Algorithm::MlKem512, + reallyme_crypto::ml_kem_512::generate_ml_kem_512_keypair() + .expect("ML-KEM-512 key generation"), + ), + ( + Algorithm::MlKem768, + reallyme_crypto::ml_kem_768::generate_ml_kem_768_keypair() + .expect("ML-KEM-768 key generation"), + ), + ( + Algorithm::MlKem1024, + reallyme_crypto::ml_kem_1024::generate_ml_kem_1024_keypair() + .expect("ML-KEM-1024 key generation"), + ), + ]; + + for (algorithm, (public_key, _)) in keypairs { + let cose_key = cose_key_from_public_bytes(algorithm, &public_key) + .expect("build ML-KEM public COSE_Key"); + let encoded = reallyme_cose::cose_key_to_vec(&cose_key).expect("encode COSE_Key"); + let decoded = reallyme_cose::cose_key_from_slice(&encoded).expect("decode COSE_Key"); + assert_eq!( + cose_key_to_public_bytes(&decoded).expect("extract public key"), + public_key, + ); + } +} + +#[test] +fn cose_key_rejects_wrong_length_ed25519_public() { + use reallyme_cose::CoseError; + + for len in [0_usize, 31, 33] { + let res = cose_key_from_public_bytes(Algorithm::Ed25519, &vec![7_u8; len]); + assert_eq!(res.err(), Some(CoseError::InvalidKeyMaterial), "len {len}"); + } +} + +#[test] +fn cose_key_rejects_wrong_length_x25519_public() { + use reallyme_cose::CoseError; + + let res = cose_key_from_public_bytes(Algorithm::X25519, &[7_u8; 31]); + assert_eq!(res.err(), Some(CoseError::InvalidKeyMaterial)); +} + +#[test] +fn cose_key_rejects_all_canonical_x25519_low_order_public_keys() { + use reallyme_cose::CoseError; + + // Canonical low-order Montgomery encodings from curve25519-dalek's + // X25519_LOW_ORDER_POINTS table. Pinning the full set here proves the COSE + // key boundary preserves reallyme-crypto's contributory-behavior policy. + for encoded in [ + "0000000000000000000000000000000000000000000000000000000000000000", + "0100000000000000000000000000000000000000000000000000000000000000", + "e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b800", + "5f9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f1157", + "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + "eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + ] { + let public_key = decode_hex_32(encoded).expect("fixed X25519 test vector must decode"); + let result = cose_key_from_public_bytes(Algorithm::X25519, &public_key); + assert_eq!( + result.err(), + Some(CoseError::InvalidKeyMaterial), + "{encoded}" + ); + } +} + +#[test] +fn cose_key_rejects_all_known_ed25519_low_order_public_key_encodings() { + use reallyme_cose::CoseError; + + // These are the canonical low-order Edwards encodings and non-canonical + // aliases documented by the C2SP CCTV Ed25519 corpus. The primitive corpus + // remains in reallyme-crypto; this pins the COSE boundary's stricter policy. + for encoded in [ + "0000000000000000000000000000000000000000000000000000000000000000", + "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + "0000000000000000000000000000000000000000000000000000000000000080", + "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0100000000000000000000000000000000000000000000000000000000000000", + "0100000000000000000000000000000000000000000000000000000000000080", + "eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + "eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05", + "26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85", + "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a", + "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa", + "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + ] { + let public_key = decode_hex_32(encoded).expect("fixed Ed25519 test vector must decode"); + let result = cose_key_from_public_bytes(Algorithm::Ed25519, &public_key); + assert_eq!( + result.err(), + Some(CoseError::InvalidKeyMaterial), + "{encoded}" + ); + } +} + +fn decode_hex_32(encoded: &str) -> Option<[u8; 32]> { + if encoded.len() != 64 { + return None; + } + let mut output = [0_u8; 32]; + for (slot, pair) in output.iter_mut().zip(encoded.as_bytes().chunks_exact(2)) { + let high = decode_hex_nibble(pair[0])?; + let low = decode_hex_nibble(pair[1])?; + *slot = high.checked_mul(16)?.checked_add(low)?; + } + Some(output) +} + +const fn decode_hex_nibble(byte: u8) -> Option { + match byte { + b'0'..=b'9' => Some(byte - b'0'), + b'a'..=b'f' => Some(byte - b'a' + 10), + b'A'..=b'F' => Some(byte - b'A' + 10), + _ => None, + } +} diff --git a/crates/cose/tests/cose_suite/cose_private_key_tests.rs b/crates/cose/tests/cose_suite/cose_private_key_tests.rs new file mode 100644 index 0000000..4833cb8 --- /dev/null +++ b/crates/cose/tests/cose_suite/cose_private_key_tests.rs @@ -0,0 +1,250 @@ +#![allow(missing_docs, clippy::expect_used, clippy::unwrap_used)] +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use ciborium::value::Value; +use coset::{iana, CborSerializable, CoseKeyBuilder}; +use reallyme_cose::{ + cose_key_from_private_bytes, cose_key_from_public_bytes, cose_key_from_slice, + cose_key_to_private_bytes, cose_key_to_vec, Algorithm, CoseError, +}; + +use super::support::{gen_ed25519, gen_p256, gen_p384, gen_p521, gen_secp256k1, gen_x25519}; + +#[test] +fn cose_key_ed25519_private_roundtrip() { + let k = gen_ed25519(); + + let cose_key = cose_key_from_private_bytes(k.alg, &k.private, Some(&k.public)).unwrap(); + let encoded = cose_key_to_vec(&cose_key).expect("constructed private key must be canonical"); + let reparsed = cose_key_from_slice(&encoded).expect("canonical private key must parse"); + let out = cose_key_to_private_bytes(&reparsed).unwrap(); + + assert_eq!(out.as_slice(), k.private.as_slice()); +} + +#[test] +fn cose_key_p256_private_roundtrip() { + let k = gen_p256(); + + let cose_key = cose_key_from_private_bytes(k.alg, &k.private, Some(&k.public)).unwrap(); + + let out = cose_key_to_private_bytes(&cose_key).unwrap(); + + assert_eq!(out.as_slice(), k.private.as_slice()); +} + +#[test] +fn cose_key_p384_private_roundtrip() { + let k = gen_p384(); + + let cose_key = cose_key_from_private_bytes(k.alg, &k.private, Some(&k.public)).unwrap(); + + let out = cose_key_to_private_bytes(&cose_key).unwrap(); + + assert_eq!(out.as_slice(), k.private.as_slice()); +} + +#[test] +fn cose_key_p521_private_roundtrip() { + let k = gen_p521(); + + let cose_key = cose_key_from_private_bytes(k.alg, &k.private, Some(&k.public)).unwrap(); + + let out = cose_key_to_private_bytes(&cose_key).unwrap(); + + assert_eq!(out.as_slice(), k.private.as_slice()); +} + +#[test] +fn cose_key_secp256k1_private_roundtrip() { + let k = gen_secp256k1(); + + let cose_key = cose_key_from_private_bytes(k.alg, &k.private, Some(&k.public)).unwrap(); + + let out = cose_key_to_private_bytes(&cose_key).unwrap(); + + assert_eq!(out.as_slice(), k.private.as_slice()); +} + +#[test] +fn cose_key_private_without_public_is_rejected() { + let k = gen_ed25519(); + + let result = cose_key_from_private_bytes(k.alg, &k.private, None); + assert_eq!( + result.err(), + Some(reallyme_cose::CoseError::MissingKeyMaterial) + ); +} + +#[test] +fn parsed_private_only_okp_key_is_rejected_without_public_binding() { + let encoded = CoseKeyBuilder::new_okp_key() + .param( + iana::OkpKeyParameter::Crv as i64, + Value::Integer((iana::EllipticCurve::Ed25519 as i64).into()), + ) + .param( + iana::OkpKeyParameter::D as i64, + Value::Bytes(vec![7_u8; 32]), + ) + .algorithm(iana::Algorithm::Ed25519) + .build() + .to_vec() + .expect("test COSE_Key must encode"); + + assert_eq!( + cose_key_from_slice(&encoded).err(), + Some(CoseError::MissingKeyMaterial), + ); +} + +#[test] +fn parsed_x25519_private_key_is_rejected_as_an_unsupported_profile() { + let private_key = gen_x25519(); + let key = Value::Map(vec![ + ( + Value::Integer((iana::KeyParameter::Kty as i64).into()), + Value::Integer((iana::KeyType::OKP as i64).into()), + ), + ( + Value::Integer((iana::OkpKeyParameter::Crv as i64).into()), + Value::Integer((iana::EllipticCurve::X25519 as i64).into()), + ), + ( + Value::Integer((iana::OkpKeyParameter::X as i64).into()), + Value::Bytes(private_key.public), + ), + ( + Value::Integer((iana::OkpKeyParameter::D as i64).into()), + Value::Bytes(private_key.private), + ), + ]); + let mut encoded = Vec::new(); + ciborium::ser::into_writer(&key, &mut encoded).expect("test COSE_Key must encode"); + + assert_eq!( + cose_key_from_slice(&encoded).err(), + Some(CoseError::UnsupportedAlgorithm), + ); +} + +#[test] +fn parsed_private_only_ec2_key_is_rejected_without_public_binding() { + let encoded = CoseKeyBuilder::default() + .key_type(iana::KeyType::EC2) + .param( + iana::Ec2KeyParameter::Crv as i64, + Value::Integer((iana::EllipticCurve::P_256 as i64).into()), + ) + .param( + iana::Ec2KeyParameter::D as i64, + Value::Bytes(vec![0_u8; 32]), + ) + .algorithm(iana::Algorithm::ESP256) + .build() + .to_vec() + .expect("test COSE_Key must encode"); + + assert_eq!( + cose_key_from_slice(&encoded).err(), + Some(CoseError::MissingKeyMaterial), + ); +} + +#[test] +fn cose_key_private_missing_d_is_rejected() { + let k = gen_ed25519(); + + // build a public-only COSE_Key + let cose_key = cose_key_from_public_bytes(k.alg, &k.public).unwrap(); + + let res = cose_key_to_private_bytes(&cose_key); + + assert!(res.is_err()); +} + +#[test] +fn cose_key_private_rejects_wrong_length_ml_kem_key() { + let k = gen_ed25519(); + + let res = cose_key_from_private_bytes(Algorithm::MlKem1024, &k.private, Some(&k.public)); + + assert!(res.is_err()); +} + +#[test] +fn cose_key_ml_kem_private_roundtrips_and_binds_public_key() { + let (public_key, private_key) = reallyme_crypto::ml_kem_768::generate_ml_kem_768_keypair() + .expect("ML-KEM-768 key generation"); + let cose_key = + cose_key_from_private_bytes(Algorithm::MlKem768, &private_key, Some(&public_key)) + .expect("build ML-KEM private COSE_Key"); + let encoded = reallyme_cose::cose_key_to_vec(&cose_key).expect("encode COSE_Key"); + let decoded = reallyme_cose::cose_key_from_slice(&encoded).expect("decode COSE_Key"); + + assert_eq!( + reallyme_cose::cose_key_to_public_bytes(&decoded).expect("extract public key"), + public_key, + ); + assert_eq!( + reallyme_cose::cose_key_to_private_bytes(&decoded) + .expect("extract private key") + .as_slice(), + private_key.as_slice(), + ); + + let (other_public_key, _) = reallyme_crypto::ml_kem_768::generate_ml_kem_768_keypair() + .expect("second ML-KEM-768 key generation"); + assert_eq!( + cose_key_from_private_bytes(Algorithm::MlKem768, &private_key, Some(&other_public_key),) + .err(), + Some(reallyme_cose::CoseError::InvalidKeyMaterial), + ); +} + +#[test] +fn cose_key_rejects_wrong_length_ed25519_private() { + use reallyme_cose::CoseError; + + let res = cose_key_from_private_bytes(Algorithm::Ed25519, &[7_u8; 31], None); + assert_eq!(res.err(), Some(CoseError::InvalidKeyMaterial)); +} + +#[test] +fn cose_key_rejects_wrong_length_ec2_private() { + use reallyme_cose::CoseError; + + let res = cose_key_from_private_bytes(Algorithm::P256, &[7_u8; 31], None); + assert_eq!(res.err(), Some(CoseError::InvalidKeyMaterial)); +} + +#[test] +fn cose_key_rejects_mismatched_ed25519_private_and_public_keys() { + let private = gen_ed25519(); + let public = gen_ed25519(); + + let result = + cose_key_from_private_bytes(Algorithm::Ed25519, &private.private, Some(&public.public)); + + assert_eq!( + result.err(), + Some(reallyme_cose::CoseError::InvalidKeyMaterial) + ); +} + +#[test] +fn cose_key_rejects_mismatched_p256_private_and_public_keys() { + let private = gen_p256(); + let public = gen_p256(); + + let result = + cose_key_from_private_bytes(Algorithm::P256, &private.private, Some(&public.public)); + + assert_eq!( + result.err(), + Some(reallyme_cose::CoseError::InvalidKeyMaterial) + ); +} diff --git a/tests/cose_suite/detached_reject_wrong_payload_tests.rs b/crates/cose/tests/cose_suite/detached_reject_wrong_payload_tests.rs similarity index 95% rename from tests/cose_suite/detached_reject_wrong_payload_tests.rs rename to crates/cose/tests/cose_suite/detached_reject_wrong_payload_tests.rs index b3647e4..bc84625 100644 --- a/tests/cose_suite/detached_reject_wrong_payload_tests.rs +++ b/crates/cose/tests/cose_suite/detached_reject_wrong_payload_tests.rs @@ -17,7 +17,7 @@ fn cose_detached_rejects_wrong_payload() { let cose = cose_sign1_detached(k.alg, &payload, &k.private, Some(kid)).expect("sign detached"); - let resolver = |k_: &[u8]| { + let resolver = |_, k_: &[u8]| { if k_ == kid { Some(k.public.clone()) } else { @@ -40,7 +40,7 @@ fn cose_detached_with_kid_rejects_wrong_payload() { let cose = cose_sign1_detached(k.alg, &payload, &k.private, Some(kid)).expect("sign detached"); - let resolver = |k_: &[u8]| { + let resolver = |_, k_: &[u8]| { if k_ == kid { Some(k.public.clone()) } else { diff --git a/tests/cose_suite/detached_roundtrip_tests.rs b/crates/cose/tests/cose_suite/detached_roundtrip_tests.rs similarity index 94% rename from tests/cose_suite/detached_roundtrip_tests.rs rename to crates/cose/tests/cose_suite/detached_roundtrip_tests.rs index 9acc8df..de5dd80 100644 --- a/tests/cose_suite/detached_roundtrip_tests.rs +++ b/crates/cose/tests/cose_suite/detached_roundtrip_tests.rs @@ -15,7 +15,7 @@ fn cose_detached_roundtrip() { let cose = cose_sign1_detached(k.alg, &payload, &k.private, Some(kid)).unwrap(); - let resolver = |k_: &[u8]| { + let resolver = |_, k_: &[u8]| { if k_ == kid { Some(k.public.clone()) } else { @@ -35,7 +35,7 @@ fn cose_detached_allows_empty_kid_when_resolver_accepts_default_key() { let cose = cose_sign1_detached(k.alg, &payload, &k.private, None).unwrap(); - let resolver = |kid: &[u8]| { + let resolver = |_, kid: &[u8]| { if kid.is_empty() { Some(k.public.clone()) } else { diff --git a/crates/cose/tests/cose_suite/encrypt_semantic_tests.rs b/crates/cose/tests/cose_suite/encrypt_semantic_tests.rs new file mode 100644 index 0000000..3c48e9c --- /dev/null +++ b/crates/cose/tests/cose_suite/encrypt_semantic_tests.rs @@ -0,0 +1,427 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use buffa::{EnumValue, Message}; +use reallyme_cose::wire::cose_error_proto; +use reallyme_cose::wire::cose_operation_request::Operation; +use reallyme_cose::wire::{ + decode_cose_error, execute_operation_proto, execute_operation_proto_json, + CoseContentEncryptionAlgorithm as ProtoContentAlgorithm, CoseErrorReason, CoseKemAlgorithm, + CoseMlKemDecryptRequest as ProtoDecryptRequest, CoseMlKemDecryptResult, + CoseMlKemEncryptRequest as ProtoEncryptRequest, CoseMlKemEncryptResult, + CoseMlKemMode as ProtoMode, CoseOperationRequest, +}; +use reallyme_cose::{ + cose_decrypt_ml_kem_with_external_aad, cose_encrypt_ml_kem_direct_with_external_aad, + cose_encrypt_ml_kem_key_wrap_with_external_aad, cose_key_from_public_bytes, + derive_kid_from_cose_key_public, Algorithm, CoseContentEncryptionAlgorithm, CoseError, + CoseMlKemAlgorithm, CoseMlKemDecryptRequest, CoseMlKemEncryptRequest, CoseMlKemMode, + DecryptedCoseEncrypt, +}; +use zeroize::Zeroizing; + +use super::support::{decode_operation_output, OperationOutputStatus}; + +const PLAINTEXT: &[u8] = b"authenticated COSE plaintext"; +const EXTERNAL_AAD: &[u8] = b"COSE external aad"; +const SUPP_PRIV_INFO: &[u8] = b"COSE private context"; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum ErrorBranch { + Primitive, + Provider, + Backend, +} + +struct MlKemFixture { + public_key: Zeroizing>, + private_key: Zeroizing>, + kid: Zeroizing>, +} + +#[test] +fn ml_kem_operations_match_native_binary_and_proto_json_semantics() { + let MlKemFixture { + public_key, + private_key, + kid, + } = keypair(); + + for mode in [CoseMlKemMode::Direct, CoseMlKemMode::KeyWrap] { + let native_request = native_encrypt_request(&public_key, &kid); + let native_ciphertext = match mode { + CoseMlKemMode::Direct => { + cose_encrypt_ml_kem_direct_with_external_aad(&native_request, EXTERNAL_AAD) + } + CoseMlKemMode::KeyWrap => { + cose_encrypt_ml_kem_key_wrap_with_external_aad(&native_request, EXTERNAL_AAD) + } + _ => return, + } + .expect("native ML-KEM encryption must succeed"); + assert_native_decryption(&native_ciphertext, &private_key, &kid, mode); + + let binary_ciphertext = execute_encrypt_binary(encrypt_operation( + mode, + proto_encrypt_request(&public_key, &kid), + )); + assert_native_decryption(&binary_ciphertext, &private_key, &kid, mode); + + let json_ciphertext = execute_encrypt_json(encrypt_operation( + mode, + proto_encrypt_request(&public_key, &kid), + )); + assert_native_decryption(&json_ciphertext, &private_key, &kid, mode); + + let decrypted = execute_decrypt_both( + proto_decrypt_request(&native_ciphertext, &private_key, &kid, EXTERNAL_AAD), + proto_decrypt_request(&native_ciphertext, &private_key, &kid, EXTERNAL_AAD), + ); + assert_eq!(decrypted.plaintext, PLAINTEXT); + assert_eq!( + decrypted.content_algorithm.as_known(), + Some(ProtoContentAlgorithm::Aes128Gcm) + ); + assert_eq!( + decrypted.kem_algorithm.as_known(), + Some(CoseKemAlgorithm::MlKem512) + ); + assert_eq!(decrypted.mode.as_known(), Some(proto_mode(mode))); + assert_eq!(decrypted.recipient_kid, kid.as_slice()); + } +} + +#[test] +fn ml_kem_failures_preserve_native_and_exact_wire_semantics() { + let MlKemFixture { + public_key, + private_key, + kid, + } = keypair(); + let missing_kid_request = CoseMlKemEncryptRequest::new( + CoseMlKemAlgorithm::MlKem512, + CoseContentEncryptionAlgorithm::Aes128Gcm, + &public_key, + &[], + PLAINTEXT, + Some(SUPP_PRIV_INFO), + ); + assert_native_error( + cose_encrypt_ml_kem_direct_with_external_aad(&missing_kid_request, EXTERNAL_AAD), + CoseError::MissingKid, + ); + assert_error( + Operation::MlKemEncryptDirect(Box::new(proto_encrypt_request(&public_key, &[]))), + Operation::MlKemEncryptDirect(Box::new(proto_encrypt_request(&public_key, &[]))), + ErrorBranch::Primitive, + CoseErrorReason::EncryptMissingKid, + ); + + let invalid_public_key = Zeroizing::new(vec![0_u8; 31]); + let invalid_key_request = native_encrypt_request(&invalid_public_key, &kid); + assert_native_error( + cose_encrypt_ml_kem_direct_with_external_aad(&invalid_key_request, EXTERNAL_AAD), + CoseError::InvalidKeyMaterial, + ); + assert_error( + Operation::MlKemEncryptDirect(Box::new(proto_encrypt_request(&invalid_public_key, &kid))), + Operation::MlKemEncryptDirect(Box::new(proto_encrypt_request(&invalid_public_key, &kid))), + ErrorBranch::Primitive, + CoseErrorReason::KeyInvalidKeyMaterial, + ); + + let native_ciphertext = cose_encrypt_ml_kem_direct_with_external_aad( + &native_encrypt_request(&public_key, &kid), + EXTERNAL_AAD, + ) + .expect("valid direct fixture must encrypt"); + let invalid_private_key = Zeroizing::new(vec![0_u8; 63]); + assert_native_error( + cose_decrypt_ml_kem_with_external_aad( + &native_decrypt_request(&native_ciphertext, &invalid_private_key, &kid), + EXTERNAL_AAD, + ), + CoseError::InvalidKeyMaterial, + ); + assert_error( + Operation::MlKemDecrypt(Box::new(proto_decrypt_request( + &native_ciphertext, + &invalid_private_key, + &kid, + EXTERNAL_AAD, + ))), + Operation::MlKemDecrypt(Box::new(proto_decrypt_request( + &native_ciphertext, + &invalid_private_key, + &kid, + EXTERNAL_AAD, + ))), + ErrorBranch::Primitive, + CoseErrorReason::KeyInvalidKeyMaterial, + ); + + let wrong_kid = Zeroizing::new(vec![0xA5_u8; kid.len()]); + assert_native_error( + cose_decrypt_ml_kem_with_external_aad( + &native_decrypt_request(&native_ciphertext, &private_key, &wrong_kid), + EXTERNAL_AAD, + ), + CoseError::KidMismatch, + ); + assert_error( + Operation::MlKemDecrypt(Box::new(proto_decrypt_request( + &native_ciphertext, + &private_key, + &wrong_kid, + EXTERNAL_AAD, + ))), + Operation::MlKemDecrypt(Box::new(proto_decrypt_request( + &native_ciphertext, + &private_key, + &wrong_kid, + EXTERNAL_AAD, + ))), + ErrorBranch::Primitive, + CoseErrorReason::EncryptKidMismatch, + ); + + assert_native_error( + cose_decrypt_ml_kem_with_external_aad( + &native_decrypt_request(&native_ciphertext, &private_key, &kid), + b"wrong external aad", + ), + CoseError::AuthenticationFailed, + ); + assert_error( + Operation::MlKemDecrypt(Box::new(proto_decrypt_request( + &native_ciphertext, + &private_key, + &kid, + b"wrong external aad", + ))), + Operation::MlKemDecrypt(Box::new(proto_decrypt_request( + &native_ciphertext, + &private_key, + &kid, + b"wrong external aad", + ))), + ErrorBranch::Primitive, + CoseErrorReason::EncryptAuthenticationFailed, + ); + + let mut unsupported = proto_encrypt_request(&public_key, &kid); + unsupported.kem_algorithm = EnumValue::from(CoseKemAlgorithm::XWing768); + let mut unsupported_json = proto_encrypt_request(&public_key, &kid); + unsupported_json.kem_algorithm = EnumValue::from(CoseKemAlgorithm::XWing768); + assert_error( + Operation::MlKemEncryptDirect(Box::new(unsupported)), + Operation::MlKemEncryptDirect(Box::new(unsupported_json)), + ErrorBranch::Provider, + CoseErrorReason::CommonUnsupportedAlgorithm, + ); +} + +fn keypair() -> MlKemFixture { + let (public_key, private_key) = reallyme_crypto::ml_kem_512::generate_ml_kem_512_keypair() + .expect("ML-KEM-512 key generation must succeed"); + let public_key = Zeroizing::new(public_key); + let cose_key = cose_key_from_public_bytes(Algorithm::MlKem512, &public_key) + .expect("ML-KEM public COSE_Key must construct"); + let kid = + derive_kid_from_cose_key_public(&cose_key).expect("ML-KEM kid derivation must succeed"); + MlKemFixture { + public_key, + private_key, + kid, + } +} + +fn native_encrypt_request<'a>(public_key: &'a [u8], kid: &'a [u8]) -> CoseMlKemEncryptRequest<'a> { + CoseMlKemEncryptRequest::new( + CoseMlKemAlgorithm::MlKem512, + CoseContentEncryptionAlgorithm::Aes128Gcm, + public_key, + kid, + PLAINTEXT, + Some(SUPP_PRIV_INFO), + ) +} + +fn native_decrypt_request<'a>( + cose_encrypt: &'a [u8], + private_key: &'a [u8], + kid: &'a [u8], +) -> CoseMlKemDecryptRequest<'a> { + CoseMlKemDecryptRequest::new(cose_encrypt, private_key, kid, Some(SUPP_PRIV_INFO)) +} + +fn proto_encrypt_request(public_key: &[u8], kid: &[u8]) -> ProtoEncryptRequest { + ProtoEncryptRequest { + kem_algorithm: EnumValue::from(CoseKemAlgorithm::MlKem512), + content_algorithm: EnumValue::from(ProtoContentAlgorithm::Aes128Gcm), + recipient_public_key: public_key.to_vec(), + recipient_kid: kid.to_vec(), + plaintext: PLAINTEXT.to_vec(), + external_aad: EXTERNAL_AAD.to_vec(), + supp_priv_info: SUPP_PRIV_INFO.to_vec(), + has_supp_priv_info: true, + __buffa_unknown_fields: Default::default(), + } +} + +fn proto_decrypt_request( + cose_encrypt: &[u8], + private_key: &[u8], + kid: &[u8], + external_aad: &[u8], +) -> ProtoDecryptRequest { + ProtoDecryptRequest { + cose_encrypt: cose_encrypt.to_vec(), + recipient_private_key: private_key.to_vec(), + expected_recipient_kid: kid.to_vec(), + external_aad: external_aad.to_vec(), + supp_priv_info: SUPP_PRIV_INFO.to_vec(), + has_supp_priv_info: true, + __buffa_unknown_fields: Default::default(), + } +} + +fn encrypt_operation(mode: CoseMlKemMode, request: ProtoEncryptRequest) -> Operation { + match mode { + CoseMlKemMode::Direct => Operation::MlKemEncryptDirect(Box::new(request)), + CoseMlKemMode::KeyWrap => Operation::MlKemEncryptKeyWrap(Box::new(request)), + _ => Operation::MlKemEncryptDirect(Box::new(request)), + } +} + +fn proto_mode(mode: CoseMlKemMode) -> ProtoMode { + match mode { + CoseMlKemMode::Direct => ProtoMode::Direct, + CoseMlKemMode::KeyWrap => ProtoMode::KeyWrap, + _ => ProtoMode::Unspecified, + } +} + +fn assert_native_decryption( + cose_encrypt: &[u8], + private_key: &[u8], + kid: &[u8], + expected_mode: CoseMlKemMode, +) { + let decrypted = cose_decrypt_ml_kem_with_external_aad( + &native_decrypt_request(cose_encrypt, private_key, kid), + EXTERNAL_AAD, + ) + .expect("native ML-KEM decryption must succeed"); + assert_decrypted(&decrypted, expected_mode, kid); +} + +fn assert_decrypted(decrypted: &DecryptedCoseEncrypt, expected_mode: CoseMlKemMode, kid: &[u8]) { + assert_eq!(decrypted.plaintext.as_slice(), PLAINTEXT); + assert_eq!( + decrypted.content_algorithm, + CoseContentEncryptionAlgorithm::Aes128Gcm + ); + assert_eq!(decrypted.kem_algorithm, CoseMlKemAlgorithm::MlKem512); + assert_eq!(decrypted.mode, expected_mode); + assert_eq!(decrypted.kid.as_slice(), kid); +} + +fn execute_encrypt_binary(operation: Operation) -> Zeroizing> { + decode_encrypt_result(process_binary(operation)) +} + +fn execute_encrypt_json(operation: Operation) -> Zeroizing> { + decode_encrypt_result(process_json(operation)) +} + +fn decode_encrypt_result(payload: Zeroizing>) -> Zeroizing> { + let mut result = CoseMlKemEncryptResult::decode_from_slice(&payload) + .expect("ML-KEM encrypt result must decode"); + Zeroizing::new(core::mem::take(&mut result.cose_encrypt)) +} + +fn execute_decrypt_both( + binary_request: ProtoDecryptRequest, + json_request: ProtoDecryptRequest, +) -> CoseMlKemDecryptResult { + let binary_operation = Operation::MlKemDecrypt(Box::new(binary_request)); + let json_operation = Operation::MlKemDecrypt(Box::new(json_request)); + let binary_envelope = process_binary_envelope(binary_operation); + let json_envelope = process_json_envelope(json_operation); + assert_eq!(binary_envelope.as_slice(), json_envelope.as_slice()); + let payload = decode_success(&binary_envelope); + CoseMlKemDecryptResult::decode_from_slice(&payload).expect("ML-KEM decrypt result must decode") +} + +fn process_binary(operation: Operation) -> Zeroizing> { + decode_success(&process_binary_envelope(operation)) +} + +fn process_json(operation: Operation) -> Zeroizing> { + decode_success(&process_json_envelope(operation)) +} + +fn process_binary_envelope(operation: Operation) -> Zeroizing> { + let request = operation_request(operation); + execute_operation_proto(&Zeroizing::new(request.encode_to_vec())) +} + +fn process_json_envelope(operation: Operation) -> Zeroizing> { + let request = operation_request(operation); + let json = Zeroizing::new( + serde_json::to_string(&request).expect("generated encrypt ProtoJSON must encode"), + ); + execute_operation_proto_json(&json) +} + +fn decode_success(envelope: &[u8]) -> Zeroizing> { + let output = decode_operation_output(envelope) + .ok() + .expect("successful encrypt envelope must decode"); + assert_eq!(output.status(), OperationOutputStatus::Result); + Zeroizing::new(output.bytes().to_vec()) +} + +fn assert_error( + binary_operation: Operation, + json_operation: Operation, + expected_branch: ErrorBranch, + expected_reason: CoseErrorReason, +) { + let binary_envelope = process_binary_envelope(binary_operation); + let json_envelope = process_json_envelope(json_operation); + assert_eq!(binary_envelope.as_slice(), json_envelope.as_slice()); + let output = match decode_operation_output(&binary_envelope) { + Ok(output) | Err(output) => output, + }; + assert_eq!(output.status(), OperationOutputStatus::CoseError); + let error = decode_cose_error(output.bytes()).expect("structured encrypt error must decode"); + let Some(branch) = error.error.as_ref() else { + assert!(error.error.is_some(), "structured error branch must exist"); + return; + }; + let (branch, reason) = match branch { + cose_error_proto::Error::Primitive(error) => { + (ErrorBranch::Primitive, error.reason.as_known()) + } + cose_error_proto::Error::Provider(error) => { + (ErrorBranch::Provider, error.reason.as_known()) + } + cose_error_proto::Error::Backend(error) => (ErrorBranch::Backend, error.reason.as_known()), + }; + assert_eq!(branch, expected_branch); + assert_eq!(reason, Some(expected_reason)); +} + +fn assert_native_error(result: Result, expected: CoseError) { + assert!(matches!(result, Err(error) if error == expected)); +} + +fn operation_request(operation: Operation) -> CoseOperationRequest { + CoseOperationRequest { + operation: Some(operation), + __buffa_unknown_fields: Default::default(), + } +} diff --git a/crates/cose/tests/cose_suite/external_aad_tests.rs b/crates/cose/tests/cose_suite/external_aad_tests.rs new file mode 100644 index 0000000..2e36c23 --- /dev/null +++ b/crates/cose/tests/cose_suite/external_aad_tests.rs @@ -0,0 +1,84 @@ +#![allow(missing_docs, clippy::expect_used, clippy::unwrap_used)] +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use reallyme_cose::{ + cose_sign1_detached_with_options_and_external_aad, cose_sign1_with_options_and_external_aad, + cose_verify1_detached_with_policy_and_external_aad, cose_verify1_with_policy_and_external_aad, + CoseError, CosePolicy, CoseSign1EncodeOptions, +}; + +use crate::support::{gen_ed25519, sample_payload, test_kid}; + +#[test] +fn attached_external_aad_roundtrips_and_wrong_aad_fails_authentication() { + let key = gen_ed25519(); + let payload = sample_payload(); + let external_aad = b"reallyme-cose/external-aad/v1"; + let cose = cose_sign1_with_options_and_external_aad( + key.alg, + &payload, + &key.private, + Some(test_kid()), + external_aad, + CoseSign1EncodeOptions::default(), + ) + .expect("attached signing with external AAD must succeed"); + + let verified = cose_verify1_with_policy_and_external_aad( + &cose, + external_aad, + &CosePolicy::default(), + |_, kid| (kid == test_kid()).then(|| key.public.clone()), + ) + .expect("matching external AAD must verify"); + assert_eq!(verified.payload.as_slice(), payload.as_slice()); + + let error = cose_verify1_with_policy_and_external_aad( + &cose, + b"wrong-external-aad", + &CosePolicy::default(), + |_, kid| (kid == test_kid()).then(|| key.public.clone()), + ) + .err() + .expect("wrong external AAD must not verify"); + assert_eq!(error, CoseError::InvalidSignature); +} + +#[test] +fn detached_external_aad_roundtrips_and_wrong_aad_fails_authentication() { + let key = gen_ed25519(); + let payload = sample_payload(); + let external_aad = b"reallyme-cose/detached-external-aad/v1"; + let cose = cose_sign1_detached_with_options_and_external_aad( + key.alg, + &payload, + &key.private, + Some(test_kid()), + external_aad, + CoseSign1EncodeOptions::default(), + ) + .expect("detached signing with external AAD must succeed"); + + let verified = cose_verify1_detached_with_policy_and_external_aad( + &cose, + &payload, + external_aad, + &CosePolicy::default(), + |_, kid| (kid == test_kid()).then(|| key.public.clone()), + ) + .expect("matching detached external AAD must verify"); + assert_eq!(verified.kid.as_slice(), test_kid()); + + let error = cose_verify1_detached_with_policy_and_external_aad( + &cose, + &payload, + b"wrong-external-aad", + &CosePolicy::default(), + |_, kid| (kid == test_kid()).then(|| key.public.clone()), + ) + .err() + .expect("wrong detached external AAD must not verify"); + assert_eq!(error, CoseError::InvalidSignature); +} diff --git a/tests/cose_suite/interop_verify_tests.rs b/crates/cose/tests/cose_suite/interop_verify_tests.rs similarity index 74% rename from tests/cose_suite/interop_verify_tests.rs rename to crates/cose/tests/cose_suite/interop_verify_tests.rs index 8d67995..6ff54b8 100644 --- a/tests/cose_suite/interop_verify_tests.rs +++ b/crates/cose/tests/cose_suite/interop_verify_tests.rs @@ -15,7 +15,7 @@ use reallyme_cose::{cose_sign1, cose_verify1, CoseError}; use reallyme_crypto::core::Algorithm; use reallyme_crypto::dispatch::sign; -use crate::support::{gen_ed25519, gen_p256, sample_payload, test_kid}; +use crate::support::{gen_ed25519, gen_p256, gen_secp256k1, sample_payload, test_kid}; fn encode_value(value: &Value) -> Vec { let mut encoded = Vec::new(); @@ -28,7 +28,7 @@ fn encode_value(value: &Value) -> Vec { fn build_reordered_header_sign1(key: &crate::support::TestKey, payload: &[u8]) -> Vec { let protected_map = Value::Map(vec![ (Value::Integer(4.into()), Value::Bytes(test_kid().to_vec())), - (Value::Integer(1.into()), Value::Integer((-8).into())), + (Value::Integer(1.into()), Value::Integer((-19).into())), ]); let protected_bytes = encode_value(&protected_map); @@ -56,12 +56,12 @@ fn verify_accepts_non_canonical_protected_header_order() { let payload = sample_payload(); let cose_bytes = build_reordered_header_sign1(&key, &payload); - let verified = cose_verify1(&cose_bytes, |kid| { + let verified = cose_verify1(&cose_bytes, |_, kid| { (kid == test_kid()).then(|| key.public.clone()) }) .expect("reordered protected header must verify over received bytes"); - assert_eq!(verified, payload); + assert_eq!(verified.as_slice(), payload.as_slice()); } #[test] @@ -74,12 +74,12 @@ fn verify_accepts_cose_sign1_tag_18_root() { ciborium::de::from_reader(Cursor::new(untagged.as_slice())).expect("must decode"); let tagged = encode_value(&Value::Tag(18, Box::new(decoded))); - let verified = cose_verify1(&tagged, |kid| { + let verified = cose_verify1(&tagged, |_, kid| { (kid == test_kid()).then(|| key.public.clone()) }) .expect("tag 18 COSE_Sign1 must verify"); - assert_eq!(verified, payload); + assert_eq!(verified.as_slice(), payload.as_slice()); } #[test] @@ -92,12 +92,12 @@ fn ecdsa_der_encoded_signature_is_rejected() { cose.signature = raw_to_der(&cose.signature); let mutated = cose.to_vec().unwrap(); - let err = cose_verify1(&mutated, |kid| { + let err = cose_verify1(&mutated, |_, kid| { (kid == test_kid()).then(|| key.public.clone()) }) .expect_err("DER-encoded ECDSA signature must be rejected"); - assert_eq!(err, CoseError::InvalidSignature); + assert_eq!(err, CoseError::InvalidSignatureEncoding); } #[test] @@ -110,6 +110,38 @@ fn ecdsa_signature_is_fixed_width_r_s() { assert_eq!(cose.signature.len(), 64, "ES256 must be raw r||s"); } +#[test] +fn resolver_binds_same_width_ec_keys_to_the_expected_algorithm() { + let payload = sample_payload(); + let p256 = gen_p256(); + let secp256k1 = gen_secp256k1(); + + for (signed, wrong_key) in [ + ( + cose_sign1(p256.alg, &payload, &p256.private, Some(test_kid())) + .expect("P-256 fixture must sign"), + &secp256k1, + ), + ( + cose_sign1( + secp256k1.alg, + &payload, + &secp256k1.private, + Some(test_kid()), + ) + .expect("secp256k1 fixture must sign"), + &p256, + ), + ] { + let error = cose_verify1(&signed, |expected_algorithm, kid| { + (expected_algorithm == wrong_key.alg && kid == test_kid()) + .then(|| wrong_key.public.clone()) + }) + .expect_err("resolver must not return a key bound to another algorithm"); + assert_eq!(error, CoseError::KeyNotResolved); + } +} + /// Minimal DER ECDSA-Sig-Value encoder for negative-path fixtures. fn raw_to_der(raw: &[u8]) -> Vec { let half = raw.len() / 2; diff --git a/crates/cose/tests/cose_suite/key_family_semantic_tests.rs b/crates/cose/tests/cose_suite/key_family_semantic_tests.rs new file mode 100644 index 0000000..08e9c6f --- /dev/null +++ b/crates/cose/tests/cose_suite/key_family_semantic_tests.rs @@ -0,0 +1,250 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +#![allow(clippy::expect_used)] + +use buffa::{EnumValue, Message}; +use reallyme_cose::wire::cose_operation_request::Operation; +use reallyme_cose::wire::{ + cose_algorithm_identifier, cose_error_proto, decode_cose_error, execute_operation_proto, + execute_operation_proto_json, CoseAlgorithmIdentifier, CoseErrorReason, CoseKemAlgorithm, + CoseKeyBytesRequest, CoseKeyBytesResult, CoseKeyFromPrivateBytesRequest, + CoseKeyFromPublicBytesRequest, CoseMultikeyResult, CoseMultikeyToCoseKeyRequest, + CoseOperationRequest, CoseSignatureAlgorithm, +}; +use reallyme_cose::{ + cose_key_from_private_bytes, cose_key_from_public_bytes, cose_key_to_multikey, cose_key_to_vec, + derive_kid_from_cose_key_public, multikey_to_cose_key, +}; +use zeroize::Zeroizing; + +use super::support::{decode_operation_output, gen_ed25519, OperationOutputStatus}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum ErrorBranch { + Primitive, + Provider, + Backend, +} + +#[test] +fn remaining_key_family_operations_match_native_binary_and_proto_json() { + let mut fixture = gen_ed25519(); + let public_key = Zeroizing::new(core::mem::take(&mut fixture.public)); + let private_key = Zeroizing::new(core::mem::take(&mut fixture.private)); + + let native_public = cose_key_from_public_bytes(fixture.alg, &public_key) + .expect("public fixture must construct"); + let encoded_public = cose_key_to_vec(&native_public).expect("public fixture must encode"); + let constructed_public = execute_key_bytes(Operation::KeyFromPublicBytes(Box::new( + CoseKeyFromPublicBytesRequest { + algorithm: ed25519_identifier(), + public_key: public_key.to_vec(), + __buffa_unknown_fields: Default::default(), + }, + ))); + assert_eq!(constructed_public.as_slice(), encoded_public.as_slice()); + + let native_private = cose_key_from_private_bytes(fixture.alg, &private_key, Some(&public_key)) + .expect("private fixture must construct"); + let encoded_private = + cose_key_to_vec(&native_private).expect("constructed private key must encode canonically"); + let constructed_private = execute_key_bytes(Operation::KeyFromPrivateBytes(Box::new( + CoseKeyFromPrivateBytesRequest { + algorithm: ed25519_identifier(), + private_key: private_key.to_vec(), + public_key: public_key.to_vec(), + has_public_key: true, + __buffa_unknown_fields: Default::default(), + }, + ))); + assert_eq!(constructed_private.as_slice(), encoded_private.as_slice()); + + let extracted_public = execute_key_bytes(Operation::KeyToPublicBytes(Box::new(key_request( + &encoded_private, + )))); + assert_eq!(extracted_public.as_slice(), public_key.as_slice()); + + let extracted_private = execute_key_bytes(Operation::KeyToPrivateBytes(Box::new(key_request( + &encoded_private, + )))); + assert_eq!(extracted_private.as_slice(), private_key.as_slice()); + + let native_kid = Zeroizing::new( + derive_kid_from_cose_key_public(&native_private).expect("kid derivation must succeed"), + ); + let derived_kid = execute_key_bytes(Operation::KeyDerivePublicKid(Box::new(key_request( + &encoded_private, + )))); + assert_eq!(derived_kid.as_slice(), native_kid.as_slice()); + + let native_multikey = Zeroizing::new( + cose_key_to_multikey(&native_private).expect("Multikey conversion must succeed"), + ); + let converted_multikey = execute_multikey(Operation::KeyToMultikey(Box::new(key_request( + &encoded_private, + )))); + assert_eq!(converted_multikey.as_str(), native_multikey.as_str()); + + let native_from_multikey = + multikey_to_cose_key(&native_multikey).expect("Multikey fixture must parse"); + let native_from_multikey = + cose_key_to_vec(&native_from_multikey).expect("Multikey key must encode"); + let converted_key = execute_key_bytes(Operation::MultikeyToCoseKey(Box::new( + CoseMultikeyToCoseKeyRequest { + multikey: native_multikey.to_string(), + __buffa_unknown_fields: Default::default(), + }, + ))); + assert_eq!(converted_key.as_slice(), native_from_multikey.as_slice()); +} + +#[test] +fn remaining_key_family_failures_preserve_exact_branch_and_reason() { + let mut fixture = gen_ed25519(); + let public_key = Zeroizing::new(core::mem::take(&mut fixture.public)); + let private_key = Zeroizing::new(core::mem::take(&mut fixture.private)); + let public_cose_key = cose_key_from_public_bytes(fixture.alg, &public_key) + .and_then(|key| cose_key_to_vec(&key)) + .expect("public fixture must encode"); + + assert_error( + Operation::KeyFromPublicBytes(Box::new(CoseKeyFromPublicBytesRequest { + algorithm: ed25519_identifier(), + public_key: vec![0_u8; 31], + __buffa_unknown_fields: Default::default(), + })), + ErrorBranch::Primitive, + CoseErrorReason::KeyInvalidKeyMaterial, + ); + assert_error( + Operation::KeyFromPrivateBytes(Box::new(CoseKeyFromPrivateBytesRequest { + algorithm: ed25519_identifier(), + private_key: private_key.to_vec(), + public_key: Vec::new(), + has_public_key: false, + __buffa_unknown_fields: Default::default(), + })), + ErrorBranch::Primitive, + CoseErrorReason::KeyMissingKeyMaterial, + ); + assert_error( + Operation::KeyToPrivateBytes(Box::new(key_request(&public_cose_key))), + ErrorBranch::Primitive, + CoseErrorReason::KeyMissingKeyMaterial, + ); + assert_error( + Operation::MultikeyToCoseKey(Box::new(CoseMultikeyToCoseKeyRequest { + multikey: "not-a-multikey".to_owned(), + __buffa_unknown_fields: Default::default(), + })), + ErrorBranch::Primitive, + CoseErrorReason::MultikeyInvalidMultikey, + ); + assert_error( + Operation::KeyFromPublicBytes(Box::new(CoseKeyFromPublicBytesRequest { + algorithm: buffa::MessageField::some(CoseAlgorithmIdentifier { + algorithm: Some(cose_algorithm_identifier::Algorithm::Kem(EnumValue::from( + CoseKemAlgorithm::XWing768, + ))), + __buffa_unknown_fields: Default::default(), + }), + public_key: public_key.to_vec(), + __buffa_unknown_fields: Default::default(), + })), + ErrorBranch::Provider, + CoseErrorReason::CommonUnsupportedAlgorithm, + ); +} + +fn ed25519_identifier( +) -> buffa::MessageField> { + buffa::MessageField::some(CoseAlgorithmIdentifier { + algorithm: Some(cose_algorithm_identifier::Algorithm::Signature( + EnumValue::from(CoseSignatureAlgorithm::Ed25519), + )), + __buffa_unknown_fields: Default::default(), + }) +} + +fn key_request(encoded: &[u8]) -> CoseKeyBytesRequest { + CoseKeyBytesRequest { + cose_key: encoded.to_vec(), + __buffa_unknown_fields: Default::default(), + } +} + +fn operation_request(operation: Operation) -> CoseOperationRequest { + CoseOperationRequest { + operation: Some(operation), + __buffa_unknown_fields: Default::default(), + } +} + +fn execute_key_bytes(operation: Operation) -> Zeroizing> { + let payload = execute(operation); + let mut result = CoseKeyBytesResult::decode_from_slice(&payload) + .expect("key-family result must decode as CoseKeyBytesResult"); + Zeroizing::new(core::mem::take(&mut result.key_bytes)) +} + +fn execute_multikey(operation: Operation) -> Zeroizing { + let payload = execute(operation); + let mut result = CoseMultikeyResult::decode_from_slice(&payload) + .expect("key-family result must decode as CoseMultikeyResult"); + Zeroizing::new(core::mem::take(&mut result.multikey)) +} + +fn execute(operation: Operation) -> Zeroizing> { + let request = operation_request(operation); + let encoded_request = Zeroizing::new(request.encode_to_vec()); + let json_request = Zeroizing::new( + serde_json::to_string(&request).expect("generated request ProtoJSON must encode"), + ); + let binary_envelope = execute_operation_proto(&encoded_request); + let json_envelope = execute_operation_proto_json(&json_request); + assert_eq!(binary_envelope.as_slice(), json_envelope.as_slice()); + + let output = decode_operation_output(&binary_envelope) + .ok() + .expect("successful key-family envelope must decode"); + assert_eq!(output.status(), OperationOutputStatus::Result); + Zeroizing::new(output.bytes().to_vec()) +} + +fn assert_error( + operation: Operation, + expected_branch: ErrorBranch, + expected_reason: CoseErrorReason, +) { + let request = operation_request(operation); + let encoded_request = Zeroizing::new(request.encode_to_vec()); + let json_request = Zeroizing::new( + serde_json::to_string(&request).expect("generated request ProtoJSON must encode"), + ); + let binary_envelope = execute_operation_proto(&encoded_request); + let json_envelope = execute_operation_proto_json(&json_request); + assert_eq!(binary_envelope.as_slice(), json_envelope.as_slice()); + + let output = match decode_operation_output(&binary_envelope) { + Ok(output) | Err(output) => output, + }; + assert_eq!(output.status(), OperationOutputStatus::CoseError); + let error = decode_cose_error(output.bytes()).expect("structured key-family error must decode"); + let Some(error_branch) = error.error.as_ref() else { + assert!(error.error.is_some(), "structured error branch must exist"); + return; + }; + let (branch, reason) = match error_branch { + cose_error_proto::Error::Primitive(error) => { + (ErrorBranch::Primitive, error.reason.as_known()) + } + cose_error_proto::Error::Provider(error) => { + (ErrorBranch::Provider, error.reason.as_known()) + } + cose_error_proto::Error::Backend(error) => (ErrorBranch::Backend, error.reason.as_known()), + }; + assert_eq!(branch, expected_branch); + assert_eq!(reason, Some(expected_reason)); +} diff --git a/crates/cose/tests/cose_suite/key_parse_adapter_tests.rs b/crates/cose/tests/cose_suite/key_parse_adapter_tests.rs new file mode 100644 index 0000000..b4b96bc --- /dev/null +++ b/crates/cose/tests/cose_suite/key_parse_adapter_tests.rs @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use buffa::Message; +use reallyme_cose::limits::MAX_COSE_KEY_BYTES; +use reallyme_cose::wire::cose_operation_request::Operation; +use reallyme_cose::wire::{ + cose_error_proto, decode_cose_error, execute_operation_proto, execute_operation_proto_json, + CoseErrorProto, CoseErrorReason, CoseKeyBytesRequest, CoseKeyBytesResult, CoseOperationRequest, +}; +use reallyme_cose::{cose_key_from_public_bytes, cose_key_to_vec, Algorithm}; + +use crate::support::{decode_operation_output, OperationOutput, OperationOutputStatus}; + +const RFC_8032_ED25519_PUBLIC_KEY: [u8; 32] = [ + 0xd7, 0x5a, 0x98, 0x01, 0x82, 0xb1, 0x0a, 0xb7, 0xd5, 0x4b, 0xfe, 0xd3, 0xc9, 0x64, 0x07, 0x3a, + 0x0e, 0xe1, 0x72, 0xf3, 0xda, 0xa6, 0x23, 0x25, 0xaf, 0x02, 0x1a, 0x68, 0xf7, 0x07, 0x51, 0x1a, +]; + +#[derive(Clone, Copy)] +enum AdapterLane { + Protobuf, + ProtoJson, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum ErrorBranch { + Primitive, + Provider, + Backend, +} + +#[test] +fn key_parse_adapters_return_canonical_key_bytes() { + let key = cose_key_from_public_bytes(Algorithm::Ed25519, &RFC_8032_ED25519_PUBLIC_KEY) + .expect("fixture key must be valid"); + let encoded = cose_key_to_vec(&key).expect("fixture key must encode"); + + for lane in [AdapterLane::Protobuf, AdapterLane::ProtoJson] { + let output = execute(lane, encoded.to_vec()); + assert_eq!(output.status(), OperationOutputStatus::Result); + let result = CoseKeyBytesResult::decode_from_slice(output.bytes()) + .expect("key parse result must decode"); + assert_eq!(result.key_bytes, encoded.as_slice()); + } +} + +#[test] +fn key_parse_adapters_preserve_semantic_failures() { + let oversized_len = MAX_COSE_KEY_BYTES + .checked_add(1) + .expect("test length must fit usize"); + let cases = [ + (Vec::new(), CoseErrorReason::CommonCbor), + ( + vec![0_u8; oversized_len], + CoseErrorReason::CommonResourceLimitExceeded, + ), + ( + vec![0xa2, 0x01, 0x01, 0x01, 0x01], + CoseErrorReason::CommonDuplicateMapLabel, + ), + ]; + + for lane in [AdapterLane::Protobuf, AdapterLane::ProtoJson] { + for (input, expected_reason) in &cases { + assert_error( + &execute(lane, input.clone()), + ErrorBranch::Primitive, + *expected_reason, + ); + } + } +} + +#[test] +fn malformed_transport_stays_separate_from_semantic_failure() { + let protobuf = decode_envelope(&execute_operation_proto(&[0xff])); + assert_error( + &protobuf, + ErrorBranch::Primitive, + CoseErrorReason::CommonMalformedProtobuf, + ); + + let proto_json = decode_envelope(&execute_operation_proto_json("{")); + assert_error( + &proto_json, + ErrorBranch::Primitive, + CoseErrorReason::CommonMalformedJson, + ); +} + +fn execute(lane: AdapterLane, cose_key: Vec) -> OperationOutput { + let request = CoseOperationRequest { + operation: Some(Operation::KeyParse(Box::new(CoseKeyBytesRequest { + cose_key, + __buffa_unknown_fields: Default::default(), + }))), + __buffa_unknown_fields: Default::default(), + }; + + match lane { + AdapterLane::Protobuf => { + decode_envelope(&execute_operation_proto(&request.encode_to_vec())) + } + AdapterLane::ProtoJson => { + let json = serde_json::to_string(&request).expect("request ProtoJSON must encode"); + decode_envelope(&execute_operation_proto_json(&json)) + } + } +} + +fn decode_envelope(bytes: &[u8]) -> OperationOutput { + match decode_operation_output(bytes) { + Ok(output) | Err(output) => output, + } +} + +fn assert_error(output: &OperationOutput, branch: ErrorBranch, reason: CoseErrorReason) { + assert_eq!(output.status(), OperationOutputStatus::CoseError); + let error = decode_cose_error(output.bytes()).ok(); + assert!(error.is_some(), "structured error must decode"); + let Some(error) = error else { + return; + }; + assert_eq!(error_branch(&error), Some(branch)); + assert_eq!(error_reason(&error), Some(reason)); +} + +fn error_branch(error: &CoseErrorProto) -> Option { + match error.error.as_ref()? { + cose_error_proto::Error::Primitive(_) => Some(ErrorBranch::Primitive), + cose_error_proto::Error::Provider(_) => Some(ErrorBranch::Provider), + cose_error_proto::Error::Backend(_) => Some(ErrorBranch::Backend), + } +} + +fn error_reason(error: &CoseErrorProto) -> Option { + let reason = match error.error.as_ref()? { + cose_error_proto::Error::Primitive(value) => value.reason, + cose_error_proto::Error::Provider(value) => value.reason, + cose_error_proto::Error::Backend(value) => value.reason, + }; + reason.as_known() +} diff --git a/crates/cose/tests/cose_suite/key_parse_differential_tests.rs b/crates/cose/tests/cose_suite/key_parse_differential_tests.rs new file mode 100644 index 0000000..f3f0a35 --- /dev/null +++ b/crates/cose/tests/cose_suite/key_parse_differential_tests.rs @@ -0,0 +1,497 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use std::io::Cursor; + +use buffa::{EnumValue, Message}; +use ciborium::{ser::into_writer, value::Value}; +use reallyme_cose::limits::MAX_COSE_KEY_BYTES; +use reallyme_cose::wire::cose_error_proto; +use reallyme_cose::wire::cose_operation_request::Operation; +use reallyme_cose::wire::{ + cose_operation_response_v2, cose_operation_result, decode_cose_error, execute_operation_proto, + execute_operation_proto_json, CoseBackendError, CoseErrorProto, CoseErrorReason, + CoseKeyBytesRequest, CoseKeyBytesResult, CoseOperationRequest, CoseOperationResponseV2, + CoseOperationResult, CosePrimitiveError, CoseProviderError, MAX_COSE_PROTO_MESSAGE_BYTES, +}; +use reallyme_cose::{ + cose_key_from_private_bytes, cose_key_from_public_bytes, cose_key_from_slice, cose_key_to_vec, + Algorithm, CoseError, +}; +use reallyme_crypto::dispatch::generate_keypair; +use zeroize::Zeroizing; + +use crate::support::{decode_operation_output, OperationOutput, OperationOutputStatus}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum ExpectedBranch { + Primitive, + Provider, + Backend, +} + +#[derive(Clone, Copy)] +enum ExpectedNative { + Cbor, + InvalidKeyMaterial, + MissingKeyMaterial, + NonCanonicalCbor, + ResourceLimitExceeded, + UnexpectedCborTag, + DuplicateMapLabel, + UnsupportedAlgorithm, +} + +#[derive(Clone, Copy)] +struct ExpectedFailure { + native: ExpectedNative, + branch: ExpectedBranch, + reason: CoseErrorReason, +} + +#[test] +fn native_protobuf_and_proto_json_success_outputs_are_identical() { + for (fixture_index, encoded) in success_fixtures().into_iter().enumerate() { + let native = cose_key_from_slice(&encoded); + assert!( + native.is_ok(), + "native success fixture {fixture_index} failed with {:?}", + native.as_ref().err(), + ); + let Ok(native) = native else { + return; + }; + let native_bytes = cose_key_to_vec(&native).expect("native result must encode"); + let expected_envelope = expected_result_envelope(&native_bytes); + let (protobuf_envelope, json_envelope) = execute_adapters(&encoded); + + assert_eq!(protobuf_envelope.as_slice(), expected_envelope.as_slice()); + assert_eq!(json_envelope.as_slice(), expected_envelope.as_slice()); + assert_eq!(protobuf_envelope.as_slice(), json_envelope.as_slice()); + + let protobuf_result = decode_key_result(&protobuf_envelope); + let json_result = decode_key_result(&json_envelope); + assert_eq!(protobuf_result.key_bytes, native_bytes.as_slice()); + assert_eq!(json_result.key_bytes, native_bytes.as_slice()); + } +} + +#[test] +fn hostile_semantic_inputs_preserve_exact_native_and_wire_failures() { + for (fixture_index, (input, expected)) in failure_fixtures().into_iter().enumerate() { + assert_native_error( + cose_key_from_slice(&input).err(), + expected.native, + fixture_index, + ); + + let expected_envelope = expected_error_envelope(expected); + let (protobuf_envelope, json_envelope) = execute_adapters(&input); + assert_eq!(protobuf_envelope.as_slice(), expected_envelope.as_slice()); + assert_eq!(json_envelope.as_slice(), expected_envelope.as_slice()); + + assert_error(&protobuf_envelope, expected); + assert_error(&json_envelope, expected); + } +} + +#[test] +fn adapter_only_failures_do_not_enter_key_semantics() { + let missing_operation = CoseOperationRequest { + operation: None, + __buffa_unknown_fields: Default::default(), + }; + assert_error( + &execute_operation_proto(&missing_operation.encode_to_vec()), + primitive( + ExpectedNative::Cbor, + CoseErrorReason::CommonInvalidParameter, + ), + ); + + let oversized_len = MAX_COSE_PROTO_MESSAGE_BYTES + .checked_add(1) + .expect("protobuf test length must fit usize"); + let oversized = vec![0_u8; oversized_len]; + assert_error( + &execute_operation_proto(&oversized), + primitive( + ExpectedNative::ResourceLimitExceeded, + CoseErrorReason::CommonResourceLimitExceeded, + ), + ); +} + +#[test] +fn concurrent_parses_are_deterministic_and_independently_owned() { + let encoded = success_fixtures() + .into_iter() + .next() + .expect("at least one fixture must exist"); + let expected = encoded.to_vec(); + + let mut outputs = std::thread::scope(|scope| { + let handles: Vec<_> = (0..8) + .map(|_| { + let input = encoded.as_slice(); + scope.spawn(move || { + let key = cose_key_from_slice(input).expect("concurrent parse must succeed"); + cose_key_to_vec(&key).expect("concurrent result must encode") + }) + }) + .collect(); + handles + .into_iter() + .map(|handle| handle.join().expect("concurrent parse must not panic")) + .collect::>() + }); + + assert!(outputs + .iter() + .all(|output| output.as_slice() == expected.as_slice())); + outputs[0][0] ^= 0x01; + assert!(outputs[1..] + .iter() + .all(|output| output.as_slice() == expected.as_slice())); +} + +fn success_fixtures() -> Vec>> { + let (classical_public, classical_private) = + generate_keypair(Algorithm::Ed25519).expect("Ed25519 fixture generation must succeed"); + let (pq_public, pq_private) = + generate_keypair(Algorithm::MlKem768).expect("ML-KEM-768 fixture generation must succeed"); + + vec![ + encode_public(Algorithm::Ed25519, &classical_public), + encode_ed25519_private(&classical_private, &classical_public), + encode_public(Algorithm::MlKem768, &pq_public), + encode_private(Algorithm::MlKem768, &pq_private, &pq_public), + ] +} + +fn failure_fixtures() -> Vec<(Zeroizing>, ExpectedFailure)> { + let mut cases = vec![ + ( + Zeroizing::new(vec![0xff]), + primitive(ExpectedNative::Cbor, CoseErrorReason::CommonCbor), + ), + ( + Zeroizing::new(vec![0xbf, 0xff]), + primitive( + ExpectedNative::NonCanonicalCbor, + CoseErrorReason::CommonNonCanonicalCbor, + ), + ), + ( + Zeroizing::new(vec![0xa1, 0x18, 0x01, 0x01]), + primitive( + ExpectedNative::NonCanonicalCbor, + CoseErrorReason::CommonNonCanonicalCbor, + ), + ), + ( + Zeroizing::new(vec![0xa2, 0x01, 0x01, 0x01, 0x01]), + primitive( + ExpectedNative::DuplicateMapLabel, + CoseErrorReason::CommonDuplicateMapLabel, + ), + ), + ( + Zeroizing::new(vec![0xc1, 0xa0]), + primitive( + ExpectedNative::UnexpectedCborTag, + CoseErrorReason::CommonUnexpectedCborTag, + ), + ), + ( + Zeroizing::new(vec![0xa0, 0x00]), + primitive(ExpectedNative::Cbor, CoseErrorReason::CommonCbor), + ), + ( + deeply_nested_input(), + primitive( + ExpectedNative::ResourceLimitExceeded, + CoseErrorReason::CommonResourceLimitExceeded, + ), + ), + ( + Zeroizing::new(vec![0xb9, 0x04, 0x01]), + primitive( + ExpectedNative::ResourceLimitExceeded, + CoseErrorReason::CommonResourceLimitExceeded, + ), + ), + ( + Zeroizing::new(vec![ + 0_u8; + MAX_COSE_KEY_BYTES + .checked_add(1) + .expect("COSE_Key test length must fit usize") + ]), + primitive( + ExpectedNative::ResourceLimitExceeded, + CoseErrorReason::CommonResourceLimitExceeded, + ), + ), + ( + missing_key_material_input(), + primitive( + ExpectedNative::MissingKeyMaterial, + CoseErrorReason::KeyMissingKeyMaterial, + ), + ), + ( + invalid_key_length_input(), + primitive( + ExpectedNative::InvalidKeyMaterial, + CoseErrorReason::KeyInvalidKeyMaterial, + ), + ), + ( + unsupported_algorithm_input(), + ExpectedFailure { + native: ExpectedNative::UnsupportedAlgorithm, + branch: ExpectedBranch::Provider, + reason: CoseErrorReason::CommonUnsupportedAlgorithm, + }, + ), + ]; + cases.push(( + mismatched_private_key_input(), + primitive( + ExpectedNative::InvalidKeyMaterial, + CoseErrorReason::KeyInvalidKeyMaterial, + ), + )); + cases +} + +fn primitive(native: ExpectedNative, reason: CoseErrorReason) -> ExpectedFailure { + ExpectedFailure { + native, + branch: ExpectedBranch::Primitive, + reason, + } +} + +fn encode_public(algorithm: Algorithm, public_key: &[u8]) -> Zeroizing> { + let key = cose_key_from_public_bytes(algorithm, public_key) + .expect("public fixture COSE_Key must build"); + cose_key_to_vec(&key).expect("public fixture COSE_Key must encode") +} + +fn encode_private( + algorithm: Algorithm, + private_key: &[u8], + public_key: &[u8], +) -> Zeroizing> { + let key = cose_key_from_private_bytes(algorithm, private_key, Some(public_key)) + .expect("private fixture COSE_Key must build"); + cose_key_to_vec(&key).expect("private fixture COSE_Key must encode") +} + +fn encode_ed25519_private(private_key: &[u8], public_key: &[u8]) -> Zeroizing> { + // Assemble kty, alg, crv, x, and d in RFC 8949 bytewise map-key order so + // this parse fixture is independent of the separate construction route. + let mut encoded = Zeroizing::new(vec![ + 0xa5, 0x01, 0x01, 0x03, 0x32, 0x20, 0x06, 0x21, 0x58, 0x20, + ]); + encoded.extend_from_slice(public_key); + encoded.extend_from_slice(&[0x23, 0x58, 0x20]); + encoded.extend_from_slice(private_key); + encoded +} + +fn deeply_nested_input() -> Zeroizing> { + let mut value = Value::Null; + for _ in 0..40 { + value = Value::Array(vec![value]); + } + let mut encoded = Zeroizing::new(Vec::new()); + into_writer(&value, Cursor::new(&mut *encoded)).expect("nested fixture must encode"); + encoded +} + +fn missing_key_material_input() -> Zeroizing> { + Zeroizing::new(vec![0xa3, 0x01, 0x01, 0x03, 0x32, 0x20, 0x06]) +} + +fn invalid_key_length_input() -> Zeroizing> { + let mut encoded = Zeroizing::new(vec![ + 0xa4, 0x01, 0x01, 0x03, 0x32, 0x20, 0x06, 0x21, 0x58, 0x1f, + ]); + encoded.extend_from_slice(&[0_u8; 31]); + encoded +} + +fn unsupported_algorithm_input() -> Zeroizing> { + let mut encoded = Zeroizing::new(vec![ + 0xa4, 0x01, 0x01, 0x03, 0x26, 0x20, 0x06, 0x21, 0x58, 0x20, + ]); + encoded.extend_from_slice(&[0_u8; 32]); + encoded +} + +fn mismatched_private_key_input() -> Zeroizing> { + let (public_key, private_key) = + generate_keypair(Algorithm::Ed25519).expect("first Ed25519 fixture must generate"); + let (_, other_private_key) = + generate_keypair(Algorithm::Ed25519).expect("second Ed25519 fixture must generate"); + let mut encoded = encode_ed25519_private(&private_key, &public_key); + let offset = encoded + .windows(private_key.len()) + .position(|window| window == private_key.as_slice()) + .expect("private key bytes must occur in encoded fixture"); + let end = offset + .checked_add(other_private_key.len()) + .expect("fixture private-key offset must not overflow"); + encoded[offset..end].copy_from_slice(&other_private_key); + encoded +} + +fn execute_adapters(input: &[u8]) -> (Zeroizing>, Zeroizing>) { + let request = operation_request(input); + let protobuf = execute_operation_proto(&request.encode_to_vec()); + let json = Zeroizing::new( + serde_json::to_string(&request).expect("generated request ProtoJSON must encode"), + ); + let proto_json = execute_operation_proto_json(&json); + (protobuf, proto_json) +} + +fn operation_request(input: &[u8]) -> CoseOperationRequest { + CoseOperationRequest { + operation: Some(Operation::KeyParse(Box::new(CoseKeyBytesRequest { + cose_key: input.to_vec(), + __buffa_unknown_fields: Default::default(), + }))), + __buffa_unknown_fields: Default::default(), + } +} + +fn expected_result_envelope(key_bytes: &[u8]) -> Zeroizing> { + let result = CoseKeyBytesResult { + key_bytes: key_bytes.to_vec(), + __buffa_unknown_fields: Default::default(), + }; + Zeroizing::new( + CoseOperationResponseV2 { + outcome: Some(cose_operation_response_v2::Outcome::Result(Box::new( + CoseOperationResult { + result: Some(cose_operation_result::Result::KeyParse(Box::new(result))), + __buffa_unknown_fields: Default::default(), + }, + ))), + __buffa_unknown_fields: Default::default(), + } + .encode_to_vec(), + ) +} + +fn expected_error_envelope(expected: ExpectedFailure) -> Zeroizing> { + let reason = EnumValue::from(expected.reason); + let error = CoseErrorProto { + error: Some(match expected.branch { + ExpectedBranch::Primitive => { + cose_error_proto::Error::Primitive(Box::new(CosePrimitiveError { + reason, + __buffa_unknown_fields: Default::default(), + })) + } + ExpectedBranch::Provider => { + cose_error_proto::Error::Provider(Box::new(CoseProviderError { + reason, + __buffa_unknown_fields: Default::default(), + })) + } + ExpectedBranch::Backend => { + cose_error_proto::Error::Backend(Box::new(CoseBackendError { + reason, + __buffa_unknown_fields: Default::default(), + })) + } + }), + __buffa_unknown_fields: Default::default(), + }; + Zeroizing::new( + CoseOperationResponseV2 { + outcome: Some(cose_operation_response_v2::Outcome::Error(Box::new(error))), + __buffa_unknown_fields: Default::default(), + } + .encode_to_vec(), + ) +} + +fn decode_key_result(envelope: &[u8]) -> CoseKeyBytesResult { + let output = decode_output(envelope); + assert_eq!(output.status(), OperationOutputStatus::Result); + CoseKeyBytesResult::decode_from_slice(output.bytes()).expect("key result must decode") +} + +fn assert_error(envelope: &[u8], expected: ExpectedFailure) { + let output = decode_output(envelope); + assert_eq!(output.status(), OperationOutputStatus::CoseError); + let error = decode_cose_error(output.bytes()).ok(); + assert!(error.is_some(), "structured error must decode"); + let Some(error) = error else { + return; + }; + let error_branch = error.error.as_ref(); + assert!(error_branch.is_some(), "error branch must exist"); + let Some(error_branch) = error_branch else { + return; + }; + let (branch, reason) = match error_branch { + cose_error_proto::Error::Primitive(value) => (ExpectedBranch::Primitive, value.reason), + cose_error_proto::Error::Provider(value) => (ExpectedBranch::Provider, value.reason), + cose_error_proto::Error::Backend(value) => (ExpectedBranch::Backend, value.reason), + }; + assert_eq!(branch, expected.branch); + assert_eq!(reason.as_known(), Some(expected.reason)); +} + +fn assert_native_error(actual: Option, expected: ExpectedNative, fixture_index: usize) { + let matches = matches!( + (actual, expected), + (Some(CoseError::Cbor), ExpectedNative::Cbor) + | ( + Some(CoseError::InvalidKeyMaterial), + ExpectedNative::InvalidKeyMaterial + ) + | ( + Some(CoseError::MissingKeyMaterial), + ExpectedNative::MissingKeyMaterial + ) + | ( + Some(CoseError::NonCanonicalCbor), + ExpectedNative::NonCanonicalCbor + ) + | ( + Some(CoseError::ResourceLimitExceeded), + ExpectedNative::ResourceLimitExceeded + ) + | ( + Some(CoseError::UnexpectedCborTag), + ExpectedNative::UnexpectedCborTag + ) + | ( + Some(CoseError::DuplicateMapLabel), + ExpectedNative::DuplicateMapLabel + ) + | ( + Some(CoseError::UnsupportedAlgorithm), + ExpectedNative::UnsupportedAlgorithm + ) + ); + assert!( + matches, + "native failure fixture {fixture_index} did not match the expected typed variant" + ); +} + +fn decode_output(envelope: &[u8]) -> OperationOutput { + match decode_operation_output(envelope) { + Ok(output) | Err(output) => output, + } +} diff --git a/tests/cose_suite/kid_derive_tests.rs b/crates/cose/tests/cose_suite/kid_derive_tests.rs similarity index 100% rename from tests/cose_suite/kid_derive_tests.rs rename to crates/cose/tests/cose_suite/kid_derive_tests.rs diff --git a/tests/cose_suite/kid_tests.rs b/crates/cose/tests/cose_suite/kid_tests.rs similarity index 72% rename from tests/cose_suite/kid_tests.rs rename to crates/cose/tests/cose_suite/kid_tests.rs index 3d3de99..556cb23 100644 --- a/tests/cose_suite/kid_tests.rs +++ b/crates/cose/tests/cose_suite/kid_tests.rs @@ -4,19 +4,14 @@ // SPDX-License-Identifier: Apache-2.0 use reallyme_cose::{ - cose_sign1, cose_verify1, validate_cose_sign1_policy, Algorithm, CoseError, CosePolicy, + cose_sign1, cose_verify1, cose_verify1_with_policy, Algorithm, CoseError, CosePolicy, }; -use coset::{CborSerializable, CoseSign1}; - use super::support::{gen_ed25519, gen_p384, gen_p521, test_kid, TestKey}; #[test] fn policy_rejects_missing_kid() { - let policy = CosePolicy { - require_kid: true, - ..Default::default() - }; + let policy = CosePolicy::new().with_require_kid(true); let k = gen_ed25519(); @@ -25,9 +20,8 @@ fn policy_rejects_missing_kid() { ) .unwrap(); - let cose = CoseSign1::from_slice(&cose_bytes).unwrap(); - - assert!(validate_cose_sign1_policy(&cose, &policy).is_err()); + let result = cose_verify1_with_policy(&cose_bytes, &policy, |_, _| Some(k.public.clone())); + assert!(matches!(result, Err(CoseError::MissingKid))); } #[test] @@ -51,7 +45,7 @@ fn verify_uses_kid_for_key_selection() { let cose_bytes = cose_sign1(k.alg, b"hello", &k.private, Some(kid)).unwrap(); - let resolver = |k_: &[u8]| { + let resolver = |_, k_: &[u8]| { if k_ == kid { Some(k.public.clone()) } else { @@ -61,7 +55,7 @@ fn verify_uses_kid_for_key_selection() { let payload = cose_verify1(&cose_bytes, resolver).unwrap(); - assert_eq!(payload, b"hello"); + assert_eq!(payload.as_slice(), b"hello"); } #[test] @@ -70,7 +64,7 @@ fn verify_fails_with_unknown_kid() { let cose_bytes = cose_sign1(k.alg, b"hello", &k.private, Some(b"unknown")).unwrap(); - let resolver = |_k: &[u8]| None; + let resolver = |_, _k: &[u8]| None; assert_eq!( cose_verify1(&cose_bytes, resolver).unwrap_err(), @@ -79,16 +73,15 @@ fn verify_fails_with_unknown_kid() { } fn policy_allows_algorithm(k: &TestKey, alg: Algorithm) { - let policy = CosePolicy { - allowed_algs: vec![alg], - ..Default::default() - }; + let policy = CosePolicy::new().allow_algorithm(alg); let cose_bytes = cose_sign1(k.alg, b"hello", &k.private, Some(test_kid())).unwrap(); - let cose = CoseSign1::from_slice(&cose_bytes).unwrap(); - - validate_cose_sign1_policy(&cose, &policy).unwrap(); + let payload = cose_verify1_with_policy(&cose_bytes, &policy, |_, kid| { + (kid == test_kid()).then(|| k.public.clone()) + }) + .unwrap(); + assert_eq!(payload.payload.as_slice(), b"hello"); } #[test] @@ -97,7 +90,7 @@ fn verify_allows_empty_kid_when_resolver_accepts_default_key() { let cose_bytes = cose_sign1(k.alg, b"hello", &k.private, None).unwrap(); - let resolver = |kid: &[u8]| { + let resolver = |_, kid: &[u8]| { if kid.is_empty() { Some(k.public.clone()) } else { @@ -107,7 +100,7 @@ fn verify_allows_empty_kid_when_resolver_accepts_default_key() { let payload = cose_verify1(&cose_bytes, resolver).unwrap(); - assert_eq!(payload, b"hello"); + assert_eq!(payload.as_slice(), b"hello"); } #[test] @@ -116,7 +109,7 @@ fn verify_fails_with_missing_kid_when_default_key_is_not_resolved() { let cose_bytes = cose_sign1(k.alg, b"hello", &k.private, None).unwrap(); - let resolver = |_kid: &[u8]| None; + let resolver = |_, _kid: &[u8]| None; assert_eq!( cose_verify1(&cose_bytes, resolver).unwrap_err(), diff --git a/tests/cose_suite/malicious_cbor_tests.rs b/crates/cose/tests/cose_suite/malicious_cbor_tests.rs similarity index 54% rename from tests/cose_suite/malicious_cbor_tests.rs rename to crates/cose/tests/cose_suite/malicious_cbor_tests.rs index 46abda4..fe04505 100644 --- a/tests/cose_suite/malicious_cbor_tests.rs +++ b/crates/cose/tests/cose_suite/malicious_cbor_tests.rs @@ -6,7 +6,7 @@ use std::io::Cursor; use ciborium::{ser::into_writer, value::Value}; -use coset::{iana, CborSerializable, CoseSign1, RegisteredLabelWithPrivate}; +use coset::{iana, CborSerializable, ContentType, CoseSign1, RegisteredLabelWithPrivate}; use reallyme_cose::{ cose_key_from_slice, cose_sign1, cose_verify1, limits::MAX_COSE_KEY_BYTES, limits::MAX_COSE_SIGN1_BYTES, CoseError, @@ -18,7 +18,7 @@ use crate::support::{gen_ed25519, sample_payload, test_kid}; fn cose_sign1_rejects_oversized_input_before_cbor_parse() { let oversized = vec![0_u8; MAX_COSE_SIGN1_BYTES + 1]; - let err = cose_verify1(&oversized, |_| None).expect_err("oversized input must be rejected"); + let err = cose_verify1(&oversized, |_, _| None).expect_err("oversized input must be rejected"); assert_eq!(err, CoseError::ResourceLimitExceeded); } @@ -27,7 +27,7 @@ fn cose_sign1_rejects_oversized_input_before_cbor_parse() { fn cose_sign1_rejects_malformed_cbor() { let malformed = [0xff_u8]; - let err = cose_verify1(&malformed, |_| None).expect_err("malformed CBOR must fail closed"); + let err = cose_verify1(&malformed, |_, _| None).expect_err("malformed CBOR must fail closed"); assert_eq!(err, CoseError::Cbor); } @@ -36,7 +36,7 @@ fn cose_sign1_rejects_malformed_cbor() { fn cose_sign1_rejects_indefinite_forms() { let indefinite_protected_bstr = [0x84, 0x5f, 0xff, 0xa0, 0xf6, 0x40]; - let err = cose_verify1(&indefinite_protected_bstr, |_| None) + let err = cose_verify1(&indefinite_protected_bstr, |_, _| None) .expect_err("indefinite-length CBOR must be rejected"); assert_eq!(err, CoseError::NonCanonicalCbor); @@ -46,7 +46,7 @@ fn cose_sign1_rejects_indefinite_forms() { fn cose_sign1_rejects_unexpected_top_level_tag() { let tagged_empty_array = [0xc1, 0x80]; - let err = cose_verify1(&tagged_empty_array, |_| None) + let err = cose_verify1(&tagged_empty_array, |_, _| None) .expect_err("unexpected top-level tags must be rejected"); assert_eq!(err, CoseError::UnexpectedCborTag); @@ -65,7 +65,7 @@ fn cose_sign1_rejects_nested_tags() { ); let encoded = encode_value(&value); - let err = cose_verify1(&encoded, |_| None).expect_err("nested tags must be rejected"); + let err = cose_verify1(&encoded, |_, _| None).expect_err("nested tags must be rejected"); assert_eq!(err, CoseError::UnexpectedCborTag); } @@ -85,7 +85,7 @@ fn cose_sign1_rejects_critical_protected_header() { cose.protected.original_data = None; let mutated = cose.to_vec().expect("mutated COSE must encode"); - let err = cose_verify1(&mutated, |_| Some(key.public.clone())) + let err = cose_verify1(&mutated, |_, _| Some(key.public.clone())) .expect_err("critical headers must be rejected before crypto"); assert_eq!(err, CoseError::UnsupportedCriticalHeader); @@ -100,17 +100,52 @@ fn cose_sign1_rejects_integrity_fields_in_unprotected_header() { cose.unprotected.key_id = b"shadow-kid".to_vec(); let mutated = cose.to_vec().expect("mutated COSE must encode"); - let err = cose_verify1(&mutated, |_| Some(key.public.clone())) + let err = cose_verify1(&mutated, |_, _| Some(key.public.clone())) .expect_err("unprotected kid must be rejected before crypto"); assert_eq!(err, CoseError::UnprotectedHeaderNotAllowed); } +#[test] +fn cose_sign1_rejects_authenticated_header_semantics_not_exposed_by_the_api() { + let key = gen_ed25519(); + let cose_bytes = cose_sign1(key.alg, &sample_payload(), &key.private, Some(test_kid())) + .expect("fixture signing must succeed"); + let mut cose = CoseSign1::from_slice(&cose_bytes).expect("fixture COSE must decode"); + cose.protected.header.content_type = + Some(ContentType::Text("application/reallyme-test".to_owned())); + cose.protected.original_data = None; + let mutated = cose.to_vec().expect("mutated COSE must encode"); + + let err = cose_verify1(&mutated, |_, _| Some(key.public.clone())) + .expect_err("unrepresented authenticated header semantics must fail closed"); + + assert_eq!(err, CoseError::InvalidFormat); +} + +#[test] +fn cose_sign1_rejects_invalid_signature_type_after_owning_payload() { + let value = Value::Array(vec![ + Value::Bytes(Vec::new()), + Value::Map(Vec::new()), + Value::Bytes(b"sensitive rejected payload".to_vec()), + Value::Text("not-a-signature".to_owned()), + ]); + let encoded = encode_value(&value); + + let err = cose_verify1(&encoded, |_, _| None) + .expect_err("a non-byte-string signature must fail closed"); + + assert_eq!(err, CoseError::InvalidSignatureEncoding); +} + #[test] fn cose_key_rejects_oversized_input_before_cbor_parse() { let oversized = vec![0_u8; MAX_COSE_KEY_BYTES + 1]; - let err = cose_key_from_slice(&oversized).expect_err("oversized key must be rejected"); + let err = cose_key_from_slice(&oversized) + .err() + .expect("oversized key must be rejected"); assert_eq!(err, CoseError::ResourceLimitExceeded); } @@ -120,7 +155,37 @@ fn cose_key_rejects_indefinite_forms() { let indefinite_map = [0xbf, 0xff]; let err = cose_key_from_slice(&indefinite_map) - .expect_err("indefinite-length COSE_Key CBOR must be rejected"); + .err() + .expect("indefinite-length COSE_Key CBOR must be rejected"); + + assert_eq!(err, CoseError::NonCanonicalCbor); +} + +#[test] +fn cose_key_rejects_non_deterministic_map_order() { + let value = Value::Map(vec![ + ( + Value::Integer((iana::KeyParameter::Alg as i64).into()), + Value::Integer((iana::Algorithm::Ed25519 as i64).into()), + ), + ( + Value::Integer((iana::KeyParameter::Kty as i64).into()), + Value::Integer((iana::KeyType::OKP as i64).into()), + ), + ( + Value::Integer((iana::OkpKeyParameter::Crv as i64).into()), + Value::Integer((iana::EllipticCurve::Ed25519 as i64).into()), + ), + ( + Value::Integer((iana::OkpKeyParameter::X as i64).into()), + Value::Bytes(vec![7_u8; 32]), + ), + ]); + let encoded = encode_value(&value); + + let err = cose_key_from_slice(&encoded) + .err() + .expect("non-deterministic COSE_Key map ordering must fail"); assert_eq!(err, CoseError::NonCanonicalCbor); } @@ -129,7 +194,9 @@ fn cose_key_rejects_indefinite_forms() { fn cose_key_rejects_unexpected_tags() { let tagged_empty_map = [0xc1, 0xa0]; - let err = cose_key_from_slice(&tagged_empty_map).expect_err("COSE_Key tags must be rejected"); + let err = cose_key_from_slice(&tagged_empty_map) + .err() + .expect("COSE_Key tags must be rejected"); assert_eq!(err, CoseError::UnexpectedCborTag); } @@ -142,7 +209,9 @@ fn cose_key_rejects_excessive_nesting_before_shape_decode() { } let encoded = encode_value(&value); - let err = cose_key_from_slice(&encoded).expect_err("deep nesting must be rejected"); + let err = cose_key_from_slice(&encoded) + .err() + .expect("deep nesting must be rejected"); assert_eq!(err, CoseError::ResourceLimitExceeded); } diff --git a/crates/cose/tests/cose_suite/ml_kem_encrypt_tests.rs b/crates/cose/tests/cose_suite/ml_kem_encrypt_tests.rs new file mode 100644 index 0000000..e524782 --- /dev/null +++ b/crates/cose/tests/cose_suite/ml_kem_encrypt_tests.rs @@ -0,0 +1,411 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use coset::{ContentType, CoseEncrypt, TaggedCborSerializable}; +use reallyme_cose::{ + cose_decrypt_ml_kem, cose_decrypt_ml_kem_with_external_aad, cose_encrypt_ml_kem_direct, + cose_encrypt_ml_kem_direct_with_external_aad, cose_encrypt_ml_kem_key_wrap, + cose_key_from_public_bytes, derive_kid_from_cose_key_public, CoseContentEncryptionAlgorithm, + CoseError, CoseMlKemAlgorithm, CoseMlKemDecryptRequest, CoseMlKemEncryptRequest, CoseMlKemMode, +}; +use reallyme_crypto::core::Algorithm; +use zeroize::Zeroizing; + +const PLAINTEXT: &[u8] = b"ReallyMe ML-KEM COSE profile test plaintext"; +const EXTERNAL_AAD: &[u8] = b"authenticated transport metadata"; +#[test] +fn every_direct_kem_and_content_algorithm_round_trips() { + for kem_algorithm in kem_algorithms() { + let (public_key, private_key, kid) = keypair(kem_algorithm); + for content_algorithm in content_algorithms() { + let request = encrypt_request( + kem_algorithm, + content_algorithm, + &public_key, + &kid, + PLAINTEXT, + ); + let encoded = cose_encrypt_ml_kem_direct(&request).expect("direct encryption"); + let decrypted = cose_decrypt_ml_kem(&decrypt_request(&encoded, &private_key, &kid)) + .expect("direct decryption"); + + assert_eq!(decrypted.plaintext.as_slice(), PLAINTEXT); + assert_eq!(decrypted.kem_algorithm, kem_algorithm); + assert_eq!(decrypted.content_algorithm, content_algorithm); + assert_eq!(decrypted.mode, CoseMlKemMode::Direct); + assert_eq!(decrypted.kid.as_slice(), kid.as_slice()); + + let cose = CoseEncrypt::from_tagged_slice(&encoded).expect("tagged COSE_Encrypt"); + assert!(cose.protected.header.alg.is_some()); + assert!(cose.unprotected.alg.is_none()); + let recipient = cose.recipients.first().expect("one recipient"); + assert!(recipient.protected.header.alg.is_some()); + assert!(recipient.unprotected.alg.is_none()); + assert_eq!(recipient.protected.header.key_id.as_slice(), kid.as_slice()); + assert!(recipient.unprotected.key_id.is_empty()); + assert!(recipient.ciphertext.is_none()); + } + } +} + +#[test] +fn every_wrapped_kem_and_content_algorithm_round_trips() { + for kem_algorithm in kem_algorithms() { + let (public_key, private_key, kid) = keypair(kem_algorithm); + for content_algorithm in content_algorithms() { + let request = encrypt_request( + kem_algorithm, + content_algorithm, + &public_key, + &kid, + PLAINTEXT, + ); + let encoded = cose_encrypt_ml_kem_key_wrap(&request).expect("wrapped encryption"); + let decrypted = cose_decrypt_ml_kem(&decrypt_request(&encoded, &private_key, &kid)) + .expect("wrapped decryption"); + + assert_eq!(decrypted.plaintext.as_slice(), PLAINTEXT); + assert_eq!(decrypted.kem_algorithm, kem_algorithm); + assert_eq!(decrypted.content_algorithm, content_algorithm); + assert_eq!(decrypted.mode, CoseMlKemMode::KeyWrap); + assert_eq!(decrypted.kid.as_slice(), kid.as_slice()); + + let cose = CoseEncrypt::from_tagged_slice(&encoded).expect("tagged COSE_Encrypt"); + let wrapped = cose + .recipients + .first() + .and_then(|recipient| recipient.ciphertext.as_ref()) + .expect("wrapped CEK"); + assert_eq!(wrapped.len(), content_key_length(content_algorithm) + 8); + } + } +} + +#[test] +fn external_aad_and_supp_priv_info_are_required_to_match() { + let (public_key, private_key, kid) = keypair(CoseMlKemAlgorithm::MlKem768); + let supp_priv_info = b"mutually known private context"; + let request = encrypt_request( + CoseMlKemAlgorithm::MlKem768, + CoseContentEncryptionAlgorithm::Aes192Gcm, + &public_key, + &kid, + PLAINTEXT, + ) + .with_supp_priv_info(Some(supp_priv_info)); + let encoded = cose_encrypt_ml_kem_direct_with_external_aad(&request, EXTERNAL_AAD) + .expect("encryption with context"); + + let decrypt = + decrypt_request(&encoded, &private_key, &kid).with_supp_priv_info(Some(supp_priv_info)); + let decrypted = + cose_decrypt_ml_kem_with_external_aad(&decrypt, EXTERNAL_AAD).expect("matching context"); + assert_eq!(decrypted.plaintext.as_slice(), PLAINTEXT); + + assert_eq!( + cose_decrypt_ml_kem_with_external_aad(&decrypt, b"wrong aad").map(|_| ()), + Err(CoseError::AuthenticationFailed), + ); + + let decrypt = decrypt.with_supp_priv_info(Some(b"wrong private context")); + assert_eq!( + cose_decrypt_ml_kem_with_external_aad(&decrypt, EXTERNAL_AAD).map(|_| ()), + Err(CoseError::AuthenticationFailed), + ); +} + +#[test] +fn wrong_kid_fails_before_private_key_use() { + let (public_key, private_key, kid) = keypair(CoseMlKemAlgorithm::MlKem512); + let request = encrypt_request( + CoseMlKemAlgorithm::MlKem512, + CoseContentEncryptionAlgorithm::Aes128Gcm, + &public_key, + &kid, + PLAINTEXT, + ); + let encoded = cose_encrypt_ml_kem_direct(&request).expect("direct encryption"); + let mut wrong_kid = kid; + wrong_kid[0] ^= 0x80; + let decrypt = decrypt_request(&encoded, &private_key, &wrong_kid); + + assert_eq!( + cose_decrypt_ml_kem(&decrypt).map(|_| ()), + Err(CoseError::KidMismatch), + ); +} + +#[test] +fn malformed_recipient_kid_length_is_rejected_as_structure() { + let (public_key, private_key, kid) = keypair(CoseMlKemAlgorithm::MlKem512); + let request = encrypt_request( + CoseMlKemAlgorithm::MlKem512, + CoseContentEncryptionAlgorithm::Aes128Gcm, + &public_key, + &kid, + PLAINTEXT, + ); + let encoded = cose_encrypt_ml_kem_direct(&request).expect("direct encryption"); + let mut cose = CoseEncrypt::from_tagged_slice(&encoded).expect("tagged COSE_Encrypt"); + cose.recipients + .first_mut() + .expect("recipient") + .protected + .header + .key_id + .truncate(31); + cose.recipients + .first_mut() + .expect("recipient") + .protected + .original_data = None; + let malformed = cose.to_tagged_vec().expect("encode malformed object"); + + assert_eq!( + cose_decrypt_ml_kem(&decrypt_request(&malformed, &private_key, &kid)).map(|_| ()), + Err(CoseError::InvalidRecipient), + ); +} + +#[test] +fn encryption_rejects_a_kid_that_does_not_identify_the_public_key() { + let (public_key, _, mut kid) = keypair(CoseMlKemAlgorithm::MlKem512); + kid[0] ^= 0x80; + let request = encrypt_request( + CoseMlKemAlgorithm::MlKem512, + CoseContentEncryptionAlgorithm::Aes128Gcm, + &public_key, + &kid, + PLAINTEXT, + ); + + assert_eq!( + cose_encrypt_ml_kem_direct(&request).map(|_| ()), + Err(CoseError::KidMismatch), + ); +} + +#[test] +fn encryption_rejects_malformed_public_key_encoding_as_key_material() { + let (mut malformed_public_key, _, _) = keypair(CoseMlKemAlgorithm::MlKem512); + malformed_public_key.fill(0xff); + let request = encrypt_request( + CoseMlKemAlgorithm::MlKem512, + CoseContentEncryptionAlgorithm::Aes128Gcm, + &malformed_public_key, + &[0u8; 32], + PLAINTEXT, + ); + + assert_eq!( + cose_encrypt_ml_kem_direct(&request).map(|_| ()), + Err(CoseError::InvalidKeyMaterial), + ); +} + +#[test] +fn decryption_rejects_a_private_key_that_does_not_own_the_protected_kid() { + let (public_key, _, kid) = keypair(CoseMlKemAlgorithm::MlKem768); + let (_, wrong_private_key, _) = keypair(CoseMlKemAlgorithm::MlKem768); + let request = encrypt_request( + CoseMlKemAlgorithm::MlKem768, + CoseContentEncryptionAlgorithm::Aes192Gcm, + &public_key, + &kid, + PLAINTEXT, + ); + let encoded = cose_encrypt_ml_kem_direct(&request).expect("direct encryption"); + + assert_eq!( + cose_decrypt_ml_kem(&decrypt_request(&encoded, &wrong_private_key, &kid)).map(|_| ()), + Err(CoseError::KidMismatch), + ); +} + +#[test] +fn wrapped_cek_tampering_is_not_collapsed_into_content_authentication_failure() { + let (public_key, private_key, kid) = keypair(CoseMlKemAlgorithm::MlKem1024); + let request = encrypt_request( + CoseMlKemAlgorithm::MlKem1024, + CoseContentEncryptionAlgorithm::Aes256Gcm, + &public_key, + &kid, + PLAINTEXT, + ); + let encoded = cose_encrypt_ml_kem_key_wrap(&request).expect("wrapped encryption"); + let mut cose = CoseEncrypt::from_tagged_slice(&encoded).expect("tagged COSE_Encrypt"); + let wrapped = cose + .recipients + .first_mut() + .and_then(|recipient| recipient.ciphertext.as_mut()) + .expect("wrapped CEK"); + wrapped[0] ^= 0x80; + let tampered = cose.to_tagged_vec().expect("encode tampered object"); + + assert_eq!( + cose_decrypt_ml_kem(&decrypt_request(&tampered, &private_key, &kid)).map(|_| ()), + Err(CoseError::KeyUnwrapFailed), + ); +} + +#[test] +fn direct_profile_rejects_a_recipient_ciphertext() { + let (public_key, private_key, kid) = keypair(CoseMlKemAlgorithm::MlKem512); + let request = encrypt_request( + CoseMlKemAlgorithm::MlKem512, + CoseContentEncryptionAlgorithm::Aes128Gcm, + &public_key, + &kid, + PLAINTEXT, + ); + let encoded = cose_encrypt_ml_kem_direct(&request).expect("direct encryption"); + let mut cose = CoseEncrypt::from_tagged_slice(&encoded).expect("tagged COSE_Encrypt"); + cose.recipients.first_mut().expect("recipient").ciphertext = Some(vec![0u8; 24]); + let malformed = cose.to_tagged_vec().expect("encode malformed object"); + + assert_eq!( + cose_decrypt_ml_kem(&decrypt_request(&malformed, &private_key, &kid)).map(|_| ()), + Err(CoseError::InvalidRecipient), + ); +} + +#[test] +fn profile_rejects_unsupported_body_header_semantics() { + let (public_key, private_key, kid) = keypair(CoseMlKemAlgorithm::MlKem512); + let request = encrypt_request( + CoseMlKemAlgorithm::MlKem512, + CoseContentEncryptionAlgorithm::Aes128Gcm, + &public_key, + &kid, + PLAINTEXT, + ); + let encoded = cose_encrypt_ml_kem_direct(&request).expect("direct encryption"); + let mut cose = CoseEncrypt::from_tagged_slice(&encoded).expect("tagged COSE_Encrypt"); + cose.unprotected.content_type = Some(ContentType::Text("application/example".to_owned())); + let malformed = cose.to_tagged_vec().expect("encode unsupported header"); + + assert_eq!( + cose_decrypt_ml_kem(&decrypt_request(&malformed, &private_key, &kid)).map(|_| ()), + Err(CoseError::InvalidFormat), + ); +} + +#[test] +fn profile_rejects_unsupported_recipient_header_semantics() { + let (public_key, private_key, kid) = keypair(CoseMlKemAlgorithm::MlKem512); + let request = encrypt_request( + CoseMlKemAlgorithm::MlKem512, + CoseContentEncryptionAlgorithm::Aes128Gcm, + &public_key, + &kid, + PLAINTEXT, + ); + let encoded = cose_encrypt_ml_kem_direct(&request).expect("direct encryption"); + let mut cose = CoseEncrypt::from_tagged_slice(&encoded).expect("tagged COSE_Encrypt"); + let recipient = cose.recipients.first_mut().expect("recipient"); + recipient.protected.original_data = None; + recipient.protected.header.content_type = + Some(ContentType::Text("application/example".to_owned())); + let malformed = cose.to_tagged_vec().expect("encode unsupported header"); + + assert_eq!( + cose_decrypt_ml_kem(&decrypt_request(&malformed, &private_key, &kid)).map(|_| ()), + Err(CoseError::InvalidRecipient), + ); +} + +#[test] +fn invalid_encapsulated_key_length_is_rejected_structurally() { + let (public_key, private_key, kid) = keypair(CoseMlKemAlgorithm::MlKem768); + let request = encrypt_request( + CoseMlKemAlgorithm::MlKem768, + CoseContentEncryptionAlgorithm::Aes192Gcm, + &public_key, + &kid, + PLAINTEXT, + ); + let encoded = cose_encrypt_ml_kem_direct(&request).expect("direct encryption"); + let mut cose = CoseEncrypt::from_tagged_slice(&encoded).expect("tagged COSE_Encrypt"); + let recipient = cose.recipients.first_mut().expect("recipient"); + let (_, encapsulated_key) = recipient.unprotected.rest.first_mut().expect("ek header"); + *encapsulated_key = ciborium::value::Value::Bytes(vec![0u8; 16]); + let malformed = cose.to_tagged_vec().expect("encode malformed object"); + + assert_eq!( + cose_decrypt_ml_kem(&decrypt_request(&malformed, &private_key, &kid)).map(|_| ()), + Err(CoseError::InvalidEncapsulatedKey), + ); +} + +fn encrypt_request<'a>( + kem_algorithm: CoseMlKemAlgorithm, + content_algorithm: CoseContentEncryptionAlgorithm, + public_key: &'a [u8], + kid: &'a [u8], + plaintext: &'a [u8], +) -> CoseMlKemEncryptRequest<'a> { + CoseMlKemEncryptRequest::new( + kem_algorithm, + content_algorithm, + public_key, + kid, + plaintext, + None, + ) +} + +fn decrypt_request<'a>( + encoded: &'a [u8], + private_key: &'a [u8], + kid: &'a [u8], +) -> CoseMlKemDecryptRequest<'a> { + CoseMlKemDecryptRequest::new(encoded, private_key, kid, None) +} + +fn keypair(algorithm: CoseMlKemAlgorithm) -> (Vec, Zeroizing>, Zeroizing>) { + let (public_key, private_key, crypto_algorithm) = if algorithm == CoseMlKemAlgorithm::MlKem512 { + let (public_key, private_key) = reallyme_crypto::ml_kem_512::generate_ml_kem_512_keypair() + .expect("ML-KEM-512 key generation"); + (public_key, private_key, Algorithm::MlKem512) + } else if algorithm == CoseMlKemAlgorithm::MlKem768 { + let (public_key, private_key) = reallyme_crypto::ml_kem_768::generate_ml_kem_768_keypair() + .expect("ML-KEM-768 key generation"); + (public_key, private_key, Algorithm::MlKem768) + } else { + let (public_key, private_key) = + reallyme_crypto::ml_kem_1024::generate_ml_kem_1024_keypair() + .expect("ML-KEM-1024 key generation"); + (public_key, private_key, Algorithm::MlKem1024) + }; + let cose_key = cose_key_from_public_bytes(crypto_algorithm, &public_key) + .expect("ML-KEM public COSE_Key conversion"); + let kid = derive_kid_from_cose_key_public(&cose_key).expect("ML-KEM public kid derivation"); + (public_key, private_key, kid) +} + +fn kem_algorithms() -> [CoseMlKemAlgorithm; 3] { + [ + CoseMlKemAlgorithm::MlKem512, + CoseMlKemAlgorithm::MlKem768, + CoseMlKemAlgorithm::MlKem1024, + ] +} + +fn content_algorithms() -> [CoseContentEncryptionAlgorithm; 3] { + [ + CoseContentEncryptionAlgorithm::Aes128Gcm, + CoseContentEncryptionAlgorithm::Aes192Gcm, + CoseContentEncryptionAlgorithm::Aes256Gcm, + ] +} + +fn content_key_length(algorithm: CoseContentEncryptionAlgorithm) -> usize { + if algorithm == CoseContentEncryptionAlgorithm::Aes128Gcm { + 16 + } else if algorithm == CoseContentEncryptionAlgorithm::Aes192Gcm { + 24 + } else { + 32 + } +} diff --git a/tests/cose_suite/multikey_tests.rs b/crates/cose/tests/cose_suite/multikey_tests.rs similarity index 100% rename from tests/cose_suite/multikey_tests.rs rename to crates/cose/tests/cose_suite/multikey_tests.rs diff --git a/tests/cose_suite/multikey_to_cose_tests.rs b/crates/cose/tests/cose_suite/multikey_to_cose_tests.rs similarity index 75% rename from tests/cose_suite/multikey_to_cose_tests.rs rename to crates/cose/tests/cose_suite/multikey_to_cose_tests.rs index cd6c8eb..cff6a40 100644 --- a/tests/cose_suite/multikey_to_cose_tests.rs +++ b/crates/cose/tests/cose_suite/multikey_to_cose_tests.rs @@ -6,10 +6,13 @@ use reallyme_codec::multikey::encode_multikey; use reallyme_cose::{ cose_key_from_public_bytes, cose_key_to_multikey, cose_key_to_public_bytes, - multikey_to_cose_key, CoseError, + multikey_to_cose_key, }; -use super::support::{gen_ed25519, gen_p256, gen_p384, gen_p521, gen_secp256k1, gen_x25519}; +use super::support::{ + gen_ed25519, gen_mlkem1024, gen_mlkem512, gen_mlkem768, gen_p256, gen_p384, gen_p521, + gen_secp256k1, gen_x25519, +}; #[test] fn multikey_to_cose_ed25519_roundtrip() { @@ -111,14 +114,23 @@ fn multikey_to_cose_rejects_unknown_codec() { } #[test] -fn multikey_to_cose_rejects_algorithm_unsupported_by_current_mapping() { - const ML_DSA_87_PUBLIC_KEY_LEN: usize = 2_592; - - let multikey = encode_multikey("mldsa-87-pub", &[0u8; ML_DSA_87_PUBLIC_KEY_LEN]).unwrap(); - - let res = multikey_to_cose_key(&multikey); +fn multikey_to_cose_supports_mldsa87() { + let key = crate::support::gen_mldsa87(); + let multikey = encode_multikey("mldsa-87-pub", &key.public).unwrap(); + let cose_key = multikey_to_cose_key(&multikey).unwrap(); + assert_eq!(cose_key_to_public_bytes(&cose_key).unwrap(), key.public); +} - assert!(matches!(res.unwrap_err(), CoseError::UnsupportedAlgorithm)); +#[test] +fn multikey_to_cose_supports_all_reallyme_ml_kem_profiles() { + for key in [gen_mlkem512(), gen_mlkem768(), gen_mlkem1024()] { + let cose_key = cose_key_from_public_bytes(key.alg, &key.public).unwrap(); + let multikey = cose_key_to_multikey(&cose_key).unwrap(); + let decoded = multikey_to_cose_key(&multikey).unwrap(); + + assert_eq!(cose_key_to_public_bytes(&decoded).unwrap(), key.public); + assert_eq!(cose_key_to_multikey(&decoded).unwrap(), multikey); + } } #[test] diff --git a/crates/cose/tests/cose_suite/operation_response_v2_tests.rs b/crates/cose/tests/cose_suite/operation_response_v2_tests.rs new file mode 100644 index 0000000..899094d --- /dev/null +++ b/crates/cose/tests/cose_suite/operation_response_v2_tests.rs @@ -0,0 +1,466 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +#![allow(clippy::expect_used, clippy::panic)] + +use buffa::{EnumValue, Message}; +use reallyme_cose::wire::{ + cose_algorithm_identifier, cose_error_proto, cose_operation_request, + cose_operation_response_v2, cose_operation_result, decode_operation_response_for_request, + execute_operation_proto, execute_operation_proto_json, CoseAlgorithmIdentifier, + CoseContentEncryptionAlgorithm, CoseErrorProto, CoseErrorReason, CoseKemAlgorithm, + CoseKeyBytesRequest, CoseKeyFromPrivateBytesRequest, CoseKeyFromPublicBytesRequest, + CoseMlKemDecryptRequest, CoseMlKemEncryptRequest, CoseMultikeyToCoseKeyRequest, + CoseOperationRequest, CoseOperationResponseV2, CoseOperationResult, + CoseSign1CreateDetachedRequest, CoseSign1CreateRequest, CoseSign1Options, + CoseSign1VerifyDetachedRequest, CoseSign1VerifyRequest, CoseSignatureAlgorithm, + MAX_COSE_PROTO_MESSAGE_BYTES, +}; +use reallyme_cose::{ + cose_decrypt_ml_kem, cose_key_from_public_bytes, derive_kid_from_cose_key_public, Algorithm, + CoseMlKemDecryptRequest as NativeDecryptRequest, +}; +use zeroize::Zeroizing; + +use super::support::{gen_ed25519, gen_mlkem512, test_kid}; + +type RequestBranch = cose_operation_request::Operation; +type ResponseOutcome = cose_operation_response_v2::Outcome; +type ResultBranch = cose_operation_result::Result; +const PAYLOAD: &[u8] = b"discriminated operation response payload"; + +#[test] +fn every_executable_operation_returns_its_exact_version_two_variant() { + let mut signing_key = gen_ed25519(); + let public_key = Zeroizing::new(core::mem::take(&mut signing_key.public)); + let private_key = Zeroizing::new(core::mem::take(&mut signing_key.private)); + + let request = operation(RequestBranch::KeyFromPublicBytes(Box::new( + CoseKeyFromPublicBytesRequest { + algorithm: ed25519_identifier(), + public_key: public_key.to_vec(), + __buffa_unknown_fields: Default::default(), + }, + ))); + let public_cose = match take_result_branch(execute_result(&request)) { + ResultBranch::KeyFromPublicBytes(mut message) => { + Zeroizing::new(core::mem::take(&mut message.key_bytes)) + } + _ => panic!("key_from_public_bytes returned the wrong v2 result variant"), + }; + + let request = operation(RequestBranch::KeyFromPrivateBytes(Box::new( + CoseKeyFromPrivateBytesRequest { + algorithm: ed25519_identifier(), + private_key: private_key.to_vec(), + public_key: public_key.to_vec(), + has_public_key: true, + __buffa_unknown_fields: Default::default(), + }, + ))); + let private_cose = match take_result_branch(execute_result(&request)) { + ResultBranch::KeyFromPrivateBytes(mut message) => { + Zeroizing::new(core::mem::take(&mut message.key_bytes)) + } + _ => panic!("key_from_private_bytes returned the wrong v2 result variant"), + }; + + assert_key_result( + operation(RequestBranch::KeyParse(Box::new(key_request(&public_cose)))), + ResultBranchKind::Parse, + ); + assert_key_result( + operation(RequestBranch::KeyToPublicBytes(Box::new(key_request( + &private_cose, + )))), + ResultBranchKind::PublicBytes, + ); + assert_key_result( + operation(RequestBranch::KeyToPrivateBytes(Box::new(key_request( + &private_cose, + )))), + ResultBranchKind::PrivateBytes, + ); + assert_key_result( + operation(RequestBranch::KeyDerivePublicKid(Box::new(key_request( + &public_cose, + )))), + ResultBranchKind::PublicKid, + ); + + let request = operation(RequestBranch::KeyToMultikey(Box::new(key_request( + &public_cose, + )))); + let multikey = match take_result_branch(execute_result(&request)) { + ResultBranch::KeyToMultikey(mut message) => { + Zeroizing::new(core::mem::take(&mut message.multikey)) + } + _ => panic!("key_to_multikey returned the wrong v2 result variant"), + }; + let request = operation(RequestBranch::MultikeyToCoseKey(Box::new( + CoseMultikeyToCoseKeyRequest { + multikey: multikey.to_string(), + __buffa_unknown_fields: Default::default(), + }, + ))); + match take_result_branch(execute_result(&request)) { + ResultBranch::MultikeyToCoseKey(message) => assert!(!message.key_bytes.is_empty()), + _ => panic!("multikey_to_cose_key returned the wrong v2 result variant"), + } + + let request = operation(RequestBranch::Sign1Create(Box::new(sign_request( + &private_key, + )))); + let attached = match take_result_branch(execute_result(&request)) { + ResultBranch::Sign1Create(mut message) => { + Zeroizing::new(core::mem::take(&mut message.cose_sign1)) + } + _ => panic!("sign1_create returned the wrong v2 result variant"), + }; + let request = operation(RequestBranch::Sign1CreateDetached(Box::new( + detached_sign_request(&private_key), + ))); + let detached = match take_result_branch(execute_result(&request)) { + ResultBranch::Sign1CreateDetached(mut message) => { + Zeroizing::new(core::mem::take(&mut message.cose_sign1)) + } + _ => panic!("sign1_create_detached returned the wrong v2 result variant"), + }; + + let request = operation(RequestBranch::Sign1Verify(Box::new(verify_request( + &attached, + &public_key, + )))); + match take_result_branch(execute_result(&request)) { + ResultBranch::Sign1Verify(message) => assert_eq!(message.payload, PAYLOAD), + _ => panic!("sign1_verify returned the wrong v2 result variant"), + } + let request = operation(RequestBranch::Sign1VerifyDetached(Box::new( + detached_verify_request(&detached, &public_key), + ))); + match take_result_branch(execute_result(&request)) { + ResultBranch::Sign1VerifyDetached(message) => assert!(message.payload.is_empty()), + _ => panic!("sign1_verify_detached returned the wrong v2 result variant"), + } + + let mut kem_key = gen_mlkem512(); + let kem_public = Zeroizing::new(core::mem::take(&mut kem_key.public)); + let kem_private = Zeroizing::new(core::mem::take(&mut kem_key.private)); + let public_cose_key = cose_key_from_public_bytes(Algorithm::MlKem512, &kem_public) + .expect("ML-KEM public key must construct"); + let kid = Zeroizing::new( + derive_kid_from_cose_key_public(&public_cose_key).expect("ML-KEM kid must derive"), + ); + + let direct_request = operation(RequestBranch::MlKemEncryptDirect(Box::new( + encrypt_request(&kem_public, &kid), + ))); + let direct = match take_result_branch(execute_result(&direct_request)) { + ResultBranch::MlKemEncryptDirect(mut message) => { + Zeroizing::new(core::mem::take(&mut message.cose_encrypt)) + } + _ => panic!("ml_kem_encrypt_direct returned the wrong v2 result variant"), + }; + assert_plaintext(&direct, &kem_private, &kid); + + let key_wrap_request = operation(RequestBranch::MlKemEncryptKeyWrap(Box::new( + encrypt_request(&kem_public, &kid), + ))); + let key_wrapped = match take_result_branch(execute_result(&key_wrap_request)) { + ResultBranch::MlKemEncryptKeyWrap(mut message) => { + Zeroizing::new(core::mem::take(&mut message.cose_encrypt)) + } + _ => panic!("ml_kem_encrypt_key_wrap returned the wrong v2 result variant"), + }; + assert_plaintext(&key_wrapped, &kem_private, &kid); + + let request = operation(RequestBranch::MlKemDecrypt(Box::new(decrypt_request( + &direct, + &kem_private, + &kid, + )))); + match take_result_branch(execute_result(&request)) { + ResultBranch::MlKemDecrypt(message) => { + assert_eq!(message.plaintext, PAYLOAD); + } + _ => panic!("ml_kem_decrypt returned the wrong v2 result variant"), + } +} + +#[test] +fn operation_error_outcomes_match_binary_and_json_contracts() { + let request = operation(RequestBranch::KeyParse(Box::new(CoseKeyBytesRequest { + cose_key: vec![0xff], + __buffa_unknown_fields: Default::default(), + }))); + let binary_request = Zeroizing::new(request.encode_to_vec()); + let binary = execute_operation_proto(&binary_request); + let json = Zeroizing::new( + serde_json::to_string(&request).expect("generated request must encode as ProtoJSON"), + ); + let json_response = execute_operation_proto_json(&json); + assert_eq!(binary.as_slice(), json_response.as_slice()); + + let response = decode_operation_response_for_request(&request, &binary) + .expect("generated v2 error response must validate"); + let error = take_error(response); + assert!(matches!( + error.error, + Some(cose_error_proto::Error::Primitive(_)) + )); +} + +#[test] +fn version_two_decoder_rejects_mismatched_and_hostile_responses_as_backend_failures() { + let request = operation(RequestBranch::KeyParse(Box::default())); + let mismatched = response_with_result(ResultBranch::KeyToPublicBytes(Box::default())); + assert_backend_error( + decode_operation_response_for_request(&request, &mismatched.encode_to_vec()), + CoseErrorReason::BackendInternal, + ); + + let missing_outcome = CoseOperationResponseV2::default(); + assert_backend_error( + decode_operation_response_for_request(&request, &missing_outcome.encode_to_vec()), + CoseErrorReason::BackendInternal, + ); + + let verify_request = operation(RequestBranch::Sign1Verify(Box::default())); + let invalid_metadata = response_with_result(ResultBranch::Sign1Verify(Box::new( + reallyme_cose::wire::CoseSign1VerifyResult { + payload: Vec::new(), + algorithm: EnumValue::from(42_424_242), + kid: Vec::new(), + __buffa_unknown_fields: Default::default(), + }, + ))); + assert_backend_error( + decode_operation_response_for_request(&verify_request, &invalid_metadata.encode_to_vec()), + CoseErrorReason::BackendInternal, + ); + + let mut unknown_field = mismatched.encode_to_vec(); + unknown_field.extend_from_slice(&[0x1a, 0x00]); + assert_backend_error( + decode_operation_response_for_request(&request, &unknown_field), + CoseErrorReason::BackendInternal, + ); + + let oversized_len = MAX_COSE_PROTO_MESSAGE_BYTES + .checked_add(33) + .expect("test response size must fit usize"); + let oversized = Zeroizing::new(vec![0_u8; oversized_len]); + assert_backend_error( + decode_operation_response_for_request(&request, &oversized), + CoseErrorReason::CommonResourceLimitExceeded, + ); +} + +#[derive(Clone, Copy)] +enum ResultBranchKind { + Parse, + PublicBytes, + PrivateBytes, + PublicKid, +} + +fn assert_key_result(request: CoseOperationRequest, expected: ResultBranchKind) { + let result = take_result_branch(execute_result(&request)); + let message = match (expected, result) { + (ResultBranchKind::Parse, ResultBranch::KeyParse(message)) + | (ResultBranchKind::PublicBytes, ResultBranch::KeyToPublicBytes(message)) + | (ResultBranchKind::PrivateBytes, ResultBranch::KeyToPrivateBytes(message)) + | (ResultBranchKind::PublicKid, ResultBranch::KeyDerivePublicKid(message)) => message, + _ => panic!("key operation returned the wrong v2 result variant"), + }; + assert!(!message.key_bytes.is_empty()); +} + +fn execute_v2(request: &CoseOperationRequest) -> CoseOperationResponseV2 { + let request_bytes = Zeroizing::new(request.encode_to_vec()); + let response_bytes = execute_operation_proto(&request_bytes); + let response = decode_operation_response_for_request(request, &response_bytes) + .expect("generated binary v2 response must validate for its request"); + + let request_json = Zeroizing::new( + serde_json::to_string(request).expect("generated request must encode as ProtoJSON"), + ); + let json_response_bytes = execute_operation_proto_json(&request_json); + let _json_response = decode_operation_response_for_request(request, &json_response_bytes) + .expect("generated ProtoJSON v2 response must validate for its request"); + if !request_has_randomized_output(request) { + assert_eq!(response_bytes.as_slice(), json_response_bytes.as_slice()); + } + response +} + +fn request_has_randomized_output(request: &CoseOperationRequest) -> bool { + matches!( + request.operation.as_ref(), + Some(RequestBranch::MlKemEncryptDirect(_) | RequestBranch::MlKemEncryptKeyWrap(_)) + ) +} + +fn execute_result(request: &CoseOperationRequest) -> CoseOperationResult { + take_operation_result(execute_v2(request)) +} + +fn take_operation_result(mut response: CoseOperationResponseV2) -> CoseOperationResult { + match response.outcome.take() { + Some(ResponseOutcome::Result(result)) => *result, + _ => panic!("operation returned a v2 error outcome"), + } +} + +fn take_result_branch(mut result: CoseOperationResult) -> ResultBranch { + result + .result + .take() + .expect("successful v2 result must contain an operation branch") +} + +fn take_error(mut response: CoseOperationResponseV2) -> CoseErrorProto { + match response.outcome.take() { + Some(ResponseOutcome::Error(error)) => *error, + _ => panic!("operation returned a v2 success outcome"), + } +} + +fn assert_backend_error( + result: Result, + expected: CoseErrorReason, +) { + let error = take_error(result.expect_err("hostile v2 response must be rejected")); + match error.error { + Some(cose_error_proto::Error::Backend(error)) => { + assert_eq!(error.reason.as_known(), Some(expected)); + } + _ => panic!("hostile v2 response did not return a backend error"), + } +} + +fn response_with_result(result: ResultBranch) -> CoseOperationResponseV2 { + CoseOperationResponseV2 { + outcome: Some(ResponseOutcome::Result(Box::new(CoseOperationResult { + result: Some(result), + __buffa_unknown_fields: Default::default(), + }))), + __buffa_unknown_fields: Default::default(), + } +} + +fn operation(operation: RequestBranch) -> CoseOperationRequest { + CoseOperationRequest { + operation: Some(operation), + __buffa_unknown_fields: Default::default(), + } +} + +fn ed25519_identifier( +) -> buffa::MessageField> { + buffa::MessageField::some(CoseAlgorithmIdentifier { + algorithm: Some(cose_algorithm_identifier::Algorithm::Signature( + EnumValue::from(CoseSignatureAlgorithm::Ed25519), + )), + __buffa_unknown_fields: Default::default(), + }) +} + +fn key_request(cose_key: &[u8]) -> CoseKeyBytesRequest { + CoseKeyBytesRequest { + cose_key: cose_key.to_vec(), + __buffa_unknown_fields: Default::default(), + } +} + +fn sign_options() -> buffa::MessageField> { + buffa::MessageField::some(CoseSign1Options::default()) +} + +fn sign_request(private_key: &[u8]) -> CoseSign1CreateRequest { + CoseSign1CreateRequest { + algorithm: EnumValue::from(CoseSignatureAlgorithm::Ed25519), + payload: PAYLOAD.to_vec(), + private_key: private_key.to_vec(), + kid: test_kid().to_vec(), + has_kid: true, + options: sign_options(), + external_aad: Vec::new(), + __buffa_unknown_fields: Default::default(), + } +} + +fn detached_sign_request(private_key: &[u8]) -> CoseSign1CreateDetachedRequest { + CoseSign1CreateDetachedRequest { + algorithm: EnumValue::from(CoseSignatureAlgorithm::Ed25519), + payload: PAYLOAD.to_vec(), + private_key: private_key.to_vec(), + kid: test_kid().to_vec(), + has_kid: true, + options: sign_options(), + external_aad: Vec::new(), + __buffa_unknown_fields: Default::default(), + } +} + +fn verify_request(cose_sign1: &[u8], public_key: &[u8]) -> CoseSign1VerifyRequest { + CoseSign1VerifyRequest { + cose_sign1: cose_sign1.to_vec(), + public_key: public_key.to_vec(), + max_cose_sign1_bytes: 0, + max_detached_payload_bytes: 0, + require_kid: true, + allowed_algorithms: vec![EnumValue::from(CoseSignatureAlgorithm::Ed25519)], + external_aad: Vec::new(), + expected_kid: test_kid().to_vec(), + __buffa_unknown_fields: Default::default(), + } +} + +fn detached_verify_request(cose_sign1: &[u8], public_key: &[u8]) -> CoseSign1VerifyDetachedRequest { + CoseSign1VerifyDetachedRequest { + cose_sign1: cose_sign1.to_vec(), + payload: PAYLOAD.to_vec(), + public_key: public_key.to_vec(), + max_cose_sign1_bytes: 0, + max_detached_payload_bytes: 0, + require_kid: true, + allowed_algorithms: vec![EnumValue::from(CoseSignatureAlgorithm::Ed25519)], + external_aad: Vec::new(), + expected_kid: test_kid().to_vec(), + __buffa_unknown_fields: Default::default(), + } +} + +fn encrypt_request(public_key: &[u8], kid: &[u8]) -> CoseMlKemEncryptRequest { + CoseMlKemEncryptRequest { + kem_algorithm: EnumValue::from(CoseKemAlgorithm::MlKem512), + content_algorithm: EnumValue::from(CoseContentEncryptionAlgorithm::Aes128Gcm), + recipient_public_key: public_key.to_vec(), + recipient_kid: kid.to_vec(), + plaintext: PAYLOAD.to_vec(), + external_aad: Vec::new(), + supp_priv_info: Vec::new(), + has_supp_priv_info: false, + __buffa_unknown_fields: Default::default(), + } +} + +fn decrypt_request(cose_encrypt: &[u8], private_key: &[u8], kid: &[u8]) -> CoseMlKemDecryptRequest { + CoseMlKemDecryptRequest { + cose_encrypt: cose_encrypt.to_vec(), + recipient_private_key: private_key.to_vec(), + expected_recipient_kid: kid.to_vec(), + external_aad: Vec::new(), + supp_priv_info: Vec::new(), + has_supp_priv_info: false, + __buffa_unknown_fields: Default::default(), + } +} + +fn assert_plaintext(cose_encrypt: &[u8], private_key: &[u8], kid: &[u8]) { + let request = NativeDecryptRequest::new(cose_encrypt, private_key, kid, None); + let decrypted = cose_decrypt_ml_kem(&request).expect("v2 ciphertext must decrypt natively"); + assert_eq!(decrypted.plaintext.as_slice(), PAYLOAD); +} diff --git a/tests/cose_suite/platform_consumer_tests.rs b/crates/cose/tests/cose_suite/platform_consumer_tests.rs similarity index 69% rename from tests/cose_suite/platform_consumer_tests.rs rename to crates/cose/tests/cose_suite/platform_consumer_tests.rs index 463c7c4..966e77a 100644 --- a/tests/cose_suite/platform_consumer_tests.rs +++ b/crates/cose/tests/cose_suite/platform_consumer_tests.rs @@ -27,11 +27,11 @@ fn cose_sign1_tagged_emits_root_tag_18_and_verifies() { let decoded = CoseSign1::from_tagged_slice(&tagged).unwrap(); assert_eq!(decoded.payload.as_deref(), Some(payload.as_slice())); - let verified = cose_verify1(&tagged, |kid| { + let verified = cose_verify1(&tagged, |_, kid| { (kid == test_kid()).then(|| key.public.clone()) }) .unwrap(); - assert_eq!(verified, payload); + assert_eq!(verified.as_slice(), payload); } #[test] @@ -40,20 +40,18 @@ fn cose_sign1_detached_tagged_emits_root_tag_18_and_verifies_with_policy() { let payload = b"detached tagged payload"; let tagged = cose_sign1_detached_tagged(key.alg, payload, &key.private, Some(test_kid())).unwrap(); - let policy = CosePolicy { - require_kid: true, - allowed_algs: vec![Algorithm::Ed25519], - ..Default::default() - }; + let policy = CosePolicy::new() + .with_require_kid(true) + .allow_algorithm(Algorithm::Ed25519); assert_eq!(tagged.first(), Some(&0xd2)); - let metadata = cose_verify1_detached_with_policy(&tagged, payload, &policy, |kid| { + let metadata = cose_verify1_detached_with_policy(&tagged, payload, &policy, |_, kid| { (kid == test_kid()).then(|| key.public.clone()) }) .unwrap(); assert_eq!(metadata.alg, Algorithm::Ed25519); - assert_eq!(metadata.kid, test_kid()); + assert_eq!(metadata.kid.as_slice(), test_kid()); } #[test] @@ -61,20 +59,18 @@ fn cose_verify1_with_policy_returns_verified_payload_alg_and_kid() { let key = gen_p256(); let payload = b"policy-bound payload"; let cose = cose_sign1(key.alg, payload, &key.private, Some(test_kid())).unwrap(); - let policy = CosePolicy { - require_kid: true, - allowed_algs: vec![Algorithm::P256], - ..Default::default() - }; + let policy = CosePolicy::new() + .with_require_kid(true) + .allow_algorithm(Algorithm::P256); - let verified = cose_verify1_with_policy(&cose, &policy, |kid| { + let verified = cose_verify1_with_policy(&cose, &policy, |_, kid| { (kid == test_kid()).then(|| key.public.clone()) }) .unwrap(); - assert_eq!(verified.payload, payload); + assert_eq!(verified.payload.as_slice(), payload); assert_eq!(verified.alg, Algorithm::P256); - assert_eq!(verified.kid, test_kid()); + assert_eq!(verified.kid.as_slice(), test_kid()); } #[test] @@ -83,13 +79,14 @@ fn cose_verify1_with_metadata_uses_default_policy_and_returns_header_metadata() let payload = b"default metadata payload"; let cose = cose_sign1(key.alg, payload, &key.private, Some(test_kid())).unwrap(); - let verified = - cose_verify1_with_metadata(&cose, |kid| (kid == test_kid()).then(|| key.public.clone())) - .unwrap(); + let verified = cose_verify1_with_metadata(&cose, |_, kid| { + (kid == test_kid()).then(|| key.public.clone()) + }) + .unwrap(); - assert_eq!(verified.payload, payload); + assert_eq!(verified.payload.as_slice(), payload); assert_eq!(verified.alg, Algorithm::Ed25519); - assert_eq!(verified.kid, test_kid()); + assert_eq!(verified.kid.as_slice(), test_kid()); } #[test] @@ -97,13 +94,11 @@ fn cose_verify1_with_policy_rejects_disallowed_algorithm_before_key_resolution() let key = gen_ed25519(); let cose = cose_sign1(key.alg, b"wrong algorithm", &key.private, Some(test_kid())).unwrap(); let resolver_called = Cell::new(false); - let policy = CosePolicy { - require_kid: true, - allowed_algs: vec![Algorithm::P256], - ..Default::default() - }; + let policy = CosePolicy::new() + .with_require_kid(true) + .allow_algorithm(Algorithm::P256); - let result = cose_verify1_with_policy(&cose, &policy, |kid| { + let result = cose_verify1_with_policy(&cose, &policy, |_, kid| { resolver_called.set(true); (kid == test_kid()).then(|| key.public.clone()) }); @@ -120,27 +115,22 @@ fn attached_sign1_limit_can_be_raised_for_platform_reports() { &payload, &key.private, Some(test_kid()), - CoseSign1EncodeOptions { - tag: true, - max_cose_sign1_bytes: PLATFORM_COSE_SIGN1_LIMIT, - }, + CoseSign1EncodeOptions::tagged().with_max_cose_sign1_bytes(PLATFORM_COSE_SIGN1_LIMIT), ) .unwrap(); - let policy = CosePolicy { - require_kid: true, - allowed_algs: vec![Algorithm::Ed25519], - max_cose_sign1_bytes: PLATFORM_COSE_SIGN1_LIMIT, - ..Default::default() - }; - - let result = cose_verify1(&cose, |_| Some(key.public.clone())); + let policy = CosePolicy::new() + .with_require_kid(true) + .allow_algorithm(Algorithm::Ed25519) + .with_max_cose_sign1_bytes(PLATFORM_COSE_SIGN1_LIMIT); + + let result = cose_verify1(&cose, |_, _| Some(key.public.clone())); assert!(matches!(result, Err(CoseError::ResourceLimitExceeded))); - let verified = cose_verify1_with_policy(&cose, &policy, |kid| { + let verified = cose_verify1_with_policy(&cose, &policy, |_, kid| { (kid == test_kid()).then(|| key.public.clone()) }) .unwrap(); - assert_eq!(verified.payload, payload); + assert_eq!(verified.payload.as_slice(), payload.as_slice()); } fn deterministic_payload(len: usize) -> Vec { diff --git a/crates/cose/tests/cose_suite/platform_signer_tests.rs b/crates/cose/tests/cose_suite/platform_signer_tests.rs new file mode 100644 index 0000000..be216ee --- /dev/null +++ b/crates/cose/tests/cose_suite/platform_signer_tests.rs @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use reallyme_cose::{ + cose_sign1_detached_with_signer, cose_sign1_with_signer, cose_verify1_detached_with_policy, + cose_verify1_with_policy, Algorithm, CoseError, CosePolicy, CoseSign1EncodeOptions, CoseSigner, + CoseSignerError, +}; +use reallyme_crypto::dispatch::{generate_keypair, sign}; +use zeroize::Zeroizing; + +const PAYLOAD: &[u8] = b"platform-owned signing payload"; +const KID: &[u8] = b"platform-key-handle"; + +struct TestPlatformSigner { + algorithm: Algorithm, + private_key: Zeroizing>, +} + +impl CoseSigner for TestPlatformSigner { + fn algorithm(&self) -> Algorithm { + self.algorithm + } + + fn sign(&self, sig_structure: &[u8]) -> Result>, CoseSignerError> { + sign(self.algorithm, &self.private_key, sig_structure) + .map(Zeroizing::new) + .map_err(|_| CoseSignerError::Backend) + } +} + +struct FailingPlatformSigner { + error: CoseSignerError, +} + +impl CoseSigner for FailingPlatformSigner { + fn algorithm(&self) -> Algorithm { + Algorithm::Ed25519 + } + + fn sign(&self, _sig_structure: &[u8]) -> Result>, CoseSignerError> { + Err(self.error) + } +} + +#[test] +fn provider_owned_key_signs_attached_and_detached_without_key_export() { + let (public_key, private_key) = + generate_keypair(Algorithm::Ed25519).expect("Ed25519 fixture generation must succeed"); + let signer = TestPlatformSigner { + algorithm: Algorithm::Ed25519, + private_key, + }; + let policy = CosePolicy::new() + .with_require_kid(true) + .allow_algorithm(Algorithm::Ed25519); + + let attached = cose_sign1_with_signer( + &signer, + PAYLOAD, + Some(KID), + &[], + CoseSign1EncodeOptions::default(), + ) + .expect("provider-backed attached signing must succeed"); + let verified = cose_verify1_with_policy(&attached, &policy, |_, _| Some(public_key.to_vec())) + .expect("provider-backed attached signature must verify"); + assert_eq!(verified.payload.as_slice(), PAYLOAD); + assert_eq!(verified.kid.as_slice(), KID); + + let detached = cose_sign1_detached_with_signer( + &signer, + PAYLOAD, + Some(KID), + &[], + CoseSign1EncodeOptions::default(), + ) + .expect("provider-backed detached signing must succeed"); + let verified = cose_verify1_detached_with_policy(&detached, PAYLOAD, &policy, |_, _| { + Some(public_key.clone()) + }) + .expect("provider-backed detached signature must verify"); + assert_eq!(verified.kid.as_slice(), KID); +} + +#[test] +fn provider_failures_map_to_stable_native_errors() { + for (provider_error, expected) in [ + ( + CoseSignerError::UnsupportedAlgorithm, + CoseError::UnsupportedAlgorithm, + ), + (CoseSignerError::InvalidKey, CoseError::InvalidKeyMaterial), + (CoseSignerError::Unavailable, CoseError::ProviderUnavailable), + (CoseSignerError::Backend, CoseError::Crypto), + ] { + let signer = FailingPlatformSigner { + error: provider_error, + }; + let result = cose_sign1_with_signer( + &signer, + PAYLOAD, + Some(KID), + &[], + CoseSign1EncodeOptions::default(), + ); + assert_eq!(result, Err(expected)); + } +} diff --git a/tests/cose_suite/rfc9052_9053_profile_tests.rs b/crates/cose/tests/cose_suite/rfc9052_9053_profile_tests.rs similarity index 52% rename from tests/cose_suite/rfc9052_9053_profile_tests.rs rename to crates/cose/tests/cose_suite/rfc9052_9053_profile_tests.rs index a32447f..fa75a03 100644 --- a/tests/cose_suite/rfc9052_9053_profile_tests.rs +++ b/crates/cose/tests/cose_suite/rfc9052_9053_profile_tests.rs @@ -6,20 +6,22 @@ use std::io::Cursor; use ciborium::{ser::into_writer, value::Value}; -use coset::{iana, CborSerializable, Label, RegisteredLabelWithPrivate}; +use coset::{iana, CborSerializable, CoseKey as RawCoseKey, Label, RegisteredLabelWithPrivate}; use reallyme_cose::{ cose_key_from_private_bytes, cose_key_from_public_bytes, cose_key_from_slice, - cose_key_to_multikey, cose_key_to_public_bytes, cose_key_to_vec, cose_sign1, cose_verify1, + cose_key_to_multikey, cose_key_to_vec, cose_sign1, cose_verify1, derive_kid_from_cose_key_public, CoseError, }; -use crate::support::{gen_ed25519, gen_p256, gen_p384, sample_payload, test_kid}; +use crate::support::{gen_ed25519, gen_mldsa44, gen_p256, gen_p384, sample_payload, test_kid}; #[test] fn cose_key_rejects_duplicate_map_labels() { let duplicate_kty = [0xa2, 0x01, 0x01, 0x01, 0x01]; - let err = cose_key_from_slice(&duplicate_kty).expect_err("duplicate labels must be rejected"); + let err = cose_key_from_slice(&duplicate_kty) + .err() + .expect("duplicate labels must be rejected"); assert_eq!(err, CoseError::DuplicateMapLabel); } @@ -28,7 +30,7 @@ fn cose_key_rejects_duplicate_map_labels() { fn cose_sign1_rejects_duplicate_protected_header_labels() { let duplicate_protected_alg = [0x84, 0x45, 0xa2, 0x01, 0x27, 0x01, 0x27, 0xa0, 0x40, 0x40]; - let err = cose_verify1(&duplicate_protected_alg, |_| None) + let err = cose_verify1(&duplicate_protected_alg, |_, _| None) .expect_err("duplicate protected labels must be rejected"); assert_eq!(err, CoseError::DuplicateMapLabel); @@ -40,7 +42,7 @@ fn cose_sign1_rejects_duplicate_unprotected_header_labels() { 0x84, 0x43, 0xa1, 0x01, 0x27, 0xa2, 0x04, 0x41, 0x61, 0x04, 0x41, 0x62, 0x40, 0x40, ]; - let err = cose_verify1(&duplicate_unprotected_kid, |_| None) + let err = cose_verify1(&duplicate_unprotected_kid, |_, _| None) .expect_err("duplicate unprotected labels must be rejected"); assert_eq!(err, CoseError::DuplicateMapLabel); @@ -51,33 +53,27 @@ fn cose_key_rejects_noncanonical_integer_labels() { let noncanonical_kty_label = [0xa1, 0x18, 0x01, 0x01]; let err = cose_key_from_slice(&noncanonical_kty_label) - .expect_err("non-minimal integer labels must be rejected"); + .err() + .expect("non-minimal integer labels must be rejected"); assert_eq!(err, CoseError::NonCanonicalCbor); } -#[test] -fn cose_key_rejects_algorithm_key_mismatch() { - let key = gen_p256(); - let mut cose_key = - cose_key_from_public_bytes(key.alg, &key.public).expect("fixture COSE_Key must build"); - cose_key.alg = Some(RegisteredLabelWithPrivate::Assigned(iana::Algorithm::EdDSA)); - - let err = - cose_key_to_public_bytes(&cose_key).expect_err("P-256 key with EdDSA alg must be rejected"); - - assert_eq!(err, CoseError::UnsupportedAlgorithm); -} - #[test] fn decoded_cose_key_rejects_algorithm_key_mismatch() { let key = gen_p256(); - let mut cose_key = + let cose_key = cose_key_from_public_bytes(key.alg, &key.public).expect("fixture COSE_Key must build"); - cose_key.alg = Some(RegisteredLabelWithPrivate::Assigned(iana::Algorithm::EdDSA)); - let encoded = cose_key.to_vec().expect("fixture COSE_Key must encode"); + let encoded = cose_key_to_vec(&cose_key).expect("fixture COSE_Key must encode"); + let mut raw = RawCoseKey::from_slice(&encoded).expect("fixture raw COSE_Key must decode"); + raw.alg = Some(RegisteredLabelWithPrivate::Assigned( + iana::Algorithm::Ed25519, + )); + let encoded = raw.to_vec().expect("mutated fixture COSE_Key must encode"); - let err = cose_key_from_slice(&encoded).expect_err("mismatched alg must fail at decode"); + let err = cose_key_from_slice(&encoded) + .err() + .expect("mismatched alg must fail at decode"); assert_eq!(err, CoseError::UnsupportedAlgorithm); } @@ -85,17 +81,23 @@ fn decoded_cose_key_rejects_algorithm_key_mismatch() { #[test] fn cose_key_rejects_curve_coordinate_length_mismatch() { let key = gen_p256(); - let mut cose_key = + let cose_key = cose_key_from_public_bytes(key.alg, &key.public).expect("fixture COSE_Key must build"); + let encoded = cose_key_to_vec(&cose_key).expect("fixture COSE_Key must encode"); + let mut raw = RawCoseKey::from_slice(&encoded).expect("fixture raw COSE_Key must decode"); replace_param_integer( - &mut cose_key.params, + &mut raw.params, iana::Ec2KeyParameter::Crv as i64, iana::EllipticCurve::P_384 as i64, ); - cose_key.alg = Some(RegisteredLabelWithPrivate::Assigned(iana::Algorithm::ES384)); + raw.alg = Some(RegisteredLabelWithPrivate::Assigned( + iana::Algorithm::ESP384, + )); + let encoded = raw.to_vec().expect("mutated fixture COSE_Key must encode"); - let err = cose_key_to_public_bytes(&cose_key) - .expect_err("P-384 curve with P-256 coordinates must be rejected"); + let err = cose_key_from_slice(&encoded) + .err() + .expect("P-384 curve with P-256 coordinates must be rejected"); assert_eq!(err, CoseError::InvalidKeyMaterial); } @@ -103,22 +105,75 @@ fn cose_key_rejects_curve_coordinate_length_mismatch() { #[test] fn decoded_cose_key_rejects_invalid_coordinate_length() { let key = gen_p256(); - let mut cose_key = + let cose_key = cose_key_from_public_bytes(key.alg, &key.public).expect("fixture COSE_Key must build"); + let encoded = cose_key_to_vec(&cose_key).expect("fixture COSE_Key must encode"); + let mut raw = RawCoseKey::from_slice(&encoded).expect("fixture raw COSE_Key must decode"); replace_param_bytes( - &mut cose_key.params, + &mut raw.params, iana::Ec2KeyParameter::X as i64, vec![0_u8; 31], ); - let encoded = cose_key.to_vec().expect("fixture COSE_Key must encode"); + let encoded = raw.to_vec().expect("mutated fixture COSE_Key must encode"); - let err = cose_key_from_slice(&encoded).expect_err("short EC coordinate must fail at decode"); + let err = cose_key_from_slice(&encoded) + .err() + .expect("short EC coordinate must fail at decode"); assert_eq!(err, CoseError::InvalidKeyMaterial); } #[test] -fn cose_sign1_rejects_algorithm_key_mismatch_during_verification() { +fn decoded_cose_key_rejects_present_but_empty_okp_parameters() { + let key = gen_ed25519(); + let cose_key = cose_key_from_private_bytes(key.alg, &key.private, Some(&key.public)) + .expect("fixture private COSE_Key must build"); + let encoded = cose_key_to_vec(&cose_key).expect("fixture COSE_Key must encode"); + + assert_empty_parameters_rejected( + &encoded, + &[ + iana::OkpKeyParameter::X as i64, + iana::OkpKeyParameter::D as i64, + ], + ); +} + +#[test] +fn decoded_cose_key_rejects_present_but_empty_ec2_parameters() { + let key = gen_p256(); + let cose_key = cose_key_from_private_bytes(key.alg, &key.private, Some(&key.public)) + .expect("fixture private COSE_Key must build"); + let encoded = cose_key_to_vec(&cose_key).expect("fixture COSE_Key must encode"); + + assert_empty_parameters_rejected( + &encoded, + &[ + iana::Ec2KeyParameter::X as i64, + iana::Ec2KeyParameter::Y as i64, + iana::Ec2KeyParameter::D as i64, + ], + ); +} + +#[test] +fn decoded_cose_key_rejects_present_but_empty_akp_parameters() { + let key = gen_mldsa44(); + let cose_key = cose_key_from_private_bytes(key.alg, &key.private, Some(&key.public)) + .expect("fixture private COSE_Key must build"); + let encoded = cose_key_to_vec(&cose_key).expect("fixture COSE_Key must encode"); + + assert_empty_parameters_rejected( + &encoded, + &[ + iana::AkpKeyParameter::Pub as i64, + iana::AkpKeyParameter::Priv as i64, + ], + ); +} + +#[test] +fn cose_sign1_reports_algorithm_key_mismatch_as_invalid_key_material() { let signing_key = gen_p256(); let wrong_curve_key = gen_p384(); let cose = cose_sign1( @@ -129,10 +184,10 @@ fn cose_sign1_rejects_algorithm_key_mismatch_during_verification() { ) .expect("fixture signing must succeed"); - let err = cose_verify1(&cose, |_| Some(wrong_curve_key.public.clone())) + let err = cose_verify1(&cose, |_, _| Some(wrong_curve_key.public.clone())) .expect_err("wrong curve public key must not verify"); - assert_eq!(err, CoseError::InvalidSignature); + assert_eq!(err, CoseError::InvalidKeyMaterial); } #[test] @@ -158,7 +213,7 @@ fn decoded_cose_key_reencodes_canonically() { let key = gen_ed25519(); let cose_key = cose_key_from_public_bytes(key.alg, &key.public).expect("fixture COSE_Key must build"); - let encoded = cose_key.to_vec().expect("fixture COSE_Key must encode"); + let encoded = cose_key_to_vec(&cose_key).expect("fixture COSE_Key must encode"); let decoded = cose_key_from_slice(&encoded).expect("canonical COSE_Key must decode"); assert_eq!( @@ -174,7 +229,9 @@ fn malformed_cose_key_map_with_wrong_label_type_is_rejected() { Value::Text("EC2".to_owned()), )])); - let err = cose_key_from_slice(&malformed).expect_err("wrong kty type must be rejected"); + let err = cose_key_from_slice(&malformed) + .err() + .expect("wrong kty type must be rejected"); assert_eq!(err, CoseError::UnsupportedAlgorithm); } @@ -197,6 +254,21 @@ fn replace_param_bytes(params: &mut [(Label, Value)], label: i64, value: Vec } } +fn assert_empty_parameters_rejected(encoded: &[u8], labels: &[i64]) { + for label in labels { + let mut raw = + RawCoseKey::from_slice(encoded).expect("fixture raw COSE_Key must decode for mutation"); + replace_param_bytes(&mut raw.params, *label, Vec::new()); + let mutated = raw.to_vec().expect("mutated fixture COSE_Key must encode"); + + assert_eq!( + cose_key_from_slice(&mutated).err(), + Some(CoseError::InvalidKeyMaterial), + "empty key parameter {label} must be rejected", + ); + } +} + fn encode_value(value: &Value) -> Vec { let mut encoded = Vec::new(); into_writer(value, Cursor::new(&mut encoded)).expect("test CBOR encoding must succeed"); diff --git a/tests/cose_suite/roundtrip_eddsa_tests.rs b/crates/cose/tests/cose_suite/roundtrip_eddsa_tests.rs similarity index 94% rename from tests/cose_suite/roundtrip_eddsa_tests.rs rename to crates/cose/tests/cose_suite/roundtrip_eddsa_tests.rs index 4bfbf81..f9845ff 100644 --- a/tests/cose_suite/roundtrip_eddsa_tests.rs +++ b/crates/cose/tests/cose_suite/roundtrip_eddsa_tests.rs @@ -15,7 +15,7 @@ fn cose_eddsa_roundtrip() { let cose = cose_sign1(k.alg, &payload, &k.private, Some(kid)).unwrap(); - let resolver = |k_: &[u8]| { + let resolver = |_, k_: &[u8]| { if k_ == kid { Some(k.public.clone()) } else { diff --git a/tests/cose_suite/roundtrip_es256k_tests.rs b/crates/cose/tests/cose_suite/roundtrip_es256k_tests.rs similarity index 94% rename from tests/cose_suite/roundtrip_es256k_tests.rs rename to crates/cose/tests/cose_suite/roundtrip_es256k_tests.rs index a9037fd..dc13004 100644 --- a/tests/cose_suite/roundtrip_es256k_tests.rs +++ b/crates/cose/tests/cose_suite/roundtrip_es256k_tests.rs @@ -15,7 +15,7 @@ fn cose_secp256k1_roundtrip() { let cose = cose_sign1(k.alg, &payload, &k.private, Some(kid)).unwrap(); - let resolver = |k_: &[u8]| { + let resolver = |_, k_: &[u8]| { if k_ == kid { Some(k.public.clone()) } else { diff --git a/tests/cose_suite/roundtrip_p256_tests.rs b/crates/cose/tests/cose_suite/roundtrip_p256_tests.rs similarity index 96% rename from tests/cose_suite/roundtrip_p256_tests.rs rename to crates/cose/tests/cose_suite/roundtrip_p256_tests.rs index 946590e..140da07 100644 --- a/tests/cose_suite/roundtrip_p256_tests.rs +++ b/crates/cose/tests/cose_suite/roundtrip_p256_tests.rs @@ -31,7 +31,7 @@ fn cose_ec2_roundtrip(k: &TestKey) { let cose = cose_sign1(k.alg, &payload, &k.private, Some(kid)).unwrap(); - let resolver = |k_: &[u8]| { + let resolver = |_, k_: &[u8]| { if k_ == kid { Some(k.public.clone()) } else { diff --git a/crates/cose/tests/cose_suite/sign1_semantic_tests.rs b/crates/cose/tests/cose_suite/sign1_semantic_tests.rs new file mode 100644 index 0000000..64b00a7 --- /dev/null +++ b/crates/cose/tests/cose_suite/sign1_semantic_tests.rs @@ -0,0 +1,396 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use buffa::{EnumValue, Message}; +use reallyme_cose::wire::cose_error_proto; +use reallyme_cose::wire::cose_operation_request::Operation; +use reallyme_cose::wire::{ + decode_cose_error, execute_operation_proto, execute_operation_proto_json, CoseErrorReason, + CoseOperationRequest, CoseSign1CreateDetachedRequest, CoseSign1CreateRequest, + CoseSign1CreateResult, CoseSign1Options, CoseSign1VerifyDetachedRequest, + CoseSign1VerifyRequest, CoseSign1VerifyResult, CoseSignatureAlgorithm, +}; +use reallyme_cose::{ + cose_sign1_detached_with_options_and_external_aad, cose_sign1_with_options_and_external_aad, + cose_verify1_detached_with_policy_and_external_aad, cose_verify1_with_policy_and_external_aad, + Algorithm, CoseError, CosePolicy, CoseSign1EncodeOptions, +}; +use zeroize::Zeroizing; + +use super::support::{ + decode_operation_output, gen_ed25519, sample_payload, test_kid, OperationOutputStatus, +}; + +const EXTERNAL_AAD: &[u8] = b"reallyme-cose/sign1/external-aad"; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum ErrorBranch { + Primitive, + Provider, + Backend, +} + +#[test] +fn sign1_operations_match_native_binary_and_proto_json() { + let mut fixture = gen_ed25519(); + let public_key = Zeroizing::new(core::mem::take(&mut fixture.public)); + let private_key = Zeroizing::new(core::mem::take(&mut fixture.private)); + let payload = Zeroizing::new(sample_payload()); + let options = CoseSign1EncodeOptions::tagged(); + + let native_attached = cose_sign1_with_options_and_external_aad( + fixture.alg, + &payload, + &private_key, + Some(test_kid()), + EXTERNAL_AAD, + options, + ) + .expect("native attached signing must succeed"); + let wire_attached = execute_create(Operation::Sign1Create(Box::new(create_request( + &payload, + &private_key, + true, + )))); + assert_eq!(wire_attached.as_slice(), native_attached.as_slice()); + + let native_detached = cose_sign1_detached_with_options_and_external_aad( + fixture.alg, + &payload, + &private_key, + Some(test_kid()), + EXTERNAL_AAD, + options, + ) + .expect("native detached signing must succeed"); + let wire_detached = execute_create(Operation::Sign1CreateDetached(Box::new( + create_detached_request(&payload, &private_key, true), + ))); + assert_eq!(wire_detached.as_slice(), native_detached.as_slice()); + + let policy = verification_policy(vec![Algorithm::Ed25519]); + let native_verified = cose_verify1_with_policy_and_external_aad( + &native_attached, + EXTERNAL_AAD, + &policy, + |_, _| Some(public_key.to_vec()), + ) + .expect("native attached verification must succeed"); + let wire_verified = execute_verify(Operation::Sign1Verify(Box::new(verify_request( + &native_attached, + &public_key, + test_kid(), + vec![CoseSignatureAlgorithm::Ed25519], + )))); + assert_eq!(wire_verified.payload, native_verified.payload.as_slice()); + assert_eq!( + wire_verified.algorithm.as_known(), + Some(CoseSignatureAlgorithm::Ed25519) + ); + assert_eq!(wire_verified.kid, native_verified.kid.as_slice()); + + let native_detached_verified = cose_verify1_detached_with_policy_and_external_aad( + &native_detached, + &payload, + EXTERNAL_AAD, + &policy, + |_, _| Some(public_key.to_vec()), + ) + .expect("native detached verification must succeed"); + let wire_detached_verified = execute_verify(Operation::Sign1VerifyDetached(Box::new( + verify_detached_request( + &native_detached, + &payload, + &public_key, + test_kid(), + vec![CoseSignatureAlgorithm::Ed25519], + ), + ))); + assert!(wire_detached_verified.payload.is_empty()); + assert_eq!( + wire_detached_verified.algorithm.as_known(), + Some(CoseSignatureAlgorithm::Ed25519) + ); + assert_eq!( + wire_detached_verified.kid, + native_detached_verified.kid.as_slice() + ); +} + +#[test] +fn sign1_failures_preserve_native_and_exact_wire_semantics() { + let mut fixture = gen_ed25519(); + let public_key = Zeroizing::new(core::mem::take(&mut fixture.public)); + let private_key = Zeroizing::new(core::mem::take(&mut fixture.private)); + let payload = Zeroizing::new(sample_payload()); + let invalid_private_key = Zeroizing::new(vec![0_u8; 31]); + + assert_native_error( + cose_sign1_with_options_and_external_aad( + fixture.alg, + &payload, + &invalid_private_key, + Some(test_kid()), + EXTERNAL_AAD, + CoseSign1EncodeOptions::tagged(), + ), + CoseError::InvalidKeyMaterial, + ); + assert_error( + Operation::Sign1Create(Box::new(create_request( + &payload, + &invalid_private_key, + true, + ))), + ErrorBranch::Primitive, + CoseErrorReason::KeyInvalidKeyMaterial, + ); + + let detached = cose_sign1_detached_with_options_and_external_aad( + fixture.alg, + &payload, + &private_key, + Some(test_kid()), + EXTERNAL_AAD, + CoseSign1EncodeOptions::tagged(), + ) + .expect("detached fixture must sign"); + let policy = verification_policy(vec![Algorithm::Ed25519]); + assert_native_error( + cose_verify1_with_policy_and_external_aad(&detached, EXTERNAL_AAD, &policy, |_, _| { + Some(public_key.to_vec()) + }), + CoseError::MissingPayload, + ); + assert_error( + Operation::Sign1Verify(Box::new(verify_request( + &detached, + &public_key, + test_kid(), + vec![CoseSignatureAlgorithm::Ed25519], + ))), + ErrorBranch::Primitive, + CoseErrorReason::Sign1MissingPayload, + ); + + let wrong_payload = Zeroizing::new(b"wrong detached payload".to_vec()); + assert_native_error( + cose_verify1_detached_with_policy_and_external_aad( + &detached, + &wrong_payload, + EXTERNAL_AAD, + &policy, + |_, _| Some(public_key.to_vec()), + ), + CoseError::InvalidSignature, + ); + assert_error( + Operation::Sign1VerifyDetached(Box::new(verify_detached_request( + &detached, + &wrong_payload, + &public_key, + test_kid(), + vec![CoseSignatureAlgorithm::Ed25519], + ))), + ErrorBranch::Primitive, + CoseErrorReason::Sign1InvalidSignature, + ); + + assert_error( + Operation::Sign1VerifyDetached(Box::new(verify_detached_request( + &detached, + &payload, + &public_key, + b"different-kid", + vec![CoseSignatureAlgorithm::Ed25519], + ))), + ErrorBranch::Primitive, + CoseErrorReason::Sign1KidKeyMismatch, + ); + + let disallowed_policy = verification_policy(vec![Algorithm::P256]); + assert_native_error( + cose_verify1_detached_with_policy_and_external_aad( + &detached, + &payload, + EXTERNAL_AAD, + &disallowed_policy, + |_, _| Some(public_key.to_vec()), + ), + CoseError::UnsupportedAlgorithm, + ); + assert_error( + Operation::Sign1VerifyDetached(Box::new(verify_detached_request( + &detached, + &payload, + &public_key, + test_kid(), + vec![CoseSignatureAlgorithm::EcdsaP256Sha256], + ))), + ErrorBranch::Provider, + CoseErrorReason::CommonUnsupportedAlgorithm, + ); +} + +fn create_request(payload: &[u8], private_key: &[u8], tagged: bool) -> CoseSign1CreateRequest { + CoseSign1CreateRequest { + algorithm: EnumValue::from(CoseSignatureAlgorithm::Ed25519), + payload: payload.to_vec(), + private_key: private_key.to_vec(), + kid: test_kid().to_vec(), + has_kid: true, + options: sign_options(tagged), + external_aad: EXTERNAL_AAD.to_vec(), + __buffa_unknown_fields: Default::default(), + } +} + +fn create_detached_request( + payload: &[u8], + private_key: &[u8], + tagged: bool, +) -> CoseSign1CreateDetachedRequest { + CoseSign1CreateDetachedRequest { + algorithm: EnumValue::from(CoseSignatureAlgorithm::Ed25519), + payload: payload.to_vec(), + private_key: private_key.to_vec(), + kid: test_kid().to_vec(), + has_kid: true, + options: sign_options(tagged), + external_aad: EXTERNAL_AAD.to_vec(), + __buffa_unknown_fields: Default::default(), + } +} + +fn sign_options( + tagged: bool, +) -> buffa::MessageField> { + buffa::MessageField::some(CoseSign1Options { + tag: tagged, + max_cose_sign1_bytes: 0, + __buffa_unknown_fields: Default::default(), + }) +} + +fn verify_request( + cose_sign1: &[u8], + public_key: &[u8], + expected_kid: &[u8], + allowed_algorithms: Vec, +) -> CoseSign1VerifyRequest { + CoseSign1VerifyRequest { + cose_sign1: cose_sign1.to_vec(), + public_key: public_key.to_vec(), + max_cose_sign1_bytes: 0, + max_detached_payload_bytes: 0, + require_kid: true, + allowed_algorithms: allowed_algorithms + .into_iter() + .map(EnumValue::from) + .collect(), + external_aad: EXTERNAL_AAD.to_vec(), + expected_kid: expected_kid.to_vec(), + __buffa_unknown_fields: Default::default(), + } +} + +fn verify_detached_request( + cose_sign1: &[u8], + payload: &[u8], + public_key: &[u8], + expected_kid: &[u8], + allowed_algorithms: Vec, +) -> CoseSign1VerifyDetachedRequest { + CoseSign1VerifyDetachedRequest { + cose_sign1: cose_sign1.to_vec(), + payload: payload.to_vec(), + public_key: public_key.to_vec(), + max_cose_sign1_bytes: 0, + max_detached_payload_bytes: 0, + require_kid: true, + allowed_algorithms: allowed_algorithms + .into_iter() + .map(EnumValue::from) + .collect(), + external_aad: EXTERNAL_AAD.to_vec(), + expected_kid: expected_kid.to_vec(), + __buffa_unknown_fields: Default::default(), + } +} + +fn verification_policy(allowed_algorithms: Vec) -> CosePolicy { + CosePolicy::new() + .with_require_kid(true) + .with_allowed_algorithms(allowed_algorithms) +} + +fn execute_create(operation: Operation) -> Zeroizing> { + let payload = execute(operation); + let mut result = CoseSign1CreateResult::decode_from_slice(&payload) + .expect("Sign1 create result must decode"); + Zeroizing::new(core::mem::take(&mut result.cose_sign1)) +} + +fn execute_verify(operation: Operation) -> CoseSign1VerifyResult { + CoseSign1VerifyResult::decode_from_slice(&execute(operation)) + .expect("Sign1 verify result must decode") +} + +fn execute(operation: Operation) -> Zeroizing> { + let request = operation_request(operation); + let binary_request = Zeroizing::new(request.encode_to_vec()); + let json_request = Zeroizing::new( + serde_json::to_string(&request).expect("generated Sign1 ProtoJSON must encode"), + ); + let binary_envelope = execute_operation_proto(&binary_request); + let json_envelope = execute_operation_proto_json(&json_request); + assert_eq!(binary_envelope.as_slice(), json_envelope.as_slice()); + let output = decode_operation_output(&binary_envelope) + .ok() + .expect("successful Sign1 envelope must decode"); + assert_eq!(output.status(), OperationOutputStatus::Result); + Zeroizing::new(output.bytes().to_vec()) +} + +fn assert_error(operation: Operation, expected_branch: ErrorBranch, expected: CoseErrorReason) { + let request = operation_request(operation); + let binary_request = Zeroizing::new(request.encode_to_vec()); + let json_request = Zeroizing::new( + serde_json::to_string(&request).expect("generated Sign1 ProtoJSON must encode"), + ); + let binary_envelope = execute_operation_proto(&binary_request); + let json_envelope = execute_operation_proto_json(&json_request); + assert_eq!(binary_envelope.as_slice(), json_envelope.as_slice()); + let output = match decode_operation_output(&binary_envelope) { + Ok(output) | Err(output) => output, + }; + assert_eq!(output.status(), OperationOutputStatus::CoseError); + let error = decode_cose_error(output.bytes()).expect("structured Sign1 error must decode"); + let Some(branch) = error.error.as_ref() else { + assert!(error.error.is_some(), "structured error branch must exist"); + return; + }; + let (branch, reason) = match branch { + cose_error_proto::Error::Primitive(error) => { + (ErrorBranch::Primitive, error.reason.as_known()) + } + cose_error_proto::Error::Provider(error) => { + (ErrorBranch::Provider, error.reason.as_known()) + } + cose_error_proto::Error::Backend(error) => (ErrorBranch::Backend, error.reason.as_known()), + }; + assert_eq!(branch, expected_branch); + assert_eq!(reason, Some(expected)); +} + +fn assert_native_error(result: Result, expected: CoseError) { + assert!(matches!(result, Err(error) if error == expected)); +} + +fn operation_request(operation: Operation) -> CoseOperationRequest { + CoseOperationRequest { + operation: Some(operation), + __buffa_unknown_fields: Default::default(), + } +} diff --git a/tests/cose_suite/signing_algorithm_coverage_tests.rs b/crates/cose/tests/cose_suite/signing_algorithm_coverage_tests.rs similarity index 72% rename from tests/cose_suite/signing_algorithm_coverage_tests.rs rename to crates/cose/tests/cose_suite/signing_algorithm_coverage_tests.rs index 69e58cf..6d11aa3 100644 --- a/tests/cose_suite/signing_algorithm_coverage_tests.rs +++ b/crates/cose/tests/cose_suite/signing_algorithm_coverage_tests.rs @@ -7,7 +7,8 @@ use coset::{CborSerializable, CoseSign1}; use reallyme_cose::{cose_sign1, cose_sign1_detached, cose_verify1, cose_verify1_detached}; use crate::support::{ - gen_ed25519, gen_p256, gen_p384, gen_p521, gen_secp256k1, sample_payload, TestKey, + gen_ed25519, gen_mldsa44, gen_mldsa65, gen_mldsa87, gen_p256, gen_p384, gen_p521, + gen_secp256k1, sample_payload, TestKey, }; type SigningCase = (&'static str, fn() -> TestKey, usize); @@ -16,12 +17,15 @@ type SigningCase = (&'static str, fn() -> TestKey, usize); fn all_supported_signing_algorithms_roundtrip_attached_and_detached() { // Expected signature widths are the RFC 9053 / RFC 8032 fixed encodings: // Ed25519 = 64, ECDSA = 2 * coordinate width. - let cases: [SigningCase; 5] = [ - ("EdDSA", gen_ed25519, 64), + let cases: [SigningCase; 8] = [ + ("Ed25519", gen_ed25519, 64), ("ES256", gen_p256, 64), ("ES384", gen_p384, 96), ("ES512", gen_p521, 132), ("ES256K", gen_secp256k1, 64), + ("ML-DSA-44", gen_mldsa44, 2_420), + ("ML-DSA-65", gen_mldsa65, 3_309), + ("ML-DSA-87", gen_mldsa87, 4_627), ]; for (label, make_key, expected_signature_len) in cases { @@ -36,7 +40,7 @@ fn all_supported_signing_algorithms_roundtrip_attached_and_detached() { expected_signature_len, "signature width for {label}" ); - let attached_payload = cose_verify1(&attached, |requested_kid| { + let attached_payload = cose_verify1(&attached, |_, requested_kid| { if requested_kid == kid { Some(key.public.clone()) } else { @@ -44,10 +48,14 @@ fn all_supported_signing_algorithms_roundtrip_attached_and_detached() { } }) .unwrap(); - assert_eq!(attached_payload, payload, "attached COSE_Sign1 {label}"); + assert_eq!( + attached_payload.as_slice(), + payload.as_slice(), + "attached COSE_Sign1 {label}" + ); let detached = cose_sign1_detached(key.alg, &payload, &key.private, Some(kid)).unwrap(); - cose_verify1_detached(&detached, &payload, |requested_kid| { + cose_verify1_detached(&detached, &payload, |_, requested_kid| { if requested_kid == kid { Some(key.public.clone()) } else { diff --git a/crates/cose/tests/cose_suite/support.rs b/crates/cose/tests/cose_suite/support.rs new file mode 100644 index 0000000..2869110 --- /dev/null +++ b/crates/cose/tests/cose_suite/support.rs @@ -0,0 +1,248 @@ +#![allow(missing_docs, clippy::expect_used, clippy::unwrap_used)] +#![allow(dead_code)] +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +use reallyme_cose::Algorithm; +use reallyme_crypto::dispatch::generate_keypair; + +#[cfg(feature = "wire")] +use buffa::Message; +#[cfg(feature = "wire")] +use reallyme_cose::wire::{ + cose_operation_response_v2, cose_operation_result, decode_operation_response, + execute_operation_proto, execute_operation_proto_json, +}; + +#[derive(Debug)] +pub struct TestKey { + pub alg: Algorithm, + pub public: Vec, + pub private: Vec, +} + +pub fn gen_ed25519() -> TestKey { + let (public, private) = generate_keypair(Algorithm::Ed25519).unwrap(); + + TestKey { + alg: Algorithm::Ed25519, + public, + private: private.to_vec(), + } +} + +pub fn gen_p256() -> TestKey { + let (public, private) = generate_keypair(Algorithm::P256).unwrap(); + + TestKey { + alg: Algorithm::P256, + public, + private: private.to_vec(), + } +} + +pub fn gen_p384() -> TestKey { + let (public, private) = generate_keypair(Algorithm::P384).unwrap(); + + TestKey { + alg: Algorithm::P384, + public, + private: private.to_vec(), + } +} + +pub fn gen_p521() -> TestKey { + let (public, private) = generate_keypair(Algorithm::P521).unwrap(); + + TestKey { + alg: Algorithm::P521, + public, + private: private.to_vec(), + } +} + +pub fn gen_secp256k1() -> TestKey { + let (public, private) = generate_keypair(Algorithm::Secp256k1).unwrap(); + + TestKey { + alg: Algorithm::Secp256k1, + public, + private: private.to_vec(), + } +} + +pub fn gen_x25519() -> TestKey { + let (public, private) = generate_keypair(Algorithm::X25519).unwrap(); + + TestKey { + alg: Algorithm::X25519, + public, + private: private.to_vec(), + } +} + +pub fn gen_mldsa87() -> TestKey { + let (public, private) = generate_keypair(Algorithm::MlDsa87).unwrap(); + + TestKey { + alg: Algorithm::MlDsa87, + public, + private: private.to_vec(), + } +} + +pub fn gen_mldsa44() -> TestKey { + let (public, private) = generate_keypair(Algorithm::MlDsa44).unwrap(); + + TestKey { + alg: Algorithm::MlDsa44, + public, + private: private.to_vec(), + } +} + +pub fn gen_mldsa65() -> TestKey { + let (public, private) = generate_keypair(Algorithm::MlDsa65).unwrap(); + + TestKey { + alg: Algorithm::MlDsa65, + public, + private: private.to_vec(), + } +} + +pub fn gen_mlkem512() -> TestKey { + let (public, private) = generate_keypair(Algorithm::MlKem512).unwrap(); + + TestKey { + alg: Algorithm::MlKem512, + public, + private: private.to_vec(), + } +} + +pub fn gen_mlkem768() -> TestKey { + let (public, private) = generate_keypair(Algorithm::MlKem768).unwrap(); + + TestKey { + alg: Algorithm::MlKem768, + public, + private: private.to_vec(), + } +} + +pub fn gen_mlkem1024() -> TestKey { + let (public, private) = generate_keypair(Algorithm::MlKem1024).unwrap(); + + TestKey { + alg: Algorithm::MlKem1024, + public, + private: private.to_vec(), + } +} + +pub fn sample_payload() -> Vec { + b"hello cose world".to_vec() +} + +/// Shared test `kid` +pub fn test_kid() -> &'static [u8] { + b"test-key" +} + +#[cfg(feature = "wire")] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum OperationOutputStatus { + Result, + CoseError, +} + +#[cfg(feature = "wire")] +pub struct OperationOutput { + status: OperationOutputStatus, + bytes: Vec, +} + +#[cfg(feature = "wire")] +impl OperationOutput { + pub fn status(&self) -> OperationOutputStatus { + self.status + } + + pub fn bytes(&self) -> &[u8] { + &self.bytes + } +} + +#[cfg(feature = "wire")] +pub fn execute_binary_output(request: &[u8]) -> OperationOutput { + match decode_operation_output(&execute_operation_proto(request)) { + Ok(output) | Err(output) => output, + } +} + +#[cfg(feature = "wire")] +pub fn execute_json_output(request: &str) -> OperationOutput { + match decode_operation_output(&execute_operation_proto_json(request)) { + Ok(output) | Err(output) => output, + } +} + +#[cfg(feature = "wire")] +pub fn decode_operation_output(bytes: &[u8]) -> Result { + match decode_operation_response(bytes) { + Ok(response) => Ok(operation_output_from_response(response)), + Err(response) => Err(operation_output_from_response(response)), + } +} + +#[cfg(feature = "wire")] +fn operation_output_from_response( + mut response: reallyme_cose::wire::CoseOperationResponseV2, +) -> OperationOutput { + match response.outcome.take() { + Some(cose_operation_response_v2::Outcome::Result(result)) => OperationOutput { + status: OperationOutputStatus::Result, + bytes: encode_result_branch(*result), + }, + Some(cose_operation_response_v2::Outcome::Error(error)) => OperationOutput { + status: OperationOutputStatus::CoseError, + bytes: error.encode_to_vec(), + }, + None => OperationOutput { + status: OperationOutputStatus::CoseError, + bytes: Vec::new(), + }, + } +} + +#[cfg(feature = "wire")] +fn encode_result_branch(mut result: reallyme_cose::wire::CoseOperationResult) -> Vec { + match result.result.take() { + Some(cose_operation_result::Result::Sign1Create(message)) + | Some(cose_operation_result::Result::Sign1CreateDetached(message)) => { + message.encode_to_vec() + } + Some(cose_operation_result::Result::Sign1Verify(message)) + | Some(cose_operation_result::Result::Sign1VerifyDetached(message)) => { + message.encode_to_vec() + } + Some(cose_operation_result::Result::KeyFromPublicBytes(message)) + | Some(cose_operation_result::Result::KeyFromPrivateBytes(message)) + | Some(cose_operation_result::Result::KeyParse(message)) + | Some(cose_operation_result::Result::KeyToPublicBytes(message)) + | Some(cose_operation_result::Result::KeyToPrivateBytes(message)) + | Some(cose_operation_result::Result::KeyDerivePublicKid(message)) + | Some(cose_operation_result::Result::MultikeyToCoseKey(message)) => { + message.encode_to_vec() + } + Some(cose_operation_result::Result::KeyToMultikey(message)) => message.encode_to_vec(), + Some(cose_operation_result::Result::MlKemEncryptDirect(message)) + | Some(cose_operation_result::Result::MlKemEncryptKeyWrap(message)) => { + message.encode_to_vec() + } + Some(cose_operation_result::Result::MlKemDecrypt(message)) => message.encode_to_vec(), + None => Vec::new(), + } +} diff --git a/tests/cose_suite/tamper_reject_tests.rs b/crates/cose/tests/cose_suite/tamper_reject_tests.rs similarity index 95% rename from tests/cose_suite/tamper_reject_tests.rs rename to crates/cose/tests/cose_suite/tamper_reject_tests.rs index ac52c9e..4adc8ea 100644 --- a/tests/cose_suite/tamper_reject_tests.rs +++ b/crates/cose/tests/cose_suite/tamper_reject_tests.rs @@ -21,7 +21,7 @@ fn cose_rejects_tampered_payload() { let last = tampered.len() - 1; tampered[last] ^= 0xff; - let resolver = |k_: &[u8]| { + let resolver = |_, k_: &[u8]| { if k_ == kid { Some(k.public.clone()) } else { diff --git a/crates/cose/tests/cose_suite/wire_tests.rs b/crates/cose/tests/cose_suite/wire_tests.rs new file mode 100644 index 0000000..558f7d3 --- /dev/null +++ b/crates/cose/tests/cose_suite/wire_tests.rs @@ -0,0 +1,919 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +#![allow(clippy::panic)] + +use buffa::{EnumValue, Message}; +use coset::{CoseEncrypt, TaggedCborSerializable}; +use reallyme_cose::wire::cose_operation_request::Operation as CoseOperationRequestBranch; +use reallyme_cose::wire::{ + cose_algorithm_identifier, decode_cose_error, execute_operation_proto, + execute_operation_proto_json, CoseAlgorithmIdentifier, CoseContentEncryptionAlgorithm, + CoseErrorProto, CoseErrorReason, CoseKemAlgorithm, CoseKeyAgreementAlgorithm, + CoseKeyBytesResult, CoseKeyFromPublicBytesRequest, CoseMlKemDecryptRequest, + CoseMlKemDecryptResult, CoseMlKemEncryptRequest, CoseMlKemEncryptResult, CoseMlKemMode, + CoseOperationRequest, CosePrimitiveError, CoseSign1CreateDetachedRequest, + CoseSign1CreateRequest, CoseSign1CreateResult, CoseSign1Options, + CoseSign1VerifyDetachedRequest, CoseSign1VerifyRequest, CoseSign1VerifyResult, + CoseSignatureAlgorithm, MAX_COSE_PROTO_MESSAGE_BYTES, +}; +use reallyme_cose::{cose_key_from_public_bytes, derive_kid_from_cose_key_public}; + +use crate::support::{ + decode_operation_output, gen_ed25519, sample_payload, test_kid, OperationOutput, + OperationOutputStatus, +}; + +fn signature_value(algorithm: CoseSignatureAlgorithm) -> EnumValue { + EnumValue::from(algorithm) +} + +fn kem_value(algorithm: CoseKemAlgorithm) -> EnumValue { + EnumValue::from(algorithm) +} + +fn signature_identifier( + algorithm: CoseSignatureAlgorithm, +) -> buffa::MessageField> { + buffa::MessageField::some(CoseAlgorithmIdentifier { + algorithm: Some(cose_algorithm_identifier::Algorithm::Signature( + signature_value(algorithm), + )), + __buffa_unknown_fields: Default::default(), + }) +} + +fn key_agreement_identifier( + algorithm: CoseKeyAgreementAlgorithm, +) -> buffa::MessageField> { + buffa::MessageField::some(CoseAlgorithmIdentifier { + algorithm: Some(cose_algorithm_identifier::Algorithm::KeyAgreement( + EnumValue::from(algorithm), + )), + __buffa_unknown_fields: Default::default(), + }) +} + +fn error_reason(error: &CoseErrorProto) -> Option { + let reason = match error.error.as_ref()? { + reallyme_cose::wire::cose_error_proto::Error::Primitive(error) => error.reason, + reallyme_cose::wire::cose_error_proto::Error::Provider(error) => error.reason, + reallyme_cose::wire::cose_error_proto::Error::Backend(error) => error.reason, + }; + reason.as_known() +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum ExpectedErrorBranch { + Primitive, + Provider, + Backend, +} + +fn error_branch(error: &CoseErrorProto) -> Option { + match error.error.as_ref()? { + reallyme_cose::wire::cose_error_proto::Error::Primitive(_) => { + Some(ExpectedErrorBranch::Primitive) + } + reallyme_cose::wire::cose_error_proto::Error::Provider(_) => { + Some(ExpectedErrorBranch::Provider) + } + reallyme_cose::wire::cose_error_proto::Error::Backend(_) => { + Some(ExpectedErrorBranch::Backend) + } + } +} + +fn sign1_create_request( + algorithm: CoseSignatureAlgorithm, + payload: Vec, + private_key: Vec, + kid: Vec, + has_kid: bool, +) -> CoseSign1CreateRequest { + CoseSign1CreateRequest { + algorithm: signature_value(algorithm), + payload, + private_key, + kid, + has_kid, + options: Default::default(), + external_aad: Vec::new(), + __buffa_unknown_fields: Default::default(), + } +} + +fn sign1_create_detached_request( + algorithm: CoseSignatureAlgorithm, + payload: Vec, + private_key: Vec, + kid: Vec, + has_kid: bool, +) -> CoseSign1CreateDetachedRequest { + CoseSign1CreateDetachedRequest { + algorithm: signature_value(algorithm), + payload, + private_key, + kid, + has_kid, + options: Default::default(), + external_aad: Vec::new(), + __buffa_unknown_fields: Default::default(), + } +} + +fn sign1_verify_request( + cose_sign1: Vec, + public_key: Vec, + require_kid: bool, +) -> CoseSign1VerifyRequest { + CoseSign1VerifyRequest { + cose_sign1, + public_key, + max_cose_sign1_bytes: 0, + max_detached_payload_bytes: 0, + require_kid, + allowed_algorithms: vec![signature_value(CoseSignatureAlgorithm::Ed25519)], + external_aad: Vec::new(), + expected_kid: Vec::new(), + __buffa_unknown_fields: Default::default(), + } +} + +fn sign1_verify_detached_request( + cose_sign1: Vec, + payload: Vec, + public_key: Vec, + require_kid: bool, +) -> CoseSign1VerifyDetachedRequest { + CoseSign1VerifyDetachedRequest { + cose_sign1, + payload, + public_key, + max_cose_sign1_bytes: 0, + max_detached_payload_bytes: 0, + require_kid, + allowed_algorithms: vec![signature_value(CoseSignatureAlgorithm::Ed25519)], + external_aad: Vec::new(), + expected_kid: Vec::new(), + __buffa_unknown_fields: Default::default(), + } +} + +fn operation_request(operation: CoseOperationRequestBranch) -> CoseOperationRequest { + CoseOperationRequest { + operation: Some(operation), + __buffa_unknown_fields: Default::default(), + } +} + +fn decode_operation_envelope(bytes: &[u8]) -> OperationOutput { + let envelope = execute_operation_proto(bytes); + match decode_operation_output(&envelope) { + Ok(output) => output, + Err(output) => output, + } +} + +fn execute_operation_bytes(bytes: &[u8]) -> OperationOutput { + decode_operation_envelope(bytes) +} + +fn execute_operation_json(json: &str) -> OperationOutput { + let envelope = execute_operation_proto_json(json); + match decode_operation_output(&envelope) { + Ok(output) => output, + Err(output) => output, + } +} + +fn execute_typed_request( + bytes: &[u8], + wrap: fn(Box) -> CoseOperationRequestBranch, +) -> OperationOutput +where + M: Message, +{ + let request = match M::decode_from_slice(bytes) { + Ok(request) => request, + Err(_) => return execute_operation_bytes(bytes), + }; + execute_operation_bytes(&operation_request(wrap(Box::new(request))).encode_to_vec()) +} + +fn execute_cose_sign1_create_request(bytes: &[u8]) -> OperationOutput { + execute_typed_request(bytes, CoseOperationRequestBranch::Sign1Create) +} + +fn execute_cose_sign1_create_detached_request(bytes: &[u8]) -> OperationOutput { + execute_typed_request(bytes, CoseOperationRequestBranch::Sign1CreateDetached) +} + +fn execute_cose_sign1_verify_request(bytes: &[u8]) -> OperationOutput { + execute_typed_request(bytes, CoseOperationRequestBranch::Sign1Verify) +} + +fn execute_cose_sign1_verify_detached_request(bytes: &[u8]) -> OperationOutput { + execute_typed_request(bytes, CoseOperationRequestBranch::Sign1VerifyDetached) +} + +fn assert_error_reason(output: &OperationOutput, expected: CoseErrorReason) { + assert_eq!(output.status(), OperationOutputStatus::CoseError); + let error = match decode_cose_error(output.bytes()) { + Ok(error) => error, + Err(_) => panic!("error protobuf must decode"), + }; + assert_eq!(error_reason(&error), Some(expected)); +} + +fn assert_error_branch_and_reason( + output: &OperationOutput, + expected_branch: ExpectedErrorBranch, + expected_reason: CoseErrorReason, +) { + assert_eq!(output.status(), OperationOutputStatus::CoseError); + let error = match decode_cose_error(output.bytes()) { + Ok(error) => error, + Err(_) => panic!("error protobuf must decode"), + }; + assert_eq!(error_branch(&error), Some(expected_branch)); + assert_eq!(error_reason(&error), Some(expected_reason)); +} + +#[test] +fn cose_error_oneof_wire_bytes_are_stable() { + let cases = [ + ( + reallyme_cose::wire::cose_error_proto::Error::Primitive(Box::new(CosePrimitiveError { + reason: EnumValue::from(CoseErrorReason::Sign1InvalidSignature), + __buffa_unknown_fields: Default::default(), + })), + &[0x0a, 0x03, 0x08, 0xa4, 0x03][..], + ), + ( + reallyme_cose::wire::cose_error_proto::Error::Provider(Box::new( + reallyme_cose::wire::CoseProviderError { + reason: EnumValue::from(CoseErrorReason::CommonUnsupportedAlgorithm), + __buffa_unknown_fields: Default::default(), + }, + )), + &[0x12, 0x03, 0x08, 0xc8, 0x01][..], + ), + ( + reallyme_cose::wire::cose_error_proto::Error::Backend(Box::new( + reallyme_cose::wire::CoseBackendError { + reason: EnumValue::from(CoseErrorReason::BackendInternal), + __buffa_unknown_fields: Default::default(), + }, + )), + &[0x1a, 0x03, 0x08, 0xad, 0x02][..], + ), + ]; + for (branch, expected) in cases { + let error = CoseErrorProto { + error: Some(branch), + __buffa_unknown_fields: Default::default(), + }; + assert_eq!(error.encode_to_vec(), expected); + } +} + +#[test] +fn sign1_wire_attached_happy_path_round_trips() { + let key = gen_ed25519(); + let payload = sample_payload(); + let create = sign1_create_request( + CoseSignatureAlgorithm::Ed25519, + payload.clone(), + key.private, + test_kid().to_vec(), + true, + ); + let signed = execute_cose_sign1_create_request(&create.encode_to_vec()); + assert_eq!(signed.status(), OperationOutputStatus::Result); + let create_result = CoseSign1CreateResult::decode_from_slice(signed.bytes()) + .expect("create result protobuf must decode"); + + let verify = sign1_verify_request(create_result.cose_sign1.clone(), key.public, true); + let verified = execute_cose_sign1_verify_request(&verify.encode_to_vec()); + assert_eq!(verified.status(), OperationOutputStatus::Result); + let verify_result = CoseSign1VerifyResult::decode_from_slice(verified.bytes()) + .expect("verify result protobuf must decode"); + assert_eq!(verify_result.payload, payload); + assert_eq!(verify_result.kid, test_kid()); +} + +#[test] +fn sign1_wire_external_aad_round_trips_and_wrong_aad_is_invalid_signature() { + let key = gen_ed25519(); + let payload = sample_payload(); + let external_aad = b"reallyme-cose/wire-aad/v1".to_vec(); + let mut create = sign1_create_request( + CoseSignatureAlgorithm::Ed25519, + payload.clone(), + key.private, + test_kid().to_vec(), + true, + ); + create.external_aad = external_aad.clone(); + let signed = execute_cose_sign1_create_request(&create.encode_to_vec()); + assert_eq!(signed.status(), OperationOutputStatus::Result); + let create_result = CoseSign1CreateResult::decode_from_slice(signed.bytes()) + .expect("create result protobuf must decode"); + + let mut verify = sign1_verify_request(create_result.cose_sign1.clone(), key.public, true); + verify.external_aad = external_aad; + let verified = execute_cose_sign1_verify_request(&verify.encode_to_vec()); + assert_eq!(verified.status(), OperationOutputStatus::Result); + + verify.external_aad = b"wrong-wire-aad".to_vec(); + let rejected = execute_cose_sign1_verify_request(&verify.encode_to_vec()); + assert_error_reason(&rejected, CoseErrorReason::Sign1InvalidSignature); +} + +#[test] +fn sign1_wire_expected_kid_binds_the_supplied_public_key() { + let key = gen_ed25519(); + let create = sign1_create_request( + CoseSignatureAlgorithm::Ed25519, + sample_payload(), + key.private, + test_kid().to_vec(), + true, + ); + let signed = execute_cose_sign1_create_request(&create.encode_to_vec()); + assert_eq!(signed.status(), OperationOutputStatus::Result); + let create_result = CoseSign1CreateResult::decode_from_slice(signed.bytes()) + .expect("create result protobuf must decode"); + + let mut verify = sign1_verify_request(create_result.cose_sign1.clone(), key.public, true); + verify.expected_kid = test_kid().to_vec(); + let verified = execute_cose_sign1_verify_request(&verify.encode_to_vec()); + assert_eq!(verified.status(), OperationOutputStatus::Result); + + verify.expected_kid = b"different-trusted-kid".to_vec(); + let rejected = execute_cose_sign1_verify_request(&verify.encode_to_vec()); + assert_error_branch_and_reason( + &rejected, + ExpectedErrorBranch::Primitive, + CoseErrorReason::Sign1KidKeyMismatch, + ); +} + +#[test] +fn execute_operation_proto_dispatches_attached_sign1_happy_path() { + let key = gen_ed25519(); + let payload = sample_payload(); + let create = operation_request(CoseOperationRequestBranch::Sign1Create(Box::new( + sign1_create_request( + CoseSignatureAlgorithm::Ed25519, + payload.clone(), + key.private, + test_kid().to_vec(), + true, + ), + ))); + let signed = execute_operation_bytes(&create.encode_to_vec()); + assert_eq!(signed.status(), OperationOutputStatus::Result); + let create_result = CoseSign1CreateResult::decode_from_slice(signed.bytes()) + .expect("create result protobuf must decode"); + + let verify = operation_request(CoseOperationRequestBranch::Sign1Verify(Box::new( + sign1_verify_request(create_result.cose_sign1.clone(), key.public, true), + ))); + let verified = execute_operation_bytes(&verify.encode_to_vec()); + assert_eq!(verified.status(), OperationOutputStatus::Result); + let verify_result = CoseSign1VerifyResult::decode_from_slice(verified.bytes()) + .expect("verify result protobuf must decode"); + assert_eq!(verify_result.payload, payload); + assert_eq!(verify_result.kid, test_kid()); +} + +#[test] +fn execute_operation_proto_envelope_bytes_round_trip_status_and_payload() { + let key = gen_ed25519(); + let request = operation_request(CoseOperationRequestBranch::Sign1Create(Box::new( + sign1_create_request( + CoseSignatureAlgorithm::Ed25519, + sample_payload(), + key.private, + test_kid().to_vec(), + true, + ), + ))); + + let request_bytes = request.encode_to_vec(); + let envelope_bytes = execute_operation_proto(&request_bytes); + let envelope = match decode_operation_output(envelope_bytes.as_slice()) { + Ok(envelope) => envelope, + Err(_) => panic!("result envelope protobuf must decode"), + }; + assert_eq!(envelope.status(), OperationOutputStatus::Result); + let create_result = CoseSign1CreateResult::decode_from_slice(envelope.bytes()) + .expect("enveloped create result protobuf must decode"); + assert!(!create_result.cose_sign1.is_empty()); +} + +#[test] +fn proto_json_and_binary_operation_match_for_generated_dispatcher() { + let key = gen_ed25519(); + let request = operation_request(CoseOperationRequestBranch::Sign1Create(Box::new( + sign1_create_request( + CoseSignatureAlgorithm::Ed25519, + sample_payload(), + key.private, + test_kid().to_vec(), + true, + ), + ))); + + let protobuf_output = execute_operation_bytes(&request.encode_to_vec()); + let json = serde_json::to_string(&request).expect("operation JSON must encode"); + let json_output = execute_operation_json(&json); + assert_eq!(protobuf_output.status(), OperationOutputStatus::Result); + assert_eq!(json_output.status(), OperationOutputStatus::Result); + assert_eq!(protobuf_output.bytes(), json_output.bytes()); +} + +#[test] +fn execute_operation_proto_missing_operation_returns_structured_error() { + let request = CoseOperationRequest { + operation: None, + __buffa_unknown_fields: Default::default(), + }; + let output = execute_operation_bytes(&request.encode_to_vec()); + assert_error_branch_and_reason( + &output, + ExpectedErrorBranch::Primitive, + CoseErrorReason::CommonInvalidParameter, + ); +} + +#[test] +fn cose_key_dispatch_accepts_family_scoped_signature_and_key_agreement_selectors() { + let ed25519 = gen_ed25519(); + let ed25519_request = CoseKeyFromPublicBytesRequest { + algorithm: signature_identifier(CoseSignatureAlgorithm::Ed25519), + public_key: ed25519.public, + __buffa_unknown_fields: Default::default(), + }; + let ed25519_output = execute_operation_bytes( + &operation_request(CoseOperationRequestBranch::KeyFromPublicBytes(Box::new( + ed25519_request, + ))) + .encode_to_vec(), + ); + assert_eq!(ed25519_output.status(), OperationOutputStatus::Result); + let ed25519_result = CoseKeyBytesResult::decode_from_slice(ed25519_output.bytes()) + .expect("Ed25519 COSE_Key result must decode"); + assert!(!ed25519_result.key_bytes.is_empty()); + + let (x25519_public, _) = reallyme_crypto::x25519::generate_x25519_keypair() + .expect("X25519 test keypair generation must succeed"); + let x25519_request = CoseKeyFromPublicBytesRequest { + algorithm: key_agreement_identifier(CoseKeyAgreementAlgorithm::X25519), + public_key: x25519_public, + __buffa_unknown_fields: Default::default(), + }; + let x25519_output = execute_operation_bytes( + &operation_request(CoseOperationRequestBranch::KeyFromPublicBytes(Box::new( + x25519_request, + ))) + .encode_to_vec(), + ); + assert_eq!(x25519_output.status(), OperationOutputStatus::Result); + let x25519_result = CoseKeyBytesResult::decode_from_slice(x25519_output.bytes()) + .expect("X25519 COSE_Key result must decode"); + assert!(!x25519_result.key_bytes.is_empty()); +} + +#[test] +fn binary_proto_decoder_rejects_unknown_length_delimited_fields() { + let mut bytes = CoseOperationRequest::default().encode_to_vec(); + // Field 100, wire type 2, followed by bytes that must not be retained in + // generated UnknownFields storage. + bytes.extend_from_slice(&[0xa2, 0x06, 0x06]); + bytes.extend_from_slice(b"secret"); + + let output = execute_operation_bytes(&bytes); + assert_error_reason(&output, CoseErrorReason::CommonMalformedProtobuf); +} + +#[test] +fn ml_kem_wire_binary_and_proto_json_paths_preserve_authenticated_metadata() { + const PLAINTEXT: &[u8] = b"protobuf operation ML-KEM plaintext"; + const EXTERNAL_AAD: &[u8] = b"protobuf operation external aad"; + + for expected_mode in [CoseMlKemMode::Direct, CoseMlKemMode::KeyWrap] { + let (public_key, private_key) = reallyme_crypto::ml_kem_512::generate_ml_kem_512_keypair() + .expect("ML-KEM-512 key generation"); + let cose_key = + cose_key_from_public_bytes(reallyme_crypto::core::Algorithm::MlKem512, &public_key) + .expect("ML-KEM public COSE_Key conversion"); + let kid = derive_kid_from_cose_key_public(&cose_key).expect("ML-KEM public kid derivation"); + let encrypt = CoseMlKemEncryptRequest { + kem_algorithm: kem_value(CoseKemAlgorithm::MlKem512), + content_algorithm: EnumValue::from(CoseContentEncryptionAlgorithm::Aes128Gcm), + recipient_public_key: public_key, + recipient_kid: kid.to_vec(), + plaintext: PLAINTEXT.to_vec(), + external_aad: EXTERNAL_AAD.to_vec(), + supp_priv_info: b"private-context".to_vec(), + has_supp_priv_info: true, + __buffa_unknown_fields: Default::default(), + }; + let operation = if expected_mode == CoseMlKemMode::Direct { + CoseOperationRequestBranch::MlKemEncryptDirect(Box::new(encrypt)) + } else { + CoseOperationRequestBranch::MlKemEncryptKeyWrap(Box::new(encrypt)) + }; + let request = operation_request(operation); + let encrypted = if expected_mode == CoseMlKemMode::Direct { + execute_operation_bytes(&request.encode_to_vec()) + } else { + let json = serde_json::to_string(&request).expect("ML-KEM request JSON must encode"); + execute_operation_json(&json) + }; + assert_eq!(encrypted.status(), OperationOutputStatus::Result); + let mut encrypted_result = CoseMlKemEncryptResult::decode_from_slice(encrypted.bytes()) + .expect("ML-KEM encrypt result must decode"); + + let decrypt = CoseMlKemDecryptRequest { + cose_encrypt: core::mem::take(&mut encrypted_result.cose_encrypt), + recipient_private_key: private_key.to_vec(), + expected_recipient_kid: kid.to_vec(), + external_aad: EXTERNAL_AAD.to_vec(), + supp_priv_info: b"private-context".to_vec(), + has_supp_priv_info: true, + __buffa_unknown_fields: Default::default(), + }; + let decrypted = execute_operation_bytes( + &operation_request(CoseOperationRequestBranch::MlKemDecrypt(Box::new(decrypt))) + .encode_to_vec(), + ); + assert_eq!(decrypted.status(), OperationOutputStatus::Result); + let decrypted_result = CoseMlKemDecryptResult::decode_from_slice(decrypted.bytes()) + .expect("ML-KEM decrypt result must decode"); + assert_eq!(decrypted_result.plaintext, PLAINTEXT); + assert_eq!( + decrypted_result.content_algorithm.as_known(), + Some(CoseContentEncryptionAlgorithm::Aes128Gcm) + ); + assert_eq!( + decrypted_result.kem_algorithm.as_known(), + Some(CoseKemAlgorithm::MlKem512) + ); + assert_eq!(decrypted_result.mode.as_known(), Some(expected_mode)); + assert_eq!(decrypted_result.recipient_kid.as_slice(), kid.as_slice()); + } +} + +#[test] +fn ml_kem_wire_preserves_private_key_to_kid_mismatch() { + let (public_key, _) = reallyme_crypto::ml_kem_512::generate_ml_kem_512_keypair() + .expect("ML-KEM-512 recipient key generation"); + let (_, wrong_private_key) = reallyme_crypto::ml_kem_512::generate_ml_kem_512_keypair() + .expect("ML-KEM-512 wrong key generation"); + let cose_key = + cose_key_from_public_bytes(reallyme_crypto::core::Algorithm::MlKem512, &public_key) + .expect("ML-KEM public COSE_Key conversion"); + let kid = derive_kid_from_cose_key_public(&cose_key).expect("ML-KEM public kid derivation"); + let encrypt = CoseMlKemEncryptRequest { + kem_algorithm: kem_value(CoseKemAlgorithm::MlKem512), + content_algorithm: EnumValue::from(CoseContentEncryptionAlgorithm::Aes128Gcm), + recipient_public_key: public_key, + recipient_kid: kid.to_vec(), + plaintext: b"key binding".to_vec(), + external_aad: Vec::new(), + supp_priv_info: Vec::new(), + has_supp_priv_info: false, + __buffa_unknown_fields: Default::default(), + }; + let encrypted = execute_operation_bytes( + &operation_request(CoseOperationRequestBranch::MlKemEncryptDirect(Box::new( + encrypt, + ))) + .encode_to_vec(), + ); + assert_eq!(encrypted.status(), OperationOutputStatus::Result); + let mut encrypted_result = CoseMlKemEncryptResult::decode_from_slice(encrypted.bytes()) + .expect("ML-KEM encrypt result must decode"); + let decrypt = CoseMlKemDecryptRequest { + cose_encrypt: core::mem::take(&mut encrypted_result.cose_encrypt), + recipient_private_key: wrong_private_key.to_vec(), + expected_recipient_kid: kid.to_vec(), + external_aad: Vec::new(), + supp_priv_info: Vec::new(), + has_supp_priv_info: false, + __buffa_unknown_fields: Default::default(), + }; + let output = execute_operation_bytes( + &operation_request(CoseOperationRequestBranch::MlKemDecrypt(Box::new(decrypt))) + .encode_to_vec(), + ); + + assert_error_branch_and_reason( + &output, + ExpectedErrorBranch::Primitive, + CoseErrorReason::EncryptKidMismatch, + ); +} + +#[test] +fn ml_kem_wire_preserves_encrypt_specific_missing_kid_reason() { + let (public_key, _) = reallyme_crypto::ml_kem_512::generate_ml_kem_512_keypair() + .expect("ML-KEM-512 recipient key generation"); + let request = CoseMlKemEncryptRequest { + kem_algorithm: kem_value(CoseKemAlgorithm::MlKem512), + content_algorithm: EnumValue::from(CoseContentEncryptionAlgorithm::Aes128Gcm), + recipient_public_key: public_key, + recipient_kid: Vec::new(), + plaintext: b"missing kid".to_vec(), + external_aad: Vec::new(), + supp_priv_info: Vec::new(), + has_supp_priv_info: false, + __buffa_unknown_fields: Default::default(), + }; + let output = execute_operation_bytes( + &operation_request(CoseOperationRequestBranch::MlKemEncryptDirect(Box::new( + request, + ))) + .encode_to_vec(), + ); + + assert_error_branch_and_reason( + &output, + ExpectedErrorBranch::Primitive, + CoseErrorReason::EncryptMissingKid, + ); +} + +#[test] +fn ml_kem_wire_preserves_encrypt_specific_unprotected_header_reason() { + let (public_key, private_key) = reallyme_crypto::ml_kem_512::generate_ml_kem_512_keypair() + .expect("ML-KEM-512 recipient key generation"); + let cose_key = + cose_key_from_public_bytes(reallyme_crypto::core::Algorithm::MlKem512, &public_key) + .expect("ML-KEM public COSE_Key conversion"); + let kid = derive_kid_from_cose_key_public(&cose_key).expect("ML-KEM public kid derivation"); + let encrypt = CoseMlKemEncryptRequest { + kem_algorithm: kem_value(CoseKemAlgorithm::MlKem512), + content_algorithm: EnumValue::from(CoseContentEncryptionAlgorithm::Aes128Gcm), + recipient_public_key: public_key, + recipient_kid: kid.to_vec(), + plaintext: b"protected algorithm".to_vec(), + external_aad: Vec::new(), + supp_priv_info: Vec::new(), + has_supp_priv_info: false, + __buffa_unknown_fields: Default::default(), + }; + let encrypted = execute_operation_bytes( + &operation_request(CoseOperationRequestBranch::MlKemEncryptDirect(Box::new( + encrypt, + ))) + .encode_to_vec(), + ); + assert_eq!(encrypted.status(), OperationOutputStatus::Result); + let encrypted_result = CoseMlKemEncryptResult::decode_from_slice(encrypted.bytes()) + .expect("ML-KEM encrypt result must decode"); + let mut malformed = CoseEncrypt::from_tagged_slice(&encrypted_result.cose_encrypt) + .expect("generated COSE_Encrypt must decode"); + let recipient = malformed + .recipients + .first_mut() + .expect("generated COSE_Encrypt has one recipient"); + recipient.unprotected.alg = recipient.protected.header.alg.take(); + recipient.protected.original_data = None; + let malformed = malformed + .to_tagged_vec() + .expect("malformed COSE_Encrypt must encode"); + let decrypt = CoseMlKemDecryptRequest { + cose_encrypt: malformed, + recipient_private_key: private_key.to_vec(), + expected_recipient_kid: kid.to_vec(), + external_aad: Vec::new(), + supp_priv_info: Vec::new(), + has_supp_priv_info: false, + __buffa_unknown_fields: Default::default(), + }; + let output = execute_operation_bytes( + &operation_request(CoseOperationRequestBranch::MlKemDecrypt(Box::new(decrypt))) + .encode_to_vec(), + ); + + assert_error_branch_and_reason( + &output, + ExpectedErrorBranch::Primitive, + CoseErrorReason::EncryptUnprotectedHeaderNotAllowed, + ); +} + +#[test] +fn ml_kem_wire_rejects_unknown_content_algorithm_without_fallback() { + let (public_key, _) = reallyme_crypto::ml_kem_512::generate_ml_kem_512_keypair() + .expect("ML-KEM-512 key generation"); + let request = CoseMlKemEncryptRequest { + kem_algorithm: kem_value(CoseKemAlgorithm::MlKem512), + content_algorithm: EnumValue::from(999_999), + recipient_public_key: public_key, + recipient_kid: b"recipient".to_vec(), + plaintext: b"plaintext".to_vec(), + external_aad: Vec::new(), + supp_priv_info: Vec::new(), + has_supp_priv_info: false, + __buffa_unknown_fields: Default::default(), + }; + let output = execute_operation_bytes( + &operation_request(CoseOperationRequestBranch::MlKemEncryptDirect(Box::new( + request, + ))) + .encode_to_vec(), + ); + assert_error_branch_and_reason( + &output, + ExpectedErrorBranch::Primitive, + CoseErrorReason::CommonInvalidParameter, + ); +} + +#[test] +fn ml_kem_wire_rejects_known_but_unsupported_hybrid_without_fallback() { + let request = CoseMlKemEncryptRequest { + kem_algorithm: kem_value(CoseKemAlgorithm::XWing768), + content_algorithm: EnumValue::from(CoseContentEncryptionAlgorithm::Aes128Gcm), + recipient_public_key: Vec::new(), + recipient_kid: Vec::new(), + plaintext: Vec::new(), + external_aad: Vec::new(), + supp_priv_info: Vec::new(), + has_supp_priv_info: false, + __buffa_unknown_fields: Default::default(), + }; + let output = execute_operation_bytes( + &operation_request(CoseOperationRequestBranch::MlKemEncryptDirect(Box::new( + request, + ))) + .encode_to_vec(), + ); + assert_error_branch_and_reason( + &output, + ExpectedErrorBranch::Provider, + CoseErrorReason::CommonUnsupportedAlgorithm, + ); +} + +#[test] +fn sign1_wire_detached_rejects_wrong_payload_as_invalid_signature() { + let key = gen_ed25519(); + let create = sign1_create_detached_request( + CoseSignatureAlgorithm::Ed25519, + sample_payload(), + key.private, + test_kid().to_vec(), + true, + ); + let signed = execute_cose_sign1_create_detached_request(&create.encode_to_vec()); + assert_eq!(signed.status(), OperationOutputStatus::Result); + let create_result = CoseSign1CreateResult::decode_from_slice(signed.bytes()) + .expect("create result protobuf must decode"); + + let verify = sign1_verify_detached_request( + create_result.cose_sign1.clone(), + b"wrong payload".to_vec(), + key.public, + true, + ); + let output = execute_cose_sign1_verify_detached_request(&verify.encode_to_vec()); + assert_error_reason(&output, CoseErrorReason::Sign1InvalidSignature); +} + +#[test] +fn invalid_private_key_length_is_not_reported_as_signature_failure() { + let create = sign1_create_request( + CoseSignatureAlgorithm::Ed25519, + sample_payload(), + vec![0], + Vec::new(), + false, + ); + let output = execute_cose_sign1_create_request(&create.encode_to_vec()); + assert_error_reason(&output, CoseErrorReason::KeyInvalidKeyMaterial); +} + +#[test] +fn unknown_signature_algorithm_is_invalid_without_fallback() { + let mut request = sign1_create_request( + CoseSignatureAlgorithm::Ed25519, + sample_payload(), + vec![0; 32], + Vec::new(), + false, + ); + // The pre-release compact value is deliberately reserved. Decoding it as + // another algorithm would silently reinterpret an old wire request. + request.algorithm = EnumValue::from(1); + let output = execute_cose_sign1_create_request(&request.encode_to_vec()); + assert_eq!(output.status(), OperationOutputStatus::CoseError); + let error = match decode_cose_error(output.bytes()) { + Ok(error) => error, + Err(_) => panic!("error protobuf must decode"), + }; + assert!(matches!( + error.error, + Some(reallyme_cose::wire::cose_error_proto::Error::Primitive(_)) + )); + assert_eq!( + error_reason(&error), + Some(CoseErrorReason::CommonInvalidParameter) + ); +} + +#[test] +fn malformed_protobuf_returns_structured_error_bytes() { + let output = execute_cose_sign1_create_request(&[0xff]); + assert_error_reason(&output, CoseErrorReason::CommonMalformedProtobuf); +} + +#[test] +fn oversized_protobuf_returns_resource_limit_error() { + let oversized = vec![0_u8; MAX_COSE_PROTO_MESSAGE_BYTES + 1]; + let output = execute_cose_sign1_create_request(&oversized); + assert_error_reason(&output, CoseErrorReason::CommonResourceLimitExceeded); +} + +#[test] +fn proto_sign1_create_limit_cannot_exceed_wire_message_cap() { + let mut request = sign1_create_request( + CoseSignatureAlgorithm::Ed25519, + b"payload".to_vec(), + vec![7; 32], + Vec::new(), + false, + ); + request.options = buffa::MessageField::some(CoseSign1Options { + tag: false, + max_cose_sign1_bytes: u64::try_from(MAX_COSE_PROTO_MESSAGE_BYTES + 1).unwrap_or(u64::MAX), + __buffa_unknown_fields: Default::default(), + }); + + let output = execute_cose_sign1_create_request(&request.encode_to_vec()); + assert_error_reason(&output, CoseErrorReason::CommonResourceLimitExceeded); +} + +#[test] +fn proto_verify_limits_cannot_exceed_wire_message_cap() { + let mut attached = sign1_verify_request(Vec::new(), Vec::new(), false); + attached.max_cose_sign1_bytes = + u64::try_from(MAX_COSE_PROTO_MESSAGE_BYTES + 1).unwrap_or(u64::MAX); + + let output = execute_cose_sign1_verify_request(&attached.encode_to_vec()); + assert_error_reason(&output, CoseErrorReason::CommonResourceLimitExceeded); + + let mut detached = sign1_verify_detached_request(Vec::new(), Vec::new(), Vec::new(), false); + detached.max_detached_payload_bytes = + u64::try_from(MAX_COSE_PROTO_MESSAGE_BYTES + 1).unwrap_or(u64::MAX); + + let output = execute_cose_sign1_verify_detached_request(&detached.encode_to_vec()); + assert_error_reason(&output, CoseErrorReason::CommonResourceLimitExceeded); +} + +#[test] +fn missing_error_branch_is_not_accepted_as_structured_error() { + let response = match decode_cose_error(&[]) { + Ok(_) => panic!("empty error envelope must fail"), + Err(response) => response, + }; + let output = match decode_operation_output(&response.encode_to_vec()) { + Ok(output) | Err(output) => output, + }; + assert_error_branch_and_reason( + &output, + ExpectedErrorBranch::Primitive, + CoseErrorReason::CommonMalformedProtobuf, + ); +} + +#[test] +fn json_request_adapter_preserves_protobuf_bytes() { + let request = sign1_create_request( + CoseSignatureAlgorithm::Ed25519, + b"abc".to_vec(), + vec![7; 32], + b"kid".to_vec(), + true, + ); + let json = serde_json::to_string(&request).expect("request JSON must encode"); + assert!(json.contains("\"payload\":\"YWJj\"")); + let decoded: CoseSign1CreateRequest = + serde_json::from_str(&json).expect("request JSON must decode"); + assert_eq!(decoded.encode_to_vec(), request.encode_to_vec()); +} + +#[test] +fn operation_proto_json_rejects_unknown_nested_fields() { + let output = execute_operation_json( + r#"{"sign1Create":{"algorithm":"COSE_SIGNATURE_ALGORITHM_ED25519","payload":"","privateKey":"","kid":"","hasKid":false,"externalAad":"","private_key_typo":""}}"#, + ); + assert_error_reason(&output, CoseErrorReason::CommonMalformedJson); +} diff --git a/crates/cose/tests/test_wasm_sign1.rs b/crates/cose/tests/test_wasm_sign1.rs new file mode 100644 index 0000000..255953c --- /dev/null +++ b/crates/cose/tests/test_wasm_sign1.rs @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! WASM runtime tests for the COSE_Sign1 boundary. + +#![allow(clippy::expect_used, missing_docs)] +#![cfg(all(feature = "wasm", target_arch = "wasm32"))] + +use reallyme_cose::{cose_sign1, cose_verify1, Algorithm, CoseError}; +use wasm_bindgen_test::wasm_bindgen_test; + +// RFC 8032 test vector 1 is public test material, not a deployed secret. +const ED25519_PRIVATE_KEY: [u8; 32] = [ + 0x9d, 0x61, 0xb1, 0x9d, 0xef, 0xfd, 0x5a, 0x60, 0xba, 0x84, 0x4a, 0xf4, 0x92, 0xec, 0x2c, 0xc4, + 0x44, 0x49, 0xc5, 0x69, 0x7b, 0x32, 0x69, 0x19, 0x70, 0x3b, 0xac, 0x03, 0x1c, 0xae, 0x7f, 0x60, +]; +const ED25519_PUBLIC_KEY: [u8; 32] = [ + 0xd7, 0x5a, 0x98, 0x01, 0x82, 0xb1, 0x0a, 0xb7, 0xd5, 0x4b, 0xfe, 0xd3, 0xc9, 0x64, 0x07, 0x3a, + 0x0e, 0xe1, 0x72, 0xf3, 0xda, 0xa6, 0x23, 0x25, 0xaf, 0x02, 0x1a, 0x68, 0xf7, 0x07, 0x51, 0x1a, +]; +const KEY_ID: &[u8] = b"wasm-ed25519-rfc8032-1"; +const PAYLOAD: &[u8] = b"ReallyMe COSE wasm runtime test"; + +#[wasm_bindgen_test] +fn sign1_roundtrip_and_tamper_rejection_execute_in_wasm() { + let encoded = cose_sign1( + Algorithm::Ed25519, + PAYLOAD, + &ED25519_PRIVATE_KEY, + Some(KEY_ID), + ) + .expect("RFC 8032 key must sign in the wasm lane"); + + let verified = cose_verify1(&encoded, |_, kid| { + (kid == KEY_ID).then(|| ED25519_PUBLIC_KEY.to_vec()) + }) + .expect("matching RFC 8032 key must verify in the wasm lane"); + assert_eq!(verified.as_slice(), PAYLOAD); + + let mut tampered = encoded; + let signature_byte = tampered + .last_mut() + .expect("COSE_Sign1 encoding must contain a signature byte"); + *signature_byte ^= 0x01; + let result = cose_verify1(&tampered, |_, kid| { + (kid == KEY_ID).then(|| ED25519_PUBLIC_KEY.to_vec()) + }); + assert_eq!(result.err(), Some(CoseError::InvalidSignature)); +} diff --git a/crates/proto/Cargo.toml b/crates/proto/Cargo.toml new file mode 100644 index 0000000..3295c1e --- /dev/null +++ b/crates/proto/Cargo.toml @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "reallyme-cose-proto" +version = "0.2.0" +description = "Generated protobuf boundary types for ReallyMe COSE." +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +keywords = ["cose", "protobuf", "identity", "credentials"] +categories = ["cryptography", "encoding"] +publish = true +include = [ + "/proto/**/*.proto", + "/src/**/*.rs", + "/tests/**/*.rs", + "/Cargo.toml", + "/README.md", + "/LICENSE", + "/NOTICE", +] + +[features] +default = ["generated"] +generated = ["dep:buffa", "buffa/json", "dep:serde", "dep:zeroize"] + +[dependencies] +buffa = { workspace = true, optional = true } +serde = { workspace = true, optional = true } +zeroize = { workspace = true, optional = true } + +[dev-dependencies] +serde_json.workspace = true + +[lints] +workspace = true diff --git a/crates/proto/LICENSE b/crates/proto/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/crates/proto/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/crates/proto/NOTICE b/crates/proto/NOTICE new file mode 100644 index 0000000..da056af --- /dev/null +++ b/crates/proto/NOTICE @@ -0,0 +1,4 @@ +ReallyMe COSE +Copyright © 2026 ReallyMe LLC. All rights reserved. + +This product includes software developed by ReallyMe LLC. diff --git a/crates/proto/README.md b/crates/proto/README.md new file mode 100644 index 0000000..32b5292 --- /dev/null +++ b/crates/proto/README.md @@ -0,0 +1,51 @@ + + +# reallyme-cose-proto + +Generated protobuf boundary types for the ReallyMe COSE wire contract. + +This crate is a low-level generated contract crate, not the ergonomic COSE SDK. +Most consumers should depend on `reallyme-cose`; service, FFI, and generated +adapter code may use this crate when it needs the protobuf message types +directly. + +This crate defines messages only; it intentionally declares no protobuf service. +`CoseOperationRequest` is the single executable adapter request. +`CoseOperationResponseV2` is the binary response: its outcome oneof +contains either `CoseError` or `CoseOperationResult`, whose oneof preserves the +exact identity of all 15 operations. JSON is a generated ProtoJSON request +convenience; executable responses remain binary protobuf messages. + +Algorithm selectors are family-scoped. Signature, key-agreement, KEM, and +content-encryption values use the same numeric bands as the corresponding +`reallyme-crypto-proto` families; these protobuf values are not IANA COSE +algorithm identifiers. Operation-specific messages use the narrow family enum. +Only COSE_Key conversion uses `CoseAlgorithmIdentifier`, whose oneof can carry +more than one algorithm family. Earlier compact enum values are reserved so an +old request cannot be silently reinterpreted as a different algorithm. + +The source of truth is `proto/reallyme/cose/v1/cose.proto` inside this crate. +Regenerate this crate with `buf generate` from the repository root after +changing the schema, then run `node scripts/harden-generated-cose-proto.mjs`. + +The hardening pass redacts byte-valued request/result fields from `Debug` and +adds zeroization to generated `clear`, JSON partial-deserialization owners, and +message drop paths. Sensitive messages also recursively wipe length-delimited +unknown protobuf fields on `clear` and drop. Buffa requires generated messages +to implement `Clone`; each clone is therefore an additional transient byte +owner and is wiped on drop. Generated `PartialEq` is not a constant-time secret +comparison primitive. ProtoJSON serialization returns a caller-owned `String` +that this crate cannot wipe, so callers must retain sensitive JSON in a +zeroizing owner and release it promptly after transport. Managed-language +protobuf generators cannot promise equivalent memory erasure, so SDK wrappers +must document best-effort buffer clearing separately. + +This crate's generated Buffa mapping is the only JSON contract for the COSE +wire messages. `buf.gen.yaml` enables strict ProtoJSON and borrowed Rust views; +the hardening pass also redacts sensitive byte fields from borrowed-view +`Debug`. Borrowed views reference caller-owned protobuf bytes and cannot erase +that memory, so only owned generated messages claim wipe-on-drop behavior. diff --git a/crates/proto/proto/reallyme/cose/v1/cose.proto b/crates/proto/proto/reallyme/cose/v1/cose.proto new file mode 100644 index 0000000..09ebdbb --- /dev/null +++ b/crates/proto/proto/reallyme/cose/v1/cose.proto @@ -0,0 +1,500 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package reallyme.cose.v1; + +option go_package = "github.com/reallyme/cose/gen/go/reallyme/cose/v1;cosev1"; +option java_multiple_files = true; +option java_outer_classname = "CoseProto"; +option java_package = "me.really.cose.v1"; +option swift_prefix = "ReallyMeProto"; + +// CoseError is the public, non-PII error envelope for COSE boundary failures. +// The branch is part of the stable contract: primitive covers caller-controlled +// input and COSE semantic failures, provider covers algorithm/provider +// availability, and backend covers cryptographic backend, serialization, or +// internal adapter failures. +message CoseError { + oneof error { + CosePrimitiveError primitive = 1; + CoseProviderError provider = 2; + CoseBackendError backend = 3; + } +} + +// CosePrimitiveError describes failures owned by COSE input validation, +// byte-format parsing, key material handling, signing inputs, and signature +// verification semantics. +message CosePrimitiveError { + // Reason must be a caller-input, COSE semantic, Sign1, Key, or Multikey + // reason. Provider-only and backend-only reasons are rejected by the Rust + // wire adapter. + CoseErrorReason reason = 1; +} + +// CoseProviderError describes provider selection and algorithm availability +// failures. It intentionally contains no backend exception text. +message CoseProviderError { + // Reason must be COSE_ERROR_REASON_COMMON_UNSUPPORTED_ALGORITHM or one of + // the COSE_ERROR_REASON_PROVIDER_* values. + CoseErrorReason reason = 1; +} + +// CoseBackendError describes cryptographic backend, dispatch, and internal +// failures that are unsafe to expose as raw backend exception text. +message CoseBackendError { + // Reason must describe cryptographic backend failure, output serialization, + // dispatch failure, or an internal adapter invariant. Malformed or oversized + // caller input belongs to the primitive branch. + CoseErrorReason reason = 1; +} + +// Family-scoped protobuf selectors intentionally match reallyme/crypto's +// public algorithm numbers. They are not IANA or ReallyMe private-use COSE +// algorithm identifiers. +enum CoseSignatureAlgorithm { + COSE_SIGNATURE_ALGORITHM_UNSPECIFIED = 0; + reserved 1 to 14; + // EdDSA: 100-199; elliptic-curve signatures: 200-299; + // post-quantum signatures: 1000-1099. + COSE_SIGNATURE_ALGORITHM_ED25519 = 100; + COSE_SIGNATURE_ALGORITHM_ECDSA_P256_SHA256 = 200; + COSE_SIGNATURE_ALGORITHM_ECDSA_P384_SHA384 = 210; + COSE_SIGNATURE_ALGORITHM_ECDSA_P521_SHA512 = 220; + COSE_SIGNATURE_ALGORITHM_ECDSA_SECP256K1_SHA256 = 230; + COSE_SIGNATURE_ALGORITHM_ML_DSA_44 = 1000; + COSE_SIGNATURE_ALGORITHM_ML_DSA_65 = 1010; + COSE_SIGNATURE_ALGORITHM_ML_DSA_87 = 1020; +} + +enum CoseKeyAgreementAlgorithm { + COSE_KEY_AGREEMENT_ALGORITHM_UNSPECIFIED = 0; + reserved 1 to 14; + // Montgomery-form curves: 100-199. + COSE_KEY_AGREEMENT_ALGORITHM_X25519 = 100; +} + +enum CoseKemAlgorithm { + COSE_KEM_ALGORITHM_UNSPECIFIED = 0; + reserved 1 to 14; + // Post-quantum KEMs: 1000-1099; hybrid KEMs: 1100-1199. + COSE_KEM_ALGORITHM_ML_KEM_512 = 1000; + COSE_KEM_ALGORITHM_ML_KEM_768 = 1010; + COSE_KEM_ALGORITHM_ML_KEM_1024 = 1020; + COSE_KEM_ALGORITHM_X_WING_768 = 1100; +} + +// CoseAlgorithmIdentifier is used only by COSE_Key operations that accept +// more than one algorithm family. Operation-specific messages use the narrower +// family enum directly so invalid combinations are unrepresentable. +message CoseAlgorithmIdentifier { + oneof algorithm { + CoseSignatureAlgorithm signature = 1; + CoseKeyAgreementAlgorithm key_agreement = 2; + CoseKemAlgorithm kem = 3; + } +} + +// CoseContentEncryptionAlgorithm identifies the protected COSE_Encrypt content +// algorithm. These enum values are protobuf identifiers, not COSE algorithm +// registry values. +enum CoseContentEncryptionAlgorithm { + COSE_CONTENT_ENCRYPTION_ALGORITHM_UNSPECIFIED = 0; + reserved 1 to 3; + COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_128_GCM = 100; + COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_192_GCM = 110; + COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_256_GCM = 120; +} + +// CoseMlKemMode reports which COSE_Recipient construction was authenticated. +enum CoseMlKemMode { + COSE_ML_KEM_MODE_UNSPECIFIED = 0; + COSE_ML_KEM_MODE_DIRECT = 1; + COSE_ML_KEM_MODE_KEY_WRAP = 2; +} + +// CoseOperationRequest is the single executable protobuf entrypoint for +// FFI and generated-SDK adapters. Native Rust SDKs +// should keep using typed methods; adapters can use this generated oneof to +// avoid hand-rolled operation routing and error-envelope plumbing. +message CoseOperationRequest { + reserved 1 to 15; + + oneof operation { + // COSE_Sign1 operations: 1000-1999. + CoseSign1CreateRequest sign1_create = 1000; + CoseSign1CreateDetachedRequest sign1_create_detached = 1001; + CoseSign1VerifyRequest sign1_verify = 1002; + CoseSign1VerifyDetachedRequest sign1_verify_detached = 1003; + + // COSE_Key and Multikey conversion operations: 2000-2999. + CoseKeyFromPublicBytesRequest key_from_public_bytes = 2000; + CoseKeyFromPrivateBytesRequest key_from_private_bytes = 2001; + CoseKeyBytesRequest key_parse = 2002; + CoseKeyBytesRequest key_to_public_bytes = 2003; + CoseKeyBytesRequest key_to_private_bytes = 2004; + CoseKeyBytesRequest key_derive_public_kid = 2005; + CoseKeyBytesRequest key_to_multikey = 2006; + CoseMultikeyToCoseKeyRequest multikey_to_cose_key = 2007; + + // COSE_Encrypt and COSE_Recipient operations: 3000-3999. + CoseMlKemEncryptRequest ml_kem_encrypt_direct = 3000; + CoseMlKemEncryptRequest ml_kem_encrypt_key_wrap = 3001; + CoseMlKemDecryptRequest ml_kem_decrypt = 3002; + } +} + +// CoseOperationResponseV2 is the executable response contract. Its generated +// oneof distinguishes success from failure without an out-of-band status or an +// opaque operation-specific payload. +message CoseOperationResponseV2 { + oneof outcome { + CoseOperationResult result = 1; + CoseError error = 2; + } +} + +// CoseOperationResult identifies the exact operation that produced a +// successful response. Field numbers mirror CoseOperationRequest so generated +// clients can audit request/result pairing without a separate registry. +message CoseOperationResult { + reserved 1 to 15; + + oneof result { + // COSE_Sign1 results: 1000-1999. + CoseSign1CreateResult sign1_create = 1000; + CoseSign1CreateResult sign1_create_detached = 1001; + CoseSign1VerifyResult sign1_verify = 1002; + CoseSign1VerifyResult sign1_verify_detached = 1003; + + // COSE_Key and Multikey conversion results: 2000-2999. + CoseKeyBytesResult key_from_public_bytes = 2000; + CoseKeyBytesResult key_from_private_bytes = 2001; + CoseKeyBytesResult key_parse = 2002; + CoseKeyBytesResult key_to_public_bytes = 2003; + CoseKeyBytesResult key_to_private_bytes = 2004; + CoseKeyBytesResult key_derive_public_kid = 2005; + CoseMultikeyResult key_to_multikey = 2006; + CoseKeyBytesResult multikey_to_cose_key = 2007; + + // COSE_Encrypt and COSE_Recipient results: 3000-3999. + CoseMlKemEncryptResult ml_kem_encrypt_direct = 3000; + CoseMlKemEncryptResult ml_kem_encrypt_key_wrap = 3001; + CoseMlKemDecryptResult ml_kem_decrypt = 3002; + } +} + +// CoseMlKemEncryptRequest creates one-recipient COSE_Encrypt using the +// ReallyMe pre-IANA ML-KEM profile. +message CoseMlKemEncryptRequest { + // ML-KEM-512, ML-KEM-768, or ML-KEM-1024. + CoseKemAlgorithm kem_algorithm = 1; + // Protected content-encryption algorithm. + CoseContentEncryptionAlgorithm content_algorithm = 2; + // Recipient FIPS 203 ML-KEM encapsulation public key. + bytes recipient_public_key = 3; + // SHA-256 thumbprint of the canonical public-only, algorithm-bound COSE_Key. + // The encrypt operation rejects any value that does not identify + // recipient_public_key exactly. + bytes recipient_kid = 4; + // SENSITIVE: plaintext encrypted into the COSE_Encrypt ciphertext. + bytes plaintext = 5; + // SENSITIVE: external AAD authenticated but not carried in COSE. + bytes external_aad = 6; + // SENSITIVE: mutually known private KDF context agreed out of band. + bytes supp_priv_info = 7; + // Distinguishes omitted SuppPrivInfo from an intentionally empty byte string. + bool has_supp_priv_info = 8; +} + +message CoseMlKemEncryptResult { + // SENSITIVE: tagged COSE_Encrypt containing encrypted application content. + bytes cose_encrypt = 1; +} + +// CoseMlKemDecryptRequest authenticates and decrypts the ReallyMe pre-IANA +// ML-KEM COSE profile. +message CoseMlKemDecryptRequest { + // SENSITIVE: tagged or untagged COSE_Encrypt bytes. + bytes cose_encrypt = 1; + // SENSITIVE: 64-octet FIPS 203 private seed d || z. + bytes recipient_private_key = 2; + // Expected SHA-256 canonical public COSE_Key thumbprint. Decryption derives + // the public key from recipient_private_key and rejects a protected kid + // bound to any other key before decapsulation. + bytes expected_recipient_kid = 3; + // SENSITIVE: external AAD authenticated but not carried in COSE. + bytes external_aad = 4; + // SENSITIVE: mutually known private KDF context agreed out of band. + bytes supp_priv_info = 5; + // Distinguishes omitted SuppPrivInfo from an intentionally empty byte string. + bool has_supp_priv_info = 6; +} + +message CoseMlKemDecryptResult { + // SENSITIVE: authenticated plaintext. + bytes plaintext = 1; + CoseContentEncryptionAlgorithm content_algorithm = 2; + CoseKemAlgorithm kem_algorithm = 3; + CoseMlKemMode mode = 4; + // Authenticated canonical public COSE_Key thumbprint from the recipient. + bytes recipient_kid = 5; +} + +message CoseSign1Options { + // Emit the registered COSE_Sign1 root tag (18). False emits the untagged + // COSE_Sign1 structure used by most byte-level SDK APIs. + bool tag = 1; + // Optional maximum encoded COSE_Sign1 size. A value of 0 uses the crate + // default. + uint64 max_cose_sign1_bytes = 2; +} + +// CoseSign1CreateRequest signs an attached payload. +message CoseSign1CreateRequest { + // Algorithm used for the protected header and signing backend. + CoseSignatureAlgorithm algorithm = 1; + // SENSITIVE: payload bytes to embed in the COSE_Sign1 message. Payloads can + // carry credential claims or PII and must not be logged. + bytes payload = 2; + // SENSITIVE: raw private key bytes for the selected algorithm. Callers must + // keep this in a sensitive-buffer owner outside this transient protobuf + // container. + bytes private_key = 3; + // Protected-header kid bytes. Ignored unless has_kid is true. + bytes kid = 4; + // Whether kid is present. This distinguishes an omitted kid from an + // intentionally empty kid byte string. + bool has_kid = 5; + // Optional encoding controls. If omitted, the Rust SDK defaults apply. + CoseSign1Options options = 6; + // SENSITIVE: application-supplied external AAD authenticated by the + // signature but not embedded in COSE_Sign1. + bytes external_aad = 7; +} + +// CoseSign1CreateDetachedRequest signs a detached payload. The payload is +// authenticated but not embedded in the resulting COSE_Sign1 message. +message CoseSign1CreateDetachedRequest { + // Algorithm used for the protected header and signing backend. + CoseSignatureAlgorithm algorithm = 1; + // SENSITIVE: detached payload bytes authenticated by the signature. Payloads + // can carry credential claims or PII and must not be logged. + bytes payload = 2; + // SENSITIVE: raw private key bytes for the selected algorithm. Callers must + // keep this in a sensitive-buffer owner outside this transient protobuf + // container. + bytes private_key = 3; + // Protected-header kid bytes. Ignored unless has_kid is true. + bytes kid = 4; + // Whether kid is present. This distinguishes an omitted kid from an + // intentionally empty kid byte string. + bool has_kid = 5; + // Optional encoding controls. If omitted, the Rust SDK defaults apply. + CoseSign1Options options = 6; + // SENSITIVE: application-supplied external AAD authenticated by the + // signature but not embedded in COSE_Sign1. + bytes external_aad = 7; +} + +// CoseSign1CreateResult contains the encoded COSE_Sign1 bytes produced by a +// signing operation. +message CoseSign1CreateResult { + // SENSITIVE: encoded COSE_Sign1 bytes, tagged or untagged according to + // request options. Attached COSE_Sign1 messages contain payload bytes. + bytes cose_sign1 = 1; +} + +// CoseSign1VerifyRequest verifies a COSE_Sign1 with an attached payload. The +// public_key field is the protobuf-boundary equivalent of the Rust SDK key +// resolver output for the protected-header kid. +message CoseSign1VerifyRequest { + // SENSITIVE: encoded COSE_Sign1 bytes to verify. Tagged and untagged inputs + // are accepted; attached messages contain payload bytes. + bytes cose_sign1 = 1; + // Raw public key bytes selected by the caller for the protected-header kid. + bytes public_key = 2; + // Optional maximum accepted encoded COSE_Sign1 size. A value of 0 uses the + // crate default. + uint64 max_cose_sign1_bytes = 3; + // Reserved for shape parity with detached verification. A value of 0 uses the + // crate default; attached verification does not consume a detached payload. + uint64 max_detached_payload_bytes = 4; + // Require a non-empty protected-header kid before verification. + bool require_kid = 5; + // Optional algorithm allow-list. Empty means any COSE algorithm supported by + // this crate and operation. + repeated CoseSignatureAlgorithm allowed_algorithms = 6; + // SENSITIVE: external AAD supplied by the application for verification. + bytes external_aad = 7; + // Optional trusted key-selection identifier. When non-empty, the + // protected-header kid must match this value before public_key can be + // returned by the adapter's resolver. Empty preserves resolver-style use + // where public_key was already selected outside this request. + // Selection of this value belongs to the caller's trust-store boundary. + bytes expected_kid = 8; +} + +// CoseSign1VerifyDetachedRequest verifies a COSE_Sign1 whose payload is carried +// out of band. +message CoseSign1VerifyDetachedRequest { + // SENSITIVE: encoded COSE_Sign1 bytes to verify. Tagged and untagged inputs + // are accepted. + bytes cose_sign1 = 1; + // SENSITIVE: detached payload bytes that must match the signature. Payloads + // can carry credential claims or PII and must not be logged. + bytes payload = 2; + // Raw public key bytes selected by the caller for the protected-header kid. + bytes public_key = 3; + // Optional maximum accepted encoded COSE_Sign1 size. A value of 0 uses the + // crate default. + uint64 max_cose_sign1_bytes = 4; + // Optional maximum accepted detached payload size. A value of 0 uses the + // crate default. + uint64 max_detached_payload_bytes = 5; + // Require a non-empty protected-header kid before verification. + bool require_kid = 6; + // Optional algorithm allow-list. Empty means any COSE algorithm supported by + // this crate and operation. + repeated CoseSignatureAlgorithm allowed_algorithms = 7; + // SENSITIVE: external AAD supplied by the application for verification. + bytes external_aad = 8; + // Optional trusted key-selection identifier. When non-empty, the + // protected-header kid must match this value before public_key can be + // returned by the adapter's resolver. Empty preserves resolver-style use + // where public_key was already selected outside this request. + // Selection of this value belongs to the caller's trust-store boundary. + bytes expected_kid = 9; +} + +// CoseSign1VerifyResult contains the verified attached payload and protected +// header metadata. Detached verification returns an empty payload field. +message CoseSign1VerifyResult { + // SENSITIVE: verified attached payload bytes, or empty for detached + // verification. Payloads can carry credential claims or PII and must not be + // logged. + bytes payload = 1; + // Verified protected-header algorithm. + CoseSignatureAlgorithm algorithm = 2; + // Verified protected-header kid bytes. + bytes kid = 3; +} + +// CoseKeyFromPublicBytesRequest builds a public COSE_Key from raw public key +// bytes and an explicit algorithm binding. +message CoseKeyFromPublicBytesRequest { + // Algorithm that determines the COSE_Key kty/crv/alg profile. + CoseAlgorithmIdentifier algorithm = 1; + // Raw public key bytes for the selected algorithm. + bytes public_key = 2; +} + +// CoseKeyFromPrivateBytesRequest builds a private COSE_Key from raw private key +// bytes and matching public key bytes. +message CoseKeyFromPrivateBytesRequest { + // Algorithm that determines the COSE_Key kty/crv/alg profile. + CoseAlgorithmIdentifier algorithm = 1; + // SENSITIVE: raw private key bytes. Callers must keep this in a + // sensitive-buffer owner outside this transient protobuf container. + bytes private_key = 2; + // Matching raw public key bytes. Ignored unless has_public_key is true; + // supported private-key constructions reject requests that omit it. + bytes public_key = 3; + // Whether public_key is present. + bool has_public_key = 4; +} + +// CoseKeyBytesRequest carries encoded COSE_Key bytes for parse, extraction, +// deterministic kid derivation, and Multikey conversion operations. +message CoseKeyBytesRequest { + // SENSITIVE: encoded COSE_Key bytes. This may contain private key material + // for private extraction; wipe transient protobuf containers as soon as + // practical. + bytes cose_key = 1; +} + +// CoseKeyBytesResult carries encoded COSE_Key bytes or raw key/kid bytes, +// depending on the operation-specific result envelope. +message CoseKeyBytesResult { + // SENSITIVE: operation-specific bytes. For private-key extraction this + // contains private key material and must be moved into a sensitive-buffer + // owner by receivers. + bytes key_bytes = 1; +} + +// CoseMultikeyToCoseKeyRequest converts a Multikey string into a public +// COSE_Key. +message CoseMultikeyToCoseKeyRequest { + // Multikey string to parse. + string multikey = 1; +} + +// CoseMultikeyResult contains a Multikey string produced from a public COSE_Key. +message CoseMultikeyResult { + // Multikey string. + string multikey = 1; +} + +// CoseErrorReason is the component-owned reason-code enum for reallyme/cose. +// Values are stable public boundary codes; internal Rust errors must map into +// one of these before crossing RPC, SDK, FFI, storage, audit, or telemetry +// boundaries. Numeric bands keep evolving failure families adjacent without +// forcing unrelated public wire values to move. +enum CoseErrorReason { + COSE_ERROR_REASON_UNSPECIFIED = 0; + reserved 1 to 37; + + // Common COSE binary and CBOR boundary failures: 100-199. + COSE_ERROR_REASON_COMMON_CBOR = 100; + COSE_ERROR_REASON_COMMON_INVALID_FORMAT = 101; + COSE_ERROR_REASON_COMMON_RESOURCE_LIMIT_EXCEEDED = 102; + COSE_ERROR_REASON_COMMON_NON_CANONICAL_CBOR = 103; + COSE_ERROR_REASON_COMMON_UNEXPECTED_CBOR_TAG = 104; + COSE_ERROR_REASON_COMMON_DUPLICATE_MAP_LABEL = 105; + COSE_ERROR_REASON_COMMON_MALFORMED_PROTOBUF = 120; + COSE_ERROR_REASON_COMMON_MALFORMED_JSON = 121; + COSE_ERROR_REASON_COMMON_INVALID_PARAMETER = 130; + COSE_ERROR_REASON_COMMON_INVALID_LENGTH = 131; + COSE_ERROR_REASON_COMMON_INVALID_ENCODING = 132; + + // Provider failures: 200-299. + COSE_ERROR_REASON_COMMON_UNSUPPORTED_ALGORITHM = 200; + COSE_ERROR_REASON_PROVIDER_UNAVAILABLE = 201; + + // Backend failures: 300-399. + COSE_ERROR_REASON_COMMON_CRYPTO_FAILED = 300; + COSE_ERROR_REASON_BACKEND_INTERNAL = 301; + + // COSE_Sign1 failures: 400-499. + COSE_ERROR_REASON_SIGN1_KID_KEY_MISMATCH = 400; + COSE_ERROR_REASON_SIGN1_MISSING_PAYLOAD = 401; + COSE_ERROR_REASON_SIGN1_MISSING_KID = 402; + COSE_ERROR_REASON_SIGN1_KEY_NOT_RESOLVED = 403; + COSE_ERROR_REASON_SIGN1_UNSUPPORTED_CRITICAL_HEADER = 410; + COSE_ERROR_REASON_SIGN1_UNPROTECTED_HEADER_NOT_ALLOWED = 411; + COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE = 420; + COSE_ERROR_REASON_SIGN1_MISSING_PRIVATE_KEY = 421; + COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE_ENCODING = 422; + + // COSE_Key material and key-shape failures: 500-599. + COSE_ERROR_REASON_KEY_MISSING_KEY_MATERIAL = 500; + COSE_ERROR_REASON_KEY_INVALID_KEY_MATERIAL = 501; + + // Multikey decoding and COSE_Key conversion failures: 600-699. + COSE_ERROR_REASON_MULTIKEY_INVALID_MULTIKEY = 600; + + // COSE_Encrypt and COSE_Recipient failures: 700-799. + COSE_ERROR_REASON_ENCRYPT_MISSING_CIPHERTEXT = 700; + COSE_ERROR_REASON_ENCRYPT_INVALID_IV = 701; + COSE_ERROR_REASON_ENCRYPT_INVALID_RECIPIENT = 702; + COSE_ERROR_REASON_ENCRYPT_MISSING_ENCAPSULATED_KEY = 703; + COSE_ERROR_REASON_ENCRYPT_INVALID_ENCAPSULATED_KEY = 704; + COSE_ERROR_REASON_ENCRYPT_AUTHENTICATION_FAILED = 720; + COSE_ERROR_REASON_ENCRYPT_KEY_UNWRAP_FAILED = 721; + COSE_ERROR_REASON_ENCRYPT_KID_MISMATCH = 730; + COSE_ERROR_REASON_ENCRYPT_MISSING_KID = 731; + COSE_ERROR_REASON_ENCRYPT_UNPROTECTED_HEADER_NOT_ALLOWED = 740; +} diff --git a/crates/proto/src/generated.rs b/crates/proto/src/generated.rs new file mode 100644 index 0000000..fa5fda4 --- /dev/null +++ b/crates/proto/src/generated.rs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Generated Buffa code boundary. + +/// Canonical protobuf package for ReallyMe COSE boundary messages. +pub const COSE_PROTO_PACKAGE: &str = "reallyme.cose.v1"; + +/// Generated Buffa protobuf message types. +#[allow(missing_docs)] +#[cfg(feature = "generated")] +#[path = "generated/buffa/mod.rs"] +pub mod proto; diff --git a/crates/proto/src/generated/buffa/mod.rs b/crates/proto/src/generated/buffa/mod.rs new file mode 100644 index 0000000..4fd1944 --- /dev/null +++ b/crates/proto/src/generated/buffa/mod.rs @@ -0,0 +1,56 @@ +// @generated by buffa-codegen. DO NOT EDIT. +#![allow( + non_camel_case_types, + dead_code, + unused_imports, + unused_qualifications, + clippy::derivable_impls, + clippy::match_single_binding, + clippy::uninlined_format_args, + clippy::doc_lazy_continuation, + clippy::module_inception +)] + +#[allow( + non_camel_case_types, + dead_code, + unused_imports, + unused_qualifications, + clippy::derivable_impls, + clippy::match_single_binding, + clippy::uninlined_format_args, + clippy::doc_lazy_continuation, + clippy::module_inception +)] +pub mod reallyme { + use super::*; + #[allow( + non_camel_case_types, + dead_code, + unused_imports, + unused_qualifications, + clippy::derivable_impls, + clippy::match_single_binding, + clippy::uninlined_format_args, + clippy::doc_lazy_continuation, + clippy::module_inception + )] + pub mod cose { + use super::*; + #[allow( + non_camel_case_types, + dead_code, + unused_imports, + unused_qualifications, + clippy::derivable_impls, + clippy::match_single_binding, + clippy::uninlined_format_args, + clippy::doc_lazy_continuation, + clippy::module_inception + )] + pub mod v1 { + use super::*; + include!("reallyme.cose.v1.mod.rs"); + } + } +} diff --git a/crates/proto/src/generated/buffa/reallyme.cose.v1.cose.__oneof.rs b/crates/proto/src/generated/buffa/reallyme.cose.v1.cose.__oneof.rs new file mode 100644 index 0000000..4fd2dba --- /dev/null +++ b/crates/proto/src/generated/buffa/reallyme.cose.v1.cose.__oneof.rs @@ -0,0 +1,481 @@ +// @generated by buffa-codegen. DO NOT EDIT. +// source: reallyme/cose/v1/cose.proto + +pub mod cose_error { + #[allow(unused_imports)] + use super::*; + #[derive(Clone, PartialEq, Debug)] + pub enum Error { + Primitive(::buffa::alloc::boxed::Box), + Provider(::buffa::alloc::boxed::Box), + Backend(::buffa::alloc::boxed::Box), + } + impl ::buffa::Oneof for Error {} + impl From for Error { + fn from(v: super::super::super::CosePrimitiveError) -> Self { + Self::Primitive(::buffa::alloc::boxed::Box::new(v)) + } + } + impl From + for ::core::option::Option { + fn from(v: super::super::super::CosePrimitiveError) -> Self { + Self::Some(Error::from(v)) + } + } + impl From for Error { + fn from(v: super::super::super::CoseProviderError) -> Self { + Self::Provider(::buffa::alloc::boxed::Box::new(v)) + } + } + impl From for ::core::option::Option { + fn from(v: super::super::super::CoseProviderError) -> Self { + Self::Some(Error::from(v)) + } + } + impl From for Error { + fn from(v: super::super::super::CoseBackendError) -> Self { + Self::Backend(::buffa::alloc::boxed::Box::new(v)) + } + } + impl From for ::core::option::Option { + fn from(v: super::super::super::CoseBackendError) -> Self { + Self::Some(Error::from(v)) + } + } + impl serde::Serialize for Error { + fn serialize( + &self, + s: S, + ) -> ::core::result::Result { + use serde::ser::SerializeMap; + let mut map = s.serialize_map(Some(1))?; + match self { + Self::Primitive(v) => { + map.serialize_entry("primitive", v)?; + } + Self::Provider(v) => { + map.serialize_entry("provider", v)?; + } + Self::Backend(v) => { + map.serialize_entry("backend", v)?; + } + } + map.end() + } + } +} +pub mod cose_algorithm_identifier { + #[allow(unused_imports)] + use super::*; + #[derive(Clone, PartialEq, Debug)] + pub enum Algorithm { + Signature(::buffa::EnumValue), + KeyAgreement(::buffa::EnumValue), + Kem(::buffa::EnumValue), + } + impl ::buffa::Oneof for Algorithm {} + impl serde::Serialize for Algorithm { + fn serialize( + &self, + s: S, + ) -> ::core::result::Result { + use serde::ser::SerializeMap; + let mut map = s.serialize_map(Some(1))?; + match self { + Self::Signature(v) => { + map.serialize_entry("signature", v)?; + } + Self::KeyAgreement(v) => { + map.serialize_entry("keyAgreement", v)?; + } + Self::Kem(v) => { + map.serialize_entry("kem", v)?; + } + } + map.end() + } + } +} +pub mod cose_operation_request { + #[allow(unused_imports)] + use super::*; + #[derive(Clone, PartialEq, Debug)] + pub enum Operation { + Sign1Create( + ::buffa::alloc::boxed::Box, + ), + Sign1CreateDetached( + ::buffa::alloc::boxed::Box< + super::super::super::CoseSign1CreateDetachedRequest, + >, + ), + Sign1Verify( + ::buffa::alloc::boxed::Box, + ), + Sign1VerifyDetached( + ::buffa::alloc::boxed::Box< + super::super::super::CoseSign1VerifyDetachedRequest, + >, + ), + KeyFromPublicBytes( + ::buffa::alloc::boxed::Box< + super::super::super::CoseKeyFromPublicBytesRequest, + >, + ), + KeyFromPrivateBytes( + ::buffa::alloc::boxed::Box< + super::super::super::CoseKeyFromPrivateBytesRequest, + >, + ), + KeyParse(::buffa::alloc::boxed::Box), + KeyToPublicBytes( + ::buffa::alloc::boxed::Box, + ), + KeyToPrivateBytes( + ::buffa::alloc::boxed::Box, + ), + KeyDerivePublicKid( + ::buffa::alloc::boxed::Box, + ), + KeyToMultikey( + ::buffa::alloc::boxed::Box, + ), + MultikeyToCoseKey( + ::buffa::alloc::boxed::Box, + ), + MlKemEncryptDirect( + ::buffa::alloc::boxed::Box, + ), + MlKemEncryptKeyWrap( + ::buffa::alloc::boxed::Box, + ), + MlKemDecrypt( + ::buffa::alloc::boxed::Box, + ), + } + impl ::buffa::Oneof for Operation {} + impl From for Operation { + fn from(v: super::super::super::CoseSign1CreateRequest) -> Self { + Self::Sign1Create(::buffa::alloc::boxed::Box::new(v)) + } + } + impl From + for ::core::option::Option { + fn from(v: super::super::super::CoseSign1CreateRequest) -> Self { + Self::Some(Operation::from(v)) + } + } + impl From for Operation { + fn from(v: super::super::super::CoseSign1CreateDetachedRequest) -> Self { + Self::Sign1CreateDetached(::buffa::alloc::boxed::Box::new(v)) + } + } + impl From + for ::core::option::Option { + fn from(v: super::super::super::CoseSign1CreateDetachedRequest) -> Self { + Self::Some(Operation::from(v)) + } + } + impl From for Operation { + fn from(v: super::super::super::CoseSign1VerifyRequest) -> Self { + Self::Sign1Verify(::buffa::alloc::boxed::Box::new(v)) + } + } + impl From + for ::core::option::Option { + fn from(v: super::super::super::CoseSign1VerifyRequest) -> Self { + Self::Some(Operation::from(v)) + } + } + impl From for Operation { + fn from(v: super::super::super::CoseSign1VerifyDetachedRequest) -> Self { + Self::Sign1VerifyDetached(::buffa::alloc::boxed::Box::new(v)) + } + } + impl From + for ::core::option::Option { + fn from(v: super::super::super::CoseSign1VerifyDetachedRequest) -> Self { + Self::Some(Operation::from(v)) + } + } + impl From for Operation { + fn from(v: super::super::super::CoseKeyFromPublicBytesRequest) -> Self { + Self::KeyFromPublicBytes(::buffa::alloc::boxed::Box::new(v)) + } + } + impl From + for ::core::option::Option { + fn from(v: super::super::super::CoseKeyFromPublicBytesRequest) -> Self { + Self::Some(Operation::from(v)) + } + } + impl From for Operation { + fn from(v: super::super::super::CoseKeyFromPrivateBytesRequest) -> Self { + Self::KeyFromPrivateBytes(::buffa::alloc::boxed::Box::new(v)) + } + } + impl From + for ::core::option::Option { + fn from(v: super::super::super::CoseKeyFromPrivateBytesRequest) -> Self { + Self::Some(Operation::from(v)) + } + } + impl From for Operation { + fn from(v: super::super::super::CoseMultikeyToCoseKeyRequest) -> Self { + Self::MultikeyToCoseKey(::buffa::alloc::boxed::Box::new(v)) + } + } + impl From + for ::core::option::Option { + fn from(v: super::super::super::CoseMultikeyToCoseKeyRequest) -> Self { + Self::Some(Operation::from(v)) + } + } + impl From for Operation { + fn from(v: super::super::super::CoseMlKemDecryptRequest) -> Self { + Self::MlKemDecrypt(::buffa::alloc::boxed::Box::new(v)) + } + } + impl From + for ::core::option::Option { + fn from(v: super::super::super::CoseMlKemDecryptRequest) -> Self { + Self::Some(Operation::from(v)) + } + } + impl serde::Serialize for Operation { + fn serialize( + &self, + s: S, + ) -> ::core::result::Result { + use serde::ser::SerializeMap; + let mut map = s.serialize_map(Some(1))?; + match self { + Self::Sign1Create(v) => { + map.serialize_entry("sign1Create", v)?; + } + Self::Sign1CreateDetached(v) => { + map.serialize_entry("sign1CreateDetached", v)?; + } + Self::Sign1Verify(v) => { + map.serialize_entry("sign1Verify", v)?; + } + Self::Sign1VerifyDetached(v) => { + map.serialize_entry("sign1VerifyDetached", v)?; + } + Self::KeyFromPublicBytes(v) => { + map.serialize_entry("keyFromPublicBytes", v)?; + } + Self::KeyFromPrivateBytes(v) => { + map.serialize_entry("keyFromPrivateBytes", v)?; + } + Self::KeyParse(v) => { + map.serialize_entry("keyParse", v)?; + } + Self::KeyToPublicBytes(v) => { + map.serialize_entry("keyToPublicBytes", v)?; + } + Self::KeyToPrivateBytes(v) => { + map.serialize_entry("keyToPrivateBytes", v)?; + } + Self::KeyDerivePublicKid(v) => { + map.serialize_entry("keyDerivePublicKid", v)?; + } + Self::KeyToMultikey(v) => { + map.serialize_entry("keyToMultikey", v)?; + } + Self::MultikeyToCoseKey(v) => { + map.serialize_entry("multikeyToCoseKey", v)?; + } + Self::MlKemEncryptDirect(v) => { + map.serialize_entry("mlKemEncryptDirect", v)?; + } + Self::MlKemEncryptKeyWrap(v) => { + map.serialize_entry("mlKemEncryptKeyWrap", v)?; + } + Self::MlKemDecrypt(v) => { + map.serialize_entry("mlKemDecrypt", v)?; + } + } + map.end() + } + } +} +pub mod cose_operation_response_v2 { + #[allow(unused_imports)] + use super::*; + #[derive(Clone, PartialEq, Debug)] + pub enum Outcome { + Result(::buffa::alloc::boxed::Box), + Error(::buffa::alloc::boxed::Box), + } + impl ::buffa::Oneof for Outcome {} + impl From for Outcome { + fn from(v: super::super::super::CoseOperationResult) -> Self { + Self::Result(::buffa::alloc::boxed::Box::new(v)) + } + } + impl From + for ::core::option::Option { + fn from(v: super::super::super::CoseOperationResult) -> Self { + Self::Some(Outcome::from(v)) + } + } + impl From for Outcome { + fn from(v: super::super::super::CoseError) -> Self { + Self::Error(::buffa::alloc::boxed::Box::new(v)) + } + } + impl From for ::core::option::Option { + fn from(v: super::super::super::CoseError) -> Self { + Self::Some(Outcome::from(v)) + } + } + impl serde::Serialize for Outcome { + fn serialize( + &self, + s: S, + ) -> ::core::result::Result { + use serde::ser::SerializeMap; + let mut map = s.serialize_map(Some(1))?; + match self { + Self::Result(v) => { + map.serialize_entry("result", v)?; + } + Self::Error(v) => { + map.serialize_entry("error", v)?; + } + } + map.end() + } + } +} +pub mod cose_operation_result { + #[allow(unused_imports)] + use super::*; + #[derive(Clone, PartialEq, Debug)] + pub enum Result { + Sign1Create( + ::buffa::alloc::boxed::Box, + ), + Sign1CreateDetached( + ::buffa::alloc::boxed::Box, + ), + Sign1Verify( + ::buffa::alloc::boxed::Box, + ), + Sign1VerifyDetached( + ::buffa::alloc::boxed::Box, + ), + KeyFromPublicBytes( + ::buffa::alloc::boxed::Box, + ), + KeyFromPrivateBytes( + ::buffa::alloc::boxed::Box, + ), + KeyParse(::buffa::alloc::boxed::Box), + KeyToPublicBytes( + ::buffa::alloc::boxed::Box, + ), + KeyToPrivateBytes( + ::buffa::alloc::boxed::Box, + ), + KeyDerivePublicKid( + ::buffa::alloc::boxed::Box, + ), + KeyToMultikey( + ::buffa::alloc::boxed::Box, + ), + MultikeyToCoseKey( + ::buffa::alloc::boxed::Box, + ), + MlKemEncryptDirect( + ::buffa::alloc::boxed::Box, + ), + MlKemEncryptKeyWrap( + ::buffa::alloc::boxed::Box, + ), + MlKemDecrypt( + ::buffa::alloc::boxed::Box, + ), + } + impl ::buffa::Oneof for Result {} + impl From for Result { + fn from(v: super::super::super::CoseMultikeyResult) -> Self { + Self::KeyToMultikey(::buffa::alloc::boxed::Box::new(v)) + } + } + impl From + for ::core::option::Option { + fn from(v: super::super::super::CoseMultikeyResult) -> Self { + Self::Some(Result::from(v)) + } + } + impl From for Result { + fn from(v: super::super::super::CoseMlKemDecryptResult) -> Self { + Self::MlKemDecrypt(::buffa::alloc::boxed::Box::new(v)) + } + } + impl From + for ::core::option::Option { + fn from(v: super::super::super::CoseMlKemDecryptResult) -> Self { + Self::Some(Result::from(v)) + } + } + impl serde::Serialize for Result { + fn serialize( + &self, + s: S, + ) -> ::core::result::Result { + use serde::ser::SerializeMap; + let mut map = s.serialize_map(Some(1))?; + match self { + Self::Sign1Create(v) => { + map.serialize_entry("sign1Create", v)?; + } + Self::Sign1CreateDetached(v) => { + map.serialize_entry("sign1CreateDetached", v)?; + } + Self::Sign1Verify(v) => { + map.serialize_entry("sign1Verify", v)?; + } + Self::Sign1VerifyDetached(v) => { + map.serialize_entry("sign1VerifyDetached", v)?; + } + Self::KeyFromPublicBytes(v) => { + map.serialize_entry("keyFromPublicBytes", v)?; + } + Self::KeyFromPrivateBytes(v) => { + map.serialize_entry("keyFromPrivateBytes", v)?; + } + Self::KeyParse(v) => { + map.serialize_entry("keyParse", v)?; + } + Self::KeyToPublicBytes(v) => { + map.serialize_entry("keyToPublicBytes", v)?; + } + Self::KeyToPrivateBytes(v) => { + map.serialize_entry("keyToPrivateBytes", v)?; + } + Self::KeyDerivePublicKid(v) => { + map.serialize_entry("keyDerivePublicKid", v)?; + } + Self::KeyToMultikey(v) => { + map.serialize_entry("keyToMultikey", v)?; + } + Self::MultikeyToCoseKey(v) => { + map.serialize_entry("multikeyToCoseKey", v)?; + } + Self::MlKemEncryptDirect(v) => { + map.serialize_entry("mlKemEncryptDirect", v)?; + } + Self::MlKemEncryptKeyWrap(v) => { + map.serialize_entry("mlKemEncryptKeyWrap", v)?; + } + Self::MlKemDecrypt(v) => { + map.serialize_entry("mlKemDecrypt", v)?; + } + } + map.end() + } + } +} diff --git a/crates/proto/src/generated/buffa/reallyme.cose.v1.cose.__view.rs b/crates/proto/src/generated/buffa/reallyme.cose.v1.cose.__view.rs new file mode 100644 index 0000000..573e217 --- /dev/null +++ b/crates/proto/src/generated/buffa/reallyme.cose.v1.cose.__view.rs @@ -0,0 +1,11036 @@ +// @generated by buffa-codegen. DO NOT EDIT. +// source: reallyme/cose/v1/cose.proto + +/// CoseError is the public, non-PII error envelope for COSE boundary failures. +/// The branch is part of the stable contract: primitive covers caller-controlled +/// input and COSE semantic failures, provider covers algorithm/provider +/// availability, and backend covers cryptographic backend, serialization, or +/// internal adapter failures. +#[derive(Clone, Debug, Default)] +pub struct CoseErrorView<'a> { + pub error: ::core::option::Option< + super::super::__buffa::view::oneof::cose_error::Error<'a>, + >, + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl<'a> ::buffa::MessageView<'a> for CoseErrorView<'a> { + type Owned = super::super::CoseError; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_error::Error::Primitive( + ref mut existing, + ), + ) = view.error + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.error = Some( + super::super::__buffa::view::oneof::cose_error::Error::Primitive( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_error::Error::Provider( + ref mut existing, + ), + ) = view.error + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.error = Some( + super::super::__buffa::view::oneof::cose_error::Error::Provider( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_error::Error::Backend( + ref mut existing, + ), + ) = view.error + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.error = Some( + super::super::__buffa::view::oneof::cose_error::Error::Backend( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseError { + error: match self.error.as_ref() { + ::core::option::Option::Some(v) => { + ::core::option::Option::Some( + match v { + super::super::__buffa::view::oneof::cose_error::Error::Primitive( + v, + ) => { + super::super::__buffa::oneof::cose_error::Error::Primitive( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_error::Error::Provider( + v, + ) => { + super::super::__buffa::oneof::cose_error::Error::Provider( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_error::Error::Backend( + v, + ) => { + super::super::__buffa::oneof::cose_error::Error::Backend( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + }, + ) + } + ::core::option::Option::None => ::core::option::Option::None, + }, + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseErrorView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, __cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if let ::core::option::Option::Some(ref v) = self.error { + match v { + super::super::__buffa::view::oneof::cose_error::Error::Primitive(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 1u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_error::Error::Provider(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 1u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_error::Error::Backend(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 1u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + } + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + __cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if let ::core::option::Option::Some(ref v) = self.error { + match v { + super::super::__buffa::view::oneof::cose_error::Error::Primitive(x) => { + ::buffa::types::put_len_delimited_header( + 1u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_error::Error::Provider(x) => { + ::buffa::types::put_len_delimited_header( + 2u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_error::Error::Backend(x) => { + ::buffa::types::put_len_delimited_header( + 3u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + } + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseErrorView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if let ::core::option::Option::Some(ref __ov) = self.error { + match __ov { + super::super::__buffa::view::oneof::cose_error::Error::Primitive(v) => { + __map.serialize_entry("primitive", v)?; + } + super::super::__buffa::view::oneof::cose_error::Error::Provider(v) => { + __map.serialize_entry("provider", v)?; + } + super::super::__buffa::view::oneof::cose_error::Error::Backend(v) => { + __map.serialize_entry("backend", v)?; + } + } + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseErrorView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseError"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseError"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseError"; +} +::buffa::impl_default_view_instance!(CoseErrorView); +::buffa::impl_view_reborrow!(CoseErrorView); +/** Self-contained, `'static` owned view of a `CoseError` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseErrorView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseErrorView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone, Debug)] +pub struct CoseErrorOwnedView(::buffa::OwnedView>); +impl CoseErrorOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseErrorOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseErrorOwnedView(::buffa::OwnedView::decode_with_options(bytes, opts)?), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseError, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseErrorOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseErrorView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseErrorView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseError { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// Oneof `error`. + #[must_use] + pub fn error( + &self, + ) -> ::core::option::Option< + &super::super::__buffa::view::oneof::cose_error::Error<'_>, + > { + self.0.reborrow().error.as_ref() + } +} +impl ::core::convert::From<::buffa::OwnedView>> +for CoseErrorOwnedView { + fn from(inner: ::buffa::OwnedView>) -> Self { + CoseErrorOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseErrorOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef<::buffa::OwnedView>> +for CoseErrorOwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseError { + type View<'a> = CoseErrorView<'a>; + type ViewHandle = CoseErrorOwnedView; +} +impl ::serde::Serialize for CoseErrorOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +/// CosePrimitiveError describes failures owned by COSE input validation, +/// byte-format parsing, key material handling, signing inputs, and signature +/// verification semantics. +#[derive(Clone, Debug, Default)] +pub struct CosePrimitiveErrorView<'a> { + /// Reason must be a caller-input, COSE semantic, Sign1, Key, or Multikey + /// reason. Provider-only and backend-only reasons are rejected by the Rust + /// wire adapter. + /// + /// Field 1: `reason` + pub reason: ::buffa::EnumValue, + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl<'a> ::buffa::MessageView<'a> for CosePrimitiveErrorView<'a> { + type Owned = super::super::CosePrimitiveError; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.reason = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(&mut cur)?, + ); + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CosePrimitiveError { + reason: self.reason, + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CosePrimitiveErrorView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + { + let val = self.reason.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + { + let val = self.reason.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(1u32, val, buf); + } + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CosePrimitiveErrorView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if !::buffa::json_helpers::skip_if::is_default_enum_value(&self.reason) { + __map.serialize_entry("reason", &self.reason)?; + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CosePrimitiveErrorView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CosePrimitiveError"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CosePrimitiveError"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CosePrimitiveError"; +} +::buffa::impl_default_view_instance!(CosePrimitiveErrorView); +::buffa::impl_view_reborrow!(CosePrimitiveErrorView); +/** Self-contained, `'static` owned view of a `CosePrimitiveError` message. + + Wraps [`::buffa::OwnedView`]`<`[`CosePrimitiveErrorView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CosePrimitiveErrorView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone, Debug)] +pub struct CosePrimitiveErrorOwnedView( + ::buffa::OwnedView>, +); +impl CosePrimitiveErrorOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CosePrimitiveErrorOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CosePrimitiveErrorOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CosePrimitiveError, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CosePrimitiveErrorOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CosePrimitiveErrorView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CosePrimitiveErrorView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CosePrimitiveError { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// Reason must be a caller-input, COSE semantic, Sign1, Key, or Multikey + /// reason. Provider-only and backend-only reasons are rejected by the Rust + /// wire adapter. + /// + /// Field 1: `reason` + #[must_use] + pub fn reason(&self) -> ::buffa::EnumValue { + self.0.reborrow().reason + } +} +impl ::core::convert::From<::buffa::OwnedView>> +for CosePrimitiveErrorOwnedView { + fn from(inner: ::buffa::OwnedView>) -> Self { + CosePrimitiveErrorOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CosePrimitiveErrorOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef<::buffa::OwnedView>> +for CosePrimitiveErrorOwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CosePrimitiveError { + type View<'a> = CosePrimitiveErrorView<'a>; + type ViewHandle = CosePrimitiveErrorOwnedView; +} +impl ::serde::Serialize for CosePrimitiveErrorOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +/// CoseProviderError describes provider selection and algorithm availability +/// failures. It intentionally contains no backend exception text. +#[derive(Clone, Debug, Default)] +pub struct CoseProviderErrorView<'a> { + /// Reason must be COSE_ERROR_REASON_COMMON_UNSUPPORTED_ALGORITHM or one of + /// the COSE_ERROR_REASON_PROVIDER_* values. + /// + /// Field 1: `reason` + pub reason: ::buffa::EnumValue, + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl<'a> ::buffa::MessageView<'a> for CoseProviderErrorView<'a> { + type Owned = super::super::CoseProviderError; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.reason = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(&mut cur)?, + ); + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseProviderError { + reason: self.reason, + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseProviderErrorView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + { + let val = self.reason.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + { + let val = self.reason.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(1u32, val, buf); + } + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseProviderErrorView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if !::buffa::json_helpers::skip_if::is_default_enum_value(&self.reason) { + __map.serialize_entry("reason", &self.reason)?; + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseProviderErrorView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseProviderError"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseProviderError"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseProviderError"; +} +::buffa::impl_default_view_instance!(CoseProviderErrorView); +::buffa::impl_view_reborrow!(CoseProviderErrorView); +/** Self-contained, `'static` owned view of a `CoseProviderError` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseProviderErrorView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseProviderErrorView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone, Debug)] +pub struct CoseProviderErrorOwnedView( + ::buffa::OwnedView>, +); +impl CoseProviderErrorOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseProviderErrorOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseProviderErrorOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseProviderError, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseProviderErrorOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseProviderErrorView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseProviderErrorView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseProviderError { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// Reason must be COSE_ERROR_REASON_COMMON_UNSUPPORTED_ALGORITHM or one of + /// the COSE_ERROR_REASON_PROVIDER_* values. + /// + /// Field 1: `reason` + #[must_use] + pub fn reason(&self) -> ::buffa::EnumValue { + self.0.reborrow().reason + } +} +impl ::core::convert::From<::buffa::OwnedView>> +for CoseProviderErrorOwnedView { + fn from(inner: ::buffa::OwnedView>) -> Self { + CoseProviderErrorOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseProviderErrorOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef<::buffa::OwnedView>> +for CoseProviderErrorOwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseProviderError { + type View<'a> = CoseProviderErrorView<'a>; + type ViewHandle = CoseProviderErrorOwnedView; +} +impl ::serde::Serialize for CoseProviderErrorOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +/// CoseBackendError describes cryptographic backend, dispatch, and internal +/// failures that are unsafe to expose as raw backend exception text. +#[derive(Clone, Debug, Default)] +pub struct CoseBackendErrorView<'a> { + /// Reason must describe cryptographic backend failure, output serialization, + /// dispatch failure, or an internal adapter invariant. Malformed or oversized + /// caller input belongs to the primitive branch. + /// + /// Field 1: `reason` + pub reason: ::buffa::EnumValue, + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl<'a> ::buffa::MessageView<'a> for CoseBackendErrorView<'a> { + type Owned = super::super::CoseBackendError; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.reason = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(&mut cur)?, + ); + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseBackendError { + reason: self.reason, + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseBackendErrorView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + { + let val = self.reason.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + { + let val = self.reason.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(1u32, val, buf); + } + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseBackendErrorView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if !::buffa::json_helpers::skip_if::is_default_enum_value(&self.reason) { + __map.serialize_entry("reason", &self.reason)?; + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseBackendErrorView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseBackendError"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseBackendError"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseBackendError"; +} +::buffa::impl_default_view_instance!(CoseBackendErrorView); +::buffa::impl_view_reborrow!(CoseBackendErrorView); +/** Self-contained, `'static` owned view of a `CoseBackendError` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseBackendErrorView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseBackendErrorView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone, Debug)] +pub struct CoseBackendErrorOwnedView(::buffa::OwnedView>); +impl CoseBackendErrorOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseBackendErrorOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseBackendErrorOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseBackendError, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseBackendErrorOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseBackendErrorView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseBackendErrorView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseBackendError { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// Reason must describe cryptographic backend failure, output serialization, + /// dispatch failure, or an internal adapter invariant. Malformed or oversized + /// caller input belongs to the primitive branch. + /// + /// Field 1: `reason` + #[must_use] + pub fn reason(&self) -> ::buffa::EnumValue { + self.0.reborrow().reason + } +} +impl ::core::convert::From<::buffa::OwnedView>> +for CoseBackendErrorOwnedView { + fn from(inner: ::buffa::OwnedView>) -> Self { + CoseBackendErrorOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseBackendErrorOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef<::buffa::OwnedView>> +for CoseBackendErrorOwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseBackendError { + type View<'a> = CoseBackendErrorView<'a>; + type ViewHandle = CoseBackendErrorOwnedView; +} +impl ::serde::Serialize for CoseBackendErrorOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +/// CoseAlgorithmIdentifier is used only by COSE_Key operations that accept +/// more than one algorithm family. Operation-specific messages use the narrower +/// family enum directly so invalid combinations are unrepresentable. +#[derive(Clone, Debug, Default)] +pub struct CoseAlgorithmIdentifierView<'a> { + pub algorithm: ::core::option::Option< + super::super::__buffa::view::oneof::cose_algorithm_identifier::Algorithm, + >, + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl<'a> ::buffa::MessageView<'a> for CoseAlgorithmIdentifierView<'a> { + type Owned = super::super::CoseAlgorithmIdentifier; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.algorithm = Some( + super::super::__buffa::view::oneof::cose_algorithm_identifier::Algorithm::Signature( + ::buffa::EnumValue::from(::buffa::types::decode_int32(&mut cur)?), + ), + ); + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.algorithm = Some( + super::super::__buffa::view::oneof::cose_algorithm_identifier::Algorithm::KeyAgreement( + ::buffa::EnumValue::from(::buffa::types::decode_int32(&mut cur)?), + ), + ); + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.algorithm = Some( + super::super::__buffa::view::oneof::cose_algorithm_identifier::Algorithm::Kem( + ::buffa::EnumValue::from(::buffa::types::decode_int32(&mut cur)?), + ), + ); + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result< + super::super::CoseAlgorithmIdentifier, + ::buffa::DecodeError, + > { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result< + super::super::CoseAlgorithmIdentifier, + ::buffa::DecodeError, + > { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseAlgorithmIdentifier { + algorithm: self + .algorithm + .as_ref() + .map(|v| match v { + super::super::__buffa::view::oneof::cose_algorithm_identifier::Algorithm::Signature( + v, + ) => { + super::super::__buffa::oneof::cose_algorithm_identifier::Algorithm::Signature( + *v, + ) + } + super::super::__buffa::view::oneof::cose_algorithm_identifier::Algorithm::KeyAgreement( + v, + ) => { + super::super::__buffa::oneof::cose_algorithm_identifier::Algorithm::KeyAgreement( + *v, + ) + } + super::super::__buffa::view::oneof::cose_algorithm_identifier::Algorithm::Kem( + v, + ) => { + super::super::__buffa::oneof::cose_algorithm_identifier::Algorithm::Kem( + *v, + ) + } + }), + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseAlgorithmIdentifierView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if let ::core::option::Option::Some(ref v) = self.algorithm { + match v { + super::super::__buffa::view::oneof::cose_algorithm_identifier::Algorithm::Signature( + x, + ) => { + size += 1u64 + ::buffa::types::int32_encoded_len(x.to_i32()) as u64; + } + super::super::__buffa::view::oneof::cose_algorithm_identifier::Algorithm::KeyAgreement( + x, + ) => { + size += 1u64 + ::buffa::types::int32_encoded_len(x.to_i32()) as u64; + } + super::super::__buffa::view::oneof::cose_algorithm_identifier::Algorithm::Kem( + x, + ) => { + size += 1u64 + ::buffa::types::int32_encoded_len(x.to_i32()) as u64; + } + } + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if let ::core::option::Option::Some(ref v) = self.algorithm { + match v { + super::super::__buffa::view::oneof::cose_algorithm_identifier::Algorithm::Signature( + x, + ) => { + ::buffa::types::put_int32_field(1u32, x.to_i32(), buf); + } + super::super::__buffa::view::oneof::cose_algorithm_identifier::Algorithm::KeyAgreement( + x, + ) => { + ::buffa::types::put_int32_field(2u32, x.to_i32(), buf); + } + super::super::__buffa::view::oneof::cose_algorithm_identifier::Algorithm::Kem( + x, + ) => { + ::buffa::types::put_int32_field(3u32, x.to_i32(), buf); + } + } + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseAlgorithmIdentifierView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if let ::core::option::Option::Some(ref __ov) = self.algorithm { + match __ov { + super::super::__buffa::view::oneof::cose_algorithm_identifier::Algorithm::Signature( + v, + ) => { + __map.serialize_entry("signature", v)?; + } + super::super::__buffa::view::oneof::cose_algorithm_identifier::Algorithm::KeyAgreement( + v, + ) => { + __map.serialize_entry("keyAgreement", v)?; + } + super::super::__buffa::view::oneof::cose_algorithm_identifier::Algorithm::Kem( + v, + ) => { + __map.serialize_entry("kem", v)?; + } + } + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseAlgorithmIdentifierView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseAlgorithmIdentifier"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseAlgorithmIdentifier"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseAlgorithmIdentifier"; +} +::buffa::impl_default_view_instance!(CoseAlgorithmIdentifierView); +::buffa::impl_view_reborrow!(CoseAlgorithmIdentifierView); +/** Self-contained, `'static` owned view of a `CoseAlgorithmIdentifier` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseAlgorithmIdentifierView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseAlgorithmIdentifierView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone, Debug)] +pub struct CoseAlgorithmIdentifierOwnedView( + ::buffa::OwnedView>, +); +impl CoseAlgorithmIdentifierOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseAlgorithmIdentifierOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseAlgorithmIdentifierOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseAlgorithmIdentifier, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseAlgorithmIdentifierOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseAlgorithmIdentifierView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseAlgorithmIdentifierView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseAlgorithmIdentifier { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// Oneof `algorithm`. + #[must_use] + pub fn algorithm( + &self, + ) -> ::core::option::Option< + &super::super::__buffa::view::oneof::cose_algorithm_identifier::Algorithm, + > { + self.0.reborrow().algorithm.as_ref() + } +} +impl ::core::convert::From<::buffa::OwnedView>> +for CoseAlgorithmIdentifierOwnedView { + fn from(inner: ::buffa::OwnedView>) -> Self { + CoseAlgorithmIdentifierOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseAlgorithmIdentifierOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef<::buffa::OwnedView>> +for CoseAlgorithmIdentifierOwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseAlgorithmIdentifier { + type View<'a> = CoseAlgorithmIdentifierView<'a>; + type ViewHandle = CoseAlgorithmIdentifierOwnedView; +} +impl ::serde::Serialize for CoseAlgorithmIdentifierOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +/// CoseOperationRequest is the single executable protobuf entrypoint for +/// FFI and generated-SDK adapters. Native Rust SDKs +/// should keep using typed methods; adapters can use this generated oneof to +/// avoid hand-rolled operation routing and error-envelope plumbing. +#[derive(Clone, Debug, Default)] +pub struct CoseOperationRequestView<'a> { + pub operation: ::core::option::Option< + super::super::__buffa::view::oneof::cose_operation_request::Operation<'a>, + >, + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl<'a> ::buffa::MessageView<'a> for CoseOperationRequestView<'a> { + type Owned = super::super::CoseOperationRequest; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1000u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1Create( + ref mut existing, + ), + ) = view.operation + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.operation = Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1Create( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 1001u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1CreateDetached( + ref mut existing, + ), + ) = view.operation + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.operation = Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1CreateDetached( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 1002u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1Verify( + ref mut existing, + ), + ) = view.operation + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.operation = Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1Verify( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 1003u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1VerifyDetached( + ref mut existing, + ), + ) = view.operation + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.operation = Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1VerifyDetached( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 2000u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyFromPublicBytes( + ref mut existing, + ), + ) = view.operation + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.operation = Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyFromPublicBytes( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 2001u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyFromPrivateBytes( + ref mut existing, + ), + ) = view.operation + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.operation = Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyFromPrivateBytes( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 2002u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyParse( + ref mut existing, + ), + ) = view.operation + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.operation = Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyParse( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 2003u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyToPublicBytes( + ref mut existing, + ), + ) = view.operation + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.operation = Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyToPublicBytes( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 2004u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyToPrivateBytes( + ref mut existing, + ), + ) = view.operation + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.operation = Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyToPrivateBytes( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 2005u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyDerivePublicKid( + ref mut existing, + ), + ) = view.operation + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.operation = Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyDerivePublicKid( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 2006u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyToMultikey( + ref mut existing, + ), + ) = view.operation + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.operation = Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyToMultikey( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 2007u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::MultikeyToCoseKey( + ref mut existing, + ), + ) = view.operation + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.operation = Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::MultikeyToCoseKey( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 3000u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::MlKemEncryptDirect( + ref mut existing, + ), + ) = view.operation + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.operation = Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::MlKemEncryptDirect( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 3001u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::MlKemEncryptKeyWrap( + ref mut existing, + ), + ) = view.operation + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.operation = Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::MlKemEncryptKeyWrap( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 3002u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::MlKemDecrypt( + ref mut existing, + ), + ) = view.operation + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.operation = Some( + super::super::__buffa::view::oneof::cose_operation_request::Operation::MlKemDecrypt( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result< + super::super::CoseOperationRequest, + ::buffa::DecodeError, + > { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result< + super::super::CoseOperationRequest, + ::buffa::DecodeError, + > { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseOperationRequest { + operation: match self.operation.as_ref() { + ::core::option::Option::Some(v) => { + ::core::option::Option::Some( + match v { + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1Create( + v, + ) => { + super::super::__buffa::oneof::cose_operation_request::Operation::Sign1Create( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1CreateDetached( + v, + ) => { + super::super::__buffa::oneof::cose_operation_request::Operation::Sign1CreateDetached( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1Verify( + v, + ) => { + super::super::__buffa::oneof::cose_operation_request::Operation::Sign1Verify( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1VerifyDetached( + v, + ) => { + super::super::__buffa::oneof::cose_operation_request::Operation::Sign1VerifyDetached( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyFromPublicBytes( + v, + ) => { + super::super::__buffa::oneof::cose_operation_request::Operation::KeyFromPublicBytes( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyFromPrivateBytes( + v, + ) => { + super::super::__buffa::oneof::cose_operation_request::Operation::KeyFromPrivateBytes( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyParse( + v, + ) => { + super::super::__buffa::oneof::cose_operation_request::Operation::KeyParse( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyToPublicBytes( + v, + ) => { + super::super::__buffa::oneof::cose_operation_request::Operation::KeyToPublicBytes( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyToPrivateBytes( + v, + ) => { + super::super::__buffa::oneof::cose_operation_request::Operation::KeyToPrivateBytes( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyDerivePublicKid( + v, + ) => { + super::super::__buffa::oneof::cose_operation_request::Operation::KeyDerivePublicKid( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyToMultikey( + v, + ) => { + super::super::__buffa::oneof::cose_operation_request::Operation::KeyToMultikey( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::MultikeyToCoseKey( + v, + ) => { + super::super::__buffa::oneof::cose_operation_request::Operation::MultikeyToCoseKey( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::MlKemEncryptDirect( + v, + ) => { + super::super::__buffa::oneof::cose_operation_request::Operation::MlKemEncryptDirect( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::MlKemEncryptKeyWrap( + v, + ) => { + super::super::__buffa::oneof::cose_operation_request::Operation::MlKemEncryptKeyWrap( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::MlKemDecrypt( + v, + ) => { + super::super::__buffa::oneof::cose_operation_request::Operation::MlKemDecrypt( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + }, + ) + } + ::core::option::Option::None => ::core::option::Option::None, + }, + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseOperationRequestView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, __cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if let ::core::option::Option::Some(ref v) = self.operation { + match v { + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1Create( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1CreateDetached( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1Verify( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1VerifyDetached( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyFromPublicBytes( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyFromPrivateBytes( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyParse( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyToPublicBytes( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyToPrivateBytes( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyDerivePublicKid( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyToMultikey( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::MultikeyToCoseKey( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::MlKemEncryptDirect( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 3u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::MlKemEncryptKeyWrap( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 3u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::MlKemDecrypt( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 3u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + } + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + __cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if let ::core::option::Option::Some(ref v) = self.operation { + match v { + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1Create( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 1000u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1CreateDetached( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 1001u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1Verify( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 1002u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1VerifyDetached( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 1003u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyFromPublicBytes( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2000u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyFromPrivateBytes( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2001u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyParse( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2002u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyToPublicBytes( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2003u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyToPrivateBytes( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2004u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyDerivePublicKid( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2005u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyToMultikey( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2006u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::MultikeyToCoseKey( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2007u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::MlKemEncryptDirect( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 3000u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::MlKemEncryptKeyWrap( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 3001u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::MlKemDecrypt( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 3002u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + } + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseOperationRequestView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if let ::core::option::Option::Some(ref __ov) = self.operation { + match __ov { + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1Create( + v, + ) => { + __map.serialize_entry("sign1Create", v)?; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1CreateDetached( + v, + ) => { + __map.serialize_entry("sign1CreateDetached", v)?; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1Verify( + v, + ) => { + __map.serialize_entry("sign1Verify", v)?; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::Sign1VerifyDetached( + v, + ) => { + __map.serialize_entry("sign1VerifyDetached", v)?; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyFromPublicBytes( + v, + ) => { + __map.serialize_entry("keyFromPublicBytes", v)?; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyFromPrivateBytes( + v, + ) => { + __map.serialize_entry("keyFromPrivateBytes", v)?; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyParse( + v, + ) => { + __map.serialize_entry("keyParse", v)?; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyToPublicBytes( + v, + ) => { + __map.serialize_entry("keyToPublicBytes", v)?; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyToPrivateBytes( + v, + ) => { + __map.serialize_entry("keyToPrivateBytes", v)?; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyDerivePublicKid( + v, + ) => { + __map.serialize_entry("keyDerivePublicKid", v)?; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::KeyToMultikey( + v, + ) => { + __map.serialize_entry("keyToMultikey", v)?; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::MultikeyToCoseKey( + v, + ) => { + __map.serialize_entry("multikeyToCoseKey", v)?; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::MlKemEncryptDirect( + v, + ) => { + __map.serialize_entry("mlKemEncryptDirect", v)?; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::MlKemEncryptKeyWrap( + v, + ) => { + __map.serialize_entry("mlKemEncryptKeyWrap", v)?; + } + super::super::__buffa::view::oneof::cose_operation_request::Operation::MlKemDecrypt( + v, + ) => { + __map.serialize_entry("mlKemDecrypt", v)?; + } + } + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseOperationRequestView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseOperationRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseOperationRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseOperationRequest"; +} +::buffa::impl_default_view_instance!(CoseOperationRequestView); +::buffa::impl_view_reborrow!(CoseOperationRequestView); +/** Self-contained, `'static` owned view of a `CoseOperationRequest` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseOperationRequestView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseOperationRequestView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone, Debug)] +pub struct CoseOperationRequestOwnedView( + ::buffa::OwnedView>, +); +impl CoseOperationRequestOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseOperationRequestOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseOperationRequestOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseOperationRequest, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseOperationRequestOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseOperationRequestView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseOperationRequestView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseOperationRequest { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// Oneof `operation`. + #[must_use] + pub fn operation( + &self, + ) -> ::core::option::Option< + &super::super::__buffa::view::oneof::cose_operation_request::Operation<'_>, + > { + self.0.reborrow().operation.as_ref() + } +} +impl ::core::convert::From<::buffa::OwnedView>> +for CoseOperationRequestOwnedView { + fn from(inner: ::buffa::OwnedView>) -> Self { + CoseOperationRequestOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseOperationRequestOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef<::buffa::OwnedView>> +for CoseOperationRequestOwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseOperationRequest { + type View<'a> = CoseOperationRequestView<'a>; + type ViewHandle = CoseOperationRequestOwnedView; +} +impl ::serde::Serialize for CoseOperationRequestOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +/// CoseOperationResponseV2 is the executable response contract. Its generated +/// oneof distinguishes success from failure without an out-of-band status or an +/// opaque operation-specific payload. +#[derive(Clone, Debug, Default)] +pub struct CoseOperationResponseV2View<'a> { + pub outcome: ::core::option::Option< + super::super::__buffa::view::oneof::cose_operation_response_v2::Outcome<'a>, + >, + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl<'a> ::buffa::MessageView<'a> for CoseOperationResponseV2View<'a> { + type Owned = super::super::CoseOperationResponseV2; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_response_v2::Outcome::Result( + ref mut existing, + ), + ) = view.outcome + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.outcome = Some( + super::super::__buffa::view::oneof::cose_operation_response_v2::Outcome::Result( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_response_v2::Outcome::Error( + ref mut existing, + ), + ) = view.outcome + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.outcome = Some( + super::super::__buffa::view::oneof::cose_operation_response_v2::Outcome::Error( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result< + super::super::CoseOperationResponseV2, + ::buffa::DecodeError, + > { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result< + super::super::CoseOperationResponseV2, + ::buffa::DecodeError, + > { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseOperationResponseV2 { + outcome: match self.outcome.as_ref() { + ::core::option::Option::Some(v) => { + ::core::option::Option::Some( + match v { + super::super::__buffa::view::oneof::cose_operation_response_v2::Outcome::Result( + v, + ) => { + super::super::__buffa::oneof::cose_operation_response_v2::Outcome::Result( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_response_v2::Outcome::Error( + v, + ) => { + super::super::__buffa::oneof::cose_operation_response_v2::Outcome::Error( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + }, + ) + } + ::core::option::Option::None => ::core::option::Option::None, + }, + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseOperationResponseV2View<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, __cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if let ::core::option::Option::Some(ref v) = self.outcome { + match v { + super::super::__buffa::view::oneof::cose_operation_response_v2::Outcome::Result( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 1u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_response_v2::Outcome::Error( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 1u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + } + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + __cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if let ::core::option::Option::Some(ref v) = self.outcome { + match v { + super::super::__buffa::view::oneof::cose_operation_response_v2::Outcome::Result( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 1u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_response_v2::Outcome::Error( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + } + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseOperationResponseV2View<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if let ::core::option::Option::Some(ref __ov) = self.outcome { + match __ov { + super::super::__buffa::view::oneof::cose_operation_response_v2::Outcome::Result( + v, + ) => { + __map.serialize_entry("result", v)?; + } + super::super::__buffa::view::oneof::cose_operation_response_v2::Outcome::Error( + v, + ) => { + __map.serialize_entry("error", v)?; + } + } + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseOperationResponseV2View<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseOperationResponseV2"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseOperationResponseV2"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseOperationResponseV2"; +} +::buffa::impl_default_view_instance!(CoseOperationResponseV2View); +::buffa::impl_view_reborrow!(CoseOperationResponseV2View); +/** Self-contained, `'static` owned view of a `CoseOperationResponseV2` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseOperationResponseV2View`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseOperationResponseV2View`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone, Debug)] +pub struct CoseOperationResponseV2OwnedView( + ::buffa::OwnedView>, +); +impl CoseOperationResponseV2OwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseOperationResponseV2OwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseOperationResponseV2OwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseOperationResponseV2, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseOperationResponseV2OwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseOperationResponseV2View`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseOperationResponseV2View<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseOperationResponseV2 { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// Oneof `outcome`. + #[must_use] + pub fn outcome( + &self, + ) -> ::core::option::Option< + &super::super::__buffa::view::oneof::cose_operation_response_v2::Outcome<'_>, + > { + self.0.reborrow().outcome.as_ref() + } +} +impl ::core::convert::From<::buffa::OwnedView>> +for CoseOperationResponseV2OwnedView { + fn from(inner: ::buffa::OwnedView>) -> Self { + CoseOperationResponseV2OwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseOperationResponseV2OwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef<::buffa::OwnedView>> +for CoseOperationResponseV2OwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseOperationResponseV2 { + type View<'a> = CoseOperationResponseV2View<'a>; + type ViewHandle = CoseOperationResponseV2OwnedView; +} +impl ::serde::Serialize for CoseOperationResponseV2OwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +/// CoseOperationResult identifies the exact operation that produced a +/// successful response. Field numbers mirror CoseOperationRequest so generated +/// clients can audit request/result pairing without a separate registry. +#[derive(Clone, Debug, Default)] +pub struct CoseOperationResultView<'a> { + pub result: ::core::option::Option< + super::super::__buffa::view::oneof::cose_operation_result::Result<'a>, + >, + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl<'a> ::buffa::MessageView<'a> for CoseOperationResultView<'a> { + type Owned = super::super::CoseOperationResult; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1000u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1Create( + ref mut existing, + ), + ) = view.result + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.result = Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1Create( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 1001u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1CreateDetached( + ref mut existing, + ), + ) = view.result + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.result = Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1CreateDetached( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 1002u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1Verify( + ref mut existing, + ), + ) = view.result + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.result = Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1Verify( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 1003u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1VerifyDetached( + ref mut existing, + ), + ) = view.result + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.result = Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1VerifyDetached( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 2000u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyFromPublicBytes( + ref mut existing, + ), + ) = view.result + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.result = Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyFromPublicBytes( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 2001u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyFromPrivateBytes( + ref mut existing, + ), + ) = view.result + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.result = Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyFromPrivateBytes( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 2002u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyParse( + ref mut existing, + ), + ) = view.result + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.result = Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyParse( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 2003u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyToPublicBytes( + ref mut existing, + ), + ) = view.result + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.result = Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyToPublicBytes( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 2004u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyToPrivateBytes( + ref mut existing, + ), + ) = view.result + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.result = Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyToPrivateBytes( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 2005u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyDerivePublicKid( + ref mut existing, + ), + ) = view.result + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.result = Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyDerivePublicKid( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 2006u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyToMultikey( + ref mut existing, + ), + ) = view.result + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.result = Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyToMultikey( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 2007u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::MultikeyToCoseKey( + ref mut existing, + ), + ) = view.result + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.result = Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::MultikeyToCoseKey( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 3000u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::MlKemEncryptDirect( + ref mut existing, + ), + ) = view.result + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.result = Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::MlKemEncryptDirect( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 3001u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::MlKemEncryptKeyWrap( + ref mut existing, + ), + ) = view.result + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.result = Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::MlKemEncryptKeyWrap( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + 3002u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + if let Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::MlKemDecrypt( + ref mut existing, + ), + ) = view.result + { + ::buffa::MessageView::merge_into_view( + &mut **existing, + sub, + __sub_ctx, + )?; + } else { + view.result = Some( + super::super::__buffa::view::oneof::cose_operation_result::Result::MlKemDecrypt( + ::buffa::alloc::boxed::Box::new( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ), + ), + ); + } + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result< + super::super::CoseOperationResult, + ::buffa::DecodeError, + > { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result< + super::super::CoseOperationResult, + ::buffa::DecodeError, + > { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseOperationResult { + result: match self.result.as_ref() { + ::core::option::Option::Some(v) => { + ::core::option::Option::Some( + match v { + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1Create( + v, + ) => { + super::super::__buffa::oneof::cose_operation_result::Result::Sign1Create( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1CreateDetached( + v, + ) => { + super::super::__buffa::oneof::cose_operation_result::Result::Sign1CreateDetached( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1Verify( + v, + ) => { + super::super::__buffa::oneof::cose_operation_result::Result::Sign1Verify( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1VerifyDetached( + v, + ) => { + super::super::__buffa::oneof::cose_operation_result::Result::Sign1VerifyDetached( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyFromPublicBytes( + v, + ) => { + super::super::__buffa::oneof::cose_operation_result::Result::KeyFromPublicBytes( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyFromPrivateBytes( + v, + ) => { + super::super::__buffa::oneof::cose_operation_result::Result::KeyFromPrivateBytes( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyParse( + v, + ) => { + super::super::__buffa::oneof::cose_operation_result::Result::KeyParse( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyToPublicBytes( + v, + ) => { + super::super::__buffa::oneof::cose_operation_result::Result::KeyToPublicBytes( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyToPrivateBytes( + v, + ) => { + super::super::__buffa::oneof::cose_operation_result::Result::KeyToPrivateBytes( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyDerivePublicKid( + v, + ) => { + super::super::__buffa::oneof::cose_operation_result::Result::KeyDerivePublicKid( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyToMultikey( + v, + ) => { + super::super::__buffa::oneof::cose_operation_result::Result::KeyToMultikey( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_result::Result::MultikeyToCoseKey( + v, + ) => { + super::super::__buffa::oneof::cose_operation_result::Result::MultikeyToCoseKey( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_result::Result::MlKemEncryptDirect( + v, + ) => { + super::super::__buffa::oneof::cose_operation_result::Result::MlKemEncryptDirect( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_result::Result::MlKemEncryptKeyWrap( + v, + ) => { + super::super::__buffa::oneof::cose_operation_result::Result::MlKemEncryptKeyWrap( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + super::super::__buffa::view::oneof::cose_operation_result::Result::MlKemDecrypt( + v, + ) => { + super::super::__buffa::oneof::cose_operation_result::Result::MlKemDecrypt( + ::buffa::alloc::boxed::Box::new( + v.to_owned_from_source(__buffa_src)?, + ), + ) + } + }, + ) + } + ::core::option::Option::None => ::core::option::Option::None, + }, + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseOperationResultView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, __cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if let ::core::option::Option::Some(ref v) = self.result { + match v { + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1Create( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1CreateDetached( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1Verify( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1VerifyDetached( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyFromPublicBytes( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyFromPrivateBytes( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyParse( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyToPublicBytes( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyToPrivateBytes( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyDerivePublicKid( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyToMultikey( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::MultikeyToCoseKey( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::MlKemEncryptDirect( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 3u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::MlKemEncryptKeyWrap( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 3u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::MlKemDecrypt( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 3u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + } + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + __cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if let ::core::option::Option::Some(ref v) = self.result { + match v { + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1Create( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 1000u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1CreateDetached( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 1001u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1Verify( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 1002u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1VerifyDetached( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 1003u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyFromPublicBytes( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2000u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyFromPrivateBytes( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2001u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyParse( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2002u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyToPublicBytes( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2003u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyToPrivateBytes( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2004u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyDerivePublicKid( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2005u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyToMultikey( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2006u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_result::Result::MultikeyToCoseKey( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2007u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_result::Result::MlKemEncryptDirect( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 3000u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_result::Result::MlKemEncryptKeyWrap( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 3001u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + super::super::__buffa::view::oneof::cose_operation_result::Result::MlKemDecrypt( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 3002u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + } + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseOperationResultView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if let ::core::option::Option::Some(ref __ov) = self.result { + match __ov { + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1Create( + v, + ) => { + __map.serialize_entry("sign1Create", v)?; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1CreateDetached( + v, + ) => { + __map.serialize_entry("sign1CreateDetached", v)?; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1Verify( + v, + ) => { + __map.serialize_entry("sign1Verify", v)?; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::Sign1VerifyDetached( + v, + ) => { + __map.serialize_entry("sign1VerifyDetached", v)?; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyFromPublicBytes( + v, + ) => { + __map.serialize_entry("keyFromPublicBytes", v)?; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyFromPrivateBytes( + v, + ) => { + __map.serialize_entry("keyFromPrivateBytes", v)?; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyParse( + v, + ) => { + __map.serialize_entry("keyParse", v)?; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyToPublicBytes( + v, + ) => { + __map.serialize_entry("keyToPublicBytes", v)?; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyToPrivateBytes( + v, + ) => { + __map.serialize_entry("keyToPrivateBytes", v)?; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyDerivePublicKid( + v, + ) => { + __map.serialize_entry("keyDerivePublicKid", v)?; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::KeyToMultikey( + v, + ) => { + __map.serialize_entry("keyToMultikey", v)?; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::MultikeyToCoseKey( + v, + ) => { + __map.serialize_entry("multikeyToCoseKey", v)?; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::MlKemEncryptDirect( + v, + ) => { + __map.serialize_entry("mlKemEncryptDirect", v)?; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::MlKemEncryptKeyWrap( + v, + ) => { + __map.serialize_entry("mlKemEncryptKeyWrap", v)?; + } + super::super::__buffa::view::oneof::cose_operation_result::Result::MlKemDecrypt( + v, + ) => { + __map.serialize_entry("mlKemDecrypt", v)?; + } + } + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseOperationResultView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseOperationResult"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseOperationResult"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseOperationResult"; +} +::buffa::impl_default_view_instance!(CoseOperationResultView); +::buffa::impl_view_reborrow!(CoseOperationResultView); +/** Self-contained, `'static` owned view of a `CoseOperationResult` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseOperationResultView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseOperationResultView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone, Debug)] +pub struct CoseOperationResultOwnedView( + ::buffa::OwnedView>, +); +impl CoseOperationResultOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseOperationResultOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseOperationResultOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseOperationResult, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseOperationResultOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseOperationResultView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseOperationResultView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseOperationResult { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// Oneof `result`. + #[must_use] + pub fn result( + &self, + ) -> ::core::option::Option< + &super::super::__buffa::view::oneof::cose_operation_result::Result<'_>, + > { + self.0.reborrow().result.as_ref() + } +} +impl ::core::convert::From<::buffa::OwnedView>> +for CoseOperationResultOwnedView { + fn from(inner: ::buffa::OwnedView>) -> Self { + CoseOperationResultOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseOperationResultOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef<::buffa::OwnedView>> +for CoseOperationResultOwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseOperationResult { + type View<'a> = CoseOperationResultView<'a>; + type ViewHandle = CoseOperationResultOwnedView; +} +impl ::serde::Serialize for CoseOperationResultOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +/// CoseMlKemEncryptRequest creates one-recipient COSE_Encrypt using the +/// ReallyMe pre-IANA ML-KEM profile. +#[derive(Clone, Default)] +pub struct CoseMlKemEncryptRequestView<'a> { + /// ML-KEM-512, ML-KEM-768, or ML-KEM-1024. + /// + /// Field 1: `kem_algorithm` + pub kem_algorithm: ::buffa::EnumValue, + /// Protected content-encryption algorithm. + /// + /// Field 2: `content_algorithm` + pub content_algorithm: ::buffa::EnumValue< + super::super::CoseContentEncryptionAlgorithm, + >, + /// Recipient FIPS 203 ML-KEM encapsulation public key. + /// + /// Field 3: `recipient_public_key` + pub recipient_public_key: &'a [u8], + /// SHA-256 thumbprint of the canonical public-only, algorithm-bound COSE_Key. + /// The encrypt operation rejects any value that does not identify + /// recipient_public_key exactly. + /// + /// Field 4: `recipient_kid` + pub recipient_kid: &'a [u8], + /// SENSITIVE: plaintext encrypted into the COSE_Encrypt ciphertext. + /// + /// Field 5: `plaintext` + pub plaintext: &'a [u8], + /// SENSITIVE: external AAD authenticated but not carried in COSE. + /// + /// Field 6: `external_aad` + pub external_aad: &'a [u8], + /// SENSITIVE: mutually known private KDF context agreed out of band. + /// + /// Field 7: `supp_priv_info` + pub supp_priv_info: &'a [u8], + /// Distinguishes omitted SuppPrivInfo from an intentionally empty byte string. + /// + /// Field 8: `has_supp_priv_info` + pub has_supp_priv_info: bool, + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl ::core::fmt::Debug for CoseMlKemEncryptRequestView<'_> { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseMlKemEncryptRequestView()") + } +} +impl<'a> ::buffa::MessageView<'a> for CoseMlKemEncryptRequestView<'a> { + type Owned = super::super::CoseMlKemEncryptRequest; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.kem_algorithm = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(&mut cur)?, + ); + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.content_algorithm = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(&mut cur)?, + ); + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.recipient_public_key = ::buffa::types::borrow_bytes(&mut cur)?; + } + 4u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.recipient_kid = ::buffa::types::borrow_bytes(&mut cur)?; + } + 5u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.plaintext = ::buffa::types::borrow_bytes(&mut cur)?; + } + 6u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.external_aad = ::buffa::types::borrow_bytes(&mut cur)?; + } + 7u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.supp_priv_info = ::buffa::types::borrow_bytes(&mut cur)?; + } + 8u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.has_supp_priv_info = ::buffa::types::decode_bool(&mut cur)?; + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result< + super::super::CoseMlKemEncryptRequest, + ::buffa::DecodeError, + > { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result< + super::super::CoseMlKemEncryptRequest, + ::buffa::DecodeError, + > { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseMlKemEncryptRequest { + kem_algorithm: self.kem_algorithm, + content_algorithm: self.content_algorithm, + recipient_public_key: (self.recipient_public_key).to_vec(), + recipient_kid: (self.recipient_kid).to_vec(), + plaintext: (self.plaintext).to_vec(), + external_aad: (self.external_aad).to_vec(), + supp_priv_info: (self.supp_priv_info).to_vec(), + has_supp_priv_info: self.has_supp_priv_info, + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseMlKemEncryptRequestView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + { + let val = self.kem_algorithm.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + { + let val = self.content_algorithm.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + if !self.recipient_public_key.is_empty() { + size + += 1u64 + + ::buffa::types::bytes_encoded_len(&self.recipient_public_key) + as u64; + } + if !self.recipient_kid.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.recipient_kid) as u64; + } + if !self.plaintext.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.plaintext) as u64; + } + if !self.external_aad.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.external_aad) as u64; + } + if !self.supp_priv_info.is_empty() { + size + += 1u64 + ::buffa::types::bytes_encoded_len(&self.supp_priv_info) as u64; + } + if self.has_supp_priv_info { + size += 1u64 + ::buffa::types::BOOL_ENCODED_LEN as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + { + let val = self.kem_algorithm.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(1u32, val, buf); + } + } + { + let val = self.content_algorithm.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(2u32, val, buf); + } + } + if !self.recipient_public_key.is_empty() { + ::buffa::types::put_shared_bytes_field( + 3u32, + &self.recipient_public_key, + buf, + ); + } + if !self.recipient_kid.is_empty() { + ::buffa::types::put_shared_bytes_field(4u32, &self.recipient_kid, buf); + } + if !self.plaintext.is_empty() { + ::buffa::types::put_shared_bytes_field(5u32, &self.plaintext, buf); + } + if !self.external_aad.is_empty() { + ::buffa::types::put_shared_bytes_field(6u32, &self.external_aad, buf); + } + if !self.supp_priv_info.is_empty() { + ::buffa::types::put_shared_bytes_field(7u32, &self.supp_priv_info, buf); + } + if self.has_supp_priv_info { + ::buffa::types::put_bool_field(8u32, self.has_supp_priv_info, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseMlKemEncryptRequestView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if !::buffa::json_helpers::skip_if::is_default_enum_value(&self.kem_algorithm) { + __map.serialize_entry("kemAlgorithm", &self.kem_algorithm)?; + } + if !::buffa::json_helpers::skip_if::is_default_enum_value( + &self.content_algorithm, + ) { + __map.serialize_entry("contentAlgorithm", &self.content_algorithm)?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.recipient_public_key) { + __map + .serialize_entry( + "recipientPublicKey", + &::buffa::json_helpers::BytesJson(self.recipient_public_key), + )?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.recipient_kid) { + __map + .serialize_entry( + "recipientKid", + &::buffa::json_helpers::BytesJson(self.recipient_kid), + )?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.plaintext) { + __map + .serialize_entry( + "plaintext", + &::buffa::json_helpers::BytesJson(self.plaintext), + )?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.external_aad) { + __map + .serialize_entry( + "externalAad", + &::buffa::json_helpers::BytesJson(self.external_aad), + )?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.supp_priv_info) { + __map + .serialize_entry( + "suppPrivInfo", + &::buffa::json_helpers::BytesJson(self.supp_priv_info), + )?; + } + if self.has_supp_priv_info { + __map.serialize_entry("hasSuppPrivInfo", &self.has_supp_priv_info)?; + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseMlKemEncryptRequestView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseMlKemEncryptRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseMlKemEncryptRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseMlKemEncryptRequest"; +} +::buffa::impl_default_view_instance!(CoseMlKemEncryptRequestView); +::buffa::impl_view_reborrow!(CoseMlKemEncryptRequestView); +/** Self-contained, `'static` owned view of a `CoseMlKemEncryptRequest` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseMlKemEncryptRequestView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseMlKemEncryptRequestView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone)] +pub struct CoseMlKemEncryptRequestOwnedView( + ::buffa::OwnedView>, +); +impl ::core::fmt::Debug for CoseMlKemEncryptRequestOwnedView { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseMlKemEncryptRequestOwnedView()") + } +} +impl CoseMlKemEncryptRequestOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseMlKemEncryptRequestOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseMlKemEncryptRequestOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseMlKemEncryptRequest, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseMlKemEncryptRequestOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseMlKemEncryptRequestView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseMlKemEncryptRequestView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseMlKemEncryptRequest { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// ML-KEM-512, ML-KEM-768, or ML-KEM-1024. + /// + /// Field 1: `kem_algorithm` + #[must_use] + pub fn kem_algorithm(&self) -> ::buffa::EnumValue { + self.0.reborrow().kem_algorithm + } + /// Protected content-encryption algorithm. + /// + /// Field 2: `content_algorithm` + #[must_use] + pub fn content_algorithm( + &self, + ) -> ::buffa::EnumValue { + self.0.reborrow().content_algorithm + } + /// Recipient FIPS 203 ML-KEM encapsulation public key. + /// + /// Field 3: `recipient_public_key` + #[must_use] + pub fn recipient_public_key(&self) -> &'_ [u8] { + self.0.reborrow().recipient_public_key + } + /// SHA-256 thumbprint of the canonical public-only, algorithm-bound COSE_Key. + /// The encrypt operation rejects any value that does not identify + /// recipient_public_key exactly. + /// + /// Field 4: `recipient_kid` + #[must_use] + pub fn recipient_kid(&self) -> &'_ [u8] { + self.0.reborrow().recipient_kid + } + /// SENSITIVE: plaintext encrypted into the COSE_Encrypt ciphertext. + /// + /// Field 5: `plaintext` + #[must_use] + pub fn plaintext(&self) -> &'_ [u8] { + self.0.reborrow().plaintext + } + /// SENSITIVE: external AAD authenticated but not carried in COSE. + /// + /// Field 6: `external_aad` + #[must_use] + pub fn external_aad(&self) -> &'_ [u8] { + self.0.reborrow().external_aad + } + /// SENSITIVE: mutually known private KDF context agreed out of band. + /// + /// Field 7: `supp_priv_info` + #[must_use] + pub fn supp_priv_info(&self) -> &'_ [u8] { + self.0.reborrow().supp_priv_info + } + /// Distinguishes omitted SuppPrivInfo from an intentionally empty byte string. + /// + /// Field 8: `has_supp_priv_info` + #[must_use] + pub fn has_supp_priv_info(&self) -> bool { + self.0.reborrow().has_supp_priv_info + } +} +impl ::core::convert::From<::buffa::OwnedView>> +for CoseMlKemEncryptRequestOwnedView { + fn from(inner: ::buffa::OwnedView>) -> Self { + CoseMlKemEncryptRequestOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseMlKemEncryptRequestOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef<::buffa::OwnedView>> +for CoseMlKemEncryptRequestOwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseMlKemEncryptRequest { + type View<'a> = CoseMlKemEncryptRequestView<'a>; + type ViewHandle = CoseMlKemEncryptRequestOwnedView; +} +impl ::serde::Serialize for CoseMlKemEncryptRequestOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +#[derive(Clone, Default)] +pub struct CoseMlKemEncryptResultView<'a> { + /// SENSITIVE: tagged COSE_Encrypt containing encrypted application content. + /// + /// Field 1: `cose_encrypt` + pub cose_encrypt: &'a [u8], + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl ::core::fmt::Debug for CoseMlKemEncryptResultView<'_> { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseMlKemEncryptResultView()") + } +} +impl<'a> ::buffa::MessageView<'a> for CoseMlKemEncryptResultView<'a> { + type Owned = super::super::CoseMlKemEncryptResult; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.cose_encrypt = ::buffa::types::borrow_bytes(&mut cur)?; + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result< + super::super::CoseMlKemEncryptResult, + ::buffa::DecodeError, + > { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result< + super::super::CoseMlKemEncryptResult, + ::buffa::DecodeError, + > { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseMlKemEncryptResult { + cose_encrypt: (self.cose_encrypt).to_vec(), + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseMlKemEncryptResultView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.cose_encrypt.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.cose_encrypt) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.cose_encrypt.is_empty() { + ::buffa::types::put_shared_bytes_field(1u32, &self.cose_encrypt, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseMlKemEncryptResultView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.cose_encrypt) { + __map + .serialize_entry( + "coseEncrypt", + &::buffa::json_helpers::BytesJson(self.cose_encrypt), + )?; + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseMlKemEncryptResultView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseMlKemEncryptResult"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseMlKemEncryptResult"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseMlKemEncryptResult"; +} +::buffa::impl_default_view_instance!(CoseMlKemEncryptResultView); +::buffa::impl_view_reborrow!(CoseMlKemEncryptResultView); +/** Self-contained, `'static` owned view of a `CoseMlKemEncryptResult` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseMlKemEncryptResultView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseMlKemEncryptResultView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone)] +pub struct CoseMlKemEncryptResultOwnedView( + ::buffa::OwnedView>, +); +impl ::core::fmt::Debug for CoseMlKemEncryptResultOwnedView { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseMlKemEncryptResultOwnedView()") + } +} +impl CoseMlKemEncryptResultOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseMlKemEncryptResultOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseMlKemEncryptResultOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseMlKemEncryptResult, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseMlKemEncryptResultOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseMlKemEncryptResultView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseMlKemEncryptResultView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseMlKemEncryptResult { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// SENSITIVE: tagged COSE_Encrypt containing encrypted application content. + /// + /// Field 1: `cose_encrypt` + #[must_use] + pub fn cose_encrypt(&self) -> &'_ [u8] { + self.0.reborrow().cose_encrypt + } +} +impl ::core::convert::From<::buffa::OwnedView>> +for CoseMlKemEncryptResultOwnedView { + fn from(inner: ::buffa::OwnedView>) -> Self { + CoseMlKemEncryptResultOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseMlKemEncryptResultOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef<::buffa::OwnedView>> +for CoseMlKemEncryptResultOwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseMlKemEncryptResult { + type View<'a> = CoseMlKemEncryptResultView<'a>; + type ViewHandle = CoseMlKemEncryptResultOwnedView; +} +impl ::serde::Serialize for CoseMlKemEncryptResultOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +/// CoseMlKemDecryptRequest authenticates and decrypts the ReallyMe pre-IANA +/// ML-KEM COSE profile. +#[derive(Clone, Default)] +pub struct CoseMlKemDecryptRequestView<'a> { + /// SENSITIVE: tagged or untagged COSE_Encrypt bytes. + /// + /// Field 1: `cose_encrypt` + pub cose_encrypt: &'a [u8], + /// SENSITIVE: 64-octet FIPS 203 private seed d || z. + /// + /// Field 2: `recipient_private_key` + pub recipient_private_key: &'a [u8], + /// Expected SHA-256 canonical public COSE_Key thumbprint. Decryption derives + /// the public key from recipient_private_key and rejects a protected kid + /// bound to any other key before decapsulation. + /// + /// Field 3: `expected_recipient_kid` + pub expected_recipient_kid: &'a [u8], + /// SENSITIVE: external AAD authenticated but not carried in COSE. + /// + /// Field 4: `external_aad` + pub external_aad: &'a [u8], + /// SENSITIVE: mutually known private KDF context agreed out of band. + /// + /// Field 5: `supp_priv_info` + pub supp_priv_info: &'a [u8], + /// Distinguishes omitted SuppPrivInfo from an intentionally empty byte string. + /// + /// Field 6: `has_supp_priv_info` + pub has_supp_priv_info: bool, + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl ::core::fmt::Debug for CoseMlKemDecryptRequestView<'_> { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseMlKemDecryptRequestView()") + } +} +impl<'a> ::buffa::MessageView<'a> for CoseMlKemDecryptRequestView<'a> { + type Owned = super::super::CoseMlKemDecryptRequest; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.cose_encrypt = ::buffa::types::borrow_bytes(&mut cur)?; + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.recipient_private_key = ::buffa::types::borrow_bytes(&mut cur)?; + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.expected_recipient_kid = ::buffa::types::borrow_bytes(&mut cur)?; + } + 4u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.external_aad = ::buffa::types::borrow_bytes(&mut cur)?; + } + 5u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.supp_priv_info = ::buffa::types::borrow_bytes(&mut cur)?; + } + 6u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.has_supp_priv_info = ::buffa::types::decode_bool(&mut cur)?; + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result< + super::super::CoseMlKemDecryptRequest, + ::buffa::DecodeError, + > { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result< + super::super::CoseMlKemDecryptRequest, + ::buffa::DecodeError, + > { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseMlKemDecryptRequest { + cose_encrypt: (self.cose_encrypt).to_vec(), + recipient_private_key: (self.recipient_private_key).to_vec(), + expected_recipient_kid: (self.expected_recipient_kid).to_vec(), + external_aad: (self.external_aad).to_vec(), + supp_priv_info: (self.supp_priv_info).to_vec(), + has_supp_priv_info: self.has_supp_priv_info, + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseMlKemDecryptRequestView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.cose_encrypt.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.cose_encrypt) as u64; + } + if !self.recipient_private_key.is_empty() { + size + += 1u64 + + ::buffa::types::bytes_encoded_len(&self.recipient_private_key) + as u64; + } + if !self.expected_recipient_kid.is_empty() { + size + += 1u64 + + ::buffa::types::bytes_encoded_len(&self.expected_recipient_kid) + as u64; + } + if !self.external_aad.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.external_aad) as u64; + } + if !self.supp_priv_info.is_empty() { + size + += 1u64 + ::buffa::types::bytes_encoded_len(&self.supp_priv_info) as u64; + } + if self.has_supp_priv_info { + size += 1u64 + ::buffa::types::BOOL_ENCODED_LEN as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.cose_encrypt.is_empty() { + ::buffa::types::put_shared_bytes_field(1u32, &self.cose_encrypt, buf); + } + if !self.recipient_private_key.is_empty() { + ::buffa::types::put_shared_bytes_field( + 2u32, + &self.recipient_private_key, + buf, + ); + } + if !self.expected_recipient_kid.is_empty() { + ::buffa::types::put_shared_bytes_field( + 3u32, + &self.expected_recipient_kid, + buf, + ); + } + if !self.external_aad.is_empty() { + ::buffa::types::put_shared_bytes_field(4u32, &self.external_aad, buf); + } + if !self.supp_priv_info.is_empty() { + ::buffa::types::put_shared_bytes_field(5u32, &self.supp_priv_info, buf); + } + if self.has_supp_priv_info { + ::buffa::types::put_bool_field(6u32, self.has_supp_priv_info, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseMlKemDecryptRequestView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.cose_encrypt) { + __map + .serialize_entry( + "coseEncrypt", + &::buffa::json_helpers::BytesJson(self.cose_encrypt), + )?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.recipient_private_key) { + __map + .serialize_entry( + "recipientPrivateKey", + &::buffa::json_helpers::BytesJson(self.recipient_private_key), + )?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.expected_recipient_kid) { + __map + .serialize_entry( + "expectedRecipientKid", + &::buffa::json_helpers::BytesJson(self.expected_recipient_kid), + )?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.external_aad) { + __map + .serialize_entry( + "externalAad", + &::buffa::json_helpers::BytesJson(self.external_aad), + )?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.supp_priv_info) { + __map + .serialize_entry( + "suppPrivInfo", + &::buffa::json_helpers::BytesJson(self.supp_priv_info), + )?; + } + if self.has_supp_priv_info { + __map.serialize_entry("hasSuppPrivInfo", &self.has_supp_priv_info)?; + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseMlKemDecryptRequestView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseMlKemDecryptRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseMlKemDecryptRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseMlKemDecryptRequest"; +} +::buffa::impl_default_view_instance!(CoseMlKemDecryptRequestView); +::buffa::impl_view_reborrow!(CoseMlKemDecryptRequestView); +/** Self-contained, `'static` owned view of a `CoseMlKemDecryptRequest` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseMlKemDecryptRequestView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseMlKemDecryptRequestView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone)] +pub struct CoseMlKemDecryptRequestOwnedView( + ::buffa::OwnedView>, +); +impl ::core::fmt::Debug for CoseMlKemDecryptRequestOwnedView { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseMlKemDecryptRequestOwnedView()") + } +} +impl CoseMlKemDecryptRequestOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseMlKemDecryptRequestOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseMlKemDecryptRequestOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseMlKemDecryptRequest, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseMlKemDecryptRequestOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseMlKemDecryptRequestView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseMlKemDecryptRequestView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseMlKemDecryptRequest { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// SENSITIVE: tagged or untagged COSE_Encrypt bytes. + /// + /// Field 1: `cose_encrypt` + #[must_use] + pub fn cose_encrypt(&self) -> &'_ [u8] { + self.0.reborrow().cose_encrypt + } + /// SENSITIVE: 64-octet FIPS 203 private seed d || z. + /// + /// Field 2: `recipient_private_key` + #[must_use] + pub fn recipient_private_key(&self) -> &'_ [u8] { + self.0.reborrow().recipient_private_key + } + /// Expected SHA-256 canonical public COSE_Key thumbprint. Decryption derives + /// the public key from recipient_private_key and rejects a protected kid + /// bound to any other key before decapsulation. + /// + /// Field 3: `expected_recipient_kid` + #[must_use] + pub fn expected_recipient_kid(&self) -> &'_ [u8] { + self.0.reborrow().expected_recipient_kid + } + /// SENSITIVE: external AAD authenticated but not carried in COSE. + /// + /// Field 4: `external_aad` + #[must_use] + pub fn external_aad(&self) -> &'_ [u8] { + self.0.reborrow().external_aad + } + /// SENSITIVE: mutually known private KDF context agreed out of band. + /// + /// Field 5: `supp_priv_info` + #[must_use] + pub fn supp_priv_info(&self) -> &'_ [u8] { + self.0.reborrow().supp_priv_info + } + /// Distinguishes omitted SuppPrivInfo from an intentionally empty byte string. + /// + /// Field 6: `has_supp_priv_info` + #[must_use] + pub fn has_supp_priv_info(&self) -> bool { + self.0.reborrow().has_supp_priv_info + } +} +impl ::core::convert::From<::buffa::OwnedView>> +for CoseMlKemDecryptRequestOwnedView { + fn from(inner: ::buffa::OwnedView>) -> Self { + CoseMlKemDecryptRequestOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseMlKemDecryptRequestOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef<::buffa::OwnedView>> +for CoseMlKemDecryptRequestOwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseMlKemDecryptRequest { + type View<'a> = CoseMlKemDecryptRequestView<'a>; + type ViewHandle = CoseMlKemDecryptRequestOwnedView; +} +impl ::serde::Serialize for CoseMlKemDecryptRequestOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +#[derive(Clone, Default)] +pub struct CoseMlKemDecryptResultView<'a> { + /// SENSITIVE: authenticated plaintext. + /// + /// Field 1: `plaintext` + pub plaintext: &'a [u8], + /// Field 2: `content_algorithm` + pub content_algorithm: ::buffa::EnumValue< + super::super::CoseContentEncryptionAlgorithm, + >, + /// Field 3: `kem_algorithm` + pub kem_algorithm: ::buffa::EnumValue, + /// Field 4: `mode` + pub mode: ::buffa::EnumValue, + /// Authenticated canonical public COSE_Key thumbprint from the recipient. + /// + /// Field 5: `recipient_kid` + pub recipient_kid: &'a [u8], + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl ::core::fmt::Debug for CoseMlKemDecryptResultView<'_> { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseMlKemDecryptResultView()") + } +} +impl<'a> ::buffa::MessageView<'a> for CoseMlKemDecryptResultView<'a> { + type Owned = super::super::CoseMlKemDecryptResult; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.plaintext = ::buffa::types::borrow_bytes(&mut cur)?; + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.content_algorithm = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(&mut cur)?, + ); + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.kem_algorithm = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(&mut cur)?, + ); + } + 4u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.mode = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(&mut cur)?, + ); + } + 5u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.recipient_kid = ::buffa::types::borrow_bytes(&mut cur)?; + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result< + super::super::CoseMlKemDecryptResult, + ::buffa::DecodeError, + > { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result< + super::super::CoseMlKemDecryptResult, + ::buffa::DecodeError, + > { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseMlKemDecryptResult { + plaintext: (self.plaintext).to_vec(), + content_algorithm: self.content_algorithm, + kem_algorithm: self.kem_algorithm, + mode: self.mode, + recipient_kid: (self.recipient_kid).to_vec(), + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseMlKemDecryptResultView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.plaintext.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.plaintext) as u64; + } + { + let val = self.content_algorithm.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + { + let val = self.kem_algorithm.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + { + let val = self.mode.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + if !self.recipient_kid.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.recipient_kid) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.plaintext.is_empty() { + ::buffa::types::put_shared_bytes_field(1u32, &self.plaintext, buf); + } + { + let val = self.content_algorithm.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(2u32, val, buf); + } + } + { + let val = self.kem_algorithm.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(3u32, val, buf); + } + } + { + let val = self.mode.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(4u32, val, buf); + } + } + if !self.recipient_kid.is_empty() { + ::buffa::types::put_shared_bytes_field(5u32, &self.recipient_kid, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseMlKemDecryptResultView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.plaintext) { + __map + .serialize_entry( + "plaintext", + &::buffa::json_helpers::BytesJson(self.plaintext), + )?; + } + if !::buffa::json_helpers::skip_if::is_default_enum_value( + &self.content_algorithm, + ) { + __map.serialize_entry("contentAlgorithm", &self.content_algorithm)?; + } + if !::buffa::json_helpers::skip_if::is_default_enum_value(&self.kem_algorithm) { + __map.serialize_entry("kemAlgorithm", &self.kem_algorithm)?; + } + if !::buffa::json_helpers::skip_if::is_default_enum_value(&self.mode) { + __map.serialize_entry("mode", &self.mode)?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.recipient_kid) { + __map + .serialize_entry( + "recipientKid", + &::buffa::json_helpers::BytesJson(self.recipient_kid), + )?; + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseMlKemDecryptResultView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseMlKemDecryptResult"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseMlKemDecryptResult"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseMlKemDecryptResult"; +} +::buffa::impl_default_view_instance!(CoseMlKemDecryptResultView); +::buffa::impl_view_reborrow!(CoseMlKemDecryptResultView); +/** Self-contained, `'static` owned view of a `CoseMlKemDecryptResult` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseMlKemDecryptResultView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseMlKemDecryptResultView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone)] +pub struct CoseMlKemDecryptResultOwnedView( + ::buffa::OwnedView>, +); +impl ::core::fmt::Debug for CoseMlKemDecryptResultOwnedView { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseMlKemDecryptResultOwnedView()") + } +} +impl CoseMlKemDecryptResultOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseMlKemDecryptResultOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseMlKemDecryptResultOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseMlKemDecryptResult, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseMlKemDecryptResultOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseMlKemDecryptResultView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseMlKemDecryptResultView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseMlKemDecryptResult { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// SENSITIVE: authenticated plaintext. + /// + /// Field 1: `plaintext` + #[must_use] + pub fn plaintext(&self) -> &'_ [u8] { + self.0.reborrow().plaintext + } + /// Field 2: `content_algorithm` + #[must_use] + pub fn content_algorithm( + &self, + ) -> ::buffa::EnumValue { + self.0.reborrow().content_algorithm + } + /// Field 3: `kem_algorithm` + #[must_use] + pub fn kem_algorithm(&self) -> ::buffa::EnumValue { + self.0.reborrow().kem_algorithm + } + /// Field 4: `mode` + #[must_use] + pub fn mode(&self) -> ::buffa::EnumValue { + self.0.reborrow().mode + } + /// Authenticated canonical public COSE_Key thumbprint from the recipient. + /// + /// Field 5: `recipient_kid` + #[must_use] + pub fn recipient_kid(&self) -> &'_ [u8] { + self.0.reborrow().recipient_kid + } +} +impl ::core::convert::From<::buffa::OwnedView>> +for CoseMlKemDecryptResultOwnedView { + fn from(inner: ::buffa::OwnedView>) -> Self { + CoseMlKemDecryptResultOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseMlKemDecryptResultOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef<::buffa::OwnedView>> +for CoseMlKemDecryptResultOwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseMlKemDecryptResult { + type View<'a> = CoseMlKemDecryptResultView<'a>; + type ViewHandle = CoseMlKemDecryptResultOwnedView; +} +impl ::serde::Serialize for CoseMlKemDecryptResultOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +#[derive(Clone, Debug, Default)] +pub struct CoseSign1OptionsView<'a> { + /// Emit the registered COSE_Sign1 root tag (18). False emits the untagged + /// COSE_Sign1 structure used by most byte-level SDK APIs. + /// + /// Field 1: `tag` + pub tag: bool, + /// Optional maximum encoded COSE_Sign1 size. A value of 0 uses the crate + /// default. + /// + /// Field 2: `max_cose_sign1_bytes` + pub max_cose_sign1_bytes: u64, + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl<'a> ::buffa::MessageView<'a> for CoseSign1OptionsView<'a> { + type Owned = super::super::CoseSign1Options; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.tag = ::buffa::types::decode_bool(&mut cur)?; + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.max_cose_sign1_bytes = ::buffa::types::decode_uint64(&mut cur)?; + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseSign1Options { + tag: self.tag, + max_cose_sign1_bytes: self.max_cose_sign1_bytes, + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseSign1OptionsView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if self.tag { + size += 1u64 + ::buffa::types::BOOL_ENCODED_LEN as u64; + } + if self.max_cose_sign1_bytes != 0u64 { + size + += 1u64 + + ::buffa::types::uint64_encoded_len(self.max_cose_sign1_bytes) + as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.tag { + ::buffa::types::put_bool_field(1u32, self.tag, buf); + } + if self.max_cose_sign1_bytes != 0u64 { + ::buffa::types::put_uint64_field(2u32, self.max_cose_sign1_bytes, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseSign1OptionsView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if self.tag { + __map.serialize_entry("tag", &self.tag)?; + } + if !::buffa::json_helpers::skip_if::is_zero_u64(&self.max_cose_sign1_bytes) { + __map + .serialize_entry( + "maxCoseSign1Bytes", + &::buffa::json_helpers::ProtoJson(&self.max_cose_sign1_bytes), + )?; + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseSign1OptionsView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseSign1Options"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseSign1Options"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseSign1Options"; +} +::buffa::impl_default_view_instance!(CoseSign1OptionsView); +::buffa::impl_view_reborrow!(CoseSign1OptionsView); +/** Self-contained, `'static` owned view of a `CoseSign1Options` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseSign1OptionsView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseSign1OptionsView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone, Debug)] +pub struct CoseSign1OptionsOwnedView(::buffa::OwnedView>); +impl CoseSign1OptionsOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseSign1OptionsOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseSign1OptionsOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseSign1Options, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseSign1OptionsOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseSign1OptionsView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseSign1OptionsView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseSign1Options { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// Emit the registered COSE_Sign1 root tag (18). False emits the untagged + /// COSE_Sign1 structure used by most byte-level SDK APIs. + /// + /// Field 1: `tag` + #[must_use] + pub fn tag(&self) -> bool { + self.0.reborrow().tag + } + /// Optional maximum encoded COSE_Sign1 size. A value of 0 uses the crate + /// default. + /// + /// Field 2: `max_cose_sign1_bytes` + #[must_use] + pub fn max_cose_sign1_bytes(&self) -> u64 { + self.0.reborrow().max_cose_sign1_bytes + } +} +impl ::core::convert::From<::buffa::OwnedView>> +for CoseSign1OptionsOwnedView { + fn from(inner: ::buffa::OwnedView>) -> Self { + CoseSign1OptionsOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseSign1OptionsOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef<::buffa::OwnedView>> +for CoseSign1OptionsOwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseSign1Options { + type View<'a> = CoseSign1OptionsView<'a>; + type ViewHandle = CoseSign1OptionsOwnedView; +} +impl ::serde::Serialize for CoseSign1OptionsOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +/// CoseSign1CreateRequest signs an attached payload. +#[derive(Clone, Default)] +pub struct CoseSign1CreateRequestView<'a> { + /// Algorithm used for the protected header and signing backend. + /// + /// Field 1: `algorithm` + pub algorithm: ::buffa::EnumValue, + /// SENSITIVE: payload bytes to embed in the COSE_Sign1 message. Payloads can + /// carry credential claims or PII and must not be logged. + /// + /// Field 2: `payload` + pub payload: &'a [u8], + /// SENSITIVE: raw private key bytes for the selected algorithm. Callers must + /// keep this in a sensitive-buffer owner outside this transient protobuf + /// container. + /// + /// Field 3: `private_key` + pub private_key: &'a [u8], + /// Protected-header kid bytes. Ignored unless has_kid is true. + /// + /// Field 4: `kid` + pub kid: &'a [u8], + /// Whether kid is present. This distinguishes an omitted kid from an + /// intentionally empty kid byte string. + /// + /// Field 5: `has_kid` + pub has_kid: bool, + /// Optional encoding controls. If omitted, the Rust SDK defaults apply. + /// + /// Field 6: `options` + pub options: ::buffa::MessageFieldView< + super::super::__buffa::view::CoseSign1OptionsView<'a>, + >, + /// SENSITIVE: application-supplied external AAD authenticated by the + /// signature but not embedded in COSE_Sign1. + /// + /// Field 7: `external_aad` + pub external_aad: &'a [u8], + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl ::core::fmt::Debug for CoseSign1CreateRequestView<'_> { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseSign1CreateRequestView()") + } +} +impl<'a> ::buffa::MessageView<'a> for CoseSign1CreateRequestView<'a> { + type Owned = super::super::CoseSign1CreateRequest; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.algorithm = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(&mut cur)?, + ); + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.payload = ::buffa::types::borrow_bytes(&mut cur)?; + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.private_key = ::buffa::types::borrow_bytes(&mut cur)?; + } + 4u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.kid = ::buffa::types::borrow_bytes(&mut cur)?; + } + 5u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.has_kid = ::buffa::types::decode_bool(&mut cur)?; + } + 6u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + match view.options.as_mut() { + Some(existing) => { + ::buffa::MessageView::merge_into_view(existing, sub, __sub_ctx)? + } + None => { + view.options = ::buffa::MessageFieldView::set( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ); + } + } + } + 7u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.external_aad = ::buffa::types::borrow_bytes(&mut cur)?; + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result< + super::super::CoseSign1CreateRequest, + ::buffa::DecodeError, + > { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result< + super::super::CoseSign1CreateRequest, + ::buffa::DecodeError, + > { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseSign1CreateRequest { + algorithm: self.algorithm, + payload: (self.payload).to_vec(), + private_key: (self.private_key).to_vec(), + kid: (self.kid).to_vec(), + has_kid: self.has_kid, + options: match self.options.as_option() { + Some(v) => { + ::buffa::MessageField::< + super::super::CoseSign1Options, + ::buffa::Inline, + >::some(v.to_owned_from_source(__buffa_src)?) + } + None => ::buffa::MessageField::none(), + }, + external_aad: (self.external_aad).to_vec(), + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseSign1CreateRequestView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, __cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + { + let val = self.algorithm.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + if !self.payload.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.payload) as u64; + } + if !self.private_key.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.private_key) as u64; + } + if !self.kid.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.kid) as u64; + } + if self.has_kid { + size += 1u64 + ::buffa::types::BOOL_ENCODED_LEN as u64; + } + if self.options.is_set() { + let __slot = __cache.reserve(); + let inner_size = self.options.compute_size(__cache); + __cache.set(__slot, inner_size); + size + += 1u64 + ::buffa::encoding::varint_len(inner_size as u64) as u64 + + inner_size as u64; + } + if !self.external_aad.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.external_aad) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + __cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + { + let val = self.algorithm.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(1u32, val, buf); + } + } + if !self.payload.is_empty() { + ::buffa::types::put_shared_bytes_field(2u32, &self.payload, buf); + } + if !self.private_key.is_empty() { + ::buffa::types::put_shared_bytes_field(3u32, &self.private_key, buf); + } + if !self.kid.is_empty() { + ::buffa::types::put_shared_bytes_field(4u32, &self.kid, buf); + } + if self.has_kid { + ::buffa::types::put_bool_field(5u32, self.has_kid, buf); + } + if self.options.is_set() { + ::buffa::types::put_len_delimited_header( + 6u32, + u64::from(__cache.consume_next()), + buf, + ); + self.options.write_to(__cache, buf); + } + if !self.external_aad.is_empty() { + ::buffa::types::put_shared_bytes_field(7u32, &self.external_aad, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseSign1CreateRequestView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if !::buffa::json_helpers::skip_if::is_default_enum_value(&self.algorithm) { + __map.serialize_entry("algorithm", &self.algorithm)?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.payload) { + __map + .serialize_entry( + "payload", + &::buffa::json_helpers::BytesJson(self.payload), + )?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.private_key) { + __map + .serialize_entry( + "privateKey", + &::buffa::json_helpers::BytesJson(self.private_key), + )?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.kid) { + __map.serialize_entry("kid", &::buffa::json_helpers::BytesJson(self.kid))?; + } + if self.has_kid { + __map.serialize_entry("hasKid", &self.has_kid)?; + } + { + if let ::core::option::Option::Some(__v) = self.options.as_option() { + __map.serialize_entry("options", __v)?; + } + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.external_aad) { + __map + .serialize_entry( + "externalAad", + &::buffa::json_helpers::BytesJson(self.external_aad), + )?; + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseSign1CreateRequestView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseSign1CreateRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseSign1CreateRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseSign1CreateRequest"; +} +::buffa::impl_default_view_instance!(CoseSign1CreateRequestView); +::buffa::impl_view_reborrow!(CoseSign1CreateRequestView); +/** Self-contained, `'static` owned view of a `CoseSign1CreateRequest` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseSign1CreateRequestView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseSign1CreateRequestView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone)] +pub struct CoseSign1CreateRequestOwnedView( + ::buffa::OwnedView>, +); +impl ::core::fmt::Debug for CoseSign1CreateRequestOwnedView { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseSign1CreateRequestOwnedView()") + } +} +impl CoseSign1CreateRequestOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseSign1CreateRequestOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseSign1CreateRequestOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseSign1CreateRequest, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseSign1CreateRequestOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseSign1CreateRequestView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseSign1CreateRequestView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseSign1CreateRequest { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// Algorithm used for the protected header and signing backend. + /// + /// Field 1: `algorithm` + #[must_use] + pub fn algorithm(&self) -> ::buffa::EnumValue { + self.0.reborrow().algorithm + } + /// SENSITIVE: payload bytes to embed in the COSE_Sign1 message. Payloads can + /// carry credential claims or PII and must not be logged. + /// + /// Field 2: `payload` + #[must_use] + pub fn payload(&self) -> &'_ [u8] { + self.0.reborrow().payload + } + /// SENSITIVE: raw private key bytes for the selected algorithm. Callers must + /// keep this in a sensitive-buffer owner outside this transient protobuf + /// container. + /// + /// Field 3: `private_key` + #[must_use] + pub fn private_key(&self) -> &'_ [u8] { + self.0.reborrow().private_key + } + /// Protected-header kid bytes. Ignored unless has_kid is true. + /// + /// Field 4: `kid` + #[must_use] + pub fn kid(&self) -> &'_ [u8] { + self.0.reborrow().kid + } + /// Whether kid is present. This distinguishes an omitted kid from an + /// intentionally empty kid byte string. + /// + /// Field 5: `has_kid` + #[must_use] + pub fn has_kid(&self) -> bool { + self.0.reborrow().has_kid + } + /// Optional encoding controls. If omitted, the Rust SDK defaults apply. + /// + /// Field 6: `options` + #[must_use] + pub fn options( + &self, + ) -> &::buffa::MessageFieldView< + super::super::__buffa::view::CoseSign1OptionsView<'_>, + > { + &self.0.reborrow().options + } + /// SENSITIVE: application-supplied external AAD authenticated by the + /// signature but not embedded in COSE_Sign1. + /// + /// Field 7: `external_aad` + #[must_use] + pub fn external_aad(&self) -> &'_ [u8] { + self.0.reborrow().external_aad + } +} +impl ::core::convert::From<::buffa::OwnedView>> +for CoseSign1CreateRequestOwnedView { + fn from(inner: ::buffa::OwnedView>) -> Self { + CoseSign1CreateRequestOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseSign1CreateRequestOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef<::buffa::OwnedView>> +for CoseSign1CreateRequestOwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseSign1CreateRequest { + type View<'a> = CoseSign1CreateRequestView<'a>; + type ViewHandle = CoseSign1CreateRequestOwnedView; +} +impl ::serde::Serialize for CoseSign1CreateRequestOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +/// CoseSign1CreateDetachedRequest signs a detached payload. The payload is +/// authenticated but not embedded in the resulting COSE_Sign1 message. +#[derive(Clone, Default)] +pub struct CoseSign1CreateDetachedRequestView<'a> { + /// Algorithm used for the protected header and signing backend. + /// + /// Field 1: `algorithm` + pub algorithm: ::buffa::EnumValue, + /// SENSITIVE: detached payload bytes authenticated by the signature. Payloads + /// can carry credential claims or PII and must not be logged. + /// + /// Field 2: `payload` + pub payload: &'a [u8], + /// SENSITIVE: raw private key bytes for the selected algorithm. Callers must + /// keep this in a sensitive-buffer owner outside this transient protobuf + /// container. + /// + /// Field 3: `private_key` + pub private_key: &'a [u8], + /// Protected-header kid bytes. Ignored unless has_kid is true. + /// + /// Field 4: `kid` + pub kid: &'a [u8], + /// Whether kid is present. This distinguishes an omitted kid from an + /// intentionally empty kid byte string. + /// + /// Field 5: `has_kid` + pub has_kid: bool, + /// Optional encoding controls. If omitted, the Rust SDK defaults apply. + /// + /// Field 6: `options` + pub options: ::buffa::MessageFieldView< + super::super::__buffa::view::CoseSign1OptionsView<'a>, + >, + /// SENSITIVE: application-supplied external AAD authenticated by the + /// signature but not embedded in COSE_Sign1. + /// + /// Field 7: `external_aad` + pub external_aad: &'a [u8], + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl ::core::fmt::Debug for CoseSign1CreateDetachedRequestView<'_> { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseSign1CreateDetachedRequestView()") + } +} +impl<'a> ::buffa::MessageView<'a> for CoseSign1CreateDetachedRequestView<'a> { + type Owned = super::super::CoseSign1CreateDetachedRequest; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.algorithm = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(&mut cur)?, + ); + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.payload = ::buffa::types::borrow_bytes(&mut cur)?; + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.private_key = ::buffa::types::borrow_bytes(&mut cur)?; + } + 4u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.kid = ::buffa::types::borrow_bytes(&mut cur)?; + } + 5u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.has_kid = ::buffa::types::decode_bool(&mut cur)?; + } + 6u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + match view.options.as_mut() { + Some(existing) => { + ::buffa::MessageView::merge_into_view(existing, sub, __sub_ctx)? + } + None => { + view.options = ::buffa::MessageFieldView::set( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ); + } + } + } + 7u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.external_aad = ::buffa::types::borrow_bytes(&mut cur)?; + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result< + super::super::CoseSign1CreateDetachedRequest, + ::buffa::DecodeError, + > { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result< + super::super::CoseSign1CreateDetachedRequest, + ::buffa::DecodeError, + > { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseSign1CreateDetachedRequest { + algorithm: self.algorithm, + payload: (self.payload).to_vec(), + private_key: (self.private_key).to_vec(), + kid: (self.kid).to_vec(), + has_kid: self.has_kid, + options: match self.options.as_option() { + Some(v) => { + ::buffa::MessageField::< + super::super::CoseSign1Options, + ::buffa::Inline, + >::some(v.to_owned_from_source(__buffa_src)?) + } + None => ::buffa::MessageField::none(), + }, + external_aad: (self.external_aad).to_vec(), + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseSign1CreateDetachedRequestView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, __cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + { + let val = self.algorithm.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + if !self.payload.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.payload) as u64; + } + if !self.private_key.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.private_key) as u64; + } + if !self.kid.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.kid) as u64; + } + if self.has_kid { + size += 1u64 + ::buffa::types::BOOL_ENCODED_LEN as u64; + } + if self.options.is_set() { + let __slot = __cache.reserve(); + let inner_size = self.options.compute_size(__cache); + __cache.set(__slot, inner_size); + size + += 1u64 + ::buffa::encoding::varint_len(inner_size as u64) as u64 + + inner_size as u64; + } + if !self.external_aad.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.external_aad) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + __cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + { + let val = self.algorithm.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(1u32, val, buf); + } + } + if !self.payload.is_empty() { + ::buffa::types::put_shared_bytes_field(2u32, &self.payload, buf); + } + if !self.private_key.is_empty() { + ::buffa::types::put_shared_bytes_field(3u32, &self.private_key, buf); + } + if !self.kid.is_empty() { + ::buffa::types::put_shared_bytes_field(4u32, &self.kid, buf); + } + if self.has_kid { + ::buffa::types::put_bool_field(5u32, self.has_kid, buf); + } + if self.options.is_set() { + ::buffa::types::put_len_delimited_header( + 6u32, + u64::from(__cache.consume_next()), + buf, + ); + self.options.write_to(__cache, buf); + } + if !self.external_aad.is_empty() { + ::buffa::types::put_shared_bytes_field(7u32, &self.external_aad, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseSign1CreateDetachedRequestView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if !::buffa::json_helpers::skip_if::is_default_enum_value(&self.algorithm) { + __map.serialize_entry("algorithm", &self.algorithm)?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.payload) { + __map + .serialize_entry( + "payload", + &::buffa::json_helpers::BytesJson(self.payload), + )?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.private_key) { + __map + .serialize_entry( + "privateKey", + &::buffa::json_helpers::BytesJson(self.private_key), + )?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.kid) { + __map.serialize_entry("kid", &::buffa::json_helpers::BytesJson(self.kid))?; + } + if self.has_kid { + __map.serialize_entry("hasKid", &self.has_kid)?; + } + { + if let ::core::option::Option::Some(__v) = self.options.as_option() { + __map.serialize_entry("options", __v)?; + } + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.external_aad) { + __map + .serialize_entry( + "externalAad", + &::buffa::json_helpers::BytesJson(self.external_aad), + )?; + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseSign1CreateDetachedRequestView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseSign1CreateDetachedRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseSign1CreateDetachedRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseSign1CreateDetachedRequest"; +} +::buffa::impl_default_view_instance!(CoseSign1CreateDetachedRequestView); +::buffa::impl_view_reborrow!(CoseSign1CreateDetachedRequestView); +/** Self-contained, `'static` owned view of a `CoseSign1CreateDetachedRequest` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseSign1CreateDetachedRequestView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseSign1CreateDetachedRequestView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone)] +pub struct CoseSign1CreateDetachedRequestOwnedView( + ::buffa::OwnedView>, +); +impl ::core::fmt::Debug for CoseSign1CreateDetachedRequestOwnedView { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseSign1CreateDetachedRequestOwnedView()") + } +} +impl CoseSign1CreateDetachedRequestOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseSign1CreateDetachedRequestOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseSign1CreateDetachedRequestOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseSign1CreateDetachedRequest, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseSign1CreateDetachedRequestOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseSign1CreateDetachedRequestView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseSign1CreateDetachedRequestView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseSign1CreateDetachedRequest { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// Algorithm used for the protected header and signing backend. + /// + /// Field 1: `algorithm` + #[must_use] + pub fn algorithm(&self) -> ::buffa::EnumValue { + self.0.reborrow().algorithm + } + /// SENSITIVE: detached payload bytes authenticated by the signature. Payloads + /// can carry credential claims or PII and must not be logged. + /// + /// Field 2: `payload` + #[must_use] + pub fn payload(&self) -> &'_ [u8] { + self.0.reborrow().payload + } + /// SENSITIVE: raw private key bytes for the selected algorithm. Callers must + /// keep this in a sensitive-buffer owner outside this transient protobuf + /// container. + /// + /// Field 3: `private_key` + #[must_use] + pub fn private_key(&self) -> &'_ [u8] { + self.0.reborrow().private_key + } + /// Protected-header kid bytes. Ignored unless has_kid is true. + /// + /// Field 4: `kid` + #[must_use] + pub fn kid(&self) -> &'_ [u8] { + self.0.reborrow().kid + } + /// Whether kid is present. This distinguishes an omitted kid from an + /// intentionally empty kid byte string. + /// + /// Field 5: `has_kid` + #[must_use] + pub fn has_kid(&self) -> bool { + self.0.reborrow().has_kid + } + /// Optional encoding controls. If omitted, the Rust SDK defaults apply. + /// + /// Field 6: `options` + #[must_use] + pub fn options( + &self, + ) -> &::buffa::MessageFieldView< + super::super::__buffa::view::CoseSign1OptionsView<'_>, + > { + &self.0.reborrow().options + } + /// SENSITIVE: application-supplied external AAD authenticated by the + /// signature but not embedded in COSE_Sign1. + /// + /// Field 7: `external_aad` + #[must_use] + pub fn external_aad(&self) -> &'_ [u8] { + self.0.reborrow().external_aad + } +} +impl ::core::convert::From< + ::buffa::OwnedView>, +> for CoseSign1CreateDetachedRequestOwnedView { + fn from( + inner: ::buffa::OwnedView>, + ) -> Self { + CoseSign1CreateDetachedRequestOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseSign1CreateDetachedRequestOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef< + ::buffa::OwnedView>, +> for CoseSign1CreateDetachedRequestOwnedView { + fn as_ref( + &self, + ) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseSign1CreateDetachedRequest { + type View<'a> = CoseSign1CreateDetachedRequestView<'a>; + type ViewHandle = CoseSign1CreateDetachedRequestOwnedView; +} +impl ::serde::Serialize for CoseSign1CreateDetachedRequestOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +/// CoseSign1CreateResult contains the encoded COSE_Sign1 bytes produced by a +/// signing operation. +#[derive(Clone, Default)] +pub struct CoseSign1CreateResultView<'a> { + /// SENSITIVE: encoded COSE_Sign1 bytes, tagged or untagged according to + /// request options. Attached COSE_Sign1 messages contain payload bytes. + /// + /// Field 1: `cose_sign1` + pub cose_sign1: &'a [u8], + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl ::core::fmt::Debug for CoseSign1CreateResultView<'_> { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseSign1CreateResultView()") + } +} +impl<'a> ::buffa::MessageView<'a> for CoseSign1CreateResultView<'a> { + type Owned = super::super::CoseSign1CreateResult; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.cose_sign1 = ::buffa::types::borrow_bytes(&mut cur)?; + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result< + super::super::CoseSign1CreateResult, + ::buffa::DecodeError, + > { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result< + super::super::CoseSign1CreateResult, + ::buffa::DecodeError, + > { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseSign1CreateResult { + cose_sign1: (self.cose_sign1).to_vec(), + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseSign1CreateResultView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.cose_sign1.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.cose_sign1) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.cose_sign1.is_empty() { + ::buffa::types::put_shared_bytes_field(1u32, &self.cose_sign1, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseSign1CreateResultView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.cose_sign1) { + __map + .serialize_entry( + "coseSign1", + &::buffa::json_helpers::BytesJson(self.cose_sign1), + )?; + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseSign1CreateResultView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseSign1CreateResult"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseSign1CreateResult"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseSign1CreateResult"; +} +::buffa::impl_default_view_instance!(CoseSign1CreateResultView); +::buffa::impl_view_reborrow!(CoseSign1CreateResultView); +/** Self-contained, `'static` owned view of a `CoseSign1CreateResult` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseSign1CreateResultView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseSign1CreateResultView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone)] +pub struct CoseSign1CreateResultOwnedView( + ::buffa::OwnedView>, +); +impl ::core::fmt::Debug for CoseSign1CreateResultOwnedView { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseSign1CreateResultOwnedView()") + } +} +impl CoseSign1CreateResultOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseSign1CreateResultOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseSign1CreateResultOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseSign1CreateResult, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseSign1CreateResultOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseSign1CreateResultView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseSign1CreateResultView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseSign1CreateResult { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// SENSITIVE: encoded COSE_Sign1 bytes, tagged or untagged according to + /// request options. Attached COSE_Sign1 messages contain payload bytes. + /// + /// Field 1: `cose_sign1` + #[must_use] + pub fn cose_sign1(&self) -> &'_ [u8] { + self.0.reborrow().cose_sign1 + } +} +impl ::core::convert::From<::buffa::OwnedView>> +for CoseSign1CreateResultOwnedView { + fn from(inner: ::buffa::OwnedView>) -> Self { + CoseSign1CreateResultOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseSign1CreateResultOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef<::buffa::OwnedView>> +for CoseSign1CreateResultOwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseSign1CreateResult { + type View<'a> = CoseSign1CreateResultView<'a>; + type ViewHandle = CoseSign1CreateResultOwnedView; +} +impl ::serde::Serialize for CoseSign1CreateResultOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +/// CoseSign1VerifyRequest verifies a COSE_Sign1 with an attached payload. The +/// public_key field is the protobuf-boundary equivalent of the Rust SDK key +/// resolver output for the protected-header kid. +#[derive(Clone, Default)] +pub struct CoseSign1VerifyRequestView<'a> { + /// SENSITIVE: encoded COSE_Sign1 bytes to verify. Tagged and untagged inputs + /// are accepted; attached messages contain payload bytes. + /// + /// Field 1: `cose_sign1` + pub cose_sign1: &'a [u8], + /// Raw public key bytes selected by the caller for the protected-header kid. + /// + /// Field 2: `public_key` + pub public_key: &'a [u8], + /// Optional maximum accepted encoded COSE_Sign1 size. A value of 0 uses the + /// crate default. + /// + /// Field 3: `max_cose_sign1_bytes` + pub max_cose_sign1_bytes: u64, + /// Reserved for shape parity with detached verification. A value of 0 uses the + /// crate default; attached verification does not consume a detached payload. + /// + /// Field 4: `max_detached_payload_bytes` + pub max_detached_payload_bytes: u64, + /// Require a non-empty protected-header kid before verification. + /// + /// Field 5: `require_kid` + pub require_kid: bool, + /// Optional algorithm allow-list. Empty means any COSE algorithm supported by + /// this crate and operation. + /// + /// Field 6: `allowed_algorithms` + pub allowed_algorithms: ::buffa::RepeatedView< + 'a, + ::buffa::EnumValue, + >, + /// SENSITIVE: external AAD supplied by the application for verification. + /// + /// Field 7: `external_aad` + pub external_aad: &'a [u8], + /// Optional trusted key-selection identifier. When non-empty, the + /// protected-header kid must match this value before public_key can be + /// returned by the adapter's resolver. Empty preserves resolver-style use + /// where public_key was already selected outside this request. + /// Selection of this value belongs to the caller's trust-store boundary. + /// + /// Field 8: `expected_kid` + pub expected_kid: &'a [u8], + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl ::core::fmt::Debug for CoseSign1VerifyRequestView<'_> { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseSign1VerifyRequestView()") + } +} +impl<'a> ::buffa::MessageView<'a> for CoseSign1VerifyRequestView<'a> { + type Owned = super::super::CoseSign1VerifyRequest; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.cose_sign1 = ::buffa::types::borrow_bytes(&mut cur)?; + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.public_key = ::buffa::types::borrow_bytes(&mut cur)?; + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.max_cose_sign1_bytes = ::buffa::types::decode_uint64(&mut cur)?; + } + 4u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.max_detached_payload_bytes = ::buffa::types::decode_uint64( + &mut cur, + )?; + } + 5u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.require_kid = ::buffa::types::decode_bool(&mut cur)?; + } + 7u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.external_aad = ::buffa::types::borrow_bytes(&mut cur)?; + } + 8u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.expected_kid = ::buffa::types::borrow_bytes(&mut cur)?; + } + 6u32 => { + if tag.wire_type() == ::buffa::encoding::WireType::LengthDelimited { + let payload = ::buffa::types::borrow_bytes(&mut cur)?; + view.allowed_algorithms + .reserve(::buffa::encoding::count_varints(payload)); + let mut pcur: &[u8] = payload; + while !pcur.is_empty() { + view.allowed_algorithms + .push( + ::buffa::EnumValue::from( + ::buffa::types::decode_int32_packed(&mut pcur)?, + ), + ); + } + } else if tag.wire_type() == ::buffa::encoding::WireType::Varint { + view.allowed_algorithms + .push( + ::buffa::EnumValue::from( + ::buffa::types::decode_int32(&mut cur)?, + ), + ); + } else { + return Err( + ::buffa::encoding::wire_type_mismatch( + tag, + ::buffa::encoding::WireType::LengthDelimited, + ), + ); + } + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result< + super::super::CoseSign1VerifyRequest, + ::buffa::DecodeError, + > { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result< + super::super::CoseSign1VerifyRequest, + ::buffa::DecodeError, + > { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseSign1VerifyRequest { + cose_sign1: (self.cose_sign1).to_vec(), + public_key: (self.public_key).to_vec(), + max_cose_sign1_bytes: self.max_cose_sign1_bytes, + max_detached_payload_bytes: self.max_detached_payload_bytes, + require_kid: self.require_kid, + allowed_algorithms: self.allowed_algorithms.to_vec(), + external_aad: (self.external_aad).to_vec(), + expected_kid: (self.expected_kid).to_vec(), + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseSign1VerifyRequestView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.cose_sign1.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.cose_sign1) as u64; + } + if !self.public_key.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.public_key) as u64; + } + if self.max_cose_sign1_bytes != 0u64 { + size + += 1u64 + + ::buffa::types::uint64_encoded_len(self.max_cose_sign1_bytes) + as u64; + } + if self.max_detached_payload_bytes != 0u64 { + size + += 1u64 + + ::buffa::types::uint64_encoded_len(self.max_detached_payload_bytes) + as u64; + } + if self.require_kid { + size += 1u64 + ::buffa::types::BOOL_ENCODED_LEN as u64; + } + if !self.allowed_algorithms.is_empty() { + let payload: u64 = self + .allowed_algorithms + .iter() + .map(|v| ::buffa::types::int32_encoded_len(v.to_i32()) as u64) + .sum::(); + size += 1u64 + ::buffa::encoding::varint_len(payload) as u64 + payload; + } + if !self.external_aad.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.external_aad) as u64; + } + if !self.expected_kid.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.expected_kid) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.cose_sign1.is_empty() { + ::buffa::types::put_shared_bytes_field(1u32, &self.cose_sign1, buf); + } + if !self.public_key.is_empty() { + ::buffa::types::put_shared_bytes_field(2u32, &self.public_key, buf); + } + if self.max_cose_sign1_bytes != 0u64 { + ::buffa::types::put_uint64_field(3u32, self.max_cose_sign1_bytes, buf); + } + if self.max_detached_payload_bytes != 0u64 { + ::buffa::types::put_uint64_field(4u32, self.max_detached_payload_bytes, buf); + } + if self.require_kid { + ::buffa::types::put_bool_field(5u32, self.require_kid, buf); + } + if !self.allowed_algorithms.is_empty() { + let payload: u64 = self + .allowed_algorithms + .iter() + .map(|v| ::buffa::types::int32_encoded_len(v.to_i32()) as u64) + .sum::(); + ::buffa::types::put_len_delimited_header(6u32, payload, buf); + for v in &self.allowed_algorithms { + ::buffa::types::encode_int32(v.to_i32(), buf); + } + } + if !self.external_aad.is_empty() { + ::buffa::types::put_shared_bytes_field(7u32, &self.external_aad, buf); + } + if !self.expected_kid.is_empty() { + ::buffa::types::put_shared_bytes_field(8u32, &self.expected_kid, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseSign1VerifyRequestView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.cose_sign1) { + __map + .serialize_entry( + "coseSign1", + &::buffa::json_helpers::BytesJson(self.cose_sign1), + )?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.public_key) { + __map + .serialize_entry( + "publicKey", + &::buffa::json_helpers::BytesJson(self.public_key), + )?; + } + if !::buffa::json_helpers::skip_if::is_zero_u64(&self.max_cose_sign1_bytes) { + __map + .serialize_entry( + "maxCoseSign1Bytes", + &::buffa::json_helpers::ProtoJson(&self.max_cose_sign1_bytes), + )?; + } + if !::buffa::json_helpers::skip_if::is_zero_u64( + &self.max_detached_payload_bytes, + ) { + __map + .serialize_entry( + "maxDetachedPayloadBytes", + &::buffa::json_helpers::ProtoJson(&self.max_detached_payload_bytes), + )?; + } + if self.require_kid { + __map.serialize_entry("requireKid", &self.require_kid)?; + } + if !self.allowed_algorithms.is_empty() { + __map + .serialize_entry( + "allowedAlgorithms", + &::buffa::json_helpers::EnumSeqJson(&self.allowed_algorithms), + )?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.external_aad) { + __map + .serialize_entry( + "externalAad", + &::buffa::json_helpers::BytesJson(self.external_aad), + )?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.expected_kid) { + __map + .serialize_entry( + "expectedKid", + &::buffa::json_helpers::BytesJson(self.expected_kid), + )?; + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseSign1VerifyRequestView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseSign1VerifyRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseSign1VerifyRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseSign1VerifyRequest"; +} +::buffa::impl_default_view_instance!(CoseSign1VerifyRequestView); +::buffa::impl_view_reborrow!(CoseSign1VerifyRequestView); +/** Self-contained, `'static` owned view of a `CoseSign1VerifyRequest` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseSign1VerifyRequestView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseSign1VerifyRequestView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone)] +pub struct CoseSign1VerifyRequestOwnedView( + ::buffa::OwnedView>, +); +impl ::core::fmt::Debug for CoseSign1VerifyRequestOwnedView { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseSign1VerifyRequestOwnedView()") + } +} +impl CoseSign1VerifyRequestOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseSign1VerifyRequestOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseSign1VerifyRequestOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseSign1VerifyRequest, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseSign1VerifyRequestOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseSign1VerifyRequestView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseSign1VerifyRequestView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseSign1VerifyRequest { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// SENSITIVE: encoded COSE_Sign1 bytes to verify. Tagged and untagged inputs + /// are accepted; attached messages contain payload bytes. + /// + /// Field 1: `cose_sign1` + #[must_use] + pub fn cose_sign1(&self) -> &'_ [u8] { + self.0.reborrow().cose_sign1 + } + /// Raw public key bytes selected by the caller for the protected-header kid. + /// + /// Field 2: `public_key` + #[must_use] + pub fn public_key(&self) -> &'_ [u8] { + self.0.reborrow().public_key + } + /// Optional maximum accepted encoded COSE_Sign1 size. A value of 0 uses the + /// crate default. + /// + /// Field 3: `max_cose_sign1_bytes` + #[must_use] + pub fn max_cose_sign1_bytes(&self) -> u64 { + self.0.reborrow().max_cose_sign1_bytes + } + /// Reserved for shape parity with detached verification. A value of 0 uses the + /// crate default; attached verification does not consume a detached payload. + /// + /// Field 4: `max_detached_payload_bytes` + #[must_use] + pub fn max_detached_payload_bytes(&self) -> u64 { + self.0.reborrow().max_detached_payload_bytes + } + /// Require a non-empty protected-header kid before verification. + /// + /// Field 5: `require_kid` + #[must_use] + pub fn require_kid(&self) -> bool { + self.0.reborrow().require_kid + } + /// Optional algorithm allow-list. Empty means any COSE algorithm supported by + /// this crate and operation. + /// + /// Field 6: `allowed_algorithms` + #[must_use] + pub fn allowed_algorithms( + &self, + ) -> &::buffa::RepeatedView< + '_, + ::buffa::EnumValue, + > { + &self.0.reborrow().allowed_algorithms + } + /// SENSITIVE: external AAD supplied by the application for verification. + /// + /// Field 7: `external_aad` + #[must_use] + pub fn external_aad(&self) -> &'_ [u8] { + self.0.reborrow().external_aad + } + /// Optional trusted key-selection identifier. When non-empty, the + /// protected-header kid must match this value before public_key can be + /// returned by the adapter's resolver. Empty preserves resolver-style use + /// where public_key was already selected outside this request. + /// Selection of this value belongs to the caller's trust-store boundary. + /// + /// Field 8: `expected_kid` + #[must_use] + pub fn expected_kid(&self) -> &'_ [u8] { + self.0.reborrow().expected_kid + } +} +impl ::core::convert::From<::buffa::OwnedView>> +for CoseSign1VerifyRequestOwnedView { + fn from(inner: ::buffa::OwnedView>) -> Self { + CoseSign1VerifyRequestOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseSign1VerifyRequestOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef<::buffa::OwnedView>> +for CoseSign1VerifyRequestOwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseSign1VerifyRequest { + type View<'a> = CoseSign1VerifyRequestView<'a>; + type ViewHandle = CoseSign1VerifyRequestOwnedView; +} +impl ::serde::Serialize for CoseSign1VerifyRequestOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +/// CoseSign1VerifyDetachedRequest verifies a COSE_Sign1 whose payload is carried +/// out of band. +#[derive(Clone, Default)] +pub struct CoseSign1VerifyDetachedRequestView<'a> { + /// SENSITIVE: encoded COSE_Sign1 bytes to verify. Tagged and untagged inputs + /// are accepted. + /// + /// Field 1: `cose_sign1` + pub cose_sign1: &'a [u8], + /// SENSITIVE: detached payload bytes that must match the signature. Payloads + /// can carry credential claims or PII and must not be logged. + /// + /// Field 2: `payload` + pub payload: &'a [u8], + /// Raw public key bytes selected by the caller for the protected-header kid. + /// + /// Field 3: `public_key` + pub public_key: &'a [u8], + /// Optional maximum accepted encoded COSE_Sign1 size. A value of 0 uses the + /// crate default. + /// + /// Field 4: `max_cose_sign1_bytes` + pub max_cose_sign1_bytes: u64, + /// Optional maximum accepted detached payload size. A value of 0 uses the + /// crate default. + /// + /// Field 5: `max_detached_payload_bytes` + pub max_detached_payload_bytes: u64, + /// Require a non-empty protected-header kid before verification. + /// + /// Field 6: `require_kid` + pub require_kid: bool, + /// Optional algorithm allow-list. Empty means any COSE algorithm supported by + /// this crate and operation. + /// + /// Field 7: `allowed_algorithms` + pub allowed_algorithms: ::buffa::RepeatedView< + 'a, + ::buffa::EnumValue, + >, + /// SENSITIVE: external AAD supplied by the application for verification. + /// + /// Field 8: `external_aad` + pub external_aad: &'a [u8], + /// Optional trusted key-selection identifier. When non-empty, the + /// protected-header kid must match this value before public_key can be + /// returned by the adapter's resolver. Empty preserves resolver-style use + /// where public_key was already selected outside this request. + /// Selection of this value belongs to the caller's trust-store boundary. + /// + /// Field 9: `expected_kid` + pub expected_kid: &'a [u8], + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl ::core::fmt::Debug for CoseSign1VerifyDetachedRequestView<'_> { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseSign1VerifyDetachedRequestView()") + } +} +impl<'a> ::buffa::MessageView<'a> for CoseSign1VerifyDetachedRequestView<'a> { + type Owned = super::super::CoseSign1VerifyDetachedRequest; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.cose_sign1 = ::buffa::types::borrow_bytes(&mut cur)?; + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.payload = ::buffa::types::borrow_bytes(&mut cur)?; + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.public_key = ::buffa::types::borrow_bytes(&mut cur)?; + } + 4u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.max_cose_sign1_bytes = ::buffa::types::decode_uint64(&mut cur)?; + } + 5u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.max_detached_payload_bytes = ::buffa::types::decode_uint64( + &mut cur, + )?; + } + 6u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.require_kid = ::buffa::types::decode_bool(&mut cur)?; + } + 8u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.external_aad = ::buffa::types::borrow_bytes(&mut cur)?; + } + 9u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.expected_kid = ::buffa::types::borrow_bytes(&mut cur)?; + } + 7u32 => { + if tag.wire_type() == ::buffa::encoding::WireType::LengthDelimited { + let payload = ::buffa::types::borrow_bytes(&mut cur)?; + view.allowed_algorithms + .reserve(::buffa::encoding::count_varints(payload)); + let mut pcur: &[u8] = payload; + while !pcur.is_empty() { + view.allowed_algorithms + .push( + ::buffa::EnumValue::from( + ::buffa::types::decode_int32_packed(&mut pcur)?, + ), + ); + } + } else if tag.wire_type() == ::buffa::encoding::WireType::Varint { + view.allowed_algorithms + .push( + ::buffa::EnumValue::from( + ::buffa::types::decode_int32(&mut cur)?, + ), + ); + } else { + return Err( + ::buffa::encoding::wire_type_mismatch( + tag, + ::buffa::encoding::WireType::LengthDelimited, + ), + ); + } + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result< + super::super::CoseSign1VerifyDetachedRequest, + ::buffa::DecodeError, + > { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result< + super::super::CoseSign1VerifyDetachedRequest, + ::buffa::DecodeError, + > { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseSign1VerifyDetachedRequest { + cose_sign1: (self.cose_sign1).to_vec(), + payload: (self.payload).to_vec(), + public_key: (self.public_key).to_vec(), + max_cose_sign1_bytes: self.max_cose_sign1_bytes, + max_detached_payload_bytes: self.max_detached_payload_bytes, + require_kid: self.require_kid, + allowed_algorithms: self.allowed_algorithms.to_vec(), + external_aad: (self.external_aad).to_vec(), + expected_kid: (self.expected_kid).to_vec(), + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseSign1VerifyDetachedRequestView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.cose_sign1.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.cose_sign1) as u64; + } + if !self.payload.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.payload) as u64; + } + if !self.public_key.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.public_key) as u64; + } + if self.max_cose_sign1_bytes != 0u64 { + size + += 1u64 + + ::buffa::types::uint64_encoded_len(self.max_cose_sign1_bytes) + as u64; + } + if self.max_detached_payload_bytes != 0u64 { + size + += 1u64 + + ::buffa::types::uint64_encoded_len(self.max_detached_payload_bytes) + as u64; + } + if self.require_kid { + size += 1u64 + ::buffa::types::BOOL_ENCODED_LEN as u64; + } + if !self.allowed_algorithms.is_empty() { + let payload: u64 = self + .allowed_algorithms + .iter() + .map(|v| ::buffa::types::int32_encoded_len(v.to_i32()) as u64) + .sum::(); + size += 1u64 + ::buffa::encoding::varint_len(payload) as u64 + payload; + } + if !self.external_aad.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.external_aad) as u64; + } + if !self.expected_kid.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.expected_kid) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.cose_sign1.is_empty() { + ::buffa::types::put_shared_bytes_field(1u32, &self.cose_sign1, buf); + } + if !self.payload.is_empty() { + ::buffa::types::put_shared_bytes_field(2u32, &self.payload, buf); + } + if !self.public_key.is_empty() { + ::buffa::types::put_shared_bytes_field(3u32, &self.public_key, buf); + } + if self.max_cose_sign1_bytes != 0u64 { + ::buffa::types::put_uint64_field(4u32, self.max_cose_sign1_bytes, buf); + } + if self.max_detached_payload_bytes != 0u64 { + ::buffa::types::put_uint64_field(5u32, self.max_detached_payload_bytes, buf); + } + if self.require_kid { + ::buffa::types::put_bool_field(6u32, self.require_kid, buf); + } + if !self.allowed_algorithms.is_empty() { + let payload: u64 = self + .allowed_algorithms + .iter() + .map(|v| ::buffa::types::int32_encoded_len(v.to_i32()) as u64) + .sum::(); + ::buffa::types::put_len_delimited_header(7u32, payload, buf); + for v in &self.allowed_algorithms { + ::buffa::types::encode_int32(v.to_i32(), buf); + } + } + if !self.external_aad.is_empty() { + ::buffa::types::put_shared_bytes_field(8u32, &self.external_aad, buf); + } + if !self.expected_kid.is_empty() { + ::buffa::types::put_shared_bytes_field(9u32, &self.expected_kid, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseSign1VerifyDetachedRequestView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.cose_sign1) { + __map + .serialize_entry( + "coseSign1", + &::buffa::json_helpers::BytesJson(self.cose_sign1), + )?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.payload) { + __map + .serialize_entry( + "payload", + &::buffa::json_helpers::BytesJson(self.payload), + )?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.public_key) { + __map + .serialize_entry( + "publicKey", + &::buffa::json_helpers::BytesJson(self.public_key), + )?; + } + if !::buffa::json_helpers::skip_if::is_zero_u64(&self.max_cose_sign1_bytes) { + __map + .serialize_entry( + "maxCoseSign1Bytes", + &::buffa::json_helpers::ProtoJson(&self.max_cose_sign1_bytes), + )?; + } + if !::buffa::json_helpers::skip_if::is_zero_u64( + &self.max_detached_payload_bytes, + ) { + __map + .serialize_entry( + "maxDetachedPayloadBytes", + &::buffa::json_helpers::ProtoJson(&self.max_detached_payload_bytes), + )?; + } + if self.require_kid { + __map.serialize_entry("requireKid", &self.require_kid)?; + } + if !self.allowed_algorithms.is_empty() { + __map + .serialize_entry( + "allowedAlgorithms", + &::buffa::json_helpers::EnumSeqJson(&self.allowed_algorithms), + )?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.external_aad) { + __map + .serialize_entry( + "externalAad", + &::buffa::json_helpers::BytesJson(self.external_aad), + )?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.expected_kid) { + __map + .serialize_entry( + "expectedKid", + &::buffa::json_helpers::BytesJson(self.expected_kid), + )?; + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseSign1VerifyDetachedRequestView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseSign1VerifyDetachedRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseSign1VerifyDetachedRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseSign1VerifyDetachedRequest"; +} +::buffa::impl_default_view_instance!(CoseSign1VerifyDetachedRequestView); +::buffa::impl_view_reborrow!(CoseSign1VerifyDetachedRequestView); +/** Self-contained, `'static` owned view of a `CoseSign1VerifyDetachedRequest` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseSign1VerifyDetachedRequestView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseSign1VerifyDetachedRequestView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone)] +pub struct CoseSign1VerifyDetachedRequestOwnedView( + ::buffa::OwnedView>, +); +impl ::core::fmt::Debug for CoseSign1VerifyDetachedRequestOwnedView { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseSign1VerifyDetachedRequestOwnedView()") + } +} +impl CoseSign1VerifyDetachedRequestOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseSign1VerifyDetachedRequestOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseSign1VerifyDetachedRequestOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseSign1VerifyDetachedRequest, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseSign1VerifyDetachedRequestOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseSign1VerifyDetachedRequestView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseSign1VerifyDetachedRequestView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseSign1VerifyDetachedRequest { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// SENSITIVE: encoded COSE_Sign1 bytes to verify. Tagged and untagged inputs + /// are accepted. + /// + /// Field 1: `cose_sign1` + #[must_use] + pub fn cose_sign1(&self) -> &'_ [u8] { + self.0.reborrow().cose_sign1 + } + /// SENSITIVE: detached payload bytes that must match the signature. Payloads + /// can carry credential claims or PII and must not be logged. + /// + /// Field 2: `payload` + #[must_use] + pub fn payload(&self) -> &'_ [u8] { + self.0.reborrow().payload + } + /// Raw public key bytes selected by the caller for the protected-header kid. + /// + /// Field 3: `public_key` + #[must_use] + pub fn public_key(&self) -> &'_ [u8] { + self.0.reborrow().public_key + } + /// Optional maximum accepted encoded COSE_Sign1 size. A value of 0 uses the + /// crate default. + /// + /// Field 4: `max_cose_sign1_bytes` + #[must_use] + pub fn max_cose_sign1_bytes(&self) -> u64 { + self.0.reborrow().max_cose_sign1_bytes + } + /// Optional maximum accepted detached payload size. A value of 0 uses the + /// crate default. + /// + /// Field 5: `max_detached_payload_bytes` + #[must_use] + pub fn max_detached_payload_bytes(&self) -> u64 { + self.0.reborrow().max_detached_payload_bytes + } + /// Require a non-empty protected-header kid before verification. + /// + /// Field 6: `require_kid` + #[must_use] + pub fn require_kid(&self) -> bool { + self.0.reborrow().require_kid + } + /// Optional algorithm allow-list. Empty means any COSE algorithm supported by + /// this crate and operation. + /// + /// Field 7: `allowed_algorithms` + #[must_use] + pub fn allowed_algorithms( + &self, + ) -> &::buffa::RepeatedView< + '_, + ::buffa::EnumValue, + > { + &self.0.reborrow().allowed_algorithms + } + /// SENSITIVE: external AAD supplied by the application for verification. + /// + /// Field 8: `external_aad` + #[must_use] + pub fn external_aad(&self) -> &'_ [u8] { + self.0.reborrow().external_aad + } + /// Optional trusted key-selection identifier. When non-empty, the + /// protected-header kid must match this value before public_key can be + /// returned by the adapter's resolver. Empty preserves resolver-style use + /// where public_key was already selected outside this request. + /// Selection of this value belongs to the caller's trust-store boundary. + /// + /// Field 9: `expected_kid` + #[must_use] + pub fn expected_kid(&self) -> &'_ [u8] { + self.0.reborrow().expected_kid + } +} +impl ::core::convert::From< + ::buffa::OwnedView>, +> for CoseSign1VerifyDetachedRequestOwnedView { + fn from( + inner: ::buffa::OwnedView>, + ) -> Self { + CoseSign1VerifyDetachedRequestOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseSign1VerifyDetachedRequestOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef< + ::buffa::OwnedView>, +> for CoseSign1VerifyDetachedRequestOwnedView { + fn as_ref( + &self, + ) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseSign1VerifyDetachedRequest { + type View<'a> = CoseSign1VerifyDetachedRequestView<'a>; + type ViewHandle = CoseSign1VerifyDetachedRequestOwnedView; +} +impl ::serde::Serialize for CoseSign1VerifyDetachedRequestOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +/// CoseSign1VerifyResult contains the verified attached payload and protected +/// header metadata. Detached verification returns an empty payload field. +#[derive(Clone, Default)] +pub struct CoseSign1VerifyResultView<'a> { + /// SENSITIVE: verified attached payload bytes, or empty for detached + /// verification. Payloads can carry credential claims or PII and must not be + /// logged. + /// + /// Field 1: `payload` + pub payload: &'a [u8], + /// Verified protected-header algorithm. + /// + /// Field 2: `algorithm` + pub algorithm: ::buffa::EnumValue, + /// Verified protected-header kid bytes. + /// + /// Field 3: `kid` + pub kid: &'a [u8], + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl ::core::fmt::Debug for CoseSign1VerifyResultView<'_> { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseSign1VerifyResultView()") + } +} +impl<'a> ::buffa::MessageView<'a> for CoseSign1VerifyResultView<'a> { + type Owned = super::super::CoseSign1VerifyResult; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.payload = ::buffa::types::borrow_bytes(&mut cur)?; + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.algorithm = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(&mut cur)?, + ); + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.kid = ::buffa::types::borrow_bytes(&mut cur)?; + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result< + super::super::CoseSign1VerifyResult, + ::buffa::DecodeError, + > { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result< + super::super::CoseSign1VerifyResult, + ::buffa::DecodeError, + > { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseSign1VerifyResult { + payload: (self.payload).to_vec(), + algorithm: self.algorithm, + kid: (self.kid).to_vec(), + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseSign1VerifyResultView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.payload.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.payload) as u64; + } + { + let val = self.algorithm.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + if !self.kid.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.kid) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.payload.is_empty() { + ::buffa::types::put_shared_bytes_field(1u32, &self.payload, buf); + } + { + let val = self.algorithm.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(2u32, val, buf); + } + } + if !self.kid.is_empty() { + ::buffa::types::put_shared_bytes_field(3u32, &self.kid, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseSign1VerifyResultView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.payload) { + __map + .serialize_entry( + "payload", + &::buffa::json_helpers::BytesJson(self.payload), + )?; + } + if !::buffa::json_helpers::skip_if::is_default_enum_value(&self.algorithm) { + __map.serialize_entry("algorithm", &self.algorithm)?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.kid) { + __map.serialize_entry("kid", &::buffa::json_helpers::BytesJson(self.kid))?; + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseSign1VerifyResultView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseSign1VerifyResult"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseSign1VerifyResult"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseSign1VerifyResult"; +} +::buffa::impl_default_view_instance!(CoseSign1VerifyResultView); +::buffa::impl_view_reborrow!(CoseSign1VerifyResultView); +/** Self-contained, `'static` owned view of a `CoseSign1VerifyResult` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseSign1VerifyResultView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseSign1VerifyResultView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone)] +pub struct CoseSign1VerifyResultOwnedView( + ::buffa::OwnedView>, +); +impl ::core::fmt::Debug for CoseSign1VerifyResultOwnedView { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseSign1VerifyResultOwnedView()") + } +} +impl CoseSign1VerifyResultOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseSign1VerifyResultOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseSign1VerifyResultOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseSign1VerifyResult, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseSign1VerifyResultOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseSign1VerifyResultView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseSign1VerifyResultView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseSign1VerifyResult { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// SENSITIVE: verified attached payload bytes, or empty for detached + /// verification. Payloads can carry credential claims or PII and must not be + /// logged. + /// + /// Field 1: `payload` + #[must_use] + pub fn payload(&self) -> &'_ [u8] { + self.0.reborrow().payload + } + /// Verified protected-header algorithm. + /// + /// Field 2: `algorithm` + #[must_use] + pub fn algorithm(&self) -> ::buffa::EnumValue { + self.0.reborrow().algorithm + } + /// Verified protected-header kid bytes. + /// + /// Field 3: `kid` + #[must_use] + pub fn kid(&self) -> &'_ [u8] { + self.0.reborrow().kid + } +} +impl ::core::convert::From<::buffa::OwnedView>> +for CoseSign1VerifyResultOwnedView { + fn from(inner: ::buffa::OwnedView>) -> Self { + CoseSign1VerifyResultOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseSign1VerifyResultOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef<::buffa::OwnedView>> +for CoseSign1VerifyResultOwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseSign1VerifyResult { + type View<'a> = CoseSign1VerifyResultView<'a>; + type ViewHandle = CoseSign1VerifyResultOwnedView; +} +impl ::serde::Serialize for CoseSign1VerifyResultOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +/// CoseKeyFromPublicBytesRequest builds a public COSE_Key from raw public key +/// bytes and an explicit algorithm binding. +#[derive(Clone, Default)] +pub struct CoseKeyFromPublicBytesRequestView<'a> { + /// Algorithm that determines the COSE_Key kty/crv/alg profile. + /// + /// Field 1: `algorithm` + pub algorithm: ::buffa::MessageFieldView< + super::super::__buffa::view::CoseAlgorithmIdentifierView<'a>, + >, + /// Raw public key bytes for the selected algorithm. + /// + /// Field 2: `public_key` + pub public_key: &'a [u8], + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl ::core::fmt::Debug for CoseKeyFromPublicBytesRequestView<'_> { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseKeyFromPublicBytesRequestView()") + } +} +impl<'a> ::buffa::MessageView<'a> for CoseKeyFromPublicBytesRequestView<'a> { + type Owned = super::super::CoseKeyFromPublicBytesRequest; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + match view.algorithm.as_mut() { + Some(existing) => { + ::buffa::MessageView::merge_into_view(existing, sub, __sub_ctx)? + } + None => { + view.algorithm = ::buffa::MessageFieldView::set( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ); + } + } + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.public_key = ::buffa::types::borrow_bytes(&mut cur)?; + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result< + super::super::CoseKeyFromPublicBytesRequest, + ::buffa::DecodeError, + > { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result< + super::super::CoseKeyFromPublicBytesRequest, + ::buffa::DecodeError, + > { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseKeyFromPublicBytesRequest { + algorithm: match self.algorithm.as_option() { + Some(v) => { + ::buffa::MessageField::< + super::super::CoseAlgorithmIdentifier, + ::buffa::Inline, + >::some(v.to_owned_from_source(__buffa_src)?) + } + None => ::buffa::MessageField::none(), + }, + public_key: (self.public_key).to_vec(), + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseKeyFromPublicBytesRequestView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, __cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if self.algorithm.is_set() { + let __slot = __cache.reserve(); + let inner_size = self.algorithm.compute_size(__cache); + __cache.set(__slot, inner_size); + size + += 1u64 + ::buffa::encoding::varint_len(inner_size as u64) as u64 + + inner_size as u64; + } + if !self.public_key.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.public_key) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + __cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.algorithm.is_set() { + ::buffa::types::put_len_delimited_header( + 1u32, + u64::from(__cache.consume_next()), + buf, + ); + self.algorithm.write_to(__cache, buf); + } + if !self.public_key.is_empty() { + ::buffa::types::put_shared_bytes_field(2u32, &self.public_key, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseKeyFromPublicBytesRequestView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + { + if let ::core::option::Option::Some(__v) = self.algorithm.as_option() { + __map.serialize_entry("algorithm", __v)?; + } + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.public_key) { + __map + .serialize_entry( + "publicKey", + &::buffa::json_helpers::BytesJson(self.public_key), + )?; + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseKeyFromPublicBytesRequestView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseKeyFromPublicBytesRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseKeyFromPublicBytesRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseKeyFromPublicBytesRequest"; +} +::buffa::impl_default_view_instance!(CoseKeyFromPublicBytesRequestView); +::buffa::impl_view_reborrow!(CoseKeyFromPublicBytesRequestView); +/** Self-contained, `'static` owned view of a `CoseKeyFromPublicBytesRequest` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseKeyFromPublicBytesRequestView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseKeyFromPublicBytesRequestView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone)] +pub struct CoseKeyFromPublicBytesRequestOwnedView( + ::buffa::OwnedView>, +); +impl ::core::fmt::Debug for CoseKeyFromPublicBytesRequestOwnedView { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseKeyFromPublicBytesRequestOwnedView()") + } +} +impl CoseKeyFromPublicBytesRequestOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseKeyFromPublicBytesRequestOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseKeyFromPublicBytesRequestOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseKeyFromPublicBytesRequest, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseKeyFromPublicBytesRequestOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseKeyFromPublicBytesRequestView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseKeyFromPublicBytesRequestView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseKeyFromPublicBytesRequest { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// Algorithm that determines the COSE_Key kty/crv/alg profile. + /// + /// Field 1: `algorithm` + #[must_use] + pub fn algorithm( + &self, + ) -> &::buffa::MessageFieldView< + super::super::__buffa::view::CoseAlgorithmIdentifierView<'_>, + > { + &self.0.reborrow().algorithm + } + /// Raw public key bytes for the selected algorithm. + /// + /// Field 2: `public_key` + #[must_use] + pub fn public_key(&self) -> &'_ [u8] { + self.0.reborrow().public_key + } +} +impl ::core::convert::From< + ::buffa::OwnedView>, +> for CoseKeyFromPublicBytesRequestOwnedView { + fn from( + inner: ::buffa::OwnedView>, + ) -> Self { + CoseKeyFromPublicBytesRequestOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseKeyFromPublicBytesRequestOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef< + ::buffa::OwnedView>, +> for CoseKeyFromPublicBytesRequestOwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseKeyFromPublicBytesRequest { + type View<'a> = CoseKeyFromPublicBytesRequestView<'a>; + type ViewHandle = CoseKeyFromPublicBytesRequestOwnedView; +} +impl ::serde::Serialize for CoseKeyFromPublicBytesRequestOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +/// CoseKeyFromPrivateBytesRequest builds a private COSE_Key from raw private key +/// bytes and matching public key bytes. +#[derive(Clone, Default)] +pub struct CoseKeyFromPrivateBytesRequestView<'a> { + /// Algorithm that determines the COSE_Key kty/crv/alg profile. + /// + /// Field 1: `algorithm` + pub algorithm: ::buffa::MessageFieldView< + super::super::__buffa::view::CoseAlgorithmIdentifierView<'a>, + >, + /// SENSITIVE: raw private key bytes. Callers must keep this in a + /// sensitive-buffer owner outside this transient protobuf container. + /// + /// Field 2: `private_key` + pub private_key: &'a [u8], + /// Matching raw public key bytes. Ignored unless has_public_key is true; + /// supported private-key constructions reject requests that omit it. + /// + /// Field 3: `public_key` + pub public_key: &'a [u8], + /// Whether public_key is present. + /// + /// Field 4: `has_public_key` + pub has_public_key: bool, + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl ::core::fmt::Debug for CoseKeyFromPrivateBytesRequestView<'_> { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseKeyFromPrivateBytesRequestView()") + } +} +impl<'a> ::buffa::MessageView<'a> for CoseKeyFromPrivateBytesRequestView<'a> { + type Owned = super::super::CoseKeyFromPrivateBytesRequest; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + match view.algorithm.as_mut() { + Some(existing) => { + ::buffa::MessageView::merge_into_view(existing, sub, __sub_ctx)? + } + None => { + view.algorithm = ::buffa::MessageFieldView::set( + ::decode_view_ctx( + sub, + __sub_ctx, + )?, + ); + } + } + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.private_key = ::buffa::types::borrow_bytes(&mut cur)?; + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.public_key = ::buffa::types::borrow_bytes(&mut cur)?; + } + 4u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.has_public_key = ::buffa::types::decode_bool(&mut cur)?; + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result< + super::super::CoseKeyFromPrivateBytesRequest, + ::buffa::DecodeError, + > { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result< + super::super::CoseKeyFromPrivateBytesRequest, + ::buffa::DecodeError, + > { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseKeyFromPrivateBytesRequest { + algorithm: match self.algorithm.as_option() { + Some(v) => { + ::buffa::MessageField::< + super::super::CoseAlgorithmIdentifier, + ::buffa::Inline, + >::some(v.to_owned_from_source(__buffa_src)?) + } + None => ::buffa::MessageField::none(), + }, + private_key: (self.private_key).to_vec(), + public_key: (self.public_key).to_vec(), + has_public_key: self.has_public_key, + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseKeyFromPrivateBytesRequestView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, __cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if self.algorithm.is_set() { + let __slot = __cache.reserve(); + let inner_size = self.algorithm.compute_size(__cache); + __cache.set(__slot, inner_size); + size + += 1u64 + ::buffa::encoding::varint_len(inner_size as u64) as u64 + + inner_size as u64; + } + if !self.private_key.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.private_key) as u64; + } + if !self.public_key.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.public_key) as u64; + } + if self.has_public_key { + size += 1u64 + ::buffa::types::BOOL_ENCODED_LEN as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + __cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.algorithm.is_set() { + ::buffa::types::put_len_delimited_header( + 1u32, + u64::from(__cache.consume_next()), + buf, + ); + self.algorithm.write_to(__cache, buf); + } + if !self.private_key.is_empty() { + ::buffa::types::put_shared_bytes_field(2u32, &self.private_key, buf); + } + if !self.public_key.is_empty() { + ::buffa::types::put_shared_bytes_field(3u32, &self.public_key, buf); + } + if self.has_public_key { + ::buffa::types::put_bool_field(4u32, self.has_public_key, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseKeyFromPrivateBytesRequestView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + { + if let ::core::option::Option::Some(__v) = self.algorithm.as_option() { + __map.serialize_entry("algorithm", __v)?; + } + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.private_key) { + __map + .serialize_entry( + "privateKey", + &::buffa::json_helpers::BytesJson(self.private_key), + )?; + } + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.public_key) { + __map + .serialize_entry( + "publicKey", + &::buffa::json_helpers::BytesJson(self.public_key), + )?; + } + if self.has_public_key { + __map.serialize_entry("hasPublicKey", &self.has_public_key)?; + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseKeyFromPrivateBytesRequestView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseKeyFromPrivateBytesRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseKeyFromPrivateBytesRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseKeyFromPrivateBytesRequest"; +} +::buffa::impl_default_view_instance!(CoseKeyFromPrivateBytesRequestView); +::buffa::impl_view_reborrow!(CoseKeyFromPrivateBytesRequestView); +/** Self-contained, `'static` owned view of a `CoseKeyFromPrivateBytesRequest` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseKeyFromPrivateBytesRequestView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseKeyFromPrivateBytesRequestView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone)] +pub struct CoseKeyFromPrivateBytesRequestOwnedView( + ::buffa::OwnedView>, +); +impl ::core::fmt::Debug for CoseKeyFromPrivateBytesRequestOwnedView { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseKeyFromPrivateBytesRequestOwnedView()") + } +} +impl CoseKeyFromPrivateBytesRequestOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseKeyFromPrivateBytesRequestOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseKeyFromPrivateBytesRequestOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseKeyFromPrivateBytesRequest, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseKeyFromPrivateBytesRequestOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseKeyFromPrivateBytesRequestView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseKeyFromPrivateBytesRequestView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseKeyFromPrivateBytesRequest { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// Algorithm that determines the COSE_Key kty/crv/alg profile. + /// + /// Field 1: `algorithm` + #[must_use] + pub fn algorithm( + &self, + ) -> &::buffa::MessageFieldView< + super::super::__buffa::view::CoseAlgorithmIdentifierView<'_>, + > { + &self.0.reborrow().algorithm + } + /// SENSITIVE: raw private key bytes. Callers must keep this in a + /// sensitive-buffer owner outside this transient protobuf container. + /// + /// Field 2: `private_key` + #[must_use] + pub fn private_key(&self) -> &'_ [u8] { + self.0.reborrow().private_key + } + /// Matching raw public key bytes. Ignored unless has_public_key is true; + /// supported private-key constructions reject requests that omit it. + /// + /// Field 3: `public_key` + #[must_use] + pub fn public_key(&self) -> &'_ [u8] { + self.0.reborrow().public_key + } + /// Whether public_key is present. + /// + /// Field 4: `has_public_key` + #[must_use] + pub fn has_public_key(&self) -> bool { + self.0.reborrow().has_public_key + } +} +impl ::core::convert::From< + ::buffa::OwnedView>, +> for CoseKeyFromPrivateBytesRequestOwnedView { + fn from( + inner: ::buffa::OwnedView>, + ) -> Self { + CoseKeyFromPrivateBytesRequestOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseKeyFromPrivateBytesRequestOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef< + ::buffa::OwnedView>, +> for CoseKeyFromPrivateBytesRequestOwnedView { + fn as_ref( + &self, + ) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseKeyFromPrivateBytesRequest { + type View<'a> = CoseKeyFromPrivateBytesRequestView<'a>; + type ViewHandle = CoseKeyFromPrivateBytesRequestOwnedView; +} +impl ::serde::Serialize for CoseKeyFromPrivateBytesRequestOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +/// CoseKeyBytesRequest carries encoded COSE_Key bytes for parse, extraction, +/// deterministic kid derivation, and Multikey conversion operations. +#[derive(Clone, Default)] +pub struct CoseKeyBytesRequestView<'a> { + /// SENSITIVE: encoded COSE_Key bytes. This may contain private key material + /// for private extraction; wipe transient protobuf containers as soon as + /// practical. + /// + /// Field 1: `cose_key` + pub cose_key: &'a [u8], + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl ::core::fmt::Debug for CoseKeyBytesRequestView<'_> { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseKeyBytesRequestView()") + } +} +impl<'a> ::buffa::MessageView<'a> for CoseKeyBytesRequestView<'a> { + type Owned = super::super::CoseKeyBytesRequest; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.cose_key = ::buffa::types::borrow_bytes(&mut cur)?; + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result< + super::super::CoseKeyBytesRequest, + ::buffa::DecodeError, + > { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result< + super::super::CoseKeyBytesRequest, + ::buffa::DecodeError, + > { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseKeyBytesRequest { + cose_key: (self.cose_key).to_vec(), + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseKeyBytesRequestView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.cose_key.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.cose_key) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.cose_key.is_empty() { + ::buffa::types::put_shared_bytes_field(1u32, &self.cose_key, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseKeyBytesRequestView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.cose_key) { + __map + .serialize_entry( + "coseKey", + &::buffa::json_helpers::BytesJson(self.cose_key), + )?; + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseKeyBytesRequestView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseKeyBytesRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseKeyBytesRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseKeyBytesRequest"; +} +::buffa::impl_default_view_instance!(CoseKeyBytesRequestView); +::buffa::impl_view_reborrow!(CoseKeyBytesRequestView); +/** Self-contained, `'static` owned view of a `CoseKeyBytesRequest` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseKeyBytesRequestView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseKeyBytesRequestView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone)] +pub struct CoseKeyBytesRequestOwnedView( + ::buffa::OwnedView>, +); +impl ::core::fmt::Debug for CoseKeyBytesRequestOwnedView { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseKeyBytesRequestOwnedView()") + } +} +impl CoseKeyBytesRequestOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseKeyBytesRequestOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseKeyBytesRequestOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseKeyBytesRequest, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseKeyBytesRequestOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseKeyBytesRequestView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseKeyBytesRequestView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseKeyBytesRequest { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// SENSITIVE: encoded COSE_Key bytes. This may contain private key material + /// for private extraction; wipe transient protobuf containers as soon as + /// practical. + /// + /// Field 1: `cose_key` + #[must_use] + pub fn cose_key(&self) -> &'_ [u8] { + self.0.reborrow().cose_key + } +} +impl ::core::convert::From<::buffa::OwnedView>> +for CoseKeyBytesRequestOwnedView { + fn from(inner: ::buffa::OwnedView>) -> Self { + CoseKeyBytesRequestOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseKeyBytesRequestOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef<::buffa::OwnedView>> +for CoseKeyBytesRequestOwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseKeyBytesRequest { + type View<'a> = CoseKeyBytesRequestView<'a>; + type ViewHandle = CoseKeyBytesRequestOwnedView; +} +impl ::serde::Serialize for CoseKeyBytesRequestOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +/// CoseKeyBytesResult carries encoded COSE_Key bytes or raw key/kid bytes, +/// depending on the operation-specific result envelope. +#[derive(Clone, Default)] +pub struct CoseKeyBytesResultView<'a> { + /// SENSITIVE: operation-specific bytes. For private-key extraction this + /// contains private key material and must be moved into a sensitive-buffer + /// owner by receivers. + /// + /// Field 1: `key_bytes` + pub key_bytes: &'a [u8], + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl ::core::fmt::Debug for CoseKeyBytesResultView<'_> { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseKeyBytesResultView()") + } +} +impl<'a> ::buffa::MessageView<'a> for CoseKeyBytesResultView<'a> { + type Owned = super::super::CoseKeyBytesResult; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.key_bytes = ::buffa::types::borrow_bytes(&mut cur)?; + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseKeyBytesResult { + key_bytes: (self.key_bytes).to_vec(), + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseKeyBytesResultView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.key_bytes.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.key_bytes) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.key_bytes.is_empty() { + ::buffa::types::put_shared_bytes_field(1u32, &self.key_bytes, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseKeyBytesResultView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if !::buffa::json_helpers::skip_if::is_empty_bytes(self.key_bytes) { + __map + .serialize_entry( + "keyBytes", + &::buffa::json_helpers::BytesJson(self.key_bytes), + )?; + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseKeyBytesResultView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseKeyBytesResult"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseKeyBytesResult"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseKeyBytesResult"; +} +::buffa::impl_default_view_instance!(CoseKeyBytesResultView); +::buffa::impl_view_reborrow!(CoseKeyBytesResultView); +/** Self-contained, `'static` owned view of a `CoseKeyBytesResult` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseKeyBytesResultView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseKeyBytesResultView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone)] +pub struct CoseKeyBytesResultOwnedView( + ::buffa::OwnedView>, +); +impl ::core::fmt::Debug for CoseKeyBytesResultOwnedView { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseKeyBytesResultOwnedView()") + } +} +impl CoseKeyBytesResultOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseKeyBytesResultOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseKeyBytesResultOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseKeyBytesResult, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseKeyBytesResultOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseKeyBytesResultView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseKeyBytesResultView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseKeyBytesResult { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// SENSITIVE: operation-specific bytes. For private-key extraction this + /// contains private key material and must be moved into a sensitive-buffer + /// owner by receivers. + /// + /// Field 1: `key_bytes` + #[must_use] + pub fn key_bytes(&self) -> &'_ [u8] { + self.0.reborrow().key_bytes + } +} +impl ::core::convert::From<::buffa::OwnedView>> +for CoseKeyBytesResultOwnedView { + fn from(inner: ::buffa::OwnedView>) -> Self { + CoseKeyBytesResultOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseKeyBytesResultOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef<::buffa::OwnedView>> +for CoseKeyBytesResultOwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseKeyBytesResult { + type View<'a> = CoseKeyBytesResultView<'a>; + type ViewHandle = CoseKeyBytesResultOwnedView; +} +impl ::serde::Serialize for CoseKeyBytesResultOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +/// CoseMultikeyToCoseKeyRequest converts a Multikey string into a public +/// COSE_Key. +#[derive(Clone, Default)] +pub struct CoseMultikeyToCoseKeyRequestView<'a> { + /// Multikey string to parse. + /// + /// Field 1: `multikey` + pub multikey: &'a str, + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl ::core::fmt::Debug for CoseMultikeyToCoseKeyRequestView<'_> { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseMultikeyToCoseKeyRequestView()") + } +} +impl<'a> ::buffa::MessageView<'a> for CoseMultikeyToCoseKeyRequestView<'a> { + type Owned = super::super::CoseMultikeyToCoseKeyRequest; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.multikey = ::buffa::types::borrow_str(&mut cur)?; + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result< + super::super::CoseMultikeyToCoseKeyRequest, + ::buffa::DecodeError, + > { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result< + super::super::CoseMultikeyToCoseKeyRequest, + ::buffa::DecodeError, + > { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseMultikeyToCoseKeyRequest { + multikey: self.multikey.to_string(), + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseMultikeyToCoseKeyRequestView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.multikey.is_empty() { + size += 1u64 + ::buffa::types::string_encoded_len(&self.multikey) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.multikey.is_empty() { + ::buffa::types::put_string_field(1u32, &self.multikey, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseMultikeyToCoseKeyRequestView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if !::buffa::json_helpers::skip_if::is_empty_str(self.multikey) { + __map.serialize_entry("multikey", self.multikey)?; + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseMultikeyToCoseKeyRequestView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseMultikeyToCoseKeyRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseMultikeyToCoseKeyRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseMultikeyToCoseKeyRequest"; +} +::buffa::impl_default_view_instance!(CoseMultikeyToCoseKeyRequestView); +::buffa::impl_view_reborrow!(CoseMultikeyToCoseKeyRequestView); +/** Self-contained, `'static` owned view of a `CoseMultikeyToCoseKeyRequest` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseMultikeyToCoseKeyRequestView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseMultikeyToCoseKeyRequestView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone)] +pub struct CoseMultikeyToCoseKeyRequestOwnedView( + ::buffa::OwnedView>, +); +impl ::core::fmt::Debug for CoseMultikeyToCoseKeyRequestOwnedView { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseMultikeyToCoseKeyRequestOwnedView()") + } +} +impl CoseMultikeyToCoseKeyRequestOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseMultikeyToCoseKeyRequestOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseMultikeyToCoseKeyRequestOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseMultikeyToCoseKeyRequest, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseMultikeyToCoseKeyRequestOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseMultikeyToCoseKeyRequestView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseMultikeyToCoseKeyRequestView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseMultikeyToCoseKeyRequest { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// Multikey string to parse. + /// + /// Field 1: `multikey` + #[must_use] + pub fn multikey(&self) -> &'_ str { + self.0.reborrow().multikey + } +} +impl ::core::convert::From<::buffa::OwnedView>> +for CoseMultikeyToCoseKeyRequestOwnedView { + fn from( + inner: ::buffa::OwnedView>, + ) -> Self { + CoseMultikeyToCoseKeyRequestOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseMultikeyToCoseKeyRequestOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef< + ::buffa::OwnedView>, +> for CoseMultikeyToCoseKeyRequestOwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseMultikeyToCoseKeyRequest { + type View<'a> = CoseMultikeyToCoseKeyRequestView<'a>; + type ViewHandle = CoseMultikeyToCoseKeyRequestOwnedView; +} +impl ::serde::Serialize for CoseMultikeyToCoseKeyRequestOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} +/// CoseMultikeyResult contains a Multikey string produced from a public COSE_Key. +#[derive(Clone, Default)] +pub struct CoseMultikeyResultView<'a> { + /// Multikey string. + /// + /// Field 1: `multikey` + pub multikey: &'a str, + pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, +} +impl ::core::fmt::Debug for CoseMultikeyResultView<'_> { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseMultikeyResultView()") + } +} +impl<'a> ::buffa::MessageView<'a> for CoseMultikeyResultView<'a> { + type Owned = super::super::CoseMultikeyResult; + fn decode_view(buf: &'a [u8]) -> ::core::result::Result { + let __limit = ::core::cell::Cell::new(::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT); + ::decode_view_ctx( + buf, + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ) + } + fn decode_view_with_ctx( + buf: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result { + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.multikey = ::buffa::types::borrow_str(&mut cur)?; + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields.push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) + } + fn to_owned_message( + &self, + ) -> ::core::result::Result { + self.to_owned_from_source(None) + } + #[allow(clippy::useless_conversion, clippy::needless_update)] + fn to_owned_from_source( + &self, + __buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>, + ) -> ::core::result::Result { + #[allow(unused_imports)] + use ::buffa::alloc::string::ToString as _; + let _ = __buffa_src; + ::core::result::Result::Ok(super::super::CoseMultikeyResult { + multikey: self.multikey.to_string(), + __buffa_unknown_fields: self.__buffa_unknown_fields.to_owned()?.into(), + ..::core::default::Default::default() + }) + } +} +impl<'a> ::buffa::ViewEncode<'a> for CoseMultikeyResultView<'a> { + #[allow(clippy::needless_borrow, clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.multikey.is_empty() { + size += 1u64 + ::buffa::types::string_encoded_len(&self.multikey) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + #[allow(clippy::needless_borrow)] + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.multikey.is_empty() { + ::buffa::types::put_string_field(1u32, &self.multikey, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } +} +/// Serializes this view as protobuf JSON. +/// +/// Implicit-presence fields with default values are omitted, `required` +/// fields are always emitted, explicit-presence (`optional`) fields are +/// emitted only when set, bytes fields are base64-encoded, and enum +/// values are their proto name strings. +/// +/// This impl uses `serialize_map(None)` because the number of emitted +/// fields depends on default-omission rules; serializers that require +/// known map lengths (e.g. `bincode`) will return a runtime error. +/// Use the owned message type for those formats. +impl<'__a> ::serde::Serialize for CoseMultikeyResultView<'__a> { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + use ::serde::ser::SerializeMap as _; + let mut __map = __s.serialize_map(::core::option::Option::None)?; + if !::buffa::json_helpers::skip_if::is_empty_str(self.multikey) { + __map.serialize_entry("multikey", self.multikey)?; + } + __map.end() + } +} +impl<'a> ::buffa::MessageName for CoseMultikeyResultView<'a> { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseMultikeyResult"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseMultikeyResult"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseMultikeyResult"; +} +::buffa::impl_default_view_instance!(CoseMultikeyResultView); +::buffa::impl_view_reborrow!(CoseMultikeyResultView); +/** Self-contained, `'static` owned view of a `CoseMultikeyResult` message. + + Wraps [`::buffa::OwnedView`]`<`[`CoseMultikeyResultView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. + + Field accessors return borrows tied to `&self`. Use [`Self::view`] to get the full [`CoseMultikeyResultView`] when you need struct patterns, iteration helpers, or to pass the view to lifetime-parameterised code.*/ +#[derive(Clone)] +pub struct CoseMultikeyResultOwnedView( + ::buffa::OwnedView>, +); +impl ::core::fmt::Debug for CoseMultikeyResultOwnedView { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("CoseMultikeyResultOwnedView()") + } +} +impl CoseMultikeyResultOwnedView { + /// Decode an owned view from a [`::buffa::bytes::Bytes`] buffer. + /// + /// The view borrows directly from the buffer's data; the buffer is + /// retained inside the returned handle. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer contains invalid + /// protobuf data. + pub fn decode( + bytes: ::buffa::bytes::Bytes, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseMultikeyResultOwnedView(::buffa::OwnedView::decode(bytes)?), + ) + } + /// Decode with custom [`::buffa::DecodeOptions`] (recursion limit, + /// max message size). + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError`] if the buffer is invalid or + /// exceeds the configured limits. + pub fn decode_with_options( + bytes: ::buffa::bytes::Bytes, + opts: &::buffa::DecodeOptions, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseMultikeyResultOwnedView( + ::buffa::OwnedView::decode_with_options(bytes, opts)?, + ), + ) + } + /// Build from an owned message via an encode → decode round-trip. + /// + /// # Errors + /// + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are + /// somehow invalid (should not happen for well-formed messages). + pub fn from_owned( + msg: &super::super::CoseMultikeyResult, + ) -> ::core::result::Result { + ::core::result::Result::Ok( + CoseMultikeyResultOwnedView(::buffa::OwnedView::from_owned(msg)?), + ) + } + /// Borrow the full [`CoseMultikeyResultView`] with its lifetime tied to `&self`. + #[must_use] + pub fn view(&self) -> &CoseMultikeyResultView<'_> { + self.0.reborrow() + } + /// Convert to the owned message type. + /// + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::CoseMultikeyResult { + self.0.to_owned_message() + } + /// The underlying bytes buffer. + #[must_use] + pub fn bytes(&self) -> &::buffa::bytes::Bytes { + self.0.bytes() + } + /// Consume the handle, returning the underlying bytes buffer. + #[must_use] + pub fn into_bytes(self) -> ::buffa::bytes::Bytes { + self.0.into_bytes() + } + /// Multikey string. + /// + /// Field 1: `multikey` + #[must_use] + pub fn multikey(&self) -> &'_ str { + self.0.reborrow().multikey + } +} +impl ::core::convert::From<::buffa::OwnedView>> +for CoseMultikeyResultOwnedView { + fn from(inner: ::buffa::OwnedView>) -> Self { + CoseMultikeyResultOwnedView(inner) + } +} +impl ::core::convert::From +for ::buffa::OwnedView> { + fn from(wrapper: CoseMultikeyResultOwnedView) -> Self { + wrapper.0 + } +} +impl ::core::convert::AsRef<::buffa::OwnedView>> +for CoseMultikeyResultOwnedView { + fn as_ref(&self) -> &::buffa::OwnedView> { + &self.0 + } +} +impl ::buffa::HasMessageView for super::super::CoseMultikeyResult { + type View<'a> = CoseMultikeyResultView<'a>; + type ViewHandle = CoseMultikeyResultOwnedView; +} +impl ::serde::Serialize for CoseMultikeyResultOwnedView { + fn serialize<__S: ::serde::Serializer>( + &self, + __s: __S, + ) -> ::core::result::Result<__S::Ok, __S::Error> { + ::serde::Serialize::serialize(&self.0, __s) + } +} diff --git a/crates/proto/src/generated/buffa/reallyme.cose.v1.cose.__view_oneof.rs b/crates/proto/src/generated/buffa/reallyme.cose.v1.cose.__view_oneof.rs new file mode 100644 index 0000000..db91bf1 --- /dev/null +++ b/crates/proto/src/generated/buffa/reallyme.cose.v1.cose.__view_oneof.rs @@ -0,0 +1,236 @@ +// @generated by buffa-codegen. DO NOT EDIT. +// source: reallyme/cose/v1/cose.proto + +pub mod cose_error { + #[allow(unused_imports)] + use super::*; + #[derive(Clone, Debug)] + pub enum Error<'a> { + Primitive( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CosePrimitiveErrorView<'a>, + >, + ), + Provider( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseProviderErrorView<'a>, + >, + ), + Backend( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseBackendErrorView<'a>, + >, + ), + } +} +pub mod cose_algorithm_identifier { + #[allow(unused_imports)] + use super::*; + #[derive(Clone, Debug)] + pub enum Algorithm { + Signature( + ::buffa::EnumValue, + ), + KeyAgreement( + ::buffa::EnumValue, + ), + Kem(::buffa::EnumValue), + } +} +pub mod cose_operation_request { + #[allow(unused_imports)] + use super::*; + #[derive(Clone, Debug)] + pub enum Operation<'a> { + Sign1Create( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseSign1CreateRequestView<'a>, + >, + ), + Sign1CreateDetached( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseSign1CreateDetachedRequestView< + 'a, + >, + >, + ), + Sign1Verify( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseSign1VerifyRequestView<'a>, + >, + ), + Sign1VerifyDetached( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseSign1VerifyDetachedRequestView< + 'a, + >, + >, + ), + KeyFromPublicBytes( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseKeyFromPublicBytesRequestView< + 'a, + >, + >, + ), + KeyFromPrivateBytes( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseKeyFromPrivateBytesRequestView< + 'a, + >, + >, + ), + KeyParse( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseKeyBytesRequestView<'a>, + >, + ), + KeyToPublicBytes( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseKeyBytesRequestView<'a>, + >, + ), + KeyToPrivateBytes( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseKeyBytesRequestView<'a>, + >, + ), + KeyDerivePublicKid( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseKeyBytesRequestView<'a>, + >, + ), + KeyToMultikey( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseKeyBytesRequestView<'a>, + >, + ), + MultikeyToCoseKey( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseMultikeyToCoseKeyRequestView< + 'a, + >, + >, + ), + MlKemEncryptDirect( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseMlKemEncryptRequestView< + 'a, + >, + >, + ), + MlKemEncryptKeyWrap( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseMlKemEncryptRequestView< + 'a, + >, + >, + ), + MlKemDecrypt( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseMlKemDecryptRequestView< + 'a, + >, + >, + ), + } +} +pub mod cose_operation_response_v2 { + #[allow(unused_imports)] + use super::*; + #[derive(Clone, Debug)] + pub enum Outcome<'a> { + Result( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseOperationResultView<'a>, + >, + ), + Error( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseErrorView<'a>, + >, + ), + } +} +pub mod cose_operation_result { + #[allow(unused_imports)] + use super::*; + #[derive(Clone, Debug)] + pub enum Result<'a> { + Sign1Create( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseSign1CreateResultView<'a>, + >, + ), + Sign1CreateDetached( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseSign1CreateResultView<'a>, + >, + ), + Sign1Verify( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseSign1VerifyResultView<'a>, + >, + ), + Sign1VerifyDetached( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseSign1VerifyResultView<'a>, + >, + ), + KeyFromPublicBytes( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseKeyBytesResultView<'a>, + >, + ), + KeyFromPrivateBytes( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseKeyBytesResultView<'a>, + >, + ), + KeyParse( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseKeyBytesResultView<'a>, + >, + ), + KeyToPublicBytes( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseKeyBytesResultView<'a>, + >, + ), + KeyToPrivateBytes( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseKeyBytesResultView<'a>, + >, + ), + KeyDerivePublicKid( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseKeyBytesResultView<'a>, + >, + ), + KeyToMultikey( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseMultikeyResultView<'a>, + >, + ), + MultikeyToCoseKey( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseKeyBytesResultView<'a>, + >, + ), + MlKemEncryptDirect( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseMlKemEncryptResultView<'a>, + >, + ), + MlKemEncryptKeyWrap( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseMlKemEncryptResultView<'a>, + >, + ), + MlKemDecrypt( + ::buffa::alloc::boxed::Box< + super::super::super::super::__buffa::view::CoseMlKemDecryptResultView<'a>, + >, + ), + } +} diff --git a/crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs b/crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs new file mode 100644 index 0000000..b020424 --- /dev/null +++ b/crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs @@ -0,0 +1,9820 @@ +// @generated by buffa-codegen. DO NOT EDIT. +// source: reallyme/cose/v1/cose.proto + +fn __reallyme_zeroize_unknown_fields(fields: &mut ::buffa::UnknownFields) { + for mut field in ::core::mem::take(fields) { + __reallyme_zeroize_unknown_field_data(&mut field.data); + } +} + +fn __reallyme_zeroize_unknown_field_data(data: &mut ::buffa::UnknownFieldData) { + match data { + ::buffa::UnknownFieldData::LengthDelimited(bytes) => { + ::zeroize::Zeroize::zeroize(bytes); + } + ::buffa::UnknownFieldData::Group(fields) => { + __reallyme_zeroize_unknown_fields(fields); + } + ::buffa::UnknownFieldData::Varint(_) + | ::buffa::UnknownFieldData::Fixed64(_) + | ::buffa::UnknownFieldData::Fixed32(_) => {} + } +} + +/// Family-scoped protobuf selectors intentionally match reallyme/crypto's +/// public algorithm numbers. They are not IANA or ReallyMe private-use COSE +/// algorithm identifiers. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[repr(i32)] +pub enum CoseSignatureAlgorithm { + COSE_SIGNATURE_ALGORITHM_UNSPECIFIED = 0i32, + /// EdDSA: 100-199; elliptic-curve signatures: 200-299; + /// post-quantum signatures: 1000-1099. + COSE_SIGNATURE_ALGORITHM_ED25519 = 100i32, + COSE_SIGNATURE_ALGORITHM_ECDSA_P256_SHA256 = 200i32, + COSE_SIGNATURE_ALGORITHM_ECDSA_P384_SHA384 = 210i32, + COSE_SIGNATURE_ALGORITHM_ECDSA_P521_SHA512 = 220i32, + COSE_SIGNATURE_ALGORITHM_ECDSA_SECP256K1_SHA256 = 230i32, + COSE_SIGNATURE_ALGORITHM_ML_DSA_44 = 1000i32, + COSE_SIGNATURE_ALGORITHM_ML_DSA_65 = 1010i32, + COSE_SIGNATURE_ALGORITHM_ML_DSA_87 = 1020i32, +} +impl CoseSignatureAlgorithm { + ///Idiomatic alias for [`Self::COSE_SIGNATURE_ALGORITHM_UNSPECIFIED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Unspecified: Self = Self::COSE_SIGNATURE_ALGORITHM_UNSPECIFIED; + ///Idiomatic alias for [`Self::COSE_SIGNATURE_ALGORITHM_ED25519`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Ed25519: Self = Self::COSE_SIGNATURE_ALGORITHM_ED25519; + ///Idiomatic alias for [`Self::COSE_SIGNATURE_ALGORITHM_ECDSA_P256_SHA256`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EcdsaP256Sha256: Self = Self::COSE_SIGNATURE_ALGORITHM_ECDSA_P256_SHA256; + ///Idiomatic alias for [`Self::COSE_SIGNATURE_ALGORITHM_ECDSA_P384_SHA384`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EcdsaP384Sha384: Self = Self::COSE_SIGNATURE_ALGORITHM_ECDSA_P384_SHA384; + ///Idiomatic alias for [`Self::COSE_SIGNATURE_ALGORITHM_ECDSA_P521_SHA512`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EcdsaP521Sha512: Self = Self::COSE_SIGNATURE_ALGORITHM_ECDSA_P521_SHA512; + ///Idiomatic alias for [`Self::COSE_SIGNATURE_ALGORITHM_ECDSA_SECP256K1_SHA256`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EcdsaSecp256k1Sha256: Self = Self::COSE_SIGNATURE_ALGORITHM_ECDSA_SECP256K1_SHA256; + ///Idiomatic alias for [`Self::COSE_SIGNATURE_ALGORITHM_ML_DSA_44`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const MlDsa44: Self = Self::COSE_SIGNATURE_ALGORITHM_ML_DSA_44; + ///Idiomatic alias for [`Self::COSE_SIGNATURE_ALGORITHM_ML_DSA_65`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const MlDsa65: Self = Self::COSE_SIGNATURE_ALGORITHM_ML_DSA_65; + ///Idiomatic alias for [`Self::COSE_SIGNATURE_ALGORITHM_ML_DSA_87`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const MlDsa87: Self = Self::COSE_SIGNATURE_ALGORITHM_ML_DSA_87; +} +impl ::core::default::Default for CoseSignatureAlgorithm { + fn default() -> Self { + Self::COSE_SIGNATURE_ALGORITHM_UNSPECIFIED + } +} +impl ::serde::Serialize for CoseSignatureAlgorithm { + fn serialize( + &self, + s: S, + ) -> ::core::result::Result { + s.serialize_str(::buffa::Enumeration::proto_name(self)) + } +} +impl<'de> ::serde::Deserialize<'de> for CoseSignatureAlgorithm { + fn deserialize>( + d: D, + ) -> ::core::result::Result { + struct _V; + impl ::serde::de::Visitor<'_> for _V { + type Value = CoseSignatureAlgorithm; + fn expecting( + &self, + f: &mut ::core::fmt::Formatter<'_>, + ) -> ::core::fmt::Result { + f.write_str( + concat!( + "a string, integer, or null for ", + stringify!(CoseSignatureAlgorithm) + ), + ) + } + fn visit_str( + self, + v: &str, + ) -> ::core::result::Result { + ::from_proto_name(v) + .ok_or_else(|| { ::serde::de::Error::unknown_variant(v, &[]) }) + } + fn visit_i64( + self, + v: i64, + ) -> ::core::result::Result { + let v32 = i32::try_from(v) + .map_err(|_| { + ::serde::de::Error::custom("enum value out of i32 range") + })?; + ::from_i32(v32) + .ok_or_else(|| { + ::serde::de::Error::custom("unknown enum value") + }) + } + fn visit_u64( + self, + v: u64, + ) -> ::core::result::Result { + let v32 = i32::try_from(v) + .map_err(|_| { + ::serde::de::Error::custom("enum value out of i32 range") + })?; + ::from_i32(v32) + .ok_or_else(|| { + ::serde::de::Error::custom("unknown enum value") + }) + } + fn visit_unit( + self, + ) -> ::core::result::Result { + ::core::result::Result::Ok(::core::default::Default::default()) + } + } + d.deserialize_any(_V) + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseSignatureAlgorithm { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +impl ::buffa::Enumeration for CoseSignatureAlgorithm { + fn from_i32(value: i32) -> ::core::option::Option { + match value { + 0i32 => { + ::core::option::Option::Some(Self::COSE_SIGNATURE_ALGORITHM_UNSPECIFIED) + } + 100i32 => { + ::core::option::Option::Some(Self::COSE_SIGNATURE_ALGORITHM_ED25519) + } + 200i32 => { + ::core::option::Option::Some( + Self::COSE_SIGNATURE_ALGORITHM_ECDSA_P256_SHA256, + ) + } + 210i32 => { + ::core::option::Option::Some( + Self::COSE_SIGNATURE_ALGORITHM_ECDSA_P384_SHA384, + ) + } + 220i32 => { + ::core::option::Option::Some( + Self::COSE_SIGNATURE_ALGORITHM_ECDSA_P521_SHA512, + ) + } + 230i32 => { + ::core::option::Option::Some( + Self::COSE_SIGNATURE_ALGORITHM_ECDSA_SECP256K1_SHA256, + ) + } + 1000i32 => { + ::core::option::Option::Some(Self::COSE_SIGNATURE_ALGORITHM_ML_DSA_44) + } + 1010i32 => { + ::core::option::Option::Some(Self::COSE_SIGNATURE_ALGORITHM_ML_DSA_65) + } + 1020i32 => { + ::core::option::Option::Some(Self::COSE_SIGNATURE_ALGORITHM_ML_DSA_87) + } + _ => ::core::option::Option::None, + } + } + fn to_i32(&self) -> i32 { + *self as i32 + } + fn proto_name(&self) -> &'static str { + match self { + Self::COSE_SIGNATURE_ALGORITHM_UNSPECIFIED => { + "COSE_SIGNATURE_ALGORITHM_UNSPECIFIED" + } + Self::COSE_SIGNATURE_ALGORITHM_ED25519 => "COSE_SIGNATURE_ALGORITHM_ED25519", + Self::COSE_SIGNATURE_ALGORITHM_ECDSA_P256_SHA256 => { + "COSE_SIGNATURE_ALGORITHM_ECDSA_P256_SHA256" + } + Self::COSE_SIGNATURE_ALGORITHM_ECDSA_P384_SHA384 => { + "COSE_SIGNATURE_ALGORITHM_ECDSA_P384_SHA384" + } + Self::COSE_SIGNATURE_ALGORITHM_ECDSA_P521_SHA512 => { + "COSE_SIGNATURE_ALGORITHM_ECDSA_P521_SHA512" + } + Self::COSE_SIGNATURE_ALGORITHM_ECDSA_SECP256K1_SHA256 => { + "COSE_SIGNATURE_ALGORITHM_ECDSA_SECP256K1_SHA256" + } + Self::COSE_SIGNATURE_ALGORITHM_ML_DSA_44 => { + "COSE_SIGNATURE_ALGORITHM_ML_DSA_44" + } + Self::COSE_SIGNATURE_ALGORITHM_ML_DSA_65 => { + "COSE_SIGNATURE_ALGORITHM_ML_DSA_65" + } + Self::COSE_SIGNATURE_ALGORITHM_ML_DSA_87 => { + "COSE_SIGNATURE_ALGORITHM_ML_DSA_87" + } + } + } + fn from_proto_name(name: &str) -> ::core::option::Option { + match name { + "COSE_SIGNATURE_ALGORITHM_UNSPECIFIED" => { + ::core::option::Option::Some(Self::COSE_SIGNATURE_ALGORITHM_UNSPECIFIED) + } + "COSE_SIGNATURE_ALGORITHM_ED25519" => { + ::core::option::Option::Some(Self::COSE_SIGNATURE_ALGORITHM_ED25519) + } + "COSE_SIGNATURE_ALGORITHM_ECDSA_P256_SHA256" => { + ::core::option::Option::Some( + Self::COSE_SIGNATURE_ALGORITHM_ECDSA_P256_SHA256, + ) + } + "COSE_SIGNATURE_ALGORITHM_ECDSA_P384_SHA384" => { + ::core::option::Option::Some( + Self::COSE_SIGNATURE_ALGORITHM_ECDSA_P384_SHA384, + ) + } + "COSE_SIGNATURE_ALGORITHM_ECDSA_P521_SHA512" => { + ::core::option::Option::Some( + Self::COSE_SIGNATURE_ALGORITHM_ECDSA_P521_SHA512, + ) + } + "COSE_SIGNATURE_ALGORITHM_ECDSA_SECP256K1_SHA256" => { + ::core::option::Option::Some( + Self::COSE_SIGNATURE_ALGORITHM_ECDSA_SECP256K1_SHA256, + ) + } + "COSE_SIGNATURE_ALGORITHM_ML_DSA_44" => { + ::core::option::Option::Some(Self::COSE_SIGNATURE_ALGORITHM_ML_DSA_44) + } + "COSE_SIGNATURE_ALGORITHM_ML_DSA_65" => { + ::core::option::Option::Some(Self::COSE_SIGNATURE_ALGORITHM_ML_DSA_65) + } + "COSE_SIGNATURE_ALGORITHM_ML_DSA_87" => { + ::core::option::Option::Some(Self::COSE_SIGNATURE_ALGORITHM_ML_DSA_87) + } + _ => ::core::option::Option::None, + } + } + fn values() -> &'static [Self] { + &[ + Self::COSE_SIGNATURE_ALGORITHM_UNSPECIFIED, + Self::COSE_SIGNATURE_ALGORITHM_ED25519, + Self::COSE_SIGNATURE_ALGORITHM_ECDSA_P256_SHA256, + Self::COSE_SIGNATURE_ALGORITHM_ECDSA_P384_SHA384, + Self::COSE_SIGNATURE_ALGORITHM_ECDSA_P521_SHA512, + Self::COSE_SIGNATURE_ALGORITHM_ECDSA_SECP256K1_SHA256, + Self::COSE_SIGNATURE_ALGORITHM_ML_DSA_44, + Self::COSE_SIGNATURE_ALGORITHM_ML_DSA_65, + Self::COSE_SIGNATURE_ALGORITHM_ML_DSA_87, + ] + } +} +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[repr(i32)] +pub enum CoseKeyAgreementAlgorithm { + COSE_KEY_AGREEMENT_ALGORITHM_UNSPECIFIED = 0i32, + /// Montgomery-form curves: 100-199. + COSE_KEY_AGREEMENT_ALGORITHM_X25519 = 100i32, +} +impl CoseKeyAgreementAlgorithm { + ///Idiomatic alias for [`Self::COSE_KEY_AGREEMENT_ALGORITHM_UNSPECIFIED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Unspecified: Self = Self::COSE_KEY_AGREEMENT_ALGORITHM_UNSPECIFIED; + ///Idiomatic alias for [`Self::COSE_KEY_AGREEMENT_ALGORITHM_X25519`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const X25519: Self = Self::COSE_KEY_AGREEMENT_ALGORITHM_X25519; +} +impl ::core::default::Default for CoseKeyAgreementAlgorithm { + fn default() -> Self { + Self::COSE_KEY_AGREEMENT_ALGORITHM_UNSPECIFIED + } +} +impl ::serde::Serialize for CoseKeyAgreementAlgorithm { + fn serialize( + &self, + s: S, + ) -> ::core::result::Result { + s.serialize_str(::buffa::Enumeration::proto_name(self)) + } +} +impl<'de> ::serde::Deserialize<'de> for CoseKeyAgreementAlgorithm { + fn deserialize>( + d: D, + ) -> ::core::result::Result { + struct _V; + impl ::serde::de::Visitor<'_> for _V { + type Value = CoseKeyAgreementAlgorithm; + fn expecting( + &self, + f: &mut ::core::fmt::Formatter<'_>, + ) -> ::core::fmt::Result { + f.write_str( + concat!( + "a string, integer, or null for ", + stringify!(CoseKeyAgreementAlgorithm) + ), + ) + } + fn visit_str( + self, + v: &str, + ) -> ::core::result::Result { + ::from_proto_name(v) + .ok_or_else(|| { ::serde::de::Error::unknown_variant(v, &[]) }) + } + fn visit_i64( + self, + v: i64, + ) -> ::core::result::Result { + let v32 = i32::try_from(v) + .map_err(|_| { + ::serde::de::Error::custom("enum value out of i32 range") + })?; + ::from_i32(v32) + .ok_or_else(|| { + ::serde::de::Error::custom("unknown enum value") + }) + } + fn visit_u64( + self, + v: u64, + ) -> ::core::result::Result { + let v32 = i32::try_from(v) + .map_err(|_| { + ::serde::de::Error::custom("enum value out of i32 range") + })?; + ::from_i32(v32) + .ok_or_else(|| { + ::serde::de::Error::custom("unknown enum value") + }) + } + fn visit_unit( + self, + ) -> ::core::result::Result { + ::core::result::Result::Ok(::core::default::Default::default()) + } + } + d.deserialize_any(_V) + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseKeyAgreementAlgorithm { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +impl ::buffa::Enumeration for CoseKeyAgreementAlgorithm { + fn from_i32(value: i32) -> ::core::option::Option { + match value { + 0i32 => { + ::core::option::Option::Some( + Self::COSE_KEY_AGREEMENT_ALGORITHM_UNSPECIFIED, + ) + } + 100i32 => { + ::core::option::Option::Some(Self::COSE_KEY_AGREEMENT_ALGORITHM_X25519) + } + _ => ::core::option::Option::None, + } + } + fn to_i32(&self) -> i32 { + *self as i32 + } + fn proto_name(&self) -> &'static str { + match self { + Self::COSE_KEY_AGREEMENT_ALGORITHM_UNSPECIFIED => { + "COSE_KEY_AGREEMENT_ALGORITHM_UNSPECIFIED" + } + Self::COSE_KEY_AGREEMENT_ALGORITHM_X25519 => { + "COSE_KEY_AGREEMENT_ALGORITHM_X25519" + } + } + } + fn from_proto_name(name: &str) -> ::core::option::Option { + match name { + "COSE_KEY_AGREEMENT_ALGORITHM_UNSPECIFIED" => { + ::core::option::Option::Some( + Self::COSE_KEY_AGREEMENT_ALGORITHM_UNSPECIFIED, + ) + } + "COSE_KEY_AGREEMENT_ALGORITHM_X25519" => { + ::core::option::Option::Some(Self::COSE_KEY_AGREEMENT_ALGORITHM_X25519) + } + _ => ::core::option::Option::None, + } + } + fn values() -> &'static [Self] { + &[ + Self::COSE_KEY_AGREEMENT_ALGORITHM_UNSPECIFIED, + Self::COSE_KEY_AGREEMENT_ALGORITHM_X25519, + ] + } +} +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[repr(i32)] +pub enum CoseKemAlgorithm { + COSE_KEM_ALGORITHM_UNSPECIFIED = 0i32, + /// Post-quantum KEMs: 1000-1099; hybrid KEMs: 1100-1199. + COSE_KEM_ALGORITHM_ML_KEM_512 = 1000i32, + COSE_KEM_ALGORITHM_ML_KEM_768 = 1010i32, + COSE_KEM_ALGORITHM_ML_KEM_1024 = 1020i32, + COSE_KEM_ALGORITHM_X_WING_768 = 1100i32, +} +impl CoseKemAlgorithm { + ///Idiomatic alias for [`Self::COSE_KEM_ALGORITHM_UNSPECIFIED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Unspecified: Self = Self::COSE_KEM_ALGORITHM_UNSPECIFIED; + ///Idiomatic alias for [`Self::COSE_KEM_ALGORITHM_ML_KEM_512`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const MlKem512: Self = Self::COSE_KEM_ALGORITHM_ML_KEM_512; + ///Idiomatic alias for [`Self::COSE_KEM_ALGORITHM_ML_KEM_768`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const MlKem768: Self = Self::COSE_KEM_ALGORITHM_ML_KEM_768; + ///Idiomatic alias for [`Self::COSE_KEM_ALGORITHM_ML_KEM_1024`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const MlKem1024: Self = Self::COSE_KEM_ALGORITHM_ML_KEM_1024; + ///Idiomatic alias for [`Self::COSE_KEM_ALGORITHM_X_WING_768`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const XWing768: Self = Self::COSE_KEM_ALGORITHM_X_WING_768; +} +impl ::core::default::Default for CoseKemAlgorithm { + fn default() -> Self { + Self::COSE_KEM_ALGORITHM_UNSPECIFIED + } +} +impl ::serde::Serialize for CoseKemAlgorithm { + fn serialize( + &self, + s: S, + ) -> ::core::result::Result { + s.serialize_str(::buffa::Enumeration::proto_name(self)) + } +} +impl<'de> ::serde::Deserialize<'de> for CoseKemAlgorithm { + fn deserialize>( + d: D, + ) -> ::core::result::Result { + struct _V; + impl ::serde::de::Visitor<'_> for _V { + type Value = CoseKemAlgorithm; + fn expecting( + &self, + f: &mut ::core::fmt::Formatter<'_>, + ) -> ::core::fmt::Result { + f.write_str( + concat!( + "a string, integer, or null for ", stringify!(CoseKemAlgorithm) + ), + ) + } + fn visit_str( + self, + v: &str, + ) -> ::core::result::Result { + ::from_proto_name(v) + .ok_or_else(|| { ::serde::de::Error::unknown_variant(v, &[]) }) + } + fn visit_i64( + self, + v: i64, + ) -> ::core::result::Result { + let v32 = i32::try_from(v) + .map_err(|_| { + ::serde::de::Error::custom("enum value out of i32 range") + })?; + ::from_i32(v32) + .ok_or_else(|| { + ::serde::de::Error::custom("unknown enum value") + }) + } + fn visit_u64( + self, + v: u64, + ) -> ::core::result::Result { + let v32 = i32::try_from(v) + .map_err(|_| { + ::serde::de::Error::custom("enum value out of i32 range") + })?; + ::from_i32(v32) + .ok_or_else(|| { + ::serde::de::Error::custom("unknown enum value") + }) + } + fn visit_unit( + self, + ) -> ::core::result::Result { + ::core::result::Result::Ok(::core::default::Default::default()) + } + } + d.deserialize_any(_V) + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseKemAlgorithm { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +impl ::buffa::Enumeration for CoseKemAlgorithm { + fn from_i32(value: i32) -> ::core::option::Option { + match value { + 0i32 => ::core::option::Option::Some(Self::COSE_KEM_ALGORITHM_UNSPECIFIED), + 1000i32 => ::core::option::Option::Some(Self::COSE_KEM_ALGORITHM_ML_KEM_512), + 1010i32 => ::core::option::Option::Some(Self::COSE_KEM_ALGORITHM_ML_KEM_768), + 1020i32 => ::core::option::Option::Some(Self::COSE_KEM_ALGORITHM_ML_KEM_1024), + 1100i32 => ::core::option::Option::Some(Self::COSE_KEM_ALGORITHM_X_WING_768), + _ => ::core::option::Option::None, + } + } + fn to_i32(&self) -> i32 { + *self as i32 + } + fn proto_name(&self) -> &'static str { + match self { + Self::COSE_KEM_ALGORITHM_UNSPECIFIED => "COSE_KEM_ALGORITHM_UNSPECIFIED", + Self::COSE_KEM_ALGORITHM_ML_KEM_512 => "COSE_KEM_ALGORITHM_ML_KEM_512", + Self::COSE_KEM_ALGORITHM_ML_KEM_768 => "COSE_KEM_ALGORITHM_ML_KEM_768", + Self::COSE_KEM_ALGORITHM_ML_KEM_1024 => "COSE_KEM_ALGORITHM_ML_KEM_1024", + Self::COSE_KEM_ALGORITHM_X_WING_768 => "COSE_KEM_ALGORITHM_X_WING_768", + } + } + fn from_proto_name(name: &str) -> ::core::option::Option { + match name { + "COSE_KEM_ALGORITHM_UNSPECIFIED" => { + ::core::option::Option::Some(Self::COSE_KEM_ALGORITHM_UNSPECIFIED) + } + "COSE_KEM_ALGORITHM_ML_KEM_512" => { + ::core::option::Option::Some(Self::COSE_KEM_ALGORITHM_ML_KEM_512) + } + "COSE_KEM_ALGORITHM_ML_KEM_768" => { + ::core::option::Option::Some(Self::COSE_KEM_ALGORITHM_ML_KEM_768) + } + "COSE_KEM_ALGORITHM_ML_KEM_1024" => { + ::core::option::Option::Some(Self::COSE_KEM_ALGORITHM_ML_KEM_1024) + } + "COSE_KEM_ALGORITHM_X_WING_768" => { + ::core::option::Option::Some(Self::COSE_KEM_ALGORITHM_X_WING_768) + } + _ => ::core::option::Option::None, + } + } + fn values() -> &'static [Self] { + &[ + Self::COSE_KEM_ALGORITHM_UNSPECIFIED, + Self::COSE_KEM_ALGORITHM_ML_KEM_512, + Self::COSE_KEM_ALGORITHM_ML_KEM_768, + Self::COSE_KEM_ALGORITHM_ML_KEM_1024, + Self::COSE_KEM_ALGORITHM_X_WING_768, + ] + } +} +/// CoseContentEncryptionAlgorithm identifies the protected COSE_Encrypt content +/// algorithm. These enum values are protobuf identifiers, not COSE algorithm +/// registry values. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[repr(i32)] +pub enum CoseContentEncryptionAlgorithm { + COSE_CONTENT_ENCRYPTION_ALGORITHM_UNSPECIFIED = 0i32, + COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_128_GCM = 100i32, + COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_192_GCM = 110i32, + COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_256_GCM = 120i32, +} +impl CoseContentEncryptionAlgorithm { + ///Idiomatic alias for [`Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_UNSPECIFIED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Unspecified: Self = Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_UNSPECIFIED; + ///Idiomatic alias for [`Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_128_GCM`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Aes128Gcm: Self = Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_128_GCM; + ///Idiomatic alias for [`Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_192_GCM`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Aes192Gcm: Self = Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_192_GCM; + ///Idiomatic alias for [`Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_256_GCM`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Aes256Gcm: Self = Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_256_GCM; +} +impl ::core::default::Default for CoseContentEncryptionAlgorithm { + fn default() -> Self { + Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_UNSPECIFIED + } +} +impl ::serde::Serialize for CoseContentEncryptionAlgorithm { + fn serialize( + &self, + s: S, + ) -> ::core::result::Result { + s.serialize_str(::buffa::Enumeration::proto_name(self)) + } +} +impl<'de> ::serde::Deserialize<'de> for CoseContentEncryptionAlgorithm { + fn deserialize>( + d: D, + ) -> ::core::result::Result { + struct _V; + impl ::serde::de::Visitor<'_> for _V { + type Value = CoseContentEncryptionAlgorithm; + fn expecting( + &self, + f: &mut ::core::fmt::Formatter<'_>, + ) -> ::core::fmt::Result { + f.write_str( + concat!( + "a string, integer, or null for ", + stringify!(CoseContentEncryptionAlgorithm) + ), + ) + } + fn visit_str( + self, + v: &str, + ) -> ::core::result::Result { + ::from_proto_name( + v, + ) + .ok_or_else(|| { ::serde::de::Error::unknown_variant(v, &[]) }) + } + fn visit_i64( + self, + v: i64, + ) -> ::core::result::Result { + let v32 = i32::try_from(v) + .map_err(|_| { + ::serde::de::Error::custom("enum value out of i32 range") + })?; + ::from_i32(v32) + .ok_or_else(|| { + ::serde::de::Error::custom("unknown enum value") + }) + } + fn visit_u64( + self, + v: u64, + ) -> ::core::result::Result { + let v32 = i32::try_from(v) + .map_err(|_| { + ::serde::de::Error::custom("enum value out of i32 range") + })?; + ::from_i32(v32) + .ok_or_else(|| { + ::serde::de::Error::custom("unknown enum value") + }) + } + fn visit_unit( + self, + ) -> ::core::result::Result { + ::core::result::Result::Ok(::core::default::Default::default()) + } + } + d.deserialize_any(_V) + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseContentEncryptionAlgorithm { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +impl ::buffa::Enumeration for CoseContentEncryptionAlgorithm { + fn from_i32(value: i32) -> ::core::option::Option { + match value { + 0i32 => { + ::core::option::Option::Some( + Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_UNSPECIFIED, + ) + } + 100i32 => { + ::core::option::Option::Some( + Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_128_GCM, + ) + } + 110i32 => { + ::core::option::Option::Some( + Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_192_GCM, + ) + } + 120i32 => { + ::core::option::Option::Some( + Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_256_GCM, + ) + } + _ => ::core::option::Option::None, + } + } + fn to_i32(&self) -> i32 { + *self as i32 + } + fn proto_name(&self) -> &'static str { + match self { + Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_UNSPECIFIED => { + "COSE_CONTENT_ENCRYPTION_ALGORITHM_UNSPECIFIED" + } + Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_128_GCM => { + "COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_128_GCM" + } + Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_192_GCM => { + "COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_192_GCM" + } + Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_256_GCM => { + "COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_256_GCM" + } + } + } + fn from_proto_name(name: &str) -> ::core::option::Option { + match name { + "COSE_CONTENT_ENCRYPTION_ALGORITHM_UNSPECIFIED" => { + ::core::option::Option::Some( + Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_UNSPECIFIED, + ) + } + "COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_128_GCM" => { + ::core::option::Option::Some( + Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_128_GCM, + ) + } + "COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_192_GCM" => { + ::core::option::Option::Some( + Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_192_GCM, + ) + } + "COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_256_GCM" => { + ::core::option::Option::Some( + Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_256_GCM, + ) + } + _ => ::core::option::Option::None, + } + } + fn values() -> &'static [Self] { + &[ + Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_UNSPECIFIED, + Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_128_GCM, + Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_192_GCM, + Self::COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_256_GCM, + ] + } +} +/// CoseMlKemMode reports which COSE_Recipient construction was authenticated. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[repr(i32)] +pub enum CoseMlKemMode { + COSE_ML_KEM_MODE_UNSPECIFIED = 0i32, + COSE_ML_KEM_MODE_DIRECT = 1i32, + COSE_ML_KEM_MODE_KEY_WRAP = 2i32, +} +impl CoseMlKemMode { + ///Idiomatic alias for [`Self::COSE_ML_KEM_MODE_UNSPECIFIED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Unspecified: Self = Self::COSE_ML_KEM_MODE_UNSPECIFIED; + ///Idiomatic alias for [`Self::COSE_ML_KEM_MODE_DIRECT`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Direct: Self = Self::COSE_ML_KEM_MODE_DIRECT; + ///Idiomatic alias for [`Self::COSE_ML_KEM_MODE_KEY_WRAP`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const KeyWrap: Self = Self::COSE_ML_KEM_MODE_KEY_WRAP; +} +impl ::core::default::Default for CoseMlKemMode { + fn default() -> Self { + Self::COSE_ML_KEM_MODE_UNSPECIFIED + } +} +impl ::serde::Serialize for CoseMlKemMode { + fn serialize( + &self, + s: S, + ) -> ::core::result::Result { + s.serialize_str(::buffa::Enumeration::proto_name(self)) + } +} +impl<'de> ::serde::Deserialize<'de> for CoseMlKemMode { + fn deserialize>( + d: D, + ) -> ::core::result::Result { + struct _V; + impl ::serde::de::Visitor<'_> for _V { + type Value = CoseMlKemMode; + fn expecting( + &self, + f: &mut ::core::fmt::Formatter<'_>, + ) -> ::core::fmt::Result { + f.write_str( + concat!("a string, integer, or null for ", stringify!(CoseMlKemMode)), + ) + } + fn visit_str( + self, + v: &str, + ) -> ::core::result::Result { + ::from_proto_name(v) + .ok_or_else(|| { ::serde::de::Error::unknown_variant(v, &[]) }) + } + fn visit_i64( + self, + v: i64, + ) -> ::core::result::Result { + let v32 = i32::try_from(v) + .map_err(|_| { + ::serde::de::Error::custom("enum value out of i32 range") + })?; + ::from_i32(v32) + .ok_or_else(|| { + ::serde::de::Error::custom("unknown enum value") + }) + } + fn visit_u64( + self, + v: u64, + ) -> ::core::result::Result { + let v32 = i32::try_from(v) + .map_err(|_| { + ::serde::de::Error::custom("enum value out of i32 range") + })?; + ::from_i32(v32) + .ok_or_else(|| { + ::serde::de::Error::custom("unknown enum value") + }) + } + fn visit_unit( + self, + ) -> ::core::result::Result { + ::core::result::Result::Ok(::core::default::Default::default()) + } + } + d.deserialize_any(_V) + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseMlKemMode { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +impl ::buffa::Enumeration for CoseMlKemMode { + fn from_i32(value: i32) -> ::core::option::Option { + match value { + 0i32 => ::core::option::Option::Some(Self::COSE_ML_KEM_MODE_UNSPECIFIED), + 1i32 => ::core::option::Option::Some(Self::COSE_ML_KEM_MODE_DIRECT), + 2i32 => ::core::option::Option::Some(Self::COSE_ML_KEM_MODE_KEY_WRAP), + _ => ::core::option::Option::None, + } + } + fn to_i32(&self) -> i32 { + *self as i32 + } + fn proto_name(&self) -> &'static str { + match self { + Self::COSE_ML_KEM_MODE_UNSPECIFIED => "COSE_ML_KEM_MODE_UNSPECIFIED", + Self::COSE_ML_KEM_MODE_DIRECT => "COSE_ML_KEM_MODE_DIRECT", + Self::COSE_ML_KEM_MODE_KEY_WRAP => "COSE_ML_KEM_MODE_KEY_WRAP", + } + } + fn from_proto_name(name: &str) -> ::core::option::Option { + match name { + "COSE_ML_KEM_MODE_UNSPECIFIED" => { + ::core::option::Option::Some(Self::COSE_ML_KEM_MODE_UNSPECIFIED) + } + "COSE_ML_KEM_MODE_DIRECT" => { + ::core::option::Option::Some(Self::COSE_ML_KEM_MODE_DIRECT) + } + "COSE_ML_KEM_MODE_KEY_WRAP" => { + ::core::option::Option::Some(Self::COSE_ML_KEM_MODE_KEY_WRAP) + } + _ => ::core::option::Option::None, + } + } + fn values() -> &'static [Self] { + &[ + Self::COSE_ML_KEM_MODE_UNSPECIFIED, + Self::COSE_ML_KEM_MODE_DIRECT, + Self::COSE_ML_KEM_MODE_KEY_WRAP, + ] + } +} +/// CoseErrorReason is the component-owned reason-code enum for reallyme/cose. +/// Values are stable public boundary codes; internal Rust errors must map into +/// one of these before crossing RPC, SDK, FFI, storage, audit, or telemetry +/// boundaries. Numeric bands keep evolving failure families adjacent without +/// forcing unrelated public wire values to move. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[repr(i32)] +pub enum CoseErrorReason { + COSE_ERROR_REASON_UNSPECIFIED = 0i32, + /// Common COSE binary and CBOR boundary failures: 100-199. + COSE_ERROR_REASON_COMMON_CBOR = 100i32, + COSE_ERROR_REASON_COMMON_INVALID_FORMAT = 101i32, + COSE_ERROR_REASON_COMMON_RESOURCE_LIMIT_EXCEEDED = 102i32, + COSE_ERROR_REASON_COMMON_NON_CANONICAL_CBOR = 103i32, + COSE_ERROR_REASON_COMMON_UNEXPECTED_CBOR_TAG = 104i32, + COSE_ERROR_REASON_COMMON_DUPLICATE_MAP_LABEL = 105i32, + COSE_ERROR_REASON_COMMON_MALFORMED_PROTOBUF = 120i32, + COSE_ERROR_REASON_COMMON_MALFORMED_JSON = 121i32, + COSE_ERROR_REASON_COMMON_INVALID_PARAMETER = 130i32, + COSE_ERROR_REASON_COMMON_INVALID_LENGTH = 131i32, + COSE_ERROR_REASON_COMMON_INVALID_ENCODING = 132i32, + /// Provider failures: 200-299. + COSE_ERROR_REASON_COMMON_UNSUPPORTED_ALGORITHM = 200i32, + COSE_ERROR_REASON_PROVIDER_UNAVAILABLE = 201i32, + /// Backend failures: 300-399. + COSE_ERROR_REASON_COMMON_CRYPTO_FAILED = 300i32, + COSE_ERROR_REASON_BACKEND_INTERNAL = 301i32, + /// COSE_Sign1 failures: 400-499. + COSE_ERROR_REASON_SIGN1_KID_KEY_MISMATCH = 400i32, + COSE_ERROR_REASON_SIGN1_MISSING_PAYLOAD = 401i32, + COSE_ERROR_REASON_SIGN1_MISSING_KID = 402i32, + COSE_ERROR_REASON_SIGN1_KEY_NOT_RESOLVED = 403i32, + COSE_ERROR_REASON_SIGN1_UNSUPPORTED_CRITICAL_HEADER = 410i32, + COSE_ERROR_REASON_SIGN1_UNPROTECTED_HEADER_NOT_ALLOWED = 411i32, + COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE = 420i32, + COSE_ERROR_REASON_SIGN1_MISSING_PRIVATE_KEY = 421i32, + COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE_ENCODING = 422i32, + /// COSE_Key material and key-shape failures: 500-599. + COSE_ERROR_REASON_KEY_MISSING_KEY_MATERIAL = 500i32, + COSE_ERROR_REASON_KEY_INVALID_KEY_MATERIAL = 501i32, + /// Multikey decoding and COSE_Key conversion failures: 600-699. + COSE_ERROR_REASON_MULTIKEY_INVALID_MULTIKEY = 600i32, + /// COSE_Encrypt and COSE_Recipient failures: 700-799. + COSE_ERROR_REASON_ENCRYPT_MISSING_CIPHERTEXT = 700i32, + COSE_ERROR_REASON_ENCRYPT_INVALID_IV = 701i32, + COSE_ERROR_REASON_ENCRYPT_INVALID_RECIPIENT = 702i32, + COSE_ERROR_REASON_ENCRYPT_MISSING_ENCAPSULATED_KEY = 703i32, + COSE_ERROR_REASON_ENCRYPT_INVALID_ENCAPSULATED_KEY = 704i32, + COSE_ERROR_REASON_ENCRYPT_AUTHENTICATION_FAILED = 720i32, + COSE_ERROR_REASON_ENCRYPT_KEY_UNWRAP_FAILED = 721i32, + COSE_ERROR_REASON_ENCRYPT_KID_MISMATCH = 730i32, + COSE_ERROR_REASON_ENCRYPT_MISSING_KID = 731i32, + COSE_ERROR_REASON_ENCRYPT_UNPROTECTED_HEADER_NOT_ALLOWED = 740i32, +} +impl CoseErrorReason { + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_UNSPECIFIED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Unspecified: Self = Self::COSE_ERROR_REASON_UNSPECIFIED; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_COMMON_CBOR`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const CommonCbor: Self = Self::COSE_ERROR_REASON_COMMON_CBOR; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_COMMON_INVALID_FORMAT`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const CommonInvalidFormat: Self = Self::COSE_ERROR_REASON_COMMON_INVALID_FORMAT; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_COMMON_RESOURCE_LIMIT_EXCEEDED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const CommonResourceLimitExceeded: Self = Self::COSE_ERROR_REASON_COMMON_RESOURCE_LIMIT_EXCEEDED; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_COMMON_NON_CANONICAL_CBOR`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const CommonNonCanonicalCbor: Self = Self::COSE_ERROR_REASON_COMMON_NON_CANONICAL_CBOR; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_COMMON_UNEXPECTED_CBOR_TAG`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const CommonUnexpectedCborTag: Self = Self::COSE_ERROR_REASON_COMMON_UNEXPECTED_CBOR_TAG; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_COMMON_DUPLICATE_MAP_LABEL`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const CommonDuplicateMapLabel: Self = Self::COSE_ERROR_REASON_COMMON_DUPLICATE_MAP_LABEL; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_COMMON_MALFORMED_PROTOBUF`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const CommonMalformedProtobuf: Self = Self::COSE_ERROR_REASON_COMMON_MALFORMED_PROTOBUF; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_COMMON_MALFORMED_JSON`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const CommonMalformedJson: Self = Self::COSE_ERROR_REASON_COMMON_MALFORMED_JSON; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_COMMON_INVALID_PARAMETER`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const CommonInvalidParameter: Self = Self::COSE_ERROR_REASON_COMMON_INVALID_PARAMETER; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_COMMON_INVALID_LENGTH`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const CommonInvalidLength: Self = Self::COSE_ERROR_REASON_COMMON_INVALID_LENGTH; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_COMMON_INVALID_ENCODING`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const CommonInvalidEncoding: Self = Self::COSE_ERROR_REASON_COMMON_INVALID_ENCODING; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_COMMON_UNSUPPORTED_ALGORITHM`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const CommonUnsupportedAlgorithm: Self = Self::COSE_ERROR_REASON_COMMON_UNSUPPORTED_ALGORITHM; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_PROVIDER_UNAVAILABLE`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const ProviderUnavailable: Self = Self::COSE_ERROR_REASON_PROVIDER_UNAVAILABLE; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_COMMON_CRYPTO_FAILED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const CommonCryptoFailed: Self = Self::COSE_ERROR_REASON_COMMON_CRYPTO_FAILED; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_BACKEND_INTERNAL`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const BackendInternal: Self = Self::COSE_ERROR_REASON_BACKEND_INTERNAL; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_SIGN1_KID_KEY_MISMATCH`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Sign1KidKeyMismatch: Self = Self::COSE_ERROR_REASON_SIGN1_KID_KEY_MISMATCH; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_SIGN1_MISSING_PAYLOAD`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Sign1MissingPayload: Self = Self::COSE_ERROR_REASON_SIGN1_MISSING_PAYLOAD; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_SIGN1_MISSING_KID`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Sign1MissingKid: Self = Self::COSE_ERROR_REASON_SIGN1_MISSING_KID; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_SIGN1_KEY_NOT_RESOLVED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Sign1KeyNotResolved: Self = Self::COSE_ERROR_REASON_SIGN1_KEY_NOT_RESOLVED; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_SIGN1_UNSUPPORTED_CRITICAL_HEADER`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Sign1UnsupportedCriticalHeader: Self = Self::COSE_ERROR_REASON_SIGN1_UNSUPPORTED_CRITICAL_HEADER; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_SIGN1_UNPROTECTED_HEADER_NOT_ALLOWED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Sign1UnprotectedHeaderNotAllowed: Self = Self::COSE_ERROR_REASON_SIGN1_UNPROTECTED_HEADER_NOT_ALLOWED; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Sign1InvalidSignature: Self = Self::COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_SIGN1_MISSING_PRIVATE_KEY`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Sign1MissingPrivateKey: Self = Self::COSE_ERROR_REASON_SIGN1_MISSING_PRIVATE_KEY; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE_ENCODING`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Sign1InvalidSignatureEncoding: Self = Self::COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE_ENCODING; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_KEY_MISSING_KEY_MATERIAL`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const KeyMissingKeyMaterial: Self = Self::COSE_ERROR_REASON_KEY_MISSING_KEY_MATERIAL; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_KEY_INVALID_KEY_MATERIAL`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const KeyInvalidKeyMaterial: Self = Self::COSE_ERROR_REASON_KEY_INVALID_KEY_MATERIAL; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_MULTIKEY_INVALID_MULTIKEY`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const MultikeyInvalidMultikey: Self = Self::COSE_ERROR_REASON_MULTIKEY_INVALID_MULTIKEY; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_ENCRYPT_MISSING_CIPHERTEXT`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EncryptMissingCiphertext: Self = Self::COSE_ERROR_REASON_ENCRYPT_MISSING_CIPHERTEXT; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_ENCRYPT_INVALID_IV`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EncryptInvalidIv: Self = Self::COSE_ERROR_REASON_ENCRYPT_INVALID_IV; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_ENCRYPT_INVALID_RECIPIENT`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EncryptInvalidRecipient: Self = Self::COSE_ERROR_REASON_ENCRYPT_INVALID_RECIPIENT; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_ENCRYPT_MISSING_ENCAPSULATED_KEY`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EncryptMissingEncapsulatedKey: Self = Self::COSE_ERROR_REASON_ENCRYPT_MISSING_ENCAPSULATED_KEY; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_ENCRYPT_INVALID_ENCAPSULATED_KEY`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EncryptInvalidEncapsulatedKey: Self = Self::COSE_ERROR_REASON_ENCRYPT_INVALID_ENCAPSULATED_KEY; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_ENCRYPT_AUTHENTICATION_FAILED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EncryptAuthenticationFailed: Self = Self::COSE_ERROR_REASON_ENCRYPT_AUTHENTICATION_FAILED; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_ENCRYPT_KEY_UNWRAP_FAILED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EncryptKeyUnwrapFailed: Self = Self::COSE_ERROR_REASON_ENCRYPT_KEY_UNWRAP_FAILED; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_ENCRYPT_KID_MISMATCH`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EncryptKidMismatch: Self = Self::COSE_ERROR_REASON_ENCRYPT_KID_MISMATCH; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_ENCRYPT_MISSING_KID`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EncryptMissingKid: Self = Self::COSE_ERROR_REASON_ENCRYPT_MISSING_KID; + ///Idiomatic alias for [`Self::COSE_ERROR_REASON_ENCRYPT_UNPROTECTED_HEADER_NOT_ALLOWED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EncryptUnprotectedHeaderNotAllowed: Self = Self::COSE_ERROR_REASON_ENCRYPT_UNPROTECTED_HEADER_NOT_ALLOWED; +} +impl ::core::default::Default for CoseErrorReason { + fn default() -> Self { + Self::COSE_ERROR_REASON_UNSPECIFIED + } +} +impl ::serde::Serialize for CoseErrorReason { + fn serialize( + &self, + s: S, + ) -> ::core::result::Result { + s.serialize_str(::buffa::Enumeration::proto_name(self)) + } +} +impl<'de> ::serde::Deserialize<'de> for CoseErrorReason { + fn deserialize>( + d: D, + ) -> ::core::result::Result { + struct _V; + impl ::serde::de::Visitor<'_> for _V { + type Value = CoseErrorReason; + fn expecting( + &self, + f: &mut ::core::fmt::Formatter<'_>, + ) -> ::core::fmt::Result { + f.write_str( + concat!( + "a string, integer, or null for ", stringify!(CoseErrorReason) + ), + ) + } + fn visit_str( + self, + v: &str, + ) -> ::core::result::Result { + ::from_proto_name(v) + .ok_or_else(|| { ::serde::de::Error::unknown_variant(v, &[]) }) + } + fn visit_i64( + self, + v: i64, + ) -> ::core::result::Result { + let v32 = i32::try_from(v) + .map_err(|_| { + ::serde::de::Error::custom("enum value out of i32 range") + })?; + ::from_i32(v32) + .ok_or_else(|| { + ::serde::de::Error::custom("unknown enum value") + }) + } + fn visit_u64( + self, + v: u64, + ) -> ::core::result::Result { + let v32 = i32::try_from(v) + .map_err(|_| { + ::serde::de::Error::custom("enum value out of i32 range") + })?; + ::from_i32(v32) + .ok_or_else(|| { + ::serde::de::Error::custom("unknown enum value") + }) + } + fn visit_unit( + self, + ) -> ::core::result::Result { + ::core::result::Result::Ok(::core::default::Default::default()) + } + } + d.deserialize_any(_V) + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseErrorReason { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +impl ::buffa::Enumeration for CoseErrorReason { + fn from_i32(value: i32) -> ::core::option::Option { + match value { + 0i32 => ::core::option::Option::Some(Self::COSE_ERROR_REASON_UNSPECIFIED), + 100i32 => ::core::option::Option::Some(Self::COSE_ERROR_REASON_COMMON_CBOR), + 101i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_INVALID_FORMAT, + ) + } + 102i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_RESOURCE_LIMIT_EXCEEDED, + ) + } + 103i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_NON_CANONICAL_CBOR, + ) + } + 104i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_UNEXPECTED_CBOR_TAG, + ) + } + 105i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_DUPLICATE_MAP_LABEL, + ) + } + 120i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_MALFORMED_PROTOBUF, + ) + } + 121i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_MALFORMED_JSON, + ) + } + 130i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_INVALID_PARAMETER, + ) + } + 131i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_INVALID_LENGTH, + ) + } + 132i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_INVALID_ENCODING, + ) + } + 200i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_UNSUPPORTED_ALGORITHM, + ) + } + 201i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_PROVIDER_UNAVAILABLE, + ) + } + 300i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_CRYPTO_FAILED, + ) + } + 301i32 => { + ::core::option::Option::Some(Self::COSE_ERROR_REASON_BACKEND_INTERNAL) + } + 400i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_SIGN1_KID_KEY_MISMATCH, + ) + } + 401i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_SIGN1_MISSING_PAYLOAD, + ) + } + 402i32 => { + ::core::option::Option::Some(Self::COSE_ERROR_REASON_SIGN1_MISSING_KID) + } + 403i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_SIGN1_KEY_NOT_RESOLVED, + ) + } + 410i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_SIGN1_UNSUPPORTED_CRITICAL_HEADER, + ) + } + 411i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_SIGN1_UNPROTECTED_HEADER_NOT_ALLOWED, + ) + } + 420i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE, + ) + } + 421i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_SIGN1_MISSING_PRIVATE_KEY, + ) + } + 422i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE_ENCODING, + ) + } + 500i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_KEY_MISSING_KEY_MATERIAL, + ) + } + 501i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_KEY_INVALID_KEY_MATERIAL, + ) + } + 600i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_MULTIKEY_INVALID_MULTIKEY, + ) + } + 700i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_ENCRYPT_MISSING_CIPHERTEXT, + ) + } + 701i32 => { + ::core::option::Option::Some(Self::COSE_ERROR_REASON_ENCRYPT_INVALID_IV) + } + 702i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_ENCRYPT_INVALID_RECIPIENT, + ) + } + 703i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_ENCRYPT_MISSING_ENCAPSULATED_KEY, + ) + } + 704i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_ENCRYPT_INVALID_ENCAPSULATED_KEY, + ) + } + 720i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_ENCRYPT_AUTHENTICATION_FAILED, + ) + } + 721i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_ENCRYPT_KEY_UNWRAP_FAILED, + ) + } + 730i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_ENCRYPT_KID_MISMATCH, + ) + } + 731i32 => { + ::core::option::Option::Some(Self::COSE_ERROR_REASON_ENCRYPT_MISSING_KID) + } + 740i32 => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_ENCRYPT_UNPROTECTED_HEADER_NOT_ALLOWED, + ) + } + _ => ::core::option::Option::None, + } + } + fn to_i32(&self) -> i32 { + *self as i32 + } + fn proto_name(&self) -> &'static str { + match self { + Self::COSE_ERROR_REASON_UNSPECIFIED => "COSE_ERROR_REASON_UNSPECIFIED", + Self::COSE_ERROR_REASON_COMMON_CBOR => "COSE_ERROR_REASON_COMMON_CBOR", + Self::COSE_ERROR_REASON_COMMON_INVALID_FORMAT => { + "COSE_ERROR_REASON_COMMON_INVALID_FORMAT" + } + Self::COSE_ERROR_REASON_COMMON_RESOURCE_LIMIT_EXCEEDED => { + "COSE_ERROR_REASON_COMMON_RESOURCE_LIMIT_EXCEEDED" + } + Self::COSE_ERROR_REASON_COMMON_NON_CANONICAL_CBOR => { + "COSE_ERROR_REASON_COMMON_NON_CANONICAL_CBOR" + } + Self::COSE_ERROR_REASON_COMMON_UNEXPECTED_CBOR_TAG => { + "COSE_ERROR_REASON_COMMON_UNEXPECTED_CBOR_TAG" + } + Self::COSE_ERROR_REASON_COMMON_DUPLICATE_MAP_LABEL => { + "COSE_ERROR_REASON_COMMON_DUPLICATE_MAP_LABEL" + } + Self::COSE_ERROR_REASON_COMMON_MALFORMED_PROTOBUF => { + "COSE_ERROR_REASON_COMMON_MALFORMED_PROTOBUF" + } + Self::COSE_ERROR_REASON_COMMON_MALFORMED_JSON => { + "COSE_ERROR_REASON_COMMON_MALFORMED_JSON" + } + Self::COSE_ERROR_REASON_COMMON_INVALID_PARAMETER => { + "COSE_ERROR_REASON_COMMON_INVALID_PARAMETER" + } + Self::COSE_ERROR_REASON_COMMON_INVALID_LENGTH => { + "COSE_ERROR_REASON_COMMON_INVALID_LENGTH" + } + Self::COSE_ERROR_REASON_COMMON_INVALID_ENCODING => { + "COSE_ERROR_REASON_COMMON_INVALID_ENCODING" + } + Self::COSE_ERROR_REASON_COMMON_UNSUPPORTED_ALGORITHM => { + "COSE_ERROR_REASON_COMMON_UNSUPPORTED_ALGORITHM" + } + Self::COSE_ERROR_REASON_PROVIDER_UNAVAILABLE => { + "COSE_ERROR_REASON_PROVIDER_UNAVAILABLE" + } + Self::COSE_ERROR_REASON_COMMON_CRYPTO_FAILED => { + "COSE_ERROR_REASON_COMMON_CRYPTO_FAILED" + } + Self::COSE_ERROR_REASON_BACKEND_INTERNAL => { + "COSE_ERROR_REASON_BACKEND_INTERNAL" + } + Self::COSE_ERROR_REASON_SIGN1_KID_KEY_MISMATCH => { + "COSE_ERROR_REASON_SIGN1_KID_KEY_MISMATCH" + } + Self::COSE_ERROR_REASON_SIGN1_MISSING_PAYLOAD => { + "COSE_ERROR_REASON_SIGN1_MISSING_PAYLOAD" + } + Self::COSE_ERROR_REASON_SIGN1_MISSING_KID => { + "COSE_ERROR_REASON_SIGN1_MISSING_KID" + } + Self::COSE_ERROR_REASON_SIGN1_KEY_NOT_RESOLVED => { + "COSE_ERROR_REASON_SIGN1_KEY_NOT_RESOLVED" + } + Self::COSE_ERROR_REASON_SIGN1_UNSUPPORTED_CRITICAL_HEADER => { + "COSE_ERROR_REASON_SIGN1_UNSUPPORTED_CRITICAL_HEADER" + } + Self::COSE_ERROR_REASON_SIGN1_UNPROTECTED_HEADER_NOT_ALLOWED => { + "COSE_ERROR_REASON_SIGN1_UNPROTECTED_HEADER_NOT_ALLOWED" + } + Self::COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE => { + "COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE" + } + Self::COSE_ERROR_REASON_SIGN1_MISSING_PRIVATE_KEY => { + "COSE_ERROR_REASON_SIGN1_MISSING_PRIVATE_KEY" + } + Self::COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE_ENCODING => { + "COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE_ENCODING" + } + Self::COSE_ERROR_REASON_KEY_MISSING_KEY_MATERIAL => { + "COSE_ERROR_REASON_KEY_MISSING_KEY_MATERIAL" + } + Self::COSE_ERROR_REASON_KEY_INVALID_KEY_MATERIAL => { + "COSE_ERROR_REASON_KEY_INVALID_KEY_MATERIAL" + } + Self::COSE_ERROR_REASON_MULTIKEY_INVALID_MULTIKEY => { + "COSE_ERROR_REASON_MULTIKEY_INVALID_MULTIKEY" + } + Self::COSE_ERROR_REASON_ENCRYPT_MISSING_CIPHERTEXT => { + "COSE_ERROR_REASON_ENCRYPT_MISSING_CIPHERTEXT" + } + Self::COSE_ERROR_REASON_ENCRYPT_INVALID_IV => { + "COSE_ERROR_REASON_ENCRYPT_INVALID_IV" + } + Self::COSE_ERROR_REASON_ENCRYPT_INVALID_RECIPIENT => { + "COSE_ERROR_REASON_ENCRYPT_INVALID_RECIPIENT" + } + Self::COSE_ERROR_REASON_ENCRYPT_MISSING_ENCAPSULATED_KEY => { + "COSE_ERROR_REASON_ENCRYPT_MISSING_ENCAPSULATED_KEY" + } + Self::COSE_ERROR_REASON_ENCRYPT_INVALID_ENCAPSULATED_KEY => { + "COSE_ERROR_REASON_ENCRYPT_INVALID_ENCAPSULATED_KEY" + } + Self::COSE_ERROR_REASON_ENCRYPT_AUTHENTICATION_FAILED => { + "COSE_ERROR_REASON_ENCRYPT_AUTHENTICATION_FAILED" + } + Self::COSE_ERROR_REASON_ENCRYPT_KEY_UNWRAP_FAILED => { + "COSE_ERROR_REASON_ENCRYPT_KEY_UNWRAP_FAILED" + } + Self::COSE_ERROR_REASON_ENCRYPT_KID_MISMATCH => { + "COSE_ERROR_REASON_ENCRYPT_KID_MISMATCH" + } + Self::COSE_ERROR_REASON_ENCRYPT_MISSING_KID => { + "COSE_ERROR_REASON_ENCRYPT_MISSING_KID" + } + Self::COSE_ERROR_REASON_ENCRYPT_UNPROTECTED_HEADER_NOT_ALLOWED => { + "COSE_ERROR_REASON_ENCRYPT_UNPROTECTED_HEADER_NOT_ALLOWED" + } + } + } + fn from_proto_name(name: &str) -> ::core::option::Option { + match name { + "COSE_ERROR_REASON_UNSPECIFIED" => { + ::core::option::Option::Some(Self::COSE_ERROR_REASON_UNSPECIFIED) + } + "COSE_ERROR_REASON_COMMON_CBOR" => { + ::core::option::Option::Some(Self::COSE_ERROR_REASON_COMMON_CBOR) + } + "COSE_ERROR_REASON_COMMON_INVALID_FORMAT" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_INVALID_FORMAT, + ) + } + "COSE_ERROR_REASON_COMMON_RESOURCE_LIMIT_EXCEEDED" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_RESOURCE_LIMIT_EXCEEDED, + ) + } + "COSE_ERROR_REASON_COMMON_NON_CANONICAL_CBOR" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_NON_CANONICAL_CBOR, + ) + } + "COSE_ERROR_REASON_COMMON_UNEXPECTED_CBOR_TAG" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_UNEXPECTED_CBOR_TAG, + ) + } + "COSE_ERROR_REASON_COMMON_DUPLICATE_MAP_LABEL" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_DUPLICATE_MAP_LABEL, + ) + } + "COSE_ERROR_REASON_COMMON_MALFORMED_PROTOBUF" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_MALFORMED_PROTOBUF, + ) + } + "COSE_ERROR_REASON_COMMON_MALFORMED_JSON" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_MALFORMED_JSON, + ) + } + "COSE_ERROR_REASON_COMMON_INVALID_PARAMETER" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_INVALID_PARAMETER, + ) + } + "COSE_ERROR_REASON_COMMON_INVALID_LENGTH" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_INVALID_LENGTH, + ) + } + "COSE_ERROR_REASON_COMMON_INVALID_ENCODING" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_INVALID_ENCODING, + ) + } + "COSE_ERROR_REASON_COMMON_UNSUPPORTED_ALGORITHM" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_UNSUPPORTED_ALGORITHM, + ) + } + "COSE_ERROR_REASON_PROVIDER_UNAVAILABLE" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_PROVIDER_UNAVAILABLE, + ) + } + "COSE_ERROR_REASON_COMMON_CRYPTO_FAILED" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_COMMON_CRYPTO_FAILED, + ) + } + "COSE_ERROR_REASON_BACKEND_INTERNAL" => { + ::core::option::Option::Some(Self::COSE_ERROR_REASON_BACKEND_INTERNAL) + } + "COSE_ERROR_REASON_SIGN1_KID_KEY_MISMATCH" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_SIGN1_KID_KEY_MISMATCH, + ) + } + "COSE_ERROR_REASON_SIGN1_MISSING_PAYLOAD" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_SIGN1_MISSING_PAYLOAD, + ) + } + "COSE_ERROR_REASON_SIGN1_MISSING_KID" => { + ::core::option::Option::Some(Self::COSE_ERROR_REASON_SIGN1_MISSING_KID) + } + "COSE_ERROR_REASON_SIGN1_KEY_NOT_RESOLVED" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_SIGN1_KEY_NOT_RESOLVED, + ) + } + "COSE_ERROR_REASON_SIGN1_UNSUPPORTED_CRITICAL_HEADER" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_SIGN1_UNSUPPORTED_CRITICAL_HEADER, + ) + } + "COSE_ERROR_REASON_SIGN1_UNPROTECTED_HEADER_NOT_ALLOWED" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_SIGN1_UNPROTECTED_HEADER_NOT_ALLOWED, + ) + } + "COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE, + ) + } + "COSE_ERROR_REASON_SIGN1_MISSING_PRIVATE_KEY" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_SIGN1_MISSING_PRIVATE_KEY, + ) + } + "COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE_ENCODING" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE_ENCODING, + ) + } + "COSE_ERROR_REASON_KEY_MISSING_KEY_MATERIAL" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_KEY_MISSING_KEY_MATERIAL, + ) + } + "COSE_ERROR_REASON_KEY_INVALID_KEY_MATERIAL" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_KEY_INVALID_KEY_MATERIAL, + ) + } + "COSE_ERROR_REASON_MULTIKEY_INVALID_MULTIKEY" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_MULTIKEY_INVALID_MULTIKEY, + ) + } + "COSE_ERROR_REASON_ENCRYPT_MISSING_CIPHERTEXT" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_ENCRYPT_MISSING_CIPHERTEXT, + ) + } + "COSE_ERROR_REASON_ENCRYPT_INVALID_IV" => { + ::core::option::Option::Some(Self::COSE_ERROR_REASON_ENCRYPT_INVALID_IV) + } + "COSE_ERROR_REASON_ENCRYPT_INVALID_RECIPIENT" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_ENCRYPT_INVALID_RECIPIENT, + ) + } + "COSE_ERROR_REASON_ENCRYPT_MISSING_ENCAPSULATED_KEY" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_ENCRYPT_MISSING_ENCAPSULATED_KEY, + ) + } + "COSE_ERROR_REASON_ENCRYPT_INVALID_ENCAPSULATED_KEY" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_ENCRYPT_INVALID_ENCAPSULATED_KEY, + ) + } + "COSE_ERROR_REASON_ENCRYPT_AUTHENTICATION_FAILED" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_ENCRYPT_AUTHENTICATION_FAILED, + ) + } + "COSE_ERROR_REASON_ENCRYPT_KEY_UNWRAP_FAILED" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_ENCRYPT_KEY_UNWRAP_FAILED, + ) + } + "COSE_ERROR_REASON_ENCRYPT_KID_MISMATCH" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_ENCRYPT_KID_MISMATCH, + ) + } + "COSE_ERROR_REASON_ENCRYPT_MISSING_KID" => { + ::core::option::Option::Some(Self::COSE_ERROR_REASON_ENCRYPT_MISSING_KID) + } + "COSE_ERROR_REASON_ENCRYPT_UNPROTECTED_HEADER_NOT_ALLOWED" => { + ::core::option::Option::Some( + Self::COSE_ERROR_REASON_ENCRYPT_UNPROTECTED_HEADER_NOT_ALLOWED, + ) + } + _ => ::core::option::Option::None, + } + } + fn values() -> &'static [Self] { + &[ + Self::COSE_ERROR_REASON_UNSPECIFIED, + Self::COSE_ERROR_REASON_COMMON_CBOR, + Self::COSE_ERROR_REASON_COMMON_INVALID_FORMAT, + Self::COSE_ERROR_REASON_COMMON_RESOURCE_LIMIT_EXCEEDED, + Self::COSE_ERROR_REASON_COMMON_NON_CANONICAL_CBOR, + Self::COSE_ERROR_REASON_COMMON_UNEXPECTED_CBOR_TAG, + Self::COSE_ERROR_REASON_COMMON_DUPLICATE_MAP_LABEL, + Self::COSE_ERROR_REASON_COMMON_MALFORMED_PROTOBUF, + Self::COSE_ERROR_REASON_COMMON_MALFORMED_JSON, + Self::COSE_ERROR_REASON_COMMON_INVALID_PARAMETER, + Self::COSE_ERROR_REASON_COMMON_INVALID_LENGTH, + Self::COSE_ERROR_REASON_COMMON_INVALID_ENCODING, + Self::COSE_ERROR_REASON_COMMON_UNSUPPORTED_ALGORITHM, + Self::COSE_ERROR_REASON_PROVIDER_UNAVAILABLE, + Self::COSE_ERROR_REASON_COMMON_CRYPTO_FAILED, + Self::COSE_ERROR_REASON_BACKEND_INTERNAL, + Self::COSE_ERROR_REASON_SIGN1_KID_KEY_MISMATCH, + Self::COSE_ERROR_REASON_SIGN1_MISSING_PAYLOAD, + Self::COSE_ERROR_REASON_SIGN1_MISSING_KID, + Self::COSE_ERROR_REASON_SIGN1_KEY_NOT_RESOLVED, + Self::COSE_ERROR_REASON_SIGN1_UNSUPPORTED_CRITICAL_HEADER, + Self::COSE_ERROR_REASON_SIGN1_UNPROTECTED_HEADER_NOT_ALLOWED, + Self::COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE, + Self::COSE_ERROR_REASON_SIGN1_MISSING_PRIVATE_KEY, + Self::COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE_ENCODING, + Self::COSE_ERROR_REASON_KEY_MISSING_KEY_MATERIAL, + Self::COSE_ERROR_REASON_KEY_INVALID_KEY_MATERIAL, + Self::COSE_ERROR_REASON_MULTIKEY_INVALID_MULTIKEY, + Self::COSE_ERROR_REASON_ENCRYPT_MISSING_CIPHERTEXT, + Self::COSE_ERROR_REASON_ENCRYPT_INVALID_IV, + Self::COSE_ERROR_REASON_ENCRYPT_INVALID_RECIPIENT, + Self::COSE_ERROR_REASON_ENCRYPT_MISSING_ENCAPSULATED_KEY, + Self::COSE_ERROR_REASON_ENCRYPT_INVALID_ENCAPSULATED_KEY, + Self::COSE_ERROR_REASON_ENCRYPT_AUTHENTICATION_FAILED, + Self::COSE_ERROR_REASON_ENCRYPT_KEY_UNWRAP_FAILED, + Self::COSE_ERROR_REASON_ENCRYPT_KID_MISMATCH, + Self::COSE_ERROR_REASON_ENCRYPT_MISSING_KID, + Self::COSE_ERROR_REASON_ENCRYPT_UNPROTECTED_HEADER_NOT_ALLOWED, + ] + } +} +/// CoseError is the public, non-PII error envelope for COSE boundary failures. +/// The branch is part of the stable contract: primitive covers caller-controlled +/// input and COSE semantic failures, provider covers algorithm/provider +/// availability, and backend covers cryptographic backend, serialization, or +/// internal adapter failures. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseError { + #[serde(flatten)] + pub error: ::core::option::Option<__buffa::oneof::cose_error::Error>, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseError { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseError").field("error", &self.error).finish() + } +} +impl CoseError { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseError"; +} +::buffa::impl_default_instance!(CoseError); +impl ::buffa::MessageName for CoseError { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseError"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseError"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseError"; +} +impl ::buffa::Message for CoseError { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, __cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if let ::core::option::Option::Some(ref v) = self.error { + match v { + __buffa::oneof::cose_error::Error::Primitive(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 1u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_error::Error::Provider(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 1u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_error::Error::Backend(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 1u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + } + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + __cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if let ::core::option::Option::Some(ref v) = self.error { + match v { + __buffa::oneof::cose_error::Error::Primitive(x) => { + ::buffa::types::put_len_delimited_header( + 1u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_error::Error::Provider(x) => { + ::buffa::types::put_len_delimited_header( + 2u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_error::Error::Backend(x) => { + ::buffa::types::put_len_delimited_header( + 3u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + } + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_error::Error::Primitive(ref mut existing), + ) = self.error + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.error = ::core::option::Option::Some( + __buffa::oneof::cose_error::Error::Primitive( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_error::Error::Provider(ref mut existing), + ) = self.error + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.error = ::core::option::Option::Some( + __buffa::oneof::cose_error::Error::Provider( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_error::Error::Backend(ref mut existing), + ) = self.error + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.error = ::core::option::Option::Some( + __buffa::oneof::cose_error::Error::Backend( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + self.error = ::core::option::Option::None; + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseError { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseError"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl<'de> serde::Deserialize<'de> for CoseError { + fn deserialize>( + d: D, + ) -> ::core::result::Result { + struct _V; + impl<'de> serde::de::Visitor<'de> for _V { + type Value = CoseError; + fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("struct CoseError") + } + #[allow(clippy::field_reassign_with_default)] + fn visit_map>( + self, + mut map: A, + ) -> ::core::result::Result { + let mut __oneof_error: ::core::option::Option< + __buffa::oneof::cose_error::Error, + > = None; + while let Some(key) = map.next_key::<::buffa::alloc::string::String>()? { + match key.as_str() { + "primitive" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CosePrimitiveError, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_error.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'error'", + ), + ); + } + __oneof_error = Some( + __buffa::oneof::cose_error::Error::Primitive( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "provider" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseProviderError, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_error.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'error'", + ), + ); + } + __oneof_error = Some( + __buffa::oneof::cose_error::Error::Provider( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "backend" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseBackendError, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_error.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'error'", + ), + ); + } + __oneof_error = Some( + __buffa::oneof::cose_error::Error::Backend( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + _ => { + return Err(serde::de::Error::custom("unknown field")); + } + } + } + let mut __r = ::default(); + __r.error = __oneof_error; + Ok(__r) + } + } + d.deserialize_map(_V) + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseError { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_ERROR_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseError", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +pub mod cose_error { + #[allow(unused_imports)] + use super::*; + #[doc(inline)] + pub use super::__buffa::oneof::cose_error::Error; + #[doc(inline)] + pub use super::__buffa::view::oneof::cose_error::Error as ErrorView; +} +/// CosePrimitiveError describes failures owned by COSE input validation, +/// byte-format parsing, key material handling, signing inputs, and signature +/// verification semantics. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize, ::serde::Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct CosePrimitiveError { + /// Reason must be a caller-input, COSE semantic, Sign1, Key, or Multikey + /// reason. Provider-only and backend-only reasons are rejected by the Rust + /// wire adapter. + /// + /// Field 1: `reason` + #[serde( + rename = "reason", + with = "::buffa::json_helpers::proto_enum", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_default_enum_value" + )] + pub reason: ::buffa::EnumValue, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CosePrimitiveError { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CosePrimitiveError").field("reason", &self.reason).finish() + } +} +impl CosePrimitiveError { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CosePrimitiveError"; +} +::buffa::impl_default_instance!(CosePrimitiveError); +impl ::buffa::MessageName for CosePrimitiveError { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CosePrimitiveError"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CosePrimitiveError"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CosePrimitiveError"; +} +impl ::buffa::Message for CosePrimitiveError { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + { + let val = self.reason.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + { + let val = self.reason.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(1u32, val, buf); + } + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.reason = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(buf)?, + ); + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + self.reason = ::buffa::EnumValue::from(0); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CosePrimitiveError { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CosePrimitiveError"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl ::buffa::json_helpers::ProtoElemJson for CosePrimitiveError { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_PRIMITIVE_ERROR_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CosePrimitiveError", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +/// CoseProviderError describes provider selection and algorithm availability +/// failures. It intentionally contains no backend exception text. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize, ::serde::Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseProviderError { + /// Reason must be COSE_ERROR_REASON_COMMON_UNSUPPORTED_ALGORITHM or one of + /// the COSE_ERROR_REASON_PROVIDER_* values. + /// + /// Field 1: `reason` + #[serde( + rename = "reason", + with = "::buffa::json_helpers::proto_enum", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_default_enum_value" + )] + pub reason: ::buffa::EnumValue, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseProviderError { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseProviderError").field("reason", &self.reason).finish() + } +} +impl CoseProviderError { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseProviderError"; +} +::buffa::impl_default_instance!(CoseProviderError); +impl ::buffa::MessageName for CoseProviderError { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseProviderError"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseProviderError"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseProviderError"; +} +impl ::buffa::Message for CoseProviderError { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + { + let val = self.reason.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + { + let val = self.reason.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(1u32, val, buf); + } + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.reason = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(buf)?, + ); + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + self.reason = ::buffa::EnumValue::from(0); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseProviderError { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseProviderError"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseProviderError { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_PROVIDER_ERROR_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseProviderError", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +/// CoseBackendError describes cryptographic backend, dispatch, and internal +/// failures that are unsafe to expose as raw backend exception text. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize, ::serde::Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseBackendError { + /// Reason must describe cryptographic backend failure, output serialization, + /// dispatch failure, or an internal adapter invariant. Malformed or oversized + /// caller input belongs to the primitive branch. + /// + /// Field 1: `reason` + #[serde( + rename = "reason", + with = "::buffa::json_helpers::proto_enum", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_default_enum_value" + )] + pub reason: ::buffa::EnumValue, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseBackendError { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseBackendError").field("reason", &self.reason).finish() + } +} +impl CoseBackendError { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseBackendError"; +} +::buffa::impl_default_instance!(CoseBackendError); +impl ::buffa::MessageName for CoseBackendError { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseBackendError"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseBackendError"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseBackendError"; +} +impl ::buffa::Message for CoseBackendError { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + { + let val = self.reason.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + { + let val = self.reason.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(1u32, val, buf); + } + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.reason = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(buf)?, + ); + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + self.reason = ::buffa::EnumValue::from(0); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseBackendError { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseBackendError"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseBackendError { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_BACKEND_ERROR_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseBackendError", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +/// CoseAlgorithmIdentifier is used only by COSE_Key operations that accept +/// more than one algorithm family. Operation-specific messages use the narrower +/// family enum directly so invalid combinations are unrepresentable. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseAlgorithmIdentifier { + #[serde(flatten)] + pub algorithm: ::core::option::Option< + __buffa::oneof::cose_algorithm_identifier::Algorithm, + >, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseAlgorithmIdentifier { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseAlgorithmIdentifier") + .field("algorithm", &self.algorithm) + .finish() + } +} +impl CoseAlgorithmIdentifier { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseAlgorithmIdentifier"; +} +::buffa::impl_default_instance!(CoseAlgorithmIdentifier); +impl ::buffa::MessageName for CoseAlgorithmIdentifier { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseAlgorithmIdentifier"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseAlgorithmIdentifier"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseAlgorithmIdentifier"; +} +impl ::buffa::Message for CoseAlgorithmIdentifier { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if let ::core::option::Option::Some(ref v) = self.algorithm { + match v { + __buffa::oneof::cose_algorithm_identifier::Algorithm::Signature(x) => { + size += 1u64 + ::buffa::types::int32_encoded_len(x.to_i32()) as u64; + } + __buffa::oneof::cose_algorithm_identifier::Algorithm::KeyAgreement( + x, + ) => { + size += 1u64 + ::buffa::types::int32_encoded_len(x.to_i32()) as u64; + } + __buffa::oneof::cose_algorithm_identifier::Algorithm::Kem(x) => { + size += 1u64 + ::buffa::types::int32_encoded_len(x.to_i32()) as u64; + } + } + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if let ::core::option::Option::Some(ref v) = self.algorithm { + match v { + __buffa::oneof::cose_algorithm_identifier::Algorithm::Signature(x) => { + ::buffa::types::put_int32_field(1u32, x.to_i32(), buf); + } + __buffa::oneof::cose_algorithm_identifier::Algorithm::KeyAgreement( + x, + ) => { + ::buffa::types::put_int32_field(2u32, x.to_i32(), buf); + } + __buffa::oneof::cose_algorithm_identifier::Algorithm::Kem(x) => { + ::buffa::types::put_int32_field(3u32, x.to_i32(), buf); + } + } + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.algorithm = ::core::option::Option::Some( + __buffa::oneof::cose_algorithm_identifier::Algorithm::Signature( + ::buffa::EnumValue::from(::buffa::types::decode_int32(buf)?), + ), + ); + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.algorithm = ::core::option::Option::Some( + __buffa::oneof::cose_algorithm_identifier::Algorithm::KeyAgreement( + ::buffa::EnumValue::from(::buffa::types::decode_int32(buf)?), + ), + ); + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.algorithm = ::core::option::Option::Some( + __buffa::oneof::cose_algorithm_identifier::Algorithm::Kem( + ::buffa::EnumValue::from(::buffa::types::decode_int32(buf)?), + ), + ); + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + self.algorithm = ::core::option::Option::None; + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseAlgorithmIdentifier { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseAlgorithmIdentifier"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl<'de> serde::Deserialize<'de> for CoseAlgorithmIdentifier { + fn deserialize>( + d: D, + ) -> ::core::result::Result { + struct _V; + impl<'de> serde::de::Visitor<'de> for _V { + type Value = CoseAlgorithmIdentifier; + fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("struct CoseAlgorithmIdentifier") + } + #[allow(clippy::field_reassign_with_default)] + fn visit_map>( + self, + mut map: A, + ) -> ::core::result::Result { + let mut __oneof_algorithm: ::core::option::Option< + __buffa::oneof::cose_algorithm_identifier::Algorithm, + > = None; + while let Some(key) = map.next_key::<::buffa::alloc::string::String>()? { + match key.as_str() { + "signature" => { + let v: ::core::option::Option< + ::buffa::EnumValue, + > = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + ::buffa::EnumValue, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_algorithm.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'algorithm'", + ), + ); + } + __oneof_algorithm = Some( + __buffa::oneof::cose_algorithm_identifier::Algorithm::Signature( + v, + ), + ); + } + } + "keyAgreement" | "key_agreement" => { + let v: ::core::option::Option< + ::buffa::EnumValue, + > = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + ::buffa::EnumValue, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_algorithm.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'algorithm'", + ), + ); + } + __oneof_algorithm = Some( + __buffa::oneof::cose_algorithm_identifier::Algorithm::KeyAgreement( + v, + ), + ); + } + } + "kem" => { + let v: ::core::option::Option< + ::buffa::EnumValue, + > = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + ::buffa::EnumValue, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_algorithm.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'algorithm'", + ), + ); + } + __oneof_algorithm = Some( + __buffa::oneof::cose_algorithm_identifier::Algorithm::Kem(v), + ); + } + } + _ => { + return Err(serde::de::Error::custom("unknown field")); + } + } + } + let mut __r = ::default(); + __r.algorithm = __oneof_algorithm; + Ok(__r) + } + } + d.deserialize_map(_V) + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseAlgorithmIdentifier { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_ALGORITHM_IDENTIFIER_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseAlgorithmIdentifier", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +pub mod cose_algorithm_identifier { + #[allow(unused_imports)] + use super::*; + #[doc(inline)] + pub use super::__buffa::oneof::cose_algorithm_identifier::Algorithm; + #[doc(inline)] + pub use super::__buffa::view::oneof::cose_algorithm_identifier::Algorithm as AlgorithmView; +} +/// CoseOperationRequest is the single executable protobuf entrypoint for +/// FFI and generated-SDK adapters. Native Rust SDKs +/// should keep using typed methods; adapters can use this generated oneof to +/// avoid hand-rolled operation routing and error-envelope plumbing. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseOperationRequest { + #[serde(flatten)] + pub operation: ::core::option::Option< + __buffa::oneof::cose_operation_request::Operation, + >, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseOperationRequest { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseOperationRequest") + .field("operation", &self.operation) + .finish() + } +} +impl ::core::ops::Drop for CoseOperationRequest { + fn drop(&mut self) { + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl CoseOperationRequest { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseOperationRequest"; +} +::buffa::impl_default_instance!(CoseOperationRequest); +impl ::buffa::MessageName for CoseOperationRequest { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseOperationRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseOperationRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseOperationRequest"; +} +impl ::buffa::Message for CoseOperationRequest { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, __cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if let ::core::option::Option::Some(ref v) = self.operation { + match v { + __buffa::oneof::cose_operation_request::Operation::Sign1Create(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_request::Operation::Sign1CreateDetached( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_request::Operation::Sign1Verify(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_request::Operation::Sign1VerifyDetached( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_request::Operation::KeyFromPublicBytes( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_request::Operation::KeyFromPrivateBytes( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_request::Operation::KeyParse(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_request::Operation::KeyToPublicBytes( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_request::Operation::KeyToPrivateBytes( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_request::Operation::KeyDerivePublicKid( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_request::Operation::KeyToMultikey(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_request::Operation::MultikeyToCoseKey( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_request::Operation::MlKemEncryptDirect( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 3u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_request::Operation::MlKemEncryptKeyWrap( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 3u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_request::Operation::MlKemDecrypt(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 3u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + } + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + __cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if let ::core::option::Option::Some(ref v) = self.operation { + match v { + __buffa::oneof::cose_operation_request::Operation::Sign1Create(x) => { + ::buffa::types::put_len_delimited_header( + 1000u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_request::Operation::Sign1CreateDetached( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 1001u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_request::Operation::Sign1Verify(x) => { + ::buffa::types::put_len_delimited_header( + 1002u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_request::Operation::Sign1VerifyDetached( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 1003u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_request::Operation::KeyFromPublicBytes( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2000u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_request::Operation::KeyFromPrivateBytes( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2001u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_request::Operation::KeyParse(x) => { + ::buffa::types::put_len_delimited_header( + 2002u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_request::Operation::KeyToPublicBytes( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2003u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_request::Operation::KeyToPrivateBytes( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2004u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_request::Operation::KeyDerivePublicKid( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2005u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_request::Operation::KeyToMultikey(x) => { + ::buffa::types::put_len_delimited_header( + 2006u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_request::Operation::MultikeyToCoseKey( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2007u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_request::Operation::MlKemEncryptDirect( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 3000u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_request::Operation::MlKemEncryptKeyWrap( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 3001u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_request::Operation::MlKemDecrypt(x) => { + ::buffa::types::put_len_delimited_header( + 3002u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + } + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1000u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::Sign1Create( + ref mut existing, + ), + ) = self.operation + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.operation = ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::Sign1Create( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 1001u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::Sign1CreateDetached( + ref mut existing, + ), + ) = self.operation + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.operation = ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::Sign1CreateDetached( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 1002u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::Sign1Verify( + ref mut existing, + ), + ) = self.operation + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.operation = ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::Sign1Verify( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 1003u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::Sign1VerifyDetached( + ref mut existing, + ), + ) = self.operation + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.operation = ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::Sign1VerifyDetached( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 2000u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::KeyFromPublicBytes( + ref mut existing, + ), + ) = self.operation + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.operation = ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::KeyFromPublicBytes( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 2001u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::KeyFromPrivateBytes( + ref mut existing, + ), + ) = self.operation + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.operation = ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::KeyFromPrivateBytes( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 2002u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::KeyParse( + ref mut existing, + ), + ) = self.operation + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.operation = ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::KeyParse( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 2003u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::KeyToPublicBytes( + ref mut existing, + ), + ) = self.operation + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.operation = ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::KeyToPublicBytes( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 2004u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::KeyToPrivateBytes( + ref mut existing, + ), + ) = self.operation + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.operation = ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::KeyToPrivateBytes( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 2005u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::KeyDerivePublicKid( + ref mut existing, + ), + ) = self.operation + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.operation = ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::KeyDerivePublicKid( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 2006u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::KeyToMultikey( + ref mut existing, + ), + ) = self.operation + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.operation = ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::KeyToMultikey( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 2007u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::MultikeyToCoseKey( + ref mut existing, + ), + ) = self.operation + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.operation = ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::MultikeyToCoseKey( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 3000u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::MlKemEncryptDirect( + ref mut existing, + ), + ) = self.operation + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.operation = ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::MlKemEncryptDirect( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 3001u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::MlKemEncryptKeyWrap( + ref mut existing, + ), + ) = self.operation + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.operation = ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::MlKemEncryptKeyWrap( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 3002u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::MlKemDecrypt( + ref mut existing, + ), + ) = self.operation + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.operation = ::core::option::Option::Some( + __buffa::oneof::cose_operation_request::Operation::MlKemDecrypt( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + self.operation = ::core::option::Option::None; + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseOperationRequest { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseOperationRequest"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl<'de> serde::Deserialize<'de> for CoseOperationRequest { + fn deserialize>( + d: D, + ) -> ::core::result::Result { + struct _V; + impl<'de> serde::de::Visitor<'de> for _V { + type Value = CoseOperationRequest; + fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("struct CoseOperationRequest") + } + #[allow(clippy::field_reassign_with_default)] + fn visit_map>( + self, + mut map: A, + ) -> ::core::result::Result { + let mut __oneof_operation: ::core::option::Option< + __buffa::oneof::cose_operation_request::Operation, + > = None; + while let Some(key) = map.next_key::<::buffa::alloc::string::String>()? { + match key.as_str() { + "sign1Create" | "sign1_create" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseSign1CreateRequest, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_operation.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'operation'", + ), + ); + } + __oneof_operation = Some( + __buffa::oneof::cose_operation_request::Operation::Sign1Create( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "sign1CreateDetached" | "sign1_create_detached" => { + let v: ::core::option::Option< + CoseSign1CreateDetachedRequest, + > = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseSign1CreateDetachedRequest, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_operation.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'operation'", + ), + ); + } + __oneof_operation = Some( + __buffa::oneof::cose_operation_request::Operation::Sign1CreateDetached( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "sign1Verify" | "sign1_verify" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseSign1VerifyRequest, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_operation.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'operation'", + ), + ); + } + __oneof_operation = Some( + __buffa::oneof::cose_operation_request::Operation::Sign1Verify( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "sign1VerifyDetached" | "sign1_verify_detached" => { + let v: ::core::option::Option< + CoseSign1VerifyDetachedRequest, + > = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseSign1VerifyDetachedRequest, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_operation.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'operation'", + ), + ); + } + __oneof_operation = Some( + __buffa::oneof::cose_operation_request::Operation::Sign1VerifyDetached( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "keyFromPublicBytes" | "key_from_public_bytes" => { + let v: ::core::option::Option< + CoseKeyFromPublicBytesRequest, + > = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseKeyFromPublicBytesRequest, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_operation.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'operation'", + ), + ); + } + __oneof_operation = Some( + __buffa::oneof::cose_operation_request::Operation::KeyFromPublicBytes( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "keyFromPrivateBytes" | "key_from_private_bytes" => { + let v: ::core::option::Option< + CoseKeyFromPrivateBytesRequest, + > = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseKeyFromPrivateBytesRequest, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_operation.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'operation'", + ), + ); + } + __oneof_operation = Some( + __buffa::oneof::cose_operation_request::Operation::KeyFromPrivateBytes( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "keyParse" | "key_parse" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseKeyBytesRequest, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_operation.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'operation'", + ), + ); + } + __oneof_operation = Some( + __buffa::oneof::cose_operation_request::Operation::KeyParse( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "keyToPublicBytes" | "key_to_public_bytes" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseKeyBytesRequest, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_operation.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'operation'", + ), + ); + } + __oneof_operation = Some( + __buffa::oneof::cose_operation_request::Operation::KeyToPublicBytes( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "keyToPrivateBytes" | "key_to_private_bytes" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseKeyBytesRequest, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_operation.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'operation'", + ), + ); + } + __oneof_operation = Some( + __buffa::oneof::cose_operation_request::Operation::KeyToPrivateBytes( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "keyDerivePublicKid" | "key_derive_public_kid" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseKeyBytesRequest, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_operation.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'operation'", + ), + ); + } + __oneof_operation = Some( + __buffa::oneof::cose_operation_request::Operation::KeyDerivePublicKid( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "keyToMultikey" | "key_to_multikey" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseKeyBytesRequest, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_operation.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'operation'", + ), + ); + } + __oneof_operation = Some( + __buffa::oneof::cose_operation_request::Operation::KeyToMultikey( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "multikeyToCoseKey" | "multikey_to_cose_key" => { + let v: ::core::option::Option< + CoseMultikeyToCoseKeyRequest, + > = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseMultikeyToCoseKeyRequest, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_operation.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'operation'", + ), + ); + } + __oneof_operation = Some( + __buffa::oneof::cose_operation_request::Operation::MultikeyToCoseKey( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "mlKemEncryptDirect" | "ml_kem_encrypt_direct" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseMlKemEncryptRequest, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_operation.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'operation'", + ), + ); + } + __oneof_operation = Some( + __buffa::oneof::cose_operation_request::Operation::MlKemEncryptDirect( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "mlKemEncryptKeyWrap" | "ml_kem_encrypt_key_wrap" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseMlKemEncryptRequest, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_operation.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'operation'", + ), + ); + } + __oneof_operation = Some( + __buffa::oneof::cose_operation_request::Operation::MlKemEncryptKeyWrap( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "mlKemDecrypt" | "ml_kem_decrypt" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseMlKemDecryptRequest, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_operation.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'operation'", + ), + ); + } + __oneof_operation = Some( + __buffa::oneof::cose_operation_request::Operation::MlKemDecrypt( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + _ => { + return Err(serde::de::Error::custom("unknown field")); + } + } + } + let mut __r = ::default(); + __r.operation = __oneof_operation; + Ok(__r) + } + } + d.deserialize_map(_V) + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseOperationRequest { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_OPERATION_REQUEST_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseOperationRequest", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +pub mod cose_operation_request { + #[allow(unused_imports)] + use super::*; + #[doc(inline)] + pub use super::__buffa::oneof::cose_operation_request::Operation; + #[doc(inline)] + pub use super::__buffa::view::oneof::cose_operation_request::Operation as OperationView; +} +/// CoseOperationResponseV2 is the executable response contract. Its generated +/// oneof distinguishes success from failure without an out-of-band status or an +/// opaque operation-specific payload. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseOperationResponseV2 { + #[serde(flatten)] + pub outcome: ::core::option::Option< + __buffa::oneof::cose_operation_response_v2::Outcome, + >, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseOperationResponseV2 { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseOperationResponseV2") + .field("outcome", &self.outcome) + .finish() + } +} +impl ::core::ops::Drop for CoseOperationResponseV2 { + fn drop(&mut self) { + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl CoseOperationResponseV2 { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseOperationResponseV2"; +} +::buffa::impl_default_instance!(CoseOperationResponseV2); +impl ::buffa::MessageName for CoseOperationResponseV2 { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseOperationResponseV2"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseOperationResponseV2"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseOperationResponseV2"; +} +impl ::buffa::Message for CoseOperationResponseV2 { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, __cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if let ::core::option::Option::Some(ref v) = self.outcome { + match v { + __buffa::oneof::cose_operation_response_v2::Outcome::Result(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 1u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_response_v2::Outcome::Error(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 1u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + } + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + __cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if let ::core::option::Option::Some(ref v) = self.outcome { + match v { + __buffa::oneof::cose_operation_response_v2::Outcome::Result(x) => { + ::buffa::types::put_len_delimited_header( + 1u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_response_v2::Outcome::Error(x) => { + ::buffa::types::put_len_delimited_header( + 2u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + } + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_response_v2::Outcome::Result( + ref mut existing, + ), + ) = self.outcome + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.outcome = ::core::option::Option::Some( + __buffa::oneof::cose_operation_response_v2::Outcome::Result( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_response_v2::Outcome::Error( + ref mut existing, + ), + ) = self.outcome + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.outcome = ::core::option::Option::Some( + __buffa::oneof::cose_operation_response_v2::Outcome::Error( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + self.outcome = ::core::option::Option::None; + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseOperationResponseV2 { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseOperationResponseV2"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl<'de> serde::Deserialize<'de> for CoseOperationResponseV2 { + fn deserialize>( + d: D, + ) -> ::core::result::Result { + struct _V; + impl<'de> serde::de::Visitor<'de> for _V { + type Value = CoseOperationResponseV2; + fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("struct CoseOperationResponseV2") + } + #[allow(clippy::field_reassign_with_default)] + fn visit_map>( + self, + mut map: A, + ) -> ::core::result::Result { + let mut __oneof_outcome: ::core::option::Option< + __buffa::oneof::cose_operation_response_v2::Outcome, + > = None; + while let Some(key) = map.next_key::<::buffa::alloc::string::String>()? { + match key.as_str() { + "result" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseOperationResult, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_outcome.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'outcome'", + ), + ); + } + __oneof_outcome = Some( + __buffa::oneof::cose_operation_response_v2::Outcome::Result( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "error" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseError, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_outcome.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'outcome'", + ), + ); + } + __oneof_outcome = Some( + __buffa::oneof::cose_operation_response_v2::Outcome::Error( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + _ => { + return Err(serde::de::Error::custom("unknown field")); + } + } + } + let mut __r = ::default(); + __r.outcome = __oneof_outcome; + Ok(__r) + } + } + d.deserialize_map(_V) + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseOperationResponseV2 { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_OPERATION_RESPONSE_V2_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseOperationResponseV2", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +pub mod cose_operation_response_v2 { + #[allow(unused_imports)] + use super::*; + #[doc(inline)] + pub use super::__buffa::oneof::cose_operation_response_v2::Outcome; + #[doc(inline)] + pub use super::__buffa::view::oneof::cose_operation_response_v2::Outcome as OutcomeView; +} +/// CoseOperationResult identifies the exact operation that produced a +/// successful response. Field numbers mirror CoseOperationRequest so generated +/// clients can audit request/result pairing without a separate registry. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseOperationResult { + #[serde(flatten)] + pub result: ::core::option::Option<__buffa::oneof::cose_operation_result::Result>, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseOperationResult { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseOperationResult").field("result", &self.result).finish() + } +} +impl ::core::ops::Drop for CoseOperationResult { + fn drop(&mut self) { + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl CoseOperationResult { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseOperationResult"; +} +::buffa::impl_default_instance!(CoseOperationResult); +impl ::buffa::MessageName for CoseOperationResult { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseOperationResult"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseOperationResult"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseOperationResult"; +} +impl ::buffa::Message for CoseOperationResult { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, __cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if let ::core::option::Option::Some(ref v) = self.result { + match v { + __buffa::oneof::cose_operation_result::Result::Sign1Create(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_result::Result::Sign1CreateDetached( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_result::Result::Sign1Verify(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_result::Result::Sign1VerifyDetached( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_result::Result::KeyFromPublicBytes(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_result::Result::KeyFromPrivateBytes( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_result::Result::KeyParse(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_result::Result::KeyToPublicBytes(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_result::Result::KeyToPrivateBytes(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_result::Result::KeyDerivePublicKid(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_result::Result::KeyToMultikey(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_result::Result::MultikeyToCoseKey(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 2u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_result::Result::MlKemEncryptDirect(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 3u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_result::Result::MlKemEncryptKeyWrap( + x, + ) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 3u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + __buffa::oneof::cose_operation_result::Result::MlKemDecrypt(x) => { + let __slot = __cache.reserve(); + let inner = x.compute_size(__cache); + __cache.set(__slot, inner); + size + += 3u64 + ::buffa::encoding::varint_len(inner as u64) as u64 + + inner as u64; + } + } + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + __cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if let ::core::option::Option::Some(ref v) = self.result { + match v { + __buffa::oneof::cose_operation_result::Result::Sign1Create(x) => { + ::buffa::types::put_len_delimited_header( + 1000u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_result::Result::Sign1CreateDetached( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 1001u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_result::Result::Sign1Verify(x) => { + ::buffa::types::put_len_delimited_header( + 1002u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_result::Result::Sign1VerifyDetached( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 1003u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_result::Result::KeyFromPublicBytes(x) => { + ::buffa::types::put_len_delimited_header( + 2000u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_result::Result::KeyFromPrivateBytes( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 2001u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_result::Result::KeyParse(x) => { + ::buffa::types::put_len_delimited_header( + 2002u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_result::Result::KeyToPublicBytes(x) => { + ::buffa::types::put_len_delimited_header( + 2003u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_result::Result::KeyToPrivateBytes(x) => { + ::buffa::types::put_len_delimited_header( + 2004u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_result::Result::KeyDerivePublicKid(x) => { + ::buffa::types::put_len_delimited_header( + 2005u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_result::Result::KeyToMultikey(x) => { + ::buffa::types::put_len_delimited_header( + 2006u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_result::Result::MultikeyToCoseKey(x) => { + ::buffa::types::put_len_delimited_header( + 2007u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_result::Result::MlKemEncryptDirect(x) => { + ::buffa::types::put_len_delimited_header( + 3000u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_result::Result::MlKemEncryptKeyWrap( + x, + ) => { + ::buffa::types::put_len_delimited_header( + 3001u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + __buffa::oneof::cose_operation_result::Result::MlKemDecrypt(x) => { + ::buffa::types::put_len_delimited_header( + 3002u32, + u64::from(__cache.consume_next()), + buf, + ); + x.write_to(__cache, buf); + } + } + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1000u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::Sign1Create( + ref mut existing, + ), + ) = self.result + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.result = ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::Sign1Create( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 1001u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::Sign1CreateDetached( + ref mut existing, + ), + ) = self.result + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.result = ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::Sign1CreateDetached( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 1002u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::Sign1Verify( + ref mut existing, + ), + ) = self.result + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.result = ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::Sign1Verify( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 1003u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::Sign1VerifyDetached( + ref mut existing, + ), + ) = self.result + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.result = ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::Sign1VerifyDetached( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 2000u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::KeyFromPublicBytes( + ref mut existing, + ), + ) = self.result + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.result = ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::KeyFromPublicBytes( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 2001u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::KeyFromPrivateBytes( + ref mut existing, + ), + ) = self.result + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.result = ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::KeyFromPrivateBytes( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 2002u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::KeyParse( + ref mut existing, + ), + ) = self.result + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.result = ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::KeyParse( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 2003u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::KeyToPublicBytes( + ref mut existing, + ), + ) = self.result + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.result = ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::KeyToPublicBytes( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 2004u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::KeyToPrivateBytes( + ref mut existing, + ), + ) = self.result + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.result = ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::KeyToPrivateBytes( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 2005u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::KeyDerivePublicKid( + ref mut existing, + ), + ) = self.result + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.result = ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::KeyDerivePublicKid( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 2006u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::KeyToMultikey( + ref mut existing, + ), + ) = self.result + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.result = ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::KeyToMultikey( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 2007u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::MultikeyToCoseKey( + ref mut existing, + ), + ) = self.result + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.result = ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::MultikeyToCoseKey( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 3000u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::MlKemEncryptDirect( + ref mut existing, + ), + ) = self.result + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.result = ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::MlKemEncryptDirect( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 3001u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::MlKemEncryptKeyWrap( + ref mut existing, + ), + ) = self.result + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.result = ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::MlKemEncryptKeyWrap( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + 3002u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + if let ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::MlKemDecrypt( + ref mut existing, + ), + ) = self.result + { + ::buffa::Message::merge_length_delimited(&mut **existing, buf, ctx)?; + } else { + let mut val = ::core::default::Default::default(); + ::buffa::Message::merge_length_delimited(&mut val, buf, ctx)?; + self.result = ::core::option::Option::Some( + __buffa::oneof::cose_operation_result::Result::MlKemDecrypt( + ::buffa::alloc::boxed::Box::new(val), + ), + ); + } + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + self.result = ::core::option::Option::None; + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseOperationResult { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseOperationResult"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl<'de> serde::Deserialize<'de> for CoseOperationResult { + fn deserialize>( + d: D, + ) -> ::core::result::Result { + struct _V; + impl<'de> serde::de::Visitor<'de> for _V { + type Value = CoseOperationResult; + fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("struct CoseOperationResult") + } + #[allow(clippy::field_reassign_with_default)] + fn visit_map>( + self, + mut map: A, + ) -> ::core::result::Result { + let mut __oneof_result: ::core::option::Option< + __buffa::oneof::cose_operation_result::Result, + > = None; + while let Some(key) = map.next_key::<::buffa::alloc::string::String>()? { + match key.as_str() { + "sign1Create" | "sign1_create" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseSign1CreateResult, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_result.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'result'", + ), + ); + } + __oneof_result = Some( + __buffa::oneof::cose_operation_result::Result::Sign1Create( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "sign1CreateDetached" | "sign1_create_detached" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseSign1CreateResult, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_result.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'result'", + ), + ); + } + __oneof_result = Some( + __buffa::oneof::cose_operation_result::Result::Sign1CreateDetached( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "sign1Verify" | "sign1_verify" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseSign1VerifyResult, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_result.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'result'", + ), + ); + } + __oneof_result = Some( + __buffa::oneof::cose_operation_result::Result::Sign1Verify( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "sign1VerifyDetached" | "sign1_verify_detached" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseSign1VerifyResult, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_result.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'result'", + ), + ); + } + __oneof_result = Some( + __buffa::oneof::cose_operation_result::Result::Sign1VerifyDetached( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "keyFromPublicBytes" | "key_from_public_bytes" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseKeyBytesResult, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_result.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'result'", + ), + ); + } + __oneof_result = Some( + __buffa::oneof::cose_operation_result::Result::KeyFromPublicBytes( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "keyFromPrivateBytes" | "key_from_private_bytes" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseKeyBytesResult, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_result.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'result'", + ), + ); + } + __oneof_result = Some( + __buffa::oneof::cose_operation_result::Result::KeyFromPrivateBytes( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "keyParse" | "key_parse" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseKeyBytesResult, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_result.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'result'", + ), + ); + } + __oneof_result = Some( + __buffa::oneof::cose_operation_result::Result::KeyParse( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "keyToPublicBytes" | "key_to_public_bytes" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseKeyBytesResult, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_result.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'result'", + ), + ); + } + __oneof_result = Some( + __buffa::oneof::cose_operation_result::Result::KeyToPublicBytes( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "keyToPrivateBytes" | "key_to_private_bytes" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseKeyBytesResult, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_result.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'result'", + ), + ); + } + __oneof_result = Some( + __buffa::oneof::cose_operation_result::Result::KeyToPrivateBytes( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "keyDerivePublicKid" | "key_derive_public_kid" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseKeyBytesResult, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_result.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'result'", + ), + ); + } + __oneof_result = Some( + __buffa::oneof::cose_operation_result::Result::KeyDerivePublicKid( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "keyToMultikey" | "key_to_multikey" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseMultikeyResult, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_result.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'result'", + ), + ); + } + __oneof_result = Some( + __buffa::oneof::cose_operation_result::Result::KeyToMultikey( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "multikeyToCoseKey" | "multikey_to_cose_key" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseKeyBytesResult, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_result.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'result'", + ), + ); + } + __oneof_result = Some( + __buffa::oneof::cose_operation_result::Result::MultikeyToCoseKey( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "mlKemEncryptDirect" | "ml_kem_encrypt_direct" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseMlKemEncryptResult, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_result.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'result'", + ), + ); + } + __oneof_result = Some( + __buffa::oneof::cose_operation_result::Result::MlKemEncryptDirect( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "mlKemEncryptKeyWrap" | "ml_kem_encrypt_key_wrap" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseMlKemEncryptResult, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_result.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'result'", + ), + ); + } + __oneof_result = Some( + __buffa::oneof::cose_operation_result::Result::MlKemEncryptKeyWrap( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + "mlKemDecrypt" | "ml_kem_decrypt" => { + let v: ::core::option::Option = map + .next_value_seed( + ::buffa::json_helpers::NullableDeserializeSeed( + ::buffa::json_helpers::DefaultDeserializeSeed::< + CoseMlKemDecryptResult, + >::new(), + ), + )?; + if let Some(v) = v { + if __oneof_result.is_some() { + return Err( + serde::de::Error::custom( + "multiple oneof fields set for 'result'", + ), + ); + } + __oneof_result = Some( + __buffa::oneof::cose_operation_result::Result::MlKemDecrypt( + ::buffa::alloc::boxed::Box::new(v), + ), + ); + } + } + _ => { + return Err(serde::de::Error::custom("unknown field")); + } + } + } + let mut __r = ::default(); + __r.result = __oneof_result; + Ok(__r) + } + } + d.deserialize_map(_V) + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseOperationResult { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_OPERATION_RESULT_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseOperationResult", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +pub mod cose_operation_result { + #[allow(unused_imports)] + use super::*; + #[doc(inline)] + pub use super::__buffa::oneof::cose_operation_result::Result; + #[doc(inline)] + pub use super::__buffa::view::oneof::cose_operation_result::Result as ResultView; +} +/// CoseMlKemEncryptRequest creates one-recipient COSE_Encrypt using the +/// ReallyMe pre-IANA ML-KEM profile. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseMlKemEncryptRequest { + /// ML-KEM-512, ML-KEM-768, or ML-KEM-1024. + /// + /// Field 1: `kem_algorithm` + #[serde( + rename = "kemAlgorithm", + alias = "kem_algorithm", + with = "::buffa::json_helpers::proto_enum", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_default_enum_value" + )] + pub kem_algorithm: ::buffa::EnumValue, + /// Protected content-encryption algorithm. + /// + /// Field 2: `content_algorithm` + #[serde( + rename = "contentAlgorithm", + alias = "content_algorithm", + with = "::buffa::json_helpers::proto_enum", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_default_enum_value" + )] + pub content_algorithm: ::buffa::EnumValue, + /// Recipient FIPS 203 ML-KEM encapsulation public key. + /// + /// Field 3: `recipient_public_key` + #[serde( + rename = "recipientPublicKey", + alias = "recipient_public_key", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub recipient_public_key: ::buffa::alloc::vec::Vec, + /// SHA-256 thumbprint of the canonical public-only, algorithm-bound COSE_Key. + /// The encrypt operation rejects any value that does not identify + /// recipient_public_key exactly. + /// + /// Field 4: `recipient_kid` + #[serde( + rename = "recipientKid", + alias = "recipient_kid", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub recipient_kid: ::buffa::alloc::vec::Vec, + /// SENSITIVE: plaintext encrypted into the COSE_Encrypt ciphertext. + /// + /// Field 5: `plaintext` + #[serde( + rename = "plaintext", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub plaintext: ::buffa::alloc::vec::Vec, + /// SENSITIVE: external AAD authenticated but not carried in COSE. + /// + /// Field 6: `external_aad` + #[serde( + rename = "externalAad", + alias = "external_aad", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub external_aad: ::buffa::alloc::vec::Vec, + /// SENSITIVE: mutually known private KDF context agreed out of band. + /// + /// Field 7: `supp_priv_info` + #[serde( + rename = "suppPrivInfo", + alias = "supp_priv_info", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub supp_priv_info: ::buffa::alloc::vec::Vec, + /// Distinguishes omitted SuppPrivInfo from an intentionally empty byte string. + /// + /// Field 8: `has_supp_priv_info` + #[serde( + rename = "hasSuppPrivInfo", + alias = "has_supp_priv_info", + with = "::buffa::json_helpers::proto_bool", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_false" + )] + pub has_supp_priv_info: bool, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseMlKemEncryptRequest { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseMlKemEncryptRequest") + .field("kem_algorithm", &self.kem_algorithm) + .field("content_algorithm", &self.content_algorithm) + .field("recipient_public_key", &"") + .field("recipient_kid", &"") + .field("plaintext", &"") + .field("external_aad", &"") + .field("supp_priv_info", &"") + .field("has_supp_priv_info", &self.has_supp_priv_info) + .finish() + } +} +impl<'de> ::serde::Deserialize<'de> for CoseMlKemEncryptRequest { + fn deserialize(deserializer: D) -> ::core::result::Result + where + D: ::serde::Deserializer<'de>, + { + fn deserialize_secret_bytes<'de, D>( + deserializer: D, + ) -> ::core::result::Result<::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, D::Error> + where + D: ::serde::Deserializer<'de>, + { + ::buffa::json_helpers::bytes::deserialize(deserializer) + .map(::zeroize::Zeroizing::new) + } + + + #[derive(Default, ::serde::Deserialize)] + #[serde(default, deny_unknown_fields)] + struct Wire { + #[serde(rename = "kemAlgorithm", alias = "kem_algorithm", with = "::buffa::json_helpers::proto_enum")] + kem_algorithm: ::buffa::EnumValue, + #[serde(rename = "contentAlgorithm", alias = "content_algorithm", with = "::buffa::json_helpers::proto_enum")] + content_algorithm: ::buffa::EnumValue, + #[serde(rename = "recipientPublicKey", alias = "recipient_public_key", deserialize_with = "deserialize_secret_bytes")] + recipient_public_key: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "recipientKid", alias = "recipient_kid", deserialize_with = "deserialize_secret_bytes")] + recipient_kid: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "plaintext", deserialize_with = "deserialize_secret_bytes")] + plaintext: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "externalAad", alias = "external_aad", deserialize_with = "deserialize_secret_bytes")] + external_aad: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "suppPrivInfo", alias = "supp_priv_info", deserialize_with = "deserialize_secret_bytes")] + supp_priv_info: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "hasSuppPrivInfo", alias = "has_supp_priv_info", with = "::buffa::json_helpers::proto_bool")] + has_supp_priv_info: bool, + } + + let mut wire = Wire::deserialize(deserializer)?; + Ok(Self { + kem_algorithm: wire.kem_algorithm, + content_algorithm: wire.content_algorithm, + recipient_public_key: ::core::mem::take(&mut *wire.recipient_public_key), + recipient_kid: ::core::mem::take(&mut *wire.recipient_kid), + plaintext: ::core::mem::take(&mut *wire.plaintext), + external_aad: ::core::mem::take(&mut *wire.external_aad), + supp_priv_info: ::core::mem::take(&mut *wire.supp_priv_info), + has_supp_priv_info: wire.has_supp_priv_info, + __buffa_unknown_fields: Default::default(), + }) + } +} +impl ::core::ops::Drop for CoseMlKemEncryptRequest { + fn drop(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.recipient_public_key); + ::zeroize::Zeroize::zeroize(&mut self.recipient_kid); + ::zeroize::Zeroize::zeroize(&mut self.plaintext); + ::zeroize::Zeroize::zeroize(&mut self.external_aad); + ::zeroize::Zeroize::zeroize(&mut self.supp_priv_info); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl CoseMlKemEncryptRequest { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseMlKemEncryptRequest"; +} +::buffa::impl_default_instance!(CoseMlKemEncryptRequest); +impl ::buffa::MessageName for CoseMlKemEncryptRequest { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseMlKemEncryptRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseMlKemEncryptRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseMlKemEncryptRequest"; +} +impl ::buffa::Message for CoseMlKemEncryptRequest { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + { + let val = self.kem_algorithm.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + { + let val = self.content_algorithm.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + if !self.recipient_public_key.is_empty() { + size + += 1u64 + + ::buffa::types::bytes_encoded_len(&self.recipient_public_key) + as u64; + } + if !self.recipient_kid.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.recipient_kid) as u64; + } + if !self.plaintext.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.plaintext) as u64; + } + if !self.external_aad.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.external_aad) as u64; + } + if !self.supp_priv_info.is_empty() { + size + += 1u64 + ::buffa::types::bytes_encoded_len(&self.supp_priv_info) as u64; + } + if self.has_supp_priv_info { + size += 1u64 + ::buffa::types::BOOL_ENCODED_LEN as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + { + let val = self.kem_algorithm.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(1u32, val, buf); + } + } + { + let val = self.content_algorithm.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(2u32, val, buf); + } + } + if !self.recipient_public_key.is_empty() { + ::buffa::types::put_shared_bytes_field( + 3u32, + &self.recipient_public_key, + buf, + ); + } + if !self.recipient_kid.is_empty() { + ::buffa::types::put_shared_bytes_field(4u32, &self.recipient_kid, buf); + } + if !self.plaintext.is_empty() { + ::buffa::types::put_shared_bytes_field(5u32, &self.plaintext, buf); + } + if !self.external_aad.is_empty() { + ::buffa::types::put_shared_bytes_field(6u32, &self.external_aad, buf); + } + if !self.supp_priv_info.is_empty() { + ::buffa::types::put_shared_bytes_field(7u32, &self.supp_priv_info, buf); + } + if self.has_supp_priv_info { + ::buffa::types::put_bool_field(8u32, self.has_supp_priv_info, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.kem_algorithm = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(buf)?, + ); + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.content_algorithm = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(buf)?, + ); + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.recipient_public_key, buf)?; + } + 4u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.recipient_kid, buf)?; + } + 5u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.plaintext, buf)?; + } + 6u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.external_aad, buf)?; + } + 7u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.supp_priv_info, buf)?; + } + 8u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.has_supp_priv_info = ::buffa::types::decode_bool(buf)?; + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + self.kem_algorithm = ::buffa::EnumValue::from(0); + self.content_algorithm = ::buffa::EnumValue::from(0); + ::zeroize::Zeroize::zeroize(&mut self.recipient_public_key); + ::zeroize::Zeroize::zeroize(&mut self.recipient_kid); + ::zeroize::Zeroize::zeroize(&mut self.plaintext); + ::zeroize::Zeroize::zeroize(&mut self.external_aad); + ::zeroize::Zeroize::zeroize(&mut self.supp_priv_info); + self.has_supp_priv_info = false; + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseMlKemEncryptRequest { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseMlKemEncryptRequest"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseMlKemEncryptRequest { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_ML_KEM_ENCRYPT_REQUEST_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseMlKemEncryptRequest", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseMlKemEncryptResult { + /// SENSITIVE: tagged COSE_Encrypt containing encrypted application content. + /// + /// Field 1: `cose_encrypt` + #[serde( + rename = "coseEncrypt", + alias = "cose_encrypt", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub cose_encrypt: ::buffa::alloc::vec::Vec, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseMlKemEncryptResult { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseMlKemEncryptResult") + .field("cose_encrypt", &"") + .finish() + } +} +impl<'de> ::serde::Deserialize<'de> for CoseMlKemEncryptResult { + fn deserialize(deserializer: D) -> ::core::result::Result + where + D: ::serde::Deserializer<'de>, + { + fn deserialize_secret_bytes<'de, D>( + deserializer: D, + ) -> ::core::result::Result<::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, D::Error> + where + D: ::serde::Deserializer<'de>, + { + ::buffa::json_helpers::bytes::deserialize(deserializer) + .map(::zeroize::Zeroizing::new) + } + + + #[derive(Default, ::serde::Deserialize)] + #[serde(default, deny_unknown_fields)] + struct Wire { + #[serde(rename = "coseEncrypt", alias = "cose_encrypt", deserialize_with = "deserialize_secret_bytes")] + cose_encrypt: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + } + + let mut wire = Wire::deserialize(deserializer)?; + Ok(Self { + cose_encrypt: ::core::mem::take(&mut *wire.cose_encrypt), + __buffa_unknown_fields: Default::default(), + }) + } +} +impl ::core::ops::Drop for CoseMlKemEncryptResult { + fn drop(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.cose_encrypt); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl CoseMlKemEncryptResult { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseMlKemEncryptResult"; +} +::buffa::impl_default_instance!(CoseMlKemEncryptResult); +impl ::buffa::MessageName for CoseMlKemEncryptResult { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseMlKemEncryptResult"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseMlKemEncryptResult"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseMlKemEncryptResult"; +} +impl ::buffa::Message for CoseMlKemEncryptResult { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.cose_encrypt.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.cose_encrypt) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.cose_encrypt.is_empty() { + ::buffa::types::put_shared_bytes_field(1u32, &self.cose_encrypt, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.cose_encrypt, buf)?; + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.cose_encrypt); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseMlKemEncryptResult { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseMlKemEncryptResult"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseMlKemEncryptResult { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_ML_KEM_ENCRYPT_RESULT_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseMlKemEncryptResult", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +/// CoseMlKemDecryptRequest authenticates and decrypts the ReallyMe pre-IANA +/// ML-KEM COSE profile. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseMlKemDecryptRequest { + /// SENSITIVE: tagged or untagged COSE_Encrypt bytes. + /// + /// Field 1: `cose_encrypt` + #[serde( + rename = "coseEncrypt", + alias = "cose_encrypt", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub cose_encrypt: ::buffa::alloc::vec::Vec, + /// SENSITIVE: 64-octet FIPS 203 private seed d || z. + /// + /// Field 2: `recipient_private_key` + #[serde( + rename = "recipientPrivateKey", + alias = "recipient_private_key", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub recipient_private_key: ::buffa::alloc::vec::Vec, + /// Expected SHA-256 canonical public COSE_Key thumbprint. Decryption derives + /// the public key from recipient_private_key and rejects a protected kid + /// bound to any other key before decapsulation. + /// + /// Field 3: `expected_recipient_kid` + #[serde( + rename = "expectedRecipientKid", + alias = "expected_recipient_kid", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub expected_recipient_kid: ::buffa::alloc::vec::Vec, + /// SENSITIVE: external AAD authenticated but not carried in COSE. + /// + /// Field 4: `external_aad` + #[serde( + rename = "externalAad", + alias = "external_aad", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub external_aad: ::buffa::alloc::vec::Vec, + /// SENSITIVE: mutually known private KDF context agreed out of band. + /// + /// Field 5: `supp_priv_info` + #[serde( + rename = "suppPrivInfo", + alias = "supp_priv_info", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub supp_priv_info: ::buffa::alloc::vec::Vec, + /// Distinguishes omitted SuppPrivInfo from an intentionally empty byte string. + /// + /// Field 6: `has_supp_priv_info` + #[serde( + rename = "hasSuppPrivInfo", + alias = "has_supp_priv_info", + with = "::buffa::json_helpers::proto_bool", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_false" + )] + pub has_supp_priv_info: bool, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseMlKemDecryptRequest { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseMlKemDecryptRequest") + .field("cose_encrypt", &"") + .field("recipient_private_key", &"") + .field("expected_recipient_kid", &"") + .field("external_aad", &"") + .field("supp_priv_info", &"") + .field("has_supp_priv_info", &self.has_supp_priv_info) + .finish() + } +} +impl<'de> ::serde::Deserialize<'de> for CoseMlKemDecryptRequest { + fn deserialize(deserializer: D) -> ::core::result::Result + where + D: ::serde::Deserializer<'de>, + { + fn deserialize_secret_bytes<'de, D>( + deserializer: D, + ) -> ::core::result::Result<::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, D::Error> + where + D: ::serde::Deserializer<'de>, + { + ::buffa::json_helpers::bytes::deserialize(deserializer) + .map(::zeroize::Zeroizing::new) + } + + + #[derive(Default, ::serde::Deserialize)] + #[serde(default, deny_unknown_fields)] + struct Wire { + #[serde(rename = "coseEncrypt", alias = "cose_encrypt", deserialize_with = "deserialize_secret_bytes")] + cose_encrypt: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "recipientPrivateKey", alias = "recipient_private_key", deserialize_with = "deserialize_secret_bytes")] + recipient_private_key: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "expectedRecipientKid", alias = "expected_recipient_kid", deserialize_with = "deserialize_secret_bytes")] + expected_recipient_kid: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "externalAad", alias = "external_aad", deserialize_with = "deserialize_secret_bytes")] + external_aad: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "suppPrivInfo", alias = "supp_priv_info", deserialize_with = "deserialize_secret_bytes")] + supp_priv_info: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "hasSuppPrivInfo", alias = "has_supp_priv_info", with = "::buffa::json_helpers::proto_bool")] + has_supp_priv_info: bool, + } + + let mut wire = Wire::deserialize(deserializer)?; + Ok(Self { + cose_encrypt: ::core::mem::take(&mut *wire.cose_encrypt), + recipient_private_key: ::core::mem::take(&mut *wire.recipient_private_key), + expected_recipient_kid: ::core::mem::take(&mut *wire.expected_recipient_kid), + external_aad: ::core::mem::take(&mut *wire.external_aad), + supp_priv_info: ::core::mem::take(&mut *wire.supp_priv_info), + has_supp_priv_info: wire.has_supp_priv_info, + __buffa_unknown_fields: Default::default(), + }) + } +} +impl ::core::ops::Drop for CoseMlKemDecryptRequest { + fn drop(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.cose_encrypt); + ::zeroize::Zeroize::zeroize(&mut self.recipient_private_key); + ::zeroize::Zeroize::zeroize(&mut self.expected_recipient_kid); + ::zeroize::Zeroize::zeroize(&mut self.external_aad); + ::zeroize::Zeroize::zeroize(&mut self.supp_priv_info); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl CoseMlKemDecryptRequest { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseMlKemDecryptRequest"; +} +::buffa::impl_default_instance!(CoseMlKemDecryptRequest); +impl ::buffa::MessageName for CoseMlKemDecryptRequest { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseMlKemDecryptRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseMlKemDecryptRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseMlKemDecryptRequest"; +} +impl ::buffa::Message for CoseMlKemDecryptRequest { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.cose_encrypt.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.cose_encrypt) as u64; + } + if !self.recipient_private_key.is_empty() { + size + += 1u64 + + ::buffa::types::bytes_encoded_len(&self.recipient_private_key) + as u64; + } + if !self.expected_recipient_kid.is_empty() { + size + += 1u64 + + ::buffa::types::bytes_encoded_len(&self.expected_recipient_kid) + as u64; + } + if !self.external_aad.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.external_aad) as u64; + } + if !self.supp_priv_info.is_empty() { + size + += 1u64 + ::buffa::types::bytes_encoded_len(&self.supp_priv_info) as u64; + } + if self.has_supp_priv_info { + size += 1u64 + ::buffa::types::BOOL_ENCODED_LEN as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.cose_encrypt.is_empty() { + ::buffa::types::put_shared_bytes_field(1u32, &self.cose_encrypt, buf); + } + if !self.recipient_private_key.is_empty() { + ::buffa::types::put_shared_bytes_field( + 2u32, + &self.recipient_private_key, + buf, + ); + } + if !self.expected_recipient_kid.is_empty() { + ::buffa::types::put_shared_bytes_field( + 3u32, + &self.expected_recipient_kid, + buf, + ); + } + if !self.external_aad.is_empty() { + ::buffa::types::put_shared_bytes_field(4u32, &self.external_aad, buf); + } + if !self.supp_priv_info.is_empty() { + ::buffa::types::put_shared_bytes_field(5u32, &self.supp_priv_info, buf); + } + if self.has_supp_priv_info { + ::buffa::types::put_bool_field(6u32, self.has_supp_priv_info, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.cose_encrypt, buf)?; + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.recipient_private_key, buf)?; + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.expected_recipient_kid, buf)?; + } + 4u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.external_aad, buf)?; + } + 5u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.supp_priv_info, buf)?; + } + 6u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.has_supp_priv_info = ::buffa::types::decode_bool(buf)?; + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.cose_encrypt); + ::zeroize::Zeroize::zeroize(&mut self.recipient_private_key); + ::zeroize::Zeroize::zeroize(&mut self.expected_recipient_kid); + ::zeroize::Zeroize::zeroize(&mut self.external_aad); + ::zeroize::Zeroize::zeroize(&mut self.supp_priv_info); + self.has_supp_priv_info = false; + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseMlKemDecryptRequest { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseMlKemDecryptRequest"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseMlKemDecryptRequest { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_ML_KEM_DECRYPT_REQUEST_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseMlKemDecryptRequest", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseMlKemDecryptResult { + /// SENSITIVE: authenticated plaintext. + /// + /// Field 1: `plaintext` + #[serde( + rename = "plaintext", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub plaintext: ::buffa::alloc::vec::Vec, + /// Field 2: `content_algorithm` + #[serde( + rename = "contentAlgorithm", + alias = "content_algorithm", + with = "::buffa::json_helpers::proto_enum", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_default_enum_value" + )] + pub content_algorithm: ::buffa::EnumValue, + /// Field 3: `kem_algorithm` + #[serde( + rename = "kemAlgorithm", + alias = "kem_algorithm", + with = "::buffa::json_helpers::proto_enum", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_default_enum_value" + )] + pub kem_algorithm: ::buffa::EnumValue, + /// Field 4: `mode` + #[serde( + rename = "mode", + with = "::buffa::json_helpers::proto_enum", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_default_enum_value" + )] + pub mode: ::buffa::EnumValue, + /// Authenticated canonical public COSE_Key thumbprint from the recipient. + /// + /// Field 5: `recipient_kid` + #[serde( + rename = "recipientKid", + alias = "recipient_kid", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub recipient_kid: ::buffa::alloc::vec::Vec, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseMlKemDecryptResult { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseMlKemDecryptResult") + .field("plaintext", &"") + .field("content_algorithm", &self.content_algorithm) + .field("kem_algorithm", &self.kem_algorithm) + .field("mode", &self.mode) + .field("recipient_kid", &"") + .finish() + } +} +impl<'de> ::serde::Deserialize<'de> for CoseMlKemDecryptResult { + fn deserialize(deserializer: D) -> ::core::result::Result + where + D: ::serde::Deserializer<'de>, + { + fn deserialize_secret_bytes<'de, D>( + deserializer: D, + ) -> ::core::result::Result<::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, D::Error> + where + D: ::serde::Deserializer<'de>, + { + ::buffa::json_helpers::bytes::deserialize(deserializer) + .map(::zeroize::Zeroizing::new) + } + + + #[derive(Default, ::serde::Deserialize)] + #[serde(default, deny_unknown_fields)] + struct Wire { + #[serde(rename = "plaintext", deserialize_with = "deserialize_secret_bytes")] + plaintext: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "contentAlgorithm", alias = "content_algorithm", with = "::buffa::json_helpers::proto_enum")] + content_algorithm: ::buffa::EnumValue, + #[serde(rename = "kemAlgorithm", alias = "kem_algorithm", with = "::buffa::json_helpers::proto_enum")] + kem_algorithm: ::buffa::EnumValue, + #[serde(rename = "mode", with = "::buffa::json_helpers::proto_enum")] + mode: ::buffa::EnumValue, + #[serde(rename = "recipientKid", alias = "recipient_kid", deserialize_with = "deserialize_secret_bytes")] + recipient_kid: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + } + + let mut wire = Wire::deserialize(deserializer)?; + Ok(Self { + plaintext: ::core::mem::take(&mut *wire.plaintext), + content_algorithm: wire.content_algorithm, + kem_algorithm: wire.kem_algorithm, + mode: wire.mode, + recipient_kid: ::core::mem::take(&mut *wire.recipient_kid), + __buffa_unknown_fields: Default::default(), + }) + } +} +impl ::core::ops::Drop for CoseMlKemDecryptResult { + fn drop(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.plaintext); + ::zeroize::Zeroize::zeroize(&mut self.recipient_kid); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl CoseMlKemDecryptResult { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseMlKemDecryptResult"; +} +::buffa::impl_default_instance!(CoseMlKemDecryptResult); +impl ::buffa::MessageName for CoseMlKemDecryptResult { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseMlKemDecryptResult"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseMlKemDecryptResult"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseMlKemDecryptResult"; +} +impl ::buffa::Message for CoseMlKemDecryptResult { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.plaintext.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.plaintext) as u64; + } + { + let val = self.content_algorithm.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + { + let val = self.kem_algorithm.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + { + let val = self.mode.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + if !self.recipient_kid.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.recipient_kid) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.plaintext.is_empty() { + ::buffa::types::put_shared_bytes_field(1u32, &self.plaintext, buf); + } + { + let val = self.content_algorithm.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(2u32, val, buf); + } + } + { + let val = self.kem_algorithm.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(3u32, val, buf); + } + } + { + let val = self.mode.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(4u32, val, buf); + } + } + if !self.recipient_kid.is_empty() { + ::buffa::types::put_shared_bytes_field(5u32, &self.recipient_kid, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.plaintext, buf)?; + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.content_algorithm = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(buf)?, + ); + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.kem_algorithm = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(buf)?, + ); + } + 4u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.mode = ::buffa::EnumValue::from(::buffa::types::decode_int32(buf)?); + } + 5u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.recipient_kid, buf)?; + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.plaintext); + self.content_algorithm = ::buffa::EnumValue::from(0); + self.kem_algorithm = ::buffa::EnumValue::from(0); + self.mode = ::buffa::EnumValue::from(0); + ::zeroize::Zeroize::zeroize(&mut self.recipient_kid); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseMlKemDecryptResult { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseMlKemDecryptResult"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseMlKemDecryptResult { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_ML_KEM_DECRYPT_RESULT_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseMlKemDecryptResult", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize, ::serde::Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseSign1Options { + /// Emit the registered COSE_Sign1 root tag (18). False emits the untagged + /// COSE_Sign1 structure used by most byte-level SDK APIs. + /// + /// Field 1: `tag` + #[serde( + rename = "tag", + with = "::buffa::json_helpers::proto_bool", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_false" + )] + pub tag: bool, + /// Optional maximum encoded COSE_Sign1 size. A value of 0 uses the crate + /// default. + /// + /// Field 2: `max_cose_sign1_bytes` + #[serde( + rename = "maxCoseSign1Bytes", + alias = "max_cose_sign1_bytes", + with = "::buffa::json_helpers::uint64", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_zero_u64" + )] + pub max_cose_sign1_bytes: u64, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseSign1Options { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseSign1Options") + .field("tag", &self.tag) + .field("max_cose_sign1_bytes", &self.max_cose_sign1_bytes) + .finish() + } +} +impl CoseSign1Options { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseSign1Options"; +} +::buffa::impl_default_instance!(CoseSign1Options); +impl ::buffa::MessageName for CoseSign1Options { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseSign1Options"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseSign1Options"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseSign1Options"; +} +impl ::buffa::Message for CoseSign1Options { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if self.tag { + size += 1u64 + ::buffa::types::BOOL_ENCODED_LEN as u64; + } + if self.max_cose_sign1_bytes != 0u64 { + size + += 1u64 + + ::buffa::types::uint64_encoded_len(self.max_cose_sign1_bytes) + as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.tag { + ::buffa::types::put_bool_field(1u32, self.tag, buf); + } + if self.max_cose_sign1_bytes != 0u64 { + ::buffa::types::put_uint64_field(2u32, self.max_cose_sign1_bytes, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.tag = ::buffa::types::decode_bool(buf)?; + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.max_cose_sign1_bytes = ::buffa::types::decode_uint64(buf)?; + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + self.tag = false; + self.max_cose_sign1_bytes = 0u64; + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseSign1Options { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseSign1Options"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseSign1Options { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_SIGN1OPTIONS_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseSign1Options", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +/// CoseSign1CreateRequest signs an attached payload. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseSign1CreateRequest { + /// Algorithm used for the protected header and signing backend. + /// + /// Field 1: `algorithm` + #[serde( + rename = "algorithm", + with = "::buffa::json_helpers::proto_enum", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_default_enum_value" + )] + pub algorithm: ::buffa::EnumValue, + /// SENSITIVE: payload bytes to embed in the COSE_Sign1 message. Payloads can + /// carry credential claims or PII and must not be logged. + /// + /// Field 2: `payload` + #[serde( + rename = "payload", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub payload: ::buffa::alloc::vec::Vec, + /// SENSITIVE: raw private key bytes for the selected algorithm. Callers must + /// keep this in a sensitive-buffer owner outside this transient protobuf + /// container. + /// + /// Field 3: `private_key` + #[serde( + rename = "privateKey", + alias = "private_key", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub private_key: ::buffa::alloc::vec::Vec, + /// Protected-header kid bytes. Ignored unless has_kid is true. + /// + /// Field 4: `kid` + #[serde( + rename = "kid", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub kid: ::buffa::alloc::vec::Vec, + /// Whether kid is present. This distinguishes an omitted kid from an + /// intentionally empty kid byte string. + /// + /// Field 5: `has_kid` + #[serde( + rename = "hasKid", + alias = "has_kid", + with = "::buffa::json_helpers::proto_bool", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_false" + )] + pub has_kid: bool, + /// Optional encoding controls. If omitted, the Rust SDK defaults apply. + /// + /// Field 6: `options` + #[serde( + rename = "options", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_unset_message_field" + )] + pub options: ::buffa::MessageField< + CoseSign1Options, + ::buffa::Inline, + >, + /// SENSITIVE: application-supplied external AAD authenticated by the + /// signature but not embedded in COSE_Sign1. + /// + /// Field 7: `external_aad` + #[serde( + rename = "externalAad", + alias = "external_aad", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub external_aad: ::buffa::alloc::vec::Vec, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseSign1CreateRequest { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseSign1CreateRequest") + .field("algorithm", &self.algorithm) + .field("payload", &"") + .field("private_key", &"") + .field("kid", &"") + .field("has_kid", &self.has_kid) + .field("options", &self.options) + .field("external_aad", &"") + .finish() + } +} +impl ::core::ops::Drop for CoseSign1CreateRequest { + fn drop(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.payload); + ::zeroize::Zeroize::zeroize(&mut self.private_key); + ::zeroize::Zeroize::zeroize(&mut self.kid); + ::zeroize::Zeroize::zeroize(&mut self.external_aad); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl<'de> ::serde::Deserialize<'de> for CoseSign1CreateRequest { + fn deserialize(deserializer: D) -> ::core::result::Result + where + D: ::serde::Deserializer<'de>, + { + fn deserialize_secret_bytes<'de, D>( + deserializer: D, + ) -> ::core::result::Result<::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, D::Error> + where + D: ::serde::Deserializer<'de>, + { + ::buffa::json_helpers::bytes::deserialize(deserializer) + .map(::zeroize::Zeroizing::new) + } + + #[derive(Default, ::serde::Deserialize)] + #[serde(default, deny_unknown_fields)] + struct Wire { + #[serde(rename = "algorithm", with = "::buffa::json_helpers::proto_enum")] + algorithm: ::buffa::EnumValue, + #[serde(rename = "payload", deserialize_with = "deserialize_secret_bytes")] + payload: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde( + rename = "privateKey", + alias = "private_key", + deserialize_with = "deserialize_secret_bytes" + )] + private_key: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "kid", deserialize_with = "deserialize_secret_bytes")] + kid: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde( + rename = "hasKid", + alias = "has_kid", + with = "::buffa::json_helpers::proto_bool" + )] + has_kid: bool, + #[serde(rename = "options")] + options: ::buffa::MessageField>, + #[serde( + rename = "externalAad", + alias = "external_aad", + deserialize_with = "deserialize_secret_bytes" + )] + external_aad: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + } + + let mut wire = Wire::deserialize(deserializer)?; + Ok(Self { + algorithm: wire.algorithm, + payload: ::core::mem::take(&mut *wire.payload), + private_key: ::core::mem::take(&mut *wire.private_key), + kid: ::core::mem::take(&mut *wire.kid), + has_kid: wire.has_kid, + options: ::core::mem::take(&mut wire.options), + external_aad: ::core::mem::take(&mut *wire.external_aad), + __buffa_unknown_fields: Default::default(), + }) + } +} +impl CoseSign1CreateRequest { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseSign1CreateRequest"; +} +::buffa::impl_default_instance!(CoseSign1CreateRequest); +impl ::buffa::MessageName for CoseSign1CreateRequest { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseSign1CreateRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseSign1CreateRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseSign1CreateRequest"; +} +impl ::buffa::Message for CoseSign1CreateRequest { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, __cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + { + let val = self.algorithm.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + if !self.payload.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.payload) as u64; + } + if !self.private_key.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.private_key) as u64; + } + if !self.kid.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.kid) as u64; + } + if self.has_kid { + size += 1u64 + ::buffa::types::BOOL_ENCODED_LEN as u64; + } + if self.options.is_set() { + let __slot = __cache.reserve(); + let inner_size = self.options.compute_size(__cache); + __cache.set(__slot, inner_size); + size + += 1u64 + ::buffa::encoding::varint_len(inner_size as u64) as u64 + + inner_size as u64; + } + if !self.external_aad.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.external_aad) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + __cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + { + let val = self.algorithm.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(1u32, val, buf); + } + } + if !self.payload.is_empty() { + ::buffa::types::put_shared_bytes_field(2u32, &self.payload, buf); + } + if !self.private_key.is_empty() { + ::buffa::types::put_shared_bytes_field(3u32, &self.private_key, buf); + } + if !self.kid.is_empty() { + ::buffa::types::put_shared_bytes_field(4u32, &self.kid, buf); + } + if self.has_kid { + ::buffa::types::put_bool_field(5u32, self.has_kid, buf); + } + if self.options.is_set() { + ::buffa::types::put_len_delimited_header( + 6u32, + u64::from(__cache.consume_next()), + buf, + ); + self.options.write_to(__cache, buf); + } + if !self.external_aad.is_empty() { + ::buffa::types::put_shared_bytes_field(7u32, &self.external_aad, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.algorithm = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(buf)?, + ); + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.payload, buf)?; + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.private_key, buf)?; + } + 4u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.kid, buf)?; + } + 5u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.has_kid = ::buffa::types::decode_bool(buf)?; + } + 6u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::Message::merge_length_delimited( + self.options.get_or_insert_default(), + buf, + ctx, + )?; + } + 7u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.external_aad, buf)?; + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + self.algorithm = ::buffa::EnumValue::from(0); + ::zeroize::Zeroize::zeroize(&mut self.payload); + ::zeroize::Zeroize::zeroize(&mut self.private_key); + ::zeroize::Zeroize::zeroize(&mut self.kid); + self.has_kid = false; + self.options = ::buffa::MessageField::none(); + ::zeroize::Zeroize::zeroize(&mut self.external_aad); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseSign1CreateRequest { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseSign1CreateRequest"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseSign1CreateRequest { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_SIGN1CREATE_REQUEST_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseSign1CreateRequest", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +/// CoseSign1CreateDetachedRequest signs a detached payload. The payload is +/// authenticated but not embedded in the resulting COSE_Sign1 message. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseSign1CreateDetachedRequest { + /// Algorithm used for the protected header and signing backend. + /// + /// Field 1: `algorithm` + #[serde( + rename = "algorithm", + with = "::buffa::json_helpers::proto_enum", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_default_enum_value" + )] + pub algorithm: ::buffa::EnumValue, + /// SENSITIVE: detached payload bytes authenticated by the signature. Payloads + /// can carry credential claims or PII and must not be logged. + /// + /// Field 2: `payload` + #[serde( + rename = "payload", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub payload: ::buffa::alloc::vec::Vec, + /// SENSITIVE: raw private key bytes for the selected algorithm. Callers must + /// keep this in a sensitive-buffer owner outside this transient protobuf + /// container. + /// + /// Field 3: `private_key` + #[serde( + rename = "privateKey", + alias = "private_key", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub private_key: ::buffa::alloc::vec::Vec, + /// Protected-header kid bytes. Ignored unless has_kid is true. + /// + /// Field 4: `kid` + #[serde( + rename = "kid", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub kid: ::buffa::alloc::vec::Vec, + /// Whether kid is present. This distinguishes an omitted kid from an + /// intentionally empty kid byte string. + /// + /// Field 5: `has_kid` + #[serde( + rename = "hasKid", + alias = "has_kid", + with = "::buffa::json_helpers::proto_bool", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_false" + )] + pub has_kid: bool, + /// Optional encoding controls. If omitted, the Rust SDK defaults apply. + /// + /// Field 6: `options` + #[serde( + rename = "options", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_unset_message_field" + )] + pub options: ::buffa::MessageField< + CoseSign1Options, + ::buffa::Inline, + >, + /// SENSITIVE: application-supplied external AAD authenticated by the + /// signature but not embedded in COSE_Sign1. + /// + /// Field 7: `external_aad` + #[serde( + rename = "externalAad", + alias = "external_aad", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub external_aad: ::buffa::alloc::vec::Vec, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseSign1CreateDetachedRequest { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseSign1CreateDetachedRequest") + .field("algorithm", &self.algorithm) + .field("payload", &"") + .field("private_key", &"") + .field("kid", &"") + .field("has_kid", &self.has_kid) + .field("options", &self.options) + .field("external_aad", &"") + .finish() + } +} +impl ::core::ops::Drop for CoseSign1CreateDetachedRequest { + fn drop(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.payload); + ::zeroize::Zeroize::zeroize(&mut self.private_key); + ::zeroize::Zeroize::zeroize(&mut self.kid); + ::zeroize::Zeroize::zeroize(&mut self.external_aad); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl<'de> ::serde::Deserialize<'de> for CoseSign1CreateDetachedRequest { + fn deserialize(deserializer: D) -> ::core::result::Result + where + D: ::serde::Deserializer<'de>, + { + fn deserialize_secret_bytes<'de, D>( + deserializer: D, + ) -> ::core::result::Result<::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, D::Error> + where + D: ::serde::Deserializer<'de>, + { + ::buffa::json_helpers::bytes::deserialize(deserializer) + .map(::zeroize::Zeroizing::new) + } + + #[derive(Default, ::serde::Deserialize)] + #[serde(default, deny_unknown_fields)] + struct Wire { + #[serde(rename = "algorithm", with = "::buffa::json_helpers::proto_enum")] + algorithm: ::buffa::EnumValue, + #[serde(rename = "payload", deserialize_with = "deserialize_secret_bytes")] + payload: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde( + rename = "privateKey", + alias = "private_key", + deserialize_with = "deserialize_secret_bytes" + )] + private_key: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "kid", deserialize_with = "deserialize_secret_bytes")] + kid: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde( + rename = "hasKid", + alias = "has_kid", + with = "::buffa::json_helpers::proto_bool" + )] + has_kid: bool, + #[serde(rename = "options")] + options: ::buffa::MessageField>, + #[serde( + rename = "externalAad", + alias = "external_aad", + deserialize_with = "deserialize_secret_bytes" + )] + external_aad: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + } + + let mut wire = Wire::deserialize(deserializer)?; + Ok(Self { + algorithm: wire.algorithm, + payload: ::core::mem::take(&mut *wire.payload), + private_key: ::core::mem::take(&mut *wire.private_key), + kid: ::core::mem::take(&mut *wire.kid), + has_kid: wire.has_kid, + options: ::core::mem::take(&mut wire.options), + external_aad: ::core::mem::take(&mut *wire.external_aad), + __buffa_unknown_fields: Default::default(), + }) + } +} +impl CoseSign1CreateDetachedRequest { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseSign1CreateDetachedRequest"; +} +::buffa::impl_default_instance!(CoseSign1CreateDetachedRequest); +impl ::buffa::MessageName for CoseSign1CreateDetachedRequest { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseSign1CreateDetachedRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseSign1CreateDetachedRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseSign1CreateDetachedRequest"; +} +impl ::buffa::Message for CoseSign1CreateDetachedRequest { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, __cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + { + let val = self.algorithm.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + if !self.payload.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.payload) as u64; + } + if !self.private_key.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.private_key) as u64; + } + if !self.kid.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.kid) as u64; + } + if self.has_kid { + size += 1u64 + ::buffa::types::BOOL_ENCODED_LEN as u64; + } + if self.options.is_set() { + let __slot = __cache.reserve(); + let inner_size = self.options.compute_size(__cache); + __cache.set(__slot, inner_size); + size + += 1u64 + ::buffa::encoding::varint_len(inner_size as u64) as u64 + + inner_size as u64; + } + if !self.external_aad.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.external_aad) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + __cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + { + let val = self.algorithm.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(1u32, val, buf); + } + } + if !self.payload.is_empty() { + ::buffa::types::put_shared_bytes_field(2u32, &self.payload, buf); + } + if !self.private_key.is_empty() { + ::buffa::types::put_shared_bytes_field(3u32, &self.private_key, buf); + } + if !self.kid.is_empty() { + ::buffa::types::put_shared_bytes_field(4u32, &self.kid, buf); + } + if self.has_kid { + ::buffa::types::put_bool_field(5u32, self.has_kid, buf); + } + if self.options.is_set() { + ::buffa::types::put_len_delimited_header( + 6u32, + u64::from(__cache.consume_next()), + buf, + ); + self.options.write_to(__cache, buf); + } + if !self.external_aad.is_empty() { + ::buffa::types::put_shared_bytes_field(7u32, &self.external_aad, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.algorithm = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(buf)?, + ); + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.payload, buf)?; + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.private_key, buf)?; + } + 4u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.kid, buf)?; + } + 5u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.has_kid = ::buffa::types::decode_bool(buf)?; + } + 6u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::Message::merge_length_delimited( + self.options.get_or_insert_default(), + buf, + ctx, + )?; + } + 7u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.external_aad, buf)?; + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + self.algorithm = ::buffa::EnumValue::from(0); + ::zeroize::Zeroize::zeroize(&mut self.payload); + ::zeroize::Zeroize::zeroize(&mut self.private_key); + ::zeroize::Zeroize::zeroize(&mut self.kid); + self.has_kid = false; + self.options = ::buffa::MessageField::none(); + ::zeroize::Zeroize::zeroize(&mut self.external_aad); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseSign1CreateDetachedRequest { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseSign1CreateDetachedRequest"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseSign1CreateDetachedRequest { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_SIGN1CREATE_DETACHED_REQUEST_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseSign1CreateDetachedRequest", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +/// CoseSign1CreateResult contains the encoded COSE_Sign1 bytes produced by a +/// signing operation. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseSign1CreateResult { + /// SENSITIVE: encoded COSE_Sign1 bytes, tagged or untagged according to + /// request options. Attached COSE_Sign1 messages contain payload bytes. + /// + /// Field 1: `cose_sign1` + #[serde( + rename = "coseSign1", + alias = "cose_sign1", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub cose_sign1: ::buffa::alloc::vec::Vec, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseSign1CreateResult { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseSign1CreateResult") + .field("cose_sign1", &"") + .finish() + } +} +impl<'de> ::serde::Deserialize<'de> for CoseSign1CreateResult { + fn deserialize(deserializer: D) -> ::core::result::Result + where + D: ::serde::Deserializer<'de>, + { + fn deserialize_secret_bytes<'de, D>( + deserializer: D, + ) -> ::core::result::Result<::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, D::Error> + where + D: ::serde::Deserializer<'de>, + { + ::buffa::json_helpers::bytes::deserialize(deserializer) + .map(::zeroize::Zeroizing::new) + } + + + #[derive(Default, ::serde::Deserialize)] + #[serde(default, deny_unknown_fields)] + struct Wire { + #[serde(rename = "coseSign1", alias = "cose_sign1", deserialize_with = "deserialize_secret_bytes")] + cose_sign1: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + } + + let mut wire = Wire::deserialize(deserializer)?; + Ok(Self { + cose_sign1: ::core::mem::take(&mut *wire.cose_sign1), + __buffa_unknown_fields: Default::default(), + }) + } +} +impl ::core::ops::Drop for CoseSign1CreateResult { + fn drop(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.cose_sign1); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl CoseSign1CreateResult { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseSign1CreateResult"; +} +::buffa::impl_default_instance!(CoseSign1CreateResult); +impl ::buffa::MessageName for CoseSign1CreateResult { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseSign1CreateResult"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseSign1CreateResult"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseSign1CreateResult"; +} +impl ::buffa::Message for CoseSign1CreateResult { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.cose_sign1.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.cose_sign1) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.cose_sign1.is_empty() { + ::buffa::types::put_shared_bytes_field(1u32, &self.cose_sign1, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.cose_sign1, buf)?; + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.cose_sign1); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseSign1CreateResult { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseSign1CreateResult"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseSign1CreateResult { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_SIGN1CREATE_RESULT_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseSign1CreateResult", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +/// CoseSign1VerifyRequest verifies a COSE_Sign1 with an attached payload. The +/// public_key field is the protobuf-boundary equivalent of the Rust SDK key +/// resolver output for the protected-header kid. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseSign1VerifyRequest { + /// SENSITIVE: encoded COSE_Sign1 bytes to verify. Tagged and untagged inputs + /// are accepted; attached messages contain payload bytes. + /// + /// Field 1: `cose_sign1` + #[serde( + rename = "coseSign1", + alias = "cose_sign1", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub cose_sign1: ::buffa::alloc::vec::Vec, + /// Raw public key bytes selected by the caller for the protected-header kid. + /// + /// Field 2: `public_key` + #[serde( + rename = "publicKey", + alias = "public_key", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub public_key: ::buffa::alloc::vec::Vec, + /// Optional maximum accepted encoded COSE_Sign1 size. A value of 0 uses the + /// crate default. + /// + /// Field 3: `max_cose_sign1_bytes` + #[serde( + rename = "maxCoseSign1Bytes", + alias = "max_cose_sign1_bytes", + with = "::buffa::json_helpers::uint64", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_zero_u64" + )] + pub max_cose_sign1_bytes: u64, + /// Reserved for shape parity with detached verification. A value of 0 uses the + /// crate default; attached verification does not consume a detached payload. + /// + /// Field 4: `max_detached_payload_bytes` + #[serde( + rename = "maxDetachedPayloadBytes", + alias = "max_detached_payload_bytes", + with = "::buffa::json_helpers::uint64", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_zero_u64" + )] + pub max_detached_payload_bytes: u64, + /// Require a non-empty protected-header kid before verification. + /// + /// Field 5: `require_kid` + #[serde( + rename = "requireKid", + alias = "require_kid", + with = "::buffa::json_helpers::proto_bool", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_false" + )] + pub require_kid: bool, + /// Optional algorithm allow-list. Empty means any COSE algorithm supported by + /// this crate and operation. + /// + /// Field 6: `allowed_algorithms` + #[serde( + rename = "allowedAlgorithms", + alias = "allowed_algorithms", + with = "::buffa::json_helpers::repeated_enum", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_vec" + )] + pub allowed_algorithms: ::buffa::alloc::vec::Vec< + ::buffa::EnumValue, + >, + /// SENSITIVE: external AAD supplied by the application for verification. + /// + /// Field 7: `external_aad` + #[serde( + rename = "externalAad", + alias = "external_aad", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub external_aad: ::buffa::alloc::vec::Vec, + /// Optional trusted key-selection identifier. When non-empty, the + /// protected-header kid must match this value before public_key can be + /// returned by the adapter's resolver. Empty preserves resolver-style use + /// where public_key was already selected outside this request. + /// Selection of this value belongs to the caller's trust-store boundary. + /// + /// Field 8: `expected_kid` + #[serde( + rename = "expectedKid", + alias = "expected_kid", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub expected_kid: ::buffa::alloc::vec::Vec, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseSign1VerifyRequest { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseSign1VerifyRequest") + .field("cose_sign1", &"") + .field("public_key", &"") + .field("max_cose_sign1_bytes", &self.max_cose_sign1_bytes) + .field("max_detached_payload_bytes", &self.max_detached_payload_bytes) + .field("require_kid", &self.require_kid) + .field("allowed_algorithms", &self.allowed_algorithms) + .field("external_aad", &"") + .field("expected_kid", &"") + .finish() + } +} +impl<'de> ::serde::Deserialize<'de> for CoseSign1VerifyRequest { + fn deserialize(deserializer: D) -> ::core::result::Result + where + D: ::serde::Deserializer<'de>, + { + fn deserialize_secret_bytes<'de, D>( + deserializer: D, + ) -> ::core::result::Result<::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, D::Error> + where + D: ::serde::Deserializer<'de>, + { + ::buffa::json_helpers::bytes::deserialize(deserializer) + .map(::zeroize::Zeroizing::new) + } + + + #[derive(Default, ::serde::Deserialize)] + #[serde(default, deny_unknown_fields)] + struct Wire { + #[serde(rename = "coseSign1", alias = "cose_sign1", deserialize_with = "deserialize_secret_bytes")] + cose_sign1: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "publicKey", alias = "public_key", deserialize_with = "deserialize_secret_bytes")] + public_key: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "maxCoseSign1Bytes", alias = "max_cose_sign1_bytes", with = "::buffa::json_helpers::uint64")] + max_cose_sign1_bytes: u64, + #[serde(rename = "maxDetachedPayloadBytes", alias = "max_detached_payload_bytes", with = "::buffa::json_helpers::uint64")] + max_detached_payload_bytes: u64, + #[serde(rename = "requireKid", alias = "require_kid", with = "::buffa::json_helpers::proto_bool")] + require_kid: bool, + #[serde(rename = "allowedAlgorithms", alias = "allowed_algorithms", with = "::buffa::json_helpers::repeated_enum")] + allowed_algorithms: ::buffa::alloc::vec::Vec<::buffa::EnumValue>, + #[serde(rename = "externalAad", alias = "external_aad", deserialize_with = "deserialize_secret_bytes")] + external_aad: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "expectedKid", alias = "expected_kid", deserialize_with = "deserialize_secret_bytes")] + expected_kid: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + } + + let mut wire = Wire::deserialize(deserializer)?; + Ok(Self { + cose_sign1: ::core::mem::take(&mut *wire.cose_sign1), + public_key: ::core::mem::take(&mut *wire.public_key), + max_cose_sign1_bytes: wire.max_cose_sign1_bytes, + max_detached_payload_bytes: wire.max_detached_payload_bytes, + require_kid: wire.require_kid, + allowed_algorithms: ::core::mem::take(&mut wire.allowed_algorithms), + external_aad: ::core::mem::take(&mut *wire.external_aad), + expected_kid: ::core::mem::take(&mut *wire.expected_kid), + __buffa_unknown_fields: Default::default(), + }) + } +} +impl ::core::ops::Drop for CoseSign1VerifyRequest { + fn drop(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.cose_sign1); + ::zeroize::Zeroize::zeroize(&mut self.public_key); + ::zeroize::Zeroize::zeroize(&mut self.external_aad); + ::zeroize::Zeroize::zeroize(&mut self.expected_kid); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl CoseSign1VerifyRequest { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseSign1VerifyRequest"; +} +::buffa::impl_default_instance!(CoseSign1VerifyRequest); +impl ::buffa::MessageName for CoseSign1VerifyRequest { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseSign1VerifyRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseSign1VerifyRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseSign1VerifyRequest"; +} +impl ::buffa::Message for CoseSign1VerifyRequest { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.cose_sign1.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.cose_sign1) as u64; + } + if !self.public_key.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.public_key) as u64; + } + if self.max_cose_sign1_bytes != 0u64 { + size + += 1u64 + + ::buffa::types::uint64_encoded_len(self.max_cose_sign1_bytes) + as u64; + } + if self.max_detached_payload_bytes != 0u64 { + size + += 1u64 + + ::buffa::types::uint64_encoded_len(self.max_detached_payload_bytes) + as u64; + } + if self.require_kid { + size += 1u64 + ::buffa::types::BOOL_ENCODED_LEN as u64; + } + if !self.allowed_algorithms.is_empty() { + let payload: u64 = self + .allowed_algorithms + .iter() + .map(|v| ::buffa::types::int32_encoded_len(v.to_i32()) as u64) + .sum::(); + size += 1u64 + ::buffa::encoding::varint_len(payload) as u64 + payload; + } + if !self.external_aad.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.external_aad) as u64; + } + if !self.expected_kid.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.expected_kid) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.cose_sign1.is_empty() { + ::buffa::types::put_shared_bytes_field(1u32, &self.cose_sign1, buf); + } + if !self.public_key.is_empty() { + ::buffa::types::put_shared_bytes_field(2u32, &self.public_key, buf); + } + if self.max_cose_sign1_bytes != 0u64 { + ::buffa::types::put_uint64_field(3u32, self.max_cose_sign1_bytes, buf); + } + if self.max_detached_payload_bytes != 0u64 { + ::buffa::types::put_uint64_field(4u32, self.max_detached_payload_bytes, buf); + } + if self.require_kid { + ::buffa::types::put_bool_field(5u32, self.require_kid, buf); + } + if !self.allowed_algorithms.is_empty() { + let payload: u64 = self + .allowed_algorithms + .iter() + .map(|v| ::buffa::types::int32_encoded_len(v.to_i32()) as u64) + .sum::(); + ::buffa::types::put_len_delimited_header(6u32, payload, buf); + for v in &self.allowed_algorithms { + ::buffa::types::encode_int32(v.to_i32(), buf); + } + } + if !self.external_aad.is_empty() { + ::buffa::types::put_shared_bytes_field(7u32, &self.external_aad, buf); + } + if !self.expected_kid.is_empty() { + ::buffa::types::put_shared_bytes_field(8u32, &self.expected_kid, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.cose_sign1, buf)?; + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.public_key, buf)?; + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.max_cose_sign1_bytes = ::buffa::types::decode_uint64(buf)?; + } + 4u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.max_detached_payload_bytes = ::buffa::types::decode_uint64(buf)?; + } + 5u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.require_kid = ::buffa::types::decode_bool(buf)?; + } + 6u32 => { + if tag.wire_type() == ::buffa::encoding::WireType::LengthDelimited { + let len = ::buffa::encoding::decode_varint(buf)?; + let len = usize::try_from(len) + .map_err(|_| ::buffa::DecodeError::MessageTooLarge)?; + if buf.remaining() < len { + return ::core::result::Result::Err( + ::buffa::DecodeError::UnexpectedEof, + ); + } + self.allowed_algorithms.reserve(len); + let mut limited = buf.take(len); + while limited.has_remaining() { + self.allowed_algorithms + .push( + ::buffa::EnumValue::from( + ::buffa::types::decode_int32_packed(&mut limited)?, + ), + ); + } + let leftover = limited.remaining(); + if leftover > 0 { + limited.advance(leftover); + } + } else if tag.wire_type() == ::buffa::encoding::WireType::Varint { + self.allowed_algorithms + .push( + ::buffa::EnumValue::from(::buffa::types::decode_int32(buf)?), + ); + } else { + return ::core::result::Result::Err( + ::buffa::encoding::wire_type_mismatch( + tag, + ::buffa::encoding::WireType::LengthDelimited, + ), + ); + } + } + 7u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.external_aad, buf)?; + } + 8u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.expected_kid, buf)?; + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.cose_sign1); + ::zeroize::Zeroize::zeroize(&mut self.public_key); + self.max_cose_sign1_bytes = 0u64; + self.max_detached_payload_bytes = 0u64; + self.require_kid = false; + self.allowed_algorithms.clear(); + ::zeroize::Zeroize::zeroize(&mut self.external_aad); + ::zeroize::Zeroize::zeroize(&mut self.expected_kid); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseSign1VerifyRequest { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseSign1VerifyRequest"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseSign1VerifyRequest { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_SIGN1VERIFY_REQUEST_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseSign1VerifyRequest", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +/// CoseSign1VerifyDetachedRequest verifies a COSE_Sign1 whose payload is carried +/// out of band. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseSign1VerifyDetachedRequest { + /// SENSITIVE: encoded COSE_Sign1 bytes to verify. Tagged and untagged inputs + /// are accepted. + /// + /// Field 1: `cose_sign1` + #[serde( + rename = "coseSign1", + alias = "cose_sign1", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub cose_sign1: ::buffa::alloc::vec::Vec, + /// SENSITIVE: detached payload bytes that must match the signature. Payloads + /// can carry credential claims or PII and must not be logged. + /// + /// Field 2: `payload` + #[serde( + rename = "payload", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub payload: ::buffa::alloc::vec::Vec, + /// Raw public key bytes selected by the caller for the protected-header kid. + /// + /// Field 3: `public_key` + #[serde( + rename = "publicKey", + alias = "public_key", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub public_key: ::buffa::alloc::vec::Vec, + /// Optional maximum accepted encoded COSE_Sign1 size. A value of 0 uses the + /// crate default. + /// + /// Field 4: `max_cose_sign1_bytes` + #[serde( + rename = "maxCoseSign1Bytes", + alias = "max_cose_sign1_bytes", + with = "::buffa::json_helpers::uint64", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_zero_u64" + )] + pub max_cose_sign1_bytes: u64, + /// Optional maximum accepted detached payload size. A value of 0 uses the + /// crate default. + /// + /// Field 5: `max_detached_payload_bytes` + #[serde( + rename = "maxDetachedPayloadBytes", + alias = "max_detached_payload_bytes", + with = "::buffa::json_helpers::uint64", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_zero_u64" + )] + pub max_detached_payload_bytes: u64, + /// Require a non-empty protected-header kid before verification. + /// + /// Field 6: `require_kid` + #[serde( + rename = "requireKid", + alias = "require_kid", + with = "::buffa::json_helpers::proto_bool", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_false" + )] + pub require_kid: bool, + /// Optional algorithm allow-list. Empty means any COSE algorithm supported by + /// this crate and operation. + /// + /// Field 7: `allowed_algorithms` + #[serde( + rename = "allowedAlgorithms", + alias = "allowed_algorithms", + with = "::buffa::json_helpers::repeated_enum", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_vec" + )] + pub allowed_algorithms: ::buffa::alloc::vec::Vec< + ::buffa::EnumValue, + >, + /// SENSITIVE: external AAD supplied by the application for verification. + /// + /// Field 8: `external_aad` + #[serde( + rename = "externalAad", + alias = "external_aad", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub external_aad: ::buffa::alloc::vec::Vec, + /// Optional trusted key-selection identifier. When non-empty, the + /// protected-header kid must match this value before public_key can be + /// returned by the adapter's resolver. Empty preserves resolver-style use + /// where public_key was already selected outside this request. + /// Selection of this value belongs to the caller's trust-store boundary. + /// + /// Field 9: `expected_kid` + #[serde( + rename = "expectedKid", + alias = "expected_kid", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub expected_kid: ::buffa::alloc::vec::Vec, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseSign1VerifyDetachedRequest { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseSign1VerifyDetachedRequest") + .field("cose_sign1", &"") + .field("payload", &"") + .field("public_key", &"") + .field("max_cose_sign1_bytes", &self.max_cose_sign1_bytes) + .field("max_detached_payload_bytes", &self.max_detached_payload_bytes) + .field("require_kid", &self.require_kid) + .field("allowed_algorithms", &self.allowed_algorithms) + .field("external_aad", &"") + .field("expected_kid", &"") + .finish() + } +} +impl<'de> ::serde::Deserialize<'de> for CoseSign1VerifyDetachedRequest { + fn deserialize(deserializer: D) -> ::core::result::Result + where + D: ::serde::Deserializer<'de>, + { + fn deserialize_secret_bytes<'de, D>( + deserializer: D, + ) -> ::core::result::Result<::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, D::Error> + where + D: ::serde::Deserializer<'de>, + { + ::buffa::json_helpers::bytes::deserialize(deserializer) + .map(::zeroize::Zeroizing::new) + } + + + #[derive(Default, ::serde::Deserialize)] + #[serde(default, deny_unknown_fields)] + struct Wire { + #[serde(rename = "coseSign1", alias = "cose_sign1", deserialize_with = "deserialize_secret_bytes")] + cose_sign1: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "payload", deserialize_with = "deserialize_secret_bytes")] + payload: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "publicKey", alias = "public_key", deserialize_with = "deserialize_secret_bytes")] + public_key: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "maxCoseSign1Bytes", alias = "max_cose_sign1_bytes", with = "::buffa::json_helpers::uint64")] + max_cose_sign1_bytes: u64, + #[serde(rename = "maxDetachedPayloadBytes", alias = "max_detached_payload_bytes", with = "::buffa::json_helpers::uint64")] + max_detached_payload_bytes: u64, + #[serde(rename = "requireKid", alias = "require_kid", with = "::buffa::json_helpers::proto_bool")] + require_kid: bool, + #[serde(rename = "allowedAlgorithms", alias = "allowed_algorithms", with = "::buffa::json_helpers::repeated_enum")] + allowed_algorithms: ::buffa::alloc::vec::Vec<::buffa::EnumValue>, + #[serde(rename = "externalAad", alias = "external_aad", deserialize_with = "deserialize_secret_bytes")] + external_aad: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "expectedKid", alias = "expected_kid", deserialize_with = "deserialize_secret_bytes")] + expected_kid: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + } + + let mut wire = Wire::deserialize(deserializer)?; + Ok(Self { + cose_sign1: ::core::mem::take(&mut *wire.cose_sign1), + payload: ::core::mem::take(&mut *wire.payload), + public_key: ::core::mem::take(&mut *wire.public_key), + max_cose_sign1_bytes: wire.max_cose_sign1_bytes, + max_detached_payload_bytes: wire.max_detached_payload_bytes, + require_kid: wire.require_kid, + allowed_algorithms: ::core::mem::take(&mut wire.allowed_algorithms), + external_aad: ::core::mem::take(&mut *wire.external_aad), + expected_kid: ::core::mem::take(&mut *wire.expected_kid), + __buffa_unknown_fields: Default::default(), + }) + } +} +impl ::core::ops::Drop for CoseSign1VerifyDetachedRequest { + fn drop(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.cose_sign1); + ::zeroize::Zeroize::zeroize(&mut self.payload); + ::zeroize::Zeroize::zeroize(&mut self.public_key); + ::zeroize::Zeroize::zeroize(&mut self.external_aad); + ::zeroize::Zeroize::zeroize(&mut self.expected_kid); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl CoseSign1VerifyDetachedRequest { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseSign1VerifyDetachedRequest"; +} +::buffa::impl_default_instance!(CoseSign1VerifyDetachedRequest); +impl ::buffa::MessageName for CoseSign1VerifyDetachedRequest { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseSign1VerifyDetachedRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseSign1VerifyDetachedRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseSign1VerifyDetachedRequest"; +} +impl ::buffa::Message for CoseSign1VerifyDetachedRequest { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.cose_sign1.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.cose_sign1) as u64; + } + if !self.payload.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.payload) as u64; + } + if !self.public_key.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.public_key) as u64; + } + if self.max_cose_sign1_bytes != 0u64 { + size + += 1u64 + + ::buffa::types::uint64_encoded_len(self.max_cose_sign1_bytes) + as u64; + } + if self.max_detached_payload_bytes != 0u64 { + size + += 1u64 + + ::buffa::types::uint64_encoded_len(self.max_detached_payload_bytes) + as u64; + } + if self.require_kid { + size += 1u64 + ::buffa::types::BOOL_ENCODED_LEN as u64; + } + if !self.allowed_algorithms.is_empty() { + let payload: u64 = self + .allowed_algorithms + .iter() + .map(|v| ::buffa::types::int32_encoded_len(v.to_i32()) as u64) + .sum::(); + size += 1u64 + ::buffa::encoding::varint_len(payload) as u64 + payload; + } + if !self.external_aad.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.external_aad) as u64; + } + if !self.expected_kid.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.expected_kid) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.cose_sign1.is_empty() { + ::buffa::types::put_shared_bytes_field(1u32, &self.cose_sign1, buf); + } + if !self.payload.is_empty() { + ::buffa::types::put_shared_bytes_field(2u32, &self.payload, buf); + } + if !self.public_key.is_empty() { + ::buffa::types::put_shared_bytes_field(3u32, &self.public_key, buf); + } + if self.max_cose_sign1_bytes != 0u64 { + ::buffa::types::put_uint64_field(4u32, self.max_cose_sign1_bytes, buf); + } + if self.max_detached_payload_bytes != 0u64 { + ::buffa::types::put_uint64_field(5u32, self.max_detached_payload_bytes, buf); + } + if self.require_kid { + ::buffa::types::put_bool_field(6u32, self.require_kid, buf); + } + if !self.allowed_algorithms.is_empty() { + let payload: u64 = self + .allowed_algorithms + .iter() + .map(|v| ::buffa::types::int32_encoded_len(v.to_i32()) as u64) + .sum::(); + ::buffa::types::put_len_delimited_header(7u32, payload, buf); + for v in &self.allowed_algorithms { + ::buffa::types::encode_int32(v.to_i32(), buf); + } + } + if !self.external_aad.is_empty() { + ::buffa::types::put_shared_bytes_field(8u32, &self.external_aad, buf); + } + if !self.expected_kid.is_empty() { + ::buffa::types::put_shared_bytes_field(9u32, &self.expected_kid, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.cose_sign1, buf)?; + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.payload, buf)?; + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.public_key, buf)?; + } + 4u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.max_cose_sign1_bytes = ::buffa::types::decode_uint64(buf)?; + } + 5u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.max_detached_payload_bytes = ::buffa::types::decode_uint64(buf)?; + } + 6u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.require_kid = ::buffa::types::decode_bool(buf)?; + } + 7u32 => { + if tag.wire_type() == ::buffa::encoding::WireType::LengthDelimited { + let len = ::buffa::encoding::decode_varint(buf)?; + let len = usize::try_from(len) + .map_err(|_| ::buffa::DecodeError::MessageTooLarge)?; + if buf.remaining() < len { + return ::core::result::Result::Err( + ::buffa::DecodeError::UnexpectedEof, + ); + } + self.allowed_algorithms.reserve(len); + let mut limited = buf.take(len); + while limited.has_remaining() { + self.allowed_algorithms + .push( + ::buffa::EnumValue::from( + ::buffa::types::decode_int32_packed(&mut limited)?, + ), + ); + } + let leftover = limited.remaining(); + if leftover > 0 { + limited.advance(leftover); + } + } else if tag.wire_type() == ::buffa::encoding::WireType::Varint { + self.allowed_algorithms + .push( + ::buffa::EnumValue::from(::buffa::types::decode_int32(buf)?), + ); + } else { + return ::core::result::Result::Err( + ::buffa::encoding::wire_type_mismatch( + tag, + ::buffa::encoding::WireType::LengthDelimited, + ), + ); + } + } + 8u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.external_aad, buf)?; + } + 9u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.expected_kid, buf)?; + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.cose_sign1); + ::zeroize::Zeroize::zeroize(&mut self.payload); + ::zeroize::Zeroize::zeroize(&mut self.public_key); + self.max_cose_sign1_bytes = 0u64; + self.max_detached_payload_bytes = 0u64; + self.require_kid = false; + self.allowed_algorithms.clear(); + ::zeroize::Zeroize::zeroize(&mut self.external_aad); + ::zeroize::Zeroize::zeroize(&mut self.expected_kid); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseSign1VerifyDetachedRequest { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseSign1VerifyDetachedRequest"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseSign1VerifyDetachedRequest { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_SIGN1VERIFY_DETACHED_REQUEST_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseSign1VerifyDetachedRequest", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +/// CoseSign1VerifyResult contains the verified attached payload and protected +/// header metadata. Detached verification returns an empty payload field. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseSign1VerifyResult { + /// SENSITIVE: verified attached payload bytes, or empty for detached + /// verification. Payloads can carry credential claims or PII and must not be + /// logged. + /// + /// Field 1: `payload` + #[serde( + rename = "payload", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub payload: ::buffa::alloc::vec::Vec, + /// Verified protected-header algorithm. + /// + /// Field 2: `algorithm` + #[serde( + rename = "algorithm", + with = "::buffa::json_helpers::proto_enum", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_default_enum_value" + )] + pub algorithm: ::buffa::EnumValue, + /// Verified protected-header kid bytes. + /// + /// Field 3: `kid` + #[serde( + rename = "kid", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub kid: ::buffa::alloc::vec::Vec, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseSign1VerifyResult { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseSign1VerifyResult") + .field("payload", &"") + .field("algorithm", &self.algorithm) + .field("kid", &"") + .finish() + } +} +impl<'de> ::serde::Deserialize<'de> for CoseSign1VerifyResult { + fn deserialize(deserializer: D) -> ::core::result::Result + where + D: ::serde::Deserializer<'de>, + { + fn deserialize_secret_bytes<'de, D>( + deserializer: D, + ) -> ::core::result::Result<::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, D::Error> + where + D: ::serde::Deserializer<'de>, + { + ::buffa::json_helpers::bytes::deserialize(deserializer) + .map(::zeroize::Zeroizing::new) + } + + + #[derive(Default, ::serde::Deserialize)] + #[serde(default, deny_unknown_fields)] + struct Wire { + #[serde(rename = "payload", deserialize_with = "deserialize_secret_bytes")] + payload: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "algorithm", with = "::buffa::json_helpers::proto_enum")] + algorithm: ::buffa::EnumValue, + #[serde(rename = "kid", deserialize_with = "deserialize_secret_bytes")] + kid: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + } + + let mut wire = Wire::deserialize(deserializer)?; + Ok(Self { + payload: ::core::mem::take(&mut *wire.payload), + algorithm: wire.algorithm, + kid: ::core::mem::take(&mut *wire.kid), + __buffa_unknown_fields: Default::default(), + }) + } +} +impl ::core::ops::Drop for CoseSign1VerifyResult { + fn drop(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.payload); + ::zeroize::Zeroize::zeroize(&mut self.kid); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl CoseSign1VerifyResult { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseSign1VerifyResult"; +} +::buffa::impl_default_instance!(CoseSign1VerifyResult); +impl ::buffa::MessageName for CoseSign1VerifyResult { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseSign1VerifyResult"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseSign1VerifyResult"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseSign1VerifyResult"; +} +impl ::buffa::Message for CoseSign1VerifyResult { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.payload.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.payload) as u64; + } + { + let val = self.algorithm.to_i32(); + if val != 0 { + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; + } + } + if !self.kid.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.kid) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.payload.is_empty() { + ::buffa::types::put_shared_bytes_field(1u32, &self.payload, buf); + } + { + let val = self.algorithm.to_i32(); + if val != 0 { + ::buffa::types::put_int32_field(2u32, val, buf); + } + } + if !self.kid.is_empty() { + ::buffa::types::put_shared_bytes_field(3u32, &self.kid, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.payload, buf)?; + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.algorithm = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(buf)?, + ); + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.kid, buf)?; + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.payload); + self.algorithm = ::buffa::EnumValue::from(0); + ::zeroize::Zeroize::zeroize(&mut self.kid); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseSign1VerifyResult { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseSign1VerifyResult"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseSign1VerifyResult { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_SIGN1VERIFY_RESULT_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseSign1VerifyResult", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +/// CoseKeyFromPublicBytesRequest builds a public COSE_Key from raw public key +/// bytes and an explicit algorithm binding. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseKeyFromPublicBytesRequest { + /// Algorithm that determines the COSE_Key kty/crv/alg profile. + /// + /// Field 1: `algorithm` + #[serde( + rename = "algorithm", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_unset_message_field" + )] + pub algorithm: ::buffa::MessageField< + CoseAlgorithmIdentifier, + ::buffa::Inline, + >, + /// Raw public key bytes for the selected algorithm. + /// + /// Field 2: `public_key` + #[serde( + rename = "publicKey", + alias = "public_key", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub public_key: ::buffa::alloc::vec::Vec, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseKeyFromPublicBytesRequest { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseKeyFromPublicBytesRequest") + .field("algorithm", &self.algorithm) + .field("public_key", &"") + .finish() + } +} +impl<'de> ::serde::Deserialize<'de> for CoseKeyFromPublicBytesRequest { + fn deserialize(deserializer: D) -> ::core::result::Result + where + D: ::serde::Deserializer<'de>, + { + fn deserialize_secret_bytes<'de, D>( + deserializer: D, + ) -> ::core::result::Result<::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, D::Error> + where + D: ::serde::Deserializer<'de>, + { + ::buffa::json_helpers::bytes::deserialize(deserializer) + .map(::zeroize::Zeroizing::new) + } + + + #[derive(Default, ::serde::Deserialize)] + #[serde(default, deny_unknown_fields)] + struct Wire { + #[serde(rename = "algorithm")] + algorithm: ::buffa::MessageField>, + #[serde(rename = "publicKey", alias = "public_key", deserialize_with = "deserialize_secret_bytes")] + public_key: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + } + + let mut wire = Wire::deserialize(deserializer)?; + Ok(Self { + algorithm: ::core::mem::take(&mut wire.algorithm), + public_key: ::core::mem::take(&mut *wire.public_key), + __buffa_unknown_fields: Default::default(), + }) + } +} +impl ::core::ops::Drop for CoseKeyFromPublicBytesRequest { + fn drop(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.public_key); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl CoseKeyFromPublicBytesRequest { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseKeyFromPublicBytesRequest"; +} +::buffa::impl_default_instance!(CoseKeyFromPublicBytesRequest); +impl ::buffa::MessageName for CoseKeyFromPublicBytesRequest { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseKeyFromPublicBytesRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseKeyFromPublicBytesRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseKeyFromPublicBytesRequest"; +} +impl ::buffa::Message for CoseKeyFromPublicBytesRequest { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, __cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if self.algorithm.is_set() { + let __slot = __cache.reserve(); + let inner_size = self.algorithm.compute_size(__cache); + __cache.set(__slot, inner_size); + size + += 1u64 + ::buffa::encoding::varint_len(inner_size as u64) as u64 + + inner_size as u64; + } + if !self.public_key.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.public_key) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + __cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.algorithm.is_set() { + ::buffa::types::put_len_delimited_header( + 1u32, + u64::from(__cache.consume_next()), + buf, + ); + self.algorithm.write_to(__cache, buf); + } + if !self.public_key.is_empty() { + ::buffa::types::put_shared_bytes_field(2u32, &self.public_key, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::Message::merge_length_delimited( + self.algorithm.get_or_insert_default(), + buf, + ctx, + )?; + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.public_key, buf)?; + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + self.algorithm = ::buffa::MessageField::none(); + ::zeroize::Zeroize::zeroize(&mut self.public_key); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseKeyFromPublicBytesRequest { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseKeyFromPublicBytesRequest"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseKeyFromPublicBytesRequest { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_KEY_FROM_PUBLIC_BYTES_REQUEST_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseKeyFromPublicBytesRequest", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +/// CoseKeyFromPrivateBytesRequest builds a private COSE_Key from raw private key +/// bytes and matching public key bytes. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseKeyFromPrivateBytesRequest { + /// Algorithm that determines the COSE_Key kty/crv/alg profile. + /// + /// Field 1: `algorithm` + #[serde( + rename = "algorithm", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_unset_message_field" + )] + pub algorithm: ::buffa::MessageField< + CoseAlgorithmIdentifier, + ::buffa::Inline, + >, + /// SENSITIVE: raw private key bytes. Callers must keep this in a + /// sensitive-buffer owner outside this transient protobuf container. + /// + /// Field 2: `private_key` + #[serde( + rename = "privateKey", + alias = "private_key", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub private_key: ::buffa::alloc::vec::Vec, + /// Matching raw public key bytes. Ignored unless has_public_key is true; + /// supported private-key constructions reject requests that omit it. + /// + /// Field 3: `public_key` + #[serde( + rename = "publicKey", + alias = "public_key", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub public_key: ::buffa::alloc::vec::Vec, + /// Whether public_key is present. + /// + /// Field 4: `has_public_key` + #[serde( + rename = "hasPublicKey", + alias = "has_public_key", + with = "::buffa::json_helpers::proto_bool", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_false" + )] + pub has_public_key: bool, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseKeyFromPrivateBytesRequest { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseKeyFromPrivateBytesRequest") + .field("algorithm", &self.algorithm) + .field("private_key", &"") + .field("public_key", &"") + .field("has_public_key", &self.has_public_key) + .finish() + } +} +impl ::core::ops::Drop for CoseKeyFromPrivateBytesRequest { + fn drop(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.private_key); + ::zeroize::Zeroize::zeroize(&mut self.public_key); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl<'de> ::serde::Deserialize<'de> for CoseKeyFromPrivateBytesRequest { + fn deserialize(deserializer: D) -> ::core::result::Result + where + D: ::serde::Deserializer<'de>, + { + fn deserialize_secret_bytes<'de, D>( + deserializer: D, + ) -> ::core::result::Result<::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, D::Error> + where + D: ::serde::Deserializer<'de>, + { + ::buffa::json_helpers::bytes::deserialize(deserializer) + .map(::zeroize::Zeroizing::new) + } + + #[derive(Default, ::serde::Deserialize)] + #[serde(default, deny_unknown_fields)] + struct Wire { + #[serde(rename = "algorithm")] + algorithm: ::buffa::MessageField>, + #[serde( + rename = "privateKey", + alias = "private_key", + deserialize_with = "deserialize_secret_bytes" + )] + private_key: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde( + rename = "publicKey", + alias = "public_key", + deserialize_with = "deserialize_secret_bytes" + )] + public_key: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde( + rename = "hasPublicKey", + alias = "has_public_key", + with = "::buffa::json_helpers::proto_bool" + )] + has_public_key: bool, + } + + let mut wire = Wire::deserialize(deserializer)?; + Ok(Self { + algorithm: wire.algorithm, + private_key: ::core::mem::take(&mut *wire.private_key), + public_key: ::core::mem::take(&mut *wire.public_key), + has_public_key: wire.has_public_key, + __buffa_unknown_fields: Default::default(), + }) + } +} +impl CoseKeyFromPrivateBytesRequest { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseKeyFromPrivateBytesRequest"; +} +::buffa::impl_default_instance!(CoseKeyFromPrivateBytesRequest); +impl ::buffa::MessageName for CoseKeyFromPrivateBytesRequest { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseKeyFromPrivateBytesRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseKeyFromPrivateBytesRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseKeyFromPrivateBytesRequest"; +} +impl ::buffa::Message for CoseKeyFromPrivateBytesRequest { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, __cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if self.algorithm.is_set() { + let __slot = __cache.reserve(); + let inner_size = self.algorithm.compute_size(__cache); + __cache.set(__slot, inner_size); + size + += 1u64 + ::buffa::encoding::varint_len(inner_size as u64) as u64 + + inner_size as u64; + } + if !self.private_key.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.private_key) as u64; + } + if !self.public_key.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.public_key) as u64; + } + if self.has_public_key { + size += 1u64 + ::buffa::types::BOOL_ENCODED_LEN as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + __cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if self.algorithm.is_set() { + ::buffa::types::put_len_delimited_header( + 1u32, + u64::from(__cache.consume_next()), + buf, + ); + self.algorithm.write_to(__cache, buf); + } + if !self.private_key.is_empty() { + ::buffa::types::put_shared_bytes_field(2u32, &self.private_key, buf); + } + if !self.public_key.is_empty() { + ::buffa::types::put_shared_bytes_field(3u32, &self.public_key, buf); + } + if self.has_public_key { + ::buffa::types::put_bool_field(4u32, self.has_public_key, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::Message::merge_length_delimited( + self.algorithm.get_or_insert_default(), + buf, + ctx, + )?; + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.private_key, buf)?; + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.public_key, buf)?; + } + 4u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + self.has_public_key = ::buffa::types::decode_bool(buf)?; + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + self.algorithm = ::buffa::MessageField::none(); + ::zeroize::Zeroize::zeroize(&mut self.private_key); + ::zeroize::Zeroize::zeroize(&mut self.public_key); + self.has_public_key = false; + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseKeyFromPrivateBytesRequest { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseKeyFromPrivateBytesRequest"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseKeyFromPrivateBytesRequest { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_KEY_FROM_PRIVATE_BYTES_REQUEST_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseKeyFromPrivateBytesRequest", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +/// CoseKeyBytesRequest carries encoded COSE_Key bytes for parse, extraction, +/// deterministic kid derivation, and Multikey conversion operations. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseKeyBytesRequest { + /// SENSITIVE: encoded COSE_Key bytes. This may contain private key material + /// for private extraction; wipe transient protobuf containers as soon as + /// practical. + /// + /// Field 1: `cose_key` + #[serde( + rename = "coseKey", + alias = "cose_key", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub cose_key: ::buffa::alloc::vec::Vec, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseKeyBytesRequest { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseKeyBytesRequest").field("cose_key", &"").finish() + } +} +impl<'de> ::serde::Deserialize<'de> for CoseKeyBytesRequest { + fn deserialize(deserializer: D) -> ::core::result::Result + where + D: ::serde::Deserializer<'de>, + { + fn deserialize_secret_bytes<'de, D>( + deserializer: D, + ) -> ::core::result::Result<::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, D::Error> + where + D: ::serde::Deserializer<'de>, + { + ::buffa::json_helpers::bytes::deserialize(deserializer) + .map(::zeroize::Zeroizing::new) + } + + + #[derive(Default, ::serde::Deserialize)] + #[serde(default, deny_unknown_fields)] + struct Wire { + #[serde(rename = "coseKey", alias = "cose_key", deserialize_with = "deserialize_secret_bytes")] + cose_key: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + } + + let mut wire = Wire::deserialize(deserializer)?; + Ok(Self { + cose_key: ::core::mem::take(&mut *wire.cose_key), + __buffa_unknown_fields: Default::default(), + }) + } +} +impl ::core::ops::Drop for CoseKeyBytesRequest { + fn drop(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.cose_key); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl CoseKeyBytesRequest { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseKeyBytesRequest"; +} +::buffa::impl_default_instance!(CoseKeyBytesRequest); +impl ::buffa::MessageName for CoseKeyBytesRequest { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseKeyBytesRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseKeyBytesRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseKeyBytesRequest"; +} +impl ::buffa::Message for CoseKeyBytesRequest { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.cose_key.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.cose_key) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.cose_key.is_empty() { + ::buffa::types::put_shared_bytes_field(1u32, &self.cose_key, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.cose_key, buf)?; + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.cose_key); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseKeyBytesRequest { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseKeyBytesRequest"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseKeyBytesRequest { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_KEY_BYTES_REQUEST_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseKeyBytesRequest", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +/// CoseKeyBytesResult carries encoded COSE_Key bytes or raw key/kid bytes, +/// depending on the operation-specific result envelope. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseKeyBytesResult { + /// SENSITIVE: operation-specific bytes. For private-key extraction this + /// contains private key material and must be moved into a sensitive-buffer + /// owner by receivers. + /// + /// Field 1: `key_bytes` + #[serde( + rename = "keyBytes", + alias = "key_bytes", + with = "::buffa::json_helpers::bytes", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_bytes" + )] + pub key_bytes: ::buffa::alloc::vec::Vec, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseKeyBytesResult { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseKeyBytesResult").field("key_bytes", &"").finish() + } +} +impl<'de> ::serde::Deserialize<'de> for CoseKeyBytesResult { + fn deserialize(deserializer: D) -> ::core::result::Result + where + D: ::serde::Deserializer<'de>, + { + fn deserialize_secret_bytes<'de, D>( + deserializer: D, + ) -> ::core::result::Result<::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, D::Error> + where + D: ::serde::Deserializer<'de>, + { + ::buffa::json_helpers::bytes::deserialize(deserializer) + .map(::zeroize::Zeroizing::new) + } + + + #[derive(Default, ::serde::Deserialize)] + #[serde(default, deny_unknown_fields)] + struct Wire { + #[serde(rename = "keyBytes", alias = "key_bytes", deserialize_with = "deserialize_secret_bytes")] + key_bytes: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + } + + let mut wire = Wire::deserialize(deserializer)?; + Ok(Self { + key_bytes: ::core::mem::take(&mut *wire.key_bytes), + __buffa_unknown_fields: Default::default(), + }) + } +} +impl ::core::ops::Drop for CoseKeyBytesResult { + fn drop(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.key_bytes); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl CoseKeyBytesResult { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseKeyBytesResult"; +} +::buffa::impl_default_instance!(CoseKeyBytesResult); +impl ::buffa::MessageName for CoseKeyBytesResult { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseKeyBytesResult"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseKeyBytesResult"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseKeyBytesResult"; +} +impl ::buffa::Message for CoseKeyBytesResult { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.key_bytes.is_empty() { + size += 1u64 + ::buffa::types::bytes_encoded_len(&self.key_bytes) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.key_bytes.is_empty() { + ::buffa::types::put_shared_bytes_field(1u32, &self.key_bytes, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_bytes(&mut self.key_bytes, buf)?; + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.key_bytes); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseKeyBytesResult { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseKeyBytesResult"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseKeyBytesResult { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_KEY_BYTES_RESULT_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseKeyBytesResult", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +/// CoseMultikeyToCoseKeyRequest converts a Multikey string into a public +/// COSE_Key. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseMultikeyToCoseKeyRequest { + /// Multikey string to parse. + /// + /// Field 1: `multikey` + #[serde( + rename = "multikey", + with = "::buffa::json_helpers::proto_string", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_str" + )] + pub multikey: ::buffa::alloc::string::String, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseMultikeyToCoseKeyRequest { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseMultikeyToCoseKeyRequest") + .field("multikey", &"") + .finish() + } +} +impl<'de> ::serde::Deserialize<'de> for CoseMultikeyToCoseKeyRequest { + fn deserialize(deserializer: D) -> ::core::result::Result + where + D: ::serde::Deserializer<'de>, + { + fn deserialize_secret_string<'de, D>( + deserializer: D, + ) -> ::core::result::Result<::zeroize::Zeroizing<::buffa::alloc::string::String>, D::Error> + where + D: ::serde::Deserializer<'de>, + { + <::buffa::alloc::string::String as ::serde::Deserialize>::deserialize(deserializer) + .map(::zeroize::Zeroizing::new) + } + + + #[derive(Default, ::serde::Deserialize)] + #[serde(default, deny_unknown_fields)] + struct Wire { + #[serde(rename = "multikey", deserialize_with = "deserialize_secret_string")] + multikey: ::zeroize::Zeroizing<::buffa::alloc::string::String>, + } + + let mut wire = Wire::deserialize(deserializer)?; + Ok(Self { + multikey: ::core::mem::take(&mut *wire.multikey), + __buffa_unknown_fields: Default::default(), + }) + } +} +impl ::core::ops::Drop for CoseMultikeyToCoseKeyRequest { + fn drop(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.multikey); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl CoseMultikeyToCoseKeyRequest { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseMultikeyToCoseKeyRequest"; +} +::buffa::impl_default_instance!(CoseMultikeyToCoseKeyRequest); +impl ::buffa::MessageName for CoseMultikeyToCoseKeyRequest { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseMultikeyToCoseKeyRequest"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseMultikeyToCoseKeyRequest"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseMultikeyToCoseKeyRequest"; +} +impl ::buffa::Message for CoseMultikeyToCoseKeyRequest { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.multikey.is_empty() { + size += 1u64 + ::buffa::types::string_encoded_len(&self.multikey) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.multikey.is_empty() { + ::buffa::types::put_string_field(1u32, &self.multikey, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_string(&mut self.multikey, buf)?; + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.multikey); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseMultikeyToCoseKeyRequest { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseMultikeyToCoseKeyRequest"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseMultikeyToCoseKeyRequest { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_MULTIKEY_TO_COSE_KEY_REQUEST_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseMultikeyToCoseKeyRequest", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; +/// CoseMultikeyResult contains a Multikey string produced from a public COSE_Key. +#[derive(Clone, PartialEq, Default)] +#[derive(::serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoseMultikeyResult { + /// Multikey string. + /// + /// Field 1: `multikey` + #[serde( + rename = "multikey", + with = "::buffa::json_helpers::proto_string", + skip_serializing_if = "::buffa::json_helpers::skip_if::is_empty_str" + )] + pub multikey: ::buffa::alloc::string::String, + #[serde(skip)] + #[doc(hidden)] + pub __buffa_unknown_fields: ::buffa::UnknownFields, +} +impl ::core::fmt::Debug for CoseMultikeyResult { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_struct("CoseMultikeyResult").field("multikey", &"").finish() + } +} +impl<'de> ::serde::Deserialize<'de> for CoseMultikeyResult { + fn deserialize(deserializer: D) -> ::core::result::Result + where + D: ::serde::Deserializer<'de>, + { + fn deserialize_secret_string<'de, D>( + deserializer: D, + ) -> ::core::result::Result<::zeroize::Zeroizing<::buffa::alloc::string::String>, D::Error> + where + D: ::serde::Deserializer<'de>, + { + <::buffa::alloc::string::String as ::serde::Deserialize>::deserialize(deserializer) + .map(::zeroize::Zeroizing::new) + } + + + #[derive(Default, ::serde::Deserialize)] + #[serde(default, deny_unknown_fields)] + struct Wire { + #[serde(rename = "multikey", deserialize_with = "deserialize_secret_string")] + multikey: ::zeroize::Zeroizing<::buffa::alloc::string::String>, + } + + let mut wire = Wire::deserialize(deserializer)?; + Ok(Self { + multikey: ::core::mem::take(&mut *wire.multikey), + __buffa_unknown_fields: Default::default(), + }) + } +} +impl ::core::ops::Drop for CoseMultikeyResult { + fn drop(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.multikey); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl CoseMultikeyResult { + /// Protobuf type URL for this message, for use with `Any::pack` and + /// `Any::unpack_if`. + /// + /// Format: `type.googleapis.com/` + pub const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseMultikeyResult"; +} +::buffa::impl_default_instance!(CoseMultikeyResult); +impl ::buffa::MessageName for CoseMultikeyResult { + const PACKAGE: &'static str = "reallyme.cose.v1"; + const NAME: &'static str = "CoseMultikeyResult"; + const FULL_NAME: &'static str = "reallyme.cose.v1.CoseMultikeyResult"; + const TYPE_URL: &'static str = "type.googleapis.com/reallyme.cose.v1.CoseMultikeyResult"; +} +impl ::buffa::Message for CoseMultikeyResult { + /// Returns the total encoded size in bytes. + /// + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. + #[allow(clippy::let_and_return)] + fn compute_size(&self, _cache: &mut ::buffa::SizeCache) -> u32 { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + let mut size = 0u64; + if !self.multikey.is_empty() { + size += 1u64 + ::buffa::types::string_encoded_len(&self.multikey) as u64; + } + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) + } + fn write_to( + &self, + _cache: &mut ::buffa::SizeCache, + buf: &mut impl ::buffa::EncodeSink, + ) { + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + if !self.multikey.is_empty() { + ::buffa::types::put_string_field(1u32, &self.multikey, buf); + } + self.__buffa_unknown_fields.write_to(buf); + } + fn merge_field( + &mut self, + tag: ::buffa::encoding::Tag, + buf: &mut impl ::buffa::bytes::Buf, + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<(), ::buffa::DecodeError> { + #[allow(unused_imports)] + use ::buffa::bytes::Buf as _; + #[allow(unused_imports)] + use ::buffa::Enumeration as _; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + ::buffa::types::merge_string(&mut self.multikey, buf)?; + } + _ => { + self.__buffa_unknown_fields + .push(::buffa::encoding::decode_unknown_field(tag, buf, ctx)?); + } + } + ::core::result::Result::Ok(()) + } + fn clear(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.multikey); + __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields); + } +} +impl ::buffa::ExtensionSet for CoseMultikeyResult { + const PROTO_FQN: &'static str = "reallyme.cose.v1.CoseMultikeyResult"; + fn unknown_fields(&self) -> &::buffa::UnknownFields { + &self.__buffa_unknown_fields + } + fn unknown_fields_mut(&mut self) -> &mut ::buffa::UnknownFields { + &mut self.__buffa_unknown_fields + } +} +impl ::buffa::json_helpers::ProtoElemJson for CoseMultikeyResult { + fn serialize_proto_json( + v: &Self, + s: S, + ) -> ::core::result::Result { + ::serde::Serialize::serialize(v, s) + } + fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>( + d: D, + ) -> ::core::result::Result { + ::deserialize(d) + } +} +#[doc(hidden)] +pub const __COSE_MULTIKEY_RESULT_JSON_ANY: ::buffa::type_registry::JsonAnyEntry = ::buffa::type_registry::JsonAnyEntry { + type_url: "type.googleapis.com/reallyme.cose.v1.CoseMultikeyResult", + to_json: ::buffa::type_registry::any_to_json::, + from_json: ::buffa::type_registry::any_from_json::, + is_wkt: false, +}; diff --git a/crates/proto/src/generated/buffa/reallyme.cose.v1.mod.rs b/crates/proto/src/generated/buffa/reallyme.cose.v1.mod.rs new file mode 100644 index 0000000..c6ed64b --- /dev/null +++ b/crates/proto/src/generated/buffa/reallyme.cose.v1.mod.rs @@ -0,0 +1,163 @@ +// @generated by buffa-codegen. DO NOT EDIT. + +include!("reallyme.cose.v1.cose.rs"); +#[allow( + non_camel_case_types, + dead_code, + unused_imports, + unused_qualifications, + clippy::derivable_impls, + clippy::match_single_binding, + clippy::uninlined_format_args, + clippy::doc_lazy_continuation, + clippy::module_inception +)] +pub mod __buffa { + #[allow(unused_imports)] + use super::*; + pub mod view { + #[allow(unused_imports)] + use super::*; + include!("reallyme.cose.v1.cose.__view.rs"); + pub mod oneof { + #[allow(unused_imports)] + use super::*; + include!("reallyme.cose.v1.cose.__view_oneof.rs"); + } + } + pub mod oneof { + #[allow(unused_imports)] + use super::*; + include!("reallyme.cose.v1.cose.__oneof.rs"); + } + /// Register this package's `Any` type entries and extension entries. + pub fn register_types(reg: &mut ::buffa::type_registry::TypeRegistry) { + reg.register_json_any(super::__COSE_ERROR_JSON_ANY); + reg.register_json_any(super::__COSE_PRIMITIVE_ERROR_JSON_ANY); + reg.register_json_any(super::__COSE_PROVIDER_ERROR_JSON_ANY); + reg.register_json_any(super::__COSE_BACKEND_ERROR_JSON_ANY); + reg.register_json_any(super::__COSE_ALGORITHM_IDENTIFIER_JSON_ANY); + reg.register_json_any(super::__COSE_OPERATION_REQUEST_JSON_ANY); + reg.register_json_any(super::__COSE_OPERATION_RESPONSE_V2_JSON_ANY); + reg.register_json_any(super::__COSE_OPERATION_RESULT_JSON_ANY); + reg.register_json_any(super::__COSE_ML_KEM_ENCRYPT_REQUEST_JSON_ANY); + reg.register_json_any(super::__COSE_ML_KEM_ENCRYPT_RESULT_JSON_ANY); + reg.register_json_any(super::__COSE_ML_KEM_DECRYPT_REQUEST_JSON_ANY); + reg.register_json_any(super::__COSE_ML_KEM_DECRYPT_RESULT_JSON_ANY); + reg.register_json_any(super::__COSE_SIGN1OPTIONS_JSON_ANY); + reg.register_json_any(super::__COSE_SIGN1CREATE_REQUEST_JSON_ANY); + reg.register_json_any(super::__COSE_SIGN1CREATE_DETACHED_REQUEST_JSON_ANY); + reg.register_json_any(super::__COSE_SIGN1CREATE_RESULT_JSON_ANY); + reg.register_json_any(super::__COSE_SIGN1VERIFY_REQUEST_JSON_ANY); + reg.register_json_any(super::__COSE_SIGN1VERIFY_DETACHED_REQUEST_JSON_ANY); + reg.register_json_any(super::__COSE_SIGN1VERIFY_RESULT_JSON_ANY); + reg.register_json_any(super::__COSE_KEY_FROM_PUBLIC_BYTES_REQUEST_JSON_ANY); + reg.register_json_any(super::__COSE_KEY_FROM_PRIVATE_BYTES_REQUEST_JSON_ANY); + reg.register_json_any(super::__COSE_KEY_BYTES_REQUEST_JSON_ANY); + reg.register_json_any(super::__COSE_KEY_BYTES_RESULT_JSON_ANY); + reg.register_json_any(super::__COSE_MULTIKEY_TO_COSE_KEY_REQUEST_JSON_ANY); + reg.register_json_any(super::__COSE_MULTIKEY_RESULT_JSON_ANY); + } +} +#[doc(inline)] +pub use self::__buffa::view::CoseErrorView; +#[doc(inline)] +pub use self::__buffa::view::CoseErrorOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CosePrimitiveErrorView; +#[doc(inline)] +pub use self::__buffa::view::CosePrimitiveErrorOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseProviderErrorView; +#[doc(inline)] +pub use self::__buffa::view::CoseProviderErrorOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseBackendErrorView; +#[doc(inline)] +pub use self::__buffa::view::CoseBackendErrorOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseAlgorithmIdentifierView; +#[doc(inline)] +pub use self::__buffa::view::CoseAlgorithmIdentifierOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseOperationRequestView; +#[doc(inline)] +pub use self::__buffa::view::CoseOperationRequestOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseOperationResponseV2View; +#[doc(inline)] +pub use self::__buffa::view::CoseOperationResponseV2OwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseOperationResultView; +#[doc(inline)] +pub use self::__buffa::view::CoseOperationResultOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseMlKemEncryptRequestView; +#[doc(inline)] +pub use self::__buffa::view::CoseMlKemEncryptRequestOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseMlKemEncryptResultView; +#[doc(inline)] +pub use self::__buffa::view::CoseMlKemEncryptResultOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseMlKemDecryptRequestView; +#[doc(inline)] +pub use self::__buffa::view::CoseMlKemDecryptRequestOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseMlKemDecryptResultView; +#[doc(inline)] +pub use self::__buffa::view::CoseMlKemDecryptResultOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseSign1OptionsView; +#[doc(inline)] +pub use self::__buffa::view::CoseSign1OptionsOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseSign1CreateRequestView; +#[doc(inline)] +pub use self::__buffa::view::CoseSign1CreateRequestOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseSign1CreateDetachedRequestView; +#[doc(inline)] +pub use self::__buffa::view::CoseSign1CreateDetachedRequestOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseSign1CreateResultView; +#[doc(inline)] +pub use self::__buffa::view::CoseSign1CreateResultOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseSign1VerifyRequestView; +#[doc(inline)] +pub use self::__buffa::view::CoseSign1VerifyRequestOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseSign1VerifyDetachedRequestView; +#[doc(inline)] +pub use self::__buffa::view::CoseSign1VerifyDetachedRequestOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseSign1VerifyResultView; +#[doc(inline)] +pub use self::__buffa::view::CoseSign1VerifyResultOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseKeyFromPublicBytesRequestView; +#[doc(inline)] +pub use self::__buffa::view::CoseKeyFromPublicBytesRequestOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseKeyFromPrivateBytesRequestView; +#[doc(inline)] +pub use self::__buffa::view::CoseKeyFromPrivateBytesRequestOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseKeyBytesRequestView; +#[doc(inline)] +pub use self::__buffa::view::CoseKeyBytesRequestOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseKeyBytesResultView; +#[doc(inline)] +pub use self::__buffa::view::CoseKeyBytesResultOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseMultikeyToCoseKeyRequestView; +#[doc(inline)] +pub use self::__buffa::view::CoseMultikeyToCoseKeyRequestOwnedView; +#[doc(inline)] +pub use self::__buffa::view::CoseMultikeyResultView; +#[doc(inline)] +pub use self::__buffa::view::CoseMultikeyResultOwnedView; +#[doc(inline)] +pub use self::__buffa::register_types; diff --git a/crates/proto/src/lib.rs b/crates/proto/src/lib.rs new file mode 100644 index 0000000..441fba7 --- /dev/null +++ b/crates/proto/src/lib.rs @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Generated protobuf boundary types for the ReallyMe COSE wire contract. + +#[cfg(feature = "generated")] +pub mod generated; diff --git a/crates/proto/tests/contract_freeze_tests.rs b/crates/proto/tests/contract_freeze_tests.rs new file mode 100644 index 0000000..faa9d71 --- /dev/null +++ b/crates/proto/tests/contract_freeze_tests.rs @@ -0,0 +1,342 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Wire-identifier freeze tests for the public COSE 0.2.0 protobuf contract. + +#![cfg(feature = "generated")] +#![allow(clippy::panic)] + +use buffa::{EnumValue, Message}; +use reallyme_cose_proto::generated::proto::reallyme::cose::v1::{ + __buffa::oneof::{ + cose_algorithm_identifier, cose_operation_request, cose_operation_response_v2, + cose_operation_result, + }, + CoseAlgorithmIdentifier, CoseContentEncryptionAlgorithm, CoseErrorReason, CoseKemAlgorithm, + CoseKeyAgreementAlgorithm, CoseKeyBytesRequest, CoseKeyBytesResult, + CoseKeyFromPrivateBytesRequest, CoseKeyFromPublicBytesRequest, CoseMlKemDecryptRequest, + CoseMlKemDecryptResult, CoseMlKemEncryptRequest, CoseMlKemEncryptResult, CoseMlKemMode, + CoseMultikeyResult, CoseMultikeyToCoseKeyRequest, CoseOperationRequest, + CoseOperationResponseV2, CoseOperationResult, CoseSign1CreateDetachedRequest, + CoseSign1CreateRequest, CoseSign1CreateResult, CoseSign1VerifyDetachedRequest, + CoseSign1VerifyRequest, CoseSign1VerifyResult, CoseSignatureAlgorithm, +}; + +type Operation = cose_operation_request::Operation; +type OperationResult = cose_operation_result::Result; + +#[test] +fn operation_oneof_field_numbers_are_frozen() { + for (operation, field_number) in [ + ( + Operation::Sign1Create(Box::::default()), + 1000, + ), + ( + Operation::Sign1CreateDetached(Box::::default()), + 1001, + ), + ( + Operation::Sign1Verify(Box::::default()), + 1002, + ), + ( + Operation::Sign1VerifyDetached(Box::::default()), + 1003, + ), + ( + Operation::KeyFromPublicBytes(Box::::default()), + 2000, + ), + ( + Operation::KeyFromPrivateBytes(Box::::default()), + 2001, + ), + ( + Operation::KeyParse(Box::::default()), + 2002, + ), + ( + Operation::KeyToPublicBytes(Box::::default()), + 2003, + ), + ( + Operation::KeyToPrivateBytes(Box::::default()), + 2004, + ), + ( + Operation::KeyDerivePublicKid(Box::::default()), + 2005, + ), + ( + Operation::KeyToMultikey(Box::::default()), + 2006, + ), + ( + Operation::MultikeyToCoseKey(Box::::default()), + 2007, + ), + ( + Operation::MlKemEncryptDirect(Box::::default()), + 3000, + ), + ( + Operation::MlKemEncryptKeyWrap(Box::::default()), + 3001, + ), + ( + Operation::MlKemDecrypt(Box::::default()), + 3002, + ), + ] { + assert_operation_field_number(operation, field_number); + } +} + +#[test] +fn version_two_response_field_numbers_are_frozen() { + for (result, field_number) in [ + ( + OperationResult::Sign1Create(Box::::default()), + 1000, + ), + ( + OperationResult::Sign1CreateDetached(Box::::default()), + 1001, + ), + ( + OperationResult::Sign1Verify(Box::::default()), + 1002, + ), + ( + OperationResult::Sign1VerifyDetached(Box::::default()), + 1003, + ), + ( + OperationResult::KeyFromPublicBytes(Box::::default()), + 2000, + ), + ( + OperationResult::KeyFromPrivateBytes(Box::::default()), + 2001, + ), + ( + OperationResult::KeyParse(Box::::default()), + 2002, + ), + ( + OperationResult::KeyToPublicBytes(Box::::default()), + 2003, + ), + ( + OperationResult::KeyToPrivateBytes(Box::::default()), + 2004, + ), + ( + OperationResult::KeyDerivePublicKid(Box::::default()), + 2005, + ), + ( + OperationResult::KeyToMultikey(Box::::default()), + 2006, + ), + ( + OperationResult::MultikeyToCoseKey(Box::::default()), + 2007, + ), + ( + OperationResult::MlKemEncryptDirect(Box::::default()), + 3000, + ), + ( + OperationResult::MlKemEncryptKeyWrap(Box::::default()), + 3001, + ), + ( + OperationResult::MlKemDecrypt(Box::::default()), + 3002, + ), + ] { + assert_result_field_number(result, field_number); + } + + let result_response = CoseOperationResponseV2 { + outcome: Some(cose_operation_response_v2::Outcome::Result(Box::default())), + __buffa_unknown_fields: Default::default(), + }; + assert_eq!(result_response.encode_to_vec(), [0x0a, 0x00]); + + let error_response = CoseOperationResponseV2 { + outcome: Some(cose_operation_response_v2::Outcome::Error(Box::default())), + __buffa_unknown_fields: Default::default(), + }; + assert_eq!(error_response.encode_to_vec(), [0x12, 0x00]); + assert_eq!( + CoseOperationResponseV2::TYPE_URL, + "type.googleapis.com/reallyme.cose.v1.CoseOperationResponseV2" + ); +} + +#[test] +fn algorithm_identifier_oneof_field_numbers_are_frozen() { + let signature = CoseAlgorithmIdentifier { + algorithm: Some(cose_algorithm_identifier::Algorithm::Signature( + EnumValue::from(CoseSignatureAlgorithm::Ed25519), + )), + __buffa_unknown_fields: Default::default(), + }; + assert_eq!(signature.encode_to_vec(), [0x08, 0x64]); + + let key_agreement = CoseAlgorithmIdentifier { + algorithm: Some(cose_algorithm_identifier::Algorithm::KeyAgreement( + EnumValue::from(CoseKeyAgreementAlgorithm::X25519), + )), + __buffa_unknown_fields: Default::default(), + }; + assert_eq!(key_agreement.encode_to_vec(), [0x10, 0x64]); + + let kem = CoseAlgorithmIdentifier { + algorithm: Some(cose_algorithm_identifier::Algorithm::Kem(EnumValue::from( + CoseKemAlgorithm::MlKem512, + ))), + __buffa_unknown_fields: Default::default(), + }; + assert_eq!(kem.encode_to_vec(), [0x18, 0xe8, 0x07]); +} + +#[test] +fn operation_request_type_name_is_frozen() { + assert_eq!( + CoseOperationRequest::TYPE_URL, + "type.googleapis.com/reallyme.cose.v1.CoseOperationRequest" + ); +} + +#[test] +fn algorithm_and_mode_numbers_are_frozen() { + for (actual, expected) in [ + (CoseSignatureAlgorithm::Unspecified as i32, 0), + (CoseSignatureAlgorithm::Ed25519 as i32, 100), + (CoseSignatureAlgorithm::EcdsaP256Sha256 as i32, 200), + (CoseSignatureAlgorithm::EcdsaP384Sha384 as i32, 210), + (CoseSignatureAlgorithm::EcdsaP521Sha512 as i32, 220), + (CoseSignatureAlgorithm::EcdsaSecp256k1Sha256 as i32, 230), + (CoseSignatureAlgorithm::MlDsa44 as i32, 1000), + (CoseSignatureAlgorithm::MlDsa65 as i32, 1010), + (CoseSignatureAlgorithm::MlDsa87 as i32, 1020), + (CoseKeyAgreementAlgorithm::Unspecified as i32, 0), + (CoseKeyAgreementAlgorithm::X25519 as i32, 100), + (CoseKemAlgorithm::Unspecified as i32, 0), + (CoseKemAlgorithm::MlKem512 as i32, 1000), + (CoseKemAlgorithm::MlKem768 as i32, 1010), + (CoseKemAlgorithm::MlKem1024 as i32, 1020), + (CoseKemAlgorithm::XWing768 as i32, 1100), + (CoseContentEncryptionAlgorithm::Unspecified as i32, 0), + (CoseContentEncryptionAlgorithm::Aes128Gcm as i32, 100), + (CoseContentEncryptionAlgorithm::Aes192Gcm as i32, 110), + (CoseContentEncryptionAlgorithm::Aes256Gcm as i32, 120), + (CoseMlKemMode::Unspecified as i32, 0), + (CoseMlKemMode::Direct as i32, 1), + (CoseMlKemMode::KeyWrap as i32, 2), + ] { + assert_eq!(actual, expected); + } +} + +#[test] +fn error_reason_numbers_are_frozen() { + for (actual, expected) in [ + (CoseErrorReason::Unspecified as i32, 0), + (CoseErrorReason::CommonCbor as i32, 100), + (CoseErrorReason::CommonInvalidFormat as i32, 101), + (CoseErrorReason::CommonResourceLimitExceeded as i32, 102), + (CoseErrorReason::CommonNonCanonicalCbor as i32, 103), + (CoseErrorReason::CommonUnexpectedCborTag as i32, 104), + (CoseErrorReason::CommonDuplicateMapLabel as i32, 105), + (CoseErrorReason::CommonMalformedProtobuf as i32, 120), + (CoseErrorReason::CommonMalformedJson as i32, 121), + (CoseErrorReason::CommonInvalidParameter as i32, 130), + (CoseErrorReason::CommonInvalidLength as i32, 131), + (CoseErrorReason::CommonInvalidEncoding as i32, 132), + (CoseErrorReason::CommonUnsupportedAlgorithm as i32, 200), + (CoseErrorReason::ProviderUnavailable as i32, 201), + (CoseErrorReason::CommonCryptoFailed as i32, 300), + (CoseErrorReason::BackendInternal as i32, 301), + (CoseErrorReason::Sign1KidKeyMismatch as i32, 400), + (CoseErrorReason::Sign1MissingPayload as i32, 401), + (CoseErrorReason::Sign1MissingKid as i32, 402), + (CoseErrorReason::Sign1KeyNotResolved as i32, 403), + (CoseErrorReason::Sign1UnsupportedCriticalHeader as i32, 410), + ( + CoseErrorReason::Sign1UnprotectedHeaderNotAllowed as i32, + 411, + ), + (CoseErrorReason::Sign1InvalidSignature as i32, 420), + (CoseErrorReason::Sign1MissingPrivateKey as i32, 421), + (CoseErrorReason::Sign1InvalidSignatureEncoding as i32, 422), + (CoseErrorReason::KeyMissingKeyMaterial as i32, 500), + (CoseErrorReason::KeyInvalidKeyMaterial as i32, 501), + (CoseErrorReason::MultikeyInvalidMultikey as i32, 600), + (CoseErrorReason::EncryptMissingCiphertext as i32, 700), + (CoseErrorReason::EncryptInvalidIv as i32, 701), + (CoseErrorReason::EncryptInvalidRecipient as i32, 702), + (CoseErrorReason::EncryptMissingEncapsulatedKey as i32, 703), + (CoseErrorReason::EncryptInvalidEncapsulatedKey as i32, 704), + (CoseErrorReason::EncryptAuthenticationFailed as i32, 720), + (CoseErrorReason::EncryptKeyUnwrapFailed as i32, 721), + (CoseErrorReason::EncryptKidMismatch as i32, 730), + (CoseErrorReason::EncryptMissingKid as i32, 731), + ( + CoseErrorReason::EncryptUnprotectedHeaderNotAllowed as i32, + 740, + ), + ] { + assert_eq!(actual, expected); + } +} + +fn assert_operation_field_number(operation: Operation, field_number: u32) { + let request = CoseOperationRequest { + operation: Some(operation), + __buffa_unknown_fields: Default::default(), + }; + let mut expected = protobuf_length_delimited_field_key(field_number); + expected.push(0); + assert_eq!(request.encode_to_vec(), expected); +} + +fn assert_result_field_number(result: OperationResult, field_number: u32) { + let result = CoseOperationResult { + result: Some(result), + __buffa_unknown_fields: Default::default(), + }; + let mut expected = protobuf_length_delimited_field_key(field_number); + expected.push(0); + assert_eq!(result.encode_to_vec(), expected); +} + +fn protobuf_length_delimited_field_key(field_number: u32) -> Vec { + let shifted = match field_number.checked_shl(3) { + Some(value) => value, + None => panic!("protobuf field-number shift overflowed"), + }; + let mut value = match shifted.checked_add(2) { + Some(value) => value, + None => panic!("protobuf field key overflowed"), + }; + let mut encoded = Vec::new(); + + loop { + let low_bits = match u8::try_from(value & 0x7f) { + Ok(value) => value, + Err(_) => panic!("protobuf field key chunk did not fit in u8"), + }; + value >>= 7; + if value == 0 { + encoded.push(low_bits); + return encoded; + } + encoded.push(low_bits | 0x80); + } +} diff --git a/crates/proto/tests/generated_tests.rs b/crates/proto/tests/generated_tests.rs new file mode 100644 index 0000000..15fe155 --- /dev/null +++ b/crates/proto/tests/generated_tests.rs @@ -0,0 +1,312 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Contract tests for generated ReallyMe COSE protobuf messages. + +#![cfg(feature = "generated")] +#![allow(clippy::panic)] + +use buffa::{EnumValue, Message}; +use reallyme_cose_proto::generated::proto::reallyme::cose::v1::{ + __buffa::oneof::{cose_algorithm_identifier, cose_error}, + CoseAlgorithmIdentifier, CoseContentEncryptionAlgorithm, CoseError, CoseErrorReason, + CoseKemAlgorithm, CoseKeyAgreementAlgorithm, CoseKeyBytesRequest, CoseKeyBytesResult, + CoseKeyBytesResultOwnedView, CoseKeyFromPrivateBytesRequest, CoseMlKemDecryptResult, + CoseMlKemDecryptResultOwnedView, CoseMultikeyResult, CoseMultikeyToCoseKeyRequest, + CoseMultikeyToCoseKeyRequestOwnedView, CoseOperationRequest, CoseOperationResponseV2, + CoseOperationResult, CosePrimitiveError, CoseSign1CreateDetachedRequest, + CoseSign1CreateRequest, CoseSign1CreateResult, CoseSign1Options, + CoseSign1VerifyDetachedRequest, CoseSign1VerifyRequest, CoseSign1VerifyResult, + CoseSignatureAlgorithm, +}; + +fn ed25519_identifier( +) -> buffa::MessageField> { + buffa::MessageField::some(CoseAlgorithmIdentifier { + algorithm: Some(cose_algorithm_identifier::Algorithm::Signature( + EnumValue::from(CoseSignatureAlgorithm::Ed25519), + )), + __buffa_unknown_fields: Default::default(), + }) +} + +#[test] +fn family_scoped_algorithm_numbers_match_the_crypto_boundary() { + assert_eq!(CoseSignatureAlgorithm::Ed25519 as i32, 100); + assert_eq!(CoseSignatureAlgorithm::EcdsaP256Sha256 as i32, 200); + assert_eq!(CoseSignatureAlgorithm::EcdsaP384Sha384 as i32, 210); + assert_eq!(CoseSignatureAlgorithm::EcdsaP521Sha512 as i32, 220); + assert_eq!(CoseSignatureAlgorithm::EcdsaSecp256k1Sha256 as i32, 230); + assert_eq!(CoseSignatureAlgorithm::MlDsa44 as i32, 1000); + assert_eq!(CoseSignatureAlgorithm::MlDsa65 as i32, 1010); + assert_eq!(CoseSignatureAlgorithm::MlDsa87 as i32, 1020); + assert_eq!(CoseKeyAgreementAlgorithm::X25519 as i32, 100); + assert_eq!(CoseKemAlgorithm::MlKem512 as i32, 1000); + assert_eq!(CoseKemAlgorithm::MlKem768 as i32, 1010); + assert_eq!(CoseKemAlgorithm::MlKem1024 as i32, 1020); + assert_eq!(CoseKemAlgorithm::XWing768 as i32, 1100); + assert_eq!(CoseContentEncryptionAlgorithm::Aes128Gcm as i32, 100); + assert_eq!(CoseContentEncryptionAlgorithm::Aes192Gcm as i32, 110); + assert_eq!(CoseContentEncryptionAlgorithm::Aes256Gcm as i32, 120); +} + +#[test] +fn top_level_operation_request_has_an_ordinary_drop_wipe_path() { + // This deliberately checks the generated explicit `Drop` implementation, + // not merely `needs_drop`, which would also be true because of the boxed + // operation branch and would therefore miss removal of the wipe hook. + #[allow(drop_bounds)] + fn assert_drop_implementation() {} + + // Unknown fields belong to the top-level message itself, so recursive leaf + // hardening is insufficient. This compile-time assertion prevents the + // postprocessor from silently dropping the ordinary-destruction hook. + assert_drop_implementation::(); +} + +#[test] +fn version_two_response_owners_have_ordinary_drop_wipe_paths() { + #[allow(drop_bounds)] + fn assert_drop_implementation() {} + + // Both wrappers can retain length-delimited unknown fields in addition to + // owning nested sensitive result messages. Their explicit Drop hooks are + // therefore independently required. + assert_drop_implementation::(); + assert_drop_implementation::(); +} + +#[test] +fn cose_error_wire_contract_is_stable() { + let error = CoseError { + error: Some(cose_error::Error::Primitive(Box::new(CosePrimitiveError { + reason: EnumValue::from(CoseErrorReason::COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE), + __buffa_unknown_fields: Default::default(), + }))), + __buffa_unknown_fields: Default::default(), + }; + + assert_eq!(error.encode_to_vec(), [0x0a, 0x03, 0x08, 0xa4, 0x03]); +} + +#[test] +fn key_bytes_result_uses_unambiguous_proto_json_and_view_accessors( +) -> Result<(), buffa::DecodeError> { + let result = CoseKeyBytesResult { + key_bytes: vec![241, 242, 243, 244], + __buffa_unknown_fields: Default::default(), + }; + + let json = serde_json::to_string(&result).unwrap_or_else(|error| { + panic!("generated key-bytes result JSON encoding failed: {error}"); + }); + assert_eq!(json, r#"{"keyBytes":"8fLz9A=="}"#); + + let view = CoseKeyBytesResultOwnedView::from_owned(&result)?; + assert_eq!(view.key_bytes(), result.key_bytes.as_slice()); + Ok(()) +} + +#[test] +fn generated_proto_json_rejects_unknown_fields() { + let request = serde_json::from_str::( + r#"{"algorithm":"COSE_SIGNATURE_ALGORITHM_ED25519","payload":"","privateKey":"","kid":"","hasKid":false,"externalAad":"","private_key_typo":""}"#, + ); + assert!(request.is_err()); +} + +#[test] +fn generated_proto_json_enum_range_errors_do_not_reflect_untrusted_values() { + for (untrusted_value, expected_diagnostic) in [ + ("42424242", "unknown enum value"), + ("9223372036854775807", "enum value out of i32 range"), + ] { + let result = serde_json::from_str::(untrusted_value); + let error = match result { + Ok(_) => panic!("invalid concrete ProtoJSON enum value was accepted"), + Err(error) => error, + }; + let diagnostic = error.to_string(); + assert!( + diagnostic.contains(expected_diagnostic), + "unexpected fixed diagnostic: {diagnostic}" + ); + assert!(!diagnostic.contains(untrusted_value)); + } +} + +#[test] +fn generated_owned_views_redact_retained_protobuf_buffers() -> Result<(), buffa::DecodeError> { + let result = CoseMlKemDecryptResult { + plaintext: vec![251, 252, 253, 254], + content_algorithm: Default::default(), + kem_algorithm: Default::default(), + mode: Default::default(), + recipient_kid: vec![241, 242, 243, 244], + __buffa_unknown_fields: Default::default(), + }; + let view = CoseMlKemDecryptResultOwnedView::from_owned(&result)?; + let debug = format!("{view:?}"); + assert!(debug.contains("")); + assert!(!debug.contains("251")); + assert!(!debug.contains("252")); + assert!(!debug.contains("253")); + assert!(!debug.contains("254")); + assert!(!debug.contains("241")); + assert!(!debug.contains("242")); + assert!(!debug.contains("243")); + assert!(!debug.contains("244")); + Ok(()) +} + +#[test] +fn generated_private_key_requests_redact_debug_output() { + let private_key = vec![251, 252, 253, 254]; + + let attached = CoseSign1CreateRequest { + algorithm: EnumValue::from(CoseSignatureAlgorithm::Ed25519), + payload: b"payload".to_vec(), + private_key: private_key.clone(), + kid: b"kid".to_vec(), + has_kid: true, + options: buffa::MessageField::some(CoseSign1Options { + tag: true, + max_cose_sign1_bytes: 0, + __buffa_unknown_fields: Default::default(), + }), + external_aad: Vec::new(), + __buffa_unknown_fields: Default::default(), + }; + assert_redacts_private_key(format!("{attached:?}")); + + let detached = CoseSign1CreateDetachedRequest { + algorithm: EnumValue::from(CoseSignatureAlgorithm::Ed25519), + payload: b"payload".to_vec(), + private_key: private_key.clone(), + kid: b"kid".to_vec(), + has_kid: true, + options: buffa::MessageField::none(), + external_aad: Vec::new(), + __buffa_unknown_fields: Default::default(), + }; + assert_redacts_private_key(format!("{detached:?}")); + + let key = CoseKeyFromPrivateBytesRequest { + algorithm: ed25519_identifier(), + private_key, + public_key: Vec::new(), + has_public_key: false, + __buffa_unknown_fields: Default::default(), + }; + assert_redacts_private_key(format!("{key:?}")); +} + +#[test] +fn generated_byte_fields_redact_debug_output() { + let cose_sign1 = CoseSign1CreateResult { + cose_sign1: vec![241, 242, 243, 244], + __buffa_unknown_fields: Default::default(), + }; + assert_redacts_bytes(format!("{cose_sign1:?}"), "cose_sign1"); + + let verified = CoseSign1VerifyResult { + payload: vec![241, 242, 243, 244], + algorithm: EnumValue::from(CoseSignatureAlgorithm::Ed25519), + kid: vec![245, 246, 247, 248], + __buffa_unknown_fields: Default::default(), + }; + let verified_debug = format!("{verified:?}"); + assert_redacts_bytes(verified_debug.clone(), "payload"); + assert_redacts_bytes(verified_debug, "kid"); + + let key_request = CoseKeyBytesRequest { + cose_key: vec![241, 242, 243, 244], + __buffa_unknown_fields: Default::default(), + }; + assert_redacts_bytes(format!("{key_request:?}"), "cose_key"); + + let key_result = CoseKeyBytesResult { + key_bytes: vec![241, 242, 243, 244], + __buffa_unknown_fields: Default::default(), + }; + assert_redacts_bytes(format!("{key_result:?}"), "key_bytes"); + + let verify = CoseSign1VerifyRequest { + cose_sign1: vec![241, 242, 243, 244], + public_key: vec![241, 242, 243, 244], + max_cose_sign1_bytes: 0, + max_detached_payload_bytes: 0, + require_kid: true, + allowed_algorithms: Vec::new(), + external_aad: vec![241, 242, 243, 244], + expected_kid: vec![245, 246, 247, 248], + __buffa_unknown_fields: Default::default(), + }; + let verify_debug = format!("{verify:?}"); + assert_redacts_bytes(verify_debug.clone(), "external_aad"); + assert_redacts_bytes(verify_debug, "expected_kid"); + + let detached_verify = CoseSign1VerifyDetachedRequest { + cose_sign1: vec![241, 242, 243, 244], + payload: vec![241, 242, 243, 244], + public_key: vec![241, 242, 243, 244], + max_cose_sign1_bytes: 0, + max_detached_payload_bytes: 0, + require_kid: true, + allowed_algorithms: Vec::new(), + external_aad: vec![241, 242, 243, 244], + expected_kid: vec![245, 246, 247, 248], + __buffa_unknown_fields: Default::default(), + }; + let detached_verify_debug = format!("{detached_verify:?}"); + assert_redacts_bytes(detached_verify_debug.clone(), "external_aad"); + assert_redacts_bytes(detached_verify_debug, "expected_kid"); +} + +#[test] +fn generated_multikey_strings_redact_owned_and_view_debug_output() -> Result<(), buffa::DecodeError> +{ + const MULTIKEY: &str = "z6MkrzSensitivePersistentIdentifier"; + let request = serde_json::from_str::(&format!( + r#"{{"multikey":"{MULTIKEY}"}}"# + )) + .unwrap_or_else(|error| panic!("generated Multikey request JSON decoding failed: {error}")); + assert_redacts_string(format!("{request:?}"), MULTIKEY); + + let view = CoseMultikeyToCoseKeyRequestOwnedView::from_owned(&request)?; + assert_redacts_string(format!("{view:?}"), MULTIKEY); + + let result = CoseMultikeyResult { + multikey: MULTIKEY.to_owned(), + __buffa_unknown_fields: Default::default(), + }; + assert_redacts_string(format!("{result:?}"), MULTIKEY); + Ok(()) +} + +fn assert_redacts_private_key(debug: String) { + assert_redacts_bytes(debug, "private_key"); +} + +fn assert_redacts_bytes(debug: String, field_name: &str) { + assert!(debug.contains(field_name)); + assert!(debug.contains("")); + assert!(!debug.contains("251")); + assert!(!debug.contains("252")); + assert!(!debug.contains("253")); + assert!(!debug.contains("254")); + assert!(!debug.contains("241")); + assert!(!debug.contains("242")); + assert!(!debug.contains("243")); + assert!(!debug.contains("244")); + assert!(!debug.contains("245")); + assert!(!debug.contains("246")); + assert!(!debug.contains("247")); + assert!(!debug.contains("248")); +} + +fn assert_redacts_string(debug: String, value: &str) { + assert!(debug.contains("")); + assert!(!debug.contains(value)); +} diff --git a/deny.toml b/deny.toml index f41466e..8f5bd9f 100644 --- a/deny.toml +++ b/deny.toml @@ -26,6 +26,7 @@ allow = [ "BSD-3-Clause", "MIT", "Unicode-3.0", + "Zlib", ] exceptions = [] diff --git a/docs/performance-baseline-0.2.0.md b/docs/performance-baseline-0.2.0.md new file mode 100644 index 0000000..603ef63 --- /dev/null +++ b/docs/performance-baseline-0.2.0.md @@ -0,0 +1,46 @@ + + +# COSE 0.2.0 Performance And Allocation Baseline + +Date: 2026-07-22 + +Command: `cargo bench --bench operation_performance --all-features` + +Environment: Apple Silicon (`Darwin arm64`), Rust and Cargo 1.96.0, release +profile with LTO, one code-generation unit, aborting panics, and overflow +checks. Throughput is host-specific evidence, not a cross-host release +threshold. Peak-allocation ceilings are executable review limits enforced by +the benchmark. + +| Operation | Case | Median estimate | Throughput estimate | Peak live allocation | +| --- | --- | ---: | ---: | ---: | +| Sign1 verify | 4 KiB attached | 57.347 µs | 68.116 MiB/s | 8,505 bytes | +| Sign1 verify | 1 MiB detached maximum | 1.4070 ms | 710.74 MiB/s | 1,048,843 bytes | +| COSE_Key parse | Ed25519, 42 bytes | 8.9154 µs | 4.4927 MiB/s | 544 bytes | +| COSE_Key parse | ML-KEM-1024, 1,581 bytes | 57.405 µs | 26.265 MiB/s | 5,504 bytes | +| Multikey conversion | Ed25519 | 26.420 µs | n/a | 544 bytes | +| Multikey conversion | ML-KEM-1024 | 3.7198 ms | n/a | 5,495 bytes | +| COSE_Encrypt decrypt | ML-KEM-512, 4 KiB plaintext | 88.979 µs | 53.172 MiB/s | 20,310 bytes | +| COSE_Encrypt decrypt | ML-KEM-1024, 1 MiB plaintext | 3.5648 ms | 280.96 MiB/s | 4,199,798 bytes | + +## Enforced Peak Limits + +| Family | Reviewed ceiling | +| --- | ---: | +| Sign1 verification | 4 MiB | +| COSE_Key parsing | 1 MiB | +| Multikey conversion | 1 MiB | +| COSE_Encrypt decryption | 12 MiB | + +The ceilings intentionally include margin for allocator and dependency changes +while still catching unbounded duplication. Raising a ceiling is a security +and performance policy change: record the new measurement, explain the cause, +and obtain review instead of silently replacing the baseline. + +The maximum detached signing fixture proves that the documented +1,048,576-byte payload boundary is reachable through the dedicated checked +canonical encoder. diff --git a/docs/platform-scope-0.2.0.json b/docs/platform-scope-0.2.0.json new file mode 100644 index 0000000..546b034 --- /dev/null +++ b/docs/platform-scope-0.2.0.json @@ -0,0 +1,39 @@ +{ + "schema": "reallyme.cose.platform_scope.v1", + "spdxCopyrightText": "Copyright © 2026 ReallyMe LLC. All rights reserved", + "spdxLicenseIdentifier": "Apache-2.0", + "release": "0.2.0", + "immediateScope": "rust_and_protobuf", + "publishableCrates": [ + "reallyme-cose-proto", + "reallyme-cose" + ], + "rustRuntimeLanes": [ + "native", + "wasm" + ], + "platformLanes": [ + { + "lane": "swift", + "decision": "planned_later_release" + }, + { + "lane": "android_kotlin", + "decision": "planned_later_release" + }, + { + "lane": "kotlin_jvm", + "decision": "planned_later_release" + }, + { + "lane": "native_abi", + "decision": "not_approved" + }, + { + "lane": "typescript_wasm_npm", + "decision": "not_approved" + } + ], + "protobufSwiftMetadataIsPackagingApproval": false, + "wasmRuntimeIsNpmPackagingApproval": false +} diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 96aae12..9642586 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -9,7 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common 0.1.7", - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -57,6 +57,7 @@ dependencies = [ "ctr 0.10.1", "ghash", "subtle", + "zeroize", ] [[package]] @@ -212,7 +213,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -234,12 +235,36 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "buffa" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97ce1f34252b8e2516042e1002d56c160908a7ff8a1c9d47e97836558d1c83a" +dependencies = [ + "base64", + "bytes", + "foldhash", + "hashbrown 0.15.5", + "once_cell", + "rustversion", + "serde", + "serde_json", + "smoothutf8", + "thiserror", +] + [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + [[package]] name = "cc" version = "1.2.67" @@ -267,7 +292,6 @@ dependencies = [ "cfg-if", "cipher 0.5.2", "cpufeatures 0.3.0", - "rand_core 0.10.1", "zeroize", ] @@ -430,7 +454,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "generic-array", + "generic-array 0.14.7", "rand_core 0.6.4", "typenum", ] @@ -485,7 +509,6 @@ dependencies = [ "curve25519-dalek-derive", "digest 0.11.3", "fiat-crypto", - "rand_core 0.10.1", "rustc_version", "subtle", "zeroize", @@ -595,13 +618,24 @@ checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core 0.10.1", "sha2", - "signature", "subtle", "zeroize", ] +[[package]] +name = "ed448-goldilocks" +version = "0.14.0-pre.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b805154de2e68f59874ec217ca36790dcffe500cd872c60fe509d28d0814a74d" +dependencies = [ + "elliptic-curve", + "hash2curve", + "rand_core 0.10.1", + "shake", + "subtle", +] + [[package]] name = "elliptic-curve" version = "0.14.1" @@ -653,37 +687,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] -name = "futures-core" -version = "0.3.32" +name = "foldhash" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", + "typenum", + "version_check", ] [[package]] name = "generic-array" -version = "0.14.7" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "ab4e5aa225bc56696909483320f0ff9b600f1a971b52e07a17d70f3d9b43254b" dependencies = [ + "rustversion", "typenum", - "version_check", ] [[package]] @@ -693,10 +719,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", ] [[package]] @@ -719,7 +743,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" dependencies = [ - "polyval 0.7.2", + "polyval 0.7.3", ] [[package]] @@ -744,6 +768,26 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hash2curve" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1eaf40612d7d854743e7189228a6d528f0f6e8502cf6a0cb831d28a218b7f3f6" +dependencies = [ + "digest 0.11.3", + "elliptic-curve", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", + "serde", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -779,12 +823,17 @@ dependencies = [ "chacha20poly1305", "hkdf", "hybrid-array", + "ml-kem", "p256", "p384", "p521", "rand_core 0.10.1", "sha2", + "sha3 0.12.0", + "shake", "subtle", + "turboshake", + "x-wing", "x25519-dalek", "zeroize", ] @@ -808,7 +857,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", ] [[package]] @@ -817,7 +866,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -852,7 +901,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", - "futures-util", "wasm-bindgen", ] @@ -871,6 +919,15 @@ dependencies = [ "wnaf", ] +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + [[package]] name = "keccak" version = "0.2.0" @@ -1120,12 +1177,6 @@ dependencies = [ "base64ct", ] -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - [[package]] name = "pkcs8" version = "0.11.0" @@ -1160,9 +1211,9 @@ dependencies = [ [[package]] name = "polyval" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b20f20e954175de5f463f67781b35583397d916b1d148738923711b2ad16bee8" +checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" dependencies = [ "cpubits", "cpufeatures 0.3.0", @@ -1229,17 +1280,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "rand" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" -dependencies = [ - "chacha20", - "getrandom 0.4.3", - "rand_core 0.10.1", -] - [[package]] name = "rand_core" version = "0.6.4" @@ -1257,22 +1297,23 @@ checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "reallyme-codec" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc0e852dfef578e3b314e890bac9aee6da6f23122abcf5eb2da9185f9ad40711" +checksum = "fd3a3125c574e35d31a7abbdac1fb1c66788f54e84e8d757fb1d941a7cd46573" dependencies = [ "reallyme-codec-base64url", "reallyme-codec-cbor", "reallyme-codec-multibase", "reallyme-codec-multicodec", "reallyme-codec-multikey", + "thiserror", ] [[package]] name = "reallyme-codec-base64url" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c565a3783ab52fb27bf9c0c55a0dea1b8e1828f94d5874cce5d110774a6f259e" +checksum = "318534e19a178ea727b2e141fde87e892c3b338d4b8a52323b29bd3a5f50cb94" dependencies = [ "base64", "thiserror", @@ -1280,65 +1321,85 @@ dependencies = [ [[package]] name = "reallyme-codec-cbor" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29088f56a703311c3b984df9e71888cdecdf310440a7a3d63e5beaab48f77db1" +checksum = "0fe95bd99c7a82d1661e131c17bdb7b757cab05969d32452e971e0d6afeaa569" dependencies = [ "cid", "multihash", "multihash-codetable", "sha2", "thiserror", + "zeroize", ] [[package]] name = "reallyme-codec-jcs" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134d7a029ae431f762034d37c5c5ea3e34eb49417b8cf06de321d85ab619474f" +checksum = "be98f72844bae8c270002805b7e727782f3903a9b244b81bffbc154582422a92" dependencies = [ + "itoa", "ryu-js", + "serde", "serde_json", "thiserror", + "zeroize", ] [[package]] name = "reallyme-codec-multibase" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c866573200c88d9858a527c2764e7786494101dd9e91ffae0ac5043a098b992c" +checksum = "060dbf70afe2e22822c726dc38d4efb24654cca7c02b134624a11777cda966f8" dependencies = [ + "base64", "bs58", "reallyme-codec-base64url", "thiserror", + "zeroize", ] [[package]] name = "reallyme-codec-multicodec" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb0f85ffa7e2b2a44a8529169a9085e392dd9a71d747ed0c81e2c7f0f895160" +checksum = "378589a32bb420707728dc36b693e15aaf34a0baa48cf91f50ec8c15c6b1c6f9" [[package]] name = "reallyme-codec-multikey" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e955e390ddddd6e54ac4f79edee3c74e2122de2cba5fbf24daabd90626c3cd0" +checksum = "fdce86dc938b3de36f8fd6f7213dd4127cbeb47018263dbbfdeab78adb60165e" dependencies = [ "reallyme-codec-multibase", "reallyme-codec-multicodec", "thiserror", ] +[[package]] +name = "reallyme-codec-pem" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32bcc148863f3d8d9e347f858e1bbf8f9df6154bab85b46da7da850e747f3633" +dependencies = [ + "base64", + "thiserror", + "zeroize", +] + [[package]] name = "reallyme-cose" -version = "0.1.1" +version = "0.2.0" dependencies = [ + "buffa", "ciborium", "coset", - "getrandom 0.2.17", "reallyme-codec", + "reallyme-cose-proto", "reallyme-crypto", + "serde", + "serde_json", "thiserror", "zeroize", ] @@ -1347,21 +1408,29 @@ dependencies = [ name = "reallyme-cose-fuzz" version = "0.0.0" dependencies = [ + "buffa", "libfuzzer-sys", "reallyme-cose", + "reallyme-crypto", + "serde_json", + "zeroize", +] + +[[package]] +name = "reallyme-cose-proto" +version = "0.2.0" +dependencies = [ + "buffa", + "serde", + "zeroize", ] [[package]] name = "reallyme-crypto" -version = "0.1.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3fd5abf6acbc465ed1dea322b4e984935b6ea5179882c1516d4df471845765" +checksum = "45d9a5ffa141792f779bd2c72a458493c9762b4405c851783b30bdfb0b02c50a" dependencies = [ - "reallyme-codec", - "reallyme-codec-base64url", - "reallyme-codec-multibase", - "reallyme-codec-multicodec", - "reallyme-codec-multikey", "reallyme-crypto-aes-kw", "reallyme-crypto-aes256-gcm", "reallyme-crypto-aes256-gcm-siv", @@ -1378,6 +1447,7 @@ dependencies = [ "reallyme-crypto-hpke", "reallyme-crypto-jwk", "reallyme-crypto-jwk-multikey", + "reallyme-crypto-kmac", "reallyme-crypto-ml-dsa-44", "reallyme-crypto-ml-dsa-65", "reallyme-crypto-ml-dsa-87", @@ -1398,13 +1468,15 @@ dependencies = [ "reallyme-crypto-slh-dsa", "reallyme-crypto-x-wing", "reallyme-crypto-x25519", + "thiserror", + "zeroize", ] [[package]] name = "reallyme-crypto-aes-kw" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928dcd3dddcd6a670bbf909f4c2844b68686d3be20b729036e59648b4ffc7bd4" +checksum = "794039d2d9307db507c80a4aebc14a9d63983dd4d1ab19fbdc5e251813f1af00" dependencies = [ "aes-kw", "reallyme-crypto-core", @@ -1413,9 +1485,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-aes256-gcm" -version = "0.1.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac30c7be3e8b0ce2dc957f6fb1f0bb540e0767fa5e700838bc52e8c0778a7a1" +checksum = "a4773577e0b2a26043edf808d2b62196be88217bc52041e8f242d908982b71ad" dependencies = [ "aes 0.9.1", "aes-gcm", @@ -1425,9 +1497,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-aes256-gcm-siv" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb36dec471f98a8b0d06a7ec4583f86fbb0a25e745f887241b46368842a12ccb" +checksum = "a1173283e822a88ea578f1062db24629cc5defcade0450891a6fa8037a3d83dc" dependencies = [ "aes 0.9.1", "aes-gcm-siv", @@ -1437,9 +1509,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-argon2id" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c91ccc30c5935c957930e9e9cea83030d602d7faa510daf68f9a146f7cc595" +checksum = "7eb67943cea5496fd2a81115502ef48682f26a47eb4b0939b99c9801061c684d" dependencies = [ "argon2", "reallyme-crypto-core", @@ -1448,9 +1520,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-chacha20-poly1305" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d411ad6b2a300593a155506db3a90c64f5aa6aa88862dacd955ec770929ee48" +checksum = "397b467db25574881c0ec4513796387ff17948537e73542a8a9e58ce7ee6fedb" dependencies = [ "chacha20poly1305", "getrandom 0.4.3", @@ -1460,9 +1532,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-concat-kdf" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed1bb24de4db0e753f2f8fc204bf0832366a6f02b392a2df753e5a5cb5e6c5e" +checksum = "ba8f36144263a30c7049c10440ad4afc9216ce3ece7e008c162df3bfb95576ea" dependencies = [ "reallyme-crypto-core", "sha2", @@ -1471,9 +1543,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-constant-time" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cac2c9e26184225fc19bbffd1ba6cf95f2acf12c46e7ca65285e680c4f01a04" +checksum = "b7e0cfe324ddafb08c87644649afb2811eeefb3923038e61bab6616e2ff9b5de" dependencies = [ "reallyme-crypto-core", "subtle", @@ -1481,37 +1553,35 @@ dependencies = [ [[package]] name = "reallyme-crypto-core" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d5ef8109af8ad6268bd322c4f3adb15f6d05b209907ebf1b49fc2b11e7dd576" +checksum = "42c236066be2f755014b40169975d9bbaf60422a0f7a5b5aded35414e8f06d10" dependencies = [ "thiserror", ] [[package]] name = "reallyme-crypto-csprng" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e020f333ba4455be39accc598d94f5cd97d5e47650b9013605209849ca08f751" +checksum = "40be6cbcead368a4a06394fe9f2e1bcd8ac5ca14b21683b1d045f2caf45b1334" dependencies = [ "getrandom 0.4.3", "reallyme-crypto-core", + "secrecy", + "subtle", "zeroize", ] [[package]] name = "reallyme-crypto-dispatch" -version = "0.1.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0cfc9872b24807a41b07ec60171bf447a4ececee85c44ace79c4684a23895d5" +checksum = "78dae1ca708a04480036b33236157eb4f1bcbf12d904167dba7eb2308d4b776e" dependencies = [ "reallyme-codec-multikey", - "reallyme-crypto-aes256-gcm", - "reallyme-crypto-aes256-gcm-siv", - "reallyme-crypto-chacha20-poly1305", "reallyme-crypto-core", "reallyme-crypto-ed25519", - "reallyme-crypto-hmac", "reallyme-crypto-ml-dsa-44", "reallyme-crypto-ml-dsa-65", "reallyme-crypto-ml-dsa-87", @@ -1522,10 +1592,6 @@ dependencies = [ "reallyme-crypto-p384", "reallyme-crypto-p521", "reallyme-crypto-secp256k1", - "reallyme-crypto-sha2", - "reallyme-crypto-sha2-256", - "reallyme-crypto-sha3", - "reallyme-crypto-sha3-256", "reallyme-crypto-x-wing", "reallyme-crypto-x25519", "thiserror", @@ -1534,21 +1600,21 @@ dependencies = [ [[package]] name = "reallyme-crypto-ed25519" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d883ff8b61c068473bb5d55b804acef208e6d192b49ef4254698e62e1691c23" +checksum = "95077fb769929eba118ede31dce55c6cd934853c9dde6c27f4cc49b1e2139819" dependencies = [ "ed25519-dalek", - "rand", "reallyme-crypto-core", + "reallyme-crypto-csprng", "zeroize", ] [[package]] name = "reallyme-crypto-hkdf" -version = "0.1.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13609c03b4b4903a6e3136e9b038e8e045afef7b4dbdaac85c409a593b44141" +checksum = "7d54ecb927a9a43e27daa67beeffa8701b932cdcbe841a7ab9ef37d7a39ddef5" dependencies = [ "hkdf", "reallyme-crypto-core", @@ -1558,33 +1624,43 @@ dependencies = [ [[package]] name = "reallyme-crypto-hmac" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da57df3ddafc6f4f5d0c5639d444d85afaaf23799d829bf4fcf4d6a87929b5e" +checksum = "a0aaf6d39535bc72eee2e8a62496ba290bb2744a1183758ea54d90cfd5aae45d" dependencies = [ "hmac", "reallyme-crypto-core", "sha2", + "subtle", "zeroize", ] [[package]] name = "reallyme-crypto-hpke" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ff2171ffe2d0dbdc68cdfea36c19bb492a59a26f18ea40e8a02ab7e7e258b03" +checksum = "dccae392c132845e0981901573a45f488986c7d64a67f639dca2e76a99dc16ab" dependencies = [ "getrandom 0.4.3", + "hkdf", + "hmac", "hpke", + "k256", + "ml-kem", + "reallyme-crypto-secp256k1", + "reallyme-crypto-x448", + "sha2", + "shake", + "subtle", "thiserror", "zeroize", ] [[package]] name = "reallyme-crypto-jwk" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "070552a8399ab0b99f94983e3114a6915e99c8ea428ca055e1f1e3a3fa6bea4c" +checksum = "9e7d1a5142875750d976d85ca15a991277f71872fdfb745b93fec4ca05a2c70e" dependencies = [ "reallyme-codec-base64url", "reallyme-codec-jcs", @@ -1597,23 +1673,32 @@ dependencies = [ [[package]] name = "reallyme-crypto-jwk-multikey" -version = "0.1.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "370c45001edac77c48d1c1e461f65351d6e2f77da7e012b8efef6a3d6d8afa8a" +checksum = "729352adf82fb545895c1e470c0e0097a3732deaa5521efe1d69fa7840a31b6c" dependencies = [ - "reallyme-codec-base64url", "reallyme-codec-multikey", "reallyme-crypto-jwk", - "reallyme-crypto-p256", - "reallyme-crypto-secp256k1", "thiserror", ] +[[package]] +name = "reallyme-crypto-kmac" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de63e0cddc6584af726031d7bf388b3e3f25b289cdf735a58bbfc200293ac19f" +dependencies = [ + "reallyme-crypto-core", + "sha3 0.10.9", + "sha3-kmac", + "zeroize", +] + [[package]] name = "reallyme-crypto-ml-dsa-44" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4b38079a6c2792f4ff47970c424b737d1da6e12ea44250b275d35d7212c473" +checksum = "b8d7fc3b252f9a64d11aaa0de176da4083bd49ae9d184685f2a0f8e036ae44a0" dependencies = [ "ml-dsa", "reallyme-crypto-core", @@ -1622,9 +1707,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-ml-dsa-65" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bacb74964487ee8b3827a16b9addc015f77ddcfc668f0503590eff495facca3d" +checksum = "8c93d1edf2cb86ad0788a606a4b627abe8e464b57c087386b337c897e33567ec" dependencies = [ "ml-dsa", "reallyme-crypto-core", @@ -1633,9 +1718,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-ml-dsa-87" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02abac85c5ac62191cad0047eb3472ebcf1c64bb57b26341aa8a490ab344a25a" +checksum = "d7ad5379ce0187227d9b217b2daa17ddf08ae68e8005fbaa7f1ff09fdfb3184a" dependencies = [ "ml-dsa", "reallyme-crypto-core", @@ -1644,9 +1729,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-ml-kem-1024" -version = "0.1.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9ff387f359bae250b8648f6bcde1178e5393190ca8ae9e85c8dfc8341f6b2a8" +checksum = "0ca3a794d0b03989f2580741be6a1f3335b0924a9a7eeb5b0bdf88bb90f099f5" dependencies = [ "ml-kem", "reallyme-crypto-core", @@ -1655,9 +1740,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-ml-kem-512" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a056e9f2c9cda6d545f43659bcc3b2b3f436615615d2ecbfe25a275a69b8f53" +checksum = "96bbf7b6c6bd64aeae0e8d089de5379389d36d7b8a39117cd872565f85b5c01f" dependencies = [ "ml-kem", "reallyme-crypto-core", @@ -1666,9 +1751,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-ml-kem-768" -version = "0.1.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8621d4867f75f3ccd4357a9e925d24e2bce08a3c839d816b4440dd53c20464f" +checksum = "ac3904cc39544ec8338088aad25a3f29f0b4a8fb1e8ef3b52258f2d556600349" dependencies = [ "ml-kem", "reallyme-crypto-core", @@ -1677,12 +1762,13 @@ dependencies = [ [[package]] name = "reallyme-crypto-p256" -version = "0.1.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c21cf8a253d4e2020863daf9be79f38da21b2e58ceb7a5fe0639a5bea019ae50" +checksum = "bca665ee00d699117c6d8391b0c7ce31a8012cecf383e7a6f01f5b44f7bd8b7d" dependencies = [ "ecdsa", "p256", + "reallyme-codec-pem", "reallyme-crypto-core", "sha2", "zeroize", @@ -1690,9 +1776,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-p384" -version = "0.1.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbd69fca5d6ae4ab79d534d53ae41b067f140c0f77a4aca972ef9cc29c1bba12" +checksum = "99b6593952b57c2716db21938d91caf10caf568dde51172a87405ed88af8b214" dependencies = [ "ecdsa", "getrandom 0.4.3", @@ -1704,9 +1790,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-p521" -version = "0.1.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82485fa6dce4ad90814b44a6cee1a20c1e2bb7de508b501e5625f1ed4b229927" +checksum = "e8f11378a9ae7b9c531028c14c8442a7c67969ff3601dfde2f3f35ce43f5b723" dependencies = [ "ecdsa", "getrandom 0.4.3", @@ -1718,9 +1804,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-pbkdf2" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5301281d36317efea00938252b2b204d9eb0bef83f9ff92bd55577f16e7f6a" +checksum = "d67cebf1c2301170a70124f89fb0d60b98da3d27697aad020164b9e2700a1f1f" dependencies = [ "pbkdf2", "reallyme-crypto-core", @@ -1730,9 +1816,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-rsa" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891830f7e286097f07e0c972e47f96e883228d072dc1e2ee082c643e77d668df" +checksum = "d9fb73e3090d6c3e1382d85369f4b5deaf3f76741fd76ea4b90cb41b7075ea48" dependencies = [ "const-oid", "crypto-bigint", @@ -1741,13 +1827,14 @@ dependencies = [ "sha2", "spki", "subtle", + "zeroize", ] [[package]] name = "reallyme-crypto-secp256k1" -version = "0.1.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a041131148618cbab8ea7f109640e1d0ea509c7ec7961130b2c86851f43719a7" +checksum = "2307cc673c30ed5e427d25d0d9f7a9e7059451c42e8ee4c4b7931f3a8209c196" dependencies = [ "ecdsa", "k256", @@ -1758,9 +1845,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-sha2" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd040b02e8af8fa810bcb2f35925c9668b2926900c90692a12724fbf1722d76" +checksum = "d117b4a909bfa43fe547139dbfffd0ef268fac2ba01c3d4406b32d3251fa5bad" dependencies = [ "sha2", "zeroize", @@ -1768,9 +1855,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-sha2-256" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147ffd55087e2a5d0800ab27617b60b438a177797dc651bcdc42b7972c57b466" +checksum = "792c68d08ffadc5d3ab40a907cd6ff5e20afe009ee48861e060b191fbce12aa4" dependencies = [ "sha2", "zeroize", @@ -1778,9 +1865,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-sha3" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab06737e2d3034877dedd7474d07bedae04c1ce7db5581947ef45cdb5e973561" +checksum = "9ac37c76780e55a44cab08859137fb66b2887706d80c0f265539880b87d1baba" dependencies = [ "sha3 0.12.0", "shake", @@ -1789,9 +1876,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-sha3-256" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3250f76ab4bc293f3955343c7cc9d0bcb4a2632cf47316b019800cfc567f9345" +checksum = "aea609279b35ce35662cd19e90b08e8034ab8afb00f972410f12317638b0fa7a" dependencies = [ "sha3 0.12.0", "zeroize", @@ -1799,9 +1886,9 @@ dependencies = [ [[package]] name = "reallyme-crypto-signer" -version = "0.1.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa77ebd4d7d7eea73141a6553309192378b093b4205993cdcd575759d55cd3b" +checksum = "b614ca54b7ca240ece7d5ffbf37671295940c6cf0f18675cedbba7bfb4eadf75" dependencies = [ "reallyme-crypto-core", "reallyme-crypto-dispatch", @@ -1812,21 +1899,21 @@ dependencies = [ [[package]] name = "reallyme-crypto-slh-dsa" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27774407e958ba6694d418c36ef091faf92d7d7dcc211ad75968011a0f8161c5" +checksum = "16775d5196c265f82c8e718fe9f8669e4d2f4013b3bbd40770d096e4afc92e67" dependencies = [ - "rand", "reallyme-crypto-core", + "reallyme-crypto-csprng", "slh-dsa", "zeroize", ] [[package]] name = "reallyme-crypto-x-wing" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c103b9e9c0d810be2138c64d92b9786bb625efa89ffcf8716bdaf8832b38336a" +checksum = "c517f135ea508527124290830409229d3882723a4b4d67f9124178f6385a6f02" dependencies = [ "getrandom 0.4.3", "ml-kem", @@ -1839,15 +1926,27 @@ dependencies = [ [[package]] name = "reallyme-crypto-x25519" -version = "0.1.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd9c65919f60ff1f394c20238c81fc982ec6761fbab018b0858a890c6b5437b" +checksum = "0deb382108befcdbd8dfcf806d304126b6627c699479e8accff8d73c8da60494" dependencies = [ "reallyme-crypto-core", "x25519-dalek", "zeroize", ] +[[package]] +name = "reallyme-crypto-x448" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "592808e870dedc4c352c8d28cf7d6b433c93b524c4dbbb071ad3ceac289dd21c" +dependencies = [ + "getrandom 0.4.3", + "reallyme-crypto-core", + "x448", + "zeroize", +] + [[package]] name = "rfc6979" version = "0.6.0" @@ -1992,6 +2091,17 @@ dependencies = [ "digest 0.11.3", ] +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak 0.1.6", + "zeroize", +] + [[package]] name = "sha3" version = "0.11.0" @@ -1999,7 +2109,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" dependencies = [ "digest 0.11.3", - "keccak", + "keccak 0.2.0", ] [[package]] @@ -2009,10 +2119,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" dependencies = [ "digest 0.11.3", - "keccak", + "keccak 0.2.0", "sponge-cursor", ] +[[package]] +name = "sha3-kmac" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b6b86fb906e40929519c1a38dc349a7f5595778cc00cc20b55eec34fddeb9ec" +dependencies = [ + "generic-array 1.4.4", + "sha3 0.10.9", + "sha3-utils", +] + +[[package]] +name = "sha3-utils" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08e0bf98cc082cbe077f06a707c94ca15796d046cc4cd2593167b5730a6727be" +dependencies = [ + "zerocopy", +] + [[package]] name = "shake" version = "0.1.0" @@ -2020,7 +2150,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" dependencies = [ "digest 0.11.3", - "keccak", + "keccak 0.2.0", "sponge-cursor", ] @@ -2041,10 +2171,10 @@ dependencies = [ ] [[package]] -name = "slab" -version = "0.4.12" +name = "simdutf8" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "slh-dsa" @@ -2066,6 +2196,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "smoothutf8" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4ec95892483d6d94284caccfddb9b77111a4dcac33ed25d624248def0fa5f1" +dependencies = [ + "simdutf8", +] + [[package]] name = "spki" version = "0.8.0" @@ -2093,9 +2232,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -2159,9 +2298,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.12+spec-1.1.0" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap", "toml_datetime", @@ -2178,6 +2317,17 @@ dependencies = [ "winnow", ] +[[package]] +name = "turboshake" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f892c6904b0bd5a9241eac848347abcbbbd2b9f3892bad23ac3e6efab8d6f06a" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", + "sponge-cursor", +] + [[package]] name = "typenum" version = "1.20.1" @@ -2275,9 +2425,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] @@ -2293,6 +2443,19 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "x-wing" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b51507b887016c3925c84591108dadb8c099f776eb04fec6a60ae3519fee856f" +dependencies = [ + "kem", + "ml-kem", + "sha3 0.12.0", + "shake", + "x25519-dalek", +] + [[package]] name = "x25519-dalek" version = "3.0.0" @@ -2305,6 +2468,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "x448" +version = "0.14.0-pre.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c166d06b9fa4328c890d46bda797269fb6aeca96393aeaad0e74b4b11550e06" +dependencies = [ + "ed448-goldilocks", + "zeroize", +] + [[package]] name = "zerocopy" version = "0.8.54" @@ -2347,6 +2520,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd2f034a4bebf216c9e4b7083603e024cf930873fd67830cfb083c9fa33129d9" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index ee0c188..0fa048d 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -9,12 +9,19 @@ publish = false edition = "2021" rust-version = "1.96" +[lints] +workspace = true + [package.metadata] cargo-fuzz = true [dependencies] +buffa = { version = "0.9.0", features = ["json"] } libfuzzer-sys = "0.4" -reallyme-cose = { path = "..", default-features = false, features = ["native"] } +reallyme-cose = { path = "../crates/cose", default-features = false, features = ["native", "wire"] } +reallyme-crypto = { version = "0.3.0", default-features = false, features = ["native", "ml-kem-512", "ml-kem-768", "ml-kem-1024"] } +serde_json = "1.0" +zeroize = "1.8" [[bin]] name = "cose_sign1" @@ -37,5 +44,33 @@ test = false doc = false bench = false +[[bin]] +name = "wire" +path = "fuzz_targets/wire.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "cose_encrypt" +path = "fuzz_targets/cose_encrypt.rs" +test = false +doc = false +bench = false + [workspace] members = ["."] + +[workspace.lints.rust] +unsafe_code = "deny" + +[workspace.lints.clippy] +dbg_macro = "deny" +expect_used = "deny" +large_include_file = "deny" +panic = "deny" +todo = "deny" +unimplemented = "deny" +unreachable = "deny" +unwrap_used = "deny" +wildcard_imports = "deny" diff --git a/fuzz/README.md b/fuzz/README.md index f46e024..8b11e77 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -16,16 +16,28 @@ SDKs and applications pass untrusted input to. - `cose_key`: COSE_Key decode, canonical re-encode, public/private extraction, `kid` derivation, and Multikey conversion. - `multikey_to_cose`: UTF-8 Multikey parsing and COSE_Key conversion. +- `wire`: malformed protobuf bytes, operation-id dispatch, discriminated response and + version-two discriminated-response decode, JSON adapter rejection paths, and guaranteed binary/ProtoJSON + execution of every migrated key, Multikey, and attached/detached Sign1 + operation plus ML-KEM direct encryption, key-wrap encryption, and decryption + for each fuzz input. +- `cose_encrypt`: valid ML-KEM direct/AES-KW construction followed by + structured mutation of headers, recipients, KDF-bound bytes, ciphertext, + and tags, plus fully arbitrary decrypt input. ## Running ```sh cargo install cargo-fuzz --locked -cargo +nightly fuzz build -cargo +nightly fuzz run cose_sign1 -- -max_total_time=300 -rss_limit_mb=4096 -cargo +nightly fuzz run cose_key -- -max_total_time=300 -rss_limit_mb=4096 -cargo +nightly fuzz run multikey_to_cose -- -max_total_time=300 -rss_limit_mb=4096 +cargo +nightly-2026-07-01 fuzz build +cargo +nightly-2026-07-01 fuzz run cose_sign1 -- -max_total_time=900 -rss_limit_mb=4096 +cargo +nightly-2026-07-01 fuzz run cose_key -- -max_total_time=900 -rss_limit_mb=4096 +cargo +nightly-2026-07-01 fuzz run multikey_to_cose -- -max_total_time=900 -rss_limit_mb=4096 +cargo +nightly-2026-07-01 fuzz run wire -- -max_total_time=900 -rss_limit_mb=4096 +cargo +nightly-2026-07-01 fuzz run cose_encrypt -- -max_total_time=900 -rss_limit_mb=4096 ``` Crash inputs are written to `fuzz/artifacts//`. Add a deterministic regression test for any reproducible crash before removing the artifact. +Scheduled CI restores and persists a per-target corpus so later runs continue +exploring from inputs discovered by earlier runs. diff --git a/fuzz/fuzz_targets/cose_encrypt.rs b/fuzz/fuzz_targets/cose_encrypt.rs new file mode 100644 index 0000000..2c295f8 --- /dev/null +++ b/fuzz/fuzz_targets/cose_encrypt.rs @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use reallyme_cose::{ + cose_decrypt_ml_kem_with_external_aad, cose_encrypt_ml_kem_direct_with_external_aad, + cose_encrypt_ml_kem_key_wrap_with_external_aad, cose_key_from_public_bytes, + derive_kid_from_cose_key_public, Algorithm, CoseContentEncryptionAlgorithm, CoseMlKemAlgorithm, + CoseMlKemDecryptRequest, CoseMlKemEncryptRequest, +}; +use zeroize::Zeroizing; + +const EXTERNAL_AAD: &[u8] = b"ReallyMe COSE_Encrypt fuzz AAD"; +const SUPP_PRIV_INFO: &[u8] = b"ReallyMe COSE_Encrypt fuzz private context"; +const MAX_FUZZ_PLAINTEXT_BYTES: usize = 4_096; + +fuzz_target!(|data: &[u8]| { + let selector = data.first().copied().unwrap_or_default(); + let (algorithm, crypto_algorithm) = match selector % 3 { + 0 => (CoseMlKemAlgorithm::MlKem512, Algorithm::MlKem512), + 1 => (CoseMlKemAlgorithm::MlKem768, Algorithm::MlKem768), + _ => (CoseMlKemAlgorithm::MlKem1024, Algorithm::MlKem1024), + }; + let content_algorithm = match selector.wrapping_div(3) % 3 { + 0 => CoseContentEncryptionAlgorithm::Aes128Gcm, + 1 => CoseContentEncryptionAlgorithm::Aes192Gcm, + _ => CoseContentEncryptionAlgorithm::Aes256Gcm, + }; + let seed = [selector.wrapping_add(1); 64]; + let keypair = match algorithm { + CoseMlKemAlgorithm::MlKem512 => { + reallyme_crypto::ml_kem_512::generate_ml_kem_512_keypair_from_seed(&seed) + } + CoseMlKemAlgorithm::MlKem768 => { + reallyme_crypto::ml_kem_768::generate_ml_kem_768_keypair_from_seed(&seed) + } + CoseMlKemAlgorithm::MlKem1024 => { + reallyme_crypto::ml_kem_1024::generate_ml_kem_1024_keypair_from_seed(&seed) + } + _ => return, + }; + let (public_key, private_key) = match keypair { + Ok(keypair) => keypair, + Err(_) => return, + }; + let cose_key = match cose_key_from_public_bytes(crypto_algorithm, &public_key) { + Ok(key) => key, + Err(_) => return, + }; + let kid = match derive_kid_from_cose_key_public(&cose_key) { + Ok(kid) => kid, + Err(_) => return, + }; + let plaintext_start = usize::from(!data.is_empty()); + let plaintext_end = data + .len() + .min(plaintext_start.saturating_add(MAX_FUZZ_PLAINTEXT_BYTES)); + let plaintext = &data[plaintext_start..plaintext_end]; + let request = CoseMlKemEncryptRequest::new( + algorithm, + content_algorithm, + &public_key, + &kid, + plaintext, + Some(SUPP_PRIV_INFO), + ); + let encoded = if selector & 1 == 0 { + cose_encrypt_ml_kem_direct_with_external_aad(&request, EXTERNAL_AAD) + } else { + cose_encrypt_ml_kem_key_wrap_with_external_aad(&request, EXTERNAL_AAD) + }; + let encoded = match encoded { + Ok(encoded) => encoded, + Err(_) => return, + }; + let decrypt = CoseMlKemDecryptRequest::new(&encoded, &private_key, &kid, Some(SUPP_PRIV_INFO)); + let _ = cose_decrypt_ml_kem_with_external_aad(&decrypt, EXTERNAL_AAD); + + // Mutate a valid object so libFuzzer reaches recipient/header/KDF/AEAD + // rejection paths that uniform random bytes would almost never discover. + let mut mutated = Zeroizing::new(encoded.to_vec()); + for mutation in data[plaintext_end..].chunks_exact(2) { + if mutated.is_empty() { + break; + } + let index = usize::from(mutation[0]) % mutated.len(); + mutated[index] ^= mutation[1]; + } + let mutated_request = + CoseMlKemDecryptRequest::new(&mutated, &private_key, &kid, Some(SUPP_PRIV_INFO)); + let _ = cose_decrypt_ml_kem_with_external_aad(&mutated_request, EXTERNAL_AAD); + + // Retain a fully arbitrary parser path as well as the structured mutations. + let raw_request = CoseMlKemDecryptRequest::new(data, &private_key, &kid, Some(SUPP_PRIV_INFO)); + let _ = cose_decrypt_ml_kem_with_external_aad(&raw_request, EXTERNAL_AAD); +}); diff --git a/fuzz/fuzz_targets/cose_sign1.rs b/fuzz/fuzz_targets/cose_sign1.rs index 3324ee7..a7321fb 100644 --- a/fuzz/fuzz_targets/cose_sign1.rs +++ b/fuzz/fuzz_targets/cose_sign1.rs @@ -10,13 +10,14 @@ fuzz_target!(|data: &[u8]| { // An unresolvable kid exercises the early fail-closed path; a resolver // that echoes attacker-controlled bytes as the public key drives the // full pipeline: header policy, signature re-encoding, backend verify. - let _ = reallyme_cose::cose_verify1(data, |_| None); - let _ = reallyme_cose::cose_verify1(data, |kid| Some(kid.to_vec())); + let _ = reallyme_cose::cose_verify1(data, |_, _| None); + let _ = reallyme_cose::cose_verify1(data, |_, kid| Some(kid.to_vec())); if let Some((split_byte, rest)) = data.split_first() { let split = usize::from(*split_byte) % rest.len().saturating_add(1); let (cose_bytes, payload) = rest.split_at(split); - let _ = reallyme_cose::cose_verify1_detached(cose_bytes, payload, |_| None); - let _ = reallyme_cose::cose_verify1_detached(cose_bytes, payload, |kid| Some(kid.to_vec())); + let _ = reallyme_cose::cose_verify1_detached(cose_bytes, payload, |_, _| None); + let _ = + reallyme_cose::cose_verify1_detached(cose_bytes, payload, |_, kid| Some(kid.to_vec())); } }); diff --git a/fuzz/fuzz_targets/wire.rs b/fuzz/fuzz_targets/wire.rs new file mode 100644 index 0000000..0383245 --- /dev/null +++ b/fuzz/fuzz_targets/wire.rs @@ -0,0 +1,225 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +#![no_main] + +use buffa::{EnumValue, Message}; +use libfuzzer_sys::fuzz_target; +use reallyme_cose::wire::cose_algorithm_identifier; +use reallyme_cose::wire::cose_operation_request::Operation; +use reallyme_cose::wire::{ + CoseAlgorithmIdentifier, CoseContentEncryptionAlgorithm, CoseKemAlgorithm, CoseKeyBytesRequest, + CoseKeyFromPrivateBytesRequest, CoseKeyFromPublicBytesRequest, CoseMlKemDecryptRequest, + CoseMlKemEncryptRequest, CoseMultikeyToCoseKeyRequest, CoseOperationRequest, + CoseSign1CreateDetachedRequest, CoseSign1CreateRequest, CoseSign1VerifyDetachedRequest, + CoseSign1VerifyRequest, CoseSignatureAlgorithm, +}; +use zeroize::Zeroizing; + +fuzz_target!(|data: &[u8]| { + let result = reallyme_cose::wire::execute_operation_proto(data); + let _ = reallyme_cose::wire::decode_operation_response(&result); + let _ = reallyme_cose::wire::decode_operation_response(data); + let _ = reallyme_cose::wire::decode_cose_error(data); + + if let Ok(json) = core::str::from_utf8(data) { + let result = reallyme_cose::wire::execute_operation_proto_json(json); + let _ = reallyme_cose::wire::decode_operation_response(&result); + } + + // Sparse operation fields are almost never selected by arbitrary protobuf + // bytes. Wrap every input so all migrated key-family routes remain + // continuously reachable through both generated transport lanes. + let signature_algorithm = signature_algorithm(data); + execute(Operation::KeyParse(Box::new(key_request(data)))); + execute(Operation::KeyFromPublicBytes(Box::new( + CoseKeyFromPublicBytesRequest { + algorithm: signature_identifier(signature_algorithm), + public_key: data.to_vec(), + __buffa_unknown_fields: Default::default(), + }, + ))); + execute(Operation::KeyFromPrivateBytes(Box::new( + CoseKeyFromPrivateBytesRequest { + algorithm: signature_identifier(signature_algorithm), + private_key: data.to_vec(), + public_key: data.to_vec(), + has_public_key: true, + __buffa_unknown_fields: Default::default(), + }, + ))); + execute(Operation::KeyToPublicBytes(Box::new(key_request(data)))); + execute(Operation::KeyToPrivateBytes(Box::new(key_request(data)))); + execute(Operation::KeyDerivePublicKid(Box::new(key_request(data)))); + execute(Operation::KeyToMultikey(Box::new(key_request(data)))); + + if let Ok(multikey) = core::str::from_utf8(data) { + execute(Operation::MultikeyToCoseKey(Box::new( + CoseMultikeyToCoseKeyRequest { + multikey: multikey.to_owned(), + __buffa_unknown_fields: Default::default(), + }, + ))); + } + + // Sign1 operation tags are equally sparse. Exercise attached and detached + // creation and verification through both transports on every iteration. + execute(Operation::Sign1Create(Box::new(sign1_create_request( + data, + signature_algorithm, + )))); + execute(Operation::Sign1CreateDetached(Box::new( + sign1_create_detached_request(data, signature_algorithm), + ))); + execute(Operation::Sign1Verify(Box::new(sign1_verify_request( + data, + signature_algorithm, + )))); + execute(Operation::Sign1VerifyDetached(Box::new( + sign1_verify_detached_request(data, signature_algorithm), + ))); + execute(Operation::MlKemEncryptDirect(Box::new( + ml_kem_encrypt_request(data), + ))); + execute(Operation::MlKemEncryptKeyWrap(Box::new( + ml_kem_encrypt_request(data), + ))); + execute(Operation::MlKemDecrypt(Box::new(ml_kem_decrypt_request( + data, + )))); +}); + +fn ml_kem_encrypt_request(data: &[u8]) -> CoseMlKemEncryptRequest { + CoseMlKemEncryptRequest { + kem_algorithm: EnumValue::from(CoseKemAlgorithm::MlKem512), + content_algorithm: EnumValue::from(CoseContentEncryptionAlgorithm::Aes128Gcm), + recipient_public_key: data.to_vec(), + recipient_kid: data.to_vec(), + plaintext: data.to_vec(), + external_aad: data.to_vec(), + supp_priv_info: data.to_vec(), + has_supp_priv_info: true, + __buffa_unknown_fields: Default::default(), + } +} + +fn ml_kem_decrypt_request(data: &[u8]) -> CoseMlKemDecryptRequest { + CoseMlKemDecryptRequest { + cose_encrypt: data.to_vec(), + recipient_private_key: data.to_vec(), + expected_recipient_kid: data.to_vec(), + external_aad: data.to_vec(), + supp_priv_info: data.to_vec(), + has_supp_priv_info: true, + __buffa_unknown_fields: Default::default(), + } +} + +fn sign1_create_request(data: &[u8], algorithm: CoseSignatureAlgorithm) -> CoseSign1CreateRequest { + CoseSign1CreateRequest { + algorithm: EnumValue::from(algorithm), + payload: data.to_vec(), + private_key: data.to_vec(), + kid: data.to_vec(), + has_kid: true, + options: Default::default(), + external_aad: data.to_vec(), + __buffa_unknown_fields: Default::default(), + } +} + +fn sign1_create_detached_request( + data: &[u8], + algorithm: CoseSignatureAlgorithm, +) -> CoseSign1CreateDetachedRequest { + CoseSign1CreateDetachedRequest { + algorithm: EnumValue::from(algorithm), + payload: data.to_vec(), + private_key: data.to_vec(), + kid: data.to_vec(), + has_kid: true, + options: Default::default(), + external_aad: data.to_vec(), + __buffa_unknown_fields: Default::default(), + } +} + +fn sign1_verify_request(data: &[u8], algorithm: CoseSignatureAlgorithm) -> CoseSign1VerifyRequest { + CoseSign1VerifyRequest { + cose_sign1: data.to_vec(), + public_key: data.to_vec(), + max_cose_sign1_bytes: 0, + max_detached_payload_bytes: 0, + require_kid: false, + allowed_algorithms: vec![EnumValue::from(algorithm)], + external_aad: data.to_vec(), + expected_kid: Vec::new(), + __buffa_unknown_fields: Default::default(), + } +} + +fn sign1_verify_detached_request( + data: &[u8], + algorithm: CoseSignatureAlgorithm, +) -> CoseSign1VerifyDetachedRequest { + CoseSign1VerifyDetachedRequest { + cose_sign1: data.to_vec(), + payload: data.to_vec(), + public_key: data.to_vec(), + max_cose_sign1_bytes: 0, + max_detached_payload_bytes: 0, + require_kid: false, + allowed_algorithms: vec![EnumValue::from(algorithm)], + external_aad: data.to_vec(), + expected_kid: Vec::new(), + __buffa_unknown_fields: Default::default(), + } +} + +fn key_request(data: &[u8]) -> CoseKeyBytesRequest { + CoseKeyBytesRequest { + cose_key: data.to_vec(), + __buffa_unknown_fields: Default::default(), + } +} + +fn signature_identifier( + algorithm: CoseSignatureAlgorithm, +) -> buffa::MessageField> { + buffa::MessageField::some(CoseAlgorithmIdentifier { + algorithm: Some(cose_algorithm_identifier::Algorithm::Signature( + EnumValue::from(algorithm), + )), + __buffa_unknown_fields: Default::default(), + }) +} + +fn signature_algorithm(data: &[u8]) -> CoseSignatureAlgorithm { + match data.first().copied().unwrap_or_default() % 8 { + 0 => CoseSignatureAlgorithm::Ed25519, + 1 => CoseSignatureAlgorithm::EcdsaP256Sha256, + 2 => CoseSignatureAlgorithm::EcdsaP384Sha384, + 3 => CoseSignatureAlgorithm::EcdsaP521Sha512, + 4 => CoseSignatureAlgorithm::EcdsaSecp256k1Sha256, + 5 => CoseSignatureAlgorithm::MlDsa44, + 6 => CoseSignatureAlgorithm::MlDsa65, + _ => CoseSignatureAlgorithm::MlDsa87, + } +} + +fn execute(operation: Operation) { + let request = CoseOperationRequest { + operation: Some(operation), + __buffa_unknown_fields: Default::default(), + }; + let encoded_request = Zeroizing::new(request.encode_to_vec()); + let result = reallyme_cose::wire::execute_operation_proto(&encoded_request); + let _ = reallyme_cose::wire::decode_operation_response_for_request(&request, &result); + + if let Ok(json) = serde_json::to_string(&request) { + let json = Zeroizing::new(json); + let result = reallyme_cose::wire::execute_operation_proto_json(&json); + let _ = reallyme_cose::wire::decode_operation_response_for_request(&request, &result); + } +} diff --git a/proto/reallyme/cose/v1/cose.proto b/proto/reallyme/cose/v1/cose.proto deleted file mode 100644 index b71051d..0000000 --- a/proto/reallyme/cose/v1/cose.proto +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved -// -// SPDX-License-Identifier: Apache-2.0 - -syntax = "proto3"; - -package reallyme.cose.v1; - -option go_package = "github.com/reallyme/cose/gen/go/reallyme/cose/v1;cosev1"; -option java_multiple_files = true; -option java_outer_classname = "CoseProto"; -option java_package = "me.really.cose.v1"; - -// CoseError is the public, non-PII error envelope for COSE boundary failures. -// The oneof keeps COSE_Sign1, COSE_Key, and Multikey failures distinct while -// the shared CoseErrorReason enum provides stable cross-language reason codes. -message CoseError { - oneof error { - CoseSign1Error sign1 = 1; - CoseKeyError key = 2; - CoseMultikeyError multikey = 3; - } -} - -// CoseSign1Error describes COSE_Sign1 structure, protected-header policy, -// signing, and signature-verification failures. -message CoseSign1Error { - // Reason must be one of the COSE_ERROR_REASON_COMMON_* or - // COSE_ERROR_REASON_SIGN1_* values. - CoseErrorReason reason = 1; -} - -// CoseKeyError describes COSE_Key structure, algorithm/key binding, key -// material extraction, and deterministic kid derivation failures. -message CoseKeyError { - // Reason must be one of the COSE_ERROR_REASON_COMMON_* or - // COSE_ERROR_REASON_KEY_* values. - CoseErrorReason reason = 1; -} - -// CoseMultikeyError describes Multikey decoding and COSE_Key conversion -// failures. -message CoseMultikeyError { - // Reason must be one of the COSE_ERROR_REASON_COMMON_*, - // COSE_ERROR_REASON_KEY_*, or COSE_ERROR_REASON_MULTIKEY_* values. - CoseErrorReason reason = 1; -} - -// CoseErrorReason is the component-owned reason-code enum for reallyme/cose. -// Values are stable public boundary codes; internal Rust errors must map into -// one of these before crossing RPC, SDK, FFI, storage, audit, or telemetry -// boundaries. Numeric ranges are intentionally split by COSE subpart: -// 100-199: common COSE binary, CBOR, algorithm, and crypto failures -// 200-299: COSE_Sign1 -// 300-399: COSE_Key and kid derivation -// 400-499: Multikey interop -enum CoseErrorReason { - COSE_ERROR_REASON_UNSPECIFIED = 0; - - // Common COSE binary and CBOR boundary failures. - COSE_ERROR_REASON_COMMON_CBOR = 100; - COSE_ERROR_REASON_COMMON_INVALID_FORMAT = 101; - COSE_ERROR_REASON_COMMON_RESOURCE_LIMIT_EXCEEDED = 102; - COSE_ERROR_REASON_COMMON_NON_CANONICAL_CBOR = 103; - COSE_ERROR_REASON_COMMON_UNEXPECTED_CBOR_TAG = 104; - COSE_ERROR_REASON_COMMON_DUPLICATE_MAP_LABEL = 105; - - // Common algorithm and cryptographic backend failures. - COSE_ERROR_REASON_COMMON_UNSUPPORTED_ALGORITHM = 120; - COSE_ERROR_REASON_COMMON_CRYPTO_FAILED = 121; - - // COSE_Sign1 payload, policy, and key-resolution failures. - COSE_ERROR_REASON_SIGN1_MISSING_PAYLOAD = 200; - COSE_ERROR_REASON_SIGN1_MISSING_KID = 201; - COSE_ERROR_REASON_SIGN1_KEY_NOT_RESOLVED = 202; - COSE_ERROR_REASON_SIGN1_UNSUPPORTED_CRITICAL_HEADER = 203; - COSE_ERROR_REASON_SIGN1_UNPROTECTED_HEADER_NOT_ALLOWED = 204; - - // COSE_Sign1 signature encoding, verification, and signing failures. - COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE = 220; - COSE_ERROR_REASON_SIGN1_MISSING_PRIVATE_KEY = 221; - - // COSE_Key material and key-shape failures. - COSE_ERROR_REASON_KEY_MISSING_KEY_MATERIAL = 300; - COSE_ERROR_REASON_KEY_INVALID_KEY_MATERIAL = 301; - - // Multikey decoding and COSE_Key conversion failures. - COSE_ERROR_REASON_MULTIKEY_INVALID_MULTIKEY = 400; -} diff --git a/scripts/check-wasm-lane.mjs b/scripts/check-wasm-lane.mjs new file mode 100755 index 0000000..35e7813 --- /dev/null +++ b/scripts/check-wasm-lane.mjs @@ -0,0 +1,49 @@ +#!/usr/bin/env node +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +import { spawnSync } from "node:child_process"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(fileURLToPath(new URL("..", import.meta.url))); +const wasmTarget = "wasm32-unknown-unknown"; +const cargoArgs = [ + "check", + "--workspace", + "--target", + wasmTarget, + "--no-default-features", + "--features", + "wasm", +]; + +function run(command, args, options = {}) { + return spawnSync(command, args, { + cwd: root, + encoding: "utf8", + stdio: options.capture ? "pipe" : "inherit", + }); +} + +const installedTargets = run("rustup", ["target", "list", "--installed"], { capture: true }); +if (installedTargets.error) { + console.error("failed to inspect installed Rust targets"); + console.error("run: rustup target add wasm32-unknown-unknown"); + process.exit(1); +} + +if (!installedTargets.stdout.split(/\r?\n/u).includes(wasmTarget)) { + console.error(`missing Rust target: ${wasmTarget}`); + console.error(`run: rustup target add ${wasmTarget}`); + process.exit(1); +} + +console.error(`running wasm lane: cargo ${cargoArgs.join(" ")}`); +const result = run("cargo", cargoArgs); +if (result.error) { + console.error("failed to run cargo"); + process.exit(1); +} +process.exit(result.status ?? 1); diff --git a/scripts/check_release_readiness.mjs b/scripts/check_release_readiness.mjs index e414e4a..452548a 100755 --- a/scripts/check_release_readiness.mjs +++ b/scripts/check_release_readiness.mjs @@ -3,158 +3,1288 @@ // // SPDX-License-Identifier: Apache-2.0 -import { readFileSync } from "node:fs"; -import { resolve } from "node:path"; -import { fileURLToPath } from "node:url"; -import { spawnSync } from "node:child_process"; +import { isDeepStrictEqual } from "node:util"; + +import { createReleaseReadinessContext } from "./release-readiness/core.mjs"; +import { assertOperationContractRouting } from "./release-readiness/operation-contract-routing.mjs"; + +const { + readText, + readJson, + fail, + loadTrackedFiles, + assertContains, + assertNotContains, + assertNotMatches, + runCommands, + assertProtoContract, + assertNodeWorkflowJobsPinNode, + assertReallyMeProtobufReleasePolicy, + assertWorkflowActionsPinned, + assertCargoFuzzWorkflowPolicy, + assertReallyMeVendoredCorePolicy, + assertTextPolicy, + assertCargoMetadataPolicy, +} = createReleaseReadinessContext({ + scriptUrl: import.meta.url, + requireTrackedFiles: true, +}); + +assertWorkflowActionsPinned(); +assertCargoFuzzWorkflowPolicy({ + version: "0.13.2", + requiredInstallSteps: [ + { job: "build", name: "Install cargo-fuzz" }, + { job: "scheduled", name: "Install cargo-fuzz" }, + ], +}); +assertReallyMeVendoredCorePolicy(); -const root = resolve(fileURLToPath(new URL("..", import.meta.url))); const expectedPackageName = "reallyme-cose"; -const expectedVersion = "0.1.2"; -const packageListArgs = - process.env.GITHUB_ACTIONS === "true" - ? ["package", "--list", "-p", expectedPackageName] - : ["package", "--list", "-p", expectedPackageName, "--allow-dirty"]; -const requiredRegistryDeps = new Map([ - ["reallyme-codec", "0.1.1"], - ["reallyme-crypto", "0.1.6"], +const expectedProtoPackageName = "reallyme-cose-proto"; +const expectedVersion = "0.2.0"; +const generatedFreshnessMode = process.argv.includes("--generated-freshness"); +const policyOnlyMode = process.argv.includes("--policy-only"); +const releasePackagesMode = process.argv.includes("--release-packages"); +const selectedModes = [generatedFreshnessMode, policyOnlyMode, releasePackagesMode].filter(Boolean); +const supportedArguments = new Set([ + "--generated-freshness", + "--policy-only", + "--release-packages", ]); +if (process.argv.slice(2).some((argument) => !supportedArguments.has(argument))) { + fail("release readiness received an unsupported mode"); +} +if (selectedModes.length > 1) { + fail("release readiness modes are mutually exclusive"); +} +const verifiedBufInstallCommand = `set -euo pipefail +install_dir="$RUNNER_TEMP/buf/bin" +mkdir -p "$install_dir" +curl --fail-with-body --location --proto '=https' --tlsv1.2 \\ + --retry 5 --retry-all-errors \\ + --output "$install_dir/buf" \\ + "https://github.com/bufbuild/buf/releases/download/v\${BUF_VERSION}/buf-Linux-x86_64" +printf '%s %s\\n' "$BUF_LINUX_X86_64_SHA256" "$install_dir/buf" \\ + | sha256sum --check --strict +chmod 0755 "$install_dir/buf" +printf '%s\\n' "$install_dir" >> "$GITHUB_PATH" +"$install_dir/buf" --version`; -function fail(message) { - console.error(`release readiness check failed: ${message}`); - process.exit(1); +const expectedPlatformScope = { + schema: "reallyme.cose.platform_scope.v1", + spdxCopyrightText: "Copyright © 2026 ReallyMe LLC. All rights reserved", + spdxLicenseIdentifier: "Apache-2.0", + release: expectedVersion, + immediateScope: "rust_and_protobuf", + publishableCrates: [expectedProtoPackageName, expectedPackageName], + rustRuntimeLanes: ["native", "wasm"], + platformLanes: [ + { lane: "swift", decision: "planned_later_release" }, + { lane: "android_kotlin", decision: "planned_later_release" }, + { lane: "kotlin_jvm", decision: "planned_later_release" }, + { lane: "native_abi", decision: "not_approved" }, + { lane: "typescript_wasm_npm", decision: "not_approved" }, + ], + protobufSwiftMetadataIsPackagingApproval: false, + wasmRuntimeIsNpmPackagingApproval: false, +}; +const platformScopePath = "docs/platform-scope-0.2.0.json"; +const platformScope = readJson(platformScopePath); +if (!isDeepStrictEqual(platformScope, expectedPlatformScope)) { + fail(`${platformScopePath} must exactly match the approved 0.2.0 platform scope`); } -function readText(path) { - return readFileSync(resolve(root, path), "utf8"); +const forbiddenPlatformPathPrefixes = [ + "packages/", + "sdk/", + "sdks/", + "crates/ffi/", + "crates/jni/", + "crates/reallyme-cose-ffi/", + "crates/reallyme-cose-jni/", + "crates/cose/src/ffi/", + "crates/cose/src/jni/", +]; +const forbiddenPlatformPaths = new Set(["crates/cose/src/ffi.rs", "crates/cose/src/jni.rs"]); +const forbiddenPlatformManifestNames = new Set([ + "Package.swift", + "build.gradle", + "build.gradle.kts", + "settings.gradle", + "settings.gradle.kts", + "gradlew", + "pom.xml", +]); +for (const trackedFile of loadTrackedFiles()) { + const manifestName = trackedFile.slice(trackedFile.lastIndexOf("/") + 1); + if ( + forbiddenPlatformPathPrefixes.some((prefix) => trackedFile.startsWith(prefix)) || + forbiddenPlatformPaths.has(trackedFile) || + forbiddenPlatformManifestNames.has(manifestName) + ) { + fail(`${trackedFile} is outside the approved Rust/protobuf-only 0.2.0 scope`); + } } -function assertContains(path, needle) { - if (!readText(path).includes(needle)) { - fail(`${path} does not contain ${needle}`); +for (const obsoleteRoot of ["benches/", "conformance/", "src/", "tests/"]) { + for (const trackedFile of loadTrackedFiles()) { + if (trackedFile.startsWith(obsoleteRoot)) { + fail(`${trackedFile} is outside the canonical workspace layout`); + } } } -function run(command, args, options = {}) { - const result = spawnSync(command, args, { - cwd: root, - encoding: "utf8", - stdio: options.capture ? "pipe" : "inherit", - env: options.env ?? process.env, - }); - if (result.error) { - throw result.error; +const MAX_HAND_WRITTEN_RUST_LINES = 500; +const inlineTestModulePattern = + /#\[cfg\(test\)\]\s*(?:#\[[^\]]+\]\s*)*mod\s+[A-Za-z_][A-Za-z0-9_]*\s*\{/u; +for (const trackedFile of loadTrackedFiles()) { + if (!trackedFile.startsWith("crates/cose/src/") || !trackedFile.endsWith(".rs")) { + continue; } - if (result.status !== 0) { - if (options.capture) { - process.stdout.write(result.stdout); - process.stderr.write(result.stderr); - } - process.exit(result.status ?? 1); + const source = readText(trackedFile); + const lineCount = source.split("\n").length; + if (lineCount > MAX_HAND_WRITTEN_RUST_LINES) { + fail( + `${trackedFile} has ${lineCount} lines; hand-written Rust modules are capped at ${MAX_HAND_WRITTEN_RUST_LINES}`, + ); } - return result; + if (inlineTestModulePattern.test(source)) { + fail(`${trackedFile} must place substantive test modules in a dedicated source file`); + } +} +for (const forbiddenPlatformFeature of ["swift", "kotlin", "android", "jni", "ffi"]) { + assertNotMatches( + "crates/cose/Cargo.toml", + new RegExp(`^${forbiddenPlatformFeature}\\s*=`, "mu"), + `a Rust ${forbiddenPlatformFeature} feature`, + ); } +assertNotMatches( + "crates/cose/Cargo.toml", + /\bcrate-type\s*=\s*\[[^\]]*"(?:cdylib|staticlib)"/su, + "a platform-native Rust library artifact", +); +assertContains("README.md", "## 0.2.0 Platform Scope"); +assertContains( + "README.md", + "The `0.2.0` distribution does not include Swift, Android/Kotlin, Kotlin/JVM", +); + +assertNodeWorkflowJobsPinNode({ nodeVersion: "24" }); -const cargoToml = readText("Cargo.toml"); -assertContains("Cargo.toml", `name = "${expectedPackageName}"`); -assertContains("Cargo.toml", `version = "${expectedVersion}"`); -assertContains("Cargo.toml", "publish = true"); +assertContains("Cargo.toml", 'default-members = ["crates/cose"]'); +assertContains("Cargo.toml", 'members = ["crates/cose", "crates/proto"]'); assertContains("Cargo.toml", 'repository = "https://github.com/reallyme/cose"'); -assertContains("Cargo.toml", 'include = ['); -assertContains("Cargo.toml", '"/src/**/*.rs"'); -assertContains("Cargo.toml", '"/tests/**/*.rs"'); -assertContains("Cargo.toml", '"/conformance/**/*.json"'); -assertContains("Cargo.toml", '"/proto/**/*.proto"'); -assertContains("Cargo.toml", '"/buf.yaml"'); -assertContains("Cargo.toml", '"/README.md"'); -assertContains("Cargo.toml", '"/LICENSE"'); -assertContains("Cargo.toml", '"/NOTICE"'); +assertNotMatches("Cargo.toml", /^\[package\]$/mu, "a package at the workspace root"); +assertContains("crates/cose/Cargo.toml", `name = "${expectedPackageName}"`); +assertContains("crates/cose/Cargo.toml", `version = "${expectedVersion}"`); +assertContains("crates/cose/Cargo.toml", "publish = true"); +assertContains("crates/cose/Cargo.toml", 'include = ['); +assertContains("crates/cose/Cargo.toml", '"/src/**/*.rs"'); +assertContains("crates/cose/Cargo.toml", '"/README.md"'); +assertContains("crates/cose/Cargo.toml", '"/LICENSE"'); +assertContains("crates/cose/Cargo.toml", '"/NOTICE"'); +assertContains("crates/cose/Cargo.toml", 'wire = ['); +assertContains("crates/cose/Cargo.toml", '"dep:buffa"'); +assertContains("crates/cose/Cargo.toml", '"dep:reallyme-cose-proto"'); +assertContains("crates/cose/Cargo.toml", '"dep:serde"'); +assertContains("crates/cose/Cargo.toml", '"dep:serde_json"'); +assertContains( + "Cargo.toml", + 'reallyme-codec = { version = "0.2.0", default-features = false, features = ["base64url", "cbor", "multikey"] }', +); +assertNotContains("Cargo.toml", 'path = "../codec'); +assertContains("crates/proto/Cargo.toml", '"buffa/json"'); +assertContains("crates/cose/src/lib.rs", 'reallyme-cose `wire` requires a runtime lane'); +assertContains("Cargo.toml", 'buffa = { version = "0.9.0", features = ["json"] }'); +assertContains( + "Cargo.toml", + `reallyme-cose-proto = { version = "${expectedVersion}", path = "crates/proto", default-features = false }`, +); +assertContains("Cargo.toml", 'serde = { version = "1.0", features = ["derive"] }'); +assertContains("Cargo.toml", 'serde_json = "1.0"'); +assertContains("Cargo.toml", 'allocation-counter = "0.8.1"'); +assertContains("Cargo.toml", 'criterion = "0.8.2"'); +assertContains("crates/cose/Cargo.toml", 'name = "operation_performance"'); +assertTextPolicy({ + files: [ + { + path: "Cargo.toml", + required: ["overflow-checks = true"], + forbidden: ["[patch.crates-io]"], + }, + { + path: ".cargo/config.toml", + required: [ + 'check-wasm = "check --workspace --target wasm32-unknown-unknown --no-default-features --features wasm"', + ], + }, + ], +}); assertContains("README.md", "actions/workflows/rust-ci.yml/badge.svg"); assertContains("README.md", "crates.io/crates/reallyme-cose"); assertContains("README.md", "Unsupported COSE Surface"); assertContains("README.md", "Resource Limits"); -assertContains("README.md", "Independent Vector Audit"); -assertContains("conformance/vectors/manifest.json", "reallyme.cose.conformance.vector_manifest.v1"); +assertContains("README.md", "COSE-Layer Vector Audit"); +assertContains("README.md", "The `wire` feature is intentionally non-additive"); +assertContains("crates/cose/Cargo.toml", "[package.metadata.cargo_check_external_types]"); +assertContains("crates/cose/Cargo.toml", '"crypto_core::algorithm::Algorithm"'); +assertContains("crates/cose/Cargo.toml", '"reallyme_cose_proto::*"'); +assertContains("crates/cose/Cargo.toml", '"zeroize::Zeroizing"'); +assertNotContains("Cargo.toml", "getrandom02"); +assertNotContains("crates/cose/Cargo.toml", "getrandom02"); +assertContains("README.md", "cargo check-wasm"); +assertContains("scripts/check-wasm-lane.mjs", "rustup target add wasm32-unknown-unknown"); +assertContains( + "scripts/release-readiness/operation-contract-routes.mjs", + "export const OPERATION_CONTRACT_ROUTES", +); +assertContains( + "scripts/release-readiness/operation-contract-routing.mjs", + "collectOperationContractRoutingViolations", +); +assertContains( + "scripts/release-readiness/operation-contract-routing.test.mjs", + "current operation contract satisfies every routing invariant", +); +assertContains("fuzz/Cargo.toml", 'name = "wire"'); +assertContains("fuzz/fuzz_targets/wire.rs", "execute_operation_proto"); +assertContains("fuzz/fuzz_targets/wire.rs", "execute_operation_proto_json"); +assertContains("fuzz/fuzz_targets/wire.rs", "decode_operation_response"); +assertContains("fuzz/fuzz_targets/wire.rs", "decode_operation_response_for_request"); +assertContains("fuzz/fuzz_targets/wire.rs", "Operation::KeyParse"); +for (const migratedKeyOperation of [ + "Operation::KeyFromPublicBytes", + "Operation::KeyFromPrivateBytes", + "Operation::KeyToPublicBytes", + "Operation::KeyToPrivateBytes", + "Operation::KeyDerivePublicKid", + "Operation::KeyToMultikey", + "Operation::MultikeyToCoseKey", +]) { + assertContains("fuzz/fuzz_targets/wire.rs", migratedKeyOperation); +} +for (const migratedSign1Operation of [ + "Operation::Sign1Create", + "Operation::Sign1CreateDetached", + "Operation::Sign1Verify", + "Operation::Sign1VerifyDetached", +]) { + assertContains("fuzz/fuzz_targets/wire.rs", migratedSign1Operation); +} +for (const migratedEncryptOperation of [ + "Operation::MlKemEncryptDirect", + "Operation::MlKemEncryptKeyWrap", + "Operation::MlKemDecrypt", +]) { + assertContains("fuzz/fuzz_targets/wire.rs", migratedEncryptOperation); +} +assertContains("fuzz/README.md", "`wire`"); +assertContains("fuzz/Cargo.toml", 'name = "cose_encrypt"'); +assertContains("fuzz/fuzz_targets/cose_encrypt.rs", "cose_decrypt_ml_kem_with_external_aad"); +assertContains("fuzz/README.md", "`cose_encrypt`"); +assertContains(".github/workflows/fuzz.yml", "cose_encrypt"); +assertContains("crates/cose/src/encrypt/types.rs", "pub enum CoseMlKemAlgorithm"); +assertContains("crates/cose/src/encrypt/types.rs", "#[non_exhaustive]\npub struct CoseMlKemEncryptRequest"); +assertContains("crates/cose/src/encrypt/types.rs", "pub(super) kem_algorithm: CoseMlKemAlgorithm"); +assertContains("crates/cose/src/encrypt/types.rs", "pub const fn new("); +assertContains("crates/cose/src/zeroize_coset.rs", "pub(crate) fn zeroize_cose_encrypt"); +assertContains("crates/cose/src/zeroize_coset.rs", "pub(crate) fn zeroize_cose_sign1"); +for (const sensitiveCborEncoder of [ + "crates/cose/src/encrypt/codec.rs", + "crates/cose/src/encrypt/kdf.rs", + "crates/cose/src/key/convert.rs", + "crates/cose/src/sign1/sign.rs", +]) { + assertContains(sensitiveCborEncoder, "encode_cbor_value("); + assertNotContains(sensitiveCborEncoder, "ciborium::ser::into_writer"); +} +assertContains("crates/cose/src/sign1/sign.rs", "encode_protected_header(protected)?"); +assertNotContains("crates/cose/src/sign1/sign.rs", "protected.clone().to_vec()"); +assertContains("crates/cose/src/limits/validate.rs", "core::str::from_utf8(text)"); +assertContains("README.md", "generated `PartialEq` is a schema"); +assertContains( + "README.md", + "pair ML-KEM-512 with AES-128-GCM, ML-KEM-768 with", +); +assertContains( + "README.md", + "Every native Sign1 key resolver receives `(expected_algorithm, protected_kid)`.", +); +assertContains( + "crates/cose/src/sign1/verify.rs", + "public_key_resolver: impl Fn(Algorithm, &[u8])", +); +assertContains( + "crates/cose/src/key/ec.rs", + "validate_supplied_point(profile, public_key, raw_len, uncompressed_len)?", +); +assertContains( + "crates/cose/src/key/validate_material.rs", + "SECP256K1_VALIDATION_SIGNATURE_BYTES", +); +assertContains("crates/cose/src/key/convert.rs", "Result>, CoseError>"); +assertContains("crates/cose/src/key/parse.rs", "pub(crate) fn parse_cose_key("); +assertContains("crates/cose/src/key/parse.rs", "fn decode_owned_cose_key("); +assertContains( + "crates/cose/src/key/parse.rs", + "parse_cose_key(CoseKeyParseInput::new(bytes))", +); +assertContains("crates/cose/src/key/mod.rs", "pub use parse::cose_key_from_slice;"); +assertNotContains("crates/cose/src/key/convert.rs", "pub fn cose_key_from_slice"); +assertContains( + "crates/cose/src/operation_contract/key/parse.rs", + "parse_cose_key(CoseKeyParseInput::new(&encoded_key))", +); +assertNotContains("crates/cose/src/operation_contract/key/parse.rs", "cose_key_from_slice"); +assertContains( + "crates/cose/src/operation_contract/execute.rs", + "super::key::parse::result(*request)", +); +for (const semanticRoute of [ + "construct_cose_key_from_public(CoseKeyFromPublicBytesInput::new(", + "construct_cose_key_from_private(CoseKeyFromPrivateBytesInput::new(", + "extract_cose_key_public(CoseKeyRefInput::new(", + "extract_cose_key_private(CoseKeyRefInput::new(", +]) { + assertContains("crates/cose/src/key/convert.rs", semanticRoute); +} +assertContains( + "crates/cose/src/key/derive_kid.rs", + "derive_cose_key_public_kid(CoseKeyRefInput::new(key))", +); +assertContains( + "crates/cose/src/multikey/convert.rs", + "convert_cose_key_to_multikey(CoseKeyRefInput::new(key))", +); +assertContains( + "crates/cose/src/multikey/convert.rs", + "convert_multikey_to_cose_key(MultikeyInput::new(multikey))", +); +for (const semanticCall of [ + "construct_cose_key_from_public(", + "construct_cose_key_from_private(", + "extract_cose_key_public(", + "extract_cose_key_private(", + "derive_cose_key_public_kid(", + "convert_cose_key_to_multikey(", + "convert_multikey_to_cose_key(", +]) { + assertContains("crates/cose/src/operation_contract/key/convert.rs", semanticCall); +} +const keyContractAdapter = readText("crates/cose/src/operation_contract/key/convert.rs"); +for (const bypassedFacade of [ + "cose_key_from_public_bytes", + "cose_key_from_private_bytes", + "cose_key_to_public_bytes", + "cose_key_to_private_bytes", + "derive_kid_from_cose_key_public", + "cose_key_to_multikey", + "multikey_to_cose_key", +]) { + // Match a complete Rust identifier so semantic names such as + // `convert_cose_key_to_multikey` do not create false policy failures. + if (new RegExp(`\\b${bypassedFacade}\\s*\\(`, "u").test(keyContractAdapter)) { + fail( + `crates/cose/src/operation_contract/key/convert.rs must not call convenience facade ${bypassedFacade}`, + ); + } +} +for (const contractRoute of [ + "super::key::convert::from_public_bytes_result(*request)", + "super::key::convert::from_private_bytes_result(*request)", + "super::key::convert::to_public_bytes_result(*request)", + "super::key::convert::to_private_bytes_result(*request)", + "super::key::convert::derive_public_kid_result(*request)", + "super::key::convert::to_multikey_result(*request)", + "super::key::convert::multikey_to_key_result(*request)", +]) { + assertContains("crates/cose/src/operation_contract/execute.rs", contractRoute); +} +assertContains( + "crates/cose/src/sign1/sign.rs", + "create_cose_sign1(CoseSign1CreateInput::new(", +); +assertContains( + "crates/cose/src/sign1/sign.rs", + "create_detached_cose_sign1(CoseSign1CreateInput::new(", +); +assertContains( + "crates/cose/src/sign1/verify.rs", + "verify_cose_sign1(", +); +assertContains( + "crates/cose/src/sign1/verify.rs", + "verify_detached_cose_sign1(", +); +for (const semanticCall of [ + "create_cose_sign1(", + "create_detached_cose_sign1(", +]) { + assertContains("crates/cose/src/operation_contract/sign1/create.rs", semanticCall); +} +for (const semanticCall of [ + "verify_cose_sign1(", + "verify_detached_cose_sign1(", +]) { + assertContains("crates/cose/src/operation_contract/sign1/verify.rs", semanticCall); +} +for (const [adapter, bypassedFacades] of [ + [ + "crates/cose/src/operation_contract/sign1/create.rs", + [ + "cose_sign1_with_options_and_external_aad", + "cose_sign1_detached_with_options_and_external_aad", + ], + ], + [ + "crates/cose/src/operation_contract/sign1/verify.rs", + [ + "cose_verify1_with_policy_and_external_aad", + "cose_verify1_detached_with_policy_and_external_aad", + ], + ], +]) { + const adapterSource = readText(adapter); + for (const bypassedFacade of bypassedFacades) { + if (new RegExp(`\\b${bypassedFacade}\\s*\\(`, "u").test(adapterSource)) { + fail(`${adapter} must not call convenience facade ${bypassedFacade}`); + } + } +} +for (const contractRoute of [ + "super::sign1::create::attached_result(*request)", + "super::sign1::create::detached_result(*request)", + "super::sign1::verify::attached_result(*request)", + "super::sign1::verify::detached_result(*request)", +]) { + assertContains("crates/cose/src/operation_contract/execute.rs", contractRoute); +} +assertContains( + "crates/cose/src/encrypt/create.rs", + "encrypt_cose_ml_kem_direct(CoseMlKemEncryptInput::new(", +); +assertContains( + "crates/cose/src/encrypt/create.rs", + "encrypt_cose_ml_kem_key_wrap(CoseMlKemEncryptInput::new(", +); +assertContains( + "crates/cose/src/encrypt/decrypt.rs", + "decrypt_cose_ml_kem(CoseMlKemDecryptInput::new(", +); +assertContains( + "crates/cose/src/operation_contract/encrypt/decrypt.rs", + "decrypt_cose_ml_kem(", +); +for (const [adapter, bypassedFacades] of [ + [ + "crates/cose/src/operation_contract/encrypt/create.rs", + [ + "cose_encrypt_ml_kem_direct_with_external_aad", + "cose_encrypt_ml_kem_key_wrap_with_external_aad", + ], + ], + [ + "crates/cose/src/operation_contract/encrypt/decrypt.rs", + ["cose_decrypt_ml_kem_with_external_aad"], + ], +]) { + const adapterSource = readText(adapter); + for (const bypassedFacade of bypassedFacades) { + if (new RegExp(`\\b${bypassedFacade}\\s*\\(`, "u").test(adapterSource)) { + fail(`${adapter} must not call convenience facade ${bypassedFacade}`); + } + } +} +for (const contractRoute of [ + "super::encrypt::create::direct_result(*request)", + "super::encrypt::create::key_wrap_result(*request)", + "super::encrypt::decrypt::result(*request)", +]) { + assertContains("crates/cose/src/operation_contract/execute.rs", contractRoute); +} +assertNotContains("crates/cose/src/wire.rs", "fn boundary_error_from_encrypt_error("); +assertNotContains("crates/cose/src/wire.rs", "fn dispatch_operation("); +assertContains("crates/cose/src/operation_contract/execute.rs", "fn dispatch_operation("); +assertContains("crates/cose/src/wire.rs", "crate::operation_contract::execute::execute_proto(request_bytes)"); +assertContains( + "crates/cose/src/wire.rs", + "crate::operation_contract::execute::execute_proto_json(request_json)", +); +assertOperationContractRouting({ readText, fail }); +for (const [family, generatedResults] of [ + ["key", ["CoseKeyBytesResult", "CoseMultikeyResult"]], + ["sign1", ["CoseSign1CreateResult", "CoseSign1VerifyResult"]], + ["encrypt", ["CoseMlKemEncryptResult", "CoseMlKemDecryptResult"]], +]) { + const modulePath = `crates/cose/src/operation_contract/${family}/mod.rs`; + const resultPath = `crates/cose/src/operation_contract/${family}/result.rs`; + assertContains(modulePath, "pub(crate) mod result;"); + assertContains(resultPath, "CoseOperationResult {"); + for (const generatedResult of generatedResults) { + assertContains(resultPath, `${generatedResult} {`); + assertNotContains("crates/cose/src/wire.rs", `${generatedResult} {`); + } +} +for (const adapterPath of [ + "crates/cose/src/operation_contract/key/parse.rs", + "crates/cose/src/operation_contract/key/convert.rs", + "crates/cose/src/operation_contract/sign1/create.rs", + "crates/cose/src/operation_contract/sign1/verify.rs", + "crates/cose/src/operation_contract/encrypt/create.rs", + "crates/cose/src/operation_contract/encrypt/decrypt.rs", +]) { + assertContains(adapterPath, "result::"); +} +for (const removedWireResultHelper of [ + "encode_key_bytes_result", + "encode_multikey_result", + "encode_sign1_create_result", + "encode_sign1_verify_result", + "encode_ml_kem_encrypt_result", + "encode_ml_kem_decrypt_result", + "signature_algorithm_to_proto", + "content_algorithm_to_proto", + "ml_kem_algorithm_to_proto", + "ml_kem_mode_to_proto", + "boundary_error_from_cose_error", +]) { + assertNotContains("crates/cose/src/wire.rs", removedWireResultHelper); +} +assertContains("crates/cose/src/zeroize_coset.rs", "struct SensitiveCborValue"); +assertContains("crates/cose/src/key/owned.rs", "fn zeroize_cose_key"); +assertContains("crates/cose/src/key/owned.rs", "core::mem::take(&mut key.key_ops)"); +assertContains("vectors/manifest.json", "reallyme.cose.conformance.vector_manifest.v1"); +assertContains("vectors/manifest.json", "cose-encrypt-ml-kem"); +assertContains("vectors/manifest.json", "cose-sign1-pq"); +assertContains("vectors/manifest.json", "cose-key-pq"); +assertContains("vectors/manifest.json", '"sha256"'); +assertContains("vectors/cose-sign1-pq.json", "ML-DSA-44"); +assertContains("vectors/cose-sign1-pq.json", "ML-DSA-65"); +assertContains("vectors/cose-sign1-pq.json", "ML-DSA-87"); +assertContains("vectors/cose-sign1-pq.json", "cose-sign1-ml-dsa-44-tampered-signature"); +assertContains("vectors/cose-sign1-pq.json", "cose-sign1-ml-dsa-44-truncated-signature"); +assertContains("vectors/cose-sign1-pq.json", "cose-sign1-ml-dsa-44-extended-signature"); +assertContains("vectors/cose-sign1.json", "cose-sign1-ed25519-node-openssl"); +assertContains("vectors/cose-sign1.json", "node_open_ssl_rfc8032"); +assertContains("vectors/cose-key-pq.json", "ML-KEM-512"); +assertContains("vectors/cose-key-pq.json", "ML-KEM-768"); +assertContains("vectors/cose-key-pq.json", "ML-KEM-1024"); +assertContains( + "vectors/cose-encrypt-ml-kem.json", + "reallyme.cose.ml_kem_encrypt.vectors.v1", +); assertContains("tools/vector-audit/Cargo.toml", 'name = "reallyme-cose-vector-audit"'); +assertContains("tools/vector-audit/src/ml_kem_encrypt.rs", "Kmac256"); +assertContains("tools/vector-audit/src/ml_kem_encrypt.rs", "unwrap_key"); +assertContains("tools/vector-goldens/Cargo.toml", 'name = "reallyme-cose-vector-goldens"'); +assertContains("tools/vector-goldens/src/ml_kem_encrypt.rs", "encapsulate_deterministic"); +assertContains("tools/vector-audit/src/pq.rs", "verify_ml_dsa"); +assertContains("tools/vector-goldens/src/pq.rs", "add_ml_dsa"); assertContains("buf.yaml", "modules:"); -assertContains("buf.yaml", "- path: proto"); -assertContains("proto/reallyme/cose/v1/cose.proto", "package reallyme.cose.v1;"); -assertContains("proto/reallyme/cose/v1/cose.proto", "message CoseError"); -assertContains("proto/reallyme/cose/v1/cose.proto", "message CoseSign1Error"); -assertContains("proto/reallyme/cose/v1/cose.proto", "message CoseKeyError"); -assertContains("proto/reallyme/cose/v1/cose.proto", "message CoseMultikeyError"); -assertContains("proto/reallyme/cose/v1/cose.proto", "enum CoseErrorReason"); -assertContains( - "proto/reallyme/cose/v1/cose.proto", +assertContains("buf.yaml", "- path: crates/proto/proto"); +assertNotContains("buf.yaml", "except:"); +assertNotContains("buf.yaml", "FIELD_NO_DELETE"); +assertNotContains("buf.yaml", "MESSAGE_NO_DELETE"); +assertContains("buf.gen.yaml", "protoc-gen-buffa"); +assertContains("buf.gen.yaml", "protoc-gen-buffa-packaging"); +assertContains("buf.gen.yaml", "views=true"); +assertContains("crates/proto/Cargo.toml", `name = "${expectedProtoPackageName}"`); +assertContains("crates/proto/Cargo.toml", `version = "${expectedVersion}"`); +assertContains("crates/proto/Cargo.toml", "publish = true"); +assertContains("crates/proto/Cargo.toml", '"/proto/**/*.proto"'); +assertContains("crates/proto/Cargo.toml", 'default = ["generated"]'); +assertContains("crates/proto/Cargo.toml", 'zeroize = { workspace = true, optional = true }'); +assertContains("crates/proto/src/generated.rs", "COSE_PROTO_PACKAGE"); +assertContains("crates/proto/src/generated/buffa/mod.rs", "@generated by buffa-codegen"); +assertContains("scripts/harden-generated-cose-proto.mjs", "Zeroize::zeroize(&mut self.private_key)"); +assertContains("scripts/harden-generated-cose-proto.mjs", "redactDebugBytes"); +assertContains("scripts/harden-generated-cose-proto.mjs", "__reallyme_zeroize_unknown_fields"); +assertContains("scripts/harden-generated-cose-proto.mjs", '"cose_key"'); +assertContains("scripts/harden-generated-cose-proto.mjs", '"key_bytes"'); +assertContains("scripts/harden-generated-cose-proto.mjs", '"external_aad"'); +assertContains("scripts/harden-generated-cose-proto.mjs", '"expected_kid"'); +assertContains("scripts/harden-generated-cose-proto.mjs", "deny_unknown_fields"); +assertContains("scripts/harden-generated-cose-proto.mjs", "unknown field"); +assertContains("scripts/harden-generated-cose-proto.mjs", '"--check-idempotent"'); +assertContains("scripts/harden-generated-cose-proto.mjs", '["CoseOperationRequest", []]'); +assertReallyMeProtobufReleasePolicy({ + generatedFreshnessMode, + buffaVersion: "0.9.0", + workflowMode: "delegated", + generatedFreshnessStepRun: + "node .release-readiness/scripts/run-consumer-check.mjs --generated-freshness", + installBufRun: verifiedBufInstallCommand, + hardeningPolicy: { + hardeningScript: "scripts/harden-generated-cose-proto.mjs", + protoSchema: "crates/proto/proto/reallyme/cose/v1/cose.proto", + generatedRust: "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs", + generatedView: "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.__view.rs", + protoCargo: "crates/proto/Cargo.toml", + requiredScriptNeedles: [ + "Zeroize::zeroize(&mut self.private_key)", + "redactDebugBytes", + "__reallyme_zeroize_unknown_fields", + '"cose_key"', + '"key_bytes"', + '"external_aad"', + '"expected_kid"', + ], + requiredCargoNeedles: ['"buffa/json"'], + // Every bytes/string field is deliberately classified. "Sensitive" here + // means the generated owner must redact and wipe the value; it includes + // public keys and Multikey strings because they are persistent identity + // correlators even when they are not cryptographic secrets. + scalarFieldClassifications: [ + { message: "CoseMlKemEncryptRequest", field: "recipient_public_key", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseMlKemEncryptRequest", field: "recipient_kid", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseMlKemEncryptRequest", field: "plaintext", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseMlKemEncryptRequest", field: "external_aad", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseMlKemEncryptRequest", field: "supp_priv_info", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseMlKemEncryptResult", field: "cose_encrypt", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseMlKemDecryptRequest", field: "cose_encrypt", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseMlKemDecryptRequest", field: "recipient_private_key", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseMlKemDecryptRequest", field: "expected_recipient_kid", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseMlKemDecryptRequest", field: "external_aad", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseMlKemDecryptRequest", field: "supp_priv_info", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseMlKemDecryptResult", field: "plaintext", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseMlKemDecryptResult", field: "recipient_kid", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseSign1CreateRequest", field: "payload", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseSign1CreateRequest", field: "private_key", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseSign1CreateRequest", field: "kid", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseSign1CreateRequest", field: "external_aad", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseSign1CreateDetachedRequest", field: "payload", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseSign1CreateDetachedRequest", field: "private_key", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseSign1CreateDetachedRequest", field: "kid", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseSign1CreateDetachedRequest", field: "external_aad", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseSign1CreateResult", field: "cose_sign1", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseSign1VerifyRequest", field: "cose_sign1", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseSign1VerifyRequest", field: "public_key", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseSign1VerifyRequest", field: "external_aad", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseSign1VerifyRequest", field: "expected_kid", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseSign1VerifyDetachedRequest", field: "cose_sign1", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseSign1VerifyDetachedRequest", field: "payload", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseSign1VerifyDetachedRequest", field: "public_key", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseSign1VerifyDetachedRequest", field: "external_aad", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseSign1VerifyDetachedRequest", field: "expected_kid", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseSign1VerifyResult", field: "payload", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseSign1VerifyResult", field: "kid", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseKeyFromPublicBytesRequest", field: "public_key", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseKeyFromPrivateBytesRequest", field: "private_key", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseKeyFromPrivateBytesRequest", field: "public_key", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseKeyBytesRequest", field: "cose_key", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseKeyBytesResult", field: "key_bytes", kind: "bytes", sensitivity: "sensitive" }, + { message: "CoseMultikeyToCoseKeyRequest", field: "multikey", kind: "string", sensitivity: "sensitive" }, + { message: "CoseMultikeyResult", field: "multikey", kind: "string", sensitivity: "sensitive" }, + ], + requiredGeneratedNeedles: [ + "fn __reallyme_zeroize_unknown_fields(", + '.field("private_key", &"")', + '.field("payload", &"")', + '.field("cose_key", &"")', + '.field("key_bytes", &"")', + "::zeroize::Zeroize::zeroize(&mut self.private_key);", + ], + forbiddenGeneratedNeedles: [ + "::buffa::alloc::format!(", + '.field("private_key", &self.private_key)', + '.field("payload", &self.payload)', + '.field("cose_key", &self.cose_key)', + ], + requiredViewNeedles: [ + 'formatter.write_str("CoseSign1CreateRequestView()")', + 'formatter.write_str("CoseMlKemDecryptResultView()")', + 'formatter.write_str("CoseSign1CreateRequestOwnedView()")', + 'formatter.write_str("CoseMlKemDecryptResultOwnedView()")', + ], + }, + generatedFreshness: { + generatedPaths: ["crates/proto/src/generated"], + commands: [ + ["buf", ["lint"]], + ["buf", ["generate"]], + ["node", ["scripts/harden-generated-cose-proto.mjs"]], + ["node", ["scripts/harden-generated-cose-proto.mjs", "--check-idempotent"]], + ["cargo", ["fmt", "--package", expectedProtoPackageName]], + ], + }, +}); +assertContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs", + "impl ::core::ops::Drop for CoseSign1CreateRequest", +); +assertContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs", + "impl ::core::ops::Drop for CoseSign1CreateDetachedRequest", +); +assertContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs", + "impl ::core::ops::Drop for CoseKeyFromPrivateBytesRequest", +); +assertContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs", + '.field("private_key", &"")', +); +assertContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs", + '.field("payload", &"")', +); +assertContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs", + '.field("cose_key", &"")', +); +assertContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs", + '.field("key_bytes", &"")', +); +assertContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs", + "::zeroize::Zeroize::zeroize(&mut self.private_key);", +); +assertNotContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs", + '.field("private_key", &self.private_key)', +); +assertNotContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs", + '.field("payload", &self.payload)', +); +assertNotContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs", + '.field("cose_key", &self.cose_key)', +); +assertNotContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs", + '.field("key_bytes", &self.key_bytes)', +); +assertContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.mod.rs", + "__view", +); +assertContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.__view.rs", + 'formatter.write_str("CoseSign1CreateRequestView()")', +); +assertContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.__view.rs", + 'formatter.write_str("CoseMlKemDecryptResultView()")', +); +assertContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.__view.rs", + 'formatter.write_str("CoseSign1CreateRequestOwnedView()")', +); +assertContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.__view.rs", + 'formatter.write_str("CoseMlKemDecryptResultOwnedView()")', +); +assertContains("crates/cose/src/wire.rs", "reallyme_cose_proto::generated::proto::reallyme::cose::v1"); +assertContains("crates/cose/src/wire.rs", "DecodeOptions::new()"); +assertContains("crates/cose/src/wire.rs", "const COSE_PROTO_UNKNOWN_FIELD_LIMIT: usize = 0;"); +assertContains("crates/cose/src/wire.rs", ".with_unknown_field_limit(COSE_PROTO_UNKNOWN_FIELD_LIMIT)"); +assertNotContains("crates/cose/src/wire.rs", "prost"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "package reallyme.cose.v1;"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", 'option swift_prefix = "ReallyMeProto";'); +assertProtoContract("crates/proto/proto/reallyme/cose/v1/cose.proto"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "message CoseError"); +assertNotContains("crates/proto/proto/reallyme/cose/v1/cose.proto", 'reserved "sign1", "key", "multikey";'); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "CosePrimitiveError primitive = 1;"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "CoseProviderError provider = 2;"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "CoseBackendError backend = 3;"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "message CosePrimitiveError"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "message CoseProviderError"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "message CoseBackendError"); +assertNotContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "message CoseSign1Error"); +assertNotContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "message CoseKeyError"); +assertNotContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "message CoseMultikeyError"); +assertNotContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "enum CoseOperation"); +assertNotContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "CoseProtoResultEnvelope"); +assertNotContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "CoseProtoResultStatus"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "message CoseOperationRequest"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "message CoseOperationResponseV2"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "message CoseOperationResult"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "CoseOperationResult result = 1;"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "CoseError error = 2;"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "reserved 1 to 15;"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "CoseSign1CreateRequest sign1_create = 1000;"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "CoseMultikeyToCoseKeyRequest multikey_to_cose_key = 2007;"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "CoseMlKemDecryptRequest ml_kem_decrypt = 3002;"); +for (const versionTwoResultField of [ + "CoseSign1CreateResult sign1_create = 1000;", + "CoseSign1CreateResult sign1_create_detached = 1001;", + "CoseSign1VerifyResult sign1_verify = 1002;", + "CoseSign1VerifyResult sign1_verify_detached = 1003;", + "CoseKeyBytesResult key_from_public_bytes = 2000;", + "CoseKeyBytesResult key_from_private_bytes = 2001;", + "CoseKeyBytesResult key_parse = 2002;", + "CoseKeyBytesResult key_to_public_bytes = 2003;", + "CoseKeyBytesResult key_to_private_bytes = 2004;", + "CoseKeyBytesResult key_derive_public_kid = 2005;", + "CoseMultikeyResult key_to_multikey = 2006;", + "CoseKeyBytesResult multikey_to_cose_key = 2007;", + "CoseMlKemEncryptResult ml_kem_encrypt_direct = 3000;", + "CoseMlKemEncryptResult ml_kem_encrypt_key_wrap = 3001;", + "CoseMlKemDecryptResult ml_kem_decrypt = 3002;", +]) { + assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", versionTwoResultField); +} +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "enum CoseSignatureAlgorithm"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "enum CoseKeyAgreementAlgorithm"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "enum CoseKemAlgorithm"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "message CoseAlgorithmIdentifier"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "COSE_SIGNATURE_ALGORITHM_ED25519 = 100;"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "COSE_SIGNATURE_ALGORITHM_ECDSA_P256_SHA256 = 200;"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "COSE_SIGNATURE_ALGORITHM_ML_DSA_44 = 1000;"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "COSE_KEY_AGREEMENT_ALGORITHM_X25519 = 100;"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "COSE_KEM_ALGORITHM_ML_KEM_512 = 1000;"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "COSE_KEM_ALGORITHM_X_WING_768 = 1100;"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "COSE_CONTENT_ENCRYPTION_ALGORITHM_AES_128_GCM = 100;"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "CoseSignatureAlgorithm algorithm = 1;"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "CoseKemAlgorithm kem_algorithm = 1;"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "CoseAlgorithmIdentifier algorithm = 1;"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "message CoseSign1CreateRequest"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "message CoseSign1VerifyResult"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "message CoseKeyBytesResult"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "SENSITIVE: raw private key bytes"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "SENSITIVE: payload bytes"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "SENSITIVE: encoded COSE_Key bytes"); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "SENSITIVE: operation-specific bytes"); +assertContains("crates/cose/src/wire.rs", "pub fn execute_operation_proto("); +assertContains("crates/cose/src/wire.rs", "pub fn execute_operation_proto_json("); +assertContains("crates/cose/src/wire.rs", "pub fn decode_operation_response("); +assertContains("crates/cose/src/wire.rs", "pub fn decode_operation_response_for_request("); +assertNotContains("crates/cose/src/wire.rs", "CoseProtoOutput"); +assertNotContains("crates/cose/src/wire.rs", "CoseProtoStatus"); +assertNotContains("crates/cose/src/wire.rs", "CoseProtoResultEnvelope"); +assertContains("crates/cose/src/wire.rs", "pub fn decode_cose_error("); +assertNotContains("crates/cose/src/wire.rs", "pub fn process_operation_output("); +assertNotContains("crates/cose/src/wire.rs", "pub struct CoseWireError"); +assertNotContains("crates/cose/src/wire.rs", "pub enum CoseWireErrorConstructionError"); +assertNotContains("crates/cose/src/wire.rs", "pub fn try_new("); +assertContains("crates/cose/src/wire.rs", "fn reason_is_valid_for_branch("); +assertNotContains("crates/cose/src/wire.rs", "pub type CoseWireResult"); +assertContains("crates/cose/src/operation_contract/mod.rs", "pub(crate) mod response_v2;"); +assertContains("crates/cose/src/operation_contract/response_v2.rs", "fn result_matches_request("); +assertContains("crates/cose/src/operation_contract/response_v2.rs", "CoseOperationResultBranch::MlKemDecrypt"); +assertContains("scripts/harden-generated-cose-proto.mjs", '["CoseOperationResponseV2", []]'); +assertContains("scripts/harden-generated-cose-proto.mjs", '["CoseOperationResult", []]'); +assertContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs", + "impl ::core::ops::Drop for CoseOperationResponseV2", +); +assertContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs", + "impl ::core::ops::Drop for CoseOperationResult", +); +assertContains("crates/cose/src/error.rs", "#[non_exhaustive]"); +assertContains("crates/cose/src/key/owned.rs", "#[must_use]\npub struct CoseKey"); +assertContains( + "crates/cose/src/key/derive_kid.rs", + "Result>, CoseError>", +); +assertContains( + "crates/cose/src/multikey/convert.rs", + "Result, CoseError>", +); +assertContains("crates/cose/src/sign1/provider.rs", "pub trait CoseSigner"); +assertContains("crates/cose/src/sign1/provider.rs", "pub enum CoseSignerError"); +assertContains("crates/cose/src/sign1/sign.rs", "pub fn cose_sign1_with_signer("); +assertContains("crates/cose/src/sign1/sign.rs", "pub fn cose_sign1_detached_with_signer("); +assertContains("crates/cose/tests/cose_suite/platform_signer_tests.rs", "CoseSignerError::Unavailable"); +assertContains("crates/cose/tests/cose_suite/concurrency_tests.rs", "thread::scope"); +for (const allocationLimit of [ + "SIGN1_PEAK_ALLOCATION_LIMIT", + "KEY_PEAK_ALLOCATION_LIMIT", + "MULTIKEY_PEAK_ALLOCATION_LIMIT", + "DECRYPT_PEAK_ALLOCATION_LIMIT", +]) { + assertContains("crates/cose/benches/operation_performance.rs", allocationLimit); +} +assertContains("crates/cose/src/policy/validate.rs", "#[must_use]\n#[derive(Debug, Clone)]\npub struct CosePolicy"); +assertContains("crates/cose/src/policy/validate.rs", "require_kid: bool"); +assertContains("crates/cose/src/policy/validate.rs", "pub fn with_require_kid"); +assertNotContains("crates/cose/src/policy/validate.rs", "pub require_kid: bool"); +assertNotContains("crates/cose/src/policy/validate.rs", "pub allowed_algs: Vec"); +assertNotContains("crates/cose/src/policy/validate.rs", "pub max_cose_sign1_bytes: usize"); +assertContains("crates/cose/src/sign1/sign.rs", "#[must_use]\n#[derive(Clone, Copy, Debug, Eq, PartialEq)]\npub struct CoseSign1EncodeOptions"); +assertContains("crates/cose/src/sign1/sign.rs", "tag: bool"); +assertContains("crates/cose/src/sign1/sign.rs", "pub const fn tagged()"); +assertNotContains("crates/cose/src/sign1/sign.rs", "pub tag: bool"); +assertNotContains("crates/cose/src/sign1/sign.rs", "pub max_cose_sign1_bytes: usize"); +assertContains("crates/cose/src/sign1/verify.rs", "#[must_use]\n#[non_exhaustive]\npub struct VerifiedCoseSign1"); +assertNotContains("crates/cose/src/wire.rs", "pub type CoseWireBytes = Zeroizing>;"); +assertNotContains("crates/cose/src/wire.rs", "pub fn encode_protobuf"); +assertContains("crates/cose/src/wire.rs", "Zeroizing::new(message.encode_to_vec())"); +assertContains("crates/cose/src/wire.rs", "COSE_PROTO_PACKAGE"); +assertContains("crates/cose/src/operation_contract/input.rs", "if limit > MAX_COSE_PROTO_MESSAGE_BYTES"); +assertNotContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs", + "CoseSign1Error", +); +assertNotContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs", + "CoseKeyError", +); +assertNotContains( + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs", + "CoseMultikeyError", +); +assertContains("crates/proto/proto/reallyme/cose/v1/cose.proto", "enum CoseErrorReason"); +assertContains( + "crates/proto/proto/reallyme/cose/v1/cose.proto", "COSE_ERROR_REASON_COMMON_CBOR = 100;", ); assertContains( - "proto/reallyme/cose/v1/cose.proto", - "COSE_ERROR_REASON_SIGN1_MISSING_PAYLOAD = 200;", + "crates/proto/proto/reallyme/cose/v1/cose.proto", + "COSE_ERROR_REASON_COMMON_MALFORMED_PROTOBUF = 120;", +); +assertContains( + "crates/proto/proto/reallyme/cose/v1/cose.proto", + "COSE_ERROR_REASON_SIGN1_KID_KEY_MISMATCH = 400;", +); +assertContains( + "crates/proto/proto/reallyme/cose/v1/cose.proto", + "COSE_ERROR_REASON_SIGN1_MISSING_PAYLOAD = 401;", +); +assertContains( + "crates/proto/proto/reallyme/cose/v1/cose.proto", + "COSE_ERROR_REASON_KEY_MISSING_KEY_MATERIAL = 500;", +); +assertContains( + "crates/proto/proto/reallyme/cose/v1/cose.proto", + "COSE_ERROR_REASON_MULTIKEY_INVALID_MULTIKEY = 600;", ); assertContains( - "proto/reallyme/cose/v1/cose.proto", - "COSE_ERROR_REASON_KEY_MISSING_KEY_MATERIAL = 300;", + "crates/proto/proto/reallyme/cose/v1/cose.proto", + "COSE_ERROR_REASON_SIGN1_INVALID_SIGNATURE_ENCODING = 422;", ); assertContains( - "proto/reallyme/cose/v1/cose.proto", - "COSE_ERROR_REASON_MULTIKEY_INVALID_MULTIKEY = 400;", + "crates/proto/proto/reallyme/cose/v1/cose.proto", + "COSE_ERROR_REASON_ENCRYPT_MISSING_KID = 731;", +); +assertContains( + "crates/proto/proto/reallyme/cose/v1/cose.proto", + "COSE_ERROR_REASON_ENCRYPT_UNPROTECTED_HEADER_NOT_ALLOWED = 740;", +); +assertContains("crates/cose/src/failure.rs", "pub(crate) fn from_encrypt_error("); +assertContains("crates/cose/src/failure.rs", "CoseFailureReason::EncryptMissingKid"); +assertContains( + "crates/cose/src/failure.rs", + "CoseFailureReason::EncryptUnprotectedHeaderNotAllowed", ); assertContains(".github/workflows/rust-ci.yml", "paths-ignore:"); +assertContains(".github/workflows/rust-ci.yml", "tools/vector-goldens/Cargo.toml"); +assertContains(".github/workflows/rust-ci.yml", "--features native,wire"); +assertContains(".github/workflows/rust-ci.yml", "--features wasm,wire --target wasm32-unknown-unknown"); +assertContains(".github/workflows/rust-ci.yml", "cargo nextest run --release --locked --workspace --all-features"); +assertContains(".github/workflows/rust-ci.yml", "wasm-pack test --node crates/cose"); +assertContains(".github/workflows/rust-ci.yml", "CARGO_CHECK_EXTERNAL_TYPES_VERSION: 0.5.0"); +assertContains(".github/workflows/rust-ci.yml", "EXTERNAL_TYPES_NIGHTLY: nightly-2026-03-20"); +assertContains(".github/workflows/rust-ci.yml", "check-external-types --manifest-path crates/cose/Cargo.toml --all-features"); assertContains(".github/workflows/fuzz.yml", "paths-ignore:"); assertContains(".github/workflows/crates-release.yml", "CARGO_REGISTRY_TOKEN"); - -for (const [crateName, version] of requiredRegistryDeps) { - const dependencyLine = `${crateName} = { version = "${version}", default-features = false`; - if (!cargoToml.includes(dependencyLine)) { - fail(`${crateName} must use crates.io version ${version} with default-features disabled`); - } +assertContains(".github/workflows/crates-release.yml", "name: Crates.io Release"); +assertContains( + ".github/workflows/crates-release.yml", + "run-name: Crates.io release ${{ inputs.version }} @ ${{ inputs.release_sha }}", +); +assertContains( + ".github/workflows/crates-release.yml", + "group: crates-release-${{ inputs.version }}-${{ inputs.release_sha }}", +); +assertContains(".github/workflows/crates-release.yml", "if: github.ref == 'refs/heads/main'"); +assertContains(".github/workflows/crates-release.yml", "environment: crates-io-release"); +assertContains(".github/workflows/crates-release.yml", "preflight_run_id:"); +assertContains(".github/workflows/crates-release.yml", "release_sha:"); +assertContains(".github/workflows/crates-release.yml", "version:"); +assertContains(".github/workflows/crates-release.yml", "verify-preflight:"); +assertContains(".github/workflows/crates-release.yml", "actions: read"); +assertContains(".github/workflows/crates-release.yml", "runs-on: ubuntu-24.04"); +assertNotContains(".github/workflows/crates-release.yml", "runs-on: ubuntu-latest"); +assertContains( + ".github/workflows/crates-release.yml", + "node scripts/verify_release_attestation.mjs", +); +assertContains( + ".github/workflows/crates-release.yml", + "actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c", +); +assertContains( + ".github/workflows/crates-release.yml", + "reallyme-cose-crates-preflight-${{ inputs.version }}-${{ inputs.release_sha }}", +); +assertContains(".github/workflows/crates-release.yml", "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0"); +assertContains(".github/workflows/crates-release.yml", "actions/setup-node@820762786026740c76f36085b0efc47a31fe5020"); +assertContains(".github/workflows/crates-release.yml", "Swatinem/rust-cache@23869a5bd66c73db3c0ac40331f3206eb23791dc"); +assertContains(".github/workflows/crates-release.yml", "node-version: '24'"); +assertContains( + ".github/workflows/crates-release.yml", + "ref: ${{ needs.verify-preflight.outputs.release_sha }}", +); +assertContains(".github/workflows/crates-release.yml", "persist-credentials: false"); +assertContains( + ".github/workflows/crates-release.yml", + "node scripts/verify_release_source.mjs", +); +assertNotContains(".github/workflows/crates-release.yml", "dry-run:"); +assertNotContains(".github/workflows/crates-release.yml", "node scripts/publish_crates_in_order.mjs inspect"); +assertNotContains(".github/workflows/crates-release.yml", "buf generate"); +assertNotContains(".github/workflows/crates-release.yml", "cargo nextest run"); +assertContains( + ".github/workflows/crates-package-preflight.yml", + "node scripts/run_pinned_release_readiness.mjs --release-packages", +); +assertContains( + ".github/workflows/protobuf-ci.yml", + "node .release-readiness/scripts/run-consumer-check.mjs --generated-freshness", +); +assertContains( + ".github/workflows/rust-ci.yml", + "node .release-readiness/scripts/run-consumer-check.mjs --policy-only", +); +for (const releaseReadinessWorkflow of [ + ".github/workflows/protobuf-ci.yml", + ".github/workflows/rust-ci.yml", +]) { + assertContains(releaseReadinessWorkflow, "repository: reallyme/release-readiness"); + assertContains( + releaseReadinessWorkflow, + "ref: f27973caf9d3a12847cac4032c361f5f553c97e9", + ); + assertContains(releaseReadinessWorkflow, "persist-credentials: false"); } +assertContains(".github/workflows/crates-release.yml", "node scripts/publish_crates_in_order.mjs publish"); +assertContains(".github/workflows/crates-release.yml", 'tag="reallyme-cose-v${RELEASE_VERSION}"'); +assertContains(".github/workflows/crates-release.yml", 'gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs"'); +assertNotContains(".github/workflows/crates-release.yml", "git push"); +assertContains(".github/workflows/crates-release.yml", "gh release create"); +assertContains(".github/workflows/crates-release.yml", "RELEASE_TAG: ${{ steps.release-tag.outputs.tag }}"); +assertNotContains( + ".github/workflows/crates-release.yml", + 'tag="${{ steps.release-tag.outputs.tag }}"', +); +assertContains("scripts/publish_crates_in_order.mjs", "Publish order"); +assertContains("scripts/publish_crates_in_order.mjs", "workspace publish dependency cycle"); +assertContains("scripts/publish_crates_in_order.mjs", "REQUIRED_PUBLISH_ORDER_EDGES"); +assertContains("scripts/publish_crates_in_order.mjs", "reallyme-cose-proto"); +assertContains("scripts/publish_crates_in_order.mjs", "checkPathDependencyVersions();"); +assertContains("scripts/publish_crates_in_order.mjs", "checkRequiredPublishOrderEdges();"); +assertContains("scripts/publish_crates_in_order.mjs", "checkReleaseVersion();"); +assertContains("scripts/publish_crates_in_order.mjs", 'const MODE_ORDER = "order";'); +assertContains("scripts/publish_crates_in_order.mjs", '["package", "--workspace", "--no-verify", "--locked"]'); +assertContains("scripts/publish_crates_in_order.mjs", '"--offline"'); +assertContains("scripts/publish_crates_in_order.mjs", "isEarlierWorkspaceDependency"); +assertContains("scripts/publish_crates_in_order.mjs", "dry-run reached unpublished ordered workspace dependencies"); +assertContains("scripts/publish_crates_in_order.mjs", "retryAfterMs"); +assertContains("scripts/publish_crates_in_order.mjs", "too many requests"); +assertContains("scripts/publish_crates_in_order.mjs", "rate-limited"); +assertContains("scripts/publish_crates_in_order.mjs", "rate limited"); +assertContains("scripts/publish_crates_in_order.mjs", "crates.io rate-limited new crate uploads"); +assertContains("scripts/publish_crates_in_order.mjs", "crates.io index has not observed a freshly published dependency yet"); +assertContains("scripts/publish_crates_in_order.mjs", "already uploaded"); +assertContains("scripts/publish_crates_in_order.mjs", "already exists"); +assertContains("scripts/publish_crates_in_order.mjs", "verifyPublishedPackageMatches(pkg)"); +assertContains("scripts/publish_crates_in_order.mjs", "const publishedChecksum = createHash(\"sha256\")"); +assertContains(".github/workflows/protobuf-ci.yml", "name: lint, generated freshness"); +assertContains(".github/workflows/protobuf-ci.yml", "runs-on: ubuntu-24.04"); +assertNotContains(".github/workflows/protobuf-ci.yml", "runs-on: ubuntu-latest"); +assertNotContains(".github/workflows/protobuf-ci.yml", "git fetch origin main:refs/remotes/origin/main"); +assertNotContains(".github/workflows/protobuf-ci.yml", "buf breaking --against"); +assertContains(".github/workflows/protobuf-ci.yml", "scripts/check_release_readiness.mjs"); +assertContains(".github/workflows/protobuf-ci.yml", "scripts/harden-generated-cose-proto.mjs"); +assertContains(".github/workflows/fuzz.yml", "- wire"); +assertContains(".github/workflows/fuzz.yml", "runs-on: ubuntu-24.04"); +assertNotContains(".github/workflows/fuzz.yml", "runs-on: ubuntu-latest"); +assertContains(".github/workflows/fuzz.yml", "CARGO_FUZZ_VERSION: 0.13.2"); +assertContains(".github/workflows/fuzz.yml", "NIGHTLY_TOOLCHAIN: nightly-2026-07-01"); +assertContains(".github/workflows/fuzz.yml", "FUZZ_MAX_TOTAL_TIME_SECONDS: 900"); +assertContains( + ".github/workflows/fuzz.yml", + "actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809", +); +assertContains(".github/workflows/fuzz.yml", "path: fuzz/corpus/${{ matrix.target }}"); +assertContains(".github/workflows/fuzz.yml", "cargo metadata --manifest-path fuzz/Cargo.toml --locked --no-deps"); +assertContains(".github/workflows/fuzz.yml", "git diff --exit-code -- fuzz/Cargo.lock"); +const cratesPackagePreflightWorkflow = ".github/workflows/crates-package-preflight.yml"; +assertContains(cratesPackagePreflightWorkflow, "name: Crates Package Preflight"); +assertContains( + cratesPackagePreflightWorkflow, + "run-name: Crates package preflight ${{ inputs.version }} @ ${{ github.sha }}", +); +assertContains(cratesPackagePreflightWorkflow, "BUF_VERSION: 1.71.0"); +assertContains(cratesPackagePreflightWorkflow, "BUFFA_VERSION: 0.9.0"); +assertContains(cratesPackagePreflightWorkflow, "CARGO_DENY_VERSION: 0.20.2"); +assertContains(cratesPackagePreflightWorkflow, "CARGO_AUDIT_VERSION: 0.22.2"); +assertContains(cratesPackagePreflightWorkflow, "CARGO_NEXTEST_VERSION: 0.9.140"); +assertContains(cratesPackagePreflightWorkflow, "CARGO_CHECK_EXTERNAL_TYPES_VERSION: 0.5.0"); +assertContains(cratesPackagePreflightWorkflow, "CARGO_FUZZ_VERSION: 0.13.2"); +assertContains(cratesPackagePreflightWorkflow, "EXTERNAL_TYPES_NIGHTLY: nightly-2026-03-20"); +assertContains(cratesPackagePreflightWorkflow, "FUZZ_NIGHTLY: nightly-2026-07-01"); +assertContains(cratesPackagePreflightWorkflow, "WASM_PACK_VERSION: 0.15.0"); +assertContains(cratesPackagePreflightWorkflow, "WASM_BINDGEN_CLI_VERSION: 0.2.126"); +assertContains(cratesPackagePreflightWorkflow, "version:"); +assertContains( + cratesPackagePreflightWorkflow, + "group: crates-package-preflight-${{ inputs.version }}-${{ github.sha }}", +); +assertContains(cratesPackagePreflightWorkflow, "verify-source-sha:"); +assertContains(cratesPackagePreflightWorkflow, "crates-package:"); +assertContains(cratesPackagePreflightWorkflow, "runs-on: ubuntu-24.04"); +assertNotContains(cratesPackagePreflightWorkflow, "runs-on: ubuntu-latest"); +assertContains(cratesPackagePreflightWorkflow, "ref: ${{ github.sha }}"); +assertContains(cratesPackagePreflightWorkflow, "fetch-depth: 0"); +assertContains(cratesPackagePreflightWorkflow, "node scripts/verify_release_source.mjs"); +assertContains(cratesPackagePreflightWorkflow, "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0"); +assertContains(cratesPackagePreflightWorkflow, "actions/setup-node@820762786026740c76f36085b0efc47a31fe5020"); +assertContains(cratesPackagePreflightWorkflow, "Swatinem/rust-cache@23869a5bd66c73db3c0ac40331f3206eb23791dc"); +assertContains( + cratesPackagePreflightWorkflow, + "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a", +); +assertContains(cratesPackagePreflightWorkflow, "node scripts/write_release_attestation.mjs"); +assertContains( + cratesPackagePreflightWorkflow, + "reallyme-cose-crates-preflight-${{ inputs.version }}-${{ github.sha }}", +); +assertContains(cratesPackagePreflightWorkflow, "node-version: '24'"); +assertContains(cratesPackagePreflightWorkflow, "BUF_LINUX_X86_64_SHA256:"); +assertContains(cratesPackagePreflightWorkflow, "sha256sum --check --strict"); +assertContains(".github/workflows/protobuf-ci.yml", "BUF_LINUX_X86_64_SHA256:"); +assertContains(".github/workflows/protobuf-ci.yml", "sha256sum --check --strict"); +assertContains(cratesPackagePreflightWorkflow, "taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853"); +assertContains(cratesPackagePreflightWorkflow, 'protoc-gen-buffa --version "$BUFFA_VERSION"'); +assertContains(cratesPackagePreflightWorkflow, 'protoc-gen-buffa-packaging --version "$BUFFA_VERSION"'); +assertContains(cratesPackagePreflightWorkflow, 'cargo install cargo-fuzz --version "$CARGO_FUZZ_VERSION" --locked'); +assertContains("scripts/run_pinned_release_readiness.mjs", "f27973caf9d3a12847cac4032c361f5f553c97e9"); +assertContains("scripts/run_pinned_release_readiness.mjs", "70cc78721738cf352024938e8fc86e73380e71b2cdf7a9a733687543167cbaae"); +assertContains("scripts/verify_release_source.mjs", "main:refs/remotes/origin/main"); +assertContains("scripts/verify_release_source.mjs", "manifest-version-mismatch"); +assertContains("scripts/verify_release_attestation.mjs", 'value.conclusion !== "success"'); +assertContains("scripts/verify_release_attestation.mjs", 'value.event !== "workflow_dispatch"'); +assertContains("scripts/verify_release_attestation.mjs", "attestation-input-mismatch"); +assertContains("scripts/verify_release_attestation.mjs", 'value.run_attempt !== 1'); +assertContains("scripts/write_release_attestation.mjs", "reallyme.cose.crates_preflight.v1"); +assertContains(".github/workflows/rust-ci.yml", "runs-on: ubuntu-24.04"); +assertNotContains(".github/workflows/rust-ci.yml", "runs-on: ubuntu-latest"); +assertContains(".gitignore", "/.release-readiness/"); -const metadataResult = run("cargo", ["metadata", "--format-version", "1", "--no-deps"], { - capture: true, +assertCargoMetadataPolicy({ + packages: [ + { + name: expectedPackageName, + version: expectedVersion, + publish: "public", + packageFiles: [ + "Cargo.toml", + "README.md", + "LICENSE", + "NOTICE", + "src/lib.rs", + "src/wire.rs", + ], + dependencies: [ + { + name: "reallyme-codec", + requirement: "^0.2.0", + source: "registry", + defaultFeatures: false, + }, + { + name: "reallyme-crypto", + requirement: "^0.3.0", + source: "registry", + defaultFeatures: false, + }, + ], + }, + { + name: expectedProtoPackageName, + version: expectedVersion, + publish: "public", + packageFiles: [ + "Cargo.toml", + "README.md", + "LICENSE", + "NOTICE", + "src/lib.rs", + "src/generated.rs", + "src/generated/buffa/mod.rs", + "src/generated/buffa/reallyme.cose.v1.cose.rs", + "src/generated/buffa/reallyme.cose.v1.cose.__view.rs", + "proto/reallyme/cose/v1/cose.proto", + ], + }, + ], }); -const metadata = JSON.parse(metadataResult.stdout); -const rootPackage = metadata.packages.find((pkg) => pkg.name === expectedPackageName); -if (!rootPackage) { - fail(`cargo metadata did not expose ${expectedPackageName}`); -} -if (rootPackage.version !== expectedVersion) { - fail(`${expectedPackageName} metadata version is ${rootPackage.version}`); -} - -for (const [crateName, version] of requiredRegistryDeps) { - const dep = rootPackage.dependencies.find((candidate) => candidate.name === crateName); - if (!dep) { - fail(`${expectedPackageName} is missing ${crateName}`); - } - if (dep.req !== `^${version}`) { - fail(`${crateName} dependency requirement is ${dep.req}, expected ^${version}`); - } - if (typeof dep.source !== "string" || !dep.source.startsWith("registry+")) { - fail(`${crateName} must resolve from crates.io, not a path or git dependency`); - } -} const validationCommands = [ + ["node", ["--test", "scripts/release-readiness/operation-contract-routing.test.mjs"]], + ["node", ["--test", "scripts/verify_release_attestation.test.mjs"]], + ["node", ["--check", "scripts/run_pinned_release_readiness.mjs"]], + ["node", ["--check", "scripts/verify_release_source.mjs"]], + ["node", ["--check", "scripts/write_release_attestation.mjs"]], + ["node", ["--check", "scripts/verify_release_attestation.mjs"]], + ["node", ["--check", "scripts/harden-generated-cose-proto.mjs"]], + ["node", ["scripts/harden-generated-cose-proto.mjs", "--check-idempotent"]], ["cargo", ["fmt", "--check"]], - ["cargo", ["check", "--workspace", "--all-features"]], - ["cargo", ["check", "--workspace", "--all-features"], { env: { ...process.env, RUSTFLAGS: "-Dwarnings" } }], + ["cargo", ["check", "--locked", "--workspace", "--all-features"]], + ["cargo", ["check", "--locked", "--workspace", "--all-features"], { env: { ...process.env, RUSTFLAGS: "-Dwarnings" } }], [ "cargo", - ["check", "--workspace", "--no-default-features"], + ["check", "--locked", "--workspace", "--no-default-features"], { env: { ...process.env, RUSTFLAGS: "-Dwarnings" } }, ], - ["cargo", ["clippy", "--workspace", "--all-targets", "--all-features", "--", "-D", "warnings"]], + [ + "cargo", + ["check", "--locked", "--workspace", "--no-default-features", "--features", "native,wire"], + { env: { ...process.env, RUSTFLAGS: "-Dwarnings" } }, + ], + ["cargo", ["clippy", "--locked", "--workspace", "--all-targets", "--all-features", "--", "-D", "warnings"]], + [ + "cargo", + [ + "+nightly-2026-03-20", + "check-external-types", + "--manifest-path", + "crates/cose/Cargo.toml", + "--all-features", + ], + ], ["cargo", ["fmt", "--manifest-path", "tools/vector-audit/Cargo.toml", "--check"]], - ["cargo", ["clippy", "--manifest-path", "tools/vector-audit/Cargo.toml", "--all-targets", "--", "-D", "warnings"]], - ["cargo", ["test", "--workspace", "--all-features"]], - ["cargo", ["run", "--manifest-path", "tools/vector-audit/Cargo.toml", "--", "."]], - ["cargo", ["check", "--workspace", "--no-default-features", "--features", "native"]], + ["cargo", ["clippy", "--locked", "--manifest-path", "tools/vector-audit/Cargo.toml", "--all-targets", "--", "-D", "warnings"]], + ["cargo", ["fmt", "--manifest-path", "tools/vector-goldens/Cargo.toml", "--check"]], + ["cargo", ["clippy", "--locked", "--manifest-path", "tools/vector-goldens/Cargo.toml", "--all-targets", "--", "-D", "warnings"]], + ["cargo", ["test", "--locked", "--workspace", "--all-features"]], + ["cargo", ["bench", "--locked", "--bench", "operation_performance", "--all-features"]], + ["cargo", ["nextest", "run", "--locked", "--workspace", "--no-default-features", "--features", "native"]], + ["cargo", ["nextest", "run", "--release", "--locked", "--workspace", "--all-features"]], + [ + "wasm-pack", + [ + "test", + "--node", + "crates/cose", + "--no-default-features", + "--features", + "wasm", + "--test", + "test_wasm_sign1", + ], + ], + ["cargo", ["run", "--locked", "--manifest-path", "tools/vector-audit/Cargo.toml", "--bin", "reallyme-cose-vector-audit", "--", "."]], + [ + "cargo", + ["metadata", "--manifest-path", "fuzz/Cargo.toml", "--locked", "--no-deps", "--format-version", "1"], + { capture: true }, + ], + ["cargo", ["fmt", "--manifest-path", "fuzz/Cargo.toml", "--check"]], + ["cargo", ["+nightly-2026-07-01", "fuzz", "build"]], + ["cargo", ["check", "--locked", "--workspace", "--no-default-features", "--features", "native"]], [ "cargo", [ "check", + "--locked", "--workspace", "--target", "wasm32-unknown-unknown", @@ -163,12 +1293,34 @@ const validationCommands = [ "wasm", ], ], - ["cargo", ["deny", "check", "bans", "licenses", "sources"]], - ["cargo", packageListArgs], + [ + "cargo", + [ + "check", + "--locked", + "--workspace", + "--target", + "wasm32-unknown-unknown", + "--no-default-features", + "--features", + "wasm,wire", + ], + ], + ["cargo", ["deny", "check"]], + ["cargo", ["audit", "--deny", "warnings"]], ]; -for (const [command, args, options] of validationCommands) { - run(command, args, options); +if (!policyOnlyMode && !generatedFreshnessMode) { + runCommands(validationCommands); } -console.log(`${expectedPackageName} ${expectedVersion} release readiness checks passed`); +const readinessMode = generatedFreshnessMode + ? "generated freshness " + : policyOnlyMode + ? "policy " + : releasePackagesMode + ? "release package " + : ""; +console.log( + `${expectedPackageName} ${expectedVersion} release readiness ${readinessMode}checks passed`, +); diff --git a/scripts/harden-generated-cose-proto.mjs b/scripts/harden-generated-cose-proto.mjs new file mode 100755 index 0000000..bf42198 --- /dev/null +++ b/scripts/harden-generated-cose-proto.mjs @@ -0,0 +1,826 @@ +#!/usr/bin/env node +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +import { readFileSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(fileURLToPath(new URL("..", import.meta.url))); +const generated = resolve( + root, + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.rs", +); +const oneof = resolve( + root, + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.__oneof.rs", +); +const generatedView = resolve( + root, + "crates/proto/src/generated/buffa/reallyme.cose.v1.cose.__view.rs", +); +const protoSource = readFileSync( + resolve(root, "crates/proto/proto/reallyme/cose/v1/cose.proto"), + "utf8", +); +const sensitiveScalarMessageNames = [ + ...protoSource.matchAll(/message\s+(\w+)\s*\{([\s\S]*?)\n\}/gu), +] + .filter((match) => /\b(?:bytes|string)\s+\w+\s*=/u.test(match[2])) + .map((match) => match[1]); +const oneofCount = [...protoSource.matchAll(/\boneof\s+\w+\s*\{/gu)].length; +const supportedArguments = new Set(["--check-idempotent"]); +const suppliedArguments = new Set(); + +function fail(message) { + console.error(`generated COSE proto hardening failed: ${message}`); + process.exit(1); +} + +for (const argument of process.argv.slice(2)) { + if (!supportedArguments.has(argument)) { + fail(`unsupported argument ${argument}`); + } + if (suppliedArguments.has(argument)) { + fail(`argument ${argument} was specified more than once`); + } + suppliedArguments.add(argument); +} +const checkIdempotent = suppliedArguments.has("--check-idempotent"); + +function replaceOnce(text, before, after, path) { + const next = text.replace(before, after); + if (next === text) { + fail(`${path} did not contain expected generated fragment: ${before}`); + } + return next; +} + +function hardenSecretMessage(text, name, privateFieldLine) { + const marker = `pub struct ${name} {`; + const markerIndex = text.indexOf(marker); + if (markerIndex < 0) { + fail(`${generated} is missing ${name}`); + } + + const serdeDerive = "#[derive(::serde::Serialize, ::serde::Deserialize)]"; + const hardenedSerdeDerive = "#[derive(::serde::Serialize)]"; + const cloneDerive = "#[derive(Clone, PartialEq, Default)]"; + const structHeader = text.slice( + Math.max(0, markerIndex - 512), + markerIndex + marker.length, + ); + if (!structHeader.includes(cloneDerive)) { + fail(`${name} is missing the Buffa-required Clone derive`); + } + + const dropImpl = `impl ::core::ops::Drop for ${name} { + fn drop(&mut self) { + ::zeroize::Zeroize::zeroize(&mut self.private_key); + } +} +`; + const hardenedDropPattern = new RegExp( + `impl ::core::ops::Drop for ${name} \\{[\\s\\S]*?::zeroize::Zeroize::zeroize\\(&mut self\\.private_key\\);[\\s\\S]*?\\n\\}\\n`, + "u", + ); + const deserializeImpl = secretDeserializeImpl(name); + const deserializeMarker = `impl<'de> ::serde::Deserialize<'de> for ${name} {`; + const deserializeIndex = text.indexOf(deserializeMarker, markerIndex); + const inherentIndex = text.indexOf(`impl ${name} {`, markerIndex); + const hardenedDeserialize = + deserializeIndex >= 0 && + inherentIndex > deserializeIndex && + text + .slice(deserializeIndex, inherentIndex) + .includes(".map(::zeroize::Zeroizing::new)") && + text + .slice(deserializeIndex, inherentIndex) + .includes("private_key: ::core::mem::take(&mut *wire.private_key)"); + if ( + structHeader.includes(hardenedSerdeDerive) && + hardenedDropPattern.test(text) && + hardenedDeserialize && + !text.includes(privateFieldLine) + ) { + return text; + } + + const serdePrefix = text.slice(0, markerIndex); + const serdeDeriveIndex = serdePrefix.lastIndexOf(serdeDerive); + if (serdeDeriveIndex < 0) { + fail(`${name} is missing the expected serde derive`); + } + text = + text.slice(0, serdeDeriveIndex) + + "#[derive(::serde::Serialize)]" + + text.slice(serdeDeriveIndex + serdeDerive.length); + + text = replaceOnce( + text, + privateFieldLine, + ' .field("private_key", &"")', + generated, + ); + + text = replaceOnce( + text, + " self.private_key.clear();", + " ::zeroize::Zeroize::zeroize(&mut self.private_key);", + generated, + ); + + const implMarker = `impl ${name} {`; + const implIndex = text.indexOf(implMarker, markerIndex); + if (implIndex < 0) { + fail(`${generated} is missing inherent impl for ${name}`); + } + + if (hardenedDropPattern.test(text) || deserializeIndex >= 0) { + fail(`${name} is only partially hardened`); + } + + return text.slice(0, implIndex) + dropImpl + deserializeImpl + text.slice(implIndex); +} + +function redactDebugBytes(text, fieldName) { + return text.replaceAll( + `.field("${fieldName}", &self.${fieldName})`, + `.field("${fieldName}", &"")`, + ); +} + +function hardenBorrowedViewDebug(text, name) { + const viewName = `${name}View`; + const deriveAndStruct = `#[derive(Clone, Debug, Default)] +pub struct ${viewName}<'a> {`; + const replacement = `#[derive(Clone, Default)] +pub struct ${viewName}<'a> {`; + const redactedViewDebug = `impl ::core::fmt::Debug for ${viewName}<'_> { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("${viewName}()") + } +} +`; + const ownedViewName = `${name}OwnedView`; + const ownedReplacement = `#[derive(Clone)] +pub struct ${ownedViewName}(`; + const redactedOwnedDebug = `impl ::core::fmt::Debug for ${ownedViewName} { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + formatter.write_str("${ownedViewName}()") + } +} +`; + if ( + text.includes(replacement) && + text.includes(redactedViewDebug) && + text.includes(ownedReplacement) && + text.includes(redactedOwnedDebug) + ) { + return text; + } + text = replaceOnce(text, deriveAndStruct, replacement, generatedView); + + const messageViewPattern = new RegExp( + `impl<'a> ::buffa::MessageView<'a>\\s+for ${viewName}<'a> \\{`, + "u", + ); + if (!messageViewPattern.test(text)) { + fail(`${generatedView} is missing MessageView for ${viewName}`); + } + text = text.replace( + messageViewPattern, + (messageViewImpl) => `${redactedViewDebug}${messageViewImpl}`, + ); + + const ownedDeriveAndStruct = `#[derive(Clone, Debug)] +pub struct ${ownedViewName}(`; + text = replaceOnce(text, ownedDeriveAndStruct, ownedReplacement, generatedView); + + const ownedImpl = `impl ${ownedViewName} {`; + return replaceOnce( + text, + ownedImpl, + `${redactedOwnedDebug}${ownedImpl}`, + generatedView, + ); +} + +function hardenByteFieldsOnDrop(text, name, fieldNames) { + const body = [ + ...fieldNames.map( + (fieldName) => ` ::zeroize::Zeroize::zeroize(&mut self.${fieldName});`, + ), + " __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields);", + ].join("\n"); + const dropImpl = `impl ::core::ops::Drop for ${name} { + fn drop(&mut self) { +${body} + } +} +`; + + const existingDropPattern = new RegExp( + `impl ::core::ops::Drop for ${name} \\{\\n fn drop\\(&mut self\\) \\{\\n[\\s\\S]*? \\}\\n\\}\\n`, + ); + if (existingDropPattern.test(text)) { + return text.replace(existingDropPattern, dropImpl); + } + + const implMarker = `impl ${name} {`; + const implIndex = text.indexOf(implMarker); + if (implIndex < 0) { + fail(`${generated} is missing inherent impl for ${name}`); + } + return text.slice(0, implIndex) + dropImpl + text.slice(implIndex); +} + +function secretDeserializeImpl(name) { + if (name === "CoseSign1CreateRequest" || name === "CoseSign1CreateDetachedRequest") { + return `impl<'de> ::serde::Deserialize<'de> for ${name} { + fn deserialize(deserializer: D) -> ::core::result::Result + where + D: ::serde::Deserializer<'de>, + { + fn deserialize_secret_bytes<'de, D>( + deserializer: D, + ) -> ::core::result::Result<::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, D::Error> + where + D: ::serde::Deserializer<'de>, + { + ::buffa::json_helpers::bytes::deserialize(deserializer) + .map(::zeroize::Zeroizing::new) + } + + #[derive(Default, ::serde::Deserialize)] + #[serde(default)] + struct Wire { + #[serde(rename = "algorithm", with = "::buffa::json_helpers::proto_enum")] + algorithm: ::buffa::EnumValue, + #[serde(rename = "payload", deserialize_with = "deserialize_secret_bytes")] + payload: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde( + rename = "privateKey", + alias = "private_key", + deserialize_with = "deserialize_secret_bytes" + )] + private_key: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde(rename = "kid", deserialize_with = "deserialize_secret_bytes")] + kid: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde( + rename = "hasKid", + alias = "has_kid", + with = "::buffa::json_helpers::proto_bool" + )] + has_kid: bool, + #[serde(rename = "options")] + options: ::buffa::MessageField>, + #[serde( + rename = "externalAad", + alias = "external_aad", + deserialize_with = "deserialize_secret_bytes" + )] + external_aad: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + } + + let mut wire = Wire::deserialize(deserializer)?; + Ok(Self { + algorithm: wire.algorithm, + payload: ::core::mem::take(&mut *wire.payload), + private_key: ::core::mem::take(&mut *wire.private_key), + kid: ::core::mem::take(&mut *wire.kid), + has_kid: wire.has_kid, + options: ::core::mem::take(&mut wire.options), + external_aad: ::core::mem::take(&mut *wire.external_aad), + __buffa_unknown_fields: Default::default(), + }) + } +} +`; + } + + if (name === "CoseKeyFromPrivateBytesRequest") { + return `impl<'de> ::serde::Deserialize<'de> for ${name} { + fn deserialize(deserializer: D) -> ::core::result::Result + where + D: ::serde::Deserializer<'de>, + { + fn deserialize_secret_bytes<'de, D>( + deserializer: D, + ) -> ::core::result::Result<::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, D::Error> + where + D: ::serde::Deserializer<'de>, + { + ::buffa::json_helpers::bytes::deserialize(deserializer) + .map(::zeroize::Zeroizing::new) + } + + #[derive(Default, ::serde::Deserialize)] + #[serde(default)] + struct Wire { + #[serde(rename = "algorithm")] + algorithm: ::buffa::MessageField>, + #[serde( + rename = "privateKey", + alias = "private_key", + deserialize_with = "deserialize_secret_bytes" + )] + private_key: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde( + rename = "publicKey", + alias = "public_key", + deserialize_with = "deserialize_secret_bytes" + )] + public_key: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, + #[serde( + rename = "hasPublicKey", + alias = "has_public_key", + with = "::buffa::json_helpers::proto_bool" + )] + has_public_key: bool, + } + + let mut wire = Wire::deserialize(deserializer)?; + Ok(Self { + algorithm: wire.algorithm, + private_key: ::core::mem::take(&mut *wire.private_key), + public_key: ::core::mem::take(&mut *wire.public_key), + has_public_key: wire.has_public_key, + __buffa_unknown_fields: Default::default(), + }) + } +} +`; + } + + fail(`no secret Deserialize hardening template for ${name}`); +} + +function genericSensitiveDeserializeImpl(name, fields) { + const hasSensitiveBytes = fields.some((field) => field.kind === "bytes"); + const hasSensitiveString = fields.some((field) => field.kind === "string"); + const wireFields = fields + .map((field) => { + const alias = field.jsonName === field.name ? "" : `, alias = "${field.name}"`; + if (field.kind === "bytes") { + return ` #[serde(rename = "${field.jsonName}"${alias}, deserialize_with = "deserialize_secret_bytes")] + ${field.name}: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>,`; + } + if (field.kind === "string") { + return ` #[serde(rename = "${field.jsonName}"${alias}, deserialize_with = "deserialize_secret_string")] + ${field.name}: ::zeroize::Zeroizing<::buffa::alloc::string::String>,`; + } + if (field.kind === "enum") { + return ` #[serde(rename = "${field.jsonName}"${alias}, with = "::buffa::json_helpers::proto_enum")] + ${field.name}: ::buffa::EnumValue<${field.enumName}>,`; + } + if (field.kind === "message") { + return ` #[serde(rename = "${field.jsonName}"${alias})] + ${field.name}: ::buffa::MessageField<${field.messageName}, ::buffa::Inline<${field.messageName}>>,`; + } + if (field.kind === "repeated_enum") { + return ` #[serde(rename = "${field.jsonName}"${alias}, with = "::buffa::json_helpers::repeated_enum")] + ${field.name}: ::buffa::alloc::vec::Vec<::buffa::EnumValue<${field.enumName}>>,`; + } + if (field.kind === "u64") { + return ` #[serde(rename = "${field.jsonName}"${alias}, with = "::buffa::json_helpers::uint64")] + ${field.name}: u64,`; + } + if (field.kind === "bool") { + return ` #[serde(rename = "${field.jsonName}"${alias}, with = "::buffa::json_helpers::proto_bool")] + ${field.name}: bool,`; + } + fail(`unsupported generated sensitive field kind ${field.kind} for ${name}`); + }) + .join("\n"); + const assignments = fields + .map((field) => { + if (field.kind === "bytes" || field.kind === "string") { + return ` ${field.name}: ::core::mem::take(&mut *wire.${field.name}),`; + } + if (field.kind === "repeated_enum") { + return ` ${field.name}: ::core::mem::take(&mut wire.${field.name}),`; + } + if (field.kind === "message") { + return ` ${field.name}: ::core::mem::take(&mut wire.${field.name}),`; + } + return ` ${field.name}: wire.${field.name},`; + }) + .join("\n"); + + const bytesDeserializer = hasSensitiveBytes + ? ` fn deserialize_secret_bytes<'de, D>( + deserializer: D, + ) -> ::core::result::Result<::zeroize::Zeroizing<::buffa::alloc::vec::Vec>, D::Error> + where + D: ::serde::Deserializer<'de>, + { + ::buffa::json_helpers::bytes::deserialize(deserializer) + .map(::zeroize::Zeroizing::new) + } + +` + : ""; + const stringDeserializer = hasSensitiveString + ? ` fn deserialize_secret_string<'de, D>( + deserializer: D, + ) -> ::core::result::Result<::zeroize::Zeroizing<::buffa::alloc::string::String>, D::Error> + where + D: ::serde::Deserializer<'de>, + { + <::buffa::alloc::string::String as ::serde::Deserialize>::deserialize(deserializer) + .map(::zeroize::Zeroizing::new) + } + +` + : ""; + + return `impl<'de> ::serde::Deserialize<'de> for ${name} { + fn deserialize(deserializer: D) -> ::core::result::Result + where + D: ::serde::Deserializer<'de>, + { +${bytesDeserializer}${stringDeserializer} + #[derive(Default, ::serde::Deserialize)] + #[serde(default)] + struct Wire { +${wireFields} + } + + let mut wire = Wire::deserialize(deserializer)?; + Ok(Self { +${assignments} + __buffa_unknown_fields: Default::default(), + }) + } +} +`; +} + +function hardenSensitiveDeserialize(text, name, fields) { + const marker = `pub struct ${name} {`; + const markerIndex = text.indexOf(marker); + if (markerIndex < 0) { + fail(`${generated} is missing ${name}`); + } + const serdeDerive = "#[derive(::serde::Serialize, ::serde::Deserialize)]"; + const hardenedSerdeDerive = "#[derive(::serde::Serialize)]"; + const deserializeImpl = genericSensitiveDeserializeImpl(name, fields); + const structHeader = text.slice(Math.max(0, markerIndex - 512), markerIndex); + const deserializeMarker = `impl<'de> ::serde::Deserialize<'de> for ${name} {`; + const deserializeIndex = text.indexOf(deserializeMarker, markerIndex); + const inherentIndex = text.indexOf(`impl ${name} {`, markerIndex); + const hardenedDeserialize = + deserializeIndex >= 0 && + inherentIndex > deserializeIndex && + text + .slice(deserializeIndex, inherentIndex) + .includes(".map(::zeroize::Zeroizing::new)") && + text + .slice(deserializeIndex, inherentIndex) + .includes("__buffa_unknown_fields: Default::default()"); + if (structHeader.includes(hardenedSerdeDerive) && hardenedDeserialize) { + return text; + } + const serdePrefix = text.slice(0, markerIndex); + const serdeDeriveIndex = serdePrefix.lastIndexOf(serdeDerive); + if (serdeDeriveIndex < 0) { + fail(`${name} is missing the expected serde derive`); + } + text = + text.slice(0, serdeDeriveIndex) + + "#[derive(::serde::Serialize)]" + + text.slice(serdeDeriveIndex + serdeDerive.length); + + const implMarker = `impl ${name} {`; + const implIndex = text.indexOf(implMarker, markerIndex); + if (implIndex < 0) { + fail(`${generated} is missing inherent impl for ${name}`); + } + if (deserializeIndex >= 0 || text.includes(deserializeImpl)) { + fail(`${name} is only partially JSON-hardened`); + } + return text.slice(0, implIndex) + deserializeImpl + text.slice(implIndex); +} + +let generatedText = readFileSync(generated, "utf8"); +const generatedHeader = `// @generated by buffa-codegen. DO NOT EDIT. +// source: reallyme/cose/v1/cose.proto +`; +const unknownFieldZeroizeHelpers = ` +fn __reallyme_zeroize_unknown_fields(fields: &mut ::buffa::UnknownFields) { + for mut field in ::core::mem::take(fields) { + __reallyme_zeroize_unknown_field_data(&mut field.data); + } +} + +fn __reallyme_zeroize_unknown_field_data(data: &mut ::buffa::UnknownFieldData) { + match data { + ::buffa::UnknownFieldData::LengthDelimited(bytes) => { + ::zeroize::Zeroize::zeroize(bytes); + } + ::buffa::UnknownFieldData::Group(fields) => { + __reallyme_zeroize_unknown_fields(fields); + } + ::buffa::UnknownFieldData::Varint(_) + | ::buffa::UnknownFieldData::Fixed64(_) + | ::buffa::UnknownFieldData::Fixed32(_) => {} + } +} +`; +if (!generatedText.includes("__reallyme_zeroize_unknown_fields")) { + generatedText = replaceOnce( + generatedText, + generatedHeader, + `${generatedHeader}${unknownFieldZeroizeHelpers}`, + generated, + ); +} +generatedText = hardenSecretMessage( + generatedText, + "CoseSign1CreateRequest", + ' .field("private_key", &self.private_key)', +); +for (const [name, fields] of [ + ["CoseSign1CreateResult", [{ name: "cose_sign1", jsonName: "coseSign1", kind: "bytes" }]], + [ + "CoseSign1VerifyRequest", + [ + { name: "cose_sign1", jsonName: "coseSign1", kind: "bytes" }, + { name: "public_key", jsonName: "publicKey", kind: "bytes" }, + { name: "max_cose_sign1_bytes", jsonName: "maxCoseSign1Bytes", kind: "u64" }, + { name: "max_detached_payload_bytes", jsonName: "maxDetachedPayloadBytes", kind: "u64" }, + { name: "require_kid", jsonName: "requireKid", kind: "bool" }, + { name: "allowed_algorithms", jsonName: "allowedAlgorithms", kind: "repeated_enum", enumName: "CoseSignatureAlgorithm" }, + { name: "external_aad", jsonName: "externalAad", kind: "bytes" }, + { name: "expected_kid", jsonName: "expectedKid", kind: "bytes" }, + ], + ], + [ + "CoseSign1VerifyDetachedRequest", + [ + { name: "cose_sign1", jsonName: "coseSign1", kind: "bytes" }, + { name: "payload", jsonName: "payload", kind: "bytes" }, + { name: "public_key", jsonName: "publicKey", kind: "bytes" }, + { name: "max_cose_sign1_bytes", jsonName: "maxCoseSign1Bytes", kind: "u64" }, + { name: "max_detached_payload_bytes", jsonName: "maxDetachedPayloadBytes", kind: "u64" }, + { name: "require_kid", jsonName: "requireKid", kind: "bool" }, + { name: "allowed_algorithms", jsonName: "allowedAlgorithms", kind: "repeated_enum", enumName: "CoseSignatureAlgorithm" }, + { name: "external_aad", jsonName: "externalAad", kind: "bytes" }, + { name: "expected_kid", jsonName: "expectedKid", kind: "bytes" }, + ], + ], + [ + "CoseSign1VerifyResult", + [ + { name: "payload", jsonName: "payload", kind: "bytes" }, + { name: "algorithm", jsonName: "algorithm", kind: "enum", enumName: "CoseSignatureAlgorithm" }, + { name: "kid", jsonName: "kid", kind: "bytes" }, + ], + ], + [ + "CoseKeyFromPublicBytesRequest", + [ + { name: "algorithm", jsonName: "algorithm", kind: "message", messageName: "CoseAlgorithmIdentifier" }, + { name: "public_key", jsonName: "publicKey", kind: "bytes" }, + ], + ], + ["CoseKeyBytesRequest", [{ name: "cose_key", jsonName: "coseKey", kind: "bytes" }]], + ["CoseKeyBytesResult", [{ name: "key_bytes", jsonName: "keyBytes", kind: "bytes" }]], + [ + "CoseMultikeyToCoseKeyRequest", + [{ name: "multikey", jsonName: "multikey", kind: "string" }], + ], + ["CoseMultikeyResult", [{ name: "multikey", jsonName: "multikey", kind: "string" }]], + [ + "CoseMlKemEncryptRequest", + [ + { name: "kem_algorithm", jsonName: "kemAlgorithm", kind: "enum", enumName: "CoseKemAlgorithm" }, + { + name: "content_algorithm", + jsonName: "contentAlgorithm", + kind: "enum", + enumName: "CoseContentEncryptionAlgorithm", + }, + { name: "recipient_public_key", jsonName: "recipientPublicKey", kind: "bytes" }, + { name: "recipient_kid", jsonName: "recipientKid", kind: "bytes" }, + { name: "plaintext", jsonName: "plaintext", kind: "bytes" }, + { name: "external_aad", jsonName: "externalAad", kind: "bytes" }, + { name: "supp_priv_info", jsonName: "suppPrivInfo", kind: "bytes" }, + { name: "has_supp_priv_info", jsonName: "hasSuppPrivInfo", kind: "bool" }, + ], + ], + [ + "CoseMlKemEncryptResult", + [{ name: "cose_encrypt", jsonName: "coseEncrypt", kind: "bytes" }], + ], + [ + "CoseMlKemDecryptRequest", + [ + { name: "cose_encrypt", jsonName: "coseEncrypt", kind: "bytes" }, + { name: "recipient_private_key", jsonName: "recipientPrivateKey", kind: "bytes" }, + { name: "expected_recipient_kid", jsonName: "expectedRecipientKid", kind: "bytes" }, + { name: "external_aad", jsonName: "externalAad", kind: "bytes" }, + { name: "supp_priv_info", jsonName: "suppPrivInfo", kind: "bytes" }, + { name: "has_supp_priv_info", jsonName: "hasSuppPrivInfo", kind: "bool" }, + ], + ], + [ + "CoseMlKemDecryptResult", + [ + { name: "plaintext", jsonName: "plaintext", kind: "bytes" }, + { + name: "content_algorithm", + jsonName: "contentAlgorithm", + kind: "enum", + enumName: "CoseContentEncryptionAlgorithm", + }, + { name: "kem_algorithm", jsonName: "kemAlgorithm", kind: "enum", enumName: "CoseKemAlgorithm" }, + { name: "mode", jsonName: "mode", kind: "enum", enumName: "CoseMlKemMode" }, + { name: "recipient_kid", jsonName: "recipientKid", kind: "bytes" }, + ], + ], +]) { + generatedText = hardenSensitiveDeserialize(generatedText, name, fields); +} +generatedText = hardenSecretMessage( + generatedText, + "CoseSign1CreateDetachedRequest", + ' .field("private_key", &self.private_key)', +); +generatedText = hardenSecretMessage( + generatedText, + "CoseKeyFromPrivateBytesRequest", + ' .field("private_key", &self.private_key)', +); +for (const fieldName of [ + "key_bytes", + "cose_key", + "cose_sign1", + "kid", + "payload", + "private_key", + "public_key", + "external_aad", + "expected_kid", + "cose_encrypt", + "plaintext", + "recipient_public_key", + "recipient_private_key", + "recipient_kid", + "expected_recipient_kid", + "supp_priv_info", + "multikey", +]) { + generatedText = redactDebugBytes(generatedText, fieldName); +} +for (const [name, fieldNames] of [ + // The top-level request owns a nested oneof that can contain private keys or + // plaintext. Its child messages wipe their declared fields; this owner must + // additionally wipe length-delimited unknown fields retained by Buffa. + ["CoseOperationRequest", []], + // Versioned result wrappers own nested messages whose sensitive scalar + // fields have dedicated wipe paths. The wrappers still need unknown-field + // cleanup so future or malicious length-delimited values cannot linger. + ["CoseOperationResponseV2", []], + ["CoseOperationResult", []], + ["CoseSign1CreateRequest", ["payload", "private_key", "kid", "external_aad"]], + ["CoseSign1CreateDetachedRequest", ["payload", "private_key", "kid", "external_aad"]], + ["CoseSign1CreateResult", ["cose_sign1"]], + ["CoseSign1VerifyRequest", ["cose_sign1", "public_key", "external_aad", "expected_kid"]], + ["CoseSign1VerifyDetachedRequest", ["cose_sign1", "payload", "public_key", "external_aad", "expected_kid"]], + ["CoseSign1VerifyResult", ["payload", "kid"]], + ["CoseKeyFromPublicBytesRequest", ["public_key"]], + ["CoseKeyFromPrivateBytesRequest", ["private_key", "public_key"]], + ["CoseKeyBytesRequest", ["cose_key"]], + ["CoseKeyBytesResult", ["key_bytes"]], + ["CoseMultikeyToCoseKeyRequest", ["multikey"]], + ["CoseMultikeyResult", ["multikey"]], + [ + "CoseMlKemEncryptRequest", + [ + "recipient_public_key", + "recipient_kid", + "plaintext", + "external_aad", + "supp_priv_info", + ], + ], + ["CoseMlKemEncryptResult", ["cose_encrypt"]], + [ + "CoseMlKemDecryptRequest", + [ + "cose_encrypt", + "recipient_private_key", + "expected_recipient_kid", + "external_aad", + "supp_priv_info", + ], + ], + ["CoseMlKemDecryptResult", ["plaintext", "recipient_kid"]], +]) { + generatedText = hardenByteFieldsOnDrop(generatedText, name, fieldNames); +} +for (const fieldName of [ + "key_bytes", + "cose_key", + "cose_sign1", + "kid", + "payload", + "private_key", + "public_key", + "external_aad", + "expected_kid", + "cose_encrypt", + "plaintext", + "recipient_public_key", + "recipient_private_key", + "recipient_kid", + "expected_recipient_kid", + "supp_priv_info", + "multikey", +]) { + generatedText = generatedText.replaceAll( + ` self.${fieldName}.clear();`, + ` ::zeroize::Zeroize::zeroize(&mut self.${fieldName});`, + ); +} +generatedText = generatedText.replaceAll( + " self.__buffa_unknown_fields.clear();", + " __reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields);", +); + +// ProtoJSON rejects unknown fields by default. Buffa's generated serde derives +// and oneof visitors currently accept them, so harden both forms here. This is +// boundary validation, not a compatibility policy: callers must not be able to +// misspell a security-relevant field and receive a successful defaulted +// operation. +generatedText = generatedText.replaceAll( + "#[serde(default)]", + "#[serde(default, deny_unknown_fields)]", +); +const ignoredUnknownField = ` _ => { + map.next_value::()?; + }`; +const strictUnknownField = ` _ => { + return Err(serde::de::Error::custom("unknown field")); + }`; +const ignoredUnknownFieldCount = + generatedText.split(ignoredUnknownField).length - 1; +const strictUnknownFieldCount = generatedText.split(strictUnknownField).length - 1; +if ( + ignoredUnknownFieldCount !== oneofCount && + !(ignoredUnknownFieldCount === 0 && strictUnknownFieldCount === oneofCount) +) { + fail( + `${generated} expected ${oneofCount} generated oneof unknown-field branches, found ${ignoredUnknownFieldCount}`, + ); +} +generatedText = generatedText.replaceAll( + ignoredUnknownField, + strictUnknownField, +); +// Buffa's enum visitors otherwise reflect attacker-controlled numeric values +// into allocated error strings. Fixed diagnostics keep boundary failures +// deterministic and avoid carrying untrusted input into logs. +generatedText = generatedText.replaceAll( + `::serde::de::Error::custom( + ::buffa::alloc::format!("enum value {v} out of i32 range"), + )`, + `::serde::de::Error::custom("enum value out of i32 range")`, +); +generatedText = generatedText.replaceAll( + `::serde::de::Error::custom( + ::buffa::alloc::format!("unknown enum value {v32}"), + )`, + `::serde::de::Error::custom("unknown enum value")`, +); +if (generatedText.includes("::buffa::alloc::format!(")) { + fail(`${generated} still contains formatted ProtoJSON errors`); +} + +const generatedPaths = [generated, generatedView, oneof]; +const idempotencyBefore = checkIdempotent + ? new Map(generatedPaths.map((path) => [path, readFileSync(path)])) + : null; +writeFileSync(generated, generatedText); + +let generatedViewText = readFileSync(generatedView, "utf8"); +for (const messageName of sensitiveScalarMessageNames) { + generatedViewText = hardenBorrowedViewDebug(generatedViewText, messageName); +} +writeFileSync(generatedView, generatedViewText); + +const oneofText = readFileSync(oneof, "utf8"); +if (!oneofText.includes(" #[derive(Clone, PartialEq, Debug)]\n pub enum Operation {")) { + fail(`${oneof} is missing the Buffa-required Clone derive for Operation`); +} + +if (idempotencyBefore !== null) { + for (const [path, before] of idempotencyBefore) { + if (!before.equals(readFileSync(path))) { + fail("generated COSE protobuf hardening is not idempotent"); + } + } +} diff --git a/scripts/publish_crates_in_order.mjs b/scripts/publish_crates_in_order.mjs index b1443be..404cfc4 100644 --- a/scripts/publish_crates_in_order.mjs +++ b/scripts/publish_crates_in_order.mjs @@ -3,23 +3,45 @@ // SPDX-License-Identifier: Apache-2.0 import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; const MODE_INSPECT = "inspect"; +const MODE_ORDER = "order"; const MODE_PUBLISH = "publish"; +const MAX_PUBLISH_ATTEMPTS = 12; +const CRATES_IO_DEFAULT_RATE_LIMIT_RETRY_MS = 60000; +const CRATES_IO_INDEX_RETRY_BASE_MS = 15000; +const REQUIRED_PUBLISH_ORDER_EDGES = [["reallyme-cose-proto", "reallyme-cose"]]; const args = process.argv.slice(2); const mode = args[0] ?? MODE_INSPECT; const allowDirty = args.includes("--allow-dirty"); const unknownArgs = args.slice(1).filter((arg) => arg !== "--allow-dirty"); +const releaseVersion = process.env.RELEASE_VERSION ?? ""; -if ((mode !== MODE_INSPECT && mode !== MODE_PUBLISH) || unknownArgs.length !== 0) { +if ( + (mode !== MODE_INSPECT && mode !== MODE_ORDER && mode !== MODE_PUBLISH) || + unknownArgs.length !== 0 +) { console.error( - `usage: node scripts/publish_crates_in_order.mjs ${MODE_INSPECT}|${MODE_PUBLISH} [--allow-dirty]`, + `usage: node scripts/publish_crates_in_order.mjs ${MODE_INSPECT}|${MODE_ORDER}|${MODE_PUBLISH} [--allow-dirty]`, ); process.exit(2); } -if (allowDirty && mode !== MODE_INSPECT) { - console.error("--allow-dirty is only supported for local package inspection"); +if (allowDirty && mode !== MODE_INSPECT && mode !== MODE_ORDER) { + console.error("--allow-dirty is only supported for local package inspection and order checks"); + process.exit(2); +} + +if (mode === MODE_PUBLISH && releaseVersion.length === 0) { + console.error("RELEASE_VERSION must be set when publishing crates."); + process.exit(2); +} + +if (releaseVersion.length !== 0 && !/^[0-9]+[.][0-9]+[.][0-9]+$/u.test(releaseVersion)) { + console.error("RELEASE_VERSION must be an exact semver release such as 0.2.0."); process.exit(2); } @@ -53,9 +75,13 @@ function retryAfterMs(output) { return Math.max(delayMs, 10000); } -const metadataResult = run("cargo", ["metadata", "--format-version", "1", "--no-deps"], { - capture: true, -}); +const metadataResult = run( + "cargo", + ["metadata", "--locked", "--format-version", "1", "--no-deps"], + { + capture: true, + }, +); if (metadataResult.status !== 0) { process.stderr.write(metadataResult.stderr); @@ -63,6 +89,7 @@ if (metadataResult.status !== 0) { } const metadata = JSON.parse(metadataResult.stdout); +const packageDirectory = path.join(metadata.target_directory, "package"); function isPublishablePackage(pkg) { return !(Array.isArray(pkg.publish) && pkg.publish.length === 0); @@ -75,8 +102,20 @@ for (const pkg of metadata.packages) { } } +function dependencyPackageName(dep) { + return dep.package ?? dep.name; +} + function isWorkspacePathDependency(dep) { - return dep.source === null && typeof dep.path === "string" && publishable.has(dep.name); + return ( + dep.source === null && + typeof dep.path === "string" && + publishable.has(dependencyPackageName(dep)) + ); +} + +function isPublishOrderingDependency(dep) { + return isWorkspacePathDependency(dep) && dep.kind !== "dev"; } function parseVersion(version) { @@ -131,11 +170,11 @@ function checkPathDependencyVersions() { const failures = []; for (const pkg of publishable.values()) { for (const dep of pkg.dependencies) { - if (!isWorkspacePathDependency(dep)) { + if (!isPublishOrderingDependency(dep)) { continue; } - const target = publishable.get(dep.name); + const target = publishable.get(dependencyPackageName(dep)); if (!isCaretReqSatisfied(dep.req, target.version)) { failures.push( `${pkg.name} depends on ${dep.name} with ${dep.req}; local version is ${target.version}`, @@ -153,6 +192,26 @@ function checkPathDependencyVersions() { } } +function checkReleaseVersion() { + if (releaseVersion.length === 0) { + return; + } + + const failures = []; + for (const pkg of publishable.values()) { + if (pkg.version !== releaseVersion) { + failures.push(`${pkg.name} is ${pkg.version}; expected ${releaseVersion}`); + } + } + if (failures.length !== 0) { + console.error("publishable crate versions do not match RELEASE_VERSION:"); + for (const failure of failures) { + console.error(`- ${failure}`); + } + process.exit(1); + } +} + const visiting = new Set(); const visited = new Set(); const ordered = []; @@ -168,8 +227,8 @@ function visit(pkg) { visiting.add(pkg.name); for (const dep of pkg.dependencies) { - const depName = dep.package ?? dep.name; - if (dep.source === null && publishable.has(depName)) { + const depName = dependencyPackageName(dep); + if (isPublishOrderingDependency(dep) && publishable.has(depName)) { visit(publishable.get(depName)); } } @@ -187,13 +246,71 @@ for (const pkg of ordered) { console.log(`- ${pkg.name} ${pkg.version}`); } -checkPathDependencyVersions(); +function checkRequiredPublishOrderEdges() { + const failures = []; + const orderedPackageNames = new Set(orderedIndexByName.keys()); + + for (const [dependencyName, packageName] of REQUIRED_PUBLISH_ORDER_EDGES) { + const dependencyIndex = orderedIndexByName.get(dependencyName); + const packageIndex = orderedIndexByName.get(packageName); + if (dependencyIndex === undefined || packageIndex === undefined) { + failures.push(`${dependencyName} before ${packageName} cannot be checked; package is missing`); + continue; + } + + if (dependencyIndex >= packageIndex) { + failures.push(`${dependencyName} must publish before ${packageName}`); + } + } + + if (failures.length !== 0) { + console.error( + `publishable packages discovered: ${[...orderedPackageNames].sort().join(", ")}`, + ); + console.error("required publish dependency order is not satisfied:"); + for (const failure of failures) { + console.error(`- ${failure}`); + } + process.exit(1); + } +} const orderedIndexByName = new Map(); ordered.forEach((pkg, index) => { orderedIndexByName.set(pkg.name, index); }); +checkPathDependencyVersions(); +checkRequiredPublishOrderEdges(); +checkReleaseVersion(); + +if (mode === MODE_ORDER) { + process.exit(0); +} + +const unpackDirectory = path.join(packageDirectory, "release-preflight"); + +if (mode === MODE_INSPECT) { + const packageArgs = ["package", "--workspace", "--no-verify", "--locked"]; + if (allowDirty) { + packageArgs.push("--allow-dirty"); + } + const packageResult = run("cargo", packageArgs); + if (packageResult.status !== 0) { + process.exit(packageResult.status ?? 1); + } + + fs.rmSync(unpackDirectory, { force: true, recursive: true }); + fs.mkdirSync(unpackDirectory, { recursive: true }); + for (const pkg of ordered) { + const archive = path.join(packageDirectory, `${pkg.name}-${pkg.version}.crate`); + const extractResult = run("tar", ["-xzf", archive, "-C", unpackDirectory]); + if (extractResult.status !== 0) { + process.exit(extractResult.status ?? 1); + } + } +} + function unresolvedRegistryPackages(output) { const missing = []; const noMatchPattern = /no matching package named `([^`]+)` found/g; @@ -220,7 +337,7 @@ function isEarlierWorkspaceDependency(pkg, depName) { } function inspectPackage(pkg) { - const listArgs = ["package", "-p", pkg.name, "--list"]; + const listArgs = ["package", "-p", pkg.name, "--list", "--locked"]; if (allowDirty) { listArgs.push("--allow-dirty"); } @@ -229,6 +346,47 @@ function inspectPackage(pkg) { process.exit(listResult.status ?? 1); } + const manifestPath = path.join(unpackDirectory, `${pkg.name}-${pkg.version}`, "Cargo.toml"); + const patchArgs = []; + for (const dep of pkg.dependencies) { + const depName = dep.package ?? dep.name; + if (!isEarlierWorkspaceDependency(pkg, depName)) { + continue; + } + const dependency = publishable.get(depName); + const dependencyPath = path.join(unpackDirectory, `${dependency.name}-${dependency.version}`); + patchArgs.push( + "--config", + `patch.crates-io.'${dependency.name}'.path=${JSON.stringify(dependencyPath)}`, + ); + } + + // Fetch the normalized archive's locked dependency graph explicitly before + // enforcing an offline build. This proves each packaged crate builds from + // its published shape, not only from the workspace path dependency graph. + const fetchArgs = ["fetch", "--manifest-path", manifestPath, ...patchArgs]; + if (patchArgs.length === 0) { + fetchArgs.push("--locked"); + } + const fetchResult = run("cargo", fetchArgs); + if (fetchResult.status !== 0) { + process.exit(fetchResult.status ?? 1); + } + + const checkArgs = [ + "check", + "--manifest-path", + manifestPath, + "--all-features", + "--locked", + "--offline", + ...patchArgs, + ]; + const checkResult = run("cargo", checkArgs); + if (checkResult.status !== 0) { + process.exit(checkResult.status ?? 1); + } + const dryRunArgs = ["publish", "-p", pkg.name, "--dry-run", "--locked"]; if (allowDirty) { dryRunArgs.push("--allow-dirty"); @@ -256,9 +414,20 @@ function inspectPackage(pkg) { } function publishPackage(pkg) { + const packageResult = run("cargo", [ + "package", + "-p", + pkg.name, + "--no-verify", + "--locked", + ]); + if (packageResult.status !== 0) { + process.exit(packageResult.status ?? 1); + } + const args = ["publish", "-p", pkg.name, "--locked"]; - for (let attempt = 1; attempt <= 12; attempt += 1) { + for (let attempt = 1; attempt <= MAX_PUBLISH_ATTEMPTS; attempt += 1) { const result = run("cargo", args, { capture: true }); process.stdout.write(result.stdout); process.stderr.write(result.stderr); @@ -269,25 +438,31 @@ function publishPackage(pkg) { const combined = `${result.stdout}\n${result.stderr}`; if (combined.includes("already uploaded") || combined.includes("already exists")) { + verifyPublishedPackageMatches(pkg); console.log(`${pkg.name} ${pkg.version} is already published; continuing.`); return; } const lowerCombined = combined.toLowerCase(); const rateLimitDelayMs = retryAfterMs(combined); - if (lowerCombined.includes("too many requests") && rateLimitDelayMs !== null) { + if ( + lowerCombined.includes("too many requests") || + lowerCombined.includes("rate-limited") || + lowerCombined.includes("rate limited") + ) { + const delayMs = rateLimitDelayMs ?? CRATES_IO_DEFAULT_RATE_LIMIT_RETRY_MS; console.log( - `crates.io rate-limited new crate uploads; retrying ${pkg.name} in ${Math.ceil(rateLimitDelayMs / 1000)}s...`, + `crates.io rate-limited new crate uploads; retrying ${pkg.name} in ${Math.ceil(delayMs / 1000)}s...`, ); - sleepMs(rateLimitDelayMs); + sleepMs(delayMs); continue; } - if (!combined.includes("no matching package named") || attempt === 12) { + if (!combined.includes("no matching package named") || attempt === MAX_PUBLISH_ATTEMPTS) { process.exit(result.status ?? 1); } - const delayMs = attempt * 15000; + const delayMs = attempt * CRATES_IO_INDEX_RETRY_BASE_MS; console.log( `crates.io index has not observed a freshly published dependency yet; retrying ${pkg.name} in ${delayMs / 1000}s...`, ); @@ -295,6 +470,59 @@ function publishPackage(pkg) { } } +function verifyPublishedPackageMatches(pkg) { + const localArchive = path.join(packageDirectory, `${pkg.name}-${pkg.version}.crate`); + if (!fs.existsSync(localArchive)) { + console.error(`${pkg.name} ${pkg.version} local package archive is missing`); + process.exit(1); + } + + const comparisonDirectory = fs.mkdtempSync(path.join(packageDirectory, "published-")); + const publishedArchive = path.join(comparisonDirectory, `${pkg.name}-${pkg.version}.crate`); + const packageName = encodeURIComponent(pkg.name); + const packageVersion = encodeURIComponent(pkg.version); + const downloadUrl = + `https://static.crates.io/crates/${packageName}/${packageName}-${packageVersion}.crate`; + + try { + const downloadResult = run( + "curl", + [ + "--fail-with-body", + "--location", + "--proto", + "=https", + "--tlsv1.2", + "--retry", + "5", + "--retry-all-errors", + "--output", + publishedArchive, + downloadUrl, + ], + { capture: true }, + ); + if (downloadResult.status !== 0) { + process.stdout.write(downloadResult.stdout); + process.stderr.write(downloadResult.stderr); + process.exit(downloadResult.status ?? 1); + } + + const localChecksum = createHash("sha256").update(fs.readFileSync(localArchive)).digest("hex"); + const publishedChecksum = createHash("sha256") + .update(fs.readFileSync(publishedArchive)) + .digest("hex"); + if (localChecksum !== publishedChecksum) { + console.error( + `${pkg.name} ${pkg.version} is already published from different source bytes`, + ); + process.exit(1); + } + } finally { + fs.rmSync(comparisonDirectory, { force: true, recursive: true }); + } +} + for (const pkg of ordered) { if (mode === MODE_INSPECT) { inspectPackage(pkg); diff --git a/scripts/release-readiness/core.mjs b/scripts/release-readiness/core.mjs new file mode 100644 index 0000000..210d44a --- /dev/null +++ b/scripts/release-readiness/core.mjs @@ -0,0 +1,2745 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +import { lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs"; +import { createHash } from "node:crypto"; +import { extname, isAbsolute, relative, resolve, sep } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; + +// This module is intentionally written as a standalone, vendorable release +// readiness core. Sister repositories should copy it byte-for-byte or consume a +// pinned upstream revision so release-critical checks do not drift silently. +export const RELEASE_READINESS_CORE_CONTRACT_VERSION = 8; + +const DEFAULT_FAILURE_PREFIX = "release readiness check failed"; + +const scrubProtoCommentsAndStrings = (source) => { + let output = ""; + let state = "normal"; + for (let index = 0; index < source.length; index += 1) { + const character = source[index]; + const next = source[index + 1]; + if (state === "normal") { + if (character === "/" && next === "/") { + output += " "; + index += 1; + state = "line-comment"; + } else if (character === "/" && next === "*") { + output += " "; + index += 1; + state = "block-comment"; + } else if (character === '"' || character === "'") { + output += " "; + state = character === '"' ? "double-quoted-string" : "single-quoted-string"; + } else { + output += character; + } + continue; + } + if (state === "line-comment") { + if (character === "\n") { + output += "\n"; + state = "normal"; + } else { + output += " "; + } + continue; + } + if (state === "block-comment") { + if (character === "*" && next === "/") { + output += " "; + index += 1; + state = "normal"; + } else { + output += character === "\n" ? "\n" : " "; + } + continue; + } + if (character === "\\" && next !== undefined) { + output += next === "\n" ? " \n" : " "; + index += 1; + } else if ( + (state === "double-quoted-string" && character === '"') || + (state === "single-quoted-string" && character === "'") + ) { + output += " "; + state = "normal"; + } else { + output += character === "\n" ? "\n" : " "; + } + } + return output; +}; + +const scrubJavaScriptCommentsAndStrings = (source) => { + let output = ""; + let state = "normal"; + for (let index = 0; index < source.length; index += 1) { + const character = source[index]; + const next = source[index + 1]; + if (state === "normal") { + if (character === "/" && next === "/") { + output += " "; + index += 1; + state = "line-comment"; + } else if (character === "/" && next === "*") { + output += " "; + index += 1; + state = "block-comment"; + } else if (character === '"' || character === "'" || character === "`") { + output += " "; + state = + character === '"' + ? "double-quoted-string" + : character === "'" + ? "single-quoted-string" + : "template-string"; + } else { + output += character; + } + continue; + } + if (state === "line-comment") { + if (character === "\n") { + output += "\n"; + state = "normal"; + } else { + output += " "; + } + continue; + } + if (state === "block-comment") { + if (character === "*" && next === "/") { + output += " "; + index += 1; + state = "normal"; + } else { + output += character === "\n" ? "\n" : " "; + } + continue; + } + if (character === "\\" && next !== undefined) { + output += next === "\n" ? " \n" : " "; + index += 1; + } else if ( + (state === "double-quoted-string" && character === '"') || + (state === "single-quoted-string" && character === "'") || + (state === "template-string" && character === "`") + ) { + output += " "; + state = "normal"; + } else { + output += character === "\n" ? "\n" : " "; + } + } + return output; +}; + +export function createReleaseReadinessContext(options) { + const { + scriptUrl, + repoRoot = "..", + requireTrackedFiles = false, + failurePrefix = DEFAULT_FAILURE_PREFIX, + } = options ?? {}; + + if (typeof scriptUrl !== "string" || scriptUrl.length === 0) { + console.error(`${failurePrefix}: scriptUrl is required`); + process.exit(1); + } + + let root; + try { + // Canonicalize the repository root so containment checks remain stable when + // the caller reached the worktree through an operating-system path alias + // such as /tmp versus /private/tmp. + root = realpathSync(resolve(fileURLToPath(new URL(repoRoot, scriptUrl)))); + } catch { + console.error(`${failurePrefix}: repository root is missing or inaccessible`); + process.exit(1); + } + let trackedFiles = null; + + const fail = (message) => { + console.error(`${failurePrefix}: ${message}`); + process.exit(1); + }; + + const resolveRepositoryPath = (path, description = "path") => { + if ( + typeof path !== "string" || + path.length === 0 || + path.includes("\0") || + isAbsolute(path) + ) { + fail(`${description} must be a non-empty repository-relative path`); + } + const absolute = resolve(root, path); + const repositoryRelative = relative(root, absolute); + if ( + repositoryRelative === ".." || + repositoryRelative.startsWith(`..${sep}`) || + isAbsolute(repositoryRelative) + ) { + fail(`${description} escapes the repository root`); + } + return absolute; + }; + + const assertCanonicalPathInsideRepository = (absolute, description) => { + let canonical; + try { + canonical = realpathSync(absolute); + } catch { + fail(`${description} is missing or inaccessible`); + } + const repositoryRelative = relative(root, canonical); + if ( + repositoryRelative === ".." || + repositoryRelative.startsWith(`..${sep}`) || + isAbsolute(repositoryRelative) + ) { + fail(`${description} resolves outside the repository root`); + } + return canonical; + }; + + const assertRegularFile = (path) => { + const absolute = resolveRepositoryPath(path); + let status; + try { + status = lstatSync(absolute); + } catch { + fail(`${path} is missing from the worktree`); + } + if (status.isSymbolicLink()) { + fail(`${path} must not be a symbolic link`); + } + if (!status.isFile()) { + fail(`${path} is not a regular file`); + } + return assertCanonicalPathInsideRepository(absolute, path); + }; + + const assertRepositoryDirectory = (path, description = path) => { + const absolute = resolveRepositoryPath(path, description); + let status; + try { + status = lstatSync(absolute); + } catch { + fail(`${description} is missing from the worktree`); + } + if (status.isSymbolicLink()) { + fail(`${description} must not be a symbolic link`); + } + if (!status.isDirectory()) { + fail(`${description} is not a directory`); + } + return assertCanonicalPathInsideRepository(absolute, description); + }; + + const run = (command, args, runOptions = {}) => { + if (typeof command !== "string" || command.length === 0) { + fail("release readiness command must be a non-empty string"); + } + if (!Array.isArray(args) || args.some((arg) => typeof arg !== "string")) { + fail(`${command} arguments must be an array of strings`); + } + const cwd = + runOptions.cwd === undefined + ? root + : assertRepositoryDirectory( + runOptions.cwd, + `${command} working directory`, + ); + const result = spawnSync(command, args, { + cwd, + encoding: "utf8", + stdio: runOptions.capture ? "pipe" : "inherit", + env: runOptions.env ?? process.env, + }); + if (result.error) { + fail(`${[command, ...args].join(" ")} failed to start: ${result.error.message}`); + } + if (result.status !== 0) { + if (runOptions.capture) { + process.stdout.write(result.stdout ?? ""); + process.stderr.write(result.stderr ?? ""); + } + process.exit(result.status ?? 1); + } + return result; + }; + + const loadTrackedFiles = () => { + if (trackedFiles !== null) { + return trackedFiles; + } + const result = spawnSync("git", ["ls-files", "-z"], { + cwd: root, + encoding: "utf8", + stdio: "pipe", + }); + if (result.error) { + fail(`git ls-files -z failed to start: ${result.error.message}`); + } + if (result.status !== 0) { + process.stdout.write(result.stdout ?? ""); + process.stderr.write(result.stderr ?? ""); + process.exit(result.status ?? 1); + } + trackedFiles = new Set(result.stdout.split("\0").filter(Boolean)); + return trackedFiles; + }; + + const requireTracked = (path) => { + resolveRepositoryPath(path); + if (!loadTrackedFiles().has(path)) { + fail(`${path} is not tracked by Git`); + } + }; + + if (requireTrackedFiles) { + const corePath = relative(root, fileURLToPath(import.meta.url)).replaceAll("\\", "/"); + requireTracked(corePath); + } + + const readText = (path) => { + if (requireTrackedFiles) { + requireTracked(path); + } + return readFileSync(assertRegularFile(path), "utf8"); + }; + + const readJson = (path) => { + try { + return JSON.parse(readText(path)); + } catch { + fail(`${path} is not valid JSON`); + } + }; + + const fingerprintFile = (path) => + createHash("sha256").update(readFileSync(assertRegularFile(path))).digest("hex"); + + const listFiles = (path) => { + resolveRepositoryPath(path); + const directory = assertRepositoryDirectory(path); + const prefix = `${path}/`; + if (requireTrackedFiles) { + const files = [...loadTrackedFiles()].filter((file) => file.startsWith(prefix)); + if (files.length === 0) { + fail(`${path} has no tracked files`); + } + return files; + } + + const files = []; + const visit = (current) => { + for (const entry of readdirSync(current).sort()) { + const absolute = resolve(current, entry); + const status = lstatSync(absolute); + if (status.isSymbolicLink()) { + fail(`${relative(root, absolute)} must not be a symbolic link`); + } + if (status.isDirectory()) { + visit(absolute); + } else if (status.isFile()) { + files.push(relative(root, absolute)); + } else { + fail(`${relative(root, absolute)} is not a regular file`); + } + } + }; + visit(directory); + return files; + }; + + const loadUntrackedFiles = () => { + const result = spawnSync( + "git", + ["ls-files", "--others", "--exclude-standard", "-z"], + { + cwd: root, + encoding: "utf8", + stdio: "pipe", + }, + ); + if (result.error) { + fail(`git ls-files --others --exclude-standard -z failed to start: ${result.error.message}`); + } + if (result.status !== 0) { + process.stdout.write(result.stdout ?? ""); + process.stderr.write(result.stderr ?? ""); + process.exit(result.status ?? 1); + } + return result.stdout.split("\0").filter(Boolean); + }; + + const assertContains = (path, needle) => { + if (!readText(path).includes(needle)) { + fail(`${path} does not contain ${needle}`); + } + }; + + const assertNotContains = (path, needle) => { + if (readText(path).includes(needle)) { + fail(`${path} must not contain ${needle}`); + } + }; + + const assertMinOccurrences = (path, needle, expectedMin) => { + const count = readText(path).split(needle).length - 1; + if (count < expectedMin) { + fail(`${path} contains ${needle} ${count} time(s), expected at least ${expectedMin}`); + } + }; + + const assertTextPolicy = (policy) => { + const files = policy?.files ?? []; + if (!Array.isArray(files) || files.length === 0) { + fail("text policy requires at least one file policy"); + } + + for (const filePolicy of files) { + const { + path, + required = [], + forbidden = [], + minimumOccurrences = [], + requiredMatches = [], + forbiddenMatches = [], + } = filePolicy ?? {}; + if (typeof path !== "string" || path.length === 0) { + fail("text file policy requires a path"); + } + for (const [policyName, needles] of [ + ["required", required], + ["forbidden", forbidden], + ]) { + if ( + !Array.isArray(needles) || + needles.some((needle) => typeof needle !== "string") + ) { + fail(`${path} ${policyName} text policy must be an array of strings`); + } + } + if (!Array.isArray(minimumOccurrences)) { + fail(`${path} minimum-occurrence policy must be an array`); + } + if (!Array.isArray(requiredMatches) || !Array.isArray(forbiddenMatches)) { + fail(`${path} regular-expression policies must be arrays`); + } + for (const needle of required) { + assertContains(path, needle); + } + for (const needle of forbidden) { + assertNotContains(path, needle); + } + for (const occurrence of minimumOccurrences) { + const { needle, count } = occurrence ?? {}; + if (typeof needle !== "string" || !Number.isSafeInteger(count) || count < 0) { + fail(`${path} has an invalid minimum-occurrence policy`); + } + assertMinOccurrences(path, needle, count); + } + for (const matchPolicy of requiredMatches) { + const { pattern, description } = matchPolicy ?? {}; + requireMatch(path, pattern, description); + } + for (const matchPolicy of forbiddenMatches) { + const { pattern, description } = matchPolicy ?? {}; + assertNotMatches(path, pattern, description); + } + } + }; + + const clonePattern = (pattern) => { + if (!(pattern instanceof RegExp)) { + fail("release readiness match assertions require a RegExp"); + } + return new RegExp(pattern.source, pattern.flags); + }; + + const requireMatch = (path, pattern, description) => { + // Clone caller-provided expressions so global or sticky regex state cannot + // make a repeated release check depend on an earlier invocation. + const match = clonePattern(pattern).exec(readText(path)); + if (match === null) { + fail(`${path} does not contain ${description}`); + } + return match; + }; + + const assertNotMatches = (path, pattern, description) => { + // Keep this assertion deterministic when a shared expression uses `g` or `y`. + if (clonePattern(pattern).test(readText(path))) { + fail(`${path} must not contain ${description}`); + } + }; + + const assertLockPackageVersion = (lock, name, version, source = null) => { + const blocks = lock.match(/\[\[package\]\]\n[\s\S]*?(?=\n\[\[package\]\]|\n*$)/g) ?? []; + const block = blocks.find( + (candidate) => + candidate.includes(`name = "${name}"\n`) && candidate.includes(`version = "${version}"\n`), + ); + if (block === undefined) { + fail(`Cargo.lock does not pin ${name} ${version}`); + } + if (source !== null && !block.includes(`source = "${source}"\n`)) { + fail(`Cargo.lock ${name} ${version} does not use ${source}`); + } + }; + + const runNodeCheck = (scriptPath, args = []) => { + const result = spawnSync(process.execPath, [scriptPath, ...args], { + cwd: root, + encoding: "utf8", + stdio: "pipe", + }); + if (result.error) { + fail(`${[scriptPath, ...args].join(" ")} failed to start: ${result.error.message}`); + } + if (result.status !== 0) { + const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim(); + const command = [scriptPath, ...args].join(" "); + fail(`${command} failed${output.length === 0 ? "" : `:\n${output}`}`); + } + }; + + const packageList = (packageName) => { + const args = ["package", "--list", "-p", packageName]; + if (process.env.GITHUB_ACTIONS !== "true") { + args.push("--allow-dirty"); + } + return run("cargo", args, { capture: true }).stdout.split(/\r?\n/u).filter(Boolean); + }; + + const assertPackageFiles = (packageName, requiredFiles) => { + const files = new Set(packageList(packageName)); + for (const file of requiredFiles) { + if (!files.has(file)) { + fail(`${packageName} package is missing ${file}`); + } + } + }; + + const runCommands = (commands) => { + if (!Array.isArray(commands) || commands.length === 0) { + fail("command policy requires at least one command"); + } + for (const entry of commands) { + if (!Array.isArray(entry) || entry.length < 2 || entry.length > 3) { + fail("command policy entries must be [command, args, options?] tuples"); + } + const [command, args, options] = entry; + if ( + options !== undefined && + (options === null || typeof options !== "object" || Array.isArray(options)) + ) { + fail("command policy options must be an object"); + } + run(command, args, options ?? {}); + } + }; + + const assertCargoMetadataDocument = (metadata, policy) => { + const packages = policy?.packages ?? []; + if ( + metadata === null || + typeof metadata !== "object" || + !Array.isArray(metadata.packages) + ) { + fail("cargo metadata did not return a packages array"); + } + if (!Array.isArray(packages) || packages.length === 0) { + fail("cargo metadata policy requires at least one package"); + } + + const packagesByName = new Map(); + for (const cargoPackage of metadata.packages) { + if ( + cargoPackage !== null && + typeof cargoPackage === "object" && + typeof cargoPackage.name === "string" + ) { + if (packagesByName.has(cargoPackage.name)) { + fail(`cargo metadata contains duplicate package ${cargoPackage.name}`); + } + packagesByName.set(cargoPackage.name, cargoPackage); + } + } + + for (const packagePolicy of packages) { + const { + name, + version, + publish = "any", + dependencies = [], + packageFiles = [], + } = packagePolicy ?? {}; + if (typeof name !== "string" || name.length === 0) { + fail("cargo metadata package policy requires a name"); + } + const cargoPackage = packagesByName.get(name); + if (cargoPackage === undefined) { + fail(`cargo metadata did not expose ${name}`); + } + if (version !== undefined && cargoPackage.version !== version) { + fail(`${name} metadata version is ${cargoPackage.version}, expected ${version}`); + } + const isPublishable = + cargoPackage.publish === null || + (Array.isArray(cargoPackage.publish) && cargoPackage.publish.length > 0); + if (publish === "public" && !isPublishable) { + fail(`${name} must be publishable`); + } + if (publish === "private" && isPublishable) { + fail(`${name} must set publish = false`); + } + if (!["any", "public", "private"].includes(publish)) { + fail(`${name} has an invalid publish policy`); + } + if (!Array.isArray(cargoPackage.dependencies)) { + fail(`${name} metadata dependencies are malformed`); + } + if (!Array.isArray(dependencies)) { + fail(`${name} dependency policy must be an array`); + } + if ( + !Array.isArray(packageFiles) || + packageFiles.some((file) => typeof file !== "string" || file.length === 0) + ) { + fail(`${name} package file policy must be an array of non-empty strings`); + } + + for (const dependencyPolicy of dependencies) { + const { + name: dependencyName, + requirement, + source = "any", + defaultFeatures, + optional, + features, + kind, + target, + rename, + } = dependencyPolicy ?? {}; + if (typeof dependencyName !== "string" || dependencyName.length === 0) { + fail(`${name} dependency policy requires a name`); + } + const candidates = cargoPackage.dependencies.filter( + (candidate) => + candidate.name === dependencyName && + (kind === undefined || candidate.kind === kind) && + (target === undefined || candidate.target === target) && + (rename === undefined || candidate.rename === rename), + ); + if (candidates.length === 0) { + fail(`${name} is missing ${dependencyName}`); + } + if (candidates.length > 1) { + fail( + `${name} dependency ${dependencyName} is ambiguous; specify kind, target, or rename`, + ); + } + const [dependency] = candidates; + if (requirement !== undefined && dependency.req !== requirement) { + fail( + `${name} dependency ${dependencyName} requirement is ${dependency.req}, expected ${requirement}`, + ); + } + if ( + source === "registry" && + (typeof dependency.source !== "string" || + !dependency.source.startsWith("registry+")) + ) { + fail(`${name} dependency ${dependencyName} must resolve from a registry`); + } + if (source === "path" && dependency.source !== null) { + fail(`${name} dependency ${dependencyName} must resolve from a path`); + } + if (!["any", "registry", "path"].includes(source)) { + fail(`${name} dependency ${dependencyName} has an invalid source policy`); + } + if ( + defaultFeatures !== undefined && + dependency.uses_default_features !== defaultFeatures + ) { + fail( + `${name} dependency ${dependencyName} default-features policy does not match`, + ); + } + if (optional !== undefined && dependency.optional !== optional) { + fail(`${name} dependency ${dependencyName} optional policy does not match`); + } + if (features !== undefined) { + if (!Array.isArray(features) || features.some((feature) => typeof feature !== "string")) { + fail(`${name} dependency ${dependencyName} features policy is invalid`); + } + const actualFeatures = new Set(dependency.features ?? []); + for (const feature of features) { + if (!actualFeatures.has(feature)) { + fail(`${name} dependency ${dependencyName} is missing feature ${feature}`); + } + } + } + } + + if (packageFiles.length > 0) { + assertPackageFiles(name, packageFiles); + } + } + + return packagesByName; + }; + + const assertCargoMetadataPolicy = (policy) => { + const metadataArgs = policy?.metadataArgs ?? [ + "metadata", + "--format-version", + "1", + "--no-deps", + ]; + const metadataResult = run("cargo", metadataArgs, { capture: true }); + let metadata; + try { + metadata = JSON.parse(metadataResult.stdout); + } catch { + fail("cargo metadata returned malformed JSON"); + } + return assertCargoMetadataDocument(metadata, policy); + }; + + const assertCargoWorkspacePolicy = (policy = {}) => { + const { + requireWorkspaceLints = true, + requirePublishInclude = true, + validatePublishablePathDependencies = true, + } = policy; + const metadataResult = run( + "cargo", + ["metadata", "--format-version", "1", "--no-deps"], + { capture: true }, + ); + let metadata; + try { + metadata = JSON.parse(metadataResult.stdout); + } catch { + fail("cargo metadata returned malformed JSON"); + } + if ( + !Array.isArray(metadata.packages) || + !Array.isArray(metadata.workspace_members) + ) { + fail("cargo workspace metadata is malformed"); + } + + const workspaceIds = new Set(metadata.workspace_members); + const workspacePackages = metadata.packages.filter((cargoPackage) => + workspaceIds.has(cargoPackage.id), + ); + const publishableByName = new Map(); + const parseSemver = (version) => { + const match = /^(\d+)\.(\d+)\.(\d+)$/u.exec(version); + if (match === null) { + return null; + } + return match.slice(1).map((part) => Number.parseInt(part, 10)); + }; + const caretIncludes = (requirement, version) => { + if (!requirement.startsWith("^")) { + return requirement === version || requirement === `=${version}`; + } + const minimum = parseSemver(requirement.slice(1)); + const actual = parseSemver(version); + if (minimum === null || actual === null || actual[0] !== minimum[0]) { + return false; + } + if (minimum[0] === 0 && actual[1] !== minimum[1]) { + return false; + } + return ( + actual[1] > minimum[1] || + (actual[1] === minimum[1] && actual[2] >= minimum[2]) + ); + }; + for (const cargoPackage of workspacePackages) { + const manifestPath = relative(root, cargoPackage.manifest_path).replaceAll("\\", "/"); + const manifest = readText(manifestPath); + if ( + requireWorkspaceLints && + !/\[lints\]\s+workspace\s*=\s*true\b/u.test(manifest) + ) { + fail(`${manifestPath} must inherit workspace lints`); + } + const publishable = + cargoPackage.publish === null || + (Array.isArray(cargoPackage.publish) && cargoPackage.publish.length > 0); + if (publishable) { + publishableByName.set(cargoPackage.name, cargoPackage); + if ( + requirePublishInclude && + !/^include\s*=\s*\[/mu.test(manifest) + ) { + fail(`${manifestPath} publishable package must use an include allowlist`); + } + } + } + + if (validatePublishablePathDependencies) { + for (const cargoPackage of publishableByName.values()) { + for (const dependency of cargoPackage.dependencies ?? []) { + if (dependency.source !== null || typeof dependency.path !== "string") { + continue; + } + const dependencyName = dependency.name; + const target = publishableByName.get(dependencyName); + if (target === undefined) { + continue; + } + if (!caretIncludes(dependency.req, target.version)) { + fail( + `${cargoPackage.name} publishable path dependency ${dependencyName} ${dependency.req} does not match ${target.version}`, + ); + } + } + } + } + }; + + const assertSpdxHeaders = (policy = {}) => { + const { + extensions = [".md", ".mjs", ".proto", ".py", ".rs", ".sh", ".toml", ".yaml", ".yml"], + names = [".gitignore"], + excludedPrefixes = [], + copyright = + "SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved", + license = "SPDX-License-Identifier: Apache-2.0", + } = policy; + for (const [policyName, values] of [ + ["extensions", extensions], + ["names", names], + ["excluded prefixes", excludedPrefixes], + ]) { + if (!Array.isArray(values) || values.some((value) => typeof value !== "string")) { + fail(`SPDX ${policyName} policy must be an array of strings`); + } + } + const extensionSet = new Set(extensions); + const nameSet = new Set(names); + for (const path of loadTrackedFiles()) { + if (excludedPrefixes.some((prefix) => pathIsInside(path, prefix))) { + continue; + } + const fileName = path.slice(path.lastIndexOf("/") + 1); + if (!nameSet.has(fileName) && !extensionSet.has(extname(fileName))) { + continue; + } + const text = readText(path); + if (!text.includes(copyright)) { + fail(`${path} is missing the ReallyMe SPDX copyright header`); + } + if (!text.includes(license)) { + fail(`${path} is missing the Apache-2.0 SPDX license header`); + } + } + }; + + const snapshotDirectory = (path) => { + const directory = assertRepositoryDirectory(path); + const snapshot = new Map(); + const visit = (current) => { + let entries; + try { + entries = readdirSync(current).sort(); + } catch { + fail(`${relative(root, current)} is missing or inaccessible`); + } + for (const entry of entries) { + const absolute = resolve(current, entry); + const status = lstatSync(absolute); + if (status.isSymbolicLink()) { + fail(`${relative(root, absolute)} must not be a symbolic link`); + } + if (status.isDirectory()) { + visit(absolute); + } else if (status.isFile()) { + const file = relative(directory, absolute); + snapshot.set(file, fingerprintFile(`${path}/${file}`)); + } else { + fail(`${relative(root, absolute)} is not a regular file`); + } + } + }; + visit(directory); + + if (requireTrackedFiles) { + const prefix = `${path}/`; + const tracked = new Set( + listFiles(path).map((file) => file.slice(prefix.length)), + ); + for (const file of snapshot.keys()) { + if (!tracked.has(file)) { + fail(`${path}/${file} is not tracked by Git`); + } + } + for (const file of tracked) { + if (!snapshot.has(file)) { + fail(`${path}/${file} is tracked by Git but missing from the worktree`); + } + } + } + + return snapshot; + }; + + const assertSnapshotsEqual = (path, before, after) => { + if (before.size !== after.size) { + fail(`${path} changed file count after regeneration`); + } + + for (const [file, contents] of before) { + const regenerated = after.get(file); + if (regenerated === undefined || contents !== regenerated) { + fail(`${path}/${file} is stale; run the protobuf generation and hardening steps`); + } + } + }; + + const pathIsInside = (path, directory) => + path === directory || path.startsWith(`${directory}/`); + + const snapshotRepositoryFilesOutside = (excludedPaths) => { + const excluded = excludedPaths.map((path) => { + resolveRepositoryPath(path); + return path.replace(/\/+$/u, ""); + }); + const files = new Set([...loadTrackedFiles(), ...loadUntrackedFiles()]); + const snapshot = new Map(); + for (const file of files) { + if (excluded.some((path) => pathIsInside(file, path))) { + continue; + } + snapshot.set(file, fingerprintFile(file)); + } + return snapshot; + }; + + const assertRepositorySnapshotsEqual = (before, after) => { + if (before.size !== after.size) { + fail("protobuf regeneration changed files outside the declared generated paths"); + } + for (const [file, contents] of before) { + const regenerated = after.get(file); + if (regenerated === undefined || contents !== regenerated) { + fail(`protobuf regeneration modified ${file} outside the declared generated paths`); + } + } + }; + + const validateGeneratedArtifactsPolicy = (regeneration) => { + const generatedPaths = regeneration?.generatedPaths ?? []; + const commands = regeneration?.commands ?? []; + if (!Array.isArray(generatedPaths) || generatedPaths.length === 0) { + fail("generated artifact freshness check requires at least one generated path"); + } + if (generatedPaths.some((path) => typeof path !== "string" || path.length === 0)) { + fail("generated artifact paths must be non-empty strings"); + } + if (!Array.isArray(commands) || commands.length === 0) { + fail("generated artifact freshness check requires at least one regeneration command"); + } + const normalizedPaths = generatedPaths.map((path) => + relative(root, assertRepositoryDirectory(path, "generated artifact path")).replaceAll( + "\\", + "/", + ), + ); + if (normalizedPaths.some((path) => path.length === 0 || path === ".")) { + fail("generated artifact paths must not include the repository root"); + } + for (const [index, path] of normalizedPaths.entries()) { + if ( + normalizedPaths.some( + (candidate, candidateIndex) => + candidateIndex !== index && pathIsInside(path, candidate), + ) + ) { + fail("generated artifact paths must not overlap"); + } + } + for (const entry of commands) { + if (!Array.isArray(entry) || entry.length < 2 || entry.length > 3) { + fail("regeneration commands must be [command, args, options?] tuples"); + } + const [command, args] = entry; + if ( + typeof command !== "string" || + command.length === 0 || + !Array.isArray(args) || + args.some((arg) => typeof arg !== "string") + ) { + fail("regeneration commands require a command and string arguments"); + } + const options = entry[2]; + if ( + options !== undefined && + (options === null || typeof options !== "object" || Array.isArray(options)) + ) { + fail("regeneration command options must be an object"); + } + } + return { generatedPaths: normalizedPaths, commands }; + }; + + const assertGeneratedArtifactsFresh = (regeneration) => { + const { generatedPaths, commands } = validateGeneratedArtifactsPolicy(regeneration); + + const snapshotsBefore = new Map( + generatedPaths.map((path) => [path, snapshotDirectory(path)]), + ); + const repositoryBefore = requireTrackedFiles + ? snapshotRepositoryFilesOutside(generatedPaths) + : null; + runCommands(commands); + for (const path of generatedPaths) { + assertSnapshotsEqual(path, snapshotsBefore.get(path), snapshotDirectory(path)); + } + if (repositoryBefore !== null) { + assertRepositorySnapshotsEqual( + repositoryBefore, + snapshotRepositoryFilesOutside(generatedPaths), + ); + } + }; + + const assertGeneratedProtoHardeningPolicy = (policy) => { + const { + hardeningScript, + protoSchema, + generatedRust, + generatedView, + protoCargo, + workflow, + workflowStepName, + workflowStepRun, + requiredScriptNeedles = [], + forbiddenScriptNeedles = [], + requiredGeneratedNeedles = [], + forbiddenGeneratedNeedles = [], + requiredViewNeedles = [], + requiredCargoNeedles = [], + scalarFieldClassifications = [], + additionalGeneratedPolicies = [], + requireIdempotence = true, + requireStrictJson = true, + requireUnknownFieldZeroization = true, + } = policy ?? {}; + + if (typeof hardeningScript !== "string" || hardeningScript.length === 0) { + fail("generated proto hardening policy requires a hardeningScript path"); + } + if (typeof generatedRust !== "string" || generatedRust.length === 0) { + fail("generated proto hardening policy requires a generatedRust path"); + } + if (typeof protoSchema !== "string" || protoSchema.length === 0) { + fail("generated proto hardening policy requires a protoSchema path"); + } + for (const [policyName, needles] of [ + ["required script", requiredScriptNeedles], + ["forbidden script", forbiddenScriptNeedles], + ["required generated", requiredGeneratedNeedles], + ["forbidden generated", forbiddenGeneratedNeedles], + ["required view", requiredViewNeedles], + ["required Cargo", requiredCargoNeedles], + ]) { + if ( + !Array.isArray(needles) || + needles.some((needle) => typeof needle !== "string") + ) { + fail(`generated proto ${policyName} policy must be an array of strings`); + } + } + if (requiredScriptNeedles.length === 0) { + fail("generated proto hardening policy requires script invariants"); + } + if (typeof requireIdempotence !== "boolean") { + fail("generated proto hardening idempotence policy must be a boolean"); + } + if (requiredGeneratedNeedles.length === 0) { + fail("generated proto hardening policy requires generated-code invariants"); + } + if (forbiddenGeneratedNeedles.length === 0) { + fail("generated proto hardening policy requires forbidden generated-code invariants"); + } + if (!Array.isArray(scalarFieldClassifications)) { + fail("generated proto scalar field classifications must be an array"); + } + if (scalarFieldClassifications.length === 0) { + fail("generated proto hardening policy requires scalar field classifications"); + } + const normalizedScalarFieldClassifications = scalarFieldClassifications.map((entry) => { + if ( + entry !== null && + typeof entry === "object" && + !Array.isArray(entry) && + typeof entry.message === "string" && + /^[A-Za-z_][A-Za-z0-9_]*$/u.test(entry.message) && + typeof entry.field === "string" && + /^[A-Za-z_][A-Za-z0-9_]*$/u.test(entry.field) && + (entry.kind === "bytes" || entry.kind === "string") && + (entry.sensitivity === "sensitive" || entry.sensitivity === "public") && + Object.keys(entry).every((key) => + ["message", "field", "kind", "sensitivity"].includes(key), + ) + ) { + return { + field: entry.field, + kind: entry.kind, + message: entry.message, + sensitivity: entry.sensitivity, + }; + } + fail( + "generated proto scalar classifications require { message, field, kind, sensitivity } objects", + ); + }); + const classificationKeys = new Set(); + for (const entry of normalizedScalarFieldClassifications) { + const key = `${entry.message}.${entry.field}:${entry.kind}`; + if (classificationKeys.has(key)) { + fail(`generated proto scalar classification is duplicated for ${key}`); + } + classificationKeys.add(key); + } + const protoText = scrubProtoCommentsAndStrings(readText(protoSchema)); + const scalarSchemaKeys = new Set(); + const messagePattern = /^\s*message\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/gmu; + for (const messageMatch of protoText.matchAll(messagePattern)) { + const openIndex = messageMatch.index + messageMatch[0].lastIndexOf("{"); + let depth = 0; + let closeIndex = -1; + for (let index = openIndex; index < protoText.length; index += 1) { + if (protoText[index] === "{") { + depth += 1; + } else if (protoText[index] === "}") { + depth -= 1; + if (depth === 0) { + closeIndex = index; + break; + } + } + } + if (closeIndex === -1) { + fail(`${protoSchema} has an unterminated message ${messageMatch[1]}`); + } + const body = protoText.slice(openIndex + 1, closeIndex); + if (/^\s+message\s+[A-Za-z_][A-Za-z0-9_]*\s*\{/mu.test(body)) { + fail( + `${protoSchema} nested messages require an explicit scalar-classifier extension`, + ); + } + if (/\bmap\s*<[^>]*(?:bytes|string)[^>]*>/u.test(body)) { + fail( + `${protoSchema} maps with bytes/string members require an explicit scalar-classifier extension`, + ); + } + for (const fieldMatch of body.matchAll( + /(?:^|[;{}])\s*(?:(?:optional|required|repeated)\s+)?(bytes|string)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*\d+(?:\s*\[[^\]]*\])?\s*(?=;)/gmu, + )) { + scalarSchemaKeys.add(`${messageMatch[1]}.${fieldMatch[2]}:${fieldMatch[1]}`); + } + } + for (const key of scalarSchemaKeys) { + if (!classificationKeys.has(key)) { + fail(`${protoSchema} has unclassified protobuf scalar field ${key}`); + } + } + for (const key of classificationKeys) { + if (!scalarSchemaKeys.has(key)) { + fail(`generated proto scalar classification is stale for ${key}`); + } + } + const sensitiveScalarFields = normalizedScalarFieldClassifications.filter( + (entry) => entry.sensitivity === "sensitive", + ); + if (sensitiveScalarFields.length === 0) { + fail("generated proto hardening policy requires at least one sensitive scalar field"); + } + if (!Array.isArray(additionalGeneratedPolicies)) { + fail("additional generated hardening policies must be an array"); + } + for (const [policyName, value] of [ + ["generated view", generatedView], + ["proto Cargo", protoCargo], + ["workflow", workflow], + ["workflow step name", workflowStepName], + ["workflow step run", workflowStepRun], + ]) { + if (value !== undefined && typeof value !== "string") { + fail(`generated proto ${policyName} policy must be a string`); + } + } + if ( + typeof generatedView === "string" && + generatedView.length !== 0 && + requiredViewNeedles.length === 0 + ) { + fail("generated proto hardening policy requires generated view invariants"); + } + if ( + typeof protoCargo === "string" && + protoCargo.length !== 0 && + requiredCargoNeedles.length === 0 + ) { + fail("generated proto hardening policy requires proto Cargo invariants"); + } + const workflowValues = [workflow, workflowStepName, workflowStepRun]; + const configuredWorkflowValues = workflowValues.filter( + (value) => typeof value === "string" && value.length !== 0, + ); + if ( + configuredWorkflowValues.length !== 0 && + configuredWorkflowValues.length !== workflowValues.length + ) { + fail("generated proto workflow policy must configure path, step name, and run command"); + } + + for (const needle of requiredScriptNeedles) { + assertContains(hardeningScript, needle); + } + if (requireIdempotence) { + assertContains(hardeningScript, '"--check-idempotent"'); + } + for (const needle of forbiddenScriptNeedles) { + assertNotContains(hardeningScript, needle); + } + const generatedRustText = readText(generatedRust); + const messageRegion = (message) => { + const startNeedle = `pub struct ${message} {`; + const start = generatedRustText.indexOf(startNeedle); + if (start === -1) { + fail(`${generatedRust} does not define generated message ${message}`); + } + const remainder = generatedRustText.slice(start + startNeedle.length); + const nextMessage = /^pub struct [A-Z][A-Za-z0-9]* \{/gmu.exec(remainder); + const end = + nextMessage === null + ? generatedRustText.length + : start + startNeedle.length + nextMessage.index; + return generatedRustText.slice(start, end); + }; + for (const { field, kind, message } of sensitiveScalarFields) { + const scope = messageRegion(message); + for (const needle of [ + `.field("${field}", &"")`, + `::zeroize::Zeroize::zeroize(&mut self.${field});`, + kind === "bytes" + ? `${field}: ::zeroize::Zeroizing<::buffa::alloc::vec::Vec>` + : `${field}: ::zeroize::Zeroizing<::buffa::alloc::string::String>`, + ]) { + if (!scope.includes(needle)) { + fail(`${generatedRust} message ${message} does not contain ${needle}`); + } + } + if (scope.includes(`.field("${field}", &self.${field})`)) { + fail(`${generatedRust} message ${message} must not expose ${field} in generated Debug output`); + } + // Buffa's generated message storage remains Vec. Sensitive + // ProtoJSON decoding must stage bytes in a zeroizing temporary, while + // generated clear and Drop paths wipe the final generated field owner. + } + if (requireStrictJson) { + assertContains(generatedRust, "#[serde(default, deny_unknown_fields)]"); + } + if (requireUnknownFieldZeroization) { + for (const needle of [ + "::buffa::UnknownFieldData::LengthDelimited(bytes)", + "::buffa::UnknownFieldData::Group(fields)", + "__reallyme_zeroize_unknown_fields(fields);", + ]) { + assertContains(hardeningScript, needle); + } + assertContains(generatedRust, "fn __reallyme_zeroize_unknown_fields("); + assertContains( + generatedRust, + "::buffa::UnknownFieldData::LengthDelimited(bytes)", + ); + assertContains( + generatedRust, + "::buffa::UnknownFieldData::Group(fields)", + ); + assertContains( + generatedRust, + "__reallyme_zeroize_unknown_fields(fields);", + ); + assertContains( + generatedRust, + "__reallyme_zeroize_unknown_fields(&mut self.__buffa_unknown_fields);", + ); + } + for (const needle of requiredGeneratedNeedles) { + assertContains(generatedRust, needle); + } + for (const needle of forbiddenGeneratedNeedles) { + assertNotContains(generatedRust, needle); + } + if (typeof generatedView === "string" && generatedView.length !== 0) { + for (const needle of requiredViewNeedles) { + assertContains(generatedView, needle); + } + } + if (typeof protoCargo === "string" && protoCargo.length !== 0) { + for (const needle of requiredCargoNeedles) { + assertContains(protoCargo, needle); + } + } + for (const generatedPolicy of additionalGeneratedPolicies) { + assertTextPolicy({ files: [generatedPolicy] }); + } + if ( + typeof workflow === "string" && + workflow.length !== 0 && + typeof workflowStepName === "string" && + workflowStepName.length !== 0 && + typeof workflowStepRun === "string" && + workflowStepRun.length !== 0 + ) { + assertWorkflowRunStep(workflow, workflowStepName, workflowStepRun); + } + }; + + const assertReallyMeProtobufReleasePolicy = (policy) => { + const { + workflow = ".github/workflows/protobuf-ci.yml", + corePath = "scripts/release-readiness/core.mjs", + bufVersion = "1.71.0", + buffaVersion = "0.8.1", + installBufStepName = "Install buf", + installBufUses = null, + installBufRun = null, + installBuffaStepName = "Install pinned Buffa generators", + lintStepName = "Lint protobuf schema", + generateStepName = "Regenerate protobuf artifacts", + hardeningPolicy, + generatedFreshnessMode = false, + generatedFreshness, + generatedFreshnessStepName = "Check release readiness generated freshness", + generatedFreshnessStepRun = "node scripts/check_release_readiness.mjs --generated-freshness", + workflowMode = "explicit", + } = policy ?? {}; + + assertContains(workflow, `BUFFA_VERSION: ${buffaVersion}`); + assertContains(workflow, `BUF_VERSION: ${bufVersion}`); + assertContains(workflow, corePath); + validateGeneratedArtifactsPolicy(generatedFreshness); + + if (installBufUses !== null) { + assertWorkflowUsesStep(workflow, installBufStepName, installBufUses); + } + if (installBufRun !== null) { + assertWorkflowRunStep(workflow, installBufStepName, installBufRun); + } + assertWorkflowRunStep( + workflow, + installBuffaStepName, + `cargo install protoc-gen-buffa --version "$BUFFA_VERSION" --locked +cargo install protoc-gen-buffa-packaging --version "$BUFFA_VERSION" --locked`, + ); + assertWorkflowRunStep(workflow, generatedFreshnessStepName, generatedFreshnessStepRun); + + if (workflowMode === "explicit") { + assertWorkflowRunStep(workflow, lintStepName, "buf lint"); + assertWorkflowRunStep(workflow, generateStepName, "buf generate"); + } else if (workflowMode === "delegated") { + const duplicateCommands = extractWorkflowSteps(workflow).filter( + (step) => + step.name !== generatedFreshnessStepName && + typeof step.run === "string" && + /(?:^|\n)\s*buf\s+(?:lint|generate)\b/u.test(step.run), + ); + if (duplicateCommands.length > 0) { + fail( + `${workflow} duplicates protobuf generation outside ${generatedFreshnessStepName}`, + ); + } + } else { + fail(`unsupported protobuf workflow mode ${workflowMode}`); + } + + assertGeneratedProtoHardeningPolicy(hardeningPolicy); + + if (generatedFreshnessMode) { + assertGeneratedArtifactsFresh(generatedFreshness); + } + }; + + const assertReallyMeVendoredCorePolicy = (policy = {}) => { + const { + scriptPath = "scripts/check_release_readiness.mjs", + corePath = "scripts/release-readiness/core.mjs", + contractVersion = RELEASE_READINESS_CORE_CONTRACT_VERSION, + } = policy; + + requireTracked(scriptPath); + requireTracked(corePath); + const executableCore = scrubJavaScriptCommentsAndStrings(readText(corePath)); + const requireCoreDeclaration = (name) => { + const declaration = new RegExp( + `\\b(?:const|function)\\s+${escapeRegExp(name)}\\b`, + "u", + ); + if (!declaration.test(executableCore)) { + fail(`${corePath} must define ${name}`); + } + }; + const requireCoreExport = (name) => { + const exportPattern = new RegExp(`\\b${escapeRegExp(name)}\\s*,`, "u"); + if (!exportPattern.test(executableCore)) { + fail(`${corePath} must export ${name}`); + } + }; + + assertContains(corePath, `RELEASE_READINESS_CORE_CONTRACT_VERSION = ${contractVersion}`); + for (const name of [ + "assertGeneratedArtifactsFresh", + "assertGeneratedProtoHardeningPolicy", + "assertReallyMeProtobufReleasePolicy", + "assertReallyMeVendoredCorePolicy", + "assertReallyMeRustProtoRepositoryPolicy", + "assertCargoMetadataPolicy", + "assertCargoWorkspacePolicy", + "assertTextPolicy", + "assertSpdxHeaders", + "assertWorkflowActionsPinned", + "assertWorkflowPolicy", + "runCommands", + "assertProtoContract", + "assertReallyMeProtoBoundaryContract", + "assertReallyMeOperationBoundaryContract", + "assertWorkflowRunStep", + "assertWorkflowUsesStep", + ]) { + requireCoreDeclaration(name); + requireCoreExport(name); + } + for (const identifier of [ + "scalarFieldClassifications", + "messagePattern", + "scalarSchemaKeys", + ]) { + if (!new RegExp(`\\b${escapeRegExp(identifier)}\\b`, "u").test(executableCore)) { + fail(`${corePath} must enforce ${identifier}`); + } + } + }; + + const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); + + const assertNodeWorkflowJobsPinNode = (workflowOptions = {}) => { + const workflowDirectoryPath = workflowOptions.workflowDirectory ?? ".github/workflows"; + const workflowDirectory = assertRepositoryDirectory( + workflowDirectoryPath, + "workflow directory", + ); + const nodeVersion = workflowOptions.nodeVersion ?? "24"; + const nodeToolCommands = workflowOptions.nodeToolCommands ?? [ + "node", + "npm", + "npx", + "pnpm", + "yarn", + "corepack", + "bun", + ]; + if ( + !Array.isArray(nodeToolCommands) || + nodeToolCommands.some( + (command) => typeof command !== "string" || !/^[A-Za-z0-9_-]+$/u.test(command), + ) + ) { + fail("Node workflow tool policy must be an array of command names"); + } + const nodeToolPattern = new RegExp( + `\\b(?:${nodeToolCommands.map(escapeRegExp).join("|")})\\b`, + "u", + ); + for (const workflowFile of readdirSync(workflowDirectory).filter((name) => /\.ya?ml$/u.test(name))) { + const workflowPath = `${workflowDirectoryPath}/${workflowFile}`; + const workflow = readText(workflowPath); + const jobsOffset = workflow.indexOf("\njobs:\n"); + if (jobsOffset === -1) { + continue; + } + const jobs = workflow.slice(jobsOffset + 1); + const jobHeaders = [...jobs.matchAll(/^ ([a-zA-Z0-9_-]+):\s*$/gm)]; + for (const [index, header] of jobHeaders.entries()) { + const nextHeader = jobHeaders[index + 1]; + const job = jobs.slice(header.index, nextHeader?.index ?? jobs.length); + const activeJob = job + .split("\n") + .filter((line) => !line.trimStart().startsWith("#")) + .join("\n"); + if (!nodeToolPattern.test(activeJob)) { + continue; + } + if (!/^\s*uses:\s*actions\/setup-node@[^\s#]+(?:\s+#.*)?$/m.test(activeJob)) { + fail(`${workflowPath} job ${header[1]} uses Node tooling without actions/setup-node`); + } + const pinnedNodeVersion = activeJob.split("\n").some((line) => { + const match = /^\s*node-version:\s*(.+?)\s*$/u.exec(line); + return match !== null && unquoteWorkflowScalar(match[1]) === nodeVersion; + }); + if (!pinnedNodeVersion) { + fail(`${workflowPath} job ${header[1]} must pin Node ${nodeVersion}`); + } + } + } + }; + + const normalizeWorkflowRunCommand = (command) => + command + .replace(/\r\n/gu, "\n") + .split("\n") + .map((line) => line.trimEnd()) + .join("\n") + .trim(); + + const stripWorkflowInlineComment = (value) => { + let quote = null; + for (let index = 0; index < value.length; index += 1) { + const character = value[index]; + if (quote !== null) { + if (character === quote && value[index - 1] !== "\\") { + quote = null; + } + continue; + } + if (character === '"' || character === "'") { + quote = character; + continue; + } + if (character === "#" && (index === 0 || /\s/u.test(value[index - 1]))) { + return value.slice(0, index); + } + } + return value; + }; + + const unquoteWorkflowScalar = (value) => { + const trimmed = stripWorkflowInlineComment(value).trim(); + if ( + (trimmed.startsWith('"') && trimmed.endsWith('"')) || + (trimmed.startsWith("'") && trimmed.endsWith("'")) + ) { + return trimmed.slice(1, -1); + } + return trimmed; + }; + + const countLeadingSpaces = (line) => { + const match = /^ */u.exec(line); + return match?.[0].length ?? 0; + }; + + const extractWorkflowStepsFromLines = (path, lines, start, end, jobName = null) => { + const steps = []; + for (let index = start; index < end; index += 1) { + const nameMatch = /^(\s*)-\s+name:\s*(.+?)\s*$/u.exec(lines[index]); + if (nameMatch === null) { + continue; + } + + const stepIndent = nameMatch[1].length; + const name = unquoteWorkflowScalar(nameMatch[2]); + let end = lines.length; + for (let cursor = index + 1; cursor < lines.length; cursor += 1) { + const candidate = lines[cursor]; + if (countLeadingSpaces(candidate) === stepIndent && /^\s*-\s+/u.test(candidate)) { + end = cursor; + break; + } + } + + let run = null; + let uses = null; + for (let cursor = index + 1; cursor < end; cursor += 1) { + const runMatch = /^(\s*)run:\s*(.*)\s*$/u.exec(lines[cursor]); + if (runMatch !== null) { + if (run !== null) { + fail(`${path} step ${name} defines run more than once`); + } + const runIndent = runMatch[1].length; + const marker = runMatch[2].trim(); + if (marker === ">") { + fail(`${path} step ${name} uses an unsupported folded run scalar`); + } + if (marker === "|") { + const blockLines = []; + for (let blockCursor = cursor + 1; blockCursor < end; blockCursor += 1) { + const blockLine = lines[blockCursor]; + if (blockLine.trim().length !== 0 && countLeadingSpaces(blockLine) <= runIndent) { + break; + } + blockLines.push(blockLine); + } + const nonBlankIndents = blockLines + .filter((line) => line.trim().length !== 0) + .map((line) => countLeadingSpaces(line)); + const blockIndent = + nonBlankIndents.length === 0 ? runIndent + 2 : Math.min(...nonBlankIndents); + run = blockLines.map((line) => line.slice(Math.min(blockIndent, line.length))).join("\n"); + } else { + run = unquoteWorkflowScalar(marker); + } + } + + const usesMatch = /^\s*uses:\s*(.+?)\s*$/u.exec(lines[cursor]); + if (usesMatch !== null) { + if (uses !== null) { + fail(`${path} step ${name} defines uses more than once`); + } + uses = unquoteWorkflowScalar(usesMatch[1]); + } + } + + steps.push({ job: jobName, name, run, uses }); + } + return steps; + }; + + const extractWorkflowJobs = (path) => { + const lines = readText(path).replace(/\r\n/gu, "\n").split("\n"); + const jobsLine = lines.findIndex((line) => /^jobs:\s*$/u.test(line)); + if (jobsLine === -1) { + return []; + } + const headers = []; + for (let index = jobsLine + 1; index < lines.length; index += 1) { + const match = /^ ([A-Za-z0-9_-]+):\s*$/u.exec(lines[index]); + if (match !== null) { + headers.push({ name: match[1], index }); + } + } + return headers.map((header, index) => ({ + name: header.name, + start: header.index, + end: headers[index + 1]?.index ?? lines.length, + lines, + })); + }; + + const parseWorkflowPermissionBlock = (path, lines, headerIndex, headerIndent, label) => { + const permissions = new Map(); + for (let index = headerIndex + 1; index < lines.length; index += 1) { + const line = lines[index]; + const trimmed = line.trim(); + if (trimmed.length === 0 || trimmed.startsWith("#")) { + continue; + } + const indent = countLeadingSpaces(line); + if (indent <= headerIndent) { + break; + } + const match = /^\s*([A-Za-z0-9_-]+):\s*(read|write|none)\s*(?:#.*)?$/u.exec(line); + if (indent !== headerIndent + 2 || match === null) { + fail(`${path} ${label} permissions must be a flat explicit mapping`); + } + if (permissions.has(match[1])) { + fail(`${path} ${label} permission ${match[1]} is defined more than once`); + } + permissions.set(match[1], match[2]); + } + if (permissions.size === 0) { + fail(`${path} ${label} permissions mapping must not be empty`); + } + return permissions; + }; + + const validateExpectedPermissions = (path, label, value) => { + if ( + value === null || + typeof value !== "object" || + Array.isArray(value) || + Object.entries(value).some( + ([scope, access]) => + !/^[A-Za-z0-9_-]+$/u.test(scope) || + !["read", "write", "none"].includes(access), + ) + ) { + fail(`${path} ${label} expected permissions must be an explicit mapping`); + } + return new Map(Object.entries(value)); + }; + + const assertPermissionMapsEqual = (path, label, actual, expected) => { + if ( + actual.size !== expected.size || + [...expected].some(([scope, access]) => actual.get(scope) !== access) + ) { + fail(`${path} ${label} permissions changed`); + } + }; + + const assertWorkflowPermissionsPolicy = (policy) => { + const { path, workflow, jobs = {} } = policy ?? {}; + if (typeof path !== "string" || path.length === 0) { + fail("workflow permissions policy requires a path"); + } + const expectedWorkflow = validateExpectedPermissions(path, "workflow", workflow); + if (jobs === null || typeof jobs !== "object" || Array.isArray(jobs)) { + fail(`${path} expected job permissions must be an explicit mapping`); + } + + const lines = readText(path).replace(/\r\n/gu, "\n").split("\n"); + const workflowHeaders = lines + .map((line, index) => ({ index, matches: /^permissions:\s*$/u.test(line) })) + .filter((entry) => entry.matches); + if (workflowHeaders.length !== 1) { + fail(`${path} must define exactly one top-level permissions mapping`); + } + const actualWorkflow = parseWorkflowPermissionBlock( + path, + lines, + workflowHeaders[0].index, + 0, + "workflow", + ); + assertPermissionMapsEqual(path, "workflow", actualWorkflow, expectedWorkflow); + + const actualJobs = new Map(); + for (const job of extractWorkflowJobs(path)) { + const headers = []; + for (let index = job.start + 1; index < job.end; index += 1) { + if (/^ {4}permissions:\s+\S/u.test(lines[index])) { + fail(`${path} job ${job.name} permissions must be a flat explicit mapping`); + } + if (/^ {4}permissions:\s*$/u.test(lines[index])) { + headers.push(index); + } + } + if (headers.length > 1) { + fail(`${path} job ${job.name} defines permissions more than once`); + } + if (headers.length === 1) { + actualJobs.set( + job.name, + parseWorkflowPermissionBlock(path, lines, headers[0], 4, `job ${job.name}`), + ); + } + } + + const expectedJobs = new Map(); + for (const [jobName, permissions] of Object.entries(jobs)) { + if (!/^[A-Za-z0-9_-]+$/u.test(jobName)) { + fail(`${path} expected job permission name is invalid`); + } + expectedJobs.set( + jobName, + validateExpectedPermissions(path, `job ${jobName}`, permissions), + ); + } + if ( + actualJobs.size !== expectedJobs.size || + [...actualJobs.keys()].some((jobName) => !expectedJobs.has(jobName)) + ) { + fail(`${path} jobs with explicit permissions changed`); + } + for (const [jobName, expected] of expectedJobs) { + const actual = actualJobs.get(jobName); + if (actual === undefined) { + fail(`${path} job ${jobName} is missing explicit permissions`); + } + assertPermissionMapsEqual(path, `job ${jobName}`, actual, expected); + } + }; + + const extractWorkflowSteps = (path) => { + const jobs = extractWorkflowJobs(path); + if (jobs.length === 0) { + const lines = readText(path).replace(/\r\n/gu, "\n").split("\n"); + return extractWorkflowStepsFromLines(path, lines, 0, lines.length); + } + return jobs.flatMap((job) => + extractWorkflowStepsFromLines(path, job.lines, job.start, job.end, job.name), + ); + }; + + const findWorkflowStep = (path, stepName) => { + if (typeof stepName !== "string" || stepName.length === 0) { + fail(`${path} workflow step policy requires a step name`); + } + const steps = extractWorkflowSteps(path).filter( + (candidate) => candidate.name === stepName, + ); + if (steps.length === 0) { + fail(`${path} is missing workflow step ${stepName}`); + } + if (steps.length > 1) { + fail(`${path} defines workflow step ${stepName} more than once`); + } + return steps[0]; + }; + + const assertWorkflowRunStep = (path, stepName, expectedRun) => { + if (typeof expectedRun !== "string" || expectedRun.length === 0) { + fail(`${path} step ${stepName} requires an expected run command`); + } + const step = findWorkflowStep(path, stepName); + if (step.run === null) { + fail(`${path} step ${stepName} does not define a run command`); + } + const actual = normalizeWorkflowRunCommand(step.run); + const expected = normalizeWorkflowRunCommand(expectedRun); + if (actual !== expected) { + fail(`${path} step ${stepName} run command changed`); + } + }; + + const assertWorkflowUsesStep = (path, stepName, expectedUses) => { + if (typeof expectedUses !== "string" || expectedUses.length === 0) { + fail(`${path} step ${stepName} requires an expected action`); + } + const step = findWorkflowStep(path, stepName); + const expected = unquoteWorkflowScalar(expectedUses); + if (step.uses !== expected) { + fail(`${path} step ${stepName} must use ${expected}`); + } + }; + + const assertWorkflowPolicy = (policy) => { + const { + path, + required = [], + forbidden = [], + runSteps = [], + usesSteps = [], + } = policy ?? {}; + if (typeof path !== "string" || path.length === 0) { + fail("workflow policy requires a path"); + } + for (const [policyName, values] of [ + ["required", required], + ["forbidden", forbidden], + ]) { + if ( + !Array.isArray(values) || + values.some((value) => typeof value !== "string") + ) { + fail(`${path} workflow ${policyName} policy must be an array of strings`); + } + } + if (!Array.isArray(runSteps) || !Array.isArray(usesSteps)) { + fail(`${path} workflow step policies must be arrays`); + } + for (const needle of required) { + assertContains(path, needle); + } + for (const needle of forbidden) { + assertNotContains(path, needle); + } + for (const step of runSteps) { + assertWorkflowRunStep(path, step?.name, step?.run); + } + for (const step of usesSteps) { + assertWorkflowUsesStep(path, step?.name, step?.uses); + } + }; + + const assertWorkflowActionsPinned = (workflowOptions = {}) => { + const workflowDirectoryPath = workflowOptions.workflowDirectory ?? ".github/workflows"; + const workflowDirectory = assertRepositoryDirectory( + workflowDirectoryPath, + "workflow directory", + ); + const allowedNonShaUsesPolicy = workflowOptions.allowedNonShaUses ?? []; + if ( + !Array.isArray(allowedNonShaUsesPolicy) || + allowedNonShaUsesPolicy.some((uses) => typeof uses !== "string") + ) { + fail("allowed non-SHA workflow actions must be an array of strings"); + } + const allowedNonShaUses = new Set(allowedNonShaUsesPolicy); + const allowLocalActions = workflowOptions.allowLocalActions ?? true; + const allowDockerActions = workflowOptions.allowDockerActions ?? false; + if (typeof allowLocalActions !== "boolean" || typeof allowDockerActions !== "boolean") { + fail("workflow action allow policies must be booleans"); + } + const fullCommitUse = + /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_./-]+)?@[0-9a-f]{40}$/u; + + for (const workflowFile of readdirSync(workflowDirectory).filter((name) => /\.ya?ml$/u.test(name))) { + const workflowPath = `${workflowDirectoryPath}/${workflowFile}`; + const lines = readText(workflowPath).replace(/\r\n/gu, "\n").split("\n"); + for (const [index, line] of lines.entries()) { + const match = /^\s*(?:-\s+)?uses:\s*(.+?)\s*$/u.exec(line); + if (match === null) { + continue; + } + const uses = unquoteWorkflowScalar(match[1]); + if ( + allowedNonShaUses.has(uses) + ) { + continue; + } + if (uses.startsWith("./")) { + if (!allowLocalActions) { + fail(`${workflowPath}:${index + 1} local action ${uses} is not allowed`); + } + resolveRepositoryPath(uses.slice(2), "local workflow action"); + continue; + } + if (uses.startsWith("docker://")) { + if (!allowDockerActions) { + fail(`${workflowPath}:${index + 1} Docker action ${uses} is not allowed`); + } + if (!/^docker:\/\/[^@\s]+@sha256:[0-9a-f]{64}$/u.test(uses)) { + fail( + `${workflowPath}:${index + 1} Docker action ${uses} is not pinned to a sha256 digest`, + ); + } + continue; + } + if (!fullCommitUse.test(uses)) { + fail(`${workflowPath}:${index + 1} action ${uses} is not pinned to a full commit SHA`); + } + } + } + }; + + const extractWorkflowRunCommands = (path) => { + const lines = readText(path).replace(/\r\n/gu, "\n").split("\n"); + const commands = []; + for (let index = 0; index < lines.length; index += 1) { + const runMatch = /^(\s*)(?:-\s+)?run:\s*(.*)\s*$/u.exec(lines[index]); + if (runMatch === null) { + continue; + } + const runIndent = runMatch[1].length; + const marker = runMatch[2].trim(); + if (marker === ">") { + fail(`${path} uses an unsupported folded run scalar`); + } + if (marker === "|") { + const blockLines = []; + for (let blockCursor = index + 1; blockCursor < lines.length; blockCursor += 1) { + const blockLine = lines[blockCursor]; + if (blockLine.trim().length !== 0 && countLeadingSpaces(blockLine) <= runIndent) { + break; + } + blockLines.push(blockLine); + } + const nonBlankIndents = blockLines + .filter((line) => line.trim().length !== 0) + .map((line) => countLeadingSpaces(line)); + const blockIndent = + nonBlankIndents.length === 0 ? runIndent + 2 : Math.min(...nonBlankIndents); + commands.push( + blockLines.map((line) => line.slice(Math.min(blockIndent, line.length))).join("\n"), + ); + } else { + commands.push(unquoteWorkflowScalar(marker)); + } + } + return commands; + }; + + const assertCargoFuzzWorkflowPolicy = (policy) => { + const { + workflow = ".github/workflows/fuzz.yml", + version, + gitSource, + minimumInstallations = 2, + requiredInstallSteps = [], + } = policy ?? {}; + if (typeof workflow !== "string" || workflow.length === 0) { + fail("cargo-fuzz workflow policy requires a workflow path"); + } + const configuredSources = [ + Object.prototype.hasOwnProperty.call(policy ?? {}, "version"), + Object.prototype.hasOwnProperty.call(policy ?? {}, "gitSource"), + ].filter(Boolean).length; + if (configuredSources !== 1) { + fail("cargo-fuzz workflow policy requires exactly one exact version or Git revision"); + } + const hasVersion = Object.prototype.hasOwnProperty.call(policy ?? {}, "version"); + if (hasVersion && (typeof version !== "string" || !/^\d+\.\d+\.\d+$/u.test(version))) { + fail("cargo-fuzz workflow policy requires an exact semantic version"); + } + const hasGitSource = Object.prototype.hasOwnProperty.call(policy ?? {}, "gitSource"); + if ( + hasGitSource && + (gitSource === null || + typeof gitSource !== "object" || + Array.isArray(gitSource) || + typeof gitSource.url !== "string" || + !/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\.git$/u.test(gitSource.url) || + typeof gitSource.revision !== "string" || + !/^[0-9a-f]{40}$/u.test(gitSource.revision)) + ) { + fail("cargo-fuzz workflow policy requires an exact GitHub repository URL and revision"); + } + if (!Number.isSafeInteger(minimumInstallations) || minimumInstallations < 1) { + fail("cargo-fuzz workflow policy requires a positive installation count"); + } + if ( + !Array.isArray(requiredInstallSteps) || + requiredInstallSteps.some( + (step) => + step === null || + typeof step !== "object" || + Array.isArray(step) || + typeof step.name !== "string" || + step.name.length === 0 || + (step.job !== undefined && + (typeof step.job !== "string" || step.job.length === 0)), + ) + ) { + fail("cargo-fuzz required install steps must be named workflow steps"); + } + + const text = readText(workflow).replace(/\r\n/gu, "\n"); + const workflowRuns = extractWorkflowSteps(workflow) + .filter((step) => step.run !== null) + .map((step) => ({ ...step, command: normalizeWorkflowRunCommand(step.run) })); + const allInstallCommands = extractWorkflowRunCommands(workflow).filter( + (command) => + /^cargo\s+install(?:\s|$)/mu.test(normalizeWorkflowRunCommand(command)) && + /(?:^|\s)cargo-fuzz(?:\s|$)/u.test(normalizeWorkflowRunCommand(command)), + ); + const installSteps = workflowRuns.filter((step) => + /^cargo\s+install(?:\s|$)/mu.test(step.command) && + /(?:^|\s)cargo-fuzz(?:\s|$)/u.test(step.command), + ); + if (installSteps.length !== allInstallCommands.length) { + fail(`${workflow} cargo-fuzz installation must be in a named workflow step`); + } + if (installSteps.length < minimumInstallations) { + fail( + `${workflow} must install cargo-fuzz at least ${minimumInstallations} times`, + ); + } + const environmentVersion = hasVersion + ? new RegExp( + `^\\s*CARGO_FUZZ_VERSION:\\s*["']?${version.replaceAll(".", "\\.")}["']?\\s*$`, + "mu", + ) + : null; + for (const expected of requiredInstallSteps) { + const matches = installSteps.filter( + (step) => + step.name === expected.name && + (expected.job === undefined || step.job === expected.job), + ); + if (matches.length === 0) { + const location = + expected.job === undefined + ? expected.name + : `${expected.job}/${expected.name}`; + fail(`${workflow} is missing cargo-fuzz install step ${location}`); + } + if (matches.length > 1) { + const location = + expected.job === undefined + ? expected.name + : `${expected.job}/${expected.name}`; + fail(`${workflow} defines cargo-fuzz install step ${location} more than once`); + } + } + for (const step of installSteps) { + const command = normalizeWorkflowRunCommand(step.run); + if (!/(?:^|\s)--locked(?:\s|$)/u.test(command)) { + fail(`${workflow} cargo-fuzz installation must use --locked`); + } + if (hasGitSource) { + const expected = + `cargo install --git ${gitSource.url} --rev ${gitSource.revision} --locked cargo-fuzz`; + if (command !== expected) { + fail(`${workflow} cargo-fuzz installation must use the configured exact Git revision`); + } + continue; + } + const usesLiteralVersion = new RegExp( + `(?:^|\\s)--version\\s+${version.replaceAll(".", "\\.")}(?:\\s|$)`, + "u", + ).test(command); + const usesEnvironmentVersion = + command.includes('--version "$CARGO_FUZZ_VERSION"') || + command.includes("--version '$CARGO_FUZZ_VERSION'") || + command.includes("--version $CARGO_FUZZ_VERSION"); + if ( + !usesLiteralVersion && + !(usesEnvironmentVersion && environmentVersion !== null && environmentVersion.test(text)) + ) { + fail( + `${workflow} cargo-fuzz installation must pin version ${version}`, + ); + } + } + }; + + const assertReallyMeRustProtoRepositoryPolicy = (policy) => { + if (policy === null || typeof policy !== "object" || Array.isArray(policy)) { + fail("ReallyMe Rust protobuf repository policy must be an object"); + } + const { + generatedFreshnessMode, + vendoredCore = {}, + workflowActions = {}, + nodeWorkflows = {}, + cargoFuzz, + cargoWorkspace = {}, + spdx = {}, + protobufBoundary, + protobufRelease, + cargoMetadata, + text, + workflows = [], + } = policy; + if (typeof generatedFreshnessMode !== "boolean") { + fail("ReallyMe Rust protobuf repository policy requires generatedFreshnessMode"); + } + for (const [name, value] of [ + ["vendoredCore", vendoredCore], + ["workflowActions", workflowActions], + ["nodeWorkflows", nodeWorkflows], + ["cargoFuzz", cargoFuzz], + ["cargoWorkspace", cargoWorkspace], + ["spdx", spdx], + ["protobufBoundary", protobufBoundary], + ["protobufRelease", protobufRelease], + ]) { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + fail(`ReallyMe Rust protobuf repository policy ${name} must be an object`); + } + } + if ( + cargoMetadata !== undefined && + (cargoMetadata === null || + typeof cargoMetadata !== "object" || + Array.isArray(cargoMetadata)) + ) { + fail("ReallyMe Rust protobuf repository policy cargoMetadata must be an object"); + } + if ( + text !== undefined && + (text === null || typeof text !== "object" || Array.isArray(text)) + ) { + fail("ReallyMe Rust protobuf repository policy text must be an object"); + } + if ( + !Array.isArray(workflows) || + workflows.some( + (workflow) => + workflow === null || + typeof workflow !== "object" || + Array.isArray(workflow), + ) + ) { + fail("ReallyMe Rust protobuf repository workflows must be an array of objects"); + } + if ( + Object.prototype.hasOwnProperty.call( + protobufRelease, + "generatedFreshnessMode", + ) + ) { + fail( + "generatedFreshnessMode must be configured once at the repository-policy level", + ); + } + + assertReallyMeVendoredCorePolicy(vendoredCore); + assertWorkflowActionsPinned(workflowActions); + assertNodeWorkflowJobsPinNode(nodeWorkflows); + assertCargoFuzzWorkflowPolicy(cargoFuzz); + assertCargoWorkspacePolicy(cargoWorkspace); + assertSpdxHeaders(spdx); + assertReallyMeOperationBoundaryContract(protobufBoundary); + assertReallyMeProtobufReleasePolicy({ + ...protobufRelease, + generatedFreshnessMode, + }); + if (cargoMetadata !== undefined) { + assertCargoMetadataPolicy(cargoMetadata); + } + if (text !== undefined) { + assertTextPolicy(text); + } + for (const workflow of workflows) { + assertWorkflowPolicy(workflow); + } + }; + + const stripProtoLineComments = (text) => + text + .split("\n") + .map((line) => { + const commentStart = line.indexOf("//"); + return commentStart === -1 ? line : line.slice(0, commentStart); + }) + .join("\n"); + + const extractProtoBlocks = (protoText, keyword) => { + const blocks = []; + const declarationPattern = new RegExp(`\\b${keyword}\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\{`, "g"); + let match = declarationPattern.exec(protoText); + while (match !== null) { + let depth = 1; + let cursor = declarationPattern.lastIndex; + while (cursor < protoText.length && depth > 0) { + const char = protoText[cursor]; + if (char === "{") { + depth += 1; + } else if (char === "}") { + depth -= 1; + } + cursor += 1; + } + if (depth !== 0) { + fail(`proto ${keyword} ${match[1]} has unbalanced braces`); + } + blocks.push({ + name: match[1], + body: protoText.slice(declarationPattern.lastIndex, cursor - 1), + }); + declarationPattern.lastIndex = cursor; + match = declarationPattern.exec(protoText); + } + return blocks; + }; + + const parseProtoReservations = (path, ownerKind, ownerName, body) => { + const numberRanges = []; + const names = new Set(); + for (const declaration of body.matchAll(/\breserved\s+([^;]+);/gu)) { + for (const rawEntry of declaration[1].split(",")) { + const entry = rawEntry.trim(); + const nameMatch = /^"([A-Za-z_][A-Za-z0-9_]*)"$/u.exec(entry); + if (nameMatch !== null) { + if (names.has(nameMatch[1])) { + fail(`${path} ${ownerKind} ${ownerName} reserves name ${nameMatch[1]} more than once`); + } + names.add(nameMatch[1]); + continue; + } + const rangeMatch = /^(-?\d+)(?:\s+to\s+(-?\d+|max))?$/u.exec(entry); + if (rangeMatch === null) { + fail(`${path} ${ownerKind} ${ownerName} has unsupported reservation ${entry}`); + } + const start = Number.parseInt(rangeMatch[1], 10); + const end = + rangeMatch[2] === undefined + ? start + : rangeMatch[2] === "max" + ? ownerKind === "message" + ? 536_870_911 + : 2_147_483_647 + : Number.parseInt(rangeMatch[2], 10); + if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start > end) { + fail(`${path} ${ownerKind} ${ownerName} has invalid reservation ${entry}`); + } + if ( + numberRanges.some( + ([existingStart, existingEnd]) => + start <= existingEnd && end >= existingStart, + ) + ) { + fail(`${path} ${ownerKind} ${ownerName} has overlapping reserved number ranges`); + } + numberRanges.push([start, end]); + } + } + return { numberRanges, names }; + }; + + const isReservedProtoNumber = (number, reservations) => + reservations.numberRanges.some(([start, end]) => number >= start && number <= end); + + const assertProtoContract = (path) => { + const proto = stripProtoLineComments(readText(path)); + + for (const block of extractProtoBlocks(proto, "enum")) { + const reservations = parseProtoReservations(path, "enum", block.name, block.body); + const names = new Set(); + const numbers = new Set(); + const values = [ + ...block.body.matchAll( + /^\s*([A-Z][A-Z0-9_]*)\s*=\s*(-?\d+)\s*(?:\[[^\]]*\])?\s*;/gmu, + ), + ].map((match) => ({ + name: match[1], + number: Number.parseInt(match[2], 10), + })); + if (values.length === 0) { + fail(`${path} enum ${block.name} must define at least one value`); + } + if (values[0].number !== 0 || !values[0].name.endsWith("_UNSPECIFIED")) { + fail(`${path} enum ${block.name} must start with an UNSPECIFIED value at zero`); + } + for (const value of values) { + if ( + !Number.isSafeInteger(value.number) || + value.number < -2_147_483_648 || + value.number > 2_147_483_647 + ) { + fail(`${path} enum ${block.name} value ${value.name} is outside int32 range`); + } + if (names.has(value.name)) { + fail(`${path} enum ${block.name} defines name ${value.name} more than once`); + } + if (numbers.has(value.number)) { + fail(`${path} enum ${block.name} reuses number ${value.number}`); + } + if (reservations.names.has(value.name)) { + fail(`${path} enum ${block.name} reuses reserved name ${value.name}`); + } + if (isReservedProtoNumber(value.number, reservations)) { + fail(`${path} enum ${block.name} reuses reserved number ${value.number}`); + } + names.add(value.name); + numbers.add(value.number); + } + } + + for (const block of extractProtoBlocks(proto, "message")) { + const reservations = parseProtoReservations(path, "message", block.name, block.body); + const names = new Set(); + const numbers = new Set(); + const fields = [ + ...block.body.matchAll( + /^\s*(?:optional\s+|repeated\s+)?(?:map\s*<[^>]+>|[A-Za-z_][A-Za-z0-9_.]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(\d+)\s*(?:\[[^\]]*\])?\s*;/gmu, + ), + ].map((match) => ({ + name: match[1], + number: Number.parseInt(match[2], 10), + })); + for (const field of fields) { + if ( + !Number.isSafeInteger(field.number) || + field.number < 1 || + field.number > 536_870_911 || + (field.number >= 19_000 && field.number <= 19_999) + ) { + fail(`${path} message ${block.name} field ${field.name} has invalid number ${field.number}`); + } + if (names.has(field.name)) { + fail(`${path} message ${block.name} defines field ${field.name} more than once`); + } + if (numbers.has(field.number)) { + fail(`${path} message ${block.name} reuses field number ${field.number}`); + } + if (reservations.names.has(field.name)) { + fail(`${path} message ${block.name} reuses reserved name ${field.name}`); + } + if (isReservedProtoNumber(field.number, reservations)) { + fail(`${path} message ${block.name} reuses reserved field number ${field.number}`); + } + names.add(field.name); + numbers.add(field.number); + } + } + }; + + const assertReallyMeProtoBoundaryContract = (policy) => { + const { + protoPath, + operationRequest, + resultEnvelope, + resultStatus, + payloadField = "payload", + protoReadme, + protoCargo, + wirePath, + codecPath = wirePath, + bufGen = "buf.gen.yaml", + processProtoNeedle = "pub fn process_proto(", + processProtoJsonNeedle = "pub fn process_proto_json(", + binaryEnvelopeNeedle = "encode_proto_result_envelope", + requiredCodecNeedles = [], + forbiddenCodecNeedles = [], + sdkAdapters = [], + } = policy ?? {}; + for (const [name, value] of Object.entries({ + protoPath, + protoReadme, + protoCargo, + wirePath, + codecPath, + })) { + if (typeof value !== "string" || value.length === 0) { + fail(`protobuf boundary policy ${name} must be a non-empty string`); + } + } + if ( + !Array.isArray(requiredCodecNeedles) || + requiredCodecNeedles.some((needle) => typeof needle !== "string" || needle.length === 0) || + !Array.isArray(forbiddenCodecNeedles) || + forbiddenCodecNeedles.some((needle) => typeof needle !== "string" || needle.length === 0) + ) { + fail("protobuf boundary codec needles must be arrays of non-empty strings"); + } + if ( + !Array.isArray(sdkAdapters) || + sdkAdapters.some( + (adapter) => + adapter === null || + typeof adapter !== "object" || + Array.isArray(adapter), + ) + ) { + fail("protobuf boundary policy sdkAdapters must be an array of objects"); + } + for (const [name, value] of Object.entries({ + operationRequest, + resultEnvelope, + resultStatus, + })) { + if ( + typeof value !== "string" || + !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(value) + ) { + fail(`protobuf boundary policy ${name} must be a protobuf identifier`); + } + } + + assertProtoContract(protoPath); + const proto = stripProtoLineComments(readText(protoPath)); + if (extractProtoBlocks(proto, "service").length !== 0) { + fail(`${protoPath} must define messages only and no protobuf service`); + } + const operationBlock = extractProtoBlocks(proto, "message").find( + (block) => block.name === operationRequest, + ); + if (operationBlock === undefined || !/\boneof\s+operation\s*\{/u.test(operationBlock.body)) { + fail(`${protoPath} ${operationRequest} must define oneof operation`); + } + const envelopeBlock = extractProtoBlocks(proto, "message").find( + (block) => block.name === resultEnvelope, + ); + if (envelopeBlock === undefined) { + fail(`${protoPath} must define message ${resultEnvelope}`); + } + if ( + !new RegExp(`^\\s*${resultStatus}\\s+status\\s*=\\s*1\\s*;`, "mu").test( + envelopeBlock.body, + ) || + !new RegExp(`^\\s*bytes\\s+${payloadField}\\s*=\\s*2\\s*;`, "mu").test( + envelopeBlock.body, + ) + ) { + fail( + `${protoPath} ${resultEnvelope} must contain status = 1 and bytes ${payloadField} = 2`, + ); + } + if ( + !extractProtoBlocks(proto, "enum").some((block) => block.name === resultStatus) + ) { + fail(`${protoPath} must define enum ${resultStatus}`); + } + + assertContains( + protoReadme, + "This crate defines messages only; it intentionally declares no protobuf service.", + ); + assertContains( + protoReadme, + "JSON is a generated ProtoJSON request convenience. Results remain a binary protobuf result envelope.", + ); + assertContains(bufGen, "local: protoc-gen-buffa"); + assertContains(bufGen, "views=true"); + assertContains(bufGen, "json=true"); + assertContains(protoCargo, '"buffa/json"'); + assertContains(protoCargo, "zeroize"); + assertContains(wirePath, operationRequest); + assertContains(wirePath, resultEnvelope); + assertContains(wirePath, "Zeroizing>"); + assertContains(wirePath, processProtoNeedle); + assertContains(wirePath, processProtoJsonNeedle); + assertContains(codecPath, "DecodeOptions::new()"); + assertContains(codecPath, binaryEnvelopeNeedle); + for (const needle of requiredCodecNeedles) { + assertContains(codecPath, needle); + } + for (const needle of forbiddenCodecNeedles) { + assertNotContains(codecPath, needle); + } + assertNotContains(wirePath, "pub fn process_json("); + assertNotContains(wirePath, "pub fn process_proto_with_operation"); + assertNotContains(wirePath, "pub fn process_proto_operation"); + + for (const [index, adapter] of sdkAdapters.entries()) { + const { + path, + processProtoNeedle: adapterProcessProtoNeedle, + processProtoJsonNeedle: adapterProcessProtoJsonNeedle, + binaryEnvelopeNeedle: adapterBinaryEnvelopeNeedle = resultEnvelope, + requiredNeedles = [], + forbiddenNeedles = [], + } = adapter; + for (const [name, value] of Object.entries({ + path, + processProtoNeedle: adapterProcessProtoNeedle, + processProtoJsonNeedle: adapterProcessProtoJsonNeedle, + binaryEnvelopeNeedle: adapterBinaryEnvelopeNeedle, + })) { + if (typeof value !== "string" || value.length === 0) { + fail( + `protobuf boundary sdkAdapters[${index}].${name} must be a non-empty string`, + ); + } + } + for (const [name, needles] of Object.entries({ + requiredNeedles, + forbiddenNeedles, + })) { + if ( + !Array.isArray(needles) || + needles.some( + (needle) => typeof needle !== "string" || needle.length === 0, + ) + ) { + fail( + `protobuf boundary sdkAdapters[${index}].${name} must be an array of non-empty strings`, + ); + } + } + assertContains(path, adapterProcessProtoNeedle); + assertContains(path, adapterProcessProtoJsonNeedle); + assertContains(path, adapterBinaryEnvelopeNeedle); + for (const needle of requiredNeedles) { + assertContains(path, needle); + } + for (const needle of forbiddenNeedles) { + assertNotContains(path, needle); + } + } + }; + + const assertReallyMeOperationBoundaryContract = (policy) => { + const { + protoPath, + operationRequest, + operationResponse, + operationResult = "CodecOperationResult", + errorMessage = "CodecError", + protoReadme, + protoCargo, + wirePath, + codecPath = wirePath, + bufGen = "buf.gen.yaml", + processOperationNeedle = "pub fn process_operation_response(", + processOperationJsonNeedle = "pub fn process_operation_response_json(", + binaryResponseNeedle = "CodecOperationResponse", + requiredCodecNeedles = [], + forbiddenCodecNeedles = [], + sdkAdapters = [], + allowServices = true, + } = policy ?? {}; + for (const [name, value] of Object.entries({ + protoPath, + protoReadme, + protoCargo, + wirePath, + codecPath, + })) { + if (typeof value !== "string" || value.length === 0) { + fail(`operation boundary policy ${name} must be a non-empty string`); + } + } + if ( + !Array.isArray(requiredCodecNeedles) || + requiredCodecNeedles.some((needle) => typeof needle !== "string" || needle.length === 0) || + !Array.isArray(forbiddenCodecNeedles) || + forbiddenCodecNeedles.some((needle) => typeof needle !== "string" || needle.length === 0) + ) { + fail("operation boundary codec needles must be arrays of non-empty strings"); + } + if ( + !Array.isArray(sdkAdapters) || + sdkAdapters.some( + (adapter) => + adapter === null || + typeof adapter !== "object" || + Array.isArray(adapter), + ) + ) { + fail("operation boundary policy sdkAdapters must be an array of objects"); + } + if (typeof allowServices !== "boolean") { + fail("operation boundary policy allowServices must be a boolean"); + } + for (const [name, value] of Object.entries({ + operationRequest, + operationResponse, + operationResult, + errorMessage, + })) { + if ( + typeof value !== "string" || + !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(value) + ) { + fail(`operation boundary policy ${name} must be a protobuf identifier`); + } + } + + assertProtoContract(protoPath); + const proto = stripProtoLineComments(readText(protoPath)); + if (!allowServices && extractProtoBlocks(proto, "service").length !== 0) { + fail(`${protoPath} must define messages only and no protobuf service`); + } + const operationBlock = extractProtoBlocks(proto, "message").find( + (block) => block.name === operationRequest, + ); + if (operationBlock === undefined || !/\boneof\s+operation\s*\{/u.test(operationBlock.body)) { + fail(`${protoPath} ${operationRequest} must define oneof operation`); + } + const responseBlock = extractProtoBlocks(proto, "message").find( + (block) => block.name === operationResponse, + ); + if ( + responseBlock === undefined || + !/\boneof\s+outcome\s*\{/u.test(responseBlock.body) || + !new RegExp(`^\\s*${operationResult}\\s+result\\s*=\\s*1\\s*;`, "mu").test( + responseBlock.body, + ) || + !new RegExp(`^\\s*${errorMessage}\\s+error\\s*=\\s*2\\s*;`, "mu").test( + responseBlock.body, + ) + ) { + fail(`${protoPath} ${operationResponse} must contain a generated result/error outcome oneof`); + } + const resultBlock = extractProtoBlocks(proto, "message").find( + (block) => block.name === operationResult, + ); + if (resultBlock === undefined || !/\boneof\s+result\s*\{/u.test(resultBlock.body)) { + fail(`${protoPath} ${operationResult} must define oneof result`); + } + + assertContains( + protoReadme, + "JSON is a generated ProtoJSON request convenience. Results remain one fully", + ); + assertContains(bufGen, "local: protoc-gen-buffa"); + assertContains(bufGen, "views=true"); + assertContains(bufGen, "json=true"); + assertContains(protoCargo, '"buffa/json"'); + assertContains(protoCargo, "zeroize"); + assertContains(wirePath, operationRequest); + assertContains(wirePath, operationResponse); + assertContains(wirePath, "Zeroizing>"); + assertContains(wirePath, processOperationNeedle); + assertContains(wirePath, processOperationJsonNeedle); + assertContains(codecPath, "DecodeOptions::new()"); + assertContains(codecPath, binaryResponseNeedle); + for (const needle of requiredCodecNeedles) { + assertContains(codecPath, needle); + } + for (const needle of forbiddenCodecNeedles) { + assertNotContains(codecPath, needle); + } + assertNotContains(wirePath, "pub fn process_json("); + assertNotContains(wirePath, "pub fn process_proto_with_operation"); + assertNotContains(wirePath, "pub fn process_proto_operation"); + assertNotContains(wirePath, "CodecProtoResultEnvelope"); + + for (const [index, adapter] of sdkAdapters.entries()) { + const { + path, + processOperationNeedle: adapterProcessOperationNeedle, + processOperationJsonNeedle: adapterProcessOperationJsonNeedle, + binaryResponseNeedle: adapterBinaryResponseNeedle = operationResponse, + requiredNeedles = [], + forbiddenNeedles = [], + } = adapter; + for (const [name, value] of Object.entries({ + path, + processOperationNeedle: adapterProcessOperationNeedle, + processOperationJsonNeedle: adapterProcessOperationJsonNeedle, + binaryResponseNeedle: adapterBinaryResponseNeedle, + })) { + if (typeof value !== "string" || value.length === 0) { + fail( + `operation boundary sdkAdapters[${index}].${name} must be a non-empty string`, + ); + } + } + for (const [name, needles] of Object.entries({ + requiredNeedles, + forbiddenNeedles, + })) { + if ( + !Array.isArray(needles) || + needles.some( + (needle) => typeof needle !== "string" || needle.length === 0, + ) + ) { + fail( + `operation boundary sdkAdapters[${index}].${name} must be an array of non-empty strings`, + ); + } + } + assertContains(path, adapterProcessOperationNeedle); + assertContains(path, adapterProcessOperationJsonNeedle); + assertContains(path, adapterBinaryResponseNeedle); + for (const needle of requiredNeedles) { + assertContains(path, needle); + } + for (const needle of forbiddenNeedles) { + assertNotContains(path, needle); + } + } + }; + + return { + root, + fail, + readText, + readJson, + listFiles, + requireTracked, + loadTrackedFiles, + assertContains, + assertNotContains, + assertMinOccurrences, + assertTextPolicy, + requireMatch, + assertNotMatches, + assertLockPackageVersion, + run, + runCommands, + runNodeCheck, + packageList, + assertPackageFiles, + assertCargoMetadataDocument, + assertCargoMetadataPolicy, + assertCargoWorkspacePolicy, + snapshotDirectory, + assertSnapshotsEqual, + validateGeneratedArtifactsPolicy, + assertGeneratedArtifactsFresh, + assertGeneratedProtoHardeningPolicy, + assertReallyMeProtobufReleasePolicy, + assertReallyMeVendoredCorePolicy, + assertNodeWorkflowJobsPinNode, + assertWorkflowActionsPinned, + assertWorkflowPermissionsPolicy, + assertCargoFuzzWorkflowPolicy, + assertReallyMeRustProtoRepositoryPolicy, + normalizeWorkflowRunCommand, + extractWorkflowSteps, + assertWorkflowRunStep, + assertWorkflowUsesStep, + assertWorkflowPolicy, + stripProtoLineComments, + extractProtoBlocks, + assertProtoContract, + assertReallyMeProtoBoundaryContract, + assertReallyMeOperationBoundaryContract, + assertSpdxHeaders, + }; +} diff --git a/scripts/release-readiness/operation-contract-routes.mjs b/scripts/release-readiness/operation-contract-routes.mjs new file mode 100644 index 0000000..844ec6e --- /dev/null +++ b/scripts/release-readiness/operation-contract-routes.mjs @@ -0,0 +1,180 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +const ADAPTER_ROOT = "crates/cose/src/operation_contract/"; +const NATIVE_ROOT = "crates/cose/src/"; + +export const OPERATION_CONTRACT_ROUTES = Object.freeze([ + route( + "Sign1Create", + "sign1/create.rs", + "attached_result", + "create_cose_sign1", + "created_attached", + "sign1/sign.rs", + "cose_sign1_with_options_and_external_aad", + ), + route( + "Sign1CreateDetached", + "sign1/create.rs", + "detached_result", + "create_detached_cose_sign1", + "created_detached", + "sign1/sign.rs", + "cose_sign1_detached_with_options_and_external_aad", + ), + route( + "Sign1Verify", + "sign1/verify.rs", + "attached_result", + "verify_cose_sign1", + "verified_attached", + "sign1/verify.rs", + "cose_verify1_with_policy_and_external_aad", + ), + route( + "Sign1VerifyDetached", + "sign1/verify.rs", + "detached_result", + "verify_detached_cose_sign1", + "verified_detached", + "sign1/verify.rs", + "cose_verify1_detached_with_policy_and_external_aad", + ), + route( + "KeyFromPublicBytes", + "key/convert.rs", + "from_public_bytes_result", + "construct_cose_key_from_public", + "from_public_key", + "key/convert.rs", + "cose_key_from_public_bytes", + ), + route( + "KeyFromPrivateBytes", + "key/convert.rs", + "from_private_bytes_result", + "construct_cose_key_from_private", + "from_private_key", + "key/convert.rs", + "cose_key_from_private_bytes", + ), + route( + "KeyParse", + "key/parse.rs", + "result", + "parse_cose_key", + "parsed_key", + "key/parse.rs", + "cose_key_from_slice", + ), + route( + "KeyToPublicBytes", + "key/convert.rs", + "to_public_bytes_result", + "extract_cose_key_public", + "public_key_bytes", + "key/convert.rs", + "cose_key_to_public_bytes", + ), + route( + "KeyToPrivateBytes", + "key/convert.rs", + "to_private_bytes_result", + "extract_cose_key_private", + "private_key_bytes", + "key/convert.rs", + "cose_key_to_private_bytes", + ), + route( + "KeyDerivePublicKid", + "key/convert.rs", + "derive_public_kid_result", + "derive_cose_key_public_kid", + "key_identifier", + "key/derive_kid.rs", + "derive_kid_from_cose_key_public", + ), + route( + "KeyToMultikey", + "key/convert.rs", + "to_multikey_result", + "convert_cose_key_to_multikey", + "multikey", + "multikey/convert.rs", + "cose_key_to_multikey", + ), + route( + "MultikeyToCoseKey", + "key/convert.rs", + "multikey_to_key_result", + "convert_multikey_to_cose_key", + "from_multikey_key", + "multikey/convert.rs", + "multikey_to_cose_key", + ), + indirectEncryptRoute( + "MlKemEncryptDirect", + "direct_result", + "encrypt_cose_ml_kem_direct", + "encrypted_direct", + "cose_encrypt_ml_kem_direct_with_external_aad", + ), + indirectEncryptRoute( + "MlKemEncryptKeyWrap", + "key_wrap_result", + "encrypt_cose_ml_kem_key_wrap", + "encrypted_key_wrap", + "cose_encrypt_ml_kem_key_wrap_with_external_aad", + ), + route( + "MlKemDecrypt", + "encrypt/decrypt.rs", + "result", + "decrypt_cose_ml_kem", + "decrypted", + "encrypt/decrypt.rs", + "cose_decrypt_ml_kem_with_external_aad", + ), +]); + +function route( + variant, + adapterRelativePath, + adapterFunction, + semanticFunction, + resultFunction, + nativeRelativePath, + nativeFunction, +) { + return Object.freeze({ + variant, + adapterPath: `${ADAPTER_ROOT}${adapterRelativePath}`, + adapterFunction, + semanticFunction, + resultFunction, + nativePath: `${NATIVE_ROOT}${nativeRelativePath}`, + nativeFunction, + indirect: false, + }); +} + +function indirectEncryptRoute( + variant, + adapterFunction, + semanticFunction, + resultFunction, + nativeFunction, +) { + return Object.freeze({ + variant, + adapterPath: `${ADAPTER_ROOT}encrypt/create.rs`, + adapterFunction, + semanticFunction, + resultFunction, + nativePath: `${NATIVE_ROOT}encrypt/create.rs`, + nativeFunction, + indirect: true, + }); +} diff --git a/scripts/release-readiness/operation-contract-routing.mjs b/scripts/release-readiness/operation-contract-routing.mjs new file mode 100644 index 0000000..4aa6ab4 --- /dev/null +++ b/scripts/release-readiness/operation-contract-routing.mjs @@ -0,0 +1,402 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +import { OPERATION_CONTRACT_ROUTES } from "./operation-contract-routes.mjs"; + +const ADAPTER_ROOT = "crates/cose/src/operation_contract/"; + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); +} + +function blankRange(output, source, start, end) { + for (let index = start; index < end; index += 1) { + if (source[index] !== "\n" && source[index] !== "\r") { + output[index] = " "; + } + } +} + +// Mask comments and literals before structural checks. This prevents a route +// name in documentation, a diagnostic, or dead string data from satisfying an +// executable-call invariant. +export function maskRustNonCode(source) { + const output = Array.from(source); + let index = 0; + while (index < source.length) { + if (source.startsWith("//", index)) { + const end = source.indexOf("\n", index + 2); + const boundary = end === -1 ? source.length : end; + blankRange(output, source, index, boundary); + index = boundary; + continue; + } + if (source.startsWith("/*", index)) { + let depth = 1; + let end = index + 2; + while (end < source.length && depth > 0) { + if (source.startsWith("/*", end)) { + depth += 1; + end += 2; + } else if (source.startsWith("*/", end)) { + depth -= 1; + end += 2; + } else { + end += 1; + } + } + blankRange(output, source, index, end); + index = end; + continue; + } + + const raw = /^(?:br|cr|r)(#{0,255})"/u.exec(source.slice(index)); + if (raw !== null) { + const terminator = `"${raw[1]}`; + const contentStart = index + raw[0].length; + const closing = source.indexOf(terminator, contentStart); + const end = closing === -1 ? source.length : closing + terminator.length; + blankRange(output, source, index, end); + index = end; + continue; + } + + if (source[index] === '"') { + let end = index + 1; + while (end < source.length) { + if (source[end] === "\\") { + end += 2; + } else if (source[end] === '"') { + end += 1; + break; + } else { + end += 1; + } + } + blankRange(output, source, index, end); + index = end; + continue; + } + + if (source[index] === "'") { + const character = /^'(?:\\(?:x[0-9A-Fa-f]{2}|u\{[0-9A-Fa-f_]+\}|.)|[^'\\\r\n])'/u.exec( + source.slice(index), + ); + if (character !== null) { + const end = index + character[0].length; + blankRange(output, source, index, end); + index = end; + continue; + } + } + index += 1; + } + return output.join(""); +} + +export function rustFunctionBody(source, functionName) { + const masked = maskRustNonCode(source); + const definition = new RegExp(`\\bfn\\s+${escapeRegExp(functionName)}\\s*(?:<[^;{}]*>)?\\s*\\(`, "gu"); + const matches = [...masked.matchAll(definition)]; + if (matches.length !== 1) { + return null; + } + const bodyStart = masked.indexOf("{", matches[0].index + matches[0][0].length); + const declarationEnd = masked.indexOf(";", matches[0].index + matches[0][0].length); + if (bodyStart === -1 || (declarationEnd !== -1 && declarationEnd < bodyStart)) { + return null; + } + let depth = 0; + for (let index = bodyStart; index < masked.length; index += 1) { + if (masked[index] === "{") { + depth += 1; + } else if (masked[index] === "}") { + depth -= 1; + if (depth === 0) { + return masked.slice(bodyStart + 1, index); + } + } + } + return null; +} + +function countMatches(source, pattern) { + return [...source.matchAll(pattern)].length; +} + +function countCalls(source, functionName) { + return countMatches(source, new RegExp(`\\b${escapeRegExp(functionName)}\\s*\\(`, "gu")); +} + +function countReferences(source, identifier) { + return countMatches(source, new RegExp(`\\b${escapeRegExp(identifier)}\\b`, "gu")); +} + +function qualifiedCallPattern(path) { + const qualified = path.split("::").map(escapeRegExp).join("\\s*::\\s*"); + return new RegExp(`\\b${qualified}\\s*\\(`, "gu"); +} + +function requireFunction(readText, violations, path, functionName) { + const body = rustFunctionBody(readText(path), functionName); + if (body === null) { + violations.push(`${path} must define exactly one ${functionName} function with a body`); + } + return body; +} + +function requireExactCalls(violations, path, functionName, body, target, expected = 1) { + if (body === null) { + return; + } + const count = countCalls(body, target); + if (count !== expected) { + violations.push(`${path} ${functionName} must call ${target} exactly ${expected} time(s); found ${count}`); + } +} + +function requireExactReferences(violations, path, functionName, body, target, expected = 1) { + if (body === null) { + return; + } + const count = countReferences(body, target); + if (count !== expected) { + violations.push(`${path} ${functionName} must reference ${target} exactly ${expected} time(s); found ${count}`); + } +} + +export function collectOperationContractRoutingViolations(readText) { + const violations = []; + const wirePath = "crates/cose/src/wire.rs"; + const executePath = `${ADAPTER_ROOT}execute.rs`; + const dispatcher = requireFunction(readText, violations, executePath, "dispatch_operation"); + if (dispatcher !== null) { + for (const routePolicy of OPERATION_CONTRACT_ROUTES) { + const variantCount = countReferences(dispatcher, routePolicy.variant); + if (variantCount !== 1) { + violations.push( + `${wirePath} dispatch_operation must dispatch ${routePolicy.variant} exactly once; found ${variantCount}`, + ); + } + const adapterModule = routePolicy.adapterPath + .slice(ADAPTER_ROOT.length, -".rs".length) + .split("/") + .join("::"); + const adapterCall = `super::${adapterModule}::${routePolicy.adapterFunction}`; + const callCount = countMatches(dispatcher, qualifiedCallPattern(adapterCall)); + if (callCount !== 1) { + violations.push(`${executePath} ${routePolicy.variant} must call ${adapterCall} exactly once; found ${callCount}`); + } + } + } + + for (const [entrypoint, target] of [ + ["execute_operation_proto", "execute_proto"], + ["execute_operation_proto_json", "execute_proto_json"], + ]) { + const body = requireFunction(readText, violations, wirePath, entrypoint); + requireExactCalls(violations, wirePath, entrypoint, body, target); + } + + for (const entrypoint of ["execute_proto", "execute_proto_json"]) { + const body = requireFunction(readText, violations, executePath, entrypoint); + requireExactReferences(violations, executePath, entrypoint, body, "dispatch_operation"); + } + + const adapterSources = new Map(); + for (const routePolicy of OPERATION_CONTRACT_ROUTES) { + const source = adapterSources.get(routePolicy.adapterPath) ?? readText(routePolicy.adapterPath); + adapterSources.set(routePolicy.adapterPath, source); + const adapterBody = rustFunctionBody(source, routePolicy.adapterFunction); + if (adapterBody === null) { + violations.push(`${routePolicy.adapterPath} must define exactly one ${routePolicy.adapterFunction} adapter`); + } else if (routePolicy.indirect) { + requireExactCalls( + violations, + routePolicy.adapterPath, + routePolicy.adapterFunction, + adapterBody, + "encrypt_result", + ); + requireExactReferences( + violations, + routePolicy.adapterPath, + routePolicy.adapterFunction, + adapterBody, + routePolicy.semanticFunction, + ); + requireExactReferences( + violations, + routePolicy.adapterPath, + routePolicy.adapterFunction, + adapterBody, + routePolicy.resultFunction, + ); + } else { + requireExactCalls( + violations, + routePolicy.adapterPath, + routePolicy.adapterFunction, + adapterBody, + routePolicy.semanticFunction, + ); + requireExactCalls( + violations, + routePolicy.adapterPath, + routePolicy.adapterFunction, + adapterBody, + routePolicy.resultFunction, + ); + requireExactReferences( + violations, + routePolicy.adapterPath, + routePolicy.adapterFunction, + adapterBody, + "boundary_error_from_failure", + ); + } + + const nativeSource = readText(routePolicy.nativePath); + const semanticDefinitions = countMatches( + maskRustNonCode(nativeSource), + new RegExp( + `\\bpub\\s*\\(\\s*crate\\s*\\)\\s+fn\\s+${escapeRegExp(routePolicy.semanticFunction)}\\s*\\(`, + "gu", + ), + ); + if (semanticDefinitions !== 1) { + violations.push( + `${routePolicy.nativePath} must define ${routePolicy.semanticFunction} exactly once as pub(crate); found ${semanticDefinitions}`, + ); + } + const nativeBody = requireFunction( + readText, + violations, + routePolicy.nativePath, + routePolicy.nativeFunction, + ); + requireExactCalls( + violations, + routePolicy.nativePath, + routePolicy.nativeFunction, + nativeBody, + routePolicy.semanticFunction, + ); + } + + const keyHelper = requireFunction( + readText, + violations, + `${ADAPTER_ROOT}key/convert.rs`, + "parse_request_key", + ); + requireExactCalls(violations, `${ADAPTER_ROOT}key/convert.rs`, "parse_request_key", keyHelper, "parse_cose_key"); + requireExactReferences( + violations, + `${ADAPTER_ROOT}key/convert.rs`, + "parse_request_key", + keyHelper, + "boundary_error_from_failure", + ); + + const encryptHelper = requireFunction( + readText, + violations, + `${ADAPTER_ROOT}encrypt/create.rs`, + "encrypt_result", + ); + requireExactCalls(violations, `${ADAPTER_ROOT}encrypt/create.rs`, "encrypt_result", encryptHelper, "operation"); + requireExactCalls(violations, `${ADAPTER_ROOT}encrypt/create.rs`, "encrypt_result", encryptHelper, "convert_result"); + requireExactReferences( + violations, + `${ADAPTER_ROOT}encrypt/create.rs`, + "encrypt_result", + encryptHelper, + "boundary_error_from_failure", + ); + + for (const [path, functionName, delegate, forbidden] of [ + [ + "crates/cose/src/encrypt/create.rs", + "cose_encrypt_ml_kem_direct", + "cose_encrypt_ml_kem_direct_with_external_aad", + "encrypt_ml_kem", + ], + [ + "crates/cose/src/encrypt/create.rs", + "cose_encrypt_ml_kem_key_wrap", + "cose_encrypt_ml_kem_key_wrap_with_external_aad", + "encrypt_ml_kem", + ], + [ + "crates/cose/src/encrypt/decrypt.rs", + "cose_decrypt_ml_kem", + "cose_decrypt_ml_kem_with_external_aad", + "decrypt_ml_kem", + ], + ]) { + const body = requireFunction(readText, violations, path, functionName); + requireExactCalls(violations, path, functionName, body, delegate); + requireExactCalls(violations, path, functionName, body, forbidden, 0); + } + + const bypassedFacades = [ + "cose_key_from_slice", + "cose_key_from_public_bytes", + "cose_key_from_private_bytes", + "cose_key_to_public_bytes", + "cose_key_to_private_bytes", + "derive_kid_from_cose_key_public", + "cose_key_to_multikey", + "multikey_to_cose_key", + "cose_sign1_with_options_and_external_aad", + "cose_sign1_detached_with_options_and_external_aad", + "cose_verify1_with_policy_and_external_aad", + "cose_verify1_detached_with_policy_and_external_aad", + "cose_encrypt_ml_kem_direct_with_external_aad", + "cose_encrypt_ml_kem_key_wrap_with_external_aad", + "cose_decrypt_ml_kem_with_external_aad", + ]; + for (const [path, source] of adapterSources) { + const code = maskRustNonCode(source); + for (const facade of bypassedFacades) { + const count = countCalls(code, facade); + if (count !== 0) { + violations.push(`${path} must not call native facade ${facade}; found ${count}`); + } + } + for (const forbiddenPattern of [ + [/_impl\s*\(/gu, "a lower-level *_impl helper"], + [/\bCoseWireError\s*::/gu, "CoseWireError constructors"], + [/\bCoseErrorReason\s*::/gu, "CoseErrorReason variants"], + [/\bCoseFailure\s*::/gu, "CoseFailure constructors"], + [/\b(?:serde_json|serde)\s*::/gu, "hand-written JSON"], + [/\b(?:encode_protobuf|decode_protobuf|encode_to_vec)\s*\(/gu, "transport codec calls"], + [/\bCoseOperationResult\s*\{/gu, "generated result construction"], + ]) { + if (forbiddenPattern[0].test(code)) { + violations.push(`${path} must not contain ${forbiddenPattern[1]}`); + } + } + } + + const verifyAdapter = maskRustNonCode(readText(`${ADAPTER_ROOT}sign1/verify.rs`)); + const cryptoReferences = countMatches(verifyAdapter, /\breallyme_crypto\s*::/gu); + const constantTimeReferences = countMatches( + verifyAdapter, + /\breallyme_crypto\s*::\s*operations\s*::\s*constant_time\s*::\s*equal\s*\(/gu, + ); + if (cryptoReferences !== 1 || constantTimeReferences !== 1) { + violations.push(`${ADAPTER_ROOT}sign1/verify.rs may use only one timing-safe kid comparison from reallyme_crypto`); + } + + return violations; +} + +export function assertOperationContractRouting({ readText, fail }) { + const violations = collectOperationContractRoutingViolations(readText); + if (violations.length !== 0) { + fail(`operation-contract routing policy failed: ${violations[0]}`); + } +} diff --git a/scripts/release-readiness/operation-contract-routing.test.mjs b/scripts/release-readiness/operation-contract-routing.test.mjs new file mode 100644 index 0000000..f90ecee --- /dev/null +++ b/scripts/release-readiness/operation-contract-routing.test.mjs @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { resolve } from "node:path"; + +import { collectOperationContractRoutingViolations } from "./operation-contract-routing.mjs"; + +const repositoryRoot = fileURLToPath(new URL("../..", import.meta.url)); + +function repositoryReader(path) { + return readFileSync(resolve(repositoryRoot, path), "utf8"); +} + +function violationsAfter(path, mutate) { + return collectOperationContractRoutingViolations((requestedPath) => { + const source = repositoryReader(requestedPath); + return requestedPath === path ? mutate(source) : source; + }); +} + +function replaceOnce(source, before, after) { + assert.equal(source.split(before).length - 1, 1, `fixture must contain ${before} exactly once`); + return source.replace(before, after); +} + +test("current operation contract satisfies every routing invariant", () => { + assert.deepEqual(collectOperationContractRoutingViolations(repositoryReader), []); +}); + +test("a missing operation-contract route cannot hide behind another dispatcher branch", () => { + const violations = violationsAfter("crates/cose/src/operation_contract/execute.rs", (source) => + replaceOnce( + source, + "super::key::parse::result(*request)", + "super::key::convert::to_public_bytes_result(*request)", + ), + ); + assert.match(violations.join("\n"), /KeyParse must call .*key::parse::result exactly once; found 0/u); +}); + +test("adapter comments cannot impersonate an executable semantic call", () => { + const violations = violationsAfter("crates/cose/src/operation_contract/key/parse.rs", (source) => + replaceOnce( + source, + "parse_cose_key(CoseKeyParseInput::new(&encoded_key))", + "bypass_parse(CoseKeyParseInput::new(&encoded_key)) // parse_cose_key(CoseKeyParseInput::new(&encoded_key))", + ), + ); + assert.match(violations.join("\n"), /key\/parse\.rs result must call parse_cose_key exactly 1 time.*found 0/u); +}); + +test("duplicate semantic execution is rejected", () => { + const violations = violationsAfter("crates/cose/src/operation_contract/sign1/create.rs", (source) => + replaceOnce( + source, + "let result = create_cose_sign1(", + "let _duplicate = create_cose_sign1(\n let result = create_cose_sign1(", + ), + ); + assert.match(violations.join("\n"), /attached_result must call create_cose_sign1 exactly 1 time/u); +}); + +test("native convenience APIs cannot bypass their semantic facade", () => { + const violations = violationsAfter("crates/cose/src/encrypt/create.rs", (source) => + replaceOnce( + source, + "cose_encrypt_ml_kem_direct_with_external_aad(request, &[])", + "encrypt_ml_kem(request, CoseMlKemMode::Direct, &[])", + ), + ); + assert.match( + violations.join("\n"), + /cose_encrypt_ml_kem_direct must call cose_encrypt_ml_kem_direct_with_external_aad/u, + ); + assert.match(violations.join("\n"), /cose_encrypt_ml_kem_direct must call encrypt_ml_kem exactly 0 time.*found 1/u); +}); + +test("shared encryption adapters must invoke the selected operation exactly once", () => { + const violations = violationsAfter("crates/cose/src/operation_contract/encrypt/create.rs", (source) => + replaceOnce(source, "let output = operation(input)", "let output = bypass(input)"), + ); + assert.match(violations.join("\n"), /encrypt_result must call operation exactly 1 time.*found 0/u); +}); + +test("adapters cannot introduce independent error classification", () => { + const violations = violationsAfter("crates/cose/src/operation_contract/encrypt/decrypt.rs", (source) => + replaceOnce( + source, + "pub(crate) fn result(", + "const _: Option = Some(CoseWireError::backend_internal);\n\npub(crate) fn result(", + ), + ); + assert.match(violations.join("\n"), /encrypt\/decrypt\.rs must not contain CoseWireError constructors/u); +}); diff --git a/scripts/run_pinned_release_readiness.mjs b/scripts/run_pinned_release_readiness.mjs new file mode 100644 index 0000000..6411550 --- /dev/null +++ b/scripts/run_pinned_release_readiness.mjs @@ -0,0 +1,116 @@ +#!/usr/bin/env node +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +import { createHash, timingSafeEqual } from "node:crypto"; +import { lstatSync, readFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; + +const RELEASE_READINESS_COMMIT = "f27973caf9d3a12847cac4032c361f5f553c97e9"; +const RELEASE_READINESS_CORE_SHA256 = + "70cc78721738cf352024938e8fc86e73380e71b2cdf7a9a733687543167cbaae"; +const RELEASE_READINESS_CORE_URL = + `https://raw.githubusercontent.com/reallyme/release-readiness/${RELEASE_READINESS_COMMIT}/core.mjs`; +const VENDORED_CORE_PATH = "scripts/release-readiness/core.mjs"; +const LOCAL_CHECKER_PATH = "scripts/check_release_readiness.mjs"; +const MAX_CORE_BYTES = 262_144; +const FETCH_TIMEOUT_MILLISECONDS = 30_000; + +const fail = (reason) => { + console.error(`pinned release readiness failed: ${reason}`); + process.exit(1); +}; + +const sha256 = (value) => createHash("sha256").update(value).digest(); + +const expectedDigest = Buffer.from(RELEASE_READINESS_CORE_SHA256, "hex"); +if (expectedDigest.length !== 32) { + fail("configured core digest is invalid"); +} + +let localCore; +try { + const checkerStatus = lstatSync(LOCAL_CHECKER_PATH); + if (checkerStatus.isSymbolicLink() || !checkerStatus.isFile()) { + fail("local checker must be a regular file"); + } + const status = lstatSync(VENDORED_CORE_PATH); + if (status.isSymbolicLink() || !status.isFile()) { + fail("vendored core must be a regular file"); + } + if (status.size === 0 || status.size > MAX_CORE_BYTES) { + fail("vendored core size is outside the accepted boundary"); + } + localCore = readFileSync(VENDORED_CORE_PATH); +} catch { + fail("vendored core is missing or inaccessible"); +} +if (!timingSafeEqual(sha256(localCore), expectedDigest)) { + fail("vendored core does not match the reviewed upstream pin"); +} + +let response; +try { + response = await fetch(RELEASE_READINESS_CORE_URL, { + cache: "no-store", + redirect: "error", + signal: AbortSignal.timeout(FETCH_TIMEOUT_MILLISECONDS), + }); +} catch { + fail("pinned upstream core could not be fetched"); +} +if (!response.ok || response.body === null) { + fail("pinned upstream core returned an invalid response"); +} + +const contentLength = response.headers.get("content-length"); +if (contentLength !== null) { + if (!/^[1-9][0-9]*$/u.test(contentLength)) { + fail("pinned upstream core returned an invalid content length"); + } + const parsedLength = Number.parseInt(contentLength, 10); + if (!Number.isSafeInteger(parsedLength) || parsedLength <= 0 || parsedLength > MAX_CORE_BYTES) { + fail("pinned upstream core length is outside the accepted boundary"); + } +} + +const reader = response.body.getReader(); +const chunks = []; +let totalLength = 0; +for (;;) { + let result; + try { + result = await reader.read(); + } catch { + fail("pinned upstream core response could not be read"); + } + if (result.done) { + break; + } + const chunk = result.value; + if (!(chunk instanceof Uint8Array) || chunk.length > MAX_CORE_BYTES - totalLength) { + fail("pinned upstream core exceeds the accepted boundary"); + } + chunks.push(chunk); + totalLength += chunk.length; +} +if (totalLength === 0) { + fail("pinned upstream core is empty"); +} +const upstreamCore = Buffer.concat(chunks, totalLength); +if (!timingSafeEqual(sha256(upstreamCore), expectedDigest)) { + fail("pinned upstream core digest does not match the reviewed commit"); +} + +const checker = spawnSync(process.execPath, [LOCAL_CHECKER_PATH, ...process.argv.slice(2)], { + env: process.env, + stdio: "inherit", +}); +if (checker.error !== undefined) { + fail("local release readiness checker could not be started"); +} +if (!Number.isInteger(checker.status)) { + fail("local release readiness checker ended without a deterministic status"); +} +process.exit(checker.status); diff --git a/scripts/verify_release_attestation.mjs b/scripts/verify_release_attestation.mjs new file mode 100644 index 0000000..4d9452b --- /dev/null +++ b/scripts/verify_release_attestation.mjs @@ -0,0 +1,211 @@ +#!/usr/bin/env node +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +import { appendFileSync, lstatSync, readFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const FULL_SHA_PATTERN = /^[0-9a-f]{40}$/u; +const POSITIVE_INTEGER_PATTERN = /^[1-9][0-9]*$/u; +const REPOSITORY_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u; +const VERSION_PATTERN = /^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$/u; +const PREFLIGHT_WORKFLOW = "crates-package-preflight.yml"; +const PREFLIGHT_PATH = `.github/workflows/${PREFLIGHT_WORKFLOW}@refs/heads/main`; +const ATTESTATION_SCHEMA = "reallyme.cose.crates_preflight.v1"; +const DEFAULT_ATTESTATION_PATH = "release-attestation/crates-preflight.json"; +const MAX_COMMAND_OUTPUT_BYTES = 1_048_576; +const MAX_ATTESTATION_BYTES = 16_384; +const ATTESTATION_KEYS = Object.freeze([ + "release_sha", + "repository", + "run_attempt", + "run_id", + "schema", + "version", + "workflow", +]); + +export class ReleaseAttestationError extends Error { + constructor(code) { + super(code); + this.name = "ReleaseAttestationError"; + this.code = code; + } +} + +const fail = (code) => { + throw new ReleaseAttestationError(code); +}; + +const parsePositiveInteger = (value, code) => { + if (typeof value !== "string" || !POSITIVE_INTEGER_PATTERN.test(value)) { + fail(code); + } + const parsed = Number(value); + if (!Number.isSafeInteger(parsed)) { + fail(code); + } + return parsed; +}; + +const runJson = (arguments_, code) => { + const result = spawnSync("gh", arguments_, { + encoding: "utf8", + maxBuffer: MAX_COMMAND_OUTPUT_BYTES, + stdio: ["ignore", "pipe", "ignore"], + }); + if (result.error !== undefined || result.status !== 0 || typeof result.stdout !== "string") { + fail(code); + } + try { + return JSON.parse(result.stdout); + } catch { + fail(code); + } +}; + +const readAttestation = (path) => { + let status; + let contents; + try { + status = lstatSync(path); + if ( + status.isSymbolicLink() || + !status.isFile() || + status.size === 0 || + status.size > MAX_ATTESTATION_BYTES + ) { + fail("invalid-attestation-file"); + } + contents = readFileSync(path, "utf8"); + } catch (error) { + if (error instanceof ReleaseAttestationError) { + throw error; + } + fail("invalid-attestation-file"); + } + try { + return JSON.parse(contents); + } catch { + fail("invalid-attestation-json"); + } +}; + +const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value); + +export const verifyAttestationDocument = (value, expected) => { + if (!isRecord(value)) { + fail("invalid-attestation-document"); + } + const keys = Object.keys(value).sort(); + if (keys.length !== ATTESTATION_KEYS.length || keys.some((key, index) => key !== ATTESTATION_KEYS[index])) { + fail("invalid-attestation-document"); + } + if ( + value.schema !== ATTESTATION_SCHEMA || + value.repository !== expected.repository || + value.workflow !== PREFLIGHT_WORKFLOW || + value.run_id !== expected.runId || + value.run_attempt !== 1 || + value.release_sha !== expected.releaseSha || + value.version !== expected.releaseVersion + ) { + fail("attestation-input-mismatch"); + } +}; + +export const verifyWorkflowRun = (value, expected) => { + if (!isRecord(value)) { + fail("invalid-preflight-run"); + } + if ( + value.workflow_id !== expected.workflowId || + value.id !== expected.runId || + value.event !== "workflow_dispatch" || + value.head_branch !== "main" || + value.head_sha !== expected.releaseSha || + value.status !== "completed" || + value.conclusion !== "success" || + value.run_attempt !== 1 || + value.path !== PREFLIGHT_PATH + ) { + fail("preflight-run-mismatch"); + } +}; + +export const verifyReleaseAttestation = ({ env = process.env } = {}) => { + const repository = env.GITHUB_REPOSITORY; + const releaseSha = env.RELEASE_SHA; + const releaseVersion = env.RELEASE_VERSION; + if (typeof repository !== "string" || !REPOSITORY_PATTERN.test(repository)) { + fail("invalid-repository"); + } + if (typeof releaseSha !== "string" || !FULL_SHA_PATTERN.test(releaseSha)) { + fail("invalid-release-sha"); + } + if (typeof releaseVersion !== "string" || !VERSION_PATTERN.test(releaseVersion)) { + fail("invalid-release-version"); + } + if (typeof env.GH_TOKEN !== "string" || env.GH_TOKEN.length === 0) { + fail("missing-github-token"); + } + if (env.GITHUB_SHA !== releaseSha) { + fail("workflow-head-mismatch"); + } + const runId = parsePositiveInteger(env.PREFLIGHT_RUN_ID, "invalid-preflight-run-id"); + const workflow = runJson( + ["api", `repos/${repository}/actions/workflows/${PREFLIGHT_WORKFLOW}`], + "workflow-query-failed", + ); + if (!isRecord(workflow) || !Number.isSafeInteger(workflow.id) || workflow.id < 1) { + fail("invalid-workflow-response"); + } + const run = runJson( + ["api", `repos/${repository}/actions/runs/${runId}`], + "preflight-run-query-failed", + ); + verifyWorkflowRun(run, { workflowId: workflow.id, runId, releaseSha }); + const mainRef = runJson( + ["api", `repos/${repository}/git/ref/heads/main`], + "main-ref-query-failed", + ); + if (!isRecord(mainRef) || !isRecord(mainRef.object) || mainRef.object.sha !== releaseSha) { + fail("release-sha-is-not-current-main"); + } + const attestationPath = env.RELEASE_ATTESTATION_PATH ?? DEFAULT_ATTESTATION_PATH; + if (typeof attestationPath !== "string" || attestationPath.length === 0) { + fail("invalid-attestation-path"); + } + verifyAttestationDocument(readAttestation(attestationPath), { + repository, + runId, + releaseSha, + releaseVersion, + }); + return { releaseSha, releaseVersion, runId }; +}; + +const isMain = process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]; +if (isMain) { + try { + const attestation = verifyReleaseAttestation(); + if (process.env.RELEASE_ATTESTATION_WRITE_GITHUB_OUTPUT === "1") { + const outputPath = process.env.GITHUB_OUTPUT; + if (typeof outputPath !== "string" || outputPath.length === 0) { + fail("missing-github-output"); + } + appendFileSync( + outputPath, + `release_sha=${attestation.releaseSha}\nrelease_version=${attestation.releaseVersion}\n`, + { encoding: "utf8" }, + ); + } + console.log("reviewed crates package preflight attestation verified"); + } catch (error) { + const code = error instanceof ReleaseAttestationError ? error.code : "unexpected-failure"; + console.error(`release attestation verification failed: ${code}`); + process.exit(1); + } +} diff --git a/scripts/verify_release_attestation.test.mjs b/scripts/verify_release_attestation.test.mjs new file mode 100644 index 0000000..25adbfa --- /dev/null +++ b/scripts/verify_release_attestation.test.mjs @@ -0,0 +1,88 @@ +#!/usr/bin/env node +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + ReleaseAttestationError, + verifyAttestationDocument, + verifyWorkflowRun, +} from "./verify_release_attestation.mjs"; + +const releaseSha = "a".repeat(40); +const expected = Object.freeze({ + repository: "reallyme/cose", + runId: 123, + releaseSha, + releaseVersion: "0.2.0", +}); +const attestation = (overrides = {}) => ({ + schema: "reallyme.cose.crates_preflight.v1", + repository: expected.repository, + workflow: "crates-package-preflight.yml", + run_id: expected.runId, + run_attempt: 1, + release_sha: releaseSha, + version: expected.releaseVersion, + ...overrides, +}); +const workflowRun = (overrides = {}) => ({ + workflow_id: 456, + id: expected.runId, + event: "workflow_dispatch", + head_branch: "main", + head_sha: releaseSha, + status: "completed", + conclusion: "success", + run_attempt: 1, + path: ".github/workflows/crates-package-preflight.yml@refs/heads/main", + ...overrides, +}); + +test("reviewed attestation accepts the exact run, SHA, and version", () => { + assert.doesNotThrow(() => verifyAttestationDocument(attestation(), expected)); + assert.doesNotThrow(() => + verifyWorkflowRun(workflowRun(), { + workflowId: 456, + runId: expected.runId, + releaseSha, + }), + ); +}); + +test("attestation rejects mismatched inputs and unreviewed fields", () => { + for (const candidate of [ + attestation({ version: "0.2.1" }), + attestation({ release_sha: "b".repeat(40) }), + attestation({ run_id: 124 }), + attestation({ extra: true }), + ]) { + assert.throws( + () => verifyAttestationDocument(candidate, expected), + ReleaseAttestationError, + ); + } +}); + +test("failed, rerun, wrong-branch, and wrong-workflow runs fail closed", () => { + for (const candidate of [ + workflowRun({ conclusion: "failure" }), + workflowRun({ run_attempt: 2 }), + workflowRun({ head_branch: "feature" }), + workflowRun({ workflow_id: 457 }), + workflowRun({ path: ".github/workflows/other.yml@refs/heads/main" }), + ]) { + assert.throws( + () => + verifyWorkflowRun(candidate, { + workflowId: 456, + runId: expected.runId, + releaseSha, + }), + ReleaseAttestationError, + ); + } +}); diff --git a/scripts/verify_release_source.mjs b/scripts/verify_release_source.mjs new file mode 100644 index 0000000..6bb2e76 --- /dev/null +++ b/scripts/verify_release_source.mjs @@ -0,0 +1,117 @@ +#!/usr/bin/env node +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +import { appendFileSync, lstatSync, readFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const FULL_SHA_PATTERN = /^[0-9a-f]{40}$/u; +const VERSION_PATTERN = /^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$/u; +const MAX_COMMAND_OUTPUT_BYTES = 1_048_576; +const MAX_MANIFEST_BYTES = 65_536; +const PUBLISHABLE_MANIFESTS = Object.freeze([ + "crates/cose/Cargo.toml", + "crates/proto/Cargo.toml", +]); + +export class ReleaseSourceError extends Error { + constructor(code) { + super(code); + this.name = "ReleaseSourceError"; + this.code = code; + } +} + +const fail = (code) => { + throw new ReleaseSourceError(code); +}; + +const run = (command, arguments_, { capture = true } = {}) => { + const result = spawnSync(command, arguments_, { + encoding: "utf8", + maxBuffer: MAX_COMMAND_OUTPUT_BYTES, + stdio: capture ? ["ignore", "pipe", "ignore"] : "inherit", + }); + if (result.error !== undefined || result.status !== 0) { + fail("source-command-failed"); + } + return capture && typeof result.stdout === "string" ? result.stdout.trim() : ""; +}; + +const readManifestVersion = (path) => { + let status; + let contents; + try { + status = lstatSync(path); + if (status.isSymbolicLink() || !status.isFile() || status.size > MAX_MANIFEST_BYTES) { + fail("invalid-release-manifest"); + } + contents = readFileSync(path, "utf8"); + } catch (error) { + if (error instanceof ReleaseSourceError) { + throw error; + } + fail("invalid-release-manifest"); + } + const versions = [...contents.matchAll(/^version = "([^"]+)"$/gmu)]; + if (versions.length !== 1 || versions[0][1] === undefined) { + fail("invalid-release-manifest"); + } + return versions[0][1]; +}; + +export const verifyReleaseSource = ({ env = process.env } = {}) => { + const releaseSha = env.RELEASE_SHA; + const releaseVersion = env.RELEASE_VERSION; + if (typeof releaseSha !== "string" || !FULL_SHA_PATTERN.test(releaseSha)) { + fail("invalid-release-sha"); + } + if (typeof releaseVersion !== "string" || !VERSION_PATTERN.test(releaseVersion)) { + fail("invalid-release-version"); + } + if (env.GITHUB_SHA !== undefined && env.GITHUB_SHA !== releaseSha) { + fail("workflow-head-mismatch"); + } + if (run("git", ["rev-parse", "HEAD"]) !== releaseSha) { + fail("checkout-mismatch"); + } + run( + "git", + ["fetch", "--force", "--no-tags", "origin", "main:refs/remotes/origin/main"], + { capture: false }, + ); + if (run("git", ["rev-parse", "refs/remotes/origin/main"]) !== releaseSha) { + fail("origin-main-mismatch"); + } + for (const manifest of PUBLISHABLE_MANIFESTS) { + if (readManifestVersion(manifest) !== releaseVersion) { + fail("manifest-version-mismatch"); + } + } + return { releaseSha, releaseVersion }; +}; + +const isMain = process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1]; +if (isMain) { + try { + const identity = verifyReleaseSource(); + if (process.env.RELEASE_SOURCE_WRITE_GITHUB_OUTPUT === "1") { + const outputPath = process.env.GITHUB_OUTPUT; + if (typeof outputPath !== "string" || outputPath.length === 0) { + fail("missing-github-output"); + } + appendFileSync( + outputPath, + `release_sha=${identity.releaseSha}\nrelease_version=${identity.releaseVersion}\n`, + { encoding: "utf8" }, + ); + } + console.log("release source matches the workflow head, current main, and crate manifests"); + } catch (error) { + const code = error instanceof ReleaseSourceError ? error.code : "unexpected-failure"; + console.error(`release source verification failed: ${code}`); + process.exit(1); + } +} diff --git a/scripts/write_release_attestation.mjs b/scripts/write_release_attestation.mjs new file mode 100644 index 0000000..fa118cc --- /dev/null +++ b/scripts/write_release_attestation.mjs @@ -0,0 +1,69 @@ +#!/usr/bin/env node +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +import { mkdirSync, writeFileSync } from "node:fs"; + +const FULL_SHA_PATTERN = /^[0-9a-f]{40}$/u; +const POSITIVE_INTEGER_PATTERN = /^[1-9][0-9]*$/u; +const REPOSITORY_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u; +const VERSION_PATTERN = /^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$/u; +const OUTPUT_DIRECTORY = "release-attestation"; +const OUTPUT_PATH = `${OUTPUT_DIRECTORY}/crates-preflight.json`; + +const fail = (code) => { + console.error(`release attestation creation failed: ${code}`); + process.exit(1); +}; + +const parsePositiveInteger = (value, code) => { + if (typeof value !== "string" || !POSITIVE_INTEGER_PATTERN.test(value)) { + fail(code); + } + const parsed = Number(value); + if (!Number.isSafeInteger(parsed)) { + fail(code); + } + return parsed; +}; + +const repository = process.env.GITHUB_REPOSITORY; +const releaseSha = process.env.RELEASE_SHA; +const version = process.env.RELEASE_VERSION; +if (typeof repository !== "string" || !REPOSITORY_PATTERN.test(repository)) { + fail("invalid-repository"); +} +if (typeof releaseSha !== "string" || !FULL_SHA_PATTERN.test(releaseSha)) { + fail("invalid-release-sha"); +} +if (typeof version !== "string" || !VERSION_PATTERN.test(version)) { + fail("invalid-release-version"); +} +const runId = parsePositiveInteger(process.env.GITHUB_RUN_ID, "invalid-run-id"); +const runAttempt = parsePositiveInteger(process.env.GITHUB_RUN_ATTEMPT, "invalid-run-attempt"); +if (runAttempt !== 1) { + fail("rerun-cannot-create-release-attestation"); +} + +const attestation = { + schema: "reallyme.cose.crates_preflight.v1", + repository, + workflow: "crates-package-preflight.yml", + run_id: runId, + run_attempt: runAttempt, + release_sha: releaseSha, + version, +}; + +try { + mkdirSync(OUTPUT_DIRECTORY, { recursive: true }); + writeFileSync(OUTPUT_PATH, `${JSON.stringify(attestation, null, 2)}\n`, { + encoding: "utf8", + flag: "wx", + mode: 0o600, + }); +} catch { + fail("attestation-write-failed"); +} +console.log("reviewed crates package attestation written"); diff --git a/src/algorithm/map_from_cose.rs b/src/algorithm/map_from_cose.rs deleted file mode 100644 index 576b97a..0000000 --- a/src/algorithm/map_from_cose.rs +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved -// -// SPDX-License-Identifier: Apache-2.0 -use crate::CoseError; -use coset::iana; -use reallyme_crypto::core::Algorithm; - -/// Strict DID-scoped algorithm mapping. -pub fn algorithm_from_cose_alg(alg: &coset::Algorithm) -> Result { - match alg { - coset::Algorithm::Assigned(iana::Algorithm::EdDSA) => Ok(Algorithm::Ed25519), - - coset::Algorithm::Assigned(iana::Algorithm::ES256) => Ok(Algorithm::P256), - - coset::Algorithm::Assigned(iana::Algorithm::ES384) => Ok(Algorithm::P384), - - coset::Algorithm::Assigned(iana::Algorithm::ES512) => Ok(Algorithm::P521), - - coset::Algorithm::Assigned(iana::Algorithm::ES256K) => Ok(Algorithm::Secp256k1), - - _ => Err(CoseError::UnsupportedAlgorithm), - } -} diff --git a/src/algorithm/mod.rs b/src/algorithm/mod.rs deleted file mode 100644 index 7943648..0000000 --- a/src/algorithm/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved -// -// SPDX-License-Identifier: Apache-2.0 - -//! COSE algorithm mapping helpers. - -mod map_from_cose; - -pub use map_from_cose::algorithm_from_cose_alg; diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index e5652cd..0000000 --- a/src/error.rs +++ /dev/null @@ -1,91 +0,0 @@ -// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved -// -// SPDX-License-Identifier: Apache-2.0 - -use thiserror::Error; - -#[cfg(feature = "cose-crypto")] -use reallyme_crypto::dispatch::AlgorithmError; - -#[derive(Debug, Error, PartialEq, Eq)] -/// Error type for COSE encoding, signing, verification, key, and policy operations. -pub enum CoseError { - /// CBOR serialization or parsing failed. - #[error("cbor encoding/decoding error")] - Cbor, - - /// The requested algorithm is not supported by the current COSE mapping. - #[error("unsupported algorithm")] - UnsupportedAlgorithm, - - /// A COSE_Sign1 object did not contain an attached payload where one was required. - #[error("missing payload")] - MissingPayload, - - /// Signature verification failed or the signature encoding was invalid. - #[error("invalid signature")] - InvalidSignature, - - /// The configured cryptographic backend rejected the operation. - #[error("crypto error")] - Crypto, - - /// A Multikey value was malformed or could not be converted safely. - #[error("invalid multikey")] - InvalidMultikey, - - /// Required public or private key bytes were absent. - #[error("missing key material")] - MissingKeyMaterial, - - /// Key bytes were present but did not match the expected COSE key shape. - #[error("invalid key material")] - InvalidKeyMaterial, - - /// A policy required `kid` / key_id but the COSE object did not provide one. - #[error("missing kid")] - MissingKid, - - /// A `kid` was present, but the caller's resolver did not return a key. - #[error("key not resolved")] - KeyNotResolved, - - /// The COSE object is structurally invalid for the requested operation. - #[error("invalid COSE format")] - InvalidFormat, - - /// Encoded input exceeded the crate's deterministic resource limits. - #[error("resource limit exceeded")] - ResourceLimitExceeded, - - /// Encoded input used indefinite-length or otherwise non-canonical CBOR. - #[error("non-canonical CBOR")] - NonCanonicalCbor, - - /// Encoded input used CBOR tags outside this crate's supported profile. - #[error("unexpected CBOR tag")] - UnexpectedCborTag, - - /// Encoded input repeated a CBOR map label where uniqueness is required. - #[error("duplicate CBOR map label")] - DuplicateMapLabel, - - /// A critical protected header was present but is not supported. - #[error("unsupported critical header")] - UnsupportedCriticalHeader, - - /// An unprotected header carried fields that must be integrity protected. - #[error("unprotected header not allowed")] - UnprotectedHeaderNotAllowed, - - /// Private key material was required but absent. - #[error("missing private key material")] - MissingPrivateKey, -} - -#[cfg(feature = "cose-crypto")] -impl From for CoseError { - fn from(_: AlgorithmError) -> Self { - CoseError::Crypto - } -} diff --git a/src/key/convert.rs b/src/key/convert.rs deleted file mode 100644 index dfe426d..0000000 --- a/src/key/convert.rs +++ /dev/null @@ -1,459 +0,0 @@ -// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved -// -// SPDX-License-Identifier: Apache-2.0 -use crate::CoseError; -use ciborium::value::Value; -use coset::{iana, CborSerializable, CoseKey, CoseKeyBuilder, Label, RegisteredLabelWithPrivate}; -use reallyme_crypto::core::Algorithm; -use zeroize::Zeroizing; - -use crate::limits::validate_cose_key_bytes; - -const COMPRESSED_EC_POINT_PREFIX_LEN: usize = 1; -const COMPRESSED_EC_POINT_EVEN_PREFIX: u8 = 0x02; -const COMPRESSED_EC_POINT_ODD_PREFIX: u8 = 0x03; -const UNCOMPRESSED_EC_POINT_PREFIX: u8 = 0x04; -pub(crate) const P256_COORDINATE_BYTES: usize = 32; -pub(crate) const P384_COORDINATE_BYTES: usize = 48; -pub(crate) const P521_COORDINATE_BYTES: usize = 66; -const ED25519_PUBLIC_KEY_BYTES: usize = 32; -const ED25519_SECRET_KEY_BYTES: usize = 32; -const X25519_PUBLIC_KEY_BYTES: usize = 32; - -#[derive(Clone, Copy)] -struct Ec2Profile { - curve: iana::EllipticCurve, - alg: iana::Algorithm, - coordinate_len: usize, -} - -#[derive(Clone, Copy)] -enum KeyProfile { - Okp(OkpProfile), - Ec2(Ec2Profile), -} - -#[derive(Clone, Copy)] -struct OkpProfile { - alg: Option, - coordinate_len: usize, -} - -/// Decode a COSE_Key from canonical, untagged CBOR bytes. -pub fn cose_key_from_slice(bytes: &[u8]) -> Result { - validate_cose_key_bytes(bytes)?; - let key = CoseKey::from_slice(bytes).map_err(|_| CoseError::Cbor)?; - validate_cose_key_profile(&key)?; - Ok(key) -} - -/// Encode a COSE_Key to canonical CBOR bytes. -pub fn cose_key_to_vec(key: &CoseKey) -> Result, CoseError> { - let encoded = key.clone().to_vec().map_err(|_| CoseError::Cbor)?; - validate_cose_key_bytes(&encoded)?; - Ok(encoded) -} - -/// Build a COSE_Key from raw public key bytes -pub fn cose_key_from_public_bytes(alg: Algorithm, public_key: &[u8]) -> Result { - match alg { - Algorithm::Ed25519 => { - if public_key.len() != ED25519_PUBLIC_KEY_BYTES { - return Err(CoseError::InvalidKeyMaterial); - } - Ok(CoseKeyBuilder::new_okp_key() - .param( - iana::OkpKeyParameter::Crv as i64, - Value::Integer((iana::EllipticCurve::Ed25519 as i64).into()), - ) - .param( - iana::OkpKeyParameter::X as i64, - Value::Bytes(public_key.to_vec()), - ) - .algorithm(iana::Algorithm::EdDSA) - .build()) - } - - Algorithm::X25519 => { - if public_key.len() != X25519_PUBLIC_KEY_BYTES { - return Err(CoseError::InvalidKeyMaterial); - } - Ok(CoseKeyBuilder::new_okp_key() - .param( - iana::OkpKeyParameter::Crv as i64, - Value::Integer((iana::EllipticCurve::X25519 as i64).into()), - ) - .param( - iana::OkpKeyParameter::X as i64, - Value::Bytes(public_key.to_vec()), - ) - .build()) - } - - Algorithm::P256 | Algorithm::P384 | Algorithm::P521 | Algorithm::Secp256k1 => { - let profile = ec2_profile(alg)?; - Ok(ec2_public_key_builder(profile, public_key)? - .algorithm(profile.alg) - .build()) - } - - _ => Err(CoseError::UnsupportedAlgorithm), - } -} - -/// Extract raw public key bytes from a COSE_Key -pub fn cose_key_to_public_bytes(key: &CoseKey) -> Result, CoseError> { - let profile = validate_cose_key_profile(key)?; - match profile { - KeyProfile::Okp(profile) => { - let x = get_param_bytes(key, iana::OkpKeyParameter::X as i64) - .ok_or(CoseError::MissingKeyMaterial)?; - if x.len() != profile.coordinate_len { - return Err(CoseError::InvalidKeyMaterial); - } - - Ok(x.to_vec()) - } - - KeyProfile::Ec2(profile) => { - let x = get_param_bytes(key, iana::Ec2KeyParameter::X as i64) - .ok_or(CoseError::MissingKeyMaterial)?; - if x.len() != profile.coordinate_len { - return Err(CoseError::InvalidKeyMaterial); - } - - let y_val = get_param_value(key, iana::Ec2KeyParameter::Y as i64) - .ok_or(CoseError::MissingKeyMaterial)?; - - // EC2 compressed form: Y is a boolean "y_sign". - if let Some(y_sign) = y_val.as_bool() { - let prefix = if y_sign { - COMPRESSED_EC_POINT_ODD_PREFIX - } else { - COMPRESSED_EC_POINT_EVEN_PREFIX - }; - let len = x - .len() - .checked_add(COMPRESSED_EC_POINT_PREFIX_LEN) - .ok_or(CoseError::InvalidFormat)?; - let mut out = Vec::with_capacity(len); - out.push(prefix); - out.extend_from_slice(x); - return Ok(out); - } - - // EC2 uncompressed form: Y is bytes. - let y = y_val.as_bytes().ok_or(CoseError::InvalidFormat)?; - if y.len() != profile.coordinate_len { - return Err(CoseError::InvalidKeyMaterial); - } - - let len = x - .len() - .checked_add(y.len()) - .ok_or(CoseError::InvalidFormat)?; - let mut out = Vec::with_capacity(len); - out.extend_from_slice(x); - out.extend_from_slice(y); - Ok(out) - } - } -} - -/// Build a COSE_Key from raw private key bytes -pub fn cose_key_from_private_bytes( - alg: Algorithm, - private_key: &[u8], - public_key: Option<&[u8]>, -) -> Result { - match alg { - Algorithm::Ed25519 => { - if private_key.len() != ED25519_SECRET_KEY_BYTES { - return Err(CoseError::InvalidKeyMaterial); - } - let mut b = CoseKeyBuilder::new_okp_key() - .param( - iana::OkpKeyParameter::Crv as i64, - Value::Integer((iana::EllipticCurve::Ed25519 as i64).into()), - ) - .param( - iana::OkpKeyParameter::D as i64, - Value::Bytes(private_key.to_vec()), - ) - .algorithm(iana::Algorithm::EdDSA); - - if let Some(pk) = public_key { - b = b.param(iana::OkpKeyParameter::X as i64, Value::Bytes(pk.to_vec())); - } - - Ok(b.build()) - } - - Algorithm::P256 | Algorithm::P384 | Algorithm::P521 | Algorithm::Secp256k1 => { - let profile = ec2_profile(alg)?; - if private_key.len() != profile.coordinate_len { - return Err(CoseError::InvalidKeyMaterial); - } - let b = if let Some(pk) = public_key { - ec2_public_key_builder(profile, pk)? - .param( - iana::Ec2KeyParameter::D as i64, - Value::Bytes(private_key.to_vec()), - ) - .algorithm(profile.alg) - } else { - CoseKeyBuilder::new_ec2_priv_key( - profile.curve, - vec![], // x optional - vec![], // y optional - private_key.to_vec(), - ) - .algorithm(profile.alg) - }; - - Ok(b.build()) - } - - _ => Err(CoseError::UnsupportedAlgorithm), - } -} - -/// Extract raw private key bytes from a COSE_Key. -/// -/// The returned buffer zeroizes on drop because callers often need to hand -/// these bytes to a backend that accepts raw key material. -pub fn cose_key_to_private_bytes(key: &CoseKey) -> Result>, CoseError> { - validate_cose_key_profile(key)?; - let d = key - .params - .iter() - .find(|(l, _)| { - *l == Label::Int(iana::Ec2KeyParameter::D as i64) - || *l == Label::Int(iana::OkpKeyParameter::D as i64) - }) - .and_then(|(_, v)| v.as_bytes()) - .ok_or(CoseError::MissingKeyMaterial)?; - - Ok(Zeroizing::new(d.to_vec())) -} - -fn ec2_profile(alg: Algorithm) -> Result { - match alg { - Algorithm::P256 => Ok(Ec2Profile { - curve: iana::EllipticCurve::P_256, - alg: iana::Algorithm::ES256, - coordinate_len: P256_COORDINATE_BYTES, - }), - Algorithm::P384 => Ok(Ec2Profile { - curve: iana::EllipticCurve::P_384, - alg: iana::Algorithm::ES384, - coordinate_len: P384_COORDINATE_BYTES, - }), - Algorithm::P521 => Ok(Ec2Profile { - curve: iana::EllipticCurve::P_521, - alg: iana::Algorithm::ES512, - coordinate_len: P521_COORDINATE_BYTES, - }), - Algorithm::Secp256k1 => Ok(Ec2Profile { - curve: iana::EllipticCurve::Secp256k1, - alg: iana::Algorithm::ES256K, - coordinate_len: P256_COORDINATE_BYTES, - }), - _ => Err(CoseError::UnsupportedAlgorithm), - } -} - -fn validate_cose_key_profile(key: &CoseKey) -> Result { - match key.kty { - coset::RegisteredLabel::Assigned(iana::KeyType::OKP) => { - let crv = get_param_i64(key, iana::OkpKeyParameter::Crv as i64) - .ok_or(CoseError::InvalidFormat)?; - let profile = okp_profile(crv)?; - validate_key_algorithm(key, profile.alg)?; - validate_optional_param_len( - key, - iana::OkpKeyParameter::X as i64, - profile.coordinate_len, - )?; - validate_optional_param_len( - key, - iana::OkpKeyParameter::D as i64, - profile.coordinate_len, - )?; - - if get_param_bytes(key, iana::OkpKeyParameter::X as i64).is_none() - && get_param_bytes(key, iana::OkpKeyParameter::D as i64).is_none() - { - return Err(CoseError::MissingKeyMaterial); - } - - Ok(KeyProfile::Okp(profile)) - } - coset::RegisteredLabel::Assigned(iana::KeyType::EC2) => { - let crv = get_param_i64(key, iana::Ec2KeyParameter::Crv as i64) - .ok_or(CoseError::InvalidFormat)?; - let profile = ec2_profile_from_curve(crv)?; - validate_key_algorithm(key, Some(profile.alg))?; - validate_optional_param_len( - key, - iana::Ec2KeyParameter::X as i64, - profile.coordinate_len, - )?; - validate_optional_param_len( - key, - iana::Ec2KeyParameter::D as i64, - profile.coordinate_len, - )?; - - if let Some(y) = get_param_value(key, iana::Ec2KeyParameter::Y as i64) { - if y.as_bool().is_none() { - let y_bytes = y.as_bytes().ok_or(CoseError::InvalidFormat)?; - if !y_bytes.is_empty() && y_bytes.len() != profile.coordinate_len { - return Err(CoseError::InvalidKeyMaterial); - } - } - } - - if get_param_bytes(key, iana::Ec2KeyParameter::D as i64).is_none() - && (get_param_bytes(key, iana::Ec2KeyParameter::X as i64).is_none() - || get_param_value(key, iana::Ec2KeyParameter::Y as i64).is_none()) - { - return Err(CoseError::MissingKeyMaterial); - } - - Ok(KeyProfile::Ec2(profile)) - } - _ => Err(CoseError::UnsupportedAlgorithm), - } -} - -fn okp_profile(crv: i64) -> Result { - if crv == iana::EllipticCurve::Ed25519 as i64 { - return Ok(OkpProfile { - alg: Some(iana::Algorithm::EdDSA), - coordinate_len: ED25519_PUBLIC_KEY_BYTES, - }); - } - - if crv == iana::EllipticCurve::X25519 as i64 { - return Ok(OkpProfile { - alg: None, - coordinate_len: X25519_PUBLIC_KEY_BYTES, - }); - } - - Err(CoseError::UnsupportedAlgorithm) -} - -fn ec2_profile_from_curve(crv: i64) -> Result { - if crv == iana::EllipticCurve::P_256 as i64 { - return ec2_profile(Algorithm::P256); - } - - if crv == iana::EllipticCurve::P_384 as i64 { - return ec2_profile(Algorithm::P384); - } - - if crv == iana::EllipticCurve::P_521 as i64 { - return ec2_profile(Algorithm::P521); - } - - if crv == iana::EllipticCurve::Secp256k1 as i64 { - return ec2_profile(Algorithm::Secp256k1); - } - - Err(CoseError::UnsupportedAlgorithm) -} - -fn validate_key_algorithm( - key: &CoseKey, - expected: Option, -) -> Result<(), CoseError> { - match (&key.alg, expected) { - (None, _) => Ok(()), - (Some(RegisteredLabelWithPrivate::Assigned(actual)), Some(expected_alg)) - if *actual == expected_alg => - { - Ok(()) - } - _ => Err(CoseError::UnsupportedAlgorithm), - } -} - -fn validate_optional_param_len( - key: &CoseKey, - label: i64, - expected_len: usize, -) -> Result<(), CoseError> { - if let Some(bytes) = get_param_bytes(key, label) { - if !bytes.is_empty() && bytes.len() != expected_len { - return Err(CoseError::InvalidKeyMaterial); - } - } - - Ok(()) -} - -fn get_param_value(key: &CoseKey, label: i64) -> Option<&Value> { - key.params - .iter() - .find(|(candidate, _)| *candidate == Label::Int(label)) - .map(|(_, value)| value) -} - -fn get_param_bytes(key: &CoseKey, label: i64) -> Option<&Vec> { - get_param_value(key, label).and_then(|value| value.as_bytes()) -} - -fn get_param_i64(key: &CoseKey, label: i64) -> Option { - get_param_value(key, label) - .and_then(|value| value.as_integer()) - .and_then(|value| value.try_into().ok()) -} - -fn ec2_public_key_builder( - profile: Ec2Profile, - public_key: &[u8], -) -> Result { - let compressed_len = profile - .coordinate_len - .checked_add(COMPRESSED_EC_POINT_PREFIX_LEN) - .ok_or(CoseError::InvalidFormat)?; - let raw_len = profile - .coordinate_len - .checked_mul(2) - .ok_or(CoseError::InvalidFormat)?; - let uncompressed_len = raw_len - .checked_add(COMPRESSED_EC_POINT_PREFIX_LEN) - .ok_or(CoseError::InvalidFormat)?; - - match public_key.len() { - len if len == compressed_len - && (public_key[0] == COMPRESSED_EC_POINT_EVEN_PREFIX - || public_key[0] == COMPRESSED_EC_POINT_ODD_PREFIX) => - { - let y_sign = public_key[0] == COMPRESSED_EC_POINT_ODD_PREFIX; - let x = public_key[COMPRESSED_EC_POINT_PREFIX_LEN..compressed_len].to_vec(); - Ok(CoseKeyBuilder::new_ec2_pub_key_y_sign( - profile.curve, - x, - y_sign, - )) - } - len if len == raw_len => { - let x = public_key[..profile.coordinate_len].to_vec(); - let y = public_key[profile.coordinate_len..raw_len].to_vec(); - Ok(CoseKeyBuilder::new_ec2_pub_key(profile.curve, x, y)) - } - len if len == uncompressed_len && public_key[0] == UNCOMPRESSED_EC_POINT_PREFIX => { - let x_start = COMPRESSED_EC_POINT_PREFIX_LEN; - let y_start = x_start - .checked_add(profile.coordinate_len) - .ok_or(CoseError::InvalidFormat)?; - let x = public_key[x_start..y_start].to_vec(); - let y = public_key[y_start..uncompressed_len].to_vec(); - Ok(CoseKeyBuilder::new_ec2_pub_key(profile.curve, x, y)) - } - _ => Err(CoseError::InvalidKeyMaterial), - } -} diff --git a/src/key/derive_kid.rs b/src/key/derive_kid.rs deleted file mode 100644 index 4b9c698..0000000 --- a/src/key/derive_kid.rs +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved -// -// SPDX-License-Identifier: Apache-2.0 -use crate::CoseError; - -use coset::{iana, CborSerializable, CoseKey, CoseKeyBuilder, Label, RegisteredLabel}; -use reallyme_codec::cbor::sha2_256_content_hash; - -fn get_param_bytes(key: &CoseKey, label: i64) -> Option<&[u8]> { - key.params - .iter() - .find(|(l, _)| *l == Label::Int(label)) - .and_then(|(_, v)| v.as_bytes()) - .map(|v| v.as_slice()) -} - -fn get_param_bool(key: &CoseKey, label: i64) -> Option { - key.params - .iter() - .find(|(l, _)| *l == Label::Int(label)) - .and_then(|(_, v)| v.as_bool()) -} - -fn get_param_i64(key: &CoseKey, label: i64) -> Option { - key.params - .iter() - .find(|(l, _)| *l == Label::Int(label)) - .and_then(|(_, v)| v.as_integer()) - .and_then(|i| i.try_into().ok()) -} - -fn curve_from_i64(v: i64) -> Option { - if v == iana::EllipticCurve::Ed25519 as i64 { - Some(iana::EllipticCurve::Ed25519) - } else if v == iana::EllipticCurve::X25519 as i64 { - Some(iana::EllipticCurve::X25519) - } else if v == iana::EllipticCurve::P_256 as i64 { - Some(iana::EllipticCurve::P_256) - } else if v == iana::EllipticCurve::P_384 as i64 { - Some(iana::EllipticCurve::P_384) - } else if v == iana::EllipticCurve::P_521 as i64 { - Some(iana::EllipticCurve::P_521) - } else if v == iana::EllipticCurve::Secp256k1 as i64 { - Some(iana::EllipticCurve::Secp256k1) - } else { - None - } -} - -/// Derive `kid = SHA-256(canonical COSE_Key(public))`. -/// The input may contain private material; it is ignored for derivation. -pub fn derive_kid_from_cose_key_public(key: &CoseKey) -> Result, CoseError> { - let public_only: CoseKey = match key.kty { - RegisteredLabel::Assigned(iana::KeyType::OKP) => { - let crv_i = get_param_i64(key, iana::OkpKeyParameter::Crv as i64) - .ok_or(CoseError::InvalidFormat)?; - let crv = curve_from_i64(crv_i).ok_or(CoseError::UnsupportedAlgorithm)?; - - let x = get_param_bytes(key, iana::OkpKeyParameter::X as i64) - .ok_or(CoseError::MissingKeyMaterial)? - .to_vec(); - - // Minimal public-only OKP key: kty + crv + x (no kid/alg/ops/base_iv/d) - CoseKeyBuilder::new_okp_key() - .param( - iana::OkpKeyParameter::Crv as i64, - ciborium::value::Value::Integer((crv as i64).into()), - ) - .param( - iana::OkpKeyParameter::X as i64, - ciborium::value::Value::Bytes(x), - ) - .build() - } - - RegisteredLabel::Assigned(iana::KeyType::EC2) => { - let crv_i = get_param_i64(key, iana::Ec2KeyParameter::Crv as i64) - .ok_or(CoseError::InvalidFormat)?; - let crv = curve_from_i64(crv_i).ok_or(CoseError::UnsupportedAlgorithm)?; - - let x = get_param_bytes(key, iana::Ec2KeyParameter::X as i64) - .ok_or(CoseError::MissingKeyMaterial)? - .to_vec(); - - // If Y is boolean => compressed form (y_sign) - if let Some(y_sign) = get_param_bool(key, iana::Ec2KeyParameter::Y as i64) { - CoseKeyBuilder::new_ec2_pub_key_y_sign(crv, x, y_sign).build() - } else { - let y = get_param_bytes(key, iana::Ec2KeyParameter::Y as i64) - .ok_or(CoseError::MissingKeyMaterial)? - .to_vec(); - CoseKeyBuilder::new_ec2_pub_key(crv, x, y).build() - } - } - - _ => return Err(CoseError::UnsupportedAlgorithm), - }; - - let canonical = public_only.to_vec().map_err(|_| CoseError::Cbor)?; - - Ok(sha2_256_content_hash(&canonical).to_vec()) -} diff --git a/src/key/mod.rs b/src/key/mod.rs deleted file mode 100644 index 2cee9b0..0000000 --- a/src/key/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved -// -// SPDX-License-Identifier: Apache-2.0 - -//! COSE_Key encoding, extraction, and key identifier derivation. - -pub(crate) mod convert; -mod derive_kid; -#[cfg(feature = "cose-crypto")] -pub(crate) mod map_algorithm; - -pub use convert::{ - cose_key_from_private_bytes, cose_key_from_public_bytes, cose_key_from_slice, - cose_key_to_private_bytes, cose_key_to_public_bytes, cose_key_to_vec, -}; -pub use derive_kid::derive_kid_from_cose_key_public; diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index f6f0aa2..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved -// -// SPDX-License-Identifier: Apache-2.0 - -//! COSE helpers for ReallyMe identity software. -//! -//! The initial public surface is intentionally limited to COSE_Sign1 and -//! COSE_Key helpers. Unsupported COSE message families are documented in the -//! crate README and fail closed instead of being partially interpreted. -//! -//! # Example -//! -//! ``` -//! use reallyme_cose::{cose_sign1, cose_verify1_with_policy, Algorithm, CoseError, CosePolicy}; -//! use reallyme_crypto::dispatch::generate_keypair; -//! -//! fn sign_and_verify() -> Result<(), CoseError> { -//! let (public_key, private_key) = generate_keypair(Algorithm::Ed25519)?; -//! let kid = b"example-key"; -//! -//! let cose_bytes = cose_sign1(Algorithm::Ed25519, b"payload", &private_key, Some(kid))?; -//! let policy = CosePolicy { -//! require_kid: true, -//! allowed_algs: vec![Algorithm::Ed25519], -//! ..Default::default() -//! }; -//! -//! let verified = cose_verify1_with_policy(&cose_bytes, &policy, |requested_kid| { -//! (requested_kid == kid).then(|| public_key.clone()) -//! })?; -//! assert_eq!(verified.payload, b"payload"); -//! assert_eq!(verified.alg, Algorithm::Ed25519); -//! assert_eq!(verified.kid, kid); -//! Ok(()) -//! } -//! # sign_and_verify().unwrap(); -//! ``` - -/// Crypto algorithm selector used by the COSE public API. -/// -/// Consumers should import this re-export instead of depending directly on -/// `reallyme-crypto`; that keeps the algorithm type identical to the one used -/// by `reallyme-cose`. -pub use reallyme_crypto::core::Algorithm; - -/// COSE algorithm mapping helpers. -pub mod algorithm; -/// Typed COSE errors. -pub mod error; -pub use error::CoseError; - -/// Resource limits shared by COSE byte-boundary APIs. -pub mod limits; - -// --- COSE_Sign1 --- -pub mod sign1; -#[cfg(feature = "cose-crypto")] -pub use sign1::{ - cose_sign1, cose_sign1_detached, cose_sign1_detached_tagged, cose_sign1_detached_with_options, - cose_sign1_tagged, cose_sign1_with_options, cose_verify1, cose_verify1_detached, - cose_verify1_detached_with_metadata, cose_verify1_detached_with_policy, - cose_verify1_with_metadata, cose_verify1_with_policy, CoseSign1EncodeOptions, - VerifiedCoseSign1, VerifiedDetachedCoseSign1, -}; - -/// COSE semantic policy enforcement. -pub mod policy; -pub use policy::{validate_cose_sign1_policy, CosePolicy}; - -// --- COSE_Key --- -pub mod key; -pub use key::{ - cose_key_from_private_bytes, cose_key_from_public_bytes, cose_key_from_slice, - cose_key_to_private_bytes, cose_key_to_public_bytes, cose_key_to_vec, - derive_kid_from_cose_key_public, -}; - -/// COSE_Key and Multikey conversion helpers. -pub mod multikey; -pub use multikey::{cose_key_to_multikey, multikey_to_cose_key}; diff --git a/src/limits/mod.rs b/src/limits/mod.rs deleted file mode 100644 index 1494b8e..0000000 --- a/src/limits/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved -// -// SPDX-License-Identifier: Apache-2.0 - -//! Resource limits shared by COSE byte-boundary APIs. - -mod validate; - -pub use validate::{MAX_COSE_KEY_BYTES, MAX_COSE_SIGN1_BYTES, MAX_DETACHED_PAYLOAD_BYTES}; - -pub(crate) use validate::validate_cose_key_bytes; - -#[cfg(feature = "cose-crypto")] -pub(crate) use validate::{ - validate_cose_sign1_bytes_with_limit, validate_detached_payload, - validate_detached_payload_with_limit, validate_protected_header_bytes, -}; diff --git a/src/limits/validate.rs b/src/limits/validate.rs deleted file mode 100644 index 0f6eb02..0000000 --- a/src/limits/validate.rs +++ /dev/null @@ -1,376 +0,0 @@ -// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved -// -// SPDX-License-Identifier: Apache-2.0 - -//! Resource limits and deterministic CBOR boundary checks. - -use std::io::Cursor; - -use ciborium::{de::from_reader, value::Value}; - -use crate::CoseError; - -/// Maximum accepted encoded COSE_Sign1 size. -/// -/// The limit is intentionally small for identity credentials: callers that need -/// large payloads should use detached signing and bind the payload through an -/// application-level transport with its own resource policy. -pub const MAX_COSE_SIGN1_BYTES: usize = 65_536; - -/// Maximum accepted encoded COSE_Key size. -/// -/// Supported OKP and EC2 key shapes are compact. A larger object is treated as -/// hostile or outside this crate's supported public API surface. -pub const MAX_COSE_KEY_BYTES: usize = 16_384; - -/// Maximum accepted detached payload size for signing and verification. -pub const MAX_DETACHED_PAYLOAD_BYTES: usize = 1_048_576; - -const CBOR_INDEFINITE_ADDITIONAL_INFO: u8 = 0x1f; -const CBOR_ADDITIONAL_INFO_MASK: u8 = 0x1f; -const CBOR_MAJOR_TYPE_SHIFT: u8 = 5; -const CBOR_UINT_MAJOR: u8 = 0; -const CBOR_NEGATIVE_INT_MAJOR: u8 = 1; -const CBOR_BYTES_MAJOR: u8 = 2; -const CBOR_TEXT_MAJOR: u8 = 3; -const CBOR_ARRAY_MAJOR: u8 = 4; -const CBOR_MAP_MAJOR: u8 = 5; -const CBOR_TAG_MAJOR: u8 = 6; -const CBOR_SIMPLE_MAJOR: u8 = 7; -const CBOR_ONE_BYTE_LENGTH: u8 = 24; -const CBOR_TWO_BYTE_LENGTH: u8 = 25; -const CBOR_FOUR_BYTE_LENGTH: u8 = 26; -const CBOR_EIGHT_BYTE_LENGTH: u8 = 27; -const COSE_SIGN1_TAG: u64 = 18; -const MAX_CBOR_DEPTH: usize = 32; - -#[derive(Clone, Copy)] -pub(crate) enum CborTagPolicy { - #[cfg(feature = "cose-crypto")] - AllowCoseSign1Root, - RejectAllTags, -} - -#[cfg(feature = "cose-crypto")] -pub(crate) fn validate_cose_sign1_bytes_with_limit( - bytes: &[u8], - max_len: usize, -) -> Result<(), CoseError> { - validate_cbor_bytes(bytes, max_len, CborTagPolicy::AllowCoseSign1Root)?; - validate_cose_sign1_protected_header(bytes) -} - -pub(crate) fn validate_cose_key_bytes(bytes: &[u8]) -> Result<(), CoseError> { - validate_cbor_bytes(bytes, MAX_COSE_KEY_BYTES, CborTagPolicy::RejectAllTags) -} - -#[cfg(feature = "cose-crypto")] -pub(crate) fn validate_detached_payload(payload: &[u8]) -> Result<(), CoseError> { - validate_detached_payload_with_limit(payload, MAX_DETACHED_PAYLOAD_BYTES) -} - -#[cfg(feature = "cose-crypto")] -pub(crate) fn validate_detached_payload_with_limit( - payload: &[u8], - max_len: usize, -) -> Result<(), CoseError> { - if payload.len() > max_len { - return Err(CoseError::ResourceLimitExceeded); - } - - Ok(()) -} - -#[cfg(feature = "cose-crypto")] -pub(crate) fn validate_protected_header_bytes(bytes: &[u8]) -> Result<(), CoseError> { - if bytes.is_empty() { - return Ok(()); - } - - reject_indefinite_cbor(bytes)?; - - let value: Value = from_reader(Cursor::new(bytes)).map_err(|_| CoseError::Cbor)?; - if !matches!(value, Value::Map(_)) { - return Err(CoseError::InvalidFormat); - } - - validate_tags(&value, CborTagPolicy::RejectAllTags, 0, true) -} - -fn validate_cbor_bytes( - bytes: &[u8], - max_len: usize, - tag_policy: CborTagPolicy, -) -> Result<(), CoseError> { - if bytes.is_empty() { - return Err(CoseError::Cbor); - } - - if bytes.len() > max_len { - return Err(CoseError::ResourceLimitExceeded); - } - - reject_indefinite_cbor(bytes)?; - - let value: Value = from_reader(Cursor::new(bytes)).map_err(|_| CoseError::Cbor)?; - validate_tags(&value, tag_policy, 0, true) -} - -#[cfg(feature = "cose-crypto")] -fn validate_cose_sign1_protected_header(bytes: &[u8]) -> Result<(), CoseError> { - let value: Value = from_reader(Cursor::new(bytes)).map_err(|_| CoseError::Cbor)?; - let sign1 = match &value { - Value::Tag(tag, inner) if *tag == COSE_SIGN1_TAG => inner.as_ref(), - other => other, - }; - - let Value::Array(items) = sign1 else { - return Ok(()); - }; - - let Some(Value::Bytes(protected)) = items.first() else { - return Ok(()); - }; - - validate_protected_header_bytes(protected) -} - -fn reject_indefinite_cbor(bytes: &[u8]) -> Result<(), CoseError> { - let parsed_len = parse_cbor_item(bytes, 0, 0)?; - if parsed_len == bytes.len() { - Ok(()) - } else { - Err(CoseError::Cbor) - } -} - -fn parse_cbor_item(bytes: &[u8], offset: usize, depth: usize) -> Result { - if depth > MAX_CBOR_DEPTH { - return Err(CoseError::ResourceLimitExceeded); - } - - let first = *bytes.get(offset).ok_or(CoseError::Cbor)?; - let major = first >> CBOR_MAJOR_TYPE_SHIFT; - let additional = first & CBOR_ADDITIONAL_INFO_MASK; - let value_start = offset - .checked_add(1) - .ok_or(CoseError::ResourceLimitExceeded)?; - - if additional == CBOR_INDEFINITE_ADDITIONAL_INFO { - return match major { - CBOR_BYTES_MAJOR | CBOR_TEXT_MAJOR | CBOR_ARRAY_MAJOR | CBOR_MAP_MAJOR => { - Err(CoseError::NonCanonicalCbor) - } - _ => Err(CoseError::Cbor), - }; - } - - match major { - CBOR_UINT_MAJOR | CBOR_NEGATIVE_INT_MAJOR => { - read_argument(bytes, value_start, additional).map(|(_, next_offset)| next_offset) - } - CBOR_BYTES_MAJOR | CBOR_TEXT_MAJOR => { - let (len, data_offset) = read_argument(bytes, value_start, additional)?; - data_offset - .checked_add(len) - .filter(|end| *end <= bytes.len()) - .ok_or(CoseError::Cbor) - } - CBOR_ARRAY_MAJOR => { - let (len, mut next_offset) = read_argument(bytes, value_start, additional)?; - for _ in 0..len { - next_offset = parse_cbor_item(bytes, next_offset, next_depth(depth)?)?; - } - Ok(next_offset) - } - CBOR_MAP_MAJOR => { - let (len, mut next_offset) = read_argument(bytes, value_start, additional)?; - let mut key_ranges = Vec::new(); - for _ in 0..len { - let child_depth = next_depth(depth)?; - let key_start = next_offset; - let key_end = parse_cbor_item(bytes, next_offset, child_depth)?; - reject_duplicate_raw_key(bytes, &key_ranges, key_start, key_end)?; - key_ranges.push((key_start, key_end)); - next_offset = key_end; - next_offset = parse_cbor_item(bytes, next_offset, child_depth)?; - } - Ok(next_offset) - } - CBOR_TAG_MAJOR => { - let (_, next_offset) = read_argument(bytes, value_start, additional)?; - parse_cbor_item(bytes, next_offset, next_depth(depth)?) - } - CBOR_SIMPLE_MAJOR => parse_simple(bytes, value_start, additional), - _ => Err(CoseError::Cbor), - } -} - -fn read_argument(bytes: &[u8], offset: usize, additional: u8) -> Result<(usize, usize), CoseError> { - match additional { - value if value < CBOR_ONE_BYTE_LENGTH => Ok((usize::from(value), offset)), - CBOR_ONE_BYTE_LENGTH => { - let value = *bytes.get(offset).ok_or(CoseError::Cbor)?; - if value < CBOR_ONE_BYTE_LENGTH { - return Err(CoseError::NonCanonicalCbor); - } - Ok(( - usize::from(value), - offset - .checked_add(1) - .ok_or(CoseError::ResourceLimitExceeded)?, - )) - } - CBOR_TWO_BYTE_LENGTH => { - let end = offset - .checked_add(2) - .ok_or(CoseError::ResourceLimitExceeded)?; - let data = bytes.get(offset..end).ok_or(CoseError::Cbor)?; - let value = u16::from_be_bytes([data[0], data[1]]); - if value <= u16::from(u8::MAX) { - return Err(CoseError::NonCanonicalCbor); - } - Ok((usize::from(value), end)) - } - CBOR_FOUR_BYTE_LENGTH => { - let end = offset - .checked_add(4) - .ok_or(CoseError::ResourceLimitExceeded)?; - let data = bytes.get(offset..end).ok_or(CoseError::Cbor)?; - let value = u32::from_be_bytes([data[0], data[1], data[2], data[3]]); - if value <= u32::from(u16::MAX) { - return Err(CoseError::NonCanonicalCbor); - } - let len = usize::try_from(value).map_err(|_| CoseError::ResourceLimitExceeded)?; - Ok((len, end)) - } - CBOR_EIGHT_BYTE_LENGTH => { - let end = offset - .checked_add(8) - .ok_or(CoseError::ResourceLimitExceeded)?; - let data = bytes.get(offset..end).ok_or(CoseError::Cbor)?; - let value = u64::from_be_bytes([ - data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], - ]); - if value <= u64::from(u32::MAX) { - return Err(CoseError::NonCanonicalCbor); - } - let len = usize::try_from(value).map_err(|_| CoseError::ResourceLimitExceeded)?; - Ok((len, end)) - } - _ => Err(CoseError::Cbor), - } -} - -fn parse_simple(bytes: &[u8], offset: usize, additional: u8) -> Result { - match additional { - value if value < CBOR_ONE_BYTE_LENGTH => Ok(offset), - CBOR_ONE_BYTE_LENGTH => offset - .checked_add(1) - .filter(|end| *end <= bytes.len()) - .ok_or(CoseError::Cbor), - CBOR_TWO_BYTE_LENGTH => offset - .checked_add(2) - .filter(|end| *end <= bytes.len()) - .ok_or(CoseError::Cbor), - CBOR_FOUR_BYTE_LENGTH => offset - .checked_add(4) - .filter(|end| *end <= bytes.len()) - .ok_or(CoseError::Cbor), - CBOR_EIGHT_BYTE_LENGTH => offset - .checked_add(8) - .filter(|end| *end <= bytes.len()) - .ok_or(CoseError::Cbor), - _ => Err(CoseError::Cbor), - } -} - -fn validate_tags( - value: &Value, - tag_policy: CborTagPolicy, - depth: usize, - is_root: bool, -) -> Result<(), CoseError> { - if depth > MAX_CBOR_DEPTH { - return Err(CoseError::ResourceLimitExceeded); - } - - match value { - Value::Tag(tag, inner) => { - let root_cose_sign1 = is_root && tag_policy_allows_cose_sign1_root(tag_policy); - if !root_cose_sign1 || *tag != COSE_SIGN1_TAG { - return Err(CoseError::UnexpectedCborTag); - } - - validate_tags( - inner, - CborTagPolicy::RejectAllTags, - next_depth(depth)?, - false, - ) - } - Value::Array(values) => { - for item in values { - validate_tags(item, tag_policy, next_depth(depth)?, false)?; - } - Ok(()) - } - Value::Map(entries) => { - reject_duplicate_map_keys(entries)?; - for (key, value) in entries { - let child_depth = next_depth(depth)?; - validate_tags(key, tag_policy, child_depth, false)?; - validate_tags(value, tag_policy, child_depth, false)?; - } - Ok(()) - } - Value::Integer(_) - | Value::Bytes(_) - | Value::Float(_) - | Value::Text(_) - | Value::Bool(_) - | Value::Null => Ok(()), - _ => Err(CoseError::InvalidFormat), - } -} - -fn tag_policy_allows_cose_sign1_root(tag_policy: CborTagPolicy) -> bool { - match tag_policy { - #[cfg(feature = "cose-crypto")] - CborTagPolicy::AllowCoseSign1Root => true, - CborTagPolicy::RejectAllTags => false, - } -} - -fn reject_duplicate_map_keys(entries: &[(Value, Value)]) -> Result<(), CoseError> { - for (index, (key, _)) in entries.iter().enumerate() { - for (other_key, _) in entries.iter().skip(index.saturating_add(1)) { - if key == other_key { - return Err(CoseError::DuplicateMapLabel); - } - } - } - - Ok(()) -} - -fn reject_duplicate_raw_key( - bytes: &[u8], - prior_ranges: &[(usize, usize)], - key_start: usize, - key_end: usize, -) -> Result<(), CoseError> { - let key = bytes.get(key_start..key_end).ok_or(CoseError::Cbor)?; - for (prior_start, prior_end) in prior_ranges { - let prior = bytes.get(*prior_start..*prior_end).ok_or(CoseError::Cbor)?; - if prior == key { - return Err(CoseError::DuplicateMapLabel); - } - } - - Ok(()) -} - -fn next_depth(depth: usize) -> Result { - depth.checked_add(1).ok_or(CoseError::ResourceLimitExceeded) -} diff --git a/src/multikey/convert.rs b/src/multikey/convert.rs deleted file mode 100644 index d53031c..0000000 --- a/src/multikey/convert.rs +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved -// -// SPDX-License-Identifier: Apache-2.0 - -use coset::{iana, CoseKey, Label, RegisteredLabel}; - -use reallyme_crypto::core::Algorithm; - -use reallyme_codec::multikey::{encode_multikey, parse_multikey}; - -use crate::{cose_key_from_public_bytes, cose_key_to_public_bytes, CoseError}; - -/// Convert COSE_Key (public) → multikey string. -pub fn cose_key_to_multikey(key: &CoseKey) -> Result { - let alg = algorithm_from_cose_key(key)?; - - let public_key = cose_key_to_public_bytes(key)?; - - let codec_name = match alg { - Algorithm::Ed25519 => "ed25519-pub", - Algorithm::X25519 => "x25519-pub", - Algorithm::P256 => "p256-pub", - Algorithm::P384 => "p384-pub", - Algorithm::P521 => "p521-pub", - Algorithm::Secp256k1 => "secp256k1-pub", - // These algorithms are defined by the wider crypto plane, but this - // COSE bridge only maps the currently supported COSE_Key profiles. - // New algorithms should be added here only with an intentional - // COSE/COSE_Key representation and vectors. - Algorithm::MlDsa44 - | Algorithm::MlDsa65 - | Algorithm::MlDsa87 - | Algorithm::MlKem512 - | Algorithm::MlKem768 - | Algorithm::MlKem1024 - | Algorithm::XWing768 - | Algorithm::XWing1024 => return Err(CoseError::UnsupportedAlgorithm), - }; - - encode_multikey(codec_name, &public_key).map_err(|_| CoseError::InvalidFormat) -} - -/// Extract Algorithm from COSE_Key (strict) -fn algorithm_from_cose_key(key: &CoseKey) -> Result { - match key.kty { - RegisteredLabel::Assigned(iana::KeyType::OKP) => { - let crv = key - .params - .iter() - .find(|(l, _)| *l == Label::Int(iana::OkpKeyParameter::Crv as i64)) - .and_then(|(_, v)| v.as_integer()) - .and_then(|i| TryInto::::try_into(i).ok()) - .ok_or(CoseError::InvalidFormat)?; - - match crv { - x if x == iana::EllipticCurve::Ed25519 as i64 => Ok(Algorithm::Ed25519), - x if x == iana::EllipticCurve::X25519 as i64 => Ok(Algorithm::X25519), - _ => Err(CoseError::UnsupportedAlgorithm), - } - } - - RegisteredLabel::Assigned(iana::KeyType::EC2) => { - let crv = key - .params - .iter() - .find(|(l, _)| *l == Label::Int(iana::Ec2KeyParameter::Crv as i64)) - .and_then(|(_, v)| v.as_integer()) - .and_then(|i| TryInto::::try_into(i).ok()) - .ok_or(CoseError::InvalidFormat)?; - - match crv { - x if x == iana::EllipticCurve::P_256 as i64 => Ok(Algorithm::P256), - x if x == iana::EllipticCurve::P_384 as i64 => Ok(Algorithm::P384), - x if x == iana::EllipticCurve::P_521 as i64 => Ok(Algorithm::P521), - x if x == iana::EllipticCurve::Secp256k1 as i64 => Ok(Algorithm::Secp256k1), - _ => Err(CoseError::UnsupportedAlgorithm), - } - } - - _ => Err(CoseError::UnsupportedAlgorithm), - } -} - -/// Convert multikey string → COSE_Key (public only). -pub fn multikey_to_cose_key(multikey: &str) -> Result { - let parsed = parse_multikey(multikey).map_err(|_| CoseError::InvalidFormat)?; - - let alg = match parsed.codec_name { - "ed25519-pub" => Algorithm::Ed25519, - "x25519-pub" => Algorithm::X25519, - "p256-pub" => Algorithm::P256, - "p384-pub" => Algorithm::P384, - "p521-pub" => Algorithm::P521, - "secp256k1-pub" => Algorithm::Secp256k1, - // The multikey codec is valid, but this bridge does not currently map - // it into a deliberate COSE_Key representation. - _ => return Err(CoseError::UnsupportedAlgorithm), - }; - - // ParsedMultikey already exposes the raw public key bytes - cose_key_from_public_bytes(alg, &parsed.public_key) -} diff --git a/src/policy/validate.rs b/src/policy/validate.rs deleted file mode 100644 index b03150c..0000000 --- a/src/policy/validate.rs +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved -// -// SPDX-License-Identifier: Apache-2.0 -use crate::algorithm::algorithm_from_cose_alg; -use coset::CoseSign1; -use reallyme_crypto::core::Algorithm; - -use crate::limits::{MAX_COSE_SIGN1_BYTES, MAX_DETACHED_PAYLOAD_BYTES}; -use crate::CoseError; - -/// Verification policy for COSE_Sign1 byte-boundary APIs. -#[derive(Debug, Clone)] -pub struct CosePolicy { - /// Require a `kid` / key_id in protected header. - pub require_kid: bool, - - /// Allowed algorithms. Empty means any algorithm supported by this crate. - pub allowed_algs: Vec, - - /// Maximum accepted encoded COSE_Sign1 bytes at public verification APIs. - pub max_cose_sign1_bytes: usize, - - /// Maximum accepted detached payload bytes at detached verification APIs. - pub max_detached_payload_bytes: usize, -} - -impl Default for CosePolicy { - fn default() -> Self { - Self { - require_kid: false, - allowed_algs: Vec::new(), - max_cose_sign1_bytes: MAX_COSE_SIGN1_BYTES, - max_detached_payload_bytes: MAX_DETACHED_PAYLOAD_BYTES, - } - } -} - -/// Validate COSE_Sign1 header policy without performing cryptographic verification. -pub fn validate_cose_sign1_policy(cose: &CoseSign1, policy: &CosePolicy) -> Result<(), CoseError> { - // --- kid requirement --- - if policy.require_kid && cose.protected.header.key_id.is_empty() { - return Err(CoseError::MissingKid); - } - - // --- algorithm allow-list --- - if !policy.allowed_algs.is_empty() { - let cose_alg = cose - .protected - .header - .alg - .as_ref() - .ok_or(CoseError::UnsupportedAlgorithm)?; - - let alg = algorithm_from_cose_alg(cose_alg)?; - - if !policy.allowed_algs.contains(&alg) { - return Err(CoseError::UnsupportedAlgorithm); - } - } - - Ok(()) -} diff --git a/src/sign1/build_sig_structure.rs b/src/sign1/build_sig_structure.rs deleted file mode 100644 index 8ffd96f..0000000 --- a/src/sign1/build_sig_structure.rs +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved -// -// SPDX-License-Identifier: Apache-2.0 - -use reallyme_codec::cbor::{encode_dag_cbor, CborValue}; - -/// Build Sig_structure bytes per RFC 9052 §4.4. -pub(crate) fn build_sig_structure(protected_bytes: &[u8], payload: &[u8]) -> Vec { - let structure = CborValue::Array(vec![ - CborValue::String("Signature1".to_string()), - CborValue::Bytes(protected_bytes.to_vec()), - CborValue::Bytes(Vec::new()), - CborValue::Bytes(payload.to_vec()), - ]); - - encode_dag_cbor(&structure) -} diff --git a/src/sign1/mod.rs b/src/sign1/mod.rs deleted file mode 100644 index 8e02c05..0000000 --- a/src/sign1/mod.rs +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved -// -// SPDX-License-Identifier: Apache-2.0 - -//! COSE_Sign1 signing and verification. - -#[cfg(feature = "cose-crypto")] -mod build_sig_structure; -#[cfg(feature = "cose-crypto")] -mod convert_ecdsa_signature; -#[cfg(feature = "cose-crypto")] -mod sign; -#[cfg(feature = "cose-crypto")] -mod verify; - -#[cfg(feature = "cose-crypto")] -pub use sign::{ - cose_sign1, cose_sign1_detached, cose_sign1_detached_tagged, cose_sign1_detached_with_options, - cose_sign1_tagged, cose_sign1_with_options, CoseSign1EncodeOptions, -}; -#[cfg(feature = "cose-crypto")] -pub use verify::{ - cose_verify1, cose_verify1_detached, cose_verify1_detached_with_metadata, - cose_verify1_detached_with_policy, cose_verify1_with_metadata, cose_verify1_with_policy, - VerifiedCoseSign1, VerifiedDetachedCoseSign1, -}; diff --git a/src/sign1/sign.rs b/src/sign1/sign.rs deleted file mode 100644 index 1bcf200..0000000 --- a/src/sign1/sign.rs +++ /dev/null @@ -1,196 +0,0 @@ -// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved -// -// SPDX-License-Identifier: Apache-2.0 - -use coset::{ - CborSerializable, CoseSign1, Header, ProtectedHeader, RegisteredLabelWithPrivate, - TaggedCborSerializable, -}; -use reallyme_crypto::core::Algorithm; -use reallyme_crypto::dispatch::sign; - -use crate::{key::map_algorithm::alg_to_cose, CoseError}; - -use super::build_sig_structure::build_sig_structure; -use super::convert_ecdsa_signature::cose_signature_from_backend; -use crate::limits::{ - validate_cose_sign1_bytes_with_limit, validate_detached_payload, MAX_COSE_SIGN1_BYTES, -}; - -/// Encoding controls for COSE_Sign1 signing APIs. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct CoseSign1EncodeOptions { - /// Emit the registered COSE_Sign1 root tag (18). - pub tag: bool, - - /// Maximum encoded COSE_Sign1 size accepted after signing. - pub max_cose_sign1_bytes: usize, -} - -impl Default for CoseSign1EncodeOptions { - fn default() -> Self { - Self { - tag: false, - max_cose_sign1_bytes: MAX_COSE_SIGN1_BYTES, - } - } -} - -/// Create COSE_Sign1 with an attached payload. -pub fn cose_sign1( - alg: Algorithm, - payload: &[u8], - private_key: &[u8], - kid: Option<&[u8]>, -) -> Result, CoseError> { - cose_sign1_with_options( - alg, - payload, - private_key, - kid, - CoseSign1EncodeOptions::default(), - ) -} - -/// Create tagged COSE_Sign1 with an attached payload. -pub fn cose_sign1_tagged( - alg: Algorithm, - payload: &[u8], - private_key: &[u8], - kid: Option<&[u8]>, -) -> Result, CoseError> { - cose_sign1_with_options( - alg, - payload, - private_key, - kid, - CoseSign1EncodeOptions { - tag: true, - ..CoseSign1EncodeOptions::default() - }, - ) -} - -/// Create COSE_Sign1 with an attached payload and explicit encoding options. -pub fn cose_sign1_with_options( - alg: Algorithm, - payload: &[u8], - private_key: &[u8], - kid: Option<&[u8]>, - options: CoseSign1EncodeOptions, -) -> Result, CoseError> { - validate_detached_payload(payload)?; - let protected = build_protected_header(alg, kid)?; - let signature = sign_payload(alg, private_key, &protected, payload)?; - - let cose = CoseSign1 { - protected, - unprotected: Header::default(), - payload: Some(payload.to_vec()), - signature, - }; - - encode_cose_sign1(cose, options) -} - -/// Create COSE_Sign1 with a detached payload. -pub fn cose_sign1_detached( - alg: Algorithm, - payload: &[u8], - private_key: &[u8], - kid: Option<&[u8]>, -) -> Result, CoseError> { - cose_sign1_detached_with_options( - alg, - payload, - private_key, - kid, - CoseSign1EncodeOptions::default(), - ) -} - -/// Create tagged COSE_Sign1 with a detached payload. -pub fn cose_sign1_detached_tagged( - alg: Algorithm, - payload: &[u8], - private_key: &[u8], - kid: Option<&[u8]>, -) -> Result, CoseError> { - cose_sign1_detached_with_options( - alg, - payload, - private_key, - kid, - CoseSign1EncodeOptions { - tag: true, - ..CoseSign1EncodeOptions::default() - }, - ) -} - -/// Create COSE_Sign1 with a detached payload and explicit encoding options. -pub fn cose_sign1_detached_with_options( - alg: Algorithm, - payload: &[u8], - private_key: &[u8], - kid: Option<&[u8]>, - options: CoseSign1EncodeOptions, -) -> Result, CoseError> { - validate_detached_payload(payload)?; - let protected = build_protected_header(alg, kid)?; - let signature = sign_payload(alg, private_key, &protected, payload)?; - - let cose = CoseSign1 { - protected, - unprotected: Header::default(), - payload: None, - signature, - }; - - encode_cose_sign1(cose, options) -} - -fn build_protected_header( - alg: Algorithm, - kid: Option<&[u8]>, -) -> Result { - let cose_alg = alg_to_cose(alg)?; - let header = Header { - alg: Some(RegisteredLabelWithPrivate::Assigned(cose_alg)), - key_id: kid.map(<[u8]>::to_vec).unwrap_or_default(), - ..Default::default() - }; - - Ok(ProtectedHeader { - header, - original_data: None, - }) -} - -fn sign_payload( - alg: Algorithm, - private_key: &[u8], - protected: &ProtectedHeader, - payload: &[u8], -) -> Result, CoseError> { - let protected_bytes = protected.clone().to_vec().map_err(|_| CoseError::Cbor)?; - let to_sign = build_sig_structure(&protected_bytes, payload); - - let backend_signature = sign(alg, private_key, &to_sign).map_err(|_| CoseError::Crypto)?; - cose_signature_from_backend(alg, backend_signature).map_err(|_| CoseError::Crypto) -} - -fn encode_cose_sign1( - cose: CoseSign1, - options: CoseSign1EncodeOptions, -) -> Result, CoseError> { - let encoded = if options.tag { - cose.to_tagged_vec() - } else { - cose.to_vec() - } - .map_err(|_| CoseError::Cbor)?; - - validate_cose_sign1_bytes_with_limit(&encoded, options.max_cose_sign1_bytes)?; - Ok(encoded) -} diff --git a/src/sign1/verify.rs b/src/sign1/verify.rs deleted file mode 100644 index bb9572f..0000000 --- a/src/sign1/verify.rs +++ /dev/null @@ -1,200 +0,0 @@ -// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved -// -// SPDX-License-Identifier: Apache-2.0 - -use coset::{CborSerializable, CoseSign1, TaggedCborSerializable}; -use reallyme_crypto::core::Algorithm; -use reallyme_crypto::dispatch::verify; - -use crate::policy::{validate_cose_sign1_policy, CosePolicy}; -use crate::{key::map_algorithm::cose_to_alg, CoseError}; - -use super::build_sig_structure::build_sig_structure; -use super::convert_ecdsa_signature::backend_signature_from_cose; -use crate::limits::{ - validate_cose_sign1_bytes_with_limit, validate_detached_payload_with_limit, - validate_protected_header_bytes, -}; - -/// Verified COSE_Sign1 attached payload and protected-header metadata. -pub struct VerifiedCoseSign1 { - /// Verified attached payload bytes. - pub payload: Vec, - - /// Verified protected-header algorithm. - pub alg: Algorithm, - - /// Verified protected-header key identifier. - pub kid: Vec, -} - -/// Verified COSE_Sign1 protected-header metadata for detached payloads. -pub struct VerifiedDetachedCoseSign1 { - /// Verified protected-header algorithm. - pub alg: Algorithm, - - /// Verified protected-header key identifier. - pub kid: Vec, -} - -/// Verify COSE_Sign1 with an attached payload. -pub fn cose_verify1( - cose_bytes: &[u8], - public_key_resolver: impl Fn(&[u8]) -> Option>, -) -> Result, CoseError> { - let verified = - cose_verify1_with_policy(cose_bytes, &CosePolicy::default(), public_key_resolver)?; - Ok(verified.payload) -} - -/// Verify COSE_Sign1 with an attached payload and return verified metadata. -pub fn cose_verify1_with_metadata( - cose_bytes: &[u8], - public_key_resolver: impl Fn(&[u8]) -> Option>, -) -> Result { - cose_verify1_with_policy(cose_bytes, &CosePolicy::default(), public_key_resolver) -} - -/// Verify COSE_Sign1 with an attached payload under an explicit policy. -pub fn cose_verify1_with_policy( - cose_bytes: &[u8], - policy: &CosePolicy, - public_key_resolver: impl Fn(&[u8]) -> Option>, -) -> Result { - validate_cose_sign1_bytes_with_limit(cose_bytes, policy.max_cose_sign1_bytes)?; - let cose = decode_cose_sign1(cose_bytes)?; - let payload = cose.payload.as_ref().ok_or(CoseError::MissingPayload)?; - - let metadata = verify_cose_signature(&cose, payload, policy, public_key_resolver)?; - - Ok(VerifiedCoseSign1 { - payload: payload.clone(), - alg: metadata.alg, - kid: metadata.kid, - }) -} - -/// Verify COSE_Sign1 with a detached payload. -pub fn cose_verify1_detached( - cose_bytes: &[u8], - payload: &[u8], - public_key_resolver: impl Fn(&[u8]) -> Option>, -) -> Result<(), CoseError> { - cose_verify1_detached_with_policy( - cose_bytes, - payload, - &CosePolicy::default(), - public_key_resolver, - ) - .map(|_| ()) -} - -/// Verify COSE_Sign1 with a detached payload and return verified metadata. -pub fn cose_verify1_detached_with_metadata( - cose_bytes: &[u8], - payload: &[u8], - public_key_resolver: impl Fn(&[u8]) -> Option>, -) -> Result { - cose_verify1_detached_with_policy( - cose_bytes, - payload, - &CosePolicy::default(), - public_key_resolver, - ) -} - -/// Verify COSE_Sign1 with a detached payload under an explicit policy. -pub fn cose_verify1_detached_with_policy( - cose_bytes: &[u8], - payload: &[u8], - policy: &CosePolicy, - public_key_resolver: impl Fn(&[u8]) -> Option>, -) -> Result { - validate_cose_sign1_bytes_with_limit(cose_bytes, policy.max_cose_sign1_bytes)?; - validate_detached_payload_with_limit(payload, policy.max_detached_payload_bytes)?; - let cose = decode_cose_sign1(cose_bytes)?; - - if cose.payload.is_some() { - return Err(CoseError::InvalidFormat); - } - - verify_cose_signature(&cose, payload, policy, public_key_resolver) -} - -/// Decode untagged COSE_Sign1 bytes, or bytes carrying the registered -/// COSE_Sign1 tag (18) already allowed by the byte-boundary tag policy. -fn decode_cose_sign1(cose_bytes: &[u8]) -> Result { - CoseSign1::from_slice(cose_bytes) - .or_else(|_| CoseSign1::from_tagged_slice(cose_bytes)) - .map_err(|_| CoseError::Cbor) -} - -fn verify_cose_signature( - cose: &CoseSign1, - payload: &[u8], - policy: &CosePolicy, - public_key_resolver: impl Fn(&[u8]) -> Option>, -) -> Result { - validate_cose_sign1_structure(cose)?; - validate_cose_sign1_policy(cose, policy)?; - - let cose_alg = cose - .protected - .header - .alg - .as_ref() - .ok_or(CoseError::UnsupportedAlgorithm)?; - let alg = cose_to_alg(cose_alg)?; - - let kid: &[u8] = &cose.protected.header.key_id; - let public_key = public_key_resolver(kid).ok_or_else(|| key_resolution_error(kid))?; - - // RFC 9052 §4.4: the Sig_structure must carry the protected header bstr - // exactly as received, not a re-encoding of the parsed header. - let protected_bytes = match &cose.protected.original_data { - Some(original) => original.clone(), - None => cose - .protected - .clone() - .to_vec() - .map_err(|_| CoseError::Cbor)?, - }; - let to_verify = build_sig_structure(&protected_bytes, payload); - let backend_signature = backend_signature_from_cose(alg, &cose.signature)?; - - verify(alg, &public_key, &to_verify, &backend_signature) - .map_err(|_| CoseError::InvalidSignature)?; - - Ok(VerifiedDetachedCoseSign1 { - alg, - kid: kid.to_vec(), - }) -} - -fn key_resolution_error(kid: &[u8]) -> CoseError { - if kid.is_empty() { - CoseError::MissingKid - } else { - CoseError::KeyNotResolved - } -} - -fn validate_cose_sign1_structure(cose: &CoseSign1) -> Result<(), CoseError> { - if let Some(protected_bytes) = &cose.protected.original_data { - validate_protected_header_bytes(protected_bytes)?; - } - - if !cose.protected.header.crit.is_empty() { - return Err(CoseError::UnsupportedCriticalHeader); - } - - if cose.unprotected.alg.is_some() || !cose.unprotected.key_id.is_empty() { - return Err(CoseError::UnprotectedHeaderNotAllowed); - } - - if !cose.unprotected.crit.is_empty() { - return Err(CoseError::UnsupportedCriticalHeader); - } - - Ok(()) -} diff --git a/tests/cose_suite/conformance_vector_tests.rs b/tests/cose_suite/conformance_vector_tests.rs deleted file mode 100644 index 52c931a..0000000 --- a/tests/cose_suite/conformance_vector_tests.rs +++ /dev/null @@ -1,263 +0,0 @@ -#![allow(missing_docs, clippy::expect_used, clippy::panic, clippy::unwrap_used)] -// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved -// -// SPDX-License-Identifier: Apache-2.0 - -use reallyme_cose::{ - cose_key_from_public_bytes, cose_key_from_slice, cose_key_to_multikey, - cose_key_to_public_bytes, cose_key_to_vec, cose_verify1, cose_verify1_detached, Algorithm, - CoseError, -}; -use serde::Deserialize; - -const COSE_SIGN1_VECTORS: &str = include_str!("../../conformance/vectors/cose-sign1.json"); -const COSE_KEY_VECTORS: &str = include_str!("../../conformance/vectors/cose-key.json"); -const VECTOR_MANIFEST: &str = include_str!("../../conformance/vectors/manifest.json"); - -#[derive(Debug, Deserialize)] -struct Manifest { - suites: Vec, -} - -#[derive(Debug, Deserialize)] -struct ManifestSuite { - id: String, - case_count: usize, -} - -#[test] -fn manifest_case_counts_match_suites() { - let manifest: Manifest = serde_json::from_str(VECTOR_MANIFEST).expect("manifest must parse"); - let sign1: CoseSign1Suite = - serde_json::from_str(COSE_SIGN1_VECTORS).expect("COSE_Sign1 vectors must parse"); - let key: CoseKeySuite = - serde_json::from_str(COSE_KEY_VECTORS).expect("COSE_Key vectors must parse"); - - for suite in manifest.suites { - let actual = match suite.id.as_str() { - "cose-sign1" => sign1.cases.len(), - "cose-key" => key.cases.len(), - other => panic!("unknown manifest suite: {other}"), - }; - assert_eq!(suite.case_count, actual, "case count for {}", suite.id); - } -} - -#[derive(Debug, Deserialize)] -struct CoseSign1Suite { - cases: Vec, -} - -#[derive(Debug, Deserialize)] -struct CoseSign1Case { - id: String, - operation: String, - algorithm: String, - kid_hex: String, - public_key_hex: String, - payload_hex: String, - cose_sign1_hex: String, - expected_error: Option, -} - -#[derive(Debug, Deserialize)] -struct CoseKeySuite { - cases: Vec, -} - -#[derive(Debug, Deserialize)] -struct CoseKeyCase { - id: String, - algorithm: String, - public_key_hex: String, - cose_key_hex: String, - multikey: String, -} - -#[test] -fn portable_cose_sign1_vectors_verify() { - let suite: CoseSign1Suite = - serde_json::from_str(COSE_SIGN1_VECTORS).expect("COSE_Sign1 vectors must parse"); - - for case in suite.cases { - let kid = decode_hex(&case.kid_hex); - let public_key = decode_hex(&case.public_key_hex); - let payload = decode_hex(&case.payload_hex); - let cose = decode_hex(&case.cose_sign1_hex); - - let result = match case.operation.as_str() { - "verify_attached" => cose_verify1(&cose, |requested_kid| { - resolve_expected_kid(requested_kid, &kid, &public_key) - }) - .map(|verified_payload| { - assert_eq!(verified_payload, payload, "{}", case.id); - }), - "verify_detached" => cose_verify1_detached(&cose, &payload, |requested_kid| { - resolve_expected_kid(requested_kid, &kid, &public_key) - }), - _ => panic!("unknown vector operation: {}", case.operation), - }; - - // The algorithm field is asserted to be a supported name; the - // algorithm actually used comes from the message's protected header. - let _ = algorithm_from_name(&case.algorithm); - assert_vector_result(&case.id, result, case.expected_error.as_deref()); - } -} - -#[test] -fn portable_cose_key_vectors_roundtrip() { - let suite: CoseKeySuite = - serde_json::from_str(COSE_KEY_VECTORS).expect("COSE_Key vectors must parse"); - - for case in suite.cases { - let algorithm = algorithm_from_name(&case.algorithm); - let public_key = decode_hex(&case.public_key_hex); - let cose_key_bytes = decode_hex(&case.cose_key_hex); - - let decoded_key = cose_key_from_slice(&cose_key_bytes).expect("COSE_Key must decode"); - assert_eq!( - cose_key_to_public_bytes(&decoded_key).expect("public key must extract"), - public_key, - "{}", - case.id - ); - assert_eq!( - cose_key_to_vec(&decoded_key).expect("COSE_Key must re-encode"), - cose_key_bytes, - "{}", - case.id - ); - assert_eq!( - cose_key_to_multikey(&decoded_key).expect("multikey must encode"), - case.multikey, - "{}", - case.id - ); - - let rebuilt_key = - cose_key_from_public_bytes(algorithm, &public_key).expect("COSE_Key must rebuild"); - assert_eq!( - cose_key_to_vec(&rebuilt_key).expect("rebuilt COSE_Key must encode"), - cose_key_bytes, - "{}", - case.id - ); - } -} - -#[test] -fn portable_cose_key_vectors_roundtrip_reallyme_codec() { - // Exercise the multikey strings directly at the reallyme-codec layer, - // independent of this crate's COSE_Key conversion functions. - use reallyme_codec::multikey::{encode_multikey, parse_multikey}; - - let suite: CoseKeySuite = - serde_json::from_str(COSE_KEY_VECTORS).expect("COSE_Key vectors must parse"); - - for case in suite.cases { - let public_key = decode_hex(&case.public_key_hex); - let codec_name = multikey_codec_name(&case.algorithm); - - assert_eq!( - encode_multikey(codec_name, &public_key).expect("codec multikey must encode"), - case.multikey, - "{}", - case.id - ); - - let parsed = parse_multikey(&case.multikey).expect("codec multikey must parse"); - assert_eq!(parsed.codec_name, codec_name, "{}", case.id); - assert_eq!(parsed.public_key, public_key, "{}", case.id); - } -} - -fn multikey_codec_name(algorithm: &str) -> &'static str { - match algorithm { - "Ed25519" => "ed25519-pub", - "X25519" => "x25519-pub", - "P256" => "p256-pub", - "P384" => "p384-pub", - "P521" => "p521-pub", - "Secp256k1" => "secp256k1-pub", - other => panic!("unsupported algorithm name: {other}"), - } -} - -fn resolve_expected_kid( - requested_kid: &[u8], - expected_kid: &[u8], - public_key: &[u8], -) -> Option> { - if requested_kid == expected_kid { - Some(public_key.to_vec()) - } else { - None - } -} - -fn assert_vector_result(id: &str, result: Result<(), CoseError>, expected_error: Option<&str>) { - match expected_error { - Some(name) => { - assert_eq!( - result.expect_err(id), - cose_error_from_name(id, name), - "{id}" - ); - } - None => result.expect(id), - } -} - -fn cose_error_from_name(id: &str, name: &str) -> CoseError { - match name { - "InvalidSignature" => CoseError::InvalidSignature, - "MissingKid" => CoseError::MissingKid, - "KeyNotResolved" => CoseError::KeyNotResolved, - "MissingPayload" => CoseError::MissingPayload, - "InvalidFormat" => CoseError::InvalidFormat, - "UnsupportedAlgorithm" => CoseError::UnsupportedAlgorithm, - "UnsupportedCriticalHeader" => CoseError::UnsupportedCriticalHeader, - "UnprotectedHeaderNotAllowed" => CoseError::UnprotectedHeaderNotAllowed, - "ResourceLimitExceeded" => CoseError::ResourceLimitExceeded, - "NonCanonicalCbor" => CoseError::NonCanonicalCbor, - "UnexpectedCborTag" => CoseError::UnexpectedCborTag, - "Cbor" => CoseError::Cbor, - other => panic!("unsupported expected error in vector {id}: {other}"), - } -} - -fn algorithm_from_name(name: &str) -> Algorithm { - match name { - "Ed25519" => Algorithm::Ed25519, - "P256" => Algorithm::P256, - "P384" => Algorithm::P384, - "P521" => Algorithm::P521, - "Secp256k1" => Algorithm::Secp256k1, - "X25519" => Algorithm::X25519, - _ => panic!("unsupported algorithm name: {name}"), - } -} - -fn decode_hex(input: &str) -> Vec { - assert_eq!(input.len() % 2, 0, "hex input must have an even length"); - - input - .as_bytes() - .chunks_exact(2) - .map(|chunk| { - let high = decode_hex_nibble(chunk[0]); - let low = decode_hex_nibble(chunk[1]); - (high << 4) | low - }) - .collect() -} - -fn decode_hex_nibble(byte: u8) -> u8 { - match byte { - b'0'..=b'9' => byte - b'0', - b'a'..=b'f' => byte - b'a' + 10, - b'A'..=b'F' => byte - b'A' + 10, - _ => panic!("invalid hex character"), - } -} diff --git a/tests/cose_suite/cose_key_tests.rs b/tests/cose_suite/cose_key_tests.rs deleted file mode 100644 index 188d25b..0000000 --- a/tests/cose_suite/cose_key_tests.rs +++ /dev/null @@ -1,99 +0,0 @@ -#![allow(missing_docs, clippy::expect_used, clippy::unwrap_used)] -// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved -// -// SPDX-License-Identifier: Apache-2.0 - -use reallyme_cose::{cose_key_from_public_bytes, cose_key_to_public_bytes, Algorithm}; - -use super::support::{gen_ed25519, gen_p256, gen_p384, gen_p521, gen_secp256k1}; - -#[test] -fn cose_key_ed25519_roundtrip() { - let k = gen_ed25519(); - - let cose_key = cose_key_from_public_bytes(k.alg, &k.public).unwrap(); - - let out = cose_key_to_public_bytes(&cose_key).unwrap(); - - assert_eq!(out, k.public); -} - -#[test] -fn cose_key_p256_roundtrip() { - let k = gen_p256(); - - let cose_key = cose_key_from_public_bytes(k.alg, &k.public).unwrap(); - - let out = cose_key_to_public_bytes(&cose_key).unwrap(); - - assert_eq!(out, k.public); -} - -#[test] -fn cose_key_p384_roundtrip() { - let k = gen_p384(); - - let cose_key = cose_key_from_public_bytes(k.alg, &k.public).unwrap(); - - let out = cose_key_to_public_bytes(&cose_key).unwrap(); - - assert_eq!(out, k.public); -} - -#[test] -fn cose_key_p521_roundtrip() { - let k = gen_p521(); - - let cose_key = cose_key_from_public_bytes(k.alg, &k.public).unwrap(); - - let out = cose_key_to_public_bytes(&cose_key).unwrap(); - - assert_eq!(out, k.public); -} - -#[test] -fn cose_key_secp256k1_roundtrip() { - let k = gen_secp256k1(); - - let cose_key = cose_key_from_public_bytes(k.alg, &k.public).unwrap(); - - let out = cose_key_to_public_bytes(&cose_key).unwrap(); - - assert_eq!(out, k.public); -} - -#[test] -fn cose_key_rejects_invalid_ec_length() { - let bad = vec![0u8; 10]; - - let res = cose_key_from_public_bytes(Algorithm::P256, &bad); - - assert!(res.is_err()); -} - -#[test] -fn cose_key_rejects_unsupported_algorithm() { - let k = gen_ed25519(); - - let res = cose_key_from_public_bytes(Algorithm::MlDsa87, &k.public); - - assert!(res.is_err()); -} - -#[test] -fn cose_key_rejects_wrong_length_ed25519_public() { - use reallyme_cose::CoseError; - - for len in [0_usize, 31, 33] { - let res = cose_key_from_public_bytes(Algorithm::Ed25519, &vec![7_u8; len]); - assert_eq!(res.unwrap_err(), CoseError::InvalidKeyMaterial, "len {len}"); - } -} - -#[test] -fn cose_key_rejects_wrong_length_x25519_public() { - use reallyme_cose::CoseError; - - let res = cose_key_from_public_bytes(Algorithm::X25519, &[7_u8; 31]); - assert_eq!(res.unwrap_err(), CoseError::InvalidKeyMaterial); -} diff --git a/tests/cose_suite/cose_private_key_tests.rs b/tests/cose_suite/cose_private_key_tests.rs deleted file mode 100644 index 7eeb61a..0000000 --- a/tests/cose_suite/cose_private_key_tests.rs +++ /dev/null @@ -1,113 +0,0 @@ -#![allow(missing_docs, clippy::expect_used, clippy::unwrap_used)] -// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved -// -// SPDX-License-Identifier: Apache-2.0 - -use reallyme_cose::{ - cose_key_from_private_bytes, cose_key_from_public_bytes, cose_key_to_private_bytes, Algorithm, -}; - -use super::support::{gen_ed25519, gen_p256, gen_p384, gen_p521, gen_secp256k1}; - -#[test] -fn cose_key_ed25519_private_roundtrip() { - let k = gen_ed25519(); - - let cose_key = cose_key_from_private_bytes(k.alg, &k.private, Some(&k.public)).unwrap(); - - let out = cose_key_to_private_bytes(&cose_key).unwrap(); - - assert_eq!(out.as_slice(), k.private.as_slice()); -} - -#[test] -fn cose_key_p256_private_roundtrip() { - let k = gen_p256(); - - let cose_key = cose_key_from_private_bytes(k.alg, &k.private, Some(&k.public)).unwrap(); - - let out = cose_key_to_private_bytes(&cose_key).unwrap(); - - assert_eq!(out.as_slice(), k.private.as_slice()); -} - -#[test] -fn cose_key_p384_private_roundtrip() { - let k = gen_p384(); - - let cose_key = cose_key_from_private_bytes(k.alg, &k.private, Some(&k.public)).unwrap(); - - let out = cose_key_to_private_bytes(&cose_key).unwrap(); - - assert_eq!(out.as_slice(), k.private.as_slice()); -} - -#[test] -fn cose_key_p521_private_roundtrip() { - let k = gen_p521(); - - let cose_key = cose_key_from_private_bytes(k.alg, &k.private, Some(&k.public)).unwrap(); - - let out = cose_key_to_private_bytes(&cose_key).unwrap(); - - assert_eq!(out.as_slice(), k.private.as_slice()); -} - -#[test] -fn cose_key_secp256k1_private_roundtrip() { - let k = gen_secp256k1(); - - let cose_key = cose_key_from_private_bytes(k.alg, &k.private, Some(&k.public)).unwrap(); - - let out = cose_key_to_private_bytes(&cose_key).unwrap(); - - assert_eq!(out.as_slice(), k.private.as_slice()); -} - -#[test] -fn cose_key_private_without_public_is_allowed() { - let k = gen_ed25519(); - - let cose_key = cose_key_from_private_bytes(k.alg, &k.private, None).unwrap(); - - let out = cose_key_to_private_bytes(&cose_key).unwrap(); - - assert_eq!(out.as_slice(), k.private.as_slice()); -} - -#[test] -fn cose_key_private_missing_d_is_rejected() { - let k = gen_ed25519(); - - // build a public-only COSE_Key - let cose_key = cose_key_from_public_bytes(k.alg, &k.public).unwrap(); - - let res = cose_key_to_private_bytes(&cose_key); - - assert!(res.is_err()); -} - -#[test] -fn cose_key_private_rejects_unsupported_algorithm() { - let k = gen_ed25519(); - - let res = cose_key_from_private_bytes(Algorithm::MlDsa87, &k.private, Some(&k.public)); - - assert!(res.is_err()); -} - -#[test] -fn cose_key_rejects_wrong_length_ed25519_private() { - use reallyme_cose::CoseError; - - let res = cose_key_from_private_bytes(Algorithm::Ed25519, &[7_u8; 31], None); - assert_eq!(res.unwrap_err(), CoseError::InvalidKeyMaterial); -} - -#[test] -fn cose_key_rejects_wrong_length_ec2_private() { - use reallyme_cose::CoseError; - - let res = cose_key_from_private_bytes(Algorithm::P256, &[7_u8; 31], None); - assert_eq!(res.unwrap_err(), CoseError::InvalidKeyMaterial); -} diff --git a/tests/cose_suite/support.rs b/tests/cose_suite/support.rs deleted file mode 100644 index 603a35b..0000000 --- a/tests/cose_suite/support.rs +++ /dev/null @@ -1,84 +0,0 @@ -#![allow(missing_docs, clippy::expect_used, clippy::unwrap_used)] -#![allow(dead_code)] -// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved -// -// SPDX-License-Identifier: Apache-2.0 - -use reallyme_cose::Algorithm; -use reallyme_crypto::dispatch::generate_keypair; - -#[derive(Debug)] -pub struct TestKey { - pub alg: Algorithm, - pub public: Vec, - pub private: Vec, -} - -pub fn gen_ed25519() -> TestKey { - let (public, private) = generate_keypair(Algorithm::Ed25519).unwrap(); - - TestKey { - alg: Algorithm::Ed25519, - public, - private: private.to_vec(), - } -} - -pub fn gen_p256() -> TestKey { - let (public, private) = generate_keypair(Algorithm::P256).unwrap(); - - TestKey { - alg: Algorithm::P256, - public, - private: private.to_vec(), - } -} - -pub fn gen_p384() -> TestKey { - let (public, private) = generate_keypair(Algorithm::P384).unwrap(); - - TestKey { - alg: Algorithm::P384, - public, - private: private.to_vec(), - } -} - -pub fn gen_p521() -> TestKey { - let (public, private) = generate_keypair(Algorithm::P521).unwrap(); - - TestKey { - alg: Algorithm::P521, - public, - private: private.to_vec(), - } -} - -pub fn gen_secp256k1() -> TestKey { - let (public, private) = generate_keypair(Algorithm::Secp256k1).unwrap(); - - TestKey { - alg: Algorithm::Secp256k1, - public, - private: private.to_vec(), - } -} - -pub fn gen_x25519() -> TestKey { - let (public, private) = generate_keypair(Algorithm::X25519).unwrap(); - - TestKey { - alg: Algorithm::X25519, - public, - private: private.to_vec(), - } -} - -pub fn sample_payload() -> Vec { - b"hello cose world".to_vec() -} - -/// Shared test `kid` -pub fn test_kid() -> &'static [u8] { - b"test-key" -} diff --git a/tools/vector-audit/Cargo.lock b/tools/vector-audit/Cargo.lock index 18322d1..52399fa 100644 --- a/tools/vector-audit/Cargo.lock +++ b/tools/vector-audit/Cargo.lock @@ -2,11 +2,63 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" +dependencies = [ + "crypto-common 0.2.2", + "inout", +] + +[[package]] +name = "aes" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +dependencies = [ + "cipher", + "cpubits", + "cpufeatures 0.3.0", + "zeroize", +] + +[[package]] +name = "aes-gcm" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aes-kw" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ac571010bd60765c56085a4f1d412012a9be2663b1a2f2b19b49318653fd0d" +dependencies = [ + "aes", + "zeroize", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + [[package]] name = "base16ct" -version = "0.2.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" [[package]] name = "base64ct" @@ -20,7 +72,16 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array", + "generic-array 0.14.7", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", ] [[package]] @@ -65,11 +126,34 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "inout", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "const-oid" -version = "0.9.6" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cpubits" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" [[package]] name = "cpufeatures" @@ -80,6 +164,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crunchy" version = "0.2.4" @@ -88,11 +181,15 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" -version = "0.5.5" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" dependencies = [ - "generic-array", + "cpubits", + "ctutils", + "getrandom", + "hybrid-array", + "num-traits", "rand_core", "subtle", "zeroize", @@ -100,24 +197,54 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "generic-array", + "generic-array 0.14.7", "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "getrandom", + "hybrid-array", + "rand_core", +] + +[[package]] +name = "ctr" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", + "subtle", +] + [[package]] name = "curve25519-dalek" -version = "4.1.3" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.3.0", "curve25519-dalek-derive", - "digest", + "digest 0.11.3", "fiat-crypto", "rustc_version", "subtle", @@ -137,9 +264,9 @@ dependencies = [ [[package]] name = "der" -version = "0.7.10" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" dependencies = [ "const-oid", "pem-rfc7468", @@ -152,45 +279,54 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", "const-oid", - "crypto-common", - "subtle", + "crypto-common 0.2.2", + "ctutils", ] [[package]] name = "ecdsa" -version = "0.16.9" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" dependencies = [ "der", - "digest", + "digest 0.11.3", "elliptic-curve", "rfc6979", "signature", "spki", + "zeroize", ] [[package]] name = "ed25519" -version = "2.2.3" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" dependencies = [ - "pkcs8", "signature", ] [[package]] name = "ed25519-dalek" -version = "2.2.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" dependencies = [ "curve25519-dalek", "ed25519", - "serde", "sha2", "subtle", "zeroize", @@ -198,17 +334,17 @@ dependencies = [ [[package]] name = "elliptic-curve" -version = "0.13.8" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "crypto-common 0.2.2", + "digest 0.11.3", "ff", - "generic-array", "group", - "hkdf", + "hybrid-array", "pem-rfc7468", "pkcs8", "rand_core", @@ -219,9 +355,9 @@ dependencies = [ [[package]] name = "ff" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" dependencies = [ "rand_core", "subtle", @@ -229,37 +365,56 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.9" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", - "zeroize", +] + +[[package]] +name = "generic-array" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab4e5aa225bc56696909483320f0ff9b600f1a971b52e07a17d70f3d9b43254b" +dependencies = [ + "rustversion", + "typenum", ] [[package]] name = "getrandom" -version = "0.2.17" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "wasi", + "r-efi", + "rand_core", +] + +[[package]] +name = "ghash" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" +dependencies = [ + "polyval", ] [[package]] name = "group" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" dependencies = [ "ff", "rand_core", @@ -284,21 +439,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "hkdf" -version = "0.12.4" +name = "hmac" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "hmac", + "digest 0.11.3", ] [[package]] -name = "hmac" -version = "0.12.1" +name = "hybrid-array" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ - "digest", + "ctutils", + "subtle", + "typenum", + "zeroize", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", ] [[package]] @@ -309,16 +476,46 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "k256" -version = "0.13.4" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +checksum = "93f50113171a713f4a4231ef82eb26703607139b35dcb56241f0ceab2ae1f7d8" dependencies = [ - "cfg-if", + "cpubits", "ecdsa", "elliptic-curve", - "once_cell", + "primeorder", "sha2", "signature", + "wnaf", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + +[[package]] +name = "kem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" +dependencies = [ + "crypto-common 0.2.2", + "rand_core", ] [[package]] @@ -333,6 +530,58 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "ml-dsa" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add6b9d92e496f16f4526d68ff29da1483aba4b119baeab8bed3b9e3544a6f3d" +dependencies = [ + "const-oid", + "crypto-common 0.2.2", + "ctutils", + "hybrid-array", + "module-lattice", + "pkcs8", + "shake", + "signature", +] + +[[package]] +name = "ml-kem" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66" +dependencies = [ + "hybrid-array", + "kem", + "module-lattice", + "pkcs8", + "rand_core", + "sha3 0.11.0", + "zeroize", +] + +[[package]] +name = "module-lattice" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" +dependencies = [ + "ctutils", + "hybrid-array", + "num-traits", + "zeroize", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -341,68 +590,100 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "p256" -version = "0.13.2" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" dependencies = [ "ecdsa", "elliptic-curve", + "primefield", "primeorder", "sha2", ] [[package]] name = "p384" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +checksum = "d17b851e6b3e378ab4ecb07fa2ed23f4d15f075735f8fec9fa1e7bdce5f8301f" dependencies = [ "ecdsa", "elliptic-curve", + "fiat-crypto", + "primefield", "primeorder", "sha2", ] [[package]] name = "p521" -version = "0.13.3" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +checksum = "4ad64cc32c2dc466317c12ee5853e61f159f9eab1fe7efade0395dc2e7b43449" dependencies = [ "base16ct", "ecdsa", "elliptic-curve", + "primefield", "primeorder", - "rand_core", "sha2", ] [[package]] name = "pem-rfc7468" -version = "0.7.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" dependencies = [ "base64ct", ] [[package]] name = "pkcs8" -version = "0.10.2" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" dependencies = [ "der", "spki", ] +[[package]] +name = "polyval" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" +dependencies = [ + "cpubits", + "cpufeatures 0.3.0", + "universal-hash", +] + +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint", + "crypto-common 0.2.2", + "ff", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "primeorder" -version = "0.13.6" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" dependencies = [ "elliptic-curve", + "once_cell", + "primefield", + "serdect", + "wnaf", ] [[package]] @@ -423,40 +704,50 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand_core" -version = "0.6.4" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "reallyme-cose-vector-audit" version = "0.1.1" dependencies = [ + "aes-gcm", + "aes-kw", "bs58", "ciborium", "ed25519-dalek", "hex", "k256", + "ml-dsa", + "ml-kem", "p256", "p384", "p521", "serde", "serde_json", + "sha2", + "sha3-kmac", "thiserror", + "zeroize", ] [[package]] name = "rfc6979" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" dependencies = [ + "crypto-bigint", "hmac", - "subtle", ] [[package]] @@ -468,16 +759,22 @@ dependencies = [ "semver", ] +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + [[package]] name = "sec1" -version = "0.7.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" dependencies = [ "base16ct", + "ctutils", "der", - "generic-array", - "pkcs8", + "hybrid-array", "subtle", "zeroize", ] @@ -531,37 +828,104 @@ dependencies = [ "zmij", ] +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha2" -version = "0.10.9" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak 0.1.6", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", +] + +[[package]] +name = "sha3-kmac" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b6b86fb906e40929519c1a38dc349a7f5595778cc00cc20b55eec34fddeb9ec" +dependencies = [ + "generic-array 1.4.4", + "sha3 0.10.9", + "sha3-utils", +] + +[[package]] +name = "sha3-utils" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08e0bf98cc082cbe077f06a707c94ca15796d046cc4cd2593167b5730a6727be" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "shake" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", + "sponge-cursor", ] [[package]] name = "signature" -version = "2.2.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" dependencies = [ - "digest", + "digest 0.11.3", "rand_core", ] [[package]] name = "spki" -version = "0.7.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" dependencies = [ "base64ct", "der", ] +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + [[package]] name = "subtle" version = "2.6.1" @@ -570,9 +934,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -626,6 +990,16 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "version_check" version = "0.9.5" @@ -633,10 +1007,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +name = "wnaf" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" +dependencies = [ + "ff", + "group", + "hybrid-array", +] [[package]] name = "zerocopy" @@ -666,6 +1045,6 @@ checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zmij" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd2f034a4bebf216c9e4b7083603e024cf930873fd67830cfb083c9fa33129d9" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/tools/vector-audit/Cargo.toml b/tools/vector-audit/Cargo.toml index 11e4c21..d0f4980 100644 --- a/tools/vector-audit/Cargo.toml +++ b/tools/vector-audit/Cargo.toml @@ -9,17 +9,41 @@ edition = "2021" rust-version = "1.96" publish = false +[lints] +workspace = true + [dependencies] +aes-gcm = "0.11" +aes-kw = { version = "0.3.1", default-features = false, features = ["zeroize"] } bs58 = "0.5" ciborium = "0.2" -ed25519-dalek = "2" +ed25519-dalek = "3" hex = "0.4" -k256 = { version = "0.13", features = ["ecdsa"] } -p256 = { version = "0.13", features = ["ecdsa"] } -p384 = { version = "0.13", features = ["ecdsa"] } -p521 = { version = "0.13", features = ["ecdsa"] } +k256 = { version = "0.14", features = ["ecdsa"] } +ml-kem = { version = "0.3.2", features = ["zeroize"] } +ml-dsa = "0.1.1" +p256 = { version = "0.14", features = ["ecdsa"] } +p384 = { version = "0.14", features = ["ecdsa"] } +p521 = { version = "0.14", features = ["ecdsa"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +sha2 = "0.11" +sha3-kmac = { version = "0.3.0", default-features = false } thiserror = "2.0" +zeroize = "1.8" [workspace] + +[workspace.lints.rust] +unsafe_code = "deny" + +[workspace.lints.clippy] +dbg_macro = "deny" +expect_used = "deny" +large_include_file = "deny" +panic = "deny" +todo = "deny" +unimplemented = "deny" +unreachable = "deny" +unwrap_used = "deny" +wildcard_imports = "deny" diff --git a/tools/vector-audit/src/main.rs b/tools/vector-audit/src/main.rs index 3ad6788..184c966 100644 --- a/tools/vector-audit/src/main.rs +++ b/tools/vector-audit/src/main.rs @@ -2,12 +2,13 @@ // // SPDX-License-Identifier: Apache-2.0 -//! Independent audit for committed COSE conformance vectors. +//! COSE-layer audit for committed conformance vectors. //! //! This binary intentionally does not depend on `reallyme-cose`, -//! `reallyme-crypto`, or `reallyme-codec`. It verifies the committed JSON -//! bytes with RustCrypto, `ciborium`, and `bs58` so vector regressions are not -//! masked by bugs shared with the production implementation. +//! `reallyme-crypto`, or `reallyme-codec`. Its CBOR parsing, COSE structure, +//! KDF, key-wrap, and Multikey checks are independent from production. Direct +//! RustCrypto dependencies provide the primitive oracle; primitive ACVP and +//! adversarial conformance remain owned by `reallyme-crypto`. use std::collections::HashSet; use std::fmt::{Display, Formatter, Write}; @@ -20,11 +21,16 @@ use ciborium::{de::from_reader, ser::into_writer}; use serde::Deserialize; use thiserror::Error; +mod ml_kem_encrypt; +mod pq; +mod verify_manifest; + const CASE_ID_BYTES: usize = 96; const CASE_ID_BYTES_U8: u8 = 96; -const SIGN1_FILE: &str = "conformance/vectors/cose-sign1.json"; -const KEY_FILE: &str = "conformance/vectors/cose-key.json"; -const MANIFEST_FILE: &str = "conformance/vectors/manifest.json"; +const SIGN1_FILE: &str = "vectors/cose-sign1.json"; +const KEY_FILE: &str = "vectors/cose-key.json"; +const ML_KEM_ENCRYPT_FILE: &str = "vectors/cose-encrypt-ml-kem.json"; +const MANIFEST_FILE: &str = "vectors/manifest.json"; #[derive(Debug, Error)] #[error("{context}: {reason}")] @@ -112,6 +118,8 @@ enum AuditReason { InvalidPublicKeyLength, #[error("private seed does not derive the stated public key")] SeedPublicMismatch, + #[error("external classical COSE_Sign1 vector is missing")] + ExternalSign1VectorMissing, #[error("COSE_Sign1 root is not an array")] Sign1RootNotArray, #[error("COSE_Sign1 array length is invalid")] @@ -134,8 +142,10 @@ enum AuditReason { SignatureDidNotVerify, #[error("negative signature vector verified independently")] InvalidSignatureVerified, - #[error("signature width is not RFC 9053 fixed-width r||s")] + #[error("signature width does not match the selected algorithm")] SignatureWidth, + #[error("invalid-encoding vector unexpectedly has the valid signature width")] + InvalidSignatureEncodingWidth, #[error("protected algorithm label mismatch")] ProtectedAlgorithmMismatch, #[error("protected kid mismatch")] @@ -188,14 +198,50 @@ enum AuditReason { MulticodecPrefix, #[error("multikey key bytes mismatch")] MultikeyBytes, - #[error("manifest references an unknown suite")] - UnknownManifestSuite, #[error("manifest case count mismatch")] ManifestCaseCount, + #[error("manifest suite set is incomplete or duplicated")] + ManifestSuiteSet, + #[error("manifest suite path mismatch")] + ManifestPath, + #[error("manifest suite digest encoding is invalid")] + ManifestDigestEncoding, + #[error("manifest suite digest mismatch")] + ManifestDigest, #[error("duplicate vector id")] DuplicateCaseId, #[error("integer conversion failed")] IntegerConversion, + #[error("COSE_Encrypt root tag or array shape is invalid")] + EncryptShape, + #[error("COSE_Encrypt protected header is invalid")] + EncryptProtectedHeader, + #[error("COSE_Encrypt unprotected header is invalid")] + EncryptUnprotectedHeader, + #[error("COSE_Recipient shape is invalid")] + RecipientShape, + #[error("COSE_Recipient protected header is invalid")] + RecipientProtectedHeader, + #[error("COSE_Recipient unprotected header is invalid")] + RecipientUnprotectedHeader, + #[error("ML-KEM vector mode is invalid")] + InvalidKemMode, + #[error("ML-KEM vector algorithm metadata is inconsistent")] + KemAlgorithmMismatch, + #[error("ML-KEM vector deterministic key or encapsulation mismatch")] + KemDeterministicMismatch, + #[error("ML-KEM vector decapsulation mismatch")] + KemDecapsulationMismatch, + #[error("ML-KEM vector kid does not bind the public COSE_Key")] + KemKidMismatch, + #[error("ML-KEM vector KDF failed")] + KemKdf, + #[error("ML-KEM vector AES-KW failed")] + KemKeyWrap, + #[error("ML-KEM vector content authentication failed")] + EncryptAuthentication, + #[error("ML-KEM vector plaintext mismatch")] + EncryptPlaintextMismatch, } type AuditResult = Result; @@ -216,6 +262,13 @@ struct Sign1Case { payload_hex: String, cose_sign1_hex: String, expected_error: Option, + provenance: Option, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +enum Provenance { + NodeOpenSslRfc8032, } #[derive(Debug, Deserialize)] @@ -232,17 +285,6 @@ struct KeyCase { multikey: String, } -#[derive(Debug, Deserialize)] -struct Manifest { - suites: Vec, -} - -#[derive(Debug, Deserialize)] -struct ManifestSuite { - id: String, - case_count: usize, -} - #[derive(Clone, Copy)] enum Algorithm { Ed25519, @@ -268,10 +310,10 @@ impl Algorithm { fn cose_alg(self) -> AuditResult { match self { - Self::Ed25519 => Ok(-8), - Self::P256 => Ok(-7), - Self::P384 => Ok(-35), - Self::P521 => Ok(-36), + Self::Ed25519 => Ok(-19), + Self::P256 => Ok(-9), + Self::P384 => Ok(-51), + Self::P521 => Ok(-52), Self::Secp256k1 => Ok(-47), Self::X25519 => Err(general(AuditReason::UnsupportedAlgorithm)), } @@ -306,8 +348,12 @@ fn main() -> ExitCode { match run() { Ok(summary) => { println!( - "vector audit passed: {} COSE_Sign1 cases, {} COSE_Key cases", - summary.sign1_cases, summary.key_cases + "vector audit passed: {} classical COSE_Sign1 cases, {} PQ COSE_Sign1 cases, {} classical COSE_Key cases, {} PQ COSE_Key cases, {} ML-KEM COSE_Encrypt cases", + summary.sign1_cases, + summary.pq_sign1_cases, + summary.key_cases, + summary.pq_key_cases, + summary.ml_kem_encrypt_cases ); ExitCode::SUCCESS } @@ -321,17 +367,40 @@ fn main() -> ExitCode { struct AuditSummary { sign1_cases: usize, key_cases: usize, + pq_sign1_cases: usize, + pq_key_cases: usize, + ml_kem_encrypt_cases: usize, } fn run() -> AuditResult { let repo_root = repo_root()?; let sign1: Sign1Suite = read_json(&repo_root, SIGN1_FILE, AuditContext::General)?; let keys: KeySuite = read_json(&repo_root, KEY_FILE, AuditContext::General)?; - let manifest: Manifest = read_json(&repo_root, MANIFEST_FILE, AuditContext::Manifest)?; - - audit_manifest(&manifest, sign1.cases.len(), keys.cases.len())?; + let ml_kem_encrypt: ml_kem_encrypt::Suite = + read_json(&repo_root, ML_KEM_ENCRYPT_FILE, AuditContext::General)?; + let manifest: verify_manifest::Manifest = + read_json(&repo_root, MANIFEST_FILE, AuditContext::Manifest)?; let mut ids = HashSet::new(); + let pq_summary = pq::audit_suites(&repo_root, &mut ids)?; + + verify_manifest::verify( + &repo_root, + &manifest, + sign1.cases.len(), + keys.cases.len(), + pq_summary.sign1_cases, + pq_summary.key_cases, + ml_kem_encrypt.cases.len(), + )?; + + ensure( + sign1.cases.iter().any(|case| { + case.provenance == Some(Provenance::NodeOpenSslRfc8032) && case.expected_error.is_none() + }), + AuditReason::ExternalSign1VectorMissing, + )?; + for case in &sign1.cases { audit_unique_id(&mut ids, &case.id)?; audit_sign1(case).map_err(|error| attach_case(error, &case.id))?; @@ -340,10 +409,14 @@ fn run() -> AuditResult { audit_unique_id(&mut ids, &case.id)?; audit_key(case).map_err(|error| attach_case(error, &case.id))?; } + ml_kem_encrypt::audit_suite(&ml_kem_encrypt, &mut ids)?; Ok(AuditSummary { sign1_cases: sign1.cases.len(), key_cases: keys.cases.len(), + pq_sign1_cases: pq_summary.sign1_cases, + pq_key_cases: pq_summary.key_cases, + ml_kem_encrypt_cases: ml_kem_encrypt.cases.len(), }) } @@ -370,23 +443,6 @@ fn read_json Deserialize<'de>>( }) } -fn audit_manifest(manifest: &Manifest, sign1_cases: usize, key_cases: usize) -> AuditResult<()> { - for suite in &manifest.suites { - let actual = match suite.id.as_str() { - "cose-sign1" => sign1_cases, - "cose-key" => key_cases, - _ => return Err(manifest_error(AuditReason::UnknownManifestSuite)), - }; - ensure(suite.case_count == actual, AuditReason::ManifestCaseCount).map_err(|error| { - AuditError { - context: AuditContext::Manifest, - reason: error.reason, - } - })?; - } - Ok(()) -} - fn audit_unique_id(ids: &mut HashSet, id: &str) -> AuditResult<()> { if ids.insert(id.to_owned()) { Ok(()) @@ -430,7 +486,7 @@ fn audit_sign1(case: &Sign1Case) -> AuditResult<()> { match case.expected_error.as_deref() { None => audit_happy_sign1(case, algorithm, &kid, &parsed, declared_signature_ok), - Some("InvalidSignature") => ensure( + Some("InvalidSignature" | "InvalidSignatureEncoding") => ensure( !declared_signature_ok, AuditReason::InvalidSignatureVerified, ), @@ -531,7 +587,7 @@ fn audit_key_resolution_negative( } fn audit_unsupported_algorithm_negative(parsed: &ParsedSign1) -> AuditResult<()> { - let supported = [-8_i64, -7, -35, -36, -47]; + let supported = [-19_i64, -9, -51, -52, -47, -48, -49, -50]; let alg = map_get(&parsed.protected_map, 1); let is_supported = matches!( alg, @@ -712,7 +768,7 @@ fn derived_public(algorithm: Algorithm, seed: &[u8]) -> AuditResult> { .map_err(|_| general(AuditReason::InvalidSeedLength))?; Ok(signing_key .verifying_key() - .to_encoded_point(true) + .to_sec1_point(true) .as_bytes() .to_vec()) } @@ -722,17 +778,17 @@ fn derived_public(algorithm: Algorithm, seed: &[u8]) -> AuditResult> { .map_err(|_| general(AuditReason::InvalidSeedLength))?; Ok(signing_key .verifying_key() - .to_encoded_point(true) + .to_sec1_point(true) .as_bytes() .to_vec()) } Algorithm::P521 => { - use p521::elliptic_curve::sec1::ToEncodedPoint; + use p521::elliptic_curve::sec1::ToSec1Point; let secret_key = p521::SecretKey::from_slice(seed) .map_err(|_| general(AuditReason::InvalidSeedLength))?; Ok(secret_key .public_key() - .to_encoded_point(true) + .to_sec1_point(true) .as_bytes() .to_vec()) } @@ -742,7 +798,7 @@ fn derived_public(algorithm: Algorithm, seed: &[u8]) -> AuditResult> { .map_err(|_| general(AuditReason::InvalidSeedLength))?; Ok(signing_key .verifying_key() - .to_encoded_point(true) + .to_sec1_point(true) .as_bytes() .to_vec()) } @@ -826,7 +882,7 @@ fn cose_key_profile(algorithm: Algorithm) -> AuditResult { Algorithm::Ed25519 => Ok(CoseKeyProfile { kty: 1, crv: 6, - alg: Some(-8), + alg: Some(-19), multicodec: 0xed, }), Algorithm::X25519 => Ok(CoseKeyProfile { @@ -838,19 +894,19 @@ fn cose_key_profile(algorithm: Algorithm) -> AuditResult { Algorithm::P256 => Ok(CoseKeyProfile { kty: 2, crv: 1, - alg: Some(-7), + alg: Some(-9), multicodec: 0x1200, }), Algorithm::P384 => Ok(CoseKeyProfile { kty: 2, crv: 2, - alg: Some(-35), + alg: Some(-51), multicodec: 0x1201, }), Algorithm::P521 => Ok(CoseKeyProfile { kty: 2, crv: 3, - alg: Some(-36), + alg: Some(-52), multicodec: 0x1202, }), Algorithm::Secp256k1 => Ok(CoseKeyProfile { diff --git a/tools/vector-audit/src/ml_kem_encrypt.rs b/tools/vector-audit/src/ml_kem_encrypt.rs new file mode 100644 index 0000000..ffdb4c2 --- /dev/null +++ b/tools/vector-audit/src/ml_kem_encrypt.rs @@ -0,0 +1,584 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Independent consumer for committed ReallyMe ML-KEM `COSE_Encrypt` vectors. +//! +//! This module parses the CBOR structure directly, recomputes the AKP key +//! binding, repeats deterministic encapsulation and decapsulation with the +//! upstream ML-KEM crate, derives KMAC256 output, performs RFC 3394 unwrap when +//! required, and authenticates AES-GCM without linking `reallyme-cose`, +//! `reallyme-crypto`, or `reallyme-codec`. + +use std::collections::HashSet; +use std::io::Cursor; + +use aes_gcm::aead::consts::U12; +use aes_gcm::aead::{Aead, KeyInit, Payload}; +use aes_gcm::aes::Aes192; +use aes_gcm::{Aes128Gcm, Aes256Gcm, AesGcm}; +use aes_kw::{KwAes128, KwAes192, KwAes256}; +use ciborium::value::Value; +use ml_kem::kem::{Decapsulate, KeyExport}; +use ml_kem::{Seed, B32}; +use serde::Deserialize; +use sha2::{Digest, Sha256}; +use sha3_kmac::Kmac256; +use zeroize::{Zeroize, Zeroizing}; + +use super::{ + attach_case, audit_unique_id, encode_cbor, ensure, general, integer_matches, map_get, + AuditReason, AuditResult, +}; + +const COSE_ENCRYPT_TAG: u64 = 96; +const COSE_KEY_TYPE_AKP: i64 = 7; +const COSE_HEADER_ALGORITHM: i64 = 1; +const COSE_HEADER_KID: i64 = 4; +const COSE_HEADER_IV: i64 = 5; +const REALLYME_HEADER_EK: i64 = -65_543; +const AES_GCM_TAG_BYTES: usize = 16; +const AES_KW_OVERHEAD: usize = 8; +const BITS_PER_BYTE: usize = 8; + +type Aes192Gcm = AesGcm; + +#[derive(Debug, Deserialize)] +pub(super) struct Suite { + pub(super) cases: Vec, +} + +#[derive(Debug, Deserialize)] +pub(super) struct Case { + id: String, + kem_algorithm: String, + mode: String, + content_algorithm: String, + private_key_seed_hex: String, + public_key_hex: String, + recipient_kid_hex: String, + encapsulation_randomness_hex: String, + iv_hex: String, + cek_hex: String, + plaintext_hex: String, + external_aad_hex: String, + supp_priv_info_hex: String, + cose_encrypt_hex: String, +} + +#[derive(Clone, Copy)] +enum Kem { + MlKem512, + MlKem768, + MlKem1024, +} + +impl Kem { + fn parse(value: &str) -> AuditResult { + match value { + "ML-KEM-512" => Ok(Self::MlKem512), + "ML-KEM-768" => Ok(Self::MlKem768), + "ML-KEM-1024" => Ok(Self::MlKem1024), + _ => Err(general(AuditReason::UnsupportedAlgorithm)), + } + } + + const fn direct_algorithm(self) -> i64 { + match self { + Self::MlKem512 => -65_537, + Self::MlKem768 => -65_538, + Self::MlKem1024 => -65_539, + } + } + + const fn wrapped_algorithm(self) -> i64 { + match self { + Self::MlKem512 => -65_540, + Self::MlKem768 => -65_541, + Self::MlKem1024 => -65_542, + } + } + + const fn key_wrap_algorithm(self) -> i64 { + match self { + Self::MlKem512 => -3, + Self::MlKem768 => -4, + Self::MlKem1024 => -5, + } + } + + const fn content_algorithm(self) -> i64 { + match self { + Self::MlKem512 => 1, + Self::MlKem768 => 2, + Self::MlKem1024 => 3, + } + } + + const fn content_algorithm_name(self) -> &'static str { + match self { + Self::MlKem512 => "A128GCM", + Self::MlKem768 => "A192GCM", + Self::MlKem1024 => "A256GCM", + } + } + + const fn key_length(self) -> usize { + match self { + Self::MlKem512 => 16, + Self::MlKem768 => 24, + Self::MlKem1024 => 32, + } + } +} + +#[derive(Clone, Copy)] +enum Mode { + Direct, + KeyWrap, +} + +impl Mode { + fn parse(value: &str) -> AuditResult { + match value { + "direct" => Ok(Self::Direct), + "key_wrap" => Ok(Self::KeyWrap), + _ => Err(general(AuditReason::InvalidKemMode)), + } + } +} + +struct ParsedEncrypt { + body_protected: Vec, + recipient_protected: Vec, + iv: Vec, + ciphertext: Vec, + encapsulated_key: Vec, + recipient_ciphertext: Option>, +} + +struct EncapsulationOutput { + public_key: Vec, + ciphertext: Vec, + shared_secret: Zeroizing>, +} + +pub(super) fn audit_suite(suite: &Suite, ids: &mut HashSet) -> AuditResult<()> { + for case in &suite.cases { + audit_unique_id(ids, &case.id)?; + audit_case(case).map_err(|error| attach_case(error, &case.id))?; + } + Ok(()) +} + +fn audit_case(case: &Case) -> AuditResult<()> { + let kem = Kem::parse(&case.kem_algorithm)?; + let mode = Mode::parse(&case.mode)?; + ensure( + case.content_algorithm == kem.content_algorithm_name(), + AuditReason::KemAlgorithmMismatch, + )?; + + let seed = decode_hex(&case.private_key_seed_hex)?; + let public_key = decode_hex(&case.public_key_hex)?; + let expected_kid = decode_hex(&case.recipient_kid_hex)?; + let randomness = decode_hex(&case.encapsulation_randomness_hex)?; + let expected_iv = decode_hex(&case.iv_hex)?; + let expected_cek = decode_hex(&case.cek_hex)?; + let expected_plaintext = decode_hex(&case.plaintext_hex)?; + let external_aad = decode_hex(&case.external_aad_hex)?; + let supp_priv_info = decode_hex(&case.supp_priv_info_hex)?; + let encoded = decode_hex(&case.cose_encrypt_hex)?; + + let parsed = parse_encrypt(&encoded, kem, mode, &expected_kid)?; + ensure( + parsed.iv == expected_iv, + AuditReason::EncryptUnprotectedHeader, + )?; + ensure( + parsed.ciphertext.len() >= AES_GCM_TAG_BYTES, + AuditReason::EncryptShape, + )?; + + let seed = fixed::<64>(&seed)?; + let randomness = fixed::<32>(&randomness)?; + let mut deterministic = deterministic_encapsulation(kem, &seed, &randomness)?; + ensure( + deterministic.public_key == public_key + && deterministic.ciphertext == parsed.encapsulated_key, + AuditReason::KemDeterministicMismatch, + )?; + let mut decapsulated = decapsulate(kem, &seed, &parsed.encapsulated_key)?; + ensure( + deterministic.shared_secret.as_slice() == decapsulated.as_slice(), + AuditReason::KemDecapsulationMismatch, + )?; + deterministic.shared_secret.zeroize(); + + let derived_kid = derive_kid(kem, &public_key)?; + ensure(derived_kid == expected_kid, AuditReason::KemKidMismatch)?; + + let content_key = match mode { + Mode::Direct => { + ensure(expected_cek.is_empty(), AuditReason::KemAlgorithmMismatch)?; + ensure( + parsed.recipient_ciphertext.is_none(), + AuditReason::RecipientShape, + )?; + derive_key( + &decapsulated, + kem.content_algorithm(), + kem.key_length(), + &parsed.recipient_protected, + &supp_priv_info, + )? + } + Mode::KeyWrap => { + let wrapped = parsed + .recipient_ciphertext + .as_deref() + .ok_or_else(|| general(AuditReason::RecipientShape))?; + let kek = derive_key( + &decapsulated, + kem.key_wrap_algorithm(), + kem.key_length(), + &parsed.recipient_protected, + &supp_priv_info, + )?; + let unwrapped = unwrap_key(kem, &kek, wrapped)?; + ensure( + unwrapped.as_slice() == expected_cek, + AuditReason::KemKeyWrap, + )?; + unwrapped + } + }; + decapsulated.zeroize(); + + let enc_structure = encode_cbor(&Value::Array(vec![ + Value::Text("Encrypt".to_owned()), + Value::Bytes(parsed.body_protected), + Value::Bytes(external_aad), + ]))?; + let plaintext = decrypt_content( + kem, + &content_key, + &parsed.iv, + &enc_structure, + &parsed.ciphertext, + )?; + ensure( + plaintext.as_slice() == expected_plaintext, + AuditReason::EncryptPlaintextMismatch, + ) +} + +fn parse_encrypt( + encoded: &[u8], + kem: Kem, + mode: Mode, + expected_kid: &[u8], +) -> AuditResult { + let root: Value = ciborium::de::from_reader(Cursor::new(encoded)) + .map_err(|_| general(AuditReason::CborDecode))?; + ensure( + encode_cbor(&root)?.as_slice() == encoded, + AuditReason::EncryptShape, + )?; + let array = match root { + Value::Tag(COSE_ENCRYPT_TAG, value) => match *value { + Value::Array(array) => array, + _ => return Err(general(AuditReason::EncryptShape)), + }, + _ => return Err(general(AuditReason::EncryptShape)), + }; + ensure(array.len() == 4, AuditReason::EncryptShape)?; + + let body_protected = bytes_at(&array, 0, AuditReason::EncryptProtectedHeader)?; + let body_map = decode_map(&body_protected, AuditReason::EncryptProtectedHeader)?; + ensure( + body_map.len() == 1 + && matches!( + map_get(&body_map, COSE_HEADER_ALGORITHM), + Some(value) if integer_matches(value, kem.content_algorithm()) + ), + AuditReason::EncryptProtectedHeader, + )?; + ensure( + encode_cbor(&Value::Map(body_map))? == body_protected, + AuditReason::EncryptProtectedHeader, + )?; + + let body_unprotected = map_at(&array, 1, AuditReason::EncryptUnprotectedHeader)?; + ensure( + body_unprotected.len() == 1, + AuditReason::EncryptUnprotectedHeader, + )?; + let iv = match map_get(&body_unprotected, COSE_HEADER_IV) { + Some(Value::Bytes(bytes)) if bytes.len() == 12 => bytes.clone(), + _ => return Err(general(AuditReason::EncryptUnprotectedHeader)), + }; + let ciphertext = bytes_at(&array, 2, AuditReason::EncryptShape)?; + + let recipients = match array.get(3) { + Some(Value::Array(recipients)) if recipients.len() == 1 => recipients, + _ => return Err(general(AuditReason::RecipientShape)), + }; + let recipient = match recipients.first() { + Some(Value::Array(recipient)) if recipient.len() == 3 => recipient, + _ => return Err(general(AuditReason::RecipientShape)), + }; + let recipient_protected = bytes_at(recipient, 0, AuditReason::RecipientProtectedHeader)?; + let recipient_map = decode_map(&recipient_protected, AuditReason::RecipientProtectedHeader)?; + let expected_recipient_algorithm = match mode { + Mode::Direct => kem.direct_algorithm(), + Mode::KeyWrap => kem.wrapped_algorithm(), + }; + ensure( + recipient_map.len() == 2 + && matches!( + map_get(&recipient_map, COSE_HEADER_ALGORITHM), + Some(value) if integer_matches(value, expected_recipient_algorithm) + ) + && matches!( + map_get(&recipient_map, COSE_HEADER_KID), + Some(Value::Bytes(kid)) if kid.as_slice() == expected_kid + ), + AuditReason::RecipientProtectedHeader, + )?; + ensure( + encode_cbor(&Value::Map(recipient_map))? == recipient_protected, + AuditReason::RecipientProtectedHeader, + )?; + + let recipient_unprotected = map_at(recipient, 1, AuditReason::RecipientUnprotectedHeader)?; + ensure( + recipient_unprotected.len() == 1, + AuditReason::RecipientUnprotectedHeader, + )?; + let encapsulated_key = match map_get(&recipient_unprotected, REALLYME_HEADER_EK) { + Some(Value::Bytes(bytes)) => bytes.clone(), + _ => return Err(general(AuditReason::RecipientUnprotectedHeader)), + }; + let recipient_ciphertext = match recipient.get(2) { + Some(Value::Null) => None, + Some(Value::Bytes(bytes)) => Some(bytes.clone()), + _ => return Err(general(AuditReason::RecipientShape)), + }; + + Ok(ParsedEncrypt { + body_protected, + recipient_protected, + iv, + ciphertext, + encapsulated_key, + recipient_ciphertext, + }) +} + +fn deterministic_encapsulation( + kem: Kem, + seed: &[u8; 64], + randomness: &[u8; 32], +) -> AuditResult { + let seed = + Seed::try_from(seed.as_slice()).map_err(|_| general(AuditReason::InvalidSeedLength))?; + let message = B32::try_from(randomness.as_slice()) + .map_err(|_| general(AuditReason::InvalidSeedLength))?; + match kem { + Kem::MlKem512 => { + let private = ml_kem::ml_kem_512::DecapsulationKey::from_seed(seed); + let public = private.encapsulation_key(); + let (ciphertext, mut shared) = public.encapsulate_deterministic(&message); + let output = EncapsulationOutput { + public_key: public.to_bytes().to_vec(), + ciphertext: ciphertext.to_vec(), + shared_secret: Zeroizing::new(shared.to_vec()), + }; + shared.zeroize(); + Ok(output) + } + Kem::MlKem768 => { + let private = ml_kem::ml_kem_768::DecapsulationKey::from_seed(seed); + let public = private.encapsulation_key(); + let (ciphertext, mut shared) = public.encapsulate_deterministic(&message); + let output = EncapsulationOutput { + public_key: public.to_bytes().to_vec(), + ciphertext: ciphertext.to_vec(), + shared_secret: Zeroizing::new(shared.to_vec()), + }; + shared.zeroize(); + Ok(output) + } + Kem::MlKem1024 => { + let private = ml_kem::ml_kem_1024::DecapsulationKey::from_seed(seed); + let public = private.encapsulation_key(); + let (ciphertext, mut shared) = public.encapsulate_deterministic(&message); + let output = EncapsulationOutput { + public_key: public.to_bytes().to_vec(), + ciphertext: ciphertext.to_vec(), + shared_secret: Zeroizing::new(shared.to_vec()), + }; + shared.zeroize(); + Ok(output) + } + } +} + +fn decapsulate(kem: Kem, seed: &[u8; 64], ciphertext: &[u8]) -> AuditResult>> { + let seed = + Seed::try_from(seed.as_slice()).map_err(|_| general(AuditReason::InvalidSeedLength))?; + match kem { + Kem::MlKem512 => { + let private = ml_kem::ml_kem_512::DecapsulationKey::from_seed(seed); + let ciphertext = ml_kem::ml_kem_512::Ciphertext::try_from(ciphertext) + .map_err(|_| general(AuditReason::KemDecapsulationMismatch))?; + let mut shared = private.decapsulate(&ciphertext); + let output = Zeroizing::new(shared.to_vec()); + shared.zeroize(); + Ok(output) + } + Kem::MlKem768 => { + let private = ml_kem::ml_kem_768::DecapsulationKey::from_seed(seed); + let ciphertext = ml_kem::ml_kem_768::Ciphertext::try_from(ciphertext) + .map_err(|_| general(AuditReason::KemDecapsulationMismatch))?; + let mut shared = private.decapsulate(&ciphertext); + let output = Zeroizing::new(shared.to_vec()); + shared.zeroize(); + Ok(output) + } + Kem::MlKem1024 => { + let private = ml_kem::ml_kem_1024::DecapsulationKey::from_seed(seed); + let ciphertext = ml_kem::ml_kem_1024::Ciphertext::try_from(ciphertext) + .map_err(|_| general(AuditReason::KemDecapsulationMismatch))?; + let mut shared = private.decapsulate(&ciphertext); + let output = Zeroizing::new(shared.to_vec()); + shared.zeroize(); + Ok(output) + } + } +} + +fn derive_kid(kem: Kem, public_key: &[u8]) -> AuditResult> { + let cose_key = encode_cbor(&Value::Map(vec![ + ( + Value::Integer(1_i64.into()), + Value::Integer(COSE_KEY_TYPE_AKP.into()), + ), + ( + Value::Integer(3_i64.into()), + Value::Integer(kem.direct_algorithm().into()), + ), + ( + Value::Integer((-1_i64).into()), + Value::Bytes(public_key.to_vec()), + ), + ]))?; + Ok(Sha256::digest(cose_key).to_vec()) +} + +fn derive_key( + shared_secret: &[u8], + algorithm: i64, + output_length: usize, + recipient_protected: &[u8], + supp_priv_info: &[u8], +) -> AuditResult>> { + let output_bits = output_length + .checked_mul(BITS_PER_BYTE) + .ok_or_else(|| general(AuditReason::IntegerConversion))?; + let output_bits = + u64::try_from(output_bits).map_err(|_| general(AuditReason::IntegerConversion))?; + let context = encode_cbor(&Value::Array(vec![ + Value::Integer(algorithm.into()), + Value::Array(vec![ + Value::Integer(output_bits.into()), + Value::Bytes(recipient_protected.to_vec()), + ]), + Value::Bytes(supp_priv_info.to_vec()), + ]))?; + let mut kmac = Kmac256::new(shared_secret, &[]).map_err(|_| general(AuditReason::KemKdf))?; + kmac.update(&context); + let mut output = Zeroizing::new(vec![0_u8; output_length]); + kmac.finalize_into(&mut output); + Ok(output) +} + +fn unwrap_key(kem: Kem, kek: &[u8], wrapped: &[u8]) -> AuditResult>> { + let expected_length = wrapped + .len() + .checked_sub(AES_KW_OVERHEAD) + .ok_or_else(|| general(AuditReason::KemKeyWrap))?; + let mut output = Zeroizing::new(vec![0_u8; expected_length]); + let unwrapped = match kem { + Kem::MlKem512 => KwAes128::new_from_slice(kek) + .map_err(|_| general(AuditReason::KemKeyWrap))? + .unwrap_key(wrapped, &mut output), + Kem::MlKem768 => KwAes192::new_from_slice(kek) + .map_err(|_| general(AuditReason::KemKeyWrap))? + .unwrap_key(wrapped, &mut output), + Kem::MlKem1024 => KwAes256::new_from_slice(kek) + .map_err(|_| general(AuditReason::KemKeyWrap))? + .unwrap_key(wrapped, &mut output), + } + .map_err(|_| general(AuditReason::KemKeyWrap))?; + ensure(unwrapped.len() == expected_length, AuditReason::KemKeyWrap)?; + Ok(output) +} + +fn decrypt_content( + kem: Kem, + key: &[u8], + iv: &[u8], + aad: &[u8], + ciphertext: &[u8], +) -> AuditResult>> { + let iv = + <[u8; 12]>::try_from(iv).map_err(|_| general(AuditReason::EncryptUnprotectedHeader))?; + let payload = Payload { + msg: ciphertext, + aad, + }; + let plaintext = match kem { + Kem::MlKem512 => Aes128Gcm::new_from_slice(key) + .map_err(|_| general(AuditReason::EncryptAuthentication))? + .decrypt((&iv).into(), payload), + Kem::MlKem768 => Aes192Gcm::new_from_slice(key) + .map_err(|_| general(AuditReason::EncryptAuthentication))? + .decrypt((&iv).into(), payload), + Kem::MlKem1024 => Aes256Gcm::new_from_slice(key) + .map_err(|_| general(AuditReason::EncryptAuthentication))? + .decrypt((&iv).into(), payload), + } + .map_err(|_| general(AuditReason::EncryptAuthentication))?; + Ok(Zeroizing::new(plaintext)) +} + +fn decode_map(bytes: &[u8], reason: AuditReason) -> AuditResult> { + match ciborium::de::from_reader::(Cursor::new(bytes)) { + Ok(Value::Map(map)) => Ok(map), + Ok(_) | Err(_) => Err(general(reason)), + } +} + +fn bytes_at(array: &[Value], index: usize, reason: AuditReason) -> AuditResult> { + match array.get(index) { + Some(Value::Bytes(bytes)) => Ok(bytes.clone()), + _ => Err(general(reason)), + } +} + +fn map_at(array: &[Value], index: usize, reason: AuditReason) -> AuditResult> { + match array.get(index) { + Some(Value::Map(map)) => Ok(map.clone()), + _ => Err(general(reason)), + } +} + +fn decode_hex(value: &str) -> AuditResult> { + hex::decode(value).map_err(|_| general(AuditReason::Hex)) +} + +fn fixed(bytes: &[u8]) -> AuditResult<[u8; N]> { + <[u8; N]>::try_from(bytes).map_err(|_| general(AuditReason::InvalidSeedLength)) +} diff --git a/tools/vector-audit/src/pq.rs b/tools/vector-audit/src/pq.rs new file mode 100644 index 0000000..44fd54e --- /dev/null +++ b/tools/vector-audit/src/pq.rs @@ -0,0 +1,325 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! COSE-layer audit for ML-DSA and ML-KEM vectors. + +use std::collections::HashSet; +use std::io::Cursor; +use std::path::Path; + +use ciborium::value::Value; +use ml_dsa::{ + EncodedVerifyingKey, KeyExport, Keypair, MlDsa44, MlDsa65, MlDsa87, MlDsaParams, Seed, + Signature, SigningKey, Verifier, VerifyingKey, +}; +use serde::Deserialize; + +use super::{ + attach_case, audit_multikey, audit_unique_id, decode_hex, encode_cbor, ensure, general, + map_get, read_json, AuditContext, AuditReason, AuditResult, +}; + +const SIGN1_FILE: &str = "vectors/cose-sign1-pq.json"; +const KEY_FILE: &str = "vectors/cose-key-pq.json"; + +pub(super) struct Summary { + pub(super) sign1_cases: usize, + pub(super) key_cases: usize, +} + +#[derive(Deserialize)] +struct Suite { + cases: Vec, +} + +#[derive(Deserialize)] +struct Sign1Case { + id: String, + operation: String, + algorithm: String, + kid_hex: String, + public_key_hex: String, + private_key_seed_hex: String, + payload_hex: String, + cose_sign1_hex: String, + expected_error: Option, +} + +#[derive(Deserialize)] +struct KeyCase { + id: String, + algorithm: String, + private_key_seed_hex: String, + public_key_hex: String, + cose_key_hex: String, + multikey: String, +} + +#[derive(Clone, Copy)] +enum PqAlgorithm { + MlDsa44, + MlDsa65, + MlDsa87, + MlKem512, + MlKem768, + MlKem1024, +} + +impl PqAlgorithm { + fn parse(name: &str) -> AuditResult { + match name { + "ML-DSA-44" => Ok(Self::MlDsa44), + "ML-DSA-65" => Ok(Self::MlDsa65), + "ML-DSA-87" => Ok(Self::MlDsa87), + "ML-KEM-512" => Ok(Self::MlKem512), + "ML-KEM-768" => Ok(Self::MlKem768), + "ML-KEM-1024" => Ok(Self::MlKem1024), + _ => Err(general(AuditReason::UnsupportedAlgorithm)), + } + } + + const fn cose_algorithm(self) -> i64 { + match self { + Self::MlDsa44 => -48, + Self::MlDsa65 => -49, + Self::MlDsa87 => -50, + Self::MlKem512 => -65_537, + Self::MlKem768 => -65_538, + Self::MlKem1024 => -65_539, + } + } + + const fn multicodec(self) -> u64 { + match self { + Self::MlDsa44 => 0x1210, + Self::MlDsa65 => 0x1211, + Self::MlDsa87 => 0x1212, + Self::MlKem512 => 0x120b, + Self::MlKem768 => 0x120c, + Self::MlKem1024 => 0x120d, + } + } + + const fn signature_len(self) -> Option { + match self { + Self::MlDsa44 => Some(2_420), + Self::MlDsa65 => Some(3_309), + Self::MlDsa87 => Some(4_627), + Self::MlKem512 | Self::MlKem768 | Self::MlKem1024 => None, + } + } +} + +pub(super) fn audit_suites(repo_root: &Path, ids: &mut HashSet) -> AuditResult { + let sign1: Suite = read_json(repo_root, SIGN1_FILE, AuditContext::General)?; + let keys: Suite = read_json(repo_root, KEY_FILE, AuditContext::General)?; + + for case in &sign1.cases { + audit_unique_id(ids, &case.id)?; + audit_sign1(case).map_err(|error| attach_case(error, &case.id))?; + } + for case in &keys.cases { + audit_unique_id(ids, &case.id)?; + audit_key(case).map_err(|error| attach_case(error, &case.id))?; + } + + Ok(Summary { + sign1_cases: sign1.cases.len(), + key_cases: keys.cases.len(), + }) +} + +fn audit_sign1(case: &Sign1Case) -> AuditResult<()> { + let algorithm = PqAlgorithm::parse(&case.algorithm)?; + ensure( + matches!( + algorithm, + PqAlgorithm::MlDsa44 | PqAlgorithm::MlDsa65 | PqAlgorithm::MlDsa87 + ), + AuditReason::UnsupportedAlgorithm, + )?; + let seed = decode_hex(&case.private_key_seed_hex)?; + let public = decode_hex(&case.public_key_hex)?; + let expected_public = derive_public(algorithm, &seed)?; + ensure(public == expected_public, AuditReason::SeedPublicMismatch)?; + + let kid = decode_hex(&case.kid_hex)?; + let declared_payload = decode_hex(&case.payload_hex)?; + let cose = decode_hex(&case.cose_sign1_hex)?; + let root: Value = ciborium::de::from_reader(Cursor::new(&cose)) + .map_err(|_| general(AuditReason::CborDecode))?; + let array = match root { + Value::Array(array) if array.len() == 4 => array, + _ => return Err(general(AuditReason::Sign1ArrayLength)), + }; + let protected = match &array[0] { + Value::Bytes(bytes) => bytes, + _ => return Err(general(AuditReason::Sign1ProtectedNotBytes)), + }; + let protected_map: Value = ciborium::de::from_reader(Cursor::new(protected)) + .map_err(|_| general(AuditReason::CborDecode))?; + let protected_map = match protected_map { + Value::Map(map) => map, + _ => return Err(general(AuditReason::Sign1ProtectedNotMap)), + }; + ensure( + matches!(map_get(&protected_map, 1), Some(value) if super::integer_matches(value, algorithm.cose_algorithm())), + AuditReason::ProtectedAlgorithmMismatch, + )?; + ensure( + matches!(map_get(&protected_map, 4), Some(Value::Bytes(value)) if *value == kid), + AuditReason::ProtectedKidMismatch, + )?; + let payload = match &array[2] { + Value::Bytes(payload) => payload, + _ => return Err(general(AuditReason::AttachedPayloadMissing)), + }; + ensure( + case.operation == "verify_attached", + AuditReason::UnsupportedOperation, + )?; + ensure( + *payload == declared_payload, + AuditReason::AttachedPayloadMissing, + )?; + let signature = match &array[3] { + Value::Bytes(signature) => signature, + _ => return Err(general(AuditReason::Sign1SignatureShape)), + }; + let sig_structure = encode_cbor(&Value::Array(vec![ + Value::Text("Signature1".to_owned()), + Value::Bytes(protected.clone()), + Value::Bytes(Vec::new()), + Value::Bytes(payload.clone()), + ]))?; + let signature_valid = verify_signature(algorithm, &public, &sig_structure, signature); + let expected_signature_len = algorithm + .signature_len() + .ok_or_else(|| general(AuditReason::UnsupportedAlgorithm))?; + match case.expected_error.as_deref() { + None => { + ensure( + signature.len() == expected_signature_len, + AuditReason::SignatureWidth, + )?; + ensure(signature_valid, AuditReason::SignatureDidNotVerify) + } + Some("InvalidSignature") => { + ensure( + signature.len() == expected_signature_len, + AuditReason::SignatureWidth, + )?; + ensure(!signature_valid, AuditReason::InvalidSignatureVerified) + } + Some("InvalidSignatureEncoding") => { + ensure( + signature.len() != expected_signature_len, + AuditReason::InvalidSignatureEncodingWidth, + )?; + ensure(!signature_valid, AuditReason::InvalidSignatureVerified) + } + Some(_) => Err(general(AuditReason::UnsupportedExpectedError)), + } +} + +fn audit_key(case: &KeyCase) -> AuditResult<()> { + let algorithm = PqAlgorithm::parse(&case.algorithm)?; + let seed = decode_hex(&case.private_key_seed_hex)?; + let public = decode_hex(&case.public_key_hex)?; + ensure( + derive_public(algorithm, &seed)? == public, + AuditReason::SeedPublicMismatch, + )?; + let cose = decode_hex(&case.cose_key_hex)?; + let root: Value = ciborium::de::from_reader(Cursor::new(&cose)) + .map_err(|_| general(AuditReason::CborDecode))?; + let map = match root { + Value::Map(map) => map, + _ => return Err(general(AuditReason::CoseKeyRootNotMap)), + }; + ensure( + matches!(map_get(&map, 1), Some(value) if super::integer_matches(value, 7)), + AuditReason::CoseKeyTypeMismatch, + )?; + ensure( + matches!(map_get(&map, 3), Some(value) if super::integer_matches(value, algorithm.cose_algorithm())), + AuditReason::CoseKeyAlgorithmMismatch, + )?; + ensure( + map_get(&map, -2).is_none(), + AuditReason::CoseKeyPrivateMaterial, + )?; + let encoded_public = match map_get(&map, -1) { + Some(Value::Bytes(bytes)) => bytes, + _ => return Err(general(AuditReason::CoseKeyMissingX)), + }; + ensure(*encoded_public == public, AuditReason::OkpPublicMismatch)?; + audit_multikey(&case.multikey, algorithm.multicodec(), &public) +} + +fn derive_public(algorithm: PqAlgorithm, seed: &[u8]) -> AuditResult> { + match algorithm { + PqAlgorithm::MlDsa44 => derive_ml_dsa_public::(seed), + PqAlgorithm::MlDsa65 => derive_ml_dsa_public::(seed), + PqAlgorithm::MlDsa87 => derive_ml_dsa_public::(seed), + PqAlgorithm::MlKem512 => { + let seed = ml_kem::Seed::try_from(seed) + .map_err(|_| general(AuditReason::InvalidSeedLength))?; + Ok(ml_kem::ml_kem_512::DecapsulationKey::from_seed(seed) + .encapsulation_key() + .to_bytes() + .to_vec()) + } + PqAlgorithm::MlKem768 => { + let seed = ml_kem::Seed::try_from(seed) + .map_err(|_| general(AuditReason::InvalidSeedLength))?; + Ok(ml_kem::ml_kem_768::DecapsulationKey::from_seed(seed) + .encapsulation_key() + .to_bytes() + .to_vec()) + } + PqAlgorithm::MlKem1024 => { + let seed = ml_kem::Seed::try_from(seed) + .map_err(|_| general(AuditReason::InvalidSeedLength))?; + Ok(ml_kem::ml_kem_1024::DecapsulationKey::from_seed(seed) + .encapsulation_key() + .to_bytes() + .to_vec()) + } + } +} + +fn derive_ml_dsa_public(seed: &[u8]) -> AuditResult> { + let seed = Seed::try_from(seed).map_err(|_| general(AuditReason::InvalidSeedLength))?; + Ok(SigningKey::

::from_seed(&seed) + .verifying_key() + .to_bytes() + .to_vec()) +} + +fn verify_signature( + algorithm: PqAlgorithm, + public: &[u8], + message: &[u8], + signature: &[u8], +) -> bool { + match algorithm { + PqAlgorithm::MlDsa44 => verify_ml_dsa::(public, message, signature), + PqAlgorithm::MlDsa65 => verify_ml_dsa::(public, message, signature), + PqAlgorithm::MlDsa87 => verify_ml_dsa::(public, message, signature), + PqAlgorithm::MlKem512 | PqAlgorithm::MlKem768 | PqAlgorithm::MlKem1024 => false, + } +} + +fn verify_ml_dsa(public: &[u8], message: &[u8], signature: &[u8]) -> bool { + let Ok(encoded_public) = EncodedVerifyingKey::

::try_from(public) else { + return false; + }; + let Ok(signature) = Signature::

::try_from(signature) else { + return false; + }; + VerifyingKey::

::decode(&encoded_public) + .verify(message, &signature) + .is_ok() +} diff --git a/tools/vector-audit/src/verify_manifest.rs b/tools/vector-audit/src/verify_manifest.rs new file mode 100644 index 0000000..0def51a --- /dev/null +++ b/tools/vector-audit/src/verify_manifest.rs @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Verify that the manifest binds every vector suite by path, count, and digest. + +use std::path::Path; + +use serde::Deserialize; +use sha2::{Digest, Sha256}; + +use super::{ensure, manifest_error, AuditContext, AuditError, AuditReason, AuditResult}; + +const EXPECTED_SUITE_COUNT: usize = 5; + +#[derive(Debug, Deserialize)] +pub(super) struct Manifest { + suites: Vec, +} + +#[derive(Debug, Deserialize)] +struct ManifestSuite { + id: String, + path: String, + case_count: usize, + sha256: String, +} + +struct ExpectedSuite<'a> { + id: &'static str, + path: &'static str, + case_count: usize, + repo_root: &'a Path, +} + +pub(super) fn verify( + repo_root: &Path, + manifest: &Manifest, + sign1_cases: usize, + key_cases: usize, + pq_sign1_cases: usize, + pq_key_cases: usize, + ml_kem_encrypt_cases: usize, +) -> AuditResult<()> { + let expected = [ + ExpectedSuite { + id: "cose-sign1", + path: "cose-sign1.json", + case_count: sign1_cases, + repo_root, + }, + ExpectedSuite { + id: "cose-key", + path: "cose-key.json", + case_count: key_cases, + repo_root, + }, + ExpectedSuite { + id: "cose-sign1-pq", + path: "cose-sign1-pq.json", + case_count: pq_sign1_cases, + repo_root, + }, + ExpectedSuite { + id: "cose-key-pq", + path: "cose-key-pq.json", + case_count: pq_key_cases, + repo_root, + }, + ExpectedSuite { + id: "cose-encrypt-ml-kem", + path: "cose-encrypt-ml-kem.json", + case_count: ml_kem_encrypt_cases, + repo_root, + }, + ]; + + ensure( + manifest.suites.len() == EXPECTED_SUITE_COUNT, + AuditReason::ManifestSuiteSet, + ) + .map_err(manifest_context)?; + for expected_suite in expected { + let mut matches = manifest + .suites + .iter() + .filter(|suite| suite.id == expected_suite.id); + let suite = matches + .next() + .ok_or_else(|| manifest_error(AuditReason::ManifestSuiteSet))?; + ensure(matches.next().is_none(), AuditReason::ManifestSuiteSet) + .map_err(manifest_context)?; + ensure(suite.path == expected_suite.path, AuditReason::ManifestPath) + .map_err(manifest_context)?; + ensure( + suite.case_count == expected_suite.case_count, + AuditReason::ManifestCaseCount, + ) + .map_err(manifest_context)?; + verify_digest(&expected_suite, suite)?; + } + Ok(()) +} + +fn verify_digest(expected: &ExpectedSuite<'_>, suite: &ManifestSuite) -> AuditResult<()> { + let mut manifest_digest = [0_u8; 32]; + hex::decode_to_slice(&suite.sha256, &mut manifest_digest) + .map_err(|_| manifest_error(AuditReason::ManifestDigestEncoding))?; + let bytes = std::fs::read(expected.repo_root.join("vectors").join(expected.path)) + .map_err(|_| manifest_error(AuditReason::ReadFile))?; + let actual_digest = Sha256::digest(bytes); + ensure( + actual_digest.as_slice() == manifest_digest, + AuditReason::ManifestDigest, + ) + .map_err(manifest_context) +} + +fn manifest_context(error: AuditError) -> AuditError { + AuditError { + context: AuditContext::Manifest, + reason: error.reason, + } +} diff --git a/tools/vector-goldens/Cargo.lock b/tools/vector-goldens/Cargo.lock new file mode 100644 index 0000000..1869395 --- /dev/null +++ b/tools/vector-goldens/Cargo.lock @@ -0,0 +1,2353 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array 0.14.7", +] + +[[package]] +name = "aead" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" +dependencies = [ + "crypto-common 0.2.2", + "inout 0.2.2", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +dependencies = [ + "cipher 0.5.2", + "cpubits", + "cpufeatures 0.3.0", + "zeroize", +] + +[[package]] +name = "aes-gcm" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028" +dependencies = [ + "aead 0.6.1", + "aes 0.9.1", + "cipher 0.5.2", + "ctr 0.10.1", + "ghash", + "subtle", + "zeroize", +] + +[[package]] +name = "aes-gcm-siv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +dependencies = [ + "aead 0.5.2", + "aes 0.8.4", + "cipher 0.4.4", + "ctr 0.9.2", + "polyval 0.6.2", + "subtle", + "zeroize", +] + +[[package]] +name = "aes-kw" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ac571010bd60765c56085a4f1d412012a9be2663b1a2f2b19b49318653fd0d" +dependencies = [ + "aes 0.9.1", + "zeroize", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.2.17", + "password-hash", + "zeroize", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + +[[package]] +name = "base256emoji" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" +dependencies = [ + "const-str", + "match-lookup", +] + +[[package]] +name = "base45" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240e56f4d3c453c36faacb695c535a4d5f8c7d23dac175014f32eb0a71012a03" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "blake2s_simd" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee29928bad1e3f94c9d1528da29e07a1d3d04817ae8332de1e8b846c8439f4b3" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", + "zeroize", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cipher 0.5.2", + "cpufeatures 0.3.0", + "zeroize", +] + +[[package]] +name = "chacha20poly1305" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b89e1c441e926b9c82a8d023f6e1b7ae0adcfaa7d621814e4d60789bac751cb" +dependencies = [ + "aead 0.6.1", + "chacha20", + "cipher 0.5.2", + "poly1305", + "zeroize", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cid" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a304f95f84d169a6f31c4d0a30d784643aaa0bbc9c1e449a2c23e963ec4971" +dependencies = [ + "multibase", + "multihash", + "unsigned-varint", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout 0.1.4", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "inout 0.2.2", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "const-str" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "coset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1eb98d5e9155e2cf7cd942c8b3033097d4563b6fb0a00b9caecb74669555c058" +dependencies = [ + "ciborium", + "ciborium-io", +] + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +dependencies = [ + "cpubits", + "ctutils", + "getrandom 0.4.3", + "hybrid-array", + "num-traits", + "rand_core 0.10.1", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array 0.14.7", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "getrandom 0.4.3", + "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "ctr" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" +dependencies = [ + "cipher 0.5.2", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "curve25519-dalek-derive", + "digest 0.11.3", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "data-encoding-macro" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3259c913752a86488b501ed8680446a5ed2d5aeac6e596cb23ba3800768ea32c" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" +dependencies = [ + "data-encoding", + "syn", +] + +[[package]] +name = "der" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", + "ctutils", + "zeroize", +] + +[[package]] +name = "ecdsa" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" +dependencies = [ + "der", + "digest 0.11.3", + "elliptic-curve", + "rfc6979", + "signature", + "spki", + "zeroize", +] + +[[package]] +name = "ed25519" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" +dependencies = [ + "curve25519-dalek", + "ed25519", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" +dependencies = [ + "base16ct", + "crypto-bigint", + "crypto-common 0.2.2", + "digest 0.11.3", + "ff", + "group", + "hkdf", + "hybrid-array", + "pem-rfc7468", + "pkcs8", + "rand_core 0.10.1", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "ff" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core 0.10.1", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "generic-array" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab4e5aa225bc56696909483320f0ff9b600f1a971b52e07a17d70f3d9b43254b" +dependencies = [ + "rustversion", + "typenum", +] + +[[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.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" +dependencies = [ + "polyval 0.7.3", +] + +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff", + "rand_core 0.10.1", + "subtle", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "hpke" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5130e119706b4d8c2180da6126f7e60b6c38c2d340d539219f57051f0a7af7" +dependencies = [ + "aead 0.6.1", + "hybrid-array", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "ctutils", + "subtle", + "typenum", + "zeroize", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f50113171a713f4a4231ef82eb26703607139b35dcb56241f0ceab2ae1f7d8" +dependencies = [ + "cpubits", + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", + "signature", + "wnaf", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + +[[package]] +name = "kem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" +dependencies = [ + "crypto-common 0.2.2", + "rand_core 0.10.1", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "match-lookup" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "ml-dsa" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add6b9d92e496f16f4526d68ff29da1483aba4b119baeab8bed3b9e3544a6f3d" +dependencies = [ + "const-oid", + "crypto-common 0.2.2", + "ctutils", + "hybrid-array", + "module-lattice", + "pkcs8", + "shake", + "signature", + "zeroize", +] + +[[package]] +name = "ml-kem" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66" +dependencies = [ + "hybrid-array", + "kem", + "module-lattice", + "pkcs8", + "rand_core 0.10.1", + "sha3 0.11.0", + "zeroize", +] + +[[package]] +name = "module-lattice" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" +dependencies = [ + "ctutils", + "hybrid-array", + "num-traits", + "zeroize", +] + +[[package]] +name = "multibase" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e0e4a371cbf1dfd666b658ba137763edb23c45beb43cfe369b5593cd6b437b6" +dependencies = [ + "base-x", + "base256emoji", + "base45", + "data-encoding", + "data-encoding-macro", +] + +[[package]] +name = "multihash" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577c63b00ad74d57e8c9aa870b5fccebf2fd64a308a5aee9f1bb88e4aea19447" +dependencies = [ + "unsigned-varint", +] + +[[package]] +name = "multihash-codetable" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60384411b100398c5b2fdca476eed82e9898d09f2c60d1b1b66c5080ebe06118" +dependencies = [ + "blake2b_simd", + "blake2s_simd", + "blake3", + "digest 0.11.3", + "multihash-derive", + "ripemd", + "sha1", + "sha2", + "sha3 0.11.0", +] + +[[package]] +name = "multihash-derive" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4723acdce756db211a53f01b3419dbdf63cb48cef5df86260f55309364735fbf" +dependencies = [ + "multihash", + "multihash-derive-impl", +] + +[[package]] +name = "multihash-derive-impl" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f932556f78452e5604cef711349d337ec081a9aa3c96e67b3127c8f5df05d550" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "p256" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primefield", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d17b851e6b3e378ab4ecb07fa2ed23f4d15f075735f8fec9fa1e7bdce5f8301f" +dependencies = [ + "ecdsa", + "elliptic-curve", + "fiat-crypto", + "primefield", + "primeorder", + "sha2", +] + +[[package]] +name = "p521" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad64cc32c2dc466317c12ee5853e61f159f9eab1fe7efade0395dc2e7b43449" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primefield", + "primeorder", + "sha2", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest 0.11.3", + "hmac", +] + +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "poly1305" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" +dependencies = [ + "cpufeatures 0.3.0", + "universal-hash 0.6.1", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash 0.5.1", +] + +[[package]] +name = "polyval" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" +dependencies = [ + "cpubits", + "cpufeatures 0.3.0", + "universal-hash 0.6.1", +] + +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint", + "crypto-common 0.2.2", + "ff", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + +[[package]] +name = "primeorder" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" +dependencies = [ + "elliptic-curve", + "once_cell", + "primefield", + "serdect", + "wnaf", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "reallyme-codec" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3a3125c574e35d31a7abbdac1fb1c66788f54e84e8d757fb1d941a7cd46573" +dependencies = [ + "reallyme-codec-base64url", + "reallyme-codec-cbor", + "reallyme-codec-multibase", + "reallyme-codec-multicodec", + "reallyme-codec-multikey", + "thiserror", +] + +[[package]] +name = "reallyme-codec-base64url" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "318534e19a178ea727b2e141fde87e892c3b338d4b8a52323b29bd3a5f50cb94" +dependencies = [ + "base64", + "thiserror", +] + +[[package]] +name = "reallyme-codec-cbor" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe95bd99c7a82d1661e131c17bdb7b757cab05969d32452e971e0d6afeaa569" +dependencies = [ + "cid", + "multihash", + "multihash-codetable", + "sha2", + "thiserror", + "zeroize", +] + +[[package]] +name = "reallyme-codec-jcs" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be98f72844bae8c270002805b7e727782f3903a9b244b81bffbc154582422a92" +dependencies = [ + "itoa", + "ryu-js", + "serde", + "serde_json", + "thiserror", + "zeroize", +] + +[[package]] +name = "reallyme-codec-multibase" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "060dbf70afe2e22822c726dc38d4efb24654cca7c02b134624a11777cda966f8" +dependencies = [ + "base64", + "bs58", + "reallyme-codec-base64url", + "thiserror", + "zeroize", +] + +[[package]] +name = "reallyme-codec-multicodec" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378589a32bb420707728dc36b693e15aaf34a0baa48cf91f50ec8c15c6b1c6f9" + +[[package]] +name = "reallyme-codec-multikey" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdce86dc938b3de36f8fd6f7213dd4127cbeb47018263dbbfdeab78adb60165e" +dependencies = [ + "reallyme-codec-multibase", + "reallyme-codec-multicodec", + "thiserror", +] + +[[package]] +name = "reallyme-codec-pem" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32bcc148863f3d8d9e347f858e1bbf8f9df6154bab85b46da7da850e747f3633" +dependencies = [ + "base64", + "thiserror", + "zeroize", +] + +[[package]] +name = "reallyme-cose" +version = "0.2.0" +dependencies = [ + "ciborium", + "coset", + "reallyme-codec", + "reallyme-crypto", + "thiserror", + "zeroize", +] + +[[package]] +name = "reallyme-cose-vector-goldens" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "aes-kw", + "ciborium", + "ed25519-dalek", + "hex", + "ml-dsa", + "ml-kem", + "reallyme-cose", + "serde", + "serde_json", + "sha3-kmac", + "thiserror", + "zeroize", +] + +[[package]] +name = "reallyme-crypto" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5be3c32e719f0aa1cae63efe8a91e0454ca174f1eb7f58104b3c556b423105a" +dependencies = [ + "reallyme-crypto-aes-kw", + "reallyme-crypto-aes256-gcm", + "reallyme-crypto-aes256-gcm-siv", + "reallyme-crypto-argon2id", + "reallyme-crypto-chacha20-poly1305", + "reallyme-crypto-concat-kdf", + "reallyme-crypto-constant-time", + "reallyme-crypto-core", + "reallyme-crypto-csprng", + "reallyme-crypto-dispatch", + "reallyme-crypto-ed25519", + "reallyme-crypto-hkdf", + "reallyme-crypto-hmac", + "reallyme-crypto-hpke", + "reallyme-crypto-jwk", + "reallyme-crypto-jwk-multikey", + "reallyme-crypto-kmac", + "reallyme-crypto-ml-dsa-44", + "reallyme-crypto-ml-dsa-65", + "reallyme-crypto-ml-dsa-87", + "reallyme-crypto-ml-kem-1024", + "reallyme-crypto-ml-kem-512", + "reallyme-crypto-ml-kem-768", + "reallyme-crypto-p256", + "reallyme-crypto-p384", + "reallyme-crypto-p521", + "reallyme-crypto-pbkdf2", + "reallyme-crypto-rsa", + "reallyme-crypto-secp256k1", + "reallyme-crypto-sha2", + "reallyme-crypto-sha2-256", + "reallyme-crypto-sha3", + "reallyme-crypto-sha3-256", + "reallyme-crypto-signer", + "reallyme-crypto-slh-dsa", + "reallyme-crypto-x-wing", + "reallyme-crypto-x25519", + "thiserror", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-aes-kw" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e45401567b98f58cd57479689e1d4e4b88ba518f7f286418b614f1cb0b412c" +dependencies = [ + "aes-kw", + "reallyme-crypto-core", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-aes256-gcm" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "124e3db9478e8b2f838d1ebd95e1f011c400585783bc2d45679acc7002af8f0d" +dependencies = [ + "aes 0.9.1", + "aes-gcm", + "reallyme-crypto-core", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-aes256-gcm-siv" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0041be4e920de335620ce7ea1d72b6ba6aaeaad89fba67d5c09553b2d6b2c3c0" +dependencies = [ + "aes 0.9.1", + "aes-gcm-siv", + "reallyme-crypto-core", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-argon2id" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca429c7518f887878ee54916e0c687e368cc9dd37772c4c2d829d25132ed9766" +dependencies = [ + "argon2", + "reallyme-crypto-core", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-chacha20-poly1305" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cb1fff08beac1884c6b564fc5cf165800fc5fa11cb9ab89b0f84478122e0d01" +dependencies = [ + "chacha20poly1305", + "getrandom 0.4.3", + "reallyme-crypto-core", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-concat-kdf" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46bc9d843f0fba16d4611494855cfb3a39176fb7e687079e03334c21db14f96d" +dependencies = [ + "reallyme-crypto-core", + "sha2", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-constant-time" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1e9077cf570599b72fc0ceaff0e5b4035af78f0e8cea7f3837263610005769a" +dependencies = [ + "reallyme-crypto-core", + "subtle", +] + +[[package]] +name = "reallyme-crypto-core" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd4c4b22fb84808b656b0338c75bd6f8a710cf228c9a5e32205b53df7dc16f41" +dependencies = [ + "thiserror", +] + +[[package]] +name = "reallyme-crypto-csprng" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94abb1a85cce98e0cc0798b0a180ad4868767ef92ceb13b6b0d8839a3784ba00" +dependencies = [ + "getrandom 0.4.3", + "reallyme-crypto-core", + "secrecy", + "subtle", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-dispatch" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e697569f767e5a1dca8266b14135324d88f0d76177c3ab1e39e8140078aa81f7" +dependencies = [ + "reallyme-codec-multikey", + "reallyme-crypto-core", + "reallyme-crypto-ed25519", + "reallyme-crypto-ml-dsa-44", + "reallyme-crypto-ml-dsa-65", + "reallyme-crypto-ml-dsa-87", + "reallyme-crypto-ml-kem-1024", + "reallyme-crypto-ml-kem-512", + "reallyme-crypto-ml-kem-768", + "reallyme-crypto-p256", + "reallyme-crypto-p384", + "reallyme-crypto-p521", + "reallyme-crypto-secp256k1", + "reallyme-crypto-x-wing", + "reallyme-crypto-x25519", + "thiserror", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-ed25519" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5b3d8a2b030fc561e449a4603cf1f17316b77e3d1e7c8542640d587dc8139ef" +dependencies = [ + "ed25519-dalek", + "reallyme-crypto-core", + "reallyme-crypto-csprng", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-hkdf" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39959f563d02008423a1efaf5edd317cf7a09178e270fa4098d83266e1e15001" +dependencies = [ + "hkdf", + "reallyme-crypto-core", + "sha2", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-hmac" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa6200e6d50b7d19df03ff1b800e3608058bcd6c1572610dc3df7779dd1aa769" +dependencies = [ + "hmac", + "reallyme-crypto-core", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-hpke" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddac972be9349619027bc7b212868a6021209aa2d29ada3e08cb708e159ec3b" +dependencies = [ + "getrandom 0.4.3", + "hpke", + "thiserror", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-jwk" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ad84e7a772e1da762d7569366fa34df30d6f055027df9b969c0f548bfa3c6" +dependencies = [ + "reallyme-codec-base64url", + "reallyme-codec-jcs", + "reallyme-crypto-p256", + "reallyme-crypto-secp256k1", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "reallyme-crypto-jwk-multikey" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b3d53b09f73225959b1e67e6761e0b585ae8c82a1bbf33ae290753db3742466" +dependencies = [ + "reallyme-codec-multikey", + "reallyme-crypto-jwk", + "thiserror", +] + +[[package]] +name = "reallyme-crypto-kmac" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfdf0cd6163d0587fa06dcc675e1009861de80d8e756ae345ba341a23f94cd6" +dependencies = [ + "reallyme-crypto-core", + "sha3 0.10.9", + "sha3-kmac", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-ml-dsa-44" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388b30c38fa26dd365387387cfdbf52adabca97b423bd1f182083bbcc27a9694" +dependencies = [ + "ml-dsa", + "reallyme-crypto-core", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-ml-dsa-65" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87d37290682f58bf48169e44ac87e9f11e01b72e3b05a051ca1c498a16ceb83e" +dependencies = [ + "ml-dsa", + "reallyme-crypto-core", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-ml-dsa-87" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2cd25d74ab5d7f410a574b5bd8518f79b78d4a101f028cacbf87dc7f9c05ba6" +dependencies = [ + "ml-dsa", + "reallyme-crypto-core", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-ml-kem-1024" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56383729251b86556981d6538b59b115efb6a583b547a986d70a992fc86a0cb6" +dependencies = [ + "ml-kem", + "reallyme-crypto-core", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-ml-kem-512" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd19b8f6e3f4c117f5a7b8940f5f7435499c3577bd5a8879f82140ec89ac4855" +dependencies = [ + "ml-kem", + "reallyme-crypto-core", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-ml-kem-768" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab02fc22fdcb8a1e7ac60d3cd6680124745a3499c8f04be18961c8b8d7f5d99" +dependencies = [ + "ml-kem", + "reallyme-crypto-core", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-p256" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e47ad5eb23ab89e754ac3f999c2711daac40e7f5b0dc1902ac9f52a90ad07c7" +dependencies = [ + "ecdsa", + "p256", + "reallyme-codec-pem", + "reallyme-crypto-core", + "sha2", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-p384" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83eb6398ae1e072ba6f8ce884e7c6031438021d818eb0faf5f7b8a63e005f828" +dependencies = [ + "ecdsa", + "getrandom 0.4.3", + "p384", + "reallyme-crypto-core", + "sha2", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-p521" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8344545560f68c7fbcc7e5ba0aaf6d574420fc56f1948c84a2bf0a55897323" +dependencies = [ + "ecdsa", + "getrandom 0.4.3", + "p521", + "reallyme-crypto-core", + "sha2", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-pbkdf2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a43e926aa983a6fb52549e951f384f14bd3487594f1c7c247f79b5d45e43ada" +dependencies = [ + "pbkdf2", + "reallyme-crypto-core", + "sha2", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-rsa" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471cd0282f27757d926def3686e84f3db0ad1cc8424b89dc8b4d9c19ece08285" +dependencies = [ + "const-oid", + "crypto-bigint", + "reallyme-crypto-core", + "sha1", + "sha2", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-secp256k1" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3d849f47a8622ec82f0a6ed9335de9d16ea9b3a1d9e74dba57257b04d05966" +dependencies = [ + "ecdsa", + "k256", + "reallyme-crypto-core", + "sha2", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-sha2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258e3bf830cc8745eb6adf7c6da33a64a038ed709120159407b79850132a71bb" +dependencies = [ + "sha2", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-sha2-256" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e03fe02c39d7dc36ad23c7ae6c58c816b24a6284282d045460cb53e43a9410d5" +dependencies = [ + "sha2", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-sha3" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "042a983305bb1d5368ffe457b5953eb9b5ce372bcfc7a822608ad4200ea2f1ad" +dependencies = [ + "sha3 0.12.0", + "shake", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-sha3-256" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71724f151fbeada4cd130758b67b71f0578cc16b3c2cc5c224d664591e444f19" +dependencies = [ + "sha3 0.12.0", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-signer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "095ada9a08c26e1fe02217d48fe368a3c2b531f227dc7997367abbed28c6c41a" +dependencies = [ + "reallyme-crypto-core", + "reallyme-crypto-dispatch", + "secrecy", + "thiserror", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-slh-dsa" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f93c3740cb89ecafcf97037af23b2f37bf51001f607f6f7a2b2eb7b221522" +dependencies = [ + "reallyme-crypto-core", + "reallyme-crypto-csprng", + "slh-dsa", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-x-wing" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3681ea3e10c195883bf46c98b92faa089bd7e6242ea03dbf8e25fa76c7b5e6ed" +dependencies = [ + "getrandom 0.4.3", + "ml-kem", + "reallyme-crypto-core", + "reallyme-crypto-sha3", + "reallyme-crypto-sha3-256", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "reallyme-crypto-x25519" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3fd164962966e67f9563dc0d8af9670f239f1c72623a7199613510bc448a80" +dependencies = [ + "reallyme-crypto-core", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "rfc6979" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" +dependencies = [ + "crypto-bigint", + "hmac", +] + +[[package]] +name = "ripemd" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dd4211456b4172d7e44261920c25acf07367c4f04bb5f5d54fc21b090d9b159" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu-js" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04d056b875a9d2e6cb9a61d127afee9ac5999b9f87bcb32079d1318e505be714" + +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct", + "ctutils", + "der", + "hybrid-array", + "subtle", + "zeroize", +] + +[[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.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[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.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak 0.1.6", + "zeroize", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", +] + +[[package]] +name = "sha3" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", + "sponge-cursor", +] + +[[package]] +name = "sha3-kmac" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b6b86fb906e40929519c1a38dc349a7f5595778cc00cc20b55eec34fddeb9ec" +dependencies = [ + "generic-array 1.4.4", + "sha3 0.10.9", + "sha3-utils", +] + +[[package]] +name = "sha3-utils" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08e0bf98cc082cbe077f06a707c94ca15796d046cc4cd2593167b5730a6727be" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "shake" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", + "sponge-cursor", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + +[[package]] +name = "slh-dsa" +version = "0.2.0-rc.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "371c02fe34044d8866ddf7cb0e8204a87ef31a39f0408bed41c4253ea9dd61ed" +dependencies = [ + "const-oid", + "digest 0.11.3", + "hmac", + "hybrid-array", + "pkcs8", + "rand_core 0.10.1", + "sha2", + "sha3 0.11.0", + "signature", + "typenum", + "zerocopy", + "zeroize", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" +dependencies = [ + "zeroize", +] + +[[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.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +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 = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "unsigned-varint" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" + +[[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 = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wnaf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" +dependencies = [ + "ff", + "group", + "hybrid-array", +] + +[[package]] +name = "x25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e8131a03190127fb2263afc72b322ecadae46b6ff8c6f399ff5d02f5559af6" +dependencies = [ + "curve25519-dalek", + "getrandom 0.4.3", + "rand_core 0.10.1", + "zeroize", +] + +[[package]] +name = "zerocopy" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/tools/vector-goldens/Cargo.toml b/tools/vector-goldens/Cargo.toml new file mode 100644 index 0000000..c44207e --- /dev/null +++ b/tools/vector-goldens/Cargo.toml @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "reallyme-cose-vector-goldens" +version = "0.1.0" +edition = "2021" +rust-version = "1.96" +publish = false + +[lints] +workspace = true + +[dependencies] +aes-gcm = "0.11" +aes-kw = { version = "0.3.1", default-features = false, features = ["zeroize"] } +ciborium = "0.2" +ed25519-dalek = "3" +hex = "0.4" +ml-kem = { version = "0.3.2", features = ["zeroize"] } +ml-dsa = "0.1.1" +reallyme-cose = { path = "../../crates/cose" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sha3-kmac = { version = "0.3.0", default-features = false } +thiserror = "2.0" +zeroize = "1.8" + +[workspace] + +[workspace.lints.rust] +unsafe_code = "deny" + +[workspace.lints.clippy] +dbg_macro = "deny" +expect_used = "deny" +large_include_file = "deny" +panic = "deny" +todo = "deny" +unimplemented = "deny" +unreachable = "deny" +unwrap_used = "deny" +wildcard_imports = "deny" diff --git a/tools/vector-goldens/src/main.rs b/tools/vector-goldens/src/main.rs new file mode 100644 index 0000000..0e9e720 --- /dev/null +++ b/tools/vector-goldens/src/main.rs @@ -0,0 +1,372 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Deterministically regenerates the committed COSE maintenance vectors. +//! +//! The separate `reallyme-cose-vector-audit` binary verifies the resulting +//! bytes with direct RustCrypto dependencies, so regeneration and audit do not +//! share the production COSE parser, verifier, or ML-KEM decryptor. + +use std::fs; +use std::io::Cursor; +use std::path::{Path, PathBuf}; + +use ciborium::value::Value; +use ed25519_dalek::{Signer, SigningKey}; +use reallyme_cose::{cose_sign1, cose_sign1_detached, Algorithm}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +mod ml_kem_encrypt; +mod pq; + +const VECTOR_PATH: &str = "vectors/cose-sign1.json"; +const ED25519_ALGORITHM: i64 = -19; + +#[derive(Debug, Error)] +enum RegenerateError { + #[error("vector file could not be read")] + Read, + #[error("vector JSON could not be decoded")] + JsonDecode, + #[error("vector JSON could not be encoded")] + JsonEncode, + #[error("vector file could not be written")] + Write, + #[error("vector case is missing")] + MissingCase, + #[error("vector hex is invalid")] + Hex, + #[error("vector algorithm is unsupported")] + Algorithm, + #[error("COSE signing failed")] + Sign, + #[error("COSE encryption vector generation failed")] + Encrypt, + #[error("post-quantum key and Sign1 vector generation failed")] + PostQuantum, + #[error("CBOR decoding failed")] + CborDecode, + #[error("CBOR encoding failed")] + CborEncode, + #[error("COSE_Sign1 shape is invalid")] + Sign1Shape, + #[error("Ed25519 private seed has the wrong length")] + SeedLength, + #[error("ECDSA signature has the wrong width")] + SignatureWidth, + #[error("integer conversion failed")] + IntegerConversion, +} + +#[derive(Debug, Deserialize, Serialize)] +struct Suite { + schema: String, + suite: String, + cases: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +struct Case { + id: String, + operation: String, + algorithm: String, + kid_hex: String, + public_key_hex: String, + private_key_seed_hex: String, + payload_hex: String, + cose_sign1_hex: String, + expected_error: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + provenance: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +enum Provenance { + NodeOpenSslRfc8032, +} + +struct Bases { + ed_attached: Vec, + ed_detached: Vec, + p256_attached: Vec, + p256_detached: Vec, + p384_attached: Vec, + p384_detached: Vec, + p521_attached: Vec, + p521_detached: Vec, + secp256k1_attached: Vec, + secp256k1_detached: Vec, +} + +fn main() -> Result<(), RegenerateError> { + regenerate_sign1()?; + ml_kem_encrypt::regenerate().map_err(|_| RegenerateError::Encrypt)?; + pq::regenerate().map_err(|_| RegenerateError::PostQuantum) +} + +fn regenerate_sign1() -> Result<(), RegenerateError> { + let root = repository_root(); + let path = root.join(VECTOR_PATH); + let input = fs::read(&path).map_err(|_| RegenerateError::Read)?; + let mut suite: Suite = + serde_json::from_slice(&input).map_err(|_| RegenerateError::JsonDecode)?; + let bases = build_bases(&suite)?; + + for case in &mut suite.cases { + let bytes = regenerate_case(case, &bases)?; + case.cose_sign1_hex = hex::encode(bytes); + } + + let mut output = serde_json::to_vec_pretty(&suite).map_err(|_| RegenerateError::JsonEncode)?; + output.push(b'\n'); + fs::write(path, output).map_err(|_| RegenerateError::Write) +} + +fn repository_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../..") +} + +fn build_bases(suite: &Suite) -> Result { + Ok(Bases { + ed_attached: sign_case(find_case(suite, "cose-sign1-ed25519-attached")?, false)?, + ed_detached: sign_case(find_case(suite, "cose-sign1-ed25519-detached")?, true)?, + p256_attached: sign_case(find_case(suite, "cose-sign1-es256-attached")?, false)?, + p256_detached: sign_case(find_case(suite, "cose-sign1-es256-detached")?, true)?, + p384_attached: sign_case(find_case(suite, "cose-sign1-es384-attached")?, false)?, + p384_detached: sign_case(find_case(suite, "cose-sign1-es384-detached")?, true)?, + p521_attached: sign_case(find_case(suite, "cose-sign1-es512-attached")?, false)?, + p521_detached: sign_case(find_case(suite, "cose-sign1-es512-detached")?, true)?, + secp256k1_attached: sign_case(find_case(suite, "cose-sign1-es256k-attached")?, false)?, + secp256k1_detached: sign_case(find_case(suite, "cose-sign1-es256k-detached")?, true)?, + }) +} + +fn find_case<'a>(suite: &'a Suite, id: &str) -> Result<&'a Case, RegenerateError> { + suite + .cases + .iter() + .find(|case| case.id == id) + .ok_or(RegenerateError::MissingCase) +} + +fn sign_case(case: &Case, detached: bool) -> Result, RegenerateError> { + let algorithm = parse_algorithm(&case.algorithm)?; + let private_key = decode_hex(&case.private_key_seed_hex)?; + let kid = decode_hex(&case.kid_hex)?; + let payload = decode_hex(&case.payload_hex)?; + let encoded = if detached { + cose_sign1_detached(algorithm, &payload, &private_key, Some(&kid)) + } else { + cose_sign1(algorithm, &payload, &private_key, Some(&kid)) + } + .map_err(|_| RegenerateError::Sign)?; + Ok(encoded.to_vec()) +} + +fn regenerate_case(case: &Case, bases: &Bases) -> Result, RegenerateError> { + let bytes = match case.id.as_str() { + "cose-sign1-ed25519-attached" => bases.ed_attached.clone(), + "cose-sign1-ed25519-detached" + | "cose-sign1-ed25519-detached-wrong-payload" + | "cose-sign1-ed25519-detached-wrong-kid" + | "cose-sign1-ed25519-detached-as-attached" => bases.ed_detached.clone(), + "cose-sign1-es256-attached" => bases.p256_attached.clone(), + "cose-sign1-es256-detached" | "cose-sign1-es256-detached-wrong-payload" => { + bases.p256_detached.clone() + } + "cose-sign1-es256-der-signature" => ecdsa_der_variant(&bases.p256_attached)?, + "cose-sign1-es384-attached" => bases.p384_attached.clone(), + "cose-sign1-es384-detached" => bases.p384_detached.clone(), + "cose-sign1-es512-attached" => bases.p521_attached.clone(), + "cose-sign1-es512-detached" => bases.p521_detached.clone(), + "cose-sign1-es256k-attached" => bases.secp256k1_attached.clone(), + "cose-sign1-es256k-detached" => bases.secp256k1_detached.clone(), + "cose-sign1-ed25519-tampered-signature" => tampered_variant(&bases.ed_attached)?, + "cose-sign1-ed25519-unsupported-alg" | "cose-sign1-ed25519-missing-alg" => { + decode_hex(&case.cose_sign1_hex)? + } + "cose-sign1-ed25519-crit-header" => critical_header_variant(case)?, + "cose-sign1-ed25519-unprotected-kid" => unprotected_kid_variant(&bases.ed_attached)?, + "cose-sign1-ed25519-attached-as-detached" => bases.ed_attached.clone(), + "cose-sign1-ed25519-reordered-protected-header" => reordered_header_variant(case)?, + "cose-sign1-ed25519-tagged-root" => tagged_variant(&bases.ed_attached)?, + // This fixed signature is produced by Node's OpenSSL-backed Ed25519 + // implementation. Regeneration must preserve the external oracle. + "cose-sign1-ed25519-node-openssl" => decode_hex(&case.cose_sign1_hex)?, + _ => return Err(RegenerateError::MissingCase), + }; + Ok(bytes) +} + +fn critical_header_variant(case: &Case) -> Result, RegenerateError> { + let kid = decode_hex(&case.kid_hex)?; + let payload = decode_hex(&case.payload_hex)?; + let seed = decode_hex(&case.private_key_seed_hex)?; + let protected = Value::Map(vec![ + ( + Value::Integer(1_i64.into()), + Value::Integer(ED25519_ALGORITHM.into()), + ), + ( + Value::Integer(2_i64.into()), + Value::Array(vec![Value::Integer(1_i64.into())]), + ), + (Value::Integer(4_i64.into()), Value::Bytes(kid)), + ]); + signed_ed25519_value(protected, Value::Map(Vec::new()), Some(payload), &seed) +} + +fn reordered_header_variant(case: &Case) -> Result, RegenerateError> { + let kid = decode_hex(&case.kid_hex)?; + let payload = decode_hex(&case.payload_hex)?; + let seed = decode_hex(&case.private_key_seed_hex)?; + let protected = Value::Map(vec![ + (Value::Integer(4_i64.into()), Value::Bytes(kid)), + ( + Value::Integer(1_i64.into()), + Value::Integer(ED25519_ALGORITHM.into()), + ), + ]); + signed_ed25519_value(protected, Value::Map(Vec::new()), Some(payload), &seed) +} + +fn signed_ed25519_value( + protected: Value, + unprotected: Value, + payload: Option>, + seed: &[u8], +) -> Result, RegenerateError> { + let seed: [u8; 32] = seed.try_into().map_err(|_| RegenerateError::SeedLength)?; + let protected_bytes = encode_cbor(&protected)?; + let signing_payload = payload.as_deref().ok_or(RegenerateError::Sign1Shape)?; + let sig_structure = Value::Array(vec![ + Value::Text("Signature1".to_owned()), + Value::Bytes(protected_bytes.clone()), + Value::Bytes(Vec::new()), + Value::Bytes(signing_payload.to_vec()), + ]); + let signature = SigningKey::from_bytes(&seed) + .sign(&encode_cbor(&sig_structure)?) + .to_bytes() + .to_vec(); + encode_cbor(&Value::Array(vec![ + Value::Bytes(protected_bytes), + unprotected, + payload.map_or(Value::Null, Value::Bytes), + Value::Bytes(signature), + ])) +} + +fn unprotected_kid_variant(base: &[u8]) -> Result, RegenerateError> { + let mut value = decode_cbor(base)?; + let array = value.as_array_mut().ok_or(RegenerateError::Sign1Shape)?; + let unprotected = array + .get_mut(1) + .and_then(Value::as_map_mut) + .ok_or(RegenerateError::Sign1Shape)?; + unprotected.push(( + Value::Integer(4_i64.into()), + Value::Bytes(b"shadow-kid".to_vec()), + )); + encode_cbor(&value) +} + +fn tampered_variant(base: &[u8]) -> Result, RegenerateError> { + let mut value = decode_cbor(base)?; + let signature = value + .as_array_mut() + .and_then(|array| array.get_mut(3)) + .and_then(Value::as_bytes_mut) + .ok_or(RegenerateError::Sign1Shape)?; + let last = signature + .last_mut() + .ok_or(RegenerateError::SignatureWidth)?; + *last ^= 0xff; + encode_cbor(&value) +} + +fn ecdsa_der_variant(base: &[u8]) -> Result, RegenerateError> { + let mut value = decode_cbor(base)?; + let signature = value + .as_array_mut() + .and_then(|array| array.get_mut(3)) + .and_then(Value::as_bytes_mut) + .ok_or(RegenerateError::Sign1Shape)?; + *signature = raw_p256_to_der(signature)?; + encode_cbor(&value) +} + +fn raw_p256_to_der(raw: &[u8]) -> Result, RegenerateError> { + if raw.len() != 64 { + return Err(RegenerateError::SignatureWidth); + } + let r = der_integer(&raw[..32])?; + let s = der_integer(&raw[32..])?; + let content_len = r + .len() + .checked_add(s.len()) + .ok_or(RegenerateError::IntegerConversion)?; + let content_len = u8::try_from(content_len).map_err(|_| RegenerateError::IntegerConversion)?; + let mut der = Vec::with_capacity(usize::from(content_len) + 2); + der.extend_from_slice(&[0x30, content_len]); + der.extend_from_slice(&r); + der.extend_from_slice(&s); + Ok(der) +} + +fn der_integer(scalar: &[u8]) -> Result, RegenerateError> { + let first_nonzero = scalar + .iter() + .position(|byte| *byte != 0) + .unwrap_or(scalar.len()); + let body = &scalar[first_nonzero..]; + if body.is_empty() { + return Err(RegenerateError::SignatureWidth); + } + let padding = usize::from(body[0] & 0x80 != 0); + let length = body + .len() + .checked_add(padding) + .ok_or(RegenerateError::IntegerConversion)?; + let length = u8::try_from(length).map_err(|_| RegenerateError::IntegerConversion)?; + let mut encoded = Vec::with_capacity(usize::from(length) + 2); + encoded.extend_from_slice(&[0x02, length]); + if padding == 1 { + encoded.push(0); + } + encoded.extend_from_slice(body); + Ok(encoded) +} + +fn tagged_variant(base: &[u8]) -> Result, RegenerateError> { + encode_cbor(&Value::Tag(18, Box::new(decode_cbor(base)?))) +} + +fn parse_algorithm(value: &str) -> Result { + match value { + "Ed25519" => Ok(Algorithm::Ed25519), + "P256" => Ok(Algorithm::P256), + "P384" => Ok(Algorithm::P384), + "P521" => Ok(Algorithm::P521), + "Secp256k1" => Ok(Algorithm::Secp256k1), + _ => Err(RegenerateError::Algorithm), + } +} + +fn decode_hex(value: &str) -> Result, RegenerateError> { + hex::decode(value).map_err(|_| RegenerateError::Hex) +} + +fn decode_cbor(bytes: &[u8]) -> Result { + ciborium::de::from_reader(Cursor::new(bytes)).map_err(|_| RegenerateError::CborDecode) +} + +fn encode_cbor(value: &Value) -> Result, RegenerateError> { + let mut bytes = Vec::new(); + ciborium::ser::into_writer(value, Cursor::new(&mut bytes)) + .map_err(|_| RegenerateError::CborEncode)?; + Ok(bytes) +} diff --git a/tools/vector-goldens/src/ml_kem_encrypt.rs b/tools/vector-goldens/src/ml_kem_encrypt.rs new file mode 100644 index 0000000..1cbe8cb --- /dev/null +++ b/tools/vector-goldens/src/ml_kem_encrypt.rs @@ -0,0 +1,468 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Deterministic maintenance generator for the ReallyMe ML-KEM COSE profile. +//! +//! Production encryption deliberately obtains ML-KEM randomness, CEKs, and +//! nonces from the operating-system CSPRNG. This unpublished tool supplies +//! fixed inputs only so the committed wire fixtures can be regenerated +//! byte-for-byte and audited by an implementation that does not link COSE. + +use std::fs; +use std::io::Cursor; +use std::path::{Path, PathBuf}; + +use aes_gcm::aead::consts::U12; +use aes_gcm::aead::{Aead, KeyInit, Payload}; +use aes_gcm::aes::Aes192; +use aes_gcm::{Aes128Gcm, Aes256Gcm, AesGcm}; +use aes_kw::{KwAes128, KwAes192, KwAes256}; +use ciborium::value::Value; +use ml_kem::kem::KeyExport; +use ml_kem::{Seed, B32}; +use reallyme_cose::{ + cose_key_from_public_bytes, derive_kid_from_cose_key_public, Algorithm, + REALLYME_COSE_ALG_ML_KEM_1024, REALLYME_COSE_ALG_ML_KEM_1024_A256KW, + REALLYME_COSE_ALG_ML_KEM_512, REALLYME_COSE_ALG_ML_KEM_512_A128KW, + REALLYME_COSE_ALG_ML_KEM_768, REALLYME_COSE_ALG_ML_KEM_768_A192KW, REALLYME_COSE_HEADER_EK, +}; +use serde::Serialize; +use sha3_kmac::Kmac256; +use thiserror::Error; +use zeroize::{Zeroize, Zeroizing}; + +const VECTOR_PATH: &str = "vectors/cose-encrypt-ml-kem.json"; +const COSE_ENCRYPT_TAG: u64 = 96; +const COSE_HEADER_ALGORITHM: i64 = 1; +const COSE_HEADER_KID: i64 = 4; +const COSE_HEADER_IV: i64 = 5; +const AES_GCM_A128: i64 = 1; +const AES_GCM_A192: i64 = 2; +const AES_GCM_A256: i64 = 3; +const AES_KW_A128: i64 = -3; +const AES_KW_A192: i64 = -4; +const AES_KW_A256: i64 = -5; +const AES_KW_OVERHEAD: usize = 8; +const BITS_PER_BYTE: usize = 8; + +type Aes192Gcm = AesGcm; + +#[derive(Debug, Error)] +pub(super) enum GenerateError { + #[error("ML-KEM vector parameter is invalid")] + InvalidParameter, + #[error("ML-KEM vector arithmetic overflowed")] + LengthOverflow, + #[error("ML-KEM vector cryptography failed")] + Crypto, + #[error("ML-KEM vector CBOR encoding failed")] + Cbor, + #[error("ML-KEM vector JSON encoding failed")] + Json, + #[error("ML-KEM vector file could not be written")] + Write, +} + +#[derive(Serialize)] +struct Suite { + schema: &'static str, + suite: &'static str, + note: &'static str, + cases: Vec, +} + +#[derive(Serialize)] +struct Case { + id: String, + kem_algorithm: &'static str, + mode: &'static str, + content_algorithm: &'static str, + private_key_seed_hex: String, + public_key_hex: String, + recipient_kid_hex: String, + encapsulation_randomness_hex: String, + iv_hex: String, + cek_hex: String, + plaintext_hex: String, + external_aad_hex: String, + supp_priv_info_hex: String, + cose_encrypt_hex: String, +} + +#[derive(Clone, Copy)] +enum Kem { + MlKem512, + MlKem768, + MlKem1024, +} + +impl Kem { + const fn name(self) -> &'static str { + match self { + Self::MlKem512 => "ML-KEM-512", + Self::MlKem768 => "ML-KEM-768", + Self::MlKem1024 => "ML-KEM-1024", + } + } + + const fn algorithm(self) -> Algorithm { + match self { + Self::MlKem512 => Algorithm::MlKem512, + Self::MlKem768 => Algorithm::MlKem768, + Self::MlKem1024 => Algorithm::MlKem1024, + } + } + + const fn direct_algorithm(self) -> i64 { + match self { + Self::MlKem512 => REALLYME_COSE_ALG_ML_KEM_512, + Self::MlKem768 => REALLYME_COSE_ALG_ML_KEM_768, + Self::MlKem1024 => REALLYME_COSE_ALG_ML_KEM_1024, + } + } + + const fn wrapped_algorithm(self) -> i64 { + match self { + Self::MlKem512 => REALLYME_COSE_ALG_ML_KEM_512_A128KW, + Self::MlKem768 => REALLYME_COSE_ALG_ML_KEM_768_A192KW, + Self::MlKem1024 => REALLYME_COSE_ALG_ML_KEM_1024_A256KW, + } + } + + const fn key_wrap_algorithm(self) -> i64 { + match self { + Self::MlKem512 => AES_KW_A128, + Self::MlKem768 => AES_KW_A192, + Self::MlKem1024 => AES_KW_A256, + } + } + + const fn key_length(self) -> usize { + match self { + Self::MlKem512 => 16, + Self::MlKem768 => 24, + Self::MlKem1024 => 32, + } + } +} + +#[derive(Clone, Copy)] +enum Mode { + Direct, + KeyWrap, +} + +struct EncapsulationOutput { + public_key: Vec, + ciphertext: Vec, + shared_secret: Zeroizing>, +} + +impl Mode { + const fn name(self) -> &'static str { + match self { + Self::Direct => "direct", + Self::KeyWrap => "key_wrap", + } + } +} + +pub(super) fn regenerate() -> Result<(), GenerateError> { + let mut cases = Vec::with_capacity(6); + for kem in [Kem::MlKem512, Kem::MlKem768, Kem::MlKem1024] { + cases.push(generate_case(kem, Mode::Direct)?); + cases.push(generate_case(kem, Mode::KeyWrap)?); + } + + let suite = Suite { + schema: "reallyme.cose.ml_kem_encrypt.vectors.v1", + suite: "cose-encrypt-ml-kem", + note: "Deterministic maintenance fixtures for the ReallyMe pre-IANA COSE ML-KEM profile. Fixed private seeds, encapsulation randomness, CEKs, and nonces are test data only and MUST NOT be used as production key-generation examples.", + cases, + }; + let mut output = serde_json::to_vec_pretty(&suite).map_err(|_| GenerateError::Json)?; + output.push(b'\n'); + fs::write(repository_root().join(VECTOR_PATH), output).map_err(|_| GenerateError::Write) +} + +fn generate_case(kem: Kem, mode: Mode) -> Result { + let marker = match kem { + Kem::MlKem512 => 0x51, + Kem::MlKem768 => 0x76, + Kem::MlKem1024 => 0xA1, + }; + let seed = patterned::<64>(marker); + let randomness = patterned::<32>(marker.wrapping_add(1)); + let iv = patterned::<12>(marker.wrapping_add(2)); + let plaintext = format!( + "ReallyMe deterministic {} {} COSE vector", + kem.name(), + mode.name() + ) + .into_bytes(); + let external_aad = format!("ReallyMe {} external AAD", kem.name()).into_bytes(); + let supp_priv_info = format!("ReallyMe {} private KDF context", kem.name()).into_bytes(); + let mut encapsulation = deterministic_encapsulation(kem, &seed, &randomness)?; + + let public_cose_key = cose_key_from_public_bytes(kem.algorithm(), &encapsulation.public_key) + .map_err(|_| GenerateError::Crypto)?; + let kid = + derive_kid_from_cose_key_public(&public_cose_key).map_err(|_| GenerateError::Crypto)?; + + let recipient_algorithm = match mode { + Mode::Direct => kem.direct_algorithm(), + Mode::KeyWrap => kem.wrapped_algorithm(), + }; + let recipient_protected = encode_cbor(&Value::Map(vec![ + ( + Value::Integer(COSE_HEADER_ALGORITHM.into()), + Value::Integer(recipient_algorithm.into()), + ), + ( + Value::Integer(COSE_HEADER_KID.into()), + Value::Bytes(kid.to_vec()), + ), + ]))?; + let content_algorithm = content_algorithm(kem); + let content_key_length = kem.key_length(); + let mut cek = Zeroizing::new(Vec::new()); + let (content_key, recipient_ciphertext) = match mode { + Mode::Direct => ( + derive_key( + &encapsulation.shared_secret, + content_algorithm, + content_key_length, + &recipient_protected, + &supp_priv_info, + )?, + Value::Null, + ), + Mode::KeyWrap => { + let kek = derive_key( + &encapsulation.shared_secret, + kem.key_wrap_algorithm(), + kem.key_length(), + &recipient_protected, + &supp_priv_info, + )?; + *cek = patterned_vec(content_key_length, marker.wrapping_add(3)); + let wrapped = wrap_key(kem, &kek, &cek)?; + (Zeroizing::new(cek.to_vec()), Value::Bytes(wrapped)) + } + }; + encapsulation.shared_secret.zeroize(); + + let body_protected = encode_cbor(&Value::Map(vec![( + Value::Integer(COSE_HEADER_ALGORITHM.into()), + Value::Integer(content_algorithm.into()), + )]))?; + let enc_structure = encode_cbor(&Value::Array(vec![ + Value::Text("Encrypt".to_owned()), + Value::Bytes(body_protected.clone()), + Value::Bytes(external_aad.clone()), + ]))?; + let ciphertext = encrypt_content(kem, &content_key, &iv, &enc_structure, &plaintext)?; + + let cose_encrypt = encode_cbor(&Value::Tag( + COSE_ENCRYPT_TAG, + Box::new(Value::Array(vec![ + Value::Bytes(body_protected), + Value::Map(vec![( + Value::Integer(COSE_HEADER_IV.into()), + Value::Bytes(iv.to_vec()), + )]), + Value::Bytes(ciphertext), + Value::Array(vec![Value::Array(vec![ + Value::Bytes(recipient_protected), + Value::Map(vec![( + Value::Integer(REALLYME_COSE_HEADER_EK.into()), + Value::Bytes(encapsulation.ciphertext), + )]), + recipient_ciphertext, + ])]), + ])), + ))?; + + Ok(Case { + id: format!( + "cose-encrypt-{}-{}", + kem.name().to_ascii_lowercase(), + mode.name() + ), + kem_algorithm: kem.name(), + mode: mode.name(), + content_algorithm: content_algorithm_name(kem), + private_key_seed_hex: hex::encode(seed), + public_key_hex: hex::encode(encapsulation.public_key), + recipient_kid_hex: hex::encode(kid), + encapsulation_randomness_hex: hex::encode(randomness), + iv_hex: hex::encode(iv), + cek_hex: hex::encode(cek.as_slice()), + plaintext_hex: hex::encode(plaintext), + external_aad_hex: hex::encode(external_aad), + supp_priv_info_hex: hex::encode(supp_priv_info), + cose_encrypt_hex: hex::encode(cose_encrypt), + }) +} + +fn deterministic_encapsulation( + kem: Kem, + seed: &[u8; 64], + randomness: &[u8; 32], +) -> Result { + let seed = Seed::try_from(seed.as_slice()).map_err(|_| GenerateError::InvalidParameter)?; + let message = + B32::try_from(randomness.as_slice()).map_err(|_| GenerateError::InvalidParameter)?; + match kem { + Kem::MlKem512 => { + let private = ml_kem::ml_kem_512::DecapsulationKey::from_seed(seed); + let public = private.encapsulation_key(); + let (ciphertext, mut shared) = public.encapsulate_deterministic(&message); + let output = EncapsulationOutput { + public_key: public.to_bytes().to_vec(), + ciphertext: ciphertext.to_vec(), + shared_secret: Zeroizing::new(shared.to_vec()), + }; + shared.zeroize(); + Ok(output) + } + Kem::MlKem768 => { + let private = ml_kem::ml_kem_768::DecapsulationKey::from_seed(seed); + let public = private.encapsulation_key(); + let (ciphertext, mut shared) = public.encapsulate_deterministic(&message); + let output = EncapsulationOutput { + public_key: public.to_bytes().to_vec(), + ciphertext: ciphertext.to_vec(), + shared_secret: Zeroizing::new(shared.to_vec()), + }; + shared.zeroize(); + Ok(output) + } + Kem::MlKem1024 => { + let private = ml_kem::ml_kem_1024::DecapsulationKey::from_seed(seed); + let public = private.encapsulation_key(); + let (ciphertext, mut shared) = public.encapsulate_deterministic(&message); + let output = EncapsulationOutput { + public_key: public.to_bytes().to_vec(), + ciphertext: ciphertext.to_vec(), + shared_secret: Zeroizing::new(shared.to_vec()), + }; + shared.zeroize(); + Ok(output) + } + } +} + +fn derive_key( + shared_secret: &[u8], + algorithm: i64, + output_length: usize, + recipient_protected: &[u8], + supp_priv_info: &[u8], +) -> Result>, GenerateError> { + let output_bits = output_length + .checked_mul(BITS_PER_BYTE) + .ok_or(GenerateError::LengthOverflow)?; + let output_bits = u64::try_from(output_bits).map_err(|_| GenerateError::LengthOverflow)?; + let context = encode_cbor(&Value::Array(vec![ + Value::Integer(algorithm.into()), + Value::Array(vec![ + Value::Integer(output_bits.into()), + Value::Bytes(recipient_protected.to_vec()), + ]), + Value::Bytes(supp_priv_info.to_vec()), + ]))?; + let mut kmac = Kmac256::new(shared_secret, &[]).map_err(|_| GenerateError::Crypto)?; + kmac.update(&context); + let mut output = Zeroizing::new(vec![0_u8; output_length]); + kmac.finalize_into(&mut output); + Ok(output) +} + +fn wrap_key(kem: Kem, kek: &[u8], cek: &[u8]) -> Result, GenerateError> { + let output_length = cek + .len() + .checked_add(AES_KW_OVERHEAD) + .ok_or(GenerateError::LengthOverflow)?; + let mut output = vec![0_u8; output_length]; + let wrapped = match kem { + Kem::MlKem512 => KwAes128::new_from_slice(kek) + .map_err(|_| GenerateError::Crypto)? + .wrap_key(cek, &mut output), + Kem::MlKem768 => KwAes192::new_from_slice(kek) + .map_err(|_| GenerateError::Crypto)? + .wrap_key(cek, &mut output), + Kem::MlKem1024 => KwAes256::new_from_slice(kek) + .map_err(|_| GenerateError::Crypto)? + .wrap_key(cek, &mut output), + } + .map_err(|_| GenerateError::Crypto)?; + Ok(wrapped.to_vec()) +} + +fn encrypt_content( + kem: Kem, + key: &[u8], + iv: &[u8; 12], + aad: &[u8], + plaintext: &[u8], +) -> Result, GenerateError> { + let payload = Payload { + msg: plaintext, + aad, + }; + match kem { + Kem::MlKem512 => Aes128Gcm::new_from_slice(key) + .map_err(|_| GenerateError::Crypto)? + .encrypt(iv.into(), payload), + Kem::MlKem768 => Aes192Gcm::new_from_slice(key) + .map_err(|_| GenerateError::Crypto)? + .encrypt(iv.into(), payload), + Kem::MlKem1024 => Aes256Gcm::new_from_slice(key) + .map_err(|_| GenerateError::Crypto)? + .encrypt(iv.into(), payload), + } + .map_err(|_| GenerateError::Crypto) +} + +const fn content_algorithm(kem: Kem) -> i64 { + match kem { + Kem::MlKem512 => AES_GCM_A128, + Kem::MlKem768 => AES_GCM_A192, + Kem::MlKem1024 => AES_GCM_A256, + } +} + +const fn content_algorithm_name(kem: Kem) -> &'static str { + match kem { + Kem::MlKem512 => "A128GCM", + Kem::MlKem768 => "A192GCM", + Kem::MlKem1024 => "A256GCM", + } +} + +fn patterned(marker: u8) -> [u8; N] { + let mut bytes = [0_u8; N]; + for (index, byte) in bytes.iter_mut().enumerate() { + let offset = u8::try_from(index % 251).unwrap_or(0); + *byte = marker.wrapping_add(offset); + } + bytes +} + +fn patterned_vec(length: usize, marker: u8) -> Vec { + (0..length) + .map(|index| marker.wrapping_add(u8::try_from(index % 251).unwrap_or(0))) + .collect() +} + +fn encode_cbor(value: &Value) -> Result, GenerateError> { + let mut bytes = Vec::new(); + ciborium::ser::into_writer(value, Cursor::new(&mut bytes)).map_err(|_| GenerateError::Cbor)?; + Ok(bytes) +} + +fn repository_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../..") +} diff --git a/tools/vector-goldens/src/pq.rs b/tools/vector-goldens/src/pq.rs new file mode 100644 index 0000000..181057d --- /dev/null +++ b/tools/vector-goldens/src/pq.rs @@ -0,0 +1,349 @@ +// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved +// +// SPDX-License-Identifier: Apache-2.0 + +//! Deterministic PQ COSE_Key, Multikey, and COSE_Sign1 vector generation. + +use std::fs; +use std::io::Cursor; +use std::path::{Path, PathBuf}; + +use ciborium::value::Value; +use ml_dsa::{KeyExport, Keypair, MlDsa44, MlDsa65, MlDsa87, MlDsaParams, Seed, SigningKey}; +use reallyme_cose::{ + cose_key_from_public_bytes, cose_key_to_multikey, cose_key_to_vec, cose_sign1, + derive_kid_from_cose_key_public, Algorithm, +}; +use serde::Serialize; +use thiserror::Error; + +const SIGN1_PATH: &str = "vectors/cose-sign1-pq.json"; +const KEY_PATH: &str = "vectors/cose-key-pq.json"; + +const ML_DSA_44_SEED: [u8; 32] = [ + 0x44, 0x91, 0x37, 0xf7, 0x36, 0xf5, 0xf5, 0xa3, 0x5e, 0xb9, 0xf3, 0x7c, 0x1c, 0x88, 0xc2, 0xa0, + 0xbc, 0xf1, 0x8e, 0x75, 0x7f, 0xfb, 0x92, 0x85, 0xab, 0x2c, 0x4c, 0x26, 0xc1, 0x5c, 0x55, 0xf1, +]; +const ML_DSA_65_SEED: [u8; 32] = [ + 0x65, 0x91, 0x37, 0xf7, 0x36, 0xf5, 0xf5, 0xa3, 0x5e, 0xb9, 0xf3, 0x7c, 0x1c, 0x88, 0xc2, 0xa0, + 0xbc, 0xf1, 0x8e, 0x75, 0x7f, 0xfb, 0x92, 0x85, 0xab, 0x2c, 0x4c, 0x26, 0xc1, 0x5c, 0x55, 0xf1, +]; +const ML_DSA_87_SEED: [u8; 32] = [ + 0x1e, 0x91, 0x37, 0xf7, 0x36, 0xf5, 0xf5, 0xa3, 0x5e, 0xb9, 0xf3, 0x7c, 0x1c, 0x88, 0xc2, 0xa0, + 0xbc, 0xf1, 0x8e, 0x75, 0x7f, 0xfb, 0x92, 0x85, 0xab, 0x2c, 0x4c, 0x26, 0xc1, 0x5c, 0x55, 0xf1, +]; + +#[derive(Debug, Error)] +pub(super) enum GenerateError { + #[error("PQ vector key generation failed")] + KeyGeneration, + #[error("PQ vector COSE conversion failed")] + Cose, + #[error("PQ vector serialization failed")] + Json, + #[error("PQ vector file could not be written")] + Write, + #[error("PQ COSE_Sign1 fixture could not be decoded")] + CborDecode, + #[error("PQ COSE_Sign1 fixture could not be encoded")] + CborEncode, + #[error("PQ COSE_Sign1 fixture has an invalid shape")] + Sign1Shape, +} + +#[derive(Serialize)] +struct Suite { + schema: &'static str, + suite: &'static str, + note: &'static str, + cases: Vec, +} + +#[derive(Serialize)] +struct Sign1Case { + id: String, + operation: &'static str, + algorithm: &'static str, + kid_hex: String, + public_key_hex: String, + private_key_seed_hex: String, + payload_hex: String, + cose_sign1_hex: String, + expected_error: Option<&'static str>, +} + +#[derive(Serialize)] +struct KeyCase { + id: String, + algorithm: &'static str, + private_key_seed_hex: String, + public_key_hex: String, + cose_key_hex: String, + multikey: String, +} + +pub(super) fn regenerate() -> Result<(), GenerateError> { + let mut sign1_cases = Vec::with_capacity(7); + let mut key_cases = Vec::with_capacity(6); + + add_ml_dsa::( + Algorithm::MlDsa44, + "ML-DSA-44", + &ML_DSA_44_SEED, + &mut sign1_cases, + &mut key_cases, + )?; + add_ml_dsa::( + Algorithm::MlDsa65, + "ML-DSA-65", + &ML_DSA_65_SEED, + &mut sign1_cases, + &mut key_cases, + )?; + add_ml_dsa::( + Algorithm::MlDsa87, + "ML-DSA-87", + &ML_DSA_87_SEED, + &mut sign1_cases, + &mut key_cases, + )?; + + add_ml_kem(Algorithm::MlKem512, "ML-KEM-512", 0x51, &mut key_cases)?; + add_ml_kem(Algorithm::MlKem768, "ML-KEM-768", 0x76, &mut key_cases)?; + add_ml_kem(Algorithm::MlKem1024, "ML-KEM-1024", 0xa1, &mut key_cases)?; + + write_suite( + SIGN1_PATH, + &Suite { + schema: "reallyme.cose.conformance.cose_sign1_pq.v1", + suite: "cose-sign1-pq", + note: "Deterministic ML-DSA COSE_Sign1 fixtures with independent COSE-layer parsing and direct primitive verification by the vector auditor.", + cases: sign1_cases, + }, + )?; + write_suite( + KEY_PATH, + &Suite { + schema: "reallyme.cose.conformance.cose_key_pq.v1", + suite: "cose-key-pq", + note: "ReallyMe AKP COSE_Key and Multikey fixtures for ML-DSA and the pre-IANA ReallyMe ML-KEM COSE profile.", + cases: key_cases, + }, + ) +} + +fn add_ml_dsa( + algorithm: Algorithm, + name: &'static str, + seed_bytes: &[u8; 32], + sign1_cases: &mut Vec, + key_cases: &mut Vec, +) -> Result<(), GenerateError> { + let seed = Seed::try_from(seed_bytes.as_slice()).map_err(|_| GenerateError::KeyGeneration)?; + let signing_key = SigningKey::

::from_seed(&seed); + let public = signing_key.verifying_key().to_bytes().to_vec(); + let cose_key = + cose_key_from_public_bytes(algorithm, &public).map_err(|_| GenerateError::Cose)?; + let kid = derive_kid_from_cose_key_public(&cose_key).map_err(|_| GenerateError::Cose)?; + let payload = format!("ReallyMe independent {name} COSE_Sign1 vector").into_bytes(); + let sign1 = + cose_sign1(algorithm, &payload, seed_bytes, Some(&kid)).map_err(|_| GenerateError::Cose)?; + + let kid_hex = hex::encode(&kid); + let public_key_hex = hex::encode(&public); + let private_key_seed_hex = hex::encode(seed_bytes); + let payload_hex = hex::encode(&payload); + + sign1_cases.push(Sign1Case { + id: format!("cose-sign1-{}-attached", name.to_ascii_lowercase()), + operation: "verify_attached", + algorithm: name, + kid_hex: kid_hex.clone(), + public_key_hex: public_key_hex.clone(), + private_key_seed_hex: private_key_seed_hex.clone(), + payload_hex: payload_hex.clone(), + cose_sign1_hex: hex::encode(&sign1), + expected_error: None, + }); + if algorithm == Algorithm::MlDsa44 { + add_ml_dsa_negative_cases( + name, + &kid_hex, + &public_key_hex, + &private_key_seed_hex, + &payload, + &sign1, + sign1_cases, + )?; + } + key_cases.push(key_case(algorithm, name, seed_bytes, &public)?); + Ok(()) +} + +fn add_ml_dsa_negative_cases( + name: &'static str, + kid_hex: &str, + public_key_hex: &str, + private_key_seed_hex: &str, + payload: &[u8], + sign1: &[u8], + cases: &mut Vec, +) -> Result<(), GenerateError> { + for (suffix, mutation, expected_error) in [ + ( + "tampered-signature", + Sign1Mutation::TamperSignature, + "InvalidSignature", + ), + ( + "wrong-payload", + Sign1Mutation::TamperPayload, + "InvalidSignature", + ), + ( + "truncated-signature", + Sign1Mutation::TruncateSignature, + "InvalidSignatureEncoding", + ), + ( + "extended-signature", + Sign1Mutation::ExtendSignature, + "InvalidSignatureEncoding", + ), + ] { + let (mutated, effective_payload) = mutate_sign1(sign1, payload, mutation)?; + cases.push(Sign1Case { + id: format!("cose-sign1-{}-{suffix}", name.to_ascii_lowercase()), + operation: "verify_attached", + algorithm: name, + kid_hex: kid_hex.to_owned(), + public_key_hex: public_key_hex.to_owned(), + private_key_seed_hex: private_key_seed_hex.to_owned(), + payload_hex: hex::encode(effective_payload), + cose_sign1_hex: hex::encode(mutated), + expected_error: Some(expected_error), + }); + } + Ok(()) +} + +#[derive(Clone, Copy)] +enum Sign1Mutation { + TamperSignature, + TamperPayload, + TruncateSignature, + ExtendSignature, +} + +fn mutate_sign1( + encoded: &[u8], + original_payload: &[u8], + mutation: Sign1Mutation, +) -> Result<(Vec, Vec), GenerateError> { + let mut value: Value = + ciborium::de::from_reader(Cursor::new(encoded)).map_err(|_| GenerateError::CborDecode)?; + let array = match &mut value { + Value::Array(array) if array.len() == 4 => array, + _ => return Err(GenerateError::Sign1Shape), + }; + let mut effective_payload = original_payload.to_vec(); + match mutation { + Sign1Mutation::TamperPayload => { + let payload = match array.get_mut(2) { + Some(Value::Bytes(payload)) => payload, + _ => return Err(GenerateError::Sign1Shape), + }; + let first = payload.first_mut().ok_or(GenerateError::Sign1Shape)?; + *first ^= 1; + effective_payload = payload.clone(); + } + Sign1Mutation::TamperSignature + | Sign1Mutation::TruncateSignature + | Sign1Mutation::ExtendSignature => { + let signature = match array.get_mut(3) { + Some(Value::Bytes(signature)) => signature, + _ => return Err(GenerateError::Sign1Shape), + }; + match mutation { + Sign1Mutation::TamperSignature => { + let last = signature.last_mut().ok_or(GenerateError::Sign1Shape)?; + *last ^= 1; + } + Sign1Mutation::TruncateSignature => { + signature.pop().ok_or(GenerateError::Sign1Shape)?; + } + Sign1Mutation::ExtendSignature => signature.push(0), + Sign1Mutation::TamperPayload => return Err(GenerateError::Sign1Shape), + } + } + } + let mut output = Vec::new(); + ciborium::ser::into_writer(&value, &mut output).map_err(|_| GenerateError::CborEncode)?; + Ok((output, effective_payload)) +} + +fn add_ml_kem( + algorithm: Algorithm, + name: &'static str, + marker: u8, + key_cases: &mut Vec, +) -> Result<(), GenerateError> { + let seed_bytes = patterned::<64>(marker); + let seed = + ml_kem::Seed::try_from(seed_bytes.as_slice()).map_err(|_| GenerateError::KeyGeneration)?; + let public = match algorithm { + Algorithm::MlKem512 => ml_kem::ml_kem_512::DecapsulationKey::from_seed(seed) + .encapsulation_key() + .to_bytes() + .to_vec(), + Algorithm::MlKem768 => ml_kem::ml_kem_768::DecapsulationKey::from_seed(seed) + .encapsulation_key() + .to_bytes() + .to_vec(), + Algorithm::MlKem1024 => ml_kem::ml_kem_1024::DecapsulationKey::from_seed(seed) + .encapsulation_key() + .to_bytes() + .to_vec(), + _ => return Err(GenerateError::KeyGeneration), + }; + key_cases.push(key_case(algorithm, name, &seed_bytes, &public)?); + Ok(()) +} + +fn key_case( + algorithm: Algorithm, + name: &'static str, + seed: &[u8], + public: &[u8], +) -> Result { + let key = cose_key_from_public_bytes(algorithm, public).map_err(|_| GenerateError::Cose)?; + Ok(KeyCase { + id: format!("cose-key-{}-public", name.to_ascii_lowercase()), + algorithm: name, + private_key_seed_hex: hex::encode(seed), + public_key_hex: hex::encode(public), + cose_key_hex: hex::encode(cose_key_to_vec(&key).map_err(|_| GenerateError::Cose)?), + multikey: cose_key_to_multikey(&key) + .map_err(|_| GenerateError::Cose)? + .to_string(), + }) +} + +fn write_suite(path: &str, suite: &impl Serialize) -> Result<(), GenerateError> { + let mut output = serde_json::to_vec_pretty(suite).map_err(|_| GenerateError::Json)?; + output.push(b'\n'); + fs::write(repository_root().join(path), output).map_err(|_| GenerateError::Write) +} + +fn patterned(marker: u8) -> [u8; N] { + let mut bytes = [0_u8; N]; + for (index, byte) in bytes.iter_mut().enumerate() { + let offset = u8::try_from(index % 251).unwrap_or(0); + *byte = marker.wrapping_add(offset); + } + bytes +} + +fn repository_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../..") +} diff --git a/vectors/README.md b/vectors/README.md new file mode 100644 index 0000000..914bb15 --- /dev/null +++ b/vectors/README.md @@ -0,0 +1,34 @@ + + +# COSE Conformance Vectors + +These suites pin ReallyMe's supported COSE encodings and exercise parsing, +protected-header handling, Sig_structure construction, COSE_Key profiles, +Multikey conversion, ML-KEM recipient processing, KDF inputs, AES Key Wrap, and +authenticated decryption. + +`reallyme-cose-vector-audit` is independent at the COSE layer: it does not +depend on `reallyme-cose`, `reallyme-crypto`, or `reallyme-codec`. It uses direct +RustCrypto dependencies as primitive oracles, so it is not an independent +cryptographic implementation. Each suite file is bound into `manifest.json` by +its exact SHA-256 digest and case count. + +Primitive conformance belongs to the pinned `reallyme-crypto` dependency. Its +external-vector audit covers NIST ACVP ML-DSA key generation and signature +verification, NIST ACVP ML-KEM key generation and encapsulation, and additional +CCTV and Wycheproof adversarial corpora. Keeping those large primitive suites at +the primitive boundary avoids duplicating them here while COSE tests verify that +their typed rejection behavior survives the protocol wrapper. + +The classical COSE examples in RFC 9052 and RFC 9053 use generic algorithm +identifiers that this profile deliberately rejects. They therefore cannot serve +as positive interoperability fixtures for the fully specified algorithm IDs +accepted by this crate. The `cose-sign1-ed25519-node-openssl` case instead uses +RFC 8032 test-vector key material and a signature produced by Node's +OpenSSL-backed Ed25519 implementation over a COSE Sig_structure containing the +profile's fully specified Ed25519 identifier. The golden generator deliberately +preserves those bytes rather than recreating the signature with production code. diff --git a/vectors/cose-encrypt-ml-kem.json b/vectors/cose-encrypt-ml-kem.json new file mode 100644 index 0000000..63d9f59 --- /dev/null +++ b/vectors/cose-encrypt-ml-kem.json @@ -0,0 +1,103 @@ +{ + "schema": "reallyme.cose.ml_kem_encrypt.vectors.v1", + "suite": "cose-encrypt-ml-kem", + "note": "Deterministic maintenance fixtures for the ReallyMe pre-IANA COSE ML-KEM profile. Fixed private seeds, encapsulation randomness, CEKs, and nonces are test data only and MUST NOT be used as production key-generation examples.", + "cases": [ + { + "id": "cose-encrypt-ml-kem-512-direct", + "kem_algorithm": "ML-KEM-512", + "mode": "direct", + "content_algorithm": "A128GCM", + "private_key_seed_hex": "5152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f90", + "public_key_hex": "14d68e92a302b1ac79efd874772880f7e1b497414942210d3a248a407bcb1f6316d262960031c2bfcaa492c79fc9d277489c4e4d834eaffac351c58f4bc3b3a33cba73ac439ad495426cb78750208f73885ed97456c3177ec5b57a58b285fb4ce5d07214d23fb9370c2f032567bcc37cd61308083bfcb9510d6b24df6216c4996205ba14ccf914e6a336a5a60ee1a7618e8882191030a14ca3c0a8b204d0cba55878c78406fba313782a23517309524c818a32b8db18807a44c13db6015ae4c36a4966d5390ca55476d1a3494dbb9d625b178ca7898b6c2e0d8833d2c93bbf01c7b4a00c45a3015af14ac98c2c7d6acdde406d20f3ae02142c9a24b31b48b65e3b76aa58038cc457a5c107f2a694b1d37be10159622792c3210d7604491ae53378029f36116c0506c4ebc725ddd26271a09cfec75206d62166e77ba42264ae2c11731aaa99e555b6227877c0077e8b2cbd897c45da15b3d2636082686baa99504b74bba1181da8c5d20c919b23a6d3b14b7c028788d600ce5a12dda51c73d47d48b37f424c448a7a822e1526ed34cabc99035fb5202902ca25e314967a809b804eaef4099263079ff966e0776ed375c7c1b962183a103f4a3a2f8b126d67cc19cb769f41b2023b43e20965c42841dd9a8f2fdb4c9a6847a40a4f4d4caaa155673e1500aca5a648c719973b81aa1b7bb42596aac640da72b9114498c5b4b54c78c954c9199272b85335629e71794278475995ca79394751c7227e7b9cfbc1ad352a3752d946669b8b5ee99c7cfb4b73e8479ea79a2842a33b1b61f7261e813975b15474a329a8f652522a9629730c9979d93051d52c645621e7668105885b15c033b29ac5efb60c0f211dd556883378a1151c540c40acfc55b4cd8441703524c53b65e6d8842f549657541528419632035bedb49863c232fa217c988053ee007aae732e3c350e76919e89e19e5d035450d97a7f333a4a6a3ce5aca151227413caa769989765b618772b8e0eeb4c2cdc467bbc78dcac31d31a080c177e92ac365922ccf34c24c341cea706a19f79c7f1c1a23fd8b60f5a144110addf019e5519a3aef7f811821a513f7666db17cd37a86a5dd1b82226dcc89f517ca7a8ac423c9b", + "recipient_kid_hex": "3f52b64ecf20d8240ba3efcaba367d4f1a8ea6c8353028c9105d3647fef91fc8", + "encapsulation_randomness_hex": "52535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f7071", + "iv_hex": "535455565758595a5b5c5d5e", + "cek_hex": "", + "plaintext_hex": "5265616c6c794d652064657465726d696e6973746963204d4c2d4b454d2d3531322064697265637420434f534520766563746f72", + "external_aad_hex": "5265616c6c794d65204d4c2d4b454d2d3531322065787465726e616c20414144", + "supp_priv_info_hex": "5265616c6c794d65204d4c2d4b454d2d3531322070726976617465204b444620636f6e74657874", + "cose_encrypt_hex": "d8608443a10101a1054c535455565758595a5b5c5d5e584443361e15ad88fe4c846401a4e0583ee3fced4ffbfa769afb17a0feef2d409dffbf77b2b4b50f3ac0e4380653cb51a4ccbf962509d92a5057af9c471d924cc16660490e3b8183582aa2013a000100000458203f52b64ecf20d8240ba3efcaba367d4f1a8ea6c8353028c9105d3647fef91fc8a13a00010006590300ed7b186380c5573871bf658913e2bc3b2c2304783441cc242733c5065cd55c13be28101cadd740c74343700bbe772e4b4cb535531b5b198d60b3b877209708a7a30fcc40bd4ee32916f0cbf20b233d20696670117f2d7c14bcb0f92214d85d0beb3f036f9e95a9d5e6fec3f91966d9f37c6739d0bdc5fa8114ebcced7bb221356e9e4bc2013b366fecf3ab3dc2a0aa0173fca0c1913fa357ec685594bdd61f90d17a8d70fb59cf79c97f8fe9d0b46a56bce8526a2df91826ab3e5f8ddcbd97c792762d04a62ba62d10e3a5ec7f18913d556790ae854c3da1ca17aaba50bfcff3d407a46a3fb2229a60932659c87b71b2eb7db70f35b50b1ff1b9e8ad98b3fab43236b1817c7a6d0e91f4bcbb18cab8a04f94318671c6093acf705b1f98222cb92971fb2f9883bca9e96fd254a2b53e1c2c66f60e1a17775c3f7571cfac318d557d9151a4d5029146f1dc90bd4a181c456bdc342e896e47369dbe2d6ce4ee2c6c7f4e5a48b996f3a5c161d7baeffd96e8d0a39b198a1e9241964a6dcb1fb1196ea986457c7e329d6d690da940da92f0c4ea9f4ad2fbcce99ff0a59a166d404acb0873f906b9f8a3106d48ca8e3151a775d2167836ce90376c8ce51fa4fbb8cd57f134e79cb19a9eb185be93115b28d6a1948457c88883b66c287c9846594cccb128ecb3dc05fe1ef110c51cba41275f98cb3d713acd89acdafc8cf0fb81473b7c720e07430495ceca7850016dc873a279fdae94b221bcf2943c95a6e7cd87420bc23c295b50270ecd074e2b789f1462718da5b6adc93843ac4d26566f04bf615aa10c3a96a426a6d546c258968bf235b903457de531583cc500d675177e05bdebfbbf2b478d7c7f40f2b90abb39cab580912f83ca7e0d978eaaf5d80f80cdfa3f2bfefb6604038874cdcf5e5c27cf1d002fa4c0097e9dfbc964bb0f9f7d5ce4b35bdc494e12f99af1292e8d5f22ecfc5e5cdb3c2a5fdde3a34e8f1e7fec26aae3162d2fd1802444d3585f2a8b3f886c1485eb77a34143a101072fa377e2d03e33074ac99bfc22bfd73002c6c654ad72af4a5726d9a5a48421f779f2017e4e0ffcf6" + }, + { + "id": "cose-encrypt-ml-kem-512-key_wrap", + "kem_algorithm": "ML-KEM-512", + "mode": "key_wrap", + "content_algorithm": "A128GCM", + "private_key_seed_hex": "5152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f90", + "public_key_hex": "14d68e92a302b1ac79efd874772880f7e1b497414942210d3a248a407bcb1f6316d262960031c2bfcaa492c79fc9d277489c4e4d834eaffac351c58f4bc3b3a33cba73ac439ad495426cb78750208f73885ed97456c3177ec5b57a58b285fb4ce5d07214d23fb9370c2f032567bcc37cd61308083bfcb9510d6b24df6216c4996205ba14ccf914e6a336a5a60ee1a7618e8882191030a14ca3c0a8b204d0cba55878c78406fba313782a23517309524c818a32b8db18807a44c13db6015ae4c36a4966d5390ca55476d1a3494dbb9d625b178ca7898b6c2e0d8833d2c93bbf01c7b4a00c45a3015af14ac98c2c7d6acdde406d20f3ae02142c9a24b31b48b65e3b76aa58038cc457a5c107f2a694b1d37be10159622792c3210d7604491ae53378029f36116c0506c4ebc725ddd26271a09cfec75206d62166e77ba42264ae2c11731aaa99e555b6227877c0077e8b2cbd897c45da15b3d2636082686baa99504b74bba1181da8c5d20c919b23a6d3b14b7c028788d600ce5a12dda51c73d47d48b37f424c448a7a822e1526ed34cabc99035fb5202902ca25e314967a809b804eaef4099263079ff966e0776ed375c7c1b962183a103f4a3a2f8b126d67cc19cb769f41b2023b43e20965c42841dd9a8f2fdb4c9a6847a40a4f4d4caaa155673e1500aca5a648c719973b81aa1b7bb42596aac640da72b9114498c5b4b54c78c954c9199272b85335629e71794278475995ca79394751c7227e7b9cfbc1ad352a3752d946669b8b5ee99c7cfb4b73e8479ea79a2842a33b1b61f7261e813975b15474a329a8f652522a9629730c9979d93051d52c645621e7668105885b15c033b29ac5efb60c0f211dd556883378a1151c540c40acfc55b4cd8441703524c53b65e6d8842f549657541528419632035bedb49863c232fa217c988053ee007aae732e3c350e76919e89e19e5d035450d97a7f333a4a6a3ce5aca151227413caa769989765b618772b8e0eeb4c2cdc467bbc78dcac31d31a080c177e92ac365922ccf34c24c341cea706a19f79c7f1c1a23fd8b60f5a144110addf019e5519a3aef7f811821a513f7666db17cd37a86a5dd1b82226dcc89f517ca7a8ac423c9b", + "recipient_kid_hex": "3f52b64ecf20d8240ba3efcaba367d4f1a8ea6c8353028c9105d3647fef91fc8", + "encapsulation_randomness_hex": "52535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f7071", + "iv_hex": "535455565758595a5b5c5d5e", + "cek_hex": "5455565758595a5b5c5d5e5f60616263", + "plaintext_hex": "5265616c6c794d652064657465726d696e6973746963204d4c2d4b454d2d353132206b65795f7772617020434f534520766563746f72", + "external_aad_hex": "5265616c6c794d65204d4c2d4b454d2d3531322065787465726e616c20414144", + "supp_priv_info_hex": "5265616c6c794d65204d4c2d4b454d2d3531322070726976617465204b444620636f6e74657874", + "cose_encrypt_hex": "d8608443a10101a1054c535455565758595a5b5c5d5e5846e8eba2b217776a48ebea422c4f98e3efa89bf97144f3a387b8cbde2978b95800d2b652ee1ef26b32b7c4f731247517d49348a84d7f75649a626319e5f9ce86a669058a00dc878183582aa2013a000100030458203f52b64ecf20d8240ba3efcaba367d4f1a8ea6c8353028c9105d3647fef91fc8a13a00010006590300ed7b186380c5573871bf658913e2bc3b2c2304783441cc242733c5065cd55c13be28101cadd740c74343700bbe772e4b4cb535531b5b198d60b3b877209708a7a30fcc40bd4ee32916f0cbf20b233d20696670117f2d7c14bcb0f92214d85d0beb3f036f9e95a9d5e6fec3f91966d9f37c6739d0bdc5fa8114ebcced7bb221356e9e4bc2013b366fecf3ab3dc2a0aa0173fca0c1913fa357ec685594bdd61f90d17a8d70fb59cf79c97f8fe9d0b46a56bce8526a2df91826ab3e5f8ddcbd97c792762d04a62ba62d10e3a5ec7f18913d556790ae854c3da1ca17aaba50bfcff3d407a46a3fb2229a60932659c87b71b2eb7db70f35b50b1ff1b9e8ad98b3fab43236b1817c7a6d0e91f4bcbb18cab8a04f94318671c6093acf705b1f98222cb92971fb2f9883bca9e96fd254a2b53e1c2c66f60e1a17775c3f7571cfac318d557d9151a4d5029146f1dc90bd4a181c456bdc342e896e47369dbe2d6ce4ee2c6c7f4e5a48b996f3a5c161d7baeffd96e8d0a39b198a1e9241964a6dcb1fb1196ea986457c7e329d6d690da940da92f0c4ea9f4ad2fbcce99ff0a59a166d404acb0873f906b9f8a3106d48ca8e3151a775d2167836ce90376c8ce51fa4fbb8cd57f134e79cb19a9eb185be93115b28d6a1948457c88883b66c287c9846594cccb128ecb3dc05fe1ef110c51cba41275f98cb3d713acd89acdafc8cf0fb81473b7c720e07430495ceca7850016dc873a279fdae94b221bcf2943c95a6e7cd87420bc23c295b50270ecd074e2b789f1462718da5b6adc93843ac4d26566f04bf615aa10c3a96a426a6d546c258968bf235b903457de531583cc500d675177e05bdebfbbf2b478d7c7f40f2b90abb39cab580912f83ca7e0d978eaaf5d80f80cdfa3f2bfefb6604038874cdcf5e5c27cf1d002fa4c0097e9dfbc964bb0f9f7d5ce4b35bdc494e12f99af1292e8d5f22ecfc5e5cdb3c2a5fdde3a34e8f1e7fec26aae3162d2fd1802444d3585f2a8b3f886c1485eb77a34143a101072fa377e2d03e33074ac99bfc22bfd73002c6c654ad72af4a5726d9a5a48421f779f2017e4e0ffc5818fef3640505cf32baa6a239f4dace97f345dbf425012d5a63" + }, + { + "id": "cose-encrypt-ml-kem-768-direct", + "kem_algorithm": "ML-KEM-768", + "mode": "direct", + "content_algorithm": "A192GCM", + "private_key_seed_hex": "767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5", + "public_key_hex": "59765536dc320bda853d3a597e258379e8c2b829c2f3b73334d60bd04775c7f37a33a90cf80689c40b08d903c88c148a529a43c29b9001fc51f20726c8fc4b87ca4211817e32fc066aec4f09b71c29d68d0507c4ead26c15b01f49e6cbf562b10e871d3051307f20c52a43b66a74894126a92c0243d1769bd57bc01c7908c47043fb790961e329abfb7702e1cffc8040b0456302332b3592461cfb8862c0199bab7d3477c6c085b524138b2a177bda6151710a0541bc710762ba688486897036680a9cb6f503331851be68ac33741301084f6fd18d3c5587638809c4b8411e25265fa1c268976afb2a718d034855b2ba30ca2275a4c9668abe75a960f5fa06d615a8928c43c6f8b2b8b4c451430b95c2279b500c4ea0a12778c317b97906bb263af85255155d48e5a1fac0666ce96ec632035c403ecf4bcd573ac6f65c54db24c1e173acf31707f19412bd5cb7ece993414015beb457bd6c4ad3a49c5f51b7b317b8f6c88f04574b9bec3c48486f6e08220316cb3a5230b198b6d9e80550fc63cdb99ed582037718c64bab9f3f683a1198785e602a913bbed2dc9f7f7b5a40d460370812a4ea9549f8990d982532994f7c18b6987151de49591e7b96444027a9b39d7cc072df979a1dcb24d2400c88d991ff873230a965f9106f817b1c3a3097b4f0a0667ab1fab943c1f3a953c7ad2e3c901263015e70bb858bc2597b0866f75d98038ef022763085684634133c658d79028eb9652953c5051c051aced49f2a09ab2d89cb8cd8c900a736e4a503a92c55d916932038019ff73586d38bff1a09ee678b372699075c535c60b87c1677d3e7ae6a53aa217429d889b09c2129ea8764af250c281762a3c7146d431db84bb465027806fc531ed644496c653f25779f3209aa8977af2668c185634165a0d208350cb499c4164c9ed2685fa0554a352f0823939488421765cbbe07b1960ca5f2b807f657bee62a49628c69ea12b0f75aa21acc11905bbd6f26a54f31b5d9b6b13e40c3c81a219f4b352773bd8828248d1c0578672639d6061f0c59ab6328f94a8c3380c97e256d365b014b291308981e16d0a79dc1c790d54c3bc38fbeba2d057514945b727d39ceb27b42a84779fd85c2f1550c707629e9d66c47f59dd10412125c40b2d741c95601f4367b5faa19ab215a925b73fc40c75b85cb63769b86f7cc140a82bf72121006260891a1377c1960787d6da6aadd128c739688e9f2b82d8586f55056f16307c5190f87c7246e9b710e87bc47fca91a5ba0a6189d099c7bbe3a028690201e9bca73dc9c441c879d69c2afba23972a0bcf25c00f2637dc8b40a5861195084ad6e504913ac36b87558c10718ca14279da7d83564d3d5c57ee61a7161a6b5078ca0650695c891a00b013ff619ca5e91f7461235677266ee07c4848c84dd9697b213f77c634c6e6ab6ec4c680e2ca4c87014d98aaff17abad2bbda04cb5a3a329cf06a50b32a69f27a5303b45b90036c62944460b5458657d9bfb46e97242b9c58eeac1c74e0cca46aacd618cb242242a808440c789aad00929640c742a4c82a94bc0cf8668af5821fa4894124c8de359a9a2c38082a209b1653823ec1abc80a73091617e3912206464d90ffb6c3a0479eff6c802c0a48a1a7173cf91f72d3c5ad187f0eaa0823e4480", + "recipient_kid_hex": "031c1e68160fa7ba3300ae080170347e4b8e5c0b7a464e732a2526fc3cee6e85", + "encapsulation_randomness_hex": "7778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f90919293949596", + "iv_hex": "78797a7b7c7d7e7f80818283", + "cek_hex": "", + "plaintext_hex": "5265616c6c794d652064657465726d696e6973746963204d4c2d4b454d2d3736382064697265637420434f534520766563746f72", + "external_aad_hex": "5265616c6c794d65204d4c2d4b454d2d3736382065787465726e616c20414144", + "supp_priv_info_hex": "5265616c6c794d65204d4c2d4b454d2d3736382070726976617465204b444620636f6e74657874", + "cose_encrypt_hex": "d8608443a10102a1054c78797a7b7c7d7e7f8081828358441c52a170f89d082848ffb2c0d51eb14f466a7911f87b35d3900fada5756ac179312bbb4d137da3beae21c25a3091d20f6e5e22e801ebf57cd8addd558baf26044ebd15618183582aa2013a00010001045820031c1e68160fa7ba3300ae080170347e4b8e5c0b7a464e732a2526fc3cee6e85a13a0001000659044012c8fd0c75940df5be2057c1df99f310c94f4fc4cb537137f24b0686f4ab57996933f5d78d23dab32be5b065fc80e868b5eb31936080e26d298465f71ade8abd4864dbb4e29ed0bbf38cf587cde68309822ed6b93fcf0bf96ae34dd2f8c75557e6b599f78d9e4507839dd85dc97c04e5e70082774b612d607aeaedd32c19fb9e949374fe16366b7165b5eb511be7b5ae78288612522b8521265562ea3b0b0023066b67379385f52a0ac62cfd6140c4bcf32bf7add7f389c75f05ba7b4f04ddf61177a151837bc80126cc2471fffc96406f08487052b4d84aa706b166b1db6a165bcb8f2d28588940be6d3be2e8c84d65e204e2610dea9e0c53afa718e86ecb7859169b5207022e0977be327809de9f7e31b886a7da959aca211a0f6e3aec33c6fcdddb8da5632fa21d2a7f5835fb0d86a4021625fae35704bb88de8ca3efb9be6155a71985e7b4964b2f50a545264c134951a69b790cf78acfe94ee9db449b5b618e04d7e6af6f8821b15cfca5812253137c8a7d96cddfd322a4e027832331c5909d464d9da5b8a5d5c605d09b63e7bd0262563796e4892bb808aa90477fb38df62e3ec2e24fbb7eafa1a2de67678744d536fbf013bde524246bd3f306b514d778fb0c327bfc2e5ee002bc014f179789b49b79ea099c57e51e8a55c1a447db6cb859103e0aa8117528eb0ef7a9e11dcb6595e2bc4c5f5c9454030b258ea56fa9805d4e2adc0a3eeb49da1681168f06bb2b3fa8ed47f40313830617980d09ad928710c408f13d5fef7440710a4e7a9b86ebad32476feea7fd69ddca2f2d3e850ae7b1ef0cfcfc6604f1bc7d3931cf23e7d9430040690aa5efc64fbd656a1857f28e2d03c96c04092bdaf377eb8248ac8a35edd159f3d75ed91df6a582a80f7907be14131061bbce31fd218812190748074c7ef44fe935c665993c3dd31fdda4d621869d7270ffd540219217de9834f390577f2dd6000b56b1c45fab79680175019bf40174020dfa34736aeb28575c2eff6d4f86f58a34daecf970fdc02fb91dfb980ef99f3cd6119b4afbf77cbce41c0178b8a64e640b8f1526f64b003759949f343a96695a07f2b7001889a53bc29dbc290bb29ce8506e812236c9a243f5384ea0c113ed616016c6278f5b14af5eb8e553d69ce48fdfec4aefd1f9a40a9609b0bf0dd5479504693299465a902a9bbedbc7f11316a148a9f7448e2e5bee5eb1bee85774494c560ebbedfa894dce81643eafd46a28a4bb6d1bbf04b4228cd4a29f33f726fd0a75fd90433f8c65f954478247e1844f6815f4acae08d9a835a86d247b36316ba03195e53fd2094182d8f287a5b37c8f0217ee22ab737a021a5c9ee0166836f55af6334da8e6c8ad1fef17fcec78231898a39bb15a7988d430a8e53f58262b4d1844272b731eb856f25ee8b48931494669b1a1487da0b7fc846dae82929cae5efeb6e2df570582210c50cf69ff0078de4337846d740e34785f990613a99567b6a845c177ce1ad83c46a0a699314a775bf76cf016594becdeec6ad72df6" + }, + { + "id": "cose-encrypt-ml-kem-768-key_wrap", + "kem_algorithm": "ML-KEM-768", + "mode": "key_wrap", + "content_algorithm": "A192GCM", + "private_key_seed_hex": "767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5", + "public_key_hex": "59765536dc320bda853d3a597e258379e8c2b829c2f3b73334d60bd04775c7f37a33a90cf80689c40b08d903c88c148a529a43c29b9001fc51f20726c8fc4b87ca4211817e32fc066aec4f09b71c29d68d0507c4ead26c15b01f49e6cbf562b10e871d3051307f20c52a43b66a74894126a92c0243d1769bd57bc01c7908c47043fb790961e329abfb7702e1cffc8040b0456302332b3592461cfb8862c0199bab7d3477c6c085b524138b2a177bda6151710a0541bc710762ba688486897036680a9cb6f503331851be68ac33741301084f6fd18d3c5587638809c4b8411e25265fa1c268976afb2a718d034855b2ba30ca2275a4c9668abe75a960f5fa06d615a8928c43c6f8b2b8b4c451430b95c2279b500c4ea0a12778c317b97906bb263af85255155d48e5a1fac0666ce96ec632035c403ecf4bcd573ac6f65c54db24c1e173acf31707f19412bd5cb7ece993414015beb457bd6c4ad3a49c5f51b7b317b8f6c88f04574b9bec3c48486f6e08220316cb3a5230b198b6d9e80550fc63cdb99ed582037718c64bab9f3f683a1198785e602a913bbed2dc9f7f7b5a40d460370812a4ea9549f8990d982532994f7c18b6987151de49591e7b96444027a9b39d7cc072df979a1dcb24d2400c88d991ff873230a965f9106f817b1c3a3097b4f0a0667ab1fab943c1f3a953c7ad2e3c901263015e70bb858bc2597b0866f75d98038ef022763085684634133c658d79028eb9652953c5051c051aced49f2a09ab2d89cb8cd8c900a736e4a503a92c55d916932038019ff73586d38bff1a09ee678b372699075c535c60b87c1677d3e7ae6a53aa217429d889b09c2129ea8764af250c281762a3c7146d431db84bb465027806fc531ed644496c653f25779f3209aa8977af2668c185634165a0d208350cb499c4164c9ed2685fa0554a352f0823939488421765cbbe07b1960ca5f2b807f657bee62a49628c69ea12b0f75aa21acc11905bbd6f26a54f31b5d9b6b13e40c3c81a219f4b352773bd8828248d1c0578672639d6061f0c59ab6328f94a8c3380c97e256d365b014b291308981e16d0a79dc1c790d54c3bc38fbeba2d057514945b727d39ceb27b42a84779fd85c2f1550c707629e9d66c47f59dd10412125c40b2d741c95601f4367b5faa19ab215a925b73fc40c75b85cb63769b86f7cc140a82bf72121006260891a1377c1960787d6da6aadd128c739688e9f2b82d8586f55056f16307c5190f87c7246e9b710e87bc47fca91a5ba0a6189d099c7bbe3a028690201e9bca73dc9c441c879d69c2afba23972a0bcf25c00f2637dc8b40a5861195084ad6e504913ac36b87558c10718ca14279da7d83564d3d5c57ee61a7161a6b5078ca0650695c891a00b013ff619ca5e91f7461235677266ee07c4848c84dd9697b213f77c634c6e6ab6ec4c680e2ca4c87014d98aaff17abad2bbda04cb5a3a329cf06a50b32a69f27a5303b45b90036c62944460b5458657d9bfb46e97242b9c58eeac1c74e0cca46aacd618cb242242a808440c789aad00929640c742a4c82a94bc0cf8668af5821fa4894124c8de359a9a2c38082a209b1653823ec1abc80a73091617e3912206464d90ffb6c3a0479eff6c802c0a48a1a7173cf91f72d3c5ad187f0eaa0823e4480", + "recipient_kid_hex": "031c1e68160fa7ba3300ae080170347e4b8e5c0b7a464e732a2526fc3cee6e85", + "encapsulation_randomness_hex": "7778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f90919293949596", + "iv_hex": "78797a7b7c7d7e7f80818283", + "cek_hex": "797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f90", + "plaintext_hex": "5265616c6c794d652064657465726d696e6973746963204d4c2d4b454d2d373638206b65795f7772617020434f534520766563746f72", + "external_aad_hex": "5265616c6c794d65204d4c2d4b454d2d3736382065787465726e616c20414144", + "supp_priv_info_hex": "5265616c6c794d65204d4c2d4b454d2d3736382070726976617465204b444620636f6e74657874", + "cose_encrypt_hex": "d8608443a10102a1054c78797a7b7c7d7e7f8081828358464f4d6646f7a0b740c636d357adb4a92a80da07087feb5456337de866a70e42ce1fe516e39cf0648f7fdff8f6c2927eb68d251d68e138f48e0bdd287b1198992845953dbd749b8183582aa2013a00010004045820031c1e68160fa7ba3300ae080170347e4b8e5c0b7a464e732a2526fc3cee6e85a13a0001000659044012c8fd0c75940df5be2057c1df99f310c94f4fc4cb537137f24b0686f4ab57996933f5d78d23dab32be5b065fc80e868b5eb31936080e26d298465f71ade8abd4864dbb4e29ed0bbf38cf587cde68309822ed6b93fcf0bf96ae34dd2f8c75557e6b599f78d9e4507839dd85dc97c04e5e70082774b612d607aeaedd32c19fb9e949374fe16366b7165b5eb511be7b5ae78288612522b8521265562ea3b0b0023066b67379385f52a0ac62cfd6140c4bcf32bf7add7f389c75f05ba7b4f04ddf61177a151837bc80126cc2471fffc96406f08487052b4d84aa706b166b1db6a165bcb8f2d28588940be6d3be2e8c84d65e204e2610dea9e0c53afa718e86ecb7859169b5207022e0977be327809de9f7e31b886a7da959aca211a0f6e3aec33c6fcdddb8da5632fa21d2a7f5835fb0d86a4021625fae35704bb88de8ca3efb9be6155a71985e7b4964b2f50a545264c134951a69b790cf78acfe94ee9db449b5b618e04d7e6af6f8821b15cfca5812253137c8a7d96cddfd322a4e027832331c5909d464d9da5b8a5d5c605d09b63e7bd0262563796e4892bb808aa90477fb38df62e3ec2e24fbb7eafa1a2de67678744d536fbf013bde524246bd3f306b514d778fb0c327bfc2e5ee002bc014f179789b49b79ea099c57e51e8a55c1a447db6cb859103e0aa8117528eb0ef7a9e11dcb6595e2bc4c5f5c9454030b258ea56fa9805d4e2adc0a3eeb49da1681168f06bb2b3fa8ed47f40313830617980d09ad928710c408f13d5fef7440710a4e7a9b86ebad32476feea7fd69ddca2f2d3e850ae7b1ef0cfcfc6604f1bc7d3931cf23e7d9430040690aa5efc64fbd656a1857f28e2d03c96c04092bdaf377eb8248ac8a35edd159f3d75ed91df6a582a80f7907be14131061bbce31fd218812190748074c7ef44fe935c665993c3dd31fdda4d621869d7270ffd540219217de9834f390577f2dd6000b56b1c45fab79680175019bf40174020dfa34736aeb28575c2eff6d4f86f58a34daecf970fdc02fb91dfb980ef99f3cd6119b4afbf77cbce41c0178b8a64e640b8f1526f64b003759949f343a96695a07f2b7001889a53bc29dbc290bb29ce8506e812236c9a243f5384ea0c113ed616016c6278f5b14af5eb8e553d69ce48fdfec4aefd1f9a40a9609b0bf0dd5479504693299465a902a9bbedbc7f11316a148a9f7448e2e5bee5eb1bee85774494c560ebbedfa894dce81643eafd46a28a4bb6d1bbf04b4228cd4a29f33f726fd0a75fd90433f8c65f954478247e1844f6815f4acae08d9a835a86d247b36316ba03195e53fd2094182d8f287a5b37c8f0217ee22ab737a021a5c9ee0166836f55af6334da8e6c8ad1fef17fcec78231898a39bb15a7988d430a8e53f58262b4d1844272b731eb856f25ee8b48931494669b1a1487da0b7fc846dae82929cae5efeb6e2df570582210c50cf69ff0078de4337846d740e34785f990613a99567b6a845c177ce1ad83c46a0a699314a775bf76cf016594becdeec6ad72d5820181c1cf47e5aac825d78c4eacacb1a55028ae1d550b7c2aa2ae04cf5ac924508" + }, + { + "id": "cose-encrypt-ml-kem-1024-direct", + "kem_algorithm": "ML-KEM-1024", + "mode": "direct", + "content_algorithm": "A256GCM", + "private_key_seed_hex": "a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0", + "public_key_hex": "7ba90d0ab9b78e421a14ea89e6e31a5cb9161fda58130b55ad651070c768c236bcec62a5a5d80855684f42043aae767dafa53cb1d39f36fc43f36a93effc16525005c768aa3db4401f893c6095cb5e82bb215670e81593916303c69664742c97814947a7e65fd3f754dd3a3ecdc2cdc9cb38e437215fcab7f9dc11e36b8d43a634702829c7580faa2b0bc29c3e3a7aa978e12b03b14f0660475f62ba08286b186446fbd9b9f4688cc9e68188dab8a2737fe3abcdbeec6b79f18a1c4629ca7a77adb254eb03af987447ac658a05a53ddf99aab3d316645a28f0bc8247473be1085cd4c178c2d5b9eab7a86aa8c850b964bd857d8d429a139a0ebd522bad3b53205c16d6e66100d325aca58f317642cbe76fc5ea058953815f32976468268f908c2dfc2995716ebcea49195a6635c7bd9319903f9ab53a6b5edb6c03bff34f1ba7618807133eb00e6acab4fcb77615f442f737b76622296221810daba83b8372c72ac5c821053dd05ea0349da753747f337fda856bc4013d90bb4cf4fbac21641853dc858e0382d6818166c1bcd64596c24b08e7ebcc18d39629112796da010d69af7f6b6d2d40a34c01ccbbba112fb7059548235e0383026acae629aac4222ce58c754f39b4792bb55c066b1d15aaead5c148b82ab868b74cc906cf1009d0872666f7342ada9dba7381a6728a0d31a677ba49cc2b9739d64f071863aa7303d0581c6457049f17c45e9c70fe9b766fbb57c3fca0e312c89719a08d9a7ec2bb29840c5b375c0b2ac7041e353b8c263419c4bb13321c41d32316797d9b85b0c7b0392e18558c82005bf31fcb2b440a873b60da20d511c99c0c8438fbb342c5084a11001932180e58cc0534543cb644bce223ceb4cf7a6c6150137bce18349ce6cbe8b9c19f81cf42c067c3c75e014223f7a0218edcade8c43c5ad01e89f4c25cb270e8bc901cd7162e6707e0127a0e06c1654796e45390c1eb27d22a83089b51a5d0c2e7ccb39e365c7a870006b2193518c629b1916f811a3a809dd8a56de9328cdb251c57a50f5b0b7c3e019b462a8729776fb32ba7b1caa57e603d81397665ac10e83a899f0c6f5725a2b5b68028a0773a1aa4e14568b05041e198b2ebb59f8c34a9a92872d08170a7a11e0aea396a468df0026371a4486a9a1adc79a2d7648db3c76e3b1a6985920e699a1eb11c00e1f9566371bf8ee27704f812e468ba22159277264ced05bab3b1a61f3b0d1ff441794b4dbefc2aa54b2903533bc7345c46d9b84b204267359672791d843642a665be3d38081602b406c6479090a286d14f22bcb8870a009ed09732f21394da28128b6e7b357cfda46b529656e6da2f80db415c0b69cf814549f9116b10b1851219f3fcca6066860e0ba556da95684b7a8ca6bfe0d47f71f67a1296737ab41aae196331a49de2d8b614902122923933a01f8137aa5d81430bb4a48780b9014a6d71b39ac2082aba2ca2fa5a74e1a5b96dd89548132d174b46b2a4432d3156e1d730961bc3bd220383d978fb609d314387f8b971d3485b3d0772c79114d96499b4a50ab461c54a221570da2cf2471066bb6f1e306eafa2ac28e06e5f37ae3f135849181304810565025ac380cb3db9b6b956a13397472b39cf27128a9096082a6b9dd1a1bf6e406c7210404c705a571c5ffda81e77c96883d21a1ce6a191456fcf736c847283b07a1439f4018cc713364658cbe1b417ec098a234e5caa272d05cbc688ab67b773720a47215c9a15394d456102a5cacf282571ea942ed8994556c9858aca2a1d253c25436e86156875e6a11883874e5315aac00c1aa10fd9b075209539e771ccf0b2944ad4ab87fb45d1d438f7907f7a1ac45045bc771616d90384229780a29ca490632b83454433db144e4b6fa87c4d2c1a340bca55567b7719a646e641bef0c2481fd84f99453975fc1f72dbc859c1629a453a0b3875d5ec123046c1cd487fe2c5537c07c41a376b8995918f889fb58b3139598c34f43be972219f38a4eafb6196b67f6fd41ae80398f390771100429b38cfd73b7a546982e4b876f6db76502c5ce2a813ea318ba5a2b80cca63570b56b8443c9fa39e03792acf6445c71777077504e8ba3106abaf8f47ab0069864daba302c2691730919a11c4f8637e9efb8c659b6ad3458692180fbafa20637f405e02b72a5dcc23f89c2664a5f5a3d9f1f269830e409812af59ebabef21", + "recipient_kid_hex": "6a07b401fb0cdf5ad22f667928827f82d3286a3c90b4c62d39aca841dfef9234", + "encapsulation_randomness_hex": "a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1", + "iv_hex": "a3a4a5a6a7a8a9aaabacadae", + "cek_hex": "", + "plaintext_hex": "5265616c6c794d652064657465726d696e6973746963204d4c2d4b454d2d313032342064697265637420434f534520766563746f72", + "external_aad_hex": "5265616c6c794d65204d4c2d4b454d2d313032342065787465726e616c20414144", + "supp_priv_info_hex": "5265616c6c794d65204d4c2d4b454d2d313032342070726976617465204b444620636f6e74657874", + "cose_encrypt_hex": "d8608443a10103a1054ca3a4a5a6a7a8a9aaabacadae5845c309019196d3b8257b799cc1791ab5d9a849b647af59ec35c978b3c81d4da31985d794600c45a2a6432796f668233c17b48a0f3871e9e4f51f792aebb3ff9010240fa62d5c8183582aa2013a000100020458206a07b401fb0cdf5ad22f667928827f82d3286a3c90b4c62d39aca841dfef9234a13a00010006590620c51d41c88177779a2b8222d4f8de7410cf5e81684b366030d1aab32e8b85faab01e884a4df2d4edfe304560201ff3dccdb140b72eb35906d41eaa89ae19bc0eed5b62e3d893367240e777eebc2837d0ed909522bdc02dafca164383e15f46a8adb9025789ae77dd6ad092ea7ff5b3af83f81a7d50f7505d4aec2f03884775bcf9389b1b69f6f573692fff9e3f05e49e39d084e390a49bdeba49421812bfbb1f0412cea82c9946db0bdeae6c29b85048d5ba3b2750ab86cd547b4160ced2cbb95f7325e6c7bfe75ac603cdd8965c10191ec07bf4e9aaf56195d4151d19615e1f54ac1ebe13d3ed0d7771be8e6cf2678c80bb86ce8af3ad117ec49e47b184624f7748d0c033901f70fe26e1abf27b9aa2a304d04077151bdf2e204eb60bb887c6040929dcbee11b660ecc34d72d073bfb34e0e7c9482371b0dc208fa28c1247ac590b81d4686bac634132adc53a0ab6ac913de8a0f88ab3262f57aed2cf7bea37099f7feaedc9e35e3ee6307d86c97ff74dfe56d754828b7c3d8a5c057ad3cd2b8259d5996ec95d33e05098ad72ac9746e7b29fdfaf5bfb0ee1b3e6dc69e863186b9a6aca49694f1bb7a828863b1193125d1c3824ee19c8a13967383eeb562e012ac56748fe82b9e7d5984195a19c7a0adaa9ad431e2836931eca8b9f53f15c0a61854485022154db48de7da1cf0166daa4f74147401ba845445506482037ef71a0dba98cb7f6a44bb9b65b511f7d2a193ba02d311a487d6dbedb5ec3943a18e5554869e8a4572409c098715d810d9477eb9dee86b375881c955a187c74f1fb5da60751876a064684072ecc56ad43ee3f5aa501375f8edeca68a235c3f9de288cc31113b7ff16fe2e28986402eef8f76be199f0f08779df05b82b2214f6f265541dc8fde2e82dba5d2c73207ed04edab486f68813d4a19692436bb12dedc3e347105ea8061e55508f2f6a9c2623d2d54c3f994fc4b64c3aeceb6157b2f9198152a513d989c0a7190ef79fad54c37d40b6ec390da1d17c2bff0f84da7a69d1f31ddacf8d557ab33cf0018d3f86b374113da1c09bb7ae68e10c988246fdab9ecdf93f467d346546d42120ff33efe0a5c59043ea23fb43a2beedb1b3ca8d84d7b0e6a100738e84b6c46dcbec72a42245a7073f18c1b18ff2904caa78396284e11eaddd8df015fdfa7ed73438ca5be768de28daad3eee8cbfe0e3d1390b4d505bcf8ae853ecbb1ebdd6b6292cd5bb552f11ddf6614f1a6af514186244debbd45b284539968d7b23a99d84ae0b0f0b72c40c5bc70a148f5478c4e72d4ac5d5d3974ab9b9fad1f90a51e32c4087434f27c2c099570e307f5f5c1dfcda1efd7f7f0d4c1913ba7a9be497c42fd9a29044fd4efdce9a6a652989bbf52da39f0ff9105b6867b8b8c0cbf8c8e9ee24ef78878f9e59af98726fbd885aca9c9accc1767486aba77df0d8dc63ca1fe60ca83bd4b39867a7a5216e5830fd1d4772704ffecb9b3bae78b3748ba0686d344c777270a88365e5345561c11901cddcf352ac7194cde4c80147a83a6fc7a163a8f8d2f1923ff1b665dd4e98904dec27d390254bf4b519beb88077effefa3ca625afc2868d75d52ffa51bbcdd127d6bde8fb6972b0cf009d50946d25f30b5db41c4a32ea04659848756c6713cd64694a11763cfb501f19c7161f92f3469e49e4ccc83c9ecfaaf2a3f939f6ee13bf2058a87d84c12191e77ee851524ede0f639f3f70bb88b0f5dc307ec1de266cbab9d9c92d97a077ed68c0dcc0d89ca10eb318f28de3a139fc85ae95f32723a63b32f30b472cc934ef5f3dddb0aadaa7691309ff0c6554f438b5887f3de659842e12a7478ff0f799736b3930415fc7c0cfe85e38131ea751c07b713b0fb5e5fbbb81ff7bd601426089d8fe3c4ae6402b7fce066d4585a66409be450b3cb1b06094e3b6fd81f8188f0c0e32985c5341fc2176c484f06d83e5347a116793fa7963790a303f65636b99097a438786809515b0fd1ae6ad466a3a2951b8151c67d813d08a13b18ac36d77dc9d4a2a1e0215898acdc5e606edb0f235642736339aa6237cf410f856dd5dc415f878e155254ecfbe6b003f4c761a3154fbd6d437220dceb151bbb5403fa74c8596d249b13656bc0536c23a4b5fabeee54fd8912f21ba4e29ad95d573edda6fd51c3e466a3c6afc6ca2d4f6a153c7854e7df6f0e62844051676ce4ce2610545d8bfc98f6" + }, + { + "id": "cose-encrypt-ml-kem-1024-key_wrap", + "kem_algorithm": "ML-KEM-1024", + "mode": "key_wrap", + "content_algorithm": "A256GCM", + "private_key_seed_hex": "a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0", + "public_key_hex": "7ba90d0ab9b78e421a14ea89e6e31a5cb9161fda58130b55ad651070c768c236bcec62a5a5d80855684f42043aae767dafa53cb1d39f36fc43f36a93effc16525005c768aa3db4401f893c6095cb5e82bb215670e81593916303c69664742c97814947a7e65fd3f754dd3a3ecdc2cdc9cb38e437215fcab7f9dc11e36b8d43a634702829c7580faa2b0bc29c3e3a7aa978e12b03b14f0660475f62ba08286b186446fbd9b9f4688cc9e68188dab8a2737fe3abcdbeec6b79f18a1c4629ca7a77adb254eb03af987447ac658a05a53ddf99aab3d316645a28f0bc8247473be1085cd4c178c2d5b9eab7a86aa8c850b964bd857d8d429a139a0ebd522bad3b53205c16d6e66100d325aca58f317642cbe76fc5ea058953815f32976468268f908c2dfc2995716ebcea49195a6635c7bd9319903f9ab53a6b5edb6c03bff34f1ba7618807133eb00e6acab4fcb77615f442f737b76622296221810daba83b8372c72ac5c821053dd05ea0349da753747f337fda856bc4013d90bb4cf4fbac21641853dc858e0382d6818166c1bcd64596c24b08e7ebcc18d39629112796da010d69af7f6b6d2d40a34c01ccbbba112fb7059548235e0383026acae629aac4222ce58c754f39b4792bb55c066b1d15aaead5c148b82ab868b74cc906cf1009d0872666f7342ada9dba7381a6728a0d31a677ba49cc2b9739d64f071863aa7303d0581c6457049f17c45e9c70fe9b766fbb57c3fca0e312c89719a08d9a7ec2bb29840c5b375c0b2ac7041e353b8c263419c4bb13321c41d32316797d9b85b0c7b0392e18558c82005bf31fcb2b440a873b60da20d511c99c0c8438fbb342c5084a11001932180e58cc0534543cb644bce223ceb4cf7a6c6150137bce18349ce6cbe8b9c19f81cf42c067c3c75e014223f7a0218edcade8c43c5ad01e89f4c25cb270e8bc901cd7162e6707e0127a0e06c1654796e45390c1eb27d22a83089b51a5d0c2e7ccb39e365c7a870006b2193518c629b1916f811a3a809dd8a56de9328cdb251c57a50f5b0b7c3e019b462a8729776fb32ba7b1caa57e603d81397665ac10e83a899f0c6f5725a2b5b68028a0773a1aa4e14568b05041e198b2ebb59f8c34a9a92872d08170a7a11e0aea396a468df0026371a4486a9a1adc79a2d7648db3c76e3b1a6985920e699a1eb11c00e1f9566371bf8ee27704f812e468ba22159277264ced05bab3b1a61f3b0d1ff441794b4dbefc2aa54b2903533bc7345c46d9b84b204267359672791d843642a665be3d38081602b406c6479090a286d14f22bcb8870a009ed09732f21394da28128b6e7b357cfda46b529656e6da2f80db415c0b69cf814549f9116b10b1851219f3fcca6066860e0ba556da95684b7a8ca6bfe0d47f71f67a1296737ab41aae196331a49de2d8b614902122923933a01f8137aa5d81430bb4a48780b9014a6d71b39ac2082aba2ca2fa5a74e1a5b96dd89548132d174b46b2a4432d3156e1d730961bc3bd220383d978fb609d314387f8b971d3485b3d0772c79114d96499b4a50ab461c54a221570da2cf2471066bb6f1e306eafa2ac28e06e5f37ae3f135849181304810565025ac380cb3db9b6b956a13397472b39cf27128a9096082a6b9dd1a1bf6e406c7210404c705a571c5ffda81e77c96883d21a1ce6a191456fcf736c847283b07a1439f4018cc713364658cbe1b417ec098a234e5caa272d05cbc688ab67b773720a47215c9a15394d456102a5cacf282571ea942ed8994556c9858aca2a1d253c25436e86156875e6a11883874e5315aac00c1aa10fd9b075209539e771ccf0b2944ad4ab87fb45d1d438f7907f7a1ac45045bc771616d90384229780a29ca490632b83454433db144e4b6fa87c4d2c1a340bca55567b7719a646e641bef0c2481fd84f99453975fc1f72dbc859c1629a453a0b3875d5ec123046c1cd487fe2c5537c07c41a376b8995918f889fb58b3139598c34f43be972219f38a4eafb6196b67f6fd41ae80398f390771100429b38cfd73b7a546982e4b876f6db76502c5ce2a813ea318ba5a2b80cca63570b56b8443c9fa39e03792acf6445c71777077504e8ba3106abaf8f47ab0069864daba302c2691730919a11c4f8637e9efb8c659b6ad3458692180fbafa20637f405e02b72a5dcc23f89c2664a5f5a3d9f1f269830e409812af59ebabef21", + "recipient_kid_hex": "6a07b401fb0cdf5ad22f667928827f82d3286a3c90b4c62d39aca841dfef9234", + "encapsulation_randomness_hex": "a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1", + "iv_hex": "a3a4a5a6a7a8a9aaabacadae", + "cek_hex": "a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3", + "plaintext_hex": "5265616c6c794d652064657465726d696e6973746963204d4c2d4b454d2d31303234206b65795f7772617020434f534520766563746f72", + "external_aad_hex": "5265616c6c794d65204d4c2d4b454d2d313032342065787465726e616c20414144", + "supp_priv_info_hex": "5265616c6c794d65204d4c2d4b454d2d313032342070726976617465204b444620636f6e74657874", + "cose_encrypt_hex": "d8608443a10103a1054ca3a4a5a6a7a8a9aaabacadae5847813ae99015725a816ab1b8b1def3c46133d2d6e4e3ed11ecb7951062f7e813795668cf5d3169c8674d2c18adc8c34884772eb2b7e00783881a2e47d444d48f463e881ab3438f6c8183582aa2013a000100050458206a07b401fb0cdf5ad22f667928827f82d3286a3c90b4c62d39aca841dfef9234a13a00010006590620c51d41c88177779a2b8222d4f8de7410cf5e81684b366030d1aab32e8b85faab01e884a4df2d4edfe304560201ff3dccdb140b72eb35906d41eaa89ae19bc0eed5b62e3d893367240e777eebc2837d0ed909522bdc02dafca164383e15f46a8adb9025789ae77dd6ad092ea7ff5b3af83f81a7d50f7505d4aec2f03884775bcf9389b1b69f6f573692fff9e3f05e49e39d084e390a49bdeba49421812bfbb1f0412cea82c9946db0bdeae6c29b85048d5ba3b2750ab86cd547b4160ced2cbb95f7325e6c7bfe75ac603cdd8965c10191ec07bf4e9aaf56195d4151d19615e1f54ac1ebe13d3ed0d7771be8e6cf2678c80bb86ce8af3ad117ec49e47b184624f7748d0c033901f70fe26e1abf27b9aa2a304d04077151bdf2e204eb60bb887c6040929dcbee11b660ecc34d72d073bfb34e0e7c9482371b0dc208fa28c1247ac590b81d4686bac634132adc53a0ab6ac913de8a0f88ab3262f57aed2cf7bea37099f7feaedc9e35e3ee6307d86c97ff74dfe56d754828b7c3d8a5c057ad3cd2b8259d5996ec95d33e05098ad72ac9746e7b29fdfaf5bfb0ee1b3e6dc69e863186b9a6aca49694f1bb7a828863b1193125d1c3824ee19c8a13967383eeb562e012ac56748fe82b9e7d5984195a19c7a0adaa9ad431e2836931eca8b9f53f15c0a61854485022154db48de7da1cf0166daa4f74147401ba845445506482037ef71a0dba98cb7f6a44bb9b65b511f7d2a193ba02d311a487d6dbedb5ec3943a18e5554869e8a4572409c098715d810d9477eb9dee86b375881c955a187c74f1fb5da60751876a064684072ecc56ad43ee3f5aa501375f8edeca68a235c3f9de288cc31113b7ff16fe2e28986402eef8f76be199f0f08779df05b82b2214f6f265541dc8fde2e82dba5d2c73207ed04edab486f68813d4a19692436bb12dedc3e347105ea8061e55508f2f6a9c2623d2d54c3f994fc4b64c3aeceb6157b2f9198152a513d989c0a7190ef79fad54c37d40b6ec390da1d17c2bff0f84da7a69d1f31ddacf8d557ab33cf0018d3f86b374113da1c09bb7ae68e10c988246fdab9ecdf93f467d346546d42120ff33efe0a5c59043ea23fb43a2beedb1b3ca8d84d7b0e6a100738e84b6c46dcbec72a42245a7073f18c1b18ff2904caa78396284e11eaddd8df015fdfa7ed73438ca5be768de28daad3eee8cbfe0e3d1390b4d505bcf8ae853ecbb1ebdd6b6292cd5bb552f11ddf6614f1a6af514186244debbd45b284539968d7b23a99d84ae0b0f0b72c40c5bc70a148f5478c4e72d4ac5d5d3974ab9b9fad1f90a51e32c4087434f27c2c099570e307f5f5c1dfcda1efd7f7f0d4c1913ba7a9be497c42fd9a29044fd4efdce9a6a652989bbf52da39f0ff9105b6867b8b8c0cbf8c8e9ee24ef78878f9e59af98726fbd885aca9c9accc1767486aba77df0d8dc63ca1fe60ca83bd4b39867a7a5216e5830fd1d4772704ffecb9b3bae78b3748ba0686d344c777270a88365e5345561c11901cddcf352ac7194cde4c80147a83a6fc7a163a8f8d2f1923ff1b665dd4e98904dec27d390254bf4b519beb88077effefa3ca625afc2868d75d52ffa51bbcdd127d6bde8fb6972b0cf009d50946d25f30b5db41c4a32ea04659848756c6713cd64694a11763cfb501f19c7161f92f3469e49e4ccc83c9ecfaaf2a3f939f6ee13bf2058a87d84c12191e77ee851524ede0f639f3f70bb88b0f5dc307ec1de266cbab9d9c92d97a077ed68c0dcc0d89ca10eb318f28de3a139fc85ae95f32723a63b32f30b472cc934ef5f3dddb0aadaa7691309ff0c6554f438b5887f3de659842e12a7478ff0f799736b3930415fc7c0cfe85e38131ea751c07b713b0fb5e5fbbb81ff7bd601426089d8fe3c4ae6402b7fce066d4585a66409be450b3cb1b06094e3b6fd81f8188f0c0e32985c5341fc2176c484f06d83e5347a116793fa7963790a303f65636b99097a438786809515b0fd1ae6ad466a3a2951b8151c67d813d08a13b18ac36d77dc9d4a2a1e0215898acdc5e606edb0f235642736339aa6237cf410f856dd5dc415f878e155254ecfbe6b003f4c761a3154fbd6d437220dceb151bbb5403fa74c8596d249b13656bc0536c23a4b5fabeee54fd8912f21ba4e29ad95d573edda6fd51c3e466a3c6afc6ca2d4f6a153c7854e7df6f0e62844051676ce4ce2610545d8bfc985828d14a844d73cff0ffe96a4e8af288cbe5229c0be2a97e252c9a8f1227665d14b1636b4f45ca5d2442" + } + ] +} diff --git a/vectors/cose-key-pq.json b/vectors/cose-key-pq.json new file mode 100644 index 0000000..55dba47 --- /dev/null +++ b/vectors/cose-key-pq.json @@ -0,0 +1,55 @@ +{ + "schema": "reallyme.cose.conformance.cose_key_pq.v1", + "suite": "cose-key-pq", + "note": "ReallyMe AKP COSE_Key and Multikey fixtures for ML-DSA and the pre-IANA ReallyMe ML-KEM COSE profile.", + "cases": [ + { + "id": "cose-key-ml-dsa-44-public", + "algorithm": "ML-DSA-44", + "private_key_seed_hex": "449137f736f5f5a35eb9f37c1c88c2a0bcf18e757ffb9285ab2c4c26c15c55f1", + "public_key_hex": "811dbbc20f9276252bff02f3dfc4b251f7af745bd086a766d20fc70057ef20da13b36d64d3a18cd9192e8d4f6bdeafe9c6ab5a2b0986ddd5702dc8a24d55f214d69879ef2397a47c1eb476ed2ba80d2b1b7fd6ae35715e2c304b9a276f322bf4335f82f5fa10772fa7e9c38cfa6626f5359b354939aff4d3ca14b45d58df3bef62424115ad7da67fd6c05548e4171293c4b910dafff66dd6c736704ca637d617891bc5d50a01c02544a5465268c2f23ca2e37eece192b8873cd360557c6d8b76299cd89bc9dd2e7bac1ac135fb854aff03e17303d8327b1040c3482296e85e7ac5b10222c2ffee4fca78bc39f9f452d3d3f705178eb3b90f61d6dbbe17649e1866ff8e19d58c31b3e09bb6dab331a1a3dcc56c4e74ce920310d6674fee45d6a0696543efaadac43c6f7be34483fd233a4a03d9f6d715fc3b977a582583c096b30754419e8a873669990d0a22cd70c244e928e0f9f2fa6a1a7fea339ef70603da5f14a51b1ae9353f547127494571dc5eae695b331727d7e7887ef8d832a2b75035575910b16acb1eb7f131eb5970c489a27cb8766461527ecda3086492b942d8043c02af229ee6b7652dca93b1275749cbf029db04d27d588d2bad0f5a03be3265c22bdb4b751d2e03683f9e59bed8d5f3c614b29128ddb12599b51e269cf031c7569119304e4ba659266e1a7b650d8a4a08d671d664db4c233b7b3eb59460de0c4d02476cd1e5e8a95afb85b535a091ff370e2116790dfc65b07b37da893024f81c65f058ed0243d8f6586154097c0e09042c3e53317917fb158ef8251740863ac27cb0d72a398c64dfb435d7ff08179e9498748b1d4370be59268a9a55054cc1adfcacde0525c40afb31bead4dc913f75e2f72d034a5b97e3a0b8fa2045bbcb08c9ceae32c5249a2341aeb3ce1c2685ebe95e0edae2595431ca18df145715e12757bf450ce181a0d369045b212157c4db1e0c15be0173ee92feb049660e441dec621c63cbf94184164e4cfc2e5c35c97bfbb66109074de2d041adbab11e4560177f3c5628a5111f403d2a21144230d2a58bcf0d9c8b7d7af675947161e0b8b4299daa1705e72b6ffeff41771b43fd0bb437f97685e6d7feb5cad282726d8b3ad134190c0a5807b596d97334f8ac41db21b0b6e8f1466b9aa3b0436175770facf488fc85a38cd06e03313ffb97f5a53efeb21ebdd2bf3227ace88cd25cc91f443138fca6fe9e72c27ae7df304e82dd6f99efabff03622e3d1a5ac7b1555acd51f0d50f6f4bd2a164051b2a28ce6180aa3787fa3adbaefee6221740c4ebee123e1a5d4efac67d32193eb265252188e94443eeb4e53b98c1990667b806d79c232610f624ea9c3bb695e88d4a90b77c467011bfc1c7613f2ab3648007c90ba51eeb37f6207551212f55b58c19eefd6a5857c3565e9dd5bac0ab46b2af63ccd2a920cad3c9bc9976983e1bf74fd9f8e41f612b13b12f11bf248b2807305b32ef474e7c55157390070f394851f1f495996300a229a237da21640c8afb3fad253186a8e48e507e9a0dce84060a3c4a6106565508c7fb773419557d4fe326af8223779e0ed97038d64b1b16dfd29da0d9a7ac949aead491db6be95a304b72ba82863b04f8be515df607d360b7326232ef7243b83358535bd06c5335bb123fb5ba6b33f329df0dd3c589834455b84f8bd93a1de18850a9335a23956c82132ea1487a3a7c670960351c929a2ea30d7944c9e1978d3b3ff81cf992fb5186bf6afa02bb22275429424f22baf8f9332061216eca61289defc920870fd98c39e39ed4bd807c3a155b6d4163b23ec22a7e277e8f4c176bd2ac97a8fd793e52faf7fe793262312", + "cose_key_hex": "a3010703382f20590520811dbbc20f9276252bff02f3dfc4b251f7af745bd086a766d20fc70057ef20da13b36d64d3a18cd9192e8d4f6bdeafe9c6ab5a2b0986ddd5702dc8a24d55f214d69879ef2397a47c1eb476ed2ba80d2b1b7fd6ae35715e2c304b9a276f322bf4335f82f5fa10772fa7e9c38cfa6626f5359b354939aff4d3ca14b45d58df3bef62424115ad7da67fd6c05548e4171293c4b910dafff66dd6c736704ca637d617891bc5d50a01c02544a5465268c2f23ca2e37eece192b8873cd360557c6d8b76299cd89bc9dd2e7bac1ac135fb854aff03e17303d8327b1040c3482296e85e7ac5b10222c2ffee4fca78bc39f9f452d3d3f705178eb3b90f61d6dbbe17649e1866ff8e19d58c31b3e09bb6dab331a1a3dcc56c4e74ce920310d6674fee45d6a0696543efaadac43c6f7be34483fd233a4a03d9f6d715fc3b977a582583c096b30754419e8a873669990d0a22cd70c244e928e0f9f2fa6a1a7fea339ef70603da5f14a51b1ae9353f547127494571dc5eae695b331727d7e7887ef8d832a2b75035575910b16acb1eb7f131eb5970c489a27cb8766461527ecda3086492b942d8043c02af229ee6b7652dca93b1275749cbf029db04d27d588d2bad0f5a03be3265c22bdb4b751d2e03683f9e59bed8d5f3c614b29128ddb12599b51e269cf031c7569119304e4ba659266e1a7b650d8a4a08d671d664db4c233b7b3eb59460de0c4d02476cd1e5e8a95afb85b535a091ff370e2116790dfc65b07b37da893024f81c65f058ed0243d8f6586154097c0e09042c3e53317917fb158ef8251740863ac27cb0d72a398c64dfb435d7ff08179e9498748b1d4370be59268a9a55054cc1adfcacde0525c40afb31bead4dc913f75e2f72d034a5b97e3a0b8fa2045bbcb08c9ceae32c5249a2341aeb3ce1c2685ebe95e0edae2595431ca18df145715e12757bf450ce181a0d369045b212157c4db1e0c15be0173ee92feb049660e441dec621c63cbf94184164e4cfc2e5c35c97bfbb66109074de2d041adbab11e4560177f3c5628a5111f403d2a21144230d2a58bcf0d9c8b7d7af675947161e0b8b4299daa1705e72b6ffeff41771b43fd0bb437f97685e6d7feb5cad282726d8b3ad134190c0a5807b596d97334f8ac41db21b0b6e8f1466b9aa3b0436175770facf488fc85a38cd06e03313ffb97f5a53efeb21ebdd2bf3227ace88cd25cc91f443138fca6fe9e72c27ae7df304e82dd6f99efabff03622e3d1a5ac7b1555acd51f0d50f6f4bd2a164051b2a28ce6180aa3787fa3adbaefee6221740c4ebee123e1a5d4efac67d32193eb265252188e94443eeb4e53b98c1990667b806d79c232610f624ea9c3bb695e88d4a90b77c467011bfc1c7613f2ab3648007c90ba51eeb37f6207551212f55b58c19eefd6a5857c3565e9dd5bac0ab46b2af63ccd2a920cad3c9bc9976983e1bf74fd9f8e41f612b13b12f11bf248b2807305b32ef474e7c55157390070f394851f1f495996300a229a237da21640c8afb3fad253186a8e48e507e9a0dce84060a3c4a6106565508c7fb773419557d4fe326af8223779e0ed97038d64b1b16dfd29da0d9a7ac949aead491db6be95a304b72ba82863b04f8be515df607d360b7326232ef7243b83358535bd06c5335bb123fb5ba6b33f329df0dd3c589834455b84f8bd93a1de18850a9335a23956c82132ea1487a3a7c670960351c929a2ea30d7944c9e1978d3b3ff81cf992fb5186bf6afa02bb22275429424f22baf8f9332061216eca61289defc920870fd98c39e39ed4bd807c3a155b6d4163b23ec22a7e277e8f4c176bd2ac97a8fd793e52faf7fe793262312", + "multikey": "z4sdjmBrFbj5sJMkgm1qpM9pQB8W1qjZ8HxbTNMd7pWhnBvHsUNgEuQQPmdETMQrvxwbeEBgz94Kxs7ky8WyQGms7QRVqyoxEBEgT3VvSBUUy1uQ922X5SqDyjhyhD6Vpn5d12d88sgP3okvyxe8dVQipwVaMzRctjV9htsU3ezTK9SN1u4zDBHZNpF9s8dEn5jRnv71Krv9jwA53M5JeNtz7arrScdWfvkmN7bdeja9eJykJtP5VseiLE3i9VGYQ19NYDT8sFaNk2qe8PhiiwLJUNug5TMmSTo2ZyCoTGgiZpdJDVDL74STAXdHMJo7a5f8TxXLJdYd2RFU1byyEy5gHLvgZwm3SdkmuDDq1f8Az8wxxSGRKWJX5jfUuoG1XHdSuRb9rj3b9t54xQcboU2YPrQeccbahxANxG79Qre64PV9mEAX1JmF8n26cLkqqwjgVBMcascSaPs7Syx4tTv9fa5FGnFXcjDUfGVmW7KRV2zryAvAoMDCVGxzw5B9pa4rWgKbAdKfmsm6bfkqfGaUSy8LTvLY4SJbjdEbgki56nDPJRHJF393KCr6j4tUwrz2NXi1WWVEJPFVW1ZDvBvmizycfTbgYWhQaNVCm3HBi83k9DJ6zdp4RAf7Y6zo3S8kCrapYX9eufuDLeSA1EzyCz7HDPMJtgQkcHHkddr9vS1C1phewQSMFwi2wtbX2CG9X7uAengS1ubvrBKiH52z9fr9F5ifrPSPZbFuJbH4ufDHAydDnnnt7bPTcCvzNjZgMhRkTTd5rGZRieAWxvzuFKxfhtDKFFbd2FpkJSNRHZTWpbE6qpHGzNSeFQSKjtgTVA7juW6MGDnoJinpmMNB9vkPpx4KfcQFFVhZp2wmG1MVgFNRsFmG9vYQhFDGe7PFkrLFwikk87XLcTNBGYxnQ2yz24BwDyrawW1eGTyy6u6xwmLg5c4sU3Hi8yqkh6EFqJnmMtLQrLi4ZYZZPSw6GVVcw75iihNVVT8LsvqbcoPZm7kHe2rH9geFz14TaAE6Xm9MrtHvessQBsZFm3TbfNvmaAbjPe3NQdcywNY4TeFsKWjcRqErjoStY1HYGamTzEwmu12hwrToVTkpU15wRF2pxvJy3KPf6vhB3gPsVwM9owtgBpkabwKG1pE1H39RtBzq9po2eiroNWHWmhMHYr8MjqpZikt3i5y59WGe4d5HaKbDxpTk5Yh325gSJ4Y5X2rPvrQs1DLWJopGwHEPZLyZTCA8vpkEFbd5PWHTKqLqcrEWFJy1LKro5LF9qvYzVeYXchDwzsAvuKsYdbDBRam3wc9xwxZwR8WXk12ARgsRtqGyVk8pDw48gmizjzEB1VhFqq1EvtVdsVggNhH8kzpMt7CBgrFUt3xGFt1Mi3BMtXCvDkT5w4KP2VKrjKQ9vuvi9A9FJrte3behHGSAyiuLdB9xgqny5fCHNMWLL9Fjg1VUG46TaZCwFL69hBZm13zzwPB8LPzhzUnZCjVNnxT2DzXQiuxHSS2nzxw5SnSd4tWGrou6nJQLhAwUWznibcfYmL5cPajRJ6vPyZw7h88cyyPC4BUdRAwiaJLjUMPH4MRty4aFLNndBcx6unfgGCkQB81jUGoarSqg64ng1BKjfoiXfam699k61SwjtWtH64J4DK6Wm4dtEpqCX9SgezkYELmCcKkkaGfnTpf9RXG41LsykmUYGTpdyy8qpRkHniHwbGatW1QJYVjzJU2DyQLDU97wT9snGB9WWC8j2ECQi9TeAM1dqKVrU6nHPDGrTYiop1fniVmsDWihZNHX" + }, + { + "id": "cose-key-ml-dsa-65-public", + "algorithm": "ML-DSA-65", + "private_key_seed_hex": "659137f736f5f5a35eb9f37c1c88c2a0bcf18e757ffb9285ab2c4c26c15c55f1", + "public_key_hex": "573647e17da3d83851b4c2d83802290e2146818079edbb53c9f2a9b1ee45d0de63e599d5e10f04d2ce3983556827e4c042dcca44668f2230c22d1e1c8a1db6db950fb96d2ed08a0492bf892861245066d46444d199946e6def49002f22013fbbf0c6fe1d1842697f3c58f3ae61bb453506ed588b09b528a0df239b6c809ecdcb29b75307dd722a024d6496f4331d44b409bed5a955204b35a2a466d95536fa6e0f534060c2b3187a6abf343e66b36632cea29a88a42df0d30d575337284e7ea74da85234014cde06a36e6c634cb8882f0a3cb6a84ed1e37a6f1a7f0d1a84186d7e897574a95952517de17396eecc976afeeba26d598a061a5cea258896dc006cef9740700ad084a726ba3f251ab4f6717084ccc4b7a58296d1581140d6c0440acccb443030ecb99be83261c96f738785eb0b76365c437409b35a487b3e0b55e1a401a1e5a18710539ef84786ca6d9dcefb81394a68ebb8b225d8031f68c99b9bb86a52f69e43ddcb5f96ee6de991b352b5cecbc03b1c70bdc858bd753ace250680d8f313c7a72dfe513616d04ae3f77d14ba54df3f9baffce58d73063efd7d90face6cb070172cb8d69df4f0a8ab291f3c856be8cd00183ffebc69c0bd4fe6b2e5398a42077e63d87e0b24533f3f067563c43b4699fe1bd1f2565f76234166b0a181a4db33cbfb7c7f8ba75741a612dca335b7a49ee4b22ccdbdee1a0c7db648e42a73311aeeb65d41f1e027d0c595baf32b5fe7e12c0843171e0c63c3a44b51da199b73e29b76446ca6f1189656d721dd3dc38c0504a0bbea684d0148c26fb5e7325b0dd3c3e8fca14e0e26324340e5696f285bb2069465864b528bbb70952273a2004d53bf0662e7a8de9cac1e1392e7dd518aa7afd0eb190e6d6118c6bfc853b5752cb0194f051239547c7785bd9638c0053e64daea88074696cc15462f23df4d9da31b0404e3f60485c8fa6034b31ffc7dcf607fb71328086e6306d0c5549b91719b8be4b9efaf56f59dbb29375f295ac8d35ac48971bf03a07752560b435f7679005cf2de0b27db4b56499eb2878c4b72d0b870d94a973d7dc92c186fd7dcb440452f17608020ae7b5203f3b2e95b3faf4e7b38eb151b48bcbd5c7ece249f22e097eaf1a9bd899999b0ea888d1954475d7d5e4ef931a960eecaa6d067954e82e1aedb31cd73ac1bcf5ac8165ffdcc5e2c3877fb779dbf0253df9c4f4a3d9988038386b6127483494e82a357f4581381746f6da034dcb5d43877606b7e131f0d59f6c805a8a6818d9815e758a5f8c52a625efb58702ba61ddf8cc2dabd2cd67893b482d660acaac93b33e21fd05372d86ff0a46193f843381abfd16021984295910ffed35b7c5a7ee06a147bdee375d5346013f3a05586a48d5313cc64939e8bdec8d05e51fd9844d3527a3563f71627b54e4de34349929312b04f1bd57265867143995ce9251c1685058770ef52b739c9fc741b5d3f224bbed5fe48050a35f4dfc1c1c54b54ae7d575e4d8e2737c08ae553a76f96b891bcaa8fd6dc720340ae8a1e4181a761d568e64a2f5fdc4773e0ae5dba8e07c822403a051467360b47e37ec1d7db41b97130d6fe333a22cf45c8feb9a07a80295bb10a94634c38be2269eb268f3f39a54cda372065be20c9b689fe49e9327ecca392b208a46657b1798348f0f9e8760b77698121305e51f2b3f4b3855510085f023e583ca1fc9133b33f647c9155a77cab776f7340228da9a47193bd93ffe991a9a02dfa46ff5fe0d1181f62d949f9dce6c0099d32f1ec7af0766b5c3a4ba348e26a094469447c286ca2bcc509980e3122eb624d97cd8f4fd813717506c7e901324fb3a020b9dc757ba7f4c2deaeab325a0cd87ff46393648629c8b7fa656fc546577b5de85a92c0830974e11fcfb345c528cd0595153fd7fabf4bb1e15c50c6d22b0ef23b5738dc5f31f0dd6d42060ed94424c77077e592106b16d212f99f017da673b3cfbd16ba7c365a6571e9a9194e64ae8d3dabf382a9d5fc331862e8f1b906b140bb73c51d3845a9dfae54152026f2a5be3801517b8a99004992a5169bc84740a316d48056af9e8596cd7da24f70936d94a0c6b4b4ec17a0709b3ad9009c29ba385f478e9658655565030781b4495b37fba534870a9a0071fd9b236669b4cccb82bed68dad2971eeabadf2d1a8612ebeb8fc40a35bfe02e32d06d7d7012ac1fafabbf97af0399f6c2cdfd7601e61339ff326b8693dd06c0133ed94997575a69f21cfef5ac0e3b2c38162a31b166c520ce3b37140b21b7e1110bd8317f47b889824bd6ceec1c6113e57e66ca12d8d2776b8356eab1b4fed235b6d6e38d1d298f36be4562d58e37aabff3dc1d0137969694c314c5e823d2357e79d1e2a8b378019f490ae97b528e6e39623214a45055f7226d4ddc4d4ee14e49dd623bd0a810b8afc99f809b6e438974d01063a6f15cb6527cad5a9e816ad81648063ba8f06aa9fa8bbca62c95fcded9f0f7ab6ffe613a138361c3ba9848db334482b403d1693743a038b809d49136001a11fd02d72895014c6fa0e4bec3e74a0d5d6c53522a956c6086bfe5b983d76ff7f983fd3404c0b4c9c39be5f6b4c9998e55c6bb3e92dddeb2eeb8174ca75ba87b11bd3f1058c34b04078501ee5acf3dc1e2c2fde5bfed4b78362a40e67a77251f272d497e51b04beb06348d21bc200c99a9993c4a2c22c2a419369cb6734fab70690c5b334d422fdfa9dedaa94606c1eb82b653d20c5e6d59195390f", + "cose_key_hex": "a30107033830205907a0573647e17da3d83851b4c2d83802290e2146818079edbb53c9f2a9b1ee45d0de63e599d5e10f04d2ce3983556827e4c042dcca44668f2230c22d1e1c8a1db6db950fb96d2ed08a0492bf892861245066d46444d199946e6def49002f22013fbbf0c6fe1d1842697f3c58f3ae61bb453506ed588b09b528a0df239b6c809ecdcb29b75307dd722a024d6496f4331d44b409bed5a955204b35a2a466d95536fa6e0f534060c2b3187a6abf343e66b36632cea29a88a42df0d30d575337284e7ea74da85234014cde06a36e6c634cb8882f0a3cb6a84ed1e37a6f1a7f0d1a84186d7e897574a95952517de17396eecc976afeeba26d598a061a5cea258896dc006cef9740700ad084a726ba3f251ab4f6717084ccc4b7a58296d1581140d6c0440acccb443030ecb99be83261c96f738785eb0b76365c437409b35a487b3e0b55e1a401a1e5a18710539ef84786ca6d9dcefb81394a68ebb8b225d8031f68c99b9bb86a52f69e43ddcb5f96ee6de991b352b5cecbc03b1c70bdc858bd753ace250680d8f313c7a72dfe513616d04ae3f77d14ba54df3f9baffce58d73063efd7d90face6cb070172cb8d69df4f0a8ab291f3c856be8cd00183ffebc69c0bd4fe6b2e5398a42077e63d87e0b24533f3f067563c43b4699fe1bd1f2565f76234166b0a181a4db33cbfb7c7f8ba75741a612dca335b7a49ee4b22ccdbdee1a0c7db648e42a73311aeeb65d41f1e027d0c595baf32b5fe7e12c0843171e0c63c3a44b51da199b73e29b76446ca6f1189656d721dd3dc38c0504a0bbea684d0148c26fb5e7325b0dd3c3e8fca14e0e26324340e5696f285bb2069465864b528bbb70952273a2004d53bf0662e7a8de9cac1e1392e7dd518aa7afd0eb190e6d6118c6bfc853b5752cb0194f051239547c7785bd9638c0053e64daea88074696cc15462f23df4d9da31b0404e3f60485c8fa6034b31ffc7dcf607fb71328086e6306d0c5549b91719b8be4b9efaf56f59dbb29375f295ac8d35ac48971bf03a07752560b435f7679005cf2de0b27db4b56499eb2878c4b72d0b870d94a973d7dc92c186fd7dcb440452f17608020ae7b5203f3b2e95b3faf4e7b38eb151b48bcbd5c7ece249f22e097eaf1a9bd899999b0ea888d1954475d7d5e4ef931a960eecaa6d067954e82e1aedb31cd73ac1bcf5ac8165ffdcc5e2c3877fb779dbf0253df9c4f4a3d9988038386b6127483494e82a357f4581381746f6da034dcb5d43877606b7e131f0d59f6c805a8a6818d9815e758a5f8c52a625efb58702ba61ddf8cc2dabd2cd67893b482d660acaac93b33e21fd05372d86ff0a46193f843381abfd16021984295910ffed35b7c5a7ee06a147bdee375d5346013f3a05586a48d5313cc64939e8bdec8d05e51fd9844d3527a3563f71627b54e4de34349929312b04f1bd57265867143995ce9251c1685058770ef52b739c9fc741b5d3f224bbed5fe48050a35f4dfc1c1c54b54ae7d575e4d8e2737c08ae553a76f96b891bcaa8fd6dc720340ae8a1e4181a761d568e64a2f5fdc4773e0ae5dba8e07c822403a051467360b47e37ec1d7db41b97130d6fe333a22cf45c8feb9a07a80295bb10a94634c38be2269eb268f3f39a54cda372065be20c9b689fe49e9327ecca392b208a46657b1798348f0f9e8760b77698121305e51f2b3f4b3855510085f023e583ca1fc9133b33f647c9155a77cab776f7340228da9a47193bd93ffe991a9a02dfa46ff5fe0d1181f62d949f9dce6c0099d32f1ec7af0766b5c3a4ba348e26a094469447c286ca2bcc509980e3122eb624d97cd8f4fd813717506c7e901324fb3a020b9dc757ba7f4c2deaeab325a0cd87ff46393648629c8b7fa656fc546577b5de85a92c0830974e11fcfb345c528cd0595153fd7fabf4bb1e15c50c6d22b0ef23b5738dc5f31f0dd6d42060ed94424c77077e592106b16d212f99f017da673b3cfbd16ba7c365a6571e9a9194e64ae8d3dabf382a9d5fc331862e8f1b906b140bb73c51d3845a9dfae54152026f2a5be3801517b8a99004992a5169bc84740a316d48056af9e8596cd7da24f70936d94a0c6b4b4ec17a0709b3ad9009c29ba385f478e9658655565030781b4495b37fba534870a9a0071fd9b236669b4cccb82bed68dad2971eeabadf2d1a8612ebeb8fc40a35bfe02e32d06d7d7012ac1fafabbf97af0399f6c2cdfd7601e61339ff326b8693dd06c0133ed94997575a69f21cfef5ac0e3b2c38162a31b166c520ce3b37140b21b7e1110bd8317f47b889824bd6ceec1c6113e57e66ca12d8d2776b8356eab1b4fed235b6d6e38d1d298f36be4562d58e37aabff3dc1d0137969694c314c5e823d2357e79d1e2a8b378019f490ae97b528e6e39623214a45055f7226d4ddc4d4ee14e49dd623bd0a810b8afc99f809b6e438974d01063a6f15cb6527cad5a9e816ad81648063ba8f06aa9fa8bbca62c95fcded9f0f7ab6ffe613a138361c3ba9848db334482b403d1693743a038b809d49136001a11fd02d72895014c6fa0e4bec3e74a0d5d6c53522a956c6086bfe5b983d76ff7f983fd3404c0b4c9c39be5f6b4c9998e55c6bb3e92dddeb2eeb8174ca75ba87b11bd3f1058c34b04078501ee5acf3dc1e2c2fde5bfed4b78362a40e67a77251f272d497e51b04beb06348d21bc200c99a9993c4a2c22c2a419369cb6734fab70690c5b334d422fdfa9dedaa94606c1eb82b653d20c5e6d59195390f", + "multikey": "z5FbaeAdaWA8swad7mddAXrpMhyihQMzarmCqHdutQBV5qBRFMJsHRk4GxsPbZuLZQ3HbLLcGCRTRCXq9hgv5uVPgo1hGy8WJWaqu9vjpbDvxzioPmJfMcrZnq5wQV5sGcrF1L9mFuarSAUQgXmWzerJ4Q4M7o1jtFZFZWRj4LFD4ZMkzq4UJFpWPEY16BLVTE338DgDWM2mdBCtbpivH3az7SFUY6whnUgzuDEM6QURa31VftichFEmiBjtsj2ywTDk2XYTsXasKqCMbpv9hy1WjvPnwe1SFCjPjahzJt6866U7BsjisJkUYvhNM5Te5taX3tNZH8YuCnkAKQCej5BGEDXPMnw9GStE3meRNcgZTRAs1L4tGbJn4xkQr543HYqgyPGfpMXhZRVaZ1SAmReiiJD4A5qcRY6ug2riZGTiDA4DWEYEEssKGRtiwdzv9QVo4ozfyJ9HfQhRbmXz8oJWh9rNisv8fpC44ZcJTA4AqEXg9YjcE6fRSJYDhqhoTMBTH52HqB8kpEyA22QEm4agUgM2No4CpVr13w6stiLPhoSDdQvPF5RJQVRLS7wsb3zSBwky2tmX8KcbNdQjPdViDYmucqkbi29mCVbb2qxTxjhNmMp4X5U9KSiUFM393as7nPweFuuqBoaRQFTnnHS1aQyzyPpvSyzjW9caj3eqZUBFgUyfakmqHsb283oW9keyC1ctZbgQcZMjemXhyaUo9jqX9KcNxey95CWNRJN5CueUiJBcBKnJescvivqkBXUeSnPi4HZH9Rc5EvzURnBFqBN7jW3DJCpsnZKG6zF81eo73k56CX9CdjHREDLD2hU88134S4kS9xgNYckAMpFahfFXLsP1WNhuxwfcd4hmhDt3tv15BqZCRddGqXHeeqvdod6aQiXDjk8uB81VttWmYMXiMtBp8krqmxHEnZocxge8x26c8dRevSCmYnMR7sRtiwgmQQ98zLase54Fa6A1ok1TZZM3GAaaQjmyhJP9bobmCofcdUzxo5ijMGCFZyFrjbjfhfhMi3z2dnVEpCDL91y7mFDzj2z26GWWZtGWpuenec1t33JJVUGp3uVNeTHs6XD7Rp7kmWGaGuFvn8s3qtaAkoTwphzrB6rR1XfEFzPAPeG4oRA8iy4Ziw8QovfXMbqX4xbNNV58DTgxGWKyG6TSLDvTKuk2iEkdkgoMwxGqXV4i7PjRAkY2Dk2SZjm79HNg4Z1rgaS21CqLWL4YbMVqY8RQ8em5P5sHAgLh3g4zL8cSRDp5WT4wAqkwVU76mETVhV3GmUAZds4D3Dr3BeJC4Rst5uuD6UVg58MdNhhiWNPQc8jU8danLFHnpHsjJ6GfxSNRb7RVc9DLb4W6jhoMtSZKMWWoS56pJHATrzfpdCyR3ZGMVsny77QQxWm1FUiBUsWAt9d5oDLpY6A9a36BQArv9CcD6FXBn49WAeqbhEN5WhaVEwzAkZh5LQBfzG296rg7hNTGtLeauSP7GsyJZ7Cn1gca9SKMjTKBdRKcrL1MCnBMkfspbK8wqSCLUFhDQQhXavaJNP5bGDdk1XK6vetEPXZHVNvqPdsC537ySXbURNQouzY7dgiUTjYomRktrx1fu6rTcuJEYbB9AGRCgjc2jQVZbBkabooPv5RQVPBM4ayaM2PX4wXnoLC5vVmdnU9n2vzwNBbLvBMq9hwv9Py1CzoUfQaE7Gq5yq1FizepghvDx5YnG6EttPbKrTgZfVtECE1zPAGHxFawccyaaduvjyeaPtBKMbCn913yKgJrTtvtpazH2cfdujCivbxSJ22ESU19g7P5ujjH2YemCxSmCGdsXazRa962TC8BrBcuChF46m2ceQgoYJZZ7SwWezJri95KFdrxUvgDSiaJoCLksKb64Rjq288SDNnKe1WEjJZP1W9Cg8f2xbkzQFsco7ZR7uHpgNwa4J3DwYQBM5euQiJaML61ufAsVKDokw3t9iD8PD5ASZjPz9Y35ggB6QjnrC1g4Tx8aSPXcgG7NFKYuzemsQLka9vQJYAzadRw8S6W3UmRxqMBD5ihJ3TwcmVkDuzzXL4sqDEu1w5TwToFWsyoQPesvPkDjRpSS14JVhUnbxxY8NqjZEiL2maHDq2wFCjpiA17fJrNehdcqmNSN918T6VBM5jaFQuyqDSkkmA5R35XGchti2NtM6DHm2cYd6BFDS3HP3cWAtpgFucVn6WRqxkLHuTYxMxQZXECSV2xNdASoSB75mLj6t4Vr5e1tBZ281r6ejvseMKSJ9VwUWENXCZVPBLCbpbMSCn1dARCBrHufYjMX2yd973eJGn9boQj2pFFj3uugKmqCp6uEsBbyqvyA2KYFEjnLGfyE2kmMjHKwYv4V9GZ4NfbFaD6kpHNRr5pXJgwM8dyHg3qsYKvQ3s7caeqXmTaDhZnW9FMpSGRu9EfZCa5kif6DpQL93qtHHduoZvBS9aUA156C27DQqK4J9ttyjn2K6FnCxJCpEAYEXb1hPxc3fzWX8Ytc5JLWa1gMrMwPwtjPzNZasjmEvjDxHakLDVpu5ttFdGxvMqai6nQzSRcg9PeGoB4ocfmoNXvuN9JPMSi2BCE7dQjJ1geZbHU71Pi75A6bXHDTdNzKezme135RbGUeEqy9RTNmkV4eGBBbdKJyyEk6aW7VBbLrpSx5t" + }, + { + "id": "cose-key-ml-dsa-87-public", + "algorithm": "ML-DSA-87", + "private_key_seed_hex": "1e9137f736f5f5a35eb9f37c1c88c2a0bcf18e757ffb9285ab2c4c26c15c55f1", + "public_key_hex": "90e3dd80d4bc34741c85c2c1b534b430b1540913639e15b6293a8d7982c5b966c8dd860cbef3ff19f4df540074a48be558de77b871d95634176ca9a684b8097e8af2c9fda44fb4cde6574b9f17ce2214bae45f61fda0791700290ad3f6af2d83a57ee66b3c5f06569e54f57c1a2fa2ea03122864ceed421ad3a8914c0e478e7077249f916bfc77e9613fcbd43ca3cc6170c0962c3f13e381798b559301651deaac1f043944f3d1e112b490cf14ca259ef21e43fbcbdc91a0cb7ace1447c4ee10014650bbfd8d11cb180fc46c1bb6472023282cd7a3dfd972c33551516e148d1f73bdb8719c98c342e6c661b38fa01f840cef0e758962aed805915afc48e1612663f0827e5ce9f20e792e2734a5afd522a0503c49052b43fe34e5168e10b9b52f06248727cf7a7a07ec7a96c7e2bcffa2cd3b48273032b39b1d2a6fe17ff90244996880ae85548f317342e722aae439a1f1ef6c0de6e235f1439b88d7779eebe1308acfd5d901f18bbabd2e57c639948f9ed45bdd823cb31c6dfc63ed8b74fa17a03ea3e10fab46a240a15792744562df6f3d5395d56ef4459a03a57cd9d6d87fd1e3d435338126163e0bd8d3ed5e0cec214e83f8fe3e3141724c52311b687bd2278922bd0ec0b4f3a92c99b5be8bd05bd6bd0fd103930e4493aa4804d941c80eaa92bc7468d188c615cfe4dd3c528557b965200636c4e217b9fab0d70732f7f914913d3b315067c1bd0de886beb0c203dbe5873ec5f728f10ae3ad925ad54a053a988e0154534cecce31a48837f64f4f5a75c30b718e615fbeebe4b8594301ef4cd28dd3ecaa18864c7e890d51f40baab833ad54e79d4ce6cfcac49c0037543d115b0e446ada24a99054a748d8353619e3c0f7a201b28fa9ae700123a2feaed1b2e6bf379936ac0db5c4861082e4cd25cbdac69b696ee2a6f1a73b2fb08ff6b46aa0fd357158a9df6522b47d5ae20fb013bcd731edf9bd55c1cfc7d5f85d9d3f853667625ed155514f696d2e1e4fdf60ab13b29b7f443fc254b564bdda7ed5b5c49531c95c64f70ac86f0b16dc461844dc5ffe0ea4dde64740d118410f1674b731e654c07f73e13234b9fc15c6b92a5c8db54569fe75426d3d82f3759d07a16288528d43503f24dfe1709a32c5517e3819e517478a907a56687d4122f7c87921acc6b952f3d19a1637da6175e70cbe2313a43454b36dfca8e89f857f4f4645a2a516dc2de180d1ec8c4f9ea3b7df07ef832297cf09e7f2eeb33dfd506cef44dfb6eec2f83f20e271a31b7c681e73626de08ecf5b21d51a51ad0a24e3a204f0e9053c328320eca503760047f6d1b86400b774877507586608efc68037204b42e8048320312f791122a9f88f3e0bcf852f6e1f53900c3777cffd3ec4c7d56ed4cfc0d4c72658fc0ab589bce0b3c6fdbf7556145dc063544e3424587943744745777739581d4cc55f76c393bf51b5debb2703cab56842b17549eb0972bb5c866d5c107f91c7a8ae1131860749575830ae3beb265813a52c9f8b6530db055c80035f4a41dc9bed93581c0e5bc5f029e6d5a7e8d50220bb395a65bf6882b5fabacb7e9b4a70fb31630366bd051ac89ce85bbd8297f618cd4a5db3b2b10f8f7155dbfc1625ec0b1a0778c83bdfb8baa7e993164b12eca9c3dc4d11bf72a71ce11e00de3940a30db35cd131dafc99e018b22525c72c4823f45e4dc25b9cf420567134d0ece8ac1b9073067d2b74129ef51e8c9b59ca1cf29a6ed54094418bcdd3a5d142278121db74d7a2ee5ea960e789aeaa8a90a7515f5c7e94c096901fbfdc164b5e29979d3611ce74f06079ab973dbd6773b3fa5678a4c1ec51dba9a877aeae553b80288d50f154029c4b0d0e6364fef3b1b8684e82accc56988741a63c6addc8cdefac64db57af19fe4f7039012bfd090493bd91ba237f2225885460a2df41490e212f7b437d9dcb3cf06fac09ec57ea2a85ae3c585180dadc24ef414ac54b77818d09b206fc337cb4a687dba6ad58f5b2ff9a97829bc4048356157c9c003b67e1a291fcf5a5e6a52923162a41a72313d3e5d6ccb92f375d9debf4dc266550f0b226520ef661e3fb56097d4c734a58726b533f66e72b7181f74788a4011d2133922fc765da1f017e85bad781f76ebe96f96757337ef2b14e9f1165848f3b4bceec28516ab2492c316b7903f1995b43e4a31afe9c5079f5183ba80c2537e2fce36d48ee5913502e9988987ef5196ce116dc6de546998f1b9da2fe7cc457f3b75951ee927c0d148adbe275b8c9e56c8fcdd459f025ac60c31ea9aa52ac89e9f77037e1ff0b148a97ea0faa6f915844ca571d2c43b50d18b337b5fe27154151cbb7b539fb965d8d54f8b118c5ff0a09567652d35af324d916bf9f798a2e5801dd28a19a3fc3eda3d4598eef57ab09e393fdf7d501a279b12a16151cd8d54eeb1bb86126e383f5df0da7f9b7d8b51206c2b310c89c7ba8fb381da9d9ae5db5155d958b7286048941dadb31fcdbeb7944949350c93d92e3f088d086f882b99dda35a4ada47eea5cc9e1e28b2e63896f0e9e39ecaba7904473641f493369ae4923b992045143addd051260be166eb9401b60b1c9ecf8f42828baf9799d9dd9101484e62607702680817faa2451e08435671ef315973e32a0ac7221e4897e67eb72c37974370753261d623a824f36faeea57046e92612a2573bbcd0f61b46b8d7b80bb14b634f428250670af02e6ecad1130799bed98ba9ea4c9549d513a6374771ffc2ecaede110660b33b604c3dec66e4f916a24bfc29771e11042b7390bb40334b2ce3b4b07e21a9f783eb51fec680d00c2aafbdbcb2d1bb71cca9d639a64301251f91535ef4bc136d027396aab87ffc05f5399a94a8d5e55aafdac654483d6da3f1fcc68462fd1beb558982ff527e7605dac118af5e01804c2756d961df1ac7a30ba6da38a4145c1743fb2810b95b3fdcc2d9e18b63878e808fa91e190b6b5a42a23dd211021149dc6894e911ef75c1818407af1a596f968bddca66b93e0a05e9ef715b5275d39fa49157d77d431dadf3518325f51f6c820103b4981cc6aa2fbc7be266f79ca6f1d74bb89a6a3292a90f716199747732c4edd1e45942487b76d6bbf84956df3cf004414a01fcb3cd85753038d572acb01a24ae0df24995444c1be5f91d56d548e82414bc37a5f40f5b315b9b56945665a2043bee27ad2e56d9ea93755c0356a36790ade2a130be8392cc760f0a4629239c08bb57e089c6ae5bfde492d07823d58f68e52fbd2e88309578c8ceb381e1d189f2e17e2f6f0263951ff99f072b29ea48f729889efb0689a1d4d3b558f572fe88c968275eed00ca4b968cce124c74ffb885b5c6ad285f12192ab9e60da808fd009c7158818073e6791d489e96d707629e87d380a2f9b147ba9124086db3d2becc5a24346353cc1761c2b8bd4988b21495aca7d38d525b52a1e12b39383dfe96333667e947675887c5a6bfa60909343a711fdb3135d501e429265e74117b58036116d4489fb28c56f1d1dc4bb4778ab196b6fc3f7c207c865d5e33768e88d8fcd23bee0ac19330bc21795fe96fbdccfa83ec8650f8802a606d9e146bc672353748d674935df0e3ff620f6aa05d28600f0e97dd573e77c56e3b17c5d879d443f9d680813b5a476226a3390741", + "cose_key_hex": "a3010703383120590a2090e3dd80d4bc34741c85c2c1b534b430b1540913639e15b6293a8d7982c5b966c8dd860cbef3ff19f4df540074a48be558de77b871d95634176ca9a684b8097e8af2c9fda44fb4cde6574b9f17ce2214bae45f61fda0791700290ad3f6af2d83a57ee66b3c5f06569e54f57c1a2fa2ea03122864ceed421ad3a8914c0e478e7077249f916bfc77e9613fcbd43ca3cc6170c0962c3f13e381798b559301651deaac1f043944f3d1e112b490cf14ca259ef21e43fbcbdc91a0cb7ace1447c4ee10014650bbfd8d11cb180fc46c1bb6472023282cd7a3dfd972c33551516e148d1f73bdb8719c98c342e6c661b38fa01f840cef0e758962aed805915afc48e1612663f0827e5ce9f20e792e2734a5afd522a0503c49052b43fe34e5168e10b9b52f06248727cf7a7a07ec7a96c7e2bcffa2cd3b48273032b39b1d2a6fe17ff90244996880ae85548f317342e722aae439a1f1ef6c0de6e235f1439b88d7779eebe1308acfd5d901f18bbabd2e57c639948f9ed45bdd823cb31c6dfc63ed8b74fa17a03ea3e10fab46a240a15792744562df6f3d5395d56ef4459a03a57cd9d6d87fd1e3d435338126163e0bd8d3ed5e0cec214e83f8fe3e3141724c52311b687bd2278922bd0ec0b4f3a92c99b5be8bd05bd6bd0fd103930e4493aa4804d941c80eaa92bc7468d188c615cfe4dd3c528557b965200636c4e217b9fab0d70732f7f914913d3b315067c1bd0de886beb0c203dbe5873ec5f728f10ae3ad925ad54a053a988e0154534cecce31a48837f64f4f5a75c30b718e615fbeebe4b8594301ef4cd28dd3ecaa18864c7e890d51f40baab833ad54e79d4ce6cfcac49c0037543d115b0e446ada24a99054a748d8353619e3c0f7a201b28fa9ae700123a2feaed1b2e6bf379936ac0db5c4861082e4cd25cbdac69b696ee2a6f1a73b2fb08ff6b46aa0fd357158a9df6522b47d5ae20fb013bcd731edf9bd55c1cfc7d5f85d9d3f853667625ed155514f696d2e1e4fdf60ab13b29b7f443fc254b564bdda7ed5b5c49531c95c64f70ac86f0b16dc461844dc5ffe0ea4dde64740d118410f1674b731e654c07f73e13234b9fc15c6b92a5c8db54569fe75426d3d82f3759d07a16288528d43503f24dfe1709a32c5517e3819e517478a907a56687d4122f7c87921acc6b952f3d19a1637da6175e70cbe2313a43454b36dfca8e89f857f4f4645a2a516dc2de180d1ec8c4f9ea3b7df07ef832297cf09e7f2eeb33dfd506cef44dfb6eec2f83f20e271a31b7c681e73626de08ecf5b21d51a51ad0a24e3a204f0e9053c328320eca503760047f6d1b86400b774877507586608efc68037204b42e8048320312f791122a9f88f3e0bcf852f6e1f53900c3777cffd3ec4c7d56ed4cfc0d4c72658fc0ab589bce0b3c6fdbf7556145dc063544e3424587943744745777739581d4cc55f76c393bf51b5debb2703cab56842b17549eb0972bb5c866d5c107f91c7a8ae1131860749575830ae3beb265813a52c9f8b6530db055c80035f4a41dc9bed93581c0e5bc5f029e6d5a7e8d50220bb395a65bf6882b5fabacb7e9b4a70fb31630366bd051ac89ce85bbd8297f618cd4a5db3b2b10f8f7155dbfc1625ec0b1a0778c83bdfb8baa7e993164b12eca9c3dc4d11bf72a71ce11e00de3940a30db35cd131dafc99e018b22525c72c4823f45e4dc25b9cf420567134d0ece8ac1b9073067d2b74129ef51e8c9b59ca1cf29a6ed54094418bcdd3a5d142278121db74d7a2ee5ea960e789aeaa8a90a7515f5c7e94c096901fbfdc164b5e29979d3611ce74f06079ab973dbd6773b3fa5678a4c1ec51dba9a877aeae553b80288d50f154029c4b0d0e6364fef3b1b8684e82accc56988741a63c6addc8cdefac64db57af19fe4f7039012bfd090493bd91ba237f2225885460a2df41490e212f7b437d9dcb3cf06fac09ec57ea2a85ae3c585180dadc24ef414ac54b77818d09b206fc337cb4a687dba6ad58f5b2ff9a97829bc4048356157c9c003b67e1a291fcf5a5e6a52923162a41a72313d3e5d6ccb92f375d9debf4dc266550f0b226520ef661e3fb56097d4c734a58726b533f66e72b7181f74788a4011d2133922fc765da1f017e85bad781f76ebe96f96757337ef2b14e9f1165848f3b4bceec28516ab2492c316b7903f1995b43e4a31afe9c5079f5183ba80c2537e2fce36d48ee5913502e9988987ef5196ce116dc6de546998f1b9da2fe7cc457f3b75951ee927c0d148adbe275b8c9e56c8fcdd459f025ac60c31ea9aa52ac89e9f77037e1ff0b148a97ea0faa6f915844ca571d2c43b50d18b337b5fe27154151cbb7b539fb965d8d54f8b118c5ff0a09567652d35af324d916bf9f798a2e5801dd28a19a3fc3eda3d4598eef57ab09e393fdf7d501a279b12a16151cd8d54eeb1bb86126e383f5df0da7f9b7d8b51206c2b310c89c7ba8fb381da9d9ae5db5155d958b7286048941dadb31fcdbeb7944949350c93d92e3f088d086f882b99dda35a4ada47eea5cc9e1e28b2e63896f0e9e39ecaba7904473641f493369ae4923b992045143addd051260be166eb9401b60b1c9ecf8f42828baf9799d9dd9101484e62607702680817faa2451e08435671ef315973e32a0ac7221e4897e67eb72c37974370753261d623a824f36faeea57046e92612a2573bbcd0f61b46b8d7b80bb14b634f428250670af02e6ecad1130799bed98ba9ea4c9549d513a6374771ffc2ecaede110660b33b604c3dec66e4f916a24bfc29771e11042b7390bb40334b2ce3b4b07e21a9f783eb51fec680d00c2aafbdbcb2d1bb71cca9d639a64301251f91535ef4bc136d027396aab87ffc05f5399a94a8d5e55aafdac654483d6da3f1fcc68462fd1beb558982ff527e7605dac118af5e01804c2756d961df1ac7a30ba6da38a4145c1743fb2810b95b3fdcc2d9e18b63878e808fa91e190b6b5a42a23dd211021149dc6894e911ef75c1818407af1a596f968bddca66b93e0a05e9ef715b5275d39fa49157d77d431dadf3518325f51f6c820103b4981cc6aa2fbc7be266f79ca6f1d74bb89a6a3292a90f716199747732c4edd1e45942487b76d6bbf84956df3cf004414a01fcb3cd85753038d572acb01a24ae0df24995444c1be5f91d56d548e82414bc37a5f40f5b315b9b56945665a2043bee27ad2e56d9ea93755c0356a36790ade2a130be8392cc760f0a4629239c08bb57e089c6ae5bfde492d07823d58f68e52fbd2e88309578c8ceb381e1d189f2e17e2f6f0263951ff99f072b29ea48f729889efb0689a1d4d3b558f572fe88c968275eed00ca4b968cce124c74ffb885b5c6ad285f12192ab9e60da808fd009c7158818073e6791d489e96d707629e87d380a2f9b147ba9124086db3d2becc5a24346353cc1761c2b8bd4988b21495aca7d38d525b52a1e12b39383dfe96333667e947675887c5a6bfa60909343a711fdb3135d501e429265e74117b58036116d4489fb28c56f1d1dc4bb4778ab196b6fc3f7c207c865d5e33768e88d8fcd23bee0ac19330bc21795fe96fbdccfa83ec8650f8802a606d9e146bc672353748d674935df0e3ff620f6aa05d28600f0e97dd573e77c56e3b17c5d879d443f9d680813b5a476226a3390741", + "multikey": "z5fhPUYHksYjj6nesBsReysHz3jfovRzbsTtUKVcqgv7xcV6LZNW7ovCm3o7Xoj1wMSnkKGHAbkvTRke7Rzz64mwz2YHHBaEsAHhjQfFb96CxBaZ7UQm8nMcaYztin5K3t4NL3WG8utkWAFoNugkBkoC91UudDiehVgFvbSEeHABjDmoN6ZfT6PMVbGV7m2p5spdG1dn97SgF6fhKB7YHxp9C6vKT8N57S6oh4a74NCgUpUMkKnsFd2roo1EUVt5pJFkZVA9KspqbMESZd1yYYMSNsBwGVjJwE1zwgGbhm5Y7XsLkjjt5hUU5V2LK8zzq36cx3q6fFDydG1DKFsQoeJS3H33vSDuvijr8PfZXeeN7SsTVNkBPSuQw63MyrhAH79fvezjhpGZ9regm7i1U2scXps4sv6QpHYacTP88KUKYMngKafqNRA5gXBHPd8NJ2YQH1Vc2tPnAduZDzop7eZnUAazqADDAjCThisEEfdyn6c4ZDzu8bbQMiUnFtAyfq8y3B7XWtZZ7HB8dYrLA55scbMkK2s2oTNa8NdiVFJWe9HUemLDn728F5sBUg8AYb8EqvMTTDwczUnTDyZqrfRzBtfDQP7dfhp79Fx2CoLksxxe85sZMu9eATZR1CGRtdvrZ6V5fDWFQxG51DpMgKtcpZsuMEv3fxVtSszCeujkAKmJaDT1i8vhjkcHuCkBHYM9K9hHbTXofUtsnTowmCswcaEjVkrZmhAvFPPH8819m9ZUsMmgd8jSkebxT6xUxuLo6ggzUoZjEFdyv2XTGxMobXLz79v1dFFUwEHyMTRYGKKZFAuVWwrKS9Jw4EiLyg7QnuDcnd1VWDUfaokgbknj7njse28zjNUAyoDZxx9GW69FKwdxRn9RJYpPzHQQeaWMgXVkMc8gFg1qq7ScKypbJiX9aDk7WrZwVJ5jmF4q3aM68xshntWvQprSdEmD2cXQiUfNiHZq5aYfazgM1yMX99eJQHpQe2Lf4HtQMCitM4RJ55SfUF6i6juoyRGa7LQb3ABpUZemYdiNBSrXyysPPYzT2PtqYsT2HhuwwVjxQx5osNx5AKPEvGtgV6JW8bBSG5TnCYSD3EtJbE7y1Ni54579UrLpGpASycDv13enuQPuygECtUBAGfiX6kptarp9T9MR6PbuN9nzPBctJewBgkKavqQvCQawopTYvAqVwCkq7ks73xSrrCuqf36jvz2LqrHjiWEXV3tBvmWJzAbns2Cz9XP2jYR6PxiWzSm1sYwD4EzeVSQquHibRS9URDH7KqMnoTQHuWZ63ykndEpdxcowcixCYA8wfiiMTxn5dBZi31L5pBFnpdSQtSSJvQw6fqJ6yc1Cs6mpgabZxBvuVgW8TZm1NYGa6DpsybdkukLZNbPzkgpXStqTAQ7An5sqjXHjeWai5Es3bWLT2ee9xh8y6xiZJAQxGK4SgAaPUTr5adbQPQJvNX2LovATZgjdSrwtXjuK76FbYx1q8MT9w5MahFBtbDp6LSCkQAvFKhaSHmUUrEwkPanqmw3fVytmbB2q1Kz8wPF8iVEvo654aHCdbAV6Bf2xeMHZPZ9ciyj2jJjnJkPfwk5Y61u9BRS4LVAyL4yEdPRAukvBeQjgNGegrZQLm2nFe2WSfaZFM11Kb5hvtnvasB5wPhTvjbrgRkXVUJPrzhVTfQbeBCcR6nB7fMw94huA5N5ZEu8EwsjxcCeD4wzBSUkAcSJ6ETengrEWSm3GXL58pJR9BD4Qv92XsdJ2BsuSg8D4RzkzMBDKsFNvEpKktCm76P31fdgW25k9KEGZFT4XG8EfScLg3pcfjERXWPN9QWbp7XiWQ7JbJPxhGmZ8FqPxDvq7steMiYsZqvGi6g2wy4Mgczie438EHg2AjP4nibyP3QWrPPmbr1NGBoqg4tiN1XUmbxTki4Z4fuxNQ4DCvdVJDqBaS2i627iZLifjR3UVTFh1psLitSD1hNcGnv7SjGf2y3QKtKh5tvGkKhbvYawAZXggnMQNKzRqbbEovCYkq4Hiu6ctLJjLERhv5akH8xD422hSa1gjRkETRorwd7uwT4682QLd2bCpXwm7J4vTEh4DvFoZBmykWepsY8bUgujRkcSQ5dzbMdkFFjiTX94Y3FFhy1fyMb9J4cMFk48xe9dCMffj34EXz5E3AywhB1ZVZQPV1TUevTLPbriC1AQM2YKxntRLpRt3xRbKDe3jksToagEmTbJx4w6W3U4dM1ryc7J4qgZdShVn3ScbkWD1ng6Le8KE3ZJ1vpDKc6sKeGQRSpXXzzYMQsEoNgzrxZGAH2JFpgZepnESNw4P8zfccwQAxPh4aY4fp1Se7WKi5ZiXE5RX1jE1EY5pUozDxkT6iFmCJDTPFbdN2s7eRYXoJAYUCSZSGBwAxEAfxkK182EX4KK3vX4NarWwY2DuFwtwC7fD3LqKkZ2AbRP4XtoGWKQWscpAHvrqLGq2d93G3ezrcjooQuw4iFphKcguuuoM8Vxy53jeWynPRxEAFCKb7ZGjcPaDhkn3zjXBxPdNwLBfBK3VsaffM3MMiQr3GdoBmYRTyJ6JirRVzRBKhBnEwMmjTYpNuHpUoCjDUzswteY32D5eW3rnY7JjqtSXr6WuyNxwrW2ZRbBn4EYPdWAV7AR9pCK9ayhSr3YwCFMuKcsrceKFKY9J3wxtYwhAZP4Rd1BUyW9q4VoXSqrt3fvFLhNZ7JaX1cJE4SK92N5Z4PyZfD4PbKSvvm4cJvArcbAkxgZiPkD7yCCnBsQPYotog4THjGcrajZsqLpoKhCmgHya9EsgMcnN7YqU6Lx8iaXzGmnboowHRKVerhBU7ShKsexb98mArPoTjfZMUuwpNnjm6LieprgoXeCvAWKkkNBerG7y1UCf2zh2Ckjqj4xH8HKJcnrKzupfZN5t6M2w1NyEhBBEAmiEmgc6jDc4AbH43nwFnvQjv7dBj2C1qRcHJ5rFd9ThDyFDjxEmsqQNsKD4PMVcT7QvdGYPoa4N9hMGm5pwiWFKqyhzoZVPNLpPxZazBSW4gRYpdbsqSdEuayoYE3Hw3FKnMGr5otCTPGtEDeoRGEah14P48cCyLiLRhK52rnELWoej53ZSSkhJMt23dw2TRoN5hx8p9Gv4L262YDvfoiomqgmrzVeEhjzPt2iq1pRk8ZZArYm1AuUkxHgZA2SwVQkNbzPJcMGUFjAjwRvMAoPMKiFV8VU9GX7ap51CjU7jJygVmHvfkFexiWM1xtuaWeHBQYQJ7YhP3ukJwonQ6rhwn6WgzEeLciwBNG8gyf6swxpNpxtHh946iAWi4xieqjZsAFr4qkemdkhE5zcjxoFsWfi5KTXRpPWsiZGDj1dB1fwNTu29v8Lk9Dsn12NLmB8zCHz9jhRMTKdksvHXduybyCsX6VRNrhfQP7FAixzfCCC7zW1oNnyz6JUqF9JMmTUPC2RVkfhKBsKtJjnLx5paQ1zEmwxk1Ukro8Cqq1QKeq93sUSKcWPnjU1soMP68Yco4VTcQtfP3eGSNCtxJmEyyqMR32SVcXH9o1xU" + }, + { + "id": "cose-key-ml-kem-512-public", + "algorithm": "ML-KEM-512", + "private_key_seed_hex": "5152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f90", + "public_key_hex": "14d68e92a302b1ac79efd874772880f7e1b497414942210d3a248a407bcb1f6316d262960031c2bfcaa492c79fc9d277489c4e4d834eaffac351c58f4bc3b3a33cba73ac439ad495426cb78750208f73885ed97456c3177ec5b57a58b285fb4ce5d07214d23fb9370c2f032567bcc37cd61308083bfcb9510d6b24df6216c4996205ba14ccf914e6a336a5a60ee1a7618e8882191030a14ca3c0a8b204d0cba55878c78406fba313782a23517309524c818a32b8db18807a44c13db6015ae4c36a4966d5390ca55476d1a3494dbb9d625b178ca7898b6c2e0d8833d2c93bbf01c7b4a00c45a3015af14ac98c2c7d6acdde406d20f3ae02142c9a24b31b48b65e3b76aa58038cc457a5c107f2a694b1d37be10159622792c3210d7604491ae53378029f36116c0506c4ebc725ddd26271a09cfec75206d62166e77ba42264ae2c11731aaa99e555b6227877c0077e8b2cbd897c45da15b3d2636082686baa99504b74bba1181da8c5d20c919b23a6d3b14b7c028788d600ce5a12dda51c73d47d48b37f424c448a7a822e1526ed34cabc99035fb5202902ca25e314967a809b804eaef4099263079ff966e0776ed375c7c1b962183a103f4a3a2f8b126d67cc19cb769f41b2023b43e20965c42841dd9a8f2fdb4c9a6847a40a4f4d4caaa155673e1500aca5a648c719973b81aa1b7bb42596aac640da72b9114498c5b4b54c78c954c9199272b85335629e71794278475995ca79394751c7227e7b9cfbc1ad352a3752d946669b8b5ee99c7cfb4b73e8479ea79a2842a33b1b61f7261e813975b15474a329a8f652522a9629730c9979d93051d52c645621e7668105885b15c033b29ac5efb60c0f211dd556883378a1151c540c40acfc55b4cd8441703524c53b65e6d8842f549657541528419632035bedb49863c232fa217c988053ee007aae732e3c350e76919e89e19e5d035450d97a7f333a4a6a3ce5aca151227413caa769989765b618772b8e0eeb4c2cdc467bbc78dcac31d31a080c177e92ac365922ccf34c24c341cea706a19f79c7f1c1a23fd8b60f5a144110addf019e5519a3aef7f811821a513f7666db17cd37a86a5dd1b82226dcc89f517ca7a8ac423c9b", + "cose_key_hex": "a30107033a000100002059032014d68e92a302b1ac79efd874772880f7e1b497414942210d3a248a407bcb1f6316d262960031c2bfcaa492c79fc9d277489c4e4d834eaffac351c58f4bc3b3a33cba73ac439ad495426cb78750208f73885ed97456c3177ec5b57a58b285fb4ce5d07214d23fb9370c2f032567bcc37cd61308083bfcb9510d6b24df6216c4996205ba14ccf914e6a336a5a60ee1a7618e8882191030a14ca3c0a8b204d0cba55878c78406fba313782a23517309524c818a32b8db18807a44c13db6015ae4c36a4966d5390ca55476d1a3494dbb9d625b178ca7898b6c2e0d8833d2c93bbf01c7b4a00c45a3015af14ac98c2c7d6acdde406d20f3ae02142c9a24b31b48b65e3b76aa58038cc457a5c107f2a694b1d37be10159622792c3210d7604491ae53378029f36116c0506c4ebc725ddd26271a09cfec75206d62166e77ba42264ae2c11731aaa99e555b6227877c0077e8b2cbd897c45da15b3d2636082686baa99504b74bba1181da8c5d20c919b23a6d3b14b7c028788d600ce5a12dda51c73d47d48b37f424c448a7a822e1526ed34cabc99035fb5202902ca25e314967a809b804eaef4099263079ff966e0776ed375c7c1b962183a103f4a3a2f8b126d67cc19cb769f41b2023b43e20965c42841dd9a8f2fdb4c9a6847a40a4f4d4caaa155673e1500aca5a648c719973b81aa1b7bb42596aac640da72b9114498c5b4b54c78c954c9199272b85335629e71794278475995ca79394751c7227e7b9cfbc1ad352a3752d946669b8b5ee99c7cfb4b73e8479ea79a2842a33b1b61f7261e813975b15474a329a8f652522a9629730c9979d93051d52c645621e7668105885b15c033b29ac5efb60c0f211dd556883378a1151c540c40acfc55b4cd8441703524c53b65e6d8842f549657541528419632035bedb49863c232fa217c988053ee007aae732e3c350e76919e89e19e5d035450d97a7f333a4a6a3ce5aca151227413caa769989765b618772b8e0eeb4c2cdc467bbc78dcac31d31a080c177e92ac365922ccf34c24c341cea706a19f79c7f1c1a23fd8b60f5a144110addf019e5519a3aef7f811821a513f7666db17cd37a86a5dd1b82226dcc89f517ca7a8ac423c9b", + "multikey": "z2YqPbPf7jbZqCcWZAqKgiyr6uH2YeobD6s1pGoadBXBBhyXhbFu8QhqHbCb1S6s5eS5tMKTPszokP3tutD2ijHreWnutQr8SP7xgMzH4epjDLCqPzPbe6fjDf44YyRxUoTbCBdK3gDJqHXppQQU8E8kuRB1Ha69cJegfvKgfBG5DEnwRGcPBj7vrFx7TGqxxdiCQXXc3Vw4WAthd6nKPQUbd6umCYj9cytM6on9fhUL6djLk1rjffyDiMpxELnuDz79buPzDLCXYmBgSsLeaBvfrYYufmaxNhZtDLNUMqKVCBdjnE7DmUbN3a25rsX6dw6ZwcjKf5zuA3uktkWPXQ1cpAqcnyDUFkwhUYScs18UgNXnWM6gQbdcemXpWpPZwnSXgnXMsm74EZj6MtyveNV8ya64Erpvod8rRpvqXpbA48SsrScbcpPikqvMaCqoAEQX3WWzytknv8ZFjBdTq8Neb8tfKsk3cVNjkBYcgafBhijCBqxp7AsdCPeHq1dy528mB4rGZARmWhrDMhQSB3YoUu7HGbJsja6Gx7hBfe6dqjP6uQrnnx3qooh5673neYT1HtFa3EFWPubBz3qJwjevr8WRXFHyQjdCMmCf5DQJE5eo9WMoM2HiFefiianYP8QkM8h8tQxzz4h5AnEETRQhCr2ASmbSNnmfD8gMEJsxDGKhxpNB3wRMxpAUvmSbhc2hTfE6GDvcDqLrguu8sX6ZpXskX89ZFTiL2dQ9nCzWGRMcUkNMZXjcJC72vYNcNRznfn8ySs3PKwxgaHK1cPkoNUhAXAE5XZyfifv1AkZ8GE3vQkkf4DShAL2uz6ZzLo6To4e6YRw5QbJTWWme8DgbhxqJ2E25Rvay58wyoRsVCMKhBAngjUyRhztYBxJm7Fp76c7f5XVySqKB9AbU8qgSQ5CAHgHxNwVAiuUCzNaSoSTpr8tMa2gSV52o1UNU37Q6iKJpeXsp8wmdBN8ZWyY7JSHt81ggLoTu3KeBHRa72CLM82e2hkFiynPoWaKt4mVgyBbfTvdM4VeLpCd1o9zrUVFyZZfNYd8uPmVmL5wBArJznfdohpgg6ZECsJjZK6GQPVkUJ" + }, + { + "id": "cose-key-ml-kem-768-public", + "algorithm": "ML-KEM-768", + "private_key_seed_hex": "767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5", + "public_key_hex": "59765536dc320bda853d3a597e258379e8c2b829c2f3b73334d60bd04775c7f37a33a90cf80689c40b08d903c88c148a529a43c29b9001fc51f20726c8fc4b87ca4211817e32fc066aec4f09b71c29d68d0507c4ead26c15b01f49e6cbf562b10e871d3051307f20c52a43b66a74894126a92c0243d1769bd57bc01c7908c47043fb790961e329abfb7702e1cffc8040b0456302332b3592461cfb8862c0199bab7d3477c6c085b524138b2a177bda6151710a0541bc710762ba688486897036680a9cb6f503331851be68ac33741301084f6fd18d3c5587638809c4b8411e25265fa1c268976afb2a718d034855b2ba30ca2275a4c9668abe75a960f5fa06d615a8928c43c6f8b2b8b4c451430b95c2279b500c4ea0a12778c317b97906bb263af85255155d48e5a1fac0666ce96ec632035c403ecf4bcd573ac6f65c54db24c1e173acf31707f19412bd5cb7ece993414015beb457bd6c4ad3a49c5f51b7b317b8f6c88f04574b9bec3c48486f6e08220316cb3a5230b198b6d9e80550fc63cdb99ed582037718c64bab9f3f683a1198785e602a913bbed2dc9f7f7b5a40d460370812a4ea9549f8990d982532994f7c18b6987151de49591e7b96444027a9b39d7cc072df979a1dcb24d2400c88d991ff873230a965f9106f817b1c3a3097b4f0a0667ab1fab943c1f3a953c7ad2e3c901263015e70bb858bc2597b0866f75d98038ef022763085684634133c658d79028eb9652953c5051c051aced49f2a09ab2d89cb8cd8c900a736e4a503a92c55d916932038019ff73586d38bff1a09ee678b372699075c535c60b87c1677d3e7ae6a53aa217429d889b09c2129ea8764af250c281762a3c7146d431db84bb465027806fc531ed644496c653f25779f3209aa8977af2668c185634165a0d208350cb499c4164c9ed2685fa0554a352f0823939488421765cbbe07b1960ca5f2b807f657bee62a49628c69ea12b0f75aa21acc11905bbd6f26a54f31b5d9b6b13e40c3c81a219f4b352773bd8828248d1c0578672639d6061f0c59ab6328f94a8c3380c97e256d365b014b291308981e16d0a79dc1c790d54c3bc38fbeba2d057514945b727d39ceb27b42a84779fd85c2f1550c707629e9d66c47f59dd10412125c40b2d741c95601f4367b5faa19ab215a925b73fc40c75b85cb63769b86f7cc140a82bf72121006260891a1377c1960787d6da6aadd128c739688e9f2b82d8586f55056f16307c5190f87c7246e9b710e87bc47fca91a5ba0a6189d099c7bbe3a028690201e9bca73dc9c441c879d69c2afba23972a0bcf25c00f2637dc8b40a5861195084ad6e504913ac36b87558c10718ca14279da7d83564d3d5c57ee61a7161a6b5078ca0650695c891a00b013ff619ca5e91f7461235677266ee07c4848c84dd9697b213f77c634c6e6ab6ec4c680e2ca4c87014d98aaff17abad2bbda04cb5a3a329cf06a50b32a69f27a5303b45b90036c62944460b5458657d9bfb46e97242b9c58eeac1c74e0cca46aacd618cb242242a808440c789aad00929640c742a4c82a94bc0cf8668af5821fa4894124c8de359a9a2c38082a209b1653823ec1abc80a73091617e3912206464d90ffb6c3a0479eff6c802c0a48a1a7173cf91f72d3c5ad187f0eaa0823e4480", + "cose_key_hex": "a30107033a00010001205904a059765536dc320bda853d3a597e258379e8c2b829c2f3b73334d60bd04775c7f37a33a90cf80689c40b08d903c88c148a529a43c29b9001fc51f20726c8fc4b87ca4211817e32fc066aec4f09b71c29d68d0507c4ead26c15b01f49e6cbf562b10e871d3051307f20c52a43b66a74894126a92c0243d1769bd57bc01c7908c47043fb790961e329abfb7702e1cffc8040b0456302332b3592461cfb8862c0199bab7d3477c6c085b524138b2a177bda6151710a0541bc710762ba688486897036680a9cb6f503331851be68ac33741301084f6fd18d3c5587638809c4b8411e25265fa1c268976afb2a718d034855b2ba30ca2275a4c9668abe75a960f5fa06d615a8928c43c6f8b2b8b4c451430b95c2279b500c4ea0a12778c317b97906bb263af85255155d48e5a1fac0666ce96ec632035c403ecf4bcd573ac6f65c54db24c1e173acf31707f19412bd5cb7ece993414015beb457bd6c4ad3a49c5f51b7b317b8f6c88f04574b9bec3c48486f6e08220316cb3a5230b198b6d9e80550fc63cdb99ed582037718c64bab9f3f683a1198785e602a913bbed2dc9f7f7b5a40d460370812a4ea9549f8990d982532994f7c18b6987151de49591e7b96444027a9b39d7cc072df979a1dcb24d2400c88d991ff873230a965f9106f817b1c3a3097b4f0a0667ab1fab943c1f3a953c7ad2e3c901263015e70bb858bc2597b0866f75d98038ef022763085684634133c658d79028eb9652953c5051c051aced49f2a09ab2d89cb8cd8c900a736e4a503a92c55d916932038019ff73586d38bff1a09ee678b372699075c535c60b87c1677d3e7ae6a53aa217429d889b09c2129ea8764af250c281762a3c7146d431db84bb465027806fc531ed644496c653f25779f3209aa8977af2668c185634165a0d208350cb499c4164c9ed2685fa0554a352f0823939488421765cbbe07b1960ca5f2b807f657bee62a49628c69ea12b0f75aa21acc11905bbd6f26a54f31b5d9b6b13e40c3c81a219f4b352773bd8828248d1c0578672639d6061f0c59ab6328f94a8c3380c97e256d365b014b291308981e16d0a79dc1c790d54c3bc38fbeba2d057514945b727d39ceb27b42a84779fd85c2f1550c707629e9d66c47f59dd10412125c40b2d741c95601f4367b5faa19ab215a925b73fc40c75b85cb63769b86f7cc140a82bf72121006260891a1377c1960787d6da6aadd128c739688e9f2b82d8586f55056f16307c5190f87c7246e9b710e87bc47fca91a5ba0a6189d099c7bbe3a028690201e9bca73dc9c441c879d69c2afba23972a0bcf25c00f2637dc8b40a5861195084ad6e504913ac36b87558c10718ca14279da7d83564d3d5c57ee61a7161a6b5078ca0650695c891a00b013ff619ca5e91f7461235677266ee07c4848c84dd9697b213f77c634c6e6ab6ec4c680e2ca4c87014d98aaff17abad2bbda04cb5a3a329cf06a50b32a69f27a5303b45b90036c62944460b5458657d9bfb46e97242b9c58eeac1c74e0cca46aacd618cb242242a808440c789aad00929640c742a4c82a94bc0cf8668af5821fa4894124c8de359a9a2c38082a209b1653823ec1abc80a73091617e3912206464d90ffb6c3a0479eff6c802c0a48a1a7173cf91f72d3c5ad187f0eaa0823e4480", + "multikey": "z9LYPzgHD5GB1ico2oDKgQSZLqedJKbHJovXK2BGqkZ7nLCojQRxNCZyuzjVoKDk7VvzLHhszA1yAk9hKi7BxJWMhUu4PBgHWY8Uj3WY9NPgKkcRqUFfM9xLAvuA8PJQYE21bsGtCPBU3FpXawECCjrbPqa7qf5aX3wwC5V46YitSAJh3mkFvguF6TM4erY88J8vibajRTYHB1kkmtThx4v2XigSq9AGG6fUWJAQgpk9GGAx72JUNsuiuQwVRnRoYgjm3JRd81nsHNmcX9EtMTwsxHsvL9NLGgpk9fHhTRxsYPniTVKY8vesh6G8FgKyAjaAERGvupm63HvaGEB36bbMQLU6WKbCr7qtk6QtVZbV7vsBgq8LJk9v9fgtoVNdYXi9gKbP52b4rGiZnGir22Y2CmDXHw3cw5EEnUdhBYM8e9QX15xZXdikp43Y69wMEoHEvt9y4BmckXaC3XDKrEcDyPDxViLJAvksK1wcGauUwUrgBy9cLGPt6qv8y2JbTDSkXjUNaVGsLF1gAL3LBqmq2HXr4mBEUpcpE7rpQcsaTTJKsDY98FCW2cx375VYk7PUiq3h8ZDyUy5VzFEoe4Q7Gvav55yUn4LCNS8hXME2HMFjiPgUMUbmyUxrCvPQS3mkDNLvyVG7PAosTwee9Fq83rHjLgHDLAxqJh1GRi3UqZMPSBfjicTZyL3QNah7zhaqvmPa5ZNb6DQJRdDvsYniYTZQxcepdmSFpGyU9538p4JfQyRQGsndsFoQVFvFNmFQG2yLqiEbJ7pvzTmrZj1kxho7FKcmbzXxpV1dNDu3xwMzoAbuT7KpFbeFfBtT5qRkcXgLtydo4RJ7YwoM8HMuzBRX4C97BzwnSKvhXFUZEnRP5fmKAquTdXnfCvhSQSgiwgogBJx6mdkFhfrtbvk5F4DHjsb4mzRvo2hJW2dV1DEKUsmVV7KKr3a5VYfJ4kHArPuXPK9cKr7fKJu2kNym6cLiqfFtbo3kifDpfizkDRwdxUriqZCK7uMSkNd6JvFRFvGNhULgxJPT4uXeTYTtdEJSZtZnXRH91ueMBjEcbnwGCFiJzYxprGgB3YyUtVERoreFr1UEBD9kFCo2uYUzeU6Znms3f8aRQaELChfeLdoxz14HnaGYqZrUftxrT4MoTQrCDh5C2QWKnuT2uReqrbarNFnqcwucnzsReGQzx9Br6JWAwJdcFC95Sqb3AdtW9UwuQmYGbb1pf8nvEerHtSFDmubAQuRQNLqsKv4WdaWPsVB5gNk9nAJTrgGNrL25RciPYWrSGKs5TYT6iusgU4ZoWeB3npSWhQwt8Sm4GuQoXk5UXP2tKo4nKqd6J8nt8GPjXxSk7TQSv5QP54udYPE9DoCzzyCGksGVJUvLVJrf1rEzacECmtecEdrsdT8Hdf6fqqwH1Thv3yqMARW98bZhuyLfYCt3F57pNpE5dxGKD18HAbe61xFhaVxQRy7c2HLVCDNsSnkCyTNiuBbDsaNxTANp9ufZKd6oWRLDLQqxexNTjaXLBdrJeFcXb7z3EsQbmweX9S4RyL3eaBfGS72Bubw5CBusiwah2va2NRYdkJKmpfewJJHfsDVsrcuifjNiB28DEvqPshm8T" + }, + { + "id": "cose-key-ml-kem-1024-public", + "algorithm": "ML-KEM-1024", + "private_key_seed_hex": "a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0", + "public_key_hex": "7ba90d0ab9b78e421a14ea89e6e31a5cb9161fda58130b55ad651070c768c236bcec62a5a5d80855684f42043aae767dafa53cb1d39f36fc43f36a93effc16525005c768aa3db4401f893c6095cb5e82bb215670e81593916303c69664742c97814947a7e65fd3f754dd3a3ecdc2cdc9cb38e437215fcab7f9dc11e36b8d43a634702829c7580faa2b0bc29c3e3a7aa978e12b03b14f0660475f62ba08286b186446fbd9b9f4688cc9e68188dab8a2737fe3abcdbeec6b79f18a1c4629ca7a77adb254eb03af987447ac658a05a53ddf99aab3d316645a28f0bc8247473be1085cd4c178c2d5b9eab7a86aa8c850b964bd857d8d429a139a0ebd522bad3b53205c16d6e66100d325aca58f317642cbe76fc5ea058953815f32976468268f908c2dfc2995716ebcea49195a6635c7bd9319903f9ab53a6b5edb6c03bff34f1ba7618807133eb00e6acab4fcb77615f442f737b76622296221810daba83b8372c72ac5c821053dd05ea0349da753747f337fda856bc4013d90bb4cf4fbac21641853dc858e0382d6818166c1bcd64596c24b08e7ebcc18d39629112796da010d69af7f6b6d2d40a34c01ccbbba112fb7059548235e0383026acae629aac4222ce58c754f39b4792bb55c066b1d15aaead5c148b82ab868b74cc906cf1009d0872666f7342ada9dba7381a6728a0d31a677ba49cc2b9739d64f071863aa7303d0581c6457049f17c45e9c70fe9b766fbb57c3fca0e312c89719a08d9a7ec2bb29840c5b375c0b2ac7041e353b8c263419c4bb13321c41d32316797d9b85b0c7b0392e18558c82005bf31fcb2b440a873b60da20d511c99c0c8438fbb342c5084a11001932180e58cc0534543cb644bce223ceb4cf7a6c6150137bce18349ce6cbe8b9c19f81cf42c067c3c75e014223f7a0218edcade8c43c5ad01e89f4c25cb270e8bc901cd7162e6707e0127a0e06c1654796e45390c1eb27d22a83089b51a5d0c2e7ccb39e365c7a870006b2193518c629b1916f811a3a809dd8a56de9328cdb251c57a50f5b0b7c3e019b462a8729776fb32ba7b1caa57e603d81397665ac10e83a899f0c6f5725a2b5b68028a0773a1aa4e14568b05041e198b2ebb59f8c34a9a92872d08170a7a11e0aea396a468df0026371a4486a9a1adc79a2d7648db3c76e3b1a6985920e699a1eb11c00e1f9566371bf8ee27704f812e468ba22159277264ced05bab3b1a61f3b0d1ff441794b4dbefc2aa54b2903533bc7345c46d9b84b204267359672791d843642a665be3d38081602b406c6479090a286d14f22bcb8870a009ed09732f21394da28128b6e7b357cfda46b529656e6da2f80db415c0b69cf814549f9116b10b1851219f3fcca6066860e0ba556da95684b7a8ca6bfe0d47f71f67a1296737ab41aae196331a49de2d8b614902122923933a01f8137aa5d81430bb4a48780b9014a6d71b39ac2082aba2ca2fa5a74e1a5b96dd89548132d174b46b2a4432d3156e1d730961bc3bd220383d978fb609d314387f8b971d3485b3d0772c79114d96499b4a50ab461c54a221570da2cf2471066bb6f1e306eafa2ac28e06e5f37ae3f135849181304810565025ac380cb3db9b6b956a13397472b39cf27128a9096082a6b9dd1a1bf6e406c7210404c705a571c5ffda81e77c96883d21a1ce6a191456fcf736c847283b07a1439f4018cc713364658cbe1b417ec098a234e5caa272d05cbc688ab67b773720a47215c9a15394d456102a5cacf282571ea942ed8994556c9858aca2a1d253c25436e86156875e6a11883874e5315aac00c1aa10fd9b075209539e771ccf0b2944ad4ab87fb45d1d438f7907f7a1ac45045bc771616d90384229780a29ca490632b83454433db144e4b6fa87c4d2c1a340bca55567b7719a646e641bef0c2481fd84f99453975fc1f72dbc859c1629a453a0b3875d5ec123046c1cd487fe2c5537c07c41a376b8995918f889fb58b3139598c34f43be972219f38a4eafb6196b67f6fd41ae80398f390771100429b38cfd73b7a546982e4b876f6db76502c5ce2a813ea318ba5a2b80cca63570b56b8443c9fa39e03792acf6445c71777077504e8ba3106abaf8f47ab0069864daba302c2691730919a11c4f8637e9efb8c659b6ad3458692180fbafa20637f405e02b72a5dcc23f89c2664a5f5a3d9f1f269830e409812af59ebabef21", + "cose_key_hex": "a30107033a00010002205906207ba90d0ab9b78e421a14ea89e6e31a5cb9161fda58130b55ad651070c768c236bcec62a5a5d80855684f42043aae767dafa53cb1d39f36fc43f36a93effc16525005c768aa3db4401f893c6095cb5e82bb215670e81593916303c69664742c97814947a7e65fd3f754dd3a3ecdc2cdc9cb38e437215fcab7f9dc11e36b8d43a634702829c7580faa2b0bc29c3e3a7aa978e12b03b14f0660475f62ba08286b186446fbd9b9f4688cc9e68188dab8a2737fe3abcdbeec6b79f18a1c4629ca7a77adb254eb03af987447ac658a05a53ddf99aab3d316645a28f0bc8247473be1085cd4c178c2d5b9eab7a86aa8c850b964bd857d8d429a139a0ebd522bad3b53205c16d6e66100d325aca58f317642cbe76fc5ea058953815f32976468268f908c2dfc2995716ebcea49195a6635c7bd9319903f9ab53a6b5edb6c03bff34f1ba7618807133eb00e6acab4fcb77615f442f737b76622296221810daba83b8372c72ac5c821053dd05ea0349da753747f337fda856bc4013d90bb4cf4fbac21641853dc858e0382d6818166c1bcd64596c24b08e7ebcc18d39629112796da010d69af7f6b6d2d40a34c01ccbbba112fb7059548235e0383026acae629aac4222ce58c754f39b4792bb55c066b1d15aaead5c148b82ab868b74cc906cf1009d0872666f7342ada9dba7381a6728a0d31a677ba49cc2b9739d64f071863aa7303d0581c6457049f17c45e9c70fe9b766fbb57c3fca0e312c89719a08d9a7ec2bb29840c5b375c0b2ac7041e353b8c263419c4bb13321c41d32316797d9b85b0c7b0392e18558c82005bf31fcb2b440a873b60da20d511c99c0c8438fbb342c5084a11001932180e58cc0534543cb644bce223ceb4cf7a6c6150137bce18349ce6cbe8b9c19f81cf42c067c3c75e014223f7a0218edcade8c43c5ad01e89f4c25cb270e8bc901cd7162e6707e0127a0e06c1654796e45390c1eb27d22a83089b51a5d0c2e7ccb39e365c7a870006b2193518c629b1916f811a3a809dd8a56de9328cdb251c57a50f5b0b7c3e019b462a8729776fb32ba7b1caa57e603d81397665ac10e83a899f0c6f5725a2b5b68028a0773a1aa4e14568b05041e198b2ebb59f8c34a9a92872d08170a7a11e0aea396a468df0026371a4486a9a1adc79a2d7648db3c76e3b1a6985920e699a1eb11c00e1f9566371bf8ee27704f812e468ba22159277264ced05bab3b1a61f3b0d1ff441794b4dbefc2aa54b2903533bc7345c46d9b84b204267359672791d843642a665be3d38081602b406c6479090a286d14f22bcb8870a009ed09732f21394da28128b6e7b357cfda46b529656e6da2f80db415c0b69cf814549f9116b10b1851219f3fcca6066860e0ba556da95684b7a8ca6bfe0d47f71f67a1296737ab41aae196331a49de2d8b614902122923933a01f8137aa5d81430bb4a48780b9014a6d71b39ac2082aba2ca2fa5a74e1a5b96dd89548132d174b46b2a4432d3156e1d730961bc3bd220383d978fb609d314387f8b971d3485b3d0772c79114d96499b4a50ab461c54a221570da2cf2471066bb6f1e306eafa2ac28e06e5f37ae3f135849181304810565025ac380cb3db9b6b956a13397472b39cf27128a9096082a6b9dd1a1bf6e406c7210404c705a571c5ffda81e77c96883d21a1ce6a191456fcf736c847283b07a1439f4018cc713364658cbe1b417ec098a234e5caa272d05cbc688ab67b773720a47215c9a15394d456102a5cacf282571ea942ed8994556c9858aca2a1d253c25436e86156875e6a11883874e5315aac00c1aa10fd9b075209539e771ccf0b2944ad4ab87fb45d1d438f7907f7a1ac45045bc771616d90384229780a29ca490632b83454433db144e4b6fa87c4d2c1a340bca55567b7719a646e641bef0c2481fd84f99453975fc1f72dbc859c1629a453a0b3875d5ec123046c1cd487fe2c5537c07c41a376b8995918f889fb58b3139598c34f43be972219f38a4eafb6196b67f6fd41ae80398f390771100429b38cfd73b7a546982e4b876f6db76502c5ce2a813ea318ba5a2b80cca63570b56b8443c9fa39e03792acf6445c71777077504e8ba3106abaf8f47ab0069864daba302c2691730919a11c4f8637e9efb8c659b6ad3458692180fbafa20637f405e02b72a5dcc23f89c2664a5f5a3d9f1f269830e409812af59ebabef21", + "multikey": "zmsZdziyqVdB9kLREv8GX8U1cJQivwWTUcKtn5poCEPMvh9ck5o9EppdHuuMVrkQSofTwxax6xWrCaaAn9KeDHf4gBURYBAhCLDGEmqUwjyX21FXDcRWqcM1qFEDMc8wAyffmKciKzCG3uUDgH6ND8U2Hz2HgD6z3r5unScay1aTbqBWF6wy1cKtmBrfyAaywXzQfaugbBn6DL83QGxqsfds5B6mUVeVVv6VoemU6zKWuTqHa2n1pzE6RyvWogusbDRTAwXrFpJmHdz7oCu8zWfx5yZNXoCJGn7hdxTpzL7f1BGaZTYQe6rnYAnPhZ8jRP5vvaJSmVfZakevm3rCjhBUrP8Tn313C25yMNu5GbCXCV5ysgaEbHbeJd3ULUwNouCxebe6E4cLkCQwJiiER5vENhsprBcF8U95yEeXheSAypHZHruNHA2fknKni5cR3cYjEXqebFRCXdoQnHAbSjScbGtseTFKDP7T9WJZBnbXsGzsbCh9eGQ98ZgYmQDeuLdiYN1yGNmpnGajUcZoB7FeEmKJB3k6KwU8LHoRRPSpcdpQ8as4vpRgVTEkfQGFsRRiW56cVyhdRcy195bjhSg9ZCPPWDNc8wHviEaGVnns5sUL13kyviBr4bqNbq64kESymad86hmALjK4uUarwxrCmFEGbUz19qXKUNPxXeuhGWLqMYnip9JCsBn5Y2VtRGrdXsHuXkwVeQYjuyHbCZdgqxGA5hhRcaDoTj5EMmXGBYF9cVFA6dpzMZBcqb7LfKBe8DKKA9vnqwWm2NCKpvygkaJqCGHUbWWCzyLwyYoZNG5FwoREsoXWSQUuyTpugpFJQ6iEiDKoPon6AqrGnhM8jSQcxG3WPDB7GcYc6Xh63JHmdmFAkp4qX8ZYH8AtXHq7L5iSivZnQcUVBu7TprrrHREBYrZgto3eMhskNkfEzRDvggB4KDG6iyEHzcVXFh6Nv9WFDfXF5zGrHmFAKoHAmF6PoHSEkhtJcou552zj7y9uKbqt44VsS4oVW1SQYQQmgSLuyH8Xd7GyeKfWbNKtMW5vuCHUr7PgMRKDNvueoM4XbRjzAAJZ4DWcbZaxVtoVFfxZQiHEMqGDBj5Q7m9Kdj3p99qHcsZSiDRawtPNgCFKhpvFFthSAcmywAYtMst9CJDVcstjw7k8K33DzgVjDz47P4Qiqwi6qSJogXRajWNisGGyx4e5jAHk62m4VJkQHs5HA6T5EDJsuexcqPiMSqxTKZV6o6QwLSm2ErUJF3QvZJSpFJSDS6fVCSZiH3n7shmoHK1KTAAFEfRp1v5ML18x7qTqQE23gYigcUeHfWZtdtNMbEqudhwVJjD3MfJFCKedV2TBe9nzamD6w5V9P414u9L46jQKyXk6s1c2kWA96CRQteR2WZ9KMLuUGbp7rTf3igKLC4tRprhSaMZhBgz961PtZFkrtjevN1aV3cPL9VKpF64jmsUifompq6Yp8CCNfscMzeKK6G4hf99tsZsMv1WHMB3hqDHt2ZH329pGMdiLaCJTnQeXTTfgiHacjEoxQ19i6pXWSGiCAvA8LVMUB1yxN6cqNguCadPcLRQpNHrg3A926DmKoCDXbZExgpURuwJBFwHgirHaLLh6SmkT237svkkJqi5zbajAWjWSM94UyNaczAvjwutzYLGG2HrJqw9XU1MAWj34wUWAnKV4vUHwbwyp7SCv7qseGcNMDEUzmmVG54Kg8JVKUkfxMfnuL3HvJ4Uj57HwkN7EMQW3h3So3DoLEagA43SpMPR9uhPY4DzQD2z8M91MDMTuBUYeRE3UuMzsUKy6vP5zG5JDoi81qtoStKfb7XSHDsFhPNaS77VdhqLgekyYA67hpEiYhs7j7KdMaWB18qRY1H68LLukXzeeug2AqaajeVK69b1FESRvtKp6zJdLaX7fBBdUjMKXJDhA4Ri8nLK4NMpWuSmy6yH9vPvJgxotowD1SXpTG9pTYb7cg2SHuLhavBvRyq9grYGTqBSYQNFqTcFwdFxWHpzAas5rKkbfvEtcyxyFQZf5fVz8jobavvSkRMg3ceyX3UfbiBeam2yPpgcpExWaNcAD6BrnFacfVxtFJKsiSZcJhk3gUrA5n1dG6mSNBBvbDhhEM4EbtEqNPHW2TRJRE" + } + ] +} diff --git a/conformance/vectors/cose-key.json b/vectors/cose-key.json similarity index 89% rename from conformance/vectors/cose-key.json rename to vectors/cose-key.json index 8a8cdca..f6bd834 100644 --- a/conformance/vectors/cose-key.json +++ b/vectors/cose-key.json @@ -6,28 +6,28 @@ "id": "cose-key-ed25519-public", "algorithm": "Ed25519", "public_key_hex": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c", - "cose_key_hex": "a4010103272006215820ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c", + "cose_key_hex": "a4010103322006215820ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c", "multikey": "z6MkvDqGT54cXesYGvABpF1UapVNwjCqRcafi4Px6Thv5T3Z" }, { "id": "cose-key-p256-public", "algorithm": "P256", "public_key_hex": "037b78bfe0ed44cc7321059692848a3b0c35d04e1b82d6b1e1214ab87dbd789ce3", - "cose_key_hex": "a50102032620012158207b78bfe0ed44cc7321059692848a3b0c35d04e1b82d6b1e1214ab87dbd789ce322f5", + "cose_key_hex": "a50102032820012158207b78bfe0ed44cc7321059692848a3b0c35d04e1b82d6b1e1214ab87dbd789ce322f5", "multikey": "zDnaeqyGMYu4v1aQhn1xM4cTVnaSwBwVu6iNQL295ChCKc8Xc" }, { "id": "cose-key-p384-public", "algorithm": "P384", "public_key_hex": "0352de2820d3930225c8705258613331e8d899c856e0e4ab9d1a5304ed98dcbe9e430bafed7d77da8310c7d384cc57b4b3", - "cose_key_hex": "a50102033822200221583052de2820d3930225c8705258613331e8d899c856e0e4ab9d1a5304ed98dcbe9e430bafed7d77da8310c7d384cc57b4b322f5", + "cose_key_hex": "a50102033832200221583052de2820d3930225c8705258613331e8d899c856e0e4ab9d1a5304ed98dcbe9e430bafed7d77da8310c7d384cc57b4b322f5", "multikey": "z82Lkxx6RdxCD74fDBzwctEaafBQHzbNZNkSpWAuMfy1VCZz3QgWnXnNU42E3oBUTSapDm4" }, { "id": "cose-key-p521-public", "algorithm": "P521", "public_key_hex": "0201cc5bfee5d5df62f5234b48706f56e44969229bc4f61c7e1b0e25ee7c74a62b6f0331a697fa1323e5248ad54ea3f5f5e5c95a0dde90c3f34e8496a125608074359e", - "cose_key_hex": "a50102033823200321584201cc5bfee5d5df62f5234b48706f56e44969229bc4f61c7e1b0e25ee7c74a62b6f0331a697fa1323e5248ad54ea3f5f5e5c95a0dde90c3f34e8496a125608074359e22f4", + "cose_key_hex": "a50102033833200321584201cc5bfee5d5df62f5234b48706f56e44969229bc4f61c7e1b0e25ee7c74a62b6f0331a697fa1323e5248ad54ea3f5f5e5c95a0dde90c3f34e8496a125608074359e22f4", "multikey": "z2J9gaZDcmFSVZQy5Dj47ai1f3rka7Xvgf9XYkuN2oSzy88YBhvFfrZKG3q6FQVuSiHfFxhGtFcDNRRaP2YhkhPQWamVKGpD" }, { diff --git a/vectors/cose-sign1-pq.json b/vectors/cose-sign1-pq.json new file mode 100644 index 0000000..4ab3ca7 --- /dev/null +++ b/vectors/cose-sign1-pq.json @@ -0,0 +1,84 @@ +{ + "schema": "reallyme.cose.conformance.cose_sign1_pq.v1", + "suite": "cose-sign1-pq", + "note": "Deterministic ML-DSA COSE_Sign1 fixtures with independent COSE-layer parsing and direct primitive verification by the vector auditor.", + "cases": [ + { + "id": "cose-sign1-ml-dsa-44-attached", + "operation": "verify_attached", + "algorithm": "ML-DSA-44", + "kid_hex": "96a37d942464e544464f0e6f136e918e166211325cf8e8656d3039d535855719", + "public_key_hex": "811dbbc20f9276252bff02f3dfc4b251f7af745bd086a766d20fc70057ef20da13b36d64d3a18cd9192e8d4f6bdeafe9c6ab5a2b0986ddd5702dc8a24d55f214d69879ef2397a47c1eb476ed2ba80d2b1b7fd6ae35715e2c304b9a276f322bf4335f82f5fa10772fa7e9c38cfa6626f5359b354939aff4d3ca14b45d58df3bef62424115ad7da67fd6c05548e4171293c4b910dafff66dd6c736704ca637d617891bc5d50a01c02544a5465268c2f23ca2e37eece192b8873cd360557c6d8b76299cd89bc9dd2e7bac1ac135fb854aff03e17303d8327b1040c3482296e85e7ac5b10222c2ffee4fca78bc39f9f452d3d3f705178eb3b90f61d6dbbe17649e1866ff8e19d58c31b3e09bb6dab331a1a3dcc56c4e74ce920310d6674fee45d6a0696543efaadac43c6f7be34483fd233a4a03d9f6d715fc3b977a582583c096b30754419e8a873669990d0a22cd70c244e928e0f9f2fa6a1a7fea339ef70603da5f14a51b1ae9353f547127494571dc5eae695b331727d7e7887ef8d832a2b75035575910b16acb1eb7f131eb5970c489a27cb8766461527ecda3086492b942d8043c02af229ee6b7652dca93b1275749cbf029db04d27d588d2bad0f5a03be3265c22bdb4b751d2e03683f9e59bed8d5f3c614b29128ddb12599b51e269cf031c7569119304e4ba659266e1a7b650d8a4a08d671d664db4c233b7b3eb59460de0c4d02476cd1e5e8a95afb85b535a091ff370e2116790dfc65b07b37da893024f81c65f058ed0243d8f6586154097c0e09042c3e53317917fb158ef8251740863ac27cb0d72a398c64dfb435d7ff08179e9498748b1d4370be59268a9a55054cc1adfcacde0525c40afb31bead4dc913f75e2f72d034a5b97e3a0b8fa2045bbcb08c9ceae32c5249a2341aeb3ce1c2685ebe95e0edae2595431ca18df145715e12757bf450ce181a0d369045b212157c4db1e0c15be0173ee92feb049660e441dec621c63cbf94184164e4cfc2e5c35c97bfbb66109074de2d041adbab11e4560177f3c5628a5111f403d2a21144230d2a58bcf0d9c8b7d7af675947161e0b8b4299daa1705e72b6ffeff41771b43fd0bb437f97685e6d7feb5cad282726d8b3ad134190c0a5807b596d97334f8ac41db21b0b6e8f1466b9aa3b0436175770facf488fc85a38cd06e03313ffb97f5a53efeb21ebdd2bf3227ace88cd25cc91f443138fca6fe9e72c27ae7df304e82dd6f99efabff03622e3d1a5ac7b1555acd51f0d50f6f4bd2a164051b2a28ce6180aa3787fa3adbaefee6221740c4ebee123e1a5d4efac67d32193eb265252188e94443eeb4e53b98c1990667b806d79c232610f624ea9c3bb695e88d4a90b77c467011bfc1c7613f2ab3648007c90ba51eeb37f6207551212f55b58c19eefd6a5857c3565e9dd5bac0ab46b2af63ccd2a920cad3c9bc9976983e1bf74fd9f8e41f612b13b12f11bf248b2807305b32ef474e7c55157390070f394851f1f495996300a229a237da21640c8afb3fad253186a8e48e507e9a0dce84060a3c4a6106565508c7fb773419557d4fe326af8223779e0ed97038d64b1b16dfd29da0d9a7ac949aead491db6be95a304b72ba82863b04f8be515df607d360b7326232ef7243b83358535bd06c5335bb123fb5ba6b33f329df0dd3c589834455b84f8bd93a1de18850a9335a23956c82132ea1487a3a7c670960351c929a2ea30d7944c9e1978d3b3ff81cf992fb5186bf6afa02bb22275429424f22baf8f9332061216eca61289defc920870fd98c39e39ed4bd807c3a155b6d4163b23ec22a7e277e8f4c176bd2ac97a8fd793e52faf7fe793262312", + "private_key_seed_hex": "449137f736f5f5a35eb9f37c1c88c2a0bcf18e757ffb9285ab2c4c26c15c55f1", + "payload_hex": "5265616c6c794d6520696e646570656e64656e74204d4c2d4453412d343420434f53455f5369676e3120766563746f72", + "cose_sign1_hex": "845827a201382f04582096a37d942464e544464f0e6f136e918e166211325cf8e8656d3039d535855719a058305265616c6c794d6520696e646570656e64656e74204d4c2d4453412d343420434f53455f5369676e3120766563746f72590974914cdc5f2d750922bc745f84a63f5889c12dd2f69d9e8558e23bd4e78373979537780ba99b913aecef30010bd236ef73ec060471ad2d87cf449b218f3a33cf39fcdbc23c515752fb551a9c6e9e4f515f1195f6699c9aa7f3593a5f0ff365f470d4eb8c3f254c2e08a4b029587b007f06c9f5a7f23ccfe4da0a558fe757c2ced57c6addc9d99a95609bfb07527472c191891a941e4c0f7a1ed68ab0094da19e024aee45e91c7cd09a8593404a90fa49239820656a2a92dee30b8ee940cd1eee176f7ef8453bbcf1b20eb51991469a06c2a0dab7106c4ea173da9440ba24ab1e22555508095aaabc790ae1762541729389b1a73abbb11e3834c6426e2bc75591c503b8250a0590ca4202e8af6a1259c93683d425b1746bd196031e3dd42599bfddb6e0bedfd9c1f84212aa8544d15e1c03fa5f4c18583d864a98f2767e8b14a14039869bf3261a45a5b6670bd16603e8039c160ba8c943a7a861fed88c92141ce0613e7edf550668077ecebb1a387ba406f2238989f60c2f4a3f2fa0d7fd7ea5118e757d31d0945b49af237b2de123f8e5ef14bace4dc45010a55bfe88ebba8be317b5f19a8dc636db7ad1ea3fa9adc53669eaa1b90e4a59325bc254b6664a645b911b29bf25a84d7e7d8a94a471d0199a3f51b6ffc4acb939cf1a737bf8ea15391c894feab30ee94282664c750f64788633042dcbc3fe12f0770333b5af06e508033e4fe3ea8917f07b355933ad4aebedb51c55fe135ee000e3220166163b236fb96714d2a2a6c580715f40f8000cd67a2579d54bd631240d59a9beb9a9a57d1ceef7c37ca1bc5d34fdfd51104bf23c7a89c5e97d3e1feba8c9fbbfc4f358c8238fbfe65d2d1273ef9fb51f638b340ea2fe4febd81c1daa907b879b9e65ff6d0df0a3c9a8cd22a16eb6e9c2492989730e3947fd8a69a2f082ef3c6b547804d102ef79f943eb58603834ab7da36786c7e765fde5e1f499b23c05e5087d4e29ac180a9c54a35a9737493325ab03b102261314fcb53423083a1ea3a54c2e96abf411f3d95462c2019aead356adcbf385d3c38588735d71ff83ca877b7d224d3c975a560351fa16cbae6eca867a9e5376a0e34c1ec7c3ce467c892bdda9e1d59af8b1eb304e96fa9c92ff70d2835933a8edf93b47e5664e58bcd45befd930e7d6d35ba86fb1b06e29e99bc0c3bc1abdb879fc7e0f2d5d7acee54dc6543a9e448b677a3510e768d16a18ad63a7722712799fc3055b18f2fff23beda28848147ccfeb054d32aff191ee055461af0d4eb816137ef9826ebdc9d1bb1354343b1f5893ccdef47d4f492906919d973f6c9d401009ebfdcc292cb6d083feaf474114185e6458e52f731e68399cc1986ee4142f139d2bb492936f361f4f4e0125abb300af11003bb08d3381d869ebdae73bad6a3f79b1831daca5dfc49ea488cf4dbcfec61a041963e2035dda9fc5cd07ca6e391e855d21f30cf360fe8fe506b521b7233227915efb0d9d6a84a519d26eb5d92b35d8e575e509d126e87f8515f520f3e416389f129600508d24e26272b08dec1b97a0d6361f29ef3c0f108a3b38208781d41a6ec15bf08e03e04041aa6b3259cea7334d86d1d9fa5e5264ed9ffd67f2ccc0e4f339e9c5688d93ca098374807aca487e52be58c656daf1a3a3214bddd18443ce9f4bca7d9b0f4aedabf23172e8fa7c8aaa6103fb8808f64f1d9a3fbf7437df320121daf1707c3ac58eec441819ce2529cb15398ce987e7e9fb84d8f821445ac69dc732b71a8294e19f5afdbc0f5e08d42ab8a42f2acf95dfb0e3ca4dc85766c654cac31e5ef9b2bfc6830d686ed8d55c7d787a30c455515bba0796cd4cb77e1e198947b0bf68c7b5939315361d5684d2ad01a9387824adce9a81ae3975a6b73c4f341590988a6527271ad7260d9506d7d0da9b2b7e6a9d6c30fb4b8726ffdce8ed15914dc8e785a875c7948f908480b353cd550efe7199dabc25219114f5ceb2c10d0f27134bbb66d77860960babac04cce966e71a2f09794959e24556a560371502477fa3edbb6364276fe5b7d88e20c96225dc824bc3720660c6f11777e905adadd2025b7c62de54e23e09011928b8a14b437dd0cf00f15ca5015e359ca24c0911ac83666043d7044d33441da96fe6faf0fe592b80d4f4d03d026b8867958289f4f66116e100811df35693e8483c5ba503827620e285742c7eaac485b2c075c587927d57d8e91ecae21eb0b88c71af192c8b1c0c6fca422524769fbbeacc3abe7bca7e9ed990d1c251cf5eeeb6ebeec08ed1ff9fbbaa6b5da9187729a0f68cf782f5e6733c8353511d0e03ce2ae9b10b9c093d430691f65e02aa39d4fc92df52fba52177cb7d65c430557ef8259ccd2654043a4c73728b04ab8b8cf907faa5566a229cf2d0d370bc3a319912340029dc21be1751cb7b254438a7c619da6e3f4c2ad36e6e6d0826176bd2975b9f13cef05e27461445a6c4e8e9654fe9f3249c260c6f68c7cdf60565cb82956e5b00912a82749ba39bddc46e27fba042f363e657d9b26b52f307635e2c4565b19fb3ac42079735839d1d283747e077a106fa13646640d1dfb53db4daff56e4dc1b276e90071a4a7d537ac7d9094b98f28580ad3a211f73ce2fcf770b662ebcba86688335f8a21e3ed78f2d4921cb38d69a7c25e3b2933dfbc90b543501077ec8d64b6fdc6b95597cbefac7b10c34a50dffea4acfeae1308360204a6607b2609d48fee7d4eb5be876f71c18ca77c08901363d120aba74cce485d9405bc51fad57c515707a11ddbbab554928be7003f7f3b68b2884110c256e136bd7ebe6896c7da834b9bd408a128d1e07213d13834426b11e1cca034166a1325ee6930a7b72db3251d8e9ddd11fc29dc8bf840312c59dba3cca36ea06d9e25c235cba4c27a47832cc31a54587121ab4933eae20f8ed330f62800c620eac8eb06af974fde0d06c99b25e502226087cb1acc7ccc4c577aeb90cc7f08e96a1ee15fdb47c016b22aad4da1faf428a685a09f04f8a5182cc6db022704e6478dbe2e526443e857400ff860d3bd99d7c43b82af9ec21c2004eb46a0cfcbdb69c54ec10861984da6bc7001751b3ca77d0391068b2586f2fdbe67a538d153fae8062c15e590c8e342fc0f74f6165aad336e223d96a77dce03b49cbdf197543e69f0f815cfa6734eba257eb16fd73a3a9509a553528a0a413d18155d0cded1c8a1a7786c1ad3b2734241fdaeb49a4b6efab98d0cfe644b9cfb2660f96acccad47467dda3f26bcfbdcb6f9a6f1df7a61098523103050a0e02343c76b5130883000090b0e204347797d7e9ea1aeb0b2e9fa0b1013232c3b4152596065737e85dee1e8ebec193d8c91c3d3d4ea2934454f67777c969da5a8adbabbe2fb000000000000000000000000000000000000000011242c3c", + "expected_error": null + }, + { + "id": "cose-sign1-ml-dsa-44-tampered-signature", + "operation": "verify_attached", + "algorithm": "ML-DSA-44", + "kid_hex": "96a37d942464e544464f0e6f136e918e166211325cf8e8656d3039d535855719", + "public_key_hex": "811dbbc20f9276252bff02f3dfc4b251f7af745bd086a766d20fc70057ef20da13b36d64d3a18cd9192e8d4f6bdeafe9c6ab5a2b0986ddd5702dc8a24d55f214d69879ef2397a47c1eb476ed2ba80d2b1b7fd6ae35715e2c304b9a276f322bf4335f82f5fa10772fa7e9c38cfa6626f5359b354939aff4d3ca14b45d58df3bef62424115ad7da67fd6c05548e4171293c4b910dafff66dd6c736704ca637d617891bc5d50a01c02544a5465268c2f23ca2e37eece192b8873cd360557c6d8b76299cd89bc9dd2e7bac1ac135fb854aff03e17303d8327b1040c3482296e85e7ac5b10222c2ffee4fca78bc39f9f452d3d3f705178eb3b90f61d6dbbe17649e1866ff8e19d58c31b3e09bb6dab331a1a3dcc56c4e74ce920310d6674fee45d6a0696543efaadac43c6f7be34483fd233a4a03d9f6d715fc3b977a582583c096b30754419e8a873669990d0a22cd70c244e928e0f9f2fa6a1a7fea339ef70603da5f14a51b1ae9353f547127494571dc5eae695b331727d7e7887ef8d832a2b75035575910b16acb1eb7f131eb5970c489a27cb8766461527ecda3086492b942d8043c02af229ee6b7652dca93b1275749cbf029db04d27d588d2bad0f5a03be3265c22bdb4b751d2e03683f9e59bed8d5f3c614b29128ddb12599b51e269cf031c7569119304e4ba659266e1a7b650d8a4a08d671d664db4c233b7b3eb59460de0c4d02476cd1e5e8a95afb85b535a091ff370e2116790dfc65b07b37da893024f81c65f058ed0243d8f6586154097c0e09042c3e53317917fb158ef8251740863ac27cb0d72a398c64dfb435d7ff08179e9498748b1d4370be59268a9a55054cc1adfcacde0525c40afb31bead4dc913f75e2f72d034a5b97e3a0b8fa2045bbcb08c9ceae32c5249a2341aeb3ce1c2685ebe95e0edae2595431ca18df145715e12757bf450ce181a0d369045b212157c4db1e0c15be0173ee92feb049660e441dec621c63cbf94184164e4cfc2e5c35c97bfbb66109074de2d041adbab11e4560177f3c5628a5111f403d2a21144230d2a58bcf0d9c8b7d7af675947161e0b8b4299daa1705e72b6ffeff41771b43fd0bb437f97685e6d7feb5cad282726d8b3ad134190c0a5807b596d97334f8ac41db21b0b6e8f1466b9aa3b0436175770facf488fc85a38cd06e03313ffb97f5a53efeb21ebdd2bf3227ace88cd25cc91f443138fca6fe9e72c27ae7df304e82dd6f99efabff03622e3d1a5ac7b1555acd51f0d50f6f4bd2a164051b2a28ce6180aa3787fa3adbaefee6221740c4ebee123e1a5d4efac67d32193eb265252188e94443eeb4e53b98c1990667b806d79c232610f624ea9c3bb695e88d4a90b77c467011bfc1c7613f2ab3648007c90ba51eeb37f6207551212f55b58c19eefd6a5857c3565e9dd5bac0ab46b2af63ccd2a920cad3c9bc9976983e1bf74fd9f8e41f612b13b12f11bf248b2807305b32ef474e7c55157390070f394851f1f495996300a229a237da21640c8afb3fad253186a8e48e507e9a0dce84060a3c4a6106565508c7fb773419557d4fe326af8223779e0ed97038d64b1b16dfd29da0d9a7ac949aead491db6be95a304b72ba82863b04f8be515df607d360b7326232ef7243b83358535bd06c5335bb123fb5ba6b33f329df0dd3c589834455b84f8bd93a1de18850a9335a23956c82132ea1487a3a7c670960351c929a2ea30d7944c9e1978d3b3ff81cf992fb5186bf6afa02bb22275429424f22baf8f9332061216eca61289defc920870fd98c39e39ed4bd807c3a155b6d4163b23ec22a7e277e8f4c176bd2ac97a8fd793e52faf7fe793262312", + "private_key_seed_hex": "449137f736f5f5a35eb9f37c1c88c2a0bcf18e757ffb9285ab2c4c26c15c55f1", + "payload_hex": "5265616c6c794d6520696e646570656e64656e74204d4c2d4453412d343420434f53455f5369676e3120766563746f72", + "cose_sign1_hex": "845827a201382f04582096a37d942464e544464f0e6f136e918e166211325cf8e8656d3039d535855719a058305265616c6c794d6520696e646570656e64656e74204d4c2d4453412d343420434f53455f5369676e3120766563746f72590974914cdc5f2d750922bc745f84a63f5889c12dd2f69d9e8558e23bd4e78373979537780ba99b913aecef30010bd236ef73ec060471ad2d87cf449b218f3a33cf39fcdbc23c515752fb551a9c6e9e4f515f1195f6699c9aa7f3593a5f0ff365f470d4eb8c3f254c2e08a4b029587b007f06c9f5a7f23ccfe4da0a558fe757c2ced57c6addc9d99a95609bfb07527472c191891a941e4c0f7a1ed68ab0094da19e024aee45e91c7cd09a8593404a90fa49239820656a2a92dee30b8ee940cd1eee176f7ef8453bbcf1b20eb51991469a06c2a0dab7106c4ea173da9440ba24ab1e22555508095aaabc790ae1762541729389b1a73abbb11e3834c6426e2bc75591c503b8250a0590ca4202e8af6a1259c93683d425b1746bd196031e3dd42599bfddb6e0bedfd9c1f84212aa8544d15e1c03fa5f4c18583d864a98f2767e8b14a14039869bf3261a45a5b6670bd16603e8039c160ba8c943a7a861fed88c92141ce0613e7edf550668077ecebb1a387ba406f2238989f60c2f4a3f2fa0d7fd7ea5118e757d31d0945b49af237b2de123f8e5ef14bace4dc45010a55bfe88ebba8be317b5f19a8dc636db7ad1ea3fa9adc53669eaa1b90e4a59325bc254b6664a645b911b29bf25a84d7e7d8a94a471d0199a3f51b6ffc4acb939cf1a737bf8ea15391c894feab30ee94282664c750f64788633042dcbc3fe12f0770333b5af06e508033e4fe3ea8917f07b355933ad4aebedb51c55fe135ee000e3220166163b236fb96714d2a2a6c580715f40f8000cd67a2579d54bd631240d59a9beb9a9a57d1ceef7c37ca1bc5d34fdfd51104bf23c7a89c5e97d3e1feba8c9fbbfc4f358c8238fbfe65d2d1273ef9fb51f638b340ea2fe4febd81c1daa907b879b9e65ff6d0df0a3c9a8cd22a16eb6e9c2492989730e3947fd8a69a2f082ef3c6b547804d102ef79f943eb58603834ab7da36786c7e765fde5e1f499b23c05e5087d4e29ac180a9c54a35a9737493325ab03b102261314fcb53423083a1ea3a54c2e96abf411f3d95462c2019aead356adcbf385d3c38588735d71ff83ca877b7d224d3c975a560351fa16cbae6eca867a9e5376a0e34c1ec7c3ce467c892bdda9e1d59af8b1eb304e96fa9c92ff70d2835933a8edf93b47e5664e58bcd45befd930e7d6d35ba86fb1b06e29e99bc0c3bc1abdb879fc7e0f2d5d7acee54dc6543a9e448b677a3510e768d16a18ad63a7722712799fc3055b18f2fff23beda28848147ccfeb054d32aff191ee055461af0d4eb816137ef9826ebdc9d1bb1354343b1f5893ccdef47d4f492906919d973f6c9d401009ebfdcc292cb6d083feaf474114185e6458e52f731e68399cc1986ee4142f139d2bb492936f361f4f4e0125abb300af11003bb08d3381d869ebdae73bad6a3f79b1831daca5dfc49ea488cf4dbcfec61a041963e2035dda9fc5cd07ca6e391e855d21f30cf360fe8fe506b521b7233227915efb0d9d6a84a519d26eb5d92b35d8e575e509d126e87f8515f520f3e416389f129600508d24e26272b08dec1b97a0d6361f29ef3c0f108a3b38208781d41a6ec15bf08e03e04041aa6b3259cea7334d86d1d9fa5e5264ed9ffd67f2ccc0e4f339e9c5688d93ca098374807aca487e52be58c656daf1a3a3214bddd18443ce9f4bca7d9b0f4aedabf23172e8fa7c8aaa6103fb8808f64f1d9a3fbf7437df320121daf1707c3ac58eec441819ce2529cb15398ce987e7e9fb84d8f821445ac69dc732b71a8294e19f5afdbc0f5e08d42ab8a42f2acf95dfb0e3ca4dc85766c654cac31e5ef9b2bfc6830d686ed8d55c7d787a30c455515bba0796cd4cb77e1e198947b0bf68c7b5939315361d5684d2ad01a9387824adce9a81ae3975a6b73c4f341590988a6527271ad7260d9506d7d0da9b2b7e6a9d6c30fb4b8726ffdce8ed15914dc8e785a875c7948f908480b353cd550efe7199dabc25219114f5ceb2c10d0f27134bbb66d77860960babac04cce966e71a2f09794959e24556a560371502477fa3edbb6364276fe5b7d88e20c96225dc824bc3720660c6f11777e905adadd2025b7c62de54e23e09011928b8a14b437dd0cf00f15ca5015e359ca24c0911ac83666043d7044d33441da96fe6faf0fe592b80d4f4d03d026b8867958289f4f66116e100811df35693e8483c5ba503827620e285742c7eaac485b2c075c587927d57d8e91ecae21eb0b88c71af192c8b1c0c6fca422524769fbbeacc3abe7bca7e9ed990d1c251cf5eeeb6ebeec08ed1ff9fbbaa6b5da9187729a0f68cf782f5e6733c8353511d0e03ce2ae9b10b9c093d430691f65e02aa39d4fc92df52fba52177cb7d65c430557ef8259ccd2654043a4c73728b04ab8b8cf907faa5566a229cf2d0d370bc3a319912340029dc21be1751cb7b254438a7c619da6e3f4c2ad36e6e6d0826176bd2975b9f13cef05e27461445a6c4e8e9654fe9f3249c260c6f68c7cdf60565cb82956e5b00912a82749ba39bddc46e27fba042f363e657d9b26b52f307635e2c4565b19fb3ac42079735839d1d283747e077a106fa13646640d1dfb53db4daff56e4dc1b276e90071a4a7d537ac7d9094b98f28580ad3a211f73ce2fcf770b662ebcba86688335f8a21e3ed78f2d4921cb38d69a7c25e3b2933dfbc90b543501077ec8d64b6fdc6b95597cbefac7b10c34a50dffea4acfeae1308360204a6607b2609d48fee7d4eb5be876f71c18ca77c08901363d120aba74cce485d9405bc51fad57c515707a11ddbbab554928be7003f7f3b68b2884110c256e136bd7ebe6896c7da834b9bd408a128d1e07213d13834426b11e1cca034166a1325ee6930a7b72db3251d8e9ddd11fc29dc8bf840312c59dba3cca36ea06d9e25c235cba4c27a47832cc31a54587121ab4933eae20f8ed330f62800c620eac8eb06af974fde0d06c99b25e502226087cb1acc7ccc4c577aeb90cc7f08e96a1ee15fdb47c016b22aad4da1faf428a685a09f04f8a5182cc6db022704e6478dbe2e526443e857400ff860d3bd99d7c43b82af9ec21c2004eb46a0cfcbdb69c54ec10861984da6bc7001751b3ca77d0391068b2586f2fdbe67a538d153fae8062c15e590c8e342fc0f74f6165aad336e223d96a77dce03b49cbdf197543e69f0f815cfa6734eba257eb16fd73a3a9509a553528a0a413d18155d0cded1c8a1a7786c1ad3b2734241fdaeb49a4b6efab98d0cfe644b9cfb2660f96acccad47467dda3f26bcfbdcb6f9a6f1df7a61098523103050a0e02343c76b5130883000090b0e204347797d7e9ea1aeb0b2e9fa0b1013232c3b4152596065737e85dee1e8ebec193d8c91c3d3d4ea2934454f67777c969da5a8adbabbe2fb000000000000000000000000000000000000000011242c3d", + "expected_error": "InvalidSignature" + }, + { + "id": "cose-sign1-ml-dsa-44-wrong-payload", + "operation": "verify_attached", + "algorithm": "ML-DSA-44", + "kid_hex": "96a37d942464e544464f0e6f136e918e166211325cf8e8656d3039d535855719", + "public_key_hex": "811dbbc20f9276252bff02f3dfc4b251f7af745bd086a766d20fc70057ef20da13b36d64d3a18cd9192e8d4f6bdeafe9c6ab5a2b0986ddd5702dc8a24d55f214d69879ef2397a47c1eb476ed2ba80d2b1b7fd6ae35715e2c304b9a276f322bf4335f82f5fa10772fa7e9c38cfa6626f5359b354939aff4d3ca14b45d58df3bef62424115ad7da67fd6c05548e4171293c4b910dafff66dd6c736704ca637d617891bc5d50a01c02544a5465268c2f23ca2e37eece192b8873cd360557c6d8b76299cd89bc9dd2e7bac1ac135fb854aff03e17303d8327b1040c3482296e85e7ac5b10222c2ffee4fca78bc39f9f452d3d3f705178eb3b90f61d6dbbe17649e1866ff8e19d58c31b3e09bb6dab331a1a3dcc56c4e74ce920310d6674fee45d6a0696543efaadac43c6f7be34483fd233a4a03d9f6d715fc3b977a582583c096b30754419e8a873669990d0a22cd70c244e928e0f9f2fa6a1a7fea339ef70603da5f14a51b1ae9353f547127494571dc5eae695b331727d7e7887ef8d832a2b75035575910b16acb1eb7f131eb5970c489a27cb8766461527ecda3086492b942d8043c02af229ee6b7652dca93b1275749cbf029db04d27d588d2bad0f5a03be3265c22bdb4b751d2e03683f9e59bed8d5f3c614b29128ddb12599b51e269cf031c7569119304e4ba659266e1a7b650d8a4a08d671d664db4c233b7b3eb59460de0c4d02476cd1e5e8a95afb85b535a091ff370e2116790dfc65b07b37da893024f81c65f058ed0243d8f6586154097c0e09042c3e53317917fb158ef8251740863ac27cb0d72a398c64dfb435d7ff08179e9498748b1d4370be59268a9a55054cc1adfcacde0525c40afb31bead4dc913f75e2f72d034a5b97e3a0b8fa2045bbcb08c9ceae32c5249a2341aeb3ce1c2685ebe95e0edae2595431ca18df145715e12757bf450ce181a0d369045b212157c4db1e0c15be0173ee92feb049660e441dec621c63cbf94184164e4cfc2e5c35c97bfbb66109074de2d041adbab11e4560177f3c5628a5111f403d2a21144230d2a58bcf0d9c8b7d7af675947161e0b8b4299daa1705e72b6ffeff41771b43fd0bb437f97685e6d7feb5cad282726d8b3ad134190c0a5807b596d97334f8ac41db21b0b6e8f1466b9aa3b0436175770facf488fc85a38cd06e03313ffb97f5a53efeb21ebdd2bf3227ace88cd25cc91f443138fca6fe9e72c27ae7df304e82dd6f99efabff03622e3d1a5ac7b1555acd51f0d50f6f4bd2a164051b2a28ce6180aa3787fa3adbaefee6221740c4ebee123e1a5d4efac67d32193eb265252188e94443eeb4e53b98c1990667b806d79c232610f624ea9c3bb695e88d4a90b77c467011bfc1c7613f2ab3648007c90ba51eeb37f6207551212f55b58c19eefd6a5857c3565e9dd5bac0ab46b2af63ccd2a920cad3c9bc9976983e1bf74fd9f8e41f612b13b12f11bf248b2807305b32ef474e7c55157390070f394851f1f495996300a229a237da21640c8afb3fad253186a8e48e507e9a0dce84060a3c4a6106565508c7fb773419557d4fe326af8223779e0ed97038d64b1b16dfd29da0d9a7ac949aead491db6be95a304b72ba82863b04f8be515df607d360b7326232ef7243b83358535bd06c5335bb123fb5ba6b33f329df0dd3c589834455b84f8bd93a1de18850a9335a23956c82132ea1487a3a7c670960351c929a2ea30d7944c9e1978d3b3ff81cf992fb5186bf6afa02bb22275429424f22baf8f9332061216eca61289defc920870fd98c39e39ed4bd807c3a155b6d4163b23ec22a7e277e8f4c176bd2ac97a8fd793e52faf7fe793262312", + "private_key_seed_hex": "449137f736f5f5a35eb9f37c1c88c2a0bcf18e757ffb9285ab2c4c26c15c55f1", + "payload_hex": "5365616c6c794d6520696e646570656e64656e74204d4c2d4453412d343420434f53455f5369676e3120766563746f72", + "cose_sign1_hex": "845827a201382f04582096a37d942464e544464f0e6f136e918e166211325cf8e8656d3039d535855719a058305365616c6c794d6520696e646570656e64656e74204d4c2d4453412d343420434f53455f5369676e3120766563746f72590974914cdc5f2d750922bc745f84a63f5889c12dd2f69d9e8558e23bd4e78373979537780ba99b913aecef30010bd236ef73ec060471ad2d87cf449b218f3a33cf39fcdbc23c515752fb551a9c6e9e4f515f1195f6699c9aa7f3593a5f0ff365f470d4eb8c3f254c2e08a4b029587b007f06c9f5a7f23ccfe4da0a558fe757c2ced57c6addc9d99a95609bfb07527472c191891a941e4c0f7a1ed68ab0094da19e024aee45e91c7cd09a8593404a90fa49239820656a2a92dee30b8ee940cd1eee176f7ef8453bbcf1b20eb51991469a06c2a0dab7106c4ea173da9440ba24ab1e22555508095aaabc790ae1762541729389b1a73abbb11e3834c6426e2bc75591c503b8250a0590ca4202e8af6a1259c93683d425b1746bd196031e3dd42599bfddb6e0bedfd9c1f84212aa8544d15e1c03fa5f4c18583d864a98f2767e8b14a14039869bf3261a45a5b6670bd16603e8039c160ba8c943a7a861fed88c92141ce0613e7edf550668077ecebb1a387ba406f2238989f60c2f4a3f2fa0d7fd7ea5118e757d31d0945b49af237b2de123f8e5ef14bace4dc45010a55bfe88ebba8be317b5f19a8dc636db7ad1ea3fa9adc53669eaa1b90e4a59325bc254b6664a645b911b29bf25a84d7e7d8a94a471d0199a3f51b6ffc4acb939cf1a737bf8ea15391c894feab30ee94282664c750f64788633042dcbc3fe12f0770333b5af06e508033e4fe3ea8917f07b355933ad4aebedb51c55fe135ee000e3220166163b236fb96714d2a2a6c580715f40f8000cd67a2579d54bd631240d59a9beb9a9a57d1ceef7c37ca1bc5d34fdfd51104bf23c7a89c5e97d3e1feba8c9fbbfc4f358c8238fbfe65d2d1273ef9fb51f638b340ea2fe4febd81c1daa907b879b9e65ff6d0df0a3c9a8cd22a16eb6e9c2492989730e3947fd8a69a2f082ef3c6b547804d102ef79f943eb58603834ab7da36786c7e765fde5e1f499b23c05e5087d4e29ac180a9c54a35a9737493325ab03b102261314fcb53423083a1ea3a54c2e96abf411f3d95462c2019aead356adcbf385d3c38588735d71ff83ca877b7d224d3c975a560351fa16cbae6eca867a9e5376a0e34c1ec7c3ce467c892bdda9e1d59af8b1eb304e96fa9c92ff70d2835933a8edf93b47e5664e58bcd45befd930e7d6d35ba86fb1b06e29e99bc0c3bc1abdb879fc7e0f2d5d7acee54dc6543a9e448b677a3510e768d16a18ad63a7722712799fc3055b18f2fff23beda28848147ccfeb054d32aff191ee055461af0d4eb816137ef9826ebdc9d1bb1354343b1f5893ccdef47d4f492906919d973f6c9d401009ebfdcc292cb6d083feaf474114185e6458e52f731e68399cc1986ee4142f139d2bb492936f361f4f4e0125abb300af11003bb08d3381d869ebdae73bad6a3f79b1831daca5dfc49ea488cf4dbcfec61a041963e2035dda9fc5cd07ca6e391e855d21f30cf360fe8fe506b521b7233227915efb0d9d6a84a519d26eb5d92b35d8e575e509d126e87f8515f520f3e416389f129600508d24e26272b08dec1b97a0d6361f29ef3c0f108a3b38208781d41a6ec15bf08e03e04041aa6b3259cea7334d86d1d9fa5e5264ed9ffd67f2ccc0e4f339e9c5688d93ca098374807aca487e52be58c656daf1a3a3214bddd18443ce9f4bca7d9b0f4aedabf23172e8fa7c8aaa6103fb8808f64f1d9a3fbf7437df320121daf1707c3ac58eec441819ce2529cb15398ce987e7e9fb84d8f821445ac69dc732b71a8294e19f5afdbc0f5e08d42ab8a42f2acf95dfb0e3ca4dc85766c654cac31e5ef9b2bfc6830d686ed8d55c7d787a30c455515bba0796cd4cb77e1e198947b0bf68c7b5939315361d5684d2ad01a9387824adce9a81ae3975a6b73c4f341590988a6527271ad7260d9506d7d0da9b2b7e6a9d6c30fb4b8726ffdce8ed15914dc8e785a875c7948f908480b353cd550efe7199dabc25219114f5ceb2c10d0f27134bbb66d77860960babac04cce966e71a2f09794959e24556a560371502477fa3edbb6364276fe5b7d88e20c96225dc824bc3720660c6f11777e905adadd2025b7c62de54e23e09011928b8a14b437dd0cf00f15ca5015e359ca24c0911ac83666043d7044d33441da96fe6faf0fe592b80d4f4d03d026b8867958289f4f66116e100811df35693e8483c5ba503827620e285742c7eaac485b2c075c587927d57d8e91ecae21eb0b88c71af192c8b1c0c6fca422524769fbbeacc3abe7bca7e9ed990d1c251cf5eeeb6ebeec08ed1ff9fbbaa6b5da9187729a0f68cf782f5e6733c8353511d0e03ce2ae9b10b9c093d430691f65e02aa39d4fc92df52fba52177cb7d65c430557ef8259ccd2654043a4c73728b04ab8b8cf907faa5566a229cf2d0d370bc3a319912340029dc21be1751cb7b254438a7c619da6e3f4c2ad36e6e6d0826176bd2975b9f13cef05e27461445a6c4e8e9654fe9f3249c260c6f68c7cdf60565cb82956e5b00912a82749ba39bddc46e27fba042f363e657d9b26b52f307635e2c4565b19fb3ac42079735839d1d283747e077a106fa13646640d1dfb53db4daff56e4dc1b276e90071a4a7d537ac7d9094b98f28580ad3a211f73ce2fcf770b662ebcba86688335f8a21e3ed78f2d4921cb38d69a7c25e3b2933dfbc90b543501077ec8d64b6fdc6b95597cbefac7b10c34a50dffea4acfeae1308360204a6607b2609d48fee7d4eb5be876f71c18ca77c08901363d120aba74cce485d9405bc51fad57c515707a11ddbbab554928be7003f7f3b68b2884110c256e136bd7ebe6896c7da834b9bd408a128d1e07213d13834426b11e1cca034166a1325ee6930a7b72db3251d8e9ddd11fc29dc8bf840312c59dba3cca36ea06d9e25c235cba4c27a47832cc31a54587121ab4933eae20f8ed330f62800c620eac8eb06af974fde0d06c99b25e502226087cb1acc7ccc4c577aeb90cc7f08e96a1ee15fdb47c016b22aad4da1faf428a685a09f04f8a5182cc6db022704e6478dbe2e526443e857400ff860d3bd99d7c43b82af9ec21c2004eb46a0cfcbdb69c54ec10861984da6bc7001751b3ca77d0391068b2586f2fdbe67a538d153fae8062c15e590c8e342fc0f74f6165aad336e223d96a77dce03b49cbdf197543e69f0f815cfa6734eba257eb16fd73a3a9509a553528a0a413d18155d0cded1c8a1a7786c1ad3b2734241fdaeb49a4b6efab98d0cfe644b9cfb2660f96acccad47467dda3f26bcfbdcb6f9a6f1df7a61098523103050a0e02343c76b5130883000090b0e204347797d7e9ea1aeb0b2e9fa0b1013232c3b4152596065737e85dee1e8ebec193d8c91c3d3d4ea2934454f67777c969da5a8adbabbe2fb000000000000000000000000000000000000000011242c3c", + "expected_error": "InvalidSignature" + }, + { + "id": "cose-sign1-ml-dsa-44-truncated-signature", + "operation": "verify_attached", + "algorithm": "ML-DSA-44", + "kid_hex": "96a37d942464e544464f0e6f136e918e166211325cf8e8656d3039d535855719", + "public_key_hex": "811dbbc20f9276252bff02f3dfc4b251f7af745bd086a766d20fc70057ef20da13b36d64d3a18cd9192e8d4f6bdeafe9c6ab5a2b0986ddd5702dc8a24d55f214d69879ef2397a47c1eb476ed2ba80d2b1b7fd6ae35715e2c304b9a276f322bf4335f82f5fa10772fa7e9c38cfa6626f5359b354939aff4d3ca14b45d58df3bef62424115ad7da67fd6c05548e4171293c4b910dafff66dd6c736704ca637d617891bc5d50a01c02544a5465268c2f23ca2e37eece192b8873cd360557c6d8b76299cd89bc9dd2e7bac1ac135fb854aff03e17303d8327b1040c3482296e85e7ac5b10222c2ffee4fca78bc39f9f452d3d3f705178eb3b90f61d6dbbe17649e1866ff8e19d58c31b3e09bb6dab331a1a3dcc56c4e74ce920310d6674fee45d6a0696543efaadac43c6f7be34483fd233a4a03d9f6d715fc3b977a582583c096b30754419e8a873669990d0a22cd70c244e928e0f9f2fa6a1a7fea339ef70603da5f14a51b1ae9353f547127494571dc5eae695b331727d7e7887ef8d832a2b75035575910b16acb1eb7f131eb5970c489a27cb8766461527ecda3086492b942d8043c02af229ee6b7652dca93b1275749cbf029db04d27d588d2bad0f5a03be3265c22bdb4b751d2e03683f9e59bed8d5f3c614b29128ddb12599b51e269cf031c7569119304e4ba659266e1a7b650d8a4a08d671d664db4c233b7b3eb59460de0c4d02476cd1e5e8a95afb85b535a091ff370e2116790dfc65b07b37da893024f81c65f058ed0243d8f6586154097c0e09042c3e53317917fb158ef8251740863ac27cb0d72a398c64dfb435d7ff08179e9498748b1d4370be59268a9a55054cc1adfcacde0525c40afb31bead4dc913f75e2f72d034a5b97e3a0b8fa2045bbcb08c9ceae32c5249a2341aeb3ce1c2685ebe95e0edae2595431ca18df145715e12757bf450ce181a0d369045b212157c4db1e0c15be0173ee92feb049660e441dec621c63cbf94184164e4cfc2e5c35c97bfbb66109074de2d041adbab11e4560177f3c5628a5111f403d2a21144230d2a58bcf0d9c8b7d7af675947161e0b8b4299daa1705e72b6ffeff41771b43fd0bb437f97685e6d7feb5cad282726d8b3ad134190c0a5807b596d97334f8ac41db21b0b6e8f1466b9aa3b0436175770facf488fc85a38cd06e03313ffb97f5a53efeb21ebdd2bf3227ace88cd25cc91f443138fca6fe9e72c27ae7df304e82dd6f99efabff03622e3d1a5ac7b1555acd51f0d50f6f4bd2a164051b2a28ce6180aa3787fa3adbaefee6221740c4ebee123e1a5d4efac67d32193eb265252188e94443eeb4e53b98c1990667b806d79c232610f624ea9c3bb695e88d4a90b77c467011bfc1c7613f2ab3648007c90ba51eeb37f6207551212f55b58c19eefd6a5857c3565e9dd5bac0ab46b2af63ccd2a920cad3c9bc9976983e1bf74fd9f8e41f612b13b12f11bf248b2807305b32ef474e7c55157390070f394851f1f495996300a229a237da21640c8afb3fad253186a8e48e507e9a0dce84060a3c4a6106565508c7fb773419557d4fe326af8223779e0ed97038d64b1b16dfd29da0d9a7ac949aead491db6be95a304b72ba82863b04f8be515df607d360b7326232ef7243b83358535bd06c5335bb123fb5ba6b33f329df0dd3c589834455b84f8bd93a1de18850a9335a23956c82132ea1487a3a7c670960351c929a2ea30d7944c9e1978d3b3ff81cf992fb5186bf6afa02bb22275429424f22baf8f9332061216eca61289defc920870fd98c39e39ed4bd807c3a155b6d4163b23ec22a7e277e8f4c176bd2ac97a8fd793e52faf7fe793262312", + "private_key_seed_hex": "449137f736f5f5a35eb9f37c1c88c2a0bcf18e757ffb9285ab2c4c26c15c55f1", + "payload_hex": "5265616c6c794d6520696e646570656e64656e74204d4c2d4453412d343420434f53455f5369676e3120766563746f72", + "cose_sign1_hex": "845827a201382f04582096a37d942464e544464f0e6f136e918e166211325cf8e8656d3039d535855719a058305265616c6c794d6520696e646570656e64656e74204d4c2d4453412d343420434f53455f5369676e3120766563746f72590973914cdc5f2d750922bc745f84a63f5889c12dd2f69d9e8558e23bd4e78373979537780ba99b913aecef30010bd236ef73ec060471ad2d87cf449b218f3a33cf39fcdbc23c515752fb551a9c6e9e4f515f1195f6699c9aa7f3593a5f0ff365f470d4eb8c3f254c2e08a4b029587b007f06c9f5a7f23ccfe4da0a558fe757c2ced57c6addc9d99a95609bfb07527472c191891a941e4c0f7a1ed68ab0094da19e024aee45e91c7cd09a8593404a90fa49239820656a2a92dee30b8ee940cd1eee176f7ef8453bbcf1b20eb51991469a06c2a0dab7106c4ea173da9440ba24ab1e22555508095aaabc790ae1762541729389b1a73abbb11e3834c6426e2bc75591c503b8250a0590ca4202e8af6a1259c93683d425b1746bd196031e3dd42599bfddb6e0bedfd9c1f84212aa8544d15e1c03fa5f4c18583d864a98f2767e8b14a14039869bf3261a45a5b6670bd16603e8039c160ba8c943a7a861fed88c92141ce0613e7edf550668077ecebb1a387ba406f2238989f60c2f4a3f2fa0d7fd7ea5118e757d31d0945b49af237b2de123f8e5ef14bace4dc45010a55bfe88ebba8be317b5f19a8dc636db7ad1ea3fa9adc53669eaa1b90e4a59325bc254b6664a645b911b29bf25a84d7e7d8a94a471d0199a3f51b6ffc4acb939cf1a737bf8ea15391c894feab30ee94282664c750f64788633042dcbc3fe12f0770333b5af06e508033e4fe3ea8917f07b355933ad4aebedb51c55fe135ee000e3220166163b236fb96714d2a2a6c580715f40f8000cd67a2579d54bd631240d59a9beb9a9a57d1ceef7c37ca1bc5d34fdfd51104bf23c7a89c5e97d3e1feba8c9fbbfc4f358c8238fbfe65d2d1273ef9fb51f638b340ea2fe4febd81c1daa907b879b9e65ff6d0df0a3c9a8cd22a16eb6e9c2492989730e3947fd8a69a2f082ef3c6b547804d102ef79f943eb58603834ab7da36786c7e765fde5e1f499b23c05e5087d4e29ac180a9c54a35a9737493325ab03b102261314fcb53423083a1ea3a54c2e96abf411f3d95462c2019aead356adcbf385d3c38588735d71ff83ca877b7d224d3c975a560351fa16cbae6eca867a9e5376a0e34c1ec7c3ce467c892bdda9e1d59af8b1eb304e96fa9c92ff70d2835933a8edf93b47e5664e58bcd45befd930e7d6d35ba86fb1b06e29e99bc0c3bc1abdb879fc7e0f2d5d7acee54dc6543a9e448b677a3510e768d16a18ad63a7722712799fc3055b18f2fff23beda28848147ccfeb054d32aff191ee055461af0d4eb816137ef9826ebdc9d1bb1354343b1f5893ccdef47d4f492906919d973f6c9d401009ebfdcc292cb6d083feaf474114185e6458e52f731e68399cc1986ee4142f139d2bb492936f361f4f4e0125abb300af11003bb08d3381d869ebdae73bad6a3f79b1831daca5dfc49ea488cf4dbcfec61a041963e2035dda9fc5cd07ca6e391e855d21f30cf360fe8fe506b521b7233227915efb0d9d6a84a519d26eb5d92b35d8e575e509d126e87f8515f520f3e416389f129600508d24e26272b08dec1b97a0d6361f29ef3c0f108a3b38208781d41a6ec15bf08e03e04041aa6b3259cea7334d86d1d9fa5e5264ed9ffd67f2ccc0e4f339e9c5688d93ca098374807aca487e52be58c656daf1a3a3214bddd18443ce9f4bca7d9b0f4aedabf23172e8fa7c8aaa6103fb8808f64f1d9a3fbf7437df320121daf1707c3ac58eec441819ce2529cb15398ce987e7e9fb84d8f821445ac69dc732b71a8294e19f5afdbc0f5e08d42ab8a42f2acf95dfb0e3ca4dc85766c654cac31e5ef9b2bfc6830d686ed8d55c7d787a30c455515bba0796cd4cb77e1e198947b0bf68c7b5939315361d5684d2ad01a9387824adce9a81ae3975a6b73c4f341590988a6527271ad7260d9506d7d0da9b2b7e6a9d6c30fb4b8726ffdce8ed15914dc8e785a875c7948f908480b353cd550efe7199dabc25219114f5ceb2c10d0f27134bbb66d77860960babac04cce966e71a2f09794959e24556a560371502477fa3edbb6364276fe5b7d88e20c96225dc824bc3720660c6f11777e905adadd2025b7c62de54e23e09011928b8a14b437dd0cf00f15ca5015e359ca24c0911ac83666043d7044d33441da96fe6faf0fe592b80d4f4d03d026b8867958289f4f66116e100811df35693e8483c5ba503827620e285742c7eaac485b2c075c587927d57d8e91ecae21eb0b88c71af192c8b1c0c6fca422524769fbbeacc3abe7bca7e9ed990d1c251cf5eeeb6ebeec08ed1ff9fbbaa6b5da9187729a0f68cf782f5e6733c8353511d0e03ce2ae9b10b9c093d430691f65e02aa39d4fc92df52fba52177cb7d65c430557ef8259ccd2654043a4c73728b04ab8b8cf907faa5566a229cf2d0d370bc3a319912340029dc21be1751cb7b254438a7c619da6e3f4c2ad36e6e6d0826176bd2975b9f13cef05e27461445a6c4e8e9654fe9f3249c260c6f68c7cdf60565cb82956e5b00912a82749ba39bddc46e27fba042f363e657d9b26b52f307635e2c4565b19fb3ac42079735839d1d283747e077a106fa13646640d1dfb53db4daff56e4dc1b276e90071a4a7d537ac7d9094b98f28580ad3a211f73ce2fcf770b662ebcba86688335f8a21e3ed78f2d4921cb38d69a7c25e3b2933dfbc90b543501077ec8d64b6fdc6b95597cbefac7b10c34a50dffea4acfeae1308360204a6607b2609d48fee7d4eb5be876f71c18ca77c08901363d120aba74cce485d9405bc51fad57c515707a11ddbbab554928be7003f7f3b68b2884110c256e136bd7ebe6896c7da834b9bd408a128d1e07213d13834426b11e1cca034166a1325ee6930a7b72db3251d8e9ddd11fc29dc8bf840312c59dba3cca36ea06d9e25c235cba4c27a47832cc31a54587121ab4933eae20f8ed330f62800c620eac8eb06af974fde0d06c99b25e502226087cb1acc7ccc4c577aeb90cc7f08e96a1ee15fdb47c016b22aad4da1faf428a685a09f04f8a5182cc6db022704e6478dbe2e526443e857400ff860d3bd99d7c43b82af9ec21c2004eb46a0cfcbdb69c54ec10861984da6bc7001751b3ca77d0391068b2586f2fdbe67a538d153fae8062c15e590c8e342fc0f74f6165aad336e223d96a77dce03b49cbdf197543e69f0f815cfa6734eba257eb16fd73a3a9509a553528a0a413d18155d0cded1c8a1a7786c1ad3b2734241fdaeb49a4b6efab98d0cfe644b9cfb2660f96acccad47467dda3f26bcfbdcb6f9a6f1df7a61098523103050a0e02343c76b5130883000090b0e204347797d7e9ea1aeb0b2e9fa0b1013232c3b4152596065737e85dee1e8ebec193d8c91c3d3d4ea2934454f67777c969da5a8adbabbe2fb000000000000000000000000000000000000000011242c", + "expected_error": "InvalidSignatureEncoding" + }, + { + "id": "cose-sign1-ml-dsa-44-extended-signature", + "operation": "verify_attached", + "algorithm": "ML-DSA-44", + "kid_hex": "96a37d942464e544464f0e6f136e918e166211325cf8e8656d3039d535855719", + "public_key_hex": "811dbbc20f9276252bff02f3dfc4b251f7af745bd086a766d20fc70057ef20da13b36d64d3a18cd9192e8d4f6bdeafe9c6ab5a2b0986ddd5702dc8a24d55f214d69879ef2397a47c1eb476ed2ba80d2b1b7fd6ae35715e2c304b9a276f322bf4335f82f5fa10772fa7e9c38cfa6626f5359b354939aff4d3ca14b45d58df3bef62424115ad7da67fd6c05548e4171293c4b910dafff66dd6c736704ca637d617891bc5d50a01c02544a5465268c2f23ca2e37eece192b8873cd360557c6d8b76299cd89bc9dd2e7bac1ac135fb854aff03e17303d8327b1040c3482296e85e7ac5b10222c2ffee4fca78bc39f9f452d3d3f705178eb3b90f61d6dbbe17649e1866ff8e19d58c31b3e09bb6dab331a1a3dcc56c4e74ce920310d6674fee45d6a0696543efaadac43c6f7be34483fd233a4a03d9f6d715fc3b977a582583c096b30754419e8a873669990d0a22cd70c244e928e0f9f2fa6a1a7fea339ef70603da5f14a51b1ae9353f547127494571dc5eae695b331727d7e7887ef8d832a2b75035575910b16acb1eb7f131eb5970c489a27cb8766461527ecda3086492b942d8043c02af229ee6b7652dca93b1275749cbf029db04d27d588d2bad0f5a03be3265c22bdb4b751d2e03683f9e59bed8d5f3c614b29128ddb12599b51e269cf031c7569119304e4ba659266e1a7b650d8a4a08d671d664db4c233b7b3eb59460de0c4d02476cd1e5e8a95afb85b535a091ff370e2116790dfc65b07b37da893024f81c65f058ed0243d8f6586154097c0e09042c3e53317917fb158ef8251740863ac27cb0d72a398c64dfb435d7ff08179e9498748b1d4370be59268a9a55054cc1adfcacde0525c40afb31bead4dc913f75e2f72d034a5b97e3a0b8fa2045bbcb08c9ceae32c5249a2341aeb3ce1c2685ebe95e0edae2595431ca18df145715e12757bf450ce181a0d369045b212157c4db1e0c15be0173ee92feb049660e441dec621c63cbf94184164e4cfc2e5c35c97bfbb66109074de2d041adbab11e4560177f3c5628a5111f403d2a21144230d2a58bcf0d9c8b7d7af675947161e0b8b4299daa1705e72b6ffeff41771b43fd0bb437f97685e6d7feb5cad282726d8b3ad134190c0a5807b596d97334f8ac41db21b0b6e8f1466b9aa3b0436175770facf488fc85a38cd06e03313ffb97f5a53efeb21ebdd2bf3227ace88cd25cc91f443138fca6fe9e72c27ae7df304e82dd6f99efabff03622e3d1a5ac7b1555acd51f0d50f6f4bd2a164051b2a28ce6180aa3787fa3adbaefee6221740c4ebee123e1a5d4efac67d32193eb265252188e94443eeb4e53b98c1990667b806d79c232610f624ea9c3bb695e88d4a90b77c467011bfc1c7613f2ab3648007c90ba51eeb37f6207551212f55b58c19eefd6a5857c3565e9dd5bac0ab46b2af63ccd2a920cad3c9bc9976983e1bf74fd9f8e41f612b13b12f11bf248b2807305b32ef474e7c55157390070f394851f1f495996300a229a237da21640c8afb3fad253186a8e48e507e9a0dce84060a3c4a6106565508c7fb773419557d4fe326af8223779e0ed97038d64b1b16dfd29da0d9a7ac949aead491db6be95a304b72ba82863b04f8be515df607d360b7326232ef7243b83358535bd06c5335bb123fb5ba6b33f329df0dd3c589834455b84f8bd93a1de18850a9335a23956c82132ea1487a3a7c670960351c929a2ea30d7944c9e1978d3b3ff81cf992fb5186bf6afa02bb22275429424f22baf8f9332061216eca61289defc920870fd98c39e39ed4bd807c3a155b6d4163b23ec22a7e277e8f4c176bd2ac97a8fd793e52faf7fe793262312", + "private_key_seed_hex": "449137f736f5f5a35eb9f37c1c88c2a0bcf18e757ffb9285ab2c4c26c15c55f1", + "payload_hex": "5265616c6c794d6520696e646570656e64656e74204d4c2d4453412d343420434f53455f5369676e3120766563746f72", + "cose_sign1_hex": "845827a201382f04582096a37d942464e544464f0e6f136e918e166211325cf8e8656d3039d535855719a058305265616c6c794d6520696e646570656e64656e74204d4c2d4453412d343420434f53455f5369676e3120766563746f72590975914cdc5f2d750922bc745f84a63f5889c12dd2f69d9e8558e23bd4e78373979537780ba99b913aecef30010bd236ef73ec060471ad2d87cf449b218f3a33cf39fcdbc23c515752fb551a9c6e9e4f515f1195f6699c9aa7f3593a5f0ff365f470d4eb8c3f254c2e08a4b029587b007f06c9f5a7f23ccfe4da0a558fe757c2ced57c6addc9d99a95609bfb07527472c191891a941e4c0f7a1ed68ab0094da19e024aee45e91c7cd09a8593404a90fa49239820656a2a92dee30b8ee940cd1eee176f7ef8453bbcf1b20eb51991469a06c2a0dab7106c4ea173da9440ba24ab1e22555508095aaabc790ae1762541729389b1a73abbb11e3834c6426e2bc75591c503b8250a0590ca4202e8af6a1259c93683d425b1746bd196031e3dd42599bfddb6e0bedfd9c1f84212aa8544d15e1c03fa5f4c18583d864a98f2767e8b14a14039869bf3261a45a5b6670bd16603e8039c160ba8c943a7a861fed88c92141ce0613e7edf550668077ecebb1a387ba406f2238989f60c2f4a3f2fa0d7fd7ea5118e757d31d0945b49af237b2de123f8e5ef14bace4dc45010a55bfe88ebba8be317b5f19a8dc636db7ad1ea3fa9adc53669eaa1b90e4a59325bc254b6664a645b911b29bf25a84d7e7d8a94a471d0199a3f51b6ffc4acb939cf1a737bf8ea15391c894feab30ee94282664c750f64788633042dcbc3fe12f0770333b5af06e508033e4fe3ea8917f07b355933ad4aebedb51c55fe135ee000e3220166163b236fb96714d2a2a6c580715f40f8000cd67a2579d54bd631240d59a9beb9a9a57d1ceef7c37ca1bc5d34fdfd51104bf23c7a89c5e97d3e1feba8c9fbbfc4f358c8238fbfe65d2d1273ef9fb51f638b340ea2fe4febd81c1daa907b879b9e65ff6d0df0a3c9a8cd22a16eb6e9c2492989730e3947fd8a69a2f082ef3c6b547804d102ef79f943eb58603834ab7da36786c7e765fde5e1f499b23c05e5087d4e29ac180a9c54a35a9737493325ab03b102261314fcb53423083a1ea3a54c2e96abf411f3d95462c2019aead356adcbf385d3c38588735d71ff83ca877b7d224d3c975a560351fa16cbae6eca867a9e5376a0e34c1ec7c3ce467c892bdda9e1d59af8b1eb304e96fa9c92ff70d2835933a8edf93b47e5664e58bcd45befd930e7d6d35ba86fb1b06e29e99bc0c3bc1abdb879fc7e0f2d5d7acee54dc6543a9e448b677a3510e768d16a18ad63a7722712799fc3055b18f2fff23beda28848147ccfeb054d32aff191ee055461af0d4eb816137ef9826ebdc9d1bb1354343b1f5893ccdef47d4f492906919d973f6c9d401009ebfdcc292cb6d083feaf474114185e6458e52f731e68399cc1986ee4142f139d2bb492936f361f4f4e0125abb300af11003bb08d3381d869ebdae73bad6a3f79b1831daca5dfc49ea488cf4dbcfec61a041963e2035dda9fc5cd07ca6e391e855d21f30cf360fe8fe506b521b7233227915efb0d9d6a84a519d26eb5d92b35d8e575e509d126e87f8515f520f3e416389f129600508d24e26272b08dec1b97a0d6361f29ef3c0f108a3b38208781d41a6ec15bf08e03e04041aa6b3259cea7334d86d1d9fa5e5264ed9ffd67f2ccc0e4f339e9c5688d93ca098374807aca487e52be58c656daf1a3a3214bddd18443ce9f4bca7d9b0f4aedabf23172e8fa7c8aaa6103fb8808f64f1d9a3fbf7437df320121daf1707c3ac58eec441819ce2529cb15398ce987e7e9fb84d8f821445ac69dc732b71a8294e19f5afdbc0f5e08d42ab8a42f2acf95dfb0e3ca4dc85766c654cac31e5ef9b2bfc6830d686ed8d55c7d787a30c455515bba0796cd4cb77e1e198947b0bf68c7b5939315361d5684d2ad01a9387824adce9a81ae3975a6b73c4f341590988a6527271ad7260d9506d7d0da9b2b7e6a9d6c30fb4b8726ffdce8ed15914dc8e785a875c7948f908480b353cd550efe7199dabc25219114f5ceb2c10d0f27134bbb66d77860960babac04cce966e71a2f09794959e24556a560371502477fa3edbb6364276fe5b7d88e20c96225dc824bc3720660c6f11777e905adadd2025b7c62de54e23e09011928b8a14b437dd0cf00f15ca5015e359ca24c0911ac83666043d7044d33441da96fe6faf0fe592b80d4f4d03d026b8867958289f4f66116e100811df35693e8483c5ba503827620e285742c7eaac485b2c075c587927d57d8e91ecae21eb0b88c71af192c8b1c0c6fca422524769fbbeacc3abe7bca7e9ed990d1c251cf5eeeb6ebeec08ed1ff9fbbaa6b5da9187729a0f68cf782f5e6733c8353511d0e03ce2ae9b10b9c093d430691f65e02aa39d4fc92df52fba52177cb7d65c430557ef8259ccd2654043a4c73728b04ab8b8cf907faa5566a229cf2d0d370bc3a319912340029dc21be1751cb7b254438a7c619da6e3f4c2ad36e6e6d0826176bd2975b9f13cef05e27461445a6c4e8e9654fe9f3249c260c6f68c7cdf60565cb82956e5b00912a82749ba39bddc46e27fba042f363e657d9b26b52f307635e2c4565b19fb3ac42079735839d1d283747e077a106fa13646640d1dfb53db4daff56e4dc1b276e90071a4a7d537ac7d9094b98f28580ad3a211f73ce2fcf770b662ebcba86688335f8a21e3ed78f2d4921cb38d69a7c25e3b2933dfbc90b543501077ec8d64b6fdc6b95597cbefac7b10c34a50dffea4acfeae1308360204a6607b2609d48fee7d4eb5be876f71c18ca77c08901363d120aba74cce485d9405bc51fad57c515707a11ddbbab554928be7003f7f3b68b2884110c256e136bd7ebe6896c7da834b9bd408a128d1e07213d13834426b11e1cca034166a1325ee6930a7b72db3251d8e9ddd11fc29dc8bf840312c59dba3cca36ea06d9e25c235cba4c27a47832cc31a54587121ab4933eae20f8ed330f62800c620eac8eb06af974fde0d06c99b25e502226087cb1acc7ccc4c577aeb90cc7f08e96a1ee15fdb47c016b22aad4da1faf428a685a09f04f8a5182cc6db022704e6478dbe2e526443e857400ff860d3bd99d7c43b82af9ec21c2004eb46a0cfcbdb69c54ec10861984da6bc7001751b3ca77d0391068b2586f2fdbe67a538d153fae8062c15e590c8e342fc0f74f6165aad336e223d96a77dce03b49cbdf197543e69f0f815cfa6734eba257eb16fd73a3a9509a553528a0a413d18155d0cded1c8a1a7786c1ad3b2734241fdaeb49a4b6efab98d0cfe644b9cfb2660f96acccad47467dda3f26bcfbdcb6f9a6f1df7a61098523103050a0e02343c76b5130883000090b0e204347797d7e9ea1aeb0b2e9fa0b1013232c3b4152596065737e85dee1e8ebec193d8c91c3d3d4ea2934454f67777c969da5a8adbabbe2fb000000000000000000000000000000000000000011242c3c00", + "expected_error": "InvalidSignatureEncoding" + }, + { + "id": "cose-sign1-ml-dsa-65-attached", + "operation": "verify_attached", + "algorithm": "ML-DSA-65", + "kid_hex": "ab23257f8c59a226cee2b09960e67bb8332a77c4c1b18e1c671d0c78cf3a2629", + "public_key_hex": "573647e17da3d83851b4c2d83802290e2146818079edbb53c9f2a9b1ee45d0de63e599d5e10f04d2ce3983556827e4c042dcca44668f2230c22d1e1c8a1db6db950fb96d2ed08a0492bf892861245066d46444d199946e6def49002f22013fbbf0c6fe1d1842697f3c58f3ae61bb453506ed588b09b528a0df239b6c809ecdcb29b75307dd722a024d6496f4331d44b409bed5a955204b35a2a466d95536fa6e0f534060c2b3187a6abf343e66b36632cea29a88a42df0d30d575337284e7ea74da85234014cde06a36e6c634cb8882f0a3cb6a84ed1e37a6f1a7f0d1a84186d7e897574a95952517de17396eecc976afeeba26d598a061a5cea258896dc006cef9740700ad084a726ba3f251ab4f6717084ccc4b7a58296d1581140d6c0440acccb443030ecb99be83261c96f738785eb0b76365c437409b35a487b3e0b55e1a401a1e5a18710539ef84786ca6d9dcefb81394a68ebb8b225d8031f68c99b9bb86a52f69e43ddcb5f96ee6de991b352b5cecbc03b1c70bdc858bd753ace250680d8f313c7a72dfe513616d04ae3f77d14ba54df3f9baffce58d73063efd7d90face6cb070172cb8d69df4f0a8ab291f3c856be8cd00183ffebc69c0bd4fe6b2e5398a42077e63d87e0b24533f3f067563c43b4699fe1bd1f2565f76234166b0a181a4db33cbfb7c7f8ba75741a612dca335b7a49ee4b22ccdbdee1a0c7db648e42a73311aeeb65d41f1e027d0c595baf32b5fe7e12c0843171e0c63c3a44b51da199b73e29b76446ca6f1189656d721dd3dc38c0504a0bbea684d0148c26fb5e7325b0dd3c3e8fca14e0e26324340e5696f285bb2069465864b528bbb70952273a2004d53bf0662e7a8de9cac1e1392e7dd518aa7afd0eb190e6d6118c6bfc853b5752cb0194f051239547c7785bd9638c0053e64daea88074696cc15462f23df4d9da31b0404e3f60485c8fa6034b31ffc7dcf607fb71328086e6306d0c5549b91719b8be4b9efaf56f59dbb29375f295ac8d35ac48971bf03a07752560b435f7679005cf2de0b27db4b56499eb2878c4b72d0b870d94a973d7dc92c186fd7dcb440452f17608020ae7b5203f3b2e95b3faf4e7b38eb151b48bcbd5c7ece249f22e097eaf1a9bd899999b0ea888d1954475d7d5e4ef931a960eecaa6d067954e82e1aedb31cd73ac1bcf5ac8165ffdcc5e2c3877fb779dbf0253df9c4f4a3d9988038386b6127483494e82a357f4581381746f6da034dcb5d43877606b7e131f0d59f6c805a8a6818d9815e758a5f8c52a625efb58702ba61ddf8cc2dabd2cd67893b482d660acaac93b33e21fd05372d86ff0a46193f843381abfd16021984295910ffed35b7c5a7ee06a147bdee375d5346013f3a05586a48d5313cc64939e8bdec8d05e51fd9844d3527a3563f71627b54e4de34349929312b04f1bd57265867143995ce9251c1685058770ef52b739c9fc741b5d3f224bbed5fe48050a35f4dfc1c1c54b54ae7d575e4d8e2737c08ae553a76f96b891bcaa8fd6dc720340ae8a1e4181a761d568e64a2f5fdc4773e0ae5dba8e07c822403a051467360b47e37ec1d7db41b97130d6fe333a22cf45c8feb9a07a80295bb10a94634c38be2269eb268f3f39a54cda372065be20c9b689fe49e9327ecca392b208a46657b1798348f0f9e8760b77698121305e51f2b3f4b3855510085f023e583ca1fc9133b33f647c9155a77cab776f7340228da9a47193bd93ffe991a9a02dfa46ff5fe0d1181f62d949f9dce6c0099d32f1ec7af0766b5c3a4ba348e26a094469447c286ca2bcc509980e3122eb624d97cd8f4fd813717506c7e901324fb3a020b9dc757ba7f4c2deaeab325a0cd87ff46393648629c8b7fa656fc546577b5de85a92c0830974e11fcfb345c528cd0595153fd7fabf4bb1e15c50c6d22b0ef23b5738dc5f31f0dd6d42060ed94424c77077e592106b16d212f99f017da673b3cfbd16ba7c365a6571e9a9194e64ae8d3dabf382a9d5fc331862e8f1b906b140bb73c51d3845a9dfae54152026f2a5be3801517b8a99004992a5169bc84740a316d48056af9e8596cd7da24f70936d94a0c6b4b4ec17a0709b3ad9009c29ba385f478e9658655565030781b4495b37fba534870a9a0071fd9b236669b4cccb82bed68dad2971eeabadf2d1a8612ebeb8fc40a35bfe02e32d06d7d7012ac1fafabbf97af0399f6c2cdfd7601e61339ff326b8693dd06c0133ed94997575a69f21cfef5ac0e3b2c38162a31b166c520ce3b37140b21b7e1110bd8317f47b889824bd6ceec1c6113e57e66ca12d8d2776b8356eab1b4fed235b6d6e38d1d298f36be4562d58e37aabff3dc1d0137969694c314c5e823d2357e79d1e2a8b378019f490ae97b528e6e39623214a45055f7226d4ddc4d4ee14e49dd623bd0a810b8afc99f809b6e438974d01063a6f15cb6527cad5a9e816ad81648063ba8f06aa9fa8bbca62c95fcded9f0f7ab6ffe613a138361c3ba9848db334482b403d1693743a038b809d49136001a11fd02d72895014c6fa0e4bec3e74a0d5d6c53522a956c6086bfe5b983d76ff7f983fd3404c0b4c9c39be5f6b4c9998e55c6bb3e92dddeb2eeb8174ca75ba87b11bd3f1058c34b04078501ee5acf3dc1e2c2fde5bfed4b78362a40e67a77251f272d497e51b04beb06348d21bc200c99a9993c4a2c22c2a419369cb6734fab70690c5b334d422fdfa9dedaa94606c1eb82b653d20c5e6d59195390f", + "private_key_seed_hex": "659137f736f5f5a35eb9f37c1c88c2a0bcf18e757ffb9285ab2c4c26c15c55f1", + "payload_hex": "5265616c6c794d6520696e646570656e64656e74204d4c2d4453412d363520434f53455f5369676e3120766563746f72", + "cose_sign1_hex": "845827a2013830045820ab23257f8c59a226cee2b09960e67bb8332a77c4c1b18e1c671d0c78cf3a2629a058305265616c6c794d6520696e646570656e64656e74204d4c2d4453412d363520434f53455f5369676e3120766563746f72590ced893a9043b9052bd4d1065a74551a80368bd1976b2f5b2f66c5853a9425cb44ad7ac08c4a8a357f57e6ec2caa75c890b4ece7f557e0cf62c6721714773934c18b398bd2a8d40205984194ecdd7973dc90410c0a01109460c18b40fa81db181617bff4accb0bb202f3cf96745c738409d5dd75acd47f032c68b2d0905b188d27b26cd84e146fe87c761ef25c8038f4bfb6233526d656ca01b36de35cd80b120781876e6603b68ee8bd9f98faa9d1718ee21ea84122b3372169e0db330b16eb29e8af80c424ca07a67d138303553548345f9728fa72111d15c74b17024fb1379caf09e7caa83cf696c83133070272cafd2e3ec20fecd685774054db3e667e57354985cd135c1f239e11cf96460ece56c59db7e4e0ecb8befc55a40ae4d120ae2ff686d46c9e2671c5464ce7c51eeaa398f2710579123b978e2c690c19ec738dbf3ff450b5b56e57721983e5a2eac1152994873e6d2b468e61833ffdcc8d722a13f9170f30501bf3fa41f716c02e77ef34c89f16db8e9c3f54c08068c2fca061d296a614b528b183647895d9381ee394bf15a5474b0c866ab75dd03c454080d2805678e02297e051147637e6acc86e99f9e2ab77a20bb5b115b0272cb5aea86bbc985138b76c73368f09fb013dcde614511897cfb1732c6b9ed9d5e50341aa102b05b4712be675a8dfc11b5e0785ba7442e94d1faf73e27906ad1496e71e7cc45d05017c00cfa1aaf90a8163570c04378648cde2ebcf2384f9e1c665bdf90acf9a8b737df2457111fdab67320c952e9141b7bb17832211e612df6aed096cbe0524c77f157e1c35f827e9ed81668b452c8ef5e7ebd8e64f5b3e589a6e80395a4500b9312ad73ddc7f37ae291daf1288ef7a1518dfe65c26b6aba356a33220d4fa844d631fd9b872d927cab6fb102cbe1354f24513712f758995fcd8677f5a014d5a3ce37deccc2ca9dad0920c7b1555721dc00d98e6fb2d264cace28aa8c8c077a96705c57765a7fc3229542365d4110da29ed3a2b4cca5701ef903c970c6f7bf0d8a9c6bd9362a715233d6105e4a5baf89dd3288d3fa8eb8df348fb6c42172ae6d00b32b4a757b159cd725af420d2a1307c36b385073850640f30c2dd8c0abc2fb2eded75640654e5e833f39df08cf577943023405aef877f1b83b412edfbc581173e974f17ab45db00ee2e60df95c42732344bab5186636b31dc44225e552881331fa0b75815e49e310fb42416e2737989815d2eef9099d7dea9b60a345557356a8774341482caa5172623bc2f0a913d0a592bb4c9be4d6358a49b3b61d846a79982077e9f515ad569eebd4e83add3314674c06452574acf983ae43f18cfe05798715faf35d64dcd1fd3fb9b1588208bae115932ed3d6e5bdd92539f32fd3cc5621ea59cf2c49256432381231dd4a40cc2b18f2cddbf5ea6f176d5e8fe22fcf6fcee562baa1b4f23124b8b941777f3cbef0afa9a4b2188efb56b9134405b7966fc9d7db9d2b42c58dcde6c5290ade4d9fba6aece6a6556740468ee7b3eedac27fe565bd4d97169a8b99f1449ad43044f0943e63eaab0c153155c6d55f8b4f1bd1f3e87164da3039b69f74d8e887c72f9f784418dfdf7ea22d0789414ba1e2c977a5c6cf4ff6176ea7fe8ffb020ce230d28c57b417534dd1944f82ea91d2b43fa36a9dcd30805846aa0ef1707c3e6b7d86e15fb204cef9459d42f012c12d7df4d32ebd038ae771600cedfbfb6192efe4b15bba8328018bbff698d1f5d24921a95e89d14cc0fba9d0115054d063db24a734d41628c1edaaf3f2296c54b96fe1f23dfbd8478c9206bf08e225934ee812d54180fcfdce35e5a40e5da99317cf09178c13a67e878d1150a95c9d605634b2385694253dfd4b76827361c87c7224afba4c0231c6239cd1f14af0206de2f50a73668567bd9f82914562920f825a964300e6d5522b751e99c6e7fbe1ff41babcb8161280c980e26fc2b5707ebc99bda3f5f8b93779b9c9e443e11294edbcc92d3cadbd31415f7adcaca06a4ee95dc2ad791f62c8cf454728b455e1733536c1e283cb4709ada1ac5083c98251d0e7bb1a61148e4696d9ae4b205320f0022274d6040be5705b07982b6a825873e663286ff0e6d55372fe9b0b85bd649ce2012bf2f5149de31357f9a52b221803e2ba2303601412888c90bc062c4efe48573ccb03a7268133326d6a9558a1510b06cc3a9e764308810d73b00477b172542a92588ba2306589fe26fd7980efec636b576f43f399c35102adc228ab1a62c0cb422e0d026c6ed59384423c6651247c0b80487ee3c3d3c43911635baf10366e57f55d6f3aed9c731e976e60aa9b8e6c5ec225ce472843f83723c26c8fe67932d16fc3388a71728d1512f4c969e8d709dc6b2a336b6adc59e19b6966b7e42f6722194979e2f36994ce336ca72f2b612330a4bbe9689132f551c4349e8e009537b44017d0b89680ae28bb7e8bcc5cdd1a7c0088abf536c4fb4146247e61232785f925d0b4195d1a8684962f734362599dfd8ab8ba7d4445eed4f0a1b78a3ced24b6d5f4da7cc29386cefbeea10ed360c8d26007d7e7fdcf5ad9c964233ff8d2372a4097b6dd78d3f9535f4de0e4b7864b9fbaa4ed5a3d6bdcab0ed332ad70de3d59fdac94d2bcbe431ed8321040762720dfbf77c7f90c49896b89ac2d23e173c1c871d6145b2e8943bbff213a67e37c9161d8b548c878a5bc1f8f9ebbc9d4013126b14395ea1d427c7d7078b168b74166a3d950b135d927f61aa90675145e90466e82e747a348bf242455f632a27ca3be1e2824b8a10b2e7acd1758fb1794979d2c03f92db4df7a1a65993e33cd2ebc84faa5d2264e667f7c67f46e832e5495f9deb76529cd5f00c856621295bf50772a3b4e867aa0b4995320569f572a53745ace0a4d46a453734d988db83de32ac692880fb2eda9857cb74f846abf1fa5bd29cc6a1bf69a02e7e356188d95831c32d38b2c38afb9ea4f520f7b70a654a19d0dd261c030827375fea864ca981e14db939ae108f6d0e6db0d49dae36fc60a954aefbc29054063d3c562442dca6b8c52d6bb1a321b8a383e2356dec6d42c5b42f612cd8876abaace78327c3def6f7aeb977479453161301ae4610ea4d63d00b4b673afb6499e97e63f23dd2dd158df41c04ddacc307b0f8e5f0d3ca545189e17e626486d84665cc656e3289db6f77c921a34d2513e431b5825d517c6bdb1bdd5016482e26347b0c85539d681551d0c7edc8fb7a4eab03265ba2b7c327e65057f5304ab491120367424f8ef2291164bc127082e9eddde1004da79e4d2412f0fdd8ff65f46a9730aff3618e2ae77eb14ad94c2b7598ff24f9696c4e632a7c428a3dd592aaac811c3a6e577984c113fc5179b1c588969c65d8263828fba21c7e580272652ae6f606f9cca80f125b5088768ab6bf310ba594484d61b415bda9eaa29e7927da1e5243fb54228e1651e798cd8d355e1100caef1880191ff068d52826be6be39ee95d73f577af3595062f2b1035dac6dc7d56bdeec809f6d9cb2e73cc09b717e4e2a2eda069ec919510521c6fb76d4ebcdf0f3471dd93749e784e442ff512d4671135a56c7d8b91104706a1974589dcd2ca5fd25fc41566f4da73b3950c654e663155ac702c9fa958db34759edd4c520e80372716fbbcf9dd4ab61785d928eb4bb29ea34287e573a30384449a0d55acd638b49412909826a56e5953e12915e3763a368467bdad66d339c45c7ddfdcf50c95c4f431b97179640eee0e366d8c271b1a247758cd04f18568f5eaac4332357c1482f0ecca01ae7edecb6ce0f88479caf5143739c39825ff9fa0431ce219d0c75120fb440f97b8a19cae7b91067f6b6713c6f65de8cb8fdd4d0ffa9128d91737af6edf9f879981918cefaec847cca3091c4ef7a81a1744074adf84c3a35b1c6665bd64c6103849e00dc0dd53f06af84285d3f889890616cc5bbacb90a431725b6a13afa64f3ee76066c9d7e5892db24d8b811155daff6492736856ca243032f2d02efa8d6751af5324f644e66507099c54fc7a6f7c17a1ab1c0b6f940086ded9ffb71c68157cdea43767f116e018ba9aede225ff9b87a2cf1361f1caa5d796a462bcf1cc88aa533f419fc34419e13ff3f91d12742e2973e4addf7f35d7ef21dde5a892087371297e009147888ccd22572924e9cd602b5a2ae2b087226e0075dd6df1765a009b5091b7e31ac1c436752dcbdbfa7a44e0f1850dec150251d064ede5eecd8ed6fcdeb25d57b5232d63809e5131f1bd85efcef8dda04e0692d0e8b4c61ee0823e894509cb4470875b2c77d3443fb3772c468e69807e7f832a0ae0edfa4077bde6f14c92fe0a51ede2b23eba079d4262ebab55fd7b7723cf28f26815a1cf51338a26ca875023dcc2748ef6789637b3199d08809a9efe24011c67f855ba18a38982593762cd394107a6dc063aa0ae3017b415bd8da6c5b9e6263eaabe5262e6c30f47c3e8cda1e4b5b2619e5671be6f79d69e6ec54d3058f81cab04a7ade7865bc059db6838dbb52ee41a997708b0ab901ebf37f42dd172dacc649425ffb27645ec32562b93f58760cfd237ca98e2505501432c2c4e9ff01333d4c737490a0d9eced35517b85e1ef218ad9e02f8aa3cad5dcec191b9dd800000000000000000000000000000000000611171b2226", + "expected_error": null + }, + { + "id": "cose-sign1-ml-dsa-87-attached", + "operation": "verify_attached", + "algorithm": "ML-DSA-87", + "kid_hex": "288593e128c58d8fe408b308f2f6ba042c0afc9b406e26fd6d6925e8d6f84c1f", + "public_key_hex": "90e3dd80d4bc34741c85c2c1b534b430b1540913639e15b6293a8d7982c5b966c8dd860cbef3ff19f4df540074a48be558de77b871d95634176ca9a684b8097e8af2c9fda44fb4cde6574b9f17ce2214bae45f61fda0791700290ad3f6af2d83a57ee66b3c5f06569e54f57c1a2fa2ea03122864ceed421ad3a8914c0e478e7077249f916bfc77e9613fcbd43ca3cc6170c0962c3f13e381798b559301651deaac1f043944f3d1e112b490cf14ca259ef21e43fbcbdc91a0cb7ace1447c4ee10014650bbfd8d11cb180fc46c1bb6472023282cd7a3dfd972c33551516e148d1f73bdb8719c98c342e6c661b38fa01f840cef0e758962aed805915afc48e1612663f0827e5ce9f20e792e2734a5afd522a0503c49052b43fe34e5168e10b9b52f06248727cf7a7a07ec7a96c7e2bcffa2cd3b48273032b39b1d2a6fe17ff90244996880ae85548f317342e722aae439a1f1ef6c0de6e235f1439b88d7779eebe1308acfd5d901f18bbabd2e57c639948f9ed45bdd823cb31c6dfc63ed8b74fa17a03ea3e10fab46a240a15792744562df6f3d5395d56ef4459a03a57cd9d6d87fd1e3d435338126163e0bd8d3ed5e0cec214e83f8fe3e3141724c52311b687bd2278922bd0ec0b4f3a92c99b5be8bd05bd6bd0fd103930e4493aa4804d941c80eaa92bc7468d188c615cfe4dd3c528557b965200636c4e217b9fab0d70732f7f914913d3b315067c1bd0de886beb0c203dbe5873ec5f728f10ae3ad925ad54a053a988e0154534cecce31a48837f64f4f5a75c30b718e615fbeebe4b8594301ef4cd28dd3ecaa18864c7e890d51f40baab833ad54e79d4ce6cfcac49c0037543d115b0e446ada24a99054a748d8353619e3c0f7a201b28fa9ae700123a2feaed1b2e6bf379936ac0db5c4861082e4cd25cbdac69b696ee2a6f1a73b2fb08ff6b46aa0fd357158a9df6522b47d5ae20fb013bcd731edf9bd55c1cfc7d5f85d9d3f853667625ed155514f696d2e1e4fdf60ab13b29b7f443fc254b564bdda7ed5b5c49531c95c64f70ac86f0b16dc461844dc5ffe0ea4dde64740d118410f1674b731e654c07f73e13234b9fc15c6b92a5c8db54569fe75426d3d82f3759d07a16288528d43503f24dfe1709a32c5517e3819e517478a907a56687d4122f7c87921acc6b952f3d19a1637da6175e70cbe2313a43454b36dfca8e89f857f4f4645a2a516dc2de180d1ec8c4f9ea3b7df07ef832297cf09e7f2eeb33dfd506cef44dfb6eec2f83f20e271a31b7c681e73626de08ecf5b21d51a51ad0a24e3a204f0e9053c328320eca503760047f6d1b86400b774877507586608efc68037204b42e8048320312f791122a9f88f3e0bcf852f6e1f53900c3777cffd3ec4c7d56ed4cfc0d4c72658fc0ab589bce0b3c6fdbf7556145dc063544e3424587943744745777739581d4cc55f76c393bf51b5debb2703cab56842b17549eb0972bb5c866d5c107f91c7a8ae1131860749575830ae3beb265813a52c9f8b6530db055c80035f4a41dc9bed93581c0e5bc5f029e6d5a7e8d50220bb395a65bf6882b5fabacb7e9b4a70fb31630366bd051ac89ce85bbd8297f618cd4a5db3b2b10f8f7155dbfc1625ec0b1a0778c83bdfb8baa7e993164b12eca9c3dc4d11bf72a71ce11e00de3940a30db35cd131dafc99e018b22525c72c4823f45e4dc25b9cf420567134d0ece8ac1b9073067d2b74129ef51e8c9b59ca1cf29a6ed54094418bcdd3a5d142278121db74d7a2ee5ea960e789aeaa8a90a7515f5c7e94c096901fbfdc164b5e29979d3611ce74f06079ab973dbd6773b3fa5678a4c1ec51dba9a877aeae553b80288d50f154029c4b0d0e6364fef3b1b8684e82accc56988741a63c6addc8cdefac64db57af19fe4f7039012bfd090493bd91ba237f2225885460a2df41490e212f7b437d9dcb3cf06fac09ec57ea2a85ae3c585180dadc24ef414ac54b77818d09b206fc337cb4a687dba6ad58f5b2ff9a97829bc4048356157c9c003b67e1a291fcf5a5e6a52923162a41a72313d3e5d6ccb92f375d9debf4dc266550f0b226520ef661e3fb56097d4c734a58726b533f66e72b7181f74788a4011d2133922fc765da1f017e85bad781f76ebe96f96757337ef2b14e9f1165848f3b4bceec28516ab2492c316b7903f1995b43e4a31afe9c5079f5183ba80c2537e2fce36d48ee5913502e9988987ef5196ce116dc6de546998f1b9da2fe7cc457f3b75951ee927c0d148adbe275b8c9e56c8fcdd459f025ac60c31ea9aa52ac89e9f77037e1ff0b148a97ea0faa6f915844ca571d2c43b50d18b337b5fe27154151cbb7b539fb965d8d54f8b118c5ff0a09567652d35af324d916bf9f798a2e5801dd28a19a3fc3eda3d4598eef57ab09e393fdf7d501a279b12a16151cd8d54eeb1bb86126e383f5df0da7f9b7d8b51206c2b310c89c7ba8fb381da9d9ae5db5155d958b7286048941dadb31fcdbeb7944949350c93d92e3f088d086f882b99dda35a4ada47eea5cc9e1e28b2e63896f0e9e39ecaba7904473641f493369ae4923b992045143addd051260be166eb9401b60b1c9ecf8f42828baf9799d9dd9101484e62607702680817faa2451e08435671ef315973e32a0ac7221e4897e67eb72c37974370753261d623a824f36faeea57046e92612a2573bbcd0f61b46b8d7b80bb14b634f428250670af02e6ecad1130799bed98ba9ea4c9549d513a6374771ffc2ecaede110660b33b604c3dec66e4f916a24bfc29771e11042b7390bb40334b2ce3b4b07e21a9f783eb51fec680d00c2aafbdbcb2d1bb71cca9d639a64301251f91535ef4bc136d027396aab87ffc05f5399a94a8d5e55aafdac654483d6da3f1fcc68462fd1beb558982ff527e7605dac118af5e01804c2756d961df1ac7a30ba6da38a4145c1743fb2810b95b3fdcc2d9e18b63878e808fa91e190b6b5a42a23dd211021149dc6894e911ef75c1818407af1a596f968bddca66b93e0a05e9ef715b5275d39fa49157d77d431dadf3518325f51f6c820103b4981cc6aa2fbc7be266f79ca6f1d74bb89a6a3292a90f716199747732c4edd1e45942487b76d6bbf84956df3cf004414a01fcb3cd85753038d572acb01a24ae0df24995444c1be5f91d56d548e82414bc37a5f40f5b315b9b56945665a2043bee27ad2e56d9ea93755c0356a36790ade2a130be8392cc760f0a4629239c08bb57e089c6ae5bfde492d07823d58f68e52fbd2e88309578c8ceb381e1d189f2e17e2f6f0263951ff99f072b29ea48f729889efb0689a1d4d3b558f572fe88c968275eed00ca4b968cce124c74ffb885b5c6ad285f12192ab9e60da808fd009c7158818073e6791d489e96d707629e87d380a2f9b147ba9124086db3d2becc5a24346353cc1761c2b8bd4988b21495aca7d38d525b52a1e12b39383dfe96333667e947675887c5a6bfa60909343a711fdb3135d501e429265e74117b58036116d4489fb28c56f1d1dc4bb4778ab196b6fc3f7c207c865d5e33768e88d8fcd23bee0ac19330bc21795fe96fbdccfa83ec8650f8802a606d9e146bc672353748d674935df0e3ff620f6aa05d28600f0e97dd573e77c56e3b17c5d879d443f9d680813b5a476226a3390741", + "private_key_seed_hex": "1e9137f736f5f5a35eb9f37c1c88c2a0bcf18e757ffb9285ab2c4c26c15c55f1", + "payload_hex": "5265616c6c794d6520696e646570656e64656e74204d4c2d4453412d383720434f53455f5369676e3120766563746f72", + "cose_sign1_hex": "845827a2013831045820288593e128c58d8fe408b308f2f6ba042c0afc9b406e26fd6d6925e8d6f84c1fa058305265616c6c794d6520696e646570656e64656e74204d4c2d4453412d383720434f53455f5369676e3120766563746f72591213e6d9b4e0118da18673df8a4c9a9f3ba4d7e8f338b0ada76df840f1526041cafcd89289da3563321fb9e3b07994decd2b9f9075022dfaa58567536561bbb72e5fa2808e5e54a31196552669d06ab359a60ff34038303d6af3a8818166c994f220189612820491d576f200ad7322435985ba05292ebb67ed25c34101ad204a9cfe78788f6157e601f15f4a809c54159e8a37953bc1ac7a2e24ad08c9f8c5b99e382cf3feb8fea0a9d40240cec798dd9d8208b3846664eeb61692965b0118e93cf6813192120d44713a17a0047893632dd3cc37ef546e31497ad1e66fa9a1ad079653f1c010f5da8b8e5f766b99cb60a93d3492c9e44ade4732425ce87b92fd14e4a2cf80bd9763665e6c2f9e357fce908aeb01ad4a9e76907a456a7152e19e39d8f40556d20d1a407867a4cfda2e92a21631e8ae4aa3caeba78bd4f0987debe20cbde524ff6605f498747ae53070dfd5aadb1c930060865ba842bb82cf569c2c2f0ce0c65689e40ac332809b1fbdab5d14463ee10a8d99eb614b912a4f4c95428b5579bc1ab95f4acdb3da35b5d2d24031cf95f1f6a2ea65d9cbc2bf931b2d78ae9a4a1a570100135e332b15de578f8434de43ddc1f217459c49b802aabfd807d2dd76c511487bc6f8d8679aac7c64b4e7665e2dc6641126da563ecd08b830181c648d5c455d0146468e3e3ff44b1c1c3e9cfee91a6896bf4703669fa022a6771bb7a6153774c842dd85f3e6863a2f3f1b93049eb6de01bc41ae000a7a01e12d5109902fb54e789ecb5e09d3dc6498686e6d62b5e16e9b7277bc12ed44d07e2e2a5b46159fa67ee0b4fe730f071d16d6eeeba5007c8de9005cec0e93e5c03be29b307243265084f2ad0ae3f44ef7329880c48aec0c9a5bc79a7d39cfb95ba8195f090d27fbd2792ac5b800274e3e74393d4464cae5e47f5749caf8d1ad6050a95c9e24cfc79f89101294151a082b40c75e015b9576852723c142e0c416424a7c24b81d4cff806a204e49af042495e95a021fced4b67102db59b103edac704ceea49cf8cac33e2e33f4e4ffadceed97759d3662a6f31ee02361ea9960e52d866212de56fee0fb04ab2ea563d7c5c515f70da5be29ab12e9bfe460e62b2df1f74414c343e35d9acddcbe7fbe02e9a2df24fdb0c89c06b24d1997bd980006a4d959c05df2c8666d52edf3782c75822d5948410966f16f93f49eac883b8faeeece5d2dda1147b57adefae7fb1b2994bb594e45f3ff7405d69e0e3b80ace8123b63d9bdaa258b50327e4d04b90bad79878e1d8bf5ff25f882a08c75bab0bd0e46d2afd237d7523c44b1bc3de21209fd14302c27cf49e44b195caa4752955d71418a605b7a6fc253e6b9d81d9e42bba7671cdc1efb165ef34f2d0ee16b9418dc4dd372d485bfec7b33362736b0d7bf56b59adcb905eb43606bcb5c49b765eb7be6a43f2b55b769edf703d380a17ce0b54f334344448cf9e4b08eac2a11c07e4b11279419afb469ea24e783b71999e971c6b4c0eed6a820b5c206eaa8cc2a4e298f439fbb50c263c5bcd3628ae95f2cc025d6fb642519891bf6286faf907d21553a01720e592f2096781f33437051ab99bee94913dcd8dc57a1bdc2e2bd393ef6ce9b95642c8405b0508162109b9780b40571251908f2a5637ecf1a45e556f06427878cf95e30ec99c4c8633f59f97e606ea126c435078691b55f28bac537ee962ac005e827591038c5846cc9f5e1845c93872ea9ce6d2e78388dc2544f2975cd4e36c6caa01c2482150558964306a5cc6429ee8d595439178d450f459d819e101451c6300335f45f82d5cd8ebd3feec60110d5014ade88175748778b44dccda811156505820631139614769c5221ba6c87827cbe88cf33b498bfa94239452b8a35f55c75745902c0ba55657d02b8d394475120cbd516c20b1ff38e1f01a5f2d07c7265ad9b5e079153125c9a6634cac5fa8c29577f13f03f78bcdd5cf4b183e294f5481359466798cbdef80809bfe3ebfbb8fc4bbcfaf7d5e97a558566032d84577fb3bf2862c18b9c237cc38067ba99967db01d0a4161040b06956e7bc8a94983bcbd44d7a46bd3728e6767cd6b54b10f7e77db4d36ca2290d83edc53c91f49d924577741cc8604f648ef659857c6ff3b2e93f6ae821de7c956c9ec86bd24738ebbc3bf531c778013c0b9daef6566da52d1c8febbe4c1ee8b84a2eb29d79cb2ce1f1dde64a751a401f46d8ae3c8e49ca6e61ba7a636dadbf38a6e969cc1c406a482f46ca2e0cfbd07960c1910b4f45d2a34df24e2f1054a15f16ecbabc153082e63b5558ae82596951c077a7c39b9e3cfbc255e0dec4da2f96671c4c03f99efd4021f85cad6bbb2a37efbaaf5d0e351d60af5711f97427536418b683617623cd319a4238a8bb9ee6fe89591451169e9a37f4e5b79c5b21571cbc71102d9b4a3b271ac4264f3fdbcca1b6600eef11338c661fd7dc016c4f72983098ad40362a0ecb84c7b270a4f8c4f770c31621dacf100bfba835f993e3a03edcce7f2470ae018a77225cce304014145546b056904c582c07d8542342e046b7d6755508ef1e2babb5ef98fad193e07f3d200aa791441f5a9d4d15a6a8e4cb5e2eece003370b2d1bd3c078b98d194c621be5e8ea07cebf877a2ca4f3aab4d9dd060e27e9b9d309f3b48e3176198cf46de154bd9e17126561c6ac09293b849f7a4bfd3254d1c20546ba2e1b01879ada9fd558dc9f95f3c065ff4a180c30478025736700e11d1b88a6b68a0ee171186c901b59c11368816b3fca36e9b3487f5aaafe3a00f3096f56c644e7c159834e9ea903a6031e617e02844b21bec9a09d138dd39a1e7d470acd53f78de98d2492ecd95c7b5931001146a08e361065801ef2dcef04bcd29bee57d382b62189a141121514f7092878f50fd186681cc4cc590de24288fcf109ef59319115494f7fe78c21c1c57f84bd67822b4e70b7e0161e869b0655c80aa2d80e44775da31f1b5711d15a6f8154c1f4c964965ec3b44dc60c1182a3eacc1a2ae955fd4394d5471687cbc3292a45ee6a5da2ec13aabad4e4c1e772348cc095532253203586305fc461a0be1389701da63e58438246ea442aea1424ca84b5e47de154de361d8a1b3fc8321d596eb515891078e3dd39f343dbce789ff316ddb35bb6762757fcad3ecc1273d67560e212f350e6a0d5d68d5c4a887d36f781ddc2b637f78c5488d84eae34ee0e5b6621f85475c8f54460daf75dae3aea8936e0249ad7403e24d79279257af0a82f7c809dc58524bd20607c957e977ba89c307c9dc0e423f95a3a47b66783ec0b376d81f53cf7fcc3368ba9daf50ab876e40d1617bf50e3c5546e8efbbdbe922c06d93c3b443d85278084da19d7656d16deb9cedd10b9645b45050823779ae1300bd9ebfb80aa304937750480df4f323a27639f831f1ee649f9232632dd593492ec4602a390549570bfdf4415916b15203f6cb7202f7a9566ae70c10790786cf74e148acbc4c13f0bd70987cb5623bd434c3452285fac145a7537f063e01a29f697af14ab0c42215876944741b734debfdab394d5eac518237d836fe0b1668e718a5aef2b0692a9498563388e69794d6c48c9019949fc6eda7e50c3dc812dd01bda4a6742f1145ccffd229699959507188abac5976d7ea0245eba9e9d83c31e5dc62f5fe330d4db66996ec03510b1883cf48b46025b341176452c78bf0e4de5e83d74a2077bea20f81d84b2c0eda1d56ce9575f339e0bef5142f17a146f1242945c91043e58696566b8526826aafcd896b38ce22787a955f4f490d070e129cbf015a90e6ff6a95e7047c5441fb057aef2e1ad3e3469ab55132714cb8558905c316370dd7a8476cc045b55b69c3ad31165d7089275961e2ead6aa2389f652e0bc949ffbee4170924eb668f55156143ab2d91c2841773040e66b14e73a1b122b9350c4d6cf8001f8c3da2077eaf93dfc57c252eb51bcd52a3f422d2d5c89f8d7cb456c8005c399fbc4167505a8f8ba2f9b4662c0796db3f75136370592596e53268332952295d3807bea15426eeaeb0bf7add2315849b03b6103cef5afe73a8bd2e75c3d94c7d0a5e55c784fcb30c568db0bce1c86bc63f5ab589a6dd586e9f3c8b1f35fc7945029c0c1b63b3c8ee6e0ecbbf9e9debe133f6d61f871612b3247735a619d0f4a5be3a230e76c18cb696c8c6cd131b9a081324083d5a35fd764be77a3a87502e9a4acc60b1b46ea0ea84cc147f8c7d5f806804f3b925920896be2fbe5d7001511012d7ec0057ed4fa199b3e6277fb569a0ec4f320a71e935ccb0ebade484acf8fd3016213a1a384ffb46f87c1f4e0839730afd14102d4e37d2073f9e79838c5e15d9ee8632fcfedfbd8d350e8353959c53e2b3012ae12f5acddde88a2666884cd6929ed6a1d2bf7e41f197621de5c3ecbc5f93f60a408a09203817692d7881a3eef6adbbcc76f94532db30de06a63c811b35a05e438b280521369998274ef6e85a929f3f884affc29b3e6cff027314e14fcb845c2846af9374cc44740a416e7abdbe9e6ea690281b867602d73b8c49c80c2858b1d38def4117a7874dfe4779ca253082bd9f08c0f851c22487cf7bd5750a7fcc79174fd1e41f96532982ee6a0682dec1321159482e78d5c7f8e25a62e4f29bbb01d1ad62f3d0ad7d5aa121e6291ae5bfb04f83903d7e9fc95efd01564648f99e95b62275fca0642a0eec951662140ede938f8960d55d712c08f7146bb28eab87bfefe375073a20201f1aa730374a181586aa64f5a2999fe1d4daef902516bd958f7116ace08c7d19d549c313314d3e135891f4bf2d72a81da51259296e5e578c50b44d19a6f62d19b49c46f66344ff538cc236929e6b526687be6914a2572f78d92265c0929c1907bdb851c5a6e31feadc37aea69fa389a84f244a7aa49ee38291f5ff963911b2249536ff6a427bd0064302eba09ca6dfb6138e39ed203a2f3341b7afdbbf3d39e93496ca31f8c6399ff53775ab447c45adb0d5cb7357dad9c7170e26b153e05c63c1c7b5a46668b8c952b18a3dcbb76d276f635c21613261785085157b25bc48dc764691d111ceee141a766cbee7e18e9b4212e57091ff3258303e090ef7fc285bfa54dd29f3d906b682dc88ec1d5e9f2499562bc85bf991fcfba527e3af413b2fad493cd5412718bb8c0ad71f95d65efc5433293ed4006ab72500e04394df2f552810096683021b88d7a443b226c77fe148a710a5ccb047412c43dc21e5cf806dc51823ac40f3fe9194bf2b7797c36f386cee7a1cc8a178ea57943dd3fe8f828f36b70e2749c80b5929565aae077f5308b8831325f11c15af15a3112db367315c344a09b1c89a5577741f3957946184072baa1013c28cce49fda50d1538ce61b72b4721b8c57f33cf6ec434b1a871913ae6927b8d3514edd47f67542e737e39e1b738af17b3c1e8754506df4469a1b615c0a0f165414e4a81e643b0370e66733085ffd590b192c662d12999cdda0d704b785c49919af3ed2c134339631758873da8d047a6f59137bd5b7cb3adc74e7fd982872a033eafacc60c50d794ab8fe5817b8b2bb6d5252f25a28954deca36c72d79f5bbb2a0f444bd38f2bf03203e214745fbe9df95feef0b6c4044579bf9629a0e6c8b20f15371b726c292cf867a87dc59a37afcdebb4235f959cf100e0f2e20d4f4c68c9a8a0b14284c76edef58d96c773c9e17d8e1c9a1c0e65e9783f907714611110ae88e1fa49daf2cbe0f931e1cf7f34fdf85f7873b0b20e17c2f49eeb3a322813e5a1ef93527cc802c92af2993d6cddca28c8e76f163d79a0a66f1f092a12ed76e67b7b028baddf08148a49eff52059259611ad5e189714e72c2a756b01ec4bee3f92b504e5481a5f10815bf2d565231a5b51bb034d12ba8d7a8bd022f889e7b1fa3b1a1b3a224a5c18c339a2b38b6b9e356f7cddb9448a68ed792172c7c9700d1503938bfa5781ef8c6465b0f0f79d3aad0e923e1dc0bcea7dc8342d3760948964abbfa5c0cc47d8a4577721234d866fc6b32a9e0ddbf140b300056d87c0452b7fc0cba32322a9286772b2f0a19770747dbdfb7e7c2bdeffb5cc878abb912db19cd3fafb5e0f23e4daa9c1a71687054959d90352902ee7df0006419912f87e02b8012bf4efb6b0db2179a0c2b63a50a26e84a604e068ca5aab7d91d96fd5bf45b5bfb2f5b16435b775f05a76c3709f98e2e3dd75130b06a20bd88ed263734d361e7f5a6bc50e19928a1dd05bd7a13b0ec190a1e72ec911e466a6371be21d729c8627bf6f89fd3fed1111c67a2928bf3dd1a718ae80fca7393e24eb71a558c059fd9e83f27c3d5e52f8e61fafc6cddd411ea6480bbca48075436595079e7807b84410fbeb1bb03bbe84f61173856552b0990a0181e3cc757c11ba4ecf750904eacc241c4f4a9499010c59b4c2d0e2ff36494d5d6be3036270738389a7b4bccbeb6a7497b5d9dc03182223282c468083e8e98299a6bacfd0dc1c1f2232393f4a697ac0c6acd700000000000000000000000000080e191f2a313c3e", + "expected_error": null + } + ] +} diff --git a/conformance/vectors/cose-sign1.json b/vectors/cose-sign1.json similarity index 74% rename from conformance/vectors/cose-sign1.json rename to vectors/cose-sign1.json index a7c8ff1..4c54f73 100644 --- a/conformance/vectors/cose-sign1.json +++ b/vectors/cose-sign1.json @@ -10,7 +10,7 @@ "public_key_hex": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c", "private_key_seed_hex": "0707070707070707070707070707070707070707070707070707070707070707", "payload_hex": "7265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f6164", - "cose_sign1_hex": "84582aa201270458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d3037a058247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f61645840f949f58016b8c10a576646368653e546c433d7336c86d77ab9f7d29de2ce4b338f8624426d008aacc657104c5feb3dc56bb8adfd766c5d1f5fe8a361ea488302", + "cose_sign1_hex": "84582aa201320458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d3037a058247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f616458408dadfef0fcd78603a7fb1623c1aa87031daea6a8ea95a8a2420007b3ed1a7933b46cf6a5a1ae138e7847ed0a572440e80c9352955fa72a820f7d4000065dcb0d", "expected_error": null }, { @@ -21,7 +21,7 @@ "public_key_hex": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c", "private_key_seed_hex": "0707070707070707070707070707070707070707070707070707070707070707", "payload_hex": "7265616c6c796d6520706f727461626c6520646574616368656420636f7365207061796c6f6164", - "cose_sign1_hex": "84582aa201270458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d3037a0f65840b6b7fba2e394ab18e26331ee13e69ce856d6bc0c33ee0f0f51dfea6011574b6be61660300f5bd667c65c716a23dd9e25b0e53023747586228d6d17660517d200", + "cose_sign1_hex": "84582aa201320458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d3037a0f658404b245051ed19a9a3d6366784f602cab424ab7c0793882f3bf362f3056100117234bbc06b51c118fa6857344322211951746fab5acd0b8fdcda99ed4c1aac9007", "expected_error": null }, { @@ -32,7 +32,7 @@ "public_key_hex": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c", "private_key_seed_hex": "0707070707070707070707070707070707070707070707070707070707070707", "payload_hex": "77726f6e6720706f727461626c6520646574616368656420636f7365207061796c6f6164", - "cose_sign1_hex": "84582aa201270458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d3037a0f65840b6b7fba2e394ab18e26331ee13e69ce856d6bc0c33ee0f0f51dfea6011574b6be61660300f5bd667c65c716a23dd9e25b0e53023747586228d6d17660517d200", + "cose_sign1_hex": "84582aa201320458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d3037a0f658404b245051ed19a9a3d6366784f602cab424ab7c0793882f3bf362f3056100117234bbc06b51c118fa6857344322211951746fab5acd0b8fdcda99ed4c1aac9007", "expected_error": "InvalidSignature" }, { @@ -43,7 +43,7 @@ "public_key_hex": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c", "private_key_seed_hex": "0707070707070707070707070707070707070707070707070707070707070707", "payload_hex": "7265616c6c796d6520706f727461626c6520646574616368656420636f7365207061796c6f6164", - "cose_sign1_hex": "84582aa201270458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d3037a0f65840b6b7fba2e394ab18e26331ee13e69ce856d6bc0c33ee0f0f51dfea6011574b6be61660300f5bd667c65c716a23dd9e25b0e53023747586228d6d17660517d200", + "cose_sign1_hex": "84582aa201320458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d3037a0f658404b245051ed19a9a3d6366784f602cab424ab7c0793882f3bf362f3056100117234bbc06b51c118fa6857344322211951746fab5acd0b8fdcda99ed4c1aac9007", "expected_error": "KeyNotResolved" }, { @@ -54,7 +54,7 @@ "public_key_hex": "031e18532fd4754c02f3041d9c75ceb33b83ffd81ac7ce4fe882ccb1c98bc5896e", "private_key_seed_hex": "0707070707070707070707070707070707070707070707070707070707070707", "payload_hex": "7265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f6164", - "cose_sign1_hex": "845828a201260458227265616c6c796d652d636f73652d65733235362d766563746f722d323032362d3037a058247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f61645840c74f451d5109901766fb2d54fc401684061cebb1a016a7d05f76e7bbbc7797984ff3b5e258c6b0b990ae4c1291a27787bd8f3e59dfbc2b7ec5c14076e7b63364", + "cose_sign1_hex": "845828a201280458227265616c6c796d652d636f73652d65733235362d766563746f722d323032362d3037a058247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f616458407f59a1889ce1b6e01c17573091decc3fde75649a7c9c24b3b4b4f967b33ef109bc5d66e5f54dc02f3b11b1a9eecf745e7214de4d212bf86f7979eda8cd1ba760", "expected_error": null }, { @@ -65,7 +65,7 @@ "public_key_hex": "031e18532fd4754c02f3041d9c75ceb33b83ffd81ac7ce4fe882ccb1c98bc5896e", "private_key_seed_hex": "0707070707070707070707070707070707070707070707070707070707070707", "payload_hex": "7265616c6c796d6520706f727461626c6520646574616368656420636f7365207061796c6f6164", - "cose_sign1_hex": "845828a201260458227265616c6c796d652d636f73652d65733235362d766563746f722d323032362d3037a0f658402e6ad3b50c4b98b8e4f8ee31ec4ec6e9077a41ce7ba44a6a8c1fb0be0b07b82345fbaafb95a04dcefd482c2a5c4f63c31ea7b1f7ba1976dd9a1225c8d60fdd4b", + "cose_sign1_hex": "845828a201280458227265616c6c796d652d636f73652d65733235362d766563746f722d323032362d3037a0f658400fcdd7832232a0c7099a2eb6c3adeb23c81eb8f1b379f067db877506adbbac310093ab933783c48f04966bdacbb35f9849ba72338dbd8c88f58bf77530023bf8", "expected_error": null }, { @@ -76,7 +76,7 @@ "public_key_hex": "031e18532fd4754c02f3041d9c75ceb33b83ffd81ac7ce4fe882ccb1c98bc5896e", "private_key_seed_hex": "0707070707070707070707070707070707070707070707070707070707070707", "payload_hex": "77726f6e6720706f727461626c6520646574616368656420636f7365207061796c6f6164", - "cose_sign1_hex": "845828a201260458227265616c6c796d652d636f73652d65733235362d766563746f722d323032362d3037a0f658402e6ad3b50c4b98b8e4f8ee31ec4ec6e9077a41ce7ba44a6a8c1fb0be0b07b82345fbaafb95a04dcefd482c2a5c4f63c31ea7b1f7ba1976dd9a1225c8d60fdd4b", + "cose_sign1_hex": "845828a201280458227265616c6c796d652d636f73652d65733235362d766563746f722d323032362d3037a0f658400fcdd7832232a0c7099a2eb6c3adeb23c81eb8f1b379f067db877506adbbac310093ab933783c48f04966bdacbb35f9849ba72338dbd8c88f58bf77530023bf8", "expected_error": "InvalidSignature" }, { @@ -87,8 +87,8 @@ "public_key_hex": "031e18532fd4754c02f3041d9c75ceb33b83ffd81ac7ce4fe882ccb1c98bc5896e", "private_key_seed_hex": "0707070707070707070707070707070707070707070707070707070707070707", "payload_hex": "7265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f6164", - "cose_sign1_hex": "845828a201260458227265616c6c796d652d636f73652d65733235362d766563746f722d323032362d3037a058247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f616458473045022100c74f451d5109901766fb2d54fc401684061cebb1a016a7d05f76e7bbbc77979802204ff3b5e258c6b0b990ae4c1291a27787bd8f3e59dfbc2b7ec5c14076e7b63364", - "expected_error": "InvalidSignature" + "cose_sign1_hex": "845828a201280458227265616c6c796d652d636f73652d65733235362d766563746f722d323032362d3037a058247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f61645847304502207f59a1889ce1b6e01c17573091decc3fde75649a7c9c24b3b4b4f967b33ef109022100bc5d66e5f54dc02f3b11b1a9eecf745e7214de4d212bf86f7979eda8cd1ba760", + "expected_error": "InvalidSignatureEncoding" }, { "id": "cose-sign1-es384-attached", @@ -98,7 +98,7 @@ "public_key_hex": "03b01bfc4a2d7fe2095e6627cc5caf9abd153d5b551fe7f6da615da9d1bfd101e8d191e85902c0a34eaa3a16e666880b73", "private_key_seed_hex": "070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707", "payload_hex": "7265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f6164", - "cose_sign1_hex": "845829a20138220458227265616c6c796d652d636f73652d65733338342d766563746f722d323032362d3037a058247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f61645860e84a4f8f73da40256c33e12854412b53952ea166bd75c45da383555d5788fcc37bbcc515e21065f24d68108021df422378c004a0d2fce918f99ce10b63ad9ee0f521464a81103f861d8413e4ef4b21ee9827ba6f7ba14ce6a59ff07867ca9afe", + "cose_sign1_hex": "845829a20138320458227265616c6c796d652d636f73652d65733338342d766563746f722d323032362d3037a058247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f6164586015b4f4359852f929be06e522e11a532a14bb8aebc586d95269626d14f7f8a028eac3758ad913cb1f4ac40bbd7f79e7872848b3cb3b7cd2cd1e857f9ac2cadd1414dbf151b7340f5a379e7fe3b0c6307dfcc4418832503152f11c7c1712beae52", "expected_error": null }, { @@ -109,7 +109,7 @@ "public_key_hex": "03b01bfc4a2d7fe2095e6627cc5caf9abd153d5b551fe7f6da615da9d1bfd101e8d191e85902c0a34eaa3a16e666880b73", "private_key_seed_hex": "070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707", "payload_hex": "7265616c6c796d6520706f727461626c6520646574616368656420636f7365207061796c6f6164", - "cose_sign1_hex": "845829a20138220458227265616c6c796d652d636f73652d65733338342d766563746f722d323032362d3037a0f658607bb5ba27d9643ddf0568da58189afa2b5784cf366f04ad34354a4d42a178ec82bce485460469b5f5e3147682d0d14fbef0434aa27cb9249f962da82f19eeb35511f4e8e4c3368d1e65dc7520f74aff39ad79d2062fd4dc1c9233f1290d19745f", + "cose_sign1_hex": "845829a20138320458227265616c6c796d652d636f73652d65733338342d766563746f722d323032362d3037a0f65860cd6f58042ac87a7962a20f2fca85a2d286ba9e23b2f44c0ee75f8669dc812a129d12d282d066b26633a2ad613128beccc56983a0897ba14f34618a0743ecaddb2d3c314e9672ad1098a8efff56a454821484176122fbac0d6ef0b9b2dc54dc2a", "expected_error": null }, { @@ -120,7 +120,7 @@ "public_key_hex": "0301ff3c211c59290d8390597856c86a0b690b5427b1397c2b10ba96d21d83c16e27213ea996810f36e7e0f3907d1178b384f1896de0dfb2a83a047e1e2d4cac04e93d", "private_key_seed_hex": "010707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707", "payload_hex": "7265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f6164", - "cose_sign1_hex": "845829a20138230458227265616c6c796d652d636f73652d65733531322d766563746f722d323032362d3037a058247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f616458840144aa2605fcd9bc459f248a0007effb8bb1b6e504c5abecdde9d86f42682d6fe950ddeb34ccbf69d0b3bf03acac703769a2e7d168478b31bd2f23134685f3345ca401cb95c06808d6842623744d327ef94fa724cbff2d10f18e71b536e60292c3db83dd63fe96be7e2229151baf3bc087667a68655d19c41358b2d8eed3fd7ed9ea3674", + "cose_sign1_hex": "845829a20138330458227265616c6c796d652d636f73652d65733531322d766563746f722d323032362d3037a058247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f6164588400c38b2e385d51e871c86eeec961d5f8e666515d2ad489aaa224517895f8b7d0a107bca83e43613764b9eb0ac8443a39e384e9c1429308b7157f067b56946cb2611300088cb1973649e6634ae3b5fd37d03bcdcfa8ddeb179539d2e1981ef37567a11e5fe348a873080c97ac4bbd8df186e277e4aea28b3ab6c1378d3ade87e28df92bad", "expected_error": null }, { @@ -131,7 +131,7 @@ "public_key_hex": "0301ff3c211c59290d8390597856c86a0b690b5427b1397c2b10ba96d21d83c16e27213ea996810f36e7e0f3907d1178b384f1896de0dfb2a83a047e1e2d4cac04e93d", "private_key_seed_hex": "010707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707", "payload_hex": "7265616c6c796d6520706f727461626c6520646574616368656420636f7365207061796c6f6164", - "cose_sign1_hex": "845829a20138230458227265616c6c796d652d636f73652d65733531322d766563746f722d323032362d3037a0f6588401583b823e2a26367552ded3b398f4eb5ee0563cec8ba6408b9eebc2565e3e11da2b2b1b4cc62684b9b50bb07764cee41177eac24cd8036803eecb6a4eae0a8aec27011b767682cac0c443cfa2e04002e034b47d4ccbb753a381acf66d141775dbebcefb8e6fecf6ad35c7e42be9af8f950de483d2d13de4773f30bb3e88af4d18853815", + "cose_sign1_hex": "845829a20138330458227265616c6c796d652d636f73652d65733531322d766563746f722d323032362d3037a0f6588400d0db3d5ba1015ddd20589f7680e5373e74c88d4e227258cd637b4e49ad706285f64242dea5dcae500a578a2de04c985096476e546aaff1495bcf6895b8a201454a01306561312e6b9d601a7c5bfb41f8b362993d2fd1548344653771cb000f9ad29e8691faab6258a7c80dad61f8fefb2c77e34e45dbe82cb6f7b3fe328416ee5bfedd", "expected_error": null }, { @@ -164,7 +164,7 @@ "public_key_hex": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c", "private_key_seed_hex": "0707070707070707070707070707070707070707070707070707070707070707", "payload_hex": "7265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f6164", - "cose_sign1_hex": "84582aa201270458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d3037a058247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f61645840f949f58016b8c10a576646368653e546c433d7336c86d77ab9f7d29de2ce4b338f8624426d008aacc657104c5feb3dc56bb8adfd766c5d1f5fe8a361ea4883fd", + "cose_sign1_hex": "84582aa201320458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d3037a058247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f616458408dadfef0fcd78603a7fb1623c1aa87031daea6a8ea95a8a2420007b3ed1a7933b46cf6a5a1ae138e7847ed0a572440e80c9352955fa72a820f7d4000065dcbf2", "expected_error": "InvalidSignature" }, { @@ -197,7 +197,7 @@ "public_key_hex": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c", "private_key_seed_hex": "0707070707070707070707070707070707070707070707070707070707070707", "payload_hex": "7265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f6164", - "cose_sign1_hex": "84582da301270281010458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d3037a058247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f616458406ae8b39ad4bb0e673d60daf927eee0fd496171b752ce14e0da7ac63176642734e9384c5d8bdd9c45416d33a6582a2fd36959d3255eaad308bec9946014564503", + "cose_sign1_hex": "84582da301320281010458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d3037a058247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f6164584008d083f72353f4dab0f76b5a04b682b956cf76bfad2f15ac28fc6becfd434a2123c67fbd7ac580093d8c3d30ad1e3872d1d3387f53720cf891ba2aa082ff7b0e", "expected_error": "UnsupportedCriticalHeader" }, { @@ -208,7 +208,7 @@ "public_key_hex": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c", "private_key_seed_hex": "0707070707070707070707070707070707070707070707070707070707070707", "payload_hex": "7265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f6164", - "cose_sign1_hex": "84582aa201270458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d3037a1044a736861646f772d6b696458247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f61645840f949f58016b8c10a576646368653e546c433d7336c86d77ab9f7d29de2ce4b338f8624426d008aacc657104c5feb3dc56bb8adfd766c5d1f5fe8a361ea488302", + "cose_sign1_hex": "84582aa201320458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d3037a1044a736861646f772d6b696458247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f616458408dadfef0fcd78603a7fb1623c1aa87031daea6a8ea95a8a2420007b3ed1a7933b46cf6a5a1ae138e7847ed0a572440e80c9352955fa72a820f7d4000065dcb0d", "expected_error": "UnprotectedHeaderNotAllowed" }, { @@ -219,7 +219,7 @@ "public_key_hex": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c", "private_key_seed_hex": "0707070707070707070707070707070707070707070707070707070707070707", "payload_hex": "7265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f6164", - "cose_sign1_hex": "84582aa201270458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d3037a058247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f61645840f949f58016b8c10a576646368653e546c433d7336c86d77ab9f7d29de2ce4b338f8624426d008aacc657104c5feb3dc56bb8adfd766c5d1f5fe8a361ea488302", + "cose_sign1_hex": "84582aa201320458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d3037a058247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f616458408dadfef0fcd78603a7fb1623c1aa87031daea6a8ea95a8a2420007b3ed1a7933b46cf6a5a1ae138e7847ed0a572440e80c9352955fa72a820f7d4000065dcb0d", "expected_error": "InvalidFormat" }, { @@ -230,7 +230,7 @@ "public_key_hex": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c", "private_key_seed_hex": "0707070707070707070707070707070707070707070707070707070707070707", "payload_hex": "7265616c6c796d6520706f727461626c6520646574616368656420636f7365207061796c6f6164", - "cose_sign1_hex": "84582aa201270458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d3037a0f65840b6b7fba2e394ab18e26331ee13e69ce856d6bc0c33ee0f0f51dfea6011574b6be61660300f5bd667c65c716a23dd9e25b0e53023747586228d6d17660517d200", + "cose_sign1_hex": "84582aa201320458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d3037a0f658404b245051ed19a9a3d6366784f602cab424ab7c0793882f3bf362f3056100117234bbc06b51c118fa6857344322211951746fab5acd0b8fdcda99ed4c1aac9007", "expected_error": "MissingPayload" }, { @@ -241,7 +241,7 @@ "public_key_hex": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c", "private_key_seed_hex": "0707070707070707070707070707070707070707070707070707070707070707", "payload_hex": "7265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f6164", - "cose_sign1_hex": "84582aa20458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d30370127a058247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f61645840176bf66ffd570eb96458ef3d5662ff039b0a025284aaf36a1d85f7d4c7c60b80958f01dde80557dca5d48f529cebbecac4d4b0d137321929e23e680272782008", + "cose_sign1_hex": "84582aa20458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d30370132a058247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f6164584094ac2ef7f9c3921868c1e0ed19451480af9c58769333f976c8ebada75ca2500a78748ab733a6bfb763f7c38d5627469bb1d948dee4146ee934afc39c9b6deb0f", "expected_error": null }, { @@ -252,8 +252,20 @@ "public_key_hex": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c", "private_key_seed_hex": "0707070707070707070707070707070707070707070707070707070707070707", "payload_hex": "7265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f6164", - "cose_sign1_hex": "d284582aa201270458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d3037a058247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f61645840f949f58016b8c10a576646368653e546c433d7336c86d77ab9f7d29de2ce4b338f8624426d008aacc657104c5feb3dc56bb8adfd766c5d1f5fe8a361ea488302", + "cose_sign1_hex": "d284582aa201320458247265616c6c796d652d636f73652d656432353531392d766563746f722d323032362d3037a058247265616c6c796d6520706f727461626c6520636f7365207369676e31207061796c6f616458408dadfef0fcd78603a7fb1623c1aa87031daea6a8ea95a8a2420007b3ed1a7933b46cf6a5a1ae138e7847ed0a572440e80c9352955fa72a820f7d4000065dcb0d", "expected_error": null + }, + { + "id": "cose-sign1-ed25519-node-openssl", + "operation": "verify_attached", + "algorithm": "Ed25519", + "kid_hex": "6e6f64652d6f70656e73736c2d726663383033322d31", + "public_key_hex": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", + "private_key_seed_hex": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", + "payload_hex": "", + "cose_sign1_hex": "84581ba2013204566e6f64652d6f70656e73736c2d726663383033322d31a04058401483ba346476f0d8abaf786fd5e2025d7509830c8534f4841740b6500f67462f42a5f23dbfb2d9bfabea89f7bb7f5628391465f2a3e3e1752857e5fcf0d7000b", + "expected_error": null, + "provenance": "node_open_ssl_rfc8032" } ] } diff --git a/vectors/manifest.json b/vectors/manifest.json new file mode 100644 index 0000000..a7e4044 --- /dev/null +++ b/vectors/manifest.json @@ -0,0 +1,40 @@ +{ + "schema": "reallyme.cose.conformance.vector_manifest.v1", + "suites": [ + { + "id": "cose-sign1", + "source": "ReallyMe deterministic fixture", + "path": "cose-sign1.json", + "case_count": 24, + "sha256": "62ec6a86f7d6e99d5f48614a96d707ad2649855d870613fd3418b80daca008e9" + }, + { + "id": "cose-key", + "source": "ReallyMe generated public-key fixture", + "path": "cose-key.json", + "case_count": 6, + "sha256": "0187a4a9f7cc8d64fe69b8259ac692c03d43b8582fa79a5d505e8d7c1b10e7a1" + }, + { + "id": "cose-sign1-pq", + "source": "ReallyMe deterministic ML-DSA fixture with independent COSE-layer audit", + "path": "cose-sign1-pq.json", + "case_count": 7, + "sha256": "ce057de89c273a6bb86541d5cc790f09837aabf3d75071d7e40e3536644ecee2" + }, + { + "id": "cose-key-pq", + "source": "ReallyMe generated PQ AKP fixture with independent COSE-layer audit", + "path": "cose-key-pq.json", + "case_count": 6, + "sha256": "fb64a87cac7ceeeefb4f2fff94fedfe0720a6d59db94ef3917f321265609d434" + }, + { + "id": "cose-encrypt-ml-kem", + "source": "ReallyMe deterministic ML-KEM COSE_Encrypt fixture", + "path": "cose-encrypt-ml-kem.json", + "case_count": 6, + "sha256": "beaf3b1e2099f59b94d4c8f27dab5728a0e2ec91e6b4ae87fa7fc6a864e9a331" + } + ] +}