Skip to content

Commit eac4cf5

Browse files
keytrans: Support multiple auditors
1 parent d557675 commit eac4cf5

File tree

13 files changed

+162
-107
lines changed

13 files changed

+162
-107
lines changed

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,6 @@ futures-util = "0.3"
122122
ghash = "0.5.0"
123123
heck = "0.5"
124124
hex = "0.4"
125-
hex-literal = "0.4.1"
126125
hickory-proto = "0.24.1"
127126
hkdf = "0.12"
128127
hmac = "0.12.0"

RELEASE_NOTES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@ v0.71.0
1515
- net: Futures returned by ChatConnection.send() will now return more specific errors on failure
1616

1717
- New SVR2 enclaves for staging and production.
18+
19+
- keytrans: Support multiple auditors

rust/bridge/shared/testing/src/keytrans.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ use std::time::SystemTime;
88
use const_str::hex;
99
use libsignal_bridge_macros::*;
1010
use libsignal_core::Aci;
11-
use libsignal_keytrans::{StoredAccountData, StoredMonitoringData, StoredTreeHead, TreeHead};
11+
use libsignal_keytrans::{
12+
Signature, StoredAccountData, StoredMonitoringData, StoredTreeHead, TreeHead,
13+
};
1214
use libsignal_net::keytrans::SearchResult;
1315
use libsignal_protocol::IdentityKey;
1416
use uuid::Uuid;
@@ -28,7 +30,10 @@ fn TESTING_ChatSearchResult() -> SearchResult {
2830
tree_head: Some(TreeHead {
2931
tree_size: 42,
3032
timestamp: 42424242,
31-
signature: vec![1, 2, 3],
33+
signatures: vec![Signature {
34+
auditor_public_key: vec![1, 2, 3],
35+
signature: vec![4, 5, 6],
36+
}],
3237
}),
3338
root: vec![42; 32],
3439
});

rust/keytrans/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ prost-build = { workspace = true }
2828
assert_matches = { workspace = true }
2929
const-str = { workspace = true }
3030
criterion = { workspace = true }
31-
hex = { workspace = true }
3231
proptest = { workspace = true }
3332
test-case = { workspace = true }
3433
uuid = { workspace = true }

rust/keytrans/benches/verify.rs

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,55 +8,52 @@ use std::time::{Duration, SystemTime};
88
use const_str::hex;
99
use criterion::{criterion_group, criterion_main, Criterion};
1010
use libsignal_keytrans::{
11-
CondensedTreeSearchResponse, DeploymentMode, FullSearchResponse, FullTreeHead, KeyTransparency,
12-
PublicConfig, SearchContext, SlimSearchRequest, VerifyingKey, VrfPublicKey,
11+
ChatSearchResponse, DeploymentMode, FullSearchResponse, KeyTransparency, PublicConfig,
12+
SearchContext, SlimSearchRequest, VerifyingKey, VrfPublicKey,
1313
};
1414
use prost::Message as _;
1515

