Skip to main content

ecdh_omr/
blinding.rs

1// SPDX-FileCopyrightText: 2024-2026 eaon <eaon@posteo.net>
2// SPDX-License-Identifier: EUPL-1.2
3
4#[cfg(feature = "rustcrypto-ec")]
5use std::ops::Add;
6
7#[cfg(feature = "dalek-ristretto255")]
8use curve25519_dalek::traits::Identity;
9#[cfg(feature = "rustcrypto-ec")]
10use elliptic_curve::{
11    AffinePoint, Curve, CurveArithmetic, Generate, ProjectivePoint,
12    point::PointCompression,
13    sec1::{CompressedPoint, CompressedPointSize, FromSec1Point, ModulusSize, ToSec1Point},
14};
15use hybrid_array::Array;
16#[cfg(feature = "rustcrypto-ec")]
17use hybrid_array::ArraySize;
18use rand_core::CryptoRng;
19use typenum::Sum;
20#[cfg(feature = "rustcrypto-ec")]
21use typenum::Unsigned;
22
23#[cfg(feature = "dalek-x25519")]
24use crate::DalekX25519;
25#[cfg(feature = "rustcrypto-ec")]
26use crate::EllipticCurve;
27#[cfg(feature = "dalek-ristretto255")]
28use crate::{DalekRistretto255, dalek_ristretto255};
29use crate::{KeyPair, error::*};
30
31type BlindedPublicKeySize<C> = Sum<<C as KeyPair>::PublicKeySize, <C as KeyPair>::PublicKeySize>;
32
33/// An ECDH public key that has been blinded, enabling a third party to send a message without
34/// knowing the cryptographic identity of the recipient.
35#[derive(Debug, Clone)]
36pub struct BlindedPublicKey<K: KeyPair> {
37    /// The blinded public key that can be used to perform a normal Diffie-Hellman key agreement.
38    pub(crate) inner: K::PublicKey,
39    /// Blinding factor used to create the blinded public key.
40    ///
41    /// Carrying this piece of information is necessary because we want a third party to be able to
42    /// share a secret of its choosing with the party controlling the secret key of the blinded
43    /// public key.
44    pub(crate) blinding_factor: K::PublicKey,
45}
46
47/// Blind a public key.
48pub trait Blind<K: KeyPair> {
49    /// Blind a public key with the supplied RNG.
50    fn blind(&self, csprng: &mut impl CryptoRng) -> BlindedPublicKey<K>;
51}
52
53/// (De)serialization for [`BlindedPublicKey`].
54///
55/// # Security
56///
57/// Implementers **must** validate deserialized public keys in [`from_bytes`](Self::from_bytes).
58/// Small-order and identity points must be rejected with [`Error::InvalidPoint`].
59/// Failure to validate can lead to key recovery or other cryptographic attacks.
60pub trait Blinded: Sized {
61    /// A bytes array type with the size of the serialized [`BlindedPublicKey`].
62    type BytesArray;
63    /// Parse a [`BlindedPublicKey`] from a `BytesArray`.
64    ///
65    /// # Security
66    ///
67    /// Implementers **must** validate deserialized blinded public key as well as its blinding
68    /// factor and reject small-order and identity points with [`Error::InvalidPoint`].
69    fn from_bytes(bytes: &Self::BytesArray) -> Result<Self>;
70    /// Serialize [`BlindedPublicKey`] to a `BytesArray`.
71    fn to_bytes(&self) -> Self::BytesArray;
72}
73
74impl<'a, K: KeyPair> TryFrom<&'a [u8]> for BlindedPublicKey<K>
75where
76    Self: Blinded,
77    <Self as Blinded>::BytesArray: TryFrom<&'a [u8]>,
78{
79    type Error = Error;
80
81    fn try_from(bytes: &'a [u8]) -> Result<Self> {
82        Self::from_bytes(
83            &<Self as Blinded>::BytesArray::try_from(bytes).map_err(|_| Error::Decoding)?,
84        )
85    }
86}
87
88#[cfg(feature = "dalek-ristretto255")]
89pub(crate) fn checked_decompress_ristretto255_public_key(
90    bytes: [u8; 32],
91) -> Result<dalek_ristretto255::PublicKey> {
92    let decompressed = curve25519_dalek::ristretto::CompressedRistretto(bytes)
93        .decompress()
94        .ok_or(Error::Decoding)?;
95
96    (decompressed != curve25519_dalek::ristretto::RistrettoPoint::identity())
97        .then_some(dalek_ristretto255::PublicKey(decompressed))
98        .ok_or(Error::InvalidPoint)
99}
100
101#[cfg(feature = "dalek-x25519")]
102pub(crate) fn checked_x25519_public_key(bytes: [u8; 32]) -> Result<x25519_dalek::PublicKey> {
103    // Sign choice irrelevant for small-order check
104    curve25519_dalek::montgomery::MontgomeryPoint(bytes)
105        .to_edwards(0)
106        .filter(|point| !point.is_small_order())
107        .map(|_| x25519_dalek::PublicKey::from(bytes))
108        .ok_or(Error::InvalidPoint)
109}
110
111#[cfg(any(feature = "dalek-ristretto255", feature = "dalek-x25519"))]
112macro_rules! dalek_blinding {
113    (
114        $random_blind_fn:ident,
115        $blind_fn:ident,
116        (
117            $curve:ty,
118            $public:ty,
119            $secret:ty,
120            $public_from_secret:expr,
121            $shared_secret_to_public:expr,
122            $checked_public_key:expr,
123            $public_to_bytes:expr $(,)?
124        ) $(,)?
125    ) => {
126        pub(crate) fn $random_blind_fn(
127            public_key: &$public,
128            csprng: &mut impl CryptoRng,
129        ) -> ($public, $secret) {
130            let blinding_factor_secret = <$secret>::random_from_rng(csprng);
131            let blinded_public_key = $blind_fn(public_key, &blinding_factor_secret);
132
133            (blinded_public_key, blinding_factor_secret)
134        }
135
136        pub(crate) fn $blind_fn(public_key: &$public, blinding_factor_secret: &$secret) -> $public {
137            let blinded_shared_secret = blinding_factor_secret.diffie_hellman(public_key);
138
139            $shared_secret_to_public(blinded_shared_secret)
140        }
141
142        impl Blind<$curve> for $public {
143            fn blind(&self, csprng: &mut impl CryptoRng) -> BlindedPublicKey<$curve> {
144                let (inner, blinding_factor_secret) = $random_blind_fn(self, csprng);
145
146                BlindedPublicKey {
147                    inner,
148                    blinding_factor: $public_from_secret(&blinding_factor_secret),
149                }
150            }
151        }
152
153        impl Blinded for BlindedPublicKey<$curve> {
154            type BytesArray = Array<u8, BlindedPublicKeySize<$curve>>;
155
156            fn from_bytes(bytes: &Self::BytesArray) -> Result<Self> {
157                let (inner_bytes, blinding_factor_bytes) =
158                    bytes.split::<<$curve as KeyPair>::PublicKeySize>();
159                let inner = $checked_public_key(inner_bytes.into())?;
160                let blinding_factor = $checked_public_key(blinding_factor_bytes.into())?;
161
162                Ok(Self {
163                    inner,
164                    blinding_factor,
165                })
166            }
167
168            fn to_bytes(&self) -> Self::BytesArray {
169                $public_to_bytes(&self.inner)
170                    .into_iter()
171                    .chain($public_to_bytes(&self.blinding_factor))
172                    .collect()
173            }
174        }
175    };
176}
177
178#[cfg(feature = "dalek-ristretto255")]
179dalek_blinding!(
180    random_blind_dalek_ristretto255,
181    blind_dalek_ristretto255,
182    (
183        DalekRistretto255,
184        dalek_ristretto255::PublicKey,
185        dalek_ristretto255::StaticSecret,
186        dalek_ristretto255::PublicKey::from,
187        |secret: dalek_ristretto255::SharedSecret| dalek_ristretto255::PublicKey(secret.0),
188        checked_decompress_ristretto255_public_key,
189        |public: &dalek_ristretto255::PublicKey| public.to_bytes(),
190    ),
191);
192
193#[cfg(feature = "dalek-x25519")]
194dalek_blinding!(
195    random_blind_dalek_x25519,
196    blind_dalek_x25519,
197    (
198        DalekX25519,
199        x25519_dalek::PublicKey,
200        x25519_dalek::StaticSecret,
201        x25519_dalek::PublicKey::from,
202        |secret: x25519_dalek::SharedSecret| x25519_dalek::PublicKey::from(*secret.as_bytes()),
203        checked_x25519_public_key,
204        |public: &x25519_dalek::PublicKey| *public.as_bytes(),
205    ),
206);
207
208#[cfg(feature = "rustcrypto-ec")]
209fn diffie_hellman_affine<C: CurveArithmetic>(
210    secret_key: &elliptic_curve::SecretKey<C>,
211    public_key: &elliptic_curve::PublicKey<C>,
212) -> AffinePoint<C> {
213    use elliptic_curve::group::Curve;
214
215    let public_point = ProjectivePoint::<C>::from(*public_key.as_affine());
216
217    (public_point * secret_key.to_nonzero_scalar().as_ref()).to_affine()
218}
219
220#[cfg(feature = "rustcrypto-ec")]
221pub(crate) fn random_blind_rcec<C: CurveArithmetic>(
222    public_key: &elliptic_curve::PublicKey<C>,
223    csprng: &mut impl CryptoRng,
224) -> (elliptic_curve::PublicKey<C>, elliptic_curve::SecretKey<C>) {
225    let blinding_factor_secret = elliptic_curve::SecretKey::<C>::generate_from_rng(csprng);
226    let blinded_public_key = blind_rcec(public_key, &blinding_factor_secret);
227
228    (blinded_public_key, blinding_factor_secret)
229}
230
231#[cfg(feature = "rustcrypto-ec")]
232pub(crate) fn blind_rcec<C: CurveArithmetic>(
233    public_key: &elliptic_curve::PublicKey<C>,
234    blinding_factor_secret: &elliptic_curve::SecretKey<C>,
235) -> elliptic_curve::PublicKey<C> {
236    let blinded_shared_secret = diffie_hellman_affine(blinding_factor_secret, public_key);
237
238    // SAFETY: Blinded shared secret is never an identity point
239    #[allow(unsafe_code)]
240    unsafe {
241        elliptic_curve::PublicKey::<C>::from_affine(blinded_shared_secret).unwrap_unchecked()
242    }
243}
244
245#[cfg(feature = "rustcrypto-ec")]
246impl<C: CurveArithmetic> Blind<EllipticCurve<C>> for elliptic_curve::PublicKey<C>
247where
248    <C as Curve>::FieldBytesSize: ModulusSize,
249{
250    fn blind(&self, csprng: &mut impl CryptoRng) -> BlindedPublicKey<EllipticCurve<C>> {
251        let (inner, blinding_factor_secret) = random_blind_rcec(self, csprng);
252
253        BlindedPublicKey {
254            inner,
255            blinding_factor: blinding_factor_secret.public_key(),
256        }
257    }
258}
259
260#[cfg(feature = "rustcrypto-ec")]
261impl<C: CurveArithmetic + PointCompression> Blinded for BlindedPublicKey<EllipticCurve<C>>
262where
263    <C as Curve>::FieldBytesSize: ModulusSize,
264    <C as CurveArithmetic>::AffinePoint: ToSec1Point<C> + FromSec1Point<C>,
265    CompressedPointSize<C>: Add<CompressedPointSize<C>> + ArraySize,
266    Sum<CompressedPointSize<C>, CompressedPointSize<C>>: ArraySize,
267{
268    type BytesArray = Array<u8, BlindedPublicKeySize<EllipticCurve<C>>>;
269
270    fn from_bytes(bytes: &Self::BytesArray) -> Result<Self> {
271        // from_sec1_bytes also rejects identity points, but we can't distinguish between
272        // decoding errors and invalid points from the provided error
273        let public_size = CompressedPointSize::<C>::USIZE;
274        let from_bytes = elliptic_curve::PublicKey::<C>::from_sec1_bytes;
275
276        Ok(Self {
277            inner: from_bytes(&bytes[..public_size]).map_err(|_| Error::Decoding)?,
278            blinding_factor: from_bytes(&bytes[public_size..]).map_err(|_| Error::Decoding)?,
279        })
280    }
281
282    fn to_bytes(&self) -> Self::BytesArray {
283        CompressedPoint::<C>::from(&self.inner)
284            .into_iter()
285            .chain(CompressedPoint::<C>::from(&self.blinding_factor))
286            .collect()
287    }
288}