Skip to content

Commit 19106b3

Browse files
authored
Merge pull request #1062 from tnull/2026-08-fix-vss-entropy-source
Prevent VSS encryption nonce reuse
2 parents 2cebf35 + c67d7c0 commit 19106b3

1 file changed

Lines changed: 124 additions & 13 deletions

File tree

src/io/vss_store.rs

Lines changed: 124 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,6 @@ impl VssStore {
111111
let mut entropy_seed = [0u8; 32];
112112
getrandom::fill(&mut entropy_seed).expect("Failed to generate random bytes");
113113
let entropy_source = RandomBytes::new(entropy_seed);
114-
let setup_entropy_source = RandomBytes::new(entropy_seed);
115114

116115
let setup_retry_policy = retry_policy();
117116
let setup_client = VssClient::new_with_headers(
@@ -126,19 +125,22 @@ impl VssStore {
126125

127126
let setup_store_id = store_id.clone();
128127
let runtime_handle = internal_runtime.handle().clone();
129-
let schema_version = std::thread::spawn(move || {
130-
runtime_handle.block_on(async {
131-
determine_and_write_schema_version(
132-
&setup_client,
133-
&setup_store_id,
134-
data_encryption_key,
135-
&setup_key_obfuscator,
136-
&setup_entropy_source,
137-
)
138-
.await
139-
})
128+
let schema_version = std::thread::scope(|scope| {
129+
scope
130+
.spawn(|| {
131+
runtime_handle.block_on(async {
132+
determine_and_write_schema_version(
133+
&setup_client,
134+
&setup_store_id,
135+
data_encryption_key,
136+
&setup_key_obfuscator,
137+
&entropy_source,
138+
)
139+
.await
140+
})
141+
})
142+
.join()
140143
})
141-
.join()
142144
.map_err(|_| io::Error::new(io::ErrorKind::Other, "VSS schema setup task panicked"))??;
143145

144146
let inner = Arc::new(VssStoreInner::new(
@@ -1111,6 +1113,115 @@ impl VssStoreBuilder {
11111113
}
11121114
}
11131115

1116+
#[cfg(test)]
1117+
mod nonce_tests {
1118+
use std::io::{Read, Write};
1119+
use std::net::{TcpListener, TcpStream};
1120+
1121+
use vss_client::types::{ErrorCode, ErrorResponse, ListKeyVersionsResponse, PutObjectResponse};
1122+
1123+
use super::*;
1124+
1125+
fn read_request(stream: &mut TcpStream) -> (String, Vec<u8>) {
1126+
let mut request = Vec::new();
1127+
let header_end = loop {
1128+
let mut buffer = [0; 1024];
1129+
let bytes_read = stream.read(&mut buffer).unwrap();
1130+
assert!(bytes_read > 0, "HTTP request ended before its headers");
1131+
request.extend_from_slice(&buffer[..bytes_read]);
1132+
if let Some(header_end) = request.windows(4).position(|w| w == b"\r\n\r\n") {
1133+
break header_end + 4;
1134+
}
1135+
};
1136+
1137+
let headers = std::str::from_utf8(&request[..header_end]).unwrap();
1138+
let path = headers.split_whitespace().nth(1).unwrap().to_string();
1139+
let content_length = headers
1140+
.lines()
1141+
.filter_map(|line| line.split_once(':'))
1142+
.find(|(name, _)| name.eq_ignore_ascii_case("content-length"))
1143+
.map(|(_, value)| value.trim().parse::<usize>().unwrap())
1144+
.unwrap_or(0);
1145+
while request.len() < header_end + content_length {
1146+
let mut buffer = [0; 1024];
1147+
let bytes_read = stream.read(&mut buffer).unwrap();
1148+
assert!(bytes_read > 0, "HTTP request ended before its body");
1149+
request.extend_from_slice(&buffer[..bytes_read]);
1150+
}
1151+
1152+
(path, request[header_end..header_end + content_length].to_vec())
1153+
}
1154+
1155+
fn write_response(stream: &mut TcpStream, status: &str, body: Vec<u8>) {
1156+
let headers = format!(
1157+
"HTTP/1.1 {status}\r\ncontent-length: {}\r\nvss-protocol-version: 0\r\nconnection: close\r\n\r\n",
1158+
body.len()
1159+
);
1160+
stream.write_all(headers.as_bytes()).unwrap();
1161+
stream.write_all(&body).unwrap();
1162+
}
1163+
1164+
fn serve_empty_vss(listener: TcpListener) -> Vec<PutObjectRequest> {
1165+
let mut put_requests = Vec::new();
1166+
while put_requests.len() < 2 {
1167+
let (mut stream, _) = listener.accept().unwrap();
1168+
let (path, body) = read_request(&mut stream);
1169+
match path.as_str() {
1170+
"/getObject" => {
1171+
let response = ErrorResponse {
1172+
error_code: ErrorCode::NoSuchKeyException.into(),
1173+
message: "missing".to_string(),
1174+
};
1175+
write_response(&mut stream, "404 Not Found", response.encode_to_vec());
1176+
},
1177+
"/listKeyVersions" => write_response(
1178+
&mut stream,
1179+
"200 OK",
1180+
ListKeyVersionsResponse::default().encode_to_vec(),
1181+
),
1182+
"/putObjects" => {
1183+
put_requests.push(PutObjectRequest::decode(&body[..]).unwrap());
1184+
write_response(
1185+
&mut stream,
1186+
"200 OK",
1187+
PutObjectResponse::default().encode_to_vec(),
1188+
);
1189+
},
1190+
_ => panic!("unexpected VSS endpoint: {path}"),
1191+
}
1192+
}
1193+
put_requests
1194+
}
1195+
1196+
fn nonce(request: &PutObjectRequest) -> Vec<u8> {
1197+
let storable = Storable::decode(&request.transaction_items[0].value[..]).unwrap();
1198+
storable.encryption_metadata.unwrap().nonce
1199+
}
1200+
1201+
#[tokio::test]
1202+
async fn schema_and_main_writes_use_unique_nonces() {
1203+
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1204+
let base_url = format!("http://{}", listener.local_addr().unwrap());
1205+
let server = std::thread::spawn(move || serve_empty_vss(listener));
1206+
1207+
let store = VssStore::new(
1208+
base_url,
1209+
"store".to_string(),
1210+
[42; 32],
1211+
Arc::new(FixedHeaders::new(HashMap::new())),
1212+
)
1213+
.unwrap();
1214+
KVStore::write(&store, "namespace", "", "key", vec![42]).await.unwrap();
1215+
1216+
let put_requests = server.join().unwrap();
1217+
assert_ne!(
1218+
nonce(&put_requests[0]),
1219+
nonce(&put_requests[1]),
1220+
"schema setup and the first store write reused an encryption nonce"
1221+
);
1222+
}
1223+
}
1224+
11141225
#[cfg(test)]
11151226
#[cfg(vss_test)]
11161227
mod tests {

0 commit comments

Comments
 (0)