hap_crypto/x25519.rs
1//! X25519 (Curve25519) ephemeral Diffie-Hellman for Pair Verify (M3).
2//!
3//! HomeKit Pair Verify opens each session with a fresh ephemeral X25519 key
4//! exchange: the controller sends an ephemeral public key in M1, the accessory
5//! replies with its own in M2, and both sides derive the same 32-byte shared
6//! secret. That shared secret is the input keying material for the HKDF-SHA512
7//! derivation of the Pair-Verify encryption key and the directional session
8//! keys (handled elsewhere in M3).
9//!
10//! The curve arithmetic is never reimplemented here; it comes from the vetted
11//! [`x25519_dalek`] crate. This module only adapts the primitive to the
12//! fixed-size byte shapes the protocol code needs and pins it to the published
13//! RFC 7748 known-answer vector.
14//!
15//! X25519 is infallible for the fixed 32-byte inputs used here, so nothing in
16//! this module returns a `Result`.
17
18use x25519_dalek::{PublicKey, StaticSecret};
19
20/// An ephemeral X25519 keypair for a single Pair Verify exchange.
21///
22/// "Ephemeral" here means *per session*: a new keypair is generated with
23/// [`EphemeralKeypair::generate`] for each Pair Verify run and discarded once
24/// the session keys are derived. [`EphemeralKeypair::from_secret`] reconstructs
25/// a keypair from a fixed scalar for deterministic tests and trace replay.
26///
27/// The underlying secret is held as a [`StaticSecret`] (which zeroizes on drop)
28/// rather than exposed as raw bytes; only the public key and the computed shared
29/// secret leave this type.
30pub struct EphemeralKeypair {
31 secret: StaticSecret,
32 public: [u8; 32],
33}
34
35impl EphemeralKeypair {
36 /// Generate a fresh random ephemeral keypair using the operating system
37 /// CSPRNG.
38 ///
39 /// This is the constructor production code uses; every Pair Verify session
40 /// gets its own keypair.
41 #[must_use]
42 pub fn generate() -> Self {
43 let secret = StaticSecret::random();
44 let public = PublicKey::from(&secret).to_bytes();
45 Self { secret, public }
46 }
47
48 /// Reconstruct a keypair from a fixed 32-byte secret scalar.
49 ///
50 /// The scalar is clamped internally by X25519, so any 32-byte value is
51 /// accepted and the resulting public key matches the X25519 definition.
52 /// This is the deterministic path used to replay captured traces and to
53 /// assert the RFC 7748 known-answer vector; production code calls
54 /// [`EphemeralKeypair::generate`] instead.
55 #[must_use]
56 pub fn from_secret(scalar: [u8; 32]) -> Self {
57 let secret = StaticSecret::from(scalar);
58 let public = PublicKey::from(&secret).to_bytes();
59 Self { secret, public }
60 }
61
62 /// This keypair's X25519 public key — the 32 bytes sent on the wire.
63 #[must_use]
64 pub fn public(&self) -> [u8; 32] {
65 self.public
66 }
67
68 /// Compute the X25519 shared secret against the peer's 32-byte public key.
69 ///
70 /// Both sides of a Pair Verify exchange derive the same value:
71 /// `DH(self_secret, peer_public) == DH(peer_secret, self_public)`.
72 #[must_use]
73 pub fn diffie_hellman(&self, peer_public: &[u8; 32]) -> [u8; 32] {
74 let peer = PublicKey::from(*peer_public);
75 self.secret.diffie_hellman(&peer).to_bytes()
76 }
77}
78
79/// Compute an X25519 shared secret from a raw 32-byte secret scalar and a peer's
80/// 32-byte public key.
81///
82/// A free-function convenience equivalent to
83/// `EphemeralKeypair::from_secret(secret).diffie_hellman(peer_public)`, useful
84/// where only the shared secret is needed (e.g. trace replay) without holding a
85/// keypair.
86#[must_use]
87pub fn x25519_shared(secret: &[u8; 32], peer_public: &[u8; 32]) -> [u8; 32] {
88 let secret = StaticSecret::from(*secret);
89 let peer = PublicKey::from(*peer_public);
90 secret.diffie_hellman(&peer).to_bytes()
91}
92
93#[cfg(test)]
94// Test code only: CLAUDE.md carves out `unwrap`/`expect` for tests with a
95// documented justification. A failed `unwrap` here is itself a test failure.
96#[allow(clippy::unwrap_used, clippy::expect_used)]
97mod tests {
98 use super::*;
99
100 fn h(s: &str) -> [u8; 32] {
101 hex::decode(s).unwrap().try_into().unwrap()
102 }
103
104 // RFC 7748 §6.1 "Curve25519" known-answer vector. The published hex literals
105 // for Alice's and Bob's private/public keys and the resulting shared secret
106 // K. See <https://www.rfc-editor.org/rfc/rfc7748#section-6.1>.
107 const ALICE_PRIV: &str = "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a";
108 const ALICE_PUB: &str = "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a";
109 const BOB_PRIV: &str = "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb";
110 const BOB_PUB: &str = "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f";
111 const SHARED_K: &str = "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742";
112
113 #[test]
114 fn alice_public_matches_rfc7748() {
115 assert_eq!(
116 EphemeralKeypair::from_secret(h(ALICE_PRIV)).public(),
117 h(ALICE_PUB)
118 );
119 }
120
121 #[test]
122 fn bob_public_matches_rfc7748() {
123 assert_eq!(
124 EphemeralKeypair::from_secret(h(BOB_PRIV)).public(),
125 h(BOB_PUB)
126 );
127 }
128
129 #[test]
130 fn alice_dh_bob_pub_matches_rfc7748_k() {
131 let alice = EphemeralKeypair::from_secret(h(ALICE_PRIV));
132 assert_eq!(alice.diffie_hellman(&h(BOB_PUB)), h(SHARED_K));
133 }
134
135 #[test]
136 fn bob_dh_alice_pub_matches_rfc7748_k() {
137 let bob = EphemeralKeypair::from_secret(h(BOB_PRIV));
138 assert_eq!(bob.diffie_hellman(&h(ALICE_PUB)), h(SHARED_K));
139 }
140
141 #[test]
142 fn dh_is_symmetric() {
143 let alice = EphemeralKeypair::from_secret(h(ALICE_PRIV));
144 let bob = EphemeralKeypair::from_secret(h(BOB_PRIV));
145 assert_eq!(
146 alice.diffie_hellman(&bob.public()),
147 bob.diffie_hellman(&alice.public())
148 );
149 }
150
151 #[test]
152 fn free_function_matches_rfc7748_k() {
153 assert_eq!(x25519_shared(&h(ALICE_PRIV), &h(BOB_PUB)), h(SHARED_K));
154 assert_eq!(x25519_shared(&h(BOB_PRIV), &h(ALICE_PUB)), h(SHARED_K));
155 }
156
157 #[test]
158 fn generate_then_exchange_roundtrips() {
159 let alice = EphemeralKeypair::generate();
160 let bob = EphemeralKeypair::generate();
161 assert_eq!(alice.public().len(), 32);
162 assert_eq!(
163 alice.diffie_hellman(&bob.public()),
164 bob.diffie_hellman(&alice.public())
165 );
166 }
167}