Skip to content

Commit 5cfd735

Browse files
Trust-Quorum: Messages, Config, Crypto (#7859)
This is the initial commit of the "real" trust quorum code as described in RFD 238. This commit provides some of the foundation needed to implement the trust quorum reconfiguration protocol. The protocol itself will be implemented in a `Node` type in a [sans-io](https://sans-io.readthedocs.io/) style with property based tests for the full protocol, similar to LRTQ. Async code will utilize the protocol and forward messages over sprockets channels, and handle requests from Nexus. This initial code was split out to keep the PR small, although it has been used in some preliminary protocol code already that will come in a follow up PR. --------- Co-authored-by: Rain <[email protected]>
1 parent b5a9136 commit 5cfd735

File tree

11 files changed

+656
-3
lines changed

11 files changed

+656
-3
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ members = [
131131
"sled-storage",
132132
"sp-sim",
133133
"test-utils",
134+
"trust-quorum",
134135
"trust-quorum/gfss",
135136
"typed-rng",
136137
"update-common",
@@ -279,6 +280,7 @@ default-members = [
279280
"sled-hardware/types",
280281
"sled-storage",
281282
"sp-sim",
283+
"trust-quorum",
282284
"trust-quorum/gfss",
283285
"test-utils",
284286
"typed-rng",
@@ -401,6 +403,7 @@ dev-tools-common = { path = "dev-tools/common" }
401403
# Having the i-implement-... feature here makes diesel go away from the workspace-hack
402404
diesel = { version = "2.2.9", features = ["i-implement-a-third-party-backend-and-opt-into-breaking-changes", "postgres", "r2d2", "chrono", "serde_json", "network-address", "uuid"] }
403405
diesel-dtrace = "0.4.2"
406+
digest = "0.10.7"
404407
dns-server = { path = "dns-server" }
405408
dns-server-api = { path = "dns-server-api" }
406409
dns-service-client = { path = "clients/dns-service-client" }
@@ -435,6 +438,7 @@ gateway-sp-comms = { git = "https://github.com/oxidecomputer/management-gateway-
435438
gateway-test-utils = { path = "gateway-test-utils" }
436439
gateway-types = { path = "gateway-types" }
437440
gethostname = "0.5.0"
441+
gfss = { path = "trust-quorum/gfss" }
438442
glob = "0.3.2"
439443
guppy = "0.17.17"
440444
headers = "0.4.0"
@@ -679,7 +683,7 @@ static_assertions = "1.1.0"
679683
steno = "0.4.1"
680684
strum = { version = "0.26", features = [ "derive" ] }
681685
subprocess = "0.2.9"
682-
subtle = "2.6"
686+
subtle = "2.6.1"
683687
supports-color = "3.0.2"
684688
swrite = "0.1.0"
685689
sync-ptr = "0.1.1"

trust-quorum/Cargo.toml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
[package]
2+
name = "trust-quorum"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license = "MPL-2.0"
6+
7+
[lints]
8+
workspace = true
9+
10+
[dependencies]
11+
bcs.workspace = true
12+
bootstore.workspace = true
13+
camino.workspace = true
14+
chacha20poly1305.workspace = true
15+
derive_more.workspace = true
16+
gfss.workspace = true
17+
hex.workspace = true
18+
hkdf.workspace = true
19+
rand = { workspace = true, features = ["getrandom"] }
20+
secrecy.workspace = true
21+
serde.workspace = true
22+
serde_with.workspace = true
23+
sha3.workspace = true
24+
slog.workspace = true
25+
slog-error-chain.workspace = true
26+
subtle.workspace = true
27+
thiserror.workspace = true
28+
tokio.workspace = true
29+
uuid.workspace = true
30+
omicron-uuid-kinds.workspace = true
31+
zeroize.workspace = true
32+
omicron-workspace-hack.workspace = true
33+
34+
[dev-dependencies]
35+
proptest.workspace = true
36+
test-strategy.workspace = true

trust-quorum/gfss/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ license = "MPL-2.0"
99
workspace = true
1010

1111
[dependencies]
12+
digest.workspace = true
1213
rand = { workspace = true, features = ["getrandom"] }
1314
secrecy.workspace = true
15+
serde.workspace = true
1416
subtle.workspace = true
1517
thiserror.workspace = true
1618
zeroize.workspace = true

trust-quorum/gfss/src/gf256.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use core::fmt::{self, Binary, Display, Formatter, LowerHex, UpperHex};
2626
use core::ops::{Add, AddAssign, Div, Mul, MulAssign, Sub};
2727
use rand::Rng;
2828
use rand::distributions::{Distribution, Standard};
29+
use serde::{Deserialize, Serialize};
2930
use subtle::ConstantTimeEq;
3031
use zeroize::Zeroize;
3132

@@ -34,9 +35,15 @@ use zeroize::Zeroize;
3435
/// We explicitly don't enable the equality operators to prevent ourselves from
3536
/// accidentally using those instead of the constant time ones.
3637
#[repr(transparent)]
37-
#[derive(Debug, Clone, Copy, Zeroize)]
38+
#[derive(Debug, Clone, Copy, Zeroize, Serialize, Deserialize)]
3839
pub struct Gf256(u8);
3940

41+
impl AsRef<u8> for Gf256 {
42+
fn as_ref(&self) -> &u8 {
43+
&self.0
44+
}
45+
}
46+
4047
impl Gf256 {
4148
pub fn new(n: u8) -> Gf256 {
4249
Gf256(n)

trust-quorum/gfss/src/shamir.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
//! Shamir secret sharing over GF(2^8)
66
7+
use digest::Digest;
78
use rand::{Rng, rngs::OsRng};
89
use secrecy::Secret;
10+
use serde::{Deserialize, Serialize};
911
use subtle::ConstantTimeEq;
1012
use zeroize::{Zeroize, ZeroizeOnDrop};
1113

@@ -101,12 +103,35 @@ impl<'a> ValidShares<'a> {
101103
}
102104
}
103105

104-
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
106+
#[derive(Clone, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
105107
pub struct Share {
106108
pub x_coordinate: Gf256,
107109
pub y_coordinates: Box<[Gf256]>,
108110
}
109111

112+
impl Share {
113+
// Return a cryptographic hash of a Share using the parameterized
114+
// algorithm.
115+
pub fn digest<D: Digest>(&self, output: &mut [u8]) {
116+
let mut hasher = D::new();
117+
hasher.update([*self.x_coordinate.as_ref()]);
118+
// Implementing AsRef<[u8]> for Box<[Gf256]> doesn't work due to
119+
// coherence rules. To get around that we'd need a transparent newtype
120+
// for the y_coordinates and some unsafe code, which we're loathe to do.
121+
let mut ys: Vec<u8> =
122+
self.y_coordinates.iter().map(|y| *y.as_ref()).collect();
123+
hasher.update(&ys);
124+
output.copy_from_slice(&hasher.finalize());
125+
ys.zeroize();
126+
}
127+
}
128+
129+
impl std::fmt::Debug for Share {
130+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131+
f.debug_struct("KeyShareGf256").finish()
132+
}
133+
}
134+
110135
pub struct SecretShares {
111136
pub threshold: ValidThreshold,
112137
pub shares: Secret<Vec<Share>>,

trust-quorum/src/configuration.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
//! A configuration of a trust quroum at a given epoch
6+
7+
use crate::crypto::{EncryptedRackSecret, RackSecret, Salt, Sha3_256Digest};
8+
use crate::{Epoch, PlatformId, ReconfigureMsg, Threshold};
9+
use gfss::shamir::SplitError;
10+
use omicron_uuid_kinds::RackUuid;
11+
use secrecy::ExposeSecret;
12+
use serde::{Deserialize, Serialize};
13+
use slog_error_chain::SlogInlineError;
14+
use std::collections::BTreeMap;
15+
16+
#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq, SlogInlineError)]
17+
pub enum ConfigurationError {
18+
#[error("rack secret split error")]
19+
RackSecretSplit(
20+
#[from]
21+
#[source]
22+
SplitError,
23+
),
24+
#[error("too many members: must be fewer than 255")]
25+
TooManyMembers,
26+
}
27+
28+
/// The configuration for a given epoch.
29+
///
30+
/// Only valid for non-lrtq configurations
31+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32+
pub struct Configuration {
33+
/// Unique Id of the rack
34+
pub rack_id: RackUuid,
35+
36+
// Unique, monotonically increasing identifier for a configuration
37+
pub epoch: Epoch,
38+
39+
/// Who was the coordinator of this reconfiguration?
40+
pub coordinator: PlatformId,
41+
42+
// All members of the current configuration and the hash of their key shares
43+
pub members: BTreeMap<PlatformId, Sha3_256Digest>,
44+
45+
/// The number of sleds required to reconstruct the rack secret
46+
pub threshold: Threshold,
47+
48+
// There is no previous configuration for the initial configuration
49+
pub previous_configuration: Option<PreviousConfiguration>,
50+
}
51+
52+
impl Configuration {
53+
/// Create a new configuration for the trust quorum
54+
///
55+
/// `previous_configuration` is never filled in upon construction. A
56+
/// coordinator will fill this in as necessary after retrieving shares for
57+
/// the last committed epoch.
58+
pub fn new(
59+
coordinator: PlatformId,
60+
reconfigure_msg: &ReconfigureMsg,
61+
) -> Result<Configuration, ConfigurationError> {
62+
let rack_secret = RackSecret::new();
63+
let shares = rack_secret.split(
64+
reconfigure_msg.threshold,
65+
reconfigure_msg
66+
.members
67+
.len()
68+
.try_into()
69+
.map_err(|_| ConfigurationError::TooManyMembers)?,
70+
)?;
71+
72+
let share_digests = shares.shares.expose_secret().iter().map(|s| {
73+
let mut digest = Sha3_256Digest::default();
74+
s.digest::<sha3::Sha3_256>(&mut digest.0);
75+
digest
76+
});
77+
78+
let members = reconfigure_msg
79+
.members
80+
.iter()
81+
.cloned()
82+
.zip(share_digests)
83+
.collect();
84+
85+
Ok(Configuration {
86+
rack_id: reconfigure_msg.rack_id,
87+
epoch: reconfigure_msg.epoch,
88+
coordinator,
89+
members,
90+
threshold: reconfigure_msg.threshold,
91+
previous_configuration: None,
92+
})
93+
}
94+
}
95+
96+
/// Information for the last committed configuration that is necessary to track
97+
/// in the next `Configuration`.
98+
#[derive(
99+
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
100+
)]
101+
pub struct PreviousConfiguration {
102+
/// The epoch of the last committed configuration
103+
pub epoch: Epoch,
104+
105+
/// Is the previous configuration LRTQ?
106+
pub is_lrtq: bool,
107+
108+
/// The encrypted rack secret for the last committed epoch
109+
///
110+
/// This allows us to derive old encryption keys so they can be rotated
111+
pub encrypted_last_committed_rack_secret: EncryptedRackSecret,
112+
113+
/// A random value used to derive the key to encrypt the rack secret from
114+
/// the last committed epoch.
115+
///
116+
/// We only encrypt the rack secret once and so we use a nonce of all zeros.
117+
/// This is why there is no corresponding `nonce` field.
118+
pub encrypted_last_committed_rack_secret_salt: Salt,
119+
}

0 commit comments

Comments
 (0)