Skip to content

Commit c6be7b0

Browse files
feat: add collision resistance for truncated key id (#45)
* allow truncated key id collisions for public tokens * avoid collisions when creating keys * address review comments
1 parent 8552687 commit c6be7b0

File tree

9 files changed

+243
-88
lines changed

9 files changed

+243
-88
lines changed

src/amortized_tokens/server.rs

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use typenum::Unsigned;
77
use voprf::{BlindedElement, Group, Result, VoprfServer, VoprfServerBatchEvaluateFinishResult};
88

99
use crate::{
10-
NonceStore, TokenInput,
10+
COLLISION_AVOIDANCE_ATTEMPTS, NonceStore, TokenInput,
1111
common::{
1212
errors::{CreateKeypairError, IssueTokenResponseError, RedeemTokenError},
1313
private::{PrivateCipherSuite, PublicKey, public_key_to_token_key_id},
@@ -25,6 +25,14 @@ pub struct Server<CS: PrivateCipherSuite> {
2525
}
2626

2727
impl<CS: PrivateCipherSuite> Server<CS> {
28+
fn server_from_seed(seed: &[u8], info: &[u8]) -> Result<VoprfServer<CS>, CreateKeypairError>
29+
where
30+
<CS::Group as Group>::Scalar: Send + Sync,
31+
<CS::Group as Group>::Elem: Send + Sync,
32+
{
33+
VoprfServer::<CS>::new_from_seed(seed, info).map_err(|_| CreateKeypairError::SeedError)
34+
}
35+
2836
/// Create a new server. The new server does not contain any key material.
2937
#[must_use]
3038
pub const fn new() -> Self {
@@ -45,14 +53,29 @@ impl<CS: PrivateCipherSuite> Server<CS> {
4553
<CS::Group as Group>::Scalar: Send + Sync,
4654
<CS::Group as Group>::Elem: Send + Sync,
4755
{
48-
let mut seed = GenericArray::<_, <CS::Group as Group>::ScalarLen>::default();
49-
OsRng.fill_bytes(&mut seed);
50-
self.create_keypair_internal(key_store, &seed, b"PrivacyPass")
51-
.await
56+
for _ in 0..COLLISION_AVOIDANCE_ATTEMPTS {
57+
let mut seed = GenericArray::<_, <CS::Group as Group>::ScalarLen>::default();
58+
OsRng.fill_bytes(&mut seed);
59+
let server = Self::server_from_seed(&seed, b"PrivacyPass")?;
60+
let public_key = server.get_public_key();
61+
let truncated_token_key_id =
62+
truncate_token_key_id(&public_key_to_token_key_id::<CS>(&public_key));
63+
64+
if key_store.get(&truncated_token_key_id).await.is_some() {
65+
continue;
66+
}
67+
68+
if key_store.insert(truncated_token_key_id, server).await {
69+
return Ok(public_key);
70+
}
71+
}
72+
Err(CreateKeypairError::CollisionExhausted)
5273
}
5374

54-
/// Creates a new keypair and inserts it into the key store.
55-
async fn create_keypair_internal<BKS: PrivateKeyStore<CS = CS>>(
75+
/// Creates a new keypair with explicit parameters and inserts it into the
76+
/// key store.
77+
#[cfg(feature = "kat")]
78+
pub async fn create_keypair_with_params<BKS: PrivateKeyStore<CS = CS>>(
5679
&self,
5780
key_store: &BKS,
5881
seed: &[u8],
@@ -62,31 +85,14 @@ impl<CS: PrivateCipherSuite> Server<CS> {
6285
<CS::Group as Group>::Scalar: Send + Sync,
6386
<CS::Group as Group>::Elem: Send + Sync,
6487
{
65-
let server = VoprfServer::<CS>::new_from_seed(seed, info)
66-
.map_err(|_| CreateKeypairError::SeedError)?;
88+
let server = Self::server_from_seed(seed, info)?;
6789
let public_key = server.get_public_key();
6890
let truncated_token_key_id =
6991
truncate_token_key_id(&public_key_to_token_key_id::<CS>(&server.get_public_key()));
7092
key_store.insert(truncated_token_key_id, server).await;
7193
Ok(public_key)
7294
}
7395

74-
/// Creates a new keypair with explicit parameters and inserts it into the
75-
/// key store.
76-
#[cfg(feature = "kat")]
77-
pub async fn create_keypair_with_params<BKS: PrivateKeyStore<CS = CS>>(
78-
&self,
79-
key_store: &BKS,
80-
seed: &[u8],
81-
info: &[u8],
82-
) -> Result<PublicKey<CS>, CreateKeypairError>
83-
where
84-
<CS::Group as Group>::Scalar: Send + Sync,
85-
<CS::Group as Group>::Elem: Send + Sync,
86-
{
87-
self.create_keypair_internal(key_store, seed, info).await
88-
}
89-
9096
/// Issues a token response.
9197
///
9298
/// # Errors

src/common/errors.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@ pub enum SerializationError {
1313
/// Errors that can occur when creating a keypair.
1414
#[derive(Error, Debug, PartialEq, Eq)]
1515
pub enum CreateKeypairError {
16-
#[error("Seed is too long")]
17-
/// Error when the seed is too long.
16+
#[error("Seed does not have the right length")]
17+
/// Error when the seed does not have the right length
1818
SeedError,
19+
#[error("Collision exhausted")]
20+
/// Error when collision attempts are exhausted
21+
CollisionExhausted,
1922
}
2023

2124
/// Errors that can occur when issuing token requests.

src/common/store.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@ use super::private::PrivateCipherSuite;
1212
pub trait PrivateKeyStore {
1313
/// The cipher suite used for the key store.
1414
type CS: PrivateCipherSuite;
15-
/// Inserts a keypair with a given `truncated_token_key_id` into the key store.
15+
/// Inserts a keypair with a given `truncated_token_key_id` into the key
16+
/// store, only if it does not collide with an existing
17+
/// `truncated_token_key_id`.
18+
///
19+
/// Returns `true` if the key was inserted, `false` if a collision occurred.
1620
async fn insert(
1721
&self,
1822
truncated_token_key_id: TruncatedTokenKeyId,
1923
server: VoprfServer<Self::CS>,
20-
);
24+
) -> bool;
2125
/// Returns a keypair with a given `truncated_token_key_id` from the key store.
2226
async fn get(
2327
&self,

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,5 @@ impl TokenInput {
101101
token_input
102102
}
103103
}
104+
105+
pub(crate) const COLLISION_AVOIDANCE_ATTEMPTS: usize = 100;

src/private_tokens/server.rs

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
//! Server-side implementation of Privately Verifiable Token protocol.
22
3-
use generic_array::ArrayLength;
3+
use generic_array::{ArrayLength, GenericArray};
44
use rand::{RngCore, rngs::OsRng};
55
use sha2::digest::OutputSizeUser;
66
use typenum::Unsigned;
77
use voprf::{BlindedElement, Group, Result, VoprfServer};
88

99
use crate::{
10-
NonceStore, TokenInput,
10+
COLLISION_AVOIDANCE_ATTEMPTS, NonceStore, TokenInput,
1111
auth::authorize::Token,
1212
common::{
1313
errors::{CreateKeypairError, IssueTokenResponseError, RedeemTokenError},
@@ -26,6 +26,10 @@ pub struct Server<CS: PrivateCipherSuite> {
2626
}
2727

2828
impl<CS: PrivateCipherSuite> Server<CS> {
29+
fn server_from_seed(seed: &[u8], info: &[u8]) -> Result<VoprfServer<CS>, CreateKeypairError> {
30+
VoprfServer::<CS>::new_from_seed(seed, info).map_err(|_| CreateKeypairError::SeedError)
31+
}
32+
2933
/// Creates a new server.
3034
#[must_use]
3135
pub const fn new() -> Self {
@@ -42,26 +46,23 @@ impl<CS: PrivateCipherSuite> Server<CS> {
4246
&self,
4347
key_store: &PKS,
4448
) -> Result<PublicKey<CS>, CreateKeypairError> {
45-
let mut seed = vec![0u8; <<CS::Group as Group>::ScalarLen as Unsigned>::USIZE];
46-
OsRng.fill_bytes(&mut seed);
47-
self.create_keypair_internal(key_store, &seed, b"PrivacyPass")
48-
.await
49-
}
49+
for _ in 0..COLLISION_AVOIDANCE_ATTEMPTS {
50+
let mut seed = GenericArray::<_, <CS::Group as Group>::ScalarLen>::default();
51+
OsRng.fill_bytes(&mut seed);
52+
let server = Self::server_from_seed(&seed, b"PrivacyPass")?;
53+
let public_key = server.get_public_key();
54+
let truncated_token_key_id =
55+
truncate_token_key_id(&public_key_to_token_key_id::<CS>(&public_key));
5056

51-
/// Creates a new keypair and inserts it into the key store.
52-
async fn create_keypair_internal<PKS: PrivateKeyStore<CS = CS>>(
53-
&self,
54-
key_store: &PKS,
55-
seed: &[u8],
56-
info: &[u8],
57-
) -> Result<PublicKey<CS>, CreateKeypairError> {
58-
let server = VoprfServer::<CS>::new_from_seed(seed, info)
59-
.map_err(|_| CreateKeypairError::SeedError)?;
60-
let public_key = server.get_public_key();
61-
let truncated_token_key_id =
62-
truncate_token_key_id(&public_key_to_token_key_id::<CS>(&server.get_public_key()));
63-
key_store.insert(truncated_token_key_id, server).await;
64-
Ok(public_key)
57+
if key_store.get(&truncated_token_key_id).await.is_some() {
58+
continue;
59+
}
60+
61+
if key_store.insert(truncated_token_key_id, server).await {
62+
return Ok(public_key);
63+
}
64+
}
65+
Err(CreateKeypairError::CollisionExhausted)
6566
}
6667

6768
/// Creates a new keypair with explicit parameters and inserts it into the
@@ -73,7 +74,12 @@ impl<CS: PrivateCipherSuite> Server<CS> {
7374
seed: &[u8],
7475
info: &[u8],
7576
) -> Result<PublicKey<CS>, CreateKeypairError> {
76-
self.create_keypair_internal(key_store, seed, info).await
77+
let server = Self::server_from_seed(seed, info)?;
78+
let public_key = server.get_public_key();
79+
let truncated_token_key_id =
80+
truncate_token_key_id(&public_key_to_token_key_id::<CS>(&server.get_public_key()));
81+
key_store.insert(truncated_token_key_id, server).await;
82+
Ok(public_key)
7783
}
7884

7985
/// Issues a token response.

src/public_tokens/server.rs

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use generic_array::ArrayLength;
66
use rand::{CryptoRng, RngCore, rngs::OsRng};
77

88
use crate::{
9-
NonceStore, TokenInput, TokenType, TruncatedTokenKeyId,
9+
COLLISION_AVOIDANCE_ATTEMPTS, NonceStore, TokenInput, TokenType, TruncatedTokenKeyId,
1010
auth::authorize::Token,
1111
common::errors::{CreateKeypairError, IssueTokenResponseError, RedeemTokenError},
1212
};
@@ -18,8 +18,12 @@ use super::{NK, TokenRequest, TokenResponse, public_key_to_token_key_id, truncat
1818
#[async_trait]
1919

2020
pub trait IssuerKeyStore: Send + Sync {
21-
/// Inserts a keypair with a given `truncated_token_key_id` into the key store.
22-
async fn insert(&self, truncated_token_key_id: TruncatedTokenKeyId, server: KeyPair);
21+
/// Inserts a keypair with a given `truncated_token_key_id` into the key
22+
/// store, only if it does not collide with an existing
23+
/// `truncated_token_key_id`.
24+
///
25+
/// Returns `true` if the key was inserted, `false` if a collision occurred.
26+
async fn insert(&self, truncated_token_key_id: TruncatedTokenKeyId, server: KeyPair) -> bool;
2327
/// Returns a keypair with a given `truncated_token_key_id` from the key store.
2428
async fn get(&self, truncated_token_key_id: &TruncatedTokenKeyId) -> Option<KeyPair>;
2529
}
@@ -30,8 +34,8 @@ pub trait IssuerKeyStore: Send + Sync {
3034
pub trait OriginKeyStore {
3135
/// Inserts a keypair with a given `truncated_token_key_id` into the key store.
3236
async fn insert(&self, truncated_token_key_id: TruncatedTokenKeyId, server: PublicKey);
33-
/// Returns a keypair with a given `truncated_token_key_id` from the key store.
34-
async fn get(&self, truncated_token_key_id: &TruncatedTokenKeyId) -> Option<PublicKey>;
37+
/// Returns all public keys with a given `truncated_token_key_id` from the key store.
38+
async fn get(&self, truncated_token_key_id: &TruncatedTokenKeyId) -> Vec<PublicKey>;
3539
}
3640

3741
/// Serializes a keypair into a DER-encoded PKCS#8 document.
@@ -64,14 +68,23 @@ impl IssuerServer {
6468
rng: &mut R,
6569
key_store: &IKS,
6670
) -> Result<PublicKey, CreateKeypairError> {
67-
let key_pair =
68-
KeyPair::generate(rng, KEYSIZE_IN_BITS).map_err(|_| CreateKeypairError::SeedError)?;
69-
let truncated_token_key_id =
70-
truncate_token_key_id(&public_key_to_token_key_id(&key_pair.pk));
71-
key_store
72-
.insert(truncated_token_key_id, key_pair.clone())
73-
.await;
74-
Ok(key_pair.pk)
71+
for _ in 0..COLLISION_AVOIDANCE_ATTEMPTS {
72+
let key_pair = KeyPair::generate(rng, KEYSIZE_IN_BITS)
73+
.map_err(|_| CreateKeypairError::SeedError)?;
74+
let truncated_token_key_id =
75+
truncate_token_key_id(&public_key_to_token_key_id(&key_pair.pk));
76+
77+
if key_store.get(&truncated_token_key_id).await.is_some() {
78+
continue;
79+
}
80+
81+
let public_key = key_pair.pk.clone();
82+
83+
if key_store.insert(truncated_token_key_id, key_pair).await {
84+
return Ok(public_key);
85+
}
86+
}
87+
Err(CreateKeypairError::CollisionExhausted)
7588
}
7689

7790
/// Issues a new token response.
@@ -152,17 +165,26 @@ impl OriginServer {
152165
*token.token_key_id(),
153166
);
154167

155-
let public_key = key_store
156-
.get(&truncate_token_key_id(token.token_key_id()))
157-
.await
158-
.ok_or(RedeemTokenError::KeyIdNotFound)?;
168+
let truncated_token_key_id = truncate_token_key_id(token.token_key_id());
169+
let public_keys = key_store.get(&truncated_token_key_id).await;
170+
if public_keys.is_empty() {
171+
return Err(RedeemTokenError::KeyIdNotFound);
172+
}
159173

160174
let options = Options::default();
161175
let signature = Signature(token.authenticator().to_vec());
176+
let token_input_bytes = token_input.serialize();
177+
178+
let verified = public_keys.iter().any(|public_key| {
179+
signature
180+
.verify(public_key, None, &token_input_bytes, &options)
181+
.is_ok()
182+
});
183+
184+
if !verified {
185+
return Err(RedeemTokenError::InvalidToken);
186+
}
162187

163-
signature
164-
.verify(&public_key, None, token_input.serialize(), &options)
165-
.map_err(|_| RedeemTokenError::InvalidToken)?;
166188
nonce_store.insert(token.nonce()).await;
167189
Ok(())
168190
}

src/test_utils/private_memory_store.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
//! This module contains in-memory implementations of the `PrivateKeyStore` trait.
22
use async_trait::async_trait;
3-
use std::{collections::HashMap, fmt::Debug};
3+
use std::{
4+
collections::{HashMap, hash_map::Entry},
5+
fmt::Debug,
6+
};
47
use tokio::sync::Mutex;
58
use voprf::*;
69

@@ -18,9 +21,18 @@ pub struct MemoryKeyStoreVoprf<CS: PrivateCipherSuite> {
1821
impl<C: PrivateCipherSuite> PrivateKeyStore for MemoryKeyStoreVoprf<C> {
1922
type CS = C;
2023

21-
async fn insert(&self, truncated_token_key_id: TruncatedTokenKeyId, server: VoprfServer<C>) {
24+
async fn insert(
25+
&self,
26+
truncated_token_key_id: TruncatedTokenKeyId,
27+
server: VoprfServer<C>,
28+
) -> bool {
2229
let mut keys = self.keys.lock().await;
23-
keys.insert(truncated_token_key_id, server.clone());
30+
if let Entry::Vacant(e) = keys.entry(truncated_token_key_id) {
31+
e.insert(server);
32+
true
33+
} else {
34+
false
35+
}
2436
}
2537

2638
async fn get(&self, truncated_token_key_id: &TruncatedTokenKeyId) -> Option<VoprfServer<C>> {

0 commit comments

Comments
 (0)