Skip to content

Commit 4ed16fe

Browse files
committed
feat: add ic_delegation_store for deeplink auth
1 parent cedf94c commit 4ed16fe

File tree

12 files changed

+863
-127
lines changed

12 files changed

+863
-127
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ members = [
33
"src/cli_airdrop",
44
"src/cli_cryptogram",
55
"src/cli_dmsg",
6+
"src/ic_delegation_store",
67
"src/ic_dmsg_minter",
78
"src/ic_message",
89
"src/ic_message_channel",
@@ -30,7 +31,6 @@ categories = ["web-programming"]
3031
license = "MIT OR Apache-2.0"
3132

3233
[workspace.dependencies]
33-
async-trait = "0.1"
3434
bytes = "1"
3535
base64 = "0.22"
3636
candid = "0.10"
@@ -59,6 +59,9 @@ ic-certification = "3.0"
5959
ic-oss-types = "1.2"
6060
ic-canister-sig-creation = "1.3"
6161
ic-ed25519 = { version = "0.3" }
62+
ic-http-certification = "3"
63+
lazy_static = "1.5"
64+
once_cell = "1.21"
6265
chrono = { version = "0.4", features = ["serde"] }
6366
clap = { version = "=4.5", features = ["derive"] }
6467
ic-dummy-getrandom-for-wasm = "0.1"

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ twiggy:
1818

1919
# cargo install ic-wasm
2020
build-wasm:
21+
cargo build --release --target wasm32-unknown-unknown --package ic_delegation_store
2122
cargo build --release --target wasm32-unknown-unknown --package ic_dmsg_minter
2223
cargo build --release --target wasm32-unknown-unknown --package ic_message
2324
cargo build --release --target wasm32-unknown-unknown --package ic_message_channel
@@ -27,6 +28,7 @@ build-wasm:
2728

2829
# cargo install candid-extractor
2930
build-did:
31+
candid-extractor target/wasm32-unknown-unknown/release/ic_delegation_store.wasm > src/ic_delegation_store/ic_delegation_store.did
3032
candid-extractor target/wasm32-unknown-unknown/release/ic_dmsg_minter.wasm > src/ic_dmsg_minter/ic_dmsg_minter.did
3133
candid-extractor target/wasm32-unknown-unknown/release/ic_message.wasm > src/ic_message/ic_message.did
3234
candid-extractor target/wasm32-unknown-unknown/release/ic_message_channel.wasm > src/ic_message_channel/ic_message_channel.did

canister_ids.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
"ic": "n3bau-gaaaa-aaaaj-qa4oq-cai",
1515
"local": "n3bau-gaaaa-aaaaj-qa4oq-cai"
1616
},
17+
"ic_delegation_store": {
18+
"ic": "asxpf-ciaaa-aaaap-an33a-cai"
19+
},
1720
"ic_dmsg_minter": {
1821
"ic": "ql553-iqaaa-aaaap-anuyq-cai",
1922
"local": "ql553-iqaaa-aaaap-anuyq-cai"

dfx.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
{
22
"canisters": {
3+
"ic_delegation_store": {
4+
"candid": "src/ic_delegation_store/ic_delegation_store.did",
5+
"declarations": {
6+
"node_compatibility": true
7+
},
8+
"package": "ic_delegation_store",
9+
"optimize": "cycles",
10+
"gzip": true,
11+
"type": "rust"
12+
},
313
"ic_dmsg_minter": {
414
"candid": "src/ic_dmsg_minter/ic_dmsg_minter.did",
515
"declarations": {

src/ic_delegation_store/Cargo.toml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
[package]
2+
name = "ic_delegation_store"
3+
description = ""
4+
publish = false
5+
repository = "https://github.com/ldclabs/ic-panda/tree/main/src/ic_delegation_store"
6+
version.workspace = true
7+
edition.workspace = true
8+
keywords.workspace = true
9+
categories.workspace = true
10+
license.workspace = true
11+
12+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
13+
14+
[lib]
15+
crate-type = ["cdylib"]
16+
17+
[dependencies]
18+
base64 = { workspace = true }
19+
candid = { workspace = true }
20+
ciborium = { workspace = true }
21+
ic-cdk = { workspace = true }
22+
serde = { workspace = true }
23+
ic-stable-structures = { workspace = true }
24+
ic-http-certification = { workspace = true }
25+
ic-dummy-getrandom-for-wasm = { workspace = true }
26+
ic_auth_types = { workspace = true }
27+
ic_auth_verifier = { workspace = true, features = ["envelope"] }
28+
lazy_static = { workspace = true }
29+
once_cell = { workspace = true }
30+
url = { workspace = true }
31+
serde_json = { workspace = true }
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
type CanisterArgs = variant { Upgrade : UpgradeArgs; Init : InitArgs };
2+
type InitArgs = record { allowed_origins : vec text };
3+
type UpgradeArgs = record { allowed_origins : opt vec text };
4+
service : (opt CanisterArgs) -> {}
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
2+
use candid::CandidType;
3+
use ciborium::from_reader;
4+
use ic_auth_types::{cbor_into_vec, ByteBufB64};
5+
use ic_http_certification::{HeaderField, HttpRequest, HttpUpdateRequest};
6+
use serde::{Deserialize, Serialize};
7+
use std::str::FromStr;
8+
use url::Url;
9+
10+
use crate::store;
11+
12+
#[derive(CandidType, Deserialize, Serialize, Clone, Default)]
13+
pub struct HttpResponse {
14+
pub status_code: u16,
15+
pub headers: Vec<HeaderField>,
16+
pub body: ByteBufB64,
17+
pub upgrade: Option<bool>,
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+
static CBOR: &str = "application/cbor";
29+
static JSON: &str = "application/json";
30+
static IC_CERTIFICATE_HEADER: &str = "ic-certificate";
31+
static IC_CERTIFICATE_EXPRESSION_HEADER: &str = "ic-certificateexpression";
32+
33+
// request url example:
34+
// https://asxpf-ciaaa-aaaap-an33a-cai.icp0.io/delegation?pubkey=MCowBQYDK2VwAyEAV8Gp3Z2GnO3BQWpfxQD0KWD0GVm8Mk8f_B_hGLDHYFg
35+
// Response body example:
36+
// {
37+
// "result": "pGFhYGFkgqJhZKJhZRsYboTFK2eWmGFw..gteG"
38+
// }
39+
#[ic_cdk::query(hidden = true)]
40+
async fn http_request(request: HttpRequest<'static>) -> HttpResponse {
41+
if request.method().as_str() == "POST" {
42+
return HttpResponse {
43+
status_code: 200,
44+
headers: vec![],
45+
body: b"Upgrade".to_vec().into(),
46+
upgrade: Some(true),
47+
};
48+
}
49+
50+
let witness = store::state::http_tree_with(|t| {
51+
t.witness(&store::state::DEFAULT_CERT_ENTRY, request.url())
52+
.expect("get witness failed")
53+
});
54+
55+
let certified_data = ic_cdk::api::data_certificate().expect("no data certificate available");
56+
57+
let mut headers = vec![
58+
("x-content-type-options".to_string(), "nosniff".to_string()),
59+
(
60+
IC_CERTIFICATE_EXPRESSION_HEADER.to_string(),
61+
store::state::DEFAULT_CEL_EXPR.clone(),
62+
),
63+
(
64+
IC_CERTIFICATE_HEADER.to_string(),
65+
format!(
66+
"certificate=:{}:, tree=:{}:, expr_path=:{}:, version=2",
67+
BASE64.encode(certified_data),
68+
BASE64.encode(cbor_into_vec(&witness).expect("failed to serialize witness")),
69+
BASE64.encode(
70+
cbor_into_vec(&store::state::DEFAULT_EXPR_PATH.to_expr_path())
71+
.expect("failed to serialize expr path")
72+
)
73+
),
74+
),
75+
];
76+
77+
let req_url = match parse_url(request.url()) {
78+
Ok(url) => url,
79+
Err(err) => {
80+
headers.push(("content-type".to_string(), "text/plain".to_string()));
81+
return HttpResponse {
82+
status_code: 400,
83+
headers,
84+
body: err.into_bytes().into(),
85+
upgrade: None,
86+
};
87+
}
88+
};
89+
90+
let in_cbor = supports_cbor(request.headers());
91+
let origin = request
92+
.headers()
93+
.iter()
94+
.find(|(name, _)| name == "origin")
95+
.map(|(_, value)| value.clone());
96+
97+
let rt = match (request.method().as_str(), req_url.path()) {
98+
("HEAD", _) => Ok(Vec::new()),
99+
("GET", "/delegation") => get_delegation(req_url, origin, in_cbor),
100+
(method, path) => Err(format!("http_request, method {method}, path: {path}")),
101+
};
102+
103+
match rt {
104+
Ok(body) => {
105+
if in_cbor {
106+
headers.push(("content-type".to_string(), CBOR.to_string()));
107+
} else {
108+
headers.push(("content-type".to_string(), JSON.to_string()));
109+
}
110+
headers.push(("content-length".to_string(), body.len().to_string()));
111+
HttpResponse {
112+
status_code: 200,
113+
headers,
114+
body: body.into(),
115+
upgrade: None,
116+
}
117+
}
118+
Err(err) => {
119+
headers.push(("content-type".to_string(), "text/plain".to_string()));
120+
HttpResponse {
121+
status_code: 400,
122+
headers,
123+
body: err.into_bytes().into(),
124+
upgrade: None,
125+
}
126+
}
127+
}
128+
}
129+
130+
// request url example:
131+
// https://asxpf-ciaaa-aaaap-an33a-cai.icp0.io/delegation
132+
// Request body example:
133+
// {
134+
// "payload": "hMECAaEwggKtBgkq..rjv"
135+
// }
136+
// Response body example:
137+
// {
138+
// "result": "MCowBQYDK2VwAyEAV8Gp3Z2GnO3BQWpfxQD0KWD0GVm8Mk8f_B_hGLDHYFg="
139+
// }
140+
#[ic_cdk::update(hidden = true)]
141+
async fn http_request_update(request: HttpUpdateRequest<'static>) -> HttpResponse {
142+
let mut headers = vec![("x-content-type-options".to_string(), "nosniff".to_string())];
143+
144+
let req_url = match parse_url(request.url()) {
145+
Ok(url) => url,
146+
Err(err) => {
147+
return HttpResponse {
148+
status_code: 400,
149+
headers,
150+
body: err.into_bytes().into(),
151+
upgrade: None,
152+
};
153+
}
154+
};
155+
156+
let in_cbor = supports_cbor(request.headers());
157+
let origin = request
158+
.headers()
159+
.iter()
160+
.find(|(name, _)| name == "origin")
161+
.map(|(_, value)| value.clone())
162+
.unwrap_or_default();
163+
164+
let rt = match (request.method().as_str(), req_url.path()) {
165+
("POST", "/delegation") => put_delegation(request.body(), origin, in_cbor).await,
166+
(method, path) => Err(format!(
167+
"http_request_update, method {method}, path: {path}"
168+
)),
169+
};
170+
171+
match rt {
172+
Ok(body) => {
173+
if in_cbor {
174+
headers.push(("content-type".to_string(), CBOR.to_string()));
175+
} else {
176+
headers.push(("content-type".to_string(), JSON.to_string()));
177+
}
178+
headers.push(("content-length".to_string(), body.len().to_string()));
179+
HttpResponse {
180+
status_code: 200,
181+
headers,
182+
body: body.into(),
183+
upgrade: None,
184+
}
185+
}
186+
187+
Err(err) => {
188+
headers.push(("content-type".to_string(), "text/plain".to_string()));
189+
HttpResponse {
190+
status_code: 400,
191+
headers,
192+
body: err.into_bytes().into(),
193+
upgrade: None,
194+
}
195+
}
196+
}
197+
}
198+
199+
#[derive(Deserialize)]
200+
struct Request {
201+
payload: ByteBufB64,
202+
}
203+
204+
#[derive(Serialize)]
205+
struct Response {
206+
result: ByteBufB64,
207+
}
208+
209+
fn get_delegation(url: Url, origin: Option<String>, in_cbor: bool) -> Result<Vec<u8>, String> {
210+
if let Some((key, value)) = url.query_pairs().next() {
211+
match key.as_ref() {
212+
"pubkey" => {
213+
let pubkey = ByteBufB64::from_str(value.as_ref())
214+
.map_err(|err| format!("invalid pubkey: {value}, error: {err}"))?;
215+
if pubkey.len() > 48 {
216+
return Err(format!("pubkey too long: {}", pubkey.len()));
217+
}
218+
219+
let result = store::state::get_delegation(pubkey, origin)
220+
.ok_or_else(|| format!("no delegation found for pubkey: {}", value))?;
221+
if in_cbor {
222+
return cbor_into_vec(&Response { result })
223+
.map_err(|err| format!("failed to serialize agent in CBOR, error: {err}"));
224+
} else {
225+
return serde_json::to_vec(&Response { result })
226+
.map_err(|err| format!("failed to serialize agent in JSON, error: {err}"));
227+
}
228+
}
229+
other => {
230+
Err(format!("invalid query parameter: {other}={value}"))?;
231+
}
232+
}
233+
}
234+
235+
Err("missing query parameter".to_string())
236+
}
237+
238+
async fn put_delegation(body: &[u8], origin: String, in_cbor: bool) -> Result<Vec<u8>, String> {
239+
let req: Request = if in_cbor {
240+
from_reader(body)
241+
.map_err(|err| format!("failed to decode Request from CBOR, error: {err}"))?
242+
} else {
243+
serde_json::from_slice(body)
244+
.map_err(|err| format!("failed to decode Request from JSON, error: {err}"))?
245+
};
246+
247+
let result = store::state::put_delegation(req.payload, origin)?;
248+
if in_cbor {
249+
cbor_into_vec(&Response { result })
250+
.map_err(|err| format!("failed to serialize agent in CBOR, error: {err}"))
251+
} else {
252+
serde_json::to_vec(&Response { result })
253+
.map_err(|err| format!("failed to serialize agent in JSON, error: {err}"))
254+
}
255+
}
256+
257+
fn parse_url(s: &str) -> Result<Url, String> {
258+
let url = if s.starts_with('/') {
259+
Url::parse(format!("http://localhost{}", s).as_str())
260+
} else {
261+
Url::parse(s)
262+
};
263+
url.map_err(|err| format!("failed to parse url {s}, error: {err}"))
264+
}
265+
266+
fn supports_cbor(headers: &[HeaderField]) -> bool {
267+
headers
268+
.iter()
269+
.any(|(name, value)| (name == "accept" || name == "content-type") && value.contains(CBOR))
270+
}

0 commit comments

Comments
 (0)