Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ bump. Currently experimental: sync plugins.

# Unreleased

* feat: `icp identity import` can now be used with a `--delegation` flag to import a delegated identity. This is most useful for containers or other internal-only delegations; for anything involving a network, `icp identity delegation request` remains the recommended way to work with delegations.

# v1.0.0

* feat: The default gateway domain is now `icp.net`, not `icp0.io`.
Expand Down
45 changes: 40 additions & 5 deletions crates/icp-cli/src/commands/identity/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ use clap::{ArgGroup, Args};
use dialoguer::Password;
use elliptic_curve::zeroize::Zeroizing;
use icp::identity::{
delegation::DelegationChain,
key::{CreateFormat, CreateIdentityError, IdentityKey, create_identity},
manifest::IdentityKeyAlgorithm,
seed::derive_key_from_seed_slip10,
};
use icp::{fs::read_to_string, prelude::*};
use icp::{
fs::{json, read_to_string},
prelude::*,
};
use itertools::Itertools;
use k256::Secp256k1;
use p256::NistP256;
Expand Down Expand Up @@ -65,6 +69,12 @@ pub(crate) struct ImportArgs {
/// Curve for SLIP-0010 key derivation from a seed phrase
#[arg(long, value_enum, default_value_t = IdentityKeyAlgorithm::Secp256k1, requires = "seed")]
seed_curve: IdentityKeyAlgorithm,

/// Attach a signed delegation chain JSON (same format as `icp identity delegation sign`).
///
/// For security, in most cases it is better to use `icp identity delegation` instead.
#[arg(long, value_name = "FILE")]
delegation: Option<PathBuf>,
}

pub(crate) async fn exec(ctx: &Context, args: &ImportArgs) -> Result<(), anyhow::Error> {
Expand All @@ -89,6 +99,12 @@ pub(crate) async fn exec(ctx: &Context, args: &ImportArgs) -> Result<(), anyhow:
}
}
};
let chain = args
.delegation
.as_deref()
.map(json::load::<DelegationChain>)
.transpose()?;

if let Some(from_pem) = &args.from_pem {
import_from_pem(
ctx,
Expand All @@ -97,18 +113,35 @@ pub(crate) async fn exec(ctx: &Context, args: &ImportArgs) -> Result<(), anyhow:
args.decryption_password_from_file.as_deref(),
args.assert_key_type.clone(),
format,
chain.as_ref(),
)
.await?;
} else if let Some(path) = &args.from_seed_file {
let phrase = read_to_string(path).context(ReadSeedFileSnafu)?;
import_from_seed_phrase(ctx, &args.name, &phrase, args.seed_curve.clone(), format).await?;
import_from_seed_phrase(
ctx,
&args.name,
&phrase,
args.seed_curve.clone(),
format,
chain.as_ref(),
)
.await?;
} else if args.read_seed_phrase {
let phrase = Password::new()
.with_prompt("Enter seed phrase")
.with_confirmation("Re-enter seed phrase", "Seed phrases do not match")
.interact()
.context(ReadSeedPhraseFromTerminalSnafu)?;
import_from_seed_phrase(ctx, &args.name, &phrase, args.seed_curve.clone(), format).await?;
import_from_seed_phrase(
ctx,
&args.name,
&phrase,
args.seed_curve.clone(),
format,
chain.as_ref(),
)
.await?;
} else {
unreachable!();
}
Expand All @@ -131,6 +164,7 @@ async fn import_from_pem(
decryption_password_file: Option<&Path>,
known_key_type: Option<IdentityKeyAlgorithm>,
format: CreateFormat,
delegation: Option<&DelegationChain>,
) -> Result<(), LoadKeyError> {
// the pem file may be in SEC1 format or PKCS#8 format
// - if SEC1, the key algorithm can be embedded, separate, or missing
Expand Down Expand Up @@ -183,7 +217,7 @@ async fn import_from_pem(

ctx.dirs
.identity()?
.with_write(async move |dirs| create_identity(dirs, name, key, format))
.with_write(async move |dirs| create_identity(dirs, name, key, format, delegation))
.await??;

Ok(())
Expand Down Expand Up @@ -380,12 +414,13 @@ async fn import_from_seed_phrase(
phrase: &str,
algorithm: IdentityKeyAlgorithm,
format: CreateFormat,
delegation: Option<&DelegationChain>,
) -> Result<(), DeriveKeyError> {
let mnemonic = Mnemonic::from_phrase(phrase, Language::English).context(ParseMnemonicSnafu)?;
let key = derive_key_from_seed_slip10(&mnemonic, &algorithm);
ctx.dirs
.identity()?
.with_write(async move |dirs| create_identity(dirs, name, key, format))
.with_write(async move |dirs| create_identity(dirs, name, key, format, delegation))
.await??;
Ok(())
}
Expand Down
1 change: 1 addition & 0 deletions crates/icp-cli/src/commands/identity/new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ pub(crate) async fn exec(ctx: &Context, args: &NewArgs) -> Result<(), anyhow::Er
&args.name,
derive_key_from_seed_slip10(&mnemonic, &IdentityKeyAlgorithm::Secp256k1),
format,
None,
)
})
.await??;
Expand Down
4 changes: 4 additions & 0 deletions crates/icp-cli/tests/assets/session_p256.pub.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8ml7yIPIr2DlV38RXoats0N55Rxs
yzkLwFOQHqVgQ0lFXdi/XxNgWgrJX3zL8m3BTNS2SwaqIUtllkRLrgcqnQ==
-----END PUBLIC KEY-----
118 changes: 118 additions & 0 deletions crates/icp-cli/tests/identity_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1249,6 +1249,124 @@ fn identity_link_hsm_rename() {
assert_eq!(principal_before_str, principal_after_str);
}

