Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5adbeb3
initial impl
adamspofford-dfinity Apr 1, 2026
592c7b5
fix canister signature code
adamspofford-dfinity Apr 1, 2026
a97a46e
Shortcode flow
adamspofford-dfinity Apr 2, 2026
447dcd5
mv
adamspofford-dfinity Apr 2, 2026
8d73890
Automatically open the URL
adamspofford-dfinity Apr 2, 2026
961cb7a
Switch to keyring storage
adamspofford-dfinity Apr 7, 2026
615130d
New flow and fixed mainnet canister
adamspofford-dfinity Apr 9, 2026
543a252
Add storage mode parameter
adamspofford-dfinity Apr 10, 2026
fdbc4e7
Add .well-known record and host param
adamspofford-dfinity Apr 10, 2026
a15ce47
clippy
adamspofford-dfinity Apr 10, 2026
605d923
move out of operations module
adamspofford-dfinity Apr 10, 2026
e3f2117
fix lockfile
adamspofford-dfinity Apr 15, 2026
390b651
hide commands for beta release and use final domain
adamspofford-dfinity Apr 17, 2026
8c4b7d6
accept domains without scheme
adamspofford-dfinity Apr 17, 2026
5a3f3fb
Check for duplicate name before beginning
adamspofford-dfinity Apr 17, 2026
f6738e3
audit
adamspofford-dfinity Apr 17, 2026
0c23c62
stuff
adamspofford-dfinity Apr 21, 2026
3ed7c68
lint
adamspofford-dfinity Apr 21, 2026
1dc6bd6
artefact of earlier design
adamspofford-dfinity Apr 22, 2026
be083b3
bump ic-agent
adamspofford-dfinity Apr 22, 2026
0f63184
Merge branch 'main' into spofford/ii-login-poc
adamspofford-dfinity Apr 22, 2026
735861d
[copilot] unit tests for to_agent_types
adamspofford-dfinity Apr 22, 2026
1ec57ee
fix copilot hallucinations
adamspofford-dfinity Apr 22, 2026
ab38aac
validate password
adamspofford-dfinity Apr 22, 2026
620d85c
DRY pem parse
adamspofford-dfinity Apr 22, 2026
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
112 changes: 111 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ assert_cmd = "2"
async-dropper = { version = "0.3.0", features = ["tokio", "simple"] }
async-trait = "0.1.88"
axoupdater = "0.10.0"
axum = "0.8"
base64 = "0.22"
backoff = { version = "0.4", features = ["tokio"] }
bigdecimal = "0.4.10"
bip32 = "0.5.0"
Expand Down Expand Up @@ -67,6 +69,7 @@ mockall = "0.14.0"
nix = { version = "0.31.2", features = ["process", "signal"] }
notify = "8.2.0"
num-bigint = "0.4.6"
open = "5"
num-integer = "0.1.46"
num-traits = "0.2.19"
p256 = { version = "0.13.2", features = ["pem", "pkcs8", "std"] }
Expand Down
3 changes: 3 additions & 0 deletions crates/icp-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ anstyle.workspace = true
anyhow.workspace = true
async-trait.workspace = true
axoupdater.workspace = true
axum.workspace = true
backoff.workspace = true
base64.workspace = true
bigdecimal.workspace = true
bip32.workspace = true
byte-unit.workspace = true
Expand Down Expand Up @@ -47,6 +49,7 @@ lazy_static.workspace = true
num-bigint.workspace = true
num-integer.workspace = true
num-traits.workspace = true
open.workspace = true
p256.workspace = true
pem.workspace = true
phf.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion crates/icp-cli/src/commands/identity/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ pub(crate) async fn exec(ctx: &Context, args: &ImportArgs) -> Result<(), anyhow:
unreachable!();
}

info!("Identity \"{}\" created", args.name);
info!("Identity `{}` created", args.name);

