use crate::{client::build_client, outcome::Outcome, output::StdCtx};
use anyhow::{Context, Result, anyhow, ensure};
use clap::Args as ClapArgs;
use p256::{PublicKey, elliptic_curve::sec1::ToEncodedPoint};
use serde::Serialize;
use std::collections::HashSet;
use std::fmt::{self, Display, Formatter};
use std::time::{SystemTime, UNIX_EPOCH};
use turnkey_client::generated::{CreateTvcQuorumKeyIntent, CreateTvcQuorumKeyResult};
const MAX_OPERATOR_ENCRYPT_KEY_COUNT_EXCLUSIVE: usize = 255;
#[derive(Debug, ClapArgs)]
#[command(about, long_about = None)]
pub struct Args {
#[arg(long, env = "TVC_QUORUM_KEY_THRESHOLD")]
pub threshold: u8,
#[arg(long, value_name = "HEX,HEX,...", env = "TVC_OPERATOR_ENCRYPT_KEYS")]
pub operator_encrypt_keys: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct QuorumKeyCreated {
quorum_key_id: String,
quorum_public_key: String,
share_ids: Vec<String>,
}
impl Display for QuorumKeyCreated {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"Quorum Key ID: {}\nQuorum Public Key: {}\nShare IDs: {}",
self.quorum_key_id,
self.quorum_public_key,
self.share_ids.join(", ")
)
}
}
pub async fn run(_ctx: &mut StdCtx, args: Args) -> Result<Outcome> {
let operator_encrypt_keys = parse_operator_encrypt_keys(&args.operator_encrypt_keys)?;
validate_operator_count(operator_encrypt_keys.len())?;
validate_threshold(args.threshold, operator_encrypt_keys.len())?;
let intent = build_create_tvc_quorum_key_intent(args.threshold, operator_encrypt_keys);
let expected_share_count = intent.operator_encrypt_keys.len();
let auth = build_client().await?;
let timestamp_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.context("system time before unix epoch")?
.as_millis();
let result = auth
.client
.create_tvc_quorum_key(auth.org_id, timestamp_ms, intent)
.await
.map_err(|error| anyhow!("failed to create hosted TVC quorum key: {error}"))?;
let output = validate_result(result.result, expected_share_count)?;
Ok(Outcome::KeysCreateQuorumKey(output))
}
fn parse_operator_encrypt_keys(input: &str) -> Result<Vec<String>> {
let mut seen = HashSet::new();
input
.split(',')
.enumerate()
.map(|(index, key)| {
let key = key.trim();
ensure!(
!key.is_empty(),
"operator encryption public key at index {index} is empty"
);
let bytes = hex::decode(key).with_context(|| {
format!(
"operator encryption public key at index {index} must be bare hex encoded"
)
})?;
ensure!(
bytes.len() == 65 && bytes.first() == Some(&0x04),
"operator encryption public key at index {index} must be a 65-byte uncompressed P-256 public key"
);
let public_key = PublicKey::from_sec1_bytes(&bytes).with_context(|| {
format!("operator encryption public key at index {index} is not a valid P-256 point")
})?;
let normalized = hex::encode(public_key.to_encoded_point(false).as_bytes());
ensure!(
seen.insert(normalized.clone()),
"duplicate operator encryption public key at index {index}"
);
Ok(normalized)
})
.collect()
}
fn validate_operator_count(operator_count: usize) -> Result<()> {
ensure!(
operator_count < MAX_OPERATOR_ENCRYPT_KEY_COUNT_EXCLUSIVE,
"operator encryption public key count ({operator_count}) must be less than {MAX_OPERATOR_ENCRYPT_KEY_COUNT_EXCLUSIVE}"
);
Ok(())
}
fn validate_threshold(threshold: u8, operator_count: usize) -> Result<()> {
ensure!(threshold >= 2, "threshold must be at least 2");
ensure!(
threshold as usize <= operator_count,
"threshold ({threshold}) cannot exceed operator encryption public key count ({operator_count})"
);
Ok(())
}
fn build_create_tvc_quorum_key_intent(
threshold: u8,
operator_encrypt_keys: Vec<String>,
) -> CreateTvcQuorumKeyIntent {
CreateTvcQuorumKeyIntent {
threshold: u32::from(threshold),
operator_encrypt_keys,
}
}
fn validate_result(
result: CreateTvcQuorumKeyResult,
expected_share_count: usize,
) -> Result<QuorumKeyCreated> {
ensure!(
!result.quorum_key_id.trim().is_empty(),
"create TVC quorum key response contained an empty quorum key ID"
);
ensure!(
!result.quorum_public_key.trim().is_empty(),
"create TVC quorum key response contained an empty quorum public key"
);
ensure!(
result.share_ids.len() == expected_share_count,
"create TVC quorum key response returned {} share IDs for {expected_share_count} operator encryption public keys",
result.share_ids.len()
);
ensure!(
result.share_ids.iter().all(|id| !id.trim().is_empty()),
"create TVC quorum key response contained an empty share ID"
);
Ok(QuorumKeyCreated {
quorum_key_id: result.quorum_key_id,
quorum_public_key: result.quorum_public_key,
share_ids: result.share_ids,
})
}
#[cfg(test)]
mod tests {
use super::*;
use qos_p256::P256Pair;
use serde_json::Value;
fn operator_encrypt_key() -> String {
let public_key = P256Pair::generate().unwrap().public_key().to_bytes();
hex::encode(&public_key[..65])
}
fn result_with_share_ids(share_ids: &[&str]) -> CreateTvcQuorumKeyResult {
CreateTvcQuorumKeyResult {
quorum_key_id: "quorum-key-id".to_string(),
quorum_public_key: "quorum-public-key".to_string(),
share_ids: share_ids.iter().map(|id| (*id).to_string()).collect(),
}
}
#[test]
fn parses_trims_and_normalizes_operator_encrypt_keys() {
let first = operator_encrypt_key();
let second = operator_encrypt_key();
let parsed = parse_operator_encrypt_keys(&format!(
" {} , {} ",
first.to_uppercase(),
second.to_uppercase()
))
.unwrap();
assert_eq!(parsed, vec![first, second]);
}
#[test]
fn rejects_empty_operator_encrypt_key() {
let key = operator_encrypt_key();
let error = parse_operator_encrypt_keys(&format!("{key},,"))
.unwrap_err()
.to_string();
assert_eq!(error, "operator encryption public key at index 1 is empty");
}
#[test]
fn rejects_non_hex_operator_encrypt_key() {
let error = parse_operator_encrypt_keys("not-hex")
.unwrap_err()
.to_string();
assert_eq!(
error,
"operator encryption public key at index 0 must be bare hex encoded"
);
}
#[test]
fn rejects_wrong_length_operator_encrypt_key() {
let error = parse_operator_encrypt_keys("04abcd")
.unwrap_err()
.to_string();
assert_eq!(
error,
"operator encryption public key at index 0 must be a 65-byte uncompressed P-256 public key"
);
}
#[test]
fn rejects_compressed_operator_encrypt_key() {
let uncompressed = operator_encrypt_key();
let public_key = PublicKey::from_sec1_bytes(&hex::decode(uncompressed).unwrap()).unwrap();
let compressed = hex::encode(public_key.to_encoded_point(true).as_bytes());
let error = parse_operator_encrypt_keys(&compressed)
.unwrap_err()
.to_string();
assert_eq!(
error,
"operator encryption public key at index 0 must be a 65-byte uncompressed P-256 public key"
);
}
#[test]
fn rejects_invalid_curve_point() {
let invalid = format!("04{}", "00".repeat(64));
let error = parse_operator_encrypt_keys(&invalid)
.unwrap_err()
.to_string();
assert_eq!(
error,
"operator encryption public key at index 0 is not a valid P-256 point"
);
}
#[test]
fn rejects_duplicate_operator_encrypt_keys_after_normalization() {
let key = operator_encrypt_key();
let error = parse_operator_encrypt_keys(&format!("{key},{}", key.to_uppercase()))
.unwrap_err()
.to_string();
assert_eq!(error, "duplicate operator encryption public key at index 1");
}
#[test]
fn validates_threshold_boundaries() {
assert_eq!(
validate_threshold(1, 2).unwrap_err().to_string(),
"threshold must be at least 2"
);
assert_eq!(
validate_threshold(3, 2).unwrap_err().to_string(),
"threshold (3) cannot exceed operator encryption public key count (2)"
);
assert!(validate_threshold(2, 2).is_ok());
}
#[test]
fn validates_operator_count_boundaries() {
assert!(validate_operator_count(254).is_ok());
assert_eq!(
validate_operator_count(255).unwrap_err().to_string(),
"operator encryption public key count (255) must be less than 255"
);
}
#[test]
fn builds_exact_create_intent() {
let operator_encrypt_keys = vec![operator_encrypt_key(), operator_encrypt_key()];
let intent = build_create_tvc_quorum_key_intent(2, operator_encrypt_keys.clone());
assert_eq!(
intent,
CreateTvcQuorumKeyIntent {
threshold: 2,
operator_encrypt_keys,
}
);
}
#[test]
fn validates_and_formats_result() {
let output = validate_result(result_with_share_ids(&["share-1", "share-2"]), 2).unwrap();
assert_eq!(
output,
QuorumKeyCreated {
quorum_key_id: "quorum-key-id".to_string(),
quorum_public_key: "quorum-public-key".to_string(),
share_ids: vec!["share-1".to_string(), "share-2".to_string()],
}
);
assert_eq!(
output.to_string(),
"Quorum Key ID: quorum-key-id\nQuorum Public Key: quorum-public-key\nShare IDs: share-1, share-2"
);
let json: Value = serde_json::to_value(&output).unwrap();
assert_eq!(
json,
serde_json::json!({
"quorumKeyId": "quorum-key-id",
"quorumPublicKey": "quorum-public-key",
"shareIds": ["share-1", "share-2"],
})
);
}
}