Skip to content

Commit 43a0bc6

Browse files
authored
feat: propagate tag during keygen from existing shares (#444)
Closes zama-ai/kms-internal#2920
1 parent 74847b7 commit 43a0bc6

File tree

9 files changed

+153
-8
lines changed

9 files changed

+153
-8
lines changed

core-client/src/keygen.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ pub(crate) async fn do_keygen(
8585
.map(|id| kms_grpc::kms::v1::KeySetAddedInfo {
8686
existing_keyset_id: Some(id.into()),
8787
existing_epoch_id: shared_config.existing_epoch_id.map(Into::into),
88+
use_existing_key_tag: shared_config.use_existing_key_tag,
8889
..Default::default()
8990
});
9091
let dkg_req = internal_client.key_gen_request(

core-client/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,10 @@ pub struct SharedKeyGenParameters {
595595
/// Epoch ID for the existing keyset (optional, defaults to the request's epoch).
596596
#[clap(long)]
597597
pub existing_epoch_id: Option<EpochId>,
598+
/// Reuse the tag from the existing keyset instead of using the new key ID as tag.
599+
/// This is only used when generating a key from existing shares.
600+
#[clap(long, default_value_t = false)]
601+
pub use_existing_key_tag: bool,
598602
pub context_id: Option<ContextId>,
599603
pub epoch_id: Option<EpochId>,
600604
}

core-client/tests/integration/integration_test.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1632,6 +1632,7 @@ async fn test_threshold_mpc_context_switch_6(ctx: &DockerComposeThresholdTestNoI
16321632
compressed: false,
16331633
existing_keyset_id: None,
16341634
existing_epoch_id: None,
1635+
use_existing_key_tag: false,
16351636
},
16361637
200,
16371638
)
@@ -1842,6 +1843,7 @@ async fn test_threshold_reshare(ctx: &DockerComposeThresholdTestNoInitSixParty)
18421843
compressed: false,
18431844
existing_keyset_id: None,
18441845
existing_epoch_id: None,
1846+
use_existing_key_tag: false,
18451847
},
18461848
200,
18471849
)

core-client/tests/integration/integration_test_isolated.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2028,6 +2028,7 @@ async fn real_preproc_and_keygen_compressed_isolated(
20282028
compressed: true,
20292029
existing_keyset_id: None,
20302030
existing_epoch_id: None,
2031+
use_existing_key_tag: false,
20312032
context_id: None,
20322033
epoch_id: None,
20332034
},

core/grpc/proto/kms.v1.proto

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ message KeySetAddedInfo {
5757
// Can be set if KeyGenSecretKeyConfig::UseExisting is used,
5858
// if it's not set then the epoch ID from the KeyGenRequest is used.
5959
RequestId existing_epoch_id = 4;
60+
61+
// If true, use the tag from the existing keyset's PublicKeyMaterial
62+
// instead of deriving the tag from the new request ID.
63+
//
64+
// This option is only used when KeyGenSecretKeyConfig::UseExisting is used.
65+
// Otherwise it is ignored.
66+
bool use_existing_key_tag = 5;
6067
}
6168

6269
// The keyset configuration message.

core/service/src/client/tests/common.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub(crate) fn compressed_keygen_config() -> (Option<KeySetConfig>, Option<KeySet
4848
pub(crate) fn compressed_from_existing_keygen_config(
4949
existing_keyset_id: &RequestId,
5050
existing_epoch_id: &kms_grpc::identifiers::EpochId,
51+
use_existing_key_tag: bool,
5152
) -> (Option<KeySetConfig>, Option<KeySetAddedInfo>) {
5253
(
5354
Some(KeySetConfig {
@@ -63,6 +64,7 @@ pub(crate) fn compressed_from_existing_keygen_config(
6364
to_keyset_id_decompression_only: None,
6465
existing_keyset_id: Some((*existing_keyset_id).into()),
6566
existing_epoch_id: Some((*existing_epoch_id).into()),
67+
use_existing_key_tag,
6668
}),
6769
)
6870
}
@@ -83,6 +85,7 @@ pub(crate) fn decompression_keygen_config(
8385
to_keyset_id_decompression_only: Some((*to_keyset_id).into()),
8486
existing_keyset_id: None,
8587
existing_epoch_id: None,
88+
use_existing_key_tag: false,
8689
}),
8790
)
8891
}

