Host & Client force randomness on each other.

Updated Reserved entry order. Made DataType ID changes easier in future
(this MAY NOT  happen again once in use).
This commit is contained in:
Alex Emmet 2026-06-26 17:08:48 +02:00
commit f4118f28ba
25 changed files with 1032 additions and 667 deletions

128
crypto/src/auth.rs Normal file
View file

@ -0,0 +1,128 @@
/*
* Canonical signed payloads for the authentication handshake.
*
* Each payload starts with a unique [`domain`] tag so a signature for one step
* cannot be replayed as another.
*
* Handshake:
* Step 1. Client -> Host : Identification { version, id } (unsigned hello)
* Step 2. Host -> Client : Challenge { server_challenge, host_sig } host_sig over challenge_payload
* Step 3. Client -> Host : ChallengeResponse { client_nonce, sig } sig over login_proof_payload
* Step 4. Host -> Client : IdentificationResponse { connected, id, host_sig } host_sig over host_final_payload
*/
/// Domain-separation tags — a distinct leading byte per signed context.
pub mod domain {
/// Host's signature over the challenge it issues (step 2).
pub const CHALLENGE: u8 = 0x10;
/// Client's authenticating proof for a login (step 3).
pub const LOGIN_PROOF: u8 = 0x11;
/// Client's authenticating proof for a registration (step 3).
pub const REGISTER_PROOF: u8 = 0x12;
/// Host's final confirmation signature (step 4).
pub const HOST_FINAL: u8 = 0x13;
}
/*
* Host's challenge (step 2): binds `id` and `server_challenge` to prove host
* key possession before the client reveals its proof.
*/
pub fn challenge_payload(id: u64, server_challenge: u128) -> Vec<u8> {
let mut p = Vec::with_capacity(1 + 8 + 16);
p.push(domain::CHALLENGE);
p.extend_from_slice(&id.to_be_bytes());
p.extend_from_slice(&server_challenge.to_be_bytes());
p
}
/*
* Client's login proof (step 3): binds version, id, server_challenge, client_nonce.
*/
pub fn login_proof_payload(
version: &str,
id: u64,
server_challenge: u128,
client_nonce: u128,
) -> Vec<u8> {
let mut p = Vec::with_capacity(1 + version.len() + 8 + 16 + 16);
p.push(domain::LOGIN_PROOF);
p.extend_from_slice(version.as_bytes());
p.extend_from_slice(&id.to_be_bytes());
p.extend_from_slice(&server_challenge.to_be_bytes());
p.extend_from_slice(&client_nonce.to_be_bytes());
p
}
/*
* Client's registration proof (step 3): binds version, public_keys, server_challenge, client_nonce.
*/
pub fn register_proof_payload(
version: &str,
public_keys: &[u8],
server_challenge: u128,
client_nonce: u128,
) -> Vec<u8> {
let mut p = Vec::with_capacity(1 + version.len() + 16 + 16 + public_keys.len());
p.push(domain::REGISTER_PROOF);
p.extend_from_slice(version.as_bytes());
p.extend_from_slice(&server_challenge.to_be_bytes());
p.extend_from_slice(&client_nonce.to_be_bytes());
p.extend_from_slice(public_keys);
p
}
/*
* Host's final confirmation (step 4): binds id, client_nonce, server_challenge
* to prove host liveness over a value the client chose.
*/
pub fn host_final_payload(id: u64, client_nonce: u128, server_challenge: u128) -> Vec<u8> {
let mut p = Vec::with_capacity(1 + 8 + 16 + 16);
p.push(domain::HOST_FINAL);
p.extend_from_slice(&id.to_be_bytes());
p.extend_from_slice(&client_nonce.to_be_bytes());
p.extend_from_slice(&server_challenge.to_be_bytes());
p
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn domain_tags_are_distinct() {
let tags = [
domain::CHALLENGE,
domain::LOGIN_PROOF,
domain::REGISTER_PROOF,
domain::HOST_FINAL,
];
for (i, a) in tags.iter().enumerate() {
for b in &tags[i + 1..] {
assert_ne!(a, b, "domain tags must be unique");
}
}
}
#[test]
fn login_and_register_proofs_never_collide() {
let login = login_proof_payload("1.0", 7, 1, 2);
let register = register_proof_payload("1.0", &7u64.to_be_bytes(), 1, 2);
assert_ne!(login, register);
assert_ne!(login[0], register[0]);
}
#[test]
fn challenge_binds_id_and_value() {
assert_ne!(challenge_payload(1, 9), challenge_payload(2, 9));
assert_ne!(challenge_payload(1, 9), challenge_payload(1, 8));
assert_eq!(challenge_payload(1, 9)[0], domain::CHALLENGE);
}
#[test]
fn proofs_bind_the_server_challenge() {
assert_ne!(
login_proof_payload("1.0", 3, 100, 200),
login_proof_payload("1.0", 3, 101, 200),
);
}
}