├── .cz.yaml ├── .editorconfig ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── repo_policies │ └── BOT_APPROVED_FILES └── workflows │ ├── build-and-test.yml │ ├── commitizen.yml │ ├── create-release-pr.yml │ ├── e2e-tests.yml │ ├── generate-changelog.yml │ └── release.yml ├── .gitignore ├── .node-version ├── .npmrc ├── .pre-commit-config.yaml ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── dfx.json ├── examples ├── certification │ └── certified-counter │ │ ├── README.md │ │ └── src │ │ ├── backend │ │ ├── Cargo.toml │ │ ├── backend.did │ │ └── src │ │ │ └── lib.rs │ │ └── frontend │ │ ├── .ic-assets.json │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ ├── assets │ │ │ ├── favicon.ico │ │ │ └── logo.svg │ │ ├── index.ts │ │ └── main.css │ │ ├── tsconfig.json │ │ └── vite.config.mts └── http-certification │ ├── assets │ ├── README.md │ └── src │ │ ├── backend │ │ ├── Cargo.toml │ │ ├── backend.did │ │ └── src │ │ │ └── lib.rs │ │ ├── frontend │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── App.module.css │ │ │ ├── App.tsx │ │ │ ├── assets │ │ │ │ └── favicon.ico │ │ │ ├── index.css │ │ │ ├── index.tsx │ │ │ └── logo.svg │ │ ├── tsconfig.json │ │ └── vite.config.ts │ │ └── tests │ │ ├── jest.config.ts │ │ ├── package.json │ │ ├── src │ │ ├── http.spec.ts │ │ └── wasm.ts │ │ └── tsconfig.json │ ├── custom-assets │ ├── README.md │ └── src │ │ ├── backend │ │ ├── Cargo.toml │ │ ├── backend.did │ │ └── src │ │ │ └── lib.rs │ │ ├── frontend │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── App.module.css │ │ │ ├── App.tsx │ │ │ ├── assets │ │ │ │ └── favicon.ico │ │ │ ├── index.css │ │ │ ├── index.tsx │ │ │ └── logo.svg │ │ ├── tsconfig.json │ │ └── vite.config.ts │ │ └── tests │ │ ├── jest.config.ts │ │ ├── package.json │ │ ├── src │ │ ├── http.spec.ts │ │ └── wasm.ts │ │ └── tsconfig.json │ ├── json-api │ ├── README.md │ └── src │ │ ├── backend │ │ ├── Cargo.toml │ │ ├── backend.did │ │ └── src │ │ │ ├── lib.rs │ │ │ └── types.rs │ │ └── tests │ │ ├── jest.config.ts │ │ ├── package.json │ │ ├── src │ │ ├── request.ts │ │ ├── response.ts │ │ ├── todos.spec.ts │ │ └── wasm.ts │ │ └── tsconfig.json │ ├── skip-certification │ ├── README.md │ └── src │ │ ├── backend │ │ ├── Cargo.toml │ │ ├── backend.did │ │ └── src │ │ │ └── lib.rs │ │ └── tests │ │ ├── jest.config.ts │ │ ├── package.json │ │ ├── src │ │ ├── http.spec.ts │ │ └── wasm.ts │ │ └── tsconfig.json │ └── upgrade-to-update-call │ ├── README.md │ └── src │ ├── backend.did │ ├── motoko-backend │ └── main.mo │ ├── rust-backend │ ├── Cargo.toml │ └── src │ │ └── lib.rs │ └── tests │ ├── jest.config.ts │ ├── package.json │ ├── src │ ├── http.spec.ts │ └── wasm.ts │ └── tsconfig.json ├── package.json ├── packages ├── certificate-verification-js │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── error.ts │ │ ├── index.spec.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── tsconfig.types.json │ └── vite.config.ts ├── ic-asset-certification │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ ├── src │ │ ├── asset.rs │ │ ├── asset_config.rs │ │ ├── asset_map.rs │ │ ├── asset_router.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ └── types.rs │ └── tests │ │ ├── common │ │ └── mod.rs │ │ └── large_assets.rs ├── ic-cbor │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ └── src │ │ ├── cbor_parse_certificate.rs │ │ ├── cbor_parse_hash_tree.rs │ │ ├── cbor_parser.rs │ │ ├── error.rs │ │ └── lib.rs ├── ic-certificate-verification │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ └── src │ │ ├── certificate_verification.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ └── signature_verification │ │ ├── mod.rs │ │ ├── reproducible_rng.rs │ │ ├── signature_cache.rs │ │ └── tests.rs ├── ic-certification-testing-wasm │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ ├── package.json │ └── src │ │ ├── certificate_builder.rs │ │ └── lib.rs ├── ic-certification-testing │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ └── src │ │ ├── certificate.rs │ │ ├── certificate_builder.rs │ │ ├── encoding.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── signature.rs │ │ └── tree.rs ├── ic-certification │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ └── src │ │ ├── certificate.rs │ │ ├── hash_tree │ │ ├── mod.rs │ │ └── tests.rs │ │ ├── lib.rs │ │ ├── nested_rb_tree.rs │ │ └── rb_tree │ │ ├── mod.rs │ │ └── tests.rs ├── ic-http-certification-tests │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ └── tests │ │ ├── v1_response_verification.rs │ │ ├── v2_response_verification_certification_scenarios.rs │ │ ├── v2_response_verification_happy_path.rs │ │ └── v2_response_verification_sad_path.rs ├── ic-http-certification │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ └── src │ │ ├── cel │ │ ├── cel_builder.rs │ │ ├── cel_types.rs │ │ ├── create_cel_expr.rs │ │ ├── fixtures.rs │ │ └── mod.rs │ │ ├── error.rs │ │ ├── hash │ │ ├── mod.rs │ │ ├── request_hash.rs │ │ └── response_hash.rs │ │ ├── http │ │ ├── header_field.rs │ │ ├── http_request.rs │ │ ├── http_response.rs │ │ └── mod.rs │ │ ├── lib.rs │ │ ├── tree │ │ ├── certification.rs │ │ ├── certification_tree.rs │ │ ├── certification_tree_entry.rs │ │ ├── certification_tree_path.rs │ │ └── mod.rs │ │ └── utils │ │ ├── mod.rs │ │ ├── response_header.rs │ │ ├── skip_certification.rs │ │ └── wildcard_paths.rs ├── ic-representation-independent-hash │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ └── src │ │ ├── lib.rs │ │ └── representation_independent_hash.rs ├── ic-response-verification-test-utils │ ├── Cargo.toml │ ├── LICENSE │ └── src │ │ ├── asset_tree.rs │ │ ├── certificate.rs │ │ ├── encoding.rs │ │ ├── hash.rs │ │ ├── lib.rs │ │ ├── timestamp.rs │ │ ├── utils.rs │ │ └── v2_certificate_fixture.rs ├── ic-response-verification-tests │ ├── LICENSE │ ├── README.md │ └── src │ │ ├── frontend │ │ ├── .ic-assets.json │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── assets │ │ │ │ ├── a │ │ │ │ │ ├── b │ │ │ │ │ │ └── something.txt │ │ │ │ │ └── c │ │ │ │ │ │ └── else.txt │ │ │ │ ├── another sample asset.txt │ │ │ │ ├── capture-d’écran-2023-10-26-à.txt │ │ │ │ ├── favicon.ico │ │ │ │ ├── hello │ │ │ │ │ └── index.html │ │ │ │ ├── logo2.svg │ │ │ │ ├── main.css │ │ │ │ ├── sample-asset.txt │ │ │ │ └── world.html │ │ │ └── index.js │ │ ├── tsconfig.json │ │ └── vite.config.mts │ │ ├── rust-tests │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── agent.rs │ │ │ └── main.rs │ │ └── wasm-tests │ │ ├── package.json │ │ ├── src │ │ ├── http-interface │ │ │ ├── canister_http_interface.did │ │ │ ├── canister_http_interface.ts │ │ │ └── canister_http_interface_types.d.ts │ │ └── main.ts │ │ └── tsconfig.json ├── ic-response-verification-wasm │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ ├── package.json │ └── src │ │ ├── lib.rs │ │ ├── request.rs │ │ └── response.rs └── ic-response-verification │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ └── src │ ├── base64.rs │ ├── cel │ ├── ast_mapping.rs │ ├── error.rs │ ├── mod.rs │ ├── parser.rs │ └── tests.rs │ ├── error.rs │ ├── lib.rs │ ├── test_utils.rs │ ├── types │ ├── mod.rs │ ├── verification_result.rs │ └── verified_response.rs │ ├── validation │ ├── common_validation.rs │ ├── mod.rs │ ├── v1_validation.rs │ └── v2_validation.rs │ └── verification │ ├── body.rs │ ├── certificate_header.rs │ ├── certificate_header_field.rs │ ├── mod.rs │ └── verify_request_response_pair.rs ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── rust-toolchain.toml ├── scripts ├── e2e.sh └── package.sh └── tsconfig.json /.cz.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | commitizen: 3 | name: cz_conventional_commits 4 | tag_format: $version 5 | version: 3.0.3 6 | version_files: 7 | - Cargo.toml 8 | - packages/certificate-verification-js/package.json:version 9 | - packages/ic-certification-testing-wasm/package.json:version 10 | - packages/ic-response-verification-wasm/package.json:version 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{rs,toml}] 12 | indent_size = 4 13 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @dfinity/trust 2 | -------------------------------------------------------------------------------- /.github/repo_policies/BOT_APPROVED_FILES: -------------------------------------------------------------------------------- 1 | .cz.yaml 2 | CHANGELOG.md 3 | Cargo.lock 4 | Cargo.toml 5 | packages/certificate-verification-js/package.json 6 | packages/ic-certification-testing-wasm/package.json 7 | packages/ic-response-verification-wasm/package.json 8 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: build_and_test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | env: 10 | SCCACHE_GHA_ENABLED: 'true' 11 | RUSTC_WRAPPER: 'sccache' 12 | 13 | jobs: 14 | build_and_test_rust: 15 | name: build_and_test_rust:required 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Run sccache-cache 22 | uses: mozilla-actions/sccache-action@v0.0.9 23 | 24 | - name: Build Cargo crates 25 | run: cargo build --release 26 | 27 | - name: Test Cargo crates 28 | run: cargo test --all-features 29 | 30 | - name: Build Cargo docs 31 | run: cargo doc --no-deps 32 | 33 | - name: Lint Rust 34 | run: cargo clippy --all-targets --all-features 35 | 36 | - name: Check Rust formatting 37 | run: cargo fmt --all -- --check 38 | 39 | build_and_test_js: 40 | name: build_and_test_js:required 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@v4 45 | 46 | - name: Setup PNPM 47 | uses: dfinity/ci-tools/actions/setup-pnpm@main 48 | 49 | - name: Setup DFX 50 | uses: dfinity/setup-dfx@main 51 | with: 52 | dfx-version: 'auto' 53 | 54 | - name: Run sccache-cache 55 | uses: mozilla-actions/sccache-action@v0.0.9 56 | 57 | # Triggers installation of the Rust toolchain 58 | # Must be done before wasm-pack is installed 59 | - name: Cargo metadata 60 | run: cargo metadata --format-version 1 61 | 62 | - name: Setup wasm-pack 63 | run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 64 | 65 | - name: Generate canister declarations 66 | run: dfx generate 67 | 68 | - name: Build NPM packages 69 | run: pnpm build 70 | 71 | - name: Build canisters 72 | run: dfx build --check 73 | 74 | - name: Test NPM packages 75 | run: pnpm test 76 | 77 | - name: Wasm test 78 | run: wasm-pack test --node packages/ic-response-verification --features=js 79 | 80 | - name: Check Typescript formatting 81 | run: pnpm run format:check 82 | -------------------------------------------------------------------------------- /.github/workflows/commitizen.yml: -------------------------------------------------------------------------------- 1 | name: commitizen 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | check_commit_messages: 8 | name: check_commit_messages:required 9 | uses: dfinity/ci-tools/.github/workflows/check-commit-messages.yaml@main 10 | with: 11 | target_branch: 'main' 12 | 13 | check_pr_title: 14 | name: check_pr_title:required 15 | uses: dfinity/ci-tools/.github/workflows/check-pr-title.yaml@main 16 | -------------------------------------------------------------------------------- /.github/workflows/create-release-pr.yml: -------------------------------------------------------------------------------- 1 | name: create_release_pr 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | SCCACHE_GHA_ENABLED: 'true' 7 | RUSTC_WRAPPER: 'sccache' 8 | 9 | jobs: 10 | bump_version: 11 | name: bump_version 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Create GitHub App Token 15 | uses: actions/create-github-app-token@v1 16 | id: generate_token 17 | with: 18 | app-id: ${{ vars.PR_AUTOMATION_BOT_PUBLIC_APP_ID }} 19 | private-key: ${{ secrets.PR_AUTOMATION_BOT_PUBLIC_PRIVATE_KEY }} 20 | 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Setup Python 27 | uses: dfinity/ci-tools/actions/setup-python@main 28 | 29 | - name: Setup Commitizen 30 | uses: dfinity/ci-tools/actions/setup-commitizen@main 31 | 32 | - name: Setup PNPM 33 | uses: dfinity/ci-tools/actions/setup-pnpm@main 34 | 35 | - name: Setup DFX 36 | uses: dfinity/setup-dfx@main 37 | 38 | - name: Run sccache-cache 39 | uses: mozilla-actions/sccache-action@v0.0.9 40 | 41 | # Triggers installation of the Rust toolchain 42 | # Must be done before wasm-pack is installed 43 | - name: Cargo metadata 44 | run: cargo metadata --format-version 1 45 | 46 | - name: Setup wasm-pack 47 | run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 48 | 49 | - name: Bump version 50 | id: bump_version 51 | uses: dfinity/ci-tools/actions/bump-version@main 52 | 53 | - name: Print Version 54 | run: echo "Bumping to version ${{ steps.bump_version.outputs.version }}" 55 | 56 | - name: Generate canister declarations 57 | run: dfx generate 58 | 59 | - name: Update Cargo.lock 60 | run: | 61 | cargo build 62 | pnpm build 63 | 64 | - name: Create Pull Request 65 | uses: dfinity/ci-tools/actions/create-pr@main 66 | with: 67 | token: ${{ steps.generate_token.outputs.token }} 68 | branch_name: 'release/${{ steps.bump_version.outputs.version }}' 69 | pull_request_title: 'chore: release ${{ steps.bump_version.outputs.version }}' 70 | pull_request_body: | 71 | After merging this PR, tag the merge commit with: 72 | ```shell 73 | git tag ${{ steps.bump_version.outputs.version }} 74 | git push origin ${{ steps.bump_version.outputs.version }} 75 | ``` 76 | commit_message: 'chore: release ${{ steps.bump_version.outputs.version }}' 77 | -------------------------------------------------------------------------------- /.github/workflows/e2e-tests.yml: -------------------------------------------------------------------------------- 1 | name: e2e_tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | env: 10 | SCCACHE_GHA_ENABLED: 'true' 11 | RUSTC_WRAPPER: 'sccache' 12 | 13 | jobs: 14 | e2e_tests: 15 | name: e2e_tests:required 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Setup PNPM 22 | uses: dfinity/ci-tools/actions/setup-pnpm@main 23 | 24 | - name: Run sccache-cache 25 | uses: mozilla-actions/sccache-action@v0.0.9 26 | 27 | - name: Setup e2e Deps Cache 28 | uses: actions/cache@v3 29 | with: 30 | path: tmp/ 31 | key: ${{ runner.os }}-tmp 32 | 33 | - name: Setup wasm-pack 34 | run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 35 | 36 | - name: e2e tests 37 | run: ./scripts/e2e.sh --use-latest-dfx 38 | -------------------------------------------------------------------------------- /.github/workflows/generate-changelog.yml: -------------------------------------------------------------------------------- 1 | name: generate_changelog 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | generate_changelog: 10 | uses: dfinity/ci-tools/.github/workflows/generate-changelog.yaml@main 11 | with: 12 | token_app_id: ${{ vars.PR_AUTOMATION_BOT_PUBLIC_APP_ID }} 13 | secrets: 14 | token_private_key: ${{ secrets.PR_AUTOMATION_BOT_PUBLIC_PRIVATE_KEY }} 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Various IDEs and Editors 2 | .vscode/ 3 | .idea/ 4 | **/*~ 5 | 6 | # Mac OSX temporary files 7 | .DS_Store 8 | **/.DS_Store 9 | 10 | # DFX 11 | .dfx/ 12 | .env 13 | 14 | # Rust 15 | .cargo/ 16 | target/ 17 | 18 | # NPM 19 | node_modules/ 20 | 21 | # Other build files 22 | declarations/ 23 | dist/ 24 | tmp/ 25 | pkg/ 26 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 22.12.0 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - hooks: 3 | - id: commitizen 4 | repo: https://github.com/commitizen-tools/commitizen 5 | rev: v2.35.0 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .cargo 3 | .dfx 4 | target 5 | dist 6 | declarations 7 | node_modules 8 | .DS_Store 9 | /tmp 10 | 11 | pnpm-lock.yaml 12 | 13 | # commitizen is formatting the .cz.yaml file in a way that Prettier does not like 14 | .cz.yaml 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "arrowParens": "avoid" 5 | } 6 | -------------------------------------------------------------------------------- /examples/certification/certified-counter/README.md: -------------------------------------------------------------------------------- 1 | # Certified Counter 2 | 3 | This example project demonstrates how to create a certification for non-replicated query call responses from a simple counter canister and verify that certification client side. 4 | 5 | ## Running the project locally 6 | 7 | From this project's directory: 8 | 9 | ```shell 10 | cd examples/certification/certified-counter 11 | ``` 12 | 13 | Start DFX: 14 | 15 | ```shell 16 | dfx start --background 17 | ``` 18 | 19 | Create canisters: 20 | 21 | ```shell 22 | dfx canister create --all 23 | ``` 24 | 25 | Generate backend canister bindings: 26 | 27 | ```shell 28 | dfx generate backend 29 | ``` 30 | 31 | Back to the root of repository: 32 | 33 | ```shell 34 | cd ../../ 35 | ``` 36 | 37 | Install pnpm dependencies: 38 | 39 | ```shell 40 | pnpm i 41 | ``` 42 | 43 | Build the `@dfinity/certificate-verification` package: 44 | 45 | ```shell 46 | pnpm run --filter @dfinity/certificate-verification build 47 | ``` 48 | 49 | Now change to this project's directory again: 50 | 51 | ```shell 52 | cd examples/certification/certified-counter 53 | ``` 54 | 55 | Build and deploy the canisters: 56 | 57 | ```shell 58 | dfx deploy 59 | ``` 60 | 61 | Print the web URL of the canister: 62 | 63 | ```shell 64 | echo "http://$(dfx canister id certification_certified_counter_frontend).localhost:$(dfx info webserver-port)" 65 | ``` 66 | 67 | Now you can open that URL in your web browser. 68 | -------------------------------------------------------------------------------- /examples/certification/certified-counter/src/backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "certification_certified_counter_backend" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | anyhow.workspace = true 11 | candid.workspace = true 12 | ic-cdk.workspace = true 13 | ic-certification = { workspace = true, features = ["serde"] } 14 | serde.workspace = true 15 | serde_cbor.workspace = true 16 | sha2.workspace = true 17 | -------------------------------------------------------------------------------- /examples/certification/certified-counter/src/backend/backend.did: -------------------------------------------------------------------------------- 1 | type certified_counter = record { 2 | "count" : nat32; 3 | "certificate" : blob; 4 | "witness" : blob; 5 | }; 6 | 7 | service : { 8 | "inc_count" : () -> (); 9 | "get_count" : () -> (certified_counter) query; 10 | }; 11 | -------------------------------------------------------------------------------- /examples/certification/certified-counter/src/backend/src/lib.rs: -------------------------------------------------------------------------------- 1 | use candid::CandidType; 2 | use ic_cdk::*; 3 | use ic_certification::{AsHashTree, RbTree}; 4 | use serde::Serialize; 5 | use sha2::{Digest, Sha256}; 6 | use std::cell::*; 7 | 8 | thread_local! { 9 | static COUNTER: Cell = Cell::new(0); 10 | static TREE: RefCell>> = RefCell::new(RbTree::new()); 11 | } 12 | 13 | pub fn hash(data: &[u8]) -> [u8; 32] { 14 | let mut hasher = Sha256::new(); 15 | hasher.update(data); 16 | hasher.finalize().into() 17 | } 18 | 19 | #[update] 20 | fn inc_count() { 21 | let count = COUNTER.with(|counter| { 22 | let count = counter.get() + 1; 23 | counter.set(count); 24 | count 25 | }); 26 | 27 | TREE.with(|tree| { 28 | let mut tree = tree.borrow_mut(); 29 | tree.insert("count", hash(&count.to_be_bytes()).to_vec()); 30 | 31 | ic_cdk::api::set_certified_data(&tree.root_hash()); 32 | }) 33 | } 34 | 35 | #[derive(CandidType)] 36 | struct CertifiedCounter { 37 | count: u32, 38 | certificate: Vec, 39 | witness: Vec, 40 | } 41 | 42 | fn get_count_witness() -> anyhow::Result> { 43 | TREE.with(|tree| { 44 | let tree = tree.borrow(); 45 | let mut witness = vec![]; 46 | let mut witness_serializer = serde_cbor::Serializer::new(&mut witness); 47 | 48 | witness_serializer.self_describe()?; 49 | 50 | tree.witness(b"count") 51 | .serialize(&mut witness_serializer) 52 | .unwrap(); 53 | 54 | Ok(witness) 55 | }) 56 | } 57 | 58 | #[query] 59 | fn get_count() -> CertifiedCounter { 60 | let certificate = ic_cdk::api::data_certificate().expect("No data certificate available"); 61 | 62 | let witness = match get_count_witness() { 63 | Ok(tree) => tree, 64 | Err(err) => { 65 | ic_cdk::trap(&format!("Error getting count witness: {:?}", err)); 66 | } 67 | }; 68 | 69 | let count = COUNTER.with(|counter| counter.get()); 70 | 71 | CertifiedCounter { 72 | count, 73 | certificate, 74 | witness, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /examples/certification/certified-counter/src/frontend/.ic-assets.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "match": "**/*", 4 | "security_policy": "hardened", 5 | "headers": { 6 | "content-security-policy": "default-src 'self'; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content; script-src 'self' 'unsafe-eval'; connect-src 'self' http://localhost:8000", 7 | "cross-origin-embedder-policy": "require-corp" 8 | } 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /examples/certification/certified-counter/src/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Certified Counter 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 |
17 |
18 | 19 |
20 | 23 | 24 |
25 |
26 |
27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /examples/certification/certified-counter/src/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "certification-certified-counter-frontend", 3 | "scripts": { 4 | "start": "vite", 5 | "build": "vite build" 6 | }, 7 | "dependencies": { 8 | "@dfinity/certificate-verification": "workspace:*" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/certification/certified-counter/src/frontend/src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/response-verification/b4c8c472e5ca98502f846ef3b860d1f648ced68d/examples/certification/certified-counter/src/frontend/src/assets/favicon.ico -------------------------------------------------------------------------------- /examples/certification/certified-counter/src/frontend/src/index.ts: -------------------------------------------------------------------------------- 1 | import { verifyCertification } from '@dfinity/certificate-verification'; 2 | import { Actor, HttpAgent, compare, lookup_path } from '@dfinity/agent'; 3 | import { Principal } from '@dfinity/principal'; 4 | import { 5 | idlFactory, 6 | _SERVICE, 7 | } from '../../declarations/certification_certified_counter_backend.did'; 8 | 9 | const canisterId = 10 | process.env.CANISTER_ID_CERTIFICATION_CERTIFIED_COUNTER_BACKEND ?? ''; 11 | const dfxNetwork = process.env.DFX_NETWORK ?? ''; 12 | 13 | const agent = new HttpAgent(); 14 | 15 | if (dfxNetwork !== 'ic') { 16 | agent.fetchRootKey().catch(err => { 17 | console.warn( 18 | 'Unable to fetch root key. Check to ensure that your local replica is running', 19 | ); 20 | console.error(err); 21 | }); 22 | } 23 | 24 | // Creates an actor with using the candid interface and the HttpAgent 25 | const backend = Actor.createActor<_SERVICE>(idlFactory, { 26 | agent, 27 | canisterId, 28 | }); 29 | 30 | const buttonElement = document.querySelector('#counter-inc'); 31 | if (!buttonElement) { 32 | throw new Error('Counter inc element not found'); 33 | } 34 | 35 | const countElement = document.querySelector('#counter-count'); 36 | if (!countElement) { 37 | throw new Error('Counter count element not found'); 38 | } 39 | 40 | async function hashUInt32( 41 | value: number, 42 | littleEndian = false, 43 | ): Promise { 44 | const buffer = new ArrayBuffer(4); 45 | const view = new DataView(buffer); 46 | view.setUint32(0, value, littleEndian); 47 | return await crypto.subtle.digest('SHA-256', view); 48 | } 49 | 50 | buttonElement.addEventListener('click', async event => { 51 | event.preventDefault(); 52 | 53 | buttonElement.setAttribute('disabled', String(true)); 54 | await backend.inc_count(); 55 | const { count, certificate, witness } = await backend.get_count(); 56 | buttonElement.removeAttribute('disabled'); 57 | 58 | const agent = new HttpAgent(); 59 | await agent.fetchRootKey(); 60 | const tree = await verifyCertification({ 61 | canisterId: Principal.fromText(canisterId), 62 | encodedCertificate: new Uint8Array(certificate).buffer, 63 | encodedTree: new Uint8Array(witness).buffer, 64 | rootKey: agent.rootKey, 65 | maxCertificateTimeOffsetMs: 50000, 66 | }); 67 | 68 | const treeHash = lookup_path(['count'], tree); 69 | if (!treeHash) { 70 | throw new Error('Count not found in tree'); 71 | } 72 | 73 | const responseHash = await hashUInt32(count); 74 | if (!(treeHash instanceof ArrayBuffer) || !equal(responseHash, treeHash)) { 75 | throw new Error('Count hash does not match'); 76 | } 77 | 78 | countElement.innerText = String(count); 79 | 80 | return false; 81 | }); 82 | 83 | function equal(a: ArrayBuffer, b: ArrayBuffer): boolean { 84 | return compare(a, b) === 0; 85 | } 86 | -------------------------------------------------------------------------------- /examples/certification/certified-counter/src/frontend/src/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | font-size: 1.5rem; 4 | } 5 | 6 | .logo { 7 | max-width: 50vw; 8 | max-height: 25vw; 9 | display: block; 10 | margin: auto; 11 | } 12 | 13 | .counter { 14 | display: flex; 15 | justify-content: center; 16 | gap: 0.5em; 17 | flex-flow: row wrap; 18 | max-width: 40vw; 19 | margin: auto; 20 | align-items: baseline; 21 | } 22 | 23 | .counter-inc { 24 | padding: 5px 20px; 25 | margin: 10px auto; 26 | float: right; 27 | } 28 | 29 | .counter-count { 30 | margin: 10px auto; 31 | padding: 10px 60px; 32 | border: 1px solid #222; 33 | } 34 | 35 | .counter-count:empty { 36 | display: none; 37 | } 38 | -------------------------------------------------------------------------------- /examples/certification/certified-counter/src/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["vite/client"], 5 | "noEmit": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/certification/certified-counter/src/frontend/vite.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from 'vite'; 2 | import checker from 'vite-plugin-checker'; 3 | import { viteStaticCopy } from 'vite-plugin-static-copy'; 4 | 5 | export default defineConfig(({ mode }) => { 6 | const env = loadEnv(mode, '../../../../../', ''); 7 | 8 | return { 9 | plugins: [ 10 | checker({ typescript: true }), 11 | viteStaticCopy({ 12 | targets: [ 13 | { 14 | src: '.ic-assets.json', 15 | dest: '.', 16 | }, 17 | ], 18 | }), 19 | ], 20 | optimizeDeps: { 21 | esbuildOptions: { 22 | define: { 23 | global: 'globalThis', 24 | }, 25 | }, 26 | }, 27 | define: { 28 | 'process.env': { 29 | CANISTER_ID_CERTIFICATION_CERTIFIED_COUNTER_BACKEND: 30 | env.CANISTER_ID_CERTIFICATION_CERTIFIED_COUNTER_BACKEND, 31 | DFX_NETWORK: env.DFX_NETWORK, 32 | }, 33 | }, 34 | server: { 35 | proxy: { 36 | '/api': 'http://127.0.0.1:8000', 37 | }, 38 | }, 39 | }; 40 | }); 41 | -------------------------------------------------------------------------------- /examples/http-certification/assets/src/backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "http_certification_assets_backend" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | candid.workspace = true 11 | ic-cdk.workspace = true 12 | ic-http-certification.workspace = true 13 | ic-asset-certification.workspace = true 14 | include_dir = { version = "0.7", features = ["glob"] } 15 | 16 | # The following dependencies are only necessary for JSON serialization of metrics 17 | serde.workspace = true 18 | serde_json.workspace = true 19 | -------------------------------------------------------------------------------- /examples/http-certification/assets/src/backend/backend.did: -------------------------------------------------------------------------------- 1 | type HeaderField = record { text; text }; 2 | 3 | type HttpRequest = record { 4 | method : text; 5 | url : text; 6 | headers : vec HeaderField; 7 | body : blob; 8 | certificate_version : opt nat16; 9 | }; 10 | 11 | type HttpResponse = record { 12 | status_code : nat16; 13 | headers : vec HeaderField; 14 | body : blob; 15 | }; 16 | 17 | service : { 18 | http_request : (request : HttpRequest) -> (HttpResponse) query; 19 | }; 20 | -------------------------------------------------------------------------------- /examples/http-certification/assets/src/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Solid App 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/http-certification/assets/src/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "http-certification-assets-frontend", 3 | "scripts": { 4 | "start": "vite", 5 | "build": "vite build" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/http-certification/assets/src/frontend/src/App.module.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .logo { 6 | animation: logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | pointer-events: none; 9 | } 10 | 11 | .header { 12 | background-color: #282c34; 13 | min-height: 100vh; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: center; 18 | font-size: calc(10px + 2vmin); 19 | color: white; 20 | } 21 | 22 | .link { 23 | color: #b318f0; 24 | } 25 | 26 | @keyframes logo-spin { 27 | from { 28 | transform: rotate(0deg); 29 | } 30 | to { 31 | transform: rotate(360deg); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/http-certification/assets/src/frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import type { Component } from 'solid-js'; 2 | 3 | import logo from './logo.svg'; 4 | import styles from './App.module.css'; 5 | 6 | const App: Component = () => { 7 | return ( 8 |
9 |
10 | logo 11 |

12 | Edit src/App.tsx and save to reload. 13 |

14 | 20 | Learn Solid 21 | 22 |
23 |
24 | ); 25 | }; 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /examples/http-certification/assets/src/frontend/src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/response-verification/b4c8c472e5ca98502f846ef3b860d1f648ced68d/examples/http-certification/assets/src/frontend/src/assets/favicon.ico -------------------------------------------------------------------------------- /examples/http-certification/assets/src/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /examples/http-certification/assets/src/frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | /* @refresh reload */ 2 | import { render } from 'solid-js/web'; 3 | 4 | import './index.css'; 5 | import App from './App'; 6 | 7 | const root = document.getElementById('root'); 8 | 9 | if (import.meta.env.DEV && !(root instanceof HTMLElement)) { 10 | throw new Error( 11 | 'Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?', 12 | ); 13 | } 14 | 15 | render(() => , root!); 16 | -------------------------------------------------------------------------------- /examples/http-certification/assets/src/frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/http-certification/assets/src/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../../tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "jsxImportSource": "solid-js", 6 | "types": ["vite/client"], 7 | "noEmit": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/http-certification/assets/src/frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import solidPlugin from 'vite-plugin-solid'; 3 | 4 | // import the compression plugin 5 | import viteCompressionPlugin from 'vite-plugin-compression'; 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | solidPlugin(), 10 | 11 | // setup Gzip compression 12 | viteCompressionPlugin({ 13 | algorithm: 'gzip', 14 | // this extension will be referenced later in the canister code 15 | ext: '.gz', 16 | // ensure to not delete the original files 17 | deleteOriginFile: false, 18 | threshold: 0, 19 | }), 20 | 21 | // setup Brotli compression 22 | viteCompressionPlugin({ 23 | algorithm: 'brotliCompress', 24 | // this extension will be referenced later in the canister code 25 | ext: '.br', 26 | // ensure to not delete the original files 27 | deleteOriginFile: false, 28 | threshold: 0, 29 | }), 30 | ], 31 | server: { 32 | port: 3000, 33 | }, 34 | build: { 35 | target: 'esnext', 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /examples/http-certification/assets/src/tests/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | 3 | const config: Config = { 4 | watch: false, 5 | preset: 'ts-jest/presets/js-with-ts', 6 | testEnvironment: 'node', 7 | verbose: true, 8 | testTimeout: 30_000, 9 | }; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /examples/http-certification/assets/src/tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "http-certification-assets-tests", 3 | "private": true, 4 | "scripts": { 5 | "test": "jest" 6 | }, 7 | "devDependencies": { 8 | "@dfinity/response-verification": "workspace:*" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/http-certification/assets/src/tests/src/wasm.ts: -------------------------------------------------------------------------------- 1 | import { type CanisterFixture, type PocketIc } from '@dfinity/pic'; 2 | import { resolve } from 'node:path'; 3 | import { 4 | type _SERVICE, 5 | idlFactory, 6 | } from '../../declarations/http_certification_assets_backend.did'; 7 | 8 | export const BACKEND_WASM_PATH = resolve( 9 | __dirname, 10 | '..', 11 | '..', 12 | '..', 13 | '..', 14 | '..', 15 | '..', 16 | 'target', 17 | 'wasm32-unknown-unknown', 18 | 'release', 19 | 'http_certification_assets_backend.wasm', 20 | ); 21 | 22 | export async function setupBackendCanister( 23 | pic: PocketIc, 24 | initialDate: Date, 25 | ): Promise> { 26 | await pic.setTime(initialDate.getTime()); 27 | 28 | return await pic.setupCanister<_SERVICE>({ 29 | idlFactory, 30 | wasm: BACKEND_WASM_PATH, 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /examples/http-certification/assets/src/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../../tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "types": ["jest", "node"] 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /examples/http-certification/custom-assets/src/backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "http_certification_custom_assets_backend" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | candid.workspace = true 11 | ic-cdk.workspace = true 12 | ic-http-certification.workspace = true 13 | lazy_static.workspace = true 14 | include_dir = { version = "0.7", features = ["glob"] } 15 | 16 | # The following dependencies are only necessary for JSON serialization of metrics 17 | serde.workspace = true 18 | serde_json.workspace = true 19 | -------------------------------------------------------------------------------- /examples/http-certification/custom-assets/src/backend/backend.did: -------------------------------------------------------------------------------- 1 | type HeaderField = record { text; text }; 2 | 3 | type HttpRequest = record { 4 | method : text; 5 | url : text; 6 | headers : vec HeaderField; 7 | body : blob; 8 | certificate_version : opt nat16; 9 | }; 10 | 11 | type HttpResponse = record { 12 | status_code : nat16; 13 | headers : vec HeaderField; 14 | body : blob; 15 | }; 16 | 17 | service : { 18 | http_request : (request : HttpRequest) -> (HttpResponse) query; 19 | }; 20 | -------------------------------------------------------------------------------- /examples/http-certification/custom-assets/src/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Solid App 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/http-certification/custom-assets/src/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "http-certification-custom-assets-frontend", 3 | "scripts": { 4 | "start": "vite", 5 | "build": "vite build" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/http-certification/custom-assets/src/frontend/src/App.module.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .logo { 6 | animation: logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | pointer-events: none; 9 | } 10 | 11 | .header { 12 | background-color: #282c34; 13 | min-height: 100vh; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: center; 18 | font-size: calc(10px + 2vmin); 19 | color: white; 20 | } 21 | 22 | .link { 23 | color: #b318f0; 24 | } 25 | 26 | @keyframes logo-spin { 27 | from { 28 | transform: rotate(0deg); 29 | } 30 | to { 31 | transform: rotate(360deg); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/http-certification/custom-assets/src/frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import type { Component } from 'solid-js'; 2 | 3 | import logo from './logo.svg'; 4 | import styles from './App.module.css'; 5 | 6 | const App: Component = () => { 7 | return ( 8 |
9 |
10 | logo 11 |

