use crate::error::{EngramError, Result};
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use sha2::{Digest, Sha256};
use std::io::{Read, Seek, SeekFrom, Write};
pub const SIGNATURE_BLOCK_MAGIC: [u8; 4] = [0x53, 0x49, 0x47, 0x4E];
pub const SIGNATURE_BLOCK_SIZE: usize = 104;
pub const ALGORITHM_ED25519: u8 = 0x01;
#[derive(Debug, Clone)]
pub struct SignatureBlock {
pub public_key: [u8; 32],
pub signature: [u8; 64],
}
impl SignatureBlock {
pub fn sign_archive<S: Read + Seek>(file: &mut S, signing_key: &SigningKey) -> Result<Self> {
let content_end = file.stream_position()?;
file.seek(SeekFrom::Start(0))?;
let mut hasher = Sha256::new();
let mut buf = [0u8; 8192];
let mut remaining = content_end;
while remaining > 0 {
let to_read = std::cmp::min(remaining, buf.len() as u64) as usize;
file.read_exact(&mut buf[..to_read])?;
hasher.update(&buf[..to_read]);
remaining -= to_read as u64;
}
let hash: [u8; 32] = hasher.finalize().into();
let signature = signing_key.sign(&hash);
let public_key = signing_key.verifying_key();
file.seek(SeekFrom::Start(content_end))?;
Ok(Self {
public_key: public_key.to_bytes(),
signature: signature.to_bytes(),
})
}
pub fn write_to<W: Write>(&self, mut writer: W) -> Result<usize> {
let mut bytes_written = 0;
writer.write_all(&SIGNATURE_BLOCK_MAGIC)?;
bytes_written += 4;
writer.write_all(&[ALGORITHM_ED25519])?;
bytes_written += 1;
writer.write_all(&self.public_key)?;
bytes_written += 32;
writer.write_all(&self.signature)?;
bytes_written += 64;
writer.write_all(&[0u8; 3])?;
bytes_written += 3;
debug_assert_eq!(bytes_written, SIGNATURE_BLOCK_SIZE);
Ok(bytes_written)
}
pub fn read_from<R: Read>(mut reader: R) -> Result<Self> {
let mut magic = [0u8; 4];
reader.read_exact(&mut magic)?;
if magic != SIGNATURE_BLOCK_MAGIC {
return Err(EngramError::InvalidFormat(
"Invalid signature block magic (expected SIGN)".to_string(),
));
}
let mut algo = [0u8; 1];
reader.read_exact(&mut algo)?;
if algo[0] != ALGORITHM_ED25519 {
return Err(EngramError::InvalidSignature);
}
let mut public_key = [0u8; 32];
reader.read_exact(&mut public_key)?;
let mut signature = [0u8; 64];
reader.read_exact(&mut signature)?;
let mut reserved = [0u8; 3];
reader.read_exact(&mut reserved)?;
Ok(Self {
public_key,
signature,
})
}
pub fn verify<S: Read + Seek>(
&self,
file: &mut S,
content_end: u64,
verifying_key: &VerifyingKey,
) -> Result<bool> {
file.seek(SeekFrom::Start(0))?;
let mut hasher = Sha256::new();
let mut buf = [0u8; 8192];
let mut remaining = content_end;
while remaining > 0 {
let to_read = std::cmp::min(remaining, buf.len() as u64) as usize;
file.read_exact(&mut buf[..to_read])?;
hasher.update(&buf[..to_read]);
remaining -= to_read as u64;
}
let hash: [u8; 32] = hasher.finalize().into();
let signature = Signature::from_bytes(&self.signature);
match verifying_key.verify(&hash, &signature) {
Ok(()) => Ok(true),
Err(_) => Ok(false),
}
}
pub fn verifying_key(&self) -> Result<VerifyingKey> {
VerifyingKey::from_bytes(&self.public_key).map_err(|_| EngramError::InvalidPublicKey)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::rngs::OsRng;
use std::io::Cursor;
#[test]
fn test_signature_block_roundtrip() {
let block = SignatureBlock {
public_key: [0xAA; 32],
signature: [0xBB; 64],
};
let mut buf = Vec::new();
let written = block.write_to(&mut buf).unwrap();
assert_eq!(written, SIGNATURE_BLOCK_SIZE);
assert_eq!(buf.len(), SIGNATURE_BLOCK_SIZE);
let parsed = SignatureBlock::read_from(&buf[..]).unwrap();
assert_eq!(parsed.public_key, block.public_key);
assert_eq!(parsed.signature, block.signature);
}
#[test]
fn test_sign_and_verify() {
let mut csprng = OsRng;
let signing_key = SigningKey::generate(&mut csprng);
let verifying_key = signing_key.verifying_key();
let content = b"fake archive header and file data and central directory";
let mut cursor = Cursor::new(Vec::new());
cursor.write_all(content).unwrap();
let block = SignatureBlock::sign_archive(&mut cursor, &signing_key).unwrap();
let content_end = content.len() as u64;
let result = block
.verify(&mut cursor, content_end, &verifying_key)
.unwrap();
assert!(result, "Signature should verify");
}
#[test]
fn test_verify_fails_with_wrong_key() {
let mut csprng = OsRng;
let signing_key = SigningKey::generate(&mut csprng);
let wrong_key = SigningKey::generate(&mut csprng);
let wrong_verifying_key = wrong_key.verifying_key();
let content = b"some archive content";
let mut cursor = Cursor::new(Vec::new());
cursor.write_all(content).unwrap();
let block = SignatureBlock::sign_archive(&mut cursor, &signing_key).unwrap();
let result = block
.verify(&mut cursor, content.len() as u64, &wrong_verifying_key)
.unwrap();
assert!(!result, "Signature should NOT verify with wrong key");
}
#[test]
fn test_verify_fails_with_tampered_content() {
let mut csprng = OsRng;
let signing_key = SigningKey::generate(&mut csprng);
let verifying_key = signing_key.verifying_key();
let content = b"original archive content";
let mut cursor = Cursor::new(Vec::new());
cursor.write_all(content).unwrap();
let block = SignatureBlock::sign_archive(&mut cursor, &signing_key).unwrap();
cursor.seek(SeekFrom::Start(0)).unwrap();
cursor.write_all(b"TAMPERED").unwrap();
let result = block
.verify(&mut cursor, content.len() as u64, &verifying_key)
.unwrap();
assert!(!result, "Signature should NOT verify after tampering");
}
#[test]
fn test_invalid_magic_rejected() {
let mut data = vec![0xFFu8; SIGNATURE_BLOCK_SIZE];
data[4] = ALGORITHM_ED25519;
let result = SignatureBlock::read_from(&data[..]);
assert!(result.is_err());
}
}