Skip to content

Commit 2f96158

Browse files
authored
dhkem: impl (Try)KeyInit/KeyExport for DecapsulationKey (#227)
Support for importing and exporting such keys is part of the HPKE spec, which it calls `SerializePrivateKey` and `DeserializePrivateKey`. This changes the inner types of `EcdhDecapsulationKey` and `X25519DecapsulationKey` to `elliptic_curve::SecretKey` and `x25519_dalek::StaticSecret` respectively, and impls the traits appropriate to the descriptions in RFC9810 §7.1.2: - `EcdhDecapsulationKey` impls `TryKeyInit` because it MUST reject non-canonical (unreduced) scalar representatives - `X25519DecapsulationKey` supports infallible `KeyInit` via clamping Both are able to implement `KeyExport` using their respective serialization formats. This required some changes to `elliptic-curve` since there were gaps in the `SecretKey`/`PublicKey` types, so a temporary git dependency has been added.
1 parent 38c5385 commit 2f96158

File tree

6 files changed

+152
-86
lines changed

6 files changed

+152
-86
lines changed

Cargo.lock

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ debug = true
1414
[patch.crates-io]
1515
ml-kem = { path = "./ml-kem" }
1616
module-lattice = { path = "./module-lattice" }
17+
18+
elliptic-curve = { git = "https://github.com/RustCrypto/traits" }

dhkem/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ k256 = ["dep:k256", "ecdh"]
4141
p256 = ["dep:p256", "ecdh"]
4242
p384 = ["dep:p384", "ecdh"]
4343
p521 = ["dep:p521", "ecdh"]
44-
x25519 = ["dep:x25519", "x25519/reusable_secrets"]
44+
x25519 = ["dep:x25519", "x25519/static_secrets"]
4545
zeroize = ["dep:zeroize"]
4646

4747
[package.metadata.docs.rs]

dhkem/src/ecdh_kem.rs

Lines changed: 86 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use crate::{DecapsulationKey, EncapsulationKey};
44
use core::marker::PhantomData;
55
use elliptic_curve::{
6-
AffinePoint, CurveArithmetic, Error, FieldBytesSize, PublicKey,
6+
AffinePoint, CurveArithmetic, Error, FieldBytes, FieldBytesSize, PublicKey, SecretKey,
77
ecdh::EphemeralSecret,
88
sec1::{
99
FromEncodedPoint, ModulusSize, ToEncodedPoint, UncompressedPoint, UncompressedPointSize,
@@ -15,16 +15,6 @@ use kem::{
1515
};
1616
use rand_core::{CryptoRng, TryCryptoRng};
1717

18-
/// Elliptic Curve Diffie-Hellman Decapsulation Key (i.e. secret decryption key)
19-
///
20-
/// Generic around an elliptic curve `C`.
21-
pub type EcdhDecapsulationKey<C> = DecapsulationKey<EphemeralSecret<C>, PublicKey<C>>;
22-
23-
/// Elliptic Curve Diffie-Hellman Encapsulation Key (i.e. public encryption key)
24-
///
25-
/// Generic around an elliptic curve `C`.
26-
pub type EcdhEncapsulationKey<C> = EncapsulationKey<PublicKey<C>>;
27-
2818
/// Generic Elliptic Curve Diffie-Hellman KEM adapter compatible with curves implemented using
2919
/// traits from the `elliptic-curve` crate.
3020
///
@@ -45,13 +35,94 @@ where
4535
type SharedKeySize = FieldBytesSize<C>;
4636
}
4737

38+
/// Elliptic Curve Diffie-Hellman Decapsulation Key (i.e. secret decryption key)
39+
///
40+
/// Generic around an elliptic curve `C`.
41+
pub type EcdhDecapsulationKey<C> = DecapsulationKey<SecretKey<C>, PublicKey<C>>;
42+
43+
impl<C> KeySizeUser for EcdhDecapsulationKey<C>
44+
where
45+
C: CurveArithmetic,
46+
{
47+
type KeySize = FieldBytesSize<C>;
48+
}
49+
50+
/// From [RFC9810 §7.1.2]: `SerializePrivateKey` and `DeserializePrivateKey`:
51+
///
52+
/// > DeserializePrivateKey() performs the Octet-String-to-Field-Element conversion
53+
/// > according to [SECG].
54+
///
55+
/// [RFC9810 §7.1.2]: https://datatracker.ietf.org/doc/html/rfc9180#section-7.1.2
56+
/// [SECG]: https://www.secg.org/sec1-v2.pdf
57+
impl<C> TryKeyInit for EcdhDecapsulationKey<C>
58+
where
59+
C: CurveArithmetic,
60+
{
61+
fn new(key: &FieldBytes<C>) -> Result<Self, InvalidKey> {
62+
SecretKey::from_bytes(key)
63+
.map(Into::into)
64+
.map_err(|_| InvalidKey)
65+
}
66+
}
67+
68+
/// From [RFC9810 §7.1.2]: `SerializePrivateKey` and `DeserializePrivateKey`:
69+
///
70+
/// > the SerializePrivateKey() function of the KEM performs the Field-Element-to-Octet-String
71+
/// > conversion according to [SECG]. If the private key is an integer outside the range
72+
/// > `[0, order-1]`, where order is the order of the curve being used, the private key MUST be
73+
/// > reduced to its representative in `[0, order-1]` before being serialized.
74+
///
75+
/// [RFC9810 §7.1.2]: https://datatracker.ietf.org/doc/html/rfc9180#section-7.1.2
76+
/// [SECG]: https://www.secg.org/sec1-v2.pdf
77+
impl<C> KeyExport for EcdhDecapsulationKey<C>
78+
where
79+
C: CurveArithmetic,
80+
{
81+
fn to_bytes(&self) -> FieldBytes<C> {
82+
self.dk.to_bytes()
83+
}
84+
}
85+
86+
impl<C> Generate for EcdhDecapsulationKey<C>
87+
where
88+
C: CurveArithmetic,
89+
FieldBytesSize<C>: ModulusSize,
90+
{
91+
fn try_generate_from_rng<R: TryCryptoRng + ?Sized>(rng: &mut R) -> Result<Self, R::Error> {
92+
Ok(SecretKey::try_generate_from_rng(rng)?.into())
93+
}
94+
}
95+
96+
impl<C> TryDecapsulate<EcdhKem<C>> for EcdhDecapsulationKey<C>
97+
where
98+
C: CurveArithmetic,
99+
FieldBytesSize<C>: ModulusSize,
100+
AffinePoint<C>: FromEncodedPoint<C> + ToEncodedPoint<C>,
101+
{
102+
type Error = Error;
103+
104+
fn try_decapsulate(
105+
&self,
106+
encapsulated_key: &Ciphertext<EcdhKem<C>>,
107+
) -> Result<SharedKey<EcdhKem<C>>, Error> {
108+
let encapsulated_key = PublicKey::<C>::from_sec1_bytes(encapsulated_key)?;
109+
let shared_secret = self.dk.diffie_hellman(&encapsulated_key);
110+
Ok(shared_secret.raw_secret_bytes().clone())
111+
}
112+
}
113+
114+
/// Elliptic Curve Diffie-Hellman Encapsulation Key (i.e. public encryption key)
115+
///
116+
/// Generic around an elliptic curve `C`.
117+
pub type EcdhEncapsulationKey<C> = EncapsulationKey<PublicKey<C>>;
118+
48119
/// From [RFC9810 §7.1.1]: `SerializePublicKey` and `DeserializePublicKey`:
49120
///
50121
/// > For P-256, P-384, and P-521, the SerializePublicKey() function of the
51122
/// > KEM performs the uncompressed Elliptic-Curve-Point-to-Octet-String
52123
/// > conversion according to [SECG].
53124
///
54-
/// [RFC9810 §7.1.1]: https://datatracker.ietf.org/doc/html/rfc9180#name-serializepublickey-and-dese
125+
/// [RFC9810 §7.1.1]: https://datatracker.ietf.org/doc/html/rfc9180#section-7.1.1
55126
/// [SECG]: https://www.secg.org/sec1-v2.pdf
56127
impl<C> KeySizeUser for EcdhEncapsulationKey<C>
57128
where
@@ -66,7 +137,7 @@ where
66137
/// > DeserializePublicKey() performs the uncompressed
67138
/// > Octet-String-to-Elliptic-Curve-Point conversion.
68139
///
69-
/// [RFC9810 §7.1.1]: https://datatracker.ietf.org/doc/html/rfc9180#name-serializepublickey-and-dese
140+
/// [RFC9810 §7.1.1]: https://datatracker.ietf.org/doc/html/rfc9180#section-7.1.1
70141
impl<C> TryKeyInit for EcdhEncapsulationKey<C>
71142
where
72143
C: CurveArithmetic,
@@ -86,7 +157,7 @@ where
86157
/// > KEM performs the uncompressed Elliptic-Curve-Point-to-Octet-String
87158
/// > conversion according to [SECG].
88159
///
89-
/// [RFC9810 §7.1.1]: https://datatracker.ietf.org/doc/html/rfc9180#name-serializepublickey-and-dese
160+
/// [RFC9810 §7.1.1]: https://datatracker.ietf.org/doc/html/rfc9180#section-7.1.1
90161
/// [SECG]: https://www.secg.org/sec1-v2.pdf
91162
impl<C> KeyExport for EcdhEncapsulationKey<C>
92163
where
@@ -95,20 +166,7 @@ where
95166
AffinePoint<C>: FromEncodedPoint<C> + ToEncodedPoint<C>,
96167
{
97168
fn to_bytes(&self) -> UncompressedPoint<C> {
98-
// TODO(tarcieri): self.0.to_uncompressed_point()
99-
let mut ret = UncompressedPoint::<C>::default();
100-
ret.copy_from_slice(self.to_encoded_point(false).as_bytes());
101-
ret
102-
}
103-
}
104-
105-
impl<C> Generate for EcdhDecapsulationKey<C>
106-
where
107-
C: CurveArithmetic,
108-
FieldBytesSize<C>: ModulusSize,
109-
{
110-
fn try_generate_from_rng<R: TryCryptoRng + ?Sized>(rng: &mut R) -> Result<Self, R::Error> {
111-
Ok(EphemeralSecret::try_generate_from_rng(rng)?.into())
169+
self.0.to_uncompressed_point()
112170
}
113171
}
114172

@@ -133,21 +191,3 @@ where
133191
(pk, ss.raw_secret_bytes().clone())
134192
}
135193
}
136-
137-
impl<C> TryDecapsulate<EcdhKem<C>> for EcdhDecapsulationKey<C>
138-
where
139-
C: CurveArithmetic,
140-
FieldBytesSize<C>: ModulusSize,
141-
AffinePoint<C>: FromEncodedPoint<C> + ToEncodedPoint<C>,
142-
{
143-
type Error = Error;
144-
145-
fn try_decapsulate(
146-
&self,
147-
encapsulated_key: &Ciphertext<EcdhKem<C>>,
148-
) -> Result<SharedKey<EcdhKem<C>>, Error> {
149-
let encapsulated_key = PublicKey::<C>::from_sec1_bytes(encapsulated_key)?;
150-
let shared_secret = self.dk.diffie_hellman(&encapsulated_key);
151-
Ok(shared_secret.raw_secret_bytes().clone())
152-
}
153-
}

