Skip to content

Trust-Quorum: Messages, Config, Crypto #7859

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: ajs/shamir
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ members = [
"sled-storage",
"sp-sim",
"test-utils",
"trust-quorum",
"trust-quorum/gfss",
"typed-rng",
"update-common",
Expand Down Expand Up @@ -265,6 +266,7 @@ default-members = [
"sled-hardware/types",
"sled-storage",
"sp-sim",
"trust-quorum",
"trust-quorum/gfss",
"test-utils",
"typed-rng",
Expand Down Expand Up @@ -386,6 +388,7 @@ derive-where = "1.2.7"
# Having the i-implement-... feature here makes diesel go away from the workspace-hack
diesel = { version = "2.2.8", features = ["i-implement-a-third-party-backend-and-opt-into-breaking-changes", "postgres", "r2d2", "chrono", "serde_json", "network-address", "uuid"] }
diesel-dtrace = "0.4.2"
digest = "0.10.7"
dns-server = { path = "dns-server" }
dns-server-api = { path = "dns-server-api" }
dns-service-client = { path = "clients/dns-service-client" }
Expand Down Expand Up @@ -420,6 +423,7 @@ gateway-sp-comms = { git = "https://github.com/oxidecomputer/management-gateway-
gateway-test-utils = { path = "gateway-test-utils" }
gateway-types = { path = "gateway-types" }
gethostname = "0.5.0"
gfss = { path = "trust-quorum/gfss" }
glob = "0.3.2"
guppy = "0.17.17"
headers = "0.4.0"
Expand Down Expand Up @@ -658,6 +662,7 @@ static_assertions = "1.1.0"
steno = "0.4.1"
strum = { version = "0.26", features = [ "derive" ] }
subprocess = "0.2.9"
subtle = "2.6.1"
supports-color = "3.0.2"
swrite = "0.1.0"
sync-ptr = "0.1.1"
Expand Down
36 changes: 36 additions & 0 deletions trust-quorum/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[package]
name = "trust-quorum"
version = "0.1.0"
edition = "2021"
license = "MPL-2.0"

[lints]
workspace = true

[dependencies]
bcs.workspace = true
bootstore.workspace = true
camino.workspace = true
chacha20poly1305.workspace = true
derive_more.workspace = true
gfss.workspace = true
hex.workspace = true
hkdf.workspace = true
rand = { workspace = true, features = ["getrandom"] }
secrecy.workspace = true
serde.workspace = true
serde_with.workspace = true
sha3.workspace = true
slog.workspace = true
slog-error-chain.workspace = true
subtle.workspace = true
thiserror.workspace = true
tokio.workspace = true
uuid.workspace = true
omicron-uuid-kinds.workspace = true
zeroize.workspace = true
omicron-workspace-hack.workspace = true

[dev-dependencies]
proptest.workspace = true
test-strategy.workspace = true
4 changes: 3 additions & 1 deletion trust-quorum/gfss/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ license = "MPL-2.0"
workspace = true

[dependencies]
digest.workspace = true
rand = { workspace = true, features = ["getrandom"] }
secrecy.workspace = true
subtle = "2.6"
serde.workspace = true
subtle.workspace = true
thiserror.workspace = true
zeroize.workspace = true
omicron-workspace-hack.workspace = true
Expand Down
9 changes: 8 additions & 1 deletion trust-quorum/gfss/src/gf256.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use core::fmt::{self, Binary, Display, Formatter, LowerHex, UpperHex};
use core::ops::{Add, AddAssign, Div, Mul, MulAssign, Sub};
use rand::Rng;
use rand::distributions::{Distribution, Standard};
use serde::{Deserialize, Serialize};
use subtle::ConstantTimeEq;
use zeroize::Zeroize;

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

impl AsRef<u8> for Gf256 {
fn as_ref(&self) -> &u8 {
&self.0
}
}

impl Gf256 {
pub fn new(n: u8) -> Gf256 {
Gf256(n)
Expand Down
27 changes: 26 additions & 1 deletion trust-quorum/gfss/src/shamir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

//! Shamir secret sharing over GF(2^8)

use digest::Digest;
use rand::{Rng, rngs::OsRng};
use secrecy::Secret;
use serde::{Deserialize, Serialize};
use subtle::ConstantTimeEq;
use zeroize::{Zeroize, ZeroizeOnDrop};

Expand Down Expand Up @@ -101,12 +103,35 @@ impl<'a> ValidShares<'a> {
}
}

#[derive(Clone, Zeroize, ZeroizeOnDrop)]
#[derive(Clone, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
pub struct Share {
pub x_coordinate: Gf256,
pub y_coordinates: Box<[Gf256]>,
}

impl Share {
// Return a cryptographic hash of a Share using the parameterized
// algorithm.
pub fn digest<D: Digest>(&self, output: &mut [u8]) {
let mut hasher = D::new();
hasher.update([*self.x_coordinate.as_ref()]);
// Implementing AsRef<[u8]> for Box<[Gf256]> doesn't work due to
// coherence rules. To get around that we'd need a transparent newtype
// for the y_coordinates and some unsafe code, which we're loathe to do.
let mut ys: Vec<u8> =
self.y_coordinates.iter().map(|y| *y.as_ref()).collect();
hasher.update(&ys);
output.copy_from_slice(&hasher.finalize());
ys.zeroize();
}
}

impl std::fmt::Debug for Share {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KeyShareGf256").finish()
}
}

pub struct SecretShares {
pub threshold: ValidThreshold,
pub shares: Secret<Vec<Share>>,
Expand Down
119 changes: 119 additions & 0 deletions trust-quorum/src/configuration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! A configuration of a trust quroum at a given epoch

use crate::crypto::{EncryptedRackSecret, RackSecret, Salt, Sha3_256Digest};
use crate::{Epoch, PlatformId, ReconfigureMsg, Threshold};
use gfss::shamir::SplitError;
use omicron_uuid_kinds::RackUuid;
use secrecy::ExposeSecret;
use serde::{Deserialize, Serialize};
use slog_error_chain::SlogInlineError;
use std::collections::BTreeMap;

#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq, SlogInlineError)]
pub enum ConfigurationError {
#[error("rack secret split error")]
RackSecretSplit(
#[from]
#[source]
SplitError,
),
#[error("too many members: must be fewer than 255")]
TooManyMembers,
}

/// The configuration for a given epoch.
///
/// Only valid for non-lrtq configurations
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Configuration {
/// Unique Id of the rack
pub rack_id: RackUuid,

// Unique, monotonically increasing identifier for a configuration
pub epoch: Epoch,

/// Who was the coordinator of this reconfiguration?
pub coordinator: PlatformId,

// All members of the current configuration and the hash of their key shares
pub members: BTreeMap<PlatformId, Sha3_256Digest>,

/// The number of sleds required to reconstruct the rack secret
pub threshold: Threshold,

// There is no previous configuration for the initial configuration
pub previous_configuration: Option<PreviousConfiguration>,
}

impl Configuration {
/// Create a new configuration for the trust quorum
///
/// `previous_configuration` is never filled in upon construction. A
/// coordinator will fill this in as necessary after retrieving shares for
/// the last committed epoch.
pub fn new(
coordinator: PlatformId,
reconfigure_msg: &ReconfigureMsg,
) -> Result<Configuration, ConfigurationError> {
let rack_secret = RackSecret::new();
let shares = rack_secret.split(
reconfigure_msg.threshold,
reconfigure_msg
.members
.len()
.try_into()
.map_err(|_| ConfigurationError::TooManyMembers)?,
)?;

let share_digests = shares.shares.expose_secret().iter().map(|s| {
let mut digest = Sha3_256Digest::default();
s.digest::<sha3::Sha3_256>(&mut digest.0);
digest
});

let members = reconfigure_msg
.members
.iter()
.cloned()
.zip(share_digests)
.collect();

Ok(Configuration {
rack_id: reconfigure_msg.rack_id,
epoch: reconfigure_msg.epoch,
coordinator,
members,
threshold: reconfigure_msg.threshold,
previous_configuration: None,
})
}
}

/// Information for the last committed configuration that is necessary to track
/// in the next `Configuration`.
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
)]
pub struct PreviousConfiguration {
/// The epoch of the last committed configuration
pub epoch: Epoch,

/// Is the previous configuration LRTQ?
pub is_lrtq: bool,

/// The encrypted rack secret for the last committed epoch
///
/// This allows us to derive old encryption keys so they can be rotated
pub encrypted_last_committed_rack_secret: EncryptedRackSecret,

/// A random value used to derive the key to encrypt the rack secret from
/// the last committed epoch.
///
/// We only encrypt the rack secret once and so we use a nonce of all zeros.
/// This is why there is no corresponding `nonce` field.
pub encrypted_last_committed_rack_secret_salt: Salt,
}
Loading
Loading