use protocol::identity::{SEED_BYTES, seed_from_bytes};
use crate::error::Error;
pub struct LaunchIdentity {
vm_seed: [u8; SEED_BYTES],
host_seed: [u8; SEED_BYTES],
}
impl std::fmt::Debug for LaunchIdentity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("LaunchIdentity(<two seeds>)")
}
}
impl LaunchIdentity {
pub fn from_seeds(
vm_seed: [u8; SEED_BYTES],
host_seed: [u8; SEED_BYTES],
) -> Result<Self, Error> {
seed_from_bytes(&vm_seed)
.and_then(|_| seed_from_bytes(&host_seed))
.map_err(|error| Error::invalid_arg(format!("an identity seed: {error}")))?;
Ok(Self { vm_seed, host_seed })
}
pub fn seed_field(&self) -> String {
use base64::Engine as _;
base64::engine::general_purpose::STANDARD.encode(self.vm_seed)
}
pub fn host_public_field(&self) -> String {
use base64::Engine as _;
base64::engine::general_purpose::STANDARD.encode(public_of(&self.host_seed))
}
pub fn keep(self) -> TunnelIdentity {
TunnelIdentity {
host_seed: self.host_seed,
vm_public_key: public_of(&self.vm_seed),
}
}
}
#[derive(Clone)]
pub struct TunnelIdentity {
host_seed: [u8; SEED_BYTES],
vm_public_key: [u8; SEED_BYTES],
}
impl std::fmt::Debug for TunnelIdentity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"TunnelIdentity(vm_public_key: {}, host_seed: <secret>)",
const_hex::encode(self.vm_public_key)
)
}
}
impl TunnelIdentity {
pub fn from_parts(host_seed: &[u8], vm_public_key: &[u8]) -> Result<Self, Error> {
let host_seed = seed_from_bytes(host_seed)
.map_err(|error| Error::invalid_arg(format!("the stored host seed: {error}")))?;
let vm_public_key: [u8; SEED_BYTES] = vm_public_key.try_into().map_err(|_| {
Error::invalid_arg(format!(
"the stored VM public key is {} bytes, and an x25519 public key is exactly \
{SEED_BYTES}",
vm_public_key.len()
))
})?;
Ok(Self {
host_seed,
vm_public_key,
})
}
pub fn vm_public_key(&self) -> [u8; SEED_BYTES] {
self.vm_public_key
}
pub fn host_seed(&self) -> [u8; SEED_BYTES] {
self.host_seed
}
pub fn host_seed_base64(&self) -> String {
use base64::Engine as _;
base64::engine::general_purpose::STANDARD.encode(self.host_seed)
}
pub fn vm_public_key_base64(&self) -> String {
use base64::Engine as _;
base64::engine::general_purpose::STANDARD.encode(self.vm_public_key)
}
pub fn from_encoded_parts(host_seed: &str, vm_public_key: &str) -> Result<Self, Error> {
use base64::Engine as _;
let b64 = base64::engine::general_purpose::STANDARD;
let host = b64.decode(host_seed).map_err(|_| {
Error::invalid_arg(
"the identity host seed is not valid base64. It is the identityHostSeed \
value from `run --identity`'s envelope, or the registry record's \
identityHostSeed field."
.to_string(),
)
})?;
let vm = b64.decode(vm_public_key).map_err(|_| {
Error::invalid_arg(
"the identity VM public key is not valid base64. It is the \
identityVmPublicKey value from `run --identity`'s envelope."
.to_string(),
)
})?;
Self::from_parts(&host, &vm)
}
}
fn public_of(seed: &[u8; SEED_BYTES]) -> [u8; SEED_BYTES] {
*x25519_dalek::PublicKey::from(&x25519_dalek::StaticSecret::from(*seed)).as_bytes()
}
#[cfg(test)]
mod tests {
use super::*;
fn launch(vm_seed: [u8; SEED_BYTES], host_seed: [u8; SEED_BYTES]) -> LaunchIdentity {
LaunchIdentity::from_seeds(vm_seed, host_seed).expect("valid seeds")
}
#[test]
fn an_all_zero_seed_is_refused_on_either_side() {
assert!(LaunchIdentity::from_seeds([0_u8; SEED_BYTES], [9_u8; SEED_BYTES]).is_err());
let error = LaunchIdentity::from_seeds([7_u8; SEED_BYTES], [0_u8; SEED_BYTES])
.expect_err("a zero host seed is refused");
assert_eq!(error.kind(), crate::error::ErrorKind::InvalidArg);
}
#[test]
fn keeping_an_identity_drops_the_vm_secret() {
let vm_seed = [7_u8; SEED_BYTES];
let host_seed = [9_u8; SEED_BYTES];
let kept = launch(vm_seed, host_seed).keep();
assert_eq!(kept.vm_public_key(), public_of(&vm_seed));
assert_eq!(kept.host_seed(), host_seed);
assert_ne!(kept.vm_public_key(), vm_seed);
}
#[test]
fn a_kept_identity_round_trips_through_its_stored_parts() {
let kept = launch([7_u8; 32], [9_u8; 32]).keep();
let restored = TunnelIdentity::from_parts(&kept.host_seed(), &kept.vm_public_key())
.expect("what was stored restores");
assert_eq!(restored.vm_public_key(), kept.vm_public_key());
assert_eq!(restored.host_seed(), kept.host_seed());
}
#[test]
fn stored_parts_of_the_wrong_shape_are_refused_with_the_length() {
let error = TunnelIdentity::from_parts(&[1_u8; 16], &[2_u8; 32])
.expect_err("a short host seed is refused");
assert!(error.to_string().contains("16"), "{error}");
let error = TunnelIdentity::from_parts(&[1_u8; 32], &[2_u8; 31])
.expect_err("a short public key is refused");
assert!(error.to_string().contains("31"), "{error}");
}
#[test]
fn debug_prints_the_pin_and_never_a_secret() {
let launch = launch([0x5A_u8; 32], [0x3C_u8; 32]);
let rendered = format!("{launch:?}");
assert!(!rendered.to_lowercase().contains("5a5a"), "{rendered}");
assert!(!rendered.to_lowercase().contains("3c3c"), "{rendered}");
let kept = launch.keep();
let rendered = format!("{kept:?}");
assert!(
rendered.contains(&const_hex::encode(kept.vm_public_key())),
"{rendered}"
);
assert!(!rendered.to_lowercase().contains("3c3c3c"), "{rendered}");
}
}