Skip to content

Commit b3e4f2d

Browse files
committed
feat: support ic-cose
1 parent 2f7bda0 commit b3e4f2d

File tree

10 files changed

+604
-170
lines changed

10 files changed

+604
-170
lines changed

Cargo.lock

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

src/idempotent-proxy-canister/Cargo.toml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,6 @@ license.workspace = true
1616
crate-type = ["cdylib"]
1717

1818
[dependencies]
19-
bytes = "1.6"
20-
candid = "0.10"
21-
ic-cdk = "0.14"
22-
ic-cdk-timers = "0.8"
23-
ic-stable-structures = "0.6"
2419
http = { workspace = true }
2520
base64 = { workspace = true }
2621
ciborium = { workspace = true }
@@ -29,3 +24,10 @@ serde = { workspace = true }
2924
serde_json = { workspace = true }
3025
serde_bytes = { workspace = true }
3126
sha3 = { workspace = true }
27+
bytes = "1.6"
28+
candid = "0.10"
29+
ic-cdk = "0.14"
30+
ic-cdk-timers = "0.8"
31+
ic-stable-structures = "0.6"
32+
ic_cose_types = "0.1"
33+
getrandom = { version = "0.2", features = ["custom"] }

src/idempotent-proxy-canister/idempotent-proxy-canister.did

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type CanisterHttpRequestArgument = record {
1313
headers : vec HttpHeader;
1414
};
1515
type ChainArgs = variant { Upgrade : UpgradeArgs; Init : InitArgs };
16+
type CoseClient = record { id : principal; namespace : text };
1617
type HttpHeader = record { value : text; name : text };
1718
type HttpMethod = variant { get; head; post };
1819
type HttpResponse = record {
@@ -23,18 +24,19 @@ type HttpResponse = record {
2324
type InitArgs = record {
2425
service_fee : nat64;
2526
ecdsa_key_name : text;
27+
cose : opt CoseClient;
2628
proxy_token_refresh_interval : nat64;
2729
subnet_size : nat64;
2830
};
2931
type Result = variant { Ok : bool; Err : text };
3032
type Result_1 = variant { Ok; Err : text };
31-
type Result_2 = variant { Ok : State; Err };
32-
type State = record {
33+
type Result_2 = variant { Ok : StateInfo; Err };
34+
type StateInfo = record {
3335
proxy_token_public_key : text;
3436
service_fee : nat64;
3537
ecdsa_key_name : text;
3638
managers : vec principal;
37-
allowed_callers : vec principal;
39+
cose : opt CoseClient;
3840
uncollectible_cycles : nat;
3941
agents : vec Agent;
4042
incoming_cycles : nat;
@@ -48,6 +50,7 @@ type TransformContext = record {
4850
};
4951
type UpgradeArgs = record {
5052
service_fee : opt nat64;
53+
cose : opt CoseClient;
5154
proxy_token_refresh_interval : opt nat64;
5255
subnet_size : opt nat64;
5356
};
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use candid::{utils::ArgumentEncoder, CandidType, Principal};
2+
use ic_cose_types::types::{PublicKeyInput, PublicKeyOutput, SignInput};
3+
use serde::{Deserialize, Serialize};
4+
use serde_bytes::ByteBuf;
5+
6+
#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
7+
pub struct CoseClient {
8+
pub id: Principal,
9+
pub namespace: String,
10+
}
11+
12+
impl CoseClient {
13+
pub async fn ecdsa_public_key(&self, derivation_path: Vec<ByteBuf>) -> Result<ByteBuf, String> {
14+
let output: Result<PublicKeyOutput, String> = call(
15+
self.id,
16+
"ecdsa_public_key",
17+
(Some(PublicKeyInput {
18+
ns: self.namespace.clone(),
19+
derivation_path,
20+
}),),
21+
0,
22+
)
23+
.await?;
24+
let output = output?;
25+
Ok(output.public_key)
26+
}
27+
28+
pub async fn ecdsa_sign(
29+
&self,
30+
derivation_path: Vec<ByteBuf>,
31+
message: ByteBuf,
32+
) -> Result<ByteBuf, String> {
33+
let output: Result<ByteBuf, String> = call(
34+
self.id,
35+
"ecdsa_sign",
36+
(SignInput {
37+
ns: self.namespace.clone(),
38+
derivation_path,
39+
message,
40+
},),
41+
0,
42+
)
43+
.await?;
44+
output
45+
}
46+
}
47+
48+
async fn call<In, Out>(id: Principal, method: &str, args: In, cycles: u128) -> Result<Out, String>
49+
where
50+
In: ArgumentEncoder + Send,
51+
Out: candid::CandidType + for<'a> candid::Deserialize<'a>,
52+
{
53+
let (res,): (Out,) = ic_cdk::api::call::call_with_payment128(id, method, args, cycles)
54+
.await
55+
.map_err(|(code, msg)| {
56+
format!(
57+
"failed to call {} on {:?}, code: {}, message: {}",
58+
method, &id, code as u32, msg
59+
)
60+
})?;
61+
Ok(res)
62+
}
Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,4 @@
1-
use base64::{engine::general_purpose::URL_SAFE_NO_PAD as base64_url, Engine};
2-
use ciborium::into_writer;
31
use ic_cdk::api::management_canister::ecdsa;
4-
use serde_bytes::ByteBuf;
5-
use sha3::{Digest, Sha3_256};
6-
7-
// use Idempotent Proxy's Token: Token(pub u64, pub String, pub ByteBuf);
8-
// https://github.com/ldclabs/idempotent-proxy/blob/main/src/idempotent-proxy-types/src/auth.rs#L15
9-
pub async fn sign_proxy_token(
10-
key_name: &str,
11-
expire_at: u64, // UNIX timestamp, in seconds
12-
message: &str, // use RPCAgent.name as message
13-
) -> Result<String, String> {
14-
let mut buf: Vec<u8> = Vec::new();
15-
into_writer(&(expire_at, message), &mut buf).expect("failed to encode Token in CBOR format");
16-
let digest = sha3_256(&buf);
17-
let sig = sign_with(key_name, vec![b"sign_proxy_token".to_vec()], digest)
18-
.await
19-
.map_err(err_string)?;
20-
buf.clear();
21-
into_writer(&(expire_at, message, ByteBuf::from(sig)), &mut buf).map_err(err_string)?;
22-
Ok(base64_url.encode(buf))
23-
}
24-
25-
pub async fn get_proxy_token_public_key(key_name: &str) -> Result<String, String> {
26-
let pk = public_key_with(key_name, vec![b"sign_proxy_token".to_vec()]).await?;
27-
Ok(base64_url.encode(pk.public_key))
28-
}
292

303
pub async fn sign_with(
314
key_name: &str,
@@ -67,13 +40,3 @@ pub async fn public_key_with(
6740

6841
Ok(response)
6942
}
70-
71-
pub fn err_string(err: impl std::fmt::Display) -> String {
72-
err.to_string()
73-
}
74-
75-
pub fn sha3_256(data: &[u8]) -> [u8; 32] {
76-
let mut hasher = Sha3_256::new();
77-
hasher.update(data);
78-
hasher.finalize().into()
79-
}

src/idempotent-proxy-canister/src/init.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use candid::CandidType;
22
use serde::Deserialize;
33
use std::time::Duration;
44

5-
use crate::{store, tasks};
5+
use crate::{cose::CoseClient, store, tasks};
66

77
#[derive(Clone, Debug, CandidType, Deserialize)]
88
pub enum ChainArgs {
@@ -16,13 +16,15 @@ pub struct InitArgs {
1616
proxy_token_refresh_interval: u64, // seconds
1717
subnet_size: u64, // set to 0 to disable receiving cycles
1818
service_fee: u64, // in cycles
19+
cose: Option<CoseClient>,
1920
}
2021

2122
#[derive(Clone, Debug, CandidType, Deserialize)]
2223
pub struct UpgradeArgs {
2324
proxy_token_refresh_interval: Option<u64>, // seconds
2425
subnet_size: Option<u64>,
2526
service_fee: Option<u64>, // in cycles
27+
cose: Option<CoseClient>,
2628
}
2729

2830
#[ic_cdk::init]
@@ -42,6 +44,7 @@ fn init(args: Option<ChainArgs>) {
4244
} else {
4345
100_000_000
4446
};
47+
s.cose = args.cose;
4548
});
4649
}
4750
ChainArgs::Upgrade(_) => {
@@ -89,6 +92,9 @@ fn post_upgrade(args: Option<ChainArgs>) {
8992
if let Some(service_fee) = args.service_fee {
9093
s.service_fee = service_fee;
9194
}
95+
if let Some(cose) = args.cose {
96+
s.cose = Some(cose);
97+
}
9298
});
9399
}
94100
Some(ChainArgs::Init(_)) => {
@@ -101,6 +107,7 @@ fn post_upgrade(args: Option<ChainArgs>) {
101107

102108
ic_cdk_timers::set_timer(Duration::from_secs(0), || {
103109
ic_cdk::spawn(async {
110+
store::state::init_ecdsa_public_key().await;
104111
tasks::refresh_proxy_token().await;
105112
})
106113
});

src/idempotent-proxy-canister/src/lib.rs

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
use candid::{Nat, Principal};
1+
use candid::{CandidType, Nat, Principal};
22
use ciborium::into_writer;
33
use futures::FutureExt;
44
use ic_cdk::api::management_canister::http_request::{CanisterHttpRequestArgument, HttpResponse};
5+
use serde::{Deserialize, Serialize};
56
use std::collections::BTreeSet;
67

78
mod agent;
9+
mod cose;
810
mod cycles;
911
mod ecdsa;
1012
mod init;
1113
mod store;
1214
mod tasks;
1315

14-
use crate::init::ChainArgs;
16+
use crate::{agent::Agent, cose::CoseClient, init::ChainArgs};
1517

1618
static ANONYMOUS: Principal = Principal::anonymous();
1719

@@ -39,9 +41,9 @@ fn validate_admin_set_managers(args: BTreeSet<Principal>) -> Result<(), String>
3941
async fn admin_set_agents(agents: Vec<agent::Agent>) -> Result<(), String> {
4042
validate_admin_set_agents(agents.clone())?;
4143

42-
let (ecdsa_key_name, proxy_token_refresh_interval) =
43-
store::state::with(|s| (s.ecdsa_key_name.clone(), s.proxy_token_refresh_interval));
44-
tasks::update_proxy_token(ecdsa_key_name, proxy_token_refresh_interval, agents).await;
44+
let (signer, proxy_token_refresh_interval) =
45+
store::state::with(|s| (s.signer(), s.proxy_token_refresh_interval));
46+
tasks::update_proxy_token(signer, proxy_token_refresh_interval, agents).await;
4547
Ok(())
4648
}
4749

@@ -64,14 +66,43 @@ fn admin_remove_caller(id: Principal) -> Result<bool, String> {
6466
store::state::with_mut(|r| Ok(r.allowed_callers.remove(&id)))
6567
}
6668

69+
#[derive(CandidType, Deserialize, Serialize)]
70+
pub struct StateInfo {
71+
pub ecdsa_key_name: String,
72+
pub proxy_token_public_key: String,
73+
pub proxy_token_refresh_interval: u64, // seconds
74+
pub agents: Vec<Agent>,
75+
pub managers: BTreeSet<Principal>,
76+
pub subnet_size: u64,
77+
pub service_fee: u64, // in cycles
78+
pub incoming_cycles: u128,
79+
pub uncollectible_cycles: u128,
80+
pub cose: Option<CoseClient>,
81+
}
82+
6783
#[ic_cdk::query]
68-
fn get_state() -> Result<store::State, ()> {
69-
let mut s = store::state::with(|s| s.clone());
70-
if is_controller_or_manager().is_err() {
71-
s.agents.iter_mut().for_each(|a| {
72-
a.proxy_token = None;
73-
})
74-
}
84+
fn get_state() -> Result<StateInfo, ()> {
85+
let s = store::state::with(|s| StateInfo {
86+
ecdsa_key_name: s.ecdsa_key_name.clone(),
87+
proxy_token_public_key: s.proxy_token_public_key.clone(),
88+
proxy_token_refresh_interval: s.proxy_token_refresh_interval,
89+
agents: s
90+
.agents
91+
.iter()
92+
.map(|a| Agent {
93+
name: a.name.clone(),
94+
endpoint: a.endpoint.clone(),
95+
max_cycles: a.max_cycles,
96+
proxy_token: a.proxy_token.clone(),
97+
})
98+
.collect(),
99+
managers: s.managers.clone(),
100+
subnet_size: s.subnet_size,
101+
service_fee: s.service_fee,
102+
incoming_cycles: s.incoming_cycles,
103+
uncollectible_cycles: s.uncollectible_cycles,
104+
cose: s.cose.clone(),
105+
});
75106
Ok(s)
76107
}
77108

@@ -256,4 +287,21 @@ fn is_controller_or_manager() -> Result<(), String> {
256287
}
257288
}
258289

290+
#[cfg(all(
291+
target_arch = "wasm32",
292+
target_vendor = "unknown",
293+
target_os = "unknown"
294+
))]
295+
/// A getrandom implementation that always fails
296+
pub fn always_fail(_buf: &mut [u8]) -> Result<(), getrandom::Error> {
297+
Err(getrandom::Error::UNSUPPORTED)
298+
}
299+
300+
#[cfg(all(
301+
target_arch = "wasm32",
302+
target_vendor = "unknown",
303+
target_os = "unknown"
304+
))]
305+
getrandom::register_custom_getrandom!(always_fail);
306+
259307
ic_cdk::export_candid!();

0 commit comments

Comments
 (0)