Skip to content

Commit 63fc537

Browse files
committed
refactor: Unify root seed sealing / unsealing
1 parent a85f021 commit 63fc537

File tree

4 files changed

+158
-97
lines changed

4 files changed

+158
-97
lines changed

common/src/api/provision.rs

Lines changed: 114 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use anyhow::Context;
1+
use anyhow::{ensure, Context};
22
use bitcoin::secp256k1::PublicKey;
33
use secrecy::ExposeSecret;
44
use serde::{Deserialize, Serialize};
@@ -21,6 +21,13 @@ pub struct ProvisionRequest {
2121
pub root_seed: RootSeed,
2222
}
2323

24+
#[derive(Serialize, Deserialize)]
25+
pub struct NodeInstanceSeed {
26+
pub node: Node,
27+
pub instance: Instance,
28+
pub sealed_seed: SealedSeed,
29+
}
30+
2431
#[derive(Serialize, Deserialize)]
2532
pub struct Node {
2633
pub user_pk: UserPk,
@@ -34,22 +41,39 @@ pub struct Instance {
3441
}
3542

3643
/// Uniquely identifies a sealed seed using its primary key fields.
37-
#[derive(Serialize, Deserialize)]
44+
#[derive(Clone, Serialize, Deserialize)]
3845
pub struct SealedSeedId {
3946
pub node_pk: PublicKey,
4047
pub measurement: Measurement,
4148
pub machine_id: MachineId,
4249
pub min_cpusvn: MinCpusvn,
4350
}
4451

45-
#[derive(Serialize, Deserialize)]
52+
/// The user node's provisioned seed that is sealed and persisted using its
53+
/// platform enclave keys that are software and version specific.
54+
///
55+
/// This struct is returned directly from the DB so it should be considered as
56+
/// untrusted and not-yet-validated. To validate and convert a [`SealedSeed`]
57+
/// into a [`RootSeed`], use [`unseal_and_validate`]. To encrypt an existing
58+
/// [`RootSeed`] into a [`SealedSeed`], use [`seal_from_root_seed`].
59+
///
60+
/// See [`crate::enclave::seal`] for more implementation details.
61+
///
62+
/// [`unseal_and_validate`]: Self::unseal_and_validate
63+
/// [`seal_from_root_seed`]: Self::seal_from_root_seed
64+
#[derive(Clone, Serialize, Deserialize)]
4665
pub struct SealedSeed {
4766
#[serde(flatten)]
4867
pub id: SealedSeedId,
68+
/// The fully serialized + sealed root seed.
69+
// NOTE: This should probably be renamed to `raw` or `ciphertext` or smth
70+
// but that requires a DB migration
4971
pub seed: Vec<u8>,
5072
}
5173

5274
impl SealedSeed {
75+
const LABEL: &'static [u8] = b"sealed seed";
76+
5377
pub fn new(
5478
node_pk: PublicKey,
5579
measurement: Measurement,
@@ -67,38 +91,101 @@ impl SealedSeed {
6791
seed,
6892
}
6993
}
70-
}
7194

72-
/// The enclave's provisioned secrets that it will seal and persist using its
73-
/// platform enclave keys that are software and version specific.
74-
///
75-
/// See: [`crate::enclave::seal`]
76-
// TODO(phlip9): rename this or SealedSeed?
77-
pub struct ProvisionedSecrets {
78-
pub root_seed: RootSeed,
79-
}
95+
pub fn seal_from_root_seed<R: Crng>(
96+
rng: &mut R,
97+
root_seed: &RootSeed,
98+
) -> anyhow::Result<Self> {
99+
// Construct the root seed ciphertext
100+
let root_seed_ref = root_seed.expose_secret().as_slice();
101+
let sealed = enclave::seal(rng, Self::LABEL, root_seed_ref.into())
102+
.context("Failed to seal root seed")?;
103+
let sealed_bytes = sealed.serialize();
80104

81-
impl ProvisionedSecrets {
82-
const LABEL: &'static [u8] = b"provisioned secrets";
105+
// Derive / compute the other fields
106+
let node_pk = root_seed.derive_node_pk(rng);
107+
let measurement = enclave::measurement();
108+
let machine_id = enclave::machine_id();
109+
let min_cpusvn = enclave::MIN_SGX_CPUSVN;
83110

84-
pub fn seal(&self, rng: &mut dyn Crng) -> anyhow::Result<Sealed<'_>> {
85-
let root_seed_ref = self.root_seed.expose_secret().as_slice();
86-
enclave::seal(rng, Self::LABEL, root_seed_ref.into())
87-
.context("Failed to seal provisioned secrets")
111+
Ok(Self::new(
112+
node_pk,
113+
measurement,
114+
machine_id,
115+
min_cpusvn,
116+
sealed_bytes,
117+
))
88118
}
89119

90-
pub fn unseal(sealed: Sealed<'_>) -> anyhow::Result<Self> {
91-
let bytes = enclave::unseal(Self::LABEL, sealed)
120+
pub fn unseal_and_validate<R: Crng>(
121+
self,
122+
rng: &mut R,
123+
) -> anyhow::Result<RootSeed> {
124+
// Compute the SGX fields
125+
let measurement = enclave::measurement();
126+
let machine_id = enclave::machine_id();
127+
let min_cpusvn = enclave::MIN_SGX_CPUSVN;
128+
129+
// Validate SGX fields
130+
ensure!(
131+
self.id.measurement == measurement,
132+
"Saved measurement doesn't match current measurement",
133+
);
134+
ensure!(
135+
self.id.machine_id == machine_id,
136+
"Saved machine id doesn't match current machine id",
137+
);
138+
ensure!(
139+
self.id.min_cpusvn == min_cpusvn,
140+
"Saved min CPUSVN doesn't match current min CPUSVN",
141+
);
142+
143+
// Unseal
144+
let sealed = Sealed::deserialize(&self.seed)
145+
.context("Failed to deserialize sealed seed")?;
146+
let unsealed_bytes = enclave::unseal(Self::LABEL, sealed)
92147
.context("Failed to unseal provisioned secrets")?;
93-
let root_seed = RootSeed::try_from(bytes.as_slice())
148+
149+
// Reconstruct root seed
150+
let root_seed = RootSeed::try_from(unsealed_bytes.as_slice())
94151
.context("Failed to deserialize root seed")?;
95-
Ok(Self { root_seed })
152+
153+
// Validate node_pk
154+
let derived_node_pk = root_seed.derive_node_pk(rng);
155+
ensure!(
156+
self.id.node_pk == derived_node_pk,
157+
"Saved node pk doesn't match derived node pk"
158+
);
159+
160+
// Validation complete, everything OK.
161+
Ok(root_seed)
96162
}
97163
}
98164

99-
#[derive(Serialize, Deserialize)]
100-
pub struct NodeInstanceSeed {
101-
pub node: Node,
102-
pub instance: Instance,
103-
pub sealed_seed: SealedSeed,
165+
#[cfg(test)]
166+
mod test {
167+
use proptest::arbitrary::any;
168+
use proptest::proptest;
169+
use secrecy::ExposeSecret;
170+
171+
use super::*;
172+
use crate::rng::SysRng;
173+
174+
proptest! {
175+
#[test]
176+
fn seal_unseal_roundtrip(root_seed1 in any::<RootSeed>()) {
177+
let mut rng = SysRng::new();
178+
179+
let root_seed2 =
180+
SealedSeed::seal_from_root_seed(&mut rng, &root_seed1)
181+
.unwrap()
182+
.unseal_and_validate(&mut rng)
183+
.unwrap();
184+
185+
assert_eq!(
186+
root_seed1.expose_secret(),
187+
root_seed2.expose_secret(),
188+
);
189+
}
190+
}
104191
}

node/src/api/mock.rs

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ use bitcoin::secp256k1::PublicKey;
88
use common::api::def::{NodeBackendApi, NodeRunnerApi};
99
use common::api::error::{BackendApiError, RunnerApiError};
1010
use common::api::provision::{
11-
Instance, Node, NodeInstanceSeed, ProvisionedSecrets, SealedSeed,
12-
SealedSeedId,
11+
Instance, Node, NodeInstanceSeed, SealedSeed, SealedSeedId,
1312
};
1413
use common::api::runner::UserPorts;
1514
use common::api::vfs::{NodeDirectory, NodeFile, NodeFileId};
@@ -18,7 +17,7 @@ use common::enclave::{self, Measurement};
1817
use common::rng::SysRng;
1918
use common::root_seed::RootSeed;
2019
use once_cell::sync::Lazy;
21-
use secrecy::{ExposeSecret, Secret};
20+
use secrecy::Secret;
2221
use tokio::sync::mpsc;
2322

2423
use crate::api::ApiClient;
@@ -29,29 +28,27 @@ type Data = Vec<u8>;
2928

3029
// --- test fixtures --- //
3130

32-
fn make_seed(bytes: [u8; 32]) -> RootSeed {
31+
fn make_root_seed(bytes: [u8; 32]) -> RootSeed {
3332
RootSeed::new(Secret::new(bytes))
3433
}
35-
fn make_node_pk(seed: &RootSeed) -> PublicKey {
36-
PublicKey::from_keypair(&seed.derive_node_key_pair(&mut SysRng::new()))
34+
fn make_node_pk(root_seed: &RootSeed) -> PublicKey {
35+
root_seed.derive_node_pk(&mut SysRng::new())
3736
}
38-
fn make_sealed_seed(seed: &RootSeed) -> Vec<u8> {
39-
let seed = make_seed(*seed.expose_secret());
40-
let provisioned_secrets = ProvisionedSecrets { root_seed: seed };
41-
let sealed_secrets = provisioned_secrets.seal(&mut SysRng::new()).unwrap();
42-
sealed_secrets.serialize()
37+
fn make_sealed_seed(root_seed: &RootSeed) -> SealedSeed {
38+
SealedSeed::seal_from_root_seed(&mut SysRng::new(), root_seed)
39+
.expect("Failed to seal test root seed")
4340
}
4441

45-
static SEED1: Lazy<RootSeed> = Lazy::new(|| make_seed([0x42; 32]));
46-
static SEED2: Lazy<RootSeed> = Lazy::new(|| make_seed([0x69; 32]));
42+
static SEED1: Lazy<RootSeed> = Lazy::new(|| make_root_seed([0x42; 32]));
43+
static SEED2: Lazy<RootSeed> = Lazy::new(|| make_root_seed([0x69; 32]));
4744

4845
static NODE_PK1: Lazy<PublicKey> = Lazy::new(|| make_node_pk(&SEED1));
4946
static NODE_PK2: Lazy<PublicKey> = Lazy::new(|| make_node_pk(&SEED2));
5047

51-
static SEALED_SEED1: Lazy<Vec<u8>> = Lazy::new(|| make_sealed_seed(&SEED1));
52-
static SEALED_SEED2: Lazy<Vec<u8>> = Lazy::new(|| make_sealed_seed(&SEED2));
48+
static SEALED_SEED1: Lazy<SealedSeed> = Lazy::new(|| make_sealed_seed(&SEED1));
49+
static SEALED_SEED2: Lazy<SealedSeed> = Lazy::new(|| make_sealed_seed(&SEED2));
5350

54-
pub fn sealed_seed(node_pk: &PublicKey) -> Vec<u8> {
51+
pub fn sealed_seed(node_pk: &PublicKey) -> SealedSeed {
5552
if node_pk == &*NODE_PK1 {
5653
SEALED_SEED1.clone()
5754
} else if node_pk == &*NODE_PK2 {
@@ -155,14 +152,7 @@ impl NodeBackendApi for MockApiClient {
155152
&self,
156153
data: SealedSeedId,
157154
) -> Result<Option<SealedSeed>, BackendApiError> {
158-
let sealed_seed = SealedSeed::new(
159-
data.node_pk,
160-
data.measurement,
161-
data.machine_id,
162-
data.min_cpusvn,
163-
sealed_seed(&data.node_pk),
164-
);
165-
Ok(Some(sealed_seed))
155+
Ok(Some(sealed_seed(&data.node_pk)))
166156
}
167157

168158
async fn create_node_instance_seed(

node/src/init.rs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@ use std::time::Duration;
66
use anyhow::{bail, ensure, Context};
77
use bitcoin::blockdata::constants::genesis_block;
88
use bitcoin::BlockHash;
9-
use common::api::provision::{Node, ProvisionedSecrets, SealedSeedId};
9+
use common::api::provision::{Node, SealedSeedId};
1010
use common::api::runner::{Port, UserPorts};
1111
use common::api::UserPk;
1212
use common::cli::{Network, RunArgs};
1313
use common::client::tls::node_run_tls_config;
1414
use common::enclave::{
15-
self, MachineId, Measurement, MinCpusvn, Sealed, MIN_SGX_CPUSVN,
15+
self, MachineId, Measurement, MinCpusvn, MIN_SGX_CPUSVN,
1616
};
1717
use common::rng::Crng;
18+
use common::root_seed::RootSeed;
1819
use common::shutdown::ShutdownChannel;
1920
use common::task::LxTask;
2021
use futures::future;
@@ -119,14 +120,15 @@ impl UserNode {
119120
let (channel_monitor_updated_tx, channel_monitor_updated_rx) =
120121
mpsc::channel(DEFAULT_CHANNEL_SIZE);
121122

122-
// Initialize LexeBitcoind, fetch provisioned data
123+
// Initialize LexeBitcoind while fetching provisioned secrets
123124
let (bitcoind_res, fetch_res) = tokio::join!(
124125
LexeBitcoind::init(
125126
args.bitcoind_rpc.clone(),
126127
args.network,
127128
shutdown.clone(),
128129
),
129130
fetch_provisioned_secrets(
131+
rng,
130132
api.as_ref(),
131133
user_pk,
132134
measurement,
@@ -136,9 +138,8 @@ impl UserNode {
136138
);
137139
let bitcoind =
138140
bitcoind_res.context("Failed to init bitcoind client")?;
139-
let (node, provisioned_secrets) =
141+
let (node, root_seed) =
140142
fetch_res.context("Failed to fetch provisioned secrets")?;
141-
let root_seed = &provisioned_secrets.root_seed;
142143
let bitcoind = Arc::new(bitcoind);
143144

144145
// Collect all handles to spawned tasks
@@ -148,8 +149,9 @@ impl UserNode {
148149
tasks.push(("refresh fees", bitcoind.spawn_refresh_fees_task()));
149150

150151
// Build LexeKeysManager from node init data
151-
let keys_manager = LexeKeysManager::init(rng, &node.node_pk, root_seed)
152-
.context("Failed to construct keys manager")?;
152+
let keys_manager =
153+
LexeKeysManager::init(rng, &node.node_pk, &root_seed)
154+
.context("Failed to construct keys manager")?;
153155
let node_pk = keys_manager.derive_pk(rng);
154156

155157
// LexeBitcoind implements BlockSource, FeeEstimator and
@@ -252,7 +254,7 @@ impl UserNode {
252254

253255
// Build owner service TLS config for authenticating owner
254256
let node_dns = args.node_dns_name.clone();
255-
let owner_tls = node_run_tls_config(rng, root_seed, vec![node_dns])
257+
let owner_tls = node_run_tls_config(rng, &root_seed, vec![node_dns])
256258
.context("Failed to build owner service TLS config")?;
257259

258260
// Start warp service for owner
@@ -514,13 +516,14 @@ fn init_api(args: &RunArgs) -> ApiClientType {
514516
}
515517

516518
/// Fetches previously provisioned secrets from the API.
517-
async fn fetch_provisioned_secrets(
519+
async fn fetch_provisioned_secrets<R: Crng>(
520+
rng: &mut R,
518521
api: &dyn ApiClient,
519522
user_pk: UserPk,
520523
measurement: Measurement,
521524
machine_id: MachineId,
522525
min_cpusvn: MinCpusvn,
523-
) -> anyhow::Result<(Node, ProvisionedSecrets)> {
526+
) -> anyhow::Result<(Node, RootSeed)> {
524527
debug!(%user_pk, %measurement, %machine_id, %min_cpusvn, "fetching provisioned secrets");
525528

526529
let (node_res, instance_res) = tokio::join!(
@@ -557,20 +560,17 @@ async fn fetch_provisioned_secrets(
557560
min_cpusvn,
558561
};
559562

560-
let raw_sealed_data = api
563+
let sealed_seed = api
561564
.get_sealed_seed(sealed_seed_id)
562565
.await
563566
.context("Error while fetching sealed seed")?
564567
.context("Sealed seed wasn't persisted with node & instance")?;
565568

566-
let sealed_data = Sealed::deserialize(&raw_sealed_data.seed)
567-
.context("Failed to deserialize sealed seed")?;
569+
let root_seed = sealed_seed
570+
.unseal_and_validate(rng)
571+
.context("Could not validate or unseal sealed seed")?;
568572

569-
let provisioned_secrets =
570-
ProvisionedSecrets::unseal(sealed_data)
571-
.context("Failed to unseal provisioned secrets")?;
572-
573-
Ok((node, provisioned_secrets))
573+
Ok((node, root_seed))
574574
}
575575
(None, None) => bail!("Enclave version has not been provisioned yet"),
576576
_ => bail!("Node and instance should have been persisted together"),

0 commit comments

Comments
 (0)