use std::sync::LazyLock;
use p256::elliptic_curve::group::ff::{Field, PrimeField}; use p256::elliptic_curve::sec1::FromEncodedPoint;
use p256::elliptic_curve::sec1::ToEncodedPoint;
use p256::{AffinePoint, EncodedPoint, ProjectivePoint, Scalar};
use ring::digest::{digest, SHA256};
use ring::hmac;
use ring::rand::SecureRandom;
use subtle::ConstantTimeEq;
use crate::error::{Error, Result};
use crate::pase::kdf::hkdf_expand;
#[rustfmt::skip]
const M_BYTES: [u8; 65] = [
0x04, 0x88, 0x6e, 0x2f, 0x97, 0xac, 0xe4, 0x6e, 0x55, 0xba, 0x9d, 0xd7, 0x24, 0x25, 0x79, 0xf2,
0x99, 0x3b, 0x64, 0xe1, 0x6e, 0xf3, 0xdc, 0xab, 0x95, 0xaf, 0xd4, 0x97, 0x33, 0x3d, 0x8f, 0xa1,
0x2f, 0x5f, 0xf3, 0x55, 0x16, 0x3e, 0x43, 0xce, 0x22, 0x4e, 0x0b, 0x0e, 0x65, 0xff, 0x02, 0xac,
0x8e, 0x5c, 0x7b, 0xe0, 0x94, 0x19, 0xc7, 0x85, 0xe0, 0xca, 0x54, 0x7d, 0x55, 0xa1, 0x2e, 0x2d,
0x20,
];
#[rustfmt::skip]
const N_BYTES: [u8; 65] = [
0x04, 0xd8, 0xbb, 0xd6, 0xc6, 0x39, 0xc6, 0x29, 0x37, 0xb0, 0x4d, 0x99, 0x7f, 0x38, 0xc3, 0x77,
0x07, 0x19, 0xc6, 0x29, 0xd7, 0x01, 0x4d, 0x49, 0xa2, 0x4b, 0x4f, 0x98, 0xba, 0xa1, 0x29, 0x2b,
0x49, 0x07, 0xd6, 0x0a, 0xa6, 0xbf, 0xad, 0xe4, 0x50, 0x08, 0xa6, 0x36, 0x33, 0x7f, 0x51, 0x68,
0xc6, 0x4d, 0x9b, 0xd3, 0x60, 0x34, 0x80, 0x8c, 0xd5, 0x64, 0x49, 0x0b, 0x1e, 0x65, 0x6e, 0xdb,
0xe7,
];
static M_POINT: LazyLock<ProjectivePoint> = LazyLock::new(|| point_from_spec_bytes(&M_BYTES));
static N_POINT: LazyLock<ProjectivePoint> = LazyLock::new(|| point_from_spec_bytes(&N_BYTES));
const INFO_CONFIRMATION_KEYS: &[u8] = b"ConfirmationKeys";
const INFO_SESSION_KEYS: &[u8] = b"SessionKeys";
#[allow(clippy::expect_used)] fn point_from_spec_bytes(bytes: &[u8; 65]) -> ProjectivePoint {
let encoded = EncodedPoint::from_bytes(bytes.as_slice())
.expect("M/N bytes are valid SEC1 uncompressed encoding");
let affine_opt: Option<AffinePoint> = AffinePoint::from_encoded_point(&encoded).into();
affine_opt
.map(ProjectivePoint::from)
.expect("M/N bytes are a valid P-256 affine point")
}
fn decode_peer_point(bytes: &[u8; 65]) -> Result<ProjectivePoint> {
let encoded =
EncodedPoint::from_bytes(bytes.as_slice()).map_err(|_| Error::InvalidParameter)?;
let affine_opt: Option<AffinePoint> = AffinePoint::from_encoded_point(&encoded).into();
affine_opt
.map(ProjectivePoint::from)
.ok_or(Error::InvalidParameter)
}
fn encode_point(p: &ProjectivePoint) -> [u8; 65] {
let encoded = p.to_affine().to_encoded_point(false);
let mut out = [0u8; 65];
out.copy_from_slice(encoded.as_bytes());
out
}
pub(crate) fn sample_scalar(rng: &dyn SecureRandom) -> Result<Scalar> {
for _ in 0..16 {
let mut bytes = [0u8; 32];
rng.fill(&mut bytes).map_err(|_| Error::InvalidScalar)?;
let scalar_opt: Option<Scalar> = Scalar::from_repr(bytes.into()).into();
if let Some(s) = scalar_opt {
if !bool::from(s.is_zero()) {
return Ok(s);
}
}
}
Err(Error::InvalidScalar)
}
pub(crate) fn compute_x(x: &Scalar, w0: &Scalar) -> [u8; 65] {
let xp = ProjectivePoint::GENERATOR * x;
let w0m = *M_POINT * w0;
encode_point(&(xp + w0m))
}
pub(crate) fn compute_y(y: &Scalar, w0: &Scalar) -> [u8; 65] {
let yp = ProjectivePoint::GENERATOR * y;
let w0n = *N_POINT * w0;
encode_point(&(yp + w0n))
}
pub(crate) fn compute_z_v_prover(
x: &Scalar,
w0: &Scalar,
w1: &Scalar,
y_bytes: &[u8; 65],
) -> Result<([u8; 65], [u8; 65])> {
let y = decode_peer_point(y_bytes)?;
let w0n = *N_POINT * w0;
let yn = y - w0n;
let z = yn * x;
let v = yn * w1;
Ok((encode_point(&z), encode_point(&v)))
}
pub(crate) fn compute_z_v_verifier(
y: &Scalar,
w0: &Scalar,
l_bytes: &[u8; 65],
x_bytes: &[u8; 65],
) -> Result<([u8; 65], [u8; 65])> {
let x_point = decode_peer_point(x_bytes)?;
let w0m = *M_POINT * w0;
let x_minus_w0m = x_point - w0m;
let z_point = x_minus_w0m * y;
let l_point = decode_peer_point(l_bytes)?;
let v_point = l_point * y;
Ok((encode_point(&z_point), encode_point(&v_point)))
}
pub(crate) fn transcript_hash(
context: &[u8],
x_bytes: &[u8; 65],
y_bytes: &[u8; 65],
z_bytes: &[u8; 65],
v_bytes: &[u8; 65],
w0: &Scalar,
) -> [u8; 32] {
let m_bytes = &M_BYTES;
let n_bytes = &N_BYTES;
let w0_be = scalar_to_be_bytes(w0);
let empty: &[u8] = b"";
let mut buf: Vec<u8> = Vec::with_capacity(1024);
append_length_prefixed(&mut buf, context);
append_length_prefixed(&mut buf, empty); append_length_prefixed(&mut buf, empty); append_length_prefixed(&mut buf, m_bytes.as_slice());
append_length_prefixed(&mut buf, n_bytes.as_slice());
append_length_prefixed(&mut buf, x_bytes.as_slice());
append_length_prefixed(&mut buf, y_bytes.as_slice());
append_length_prefixed(&mut buf, z_bytes.as_slice());
append_length_prefixed(&mut buf, v_bytes.as_slice());
append_length_prefixed(&mut buf, w0_be.as_slice());
let d = digest(&SHA256, &buf);
let mut out = [0u8; 32];
out.copy_from_slice(d.as_ref());
out
}
fn append_length_prefixed(buf: &mut Vec<u8>, data: &[u8]) {
let len_u64 = data.len() as u64;
buf.extend_from_slice(&len_u64.to_le_bytes());
buf.extend_from_slice(data);
}
fn scalar_to_be_bytes(s: &Scalar) -> [u8; 32] {
let fb = s.to_bytes();
let mut out = [0u8; 32];
out.copy_from_slice(&fb);
out
}
pub(crate) fn ka_ke_from_transcript(t_t: &[u8; 32]) -> ([u8; 16], [u8; 16]) {
let mut ka = [0u8; 16];
let mut ke = [0u8; 16];
ka.copy_from_slice(&t_t[..16]);
ke.copy_from_slice(&t_t[16..]);
(ka, ke)
}
#[allow(clippy::similar_names)]
pub(crate) fn derive_confirmation_keys(ka: &[u8; 16]) -> Result<([u8; 16], [u8; 16])> {
let mut out = [0u8; 32];
hkdf_expand(ka, INFO_CONFIRMATION_KEYS, &mut out)?;
let mut kca = [0u8; 16];
let mut kcb = [0u8; 16];
kca.copy_from_slice(&out[..16]);
kcb.copy_from_slice(&out[16..]);
Ok((kca, kcb))
}
pub(crate) fn compute_ca(kca: &[u8; 16], y_bytes: &[u8; 65]) -> [u8; 32] {
let key = hmac::Key::new(hmac::HMAC_SHA256, kca);
let tag = hmac::sign(&key, y_bytes.as_slice());
let mut out = [0u8; 32];
out.copy_from_slice(tag.as_ref());
out
}
pub(crate) fn compute_cb(kcb: &[u8; 16], x_bytes: &[u8; 65]) -> [u8; 32] {
let key = hmac::Key::new(hmac::HMAC_SHA256, kcb);
let tag = hmac::sign(&key, x_bytes.as_slice());
let mut out = [0u8; 32];
out.copy_from_slice(tag.as_ref());
out
}
pub(crate) fn verify_tag(expected: &[u8; 32], received: &[u8; 32]) -> Result<()> {
if expected.ct_eq(received).unwrap_u8() == 1 {
Ok(())
} else {
Err(Error::ConfirmationTagMismatch)
}
}
pub(crate) fn derive_session_keys(ke: &[u8; 16]) -> Result<[u8; 48]> {
let mut out = [0u8; 48];
hkdf_expand(ke, INFO_SESSION_KEYS, &mut out)?;
Ok(out)
}
const SPAKE_CONTEXT: &[u8] = b"CHIP PAKE V1 Commissioning";
pub(crate) fn hash_context(extras: &[&[u8]]) -> [u8; 32] {
let mut buf: Vec<u8> =
Vec::with_capacity(SPAKE_CONTEXT.len() + extras.iter().map(|e| e.len()).sum::<usize>());
buf.extend_from_slice(SPAKE_CONTEXT);
for e in extras {
buf.extend_from_slice(e);
}
let d = digest(&SHA256, &buf);
let mut out = [0u8; 32];
out.copy_from_slice(d.as_ref());
out
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
use ring::rand::SystemRandom;
#[test]
fn hash_context_empty_extras_is_deterministic() {
let a = hash_context(&[]);
let b = hash_context(&[]);
assert_eq!(a, b);
}
#[test]
fn hash_context_with_extras_differs_from_empty() {
let empty = hash_context(&[]);
let with_extra = hash_context(&[b"hello"]);
assert_ne!(empty, with_extra);
}
#[test]
fn hash_context_is_sha256_of_spake_context_concatenation() {
let req = b"req_bytes";
let resp = b"resp_bytes";
let mut buf = Vec::new();
buf.extend_from_slice(b"CHIP PAKE V1 Commissioning");
buf.extend_from_slice(req);
buf.extend_from_slice(resp);
let expected_raw = digest(&SHA256, &buf);
let mut expected = [0u8; 32];
expected.copy_from_slice(expected_raw.as_ref());
let got = hash_context(&[req, resp]);
assert_eq!(got, expected);
}
#[test]
fn m_and_n_decode_as_valid_p256_points() {
let m = point_from_spec_bytes(&M_BYTES);
let n = point_from_spec_bytes(&N_BYTES);
assert_ne!(encode_point(&m), [0u8; 65]);
assert_ne!(encode_point(&n), [0u8; 65]);
assert_ne!(encode_point(&m), encode_point(&n));
}
#[test]
fn m_and_n_round_trip() {
let m = point_from_spec_bytes(&M_BYTES);
let n = point_from_spec_bytes(&N_BYTES);
assert_eq!(encode_point(&m), M_BYTES);
assert_eq!(encode_point(&n), N_BYTES);
}
#[test]
fn lazylock_m_n_points_pin_to_decoded_bytes() {
assert_eq!(*M_POINT, point_from_spec_bytes(&M_BYTES));
assert_eq!(*N_POINT, point_from_spec_bytes(&N_BYTES));
}
#[test]
fn sample_scalar_yields_nonzero() {
let rng = SystemRandom::new();
for _ in 0..16 {
let s = sample_scalar(&rng).unwrap();
assert!(!bool::from(s.is_zero()));
}
}
#[test]
fn compute_x_starts_with_uncompressed_prefix() {
let rng = SystemRandom::new();
let x = sample_scalar(&rng).unwrap();
let w0 = sample_scalar(&rng).unwrap();
let x_bytes = compute_x(&x, &w0);
assert_eq!(
x_bytes[0], 0x04,
"X must be SEC1 uncompressed (prefix 0x04)"
);
}
#[test]
fn compute_y_starts_with_uncompressed_prefix() {
let rng = SystemRandom::new();
let y = sample_scalar(&rng).unwrap();
let w0 = sample_scalar(&rng).unwrap();
let y_bytes = compute_y(&y, &w0);
assert_eq!(
y_bytes[0], 0x04,
"Y must be SEC1 uncompressed (prefix 0x04)"
);
}
#[test]
fn compute_z_v_prover_and_verifier_agree() {
let rng = SystemRandom::new();
let scalar_x = sample_scalar(&rng).unwrap();
let scalar_y = sample_scalar(&rng).unwrap();
let w0 = sample_scalar(&rng).unwrap();
let w1 = sample_scalar(&rng).unwrap();
let x_bytes = compute_x(&scalar_x, &w0);
let y_bytes = compute_y(&scalar_y, &w0);
let l_bytes = encode_point(&(ProjectivePoint::GENERATOR * w1));
let (z_prover, v_prover) = compute_z_v_prover(&scalar_x, &w0, &w1, &y_bytes).unwrap();
let (z_verifier, v_verifier) =
compute_z_v_verifier(&scalar_y, &w0, &l_bytes, &x_bytes).unwrap();
assert_eq!(
z_prover, z_verifier,
"Z must match between prover and verifier"
);
assert_eq!(
v_prover, v_verifier,
"V must match between prover and verifier"
);
}
#[test]
fn transcript_hash_is_deterministic() {
let rng = SystemRandom::new();
let w0 = sample_scalar(&rng).unwrap();
let context = b"CHIP PAKE V1 Commissioning";
let x_pt = [0x01u8; 65];
let y_pt = [0x02u8; 65];
let z_pt = [0x03u8; 65];
let v_pt = [0x04u8; 65];
let hash_a = transcript_hash(context, &x_pt, &y_pt, &z_pt, &v_pt, &w0);
let hash_b = transcript_hash(context, &x_pt, &y_pt, &z_pt, &v_pt, &w0);
assert_eq!(hash_a, hash_b, "transcript_hash must be deterministic");
}
#[test]
fn transcript_hash_is_input_sensitive() {
let rng = SystemRandom::new();
let w0 = sample_scalar(&rng).unwrap();
let context = b"CHIP PAKE V1 Commissioning";
let x_pt = [0x01u8; 65];
let y_pt = [0x02u8; 65];
let z_pt = [0x03u8; 65];
let v_pt = [0x04u8; 65];
let base = transcript_hash(context, &x_pt, &y_pt, &z_pt, &v_pt, &w0);
let mut x_pt2 = x_pt;
x_pt2[10] ^= 1;
let changed = transcript_hash(context, &x_pt2, &y_pt, &z_pt, &v_pt, &w0);
assert_ne!(base, changed, "different X must produce different hash");
}
#[test]
#[allow(clippy::cast_possible_truncation)] fn ka_ke_split_is_correct() {
let t_t: [u8; 32] = {
let mut buf = [0u8; 32];
for (i, byte) in buf.iter_mut().enumerate() {
*byte = i as u8;
}
buf
};
let (ka, ke) = ka_ke_from_transcript(&t_t);
assert_eq!(ka, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
assert_eq!(
ke,
[16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
);
assert_ne!(ka, ke, "Ka and Ke must be different halves");
}
#[test]
#[allow(clippy::similar_names)]
fn confirmation_keys_split_correctly() {
let ka = [0x42u8; 16];
let (kca, kcb) = derive_confirmation_keys(&ka).unwrap();
assert_ne!(kca, kcb, "KcA and KcB must differ");
assert_eq!(kca.len(), 16);
assert_eq!(kcb.len(), 16);
}
#[test]
fn confirmation_tags_are_input_sensitive() {
let kca = [0x11u8; 16];
let y1 = [0x22u8; 65];
let mut y2 = y1;
y2[5] ^= 1;
assert_ne!(
compute_ca(&kca, &y1),
compute_ca(&kca, &y2),
"different Y must produce different cA"
);
}
#[test]
fn verify_tag_accepts_matching_tags() {
let t1 = [0x11u8; 32];
let t2 = [0x11u8; 32];
verify_tag(&t1, &t2).unwrap();
}
#[test]
fn verify_tag_rejects_mismatched_tags() {
let t1 = [0x11u8; 32];
let mut t3 = t1;
t3[31] ^= 1;
assert!(
matches!(verify_tag(&t1, &t3), Err(Error::ConfirmationTagMismatch)),
"mismatched tags must return ConfirmationTagMismatch"
);
}
#[test]
fn session_keys_are_48_bytes_and_deterministic() {
let ke = [0x42u8; 16];
let a = derive_session_keys(&ke).unwrap();
let b = derive_session_keys(&ke).unwrap();
assert_eq!(a, b, "derive_session_keys must be deterministic");
assert_eq!(a.len(), 48);
}
#[test]
#[allow(clippy::similar_names)] fn full_handshake_math_produces_matching_session_keys() {
let rng = SystemRandom::new();
let scalar_x = sample_scalar(&rng).unwrap();
let scalar_y = sample_scalar(&rng).unwrap();
let w0 = sample_scalar(&rng).unwrap();
let w1 = sample_scalar(&rng).unwrap();
let x_bytes = compute_x(&scalar_x, &w0);
let y_bytes = compute_y(&scalar_y, &w0);
let l_bytes = encode_point(&(ProjectivePoint::GENERATOR * w1));
let (z_prover, v_prover) = compute_z_v_prover(&scalar_x, &w0, &w1, &y_bytes).unwrap();
let (z_verifier, v_verifier) =
compute_z_v_verifier(&scalar_y, &w0, &l_bytes, &x_bytes).unwrap();
assert_eq!(z_prover, z_verifier, "Z must match");
assert_eq!(v_prover, v_verifier, "V must match");
let context = b"CHIP PAKE V1 Commissioning";
let tt_prover = transcript_hash(context, &x_bytes, &y_bytes, &z_prover, &v_prover, &w0);
let tt_verifier =
transcript_hash(context, &x_bytes, &y_bytes, &z_verifier, &v_verifier, &w0);
assert_eq!(tt_prover, tt_verifier, "TT_HASH must match");
let (ka_prover, ke_prover) = ka_ke_from_transcript(&tt_prover);
let (ka_verifier, ke_verifier) = ka_ke_from_transcript(&tt_verifier);
assert_eq!(ka_prover, ka_verifier, "Ka must match");
assert_eq!(ke_prover, ke_verifier, "Ke must match");
let session_keys_p = derive_session_keys(&ke_prover).unwrap();
let session_keys_v = derive_session_keys(&ke_verifier).unwrap();
assert_eq!(
session_keys_p, session_keys_v,
"SessionKeys must match end-to-end"
);
let (confirm_a_prover, confirm_b_prover) = derive_confirmation_keys(&ka_prover).unwrap();
let (confirm_a_verifier, confirm_b_verifier) =
derive_confirmation_keys(&ka_verifier).unwrap();
assert_eq!(confirm_a_prover, confirm_a_verifier, "KcA must match");
assert_eq!(confirm_b_prover, confirm_b_verifier, "KcB must match");
let tag_a_prover = compute_ca(&confirm_a_prover, &y_bytes);
let tag_a_verifier = compute_ca(&confirm_a_verifier, &y_bytes);
assert_eq!(tag_a_prover, tag_a_verifier, "cA must match");
let tag_b_prover = compute_cb(&confirm_b_prover, &x_bytes);
let tag_b_verifier = compute_cb(&confirm_b_verifier, &x_bytes);
assert_eq!(tag_b_prover, tag_b_verifier, "cB must match");
verify_tag(&tag_a_prover, &tag_a_verifier).unwrap();
verify_tag(&tag_b_prover, &tag_b_verifier).unwrap();
}
}