use std::{collections::HashSet, fmt};
use qos_crypto::sha_256;
use qos_nsm::types::NsmResponse;
use qos_p256::{P256Pair, P256Public};
use crate::protocol::{
services::attestation, Hash256, ProtocolError, ProtocolState, QosHash,
};
#[derive(
PartialEq,
Eq,
Clone,
borsh::BorshSerialize,
borsh::BorshDeserialize,
serde::Serialize,
serde::Deserialize,
)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(any(feature = "mock", test), derive(Default))]
pub struct NitroConfig {
#[serde(with = "qos_hex::serde")]
pub pcr0: Vec<u8>,
#[serde(with = "qos_hex::serde")]
pub pcr1: Vec<u8>,
#[serde(with = "qos_hex::serde")]
pub pcr2: Vec<u8>,
#[serde(with = "qos_hex::serde")]
pub pcr3: Vec<u8>,
#[serde(with = "qos_hex::serde")]
pub aws_root_certificate: Vec<u8>,
pub qos_commit: String,
}
impl fmt::Debug for NitroConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NitroConfig")
.field("pcr0", &qos_hex::encode(&self.pcr0))
.field("pcr1", &qos_hex::encode(&self.pcr1))
.field("pcr2", &qos_hex::encode(&self.pcr2))
.field("pcr3", &qos_hex::encode(&self.pcr3))
.field("qos_commit", &self.qos_commit)
.finish_non_exhaustive()
}
}
#[derive(
PartialEq,
Eq,
Clone,
Copy,
borsh::BorshSerialize,
borsh::BorshDeserialize,
serde::Serialize,
serde::Deserialize,
)]
pub enum RestartPolicy {
Never,
Always,
}
impl fmt::Debug for RestartPolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Never => write!(f, "RestartPolicy::Never")?,
Self::Always => write!(f, "RestartPolicy::Always")?,
};
Ok(())
}
}
#[cfg(any(feature = "mock", test))]
impl Default for RestartPolicy {
fn default() -> Self {
Self::Never
}
}
impl TryFrom<String> for RestartPolicy {
type Error = ProtocolError;
fn try_from(s: String) -> Result<RestartPolicy, Self::Error> {
match s.to_ascii_lowercase().as_str() {
"never" => Ok(Self::Never),
"always" => Ok(Self::Always),
_ => Err(ProtocolError::FailedToParseFromString),
}
}
}
#[derive(
PartialEq,
Eq,
Clone,
serde::Serialize,
serde::Deserialize,
borsh::BorshSerialize,
borsh::BorshDeserialize,
)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum BridgeConfig {
Server {
port: u16,
host: String,
},
Client {
port: u16,
host: Option<String>,
},
}
impl Default for BridgeConfig {
fn default() -> Self {
Self::Server {
port: DEFAULT_APP_HOST_PORT,
host: DEFAULT_APP_HOST_IP.into(),
}
}
}
impl BridgeConfig {
pub fn port(&self) -> u16 {
match self {
Self::Server { port, host: _ } | Self::Client { port, host: _ } => {
*port
}
}
}
}
#[derive(
PartialEq,
Eq,
Clone,
borsh::BorshSerialize,
borsh::BorshDeserialize,
serde::Serialize,
serde::Deserialize,
)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(any(feature = "mock", test), derive(Default))]
pub struct PivotConfig {
#[serde(with = "qos_hex::serde")]
pub hash: Hash256,
pub restart: RestartPolicy,
pub bridge_config: Vec<BridgeConfig>,
pub debug_mode: bool,
pub args: Vec<String>,
}
#[derive(
PartialEq,
Eq,
Clone,
Debug,
borsh::BorshSerialize,
borsh::BorshDeserialize,
serde::Serialize,
serde::Deserialize,
)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(any(feature = "mock", test), derive(Default))]
pub struct PivotConfigV0 {
#[serde(with = "qos_hex::serde")]
pub hash: Hash256,
pub restart: RestartPolicy,
pub args: Vec<String>,
}
impl From<PivotConfigV0> for PivotConfig {
fn from(value: PivotConfigV0) -> Self {
Self {
hash: value.hash,
restart: value.restart,
args: value.args,
debug_mode: false,
bridge_config: Vec::new(),
}
}
}
impl fmt::Debug for PivotConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PivotConfig")
.field("hash", &qos_hex::encode(&self.hash))
.field("restart", &self.restart)
.field("args", &self.args.join(" "))
.finish()
}
}
#[derive(
PartialEq,
Clone,
borsh::BorshSerialize,
borsh::BorshDeserialize,
Eq,
PartialOrd,
Ord,
serde::Serialize,
serde::Deserialize,
)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(any(feature = "mock", test), derive(Default))]
pub struct QuorumMember {
pub alias: String,
#[serde(with = "qos_hex::serde")]
pub pub_key: Vec<u8>,
}
impl fmt::Debug for QuorumMember {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("QuorumMember")
.field("alias", &self.alias)
.field("pub_key", &qos_hex::encode(&self.pub_key))
.finish()
}
}
#[derive(
PartialEq,
Eq,
Debug,
Clone,
borsh::BorshSerialize,
borsh::BorshDeserialize,
serde::Serialize,
serde::Deserialize,
)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(any(feature = "mock", test), derive(Default))]
pub struct ManifestSet {
pub threshold: u32,
pub members: Vec<QuorumMember>,
}
#[derive(
PartialEq,
Eq,
Debug,
Clone,
borsh::BorshSerialize,
borsh::BorshDeserialize,
serde::Serialize,
serde::Deserialize,
)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(any(feature = "mock", test), derive(Default))]
pub struct ShareSet {
pub threshold: u32,
pub members: Vec<QuorumMember>,
}
#[derive(
PartialEq,
PartialOrd,
Ord,
Eq,
Clone,
borsh::BorshSerialize,
borsh::BorshDeserialize,
serde::Serialize,
serde::Deserialize,
)]
#[serde(rename_all = "camelCase")]
pub struct MemberPubKey {
#[serde(with = "qos_hex::serde")]
pub pub_key: Vec<u8>,
}
impl fmt::Debug for MemberPubKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MemberPubKey")
.field("pub_key", &qos_hex::encode(&self.pub_key))
.finish()
}
}
#[derive(
PartialEq,
Eq,
Debug,
Clone,
borsh::BorshSerialize,
borsh::BorshDeserialize,
serde::Serialize,
serde::Deserialize,
)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(any(feature = "mock", test), derive(Default))]
pub struct PatchSet {
pub threshold: u32,
pub members: Vec<MemberPubKey>,
}
#[derive(
PartialEq,
Eq,
Clone,
borsh::BorshSerialize,
borsh::BorshDeserialize,
serde::Serialize,
serde::Deserialize,
)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(any(feature = "mock", test), derive(Default))]
pub struct Namespace {
pub name: String,
pub nonce: u32,
#[serde(with = "qos_hex::serde")]
pub quorum_key: Vec<u8>,
}
impl fmt::Debug for Namespace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Namespace")
.field("name", &self.name)
.field("nonce", &self.nonce)
.field("quorum_key", &qos_hex::encode(&self.quorum_key))
.finish()
}
}
pub const DEFAULT_APP_HOST_PORT: u16 = 3000;
pub const DEFAULT_APP_HOST_IP: &str = "0.0.0.0";
#[derive(
PartialEq,
Eq,
Debug,
Clone,
borsh::BorshSerialize,
borsh::BorshDeserialize,
serde::Serialize,
serde::Deserialize,
)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(any(feature = "mock", test), derive(Default))]
pub struct Manifest {
pub namespace: Namespace,
pub pivot: PivotConfig,
pub manifest_set: ManifestSet,
pub share_set: ShareSet,
pub enclave: NitroConfig,
pub patch_set: PatchSet,
}
#[derive(
PartialEq, Eq, Debug, Clone, borsh::BorshDeserialize, serde::Deserialize,
)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(any(feature = "mock", test), derive(Default))]
pub struct ManifestV0 {
pub namespace: Namespace,
pub pivot: PivotConfigV0,
pub manifest_set: ManifestSet,
pub share_set: ShareSet,
pub enclave: NitroConfig,
pub patch_set: PatchSet,
}
impl From<ManifestV0> for Manifest {
fn from(old: ManifestV0) -> Self {
Self {
namespace: old.namespace,
pivot: old.pivot.into(),
manifest_set: old.manifest_set,
share_set: old.share_set,
enclave: old.enclave,
patch_set: old.patch_set,
}
}
}
impl Manifest {
pub fn try_from_slice_compat(buf: &[u8]) -> Result<Self, borsh::io::Error> {
use borsh::BorshDeserialize;
if let Ok(v0) = serde_json::from_slice::<ManifestV0>(buf) {
return Ok(v0.into());
};
let result = Self::try_from_slice(buf);
if result.is_err() {
let old = ManifestV0::try_from_slice(buf)?;
Ok(old.into())
} else {
result
}
}
}
#[derive(
PartialEq,
Eq,
Clone,
borsh::BorshSerialize,
borsh::BorshDeserialize,
serde::Serialize,
serde::Deserialize,
)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(any(feature = "mock", test), derive(Default))]
pub struct Approval {
#[serde(with = "qos_hex::serde")]
pub signature: Vec<u8>,
pub member: QuorumMember,
}
impl fmt::Debug for Approval {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Approval")
.field("signature", &qos_hex::encode(&self.signature))
.field("member", &self.member)
.finish()
}
}
impl Approval {
pub(crate) fn verify(&self, msg: &[u8]) -> Result<(), ProtocolError> {
let pub_key = P256Public::from_bytes(&self.member.pub_key)?;
if pub_key.verify(msg, &self.signature).is_ok() {
Ok(())
} else {
Err(ProtocolError::CouldNotVerifyApproval)
}
}
}
#[derive(
PartialEq,
Eq,
Debug,
Clone,
borsh::BorshSerialize,
borsh::BorshDeserialize,
serde::Serialize,
serde::Deserialize,
)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(any(feature = "mock", test), derive(Default))]
pub struct ManifestEnvelope {
pub manifest: Manifest,
pub manifest_set_approvals: Vec<Approval>,
pub share_set_approvals: Vec<Approval>,
}
#[derive(PartialEq, Eq, Debug, Clone, borsh::BorshDeserialize)]
#[cfg_attr(any(feature = "mock", test), derive(Default))]
pub struct ManifestEnvelopeV0 {
pub manifest: ManifestV0,
pub manifest_set_approvals: Vec<Approval>,
pub share_set_approvals: Vec<Approval>,
}
impl ManifestEnvelope {
pub fn check_approvals(&self) -> Result<(), ProtocolError> {
let mut uniq_members = HashSet::new();
for approval in &self.manifest_set_approvals {
let member_pub_key =
P256Public::from_bytes(&approval.member.pub_key)?;
let is_valid_signature = member_pub_key
.verify(&self.manifest.qos_hash(), &approval.signature)
.is_ok();
if !is_valid_signature {
return Err(ProtocolError::InvalidManifestApproval(
approval.clone(),
));
}
if !self.manifest.manifest_set.members.contains(&approval.member) {
return Err(ProtocolError::NotManifestSetMember);
}
if !uniq_members.insert(approval.member.qos_hash()) {
return Err(ProtocolError::DuplicateApproval);
}
}
if uniq_members.len() < self.manifest.manifest_set.threshold as usize {
return Err(ProtocolError::NotEnoughApprovals);
}
Ok(())
}
pub fn try_from_slice_compat(buf: &[u8]) -> Result<Self, borsh::io::Error> {
use borsh::BorshDeserialize;
let result = Self::try_from_slice(buf);
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(
state: &mut ProtocolState,
manifest_envelope: &ManifestEnvelope,
pivot: &[u8],
) -> Result<NsmResponse, ProtocolError> {
manifest_envelope.check_approvals()?;
if !manifest_envelope.share_set_approvals.is_empty() {
return Err(ProtocolError::BadShareSetApprovals);
}
let actual_hash = sha_256(pivot);
let expected_hash = manifest_envelope.manifest.pivot.hash;
if actual_hash != expected_hash {
return Err(ProtocolError::InvalidPivotHash {
expected: qos_hex::encode(&expected_hash),
actual: qos_hex::encode(&actual_hash),
});
};
let ephemeral_key = P256Pair::generate()?;
state.handles.put_ephemeral_key(&ephemeral_key)?;
state.handles.put_pivot(pivot)?;
state.handles.put_manifest_envelope(manifest_envelope)?;
let nsm_response = attestation::get_post_boot_attestation_doc(
&*state.attestor,
ephemeral_key.public_key().to_bytes(),
manifest_envelope.manifest.qos_hash().to_vec(),
);
Ok(nsm_response)
}
pub(in crate::protocol) fn boot_standard(
state: &mut ProtocolState,
manifest_envelope: &ManifestEnvelope,
pivot: &[u8],
) -> Result<NsmResponse, ProtocolError> {
let nsm_response = put_manifest_and_pivot(state, manifest_envelope, pivot)?;
Ok(nsm_response)
}
#[cfg(test)]
mod test {
use std::path::Path;
use qos_nsm::mock::MockNsm;
use qos_test_primitives::PathWrapper;
use super::*;
use crate::handles::Handles;
fn get_manifest() -> (Manifest, Vec<(P256Pair, QuorumMember)>, Vec<u8>) {
let quorum_pair = P256Pair::generate().unwrap();
let member1_pair = P256Pair::generate().unwrap();
let member2_pair = P256Pair::generate().unwrap();
let member3_pair = P256Pair::generate().unwrap();
let pivot = b"this is a pivot binary".to_vec();
let quorum_members = vec![
QuorumMember {
alias: "member1".to_string(),
pub_key: member1_pair.public_key().to_bytes(),
},
QuorumMember {
alias: "member2".to_string(),
pub_key: member2_pair.public_key().to_bytes(),
},
QuorumMember {
alias: "member3".to_string(),
pub_key: member3_pair.public_key().to_bytes(),
},
];
let member_with_keys = vec![
(member1_pair, quorum_members.first().unwrap().clone()),
(member2_pair, quorum_members.get(1).unwrap().clone()),
(member3_pair, quorum_members.get(2).unwrap().clone()),
];
let manifest = Manifest {
namespace: Namespace {
nonce: 420,
name: "vape lord".to_string(),
quorum_key: quorum_pair.public_key().to_bytes(),
},
enclave: NitroConfig {
pcr0: vec![4; 32],
pcr1: vec![3; 32],
pcr2: vec![2; 32],
pcr3: vec![1; 32],
aws_root_certificate: b"cert lord".to_vec(),
qos_commit: "mock qos commit".to_string(),
},
pivot: PivotConfig {
hash: sha_256(&pivot),
restart: RestartPolicy::Always,
args: vec![],
..Default::default()
},
manifest_set: ManifestSet { threshold: 2, members: quorum_members },
share_set: ShareSet { threshold: 2, members: vec![] },
..Default::default()
};
(manifest, member_with_keys, pivot)
}
#[test]
fn manifest_hash() {
let (manifest, _members, _pivot) = get_manifest();
let hashes: Vec<_> = (0..10).map(|_| manifest.qos_hash()).collect();
let is_valid = (1..10).all(|i| hashes[i] == hashes[0]);
assert!(is_valid);
}
#[test]
fn boot_standard_accepts_approved_manifest() {
let (manifest, members, pivot) = get_manifest();
let manifest_envelope = {
let manifest_hash = manifest.qos_hash();
let approvals = members
.into_iter()
.map(|(pair, member)| Approval {
signature: pair.sign(&manifest_hash).unwrap(),
member,
})
.collect();
ManifestEnvelope {
manifest,
manifest_set_approvals: approvals,
share_set_approvals: vec![],
}
};
let pivot_file =
"boot_standard_accepts_approved_manifest.pivot".to_string();
let ephemeral_file =
"boot_standard_accepts_approved_manifest_eph.secret".to_string();
let manifest_file =
"boot_standard_accepts_approved_manifest.manifest".to_string();
let handles = Handles::new(
ephemeral_file.clone(),
"quorum_key".to_string(),
manifest_file.clone(),
pivot_file.clone(),
);
let mut protocol_state =
ProtocolState::new(Box::new(MockNsm), handles.clone(), None);
let _nsm_resposne =
boot_standard(&mut protocol_state, &manifest_envelope, &pivot)
.unwrap();
assert!(Path::new(&pivot_file).exists());
assert!(Path::new(&ephemeral_file).exists());
assert_eq!(handles.get_manifest_envelope().unwrap(), manifest_envelope);
std::fs::remove_file(pivot_file).unwrap();
std::fs::remove_file(ephemeral_file).unwrap();
std::fs::remove_file(manifest_file).unwrap();
}
#[test]
fn boot_standard_rejects_manifest_if_not_enough_approvals() {
let (manifest, members, pivot) = get_manifest();
let manifest_envelope = {
let manifest_hash = manifest.qos_hash();
let approvals = members
[0usize..manifest.manifest_set.threshold as usize - 1]
.iter()
.map(|(pair, member)| Approval {
signature: pair.sign(&manifest_hash).unwrap(),
member: member.clone(),
})
.collect();
ManifestEnvelope {
manifest,
manifest_set_approvals: approvals,
share_set_approvals: vec![],
}
};
let pivot_file =
"boot_standard_rejects_manifest_if_not_enough_approvals.pivot"
.to_string();
let ephemeral_file =
"boot_standard_rejects_manifest_if_not_enough_approvals.secret"
.to_string();
let manifest_file =
"boot_standard_rejects_manifest_if_not_enough_approvals.manifest"
.to_string();
let handles = Handles::new(
ephemeral_file.clone(),
"quorum_key".to_string(),
manifest_file,
pivot_file,
);
let mut protocol_state =
ProtocolState::new(Box::new(MockNsm), handles.clone(), None);
let nsm_resposne =
boot_standard(&mut protocol_state, &manifest_envelope, &pivot);
assert!(!handles.manifest_envelope_exists());
assert!(!handles.pivot_exists());
assert!(!Path::new(&ephemeral_file).exists());
assert!(nsm_resposne.is_err());
}
#[test]
fn boot_standard_rejects_unapproved_manifest() {
let (manifest, members, pivot) = get_manifest();
let manifest_envelope = {
let approvals = members
.into_iter()
.map(|(_pair, member)| Approval {
signature: vec![0, 0],
member,
})
.collect();
ManifestEnvelope {
manifest,
manifest_set_approvals: approvals,
share_set_approvals: vec![],
}
};
let pivot_file =
"boot_standard_rejects_unapproved_manifest.pivot".to_string();
let ephemeral_file =
"boot_standard_rejects_unapproved_manifest.secret".to_string();
let manifest_file =
"boot_standard_rejects_unapproved_manifest.manifest".to_string();
let handles = Handles::new(
ephemeral_file.clone(),
"quorum_key".to_string(),
manifest_file,
pivot_file,
);
let mut protocol_state =
ProtocolState::new(Box::new(MockNsm), handles.clone(), None);
let nsm_resposne =
boot_standard(&mut protocol_state, &manifest_envelope, &pivot);
assert!(!handles.manifest_envelope_exists());
assert!(!handles.pivot_exists());
assert!(!Path::new(&ephemeral_file).exists());
assert!(nsm_resposne.is_err());
}
#[test]
fn boot_standard_rejects_manifest_envelope_with_share_set_approvals() {
let (manifest, members, pivot) = get_manifest();
let manifest_envelope = {
let manifest_hash = manifest.qos_hash();
let mut approvals: Vec<_> = members
.into_iter()
.map(|(pair, member)| Approval {
signature: pair.sign(&manifest_hash).unwrap(),
member,
})
.collect();
ManifestEnvelope {
manifest,
manifest_set_approvals: approvals.clone(),
share_set_approvals: vec![approvals.remove(0)],
}
};
let pivot_file: PathWrapper =
"boot_standard_rejects_manifest_envelope_with_share_set_approvals.pivot".into();
let ephemeral_file: PathWrapper =
"boot_standard_rejects_manifest_envelope_with_share_set_approvals_eph.secret".into();
let manifest_file: PathWrapper =
"boot_standard_rejects_manifest_envelope_with_share_set_approvals.manifest".into();
let handles = Handles::new(
(*ephemeral_file).to_string(),
"quorum_key".to_string(),
(*manifest_file).to_string(),
(*pivot_file).to_string(),
);
let mut protocol_state =
ProtocolState::new(Box::new(MockNsm), handles, None);
let error =
boot_standard(&mut protocol_state, &manifest_envelope, &pivot)
.unwrap_err();
assert_eq!(error, ProtocolError::BadShareSetApprovals);
assert!(!Path::new(&*pivot_file).exists());
assert!(!Path::new(&*ephemeral_file).exists());
assert!(!Path::new(&*manifest_file).exists());
}
#[test]
fn boot_standard_rejects_approval_from_non_manifest_set_member() {
let (manifest, members, pivot) = get_manifest();
let manifest_envelope = {
let manifest_hash = manifest.qos_hash();
let mut approvals: Vec<_> = members
.into_iter()
.map(|(pair, member)| Approval {
signature: pair.sign(&manifest_hash).unwrap(),
member,
})
.collect();
let approval = approvals.get_mut(0).unwrap();
let pair = P256Pair::generate().unwrap();
approval.member.pub_key = pair.public_key().to_bytes();
approval.signature = pair.sign(&manifest.qos_hash()).unwrap();
ManifestEnvelope {
manifest,
manifest_set_approvals: approvals.clone(),
share_set_approvals: vec![],
}
};
let pivot_file: PathWrapper =
"boot_standard_rejects_approval_from_non_manifest_set_member.pivot"
.into();
let ephemeral_file: PathWrapper =
"boot_standard_rejects_approval_from_non_manifest_set_member.secret".into();
let manifest_file: PathWrapper =
"boot_standard_rejects_approval_from_non_manifest_set_member.manifest".into();
let handles = Handles::new(
(*ephemeral_file).to_string(),
"quorum_key".to_string(),
(*manifest_file).to_string(),
(*pivot_file).to_string(),
);
let mut protocol_state =
ProtocolState::new(Box::new(MockNsm), handles, None);
let error =
boot_standard(&mut protocol_state, &manifest_envelope, &pivot)
.unwrap_err();
assert_eq!(error, ProtocolError::NotManifestSetMember);
assert!(!Path::new(&*pivot_file).exists());
assert!(!Path::new(&*ephemeral_file).exists());
assert!(!Path::new(&*manifest_file).exists());
}
#[test]
fn check_approvals_rejects_duplicates() {
let (manifest, members, ..) = get_manifest();
let manifest_envelope = {
let manifest_hash = manifest.qos_hash();
let mut approvals: Vec<_> = members[..1]
.iter()
.cloned()
.map(|(pair, member)| Approval {
signature: pair.sign(&manifest_hash).unwrap(),
member,
})
.collect();
let duplicate_approval = approvals[0].clone();
approvals.push(duplicate_approval);
ManifestEnvelope {
manifest,
manifest_set_approvals: approvals.clone(),
share_set_approvals: vec![],
}
};
let err = manifest_envelope.check_approvals().unwrap_err();
assert_eq!(err, ProtocolError::DuplicateApproval);
}
#[test]
fn try_from_slice_compat_works() {
let bytes = std::fs::read("./fixtures/old_manifest").unwrap();
let manifest = Manifest::try_from_slice_compat(&bytes).unwrap();
assert_eq!(manifest.namespace.name, "quit-coding-to-vape");
assert_eq!(manifest.pivot.bridge_config.len(), 0);
}
}