bitcoin_ohttp/
config.rs

1use crate::{
2    err::{Error, Res},
3    hpke::{Aead as AeadId, Kdf, Kem},
4    KeyId,
5};
6use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt};
7use std::{
8    convert::TryFrom,
9    io::{BufRead, BufReader, Cursor, Read},
10};
11
12#[cfg(feature = "nss")]
13use crate::nss::{
14    hpke::{generate_key_pair, Config as HpkeConfig, HpkeR},
15    PrivateKey, PublicKey,
16};
17
18#[cfg(feature = "rust-hpke")]
19use crate::rh::hpke::{
20    derive_key_pair, generate_key_pair, Config as HpkeConfig, HpkeR, PrivateKey, PublicKey,
21};
22
23/// A tuple of KDF and AEAD identifiers.
24#[derive(Debug, Copy, Clone, PartialEq, Eq)]
25pub struct SymmetricSuite {
26    kdf: Kdf,
27    aead: AeadId,
28}
29
30impl SymmetricSuite {
31    #[must_use]
32    pub const fn new(kdf: Kdf, aead: AeadId) -> Self {
33        Self { kdf, aead }
34    }
35
36    #[must_use]
37    pub fn kdf(self) -> Kdf {
38        self.kdf
39    }
40
41    #[must_use]
42    pub fn aead(self) -> AeadId {
43        self.aead
44    }
45}
46
47/// The key configuration of a server.  This can be used by both client and server.
48/// An important invariant of this structure is that it does not include
49/// any combination of KEM, KDF, and AEAD that is not supported.
50#[allow(clippy::module_name_repetitions)]
51#[derive(Debug, Clone)]
52pub struct KeyConfig {
53    pub(crate) key_id: KeyId,
54    pub(crate) kem: Kem,
55    pub(crate) symmetric: Vec<SymmetricSuite>,
56    pub(crate) sk: Option<PrivateKey>,
57    pub(crate) pk: PublicKey,
58}
59
60impl KeyConfig {
61    fn strip_unsupported(symmetric: &mut Vec<SymmetricSuite>, kem: Kem) {
62        symmetric.retain(|s| HpkeConfig::new(kem, s.kdf(), s.aead()).supported());
63    }
64
65    /// Construct a configuration for the server side.
66    /// # Panics
67    /// If the configurations don't include a supported configuration.
68    pub fn new(key_id: u8, kem: Kem, mut symmetric: Vec<SymmetricSuite>) -> Res<Self> {
69        Self::strip_unsupported(&mut symmetric, kem);
70        assert!(!symmetric.is_empty());
71        let (sk, pk) = generate_key_pair(kem)?;
72        Ok(Self {
73            key_id,
74            kem,
75            symmetric,
76            sk: Some(sk),
77            pk,
78        })
79    }
80
81    /// Derive a configuration for the server side from input keying material,
82    /// using the `DeriveKeyPair` functionality of the HPKE KEM defined here:
83    /// <https://www.ietf.org/archive/id/draft-irtf-cfrg-hpke-12.html#section-4>
84    /// # Panics
85    /// If the configurations don't include a supported configuration.
86    #[allow(unused)]
87    pub fn derive(
88        key_id: u8,
89        kem: Kem,
90        mut symmetric: Vec<SymmetricSuite>,
91        ikm: &[u8],
92    ) -> Res<Self> {
93        #[cfg(feature = "rust-hpke")]
94        {
95            Self::strip_unsupported(&mut symmetric, kem);
96            assert!(!symmetric.is_empty());
97            let (sk, pk) = derive_key_pair(kem, ikm)?;
98            Ok(Self {
99                key_id,
100                kem,
101                symmetric,
102                sk: Some(sk),
103                pk,
104            })
105        }
106        #[cfg(not(feature = "rust-hpke"))]
107        {
108            Err(Error::Unsupported)
109        }
110    }
111
112    /// Encode a list of key configurations.
113    ///
114    /// This produces the key configuration format that is used for
115    /// the "application/ohttp-keys" media type.
116    /// Each item in the list is written as per [`encode()`].
117    ///
118    /// # Panics
119    /// Not as a result of this function.
120    ///
121    /// [`encode()`]: Self::encode
122    pub fn encode_list(list: &[impl AsRef<Self>]) -> Res<Vec<u8>> {
123        let mut buf = Vec::new();
124        for c in list {
125            let offset = buf.len();
126            buf.write_u16::<NetworkEndian>(0)?;
127            c.as_ref().write(&mut buf)?;
128            let len = buf.len() - offset - 2;
129            buf[offset] = u8::try_from(len >> 8)?;
130            buf[offset + 1] = u8::try_from(len & 0xff).unwrap();
131        }
132        Ok(buf)
133    }
134
135    fn write(&self, buf: &mut Vec<u8>) -> Res<()> {
136        buf.write_u8(self.key_id)?;
137        buf.write_u16::<NetworkEndian>(u16::from(self.kem))?;
138        let pk_buf = self.pk.key_data()?;
139        buf.extend_from_slice(&pk_buf);
140        buf.write_u16::<NetworkEndian>((self.symmetric.len() * 4).try_into()?)?;
141        for s in &self.symmetric {
142            buf.write_u16::<NetworkEndian>(u16::from(s.kdf()))?;
143            buf.write_u16::<NetworkEndian>(u16::from(s.aead()))?;
144        }
145        Ok(())
146    }
147
148    /// Encode into a wire format.  This shares a format with the core of ECH:
149    ///
150    /// ```tls-format
151    /// opaque HpkePublicKey[Npk];
152    /// uint16 HpkeKemId;  // Defined in I-D.irtf-cfrg-hpke
153    /// uint16 HpkeKdfId;  // Defined in I-D.irtf-cfrg-hpke
154    /// uint16 HpkeAeadId; // Defined in I-D.irtf-cfrg-hpke
155    ///
156    /// struct {
157    ///   HpkeKdfId kdf_id;
158    ///   HpkeAeadId aead_id;
159    /// } ECHCipherSuite;
160    ///
161    /// struct {
162    ///   uint8 key_id;
163    ///   HpkeKemId kem_id;
164    ///   HpkePublicKey public_key;
165    ///   ECHCipherSuite cipher_suites<4..2^16-4>;
166    /// } ECHKeyConfig;
167    /// ```
168    /// # Panics
169    /// Not as a result of this function.
170    pub fn encode(&self) -> Res<Vec<u8>> {
171        let mut buf = Vec::new();
172        self.write(&mut buf)?;
173        Ok(buf)
174    }
175
176    /// Construct a configuration from the encoded server configuration.
177    /// The format of `encoded_config` is the output of `Self::encode`.
178    pub fn decode(encoded_config: &[u8]) -> Res<Self> {
179        let end_position = u64::try_from(encoded_config.len())?;
180        let mut r = Cursor::new(encoded_config);
181        let key_id = r.read_u8()?;
182        let kem = Kem::try_from(r.read_u16::<NetworkEndian>()?)?;
183
184        // Note that the KDF and AEAD doesn't matter here.
185        let kem_config = HpkeConfig::new(kem, Kdf::HkdfSha256, AeadId::Aes128Gcm);
186        if !kem_config.supported() {
187            return Err(Error::Unsupported);
188        }
189        let mut pk_buf = vec![0; kem_config.kem().n_pk()];
190        r.read_exact(&mut pk_buf)?;
191
192        let sym_len = r.read_u16::<NetworkEndian>()?;
193        let mut sym = vec![0; usize::from(sym_len)];
194        r.read_exact(&mut sym)?;
195        if sym.is_empty() || (sym.len() % 4 != 0) {
196            return Err(Error::Format);
197        }
198        let sym_count = sym.len() / 4;
199        let mut sym_r = BufReader::new(&sym[..]);
200        let mut symmetric = Vec::with_capacity(sym_count);
201        for _ in 0..sym_count {
202            let kdf = Kdf::try_from(sym_r.read_u16::<NetworkEndian>()?)?;
203            let aead = AeadId::try_from(sym_r.read_u16::<NetworkEndian>()?)?;
204            symmetric.push(SymmetricSuite::new(kdf, aead));
205        }
206
207        // Check that there was nothing extra and we are at the end of the buffer.
208        if r.position() != end_position {
209            return Err(Error::Format);
210        }
211
212        Self::strip_unsupported(&mut symmetric, kem);
213        let pk = HpkeR::decode_public_key(kem_config.kem(), &pk_buf)?;
214
215        Ok(Self {
216            key_id,
217            kem,
218            symmetric,
219            sk: None,
220            pk,
221        })
222    }
223
224    /// Decode a list of key configurations.
225    /// This only returns the valid and supported key configurations;
226    /// unsupported configurations are dropped silently.
227    pub fn decode_list(encoded_list: &[u8]) -> Res<Vec<Self>> {
228        let end_position = u64::try_from(encoded_list.len())?;
229        let mut r = Cursor::new(encoded_list);
230        let mut configs = Vec::new();
231        loop {
232            if r.position() == end_position {
233                break;
234            }
235            let len = usize::from(r.read_u16::<NetworkEndian>()?);
236            let buf = r.fill_buf()?;
237            if len > buf.len() {
238                return Err(Error::Truncated);
239            }
240            let res = Self::decode(&buf[..len]);
241            r.consume(len);
242            match res {
243                Ok(config) => configs.push(config),
244                Err(Error::Unsupported) => continue,
245                Err(e) => return Err(e),
246            }
247        }
248        Ok(configs)
249    }
250
251    /// Select creates a new configuration that contains the identified symmetric suite.
252    ///
253    /// # Errors
254    /// If the given suite is not supported by this configuration.
255    pub fn select(&self, sym: SymmetricSuite) -> Res<HpkeConfig> {
256        if self.symmetric.contains(&sym) {
257            let config = HpkeConfig::new(self.kem, sym.kdf(), sym.aead());
258            Ok(config)
259        } else {
260            Err(Error::Unsupported)
261        }
262    }
263}
264
265impl AsRef<Self> for KeyConfig {
266    fn as_ref(&self) -> &Self {
267        self
268    }
269}
270
271#[cfg(test)]
272mod test {
273    use crate::{
274        hpke::{Aead, Kdf, Kem},
275        init, Error, KeyConfig, KeyId, SymmetricSuite,
276    };
277    use std::iter::zip;
278
279    const KEY_ID: KeyId = 1;
280    const KEM: Kem = Kem::K256Sha256;
281    const SYMMETRIC: &[SymmetricSuite] = &[
282        SymmetricSuite::new(Kdf::HkdfSha256, Aead::Aes128Gcm),
283        SymmetricSuite::new(Kdf::HkdfSha256, Aead::ChaCha20Poly1305),
284    ];
285
286    #[test]
287    fn encode_decode_config_list() {
288        const COUNT: usize = 3;
289        init();
290
291        let mut configs = Vec::with_capacity(COUNT);
292        configs.resize_with(COUNT, || {
293            KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap()
294        });
295
296        let buf = KeyConfig::encode_list(&configs).unwrap();
297        let decoded_list = KeyConfig::decode_list(&buf).unwrap();
298        for (original, decoded) in zip(&configs, &decoded_list) {
299            assert_eq!(decoded.key_id, original.key_id);
300            assert_eq!(decoded.kem, original.kem);
301            assert_eq!(
302                decoded.pk.key_data().unwrap(),
303                original.pk.key_data().unwrap()
304            );
305            assert!(decoded.sk.is_none());
306            assert!(original.sk.is_some());
307        }
308
309        // Check that truncation errors in `KeyConfig::decode` are caught.
310        assert!(KeyConfig::decode_list(&buf[..buf.len() - 3]).is_err());
311    }
312
313    #[test]
314    fn empty_config_list() {
315        let list = KeyConfig::decode_list(&[]).unwrap();
316        assert!(list.is_empty());
317
318        // A reserved KEM ID is not bad.  Note that we don't check that the data
319        // following the KEM ID is even the minimum length, allowing this to be
320        // zero bytes, where you need at least some bytes in a public key and some
321        // bytes to identify at least one KDF and AEAD (i.e., more than 6 bytes).
322        let list = KeyConfig::decode_list(&[0, 3, 0, 0, 0]).unwrap();
323        assert!(list.is_empty());
324    }
325
326    #[test]
327    fn bad_config_list_length() {
328        init();
329
330        // A one byte length for a config.
331        let res = KeyConfig::decode_list(&[0]);
332        assert!(matches!(res, Err(Error::Io(_))));
333    }
334
335    #[test]
336    fn decode_bad_config() {
337        init();
338
339        let mut x25519 = KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC))
340            .unwrap()
341            .encode()
342            .unwrap();
343        {
344            // Truncation tests.
345            let trunc = |n: usize| KeyConfig::decode(&x25519[..n]);
346
347            // x25519, truncated inside the KEM ID.
348            assert!(matches!(trunc(2), Err(Error::Io(_))));
349            // ... inside the public key.
350            assert!(matches!(trunc(4), Err(Error::Io(_))));
351            // ... inside the length of the KDF+AEAD list.
352            assert!(matches!(trunc(36), Err(Error::Io(_))));
353            // ... inside the KDF+AEAD list.
354            assert!(matches!(trunc(38), Err(Error::Io(_))));
355        }
356
357        // And then with an extra byte at the end.
358        x25519.push(0);
359        assert!(matches!(KeyConfig::decode(&x25519), Err(Error::Format)));
360    }
361
362    /// Truncate the KDF+AEAD list badly.
363    #[test]
364    fn truncate_kdf_aead_list() {
365        init();
366
367        let mut x25519 = KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC))
368            .unwrap()
369            .encode()
370            .unwrap();
371        x25519.truncate(38);
372        assert_eq!(usize::from(x25519[36]), SYMMETRIC.len() * 4);
373        x25519[36] = 1;
374        assert!(matches!(KeyConfig::decode(&x25519), Err(Error::Format)));
375    }
376}