use protocol::identity::{SEED_BYTES, seed_from_bytes};
use crate::error::{Error, ErrorKind};
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 generate() -> Result<Self, Error> {
let mut vm_seed = [0_u8; SEED_BYTES];
let mut host_seed = [0_u8; SEED_BYTES];
getrandom::fill(&mut vm_seed)
.and_then(|()| getrandom::fill(&mut host_seed))
.map_err(|error| {
Error::new(
ErrorKind::Unexpected,
format!("the OS random pool is unavailable: {error}"),
)
})?;
seed_from_bytes(&vm_seed)
.and_then(|_| seed_from_bytes(&host_seed))
.map_err(|error| Error::new(ErrorKind::Unexpected, error.to_string()))?;
Ok(Self { vm_seed, host_seed })
}
#[cfg(test)]
pub fn from_seeds(vm_seed: [u8; SEED_BYTES], host_seed: [u8; SEED_BYTES]) -> Self {
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)
}
pub fn initiator(&self) -> Result<snow::HandshakeState, Error> {
snow::Builder::new(
protocol::identity::NOISE_PATTERN
.parse()
.expect("the pattern is a compile-time constant the protocol crate tests"),
)
.local_private_key(&self.host_seed)
.and_then(|builder| builder.remote_public_key(&self.vm_public_key))
.and_then(|builder| builder.build_initiator())
.map_err(|error| {
Error::new(
ErrorKind::Unexpected,
format!("could not build the identity initiator: {error}"),
)
})
}
}
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::*;
#[test]
fn generated_material_is_fresh_and_valid() {
let first = LaunchIdentity::generate().expect("the pool is available");
let second = LaunchIdentity::generate().expect("the pool is available");
assert_ne!(first.seed_field(), second.seed_field());
assert_ne!(first.host_public_field(), second.host_public_field());
assert_eq!(first.seed_field().len(), 44);
assert_eq!(first.host_public_field().len(), 44);
}
#[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 = LaunchIdentity::from_seeds(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 = LaunchIdentity::from_seeds([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 = LaunchIdentity::from_seeds([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}");
}
#[test]
fn the_initiator_completes_against_a_responder_built_from_the_payload_fields() {
use base64::Engine as _;
let b64 = base64::engine::general_purpose::STANDARD;
let launch = LaunchIdentity::from_seeds([7_u8; 32], [9_u8; 32]);
let vm_seed_bytes: [u8; 32] = b64
.decode(launch.seed_field())
.expect("valid base64")
.try_into()
.expect("32 bytes");
let host_public_bytes: [u8; 32] = b64
.decode(launch.host_public_field())
.expect("valid base64")
.try_into()
.expect("32 bytes");
let mut responder =
snow::Builder::new(protocol::identity::NOISE_PATTERN.parse().expect("parses"))
.local_private_key(&vm_seed_bytes)
.expect("a 32-byte secret")
.remote_public_key(&host_public_bytes)
.expect("a 32-byte key")
.build_responder()
.expect("builds");
let kept = launch.keep();
let mut initiator = kept.initiator().expect("builds");
let mut buffer = [0_u8; 1024];
let mut scratch = [0_u8; 1024];
let written = initiator.write_message(&[], &mut buffer).expect("writes");
responder
.read_message(&buffer[..written], &mut scratch)
.expect("the daemon accepts the launching host");
let written = responder.write_message(&[], &mut buffer).expect("writes");
initiator
.read_message(&buffer[..written], &mut scratch)
.expect("the host accepts its own VM");
let mut host_transport = initiator.into_transport_mode().expect("transport");
let mut vm_transport = responder.into_transport_mode().expect("transport");
let sent = host_transport
.write_message(b"ping", &mut buffer)
.expect("encrypts");
let received = vm_transport
.read_message(&buffer[..sent], &mut scratch)
.expect("decrypts");
assert_eq!(&scratch[..received], b"ping");
}
}