diff --git a/Cargo.lock b/Cargo.lock index ef6aa16a8e..44ab86cb84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1010,7 +1010,6 @@ dependencies = [ "hex", "hotwatch", "ic-agent", - "ic-identity-manager", "ic-types", "indicatif", "lazy-init", @@ -1018,15 +1017,17 @@ dependencies = [ "libflate", "mockall", "mockito", + "pem 0.7.0", "petgraph", "proptest", "rand 0.7.3", "regex", "reqwest", + "ring", "semver", "serde", "serde_bytes", - "serde_cbor 0.11.1", + "serde_cbor", "serde_json", "serde_repr", "shell-words", @@ -1037,6 +1038,7 @@ dependencies = [ "sysinfo", "tar", "tempfile", + "thiserror", "tokio", "toml", "url 2.1.1", @@ -1692,25 +1694,12 @@ dependencies = [ "rustls", "serde", "serde_bytes", - "serde_cbor 0.11.1", + "serde_cbor", "thiserror", "url 2.1.1", "webpki-roots 0.20.0", ] -[[package]] -name = "ic-identity-manager" -version = "0.6.6" -dependencies = [ - "ic-agent", - "ic-types", - "openssl", - "pem 0.7.0", - "ring", - "serde", - "serde_cbor 0.10.2", -] - [[package]] name = "ic-types" version = "0.1.0" @@ -3124,17 +3113,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_cbor" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7081ed758ec726a6ed8ee7e92f5d3f6e6f8c3901b1f972e3a4a2f2599fad14f" -dependencies = [ - "byteorder", - "half", - "serde", -] - [[package]] name = "serde_cbor" version = "0.11.1" diff --git a/Cargo.toml b/Cargo.toml index c6e1df3b7c..23380a657d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,4 @@ [workspace] members = [ "src/dfx", - "src/ic_identity_manager", ] diff --git a/docs/process/release.adoc b/docs/process/release.adoc index e1420990ca..76830e83c8 100644 --- a/docs/process/release.adoc +++ b/docs/process/release.adoc @@ -248,11 +248,10 @@ export NEW_DFX_VERSION= ---- git switch -c $USER/release-$NEW_DFX_VERSION ---- -. Update the `version` field for the following files: +. Update the `version` field in this file: + .... `src/dfx/Cargo.toml` -`src/ic_identity_manager/Cargo.toml` .... . Apply these changes to `Cargo.lock` by running the following command: + diff --git a/e2e/bats/assets/identity/identity.mo b/e2e/bats/assets/identity/identity.mo index ac1e634acd..b357e8db82 100644 --- a/e2e/bats/assets/identity/identity.mo +++ b/e2e/bats/assets/identity/identity.mo @@ -1,6 +1,9 @@ import P "mo:base/Principal"; +import Prim "mo:prim"; actor Self { + private let initializer : Principal = Prim.caller(); + public shared(msg) func fromCall(): async Principal { msg.caller }; @@ -12,5 +15,9 @@ actor Self { }; public query func isMyself(id: Principal) : async Bool { id == P.fromActor(Self) - }; + }; + + public shared query(msg) func amInitializer() : async Bool { + msg.caller == initializer + }; }; diff --git a/e2e/bats/identity.bash b/e2e/bats/identity.bash index 1d6aa0dec4..87495e1ac9 100644 --- a/e2e/bats/identity.bash +++ b/e2e/bats/identity.bash @@ -6,11 +6,16 @@ setup() { # We want to work from a temporary directory, different for every test. cd $(mktemp -d -t dfx-e2e-XXXXXXXX) + # Each test gets its own home directory in order to have its own identities. + mkdir $(pwd)/home-for-test + export HOME=$(pwd)/home-for-test + dfx_new } teardown() { dfx_stop + rm -rf $(pwd)/home-for-test } @test "calls and query receive the same principal from dfx" { @@ -32,3 +37,73 @@ teardown() { assert_command dfx canister call e2e_project isMyself "$ID_CALL" assert_eq '(false)' } + +@test "dfx ping creates the default identity on first run" { + install_asset identity + dfx_start + assert_command dfx ping + assert_match 'Creating the "default" identity.' "$stderr" + assert_match "ic_api_version" "$stdout" +} + +@test "dfx canister: creates the default identity on first run" { + install_asset identity + dfx_start + assert_command dfx canister create e2e_project + assert_match 'Creating the "default" identity.' "$stderr" +} + +@test "after using a specific identity while creating a canister, that identity is the initializer" { + install_asset identity + dfx_start + assert_command dfx identity new alice + assert_command dfx identity new bob + + dfx --identity alice canister create --all + assert_command dfx --identity alice build + assert_command dfx --identity alice canister install --all + + assert_command dfx --identity alice canister call e2e_project amInitializer + assert_eq '(true)' + + assert_command dfx --identity bob canister call e2e_project amInitializer + assert_eq '(false)' + + # these all fail (other identities are not initializer; cannot store assets): + assert_command_fail dfx --identity bob canister call e2e_project_assets store '("B", vec { 88; 87; 86; })' + assert_command_fail dfx --identity default canister call e2e_project_assets store '("B", vec { 88; 87; 86; })' + assert_command_fail dfx canister call e2e_project_assets store '("B", vec { 88; 87; 86; })' + assert_command_fail dfx canister call e2e_project_assets retrieve '("B")' + + # but alice, the initializer, can store assets: + assert_command dfx --identity alice canister call e2e_project_assets store '("B", vec { 88; 87; 86; })' + assert_eq '()' + assert_command dfx canister call e2e_project_assets retrieve '("B")' + assert_eq '(vec { 88; 87; 86; })' +} + +@test "after renaming an identity, the renamed identity is still initializer" { + install_asset identity + dfx_start + assert_command dfx identity new alice + + dfx --identity alice canister create --all + assert_command dfx --identity alice build + assert_command dfx --identity alice canister install --all + assert_command dfx --identity alice canister call e2e_project amInitializer + assert_eq '(true)' + assert_command dfx canister call e2e_project amInitializer + assert_eq '(false)' + + assert_command dfx identity rename alice bob + + assert_command dfx identity whoami + assert_eq 'default' + assert_command dfx --identity bob canister call e2e_project amInitializer + assert_eq '(true)' + + assert_command dfx --identity bob canister call e2e_project_assets store '("B", vec { 40; 67; })' + assert_eq '()' + assert_command dfx canister call e2e_project_assets retrieve '("B")' + assert_eq '(vec { 40; 67; })' +} diff --git a/e2e/bats/identity_command.bash b/e2e/bats/identity_command.bash new file mode 100644 index 0000000000..a6d56692a8 --- /dev/null +++ b/e2e/bats/identity_command.bash @@ -0,0 +1,270 @@ +#!/usr/bin/env bats + +load utils/_ + +setup() { + # We want to work from a temporary directory, different for every test. + export TEMPORARY_HOME=$(mktemp -d -t dfx-identity-home-XXXXXXXX) + export HOME=$TEMPORARY_HOME +} + +teardown() { + rm -rf $TEMPORARY_HOME +} + +## +## dfx identity list +## + +@test "identity list: shows identities in alpha order" { + assert_command dfx identity new dan + assert_command dfx identity new frank + assert_command dfx identity new alice + assert_command dfx identity new bob + assert_command dfx identity list + assert_match 'alice bob dan default frank' + assert_command dfx identity new charlie + assert_command dfx identity list + assert_match 'alice bob charlie dan default frank' +} + +@test "identity list: shows the anonymous identity" { + assert_command dfx identity list + # this should include anonymous, but we do not yet have support. + assert_eq 'default' "$stdout" +} + +@test "identity list: shows the default identity" { + assert_command dfx identity list + assert_match 'default' "$stdout" + assert_match 'Creating the "default" identity.' "$stderr" +} + +## +## dfx identity new +## + +@test "identity new: creates a new identity" { + assert_command dfx identity new alice + assert_command head $HOME/.config/dfx/identity/alice/identity.pem + assert_match "BEGIN PRIVATE KEY" + + # does not change the default identity + assert_command dfx identity whoami + assert_eq 'default' +} + +@test "identity new: cannot create an identity called anonymous" { + assert_command_fail dfx identity new anonymous +} + +@test "identity new: cannot create an identity that already exists" { + assert_command dfx identity new bob + assert_command_fail dfx identity new bob + assert_match "Identity already exists" +} + +## +## dfx identity remove +## + +@test "identity remove: can remove an identity that exists" { + assert_command_fail head $HOME/.config/dfx/identity/alice/identity.pem + assert_command dfx identity new alice + + assert_command head $HOME/.config/dfx/identity/alice/identity.pem + assert_match "BEGIN PRIVATE KEY" + assert_command dfx identity list + assert_match 'alice default' + + assert_command dfx identity remove alice + assert_command_fail cat $HOME/.config/dfx/identity/alice/identity.pem + + assert_command dfx identity list + assert_match 'default' +} + +@test "identity remove: reports an error if no such identity" { + assert_command_fail dfx identity remove charlie +} + +@test "identity remove: cannot remove the non-default active identity" { + assert_command dfx identity new alice + assert_command dfx identity use alice + assert_command_fail dfx identity remove alice + + assert_command head $HOME/.config/dfx/identity/alice/identity.pem + assert_match "BEGIN PRIVATE KEY" + assert_command dfx identity list + assert_match 'alice default' +} + +@test "identity remove: cannot remove the default identity" { + # a new one will just get created again + assert_command_fail dfx identity remove default + assert_match "Cannot delete the default identity" +} + + +@test "identity remove: cannot remove the anonymous identity" { + assert_command_fail dfx identity remove anonymous +} + + +## +## dfx identity rename +## + +@test "identity rename: can rename an identity" { + assert_command dfx identity new alice + assert_command dfx identity list + assert_match 'alice default' + assert_command head $HOME/.config/dfx/identity/alice/identity.pem + assert_match "BEGIN PRIVATE KEY" + local key=$(cat $HOME/.config/dfx/identity/alice/identity.pem) + + assert_command dfx identity rename alice bob + + assert_command dfx identity list + assert_match 'bob default' + assert_command cat $HOME/.config/dfx/identity/bob/identity.pem + assert_eq "$key" "$(cat $HOME/.config/dfx/identity/bob/identity.pem)" + assert_match "BEGIN PRIVATE KEY" + assert_command_fail cat $HOME/.config/dfx/identity/alice/identity.pem +} + +@test "identity rename: can rename the default identity, which also changes the default" { + assert_command dfx identity list + assert_match 'default' + assert_command dfx identity rename default bob + assert_command dfx identity list + assert_match 'bob' + assert_command head $HOME/.config/dfx/identity/bob/identity.pem + assert_match "BEGIN PRIVATE KEY" + + assert_command dfx identity whoami + assert_eq 'bob' +} + +@test "identity rename: can rename the selected identity, which also changes the default" { + assert_command dfx identity new alice + assert_command dfx identity use alice + assert_command dfx identity list + assert_match 'alice default' + assert_command dfx identity rename alice charlie + + assert_command dfx identity list + assert_match 'charlie default' + + assert_command dfx identity whoami + assert_eq 'charlie' + + assert_command head $HOME/.config/dfx/identity/charlie/identity.pem + assert_match "BEGIN PRIVATE KEY" + assert_command_fail cat $HOME/.config/dfx/identity/alice/identity.pem +} + +@test "identity rename: cannot create an anonymous identity via rename" { + assert_command dfx identity new alice + assert_command_fail dfx identity rename alice anonymous + assert_match "Cannot create an anonymous identity" +} + +## +## dfx identity use +## + +@test "identity use: switches to an existing identity" { + assert_command dfx identity new alice + assert_command dfx identity whoami + assert_eq 'default' + assert_command dfx identity use alice + assert_command dfx identity whoami + assert_eq 'alice' + + ## and back + assert_command dfx identity use default + assert_command dfx identity whoami + assert_eq 'default' +} + +@test "identity use: cannot use an identity that has not been created yet" { + assert_command_fail dfx identity use alice + assert_command dfx identity whoami + assert_eq 'default' +} + +@test "identity use: cannot switch to the anonymous identity" { + # this should actually succeed, but we do not yet have support for + # the anonymous identity. + assert_command_fail dfx identity use anonymous +} + +## +## dfx identity whoami +## + +@test "identity whoami: creates the default identity on first run" { + # Just an example. All the identity commands do this. + assert_command dfx identity whoami + assert_eq 'default' "$stdout" + assert_match 'Creating the "default" identity.' "$stderr" +} + +@test "identity whoami: shows the current identity" { + assert_command dfx identity whoami + assert_eq 'default' "$stdout" + assert_command dfx identity new charlie + assert_command dfx identity whoami + assert_eq 'default' + assert_command dfx identity use charlie + assert_command dfx identity whoami + assert_eq 'charlie' +} + +## dfx --identity (+other commands) + +@test "dfx --identity (name) identity whoami: shows the overriding identity" { + assert_command dfx identity whoami + assert_eq 'default' "$stdout" + assert_command dfx identity new charlie + assert_command dfx identity new alice + assert_command dfx --identity charlie identity whoami + assert_eq 'charlie' + assert_command dfx --identity alice identity whoami + assert_eq 'alice' +} + +@test "dfx --identity does not persistently change the selected identity" { + assert_command dfx identity whoami + assert_eq 'default' "$stdout" + assert_command dfx identity new charlie + assert_command dfx identity new alice + assert_command dfx identity use charlie + assert_command dfx identity whoami + assert_eq 'charlie' + assert_command dfx --identity alice identity whoami + assert_eq 'alice' + assert_command dfx identity whoami + assert_eq 'charlie' +} + +## +## Identity key migration +## +@test "identity manager copies existing key from ~/.dfinity/identity/creds.pem" { + assert_command dfx identity whoami + assert_command mkdir -p $TEMPORARY_HOME/.dfinity/identity + assert_command mv $TEMPORARY_HOME/.config/dfx/identity/default/identity.pem $TEMPORARY_HOME/.dfinity/identity/creds.pem + ORIGINAL_KEY=$(cat $TEMPORARY_HOME/.dfinity/identity/creds.pem) + assert_command rmdir $TEMPORARY_HOME/.config/dfx/identity/default + assert_command rmdir $TEMPORARY_HOME/.config/dfx/identity + assert_command rm $TEMPORARY_HOME/.config/dfx/identity.json + assert_command rmdir $TEMPORARY_HOME/.config/dfx + assert_command rmdir $TEMPORARY_HOME/.config + + assert_command dfx identity whoami + + assert_match "migrating key from" + assert_eq "$(cat $TEMPORARY_HOME/.config/dfx/identity/default/identity.pem)" "$ORIGINAL_KEY" +} diff --git a/src/dfx/Cargo.toml b/src/dfx/Cargo.toml index c72779accf..215bd673d5 100644 --- a/src/dfx/Cargo.toml +++ b/src/dfx/Cargo.toml @@ -32,16 +32,17 @@ erased-serde = "0.3.10" flate2 = "1.0.11" futures = "0.1.28" hex = "0.4.2" -ic-identity-manager = { path = "../ic_identity_manager" } indicatif = "0.13.0" lazy-init = "0.3.0" lazy_static = "1.4.0" libflate = "0.1.27" hotwatch = "0.4.3" mockall = "0.6.0" +pem = "0.7.0" petgraph = "0.5.0" rand = "0.7.2" regex = "1.3.1" +ring = "0.16.11" reqwest = { version = "0.10.4", features = [ "blocking", "json", "rustls-tls" ] } semver = "0.9.0" serde = "1.0" @@ -57,6 +58,7 @@ slog-term = "2.5.0" sysinfo = "0.9.6" tar = "0.4.26" tempfile = "3.1.0" +thiserror = "1.0.20" toml = "0.5.5" tokio = "0.2.10" url = "2.1.0" diff --git a/src/dfx/src/commands/identity/list.rs b/src/dfx/src/commands/identity/list.rs new file mode 100644 index 0000000000..680280bd3a --- /dev/null +++ b/src/dfx/src/commands/identity/list.rs @@ -0,0 +1,29 @@ +use crate::lib::environment::Environment; +use crate::lib::error::DfxResult; +use crate::lib::identity::identity_manager::IdentityManager; +use crate::lib::message::UserMessage; +use clap::{App, ArgMatches, SubCommand}; +use std::io::Write; + +pub fn construct() -> App<'static, 'static> { + SubCommand::with_name("list").about(UserMessage::ListIdentities.to_str()) +} + +pub fn exec(env: &dyn Environment, _args: &ArgMatches<'_>) -> DfxResult { + let mgr = IdentityManager::new(env)?; + let identities = mgr.get_identity_names()?; + let current_identity = mgr.get_selected_identity_name(); + for identity in identities { + if current_identity == &identity { + // same identity, suffix with '*'. + print!("{}", identity); + std::io::stdout().flush()?; + eprint!(" *"); + std::io::stderr().flush()?; + println!(); + } else { + println!("{}", identity); + } + } + Ok(()) +} diff --git a/src/dfx/src/commands/identity/mod.rs b/src/dfx/src/commands/identity/mod.rs new file mode 100644 index 0000000000..dae8478eed --- /dev/null +++ b/src/dfx/src/commands/identity/mod.rs @@ -0,0 +1,48 @@ +use crate::commands::CliCommand; +use crate::lib::environment::Environment; +use crate::lib::error::{DfxError, DfxResult}; +use crate::lib::message::UserMessage; +use clap::{App, ArgMatches, SubCommand}; + +mod list; +mod new; +mod remove; +mod rename; +mod r#use; +mod whoami; + +fn builtins() -> Vec { + vec![ + CliCommand::new(list::construct(), list::exec), + CliCommand::new(new::construct(), new::exec), + CliCommand::new(remove::construct(), remove::exec), + CliCommand::new(rename::construct(), rename::exec), + CliCommand::new(r#use::construct(), r#use::exec), + CliCommand::new(whoami::construct(), whoami::exec), + ] +} + +pub fn construct() -> App<'static, 'static> { + SubCommand::with_name("identity") + .about(UserMessage::ManageIdentity.to_str()) + .subcommands(builtins().into_iter().map(|x| x.get_subcommand().clone())) +} + +pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { + let subcommand = args.subcommand(); + + if let (name, Some(subcommand_args)) = subcommand { + match builtins().into_iter().find(|x| name == x.get_name()) { + Some(cmd) => cmd.execute(env, subcommand_args), + None => Err(DfxError::UnknownCommand(format!( + "Command {} not found.", + name + ))), + } + } else { + construct().write_help(&mut std::io::stderr())?; + eprintln!(); + eprintln!(); + Ok(()) + } +} diff --git a/src/dfx/src/commands/identity/new.rs b/src/dfx/src/commands/identity/new.rs new file mode 100644 index 0000000000..90fe4bc0b7 --- /dev/null +++ b/src/dfx/src/commands/identity/new.rs @@ -0,0 +1,26 @@ +use crate::lib::environment::Environment; +use crate::lib::error::DfxResult; +use crate::lib::identity::identity_manager::IdentityManager; +use crate::lib::message::UserMessage; +use clap::{App, Arg, ArgMatches, SubCommand}; +use slog::info; + +pub fn construct() -> App<'static, 'static> { + SubCommand::with_name("new") + .about(UserMessage::NewIdentity.to_str()) + .arg( + Arg::with_name("identity") + .help("The identity to create.") + .required(true) + .takes_value(true), + ) +} + +pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { + let name = args.value_of("identity").unwrap(); + + let log = env.get_logger(); + info!(log, r#"Creating identity: "{}"."#, name); + + IdentityManager::new(env)?.create_new_identity(name) +} diff --git a/src/dfx/src/commands/identity/remove.rs b/src/dfx/src/commands/identity/remove.rs new file mode 100644 index 0000000000..d9e324feb3 --- /dev/null +++ b/src/dfx/src/commands/identity/remove.rs @@ -0,0 +1,26 @@ +use crate::lib::environment::Environment; +use crate::lib::error::DfxResult; +use crate::lib::identity::identity_manager::IdentityManager; +use crate::lib::message::UserMessage; +use clap::{App, Arg, ArgMatches, SubCommand}; +use slog::info; + +pub fn construct() -> App<'static, 'static> { + SubCommand::with_name("remove") + .about(UserMessage::RemoveIdentity.to_str()) + .arg( + Arg::with_name("identity") + .help("The identity to remove.") + .required(true) + .takes_value(true), + ) +} + +pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { + let name = args.value_of("identity").unwrap(); + + let log = env.get_logger(); + info!(log, r#"Removing identity "{}"."#, name); + + IdentityManager::new(env)?.remove(name) +} diff --git a/src/dfx/src/commands/identity/rename.rs b/src/dfx/src/commands/identity/rename.rs new file mode 100644 index 0000000000..39054bea44 --- /dev/null +++ b/src/dfx/src/commands/identity/rename.rs @@ -0,0 +1,38 @@ +use crate::lib::environment::Environment; +use crate::lib::error::DfxResult; +use crate::lib::identity::identity_manager::IdentityManager; +use crate::lib::message::UserMessage; +use clap::{App, Arg, ArgMatches, SubCommand}; +use slog::info; + +pub fn construct() -> App<'static, 'static> { + SubCommand::with_name("rename") + .about(UserMessage::RenameIdentity.to_str()) + .arg( + Arg::with_name("from") + .help("The current name of the identity.") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("to") + .help("The new name of the identity.") + .required(true) + .takes_value(true), + ) +} + +pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { + let from = args.value_of("from").unwrap(); + let to = args.value_of("to").unwrap(); + + let log = env.get_logger(); + info!(log, r#"Renaming identity "{}" to "{}"."#, from, to); + + let renamed_default = IdentityManager::new(env)?.rename(from, to)?; + + if renamed_default { + info!(log, r#"Now using identity: "{}"."#, to); + } + Ok(()) +} diff --git a/src/dfx/src/commands/identity/use.rs b/src/dfx/src/commands/identity/use.rs new file mode 100644 index 0000000000..db3023d98b --- /dev/null +++ b/src/dfx/src/commands/identity/use.rs @@ -0,0 +1,26 @@ +use crate::lib::environment::Environment; +use crate::lib::error::DfxResult; +use crate::lib::identity::identity_manager::IdentityManager; +use crate::lib::message::UserMessage; +use clap::{App, Arg, ArgMatches, SubCommand}; +use slog::info; + +pub fn construct() -> App<'static, 'static> { + SubCommand::with_name("use") + .about(UserMessage::UseIdentity.to_str()) + .arg( + Arg::with_name("identity") + .help("The identity to use.") + .required(true) + .takes_value(true), + ) +} + +pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { + let identity = args.value_of("identity").unwrap(); + + let log = env.get_logger(); + info!(log, r#"Using identity: "{}"."#, identity); + + IdentityManager::new(env)?.use_identity_named(identity) +} diff --git a/src/dfx/src/commands/identity/whoami.rs b/src/dfx/src/commands/identity/whoami.rs new file mode 100644 index 0000000000..d88e72276f --- /dev/null +++ b/src/dfx/src/commands/identity/whoami.rs @@ -0,0 +1,16 @@ +use crate::lib::environment::Environment; +use crate::lib::error::DfxResult; +use crate::lib::identity::identity_manager::IdentityManager; +use crate::lib::message::UserMessage; +use clap::{App, ArgMatches, SubCommand}; + +pub fn construct() -> App<'static, 'static> { + SubCommand::with_name("whoami").about(UserMessage::ShowIdentity.to_str()) +} + +pub fn exec(env: &dyn Environment, _args: &ArgMatches<'_>) -> DfxResult { + let mgr = IdentityManager::new(env)?; + let identity = mgr.get_selected_identity_name(); + println!("{}", identity); + Ok(()) +} diff --git a/src/dfx/src/commands/mod.rs b/src/dfx/src/commands/mod.rs index e95522e839..5a6981b2e9 100644 --- a/src/dfx/src/commands/mod.rs +++ b/src/dfx/src/commands/mod.rs @@ -7,6 +7,7 @@ mod build; mod cache; mod canister; mod config; +mod identity; mod language_service; mod new; mod ping; @@ -47,6 +48,7 @@ pub fn builtin() -> Vec { CliCommand::new(cache::construct(), cache::exec), CliCommand::new(canister::construct(), canister::exec), CliCommand::new(config::construct(), config::exec), + CliCommand::new(identity::construct(), identity::exec), CliCommand::new(language_service::construct(), language_service::exec), CliCommand::new(new::construct(), new::exec), CliCommand::new(ping::construct(), ping::exec), diff --git a/src/dfx/src/commands/ping.rs b/src/dfx/src/commands/ping.rs index 1daee7e061..4d38ba5afb 100644 --- a/src/dfx/src/commands/ping.rs +++ b/src/dfx/src/commands/ping.rs @@ -37,7 +37,7 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { other => Err(other), })?; - let env = AgentEnvironment::new(env, network_descriptor); + let env = AgentEnvironment::new(env, network_descriptor)?; let agent = env .get_agent() diff --git a/src/dfx/src/config/cache.rs b/src/dfx/src/config/cache.rs index 5f1df59e13..f346175616 100644 --- a/src/dfx/src/config/cache.rs +++ b/src/dfx/src/config/cache.rs @@ -69,27 +69,6 @@ impl Cache for DiskBasedCache { } } -/// Provides a profile for the user. This is located outside the -/// ephemeral cache as the data here need to be persisted and strictly -/// a user expects to clear the .cache directory with minimal -/// consequences. -pub fn get_profile_path() -> DfxResult { - let home = std::env::var("HOME") - .map_err(|_| CacheError(CacheErrorKind::CannotFindUserHomeDirectory()))?; - - let p = PathBuf::from(home).join(".dfinity").join("identity"); - - if !p.exists() { - if let Err(e) = std::fs::create_dir_all(&p) { - return Err(CacheError(CacheErrorKind::CannotCreateCacheDirectory(p, e))); - } - } else if !p.is_dir() { - return Err(CacheError(CacheErrorKind::CacheShouldBeADirectory(p))); - } - - Ok(p) -} - pub fn get_cache_root() -> DfxResult { let home = std::env::var("HOME") .map_err(|_| CacheError(CacheErrorKind::CannotFindUserHomeDirectory()))?; diff --git a/src/dfx/src/lib/config.rs b/src/dfx/src/lib/config.rs new file mode 100644 index 0000000000..056d2c366a --- /dev/null +++ b/src/dfx/src/lib/config.rs @@ -0,0 +1,21 @@ +use crate::lib::error::{ConfigErrorKind, DfxError, DfxResult}; +use std::path::PathBuf; + +pub fn get_config_dfx_dir_path() -> DfxResult { + let home = std::env::var("HOME") + .map_err(|_| DfxError::ConfigError(ConfigErrorKind::CannotFindUserHomeDirectory()))?; + + let p = PathBuf::from(home).join(".config").join("dfx"); + + if !p.exists() { + std::fs::create_dir_all(&p).map_err(|e| { + DfxError::ConfigError(ConfigErrorKind::CouldNotCreateConfigDirectory(p.clone(), e)) + })?; + } else if !p.is_dir() { + return Err(DfxError::ConfigError( + ConfigErrorKind::HomeConfigDfxShouldBeADirectory(p), + )); + } + + Ok(p) +} diff --git a/src/dfx/src/lib/environment.rs b/src/dfx/src/lib/environment.rs index 9121147625..50b1a5ff8b 100644 --- a/src/dfx/src/lib/environment.rs +++ b/src/dfx/src/lib/environment.rs @@ -1,12 +1,12 @@ -use crate::config::cache::{get_profile_path, Cache, DiskBasedCache}; +use crate::config::cache::{Cache, DiskBasedCache}; use crate::config::dfinity::Config; use crate::config::{cache, dfx_version}; use crate::lib::error::{DfxError, DfxResult}; -use crate::lib::identity::Identity; +use crate::lib::identity::identity_manager::IdentityManager; use crate::lib::network::network_descriptor::NetworkDescriptor; use crate::lib::progress_bar::ProgressBar; -use ic_agent::{Agent, AgentConfig}; +use ic_agent::{Agent, AgentConfig, Identity}; use semver::Version; use slog::{Logger, Record}; use std::collections::BTreeMap; @@ -32,6 +32,10 @@ pub trait Environment { fn get_state_dir(&self) -> PathBuf; fn get_version(&self) -> &Version; + /// This is value of the name passed to dfx `--identity ` + /// Notably, it is _not_ the name of the default identity or selected identity + fn get_identity_override(&self) -> &Option; + // Explicit lifetimes are actually needed for mockall to work properly. #[allow(clippy::needless_lifetimes)] fn get_agent<'a>(&'a self) -> Option<&'a Agent>; @@ -60,6 +64,8 @@ pub struct EnvironmentImpl { logger: Option, progress: bool, + + identity_override: Option, } impl EnvironmentImpl { @@ -114,6 +120,7 @@ impl EnvironmentImpl { version: version.clone(), logger: None, progress: true, + identity_override: None, }) } @@ -126,6 +133,11 @@ impl EnvironmentImpl { self.progress = progress; self } + + pub fn with_identity_override(mut self, identity: Option) -> Self { + self.identity_override = identity; + self + } } impl Environment for EnvironmentImpl { @@ -153,6 +165,10 @@ impl Environment for EnvironmentImpl { &self.version } + fn get_identity_override(&self) -> &Option { + &self.identity_override + } + fn get_agent(&self) -> Option<&Agent> { // create an AgentEnvironment explicitly, in order to specify network and agent. // See install, build for examples. @@ -191,15 +207,19 @@ pub struct AgentEnvironment<'a> { } impl<'a> AgentEnvironment<'a> { - pub fn new(backend: &'a dyn Environment, network_descriptor: NetworkDescriptor) -> Self { - let identity = get_profile_path().expect("Failed to access profile"); + pub fn new( + backend: &'a dyn Environment, + network_descriptor: NetworkDescriptor, + ) -> DfxResult { + let identity = IdentityManager::new(backend)?.instantiate_selected_identity()?; + let agent_url = network_descriptor.providers.first().unwrap(); - AgentEnvironment { + Ok(AgentEnvironment { backend, agent: create_agent(backend.get_logger().clone(), agent_url, identity) .expect("Failed to construct agent."), network_descriptor, - } + }) } } @@ -228,6 +248,10 @@ impl<'a> Environment for AgentEnvironment<'a> { self.backend.get_version() } + fn get_identity_override(&self) -> &Option { + self.backend.get_identity_override() + } + fn get_agent(&self) -> Option<&Agent> { Some(&self.agent) } @@ -383,13 +407,13 @@ impl ic_agent::PasswordManager for AgentClient { } } -fn create_agent(logger: Logger, url: &str, identity: PathBuf) -> Option { +fn create_agent(logger: Logger, url: &str, identity: Box) -> Option { AgentClient::new(logger, url.to_string()) .ok() .and_then(|executor| { Agent::new(AgentConfig { url: url.to_string(), - identity: Box::new(Identity::new(identity)), + identity, password_manager: Some(Box::new(executor)), ..AgentConfig::default() }) diff --git a/src/dfx/src/lib/error/config.rs b/src/dfx/src/lib/error/config.rs new file mode 100644 index 0000000000..901ffa059d --- /dev/null +++ b/src/dfx/src/lib/error/config.rs @@ -0,0 +1,15 @@ +use std::io; +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ConfigErrorKind { + #[error("Cannot find the home directory.")] + CannotFindUserHomeDirectory(), + + #[error(r#"The configuration folder "{0}" should be a directory."#)] + HomeConfigDfxShouldBeADirectory(PathBuf), + + #[error(r#"Could not create the configuration folder at "{0}". Error: {1}"#)] + CouldNotCreateConfigDirectory(PathBuf, io::Error), +} diff --git a/src/dfx/src/lib/error/identity.rs b/src/dfx/src/lib/error/identity.rs new file mode 100644 index 0000000000..25b8527c14 --- /dev/null +++ b/src/dfx/src/lib/error/identity.rs @@ -0,0 +1,34 @@ +use ring::error::Unspecified; +use std::io; +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum IdentityErrorKind { + #[error("Identity already exists")] + IdentityAlreadyExists(), + + #[error("Identity {0} does not exist at {1}")] + IdentityDoesNotExist(String, PathBuf), + + #[error("Could not generate key")] + CouldNotGenerateKey(Unspecified), + + #[error(r#"Could not create the identity folder at "{0}". Error: {1}"#)] + CouldNotCreateIdentityDirectory(PathBuf, io::Error), + + #[error(r#"Could not rename identity directory {0} to {1}: {2}"#)] + CouldNotRenameIdentityDirectory(PathBuf, PathBuf, io::Error), + + #[error("Cannot delete the default identity")] + CannotDeleteDefaultIdentity(), + + #[error("Cannot create an anonymous identity")] + CannotCreateAnonymousIdentity(), + + #[error("Cannot find the home directory.")] + CannotFindUserHomeDirectory(), + + #[error("An error occurred while reading {1}: {0}")] + AgentPemError(ic_agent::PemError, PathBuf), +} diff --git a/src/dfx/src/lib/error/mod.rs b/src/dfx/src/lib/error/mod.rs index 7e7c8857fb..35a3f9d30a 100644 --- a/src/dfx/src/lib/error/mod.rs +++ b/src/dfx/src/lib/error/mod.rs @@ -3,9 +3,13 @@ use ic_types::principal::PrincipalError; mod build; mod cache; +mod config; +mod identity; pub use build::BuildErrorKind; pub use cache::CacheErrorKind; +pub use config::ConfigErrorKind; +pub use identity::IdentityErrorKind; use serde::export::Formatter; use std::ffi::OsString; use std::fmt::Display; @@ -21,6 +25,11 @@ pub enum DfxError { /// An error happened while managing the cache. CacheError(CacheErrorKind), + ConfigError(ConfigErrorKind), + + /// An error happened while managing identities. + IdentityError(IdentityErrorKind), + IdeError(String), Clap(clap::Error), @@ -146,6 +155,12 @@ impl Display for DfxError { DfxError::BuildError(err) => { f.write_fmt(format_args!("Build failed. Reason:\n {}", err))?; } + DfxError::ConfigError(err) => { + f.write_fmt(format_args!("Config error:\n {}", err))?; + } + DfxError::IdentityError(err) => { + f.write_fmt(format_args!("Identity error:\n {}", err))?; + } DfxError::IdeError(msg) => { f.write_fmt(format_args!( "The Motoko Language Server returned an error:\n{}", @@ -220,7 +235,6 @@ impl Display for DfxError { DfxError::CouldNotSaveCanisterIds(path, error) => { f.write_fmt(format_args!("Failed to save {} due to: {}", path, error))?; } - err => { f.write_fmt(format_args!("An error occured:\n{:#?}", err))?; } diff --git a/src/dfx/src/lib/identity.rs b/src/dfx/src/lib/identity.rs deleted file mode 100644 index 055cb82eaf..0000000000 --- a/src/dfx/src/lib/identity.rs +++ /dev/null @@ -1,64 +0,0 @@ -use ic_agent::Signature; -use ic_types::principal::Principal; -use std::path::PathBuf; - -pub struct Identity(ic_identity_manager::Identity); - -impl Identity { - /// Construct a new identity handling object, providing given - /// configuration. - pub fn new(identity_config_path: PathBuf) -> Self { - Self( - // We expect an identity profile to be provided. - ic_identity_manager::Identity::new(identity_config_path) - .expect("Expected a valid identity configuration"), - ) - } -} - -impl ic_agent::Identity for Identity { - fn sender(&self) -> Result { - Ok(self.0.sender()) - } - - fn sign(&self, blob: &[u8], _: &Principal) -> Result { - let signature_tuple = self.0.sign(blob).map_err(|e| e.to_string())?; - - let signature = signature_tuple.signature; - let public_key = signature_tuple.public_key; - Ok(Signature { - public_key, - signature, - }) - } -} - -#[cfg(test)] -mod test { - use super::*; - use ic_agent::{Identity, RequestId}; - use tempfile::tempdir; - - #[test] - fn request_id_identity() { - let dir = tempdir().unwrap(); - - let signer = super::Identity::new(dir.into_path()); - let sender = signer.sender().expect("Failed to get the sender."); - let msg = { - let domain_separator: &[u8] = b"\x0Aic-request"; - let request_id = RequestId::new(&[4; 32]); - let mut buf = vec![]; - buf.extend_from_slice(domain_separator); - buf.extend_from_slice(request_id.as_slice()); - buf - }; - let signature = signer.sign(&msg, &sender).expect("Failed to sign."); - - // Assert the principal is used for the public key. - assert_eq!( - sender, - Principal::self_authenticating(signature.public_key.as_slice()) - ); - } -} diff --git a/src/dfx/src/lib/identity/identity_manager.rs b/src/dfx/src/lib/identity/identity_manager.rs new file mode 100644 index 0000000000..666c37fbe6 --- /dev/null +++ b/src/dfx/src/lib/identity/identity_manager.rs @@ -0,0 +1,319 @@ +use crate::lib::config::get_config_dfx_dir_path; +use crate::lib::environment::Environment; +use crate::lib::error::{DfxError, DfxResult, IdentityErrorKind}; +use ic_agent::{BasicIdentity, Identity}; +use pem::{encode, Pem}; +use ring::{rand, signature}; +use serde::{Deserialize, Serialize}; +use slog::Logger; +use std::fs; +use std::path::{Path, PathBuf}; + +const DEFAULT_IDENTITY_NAME: &str = "default"; +const ANONYMOUS_IDENTITY_NAME: &str = "anonymous"; +const IDENTITY_PEM: &str = "identity.pem"; + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +struct Configuration { + #[serde(default = "default_identity")] + pub default: String, +} + +fn default_identity() -> String { + String::from(DEFAULT_IDENTITY_NAME) +} + +#[derive(Clone, Debug)] +pub struct IdentityManager { + identity_json_path: PathBuf, + identity_root_path: PathBuf, + configuration: Configuration, + selected_identity: String, +} + +impl IdentityManager { + pub fn new(env: &dyn Environment) -> DfxResult { + let config_dfx_dir_path = get_config_dfx_dir_path()?; + let identity_root_path = config_dfx_dir_path.join("identity"); + let identity_json_path = config_dfx_dir_path.join("identity.json"); + + let configuration = if identity_json_path.exists() { + read_configuration(&identity_json_path) + } else { + initialize(env.get_logger(), &identity_json_path, &identity_root_path) + }?; + + let identity_override = env.get_identity_override(); + let selected_identity = identity_override + .clone() + .unwrap_or_else(|| configuration.default.clone()); + + let mgr = IdentityManager { + identity_json_path, + identity_root_path, + configuration, + selected_identity, + }; + + if let Some(identity) = identity_override { + mgr.require_identity_exists(&identity)?; + } + + Ok(mgr) + } + + /// Create an Identity instance for use with an Agent + pub fn instantiate_selected_identity(&self) -> DfxResult> { + let pem_path = self.get_selected_identity_pem_path(); + let basic = BasicIdentity::from_pem_file(&pem_path).map_err(|e| { + DfxError::IdentityError(IdentityErrorKind::AgentPemError(e, pem_path.clone())) + })?; + + let b: Box = Box::new(basic); + Ok(b) + } + + /// Create a new identity (name -> generated key) + pub fn create_new_identity(&self, name: &str) -> DfxResult { + if name == ANONYMOUS_IDENTITY_NAME { + return Err(DfxError::IdentityError( + IdentityErrorKind::CannotCreateAnonymousIdentity(), + )); + } + let identity_dir = self.get_identity_dir_path(name); + + if identity_dir.exists() { + return Err(DfxError::IdentityError( + IdentityErrorKind::IdentityAlreadyExists(), + )); + } + std::fs::create_dir_all(&identity_dir).map_err(|e| { + DfxError::IdentityError(IdentityErrorKind::CouldNotCreateIdentityDirectory( + identity_dir.clone(), + e, + )) + })?; + + let pem_file = identity_dir.join(IDENTITY_PEM); + generate_key(&pem_file) + } + + /// Return a sorted list of all available identity names + pub fn get_identity_names(&self) -> DfxResult> { + let mut names = self + .identity_root_path + .read_dir()? + .filter(|entry_result| match entry_result { + Ok(dir_entry) => match dir_entry.file_type() { + Ok(file_type) => file_type.is_dir(), + _ => false, + }, + _ => false, + }) + .map(|entry_result| { + entry_result.map(|entry| entry.file_name().to_string_lossy().to_string()) + }) + .collect::, std::io::Error>>()?; + + names.sort(); + + Ok(names) + } + + /// Return the name of the currently selected (active) identity + pub fn get_selected_identity_name(&self) -> &String { + &self.selected_identity + } + + /// Remove a named identity. + /// Removing the selected identity is not allowed. + pub fn remove(&self, name: &str) -> DfxResult { + self.require_identity_exists(name)?; + + if self.configuration.default == name { + return Err(DfxError::IdentityError( + IdentityErrorKind::CannotDeleteDefaultIdentity(), + )); + } + let dir = self.get_identity_dir_path(name); + let pem = self.get_identity_pem_path(name); + + std::fs::remove_file(&pem).map_err(|e| DfxError::IoWithPath(e, pem))?; + std::fs::remove_dir(&dir).map_err(|e| DfxError::IoWithPath(e, dir)) + } + + /// Rename an identity. + /// If renaming the selected (default) identity, changes that + /// to refer to the new identity name. + pub fn rename(&self, from: &str, to: &str) -> DfxResult { + if to == ANONYMOUS_IDENTITY_NAME { + return Err(DfxError::IdentityError( + IdentityErrorKind::CannotCreateAnonymousIdentity(), + )); + } + self.require_identity_exists(from)?; + + let from_dir = self.get_identity_dir_path(from); + let to_dir = self.get_identity_dir_path(to); + + if to_dir.exists() { + return Err(DfxError::IdentityError( + IdentityErrorKind::IdentityAlreadyExists(), + )); + } + + std::fs::rename(&from_dir, &to_dir).map_err(|e| { + DfxError::IdentityError(IdentityErrorKind::CouldNotRenameIdentityDirectory( + from_dir, to_dir, e, + )) + })?; + + if from == self.configuration.default { + self.write_default_identity(to)?; + Ok(true) + } else { + Ok(false) + } + } + + /// Select an identity by name to use by default + pub fn use_identity_named(&self, name: &str) -> DfxResult { + self.require_identity_exists(name)?; + self.write_default_identity(name) + } + + fn write_default_identity(&self, name: &str) -> DfxResult { + let config = Configuration { + default: String::from(name), + }; + write_configuration(&self.identity_json_path, &config) + } + + fn require_identity_exists(&self, name: &str) -> DfxResult { + let identity_pem_path = self.get_identity_pem_path(name); + + if !identity_pem_path.exists() { + Err(DfxError::IdentityError( + IdentityErrorKind::IdentityDoesNotExist(String::from(name), identity_pem_path), + )) + } else { + Ok(()) + } + } + + fn get_identity_dir_path(&self, identity: &str) -> PathBuf { + self.identity_root_path.join(&identity) + } + + fn get_identity_pem_path(&self, identity: &str) -> PathBuf { + self.get_identity_dir_path(identity).join(IDENTITY_PEM) + } + + fn get_selected_identity_pem_path(&self) -> PathBuf { + self.get_identity_pem_path(&self.selected_identity) + } +} + +fn initialize( + logger: &Logger, + identity_json_path: &Path, + identity_root_path: &Path, +) -> DfxResult { + slog::info!(logger, r#"Creating the "default" identity."#); + + let identity_dir = identity_root_path.join(DEFAULT_IDENTITY_NAME); + let identity_pem_path = identity_dir.join(IDENTITY_PEM); + if !identity_pem_path.exists() { + if !identity_dir.exists() { + std::fs::create_dir_all(&identity_dir).map_err(|e| { + DfxError::IdentityError(IdentityErrorKind::CouldNotCreateIdentityDirectory( + identity_dir.clone(), + e, + )) + })?; + } + + let creds_pem_path = get_legacy_creds_pem_path()?; + if creds_pem_path.exists() { + slog::info!( + logger, + " - migrating key from {} to {}", + creds_pem_path.display(), + identity_pem_path.display() + ); + fs::copy(creds_pem_path, identity_pem_path)?; + } else { + slog::info!( + logger, + " - generating new key at {}", + identity_pem_path.display() + ); + generate_key(&identity_pem_path)?; + } + } else { + slog::info!( + logger, + " - using key already in place at {}", + identity_pem_path.display() + ); + } + + let config = Configuration { + default: String::from(DEFAULT_IDENTITY_NAME), + }; + write_configuration(&identity_json_path, &config)?; + + Ok(config) +} + +fn get_legacy_creds_pem_path() -> DfxResult { + let home = std::env::var("HOME") + .map_err(|_| DfxError::IdentityError(IdentityErrorKind::CannotFindUserHomeDirectory()))?; + + Ok(PathBuf::from(home) + .join(".dfinity") + .join("identity") + .join("creds.pem")) +} + +fn read_configuration(path: &Path) -> DfxResult { + let content = + std::fs::read_to_string(&path).map_err(|e| DfxError::IoWithPath(e, PathBuf::from(path)))?; + serde_json::from_str(&content).map_err(DfxError::from) +} + +fn write_configuration(path: &Path, config: &Configuration) -> DfxResult { + let content = serde_json::to_string_pretty(&config)?; + + std::fs::write(&path, content).map_err(|err| DfxError::IoWithPath(err, PathBuf::from(path))) +} + +fn generate_key(pem_file: &Path) -> DfxResult { + let rng = rand::SystemRandom::new(); + let pkcs8_bytes = signature::Ed25519KeyPair::generate_pkcs8(&rng) + .map_err(|x| DfxError::IdentityError(IdentityErrorKind::CouldNotGenerateKey(x)))?; + + let encoded_pem = encode_pem_private_key(&(*pkcs8_bytes.as_ref())); + fs::write(&pem_file, encoded_pem)?; + + let mut permissions = fs::metadata(&pem_file)?.permissions(); + permissions.set_readonly(true); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + permissions.set_mode(0o400); + } + + fs::set_permissions(&pem_file, permissions)?; + + Ok(()) +} + +fn encode_pem_private_key(key: &[u8]) -> String { + let pem = Pem { + tag: "PRIVATE KEY".to_owned(), + contents: key.to_vec(), + }; + encode(&pem) +} diff --git a/src/dfx/src/lib/identity/mod.rs b/src/dfx/src/lib/identity/mod.rs new file mode 100644 index 0000000000..a9a3539420 --- /dev/null +++ b/src/dfx/src/lib/identity/mod.rs @@ -0,0 +1 @@ +pub mod identity_manager; diff --git a/src/dfx/src/lib/message.rs b/src/dfx/src/lib/message.rs index 6ec9504804..a1c9294f15 100644 --- a/src/dfx/src/lib/message.rs +++ b/src/dfx/src/lib/message.rs @@ -100,6 +100,27 @@ user_message!( OptionValue => "Specifies the new value to set. If you don't specify a value, the command displays the current value of the option from the configuration file.", OptionFormat => "Specifies the format of the output. By default, it uses JSON.", + // dfx identity mod + ManageIdentity => "Manages identities used to communicate with the Internet Computer network. Setting an identity enables to test user-based access controls", + + // dfx identity new + NewIdentity => "Create a new identity.", + + // dfx identity list + ListIdentities => "List identities.", + + // dfx identity remove + RemoveIdentity => "Remove an identity.", + + // dfx identity rename + RenameIdentity => "Rename an identity.", + + // dfx identity use + UseIdentity => "Specify the identity to use.", + + // dfx identity whoami + ShowIdentity => "Show the name of the current identity.", + // dfx new CreateProject => "Creates a new project.", ProjectName => "Specifies the name of the project to create.", diff --git a/src/dfx/src/lib/mod.rs b/src/dfx/src/lib/mod.rs index 5497fe0dc4..ecc3b37fc6 100644 --- a/src/dfx/src/lib/mod.rs +++ b/src/dfx/src/lib/mod.rs @@ -1,5 +1,6 @@ pub mod builders; pub mod canister_info; +pub mod config; pub mod environment; pub mod error; pub mod identity; diff --git a/src/dfx/src/lib/provider.rs b/src/dfx/src/lib/provider.rs index 82e6b41585..fc1f59958b 100644 --- a/src/dfx/src/lib/provider.rs +++ b/src/dfx/src/lib/provider.rs @@ -81,7 +81,7 @@ pub fn create_agent_environment<'a>( ) -> DfxResult> { let network_descriptor = get_network_descriptor(env, args)?; - Ok(AgentEnvironment::new(env, network_descriptor)) + AgentEnvironment::new(env, network_descriptor) } pub fn command_line_provider_to_url(s: &str) -> DfxResult { diff --git a/src/dfx/src/main.rs b/src/dfx/src/main.rs index f0e741574e..7ba703f1a0 100644 --- a/src/dfx/src/main.rs +++ b/src/dfx/src/main.rs @@ -42,6 +42,11 @@ fn cli(_: &impl Environment) -> App<'_, '_> { .long("logfile") .takes_value(true), ) + .arg( + Arg::with_name("identity") + .long("identity") + .takes_value(true), + ) .subcommands( commands::builtin() .into_iter() @@ -153,10 +158,15 @@ fn main() { let (progress_bar, log) = setup_logging(&matches); + let identity_name = matches.value_of("identity").map(String::from); + // Need to recreate the environment because we use it to get matches. // TODO(hansl): resolve this double-create problem. - match EnvironmentImpl::new().map(|x| x.with_logger(log).with_progress_bar(progress_bar)) - { + match EnvironmentImpl::new().map(|x| { + x.with_logger(log) + .with_progress_bar(progress_bar) + .with_identity_override(identity_name) + }) { Ok(env) => { slog::trace!( env.get_logger(), diff --git a/src/ic_identity_manager/Cargo.toml b/src/ic_identity_manager/Cargo.toml deleted file mode 100644 index c797698105..0000000000 --- a/src/ic_identity_manager/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "ic-identity-manager" -version = "0.6.6" -authors = ["DFINITY Stiftung"] -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -openssl = "0.10.28" -pem = "0.7.0" -ring = "0.16.11" -serde = { version = "1.0", features = ["derive"] } - -[dependencies.ic-agent] -version = "0.1.0" -git = "ssh://git@github.com/dfinity-lab/agent-rust.git" -branch = "next" -rev = "e61e646c7a810c13c1b3d898d1d2ed7366b67a0a" - -[dependencies.ic-types] -version = "0.1.0" -git = "ssh://git@github.com/dfinity-lab/agent-rust.git" -branch = "next" -rev = "e61e646c7a810c13c1b3d898d1d2ed7366b67a0a" - -[dev-dependencies] -serde_cbor = "0.10" diff --git a/src/ic_identity_manager/examples/main.rs b/src/ic_identity_manager/examples/main.rs deleted file mode 100644 index 87b0d2386b..0000000000 --- a/src/ic_identity_manager/examples/main.rs +++ /dev/null @@ -1,38 +0,0 @@ -use ic_identity_manager::crypto_error::Result; -use ic_identity_manager::Identity; -use ic_types::principal::Principal; -use ring::signature::{self, KeyPair}; -use std::env::current_dir; -use std::fs; - -fn main() -> Result<()> { - const MESSAGE: &[u8] = b"Hello World!! This is an example test"; - let pwd = current_dir()?; - let identity = Identity::new(pwd)?; - let signed_message = identity.sign(MESSAGE)?; - - let pkcs8_bytes = pem::parse(fs::read("creds.pem").unwrap()).unwrap().contents; - let key_pair = signature::Ed25519KeyPair::from_pkcs8(pkcs8_bytes.as_ref())?; - let sig = key_pair.sign(MESSAGE); - assert_eq!(sig.as_ref().to_vec(), signed_message.signature); - assert_eq!( - Principal::self_authenticating(&key_pair.public_key()), - signed_message.signer - ); - assert_eq!( - key_pair.public_key().as_ref().to_vec(), - signed_message.public_key - ); - - let peer_public_key_bytes = key_pair.public_key().as_ref(); - let peer_public_key = - signature::UnparsedPublicKey::new(&signature::ED25519, peer_public_key_bytes); - peer_public_key.verify(MESSAGE, sig.as_ref())?; - - let signed_message_2 = identity.sign(MESSAGE)?; - assert_eq!(*signed_message_2.signature, *signed_message.signature); - assert_eq!(signed_message_2.public_key, signed_message.public_key); - assert_eq!(signed_message_2.signer, signed_message.signer); - - Ok(()) -} diff --git a/src/ic_identity_manager/src/basic.rs b/src/ic_identity_manager/src/basic.rs deleted file mode 100644 index 5bc419d695..0000000000 --- a/src/ic_identity_manager/src/basic.rs +++ /dev/null @@ -1,130 +0,0 @@ -//! Provides a basic example provider that utilizes unencrypted PEM -//! files. This is provided as a basic stepping stone to provide -//! further functionality. Note that working with unencrypted PEM is -//! not the best idea. -//! -//! However, there are two options: i) prompt the user per call, as -//! the agent is "stateless" or ii) provide long-running service -//! providers -- such as PGP, ssh-agent. -use crate::crypto_error::{Error, Result}; -use crate::types::Signature; - -use ic_types::principal::Principal; -use pem::{encode, Pem}; -use ring::signature::Ed25519KeyPair; -use ring::{ - rand, - signature::{self, KeyPair}, -}; -use std::fs; -use std::path::{Path, PathBuf}; - -// This module should not be re-exported. We want to ensure -// construction and handling of keys is done only here. -use self::private::BasicSignerReady; - -#[derive(Clone)] -pub struct BasicSigner { - path: PathBuf, -} - -impl BasicSigner { - pub fn new(path: PathBuf) -> Result { - if !path.is_dir() { - return Err(Error::ProviderFailedToInitialize); - } - Ok(Self { path }) - } -} - -fn generate(profile_path: &impl AsRef) -> Result { - let rng = rand::SystemRandom::new(); - let pkcs8_bytes = signature::Ed25519KeyPair::generate_pkcs8(&rng)?; - // We create a temporary file that gets overwritten every time - // we create a new provider for now. - let pem_file = profile_path.as_ref().join("creds.pem"); - fs::write(&pem_file, encode_pem_private_key(&(*pkcs8_bytes.as_ref())))?; - - // Build permissions, depending on platform. On POSIX we set 400. On anything else we - // just don't do anything. - let mut permissions = fs::metadata(&pem_file)?.permissions(); - permissions.set_readonly(true); - - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - permissions.set_mode(0o400); - } - - fs::set_permissions(&pem_file, permissions)?; - - assert_eq!( - pem::parse(fs::read(&pem_file)?)?.contents, - pkcs8_bytes.as_ref() - ); - Ok(pem_file) -} - -impl BasicSigner { - pub fn provide(&self) -> Result { - let mut dir = fs::read_dir(&self.path)?; - let name: std::ffi::OsString = "creds.pem".to_owned().into(); - let pem_file = if dir.any(|n| match n { - Ok(n) => n.file_name() == name, - Err(_) => false, - }) { - self.path.join("creds.pem") - } else { - generate(&self.path)? - }; - - let pkcs8_bytes = pem::parse(fs::read(pem_file)?)?.contents; - let key_pair = signature::Ed25519KeyPair::from_pkcs8(pkcs8_bytes.as_ref())?; - - Ok(BasicSignerReady { key_pair }) - } -} - -// The contents of this module while public, that is can be known and -// handled as of the new Rust iteration by other modules in the crate, -// the type constructor and associated functions shall be visible only -// by the parent module, and should not be re-exported. This is -// essentially a sealed type. -mod private { - use super::*; - /// We enforce a state transition, reading the key as necessary, only - /// to sign. TODO(eftychis): We should erase pin and erase the key - /// from memory afterwards. - pub struct BasicSignerReady { - pub key_pair: Ed25519KeyPair, - } - - impl BasicSignerReady { - pub fn sign(&self, msg: &[u8]) -> Result { - let signature = self.key_pair.sign(msg); - // At this point we shall validate the signature in this first - // skeleton version. - let public_key_bytes = self.key_pair.public_key().as_ref(); - - let public_key = - signature::UnparsedPublicKey::new(&signature::ED25519, public_key_bytes); - public_key.verify(msg, signature.as_ref())?; - Ok(Signature { - signer: self.principal(), - signature: signature.as_ref().to_vec(), - public_key: public_key_bytes.to_vec(), - }) - } - pub fn principal(&self) -> Principal { - Principal::self_authenticating(&self.key_pair.public_key()) - } - } -} - -fn encode_pem_private_key(key: &[u8]) -> String { - let pem = Pem { - tag: "PRIVATE KEY".to_owned(), - contents: key.to_vec(), - }; - encode(&pem) -} diff --git a/src/ic_identity_manager/src/crypto_error.rs b/src/ic_identity_manager/src/crypto_error.rs deleted file mode 100644 index b8cc656441..0000000000 --- a/src/ic_identity_manager/src/crypto_error.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::{error, result}; - -#[derive(Debug)] -/// Error type for Identity operations. This can involve system -/// runtime faults, setup or cryptography failures. -pub enum Error { - /// A CryptoError is isomorphic to unit on purpose. In case of - /// such a failure, as Rust is eager in general so we don't have - /// to worry about lazy evaluation of errors. - CryptoError, - /// No provider was found. - NoProvider, - /// Failed to initialize. - IdentityFailedToInitialize, - /// Failed to initialize provider. - ProviderFailedToInitialize, - /// Failed to parse provided PEM input string. - PemError(pem::PemError), - /// Failed to access file. - IOError(std::io::Error), -} - -impl From for Error { - fn from(_: ring::error::Unspecified) -> Self { - Error::CryptoError - } -} - -impl From for Error { - fn from(_: ring::error::KeyRejected) -> Self { - Error::CryptoError - } -} - -impl From for Error { - fn from(e: pem::PemError) -> Self { - Error::PemError(e) - } -} - -impl From for Error { - fn from(e: std::io::Error) -> Self { - Error::IOError(e) - } -} - -impl error::Error for Error { - // We do not need source for now. -} - -impl std::fmt::Display for Error { - fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // Currently Display = Debug for all intents and purposes. - write!(fmt, "{:?}", self) - } -} - -pub type Result = result::Result; diff --git a/src/ic_identity_manager/src/lib.rs b/src/ic_identity_manager/src/lib.rs deleted file mode 100644 index a6630fba81..0000000000 --- a/src/ic_identity_manager/src/lib.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! # Usage -//! -//! We expose a single type [`Identity`], currently providing only -//! signing and principal. -//! -//! # Examples -//! ```not_run -//! use ic_identity_manager::identity::Identity; -//! -//! let identity = -//! Identity::new(std::path::PathBuf::from("temp_dir")).expect("Failed to construct an identity object"); -//! let _signed_message = identity.sign(b"Hello World! This is Bob").expect("Signing failed"); -//! ``` - -/// Provides basic error type and messages. -pub mod crypto_error; - -mod basic; -mod types; - -use crate::basic::BasicSigner; -use crate::crypto_error::Error; -use crate::crypto_error::Result; -use crate::types::Signature; - -use ic_types::principal::Principal; -use std::path::PathBuf; - -/// An identity is a construct that denotes the set of claims of an -/// entity about itself. Namely it collects principals, under which -/// the owner of this object can authenticate and provides basic -/// operations. -pub struct Identity { - inner: BasicSigner, -} - -impl Identity { - /// Return a corresponding provided a profile path. We pass a simple - /// configuration for now, but this might change in the future. - pub fn new(path: PathBuf) -> Result { - let basic_provider = BasicSigner::new(path)?; - Ok(Self { - inner: basic_provider, - }) - } - - pub fn sender(&self) -> Principal { - let identity = self - .inner - .provide() - .expect("Could not find an identity provider."); - - identity.principal() - } - - /// Sign the provided message assuming a certain principal. - pub fn sign(&self, msg: &[u8]) -> Result { - let identity = self - .inner - .provide() - .map_err(|_| Error::IdentityFailedToInitialize)?; - identity.sign(msg) - } -} diff --git a/src/ic_identity_manager/src/types.rs b/src/ic_identity_manager/src/types.rs deleted file mode 100644 index 555e7a9e21..0000000000 --- a/src/ic_identity_manager/src/types.rs +++ /dev/null @@ -1,10 +0,0 @@ -use ic_types::principal::Principal; - -// Note perhaps in the future we will need to indicate the schema -// type. -#[derive(Clone)] -pub struct Signature { - pub signer: Principal, - pub public_key: Vec, - pub signature: Vec, -}