use crate::operator_key::load_operator_pair;
use crate::pair::Pair;
use crate::provisioning::ProvisionBundle;
use crate::quorum_key_metadata::QuorumKeyMetadata;
use crate::util::{read_json_file, write_file};
use anyhow::{anyhow, Context};
use clap::Args as ClapArgs;
use qos_core::protocol::services::boot::{Approval, ManifestEnvelope, QuorumMember};
use qos_core::protocol::QosHash;
use serde::Serialize;
use std::path::{Path, PathBuf};
use zeroize::Zeroizing;
#[derive(Debug, ClapArgs)]
#[command(about, long_about = None)]
pub struct Args {
#[arg(long, value_name = "PATH", env = "TVC_QUORUM_KEY_METADATA")]
pub quorum_key_metadata: PathBuf,
#[arg(long, value_name = "PATH", env = "TVC_PROVISION_BUNDLE")]
pub provision_bundle: PathBuf,
#[arg(long, value_name = "PATH", env = "TVC_OPERATOR_SEED")]
pub operator_seed: Option<PathBuf>,
#[arg(long, env = "TVC_DANGEROUS_SKIP_VERIFICATION")]
pub dangerous_skip_verification: bool,
#[arg(long, value_name = "PATH", env = "TVC_RE_ENCRYPTED_OUT")]
pub re_encrypted_out: Option<PathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
struct ReEncryptedShareOutput {
re_encrypted_share: String,
share_approval: Approval,
}
pub async fn run(args: Args) -> anyhow::Result<()> {
if args.dangerous_skip_verification {
eprintln!(
"WARNING: Skipping verification! This is dangerous and should not be used for sensitive applications."
);
}
let quorum_key_metadata: QuorumKeyMetadata =
read_json_file(&args.quorum_key_metadata, "quorum key metadata file").await?;
let provision_bundle: ProvisionBundle =
read_json_file(&args.provision_bundle, "provision bundle").await?;
let operator_pair = load_operator_pair(args.operator_seed.as_deref()).await?;
let output = build_re_encrypted_share_output(
&quorum_key_metadata,
&provision_bundle,
&operator_pair,
args.dangerous_skip_verification,
)
.await?;
write_output(args.re_encrypted_out.as_deref(), &output).await
}
async fn build_re_encrypted_share_output(
quorum_key_metadata: &QuorumKeyMetadata,
provision_bundle: &ProvisionBundle,
operator_pair: &dyn Pair,
dangerous_skip_verification: bool,
) -> anyhow::Result<ReEncryptedShareOutput> {
ensure_quorum_key_matches_manifest(quorum_key_metadata, provision_bundle)?;
let ephemeral_public_key =
provision_bundle.ephemeral_public_key(dangerous_skip_verification)?;
let operator_public_key = operator_pair.public_key();
let encrypted_share = quorum_key_metadata.encrypted_share_for_operator(&operator_public_key)?;
let member = find_share_set_member(provision_bundle.manifest_envelope(), &operator_public_key)?;
let re_encrypted_share = {
let plaintext_share = Zeroizing::new(
operator_pair
.decrypt(encrypted_share)
.await
.context("failed to decrypt share with operator key")?,
);
ephemeral_public_key
.encrypt(plaintext_share.as_slice())
.map_err(|err| anyhow!("failed to encrypt share to ephemeral key: {err:?}"))?
};
let signature = operator_pair
.sign(
provision_bundle
.manifest_envelope()
.manifest
.qos_hash()
.to_vec(),
)
.await
.context("failed to sign share approval with operator key")?;
let share_approval = Approval { signature, member };
Ok(ReEncryptedShareOutput {
re_encrypted_share: hex::encode(re_encrypted_share),
share_approval,
})
}
fn ensure_quorum_key_matches_manifest(
quorum_key_metadata: &QuorumKeyMetadata,
provision_bundle: &ProvisionBundle,
) -> anyhow::Result<()> {
let metadata_quorum_key = quorum_key_metadata.quorum_key_public_bytes()?;
let manifest_quorum_key = &provision_bundle
.manifest_envelope()
.manifest
.namespace
.quorum_key;
if metadata_quorum_key.as_slice() != manifest_quorum_key.as_slice() {
anyhow::bail!(
"quorum key metadata quorumKeyPublic ({}) does not match provision bundle manifest quorumKey ({})",
hex::encode(metadata_quorum_key),
hex::encode(manifest_quorum_key),
);
}
Ok(())
}
fn find_share_set_member(
manifest_envelope: &ManifestEnvelope,
operator_public_key: &[u8],
) -> anyhow::Result<QuorumMember> {
manifest_envelope
.manifest
.share_set
.members
.iter()
.find(|member| member.pub_key == operator_public_key)
.cloned()
.ok_or_else(|| {
anyhow!(
"operator ({}) not part of share set",
hex::encode(operator_public_key)
)
})
}
async fn write_output(path: Option<&Path>, output: &ReEncryptedShareOutput) -> anyhow::Result<()> {
let contents =
serde_json::to_string_pretty(output).context("failed to serialize re-encrypted share")?;
if let Some(path) = path {
write_file(path, &contents).await?;
eprintln!("Re-encrypted share written to: {}", path.display());
} else {
println!("{contents}");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{
build_re_encrypted_share_output, ensure_quorum_key_matches_manifest, find_share_set_member,
ReEncryptedShareOutput,
};
use crate::pair::LocalPair;
use crate::provisioning::ProvisionBundle;
use crate::quorum_key_metadata::{EncryptedShareMetadata, QuorumKeyMetadata};
use qos_core::protocol::services::boot::{
Approval, Manifest, ManifestEnvelope, ManifestSet, Namespace, NitroConfig, PatchSet,
PivotConfig, QuorumMember, RestartPolicy, ShareSet,
};
use qos_core::protocol::QosHash;
use qos_p256::{P256Pair, P256Public};
use serde_json::json;
fn sample_manifest_envelope(
quorum_key: Vec<u8>,
share_set_members: Vec<QuorumMember>,
) -> ManifestEnvelope {
ManifestEnvelope {
manifest: Manifest {
namespace: Namespace {
name: "test-namespace".to_string(),
nonce: 7,
quorum_key,
},
pivot: PivotConfig {
hash: [0; 32],
restart: RestartPolicy::Never,
bridge_config: vec![],
debug_mode: false,
args: vec![],
},
manifest_set: ManifestSet {
threshold: 0,
members: vec![],
},
share_set: ShareSet {
threshold: share_set_members.len() as u32,
members: share_set_members,
},
enclave: NitroConfig {
pcr0: vec![0; 48],
pcr1: vec![1; 48],
pcr2: vec![2; 48],
pcr3: vec![3; 48],
aws_root_certificate: vec![],
qos_commit: "test-commit".to_string(),
},
patch_set: PatchSet {
threshold: 0,
members: vec![],
},
},
manifest_set_approvals: vec![],
share_set_approvals: vec![],
}
}
fn bundle_with_ephemeral_key(
ephemeral_key: &P256Public,
quorum_key: Vec<u8>,
share_set_members: Vec<QuorumMember>,
) -> ProvisionBundle {
ProvisionBundle::new(
"deploy-123".to_string(),
b"not parsed when verification is skipped",
sample_manifest_envelope(quorum_key, share_set_members),
1_712_345_678_901,
&ephemeral_key.to_bytes(),
)
}
fn local_pair_from_pair(pair: &P256Pair) -> LocalPair {
let seed_hex = String::from_utf8(pair.to_master_seed_hex()).unwrap();
LocalPair::from_hex_seed(&seed_hex).unwrap()
}
fn quorum_key_metadata(
quorum_pair: &P256Pair,
operator_pair: &P256Pair,
plaintext_share: &[u8],
) -> QuorumKeyMetadata {
QuorumKeyMetadata {
quorum_key_public: hex::encode(quorum_pair.public_key().to_bytes()),
threshold: 1,
shares: vec![EncryptedShareMetadata {
operator_public_key: hex::encode(operator_pair.public_key().to_bytes()),
share: hex::encode(operator_pair.public_key().encrypt(plaintext_share).unwrap()),
}],
}
}
#[test]
fn output_serializes_expected_json_shape_with_hex() {
let output = ReEncryptedShareOutput {
re_encrypted_share: "010203".to_string(),
share_approval: Approval {
signature: vec![0xde, 0xad, 0xbe, 0xef],
member: QuorumMember {
alias: "operator-1".to_string(),
pub_key: vec![0xaa, 0xbb, 0xcc],
},
},
};
let value = serde_json::to_value(&output).unwrap();
assert_eq!(
value,
json!({
"reEncryptedShare": "010203",
"shareApproval": {
"signature": "deadbeef",
"member": {
"alias": "operator-1",
"pubKey": "aabbcc",
},
},
})
);
}
#[test]
fn finds_operator_share_set_member_by_public_key() {
let operator_pair = P256Pair::generate().unwrap();
let member = QuorumMember {
alias: "operator-1".to_string(),
pub_key: operator_pair.public_key().to_bytes(),
};
let manifest_envelope = sample_manifest_envelope(
P256Pair::generate().unwrap().public_key().to_bytes(),
vec![member.clone()],
);
let found =
find_share_set_member(&manifest_envelope, &operator_pair.public_key().to_bytes())
.unwrap();
assert_eq!(found, member);
}
#[test]
fn rejects_operator_missing_from_share_set() {
let operator_pair = P256Pair::generate().unwrap();
let other_pair = P256Pair::generate().unwrap();
let manifest_envelope = sample_manifest_envelope(
P256Pair::generate().unwrap().public_key().to_bytes(),
vec![QuorumMember {
alias: "operator-1".to_string(),
pub_key: other_pair.public_key().to_bytes(),
}],
);
assert!(
find_share_set_member(&manifest_envelope, &operator_pair.public_key().to_bytes())
.is_err()
);
}
#[test]
fn selects_operator_share_from_metadata() {
let quorum_pair = P256Pair::generate().unwrap();
let first_operator = P256Pair::generate().unwrap();
let second_operator = P256Pair::generate().unwrap();
let first_share = b"first share";
let second_share = b"second share";
let first_encrypted_share = first_operator.public_key().encrypt(first_share).unwrap();
let second_encrypted_share = second_operator.public_key().encrypt(second_share).unwrap();
let metadata = QuorumKeyMetadata {
quorum_key_public: hex::encode(quorum_pair.public_key().to_bytes()),
threshold: 1,
shares: vec![
EncryptedShareMetadata {
operator_public_key: hex::encode(first_operator.public_key().to_bytes()),
share: hex::encode(&first_encrypted_share),
},
EncryptedShareMetadata {
operator_public_key: hex::encode(second_operator.public_key().to_bytes())
.to_uppercase(),
share: hex::encode(&second_encrypted_share),
},
],
};
let selected = metadata
.encrypted_share_for_operator(&second_operator.public_key().to_bytes())
.unwrap();
assert_eq!(selected, second_encrypted_share);
}
#[test]
fn rejects_operator_missing_from_metadata_shares() {
let quorum_pair = P256Pair::generate().unwrap();
let operator_pair = P256Pair::generate().unwrap();
let missing_operator_pair = P256Pair::generate().unwrap();
let metadata = quorum_key_metadata(&quorum_pair, &operator_pair, b"share");
let err = metadata
.encrypted_share_for_operator(&missing_operator_pair.public_key().to_bytes())
.unwrap_err();
assert!(err
.to_string()
.contains(&hex::encode(missing_operator_pair.public_key().to_bytes())));
}
#[test]
fn rejects_quorum_key_mismatch_without_dangerous_skip() {
let metadata_quorum_pair = P256Pair::generate().unwrap();
let manifest_quorum_pair = P256Pair::generate().unwrap();
let operator_pair = P256Pair::generate().unwrap();
let ephemeral_pair = P256Pair::generate().unwrap();
let metadata = quorum_key_metadata(&metadata_quorum_pair, &operator_pair, b"share");
let bundle = bundle_with_ephemeral_key(
&ephemeral_pair.public_key(),
manifest_quorum_pair.public_key().to_bytes(),
vec![],
);
let err = ensure_quorum_key_matches_manifest(&metadata, &bundle).unwrap_err();
assert!(err.to_string().contains("does not match"));
}
#[tokio::test]
async fn re_encrypt_round_trip_and_generates_verifiable_approval() {
let quorum_pair = P256Pair::generate().unwrap();
let operator_pair = P256Pair::generate().unwrap();
let operator_member = QuorumMember {
alias: "operator-1".to_string(),
pub_key: operator_pair.public_key().to_bytes(),
};
let operator_local_pair = local_pair_from_pair(&operator_pair);
let ephemeral_pair = P256Pair::generate().unwrap();
let plaintext_share = b"arbitrary test share bytes";
let metadata = quorum_key_metadata(&quorum_pair, &operator_pair, plaintext_share);
let bundle = bundle_with_ephemeral_key(
&ephemeral_pair.public_key(),
quorum_pair.public_key().to_bytes(),
vec![operator_member.clone()],
);
let output =
build_re_encrypted_share_output(&metadata, &bundle, &operator_local_pair, true)
.await
.unwrap();
let re_encrypted_share = hex::decode(&output.re_encrypted_share).unwrap();
let decrypted_share = ephemeral_pair.decrypt(&re_encrypted_share).unwrap();
assert_eq!(decrypted_share, plaintext_share);
assert_eq!(output.share_approval.member, operator_member);
let approval_public_key =
P256Public::from_bytes(&output.share_approval.member.pub_key).unwrap();
approval_public_key
.verify(
&bundle.manifest_envelope().manifest.qos_hash(),
&output.share_approval.signature,
)
.unwrap();
}
#[tokio::test]
async fn dangerous_skip_does_not_bypass_quorum_key_match() {
let metadata_quorum_pair = P256Pair::generate().unwrap();
let manifest_quorum_pair = P256Pair::generate().unwrap();
let operator_pair = P256Pair::generate().unwrap();
let operator_member = QuorumMember {
alias: "operator-1".to_string(),
pub_key: operator_pair.public_key().to_bytes(),
};
let operator_local_pair = local_pair_from_pair(&operator_pair);
let ephemeral_pair = P256Pair::generate().unwrap();
let metadata = quorum_key_metadata(&metadata_quorum_pair, &operator_pair, b"share");
let bundle = bundle_with_ephemeral_key(
&ephemeral_pair.public_key(),
manifest_quorum_pair.public_key().to_bytes(),
vec![operator_member],
);
let err = build_re_encrypted_share_output(&metadata, &bundle, &operator_local_pair, true)
.await
.unwrap_err();
assert!(err.to_string().contains("does not match"));
}
}