12 | Edit src/App.tsx and save to reload. 13 |

14 | 20 | Learn Solid 21 | 22 |
23 |
24 | ); 25 | }; 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /examples/http-certification/custom-assets/src/frontend/src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/response-verification/b4c8c472e5ca98502f846ef3b860d1f648ced68d/examples/http-certification/custom-assets/src/frontend/src/assets/favicon.ico -------------------------------------------------------------------------------- /examples/http-certification/custom-assets/src/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /examples/http-certification/custom-assets/src/frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | /* @refresh reload */ 2 | import { render } from 'solid-js/web'; 3 | 4 | import './index.css'; 5 | import App from './App'; 6 | 7 | const root = document.getElementById('root'); 8 | 9 | if (import.meta.env.DEV && !(root instanceof HTMLElement)) { 10 | throw new Error( 11 | 'Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?', 12 | ); 13 | } 14 | 15 | render(() => , root!); 16 | -------------------------------------------------------------------------------- /examples/http-certification/custom-assets/src/frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/http-certification/custom-assets/src/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../../tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "jsxImportSource": "solid-js", 6 | "types": ["vite/client"], 7 | "noEmit": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/http-certification/custom-assets/src/frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import solidPlugin from 'vite-plugin-solid'; 3 | 4 | // import the compression plugin 5 | import viteCompressionPlugin from 'vite-plugin-compression'; 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | solidPlugin(), 10 | 11 | // setup Gzip compression 12 | viteCompressionPlugin({ 13 | algorithm: 'gzip', 14 | // this extension will be referenced later in the canister code 15 | ext: '.gzip', 16 | // ensure to not delete the original files 17 | deleteOriginFile: false, 18 | threshold: 0, 19 | }), 20 | 21 | // setup Brotli compression 22 | viteCompressionPlugin({ 23 | algorithm: 'brotliCompress', 24 | // this extension will be referenced later in the canister code 25 | ext: '.br', 26 | // ensure to not delete the original files 27 | deleteOriginFile: false, 28 | threshold: 0, 29 | }), 30 | ], 31 | server: { 32 | port: 3000, 33 | }, 34 | build: { 35 | target: 'esnext', 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /examples/http-certification/custom-assets/src/tests/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | 3 | const config: Config = { 4 | watch: false, 5 | preset: 'ts-jest/presets/js-with-ts', 6 | testEnvironment: 'node', 7 | verbose: true, 8 | testTimeout: 30_000, 9 | }; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /examples/http-certification/custom-assets/src/tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "http-certification-custom-assets-tests", 3 | "private": true, 4 | "scripts": { 5 | "test": "jest" 6 | }, 7 | "devDependencies": { 8 | "@dfinity/response-verification": "workspace:*" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/http-certification/custom-assets/src/tests/src/wasm.ts: -------------------------------------------------------------------------------- 1 | import { type CanisterFixture, type PocketIc } from '@dfinity/pic'; 2 | import { resolve } from 'node:path'; 3 | import { 4 | type _SERVICE, 5 | idlFactory, 6 | } from '../../declarations/http_certification_custom_assets_backend.did'; 7 | 8 | export const BACKEND_WASM_PATH = resolve( 9 | __dirname, 10 | '..', 11 | '..', 12 | '..', 13 | '..', 14 | '..', 15 | '..', 16 | 'target', 17 | 'wasm32-unknown-unknown', 18 | 'release', 19 | 'http_certification_custom_assets_backend.wasm', 20 | ); 21 | 22 | export async function setupBackendCanister( 23 | pic: PocketIc, 24 | initialDate: Date, 25 | ): Promise> { 26 | await pic.setTime(initialDate.getTime()); 27 | 28 | return await pic.setupCanister<_SERVICE>({ 29 | idlFactory, 30 | wasm: BACKEND_WASM_PATH, 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /examples/http-certification/custom-assets/src/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../../tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "types": ["jest", "node"] 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /examples/http-certification/json-api/src/backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "http_certification_json_api_backend" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | candid.workspace = true 11 | ic-cdk.workspace = true 12 | serde.workspace = true 13 | ic-http-certification.workspace = true 14 | lazy_static.workspace = true 15 | serde_json.workspace = true 16 | matchit = "0.8" 17 | -------------------------------------------------------------------------------- /examples/http-certification/json-api/src/backend/backend.did: -------------------------------------------------------------------------------- 1 | type HeaderField = record { text; text }; 2 | 3 | type HttpRequest = record { 4 | method : text; 5 | url : text; 6 | headers : vec HeaderField; 7 | body : blob; 8 | certificate_version : opt nat16; 9 | }; 10 | 11 | type HttpResponse = record { 12 | status_code : nat16; 13 | headers : vec HeaderField; 14 | body : blob; 15 | upgrade : opt bool; 16 | }; 17 | 18 | type ErrResponse = record { 19 | code : nat16; 20 | message : text; 21 | }; 22 | 23 | type TodoItem = record { 24 | id : nat32; 25 | title : text; 26 | completed : bool; 27 | }; 28 | 29 | type CreateTodoItemRequest = record { 30 | title : text; 31 | }; 32 | 33 | type CreateTodoItemResponse = variant { 34 | ok : record { 35 | data : TodoItem; 36 | }; 37 | err : ErrResponse; 38 | }; 39 | 40 | type UpdateTodoItemRequest = variant { 41 | title : text; 42 | completed : bool; 43 | }; 44 | 45 | type UpdateTodoItemResponse = variant { 46 | ok : record { 47 | data : null; 48 | }; 49 | err : ErrResponse; 50 | }; 51 | 52 | type DeleteTodoItemResponse = variant { 53 | ok : record { 54 | data : null; 55 | }; 56 | err : ErrResponse; 57 | }; 58 | 59 | type ListTodoItemsResponse = variant { 60 | ok : record { 61 | data : vec TodoItem; 62 | }; 63 | err : ErrResponse; 64 | }; 65 | 66 | service : { 67 | http_request : (request : HttpRequest) -> (HttpResponse) query; 68 | http_request_update : (request : HttpRequest) -> (HttpResponse); 69 | }; 70 | -------------------------------------------------------------------------------- /examples/http-certification/json-api/src/backend/src/types.rs: -------------------------------------------------------------------------------- 1 | use ic_http_certification::{HttpRequest, HttpResponse, StatusCode}; 2 | use matchit::Params; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Clone, Serialize)] 6 | pub struct TodoItem { 7 | pub id: u32, 8 | pub title: String, 9 | pub completed: bool, 10 | } 11 | 12 | #[derive(Debug, Clone, Serialize)] 13 | pub enum ApiResponse<'a, T = ()> { 14 | #[serde(rename = "ok")] 15 | Ok { data: &'a T }, 16 | #[serde(rename = "err")] 17 | Err { code: u16, message: String }, 18 | } 19 | 20 | impl<'a, T: Serialize> ApiResponse<'a, T> { 21 | pub fn ok(data: &'a T) -> ApiResponse<'a, T> { 22 | Self::Ok { data } 23 | } 24 | 25 | pub fn not_found() -> Self { 26 | Self::err(StatusCode::NOT_FOUND, "Not found".to_string()) 27 | } 28 | 29 | pub fn not_allowed() -> Self { 30 | Self::err( 31 | StatusCode::METHOD_NOT_ALLOWED, 32 | "Method not allowed".to_string(), 33 | ) 34 | } 35 | 36 | fn err(code: StatusCode, message: String) -> Self { 37 | Self::Err { 38 | code: code.as_u16(), 39 | message, 40 | } 41 | } 42 | 43 | pub fn encode(&self) -> Vec { 44 | serde_json::to_vec(self).expect("Failed to serialize value") 45 | } 46 | } 47 | 48 | #[derive(Debug, Clone, Deserialize)] 49 | pub struct CreateTodoItemRequest { 50 | pub title: String, 51 | } 52 | 53 | pub type CreateTodoItemResponse<'a> = ApiResponse<'a, TodoItem>; 54 | 55 | #[derive(Debug, Clone, Deserialize)] 56 | pub struct UpdateTodoItemRequest { 57 | pub title: Option, 58 | pub completed: Option, 59 | } 60 | 61 | pub type UpdateTodoItemResponse<'a> = ApiResponse<'a, ()>; 62 | 63 | pub type DeleteTodoItemResponse<'a> = ApiResponse<'a, ()>; 64 | 65 | pub type ListTodosResponse<'a> = ApiResponse<'a, Vec>; 66 | 67 | pub type ErrorResponse<'a> = ApiResponse<'a, ()>; 68 | 69 | pub type RouteHandler = for<'a> fn(&'a HttpRequest, &'a Params) -> HttpResponse<'static>; 70 | -------------------------------------------------------------------------------- /examples/http-certification/json-api/src/tests/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | 3 | const config: Config = { 4 | watch: false, 5 | preset: 'ts-jest/presets/js-with-ts', 6 | testEnvironment: 'node', 7 | verbose: true, 8 | testTimeout: 30_000, 9 | }; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /examples/http-certification/json-api/src/tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "http-certification-json-api-tests", 3 | "private": true, 4 | "scripts": { 5 | "test": "jest" 6 | }, 7 | "devDependencies": { 8 | "@dfinity/response-verification": "workspace:*" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/http-certification/json-api/src/tests/src/request.ts: -------------------------------------------------------------------------------- 1 | export const CERTIFICATE_VERSION = 2; 2 | 3 | export function jsonEncode(data: T): Uint8Array { 4 | return new TextEncoder().encode(JSON.stringify(data)); 5 | } 6 | -------------------------------------------------------------------------------- /examples/http-certification/json-api/src/tests/src/response.ts: -------------------------------------------------------------------------------- 1 | import { ErrResponse } from '../../declarations/http_certification_json_api_backend.did'; 2 | 3 | export function jsonDecode(body?: Uint8Array | number[]): T { 4 | body = body ? Uint8Array.from(body) : body; 5 | 6 | return JSON.parse(new TextDecoder().decode(body)); 7 | } 8 | 9 | export interface ApiOkResponse { 10 | ok: { 11 | data: T; 12 | }; 13 | } 14 | 15 | export interface ApiErrResponse { 16 | err: ErrResponse; 17 | } 18 | 19 | export type ApiResponse = ApiOkResponse | ApiErrResponse; 20 | 21 | export type Ok = T extends ApiOkResponse ? U : never; 22 | 23 | export function isOk(res: ApiResponse): res is ApiOkResponse { 24 | return 'ok' in res; 25 | } 26 | 27 | export function isErr(res: ApiResponse): res is ApiErrResponse { 28 | return 'err' in res; 29 | } 30 | 31 | export function extractOkResponse(res?: Uint8Array | number[]): T { 32 | const decodedRes = jsonDecode>(res); 33 | 34 | if (isErr(decodedRes)) { 35 | throw new Error(`${decodedRes.err.code}: ${decodedRes.err.message}`); 36 | } 37 | 38 | return decodedRes.ok.data; 39 | } 40 | 41 | export function extractErrResponse(res?: Uint8Array | number[]): ErrResponse { 42 | const decodedRes = jsonDecode>(res); 43 | 44 | if (isErr(decodedRes)) { 45 | return decodedRes.err; 46 | } 47 | 48 | throw new Error('Expected Err response but got Ok response'); 49 | } 50 | -------------------------------------------------------------------------------- /examples/http-certification/json-api/src/tests/src/wasm.ts: -------------------------------------------------------------------------------- 1 | import { type CanisterFixture, type PocketIc } from '@dfinity/pic'; 2 | import { resolve } from 'node:path'; 3 | import { 4 | type _SERVICE, 5 | idlFactory, 6 | } from '../../declarations/http_certification_json_api_backend.did'; 7 | 8 | export const BACKEND_WASM_PATH = resolve( 9 | __dirname, 10 | '..', 11 | '..', 12 | '..', 13 | '..', 14 | '..', 15 | '..', 16 | 'target', 17 | 'wasm32-unknown-unknown', 18 | 'release', 19 | 'http_certification_json_api_backend.wasm', 20 | ); 21 | 22 | export async function setupBackendCanister( 23 | pic: PocketIc, 24 | initialDate: Date, 25 | ): Promise> { 26 | await pic.setTime(initialDate.getTime()); 27 | 28 | return await pic.setupCanister<_SERVICE>({ 29 | idlFactory, 30 | wasm: BACKEND_WASM_PATH, 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /examples/http-certification/json-api/src/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../../tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "types": ["jest", "node"] 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /examples/http-certification/skip-certification/src/backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "http_certification_skip_certification_backend" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | candid.workspace = true 11 | ic-cdk.workspace = true 12 | ic-http-certification.workspace = true 13 | serde.workspace = true 14 | serde_json.workspace = true 15 | -------------------------------------------------------------------------------- /examples/http-certification/skip-certification/src/backend/backend.did: -------------------------------------------------------------------------------- 1 | type HeaderField = record { text; text }; 2 | 3 | type HttpRequest = record { 4 | method : text; 5 | url : text; 6 | headers : vec HeaderField; 7 | body : blob; 8 | certificate_version : opt nat16; 9 | }; 10 | 11 | type HttpResponse = record { 12 | status_code : nat16; 13 | headers : vec HeaderField; 14 | body : blob; 15 | }; 16 | 17 | service : { 18 | http_request : (request : HttpRequest) -> (HttpResponse) query; 19 | }; 20 | -------------------------------------------------------------------------------- /examples/http-certification/skip-certification/src/backend/src/lib.rs: -------------------------------------------------------------------------------- 1 | use api::canister_balance; 2 | use ic_cdk::{ 3 | api::{data_certificate, set_certified_data}, 4 | *, 5 | }; 6 | use ic_http_certification::{ 7 | utils::{add_skip_certification_header, skip_certification_certified_data}, 8 | HttpResponse, 9 | }; 10 | use serde::Serialize; 11 | 12 | #[derive(Debug, Clone, Serialize)] 13 | pub struct Metrics { 14 | pub cycle_balance: u64, 15 | } 16 | 17 | #[init] 18 | fn init() { 19 | set_certified_data(&skip_certification_certified_data()); 20 | } 21 | 22 | #[query] 23 | fn http_request() -> HttpResponse<'static> { 24 | let mut response = create_response(); 25 | 26 | add_skip_certification_header(data_certificate().unwrap(), &mut response); 27 | 28 | response 29 | } 30 | 31 | fn create_response() -> HttpResponse<'static> { 32 | let metrics = Metrics { 33 | cycle_balance: canister_balance(), 34 | }; 35 | let body = serde_json::to_vec(&metrics).expect("Failed to serialize metrics"); 36 | let headers = vec![ 37 | ("content-type".to_string(), "application/json".to_string()), 38 | ( 39 | "cache-control".to_string(), 40 | "public, no-cache, no-store".to_string(), 41 | ), 42 | ("strict-transport-security".to_string(), "max-age=31536000; includeSubDomains".to_string()), 43 | ("x-frame-options".to_string(), "DENY".to_string()), 44 | ("x-content-type-options".to_string(), "nosniff".to_string()), 45 | ("content-security-policy".to_string(), "default-src 'self'; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content".to_string()), 46 | ("referrer-policy".to_string(), "no-referrer".to_string()), 47 | ("permissions-policy".to_string(), "accelerometer=(),ambient-light-sensor=(),autoplay=(),battery=(),camera=(),display-capture=(),document-domain=(),encrypted-media=(),fullscreen=(),gamepad=(),geolocation=(),gyroscope=(),layout-animations=(self),legacy-image-formats=(self),magnetometer=(),microphone=(),midi=(),oversized-images=(self),payment=(),picture-in-picture=(),publickey-credentials-get=(),speaker-selection=(),sync-xhr=(self),unoptimized-images=(self),unsized-media=(self),usb=(),screen-wake-lock=(),web-share=(),xr-spatial-tracking=()".to_string()), 48 | ("cross-origin-embedder-policy".to_string(), "require-corp".to_string()), 49 | ("cross-origin-opener-policy".to_string(), "same-origin".to_string()), 50 | ("content-length".to_string(), body.len().to_string()), 51 | ]; 52 | 53 | HttpResponse::ok(body, headers).build() 54 | } 55 | -------------------------------------------------------------------------------- /examples/http-certification/skip-certification/src/tests/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | 3 | const config: Config = { 4 | watch: false, 5 | preset: 'ts-jest/presets/js-with-ts', 6 | testEnvironment: 'node', 7 | verbose: true, 8 | testTimeout: 30_000, 9 | }; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /examples/http-certification/skip-certification/src/tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "http-certification-skip-certification-tests", 3 | "private": true, 4 | "scripts": { 5 | "test": "jest" 6 | }, 7 | "devDependencies": { 8 | "@dfinity/response-verification": "workspace:*" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/http-certification/skip-certification/src/tests/src/wasm.ts: -------------------------------------------------------------------------------- 1 | import { type CanisterFixture, type PocketIc } from '@dfinity/pic'; 2 | import { resolve } from 'node:path'; 3 | import { 4 | type _SERVICE, 5 | idlFactory, 6 | } from '../../declarations/http_certification_skip_certification_backend.did'; 7 | 8 | export const BACKEND_WASM_PATH = resolve( 9 | __dirname, 10 | '..', 11 | '..', 12 | '..', 13 | '..', 14 | '..', 15 | '..', 16 | 'target', 17 | 'wasm32-unknown-unknown', 18 | 'release', 19 | 'http_certification_skip_certification_backend.wasm', 20 | ); 21 | 22 | export async function setupBackendCanister( 23 | pic: PocketIc, 24 | initialDate: Date, 25 | ): Promise> { 26 | await pic.setTime(initialDate.getTime()); 27 | 28 | return await pic.setupCanister<_SERVICE>({ 29 | idlFactory, 30 | wasm: BACKEND_WASM_PATH, 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /examples/http-certification/skip-certification/src/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../../tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "types": ["jest", "node"] 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /examples/http-certification/upgrade-to-update-call/src/backend.did: -------------------------------------------------------------------------------- 1 | type HeaderField = record { text; text }; 2 | 3 | type HttpRequest = record { 4 | method : text; 5 | url : text; 6 | headers : vec HeaderField; 7 | body : blob; 8 | certificate_version : opt nat16; 9 | }; 10 | 11 | type HttpUpdateRequest = record { 12 | method : text; 13 | url : text; 14 | headers : vec HeaderField; 15 | body : blob; 16 | }; 17 | 18 | type HttpResponse = record { 19 | status_code : nat16; 20 | headers : vec HeaderField; 21 | body : blob; 22 | upgrade : opt bool; 23 | }; 24 | 25 | type HttpUpdateResponse = record { 26 | status_code : nat16; 27 | headers : vec HeaderField; 28 | body : blob; 29 | }; 30 | 31 | service : { 32 | http_request : (request : HttpRequest) -> (HttpResponse) query; 33 | http_request_update : (request : HttpRequest) -> (HttpResponse); 34 | }; 35 | -------------------------------------------------------------------------------- /examples/http-certification/upgrade-to-update-call/src/motoko-backend/main.mo: -------------------------------------------------------------------------------- 1 | import Text "mo:base/Text"; 2 | 3 | actor Http { 4 | type HeaderField = (Text, Text); 5 | 6 | type HttpRequest = { 7 | method : Text; 8 | url : Text; 9 | headers : [HeaderField]; 10 | body : Blob; 11 | certificate_version : ?Nat16; 12 | }; 13 | 14 | type HttpUpdateRequest = { 15 | method : Text; 16 | url : Text; 17 | headers : [HeaderField]; 18 | body : Blob; 19 | }; 20 | 21 | type HttpResponse = { 22 | status_code : Nat16; 23 | headers : [HeaderField]; 24 | body : Blob; 25 | upgrade : ?Bool; 26 | }; 27 | 28 | type HttpUpdateResponse = { 29 | status_code : Nat16; 30 | headers : [HeaderField]; 31 | body : Blob; 32 | }; 33 | 34 | public query func http_request(_req: HttpRequest) : async HttpResponse { 35 | return { 36 | status_code = 200; 37 | headers = []; 38 | body = ""; 39 | upgrade = ?true; 40 | }; 41 | }; 42 | 43 | public func http_request_update(_req: HttpUpdateRequest) : async HttpUpdateResponse { 44 | return { 45 | status_code = 418; 46 | headers = []; 47 | body = Text.encodeUtf8("I'm a teapot"); 48 | }; 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /examples/http-certification/upgrade-to-update-call/src/rust-backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "http_certification_upgrade_to_update_call_rust_backend" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | candid.workspace = true 11 | ic-cdk.workspace = true 12 | ic-http-certification.workspace = true 13 | -------------------------------------------------------------------------------- /examples/http-certification/upgrade-to-update-call/src/rust-backend/src/lib.rs: -------------------------------------------------------------------------------- 1 | use ic_cdk::*; 2 | use ic_http_certification::{HttpResponse, HttpUpdateResponse, StatusCode}; 3 | 4 | #[query] 5 | fn http_request() -> HttpResponse<'static> { 6 | HttpResponse::builder().with_upgrade(true).build() 7 | } 8 | 9 | #[update] 10 | fn http_request_update() -> HttpUpdateResponse<'static> { 11 | HttpResponse::builder() 12 | .with_status_code(StatusCode::IM_A_TEAPOT) 13 | .with_body(b"I'm a teapot") 14 | .build_update() 15 | } 16 | -------------------------------------------------------------------------------- /examples/http-certification/upgrade-to-update-call/src/tests/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | 3 | const config: Config = { 4 | watch: false, 5 | preset: 'ts-jest/presets/js-with-ts', 6 | testEnvironment: 'node', 7 | verbose: true, 8 | testTimeout: 30_000, 9 | }; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /examples/http-certification/upgrade-to-update-call/src/tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "http-certification-upgrade-to-update-call-tests", 3 | "private": true, 4 | "scripts": { 5 | "test": "jest" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/http-certification/upgrade-to-update-call/src/tests/src/http.spec.ts: -------------------------------------------------------------------------------- 1 | import { Actor, PocketIc, PocketIcServer } from '@dfinity/pic'; 2 | 3 | import { 4 | _SERVICE as RUST_SERVICE, 5 | HttpRequest, 6 | } from '../../declarations/rust-backend/http_certification_upgrade_to_update_call_rust_backend.did'; 7 | import { _SERVICE as MOTOKO_SERVICE } from '../../declarations/motoko-backend/http_certification_upgrade_to_update_call_motoko_backend.did'; 8 | import { setupMotokoBackendCanister, setupRustBackendCanister } from './wasm'; 9 | 10 | describe('HTTP', () => { 11 | let picServer: PocketIcServer; 12 | let pic: PocketIc; 13 | let rustActor: Actor; 14 | let motokoActor: Actor; 15 | 16 | beforeAll(async () => { 17 | picServer = await PocketIcServer.start(); 18 | }); 19 | 20 | afterAll(async () => { 21 | picServer.stop(); 22 | }); 23 | 24 | beforeEach(async () => { 25 | pic = await PocketIc.create(picServer.getUrl()); 26 | 27 | const rustFixture = await setupRustBackendCanister(pic); 28 | rustActor = rustFixture.actor; 29 | 30 | const motokoFixture = await setupMotokoBackendCanister(pic); 31 | motokoActor = motokoFixture.actor; 32 | }); 33 | 34 | afterEach(async () => { 35 | await pic.tearDown(); 36 | }); 37 | 38 | it('should upgrade to an update call', async () => { 39 | const request: HttpRequest = { 40 | url: '/', 41 | method: 'GET', 42 | headers: [], 43 | body: new Uint8Array(), 44 | certificate_version: [], 45 | }; 46 | 47 | const rustResponse = await rustActor.http_request(request); 48 | expect(rustResponse.upgrade).toEqual([true]); 49 | 50 | const motokoResponse = await motokoActor.http_request(request); 51 | expect(motokoResponse.upgrade).toEqual([true]); 52 | 53 | const rustUpdateResponse = await rustActor.http_request_update(request); 54 | expect(rustUpdateResponse.status_code).toBe(418); 55 | expect(rustUpdateResponse.body).toEqual( 56 | new TextEncoder().encode("I'm a teapot"), 57 | ); 58 | 59 | const motokoUpdateResponse = await motokoActor.http_request_update(request); 60 | expect(motokoUpdateResponse.status_code).toBe(418); 61 | expect(motokoUpdateResponse.body).toEqual( 62 | new TextEncoder().encode("I'm a teapot"), 63 | ); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /examples/http-certification/upgrade-to-update-call/src/tests/src/wasm.ts: -------------------------------------------------------------------------------- 1 | import { type CanisterFixture, type PocketIc } from '@dfinity/pic'; 2 | import { resolve } from 'node:path'; 3 | import { 4 | type _SERVICE as RUST_SERVICE, 5 | idlFactory as rustIdlFactory, 6 | } from '../../declarations/rust-backend/http_certification_upgrade_to_update_call_rust_backend.did'; 7 | import { 8 | type _SERVICE as MOTOKO_SERVICE, 9 | idlFactory as motokoIdlFactory, 10 | } from '../../declarations/motoko-backend/http_certification_upgrade_to_update_call_motoko_backend.did'; 11 | 12 | const RUST_BACKEND_WASM_PATH = resolve( 13 | __dirname, 14 | '..', 15 | '..', 16 | '..', 17 | '..', 18 | '..', 19 | '..', 20 | '.dfx', 21 | 'local', 22 | 'canisters', 23 | 'http_certification_upgrade_to_update_call_rust_backend', 24 | 'http_certification_upgrade_to_update_call_rust_backend.wasm.gz', 25 | ); 26 | 27 | const Motoko_BACKEND_WASM_PATH = resolve( 28 | __dirname, 29 | '..', 30 | '..', 31 | '..', 32 | '..', 33 | '..', 34 | '..', 35 | '.dfx', 36 | 'local', 37 | 'canisters', 38 | 'http_certification_upgrade_to_update_call_motoko_backend', 39 | 'http_certification_upgrade_to_update_call_motoko_backend.wasm.gz', 40 | ); 41 | 42 | export async function setupRustBackendCanister( 43 | pic: PocketIc, 44 | ): Promise> { 45 | return await pic.setupCanister({ 46 | idlFactory: rustIdlFactory, 47 | wasm: RUST_BACKEND_WASM_PATH, 48 | }); 49 | } 50 | 51 | export async function setupMotokoBackendCanister( 52 | pic: PocketIc, 53 | ): Promise> { 54 | return await pic.setupCanister({ 55 | idlFactory: motokoIdlFactory, 56 | wasm: Motoko_BACKEND_WASM_PATH, 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /examples/http-certification/upgrade-to-update-call/src/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../../tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "types": ["jest", "node"] 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "response-verification", 3 | "private": true, 4 | "engines": { 5 | "node": "^22", 6 | "pnpm": "^9", 7 | "npm": "please use pnpm" 8 | }, 9 | "packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c", 10 | "scripts": { 11 | "build": "pnpm run -r build", 12 | "format": "prettier --write .", 13 | "format:check": "prettier --check .", 14 | "test": "pnpm run -r test" 15 | }, 16 | "devDependencies": { 17 | "@dfinity/agent": "^1.0.1", 18 | "@dfinity/candid": "^1.0.1", 19 | "@dfinity/principal": "^1.0.1", 20 | "@dfinity/pic": "0.12.0", 21 | "@types/jest": "^29.5.13", 22 | "@types/node": "~22.10.2", 23 | "jest": "^29.7.0", 24 | "prettier": "^3.3.3", 25 | "ts-jest": "^29.2.5", 26 | "typescript": "~5.6.2", 27 | "vite": "^5.4.6", 28 | "vite-plugin-checker": "^0.8.0", 29 | "vite-plugin-static-copy": "^1.0.6", 30 | "vite-plugin-compression": "^0.5.1", 31 | "vite-plugin-solid": "^2.10.2", 32 | "vitest": "^2.1.1", 33 | "solid-js": "^1.7.6" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/certificate-verification-js/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /packages/certificate-verification-js/README.md: -------------------------------------------------------------------------------- 1 | # Certificate Verification 2 | 3 | [Certificate verification](https://internetcomputer.org/docs/references/ic-interface-spec#canister-signatures) on the [Internet Computer](https://dfinity.org) is the process of verifying that a canister's response to a [query call](https://internetcomputer.org/docs/references/ic-interface-spec#http-query) has gone through consensus with other replicas hosting the same canister. 4 | 5 | This package partially encapsulates the protocol for such verification. It performs the following actions: 6 | 7 | - [Decoding](https://internetcomputer.org/docs/references/ic-interface-spec#certification-encoding) of the certificate and the canister provided tree 8 | - Verification of the certificate's [root of trust](https://internetcomputer.org/docs/references/ic-interface-spec#root-of-trust) 9 | - Verification of the certificate's [delegations](https://internetcomputer.org/docs/references/ic-interface-spec#certification-delegation) (if any) 10 | - Decoding of a canister provided merkle tree 11 | - Verification that the canister provided merkle tree's root hash matches the canister's [certified data](https://internetcomputer.org/docs/references/ic-interface-spec#system-api-certified-data) 12 | 13 | ## Usage 14 | 15 | In the following example, `canister` is an actor created with `@dfinity/agent-js` for a canister with the following candid: 16 | 17 | ```candid 18 | type certified_response = record { 19 | "data" : nat32; 20 | "certificate" : blob; 21 | "witness" : blob; 22 | }; 23 | 24 | service : { 25 | "get_data" : () -> (certified_response) query; 26 | }; 27 | ``` 28 | 29 | Check [ic-certification](https://docs.rs/ic_certification/latest/ic_certification/) for details on how to create `certificate` and `witness` inside your canister. 30 | 31 | `calculateDataHash` is a userland provided function that can calculate the hash of the data returned from the canister. This must be calculated in the same way on the canister and the frontend. 32 | 33 | ```javascript 34 | const { data, certificate, witness } = await canister.get_data(); 35 | 36 | const tree = await verifyCertification({ 37 | canisterId: Principal.fromText(canisterId), 38 | encodedCertificate: new Uint8Array(certificate).buffer, 39 | encodedTree: new Uint8Array(witness).buffer, 40 | rootKey: agent.rootKey, 41 | maxCertificateTimeOffsetMs: 50000, 42 | }); 43 | 44 | const treeDataHash = lookup_path(['count'], tree); 45 | const responseDataHash = calculateDataHash(data); 46 | 47 | if (treeDataHash !== responseDataHash) { 48 | // The data returned from the canister does not match the certified data. 49 | } 50 | ``` 51 | 52 | ## Examples 53 | 54 | See the [certified counter example](https://github.com/dfinity/response-verification/tree/main/examples/certification/certified-counter) for a full e2e example of how to create a certification and verify it using this package. 55 | -------------------------------------------------------------------------------- /packages/certificate-verification-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dfinity/certificate-verification", 3 | "description": "Client side certificate verification for the Internet Computer", 4 | "version": "3.0.3", 5 | "author": "DFINITY Stiftung", 6 | "license": "Apache-2.0", 7 | "repository": "github:dfinity/response-verification", 8 | "bugs": "https://github.com/dfinity/response-verification/issues", 9 | "keywords": [ 10 | "internet-computer", 11 | "icp", 12 | "dfinity", 13 | "certificate", 14 | "verification" 15 | ], 16 | "type": "module", 17 | "files": [ 18 | "dist" 19 | ], 20 | "main": "dist/certificate-verification.umd.cjs", 21 | "module": "dist/certificate-verification.js", 22 | "types": "dist/index.d.ts", 23 | "scripts": { 24 | "build": "vite build && tsc -p ./tsconfig.types.json", 25 | "test": "vitest run", 26 | "test:watch": "vitest watch", 27 | "test:coverage": "vitest run --coverage" 28 | }, 29 | "peerDependencies": { 30 | "@dfinity/agent": "^1.0.1", 31 | "@dfinity/candid": "^1.0.1", 32 | "@dfinity/principal": "^1.0.1" 33 | }, 34 | "devDependencies": { 35 | "@dfinity/certification-testing": "workspace:*" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/certificate-verification-js/src/error.ts: -------------------------------------------------------------------------------- 1 | export class CertificateVerificationError extends Error { 2 | constructor(message: string) { 3 | super(message); 4 | this.name = this.constructor.name; 5 | } 6 | } 7 | 8 | export class CertificateTimeError extends Error { 9 | constructor(message: string) { 10 | super(message); 11 | this.name = this.constructor.name; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/certificate-verification-js/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Cbor, 3 | Certificate, 4 | HashTree, 5 | reconstruct, 6 | compare, 7 | } from '@dfinity/agent'; 8 | import { Principal } from '@dfinity/principal'; 9 | import { PipeArrayBuffer, lebDecode } from '@dfinity/candid'; 10 | import { CertificateTimeError, CertificateVerificationError } from './error'; 11 | 12 | export interface VerifyCertificationParams { 13 | canisterId: Principal; 14 | encodedCertificate: ArrayBuffer; 15 | encodedTree: ArrayBuffer; 16 | rootKey: ArrayBuffer; 17 | maxCertificateTimeOffsetMs: number; 18 | } 19 | 20 | export async function verifyCertification({ 21 | canisterId, 22 | encodedCertificate, 23 | encodedTree, 24 | rootKey, 25 | maxCertificateTimeOffsetMs, 26 | }: VerifyCertificationParams): Promise { 27 | const nowMs = Date.now(); 28 | const certificate = await Certificate.create({ 29 | certificate: encodedCertificate, 30 | canisterId, 31 | rootKey, 32 | }); 33 | const tree = Cbor.decode(encodedTree); 34 | 35 | validateCertificateTime(certificate, maxCertificateTimeOffsetMs, nowMs); 36 | await validateTree(tree, certificate, canisterId); 37 | 38 | return tree; 39 | } 40 | 41 | function validateCertificateTime( 42 | certificate: Certificate, 43 | maxCertificateTimeOffsetMs: number, 44 | nowMs: number, 45 | ): void { 46 | const certificateTimeNs = lebDecode( 47 | new PipeArrayBuffer(certificate.lookup(['time'])), 48 | ); 49 | const certificateTimeMs = Number(certificateTimeNs / BigInt(1_000_000)); 50 | 51 | if (certificateTimeMs - maxCertificateTimeOffsetMs > nowMs) { 52 | throw new CertificateTimeError( 53 | `Invalid certificate: time ${certificateTimeMs} is too far in the future (current time: ${nowMs})`, 54 | ); 55 | } 56 | 57 | if (certificateTimeMs + maxCertificateTimeOffsetMs < nowMs) { 58 | throw new CertificateTimeError( 59 | `Invalid certificate: time ${certificateTimeMs} is too far in the past (current time: ${nowMs})`, 60 | ); 61 | } 62 | } 63 | 64 | async function validateTree( 65 | tree: HashTree, 66 | certificate: Certificate, 67 | canisterId: Principal, 68 | ): Promise { 69 | const treeRootHash = await reconstruct(tree); 70 | const certifiedData = certificate.lookup([ 71 | 'canister', 72 | canisterId.toUint8Array(), 73 | 'certified_data', 74 | ]); 75 | 76 | if (!certifiedData) { 77 | throw new CertificateVerificationError( 78 | 'Could not find certified data in the certificate.', 79 | ); 80 | } 81 | 82 | if (!equal(certifiedData, treeRootHash)) { 83 | throw new CertificateVerificationError( 84 | 'Tree root hash did not match the certified data in the certificate.', 85 | ); 86 | } 87 | } 88 | 89 | function equal(a: ArrayBuffer, b: ArrayBuffer): boolean { 90 | return compare(a, b) === 0; 91 | } 92 | -------------------------------------------------------------------------------- /packages/certificate-verification-js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "baseUrl": "./src" 6 | }, 7 | "references": [ 8 | { 9 | "path": "./tsconfig.node.json" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/certificate-verification-js/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/certificate-verification-js/tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist", 5 | "emitDeclarationOnly": true, 6 | "declaration": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/certificate-verification-js/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { defineConfig } from 'vitest/config'; 3 | import checker from 'vite-plugin-checker'; 4 | 5 | export default defineConfig({ 6 | plugins: [checker({ typescript: true })], 7 | build: { 8 | lib: { 9 | entry: resolve(__dirname, 'src', 'index.ts'), 10 | name: '@dfinity/certificate-verification', 11 | fileName: 'certificate-verification', 12 | }, 13 | sourcemap: true, 14 | rollupOptions: { 15 | external: ['@dfinity/agent', '@dfinity/principal', '@dfinity/candid'], 16 | output: { 17 | globals: { 18 | '@dfinity/agent': 'dfinity-agent', 19 | '@dfinity/principal': 'dfinity-principal', 20 | '@dfinity/candid': 'dfinity-candid', 21 | }, 22 | }, 23 | }, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /packages/ic-asset-certification/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic-asset-certification" 3 | description = "Certification for static assets served over HTTP on the Internet Computer" 4 | readme = "README.md" 5 | documentation = "https://docs.rs/ic-asset-certification" 6 | categories = ["api-bindings", "data-structures", "algorithms", "cryptography::cryptocurrencies"] 7 | keywords = ["internet-computer", "agent", "utility", "icp", "dfinity"] 8 | include = ["src", "Cargo.toml", "LICENSE", "README.md"] 9 | 10 | version.workspace = true 11 | authors.workspace = true 12 | edition.workspace = true 13 | repository.workspace = true 14 | license.workspace = true 15 | homepage.workspace = true 16 | 17 | [dependencies] 18 | http.workspace = true 19 | ic-certification.workspace = true 20 | ic-http-certification.workspace = true 21 | thiserror.workspace = true 22 | globset = "0.4" 23 | 24 | [dev-dependencies] 25 | rand_chacha.workspace = true 26 | rstest.workspace = true 27 | assert_matches.workspace = true 28 | ic-response-verification.workspace = true 29 | ic-response-verification-test-utils.workspace = true 30 | ic-certification-testing.workspace = true 31 | once_cell.workspace = true 32 | -------------------------------------------------------------------------------- /packages/ic-asset-certification/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /packages/ic-asset-certification/src/asset.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | /// An asset to be certified and served by an [AssetRouter](crate::AssetRouter). 4 | /// 5 | /// Use the [new](Asset::new) associated function to create instances of 6 | /// this struct. 7 | /// 8 | /// # Examples 9 | /// 10 | /// ## With owned values 11 | /// 12 | /// ``` 13 | /// use ic_asset_certification::Asset; 14 | /// 15 | /// let path = String::from("foo"); 16 | /// let content = vec![1, 2, 3]; 17 | /// 18 | /// let asset = Asset::new(path, content); 19 | /// ``` 20 | /// 21 | /// ## With borrowed values 22 | /// 23 | /// ``` 24 | /// use ic_asset_certification::Asset; 25 | /// 26 | /// let path = "foo"; 27 | /// let content = [1, 2, 3].as_slice(); 28 | /// 29 | /// let asset = Asset::new(path, content); 30 | /// ``` 31 | #[derive(Debug, Clone, PartialEq, Eq)] 32 | pub struct Asset<'content, 'path> { 33 | pub(crate) path: Cow<'path, str>, 34 | pub(crate) url: Cow<'path, str>, 35 | pub(crate) content: Cow<'content, [u8]>, 36 | } 37 | 38 | impl<'content, 'path> Asset<'content, 'path> { 39 | /// Creates a new asset with the given path and content. 40 | /// Both parameters may be owned values, or references so developers are free to 41 | /// choose whichever option is best suited for their use case. 42 | pub fn new(path: impl Into>, content: impl Into>) -> Self { 43 | let path = path.into(); 44 | 45 | Asset { 46 | url: Cow::Owned(path_to_url(path.as_ref())), 47 | path, 48 | content: content.into(), 49 | } 50 | } 51 | } 52 | 53 | fn path_to_url(path: &str) -> String { 54 | if !path.starts_with('/') { 55 | format!("/{}", path) 56 | } else { 57 | path.to_string() 58 | } 59 | } 60 | 61 | #[cfg(test)] 62 | mod tests { 63 | use super::*; 64 | use rstest::*; 65 | 66 | #[rstest] 67 | fn asset_new_owned_values() { 68 | let path = String::from("foo"); 69 | let content = vec![1, 2, 3]; 70 | 71 | let asset = Asset::new(path.clone(), content.clone()); 72 | 73 | assert_eq!(asset.path, path); 74 | assert_eq!(asset.url, "/foo"); 75 | assert_eq!(asset.content, content); 76 | } 77 | 78 | #[rstest] 79 | fn asset_new_borrowed_values() { 80 | let path = "foo"; 81 | let content = [1, 2, 3].as_slice(); 82 | 83 | let asset = Asset::new(path, content); 84 | 85 | assert_eq!(asset.path, path); 86 | assert_eq!(asset.url, "/foo"); 87 | assert_eq!(asset.content, content); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/ic-asset-certification/src/asset_map.rs: -------------------------------------------------------------------------------- 1 | use crate::{AssetEncoding, CertifiedAssetResponse, RequestKey}; 2 | use ic_http_certification::HttpResponse; 3 | use std::collections::{hash_map::Iter, HashMap}; 4 | 5 | /// A map of assets, indexed by path, encoding, and the starting range. 6 | pub trait AssetMap<'content> { 7 | /// Get an asset by path, encoding, and starting range. 8 | /// 9 | /// For standard assets, the path refers to the asset's path, e.g. `/index.html`. 10 | /// 11 | /// For fallback assets, the path refers to the scope that the fallback is valid for, e.g. `/`. 12 | /// See the [fallback_for](crate::AssetConfig::File::fallback_for) config option for more information 13 | /// on fallback scopes. 14 | /// 15 | /// For all types of assets, the encoding refers to the encoding of the asset, see [AssetEncoding]. 16 | /// 17 | /// Assets greater than 2mb are split into multiple ranges, the starting range allows retrieval of 18 | /// individual chunks of these large assets. The first range is `Some(0)`, the second range is 19 | /// `Some(ASSET_CHUNK_SIZE)`, the third range is `Some(ASSET_CHUNK_SIZE * 2)`, and so on. The entire asset can 20 | /// also be retrieved by passing `None` as the starting range. See [ASSET_CHUNK_SIZE](crate::ASSET_CHUNK_SIZE) for the size of each chunk. 21 | fn get( 22 | &self, 23 | path: impl Into, 24 | encoding: Option, 25 | starting_range: Option, 26 | ) -> Option<&HttpResponse<'content>>; 27 | 28 | /// Returns the number of assets in the map. 29 | fn len(&self) -> usize; 30 | 31 | /// Returns `true` if the map contains no assets. 32 | fn is_empty(&self) -> bool { 33 | self.len() == 0 34 | } 35 | 36 | /// Returns an iterator over the assets in the map. 37 | fn iter(&'content self) -> AssetMapIterator<'content>; 38 | } 39 | 40 | impl<'content> AssetMap<'content> for HashMap> { 41 | fn get( 42 | &self, 43 | path: impl Into, 44 | encoding: Option, 45 | range_begin: Option, 46 | ) -> Option<&HttpResponse<'content>> { 47 | let req_key = RequestKey::new(path, encoding.map(|e| e.to_string()), range_begin); 48 | 49 | self.get(&req_key).map(|e| &e.response) 50 | } 51 | 52 | fn len(&self) -> usize { 53 | self.len() 54 | } 55 | 56 | fn iter(&'content self) -> AssetMapIterator<'content> { 57 | AssetMapIterator { inner: self.iter() } 58 | } 59 | } 60 | 61 | /// An iterator over the assets in an asset map. 62 | #[derive(Debug)] 63 | pub struct AssetMapIterator<'content> { 64 | inner: Iter<'content, RequestKey, CertifiedAssetResponse<'content>>, 65 | } 66 | 67 | impl<'content> Iterator for AssetMapIterator<'content> { 68 | type Item = ( 69 | (&'content str, Option<&'content str>, Option), 70 | &'content HttpResponse<'content>, 71 | ); 72 | 73 | fn next(&mut self) -> Option { 74 | self.inner.next().map(|(key, asset)| { 75 | ( 76 | (key.path.as_str(), key.encoding.as_deref(), key.range_begin), 77 | &asset.response, 78 | ) 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/ic-asset-certification/src/error.rs: -------------------------------------------------------------------------------- 1 | /// Asset certification result type. 2 | pub type AssetCertificationResult = Result; 3 | 4 | /// Asset certification error type. 5 | #[derive(thiserror::Error, Debug, Clone)] 6 | pub enum AssetCertificationError { 7 | /// Thrown when a suitable asset cannot be found for a given request url. 8 | #[error(r#"No asset was found matching the current request url: {request_url}"#)] 9 | NoAssetMatchingRequestUrl { 10 | /// The request url that was not matched to any asset. 11 | request_url: String, 12 | }, 13 | 14 | /// Thrown when the asset certification process fails. 15 | #[error(r#"HTTP Certification Error: "{0}""#)] 16 | HttpCertificationError(#[from] ic_http_certification::HttpCertificationError), 17 | 18 | /// Thrown when glob pattern parsing fails. 19 | #[error(r#"Glob error: {0}"#)] 20 | GlobsetError(#[from] globset::Error), 21 | 22 | /// Request 23 | #[error(r#"Request error: {0}"#)] 24 | RequestError(String), 25 | } 26 | -------------------------------------------------------------------------------- /packages/ic-asset-certification/src/types.rs: -------------------------------------------------------------------------------- 1 | use ic_http_certification::{HttpCertificationTreeEntry, HttpResponse}; 2 | 3 | #[derive(Debug, Clone)] 4 | pub(crate) struct CertifiedAssetResponse<'a> { 5 | pub(crate) response: HttpResponse<'a>, 6 | pub(crate) tree_entry: HttpCertificationTreeEntry<'a>, 7 | } 8 | 9 | /// A key created from request data, to retrieve the corresponding response. 10 | #[derive(Debug, Eq, Hash, PartialEq, Clone)] 11 | pub(crate) struct RequestKey { 12 | /// Path of the requested asset. 13 | pub(crate) path: String, 14 | /// The encoding of the asset. 15 | pub(crate) encoding: Option, 16 | /// The beginning of the requested range (if any), counting from 0. 17 | pub(crate) range_begin: Option, 18 | } 19 | 20 | impl RequestKey { 21 | pub(crate) fn new( 22 | path: impl Into, 23 | encoding: Option, 24 | range_begin: Option, 25 | ) -> Self { 26 | Self { 27 | path: path.into(), 28 | encoding, 29 | range_begin, 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/ic-asset-certification/tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use ic_asset_certification::ASSET_CHUNK_SIZE; 2 | use ic_response_verification_test_utils::hash; 3 | use rand_chacha::{ 4 | rand_core::{RngCore, SeedableRng}, 5 | ChaCha20Rng, 6 | }; 7 | 8 | pub fn asset_chunk(asset_body: &[u8], chunk_number: usize) -> &[u8] { 9 | let start = chunk_number * ASSET_CHUNK_SIZE; 10 | let end = start + ASSET_CHUNK_SIZE; 11 | &asset_body[start..end.min(asset_body.len())] 12 | } 13 | 14 | pub fn asset_body(asset_name: &str, asset_size: usize) -> Vec { 15 | let mut rng = ChaCha20Rng::from_seed(hash(asset_name)); 16 | let mut body = vec![0u8; asset_size]; 17 | rng.fill_bytes(&mut body); 18 | 19 | body 20 | } 21 | 22 | #[macro_export] 23 | macro_rules! assert_contains { 24 | ($vec:expr, $elems:expr) => { 25 | for elem in $elems { 26 | assert!( 27 | $vec.contains(&elem), 28 | "assertion failed: Expected vector {:?} to contain element {:?}", 29 | $vec, 30 | elem 31 | ); 32 | } 33 | }; 34 | } 35 | 36 | #[macro_export] 37 | macro_rules! assert_response_eq { 38 | ($actual:expr, $expected:expr) => { 39 | let actual: &ic_http_certification::HttpResponse = &$actual; 40 | let expected: &ic_http_certification::HttpResponse = &$expected; 41 | 42 | assert_eq!(actual.status_code(), expected.status_code()); 43 | assert_eq!(actual.body(), expected.body()); 44 | assert_contains!(actual.headers(), expected.headers()); 45 | }; 46 | } 47 | 48 | #[macro_export] 49 | macro_rules! assert_verified_response_eq { 50 | ($actual:expr, $expected:expr) => { 51 | let actual: &ic_response_verification::types::VerifiedResponse = &$actual; 52 | let expected: &ic_http_certification::HttpResponse = &$expected; 53 | 54 | assert_eq!(actual.status_code, Some(expected.status_code().as_u16())); 55 | assert_eq!(actual.body, expected.body()); 56 | assert_contains!(actual.headers, expected.headers()); 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /packages/ic-cbor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic-cbor" 3 | description = "CBOR decoding for Internet Computer clients" 4 | readme = "README.md" 5 | documentation = "https://docs.rs/ic-cbor" 6 | categories = ["api-bindings", "algorithms", "cryptography::cryptocurrencies"] 7 | keywords = ["internet-computer", "icp", "dfinity", "cbor"] 8 | include = ["src", "Cargo.toml", "README.md"] 9 | 10 | version.workspace = true 11 | authors.workspace = true 12 | edition.workspace = true 13 | repository.workspace = true 14 | license.workspace = true 15 | homepage.workspace = true 16 | 17 | [dependencies] 18 | candid.workspace = true 19 | nom.workspace = true 20 | ic-certification.workspace = true 21 | thiserror.workspace = true 22 | leb128.workspace = true 23 | 24 | [dev-dependencies] 25 | ic-response-verification-test-utils.workspace = true 26 | -------------------------------------------------------------------------------- /packages/ic-cbor/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /packages/ic-cbor/README.md: -------------------------------------------------------------------------------- 1 | # IC CBOR 2 | -------------------------------------------------------------------------------- /packages/ic-cbor/src/cbor_parse_certificate.rs: -------------------------------------------------------------------------------- 1 | use crate::{parse_cbor, parsed_cbor_to_tree, CborError, CborResult, CborValue}; 2 | use ic_certification::{Certificate, Delegation}; 3 | 4 | pub trait CertificateToCbor { 5 | fn from_cbor(cbor: &[u8]) -> CborResult; 6 | } 7 | 8 | impl CertificateToCbor for Certificate { 9 | fn from_cbor(cbor: &[u8]) -> CborResult { 10 | let parsed_cbor = parse_cbor(cbor).map_err(|e| CborError::MalformedCbor(e.to_string()))?; 11 | 12 | parsed_cbor_to_certificate(parsed_cbor) 13 | } 14 | } 15 | 16 | fn parsed_cbor_to_certificate(parsed_cbor: CborValue) -> CborResult { 17 | let CborValue::Map(map) = parsed_cbor else { 18 | return Err(CborError::MalformedCertificate( 19 | "Expected Map when parsing Certificate Cbor".into(), 20 | )); 21 | }; 22 | 23 | let Some(tree_cbor) = map.get("tree") else { 24 | return Err(CborError::MalformedCertificate( 25 | "Expected Tree when parsing Certificate Cbor".into(), 26 | )); 27 | }; 28 | 29 | let tree = parsed_cbor_to_tree(tree_cbor)?; 30 | 31 | let signature = if let Some(CborValue::ByteString(signature)) = map.get("signature") { 32 | signature.to_owned() 33 | } else { 34 | return Err(CborError::MalformedCertificate( 35 | "Expected Signature when parsing Certificate Cbor".into(), 36 | )); 37 | }; 38 | 39 | let delegation = if let Some(CborValue::Map(delegation_map)) = map.get("delegation") { 40 | let Some(CborValue::ByteString(subnet_id)) = delegation_map.get("subnet_id") else { 41 | return Err(CborError::MalformedCertificate( 42 | "Expected Delegation Map to contain a Subnet ID when parsing Certificate Cbor" 43 | .into(), 44 | )); 45 | }; 46 | 47 | let Some(CborValue::ByteString(certificate)) = delegation_map.get("certificate") else { 48 | return Err(CborError::MalformedCertificate( 49 | "Expected Delegation Map to contain a Certificate when parsing Certificate Cbor" 50 | .into(), 51 | )); 52 | }; 53 | 54 | Some(Delegation { 55 | subnet_id: subnet_id.to_owned(), 56 | certificate: certificate.to_owned(), 57 | }) 58 | } else { 59 | None 60 | }; 61 | 62 | Ok(Certificate { 63 | tree, 64 | signature, 65 | delegation, 66 | }) 67 | } 68 | 69 | #[cfg(test)] 70 | mod tests { 71 | use super::*; 72 | use ic_response_verification_test_utils::{ 73 | cbor_encode, create_certificate, create_certificate_delegation, 74 | }; 75 | 76 | #[test] 77 | fn deserialize_from_cbor() { 78 | let certificate = create_certificate(None); 79 | 80 | let cbor = cbor_encode(&certificate); 81 | 82 | let result = Certificate::from_cbor(&cbor).unwrap(); 83 | 84 | assert_eq!(result, certificate); 85 | } 86 | 87 | #[test] 88 | fn deserialize_from_cbor_with_delegation() { 89 | let mut certificate = create_certificate(None); 90 | certificate.delegation = Some(create_certificate_delegation()); 91 | 92 | let cbor = cbor_encode(&certificate); 93 | 94 | let result = Certificate::from_cbor(&cbor).unwrap(); 95 | 96 | assert_eq!(result, certificate); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /packages/ic-cbor/src/error.rs: -------------------------------------------------------------------------------- 1 | pub type CborResult = Result; 2 | 3 | #[derive(thiserror::Error, Debug, Clone)] 4 | pub enum CborError { 5 | /// The CBOR was malformed and could not be parsed correctly 6 | #[error("Invalid cbor: {0}")] 7 | MalformedCbor(String), 8 | 9 | /// Certificate delegation canister range was not correctly CBOR encoded 10 | #[error("Invalid cbor canister ranges")] 11 | MalformedCborCanisterRanges, 12 | 13 | /// The Cbor parser expected a node of a certain type but found a different type 14 | #[error("Expected node with to have type {expected_type:?}, found {found_type:?}")] 15 | UnexpectedCborNodeType { 16 | /// The expected type of the node 17 | expected_type: String, 18 | /// The actual type of the node 19 | found_type: String, 20 | }, 21 | 22 | /// Error converting UTF-8 string 23 | #[error("Error converting UTF8 string bytes: {0}")] 24 | Utf8ConversionError(#[from] std::string::FromUtf8Error), 25 | 26 | /// The certificate was malformed and could not be parsed correctly 27 | #[error(r#"Failed to parse certificate: "{0}""#)] 28 | MalformedCertificate(String), 29 | 30 | /// The hash tree was malformed and could not be parsed correctly 31 | #[error(r#"Failed to parse hash tree: "{0}""#)] 32 | MalformedHashTree(String), 33 | 34 | /// The hash tree pruned data was not the correct length 35 | #[error(r#"Invalid pruned data: "{0}""#)] 36 | IncorrectPrunedDataLength(#[from] std::array::TryFromSliceError), 37 | 38 | #[error("UnexpectedEndOfInput")] 39 | UnexpectedEndOfInput, 40 | } 41 | -------------------------------------------------------------------------------- /packages/ic-cbor/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | 3 | mod cbor_parse_certificate; 4 | pub use cbor_parse_certificate::*; 5 | 6 | mod cbor_parse_hash_tree; 7 | pub use cbor_parse_hash_tree::*; 8 | 9 | mod error; 10 | pub use error::*; 11 | 12 | mod cbor_parser; 13 | pub use cbor_parser::*; 14 | -------------------------------------------------------------------------------- /packages/ic-certificate-verification/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic-certificate-verification" 3 | description = "Certificate verification for the Internet Computer" 4 | readme = "README.md" 5 | documentation = "https://docs.rs/ic-certificate-verification" 6 | categories = ["api-bindings", "algorithms", "cryptography::cryptocurrencies"] 7 | keywords = [ 8 | "internet-computer", 9 | "icp", 10 | "dfinity", 11 | "certificate", 12 | "verification", 13 | ] 14 | include = ["src", "Cargo.toml", "README.md"] 15 | 16 | version.workspace = true 17 | authors.workspace = true 18 | edition.workspace = true 19 | repository.workspace = true 20 | license.workspace = true 21 | homepage.workspace = true 22 | 23 | [dependencies] 24 | candid.workspace = true 25 | nom.workspace = true 26 | miracl_core_bls12381.workspace = true 27 | thiserror.workspace = true 28 | leb128.workspace = true 29 | cached.workspace = true 30 | sha2.workspace = true 31 | lazy_static.workspace = true 32 | parking_lot.workspace = true 33 | 34 | ic-certification = { workspace = true } 35 | ic-cbor.workspace = true 36 | 37 | [dev-dependencies] 38 | ic-response-verification-test-utils.workspace = true 39 | ic-certification-testing.workspace = true 40 | rand.workspace = true 41 | rand_chacha.workspace = true 42 | 43 | ic-types.workspace = true 44 | -------------------------------------------------------------------------------- /packages/ic-certificate-verification/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /packages/ic-certificate-verification/README.md: -------------------------------------------------------------------------------- 1 | # Certificate Verification 2 | 3 | [Certificate verification](https://internetcomputer.org/docs/references/ic-interface-spec#canister-signatures) on the [Internet Computer](https://dfinity.org) is the process of verifying that a canister's response to a [query call](https://internetcomputer.org/docs/references/ic-interface-spec#http-query) has gone through consensus with other replicas hosting the same canister. 4 | 5 | This package partially encapsulates the protocol for such verification. It performs the following actions: 6 | 7 | - [Decoding](https://internetcomputer.org/docs/references/ic-interface-spec#certification-encoding) of the certificate and the canister provided tree 8 | - Verification of the certificate's [root of trust](https://internetcomputer.org/docs/references/ic-interface-spec#root-of-trust) 9 | - Verification of the certificate's [delegations](https://internetcomputer.org/docs/references/ic-interface-spec#certification-delegation) (if any) 10 | - Decoding of a canister provided merkle tree 11 | - Verification that the canister provided merkle tree's root hash matches the canister's [certified data](https://internetcomputer.org/docs/references/ic-interface-spec#system-api-certified-data) 12 | -------------------------------------------------------------------------------- /packages/ic-certificate-verification/src/error.rs: -------------------------------------------------------------------------------- 1 | use candid::Principal; 2 | 3 | /// Convenience type that represents the Result of performing certificate verification 4 | pub type CertificateVerificationResult = Result; 5 | 6 | #[derive(thiserror::Error, Debug, Clone)] 7 | pub enum CertificateVerificationError { 8 | /// Unexpected public key length 9 | #[error( 10 | "BLS DER-encoded public key must be {expected} bytes long, but is {actual} bytes long" 11 | )] 12 | DerKeyLengthMismatch { 13 | /// Expected size of the public key 14 | expected: usize, 15 | /// Actual size of the public key 16 | actual: usize, 17 | }, 18 | 19 | /// Unexpected public key prefix 20 | #[error("BLS DER-encoded public key is invalid. Expected the following prefix: {expected:?}, but got {actual:?}")] 21 | DerPrefixMismatch { 22 | /// Expected public key prefix 23 | expected: Vec, 24 | /// Actual public key prefix 25 | actual: Vec, 26 | }, 27 | 28 | /// The certificate's time was too far in the future 29 | #[error("Certificate time is too far in the future. Received {certificate_time:?}, expected {max_certificate_time:?} or earlier")] 30 | TimeTooFarInTheFuture { 31 | /// The actual certificate time 32 | certificate_time: u128, 33 | /// The maximum expected certificate time 34 | max_certificate_time: u128, 35 | }, 36 | 37 | /// The certificate's time was too far in the past 38 | #[error("Certificate time is too far in the past. Received {certificate_time:?}, expected {min_certificate_time:?} or later")] 39 | TimeTooFarInThePast { 40 | /// The actual certificate time 41 | certificate_time: u128, 42 | /// The minimum expected certificate time 43 | min_certificate_time: u128, 44 | }, 45 | 46 | /// Certificate is for a different subnet 47 | #[error( 48 | "Canister ID {canister_id} is not within the certificate's range: {canister_ranges:?}" 49 | )] 50 | PrincipalOutOfRange { 51 | /// The canister ID that was looked up in the certificate 52 | canister_id: Principal, 53 | /// The canister ID ranges that were found in the certificate 54 | canister_ranges: Vec<(Principal, Principal)>, 55 | }, 56 | 57 | /// Certificate delegation is missing the required canister range 58 | #[error("Subnet canister ID ranges not found in certificate at path: {path:?}")] 59 | SubnetCanisterIdRangesNotFound { 60 | /// The path that was used to look up the canister ranges in the certificate 61 | path: Vec>, 62 | }, 63 | 64 | /// Certificate delegation is missing the required public key 65 | #[error("Subnet public key not found in certificate at path: {path:?}")] 66 | SubnetPublicKeyNotFound { 67 | /// The path that was used to look up the public key in the certificate 68 | path: Vec>, 69 | }, 70 | 71 | /// The certificate was expected to have a "time" path, but it was missing 72 | #[error(r#"Time not found in certificate at path: {path:?}"#)] 73 | MissingTimePathInTree { 74 | /// The path that was used to look up the time in the certificate 75 | path: Vec>, 76 | }, 77 | 78 | /// Encountered an overflow error while decoding leb encoded timestamp 79 | #[error("Certificate time decoding failed due to an overflow: {timestamp:?}")] 80 | TimeDecodingFailed { timestamp: Vec }, 81 | 82 | /// Failed to verify the certificate's signature 83 | #[error("Signature verification failed")] 84 | SignatureVerificationFailed, 85 | 86 | /// Failed to decode CBOR 87 | #[error("CBOR decoding failed")] 88 | CborDecodingFailed(#[from] ic_cbor::CborError), 89 | 90 | /// The certificate contained more than one delegation. 91 | #[error("The certificate contained more than one delegation")] 92 | CertificateHasTooManyDelegations, 93 | } 94 | -------------------------------------------------------------------------------- /packages/ic-certificate-verification/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | 3 | mod signature_verification; 4 | 5 | mod error; 6 | pub use error::*; 7 | 8 | mod certificate_verification; 9 | pub use certificate_verification::*; 10 | -------------------------------------------------------------------------------- /packages/ic-certificate-verification/src/signature_verification/mod.rs: -------------------------------------------------------------------------------- 1 | use self::signature_cache::{SignatureCache, SignatureCacheEntry}; 2 | use crate::CertificateVerificationError; 3 | use miracl_core_bls12381::bls12381::bls::{core_verify, BLS_OK}; 4 | 5 | mod signature_cache; 6 | 7 | #[cfg(test)] 8 | mod reproducible_rng; 9 | 10 | #[cfg(test)] 11 | mod tests; 12 | 13 | pub fn verify_signature( 14 | pk: &[u8], 15 | sig: &[u8], 16 | msg: &[u8], 17 | ) -> Result<(), CertificateVerificationError> { 18 | let entry = SignatureCacheEntry::new(pk, sig, msg); 19 | 20 | if SignatureCache::global().contains(&entry) { 21 | return Ok(()); 22 | } 23 | 24 | let result = core_verify(sig, msg, pk); 25 | 26 | if !matches!(result, BLS_OK) { 27 | return Err(CertificateVerificationError::SignatureVerificationFailed); 28 | } 29 | 30 | SignatureCache::global().insert(&entry); 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /packages/ic-certificate-verification/src/signature_verification/reproducible_rng.rs: -------------------------------------------------------------------------------- 1 | use rand::{CryptoRng, Error, Rng, RngCore, SeedableRng}; 2 | use rand_chacha::ChaCha20Rng; 3 | 4 | /// Byte length of the seed type used in [`ReproducibleRng`]. 5 | const SEED_LEN: usize = 32; 6 | 7 | /// Provides a seeded RNG, where the randomly chosen seed is printed on standard output. 8 | pub fn reproducible_rng() -> ReproducibleRng { 9 | ReproducibleRng::new() 10 | } 11 | 12 | /// Wraps the logic of [`reproducible_rng`] into a separate struct. 13 | /// 14 | /// This is needed when [`reproducible_rng`] cannot be used because its 15 | /// return type `impl Rng + CryptoRng` can only be used as function parameter 16 | /// or as return type 17 | /// (See [impl trait type](https://doc.rust-lang.org/reference/types/impl-trait.html)). 18 | pub struct ReproducibleRng { 19 | rng: ChaCha20Rng, 20 | seed: [u8; SEED_LEN], 21 | } 22 | 23 | impl ReproducibleRng { 24 | /// Randomly generates a seed and prints it to `stdout`. 25 | pub fn new() -> Self { 26 | let mut seed = [0u8; SEED_LEN]; 27 | rand::thread_rng().fill(&mut seed); 28 | Self::from_seed_internal(seed) 29 | } 30 | 31 | fn from_seed_internal(seed: [u8; SEED_LEN]) -> Self { 32 | let rng = ChaCha20Rng::from_seed(seed); 33 | Self { rng, seed } 34 | } 35 | } 36 | 37 | impl std::fmt::Debug for ReproducibleRng { 38 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 39 | write!( 40 | f, 41 | "Copy the seed below to reproduce the failed test.\n 42 | let seed: [u8; 32] = {:?};", 43 | self.seed 44 | ) 45 | } 46 | } 47 | 48 | impl Default for ReproducibleRng { 49 | fn default() -> Self { 50 | Self::new() 51 | } 52 | } 53 | 54 | impl RngCore for ReproducibleRng { 55 | fn next_u32(&mut self) -> u32 { 56 | self.rng.next_u32() 57 | } 58 | 59 | fn next_u64(&mut self) -> u64 { 60 | self.rng.next_u64() 61 | } 62 | 63 | fn fill_bytes(&mut self, dest: &mut [u8]) { 64 | self.rng.fill(dest) 65 | } 66 | 67 | fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error> { 68 | self.rng.try_fill_bytes(dest) 69 | } 70 | } 71 | 72 | impl CryptoRng for ReproducibleRng {} 73 | -------------------------------------------------------------------------------- /packages/ic-certification-testing-wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic-certification-testing-wasm" 3 | description = "Utilities for testing applications that work with Internet Computer certification" 4 | include = ["src", "Cargo.toml", "README.md"] 5 | 6 | version.workspace = true 7 | authors.workspace = true 8 | edition.workspace = true 9 | repository.workspace = true 10 | license.workspace = true 11 | homepage.workspace = true 12 | 13 | [features] 14 | debug = [] 15 | 16 | [lib] 17 | crate-type = ["cdylib", "rlib"] 18 | 19 | [dependencies] 20 | wasm-bindgen.workspace = true 21 | console_error_panic_hook.workspace = true 22 | wasm-bindgen-console-logger.workspace = true 23 | serde-wasm-bindgen.workspace = true 24 | ic-certification-testing.workspace = true 25 | log.workspace = true 26 | ic-types.workspace = true 27 | -------------------------------------------------------------------------------- /packages/ic-certification-testing-wasm/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /packages/ic-certification-testing-wasm/README.md: -------------------------------------------------------------------------------- 1 | # Certification Testing 2 | 3 | [Certificate verification](https://internetcomputer.org/docs/references/ic-interface-spec#canister-signatures) on the [Internet Computer](https://dfinity.org) is the process of verifying that a canister's response to a [query call](https://internetcomputer.org/docs/references/ic-interface-spec#http-query) has gone through consensus with other replicas hosting the same canister. 4 | 5 | This package provides a set of utilities to create these certificates for the purpose of testing in any Javascript client with `wasm` support that may need to verify them. 6 | 7 | ## Usage 8 | 9 | First, a hash tree must be created containing the data that needs to be certified. This can be done using the [@dfinity/agent](https://www.npmjs.com/package/@dfinity/agent) library. The root hash of this tree is then used to create the certificate. 10 | 11 | The [@dfinity/certificate-verification](https://www.npmjs.com/package/@dfinity/certificate-verification) library can then be used to decode the certificate and verify it. 12 | 13 | ```typescript 14 | import { describe, expect, it } from 'vitest'; 15 | import { HashTree, reconstruct, Cbor } from '@dfinity/agent'; 16 | import { CertificateBuilder } from '@dfinity/certification-testing'; 17 | import { verifyCertification } from '@dfinity/certificate-verification'; 18 | import { Principal } from '@dfinity/principal'; 19 | import { createHash } from 'node:crypto'; 20 | 21 | const userId = '1234'; 22 | 23 | const username = 'testuser'; 24 | const usernameHash = new Uint8Array( 25 | createHash('sha256').update(username).digest(), 26 | ); 27 | 28 | const hashTree: HashTree = [ 29 | 2, 30 | new Uint8Array(Buffer.from(userId)), 31 | [3, usernameHash], 32 | ]; 33 | const rootHash = await reconstruct(hashTree); 34 | const cborEncodedTree = Cbor.encode(hashTree); 35 | 36 | const canisterId = Principal.fromUint8Array( 37 | new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1]), 38 | ); 39 | const time = BigInt(Date.now()); 40 | const MAX_CERT_TIME_OFFSET_MS = 300_000; 41 | 42 | let certificate = new CertificateBuilder( 43 | canisterId.toString(), 44 | new Uint8Array(rootHash), 45 | ) 46 | .withTime(time) 47 | .build(); 48 | 49 | const decodedHashTree = await verifyCertification({ 50 | canisterId, 51 | encodedCertificate: certificate.cborEncodedCertificate, 52 | encodedTree: cborEncodedTree, 53 | maxCertificateTimeOffsetMs: MAX_CERT_TIME_OFFSET_MS, 54 | rootKey: certificate.rootKey, 55 | }); 56 | expect(decodedHashTree).toEqual(hashTree); 57 | ``` 58 | -------------------------------------------------------------------------------- /packages/ic-certification-testing-wasm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dfinity/certification-testing", 3 | "description": "Utilities for testing applications that work with Internet Computer certification", 4 | "version": "3.0.3", 5 | "author": "DFINITY Stiftung", 6 | "license": "Apache-2.0", 7 | "repository": "github:dfinity/response-verification", 8 | "bugs": "https://github.com/dfinity/response-verification/issues", 9 | "keywords": [ 10 | "internet-computer", 11 | "icp", 12 | "dfinity", 13 | "certification", 14 | "test-utils" 15 | ], 16 | "files": [ 17 | "dist" 18 | ], 19 | "main": "./dist/nodejs/nodejs.js", 20 | "browser": "./dist/web/web.js", 21 | "types": "./dist/web/web.d.ts", 22 | "scripts": { 23 | "build": "../../scripts/package.sh . ./dist" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/ic-certification-testing-wasm/src/certificate_builder.rs: -------------------------------------------------------------------------------- 1 | use ic_certification_testing::{ 2 | CanisterIdRange, CertificateBuilder as CertificateBuilderImpl, CertificationTestResult, 3 | }; 4 | use std::borrow::BorrowMut; 5 | use wasm_bindgen::prelude::*; 6 | 7 | #[derive(Debug, Clone)] 8 | #[wasm_bindgen(inspectable, getter_with_clone)] 9 | pub struct CertificateData { 10 | pub certificate: JsValue, 11 | 12 | #[wasm_bindgen(js_name = rootKey)] 13 | pub root_key: Vec, 14 | 15 | #[wasm_bindgen(js_name = cborEncodedCertificate)] 16 | pub cbor_encoded_certificate: Vec, 17 | } 18 | 19 | #[wasm_bindgen] 20 | pub struct CertificateBuilder { 21 | builder: CertificateBuilderImpl, 22 | } 23 | 24 | #[wasm_bindgen] 25 | impl CertificateBuilder { 26 | #[wasm_bindgen(constructor)] 27 | pub fn new( 28 | canister_id: &str, 29 | certified_data: &[u8], 30 | ) -> CertificationTestResult { 31 | CertificateBuilderImpl::new(canister_id, certified_data) 32 | .map(|builder| CertificateBuilder { builder }) 33 | } 34 | 35 | #[wasm_bindgen(js_name = withDelegation)] 36 | pub fn with_delegation( 37 | mut self, 38 | subnet_id: u64, 39 | canister_id_ranges: Vec, 40 | ) -> CertificationTestResult { 41 | let canister_id_ranges = canister_id_ranges 42 | .into_iter() 43 | .map(|v| serde_wasm_bindgen::from_value::(v)) 44 | .map(|v| v.map(|v| (v.low, v.high))) 45 | .collect::>()?; 46 | 47 | self.builder.with_delegation(subnet_id, canister_id_ranges); 48 | 49 | Ok(self) 50 | } 51 | 52 | #[wasm_bindgen(js_name = withTime)] 53 | pub fn with_time_js(mut self, time: u64) -> Self { 54 | self.builder 55 | .borrow_mut() 56 | .with_time(u128::from(time) * 1_000_000); 57 | 58 | self 59 | } 60 | 61 | #[wasm_bindgen(js_name = withInvalidSignature)] 62 | pub fn with_invalid_signature(mut self) -> Self { 63 | self.builder.with_invalid_signature(); 64 | 65 | self 66 | } 67 | 68 | pub fn build(self) -> CertificationTestResult { 69 | let certificate_data = self.builder.build()?; 70 | 71 | Ok(CertificateData { 72 | certificate: serde_wasm_bindgen::to_value(&certificate_data.certificate)?, 73 | root_key: certificate_data.root_key, 74 | cbor_encoded_certificate: certificate_data.cbor_encoded_certificate, 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/ic-certification-testing-wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | 3 | pub mod certificate_builder; 4 | pub use certificate_builder::*; 5 | 6 | use wasm_bindgen::prelude::*; 7 | 8 | #[wasm_bindgen(start)] 9 | pub fn main() { 10 | console_error_panic_hook::set_once(); 11 | log::set_logger(&wasm_bindgen_console_logger::DEFAULT_LOGGER).unwrap(); 12 | log::set_max_level(log::LevelFilter::Info); 13 | } 14 | -------------------------------------------------------------------------------- /packages/ic-certification-testing/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic-certification-testing" 3 | description = "Utilities for testing applications that work with Internet Computer certification" 4 | readme = "README.md" 5 | documentation = "https://docs.rs/ic-certification-testing" 6 | categories = ["api-bindings", "algorithms", "cryptography::cryptocurrencies"] 7 | keywords = ["internet-computer", "icp", "dfinity", "certification", "testing"] 8 | include = ["src", "Cargo.toml", "README.md"] 9 | 10 | version.workspace = true 11 | authors.workspace = true 12 | edition.workspace = true 13 | repository.workspace = true 14 | license.workspace = true 15 | homepage.workspace = true 16 | 17 | [lib] 18 | crate-type = ["cdylib", "rlib"] 19 | 20 | [dependencies] 21 | leb128.workspace = true 22 | thiserror.workspace = true 23 | wasm-bindgen.workspace = true 24 | js-sys.workspace = true 25 | console_error_panic_hook.workspace = true 26 | wasm-bindgen-console-logger.workspace = true 27 | log.workspace = true 28 | serde.workspace = true 29 | serde_cbor.workspace = true 30 | serde-wasm-bindgen.workspace = true 31 | rand.workspace = true 32 | getrandom.workspace = true 33 | 34 | ic-types.workspace = true 35 | ic-crypto-tree-hash.workspace = true 36 | ic-crypto-internal-threshold-sig-bls12381.workspace = true 37 | ic-crypto-internal-seed.workspace = true 38 | ic-crypto-internal-types.workspace = true 39 | -------------------------------------------------------------------------------- /packages/ic-certification-testing/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /packages/ic-certification-testing/README.md: -------------------------------------------------------------------------------- 1 | # Certification Testing 2 | 3 | [Certificate verification](https://internetcomputer.org/docs/references/ic-interface-spec#canister-signatures) on the [Internet Computer](https://dfinity.org) is the process of verifying that a canister's response to a [query call](https://internetcomputer.org/docs/references/ic-interface-spec#http-query) has gone through consensus with other replicas hosting the same canister. 4 | 5 | This package provides a set of utilities to create these certificates for the purpose of testing in any Rust client that may need to verify them. 6 | 7 | ## Usage 8 | 9 | First, a hash tree must be created containing the data that needs to be certified. This can be done using the [ic-certification](https://docs.rs/ic_certification/latest/ic_certification/) library. The root hash of this tree is then used to create the certificate. 10 | 11 | The [ic-certification](https://docs.rs/ic-certification/latest/ic_certification/), [ic-cbor](https://docs.rs/ic-cbor/latest/ic_cbor/) and [ic-certificate-verification](https://docs.rs/ic-certificate-verification/latest/ic_certificate_verification/) libraries can then be used to decode the certificate and verify it. 12 | 13 | ```rust 14 | use ic_certification_testing::{CertificateBuilder, CertificateData}; 15 | use ic_cbor::CertificateToCbor; 16 | use ic_certificate_verification::VerifyCertificate; 17 | use ic_certification::{Certificate, AsHashTree, RbTree}; 18 | use ic_types::CanisterId; 19 | use sha2::{Digest, Sha256}; 20 | use std::time::{SystemTime, UNIX_EPOCH}; 21 | 22 | type Hash = [u8; 32]; 23 | 24 | fn hash(data: T) -> Hash 25 | where 26 | T: AsRef<[u8]>, 27 | { 28 | let mut hasher = Sha256::new(); 29 | hasher.update(data); 30 | hasher.finalize().into() 31 | } 32 | 33 | fn get_timestamp() -> u128 { 34 | SystemTime::now() 35 | .duration_since(UNIX_EPOCH) 36 | .unwrap() 37 | .as_nanos() 38 | } 39 | 40 | fn usage_example() { 41 | let canister_id = CanisterId::from_u64(42); 42 | let mut rb_tree = RbTree::<&'static str, Hash>::new(); 43 | 44 | let data_key = "key1"; 45 | let data_hash = hash("value1"); 46 | rb_tree.insert(data_key, data_hash); 47 | 48 | let certified_data = rb_tree.root_hash(); 49 | 50 | let current_timestamp = get_timestamp(); 51 | 52 | let mut certificate_builder = 53 | CertificateBuilder::new(&canister_id.get().0.to_text(), &certified_data) 54 | .expect("Failed to parse canister id"); 55 | 56 | let CertificateData { 57 | cbor_encoded_certificate, 58 | root_key, 59 | certificate: _, 60 | } = certificate_builder 61 | .with_time(current_timestamp) 62 | .build() 63 | .expect("Invalid certificate params provided"); 64 | 65 | let certificate = Certificate::from_cbor(&cbor_encoded_certificate) 66 | .expect("Failed to deserialize certificate"); 67 | 68 | certificate 69 | .verify(&canister_id.get().to_vec(), &root_key) 70 | .expect("Failed to verify certificate"); 71 | } 72 | ``` 73 | -------------------------------------------------------------------------------- /packages/ic-certification-testing/src/certificate.rs: -------------------------------------------------------------------------------- 1 | use crate::{encoding::serialize_to_cbor, error::CertificationTestResult}; 2 | use ic_crypto_tree_hash::{flatmap, Label, LabeledTree}; 3 | use ic_types::{CanisterId, SubnetId}; 4 | 5 | pub(crate) fn create_certificate_tree( 6 | canister_id: &CanisterId, 7 | certified_data: &[u8], 8 | encoded_time: &[u8], 9 | ) -> LabeledTree> { 10 | LabeledTree::SubTree(flatmap![ 11 | Label::from("canister") => LabeledTree::SubTree(flatmap![ 12 | Label::from(canister_id.get_ref().to_vec()) => LabeledTree::SubTree(flatmap![ 13 | Label::from("certified_data") => LabeledTree::Leaf(certified_data.to_vec()), 14 | ]) 15 | ]), 16 | Label::from("time") => LabeledTree::Leaf(encoded_time.to_vec())]) 17 | } 18 | 19 | pub(crate) fn create_delegation_tree( 20 | delegatee_public_key: &[u8], 21 | encoded_time: &[u8], 22 | subnet_id: &SubnetId, 23 | canister_ranges: &[(CanisterId, CanisterId)], 24 | ) -> CertificationTestResult>> { 25 | let canister_ranges = serialize_to_cbor(&canister_ranges.to_vec()); 26 | 27 | Ok(LabeledTree::SubTree(flatmap![ 28 | Label::from("subnet") => LabeledTree::SubTree(flatmap![ 29 | Label::from(subnet_id.get_ref().to_vec()) => LabeledTree::SubTree(flatmap![ 30 | Label::from("canister_ranges") => LabeledTree::Leaf(canister_ranges), 31 | Label::from("public_key") => LabeledTree::Leaf(delegatee_public_key.to_vec()), 32 | ]) 33 | ]), 34 | Label::from("time") => LabeledTree::Leaf(encoded_time.to_vec()) 35 | ])) 36 | } 37 | -------------------------------------------------------------------------------- /packages/ic-certification-testing/src/encoding.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{CertificationTestError, CertificationTestResult}; 2 | use serde::Serialize; 3 | 4 | pub(crate) fn serialize_to_cbor(payload: &T) -> Vec { 5 | let mut serializer = serde_cbor::Serializer::new(Vec::new()); 6 | serializer.self_describe().unwrap(); 7 | payload.serialize(&mut serializer).unwrap(); 8 | serializer.into_inner() 9 | } 10 | 11 | pub(crate) fn leb_encode_timestamp(timestamp: u128) -> CertificationTestResult> { 12 | let mut encoded_time = vec![]; 13 | 14 | leb128::write::unsigned(&mut encoded_time, timestamp as u64) 15 | .map_err(|_| CertificationTestError::TimestampLebEncodingFailed)?; 16 | 17 | Ok(encoded_time) 18 | } 19 | -------------------------------------------------------------------------------- /packages/ic-certification-testing/src/error.rs: -------------------------------------------------------------------------------- 1 | use ic_crypto_internal_threshold_sig_bls12381::api::threshold_sign_error::ClibThresholdSignError; 2 | use ic_crypto_tree_hash::TreeHashError; 3 | use thiserror::Error; 4 | use wasm_bindgen::prelude::*; 5 | 6 | #[wasm_bindgen] 7 | #[derive(Error, Debug)] 8 | pub enum CertificationTestError { 9 | #[error("could not hash tree")] 10 | UnableToHashTree, 11 | 12 | #[error("could not generate witness")] 13 | WitnessGenerationFailed, 14 | 15 | #[error("could not sign message")] 16 | ThresholdSigningFailed, 17 | 18 | #[error("could not serialize certificate to cbor")] 19 | CertificateSerializationFailed, 20 | 21 | #[error("could not leb encode timestamp")] 22 | TimestampLebEncodingFailed, 23 | 24 | #[error("could not parse canister ID")] 25 | CanisterIdParsingFailed, 26 | 27 | #[error("could not encode public key")] 28 | PublicKeyEncodingFailed, 29 | 30 | #[error("one of canister params or a custom tree must be provided")] 31 | CanisterParamsOrCustomTreeRequired, 32 | 33 | #[error("only one of canister params or a custom tree may be provided")] 34 | BothCanisterParamsAndCustomTreeProvided, 35 | 36 | #[error("failed to merge witnesses")] 37 | WitnessMergingFailed, 38 | } 39 | 40 | impl From for CertificationTestError { 41 | fn from(_: TreeHashError) -> Self { 42 | CertificationTestError::UnableToHashTree 43 | } 44 | } 45 | 46 | impl From for CertificationTestError { 47 | fn from(_: ClibThresholdSignError) -> Self { 48 | CertificationTestError::ThresholdSigningFailed 49 | } 50 | } 51 | 52 | impl From for CertificationTestError { 53 | fn from(_: serde_wasm_bindgen::Error) -> Self { 54 | CertificationTestError::CertificateSerializationFailed 55 | } 56 | } 57 | 58 | pub type CertificationTestResult = Result; 59 | -------------------------------------------------------------------------------- /packages/ic-certification-testing/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | 3 | mod certificate_builder; 4 | pub use certificate_builder::*; 5 | 6 | mod error; 7 | pub use error::*; 8 | 9 | mod certificate; 10 | mod encoding; 11 | mod signature; 12 | mod tree; 13 | -------------------------------------------------------------------------------- /packages/ic-certification-testing/src/signature.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{CertificationTestError, CertificationTestResult}; 2 | use ic_crypto_internal_seed::Seed; 3 | use ic_crypto_internal_threshold_sig_bls12381::{ 4 | api::{combined_public_key, generate_threshold_key, public_key_to_der, sign_message}, 5 | types::SecretKeyBytes, 6 | }; 7 | use ic_crypto_internal_types::sign::threshold_sig::public_key::{ 8 | bls12_381::PublicKeyBytes, CspThresholdSigPublicKey, 9 | }; 10 | use ic_crypto_tree_hash::MixedHashTree; 11 | use ic_types::{ 12 | consensus::certification::CertificationContent, 13 | crypto::{ 14 | threshold_sig::ThresholdSigPublicKey, CombinedThresholdSig, CombinedThresholdSigOf, 15 | CryptoHash, Signable, 16 | }, 17 | messages::Blob, 18 | CryptoHashOfPartialState, NumberOfNodes, 19 | }; 20 | use rand::{thread_rng, Rng}; 21 | 22 | #[derive(Debug, Clone)] 23 | pub(crate) struct KeyPair { 24 | pub(crate) public_key: Vec, 25 | pub(crate) private_key: SecretKeyBytes, 26 | } 27 | 28 | pub(crate) fn generate_keypair() -> CertificationTestResult { 29 | let mut seed: [u8; 32] = [0; 32]; 30 | thread_rng().fill(&mut seed); 31 | 32 | let (public_coefficients, secret_key_bytes) = generate_threshold_key( 33 | Seed::from_bytes(&seed), 34 | NumberOfNodes::new(1), 35 | NumberOfNodes::new(1), 36 | ) 37 | .unwrap(); 38 | 39 | let private_key = secret_key_bytes.first().unwrap().clone(); 40 | let public_key = ThresholdSigPublicKey::from(CspThresholdSigPublicKey::from( 41 | combined_public_key(&public_coefficients).unwrap(), 42 | )); 43 | 44 | let public_key = public_key_to_der(PublicKeyBytes(public_key.into_bytes())) 45 | .map_err(|_| CertificationTestError::PublicKeyEncodingFailed)?; 46 | 47 | Ok(KeyPair { 48 | public_key, 49 | private_key, 50 | }) 51 | } 52 | 53 | pub(crate) fn get_tree_signature( 54 | mixed_hash_tree: &MixedHashTree, 55 | private_key: &SecretKeyBytes, 56 | ) -> CertificationTestResult { 57 | let root_hash = CryptoHashOfPartialState::from(CryptoHash(mixed_hash_tree.digest().to_vec())); 58 | 59 | let signature = sign_message( 60 | CertificationContent::new(root_hash) 61 | .as_signed_bytes() 62 | .as_slice(), 63 | private_key, 64 | )?; 65 | let signature: CombinedThresholdSigOf = 66 | CombinedThresholdSigOf::from(CombinedThresholdSig(signature.0.to_vec())); 67 | 68 | Ok(Blob(signature.get().0)) 69 | } 70 | -------------------------------------------------------------------------------- /packages/ic-certification-testing/src/tree.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{CertificationTestError, CertificationTestResult}; 2 | use ic_crypto_tree_hash::{ 3 | HashTreeBuilder, HashTreeBuilderImpl, LabeledTree, MixedHashTree, WitnessGenerator, 4 | }; 5 | 6 | pub(crate) fn get_mixed_hash_tree( 7 | tree: &LabeledTree>, 8 | ) -> CertificationTestResult { 9 | let mut hash_tree_builder = HashTreeBuilderImpl::new(); 10 | hash_full_tree(&mut hash_tree_builder, tree); 11 | 12 | let witness_gen = hash_tree_builder 13 | .witness_generator() 14 | .ok_or(CertificationTestError::WitnessGenerationFailed)?; 15 | 16 | let mixed_hash_tree = witness_gen 17 | .mixed_hash_tree(tree) 18 | .map_err(|_| CertificationTestError::WitnessMergingFailed)?; 19 | 20 | Ok(mixed_hash_tree) 21 | } 22 | 23 | fn hash_full_tree(hash_tree_builder: &mut HashTreeBuilderImpl, tree: &LabeledTree>) { 24 | match tree { 25 | LabeledTree::Leaf(bytes) => { 26 | hash_tree_builder.start_leaf(); 27 | hash_tree_builder.write_leaf(&bytes[..]); 28 | hash_tree_builder.finish_leaf(); 29 | } 30 | LabeledTree::SubTree(map) => { 31 | hash_tree_builder.start_subtree(); 32 | for (l, child) in map.iter() { 33 | hash_tree_builder.new_edge(l.clone()); 34 | hash_full_tree(hash_tree_builder, child); 35 | } 36 | hash_tree_builder.finish_subtree(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/ic-certification/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic-certification" 3 | description = "Types related to the Internet Computer Public Specification." 4 | readme = "README.md" 5 | documentation = "https://docs.rs/ic-certification" 6 | categories = ["api-bindings", "data-structures", "algorithms", "cryptography::cryptocurrencies"] 7 | keywords = ["internet-computer", "agent", "utility", "icp", "dfinity"] 8 | include = ["src", "Cargo.toml", "LICENSE", "README.md"] 9 | 10 | version.workspace = true 11 | authors.workspace = true 12 | edition.workspace = true 13 | repository.workspace = true 14 | license.workspace = true 15 | homepage.workspace = true 16 | 17 | [dependencies] 18 | hex.workspace = true 19 | sha2.workspace = true 20 | 21 | [dependencies.serde] 22 | workspace = true 23 | optional = true 24 | 25 | [dependencies.serde_bytes] 26 | workspace = true 27 | optional = true 28 | 29 | [dev-dependencies] 30 | serde.workspace = true 31 | serde_cbor.workspace = true 32 | rstest.workspace = true 33 | 34 | [features] 35 | serde = ['dep:serde', 'dep:serde_bytes'] 36 | default = ['serde'] 37 | -------------------------------------------------------------------------------- /packages/ic-certification/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /packages/ic-certification/README.md: -------------------------------------------------------------------------------- 1 | # IC Certification 2 | 3 | A collection of types related to the Internet Computer Protocol. 4 | 5 | If you need support for the serde library, you will need to use the `serde` feature 6 | (available by default). 7 | -------------------------------------------------------------------------------- /packages/ic-certification/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # IC Certification 2 | 3 | #![deny(clippy::all)] 4 | #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] 5 | 6 | use hex::FromHexError; 7 | 8 | pub mod certificate; 9 | pub mod hash_tree; 10 | pub use crate::hash_tree::*; 11 | pub mod rb_tree; 12 | pub use crate::rb_tree::*; 13 | pub mod nested_rb_tree; 14 | pub use crate::nested_rb_tree::*; 15 | 16 | #[doc(inline)] 17 | pub use hash_tree::LookupResult; 18 | 19 | /// A HashTree representing a full tree. 20 | pub type HashTree = hash_tree::HashTree>; 21 | /// A HashTreeNode representing a node in a tree. 22 | pub type HashTreeNode = hash_tree::HashTreeNode>; 23 | /// For labeled [`HashTreeNode`](hash_tree::HashTreeNode) 24 | pub type Label = hash_tree::Label>; 25 | /// A result of looking up for a subtree. 26 | pub type SubtreeLookupResult = hash_tree::SubtreeLookupResult>; 27 | 28 | /// A `Delegation` as defined in 29 | pub type Delegation = certificate::Delegation>; 30 | /// A `Certificate` as defined in 31 | pub type Certificate = certificate::Certificate>; 32 | 33 | /// Create an empty hash tree. 34 | #[inline] 35 | pub fn empty() -> HashTree { 36 | hash_tree::empty() 37 | } 38 | 39 | /// Create a forked tree from two trees or node. 40 | #[inline] 41 | pub fn fork(left: HashTree, right: HashTree) -> HashTree { 42 | hash_tree::fork(left, right) 43 | } 44 | 45 | /// Create a labeled hash tree. 46 | #[inline] 47 | pub fn labeled, N: Into>(label: L, node: N) -> HashTree { 48 | hash_tree::label(label, node) 49 | } 50 | 51 | /// Create a leaf in the tree. 52 | #[inline] 53 | pub fn leaf>>(leaf: L) -> HashTree { 54 | hash_tree::leaf(leaf) 55 | } 56 | 57 | /// Create a pruned tree node. 58 | #[inline] 59 | pub fn pruned>(content: C) -> HashTree { 60 | hash_tree::pruned(content) 61 | } 62 | 63 | /// Create a pruned tree node, from a hex representation of the data. Useful for 64 | /// testing or hard coded values. 65 | #[inline] 66 | pub fn pruned_from_hex>(content: C) -> Result { 67 | hash_tree::pruned_from_hex(content) 68 | } 69 | 70 | #[cfg(feature = "serde")] 71 | mod serde_impl { 72 | use std::borrow::Cow; 73 | 74 | use serde::Deserialize; 75 | use serde_bytes::{ByteBuf, Bytes}; 76 | 77 | /// A trait to genericize deserializing owned or borrowed bytes 78 | pub trait Storage { 79 | type Temp<'a>: Deserialize<'a>; 80 | type Value<'a>: AsRef<[u8]>; 81 | fn convert(t: Self::Temp<'_>) -> Self::Value<'_>; 82 | } 83 | 84 | /// `Vec` 85 | pub struct VecStorage; 86 | /// `&[u8]` 87 | pub struct SliceStorage; 88 | /// `Cow<[u8]>` 89 | pub struct CowStorage; 90 | 91 | impl Storage for VecStorage { 92 | type Temp<'a> = ByteBuf; 93 | type Value<'a> = Vec; 94 | fn convert(t: Self::Temp<'_>) -> Self::Value<'_> { 95 | t.into_vec() 96 | } 97 | } 98 | 99 | impl Storage for SliceStorage { 100 | type Temp<'a> = &'a Bytes; 101 | type Value<'a> = &'a [u8]; 102 | fn convert(t: Self::Temp<'_>) -> Self::Value<'_> { 103 | t.as_ref() 104 | } 105 | } 106 | 107 | impl Storage for CowStorage { 108 | type Temp<'a> = &'a Bytes; 109 | type Value<'a> = Cow<'a, [u8]>; 110 | fn convert(t: Self::Temp<'_>) -> Self::Value<'_> { 111 | Cow::Borrowed(t.as_ref()) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /packages/ic-http-certification-tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic-http-certification-tests" 3 | description = "Tests for the ic-http-certification and ic-response-verification packages" 4 | readme = "README.md" 5 | 6 | version.workspace = true 7 | authors.workspace = true 8 | edition.workspace = true 9 | repository.workspace = true 10 | license.workspace = true 11 | homepage.workspace = true 12 | 13 | [dev-dependencies] 14 | ic-response-verification-test-utils.workspace = true 15 | ic-response-verification.workspace = true 16 | ic-http-certification.workspace = true 17 | ic-certificate-verification.workspace = true 18 | ic-certification-testing.workspace = true 19 | 20 | ic-types.workspace = true 21 | 22 | candid.workspace = true 23 | hex.workspace = true 24 | rstest.workspace = true 25 | assert_matches.workspace = true 26 | -------------------------------------------------------------------------------- /packages/ic-http-certification-tests/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /packages/ic-http-certification-tests/README.md: -------------------------------------------------------------------------------- 1 | # HTTP Certification Tests 2 | 3 | This crate holds integration tests for the [`ic-http-certification`](../ic-http-certification/README.md) and [`ic-response-verification`](../ic-response-verification/README.md) crates. Each crate has its own unit tests to validate functionality in isolation, while these integration tests serve the purpose of allowing these crates to mutually verify one another when used in combination. 4 | -------------------------------------------------------------------------------- /packages/ic-http-certification/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic-http-certification" 3 | description = "Certification for HTTP responses for the Internet Computer" 4 | readme = "README.md" 5 | documentation = "https://docs.rs/ic-http-certification" 6 | categories = ["api-bindings", "data-structures", "algorithms", "cryptography::cryptocurrencies"] 7 | keywords = ["internet-computer", "agent", "utility", "icp", "dfinity"] 8 | include = ["src", "Cargo.toml", "LICENSE", "README.md"] 9 | 10 | version.workspace = true 11 | authors.workspace = true 12 | edition.workspace = true 13 | repository.workspace = true 14 | license.workspace = true 15 | homepage.workspace = true 16 | 17 | [dependencies] 18 | candid.workspace = true 19 | serde.workspace = true 20 | http.workspace = true 21 | urlencoding.workspace = true 22 | ic-representation-independent-hash.workspace = true 23 | ic-certification = { workspace = true, features = ["serde"] } 24 | thiserror.workspace = true 25 | base64.workspace = true 26 | serde_cbor.workspace = true 27 | 28 | [dev-dependencies] 29 | rstest.workspace = true 30 | rstest_reuse.workspace = true 31 | hex.workspace = true 32 | assert_matches.workspace = true 33 | -------------------------------------------------------------------------------- /packages/ic-http-certification/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /packages/ic-http-certification/src/cel/mod.rs: -------------------------------------------------------------------------------- 1 | //! The CEL module contains functions and builders for creating CEL expression 2 | //! definitions and converting them into their `String` representation. 3 | 4 | mod cel_builder; 5 | pub use cel_builder::*; 6 | 7 | mod cel_types; 8 | pub use cel_types::*; 9 | 10 | mod create_cel_expr; 11 | pub use create_cel_expr::*; 12 | 13 | #[cfg(test)] 14 | mod fixtures; 15 | -------------------------------------------------------------------------------- /packages/ic-http-certification/src/error.rs: -------------------------------------------------------------------------------- 1 | //! The error module contains types for common errors that may be thrown 2 | //! by other modules in this crate. 3 | 4 | /// HTTP certification result type. 5 | pub type HttpCertificationResult = Result; 6 | 7 | /// HTTP certification error type. 8 | #[derive(thiserror::Error, Debug, Clone)] 9 | pub enum HttpCertificationError { 10 | /// The URL was malformed and could not be parsed correctly. 11 | #[error(r#"Failed to parse url: "{0}""#)] 12 | MalformedUrl(String), 13 | 14 | /// Error converting UTF-8 string. 15 | #[error(r#"Error converting UTF8 string bytes: "{0}""#)] 16 | Utf8ConversionError(#[from] std::string::FromUtf8Error), 17 | 18 | /// Error converting bytes to string. 19 | #[error(r#"Wildcard path "{wildcard_path}" is too specific for request path "{request_path}", use a less specific wildcard path"#)] 20 | WildcardPathNotValidForRequestPath { 21 | /// The wildcard path that was not valid for the request path. 22 | wildcard_path: String, 23 | 24 | /// The request path that was not valid for the wildcard path. 25 | request_path: String, 26 | }, 27 | 28 | /// The `IC-CertificateExpression` header in a response did not match the Cel expression used to certify the [HttpResponse](crate::HttpResponse). 29 | #[error(r#"The IC-CertificateExpression header in the response did not match the Cel expression used to certify the response. Expected: "{expected}", Actual: "{actual}""#)] 30 | CertificateExpressionHeaderMismatch { 31 | /// The expected value of the `IC-CertificateExpression` header. This is the Cel expression used to certify the [HttpResponse](crate::HttpResponse). 32 | expected: String, 33 | 34 | /// The actual value of the `IC-CertificateExpression` header. 35 | actual: String, 36 | }, 37 | 38 | /// The `IC-CertificateExpression header` was missing from the [HttpResponse](crate::HttpResponse). 39 | #[error(r#"The IC-CertificateExpression header was missing from the response. Expected: "{expected}""#)] 40 | CertificateExpressionHeaderMissing { 41 | /// The expected value of the `IC-CertificateExpression` header. This is the Cel expression used to certify the [HttpResponse](crate::HttpResponse). 42 | expected: String, 43 | }, 44 | 45 | /// The `IC-CertificateExpression` header in a response contained multiple values. 46 | #[error(r#"The IC-CertificateExpression header in the response contained multiple values. Expected only one: "{expected}""#)] 47 | MultipleCertificateExpressionHeaders { 48 | /// The expected value of the `IC-CertificateExpression` header. This is the Cel expression used to certify the [HttpResponse](crate::HttpResponse). 49 | expected: String, 50 | }, 51 | 52 | /// Error converting a number into an HTTP status code. 53 | #[error(r#"Error converting number into HTTP status code: "{status_code}""#)] 54 | InvalidHttpStatusCode { 55 | /// The HTTP status code that was not recognized. 56 | status_code: u16, 57 | }, 58 | } 59 | -------------------------------------------------------------------------------- /packages/ic-http-certification/src/hash/mod.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for calculating 2 | //! [Representation Independent Hashes](https://internetcomputer.org/docs/references/ic-interface-spec/#hash-of-map) 3 | //! of [HttpRequest](crate::HttpRequest) and [HttpResponse](crate::HttpRequest) objects. 4 | 5 | mod request_hash; 6 | pub use request_hash::*; 7 | 8 | mod response_hash; 9 | pub use response_hash::*; 10 | 11 | /// Sha256 Digest: 32 bytes 12 | pub type Hash = [u8; 32]; 13 | -------------------------------------------------------------------------------- /packages/ic-http-certification/src/http/header_field.rs: -------------------------------------------------------------------------------- 1 | /// An HTTP header field, represented as a tuple of (name, value). 2 | pub type HeaderField = (String, String); 3 | -------------------------------------------------------------------------------- /packages/ic-http-certification/src/http/mod.rs: -------------------------------------------------------------------------------- 1 | //! The HTTP module contains types for representing HTTP requests and responses in Rust. 2 | //! These types are Candid-encodable and are used by canisters that implement the 3 | //! HTTP interface required by the HTTP Gateway Protocol. 4 | 5 | mod header_field; 6 | mod http_request; 7 | mod http_response; 8 | 9 | pub use header_field::*; 10 | pub use http_request::*; 11 | pub use http_response::*; 12 | -------------------------------------------------------------------------------- /packages/ic-http-certification/src/tree/mod.rs: -------------------------------------------------------------------------------- 1 | //! The Tree module contains functions and builders for managing certified 2 | //! [HttpRequest](crate::HttpRequest) and [HttpResponse](crate::HttpResponse) pairs in a 3 | //! purpose-build HTTP certification data structure. 4 | //! 5 | //! Certifications are prepared using the [HttpCertification] enum. 6 | 7 | mod certification; 8 | mod certification_tree; 9 | mod certification_tree_entry; 10 | mod certification_tree_path; 11 | 12 | pub use certification::*; 13 | pub use certification_tree::*; 14 | pub use certification_tree_entry::*; 15 | pub use certification_tree_path::*; 16 | -------------------------------------------------------------------------------- /packages/ic-http-certification/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | //! The utils module contains utility functions used internally by this library. 2 | //! They are exported for use in other libraries that depend on this one, or for 3 | //! advanced use cases that require custom logic. 4 | 5 | mod response_header; 6 | pub use response_header::*; 7 | 8 | mod wildcard_paths; 9 | pub use wildcard_paths::*; 10 | 11 | mod skip_certification; 12 | pub use skip_certification::*; 13 | -------------------------------------------------------------------------------- /packages/ic-http-certification/src/utils/skip_certification.rs: -------------------------------------------------------------------------------- 1 | use super::add_v2_certificate_header; 2 | use crate::{ 3 | DefaultCelBuilder, Hash, HttpCertificationPath, HttpResponse, 4 | CERTIFICATE_EXPRESSION_HEADER_NAME, 5 | }; 6 | use ic_certification::{hash_tree::leaf, labeled, HashTree}; 7 | use ic_representation_independent_hash::hash; 8 | 9 | /// Adds the `IC-Certificate` and `IC-Certificate-Expression` headers to a given [`HttpResponse`]. These headers are used by the HTTP Gateway 10 | /// to verify the authenticity of query call responses. In this case, the headers are pre-configured to instruct 11 | /// the HTTP Gateway to skip certification verification in a secure way. Secure in this context means that 12 | /// the decision to skip certification is made by the canister itself, and not by the replica, API boundary nodes 13 | /// or any other intermediate party. 14 | /// 15 | /// # Arguments 16 | /// 17 | /// * `data_certificate` - A certificate used by the HTTP Gateway to verify a response. 18 | /// Retrieved using `ic_cdk::api::data_certificate`. 19 | /// * `response` - The [`HttpResponse`] to add the certificate header to. 20 | /// Created using [`HttpResponse::builder()`](crate::HttpResponse::builder). 21 | /// 22 | /// # Examples 23 | /// 24 | /// ``` 25 | /// use ic_http_certification::{HttpResponse, DefaultCelBuilder, utils::add_skip_certification_header, CERTIFICATE_EXPRESSION_HEADER_NAME, CERTIFICATE_HEADER_NAME}; 26 | /// 27 | /// let mut response = HttpResponse::builder().build(); 28 | /// 29 | /// // this should normally be retrieved using `ic_cdk::api::data_certificate()`. 30 | /// let data_certificate = vec![1, 2, 3]; 31 | /// 32 | /// add_skip_certification_header(data_certificate, &mut response); 33 | /// 34 | /// assert_eq!( 35 | /// response.headers(), 36 | /// vec![ 37 | /// ( 38 | /// CERTIFICATE_HEADER_NAME.to_string(), 39 | /// "certificate=:AQID:, tree=:2dn3gwJJaHR0cF9leHBygwJDPCo+gwJYIMMautvQsFn51GT9bfTani3Ah659C0BGjTNyJtQTszcjggNA:, expr_path=:2dn3gmlodHRwX2V4cHJjPCo+:, version=2".to_string(), 40 | /// ), 41 | /// ( 42 | /// CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(), 43 | /// DefaultCelBuilder::skip_certification().to_string() 44 | /// ), 45 | /// ] 46 | /// ); 47 | /// ``` 48 | pub fn add_skip_certification_header(data_certificate: Vec, response: &mut HttpResponse) { 49 | add_v2_certificate_header( 50 | &data_certificate, 51 | response, 52 | &skip_certification_asset_tree(), 53 | &HttpCertificationPath::wildcard("").to_expr_path(), 54 | ); 55 | 56 | response.add_header(( 57 | CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(), 58 | DefaultCelBuilder::skip_certification().to_string(), 59 | )); 60 | } 61 | 62 | /// Returns the hash of the certified data that can be used to instruct HTTP Gateways to skip certification. 63 | /// 64 | /// # Examples 65 | /// 66 | /// ```ignore 67 | /// use ic_http_certification::utils::skip_certification_certified_data; 68 | /// use ic_cdk::api::set_certified_data; 69 | /// 70 | /// let certified_data = skip_certification_certified_data(); 71 | /// 72 | /// set_certified_data(&certified_data); 73 | /// ``` 74 | pub fn skip_certification_certified_data() -> Hash { 75 | skip_certification_asset_tree().digest() 76 | } 77 | 78 | fn skip_certification_asset_tree() -> HashTree { 79 | let cel_expr_hash = hash( 80 | DefaultCelBuilder::skip_certification() 81 | .to_string() 82 | .as_bytes(), 83 | ); 84 | 85 | labeled( 86 | "http_expr", 87 | labeled("<*>", labeled(cel_expr_hash, leaf(vec![]))), 88 | ) 89 | } 90 | 91 | #[cfg(test)] 92 | mod tests { 93 | use super::*; 94 | 95 | #[test] 96 | fn test_skip_certification_certified_data() { 97 | let certified_data = skip_certification_certified_data(); 98 | 99 | assert_eq!( 100 | certified_data, 101 | [ 102 | 85, 236, 195, 28, 62, 128, 71, 252, 21, 143, 32, 234, 10, 160, 96, 154, 172, 199, 103 | 181, 126, 6, 234, 64, 220, 65, 134, 2, 114, 167, 214, 66, 145 104 | ] 105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /packages/ic-representation-independent-hash/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic-representation-independent-hash" 3 | description = "A library for computing representation-independent hashes as described in the Internet Computer interface specification." 4 | readme = "README.md" 5 | documentation = "https://docs.rs/ic-representation-independent-hash" 6 | categories = ["api-bindings", "algorithms", "cryptography::cryptocurrencies"] 7 | keywords = ["icp", "dfinity", "representation", "independent", "hash"] 8 | include = ["src", "Cargo.toml", "README.md"] 9 | 10 | version.workspace = true 11 | authors.workspace = true 12 | edition.workspace = true 13 | repository.workspace = true 14 | license.workspace = true 15 | homepage.workspace = true 16 | 17 | [dependencies] 18 | sha2.workspace = true 19 | leb128.workspace = true 20 | 21 | [dev-dependencies] 22 | hex.workspace = true 23 | -------------------------------------------------------------------------------- /packages/ic-representation-independent-hash/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /packages/ic-representation-independent-hash/README.md: -------------------------------------------------------------------------------- 1 | # Representation Independent Hash 2 | 3 | Utilities for calculating [Representation Independent Hashes](https://internetcomputer.org/docs/references/ic-interface-spec/#hash-of-map) of arbitrary Rust objects. 4 | -------------------------------------------------------------------------------- /packages/ic-representation-independent-hash/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Representation Independent Hash 2 | 3 | #![deny(missing_docs, missing_debug_implementations, rustdoc::all, clippy::all)] 4 | 5 | /// Type alias for a SHA-256 hash. 6 | pub type Sha256Digest = [u8; 32]; 7 | 8 | mod representation_independent_hash; 9 | pub use representation_independent_hash::*; 10 | 11 | use sha2::{Digest, Sha256}; 12 | 13 | /// Calculates the SHA-256 hash of the given slice. 14 | pub fn hash(data: &[u8]) -> Sha256Digest { 15 | let mut hasher = Sha256::new(); 16 | hasher.update(data); 17 | hasher.finalize().into() 18 | } 19 | 20 | #[cfg(test)] 21 | mod tests { 22 | use super::*; 23 | 24 | #[test] 25 | fn hash_text() { 26 | let text = "Hello World!"; 27 | let expected_hash: Sha256Digest = [ 28 | 127, 131, 177, 101, 127, 241, 252, 83, 185, 45, 193, 129, 72, 161, 214, 93, 252, 45, 29 | 75, 31, 163, 214, 119, 40, 74, 221, 210, 0, 18, 109, 144, 105, 30 | ]; 31 | 32 | let result = hash(text.as_bytes()); 33 | 34 | assert_eq!(result, expected_hash); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/ic-response-verification-test-utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic-response-verification-test-utils" 3 | 4 | version.workspace = true 5 | authors.workspace = true 6 | edition.workspace = true 7 | repository.workspace = true 8 | license.workspace = true 9 | homepage.workspace = true 10 | 11 | [dependencies] 12 | base64.workspace = true 13 | hex.workspace = true 14 | ic-certification-testing.workspace = true 15 | ic-types.workspace = true 16 | sha2.workspace = true 17 | serde_cbor.workspace = true 18 | leb128.workspace = true 19 | serde.workspace = true 20 | flate2.workspace = true 21 | ic-certification = { workspace = true, features = ["default"] } 22 | ic-http-certification = { workspace = true } 23 | -------------------------------------------------------------------------------- /packages/ic-response-verification-test-utils/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /packages/ic-response-verification-test-utils/src/asset_tree.rs: -------------------------------------------------------------------------------- 1 | use crate::hash::hash; 2 | use crate::{cbor_encode, hash_from_hex}; 3 | use ic_certification::{labeled, labeled_hash, AsHashTree, Hash, HashTree, RbTree}; 4 | 5 | const LABEL_ASSETS: &[u8] = b"http_assets"; 6 | 7 | pub struct AssetTree { 8 | tree: RbTree<&'static str, Hash>, 9 | } 10 | 11 | impl Default for AssetTree { 12 | fn default() -> Self { 13 | let mut asset_tree = Self::new(); 14 | let body = "Hello World!"; 15 | 16 | asset_tree.insert(Self::DEFAULT_PATH, body); 17 | 18 | asset_tree 19 | } 20 | } 21 | 22 | impl AssetTree { 23 | pub const DEFAULT_PATH: &'static str = "/"; 24 | 25 | pub fn new() -> Self { 26 | Self { 27 | tree: RbTree::new(), 28 | } 29 | } 30 | 31 | pub fn insert(&mut self, path: &'static str, body: &str) { 32 | let body_hash = hash(body); 33 | 34 | self.tree.insert(path, body_hash); 35 | } 36 | 37 | pub fn serialize_to_cbor(&self, path: Option<&'static str>) -> Vec { 38 | let path = path.unwrap_or(Self::DEFAULT_PATH); 39 | let tree = self.tree.witness(path.as_bytes()); 40 | let labeled_tree = labeled(LABEL_ASSETS, tree); 41 | 42 | cbor_encode::(&labeled_tree) 43 | } 44 | 45 | pub fn get_certified_data(&self) -> [u8; 32] { 46 | let root_hash = self.tree.root_hash(); 47 | 48 | labeled_hash(LABEL_ASSETS, &root_hash) 49 | } 50 | } 51 | 52 | pub fn create_certified_data(data: &str) -> [u8; 32] { 53 | let hash = hash_from_hex(data); 54 | 55 | labeled_hash(LABEL_ASSETS, &hash) 56 | } 57 | -------------------------------------------------------------------------------- /packages/ic-response-verification-test-utils/src/encoding.rs: -------------------------------------------------------------------------------- 1 | use base64::{engine::general_purpose, Engine as _}; 2 | use flate2::write::{DeflateEncoder, GzEncoder}; 3 | use flate2::Compression; 4 | use serde::Serialize; 5 | use std::io::Write; 6 | 7 | pub fn base64_encode(data: &[u8]) -> String { 8 | general_purpose::STANDARD.encode(data) 9 | } 10 | 11 | pub fn base64_decode(data: &str) -> Vec { 12 | general_purpose::STANDARD.decode(data).unwrap() 13 | } 14 | 15 | pub fn hex_encode(data: &[u8]) -> String { 16 | hex::encode(data) 17 | } 18 | 19 | pub fn hex_decode(data: &str) -> Vec { 20 | hex::decode(data).unwrap() 21 | } 22 | 23 | pub fn cbor_encode(payload: &T) -> Vec { 24 | let mut serializer = serde_cbor::Serializer::new(Vec::new()); 25 | serializer.self_describe().unwrap(); 26 | payload.serialize(&mut serializer).unwrap(); 27 | serializer.into_inner() 28 | } 29 | 30 | pub fn leb_encode_timestamp(timestamp: u128) -> Vec { 31 | let mut encoded_time = vec![]; 32 | leb128::write::unsigned(&mut encoded_time, timestamp as u64).unwrap(); 33 | encoded_time 34 | } 35 | 36 | pub fn gzip_encode(data: &[u8]) -> Vec { 37 | let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); 38 | encoder.write_all(data).unwrap(); 39 | 40 | encoder.finish().unwrap() 41 | } 42 | 43 | pub fn deflate_encode(data: &[u8]) -> Vec { 44 | let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default()); 45 | encoder.write_all(data).unwrap(); 46 | 47 | encoder.finish().unwrap() 48 | } 49 | -------------------------------------------------------------------------------- /packages/ic-response-verification-test-utils/src/hash.rs: -------------------------------------------------------------------------------- 1 | use ic_certification::Hash; 2 | use sha2::{Digest, Sha256}; 3 | 4 | pub fn hash(data: T) -> Hash 5 | where 6 | T: AsRef<[u8]>, 7 | { 8 | let mut hasher = Sha256::new(); 9 | hasher.update(data); 10 | hasher.finalize().into() 11 | } 12 | 13 | pub fn hash_from_hex>(data: T) -> Hash { 14 | hex::decode(data).unwrap().try_into().unwrap() 15 | } 16 | -------------------------------------------------------------------------------- /packages/ic-response-verification-test-utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | 3 | mod asset_tree; 4 | pub use asset_tree::*; 5 | 6 | mod certificate; 7 | pub use certificate::*; 8 | 9 | mod encoding; 10 | pub use encoding::*; 11 | 12 | mod hash; 13 | pub use hash::*; 14 | 15 | mod timestamp; 16 | pub use timestamp::*; 17 | 18 | mod utils; 19 | pub use utils::*; 20 | 21 | mod v2_certificate_fixture; 22 | pub use v2_certificate_fixture::*; 23 | -------------------------------------------------------------------------------- /packages/ic-response-verification-test-utils/src/timestamp.rs: -------------------------------------------------------------------------------- 1 | use std::time::{SystemTime, UNIX_EPOCH}; 2 | 3 | pub fn get_timestamp(time: SystemTime) -> u128 { 4 | time.duration_since(UNIX_EPOCH).unwrap().as_nanos() 5 | } 6 | 7 | pub fn get_current_timestamp() -> u128 { 8 | get_timestamp(SystemTime::now()) 9 | } 10 | -------------------------------------------------------------------------------- /packages/ic-response-verification-test-utils/src/utils.rs: -------------------------------------------------------------------------------- 1 | pub fn remove_whitespace(s: &str) -> String { 2 | s.chars().filter(|c| !c.is_whitespace()).collect() 3 | } 4 | -------------------------------------------------------------------------------- /packages/ic-response-verification-test-utils/src/v2_certificate_fixture.rs: -------------------------------------------------------------------------------- 1 | use crate::{cbor_encode, create_versioned_certificate_header}; 2 | use ic_certification_testing::{CertificateBuilder, CertificateData}; 3 | use ic_http_certification::{HttpCertificationTree, HttpCertificationTreeEntry}; 4 | use ic_types::CanisterId; 5 | 6 | pub struct V2TreeFixture { 7 | pub tree_cbor: Vec, 8 | pub certified_data: [u8; 32], 9 | } 10 | 11 | pub fn create_v2_tree_fixture( 12 | req_path: &str, 13 | certification_tree_entry: &HttpCertificationTreeEntry, 14 | ) -> V2TreeFixture { 15 | let mut tree = HttpCertificationTree::default(); 16 | tree.insert(certification_tree_entry); 17 | 18 | let certified_data = tree.root_hash(); 19 | let witness = tree.witness(certification_tree_entry, req_path).unwrap(); 20 | let tree_cbor = cbor_encode(&witness); 21 | 22 | V2TreeFixture { 23 | tree_cbor, 24 | certified_data, 25 | } 26 | } 27 | 28 | pub struct V2CertificateFixture { 29 | pub root_key: Vec, 30 | pub certificate_cbor: Vec, 31 | pub canister_id: CanisterId, 32 | } 33 | 34 | pub fn create_v2_certificate_fixture( 35 | certified_data: &[u8; 32], 36 | current_time: &u128, 37 | ) -> V2CertificateFixture { 38 | let canister_id = CanisterId::from_u64(5); 39 | 40 | let CertificateData { 41 | root_key, 42 | certificate: _, 43 | cbor_encoded_certificate, 44 | } = CertificateBuilder::new(&canister_id.to_string(), certified_data) 45 | .unwrap() 46 | .with_time(*current_time) 47 | .with_delegation(123, vec![(0, 10)]) 48 | .build() 49 | .unwrap(); 50 | 51 | V2CertificateFixture { 52 | root_key, 53 | certificate_cbor: cbor_encoded_certificate, 54 | canister_id, 55 | } 56 | } 57 | 58 | pub fn create_v2_header( 59 | certification_tree_entry: &HttpCertificationTreeEntry, 60 | certificate_cbor: &[u8], 61 | tree_cbor: &[u8], 62 | ) -> String { 63 | create_versioned_certificate_header( 64 | certificate_cbor, 65 | tree_cbor, 66 | cbor_encode(&certification_tree_entry.path.to_expr_path()).as_slice(), 67 | 2, 68 | ) 69 | } 70 | 71 | pub struct V2Fixture { 72 | pub root_key: Vec, 73 | pub certificate_header: String, 74 | pub canister_id: CanisterId, 75 | } 76 | 77 | pub fn create_v2_fixture( 78 | req_path: &str, 79 | certification_tree_entry: &HttpCertificationTreeEntry, 80 | current_time: &u128, 81 | ) -> V2Fixture { 82 | let V2TreeFixture { 83 | tree_cbor, 84 | certified_data, 85 | } = create_v2_tree_fixture(req_path, certification_tree_entry); 86 | 87 | let V2CertificateFixture { 88 | root_key, 89 | certificate_cbor, 90 | canister_id, 91 | } = create_v2_certificate_fixture(&certified_data, current_time); 92 | 93 | let certificate_header = 94 | create_v2_header(certification_tree_entry, &certificate_cbor, &tree_cbor); 95 | 96 | V2Fixture { 97 | root_key, 98 | certificate_header, 99 | canister_id, 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/README.md: -------------------------------------------------------------------------------- 1 | # Response Verification e2e Tests 2 | 3 | ## Run tests 4 | 5 | ```shell 6 | ./scripts/e2e.sh 7 | ``` 8 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/frontend/.ic-assets.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "match": "**/*", 4 | "security_policy": "hardened", 5 | "headers": { 6 | "content-security-policy": "default-src 'self';script-src 'self' 'unsafe-eval';connect-src 'self' https://ic0.app https://*.ic0.app;img-src 'self' data:;style-src * 'unsafe-inline';style-src-elem * 'unsafe-inline';font-src *;object-src 'none';base-uri 'self';frame-ancestors 'none';form-action 'self';upgrade-insecure-requests;", 7 | "cross-origin-embedder-policy": "require-corp" 8 | } 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | canister 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | DFINITY logo 15 |
16 |
17 | 18 |
19 | 20 | 21 | 22 |
23 | 24 |
25 |
26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "response-verification-tests-frontend", 3 | "scripts": { 4 | "start": "vite", 5 | "build": "vite build" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/frontend/src/assets/a/b/something.txt: -------------------------------------------------------------------------------- 1 | something 2 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/frontend/src/assets/a/c/else.txt: -------------------------------------------------------------------------------- 1 | else 2 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/frontend/src/assets/another sample asset.txt: -------------------------------------------------------------------------------- 1 | This is another sample asset with special characters 2 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/frontend/src/assets/capture-d’écran-2023-10-26-à.txt: -------------------------------------------------------------------------------- 1 | Bonjour tout le monde! 2 | 3 | (This file is used to test assets with special characters) 4 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/frontend/src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/response-verification/b4c8c472e5ca98502f846ef3b860d1f648ced68d/packages/ic-response-verification-tests/src/frontend/src/assets/favicon.ico -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/frontend/src/assets/hello/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | canister 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

Hello World!

15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/frontend/src/assets/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | font-size: 1.5rem; 4 | } 5 | 6 | img { 7 | max-width: 50vw; 8 | max-height: 25vw; 9 | display: block; 10 | margin: auto; 11 | } 12 | 13 | form { 14 | display: flex; 15 | justify-content: center; 16 | gap: 0.5em; 17 | flex-flow: row wrap; 18 | max-width: 40vw; 19 | margin: auto; 20 | align-items: baseline; 21 | } 22 | 23 | button[type='submit'] { 24 | padding: 5px 20px; 25 | margin: 10px auto; 26 | float: right; 27 | } 28 | 29 | #greeting { 30 | margin: 10px auto; 31 | padding: 10px 60px; 32 | border: 1px solid #222; 33 | } 34 | 35 | #greeting:empty { 36 | display: none; 37 | } 38 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/frontend/src/assets/sample-asset.txt: -------------------------------------------------------------------------------- 1 | This is a sample asset! 2 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/frontend/src/assets/world.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | canister 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

