Files
wifi-densepose/vendor/ruvector/crates/rvf/rvf-crypto/src/sign.rs

189 lines
6.4 KiB
Rust

//! Ed25519 segment signing and verification.
//!
//! Signs the canonical representation: header bytes || content_hash || context.
//! ML-DSA-65 is a future TODO behind a feature flag.
use alloc::vec::Vec;
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use rvf_types::{SegmentHeader, SignatureFooter};
use crate::hash::shake256_128;
/// Ed25519 algorithm identifier (matches `SignatureAlgo::Ed25519`).
const SIG_ALGO_ED25519: u16 = 0;
/// Build the canonical message to sign for a segment.
///
/// signed_data = segment_header_bytes[0..40] || content_hash || context_string || segment_id
fn build_signed_data(header: &SegmentHeader, payload: &[u8]) -> Vec<u8> {
// Safe serialization of header fields to bytes, matching the wire format
// layout (see write_path.rs header_to_bytes). Avoids unsafe transmute which
// relies on compiler-specific struct layout guarantees.
let header_bytes = header_to_sign_bytes(header);
let mut msg = Vec::with_capacity(40 + 16 + 32);
// First 40 bytes of header (up to but not including content_hash at offset 0x28)
msg.extend_from_slice(&header_bytes[..40]);
// Content hash from header
msg.extend_from_slice(&header.content_hash);
// Context string for domain separation
msg.extend_from_slice(b"RVF-v1-segment");
// Segment ID bytes for replay prevention
msg.extend_from_slice(&header.segment_id.to_le_bytes());
// Include payload hash for binding
let payload_hash = shake256_128(payload);
msg.extend_from_slice(&payload_hash);
msg
}
/// Safely serialize a `SegmentHeader` into its 64-byte wire representation.
///
/// This mirrors the layout in `write_path::header_to_bytes` but lives here to
/// avoid an unsafe `transmute` / pointer cast whose correctness depends on
/// padding and alignment guarantees that are not enforced by the language.
fn header_to_sign_bytes(h: &SegmentHeader) -> [u8; 64] {
let mut buf = [0u8; 64];
buf[0x00..0x04].copy_from_slice(&h.magic.to_le_bytes());
buf[0x04] = h.version;
buf[0x05] = h.seg_type;
buf[0x06..0x08].copy_from_slice(&h.flags.to_le_bytes());
buf[0x08..0x10].copy_from_slice(&h.segment_id.to_le_bytes());
buf[0x10..0x18].copy_from_slice(&h.payload_length.to_le_bytes());
buf[0x18..0x20].copy_from_slice(&h.timestamp_ns.to_le_bytes());
buf[0x20] = h.checksum_algo;
buf[0x21] = h.compression;
buf[0x22..0x24].copy_from_slice(&h.reserved_0.to_le_bytes());
buf[0x24..0x28].copy_from_slice(&h.reserved_1.to_le_bytes());
buf[0x28..0x38].copy_from_slice(&h.content_hash);
buf[0x38..0x3C].copy_from_slice(&h.uncompressed_len.to_le_bytes());
buf[0x3C..0x40].copy_from_slice(&h.alignment_pad.to_le_bytes());
buf
}
/// Sign a segment with Ed25519, producing a `SignatureFooter`.
pub fn sign_segment(header: &SegmentHeader, payload: &[u8], key: &SigningKey) -> SignatureFooter {
let msg = build_signed_data(header, payload);
let sig: Signature = key.sign(&msg);
let sig_bytes = sig.to_bytes();
let mut signature = [0u8; SignatureFooter::MAX_SIG_LEN];
signature[..64].copy_from_slice(&sig_bytes);
SignatureFooter {
sig_algo: SIG_ALGO_ED25519,
sig_length: 64,
signature,
footer_length: SignatureFooter::compute_footer_length(64),
}
}
/// Verify a segment signature using Ed25519.
///
/// Returns `true` if the signature is valid, `false` otherwise.
pub fn verify_segment(
header: &SegmentHeader,
payload: &[u8],
footer: &SignatureFooter,
pubkey: &VerifyingKey,
) -> bool {
if footer.sig_algo != SIG_ALGO_ED25519 {
return false;
}
if footer.sig_length != 64 {
return false;
}
let msg = build_signed_data(header, payload);
let sig_bytes: [u8; 64] = match footer.signature[..64].try_into() {
Ok(b) => b,
Err(_) => return false,
};
let sig = Signature::from_bytes(&sig_bytes);
pubkey.verify(&msg, &sig).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::SigningKey;
use rand::rngs::OsRng;
fn make_test_header() -> SegmentHeader {
let mut h = SegmentHeader::new(0x01, 42);
h.timestamp_ns = 1_000_000_000;
h.payload_length = 100;
h
}
#[test]
fn sign_verify_round_trip() {
let key = SigningKey::generate(&mut OsRng);
let header = make_test_header();
let payload = b"test payload data for signing";
let footer = sign_segment(&header, payload, &key);
let pubkey = key.verifying_key();
assert!(verify_segment(&header, payload, &footer, &pubkey));
}
#[test]
fn tampered_payload_fails() {
let key = SigningKey::generate(&mut OsRng);
let header = make_test_header();
let payload = b"original payload";
let footer = sign_segment(&header, payload, &key);
let pubkey = key.verifying_key();
let tampered = b"tampered payload";
assert!(!verify_segment(&header, tampered, &footer, &pubkey));
}
#[test]
fn tampered_header_fails() {
let key = SigningKey::generate(&mut OsRng);
let header = make_test_header();
let payload = b"payload";
let footer = sign_segment(&header, payload, &key);
let pubkey = key.verifying_key();
let mut bad_header = header;
bad_header.segment_id = 999;
assert!(!verify_segment(&bad_header, payload, &footer, &pubkey));
}
#[test]
fn wrong_key_fails() {
let key1 = SigningKey::generate(&mut OsRng);
let key2 = SigningKey::generate(&mut OsRng);
let header = make_test_header();
let payload = b"payload";
let footer = sign_segment(&header, payload, &key1);
let wrong_pubkey = key2.verifying_key();
assert!(!verify_segment(&header, payload, &footer, &wrong_pubkey));
}
#[test]
fn sig_algo_is_ed25519() {
let key = SigningKey::generate(&mut OsRng);
let header = make_test_header();
let footer = sign_segment(&header, b"x", &key);
assert_eq!(footer.sig_algo, 0);
assert_eq!(footer.sig_length, 64);
}
#[test]
fn footer_length_correct() {
let key = SigningKey::generate(&mut OsRng);
let header = make_test_header();
let footer = sign_segment(&header, b"data", &key);
assert_eq!(
footer.footer_length,
SignatureFooter::compute_footer_length(64)
);
}
}