mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 20:29:23 +00:00
Smash Ciphersuite definitions into their own crates
Uses dalek-ff-group for Ed25519 and Ristretto. Uses minimal-ed448 for Ed448. Adds ciphersuite-kp256 for Secp256k1 and P-256.
This commit is contained in:
@@ -1,100 +0,0 @@
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use sha2::{Digest, Sha512};
|
||||
|
||||
use group::Group;
|
||||
use dalek_ff_group::Scalar;
|
||||
|
||||
use crate::Ciphersuite;
|
||||
|
||||
macro_rules! dalek_curve {
|
||||
(
|
||||
$feature: literal,
|
||||
|
||||
$Ciphersuite: ident,
|
||||
$Point: ident,
|
||||
$ID: literal
|
||||
) => {
|
||||
use dalek_ff_group::$Point;
|
||||
|
||||
impl Ciphersuite for $Ciphersuite {
|
||||
type F = Scalar;
|
||||
type G = $Point;
|
||||
type H = Sha512;
|
||||
|
||||
const ID: &'static [u8] = $ID;
|
||||
|
||||
fn generator() -> Self::G {
|
||||
$Point::generator()
|
||||
}
|
||||
|
||||
fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F {
|
||||
Scalar::from_hash(Sha512::new_with_prefix(&[dst, data].concat()))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Ciphersuite for Ristretto.
|
||||
///
|
||||
/// hash_to_F is implemented with a naive concatenation of the dst and data, allowing transposition
|
||||
/// between the two. This means `dst: b"abc", data: b"def"`, will produce the same scalar as
|
||||
/// `dst: "abcdef", data: b""`. Please use carefully, not letting dsts be substrings of each other.
|
||||
#[cfg(any(test, feature = "ristretto"))]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct Ristretto;
|
||||
#[cfg(any(test, feature = "ristretto"))]
|
||||
dalek_curve!("ristretto", Ristretto, RistrettoPoint, b"ristretto");
|
||||
#[cfg(any(test, feature = "ristretto"))]
|
||||
#[test]
|
||||
fn test_ristretto() {
|
||||
ff_group_tests::group::test_prime_group_bits::<_, RistrettoPoint>(&mut rand_core::OsRng);
|
||||
|
||||
assert_eq!(
|
||||
Ristretto::hash_to_F(
|
||||
b"FROST-RISTRETTO255-SHA512-v11nonce",
|
||||
&hex::decode(
|
||||
"\
|
||||
81800157bb554f299fe0b6bd658e4c4591d74168b5177bf55e8dceed59dc80c7\
|
||||
5c3430d391552f6e60ecdc093ff9f6f4488756aa6cebdbad75a768010b8f830e"
|
||||
)
|
||||
.unwrap()
|
||||
)
|
||||
.to_bytes()
|
||||
.as_ref(),
|
||||
&hex::decode("40f58e8df202b21c94f826e76e4647efdb0ea3ca7ae7e3689bc0cbe2e2f6660c").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
/// Ciphersuite for Ed25519, inspired by RFC-8032.
|
||||
///
|
||||
/// hash_to_F is implemented with a naive concatenation of the dst and data, allowing transposition
|
||||
/// between the two. This means `dst: b"abc", data: b"def"`, will produce the same scalar as
|
||||
/// `dst: "abcdef", data: b""`. Please use carefully, not letting dsts be substrings of each other.
|
||||
#[cfg(feature = "ed25519")]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct Ed25519;
|
||||
#[cfg(feature = "ed25519")]
|
||||
dalek_curve!("ed25519", Ed25519, EdwardsPoint, b"edwards25519");
|
||||
#[cfg(feature = "ed25519")]
|
||||
#[test]
|
||||
fn test_ed25519() {
|
||||
ff_group_tests::group::test_prime_group_bits::<_, EdwardsPoint>(&mut rand_core::OsRng);
|
||||
|
||||
// Ideally, a test vector from RFC-8032 (not FROST) would be here
|
||||
// Unfortunately, the IETF draft doesn't provide any vectors for the derived challenges
|
||||
assert_eq!(
|
||||
Ed25519::hash_to_F(
|
||||
b"FROST-ED25519-SHA512-v11nonce",
|
||||
&hex::decode(
|
||||
"\
|
||||
9d06a6381c7a4493929761a73692776772b274236fb5cfcc7d1b48ac3a9c249f\
|
||||
929dcc590407aae7d388761cddb0c0db6f5627aea8e217f4a033f2ec83d93509"
|
||||
)
|
||||
.unwrap()
|
||||
)
|
||||
.to_bytes()
|
||||
.as_ref(),
|
||||
&hex::decode("70652da3e8d7533a0e4b9e9104f01b48c396b5b553717784ed8d05c6a36b9609").unwrap()
|
||||
);
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use digest::{
|
||||
typenum::U114, core_api::BlockSizeUser, Update, Output, OutputSizeUser, FixedOutput,
|
||||
ExtendableOutput, XofReader, HashMarker, Digest,
|
||||
};
|
||||
use sha3::Shake256;
|
||||
|
||||
use group::Group;
|
||||
use minimal_ed448::{Scalar, Point};
|
||||
|
||||
use crate::Ciphersuite;
|
||||
|
||||
/// Shake256, fixed to a 114-byte output, as used by Ed448.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Shake256_114(Shake256);
|
||||
impl BlockSizeUser for Shake256_114 {
|
||||
type BlockSize = <Shake256 as BlockSizeUser>::BlockSize;
|
||||
fn block_size() -> usize {
|
||||
Shake256::block_size()
|
||||
}
|
||||
}
|
||||
impl OutputSizeUser for Shake256_114 {
|
||||
type OutputSize = U114;
|
||||
fn output_size() -> usize {
|
||||
114
|
||||
}
|
||||
}
|
||||
impl Update for Shake256_114 {
|
||||
fn update(&mut self, data: &[u8]) {
|
||||
self.0.update(data);
|
||||
}
|
||||
fn chain(mut self, data: impl AsRef<[u8]>) -> Self {
|
||||
Update::update(&mut self, data.as_ref());
|
||||
self
|
||||
}
|
||||
}
|
||||
impl FixedOutput for Shake256_114 {
|
||||
fn finalize_fixed(self) -> Output<Self> {
|
||||
let mut res = Default::default();
|
||||
FixedOutput::finalize_into(self, &mut res);
|
||||
res
|
||||
}
|
||||
fn finalize_into(self, out: &mut Output<Self>) {
|
||||
let mut reader = self.0.finalize_xof();
|
||||
reader.read(out);
|
||||
}
|
||||
}
|
||||
impl HashMarker for Shake256_114 {}
|
||||
|
||||
/// Ciphersuite for Ed448, inspired by RFC-8032. This is not recommended for usage.
|
||||
///
|
||||
/// hash_to_F is implemented with a naive concatenation of the dst and data, allowing transposition
|
||||
/// between the two. This means `dst: b"abc", data: b"def"`, will produce the same scalar as
|
||||
/// `dst: "abcdef", data: b""`. Please use carefully, not letting dsts be substrings of each other.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct Ed448;
|
||||
impl Ciphersuite for Ed448 {
|
||||
type F = Scalar;
|
||||
type G = Point;
|
||||
type H = Shake256_114;
|
||||
|
||||
const ID: &'static [u8] = b"ed448";
|
||||
|
||||
fn generator() -> Self::G {
|
||||
Point::generator()
|
||||
}
|
||||
|
||||
fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F {
|
||||
Scalar::wide_reduce(Self::H::digest([dst, data].concat()).as_ref().try_into().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ed448() {
|
||||
use ff::PrimeField;
|
||||
|
||||
ff_group_tests::group::test_prime_group_bits::<_, Point>(&mut rand_core::OsRng);
|
||||
|
||||
// Ideally, a test vector from RFC-8032 (not FROST) would be here
|
||||
// Unfortunately, the IETF draft doesn't provide any vectors for the derived challenges
|
||||
assert_eq!(
|
||||
Ed448::hash_to_F(
|
||||
b"FROST-ED448-SHAKE256-v11nonce",
|
||||
&hex::decode(
|
||||
"\
|
||||
89bf16040081ff2990336b200613787937ebe1f024b8cdff90eb6f1c741d91c1\
|
||||
4a2b2f5858a932ad3d3b18bd16e76ced3070d72fd79ae4402df201f5\
|
||||
25e754716a1bc1b87a502297f2a99d89ea054e0018eb55d39562fd01\
|
||||
00"
|
||||
)
|
||||
.unwrap()
|
||||
)
|
||||
.to_repr()
|
||||
.to_vec(),
|
||||
hex::decode(
|
||||
"\
|
||||
67a6f023e77361707c6e894c625e809e80f33fdb310810053ae29e28\
|
||||
e7011f3193b9020e73c183a98cc3a519160ed759376dd92c94831622\
|
||||
00"
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use sha2::Sha256;
|
||||
|
||||
use group::ff::PrimeField;
|
||||
|
||||
use elliptic_curve::{
|
||||
generic_array::GenericArray,
|
||||
bigint::{NonZero, CheckedAdd, Encoding, U384},
|
||||
hash2curve::{Expander, ExpandMsg, ExpandMsgXmd},
|
||||
};
|
||||
|
||||
use crate::Ciphersuite;
|
||||
|
||||
macro_rules! kp_curve {
|
||||
(
|
||||
$feature: literal,
|
||||
$lib: ident,
|
||||
|
||||
$Ciphersuite: ident,
|
||||
$ID: literal
|
||||
) => {
|
||||
impl Ciphersuite for $Ciphersuite {
|
||||
type F = $lib::Scalar;
|
||||
type G = $lib::ProjectivePoint;
|
||||
type H = Sha256;
|
||||
|
||||
const ID: &'static [u8] = $ID;
|
||||
|
||||
fn generator() -> Self::G {
|
||||
$lib::ProjectivePoint::GENERATOR
|
||||
}
|
||||
|
||||
fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F {
|
||||
// While one of these two libraries does support directly hashing to the Scalar field, the
|
||||
// other doesn't. While that's probably an oversight, this is a universally working method
|
||||
|
||||
// This method is from
|
||||
// https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-16.html
|
||||
// Specifically, Section 5
|
||||
|
||||
// While that draft, overall, is intended for hashing to curves, that necessitates
|
||||
// detailing how to hash to a finite field. The draft comments that its mechanism for
|
||||
// doing so, which it uses to derive field elements, is also applicable to the scalar field
|
||||
|
||||
// The hash_to_field function is intended to provide unbiased values
|
||||
// In order to do so, a wide reduction from an extra k bits is applied, minimizing bias to
|
||||
// 2^-k
|
||||
// k is intended to be the bits of security of the suite, which is 128 for secp256k1 and
|
||||
// P-256
|
||||
const K: usize = 128;
|
||||
|
||||
// L is the amount of bytes of material which should be used in the wide reduction
|
||||
// The 256 is for the bit-length of the primes, rounded up to the nearest byte threshold
|
||||
// This is a simplification of the formula from the end of section 5
|
||||
const L: usize = (256 + K) / 8; // 48
|
||||
|
||||
// In order to perform this reduction, we need to use 48-byte numbers
|
||||
// First, convert the modulus to a 48-byte number
|
||||
// This is done by getting -1 as bytes, parsing it into a U384, and then adding back one
|
||||
let mut modulus = [0; L];
|
||||
// The byte repr of scalars will be 32 big-endian bytes
|
||||
// Set the lower 32 bytes of our 48-byte array accordingly
|
||||
modulus[16 ..].copy_from_slice(&(Self::F::ZERO - Self::F::ONE).to_bytes());
|
||||
// Use a checked_add + unwrap since this addition cannot fail (being a 32-byte value with
|
||||
// 48-bytes of space)
|
||||
// While a non-panicking saturating_add/wrapping_add could be used, they'd likely be less
|
||||
// performant
|
||||
let modulus = U384::from_be_slice(&modulus).checked_add(&U384::ONE).unwrap();
|
||||
|
||||
// The defined P-256 and secp256k1 ciphersuites both use expand_message_xmd
|
||||
let mut wide = U384::from_be_bytes({
|
||||
let mut bytes = [0; 48];
|
||||
ExpandMsgXmd::<Sha256>::expand_message(&[msg], &[dst], 48)
|
||||
.unwrap()
|
||||
.fill_bytes(&mut bytes);
|
||||
bytes
|
||||
})
|
||||
.rem(&NonZero::new(modulus).unwrap())
|
||||
.to_be_bytes();
|
||||
|
||||
// Now that this has been reduced back to a 32-byte value, grab the lower 32-bytes
|
||||
let mut array = *GenericArray::from_slice(&wide[16 ..]);
|
||||
let res = $lib::Scalar::from_repr(array).unwrap();
|
||||
|
||||
// Zeroize the temp values we can due to the possibility hash_to_F is being used for nonces
|
||||
wide.zeroize();
|
||||
array.zeroize();
|
||||
res
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn test_oversize_dst<C: Ciphersuite>() {
|
||||
use sha2::Digest;
|
||||
|
||||
// The draft specifies DSTs >255 bytes should be hashed into a 32-byte DST
|
||||
let oversize_dst = [0x00; 256];
|
||||
let actual_dst = Sha256::digest([b"H2C-OVERSIZE-DST-".as_ref(), &oversize_dst].concat());
|
||||
// Test the hash_to_F function handles this
|
||||
// If it didn't, these would return different values
|
||||
assert_eq!(C::hash_to_F(&oversize_dst, &[]), C::hash_to_F(&actual_dst, &[]));
|
||||
}
|
||||
|
||||
/// Ciphersuite for Secp256k1.
|
||||
///
|
||||
/// hash_to_F is implemented via the IETF draft for hash to curve's hash_to_field (v16).
|
||||
#[cfg(feature = "secp256k1")]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct Secp256k1;
|
||||
#[cfg(feature = "secp256k1")]
|
||||
kp_curve!("secp256k1", k256, Secp256k1, b"secp256k1");
|
||||
#[cfg(feature = "secp256k1")]
|
||||
#[test]
|
||||
fn test_secp256k1() {
|
||||
ff_group_tests::group::test_prime_group_bits::<_, k256::ProjectivePoint>(&mut rand_core::OsRng);
|
||||
|
||||
// Ideally, a test vector from hash_to_field (not FROST) would be here
|
||||
// Unfortunately, the IETF draft only provides vectors for field elements, not scalars
|
||||
// Vectors have been requested in
|
||||
// https://github.com/cfrg/draft-irtf-cfrg-hash-to-curve/issues/343
|
||||
|
||||
assert_eq!(
|
||||
Secp256k1::hash_to_F(
|
||||
b"FROST-secp256k1-SHA256-v11nonce",
|
||||
&hex::decode(
|
||||
"\
|
||||
80cbea5e405d169999d8c4b30b755fedb26ab07ec8198cda4873ed8ce5e16773\
|
||||
08f89ffe80ac94dcb920c26f3f46140bfc7f95b493f8310f5fc1ea2b01f4254c"
|
||||
)
|
||||
.unwrap()
|
||||
)
|
||||
.to_repr()
|
||||
.iter()
|
||||
.copied()
|
||||
.collect::<Vec<_>>(),
|
||||
hex::decode("acc83278035223c1ba464e2d11bfacfc872b2b23e1041cf5f6130da21e4d8068").unwrap()
|
||||
);
|
||||
|
||||
test_oversize_dst::<Secp256k1>();
|
||||
}
|
||||
|
||||
/// Ciphersuite for P-256.
|
||||
///
|
||||
/// hash_to_F is implemented via the IETF draft for hash to curve's hash_to_field (v16).
|
||||
#[cfg(feature = "p256")]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct P256;
|
||||
#[cfg(feature = "p256")]
|
||||
kp_curve!("p256", p256, P256, b"P-256");
|
||||
#[cfg(feature = "p256")]
|
||||
#[test]
|
||||
fn test_p256() {
|
||||
ff_group_tests::group::test_prime_group_bits::<_, p256::ProjectivePoint>(&mut rand_core::OsRng);
|
||||
|
||||
assert_eq!(
|
||||
P256::hash_to_F(
|
||||
b"FROST-P256-SHA256-v11nonce",
|
||||
&hex::decode(
|
||||
"\
|
||||
f4e8cf80aec3f888d997900ac7e3e349944b5a6b47649fc32186d2f1238103c6\
|
||||
0c9c1a0fe806c184add50bbdcac913dda73e482daf95dcb9f35dbb0d8a9f7731"
|
||||
)
|
||||
.unwrap()
|
||||
)
|
||||
.to_repr()
|
||||
.iter()
|
||||
.copied()
|
||||
.collect::<Vec<_>>(),
|
||||
hex::decode("f871dfcf6bcd199342651adc361b92c941cb6a0d8c8c1a3b91d79e2c1bf3722d").unwrap()
|
||||
);
|
||||
|
||||
test_oversize_dst::<P256>();
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Ciphersuites for elliptic curves premised on ff/group.
|
||||
|
||||
This library, except for the not recommended Ed448 ciphersuite, was
|
||||
This library was
|
||||
[audited by Cypher Stack in March 2023](https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf),
|
||||
culminating in commit
|
||||
[669d2dbffc1dafb82a09d9419ea182667115df06](https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06).
|
||||
|
||||
@@ -26,25 +26,6 @@ use group::{
|
||||
#[cfg(any(feature = "alloc", feature = "std"))]
|
||||
use group::GroupEncoding;
|
||||
|
||||
#[cfg(feature = "dalek")]
|
||||
mod dalek;
|
||||
#[cfg(feature = "ristretto")]
|
||||
pub use dalek::Ristretto;
|
||||
#[cfg(feature = "ed25519")]
|
||||
pub use dalek::Ed25519;
|
||||
|
||||
#[cfg(feature = "kp256")]
|
||||
mod kp256;
|
||||
#[cfg(feature = "secp256k1")]
|
||||
pub use kp256::Secp256k1;
|
||||
#[cfg(feature = "p256")]
|
||||
pub use kp256::P256;
|
||||
|
||||
#[cfg(feature = "ed448")]
|
||||
mod ed448;
|
||||
#[cfg(feature = "ed448")]
|
||||
pub use ed448::*;
|
||||
|
||||
/// Unified trait defining a ciphersuite around an elliptic curve.
|
||||
pub trait Ciphersuite:
|
||||
'static + Send + Sync + Clone + Copy + PartialEq + Eq + Debug + Zeroize
|
||||
|
||||
Reference in New Issue
Block a user