use crate::crypto::{SigningKey, VerifyingKey};
use crate::key_registry::{
sign_revocation_record, sign_rotation_record, KeyRegistry, RevocationReason,
};
use crate::types::{AuthorId, FileId, VersionNumber};
use crate::Result;
use rand::SeedableRng;
#[derive(Clone)]
pub struct TestKeyPair {
pub signing: SigningKey,
pub verifying: VerifyingKey,
}
impl TestKeyPair {
#[must_use]
pub fn generate() -> Self {
let signing = SigningKey::generate();
let verifying = signing.verifying_key();
Self { signing, verifying }
}
pub fn from_seed(seed: u64) -> crate::Result<Self> {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
let mut key_bytes = [0u8; 32];
rand::RngCore::fill_bytes(&mut rng, &mut key_bytes);
let signing = SigningKey::from_bytes(&key_bytes)?;
let verifying = signing.verifying_key();
Ok(Self { signing, verifying })
}
#[must_use]
pub fn sign(&self, message: &[u8]) -> [u8; 64] {
self.signing.sign(message)
}
pub fn verify(&self, message: &[u8], signature: &[u8; 64]) -> crate::Result<()> {
self.verifying.verify(message, signature)
}
}
#[must_use]
pub const fn test_file_id() -> FileId {
FileId(42)
}
#[must_use]
pub const fn test_file_id_with_value(value: u64) -> FileId {
FileId(value)
}
#[must_use]
pub const fn test_author_id() -> AuthorId {
AuthorId(1001)
}
#[must_use]
pub const fn test_author_id_with_value(value: u64) -> AuthorId {
AuthorId(value)
}
#[must_use]
pub const fn test_version() -> VersionNumber {
VersionNumber(1)
}
#[must_use]
pub const fn test_version_with_value(value: u64) -> VersionNumber {
VersionNumber(value)
}
#[must_use]
pub fn test_data(seed: u64, size: usize) -> Vec<u8> {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
let mut data = vec![0u8; size];
rand::RngCore::fill_bytes(&mut rng, &mut data);
data
}
#[must_use]
pub fn random_test_data(size: usize) -> Vec<u8> {
use rand::RngCore;
let mut data = vec![0u8; size];
rand::rngs::OsRng.fill_bytes(&mut data);
data
}
pub struct TestDataBuilder {
size: usize,
seed: Option<u64>,
pattern: Option<u8>,
}
impl TestDataBuilder {
#[must_use]
pub const fn new() -> Self {
Self {
size: 1024,
seed: None,
pattern: None,
}
}
#[must_use]
pub const fn size(mut self, size: usize) -> Self {
self.size = size;
self
}
#[must_use]
pub const fn seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
#[must_use]
pub const fn pattern(mut self, pattern: u8) -> Self {
self.pattern = Some(pattern);
self
}
#[must_use]
pub fn build(self) -> Vec<u8> {
self.pattern.map_or_else(
|| {
self.seed.map_or_else(
|| random_test_data(self.size),
|seed| test_data(seed, self.size),
)
},
|pattern| vec![pattern; self.size],
)
}
}
impl Default for TestDataBuilder {
fn default() -> Self {
Self::new()
}
}
#[must_use]
pub const fn test_timestamp() -> u64 {
1_700_000_000_000 }
#[must_use]
#[allow(clippy::arithmetic_side_effects)] pub const fn test_timestamp_with_offset(offset_ms: u64) -> u64 {
test_timestamp() + offset_ms
}
#[derive(Debug, Default)]
pub struct TestRegistry {
inner: KeyRegistry,
next_author_id: u64,
}
impl TestRegistry {
#[must_use]
pub fn new() -> Self {
Self {
inner: KeyRegistry::new(),
next_author_id: 1,
}
}
pub fn pin(&mut self, master: &SigningKey, operational: &SigningKey) -> Result<AuthorId> {
let author = AuthorId::new(self.next_author_id);
self.next_author_id = self.next_author_id.saturating_add(1);
self.inner.register_author(
author,
master.verifying_key(),
operational.verifying_key(),
0,
)?;
Ok(author)
}
pub fn pin_with_id(
&mut self,
author: AuthorId,
master: &SigningKey,
operational: &SigningKey,
) -> Result<()> {
self.inner.register_author(
author,
master.verifying_key(),
operational.verifying_key(),
0,
)
}
pub fn rotate(
&mut self,
author: AuthorId,
master: &SigningKey,
new_op: &SigningKey,
effective_version: u64,
) -> Result<u32> {
let current_epoch = self
.inner
.epochs_for(author)
.iter()
.filter(|e| e.is_valid_for(effective_version.saturating_sub(1)))
.map(|e| e.epoch)
.next_back()
.or_else(|| self.inner.epochs_for(author).iter().map(|e| e.epoch).max())
.unwrap_or(0);
let new_epoch = current_epoch.saturating_add(1);
let record = sign_rotation_record(
author,
current_epoch,
new_epoch,
new_op.verifying_key().to_bytes(),
effective_version,
master,
);
self.inner.apply_rotation(&record)?;
Ok(new_epoch)
}
pub fn revoke(
&mut self,
author: AuthorId,
master: &SigningKey,
reason: RevocationReason,
effective_version: u64,
) -> Result<()> {
let active_epoch = self
.inner
.epochs_for(author)
.iter()
.find(|e| e.is_valid_for(effective_version.saturating_sub(1)))
.map(|e| e.epoch)
.or_else(|| self.inner.epochs_for(author).iter().map(|e| e.epoch).max())
.unwrap_or(0);
let record =
sign_revocation_record(author, active_epoch, reason, effective_version, master);
self.inner.apply_revocation(&record)
}
#[must_use]
pub const fn as_registry(&self) -> &KeyRegistry {
&self.inner
}
}
impl AsRef<KeyRegistry> for TestRegistry {
fn as_ref(&self) -> &KeyRegistry {
&self.inner
}
}
#[cfg(test)]
mod tests {
use super::*;
mod keypair {
use super::*;
#[test]
fn should_generate_random_keypair() {
let kp1 = TestKeyPair::generate();
let kp2 = TestKeyPair::generate();
assert_ne!(kp1.signing.to_bytes(), kp2.signing.to_bytes());
}
#[test]
fn should_generate_deterministic_keypair_from_seed() {
let kp1 = TestKeyPair::from_seed(12345).unwrap_or_else(|_| std::process::abort());
let kp2 = TestKeyPair::from_seed(12345).unwrap_or_else(|_| std::process::abort());
assert_eq!(kp1.signing.to_bytes(), kp2.signing.to_bytes());
}
#[test]
fn should_sign_and_verify() {
let kp = TestKeyPair::generate();
let message = b"test message";
let signature = kp.sign(message);
assert!(kp.verify(message, &signature).is_ok());
}
#[test]
fn should_reject_invalid_signature() {
let kp = TestKeyPair::generate();
let message = b"test message";
let mut signature = kp.sign(message);
signature[0] ^= 1;
assert!(kp.verify(message, &signature).is_err());
}
}
mod identifiers {
use super::*;
#[test]
fn should_create_test_file_id() {
let id = test_file_id();
assert_eq!(id.as_u64(), 42);
}
#[test]
fn should_create_test_file_id_with_value() {
let id = test_file_id_with_value(12345);
assert_eq!(id.as_u64(), 12345);
}
#[test]
fn should_create_test_author_id() {
let id = test_author_id();
assert_eq!(id.as_u64(), 1001);
}
#[test]
fn should_create_test_version() {
let version = test_version();
assert_eq!(version.as_u64(), 1);
}
}
mod test_data_generation {
use super::*;
#[test]
fn should_generate_deterministic_test_data() {
let data1 = test_data(12345, 100);
let data2 = test_data(12345, 100);
assert_eq!(data1, data2);
assert_eq!(data1.len(), 100);
}
#[test]
fn should_generate_different_data_for_different_seeds() {
let data1 = test_data(12345, 100);
let data2 = test_data(54321, 100);
assert_ne!(data1, data2);
}
#[test]
fn should_generate_random_test_data() {
let data1 = random_test_data(100);
let data2 = random_test_data(100);
assert_eq!(data1.len(), 100);
assert_eq!(data2.len(), 100);
assert_ne!(data1, data2);
}
#[test]
fn should_build_test_data_with_size() {
let data = TestDataBuilder::new().size(500).build();
assert_eq!(data.len(), 500);
}
#[test]
fn should_build_test_data_with_seed() {
let data1 = TestDataBuilder::new().seed(12345).build();
let data2 = TestDataBuilder::new().seed(12345).build();
assert_eq!(data1, data2);
}
#[test]
fn should_build_test_data_with_pattern() {
let data = TestDataBuilder::new().size(100).pattern(0xAB).build();
assert_eq!(data.len(), 100);
assert!(data.iter().all(|&b| b == 0xAB));
}
}
mod timestamps {
use super::*;
#[test]
fn should_generate_test_timestamp() {
let ts = test_timestamp();
assert_eq!(ts, 1_700_000_000_000);
}
#[test]
fn should_generate_timestamp_with_offset() {
let ts = test_timestamp_with_offset(1000);
assert_eq!(ts, 1_700_000_001_000);
}
}
#[allow(clippy::unwrap_used)]
mod test_registry {
use super::*;
use crate::signature_chain::{sign_version, verify_signature};
use crate::types::VersionNumber;
fn make_version(author: AuthorId, version: u64) -> crate::serializer::VersionEntry {
crate::serializer::VersionEntry::new(
VersionNumber(version),
[0u8; 32],
[0xAB; 32],
author,
1_700_000_000_000_000_000,
0,
0,
)
}
#[test]
fn pin_returns_registry_with_active_epoch_for_new_author() {
let (master, op) = (SigningKey::generate(), SigningKey::generate());
let mut reg = TestRegistry::new();
let author = reg.pin(&master, &op).unwrap();
assert!(reg.as_registry().active_epoch_at(author, 1).is_some());
}
#[test]
fn pin_allocates_sequential_ids() {
let mut reg = TestRegistry::new();
let (ma, opa) = (SigningKey::generate(), SigningKey::generate());
let (mb, opb) = (SigningKey::generate(), SigningKey::generate());
let a = reg.pin(&ma, &opa).unwrap();
let b = reg.pin(&mb, &opb).unwrap();
assert_ne!(a, b);
assert_eq!(b.as_u64(), a.as_u64().saturating_add(1));
}
#[test]
fn pin_with_id_uses_the_supplied_id() {
let (m, op) = (SigningKey::generate(), SigningKey::generate());
let mut reg = TestRegistry::new();
let chosen = AuthorId::new(50_001);
reg.pin_with_id(chosen, &m, &op).unwrap();
assert!(reg.as_registry().active_epoch_at(chosen, 1).is_some());
}
#[test]
fn pinned_registry_accepts_signature_made_with_the_pinned_key() {
let (master, op) = (SigningKey::generate(), SigningKey::generate());
let mut reg = TestRegistry::new();
let author = reg.pin(&master, &op).unwrap();
let version = make_version(author, 7);
let sig = sign_version(&version, &op);
verify_signature(&version, &sig, reg.as_registry()).unwrap();
}
#[test]
fn rotate_rejects_signatures_made_with_the_rotated_out_key() {
let (master, op0, op1) = (
SigningKey::generate(),
SigningKey::generate(),
SigningKey::generate(),
);
let mut reg = TestRegistry::new();
let author = reg.pin(&master, &op0).unwrap();
let new_epoch = reg.rotate(author, &master, &op1, 100).unwrap();
assert_eq!(new_epoch, 1);
let version = make_version(author, 200);
let sig = sign_version(&version, &op0);
assert!(verify_signature(&version, &sig, reg.as_registry()).is_err());
}
#[test]
fn rotate_accepts_signatures_made_with_the_new_key_after_effective_version() {
let (master, op0, op1) = (
SigningKey::generate(),
SigningKey::generate(),
SigningKey::generate(),
);
let mut reg = TestRegistry::new();
let author = reg.pin(&master, &op0).unwrap();
reg.rotate(author, &master, &op1, 100).unwrap();
let version = make_version(author, 150);
let sig = sign_version(&version, &op1);
verify_signature(&version, &sig, reg.as_registry()).unwrap();
}
#[test]
fn revoke_rejects_signatures_after_effective_version() {
let (master, op) = (SigningKey::generate(), SigningKey::generate());
let mut reg = TestRegistry::new();
let author = reg.pin(&master, &op).unwrap();
reg.revoke(author, &master, RevocationReason::Compromised, 50)
.unwrap();
let version = make_version(author, 100);
let sig = sign_version(&version, &op);
assert!(verify_signature(&version, &sig, reg.as_registry()).is_err());
}
#[test]
fn revoke_preserves_signatures_before_effective_version() {
let (master, op) = (SigningKey::generate(), SigningKey::generate());
let mut reg = TestRegistry::new();
let author = reg.pin(&master, &op).unwrap();
reg.revoke(author, &master, RevocationReason::Superseded, 100)
.unwrap();
let version = make_version(author, 50);
let sig = sign_version(&version, &op);
verify_signature(&version, &sig, reg.as_registry()).unwrap();
}
#[test]
fn as_ref_matches_as_registry() {
let (m, op) = (SigningKey::generate(), SigningKey::generate());
let mut reg = TestRegistry::new();
reg.pin(&m, &op).unwrap();
let via_method = reg.as_registry() as *const KeyRegistry;
let via_asref: *const KeyRegistry = reg.as_ref();
assert_eq!(via_method, via_asref);
}
}
}