Skip to content

Commit ec33f0e

Browse files
authored
feat: CRP-2763 add a basic BLS signing service example (#94)
Adds a basic BLS signing example where a user can log in and create signatures and also see the signatures she created in the past. Additonally, there is UI to verify a custom signature.
1 parent 1a228d2 commit ec33f0e

29 files changed

+1332
-19
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: examples-basic-bls-signing
2+
on:
3+
push:
4+
branches:
5+
- main
6+
pull_request:
7+
paths:
8+
- examples/basic_bls_signing/**
9+
- backend/**
10+
- Cargo.toml
11+
- Cargo.lock
12+
- frontend/ic_vetkeys/**
13+
- package.json
14+
- package-lock.json
15+
- .github/workflows/provision-darwin.sh
16+
- .github/workflows/provision-linux.sh
17+
- .github/workflows/examples-basic-bls-signing.yml
18+
concurrency:
19+
group: ${{ github.workflow }}-${{ github.ref }}
20+
cancel-in-progress: true
21+
jobs:
22+
examples-basic-bls-signing-darwin:
23+
runs-on: macos-15
24+
steps:
25+
- uses: actions/checkout@v4
26+
- name: Provision Darwin
27+
run: |
28+
bash .github/workflows/provision-darwin.sh
29+
- name: Deploy Basic BLS Signing Darwin
30+
run: |
31+
set -eExuo pipefail
32+
cargo install candid-extractor
33+
npm i
34+
pushd examples/basic_bls_signing
35+
./deploy_locally.sh
36+
cd frontend
37+
npm run lint
38+
examples-basic-bls-signing-linux:
39+
runs-on: ubuntu-24.04
40+
steps:
41+
- uses: actions/checkout@v4
42+
- name: Provision Linux
43+
run: bash .github/workflows/provision-linux.sh
44+
- name: Deploy Basic BLS Signing Linux
45+
run: |
46+
set -eExuo pipefail
47+
cargo install candid-extractor
48+
npm i
49+
pushd examples/basic_bls_signing
50+
./deploy_locally.sh
51+
cd frontend
52+
npm run lint

Cargo.lock

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ members = [
55
"backend/rs/canisters/ic_vetkeys_encrypted_maps_canister",
66
"backend/rs/canisters/ic_vetkeys_manager_canister",
77
"backend/rs/canisters/tests",
8-
"examples/password_manager_with_metadata/backend"
8+
"examples/basic_bls_signing/backend",
9+
"examples/password_manager_with_metadata/backend",
910
]
1011
resolver = "2"
1112

@@ -39,4 +40,4 @@ ic-dummy-getrandom-for-wasm = "0.1.0"
3940
[profile.release]
4041
lto = true
4142
opt-level = 'z'
42-
panic = 'abort'
43+
panic = 'abort'
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Basic BLS Signing
2+
3+
> [!IMPORTANT]
4+
> These support libraries are under active development and are subject to change. Access to the repositories have been opened to allow for early feedback. Please check back regularly for updates.
5+
6+
The **Basic BLS signing** example demonstrates how to use **[vetKeys](https://internetcomputer.org/docs/building-apps/network-features/vetkeys/introduction)** to implement secure BLS signing on the **Internet Computer (IC)**, where every authenticated user can ask the canister (IC smart contract) to produce signatures, where the **Internet Identity Principal** identifies the signer. This canister ensures that users can only produce signature for their own principal and not for someone else's principal. Furthermore, the vetKeys in this dapp can only be produced upon a user request, as specified in the canister code, meaning that the canister cannot produce signatures for arbitrary users or messages.
7+
8+
For confirming that the canister can only produce signatures in the intended way, users need to inspect the code installed in the canister. For this, it is crucial that canisters using VetKeys have their code public.
9+
10+
![UI Screenshot](ui_screenshot.png)
11+
12+
## Features
13+
14+
- **Signer Authorization**: Only authorized users can produce signatures and only for their own identity.
15+
- **Frontend Signature Verification**: Any user can publish any signature from their principal in the canister storage and the frontend automatically checks the signature validity.
16+
17+
## Setup
18+
19+
### Prerequisites
20+
21+
- [Internet Computer software development kit](https://internetcomputer.org/docs/building-apps/getting-started/install)
22+
- [npm](https://www.npmjs.com/package/npm)
23+
24+
### Install Dependencies
25+
26+
```bash
27+
npm install
28+
```
29+
30+
### Deploy the Canisters
31+
32+
Run the local deployment script, which starts the local development environment (`dfx`) if necessary, builds both backend and frontend (asset) canisters, and installs them locally.
33+
```bash
34+
bash deploy_locally.sh
35+
```
36+
37+
## Example Components
38+
39+
### Backend
40+
41+
The backend consists of a canister that:
42+
* Produces signatures upon a user request.
43+
* Allows users to retrieve the root public key that can be used to check any user's signature for this canister.
44+
* Allows users to store signatures (real or fake) in a log datastructure.
45+
46+
### Frontend
47+
48+
The frontend is a vanilla typescript application providing a simple interface for signing, showing the signatures stored in the canister, and publishing a signature.
49+
50+
To run the frontend in development mode with hot reloading (after running `deploy_locally.sh`):
51+
52+
```bash
53+
npm run dev
54+
```
55+
56+
## Additional Resources
57+
58+
- **[What are VetKeys](https://internetcomputer.org/docs/building-apps/network-features/encryption/vetkeys)** - For more information about VetKeys and VetKD.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "ic-vetkeys-example-basic-bls-signing-backend"
3+
authors.workspace = true
4+
description.workspace = true
5+
documentation.workspace = true
6+
edition.workspace = true
7+
version.workspace = true
8+
license.workspace = true
9+
10+
[lib]
11+
path = "src/lib.rs"
12+
crate-type = ["cdylib"]
13+
14+
[dependencies]
15+
candid = { workspace = true }
16+
getrandom = { version = "0.2", features = ["custom"] }
17+
ic-cdk = { workspace = true }
18+
ic-stable-structures = { workspace = true }
19+
ic-vetkeys = { path = "../../../backend/rs/ic_vetkeys" }
20+
serde = { workspace = true }
21+
serde_bytes = { workspace = true }
22+
serde_cbor = { workspace = true }
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
ROOT_DIR := $(shell git rev-parse --show-toplevel)
2+
3+
.PHONY: compile-wasm
4+
.SILENT: compile-wasm
5+
compile-wasm:
6+
cargo build --release --target wasm32-unknown-unknown
7+
8+
.PHONY: extract-candid
9+
.SILENT: extract-candid
10+
extract-candid: compile-wasm
11+
candid-extractor $(ROOT_DIR)/target/wasm32-unknown-unknown/release/ic_vetkeys_example_basic_bls_signing_backend.wasm > backend.did
12+
13+
.PHONY: clean
14+
.SILENT: clean
15+
clean:
16+
cargo clean
17+
rm -rf .dfx
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
type Signature = record { signature : blob; message : text; timestamp : nat64 };
2+
service : (text) -> {
3+
get_my_signatures : () -> (vec Signature) query;
4+
get_my_verification_key : () -> (blob);
5+
sign_message : (text) -> (blob);
6+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
pub mod types;
2+
use candid::Principal;
3+
use ic_cdk::management_canister::{VetKDCurve, VetKDKeyId, VetKDPublicKeyArgs};
4+
use ic_cdk::{init, query, update};
5+
use ic_stable_structures::{
6+
memory_manager::{MemoryId, MemoryManager, VirtualMemory},
7+
Cell as StableCell, DefaultMemoryImpl, StableBTreeMap,
8+
};
9+
use serde_bytes::ByteBuf;
10+
use std::cell::RefCell;
11+
use types::Signature;
12+
13+
type Memory = VirtualMemory<DefaultMemoryImpl>;
14+
15+
type VetKeyPublicKey = ByteBuf;
16+
type RawSignature = ByteBuf;
17+
type RawMessage = String;
18+
type Timestamp = u64;
19+
20+
thread_local! {
21+
static MEMORY_MANAGER: RefCell<MemoryManager<DefaultMemoryImpl>> =
22+
RefCell::new(MemoryManager::init(DefaultMemoryImpl::default()));
23+
24+
static SIGNATURES: RefCell<StableBTreeMap<(Principal, Timestamp), Signature, Memory>> = RefCell::new(StableBTreeMap::init(
25+
MEMORY_MANAGER.with_borrow(|m| m.get(MemoryId::new(3))),
26+
));
27+
28+
static KEY_NAME: RefCell<StableCell<String, Memory>> =
29+
RefCell::new(StableCell::init(
30+
MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(2))),
31+
String::new(),
32+
)
33+
.expect("failed to initialize key name"));
34+
}
35+
36+
#[init]
37+
fn init(key_name_string: String) {
38+
KEY_NAME.with_borrow_mut(|key_name| {
39+
key_name
40+
.set(key_name_string)
41+
.expect("failed to set key name");
42+
});
43+
}
44+
45+
#[update]
46+
async fn sign_message(message: RawMessage) -> RawSignature {
47+
let signer = ic_cdk::api::msg_caller();
48+
let signature_bytes = ic_vetkeys::management_canister::sign_with_bls(
49+
message.as_bytes().to_vec(),
50+
context(&signer),
51+
key_id(),
52+
)
53+
.await
54+
.expect("ic_vetkeys' sign_with_bls failed");
55+
56+
SIGNATURES.with_borrow_mut(|sigs| {
57+
let timestamp = ic_cdk::api::time();
58+
let sig = Signature {
59+
message,
60+
signature: signature_bytes.clone(),
61+
timestamp,
62+
};
63+
64+
// In rare cases a user may call `sign_message` in quick succession
65+
// so that multiple signature requests are in a single consensus round,
66+
// which leads to ic_cdk::api::time() returning the same value for all
67+
// of the requests. In that case, we just keep increasing the timestamp
68+
// used in the map key until we hit a slot this is available.
69+
let mut timestamp_for_mapkey = timestamp;
70+
while sigs.get(&(signer, timestamp_for_mapkey)).is_some() {
71+
timestamp_for_mapkey += 1;
72+
}
73+
74+
assert!(sigs.insert((signer, timestamp_for_mapkey), sig).is_none());
75+
});
76+
77+
ByteBuf::from(signature_bytes)
78+
}
79+
80+
#[query]
81+
fn get_my_signatures() -> Vec<Signature> {
82+
let me = ic_cdk::api::msg_caller();
83+
SIGNATURES.with_borrow(|signer_and_timestamp_to_sig| {
84+
signer_and_timestamp_to_sig
85+
.range((me, 0)..)
86+
.take_while(|((signer, _ts), _sig)| signer == &me)
87+
.map(|((_, _), sig)| sig)
88+
.collect()
89+
})
90+
}
91+
92+
#[update]
93+
async fn get_my_verification_key() -> VetKeyPublicKey {
94+
let request = VetKDPublicKeyArgs {
95+
canister_id: None,
96+
context: context(&ic_cdk::api::msg_caller()),
97+
key_id: key_id(),
98+
};
99+
let result = ic_cdk::management_canister::vetkd_public_key(&request)
100+
.await
101+
.expect("call to vetkd_public_key failed");
102+
103+
VetKeyPublicKey::from(result.public_key)
104+
}
105+
106+
fn context(signer: &Principal) -> Vec<u8> {
107+
// A domain separator is not strictly necessary in this dapp, but having one is considered a good practice.
108+
const DOMAIN_SEPARATOR: [u8; 22] = *b"basic_bls_signing_dapp";
109+
const DOMAIN_SEPARATOR_LENGTH: u8 = DOMAIN_SEPARATOR.len() as u8;
110+
[DOMAIN_SEPARATOR_LENGTH]
111+
.into_iter()
112+
.chain(DOMAIN_SEPARATOR)
113+
.chain(signer.as_ref().iter().cloned())
114+
.collect()
115+
}
116+
117+
fn key_id() -> VetKDKeyId {
118+
VetKDKeyId {
119+
curve: VetKDCurve::Bls12_381_G2,
120+
name: KEY_NAME.with_borrow(|key_name| key_name.get().clone()),
121+
}
122+
}
123+
124+
// In the following, we register a custom getrandom implementation because
125+
// otherwise getrandom (which is a dependency of some other dependencies) fails to compile.
126+
// This is necessary because getrandom by default fails to compile for the
127+
// wasm32-unknown-unknown target (which is required for deploying a canister).
128+
// Our custom implementation always fails, which is sufficient here because
129+
// the used RNGs are _manually_ seeded rather than by the system.
130+
#[cfg(all(
131+
target_arch = "wasm32",
132+
target_vendor = "unknown",
133+
target_os = "unknown"
134+
))]
135+
getrandom::register_custom_getrandom!(always_fail);
136+
#[cfg(all(
137+
target_arch = "wasm32",
138+
target_vendor = "unknown",
139+
target_os = "unknown"
140+
))]
141+
fn always_fail(_buf: &mut [u8]) -> Result<(), getrandom::Error> {
142+
Err(getrandom::Error::UNSUPPORTED)
143+
}
144+
145+
ic_cdk::export_candid!();
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
use std::borrow::Cow;
2+
3+
use candid::{CandidType, Principal};
4+
use ic_stable_structures::{storable::Bound, Storable};
5+
use serde::{Deserialize, Serialize};
6+
7+
pub type CanisterId = Principal;
8+
9+
#[derive(CandidType, Serialize, Deserialize, Clone, Debug)]
10+
11+
pub struct Signature {
12+
pub message: String,
13+
#[serde(with = "serde_bytes")]
14+
pub signature: Vec<u8>,
15+
pub timestamp: u64,
16+
}
17+
18+
impl Storable for Signature {
19+
fn to_bytes(&self) -> Cow<[u8]> {
20+
Cow::Owned(serde_cbor::to_vec(self).expect("failed to serialize"))
21+
}
22+
23+
fn from_bytes(bytes: Cow<[u8]>) -> Self {
24+
serde_cbor::from_slice(&bytes).expect("failed to deserialize")
25+
}
26+
27+
const BOUND: Bound = Bound::Unbounded;
28+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
# Check that `dfx` is installed.
6+
dfx --version >> /dev/null
7+
8+
# Run `dfx` if it is not already running.
9+
dfx ping &> /dev/null || dfx start --background --clean >> /dev/null
10+
11+
# Deploy the Internet Identity canister.
12+
dfx deps pull && dfx deps init && dfx deps deploy
13+
14+
# Deploy backend canister.
15+
dfx deploy --argument '("dfx_test_key")' basic_bls_signing
16+
17+
# Deploy frontend canister.
18+
dfx deploy www

0 commit comments

Comments
 (0)