1616
fn bench_verify_search(c: &mut Criterion) {
1717
let sig_key = VerifyingKey::from_bytes(&hex!(
18-
"12a21ad60d5a3978e19a3b0baa8c35c55a20e10d45f39e5cb34bf6e1b3cce432"
18+
"ac0de1fd7f33552bbeb6ebc12b9d4ea10bf5f025c45073d3fb5f5648955a749e"
1919
))
2020
.unwrap();
2121
let vrf_key = VrfPublicKey::try_from(hex!(
22-
"1e71563470c1b8a6e0aadf280b6aa96f8ad064674e69b80292ee46d1ab655fcf"
22+
"ec3a268237cf5c47115cf222405d5f90cc633ebe05caf82c0dd5acf9d341dadb"
2323
))
2424
.unwrap();
2525
let auditor_key = VerifyingKey::from_bytes(&hex!(
2626
"1123b13ee32479ae6af5739e5d687b51559abf7684120511f68cde7a21a0e755"
2727
))
2828
.unwrap();
29-
let aci = uuid::uuid!("84fd7196-b3fa-4d4d-bbf8-8f1cdf2b7cea");
30-
let request = SlimSearchRequest {
31-
search_key: [b"a", aci.as_bytes().as_slice()].concat(),
32-
version: None,
33-
};
34-
let condensed_response = {
35-
let bytes = include_bytes!("../res/kt-search-response-condensed.dat");
36-
CondensedTreeSearchResponse::decode(bytes.as_slice()).unwrap()
29+
let aci = uuid::uuid!("90c979fd-eab4-4a08-b6da-69dedeab9b29");
30+
let request = SlimSearchRequest::new([b"a", aci.as_bytes().as_slice()].concat());
31+
32+
let ChatSearchResponse {
33+
tree_head: response_tree_head,
34+
aci: condensed_response,
35+
e164: _,
36+
username_hash: _,
37+
} = {
38+
let bytes = include_bytes!("../res/chat_search_response.dat");
39+
let mut response =
40+
ChatSearchResponse::decode(bytes.as_slice()).expect("can decode chat response");
41+
42+
if let Some(head) = response.tree_head.as_mut() {
43+
// we don't expect these fields to be present in the verification that follows
44+
head.distinguished = vec![];
45+
head.last = vec![];
46+
}
47+
48+
response
3749
};
38-
let response_tree_head = FullTreeHead::decode(
39-
hex!([
40-
"0a4c08f23710bbd4dfb897321a40385a",
41-
"2eee61b2a0ef463251e8f0301389c3a3",
42-
"34a0146bc6f2cb9b35938d9c16ba9922",
43-
"3a651e963fab86e64e02484e49b5718d",
44-
"d826aafe7c3e38dfe53226220603224e",
45-
"0a4c08f23710e1d4e0b897321a40a973",
46-
"dd2f6a412287f93b051bd7a5da9dc99b",
47-
"61d86db8a25c861934e00ee6895097b5",
48-
"5272f5f71de8b610b5da0b49fc263e0c",
49-
"5e33cd3de26d3a9f98fd5d2aae06"
50-
])
51-
.as_slice(),
52-
)
53-
.expect("valid test full tree head");
50+
let response_tree_head = response_tree_head.as_ref().expect("has tree head");
5451
let response = FullSearchResponse {
55-
condensed: condensed_response,
56-
tree_head: &response_tree_head,
52+
condensed: condensed_response.expect("has ACI condensed response"),
53+
tree_head: response_tree_head,
5754
};
5855

59-
let valid_at = SystemTime::UNIX_EPOCH + Duration::from_secs(1724279958);
56+
let valid_at = SystemTime::UNIX_EPOCH + Duration::from_secs(1746042060);
6057
let kt = KeyTransparency {
6158
config: PublicConfig {
6259
mode: DeploymentMode::ThirdPartyAuditing(auditor_key),
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../net/tests/data/chat_search_response.dat
-105 KB
Binary file not shown.

rust/keytrans/src/lib.rs

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ use std::time::SystemTime;
2020

2121
pub use ed25519_dalek::VerifyingKey;
2222
pub use proto::{
23-
ChatMonitorResponse, CondensedTreeSearchResponse,
24-
DistinguishedResponse as ChatDistinguishedResponse, FullTreeHead, MonitorKey, MonitorProof,
25-
MonitorRequest, MonitorResponse, SearchResponse as ChatSearchResponse, StoredAccountData,
26-
StoredMonitoringData, StoredTreeHead, TreeHead, UpdateRequest, UpdateResponse,
23+
AuditorTreeHead as SingleSignatureTreeHead, ChatMonitorResponse, CondensedTreeSearchResponse,
24+
DistinguishedResponse as ChatDistinguishedResponse, FullAuditorTreeHead, FullTreeHead,
25+
MonitorKey, MonitorProof, MonitorRequest, MonitorResponse,
26+
SearchResponse as ChatSearchResponse, Signature, StoredAccountData, StoredMonitoringData,
27+
StoredTreeHead, TreeHead, UpdateRequest, UpdateResponse,
2728
};
2829
pub use verify::Error;
2930
use verify::{verify_distinguished, verify_monitor, verify_search};
@@ -342,3 +343,50 @@ impl From<AccountData> for StoredAccountData {
342343
}
343344
}
344345
}
346+
347+
impl TreeHead {
348+
/// Takes a tree head with multiple signatures and turns it into a more
349+
/// conventional single-signature tree head struct which happens to be the
350+
/// AuditorTreeHead a.k.a. SingleSignatureTreeHead.
351+
/// The selection of the correct signature is based on the auditor public key
352+
/// available from the config.
353+
///
354+
/// Not all key transparency deployment modes have an auditor key, and it is
355+
/// possible that the source tree head will not contain the matching one. In
356+
/// both cases the function will return `None`, deciding whether to treat it an
357+
/// error or not is left to the caller.
358+
pub fn to_single_signature_tree_head(
359+
&self,
360+
config: &PublicConfig,
361+
) -> Option<SingleSignatureTreeHead> {
362+
let TreeHead {
363+
tree_size,
364+
timestamp,
365+
signatures,
366+
} = self;
367+
config
368+
.mode
369+
.get_associated_key()
370+
.and_then(|auditor_key| {
371+
signatures.iter().find(|sig| {
372+
sig.auditor_public_key.as_slice() == auditor_key.as_bytes().as_slice()
373+
})
374+
})
375+
.map(|signature| SingleSignatureTreeHead {
376+
tree_size: *tree_size,
377+
timestamp: *timestamp,
378+
signature: signature.signature.clone(),
379+
})
380+
}
381+
}
382+
383+
impl FullTreeHead {
384+
pub fn select_auditor_tree_head(
385+
&self,
386+
public_key: &VerifyingKey,
387+
) -> Option<&FullAuditorTreeHead> {
388+
self.full_auditor_tree_heads
389+
.iter()
390+
.find(|full_head| full_head.public_key.as_slice() == public_key.as_bytes().as_slice())
391+
}
392+
}

rust/keytrans/src/proto/wire.proto

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,35 @@ message PrefixProof {
1010
uint32 counter = 2;
1111
}
1212

13-
// TreeHead contains the operator's signature on the most recent version of the
13+
// AuditorTreeHead contains an auditor's signature on its most recent view of the log.
14+
message AuditorTreeHead {
15+
uint64 tree_size = 1;
16+
int64 timestamp = 2;
17+
bytes signature = 3;
18+
}
19+
20+
// TreeHead contains the key transparency service operator's signature on the most recent version of the
1421
// log.
1522
message TreeHead {
1623
uint64 tree_size = 1;
1724
int64 timestamp = 2;
18-
bytes signature = 3;
25+
// The key transparency service operator provides one Signature object per auditor.
26+
repeated Signature signatures = 3;
27+
}
28+
29+
// The signature incorporates the auditor public key so the service provides one signature per auditor.
30+
message Signature {
31+
bytes auditor_public_key = 1;
32+
bytes signature = 2;
1933
}
2034

21-
// AuditorTreeHead is provided to end-users when third-party auditing is used,
35+
// FullAuditorTreeHead is provided to end-users when third-party auditing is used,
2236
// as evidence that the log is behaving honestly.
23-
message AuditorTreeHead {
24-
TreeHead tree_head = 1;
37+
message FullAuditorTreeHead {
38+
AuditorTreeHead tree_head = 1;
2539
optional bytes root_value = 2;
2640
repeated bytes consistency = 3;
41+
bytes public_key = 4;
2742
}
2843

2944
// FullTreeHead wraps a basic TreeHead with additional information that may be
@@ -32,7 +47,7 @@ message FullTreeHead {
3247
TreeHead tree_head = 1;
3348
repeated bytes last = 2;
3449
repeated bytes distinguished = 3;
35-
optional AuditorTreeHead auditor_tree_head = 4;
50+
repeated FullAuditorTreeHead full_auditor_tree_heads = 4;
3651
}
3752

3853
// ProofStep is the output of one step of a binary search through the log.
@@ -126,4 +141,4 @@ message MonitorResponse {
126141
FullTreeHead tree_head = 1;
127142
repeated MonitorProof proofs = 2;
128143
repeated bytes inclusion = 4;
129-
}
144+
}

0 commit comments

Comments
 (0)