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:
parent
687e6f9642
commit
f4118f28ba
25 changed files with 1032 additions and 667 deletions
|
|
@ -24,7 +24,7 @@ let pt = cipher.decrypt(&ct, b"aad")?;
|
|||
|
||||
## Signatures
|
||||
|
||||
### Ed25519 (classical)
|
||||
### Ed25519
|
||||
|
||||
```rust
|
||||
use mtp_crypto::{Ed25519Signer, SignatureScheme};
|
||||
|
|
@ -34,7 +34,7 @@ let sig = signer.sign(b"message")?;
|
|||
signer.verify(b"message", &sig)?;
|
||||
```
|
||||
|
||||
### ML-DSA-65 (post-quantum, requires `pqc`)
|
||||
### ML-DSA-65
|
||||
|
||||
```rust
|
||||
use mtp_crypto::{MlDsaSigner, SignatureScheme};
|
||||
|
|
@ -47,7 +47,7 @@ signer.verify(b"message", &sig)?;
|
|||
let signer = MlDsaSigner::new(&sk, &pk)?;
|
||||
```
|
||||
|
||||
### Dual signatures (requires `pqc`)
|
||||
### Dual signatures
|
||||
|
||||
```rust
|
||||
use mtp_crypto::{sign_dual, DualSignature, Ed25519Signer, MlDsaSigner};
|
||||
|
|
@ -58,7 +58,7 @@ let dual = sign_dual(ed_signer.signing_key(), ml_signer.signing_key(), b"msg");
|
|||
dual.verify(ed_signer.verifying_key(), ml_signer.verifying_key(), b"msg")?;
|
||||
```
|
||||
|
||||
## Hybrid KEM (requires `pqc`)
|
||||
## Hybrid KEM
|
||||
|
||||
X25519 + ML-KEM-768. 64-byte shared secret. Feed into HKDF before use.
|
||||
|
||||
|
|
|
|||
128
crypto/src/auth.rs
Normal file
128
crypto/src/auth.rs
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,8 +16,10 @@ pub fn hkdf_expand(
|
|||
}
|
||||
|
||||
pub fn hkdf_extract(ikm: &[u8], salt: &[u8]) -> [u8; 32] {
|
||||
// Return the pseudo-random key (PRK) produced by HKDF-Extract directly.
|
||||
// Extract cannot fail, so this avoids the panicking expand step entirely.
|
||||
/*
|
||||
* Return the pseudo-random key (PRK) produced by HKDF-Extract directly.
|
||||
* Extract cannot fail, so this avoids the panicking expand step entirely.
|
||||
*/
|
||||
let (prk, _) = Hkdf::<Sha256>::extract(Some(salt), ikm);
|
||||
let mut out = [0u8; 32];
|
||||
out.copy_from_slice(&prk);
|
||||
|
|
|
|||
|
|
@ -348,7 +348,7 @@ impl Keyring {
|
|||
.get(*offset..*offset + 2)
|
||||
.ok_or(CryptoError::InvalidKeyLength)?
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
.expect("slice is 2 bytes, verified above"),
|
||||
) as usize;
|
||||
*offset += 2;
|
||||
let key = bytes
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod aead;
|
||||
pub mod auth;
|
||||
pub mod error;
|
||||
pub mod keypair;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue