Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
188
vendor/ruvector/crates/rvf/rvf-crypto/src/sign.rs
vendored
Normal file
188
vendor/ruvector/crates/rvf/rvf-crypto/src/sign.rs
vendored
Normal file
@@ -0,0 +1,188 @@
|
||||
//! 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user