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
+67 -69
View File
@@ -7,26 +7,25 @@
// licenses.
use core::convert::TryFrom;
use core::ops::Add;
use derive_where::derive_where;
use digest::Output;
use generic_array::sequence::Concat;
use generic_array::typenum::{Sum, Unsigned, U2, U32};
use generic_array::{ArrayLength, GenericArray};
use generic_array::typenum::{Sum, U32};
use generic_array::GenericArray;
use hkdf::Hkdf;
use hmac::{Hmac, Mac};
use rand::{CryptoRng, RngCore};
use zeroize::{Zeroize, ZeroizeOnDrop};
use crate::ciphersuite::{CipherSuite, OprfHash};
use crate::errors::utils::check_slice_size;
use crate::ciphersuite::{CipherSuite, KeGroup, OprfHash};
use crate::errors::{InternalError, ProtocolError};
use crate::hash::OutputSize;
use crate::key_exchange::group::KeGroup;
use crate::keypair::{KeyPair, PrivateKey, PrivateKeySerialization, PublicKey};
use crate::opaque::{bytestrings_from_identifiers, Identifiers};
use crate::serialization::{Input, MacExt};
use crate::key_exchange::group::Group;
use crate::key_exchange::traits::SerializedIdentifiers;
use crate::keypair::{KeyPair, PrivateKey, PublicKey};
use crate::opaque::Identifiers;
use crate::serialization::{GenericArrayExt, SliceExt, UpdateExt};
// Constant string used as salt for HKDF computation
const STR_AUTH_KEY: [u8; 7] = *b"AuthKey";
@@ -82,10 +81,9 @@ pub(crate) struct Envelope<CS: CipherSuite> {
// which is technically unrelated to the envelope's encrypted and authenticated
// contents.
pub(crate) struct OpenedEnvelope<'a, CS: CipherSuite> {
pub(crate) client_static_keypair: KeyPair<CS::KeGroup>,
pub(crate) client_static_keypair: KeyPair<KeGroup<CS>>,
pub(crate) export_key: Output<OprfHash<CS>>,
pub(crate) id_u: Input<'a, U2, <CS::KeGroup as KeGroup>::PkLen>,
pub(crate) id_s: Input<'a, U2, <CS::KeGroup as KeGroup>::PkLen>,
pub(crate) identifiers: SerializedIdentifiers<'a, KeGroup<CS>>,
}
pub(crate) struct OpenedInnerEnvelope<CS: CipherSuite> {
@@ -97,23 +95,23 @@ type SealRawResult<CS: CipherSuite> = (Envelope<CS>, Output<OprfHash<CS>>);
#[cfg(test)]
type SealRawResult<CS: CipherSuite> = (Envelope<CS>, Output<OprfHash<CS>>, Output<OprfHash<CS>>);
#[cfg(not(test))]
type SealResult<CS: CipherSuite> = (Envelope<CS>, PublicKey<CS::KeGroup>, Output<OprfHash<CS>>);
type SealResult<CS: CipherSuite> = (Envelope<CS>, PublicKey<KeGroup<CS>>, Output<OprfHash<CS>>);
#[cfg(test)]
type SealResult<CS: CipherSuite> = (
Envelope<CS>,
PublicKey<CS::KeGroup>,
PublicKey<KeGroup<CS>>,
Output<OprfHash<CS>>,
Output<OprfHash<CS>>,
);
pub(crate) type EnvelopeLen<CS: CipherSuite> = Sum<NonceLen, OutputSize<OprfHash<CS>>>;
pub(crate) type EnvelopeLen<CS: CipherSuite> = Sum<OutputSize<OprfHash<CS>>, NonceLen>;
impl<CS: CipherSuite> Envelope<CS> {
#[allow(clippy::type_complexity)]
pub(crate) fn seal<R: RngCore + CryptoRng>(
rng: &mut R,
randomized_pwd_hasher: Hkdf<OprfHash<CS>>,
server_s_pk: &PublicKey<CS::KeGroup>,
server_s_pk: &PublicKey<KeGroup<CS>>,
ids: Identifiers,
) -> Result<SealResult<CS>, ProtocolError> {
let mut nonce = GenericArray::default();
@@ -125,12 +123,16 @@ impl<CS: CipherSuite> Envelope<CS> {
);
let server_s_pk_bytes = server_s_pk.serialize();
let (id_u, id_s) = bytestrings_from_identifiers::<CS::KeGroup>(
let identifiers = SerializedIdentifiers::<KeGroup<CS>>::from_identifiers(
ids,
client_s_pk.serialize(),
server_s_pk_bytes.clone(),
)?;
let aad = construct_aad(id_u.iter(), id_s.iter(), &server_s_pk_bytes);
let aad = construct_aad(
identifiers.client.iter(),
identifiers.server.iter(),
&server_s_pk_bytes,
);
let result = Self::seal_raw(randomized_pwd_hasher, nonce, aad, mode)?;
Ok((
@@ -183,7 +185,7 @@ impl<CS: CipherSuite> Envelope<CS> {
pub(crate) fn open<'a>(
&self,
randomized_pwd_hasher: Hkdf<OprfHash<CS>>,
server_s_pk: PublicKey<CS::KeGroup>,
server_s_pk: PublicKey<KeGroup<CS>>,
optional_ids: Identifiers<'a>,
) -> Result<OpenedEnvelope<'a, CS>, ProtocolError> {
let client_static_keypair = match self.mode {
@@ -196,20 +198,23 @@ impl<CS: CipherSuite> Envelope<CS> {
};
let server_s_pk_bytes = server_s_pk.serialize();
let (id_u, id_s) = bytestrings_from_identifiers::<CS::KeGroup>(
let identifiers = SerializedIdentifiers::<KeGroup<CS>>::from_identifiers(
optional_ids,
client_static_keypair.public().serialize(),
server_s_pk_bytes.clone(),
)?;
let aad = construct_aad(id_u.iter(), id_s.iter(), &server_s_pk_bytes);
let aad = construct_aad(
identifiers.client.iter(),
identifiers.server.iter(),
&server_s_pk_bytes,
);
let opened = self.open_raw(randomized_pwd_hasher, aad)?;
Ok(OpenedEnvelope {
client_static_keypair,
export_key: opened.export_key,
id_u,
id_s,
identifiers,
})
}
@@ -249,45 +254,22 @@ impl<CS: CipherSuite> Envelope<CS> {
}
}
fn hmac_key_size() -> usize {
OutputSize::<OprfHash<CS>>::USIZE
}
#[cfg(test)]
pub(crate) fn len() -> usize {
use generic_array::typenum::Unsigned;
OutputSize::<OprfHash<CS>>::USIZE + NonceLen::USIZE
}
pub(crate) fn serialize(&self) -> GenericArray<u8, EnvelopeLen<CS>>
where
// Envelope: Nonce + Hash
NonceLen: Add<OutputSize<OprfHash<CS>>>,
EnvelopeLen<CS>: ArrayLength<u8>,
{
self.nonce.concat(self.hmac.clone())
pub(crate) fn serialize(&self) -> GenericArray<u8, EnvelopeLen<CS>> {
self.nonce.concat_ext(&self.hmac)
}
pub(crate) fn deserialize(bytes: &[u8]) -> Result<Self, ProtocolError> {
let mode = InnerEnvelopeMode::Internal; // Better way to hard-code this?
if bytes.len() < NonceLen::USIZE {
return Err(ProtocolError::SerializationError);
}
let nonce = GenericArray::clone_from_slice(&bytes[..NonceLen::USIZE]);
let remainder = match mode {
InnerEnvelopeMode::Zero => {
return Err(InternalError::IncompatibleEnvelopeModeError.into())
}
InnerEnvelopeMode::Internal => &bytes[NonceLen::USIZE..],
};
let hmac_key_size = Self::hmac_key_size();
let hmac = check_slice_size(remainder, hmac_key_size, "hmac_key_size")?;
pub(crate) fn deserialize_take(bytes: &mut &[u8]) -> Result<Self, ProtocolError> {
Ok(Self {
mode,
nonce,
hmac: GenericArray::clone_from_slice(hmac),
mode: InnerEnvelopeMode::Internal,
nonce: bytes.take_array("nonce")?,
hmac: bytes.take_array("hmac")?,
})
}
}
@@ -297,33 +279,28 @@ impl<CS: CipherSuite> Envelope<CS> {
fn build_inner_envelope_internal<CS: CipherSuite>(
randomized_pwd_hasher: Hkdf<OprfHash<CS>>,
nonce: GenericArray<u8, NonceLen>,
) -> Result<PublicKey<CS::KeGroup>, ProtocolError> {
let mut keypair_seed = GenericArray::<_, <CS::KeGroup as KeGroup>::SkLen>::default();
) -> Result<PublicKey<KeGroup<CS>>, ProtocolError> {
let mut keypair_seed = GenericArray::<_, <KeGroup<CS> as Group>::SkLen>::default();
randomized_pwd_hasher
.expand(&nonce.concat(STR_PRIVATE_KEY.into()), &mut keypair_seed)
.map_err(|_| InternalError::HkdfError)?;
let client_static_keypair =
PrivateKey::<CS::KeGroup>::deserialize_key_pair(&CS::KeGroup::serialize_sk(
CS::KeGroup::derive_auth_keypair::<CS::OprfCs>(keypair_seed)?,
))?;
let client_s_sk = PrivateKey::new(KeGroup::<CS>::derive_scalar(keypair_seed)?);
Ok(client_static_keypair.public().clone())
Ok(client_s_sk.public_key())
}
fn recover_keys_internal<CS: CipherSuite>(
randomized_pwd_hasher: Hkdf<OprfHash<CS>>,
nonce: GenericArray<u8, NonceLen>,
) -> Result<KeyPair<CS::KeGroup>, ProtocolError> {
let mut keypair_seed = GenericArray::<_, <CS::KeGroup as KeGroup>::SkLen>::default();
) -> Result<KeyPair<KeGroup<CS>>, ProtocolError> {
let mut keypair_seed = GenericArray::<_, <KeGroup<CS> as Group>::SkLen>::default();
randomized_pwd_hasher
.expand(&nonce.concat(STR_PRIVATE_KEY.into()), &mut keypair_seed)
.map_err(|_| InternalError::HkdfError)?;
let client_static_keypair =
PrivateKey::<CS::KeGroup>::deserialize_key_pair(&CS::KeGroup::serialize_sk(
CS::KeGroup::derive_auth_keypair::<CS::OprfCs>(keypair_seed)?,
))?;
let client_s_sk = PrivateKey::new(KeGroup::<CS>::derive_scalar(keypair_seed)?);
let client_s_pk = client_s_sk.public_key();
Ok(client_static_keypair)
Ok(KeyPair::new(client_s_sk, client_s_pk))
}
fn construct_aad<'a>(
@@ -333,3 +310,24 @@ fn construct_aad<'a>(
) -> impl Iterator<Item = &'a [u8]> {
[server_s_pk].into_iter().chain(id_s).chain(id_u)
}
//////////////////////////
// Test Implementations //
//===================== //
//////////////////////////
#[cfg(test)]
use crate::serialization::AssertZeroized;
#[cfg(test)]
impl<CS: CipherSuite> AssertZeroized for Envelope<CS> {
fn assert_zeroized(&self) {
let Self { mode, nonce, hmac } = self;
assert_eq!(mode, &InnerEnvelopeMode::Zero);
for byte in nonce.iter().chain(hmac) {
assert_eq!(byte, &0);
}
}
}