From 54fc9a2a1fae3ab6b7162b2edd9f1feab1c5edb1 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 13 Jul 2026 11:23:11 -0400 Subject: [PATCH] feat(network): require explicit root-key for connected networks The root-key for a connected network was optional and silently defaulted to the IC mainnet key. That masked misconfiguration: a connected network pointing somewhere non-mainnet would appear to work until the first real message-verification failure. Make root-key required for connected networks and give it three forms: - `mainnet`: use the canonical IC mainnet root key (e.g. to reach mainnet through a custom boundary node without repeating the literal). - `fetch`: fetch the key from the network on each use. Trust-on-first-use and unverified, so intended only for testnets you or someone you trust operate; a warning is printed on every fetch. - a hex-encoded key, as before. The fetch happens in the network Accessor via an anonymous bootstrap agent, so the key is resolved before identity/delegation verification needs it. `network status` now reports provenance: a `root_key_source` field in --json and a `(fetched - ...)` / `(mainnet)` / `(configured)` label in text. Only the built-in `ic` network keeps the implicit mainnet key. This is technically breaking, but non-mainnet connected networks already needed an explicit key, so in practice only mainnet-via-custom-URL configs must add `root-key: mainnet`. Also fixes the icp-network-inline / icp-network-connected examples, which defined a `staging` network but no matching environment and told users to run `icp deploy --network staging` (deploy takes --environment, not --network); both now define a `staging` environment. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 5 + crates/icp-cli/src/commands/network/status.rs | 16 ++- crates/icp-cli/src/options.rs | 25 ++-- crates/icp-cli/tests/network_status_tests.rs | 73 ++++++++++ crates/icp-cli/tests/network_tests.rs | 1 + crates/icp-cli/tests/project_tests.rs | 1 + crates/icp/src/context/init.rs | 7 +- crates/icp/src/context/mod.rs | 7 +- crates/icp/src/context/tests.rs | 11 ++ crates/icp/src/lib.rs | 5 +- crates/icp/src/manifest/network.rs | 126 +++++++++++++++--- crates/icp/src/network/access.rs | 78 ++++++++++- crates/icp/src/network/mod.rs | 17 +-- crates/icp/src/project.rs | 4 +- docs/concepts/environments.md | 2 + docs/concepts/project-model.md | 1 + docs/migration/from-dfx.md | 3 +- docs/reference/cli.md | 50 +++---- docs/reference/configuration.md | 12 +- docs/schemas/icp-yaml-schema.json | 26 +++- docs/schemas/network-yaml-schema.json | 26 +++- examples/icp-network-connected/README.md | 4 +- examples/icp-network-connected/icp.yaml | 4 + .../networks/staging.yaml | 1 + examples/icp-network-inline/README.md | 4 +- examples/icp-network-inline/icp.yaml | 5 + 26 files changed, 412 insertions(+), 102 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b5ee1e00..c2459a8de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ bump. Currently experimental: project bundling # Unreleased * feat: `icp canister delete` will now send the canister's remaining cycles to the caller +* feat: Connected networks now take an explicit `root-key`, which accepts a hex-encoded key or one of two new values: + * `mainnet`: use the canonical IC mainnet root key — handy for reaching mainnet through a custom boundary node without repeating the literal. + * `fetch`: fetch the key from the network on each use. This is trust-on-first-use and does *not* verify the key's provenance, so it's meant only for testnets you or someone you trust operate; `icp` prints a warning whenever it fetches. + * `network status` reports where the key came from — a `root_key_source` field in `--json`, and a `(fetched - unverified, trust-on-first-use)` label in text output. + * `root-key` is now required for connected networks (previously optional, silently defaulting to the mainnet key). This is technically breaking, but most working projects are unaffected: a non-mainnet connected network already needed an explicit key, so in practice only a mainnet-via-custom-URL network needs to add `root-key: mainnet`. The built-in `ic` network is unchanged. # v1.0.2 diff --git a/crates/icp-cli/src/commands/network/status.rs b/crates/icp-cli/src/commands/network/status.rs index 37f9ed00f..84802085c 100644 --- a/crates/icp-cli/src/commands/network/status.rs +++ b/crates/icp-cli/src/commands/network/status.rs @@ -1,6 +1,9 @@ use anyhow::Context as _; use clap::Args; -use icp::{context::Context, network::Configuration}; +use icp::{ + context::Context, + network::{Configuration, RootKeySource}, +}; use serde::Serialize; use super::args::NetworkOrEnvironmentArgs; @@ -48,6 +51,7 @@ struct NetworkStatus { #[serde(skip_serializing_if = "Option::is_none")] proxy_canister_principal: Option, root_key: String, + root_key_source: RootKeySource, } pub(crate) async fn exec(ctx: &Context, args: &StatusArgs) -> Result<(), anyhow::Error> { @@ -79,6 +83,7 @@ pub(crate) async fn exec(ctx: &Context, args: &StatusArgs) -> Result<(), anyhow: api_url: network_access.api_url.to_string(), gateway_url: network_access.http_gateway_url.map(|u| u.to_string()), root_key: hex::encode(network_access.root_key), + root_key_source: network_access.root_key_source, candid_ui_principal: descriptor.candid_ui_canister_id.map(|p| p.to_string()), proxy_canister_principal: descriptor.proxy_canister_id.map(|p| p.to_string()), } @@ -90,6 +95,7 @@ pub(crate) async fn exec(ctx: &Context, args: &StatusArgs) -> Result<(), anyhow: candid_ui_principal: None, proxy_canister_principal: None, root_key: hex::encode(network_access.root_key), + root_key_source: network_access.root_key_source, }, }; @@ -102,7 +108,13 @@ pub(crate) async fn exec(ctx: &Context, args: &StatusArgs) -> Result<(), anyhow: if let Some(gateway_url) = status.gateway_url { output.push_str(&format!("Gateway Url: {}\n", gateway_url)); } - output.push_str(&format!("Root Key: {}\n", status.root_key)); + let root_key_note = match status.root_key_source { + RootKeySource::Managed => "", + RootKeySource::Mainnet => " (mainnet)", + RootKeySource::Configured => " (configured)", + RootKeySource::Fetched => " (fetched - unverified, trust-on-first-use)", + }; + output.push_str(&format!("Root Key: {}{root_key_note}\n", status.root_key)); if let Some(ref principal) = status.candid_ui_principal { output.push_str(&format!("Candid UI Principal: {}\n", principal)); } diff --git a/crates/icp-cli/src/options.rs b/crates/icp-cli/src/options.rs index 54722beb5..77c087974 100644 --- a/crates/icp-cli/src/options.rs +++ b/crates/icp-cli/src/options.rs @@ -2,6 +2,7 @@ use clap::error::ErrorKind; use clap::{ArgGroup, ArgMatches, Args, FromArgMatches}; use icp::context::{EnvironmentSelection, NetworkSelection}; use icp::identity::IdentitySelection; +use icp::network::RootKeySpec; use icp::prelude::LOCAL; use url::Url; @@ -64,19 +65,8 @@ impl From for EnvironmentSelection { } } -#[derive(Clone, Debug)] -pub(crate) struct RootKey(pub Vec); - -fn parse_root_key(input: &str) -> Result { - let v = hex::decode(input).map_err(|e| format!("Invalid root key hex string: {e}"))?; - if v.len() != 133 { - Err(format!( - "Invalid root key. Expected 133 bytes but got {}", - v.len() - )) - } else { - Ok(RootKey(v)) - } +fn parse_root_key(input: &str) -> Result { + RootKeySpec::try_from(input.to_string()) } #[derive(Clone, Debug)] @@ -100,16 +90,17 @@ pub(crate) struct NetworkOptInner { network: Option, /// The root key to use if connecting to a network by URL. - /// Required when using `--network `. + /// Required when using `--network `. One of `mainnet`, `fetch`, or a + /// 266-character hex-encoded root key. #[arg(long, short = 'k', requires = "network", help_heading = heading::NETWORK_PARAMETERS, value_parser = parse_root_key)] - root_key: Option, + root_key: Option, } // This is wrapper around NetworkOptInner that will do some additional // validation to only allow --root-key when the network is a url. #[derive(Clone, Debug, Default)] pub(crate) enum NetworkOpt { - Url(Url, RootKey), + Url(Url, RootKeySpec), Name(String), @@ -169,7 +160,7 @@ impl Args for NetworkOpt { impl From for NetworkSelection { fn from(v: NetworkOpt) -> Self { match v { - NetworkOpt::Url(url, RootKey(key)) => NetworkSelection::Url(url, key), + NetworkOpt::Url(url, root_key) => NetworkSelection::Url(url, root_key), NetworkOpt::Name(name) => NetworkSelection::Named(name), NetworkOpt::None => NetworkSelection::Default, } diff --git a/crates/icp-cli/tests/network_status_tests.rs b/crates/icp-cli/tests/network_status_tests.rs index a1281493a..fa5f4298e 100644 --- a/crates/icp-cli/tests/network_status_tests.rs +++ b/crates/icp-cli/tests/network_status_tests.rs @@ -1,4 +1,5 @@ use icp::fs::write_string; +use indoc::formatdoc; use predicates::str::{PredicateStrExt, contains}; mod common; @@ -135,6 +136,7 @@ networks: - name: connected-network mode: connected url: https://ic0.app + root-key: mainnet "#, ) .expect("failed to write project manifest"); @@ -147,6 +149,77 @@ networks: .stdout(contains("Url: https://ic0.app")); } +/// End-to-end test of the `root-key: fetch` path with no external network: +/// start a managed local network in one project, then in a second project +/// define a connected network pointing at it with `root-key: fetch`. `icp` +/// should fetch the running network's root key trust-on-first-use, warn about +/// it, and `network status` should report the key as `fetched` and match the +/// real root key. +#[tokio::test] +async fn status_connected_network_fetches_root_key() { + let ctx = TestContext::new(); + + // Provider project: start a managed local network on a random port. + let provider = ctx.create_project_dir("provider"); + write_string(&provider.join("icp.yaml"), NETWORK_RANDOM_PORT) + .expect("failed to write provider manifest"); + let _guard = ctx.start_network_in(&provider, "random-network").await; + let network = ctx.wait_for_network_descriptor(&provider, "random-network"); + ctx.ping_until_healthy(&provider, "random-network"); + + // Consumer project: connect to the provider's network and fetch its root key. + let consumer = ctx.create_project_dir("consumer"); + write_string( + &consumer.join("icp.yaml"), + &formatdoc! {r#" + networks: + - name: fetched-network + mode: connected + url: http://localhost:{port} + root-key: fetch + "#, + port = network.gateway_port, + }, + ) + .expect("failed to write consumer manifest"); + + // Text output: key is labeled as fetched, and the CLI warns on stderr. + ctx.icp() + .current_dir(&consumer) + .args(["network", "status", "fetched-network"]) + .assert() + .success() + .stdout(contains("Root Key:")) + .stdout(contains("(fetched - unverified, trust-on-first-use)")) + .stderr(contains("provenance is not verified")); + + // JSON output: root_key_source is "fetched" and the fetched key matches the + // running network's actual root key. (The warning lands on stderr, so the + // JSON on stdout stays clean.) + let output = ctx + .icp() + .current_dir(&consumer) + .args(["network", "status", "fetched-network", "--json"]) + .assert() + .success() + .get_output() + .stdout + .clone(); + + let json_str = String::from_utf8(output).expect("output should be valid UTF-8"); + let json: serde_json::Value = + serde_json::from_str(&json_str).expect("output should be valid JSON"); + + assert_eq!(json["root_key_source"], "fetched"); + assert_eq!( + json["root_key"] + .as_str() + .expect("root_key should be a string"), + hex::encode(&network.root_key), + "fetched root key should match the running network's root key" + ); +} + #[test] fn status_not_in_project() { let ctx = TestContext::new(); diff --git a/crates/icp-cli/tests/network_tests.rs b/crates/icp-cli/tests/network_tests.rs index 151684762..338c2f24b 100644 --- a/crates/icp-cli/tests/network_tests.rs +++ b/crates/icp-cli/tests/network_tests.rs @@ -612,6 +612,7 @@ async fn cannot_override_ic() { - name: ic mode: connected url: http://fake-ic.local + root-key: mainnet "#}, ) .expect("failed to write project manifest"); diff --git a/crates/icp-cli/tests/project_tests.rs b/crates/icp-cli/tests/project_tests.rs index d0090d914..7aaee9d5f 100644 --- a/crates/icp-cli/tests/project_tests.rs +++ b/crates/icp-cli/tests/project_tests.rs @@ -280,6 +280,7 @@ fn redefine_ic_network_disallowed() { - name: ic mode: connected url: https://fake-ic.io + root-key: mainnet "#, ) .expect("failed to write project manifest"); diff --git a/crates/icp/src/context/init.rs b/crates/icp/src/context/init.rs index 8ba44f9a2..8a274415b 100644 --- a/crates/icp/src/context/init.rs +++ b/crates/icp/src/context/init.rs @@ -128,15 +128,16 @@ pub fn initialize( )); } + // Agent creator + let agent_creator = Arc::new(agent::Creator); + // Network accessor let netaccess = Arc::new(network::Accessor { project_root_locate: project_root_locate.clone(), descriptors: dirs.port_descriptor(), + agent: agent_creator.clone(), }); - // Agent creator - let agent_creator = Arc::new(agent::Creator); - // Setup environment Ok(Context { dirs, diff --git a/crates/icp/src/context/mod.rs b/crates/icp/src/context/mod.rs index 327771885..a7b2f9dc2 100644 --- a/crates/icp/src/context/mod.rs +++ b/crates/icp/src/context/mod.rs @@ -7,6 +7,7 @@ use crate::{ canister::{build::Build, sync::Synchronize}, directories, identity::IdentitySelection, + manifest::network::RootKeySpec, network::{Configuration as NetworkConfiguration, access::NetworkAccess}, prelude::*, store_id::{IdMapping, LookupIdError}, @@ -30,7 +31,7 @@ pub enum NetworkSelection { /// Use a named network Named(String), /// Use a network by URL - Url(Url, Vec), + Url(Url, RootKeySpec), } /// Selection type for environments - similar to IdentitySelection @@ -180,7 +181,7 @@ impl Context { http_gateway_url: Some( IC_MAINNET_NETWORK_GATEWAY_URL.parse().unwrap(), ), - root_key: IC_ROOT_KEY.to_vec(), + root_key: RootKeySpec::Mainnet, }, }, } @@ -197,7 +198,7 @@ impl Context { connected: crate::network::Connected { api_url: url.clone(), http_gateway_url: Some(url.clone()), - root_key: root_key.to_vec(), + root_key: root_key.clone(), }, }, }, diff --git a/crates/icp/src/context/tests.rs b/crates/icp/src/context/tests.rs index 86b55f71c..f8e9a4e9a 100644 --- a/crates/icp/src/context/tests.rs +++ b/crates/icp/src/context/tests.rs @@ -350,6 +350,7 @@ async fn test_get_agent_for_env_uses_environment_network() { "local", NetworkAccess { root_key: local_root_key.clone(), + root_key_source: crate::network::RootKeySource::Configured, api_url: Url::parse("http://localhost:8000").unwrap(), http_gateway_url: None, use_friendly_domains: false, @@ -359,6 +360,7 @@ async fn test_get_agent_for_env_uses_environment_network() { "staging", NetworkAccess { root_key: staging_root_key.clone(), + root_key_source: crate::network::RootKeySource::Configured, api_url: Url::parse("http://staging:9000").unwrap(), http_gateway_url: None, use_friendly_domains: false, @@ -433,6 +435,7 @@ async fn test_get_agent_for_network_success() { "local", NetworkAccess { root_key: root_key.clone(), + root_key_source: crate::network::RootKeySource::Configured, api_url: Url::parse("http://localhost:8000").unwrap(), http_gateway_url: None, use_friendly_domains: false, @@ -647,6 +650,7 @@ async fn test_get_agent_defaults_inside_project_with_default_local() { LOCAL, NetworkAccess { root_key: local_root_key.clone(), + root_key_source: crate::network::RootKeySource::Configured, api_url: Url::parse(DEFAULT_LOCAL_NETWORK_URL).unwrap(), http_gateway_url: None, use_friendly_domains: false, @@ -720,6 +724,7 @@ async fn test_get_agent_defaults_with_overridden_local_network() { LOCAL, NetworkAccess { root_key: custom_root_key.clone(), + root_key_source: crate::network::RootKeySource::Configured, api_url: Url::parse("http://localhost:9000").unwrap(), // Custom port http_gateway_url: None, use_friendly_domains: false, @@ -820,6 +825,7 @@ async fn test_get_agent_defaults_with_overridden_local_environment() { LOCAL, NetworkAccess { root_key: local_root_key.clone(), + root_key_source: crate::network::RootKeySource::Configured, api_url: Url::parse(DEFAULT_LOCAL_NETWORK_URL).unwrap(), http_gateway_url: None, use_friendly_domains: false, @@ -829,6 +835,7 @@ async fn test_get_agent_defaults_with_overridden_local_environment() { "custom", NetworkAccess { root_key: custom_root_key.clone(), + root_key_source: crate::network::RootKeySource::Configured, api_url: Url::parse("http://localhost:7000").unwrap(), http_gateway_url: None, use_friendly_domains: false, @@ -864,6 +871,7 @@ async fn test_get_agent_explicit_network_inside_project() { LOCAL, NetworkAccess { root_key: local_root_key.clone(), + root_key_source: crate::network::RootKeySource::Configured, api_url: Url::parse(DEFAULT_LOCAL_NETWORK_URL).unwrap(), http_gateway_url: None, use_friendly_domains: false, @@ -873,6 +881,7 @@ async fn test_get_agent_explicit_network_inside_project() { "staging", NetworkAccess { root_key: staging_root_key.clone(), + root_key_source: crate::network::RootKeySource::Configured, api_url: Url::parse("http://localhost:8001").unwrap(), http_gateway_url: None, use_friendly_domains: false, @@ -909,6 +918,7 @@ async fn test_get_agent_explicit_environment_inside_project() { LOCAL, NetworkAccess { root_key: local_root_key.clone(), + root_key_source: crate::network::RootKeySource::Configured, api_url: Url::parse(DEFAULT_LOCAL_NETWORK_URL).unwrap(), http_gateway_url: None, use_friendly_domains: false, @@ -918,6 +928,7 @@ async fn test_get_agent_explicit_environment_inside_project() { "staging", NetworkAccess { root_key: staging_root_key.clone(), + root_key_source: crate::network::RootKeySource::Configured, api_url: Url::parse("http://localhost:8001").unwrap(), http_gateway_url: None, use_friendly_domains: false, diff --git a/crates/icp/src/lib.rs b/crates/icp/src/lib.rs index d5ad70cf0..d7461f499 100644 --- a/crates/icp/src/lib.rs +++ b/crates/icp/src/lib.rs @@ -349,10 +349,10 @@ impl MockProjectLoader { /// - "prod" (ic network, backend and frontend only) pub fn complex() -> Self { use crate::{ - context::IC_ROOT_KEY, manifest::{ adapter::prebuilt::{Adapter as PrebuiltAdapter, LocalSource, SourceField}, canister::{BuildStep, BuildSteps, SyncSteps}, + network::RootKeySpec, }, network::{ Configuration, Connected, Gateway, Managed, ManagedLauncherConfig, ManagedMode, @@ -460,7 +460,7 @@ impl MockProjectLoader { connected: Connected { api_url: IC_MAINNET_NETWORK_API_URL.parse().unwrap(), http_gateway_url: Some(IC_MAINNET_NETWORK_GATEWAY_URL.parse().unwrap()), - root_key: IC_ROOT_KEY.to_vec(), + root_key: RootKeySpec::Mainnet, }, }, }; @@ -711,6 +711,7 @@ mod tests { - name: test-network mode: connected url: https://somenetwork.icp + root-key: mainnet environments: - name: local network: test-network diff --git a/crates/icp/src/manifest/network.rs b/crates/icp/src/manifest/network.rs index d892f9aa2..16fc14f1b 100644 --- a/crates/icp/src/manifest/network.rs +++ b/crates/icp/src/manifest/network.rs @@ -123,10 +123,16 @@ pub struct Connected { #[serde(flatten)] pub endpoints: Endpoints, - /// The root key of this network - #[serde(skip_serializing_if = "Option::is_none")] - #[schemars(with = "Option", regex(pattern = "^[0-9a-f]{266}$"))] - pub root_key: Option, + /// How to obtain the root key used to verify responses from this network. + /// + /// One of: + /// - `mainnet`: use the canonical IC mainnet root key (e.g. to reach mainnet + /// through a non-default boundary node without repeating the key literal). + /// - `fetch`: fetch the root key from the network on each use. This is + /// trust-on-first-use and does *not* verify the key's provenance; only use it + /// for testnets that you (or someone you trust) operate. + /// - a 266-character hex-encoded root key (133 bytes). + pub root_key: RootKeySpec, } #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] @@ -150,22 +156,67 @@ pub enum Endpoints { }, } +/// The expected byte length of a root key (133 bytes / 266 hex characters). +pub const ROOT_KEY_LEN: usize = 133; + +/// How to obtain the root key used to verify responses from a connected network. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] #[serde(try_from = "String", into = "String")] -pub struct RootKey(pub Vec); +pub enum RootKeySpec { + /// Use the canonical IC mainnet root key. + Mainnet, + /// Fetch the root key from the network on each use (trust-on-first-use, unverified). + Fetch, + /// A specific root key (133 bytes). + Explicit(Vec), +} -impl TryFrom for RootKey { - type Error = hex::FromHexError; +impl TryFrom for RootKeySpec { + type Error = String; fn try_from(value: String) -> Result { - let bytes = hex::decode(value)?; - Ok(RootKey(bytes)) + match value.as_str() { + "mainnet" => Ok(RootKeySpec::Mainnet), + "fetch" => Ok(RootKeySpec::Fetch), + hex => { + let bytes = hex::decode(hex) + .map_err(|e| format!("invalid root key: expected \"mainnet\", \"fetch\", or a hex-encoded key, but failed to decode as hex: {e}"))?; + if bytes.len() != ROOT_KEY_LEN { + return Err(format!( + "invalid root key: expected {ROOT_KEY_LEN} bytes but got {}", + bytes.len() + )); + } + Ok(RootKeySpec::Explicit(bytes)) + } + } + } +} + +impl From for String { + fn from(spec: RootKeySpec) -> Self { + match spec { + RootKeySpec::Mainnet => "mainnet".to_string(), + RootKeySpec::Fetch => "fetch".to_string(), + RootKeySpec::Explicit(bytes) => hex::encode(bytes), + } } } -impl From for String { - fn from(root_key: RootKey) -> Self { - hex::encode(root_key.0) +impl JsonSchema for RootKeySpec { + fn schema_name() -> std::borrow::Cow<'static, str> { + "RootKeySpec".into() + } + + fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + schemars::json_schema!({ + "type": "string", + "description": "Root key: \"mainnet\", \"fetch\", or a 266-character hex-encoded key.", + "anyOf": [ + { "enum": ["mainnet", "fetch"] }, + { "pattern": "^[0-9a-f]{266}$" } + ] + }) } } @@ -220,6 +271,7 @@ mod tests { name: my-network mode: connected url: https://ic0.app + root-key: mainnet "#}), NetworkManifest { name: "my-network".to_string(), @@ -227,12 +279,50 @@ mod tests { endpoints: Endpoints::Implicit { url: "https://ic0.app".parse().unwrap(), }, - root_key: None + root_key: RootKeySpec::Mainnet, + }), + }, + ); + } + + #[test] + fn connected_network_fetch() { + assert_eq!( + validate_network_yaml(indoc! {r#" + name: my-network + mode: connected + url: https://testnet.example.com + root-key: fetch + "#}), + NetworkManifest { + name: "my-network".to_string(), + configuration: Mode::Connected(Connected { + endpoints: Endpoints::Implicit { + url: "https://testnet.example.com".parse().unwrap(), + }, + root_key: RootKeySpec::Fetch, }), }, ); } + #[test] + fn connected_network_requires_root_key() { + match serde_yaml::from_str::(indoc! {r#" + name: my-network + mode: connected + url: https://ic0.app + "#}) + { + Ok(_) => panic!("a connected network without a root key should fail"), + Err(err) => { + if !format!("{err}").contains("root-key") { + panic!("unexpected error for missing root key: {err}"); + } + } + }; + } + #[test] fn just_a_name_fails() { match serde_yaml::from_str::(r#"name: my-network"#) { @@ -269,16 +359,14 @@ mod tests { endpoints: Endpoints::Implicit { url: "https://ic0.app".parse().unwrap(), }, - root_key: Some( - RootKey::try_from( - "308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c050302010\ + root_key: RootKeySpec::try_from( + "308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c050302010\ 361008b52b4994f94c7ce4be1c1542d7c81dc79fea17d49efe8fa42e8566373581d4b9\ 69c4a59e96a0ef51b711fe5027ec01601182519d0a788f4bfe388e593b97cd1d7e4490\ 4de79422430bca686ac8c21305b3397b5ba4d7037d17877312fb7ee34" - .to_string() - ) - .unwrap() + .to_string() ) + .unwrap(), }), }, ); diff --git a/crates/icp/src/network/access.rs b/crates/icp/src/network/access.rs index 6a7616db8..05dbe3b4c 100644 --- a/crates/icp/src/network/access.rs +++ b/crates/icp/src/network/access.rs @@ -1,16 +1,41 @@ +use std::sync::Arc; + +use ic_agent::{AgentError, identity::AnonymousIdentity}; +use serde::Serialize; use snafu::{OptionExt, ResultExt, Snafu}; use url::Url; use crate::{ + agent::{Create, CreateAgentError}, + context::IC_ROOT_KEY, + manifest::network::RootKeySpec, network::{Connected, NetworkDirectory, directory::LoadNetworkFileError}, prelude::*, }; +/// Where a network's root key came from. Used for display so users can tell a +/// trusted/pinned key apart from one that was fetched trust-on-first-use. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum RootKeySource { + /// Key belongs to a managed network we launched. + Managed, + /// The canonical IC mainnet root key. + Mainnet, + /// An explicit key pinned in the manifest or on the command line. + Configured, + /// Fetched from the network (trust-on-first-use, provenance unverified). + Fetched, +} + #[derive(Clone)] pub struct NetworkAccess { - /// Network's root-key + /// Network's (resolved) root key. pub root_key: Vec, + /// Where [`Self::root_key`] came from. + pub root_key_source: RootKeySource, + /// Routing configuration pub api_url: Url, pub http_gateway_url: Option, @@ -44,6 +69,20 @@ pub enum GetNetworkAccessError { #[snafu(display("failed to load network descriptor"))] LoadNetworkDescriptor { source: LoadNetworkFileError }, + + #[snafu(display("failed to create agent to fetch root key from {url}"))] + CreateBootstrapAgent { + url: Url, + #[snafu(source(from(CreateAgentError, Box::new)))] + source: Box, + }, + + #[snafu(display("failed to fetch root key from {url}"))] + FetchRootKey { + url: Url, + #[snafu(source(from(AgentError, Box::new)))] + source: Box, + }, } pub async fn get_managed_network_access( @@ -81,6 +120,7 @@ pub async fn get_managed_network_access( let http_gateway_url = Url::parse(&format!("http://{}:{port}", desc.gateway.host)).unwrap(); Ok(NetworkAccess { root_key: desc.root_key, + root_key_source: RootKeySource::Managed, api_url: http_gateway_url.clone(), http_gateway_url: Some(http_gateway_url), use_friendly_domains: desc.use_friendly_domains, @@ -89,13 +129,47 @@ pub async fn get_managed_network_access( pub async fn get_connected_network_access( connected: &Connected, + agent: &Arc, ) -> Result { - let root_key = connected.root_key.clone(); + let (root_key, root_key_source) = match &connected.root_key { + RootKeySpec::Mainnet => (IC_ROOT_KEY.to_vec(), RootKeySource::Mainnet), + RootKeySpec::Explicit(bytes) => (bytes.clone(), RootKeySource::Configured), + RootKeySpec::Fetch => { + let root_key = fetch_root_key(agent, &connected.api_url).await?; + (root_key, RootKeySource::Fetched) + } + }; Ok(NetworkAccess { root_key, + root_key_source, api_url: connected.api_url.clone(), http_gateway_url: connected.http_gateway_url.clone(), use_friendly_domains: false, }) } + +/// Fetch a network's root key trust-on-first-use. This does *not* verify the +/// key's provenance, so we warn the user that responses cannot be trusted the +/// way a pinned key allows. +async fn fetch_root_key( + agent: &Arc, + api_url: &Url, +) -> Result, GetNetworkAccessError> { + tracing::warn!( + "fetching the root key from {api_url}; its provenance is not verified (trust-on-first-use)" + ); + let bootstrap = agent + .create(Arc::new(AnonymousIdentity), api_url.as_str()) + .await + .context(CreateBootstrapAgentSnafu { + url: api_url.clone(), + })?; + bootstrap + .fetch_root_key() + .await + .context(FetchRootKeySnafu { + url: api_url.clone(), + })?; + Ok(bootstrap.read_root_key()) +} diff --git a/crates/icp/src/network/mod.rs b/crates/icp/src/network/mod.rs index 6ac94746f..3d025fb40 100644 --- a/crates/icp/src/network/mod.rs +++ b/crates/icp/src/network/mod.rs @@ -5,6 +5,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Deserializer, Serialize}; use snafu::prelude::*; +pub use crate::manifest::network::RootKeySpec; +pub use access::RootKeySource; pub use directory::{LoadPidError, NetworkDirectory, SavePidError}; pub use managed::run::{RunNetworkError, run_network}; use strum::EnumString; @@ -172,10 +174,8 @@ pub struct Connected { /// The URL this network's HTTP gateway can be reached at. pub http_gateway_url: Option, - /// The root key of this network - #[serde(with = "hex::serde")] - #[schemars(with = "String")] - pub root_key: Vec, + /// How to obtain the root key used to verify responses from this network. + pub root_key: RootKeySpec, } #[derive(Clone, Debug, Deserialize, PartialEq, JsonSchema, Serialize)] @@ -232,9 +232,7 @@ impl From for Gateway { impl From for Connected { fn from(value: ManifestConnected) -> Self { - let root_key = value - .root_key - .map_or_else(|| crate::context::IC_ROOT_KEY.to_vec(), |rk| rk.0); + let root_key = value.root_key; match value.endpoints { Endpoints::Implicit { url } => Connected { api_url: url.clone(), @@ -358,6 +356,9 @@ pub struct Accessor { // Port descriptors dir pub descriptors: PathBuf, + + // Used to build a bootstrap agent when a connected network fetches its root key + pub agent: Arc, } #[async_trait] @@ -384,7 +385,7 @@ impl Access for Accessor { Ok(get_managed_network_access(nd).await?) } Configuration::Connected { connected: cfg } => { - Ok(get_connected_network_access(cfg).await?) + Ok(get_connected_network_access(cfg, &self.agent).await?) } } } diff --git a/crates/icp/src/project.rs b/crates/icp/src/project.rs index 3b8bcf6b7..a665be6de 100644 --- a/crates/icp/src/project.rs +++ b/crates/icp/src/project.rs @@ -7,7 +7,6 @@ use snafu::prelude::*; use crate::{ Canister, Environment, InitArgs, Network, Project, canister::recipe, - context::IC_ROOT_KEY, fs, manifest::{ ArgsFormat, CANISTER_MANIFEST, CanisterManifest, EnvironmentManifest, Item, @@ -16,6 +15,7 @@ use crate::{ canister::{Instructions, SyncSteps}, environment::CanisterSelection, load_manifest_from_path, + network::RootKeySpec, recipe::RecipeType, }, network::{ @@ -349,7 +349,7 @@ pub async fn consolidate_manifest( connected: Connected { api_url: IC_MAINNET_NETWORK_API_URL.parse().unwrap(), http_gateway_url: Some(IC_MAINNET_NETWORK_GATEWAY_URL.parse().unwrap()), - root_key: IC_ROOT_KEY.to_vec(), + root_key: RootKeySpec::Mainnet, }, }, }, diff --git a/docs/concepts/environments.md b/docs/concepts/environments.md index 5eb6434a7..7be0b04be 100644 --- a/docs/concepts/environments.md +++ b/docs/concepts/environments.md @@ -46,6 +46,7 @@ networks: - name: testnet mode: connected url: https://testnet.ic0.app + root-key: fetch ``` Use connected networks for shared testnets and production. @@ -58,6 +59,7 @@ networks: mode: connected api-url: https://api.testnet.example.com http-gateway-url: https://testnet.example.com + root-key: fetch ``` If `http-gateway-url` is omitted, canister URLs will not be printed during deploy operations. diff --git a/docs/concepts/project-model.md b/docs/concepts/project-model.md index d5f1f89ca..ced036d84 100644 --- a/docs/concepts/project-model.md +++ b/docs/concepts/project-model.md @@ -70,6 +70,7 @@ networks: configuration: mode: connected url: https://icp-api.io + root-key: mainnet - name: local configuration: mode: managed diff --git a/docs/migration/from-dfx.md b/docs/migration/from-dfx.md index 5faa95093..fa54d37d8 100644 --- a/docs/migration/from-dfx.md +++ b/docs/migration/from-dfx.md @@ -263,6 +263,7 @@ networks: - name: staging mode: connected url: https://icp-api.io + root-key: mainnet environments: - name: staging @@ -322,7 +323,7 @@ networks: - dfx's `"type": "ephemeral"` maps to icp-cli's `mode: managed` (local networks that icp-cli controls) - dfx's `"providers"` array (which can list multiple URLs for redundancy) becomes a single `url` field in icp-cli - dfx's `"bind"` address for local networks maps to icp-cli's `gateway.bind` and `gateway.port` -- **Root key handling**: dfx automatically fetches the root key from non-mainnet networks at runtime. icp-cli requires you to specify the `root-key` explicitly in the configuration for testnets (connected networks). For local managed networks, icp-cli retrieves the root key from the network launcher. The root key is the public key used to verify responses from the network. Explicit configuration ensures the root key comes from a trusted source rather than the network itself. +- **Root key handling**: dfx automatically fetches the root key from non-mainnet networks at runtime. icp-cli requires you to specify the `root-key` explicitly for connected networks, with one of three values: `mainnet` (use the canonical mainnet key — handy for a custom boundary node), `fetch` (fetch it from the network on each use — the equivalent of dfx's automatic behavior, trust-on-first-use and unverified), or a hex-encoded key to pin. For local managed networks, icp-cli retrieves the root key from the network launcher. Requiring an explicit choice means you opt into fetching rather than getting it silently. **Note:** icp-cli uses `https://icp-api.io` as the default IC mainnet URL, while dfx currently uses `https://icp0.io`. Both URLs point to the same IC mainnet, but `https://icp-api.io` is the recommended API gateway. The implicit `ic` network in icp-cli is configured with `https://icp-api.io`. diff --git a/docs/reference/cli.md b/docs/reference/cli.md index ce3dfab67..5dc5eb162 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -163,7 +163,7 @@ Make a canister call ###### **Options:** * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--args-file ` — Path to a file containing call arguments @@ -239,7 +239,7 @@ Examples: ###### **Options:** * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--controller ` — One or more controllers for the canister. Repeat `--controller` to specify multiple @@ -275,7 +275,7 @@ Cycles will be sent to the caller via the cycles ledger. This is done by install ###### **Options:** * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--proxy ` — Principal of a proxy canister to route the management canister call through @@ -332,7 +332,7 @@ Install a built WASM to a canister on a network * `-y`, `--yes` — Skip confirmation prompts, including the Candid interface compatibility check and the dangerous-operation prompt for `--wasm-memory-persistence replace` * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--proxy ` — Principal of a proxy canister to route the management canister call through @@ -367,7 +367,7 @@ Fetch and display canister logs ###### **Options:** * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `-f`, `--follow` — Continuously fetch and display new logs until interrupted with Ctrl+C @@ -396,7 +396,7 @@ Read a metadata section from a canister ###### **Options:** * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--json` — Output command results as JSON @@ -416,7 +416,7 @@ Migrate a canister ID from one subnet to another ###### **Options:** * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--replace ` — The canister to replace with the source canister's ID @@ -456,7 +456,7 @@ Queries the canister_status endpoint of the management canister and displays onl ###### **Options:** * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--json` — Format output as JSON @@ -477,7 +477,7 @@ Change a canister's settings to specified values ###### **Options:** * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `-f`, `--force` — Force the operation without confirmation prompts @@ -518,7 +518,7 @@ Synchronize a canister's settings with those defined in the project ###### **Options:** * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--proxy ` — Principal of a proxy canister to route the management canister calls through @@ -555,7 +555,7 @@ Create a snapshot of a canister's state ###### **Options:** * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--replace ` — Replace an existing snapshot instead of creating a new one. The old snapshot will be deleted once the new one is successfully created @@ -579,7 +579,7 @@ Delete a canister snapshot ###### **Options:** * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--proxy ` — Principal of a proxy canister to route the management canister call through @@ -600,7 +600,7 @@ Download a snapshot to local disk ###### **Options:** * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `-o`, `--output ` — Output directory for the snapshot files @@ -622,7 +622,7 @@ List all snapshots for a canister ###### **Options:** * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--json` — Output command results as JSON @@ -645,7 +645,7 @@ Restore a canister from a snapshot ###### **Options:** * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--proxy ` — Principal of a proxy canister to route the management canister calls through @@ -665,7 +665,7 @@ Upload a snapshot from local disk ###### **Options:** * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `-i`, `--input ` — Input directory containing the snapshot files @@ -690,7 +690,7 @@ Start a canister on a network ###### **Options:** * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--proxy ` — Principal of a proxy canister to route the management canister call through @@ -727,7 +727,7 @@ Examples: ###### **Options:** * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `-i`, `--id-only` — Only print the canister ids @@ -750,7 +750,7 @@ Stop a canister on a network ###### **Options:** * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--proxy ` — Principal of a proxy canister to route the management canister call through @@ -771,7 +771,7 @@ Top up a canister with cycles * `--amount ` — Amount of cycles to top up. Supports suffixes: k (thousand), m (million), b (billion), t (trillion) * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as @@ -800,7 +800,7 @@ Display the cycles balance ###### **Options:** * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--subaccount ` — The subaccount to check the balance for @@ -825,7 +825,7 @@ Exactly one of --icp or --cycles must be provided. * `--from-subaccount ` — Subaccount to withdraw the ICP from * `--to-subaccount ` — Subaccount to deposit the cycles to * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--json` — Output command results as JSON @@ -848,7 +848,7 @@ Transfer cycles to another principal * `--to-subaccount ` — The subaccount to transfer to (only if the receiver is a principal) * `--from-subaccount ` — The subaccount to transfer cycles from * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--json` — Output command results as JSON @@ -1668,7 +1668,7 @@ Display the token balance on the ledger (default token: icp) ###### **Options:** * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--subaccount ` — The subaccount to check the balance for @@ -1694,7 +1694,7 @@ Transfer ICP or ICRC1 tokens through their ledger (default token: icp) * `--to-subaccount ` — The subaccount to transfer to (only if the receiver is a principal) * `--from-subaccount ` — The subaccount to transfer from * `-n`, `--network ` — Name or URL of the network to target, conflicts with environment argument -* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` +* `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network `. One of `mainnet`, `fetch`, or a 266-character hex-encoded root key * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--json` — Output command results as JSON diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index fa5ab514d..3e6061b6c 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -294,7 +294,7 @@ networks: - name: testnet mode: connected url: https://testnet.ic0.app - root-key: # For non-mainnet + root-key: fetch # mainnet | fetch | ``` | Property | Type | Required | Description | @@ -302,7 +302,15 @@ networks: | `name` | string | Yes | Network identifier | | `mode` | string | Yes | `connected` | | `url` | string | Yes | Network endpoint URL | -| `root-key` | string | No | Hex-encoded root key (non-mainnet only) | +| `root-key` | string | Yes | How to obtain the root key: see below | + +The `root-key` is the public key used to verify responses from the network, and is required for connected networks. It accepts one of: + +- **`mainnet`** — use the canonical IC mainnet root key. Use this to reach mainnet through a non-default boundary node without repeating the key literal; responses are still fully verified against the real mainnet key. +- **`fetch`** — fetch the root key from the network on each use. This is trust-on-first-use and does **not** verify the key's provenance, so only use it for testnets that you (or someone you trust) operate. `icp` prints a warning whenever it fetches, and `network status` labels such a key as `fetched`. +- **a 266-character hex-encoded key** — pin a specific root key. + +> The built-in `ic` network always uses the mainnet root key and cannot be redefined. ### Docker Network diff --git a/docs/schemas/icp-yaml-schema.json b/docs/schemas/icp-yaml-schema.json index 34697af4b..937235c9c 100644 --- a/docs/schemas/icp-yaml-schema.json +++ b/docs/schemas/icp-yaml-schema.json @@ -273,14 +273,13 @@ ], "properties": { "root-key": { - "description": "The root key of this network", - "pattern": "^[0-9a-f]{266}$", - "type": [ - "string", - "null" - ] + "$ref": "#/$defs/RootKeySpec", + "description": "How to obtain the root key used to verify responses from this network.\n\nOne of:\n- `mainnet`: use the canonical IC mainnet root key (e.g. to reach mainnet\n through a non-default boundary node without repeating the key literal).\n- `fetch`: fetch the root key from the network on each use. This is\n trust-on-first-use and does *not* verify the key's provenance; only use it\n for testnets that you (or someone you trust) operate.\n- a 266-character hex-encoded root key (133 bytes)." } }, + "required": [ + "root-key" + ], "type": "object" }, "ControllerRef": { @@ -798,6 +797,21 @@ ], "type": "object" }, + "RootKeySpec": { + "anyOf": [ + { + "enum": [ + "mainnet", + "fetch" + ] + }, + { + "pattern": "^[0-9a-f]{266}$" + } + ], + "description": "Root key: \"mainnet\", \"fetch\", or a 266-character hex-encoded key.", + "type": "string" + }, "Settings": { "description": "Canister settings, such as compute and memory allocation.", "properties": { diff --git a/docs/schemas/network-yaml-schema.json b/docs/schemas/network-yaml-schema.json index e3e6af387..ca39e807f 100644 --- a/docs/schemas/network-yaml-schema.json +++ b/docs/schemas/network-yaml-schema.json @@ -39,14 +39,13 @@ ], "properties": { "root-key": { - "description": "The root key of this network", - "pattern": "^[0-9a-f]{266}$", - "type": [ - "string", - "null" - ] + "$ref": "#/$defs/RootKeySpec", + "description": "How to obtain the root key used to verify responses from this network.\n\nOne of:\n- `mainnet`: use the canonical IC mainnet root key (e.g. to reach mainnet\n through a non-default boundary node without repeating the key literal).\n- `fetch`: fetch the root key from the network on each use. This is\n trust-on-first-use and does *not* verify the key's provenance; only use it\n for testnets that you (or someone you trust) operate.\n- a 266-character hex-encoded root key (133 bytes)." } }, + "required": [ + "root-key" + ], "type": "object" }, "Gateway": { @@ -278,6 +277,21 @@ ], "type": "object" }, + "RootKeySpec": { + "anyOf": [ + { + "enum": [ + "mainnet", + "fetch" + ] + }, + { + "pattern": "^[0-9a-f]{266}$" + } + ], + "description": "Root key: \"mainnet\", \"fetch\", or a 266-character hex-encoded key.", + "type": "string" + }, "SubnetKind": { "enum": [ "application", diff --git a/examples/icp-network-connected/README.md b/examples/icp-network-connected/README.md index 658520ead..2b6174f72 100644 --- a/examples/icp-network-connected/README.md +++ b/examples/icp-network-connected/README.md @@ -8,8 +8,8 @@ This project defines a `staging` network that is an alias for the mainnet. This ## Instructions -To deploy the canister to the `staging` network, run the following command: +This project also defines a `staging` environment that targets the `staging` network. To deploy the canister to it, run: ```bash -icp deploy --network staging +icp deploy --environment staging ``` diff --git a/examples/icp-network-connected/icp.yaml b/examples/icp-network-connected/icp.yaml index 1759a028c..25c78c5ad 100644 --- a/examples/icp-network-connected/icp.yaml +++ b/examples/icp-network-connected/icp.yaml @@ -11,3 +11,7 @@ canisters: networks: - networks/staging.yaml + +environments: + - name: staging + network: staging diff --git a/examples/icp-network-connected/networks/staging.yaml b/examples/icp-network-connected/networks/staging.yaml index 2098c0387..be462a0ee 100644 --- a/examples/icp-network-connected/networks/staging.yaml +++ b/examples/icp-network-connected/networks/staging.yaml @@ -5,3 +5,4 @@ name: staging mode: connected url: https://icp.net +root-key: mainnet diff --git a/examples/icp-network-inline/README.md b/examples/icp-network-inline/README.md index b50d4a6c9..8cd4727b9 100644 --- a/examples/icp-network-inline/README.md +++ b/examples/icp-network-inline/README.md @@ -8,8 +8,8 @@ This project defines a `staging` network that is an alias for the mainnet. This ## Instructions -To deploy the canister to the `staging` network, run the following command: +This project also defines a `staging` environment that targets the `staging` network. To deploy the canister to it, run: ```bash -icp deploy --network staging +icp deploy --environment staging ``` diff --git a/examples/icp-network-inline/icp.yaml b/examples/icp-network-inline/icp.yaml index 959d893be..fe73080c4 100644 --- a/examples/icp-network-inline/icp.yaml +++ b/examples/icp-network-inline/icp.yaml @@ -13,3 +13,8 @@ networks: - name: staging mode: connected url: https://icp.net + root-key: mainnet + +environments: + - name: staging + network: staging