Skip to content

Commit cadec3f

Browse files
Verify SHA-256 of contract code matches contract hash when fetching (#2465)
### What Add SHA-256 integrity verification to `get_remote_wasm_from_hash` in `utils::rpc`. After fetching Wasm bytecode, compute `sha256(returned_bytes)` and compare it to the requested hash. Return a clear error with both the expected and computed hashes if they don't match. Add unit tests for matching and mismatched hashes. ### Why The CLI fetched WASM bytecode from RPC servers without verifying the returned bytes matched the requested hash. While most data is trusted from the connected RPC, it's good defensive approach if the CLI verifies the contract code because it gets cached against the hash for future use locally. Close #2463
1 parent b36124d commit cadec3f

File tree

1 file changed

+56
-4
lines changed

1 file changed

+56
-4
lines changed

cmd/soroban-cli/src/utils.rs

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -298,11 +298,31 @@ pub mod rpc {
298298
));
299299
}
300300
let contract_data_entry = &entries[0];
301-
match LedgerEntryData::from_xdr_base64(&contract_data_entry.xdr, Limits::none())? {
302-
LedgerEntryData::ContractCode(xdr::ContractCodeEntry { code, .. }) => Ok(code.into()),
303-
scval => Err(Error::UnexpectedContractCodeDataType(scval)),
304-
}
301+
let code = match LedgerEntryData::from_xdr_base64(&contract_data_entry.xdr, Limits::none())?
302+
{
303+
LedgerEntryData::ContractCode(xdr::ContractCodeEntry { code, .. }) => Vec::from(code),
304+
scval => return Err(Error::UnexpectedContractCodeDataType(scval)),
305+
};
306+
super::verify_wasm_hash(&code, hash)?;
307+
Ok(code)
308+
}
309+
}
310+
311+
// Uses `Error::NotFound` because `soroban_rpc::Error` has no integrity/mismatch
312+
// variant. The message makes the actual failure reason clear.
313+
fn verify_wasm_hash(code: &[u8], expected_hash: &Hash) -> Result<(), soroban_rpc::Error> {
314+
let computed_hash = Hash(Sha256::digest(code).into());
315+
if computed_hash != *expected_hash {
316+
return Err(soroban_rpc::Error::NotFound(
317+
"WASM hash mismatch".to_string(),
318+
format!(
319+
"expected {}, got {}",
320+
hex::encode(expected_hash.0),
321+
hex::encode(computed_hash.0),
322+
),
323+
));
305324
}
325+
Ok(())
306326
}
307327

308328
#[cfg(test)]
@@ -324,4 +344,36 @@ mod tests {
324344
Err(err) => panic!("Failed to parse contract id: {err}"),
325345
}
326346
}
347+
348+
#[test]
349+
fn test_verify_wasm_hash_matching() {
350+
use sha2::{Digest, Sha256};
351+
use stellar_xdr::curr::Hash;
352+
353+
let wasm_bytes = b"\0asm fake wasm content";
354+
let correct_hash = Hash(Sha256::digest(wasm_bytes).into());
355+
assert!(verify_wasm_hash(wasm_bytes, &correct_hash).is_ok());
356+
}
357+
358+
#[test]
359+
fn test_verify_wasm_hash_mismatch() {
360+
use stellar_xdr::curr::Hash;
361+
362+
let wasm_bytes = b"\0asm fake wasm content";
363+
let wrong_hash = Hash([0xAB; 32]);
364+
let err = verify_wasm_hash(wasm_bytes, &wrong_hash).unwrap_err();
365+
let err_msg = err.to_string();
366+
assert!(
367+
err_msg.contains("WASM hash mismatch"),
368+
"expected 'WASM hash mismatch' in error: {err_msg}"
369+
);
370+
assert!(
371+
err_msg.contains("abababababababababababababababababababababababababababababababab"),
372+
"expected expected-hash in error: {err_msg}"
373+
);
374+
assert!(
375+
err_msg.contains("501dc4e05f47c4713c4a27e89a5b07ed769bb2cc858bcf46de9bed13ae65af29"),
376+
"expected computed-hash in error: {err_msg}"
377+
);
378+
}
327379
}

0 commit comments

Comments
 (0)