Hello World?????

15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/frontend/src/index.js: -------------------------------------------------------------------------------- 1 | document.querySelector('form').addEventListener('submit', async e => { 2 | e.preventDefault(); 3 | 4 | const name = document.getElementById('name').value.toString(); 5 | 6 | document.getElementById('greeting').innerText = `Hello ${name}!`; 7 | 8 | return false; 9 | }); 10 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["vite/client"], 5 | "noEmit": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/frontend/vite.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import checker from 'vite-plugin-checker'; 3 | import { viteStaticCopy } from 'vite-plugin-static-copy'; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | checker({ typescript: true }), 8 | viteStaticCopy({ 9 | targets: [ 10 | { 11 | src: '.ic-assets.json', 12 | dest: '.', 13 | }, 14 | { 15 | src: 'src/assets', 16 | dest: '.', 17 | }, 18 | ], 19 | }), 20 | ], 21 | optimizeDeps: { 22 | esbuildOptions: { 23 | define: { 24 | global: 'globalThis', 25 | }, 26 | }, 27 | }, 28 | server: { 29 | proxy: { 30 | '/api': 'http://127.0.0.1:8000', 31 | }, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/rust-tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic-response-verification-tests" 3 | 4 | version.workspace = true 5 | authors.workspace = true 6 | edition.workspace = true 7 | repository.workspace = true 8 | license.workspace = true 9 | homepage.workspace = true 10 | 11 | [dependencies] 12 | hex.workspace = true 13 | http.workspace = true 14 | ic-agent.workspace = true 15 | ic-utils.workspace = true 16 | tokio.workspace = true 17 | ic-response-verification.workspace = true 18 | ic-http-certification.workspace = true 19 | anyhow.workspace = true 20 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/rust-tests/src/agent.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use ic_agent::Agent; 3 | 4 | pub async fn create_agent(url: &str) -> Result { 5 | let agent = Agent::builder().with_url(url).build()?; 6 | agent.fetch_root_key().await?; 7 | 8 | Ok(agent) 9 | } 10 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/wasm-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "response-verification-tests", 3 | "scripts": { 4 | "e2e-test": "ts-node ./src/main.ts" 5 | }, 6 | "dependencies": { 7 | "@dfinity/response-verification": "workspace:*" 8 | }, 9 | "devDependencies": { 10 | "ts-node": "^10.9.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/wasm-tests/src/http-interface/canister_http_interface.did: -------------------------------------------------------------------------------- 1 | // adapted from https://internetcomputer.org/docs/references/ic-interface-spec/#http-gateway-interface 2 | 3 | type HeaderField = record { text; text; }; 4 | 5 | type HttpRequest = record { 6 | method: text; 7 | url: text; 8 | headers: vec HeaderField; 9 | body: blob; 10 | certificate_version: opt nat16; 11 | }; 12 | 13 | type HttpUpdateRequest = record { 14 | method: text; 15 | url: text; 16 | headers: vec HeaderField; 17 | body: blob; 18 | }; 19 | 20 | type HttpResponse = record { 21 | status_code: nat16; 22 | headers: vec HeaderField; 23 | body: blob; 24 | upgrade : opt bool; 25 | streaming_strategy: opt StreamingStrategy; 26 | }; 27 | 28 | type Token = variant { 29 | "type": reserved; 30 | }; 31 | 32 | type StreamingCallbackHttpResponse = record { 33 | body: blob; 34 | token: opt Token; 35 | }; 36 | 37 | type StreamingStrategy = variant { 38 | Callback: record { 39 | callback: func (Token) -> (opt StreamingCallbackHttpResponse) query; 40 | token: Token; 41 | }; 42 | }; 43 | 44 | service : { 45 | http_request: (request: HttpRequest) -> (HttpResponse) query; 46 | http_request_update: (request: HttpUpdateRequest) -> (HttpResponse); 47 | } -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/wasm-tests/src/http-interface/canister_http_interface.ts: -------------------------------------------------------------------------------- 1 | import { IDL } from '@dfinity/candid'; 2 | 3 | const Token = IDL.Unknown; 4 | 5 | export const streamingCallbackHttpResponseType = IDL.Record({ 6 | token: IDL.Opt(Token), 7 | body: IDL.Vec(IDL.Nat8), 8 | }); 9 | 10 | export const idlFactory: IDL.InterfaceFactory = ({ IDL }) => { 11 | const HeaderField = IDL.Tuple(IDL.Text, IDL.Text); 12 | const HttpRequest = IDL.Record({ 13 | url: IDL.Text, 14 | method: IDL.Text, 15 | body: IDL.Vec(IDL.Nat8), 16 | headers: IDL.Vec(HeaderField), 17 | certificate_version: IDL.Opt(IDL.Nat16), 18 | }); 19 | const StreamingStrategy = IDL.Variant({ 20 | Callback: IDL.Record({ 21 | token: Token, 22 | callback: IDL.Func( 23 | [Token], 24 | [IDL.Opt(streamingCallbackHttpResponseType)], 25 | ['query'], 26 | ), 27 | }), 28 | }); 29 | const HttpResponse = IDL.Record({ 30 | body: IDL.Vec(IDL.Nat8), 31 | headers: IDL.Vec(HeaderField), 32 | upgrade: IDL.Opt(IDL.Bool), 33 | streaming_strategy: IDL.Opt(StreamingStrategy), 34 | status_code: IDL.Nat16, 35 | }); 36 | const HttpUpdateRequest = IDL.Record({ 37 | url: IDL.Text, 38 | method: IDL.Text, 39 | body: IDL.Vec(IDL.Nat8), 40 | headers: IDL.Vec(HeaderField), 41 | }); 42 | return IDL.Service({ 43 | http_request: IDL.Func([HttpRequest], [HttpResponse], ['query']), 44 | http_request_update: IDL.Func([HttpUpdateRequest], [HttpResponse], []), 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/wasm-tests/src/http-interface/canister_http_interface_types.d.ts: -------------------------------------------------------------------------------- 1 | import type { Principal } from '@dfinity/principal'; 2 | import type { ActorMethod } from '@dfinity/agent'; 3 | import { IDL } from '@dfinity/candid'; 4 | 5 | export type HeaderField = [string, string]; 6 | export interface HttpRequest { 7 | url: string; 8 | method: string; 9 | body: Uint8Array; 10 | headers: Array; 11 | certificate_version: [] | [number]; 12 | } 13 | export interface HttpResponse { 14 | body: Uint8Array; 15 | headers: Array; 16 | upgrade: [] | [boolean]; 17 | streaming_strategy: [] | [StreamingStrategy]; 18 | status_code: number; 19 | } 20 | export interface HttpUpdateRequest { 21 | url: string; 22 | method: string; 23 | body: Uint8Array; 24 | headers: Array; 25 | } 26 | export interface StreamingCallbackHttpResponse { 27 | token: [] | [Token]; 28 | body: Uint8Array; 29 | } 30 | export type StreamingStrategy = { 31 | Callback: { token: Token; callback: [Principal, string] }; 32 | }; 33 | export type Token = { type: () => IDL.Type }; 34 | export interface _SERVICE { 35 | http_request: ActorMethod<[HttpRequest], HttpResponse>; 36 | http_request_update: ActorMethod<[HttpUpdateRequest], HttpResponse>; 37 | } 38 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/wasm-tests/src/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | VerificationInfo, 3 | Request, 4 | Response, 5 | getMinVerificationVersion, 6 | verifyRequestResponsePair, 7 | } from '@dfinity/response-verification'; 8 | 9 | import { idlFactory } from './http-interface/canister_http_interface'; 10 | import { 11 | HttpRequest, 12 | _SERVICE, 13 | } from './http-interface/canister_http_interface_types'; 14 | import { HttpAgent, ActorSubclass, Actor, Agent } from '@dfinity/agent'; 15 | import { Principal } from '@dfinity/principal'; 16 | import assert from 'node:assert'; 17 | import { exit } from 'process'; 18 | 19 | async function createAgentAndActor( 20 | gatewayUrl: string, 21 | canisterId: Principal, 22 | ): Promise<[HttpAgent, ActorSubclass<_SERVICE>]> { 23 | const agent = new HttpAgent({ host: gatewayUrl }); 24 | await agent.fetchRootKey(); 25 | 26 | const actor = Actor.createActor<_SERVICE>(idlFactory, { 27 | agent, 28 | canisterId: canisterId, 29 | }); 30 | return [agent, actor]; 31 | } 32 | 33 | async function main(): Promise { 34 | try { 35 | const replicaAddress = process.env['DFX_REPLICA_ADDRESS']; 36 | if (!replicaAddress) { 37 | throw 'The `DFX_REPLICA_ADDRESS` env variable was not provided'; 38 | } 39 | 40 | if (process.argv.length === 2) { 41 | throw 'The canister ID arg was not provided'; 42 | } 43 | const canisterId = process.argv[3]; 44 | const principal = Principal.fromText(canisterId); 45 | 46 | const [agent, actor] = await createAgentAndActor(replicaAddress, principal); 47 | 48 | await v1Test(principal, agent, actor); 49 | await v2Test(principal, agent, actor); 50 | } catch (error) { 51 | console.error('Error running e2e tests...', error); 52 | exit(1); 53 | } 54 | } 55 | 56 | async function v1Test( 57 | canisterId: Principal, 58 | agent: Agent, 59 | actor: ActorSubclass<_SERVICE>, 60 | ): Promise { 61 | const resultOne = await performTest( 62 | canisterId, 63 | 'GET', 64 | '/', 65 | null, 66 | agent, 67 | actor, 68 | ); 69 | assert.equal(resultOne.verificationVersion, 1); 70 | 71 | const resultTwo = await performTest( 72 | canisterId, 73 | 'GET', 74 | '/', 75 | null, 76 | agent, 77 | actor, 78 | ); 79 | assert.equal(resultTwo.verificationVersion, 1); 80 | } 81 | 82 | async function v2Test( 83 | canisterId: Principal, 84 | agent: Agent, 85 | actor: ActorSubclass<_SERVICE>, 86 | ): Promise { 87 | const resultOne = await performTest(canisterId, 'GET', '/', 2, agent, actor); 88 | assert.equal(resultOne.verificationVersion, 2); 89 | 90 | const resultTwo = await performTest(canisterId, 'GET', '/', 2, agent, actor); 91 | assert.equal(resultTwo.verificationVersion, 2); 92 | } 93 | 94 | async function performTest( 95 | canisterId: Principal, 96 | method: string, 97 | url: string, 98 | certificateVersion: number | null, 99 | agent: Agent, 100 | actor: ActorSubclass<_SERVICE>, 101 | ): Promise { 102 | let httpRequest: HttpRequest = { 103 | method, 104 | body: new Uint8Array(), 105 | certificate_version: certificateVersion ? [certificateVersion] : [], 106 | headers: [], 107 | url, 108 | }; 109 | 110 | let httpResponse = await actor.http_request(httpRequest); 111 | 112 | const currentTimeNs = BigInt.asUintN(64, BigInt(Date.now() * 1_000_000)); // from ms to nanoseconds 113 | const maxCertTimeOffsetNs = BigInt.asUintN(64, BigInt(300_000_000_000)); 114 | 115 | if (!agent.rootKey) { 116 | throw 'Agent does not have root key'; 117 | } 118 | 119 | return verifyRequestResponsePair( 120 | httpRequest, 121 | httpResponse, 122 | canisterId.toUint8Array(), 123 | currentTimeNs, 124 | maxCertTimeOffsetNs, 125 | new Uint8Array(agent.rootKey), 126 | certificateVersion ?? getMinVerificationVersion(), 127 | ); 128 | } 129 | 130 | main(); 131 | -------------------------------------------------------------------------------- /packages/ic-response-verification-tests/src/wasm-tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./wasm-tests/**/*"], 3 | "compilerOptions": { 4 | "esModuleInterop": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/ic-response-verification-wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic-response-verification-wasm" 3 | description = "WASM version of client side response verification for the Internet Computer" 4 | include = ["src", "Cargo.toml", "README.md"] 5 | 6 | 7 | version.workspace = true 8 | authors.workspace = true 9 | edition.workspace = true 10 | repository.workspace = true 11 | license.workspace = true 12 | homepage.workspace = true 13 | 14 | [lib] 15 | crate-type = ["cdylib", "rlib"] 16 | 17 | [package.metadata.wasm-pack.profile.release] 18 | wasm-opt = ["-Oz", "--enable-mutable-globals"] 19 | 20 | [dependencies] 21 | ic-response-verification = { workspace = true, features = ["js"] } 22 | ic-http-certification.workspace = true 23 | console_error_panic_hook.workspace = true 24 | js-sys.workspace = true 25 | wasm-bindgen.workspace = true 26 | log.workspace = true 27 | wasm-bindgen-console-logger.workspace = true 28 | 29 | [dev-dependencies] 30 | base64.workspace = true 31 | wasm-bindgen-test.workspace = true 32 | ic-response-verification-test-utils.workspace = true 33 | -------------------------------------------------------------------------------- /packages/ic-response-verification-wasm/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /packages/ic-response-verification-wasm/README.md: -------------------------------------------------------------------------------- 1 | # Response Verification 2 | 3 | Response verification on the [Internet Computer](https://dfinity.org) is the process of verifying that an HTTP-compatible canister response from a replica has gone through consensus with other replicas hosting the same canister. It is the counterpart to [HTTP Certification](#http-certification). 4 | 5 | The `ic-response-verification` and `@dfinity/response-verification` packages encapsulate this verification protocol. It is used by [ICX Proxy](https://github.com/dfinity/ic/tree/master/rs/boundary_node/icx_proxy) and the [local HTTP Proxy](https://github.com/dfinity/http-proxy) and may be used by other implementations of the [HTTP Gateway Protocol](https://internetcomputer.org/docs/references/ic-interface-spec/#http-gateway) in the future. 6 | 7 | ## Usage 8 | 9 | ```javascript 10 | import initResponseVerification, { 11 | verifyRequestResponsePair, 12 | ResponseVerificationError, 13 | ResponseVerificationErrorCode, 14 | } from '@dfinity/response-verification'; 15 | 16 | // this is necessary for web, but not for NodeJS consumers 17 | await initResponseVerification(); 18 | 19 | try { 20 | const result = verifyRequestResponsePair( 21 | request, 22 | response, 23 | canister_id, 24 | current_time_ns, 25 | max_cert_time_offset_ns, 26 | fromHex(IC_ROOT_KEY), 27 | ); 28 | 29 | // do something with the result 30 | // `result.passed` will be true if verification succeeds, false otherwise, and 31 | // `result.response` will contain the certified response object if verification was successful. 32 | } catch (error) { 33 | if (error instanceof ResponseVerificationError) { 34 | switch (error.code) { 35 | case ResponseVerificationErrorCode.MalformedCbor: 36 | // the cbor returned from the replica was malformed. 37 | // ... 38 | break; 39 | 40 | case ResponseVerificationErrorCode.MalformedCertificate: 41 | // the certificate returned from the replica was malformed. 42 | // ... 43 | break; 44 | 45 | // Other error cases... 46 | } 47 | } 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /packages/ic-response-verification-wasm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dfinity/response-verification", 3 | "description": "Client side response verification for the Internet Computer", 4 | "version": "3.0.3", 5 | "author": "DFINITY Stiftung", 6 | "license": "Apache-2.0", 7 | "repository": "github:dfinity/response-verification", 8 | "bugs": "https://github.com/dfinity/response-verification/issues", 9 | "keywords": [ 10 | "internet-computer", 11 | "icp", 12 | "dfinity", 13 | "response-verification", 14 | "certification" 15 | ], 16 | "files": [ 17 | "dist" 18 | ], 19 | "main": "./dist/nodejs/nodejs.js", 20 | "browser": "./dist/web/web.js", 21 | "types": "./dist/web/web.d.ts", 22 | "scripts": { 23 | "build": "../../scripts/package.sh . ./dist", 24 | "test": "wasm-pack test --node" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/ic-response-verification-wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | 3 | use crate::request::request_from_js; 4 | use crate::response::response_from_js; 5 | use ic_response_verification::{ 6 | types::VerificationInfo, verify_request_response_pair as verify_request_response_pair_impl, 7 | ResponseVerificationJsError, MAX_VERIFICATION_VERSION, MIN_VERIFICATION_VERSION, 8 | }; 9 | use wasm_bindgen::{prelude::*, JsCast}; 10 | 11 | mod request; 12 | mod response; 13 | 14 | #[wasm_bindgen] 15 | extern "C" { 16 | #[wasm_bindgen(typescript_type = "VerificationInfo")] 17 | pub type JsVerificationInfo; 18 | 19 | #[wasm_bindgen(typescript_type = "Request")] 20 | pub type JsRequest; 21 | 22 | #[wasm_bindgen(typescript_type = "Response")] 23 | pub type JsResponse; 24 | } 25 | 26 | #[wasm_bindgen(js_name = getMinVerificationVersion)] 27 | pub fn get_min_verification_version() -> u8 { 28 | return MIN_VERIFICATION_VERSION; 29 | } 30 | 31 | #[wasm_bindgen(js_name = getMaxVerificationVersion)] 32 | pub fn get_max_verification_version() -> u8 { 33 | return MAX_VERIFICATION_VERSION; 34 | } 35 | 36 | #[wasm_bindgen(start)] 37 | pub fn main_js() { 38 | console_error_panic_hook::set_once(); 39 | log::set_logger(&wasm_bindgen_console_logger::DEFAULT_LOGGER).unwrap(); 40 | log::set_max_level(log::LevelFilter::Trace); 41 | } 42 | 43 | /// The primary entry point for verifying a request and response pair. This will verify the response 44 | /// with respect to the request, according the [Response Verification Spec](). 45 | #[wasm_bindgen(js_name = verifyRequestResponsePair)] 46 | pub fn verify_request_response_pair( 47 | request: JsRequest, 48 | response: JsResponse, 49 | canister_id: &[u8], 50 | current_time_ns: u64, 51 | max_cert_time_offset_ns: u64, 52 | ic_public_key: &[u8], 53 | min_requested_verification_version: u8, 54 | ) -> Result { 55 | let request = request_from_js(JsValue::from(request)); 56 | let response = response_from_js(JsValue::from(response)); 57 | 58 | verify_request_response_pair_impl( 59 | request.into(), 60 | response.into(), 61 | canister_id, 62 | current_time_ns as u128, 63 | max_cert_time_offset_ns as u128, 64 | ic_public_key, 65 | min_requested_verification_version, 66 | ) 67 | .map(|verification_result| { 68 | JsValue::from(VerificationInfo::from(verification_result)) 69 | .unchecked_into::() 70 | }) 71 | .map_err(|e| ResponseVerificationJsError::from(e)) 72 | } 73 | -------------------------------------------------------------------------------- /packages/ic-response-verification-wasm/src/request.rs: -------------------------------------------------------------------------------- 1 | use ic_http_certification::{HttpRequest, Method}; 2 | use std::str::FromStr; 3 | use wasm_bindgen::{prelude::*, JsCast}; 4 | 5 | #[wasm_bindgen(typescript_custom_section)] 6 | const REQUEST: &'static str = r#" 7 | interface Request { 8 | url: string; 9 | method: string; 10 | body: Uint8Array | number[]; 11 | headers: [string, string][]; 12 | certificate_version: [] | [number], 13 | } 14 | "#; 15 | 16 | pub fn request_from_js(req: JsValue) -> HttpRequest<'static> { 17 | use js_sys::{Array, JsString, Object, Uint8Array}; 18 | 19 | let method_str = JsString::from("method"); 20 | let url_str = JsString::from("url"); 21 | let headers_str = JsString::from("headers"); 22 | let body_str = JsString::from("body"); 23 | 24 | let mut method = String::from(""); 25 | let mut url = String::from(""); 26 | let mut headers = Vec::new(); 27 | let mut body = Vec::new(); 28 | 29 | let req = Object::unchecked_from_js(req); 30 | for entry in Object::entries(&req).iter() { 31 | let entry = Array::unchecked_from_js(entry); 32 | let k = JsString::unchecked_from_js(entry.get(0)); 33 | 34 | if k == method_str { 35 | method = JsString::unchecked_from_js(entry.get(1)) 36 | .as_string() 37 | .unwrap(); 38 | } 39 | 40 | if k == url_str { 41 | url = JsString::unchecked_from_js(entry.get(1)) 42 | .as_string() 43 | .unwrap(); 44 | } 45 | 46 | if k == headers_str { 47 | let headers_v = Array::unchecked_from_js(entry.get(1)); 48 | let headers_v = headers_v.iter(); 49 | headers = Vec::with_capacity(headers_v.len()); 50 | for header in headers_v { 51 | let header = Array::unchecked_from_js(header); 52 | let header_name = header.get(0).as_string().unwrap(); 53 | let header_val = header.get(1).as_string().unwrap(); 54 | headers.push((header_name, header_val)) 55 | } 56 | } 57 | 58 | if k == body_str { 59 | body = Uint8Array::unchecked_from_js(entry.get(1)).to_vec(); 60 | } 61 | } 62 | 63 | HttpRequest::builder() 64 | .with_method(Method::from_str(&method).unwrap()) 65 | .with_url(url) 66 | .with_headers(headers) 67 | .with_body(body) 68 | .build() 69 | } 70 | 71 | #[cfg(test)] 72 | mod tests { 73 | use super::*; 74 | use js_sys::JSON; 75 | use wasm_bindgen_test::wasm_bindgen_test; 76 | 77 | #[wasm_bindgen_test] 78 | fn request_from() { 79 | let v = JSON::parse( 80 | r#"{ 81 | "method": "GET", 82 | "url": "http://url.com", 83 | "headers": [ 84 | ["header1", "header1val"], 85 | ["header2", "header2val"] 86 | ], 87 | "body": [0, 1, 2, 3, 4, 5, 6] 88 | }"#, 89 | ) 90 | .expect("failed to parse JSON"); 91 | let r = request_from_js(v); 92 | 93 | assert_eq!( 94 | r, 95 | HttpRequest::get("http://url.com") 96 | .with_headers(vec![ 97 | ("header1".into(), "header1val".into()), 98 | ("header2".into(), "header2val".into()), 99 | ]) 100 | .with_body(vec![0, 1, 2, 3, 4, 5, 6]) 101 | .build() 102 | ); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/ic-response-verification-wasm/src/response.rs: -------------------------------------------------------------------------------- 1 | use ic_http_certification::HttpResponse; 2 | use wasm_bindgen::{prelude::*, JsCast}; 3 | 4 | #[wasm_bindgen(typescript_custom_section)] 5 | const RESPONSE: &'static str = r#" 6 | interface Response { 7 | status_code: number; 8 | headers: [string, string][]; 9 | body: Uint8Array | number[]; 10 | } 11 | "#; 12 | 13 | pub fn response_from_js(resp: JsValue) -> HttpResponse<'static> { 14 | use js_sys::{Array, JsString, Number, Object, Uint8Array}; 15 | 16 | let status_code_str = JsString::from("status_code"); 17 | let headers_str = JsString::from("headers"); 18 | let body_str = JsString::from("body"); 19 | 20 | let mut status_code: u16 = 0; 21 | let mut headers = Vec::new(); 22 | let mut body = Vec::new(); 23 | 24 | let resp = Object::unchecked_from_js(resp); 25 | for entry in Object::entries(&resp).iter() { 26 | let entry = Array::unchecked_from_js(entry); 27 | let k = JsString::unchecked_from_js(entry.get(0)); 28 | 29 | if k == status_code_str { 30 | status_code = Number::unchecked_from_js(entry.get(1)).as_f64().unwrap() as u16; 31 | } 32 | 33 | if k == headers_str { 34 | let headers_v = Array::unchecked_from_js(entry.get(1)); 35 | let headers_v = headers_v.iter(); 36 | headers = Vec::with_capacity(headers_v.len()); 37 | for header in headers_v { 38 | let header = Array::unchecked_from_js(header); 39 | let header_name = header.get(0).as_string().unwrap(); 40 | let header_val = header.get(1).as_string().unwrap(); 41 | headers.push((header_name, header_val)) 42 | } 43 | } 44 | 45 | if k == body_str { 46 | body = Uint8Array::unchecked_from_js(entry.get(1)).to_vec(); 47 | } 48 | } 49 | 50 | HttpResponse::builder() 51 | .with_status_code(status_code.try_into().unwrap()) 52 | .with_headers(headers) 53 | .with_body(body) 54 | .build() 55 | } 56 | 57 | #[cfg(test)] 58 | mod tests { 59 | use super::*; 60 | use ic_http_certification::StatusCode; 61 | use js_sys::JSON; 62 | use wasm_bindgen_test::wasm_bindgen_test; 63 | 64 | #[wasm_bindgen_test] 65 | fn request_from() { 66 | let v = JSON::parse( 67 | r#"{ 68 | "status_code": 200, 69 | "body": [0, 1, 2, 3, 4, 5, 6], 70 | "headers": [ 71 | ["header1", "header1val"], 72 | ["header2", "header2val"] 73 | ] 74 | }"#, 75 | ) 76 | .expect("failed to parse JSON"); 77 | let r = response_from_js(v); 78 | 79 | assert_eq!( 80 | r, 81 | HttpResponse::builder() 82 | .with_status_code(StatusCode::OK) 83 | .with_body(vec![0, 1, 2, 3, 4, 5, 6]) 84 | .with_headers(vec![ 85 | ("header1".into(), "header1val".into()), 86 | ("header2".into(), "header2val".into()) 87 | ]) 88 | .build() 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/ic-response-verification/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic-response-verification" 3 | description = "Client side response verification for the Internet Computer" 4 | readme = "README.md" 5 | documentation = "https://docs.rs/ic-response-verification" 6 | categories = ["api-bindings", "algorithms", "cryptography::cryptocurrencies", "wasm"] 7 | keywords = ["internet-computer", "icp", "dfinity", "response", "verification"] 8 | include = ["src", "Cargo.toml", "README.md"] 9 | 10 | version.workspace = true 11 | authors.workspace = true 12 | edition.workspace = true 13 | repository.workspace = true 14 | license.workspace = true 15 | homepage.workspace = true 16 | 17 | [features] 18 | js = ["dep:wasm-bindgen", "dep:js-sys"] 19 | 20 | [lib] 21 | crate-type = ["cdylib", "rlib"] 22 | 23 | [dependencies] 24 | base64.workspace = true 25 | nom.workspace = true 26 | js-sys = { version = "0.3", optional = true } 27 | wasm-bindgen = { version = "0.2", optional = true } 28 | thiserror.workspace = true 29 | sha2.workspace = true 30 | http.workspace = true 31 | ic-certification.workspace = true 32 | ic-http-certification.workspace = true 33 | ic-representation-independent-hash.workspace = true 34 | ic-cbor.workspace = true 35 | ic-certificate-verification.workspace = true 36 | flate2.workspace = true 37 | leb128.workspace = true 38 | candid.workspace = true 39 | log.workspace = true 40 | hex.workspace = true 41 | urlencoding.workspace = true 42 | 43 | [dev-dependencies] 44 | serde_cbor.workspace = true 45 | wasm-bindgen-test.workspace = true 46 | ic-certification.workspace = true 47 | candid.workspace = true 48 | serde.workspace = true 49 | ic-response-verification-test-utils.workspace = true 50 | ic-crypto-tree-hash.workspace = true 51 | ic-types.workspace = true 52 | rstest.workspace = true 53 | ic-certification-testing.workspace = true 54 | assert_matches.workspace = true 55 | -------------------------------------------------------------------------------- /packages/ic-response-verification/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /packages/ic-response-verification/README.md: -------------------------------------------------------------------------------- 1 | # Response Verification 2 | 3 | Response verification on the [Internet Computer](https://dfinity.org) is the process of verifying that an HTTP-compatible canister response from a replica has gone through consensus with other replicas hosting the same canister. It is the counterpart to [HTTP Certification](#http-certification). 4 | 5 | The `ic-response-verification` and `@dfinity/response-verification` packages encapsulate this verification protocol. It is primarily used by [the `ic-http-gateway` library](https://github.com/dfinity/http-gateway/tree/main/packages/ic-http-gateway) and may be used by other implementations of the [HTTP Gateway Protocol](https://internetcomputer.org/docs/references/ic-interface-spec/#http-gateway) in the future. 6 | -------------------------------------------------------------------------------- /packages/ic-response-verification/src/base64.rs: -------------------------------------------------------------------------------- 1 | use base64::{ 2 | alphabet::STANDARD, 3 | engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig}, 4 | }; 5 | 6 | pub const BASE64: GeneralPurpose = { 7 | let config = GeneralPurposeConfig::new() 8 | .with_decode_allow_trailing_bits(true) 9 | .with_decode_padding_mode(DecodePaddingMode::Indifferent); 10 | 11 | GeneralPurpose::new(&STANDARD, config) 12 | }; 13 | -------------------------------------------------------------------------------- /packages/ic-response-verification/src/cel/mod.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for parsing CEL expressions into Rust consumable types. 2 | 3 | mod error; 4 | pub use error::*; 5 | 6 | mod ast_mapping; 7 | mod parser; 8 | 9 | pub(crate) use ast_mapping::map_cel_ast; 10 | pub(crate) use parser::parse_cel_expression; 11 | 12 | #[cfg(test)] 13 | mod tests; 14 | -------------------------------------------------------------------------------- /packages/ic-response-verification/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Response Verification 2 | 3 | #![deny(missing_docs, missing_debug_implementations, rustdoc::all, clippy::all)] 4 | 5 | mod verification; 6 | pub use verification::*; 7 | 8 | mod error; 9 | pub use error::*; 10 | 11 | pub mod cel; 12 | pub mod types; 13 | 14 | mod base64; 15 | mod validation; 16 | 17 | #[cfg(test)] 18 | mod test_utils; 19 | -------------------------------------------------------------------------------- /packages/ic-response-verification/src/test_utils.rs: -------------------------------------------------------------------------------- 1 | use ic_certification::hash_tree::{fork, label, leaf, pruned_from_hex, Hash}; 2 | use ic_certification::HashTree; 3 | use ic_response_verification_test_utils::{base64_encode, hex_decode}; 4 | 5 | pub struct CreateTreeOptions<'a> { 6 | pub path: Option<&'a str>, 7 | pub body_sha: Option<&'a [u8]>, 8 | } 9 | 10 | pub fn create_tree(options: Option) -> HashTree { 11 | const DEFAULT_PATH: &str = "/"; 12 | let path = options 13 | .as_ref() 14 | .and_then(|options| options.path) 15 | .unwrap_or(DEFAULT_PATH); 16 | 17 | let default_body_sha = 18 | hex_decode("784C0F825A938AA7F471587CDF7C7796F828F9362495E2B9C8490F2232359BDB"); 19 | let body_sha = options 20 | .as_ref() 21 | .and_then(|options| options.body_sha) 22 | .unwrap_or(&default_body_sha); 23 | 24 | fork( 25 | label( 26 | "http_assets", 27 | fork( 28 | fork( 29 | label(path, leaf(body_sha)), 30 | create_pruned( 31 | "D7CD0A6CF52A2070DC51FE1D7B6A87078888719E16849C748C35E7FC7B69F95C", 32 | ), 33 | ), 34 | create_pruned("582B5321336646D48C6F8BF1913333BB4EBF65C09FFBF8207C012E1F54071261"), 35 | ), 36 | ), 37 | create_pruned("21334B26681D7220E3D2D7DCB23A89CCDB1B1E044EB35EBBE491F66D4080D078"), 38 | ) 39 | } 40 | 41 | pub fn create_pruned(data: &str) -> HashTree { 42 | pruned_from_hex(data).unwrap() 43 | } 44 | 45 | pub fn sha256_from_hex(data: &str) -> Hash { 46 | TryFrom::try_from(hex_decode(data)).unwrap() 47 | } 48 | 49 | pub fn create_encoded_header_field>(name: &str, value: T) -> String { 50 | let value = base64_encode(value.as_ref()); 51 | 52 | create_header_field(name, &value) 53 | } 54 | 55 | pub fn create_header_field(name: &str, value: &str) -> String { 56 | format!("{}=:{}:", name, value) 57 | } 58 | 59 | pub fn remove_whitespace(s: &str) -> String { 60 | s.chars().filter(|c| !c.is_whitespace()).collect() 61 | } 62 | -------------------------------------------------------------------------------- /packages/ic-response-verification/src/types/mod.rs: -------------------------------------------------------------------------------- 1 | //! Public types used for response verification. 2 | 3 | /// Types to represent the result of verifying a request/response pair's certification. 4 | mod verification_result; 5 | pub use verification_result::*; 6 | 7 | /// Types to represent a certified response that clients can use to determine which parts of a response are safe to use. 8 | mod verified_response; 9 | pub use verified_response::*; 10 | -------------------------------------------------------------------------------- /packages/ic-response-verification/src/types/verification_result.rs: -------------------------------------------------------------------------------- 1 | use crate::types::VerifiedResponse; 2 | 3 | #[cfg(all(target_arch = "wasm32", feature = "js"))] 4 | use wasm_bindgen::prelude::*; 5 | 6 | #[cfg(all(target_arch = "wasm32", feature = "js"))] 7 | #[wasm_bindgen(typescript_custom_section)] 8 | const VERIFICATION_RESULT: &'static str = r#" 9 | type VerificationInfo = { 10 | response?: VerifiedResponse; 11 | verificationVersion: number; 12 | } 13 | "#; 14 | 15 | /// Result of verifying the provided request/response pair's certification. 16 | #[derive(Debug)] 17 | pub struct VerificationInfo { 18 | /// Response object including the status code, body and headers that were included in the 19 | /// certification and passed verification. If verification failed then this object will be 20 | /// empty. 21 | pub response: Option, 22 | /// The version of verification that was used to verify the response 23 | pub verification_version: u16, 24 | } 25 | 26 | #[cfg(all(target_arch = "wasm32", feature = "js"))] 27 | impl From for JsValue { 28 | fn from(verification_result: VerificationInfo) -> Self { 29 | use js_sys::{Array, Number, Object}; 30 | 31 | let verification_version = Number::from(verification_result.verification_version); 32 | let verification_version_entry = 33 | Array::of2(&JsValue::from("verificationVersion"), &verification_version); 34 | 35 | let response = JsValue::from(verification_result.response); 36 | let response_entry = Array::of2(&JsValue::from("response"), &response.into()); 37 | 38 | let result = 39 | Object::from_entries(&Array::of2(&response_entry, &verification_version_entry)) 40 | .unwrap(); 41 | 42 | JsValue::from(result) 43 | } 44 | } 45 | 46 | #[cfg(all(target_arch = "wasm32", feature = "js", test))] 47 | mod tests { 48 | use super::*; 49 | use js_sys::JSON; 50 | use wasm_bindgen::JsValue; 51 | use wasm_bindgen_test::wasm_bindgen_test; 52 | 53 | #[wasm_bindgen_test] 54 | fn serialize_verification_result_with_no_response() { 55 | let expected = r#"{"verificationVersion":1}"#; 56 | 57 | assert_eq!( 58 | JSON::stringify(&JsValue::from(VerificationInfo { 59 | response: None, 60 | verification_version: 1, 61 | })) 62 | .unwrap(), 63 | expected 64 | ); 65 | } 66 | 67 | #[wasm_bindgen_test] 68 | fn serialize_verification_result_with_response() { 69 | let expected = r#"{"response":{"statusCode":200,"body":{"0":0,"1":1,"2":2},"headers":[]},"verificationVersion":2}"#; 70 | 71 | assert_eq!( 72 | JSON::stringify(&JsValue::from(VerificationInfo { 73 | response: Some(VerifiedResponse { 74 | status_code: Some(200), 75 | body: vec![0, 1, 2], 76 | headers: vec![], 77 | }), 78 | verification_version: 2, 79 | })) 80 | .unwrap(), 81 | expected 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/ic-response-verification/src/types/verified_response.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(target_arch = "wasm32", feature = "js"))] 2 | use wasm_bindgen::prelude::*; 3 | 4 | #[cfg(all(target_arch = "wasm32", feature = "js"))] 5 | #[wasm_bindgen(typescript_custom_section)] 6 | const VERIFIED_RESPONSE: &'static str = r#" 7 | interface VerifiedResponse { 8 | statusCode?: number; 9 | headers: [string, string][]; 10 | body: Uint8Array; 11 | } 12 | "#; 13 | 14 | /// Represents a certified Response from the [Internet Computer](https://internetcomputer.org). 15 | #[derive(Debug, PartialEq, Eq)] 16 | pub struct VerifiedResponse { 17 | /// The HTTP status code of the response, i.e. 200. 18 | pub status_code: Option, 19 | /// The HTTP headers of the request, i.e. \[\["Ic-Certificate", "certificate=:2dn3o2R0cmVlgw=:, tree=:2dn3gwGDA:"\]\] 20 | pub headers: Vec<(String, String)>, 21 | /// The body of the request as a candid decoded blob, i.e. \[60, 33, 100, 111, 99\] 22 | pub body: Vec, 23 | } 24 | 25 | #[cfg(all(target_arch = "wasm32", feature = "js"))] 26 | impl From for JsValue { 27 | fn from(response: VerifiedResponse) -> Self { 28 | use js_sys::{Array, Number, Object, Uint8Array}; 29 | 30 | let body = Uint8Array::from(response.body.as_slice()); 31 | 32 | let headers = Array::new(); 33 | for (k, v) in response.headers.iter() { 34 | let value = JsValue::from(v); 35 | headers.push(&Array::of2(&k.into(), &value.into())); 36 | } 37 | 38 | let body_entry = Array::of2(&JsValue::from("body"), &body); 39 | let headers_entry = Array::of2(&JsValue::from("headers"), &headers); 40 | 41 | let js_response = match response.status_code { 42 | Some(status_code) => { 43 | let status_code = Number::from(status_code); 44 | let status_code_entry = Array::of2(&JsValue::from("statusCode"), &status_code); 45 | 46 | Object::from_entries(&Array::of3(&status_code_entry, &body_entry, &headers_entry)) 47 | .unwrap() 48 | } 49 | _ => Object::from_entries(&Array::of2(&body_entry, &headers_entry)).unwrap(), 50 | }; 51 | 52 | JsValue::from(js_response) 53 | } 54 | } 55 | 56 | #[cfg(all(target_arch = "wasm32", feature = "js", test))] 57 | mod tests { 58 | use super::*; 59 | use js_sys::JSON; 60 | use wasm_bindgen::JsValue; 61 | use wasm_bindgen_test::wasm_bindgen_test; 62 | 63 | #[wasm_bindgen_test] 64 | fn serialize_response_with_headers() { 65 | let expected = 66 | r#"{"statusCode":200,"body":{"0":0,"1":1,"2":2},"headers":[["header1","header1val"]]}"#; 67 | 68 | assert_eq!( 69 | JSON::stringify(&JsValue::from(VerifiedResponse { 70 | status_code: Some(200), 71 | body: vec![0, 1, 2], 72 | headers: vec![("header1".into(), "header1val".into())], 73 | })) 74 | .unwrap(), 75 | expected 76 | ); 77 | } 78 | 79 | #[wasm_bindgen_test] 80 | fn serialize_response_with_empty_headers() { 81 | let expected = r#"{"statusCode":200,"body":{"0":0,"1":1,"2":2},"headers":[]}"#; 82 | 83 | assert_eq!( 84 | JSON::stringify(&JsValue::from(VerifiedResponse { 85 | status_code: Some(200), 86 | body: vec![0, 1, 2], 87 | headers: vec![], 88 | })) 89 | .unwrap(), 90 | expected 91 | ); 92 | } 93 | 94 | #[wasm_bindgen_test] 95 | fn serialize_response_without_status_code() { 96 | let expected = r#"{"body":{"0":0,"1":1,"2":2},"headers":[["header1","header1val"]]}"#; 97 | 98 | assert_eq!( 99 | JSON::stringify(&JsValue::from(VerifiedResponse { 100 | status_code: None, 101 | body: vec![0, 1, 2], 102 | headers: vec![("header1".into(), "header1val".into())], 103 | })) 104 | .unwrap(), 105 | expected 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /packages/ic-response-verification/src/validation/mod.rs: -------------------------------------------------------------------------------- 1 | mod common_validation; 2 | pub(crate) use common_validation::*; 3 | 4 | mod v1_validation; 5 | pub(crate) use v1_validation::*; 6 | 7 | mod v2_validation; 8 | pub(crate) use v2_validation::*; 9 | -------------------------------------------------------------------------------- /packages/ic-response-verification/src/verification/body.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ResponseVerificationResult; 2 | use flate2::read::{DeflateDecoder, GzDecoder}; 3 | use std::io::Read; 4 | 5 | const MAX_CHUNK_SIZE_TO_DECOMPRESS: usize = 1_024; 6 | 7 | pub fn decode_body(body: &[u8], encoding: Option<&str>) -> ResponseVerificationResult> { 8 | match encoding { 9 | Some("gzip") => body_from_decoder(GzDecoder::new(body)), 10 | Some("deflate") => body_from_decoder(DeflateDecoder::new(body)), 11 | _ => Ok(body.to_owned()), 12 | } 13 | } 14 | 15 | fn body_from_decoder(mut decoder: D) -> ResponseVerificationResult> { 16 | let mut decoded = Vec::new(); 17 | let mut buffer = [0u8; MAX_CHUNK_SIZE_TO_DECOMPRESS]; 18 | 19 | loop { 20 | let bytes = decoder.read(&mut buffer)?; 21 | 22 | if bytes == 0 { 23 | return Ok(decoded); 24 | } 25 | 26 | decoded.extend_from_slice(&buffer[..bytes]); 27 | } 28 | } 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | use super::*; 33 | use flate2::write::{DeflateEncoder, GzEncoder}; 34 | use flate2::Compression; 35 | use std::io::Write; 36 | 37 | const BODY: &[u8] = &[1, 2, 3, 4, 5, 6, 7, 8]; 38 | 39 | #[test] 40 | fn decode_simple_body() { 41 | let result = decode_body(BODY, None).unwrap(); 42 | 43 | assert_eq!(result.as_slice(), BODY); 44 | } 45 | 46 | #[test] 47 | fn decode_gzip_body() { 48 | let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); 49 | encoder.write_all(BODY).unwrap(); 50 | let encoded_body = encoder.finish().unwrap(); 51 | 52 | let result = decode_body(&encoded_body, Some("gzip")).unwrap(); 53 | 54 | assert_eq!(result.as_slice(), BODY); 55 | } 56 | 57 | #[test] 58 | fn decode_deflate_body() { 59 | let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default()); 60 | encoder.write_all(BODY).unwrap(); 61 | let encoded_body = encoder.finish().unwrap(); 62 | 63 | let result = decode_body(&encoded_body, Some("deflate")).unwrap(); 64 | 65 | assert_eq!(result.as_slice(), BODY); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/ic-response-verification/src/verification/certificate_header_field.rs: -------------------------------------------------------------------------------- 1 | use nom::{ 2 | bytes::complete::take_while, 3 | bytes::complete::{tag, take_until}, 4 | character::complete::char, 5 | combinator::{eof, opt}, 6 | sequence::{delimited, terminated}, 7 | IResult, 8 | }; 9 | 10 | /// Parsed key, value pair for an `Ic-Certificate` header field. 11 | #[derive(Debug)] 12 | pub struct CertificateHeaderField<'a>(pub &'a str, pub &'a str); 13 | 14 | impl<'a> CertificateHeaderField<'a> { 15 | /// Parses the given header field string and returns a new CertificateHeaderField. 16 | pub fn from(header_field: &'a str) -> Option> { 17 | extract_header_field(header_field.trim()) 18 | .map(|(name, value)| CertificateHeaderField(name, value)) 19 | } 20 | } 21 | 22 | fn extract_header_field(header_field: &str) -> Option<(&str, &str)> { 23 | fn drop_delimiters(v: char, i: &str) -> IResult<&str, &str> { 24 | delimited(opt(char(v)), take_while(|e| e != v), opt(char(v)))(i) 25 | } 26 | 27 | fn until_terminated<'a>(v: &str, i: &'a str) -> IResult<&'a str, &'a str> { 28 | terminated(take_until(v), tag(v))(i) 29 | } 30 | 31 | fn extract(i: &str) -> IResult<&str, (&str, &str)> { 32 | let (i, name) = until_terminated("=", i)?; 33 | let (i, value) = drop_delimiters(':', i)?; 34 | 35 | eof(i)?; 36 | 37 | Ok((i, (name, value))) 38 | } 39 | 40 | extract(header_field).ok().and_then(|(_, (name, value))| { 41 | if name.is_empty() || value.is_empty() { 42 | None 43 | } else { 44 | Some((name, value)) 45 | } 46 | }) 47 | } 48 | 49 | #[cfg(test)] 50 | mod tests { 51 | use super::*; 52 | use crate::test_utils::create_encoded_header_field; 53 | use ic_response_verification_test_utils::{base64_encode, cbor_encode, create_certificate}; 54 | 55 | #[test] 56 | fn certificate_header_field_parses_valid_field() { 57 | let name = "certificate"; 58 | let value = cbor_encode(&create_certificate(None)); 59 | let header_field = create_encoded_header_field(name, &value); 60 | 61 | let CertificateHeaderField(result_name, result_value) = 62 | CertificateHeaderField::from(header_field.as_str()).unwrap(); 63 | 64 | assert_eq!(result_name, name); 65 | assert_eq!(result_value, base64_encode(&value)); 66 | } 67 | 68 | #[test] 69 | fn certificate_header_field_parses_valid_field_without_delimiters() { 70 | let name = "version"; 71 | let value = 2.to_string(); 72 | let header_field = format!("{}={}", name, value); 73 | 74 | let CertificateHeaderField(result_name, result_value) = 75 | CertificateHeaderField::from(header_field.as_str()).unwrap(); 76 | 77 | assert_eq!(result_name, name); 78 | assert_eq!(result_value, value); 79 | } 80 | 81 | #[test] 82 | fn certificate_header_field_parses_valid_field_with_empty_values() { 83 | let header_field = create_encoded_header_field("", ""); 84 | 85 | let result = CertificateHeaderField::from(header_field.as_str()); 86 | 87 | assert!(result.is_none()); 88 | } 89 | 90 | #[test] 91 | fn certificate_header_field_does_not_parse_empty_field() { 92 | let result = CertificateHeaderField::from(""); 93 | 94 | assert!(result.is_none()); 95 | } 96 | 97 | #[test] 98 | fn certificate_header_field_does_not_parse_invalid_field() { 99 | let name = "certificate"; 100 | let value = cbor_encode(&create_certificate(None)); 101 | let value = base64_encode(&value); 102 | 103 | let header_field = format!("{}:{}", name, value); 104 | 105 | let result = CertificateHeaderField::from(header_field.as_str()); 106 | 107 | assert!(result.is_none()); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /packages/ic-response-verification/src/verification/mod.rs: -------------------------------------------------------------------------------- 1 | //! The primary entry point for the repsonse verification API. 2 | 3 | mod body; 4 | mod certificate_header_field; 5 | 6 | mod certificate_header; 7 | pub use certificate_header::*; 8 | 9 | mod verify_request_response_pair; 10 | pub use verify_request_response_pair::*; 11 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'examples/certification/certified-counter/src/frontend' 3 | - 'examples/http-certification/assets/src/frontend' 4 | - 'examples/http-certification/assets/src/tests' 5 | - 'examples/http-certification/custom-assets/src/frontend' 6 | - 'examples/http-certification/custom-assets/src/tests' 7 | - 'examples/http-certification/json-api/src/tests' 8 | - 'examples/http-certification/skip-certification/src/tests' 9 | - 'examples/http-certification/upgrade-to-update-call/src/tests' 10 | - 'packages/certificate-verification-js' 11 | - 'packages/ic-certification-testing-wasm' 12 | - 'packages/ic-response-verification-wasm' 13 | - 'packages/ic-response-verification-tests/src/frontend' 14 | - 'packages/ic-response-verification-tests/src/wasm-tests' 15 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.78.0" 3 | profile = "minimal" 4 | components = ["rustfmt", "clippy"] 5 | targets = ["wasm32-unknown-unknown"] 6 | -------------------------------------------------------------------------------- /scripts/e2e.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | # Set the dfx command, this might be overwritten 5 | DFX=dfx 6 | 7 | # By default, we use the local DFX 8 | USE_LATEST_DFX=0 9 | 10 | # in case we decide to use the latest dfx 11 | SDK_GIT_BRANCH="master" 12 | SDK_REPO_DIR="$(pwd)/tmp/sdk" 13 | 14 | # Function to display usage 15 | print_usage() { 16 | echo "Run the end to end tests" 17 | echo 18 | echo "Usage: $0 [options]" 19 | echo 20 | echo "Options:" 21 | echo " -h Show this help message and exit" 22 | echo " --use-latest-dfx Clone, build and use the latest dfx" 23 | echo 24 | } 25 | 26 | 27 | # Download the SDK repo so we can build and test against the latest changes 28 | download_sdk_repo() { 29 | 30 | if [ -d "$SDK_REPO_DIR" ]; then 31 | echo "SDK repo already cloned, updating..." 32 | 33 | pushd "$SDK_REPO_DIR" || clean_exit 34 | git reset --hard 35 | git clean -fxd -e target 36 | git fetch 37 | git checkout "$SDK_GIT_BRANCH" 38 | git pull 39 | popd || clean_exit 40 | else 41 | echo "SDK repo not cloned yet, cloning..." 42 | 43 | git clone "https://github.com/dfinity/sdk" "$SDK_REPO_DIR" 44 | pushd "$SDK_REPO_DIR" || clean_exit 45 | git checkout "$SDK_GIT_BRANCH" 46 | popd || clean_exit 47 | fi 48 | } 49 | 50 | # check if the $DFX command exists 51 | check_dfx_command() { 52 | if ! command -v $DFX &> /dev/null 53 | then 54 | echo "$DFX command was not found in your path" 55 | exit 3 56 | fi 57 | } 58 | 59 | build_dfx() { 60 | echo "Building DFX..." 61 | 62 | pushd "$SDK_REPO_DIR" || clean_exit 63 | cargo build -p dfx 64 | 65 | # override dfx path 66 | DFX="$(pwd)/target/debug/dfx" 67 | popd || clean_exit 68 | 69 | echo "DFX built at $DFX." 70 | } 71 | 72 | dfx_start() { 73 | echo "Starting DFX..." 74 | 75 | check_dfx_command 76 | 77 | "$DFX" start --clean --background -qq --log file --logfile "./replica.log" -vv 78 | 79 | DFX_REPLICA_PORT=$("$DFX" info webserver-port) 80 | DFX_REPLICA_ADDRESS="http://localhost:$DFX_REPLICA_PORT" 81 | 82 | echo "DFX local replica running at $DFX_REPLICA_ADDRESS." 83 | } 84 | 85 | dfx_stop() { 86 | echo "Stopping DFX..." 87 | 88 | check_dfx_command 89 | 90 | "$DFX" stop 91 | } 92 | 93 | deploy_dfx_project() { 94 | echo "Deploying DFX project..." 95 | 96 | check_dfx_command 97 | 98 | "$DFX" deploy response_verification_tests_frontend 99 | 100 | echo "getting canister id..." 101 | "$DFX" canister id response_verification_tests_frontend 102 | DFX_CANISTER_ID=$("$DFX" canister id response_verification_tests_frontend) 103 | echo "$DFX_CANISTER_ID" 104 | } 105 | 106 | clean_exit() { 107 | echo "Performing clean exit..." 108 | 109 | dfx_stop 110 | 111 | echo "TESTS FAILED!" 112 | exit 1 113 | } 114 | 115 | run_e2e_tests() { 116 | echo "Running e2e tests..." 117 | 118 | if [ -z "$DFX_REPLICA_ADDRESS" ]; then 119 | echo "$DFX_REPLICA_ADDRESS must be defined!" 120 | clean_exit 121 | fi 122 | 123 | if [ -z "$DFX_CANISTER_ID" ]; then 124 | echo "DFX_CANISTER_ID must be defined!" 125 | clean_exit 126 | fi 127 | 128 | DFX_REPLICA_ADDRESS=$DFX_REPLICA_ADDRESS RUST_BACKTRACE=1 cargo run -p ic-response-verification-tests -- "$DFX_CANISTER_ID" || clean_exit 129 | 130 | pnpm run -F @dfinity/response-verification build || clean_exit 131 | DFX_REPLICA_ADDRESS=$DFX_REPLICA_ADDRESS pnpm run -F response-verification-tests e2e-test -- "$DFX_CANISTER_ID" || clean_exit 132 | } 133 | 134 | # Parse the script arguments 135 | for arg in "$@"; do 136 | case $arg in 137 | --use-latest-dfx) 138 | USE_LATEST_DFX=1 139 | shift 140 | ;; 141 | -h) 142 | print_usage 143 | exit 0 144 | ;; 145 | *) 146 | echo "Unknown option: $arg" 147 | exit 1 148 | ;; 149 | esac 150 | done 151 | 152 | 153 | pnpm i --frozen-lockfile 154 | 155 | if [ $USE_LATEST_DFX -eq 1 ]; then 156 | # build latest dfx 157 | download_sdk_repo 158 | build_dfx 159 | fi 160 | 161 | dfx_start 162 | deploy_dfx_project 163 | run_e2e_tests 164 | dfx_stop 165 | 166 | echo "TESTS PASSED!" 167 | -------------------------------------------------------------------------------- /scripts/package.sh: -------------------------------------------------------------------------------- 1 | PKG_ROOT=$1 2 | OUT_DIR=$2 3 | 4 | early_exit() { 5 | echo "Performing early exit..." 6 | 7 | exit 1 8 | } 9 | 10 | build_release_packages() { 11 | wasm-pack build --target web --out-name web --out-dir $OUT_DIR/web --release $PKG_ROOT || early_exit 12 | wasm-pack build --target nodejs --out-name nodejs --out-dir $OUT_DIR/nodejs --release $PKG_ROOT || early_exit 13 | } 14 | 15 | build_debug_packages() { 16 | wasm-pack build --target web --out-name web --out-dir $OUT_DIR/debug/dist/web --dev $PKG_ROOT -- || early_exit 17 | wasm-pack build --target nodejs --out-name nodejs --out-dir $OUT_DIR/debug/dist/nodejs --dev $PKG_ROOT -- || early_exit 18 | } 19 | 20 | delete_generated_files() { 21 | find $OUT_DIR -name ".gitignore" -type f -delete || early_exit 22 | find $OUT_DIR -name "README.md" -type f -delete || early_exit 23 | find $OUT_DIR -name "package.json" -type f -delete || early_exit 24 | find $OUT_DIR -name "LICENSE" -type f -delete || early_exit 25 | } 26 | 27 | add_debug_files() { 28 | cp $PKG_ROOT/package.json $OUT_DIR/debug/ || early_exit 29 | cp $PKG_ROOT/LICENSE $OUT_DIR/debug/ || early_exit 30 | } 31 | 32 | build_release_packages 33 | build_debug_packages 34 | delete_generated_files 35 | add_debug_files 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // compilation 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "isolatedModules": true, 7 | "preserveConstEnums": true, 8 | "esModuleInterop": true, 9 | // resolution 10 | "baseUrl": ".", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | // type checking 14 | "allowJs": false, 15 | "checkJs": false, 16 | "skipLibCheck": true, 17 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 18 | // strictness 19 | "strict": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "noImplicitAny": true, 23 | "noImplicitOverride": true, 24 | "noImplicitReturns": true, 25 | "noImplicitThis": true, 26 | "noUnusedLocals": true, 27 | "noUnusedParameters": true 28 | } 29 | } 30 | --------------------------------------------------------------------------------