Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Unreleased

* feat: Password-protected identities now only need your password once every five minutes (configurable)
Comment thread
adamspofford-dfinity marked this conversation as resolved.
Outdated
* feat: `icp identity delegation request/sign/use` now permit creating and importing identity delegations
* feat: `icp identity import` now takes `--seed-curve`, for seed phrases for non-k256 keys.
* fix: `icp canister settings show` now outputs only the canister settings, consistent with the command name
Expand Down
152 changes: 110 additions & 42 deletions crates/icp-cli/src/commands/identity/login.rs
Original file line number Diff line number Diff line change
@@ -1,92 +1,155 @@
use std::time::Duration;

use clap::Args;
use dialoguer::Password;
use icp::{
context::Context,
identity::{
key,
manifest::{IdentityList, IdentitySpec},
manifest::{IdentityList, IdentitySpec, PemFormat},
},
settings::Settings,
};
use snafu::{OptionExt, ResultExt, Snafu};
use tracing::info;

use crate::commands::identity::link::ii;
use crate::commands::identity::{delegation::sign::DurationArg, link::ii};

/// Re-authenticate an Internet Identity delegation
/// Re-authenticate an Internet Identity delegation or create a PEM session delegation
#[derive(Debug, Args)]
pub(crate) struct LoginArgs {
/// Name of the identity to re-authenticate
name: String,

/// Session delegation duration (e.g. "30m", "8h", "1d"). Note that 5m extra is
/// added when creating the delegation to account for clock drift.
/// Required for PEM identities when session caching is disabled in settings.
/// Not applicable for Internet Identity (yet).
#[arg(long)]
duration: Option<DurationArg>,
}

pub(crate) async fn exec(ctx: &Context, args: &LoginArgs) -> Result<(), LoginError> {
let (algorithm, storage, host) = ctx
let spec = ctx
.dirs
.identity()?
.with_read(async |dirs| {
let list = IdentityList::load_from(dirs)?;
let spec = list
.identities
list.identities
.get(&args.name)
.context(IdentityNotFoundSnafu { name: &args.name })?;
match spec {
IdentitySpec::InternetIdentity {
algorithm,
storage,
host,
..
} => Ok((algorithm.clone(), *storage, host.clone())),
_ => NotIiSnafu { name: &args.name }.fail(),
}
.cloned()
.context(IdentityNotFoundSnafu { name: &args.name })
})
.await??;

let der_public_key = ctx
.dirs
.identity()?
.with_read(async |dirs| {
key::load_ii_session_public_key(dirs, &args.name, &algorithm, &storage, || {
Password::new()
.with_prompt("Enter identity password")
.interact()
.map_err(|e| e.to_string())
})
})
.await?
.context(LoadSessionKeySnafu)?;
match spec {
IdentitySpec::InternetIdentity {
algorithm,
storage,
host,
..
} => {
if args.duration.is_some() {
return DurationSnafu { name: &args.name }.fail();
}

let chain = ii::recv_delegation(&host, &der_public_key)
.await
.context(PollSnafu)?;
let password_func = ctx.password_func.clone();
let der_public_key = ctx
.dirs
.identity()?
.with_read(async |dirs| {
key::load_ii_session_public_key(dirs, &args.name, &algorithm, &storage, || {
password_func()
})
})
.await?
.context(LoadSessionKeySnafu)?;

ctx.dirs
.identity()?
.with_write(async |dirs| key::update_ii_delegation(dirs, &args.name, &chain))
.await?
.context(UpdateDelegationSnafu)?;
let chain = ii::recv_delegation(&host, &der_public_key)
.await
.context(PollSnafu)?;

ctx.dirs
.identity()?
.with_write(async |dirs| key::update_ii_delegation(dirs, &args.name, &chain))
.await?
.context(UpdateDelegationSnafu)?;

info!("Identity `{}` re-authenticated", args.name);
info!("Identity `{}` re-authenticated", args.name);
}

IdentitySpec::Pem {
format: PemFormat::Pbes2,
algorithm,
..
} => {
let duration = match &args.duration {
Some(d) => Duration::from_nanos(d.as_nanos()) + Duration::from_secs(5 * 60),
None => {
let settings = ctx
.dirs
.settings()?
.with_read(async |dirs| Settings::load_from(dirs))
.await??;
settings
.session_length
.map(|m| Duration::from_secs((u64::from(m) + 5) * 60))
.context(DurationRequiredSnafu { name: &args.name })?
}
};

let password_func = ctx.password_func.clone();
ctx.dirs
.identity()?
.with_read(async |dirs| {
key::create_explicit_pem_session(
dirs,
&args.name,
&algorithm,
|| password_func(),
duration,
)
})
.await?
.context(CreatePemSessionSnafu)?;

info!("Session delegation created for identity `{}`", args.name);
}
_ => {
return UnsupportedIdentityTypeSnafu { name: &args.name }.fail();
}
}

Ok(())
}

