diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 318dd49a..4d605139 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,10 +41,6 @@ jobs: - name: Build NPM packages run: pnpm build - - name: Pack @dfinity/response-verification NPM package - working-directory: packages/ic-response-verification-wasm - run: npm pack --pack-destination ../../ - - name: Generate release notes run: cz changelog ${{ github.ref_name }} --file-name RELEASE_NOTES.md @@ -58,6 +54,29 @@ jobs: env: CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} + # `ic-certification-testing` cannot be published since it relies on unpublished crates + # from the `ic` repository. Namely: + # - ic-types + # - ic-crypto-tree-hash + # - ic-crypto-internal-threshold-sig-bls12381 + # - ic-crypto-internal-seed + # - ic-crypto-internal-types + # + # - name: Release ic-certification-testing Cargo crate + # run: cargo publish -p ic-certification-testing --token ${CRATES_TOKEN} + # env: + # CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} + + - name: Pack @dfinity/certification-testing NPM package + working-directory: packages/ic-certification-testing-wasm + run: npm pack --pack-destination ../../ + + - name: Release @dfinity/certification-testing NPM package + working-directory: packages/ic-certification-testing-wasm + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Release ic-cbor Cargo crate run: cargo publish -p ic-cbor --token ${CRATES_TOKEN} env: @@ -68,11 +87,25 @@ jobs: env: CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} + - name: Pack @dfinity/certificate-verification NPM package + working-directory: packages/certificate-verification-js + run: npm pack --pack-destination ../../ + + - name: Release @dfinity/certificate-verification NPM package + working-directory: packages/certificate-verification-js + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Release ic-response-verification Cargo crate run: cargo publish -p ic-response-verification --token ${CRATES_TOKEN} env: CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} + - name: Pack @dfinity/response-verification NPM package + working-directory: packages/ic-response-verification-wasm + run: npm pack --pack-destination ../../ + - name: Release @dfinity/response-verification NPM package working-directory: packages/ic-response-verification-wasm run: npm publish --access public @@ -88,6 +121,8 @@ jobs: target/package/ic-cbor-${{ github.ref_name }}.crate, target/package/ic-certificate-verification-${{ github.ref_name }}.crate, target/package/ic-response-verification-${{ github.ref_name }}.crate, + dfinity-certification-testing-${{ github.ref_name }}.tgz, + dfinity-certificate-verification-${{ github.ref_name }}.tgz, dfinity-response-verification-${{ github.ref_name }}.tgz bodyFile: 'RELEASE_NOTES.md' tag: '${{ github.ref_name }}' diff --git a/packages/ic-certification-testing-wasm/README.md b/packages/ic-certification-testing-wasm/README.md index 404c076f..b2074fbd 100644 --- a/packages/ic-certification-testing-wasm/README.md +++ b/packages/ic-certification-testing-wasm/README.md @@ -1 +1,59 @@ # Certification Testing + +[Certificate verification](https://internetcomputer.org/docs/current/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/current/references/ic-interface-spec#http-query) has gone through consensus with other replicas hosting the same canister. + +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. + +## Usage + +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. + +The [@dfinity/certificate-verification](https://www.npmjs.com/package/@dfinity/certificate-verification) library can then be used to decode the certificate and verify it. + +```typescript +import { describe, expect, it } from 'vitest'; +import { HashTree, reconstruct, Cbor } from '@dfinity/agent'; +import { CertificateBuilder } from '@dfinity/certification-testing'; +import { verifyCertification } from '@dfinity/certificate-verification'; +import { Principal } from '@dfinity/principal'; +import { createHash, webcrypto } from 'node:crypto'; + +globalThis.crypto = webcrypto as Crypto; + +const userId = '1234'; + +const username = 'testuser'; +const usernameHash = new Uint8Array( + createHash('sha256').update(username).digest(), +); + +const hashTree: HashTree = [ + 2, + new Uint8Array(Buffer.from(userId)), + [3, usernameHash], +]; +const rootHash = await reconstruct(hashTree); +const cborEncodedTree = Cbor.encode(hashTree); + +const canisterId = Principal.fromUint8Array( + new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1]), +); +const time = BigInt(Date.now()); +const MAX_CERT_TIME_OFFSET_MS = 300_000; + +let certificate = new CertificateBuilder( + canisterId.toString(), + new Uint8Array(rootHash), +) + .withTime(time) + .build(); + +const decodedHashTree = await verifyCertification({ + canisterId, + encodedCertificate: certificate.cborEncodedCertificate, + encodedTree: cborEncodedTree, + maxCertificateTimeOffsetMs: MAX_CERT_TIME_OFFSET_MS, + rootKey: certificate.rootKey, +}); +expect(decodedHashTree).toEqual(hashTree); +``` diff --git a/packages/ic-certification-testing/README.md b/packages/ic-certification-testing/README.md index 404c076f..08f43e01 100644 --- a/packages/ic-certification-testing/README.md +++ b/packages/ic-certification-testing/README.md @@ -1 +1,73 @@ # Certification Testing + +[Certificate verification](https://internetcomputer.org/docs/current/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/current/references/ic-interface-spec#http-query) has gone through consensus with other replicas hosting the same canister. + +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. + +## Usage + +First, a hash tree must be created containing the data that needs to be certified. This can be done using the [ic-certified-map](https://docs.rs/ic-certified-map/latest/ic_certified_map/) library. The root hash of this tree is then used to create the certificate. + +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. + +```rust +use ic_certification_testing::{CertificateBuilder, CertificateData}; +use ic_cbor::CertificateToCbor; +use ic_certificate_verification::VerifyCertificate; +use ic_certification::Certificate; +use ic_certified_map::{AsHashTree, RbTree}; +use ic_types::CanisterId; +use sha2::{Digest, Sha256}; +use std::time::{SystemTime, UNIX_EPOCH}; + +type Hash = [u8; 32]; + +fn hash(data: T) -> Hash +where + T: AsRef<[u8]>, +{ + let mut hasher = Sha256::new(); + hasher.update(data); + hasher.finalize().into() +} + +fn get_timestamp() -> u128 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() +} + +fn usage_example() { + let canister_id = CanisterId::from_u64(42); + let mut rb_tree = RbTree::<&'static str, Hash>::new(); + + let data_key = "key1"; + let data_hash = hash("value1"); + rb_tree.insert(data_key, data_hash); + + let certified_data = rb_tree.root_hash(); + + let current_timestamp = get_timestamp(); + + let mut certificate_builder = + CertificateBuilder::new(&canister_id.get().0.to_text(), &certified_data) + .expect("Failed to parse canister id"); + + let CertificateData { + cbor_encoded_certificate, + root_key, + certificate: _, + } = certificate_builder + .with_time(current_timestamp) + .build() + .expect("Invalid certificate params provided"); + + let certificate = Certificate::from_cbor(&cbor_encoded_certificate) + .expect("Failed to deserialize certificate"); + + certificate + .verify(&canister_id.get().to_vec(), &root_key) + .expect("Failed to verify certificate"); +} +```