dhkem/src/x25519_kem.rs

Lines changed: 59 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,11 @@
11
use crate::{DecapsulationKey, EncapsulationKey};
22
use kem::{
3-
Decapsulate, Encapsulate, Generate, InvalidKey, Kem, Key, KeyExport, KeySizeUser, TryKeyInit,
4-
common::array::Array, consts::U32,
3+
Decapsulate, Encapsulate, Generate, InvalidKey, Kem, Key, KeyExport, KeyInit, KeySizeUser,
4+
TryKeyInit,
5+
common::array::{Array, sizes::U32},
56
};
6-
use rand_core::{CryptoRng, TryCryptoRng, UnwrapErr};
7-
use x25519::{PublicKey, ReusableSecret};
8-
9-
/// Elliptic Curve Diffie-Hellman Decapsulation Key (i.e. secret decryption key)
10-
///
11-
/// Generic around an elliptic curve `C`.
12-
pub type X25519DecapsulationKey = DecapsulationKey<ReusableSecret, PublicKey>;
13-
14-
/// Elliptic Curve Diffie-Hellman Encapsulation Key (i.e. public encryption key)
15-
///
16-
/// Generic around an elliptic curve `C`.
17-
pub type X25519EncapsulationKey = EncapsulationKey<PublicKey>;
7+
use rand_core::{CryptoRng, TryCryptoRng};
8+
use x25519::{PublicKey, StaticSecret};
189

