@@ -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 ) ]
2637pub 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}
0 commit comments