SIGMA-I Key Exchange (#378)

* Move `KeGroup` to `KeyExchange::Group`

- Introduce `KeyExchange::Hash`, which separates the OPRF hash from the one used in `KeyExchange`.
- Remove `De/Serialize` requirement on key exchange messages and states, which forced a lot of where bounds on downstream users.
- Rename `KeGroup` to `Group`.
- Replace `D` generic for hash with `H`.

* Use `voprf::derive_key()` directly

* Implement SIGMA-I key exchange

* Improve `KeyExchange` for SIGMA-I and Ed25519

* Implement EdDSA

* Un-qualify some method calls

* SIGMA-I: only include client identity in client mac

* SIGMA-I: include server mac in client signature

* Expose key exchange types in `crate` & move modules

* Implement Ed25519ph

* Document `ed25519` crate feature

* Remove `ristretto255-voprf` crate feature

* Adjust CI crate feature testing

* Fix Rustdoc

* Remove unnecessary generic parameters from SIGMA-I

* Properly mark to-do's with TODO

* Assorted fixes

* SIGMA-I: include context in signature

* SIGMA-I: include identifiers in signature

* Merge `ServerLoginStart/FinishParameters`

* Re-export more necessary types

* More carefully expose types

* Add ECDSA test

* SIGMA-I: share context hashing

* De-duplicate client static public key storage

* Hide `KeyExchange` better

* Use the correct hash in the root documentation

* Bump `derive-where`

* Format documentation examples a bit further

* Add remote OPRF seed documentation

* Rename `deserialize_key_pair` to `deserialize_take_key_pair`

* Add more key tests

* Remove `SharedSecret` trait

* SIGMA-I refactor message API

* Share more implementation between 3DH and SIGMA-I

* Remove unnecessary zero scalar check for Curve25519

* Use correct hash in test

* Add some more TODOs

* Exclude `tests` folder from Cargo publishing

* Enable missing dependencies

* Use right crate for testing Ed25519

* Remove unnecessary `Sized` constraints

* Remove unnecessary `ecdsa` crate features

* Move signature de/serialization to trait methods

* Nit: move import to appropriate location

* Add warning to SIGMA-I
This commit is contained in:
daxpedda
2025-05-19 13:56:25 -07:00
committed by GitHub
parent 58b4d746c0
commit bebd2c605b
37 changed files with 9402 additions and 3375 deletions
+53 -44
View File
@@ -8,26 +8,25 @@
//! Key Exchange group implementation for Curve25519
pub use curve25519_dalek;
use curve25519_dalek::montgomery::MontgomeryPoint;
use curve25519_dalek::scalar;
use curve25519_dalek::traits::Identity;
use digest::core_api::BlockSizeUser;
use digest::{FixedOutput, HashMarker, OutputSizeUser};
use generic_array::typenum::{IsLess, IsLessOrEqual, U256, U32};
use generic_array::typenum::U32;
use generic_array::GenericArray;
use rand::{CryptoRng, RngCore};
use subtle::ConstantTimeEq;
use zeroize::Zeroize;
use super::KeGroup;
use super::Group;
use crate::errors::{InternalError, ProtocolError};
use crate::key_exchange::tripledh::DiffieHellman;
use crate::key_exchange::shared::DiffieHellman;
use crate::serialization::SliceExt;
/// Implementation for Curve25519.
pub struct Curve25519;
/// The implementation of such a subgroup for Curve25519
impl KeGroup for Curve25519 {
impl Group for Curve25519 {
type Pk = MontgomeryPoint;
type PkLen = U32;
type Sk = Scalar;
@@ -37,50 +36,28 @@ impl KeGroup for Curve25519 {
pk.to_bytes().into()
}
fn deserialize_pk(bytes: &[u8]) -> Result<Self::Pk, ProtocolError> {
fn deserialize_take_pk(bytes: &mut &[u8]) -> Result<Self::Pk, ProtocolError> {
bytes
.try_into()
.take_array::<U32>("public key")
.ok()
.map(MontgomeryPoint)
.map(|array| MontgomeryPoint(array.into()))
.filter(|pk| pk != &MontgomeryPoint::identity())
.ok_or(ProtocolError::SerializationError)
}
fn random_sk<R: RngCore + CryptoRng>(rng: &mut R) -> Self::Sk {
loop {
// Sample 32 random bytes and then clamp, as described in https://cr.yp.to/ecdh.html
let mut scalar_bytes = [0u8; 32];
rng.fill_bytes(&mut scalar_bytes);
let scalar = scalar::clamp_integer(scalar_bytes);
// Sample 32 random bytes and then clamp, as described in https://cr.yp.to/ecdh.html
let mut scalar_bytes = [0u8; 32];
rng.fill_bytes(&mut scalar_bytes);
let scalar = scalar::clamp_integer(scalar_bytes);
if scalar != curve25519_dalek::Scalar::ZERO.to_bytes() {
break Scalar(scalar);
}
}
Scalar(scalar)
}
fn hash_to_scalar<'a, H>(_input: &[&[u8]], _dst: &[&[u8]]) -> Result<Self::Sk, InternalError>
where
H: BlockSizeUser + Default + FixedOutput + HashMarker,
H::OutputSize: IsLess<U256> + IsLessOrEqual<H::BlockSize>,
{
unimplemented!()
}
fn derive_auth_keypair<CS: voprf::CipherSuite>(
seed: GenericArray<u8, Self::SkLen>,
) -> Result<Self::Sk, InternalError>
where
<CS::Hash as OutputSizeUser>::OutputSize:
IsLess<U256> + IsLessOrEqual<<CS::Hash as BlockSizeUser>::BlockSize>,
{
fn derive_scalar(seed: GenericArray<u8, Self::SkLen>) -> Result<Self::Sk, InternalError> {
Ok(Scalar(scalar::clamp_integer(seed.into())))
}
fn is_zero_scalar(scalar: Self::Sk) -> subtle::Choice {
scalar.0.ct_eq(&curve25519_dalek::Scalar::ZERO.to_bytes())
}
fn public_key(sk: Self::Sk) -> Self::Pk {
MontgomeryPoint::mul_base_clamped(sk.0)
}
@@ -89,22 +66,21 @@ impl KeGroup for Curve25519 {
sk.0.into()
}
fn deserialize_sk(bytes: &[u8]) -> Result<Self::Sk, ProtocolError> {
fn deserialize_take_sk(bytes: &mut &[u8]) -> Result<Self::Sk, ProtocolError> {
bytes
.try_into()
.take_array::<U32>("secret key")
.ok()
.and_then(|bytes| {
let scalar = scalar::clamp_integer(bytes);
(scalar == bytes).then_some(scalar)
let scalar = scalar::clamp_integer(bytes.into());
(scalar == *bytes).then_some(scalar)
})
.filter(|scalar| scalar != &curve25519_dalek::Scalar::ZERO.to_bytes())
.map(Scalar)
.ok_or(ProtocolError::SerializationError)
}
}
/// Curve25519 scalar.
#[derive(Clone, Copy, Zeroize)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Zeroize)]
pub struct Scalar([u8; 32]);
impl DiffieHellman<Curve25519> for Scalar {
@@ -112,3 +88,36 @@ impl DiffieHellman<Curve25519> for Scalar {
Curve25519::serialize_pk(pk.mul_clamped(self.0))
}
}
//////////////////////////
// Test Implementations //
//===================== //
//////////////////////////
#[cfg(test)]
use crate::serialization::AssertZeroized;
#[cfg(test)]
impl AssertZeroized for MontgomeryPoint {
fn assert_zeroized(&self) {
assert_eq!(*self, MontgomeryPoint::default());
}
}
#[cfg(test)]
impl AssertZeroized for Scalar {
fn assert_zeroized(&self) {
assert_eq!(*self, Scalar(<_>::default()));
}
}
#[test]
fn non_zero_scalar() {
use std::vec;
use crate::tests::mock_rng::CycleRng;
let mut rng = CycleRng::new(vec![0]);
let sk = Curve25519::random_sk(&mut rng);
assert_ne!(sk.0, curve25519_dalek::Scalar::ZERO.to_bytes());
}
+459
View File
@@ -0,0 +1,459 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// This source code is dual-licensed under either the MIT license found in the
// LICENSE-MIT file in the root directory of this source tree or the Apache
// License, Version 2.0 found in the LICENSE-APACHE file in the root directory
// of this source tree. You may select, at your option, one of the above-listed
// licenses.
//! Key Exchange group implementation for Ed25519
use core::iter;
use curve25519_dalek::edwards::CompressedEdwardsY;
use curve25519_dalek::traits::IsIdentity;
use curve25519_dalek::{EdwardsPoint, Scalar};
use digest::Digest;
pub use ed25519_dalek;
use ed25519_dalek::hazmat::ExpandedSecretKey;
use ed25519_dalek::{SecretKey, Sha512};
use generic_array::sequence::Concat;
use generic_array::typenum::{U32, U64};
use generic_array::GenericArray;
use rand::{CryptoRng, RngCore};
use zeroize::Zeroize;
use super::Group;
use crate::ciphersuite::CipherSuite;
use crate::errors::{InternalError, ProtocolError};
use crate::key_exchange::sigma_i::hash_eddsa::implementation::HashEddsaImpl;
use crate::key_exchange::sigma_i::pure_eddsa::implementation::PureEddsaImpl;
pub use crate::key_exchange::sigma_i::shared::PreHash;
use crate::key_exchange::sigma_i::{CachedMessage, Message, MessageBuilder};
use crate::serialization::{SliceExt, UpdateExt};
/// Implementation for Ed25519.
pub struct Ed25519;
impl Group for Ed25519 {
type Pk = VerifyingKey;
type PkLen = U32;
type Sk = SigningKey;
type SkLen = U32;
fn serialize_pk(pk: Self::Pk) -> GenericArray<u8, Self::PkLen> {
pk.compressed.0.into()
}
fn deserialize_take_pk(bytes: &mut &[u8]) -> Result<Self::Pk, ProtocolError> {
let compressed = bytes
.take_array("public key")
.map(|bytes| CompressedEdwardsY(bytes.into()))?;
if let Some(point) = compressed.decompress().filter(|point| !point.is_identity()) {
Ok(VerifyingKey { point, compressed })
} else {
Err(ProtocolError::SerializationError)
}
}
fn random_sk<R: RngCore + CryptoRng>(rng: &mut R) -> Self::Sk {
let mut sk = <[u8; 32]>::default();
rng.fill_bytes(&mut sk);
SigningKey::from_bytes(sk)
}
fn derive_scalar(seed: GenericArray<u8, Self::SkLen>) -> Result<Self::Sk, InternalError> {
Ok(SigningKey::from_bytes(seed.into()))
}
fn public_key(sk: Self::Sk) -> Self::Pk {
sk.verifying_key
}
fn serialize_sk(sk: Self::Sk) -> GenericArray<u8, Self::SkLen> {
sk.sk.into()
}
fn deserialize_take_sk(bytes: &mut &[u8]) -> Result<Self::Sk, ProtocolError> {
Ok(SigningKey::from_bytes(
bytes.take_array("secret key")?.into(),
))
}
}
/// Ed25519 verifying key.
// `ed25519_dalek::VerifyingKey` doesn't implement `Zeroize`.
// TODO: remove after https://github.com/dalek-cryptography/curve25519-dalek/pull/747.
// Required for manual implementation of EdDSA.
// TODO: remove after https://github.com/dalek-cryptography/curve25519-dalek/pull/556.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Zeroize)]
pub struct VerifyingKey {
point: EdwardsPoint,
compressed: CompressedEdwardsY,
}
/// Ed25519 siging key.
// We store the `ExpandedSecret` in memory to avoid computing it on demand and then discarding it
// again.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Zeroize)]
pub struct SigningKey {
// `ed25519_dalek::SigningKey` doesn't implement `Zeroize`. See
// https://github.com/dalek-cryptography/curve25519-dalek/pull/747
// Required for manual implementation of EdDSA.
// TODO: remove after https://github.com/dalek-cryptography/curve25519-dalek/pull/556.
sk: SecretKey,
verifying_key: VerifyingKey,
// `ed25519_dalek::ExpandedSecret` doesn't implement traits we need. See
// TODO: remove after https://github.com/dalek-cryptography/curve25519-dalek/pull/748 and
// https://github.com/dalek-cryptography/curve25519-dalek/pull/747.
scalar: Scalar,
hash_prefix: [u8; 32],
}
impl SigningKey {
fn from_bytes(sk: [u8; 32]) -> Self {
let ExpandedSecretKey {
scalar,
hash_prefix,
} = ExpandedSecretKey::from(&sk);
let point = EdwardsPoint::mul_base(&scalar);
let verifying_key = VerifyingKey {
point,
compressed: point.compress(),
};
SigningKey {
sk,
verifying_key,
scalar,
hash_prefix,
}
}
}
impl PureEddsaImpl for Ed25519 {
type Signature = Signature;
type SignatureLen = U64;
fn sign<CS: CipherSuite, KE: Group>(
sk: &Self::Sk,
message: &Message<CS, KE>,
) -> (Self::Signature, CachedMessage<CS, KE>) {
(sign(sk, false, message.sign_message()), message.to_cached())
}
/// Validates that the signature was created by signing the given message
/// with the corresponding private key.
fn verify<CS: CipherSuite, KE: Group>(
pk: &Self::Pk,
message_builder: MessageBuilder<'_, CS>,
state: CachedMessage<CS, KE>,
signature: &Self::Signature,
) -> Result<(), ProtocolError> {
verify(
pk,
false,
message_builder.build::<KE>(state).verify_message(),
signature,
)
}
fn deserialize_take_signature(bytes: &mut &[u8]) -> Result<Self::Signature, ProtocolError> {
Signature::deserialize_take(bytes)
}
fn serialize_signature(signature: &Self::Signature) -> GenericArray<u8, Self::SignatureLen> {
signature.serialize()
}
}
impl HashEddsaImpl for Ed25519 {
type Signature = Signature;
type SignatureLen = U64;
type VerifyState<CS: CipherSuite, KE: Group> = PreHash<Sha512>;
fn sign<CS: CipherSuite, KE: Group>(
sk: &Self::Sk,
message: &Message<CS, KE>,
) -> (Self::Signature, Self::VerifyState<CS, KE>) {
let hash = message.hash::<Sha512>();
(
sign(sk, true, iter::once(hash.sign.finalize().as_slice())),
PreHash(hash.verify.finalize()),
)
}
/// Validates that the signature was created by signing the given message
/// with the corresponding private key.
fn verify<CS: CipherSuite, KE: Group>(
pk: &Self::Pk,
state: Self::VerifyState<CS, KE>,
signature: &Self::Signature,
) -> Result<(), ProtocolError> {
verify(pk, true, iter::once(state.0.as_slice()), signature)
}
fn deserialize_take_signature(bytes: &mut &[u8]) -> Result<Self::Signature, ProtocolError> {
Signature::deserialize_take(bytes)
}
fn serialize_signature(signature: &Self::Signature) -> GenericArray<u8, Self::SignatureLen> {
signature.serialize()
}
}
// This contains a manual implementation of EdDSA because `ed25519-dalek`
// doesn't support message streaming. See
// TODO: remove after https://github.com/dalek-cryptography/curve25519-dalek/pull/556.
fn sign<'a>(
sk: &SigningKey,
pre_hash: bool,
message: impl Clone + Iterator<Item = &'a [u8]>,
) -> Signature {
let mut h = Sha512::new();
if pre_hash {
h.update(b"SigEd25519 no Ed25519 collisions");
h.update([1]); // Ed25519ph
h.update([0]);
}
h.update(sk.hash_prefix);
h.update_iter(message.clone());
let r = Scalar::from_hash(h);
#[allow(non_snake_case)]
let R = EdwardsPoint::mul_base(&r).compress();
h = Sha512::new();
if pre_hash {
h.update(b"SigEd25519 no Ed25519 collisions");
h.update([1]); // Ed25519ph
h.update([0]);
}
h.update(R.as_bytes());
h.update(sk.verifying_key.compressed.0);
h.update_iter(message);
let k = Scalar::from_hash(h);
let s: Scalar = (k * sk.scalar) + r;
Signature { R, s }
}
fn verify<'a>(
pk: &VerifyingKey,
pre_hash: bool,
message: impl Iterator<Item = &'a [u8]>,
signature: &Signature,
) -> Result<(), ProtocolError> {
let mut h = Sha512::new();
if pre_hash {
h.update(b"SigEd25519 no Ed25519 collisions");
h.update([1]); // Ed25519ph
h.update([0]);
}
h.update(signature.R.as_bytes());
h.update(pk.compressed.as_bytes());
h.update_iter(message);
let k = Scalar::from_hash(h);
#[allow(non_snake_case)]
let minus_A: EdwardsPoint = -pk.point;
#[allow(non_snake_case)]
let expected_R =
EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &(minus_A), &signature.s).compress();
if expected_R == signature.R {
Ok(())
} else {
Err(ProtocolError::InvalidLoginError)
}
}
/// Ed25519 Signature.
// `ed25519_dalek::Signature` doesn't implement validation with Serde de/serialization.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[allow(non_snake_case)]
pub struct Signature {
R: CompressedEdwardsY,
s: Scalar,
}
impl Signature {
/// Expects the `R` and `s` components of a Ed25519 signature with no added
/// framing.
pub fn from_slice(mut bytes: &[u8]) -> Result<Self, ProtocolError> {
Self::deserialize_take(&mut bytes)
}
fn deserialize_take(bytes: &mut &[u8]) -> Result<Self, ProtocolError> {
#[allow(non_snake_case)]
let R = CompressedEdwardsY(bytes.take_array("signature R")?.into());
let s = Scalar::from_canonical_bytes(bytes.take_array("signature s")?.into())
.into_option()
.ok_or(ProtocolError::SerializationError)?;
Ok(Self { R, s })
}
fn serialize(&self) -> GenericArray<u8, U64> {
GenericArray::from(self.R.0).concat(GenericArray::from(self.s.to_bytes()))
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Signature {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
Signature::deserialize_take(
&mut (GenericArray::<_, U64>::deserialize(deserializer)?.as_slice()),
)
.map_err(D::Error::custom)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Signature {
fn serialize<SK>(&self, serializer: SK) -> Result<SK::Ok, SK::Error>
where
SK: serde::Serializer,
{
self.serialize().serialize(serializer)
}
}
impl Zeroize for Signature {
fn zeroize(&mut self) {
self.R.0 = [0; 32];
self.s = Scalar::default();
}
}
//////////////////////////
// Test Implementations //
//===================== //
//////////////////////////
#[cfg(test)]
use crate::serialization::AssertZeroized;
#[cfg(test)]
impl AssertZeroized for VerifyingKey {
fn assert_zeroized(&self) {
use curve25519_dalek::traits::Identity;
let Self { point, compressed } = self;
assert_eq!(point, &EdwardsPoint::identity());
assert_eq!(compressed, &EdwardsPoint::identity().compress());
}
}
#[cfg(test)]
impl AssertZeroized for SigningKey {
fn assert_zeroized(&self) {
let Self {
sk,
verifying_key,
scalar,
hash_prefix,
} = self;
verifying_key.assert_zeroized();
for byte in sk.iter().chain(scalar.to_bytes().iter()).chain(hash_prefix) {
assert_eq!(byte, &0);
}
}
}
#[cfg(test)]
mod test {
use std::iter;
use ed25519_dalek::{Signer, SigningKey, Verifier, VerifyingKey};
use rand::rngs::OsRng;
use super::*;
#[test]
fn pure_eddsa() {
let mut message = [0; 1024];
OsRng.fill_bytes(&mut message);
let mut sk = SecretKey::default();
OsRng.fill_bytes(&mut sk);
let signing_key = SigningKey::from_bytes(&sk);
let signature = signing_key.sign(&message);
let custom_sk = Ed25519::deserialize_take_sk(&mut sk.as_slice()).unwrap();
let custom_signature = sign(&custom_sk, false, iter::once(message.as_slice()));
assert_eq!(
signature.to_bytes(),
custom_signature.serialize().as_slice()
);
let verifying_key = VerifyingKey::from(&signing_key);
verifying_key.verify(&message, &signature).unwrap();
let custom_pk = Ed25519::public_key(custom_sk);
verify(
&custom_pk,
false,
iter::once(message.as_slice()),
&custom_signature,
)
.unwrap();
}
#[test]
fn hash_eddsa() {
let mut message = [0; 1024];
OsRng.fill_bytes(&mut message);
let message = Sha512::new_with_prefix(message);
let pre_hash = message.clone().finalize();
let mut sk = SecretKey::default();
OsRng.fill_bytes(&mut sk);
let signing_key = SigningKey::from_bytes(&sk);
let signature = signing_key.sign_prehashed(message.clone(), None).unwrap();
let custom_sk = Ed25519::deserialize_take_sk(&mut sk.as_slice()).unwrap();
let custom_signature = sign(&custom_sk, true, iter::once(pre_hash.as_slice()));
assert_eq!(
signature.to_bytes(),
custom_signature.serialize().as_slice()
);
let verifying_key = VerifyingKey::from(&signing_key);
verifying_key
.verify_prehashed(message, None, &signature)
.unwrap();
let custom_pk = Ed25519::public_key(custom_sk);
verify(
&custom_pk,
true,
iter::once(pre_hash.as_slice()),
&custom_signature,
)
.unwrap();
}
}
+108 -52
View File
@@ -6,101 +6,157 @@
// of this source tree. You may select, at your option, one of the above-listed
// licenses.
use digest::core_api::BlockSizeUser;
use digest::{FixedOutput, HashMarker};
use elliptic_curve::group::cofactor::CofactorGroup;
use elliptic_curve::hash2curve::{ExpandMsgXmd, FromOkm, GroupDigest};
use elliptic_curve::sec1::{FromEncodedPoint, ModulusSize, ToEncodedPoint};
//! Implementation for EC curves via [`elliptic_curve`] traits.
use core::fmt::{self, Debug, Formatter};
use derive_where::derive_where;
use elliptic_curve::group::GroupEncoding;
use elliptic_curve::ops::MulByGenerator;
use elliptic_curve::sec1::{ModulusSize, ToEncodedPoint};
use elliptic_curve::{
AffinePoint, Field, FieldBytesSize, Group, ProjectivePoint, PublicKey, Scalar, SecretKey,
point, CurveArithmetic, FieldBytesSize, Group as _, NonZeroScalar, ProjectivePoint, Scalar,
SecretKey,
};
use generic_array::typenum::{IsLess, IsLessOrEqual, U256};
use generic_array::GenericArray;
use rand::{CryptoRng, RngCore};
use voprf::Mode;
use zeroize::Zeroize;
use super::KeGroup;
use super::{Group, STR_OPAQUE_DERIVE_AUTH_KEY_PAIR};
use crate::errors::{InternalError, ProtocolError};
use crate::key_exchange::tripledh::DiffieHellman;
use crate::key_exchange::shared::DiffieHellman;
use crate::serialization::SliceExt;
impl<G> KeGroup for G
impl<G> Group for G
where
G: GroupDigest,
Self: CurveArithmetic + voprf::CipherSuite<Group = Self> + voprf::Group<Scalar = Scalar<Self>>,
FieldBytesSize<Self>: ModulusSize,
AffinePoint<Self>: FromEncodedPoint<Self> + ToEncodedPoint<Self>,
ProjectivePoint<Self>: CofactorGroup + ToEncodedPoint<Self>,
Scalar<Self>: FromOkm,
ProjectivePoint<Self>: GroupEncoding<
Repr = GenericArray<u8, <FieldBytesSize<Self> as ModulusSize>::CompressedPointSize>,
> + ToEncodedPoint<Self>,
{
type Pk = ProjectivePoint<Self>;
type Pk = NonIdentity<Self>;
type PkLen = <FieldBytesSize<Self> as ModulusSize>::CompressedPointSize;
type Sk = Scalar<Self>;
type Sk = NonZeroScalar<Self>;
type SkLen = FieldBytesSize<Self>;
fn serialize_pk(pk: Self::Pk) -> GenericArray<u8, Self::PkLen> {
GenericArray::clone_from_slice(pk.to_encoded_point(true).as_bytes())
GenericArray::clone_from_slice(pk.0.to_encoded_point(true).as_bytes())
}
fn deserialize_pk(bytes: &[u8]) -> Result<Self::Pk, ProtocolError> {
PublicKey::<Self>::from_sec1_bytes(bytes)
.map(|public_key| public_key.to_projective())
.map_err(|_| ProtocolError::SerializationError)
fn deserialize_take_pk(bytes: &mut &[u8]) -> Result<Self::Pk, ProtocolError> {
point::NonIdentity::<ProjectivePoint<Self>>::from_bytes(&bytes.take_array("public key")?)
.into_option()
.map(NonIdentity)
.ok_or(ProtocolError::SerializationError)
}
fn random_sk<R: RngCore + CryptoRng>(rng: &mut R) -> Self::Sk {
*SecretKey::<Self>::random(rng).to_nonzero_scalar()
SecretKey::<Self>::random(rng).to_nonzero_scalar()
}
// Implements the `HashToScalar()` function from
// <https://www.ietf.org/archive/id/draft-irtf-cfrg-voprf-19.html#section-4>
fn hash_to_scalar<H>(input: &[&[u8]], dst: &[&[u8]]) -> Result<Self::Sk, InternalError>
where
H: BlockSizeUser + Default + FixedOutput + HashMarker,
H::OutputSize: IsLess<U256> + IsLessOrEqual<H::BlockSize>,
{
Self::hash_to_scalar::<ExpandMsgXmd<H>>(input, dst)
.map_err(|_| InternalError::HashToScalar)
.and_then(|scalar| {
if bool::from(scalar.is_zero()) {
Err(InternalError::HashToScalar)
} else {
Ok(scalar)
}
fn derive_scalar(seed: GenericArray<u8, Self::SkLen>) -> Result<Self::Sk, InternalError> {
voprf::derive_key::<Self>(&seed, &STR_OPAQUE_DERIVE_AUTH_KEY_PAIR, Mode::Oprf)
.map(|scalar| {
NonZeroScalar::new(scalar).expect("`voprf::derive_key()` returned a zero scalar")
})
.map_err(InternalError::from)
}
fn public_key(sk: Self::Sk) -> Self::Pk {
ProjectivePoint::<Self>::generator() * sk
}
fn is_zero_scalar(scalar: Self::Sk) -> subtle::Choice {
scalar.is_zero()
// Non-panicking version in https://github.com/RustCrypto/traits/pull/1833.
NonIdentity(
point::NonIdentity::new(ProjectivePoint::<Self>::mul_by_generator(&*sk))
.expect("multiplying with a non-zero scalar can never yield the identity element"),
)
}
fn serialize_sk(sk: Self::Sk) -> GenericArray<u8, Self::SkLen> {
sk.into()
}
fn deserialize_sk(bytes: &[u8]) -> Result<Self::Sk, ProtocolError> {
SecretKey::<Self>::from_slice(bytes)
.map(|secret_key| *secret_key.to_nonzero_scalar())
fn deserialize_take_sk(bytes: &mut &[u8]) -> Result<Self::Sk, ProtocolError> {
SecretKey::<Self>::from_bytes(&bytes.take_array("secret key")?)
.map(|secret_key| secret_key.to_nonzero_scalar())
.map_err(|_| ProtocolError::SerializationError)
}
}
impl<G> DiffieHellman<G> for Scalar<G>
/// Wrapper around [`NonIdentity`](point::NonIdentity) to implement [`Zeroize`].
// TODO: remove after https://github.com/RustCrypto/traits/pull/1832.
#[derive_where(Clone, Copy)]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize, serde::Serialize),
serde(bound(
deserialize = "point::NonIdentity<ProjectivePoint<G>>: serde::Deserialize<'de>",
serialize = "point::NonIdentity<ProjectivePoint<G>>: serde::Serialize"
))
)]
pub struct NonIdentity<G: CurveArithmetic>(pub point::NonIdentity<ProjectivePoint<G>>);
impl<G: CurveArithmetic> Debug for NonIdentity<G> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_tuple("NonIdentity")
.field(&self.0.to_point())
.finish()
}
}
impl<G: CurveArithmetic> PartialEq for NonIdentity<G> {
fn eq(&self, other: &Self) -> bool {
self.0.to_point().eq(&other.0.to_point())
}
}
impl<G: CurveArithmetic> Eq for NonIdentity<G> {}
impl<G: CurveArithmetic> Zeroize for NonIdentity<G> {
fn zeroize(&mut self) {
self.0 = point::NonIdentity::new(ProjectivePoint::<G>::generator()).unwrap();
}
}
impl<G> DiffieHellman<G> for NonZeroScalar<G>
where
G: GroupDigest,
G: CurveArithmetic + voprf::CipherSuite<Group = G> + voprf::Group<Scalar = Scalar<G>>,
FieldBytesSize<G>: ModulusSize,
AffinePoint<G>: FromEncodedPoint<G> + ToEncodedPoint<G>,
ProjectivePoint<G>: CofactorGroup + ToEncodedPoint<G>,
Scalar<G>: FromOkm,
ProjectivePoint<G>: GroupEncoding<
Repr = GenericArray<u8, <FieldBytesSize<G> as ModulusSize>::CompressedPointSize>,
> + ToEncodedPoint<G>,
{
fn diffie_hellman(
self,
pk: ProjectivePoint<G>,
pk: NonIdentity<G>,
) -> GenericArray<u8, <FieldBytesSize<G> as ModulusSize>::CompressedPointSize> {
GenericArray::clone_from_slice((pk * self).to_encoded_point(true).as_bytes())
GenericArray::clone_from_slice((pk.0 * self).to_encoded_point(true).as_bytes())
}
}
//////////////////////////
// Test Implementations //
//===================== //
//////////////////////////
#[cfg(test)]
use crate::serialization::AssertZeroized;
#[cfg(test)]
impl<G: CurveArithmetic> AssertZeroized for NonIdentity<G> {
fn assert_zeroized(&self) {
assert_eq!(self.0.to_point(), ProjectivePoint::<G>::generator());
}
}
#[cfg(test)]
impl<G: CurveArithmetic> AssertZeroized for NonZeroScalar<G> {
fn assert_zeroized(&self) {
use elliptic_curve::Field;
assert_eq!(**self, Scalar::<G>::ONE);
}
}
+13 -75
View File
@@ -6,18 +6,16 @@
// of this source tree. You may select, at your option, one of the above-listed
// licenses.
//! Includes the [`KeGroup`] trait and definitions for the key exchange groups
//! Includes the [`Group`] trait and definitions for the key exchange groups
#[cfg(feature = "curve25519")]
pub mod curve25519;
mod elliptic_curve;
#[cfg(feature = "ed25519")]
pub mod ed25519;
pub mod elliptic_curve;
#[cfg(feature = "ristretto255")]
pub mod ristretto255;
use digest::core_api::BlockSizeUser;
use digest::{FixedOutput, HashMarker, OutputSizeUser};
use generic_array::sequence::Concat;
use generic_array::typenum::{IsLess, IsLessOrEqual, U256};
use generic_array::{ArrayLength, GenericArray};
use rand::{CryptoRng, RngCore};
use zeroize::Zeroize;
@@ -27,7 +25,7 @@ use crate::errors::{InternalError, ProtocolError};
const STR_OPAQUE_DERIVE_AUTH_KEY_PAIR: [u8; 33] = *b"OPAQUE-DeriveDiffieHellmanKeyPair";
/// A group representation for use in the key exchange
pub trait KeGroup {
pub trait Group {
/// Public key
type Pk: Copy + Zeroize;
/// Length of the public key
@@ -41,65 +39,15 @@ pub trait KeGroup {
fn serialize_pk(pk: Self::Pk) -> GenericArray<u8, Self::PkLen>;
/// Return a public key from its fixed-length bytes representation
fn deserialize_pk(bytes: &[u8]) -> Result<Self::Pk, ProtocolError>;
///
/// The deserialized bytes must be taken from `bytes`.
fn deserialize_take_pk(bytes: &mut &[u8]) -> Result<Self::Pk, ProtocolError>;
/// Generate a random secret key
fn random_sk<R: RngCore + CryptoRng>(rng: &mut R) -> Self::Sk;
/// Hashes a slice of pseudo-random bytes to a scalar
///
/// # Errors
/// [`InternalError::HashToScalar`] if the `input` is empty or longer then
/// [`u16::MAX`].
fn hash_to_scalar<H>(input: &[&[u8]], dst: &[&[u8]]) -> Result<Self::Sk, InternalError>
where
H: BlockSizeUser + Default + FixedOutput + HashMarker,
H::OutputSize: IsLess<U256> + IsLessOrEqual<H::BlockSize>;
/// Corresponds to the `DeriveAuthKeyPair()` function defined in
/// <https://www.ietf.org/archive/id/draft-irtf-cfrg-opaque-08.html#section-6.4.2>
///
/// Note that we cannot call the voprf crate directly since we need to
/// ensure that the [`KeGroup`] is used for the
/// [`hash_to_scalar`](Self::hash_to_scalar) operation (as opposed to
/// the [`OprfGroup`](voprf::Group)).
fn derive_auth_keypair<CS: voprf::CipherSuite>(
seed: GenericArray<u8, Self::SkLen>,
) -> Result<Self::Sk, InternalError>
where
<CS::Hash as OutputSizeUser>::OutputSize:
IsLess<U256> + IsLessOrEqual<<CS::Hash as BlockSizeUser>::BlockSize>,
{
let info = &STR_OPAQUE_DERIVE_AUTH_KEY_PAIR;
let dst_1 = GenericArray::from(STR_DERIVE_KEYPAIR)
.concat(STR_OPRF.into())
.concat([voprf::Mode::Oprf.to_u8()].into())
.concat([b'-'].into());
let dst_2 = CS::ID.as_bytes();
let info_len = i2osp_2(info.len())
.map_err(|_| InternalError::OprfError(voprf::Error::DeriveKeyPair))?;
for counter in 0_u8..=u8::MAX {
// deriveInput = seed || I2OSP(len(info), 2) || info
// skS = G.HashToScalar(deriveInput || I2OSP(counter, 1), DST = "DeriveKeyPair"
// || contextString)
let sk_s = Self::hash_to_scalar::<CS::Hash>(
&[&seed, &info_len, info, &counter.to_be_bytes()],
&[&dst_1, dst_2],
)
.map_err(|_| InternalError::OprfError(voprf::Error::DeriveKeyPair))?;
if !bool::from(Self::is_zero_scalar(sk_s)) {
return Ok(sk_s);
}
}
Err(InternalError::OprfError(voprf::Error::DeriveKeyPair))
}
/// Returns `true` if the scalar is zero.
fn is_zero_scalar(scalar: Self::Sk) -> subtle::Choice;
/// Deterministically derive a [`Self::Sk`] from `seed`.
fn derive_scalar(seed: GenericArray<u8, Self::SkLen>) -> Result<Self::Sk, InternalError>;
/// Return a public key from its secret key
fn public_key(sk: Self::Sk) -> Self::Pk;
@@ -108,17 +56,7 @@ pub trait KeGroup {
fn serialize_sk(sk: Self::Sk) -> GenericArray<u8, Self::SkLen>;
/// Return a public key from its fixed-length bytes representation
fn deserialize_sk(bytes: &[u8]) -> Result<Self::Sk, ProtocolError>;
}
// Helper functions used to compute DeriveAuthKeyPair() (taken from the voprf
// crate)
const STR_OPRF: [u8; 7] = *b"OPRFV1-";
const STR_DERIVE_KEYPAIR: [u8; 13] = *b"DeriveKeyPair";
fn i2osp_2(input: usize) -> Result<[u8; 2], InternalError> {
u16::try_from(input)
.map(|input| input.to_be_bytes())
.map_err(|_| InternalError::OprfInternalError(voprf::InternalError::I2osp))
///
/// The deserialized bytes must be taken from `bytes`.
fn deserialize_take_sk(bytes: &mut &[u8]) -> Result<Self::Sk, ProtocolError>;
}
+53 -55
View File
@@ -8,6 +8,7 @@
//! Key Exchange group implementation for ristretto255
pub use curve25519_dalek;
use curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT;
use curve25519_dalek::ristretto::{CompressedRistretto, RistrettoPoint};
use curve25519_dalek::scalar::Scalar;
@@ -17,19 +18,19 @@ use digest::{FixedOutput, HashMarker};
use generic_array::typenum::{IsLess, IsLessOrEqual, U256, U32};
use generic_array::GenericArray;
use rand::{CryptoRng, RngCore};
use subtle::ConstantTimeEq;
use voprf::Group;
use voprf::Mode;
use super::KeGroup;
use super::{Group, STR_OPAQUE_DERIVE_AUTH_KEY_PAIR};
use crate::errors::{InternalError, ProtocolError};
use crate::key_exchange::tripledh::DiffieHellman;
use crate::key_exchange::shared::DiffieHellman;
use crate::serialization::SliceExt;
/// Implementation for Ristretto255.
// This is necessary because Rust lacks specialization, otherwise we could
// implement `KeGroup` for `voprf::Ristretto255`.
pub struct Ristretto255;
impl KeGroup for Ristretto255 {
impl Group for Ristretto255 {
type Pk = RistrettoPoint;
type PkLen = U32;
type Sk = Scalar;
@@ -39,8 +40,8 @@ impl KeGroup for Ristretto255 {
pk.compress().to_bytes().into()
}
fn deserialize_pk(bytes: &[u8]) -> Result<Self::Pk, ProtocolError> {
CompressedRistretto::from_slice(bytes)
fn deserialize_take_pk(bytes: &mut &[u8]) -> Result<Self::Pk, ProtocolError> {
CompressedRistretto::from_slice(&bytes.take_array::<U32>("public key")?)
.map_err(|_| ProtocolError::SerializationError)?
.decompress()
.filter(|point| point != &RistrettoPoint::identity())
@@ -49,21 +50,7 @@ impl KeGroup for Ristretto255 {
fn random_sk<R: RngCore + CryptoRng>(rng: &mut R) -> Self::Sk {
loop {
let scalar = {
#[cfg(not(test))]
{
Scalar::random(rng)
}
// Tests need an exact conversion from bytes to scalar, sampling only 32 bytes
// from rng
#[cfg(test)]
{
let mut scalar_bytes = [0u8; 32];
rng.fill_bytes(&mut scalar_bytes);
Scalar::from_bytes_mod_order(scalar_bytes)
}
};
let scalar = Scalar::random(rng);
if scalar != Scalar::ZERO {
break scalar;
@@ -71,19 +58,9 @@ impl KeGroup for Ristretto255 {
}
}
// Implements the `HashToScalar()` function from
// <https://www.ietf.org/archive/id/draft-irtf-cfrg-voprf-19.html#section-4>
fn hash_to_scalar<'a, H>(input: &[&[u8]], dst: &[&[u8]]) -> Result<Self::Sk, InternalError>
where
H: BlockSizeUser + Default + FixedOutput + HashMarker,
H::OutputSize: IsLess<U256> + IsLessOrEqual<H::BlockSize>,
{
<voprf::Ristretto255 as Group>::hash_to_scalar::<H>(input, dst)
.map_err(InternalError::OprfInternalError)
}
fn is_zero_scalar(scalar: Self::Sk) -> subtle::Choice {
scalar.ct_eq(&Scalar::ZERO)
fn derive_scalar(seed: GenericArray<u8, Self::SkLen>) -> Result<Self::Sk, InternalError> {
voprf::derive_key::<Self>(&seed, &STR_OPAQUE_DERIVE_AUTH_KEY_PAIR, Mode::Oprf)
.map_err(InternalError::from)
}
fn public_key(sk: Self::Sk) -> Self::Pk {
@@ -94,17 +71,16 @@ impl KeGroup for Ristretto255 {
sk.to_bytes().into()
}
fn deserialize_sk(bytes: &[u8]) -> Result<Self::Sk, ProtocolError> {
fn deserialize_take_sk(bytes: &mut &[u8]) -> Result<Self::Sk, ProtocolError> {
bytes
.try_into()
.take_array::<U32>("secret key")
.ok()
.and_then(|bytes| Scalar::from_canonical_bytes(bytes).into())
.and_then(|bytes| Scalar::from_canonical_bytes(bytes.into()).into())
.filter(|scalar| scalar != &Scalar::ZERO)
.ok_or(ProtocolError::SerializationError)
}
}
#[cfg(feature = "ristretto255-voprf")]
impl voprf::CipherSuite for Ristretto255 {
const ID: &'static str = voprf::Ristretto255::ID;
@@ -113,14 +89,14 @@ impl voprf::CipherSuite for Ristretto255 {
type Hash = <voprf::Ristretto255 as voprf::CipherSuite>::Hash;
}
impl Group for Ristretto255 {
type Elem = <voprf::Ristretto255 as Group>::Elem;
impl voprf::Group for Ristretto255 {
type Elem = <voprf::Ristretto255 as voprf::Group>::Elem;
type ElemLen = <voprf::Ristretto255 as Group>::ElemLen;
type ElemLen = <voprf::Ristretto255 as voprf::Group>::ElemLen;
type Scalar = <voprf::Ristretto255 as Group>::Scalar;
type Scalar = <voprf::Ristretto255 as voprf::Group>::Scalar;
type ScalarLen = <voprf::Ristretto255 as Group>::ScalarLen;
type ScalarLen = <voprf::Ristretto255 as voprf::Group>::ScalarLen;
fn hash_to_curve<H>(
input: &[&[u8]],
@@ -130,7 +106,7 @@ impl Group for Ristretto255 {
H: BlockSizeUser + Default + FixedOutput + HashMarker,
H::OutputSize: IsLess<U256> + IsLessOrEqual<H::BlockSize>,
{
<voprf::Ristretto255 as Group>::hash_to_curve::<H>(input, dst)
<voprf::Ristretto255 as voprf::Group>::hash_to_curve::<H>(input, dst)
}
fn hash_to_scalar<H>(
@@ -141,43 +117,43 @@ impl Group for Ristretto255 {
H: BlockSizeUser + Default + FixedOutput + HashMarker,
H::OutputSize: IsLess<U256> + IsLessOrEqual<H::BlockSize>,
{
<voprf::Ristretto255 as Group>::hash_to_scalar::<H>(input, dst)
<voprf::Ristretto255 as voprf::Group>::hash_to_scalar::<H>(input, dst)
}
fn base_elem() -> Self::Elem {
<voprf::Ristretto255 as Group>::base_elem()
<voprf::Ristretto255 as voprf::Group>::base_elem()
}
fn identity_elem() -> Self::Elem {
<voprf::Ristretto255 as Group>::identity_elem()
<voprf::Ristretto255 as voprf::Group>::identity_elem()
}
fn serialize_elem(elem: Self::Elem) -> GenericArray<u8, Self::ElemLen> {
<voprf::Ristretto255 as Group>::serialize_elem(elem)
<voprf::Ristretto255 as voprf::Group>::serialize_elem(elem)
}
fn deserialize_elem(element_bits: &[u8]) -> voprf::Result<Self::Elem> {
<voprf::Ristretto255 as Group>::deserialize_elem(element_bits)
<voprf::Ristretto255 as voprf::Group>::deserialize_elem(element_bits)
}
fn random_scalar<R: RngCore + CryptoRng>(rng: &mut R) -> Self::Scalar {
<voprf::Ristretto255 as Group>::random_scalar(rng)
<voprf::Ristretto255 as voprf::Group>::random_scalar(rng)
}
fn invert_scalar(scalar: Self::Scalar) -> Self::Scalar {
<voprf::Ristretto255 as Group>::invert_scalar(scalar)
<voprf::Ristretto255 as voprf::Group>::invert_scalar(scalar)
}
fn is_zero_scalar(scalar: Self::Scalar) -> subtle::Choice {
<voprf::Ristretto255 as Group>::is_zero_scalar(scalar)
<voprf::Ristretto255 as voprf::Group>::is_zero_scalar(scalar)
}
fn serialize_scalar(scalar: Self::Scalar) -> GenericArray<u8, Self::ScalarLen> {
<voprf::Ristretto255 as Group>::serialize_scalar(scalar)
<voprf::Ristretto255 as voprf::Group>::serialize_scalar(scalar)
}
fn deserialize_scalar(scalar_bits: &[u8]) -> voprf::Result<Self::Scalar> {
<voprf::Ristretto255 as Group>::deserialize_scalar(scalar_bits)
<voprf::Ristretto255 as voprf::Group>::deserialize_scalar(scalar_bits)
}
}
@@ -186,3 +162,25 @@ impl DiffieHellman<Ristretto255> for Scalar {
Ristretto255::serialize_pk(pk * self)
}
}
//////////////////////////
// Test Implementations //
//===================== //
//////////////////////////
#[cfg(test)]
use crate::serialization::AssertZeroized;
#[cfg(test)]
impl AssertZeroized for RistrettoPoint {
fn assert_zeroized(&self) {
assert_eq!(*self, RistrettoPoint::default());
}
}
#[cfg(test)]
impl AssertZeroized for Scalar {
fn assert_zeroized(&self) {
assert_eq!(*self, Scalar::default());
}
}