Closed: BNT-8 - vetKeys - Enabling Privacy Preserving Applications on the IC

Hello hackers and bounty hunters,

In time for the publication of the mock API for vetKeys and the associated community conversation, we’ll open a couple of bounties to get you acquainted with the API and its many applications before the actual implementation will be available on the IC.

Interested? Please read on…

Overview

Status: Closed
Project Type: Cooperative/Contest - Multiple workers can submit work, and the bounty is shared
Time Commitment: Days
Experience Level: Intermediate/Advanced
Size: USD 24’000 in ICP (at time of distribution)
Deadline: September 10th EoD AoE

Description

The VetKeys bounties are an exciting opportunity for IC developers to participate in the ongoing development and assessment of the vetKeys feature. As part of this program, two demos of the vetKeys feature have been released, and developers are invited to explore the API capabilities, build applications, and provide valuable feedback.

The bounty program focuses on four key use cases: IBE (Identity-Based Encryption), Group Sharing, Timelock Encryption, and Open/Blue skies. Each use case has a prize pool of $6000, offering participants the chance to win rewards for their innovative contributions and valuable feedback on whether the proposed API addresses use case needs.

Links to the proposed API and demos:

Use Cases

There are four key areas in which we’d love to see submissions:

  • IBE ($6000 prize pool): Identity-Based Encryption (IBE) is a cryptographic scheme where the public key of an entity is derived from its unique identifier, such as an email address, username, or principal. IBE allows for secure communication between entities without the need for exchanging public keys beforehand. Some examples:

    • Encrypted file sharing: Encrypt files and documents based on the identities of authorized users.
    • E2EE messaging: Create or extend a messaging application where users could securely communicate without the need for exchanging public keys beforehand.
    • Secure email communication: Traditional email encryption often requires users to use a public key infrastructure (PKI). A secure email dapp could allow users could send encrypted emails based on recipient identifiers (such as email or wallet addresses), eliminating the need for pre-shared keys or complex setups.
    • Private transactions: By encrypting financial data based on user identifiers, you could ensure that only authorized parties can access and process sensitive financial information.
  • Group Sharing ($6000 prize pool): By Group sharing we refer to the ability to securely share information and resources within a defined group of individuals. Developers can create applications that enable efficient and secure collaboration among group members. For instance:

    • File-sharing platform: Allow users within a specific group to access and collaborate on shared documents, ensuring only authorized members can view and modify the content.
    • Private social networking: A social networking platform that allows users to create private groups for sharing content, discussions, and media.
    • Gaming communities: Encrypted group sharing can be leveraged in gaming communities to provide a secure platform for players to communicate, share game-related content, and coordinate gameplay.
  • Timelock Encryption ($6000 prize pool): Timelock Encryption involves the concept of encrypting data and setting a time-based restriction on when the encrypted data can be accessed. Participants could build applications that use vetKeys and the IC’s notion of time to provide secure access to sensitive information for a specific period. For example

    • Secure file sharing with expiry: a secure document storage system could use timelock encryption to grant temporary access to confidential files for a limited time.
    • Private voting: Each vote in a voting system could be encrypted with a time-based restriction, ensuring that the vote remains confidential and can only be decrypted within a specific timeframe. This protects the privacy and integrity of the voting process while allowing authorized authorities to decrypt and count the votes during the designated period.
    • Time-limited private auction: Similar to the private voting scenario, bids could be encrypted with a time-based restriction, allowing them to be revealed and considered only within a specified timeframe.
    • MEV protection: Timelock Encryption could be a valuable tool for mitigating Miner/Maximal Extractable Value (MEV) exploits in blockchain-based DeFi systems. Sensitive transaction details could be encrypted with a time-based restriction, ensuring that the contents of the transactions remain confidential until a specified time.
  • Open / Blue skies ($6000 prize pool): The Open/Blue skies category is an open-ended opportunity for developers to explore innovative and novel use cases that leverage the vetKeys feature. Participants are encouraged to think outside the box and come up with creative applications that can benefit from the proposed API. For instance, a developer might create a decentralized identity management system that uses vetKeys for secure user authentication and authorization.
    Further inspiration:

    • VRF (Verifiable Random Function): VRF is a primitive that generates random output while providing verifiable proof of its correctness. Developers can explore how VRF can be integrated into applications to generate random numbers or ensure randomness in various scenarios, such as gaming, voting systems, or random selection processes.
    • Witness encryption: Witness encryption is a scheme where the decryption of a ciphertext requires a specific condition or witness to be satisfied. Participants could explore the applications of witness encryption in scenarios like secure multi-party computation, anonymous credentials, or access control systems.

