use std::io::{self, Read, Write};
use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine};
use super::mldsa::{Signature, SigningKey, VerificationKey, MLDSA87_SIGNATURE_BYTES};
const SIGNED_MAGIC: &[u8] = b"age-signature/v1\n";
pub fn sign_encrypted_file<W: Write>(
encrypted_data: &[u8],
signing_key: &SigningKey,
mut output: W,
) -> io::Result<()> {
output.write_all(encrypted_data)?;
let signature = signing_key.sign(encrypted_data)?;
output.write_all(SIGNED_MAGIC)?;
output.write_all(b"-> ML-DSA-87 ")?;
output.write_all(signing_key.to_public().to_string().as_bytes())?;
output.write_all(b"\n")?;
let sig_b64 = BASE64_STANDARD_NO_PAD.encode(signature.as_bytes());
output.write_all(sig_b64.as_bytes())?;
output.write_all(b"\n")?;
Ok(())
}
pub fn verify_and_extract(
signed_data: &[u8],
expected_verification_key: &VerificationKey,
) -> io::Result<Vec<u8>> {
let magic_pos = signed_data
.windows(SIGNED_MAGIC.len())
.rposition(|window| window == SIGNED_MAGIC)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "not a signed age file"))?;
let encrypted_data = &signed_data[..magic_pos];
let footer = &signed_data[magic_pos + SIGNED_MAGIC.len()..];
let footer_str = std::str::from_utf8(footer)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid signature footer"))?;
let mut lines = footer_str.lines();
let header_line = lines
.next()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing signature header"))?;
if !header_line.starts_with("-> ML-DSA-87 ") {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid signature header",
));
}
let verification_key_str = &header_line[13..]; let verification_key: VerificationKey = verification_key_str.parse().map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
"invalid verification key in signature",
)
})?;
if &verification_key != expected_verification_key {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"verification key mismatch",
));
}
let sig_line = lines
.next()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing signature data"))?;
let sig_bytes = BASE64_STANDARD_NO_PAD
.decode(sig_line)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid signature encoding"))?;
let signature = Signature::from_bytes(sig_bytes)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid signature length"))?;
verification_key.verify(encrypted_data, &signature)?;
Ok(encrypted_data.to_vec())
}
pub fn verify_and_extract_with_key(signed_data: &[u8]) -> io::Result<(Vec<u8>, VerificationKey)> {
let magic_pos = signed_data
.windows(SIGNED_MAGIC.len())
.rposition(|window| window == SIGNED_MAGIC)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "not a signed age file"))?;
let encrypted_data = &signed_data[..magic_pos];
let footer = &signed_data[magic_pos + SIGNED_MAGIC.len()..];
let footer_str = std::str::from_utf8(footer)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid signature footer"))?;
let mut lines = footer_str.lines();
let header_line = lines
.next()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing signature header"))?;
if !header_line.starts_with("-> ML-DSA-87 ") {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid signature header",
));
}
let verification_key_str = &header_line[13..]; let verification_key: VerificationKey = verification_key_str.parse().map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
"invalid verification key in signature",
)
})?;
let sig_line = lines
.next()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing signature data"))?;
let sig_bytes = BASE64_STANDARD_NO_PAD
.decode(sig_line)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid signature encoding"))?;
let signature = Signature::from_bytes(sig_bytes)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid signature length"))?;
verification_key.verify(encrypted_data, &signature)?;
Ok((encrypted_data.to_vec(), verification_key))
}
#[cfg(test)]
mod tests {
use super::super::mldsa::SigningKey;
use super::*;
#[test]
fn sign_and_verify_round_trip() {
let signing_key = SigningKey::generate();
let verification_key = signing_key.to_public();
let encrypted_data = b"fake encrypted age file data";
let mut signed_output = Vec::new();
sign_encrypted_file(encrypted_data, &signing_key, &mut signed_output).unwrap();
let extracted = verify_and_extract(&signed_output, &verification_key).unwrap();
assert_eq!(extracted, encrypted_data);
}
#[test]
fn verify_with_wrong_key_fails() {
let signing_key = SigningKey::generate();
let wrong_key = SigningKey::generate().to_public();
let encrypted_data = b"fake encrypted age file data";
let mut signed_output = Vec::new();
sign_encrypted_file(encrypted_data, &signing_key, &mut signed_output).unwrap();
assert!(verify_and_extract(&signed_output, &wrong_key).is_err());
}
#[test]
fn verify_tampered_data_fails() {
let signing_key = SigningKey::generate();
let verification_key = signing_key.to_public();
let encrypted_data = b"fake encrypted age file data";
let mut signed_output = Vec::new();
sign_encrypted_file(encrypted_data, &signing_key, &mut signed_output).unwrap();
signed_output[10] ^= 0x01;
assert!(verify_and_extract(&signed_output, &verification_key).is_err());
}
#[test]
fn verify_and_extract_with_key_works() {
let signing_key = SigningKey::generate();
let verification_key = signing_key.to_public();
let encrypted_data = b"fake encrypted age file data";
let mut signed_output = Vec::new();
sign_encrypted_file(encrypted_data, &signing_key, &mut signed_output).unwrap();
let (extracted, extracted_key) = verify_and_extract_with_key(&signed_output).unwrap();
assert_eq!(extracted, encrypted_data);
assert_eq!(extracted_key, verification_key);
}
}