Skip to content

Commit a144310

Browse files
Merge upstream develop
2 parents 7f8a3a5 + 5206eb6 commit a144310

7 files changed

Lines changed: 98 additions & 18 deletions

File tree

.gitlab/images/rust-dind/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ ARG RUST_IMAGE
22

33
# Debian bookworm (the rust base) has no Compose v2 plugin in its repos, so
44
# we copy the binaries from the official docker image, pinned by tag.
5-
FROM docker:28-cli AS docker
5+
FROM docker:29-cli AS docker
66
FROM ${RUST_IMAGE}
77

88
COPY --from=docker /usr/local/bin/docker /usr/local/bin/docker

Cargo.lock

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

packages/react-native/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
"react-native": "./src/index.tsx",
99
"exports": {
1010
".": {
11+
"react-server": "./src/index.web.ts",
12+
"node": "./src/index.web.ts",
1113
"browser": "./src/index.web.ts",
1214
"react-native": "./src/index.tsx",
1315
"source": "./src/index.tsx",

packages/rs-core/src/api.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,10 @@ impl PolycentricCore {
176176
for entry in contents {
177177
let digest = ContentDigest::decode(entry.digest_bytes.as_slice())
178178
.map_err(|e| CoreError::Decode(format!("Failed to decode ContentDigest: {e}")))?;
179-
client.copy_content(&digest, entry.content_bytes);
179+
if let Err(e) = client.copy_content(&digest, entry.content_bytes) {
180+
// Skip content that doesn't match its digest; keep the rest.
181+
crate::logging::log_warn(|| format!("dropping content: {e}"));
182+
}
180183
}
181184
Ok(())
182185
}

packages/rs-core/src/client.rs

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ use polycentric_common::{
33
models::{
44
Serializable, collections,
55
protos_v2::{
6-
self, Content, ContentDigest, Event, EventBundle, EventProof, Identity, PublicKey,
7-
SerializedContent, SignedEvent, VectorClock, content::ContentBody,
6+
self, Content, ContentDigest, ContentDigestType, Event, EventBundle, EventProof,
7+
Identity, PublicKey, SerializedContent, SignedEvent, VectorClock, content::ContentBody,
88
},
99
},
1010
};
@@ -22,6 +22,17 @@ fn hex_short(bytes: &[u8]) -> String {
2222
bytes.iter().take(4).map(|b| format!("{:02x}", b)).collect()
2323
}
2424

25+
/// True when `content_bytes` actually hash to `digest`. Only SHA-256 digests
26+
/// are supported; any other type fails verification (can't verify ⇒ reject).
27+
fn content_matches_digest(digest: &ContentDigest, content_bytes: &[u8]) -> bool {
28+
use sha2::{Digest, Sha256};
29+
30+
if digest.r#type != ContentDigestType::Sha256 as i32 {
31+
return false;
32+
}
33+
Sha256::digest(content_bytes).as_slice() == digest.value.as_slice()
34+
}
35+
2536
#[derive(Default)]
2637
pub struct PolycentricClient {
2738
servers: Mutex<Vec<String>>,
@@ -50,9 +61,22 @@ impl PolycentricClient {
5061
self.event_store.insert(signed_event)
5162
}
5263

53-
/// Copy content bytes into the content store, keyed by digest.
54-
pub fn copy_content(&mut self, digest: &ContentDigest, content_bytes: Vec<u8>) {
64+
/// Copy content bytes into the content store, keyed by digest, after
65+
/// verifying the bytes hash to that digest. Mismatched (or
66+
/// unsupported-digest) content is rejected and not stored.
67+
pub fn copy_content(
68+
&mut self,
69+
digest: &ContentDigest,
70+
content_bytes: Vec<u8>,
71+
) -> Result<(), CoreError> {
72+
if !content_matches_digest(digest, &content_bytes) {
73+
let digest_hex: String = digest.value.iter().map(|b| format!("{b:02x}")).collect();
74+
return Err(CoreError::InvalidEvent(format!(
75+
"content does not match digest {digest_hex}"
76+
)));
77+
}
5578
self.content_store.insert(digest, content_bytes);
79+
Ok(())
5680
}
5781

5882
/// First locally-valid bundle at `(identity, collection, sequence)`.
@@ -130,8 +154,11 @@ impl PolycentricClient {
130154
});
131155

132156
for (signed_event, event, serialized, proofs) in prepared {
133-
if let (Some(digest), Some(content)) = (event.content_digest.as_ref(), serialized) {
134-
self.copy_content(digest, content.content_bytes);
157+
if let (Some(digest), Some(content)) = (event.content_digest.as_ref(), serialized)
158+
&& let Err(e) = self.copy_content(digest, content.content_bytes)
159+
{
160+
// Keep the validly-signed event; just drop the bad content.
161+
crate::logging::log_warn(|| format!("dropping content: {e}"));
135162
}
136163
if !proofs.is_empty()
137164
&& let Ok(key) = EventKey::from_event(event)
@@ -754,7 +781,9 @@ mod tests {
754781
}
755782
}
756783

757-
client.copy_content(&digest, content_bytes);
784+
client
785+
.copy_content(&digest, content_bytes)
786+
.expect("content matches its computed digest");
758787
let signed = sign_event(
759788
signer,
760789
&id_string,
@@ -1197,4 +1226,48 @@ mod tests {
11971226
vc.err()
11981227
);
11991228
}
1229+
1230+
#[test]
1231+
fn copy_content_stores_matching_content() {
1232+
let mut client = PolycentricClient::default();
1233+
let bytes = b"hello content".to_vec();
1234+
let digest = sha256_digest(&bytes);
1235+
1236+
client
1237+
.copy_content(&digest, bytes.clone())
1238+
.expect("content matching its digest is accepted");
1239+
1240+
let stored = client.find_content_from_digest(&digest);
1241+
assert_eq!(stored.map(|c| c.content_bytes), Some(bytes));
1242+
}
1243+
1244+
#[test]
1245+
fn copy_content_rejects_mismatched_content() {
1246+
let mut client = PolycentricClient::default();
1247+
// Digest is computed over different bytes than we try to insert.
1248+
let digest = sha256_digest(b"the real content");
1249+
let tampered = b"tampered content".to_vec();
1250+
1251+
assert!(
1252+
client.copy_content(&digest, tampered).is_err(),
1253+
"content that does not hash to its digest must be rejected"
1254+
);
1255+
// Rejected content is never stored.
1256+
assert!(client.find_content_from_digest(&digest).is_none());
1257+
}
1258+
1259+
#[test]
1260+
fn copy_content_rejects_unsupported_digest_type() {
1261+
let mut client = PolycentricClient::default();
1262+
let bytes = b"hello content".to_vec();
1263+
// Correct hash value, but a digest type we can't verify.
1264+
let mut digest = sha256_digest(&bytes);
1265+
digest.r#type = ContentDigestType::Sha256 as i32 + 1;
1266+
1267+
assert!(
1268+
client.copy_content(&digest, bytes).is_err(),
1269+
"unsupported digest types must be rejected"
1270+
);
1271+
assert!(client.find_content_from_digest(&digest).is_none());
1272+
}
12001273
}

pnpm-lock.yaml

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

services/server/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ tonic = { workspace = true, features = ["router", "transport"] }
2828
tonic-prost.workspace = true
2929
tonic-reflection = "0.14"
3030
tonic-web = "0.14"
31-
tower-http = { version = "0.6", features = ["cors"] }
31+
tower-http = { version = "0.7", features = ["cors"] }
3232
tower-layer = "0.3"
3333
http = "1"
3434

3535
[dev-dependencies]
3636
mockito = "1"
37+
tonic-prost-build = "*"
3738

0 commit comments

Comments
 (0)