#[test]
fn identity_import_delegation() {
let ctx = TestContext::new();

// Import the root identity that signs the delegation.
ctx.icp()
.args(["identity", "import", "root-identity", "--from-pem"])
.arg(ctx.make_asset("decrypted_sec1_k256.pem"))
.assert()
.success();

// Sign a delegation from root-identity to the session key whose private half lives in
// `decrypted_sec1_p256.pem` (its SPKI public key is `session_p256.pub.pem`).
let sign_output = ctx
.icp()
.args([
"identity",
"delegation",
"sign",
"--identity",
"root-identity",
"--key-pem",
])
.arg(ctx.make_asset("session_p256.pub.pem"))
.args(["--duration", "1d"])
.assert()
.success();
let chain_json_file = ctx.home_path().join("delegation-chain.json");
std::fs::write(&chain_json_file, &sign_output.get_output().stdout).unwrap();

// Import the session key and attach the chain in a single step.
ctx.icp()
.args([
"identity",
"import",
"delegated-identity",
"--storage",
"plaintext",
"--from-pem",
])
.arg(ctx.make_asset("decrypted_sec1_p256.pem"))
.arg("--delegation")
.arg(&chain_json_file)
.assert()
.success();

// The delegated identity presents the root's principal (chains are rooted at the signer),
// and loads successfully (validating the attached chain and session key).
let root_principal = str::from_utf8(
&ctx.icp()
.args(["identity", "principal", "--identity", "root-identity"])
.assert()
.success()
.get_output()
.stdout,
)
.unwrap()
.trim()
.to_string();
ctx.icp()
.args(["identity", "principal", "--identity", "delegated-identity"])
.assert()
.success()
.stdout(eq(root_principal).trim());

// Importing a key that is not the chain's session key must be rejected.
ctx.icp()
.args([
"identity",
"import",
"wrong-key",
"--storage",
"plaintext",
"--from-pem",
])
.arg(ctx.make_asset("decrypted_sec1_k256.pem"))
.arg("--delegation")
.arg(&chain_json_file)
.assert()
.failure()
.stderr(contains("does not match the session key"));

// A chain whose signature does not verify must be rejected at import, not persisted to
// fail on every later load. Tamper with the (correctly-keyed) chain's signature.
let mut chain: serde_json::Value =
serde_json::from_slice(&sign_output.get_output().stdout).unwrap();
let sig = chain["delegations"][0]["signature"].as_str().unwrap();
let tampered_sig: String = sig
.chars()
.enumerate()
.map(|(i, c)| {
if i == 0 {
if c == 'a' { 'b' } else { 'a' }
} else {
c
}
})
.collect();
chain["delegations"][0]["signature"] = serde_json::Value::String(tampered_sig);
let tampered_chain_file = ctx.home_path().join("tampered-chain.json");
std::fs::write(&tampered_chain_file, serde_json::to_vec(&chain).unwrap()).unwrap();

ctx.icp()
.args([
"identity",
"import",
"tampered",
"--storage",
"plaintext",
"--from-pem",
])
.arg(ctx.make_asset("decrypted_sec1_p256.pem"))
.arg("--delegation")
.arg(&tampered_chain_file)
.assert()
.failure();
}

#[cfg(unix)] // moc
#[tokio::test]
async fn identity_delegation_whoami() {
Expand Down
Loading
Loading