#[derive(Debug, Snafu)]
pub(crate) enum LoginError {
#[snafu(transparent)]
LockIdentityDir { source: icp::fs::lock::LockError },
LockDir { source: icp::fs::lock::LockError },

#[snafu(transparent)]
LoadManifest {
source: icp::identity::manifest::LoadIdentityManifestError,
},

#[snafu(transparent)]
LoadSettings {
source: icp::settings::LoadSettingsError,
},

#[snafu(display("no identity found with name `{name}`"))]
IdentityNotFound { name: String },

#[snafu(display("`--duration` cannot be used with Internet Identity `{name}`"))]
Duration { name: String },

#[snafu(display(
"identity `{name}` is not an Internet Identity; use `icp identity link ii` instead"
"session caching is disabled; specify `--duration` to create a session delegation for `{name}`"
))]
NotIi { name: String },
DurationRequired { name: String },

#[snafu(display("identity `{name}` does not support logins"))]
UnsupportedIdentityType { name: String },

#[snafu(display("failed to load II session key"))]
LoadSessionKey { source: key::LoadIdentityError },
Expand All @@ -98,4 +161,9 @@ pub(crate) enum LoginError {
UpdateDelegation {
source: key::UpdateIiDelegationError,
},

#[snafu(display("failed to create PEM session delegation"))]
CreatePemSession {
source: key::CreateExplicitPemSessionError,
},
}
1 change: 0 additions & 1 deletion crates/icp-cli/src/commands/identity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ pub(crate) enum Command {
#[command(subcommand)]
Link(link::Command),
List(list::ListArgs),
#[command(hide = true)] // todo remove when II login is out of beta
Login(login::LoginArgs),
New(new::NewArgs),
Principal(principal::PrincipalArgs),
Expand Down
71 changes: 71 additions & 0 deletions crates/icp-cli/src/commands/settings.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::{fmt, str::FromStr};

use clap::{Args, Subcommand};
use icp::{
context::Context,
Expand Down Expand Up @@ -28,6 +30,8 @@ enum Setting {
Telemetry(TelemetryArgs),
/// Enable or disable the CLI update check
UpdateCheck(UpdateCheckArgs),
/// Set the session length for password-protected PEM identities
SessionLength(SessionLengthArgs),
}

#[derive(Debug, Args)]
Expand All @@ -49,11 +53,52 @@ struct UpdateCheckArgs {
value: Option<UpdateCheck>,
}

#[derive(Debug, Args)]
struct SessionLengthArgs {
/// Set to `<N>m` (e.g. `5m`) or `disabled`. If omitted, prints the current value.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent suffix support with icp identity login --duration. Would be nice to support identical syntax

///
/// Note that due to clock drift, 5 minutes are added to the given value,
/// so `5m` produces a 10-minute-expiry delegation. `disabled` turns off
/// session caching entirely.
value: Option<SessionLengthValue>,
}

/// A session-length value: either `<N>m` (whole minutes) or `disabled`.
#[derive(Debug, Clone)]
pub struct SessionLengthValue(pub Option<u32>);

impl FromStr for SessionLengthValue {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "disabled" {
return Ok(Self(None));
}
let digits = s
.strip_suffix('m')
.ok_or_else(|| format!("expected `<N>m` or `disabled`, got `{s}`"))?;
let n: u32 = digits
.parse()
.map_err(|_| format!("expected a whole number before `m`, got `{digits}`"))?;
Ok(Self(Some(n)))
}
}

impl fmt::Display for SessionLengthValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
Some(n) => write!(f, "{n}m"),
None => write!(f, "disabled"),
}
}
}

