use crate::pairing::{CONFIRMATION_LEN, NONCE_LEN, PUBLIC_KEY_LEN, SESSION_ID_LEN};
use crate::vault::MasterKey;
use crate::{Error, Result};
use hkdf::Hkdf;
use hmac::{Hmac, Mac};
use p256::PublicKey;
use p256::ecdh::EphemeralSecret;
use p256::elliptic_curve::Generate;
use p256::elliptic_curve::sec1::ToSec1Point as _;
use qualia_dik::{DevicePublicKey, DeviceSignature};
use secrecy::zeroize::Zeroize;
use sha2::{Digest, Sha256};
pub const CHALLENGE_LEN: usize = PUBLIC_KEY_LEN + NONCE_LEN;
pub const RESPONSE_LEN: usize = PUBLIC_KEY_LEN + NONCE_LEN + PUBLIC_KEY_LEN + CONFIRMATION_LEN;
pub const RESPONSE_SIGNED_LEN: usize = RESPONSE_LEN + qualia_dik::device::SIGNATURE_LEN;
pub const CONFIRM_LEN: usize = CONFIRMATION_LEN;
const HKDF_INFO_PROOF: &[u8] = b"Qualia2FA auth v1 proof key";
const HKDF_INFO_CONFIRM: &[u8] = b"Qualia2FA auth v1 confirm key";
const PROOF_LABEL: &[u8] = b"Qualia2FA auth v1 proof";
const CONFIRM_LABEL: &[u8] = b"Qualia2FA auth v1 confirm";
const SESSION_LABEL: &[u8] = b"Qualia2FA auth v1 session";
const SIGNATURE_LABEL: &[u8] = b"Qualia2FA auth v1 device signature";
pub struct Verifier {
secret: EphemeralSecret,
nonce: [u8; NONCE_LEN],
public_key: [u8; PUBLIC_KEY_LEN],
}
impl Verifier {
#[must_use]
pub fn challenge(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(CHALLENGE_LEN);
out.extend_from_slice(&self.public_key);
out.extend_from_slice(&self.nonce);
out
}
#[must_use]
pub const fn public_key(&self) -> &[u8; PUBLIC_KEY_LEN] {
&self.public_key
}
#[must_use]
pub const fn nonce(&self) -> &[u8; NONCE_LEN] {
&self.nonce
}
pub fn verify(self, response: &[u8], master: &MasterKey) -> Result<Verified> {
if response.len() != RESPONSE_SIGNED_LEN {
return Err(Error::AuthMessageMalformed {
reason: "wrong response length",
});
}
let body = &response[..RESPONSE_LEN];
let mut responder_public = [0_u8; PUBLIC_KEY_LEN];
responder_public.copy_from_slice(&body[..PUBLIC_KEY_LEN]);
let mut responder_nonce = [0_u8; NONCE_LEN];
responder_nonce.copy_from_slice(&body[PUBLIC_KEY_LEN..PUBLIC_KEY_LEN + NONCE_LEN]);
let mut device_public = [0_u8; PUBLIC_KEY_LEN];
device_public.copy_from_slice(
&body[PUBLIC_KEY_LEN + NONCE_LEN..PUBLIC_KEY_LEN + NONCE_LEN + PUBLIC_KEY_LEN],
);
let mut tag = [0_u8; CONFIRMATION_LEN];
tag.copy_from_slice(&body[PUBLIC_KEY_LEN + NONCE_LEN + PUBLIC_KEY_LEN..]);
let responder_public = PublicKey::from_sec1_bytes(&responder_public).map_err(|_| {
Error::AuthMessageMalformed {
reason: "the ephemeral public key in the response is not a valid P-256 SEC1 point",
}
})?;
let device_key = DevicePublicKey::from_sec1_bytes(&device_public).map_err(|_| {
Error::AuthMessageMalformed {
reason: "the device public key in the response is not a valid P-256 SEC1 point",
}
})?;
let session_id = session_id_of(
&self.public_key,
&self.nonce,
&body[..PUBLIC_KEY_LEN],
&responder_nonce,
);
let shared = self.secret.diffie_hellman(&responder_public);
let keys = AuthKeys::derive(master, shared.raw_secret_bytes(), &session_id);
if !constant_time_eq(&tag, &proof_tag(&keys.proof, &session_id)) {
return Err(Error::AuthProofFailed);
}
let message = signature_message(
&session_id,
&self.public_key,
&self.nonce,
&body[..PUBLIC_KEY_LEN],
&responder_nonce,
&tag,
);
qualia_dik::device::verify(
&device_key,
&message,
&signature_of(&response[RESPONSE_LEN..])?,
)?;
Ok(Verified {
session_id,
device_key,
device_public,
confirm: keys.confirm,
})
}
}
impl core::fmt::Debug for Verifier {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Verifier")
.field("public_key", &"<65 bytes>")
.field("nonce", &"<redacted>")
.field("secret", &"<redacted>")
.finish()
}
}
pub struct Verified {
session_id: [u8; SESSION_ID_LEN],
device_key: DevicePublicKey,
device_public: [u8; PUBLIC_KEY_LEN],
confirm: [u8; 32],
}
impl Verified {
#[must_use]
pub const fn session_id(&self) -> &[u8; SESSION_ID_LEN] {
&self.session_id
}
#[must_use]
pub const fn device_key(&self) -> &DevicePublicKey {
&self.device_key
}
#[must_use]
pub const fn device_public(&self) -> &[u8; PUBLIC_KEY_LEN] {
&self.device_public
}
#[must_use]
pub fn confirmation(&self) -> [u8; CONFIRM_LEN] {
confirm_tag(&self.confirm, &self.session_id)
}
}
impl core::fmt::Debug for Verified {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Verified")
.field("session_id", &&self.session_id[..8])
.field("device_id", &self.device_key.device_id())
.field("confirm", &"<redacted>")
.finish()
}
}
pub struct Responder {
secret: EphemeralSecret,
public_key: [u8; PUBLIC_KEY_LEN],
nonce: [u8; NONCE_LEN],
verifier_public: [u8; PUBLIC_KEY_LEN],
verifier_nonce: [u8; NONCE_LEN],
}
impl Responder {
#[must_use]
pub fn session_id(&self) -> [u8; SESSION_ID_LEN] {
session_id_of(
&self.verifier_public,
&self.verifier_nonce,
&self.public_key,
&self.nonce,
)
}
#[must_use]
pub const fn public_key(&self) -> &[u8; PUBLIC_KEY_LEN] {
&self.public_key
}
#[must_use]
pub const fn nonce(&self) -> &[u8; NONCE_LEN] {
&self.nonce
}
pub fn respond(
&self,
master: &MasterKey,
device_public: &[u8],
sign: impl FnOnce(&[u8]) -> Result<DeviceSignature>,
) -> Result<Vec<u8>> {
if device_public.len() != PUBLIC_KEY_LEN {
return Err(Error::AuthMessageMalformed {
reason: "device identity public key length is not 65 bytes",
});
}
DevicePublicKey::from_sec1_bytes(device_public).map_err(|_| {
Error::AuthMessageMalformed {
reason: "the device identity public key is not a valid P-256 SEC1 point",
}
})?;
let verifier_public = PublicKey::from_sec1_bytes(&self.verifier_public).map_err(|_| {
Error::AuthMessageMalformed {
reason: "the ephemeral public key in the challenge is not a valid P-256 SEC1 point",
}
})?;
let session_id = self.session_id();
let shared = self.secret.diffie_hellman(&verifier_public);
let keys = AuthKeys::derive(master, shared.raw_secret_bytes(), &session_id);
let tag = proof_tag(&keys.proof, &session_id);
let message = signature_message(
&session_id,
&self.verifier_public,
&self.verifier_nonce,
&self.public_key,
&self.nonce,
&tag,
);
let signature = sign(&message)?;
let mut out = Vec::with_capacity(RESPONSE_SIGNED_LEN);
out.extend_from_slice(&self.public_key);
out.extend_from_slice(&self.nonce);
out.extend_from_slice(device_public);
out.extend_from_slice(&tag);
out.extend_from_slice(signature.as_bytes());
Ok(out)
}
}
impl core::fmt::Debug for Responder {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Responder")
.field("public_key", &"<65 bytes>")
.field("nonce", &"<redacted>")
.field("secret", &"<redacted>")
.finish()
}
}
#[must_use = "the challenge must be sent out, otherwise this authentication cannot start"]
pub fn begin() -> Result<(Vec<u8>, Verifier)> {
let secret = EphemeralSecret::generate();
let point = secret.public_key().to_sec1_point(false);
let mut public_key = [0_u8; PUBLIC_KEY_LEN];
public_key.copy_from_slice(point.as_bytes());
let mut nonce = [0_u8; NONCE_LEN];
crate::getrandom::fill(&mut nonce).map_err(|_| Error::RandomnessUnavailable)?;
let mut challenge = Vec::with_capacity(CHALLENGE_LEN);
challenge.extend_from_slice(&public_key);
challenge.extend_from_slice(&nonce);
Ok((
challenge,
Verifier {
secret,
nonce,
public_key,
},
))
}
pub fn accept(challenge: &[u8]) -> Result<Responder> {
if challenge.len() != CHALLENGE_LEN {
return Err(Error::AuthMessageMalformed {
reason: "wrong challenge length",
});
}
let mut verifier_public = [0_u8; PUBLIC_KEY_LEN];
verifier_public.copy_from_slice(&challenge[..PUBLIC_KEY_LEN]);
let mut verifier_nonce = [0_u8; NONCE_LEN];
verifier_nonce.copy_from_slice(&challenge[PUBLIC_KEY_LEN..]);
PublicKey::from_sec1_bytes(&verifier_public).map_err(|_| Error::AuthMessageMalformed {
reason: "the ephemeral public key in the challenge is not a valid P-256 SEC1 point",
})?;
let secret = EphemeralSecret::generate();
let point = secret.public_key().to_sec1_point(false);
let mut public_key = [0_u8; PUBLIC_KEY_LEN];
public_key.copy_from_slice(point.as_bytes());
let mut nonce = [0_u8; NONCE_LEN];
crate::getrandom::fill(&mut nonce).map_err(|_| Error::RandomnessUnavailable)?;
Ok(Responder {
secret,
public_key,
nonce,
verifier_public,
verifier_nonce,
})
}
pub fn confirm(
master: &MasterKey,
responder: &Responder,
session_id: &[u8; SESSION_ID_LEN],
confirmation: &[u8],
) -> Result<()> {
if confirmation.len() != CONFIRM_LEN {
return Err(Error::AuthMessageMalformed {
reason: "wrong confirmation tag length",
});
}
if *session_id != responder.session_id() {
return Err(Error::AuthProofFailed);
}
let verifier_public = PublicKey::from_sec1_bytes(&responder.verifier_public).map_err(|_| {
Error::AuthMessageMalformed {
reason: "the ephemeral public key in the challenge is not a valid P-256 SEC1 point",
}
})?;
let shared = responder.secret.diffie_hellman(&verifier_public);
let keys = AuthKeys::derive(master, shared.raw_secret_bytes(), session_id);
if !constant_time_eq(confirmation, &confirm_tag(&keys.confirm, session_id)) {
return Err(Error::AuthProofFailed);
}
Ok(())
}
struct AuthKeys {
proof: [u8; 32],
confirm: [u8; 32],
}
impl AuthKeys {
fn derive(master: &MasterKey, shared: &[u8], session_id: &[u8; SESSION_ID_LEN]) -> Self {
let hkdf = Hkdf::<Sha256>::new(Some(shared), master.expose());
let mut proof = [0_u8; 32];
let mut confirm = [0_u8; 32];
let failed = hkdf.expand(HKDF_INFO_PROOF, &mut proof).is_err()
|| hkdf.expand(HKDF_INFO_CONFIRM, &mut confirm).is_err();
if failed {
proof.zeroize();
confirm.zeroize();
}
let _ = session_id;
Self { proof, confirm }
}
}
impl Drop for AuthKeys {
fn drop(&mut self) {
self.proof.zeroize();
self.confirm.zeroize();
}
}
fn session_id_of(
verifier_public: &[u8; PUBLIC_KEY_LEN],
verifier_nonce: &[u8; NONCE_LEN],
responder_public: &[u8],
responder_nonce: &[u8; NONCE_LEN],
) -> [u8; SESSION_ID_LEN] {
let mut hasher = Sha256::new();
hasher.update(SESSION_LABEL);
hasher.update(verifier_public);
hasher.update(verifier_nonce);
hasher.update(responder_public);
hasher.update(responder_nonce);
let digest = hasher.finalize();
let mut out = [0_u8; SESSION_ID_LEN];
out.copy_from_slice(&digest);
out
}
fn signature_message(
session_id: &[u8; SESSION_ID_LEN],
verifier_public: &[u8; PUBLIC_KEY_LEN],
verifier_nonce: &[u8; NONCE_LEN],
responder_public: &[u8],
responder_nonce: &[u8; NONCE_LEN],
tag: &[u8; CONFIRMATION_LEN],
) -> Vec<u8> {
let mut message = Vec::with_capacity(
SIGNATURE_LABEL.len()
+ SESSION_ID_LEN
+ PUBLIC_KEY_LEN
+ NONCE_LEN
+ responder_public.len()
+ NONCE_LEN
+ CONFIRMATION_LEN,
);
message.extend_from_slice(SIGNATURE_LABEL);
message.extend_from_slice(session_id);
message.extend_from_slice(verifier_public);
message.extend_from_slice(verifier_nonce);
message.extend_from_slice(responder_public);
message.extend_from_slice(responder_nonce);
message.extend_from_slice(tag);
message
}
fn signature_of(bytes: &[u8]) -> Result<DeviceSignature> {
Ok(DeviceSignature::from_bytes(bytes)?)
}
fn proof_tag(key: &[u8; 32], session_id: &[u8; SESSION_ID_LEN]) -> [u8; CONFIRMATION_LEN] {
tag(key, PROOF_LABEL, session_id)
}
fn confirm_tag(key: &[u8; 32], session_id: &[u8; SESSION_ID_LEN]) -> [u8; CONFIRM_LEN] {
tag(key, CONFIRM_LABEL, session_id)
}
fn tag(key: &[u8; 32], label: &[u8], session_id: &[u8; SESSION_ID_LEN]) -> [u8; CONFIRMATION_LEN] {
let Ok(mut mac) = <Hmac<Sha256> as hmac::digest::KeyInit>::new_from_slice(key) else {
return [0_u8; CONFIRMATION_LEN];
};
mac.update(label);
mac.update(session_id);
let digest = mac.finalize().into_bytes();
let mut out = [0_u8; CONFIRMATION_LEN];
out.copy_from_slice(&digest);
out
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
use subtle::ConstantTimeEq;
if a.len() != b.len() {
return false;
}
a.ct_eq(b).into()
}
#[cfg(test)]
mod tests {
use super::*;
use p256::ecdsa::{SigningKey, signature::Signer as _};
fn device(scalar: u8) -> (SigningKey, [u8; PUBLIC_KEY_LEN]) {
let mut bytes = [0_u8; 32];
bytes[31] = scalar;
let signing = SigningKey::from_bytes((&bytes).into()).expect("a valid scalar");
let public = signing.verifying_key().to_sec1_point(false);
let mut out = [0_u8; PUBLIC_KEY_LEN];
out.copy_from_slice(public.as_bytes());
(signing, out)
}
fn new_master() -> MasterKey {
MasterKey::generate().expect("the random source is available")
}
fn handshake(master: &MasterKey) -> (Verified, Responder, [u8; PUBLIC_KEY_LEN], Vec<u8>) {
let (signing, device_public) = device(7);
let (challenge, verifier) = begin().expect("challenge generation succeeds");
let responder = accept(&challenge).expect("challenge accepted");
let response = responder
.respond(master, &device_public, |message| {
let signature: p256::ecdsa::Signature = signing.sign(message);
signature_of(&signature.to_bytes())
})
.expect("response generation succeeds");
let verified = verifier
.verify(&response, master)
.expect("response verification succeeds");
(verified, responder, device_public, response)
}
#[test]
fn handshake_succeeds_and_both_sides_agree_on_the_session() {
let master = new_master();
let (verified, responder, device_public, _) = handshake(&master);
assert_eq!(
verified.session_id(),
&responder.session_id(),
"both sides must compute the same session identifier"
);
assert_eq!(verified.device_public(), &device_public);
confirm(
&master,
&responder,
verified.session_id(),
&verified.confirmation(),
)
.expect("the confirmation tag must be accepted");
}
#[test]
fn responder_without_the_master_key_cannot_answer() {
let real = new_master();
let wrong = new_master();
let (_, device_public) = device(7);
let (challenge, verifier) = begin().expect("challenge generation succeeds");
let responder = accept(&challenge).expect("challenge accepted");
let response = responder
.respond(&wrong, &device_public, |digest| {
let (signing, _) = device(7);
let signature: p256::ecdsa::Signature = signing.sign(digest);
signature_of(&signature.to_bytes())
})
.expect("signing itself succeeds");
assert!(
matches!(
verifier.verify(&response, &real),
Err(Error::AuthProofFailed)
),
"authentication passed with the wrong master key"
);
}
#[test]
fn forged_signature_is_rejected_even_with_a_valid_tag() {
let master = new_master();
let (challenge, verifier) = begin().expect("challenge generation succeeds");
let responder = accept(&challenge).expect("challenge accepted");
let (real_signing, real_device) = device(7);
let response = responder
.respond(&master, &real_device, |message| {
let signature: p256::ecdsa::Signature = real_signing.sign(message);
signature_of(&signature.to_bytes())
})
.expect("response generation succeeds");
let (_attacker_signing, attacker_device) = device(9);
let mut forged = response.clone();
for byte in &mut forged[RESPONSE_LEN..] {
*byte = 0;
}
assert_ne!(attacker_device, real_device);
assert!(
matches!(
verifier.verify(&forged, &master),
Err(Error::Device(
qualia_dik::Error::DeviceSignatureInvalid { .. }
))
),
"a response with a replaced invalid signature was accepted"
);
}
#[test]
fn tampering_with_the_response_is_detected() {
let master = new_master();
let (signing, device_public) = device(7);
for index in 0..RESPONSE_SIGNED_LEN {
let (challenge, verifier) = begin().expect("challenge generation succeeds");
let responder = accept(&challenge).expect("challenge accepted");
let mut response = responder
.respond(&master, &device_public, |digest| {
let signature: p256::ecdsa::Signature = signing.sign(digest);
signature_of(&signature.to_bytes())
})
.expect("response generation succeeds");
response[index] ^= 0x01;
assert!(
verifier.verify(&response, &master).is_err(),
"byte {index} was flipped but authentication still passed"
);
}
}
#[test]
fn challenge_with_wrong_length_is_rejected() {
for length in [0_usize, 1, CHALLENGE_LEN - 1, CHALLENGE_LEN + 1] {
let mut challenge = vec![0_u8; length];
if length > 0 {
challenge[0] = 0x04;
}
assert!(
matches!(accept(&challenge), Err(Error::AuthMessageMalformed { .. })),
"a challenge of length {length} must be rejected"
);
}
}
#[test]
fn challenge_with_off_curve_key_is_rejected() {
let (challenge, _) = begin().expect("challenge generation succeeds");
let mut broken = challenge.clone();
broken[PUBLIC_KEY_LEN - 1] ^= 0x01;
assert!(matches!(
accept(&broken),
Err(Error::AuthMessageMalformed { .. })
));
}
#[test]
fn response_with_off_curve_key_is_rejected() {
let master = new_master();
let (challenge, verifier) = begin().expect("challenge generation succeeds");
let responder = accept(&challenge).expect("challenge accepted");
let (signing, device_public) = device(7);
let mut response = responder
.respond(&master, &device_public, |message| {
let signature: p256::ecdsa::Signature = signing.sign(message);
signature_of(&signature.to_bytes())
})
.expect("response generation succeeds");
response[PUBLIC_KEY_LEN - 1] ^= 0x01;
assert!(matches!(
verifier.verify(&response, &master),
Err(Error::AuthMessageMalformed { .. })
));
}
#[test]
fn wrong_confirmation_is_rejected() {
let master = new_master();
let (verified, responder, _, _) = handshake(&master);
let mut confirmation = verified.confirmation();
confirmation[0] ^= 0x01;
assert!(matches!(
confirm(&master, &responder, verified.session_id(), &confirmation),
Err(Error::AuthProofFailed)
));
assert!(matches!(
confirm(
&master,
&responder,
verified.session_id(),
&confirmation[..1]
),
Err(Error::AuthMessageMalformed { .. })
));
}
#[test]
fn confirmation_with_a_mismatched_session_id_is_rejected() {
let master = new_master();
let (verified, responder, _, _) = handshake(&master);
let mut wrong_session = *verified.session_id();
wrong_session[0] ^= 0x01;
assert!(matches!(
confirm(
&master,
&responder,
&wrong_session,
&verified.confirmation()
),
Err(Error::AuthProofFailed)
));
}
#[test]
fn confirmation_with_the_wrong_master_key_is_rejected() {
let master = new_master();
let (verified, responder, _, _) = handshake(&master);
let other = new_master();
assert!(matches!(
confirm(
&other,
&responder,
verified.session_id(),
&verified.confirmation()
),
Err(Error::AuthProofFailed)
));
}
#[test]
fn two_sessions_never_share_a_session_id_or_tag() {
let master = new_master();
let mut sessions = Vec::new();
let mut tags = Vec::new();
for _ in 0..8 {
let (_, _, _, response) = handshake(&master);
let (challenge, verifier) = begin().expect("challenge generation succeeds");
let responder = accept(&challenge).expect("challenge accepted");
sessions.push(responder.session_id());
tags.push(response[PUBLIC_KEY_LEN + NONCE_LEN + PUBLIC_KEY_LEN..].to_vec());
drop(verifier);
}
for i in 0..sessions.len() {
for j in (i + 1)..sessions.len() {
assert_ne!(
sessions[i], sessions[j],
"two sessions produced the same session identifier"
);
assert_ne!(tags[i], tags[j], "two sessions produced the same proof tag");
}
}
}
#[test]
fn message_lengths_are_stable() {
assert_eq!(CHALLENGE_LEN, 65 + 32);
assert_eq!(RESPONSE_LEN, 65 + 32 + 65 + 32);
assert_eq!(RESPONSE_SIGNED_LEN, RESPONSE_LEN + 64);
assert_eq!(CONFIRM_LEN, 32);
}
#[test]
fn response_never_contains_the_master_key() {
let master = new_master();
let (_, _, _, response) = handshake(&master);
assert!(
!response
.windows(crate::vault::MASTER_KEY_LEN)
.any(|w| w == master.expose()),
"the master key appears in plaintext inside the response"
);
}
#[test]
fn debug_output_is_redacted() {
let master = new_master();
let (challenge, verifier) = begin().expect("challenge generation succeeds");
let responder = accept(&challenge).expect("challenge accepted");
let (signing, device_public) = device(7);
let response = responder
.respond(&master, &device_public, |message| {
let signature: p256::ecdsa::Signature = signing.sign(message);
signature_of(&signature.to_bytes())
})
.expect("response generation succeeds");
let verifier_text = format!("{verifier:?}");
let responder_text = format!("{responder:?}");
let verified = verifier
.verify(&response, &master)
.expect("verification succeeds");
let verified_text = format!("{verified:?}");
let master_hex: String = master.expose().iter().map(|b| format!("{b:02x}")).collect();
let responder_nonce_hex: String = responder_side_nonce(&response)
.iter()
.map(|b| format!("{b:02x}"))
.collect();
for (what, text) in [
("Verifier", &verifier_text),
("Responder", &responder_text),
("Verified", &verified_text),
] {
assert!(
!text.contains(&master_hex),
"the Debug output of {what} leaks the master key"
);
assert!(
!text.contains(&responder_nonce_hex),
"the Debug output of {what} leaks the nonce"
);
assert!(
text.contains("redacted") || text.contains("bytes"),
"the Debug output of {what} has no redaction placeholder: {text}"
);
}
}
fn responder_side_nonce(response: &[u8]) -> [u8; NONCE_LEN] {
let mut out = [0_u8; NONCE_LEN];
out.copy_from_slice(&response[PUBLIC_KEY_LEN..PUBLIC_KEY_LEN + NONCE_LEN]);
out
}
#[test]
fn constant_time_eq_behaves() {
assert!(constant_time_eq(&[1, 2, 3], &[1, 2, 3]));
assert!(!constant_time_eq(&[1, 2, 3], &[1, 2, 4]));
assert!(
!constant_time_eq(&[1, 2], &[1, 2, 3]),
"different lengths must be false"
);
assert!(constant_time_eq(&[], &[]));
}
}