Add project files.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
use libcrux_hmac::hmac;
|
||||
|
||||
use crate::{kdf, util::compare, Epoch};
|
||||
pub mod serialize;
|
||||
pub type Mac = Vec<u8>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Ciphertext MAC is invalid")]
|
||||
InvalidCtMac,
|
||||
#[error("Encapsulation key MAC is invalid")]
|
||||
InvalidHdrMac,
|
||||
#[error("Authenticator previous root key present when should be erased")]
|
||||
AuthenticatorRootKeyPresent,
|
||||
#[error("Authenticator previous root key missing")]
|
||||
AuthenticatorRootKeyMissing,
|
||||
#[error("Authenticator previous MAC key present when should be erased")]
|
||||
AuthenticatorMacKeyPresent,
|
||||
#[error("Authenticator previous MAC key missing")]
|
||||
AuthenticatorMacKeyMissing,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
pub struct Authenticator {
|
||||
root_key: Mac,
|
||||
mac_key: Mac,
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl Authenticator {
|
||||
pub const MACSIZE: usize = 32usize;
|
||||
pub fn new(root_key: Vec<u8>, ep: Epoch) -> Self {
|
||||
let mut result = Self {
|
||||
root_key: vec![0u8; 32],
|
||||
mac_key: vec![0u8; 32],
|
||||
};
|
||||
result.update(ep, &root_key);
|
||||
result
|
||||
}
|
||||
|
||||
pub fn update(&mut self, ep: Epoch, k: &[u8]) {
|
||||
let ikm = [self.root_key.as_slice(), k].concat();
|
||||
let info = [
|
||||
b"Signal_PQCKA_V1_MLKEM768:Authenticator Update".as_slice(),
|
||||
&ep.to_be_bytes(),
|
||||
]
|
||||
.concat();
|
||||
let kdf_out = kdf::hkdf_to_vec(&[0u8; 32], &ikm, &info, 64);
|
||||
self.root_key = kdf_out[..32].to_vec();
|
||||
self.mac_key = kdf_out[32..].to_vec();
|
||||
}
|
||||
|
||||
#[hax_lib::requires(expected_mac.len() == Authenticator::MACSIZE)]
|
||||
pub fn verify_ct(&self, ep: Epoch, ct: &[u8], expected_mac: &[u8]) -> Result<(), Error> {
|
||||
if compare(expected_mac, &self.mac_ct(ep, ct)) != 0 {
|
||||
Err(Error::InvalidCtMac)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::ensures(|res| res.len() == Authenticator::MACSIZE)]
|
||||
pub fn mac_ct(&self, ep: Epoch, ct: &[u8]) -> Mac {
|
||||
let ct_mac_data = [
|
||||
b"Signal_PQCKA_V1_MLKEM768:ciphertext".as_slice(),
|
||||
&ep.to_be_bytes(),
|
||||
ct,
|
||||
]
|
||||
.concat();
|
||||
hmac(
|
||||
libcrux_hmac::Algorithm::Sha256,
|
||||
&self.mac_key,
|
||||
&ct_mac_data,
|
||||
Some(Self::MACSIZE),
|
||||
)
|
||||
}
|
||||
|
||||
#[hax_lib::requires(expected_mac.len() == Authenticator::MACSIZE)]
|
||||
pub fn verify_hdr(&self, ep: Epoch, hdr: &[u8], expected_mac: &[u8]) -> Result<(), Error> {
|
||||
if compare(expected_mac, &self.mac_hdr(ep, hdr)) != 0 {
|
||||
Err(Error::InvalidHdrMac)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::ensures(|res| res.len() == Authenticator::MACSIZE)]
|
||||
pub fn mac_hdr(&self, ep: Epoch, hdr: &[u8]) -> Mac {
|
||||
let ct_mac_data = [
|
||||
b"Signal_PQCKA_V1_MLKEM768:ekheader".as_slice(),
|
||||
&ep.to_be_bytes(),
|
||||
hdr,
|
||||
]
|
||||
.concat();
|
||||
hmac(
|
||||
libcrux_hmac::Algorithm::Sha256,
|
||||
&self.mac_key,
|
||||
&ct_mac_data,
|
||||
Some(Self::MACSIZE),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
use crate::proto;
|
||||
|
||||
use super::Authenticator;
|
||||
|
||||
impl Authenticator {
|
||||
pub fn into_pb(self) -> proto::pq_ratchet::Authenticator {
|
||||
proto::pq_ratchet::Authenticator {
|
||||
root_key: self.root_key,
|
||||
mac_key: self.mac_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: &proto::pq_ratchet::Authenticator) -> Self {
|
||||
Self {
|
||||
root_key: pb.root_key.clone(),
|
||||
mac_key: pb.mac_key.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::authenticator::Authenticator;
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
let auth = Authenticator::new(vec![42u8; 32], 1);
|
||||
let ack = auth.mac_ct(1, b"123");
|
||||
|
||||
let pb_auth = auth.into_pb();
|
||||
|
||||
let new_auth = Authenticator::from_pb(&pb_auth);
|
||||
let new_mac = new_auth.mac_ct(1, b"123");
|
||||
|
||||
assert_eq!(ack, new_mac);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,706 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
use super::{Direction, Epoch, EpochSecret, Error};
|
||||
use crate::kdf;
|
||||
use crate::proto::pq_ratchet as pqrpb;
|
||||
use crate::proto::pq_ratchet::ChainParams as ChainParamsPB;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// Parameters for controlling the behavior of PQR key chains.
|
||||
/// It's recommended to use the Default API for overriding values,
|
||||
/// as future values may be added to this struct, and Default allows
|
||||
/// them to be added in a backwards-compatible fashion.
|
||||
/// IE: let params = ChainParams{max_jump: 10, ..Default::default()};
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ChainParams {
|
||||
/// Disallow requesting a key that is more than MAX_JUMP ahead of `ctr`.
|
||||
/// If zero, defaults to the current library-compiled default value.
|
||||
pub max_jump: u32,
|
||||
/// Keep around keys back to at least `ctr - MAX_OOO_KEYS`, in case an out-of-order
|
||||
/// message comes in. Messages older than this that arrive out-of-order
|
||||
/// will not be able to be decrypted and will return Error::KeyTrimmed.
|
||||
/// If zero, defaults to the current library-compiled default value.
|
||||
pub max_ooo_keys: u32,
|
||||
}
|
||||
|
||||
impl Default for ChainParams {
|
||||
fn default() -> Self {
|
||||
DEFAULT_CHAIN_PARAMS
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_CHAIN_PARAMS: ChainParams = ChainParams {
|
||||
max_jump: 25_000,
|
||||
max_ooo_keys: 2_000,
|
||||
};
|
||||
|
||||
impl ChainParams {
|
||||
pub(crate) fn into_pb(self) -> ChainParamsPB {
|
||||
ChainParamsPB {
|
||||
max_jump: if self.max_jump == DEFAULT_CHAIN_PARAMS.max_jump {
|
||||
0
|
||||
} else {
|
||||
self.max_jump
|
||||
},
|
||||
max_ooo_keys: if self.max_ooo_keys == DEFAULT_CHAIN_PARAMS.max_ooo_keys {
|
||||
0
|
||||
} else {
|
||||
self.max_ooo_keys
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Public wrapper for test utilities and benchmarks.
|
||||
/// For internal use, call `into_pb` directly.
|
||||
#[cfg(feature = "test-utils")]
|
||||
pub fn into_pb_test(self) -> ChainParamsPB {
|
||||
self.into_pb()
|
||||
}
|
||||
}
|
||||
|
||||
impl ChainParamsPB {
|
||||
// The Default for protobufs is to have everything be zeros. Therefore,
|
||||
// we use some getter functions locally to apply sane defaults to values that
|
||||
// are not explicitly set.
|
||||
|
||||
fn max_jump_or_default(&self) -> u32 {
|
||||
if self.max_jump > 0 {
|
||||
self.max_jump
|
||||
} else {
|
||||
DEFAULT_CHAIN_PARAMS.max_jump
|
||||
}
|
||||
}
|
||||
fn max_ooo_keys_or_default(&self) -> u32 {
|
||||
if self.max_ooo_keys > 0 {
|
||||
self.max_ooo_keys
|
||||
} else {
|
||||
DEFAULT_CHAIN_PARAMS.max_ooo_keys
|
||||
}
|
||||
}
|
||||
/// When the size of our key history exceeds this amount, we run a
|
||||
/// garbage collection on it.
|
||||
fn trim_size(&self) -> usize {
|
||||
let max_ooo = self.max_ooo_keys_or_default() as usize;
|
||||
hax_lib::assume!(max_ooo < 390451572);
|
||||
max_ooo * 11 / 10 + 1
|
||||
}
|
||||
}
|
||||
|
||||
struct KeyHistory {
|
||||
// Keys are stored as [u8; 4][u8; 32], where the first is the index as a BE32
|
||||
// and the second is the key.
|
||||
// data.len() <= KEY_SIZE*TRIM_SIZE
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
/// ChainEpochDirection keeps track of keys related to either half of send/recv.
|
||||
struct ChainEpochDirection {
|
||||
ctr: u32,
|
||||
// next.len() == 32
|
||||
next: Vec<u8>,
|
||||
prev: KeyHistory,
|
||||
}
|
||||
|
||||
/// ChainEpoch keeps state on a single epoch's keys.
|
||||
struct ChainEpoch {
|
||||
send: ChainEpochDirection,
|
||||
recv: ChainEpochDirection,
|
||||
}
|
||||
|
||||
/// Chain keeps track of keys for all epochs.
|
||||
pub struct Chain {
|
||||
dir: Direction,
|
||||
current_epoch: Epoch,
|
||||
send_epoch: Epoch,
|
||||
links: VecDeque<ChainEpoch>, // stores [link[current_epoch-N] .. link[current_epoch]]
|
||||
// next_root.len() == 32
|
||||
next_root: Vec<u8>,
|
||||
params: pqrpb::ChainParams,
|
||||
}
|
||||
|
||||
/// We keep around this many epochs to keep prior to the current send epoch.
|
||||
/// We'll always keep the send epoch and any subsequent epochs.
|
||||
const EPOCHS_TO_KEEP_PRIOR_TO_SEND_EPOCH: usize = 1;
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl KeyHistory {
|
||||
/// Size in bytes of a single key stored within a KeyHistory.
|
||||
const KEY_SIZE: usize = 4 + 32;
|
||||
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
data: Vec::with_capacity(Self::KEY_SIZE * 2),
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::requires(_params.trim_size() < 119304647 && self.data.len() <= KeyHistory::KEY_SIZE * _params.trim_size())]
|
||||
fn add(&mut self, k: (u32, [u8; 32]), _params: &pqrpb::ChainParams) {
|
||||
self.data.extend_from_slice(&k.0.to_be_bytes()[..]);
|
||||
self.data.extend_from_slice(&k.1[..]);
|
||||
}
|
||||
|
||||
#[hax_lib::opaque] // ordering of slices needed
|
||||
fn gc(&mut self, current_key: u32, params: &pqrpb::ChainParams) {
|
||||
if self.data.len() >= params.trim_size() * Self::KEY_SIZE {
|
||||
// We assume that k.0 is the highest key index we've ever seen, and base
|
||||
// our trimming on that.
|
||||
assert!(current_key >= params.max_ooo_keys_or_default());
|
||||
let trim_horizon = &(current_key - params.max_ooo_keys_or_default()).to_be_bytes()[..];
|
||||
|
||||
// This does a single O(n) pass over our list, dropping all keys less than
|
||||
// our computed trim horizon.
|
||||
let mut i: usize = 0;
|
||||
while i < self.data.len() {
|
||||
if matches!(
|
||||
trim_horizon.cmp(&self.data[i..i + 4]),
|
||||
std::cmp::Ordering::Greater
|
||||
) {
|
||||
self.remove(i, params);
|
||||
// Don't advance i here; we could have replaced the value there-in
|
||||
// with another old key.
|
||||
} else {
|
||||
i += Self::KEY_SIZE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.data.clear();
|
||||
}
|
||||
|
||||
#[hax_lib::requires(my_array_index <= self.data.len() && _params.trim_size() < 119304647 && self.data.len() <= KeyHistory::KEY_SIZE * _params.trim_size())]
|
||||
fn remove(&mut self, mut my_array_index: usize, _params: &pqrpb::ChainParams) {
|
||||
if my_array_index + Self::KEY_SIZE < self.data.len() {
|
||||
let new_end = self.data.len() - Self::KEY_SIZE;
|
||||
self.data.copy_within(new_end.., my_array_index);
|
||||
my_array_index = new_end;
|
||||
}
|
||||
self.data.truncate(my_array_index);
|
||||
}
|
||||
|
||||
#[hax_lib::opaque] // needs a model of step_by loop with return
|
||||
fn get(
|
||||
&mut self,
|
||||
at: u32,
|
||||
current_ctr: u32,
|
||||
params: &pqrpb::ChainParams,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
assert_eq!(self.data.len() % Self::KEY_SIZE, 0);
|
||||
if at + (params.max_ooo_keys_or_default()) < current_ctr {
|
||||
// We've already discarded this because it's too old.
|
||||
return Err(Error::KeyTrimmed(at));
|
||||
}
|
||||
let want = at.to_be_bytes();
|
||||
for i in (0..self.data.len()).step_by(Self::KEY_SIZE) {
|
||||
if self.data[i..i + 4] == want {
|
||||
let out = self.data[i + 4..i + Self::KEY_SIZE].to_vec();
|
||||
self.remove(i, params);
|
||||
return Ok(out);
|
||||
}
|
||||
}
|
||||
// This is a key we should have and we don't, so it must have already
|
||||
// been requested.
|
||||
Err(Error::KeyAlreadyRequested(at))
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl ChainEpochDirection {
|
||||
fn new(k: &[u8]) -> Self {
|
||||
Self {
|
||||
ctr: 0,
|
||||
prev: KeyHistory::new(),
|
||||
next: k.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::requires(self.next.len() > 0 && self.ctr < u32::MAX)]
|
||||
fn next_key(&mut self) -> (u32, Vec<u8>) {
|
||||
let (idx, key) = Self::next_key_internal(&mut self.next, &mut self.ctr);
|
||||
(idx, key.to_vec())
|
||||
}
|
||||
|
||||
#[hax_lib::requires(next.len() > 0 && *ctr < u32::MAX)]
|
||||
#[hax_lib::ensures(|_| *future(ctr) == ctr + 1)]
|
||||
fn next_key_internal(next: &mut [u8], ctr: &mut u32) -> (u32, [u8; 32]) {
|
||||
assert!(!next.is_empty());
|
||||
*ctr += 1;
|
||||
let mut genr8r = [0u8; 64];
|
||||
kdf::hkdf_to_slice(
|
||||
&[0u8; 32], // 32 is the hash output length
|
||||
&*next,
|
||||
[
|
||||
ctr.to_be_bytes().as_slice(),
|
||||
b"Signal PQ Ratchet V1 Chain Next",
|
||||
]
|
||||
.concat()
|
||||
.as_slice(),
|
||||
&mut genr8r,
|
||||
);
|
||||
next.copy_from_slice(&genr8r[..32]);
|
||||
(*ctr, genr8r[32..].try_into().expect("correct size"))
|
||||
}
|
||||
|
||||
fn key(&mut self, at: u32, params: &pqrpb::ChainParams) -> Result<Vec<u8>, Error> {
|
||||
match at.cmp(&self.ctr) {
|
||||
Ordering::Greater => {
|
||||
if at - self.ctr > params.max_jump_or_default() {
|
||||
return Err(Error::KeyJump(self.ctr, at));
|
||||
}
|
||||
}
|
||||
Ordering::Less => {
|
||||
return self.prev.get(at, self.ctr, params);
|
||||
}
|
||||
Ordering::Equal => {
|
||||
// We've already returned this key once, we won't do it again.
|
||||
return Err(Error::KeyAlreadyRequested(at));
|
||||
}
|
||||
}
|
||||
hax_lib::assume!(
|
||||
params.max_ooo_keys_or_default() < 390451572 && self.ctr <= u32::MAX - 390451572
|
||||
);
|
||||
if at > self.ctr + params.max_ooo_keys_or_default() {
|
||||
// We're about to make all currently-held keys obsolete - just remove
|
||||
// them all.
|
||||
self.prev.clear();
|
||||
}
|
||||
while at > self.ctr + 1 {
|
||||
hax_lib::loop_invariant!(self.ctr < u32::MAX);
|
||||
hax_lib::loop_decreases!(u32::MAX - self.ctr);
|
||||
hax_lib::assume!(self.next.len() > 0);
|
||||
let k = Self::next_key_internal(&mut self.next, &mut self.ctr);
|
||||
hax_lib::assume!(
|
||||
params.max_ooo_keys_or_default() < 390451572 && self.ctr <= u32::MAX - 390451572
|
||||
);
|
||||
// Only add keys into our history if we're not going to immediately GC them.
|
||||
if self.ctr + params.max_ooo_keys_or_default() >= at {
|
||||
hax_lib::assume!(
|
||||
params.trim_size() < 119304647
|
||||
&& self.prev.data.len() <= KeyHistory::KEY_SIZE * params.trim_size()
|
||||
);
|
||||
self.prev.add(k, params);
|
||||
}
|
||||
}
|
||||
// After we've potentially added some new keys, see if there's any we
|
||||
// want to throw away.
|
||||
self.prev.gc(self.ctr, params);
|
||||
|
||||
hax_lib::assume!(self.next.len() > 0);
|
||||
|
||||
Ok(Self::next_key_internal(&mut self.next, &mut self.ctr)
|
||||
.1
|
||||
.to_vec())
|
||||
}
|
||||
|
||||
fn into_pb(self) -> pqrpb::chain::epoch::EpochDirection {
|
||||
pqrpb::chain::epoch::EpochDirection {
|
||||
ctr: self.ctr,
|
||||
next: self.next,
|
||||
prev: self.prev.data,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_pb(pb: pqrpb::chain::epoch::EpochDirection) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
ctr: pb.ctr,
|
||||
next: pb.next,
|
||||
prev: KeyHistory { data: pb.prev },
|
||||
})
|
||||
}
|
||||
|
||||
fn clear_next(&mut self) {
|
||||
self.next.clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl Chain {
|
||||
#[hax_lib::requires(genr8r.len() == 96)]
|
||||
fn ced_for_direction(genr8r: &[u8], dir: &Direction) -> ChainEpochDirection {
|
||||
ChainEpochDirection::new(match dir {
|
||||
Direction::A2B => &genr8r[32..64],
|
||||
Direction::B2A => &genr8r[64..96],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new(initial_key: &[u8], dir: Direction, params: ChainParamsPB) -> Result<Self, Error> {
|
||||
let mut genr8r = [0u8; 96];
|
||||
kdf::hkdf_to_slice(
|
||||
&[0u8; 32],
|
||||
initial_key,
|
||||
b"Signal PQ Ratchet V1 Chain Start",
|
||||
&mut genr8r,
|
||||
);
|
||||
Ok(Self {
|
||||
dir,
|
||||
current_epoch: 0,
|
||||
send_epoch: 0,
|
||||
links: VecDeque::from([ChainEpoch {
|
||||
send: Self::ced_for_direction(&genr8r, &dir),
|
||||
recv: Self::ced_for_direction(&genr8r, &dir.switch()),
|
||||
}]),
|
||||
next_root: genr8r[0..32].to_vec(),
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_epoch(&mut self, epoch_secret: EpochSecret) {
|
||||
// This assume could be turned into a precondition but it uses private fields
|
||||
hax_lib::assume!(
|
||||
self.current_epoch < u64::MAX && epoch_secret.epoch == self.current_epoch + 1
|
||||
);
|
||||
assert!(epoch_secret.epoch == self.current_epoch + 1);
|
||||
let mut genr8r = [0u8; 96];
|
||||
kdf::hkdf_to_slice(
|
||||
&self.next_root,
|
||||
&epoch_secret.secret,
|
||||
b"Signal PQ Ratchet V1 Chain Add Epoch",
|
||||
&mut genr8r,
|
||||
);
|
||||
self.current_epoch = epoch_secret.epoch;
|
||||
self.next_root = genr8r[0..32].to_vec();
|
||||
self.links.push_back(ChainEpoch {
|
||||
send: Self::ced_for_direction(&genr8r, &self.dir),
|
||||
recv: Self::ced_for_direction(&genr8r, &self.dir.switch()),
|
||||
});
|
||||
}
|
||||
|
||||
#[hax_lib::ensures(|res| if let Ok(v) = res {v < self.links.len()} else {true})]
|
||||
fn epoch_idx(&mut self, epoch: Epoch) -> Result<usize, Error> {
|
||||
if epoch > self.current_epoch {
|
||||
return Err(Error::EpochOutOfRange(epoch));
|
||||
}
|
||||
let back = (self.current_epoch - epoch) as usize;
|
||||
let links = self.links.len();
|
||||
if back >= links {
|
||||
return Err(Error::EpochOutOfRange(epoch));
|
||||
}
|
||||
Ok(links - 1 - back)
|
||||
}
|
||||
|
||||
pub fn send_key(&mut self, epoch: Epoch) -> Result<(u32, Vec<u8>), Error> {
|
||||
if epoch < self.send_epoch {
|
||||
return Err(Error::SendKeyEpochDecreased(self.send_epoch, epoch));
|
||||
}
|
||||
let mut epoch_index = self.epoch_idx(epoch)?;
|
||||
if self.send_epoch != epoch {
|
||||
self.send_epoch = epoch;
|
||||
while epoch_index > EPOCHS_TO_KEEP_PRIOR_TO_SEND_EPOCH {
|
||||
hax_lib::loop_decreases!(epoch_index);
|
||||
self.links.pop_front();
|
||||
epoch_index -= 1;
|
||||
}
|
||||
for i in 0..epoch_index {
|
||||
hax_lib::assume!(i < self.links.len());
|
||||
self.links[i].send.clear_next();
|
||||
}
|
||||
}
|
||||
hax_lib::assume!(
|
||||
epoch_index < self.links.len()
|
||||
&& self.links[epoch_index].send.next.len() > 0
|
||||
&& self.links[epoch_index].send.ctr < u32::MAX
|
||||
);
|
||||
Ok(self.links[epoch_index].send.next_key())
|
||||
}
|
||||
|
||||
pub fn recv_key(&mut self, epoch: Epoch, index: u32) -> Result<Vec<u8>, Error> {
|
||||
let epoch_index = self.epoch_idx(epoch)?;
|
||||
self.links[epoch_index].recv.key(index, &self.params)
|
||||
}
|
||||
|
||||
#[hax_lib::opaque] // into_iter and map
|
||||
pub(crate) fn into_pb(self) -> pqrpb::Chain {
|
||||
pqrpb::Chain {
|
||||
direction: self.dir.into(),
|
||||
current_epoch: self.current_epoch,
|
||||
send_epoch: self.send_epoch,
|
||||
links: self
|
||||
.links
|
||||
.into_iter()
|
||||
.map(|link| pqrpb::chain::Epoch {
|
||||
send: Some(link.send.into_pb()),
|
||||
recv: Some(link.recv.into_pb()),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
next_root: self.next_root,
|
||||
params: Some(self.params),
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::opaque] // into_iter and map
|
||||
pub(crate) fn from_pb(pb: pqrpb::Chain) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
dir: pb.direction.try_into().map_err(|_| Error::StateDecode)?,
|
||||
current_epoch: pb.current_epoch,
|
||||
send_epoch: pb.send_epoch,
|
||||
next_root: pb.next_root,
|
||||
links: pb
|
||||
.links
|
||||
.into_iter()
|
||||
.map(|link| {
|
||||
Ok::<ChainEpoch, Error>(ChainEpoch {
|
||||
send: ChainEpochDirection::from_pb(link.send.ok_or(Error::StateDecode)?)?,
|
||||
recv: ChainEpochDirection::from_pb(link.recv.ok_or(Error::StateDecode)?)?,
|
||||
})
|
||||
})
|
||||
.collect::<Result<VecDeque<_>, _>>()?,
|
||||
params: pb.params.ok_or(Error::StateDecode)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::{Direction, EpochSecret, Error};
|
||||
use proptest::prelude::*;
|
||||
use rand::TryRngCore;
|
||||
use rand::seq::SliceRandom;
|
||||
|
||||
#[test]
|
||||
fn directions_match() {
|
||||
let mut a2b = Chain::new(b"1", Direction::A2B, ChainParams::default().into_pb()).unwrap();
|
||||
let mut b2a = Chain::new(b"1", Direction::B2A, ChainParams::default().into_pb()).unwrap();
|
||||
let sk1 = a2b.send_key(0).unwrap();
|
||||
assert_eq!(sk1.0, 1);
|
||||
assert_eq!(sk1.1, b2a.recv_key(0, 1).unwrap());
|
||||
a2b.add_epoch(EpochSecret {
|
||||
epoch: 1,
|
||||
secret: vec![2],
|
||||
});
|
||||
b2a.add_epoch(EpochSecret {
|
||||
epoch: 1,
|
||||
secret: vec![2],
|
||||
});
|
||||
let sk2 = a2b.send_key(1).unwrap();
|
||||
assert_eq!(sk2.0, 1);
|
||||
assert_eq!(sk2.1, b2a.recv_key(1, 1).unwrap());
|
||||
for _i in 2..10 {
|
||||
a2b.send_key(1).unwrap();
|
||||
}
|
||||
let sk3 = a2b.send_key(1).unwrap();
|
||||
assert_eq!(sk3.0, 10);
|
||||
assert_eq!(sk3.1, b2a.recv_key(1, 10).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn previously_returned_key() {
|
||||
let mut a2b = Chain::new(b"1", Direction::A2B, ChainParams::default().into_pb()).unwrap();
|
||||
a2b.recv_key(0, 2).expect("should get key first time");
|
||||
assert!(matches!(
|
||||
a2b.recv_key(0, 2),
|
||||
Err(Error::KeyAlreadyRequested(2))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn very_old_keys_are_trimmed() {
|
||||
let params = ChainParams {
|
||||
max_jump: 10,
|
||||
max_ooo_keys: 10,
|
||||
}
|
||||
.into_pb();
|
||||
let mut a2b = Chain::new(b"1", Direction::A2B, params).unwrap();
|
||||
a2b.recv_key(0, 10).expect("should allow this jump");
|
||||
a2b.recv_key(0, 12).expect("should allow progression");
|
||||
assert!(matches!(a2b.recv_key(0, 1), Err(Error::KeyTrimmed(1))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_order_keys() {
|
||||
let max_ooo = DEFAULT_CHAIN_PARAMS.max_ooo_keys;
|
||||
let mut a2b = Chain::new(b"1", Direction::A2B, ChainParams::default().into_pb()).unwrap();
|
||||
let mut b2a = Chain::new(b"1", Direction::B2A, ChainParams::default().into_pb()).unwrap();
|
||||
let mut keys = Vec::with_capacity(max_ooo as usize);
|
||||
for _i in 0..(max_ooo as usize) {
|
||||
keys.push(a2b.send_key(0).unwrap());
|
||||
}
|
||||
let mut rng = rand::rngs::OsRng.unwrap_err();
|
||||
keys.shuffle(&mut rng);
|
||||
for (idx, key) in keys {
|
||||
assert_eq!(b2a.recv_key(0, idx).unwrap(), key);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_old_send_keys() {
|
||||
let mut a2b = Chain::new(b"1", Direction::A2B, ChainParams::default().into_pb()).unwrap();
|
||||
a2b.send_key(0).unwrap();
|
||||
a2b.send_key(0).unwrap();
|
||||
a2b.add_epoch(EpochSecret {
|
||||
epoch: 1,
|
||||
secret: vec![2],
|
||||
});
|
||||
a2b.send_key(1).unwrap();
|
||||
assert!(matches!(
|
||||
a2b.send_key(0).unwrap_err(),
|
||||
Error::SendKeyEpochDecreased(1, 0)
|
||||
));
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum KeyHistoryAction {
|
||||
AddNewNext,
|
||||
AddNewSkip,
|
||||
RequestStored(usize),
|
||||
RequestNotStored(usize),
|
||||
GarbageCollect,
|
||||
}
|
||||
|
||||
impl KeyHistoryAction {
|
||||
fn strategy() -> impl Strategy<Value = Self> {
|
||||
proptest::prop_oneof![
|
||||
Just(Self::AddNewNext),
|
||||
Just(Self::AddNewSkip),
|
||||
any::<usize>().prop_map(Self::RequestStored),
|
||||
any::<usize>().prop_map(Self::RequestNotStored),
|
||||
Just(Self::GarbageCollect),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_history_prop_test() {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
proptest!(|(actions in proptest::collection::vec(KeyHistoryAction::strategy(), ..25))| {
|
||||
let mut kh = KeyHistory::new();
|
||||
let mut stored = vec![];
|
||||
let mut not_stored = vec![];
|
||||
let mut ctr = 0u32;
|
||||
let params = pqrpb::ChainParams {
|
||||
max_ooo_keys: 3u32,
|
||||
max_jump: 5u32,
|
||||
};
|
||||
log::debug!("========= STARTING =========");
|
||||
for action in actions {
|
||||
match action {
|
||||
KeyHistoryAction::AddNewNext => {
|
||||
log::debug!("adding {}", ctr);
|
||||
kh.add((ctr, [1u8; 32]), ¶ms);
|
||||
stored.push(ctr);
|
||||
ctr += 1;
|
||||
}
|
||||
KeyHistoryAction::AddNewSkip => {
|
||||
log::debug!("skipping {}", ctr);
|
||||
not_stored.push(ctr);
|
||||
ctr += 1;
|
||||
}
|
||||
KeyHistoryAction::RequestStored(i) => {
|
||||
if !stored.is_empty() {
|
||||
let k = stored.swap_remove(i % stored.len());
|
||||
log::debug!("requesting stored {}", k);
|
||||
kh.get(k, ctr, ¶ms).unwrap();
|
||||
not_stored.push(k);
|
||||
}
|
||||
}
|
||||
KeyHistoryAction::RequestNotStored(i) => {
|
||||
if !not_stored.is_empty() {
|
||||
let k = not_stored.swap_remove(i % not_stored.len());
|
||||
log::debug!("requesting not stored {}", k);
|
||||
kh.get(k, ctr, ¶ms).unwrap_err();
|
||||
}
|
||||
}
|
||||
KeyHistoryAction::GarbageCollect => {
|
||||
log::debug!("gc at {}", ctr);
|
||||
kh.gc(ctr, ¶ms);
|
||||
}
|
||||
}
|
||||
let mut fell_off = vec![];
|
||||
(fell_off, stored) = stored.into_iter().partition(
|
||||
|n| n + params.max_ooo_keys < ctr);
|
||||
if !fell_off.is_empty() {
|
||||
log::debug!("fell off: {:?}", fell_off);
|
||||
}
|
||||
not_stored.extend(fell_off.into_iter());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum CEDAction {
|
||||
NextKey,
|
||||
NextKeyAt(u32),
|
||||
RequestStored(usize),
|
||||
RequestNotStored(usize),
|
||||
TooHigh,
|
||||
}
|
||||
|
||||
impl CEDAction {
|
||||
fn strategy() -> impl Strategy<Value = Self> {
|
||||
proptest::prop_oneof![
|
||||
Just(Self::NextKey),
|
||||
any::<u32>().prop_map(Self::NextKeyAt),
|
||||
any::<usize>().prop_map(Self::RequestStored),
|
||||
any::<usize>().prop_map(Self::RequestNotStored),
|
||||
Just(Self::TooHigh),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ced_prop_test() {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
proptest!(|(actions in proptest::collection::vec(CEDAction::strategy(), ..25))| {
|
||||
let mut ced = ChainEpochDirection::new(&[1u8; 32]);
|
||||
let mut stored = vec![];
|
||||
let mut not_stored = vec![0];
|
||||
let mut ctr = 0u32;
|
||||
let params = pqrpb::ChainParams {
|
||||
max_ooo_keys: 3u32,
|
||||
max_jump: 5u32,
|
||||
};
|
||||
log::debug!("========= STARTING =========");
|
||||
for action in actions {
|
||||
match action {
|
||||
CEDAction::NextKey => {
|
||||
ctr += 1;
|
||||
log::debug!("next_key {}", ctr);
|
||||
let k = ced.next_key().0;
|
||||
not_stored.push(k);
|
||||
}
|
||||
CEDAction::NextKeyAt(i) => {
|
||||
let jump = i % params.max_jump;
|
||||
ctr += 1;
|
||||
for at in ctr..ctr+jump {
|
||||
stored.push(at);
|
||||
}
|
||||
ctr += jump;
|
||||
log::debug!("next_key_at {}", ctr);
|
||||
ced.key(ctr, ¶ms).unwrap();
|
||||
not_stored.push(ctr);
|
||||
}
|
||||
CEDAction::RequestStored(i) => {
|
||||
if !stored.is_empty() {
|
||||
let k = stored.swap_remove(i % stored.len());
|
||||
log::debug!("requesting stored {}", k);
|
||||
ced.key(k, ¶ms).unwrap();
|
||||
not_stored.push(k);
|
||||
}
|
||||
}
|
||||
CEDAction::RequestNotStored(i) => {
|
||||
if !not_stored.is_empty() {
|
||||
let k = not_stored.swap_remove(i % not_stored.len());
|
||||
log::debug!("requesting not stored {}", k);
|
||||
ced.key(k, ¶ms).unwrap_err();
|
||||
}
|
||||
}
|
||||
CEDAction::TooHigh => {
|
||||
let high = ctr + params.max_jump + 1;
|
||||
log::debug!("too high {}", high);
|
||||
ced.key(high, ¶ms).unwrap_err();
|
||||
}
|
||||
}
|
||||
let mut fell_off = vec![];
|
||||
(fell_off, stored) = stored.into_iter().partition(
|
||||
|n| n + params.max_ooo_keys < ctr);
|
||||
if !fell_off.is_empty() {
|
||||
log::debug!("fell off: {:?}", fell_off);
|
||||
}
|
||||
not_stored.extend(fell_off.into_iter());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pub mod gf;
|
||||
pub mod polynomial;
|
||||
pub mod round_robin;
|
||||
|
||||
#[derive(Debug, thiserror::Error, Copy, Clone, PartialEq)]
|
||||
pub enum EncodingError {
|
||||
#[error("Polynomial error: {0}")]
|
||||
PolynomialError(polynomial::PolynomialError),
|
||||
#[error("Index decoding error")]
|
||||
ChunkIndexDecodingError,
|
||||
#[error("Data decoding error")]
|
||||
ChunkDataDecodingError,
|
||||
}
|
||||
|
||||
impl From<polynomial::PolynomialError> for EncodingError {
|
||||
fn from(value: polynomial::PolynomialError) -> Self {
|
||||
Self::PolynomialError(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Chunk {
|
||||
pub index: u16,
|
||||
pub data: [u8; 32],
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
pub trait Encoder {
|
||||
#[hax_lib::requires(true)]
|
||||
fn encode_bytes(msg: &[u8]) -> Result<Self, EncodingError>
|
||||
where
|
||||
Self: Sized;
|
||||
fn next_chunk(&mut self) -> Chunk;
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
pub trait Decoder {
|
||||
#[hax_lib::requires(true)]
|
||||
fn new(len_bytes: usize) -> Result<Self, EncodingError>
|
||||
where
|
||||
Self: Sized;
|
||||
fn add_chunk(&mut self, chunk: &Chunk);
|
||||
fn decoded_message(&self) -> Option<Vec<u8>>;
|
||||
}
|
||||
|
||||
// XXX: For ease of formal verification with hax, we avoid using
|
||||
// functions that return mutable references, such as Option::take.
|
||||
// We therefore `take` the value out and store it back for the
|
||||
// encoder and decoder.
|
||||
#[hax_lib::attributes]
|
||||
impl<T: Encoder> Encoder for Option<T> {
|
||||
fn encode_bytes(msg: &[u8]) -> Result<Self, EncodingError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Ok(Some(T::encode_bytes(msg)?))
|
||||
}
|
||||
|
||||
#[hax_lib::requires(self.is_some())]
|
||||
fn next_chunk(&mut self) -> Chunk {
|
||||
let mut tmp = self.take().unwrap();
|
||||
hax_lib::fstar!(
|
||||
"Hax_lib.v_assume (f_next_chunk_pre #v_T #FStar.Tactics.Typeclasses.solve tmp)"
|
||||
);
|
||||
let chunk = T::next_chunk(&mut tmp);
|
||||
*self = Some(tmp);
|
||||
chunk
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl<T: Decoder> Decoder for Option<T> {
|
||||
fn new(len_bytes: usize) -> Result<Self, EncodingError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Ok(Some(T::new(len_bytes)?))
|
||||
}
|
||||
|
||||
#[hax_lib::requires(self.is_some())]
|
||||
fn add_chunk(&mut self, chunk: &Chunk) {
|
||||
let mut tmp = self.take().unwrap();
|
||||
hax_lib::fstar!(
|
||||
"Hax_lib.v_assume (f_add_chunk_pre #v_T #FStar.Tactics.Typeclasses.solve tmp chunk)"
|
||||
);
|
||||
T::add_chunk(&mut tmp, chunk);
|
||||
*self = Some(tmp);
|
||||
}
|
||||
|
||||
#[hax_lib::requires(self.is_some())]
|
||||
fn decoded_message(&self) -> Option<Vec<u8>> {
|
||||
let value = self.as_ref().unwrap();
|
||||
hax_lib::fstar!(
|
||||
"Hax_lib.v_assume (f_decoded_message_pre #v_T #FStar.Tactics.Typeclasses.solve value)"
|
||||
);
|
||||
T::decoded_message(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,689 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
use std::ops;
|
||||
|
||||
// https://web.eecs.utk.edu/~jplank/plank/papers/CS-07-593/primitive-polynomial-table.txt
|
||||
pub const POLY: u32 = 0x1100b; // 0o210013
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
#[repr(transparent)]
|
||||
#[hax_lib::fstar::after(
|
||||
r#"
|
||||
let to_gf (s: t_GF16) = Spec.GF16.to_bv s.f_value
|
||||
"#
|
||||
)]
|
||||
pub struct GF16 {
|
||||
pub value: u16,
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl ops::AddAssign<&GF16> for GF16 {
|
||||
#[allow(clippy::suspicious_op_assign_impl)]
|
||||
#[requires(true)]
|
||||
#[hax_lib::ensures(|result| fstar!(r#"
|
||||
to_gf self_e_future ==
|
||||
Spec.GF16.gf_add (to_gf self_) (to_gf other)
|
||||
"#))]
|
||||
fn add_assign(&mut self, other: &Self) {
|
||||
hax_lib::fstar!("Spec.GF16.xor_is_gf_add_lemma self.f_value other.f_value");
|
||||
self.value ^= other.value;
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl ops::AddAssign for GF16 {
|
||||
#[hax_lib::ensures(|result| fstar!(r#"
|
||||
to_gf self_e_future ==
|
||||
Spec.GF16.gf_add (to_gf self_) (to_gf other)
|
||||
"#))]
|
||||
fn add_assign(&mut self, other: Self) {
|
||||
hax_lib::fstar!("Spec.GF16.xor_is_gf_add_lemma self.f_value other.f_value");
|
||||
self.add_assign(&other);
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl ops::Add for GF16 {
|
||||
type Output = Self;
|
||||
#[hax_lib::ensures(|result| fstar!(r#"
|
||||
to_gf result ==
|
||||
Spec.GF16.gf_add (to_gf self_) (to_gf other)
|
||||
"#))]
|
||||
fn add(self, other: Self) -> Self {
|
||||
let mut out = self;
|
||||
out += &other;
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl ops::Add<&GF16> for GF16 {
|
||||
type Output = Self;
|
||||
#[hax_lib::ensures(|result| fstar!(r#"
|
||||
to_gf result ==
|
||||
Spec.GF16.gf_add (to_gf self_) (to_gf other)
|
||||
"#))]
|
||||
fn add(self, other: &Self) -> Self {
|
||||
let mut out = self;
|
||||
out += other;
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl ops::SubAssign<&GF16> for GF16 {
|
||||
#[allow(clippy::suspicious_op_assign_impl)]
|
||||
#[hax_lib::ensures(|result| fstar!(r#"
|
||||
to_gf self_e_future ==
|
||||
Spec.GF16.gf_sub (to_gf self_) (to_gf other)
|
||||
"#))]
|
||||
fn sub_assign(&mut self, other: &Self) {
|
||||
*self += other;
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl ops::SubAssign for GF16 {
|
||||
#[hax_lib::ensures(|result| fstar!(r#"
|
||||
to_gf self_e_future ==
|
||||
Spec.GF16.gf_sub (to_gf self_) (to_gf other)
|
||||
"#))]
|
||||
fn sub_assign(&mut self, other: Self) {
|
||||
self.sub_assign(&other);
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl ops::Sub for GF16 {
|
||||
type Output = Self;
|
||||
#[hax_lib::ensures(|result| fstar!(r#"
|
||||
to_gf result ==
|
||||
Spec.GF16.gf_sub (to_gf self_) (to_gf other)
|
||||
"#))]
|
||||
fn sub(self, other: Self) -> Self {
|
||||
let mut out = self;
|
||||
out -= &other;
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl ops::Sub<&GF16> for GF16 {
|
||||
type Output = Self;
|
||||
#[hax_lib::ensures(|result| fstar!(r#"
|
||||
to_gf result ==
|
||||
Spec.GF16.gf_sub (to_gf self_) (to_gf other)
|
||||
"#))]
|
||||
fn sub(self, other: &Self) -> Self {
|
||||
let mut out = self;
|
||||
out -= other;
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl ops::MulAssign<&GF16> for GF16 {
|
||||
fn mul_assign(&mut self, other: &Self) {
|
||||
#[cfg(all(
|
||||
not(hax),
|
||||
any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")
|
||||
))]
|
||||
if check_accelerated::TOKEN.get() {
|
||||
self.value = accelerated::mul(self.value, other.value);
|
||||
return;
|
||||
}
|
||||
self.value = unaccelerated::mul(self.value, other.value);
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl ops::MulAssign for GF16 {
|
||||
fn mul_assign(&mut self, other: Self) {
|
||||
self.mul_assign(&other);
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl ops::Mul for GF16 {
|
||||
type Output = Self;
|
||||
fn mul(self, other: Self) -> Self {
|
||||
let mut out = self;
|
||||
out *= &other;
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl ops::Mul<&GF16> for GF16 {
|
||||
type Output = Self;
|
||||
fn mul(self, other: &Self) -> Self {
|
||||
let mut out = self;
|
||||
out *= other;
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl ops::DivAssign<&GF16> for GF16 {
|
||||
#[allow(clippy::suspicious_op_assign_impl)]
|
||||
fn div_assign(&mut self, other: &Self) {
|
||||
*self = self.div_impl(other);
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl ops::DivAssign for GF16 {
|
||||
fn div_assign(&mut self, other: Self) {
|
||||
*self = self.div_impl(&other);
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl ops::Div for GF16 {
|
||||
type Output = Self;
|
||||
fn div(self, other: Self) -> Self {
|
||||
self.div_impl(&other)
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl ops::Div<&GF16> for GF16 {
|
||||
type Output = Self;
|
||||
fn div(self, other: &Self) -> Self {
|
||||
self.div_impl(other)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[hax_lib::requires(into.len() <= usize::MAX - 2)]
|
||||
#[hax_lib::ensures(|_| future(into).len() == into.len())]
|
||||
pub fn parallel_mult(a: GF16, into: &mut [GF16]) {
|
||||
let mut i: usize = 0;
|
||||
#[cfg(hax)]
|
||||
let l = into.len();
|
||||
while i + 2 <= into.len() {
|
||||
hax_lib::loop_decreases!(l - i);
|
||||
hax_lib::loop_invariant!(into.len() == l && i <= l);
|
||||
(into[i].value, into[i + 1].value) = mul2_u16(a.value, into[i].value, into[i + 1].value);
|
||||
i += 2;
|
||||
}
|
||||
if i < into.len() {
|
||||
into[i] *= a;
|
||||
}
|
||||
}
|
||||
|
||||
fn mul2_u16(a: u16, b1: u16, b2: u16) -> (u16, u16) {
|
||||
#[cfg(all(
|
||||
not(hax),
|
||||
any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")
|
||||
))]
|
||||
if check_accelerated::TOKEN.get() {
|
||||
return accelerated::mul2(a, b1, b2);
|
||||
}
|
||||
(unaccelerated::mul(a, b1), unaccelerated::mul(a, b2))
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
mod accelerated {
|
||||
#[cfg(target_arch = "x86")]
|
||||
use core::arch::x86 as arch;
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
use core::arch::x86_64 as arch;
|
||||
|
||||
pub fn mul(a: u16, b: u16) -> u16 {
|
||||
mul2(a, b, 0).0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[target_feature(enable = "pclmulqdq")]
|
||||
unsafe fn mul2_unreduced(a: u16, b1: u16, b2: u16) -> (u32, u32) {
|
||||
let a = arch::_mm_set_epi64x(0, a as i64);
|
||||
let b = arch::_mm_set_epi64x(0, ((b2 as i64) << 32) | (b1 as i64));
|
||||
let clmul = arch::_mm_clmulepi64_si128(a, b, 0);
|
||||
|
||||
// Some architectures have _mm_cvtsi128_si64, which pulls out the full 64
|
||||
// bits at once. However, more architectures have _mm_cvtsi128_si32, which
|
||||
// just pulls out 32, and it turns out that doing that twice appears to have
|
||||
// about the same latency.
|
||||
let b1out = arch::_mm_cvtsi128_si32(clmul) as u32;
|
||||
// To pull out the higher bits (for b2), shift our result right by 4 bytes
|
||||
// (32 bits), then pull out the lowest 32 bits.
|
||||
let b2out = arch::_mm_cvtsi128_si32(arch::_mm_srli_si128(clmul, 4)) as u32;
|
||||
(b1out, b2out)
|
||||
}
|
||||
|
||||
pub fn mul2(a: u16, b1: u16, b2: u16) -> (u16, u16) {
|
||||
let unreduced_products = unsafe { mul2_unreduced(a, b1, b2) };
|
||||
(
|
||||
super::reduce::poly_reduce(unreduced_products.0),
|
||||
super::reduce::poly_reduce(unreduced_products.1),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
mod accelerated {
|
||||
use core::arch::aarch64;
|
||||
|
||||
pub fn mul(a: u16, b: u16) -> u16 {
|
||||
mul2(a, b, 0).0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[target_feature(enable = "neon,aes")]
|
||||
unsafe fn mul2_unreduced(a: u16, b1: u16, b2: u16) -> u128 {
|
||||
aarch64::vmull_p64(a as u64, ((b1 as u64) << 32) | (b2 as u64))
|
||||
}
|
||||
|
||||
pub fn mul2(a: u16, b1: u16, b2: u16) -> (u16, u16) {
|
||||
let unreduced_product = unsafe { mul2_unreduced(a, b1, b2) };
|
||||
(
|
||||
super::reduce::poly_reduce((unreduced_product >> 32) as u32),
|
||||
super::reduce::poly_reduce(unreduced_product as u32),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
#[cfg(target_arch = "arm")]
|
||||
mod accelerated {
|
||||
use core::arch::arm;
|
||||
|
||||
pub fn mul(a: u16, b: u16) -> u16 {
|
||||
mul2(a, b, 0).0
|
||||
}
|
||||
|
||||
pub fn mul2(a: u16, b1: u16, b2: u16) -> (u16, u16) {
|
||||
// 32-bit ARM only provides polynomial multiplication of 8-bit
|
||||
// values, but it does provide _parallel_ multiplication of those values.
|
||||
// We use this to implement simple long-multiplication, in the form:
|
||||
// AB
|
||||
// *CD
|
||||
// ------
|
||||
// BD // 0 shifts
|
||||
// + BC // 1 shift
|
||||
// + AD // 1 shift
|
||||
// + AC // 2 shifts
|
||||
//
|
||||
// We use vmull_p8 to compute all sub-multiplications, then
|
||||
// XOR the results together with appropriate shifts to get the final
|
||||
// result.
|
||||
|
||||
let a_mul = [
|
||||
(a >> 8) as u8,
|
||||
(a >> 8) as u8,
|
||||
(a & 0xff) as u8,
|
||||
(a & 0xff) as u8,
|
||||
(a >> 8) as u8,
|
||||
(a >> 8) as u8,
|
||||
(a & 0xff) as u8,
|
||||
(a & 0xff) as u8,
|
||||
];
|
||||
let b_mul = [
|
||||
(b1 >> 8) as u8,
|
||||
(b1 & 0xff) as u8,
|
||||
(b1 >> 8) as u8,
|
||||
(b1 & 0xff) as u8,
|
||||
(b2 >> 8) as u8,
|
||||
(b2 & 0xff) as u8,
|
||||
(b2 >> 8) as u8,
|
||||
(b2 & 0xff) as u8,
|
||||
];
|
||||
let out = unsafe {
|
||||
let a_p8 = arm::vld1_p8(&a_mul as *const u8);
|
||||
let b_p8 = arm::vld1_p8(&b_mul as *const u8);
|
||||
let ab_p16 = arm::vmull_p8(a_p8, b_p8);
|
||||
let mut out = [0u16; 8];
|
||||
arm::vst1q_p16(&mut out as *mut u16, ab_p16);
|
||||
out
|
||||
};
|
||||
let (b1out, b2out) = (
|
||||
((out[0] as u32) << 16)
|
||||
^ ((out[1] as u32) << 8)
|
||||
^ ((out[2] as u32) << 8)
|
||||
^ (out[3] as u32),
|
||||
((out[4] as u32) << 16)
|
||||
^ ((out[5] as u32) << 8)
|
||||
^ ((out[6] as u32) << 8)
|
||||
^ (out[7] as u32),
|
||||
);
|
||||
(
|
||||
super::reduce::poly_reduce(b1out),
|
||||
super::reduce::poly_reduce(b2out),
|
||||
)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
#[cfg(all(
|
||||
not(hax),
|
||||
any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")
|
||||
))]
|
||||
mod check_accelerated {
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
cpufeatures::new!(use_accelerated, "aes"); // `aes` implies PMULL
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
cpufeatures::new!(use_accelerated, "pclmulqdq");
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
pub(crate) static TOKEN: LazyLock<use_accelerated::InitToken> =
|
||||
LazyLock::new(use_accelerated::init);
|
||||
}
|
||||
|
||||
mod unaccelerated {
|
||||
#[hax_lib::fstar::options("--fuel 2")]
|
||||
#[hax_lib::ensures(|result| fstar!(r#"
|
||||
let open Spec.GF16 in
|
||||
to_bv result ==
|
||||
poly_mul (to_bv a) (to_bv b)"#))]
|
||||
const fn poly_mul(a: u16, b: u16) -> u32 {
|
||||
let mut acc: u32 = 0;
|
||||
let me = a as u32;
|
||||
// Long multiplication.
|
||||
let mut shift: u32 = 0;
|
||||
hax_lib::fstar!(
|
||||
r#"
|
||||
let open Spec.GF16 in
|
||||
assert_norm Spec.GF16.(poly_mul_i (to_bv a) (to_bv b) 0 == zero #32);
|
||||
zero_lemma #U32;
|
||||
up_cast_lemma #U16 #U32 a
|
||||
"#
|
||||
);
|
||||
while shift < 16 {
|
||||
hax_lib::loop_invariant!(fstar!(
|
||||
r#"
|
||||
let open Spec.GF16 in
|
||||
v shift <= 16 /\
|
||||
to_bv acc == poly_mul_i (to_bv a) (to_bv b) (v shift)
|
||||
"#
|
||||
));
|
||||
hax_lib::loop_decreases!(16 - shift);
|
||||
hax_lib::fstar!(
|
||||
r#"
|
||||
let open Spec.GF16 in
|
||||
shift_left_bit_select_lemma b shift;
|
||||
up_cast_shift_left_lemma a shift;
|
||||
lemma_add_lift #(16+v shift) #32 (poly_mul_x_k (to_bv a) (v shift)) (to_bv acc);
|
||||
xor_is_gf_add_lemma acc (me <<! shift)
|
||||
"#
|
||||
);
|
||||
if 0 != b & (1 << shift) {
|
||||
hax_lib::fstar!(
|
||||
r#"
|
||||
let open Spec.GF16 in
|
||||
assert ((to_bv b).[v shift] == true);
|
||||
assert (poly_mul_i (to_bv a) (to_bv b) (v shift + 1) ==
|
||||
gf_add (poly_mul_i (to_bv a) (to_bv b) (v shift))
|
||||
(poly_mul_x_k (to_bv a) (v shift)))
|
||||
"#
|
||||
);
|
||||
acc ^= me << shift;
|
||||
}
|
||||
shift += 1;
|
||||
}
|
||||
acc
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[hax_lib::ensures(|result| fstar!(r#"
|
||||
let open Spec.GF16 in
|
||||
let (r1, r2) = result in
|
||||
to_bv r1 == gf16_mul (to_bv a) (to_bv b1) /\
|
||||
to_bv r2 == gf16_mul (to_bv a) (to_bv b2)
|
||||
"#))]
|
||||
pub fn mul2(a: u16, b1: u16, b2: u16) -> (u16, u16) {
|
||||
(mul(a, b1), mul(a, b2))
|
||||
}
|
||||
|
||||
#[hax_lib::ensures(|result| fstar!(r#"
|
||||
let open Spec.GF16 in
|
||||
to_bv result ==
|
||||
gf16_mul (to_bv a) (to_bv b)"#))]
|
||||
pub const fn mul(a: u16, b: u16) -> u16 {
|
||||
super::reduce::poly_reduce(poly_mul(a, b))
|
||||
}
|
||||
}
|
||||
|
||||
mod reduce {
|
||||
use super::POLY;
|
||||
|
||||
/// This is a somewhat optimized reduction that's architecture-agnostic.
|
||||
/// When reducing, we look from the highest- to lowest-order bits in the
|
||||
/// range 31..16, and we clear each 1 we find by XOR-ing with the POLY,
|
||||
/// whose topmost bit (1<<16) is set. POLY is 17 bit long, meaning that
|
||||
/// we clear the topmost bit while affecting the 16 bits below it.
|
||||
/// Now, let's consider the topmost BYTE of a u32. Assuming that byte is
|
||||
/// some constant C, we will always perform the same set of (up to) 8
|
||||
/// XORs on the 2 bytes below C, and these can be precompted as a 16-byte
|
||||
/// XOR against those bytes. Also, once we're done processing C, we never
|
||||
/// use its data again, as we (correctly) assume that it will be zero'd out.
|
||||
/// So, given the (big-endian) u32 with bytes Cxyz, no matter what xyz are,
|
||||
/// we will always do:
|
||||
/// Cxyz ^ Cab0 -> 0Dwz
|
||||
/// where the bytes `ab` are dependent entirely on C. Assume again that
|
||||
/// we now have a new u32 with bytes 0Dwz. We will again XOR this based
|
||||
/// entirely on the bits in D, and it will look something like:
|
||||
/// 0Dwz ^ 0Def -> 00uv
|
||||
/// where again, `ef` is dependent on D. And, if D==C, then ef==ab.
|
||||
/// Note as well that since we never look at the high order bits again,
|
||||
/// the XORs by C and D are unnecessary. Rather than:
|
||||
/// Cxyz ^ Cab0 -> 0Dwz
|
||||
/// we can do:
|
||||
/// Cxyz ^ 0ab0 -> CDwz
|
||||
/// then:
|
||||
/// CDwz ^ 00ef -> CDuv
|
||||
/// as we're just going to return the lowest 16 bits to the caller.
|
||||
/// Given that we're doing this byte-by-byte and there's only 256 total
|
||||
/// potential bytes, we precompute all XORs into the REDUCE_BYTES
|
||||
/// buffer. In the above example:
|
||||
/// REDUCE_BYTES[C] -> ab
|
||||
/// REDUCE_BYTES[D] -> ef
|
||||
/// Since we're mapping every byte to a u16, we take up 512B of space
|
||||
/// to do this, and our reduction is just a couple of pipelined shifts/XORs.
|
||||
#[hax_lib::fstar::verification_status(panic_free)]
|
||||
#[hax_lib::ensures(|result| fstar!(r#"
|
||||
Spec.GF16.(to_bv result == poly_reduce #gf16 (to_bv v))
|
||||
"#))]
|
||||
pub const fn poly_reduce(v: u32) -> u16 {
|
||||
let mut v = v;
|
||||
let i1 = (v >> 24) as usize;
|
||||
v ^= (REDUCE_BYTES[i1] as u32) << 8;
|
||||
let shifted_v = (v >> 16) as usize;
|
||||
let i2 = shifted_v & 0xFF;
|
||||
hax_lib::fstar!("logand_lemma $shifted_v (mk_usize 255)");
|
||||
v ^= REDUCE_BYTES[i2] as u32;
|
||||
v as u16
|
||||
}
|
||||
|
||||
/// Compute the u16 reduction associated with u8 `a`. See the comment
|
||||
/// in poly_reduce for more details.
|
||||
const fn reduce_from_byte(mut a: u8) -> u32 {
|
||||
let mut out = 0u32;
|
||||
let mut i: u32 = 8;
|
||||
while i > 0 {
|
||||
hax_lib::loop_invariant!(i <= 8);
|
||||
hax_lib::loop_decreases!(i);
|
||||
i -= 1;
|
||||
if (1 << i) & a != 0 {
|
||||
out ^= POLY << i;
|
||||
a ^= ((POLY << i) >> 16) as u8;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Compute the u16 reductions for all bytes. See the comment in
|
||||
/// poly_reduce for more details.
|
||||
const fn reduce_bytes() -> [u16; 256] {
|
||||
let mut out = [0u16; 256];
|
||||
let mut i = 0;
|
||||
while i < 256 {
|
||||
hax_lib::loop_invariant!(hax_lib::prop::constructors::and(
|
||||
(i <= 256).into(),
|
||||
hax_lib::forall(|j: usize| hax_lib::implies(
|
||||
j < i,
|
||||
out[j] == (reduce_from_byte(j as u8) as u16)
|
||||
))
|
||||
));
|
||||
hax_lib::loop_decreases!(256 - i);
|
||||
out[i] = reduce_from_byte(i as u8) as u16;
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
const REDUCE_BYTES: [u16; 256] = reduce_bytes();
|
||||
}
|
||||
|
||||
impl GF16 {
|
||||
pub const ZERO: Self = Self { value: 0 };
|
||||
pub const ONE: Self = Self { value: 1 };
|
||||
|
||||
pub fn new(value: u16) -> Self {
|
||||
Self { value }
|
||||
}
|
||||
|
||||
fn div_impl(&self, other: &Self) -> Self {
|
||||
// Within GF(p^n), inv(a) == a^(p^n-2). We're GF(2^16) == GF(65536),
|
||||
// so we can compute GF(65534).
|
||||
let mut square = *other * *other;
|
||||
let mut out = *self;
|
||||
for _i in 1..16 {
|
||||
(square.value, out.value) = mul2_u16(square.value, square.value, out.value);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub const fn const_mul(&self, other: &Self) -> Self {
|
||||
Self {
|
||||
value: unaccelerated::mul(self.value, other.value),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn const_sub(&self, other: &Self) -> Self {
|
||||
Self {
|
||||
value: self.value ^ other.value,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn const_div(&self, other: &Self) -> Self {
|
||||
// Within GF(p^n), inv(a) == a^(p^n-2). We're GF(2^16) == GF(65536),
|
||||
// so we can compute GF(65534).
|
||||
let mut square = *other;
|
||||
let mut out = *self;
|
||||
{
|
||||
// const for loop
|
||||
let mut i: usize = 1;
|
||||
while i < 16 {
|
||||
hax_lib::loop_invariant!(i <= 16);
|
||||
hax_lib::loop_decreases!(16 - i);
|
||||
square = square.const_mul(&square);
|
||||
out = out.const_mul(&square);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use galois_field_2pm::GaloisField;
|
||||
use galois_field_2pm::gf2::GFu16;
|
||||
use rand::RngCore;
|
||||
|
||||
// https://web.eecs.utk.edu/~jplank/plank/papers/CS-07-593/primitive-polynomial-table.txt
|
||||
type ExternalGF16 = GFu16<{ POLY as u128 }>;
|
||||
|
||||
#[test]
|
||||
fn add() {
|
||||
let mut rng = rand::rng();
|
||||
for _i in 0..100 {
|
||||
let x = rng.next_u32() as u16;
|
||||
let y = rng.next_u32() as u16;
|
||||
assert_eq!(
|
||||
(GF16 { value: x } + GF16 { value: y }).value,
|
||||
(ExternalGF16::new(x) + ExternalGF16::new(y)).value
|
||||
);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn mul() {
|
||||
let mut rng = rand::rng();
|
||||
for _i in 0..100 {
|
||||
let x = rng.next_u32() as u16;
|
||||
let y = rng.next_u32() as u16;
|
||||
let a = (GF16 { value: x } * GF16 { value: y }).value;
|
||||
let b = (ExternalGF16::new(x) * ExternalGF16::new(y)).value;
|
||||
println!("{x:04x} * {y:04x} = {b:04x}");
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div() {
|
||||
let mut rng = rand::rng();
|
||||
for _i in 0..100 {
|
||||
let x = rng.next_u32() as u16;
|
||||
let y = rng.next_u32() as u16;
|
||||
if y == 0 {
|
||||
continue;
|
||||
}
|
||||
assert_eq!(
|
||||
(GF16 { value: x } / GF16 { value: y }).value,
|
||||
(ExternalGF16::new(x) / ExternalGF16::new(y)).value
|
||||
);
|
||||
}
|
||||
}
|
||||
#[cfg(all(target_arch = "x86_64", target_feature = "sse2",))]
|
||||
#[test]
|
||||
fn x86_barrett_reduction() {
|
||||
// Barrett reduction. Not noticeably faster than noarch::poly_reduce,
|
||||
// but left here for posterity.
|
||||
|
||||
use super::POLY;
|
||||
const POLY_128: [u32; 4] = [POLY, 0, 0, 0]; // Little-endian
|
||||
const POLY_M128I: core::arch::x86_64::__m128i = unsafe { core::mem::transmute(POLY_128) };
|
||||
const POLY_BARRETT_REDUCTION: u32 = 0x1111a; // 2^32 / POLY with carryless division
|
||||
const POLYB_128: [u32; 4] = [POLY_BARRETT_REDUCTION, 0, 0, 0]; // Little-endian
|
||||
const POLYB_M128I: core::arch::x86_64::__m128i = unsafe { core::mem::transmute(POLYB_128) };
|
||||
let (a, b) = (0xf5u16, 0x93u16);
|
||||
|
||||
// initial multiplication
|
||||
let unreduced_product = unsafe {
|
||||
let a = core::arch::x86_64::_mm_set_epi64x(0, a as i64);
|
||||
let b = core::arch::x86_64::_mm_set_epi64x(0, b as i64);
|
||||
core::arch::x86_64::_mm_clmulepi64_si128(a, b, 0)
|
||||
};
|
||||
let result = unsafe {
|
||||
// We perform a Barrett reduction with the precomputed POLYB=2^32/POLY,
|
||||
// manually computed via XOR-based long division.
|
||||
let quotient =
|
||||
core::arch::x86_64::_mm_clmulepi64_si128(unreduced_product, POLYB_M128I, 0);
|
||||
// We need to shift the quotient down 32 bits, so we'll do a register
|
||||
// shuffle. We now the highest 32 bits of the 128b register are zero,
|
||||
// so we'll use those for the top 3 32-bit portions of the register.
|
||||
// So, given a 128-bit register with u32 values [0, a, b, c],
|
||||
// we end up with a register with [0, 0, 0, b].
|
||||
let quotient_shifted = core::arch::x86_64::_mm_shuffle_epi32(quotient, 0xf9);
|
||||
// Now that we have the quotient q=floor(a*b/POLY), we subtract
|
||||
// POLY*q from a*b to get the remainder:
|
||||
let subtrahend =
|
||||
core::arch::x86_64::_mm_clmulepi64_si128(POLY_M128I, quotient_shifted, 0);
|
||||
// Of course, our difference is computed using XOR
|
||||
core::arch::x86_64::_mm_cvtsi128_si64(core::arch::x86_64::_mm_xor_si128(
|
||||
unreduced_product,
|
||||
subtrahend,
|
||||
)) as u16
|
||||
};
|
||||
assert_eq!(
|
||||
result,
|
||||
super::reduce::poly_reduce(unsafe {
|
||||
core::arch::x86_64::_mm_cvtsi128_si64(unreduced_product)
|
||||
} as u32)
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,91 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
#![cfg(test)]
|
||||
use super::{Chunk, Decoder, Encoder};
|
||||
|
||||
pub struct RoundRobinEncoder {
|
||||
data: Vec<u8>,
|
||||
next_idx: u16,
|
||||
}
|
||||
|
||||
impl RoundRobinEncoder {
|
||||
fn num_chunks(&self) -> usize {
|
||||
self.data.len() / 32 + if self.data.len() % 32 != 0 { 1 } else { 0 }
|
||||
}
|
||||
|
||||
fn chunk_at(&self, idx: u16) -> Chunk {
|
||||
let index = (idx as usize) % self.num_chunks();
|
||||
let lb = index * 32usize;
|
||||
let ub = lb + 32usize;
|
||||
|
||||
// Prove the unwrap is safe
|
||||
Chunk {
|
||||
index: idx,
|
||||
data: self.data.as_slice()[lb..ub].try_into().unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder for RoundRobinEncoder {
|
||||
fn encode_bytes(msg: &[u8]) -> Result<Self, super::EncodingError> {
|
||||
Ok(Self {
|
||||
data: msg.to_vec(),
|
||||
next_idx: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn next_chunk(&mut self) -> Chunk {
|
||||
let index = self.next_idx;
|
||||
self.next_idx += 1;
|
||||
self.chunk_at(index)
|
||||
}
|
||||
}
|
||||
|
||||
type ChunkData = [u8; 32];
|
||||
pub struct RoundRobinDecoder {
|
||||
chunks: Vec<Option<ChunkData>>,
|
||||
is_complete: bool,
|
||||
}
|
||||
|
||||
impl RoundRobinDecoder {
|
||||
fn can_reconstruct(&self) -> bool {
|
||||
self.chunks.iter().all(|d| d.is_some())
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for RoundRobinDecoder {
|
||||
fn new(len_bytes: usize) -> Result<Self, super::EncodingError> {
|
||||
let len_chunks = (len_bytes / 32) + if len_bytes % 32 != 0 { 1 } else { 0 };
|
||||
let chunks = vec![None; len_chunks];
|
||||
Ok(Self {
|
||||
chunks,
|
||||
is_complete: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn add_chunk(&mut self, chunk: &Chunk) {
|
||||
let idx = (chunk.index as usize) % self.chunks.len();
|
||||
if let Some(data) = self.chunks[idx] {
|
||||
assert_eq!(data, chunk.data);
|
||||
} else {
|
||||
self.chunks[idx] = Some(chunk.data);
|
||||
}
|
||||
}
|
||||
|
||||
fn decoded_message(&self) -> Option<Vec<u8>> {
|
||||
if self.is_complete {
|
||||
return None;
|
||||
}
|
||||
if self.can_reconstruct() {
|
||||
let msg: Vec<u8> = self
|
||||
.chunks
|
||||
.iter()
|
||||
.map(|data| data.unwrap())
|
||||
.flat_map(|d| d.into_iter())
|
||||
.collect();
|
||||
Some(msg)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
use crate::Secret;
|
||||
use libcrux_ml_kem::mlkem768::incremental;
|
||||
use rand::{CryptoRng, Rng};
|
||||
|
||||
pub const CIPHERTEXT1_SIZE: usize = incremental::Ciphertext1::len();
|
||||
pub type Ciphertext1 = Vec<u8>;
|
||||
pub type EncapsulationState = Vec<u8>;
|
||||
pub const CIPHERTEXT2_SIZE: usize = incremental::Ciphertext2::len();
|
||||
pub type Ciphertext2 = Vec<u8>;
|
||||
pub const HEADER_SIZE: usize = incremental::pk1_len();
|
||||
pub type Header = Vec<u8>;
|
||||
pub const ENCAPSULATION_KEY_SIZE: usize = incremental::pk2_len();
|
||||
pub type EncapsulationKey = Vec<u8>;
|
||||
pub type DecapsulationKey = Vec<u8>;
|
||||
|
||||
// pub const ENCAPSULATION_STATE_SIZE: usize = incremental::encaps_state_len();
|
||||
// pub const DECAPSULATION_KEY_SIZE: usize = incremental::key_pair_compressed_len();
|
||||
|
||||
pub struct Keys {
|
||||
pub ek: EncapsulationKey,
|
||||
pub dk: DecapsulationKey,
|
||||
pub hdr: Header,
|
||||
}
|
||||
|
||||
pub fn ek_matches_header(ek: &EncapsulationKey, hdr: &Header) -> bool {
|
||||
incremental::validate_pk_bytes(hdr, ek).is_ok()
|
||||
}
|
||||
|
||||
/// Generate a new keypair and associated header.
|
||||
#[hax_lib::ensures(|result| result.hdr.len() == HEADER_SIZE && result.ek.len() == ENCAPSULATION_KEY_SIZE && result.dk.len() == 2400)]
|
||||
pub fn generate<R: Rng + CryptoRng>(rng: &mut R) -> Keys {
|
||||
let mut randomness = [0u8; libcrux_ml_kem::KEY_GENERATION_SEED_SIZE];
|
||||
rng.fill_bytes(&mut randomness);
|
||||
let k = incremental::KeyPairCompressedBytes::from_seed(randomness);
|
||||
Keys {
|
||||
hdr: k.pk1().to_vec(),
|
||||
ek: k.pk2().to_vec(),
|
||||
dk: k.sk().to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsulate with header to get initial ciphertext.
|
||||
#[hax_lib::requires(hdr.len() == 64)]
|
||||
#[hax_lib::ensures(|(ct1,es,ss)| ct1.len() == 960 && es.len() == 2080 && ss.len() == 32)]
|
||||
pub fn encaps1<R: Rng + CryptoRng>(
|
||||
hdr: &Header,
|
||||
rng: &mut R,
|
||||
) -> (Ciphertext1, EncapsulationState, Secret) {
|
||||
let mut randomness = [0u8; libcrux_ml_kem::SHARED_SECRET_SIZE];
|
||||
rng.fill_bytes(&mut randomness);
|
||||
let mut state = vec![0u8; incremental::encaps_state_len()];
|
||||
let mut ss = vec![0u8; libcrux_ml_kem::SHARED_SECRET_SIZE];
|
||||
let ct1 = incremental::encapsulate1(hdr.as_slice(), randomness, &mut state, &mut ss);
|
||||
hax_lib::assume!(ct1.is_ok());
|
||||
hax_lib::assume!(state.len() == 2080 && ss.len() == 32);
|
||||
(
|
||||
ct1.expect("should only fail based on sizes, all sizes should be correct")
|
||||
.value
|
||||
.to_vec(),
|
||||
state,
|
||||
ss,
|
||||
)
|
||||
}
|
||||
|
||||
/// Encapsulate with header and EK.
|
||||
#[hax_lib::requires(es.len() == 2080 && ek.len() == 1152)]
|
||||
#[hax_lib::ensures(|result| result.len() == 128)]
|
||||
pub fn encaps2(ek: &EncapsulationKey, es: &EncapsulationState) -> Ciphertext2 {
|
||||
let maybe_fix = potentially_fix_state_incorrectly_encoded_by_libcrux_issue_1275(es);
|
||||
let es = maybe_fix.as_ref().unwrap_or(es);
|
||||
let ct2 = incremental::encapsulate2(
|
||||
es.as_slice().try_into().expect("size should be correct"),
|
||||
ek.as_slice().try_into().expect("size should be correct"),
|
||||
);
|
||||
ct2.value.to_vec()
|
||||
}
|
||||
|
||||
/// Due to https://github.com/cryspen/libcrux/issues/1275, state may
|
||||
/// contain incorrect endian-ness. We need to fix this locally before
|
||||
/// using it. Luckily, this is doable by checking that the values in
|
||||
/// error2 are in the range [-2, 2].
|
||||
#[hax_lib::requires(es.len() == 2080)]
|
||||
#[hax_lib::ensures(|result| if let Some(es) = result {
|
||||
es.len() == 2080
|
||||
} else {
|
||||
true
|
||||
})]
|
||||
#[hax_lib::opaque]
|
||||
fn potentially_fix_state_incorrectly_encoded_by_libcrux_issue_1275(
|
||||
es: &EncapsulationState,
|
||||
) -> Option<EncapsulationState> {
|
||||
assert_eq!(es.len(), 2080);
|
||||
// Look at each value within the error2 portion of EncapsState.
|
||||
//
|
||||
// The last 32 bytes are a raw random vector, thus they don't have endianness
|
||||
// and we shouldn't flip them. All other bytes are encoded i16s.
|
||||
// This is the libcrux-specific encoding of the following struct, where all
|
||||
// PolynomialRingElements are encoded as described.
|
||||
//
|
||||
// pub struct EncapsState<const K: usize, Vector: Operations> {
|
||||
// pub(super) r_as_ntt: [PolynomialRingElement<Vector>; K],
|
||||
// pub(super) error2: PolynomialRingElement<Vector>,
|
||||
// pub(super) randomness: [u8; 32],
|
||||
// }
|
||||
//
|
||||
// To determine whether it's valid or not, we look at the encoding of `error2`.
|
||||
// All error2 values should be in the range [-2, 2].
|
||||
// This is 𝜂2 from https://nvlpubs.nist.gov/nistpubs/fips/nist.fips.203.pdf
|
||||
// page 39 table 2, generated as 𝑒2 on page 30 algorithm 14 line 17.
|
||||
const NEG1_I16: i16 = 0xFFFFu16 as i16;
|
||||
const NEG2_I16_GOOD: i16 = 0xFFFEu16 as i16;
|
||||
const NEG2_I16_BAD: i16 = 0xFEFFu16 as i16;
|
||||
|
||||
for c in es[1536..2080 - 32].chunks(2) {
|
||||
match i16::from_le_bytes(c.try_into().expect("chunk should be size 2")) {
|
||||
0x0000i16 | NEG1_I16 => {} // These have the same encoding for i16 in little/big-endian
|
||||
0x0001i16 | 0x0002i16 | NEG2_I16_GOOD => {
|
||||
return None;
|
||||
}
|
||||
0x0100i16 | 0x0200i16 | NEG2_I16_BAD => {
|
||||
// If they aren't in the expected range, we probably have a state with bad endian-ness. So flip it.
|
||||
#[cfg(not(hax))]
|
||||
log::info!("spqr fixing bad encapsulate1 stored state endianness");
|
||||
return Some(flip_endianness_of_encapsulation_state(es));
|
||||
}
|
||||
_ => {
|
||||
// We're in a weird state, so just use what we had initially.
|
||||
#[cfg(not(hax))]
|
||||
log::warn!("spqr unable to fix encapsulate1 stored state endianness");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[hax_lib::requires(es.len()%2 == 0 && es.len() == 2080)]
|
||||
#[hax_lib::ensures(|result| result.len() == es.len())]
|
||||
#[hax_lib::opaque]
|
||||
fn flip_endianness_of_encapsulation_state(es: &EncapsulationState) -> EncapsulationState {
|
||||
assert!(es.len() % 2 == 0);
|
||||
assert!(es.len() > 32);
|
||||
let mut fixed_es = es.clone();
|
||||
for i in (0..fixed_es.len() - 32).step_by(2) {
|
||||
(fixed_es[i], fixed_es[i + 1]) = (fixed_es[i + 1], fixed_es[i])
|
||||
}
|
||||
fixed_es
|
||||
}
|
||||
|
||||
/// Decapsulate ciphertext to get shared secret.
|
||||
#[hax_lib::requires(ct1.len() == 960 && ct2.len() == 128 && dk.len() == 2400)]
|
||||
#[hax_lib::ensures(|result| result.len() == 32)]
|
||||
pub fn decaps(dk: &DecapsulationKey, ct1: &Ciphertext1, ct2: &Ciphertext2) -> Secret {
|
||||
let ct1 = incremental::Ciphertext1 {
|
||||
value: ct1.as_slice().try_into().expect("size should be correct"),
|
||||
};
|
||||
let ct2 = incremental::Ciphertext2 {
|
||||
value: ct2.as_slice().try_into().expect("size should be correct"),
|
||||
};
|
||||
incremental::decapsulate_compressed_key(
|
||||
dk.as_slice().try_into().expect("size should be correct"),
|
||||
&ct1,
|
||||
&ct2,
|
||||
)
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use rand::TryRngCore;
|
||||
use rand_core::OsRng;
|
||||
|
||||
#[test]
|
||||
fn incremental_mlkem768_round_trip() {
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
let keys = generate(&mut rng);
|
||||
let (ct1, es, ss1) = encaps1(&keys.hdr, &mut rng);
|
||||
let ct2 = encaps2(&keys.ek, &es);
|
||||
let ss2 = decaps(&keys.dk, &ct1, &ct2);
|
||||
assert_eq!(ss1, ss2);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
#[hax_lib::opaque]
|
||||
#[hax_lib::ensures(|res| res.len() >= okm_len)]
|
||||
pub fn hkdf_to_vec(salt: &[u8], ikm: &[u8], info: &[u8], okm_len: usize) -> Vec<u8> {
|
||||
let mut out = vec![0u8; okm_len];
|
||||
hkdf_to_slice(salt, ikm, info, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
#[hax_lib::opaque]
|
||||
#[hax_lib::ensures(|_| future(okm).len() == okm.len())]
|
||||
pub fn hkdf_to_slice(salt: &[u8], ikm: &[u8], info: &[u8], okm: &mut [u8]) {
|
||||
hkdf::Hkdf::<sha2::Sha256>::new(Some(salt), ikm)
|
||||
.expand(info, okm)
|
||||
.expect("all lengths should work for SHA256");
|
||||
}
|
||||
+1173
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pub mod pq_ratchet;
|
||||
@@ -0,0 +1,231 @@
|
||||
// Copyright 2025 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package signal.proto.pq_ratchet;
|
||||
|
||||
message PolynomialEncoder {
|
||||
uint32 idx = 1;
|
||||
|
||||
// We'd like to use a oneof here, but proto3 doesn't allow
|
||||
// a combination of `oneof` and `repeated`. So, we just
|
||||
// only set one of these values to non-empty:
|
||||
repeated bytes pts = 2;
|
||||
repeated bytes polys = 3;
|
||||
}
|
||||
|
||||
message PolynomialDecoder {
|
||||
uint32 pts_needed = 1;
|
||||
uint32 polys = 2;
|
||||
repeated bytes pts = 3;
|
||||
bool is_complete = 4;
|
||||
}
|
||||
|
||||
message PqRatchetState {
|
||||
message VersionNegotiation {
|
||||
bytes auth_key = 1;
|
||||
Direction direction = 2;
|
||||
Version min_version = 3;
|
||||
ChainParams chain_params = 4;
|
||||
}
|
||||
VersionNegotiation version_negotiation = 1;
|
||||
Chain chain = 2;
|
||||
|
||||
oneof inner {
|
||||
V1State v1 = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message Chunk {
|
||||
uint32 index = 1;
|
||||
bytes data = 2;
|
||||
}
|
||||
|
||||
message V1Msg {
|
||||
uint64 epoch = 1;
|
||||
uint32 index = 2;
|
||||
oneof inner_msg {
|
||||
// send_ek
|
||||
Chunk hdr = 3;
|
||||
Chunk ek = 4;
|
||||
Chunk ek_ct1_ack = 5;
|
||||
bool ct1_ack = 6;
|
||||
|
||||
// send_ct
|
||||
Chunk ct1 = 7;
|
||||
Chunk ct2 = 8;
|
||||
}
|
||||
}
|
||||
|
||||
message Authenticator {
|
||||
bytes root_key = 1;
|
||||
bytes mac_key = 2;
|
||||
}
|
||||
|
||||
message V1State {
|
||||
message Unchunked {
|
||||
//// send_ek ////
|
||||
message KeysUnsampled {
|
||||
uint64 epoch = 1;
|
||||
Authenticator auth = 2;
|
||||
}
|
||||
message HeaderSent {
|
||||
uint64 epoch = 1;
|
||||
Authenticator auth = 2;
|
||||
bytes ek = 3;
|
||||
bytes dk = 4;
|
||||
}
|
||||
message EkSent {
|
||||
uint64 epoch = 1;
|
||||
Authenticator auth = 2;
|
||||
bytes dk = 3;
|
||||
}
|
||||
message EkSentCt1Received {
|
||||
uint64 epoch = 1;
|
||||
Authenticator auth = 2;
|
||||
bytes dk = 3;
|
||||
bytes ct1 = 4;
|
||||
}
|
||||
|
||||
//// send_ct ////
|
||||
message NoHeaderReceived {
|
||||
uint64 epoch = 1;
|
||||
Authenticator auth = 2;
|
||||
}
|
||||
message HeaderReceived {
|
||||
uint64 epoch = 1;
|
||||
Authenticator auth = 2;
|
||||
bytes hdr = 3;
|
||||
}
|
||||
message EkReceived {
|
||||
uint64 epoch = 1;
|
||||
Authenticator auth = 2;
|
||||
bytes hdr = 3;
|
||||
bytes ek = 4;
|
||||
}
|
||||
message Ct1Sent {
|
||||
uint64 epoch = 1;
|
||||
Authenticator auth = 2;
|
||||
bytes hdr = 3;
|
||||
bytes es = 4;
|
||||
bytes ct1 = 5;
|
||||
}
|
||||
message Ct1SentEkReceived {
|
||||
uint64 epoch = 1;
|
||||
Authenticator auth = 2;
|
||||
bytes es = 3;
|
||||
bytes ek = 4;
|
||||
bytes ct1 = 5;
|
||||
}
|
||||
message Ct2Sent {
|
||||
uint64 epoch = 1;
|
||||
Authenticator auth = 2;
|
||||
}
|
||||
}
|
||||
message Chunked {
|
||||
//// send_ek ////
|
||||
message KeysUnsampled {
|
||||
Unchunked.KeysUnsampled uc = 1;
|
||||
}
|
||||
message KeysSampled {
|
||||
Unchunked.HeaderSent uc = 1;
|
||||
PolynomialEncoder sending_hdr = 2;
|
||||
}
|
||||
message HeaderSent {
|
||||
Unchunked.EkSent uc = 1;
|
||||
PolynomialEncoder sending_ek = 2;
|
||||
PolynomialDecoder receiving_ct1 = 3;
|
||||
}
|
||||
message Ct1Received {
|
||||
Unchunked.EkSentCt1Received uc = 1;
|
||||
PolynomialEncoder sending_ek = 2;
|
||||
}
|
||||
message EkSentCt1Received {
|
||||
Unchunked.EkSentCt1Received uc = 1;
|
||||
PolynomialDecoder receiving_ct2 = 3;
|
||||
}
|
||||
|
||||
//// send_ct ////
|
||||
message NoHeaderReceived {
|
||||
Unchunked.NoHeaderReceived uc = 1;
|
||||
PolynomialDecoder receiving_hdr = 2;
|
||||
}
|
||||
message HeaderReceived {
|
||||
Unchunked.HeaderReceived uc = 1;
|
||||
PolynomialDecoder receiving_ek = 2;
|
||||
}
|
||||
message Ct1Sampled {
|
||||
Unchunked.Ct1Sent uc = 1;
|
||||
PolynomialEncoder sending_ct1 = 2;
|
||||
PolynomialDecoder receiving_ek = 3;
|
||||
}
|
||||
message EkReceivedCt1Sampled {
|
||||
Unchunked.Ct1SentEkReceived uc = 1;
|
||||
PolynomialEncoder sending_ct1 = 2;
|
||||
}
|
||||
message Ct1Acknowledged {
|
||||
Unchunked.Ct1Sent uc = 1;
|
||||
PolynomialDecoder receiving_ek = 2;
|
||||
}
|
||||
message Ct2Sampled {
|
||||
Unchunked.Ct2Sent uc = 1;
|
||||
PolynomialEncoder sending_ct2 = 2;
|
||||
}
|
||||
}
|
||||
|
||||
oneof inner_state {
|
||||
//// send_ek ////
|
||||
Chunked.KeysUnsampled keys_unsampled = 1;
|
||||
Chunked.KeysSampled keys_sampled = 2;
|
||||
Chunked.HeaderSent header_sent = 3;
|
||||
Chunked.Ct1Received ct1_received = 4;
|
||||
Chunked.EkSentCt1Received ek_sent_ct1_received = 5;
|
||||
|
||||
//// send_ct ////
|
||||
Chunked.NoHeaderReceived no_header_received = 6;
|
||||
Chunked.HeaderReceived header_received = 7;
|
||||
Chunked.Ct1Sampled ct1_sampled = 8;
|
||||
Chunked.EkReceivedCt1Sampled ek_received_ct1_sampled = 9;
|
||||
Chunked.Ct1Acknowledged ct1_acknowledged = 10;
|
||||
Chunked.Ct2Sampled ct2_sampled = 11;
|
||||
}
|
||||
}
|
||||
|
||||
message Chain {
|
||||
message Epoch {
|
||||
message EpochDirection {
|
||||
uint32 ctr = 1;
|
||||
bytes next = 2;
|
||||
bytes prev = 3;
|
||||
}
|
||||
EpochDirection send = 1;
|
||||
EpochDirection recv = 2;
|
||||
}
|
||||
Direction direction = 1;
|
||||
uint64 current_epoch = 2;
|
||||
repeated Epoch links = 3;
|
||||
bytes next_root = 4;
|
||||
uint64 send_epoch = 5;
|
||||
ChainParams params = 6;
|
||||
}
|
||||
|
||||
enum Version {
|
||||
V_0 = 0; // disabled
|
||||
V_1 = 1;
|
||||
}
|
||||
|
||||
enum Direction {
|
||||
A_2_B = 0;
|
||||
B_2_A = 1;
|
||||
}
|
||||
|
||||
message ChainParams {
|
||||
// Disallow requesting a key that is more than MAX_JUMP ahead of `ctr`.
|
||||
// If zero, defaults to 25,000.
|
||||
uint32 max_jump = 1;
|
||||
// Keep around keys back to at least `ctr - MAX_OOO_KEYS`, in case an out-of-order
|
||||
// message comes in. Messages older than this that arrive out-of-order
|
||||
// will not be able to be decrypted and will return Error::KeyTrimmed.
|
||||
uint32 max_ooo_keys = 2;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
#![allow(clippy::derive_partial_eq_without_eq)]
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/signal.proto.pq_ratchet.rs"));
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
use crate::encoding;
|
||||
|
||||
#[derive(Debug, thiserror::Error, Copy, Clone, PartialEq)]
|
||||
pub enum Error {
|
||||
#[error("General deserialization error")]
|
||||
Deserialization,
|
||||
#[error("Error with encoder/decoder serialization")]
|
||||
EncodingDecoding,
|
||||
}
|
||||
|
||||
impl From<encoding::polynomial::PolynomialError> for Error {
|
||||
fn from(_e: encoding::polynomial::PolynomialError) -> Error {
|
||||
Error::EncodingDecoding
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
#![cfg(test)]
|
||||
pub(crate) mod basic_messaging_behavior;
|
||||
pub(crate) mod generic_dr;
|
||||
pub(crate) mod messaging_behavior;
|
||||
pub(crate) mod messaging_scka;
|
||||
pub(crate) mod onlineoffline;
|
||||
pub(crate) mod orchestrator;
|
||||
pub(crate) mod pingpong_messaging_behavior;
|
||||
pub(crate) mod scka;
|
||||
pub(crate) mod v1_impls;
|
||||
pub(crate) mod x25519_scka;
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
use rand::Rng;
|
||||
use rand_core::CryptoRng;
|
||||
|
||||
use super::messaging_behavior::{self, Agent, Command, MessagingBehavior};
|
||||
|
||||
pub struct BasicMessagingBehavior {
|
||||
p_a: f64,
|
||||
p_b: f64,
|
||||
receive_all_probability: f64,
|
||||
}
|
||||
|
||||
impl BasicMessagingBehavior {
|
||||
pub fn new(p_a: f64, p_b: f64, receive_all_probability: f64) -> Self {
|
||||
Self {
|
||||
p_a,
|
||||
p_b,
|
||||
receive_all_probability,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessagingBehavior for BasicMessagingBehavior {
|
||||
fn next_commands<R: CryptoRng>(&mut self, rng: &mut R) -> Vec<messaging_behavior::Command> {
|
||||
let mut cmds = Vec::new();
|
||||
|
||||
let a_sends = rng.random_bool(self.p_a);
|
||||
let b_sends = rng.random_bool(self.p_b);
|
||||
let do_receive = rng.random_bool(self.receive_all_probability);
|
||||
|
||||
if do_receive {
|
||||
cmds.push(Command::ReceiveAll(Agent::Alex));
|
||||
cmds.push(Command::ReceiveAll(Agent::Blake));
|
||||
}
|
||||
if a_sends {
|
||||
cmds.push(Command::Send(Agent::Alex));
|
||||
}
|
||||
if b_sends {
|
||||
cmds.push(Command::Send(Agent::Blake));
|
||||
}
|
||||
|
||||
cmds
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
use rand_core::CryptoRng;
|
||||
|
||||
use crate::{chain, EpochSecret, Error};
|
||||
|
||||
use super::scka::{Scka, SckaMessage};
|
||||
|
||||
pub struct DoubleRatchet<SCKA: Scka> {
|
||||
symratchet: chain::Chain,
|
||||
asymratchet: SCKA,
|
||||
}
|
||||
|
||||
pub struct Send<SCKA: Scka> {
|
||||
pub dr: DoubleRatchet<SCKA>,
|
||||
pub msg: SCKA::Message,
|
||||
index: u32,
|
||||
key: Option<[u8; 32]>,
|
||||
}
|
||||
|
||||
pub fn dr_send<SCKA: Scka, R: CryptoRng>(
|
||||
dr: DoubleRatchet<SCKA>,
|
||||
rng: &mut R,
|
||||
) -> Result<Send<SCKA>, Error> {
|
||||
let DoubleRatchet {
|
||||
asymratchet,
|
||||
mut symratchet,
|
||||
} = dr;
|
||||
|
||||
let (so, msg, asymratchet) = asymratchet.scka_send(rng)?;
|
||||
|
||||
if let Some((epoch, key)) = so.output_key {
|
||||
symratchet.add_epoch(EpochSecret {
|
||||
epoch,
|
||||
secret: key.to_vec(),
|
||||
});
|
||||
}
|
||||
let (index, msg_key) = symratchet.send_key(so.sending_epoch)?;
|
||||
|
||||
Ok(Send {
|
||||
dr: DoubleRatchet {
|
||||
asymratchet,
|
||||
symratchet,
|
||||
},
|
||||
msg,
|
||||
index,
|
||||
key: Some(msg_key.try_into().expect("msg_key is 32B")),
|
||||
})
|
||||
}
|
||||
|
||||
pub struct Recv<SCKA: Scka> {
|
||||
pub dr: DoubleRatchet<SCKA>,
|
||||
key: Option<[u8; 32]>,
|
||||
}
|
||||
|
||||
pub fn dr_recv<SCKA: Scka>(
|
||||
dr: DoubleRatchet<SCKA>,
|
||||
msg: &SCKA::Message,
|
||||
index: u32,
|
||||
) -> Result<Recv<SCKA>, Error> {
|
||||
let DoubleRatchet {
|
||||
asymratchet,
|
||||
mut symratchet,
|
||||
} = dr;
|
||||
let (ro, asymratchet) = asymratchet.scka_recv(msg)?;
|
||||
|
||||
if let Some((epoch, key)) = ro.output_key {
|
||||
symratchet.add_epoch(EpochSecret {
|
||||
epoch,
|
||||
secret: key.to_vec(),
|
||||
});
|
||||
}
|
||||
|
||||
let msg_key = symratchet.recv_key(msg.epoch(), index)?;
|
||||
|
||||
Ok(Recv {
|
||||
dr: DoubleRatchet {
|
||||
symratchet,
|
||||
asymratchet,
|
||||
},
|
||||
key: Some(msg_key.try_into().expect("msg_key is 32B")),
|
||||
})
|
||||
}
|
||||
|
||||
mod test {
|
||||
use rand::Rng;
|
||||
use rand::TryRngCore;
|
||||
use rand_core::OsRng;
|
||||
|
||||
use crate::{
|
||||
chain, initial_state, kdf, recv, send,
|
||||
test::{scka::Scka, x25519_scka},
|
||||
ChainParams, Direction, Error, Params, Secret, SerializedMessage, SerializedState, Version,
|
||||
};
|
||||
|
||||
use super::{dr_recv, dr_send, DoubleRatchet};
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn send_hybrid_message<SCKA: Scka>(
|
||||
pq_state: &SerializedState,
|
||||
ec_state: DoubleRatchet<SCKA>,
|
||||
) -> Result<
|
||||
(
|
||||
SerializedState,
|
||||
SerializedMessage,
|
||||
DoubleRatchet<SCKA>,
|
||||
SCKA::Message,
|
||||
u32,
|
||||
Secret,
|
||||
),
|
||||
Error,
|
||||
> {
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
let (pq_send, ec_send) = (send(pq_state, &mut rng)?, dr_send(ec_state, &mut rng)?);
|
||||
|
||||
let key = kdf::hkdf_to_vec(
|
||||
&[0u8; 32],
|
||||
&[pq_send.key.unwrap_or(vec![]), ec_send.key.unwrap().to_vec()].concat(),
|
||||
b"hybrid ratchet merge",
|
||||
32,
|
||||
);
|
||||
Ok((
|
||||
pq_send.state,
|
||||
pq_send.msg,
|
||||
ec_send.dr,
|
||||
ec_send.msg,
|
||||
ec_send.index,
|
||||
key,
|
||||
))
|
||||
}
|
||||
|
||||
fn receive_hybrid_message<SCKA: Scka>(
|
||||
pq_state: &SerializedState,
|
||||
pq_msg: &SerializedMessage,
|
||||
ec_state: DoubleRatchet<SCKA>,
|
||||
ec_msg: &SCKA::Message,
|
||||
ec_idx: u32,
|
||||
) -> Result<(SerializedState, DoubleRatchet<SCKA>, Secret), Error> {
|
||||
let (pq_recv, ec_recv) = (recv(pq_state, pq_msg)?, dr_recv(ec_state, ec_msg, ec_idx)?);
|
||||
|
||||
let key = kdf::hkdf_to_vec(
|
||||
&[0u8; 32],
|
||||
&[pq_recv.key.unwrap_or(vec![]), ec_recv.key.unwrap().to_vec()].concat(),
|
||||
b"hybrid ratchet merge",
|
||||
32,
|
||||
);
|
||||
Ok((pq_recv.state, ec_recv.dr, key))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hybrid_ratchet() -> Result<(), Error> {
|
||||
let alex_ec_ratchet = x25519_scka::states::States::init_a();
|
||||
let alex_ec_chain = chain::Chain::new(
|
||||
&[43u8; 32],
|
||||
Direction::A2B,
|
||||
ChainParams::default().into_pb(),
|
||||
)?;
|
||||
|
||||
let alex_ec_state = DoubleRatchet {
|
||||
asymratchet: alex_ec_ratchet,
|
||||
symratchet: alex_ec_chain,
|
||||
};
|
||||
|
||||
let blake_ec_ratchet = x25519_scka::states::States::init_b();
|
||||
let blake_ec_chain = chain::Chain::new(
|
||||
&[43u8; 32],
|
||||
Direction::B2A,
|
||||
ChainParams::default().into_pb(),
|
||||
)?;
|
||||
|
||||
let blake_ec_state = DoubleRatchet {
|
||||
asymratchet: blake_ec_ratchet,
|
||||
symratchet: blake_ec_chain,
|
||||
};
|
||||
|
||||
let version = Version::V1;
|
||||
|
||||
let alex_pq_state = initial_state(Params {
|
||||
version,
|
||||
min_version: version,
|
||||
direction: Direction::A2B,
|
||||
auth_key: &[41u8; 32],
|
||||
chain_params: ChainParams::default(),
|
||||
})?;
|
||||
let blake_pq_state = initial_state(Params {
|
||||
version,
|
||||
min_version: version,
|
||||
direction: Direction::B2A,
|
||||
auth_key: &[41u8; 32],
|
||||
chain_params: ChainParams::default(),
|
||||
})?;
|
||||
|
||||
// Now let's send some messages
|
||||
println!("alex send");
|
||||
let (alex_pq_state, pq_msg, alex_ec_state, ec_msg, ec_idx, alex_key) =
|
||||
send_hybrid_message(&alex_pq_state, alex_ec_state)?;
|
||||
println!("blake recv");
|
||||
let (blake_pq_state, blake_ec_state, blake_key) =
|
||||
receive_hybrid_message(&blake_pq_state, &pq_msg, blake_ec_state, &ec_msg, ec_idx)?;
|
||||
|
||||
assert_eq!(alex_key, blake_key);
|
||||
|
||||
println!("blake send");
|
||||
let (mut blake_pq_state, pq_msg, mut blake_ec_state, ec_msg, ec_idx, blake_key) =
|
||||
send_hybrid_message(&blake_pq_state, blake_ec_state)?;
|
||||
println!("alex recv");
|
||||
let (mut alex_pq_state, mut alex_ec_state, alex_key) =
|
||||
receive_hybrid_message(&alex_pq_state, &pq_msg, alex_ec_state, &ec_msg, ec_idx)?;
|
||||
|
||||
assert_eq!(alex_key, blake_key);
|
||||
|
||||
// now let's mix it up a little
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
for _ in 0..1000 {
|
||||
let a_send = rng.random_bool(0.5);
|
||||
let b_send = rng.random_bool(0.5);
|
||||
let a_recv = rng.random_bool(0.7);
|
||||
let b_recv = rng.random_bool(0.7);
|
||||
|
||||
if a_send {
|
||||
println!("alex send");
|
||||
let (pq_state, pq_msg, ec_state, ec_msg, ec_idx, alex_key) =
|
||||
send_hybrid_message(&alex_pq_state, alex_ec_state)?;
|
||||
(alex_pq_state, alex_ec_state) = (pq_state, ec_state);
|
||||
if b_recv {
|
||||
println!("blake recv");
|
||||
let (pq_state, ec_state, blake_key) = receive_hybrid_message(
|
||||
&blake_pq_state,
|
||||
&pq_msg,
|
||||
blake_ec_state,
|
||||
&ec_msg,
|
||||
ec_idx,
|
||||
)?;
|
||||
|
||||
(blake_pq_state, blake_ec_state) = (pq_state, ec_state);
|
||||
|
||||
assert_eq!(alex_key, blake_key);
|
||||
}
|
||||
}
|
||||
|
||||
if b_send {
|
||||
println!("blake send");
|
||||
let (pq_state, pq_msg, ec_state, ec_msg, ec_idx, blake_key) =
|
||||
send_hybrid_message(&blake_pq_state, blake_ec_state)?;
|
||||
(blake_pq_state, blake_ec_state) = (pq_state, ec_state);
|
||||
if a_recv {
|
||||
println!("alex recv");
|
||||
let (pq_state, ec_state, alex_key) = receive_hybrid_message(
|
||||
&alex_pq_state,
|
||||
&pq_msg,
|
||||
alex_ec_state,
|
||||
&ec_msg,
|
||||
ec_idx,
|
||||
)?;
|
||||
|
||||
(alex_pq_state, alex_ec_state) = (pq_state, ec_state);
|
||||
|
||||
assert_eq!(alex_key, blake_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
use rand_core::CryptoRng;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Agent {
|
||||
Alex,
|
||||
Blake,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Command {
|
||||
Send(Agent),
|
||||
#[allow(dead_code)]
|
||||
Receive(Agent),
|
||||
ReceiveAll(Agent),
|
||||
}
|
||||
pub trait MessagingBehavior {
|
||||
fn next_commands<R: CryptoRng>(&mut self, rng: &mut R) -> Vec<Command>;
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use rand_core::{CryptoRng, OsRng};
|
||||
|
||||
use crate::{
|
||||
test::scka::{Scka, SckaInitializer, SckaVulnerability},
|
||||
Epoch, Error, Secret,
|
||||
};
|
||||
|
||||
pub trait MessagingScka {
|
||||
type CkaOutput;
|
||||
type Message;
|
||||
|
||||
fn init_a<R: CryptoRng>(rng: &mut R) -> Result<Self, Error>
|
||||
where
|
||||
Self: Sized;
|
||||
fn init_b<R: CryptoRng>(rng: &mut R) -> Result<Self, Error>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn messaging_scka_send<R: CryptoRng>(
|
||||
&mut self,
|
||||
rng: &mut R,
|
||||
) -> Result<(Option<(Epoch, Self::CkaOutput)>, Self::Message), Error>
|
||||
where
|
||||
Self::CkaOutput: Sized;
|
||||
fn messaging_scka_recv(
|
||||
&mut self,
|
||||
msg: &Self::Message,
|
||||
rng: &mut OsRng,
|
||||
) -> Result<Option<(Epoch, Self::CkaOutput)>, Error>
|
||||
where
|
||||
Self::CkaOutput: Sized;
|
||||
}
|
||||
|
||||
pub trait MessagingCkaVulnerability {
|
||||
fn vulnerable_epochs(&self) -> Vec<Epoch>;
|
||||
fn last_emitted_epoch(&self) -> Epoch;
|
||||
}
|
||||
|
||||
pub struct GenericMessagingScka<SCKA: Scka> {
|
||||
scka: SCKA,
|
||||
send_outputs: BTreeMap<Epoch, Secret>,
|
||||
recv_outputs: BTreeMap<Epoch, Secret>,
|
||||
last_emitted_epoch: Epoch,
|
||||
}
|
||||
|
||||
impl<SCKA: Scka + SckaInitializer + Clone> MessagingScka for GenericMessagingScka<SCKA> {
|
||||
type CkaOutput = Secret;
|
||||
|
||||
type Message = SCKA::Message;
|
||||
|
||||
fn init_a<R: CryptoRng>(rng: &mut R) -> Result<Self, Error>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Ok(Self {
|
||||
scka: SCKA::init_a(rng)?,
|
||||
send_outputs: BTreeMap::new(),
|
||||
recv_outputs: BTreeMap::new(),
|
||||
last_emitted_epoch: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn init_b<R: CryptoRng>(rng: &mut R) -> Result<Self, Error>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Ok(Self {
|
||||
scka: SCKA::init_b(rng)?,
|
||||
send_outputs: BTreeMap::new(),
|
||||
recv_outputs: BTreeMap::new(),
|
||||
last_emitted_epoch: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn messaging_scka_send<R: CryptoRng>(
|
||||
&mut self,
|
||||
rng: &mut R,
|
||||
) -> Result<(Option<(Epoch, Self::CkaOutput)>, Self::Message), crate::Error>
|
||||
where
|
||||
Self::CkaOutput: Sized,
|
||||
{
|
||||
let (so, msg, state) = self.scka.clone().scka_send(rng)?;
|
||||
self.scka = state;
|
||||
|
||||
// self.last_emitted_epoch = so.sending_epoch;
|
||||
let earliest_send_output = if let Some((ep, _)) = self.send_outputs.first_key_value() {
|
||||
*ep
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if let Some((ep, k)) = so.output_key {
|
||||
self.send_outputs.insert(ep, k);
|
||||
}
|
||||
|
||||
let mut take_key = false;
|
||||
if let Some(entry) = self.recv_outputs.first_entry() {
|
||||
let ep = *entry.key();
|
||||
if ep <= so.sending_epoch
|
||||
&& (earliest_send_output == 0 || ep < earliest_send_output)
|
||||
&& (self.last_emitted_epoch == 0 || ep == self.last_emitted_epoch + 1)
|
||||
{
|
||||
take_key = true;
|
||||
}
|
||||
};
|
||||
let output_key = if take_key {
|
||||
let entry = self.recv_outputs.first_entry().unwrap();
|
||||
let ep = *entry.key();
|
||||
self.last_emitted_epoch = ep;
|
||||
// info!("messaging scka send outputs: {:?}", entry);
|
||||
Some((ep, entry.remove()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok((output_key, msg))
|
||||
}
|
||||
|
||||
fn messaging_scka_recv(
|
||||
&mut self,
|
||||
msg: &Self::Message,
|
||||
_rng: &mut OsRng,
|
||||
) -> Result<Option<(Epoch, Self::CkaOutput)>, Error>
|
||||
where
|
||||
Self::CkaOutput: Sized,
|
||||
{
|
||||
let (ro, state) = self.scka.clone().scka_recv(msg)?;
|
||||
self.scka = state;
|
||||
|
||||
// self.last_emitted_epoch = ro.receiving_epoch;
|
||||
let earliest_recv_output = if let Some((ep, _)) = self.recv_outputs.first_key_value() {
|
||||
*ep
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if let Some((ep, k)) = ro.output_key {
|
||||
self.recv_outputs.insert(ep, k);
|
||||
}
|
||||
|
||||
let mut take_key = false;
|
||||
if let Some(entry) = self.send_outputs.first_entry() {
|
||||
let ep = *entry.key();
|
||||
if ep <= ro.receiving_epoch
|
||||
&& (earliest_recv_output == 0 || ep < earliest_recv_output)
|
||||
&& (self.last_emitted_epoch == 0 || ep == self.last_emitted_epoch + 1)
|
||||
{
|
||||
take_key = true;
|
||||
}
|
||||
};
|
||||
|
||||
let output_key = if take_key {
|
||||
let entry = self.send_outputs.first_entry().unwrap();
|
||||
let ep = *entry.key();
|
||||
self.last_emitted_epoch = ep;
|
||||
// info!("messaging scka recv outputs: {:?}", entry);
|
||||
Some((ep, entry.remove()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(output_key)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl<SCKA: Scka + SckaVulnerability> MessagingCkaVulnerability for GenericMessagingScka<SCKA> {
|
||||
fn vulnerable_epochs(&self) -> Vec<Epoch> {
|
||||
let mut result = self.scka.vulnerable_epochs();
|
||||
for (ep, _) in self.send_outputs.iter() {
|
||||
result.push(*ep);
|
||||
}
|
||||
for (ep, _) in self.recv_outputs.iter() {
|
||||
result.push(*ep);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn last_emitted_epoch(&self) -> Epoch {
|
||||
self.last_emitted_epoch
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use rand::TryRngCore;
|
||||
use rand_core::OsRng;
|
||||
|
||||
use crate::{
|
||||
test::{
|
||||
messaging_scka::GenericMessagingScka, onlineoffline::OnlineOfflineMessagingBehavior,
|
||||
orchestrator, pingpong_messaging_behavior::PingPongMessagingBehavior,
|
||||
},
|
||||
v1states::States,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn random_balanced() {
|
||||
type Scka = States;
|
||||
type Cka = GenericMessagingScka<Scka>;
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
orchestrator::random_balanced::<Cka, _>(&mut rng).expect("should run");
|
||||
}
|
||||
#[test]
|
||||
fn random_balanced_out_of_order() {
|
||||
type Scka = States;
|
||||
type Cka = GenericMessagingScka<Scka>;
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
orchestrator::random_balanced_out_of_order::<Cka, _>(&mut rng).expect("should run");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_balanced_healing() {
|
||||
type Scka = States;
|
||||
type Cka = GenericMessagingScka<Scka>;
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
orchestrator::random_healing_test::<Cka, _>(0.5, &mut rng).expect("should run");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pingpong_healing() {
|
||||
type Scka = States;
|
||||
type Cka = GenericMessagingScka<Scka>;
|
||||
let mut mp = PingPongMessagingBehavior::new(50, 0);
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
let hist =
|
||||
orchestrator::controlled_messaging_healing_test::<Cka, PingPongMessagingBehavior, _>(
|
||||
&mut mp, 10000, &mut rng,
|
||||
)
|
||||
.expect("should run");
|
||||
orchestrator::print_histogram(&hist);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn onlineoffline_healing() {
|
||||
type Scka = States;
|
||||
type Cka = GenericMessagingScka<Scka>;
|
||||
let mut mp = OnlineOfflineMessagingBehavior::new([0.04, 0.04], [0.05, 0.05]);
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
let hist = orchestrator::controlled_messaging_healing_test::<
|
||||
Cka,
|
||||
OnlineOfflineMessagingBehavior,
|
||||
_,
|
||||
>(&mut mp, 100000, &mut rng)
|
||||
.expect("should run");
|
||||
orchestrator::print_histogram(&hist);
|
||||
orchestrator::print_healing_stats(&orchestrator::stats_from_histogram(&hist)[0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
use rand::Rng;
|
||||
use rand::TryRngCore;
|
||||
use rand_core::OsRng;
|
||||
|
||||
use super::messaging_behavior::{Agent, Command, MessagingBehavior};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum State {
|
||||
Online,
|
||||
Offline,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn transition(self, prob_come_online: f64, prob_go_offline: f64) -> Self {
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
match self {
|
||||
State::Online => {
|
||||
if rng.random_bool(prob_go_offline) {
|
||||
State::Offline
|
||||
} else {
|
||||
State::Online
|
||||
}
|
||||
}
|
||||
State::Offline => {
|
||||
if rng.random_bool(prob_come_online) {
|
||||
State::Online
|
||||
} else {
|
||||
State::Offline
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_online(&self) -> bool {
|
||||
match self {
|
||||
State::Online => true,
|
||||
State::Offline => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ALEX: usize = 0;
|
||||
const BLAKE: usize = 1;
|
||||
|
||||
pub struct OnlineOfflineMessagingBehavior {
|
||||
prob_go_offline: [f64; 2],
|
||||
prob_come_online: [f64; 2],
|
||||
agents: [Agent; 2],
|
||||
states: [State; 2],
|
||||
}
|
||||
|
||||
impl OnlineOfflineMessagingBehavior {
|
||||
pub fn new(prob_go_offline: [f64; 2], prob_come_online: [f64; 2]) -> Self {
|
||||
Self {
|
||||
prob_go_offline,
|
||||
prob_come_online,
|
||||
agents: [Agent::Alex, Agent::Blake],
|
||||
states: [State::Online, State::Online],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessagingBehavior for OnlineOfflineMessagingBehavior {
|
||||
fn next_commands<R: rand_core::CryptoRng>(
|
||||
&mut self,
|
||||
rng: &mut R,
|
||||
) -> Vec<super::messaging_behavior::Command> {
|
||||
let agent = if rng.random_bool(0.5) { ALEX } else { BLAKE };
|
||||
|
||||
self.states[agent] = self.states[agent]
|
||||
.clone()
|
||||
.transition(self.prob_come_online[agent], self.prob_go_offline[agent]);
|
||||
let mut cmds = Vec::new();
|
||||
|
||||
if self.states[agent].is_online() {
|
||||
cmds.push(Command::ReceiveAll(self.agents[agent]));
|
||||
if rng.random_bool(0.5) {
|
||||
cmds.push(Command::Send(self.agents[agent]));
|
||||
}
|
||||
}
|
||||
|
||||
cmds
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,995 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
use std::collections::{BTreeMap, HashMap, VecDeque};
|
||||
use std::fmt::Debug;
|
||||
|
||||
use rand::prelude::*;
|
||||
use rand::rngs::OsRng;
|
||||
use rand::rngs::StdRng;
|
||||
use rand_core::CryptoRng;
|
||||
|
||||
use crate::test::messaging_behavior::{Agent, Command};
|
||||
use crate::test::messaging_scka::{MessagingCkaVulnerability, MessagingScka};
|
||||
use crate::Epoch;
|
||||
use crate::Error;
|
||||
|
||||
use super::basic_messaging_behavior::BasicMessagingBehavior;
|
||||
use super::messaging_behavior::MessagingBehavior;
|
||||
|
||||
pub struct OrchestratorBase<K>
|
||||
where
|
||||
K: MessagingScka,
|
||||
<K as MessagingScka>::CkaOutput: PartialEq + Debug,
|
||||
{
|
||||
a2b_msg_queue: VecDeque<K::Message>,
|
||||
b2a_msg_queue: VecDeque<K::Message>,
|
||||
key_history_a: Vec<K::CkaOutput>,
|
||||
key_history_b: Vec<K::CkaOutput>,
|
||||
pub alex: K,
|
||||
pub blake: K,
|
||||
pub last_emitted_epoch_a: Epoch,
|
||||
pub last_emitted_epoch_b: Epoch,
|
||||
pub a_sent: usize,
|
||||
pub b_sent: usize,
|
||||
pub a_rcvd: usize,
|
||||
pub b_rcvd: usize,
|
||||
}
|
||||
|
||||
impl<K> OrchestratorBase<K>
|
||||
where
|
||||
K: MessagingScka + MessagingCkaVulnerability,
|
||||
<K as MessagingScka>::CkaOutput: PartialEq + Debug,
|
||||
{
|
||||
pub fn new<R: CryptoRng>(rng: &mut R) -> Result<Self, Error> {
|
||||
let alex = K::init_a(rng)?;
|
||||
let blake = K::init_b(rng)?;
|
||||
|
||||
Ok(Self {
|
||||
a2b_msg_queue: VecDeque::new(),
|
||||
b2a_msg_queue: VecDeque::new(),
|
||||
key_history_a: Vec::new(),
|
||||
key_history_b: Vec::new(),
|
||||
alex,
|
||||
blake,
|
||||
last_emitted_epoch_a: 0,
|
||||
last_emitted_epoch_b: 0,
|
||||
a_sent: 0,
|
||||
b_sent: 0,
|
||||
a_rcvd: 0,
|
||||
b_rcvd: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn key_history_is_consistent(&self) {
|
||||
for (ka, kb) in self.key_history_a.iter().zip(self.key_history_b.iter()) {
|
||||
assert_eq!(ka, kb);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn data_for_agent(
|
||||
&mut self,
|
||||
is_alex: bool,
|
||||
) -> (
|
||||
&mut K,
|
||||
&mut VecDeque<K::Message>,
|
||||
&mut VecDeque<K::Message>,
|
||||
&mut Vec<K::CkaOutput>,
|
||||
) {
|
||||
if is_alex {
|
||||
(
|
||||
&mut self.alex,
|
||||
&mut self.b2a_msg_queue,
|
||||
&mut self.a2b_msg_queue,
|
||||
&mut self.key_history_a,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
&mut self.blake,
|
||||
&mut self.a2b_msg_queue,
|
||||
&mut self.b2a_msg_queue,
|
||||
&mut self.key_history_b,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send<R: CryptoRng>(&mut self, is_alex: bool, rng: &mut R) -> Result<bool, Error> {
|
||||
let mut emitted_key = false;
|
||||
let mut emitted_ep: Option<Epoch> = None;
|
||||
{
|
||||
{
|
||||
let (agent, _incoming_msg_queue, outgoing_msg_queue, key_history) =
|
||||
self.data_for_agent(is_alex);
|
||||
let (out, msg) = agent.messaging_scka_send(rng)?;
|
||||
if let Some((_ep, key)) = out {
|
||||
key_history.push(key);
|
||||
emitted_ep = Some(agent.last_emitted_epoch());
|
||||
emitted_key = true;
|
||||
}
|
||||
outgoing_msg_queue.push_back(msg);
|
||||
}
|
||||
if is_alex {
|
||||
self.a_sent += 1;
|
||||
} else {
|
||||
self.b_sent += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ep) = emitted_ep {
|
||||
if is_alex {
|
||||
self.last_emitted_epoch_a = ep;
|
||||
} else {
|
||||
self.last_emitted_epoch_b = ep;
|
||||
}
|
||||
}
|
||||
Ok(emitted_key)
|
||||
}
|
||||
|
||||
pub fn receive_in_order(&mut self, is_alex: bool) -> Result<bool, Error> {
|
||||
let mut emitted_key = false;
|
||||
let mut emitted_ep: Option<Epoch> = None;
|
||||
{
|
||||
let (agent, incoming_message_queue, _omq, key_history) = self.data_for_agent(is_alex);
|
||||
|
||||
let maybe_msg = incoming_message_queue.pop_front();
|
||||
if let Some(msg) = maybe_msg {
|
||||
let mut rng = OsRng;
|
||||
let out = agent.messaging_scka_recv(&msg, &mut rng)?;
|
||||
if let Some((_ep, key)) = out {
|
||||
key_history.push(key);
|
||||
emitted_ep = Some(agent.last_emitted_epoch());
|
||||
emitted_key = true
|
||||
}
|
||||
if is_alex {
|
||||
self.a_rcvd += 1;
|
||||
} else {
|
||||
self.b_rcvd += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(ep) = emitted_ep {
|
||||
if is_alex {
|
||||
self.last_emitted_epoch_a = ep;
|
||||
} else {
|
||||
self.last_emitted_epoch_b = ep;
|
||||
}
|
||||
}
|
||||
Ok(emitted_key)
|
||||
}
|
||||
|
||||
pub fn receive_at(&mut self, is_alex: bool, i: usize) -> Result<bool, Error> {
|
||||
let mut emitted_key = false;
|
||||
let mut emitted_ep: Option<Epoch> = None;
|
||||
{
|
||||
let (agent, incoming_message_queue, _omq, key_history) = self.data_for_agent(is_alex);
|
||||
|
||||
let maybe_msg = incoming_message_queue.remove(i);
|
||||
if let Some(msg) = maybe_msg {
|
||||
let mut rng = OsRng;
|
||||
let out = agent.messaging_scka_recv(&msg, &mut rng)?;
|
||||
if let Some((_ep, key)) = out {
|
||||
key_history.push(key);
|
||||
emitted_ep = Some(agent.last_emitted_epoch());
|
||||
emitted_key = true
|
||||
}
|
||||
if is_alex {
|
||||
self.a_rcvd += 1;
|
||||
} else {
|
||||
self.b_rcvd += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(ep) = emitted_ep {
|
||||
if is_alex {
|
||||
self.last_emitted_epoch_a = ep;
|
||||
} else {
|
||||
self.last_emitted_epoch_b = ep;
|
||||
}
|
||||
}
|
||||
Ok(emitted_key)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn drop_message_at(&mut self, is_alex: bool, i: usize) {
|
||||
let (_agent, incoming_message_queue, _omq, _key_history) = self.data_for_agent(is_alex);
|
||||
|
||||
incoming_message_queue.remove(i);
|
||||
}
|
||||
|
||||
pub fn incoming_queue_size(&self, is_alex: bool) -> usize {
|
||||
if is_alex {
|
||||
self.b2a_msg_queue.len()
|
||||
} else {
|
||||
self.a2b_msg_queue.len()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn receive_all(&mut self, is_alex: bool) -> Result<bool, Error> {
|
||||
let mut emitted_ep: Option<Epoch> = None;
|
||||
let mut emitted_key = false;
|
||||
let mut num_received = 0usize;
|
||||
let (agent, incoming_message_queue, _omq, key_history) = self.data_for_agent(is_alex);
|
||||
while !incoming_message_queue.is_empty() {
|
||||
let maybe_msg = incoming_message_queue.pop_front();
|
||||
if let Some(msg) = maybe_msg {
|
||||
let mut rng = OsRng;
|
||||
let out = agent.messaging_scka_recv(&msg, &mut rng)?;
|
||||
if let Some((_ep, key)) = out {
|
||||
key_history.push(key);
|
||||
emitted_key = true;
|
||||
emitted_ep = Some(agent.last_emitted_epoch());
|
||||
}
|
||||
num_received += 1;
|
||||
}
|
||||
}
|
||||
if is_alex {
|
||||
self.a_rcvd += num_received;
|
||||
} else {
|
||||
self.b_rcvd += num_received;
|
||||
}
|
||||
if let Some(ep) = emitted_ep {
|
||||
if is_alex {
|
||||
self.last_emitted_epoch_a = ep;
|
||||
} else {
|
||||
self.last_emitted_epoch_b = ep;
|
||||
}
|
||||
}
|
||||
Ok(emitted_key)
|
||||
}
|
||||
|
||||
pub fn last_vulnerable_epoch_a(&self) -> Epoch {
|
||||
*self.alex.vulnerable_epochs().iter().max().unwrap_or(&0u64)
|
||||
}
|
||||
|
||||
pub fn last_vulnerable_epoch_b(&self) -> Epoch {
|
||||
*self.blake.vulnerable_epochs().iter().max().unwrap_or(&0u64)
|
||||
}
|
||||
|
||||
pub fn qlen(&self, for_alex: bool) -> usize {
|
||||
if for_alex {
|
||||
self.b2a_msg_queue.len()
|
||||
} else {
|
||||
self.a2b_msg_queue.len()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_msg_queue_lengths(&self) {
|
||||
println!(
|
||||
"Alex has {} incoming, Blake has {} incoming",
|
||||
self.b2a_msg_queue.len(),
|
||||
self.a2b_msg_queue.len()
|
||||
);
|
||||
}
|
||||
|
||||
pub fn print_key_history_lengths(&self) {
|
||||
println!(
|
||||
"Alex emitted {} keys, Blake emitted {} keys",
|
||||
self.key_history_a.len(),
|
||||
self.key_history_b.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct Compromise {
|
||||
#[allow(dead_code)]
|
||||
tick: usize,
|
||||
a_sent: usize,
|
||||
b_sent: usize,
|
||||
a_rcvd: usize,
|
||||
b_rcvd: usize,
|
||||
#[allow(dead_code)]
|
||||
heals_at: Epoch,
|
||||
exposed_epochs: Vec<Epoch>,
|
||||
active_epoch: Epoch,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct EpochVulnsetInfo {
|
||||
a_start: usize,
|
||||
a_end: usize,
|
||||
b_start: usize,
|
||||
b_end: usize,
|
||||
}
|
||||
|
||||
pub struct HealingHistogramEntry {
|
||||
pub num_msgs: usize,
|
||||
pub tot_by_a: usize,
|
||||
pub tot_by_b: usize,
|
||||
}
|
||||
|
||||
pub struct HealingStats {
|
||||
pub mean: f64,
|
||||
pub stddev: f64,
|
||||
pub deciles: [usize; 11],
|
||||
}
|
||||
|
||||
pub fn stats_from_histogram(hist: &Vec<HealingHistogramEntry>) -> [HealingStats; 2] {
|
||||
// first pass: compute aggregates: count, sum(num_msgs), sum(num_msgs^2)
|
||||
let mut count = [0usize; 2];
|
||||
let mut sum = [0usize; 2];
|
||||
let mut sum_squares = [0usize; 2];
|
||||
let mut min = [usize::MAX; 2];
|
||||
let mut max = [0usize; 2];
|
||||
let mut deciles = [[0usize; 11]; 2];
|
||||
|
||||
let mut mean = [0f64; 2];
|
||||
let mut mean_square = [0f64; 2];
|
||||
let mut var = [0f64; 2];
|
||||
let mut stddev = [0f64; 2];
|
||||
|
||||
for entry in hist {
|
||||
count[0] += entry.tot_by_a;
|
||||
count[1] += entry.tot_by_b;
|
||||
sum[0] += entry.num_msgs * entry.tot_by_a;
|
||||
sum[1] += entry.num_msgs * entry.tot_by_b;
|
||||
sum_squares[0] += entry.num_msgs * entry.num_msgs * entry.tot_by_a;
|
||||
sum_squares[1] += entry.num_msgs * entry.num_msgs * entry.tot_by_b;
|
||||
if entry.tot_by_a > 0 {
|
||||
min[0] = std::cmp::min(min[0], entry.num_msgs);
|
||||
max[0] = std::cmp::max(max[0], entry.num_msgs);
|
||||
}
|
||||
if entry.tot_by_b > 0 {
|
||||
min[1] = std::cmp::min(min[1], entry.num_msgs);
|
||||
max[1] = std::cmp::max(max[1], entry.num_msgs);
|
||||
}
|
||||
}
|
||||
for i in 0..2 {
|
||||
mean[i] = (sum[i] as f64) / (count[i] as f64);
|
||||
mean_square[i] = (sum_squares[i] as f64) / (count[i] as f64);
|
||||
var[i] = mean_square[i] - mean[i] * mean[i];
|
||||
stddev[i] = var[i].sqrt();
|
||||
}
|
||||
|
||||
for i in 0..2 {
|
||||
deciles[i][0] = min[i];
|
||||
deciles[i][10] = max[i];
|
||||
let mut cummulative_count = 0usize;
|
||||
let mut ctr = 1usize;
|
||||
for entry in hist {
|
||||
cummulative_count += if i == 0 {
|
||||
entry.tot_by_a
|
||||
} else {
|
||||
entry.tot_by_b
|
||||
};
|
||||
let decile_target = (count[i] * ctr) / 10;
|
||||
if cummulative_count > decile_target {
|
||||
deciles[i][ctr] = entry.num_msgs;
|
||||
ctr += 1;
|
||||
}
|
||||
if ctr == 10 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[
|
||||
HealingStats {
|
||||
mean: mean[0],
|
||||
stddev: stddev[0],
|
||||
deciles: deciles[0],
|
||||
},
|
||||
HealingStats {
|
||||
mean: mean[1],
|
||||
stddev: stddev[1],
|
||||
deciles: deciles[1],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
pub fn print_histogram(hist: &Vec<HealingHistogramEntry>) {
|
||||
println!("num exposed,msgs exposed by a comp,msgs exposed by b comp,freq a exposed by full,freq b exposed by full,tot exposed by full");
|
||||
for entry in hist {
|
||||
println!("{},{},{}", entry.num_msgs, entry.tot_by_a, entry.tot_by_b,);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_healing_stats(stats: &HealingStats) {
|
||||
println!("mean, stddev, min,p10,p20,p30,p40,p50,p60,p70,p80,p90,max");
|
||||
println!(
|
||||
"{},{},{},{},{},{},{},{},{},{},{},{},{}",
|
||||
stats.mean,
|
||||
stats.stddev,
|
||||
stats.deciles[0],
|
||||
stats.deciles[1],
|
||||
stats.deciles[2],
|
||||
stats.deciles[3],
|
||||
stats.deciles[4],
|
||||
stats.deciles[5],
|
||||
stats.deciles[6],
|
||||
stats.deciles[7],
|
||||
stats.deciles[8],
|
||||
stats.deciles[9],
|
||||
stats.deciles[10],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn random_healing_test<CKA, R: CryptoRng>(ratio: f64, rng: &mut R) -> Result<(), Error>
|
||||
where
|
||||
CKA: MessagingScka + MessagingCkaVulnerability,
|
||||
<CKA as MessagingScka>::CkaOutput: PartialEq + Debug,
|
||||
{
|
||||
let base_send_prob = 0.5;
|
||||
let p_a = base_send_prob * ratio;
|
||||
let p_b = base_send_prob * (1.0 - ratio);
|
||||
let mut mp = BasicMessagingBehavior::new(p_a, p_b, 0.9);
|
||||
let hist =
|
||||
controlled_messaging_healing_test::<CKA, BasicMessagingBehavior, R>(&mut mp, 10000, rng)?;
|
||||
print_histogram(&hist);
|
||||
print_healing_stats(&stats_from_histogram(&hist)[0]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn controlled_messaging_healing_test<CKA, MP, R>(
|
||||
mp: &mut MP,
|
||||
num_ticks: usize,
|
||||
rng: &mut R,
|
||||
) -> Result<Vec<HealingHistogramEntry>, Error>
|
||||
where
|
||||
CKA: MessagingScka + MessagingCkaVulnerability,
|
||||
<CKA as MessagingScka>::CkaOutput: PartialEq + Debug,
|
||||
MP: MessagingBehavior,
|
||||
R: CryptoRng,
|
||||
{
|
||||
let mut message_pattern_rng = StdRng::seed_from_u64(43);
|
||||
|
||||
let mut orchestrator = OrchestratorBase::<CKA>::new(rng)?;
|
||||
let mut alex_compromises_that_heal_at = HashMap::<Epoch, Vec<Compromise>>::new();
|
||||
let mut blake_compromises_that_heal_at = HashMap::<Epoch, Vec<Compromise>>::new();
|
||||
|
||||
let mut epoch_info = BTreeMap::<Epoch, EpochVulnsetInfo>::new();
|
||||
|
||||
for tick in 0..num_ticks {
|
||||
// println!("tick {}", tick);
|
||||
let mut emitted_key = false;
|
||||
let mut alex_emitted_key = false;
|
||||
let mut blake_emitted_key = false;
|
||||
let cmds = mp.next_commands(&mut message_pattern_rng);
|
||||
for cmd in &cmds {
|
||||
match cmd {
|
||||
Command::Send(agent) => {
|
||||
let use_alex = agent == &Agent::Alex;
|
||||
let emitted_send = orchestrator.send(use_alex, rng)?;
|
||||
|
||||
let OrchestratorBase {
|
||||
a_sent,
|
||||
b_sent,
|
||||
a_rcvd,
|
||||
b_rcvd,
|
||||
..
|
||||
} = orchestrator;
|
||||
|
||||
emitted_key = emitted_key || emitted_send;
|
||||
alex_emitted_key = alex_emitted_key || (emitted_key && use_alex);
|
||||
blake_emitted_key = blake_emitted_key || (emitted_key && !use_alex);
|
||||
|
||||
if alex_emitted_key {
|
||||
// alex emitted a key and may have healed
|
||||
let emitted_ep = orchestrator.alex.last_emitted_epoch();
|
||||
epoch_info
|
||||
.entry(emitted_ep)
|
||||
.and_modify(|inf| {
|
||||
inf.a_start = a_sent;
|
||||
})
|
||||
.or_insert(EpochVulnsetInfo {
|
||||
a_start: a_sent,
|
||||
a_end: 0,
|
||||
b_start: usize::MAX,
|
||||
b_end: 0,
|
||||
});
|
||||
if emitted_ep > 0 {
|
||||
epoch_info
|
||||
.entry(emitted_ep - 1)
|
||||
.and_modify(|inf| {
|
||||
inf.a_end = a_sent;
|
||||
})
|
||||
.or_insert(EpochVulnsetInfo {
|
||||
a_start: 0,
|
||||
a_end: a_sent,
|
||||
b_start: usize::MAX,
|
||||
b_end: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
if blake_emitted_key {
|
||||
// blake emitted a key and may have healed
|
||||
// alex emitted a key and may have healed
|
||||
let emitted_ep = orchestrator.blake.last_emitted_epoch();
|
||||
epoch_info
|
||||
.entry(emitted_ep)
|
||||
.and_modify(|inf| {
|
||||
inf.b_start = b_sent;
|
||||
})
|
||||
.or_insert(EpochVulnsetInfo {
|
||||
a_start: usize::MAX,
|
||||
a_end: 0,
|
||||
b_start: b_sent,
|
||||
b_end: 0,
|
||||
});
|
||||
if emitted_ep > 0 {
|
||||
epoch_info
|
||||
.entry(emitted_ep - 1)
|
||||
.and_modify(|inf| {
|
||||
inf.b_end = b_sent;
|
||||
})
|
||||
.or_insert(EpochVulnsetInfo {
|
||||
a_start: usize::MAX,
|
||||
a_end: 0,
|
||||
b_start: 0,
|
||||
b_end: b_sent,
|
||||
});
|
||||
}
|
||||
}
|
||||
if use_alex {
|
||||
let heals_at = orchestrator.last_vulnerable_epoch_a() + 1;
|
||||
let comp = Compromise {
|
||||
tick,
|
||||
a_sent,
|
||||
b_sent,
|
||||
a_rcvd,
|
||||
b_rcvd,
|
||||
heals_at,
|
||||
exposed_epochs: orchestrator.alex.vulnerable_epochs(),
|
||||
active_epoch: orchestrator.alex.last_emitted_epoch(),
|
||||
};
|
||||
alex_compromises_that_heal_at
|
||||
.entry(heals_at)
|
||||
.or_default()
|
||||
.push(comp);
|
||||
} else {
|
||||
let heals_at = orchestrator.last_vulnerable_epoch_b() + 1;
|
||||
let comp = Compromise {
|
||||
tick,
|
||||
a_sent,
|
||||
b_sent,
|
||||
a_rcvd,
|
||||
b_rcvd,
|
||||
heals_at,
|
||||
exposed_epochs: orchestrator.blake.vulnerable_epochs(),
|
||||
active_epoch: orchestrator.blake.last_emitted_epoch(),
|
||||
};
|
||||
blake_compromises_that_heal_at
|
||||
.entry(heals_at)
|
||||
.or_default()
|
||||
.push(comp);
|
||||
}
|
||||
}
|
||||
Command::ReceiveAll(agent) => {
|
||||
let use_alex = agent == &Agent::Alex;
|
||||
let do_compromise = orchestrator.incoming_queue_size(use_alex) > 0;
|
||||
let emitted_recv = orchestrator.receive_all(use_alex)?;
|
||||
|
||||
let OrchestratorBase {
|
||||
a_sent,
|
||||
b_sent,
|
||||
a_rcvd,
|
||||
b_rcvd,
|
||||
..
|
||||
} = orchestrator;
|
||||
|
||||
emitted_key = emitted_key || emitted_recv;
|
||||
alex_emitted_key = alex_emitted_key || (emitted_key && use_alex);
|
||||
blake_emitted_key = blake_emitted_key || (emitted_key && !use_alex);
|
||||
|
||||
if alex_emitted_key {
|
||||
// alex emitted a key and may have healed
|
||||
let emitted_ep = orchestrator.alex.last_emitted_epoch();
|
||||
epoch_info
|
||||
.entry(emitted_ep)
|
||||
.and_modify(|inf| {
|
||||
inf.a_start = a_sent;
|
||||
})
|
||||
.or_insert(EpochVulnsetInfo {
|
||||
a_start: a_sent,
|
||||
a_end: 0,
|
||||
b_start: usize::MAX,
|
||||
b_end: 0,
|
||||
});
|
||||
if emitted_ep > 0 {
|
||||
epoch_info
|
||||
.entry(emitted_ep - 1)
|
||||
.and_modify(|inf| {
|
||||
inf.a_end = a_sent;
|
||||
})
|
||||
.or_insert(EpochVulnsetInfo {
|
||||
a_start: 0,
|
||||
a_end: a_sent,
|
||||
b_start: usize::MAX,
|
||||
b_end: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
if blake_emitted_key {
|
||||
// blake emitted a key and may have healed
|
||||
// alex emitted a key and may have healed
|
||||
let emitted_ep = orchestrator.blake.last_emitted_epoch();
|
||||
epoch_info
|
||||
.entry(emitted_ep)
|
||||
.and_modify(|inf| {
|
||||
inf.b_start = b_sent;
|
||||
})
|
||||
.or_insert(EpochVulnsetInfo {
|
||||
a_start: usize::MAX,
|
||||
a_end: 0,
|
||||
b_start: b_sent,
|
||||
b_end: 0,
|
||||
});
|
||||
if emitted_ep > 0 {
|
||||
epoch_info
|
||||
.entry(emitted_ep - 1)
|
||||
.and_modify(|inf| {
|
||||
inf.b_end = b_sent;
|
||||
})
|
||||
.or_insert(EpochVulnsetInfo {
|
||||
a_start: usize::MAX,
|
||||
a_end: 0,
|
||||
b_start: 0,
|
||||
b_end: b_sent,
|
||||
});
|
||||
}
|
||||
}
|
||||
if do_compromise {
|
||||
if use_alex {
|
||||
let heals_at = orchestrator.last_vulnerable_epoch_a() + 1;
|
||||
let comp = Compromise {
|
||||
tick,
|
||||
a_sent,
|
||||
b_sent,
|
||||
a_rcvd,
|
||||
b_rcvd,
|
||||
heals_at,
|
||||
exposed_epochs: orchestrator.alex.vulnerable_epochs(),
|
||||
active_epoch: orchestrator.alex.last_emitted_epoch(),
|
||||
};
|
||||
alex_compromises_that_heal_at
|
||||
.entry(heals_at)
|
||||
.or_default()
|
||||
.push(comp);
|
||||
} else {
|
||||
let heals_at = orchestrator.last_vulnerable_epoch_b() + 1;
|
||||
let comp = Compromise {
|
||||
tick,
|
||||
a_sent,
|
||||
b_sent,
|
||||
a_rcvd,
|
||||
b_rcvd,
|
||||
heals_at,
|
||||
exposed_epochs: orchestrator.blake.vulnerable_epochs(),
|
||||
active_epoch: orchestrator.blake.last_emitted_epoch(),
|
||||
};
|
||||
blake_compromises_that_heal_at
|
||||
.entry(heals_at)
|
||||
.or_default()
|
||||
.push(comp);
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::Receive(agent) => {
|
||||
let use_alex = agent == &Agent::Alex;
|
||||
let do_compromise = orchestrator.incoming_queue_size(use_alex) > 0;
|
||||
let emitted_recv = orchestrator.receive_in_order(use_alex)?;
|
||||
|
||||
let OrchestratorBase {
|
||||
a_sent,
|
||||
b_sent,
|
||||
a_rcvd,
|
||||
b_rcvd,
|
||||
..
|
||||
} = orchestrator;
|
||||
|
||||
emitted_key = emitted_key || emitted_recv;
|
||||
alex_emitted_key = alex_emitted_key || (emitted_key && use_alex);
|
||||
blake_emitted_key = blake_emitted_key || (emitted_key && !use_alex);
|
||||
|
||||
if alex_emitted_key {
|
||||
// alex emitted a key and may have healed
|
||||
let emitted_ep = orchestrator.alex.last_emitted_epoch();
|
||||
epoch_info
|
||||
.entry(emitted_ep)
|
||||
.and_modify(|inf| {
|
||||
inf.a_start = a_sent;
|
||||
})
|
||||
.or_insert(EpochVulnsetInfo {
|
||||
a_start: a_sent,
|
||||
a_end: 0,
|
||||
b_start: usize::MAX,
|
||||
b_end: 0,
|
||||
});
|
||||
if emitted_ep > 0 {
|
||||
epoch_info
|
||||
.entry(emitted_ep - 1)
|
||||
.and_modify(|inf| {
|
||||
inf.a_end = a_sent;
|
||||
})
|
||||
.or_insert(EpochVulnsetInfo {
|
||||
a_start: 0,
|
||||
a_end: a_sent,
|
||||
b_start: usize::MAX,
|
||||
b_end: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
if blake_emitted_key {
|
||||
// blake emitted a key and may have healed
|
||||
// alex emitted a key and may have healed
|
||||
let emitted_ep = orchestrator.blake.last_emitted_epoch();
|
||||
epoch_info
|
||||
.entry(emitted_ep)
|
||||
.and_modify(|inf| {
|
||||
inf.b_start = b_sent;
|
||||
})
|
||||
.or_insert(EpochVulnsetInfo {
|
||||
a_start: usize::MAX,
|
||||
a_end: 0,
|
||||
b_start: b_sent,
|
||||
b_end: 0,
|
||||
});
|
||||
if emitted_ep > 0 {
|
||||
epoch_info
|
||||
.entry(emitted_ep - 1)
|
||||
.and_modify(|inf| {
|
||||
inf.b_end = b_sent;
|
||||
})
|
||||
.or_insert(EpochVulnsetInfo {
|
||||
a_start: usize::MAX,
|
||||
a_end: 0,
|
||||
b_start: 0,
|
||||
b_end: b_sent,
|
||||
});
|
||||
}
|
||||
}
|
||||
if do_compromise {
|
||||
if use_alex {
|
||||
let heals_at = orchestrator.last_vulnerable_epoch_a() + 1;
|
||||
let comp = Compromise {
|
||||
tick,
|
||||
a_sent,
|
||||
b_sent,
|
||||
a_rcvd,
|
||||
b_rcvd,
|
||||
heals_at,
|
||||
exposed_epochs: orchestrator.alex.vulnerable_epochs(),
|
||||
active_epoch: orchestrator.alex.last_emitted_epoch(),
|
||||
};
|
||||
alex_compromises_that_heal_at
|
||||
.entry(heals_at)
|
||||
.or_default()
|
||||
.push(comp);
|
||||
} else {
|
||||
let heals_at = orchestrator.last_vulnerable_epoch_b() + 1;
|
||||
let comp = Compromise {
|
||||
tick,
|
||||
a_sent,
|
||||
b_sent,
|
||||
a_rcvd,
|
||||
b_rcvd,
|
||||
heals_at,
|
||||
exposed_epochs: orchestrator.blake.vulnerable_epochs(),
|
||||
active_epoch: orchestrator.blake.last_emitted_epoch(),
|
||||
};
|
||||
blake_compromises_that_heal_at
|
||||
.entry(heals_at)
|
||||
.or_default()
|
||||
.push(comp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
orchestrator.key_history_is_consistent();
|
||||
}
|
||||
|
||||
let mut a_comp_hist = BTreeMap::<usize, usize>::new();
|
||||
let mut b_comp_hist = BTreeMap::<usize, usize>::new();
|
||||
|
||||
for (_ep, cs) in alex_compromises_that_heal_at {
|
||||
for c in cs {
|
||||
if let Some(active_ep) = epoch_info.get(&c.active_epoch) {
|
||||
if active_ep.a_end < c.a_sent {
|
||||
// println!("skip epoch info A1 ep {}", c.active_epoch);
|
||||
continue;
|
||||
}
|
||||
let symratchet_exposed_a_msgs = active_ep.a_end.saturating_sub(c.a_sent);
|
||||
|
||||
if active_ep.b_end < c.a_rcvd {
|
||||
// println!("skip epoch info A2 ep {}", c.active_epoch);
|
||||
continue;
|
||||
}
|
||||
let symratchet_exposed_b_msgs = active_ep.b_end.saturating_sub(c.a_rcvd);
|
||||
let mut exposed_a = symratchet_exposed_a_msgs;
|
||||
let mut exposed_b = symratchet_exposed_b_msgs;
|
||||
for ep in c.exposed_epochs {
|
||||
//(c.active_epoch+1)..c.heals_at { // in c.exposed_epochs
|
||||
if let Some(epinf) = epoch_info.get(&ep) {
|
||||
if epinf.a_end >= epinf.a_start && epinf.b_end >= epinf.b_start {
|
||||
exposed_a += epinf.a_end - epinf.a_start;
|
||||
exposed_b += epinf.b_end - epinf.b_start;
|
||||
} else {
|
||||
// println!("sk epoch info: {:?}", epinf);
|
||||
}
|
||||
} else {
|
||||
// println!("No epoch info ({})", ep)
|
||||
}
|
||||
}
|
||||
a_comp_hist
|
||||
.entry(exposed_a + exposed_b)
|
||||
.and_modify(|count| *count += 1)
|
||||
.or_insert(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (_ep, cs) in blake_compromises_that_heal_at {
|
||||
for c in cs {
|
||||
if let Some(active_ep) = epoch_info.get(&c.active_epoch) {
|
||||
if active_ep.b_end < c.b_sent {
|
||||
// println!("skip epoch info B1 ep {}", c.active_epoch);
|
||||
continue;
|
||||
}
|
||||
let symratchet_exposed_b_msgs = active_ep.b_end.saturating_sub(c.b_sent);
|
||||
|
||||
if active_ep.a_end < c.b_rcvd {
|
||||
// println!("skip epoch info B2 ep {} (a_end: {} b_rcvd: {})", c.active_epoch, active_ep.a_end, c.b_rcvd);
|
||||
continue;
|
||||
}
|
||||
let symratchet_exposed_a_msgs = active_ep.a_end.saturating_sub(c.b_rcvd);
|
||||
let mut exposed_b = symratchet_exposed_b_msgs;
|
||||
let mut exposed_a = symratchet_exposed_a_msgs;
|
||||
for ep in c.exposed_epochs {
|
||||
//(c.active_epoch+1)..c.heals_at { // in c.exposed_epochs
|
||||
if let Some(epinf) = epoch_info.get(&ep) {
|
||||
if epinf.a_end >= epinf.a_start && epinf.b_end >= epinf.b_start {
|
||||
exposed_a += epinf.a_end - epinf.a_start;
|
||||
exposed_b += epinf.b_end - epinf.b_start;
|
||||
}
|
||||
} else {
|
||||
// println!("No epoch info ({})", ep)
|
||||
}
|
||||
}
|
||||
b_comp_hist
|
||||
.entry(exposed_a + exposed_b)
|
||||
.and_modify(|count| *count += 1)
|
||||
.or_insert(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let max_exposed = *std::cmp::max(
|
||||
a_comp_hist.last_key_value().unwrap().0,
|
||||
b_comp_hist.last_key_value().unwrap().0,
|
||||
);
|
||||
|
||||
let mut hist = Vec::<HealingHistogramEntry>::new();
|
||||
|
||||
for i in 0..=max_exposed {
|
||||
hist.push(HealingHistogramEntry {
|
||||
num_msgs: i,
|
||||
tot_by_a: *a_comp_hist.get(&i).unwrap_or(&0),
|
||||
tot_by_b: *b_comp_hist.get(&i).unwrap_or(&0),
|
||||
});
|
||||
}
|
||||
// orchestrator.print_key_history_lengths();
|
||||
// orchestrator.print_msg_queue_lengths();
|
||||
|
||||
Ok(hist)
|
||||
}
|
||||
|
||||
pub fn random_balanced<CKA, R>(rng: &mut R) -> Result<(), Error>
|
||||
where
|
||||
CKA: MessagingScka + MessagingCkaVulnerability,
|
||||
<CKA as MessagingScka>::CkaOutput: PartialEq + Debug,
|
||||
R: CryptoRng,
|
||||
{
|
||||
let mut a_tot = 0;
|
||||
let mut b_tot = 0;
|
||||
let mut orchestrator = OrchestratorBase::<CKA>::new(rng)?;
|
||||
for _i in 0..10000 {
|
||||
let rnd: u32 = rng.next_u32();
|
||||
let use_alex = rnd & 0x1 != 0;
|
||||
let do_receive = rnd & 0x6 != 0;
|
||||
let do_send = rnd & 0x8 != 0;
|
||||
if do_receive {
|
||||
// orchestrator.receive_in_order(use_alex)?;
|
||||
orchestrator.receive_all(use_alex)?;
|
||||
}
|
||||
if do_send {
|
||||
orchestrator.send(use_alex, rng)?;
|
||||
if use_alex {
|
||||
a_tot += 1;
|
||||
} else {
|
||||
b_tot += 1;
|
||||
}
|
||||
}
|
||||
orchestrator.key_history_is_consistent();
|
||||
}
|
||||
orchestrator.print_key_history_lengths();
|
||||
orchestrator.print_msg_queue_lengths();
|
||||
println!("Alex sent {a_tot} Blake sent {b_tot}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn random_balanced_out_of_order<CKA, R>(rng: &mut R) -> Result<(), Error>
|
||||
where
|
||||
CKA: MessagingScka + MessagingCkaVulnerability,
|
||||
<CKA as MessagingScka>::CkaOutput: PartialEq + Debug,
|
||||
R: CryptoRng,
|
||||
{
|
||||
let mut orchestrator = OrchestratorBase::<CKA>::new(rng)?;
|
||||
for _i in 0..10000 {
|
||||
let rnd = rng.next_u32();
|
||||
let use_alex = rnd & 0x1 == 0;
|
||||
let do_send = rnd & 0x2 != 0;
|
||||
let do_ooo = rnd & 0x4 == 0;
|
||||
let rcv_all = rnd & 0xF8 == 0;
|
||||
if do_ooo {
|
||||
if orchestrator.qlen(use_alex) > 0 {
|
||||
orchestrator.receive_at(
|
||||
use_alex,
|
||||
((rnd >> 8) as usize) % orchestrator.qlen(use_alex),
|
||||
)?;
|
||||
}
|
||||
} else if rcv_all {
|
||||
orchestrator.receive_all(use_alex)?;
|
||||
} else {
|
||||
orchestrator.receive_in_order(use_alex)?;
|
||||
}
|
||||
if do_send {
|
||||
orchestrator.send(use_alex, rng)?;
|
||||
}
|
||||
orchestrator.key_history_is_consistent();
|
||||
}
|
||||
orchestrator.print_key_history_lengths();
|
||||
orchestrator.print_msg_queue_lengths();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn chaos<CKA, R>(num_ticks: usize, rng: &mut R) -> Result<(), Error>
|
||||
where
|
||||
CKA: MessagingScka + MessagingCkaVulnerability,
|
||||
<CKA as MessagingScka>::CkaOutput: PartialEq + Debug,
|
||||
R: CryptoRng,
|
||||
{
|
||||
let ooo_prob = 0.7;
|
||||
let send_limit = 10;
|
||||
let drop_message_prob = 0.1;
|
||||
let mut orchestrator = OrchestratorBase::<CKA>::new(rng)?;
|
||||
for i in 0..num_ticks {
|
||||
let use_alex = if i % 100 < 50 {
|
||||
rng.random_bool(0.25)
|
||||
} else {
|
||||
rng.random_bool(0.75)
|
||||
};
|
||||
|
||||
// receive out of order
|
||||
if rng.random_bool(ooo_prob) {
|
||||
let qlen = orchestrator.qlen(use_alex);
|
||||
if qlen > 0 {
|
||||
let _received =
|
||||
orchestrator.receive_at(use_alex, rng.next_u32() as usize % qlen)?;
|
||||
}
|
||||
}
|
||||
let num_to_send = rng.next_u32() % send_limit;
|
||||
for _ in 0..num_to_send {
|
||||
orchestrator.send(use_alex, rng)?;
|
||||
if rng.random_bool(drop_message_prob) {
|
||||
orchestrator.drop_message_at(use_alex, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// don't let queue get too big
|
||||
loop {
|
||||
let qlen = orchestrator.qlen(use_alex);
|
||||
if qlen > 20 {
|
||||
let _received =
|
||||
orchestrator.receive_at(use_alex, rng.next_u32() as usize % qlen)?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
orchestrator.key_history_is_consistent();
|
||||
}
|
||||
orchestrator.print_key_history_lengths();
|
||||
orchestrator.print_msg_queue_lengths();
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
use super::messaging_behavior;
|
||||
use rand_distr::{Binomial, Distribution};
|
||||
|
||||
use super::messaging_behavior::{Agent, MessagingBehavior};
|
||||
|
||||
pub struct PingPongMessagingBehavior {
|
||||
window_size: u64,
|
||||
window_variance: u64,
|
||||
agent: Agent,
|
||||
sends_remaining: u64,
|
||||
}
|
||||
|
||||
impl PingPongMessagingBehavior {
|
||||
pub fn new(window_size: u64, window_variance: u64) -> Self {
|
||||
assert!(window_variance <= window_size);
|
||||
Self {
|
||||
window_size,
|
||||
window_variance,
|
||||
agent: Agent::Blake,
|
||||
sends_remaining: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn switch_agent(&mut self) {
|
||||
self.agent = match self.agent {
|
||||
Agent::Alex => Agent::Blake,
|
||||
Agent::Blake => Agent::Alex,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessagingBehavior for PingPongMessagingBehavior {
|
||||
fn next_commands<R: rand_core::CryptoRng>(
|
||||
&mut self,
|
||||
rng: &mut R,
|
||||
) -> Vec<super::messaging_behavior::Command> {
|
||||
let mut cmds = Vec::<messaging_behavior::Command>::new();
|
||||
if self.sends_remaining == 0 {
|
||||
self.switch_agent();
|
||||
cmds.push(messaging_behavior::Command::ReceiveAll(self.agent));
|
||||
|
||||
let bin = Binomial::new(self.window_variance, 0.5).unwrap();
|
||||
let delta = bin.sample(rng);
|
||||
self.sends_remaining = self.window_size + delta;
|
||||
} else {
|
||||
cmds.push(messaging_behavior::Command::Send(self.agent));
|
||||
self.sends_remaining -= 1;
|
||||
}
|
||||
cmds
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
use rand_core::CryptoRng;
|
||||
|
||||
use crate::{Epoch, Error, Secret};
|
||||
|
||||
pub struct SendOutput {
|
||||
pub output_key: Option<(Epoch, Secret)>,
|
||||
pub sending_epoch: Epoch,
|
||||
}
|
||||
|
||||
pub struct ReceiveOutput {
|
||||
pub output_key: Option<(Epoch, Secret)>,
|
||||
pub receiving_epoch: Epoch,
|
||||
}
|
||||
|
||||
// Sparse continuous key agreement
|
||||
pub trait Scka {
|
||||
type Message: SckaMessage;
|
||||
|
||||
fn scka_send<R: CryptoRng>(
|
||||
self,
|
||||
rng: &mut R,
|
||||
) -> Result<(SendOutput, Self::Message, Self), Error>
|
||||
where
|
||||
Self: Sized;
|
||||
fn scka_recv(self, msg: &Self::Message) -> Result<(ReceiveOutput, Self), Error>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
pub trait SckaMessage {
|
||||
fn epoch(&self) -> Epoch;
|
||||
}
|
||||
|
||||
pub trait SckaInitializer {
|
||||
// Note: in the paper we pass in an encapsulation key here to support more
|
||||
// general protocols. We will add this if it is needed.
|
||||
fn init_a<R: CryptoRng>(rng: &mut R) -> Result<Self, Error>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
// Note: in the paper we pass in an decapsulation key here to support more
|
||||
// general protocols. We will add this if it is needed.
|
||||
fn init_b<R: CryptoRng>(rng: &mut R) -> Result<Self, Error>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
pub trait SckaVulnerability {
|
||||
fn vulnerable_epochs(&self) -> Vec<Epoch>;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
use crate::v1::chunked::states;
|
||||
use crate::{
|
||||
test::scka::{Scka, SckaInitializer, SckaVulnerability},
|
||||
Epoch, Error,
|
||||
};
|
||||
use rand_core::CryptoRng;
|
||||
|
||||
use super::scka::{ReceiveOutput, SckaMessage, SendOutput};
|
||||
|
||||
impl Scka for states::States {
|
||||
type Message = states::Message;
|
||||
|
||||
fn scka_send<R: CryptoRng>(
|
||||
self,
|
||||
rng: &mut R,
|
||||
) -> Result<(SendOutput, states::Message, Self), Error> {
|
||||
let states::Send { msg, key, state } = self.send(rng)?;
|
||||
|
||||
Ok((
|
||||
SendOutput {
|
||||
output_key: key.map(|es| (es.epoch, es.secret)),
|
||||
sending_epoch: msg.epoch - 1,
|
||||
},
|
||||
msg,
|
||||
state,
|
||||
))
|
||||
}
|
||||
|
||||
fn scka_recv(self, msg: &states::Message) -> Result<(ReceiveOutput, Self), Error>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let states::Recv { key, state } = self.recv(msg)?;
|
||||
Ok((
|
||||
ReceiveOutput {
|
||||
output_key: key.map(|es| (es.epoch, es.secret)),
|
||||
receiving_epoch: msg.epoch - 1,
|
||||
},
|
||||
state,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl SckaVulnerability for states::States {
|
||||
fn vulnerable_epochs(&self) -> Vec<Epoch> {
|
||||
states::States::vulnerable_epochs(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl SckaInitializer for states::States {
|
||||
fn init_a<R: CryptoRng>(_rng: &mut R) -> Result<Self, Error>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
// TODO: pass in real auth key
|
||||
Ok(states::States::init_a(b"1"))
|
||||
}
|
||||
|
||||
fn init_b<R: CryptoRng>(_rng: &mut R) -> Result<Self, Error>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
// TODO: pass in real auth key
|
||||
Ok(states::States::init_b(b"1"))
|
||||
}
|
||||
}
|
||||
|
||||
impl SckaMessage for states::Message {
|
||||
fn epoch(&self) -> Epoch {
|
||||
self.epoch
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pub(crate) mod states;
|
||||
@@ -0,0 +1,416 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
#![allow(clippy::comparison_chain)]
|
||||
#![cfg(test)]
|
||||
use crate::test::scka::{
|
||||
ReceiveOutput, Scka, SckaInitializer, SckaMessage, SckaVulnerability, SendOutput,
|
||||
};
|
||||
use crate::Epoch;
|
||||
use curve25519_dalek::{
|
||||
ristretto::CompressedRistretto, scalar::Scalar, traits::Identity, RistrettoPoint,
|
||||
};
|
||||
use rand_08::rngs::OsRng as OsRngFromRand08;
|
||||
use rand_core::CryptoRng;
|
||||
use sha2::Digest;
|
||||
|
||||
const X25519_KEYTYPE: u8 = 1u8;
|
||||
|
||||
pub type Secret = Vec<u8>;
|
||||
|
||||
pub struct Message {
|
||||
pub epoch: Epoch,
|
||||
pub pubkey: [u8; 33],
|
||||
}
|
||||
|
||||
impl SckaMessage for Message {
|
||||
fn epoch(&self) -> Epoch {
|
||||
self.epoch
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum States {
|
||||
Send(Send),
|
||||
Recv(Recv),
|
||||
UninitSend(UninitSend),
|
||||
UninitRecv(UninitRecv),
|
||||
}
|
||||
|
||||
impl From<Send> for States {
|
||||
fn from(value: Send) -> Self {
|
||||
States::Send(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Recv> for States {
|
||||
fn from(value: Recv) -> Self {
|
||||
States::Recv(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UninitSend> for States {
|
||||
fn from(value: UninitSend) -> Self {
|
||||
States::UninitSend(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UninitRecv> for States {
|
||||
fn from(value: UninitRecv) -> Self {
|
||||
States::UninitRecv(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl States {
|
||||
pub fn init_a() -> Self {
|
||||
States::UninitSend(UninitSend { epoch: 0 })
|
||||
}
|
||||
pub fn init_b() -> Self {
|
||||
States::UninitRecv(UninitRecv { epoch: 0 })
|
||||
}
|
||||
|
||||
pub fn sending_epoch(&self) -> Epoch {
|
||||
match self {
|
||||
States::Send(state) => state.epoch,
|
||||
States::Recv(state) => state.epoch - 1,
|
||||
States::UninitSend(_state) => 0,
|
||||
States::UninitRecv(_state) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn receiving_epoch(&self) -> Epoch {
|
||||
match self {
|
||||
States::Send(state) => state.epoch - 1,
|
||||
States::Recv(state) => state.epoch,
|
||||
States::UninitSend(_state) => 0,
|
||||
States::UninitRecv(_state) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn epoch(&self) -> Epoch {
|
||||
match self {
|
||||
States::Send(state) => state.epoch,
|
||||
States::Recv(state) => state.epoch,
|
||||
States::UninitSend(state) => state.epoch,
|
||||
States::UninitRecv(state) => state.epoch,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Send {
|
||||
pub(super) epoch: Epoch,
|
||||
pub(super) remote_public: RistrettoPoint,
|
||||
}
|
||||
|
||||
impl Send {
|
||||
fn send<R: CryptoRng>(self, _rng: &mut R) -> (Option<(Epoch, Secret)>, Message, States) {
|
||||
let mut secret = Scalar::random(&mut OsRngFromRand08);
|
||||
let public = RistrettoPoint::mul_base(&secret);
|
||||
|
||||
let local_public = serialize_public_key(public);
|
||||
|
||||
let msg = Message {
|
||||
epoch: self.epoch,
|
||||
pubkey: local_public,
|
||||
};
|
||||
|
||||
// compute the shared secret to output
|
||||
let shared_secret = (secret * self.remote_public).compress();
|
||||
let shared_secret = shared_secret.as_bytes();
|
||||
|
||||
// println!(
|
||||
// "Send secret output ({}, {:?})",
|
||||
// self.epoch,
|
||||
// shared_secret.split_at(5).0
|
||||
// );
|
||||
|
||||
// update the private key for forward secrecy
|
||||
let secret_hash: [u8; 64] = sha2::Sha512::digest(shared_secret).into();
|
||||
let adjustment_scalar = Scalar::from_bytes_mod_order_wide(&secret_hash);
|
||||
secret *= adjustment_scalar;
|
||||
|
||||
let next = Recv {
|
||||
epoch: self.epoch + 1,
|
||||
local_public,
|
||||
secret,
|
||||
};
|
||||
|
||||
(Some((self.epoch, shared_secret.to_vec())), msg, next.into())
|
||||
}
|
||||
|
||||
fn recv(self, _msg: &Message) -> (Option<(Epoch, Secret)>, States) {
|
||||
(None, self.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Recv {
|
||||
pub(super) epoch: Epoch,
|
||||
pub(super) local_public: [u8; 33],
|
||||
pub(super) secret: Scalar,
|
||||
}
|
||||
|
||||
impl Recv {
|
||||
fn send<R: CryptoRng>(self, _rng: &mut R) -> (Option<(Epoch, Secret)>, Message, States) {
|
||||
let msg = Message {
|
||||
epoch: self.epoch - 1,
|
||||
pubkey: self.local_public,
|
||||
};
|
||||
(None, msg, self.into())
|
||||
}
|
||||
|
||||
fn recv(self, msg: &Message) -> (Option<(Epoch, Secret)>, States) {
|
||||
if msg.pubkey[0] != X25519_KEYTYPE {
|
||||
todo!("add error for unrecognized key")
|
||||
}
|
||||
|
||||
if msg.epoch < self.epoch {
|
||||
println!(
|
||||
"recv earlier epoch {} < {}, ignoring",
|
||||
msg.epoch, self.epoch
|
||||
);
|
||||
return (None, self.into());
|
||||
} else if msg.epoch > self.epoch {
|
||||
todo!("create invalid epoch error");
|
||||
}
|
||||
let remote_public = CompressedRistretto::from_slice(&msg.pubkey[1..33])
|
||||
.expect("ristretto properly serialized")
|
||||
.decompress()
|
||||
.unwrap();
|
||||
let shared_secret = (self.secret * remote_public).compress();
|
||||
let shared_secret = shared_secret.as_bytes();
|
||||
|
||||
// println!(
|
||||
// "Recv secret output ({},{:?})",
|
||||
// self.epoch,
|
||||
// shared_secret.split_at(5).0
|
||||
// );
|
||||
|
||||
// update the remote public key for forward secrecy
|
||||
let secret_hash: [u8; 64] = sha2::Sha512::digest(shared_secret).into();
|
||||
let adjustment_scalar = Scalar::from_bytes_mod_order_wide(&secret_hash);
|
||||
let remote_public = adjustment_scalar * remote_public;
|
||||
|
||||
let next = Send {
|
||||
epoch: self.epoch + 1,
|
||||
remote_public,
|
||||
};
|
||||
|
||||
(Some((self.epoch, shared_secret.to_vec())), next.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UninitSend {
|
||||
pub(super) epoch: Epoch,
|
||||
}
|
||||
|
||||
impl UninitSend {
|
||||
fn send<R: CryptoRng>(self, _rng: &mut R) -> (Option<(Epoch, Secret)>, Message, States) {
|
||||
let secret = Scalar::random(&mut OsRngFromRand08);
|
||||
let public = RistrettoPoint::mul_base(&secret);
|
||||
|
||||
let local_public = serialize_public_key(public);
|
||||
|
||||
let msg = Message {
|
||||
epoch: self.epoch,
|
||||
pubkey: local_public,
|
||||
};
|
||||
|
||||
let next = Recv {
|
||||
epoch: self.epoch + 1,
|
||||
local_public,
|
||||
secret,
|
||||
};
|
||||
|
||||
(None, msg, next.into())
|
||||
}
|
||||
|
||||
fn recv(self, _msg: &Message) -> (Option<(Epoch, Secret)>, States) {
|
||||
(None, self.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UninitRecv {
|
||||
pub(super) epoch: Epoch,
|
||||
}
|
||||
|
||||
impl UninitRecv {
|
||||
fn send<R: CryptoRng>(self, _rng: &mut R) -> (Option<(Epoch, Secret)>, Message, States) {
|
||||
println!("UninitRecv::send() epoch {}", self.epoch);
|
||||
let msg = Message {
|
||||
epoch: self.epoch,
|
||||
pubkey: serialize_public_key(RistrettoPoint::identity()),
|
||||
};
|
||||
(None, msg, self.into())
|
||||
}
|
||||
|
||||
fn recv(self, msg: &Message) -> (Option<(Epoch, Secret)>, States) {
|
||||
if msg.pubkey[0] != X25519_KEYTYPE {
|
||||
todo!("add error for unrecognized key")
|
||||
}
|
||||
|
||||
if msg.epoch < self.epoch {
|
||||
return (None, self.into());
|
||||
} else if msg.epoch > self.epoch {
|
||||
todo!("create invalid epoch error");
|
||||
}
|
||||
|
||||
let remote_public = CompressedRistretto::from_slice(&msg.pubkey[1..33])
|
||||
.expect("ristretto properly serialized")
|
||||
.decompress()
|
||||
.unwrap();
|
||||
|
||||
let next = Send {
|
||||
epoch: self.epoch + 1,
|
||||
remote_public,
|
||||
};
|
||||
|
||||
(None, next.into())
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_public_key(pubkey: RistrettoPoint) -> [u8; 33] {
|
||||
let mut result = [0u8; 33];
|
||||
let compressed = pubkey.compress();
|
||||
result[0] = X25519_KEYTYPE;
|
||||
result[1..].copy_from_slice(compressed.as_bytes());
|
||||
result
|
||||
}
|
||||
|
||||
impl Scka for States {
|
||||
type Message = Message;
|
||||
|
||||
fn scka_send<R: CryptoRng>(
|
||||
self,
|
||||
rng: &mut R,
|
||||
) -> Result<(SendOutput, Self::Message, Self), crate::Error>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let (output_key, msg, state) = match self {
|
||||
States::Send(state) => state.send(rng),
|
||||
States::Recv(state) => state.send(rng),
|
||||
States::UninitSend(state) => state.send(rng),
|
||||
States::UninitRecv(state) => state.send(rng),
|
||||
};
|
||||
let so = SendOutput {
|
||||
output_key,
|
||||
sending_epoch: state.sending_epoch(),
|
||||
};
|
||||
Ok((so, msg, state))
|
||||
}
|
||||
|
||||
fn scka_recv(self, msg: &Self::Message) -> Result<(ReceiveOutput, Self), crate::Error>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let (output_key, state) = match self {
|
||||
States::Send(state) => state.recv(msg),
|
||||
States::Recv(state) => state.recv(msg),
|
||||
States::UninitSend(state) => state.recv(msg),
|
||||
States::UninitRecv(state) => state.recv(msg),
|
||||
};
|
||||
|
||||
let ro = ReceiveOutput {
|
||||
output_key,
|
||||
receiving_epoch: state.receiving_epoch(),
|
||||
};
|
||||
Ok((ro, state))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl SckaInitializer for States {
|
||||
fn init_a<R: CryptoRng>(_: &mut R) -> Result<Self, crate::Error>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Ok(States::init_a())
|
||||
}
|
||||
|
||||
fn init_b<R: CryptoRng>(_: &mut R) -> Result<Self, crate::Error>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Ok(States::init_b())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl SckaVulnerability for States {
|
||||
fn vulnerable_epochs(&self) -> Vec<Epoch> {
|
||||
vec![self.epoch()]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use crate::test::messaging_scka::GenericMessagingScka;
|
||||
use crate::test::x25519_scka::states;
|
||||
use crate::test::{onlineoffline::OnlineOfflineMessagingBehavior, orchestrator};
|
||||
use crate::Error;
|
||||
use rand::TryRngCore;
|
||||
|
||||
use rand_core::OsRng;
|
||||
|
||||
#[test]
|
||||
fn balanced_healing() -> Result<(), Error> {
|
||||
type Scka = states::States;
|
||||
type Cka = GenericMessagingScka<Scka>;
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
orchestrator::random_healing_test::<Cka, _>(0.5, &mut rng)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_balanced() -> Result<(), Error> {
|
||||
type Scka = states::States;
|
||||
type Cka = GenericMessagingScka<Scka>;
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
orchestrator::random_balanced::<Cka, _>(&mut rng)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chaos() -> Result<(), Error> {
|
||||
type Scka = states::States;
|
||||
type Cka = GenericMessagingScka<Scka>;
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
orchestrator::chaos::<Cka, _>(10000, &mut rng)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn onlineoffline_healing() {
|
||||
type Scka = states::States;
|
||||
type Cka = GenericMessagingScka<Scka>;
|
||||
let mut mp = OnlineOfflineMessagingBehavior::new([0.04, 0.04], [0.05, 0.05]);
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
let hist = orchestrator::controlled_messaging_healing_test::<
|
||||
Cka,
|
||||
OnlineOfflineMessagingBehavior,
|
||||
_,
|
||||
>(&mut mp, 10000, &mut rng)
|
||||
.expect("should run");
|
||||
|
||||
orchestrator::print_histogram(&hist);
|
||||
orchestrator::print_healing_stats(&orchestrator::stats_from_histogram(&hist)[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_balanced_out_of_order() -> Result<(), Error> {
|
||||
type Scka = states::States;
|
||||
type Cka = GenericMessagingScka<Scka>;
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
orchestrator::random_balanced_out_of_order::<Cka, _>(&mut rng)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_slow_alex_healing() -> Result<(), Error> {
|
||||
type Scka = states::States;
|
||||
type Cka = GenericMessagingScka<Scka>;
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
orchestrator::random_healing_test::<Cka, _>(0.33, &mut rng)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// From libcrux-ml-kem/src/constant_time_ops.rs
|
||||
|
||||
/// Return 1 if `value` is not zero and 0 otherwise.
|
||||
fn inz(value: u8) -> u8 {
|
||||
let value = value as u16;
|
||||
|
||||
let result = ((value | (!value).wrapping_add(1)) >> 8) & 1;
|
||||
|
||||
result as u8
|
||||
}
|
||||
|
||||
#[inline(never)] // Don't inline this to avoid that the compiler optimizes this out.
|
||||
fn is_non_zero(value: u8) -> u8 {
|
||||
core::hint::black_box(inz(value))
|
||||
}
|
||||
|
||||
/// Return 1 if the bytes of `lhs` and `rhs` do not exactly
|
||||
/// match and 0 otherwise.
|
||||
#[cfg_attr(hax, hax_lib::requires(
|
||||
lhs.len() == rhs.len()
|
||||
))]
|
||||
pub(crate) fn compare(lhs: &[u8], rhs: &[u8]) -> u8 {
|
||||
let mut r: u8 = 0;
|
||||
|
||||
for i in 0..lhs.len() {
|
||||
r |= lhs[i] ^ rhs[i];
|
||||
}
|
||||
|
||||
is_non_zero(r)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pub(crate) mod chunked;
|
||||
pub(crate) mod unchunked;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::test::messaging_scka::GenericMessagingScka;
|
||||
use crate::test::{onlineoffline::OnlineOfflineMessagingBehavior, orchestrator};
|
||||
use crate::v1::chunked::states;
|
||||
use crate::Error;
|
||||
|
||||
use rand::TryRngCore;
|
||||
use rand_core::OsRng;
|
||||
|
||||
#[test]
|
||||
fn balanced_healing() -> Result<(), Error> {
|
||||
type Scka = states::States;
|
||||
type Cka = GenericMessagingScka<Scka>;
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
orchestrator::random_healing_test::<Cka, _>(0.5, &mut rng)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_balanced() -> Result<(), Error> {
|
||||
type Scka = states::States;
|
||||
type Cka = GenericMessagingScka<Scka>;
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
orchestrator::random_balanced::<Cka, _>(&mut rng)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chaos() -> Result<(), Error> {
|
||||
type Scka = states::States;
|
||||
type Cka = GenericMessagingScka<Scka>;
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
orchestrator::chaos::<Cka, _>(10000, &mut rng)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn onlineoffline_healing_unidir() {
|
||||
type Scka = states::States;
|
||||
type Cka = GenericMessagingScka<Scka>;
|
||||
let mut mp = OnlineOfflineMessagingBehavior::new([0.04, 0.04], [0.05, 0.05]);
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
orchestrator::controlled_messaging_healing_test::<Cka, OnlineOfflineMessagingBehavior, _>(
|
||||
&mut mp, 100000, &mut rng,
|
||||
)
|
||||
.expect("should run");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_balanced_out_of_order() -> Result<(), Error> {
|
||||
type Scka = states::States;
|
||||
type Cka = GenericMessagingScka<Scka>;
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
orchestrator::random_balanced_out_of_order::<Cka, _>(&mut rng)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_slow_alex_healing_auth_bidir() -> Result<(), Error> {
|
||||
type Scka = states::States;
|
||||
type Cka = GenericMessagingScka<Scka>;
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
orchestrator::random_healing_test::<Cka, _>(0.33, &mut rng)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pub(crate) mod send_ct;
|
||||
pub(crate) mod send_ek;
|
||||
pub(crate) mod states;
|
||||
@@ -0,0 +1,303 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
mod serialize;
|
||||
|
||||
use super::send_ek;
|
||||
use crate::encoding::polynomial;
|
||||
use crate::encoding::{Chunk, Decoder, Encoder};
|
||||
use crate::v1::unchunked::send_ct as unchunked;
|
||||
use crate::{authenticator, incremental_mlkem768};
|
||||
use crate::{Epoch, EpochSecret, Error};
|
||||
use rand::{CryptoRng, Rng};
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
#[hax_lib::attributes]
|
||||
pub struct NoHeaderReceived {
|
||||
pub(super) uc: unchunked::NoHeaderReceived,
|
||||
// `receiving_hdr` only decodes messages of length `incremental_mlkem768::HEADER_SIZE + authenticator::Authenticator::MACSIZE`
|
||||
#[hax_lib::refine(receiving_hdr.get_pts_needed() == (incremental_mlkem768::HEADER_SIZE + authenticator::Authenticator::MACSIZE) / 2)]
|
||||
pub(super) receiving_hdr: polynomial::PolyDecoder,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
#[hax_lib::attributes]
|
||||
pub struct HeaderReceived {
|
||||
uc: unchunked::HeaderReceived,
|
||||
// `receiving_ek` only decodes messages of length `incremental_mlkem768::ENCAPSULATION_KEY_SIZE`
|
||||
#[hax_lib::refine(receiving_ek.get_pts_needed() == incremental_mlkem768::ENCAPSULATION_KEY_SIZE / 2)]
|
||||
receiving_ek: polynomial::PolyDecoder,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
#[hax_lib::attributes]
|
||||
pub struct Ct1Sampled {
|
||||
uc: unchunked::Ct1Sent,
|
||||
sending_ct1: polynomial::PolyEncoder,
|
||||
// `receiving_ek` only decodes messages of length `incremental_mlkem768::ENCAPSULATION_KEY_SIZE`
|
||||
#[hax_lib::refine(receiving_ek.get_pts_needed() == incremental_mlkem768::ENCAPSULATION_KEY_SIZE / 2)]
|
||||
receiving_ek: polynomial::PolyDecoder,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
pub struct EkReceivedCt1Sampled {
|
||||
uc: unchunked::Ct1SentEkReceived,
|
||||
sending_ct1: polynomial::PolyEncoder,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
#[hax_lib::attributes]
|
||||
pub struct Ct1Acknowledged {
|
||||
uc: unchunked::Ct1Sent,
|
||||
// `receiving_ek` only decodes messages of length `incremental_mlkem768::ENCAPSULATION_KEY_SIZE`
|
||||
#[hax_lib::refine(receiving_ek.get_pts_needed() == incremental_mlkem768::ENCAPSULATION_KEY_SIZE / 2)]
|
||||
receiving_ek: polynomial::PolyDecoder,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
pub struct Ct2Sampled {
|
||||
uc: unchunked::Ct2Sent,
|
||||
sending_ct2: polynomial::PolyEncoder,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
pub enum NoHeaderReceivedRecvChunk {
|
||||
StillReceiving(NoHeaderReceived),
|
||||
Done(HeaderReceived),
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl NoHeaderReceived {
|
||||
pub fn new(auth_key: &[u8]) -> Self {
|
||||
let decoder = polynomial::PolyDecoder::new(
|
||||
incremental_mlkem768::HEADER_SIZE + authenticator::Authenticator::MACSIZE,
|
||||
);
|
||||
NoHeaderReceived {
|
||||
uc: unchunked::NoHeaderReceived::new(auth_key),
|
||||
receiving_hdr: decoder.expect("should be able to decode header size"),
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::requires(epoch == self.uc.epoch)]
|
||||
pub fn recv_hdr_chunk(
|
||||
self,
|
||||
epoch: Epoch,
|
||||
chunk: &Chunk,
|
||||
) -> Result<NoHeaderReceivedRecvChunk, Error> {
|
||||
assert_eq!(epoch, self.uc.epoch);
|
||||
let Self {
|
||||
uc,
|
||||
mut receiving_hdr,
|
||||
} = self;
|
||||
receiving_hdr.add_chunk(chunk);
|
||||
if let Some(mut hdr) = receiving_hdr.decoded_message() {
|
||||
let mac: authenticator::Mac = hdr.split_off(incremental_mlkem768::HEADER_SIZE);
|
||||
let receiving_ek =
|
||||
polynomial::PolyDecoder::new(incremental_mlkem768::ENCAPSULATION_KEY_SIZE);
|
||||
Ok(NoHeaderReceivedRecvChunk::Done(HeaderReceived {
|
||||
uc: uc.recv_header(epoch, hdr, &mac)?,
|
||||
receiving_ek: receiving_ek.expect("should be able to decode EncapsulationKey size"),
|
||||
}))
|
||||
} else {
|
||||
Ok(NoHeaderReceivedRecvChunk::StillReceiving(Self {
|
||||
uc,
|
||||
receiving_hdr,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn epoch(&self) -> Epoch {
|
||||
self.uc.epoch
|
||||
}
|
||||
}
|
||||
|
||||
// Once the header has been received, it seems like we could start receiving
|
||||
// EK chunks and should handle that possibility. However, this is not actually
|
||||
// correct, as the send_ek side won't start sending EK chunks until it receives
|
||||
// the first CT0 chunk. Thus, send_ct1_chunk is the only state transition
|
||||
// we need to implement here.
|
||||
impl HeaderReceived {
|
||||
pub fn send_ct1_chunk<R: Rng + CryptoRng>(
|
||||
self,
|
||||
rng: &mut R,
|
||||
) -> (Ct1Sampled, Chunk, EpochSecret) {
|
||||
let Self { uc, receiving_ek } = self;
|
||||
|
||||
let (uc, ct1, epoch_secret) = uc.send_ct1(rng);
|
||||
let encoder = polynomial::PolyEncoder::encode_bytes(&ct1);
|
||||
let mut sending_ct1 = encoder.expect("should be able to send CTSIZE");
|
||||
let chunk = sending_ct1.next_chunk();
|
||||
(
|
||||
Ct1Sampled {
|
||||
uc,
|
||||
sending_ct1,
|
||||
receiving_ek,
|
||||
},
|
||||
chunk,
|
||||
epoch_secret,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn epoch(&self) -> Epoch {
|
||||
self.uc.epoch
|
||||
}
|
||||
}
|
||||
|
||||
// Consider fixing this, but since this is only used as a return value it doesn't take too much memory.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum Ct1SampledRecvChunk {
|
||||
StillReceivingStillSending(Ct1Sampled),
|
||||
StillReceiving(Ct1Acknowledged),
|
||||
StillSending(EkReceivedCt1Sampled),
|
||||
Done(Ct2Sampled),
|
||||
}
|
||||
|
||||
#[hax_lib::requires(ct2.len() == 128 && mac.len() == authenticator::Authenticator::MACSIZE)]
|
||||
fn send_ct2_encoder(ct2: &[u8], mac: &[u8]) -> polynomial::PolyEncoder {
|
||||
let mut msg = ct2.to_vec();
|
||||
msg.extend_from_slice(mac);
|
||||
polynomial::PolyEncoder::encode_bytes(&msg).expect("should be able to send ct2")
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl Ct1Sampled {
|
||||
#[hax_lib::requires(epoch == self.uc.epoch)]
|
||||
pub fn recv_ek_chunk(
|
||||
self,
|
||||
epoch: Epoch,
|
||||
chunk: &Chunk,
|
||||
ct1_ack: bool,
|
||||
) -> Result<Ct1SampledRecvChunk, Error> {
|
||||
let Self {
|
||||
uc,
|
||||
mut receiving_ek,
|
||||
sending_ct1,
|
||||
} = self;
|
||||
receiving_ek.add_chunk(chunk);
|
||||
Ok(if let Some(decoded) = receiving_ek.decoded_message() {
|
||||
let uc = uc.recv_ek(epoch, decoded)?;
|
||||
if ct1_ack {
|
||||
let (uc, ct2, mac) = uc.send_ct2();
|
||||
Ct1SampledRecvChunk::Done(Ct2Sampled {
|
||||
uc,
|
||||
sending_ct2: send_ct2_encoder(&ct2, &mac),
|
||||
})
|
||||
} else {
|
||||
Ct1SampledRecvChunk::StillSending(EkReceivedCt1Sampled { uc, sending_ct1 })
|
||||
}
|
||||
} else if ct1_ack {
|
||||
Ct1SampledRecvChunk::StillReceiving(Ct1Acknowledged { uc, receiving_ek })
|
||||
} else {
|
||||
Ct1SampledRecvChunk::StillReceivingStillSending(Self {
|
||||
uc,
|
||||
receiving_ek,
|
||||
sending_ct1,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send_ct1_chunk(self) -> (Ct1Sampled, Chunk) {
|
||||
let Self {
|
||||
uc,
|
||||
mut sending_ct1,
|
||||
receiving_ek,
|
||||
} = self;
|
||||
let chunk = sending_ct1.next_chunk();
|
||||
(
|
||||
Ct1Sampled {
|
||||
uc,
|
||||
sending_ct1,
|
||||
receiving_ek,
|
||||
},
|
||||
chunk,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn epoch(&self) -> Epoch {
|
||||
self.uc.epoch
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl EkReceivedCt1Sampled {
|
||||
pub fn send_ct1_chunk(self) -> (EkReceivedCt1Sampled, Chunk) {
|
||||
let Self {
|
||||
uc,
|
||||
mut sending_ct1,
|
||||
} = self;
|
||||
let chunk = sending_ct1.next_chunk();
|
||||
(EkReceivedCt1Sampled { uc, sending_ct1 }, chunk)
|
||||
}
|
||||
|
||||
#[hax_lib::requires(epoch ==self.uc.epoch)]
|
||||
pub fn recv_ct1_ack(self, epoch: Epoch) -> Ct2Sampled {
|
||||
assert_eq!(epoch, self.uc.epoch);
|
||||
let (uc, ct2, mac) = self.uc.send_ct2();
|
||||
Ct2Sampled {
|
||||
uc,
|
||||
sending_ct2: send_ct2_encoder(&ct2, &mac),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn epoch(&self) -> Epoch {
|
||||
self.uc.epoch
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum Ct1AcknowledgedRecvChunk {
|
||||
StillReceiving(Ct1Acknowledged),
|
||||
Done(Ct2Sampled),
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl Ct1Acknowledged {
|
||||
#[hax_lib::requires(epoch ==self.uc.epoch)]
|
||||
pub fn recv_ek_chunk(
|
||||
self,
|
||||
epoch: Epoch,
|
||||
chunk: &Chunk,
|
||||
) -> Result<Ct1AcknowledgedRecvChunk, Error> {
|
||||
let Self {
|
||||
uc,
|
||||
mut receiving_ek,
|
||||
} = self;
|
||||
receiving_ek.add_chunk(chunk);
|
||||
Ok(if let Some(decoded) = receiving_ek.decoded_message() {
|
||||
let uc = uc.recv_ek(epoch, decoded)?;
|
||||
let (uc, ct2, mac) = uc.send_ct2();
|
||||
Ct1AcknowledgedRecvChunk::Done(Ct2Sampled {
|
||||
uc,
|
||||
sending_ct2: send_ct2_encoder(&ct2, &mac),
|
||||
})
|
||||
} else {
|
||||
Ct1AcknowledgedRecvChunk::StillReceiving(Self { uc, receiving_ek })
|
||||
})
|
||||
}
|
||||
|
||||
pub fn epoch(&self) -> Epoch {
|
||||
self.uc.epoch
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl Ct2Sampled {
|
||||
pub fn send_ct2_chunk(self) -> (Ct2Sampled, Chunk) {
|
||||
let Self {
|
||||
uc,
|
||||
mut sending_ct2,
|
||||
} = self;
|
||||
let chunk = sending_ct2.next_chunk();
|
||||
(Self { uc, sending_ct2 }, chunk)
|
||||
}
|
||||
|
||||
#[hax_lib::requires(self.uc.epoch < u64::MAX && epoch == self.uc.epoch + 1)]
|
||||
pub fn recv_next_epoch(self, epoch: Epoch) -> send_ek::KeysUnsampled {
|
||||
let uc = self.uc.recv_next_epoch(epoch);
|
||||
send_ek::KeysUnsampled { uc }
|
||||
}
|
||||
|
||||
pub fn epoch(&self) -> Epoch {
|
||||
self.uc.epoch
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
use super::*;
|
||||
use crate::encoding::polynomial;
|
||||
use crate::proto::pq_ratchet as pqrpb;
|
||||
use crate::v1::unchunked;
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl NoHeaderReceived {
|
||||
pub fn into_pb(self) -> pqrpb::v1_state::chunked::NoHeaderReceived {
|
||||
pqrpb::v1_state::chunked::NoHeaderReceived {
|
||||
uc: Some(self.uc.into_pb()),
|
||||
receiving_hdr: Some(self.receiving_hdr.into_pb()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: pqrpb::v1_state::chunked::NoHeaderReceived) -> Result<Self, Error> {
|
||||
if let Some(rhdr) = &pb.receiving_hdr {
|
||||
if rhdr.pts_needed
|
||||
!= ((crate::incremental_mlkem768::HEADER_SIZE
|
||||
+ crate::authenticator::Authenticator::MACSIZE)
|
||||
/ 2) as u32
|
||||
{
|
||||
return Err(Error::MsgDecode);
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
uc: unchunked::send_ct::NoHeaderReceived::from_pb(pb.uc.ok_or(Error::StateDecode)?)?,
|
||||
receiving_hdr: polynomial::PolyDecoder::from_pb(
|
||||
pb.receiving_hdr.ok_or(Error::StateDecode)?,
|
||||
)
|
||||
.map_err(|_| Error::StateDecode)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HeaderReceived {
|
||||
pub fn into_pb(self) -> pqrpb::v1_state::chunked::HeaderReceived {
|
||||
pqrpb::v1_state::chunked::HeaderReceived {
|
||||
uc: Some(self.uc.into_pb()),
|
||||
receiving_ek: Some(self.receiving_ek.into_pb()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: pqrpb::v1_state::chunked::HeaderReceived) -> Result<Self, Error> {
|
||||
if let Some(d) = &pb.receiving_ek {
|
||||
if d.pts_needed as usize != crate::incremental_mlkem768::ENCAPSULATION_KEY_SIZE / 2 {
|
||||
return Err(Error::MsgDecode);
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
uc: unchunked::send_ct::HeaderReceived::from_pb(pb.uc.ok_or(Error::StateDecode)?)?,
|
||||
receiving_ek: polynomial::PolyDecoder::from_pb(
|
||||
pb.receiving_ek.ok_or(Error::StateDecode)?,
|
||||
)
|
||||
.map_err(|_| Error::StateDecode)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Ct1Sampled {
|
||||
pub fn into_pb(self) -> pqrpb::v1_state::chunked::Ct1Sampled {
|
||||
pqrpb::v1_state::chunked::Ct1Sampled {
|
||||
uc: Some(self.uc.into_pb()),
|
||||
sending_ct1: Some(self.sending_ct1.into_pb()),
|
||||
receiving_ek: Some(self.receiving_ek.into_pb()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: pqrpb::v1_state::chunked::Ct1Sampled) -> Result<Self, Error> {
|
||||
if let Some(d) = &pb.receiving_ek {
|
||||
if d.pts_needed as usize != crate::incremental_mlkem768::ENCAPSULATION_KEY_SIZE / 2 {
|
||||
return Err(Error::MsgDecode);
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
uc: unchunked::send_ct::Ct1Sent::from_pb(pb.uc.ok_or(Error::StateDecode)?)?,
|
||||
sending_ct1: polynomial::PolyEncoder::from_pb(
|
||||
pb.sending_ct1.ok_or(Error::StateDecode)?,
|
||||
)
|
||||
.map_err(|_| Error::StateDecode)?,
|
||||
receiving_ek: polynomial::PolyDecoder::from_pb(
|
||||
pb.receiving_ek.ok_or(Error::StateDecode)?,
|
||||
)
|
||||
.map_err(|_| Error::StateDecode)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EkReceivedCt1Sampled {
|
||||
pub fn into_pb(self) -> pqrpb::v1_state::chunked::EkReceivedCt1Sampled {
|
||||
pqrpb::v1_state::chunked::EkReceivedCt1Sampled {
|
||||
uc: Some(self.uc.into_pb()),
|
||||
sending_ct1: Some(self.sending_ct1.into_pb()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: pqrpb::v1_state::chunked::EkReceivedCt1Sampled) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
uc: unchunked::send_ct::Ct1SentEkReceived::from_pb(pb.uc.ok_or(Error::StateDecode)?)?,
|
||||
sending_ct1: polynomial::PolyEncoder::from_pb(
|
||||
pb.sending_ct1.ok_or(Error::StateDecode)?,
|
||||
)
|
||||
.map_err(|_| Error::StateDecode)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Ct1Acknowledged {
|
||||
pub fn into_pb(self) -> pqrpb::v1_state::chunked::Ct1Acknowledged {
|
||||
pqrpb::v1_state::chunked::Ct1Acknowledged {
|
||||
uc: Some(self.uc.into_pb()),
|
||||
receiving_ek: Some(self.receiving_ek.into_pb()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: pqrpb::v1_state::chunked::Ct1Acknowledged) -> Result<Self, Error> {
|
||||
if let Some(d) = &pb.receiving_ek {
|
||||
if d.pts_needed as usize != crate::incremental_mlkem768::ENCAPSULATION_KEY_SIZE / 2 {
|
||||
return Err(Error::MsgDecode);
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
uc: unchunked::send_ct::Ct1Sent::from_pb(pb.uc.ok_or(Error::StateDecode)?)?,
|
||||
receiving_ek: polynomial::PolyDecoder::from_pb(
|
||||
pb.receiving_ek.ok_or(Error::StateDecode)?,
|
||||
)
|
||||
.map_err(|_| Error::StateDecode)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Ct2Sampled {
|
||||
pub fn into_pb(self) -> pqrpb::v1_state::chunked::Ct2Sampled {
|
||||
pqrpb::v1_state::chunked::Ct2Sampled {
|
||||
uc: Some(self.uc.into_pb()),
|
||||
sending_ct2: Some(self.sending_ct2.into_pb()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: pqrpb::v1_state::chunked::Ct2Sampled) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
uc: unchunked::send_ct::Ct2Sent::from_pb(pb.uc.ok_or(Error::StateDecode)?)?,
|
||||
sending_ct2: polynomial::PolyEncoder::from_pb(
|
||||
pb.sending_ct2.ok_or(Error::StateDecode)?,
|
||||
)
|
||||
.map_err(|_| Error::StateDecode)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
mod serialize;
|
||||
|
||||
use super::send_ct;
|
||||
use crate::authenticator;
|
||||
use crate::encoding::polynomial;
|
||||
use crate::encoding::{Chunk, Decoder, Encoder};
|
||||
use crate::incremental_mlkem768;
|
||||
use crate::v1::unchunked::send_ek as unchunked;
|
||||
use crate::{Epoch, EpochSecret, Error};
|
||||
use rand::{CryptoRng, Rng};
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
pub struct KeysUnsampled {
|
||||
pub(super) uc: unchunked::KeysUnsampled,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
pub struct KeysSampled {
|
||||
uc: unchunked::HeaderSent,
|
||||
sending_hdr: polynomial::PolyEncoder,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
#[hax_lib::attributes]
|
||||
pub struct HeaderSent {
|
||||
uc: unchunked::EkSent,
|
||||
sending_ek: polynomial::PolyEncoder,
|
||||
// `receiving_ct1` only decodes messages of length `incremental_mlkem768::CIPHERTEXT1_SIZE`
|
||||
#[hax_lib::refine(receiving_ct1.pts_needed == incremental_mlkem768::CIPHERTEXT1_SIZE / 2)]
|
||||
receiving_ct1: polynomial::PolyDecoder,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
pub struct Ct1Received {
|
||||
uc: unchunked::EkSentCt1Received,
|
||||
sending_ek: polynomial::PolyEncoder,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
#[hax_lib::attributes]
|
||||
pub struct EkSentCt1Received {
|
||||
uc: unchunked::EkSentCt1Received,
|
||||
// `receiving_ct2` only decodes messages of length `incremental_mlkem768::CIPHERTEXT2_SIZE + authenticator::Authenticator::MACSIZE`
|
||||
#[hax_lib::refine(receiving_ct2.pts_needed == (incremental_mlkem768::CIPHERTEXT2_SIZE + authenticator::Authenticator::MACSIZE) / 2)]
|
||||
receiving_ct2: polynomial::PolyDecoder,
|
||||
}
|
||||
|
||||
impl KeysUnsampled {
|
||||
pub fn new(auth_key: &[u8]) -> Self {
|
||||
Self {
|
||||
uc: unchunked::KeysUnsampled::new(auth_key),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_hdr_chunk<R: Rng + CryptoRng>(self, rng: &mut R) -> (KeysSampled, Chunk) {
|
||||
let (uc, mut to_send, mut mac) = self.uc.send_header(rng);
|
||||
to_send.append(&mut mac);
|
||||
let encoder = polynomial::PolyEncoder::encode_bytes(&to_send);
|
||||
let mut sending_hdr = encoder.expect("should be able to encode header size");
|
||||
let chunk = sending_hdr.next_chunk();
|
||||
(KeysSampled { uc, sending_hdr }, chunk)
|
||||
}
|
||||
|
||||
pub fn epoch(&self) -> Epoch {
|
||||
self.uc.epoch
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl KeysSampled {
|
||||
pub fn send_hdr_chunk(self) -> (KeysSampled, Chunk) {
|
||||
let Self {
|
||||
uc,
|
||||
mut sending_hdr,
|
||||
} = self;
|
||||
let chunk = sending_hdr.next_chunk();
|
||||
(KeysSampled { uc, sending_hdr }, chunk)
|
||||
}
|
||||
|
||||
#[hax_lib::requires(epoch == self.uc.epoch)]
|
||||
pub fn recv_ct1_chunk(self, epoch: Epoch, chunk: &Chunk) -> HeaderSent {
|
||||
assert_eq!(epoch, self.uc.epoch);
|
||||
let decoder = polynomial::PolyDecoder::new(incremental_mlkem768::CIPHERTEXT1_SIZE);
|
||||
let mut receiving_ct1 = decoder.expect("should be able to decode header size");
|
||||
receiving_ct1.add_chunk(chunk);
|
||||
let (uc, ek) = self.uc.send_ek();
|
||||
let encoder = polynomial::PolyEncoder::encode_bytes(&ek);
|
||||
let sending_ek = encoder.expect("should be able to send ek");
|
||||
HeaderSent {
|
||||
uc,
|
||||
receiving_ct1,
|
||||
sending_ek,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn epoch(&self) -> Epoch {
|
||||
self.uc.epoch
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum HeaderSentRecvChunk {
|
||||
StillReceiving(HeaderSent),
|
||||
Done(Ct1Received),
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl HeaderSent {
|
||||
pub fn send_ek_chunk(self) -> (HeaderSent, Chunk) {
|
||||
let Self {
|
||||
uc,
|
||||
mut sending_ek,
|
||||
receiving_ct1,
|
||||
} = self;
|
||||
let chunk = sending_ek.next_chunk();
|
||||
(
|
||||
HeaderSent {
|
||||
uc,
|
||||
sending_ek,
|
||||
receiving_ct1,
|
||||
},
|
||||
chunk,
|
||||
)
|
||||
}
|
||||
|
||||
#[hax_lib::requires(epoch == self.uc.epoch)]
|
||||
pub fn recv_ct1_chunk(self, epoch: Epoch, chunk: &Chunk) -> HeaderSentRecvChunk {
|
||||
assert_eq!(epoch, self.uc.epoch);
|
||||
let Self {
|
||||
uc,
|
||||
sending_ek,
|
||||
mut receiving_ct1,
|
||||
} = self;
|
||||
receiving_ct1.add_chunk(chunk);
|
||||
if let Some(decoded) = receiving_ct1.decoded_message() {
|
||||
let uc = uc.recv_ct1(epoch, decoded);
|
||||
HeaderSentRecvChunk::Done(Ct1Received { uc, sending_ek })
|
||||
} else {
|
||||
HeaderSentRecvChunk::StillReceiving(HeaderSent {
|
||||
uc,
|
||||
sending_ek,
|
||||
receiving_ct1,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn epoch(&self) -> Epoch {
|
||||
self.uc.epoch
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl Ct1Received {
|
||||
pub fn send_ek_chunk(self) -> (Ct1Received, Chunk) {
|
||||
let Self { uc, mut sending_ek } = self;
|
||||
let chunk = sending_ek.next_chunk();
|
||||
(Ct1Received { uc, sending_ek }, chunk)
|
||||
}
|
||||
|
||||
#[hax_lib::requires(epoch == self.uc.epoch)]
|
||||
pub fn recv_ct2_chunk(self, epoch: Epoch, chunk: &Chunk) -> EkSentCt1Received {
|
||||
assert_eq!(epoch, self.uc.epoch);
|
||||
hax_lib::assert!(
|
||||
(incremental_mlkem768::CIPHERTEXT2_SIZE + authenticator::Authenticator::MACSIZE) % 2
|
||||
== 0
|
||||
);
|
||||
let decoder = polynomial::PolyDecoder::new(
|
||||
incremental_mlkem768::CIPHERTEXT2_SIZE + authenticator::Authenticator::MACSIZE,
|
||||
);
|
||||
let mut receiving_ct2 = decoder.expect("should be able to decode ct2+mac size");
|
||||
receiving_ct2.add_chunk(chunk);
|
||||
EkSentCt1Received {
|
||||
uc: self.uc,
|
||||
receiving_ct2,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn epoch(&self) -> Epoch {
|
||||
self.uc.epoch
|
||||
}
|
||||
}
|
||||
|
||||
pub enum EkSentCt1ReceivedRecvChunk {
|
||||
StillReceiving(EkSentCt1Received),
|
||||
Done((send_ct::NoHeaderReceived, EpochSecret)),
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl EkSentCt1Received {
|
||||
#[hax_lib::requires(epoch == self.uc.epoch)]
|
||||
pub fn recv_ct2_chunk(
|
||||
self,
|
||||
epoch: Epoch,
|
||||
chunk: &Chunk,
|
||||
) -> Result<EkSentCt1ReceivedRecvChunk, Error> {
|
||||
assert_eq!(epoch, self.uc.epoch);
|
||||
let Self {
|
||||
uc,
|
||||
mut receiving_ct2,
|
||||
} = self;
|
||||
receiving_ct2.add_chunk(chunk);
|
||||
if let Some(mut ct2) = receiving_ct2.decoded_message() {
|
||||
let mac: authenticator::Mac = ct2.split_off(incremental_mlkem768::CIPHERTEXT2_SIZE);
|
||||
let (uc, sec) = uc.recv_ct2(ct2, mac)?;
|
||||
let decoder = polynomial::PolyDecoder::new(
|
||||
incremental_mlkem768::HEADER_SIZE + authenticator::Authenticator::MACSIZE,
|
||||
);
|
||||
Ok(EkSentCt1ReceivedRecvChunk::Done((
|
||||
send_ct::NoHeaderReceived {
|
||||
uc,
|
||||
receiving_hdr: decoder.expect("should be able to decode header size"),
|
||||
},
|
||||
sec,
|
||||
)))
|
||||
} else {
|
||||
Ok(EkSentCt1ReceivedRecvChunk::StillReceiving(
|
||||
EkSentCt1Received { uc, receiving_ct2 },
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn epoch(&self) -> Epoch {
|
||||
self.uc.epoch
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
use super::*;
|
||||
use crate::encoding::polynomial;
|
||||
use crate::proto::pq_ratchet as pqrpb;
|
||||
use crate::v1::unchunked;
|
||||
|
||||
impl KeysUnsampled {
|
||||
pub fn into_pb(self) -> pqrpb::v1_state::chunked::KeysUnsampled {
|
||||
pqrpb::v1_state::chunked::KeysUnsampled {
|
||||
uc: Some(self.uc.into_pb()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: pqrpb::v1_state::chunked::KeysUnsampled) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
uc: unchunked::send_ek::KeysUnsampled::from_pb(pb.uc.ok_or(Error::StateDecode)?)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl KeysSampled {
|
||||
pub fn into_pb(self) -> pqrpb::v1_state::chunked::KeysSampled {
|
||||
pqrpb::v1_state::chunked::KeysSampled {
|
||||
uc: Some(self.uc.into_pb()),
|
||||
sending_hdr: Some(self.sending_hdr.into_pb()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: pqrpb::v1_state::chunked::KeysSampled) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
uc: unchunked::send_ek::HeaderSent::from_pb(pb.uc.ok_or(Error::StateDecode)?)?,
|
||||
sending_hdr: polynomial::PolyEncoder::from_pb(
|
||||
pb.sending_hdr.ok_or(Error::StateDecode)?,
|
||||
)
|
||||
.map_err(|_| Error::StateDecode)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl HeaderSent {
|
||||
pub fn into_pb(self) -> pqrpb::v1_state::chunked::HeaderSent {
|
||||
pqrpb::v1_state::chunked::HeaderSent {
|
||||
uc: Some(self.uc.into_pb()),
|
||||
sending_ek: Some(self.sending_ek.into_pb()),
|
||||
receiving_ct1: Some(self.receiving_ct1.into_pb()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: pqrpb::v1_state::chunked::HeaderSent) -> Result<Self, Error> {
|
||||
if let Some(d) = &pb.receiving_ct1 {
|
||||
if d.pts_needed as usize != crate::incremental_mlkem768::CIPHERTEXT1_SIZE / 2 {
|
||||
return Err(Error::MsgDecode);
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
uc: unchunked::send_ek::EkSent::from_pb(pb.uc.ok_or(Error::StateDecode)?)?,
|
||||
sending_ek: polynomial::PolyEncoder::from_pb(pb.sending_ek.ok_or(Error::StateDecode)?)
|
||||
.map_err(|_| Error::StateDecode)?,
|
||||
receiving_ct1: polynomial::PolyDecoder::from_pb(
|
||||
pb.receiving_ct1.ok_or(Error::StateDecode)?,
|
||||
)
|
||||
.map_err(|_| Error::StateDecode)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Ct1Received {
|
||||
pub fn into_pb(self) -> pqrpb::v1_state::chunked::Ct1Received {
|
||||
pqrpb::v1_state::chunked::Ct1Received {
|
||||
uc: Some(self.uc.into_pb()),
|
||||
sending_ek: Some(self.sending_ek.into_pb()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: pqrpb::v1_state::chunked::Ct1Received) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
uc: unchunked::send_ek::EkSentCt1Received::from_pb(pb.uc.ok_or(Error::StateDecode)?)?,
|
||||
sending_ek: polynomial::PolyEncoder::from_pb(pb.sending_ek.ok_or(Error::StateDecode)?)
|
||||
.map_err(|_| Error::StateDecode)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EkSentCt1Received {
|
||||
pub fn into_pb(self) -> pqrpb::v1_state::chunked::EkSentCt1Received {
|
||||
pqrpb::v1_state::chunked::EkSentCt1Received {
|
||||
uc: Some(self.uc.into_pb()),
|
||||
receiving_ct2: Some(self.receiving_ct2.into_pb()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: pqrpb::v1_state::chunked::EkSentCt1Received) -> Result<Self, Error> {
|
||||
if let Some(d) = &pb.receiving_ct2 {
|
||||
if d.pts_needed as usize
|
||||
!= (incremental_mlkem768::CIPHERTEXT2_SIZE + authenticator::Authenticator::MACSIZE)
|
||||
/ 2
|
||||
{
|
||||
return Err(Error::MsgDecode);
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
uc: unchunked::send_ek::EkSentCt1Received::from_pb(pb.uc.ok_or(Error::StateDecode)?)?,
|
||||
receiving_ct2: polynomial::PolyDecoder::from_pb(
|
||||
pb.receiving_ct2.ok_or(Error::StateDecode)?,
|
||||
)
|
||||
.map_err(|_| Error::StateDecode)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
mod serialize;
|
||||
|
||||
use super::send_ct;
|
||||
use super::send_ek;
|
||||
use crate::encoding::Chunk;
|
||||
use crate::{EpochSecret, Error};
|
||||
use rand::{CryptoRng, Rng};
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::Epoch;
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
pub enum States {
|
||||
KeysUnsampled(send_ek::KeysUnsampled),
|
||||
KeysSampled(send_ek::KeysSampled),
|
||||
HeaderSent(send_ek::HeaderSent),
|
||||
Ct1Received(send_ek::Ct1Received),
|
||||
EkSentCt1Received(send_ek::EkSentCt1Received),
|
||||
|
||||
NoHeaderReceived(send_ct::NoHeaderReceived),
|
||||
HeaderReceived(send_ct::HeaderReceived),
|
||||
Ct1Sampled(send_ct::Ct1Sampled),
|
||||
EkReceivedCt1Sampled(send_ct::EkReceivedCt1Sampled),
|
||||
Ct1Acknowledged(send_ct::Ct1Acknowledged),
|
||||
Ct2Sampled(send_ct::Ct2Sampled),
|
||||
}
|
||||
|
||||
pub enum MessagePayload {
|
||||
None,
|
||||
Hdr(Chunk),
|
||||
Ek(Chunk),
|
||||
EkCt1Ack(Chunk),
|
||||
Ct1Ack(bool),
|
||||
Ct1(Chunk),
|
||||
Ct2(Chunk),
|
||||
}
|
||||
|
||||
pub struct Message {
|
||||
pub epoch: Epoch,
|
||||
pub payload: MessagePayload,
|
||||
}
|
||||
|
||||
pub struct Send {
|
||||
pub msg: Message,
|
||||
pub key: Option<EpochSecret>,
|
||||
pub state: States,
|
||||
}
|
||||
|
||||
pub struct Recv {
|
||||
pub key: Option<EpochSecret>,
|
||||
pub state: States,
|
||||
}
|
||||
|
||||
impl States {
|
||||
pub(crate) fn init_a(auth_key: &[u8]) -> Self {
|
||||
Self::KeysUnsampled(send_ek::KeysUnsampled::new(auth_key))
|
||||
}
|
||||
|
||||
pub(crate) fn init_b(auth_key: &[u8]) -> Self {
|
||||
Self::NoHeaderReceived(send_ct::NoHeaderReceived::new(auth_key))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn vulnerable_epochs(&self) -> Vec<Epoch> {
|
||||
match self {
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// send_ek
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
States::KeysUnsampled(_) => vec![],
|
||||
States::KeysSampled(state) => vec![state.epoch()],
|
||||
States::HeaderSent(state) => vec![state.epoch()],
|
||||
States::Ct1Received(state) => vec![state.epoch()],
|
||||
States::EkSentCt1Received(state) => vec![state.epoch()],
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// send_ct
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
States::NoHeaderReceived(_) => vec![],
|
||||
States::HeaderReceived(_) => vec![],
|
||||
States::Ct1Sampled(state) => vec![state.epoch()],
|
||||
States::EkReceivedCt1Sampled(state) => vec![state.epoch()],
|
||||
States::Ct1Acknowledged(state) => vec![state.epoch()],
|
||||
States::Ct2Sampled(state) => vec![state.epoch()],
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(test)]
|
||||
pub(crate) fn last_emitted_epoch(&self) -> Epoch {
|
||||
match self {
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// send_ek
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
States::KeysUnsampled(state) => state.epoch() - 1,
|
||||
States::KeysSampled(state) => state.epoch() - 1,
|
||||
States::HeaderSent(state) => state.epoch() - 1,
|
||||
States::Ct1Received(state) => state.epoch() - 1,
|
||||
States::EkSentCt1Received(state) => state.epoch() - 1,
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// send_ct
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
States::NoHeaderReceived(state) => state.epoch() - 1,
|
||||
States::HeaderReceived(state) => state.epoch() - 1,
|
||||
States::Ct1Sampled(state) => state.epoch() - 1,
|
||||
States::EkReceivedCt1Sampled(state) => state.epoch() - 1,
|
||||
States::Ct1Acknowledged(state) => state.epoch() - 1,
|
||||
States::Ct2Sampled(state) => state.epoch() - 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn send<R: Rng + CryptoRng>(self, rng: &mut R) -> Result<Send, Error> {
|
||||
match self {
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// send_ek
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
Self::KeysUnsampled(state) => {
|
||||
let epoch = state.epoch();
|
||||
let (state, chunk) = state.send_hdr_chunk(rng);
|
||||
|
||||
#[cfg(not(hax))]
|
||||
log::info!(
|
||||
"spqr v1.send_ek.send epoch {}: KeysUnsampled -> KeysSampled",
|
||||
epoch
|
||||
);
|
||||
Ok(Send {
|
||||
state: Self::KeysSampled(state),
|
||||
msg: Message {
|
||||
epoch,
|
||||
payload: MessagePayload::Hdr(chunk),
|
||||
},
|
||||
key: None,
|
||||
})
|
||||
}
|
||||
Self::KeysSampled(state) => {
|
||||
let epoch = state.epoch();
|
||||
let (state, chunk) = state.send_hdr_chunk();
|
||||
Ok(Send {
|
||||
state: Self::KeysSampled(state),
|
||||
msg: Message {
|
||||
epoch,
|
||||
payload: MessagePayload::Hdr(chunk),
|
||||
},
|
||||
key: None,
|
||||
})
|
||||
}
|
||||
Self::HeaderSent(state) => {
|
||||
let epoch = state.epoch();
|
||||
let (state, chunk) = state.send_ek_chunk();
|
||||
Ok(Send {
|
||||
state: Self::HeaderSent(state),
|
||||
msg: Message {
|
||||
epoch,
|
||||
payload: MessagePayload::Ek(chunk),
|
||||
},
|
||||
key: None,
|
||||
})
|
||||
}
|
||||
Self::Ct1Received(state) => {
|
||||
let epoch = state.epoch();
|
||||
let (state, chunk) = state.send_ek_chunk();
|
||||
|
||||
Ok(Send {
|
||||
state: Self::Ct1Received(state),
|
||||
msg: Message {
|
||||
epoch,
|
||||
payload: MessagePayload::EkCt1Ack(chunk),
|
||||
},
|
||||
key: None,
|
||||
})
|
||||
}
|
||||
Self::EkSentCt1Received(state) => {
|
||||
let epoch = state.epoch();
|
||||
|
||||
Ok(Send {
|
||||
state: Self::EkSentCt1Received(state),
|
||||
msg: Message {
|
||||
epoch,
|
||||
payload: MessagePayload::Ct1Ack(true),
|
||||
},
|
||||
key: None,
|
||||
})
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// send_ct
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
Self::NoHeaderReceived(state) => {
|
||||
let epoch = state.epoch();
|
||||
|
||||
Ok(Send {
|
||||
state: Self::NoHeaderReceived(state),
|
||||
msg: Message {
|
||||
epoch,
|
||||
payload: MessagePayload::None,
|
||||
},
|
||||
key: None,
|
||||
})
|
||||
}
|
||||
Self::HeaderReceived(state) => {
|
||||
let epoch = state.epoch();
|
||||
let (state, chunk, epoch_secret) = state.send_ct1_chunk(rng);
|
||||
|
||||
#[cfg(not(hax))]
|
||||
log::info!(
|
||||
"spqr v1.send_ct.send epoch {}: HeaderReceived -> Ct1Sampled",
|
||||
epoch
|
||||
);
|
||||
Ok(Send {
|
||||
state: Self::Ct1Sampled(state),
|
||||
msg: Message {
|
||||
epoch,
|
||||
payload: MessagePayload::Ct1(chunk),
|
||||
},
|
||||
key: Some(epoch_secret),
|
||||
})
|
||||
}
|
||||
Self::Ct1Sampled(state) => {
|
||||
let epoch = state.epoch();
|
||||
let (state, chunk) = state.send_ct1_chunk();
|
||||
|
||||
Ok(Send {
|
||||
state: Self::Ct1Sampled(state),
|
||||
msg: Message {
|
||||
epoch,
|
||||
payload: MessagePayload::Ct1(chunk),
|
||||
},
|
||||
key: None,
|
||||
})
|
||||
}
|
||||
Self::EkReceivedCt1Sampled(state) => {
|
||||
let epoch = state.epoch();
|
||||
let (state, chunk) = state.send_ct1_chunk();
|
||||
|
||||
Ok(Send {
|
||||
state: Self::EkReceivedCt1Sampled(state),
|
||||
msg: Message {
|
||||
epoch,
|
||||
payload: MessagePayload::Ct1(chunk),
|
||||
},
|
||||
key: None,
|
||||
})
|
||||
}
|
||||
Self::Ct1Acknowledged(state) => {
|
||||
let epoch = state.epoch();
|
||||
|
||||
Ok(Send {
|
||||
state: Self::Ct1Acknowledged(state),
|
||||
msg: Message {
|
||||
epoch,
|
||||
payload: MessagePayload::None,
|
||||
},
|
||||
key: None,
|
||||
})
|
||||
}
|
||||
Self::Ct2Sampled(state) => {
|
||||
let epoch = state.epoch();
|
||||
let (state, chunk) = state.send_ct2_chunk();
|
||||
|
||||
Ok(Send {
|
||||
state: Self::Ct2Sampled(state),
|
||||
msg: Message {
|
||||
epoch,
|
||||
payload: MessagePayload::Ct2(chunk),
|
||||
},
|
||||
key: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn recv(self, msg: &Message) -> Result<Recv, Error> {
|
||||
// println!("send_ct recv msg: {:?}", msg);
|
||||
let mut key = None;
|
||||
let state = match self {
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// send_ek
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
Self::KeysUnsampled(state) => match msg.epoch.cmp(&state.epoch()) {
|
||||
Ordering::Greater => {
|
||||
return Err(Error::EpochOutOfRange(msg.epoch));
|
||||
}
|
||||
Ordering::Less => Self::KeysUnsampled(state),
|
||||
Ordering::Equal => Self::KeysUnsampled(state),
|
||||
},
|
||||
Self::KeysSampled(state) => match msg.epoch.cmp(&state.epoch()) {
|
||||
Ordering::Greater => {
|
||||
return Err(Error::EpochOutOfRange(msg.epoch));
|
||||
}
|
||||
Ordering::Less => Self::KeysSampled(state),
|
||||
Ordering::Equal => {
|
||||
if let MessagePayload::Ct1(ref chunk) = msg.payload {
|
||||
#[cfg(not(hax))]
|
||||
log::info!(
|
||||
"spqr v1.send_ek.recv epoch {}: KeysSampled -> HeaderSent",
|
||||
msg.epoch
|
||||
);
|
||||
Self::HeaderSent(state.recv_ct1_chunk(msg.epoch, chunk))
|
||||
} else {
|
||||
Self::KeysSampled(state)
|
||||
}
|
||||
}
|
||||
},
|
||||
Self::HeaderSent(state) => match msg.epoch.cmp(&state.epoch()) {
|
||||
Ordering::Greater => {
|
||||
return Err(Error::EpochOutOfRange(msg.epoch));
|
||||
}
|
||||
Ordering::Less => Self::HeaderSent(state),
|
||||
Ordering::Equal => {
|
||||
if let MessagePayload::Ct1(ref chunk) = msg.payload {
|
||||
match state.recv_ct1_chunk(msg.epoch, chunk) {
|
||||
send_ek::HeaderSentRecvChunk::StillReceiving(state) => {
|
||||
Self::HeaderSent(state)
|
||||
}
|
||||
send_ek::HeaderSentRecvChunk::Done(state) => {
|
||||
#[cfg(not(hax))]
|
||||
log::info!(
|
||||
"spqr v1.send_ek.recv epoch {}: HeaderSent -> Ct1Received",
|
||||
msg.epoch
|
||||
);
|
||||
Self::Ct1Received(state)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Self::HeaderSent(state)
|
||||
}
|
||||
}
|
||||
},
|
||||
Self::Ct1Received(state) => match msg.epoch.cmp(&state.epoch()) {
|
||||
Ordering::Greater => {
|
||||
return Err(Error::EpochOutOfRange(msg.epoch));
|
||||
}
|
||||
Ordering::Less => Self::Ct1Received(state),
|
||||
Ordering::Equal => {
|
||||
if let MessagePayload::Ct2(ref chunk) = msg.payload {
|
||||
#[cfg(not(hax))]
|
||||
log::info!(
|
||||
"spqr v1.send_ek.recv epoch {}: Ct1Received -> EkSentCt1Received",
|
||||
msg.epoch
|
||||
);
|
||||
Self::EkSentCt1Received(state.recv_ct2_chunk(msg.epoch, chunk))
|
||||
} else {
|
||||
Self::Ct1Received(state)
|
||||
}
|
||||
}
|
||||
},
|
||||
Self::EkSentCt1Received(state) => match msg.epoch.cmp(&state.epoch()) {
|
||||
Ordering::Greater => {
|
||||
return Err(Error::EpochOutOfRange(msg.epoch));
|
||||
}
|
||||
Ordering::Less => Self::EkSentCt1Received(state),
|
||||
Ordering::Equal => {
|
||||
if let MessagePayload::Ct2(ref chunk) = msg.payload {
|
||||
match state.recv_ct2_chunk(msg.epoch, chunk)? {
|
||||
send_ek::EkSentCt1ReceivedRecvChunk::StillReceiving(state) => {
|
||||
Self::EkSentCt1Received(state)
|
||||
}
|
||||
send_ek::EkSentCt1ReceivedRecvChunk::Done((state, sec)) => {
|
||||
#[cfg(not(hax))]
|
||||
log::info!(
|
||||
"spqr v1.send_ek.recv epoch {}->{}: EkSentCt1Received -> NoHeaderReceived",
|
||||
msg.epoch, msg.epoch+1
|
||||
);
|
||||
key = Some(sec);
|
||||
Self::NoHeaderReceived(state)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Self::EkSentCt1Received(state)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// send_ct
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
Self::NoHeaderReceived(state) => match msg.epoch.cmp(&state.epoch()) {
|
||||
Ordering::Greater => {
|
||||
return Err(Error::EpochOutOfRange(msg.epoch));
|
||||
}
|
||||
Ordering::Less => Self::NoHeaderReceived(state),
|
||||
Ordering::Equal => {
|
||||
if let MessagePayload::Hdr(ref chunk) = msg.payload {
|
||||
match state.recv_hdr_chunk(msg.epoch, chunk)? {
|
||||
send_ct::NoHeaderReceivedRecvChunk::StillReceiving(state) => {
|
||||
Self::NoHeaderReceived(state)
|
||||
}
|
||||
send_ct::NoHeaderReceivedRecvChunk::Done(state) => {
|
||||
#[cfg(not(hax))]
|
||||
log::info!(
|
||||
"spqr v1.send_ct.recv epoch {}: NoHeaderReceived -> HeaderReceived",
|
||||
msg.epoch
|
||||
);
|
||||
Self::HeaderReceived(state)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Self::NoHeaderReceived(state)
|
||||
}
|
||||
}
|
||||
},
|
||||
Self::HeaderReceived(state) => match msg.epoch.cmp(&state.epoch()) {
|
||||
Ordering::Greater => {
|
||||
return Err(Error::EpochOutOfRange(msg.epoch));
|
||||
}
|
||||
Ordering::Less => Self::HeaderReceived(state),
|
||||
Ordering::Equal => Self::HeaderReceived(state),
|
||||
},
|
||||
Self::Ct1Sampled(state) => match msg.epoch.cmp(&state.epoch()) {
|
||||
Ordering::Greater => {
|
||||
return Err(Error::EpochOutOfRange(msg.epoch));
|
||||
}
|
||||
Ordering::Less => Self::Ct1Sampled(state),
|
||||
Ordering::Equal => {
|
||||
let (chunk, ack) = match msg.payload {
|
||||
MessagePayload::Ek(ref chunk) => (Some(chunk), false),
|
||||
MessagePayload::EkCt1Ack(ref chunk) => (Some(chunk), true),
|
||||
_ => (None, false),
|
||||
};
|
||||
if let Some(chunk) = chunk {
|
||||
match state.recv_ek_chunk(msg.epoch, chunk, ack)? {
|
||||
send_ct::Ct1SampledRecvChunk::StillReceivingStillSending(state) => {
|
||||
Self::Ct1Sampled(state)
|
||||
}
|
||||
send_ct::Ct1SampledRecvChunk::StillReceiving(state) => {
|
||||
#[cfg(not(hax))]
|
||||
log::info!(
|
||||
"spqr v1.send_ct.recv epoch {}: Ct1Sampled -> Ct1Acknowledged",
|
||||
msg.epoch
|
||||
);
|
||||
Self::Ct1Acknowledged(state)
|
||||
}
|
||||
send_ct::Ct1SampledRecvChunk::StillSending(state) => {
|
||||
#[cfg(not(hax))]
|
||||
log::info!(
|
||||
"spqr v1.send_ct.recv epoch {}: Ct1Sampled -> EkReceivedCt1Sampled",
|
||||
msg.epoch
|
||||
);
|
||||
Self::EkReceivedCt1Sampled(state)
|
||||
}
|
||||
send_ct::Ct1SampledRecvChunk::Done(state) => {
|
||||
#[cfg(not(hax))]
|
||||
log::info!(
|
||||
"spqr v1.send_ct.recv epoch {}: Ct1Sampled -> Ct2Sampled",
|
||||
msg.epoch
|
||||
);
|
||||
Self::Ct2Sampled(state)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Self::Ct1Sampled(state)
|
||||
}
|
||||
}
|
||||
},
|
||||
Self::EkReceivedCt1Sampled(state) => match msg.epoch.cmp(&state.epoch()) {
|
||||
Ordering::Greater => {
|
||||
return Err(Error::EpochOutOfRange(msg.epoch));
|
||||
}
|
||||
Ordering::Less => Self::EkReceivedCt1Sampled(state),
|
||||
Ordering::Equal => {
|
||||
if matches!(
|
||||
msg.payload,
|
||||
MessagePayload::Ct1Ack(true) | MessagePayload::EkCt1Ack(_)
|
||||
) {
|
||||
#[cfg(not(hax))]
|
||||
log::info!(
|
||||
"spqr v1.send_ct.recv epoch {}: EkReceivedCt1Sampled -> Ct2Sampled",
|
||||
msg.epoch
|
||||
);
|
||||
Self::Ct2Sampled(state.recv_ct1_ack(msg.epoch))
|
||||
} else {
|
||||
Self::EkReceivedCt1Sampled(state)
|
||||
}
|
||||
}
|
||||
},
|
||||
Self::Ct1Acknowledged(state) => match msg.epoch.cmp(&state.epoch()) {
|
||||
Ordering::Greater => {
|
||||
return Err(Error::EpochOutOfRange(msg.epoch));
|
||||
}
|
||||
Ordering::Less => Self::Ct1Acknowledged(state),
|
||||
Ordering::Equal => {
|
||||
// If we got all messages in order, we would never receive a msg.ek at
|
||||
// this point, since we already got our first msg.ek_ct1_ack. However,
|
||||
// we can get messages out of order, so let's use the msg.ek chunks if we
|
||||
// get them.
|
||||
let chunk = match msg.payload {
|
||||
MessagePayload::Ek(ref chunk) => Some(chunk),
|
||||
MessagePayload::EkCt1Ack(ref chunk) => Some(chunk),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(chunk) = chunk {
|
||||
match state.recv_ek_chunk(msg.epoch, chunk)? {
|
||||
send_ct::Ct1AcknowledgedRecvChunk::StillReceiving(state) => {
|
||||
Self::Ct1Acknowledged(state)
|
||||
}
|
||||
send_ct::Ct1AcknowledgedRecvChunk::Done(state) => {
|
||||
#[cfg(not(hax))]
|
||||
log::info!(
|
||||
"spqr v1.send_ct.recv epoch {}: Ct1Acknowledged -> Ct2Sampled",
|
||||
msg.epoch
|
||||
);
|
||||
Self::Ct2Sampled(state)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Self::Ct1Acknowledged(state)
|
||||
}
|
||||
}
|
||||
},
|
||||
Self::Ct2Sampled(state) => match msg.epoch.cmp(&state.epoch()) {
|
||||
Ordering::Greater => {
|
||||
if msg.epoch == state.epoch() + 1 {
|
||||
#[cfg(not(hax))]
|
||||
log::info!(
|
||||
"spqr v1.send_ct.recv epoch {}->{}: Ct2Sampled -> KeysSampled",
|
||||
msg.epoch - 1,
|
||||
msg.epoch
|
||||
);
|
||||
Self::KeysUnsampled(state.recv_next_epoch(msg.epoch))
|
||||
} else {
|
||||
return Err(Error::EpochOutOfRange(msg.epoch));
|
||||
}
|
||||
}
|
||||
Ordering::Less => Self::Ct2Sampled(state),
|
||||
Ordering::Equal => Self::Ct2Sampled(state),
|
||||
},
|
||||
};
|
||||
Ok(Recv { state, key })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
use std::cmp::min;
|
||||
|
||||
use super::*;
|
||||
use crate::proto::pq_ratchet as pqrpb;
|
||||
use crate::{Error, SerializedMessage, Version};
|
||||
use num_enum::IntoPrimitive;
|
||||
|
||||
impl States {
|
||||
pub fn into_pb(self) -> pqrpb::V1State {
|
||||
pqrpb::V1State {
|
||||
inner_state: Some(match self {
|
||||
// send_ek
|
||||
Self::KeysUnsampled(state) => {
|
||||
pqrpb::v1_state::InnerState::KeysUnsampled(state.into_pb())
|
||||
}
|
||||
Self::KeysSampled(state) => {
|
||||
pqrpb::v1_state::InnerState::KeysSampled(state.into_pb())
|
||||
}
|
||||
Self::HeaderSent(state) => pqrpb::v1_state::InnerState::HeaderSent(state.into_pb()),
|
||||
Self::Ct1Received(state) => {
|
||||
pqrpb::v1_state::InnerState::Ct1Received(state.into_pb())
|
||||
}
|
||||
Self::EkSentCt1Received(state) => {
|
||||
pqrpb::v1_state::InnerState::EkSentCt1Received(state.into_pb())
|
||||
}
|
||||
|
||||
// send_ct
|
||||
Self::NoHeaderReceived(state) => {
|
||||
pqrpb::v1_state::InnerState::NoHeaderReceived(state.into_pb())
|
||||
}
|
||||
Self::HeaderReceived(state) => {
|
||||
pqrpb::v1_state::InnerState::HeaderReceived(state.into_pb())
|
||||
}
|
||||
Self::Ct1Sampled(state) => pqrpb::v1_state::InnerState::Ct1Sampled(state.into_pb()),
|
||||
Self::EkReceivedCt1Sampled(state) => {
|
||||
pqrpb::v1_state::InnerState::EkReceivedCt1Sampled(state.into_pb())
|
||||
}
|
||||
Self::Ct1Acknowledged(state) => {
|
||||
pqrpb::v1_state::InnerState::Ct1Acknowledged(state.into_pb())
|
||||
}
|
||||
Self::Ct2Sampled(state) => pqrpb::v1_state::InnerState::Ct2Sampled(state.into_pb()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: pqrpb::V1State) -> Result<Self, Error> {
|
||||
Ok(match pb.inner_state {
|
||||
// send_ek
|
||||
Some(pqrpb::v1_state::InnerState::KeysUnsampled(pb)) => {
|
||||
Self::KeysUnsampled(send_ek::KeysUnsampled::from_pb(pb)?)
|
||||
}
|
||||
Some(pqrpb::v1_state::InnerState::KeysSampled(pb)) => {
|
||||
Self::KeysSampled(send_ek::KeysSampled::from_pb(pb)?)
|
||||
}
|
||||
Some(pqrpb::v1_state::InnerState::HeaderSent(pb)) => {
|
||||
Self::HeaderSent(send_ek::HeaderSent::from_pb(pb)?)
|
||||
}
|
||||
Some(pqrpb::v1_state::InnerState::Ct1Received(pb)) => {
|
||||
Self::Ct1Received(send_ek::Ct1Received::from_pb(pb)?)
|
||||
}
|
||||
Some(pqrpb::v1_state::InnerState::EkSentCt1Received(pb)) => {
|
||||
Self::EkSentCt1Received(send_ek::EkSentCt1Received::from_pb(pb)?)
|
||||
}
|
||||
|
||||
// send_ct
|
||||
Some(pqrpb::v1_state::InnerState::NoHeaderReceived(pb)) => {
|
||||
Self::NoHeaderReceived(send_ct::NoHeaderReceived::from_pb(pb)?)
|
||||
}
|
||||
Some(pqrpb::v1_state::InnerState::HeaderReceived(pb)) => {
|
||||
Self::HeaderReceived(send_ct::HeaderReceived::from_pb(pb)?)
|
||||
}
|
||||
Some(pqrpb::v1_state::InnerState::Ct1Sampled(pb)) => {
|
||||
Self::Ct1Sampled(send_ct::Ct1Sampled::from_pb(pb)?)
|
||||
}
|
||||
Some(pqrpb::v1_state::InnerState::EkReceivedCt1Sampled(pb)) => {
|
||||
Self::EkReceivedCt1Sampled(send_ct::EkReceivedCt1Sampled::from_pb(pb)?)
|
||||
}
|
||||
Some(pqrpb::v1_state::InnerState::Ct1Acknowledged(pb)) => {
|
||||
Self::Ct1Acknowledged(send_ct::Ct1Acknowledged::from_pb(pb)?)
|
||||
}
|
||||
Some(pqrpb::v1_state::InnerState::Ct2Sampled(pb)) => {
|
||||
Self::Ct2Sampled(send_ct::Ct2Sampled::from_pb(pb)?)
|
||||
}
|
||||
|
||||
_ => {
|
||||
return Err(Error::StateDecode);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoPrimitive)]
|
||||
#[repr(u8)]
|
||||
enum MessageType {
|
||||
None = 0,
|
||||
Hdr = 1,
|
||||
Ek = 2,
|
||||
EkCt1Ack = 3,
|
||||
Ct1Ack = 4,
|
||||
Ct1 = 5,
|
||||
Ct2 = 6,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for MessageType {
|
||||
type Error = String;
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(MessageType::None),
|
||||
1 => Ok(MessageType::Hdr),
|
||||
2 => Ok(MessageType::Ek),
|
||||
3 => Ok(MessageType::EkCt1Ack),
|
||||
4 => Ok(MessageType::Ct1Ack),
|
||||
5 => Ok(MessageType::Ct1),
|
||||
6 => Ok(MessageType::Ct2),
|
||||
_ => Err("Expected a number between 0 and 6".to_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageType {
|
||||
fn from_payload(mp: &MessagePayload) -> Self {
|
||||
match mp {
|
||||
MessagePayload::None => Self::None,
|
||||
MessagePayload::Hdr(_) => Self::Hdr,
|
||||
MessagePayload::Ek(_) => Self::Ek,
|
||||
MessagePayload::EkCt1Ack(_) => Self::EkCt1Ack,
|
||||
MessagePayload::Ct1Ack(_) => Self::Ct1Ack,
|
||||
MessagePayload::Ct1(_) => Self::Ct1,
|
||||
MessagePayload::Ct2(_) => Self::Ct2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_VARINT_BYTES_LEN: usize = 10;
|
||||
|
||||
fn encode_varint(mut a: u64, into: &mut SerializedMessage) {
|
||||
for _i in 0..MAX_VARINT_BYTES_LEN {
|
||||
let byte = (a & 0x7F) as u8;
|
||||
if a < 0x80 {
|
||||
into.push(byte);
|
||||
break;
|
||||
} else {
|
||||
into.push(0x80 | byte);
|
||||
a >>= 7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::ensures(|res| *at <= *future(at) && if res.is_ok() { *at < from.len() && *future(at) <= from.len() } else { true })]
|
||||
fn decode_varint(from: &SerializedMessage, at: &mut usize) -> Result<u64, Error> {
|
||||
let mut out = 0u64;
|
||||
|
||||
let mut i: usize = 0;
|
||||
// Helps prevent return in while loop for Hax
|
||||
let mut done = false;
|
||||
let start_at: usize = *at;
|
||||
if start_at >= from.len() {
|
||||
return Err(Error::MsgDecode);
|
||||
}
|
||||
|
||||
let max_i = min(MAX_VARINT_BYTES_LEN, from.len() - start_at);
|
||||
|
||||
while i < max_i && !done {
|
||||
hax_lib::loop_invariant!(i <= max_i && *at == start_at);
|
||||
hax_lib::loop_decreases!(max_i - i);
|
||||
|
||||
let byte = from[start_at + i];
|
||||
out |= ((byte as u64) & 0x7f) << (7 * i as i32);
|
||||
|
||||
i += 1;
|
||||
done = (byte & 0x80) == 0;
|
||||
}
|
||||
|
||||
if done {
|
||||
*at += i;
|
||||
Ok(out)
|
||||
} else {
|
||||
Err(Error::MsgDecode)
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_chunk(c: &Chunk, into: &mut SerializedMessage) {
|
||||
encode_varint(c.index as u64, into);
|
||||
hax_lib::assume!(into.len() < usize::MAX - 32);
|
||||
into.extend_from_slice(&c.data[..]);
|
||||
}
|
||||
|
||||
fn decode_chunk(from: &SerializedMessage, at: &mut usize) -> Result<Chunk, Error> {
|
||||
let index = decode_varint(from, at)?;
|
||||
let start = *at;
|
||||
hax_lib::assume!(*at < usize::MAX - 32);
|
||||
*at += 32;
|
||||
if *at > from.len() || index > 65535 {
|
||||
return Err(Error::MsgDecode);
|
||||
}
|
||||
Ok(Chunk {
|
||||
index: index as u16,
|
||||
data: from[start..*at].try_into().expect("correct size"),
|
||||
})
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl Message {
|
||||
/// Serialize a message.
|
||||
///
|
||||
/// Messages are serialized as:
|
||||
///
|
||||
/// [version] - 1 byte
|
||||
/// [epoch] - varint, 1-10 bytes
|
||||
/// [index] - varint, 1-5 bytes
|
||||
/// [message_type] - 1 byte
|
||||
///
|
||||
/// Many of the message types also have a data chunk concatenated to them, of
|
||||
/// the form:
|
||||
///
|
||||
/// [index] - varint, 1-3 bytes
|
||||
/// [chunk_data] - 32 bytes
|
||||
#[hax_lib::ensures(|res| res.len() > 0 && res[0] == Version::V1.into())]
|
||||
pub fn serialize(&self, index: u32) -> SerializedMessage {
|
||||
let mut into = Vec::with_capacity(40);
|
||||
into.push(Version::V1.into());
|
||||
encode_varint(self.epoch, &mut into);
|
||||
encode_varint(index as u64, &mut into);
|
||||
into.push(MessageType::from_payload(&self.payload).into());
|
||||
encode_chunk(
|
||||
match &self.payload {
|
||||
MessagePayload::Hdr(ref chunk) => chunk,
|
||||
MessagePayload::Ek(ref chunk) => chunk,
|
||||
MessagePayload::EkCt1Ack(ref chunk) => chunk,
|
||||
MessagePayload::Ct1(ref chunk) => chunk,
|
||||
MessagePayload::Ct2(ref chunk) => chunk,
|
||||
_ => {
|
||||
// This assumption could be proven with post-conditions on encode_varint
|
||||
hax_lib::assume!(into.len() > 0 && into[0] == Version::V1.into());
|
||||
return into;
|
||||
}
|
||||
},
|
||||
&mut into,
|
||||
);
|
||||
// This assumption could be proven with post-conditions on encode_varint and encode_chunk
|
||||
hax_lib::assume!(into.len() > 0 && into[0] == Version::V1.into());
|
||||
into
|
||||
}
|
||||
|
||||
#[hax_lib::ensures(|res| if let Ok((msg, _index, at)) = res { msg.epoch > 0 && at <= from.len() } else { true })]
|
||||
pub fn deserialize(from: &SerializedMessage) -> Result<(Self, u32, usize), Error> {
|
||||
if from.is_empty() || from[0] != Version::V1.into() {
|
||||
return Err(Error::MsgDecode);
|
||||
}
|
||||
let mut at = 1usize;
|
||||
let epoch = decode_varint(from, &mut at)? as Epoch;
|
||||
if epoch == 0 {
|
||||
return Err(Error::MsgDecode);
|
||||
}
|
||||
let index: u32 = decode_varint(from, &mut at)?
|
||||
.try_into()
|
||||
.map_err(|_| Error::MsgDecode)?;
|
||||
if at >= from.len() {
|
||||
return Err(Error::MsgDecode);
|
||||
}
|
||||
let msg_type = MessageType::try_from(from[at]).map_err(|_| Error::MsgDecode)?;
|
||||
at += 1;
|
||||
let payload = match msg_type {
|
||||
MessageType::None => MessagePayload::None,
|
||||
MessageType::Ct1Ack => MessagePayload::Ct1Ack(true),
|
||||
MessageType::Hdr => MessagePayload::Hdr(decode_chunk(from, &mut at)?),
|
||||
MessageType::Ek => MessagePayload::Ek(decode_chunk(from, &mut at)?),
|
||||
MessageType::EkCt1Ack => MessagePayload::EkCt1Ack(decode_chunk(from, &mut at)?),
|
||||
MessageType::Ct1 => MessagePayload::Ct1(decode_chunk(from, &mut at)?),
|
||||
MessageType::Ct2 => MessagePayload::Ct2(decode_chunk(from, &mut at)?),
|
||||
};
|
||||
// We allow for there to be additional trailing data in the message, so it's
|
||||
// possible that `at < from.len()`. This allows for us to potentially
|
||||
// upgrade sessions in future versions of the protocol.
|
||||
Ok((Self { epoch, payload }, index, at))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{decode_varint, encode_varint};
|
||||
use rand::RngCore;
|
||||
use rand::TryRngCore;
|
||||
use rand_core::OsRng;
|
||||
|
||||
#[test]
|
||||
fn encoding_varint() {
|
||||
let mut v = vec![];
|
||||
encode_varint(0x012C, &mut v);
|
||||
assert_eq!(&v, &[0xAC, 0x02][..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decoding_varint() {
|
||||
let v = vec![0xFF, 0xAC, 0x02, 0xFF];
|
||||
let mut at = 1usize;
|
||||
assert_eq!(0x012C, decode_varint(&v, &mut at).unwrap());
|
||||
assert_eq!(at, 3, "at <= v.len()");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decoding_varint_zero() {
|
||||
let v = vec![0x00];
|
||||
let mut at = 0usize;
|
||||
assert_eq!(0x0, decode_varint(&v, &mut at).unwrap());
|
||||
assert_eq!(at, 1, "at <= v.len()");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_varint() {
|
||||
let mut rng = OsRng.unwrap_err();
|
||||
for _i in 0..10000 {
|
||||
let u = rng.next_u64();
|
||||
let mut v = vec![];
|
||||
encode_varint(u, &mut v);
|
||||
let mut at = 0usize;
|
||||
assert_eq!(u, decode_varint(&v, &mut at).unwrap());
|
||||
assert_eq!(at, v.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
pub(crate) mod send_ct;
|
||||
pub(crate) mod send_ek;
|
||||
@@ -0,0 +1,209 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
mod serialize;
|
||||
|
||||
use super::send_ek;
|
||||
use crate::authenticator;
|
||||
use crate::incremental_mlkem768;
|
||||
use crate::kdf;
|
||||
use crate::{Epoch, EpochSecret, Error};
|
||||
use rand::{CryptoRng, Rng};
|
||||
|
||||
// START (epoch = 1)
|
||||
// │
|
||||
// ┌───────▼───────────┐
|
||||
// ┌─────► NoHeaderReceived │
|
||||
// │ └───────┬───────────┘
|
||||
// │ │
|
||||
// │ │recv_header
|
||||
// │ │
|
||||
// │ ┌───────▼───────────┐
|
||||
// │ │ HeaderReceived │
|
||||
// │ └───────┬───────────┘
|
||||
// │ │
|
||||
// │ │send_ct1
|
||||
// │ │
|
||||
// │ ┌───────▼───────────┐
|
||||
// recv_next_epoch│ │ Ct1Sent │
|
||||
// (epoch += 1) │ └───────┬───────────┘
|
||||
// │ │
|
||||
// │ │recv_ek
|
||||
// │ │
|
||||
// │ ┌───────▼───────────┐
|
||||
// │ │ Ct1SentEkReceived │
|
||||
// │ └───────┬───────────┘
|
||||
// │ │
|
||||
// │ │send_ct2
|
||||
// │ │
|
||||
// │ ┌───────▼───────────┐
|
||||
// └─────┤ Ct2Sent │
|
||||
// └───────────────────┘
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
pub struct NoHeaderReceived {
|
||||
pub epoch: Epoch,
|
||||
pub(super) auth: authenticator::Authenticator,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
#[hax_lib::attributes]
|
||||
pub struct HeaderReceived {
|
||||
pub epoch: Epoch,
|
||||
auth: authenticator::Authenticator,
|
||||
#[hax_lib::refine(hdr.len() == 64)]
|
||||
hdr: incremental_mlkem768::Header,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
#[hax_lib::attributes]
|
||||
pub struct Ct1Sent {
|
||||
pub epoch: Epoch,
|
||||
auth: authenticator::Authenticator,
|
||||
#[hax_lib::refine(hdr.len() == 64)]
|
||||
hdr: incremental_mlkem768::Header,
|
||||
#[hax_lib::refine(es.len() == 2080)]
|
||||
es: incremental_mlkem768::EncapsulationState,
|
||||
#[hax_lib::refine(ct1.len() == 960)]
|
||||
ct1: incremental_mlkem768::Ciphertext1,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
#[hax_lib::attributes]
|
||||
pub struct Ct1SentEkReceived {
|
||||
pub epoch: Epoch,
|
||||
auth: authenticator::Authenticator,
|
||||
#[hax_lib::refine(es.len() == 2080)]
|
||||
es: incremental_mlkem768::EncapsulationState,
|
||||
#[hax_lib::refine(ek.len() == 1152)]
|
||||
ek: incremental_mlkem768::EncapsulationKey,
|
||||
#[hax_lib::refine(ct1.len() == 960)]
|
||||
ct1: incremental_mlkem768::Ciphertext1,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
pub struct Ct2Sent {
|
||||
pub epoch: Epoch,
|
||||
auth: authenticator::Authenticator,
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl NoHeaderReceived {
|
||||
pub fn new(auth_key: &[u8]) -> Self {
|
||||
Self {
|
||||
epoch: 1,
|
||||
auth: authenticator::Authenticator::new(auth_key.to_vec(), 1),
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::requires(epoch == self.epoch && hdr.len() == 64 && mac.len() == authenticator::Authenticator::MACSIZE)]
|
||||
pub fn recv_header(
|
||||
self,
|
||||
epoch: Epoch,
|
||||
hdr: incremental_mlkem768::Header,
|
||||
mac: &authenticator::Mac,
|
||||
) -> Result<HeaderReceived, Error> {
|
||||
assert_eq!(epoch, self.epoch);
|
||||
self.auth.verify_hdr(self.epoch, &hdr, mac)?;
|
||||
Ok(HeaderReceived {
|
||||
epoch: self.epoch,
|
||||
auth: self.auth,
|
||||
hdr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl HeaderReceived {
|
||||
#[hax_lib::requires(self.hdr.len() == 64)]
|
||||
#[hax_lib::ensures(|(_, ct1, _)| ct1.len() == 960)]
|
||||
pub fn send_ct1<R: Rng + CryptoRng>(
|
||||
self,
|
||||
rng: &mut R,
|
||||
) -> (Ct1Sent, incremental_mlkem768::Ciphertext1, EpochSecret) {
|
||||
let Self {
|
||||
epoch,
|
||||
mut auth,
|
||||
hdr,
|
||||
} = self;
|
||||
let (ct1, es, secret) = incremental_mlkem768::encaps1(&hdr, rng);
|
||||
let info = [
|
||||
b"Signal_PQCKA_V1_MLKEM768:SCKA Key",
|
||||
epoch.to_be_bytes().as_slice(),
|
||||
]
|
||||
.concat();
|
||||
let secret = kdf::hkdf_to_vec(&[0u8; 32], &secret, &info, 32);
|
||||
auth.update(epoch, &secret);
|
||||
(
|
||||
Ct1Sent {
|
||||
epoch,
|
||||
auth,
|
||||
hdr,
|
||||
es,
|
||||
ct1: ct1.clone(),
|
||||
},
|
||||
ct1,
|
||||
EpochSecret { secret, epoch },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl Ct1Sent {
|
||||
#[hax_lib::requires(epoch == self.epoch && ek.len() == 1152)]
|
||||
pub fn recv_ek(
|
||||
self,
|
||||
epoch: Epoch,
|
||||
ek: incremental_mlkem768::EncapsulationKey,
|
||||
) -> Result<Ct1SentEkReceived, Error> {
|
||||
assert_eq!(epoch, self.epoch);
|
||||
if incremental_mlkem768::ek_matches_header(&ek, &self.hdr) {
|
||||
Ok(Ct1SentEkReceived {
|
||||
epoch: self.epoch,
|
||||
auth: self.auth,
|
||||
ek,
|
||||
es: self.es,
|
||||
ct1: self.ct1,
|
||||
})
|
||||
} else {
|
||||
Err(Error::ErroneousDataReceived)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl Ct1SentEkReceived {
|
||||
#[hax_lib::ensures(|(_, ct2, mac)| ct2.len() == 128 && mac.len() == authenticator::Authenticator::MACSIZE)]
|
||||
pub fn send_ct2(
|
||||
self,
|
||||
) -> (
|
||||
Ct2Sent,
|
||||
incremental_mlkem768::Ciphertext2,
|
||||
authenticator::Mac,
|
||||
) {
|
||||
let Self {
|
||||
epoch,
|
||||
ek,
|
||||
es,
|
||||
auth,
|
||||
mut ct1,
|
||||
} = self;
|
||||
let ct2 = incremental_mlkem768::encaps2(&ek, &es);
|
||||
ct1.extend_from_slice(&ct2);
|
||||
let mac = auth.mac_ct(epoch, &ct1);
|
||||
(Ct2Sent { epoch, auth }, ct2, mac)
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl Ct2Sent {
|
||||
#[hax_lib::requires(self.epoch < u64::MAX && next_epoch == self.epoch + 1)]
|
||||
pub fn recv_next_epoch(self, next_epoch: Epoch) -> send_ek::KeysUnsampled {
|
||||
let Self { epoch, auth } = self;
|
||||
assert_eq!(epoch + 1, next_epoch);
|
||||
send_ek::KeysUnsampled {
|
||||
epoch: epoch + 1,
|
||||
auth,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
use super::*;
|
||||
use crate::authenticator::Authenticator;
|
||||
use crate::proto::pq_ratchet as pqrpb;
|
||||
use crate::Error;
|
||||
|
||||
impl NoHeaderReceived {
|
||||
pub fn into_pb(self) -> pqrpb::v1_state::unchunked::NoHeaderReceived {
|
||||
pqrpb::v1_state::unchunked::NoHeaderReceived {
|
||||
epoch: self.epoch,
|
||||
auth: Some(self.auth.into_pb()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: pqrpb::v1_state::unchunked::NoHeaderReceived) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
epoch: pb.epoch,
|
||||
auth: Authenticator::from_pb(pb.auth.as_ref().ok_or(Error::StateDecode)?),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HeaderReceived {
|
||||
pub fn into_pb(self) -> pqrpb::v1_state::unchunked::HeaderReceived {
|
||||
pqrpb::v1_state::unchunked::HeaderReceived {
|
||||
epoch: self.epoch,
|
||||
auth: Some(self.auth.into_pb()),
|
||||
hdr: self.hdr,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: pqrpb::v1_state::unchunked::HeaderReceived) -> Result<Self, Error> {
|
||||
if pb.hdr.len() == 64 {
|
||||
Ok(Self {
|
||||
epoch: pb.epoch,
|
||||
auth: Authenticator::from_pb(pb.auth.as_ref().ok_or(Error::StateDecode)?),
|
||||
hdr: pb.hdr,
|
||||
})
|
||||
} else {
|
||||
Err(Error::StateDecode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ct1Sent {
|
||||
pub fn into_pb(self) -> pqrpb::v1_state::unchunked::Ct1Sent {
|
||||
pqrpb::v1_state::unchunked::Ct1Sent {
|
||||
epoch: self.epoch,
|
||||
auth: Some(self.auth.into_pb()),
|
||||
hdr: self.hdr,
|
||||
es: self.es,
|
||||
ct1: self.ct1.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: pqrpb::v1_state::unchunked::Ct1Sent) -> Result<Self, Error> {
|
||||
if pb.hdr.len() == 64 && pb.es.len() == 2080 && pb.ct1.len() == 960 {
|
||||
Ok(Self {
|
||||
epoch: pb.epoch,
|
||||
auth: Authenticator::from_pb(pb.auth.as_ref().ok_or(Error::StateDecode)?),
|
||||
hdr: pb.hdr,
|
||||
es: pb.es,
|
||||
ct1: pb.ct1,
|
||||
})
|
||||
} else {
|
||||
Err(Error::StateDecode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ct1SentEkReceived {
|
||||
pub fn into_pb(self) -> pqrpb::v1_state::unchunked::Ct1SentEkReceived {
|
||||
pqrpb::v1_state::unchunked::Ct1SentEkReceived {
|
||||
epoch: self.epoch,
|
||||
auth: Some(self.auth.into_pb()),
|
||||
es: self.es,
|
||||
ek: self.ek,
|
||||
ct1: self.ct1.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: pqrpb::v1_state::unchunked::Ct1SentEkReceived) -> Result<Self, Error> {
|
||||
if pb.es.len() == 2080 && pb.ct1.len() == 960 && pb.ek.len() == 1152 {
|
||||
Ok(Self {
|
||||
epoch: pb.epoch,
|
||||
auth: Authenticator::from_pb(pb.auth.as_ref().ok_or(Error::StateDecode)?),
|
||||
es: pb.es,
|
||||
ek: pb.ek,
|
||||
ct1: pb.ct1,
|
||||
})
|
||||
} else {
|
||||
Err(Error::StateDecode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ct2Sent {
|
||||
pub fn into_pb(self) -> pqrpb::v1_state::unchunked::Ct2Sent {
|
||||
pqrpb::v1_state::unchunked::Ct2Sent {
|
||||
epoch: self.epoch,
|
||||
auth: Some(self.auth.into_pb()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: pqrpb::v1_state::unchunked::Ct2Sent) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
epoch: pb.epoch,
|
||||
auth: Authenticator::from_pb(pb.auth.as_ref().ok_or(Error::StateDecode)?),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
mod serialize;
|
||||
|
||||
use super::send_ct;
|
||||
use crate::authenticator;
|
||||
use crate::incremental_mlkem768;
|
||||
use crate::kdf;
|
||||
use crate::{Epoch, EpochSecret, Error};
|
||||
use rand::{CryptoRng, Rng};
|
||||
|
||||
// START (epoch = 1)
|
||||
// │
|
||||
// ┌─────▼─────────────┐
|
||||
// ┌─────► KeysUnsampled │
|
||||
// │ └─────┬─────────────┘
|
||||
// │ │
|
||||
// │ │send_header
|
||||
// │ │
|
||||
// │ ┌─────▼─────────────┐
|
||||
// │ │ HeaderSent │
|
||||
// │ └─────┬─────────────┘
|
||||
// │ │
|
||||
// recv_ct2│ │send_ek
|
||||
// (epoch += 1)│ │
|
||||
// │ ┌─────▼─────────────┐
|
||||
// │ │ EkSent │
|
||||
// │ └─────┬─────────────┘
|
||||
// │ │
|
||||
// │ │recv_ct1
|
||||
// │ │
|
||||
// │ ┌─────▼─────────────┐
|
||||
// └─────┤ EkSentCt1Received │
|
||||
// └───────────────────┘
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
pub struct KeysUnsampled {
|
||||
pub epoch: Epoch,
|
||||
pub(super) auth: authenticator::Authenticator,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
#[hax_lib::attributes]
|
||||
pub struct HeaderSent {
|
||||
pub epoch: Epoch,
|
||||
auth: authenticator::Authenticator,
|
||||
#[hax_lib::refine(ek.len() == 1152)]
|
||||
ek: incremental_mlkem768::EncapsulationKey,
|
||||
#[hax_lib::refine(dk.len() == 2400)]
|
||||
dk: incremental_mlkem768::DecapsulationKey,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
#[hax_lib::attributes]
|
||||
pub struct EkSent {
|
||||
pub epoch: Epoch,
|
||||
auth: authenticator::Authenticator,
|
||||
#[hax_lib::refine(dk.len() == 2400)]
|
||||
dk: incremental_mlkem768::DecapsulationKey,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
#[hax_lib::attributes]
|
||||
pub struct EkSentCt1Received {
|
||||
pub epoch: Epoch,
|
||||
auth: authenticator::Authenticator,
|
||||
#[hax_lib::refine(dk.len() == 2400)]
|
||||
dk: incremental_mlkem768::DecapsulationKey,
|
||||
#[hax_lib::refine(ct1.len() == 960)]
|
||||
ct1: incremental_mlkem768::Ciphertext1,
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl KeysUnsampled {
|
||||
pub fn new(auth_key: &[u8]) -> Self {
|
||||
Self {
|
||||
epoch: 1,
|
||||
auth: authenticator::Authenticator::new(auth_key.to_vec(), 1),
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::ensures(|(_, hdr, mac)| hdr.len() == incremental_mlkem768::HEADER_SIZE && mac.len() == authenticator::Authenticator::MACSIZE)]
|
||||
pub fn send_header<R: Rng + CryptoRng>(
|
||||
self,
|
||||
rng: &mut R,
|
||||
) -> (HeaderSent, incremental_mlkem768::Header, authenticator::Mac) {
|
||||
let keys = incremental_mlkem768::generate(rng);
|
||||
let mac = self.auth.mac_hdr(self.epoch, &keys.hdr);
|
||||
(
|
||||
HeaderSent {
|
||||
epoch: self.epoch,
|
||||
auth: self.auth,
|
||||
ek: keys.ek,
|
||||
dk: keys.dk,
|
||||
},
|
||||
keys.hdr,
|
||||
mac,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl HeaderSent {
|
||||
#[hax_lib::ensures(|(_, ek)| ek.len() == 1152)]
|
||||
pub fn send_ek(self) -> (EkSent, incremental_mlkem768::EncapsulationKey) {
|
||||
(
|
||||
EkSent {
|
||||
epoch: self.epoch,
|
||||
auth: self.auth,
|
||||
dk: self.dk,
|
||||
},
|
||||
self.ek,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl EkSent {
|
||||
#[hax_lib::requires(epoch == self.epoch && ct1.len() == 960)]
|
||||
pub fn recv_ct1(
|
||||
self,
|
||||
epoch: Epoch,
|
||||
ct1: incremental_mlkem768::Ciphertext1,
|
||||
) -> EkSentCt1Received {
|
||||
assert_eq!(epoch, self.epoch);
|
||||
EkSentCt1Received {
|
||||
epoch: self.epoch,
|
||||
auth: self.auth,
|
||||
dk: self.dk,
|
||||
ct1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[hax_lib::attributes]
|
||||
impl EkSentCt1Received {
|
||||
#[hax_lib::requires(ct2.len() == incremental_mlkem768::CIPHERTEXT2_SIZE && mac.len() == authenticator::Authenticator::MACSIZE)]
|
||||
pub fn recv_ct2(
|
||||
self,
|
||||
ct2: incremental_mlkem768::Ciphertext2,
|
||||
mac: authenticator::Mac,
|
||||
) -> Result<(send_ct::NoHeaderReceived, EpochSecret), Error> {
|
||||
let Self {
|
||||
epoch,
|
||||
mut auth,
|
||||
dk,
|
||||
mut ct1,
|
||||
} = self;
|
||||
let ss = incremental_mlkem768::decaps(&dk, &ct1, &ct2);
|
||||
let info = [
|
||||
b"Signal_PQCKA_V1_MLKEM768:SCKA Key",
|
||||
epoch.to_be_bytes().as_slice(),
|
||||
]
|
||||
.concat();
|
||||
let ss = kdf::hkdf_to_vec(&[0u8; 32], &ss, &info, 32);
|
||||
|
||||
auth.update(epoch, &ss);
|
||||
ct1.extend_from_slice(&ct2);
|
||||
auth.verify_ct(epoch, &ct1, &mac)?;
|
||||
hax_lib::assume!(epoch < u64::MAX);
|
||||
Ok((
|
||||
send_ct::NoHeaderReceived {
|
||||
epoch: epoch + 1,
|
||||
auth,
|
||||
},
|
||||
EpochSecret {
|
||||
secret: ss.to_vec(),
|
||||
epoch,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
use super::*;
|
||||
use crate::authenticator::Authenticator;
|
||||
use crate::proto::pq_ratchet as pqrpb;
|
||||
use crate::Error;
|
||||
|
||||
impl KeysUnsampled {
|
||||
pub fn into_pb(self) -> pqrpb::v1_state::unchunked::KeysUnsampled {
|
||||
pqrpb::v1_state::unchunked::KeysUnsampled {
|
||||
epoch: self.epoch,
|
||||
auth: Some(self.auth.into_pb()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: pqrpb::v1_state::unchunked::KeysUnsampled) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
epoch: pb.epoch,
|
||||
auth: Authenticator::from_pb(pb.auth.as_ref().ok_or(Error::StateDecode)?),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HeaderSent {
|
||||
pub fn into_pb(self) -> pqrpb::v1_state::unchunked::HeaderSent {
|
||||
pqrpb::v1_state::unchunked::HeaderSent {
|
||||
epoch: self.epoch,
|
||||
auth: Some(self.auth.into_pb()),
|
||||
ek: self.ek,
|
||||
dk: self.dk,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: pqrpb::v1_state::unchunked::HeaderSent) -> Result<Self, Error> {
|
||||
if pb.dk.len() == 2400 && pb.ek.len() == 1152 {
|
||||
Ok(Self {
|
||||
epoch: pb.epoch,
|
||||
auth: Authenticator::from_pb(pb.auth.as_ref().ok_or(Error::StateDecode)?),
|
||||
ek: pb.ek,
|
||||
dk: pb.dk,
|
||||
})
|
||||
} else {
|
||||
Err(Error::StateDecode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EkSent {
|
||||
pub fn into_pb(self) -> pqrpb::v1_state::unchunked::EkSent {
|
||||
pqrpb::v1_state::unchunked::EkSent {
|
||||
epoch: self.epoch,
|
||||
auth: Some(self.auth.into_pb()),
|
||||
dk: self.dk,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: pqrpb::v1_state::unchunked::EkSent) -> Result<Self, Error> {
|
||||
if pb.dk.len() == 2400 {
|
||||
Ok(Self {
|
||||
epoch: pb.epoch,
|
||||
auth: Authenticator::from_pb(pb.auth.as_ref().ok_or(Error::StateDecode)?),
|
||||
dk: pb.dk,
|
||||
})
|
||||
} else {
|
||||
Err(Error::StateDecode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EkSentCt1Received {
|
||||
pub fn into_pb(self) -> pqrpb::v1_state::unchunked::EkSentCt1Received {
|
||||
pqrpb::v1_state::unchunked::EkSentCt1Received {
|
||||
epoch: self.epoch,
|
||||
auth: Some(self.auth.into_pb()),
|
||||
dk: self.dk,
|
||||
ct1: self.ct1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pb(pb: pqrpb::v1_state::unchunked::EkSentCt1Received) -> Result<Self, Error> {
|
||||
if pb.dk.len() == 2400 && pb.ct1.len() == 960 {
|
||||
Ok(Self {
|
||||
epoch: pb.epoch,
|
||||
auth: Authenticator::from_pb(pb.auth.as_ref().ok_or(Error::StateDecode)?),
|
||||
dk: pb.dk,
|
||||
ct1: pb.ct1,
|
||||
})
|
||||
} else {
|
||||
Err(Error::StateDecode)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user