use ed25519_dalek::{Signer, SigningKey};
use hap_tlv8::{Tlv8Map, Tlv8Writer};
use crate::aead::{decrypt, encrypt, hap_nonce};
use crate::error::{CryptoError, Result};
use crate::kdf::hkdf_sha512;
use crate::keys::{verify_ed25519, ControllerKeypair};
use crate::pair_setup::AccessoryPairing;
use crate::tlv_types as tlv;
use crate::x25519::EphemeralKeypair;
const PV_ENCRYPT_SALT: &[u8] = b"Pair-Verify-Encrypt-Salt";
const PV_ENCRYPT_INFO: &[u8] = b"Pair-Verify-Encrypt-Info";
const CONTROL_SALT: &[u8] = b"Control-Salt";
const CONTROL_READ_INFO: &[u8] = b"Control-Read-Encryption-Key";
const CONTROL_WRITE_INFO: &[u8] = b"Control-Write-Encryption-Key";
const NONCE_M2: &[u8] = b"PV-Msg02";
const NONCE_M3: &[u8] = b"PV-Msg03";
#[derive(Clone, PartialEq, Eq)]
pub struct SessionKeys {
pub read_key: [u8; 32],
pub write_key: [u8; 32],
}
impl core::fmt::Debug for SessionKeys {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("SessionKeys").finish_non_exhaustive()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PairVerifyStep {
Send(Vec<u8>),
Done(SessionKeys),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum State {
Init,
AwaitM2,
AwaitM4,
Done,
}
pub struct PairVerifyClient {
controller_id: String,
signing: SigningKey,
accessory: AccessoryPairing,
ephemeral: EphemeralKeypair,
shared_secret: Option<[u8; 32]>,
state: State,
}
impl PairVerifyClient {
#[must_use]
pub fn new(controller: &ControllerKeypair, accessory: &AccessoryPairing) -> Self {
Self::build(controller, accessory, EphemeralKeypair::generate())
}
#[must_use]
pub fn new_with_ephemeral(
controller: &ControllerKeypair,
accessory: &AccessoryPairing,
ephemeral_secret: [u8; 32],
) -> Self {
Self::build(
controller,
accessory,
EphemeralKeypair::from_secret(ephemeral_secret),
)
}
fn build(
controller: &ControllerKeypair,
accessory: &AccessoryPairing,
ephemeral: EphemeralKeypair,
) -> Self {
Self {
controller_id: controller.id.clone(),
signing: controller.signing_key(),
accessory: accessory.clone(),
ephemeral,
shared_secret: None,
state: State::Init,
}
}
pub fn start(&mut self) -> Vec<u8> {
let mut out = Vec::new();
let mut w = Tlv8Writer::new(&mut out);
w.push_u8(tlv::STATE, tlv::STATE_M1);
w.push(tlv::PUBLIC_KEY, &self.ephemeral.public());
self.state = State::AwaitM2;
out
}
pub fn handle(&mut self, response: &[u8]) -> Result<PairVerifyStep> {
match self.state {
State::Init => Err(CryptoError::Encoding(
"Pair Verify handle called before start",
)),
State::AwaitM2 => self.handle_m2(response),
State::AwaitM4 => self.handle_m4(response),
State::Done => Err(CryptoError::Encoding(
"Pair Verify handle called after completion",
)),
}
}
fn handle_m2(&mut self, response: &[u8]) -> Result<PairVerifyStep> {
let map = Tlv8Map::parse(response)?;
check_error(&map)?;
expect_state(&map, tlv::STATE_M2)?;
let accessory_eph_pub: [u8; 32] = map
.get(tlv::PUBLIC_KEY)
.ok_or(CryptoError::Encoding("M2 missing accessory ephemeral key"))?
.try_into()
.map_err(|_| CryptoError::Encoding("M2 accessory ephemeral key not 32 bytes"))?;
let encrypted = map
.get(tlv::ENCRYPTED_DATA)
.ok_or(CryptoError::Encoding("M2 missing encrypted data"))?;
let controller_eph_pub = self.ephemeral.public();
let shared = self.ephemeral.diffie_hellman(&accessory_eph_pub);
let pv_key = derive_key(&shared, PV_ENCRYPT_SALT, PV_ENCRYPT_INFO)?;
let nonce = hap_nonce(NONCE_M2);
let plaintext = decrypt(&pv_key, &nonce, b"", encrypted)?;
let sub = Tlv8Map::parse(&plaintext)?;
let identifier = sub
.get(tlv::IDENTIFIER)
.ok_or(CryptoError::Encoding("M2 sub-TLV missing identifier"))?;
let signature: [u8; 64] = sub
.get(tlv::SIGNATURE)
.ok_or(CryptoError::Encoding("M2 sub-TLV missing signature"))?
.try_into()
.map_err(|_| CryptoError::Encoding("M2 signature not 64 bytes"))?;
if identifier != self.accessory.pairing_id.as_bytes() {
return Err(CryptoError::Encoding(
"M2 accessory identifier does not match stored pairing",
));
}
let mut signed = Vec::with_capacity(32 + identifier.len() + 32);
signed.extend_from_slice(&accessory_eph_pub);
signed.extend_from_slice(identifier);
signed.extend_from_slice(&controller_eph_pub);
verify_ed25519(&self.accessory.ltpk, &signed, &signature)?;
let m3 = self.build_m3(&pv_key, &controller_eph_pub, &accessory_eph_pub)?;
self.shared_secret = Some(shared);
self.state = State::AwaitM4;
Ok(PairVerifyStep::Send(m3))
}
fn build_m3(
&self,
pv_key: &[u8; 32],
controller_eph_pub: &[u8; 32],
accessory_eph_pub: &[u8; 32],
) -> Result<Vec<u8>> {
let id = self.controller_id.as_bytes();
let mut signed = Vec::with_capacity(32 + id.len() + 32);
signed.extend_from_slice(controller_eph_pub);
signed.extend_from_slice(id);
signed.extend_from_slice(accessory_eph_pub);
let signature: [u8; 64] = self.signing.sign(&signed).to_bytes();
let mut sub = Vec::new();
let mut sw = Tlv8Writer::new(&mut sub);
sw.push(tlv::IDENTIFIER, id);
sw.push(tlv::SIGNATURE, &signature);
let nonce = hap_nonce(NONCE_M3);
let sealed = encrypt(pv_key, &nonce, b"", &sub)?;
let mut out = Vec::new();
let mut w = Tlv8Writer::new(&mut out);
w.push_u8(tlv::STATE, tlv::STATE_M3);
w.push(tlv::ENCRYPTED_DATA, &sealed);
Ok(out)
}
pub fn broadcast_key(&self, controller_ltpk: &[u8]) -> Result<crate::BroadcastKey> {
let shared = self
.shared_secret
.ok_or(CryptoError::Encoding("Pair Verify shared secret missing"))?;
crate::BroadcastKey::derive(&shared, controller_ltpk)
}
fn handle_m4(&mut self, response: &[u8]) -> Result<PairVerifyStep> {
let map = Tlv8Map::parse(response)?;
check_error(&map)?;
expect_state(&map, tlv::STATE_M4)?;
let shared = self
.shared_secret
.ok_or(CryptoError::Encoding("Pair Verify shared secret missing"))?;
let read_key = derive_key(&shared, CONTROL_SALT, CONTROL_READ_INFO)?;
let write_key = derive_key(&shared, CONTROL_SALT, CONTROL_WRITE_INFO)?;
self.state = State::Done;
Ok(PairVerifyStep::Done(SessionKeys {
read_key,
write_key,
}))
}
}
fn derive_key(shared: &[u8; 32], salt: &[u8], info: &[u8]) -> Result<[u8; 32]> {
let mut out = [0u8; 32];
hkdf_sha512(shared, salt, info, &mut out)?;
Ok(out)
}
fn check_error(map: &Tlv8Map) -> Result<()> {
match map.get(tlv::ERROR) {
None | Some([]) => Ok(()),
Some(_) => Err(CryptoError::Encoding(
"accessory returned a Pair Verify error",
)),
}
}
fn expect_state(map: &Tlv8Map, expected: u8) -> Result<()> {
match map.get_u8(tlv::STATE)? {
None => Ok(()),
Some(s) if s == expected => Ok(()),
Some(_) => Err(CryptoError::Encoding("unexpected Pair Verify state")),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
fn vec_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.join("test-vectors/pair-verify")
}
fn load(name: &str) -> Option<Vec<u8>> {
fs::read(vec_dir().join(name)).ok()
}
fn load32(name: &str) -> Option<[u8; 32]> {
load(name).and_then(|v| v.try_into().ok())
}
fn test_controller() -> ControllerKeypair {
ControllerKeypair::from_seed("ABCDEF01-2345-6789".to_string(), [7u8; 32])
}
fn accessory_from_fixtures() -> Option<AccessoryPairing> {
let id = String::from_utf8(load("accessory_id.txt")?)
.ok()?
.trim()
.to_string();
let ltpk = load32("accessory_ltpk.bin")?;
Some(AccessoryPairing {
pairing_id: id,
ltpk,
})
}
#[test]
fn m1_reproduces_captured() {
let (Some(accessory), Some(eph_priv), Some(m1)) = (
accessory_from_fixtures(),
load32("ios_eph_priv.bin"),
load("m1.bin"),
) else {
eprintln!("skipping m1_reproduces_captured: fixtures absent");
return;
};
let mut client =
PairVerifyClient::new_with_ephemeral(&test_controller(), &accessory, eph_priv);
assert_eq!(client.start(), m1);
}
#[test]
fn x25519_matches_captured_shared_secret() {
let (Some(eph_priv), Some(m2), Some(shared)) = (
load32("ios_eph_priv.bin"),
load("m2.bin"),
load32("shared_secret.bin"),
) else {
eprintln!("skipping x25519_matches_captured_shared_secret: fixtures absent");
return;
};
let map = Tlv8Map::parse(&m2).unwrap();
let accessory_eph: [u8; 32] = map.get(tlv::PUBLIC_KEY).unwrap().try_into().unwrap();
let kp = EphemeralKeypair::from_secret(eph_priv);
assert_eq!(kp.diffie_hellman(&accessory_eph), shared);
assert_eq!(
crate::x25519::x25519_shared(&eph_priv, &accessory_eph),
shared
);
}
#[test]
fn session_keys_match_captured() {
let (Some(shared), Some(read), Some(write)) = (
load32("shared_secret.bin"),
load32("control_read_encryption_key.bin"),
load32("control_write_encryption_key.bin"),
) else {
eprintln!("skipping session_keys_match_captured: fixtures absent");
return;
};
assert_eq!(
derive_key(&shared, CONTROL_SALT, CONTROL_READ_INFO).unwrap(),
read
);
assert_eq!(
derive_key(&shared, CONTROL_SALT, CONTROL_WRITE_INFO).unwrap(),
write
);
}
#[test]
fn full_replay_reaches_done_with_matching_keys() {
let (Some(accessory), Some(eph_priv), Some(m2), Some(m4), Some(read), Some(write)) = (
accessory_from_fixtures(),
load32("ios_eph_priv.bin"),
load("m2.bin"),
load("m4.bin"),
load32("control_read_encryption_key.bin"),
load32("control_write_encryption_key.bin"),
) else {
eprintln!("skipping full_replay_reaches_done_with_matching_keys: fixtures absent");
return;
};
let mut client =
PairVerifyClient::new_with_ephemeral(&test_controller(), &accessory, eph_priv);
let _m1 = client.start();
let step = client.handle(&m2).unwrap();
let PairVerifyStep::Send(m3) = step else {
panic!("expected Send(m3) after M2, got {step:?}");
};
let m3map = Tlv8Map::parse(&m3).unwrap();
assert_eq!(m3map.get_u8(tlv::STATE).unwrap(), Some(tlv::STATE_M3));
assert!(m3map.get(tlv::ENCRYPTED_DATA).is_some());
let done = client.handle(&m4).unwrap();
let PairVerifyStep::Done(keys) = done else {
panic!("expected Done(SessionKeys) after M4, got {done:?}");
};
assert_eq!(keys.read_key, read);
assert_eq!(keys.write_key, write);
}
#[test]
fn corrupt_m2_encrypted_data_errors() {
let (Some(accessory), Some(eph_priv), Some(m2)) = (
accessory_from_fixtures(),
load32("ios_eph_priv.bin"),
load("m2.bin"),
) else {
eprintln!("skipping corrupt_m2_encrypted_data_errors: fixtures absent");
return;
};
let map = Tlv8Map::parse(&m2).unwrap();
let accessory_eph = map.get(tlv::PUBLIC_KEY).unwrap().to_vec();
let mut enc = map.get(tlv::ENCRYPTED_DATA).unwrap().to_vec();
enc[0] ^= 0x01;
let mut tampered = Vec::new();
let mut w = Tlv8Writer::new(&mut tampered);
w.push_u8(tlv::STATE, tlv::STATE_M2);
w.push(tlv::PUBLIC_KEY, &accessory_eph);
w.push(tlv::ENCRYPTED_DATA, &enc);
let mut client =
PairVerifyClient::new_with_ephemeral(&test_controller(), &accessory, eph_priv);
let _m1 = client.start();
let err = client.handle(&tampered);
assert!(
matches!(err, Err(CryptoError::Aead | CryptoError::Signature)),
"expected Aead/Signature error, got {err:?}"
);
}
#[test]
fn handle_before_start_errors() {
let accessory = AccessoryPairing {
pairing_id: "AA:BB:CC:DD:EE:FF".to_string(),
ltpk: [0u8; 32],
};
let mut client = PairVerifyClient::new(&test_controller(), &accessory);
assert!(client.handle(b"\x06\x01\x02").is_err());
}
#[test]
fn accessory_error_in_m4_errors() {
let (Some(accessory), Some(eph_priv), Some(m2)) = (
accessory_from_fixtures(),
load32("ios_eph_priv.bin"),
load("m2.bin"),
) else {
eprintln!("skipping accessory_error_in_m4_errors: fixtures absent");
return;
};
let mut client =
PairVerifyClient::new_with_ephemeral(&test_controller(), &accessory, eph_priv);
let _m1 = client.start();
client.handle(&m2).unwrap();
let mut m4err = Vec::new();
let mut w = Tlv8Writer::new(&mut m4err);
w.push_u8(tlv::STATE, tlv::STATE_M4);
w.push_u8(tlv::ERROR, 2);
assert!(client.handle(&m4err).is_err());
}
#[test]
fn broadcast_key_matches_direct_derive_after_done() {
let (Some(accessory), Some(eph_priv), Some(m2), Some(m4)) = (
accessory_from_fixtures(),
load32("ios_eph_priv.bin"),
load("m2.bin"),
load("m4.bin"),
) else {
eprintln!("skipping broadcast_key_matches_direct_derive_after_done: fixtures absent");
return;
};
let controller = test_controller();
let mut client = PairVerifyClient::new_with_ephemeral(&controller, &accessory, eph_priv);
let _m1 = client.start();
client.handle(&m2).unwrap();
client.handle(&m4).unwrap();
let fake_ltpk = [0xABu8; 32];
let bk = client.broadcast_key(&fake_ltpk).unwrap();
let shared = {
let map = hap_tlv8::Tlv8Map::parse(&m2).unwrap();
let accessory_eph: [u8; 32] = map
.get(crate::tlv_types::PUBLIC_KEY)
.unwrap()
.try_into()
.unwrap();
crate::x25519::EphemeralKeypair::from_secret(eph_priv).diffie_hellman(&accessory_eph)
};
let direct = crate::BroadcastKey::derive(&shared, &fake_ltpk).unwrap();
assert_eq!(bk.as_bytes(), direct.as_bytes());
}
#[test]
fn broadcast_key_before_m2_errors() {
let accessory = AccessoryPairing {
pairing_id: "AA:BB:CC:DD:EE:FF".to_string(),
ltpk: [0u8; 32],
};
let mut client = PairVerifyClient::new(&test_controller(), &accessory);
let _m1 = client.start();
assert!(client.broadcast_key(&[0u8; 32]).is_err());
}
}