Skip to content

Commit fec3022

Browse files
committed
Add decaf488 support
1 parent 20e9e8a commit fec3022

17 files changed

+362
-53
lines changed

.github/workflows/main.yml

+4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ jobs:
3636
matrix:
3737
backend_feature:
3838
- --features ristretto255-ciphersuite
39+
- --features decaf448-ciphersuite
40+
- --features ristretto255-ciphersuite,decaf448-ciphersuite
3941
-
4042
frontend_feature:
4143
-
@@ -94,6 +96,8 @@ jobs:
9496
backend_feature:
9597
-
9698
- --features ristretto255-ciphersuite
99+
- --features decaf448-ciphersuite
100+
- --features ristretto255-ciphersuite,decaf448-ciphersuite
97101
frontend_feature:
98102
-
99103
- --features danger

Cargo.toml

+7
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ version = "0.5.0"
1414
[features]
1515
alloc = []
1616
danger = []
17+
decaf448 = ["dep:ed448"]
18+
decaf448-ciphersuite = ["decaf448", "dep:sha3"]
1719
default = ["ristretto255-ciphersuite", "dep:serde"]
1820
ristretto255 = ["dep:curve25519-dalek"]
1921
ristretto255-ciphersuite = ["ristretto255", "dep:sha2"]
@@ -29,6 +31,9 @@ curve25519-dalek = { version = "4", default-features = false, features = [
2931
derive-where = { version = "1", features = ["zeroize-on-drop"] }
3032
digest = "0.11.0-pre.10"
3133
displaydoc = { version = "0.2", default-features = false }
34+
ed448 = { version = "0.17.0-pre.0", default-features = false, features = [
35+
"zeroize",
36+
], optional = true }
3237
elliptic-curve = { version = "0.14.0-rc.1", features = [
3338
"hash2curve",
3439
"sec1",
@@ -40,6 +45,7 @@ serde = { version = "1", default-features = false, features = [
4045
"derive",
4146
], optional = true }
4247
sha2 = { version = "0.11.0-pre.5", default-features = false, optional = true }
48+
sha3 = { version = "0.11.0-pre.5", default-features = false, optional = true }
4349
subtle = { version = "2.3", default-features = false }
4450
zeroize = { version = "1.5", default-features = false }
4551

@@ -71,6 +77,7 @@ targets = []
7177
[patch.crates-io]
7278
crypto-bigint = { git = "https://github.com/RustCrypto/crypto-bigint.git", rev = "e97beae6593c30b4477b7f29e30e5372cd8204bf" }
7379
curve25519-dalek = { git = "https://github.com/khonsulabs/curve25519-dalek", branch = "voprf" }
80+
ed448 = { git = "https://github.com/khonsulabs/elliptic-curves", branch = "ed448-voprf" }
7481
elliptic-curve = { git = "https://github.com/RustCrypto/traits", rev = "204a4e030fa98863429ccd3797e12f9e7c45dc33" }
7582
ff = { git = "https://github.com/zkcrypto/ff", branch = "release-0.14.0" }
7683
group = { git = "https://github.com/pinkforest/group", branch = "bump-rand-0.9" }

src/ciphersuite.rs

+9-3
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@
1111
use digest::core_api::BlockSizeUser;
1212
use digest::{FixedOutput, HashMarker, OutputSizeUser};
1313
use elliptic_curve::VoprfParameters;
14+
use elliptic_curve::hash2curve::{ExpandMsg, ExpandMsgXmd};
1415
use hybrid_array::typenum::{IsLess, IsLessOrEqual, U256};
1516

1617
use crate::Group;
1718

1819
/// Configures the underlying primitives used in VOPRF
1920
pub trait CipherSuite
2021
where
21-
<Self::Hash as OutputSizeUser>::OutputSize:
22-
IsLess<U256> + IsLessOrEqual<<Self::Hash as BlockSizeUser>::BlockSize>,
22+
<Self::Hash as OutputSizeUser>::OutputSize: IsLess<U256>,
2323
{
2424
/// The ciphersuite identifier as dictated by
2525
/// <https://www.rfc-editor.org/rfc/rfc9497>
@@ -31,7 +31,11 @@ where
3131

3232
/// The main hash function to use (for HKDF computations and hashing
3333
/// transcripts).
34-
type Hash: BlockSizeUser + Default + FixedOutput + HashMarker;
34+
type Hash: Default + FixedOutput + HashMarker;
35+
36+
/// Which function to use for `expand_message` in `HashToGroup()` and
37+
/// `HashToScalar()`.
38+
type ExpandMsg: for<'a> ExpandMsg<'a>;
3539
}
3640

3741
impl<T: VoprfParameters> CipherSuite for T
@@ -46,4 +50,6 @@ where
4650
type Group = T;
4751

4852
type Hash = T::Hash;
53+
54+
type ExpandMsg = ExpandMsgXmd<Self::Hash>;
4955
}

src/common.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ pub(crate) fn generate_proof<CS: CipherSuite, R: TryRngCore + TryCryptoRng>(
179179

180180
let dst = Dst::new::<CS, _>(STR_HASH_TO_SCALAR, mode);
181181
// This can't fail, the size of the `input` is known.
182-
let c_scalar = CS::Group::hash_to_scalar::<CS::Hash>(&h2_input, &dst.as_dst()).unwrap();
182+
let c_scalar = CS::Group::hash_to_scalar::<CS::ExpandMsg>(&h2_input, &dst.as_dst()).unwrap();
183183
let s_scalar = r - &(c_scalar * &k);
184184

185185
Ok(Proof { c_scalar, s_scalar })
@@ -235,7 +235,7 @@ pub(crate) fn verify_proof<CS: CipherSuite>(
235235

236236
let dst = Dst::new::<CS, _>(STR_HASH_TO_SCALAR, mode);
237237
// This can't fail, the size of the `input` is known.
238-
let c = CS::Group::hash_to_scalar::<CS::Hash>(&h2_input, &dst.as_dst()).unwrap();
238+
let c = CS::Group::hash_to_scalar::<CS::ExpandMsg>(&h2_input, &dst.as_dst()).unwrap();
239239

240240
match c.ct_eq(&proof.c_scalar).into() {
241241
true => Ok(()),
@@ -309,7 +309,7 @@ fn compute_composites<
309309

310310
let dst = Dst::new::<CS, _>(STR_HASH_TO_SCALAR, mode);
311311
// This can't fail, the size of the `input` is known.
312-
let di = CS::Group::hash_to_scalar::<CS::Hash>(&h2_input, &dst.as_dst()).unwrap();
312+
let di = CS::Group::hash_to_scalar::<CS::ExpandMsg>(&h2_input, &dst.as_dst()).unwrap();
313313
m = c * &di + &m;
314314
z = match k_option {
315315
Some(_) => z,
@@ -344,7 +344,7 @@ pub(crate) fn derive_key_internal<CS: CipherSuite>(
344344
// deriveInput = seed || I2OSP(len(info), 2) || info
345345
// skS = G.HashToScalar(deriveInput || I2OSP(counter, 1), DST = "DeriveKeyPair"
346346
// || contextString)
347-
let sk_s = CS::Group::hash_to_scalar::<CS::Hash>(
347+
let sk_s = CS::Group::hash_to_scalar::<CS::ExpandMsg>(
348348
&[seed, &info_len, info, &counter.to_be_bytes()],
349349
&dst.as_dst(),
350350
)
@@ -410,7 +410,7 @@ pub(crate) fn hash_to_group<CS: CipherSuite>(
410410
mode: Mode,
411411
) -> Result<<CS::Group as Group>::Elem> {
412412
let dst = Dst::new::<CS, _>(STR_HASH_TO_GROUP, mode);
413-
CS::Group::hash_to_curve::<CS::Hash>(&[input], &dst.as_dst()).map_err(|_| Error::Input)
413+
CS::Group::hash_to_curve::<CS::ExpandMsg>(&[input], &dst.as_dst()).map_err(|_| Error::Input)
414414
}
415415

416416
/// Internal function that finalizes the hash input for OPRF, VOPRF & POPRF.

src/group/decaf.rs

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Copyright (c) Meta Platforms, Inc. and affiliates.
2+
//
3+
// This source code is dual-licensed under either the MIT license found in the
4+
// LICENSE-MIT file in the root directory of this source tree or the Apache
5+
// License, Version 2.0 found in the LICENSE-APACHE file in the root directory
6+
// of this source tree. You may select, at your option, one of the above-listed
7+
// licenses.
8+
9+
use ed448::{CompressedDecaf, DecafPoint, Scalar};
10+
use elliptic_curve::bigint::{Encoding, NonZero, U448, U512};
11+
use elliptic_curve::hash2curve::{ExpandMsg, ExpandMsgXof, Expander};
12+
use hybrid_array::Array;
13+
use hybrid_array::typenum::U56;
14+
use rand_core::{TryCryptoRng, TryRngCore};
15+
use subtle::ConstantTimeEq;
16+
17+
use super::Group;
18+
use crate::{Error, InternalError, Result};
19+
20+
/// [`Group`] implementation for Decaf448.
21+
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
22+
pub struct Decaf448;
23+
24+
const WIDE_ORDER: NonZero<U512> = NonZero::<U512>::new_unwrap(U512::from_be_hex("00000000000000003fffffffffffffffffffffffffffffffffffffffffffffffffffffff7cca23e9c44edb49aed63690216cc2728dc58f552378c292ab5844f3"));
25+
const ORDER: NonZero<U448> = NonZero::<U448>::new_unwrap(ed448::ORDER);
26+
27+
#[cfg(feature = "decaf448-ciphersuite")]
28+
impl crate::CipherSuite for Decaf448 {
29+
const ID: &'static str = "decaf448-SHAKE256";
30+
31+
type Group = Decaf448;
32+
33+
type Hash = super::xof_fixed_wrapper::XofFixedWrapper<sha3::Shake256, hybrid_array::sizes::U64>;
34+
35+
type ExpandMsg = ExpandMsgXof<Self::Hash>;
36+
}
37+
38+
impl Group for Decaf448 {
39+
type Elem = DecafPoint;
40+
41+
type ElemLen = U56;
42+
43+
type Scalar = Scalar;
44+
45+
type ScalarLen = U56;
46+
47+
// Implements the `hash_to_ristretto255()` function from
48+
// https://www.rfc-editor.org/rfc/rfc9380.html#appendix-C
49+
fn hash_to_curve<X>(input: &[&[u8]], dst: &[&[u8]]) -> Result<Self::Elem, InternalError>
50+
where
51+
X: for<'a> ExpandMsg<'a>,
52+
{
53+
let mut uniform_bytes = [0; 112];
54+
X::expand_message(input, dst, 112)
55+
.map_err(|_| InternalError::Input)?
56+
.fill_bytes(&mut uniform_bytes);
57+
58+
Ok(DecafPoint::from_uniform_bytes(&uniform_bytes))
59+
}
60+
61+
// Implements the `HashToScalar()` function from
62+
// https://www.rfc-editor.org/rfc/rfc9497#section-4.2
63+
fn hash_to_scalar<X>(input: &[&[u8]], dst: &[&[u8]]) -> Result<Self::Scalar, InternalError>
64+
where
65+
X: for<'a> ExpandMsg<'a>,
66+
{
67+
let mut uniform_bytes = [0; 64];
68+
X::expand_message(input, dst, 64)
69+
.map_err(|_| InternalError::Input)?
70+
.fill_bytes(&mut uniform_bytes);
71+
let uniform_bytes = U512::from_le_slice(&uniform_bytes);
72+
73+
let scalar = uniform_bytes.rem(&WIDE_ORDER);
74+
let scalar = Scalar::from_bytes(&scalar.to_le_bytes()[..56].try_into().unwrap());
75+
76+
Ok(scalar)
77+
}
78+
79+
fn base_elem() -> Self::Elem {
80+
DecafPoint::GENERATOR
81+
}
82+
83+
fn identity_elem() -> Self::Elem {
84+
DecafPoint::IDENTITY
85+
}
86+
87+
// serialization of a group element
88+
fn serialize_elem(elem: Self::Elem) -> Array<u8, Self::ElemLen> {
89+
elem.compress().0.into()
90+
}
91+
92+
fn deserialize_elem(element_bits: &[u8]) -> Result<Self::Elem> {
93+
let result = element_bits
94+
.try_into()
95+
.map(CompressedDecaf)
96+
.map_err(|_| Error::Deserialization)?
97+
.decompress();
98+
Option::from(result)
99+
.filter(|point| point != &DecafPoint::IDENTITY)
100+
.ok_or(Error::Deserialization)
101+
}
102+
103+
fn random_scalar<R: TryRngCore + TryCryptoRng>(rng: &mut R) -> Result<Self::Scalar, R::Error> {
104+
loop {
105+
let mut scalar_bytes = [0; 64];
106+
rng.try_fill_bytes(&mut scalar_bytes)?;
107+
let scalar_bytes = U512::from_le_slice(&scalar_bytes);
108+
let scalar = scalar_bytes.rem(&WIDE_ORDER);
109+
let scalar = Scalar::from_bytes(&scalar.to_le_bytes()[..56].try_into().unwrap());
110+
111+
if scalar != Scalar::ZERO {
112+
break Ok(scalar);
113+
}
114+
}
115+
}
116+
117+
fn invert_scalar(scalar: Self::Scalar) -> Self::Scalar {
118+
scalar.invert()
119+
}
120+
121+
fn is_zero_scalar(scalar: Self::Scalar) -> subtle::Choice {
122+
scalar.ct_eq(&Scalar::ZERO)
123+
}
124+
125+
#[cfg(test)]
126+
fn zero_scalar() -> Self::Scalar {
127+
Scalar::ZERO
128+
}
129+
130+
fn serialize_scalar(scalar: Self::Scalar) -> Array<u8, Self::ScalarLen> {
131+
scalar.to_bytes().into()
132+
}
133+
134+
fn deserialize_scalar(scalar_bits: &[u8]) -> Result<Self::Scalar> {
135+
scalar_bits
136+
.try_into()
137+
.ok()
138+
.map(U448::from_le_bytes)
139+
.map(|value| Scalar::from_bytes(&value.rem(&ORDER).to_le_bytes()))
140+
.filter(|scalar| scalar != &Scalar::ZERO)
141+
.ok_or(Error::Deserialization)
142+
}
143+
}

src/group/elliptic_curve.rs

+8-13
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,13 @@
88

99
use core::ops::Add;
1010

11-
use digest::core_api::BlockSizeUser;
12-
use digest::{FixedOutput, HashMarker};
1311
use elliptic_curve::group::cofactor::CofactorGroup;
14-
use elliptic_curve::hash2curve::{ExpandMsgXmd, FromOkm, GroupDigest};
12+
use elliptic_curve::hash2curve::{ExpandMsg, FromOkm, GroupDigest};
1513
use elliptic_curve::sec1::{FromEncodedPoint, ModulusSize, ToEncodedPoint};
1614
use elliptic_curve::{
1715
AffinePoint, Field, FieldBytesSize, Group as _, ProjectivePoint, PublicKey, Scalar, SecretKey,
1816
};
19-
use hybrid_array::typenum::{IsLess, IsLessOrEqual, Sum, U256};
17+
use hybrid_array::typenum::Sum;
2018
use hybrid_array::{Array, ArraySize};
2119
use rand_core::{TryCryptoRng, TryRngCore};
2220

@@ -50,22 +48,19 @@ where
5048

5149
// Implements the `hash_to_curve()` function from
5250
// https://www.rfc-editor.org/rfc/rfc9380.html#section-3
53-
fn hash_to_curve<H>(input: &[&[u8]], dst: &[&[u8]]) -> Result<Self::Elem, InternalError>
51+
fn hash_to_curve<X>(input: &[&[u8]], dst: &[&[u8]]) -> Result<Self::Elem, InternalError>
5452
where
55-
H: BlockSizeUser + Default + FixedOutput + HashMarker,
56-
H::OutputSize: IsLess<U256> + IsLessOrEqual<H::BlockSize>,
53+
X: for<'a> ExpandMsg<'a>,
5754
{
58-
Self::hash_from_bytes::<ExpandMsgXmd<H>>(input, dst).map_err(|_| InternalError::Input)
55+
Self::hash_from_bytes::<X>(input, dst).map_err(|_| InternalError::Input)
5956
}
6057

6158
// Implements the `HashToScalar()` function
62-
fn hash_to_scalar<H>(input: &[&[u8]], dst: &[&[u8]]) -> Result<Self::Scalar, InternalError>
59+
fn hash_to_scalar<X>(input: &[&[u8]], dst: &[&[u8]]) -> Result<Self::Scalar, InternalError>
6360
where
64-
H: BlockSizeUser + Default + FixedOutput + HashMarker,
65-
H::OutputSize: IsLess<U256> + IsLessOrEqual<H::BlockSize>,
61+
X: for<'a> ExpandMsg<'a>,
6662
{
67-
<Self as GroupDigest>::hash_to_scalar::<ExpandMsgXmd<H>>(input, dst)
68-
.map_err(|_| InternalError::Input)
63+
<Self as GroupDigest>::hash_to_scalar::<X>(input, dst).map_err(|_| InternalError::Input)
6964
}
7065

7166
fn base_elem() -> Self::Elem {

src/group/mod.rs

+12-9
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,20 @@
88

99
//! Defines the Group trait to specify the underlying prime order group
1010
11+
#[cfg(feature = "decaf448")]
12+
mod decaf;
1113
mod elliptic_curve;
1214
#[cfg(feature = "ristretto255")]
1315
mod ristretto;
16+
#[cfg(feature = "decaf448-ciphersuite")]
17+
mod xof_fixed_wrapper;
1418

1519
use core::ops::{Add, Mul, Sub};
1620

17-
use digest::core_api::BlockSizeUser;
18-
use digest::{FixedOutput, HashMarker};
19-
use hybrid_array::typenum::{IsLess, IsLessOrEqual, Sum, U256};
21+
use ::elliptic_curve::hash2curve::ExpandMsg;
22+
#[cfg(feature = "decaf448")]
23+
pub use decaf::Decaf448;
24+
use hybrid_array::typenum::Sum;
2025
use hybrid_array::{Array, ArraySize};
2126
use rand_core::{TryCryptoRng, TryRngCore};
2227
#[cfg(feature = "ristretto255")]
@@ -63,20 +68,18 @@ where
6368
/// # Errors
6469
/// [`Error::Input`](crate::Error::Input) if the `input` is empty or longer
6570
/// then [`u16::MAX`].
66-
fn hash_to_curve<H>(input: &[&[u8]], dst: &[&[u8]]) -> Result<Self::Elem, InternalError>
71+
fn hash_to_curve<X>(input: &[&[u8]], dst: &[&[u8]]) -> Result<Self::Elem, InternalError>
6772
where
68-
H: BlockSizeUser + Default + FixedOutput + HashMarker,
69-
H::OutputSize: IsLess<U256> + IsLessOrEqual<H::BlockSize>;
73+
X: for<'a> ExpandMsg<'a>;
7074

7175
/// Hashes a slice of pseudo-random bytes to a scalar
7276
///
7377
/// # Errors
7478
/// [`Error::Input`](crate::Error::Input) if the `input` is empty or longer
7579
/// then [`u16::MAX`].
76-
fn hash_to_scalar<H>(input: &[&[u8]], dst: &[&[u8]]) -> Result<Self::Scalar, InternalError>
80+
fn hash_to_scalar<X>(input: &[&[u8]], dst: &[&[u8]]) -> Result<Self::Scalar, InternalError>
7781
where
78-
H: BlockSizeUser + Default + FixedOutput + HashMarker,
79-
H::OutputSize: IsLess<U256> + IsLessOrEqual<H::BlockSize>;
82+
X: for<'a> ExpandMsg<'a>;
8083

8184
/// Get the base point for the group
8285
fn base_elem() -> Self::Elem;

0 commit comments

Comments
 (0)