Many of these use cases or variations were described in some detail in the first community conversation.

These bounties are quite open in the sense that it would be great to see new applications being developed and potentially put forward for subsequent grant funding, but it is also ok if the submission is an extension to an existing app (eg the encrypted notes dapp)

Acceptance Criteria

  • Uses the vetKey API
  • Falls into one of the use case categories
  • Provide feedback on the suggested system API (does it address your needs? A few sentences are enough)
  • Demo application deployed to the IC
  • Video Pitch/Demo (max. 4min)
  • Proper Readme, see here
  • Open Source license (MIT or Apache-2.0)

Evaluation Criteria

  • Design/UX
  • Functionality
  • Code quality

Note

The proposed version of this system API is for demonstration purposes only and should not be used in production.

How to participate?

Post your submission here in this thread.
Please be aware that Terms and Conditions of the DFINITY Developer Grants Program apply.

References

18 Likes

Hello everyone,

I’m excited to share some potential plans for participating in the VetKeys bounty program with my project, B3Wallet. B3Wallet is a decentralized, multi-chain digital wallet that I’ve been developing, and I believe the vetKeys API could significantly enhance its functionality and security.

Use Case 1: Identity-Based Encryption (IBE)

One idea I’m considering is implementing a username-password-based login system within B3Wallet. This would provide an alternative to the current login method using Internet Identity, offering users a familiar and straightforward way to access their wallets. The public key of a user would be derived from their unique username, enabling secure communication and transaction signing without the need for exchanging public keys beforehand.

Use Case 2: Group Sharing

Another potential enhancement for B3Wallet involves the implementation of a secure backup system for the wallet canister. This backup could be used to restore accounts and signers, ensuring that all users can recover their shared wallet in case of any issues. This would be particularly useful for wallets with multiple signers, such as those used by couples, families, or businesses.

Use Case 3: Timelock Encryption

To enhance the security of transactions, I’m considering implementing a password protection feature in B3Wallet. Before a transaction can be signed by a signer, they would need to enter a password. This password could be encrypted with a time-based restriction, ensuring that the transaction remains confidential and can only be processed within a specific timeframe.

Feedback on the vetKeys API

So far, I’ve found the vetKeys API to be well-designed and powerful. It offers a lot of flexibility and potential for enhancing the security and functionality of B3Wallet. I’m looking forward to exploring it further as I consider implementing these features.

Demo and Documentation

You can check out the current live version of B3Wallet at b3wallet.live. The source code for B3Wallet is also available on the B3Wallet GitHub repository. I will continue to update the live version and the repository as I work on implementing these features and enhancements.

I’m excited about the potential of the vetKeys API and I’m looking forward to seeing how it can enhance B3Wallet and other applications in the Internet Computer ecosystem. I welcome any feedback or suggestions from the community.

Best,
Behrad

5 Likes

The other day with some friends we were thinking about implementing on the IC the equivalent of an interac in canada (i.e. a transfer of money where the receiver has to find the correct answer to the question the sender asked in order to get the money). With vetkeys we could encrypt the question and answer with the principal of the recipient (IBE use case). Could this be a good candidate for this bounty?
Also I havent checked all the links yet, will the first API be available on motoko or its only in rust?

you can use vetkd_system_api canister in motoko, you can specify vetkd_system_api dependency in dfx.json

"main": {
    "dependencies": ["vetkd_system_api"],
    "main": "backend/main.mo",
    "type": "motoko"
},
"vetkd_system_api": {
    "type": "custom",
    "candid": "backend/vetkd_system_api/vetkd_system_api.did",
    "wasm": "backend/vetkd_system_api/vetkd_system_api.wasm.gz",
    "shrink": false
}

in main.mo, it is enough to import the canister and call its methods

import VETKD_SYSTEM_API "canister:vetkd_system_api";
Спойлер

You may also need types:

