Skip to content

Commit 9929dbb

Browse files
committed
node: fetch provisioned secrets, auth owner w/ TLS
1 parent b0a7036 commit 9929dbb

File tree

15 files changed

+270
-244
lines changed

15 files changed

+270
-244
lines changed

common/src/cli.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use proptest_derive::Arbitrary;
1717

1818
use crate::api::runner::Port;
1919
use crate::api::UserPk;
20+
use crate::constants::{NODE_PROVISION_DNS, NODE_RUN_DNS};
2021
use crate::enclave::{self, MachineId};
2122

2223
pub const DEFAULT_BACKEND_URL: &str = "http://127.0.0.1:3030";
@@ -155,6 +156,11 @@ pub struct RunArgs {
155156
#[argh(option, default = "DEFAULT_RUNNER_URL.to_owned()")]
156157
pub runner_url: String,
157158

159+
/// the DNS name the node enclave should include in its remote attestation
160+
/// certificate and the client will expect in its connection
161+
#[argh(option, default = "NODE_RUN_DNS.to_owned()")]
162+
pub node_dns_name: String,
163+
158164
/// whether to use a mock API client. Only available during development.
159165
#[argh(switch, short = 'm')]
160166
pub mock: bool,
@@ -174,6 +180,7 @@ impl Default for RunArgs {
174180
shutdown_after_sync_if_no_activity: false,
175181
inactivity_timer_sec: 3600,
176182
repl: false,
183+
node_dns_name: NODE_RUN_DNS.to_owned(),
177184
backend_url: DEFAULT_BACKEND_URL.to_owned(),
178185
runner_url: DEFAULT_RUNNER_URL.to_owned(),
179186
mock: false,
@@ -198,7 +205,9 @@ impl RunArgs {
198205
.arg("--backend-url")
199206
.arg(&self.backend_url)
200207
.arg("--runner-url")
201-
.arg(&self.runner_url);
208+
.arg(&self.runner_url)
209+
.arg("--node-dns-name")
210+
.arg(&self.node_dns_name);
202211

203212
if self.shutdown_after_sync_if_no_activity {
204213
cmd.arg("-s");
@@ -240,7 +249,7 @@ pub struct ProvisionArgs {
240249

241250
/// the DNS name the node enclave should include in its remote attestation
242251
/// certificate and the client will expect in its connection
243-
#[argh(option, default = "String::from(\"provision.lexe.tech\")")]
252+
#[argh(option, default = "NODE_PROVISION_DNS.to_owned()")]
244253
pub node_dns_name: String,
245254

246255
/// protocol://host:port of the node backend.
@@ -534,7 +543,7 @@ mod test {
534543
fn do_cmd_roundtrip(path_str: String, cmd1: &NodeCommand) {
535544
let path = Path::new(&path_str);
536545
// Convert to std::process::Command
537-
let std_cmd = cmd1.to_cmd(&path);
546+
let std_cmd = cmd1.to_cmd(path);
538547
// Convert to an iterator over &str args
539548
let mut args_iter = std_cmd.get_args().filter_map(|s| s.to_str());
540549
// Pop the first arg which contains the subcommand name e.g. 'run'
@@ -543,7 +552,7 @@ mod test {
543552
let cmd_args: Vec<&str> = args_iter.collect();
544553
dbg!(&cmd_args);
545554
// Deserialize back into struct
546-
let cmd2 = NodeCommand::from_args(&[&subcommand], &cmd_args).unwrap();
555+
let cmd2 = NodeCommand::from_args(&[subcommand], &cmd_args).unwrap();
547556
// Assert
548557
assert_eq!(*cmd1, cmd2);
549558
}
@@ -572,6 +581,7 @@ mod test {
572581
repl: false,
573582
backend_url: "".into(),
574583
runner_url: "".into(),
584+
node_dns_name: "localhost".to_owned(),
575585
mock: true,
576586
});
577587
do_cmd_roundtrip(path_str, &cmd);
@@ -595,6 +605,7 @@ mod test {
595605
repl: true,
596606
backend_url: "".into(),
597607
runner_url: "".into(),
608+
node_dns_name: "localhost".to_owned(),
598609
mock: false,
599610
});
600611
do_cmd_roundtrip(path_str, &cmd);

common/src/client/mod.rs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,94 @@
11
// TODO
22

33
pub mod certs;
4-
pub mod provision;
54
pub mod tls;
5+
6+
use anyhow::{format_err, Context};
7+
use bitcoin::secp256k1::PublicKey;
8+
use serde::{Deserialize, Serialize};
9+
10+
use crate::api::provision::ProvisionRequest;
11+
use crate::api::UserPk;
12+
use crate::attest;
13+
use crate::rng::Crng;
14+
use crate::root_seed::RootSeed;
15+
16+
#[derive(Debug, Deserialize, Serialize)]
17+
pub struct NodeInfo {
18+
pub node_pk: PublicKey,
19+
pub num_channels: usize,
20+
pub num_usable_channels: usize,
21+
pub local_balance_msat: u64,
22+
pub num_peers: usize,
23+
}
24+
25+
pub struct NodeClient {
26+
client: reqwest::Client,
27+
provision_url: String,
28+
run_url: String,
29+
}
30+
31+
impl NodeClient {
32+
#[allow(clippy::too_many_arguments)]
33+
pub fn new<R: Crng>(
34+
rng: &mut R,
35+
seed: &RootSeed,
36+
user_pk: &UserPk,
37+
proxy_url: &str,
38+
proxy_ca: &rustls::Certificate,
39+
attest_verifier: attest::ServerCertVerifier,
40+
provision_url: String,
41+
run_url: String,
42+
) -> anyhow::Result<Self> {
43+
// TODO(phlip9): actual auth in proxy header
44+
// TODO(phlip9): https only mode
45+
46+
let proxy = reqwest::Proxy::https(proxy_url)
47+
.context("Invalid proxy url")?
48+
// TODO(phlip9): should be bearer auth
49+
.basic_auth(&user_pk.to_string(), "");
50+
51+
let tls = tls::client_tls_config(rng, proxy_ca, seed, attest_verifier)?;
52+
53+
let client = reqwest::Client::builder()
54+
.proxy(proxy)
55+
.user_agent("lexe-client")
56+
.use_preconfigured_tls(tls)
57+
.build()
58+
.context("Failed to build client")?;
59+
60+
Ok(Self {
61+
client,
62+
provision_url,
63+
run_url,
64+
})
65+
}
66+
67+
pub async fn provision(&self, req: ProvisionRequest) -> anyhow::Result<()> {
68+
let provision_url = &self.provision_url;
69+
let url = format!("{provision_url}/provision");
70+
71+
let resp = self.client.post(url).json(&req).send().await?;
72+
73+
if resp.status().is_success() {
74+
Ok(())
75+
} else {
76+
let err_txt = resp.text().await?;
77+
Err(format_err!("response error: {err_txt}"))
78+
}
79+
}
80+
81+
pub async fn node_info(&self) -> anyhow::Result<NodeInfo> {
82+
let run_url = &self.run_url;
83+
let url = format!("{run_url}/owner/node_info");
84+
85+
let resp = self.client.get(url).send().await?;
86+
87+
if resp.status().is_success() {
88+
resp.json().await.map_err(Into::into)
89+
} else {
90+
let err_txt = resp.text().await?;
91+
Err(format_err!("response error: {err_txt}"))
92+
}
93+
}
94+
}

common/src/client/provision.rs

Lines changed: 0 additions & 59 deletions
This file was deleted.

common/src/constants.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
use rcgen::{DistinguishedName, DnType};
22

3+
/// Fake DNS name used by the node reverse proxy to route owner requests to a
4+
/// node awaiting provisioning. This DNS name doesn't actually resolve.
5+
pub const NODE_PROVISION_DNS: &str = "provision.lexe.tech";
6+
7+
/// Fake DNS name used by the node reverse proxy to route owner requests to a
8+
/// running node. This DNS name doesn't actually resolve.
9+
pub const NODE_RUN_DNS: &str = "run.lexe.tech";
10+
311
pub fn lexe_distinguished_name_prefix() -> DistinguishedName {
412
let mut name = DistinguishedName::new();
513
name.push(DnType::CountryName, "US");

node/src/api/client.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,22 @@ impl ApiClient for LexeApiClient {
8080
user_pk,
8181
measurement,
8282
};
83-
self.request(Method::GET, Backend, V1, "/instance", req)
84-
.await
83+
let maybe_instance: Option<Instance> = self
84+
.request(Method::GET, Backend, V1, "/instance", req)
85+
.await?;
86+
87+
if let Some(instance) = maybe_instance.as_ref() {
88+
if instance.measurement != measurement {
89+
let msg = format!(
90+
"returned instance measurement '{}' doesn't match \
91+
requested measurement '{}'",
92+
instance.measurement, measurement,
93+
);
94+
return Err(ApiError::ResponseError(msg));
95+
}
96+
}
97+
98+
Ok(maybe_instance)
8599
}
86100

87101
async fn get_sealed_seed(

node/src/api/mock.rs

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,45 +12,57 @@ use common::api::runner::UserPorts;
1212
use common::api::vfs::{Directory, File, FileId};
1313
use common::api::UserPk;
1414
use common::enclave::{self, Measurement};
15-
use common::hex;
15+
use common::rng::SysRng;
16+
use common::root_seed::RootSeed;
17+
use once_cell::sync::Lazy;
18+
use secrecy::{ExposeSecret, Secret};
1619
use tokio::sync::mpsc;
1720

1821
use crate::api::{ApiClient, ApiError};
1922
use crate::lexe::persister;
23+
use crate::provision::ProvisionedSecrets;
2024

2125
type FileName = String;
2226
type Data = Vec<u8>;
2327

24-
const HEX_SEED1: [u8; 32] = hex::decode_const(
25-
b"39ee00e3e23a9cd7e6509f56ff66daaf021cb5502e4ab3c6c393b522a6782d03",
26-
);
27-
const HEX_SEED2: [u8; 32] = hex::decode_const(
28-
b"2a784ea82ef7002ec929b435e1af283a1998878575e8ccbad73e5d0cb3a95f59",
29-
);
28+
// --- test fixtures --- //
3029

31-
pub fn seed(node_pk: PublicKey) -> Vec<u8> {
32-
let node_pk_bytes = node_pk.serialize();
30+
fn make_seed(bytes: [u8; 32]) -> RootSeed {
31+
RootSeed::new(Secret::new(bytes))
32+
}
33+
fn make_node_pk(seed: &RootSeed) -> PublicKey {
34+
PublicKey::from_keypair(&seed.derive_node_key_pair(&mut SysRng::new()))
35+
}
36+
fn make_sealed_seed(seed: &RootSeed) -> Vec<u8> {
37+
let seed = make_seed(*seed.expose_secret());
38+
let provisioned_secrets = ProvisionedSecrets { root_seed: seed };
39+
let sealed_secrets = provisioned_secrets.seal(&mut SysRng::new()).unwrap();
40+
sealed_secrets.serialize()
41+
}
42+
43+
static SEED1: Lazy<RootSeed> = Lazy::new(|| make_seed([0x42; 32]));
44+
static SEED2: Lazy<RootSeed> = Lazy::new(|| make_seed([0x69; 32]));
3345

34-
if node_pk_bytes == NODE_PK1 {
35-
HEX_SEED1.to_vec()
36-
} else if node_pk_bytes == NODE_PK2 {
37-
HEX_SEED2.to_vec()
46+
static NODE_PK1: Lazy<PublicKey> = Lazy::new(|| make_node_pk(&SEED1));
47+
static NODE_PK2: Lazy<PublicKey> = Lazy::new(|| make_node_pk(&SEED2));
48+
49+
static SEALED_SEED1: Lazy<Vec<u8>> = Lazy::new(|| make_sealed_seed(&SEED1));
50+
static SEALED_SEED2: Lazy<Vec<u8>> = Lazy::new(|| make_sealed_seed(&SEED2));
51+
52+
pub fn sealed_seed(node_pk: &PublicKey) -> Vec<u8> {
53+
if node_pk == &*NODE_PK1 {
54+
SEALED_SEED1.clone()
55+
} else if node_pk == &*NODE_PK2 {
56+
SEALED_SEED2.clone()
3857
} else {
3958
todo!("TODO(max): Programmatically generate for new users")
4059
}
4160
}
4261

43-
const NODE_PK1: [u8; 33] = hex::decode_const(
44-
b"02692f6894d5cb51bb785cc3c54f457889faf674fedea54a906f7ec99e88832d18",
45-
);
46-
const NODE_PK2: [u8; 33] = hex::decode_const(
47-
b"025336702e1317fcb55cdce19b26bd154b5d5612b87d04ff41f807372513f02b6a",
48-
);
49-
5062
fn node_pk(user_pk: UserPk) -> PublicKey {
5163
match user_pk.to_i64() {
52-
1 => PublicKey::from_slice(&NODE_PK1).unwrap(),
53-
2 => PublicKey::from_slice(&NODE_PK2).unwrap(),
64+
1 => *NODE_PK1,
65+
2 => *NODE_PK2,
5466
_ => todo!("TODO(max): Programmatically generate for new users"),
5567
}
5668
}
@@ -127,7 +139,7 @@ impl ApiClient for MockApiClient {
127139
req.measurement,
128140
req.machine_id,
129141
req.min_cpusvn,
130-
seed(req.node_pk),
142+
sealed_seed(&req.node_pk),
131143
);
132144
Ok(Some(sealed_seed))
133145
}

node/src/api/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,18 @@ pub use client::*;
1919
pub enum ApiError {
2020
#[error("Reqwest error")]
2121
Reqwest(#[from] reqwest::Error),
22+
2223
#[error("JSON serialization error")]
2324
JsonSerialization(#[from] serde_json::Error),
25+
2426
#[error("Query string serialization error")]
2527
QueryStringSerialization(#[from] serde_qs::Error),
28+
2629
#[error("Server Error: {0}")]
2730
Server(String),
31+
32+
#[error("Invalid response: {0}")]
33+
ResponseError(String),
2834
}
2935

3036
#[async_trait]

0 commit comments

Comments
 (0)