Skip to main content

opaque_vx/key_exchange/
shared.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (c) VexaHub and contributors.
3// Copyright (c) Meta Platforms, Inc. and affiliates.
4
5use core::ops::Add;
6
7use derive_where::derive_where;
8use digest::block_api::{CoreProxy, SmallBlockSizeUser};
9use digest::{Digest, Mac, Output, OutputSizeUser, Update};
10use generic_array::typenum::{IsLess, Le, NonZero, Sum, U1, U2, U32, U256, Unsigned};
11use generic_array::{ArrayLength, GenericArray};
12use hkdf::SimpleHkdf as Hkdf;
13use hkdf::SimpleHkdfExtract as HkdfExtract;
14use hmac::{KeyInit, SimpleHmac};
15use rand::{CryptoRng, Rng};
16
17use super::{
18    Deserialize, GenerateKe1Result, KeyExchange, Serialize, SerializedContext,
19    SerializedCredentialRequest, SerializedCredentialResponse, SerializedIdentifiers,
20};
21use crate::ciphersuite::{CipherSuite, KeGroup, KeHash};
22use crate::errors::{InternalError, ProtocolError};
23use crate::hash::{Hash, OutputSize, ProxyHash};
24use crate::key_exchange::group::Group;
25use crate::keypair::{KeyPair, PrivateKey, PublicKey};
26use crate::serialization::{ConcatExt, SliceExt, UpdateExt, i2osp};
27
28///////////////
29// Constants //
30// ========= //
31///////////////
32
33pub(crate) type NonceLen = U32;
34pub(super) static STR_CONTEXT: &[u8] = b"OPAQUEv1-";
35static STR_CLIENT_MAC: &[u8] = b"ClientMAC";
36static STR_HANDSHAKE_SECRET: &[u8] = b"HandshakeSecret";
37static STR_SERVER_MAC: &[u8] = b"ServerMAC";
38static STR_SESSION_KEY: &[u8] = b"SessionKey";
39static STR_OPAQUE: &[u8] = b"OPAQUE-";
40
41////////////////////////////
42// High-level API Structs //
43// ====================== //
44////////////////////////////
45
46/// Trait required by [`Group::Sk`] to be compatible with
47/// [`TripleDh`](crate::TripleDh) and [`SigmaI`](crate::SigmaI).
48pub trait DiffieHellman<G: Group> {
49    /// Diffie-Hellman key exchange.
50    fn diffie_hellman(&self, pk: &G::Pk) -> GenericArray<u8, G::PkLen>;
51}
52
53/// The client state produced after the first key exchange message
54#[cfg_attr(
55    feature = "serde",
56    derive(serde::Deserialize, serde::Serialize),
57    serde(bound(
58        deserialize = "G::Sk: serde::Deserialize<'de>",
59        serialize = "G::Sk: serde::Serialize"
60    ))
61)]
62#[derive_where(Clone, ZeroizeOnDrop)]
63#[derive_where(Debug, Eq, Hash, Ord, PartialEq, PartialOrd; G::Sk)]
64pub struct Ke1State<G: Group> {
65    pub(super) client_e_sk: PrivateKey<G>,
66    pub(super) client_nonce: GenericArray<u8, NonceLen>,
67}
68
69/// The first key exchange message
70#[cfg_attr(
71    feature = "serde",
72    derive(serde::Deserialize, serde::Serialize),
73    serde(bound(
74        deserialize = "G::Pk: serde::Deserialize<'de>",
75        serialize = "G::Pk: serde::Serialize"
76    ))
77)]
78#[derive_where(Clone, ZeroizeOnDrop)]
79#[derive_where(Debug, Eq, Hash, Ord, PartialEq, PartialOrd; G::Pk)]
80pub struct Ke1Message<G: Group> {
81    pub(super) client_nonce: GenericArray<u8, NonceLen>,
82    #[derive_where(skip(Zeroize))]
83    pub(super) client_e_pk: PublicKey<G>,
84}
85
86/////////////////////////
87// Convenience Structs //
88//==================== //
89/////////////////////////
90
91// Consists of a session key, followed by two mac keys: (session_key, km2, km3)
92pub(super) struct DerivedKeys<H: OutputSizeUser> {
93    pub(super) session_key: Output<H>,
94    pub(super) km2: Output<H>,
95    pub(super) km3: Output<H>,
96    #[cfg(test)]
97    pub(super) handshake_secret: Output<H>,
98}
99
100/// Helper bundle containing the common `TripleDH` server state that both
101/// `TripleDh` and `TripleDhKem` builders need.
102pub(super) struct Ke2BuilderCommon<G: Group, H: Hash>
103where
104    H::Core: ProxyHash,
105    <<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize: IsLess<U256>,
106    Le<<<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize, U256>: NonZero,
107    OutputSize<H>: ArrayLength,
108    G::Sk: DiffieHellman<G>,
109{
110    pub(super) server_nonce: GenericArray<u8, NonceLen>,
111    pub(super) transcript_hasher: H,
112    pub(super) client_e_pk: PublicKey<G>,
113    pub(super) server_e_pk: PublicKey<G>,
114    pub(super) shared_secret_1: GenericArray<u8, G::PkLen>,
115    pub(super) shared_secret_3: GenericArray<u8, G::PkLen>,
116}
117
118////////////////////////////////////////////////
119// Helper functions and Trait Implementations //
120// ========================================== //
121////////////////////////////////////////////////
122
123// Helper functions
124
125pub(super) fn generate_ke1<
126    R: Rng + CryptoRng,
127    KE: KeyExchange<KE1State = Ke1State<G>, KE1Message = Ke1Message<G>>,
128    G: Group,
129>(
130    rng: &mut R,
131) -> Result<GenerateKe1Result<KE>, ProtocolError> {
132    let client_e_kp = KeyPair::<G>::derive_random(rng);
133    let client_nonce = generate_nonce::<R>(rng);
134
135    let ke1_message = Ke1Message {
136        client_nonce,
137        client_e_pk: client_e_kp.public().clone(),
138    };
139
140    Ok(GenerateKe1Result {
141        state: Ke1State {
142            client_e_sk: client_e_kp.private().clone(),
143            client_nonce,
144        },
145        message: ke1_message,
146    })
147}
148
149// Generate a random nonce up to NonceLen::USIZE bytes.
150pub(super) fn generate_nonce<R: Rng + CryptoRng>(rng: &mut R) -> GenericArray<u8, NonceLen> {
151    let mut nonce_bytes = GenericArray::default();
152    rng.fill_bytes(&mut nonce_bytes);
153    nonce_bytes
154}
155
156pub(super) fn transcript<CS: CipherSuite, KE: Group>(
157    context: &SerializedContext<'_>,
158    identifiers: &SerializedIdentifiers<'_, KeGroup<CS>>,
159    credential_request: &SerializedCredentialRequest<CS>,
160    ke1_message: &Ke1MessageIter<KE>,
161    credential_response: &SerializedCredentialResponse<CS>,
162    server_nonce: GenericArray<u8, NonceLen>,
163    server_e_pk: &GenericArray<u8, KE::PkLen>,
164) -> KeHash<CS> {
165    KeHash::<CS>::new()
166        .chain_iter(context.iter())
167        .chain_iter(identifiers.client.iter())
168        .chain_iter(credential_request.iter())
169        .chain_iter(ke1_message.iter())
170        .chain_iter(identifiers.server.iter())
171        .chain_iter(credential_response.iter())
172        .chain(server_nonce)
173        .chain(server_e_pk)
174}
175
176/// Generates the server-side `TripleDH` transcript state shared by multiple
177/// key-exchange variants.
178pub(super) fn ke2_builder_common<'a, G, H, CS, R>(
179    rng: &mut R,
180    credential_request: SerializedCredentialRequest<CS>,
181    ke1_message: Ke1Message<G>,
182    credential_response: SerializedCredentialResponse<CS>,
183    client_s_pk: PublicKey<G>,
184    identifiers: SerializedIdentifiers<'a, KeGroup<CS>>,
185    context: SerializedContext<'a>,
186) -> Result<Ke2BuilderCommon<G, H>, ProtocolError>
187where
188    G: Group,
189    H: Hash,
190    R: Rng + CryptoRng,
191    CS: CipherSuite,
192    H::Core: ProxyHash,
193    <<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize: IsLess<U256>,
194    Le<<<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize, U256>: NonZero,
195    OutputSize<H>: ArrayLength,
196    G::Sk: DiffieHellman<G>,
197    CS::KeyExchange: KeyExchange<Group = G, Hash = H>,
198{
199    let server_ephemeral = KeyPair::<G>::derive_random(rng);
200    let server_nonce = generate_nonce::<R>(rng);
201    let server_e_pk_bytes = server_ephemeral.public().serialize();
202
203    let ke1_iter = ke1_message.to_iter();
204    let client_e_pk = ke1_message.client_e_pk.clone();
205
206    let transcript_hasher = transcript(
207        &context,
208        &identifiers,
209        &credential_request,
210        &ke1_iter,
211        &credential_response,
212        server_nonce,
213        &server_e_pk_bytes,
214    );
215
216    let shared_secret_1 = server_ephemeral
217        .private()
218        .ke_diffie_hellman(&ke1_message.client_e_pk);
219    let shared_secret_3 = server_ephemeral.private().ke_diffie_hellman(&client_s_pk);
220
221    Ok(Ke2BuilderCommon {
222        server_nonce,
223        transcript_hasher,
224        client_e_pk,
225        server_e_pk: server_ephemeral.public().clone(),
226        shared_secret_1,
227        shared_secret_3,
228    })
229}
230
231// Internal function which takes computed shared secrets, along with some
232// auxiliary metadata, to produce the session key and two MAC keys
233pub(super) fn derive_keys<'a, H: Hash>(
234    ikms: impl Iterator<Item = &'a [u8]>,
235    hashed_derivation_transcript: &[u8],
236) -> Result<DerivedKeys<H>, ProtocolError>
237where
238    H::Core: ProxyHash,
239    <<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize: IsLess<U256>,
240    Le<<<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize, U256>: NonZero,
241    OutputSize<H>: ArrayLength,
242{
243    let mut hkdf = HkdfExtract::<H>::new(None);
244
245    for ikm in ikms {
246        hkdf.input_ikm(ikm);
247    }
248
249    let (_, extracted_ikm) = hkdf.finalize();
250    let handshake_secret = derive_secrets::<H>(
251        &extracted_ikm,
252        STR_HANDSHAKE_SECRET,
253        hashed_derivation_transcript,
254    )?;
255    let session_key = derive_secrets::<H>(
256        &extracted_ikm,
257        STR_SESSION_KEY,
258        hashed_derivation_transcript,
259    )?;
260
261    let km2 = hkdf_expand_label::<H>(&handshake_secret, STR_SERVER_MAC, b"")?;
262    let km3 = hkdf_expand_label::<H>(&handshake_secret, STR_CLIENT_MAC, b"")?;
263
264    Ok(DerivedKeys {
265        session_key,
266        km2,
267        km3,
268        #[cfg(test)]
269        handshake_secret,
270    })
271}
272
273/// Helper function for shared functionality in KE2 MAC computation
274/// for both `TripleDH` and TripleDH-KEM
275pub(super) fn compute_ke2_macs<H: Hash>(
276    transcript_hasher: &mut H,
277    derived_keys: &DerivedKeys<H>,
278    transcript_digest: &[u8],
279) -> Result<(Output<H>, Output<H>), ProtocolError>
280where
281    H::Core: ProxyHash,
282    <<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize: IsLess<U256>,
283    Le<<<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize, U256>: NonZero,
284    OutputSize<H>: ArrayLength,
285{
286    let mut mac_hasher =
287        SimpleHmac::<H>::new_from_slice(&derived_keys.km2).map_err(|_| InternalError::HmacError)?;
288    Mac::update(&mut mac_hasher, transcript_digest);
289    let mac = mac_hasher.finalize().into_bytes();
290
291    Update::update(transcript_hasher, &mac);
292    let finalized_transcript = transcript_hasher.clone().finalize();
293
294    let mut expected_mac_hasher =
295        SimpleHmac::<H>::new_from_slice(&derived_keys.km3).map_err(|_| InternalError::HmacError)?;
296    Mac::update(&mut expected_mac_hasher, &finalized_transcript);
297    let expected_mac = expected_mac_hasher.finalize().into_bytes();
298
299    Ok((mac, expected_mac))
300}
301
302/// Finalizes the KE3 transcript by deriving session material from the provided
303/// shared secrets and verifying the server's MAC, returning both the derived
304/// keys and the client's MAC response. Callers are expected to supply any
305/// protocol-specific shared secrets (e.g. classic Diffie-Hellman results or
306/// KEM outputs) as byte slices.
307pub(super) fn finalize_ke3_transcript<'a, H: Hash>(
308    transcript_hasher: &mut H,
309    shared_secrets: impl Iterator<Item = &'a [u8]>,
310    server_mac: &Output<H>,
311) -> Result<(DerivedKeys<H>, Output<H>), ProtocolError>
312where
313    H::Core: ProxyHash,
314    <<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize: IsLess<U256>,
315    Le<<<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize, U256>: NonZero,
316    OutputSize<H>: ArrayLength,
317{
318    let transcript_digest = transcript_hasher.clone().finalize();
319    let derived_keys = derive_keys::<H>(shared_secrets, &transcript_digest)?;
320    let mut server_mac_hasher =
321        SimpleHmac::<H>::new_from_slice(&derived_keys.km2).map_err(|_| InternalError::HmacError)?;
322    Mac::update(&mut server_mac_hasher, &transcript_digest);
323    server_mac_hasher
324        .verify(server_mac)
325        .map_err(|_| ProtocolError::InvalidLoginError)?;
326
327    Update::update(transcript_hasher, server_mac);
328    let finalized_transcript = transcript_hasher.clone().finalize();
329
330    let mut client_mac_hasher =
331        SimpleHmac::<H>::new_from_slice(&derived_keys.km3).map_err(|_| InternalError::HmacError)?;
332    Mac::update(&mut client_mac_hasher, &finalized_transcript);
333
334    let client_mac = client_mac_hasher.finalize().into_bytes();
335
336    Ok((derived_keys, client_mac))
337}
338
339fn hkdf_expand_label<H: Hash>(
340    secret: &[u8],
341    label: &[u8],
342    context: &[u8],
343) -> Result<Output<H>, ProtocolError>
344where
345    H::Core: ProxyHash,
346    <<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize: IsLess<U256>,
347    Le<<<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize, U256>: NonZero,
348    OutputSize<H>: ArrayLength,
349{
350    let h = Hkdf::<H>::from_prk(secret).map_err(|_| InternalError::HkdfError)?;
351    hkdf_expand_label_extracted(&h, label, context)
352}
353
354fn hkdf_expand_label_extracted<H: Hash>(
355    hkdf: &Hkdf<H>,
356    label: &[u8],
357    context: &[u8],
358) -> Result<Output<H>, ProtocolError>
359where
360    H::Core: ProxyHash,
361    <<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize: IsLess<U256>,
362    Le<<<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize, U256>: NonZero,
363    OutputSize<H>: ArrayLength,
364{
365    let mut okm = GenericArray::default().into_ha0_4();
366
367    let length = i2osp::<U2>(OutputSize::<H>::USIZE)?;
368    let label_length = i2osp::<U1>(STR_OPAQUE.len() + label.len())?;
369    let context_len = i2osp::<U1>(context.len())?;
370
371    let hkdf_label = [
372        length.as_slice(),
373        &label_length,
374        STR_OPAQUE,
375        label,
376        &context_len,
377        context,
378    ];
379
380    hkdf.expand_multi_info(&hkdf_label, &mut okm)
381        .map_err(|_| InternalError::HkdfError)?;
382    Ok(okm)
383}
384
385fn derive_secrets<H: Hash>(
386    hkdf: &Hkdf<H>,
387    label: &[u8],
388    hashed_derivation_transcript: &[u8],
389) -> Result<Output<H>, ProtocolError>
390where
391    H::Core: ProxyHash,
392    <<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize: IsLess<U256>,
393    Le<<<H as CoreProxy>::Core as SmallBlockSizeUser>::_BlockSize, U256>: NonZero,
394    OutputSize<H>: ArrayLength,
395{
396    hkdf_expand_label_extracted::<H>(hkdf, label, hashed_derivation_transcript)
397}
398
399// Serialization and deserialization implementations
400
401impl<G: Group> Deserialize for Ke1State<G> {
402    fn deserialize_take(bytes: &mut &[u8]) -> Result<Self, ProtocolError> {
403        Ok(Self {
404            client_e_sk: PrivateKey::deserialize_take(bytes)?,
405            client_nonce: bytes.take_array("client nonce")?,
406        })
407    }
408}
409
410impl<G: Group> Serialize for Ke1State<G>
411where
412    // Ke1State: KeSk + Nonce
413    G::SkLen: Add<NonceLen>,
414    Sum<G::SkLen, NonceLen>: ArrayLength,
415{
416    type Len = Sum<G::SkLen, NonceLen>;
417
418    fn serialize(&self) -> GenericArray<u8, Self::Len> {
419        self.client_e_sk.serialize().cat(self.client_nonce)
420    }
421}
422
423impl<G: Group> Deserialize for Ke1Message<G> {
424    fn deserialize_take(input: &mut &[u8]) -> Result<Self, ProtocolError> {
425        Ok(Self {
426            client_nonce: input.take_array("client nonce")?,
427            client_e_pk: PublicKey::deserialize_take(input)?,
428        })
429    }
430}
431
432impl<G: Group> Serialize for Ke1Message<G>
433where
434    // Ke1Message: Nonce + KePk
435    NonceLen: Add<G::PkLen>,
436    Sum<NonceLen, G::PkLen>: ArrayLength,
437{
438    type Len = Sum<NonceLen, G::PkLen>;
439
440    fn serialize(&self) -> GenericArray<u8, Self::Len> {
441        self.client_nonce.cat(self.client_e_pk.serialize())
442    }
443}
444
445impl<G: Group> Ke1Message<G> {
446    pub(crate) fn to_iter(&self) -> Ke1MessageIter<G> {
447        Ke1MessageIter {
448            client_nonce: self.client_nonce,
449            client_e_pk: self.client_e_pk.serialize(),
450        }
451    }
452}
453
454#[cfg_attr(
455    feature = "serde",
456    derive(serde::Deserialize, serde::Serialize),
457    serde(bound = "")
458)]
459#[derive_where(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Zeroize)]
460pub(crate) struct Ke1MessageIter<G: Group> {
461    client_nonce: GenericArray<u8, NonceLen>,
462    client_e_pk: GenericArray<u8, G::PkLen>,
463}
464
465pub(crate) type Ke1MessageIterLen<G: Group> = Sum<NonceLen, G::PkLen>;
466
467impl<G: Group> Ke1MessageIter<G> {
468    pub(crate) fn iter(&self) -> impl Clone + Iterator<Item = &[u8]> {
469        [self.client_nonce.as_slice(), self.client_e_pk.as_slice()].into_iter()
470    }
471
472    pub(crate) fn deserialize_take(input: &mut &[u8]) -> Result<Self, ProtocolError> {
473        Ok(Ke1MessageIter {
474            client_nonce: input.take_array("client nonce")?,
475            client_e_pk: input.take_array("client ephemeral public key")?,
476        })
477    }
478}
479
480impl<G: Group> Ke1MessageIter<G>
481where
482    NonceLen: Add<G::PkLen>,
483    Ke1MessageIterLen<G>: ArrayLength,
484{
485    pub(crate) fn serialize(&self) -> GenericArray<u8, Ke1MessageIterLen<G>> {
486        self.client_nonce.cat(self.client_e_pk.clone())
487    }
488}