use crate::api::{decode_verified, encode_signed};
use crate::dictionaries;
use crate::lab::engine::{Attack, AttackOutcome, Rng};
use crate::pqsign;
const QSG1_PREFIX_LEN: usize = 4 + 1 + 1 + 4;
fn build_qsg1(message: &[u8], signature: &[u8], dict: &crate::dictionary::Dictionary) -> String {
let mut blob = Vec::with_capacity(QSG1_PREFIX_LEN + message.len() + signature.len());
blob.extend_from_slice(b"QSG1");
blob.push(1u8); blob.push(0u8); blob.extend_from_slice(&(message.len() as u32).to_be_bytes());
blob.extend_from_slice(message);
blob.extend_from_slice(signature);
let indices = crate::codec::encode_base_n(&blob, dict.base());
dict.encode(&indices).expect("índices en rango")
}
pub struct ForgeAttack;
impl ForgeAttack {
pub fn new() -> Self {
Self
}
}
impl Default for ForgeAttack {
fn default() -> Self {
Self::new()
}
}
impl Attack for ForgeAttack {
fn name(&self) -> &'static str {
"forge/adaptive"
}
fn step(&mut self, rng: &mut Rng) -> AttackOutcome {
let dict = dictionaries::ascii94();
let (vk1, sk1) = pqsign::generate_keypair();
let (vk2, sk2) = pqsign::generate_keypair();
let message = b"orden firmada del laboratorio";
match rng.below(3) {
0 => {
let sig1 = sk1.sign(message);
let sig2 = sk2.sign(message);
let mut spliced = Vec::with_capacity(pqsign::SIGNATURE_LEN);
spliced.extend_from_slice(&sig1[..pqsign::ED25519_SIG_LEN]);
spliced.extend_from_slice(&sig2[pqsign::ED25519_SIG_LEN..]);
let artifact = build_qsg1(message, &spliced, &dict);
if decode_verified(&artifact, &vk1, &dict).is_ok() {
return AttackOutcome::Breach("frankensignature verificó bajo vk1".into());
}
if decode_verified(&artifact, &vk2, &dict).is_ok() {
return AttackOutcome::Breach("frankensignature verificó bajo vk2".into());
}
AttackOutcome::Advanced
}
1 => {
let artifact = encode_signed(message, &sk1, &dict);
if decode_verified(&artifact, &vk2, &dict).is_ok() {
return AttackOutcome::Breach("firma verificó con clave equivocada".into());
}
AttackOutcome::Advanced
}
_ => {
let artifact = encode_signed(message, &sk1, &dict);
let mut chars: Vec<char> = artifact.chars().collect();
if chars.is_empty() {
return AttackOutcome::NoProgress;
}
let pos = rng.below(chars.len());
let idx = dict.symbol_to_index(chars[pos]).expect("símbolo propio");
let new = dict
.index_to_symbol((idx + 1) % dict.base())
.expect("índice válido");
if new == chars[pos] {
return AttackOutcome::NoProgress;
}
chars[pos] = new;
let mutated: String = chars.into_iter().collect();
if decode_verified(&mutated, &vk1, &dict).is_ok() {
return AttackOutcome::Breach(format!("mutación en pos {pos} verificó"));
}
AttackOutcome::Advanced
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lab::engine::run;
#[test]
fn adaptive_forgery_never_verifies() {
let mut attack = ForgeAttack::new();
let report = run(&mut attack, 1337, 90);
assert!(report.attempts > 0);
assert!(
report.is_clean(),
"ninguna falsificación debe verificar: {:?}",
report.breaches
);
}
}