1910
/// X25519 ciphertexts are compressed Montgomery x/u-coordinates.
2011
type Ciphertext = Array<u8, U32>;
@@ -35,6 +26,59 @@ impl Kem for X25519Kem {
3526
type SharedKeySize = U32;
3627
}
3728

29+
/// Elliptic Curve Diffie-Hellman Decapsulation Key (i.e. secret decryption key)
30+
///
31+
/// Generic around an elliptic curve `C`.
32+
pub type X25519DecapsulationKey = DecapsulationKey<StaticSecret, PublicKey>;
33+
34+
impl KeySizeUser for X25519DecapsulationKey {
35+
type KeySize = U32;
36+
}
37+
38+
/// From [RFC9810 §7.1.2]: `SerializePrivateKey` and `DeserializePrivateKey`:
39+
///
40+
/// > For X25519 and X448, private keys are identical to their byte string representation,
41+
/// > so little processing has to be done. [...]
42+
/// > DeserializePrivateKey() function MUST clamp its input
43+
///
44+
/// [RFC9810 §7.1.2]: https://datatracker.ietf.org/doc/html/rfc9180#section-7.1.2
45+
impl KeyInit for X25519DecapsulationKey {
46+
fn new(key: &Key<Self>) -> Self {
47+
StaticSecret::from(key.0).into()
48+
}
49+
}
50+
51+
/// From [RFC9810 §7.1.2]: `SerializePrivateKey` and `DeserializePrivateKey`:
52+
///
53+
/// > For X25519 and X448, private keys are identical to their byte string representation,
54+
/// > so little processing has to be done. The SerializePrivateKey() function MUST clamp its output
55+
///
56+
/// [RFC9810 §7.1.2]: https://datatracker.ietf.org/doc/html/rfc9180#section-7.1.2
57+
impl KeyExport for X25519DecapsulationKey {
58+
fn to_bytes(&self) -> Key<Self> {
59+
self.dk.to_bytes().into()
60+
}
61+
}
62+
63+
impl Generate for X25519DecapsulationKey {
64+
fn try_generate_from_rng<R: TryCryptoRng + ?Sized>(rng: &mut R) -> Result<Self, R::Error> {
65+
let key = Key::<Self>::try_generate_from_rng(rng)?;
66+
Ok(StaticSecret::from(key.0).into())
67+
}
68+
}
69+
70+
impl Decapsulate<X25519Kem> for X25519DecapsulationKey {
71+
fn decapsulate(&self, encapsulated_key: &Ciphertext) -> SharedKey {
72+
let public_key = PublicKey::from(encapsulated_key.0);
73+
self.dk.diffie_hellman(&public_key).to_bytes().into()
74+
}
75+
}
76+
77+
/// Elliptic Curve Diffie-Hellman Encapsulation Key (i.e. public encryption key)
78+
///
79+
/// Generic around an elliptic curve `C`.
80+
pub type X25519EncapsulationKey = EncapsulationKey<PublicKey>;
81+
3882
/// From [RFC9810 §7.1.1]: `SerializePublicKey` and `DeserializePublicKey`:
3983
///
4084
/// > For X25519 and X448, the SerializePublicKey() and
@@ -72,31 +116,15 @@ impl KeyExport for X25519EncapsulationKey {
72116
}
73117
}
74118

