Skip to content

Commit 50bb4d7

Browse files
nikolay-komarevskiysa-idx-adminr-birkner
authored
chore(boundary): salt_sharing canister implementation (dfinity#3650)
Provide all API boundary nodes with the same secret salt to anonymize the IP addresses and the sender principals when logging the incoming requests. --------- Co-authored-by: IDX GitLab Automation <[email protected]> Co-authored-by: r-birkner <[email protected]>
1 parent 5703c43 commit 50bb4d7

File tree

12 files changed

+471
-10
lines changed

12 files changed

+471
-10
lines changed

Cargo.lock

Lines changed: 26 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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ members = [
3737
"rs/boundary_node/rate_limits",
3838
"rs/boundary_node/rate_limits/api",
3939
"rs/boundary_node/rate_limits/canister_client",
40+
"rs/boundary_node/salt_sharing",
41+
"rs/boundary_node/salt_sharing/api",
4042
"rs/boundary_node/systemd_journal_gatewayd_shim",
4143
"rs/canister_client",
4244
"rs/canister_client/read_state_response_parser",
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[package]
2+
name = "salt"
3+
version.workspace = true
4+
authors.workspace = true
5+
edition.workspace = true
6+
description.workspace = true
7+
documentation.workspace = true
8+
9+
[dependencies]
10+
candid = { workspace = true }
11+
ic-canisters-http-types = { path = "../../rust_canisters/http_types" }
12+
ic-canister-log = { path = "../../rust_canisters/canister_log" }
13+
ic-cdk = { workspace = true }
14+
ic-cdk-macros = { workspace = true }
15+
ic-cdk-timers = { workspace = true }
16+
ic-stable-structures = { workspace = true }
17+
salt-api = { path = "./api" }
18+
serde = { workspace = true }
19+
serde_cbor = { workspace = true }
20+
serde_json = { workspace = true }
21+
time = { workspace = true }
22+
23+
[dev-dependencies]
24+
25+
[lib]
26+
crate-type = ["cdylib"]
27+
path = "canister/lib.rs"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "salt-api"
3+
version.workspace = true
4+
authors.workspace = true
5+
edition.workspace = true
6+
description.workspace = true
7+
documentation.workspace = true
8+
9+
[dependencies]
10+
candid = { workspace = true }
11+
serde = { workspace = true }
12+
13+
[dev-dependencies]
14+
15+
[lib]
16+
path = "src/lib.rs"
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
use candid::{CandidType, Deserialize};
2+
3+
pub type GetSaltResponse = Result<SaltResponse, GetSaltError>;
4+
5+
#[derive(CandidType, Deserialize, Debug, Clone)]
6+
pub enum SaltGenerationStrategy {
7+
StartOfMonth,
8+
}
9+
10+
#[derive(CandidType, Deserialize, Debug, Clone)]
11+
pub struct InitArg {
12+
pub regenerate_now: bool,
13+
pub salt_generation_strategy: SaltGenerationStrategy,
14+
pub registry_polling_interval_secs: u64,
15+
}
16+
17+
#[derive(CandidType, Deserialize, Debug, Clone)]
18+
pub struct SaltResponse {
19+
pub salt: Vec<u8>,
20+
pub salt_id: u64,
21+
}
22+
23+
#[derive(CandidType, Deserialize, Debug, Clone)]
24+
pub enum GetSaltError {
25+
SaltNotInitialized,
26+
Unauthorized,
27+
Internal(String),
28+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
use std::time::Duration;
2+
3+
use crate::logs::{self, Log, LogEntry, Priority, P0};
4+
use crate::storage::{StorableSalt, SALT, SALT_SIZE};
5+
use crate::time::delay_till_next_month;
6+
use candid::Principal;
7+
use ic_canister_log::{export as export_logs, log};
8+
use ic_canisters_http_types::{HttpRequest, HttpResponse, HttpResponseBuilder};
9+
use ic_cdk::{api::time, spawn};
10+
use ic_cdk_macros::{init, post_upgrade, query};
11+
use ic_cdk_timers::set_timer;
12+
use salt_api::{GetSaltError, GetSaltResponse, InitArg, SaltGenerationStrategy, SaltResponse};
13+
use std::str::FromStr;
14+
15+
// Runs when canister is first installed
16+
#[init]
17+
fn init(init_arg: InitArg) {
18+
set_timer(Duration::ZERO, || {
19+
spawn(async { init_async(init_arg).await });
20+
});
21+
}
22+
23+
// Runs on every canister upgrade
24+
#[post_upgrade]
25+
fn post_upgrade(init_arg: InitArg) {
26+
// Run the same initialization logic
27+
init(init_arg);
28+
}
29+
30+
#[query]
31+
fn get_salt() -> GetSaltResponse {
32+
get_salt_response()
33+
}
34+
35+
#[query(decoding_quota = 10000)]
36+
fn http_request(request: HttpRequest) -> HttpResponse {
37+
match request.path() {
38+
"/logs" => {
39+
use serde_json;
40+
41+
let max_skip_timestamp = match request.raw_query_param("time") {
42+
Some(arg) => match u64::from_str(arg) {
43+
Ok(value) => value,
44+
Err(_) => {
45+
return HttpResponseBuilder::bad_request()
46+
.with_body_and_content_length("failed to parse the 'time' parameter")
47+
.build()
48+
}
49+
},
50+
None => 0,
51+
};
52+
53+
let mut entries: Log = Default::default();
54+
55+
for entry in export_logs(&logs::P0) {
56+
entries.entries.push(LogEntry {
57+
timestamp: entry.timestamp,
58+
counter: entry.counter,
59+
priority: Priority::P0,
60+
file: entry.file.to_string(),
61+
line: entry.line,
62+
message: entry.message,
63+
});
64+
}
65+
66+
for entry in export_logs(&logs::P1) {
67+
entries.entries.push(LogEntry {
68+
timestamp: entry.timestamp,
69+
counter: entry.counter,
70+
priority: Priority::P1,
71+
file: entry.file.to_string(),
72+
line: entry.line,
73+
message: entry.message,
74+
});
75+
}
76+
77+
entries
78+
.entries
79+
.retain(|entry| entry.timestamp >= max_skip_timestamp);
80+
81+
HttpResponseBuilder::ok()
82+
.header("Content-Type", "application/json; charset=utf-8")
83+
.with_body_and_content_length(serde_json::to_string(&entries).unwrap_or_default())
84+
.build()
85+
}
86+
_ => HttpResponseBuilder::not_found().build(),
87+
}
88+
}
89+
90+
async fn init_async(init_arg: InitArg) {
91+
if !is_salt_init() || init_arg.regenerate_now {
92+
if let Err(err) = try_regenerate_salt().await {
93+
log!(P0, "[init_regenerate_salt_failed]: {err}");
94+
}
95+
}
96+
// Start salt generation schedule based on the argument.
97+
match init_arg.salt_generation_strategy {
98+
SaltGenerationStrategy::StartOfMonth => schedule_monthly_salt_generation(),
99+
}
100+
}
101+
102+
// Sets an execution timer (delayed future task) and returns immediately.
103+
fn schedule_monthly_salt_generation() {
104+
let delay = delay_till_next_month(time());
105+
set_timer(delay, || {
106+
spawn(async {
107+
if let Err(err) = try_regenerate_salt().await {
108+
log!(P0, "[scheduled_regenerate_salt_failed]: {err}");
109+
}
110+
// Function is called recursively to schedule next execution
111+
schedule_monthly_salt_generation();
112+
});
113+
});
114+
}
115+
116+
fn is_salt_init() -> bool {
117+
SALT.with(|cell| cell.borrow().get(&())).is_some()
118+
}
119+
120+
fn get_salt_response() -> Result<SaltResponse, GetSaltError> {
121+
let stored_salt = SALT
122+
.with(|cell| cell.borrow().get(&()))
123+
.ok_or(GetSaltError::SaltNotInitialized)?;
124+
125+
Ok(SaltResponse {
126+
salt: stored_salt.salt,
127+
salt_id: stored_salt.salt_id,
128+
})
129+
}
130+
131+
// Regenerate salt and store it in the stable memory
132+
// Can only fail, if the calls to management canister fail.
133+
async fn try_regenerate_salt() -> Result<(), String> {
134+
// Closure for getting random bytes from the IC.
135+
let rnd_call = |attempt: u32| async move {
136+
ic_cdk::call(Principal::management_canister(), "raw_rand", ())
137+
.await
138+
.map_err(|err| {
139+
format!(
140+
"Call {attempt} to raw_rand failed: code={:?}, err={}",
141+
err.0, err.1
142+
)
143+
})
144+
};
145+
146+
let (rnd_bytes_1,): ([u8; 32],) = rnd_call(1).await?;
147+
let (rnd_bytes_2,): ([u8; 32],) = rnd_call(2).await?;
148+
149+
// Concatenate arrays to form an array of 64 random bytes.
150+
let mut salt = [rnd_bytes_1, rnd_bytes_2].concat();
151+
salt.truncate(SALT_SIZE);
152+
153+
let stored_salt = StorableSalt {
154+
salt,
155+
salt_id: time(),
156+
};
157+
158+
SALT.with(|cell| {
159+
cell.borrow_mut().insert((), stored_salt);
160+
});
161+
162+
Ok(())
163+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#[cfg(any(target_family = "wasm", test))]
2+
mod canister;
3+
mod logs;
4+
#[allow(dead_code)]
5+
mod storage;
6+
#[allow(dead_code)]
7+
mod time;
8+
9+
#[allow(dead_code)]
10+
fn main() {}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use candid::Deserialize;
2+
use ic_canister_log::declare_log_buffer;
3+
4+
// High-priority messages.
5+
declare_log_buffer!(name = P0, capacity = 1000);
6+
7+
// Low-priority info messages.
8+
declare_log_buffer!(name = P1, capacity = 1000);
9+
10+
#[derive(Clone, Debug, Default, Deserialize, serde::Serialize)]
11+
pub struct Log {
12+
pub entries: Vec<LogEntry>,
13+
}
14+
15+
#[derive(Clone, Debug, Deserialize, serde::Serialize)]
16+
pub struct LogEntry {
17+
pub timestamp: u64,
18+
pub priority: Priority,
19+
pub file: String,
20+
pub line: u32,
21+
pub message: String,
22+
pub counter: u64,
23+
}
24+
25+
#[derive(Clone, Debug, Deserialize, serde::Serialize)]
26+
pub enum Priority {
27+
P0,
28+
P1,
29+
}

rs/boundary_node/salt_sharing/canister/salt_sharing_canister.did

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ type GetSaltResponse = variant {
1111

1212
// Comprehensive error for salt retrieval
1313
type GetSaltError = variant {
14+
// Salt generation is still pending. Retry later
15+
SaltNotInitialized;
1416
// Indicates an unauthorized attempt to get the salt
1517
Unauthorized;
1618
// Captures all unexpected internal errors during process
17-
Internal: text;
19+
Internal: text;
1820
};
1921

2022
// Salt response containing salt itself and additional metadata
@@ -41,20 +43,14 @@ type SaltGenerationStrategy = variant {
4143
// Generates a new salt at 00:00:00 UTC on the first day of the next calendar month
4244
// Handles calendar edge cases including: transitions between months (December-January), leap years
4345
StartOfMonth;
44-
// Generates a new salt at fixed intervals from an unspecified reference point
45-
FixedIntervalSecs: nat64;
4646
};
4747

4848
// Initialization arguments used when installing/upgrading/reinstalling the canister
4949
type InitArgs = record {
50+
// If true salt is regenerated immediately and subsequently based on the chosen strategy
51+
regenerate_now: bool;
5052
// Strategy defining salt generation
51-
// If specified:
52-
// - salt is regenerated immediately
53-
// - subsequent strategy is based on the variant
54-
// If not specified:
55-
// - preserve previously configured strategy
56-
// - no immediate salt regeneration
57-
salt_generation_strategy: opt SaltGenerationStrategy;
53+
salt_generation_strategy: SaltGenerationStrategy;
5854
// Interval (in seconds) for polling API boundary node IDs from the registry
5955
// The first polling operation occurs immediately
6056
registry_polling_interval_secs: nat64;

0 commit comments

Comments
 (0)