core/service/src/client/tests/threshold/key_gen_tests_isolated.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ async fn secure_threshold_compressed_keygen_from_existing_isolated() -> Result<(
477477
let keygen_id_2 = derive_request_id("compressed_existing_keygen_2")?;
478478

479479
let (keyset_config, keyset_added_info) =
480-
compressed_from_existing_keygen_config(&keygen_id_1, &DEFAULT_EPOCH_ID);
480+
compressed_from_existing_keygen_config(&keygen_id_1, &DEFAULT_EPOCH_ID, true);
481481

482482
threshold_key_gen_secure_isolated(
483483
clients,
@@ -519,6 +519,30 @@ async fn secure_threshold_compressed_keygen_from_existing_isolated() -> Result<(
519519
FileStorage::new(Some(material_path), StorageType::PUB, prefix.as_deref())?,
520520
);
521521
}
522+
523+
// Verify tag propagation: keys from keygen_id_2 should carry keygen_id_1's tag
524+
{
525+
use crate::vault::storage::crypto_material::CryptoMaterialReader;
526+
use tfhe::prelude::Tagged;
527+
let expected_tag: tfhe::Tag = keygen_id_1.into();
528+
for (&party_id, storage) in &pub_storage_map {
529+
let compressed_keyset: tfhe::xof_key_set::CompressedXofKeySet =
530+
CryptoMaterialReader::read_from_storage(storage, &keygen_id_2).await?;
531+
532+
let (pk, server_key) = compressed_keyset.decompress().unwrap().into_raw_parts();
533+
assert_eq!(
534+
pk.tag(),
535+
&expected_tag,
536+
"Public key for party {party_id} should have tag propagated from existing keyset"
537+
);
538+
assert_eq!(
539+
server_key.tag(),
540+
&expected_tag,
541+
"Server key for party {party_id} should have tag propagated from existing keyset"
542+
);
543+
}
544+
}
545+
522546
let client_storage = FileStorage::new(Some(material_path), StorageType::CLIENT, None)?;
523547
let mut internal_client = crate::client::client_wasm::Client::new_client(
524548
client_storage,
@@ -712,6 +736,7 @@ async fn test_insecure_threshold_decompression_keygen_isolated() -> Result<()> {
712736
to_keyset_id_decompression_only: Some(key_id_2.into()),
713737
existing_keyset_id: None,
714738
existing_epoch_id: None,
739+
use_existing_key_tag: false,
715740
}),
716741
context_id: None,
717742
epoch_id: None,

core/service/src/engine/keyset_configuration.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,12 @@ impl InternalKeySetConfig {
175175
Ok(keyset_id)
176176
}
177177

178+
pub fn use_existing_key_tag(&self) -> bool {
179+
self.keyset_added_info
180+
.as_ref()
181+
.is_some_and(|info| info.use_existing_key_tag)
182+
}
183+
178184
pub fn get_existing_epoch_id(&self) -> anyhow::Result<Option<EpochId>> {
179185
let added_info = self
180186
.keyset_added_info

core/service/src/engine/threshold/service/key_generator.rs

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use observability::{
1818
},
1919
};
2020
use tfhe::integer::compression_keys::DecompressionKey;
21+
use tfhe::prelude::Tagged;
2122
use tfhe::xof_key_set::CompressedXofKeySet;
2223
use threshold_fhe::{
2324
algebra::{
@@ -77,14 +78,18 @@ use crate::{
7778
},
7879
rate_limiter::RateLimiter,
7980
},
80-
vault::storage::{crypto_material::ThresholdCryptoMaterialStorage, Storage, StorageExt},
81+
vault::storage::{
82+
crypto_material::{CryptoMaterialReader, ThresholdCryptoMaterialStorage},
83+
Storage, StorageExt,
84+
},
8185
};
8286

8387
// === Current Module Imports ===
8488
use super::BucketMetaStore;
8589

8690
const DKG_Z64_SESSION_COUNTER: u64 = 1;
8791
const DKG_Z128_SESSION_COUNTER: u64 = 2;
92+
const ERR_FAILED_TO_READ_EXISTING_TAG: &str = "Failed to read existing tag";
8893

8994
struct DkgSessions {
9095
session_z64: SmallSession<ResiduePolyF4Z64>,
@@ -342,6 +347,14 @@ impl<
342347
// we must validate the parameter before passing it into the background process
343348
internal_keyset_config.validate()?;
344349

350+
// Read existing key tag from public storage if needed
351+
let existing_key_tag: Option<tfhe::Tag> = if internal_keyset_config.use_existing_key_tag() {
352+
let existing_keyset_id = internal_keyset_config.get_existing_keyset_id()?;
353+
Some(Self::read_existing_key_tag(&crypto_storage, &existing_keyset_id).await?)
354+
} else {
355+
None
356+
};
357+
345358
let keygen_background = async move {
346359
// Remove the preprocessing material, even if the request was cancelled we cannot reuse the preprocessing
347360
match &preproc_handle_w_mode {
@@ -366,6 +379,7 @@ impl<
366379
// Nothing to remove
367380
}
368381
}
382+
369383
match internal_keyset_config.keyset_config() {
370384
ddec_keyset_config::KeySetConfig::Standard(inner_config) => {
371385
Self::key_gen_background(
@@ -382,6 +396,7 @@ impl<
382396
eip712_domain_copy,
383397
permit,
384398
op_tag,
399+
existing_key_tag,
385400
)
386401
.await
387402
}
@@ -1005,13 +1020,13 @@ impl<
10051020

10061021
#[allow(clippy::too_many_arguments)]
10071022
async fn compressed_key_gen_from_existing_private_keyset<P>(
1008-
req_id: &RequestId,
10091023
dkg_sessions: &mut DkgSessions,
10101024
crypto_storage: ThresholdCryptoMaterialStorage<PubS, PrivS>,
10111025
params: DKGParams,
10121026
existing_keyset_id: RequestId,
10131027
existing_epoch_id: EpochId,
10141028
preprocessing: &mut P,
1029+
tag: tfhe::Tag,
10151030
) -> anyhow::Result<(
10161031
CompressedXofKeySet,
10171032
PrivateKeySet<{ ResiduePolyF4Z128::EXTENSION_DEGREE }>,
@@ -1040,7 +1055,7 @@ impl<
10401055
&mut dkg_sessions.session_z128,
10411056
preprocessing,
10421057
params,
1043-
req_id.into(),
1058+
tag,
10441059
&existing_private_keys,
10451060
)
10461061
.await?;
@@ -1049,13 +1064,13 @@ impl<
10491064

10501065
#[allow(clippy::too_many_arguments)]
10511066
async fn key_gen_from_existing_private_keyset<P>(
1052-
req_id: &RequestId,
10531067
dkg_sessions: &mut DkgSessions,
10541068
crypto_storage: ThresholdCryptoMaterialStorage<PubS, PrivS>,
10551069
params: DKGParams,
10561070
existing_keyset_id: RequestId,
10571071
existing_epoch_id: EpochId,
10581072
preprocessing: &mut P,
1073+
tag: tfhe::Tag,
10591074
) -> anyhow::Result<(
10601075
FhePubKeySet,
10611076
PrivateKeySet<{ ResiduePolyF4Z128::EXTENSION_DEGREE }>,
@@ -1084,7 +1099,7 @@ impl<
10841099
&mut dkg_sessions.session_z128,
10851100
preprocessing,
10861101
params,
1087-
req_id.into(),
1102+
tag,
10881103
&existing_private_keys,
10891104
)
10901105
.await?;
@@ -1106,6 +1121,7 @@ impl<
11061121
eip712_domain: alloy_sol_types::Eip712Domain,
11071122
permit: OwnedSemaphorePermit,
11081123
op_tag: &'static str,
1124+
existing_key_tag: Option<tfhe::Tag>,
11091125
) {
11101126
let _permit = permit;
11111127
let start = Instant::now();
@@ -1209,14 +1225,15 @@ impl<
12091225
.get_existing_epoch_id()
12101226
.expect("validated")
12111227
.unwrap_or(*epoch_id);
1228+
let tag: tfhe::Tag = existing_key_tag.unwrap_or_else(|| req_id.into());
12121229
Self::key_gen_from_existing_private_keyset(
1213-
req_id,
12141230
&mut dkg_sessions,
12151231
crypto_storage.clone(),
12161232
params,
12171233
existing_keyset_id,
12181234
existing_epoch_id,
12191235
preproc_handle.as_mut(),
1236+
tag,
12201237
)
12211238
.await
12221239
.map(|(pk, sk)| ThresholdKeyGenResult::Uncompressed(pk, sk))
@@ -1233,14 +1250,15 @@ impl<
12331250
.get_existing_epoch_id()
12341251
.expect("validated")
12351252
.unwrap_or(*epoch_id);
1253+
let tag: tfhe::Tag = existing_key_tag.unwrap_or_else(|| req_id.into());
12361254
Self::compressed_key_gen_from_existing_private_keyset(
1237-
req_id,
12381255
&mut dkg_sessions,
12391256
crypto_storage.clone(),
12401257
params,
12411258
existing_keyset_id,
12421259
existing_epoch_id,
12431260
preproc_handle.as_mut(),
1261+
tag,
12441262
)
12451263
.await
12461264
.map(|(compressed_keyset, sk)| {
@@ -1392,6 +1410,36 @@ impl<
13921410
start.elapsed().as_millis()
13931411
);
13941412
}
1413+
1414+
/// Reads the tag from an existing keyset in public storage.
1415+
/// Tries CompressedXofKeySet first, then falls back to ServerKey.
1416+
async fn read_existing_key_tag(
1417+
crypto_storage: &ThresholdCryptoMaterialStorage<PubS, PrivS>,
1418+
existing_keyset_id: &RequestId,
1419+
) -> anyhow::Result<tfhe::Tag> {
1420+
let pub_storage = crypto_storage.inner.public_storage.lock().await;
1421+
1422+
let res = if let Ok(compressed_keyset) =
1423+
<CompressedXofKeySet as CryptoMaterialReader>::read_from_storage(
1424+
&*pub_storage,
1425+
existing_keyset_id,
1426+
)
1427+
.await
1428+
{
1429+
Ok(compressed_keyset
1430+
.clone()
1431+
.into_raw_parts()
1432+
.1
1433+
.into_raw_parts()
1434+
.1)
1435+
} else {
1436+
CryptoMaterialReader::read_from_storage(&*pub_storage, existing_keyset_id)
1437+
.await
1438+
.map(|server_key: tfhe::ServerKey| server_key.tag().clone())
1439+
};
1440+
1441+
res.map_err(|e| anyhow::anyhow!("{}: {e}", ERR_FAILED_TO_READ_EXISTING_TAG))
1442+
}
13951443
}
13961444

13971445
#[tonic::async_trait]
@@ -1800,6 +1848,54 @@ mod tests {
18001848
// we don't have a trait for meta store
18011849
}
18021850

1851+
#[tokio::test]
1852+
async fn use_existing_key_tag_with_wrong_keyset_id() {
1853+
// When use_existing_key_tag is true but existing_keyset_id points to a
1854+
// non-existent key in storage, launch_dkg should fail with Internal
1855+
// because read_existing_key_tag cannot find any key material.
1856+
let (prep_ids, kg) = setup_key_generator::<
1857+
DroppingOnlineDistributedKeyGen128<{ ResiduePolyF4Z128::EXTENSION_DEGREE }>,
1858+
>()
1859+
.await;
1860+
let prep_id = prep_ids[0];
1861+
let key_id = RequestId::new_random(&mut OsRng);
1862+
let wrong_keyset_id = RequestId::new_random(&mut OsRng);
1863+
1864+
let domain = alloy_to_protobuf_domain(&dummy_domain()).unwrap();
1865+
let keyset_config = KeySetConfig {
1866+
keyset_type: kms_grpc::kms::v1::KeySetType::Standard as i32,
1867+
standard_keyset_config: Some(kms_grpc::kms::v1::StandardKeySetConfig {
1868+
compute_key_type: 0,
1869+
secret_key_config: kms_grpc::kms::v1::KeyGenSecretKeyConfig::UseExisting as i32,
1870+
compressed_key_config: 0,
1871+
}),
1872+
};
1873+
let keyset_added_info = KeySetAddedInfo {
1874+
existing_keyset_id: Some(wrong_keyset_id.into()),
1875+
use_existing_key_tag: true,
1876+
..Default::default()
1877+
};
1878+
1879+
let request = tonic::Request::new(KeyGenRequest {
1880+
request_id: Some(key_id.into()),
1881+
params: Some(FheParameter::Test as i32),
1882+
preproc_id: Some(prep_id.into()),
1883+
domain: Some(domain),
1884+
keyset_config: Some(keyset_config),
1885+
keyset_added_info: Some(keyset_added_info),
1886+
context_id: Some((*DEFAULT_MPC_CONTEXT).into()),
1887+
epoch_id: None,
1888+
});
1889+
1890+
let res = kg.key_gen(request).await.unwrap_err();
1891+
assert_eq!(res.code(), tonic::Code::Internal);
1892+
1893+
assert!(res
1894+
.internal_err()
1895+
.to_string()
1896+
.contains(ERR_FAILED_TO_READ_EXISTING_TAG));
1897+
}
1898+
18031899
#[tokio::test]
18041900
async fn sunshine() {
18051901
let (prep_ids, kg) = setup_key_generator::<

0 commit comments

Comments
 (0)