75-
impl Generate for X25519DecapsulationKey {
76-
fn try_generate_from_rng<R: TryCryptoRng + ?Sized>(rng: &mut R) -> Result<Self, R::Error> {
77-
// TODO(tarcieri): don't panic! Fallible `ReusableSecret` generation?
78-
Ok(Self::from(ReusableSecret::random_from_rng(&mut UnwrapErr(
79-
rng,
80-
))))
81-
}
82-
}
83-
84119
impl Encapsulate<X25519Kem> for X25519EncapsulationKey {
85120
fn encapsulate_with_rng<R>(&self, rng: &mut R) -> (Ciphertext, SharedKey)
86121
where
87122
R: CryptoRng + ?Sized,
88123
{
89124
// ECDH encapsulation involves creating a new ephemeral key pair and then doing DH
90-
let sk = ReusableSecret::random_from_rng(rng);
125+
let sk = StaticSecret::random_from_rng(rng);
91126
let pk = PublicKey::from(&sk);
92127
let ss = sk.diffie_hellman(&self.0);
93128
(pk.to_bytes().into(), ss.to_bytes().into())
94129
}
95130
}
96-
97-
impl Decapsulate<X25519Kem> for X25519DecapsulationKey {
98-
fn decapsulate(&self, encapsulated_key: &Ciphertext) -> SharedKey {
99-
let public_key = PublicKey::from(encapsulated_key.0);
100-
self.dk.diffie_hellman(&public_key).to_bytes().into()
101-
}
102-
}

