diff --git a/src/Cargo.lock b/src/Cargo.lock index d2bc489b..012b9c40 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -1335,6 +1335,7 @@ dependencies = [ "qos_test_primitives", "rand 0.9.2", "rustls", + "serde_json", "tokio", "tokio-rustls", "ureq", @@ -2001,6 +2002,7 @@ dependencies = [ "rustls", "serde", "serde_bytes", + "serde_json", "tokio", "tokio-vsock", "webpki-roots", diff --git a/src/integration/Cargo.toml b/src/integration/Cargo.toml index 9c1576b2..a1f68e29 100644 --- a/src/integration/Cargo.toml +++ b/src/integration/Cargo.toml @@ -23,6 +23,7 @@ tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } borsh = { workspace = true } nix = { workspace = true } rustls = { workspace = true } +serde_json = { workspace = true } webpki-roots = { workspace = true } tokio-rustls = { workspace = true } diff --git a/src/integration/examples/boot_enclave.rs b/src/integration/examples/boot_enclave.rs index f2864d56..cb248b83 100644 --- a/src/integration/examples/boot_enclave.rs +++ b/src/integration/examples/boot_enclave.rs @@ -7,7 +7,6 @@ use std::{ process::{Command, Stdio}, }; -use borsh::de::BorshDeserialize; use integration::{LOCAL_HOST, PCR3_PRE_IMAGE_PATH, QOS_DIST_DIR}; use qos_core::protocol::{ services::{ @@ -104,14 +103,13 @@ async fn main() { .success()); // Check the manifest written to file - let manifest = - Manifest::try_from_slice(&fs::read(&cli_manifest_path).unwrap()) - .unwrap(); + let manifest: Manifest = + serde_json::from_slice(&fs::read(&cli_manifest_path).unwrap()).unwrap(); - let genesis_output = { + let genesis_output: GenesisOutput = { let contents = fs::read("./mock/boot-e2e/genesis-dir/genesis_output").unwrap(); - GenesisOutput::try_from_slice(&contents).unwrap() + serde_json::from_slice(&contents).unwrap() }; // For simplicity sake, we use the same keys for the share set and manifest // set. @@ -222,9 +220,8 @@ async fn main() { assert!(child.wait().unwrap().success()); // Read in the generated approval to check it was created correctly - let approval = - Approval::try_from_slice(&fs::read(approval_path).unwrap()) - .unwrap(); + let approval: Approval = + serde_json::from_slice(&fs::read(approval_path).unwrap()).unwrap(); let personal_pair = P256Pair::from_hex_file(format!( "{}/{}.secret", personal_dir(alias), diff --git a/src/integration/tests/boot.rs b/src/integration/tests/boot.rs index 99790467..32360770 100644 --- a/src/integration/tests/boot.rs +++ b/src/integration/tests/boot.rs @@ -103,9 +103,8 @@ async fn standard_boot_e2e() { .success()); // Check the manifest written to file - let manifest = - Manifest::try_from_slice(&fs::read(&cli_manifest_path).unwrap()) - .unwrap(); + let manifest: Manifest = + serde_json::from_slice(&fs::read(&cli_manifest_path).unwrap()).unwrap(); let genesis_output = { let contents = @@ -238,9 +237,8 @@ async fn standard_boot_e2e() { assert!(child.wait().unwrap().success()); // Read in the generated approval to check it was created correctly - let approval = - Approval::try_from_slice(&fs::read(approval_path).unwrap()) - .unwrap(); + let approval: Approval = + serde_json::from_slice(&fs::read(approval_path).unwrap()).unwrap(); let personal_pair = P256Pair::from_hex_file(format!( "{}/{}.secret", personal_dir(alias), diff --git a/src/qos_client/src/cli/services.rs b/src/qos_client/src/cli/services.rs index e394eb98..39104747 100644 --- a/src/qos_client/src/cli/services.rs +++ b/src/qos_client/src/cli/services.rs @@ -113,9 +113,8 @@ pub enum Error { }, /// Failed to decode some hex CouldNotDecodeHex(qos_hex::HexError), - /// Failed to deserialize something from borsh. - #[allow(clippy::enum_variant_names)] - BorshError, + /// Failed to deserialize something from borsh or json + Deserialize, FailedToReadDrKey(qos_p256::P256Error), QosAttest(String), /// Pivot file @@ -141,9 +140,15 @@ pub enum Error { SecretDoesNotMatch, } +impl From for Error { + fn from(_: serde_json::Error) -> Self { + Self::Deserialize + } +} + impl From for Error { fn from(_: borsh::io::Error) -> Self { - Self::BorshError + Self::Deserialize } } @@ -759,7 +764,7 @@ pub(crate) fn generate_manifest>( write_with_msg( manifest_path.as_ref(), - &borsh::to_vec(&manifest).unwrap(), + &serde_json::to_vec(&manifest).expect("failed to serialize manifest"), "Manifest", ); @@ -862,7 +867,7 @@ pub(crate) fn approve_manifest>( )); write_with_msg( &approval_path, - &borsh::to_vec(&approval).expect("Failed to serialize approval"), + &serde_json::to_vec(&approval).expect("Failed to serialize approval"), "Manifest Approval", ); @@ -1010,7 +1015,7 @@ pub(crate) fn generate_manifest_envelope>( ); write_with_msg( &path, - &borsh::to_vec(&manifest_envelope) + &serde_json::to_vec(&manifest_envelope) .expect("Failed to serialize manifest envelope"), "Manifest Envelope", ); @@ -1076,7 +1081,7 @@ pub(crate) fn export_key>( write_with_msg( encrypted_quorum_key_path.as_ref(), - &borsh::to_vec(&encrypted_quorum_key).expect("valid borsh. qed."), + &serde_json::to_vec(&encrypted_quorum_key).expect("valid borsh. qed."), "Encrypted Quorum Key", ); @@ -1087,10 +1092,10 @@ pub(crate) fn inject_key>( uri: &str, encrypted_quorum_key_path: P, ) -> Result<(), Error> { - let encrypted_quorum_key = { + let encrypted_quorum_key: EncryptedQuorumKey = { let bytes = std::fs::read(encrypted_quorum_key_path) .map_err(|_| Error::FailedToReadEncryptedQuorumKey)?; - EncryptedQuorumKey::try_from_slice(&bytes) + serde_json::from_slice(&bytes) .map_err(|_| Error::InvalidEncryptedQuorumKey)? }; @@ -1201,8 +1206,8 @@ pub(crate) fn get_attestation_doc>( ); write_with_msg( manifest_envelope_path.as_ref(), - &borsh::to_vec(&manifest_envelope) - .expect("manifest enevelope is valid borsh"), + &serde_json::to_vec(&manifest_envelope) + .expect("manifest enevelope is valid json"), "Manifest envelope", ); } @@ -1313,7 +1318,7 @@ pub(crate) fn proxy_re_encrypt_share>( eph_pub.encrypt(plaintext_share).expect("Envelope encryption error") }; - let approval = borsh::to_vec(&Approval { + let approval = serde_json::to_vec(&Approval { signature: pair .sign(&manifest_envelope.manifest.qos_hash()) .expect("Failed to sign"), @@ -1573,11 +1578,10 @@ pub(crate) fn display>( file_path: P, json: bool, ) -> Result<(), Error> { - let bytes = - fs::read(file_path).map_err(|e| Error::ReadShare(e.to_string()))?; match *display_type { DisplayType::Manifest => { - let decoded = Manifest::try_from_slice_compat(&bytes)?; + let decoded = read_manifest(file_path)?; + if json { println!("{}", serde_json::to_string(&decoded).unwrap()); } else { @@ -1585,7 +1589,8 @@ pub(crate) fn display>( } } DisplayType::ManifestEnvelope => { - let decoded = ManifestEnvelope::try_from_slice(&bytes)?; + let decoded = read_manifest_envelope(file_path)?; + if json { println!("{}", serde_json::to_string(&decoded).unwrap()); } else { @@ -1593,6 +1598,8 @@ pub(crate) fn display>( } } DisplayType::GenesisOutput => { + let bytes = fs::read(file_path) + .map_err(|e| Error::ReadShare(e.to_string()))?; let decoded = GenesisOutput::try_from_slice(&bytes)?; println!("{decoded:#?}"); } @@ -1952,7 +1959,7 @@ fn find_approvals>( return None; }; - let approval = Approval::try_from_slice( + let approval: Approval = serde_json::from_slice( &fs::read(path).expect("Failed to read in approval"), ) .expect("Failed to deserialize approval"); @@ -1981,9 +1988,16 @@ fn find_approvals>( } fn read_manifest>(file: P) -> Result { - let buf = fs::read(file).map_err(Error::FailedToReadManifestFile)?; - Manifest::try_from_slice(&buf) - .map_err(|_| Error::FileDidNotHaveValidManifest) + let bytes = fs::read(file).map_err(Error::FailedToReadManifestFile)?; + + // try getting Manifest from json + let result = serde_json::from_slice::(&bytes); + if result.is_err() { + // if not try the old borsh format + Manifest::try_from_slice_compat(&bytes).map_err(Error::from) + } else { + result.map_err(Error::from) + } } fn read_attestation_doc>( @@ -2003,10 +2017,16 @@ fn read_attestation_doc>( fn read_manifest_envelope>( file: P, ) -> Result { - let buf = - fs::read(file).map_err(Error::FailedToReadManifestEnvelopeFile)?; - ManifestEnvelope::try_from_slice(&buf) - .map_err(|_| Error::FileDidNotHaveValidManifestEnvelope) + let bytes = fs::read(file).map_err(Error::FailedToReadManifestFile)?; + + // try getting Manifest from json + let result = serde_json::from_slice::(&bytes); + if result.is_err() { + // if not try the old borsh format + ManifestEnvelope::try_from_slice_compat(&bytes).map_err(Error::from) + } else { + result.map_err(Error::from) + } } fn read_attestation_approval>( @@ -2015,7 +2035,7 @@ fn read_attestation_approval>( let manifest_envelope = fs::read(path).map_err(Error::FailedToReadAttestationApproval)?; - Approval::try_from_slice(&manifest_envelope) + serde_json::from_slice(&manifest_envelope) .map_err(|_| Error::FileDidNotHaveValidAttestationApproval) } diff --git a/src/qos_core/Cargo.toml b/src/qos_core/Cargo.toml index 13d4e3a8..1f955448 100644 --- a/src/qos_core/Cargo.toml +++ b/src/qos_core/Cargo.toml @@ -22,6 +22,7 @@ borsh = { workspace = true } aws-nitro-enclaves-nsm-api = { workspace = true } serde_bytes = { workspace = true } +serde_json = { workspace = true } serde = { workspace = true } futures = { workspace = true } diff --git a/src/qos_core/src/handles.rs b/src/qos_core/src/handles.rs index 74e7a2e5..e6d515c9 100644 --- a/src/qos_core/src/handles.rs +++ b/src/qos_core/src/handles.rs @@ -1,8 +1,11 @@ //! Logic for accessing read only QOS state. -use std::{fs, os::unix::fs::PermissionsExt, path::Path}; +use std::{ + fs, + os::unix::fs::PermissionsExt, + path::{Path, PathBuf}, +}; -use borsh::BorshDeserialize; use qos_p256::P256Pair; use crate::protocol::{services::boot::ManifestEnvelope, ProtocolError}; @@ -179,8 +182,9 @@ impl Handles { ) -> Result { let contents = fs::read(&self.manifest) .map_err(|_| ProtocolError::FailedToGetManifestEnvelope)?; - let manifest = ManifestEnvelope::try_from_slice(&contents) + let manifest = serde_json::from_slice(&contents) .map_err(|_| ProtocolError::FailedToGetManifestEnvelope)?; + Ok(manifest) } @@ -195,7 +199,8 @@ impl Handles { ) -> Result<(), ProtocolError> { Self::write_as_read_only( &self.manifest, - &borsh::to_vec(manifest_envelope)?, + &serde_json::to_vec(manifest_envelope) + .map_err(|_| ProtocolError::FailedToPutManifestEnvelope)?, ProtocolError::FailedToPutManifestEnvelope, ) } @@ -219,8 +224,12 @@ impl Handles { &self.manifest, std::fs::Permissions::from_mode(0o666), )?; - fs::write(&self.manifest, borsh::to_vec(&manifest_envelope)?) - .map_err(|_| ProtocolError::FailedToPutManifestEnvelope)?; + fs::write( + &self.manifest, + serde_json::to_vec(&manifest_envelope) + .map_err(|_| ProtocolError::FailedToPutManifestEnvelope)?, + ) + .map_err(|_| ProtocolError::FailedToPutManifestEnvelope)?; // Set the permissions back to read only fs::set_permissions( @@ -272,7 +281,7 @@ impl Handles { Path::new(&self.pivot).exists() } - /// Helper function for ready only writes. + /// Helper function for ready only writes that also ensures full write atomicity by renaming at the end. fn write_as_read_only>( path: P, buf: &[u8], @@ -288,7 +297,13 @@ impl Handles { } } - fs::write(&path, buf).map_err(|_| err.clone())?; + let tmp_path = PathBuf::from(path.as_ref()).with_extension("tmp"); + + fs::write(&tmp_path, buf).map_err(|_| err.clone())?; + + // atomically move to destination once fully written to prevent partial reads + fs::rename(&tmp_path, &path)?; + fs::set_permissions(&path, fs::Permissions::from_mode(0o444)) .map_err(|_| err)?; diff --git a/src/qos_core/src/protocol/services/boot.rs b/src/qos_core/src/protocol/services/boot.rs index 864b9949..89f37bd8 100644 --- a/src/qos_core/src/protocol/services/boot.rs +++ b/src/qos_core/src/protocol/services/boot.rs @@ -295,6 +295,9 @@ impl fmt::Debug for Namespace { } /// The Manifest for the enclave. +/// NOTE: we currently use JSON format for storing this value. +/// Since we don't have any `HashMap` inside the `Manifest` it works out of the box. +/// If we ever do need a map inside, we should use a `BTreeMap` to ensure keys are sorted. #[derive( PartialEq, Eq, @@ -345,8 +348,23 @@ pub struct ManifestV0 { pub patch_set: PatchSet, } +impl From for Manifest { + fn from(old: ManifestV0) -> Self { + Self { + namespace: old.namespace, + pivot: old.pivot, + manifest_set: old.manifest_set, + share_set: old.share_set, + enclave: old.enclave, + patch_set: old.patch_set, + pool_size: None, + client_timeout_ms: None, + } + } +} + impl Manifest { - /// Read a manifest from a `u8` buffer, in a backwards compatible way + /// Read a `Manifest` in borsh encoded format from a `u8` buffer, in a backwards compatible way pub fn try_from_slice_compat(buf: &[u8]) -> Result { use borsh::BorshDeserialize; @@ -356,16 +374,7 @@ impl Manifest { if result.is_err() { let old = ManifestV0::try_from_slice(buf)?; - Ok(Self { - namespace: old.namespace, - pivot: old.pivot, - manifest_set: old.manifest_set, - share_set: old.share_set, - enclave: old.enclave, - patch_set: old.patch_set, - pool_size: None, - client_timeout_ms: None, - }) + Ok(old.into()) } else { result } @@ -437,6 +446,19 @@ pub struct ManifestEnvelope { pub share_set_approvals: Vec, } +/// [`ManifestV0`] with accompanying [`Approval`]s. +#[derive(PartialEq, Eq, Debug, Clone, borsh::BorshDeserialize)] +#[cfg_attr(any(feature = "mock", test), derive(Default))] +pub struct ManifestEnvelopeV0 { + /// Encapsulated manifest. + pub manifest: ManifestV0, + /// Approvals for [`Self::manifest`] from the manifest set. + pub manifest_set_approvals: Vec, + /// Approvals for [`Self::manifest`] from the share set. This is primarily + /// used to audit what share holders provisioned the quorum key. + pub share_set_approvals: Vec, +} + impl ManifestEnvelope { /// Check if the encapsulated manifest has K valid approvals from the /// manifest approval set. @@ -477,6 +499,25 @@ impl ManifestEnvelope { Ok(()) } + /// Read a `ManifestEnvelope` from a `u8` buffer, in a backwards compatible way + pub fn try_from_slice_compat(buf: &[u8]) -> Result { + use borsh::BorshDeserialize; + + let result = Self::try_from_slice(buf); + + // try loading the old version of manifest + if result.is_err() { + let old = ManifestEnvelopeV0::try_from_slice(buf)?; + + Ok(Self { + manifest: Manifest::from(old.manifest), + manifest_set_approvals: old.manifest_set_approvals, + share_set_approvals: old.share_set_approvals, + }) + } else { + result + } + } } pub(in crate::protocol::services) fn put_manifest_and_pivot( diff --git a/src/qos_core/src/protocol/services/genesis.rs b/src/qos_core/src/protocol/services/genesis.rs index 9e302fad..c5483e6a 100644 --- a/src/qos_core/src/protocol/services/genesis.rs +++ b/src/qos_core/src/protocol/services/genesis.rs @@ -5,6 +5,7 @@ use std::{fmt, iter::zip}; use qos_crypto::sha_512; use qos_nsm::types::{NsmRequest, NsmResponse}; use qos_p256::{P256Pair, P256Public}; +use serde::{Deserialize, Serialize}; use crate::protocol::{ services::boot::QuorumMember, ProtocolError, ProtocolState, QosHash, @@ -24,7 +25,14 @@ pub struct GenesisSet { pub threshold: u32, } -#[derive(PartialEq, Clone, borsh::BorshSerialize, borsh::BorshDeserialize)] +#[derive( + PartialEq, + Clone, + borsh::BorshSerialize, + borsh::BorshDeserialize, + Serialize, + Deserialize, +)] struct MemberShard { /// Member of the Setup Set. member: QuorumMember, @@ -45,7 +53,13 @@ impl fmt::Debug for MemberShard { /// A set of member shards used to successfully recover the quorum key during /// the genesis ceremony. #[derive( - PartialEq, Debug, Clone, borsh::BorshSerialize, borsh::BorshDeserialize, + PartialEq, + Debug, + Clone, + borsh::BorshSerialize, + borsh::BorshDeserialize, + Serialize, + Deserialize, )] pub struct RecoveredPermutation(Vec); @@ -87,7 +101,14 @@ impl fmt::Debug for GenesisMemberOutput { /// Output from running Genesis Boot. Should contain all information relevant to /// how the quorum shares where created. -#[derive(PartialEq, Clone, borsh::BorshSerialize, borsh::BorshDeserialize)] +#[derive( + PartialEq, + Clone, + borsh::BorshSerialize, + borsh::BorshDeserialize, + Serialize, + Deserialize, +)] pub struct GenesisOutput { /// Public Quorum Key, DER encoded. pub quorum_key: Vec, @@ -101,6 +122,7 @@ pub struct GenesisOutput { /// The quorum key encrypted to the DR key. None if no DR Key was provided pub dr_key_wrapped_quorum_key: Option>, /// Hash of the quorum key secret + #[serde(with = "qos_hex::serde")] pub quorum_key_hash: [u8; 64], /// Test message encrypted to the quorum public key. pub test_message_ciphertext: Vec, diff --git a/src/qos_core/src/protocol/services/key.rs b/src/qos_core/src/protocol/services/key.rs index aa784362..874f7c06 100644 --- a/src/qos_core/src/protocol/services/key.rs +++ b/src/qos_core/src/protocol/services/key.rs @@ -7,6 +7,7 @@ use qos_nsm::{ types::NsmResponse, }; use qos_p256::{P256Pair, P256Public}; +use serde::{Deserialize, Serialize}; use crate::protocol::{ services::boot::{put_manifest_and_pivot, ManifestEnvelope}, @@ -15,7 +16,7 @@ use crate::protocol::{ /// An encrypted quorum key along with a signature over the encrypted payload /// from the sender. -#[derive(BorshDeserialize, BorshSerialize)] +#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] pub struct EncryptedQuorumKey { /// The encrypted payload: a quorum key pub encrypted_quorum_key: Vec, @@ -1051,7 +1052,7 @@ mod test { std::fs::write( &*manifest_file, - borsh::to_vec(&manifest_envelope).unwrap(), + serde_json::to_vec(&manifest_envelope).unwrap(), ) .unwrap(); let handles = Handles::new( @@ -1106,7 +1107,7 @@ mod test { "inject_key_works.quorum.secret".into(); std::fs::write( &*manifest_file, - borsh::to_vec(&manifest_envelope).unwrap(), + serde_json::to_vec(&manifest_envelope).unwrap(), ) .unwrap(); @@ -1163,7 +1164,7 @@ mod test { "inject_rejects_bad_signature.quorum.secret".into(); std::fs::write( &*manifest_file, - borsh::to_vec(&manifest_envelope).unwrap(), + serde_json::to_vec(&manifest_envelope).unwrap(), ) .unwrap(); @@ -1214,7 +1215,7 @@ mod test { "inject_key_rejects_wrong_quorum_key.quorum.secret".into(); std::fs::write( &*manifest_file, - borsh::to_vec(&manifest_envelope).unwrap(), + serde_json::to_vec(&manifest_envelope).unwrap(), ) .unwrap(); @@ -1265,7 +1266,7 @@ mod test { "inject_key_rejects_invalid_quorum_key.quorum.secret".into(); std::fs::write( &*manifest_file, - borsh::to_vec(&manifest_envelope).unwrap(), + serde_json::to_vec(&manifest_envelope).unwrap(), ) .unwrap();