Skip to content

Commit fdbc4e7

Browse files
Add .well-known record and host param
1 parent 543a252 commit fdbc4e7

File tree

7 files changed

+81
-92
lines changed

7 files changed

+81
-92
lines changed

Cargo.lock

Lines changed: 4 additions & 75 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/icp-cli/src/commands/identity/link/ii.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use ic_agent::{Identity as _, export::Principal, identity::BasicIdentity};
55
use icp::{context::Context, fs::read_to_string, identity::key, prelude::*};
66
use snafu::{ResultExt, Snafu};
77
use tracing::{info, warn};
8+
use url::Url;
89

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

@@ -14,6 +15,10 @@ pub(crate) struct IiArgs {
1415
/// Name for the linked identity
1516
name: String,
1617

18+
/// Host of the II login frontend (e.g. https://example.icp0.io)
19+
#[arg(long, default_value = ii_poll::DEFAULT_HOST)]
20+
host: Url,
21+
1722
/// Where to store the session private key
1823
#[arg(long, value_enum, default_value_t)]
1924
storage: StorageMode,
@@ -51,13 +56,14 @@ pub(crate) async fn exec(ctx: &Context, args: &IiArgs) -> Result<(), IiError> {
5156
let basic = BasicIdentity::from_raw_key(&secret_key.serialize_raw());
5257
let der_public_key = basic.public_key().expect("ed25519 always has a public key");
5358

54-
let chain = ii_poll::poll_for_delegation(&der_public_key)
59+
let chain = ii_poll::poll_for_delegation(&args.host, &der_public_key)
5560
.await
5661
.context(PollSnafu)?;
5762

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

66+
let host = args.host.clone();
6167
ctx.dirs
6268
.identity()?
6369
.with_write(async |dirs| {
@@ -68,6 +74,7 @@ pub(crate) async fn exec(ctx: &Context, args: &IiArgs) -> Result<(), IiError> {
6874
&chain,
6975
ii_principal,
7076
create_format,
77+
host,
7178
)
7279
})
7380
.await?

crates/icp-cli/src/commands/identity/login.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub(crate) struct LoginArgs {
2020
}
2121

2222
pub(crate) async fn exec(ctx: &Context, args: &LoginArgs) -> Result<(), LoginError> {
23-
let (algorithm, storage) = ctx
23+
let (algorithm, storage, host) = ctx
2424
.dirs
2525
.identity()?
2626
.with_read(async |dirs| {
@@ -31,8 +31,11 @@ pub(crate) async fn exec(ctx: &Context, args: &LoginArgs) -> Result<(), LoginErr
3131
.context(IdentityNotFoundSnafu { name: &args.name })?;
3232
match spec {
3333
IdentitySpec::InternetIdentity {
34-
algorithm, storage, ..
35-
} => Ok((algorithm.clone(), storage.clone())),
34+
algorithm,
35+
storage,
36+
host,
37+
..
38+
} => Ok((algorithm.clone(), storage.clone(), host.clone())),
3639
_ => NotIiSnafu { name: &args.name }.fail(),
3740
}
3841
})
@@ -52,7 +55,7 @@ pub(crate) async fn exec(ctx: &Context, args: &LoginArgs) -> Result<(), LoginErr
5255
.await?
5356
.context(LoadSessionKeySnafu)?;
5457

55-
let chain = ii_poll::poll_for_delegation(&der_public_key)
58+
let chain = ii_poll::poll_for_delegation(&host, &der_public_key)
5659
.await
5760
.context(PollSnafu)?;
5861

crates/icp-cli/src/operations/ii_poll.rs

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ use snafu::{ResultExt, Snafu};
1414
use tokio::{net::TcpListener, sync::oneshot};
1515
use url::Url;
1616

17-
/// The hosted II login frontend for the dev account canister.
18-
const CLI_LOGIN_BASE: &str = "https://ut7yr-7iaaa-aaaag-ak7ca-cai.icp0.io/cli-login";
17+
/// Fallback host. Dummy value until we get a real domain. A staging instance can be found at ut7yr-7iaaa-aaaag-ak7ca-caia.ic0.app
18+
pub(crate) const DEFAULT_HOST: &str = "https://not.a.domain";
1919

2020
#[derive(Debug, Snafu)]
2121
pub(crate) enum IiPollError {
@@ -25,18 +25,53 @@ pub(crate) enum IiPollError {
2525
#[snafu(display("failed to run local callback server"))]
2626
ServeServer { source: std::io::Error },
2727

28+
#[snafu(display("failed to fetch `{url}`"))]
29+
FetchDiscovery { url: String, source: reqwest::Error },
30+
31+
#[snafu(display("failed to read discovery response from `{url}`"))]
32+
ReadDiscovery { url: String, source: reqwest::Error },
33+
34+
#[snafu(display(
35+
"`{url}` returned an empty login path — the response must be a single non-empty line"
36+
))]
37+
EmptyLoginPath { url: String },
38+
2839
#[snafu(display("interrupted"))]
2940
Interrupted,
3041
}
3142

