rustls/crypto/aws_lc_rs/pq/
hybrid.rs

1use alloc::boxed::Box;
2use alloc::vec::Vec;
3
4use super::INVALID_KEY_SHARE;
5use crate::crypto::{ActiveKeyExchange, CompletedKeyExchange, SharedSecret, SupportedKxGroup};
6use crate::ffdhe_groups::FfdheGroup;
7use crate::{Error, NamedGroup, ProtocolVersion};
8
9/// A generalization of hybrid key exchange.
10#[derive(Debug)]
11pub(crate) struct Hybrid {
12    pub(crate) classical: &'static dyn SupportedKxGroup,
13    pub(crate) post_quantum: &'static dyn SupportedKxGroup,
14    pub(crate) name: NamedGroup,
15    pub(crate) layout: Layout,
16}
17
18impl SupportedKxGroup for Hybrid {
19    fn start(&self) -> Result<Box<dyn ActiveKeyExchange>, Error> {
20        let classical = self.classical.start()?;
21        let post_quantum = self.post_quantum.start()?;
22
23        let combined_pub_key = self
24            .layout
25            .concat(post_quantum.pub_key(), classical.pub_key());
26
27        Ok(Box::new(ActiveHybrid {
28            classical,
29            post_quantum,
30            name: self.name,
31            layout: self.layout,
32            combined_pub_key,
33        }))
34    }
35
36    fn start_and_complete(&self, client_share: &[u8]) -> Result<CompletedKeyExchange, Error> {
37        let (post_quantum_share, classical_share) = self
38            .layout
39            .split_received_client_share(client_share)
40            .ok_or(INVALID_KEY_SHARE)?;
41
42        let cl = self
43            .classical
44            .start_and_complete(classical_share)?;
45        let pq = self
46            .post_quantum
47            .start_and_complete(post_quantum_share)?;
48
49        let combined_pub_key = self
50            .layout
51            .concat(&pq.pub_key, &cl.pub_key);
52        let secret = self
53            .layout
54            .concat(pq.secret.secret_bytes(), cl.secret.secret_bytes());
55
56        Ok(CompletedKeyExchange {
57            group: self.name,
58            pub_key: combined_pub_key,
59            secret: SharedSecret::from(secret),
60        })
61    }
62
63    fn ffdhe_group(&self) -> Option<FfdheGroup<'static>> {
64        None
65    }
66
67    fn name(&self) -> NamedGroup {
68        self.name
69    }
70
71    fn fips(&self) -> bool {
72        // Behold! The Night Mare: SP800-56C rev 2:
73        //
74        // "In addition to the currently approved techniques for the generation of the
75        // shared secret Z as specified in SP 800-56A and SP 800-56B, this Recommendation
76        // permits the use of a "hybrid" shared secret of the form Z′ = Z || T, a
77        // concatenation consisting of a "standard" shared secret Z that was generated
78        // during the execution of a key-establishment scheme (as currently specified in
79        // [SP 800-56A] or [SP 800-56B])"
80        //
81        // NIST plan to adjust this and allow both orders: see
82        // <https://csrc.nist.gov/pubs/sp/800/227/ipd> (Jan 2025) lines 1070-1080.
83        //
84        // But, for now, we follow the SP800-56C logic: the element appearing first is the
85        // one that controls approval.
86        match self.layout.post_quantum_first {
87            true => self.post_quantum.fips(),
88            false => self.classical.fips(),
89        }
90    }
91
92    fn usable_for_version(&self, version: ProtocolVersion) -> bool {
93        version == ProtocolVersion::TLSv1_3
94    }
95}
96
97struct ActiveHybrid {
98    classical: Box<dyn ActiveKeyExchange>,
99    post_quantum: Box<dyn ActiveKeyExchange>,
100    name: NamedGroup,
101    layout: Layout,
102    combined_pub_key: Vec<u8>,
103}
104
105impl ActiveKeyExchange for ActiveHybrid {
106    fn complete(self: Box<Self>, peer_pub_key: &[u8]) -> Result<SharedSecret, Error> {
107        let (post_quantum_share, classical_share) = self
108            .layout
109            .split_received_server_share(peer_pub_key)
110            .ok_or(INVALID_KEY_SHARE)?;
111
112        let cl = self
113            .classical
114            .complete(classical_share)?;
115        let pq = self
116            .post_quantum
117            .complete(post_quantum_share)?;
118
119        let secret = self
120            .layout
121            .concat(pq.secret_bytes(), cl.secret_bytes());
122        Ok(SharedSecret::from(secret))
123    }
124
125    /// Allow the classical computation to be offered and selected separately.
126    fn hybrid_component(&self) -> Option<(NamedGroup, &[u8])> {
127        Some((self.classical.group(), self.classical.pub_key()))
128    }
129
130    fn complete_hybrid_component(
131        self: Box<Self>,
132        peer_pub_key: &[u8],
133    ) -> Result<SharedSecret, Error> {
134        self.classical.complete(peer_pub_key)
135    }
136
137    fn pub_key(&self) -> &[u8] {
138        &self.combined_pub_key
139    }
140
141    fn ffdhe_group(&self) -> Option<FfdheGroup<'static>> {
142        None
143    }
144
145    fn group(&self) -> NamedGroup {
146        self.name
147    }
148}
149
150#[derive(Clone, Copy, Debug)]
151pub(crate) struct Layout {
152    /// Length of classical key share.
153    pub(crate) classical_share_len: usize,
154
155    /// Length of post-quantum key share sent by client
156    pub(crate) post_quantum_client_share_len: usize,
157
158    /// Length of post-quantum key share sent by server
159    pub(crate) post_quantum_server_share_len: usize,
160
161    /// Whether the post-quantum element comes first in shares and secrets.
162    ///
163    /// For dismal and unprincipled reasons, SECP256R1MLKEM768 has the
164    /// classical element first, while X25519MLKEM768 has it second.
165    pub(crate) post_quantum_first: bool,
166}
167
168impl Layout {
169    fn split_received_client_share<'a>(&self, share: &'a [u8]) -> Option<(&'a [u8], &'a [u8])> {
170        self.split(share, self.post_quantum_client_share_len)
171    }
172
173    fn split_received_server_share<'a>(&self, share: &'a [u8]) -> Option<(&'a [u8], &'a [u8])> {
174        self.split(share, self.post_quantum_server_share_len)
175    }
176
177    /// Return the PQ and classical component of a key share.
178    fn split<'a>(
179        &self,
180        share: &'a [u8],
181        post_quantum_share_len: usize,
182    ) -> Option<(&'a [u8], &'a [u8])> {
183        if share.len() != self.classical_share_len + post_quantum_share_len {
184            return None;
185        }
186
187        Some(match self.post_quantum_first {
188            true => {
189                let (first_share, second_share) = share.split_at(post_quantum_share_len);
190                (first_share, second_share)
191            }
192            false => {
193                let (first_share, second_share) = share.split_at(self.classical_share_len);
194                (second_share, first_share)
195            }
196        })
197    }
198
199    fn concat(&self, post_quantum: &[u8], classical: &[u8]) -> Vec<u8> {
200        match self.post_quantum_first {
201            true => [post_quantum, classical].concat(),
202            false => [classical, post_quantum].concat(),
203        }
204    }
205}