use crate::constants;
use crate::error::Result;
use crate::identity::{self, IdentityPublicKey, IdentitySecretKey};
use crate::primitives::{hmac, xwing};
use zeroize::{Zeroize, Zeroizing};
#[must_use = "contains secret token material that must not be silently discarded"]
pub fn auth_challenge(
client_pk: &IdentityPublicKey,
) -> Result<(xwing::Ciphertext, Zeroizing<[u8; 32]>)> {
let (ct, mut ss) = identity::encapsulate(client_pk)?;
let mut raw_token = hmac::hmac_sha3_256(ss.as_bytes(), constants::AUTH_HMAC_LABEL);
let token = Zeroizing::new(raw_token);
raw_token.zeroize();
ss.0.zeroize();
Ok((ct, token))
}
#[must_use = "contains secret proof material that must not be silently discarded"]
pub fn auth_respond(
client_sk: &IdentitySecretKey,
ct: &xwing::Ciphertext,
) -> Result<Zeroizing<[u8; 32]>> {
let mut ss = identity::decapsulate(client_sk, ct)?;
let mut raw_proof = hmac::hmac_sha3_256(ss.as_bytes(), constants::AUTH_HMAC_LABEL);
let proof = Zeroizing::new(raw_proof);
raw_proof.zeroize();
ss.0.zeroize();
Ok(proof)
}
#[must_use]
pub fn auth_verify(expected_token: &[u8; 32], proof: &[u8; 32]) -> bool {
hmac::hmac_sha3_256_verify_raw(expected_token, proof)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::{GeneratedIdentity, generate_identity};
#[test]
fn challenge_response_verify() {
let GeneratedIdentity {
public_key: pk,
secret_key: sk,
..
} = generate_identity().unwrap();
let (ct, token) = auth_challenge(&pk).unwrap();
let proof = auth_respond(&sk, &ct).unwrap();
assert!(auth_verify(&token, &proof));
}
#[test]
fn verify_wrong_proof() {
let GeneratedIdentity {
public_key: pk,
secret_key: sk,
..
} = generate_identity().unwrap();
let (ct, token) = auth_challenge(&pk).unwrap();
let mut proof = auth_respond(&sk, &ct).unwrap();
proof[0] ^= 0x01;
assert!(!auth_verify(&token, &proof));
}
#[test]
fn verify_different_client() {
let GeneratedIdentity {
public_key: pk_a, ..
} = generate_identity().unwrap();
let GeneratedIdentity {
secret_key: sk_b, ..
} = generate_identity().unwrap();
let (ct, token) = auth_challenge(&pk_a).unwrap();
let proof = auth_respond(&sk_b, &ct).unwrap();
assert!(!auth_verify(&token, &proof));
}
#[test]
fn token_is_nonzero() {
let GeneratedIdentity { public_key: pk, .. } = generate_identity().unwrap();
let (_, token) = auth_challenge(&pk).unwrap();
assert!(token.iter().any(|&b| b != 0));
}
#[test]
fn proof_is_nonzero() {
let GeneratedIdentity {
public_key: pk,
secret_key: sk,
..
} = generate_identity().unwrap();
let (ct, _) = auth_challenge(&pk).unwrap();
let proof = auth_respond(&sk, &ct).unwrap();
assert!(proof.iter().any(|&b| b != 0));
}
#[test]
fn auth_respond_wrong_ciphertext_fails_verify() {
let GeneratedIdentity {
public_key: pk,
secret_key: sk,
..
} = generate_identity().unwrap();
let (_, token) = auth_challenge(&pk).unwrap();
let (ct2, _) = auth_challenge(&pk).unwrap();
let proof = auth_respond(&sk, &ct2).unwrap();
assert!(!auth_verify(&token, &proof));
}
#[test]
fn auth_token_is_hmac_of_shared_secret() {
let GeneratedIdentity {
public_key: pk,
secret_key: sk,
..
} = generate_identity().unwrap();
let (ct, token) = auth_challenge(&pk).unwrap();
let proof = auth_respond(&sk, &ct).unwrap();
assert_eq!(*token, *proof);
}
#[test]
fn auth_verify_matching_raw_tokens_returns_true() {
let token = [0x42u8; 32];
assert!(auth_verify(&token, &token));
}
#[test]
fn auth_verify_mismatched_raw_tokens_returns_false() {
let token = [0x42u8; 32];
let mut wrong = token;
wrong[0] ^= 0x01;
assert!(!auth_verify(&token, &wrong));
}
#[test]
fn auth_verify_all_zero_tokens_returns_true() {
let token = [0u8; 32];
assert!(auth_verify(&token, &token));
}
#[test]
fn auth_verify_single_bit_flip_returns_false() {
for bit in 0..256u32 {
let byte = (bit / 8) as usize;
let mask = 1u8 << (bit % 8);
let token = [0xABu8; 32];
let mut wrong = token;
wrong[byte] ^= mask;
assert!(!auth_verify(&token, &wrong), "bit {} not detected", bit);
}
}
#[test]
fn auth_challenges_produce_unique_ciphertexts() {
let GeneratedIdentity { public_key: pk, .. } = generate_identity().unwrap();
let (ct1, _) = auth_challenge(&pk).unwrap();
let (ct2, _) = auth_challenge(&pk).unwrap();
assert_ne!(
ct1.as_bytes(),
ct2.as_bytes(),
"consecutive auth_challenge calls must produce distinct ciphertexts"
);
}
}