@@ -22,6 +22,7 @@ use base64::Engine as _;
2222use serde:: Serialize ;
2323use serde_json:: json;
2424use sha2:: Digest ;
25+ use std:: collections:: HashMap ;
2526use std:: str:: FromStr ;
2627
2728/// Data describing a ref-update event to be anchored.
@@ -261,22 +262,32 @@ pub async fn verify_anchor(
261262 tx_id : & str ,
262263 db : & crate :: db:: Db ,
263264) -> Result < VerifyResult > {
264- // Fetch the data item from the bundler gateway.
265- let url = format ! ( "{}/v1/tx/{}" , gateway_url. trim_end_matches( '/' ) , tx_id) ;
265+ // Fetch the data item from the Arweave gateway's data path.
266+ // Gateways serve data at /{tx_id}, not /v1/tx/{id} (which is the bundler API).
267+ let url = format ! ( "{}/{}" , gateway_url. trim_end_matches( '/' ) , tx_id) ;
266268 let resp = client
267269 . get ( & url)
268270 . send ( )
269271 . await
270- . map_err ( |e| anyhow:: anyhow!( "failed to fetch data from bundler gateway: {e}" ) ) ?;
272+ . map_err ( |e| anyhow:: anyhow!( "failed to fetch data from Arweave gateway: {e}" ) ) ?;
271273 if !resp. status ( ) . is_success ( ) {
272274 return Ok ( VerifyResult {
273275 valid : false ,
274276 anchor : serde_json:: Value :: Null ,
275277 certificate : None ,
276- errors : vec ! [ format!( "bundler gateway returned {}" , resp. status( ) ) ] ,
278+ errors : vec ! [ format!( "Arweave gateway returned {}" , resp. status( ) ) ] ,
277279 } ) ;
278280 }
281+ // Bound the untrusted response to 1 MiB to prevent memory exhaustion.
279282 let body_bytes = resp. bytes ( ) . await ?;
283+ if body_bytes. len ( ) > 1_048_576 {
284+ return Ok ( VerifyResult {
285+ valid : false ,
286+ anchor : serde_json:: Value :: Null ,
287+ certificate : None ,
288+ errors : vec ! [ "response body exceeds 1 MiB limit" . to_string( ) ] ,
289+ } ) ;
290+ }
280291
281292 // Parse the payload — could be JSON or raw bytes depending on gateway
282293 let anchor: serde_json:: Value = serde_json:: from_slice ( & body_bytes) ?;
@@ -341,26 +352,132 @@ pub async fn verify_anchor(
341352 errors. push ( format ! ( "certificate signature verification failed: {e}" ) ) ;
342353 }
343354
344- // 2. Verify prev hash linkage against the predecessor at seq - 1
355+ // 2. Verify prev hash linkage against the predecessor at seq - 1.
356+ // Fail closed: a missing declared predecessor is treated as invalid.
345357 if c. seq > 1 {
346- if let Ok ( Some ( pred) ) = db. get_cert_by_seq ( & c. repo_id , c. seq - 1 ) . await {
347- let prev_payload = serde_json:: json!( {
348- "repo_id" : pred. repo_id,
349- "ref" : pred. ref_name,
350- "old" : pred. old_sha,
351- "new" : pred. new_sha,
352- "pusher" : pred. pusher_did,
353- "node" : pred. node_did,
354- "ts" : pred. issued_at,
355- } ) ;
356- let prev_bytes = serde_json:: to_vec ( & prev_payload) ?;
357- let expected_prev = hex:: encode ( sha2:: Sha256 :: digest ( & prev_bytes) ) ;
358- if c. prev != expected_prev {
358+ match db. get_cert_by_seq ( & c. repo_id , c. seq - 1 ) . await {
359+ Ok ( Some ( pred) ) => {
360+ let prev_payload = serde_json:: json!( {
361+ "repo_id" : pred. repo_id,
362+ "ref" : pred. ref_name,
363+ "old" : pred. old_sha,
364+ "new" : pred. new_sha,
365+ "pusher" : pred. pusher_did,
366+ "node" : pred. node_did,
367+ "ts" : pred. issued_at,
368+ } ) ;
369+ let prev_bytes = serde_json:: to_vec ( & prev_payload) ?;
370+ let expected_prev = hex:: encode ( sha2:: Sha256 :: digest ( & prev_bytes) ) ;
371+ if c. prev != expected_prev {
372+ errors. push ( format ! (
373+ "prev hash mismatch: claimed {} expected {}" ,
374+ c. prev, expected_prev
375+ ) ) ;
376+ }
377+ }
378+ Ok ( None ) => {
359379 errors. push ( format ! (
360- "prev hash mismatch: claimed {} expected {}" ,
361- c. prev, expected_prev
380+ "predecessor cert seq {} not found for repo {}" ,
381+ c. seq - 1 ,
382+ c. repo_id
362383 ) ) ;
363384 }
385+ Err ( e) => {
386+ errors. push ( format ! (
387+ "error looking up predecessor seq {}: {e}" ,
388+ c. seq - 1
389+ ) ) ;
390+ }
391+ }
392+ }
393+
394+ // 3. Verify the pusher authorization proof (RFC 9421 HTTP Signature)
395+ // when all required context is available.
396+ if let ( Some ( pusher_sig) , Some ( sig_input) , Some ( content_digest) , Some ( request_path) ) = (
397+ & c. pusher_sig ,
398+ & c. signature_input ,
399+ & c. content_digest ,
400+ & c. request_path ,
401+ ) {
402+ match gitlawb_core:: http_sig:: HttpSignature :: parse (
403+ sig_input,
404+ & format ! ( "sig1=:{pusher_sig}:" ) ,
405+ ) {
406+ Ok ( http_sig) => {
407+ let mut request_values: HashMap < String , String > = HashMap :: new ( ) ;
408+ request_values. insert ( "@method" . to_string ( ) , "POST" . to_string ( ) ) ;
409+ request_values. insert ( "@path" . to_string ( ) , request_path. clone ( ) ) ;
410+ request_values. insert ( "content-digest" . to_string ( ) , content_digest. clone ( ) ) ;
411+
412+ let sig_params_value = sig_input. strip_prefix ( "sig1=" ) . unwrap_or ( sig_input) ;
413+ let components_ref: Vec < & str > =
414+ http_sig. components . iter ( ) . map ( String :: as_str) . collect ( ) ;
415+
416+ match gitlawb_core:: http_sig:: build_signing_string (
417+ & components_ref,
418+ sig_params_value,
419+ & request_values,
420+ ) {
421+ Ok ( signing_string) => {
422+ let pusher_did = gitlawb_core:: did:: Did :: from_str ( & c. pusher_did ) ;
423+ let pusher_vk = pusher_did. and_then ( |d| d. to_verifying_key ( ) ) ;
424+ match pusher_vk {
425+ Ok ( vk) => {
426+ let sig_bytes: [ u8 ; 64 ] =
427+ match base64:: engine:: general_purpose:: STANDARD
428+ . decode ( pusher_sig)
429+ {
430+ Ok ( bytes) => match bytes. as_slice ( ) . try_into ( ) {
431+ Ok ( a) => a,
432+ Err ( _) => {
433+ errors. push (
434+ "pusher signature is not 64 bytes"
435+ . to_string ( ) ,
436+ ) ;
437+ return Ok ( VerifyResult {
438+ valid : false ,
439+ anchor,
440+ certificate : cert,
441+ errors,
442+ } ) ;
443+ }
444+ } ,
445+ Err ( _) => {
446+ errors. push (
447+ "pusher signature is not valid base64"
448+ . to_string ( ) ,
449+ ) ;
450+ return Ok ( VerifyResult {
451+ valid : false ,
452+ anchor,
453+ certificate : cert,
454+ errors,
455+ } ) ;
456+ }
457+ } ;
458+ if let Err ( e) = gitlawb_core:: identity:: verify (
459+ & vk,
460+ signing_string. as_bytes ( ) ,
461+ & sig_bytes,
462+ ) {
463+ errors. push ( format ! (
464+ "pusher signature verification failed: {e}"
465+ ) ) ;
466+ }
467+ }
468+ Err ( e) => {
469+ errors. push ( format ! ( "unresolvable pusher DID: {e}" ) ) ;
470+ }
471+ }
472+ }
473+ Err ( e) => {
474+ errors. push ( format ! ( "failed to build signing string: {e}" ) ) ;
475+ }
476+ }
477+ }
478+ Err ( e) => {
479+ errors. push ( format ! ( "failed to parse pusher Signature-Input: {e}" ) ) ;
480+ }
364481 }
365482 }
366483 } else {
@@ -564,16 +681,14 @@ mod tests {
564681 #[ tokio:: test]
565682 async fn test_verify_anchor_uses_correct_gateway_url ( ) {
566683 let mut server = mockito:: Server :: new_async ( ) . await ;
684+ // Gateways serve data at /{tx_id}, not /v1/tx/{id}.
567685 let _mock = server
568- . mock ( "GET" , "/v1/tx/ does-not-exist" )
686+ . mock ( "GET" , "/does-not-exist" )
569687 . with_status ( 404 )
570688 . create_async ( )
571689 . await ;
572690
573691 let client = reqwest:: Client :: new ( ) ;
574- // verify_anchor needs a real PgPool; this test only exercises that
575- // the function correctly formats the gateway URL and handles a 404.
576- // It will error on the pool access which is expected without a test DB.
577692 let pool = sqlx:: postgres:: PgPoolOptions :: new ( )
578693 . connect_lazy ( "postgres://localhost/gitlawb_test_placeholder" )
579694 . expect ( "lazy pool creation should not fail" ) ;
@@ -582,13 +697,9 @@ mod tests {
582697
583698 match result {
584699 Ok ( r) => {
585- // With a lazy unconnected pool, get_most_recent_cert will fail,
586- // but the function still returns Ok(VerifyResult) with errors.
587700 assert ! ( !r. valid) ;
588701 }
589702 Err ( e) => {
590- // On some systems the POSTGRES connection attempt may abort
591- // rather than fail gracefully.
592703 let msg = e. to_string ( ) ;
593704 assert ! (
594705 msg. contains( "pool" ) || msg. contains( "error" ) ,
0 commit comments