if matches!(args.storage, StorageMode::Plaintext) {
warn!(
Expand Down
2 changes: 1 addition & 1 deletion crates/icp-cli/src/commands/identity/link/hsm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pub(crate) async fn exec(ctx: &Context, args: &HsmArgs) -> Result<(), HsmError>
.await?
.context(LinkHsmSnafu)?;

info!("Identity \"{}\" linked to HSM", args.name);
info!("Identity `{}` linked to HSM", args.name);

Ok(())
}
Expand Down
106 changes: 106 additions & 0 deletions crates/icp-cli/src/commands/identity/link/ii.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use clap::Args;
use dialoguer::Password;
use elliptic_curve::zeroize::Zeroizing;
use ic_agent::{Identity as _, export::Principal, identity::BasicIdentity};
use icp::{context::Context, fs::read_to_string, identity::key, prelude::*};
use snafu::{ResultExt, Snafu};
use tracing::{info, warn};

use crate::{commands::identity::StorageMode, operations::ii_poll};

/// Link an Internet Identity to a new identity
Comment thread
raymondk marked this conversation as resolved.
#[derive(Debug, Args)]
pub(crate) struct IiArgs {
/// Name for the linked identity
name: String,

/// Where to store the session private key
#[arg(long, value_enum, default_value_t)]
storage: StorageMode,

Comment thread
adamspofford-dfinity marked this conversation as resolved.
/// Read the storage password from a file instead of prompting (for --storage password)
#[arg(long, value_name = "FILE")]
storage_password_file: Option<PathBuf>,
}

pub(crate) async fn exec(ctx: &Context, args: &IiArgs) -> Result<(), IiError> {
let create_format = match args.storage {
StorageMode::Plaintext => key::CreateFormat::Plaintext,
StorageMode::Keyring => key::CreateFormat::Keyring,
StorageMode::Password => {
let password = if let Some(path) = &args.storage_password_file {
read_to_string(path)
.context(ReadStoragePasswordFileSnafu)?
.trim()
.to_string()
} else {
Password::new()
.with_prompt("Enter password to encrypt identity")
.with_confirmation("Confirm password", "Passwords do not match")
.interact()
.context(StoragePasswordTermReadSnafu)?
};
key::CreateFormat::Pbes2 {
password: Zeroizing::new(password),
}
}
Comment thread
adamspofford-dfinity marked this conversation as resolved.
};

let secret_key = ic_ed25519::PrivateKey::generate();
let identity_key = key::IdentityKey::Ed25519(secret_key.clone());
let basic = BasicIdentity::from_raw_key(&secret_key.serialize_raw());
let der_public_key = basic.public_key().expect("ed25519 always has a public key");

let chain = ii_poll::poll_for_delegation(&der_public_key)
.await
.context(PollSnafu)?;

let from_key = hex::decode(&chain.public_key).context(DecodeFromKeySnafu)?;
let ii_principal = Principal::self_authenticating(&from_key);

ctx.dirs
.identity()?
.with_write(async |dirs| {
key::link_ii_identity(
dirs,
&args.name,
identity_key,
&chain,
ii_principal,
create_format,
)
})
.await?
.context(LinkSnafu)?;

info!("Identity `{}` linked to Internet Identity", args.name);

if matches!(args.storage, StorageMode::Plaintext) {
warn!(
"This identity is stored in plaintext and is not secure. Do not use it for anything of significant value."
);
}

Ok(())
}

#[derive(Debug, Snafu)]
pub(crate) enum IiError {
#[snafu(display("failed to read storage password file"))]
ReadStoragePasswordFile { source: icp::fs::IoError },

#[snafu(display("failed to read storage password from terminal"))]
StoragePasswordTermRead { source: dialoguer::Error },

#[snafu(display("failed during II authentication"))]
Poll { source: ii_poll::IiPollError },

#[snafu(display("invalid public key in delegation chain"))]
DecodeFromKey { source: hex::FromHexError },

#[snafu(transparent)]
LockIdentityDir { source: icp::fs::lock::LockError },

#[snafu(display("failed to link II identity"))]
Link { source: key::LinkIiIdentityError },
}
2 changes: 2 additions & 0 deletions crates/icp-cli/src/commands/identity/link/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use clap::Subcommand;

pub(crate) mod hsm;
pub(crate) mod ii;

/// Link an external key to a new identity
#[derive(Debug, Subcommand)]
pub(crate) enum Command {
Hsm(hsm::HsmArgs),
Ii(ii::IiArgs),
}
Loading
Loading