use bolero::TypeGenerator;
use microvms_app::identity::{LaunchIdentity, TunnelIdentity};
use super::tunnel::{Frame, Verified, open_frame, read_reply};
const VM_SEED: [u8; 32] = [7; 32];
const HOST_SEED: [u8; 32] = [9; 32];
const OTHER_SEED: [u8; 32] = [11; 32];
const MAX_CHUNKS: usize = 8;
const MAX_JUNK_BYTES: usize = 256;
fn identity() -> TunnelIdentity {
LaunchIdentity::from_seeds(VM_SEED, HOST_SEED)
.expect("valid seeds")
.keep()
}
fn host_public() -> [u8; 32] {
use base64::Engine as _;
base64::engine::general_purpose::STANDARD
.decode(
LaunchIdentity::from_seeds(VM_SEED, HOST_SEED)
.expect("valid seeds")
.host_public_field(),
)
.expect("base64")
.try_into()
.expect("32 bytes")
}
fn responder(vm_seed: [u8; 32]) -> snow::HandshakeState {
let host = host_public();
snow::Builder::new(protocol::identity::NOISE_PATTERN.parse().expect("parses"))
.local_private_key(&vm_seed)
.and_then(|builder| builder.remote_public_key(&host))
.and_then(|builder| builder.build_responder())
.expect("the responder builds")
}
fn hello() -> (snow::HandshakeState, Vec<u8>) {
let mut initiator = crate::identity::initiator(&identity()).expect("the initiator builds");
let mut scratch = vec![0_u8; 65535];
let written = initiator
.write_message(&protocol::identity::HANDSHAKE_PAYLOAD, &mut scratch)
.expect("writes");
(initiator, scratch[..written].to_vec())
}
fn answer(mut responder: snow::HandshakeState, first: &[u8], payload: &[u8]) -> Vec<u8> {
let mut scratch = vec![0_u8; 65535];
responder
.read_message(first, &mut scratch)
.expect("the handshake message opens");
let written = responder
.write_message(payload, &mut scratch)
.expect("the reply writes");
scratch[..written].to_vec()
}
fn offer_payload(offer: bool) -> &'static [u8] {
if offer {
&protocol::identity::HANDSHAKE_PAYLOAD
} else {
&[]
}
}
#[derive(Clone, Debug, TypeGenerator)]
enum Reply {
Pinned { offer: bool },
Payload(Vec<u8>),
Replayed { offer: bool },
OtherVm { offer: bool },
Flipped { offer: bool, at: u16, bit: u8 },
Bytes(Vec<u8>),
}
impl Reply {
fn wire(&self, first: &[u8]) -> (Vec<u8>, Option<bool>) {
match self {
Reply::Pinned { offer } => (
answer(responder(VM_SEED), first, offer_payload(*offer)),
Some(*offer),
),
Reply::Payload(payload) => {
let payload = &payload[..payload.len().min(MAX_JUNK_BYTES)];
(
answer(responder(VM_SEED), first, payload),
Some(protocol::identity::offers_end_of_stream(payload)),
)
}
Reply::Replayed { offer } => {
let (_, another) = hello();
(
answer(responder(VM_SEED), &another, offer_payload(*offer)),
None,
)
}
Reply::OtherVm { offer } => {
let other = LaunchIdentity::from_seeds(OTHER_SEED, HOST_SEED)
.expect("valid seeds")
.keep();
let mut initiator =
crate::identity::initiator(&other).expect("the initiator builds");
let mut scratch = vec![0_u8; 65535];
let written = initiator
.write_message(&protocol::identity::HANDSHAKE_PAYLOAD, &mut scratch)
.expect("writes");
(
answer(
responder(OTHER_SEED),
&scratch[..written],
offer_payload(*offer),
),
None,
)
}
Reply::Flipped { offer, at, bit } => {
let mut wire = answer(responder(VM_SEED), first, offer_payload(*offer));
let at = usize::from(*at) % wire.len();
wire[at] ^= 1 << (bit % 8);
(wire, None)
}
Reply::Bytes(bytes) => (bytes[..bytes.len().min(MAX_JUNK_BYTES)].to_vec(), None),
}
}
}
fn check_reply(case: &Reply) {
let (initiator, first) = hello();
let (wire, expected) = case.wire(&first);
let mut scratch = vec![0_u8; 65535];
match (read_reply(initiator, &wire, &mut scratch, 8080), expected) {
(Ok(verified), Some(offer)) => {
assert_eq!(verified.daemon_proves_end, offer, "{case:?}");
}
(Err(error), None) => {
assert!(
error.to_string().contains("pinned key"),
"{case:?}: {error}"
);
}
(read, expected) => panic!(
"{case:?} read as {:?}, and the pin's rule says {expected:?}",
read.map(|verified| verified.daemon_proves_end)
),
}
}
#[test]
fn a_reply_opens_only_from_the_pinned_vm() {
for case in [
Reply::Pinned { offer: false },
Reply::Pinned { offer: true },
Reply::Payload(vec![0x02]),
Reply::Replayed { offer: true },
Reply::OtherVm { offer: true },
Reply::Flipped {
offer: true,
at: 0,
bit: 0,
},
Reply::Bytes(vec![0; 48]),
] {
check_reply(&case);
}
bolero::check!().with_type::<Reply>().for_each(check_reply);
}
#[derive(Clone, Debug, TypeGenerator)]
enum Step {
Deliver,
Drop,
Replay(u8),
Swap,
Flip {
at: u16,
bit: u8,
},
Junk(Vec<u8>),
}
#[derive(Clone, Debug, TypeGenerator)]
struct Path {
chunks: Vec<Vec<u8>>,
steps: Vec<Step>,
faithful: bool,
}
fn session() -> (Verified, snow::TransportState) {
let (initiator, first) = hello();
let mut daemon = responder(VM_SEED);
let mut scratch = vec![0_u8; 65535];
daemon
.read_message(&first, &mut scratch)
.expect("the handshake message opens");
let written = daemon
.write_message(&protocol::identity::HANDSHAKE_PAYLOAD, &mut scratch)
.expect("the reply writes");
let reply = scratch[..written].to_vec();
let verified = read_reply(initiator, &reply, &mut scratch, 8080).expect("the reply opens");
(verified, daemon.into_transport_mode().expect("transport"))
}
fn anchor_paths() -> Vec<Path> {
let chunks = vec![vec![0], vec![1, 2], vec![3, 4, 5]];
let path = |steps: Vec<Step>, faithful| Path {
chunks: chunks.clone(),
steps,
faithful,
};
vec![
path(Vec::new(), true),
path(vec![Step::Flip { at: 0, bit: 0 }], false),
path(vec![Step::Drop], false),
path(vec![Step::Deliver, Step::Replay(0)], false),
path(vec![Step::Swap], false),
path(vec![Step::Junk(vec![0; 32])], false),
]
}
#[test]
fn frames_open_in_order_and_the_daemons_end_only_after_every_chunk() {
for path in anchor_paths() {
check_path(&path);
}
bolero::check!().with_type::<Path>().for_each(check_path);
}
fn check_path(path: &Path) {
let chunks: Vec<Vec<u8>> = path
.chunks
.iter()
.filter(|chunk| !chunk.is_empty())
.take(MAX_CHUNKS)
.map(|chunk| chunk[..chunk.len().min(1 + usize::from(chunk[0] % 8))].to_vec())
.collect();
let (mut client, mut daemon) = session();
let mut scratch = vec![0_u8; 65535];
let frames: Vec<Vec<u8>> = chunks
.iter()
.map(Vec::as_slice)
.chain([&[][..]])
.map(|plain| {
let written = daemon.write_message(plain, &mut scratch).expect("seals");
scratch[..written].to_vec()
})
.collect();
let mut queue: std::collections::VecDeque<Vec<u8>> = frames.iter().cloned().collect();
let mut opened = 0;
let mut attacked = false;
let given: &[Step] = if path.faithful { &[] } else { &path.steps };
let steps = given.iter().map(Some).chain(std::iter::repeat(None));
for step in steps {
let frame = match step {
None | Some(Step::Deliver) => queue.pop_front(),
Some(Step::Drop) => {
attacked |= queue.pop_front().is_some();
continue;
}
Some(Step::Replay(index)) => {
attacked = true;
Some(frames[usize::from(*index) % frames.len()].clone())
}
Some(Step::Swap) => {
if queue.len() >= 2 {
queue.swap(0, 1);
attacked = true;
}
continue;
}
Some(Step::Flip { at, bit }) => queue.pop_front().map(|mut frame| {
let at = usize::from(*at) % frame.len();
frame[at] ^= 1 << (bit % 8);
attacked = true;
frame
}),
Some(Step::Junk(bytes)) => {
attacked = true;
Some(bytes[..bytes.len().min(MAX_JUNK_BYTES)].to_vec())
}
};
let Some(frame) = frame else {
assert!(
attacked,
"a path that did nothing ran out before the end: {path:?}"
);
return;
};
match open_frame(&mut client, &frame, &mut scratch) {
Ok(Frame::Chunk(count)) => {
assert!(
chunks.get(opened).map(Vec::as_slice) == Some(&scratch[..count]),
"frame {opened} opened as {:?}, not the guest's next chunk: {path:?}",
&scratch[..count]
);
opened += 1;
}
Ok(Frame::End) => {
assert_eq!(
opened,
chunks.len(),
"the daemon's end opened before every chunk: {path:?}"
);
return;
}
Err(error) => {
assert!(
attacked,
"an untouched frame failed to open: {error}: {path:?}"
);
assert!(
error.to_string().contains("did not authenticate"),
"{error}"
);
return;
}
}
}
}