32-
/// Starts a local HTTP server to receive the delegation callback from the II
33-
/// frontend, prints the login URL for the user to open, and returns the
34-
/// delegation chain once the frontend POSTs it back.
43+
/// Discovers the login path from `{host}/.well-known/ic-cli-login`, then opens
44+
/// a local HTTP server, builds the login URL, and returns the delegation chain
45+
/// once the frontend POSTs it back.
3546
pub(crate) async fn poll_for_delegation(
47+
host: &Url,
3648
der_public_key: &[u8],
3749
) -> Result<DelegationChain, IiPollError> {
3850
let key_b64 = URL_SAFE_NO_PAD.encode(der_public_key);
3951

52+
// Discover the login path.
53+
let discovery_url = host
54+
.join("/.well-known/ic-cli-login")
55+
.expect("joining an absolute path is infallible");
56+
let discovery_url_str = discovery_url.to_string();
57+
let login_path = reqwest::get(discovery_url)
58+
.await
59+
.context(FetchDiscoverySnafu {
60+
url: &discovery_url_str,
61+
})?
62+
.text()
63+
.await
64+
.context(ReadDiscoverySnafu {
65+
url: &discovery_url_str,
66+
})?;
67+
let login_path = login_path.trim();
68+
if login_path.is_empty() {
69+
return EmptyLoginPathSnafu {
70+
url: discovery_url_str,
71+
}
72+
.fail();
73+
}
74+
4075
// Bind on a random port before opening the browser so the callback URL is known.
4176
let listener = TcpListener::bind("127.0.0.1:0")
4277
.await
@@ -54,11 +89,11 @@ pub(crate) async fn poll_for_delegation(
5489
.append_pair("callback", &callback_url);
5590
scratch.query().expect("just set").to_owned()
5691
};
57-
let mut login_url = Url::parse(CLI_LOGIN_BASE).expect("valid constant");
92+
let mut login_url = host.join(login_path).expect("login_path is a valid path");
5893
login_url.set_fragment(Some(&fragment));
5994

6095
eprintln!();
61-
eprintln!(" Press Enter to open {}", {
96+
eprintln!(" Press Enter to log in at {}", {
6297
let mut display = login_url.clone();
6398
display.set_fragment(None);
6499
display

crates/icp/src/identity/key.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ use std::{
66
use ic_agent::{
77
Identity,
88
identity::{
9-
AnonymousIdentity, BasicIdentity, DelegatedIdentity, Prime256v1Identity, Secp256k1Identity,
9+
AnonymousIdentity, BasicIdentity, DelegatedIdentity, DelegationError, Prime256v1Identity,
10+
Secp256k1Identity,
1011
},
1112
};
1213
use ic_ed25519::PrivateKeyFormat;
@@ -21,6 +22,7 @@ use rand::Rng;
2122
use scrypt::Params;
2223
use sec1::{der::Decode, pem::PemLabel};
2324
use snafu::{OptionExt, ResultExt, Snafu, ensure};
25+
use url::Url;
2426
use zeroize::Zeroizing;
2527

2628
use crate::{
@@ -113,6 +115,12 @@ pub enum LoadIdentityError {
113115
source: delegation::LoadError,
114116
},
115117

118+
#[snafu(display("failed to validate delegation chain loaded from `{path}`"))]
119+
ValidateDelegationChain {
120+
path: PathBuf,
121+
source: DelegationError,
122+
},
123+
116124
#[snafu(display(
117125
"delegation for identity `{name}` has expired or will expire within 5 minutes; \
118126
run `icp identity login {name}` to re-authenticate"
@@ -398,10 +406,8 @@ fn load_ii_identity(
398406
}
399407
};
400408

401-
// Use new_unchecked because the root of the II delegation chain uses
402-
// canister signatures (OID 1.3.6.1.4.1.56387.1.2) which DelegatedIdentity::new
403-
// cannot verify client-side. The replica validates the chain on each request.
404-
let delegated = DelegatedIdentity::new_unchecked(from_key, inner, signed_delegations);
409+
let delegated = DelegatedIdentity::new(from_key, inner, signed_delegations)
410+
.context(ValidateDelegationChainSnafu { path: &chain_path })?;
405411

406412
Ok(Arc::new(delegated))
407413
}
@@ -1112,6 +1118,7 @@ pub fn link_ii_identity(
11121118
chain: &delegation::DelegationChain,
11131119
principal: ic_agent::export::Principal,
11141120
create_format: CreateFormat,
1121+
host: Url,
11151122
) -> Result<(), LinkIiIdentityError> {
11161123
let mut identity_list = IdentityList::load_from(dirs.read())?;
11171124
ensure!(
@@ -1186,6 +1193,7 @@ pub fn link_ii_identity(
11861193
algorithm,
11871194
principal,
11881195
storage: ii_storage,
1196+
host,
11891197
};
11901198
identity_list.identities.insert(name.to_string(), spec);
11911199
identity_list.write_to(dirs)?;

crates/icp/src/identity/manifest.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use ic_agent::export::Principal;
44
use serde::{Deserialize, Serialize};
55
use snafu::{Snafu, ensure};
66
use strum::{Display, EnumString};
7+
use url::Url;
78

89
use crate::{
910
fs::{
@@ -134,6 +135,9 @@ pub enum IdentitySpec {
134135
/// (`Principal::self_authenticating(from_key)`), not the session key.
135136
principal: Principal,
136137
storage: IiKeyStorage,
138+
/// The host used for II login, stored so `icp identity login` can
139+
/// re-authenticate without requiring `--host` again.
140+
host: Url,
137141
},
138142
}
139143

0 commit comments

Comments
 (0)