pub(crate) async fn exec(ctx: &Context, args: &SettingsArgs) -> Result<(), anyhow::Error> {
match &args.setting {
Setting::Autocontainerize(sub_args) => exec_autocontainerize(ctx, sub_args).await,
Setting::Telemetry(sub_args) => exec_telemetry(ctx, sub_args).await,
Setting::UpdateCheck(sub_args) => exec_update_check(ctx, sub_args).await,
Setting::SessionLength(sub_args) => exec_session_length(ctx, sub_args).await,
}
}

Expand Down Expand Up @@ -143,3 +188,29 @@ async fn exec_update_check(ctx: &Context, args: &UpdateCheckArgs) -> Result<(),
}
}
}

async fn exec_session_length(ctx: &Context, args: &SessionLengthArgs) -> Result<(), anyhow::Error> {
let dirs = ctx.dirs.settings()?;

match &args.value {
Some(SessionLengthValue(value)) => {
let value = *value;
dirs.with_write(async |dirs| {
let mut settings = Settings::load_from(dirs.read())?;
settings.session_length = value;
settings.write_to(dirs)?;
info!("Set session-length to {}", SessionLengthValue(value));
Ok(())
})
.await?
}

None => {
let settings = dirs
.with_read(async |dirs| Settings::load_from(dirs))
.await??;
println!("{}", SessionLengthValue(settings.session_length));
Ok(())
}
}
}
25 changes: 21 additions & 4 deletions crates/icp-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::sync::Arc;

use anyhow::Error;
use clap::{CommandFactory, Parser};
use commands::Command;
use icp::prelude::*;
use icp::{directories::Access, prelude::*};
use tracing::{Instrument, debug, info, subscriber::set_global_default, trace_span};
use tracing_subscriber::{Registry, layer::SubscriberExt};

Expand Down Expand Up @@ -140,19 +142,34 @@ async fn main() -> Result<(), Error> {
);

let password_func: icp::identity::PasswordFunc = match cli.identity_password_file {
Some(path) => Box::new(move || {
Some(path) => Arc::new(move || {
icp::fs::read_to_string(&path)
.map(|s| s.trim().to_string())
.map_err(|e| e.to_string())
}),
None => Box::new(|| {
None => Arc::new(|| {
dialoguer::Password::new()
.with_prompt("Enter identity password")
.interact()
.map_err(|e| e.to_string())
}),
};
let ctx = icp::context::initialize(cli.project_root_override, cli.debug, password_func)?;
let pem_session_duration = {
let dirs = icp::directories::Directories::new()?;
let settings_dirs = dirs.settings()?;
let settings = settings_dirs
.with_read(async |dirs| icp::settings::Settings::load_from(dirs))
.await??;
settings
.session_length
.map(|m| std::time::Duration::from_secs((u64::from(m) + 5) * 60))
};
let ctx = icp::context::initialize(
cli.project_root_override,
cli.debug,
password_func,
pem_session_duration,
)?;

let telemetry_session = telemetry::setup(&ctx, &raw_args, &Cli::command()).await;

Expand Down
Loading
Loading