diff --git a/Cargo.Bazel.lock b/Cargo.Bazel.lock index 6d7158858..6ffea97fd 100644 --- a/Cargo.Bazel.lock +++ b/Cargo.Bazel.lock @@ -1,5 +1,5 @@ { - "checksum": "ac1c7f35e7cd235485ebc4e9085ef127ba2881abba9deee4940ac3a40ac97b9a", + "checksum": "c442757a3917dcf43241853bc5a2e77532c6d00192aac0abd0612f2d00c993eb", "crates": { "actix-codec 0.5.2": { "name": "actix-codec", @@ -10317,6 +10317,14 @@ { "id": "actix-rt 2.10.0", "target": "actix_rt" + }, + { + "id": "hex 0.4.3", + "target": "hex" + }, + { + "id": "openssl 0.10.66", + "target": "openssl" } ], "selects": {} @@ -11919,6 +11927,75 @@ }, "license": "Apache-2.0 / MIT" }, + "foreign-types 0.3.2": { + "name": "foreign-types", + "version": "0.3.2", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/foreign-types/0.3.2/download", + "sha256": "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" + } + }, + "targets": [ + { + "Library": { + "crate_name": "foreign_types", + "crate_root": "src/lib.rs", + "srcs": [ + "**/*.rs" + ] + } + } + ], + "library_target_name": "foreign_types", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "foreign-types-shared 0.1.1", + "target": "foreign_types_shared" + } + ], + "selects": {} + }, + "edition": "2015", + "version": "0.3.2" + }, + "license": "MIT/Apache-2.0" + }, + "foreign-types-shared 0.1.1": { + "name": "foreign-types-shared", + "version": "0.1.1", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/foreign-types-shared/0.1.1/download", + "sha256": "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + } + }, + "targets": [ + { + "Library": { + "crate_name": "foreign_types_shared", + "crate_root": "src/lib.rs", + "srcs": [ + "**/*.rs" + ] + } + } + ], + "library_target_name": "foreign_types_shared", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "edition": "2015", + "version": "0.1.1" + }, + "license": "MIT/Apache-2.0" + }, "form_urlencoded 1.2.1": { "name": "form_urlencoded", "version": "1.2.1", @@ -30994,6 +31071,156 @@ }, "license": "MIT OR Apache-2.0" }, + "openssl 0.10.66": { + "name": "openssl", + "version": "0.10.66", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/openssl/0.10.66/download", + "sha256": "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" + } + }, + "targets": [ + { + "Library": { + "crate_name": "openssl", + "crate_root": "src/lib.rs", + "srcs": [ + "**/*.rs" + ] + } + }, + { + "BuildScript": { + "crate_name": "build_script_build", + "crate_root": "build.rs", + "srcs": [ + "**/*.rs" + ] + } + } + ], + "library_target_name": "openssl", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "default" + ], + "selects": {} + }, + "deps": { + "common": [ + { + "id": "bitflags 2.6.0", + "target": "bitflags" + }, + { + "id": "cfg-if 1.0.0", + "target": "cfg_if" + }, + { + "id": "foreign-types 0.3.2", + "target": "foreign_types" + }, + { + "id": "libc 0.2.157", + "target": "libc" + }, + { + "id": "once_cell 1.19.0", + "target": "once_cell" + }, + { + "id": "openssl 0.10.66", + "target": "build_script_build" + }, + { + "id": "openssl-sys 0.9.103", + "target": "openssl_sys", + "alias": "ffi" + } + ], + "selects": {} + }, + "edition": "2018", + "proc_macro_deps": { + "common": [ + { + "id": "openssl-macros 0.1.1", + "target": "openssl_macros" + } + ], + "selects": {} + }, + "version": "0.10.66" + }, + "build_script_attrs": { + "data_glob": [ + "**" + ], + "link_deps": { + "common": [ + { + "id": "openssl-sys 0.9.103", + "target": "openssl_sys", + "alias": "ffi" + } + ], + "selects": {} + } + }, + "license": "Apache-2.0" + }, + "openssl-macros 0.1.1": { + "name": "openssl-macros", + "version": "0.1.1", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/openssl-macros/0.1.1/download", + "sha256": "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" + } + }, + "targets": [ + { + "ProcMacro": { + "crate_name": "openssl_macros", + "crate_root": "src/lib.rs", + "srcs": [ + "**/*.rs" + ] + } + } + ], + "library_target_name": "openssl_macros", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "proc-macro2 1.0.86", + "target": "proc_macro2" + }, + { + "id": "quote 1.0.37", + "target": "quote" + }, + { + "id": "syn 2.0.76", + "target": "syn" + } + ], + "selects": {} + }, + "edition": "2018", + "version": "0.1.1" + }, + "license": "MIT/Apache-2.0" + }, "openssl-probe 0.1.5": { "name": "openssl-probe", "version": "0.1.5", @@ -31024,6 +31251,81 @@ }, "license": "MIT/Apache-2.0" }, + "openssl-sys 0.9.103": { + "name": "openssl-sys", + "version": "0.9.103", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/openssl-sys/0.9.103/download", + "sha256": "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" + } + }, + "targets": [ + { + "Library": { + "crate_name": "openssl_sys", + "crate_root": "src/lib.rs", + "srcs": [ + "**/*.rs" + ] + } + }, + { + "BuildScript": { + "crate_name": "build_script_main", + "crate_root": "build/main.rs", + "srcs": [ + "**/*.rs" + ] + } + } + ], + "library_target_name": "openssl_sys", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "libc 0.2.157", + "target": "libc" + }, + { + "id": "openssl-sys 0.9.103", + "target": "build_script_main" + } + ], + "selects": {} + }, + "edition": "2018", + "version": "0.9.103" + }, + "build_script_attrs": { + "data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "cc 1.1.13", + "target": "cc" + }, + { + "id": "pkg-config 0.3.30", + "target": "pkg_config" + }, + { + "id": "vcpkg 0.2.15", + "target": "vcpkg" + } + ], + "selects": {} + }, + "links": "openssl" + }, + "license": "MIT" + }, "opentelemetry 0.22.0": { "name": "opentelemetry", "version": "0.22.0", @@ -44808,6 +45110,36 @@ }, "license": "MIT" }, + "vcpkg 0.2.15": { + "name": "vcpkg", + "version": "0.2.15", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/vcpkg/0.2.15/download", + "sha256": "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + } + }, + "targets": [ + { + "Library": { + "crate_name": "vcpkg", + "crate_root": "src/lib.rs", + "srcs": [ + "**/*.rs" + ] + } + } + ], + "library_target_name": "vcpkg", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "edition": "2015", + "version": "0.2.15" + }, + "license": "MIT/Apache-2.0" + }, "version_check 0.9.5": { "name": "version_check", "version": "0.9.5", diff --git a/Cargo.lock b/Cargo.lock index dc573fea0..a2456d5cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2009,6 +2009,7 @@ dependencies = [ "fs-err", "futures", "futures-util", + "hex", "human_bytes", "humantime", "ic-base-types", @@ -2035,6 +2036,7 @@ dependencies = [ "keyring", "log", "mockall", + "openssl", "pretty_env_logger", "prost", "regex", @@ -2356,6 +2358,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -6231,12 +6248,50 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.76", +] + [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "opentelemetry" version = "0.22.0" @@ -8919,6 +8974,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index 021bc34b4..459b103b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -192,6 +192,7 @@ url = "2.5.2" wiremock = "0.6.1" human_bytes = "0.4" mockall = "0.13.0" +openssl = "0.10.66" # dre-canisters dependencies ic-cdk-timers = { git = "https://github.com/dfinity/cdk-rs.git", rev = "3e54016734c8f73fb3acabc9c9e72c960c85eb3b" } diff --git a/deny.toml b/deny.toml index 666eedd28..9d5ac7b7f 100644 --- a/deny.toml +++ b/deny.toml @@ -14,6 +14,7 @@ # The graph table configures how the dependency graph is constructed and thus # which crates the checks are performed against [graph] +exclude-dev = true # If 1 or more target triples (and optionally, target_features) are specified, # only the specified targets will be checked when running `cargo deny check`. # This means, if a particular package is only ever used as a target specific diff --git a/rs/cli/Cargo.toml b/rs/cli/Cargo.toml index 8bad18b74..f41a6f0da 100644 --- a/rs/cli/Cargo.toml +++ b/rs/cli/Cargo.toml @@ -80,6 +80,8 @@ clio = { workspace = true } [dev-dependencies] actix-rt = { workspace = true } +hex.workspace = true +openssl.workspace = true [build-dependencies] clap = { workspace = true } diff --git a/rs/cli/src/auth.rs b/rs/cli/src/auth.rs index 6dfe1fbc4..2b623c5ef 100644 --- a/rs/cli/src/auth.rs +++ b/rs/cli/src/auth.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use std::str::FromStr; use anyhow::anyhow; -use clio::{ClioPath, InputPath}; +use clio::InputPath; use cryptoki::object::AttributeInfo; use cryptoki::session::Session; use cryptoki::{ @@ -22,7 +22,7 @@ use std::sync::Mutex; use crate::commands::{AuthOpts, AuthRequirement, HsmOpts, HsmParams}; -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Neuron { pub auth: Auth, pub neuron_id: u64, @@ -43,7 +43,7 @@ pub fn hsm_key_id_to_string(s: u8) -> String { } impl Neuron { - pub(crate) async fn from_opts_and_req( + pub async fn from_opts_and_req( auth_opts: AuthOpts, requirement: AuthRequirement, network: &Network, @@ -60,16 +60,7 @@ impl Neuron { None => ( Some(STAGING_NEURON_ID), match Auth::pem(staging_known_path.clone()).await { - Ok(_) => AuthOpts { - private_key_pem: Some(InputPath::new(ClioPath::new(staging_known_path).unwrap()).unwrap()), - hsm_opts: HsmOpts { - hsm_pin: None, - hsm_params: HsmParams { - hsm_slot: None, - hsm_key_id: None, - }, - }, - }, + Ok(_) => AuthOpts::try_from(staging_known_path)?, Err(e) => match requirement { // If there is an error but auth is not needed // just send what we have since it won't be @@ -118,20 +109,6 @@ impl Neuron { } } - #[allow(dead_code)] - pub async fn new(auth: Auth, neuron_id: Option, network: &Network, include_proposer: bool) -> anyhow::Result { - let neuron_id = match neuron_id { - Some(n) => n, - None => auth.auto_detect_neuron_id(network.get_nns_urls().to_vec()).await?, - }; - debug!("Identifying as neuron ID {}", neuron_id); - Ok(Self { - auth, - neuron_id, - include_proposer, - }) - } - pub fn as_arg_vec(&self) -> Vec { self.auth.as_arg_vec() } @@ -153,9 +130,9 @@ impl Neuron { } } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Auth { - Hsm { pin: String, slot: u64, key_id: u8 }, + Hsm { pin: String, slot: u64, key_id: u8, so_path: PathBuf }, Keyfile { path: PathBuf }, Anonymous, } @@ -163,7 +140,12 @@ pub enum Auth { impl Auth { pub fn as_arg_vec(&self) -> Vec { match self { - Auth::Hsm { pin, slot, key_id } => vec![ + Auth::Hsm { + pin, + slot, + key_id, + so_path: _, + } => vec![ "--use-hsm".to_string(), "--pin".to_string(), pin.clone(), @@ -193,7 +175,9 @@ impl Auth { // FIXME: why do we even take multiple URLs if only the first one is ever used? let url = nns_urls.first().ok_or(anyhow::anyhow!("No NNS URLs provided"))?.to_owned(); match self { - Auth::Hsm { pin, slot, key_id } => IcAgentCanisterClient::from_hsm(pin.clone(), *slot, hsm_key_id_to_string(*key_id), url, lock), + Auth::Hsm { pin, slot, key_id, so_path } => { + IcAgentCanisterClient::from_hsm(pin.clone(), *slot, hsm_key_id_to_string(*key_id), url, lock, so_path.to_path_buf()) + } Auth::Keyfile { path } => IcAgentCanisterClient::from_key_file(path.clone(), url), Auth::Anonymous => IcAgentCanisterClient::from_anonymous(url), } @@ -221,8 +205,23 @@ impl Auth { } } + #[cfg(not(test))] + fn slot_description() -> &'static str { + "Nitrokey Nitrokey HSM" + } + + #[cfg(test)] + fn slot_description() -> &'static str { + "SoftHSM" + } + /// If it is called it is expected to retrieve an Auth of type Hsm or fail - fn detect_hsm_auth(maybe_pin: Option, maybe_slot: Option, maybe_key_id: Option) -> anyhow::Result { + fn detect_hsm_auth( + maybe_pin: Option, + maybe_slot: Option, + maybe_key_id: Option, + hsm_so_module: Option, + ) -> anyhow::Result { if maybe_slot.is_none() && maybe_key_id.is_none() { debug!("Scanning hardware security module devices"); } @@ -232,13 +231,17 @@ impl Auth { if let Some(key_id) = &maybe_key_id { debug!("Limiting key scan to keys with ID {}", key_id); } + let so_path = match hsm_so_module { + Some(p) => p, + None => Self::pkcs11_lib_path()?, + }; - let ctx = Pkcs11::new(Self::pkcs11_lib_path()?)?; + let ctx = Pkcs11::new(&so_path)?; ctx.initialize(CInitializeArgs::OsThreads)?; for slot in ctx.get_slots_with_token()? { let info = ctx.get_slot_info(slot)?; let token_info = ctx.get_token_info(slot)?; - if info.slot_description().starts_with("Nitrokey Nitrokey HSM") && maybe_slot.is_none() || (maybe_slot.unwrap() == slot.id()) { + if info.slot_description().starts_with("SoftHSM") && maybe_slot.is_none() || (maybe_slot.unwrap() == slot.id()) { let session = ctx.open_ro_session(slot)?; let key_id = match Auth::find_key_id_in_slot_session(&session, maybe_key_id)? { Some((key_id, label)) => { @@ -274,6 +277,7 @@ impl Auth { pin, slot: slot.id(), key_id, + so_path, }; info!("Using key ID {} of hardware security module in slot {}", key_id, slot); return Ok(detected); @@ -386,8 +390,13 @@ impl Auth { /// anonymous authentication if no HSM is detected. Prompts the user /// for a PIN if no PIN is specified and the HSM needs to be unlocked. /// Caller can optionally limit search to a specific slot or key ID. - pub async fn auto(hsm_pin: Option, hsm_slot: Option, hsm_key_id: Option) -> anyhow::Result { - tokio::task::spawn_blocking(move || Self::detect_hsm_auth(hsm_pin, hsm_slot, hsm_key_id)).await? + pub async fn auto( + hsm_pin: Option, + hsm_slot: Option, + hsm_key_id: Option, + hsm_so_module: Option, + ) -> anyhow::Result { + tokio::task::spawn_blocking(move || Self::detect_hsm_auth(hsm_pin, hsm_slot, hsm_key_id, hsm_so_module)).await? } pub async fn pem(private_key_pem: PathBuf) -> anyhow::Result { @@ -420,9 +429,10 @@ impl Auth { hsm_opts: HsmOpts { hsm_pin: pin, + hsm_so_module, hsm_params: HsmParams { hsm_slot, hsm_key_id }, }, - } => Auth::auto(pin.clone(), *hsm_slot, *hsm_key_id).await, + } => Auth::auto(pin.clone(), *hsm_slot, *hsm_key_id, hsm_so_module.clone()).await, } } } diff --git a/rs/cli/src/commands/mod.rs b/rs/cli/src/commands/mod.rs index 5729e64fe..cad726500 100644 --- a/rs/cli/src/commands/mod.rs +++ b/rs/cli/src/commands/mod.rs @@ -82,6 +82,11 @@ pub(crate) struct HsmOpts { env = "HSM_PIN" )] pub(crate) hsm_pin: Option, + + /// SO module for the HSM, usually should be left empty, but can be overriden for testing + #[clap(required = false, conflicts_with = "private_key_pem", long, global = true, env = "HSM_SO_MODULE")] + pub(crate) hsm_so_module: Option, + #[clap(flatten)] pub(crate) hsm_params: HsmParams, } @@ -103,13 +108,30 @@ pub struct AuthOpts { long, required = false, global = true, - conflicts_with_all = ["hsm_pin", "hsm_slot", "hsm_key_id"], + conflicts_with_all = ["hsm_pin", "hsm_slot", "hsm_key_id", "hsm_so_module"], env = "PRIVATE_KEY_PEM")] pub(crate) private_key_pem: Option, #[clap(flatten)] pub(crate) hsm_opts: HsmOpts, } +#[allow(dead_code)] +impl AuthOpts { + pub fn none() -> Self { + Self { + private_key_pem: None, + hsm_opts: HsmOpts { + hsm_pin: None, + hsm_so_module: None, + hsm_params: HsmParams { + hsm_slot: None, + hsm_key_id: None, + }, + }, + } + } +} + impl TryFrom for AuthOpts { type Error = anyhow::Error; @@ -118,6 +140,7 @@ impl TryFrom for AuthOpts { private_key_pem: Some(InputPath::new(ClioPath::new(value)?)?), hsm_opts: HsmOpts { hsm_pin: None, + hsm_so_module: None, hsm_params: HsmParams { hsm_slot: None, hsm_key_id: None, diff --git a/rs/cli/src/main.rs b/rs/cli/src/main.rs index 49e89d798..0a8ee569f 100644 --- a/rs/cli/src/main.rs +++ b/rs/cli/src/main.rs @@ -18,7 +18,7 @@ mod qualification; mod runner; mod subnet_manager; #[cfg(test)] -mod unit_tests; +mod tests; #[tokio::main] async fn main() -> anyhow::Result<()> { diff --git a/rs/cli/src/unit_tests/ctx_init.rs b/rs/cli/src/tests/ctx_init.rs similarity index 53% rename from rs/cli/src/unit_tests/ctx_init.rs rename to rs/cli/src/tests/ctx_init.rs index 825607e88..5dfef15c6 100644 --- a/rs/cli/src/unit_tests/ctx_init.rs +++ b/rs/cli/src/tests/ctx_init.rs @@ -1,10 +1,11 @@ -use std::{path::PathBuf, str::FromStr}; +use std::{io::Write, path::PathBuf, process::Command, str::FromStr}; use crate::{ auth::{Auth, Neuron, STAGING_KEY_PATH_FROM_HOME, STAGING_NEURON_ID}, commands::{AuthOpts, AuthRequirement, HsmOpts}, }; use clio::{ClioPath, InputPath}; +use hex; use ic_canisters::governance::governance_canister_version; use ic_management_types::Network; use itertools::Itertools; @@ -35,6 +36,7 @@ async fn get_context(network: &Network, version: IcAdminVersion) -> anyhow::Resu hsm_slot: None, hsm_key_id: None, }, + hsm_so_module: None, }, }, None, @@ -187,9 +189,6 @@ struct NeuronAuthTestScenarion<'a> { name: &'a str, neuron_id: Option, private_key_pem: Option, - hsm_pin: Option, - hsm_key_id: Option, - hsm_slot: Option, requirement: AuthRequirement, network: String, want: anyhow::Result, @@ -203,9 +202,6 @@ impl<'a> NeuronAuthTestScenarion<'a> { name, neuron_id: None, private_key_pem: None, - hsm_pin: None, - hsm_key_id: None, - hsm_slot: None, requirement: AuthRequirement::Anonymous, network: "".to_string(), want: Ok(Neuron::anonymous_neuron()), @@ -226,27 +222,6 @@ impl<'a> NeuronAuthTestScenarion<'a> { } } - fn with_pin(self, hsm_pin: &'a str) -> Self { - Self { - hsm_pin: Some(hsm_pin.to_string()), - ..self - } - } - - fn with_key_id(self, hsm_key_id: u8) -> Self { - Self { - hsm_key_id: Some(hsm_key_id), - ..self - } - } - - fn with_slot(self, hsm_slot: u64) -> Self { - Self { - hsm_slot: Some(hsm_slot), - ..self - } - } - fn when_requirement(self, auth: AuthRequirement) -> Self { Self { requirement: auth, ..self } } @@ -270,11 +245,12 @@ impl<'a> NeuronAuthTestScenarion<'a> { .as_ref() .map(|path| InputPath::new(ClioPath::new(path).unwrap()).unwrap()), hsm_opts: HsmOpts { - hsm_pin: self.hsm_pin.clone(), + hsm_pin: None, hsm_params: crate::commands::HsmParams { - hsm_slot: self.hsm_slot, - hsm_key_id: self.hsm_key_id, + hsm_slot: None, + hsm_key_id: None, }, + hsm_so_module: None, }, }, self.neuron_id, @@ -375,3 +351,293 @@ async fn init_test_neuron_and_auth() { .join("\n\n") ) } + +fn ensure_slot_exists(slot: u64, label: &str, pin: &str) -> u64 { + // If run multiple times for an existing slot it will fail but we don't care + // since its a test + let output = Command::new("softhsm2-util") + .arg("--init-token") + .arg("--slot") + .arg(slot.to_string()) + .arg("--label") + .arg(label) + .arg("--so-pin") + .arg(pin) + .arg("--pin") + .arg(pin) + .output() + .unwrap(); + if !output.status.success() { + panic!("Failed to create token: {}", String::from_utf8_lossy(&output.stderr)) + } + let slot = String::from_utf8_lossy(&output.stdout); + slot.trim().split(' ').last().unwrap().parse().unwrap() +} + +fn delete_test_slot(slot: u64, label: &str) { + // Cleanup. Again we don't care if it fails or not + Command::new("softhsm2-util") + .arg("--delete-token") + .arg(slot.to_string()) + .arg("--token") + .arg(label) + .output() + .unwrap(); +} + +fn generate_password_for_test_hsm(pin: &str, key_id: u8, label: &str) { + Command::new("pkcs11-tool") + .arg("--module") + .arg(get_softhsm2_module()) + .arg("-l") + .arg("-p") + .arg(pin) + .arg("-k") + .arg("--id") + .arg(key_id.to_string()) + .arg("--label") + .arg(label) + .arg("--key-type") + .arg("EC:prime256v1") + .output() + .unwrap(); +} + +fn import_pem_as_pkey(pin: &str, key_id: u8, label: &str, module: &PathBuf) { + // Convert the staging .pem file to have only private key + let pem = std::fs::read_to_string(get_staging_key_path()).unwrap(); + let pem_lines = pem.lines().collect_vec(); + let pem_without_markings = pem_lines[1..pem_lines.len() - 1].join(""); + println!("PEM: {}", pem_without_markings); + let hex = hex::encode(&openssl::base64::decode_block(&pem_without_markings).unwrap()); + let sub_hex_data = &hex[10..96]; + let final_hex_string = format!("302E020100{}", sub_hex_data); + let binary_data = hex::decode(final_hex_string).unwrap(); + let only_priv = get_staging_key_path().parent().unwrap().join("identity-only-priv-key.pem"); + let pkey = openssl::pkey::PKey::private_key_from_der(&binary_data).unwrap(); + let mut file = std::fs::File::create(&only_priv).unwrap(); + file.write_all(&pkey.private_key_to_pem_pkcs8().unwrap()).unwrap(); + + let output = Command::new("pkcs11-tool") + .arg("--module") + .arg(module.display().to_string()) + .arg("-l") + .arg("-p") + .arg(pin) + .arg("--write-object") + .arg(&only_priv.display().to_string()) + .arg("--type") + .arg("privkey") + .arg("--id") + .arg(key_id.to_string()) + .arg("--label") + .arg(label) + .output() + .unwrap(); + + if !output.status.success() { + panic!("Failed to create token: {}", String::from_utf8_lossy(&output.stderr)) + } +} + +fn get_softhsm2_module() -> PathBuf { + PathBuf::from_str(&std::env::var("SOFTHSM2_MODULE").unwrap_or("/usr/lib/softhsm/libsofthsm2.so".to_string())).unwrap() +} + +struct HsmTestScenario<'a> { + name: &'a str, + pin: &'a str, + slot: Option, + key_id: Option, + so_module: PathBuf, + network: Network, + requirement: AuthRequirement, + neuron_id: Option, + use_random_key: bool, +} + +#[allow(dead_code)] +impl<'a> HsmTestScenario<'a> { + fn new(name: &'a str, pin: &'a str) -> Self { + Self { + requirement: AuthRequirement::Anonymous, + name, + pin, + slot: None, + key_id: None, + so_module: get_softhsm2_module(), + network: Network::mainnet_unchecked().unwrap(), + neuron_id: None, + use_random_key: true, + } + } + + fn with_slot(self, slot: u64) -> Self { + Self { slot: Some(slot), ..self } + } + + fn with_key_id(self, key_id: u8) -> Self { + Self { + key_id: Some(key_id), + ..self + } + } + + fn with_neuron_id(self, neuron_id: u64) -> Self { + Self { + neuron_id: Some(neuron_id), + ..self + } + } + + fn for_network(self, network: Network) -> Self { + Self { network, ..self } + } + + fn when_required(self, requirement: AuthRequirement) -> Self { + Self { requirement, ..self } + } + + fn import_pem_as_hsm(self) -> Self { + Self { + use_random_key: false, + ..self + } + } + + async fn build_neuron(&self) -> anyhow::Result { + let auth_opts = AuthOpts { + private_key_pem: None, + hsm_opts: HsmOpts { + hsm_pin: Some(self.pin.to_string()), + hsm_so_module: Some(self.so_module.clone()), + hsm_params: crate::commands::HsmParams { + hsm_slot: self.slot, + hsm_key_id: self.key_id, + }, + }, + }; + + Neuron::from_opts_and_req(auth_opts, self.requirement.clone(), &self.network, self.neuron_id).await + } +} + +// For some reason left.eq(&right) doesn't work. +fn compare_neurons(left: &Neuron, right: &Neuron) -> bool { + if !left.include_proposer.eq(&right.include_proposer) || !left.neuron_id.eq(&right.neuron_id) { + return false; + } + + match (left.auth.clone(), right.auth.clone()) { + ( + Auth::Hsm { + pin: left_pin, + slot: _, + key_id: left_key_id, + so_path: left_so_path, + }, + Auth::Hsm { + pin: right_pin, + slot: _, + key_id: right_key_id, + so_path: right_so_path, + }, + ) if PartialEq::eq(&left_pin, &right_pin) && left_key_id.eq(&right_key_id) && left_so_path.eq(&right_so_path) => true, + (Auth::Keyfile { path: left_path }, Auth::Keyfile { path: right_path }) if left_path.eq(&right_path) => true, + (Auth::Anonymous, Auth::Anonymous) => true, + _ => false, + } +} + +#[tokio::test] +async fn hsm_neuron_tests() { + let test_label = "Test HSM"; + let pin = "1234"; + let key_id = 1; + let mut test_slot = 0; + + let scenarios = &[ + ( + HsmTestScenario::new("Detect slot and key id", pin).when_required(AuthRequirement::Signer), + Ok(Neuron { + auth: Auth::Hsm { + pin: pin.to_string(), + slot: 0, + key_id, + so_path: get_softhsm2_module(), + }, + include_proposer: false, + neuron_id: 0, + }), + ), + ( + HsmTestScenario::new("Can't detect neuron_id", pin).when_required(AuthRequirement::Neuron), + Err(anyhow::anyhow!("Error because private key doesn't control any neurons")), + ), + ( + HsmTestScenario::new("Detect only slot", pin) + .when_required(AuthRequirement::Signer) + .with_key_id(key_id), + Ok(Neuron { + auth: Auth::Hsm { + pin: pin.to_string(), + slot: 0, + key_id, + so_path: get_softhsm2_module(), + }, + neuron_id: 0, + include_proposer: false, + }), + ), + ( + HsmTestScenario::new("Should be able to fetch neuron_id", pin) + .when_required(AuthRequirement::Neuron) + .import_pem_as_hsm(), + Ok(Neuron { + auth: Auth::Hsm { + pin: pin.to_string(), + slot: 0, + key_id, + so_path: get_softhsm2_module(), + }, + neuron_id: 0, + include_proposer: true, + }), + ), + ]; + + let mut failed = vec![]; + for (scenario, want) in scenarios { + delete_test_slot(test_slot, test_label); + test_slot = ensure_slot_exists(0, test_label, pin); + match scenario.use_random_key { + true => generate_password_for_test_hsm(pin, key_id, test_label), + false => import_pem_as_pkey(pin, key_id, test_label, &get_softhsm2_module()), + } + let maybe_neuron = scenario.build_neuron().await; + if (want.is_err() && maybe_neuron.is_ok()) || (want.is_ok() && maybe_neuron.is_err()) { + failed.push((scenario.name, maybe_neuron, want)); + continue; + } + + if want.is_err() && maybe_neuron.is_err() { + continue; + } + + let neuron = maybe_neuron.unwrap(); + let wanted = want.as_ref().unwrap(); + + if !compare_neurons(&neuron, wanted) { + failed.push((scenario.name, Ok(neuron), want)) + } + } + + assert!( + failed.is_empty(), + "Failed scenarios:\n{}", + failed + .iter() + .map(|(name, neuron, want)| format!("Scenario `{}`\nExpected:\n\t{:?}\nGot:\n\t{:?}", name, want, neuron)) + .join("\n") + ) +} diff --git a/rs/cli/src/unit_tests/mod.rs b/rs/cli/src/tests/mod.rs similarity index 100% rename from rs/cli/src/unit_tests/mod.rs rename to rs/cli/src/tests/mod.rs diff --git a/rs/cli/src/unit_tests/update_unassigned_nodes.rs b/rs/cli/src/tests/update_unassigned_nodes.rs similarity index 100% rename from rs/cli/src/unit_tests/update_unassigned_nodes.rs rename to rs/cli/src/tests/update_unassigned_nodes.rs diff --git a/rs/cli/src/unit_tests/version.rs b/rs/cli/src/tests/version.rs similarity index 100% rename from rs/cli/src/unit_tests/version.rs rename to rs/cli/src/tests/version.rs diff --git a/rs/ic-canisters/src/lib.rs b/rs/ic-canisters/src/lib.rs index ca2fd616f..0f790abbe 100644 --- a/rs/ic-canisters/src/lib.rs +++ b/rs/ic-canisters/src/lib.rs @@ -40,9 +40,9 @@ impl IcAgentCanisterClient { Self::build_agent(url, identity) } - pub fn from_hsm(pin: String, slot: u64, key_id: String, url: Url, lock: Option>) -> anyhow::Result { + pub fn from_hsm(pin: String, slot: u64, key_id: String, url: Url, lock: Option>, hsm_so_path: PathBuf) -> anyhow::Result { let pin_fn = || Ok(pin); - let identity = ParallelHardwareIdentity::new(pkcs11_lib_path()?, slot as usize, &key_id, pin_fn, lock)?; + let identity = ParallelHardwareIdentity::new(hsm_so_path, slot as usize, &key_id, pin_fn, lock)?; Self::build_agent(url, Box::new(identity)) } @@ -93,15 +93,3 @@ pub struct CallIn { args: Vec, cycles: TCycles, } - -fn pkcs11_lib_path() -> anyhow::Result { - let lib_macos_path = PathBuf::from_str("/Library/OpenSC/lib/opensc-pkcs11.so")?; - let lib_linux_path = PathBuf::from_str("/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so")?; - if lib_macos_path.exists() { - Ok(lib_macos_path) - } else if lib_linux_path.exists() { - Ok(lib_linux_path) - } else { - Err(anyhow::anyhow!("no pkcs11 library found")) - } -} diff --git a/rs/ic-canisters/src/parallel_hardware_identity.rs b/rs/ic-canisters/src/parallel_hardware_identity.rs index 7a56f60a0..c8c7fb92f 100644 --- a/rs/ic-canisters/src/parallel_hardware_identity.rs +++ b/rs/ic-canisters/src/parallel_hardware_identity.rs @@ -17,7 +17,7 @@ use simple_asn1::{ ASN1Block::{BitString, ObjectIdentifier, OctetString, Sequence}, ASN1DecodeErr, ASN1EncodeErr, }; -use std::{path::Path, ptr, sync::Mutex}; +use std::{path::PathBuf, ptr, sync::Mutex}; use thiserror::Error; type KeyIdVec = Vec; @@ -101,23 +101,22 @@ pub struct ParallelHardwareIdentity { impl ParallelHardwareIdentity { /// Create an identity using a specific key on an HSM. - /// The filename will be something like /usr/local/lib/opensc-pkcs11.s + /// The filename will be something like /usr/local/lib/opensc-pkcs11.so /// The key_id must refer to a ECDSA key with parameters prime256v1 (secp256r1) /// The key must already have been created. You can create one with pkcs11-tool: /// $ pkcs11-tool -k --slot $SLOT -d $KEY_ID --key-type EC:prime256v1 --pin $PIN - pub fn new( - pkcs11_lib_path: P, - slot_index: usize, + pub fn new( + pkcs11_lib_path: PathBuf, + slot: usize, key_id: &str, pin_fn: PinFn, lock: Option>, ) -> Result where - P: AsRef, PinFn: FnOnce() -> Result, { let ctx = Ctx::new_and_initialize(pkcs11_lib_path)?; - let slot_id = get_slot_id(&ctx, slot_index)?; + let slot_id = get_slot_id(&ctx, slot)?; let session_handle = open_session(&ctx, slot_id)?; let pin = pin_fn().map_err(HardwareIdentityError::UserPinRequired)?; login_if_required(&ctx, session_handle, pin.clone(), slot_id)?; @@ -171,10 +170,11 @@ impl Identity for ParallelHardwareIdentity { } } -fn get_slot_id(ctx: &Ctx, slot_index: usize) -> Result { +fn get_slot_id(ctx: &Ctx, slot: usize) -> Result { ctx.get_slot_list(true)? - .get(slot_index) - .ok_or(HardwareIdentityError::NoSuchSlotIndex(slot_index)) + .iter() + .find(|s| **s == slot as u64) + .ok_or(HardwareIdentityError::NoSuchSlotIndex(slot)) .copied() } diff --git a/rs/rollout-controller/src/actions/mod.rs b/rs/rollout-controller/src/actions/mod.rs index 55b490edc..1dfb919eb 100644 --- a/rs/rollout-controller/src/actions/mod.rs +++ b/rs/rollout-controller/src/actions/mod.rs @@ -1,7 +1,8 @@ -use std::time::Duration; +use std::{path::PathBuf, str::FromStr, time::Duration}; use dre::{ - auth::{Auth, Neuron}, + auth::Neuron, + commands::AuthOpts, ic_admin::{IcAdmin, IcAdminImpl, ProposeCommand, ProposeOptions}, }; use ic_base_types::PrincipalId; @@ -112,7 +113,13 @@ pub struct ActionExecutor<'a> { impl<'a> ActionExecutor<'a> { pub async fn new(neuron_id: u64, private_key_pem: String, network: Network, simulate: bool, logger: Option<&'a Logger>) -> anyhow::Result { - let neuron = Neuron::new(Auth::pem(private_key_pem.into()).await?, Some(neuron_id), &network, true).await?; + let neuron = Neuron::from_opts_and_req( + AuthOpts::try_from(PathBuf::from_str(&private_key_pem)?)?, + dre::commands::AuthRequirement::Neuron, + &network, + Some(neuron_id), + ) + .await?; Ok(Self { ic_admin_wrapper: IcAdminImpl::new(network, None, true, neuron, simulate), logger, @@ -120,7 +127,7 @@ impl<'a> ActionExecutor<'a> { } pub async fn test(network: Network, logger: Option<&'a Logger>) -> anyhow::Result { - let neuron = Neuron::new(Auth::auto(None, None, None).await?, None, &network, true).await?; + let neuron = Neuron::from_opts_and_req(AuthOpts::none(), dre::commands::AuthRequirement::Anonymous, &network, None).await?; Ok(Self { ic_admin_wrapper: IcAdminImpl::new(network, None, true, neuron, true), logger,