immutable-trace 0.1.7

Tamper-evident immutable audit trace: signing, verification, ingestion and CLI
Documentation
use ed25519_dalek::{SigningKey, VerifyingKey};
use immutable_trace::{build_signed_record, AuditRecord, IngestError, IngestState};

#[test]
fn accepts_valid_sequential_records() {
    let signing_key = SigningKey::from_bytes(&[21u8; 32]);
    let verifying_key = VerifyingKey::from(&signing_key);

    let mut state = IngestState::default();
    state.register_device("lift-01", verifying_key);

    let r1 = build_signed_record(
        "lift-01",
        1,
        1,
        b"payload-1",
        AuditRecord::zero_hash(),
        "s3://bucket/r1.bin",
        &signing_key,
    );
    let r2 = build_signed_record(
        "lift-01",
        2,
        2,
        b"payload-2",
        r1.hash(),
        "s3://bucket/r2.bin",
        &signing_key,
    );

    assert!(state.verify_and_accept(&r1).is_ok());
    assert!(state.verify_and_accept(&r2).is_ok());
}

#[test]
fn rejects_duplicate_sequence() {
    let signing_key = SigningKey::from_bytes(&[31u8; 32]);
    let verifying_key = VerifyingKey::from(&signing_key);

    let mut state = IngestState::default();
    state.register_device("lift-01", verifying_key);

    let r1 = build_signed_record(
        "lift-01",
        1,
        1,
        b"payload-1",
        AuditRecord::zero_hash(),
        "s3://bucket/r1.bin",
        &signing_key,
    );

    assert!(state.verify_and_accept(&r1).is_ok());

    let duplicate = build_signed_record(
        "lift-01",
        1,
        2,
        b"payload-duplicate",
        AuditRecord::zero_hash(),
        "s3://bucket/r1b.bin",
        &signing_key,
    );

    let err = state.verify_and_accept(&duplicate).unwrap_err();
    assert_eq!(
        err,
        IngestError::Duplicate {
            device_id: "lift-01".to_string(),
            sequence: 1,
        }
    );
}

#[test]
fn rejects_invalid_prev_hash() {
    let signing_key = SigningKey::from_bytes(&[41u8; 32]);
    let verifying_key = VerifyingKey::from(&signing_key);

    let mut state = IngestState::default();
    state.register_device("lift-01", verifying_key);

    let r1 = build_signed_record(
        "lift-01",
        1,
        1,
        b"payload-1",
        AuditRecord::zero_hash(),
        "s3://bucket/r1.bin",
        &signing_key,
    );
    assert!(state.verify_and_accept(&r1).is_ok());

    let wrong_prev = [9u8; 32];
    let r2 = build_signed_record(
        "lift-01",
        2,
        2,
        b"payload-2",
        wrong_prev,
        "s3://bucket/r2.bin",
        &signing_key,
    );

    let err = state.verify_and_accept(&r2).unwrap_err();
    assert_eq!(err, IngestError::InvalidPrevHash("lift-01".to_string()));
}

#[test]
fn rejects_tampered_signature() {
    let signing_key = SigningKey::from_bytes(&[51u8; 32]);
    let verifying_key = VerifyingKey::from(&signing_key);

    let mut state = IngestState::default();
    state.register_device("lift-01", verifying_key);

    let mut r1 = build_signed_record(
        "lift-01",
        1,
        1,
        b"payload-1",
        AuditRecord::zero_hash(),
        "s3://bucket/r1.bin",
        &signing_key,
    );

    r1.signature[0] ^= 0x01;

    let err = state.verify_and_accept(&r1).unwrap_err();
    assert_eq!(err, IngestError::InvalidSignature("lift-01".to_string()));
}