dhkem/tests/hpke_p256_test.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22

33
use core::convert::Infallible;
44
use dhkem::NistP256DecapsulationKey;
5-
use elliptic_curve::Generate;
65
use hex_literal::hex;
76
use hkdf::Hkdf;
8-
use kem::{Encapsulate, KeyExport, TryDecapsulate};
7+
use kem::{Encapsulate, KeyExport, TryDecapsulate, TryKeyInit};
98
use rand_core::{TryCryptoRng, TryRng};
109
use sha2::Sha256;
1110

@@ -68,6 +67,7 @@ fn extract_and_expand(shared_secret: &[u8], kem_context: &[u8]) -> Vec<u8> {
6867
#[test]
6968
// section A.3.1 https://datatracker.ietf.org/doc/html/rfc9180#appendix-A.3.1
7069
fn test_dhkem_p256_hkdf_sha256() {
70+
let example_key = hex!("f3ce7fdae57e1a310d87f1ebbde6f328be0a99cdbcadf4d6589cf29de4b8ffd2");
7171
let example_pke = hex!(
7272
"04a92719c6195d5085104f469a8b9814d5838ff72b60501e2c4466e5e67b32\
7373
5ac98536d7b61a1af4b78e5b7f951c0900be863c403ce65c9bfcb9382657222d18c4"
@@ -79,10 +79,7 @@ fn test_dhkem_p256_hkdf_sha256() {
7979
let example_shared_secret =
8080
hex!("c0d26aeab536609a572b07695d933b589dcf363ff9d93c93adea537aeabb8cb8");
8181

82-
let skr = NistP256DecapsulationKey::try_generate_from_rng(&mut ConstantRng(&hex!(
83-
"f3ce7fdae57e1a310d87f1ebbde6f328be0a99cdbcadf4d6589cf29de4b8ffd2"
84-
)))
85-
.unwrap();
82+
let skr = NistP256DecapsulationKey::new(&example_key.into()).unwrap();
8683
let pkr = skr.as_ref();
8784
assert_eq!(&pkr.to_bytes(), &example_pkr);
8885

0 commit comments

Comments
 (0)