Skip to content

Commit e9598f8

Browse files
committed
test(coord-fed): Phase 1.4 — RFC 8032 cross-implementation vector + Rust unit tests
ADR-0016 Phase 1.4 — close the loop on cross-implementation consistency. Both coord-tui (Rust + ed25519-dalek) and the Zig adapter (std.crypto.sign.Ed25519) now pin RFC 8032 §7.1 TEST 1: SEED: 9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60 PUBKEY: d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a If both tests pass, the two derivations match the spec and therefore each other — which is the consistency guarantee for the shared 32-byte seed-file format that is the Phase 1 identity contract. Rust side (coord-tui/src/main.rs #[cfg(test)]): * print_pubkey_matches_rfc8032_test1 — writes the RFC seed, reads it via print_pubkey, asserts the canonical pubkey hex. * print_pubkey_roundtrip_is_deterministic — same seed file produces the same pubkey on two reads. * print_pubkey_rejects_wrong_length_seed — error path. * fresh_seed_file_is_mode_0600 — Unix-only, asserts the create-path enforces 0600. All four pass locally (`cargo test`). Zig side (coord_identity.zig): * "RFC 8032 §7.1 TEST 1 — seed derives the canonical pubkey" — same vector, same shape, called through the C-ABI surface (boj_coord_identity_init + boj_coord_identity_get_pubkey).
1 parent f42fa88 commit e9598f8

2 files changed

Lines changed: 125 additions & 0 deletions

File tree

cartridges/local-coord-mcp/ffi/coord_identity.zig

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,3 +433,39 @@ test "FFI: load_known_peers on missing file returns 0" {
433433
try std.testing.expectEqual(@as(c_int, 0), rc);
434434
try std.testing.expectEqual(@as(c_int, 0), boj_coord_identity_known_peer_count());
435435
}
436+
437+
// RFC 8032 §7.1 TEST 1 — the canonical ed25519 reference vector.
438+
// The matching test in coord-tui/src/main.rs pins the same vector,
439+
// so if both this test and that test pass, the Rust and Zig
440+
// derivations agree with the spec — and therefore with each other
441+
// — across the shared 32-byte seed-file format.
442+
//
443+
// SEED: 9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60
444+
// PUBKEY: d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a
445+
446+
test "RFC 8032 §7.1 TEST 1 — seed derives the canonical pubkey" {
447+
testResetState();
448+
const seed_hex = "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60";
449+
const expect_hex = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a";
450+
451+
var seed: [SEED_BYTES]u8 = undefined;
452+
try hexDecode(seed_hex, &seed);
453+
454+
const tmp_path = "/tmp/boj-coord-test-rfc8032.key";
455+
fs.deleteFileAbsolute(tmp_path) catch {};
456+
defer fs.deleteFileAbsolute(tmp_path) catch {};
457+
try writeSeedFile(tmp_path, seed);
458+
459+
const path_z: [:0]const u8 = tmp_path;
460+
try std.testing.expectEqual(@as(c_int, 0), boj_coord_identity_init(path_z.ptr));
461+
462+
var pubkey: [PUBKEY_BYTES]u8 = undefined;
463+
try std.testing.expectEqual(
464+
@as(c_int, @intCast(PUBKEY_BYTES)),
465+
boj_coord_identity_get_pubkey(&pubkey, PUBKEY_BYTES),
466+
);
467+
468+
var expect_bytes: [PUBKEY_BYTES]u8 = undefined;
469+
try hexDecode(expect_hex, &expect_bytes);
470+
try std.testing.expectEqualSlices(u8, &expect_bytes, &pubkey);
471+
}

coord-tui/src/main.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -853,3 +853,92 @@ fn main() -> io::Result<()> {
853853
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
854854
Ok(())
855855
}
856+
857+
// ─── Tests ────────────────────────────────────────────────────────────────────
858+
859+
#[cfg(test)]
860+
mod tests {
861+
use super::*;
862+
use std::path::PathBuf;
863+
864+
fn tmp_path(name: &str) -> PathBuf {
865+
let mut p = std::env::temp_dir();
866+
// Disambiguate across parallel tests in this module.
867+
let nanos = std::time::SystemTime::now()
868+
.duration_since(std::time::UNIX_EPOCH)
869+
.map(|d| d.subsec_nanos())
870+
.unwrap_or(0);
871+
p.push(format!("coord-tui-test-{}-{}-{}", std::process::id(), nanos, name));
872+
p
873+
}
874+
875+
fn hex_decode(s: &str) -> Vec<u8> {
876+
(0..s.len()).step_by(2)
877+
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
878+
.collect()
879+
}
880+
881+
/// RFC 8032 §7.1 TEST 1 — the canonical ed25519 reference vector.
882+
/// Pinning this seed → pubkey mapping proves the Rust derivation
883+
/// matches the spec. The Zig adapter pins the same vector in
884+
/// `coord_identity.zig`; if both match RFC 8032, they match each
885+
/// other — which is the cross-implementation consistency guarantee
886+
/// for the shared seed-file format.
887+
const RFC8032_TEST1_SEED_HEX: &str =
888+
"9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60";
889+
const RFC8032_TEST1_PUBKEY_HEX: &str =
890+
"d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a";
891+
892+
#[test]
893+
fn print_pubkey_matches_rfc8032_test1() {
894+
let path = tmp_path("rfc8032.key");
895+
let _ = std::fs::remove_file(&path);
896+
std::fs::write(&path, hex_decode(RFC8032_TEST1_SEED_HEX)).unwrap();
897+
898+
let hex = print_pubkey(Some(path.to_str().unwrap())).unwrap();
899+
assert_eq!(hex, RFC8032_TEST1_PUBKEY_HEX);
900+
901+
std::fs::remove_file(&path).unwrap();
902+
}
903+
904+
#[test]
905+
fn print_pubkey_roundtrip_is_deterministic() {
906+
// Two reads of the same seed file produce the same pubkey.
907+
let path = tmp_path("roundtrip.key");
908+
let _ = std::fs::remove_file(&path);
909+
910+
let first = print_pubkey(Some(path.to_str().unwrap())).unwrap();
911+
let second = print_pubkey(Some(path.to_str().unwrap())).unwrap();
912+
assert_eq!(first, second);
913+
assert_eq!(first.len(), 64);
914+
assert!(first.chars().all(|c| c.is_ascii_hexdigit()));
915+
916+
std::fs::remove_file(&path).unwrap();
917+
}
918+
919+
#[test]
920+
fn print_pubkey_rejects_wrong_length_seed() {
921+
let path = tmp_path("short.key");
922+
let _ = std::fs::remove_file(&path);
923+
std::fs::write(&path, b"only-five").unwrap();
924+
925+
let err = print_pubkey(Some(path.to_str().unwrap())).unwrap_err();
926+
assert!(err.contains("expected 32"), "unexpected error: {}", err);
927+
928+
std::fs::remove_file(&path).unwrap();
929+
}
930+
931+
#[cfg(unix)]
932+
#[test]
933+
fn fresh_seed_file_is_mode_0600() {
934+
use std::os::unix::fs::PermissionsExt;
935+
let path = tmp_path("perms.key");
936+
let _ = std::fs::remove_file(&path);
937+
938+
let _ = print_pubkey(Some(path.to_str().unwrap())).unwrap();
939+
let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
940+
assert_eq!(mode, 0o600, "seed file mode = {:o}, expected 0600", mode);
941+
942+
std::fs::remove_file(&path).unwrap();
943+
}
944+
}

0 commit comments

Comments
 (0)