module {
    type CanisterId = Principal;

    public type VetKDCurve = {
        #bls12_381;
    };

    public type VetKDKeyId = {
        curve : VetKDCurve;
        name : Text;
    };

    public type VetKDPublicKeyRequest = {
        canister_id : ?CanisterId;
        derivation_path : [Blob];
        key_id : VetKDKeyId;
    };

    public type VetKDPublicKeyReply = {
        public_key : Blob;
    };

    public type VetKDEncryptedKeyRequest = {
        derivation_id : Blob;
        encryption_public_key : Blob;
        key_id : VetKDKeyId;
        public_key_derivation_path : [Blob];
    };

    public type VetKDEncryptedKeyReply = {
        encrypted_key : Blob;
    };
};

You can see an example of encrypted notes with vetKD at the link

3 Likes

Thank you for this exciting feature. I think it’s very powerful as this allows for a great UX and is something that (to the best of my knowledge) neither other blockchains and, even more importantly, traditional cloud infrastructure can provide.

I’d like to use vetKeys for the browser-based AI assistant I’m working on (first prototype: https://x6occ-biaaa-aaaai-acqzq-cai.icp0.io/) to encrypt the user’s chats when storing them in the backend canister (and thus have a completely private AI chatbot powered by the IC).

If you have any feedback on my proposed idea to use vetKeys, I’d be happy to hear it. While this might not be the most novel or creative idea how to leverage vetKeys, I’m excited to integrate it as I’ve been looking for a way to guarantee privacy to users.

5 Likes

If anyone is looking to use vite instead of webpack, I found this plugin easy to work with and straightforward to integrate ic-vetkd-utils with it: GitHub - nshen/vite-plugin-wasm-pack: 🦀 Vite plugin for rust wasm-pack (I’ve also tried this one but wasn’t really having much luck with it: GitHub - Menci/vite-plugin-wasm: Add WebAssembly ESM integration (aka. Webpack's `asyncWebAssembly`) to Vite and support `wasm-pack` generated modules.). This branch has my implementation, so could serve as another example: GitHub - patnorris/DecentralizedAIonIC at firstClientSideEncryption

1 Like

These are a few questions that came to mind during development so far, would be great to get your input on them:
Once vetKeys are released in production, in which ways will the integration be facilitated? Will there be a library to use in Motoko and Rust? On the frontend, will there be an npm package? If so, what would that package include (e.g. only ic-vetkd-utils or also code like the CryptoService here: https://github.com/dfinity/examples/blob/master/motoko/encrypted-notes-dapp-vetkd/src/frontend/src/lib/crypto.ts)?

I deployed the branch with my current implementation (GitHub - patnorris/DecentralizedAIonIC at firstClientSideEncryption) to the mainnet and after login it takes ca 18 sec for the Crypto Service to be initiated. Does this seem right? Do you think this could be sped up somehow? After vetKeys are released to production, do you foresee this initiation time to change?

Are there any recommendations you can give how the user (of an app integrating vetKeys) should/could be informed about the encryption? So far, I haven’t been able to come up with any additional risk/disadvantage for the user (compared to storing the data “in plain” on the IC), besides maybe the wait time til the crypto service is initiated and that no dev could help them with data recovery in case the user loses access to the account tied to the principal. Do you see any additional risks/disadvantages for the users?

Which other security considerations or best practices are especially important for devs here? I saw you included this security checklist in the example repo (https://github.com/dfinity/examples/blob/master/motoko/encrypted-notes-dapp-vetkd/security-checklist.md), are there any others to add specific to vetKeys?

And some general initial feedback on the integration work; I think the encrypted notes example repo was great and it made it pretty easy to integrate the needed code into mine. The only real blocker I had was getting the wasm from ic-vetkd-utils to work with vite (which I had already been working with) but once I started using vite-plugin-wasm-pack (see comment above), it worked right away. I hadn’t paid attention initially that there isn’t any vetkd_system_api canister deployed yet on the IC but once understood/properly read, deploying that was also pretty swift and worked without any debugging. Thanks!

If anyone is looking to use vite instead of webpack

Thanks for sharing this!

I took an alternative approach that I’ll share too. I was able to skip the plugin by setting up a local library using PNPM workspaces. This should also work with NPM or Yarn workspaces. The library is only 3 files.

Cargo.toml:

[package]
name = "vetkd-utils"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
ic-vetkd-utils = { git = "https://github.com/dfinity/ic", rev = "39a0e5858f8b3028252783c742b49f3eb924c287" }

package.json:

{
  "name": "vetkd-utils",
  "private": true,
  "scripts": {
    "build": "wasm-pack build --target web --release"
  },
  "module": "pkg/vetkd_utils.js",
  "types": "pkg/vetkd_utils.d.ts"
}

src/lib.rs

pub use ic_vetkd_utils::*;

With this I was able to import { TransportSecretKey } from 'vetkd-utils'. This has the advantage of easily being able to update the code commit that’s referenced without having to build and distribute the WASM file manually.

Will there be a library to use in Motoko and Rust

It’s a system API, so it will be callable via the Management Canister interface with cdk-rs (I don’t know what the equivalent is for Motoko, but it should work similarly).

On the frontend, will there be an npm package

My understanding is that ic-vetkd-utils (or some version of it) will be published to NPM.

it takes ca 18 sec for the Crypto Service to be initiated

18 seconds is a long time. One thing you could do is merge multiple calls into one. There’s a call for the public key and another call for the private key that could be merged. I don’t know your app logic well, but maybe there’s some other update calls on application boot that could be merged too.

Another thing you could do is cache the keys locally. This needs to be done with care though and you need to be careful of XSS attacks getting access to the keys.

I haven’t implemented this myself yet and I’m not a security expert so I can’t advocate for how bullet proof (or not) that this solution is, but what I was thinking to do was load the II keys via a web worker (or through an IIFE if web workers don’t work), store the II keys in non-extractable crypto key storage, then load the vetKeys from the canister inside the web worker, keep the vetKeys in memory in the web worker but encrypt them and store them in local storage so they can be decrypted with the II keys when the user returns.

2 Likes

Thanks, that’s a cool approach. Do you have a repo online that implements the local library? Would be interested to take a look.

Yeah, 18 sec is a bit, so definitely worthwile for me to look more into how I can optimize it. I’ll post it here if I find something.

Storing the vetKeys encrypted in local storage between sessions sounds like an interesting idea. If I understand correctly, we would thus only have to generate new ones in case none are in the local storage and the tradeoff would be that we might have an additional attack vector (not sure how risky it is either though as the vetKeys are encrypted).

Do you have a repo online that implements the local library?

It’s not open source, but it’s literally just those 3 files that I posted in this file structure. If it’s not clear I can put together a separate repo with that setup.

- lib/
  - vetkdutils/
    - src/
      - lib.rs
    - package.json
    - Cargo.toml
- src/
  - frontend/
    - package.json (reference "vetkdutils" here)
- package.json

we would thus only have to generate new ones in case none are in the local storage and the tradeoff would be that we might have an additional attack vector

Yes exactly.

not sure how risky it is either though as the vetKeys are encrypted

I believe XSS is the biggest risk because an attacker could potentially get access to the II keys that are stored in the IndexedDB and then just decrypt everything.

A slightly updated approach that I’m thinking of is using a user provided “pin” to encrypt everything instead of the II credentials. Or to just make the user log in with II every time they leave and come back. 1st approach is harder to implement but more annoying for users. A pin should be easy enough to input quickly.

Yeah, 18 sec is a bit, so definitely worthwile for me to look more into how I can optimize it. I’ll post it here if I find something.

I’ll be interested to see if you find anything!

Cool, only needing the 3 files you posted is indeed a nicely simple approach then :slight_smile:

I don’t know enough detail about it but maybe the “pin” could also be the fingerprint sensor or other hardware II is using. So to decrypt their data, users would be asked to confirm via fingerprint or another sensor.

Sounds good, hopefully will find a couple of optimizations.

maybe the “pin” could also be the fingerprint sensor or other hardware II is using

That would be awesome. It would be possible to derive an asymmetric encryption key with WebAuthn using the PRF extension: Web Authentication: An API for accessing Public Key Credentials - Level 3. This is very new though so I think it will take some time to be implemented.

1 Like

I was able to speed up the initiation time for the Crypto Service by parallelizing some of the calls in its init() function (as you also suggested):
Initializing the wasm (for ic-vetkd-utils) took between 0.5-2 sec, I’m doing this when the app is loaded now (instead of having it as part of the overall init chain).

The two time-relevant functions in Crypto Service’s init are encrypted_symmetric_key_for_caller and symmetric_key_verification_key, each takes 8-10 sec. The other functions are pretty much negligible here (i.e. sub 100ms). So when making these two calls in parallel and awaiting them both, we can get to 8-10 sec overall init time (after the user logs in).

I’m wondering if encrypted_symmetric_key_for_caller and symmetric_key_verification_key could be further sped up by making these calls directly to the vetkd_system_api canister (instead of calling the backend canister which then calls vetkd_system_api). Is or will this be possible?

Good to hear about your speed ups.

So you parallelized on the frontend, interesting. I did it slightly differently, by making a single endpoint on the backend canister that would return both keys:

#[update]
#[candid_method]
async fn get_key_pair(transport_public_key: Vec<u8>) -> GetKeyPairDto {
    let principal = assert_principal_not_anonymous();
    let user_id = assert_has_user_id(principal).expect("user_id not found");

    let public_key_request = VetKDPublicKeyRequest {
        canister_id: None,
        derivation_path: vec![b"key".to_vec()],
        key_id: bls12_381_test_key_1(),
    };

    let (public_key_response,): (VetKDPublicKeyReply,) = call(
        vetkd_system_api_canister_id(),
        "vetkd_public_key",
        (public_key_request,),
    )
    .await
    .expect("call to vetkd_public_key failed");

    let private_key_request = VetKDEncryptedKeyRequest {
        derivation_id: user_id.into_bytes().to_vec(),
        public_key_derivation_path: vec![b"key".to_vec()],
        key_id: bls12_381_test_key_1(),
        encryption_public_key: transport_public_key,
    };

    let (private_key_response,): (VetKDEncryptedKeyReply,) = call(
        vetkd_system_api_canister_id(),
        "vetkd_encrypted_key",
        (private_key_request,),
    )
    .await
    .expect("call to vetkd_encrypted_key failed");

    GetKeyPairDto {
        public_key: hex::encode(public_key_response.public_key),
        private_key: hex::encode(private_key_response.encrypted_key),
    }
}

Your approach may actually be faster. I’ll try it out too to compare.

could be further sped up by making these calls directly to the vetkd_system_api canister

It will be a management canister interface so it will only be callable from your canister. From an access control perspective, it should be hidden behind your backend canister anyway so you can decide who should get access to what key. Otherwise anyone could derive any key they want if it was open to the frontend.

3 Likes

Getting this error message Uncaught (in promise) invalid encrypted key: verification failed while executing the following frontend function:

export async function getAes256GcmKey() {
    const seed = window.crypto.getRandomValues(new Uint8Array(32));
    const tsk = new vetkd.TransportSecretKey(seed);

    const ekBytes: Uint8Array = await backend.encrypted_symmetric_key_for_caller(tsk.public_key());
    const pkBytes: Uint8Array = await backend.symmetric_key_verification_key();

    return tsk.decrypt_and_hash(
        ekBytes,
        pkBytes,
        Principal.fromText(backendCanisterId).toUint8Array(),
        32,
        new TextEncoder().encode("aes-256-gcm")
    );
}

against the following backend:

#[update]
async fn encrypted_symmetric_key_for_caller(encryption_public_key: Vec<u8>) -> Vec<u8> {
    debug_println_caller("encrypted_symmetric_key_for_caller");

    let user_principal = caller();

    let request = VetKDEncryptedKeyRequest {
        derivation_id: user_principal.as_slice().to_vec(),
        public_key_derivation_path: vec![b"symmetric_key".to_vec()],
        key_id: bls12_381_test_key_1(),
        encryption_public_key,
    };

    let (response,): (VetKDEncryptedKeyReply,) = ic_cdk::api::call::call(
        vetkd_system_api_canister_id(),
        "vetkd_encrypted_key",
        (request,),
    )
    .await
    .expect("call to vetkd_encrypted_key failed");

    response.encrypted_key
}

#[update]
async fn symmetric_key_verification_key() -> Vec<u8> {
    let request = VetKDPublicKeyRequest {
        canister_id: None,
        derivation_path: vec![b"symmetric_key".to_vec()],
        key_id: bls12_381_test_key_1(),
    };

    let (response,): (VetKDPublicKeyReply,) = ic_cdk::api::call::call(
        vetkd_system_api_canister_id(),
        "vetkd_public_key",
        (request,),
    )
    .await
    .expect("call to vetkd_public_key failed");

    response.public_key
}

fn bls12_381_test_key_1() -> VetKDKeyId {
    VetKDKeyId {
        curve: VetKDCurve::Bls12_381,
        name: "test_key_1".to_string(),
    }
}

fn vetkd_system_api_canister_id() -> CanisterId {
    CanisterId::from_str(VETKD_SYSTEM_API_CANISTER_ID).expect("failed to create canister ID")
}

fn debug_println_caller(method_name: &str) {
    ic_cdk::println!(
        "{}: caller: {} (isAnonymous: {})",
        method_name,
        ic_cdk::caller().to_text(),
        ic_cdk::caller() == candid::Principal::anonymous()
    );
}

The example project was working just fine for me. The code above is based on that example and is only slightly different from it - instead of using hex encoded strings I pass raw blobs around. Can this be the source of an issue?

UPD:
Tried to move back to passing hex encoded strings, but getting the same error.

UPD1:
Found the problem. It is in this line. Not in the line itself, but in how client’s principal is named app_backend_principal for some reason. I tried to put backend canister’s principal in it.

Everything is good now. Thanks!

2 Likes

Cool, thanks for sharing. It’ll be interesting to see which speed ups we get with the different approaches.

A while back, I wanted to make calls on the backend canister in parallel and wrote something like this:
let executingFunctionsBuffer = Buffer.Buffer<async ?Entity.Entity>(entityIds.size());
for (entityId in entityIds.vals()) {
executingFunctionsBuffer.add(get_entity(entityId));
};
let collectingResultsBuffer = Buffer.Buffer<Entity.Entity>(entityIds.size());
var i = 0;
for (entityId in entityIds.vals()) {
switch(await executingFunctionsBuffer.get(i)) {
case null {};
case (?entity) { collectingResultsBuffer.add(entity); };
};
i += 1;
};
return collectingResultsBuffer.toArray();

does this actually make the calls in parallel (and thus speed up the execution as the canister can await all calls at the same time)? Could this be something to be used here as well (so making the two calls to get the two keys in parallel)?
The main difference I can see is that the two return types are different here (public_key_response and private_key_response) vs. having multiple of the same call as in my example, so not sure if it can be implemented easily.

This is not my forte, but I’ve asked internally for help understanding this.

does this actually make the calls in parallel

You can handle those concurrently, but not in parallel. So it could be that making the calls in parallel on the frontend would be better. I’ll try that out at some point.

1 Like

@NathanosDev

I took an alternative approach that I’ll share too. I was able to skip the plugin by setting up a local library using PNPM workspaces. This should also work with NPM or Yarn workspaces. The library is only 3 files.

I gave this a try in my project, then added this to my main package.json (there isn’t one in the frontend folder):

"dependencies": {
    ...
    "vetkd-utils": "file:./vetkd_utils"
  },

However, this gives me an error

ERROR in ./src/group_sharing_frontend/src/index.js 4:0-49
Module not found: Error: Can't resolve 'vetkd-utils' in '/home/timk/ic/group_sharing/src/group_sharing_frontend/src'
resolve 'vetkd-utils' in '/home/timk/ic/group_sharing/src/group_sharing_frontend/src'
  Parsed request is a module
  using description file: /home/timk/ic/group_sharing/package.json (relative path: ./src/group_sharing_frontend/src)
    Field 'browser' doesn't contain a valid alias configuration
    resolve as module
      /home/timk/ic/group_sharing/src/group_sharing_frontend/src/node_modules doesn't exist or is not a directory
...

Line 4 of index.js is
import { TransportSecretKey } from "vetkd-utils";

File structure is like so:

├── src
│   └── group_sharing_frontend
│        └── src
│             └── index.js
├── vetkd_utils
│   ├── Cargo.toml
│   ├── package.json
│   └── src
│       └── lib.rs
└── package.json

Not sure what I’m doing wrong there. Any ideas?

You need to use some form of workspaces to make this work. NPM, PNPM or Yarn workspaces should all work, but they may require a slightly different configuration. I used a PNPM workspace.

A workspace will link “vetkd-utils” into your node_modules so that you can import it in your frontend project.

Thanks! PNPM is new to me, but I found some information and followed all the steps. When trying to deploy I now get this:

ERROR in ./src/vetkd_utils/src/lib.rs 1:4
Module parse failed: Unexpected token (1:4)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
> pub use ic_vetkd_utils::*;

I haven’t so far been able to find what I need to change in webpack.config.js in order to resolve this.