diff --git a/builder/src/evidence.rs b/builder/src/evidence.rs index cfce177..1052208 100644 --- a/builder/src/evidence.rs +++ b/builder/src/evidence.rs @@ -366,6 +366,175 @@ pub fn write_build_evidence( } } +/// Emit evidence v2 for a database built in this invocation. Unlike +/// `attest-existing-layout`, this path has no predecessor and refuses a +/// payload whose params hash was not produced from the scanned v2 layout. +pub fn write_build_evidence_v2( + out_dir: &str, + snapshot: &str, + core_version: &str, + builder_git_commit: &str, + builder_binary: &str, + tee_platform: &str, + tee_image_measurement_hex_or_none: &str, + out_evidence: &str, +) -> ExitCode { + let result = (|| { + let out_dir = Path::new(out_dir); + let snapshot = Path::new(snapshot); + let builder_binary = Path::new(builder_binary); + let out_evidence = Path::new(out_evidence); + let payload_path = out_dir.join("root-bundle-payload.bin"); + let payload_bytes = fs::read(&payload_path) + .map_err(|e| format!("failed to read {}: {e}", payload_path.display()))?; + let payload = rootbundle::RootBundlePayload::decode(&payload_bytes) + .map_err(|e| format!("failed to decode {}: {e}", payload_path.display()))?; + if payload.build_kind != rootbundle::BuildKind::Snapshot { + return Err("full-build v2 evidence only supports snapshot payloads".into()); + } + + let layout = dbpipeline::inspect_existing_onion_layout_v2(out_dir) + .map_err(|e| format!("final Onion layout check failed: {e}"))?; + let layout_fields = BuildEvidence { + version: EVIDENCE_VERSION_V2, + builder_git_commit: String::new(), + builder_binary_sha256: [0u8; 32], + tee_platform: String::new(), + tee_image_measurement: Vec::new(), + core_version: String::new(), + snapshot_sha256: [0u8; 32], + snapshot_bytes: 0, + network_magic: payload.network_magic, + build_kind: payload.build_kind, + from_anchor: payload.from_anchor, + anchor: payload.anchor, + utxo_muhash: payload.utxo_muhash, + dust_threshold_sats: payload.dust_threshold_sats, + max_utxos_per_spk: payload.max_utxos_per_spk, + params_hash: [0u8; 32], + index_bins_per_table: 0, + chunk_bins_per_table: 0, + onion_entry_size: 0, + bucket_super_root: [0u8; 32], + onion_super_root: [0u8; 32], + root_bundle_payload_sha256: [0u8; 32], + signed_root_bundle_sha256: None, + database_manifest_sha256: [0u8; 32], + all_artifacts_manifest_sha256: [0u8; 32], + server_db_manifest_sha256: [0u8; 32], + evidence_mode: 0, + predecessor_evidence_sha256: None, + predecessor_report_sha256: None, + onion_layout_v2: Some(OnionLayoutV2 { + total_packed_entries: layout.total_packed_entries, + index_bins_per_table: layout.index_bins_per_table, + chunk_bins_per_table: layout.chunk_bins_per_table, + }), + } + .with_layout_from_summary(out_dir)?; + if layout_fields.onion_entry_size != layout.entry_size { + return Err(format!( + "build summary Onion entry size {} disagrees with scanner {}", + layout_fields.onion_entry_size, layout.entry_size + )); + } + let params = rootbundle::BuildParamsV2::current_snapshot( + layout_fields.index_bins_per_table, + layout_fields.chunk_bins_per_table, + layout.entry_size, + layout.total_packed_entries, + layout.index_bins_per_table, + layout.chunk_bins_per_table, + ); + if payload.params_hash != params.params_hash() { + return Err( + "root-bundle payload does not contain the canonical full-build v2 params hash" + .into(), + ); + } + + let (snapshot_sha256, snapshot_bytes) = sha256_file(snapshot)?; + let (builder_binary_sha256, _) = sha256_file(builder_binary)?; + let root_bundle_payload_sha256 = sha256_bytes(&payload_bytes); + let signed_root_bundle_sha256 = + optional_sha256_file(&out_dir.join("signed-root-bundle.bin"))?; + let database_manifest_sha256 = sha256_file_32(&out_dir.join("database.manifest.sha256"))?; + let all_artifacts_manifest_sha256 = + sha256_file_32(&out_dir.join("all-artifacts.manifest.sha256"))?; + let server_db_manifest_sha256 = sha256_file_32(&out_dir.join("server-db/MANIFEST.toml"))?; + let bucket_super_root = *payload + .root("merkle/bucket/super_root") + .ok_or("root-bundle payload missing merkle/bucket/super_root")?; + let onion_super_root = *payload + .root("merkle/onion/super_root") + .ok_or("root-bundle payload missing merkle/onion/super_root")?; + if onion_super_root != layout.onion_super_root { + return Err("root-bundle payload Onion root disagrees with scanner".into()); + } + let evidence = BuildEvidence { + version: EVIDENCE_VERSION_V2, + builder_git_commit: builder_git_commit.to_owned(), + builder_binary_sha256, + tee_platform: tee_platform.to_owned(), + tee_image_measurement: parse_optional_hex_bytes( + tee_image_measurement_hex_or_none, + "tee-image-measurement-hex-or-none", + )?, + core_version: core_version.to_owned(), + snapshot_sha256, + snapshot_bytes, + network_magic: payload.network_magic, + build_kind: payload.build_kind, + from_anchor: payload.from_anchor, + anchor: payload.anchor, + utxo_muhash: payload.utxo_muhash, + dust_threshold_sats: payload.dust_threshold_sats, + max_utxos_per_spk: payload.max_utxos_per_spk, + params_hash: params.params_hash(), + index_bins_per_table: layout_fields.index_bins_per_table, + chunk_bins_per_table: layout_fields.chunk_bins_per_table, + onion_entry_size: layout.entry_size, + bucket_super_root, + onion_super_root, + root_bundle_payload_sha256, + signed_root_bundle_sha256, + database_manifest_sha256, + all_artifacts_manifest_sha256, + server_db_manifest_sha256, + evidence_mode: 0, + predecessor_evidence_sha256: None, + predecessor_report_sha256: None, + onion_layout_v2: Some(OnionLayoutV2 { + total_packed_entries: layout.total_packed_entries, + index_bins_per_table: layout.index_bins_per_table, + chunk_bins_per_table: layout.chunk_bins_per_table, + }), + }; + let encoded = evidence.encode()?; + create_new_parent(out_evidence)?; + let mut writer = File::create_new(out_evidence) + .map_err(|e| format!("failed to create {}: {e}", out_evidence.display()))?; + writer + .write_all(&encoded) + .map_err(|e| format!("failed to write {}: {e}", out_evidence.display()))?; + writer + .flush() + .map_err(|e| format!("failed to flush {}: {e}", out_evidence.display()))?; + Ok::<_, String>(evidence) + })(); + + match result { + Ok(evidence) => { + print_evidence_report(&evidence, Some(out_evidence)); + ExitCode::SUCCESS + } + Err(e) => { + eprintln!("error: {e}"); + ExitCode::from(1) + } + } +} + /// Re-seal a completed v1 artifact set as proof v2 without rebuilding the /// database. The checker scans the final Onion tables and Merkle material; /// only after those checks pass does it emit a new payload and evidence file. diff --git a/builder/src/main.rs b/builder/src/main.rs index c4f4b36..b1b2faa 100644 --- a/builder/src/main.rs +++ b/builder/src/main.rs @@ -63,6 +63,12 @@ fn main() -> ExitCode { &args[10], ) } + Some("build-root-bundle-payload-v2") if args.len() == 11 => { + root_payload::build_root_bundle_payload_v2( + &args[2], &args[3], &args[4], &args[5], &args[6], &args[7], &args[8], &args[9], + &args[10], + ) + } Some("build-delta-root-bundle-payload") if args.len() == 12 => { root_payload::build_delta_root_bundle_payload( &args[2], &args[3], &args[4], &args[5], &args[6], &args[7], &args[8], &args[9], @@ -75,6 +81,9 @@ fn main() -> ExitCode { Some("write-build-evidence") if args.len() == 10 => evidence::write_build_evidence( &args[2], &args[3], &args[4], &args[5], &args[6], &args[7], &args[8], &args[9], ), + Some("write-build-evidence-v2") if args.len() == 10 => evidence::write_build_evidence_v2( + &args[2], &args[3], &args[4], &args[5], &args[6], &args[7], &args[8], &args[9], + ), Some("attest-existing-layout") if args.len() == 10 => evidence::attest_existing_layout( &args[2], &args[3], &args[4], &args[5], &args[6], &args[7], &args[8], &args[9], ), @@ -248,9 +257,11 @@ fn usage(bin: &str) { {bin} build-chunk-cuckoo [--anchor ]\n\ {bin} build-bucket-merkle [--root-only]\n\ {bin} build-root-bundle-payload \n\ + {bin} build-root-bundle-payload-v2 \n\ {bin} build-delta-root-bundle-payload \n\ {bin} write-build-receipt \n\ {bin} write-build-evidence \n\ + {bin} write-build-evidence-v2 \n\ {bin} attest-existing-layout \n\ {bin} inspect-build-evidence \n\ {bin} verify-build-evidence [--snapshot ] [--builder-bin ] [--payload ] [--database-manifest ] [--all-artifacts-manifest ] [--server-db-manifest ] [--expected-muhash ] [--expected-anchor-height ] [--expected-anchor-hash ] [--expected-report-data <64-byte-hex>] [--sev-snp-report ]\n\ diff --git a/builder/src/root_payload.rs b/builder/src/root_payload.rs index 1a05342..3c415be 100644 --- a/builder/src/root_payload.rs +++ b/builder/src/root_payload.rs @@ -300,6 +300,96 @@ pub fn build_root_bundle_payload( } } +/// Build a snapshot root payload using the final, scanner-verified Onion v2 +/// layout. This is deliberately a separate command from the v1 builder so +/// roots-only and existing v1 production paths keep their byte-level format. +pub fn build_root_bundle_payload_v2( + out_dir: &str, + network_magic_hex: &str, + chain_anchor_path: &str, + muhash_display_hex: &str, + index_bins_per_table: &str, + chunk_bins_per_table: &str, + onion_entry_size: &str, + issued_at: &str, + out_payload: &str, +) -> ExitCode { + let result = (|| { + let network_magic = parse_hex_array::<4>(network_magic_hex, "network-magic-hex")?; + let chain_anchor = rootbundle::ChainAnchor::load(chain_anchor_path) + .map_err(|e| format!("failed to read chain anchor {chain_anchor_path}: {e}"))?; + let index_bins_per_table = parse_u32_arg(index_bins_per_table, "index-bins-per-table")?; + let chunk_bins_per_table = parse_u32_arg(chunk_bins_per_table, "chunk-bins-per-table")?; + let onion_entry_size = parse_u32_arg(onion_entry_size, "onion-entry-size")?; + let issued_at = parse_i64_arg(issued_at, "issued-at-unix")?; + let out_dir = Path::new(out_dir); + let layout = dbpipeline::inspect_existing_onion_layout_v2(out_dir) + .map_err(|e| format!("final Onion layout check failed: {e}"))?; + if layout.entry_size != onion_entry_size { + return Err(format!( + "onion entry size mismatch: requested {onion_entry_size}, scanner found {}", + layout.entry_size + )); + } + let mut payload = root_bundle_payload_from_dir( + out_dir, + network_magic, + rootbundle::BuildKind::Snapshot, + rootbundle::ChainAnchor { + block_hash: [0u8; 32], + height: 0, + }, + chain_anchor, + muhash_display_hex, + index_bins_per_table, + chunk_bins_per_table, + onion_entry_size, + issued_at, + Vec::new(), + )?; + let params = rootbundle::BuildParamsV2::current_snapshot( + index_bins_per_table, + chunk_bins_per_table, + layout.entry_size, + layout.total_packed_entries, + layout.index_bins_per_table, + layout.chunk_bins_per_table, + ); + payload.params_hash = params.params_hash(); + let (payload_bytes, payload_sha256) = write_payload_file(&payload, Path::new(out_payload))?; + Ok::<_, String>((payload, layout, payload_bytes, payload_sha256)) + })(); + + match result { + Ok((payload, layout, payload_bytes, payload_sha256)) => { + println!("network_magic={}", hex::encode(payload.network_magic)); + println!("anchor_height={}", payload.anchor.height); + println!( + "anchor_hash={}", + display_hash_hex(&payload.anchor.block_hash) + ); + println!("muhash={muhash_display_hex}"); + println!("params_hash={}", hex::encode(payload.params_hash)); + println!("params_version=2"); + println!("onion_total_packed_entries={}", layout.total_packed_entries); + println!("onion_index_bins_per_table={}", layout.index_bins_per_table); + println!("onion_chunk_bins_per_table={}", layout.chunk_bins_per_table); + println!("root_entries={}", payload.roots.len()); + for root in &payload.roots { + println!("root:{}={}", root.label, hex::encode(root.root)); + } + println!("payload_bytes={payload_bytes}"); + println!("payload_sha256={}", hex::encode(payload_sha256)); + println!("payload_path={out_payload}"); + ExitCode::SUCCESS + } + Err(e) => { + eprintln!("error: {e}"); + ExitCode::from(1) + } + } +} + pub fn build_delta_root_bundle_payload( out_dir: &str, network_magic_hex: &str, diff --git a/dbpipeline/src/lib.rs b/dbpipeline/src/lib.rs index de7b7bb..3582980 100644 --- a/dbpipeline/src/lib.rs +++ b/dbpipeline/src/lib.rs @@ -3377,6 +3377,13 @@ fn verify_existing_tree_tops( seen_nodes += count; expected_count = count.div_ceil(arity); } + // A one-bin tree has no level at or above CACHE_FROM_LEVEL. The + // writer encodes an empty cache; its root is already bound by the + // verified ordered roots file rather than repeated here. + let cache_before_root = levels == 0 && total_nodes == 0 && expected_count == 1; + if cache_before_root { + last_root = Some(*root); + } if seen_nodes != total_nodes || last_root.as_ref() != Some(root) || expected_count != 1 { return Err(PipelineError::InvalidExistingOnionLayout(format!( "tree-top record {tree} totals or root mismatch" @@ -5317,6 +5324,29 @@ mod tests { let _ = std::fs::remove_dir_all(dir); } + #[test] + fn existing_onion_layout_v2_accepts_only_empty_single_bin_tree_tops() { + let dir = fresh_temp_dir("existing-onion-v2-empty-tree-top"); + let path = dir.join(ONION_MERKLE_TREE_TOPS_FILENAME); + let roots = vec![[0xa5u8; MERKLE_HASH_SIZE]; INDEX_K + CHUNK_K]; + let mut valid = Vec::new(); + valid.extend_from_slice(&((INDEX_K + CHUNK_K) as u32).to_le_bytes()); + for _ in &roots { + valid.push(ONION_MERKLE_CACHE_FROM_LEVEL as u8); + valid.extend_from_slice(&0u32.to_le_bytes()); + valid.extend_from_slice(&104u16.to_le_bytes()); + valid.push(0); + } + std::fs::write(&path, &valid).unwrap(); + verify_existing_tree_tops(&path, &roots, 1, 1, 104).unwrap(); + + let mut malformed = valid; + malformed[5] = 1; // total_nodes=1 with no cached levels is invalid. + std::fs::write(&path, malformed).unwrap(); + assert!(verify_existing_tree_tops(&path, &roots, 1, 1, 104).is_err()); + let _ = std::fs::remove_dir_all(dir); + } + #[test] fn existing_onion_layout_v2_checks_final_tables_and_roots() { let dir = fresh_temp_dir("existing-onion-v2"); diff --git a/scripts/build-snapshot-database.sh b/scripts/build-snapshot-database.sh index a03b342..0370cdc 100755 --- a/scripts/build-snapshot-database.sh +++ b/scripts/build-snapshot-database.sh @@ -34,6 +34,8 @@ # it does not stage a server-loadable DB. Default 0. # WRITE_BUILD_EVIDENCE 1 to write canonical build-evidence.bin and # build-evidence.report-data. Default 1. +# BUILD_EVIDENCE_VERSION 1 for the legacy roots-only/v1-compatible path; +# 2 for a native full-build v2 snapshot (default 1). # BUILDER_GIT_COMMIT Builder source revision to record. Default current git # HEAD with "-dirty" suffix if tracked files differ. # TEE_PLATFORM Evidence-only platform label, e.g. none, sev-snp, tdx. @@ -362,6 +364,68 @@ stage_server_db() { write_server_db_manifest "$server_dir" } +stage_direct_oram_inputs() { + local out_dir=$1 + local server_dir=$2 + local direct_dir="$out_dir/oram-direct-inputs" + local index_src="$out_dir/utxo_chunks_index_nodust.bin" + local chunk_src="$out_dir/utxo_chunks_nodust.bin" + local index_bytes chunk_bytes index_records chunk_records + + [[ -f "$index_src" ]] || fail "Direct ORAM source missing: $index_src" + [[ -f "$chunk_src" ]] || fail "Direct ORAM source missing: $chunk_src" + [[ ! -e "$direct_dir" ]] || fail "Direct ORAM staging dir already exists: $direct_dir" + mkdir -p "$direct_dir" + link_or_copy "$index_src" "$direct_dir/utxo_chunks_index_nodust.bin" + link_or_copy "$chunk_src" "$direct_dir/utxo_chunks_nodust.bin" + { + printf '%s %s\n' "$(hash_one "$direct_dir/utxo_chunks_index_nodust.bin")" utxo_chunks_index_nodust.bin + printf '%s %s\n' "$(hash_one "$direct_dir/utxo_chunks_nodust.bin")" utxo_chunks_nodust.bin + } > "$direct_dir/direct-inputs.sha256" + chmod 0644 "$direct_dir/direct-inputs.sha256" + + index_bytes=$(wc -c < "$direct_dir/utxo_chunks_index_nodust.bin" | tr -d ' ') + chunk_bytes=$(wc -c < "$direct_dir/utxo_chunks_nodust.bin" | tr -d ' ') + ((index_bytes > 0 && index_bytes % 25 == 0)) || fail "Direct ORAM index must contain 25-byte records" + ((chunk_bytes > 0 && chunk_bytes % 40 == 0)) || fail "Direct ORAM chunk must contain 40-byte records" + index_records=$((index_bytes / 25)) + chunk_records=$((chunk_bytes / 40)) + + local manifest="$server_dir/MANIFEST.toml" + local tmp="$server_dir/.MANIFEST.toml.$$" + grep -qx '\[files\]' "$manifest" || fail "server DB manifest lacks [files] section" + awk \ + -v index_sha256="$(hash_one "$direct_dir/utxo_chunks_index_nodust.bin")" \ + -v index_bytes="$index_bytes" \ + -v index_records="$index_records" \ + -v chunk_sha256="$(hash_one "$direct_dir/utxo_chunks_nodust.bin")" \ + -v chunk_bytes="$chunk_bytes" \ + -v chunk_records="$chunk_records" \ + -v index_slots_per_bin="$DIRECT_ORAM_INDEX_SLOTS_PER_BIN" \ + -v index_hash_fns="$DIRECT_ORAM_INDEX_HASH_FNS" \ + -v index_load_factor_ppb="$DIRECT_ORAM_INDEX_LOAD_FACTOR_PPB" \ + -v index_seed="$DIRECT_ORAM_INDEX_SEED" ' + /^\[files\]$/ { + print "[direct_oram]" + print "version = 1" + print "index_sha256 = \"" index_sha256 "\"" + print "index_bytes = " index_bytes + print "index_records = " index_records + print "chunk_sha256 = \"" chunk_sha256 "\"" + print "chunk_bytes = " chunk_bytes + print "chunk_records = " chunk_records + print "index_slots_per_bin = " index_slots_per_bin + print "index_hash_fns = " index_hash_fns + print "index_load_factor_ppb = " index_load_factor_ppb + print "index_seed = " index_seed + print "" + } + { print } + ' "$manifest" > "$tmp" + mv -f "$tmp" "$manifest" + chmod 0644 "$manifest" +} + diff_manifest_if_requested() { local label=$1 local expected=${2:-} @@ -418,6 +482,7 @@ if is_truthy "$ROOTS_ONLY"; then RUN_ONION_FFI=0 fi WRITE_BUILD_EVIDENCE=${WRITE_BUILD_EVIDENCE:-1} +BUILD_EVIDENCE_VERSION=${BUILD_EVIDENCE_VERSION:-1} BUILDER_GIT_COMMIT=${BUILDER_GIT_COMMIT:-$(current_git_commit)} TEE_PLATFORM=${TEE_PLATFORM:-none} TEE_IMAGE_MEASUREMENT=${TEE_IMAGE_MEASUREMENT:-none} @@ -429,6 +494,22 @@ SERVER_DB_DIR=${SERVER_DB_DIR:-"$OUT_DIR/server-db"} [[ "$PARTITIONS" =~ ^[1-9][0-9]*$ ]] || fail "PARTITIONS must be positive" [[ "$ISSUED_AT" =~ ^-?[0-9]+$ ]] || fail "ISSUED_AT must be an integer" [[ "$PUSH_BATCH_ENTRIES" =~ ^[1-9][0-9]*$ ]] || fail "PUSH_BATCH_ENTRIES must be positive" +[[ "$BUILD_EVIDENCE_VERSION" == "1" || "$BUILD_EVIDENCE_VERSION" == "2" ]] || + fail "BUILD_EVIDENCE_VERSION must be 1 or 2" +if [[ "$BUILD_EVIDENCE_VERSION" == "2" ]] && is_truthy "$ROOTS_ONLY"; then + fail "BUILD_EVIDENCE_VERSION=2 requires a full snapshot build, not ROOTS_ONLY=1" +fi +if [[ "$BUILD_EVIDENCE_VERSION" == "2" ]] && ! is_truthy "$RUN_ONION_FFI"; then + fail "BUILD_EVIDENCE_VERSION=2 requires RUN_ONION_FFI=1 for the final scanner-verified server layout" +fi +DIRECT_ORAM_INDEX_SLOTS_PER_BIN=${DIRECT_ORAM_INDEX_SLOTS_PER_BIN:-4} +DIRECT_ORAM_INDEX_HASH_FNS=${DIRECT_ORAM_INDEX_HASH_FNS:-2} +DIRECT_ORAM_INDEX_LOAD_FACTOR_PPB=${DIRECT_ORAM_INDEX_LOAD_FACTOR_PPB:-950000000} +DIRECT_ORAM_INDEX_SEED=${DIRECT_ORAM_INDEX_SEED:-8030603977422561841} +[[ "$DIRECT_ORAM_INDEX_SLOTS_PER_BIN" =~ ^[1-9][0-9]*$ ]] || fail "DIRECT_ORAM_INDEX_SLOTS_PER_BIN must be positive" +[[ "$DIRECT_ORAM_INDEX_HASH_FNS" =~ ^[1-9][0-9]*$ ]] || fail "DIRECT_ORAM_INDEX_HASH_FNS must be positive" +[[ "$DIRECT_ORAM_INDEX_LOAD_FACTOR_PPB" =~ ^[1-9][0-9]*$ ]] || fail "DIRECT_ORAM_INDEX_LOAD_FACTOR_PPB must be positive" +[[ "$DIRECT_ORAM_INDEX_SEED" =~ ^[0-9]+$ ]] || fail "DIRECT_ORAM_INDEX_SEED must be an integer" ensure_empty_or_absent_dir "$OUT_DIR" LOG_DIR="$OUT_DIR/logs" @@ -448,6 +529,7 @@ ENV_FILE="$OUT_DIR/build.env" printf 'issued_at=%s\n' "$ISSUED_AT" printf 'run_onion_ffi=%s\n' "$RUN_ONION_FFI" printf 'roots_only=%s\n' "$ROOTS_ONLY" + printf 'build_evidence_version=%s\n' "$BUILD_EVIDENCE_VERSION" printf 'builder_git_commit=%s\n' "$BUILDER_GIT_COMMIT" printf 'tee_platform=%s\n' "$TEE_PLATFORM" printf 'tee_image_measurement=%s\n' "$TEE_IMAGE_MEASUREMENT" @@ -594,8 +676,25 @@ chunk_bins=$(kv "$LOG_DIR/04-build-chunk-cuckoo.out" "bins_per_table") [[ -n "$index_bins" ]] || fail "could not parse index bins from 03-build-index-cuckoo.out" [[ -n "$chunk_bins" ]] || fail "could not parse chunk bins from 04-build-chunk-cuckoo.out" +if [[ "$BUILD_EVIDENCE_VERSION" == "2" ]]; then + is_truthy "$STAGE_SERVER_DB" || fail "BUILD_EVIDENCE_VERSION=2 requires STAGE_SERVER_DB=1" + stage_server_db "$OUT_DIR" "$SERVER_DB_DIR" + stage_direct_oram_inputs "$OUT_DIR" "$SERVER_DB_DIR" + printf 'server_db_dir=%s\n' "$SERVER_DB_DIR" | tee -a "$SUMMARY" + printf 'server_db_manifest=%s\n' "$SERVER_DB_DIR/MANIFEST.toml" | tee -a "$SUMMARY" + printf 'server_db_manifest_sha256=%s\n' "$(hash_one "$SERVER_DB_DIR/MANIFEST.toml")" | tee -a "$SUMMARY" + printf 'direct_oram_inputs=%s\n' "$OUT_DIR/oram-direct-inputs" | tee -a "$SUMMARY" +fi + +ROOT_PAYLOAD_COMMAND=build-root-bundle-payload +EVIDENCE_COMMAND=write-build-evidence +if [[ "$BUILD_EVIDENCE_VERSION" == "2" ]]; then + ROOT_PAYLOAD_COMMAND=build-root-bundle-payload-v2 + EVIDENCE_COMMAND=write-build-evidence-v2 +fi + run_step 11-build-root-bundle-payload \ - "$BIN" build-root-bundle-payload \ + "$BIN" "$ROOT_PAYLOAD_COMMAND" \ "$OUT_DIR" \ "$NETWORK_MAGIC" \ "$OUT_DIR/chain_anchor.bin" \ @@ -648,7 +747,7 @@ if is_truthy "$WRITE_RECEIPT"; then "$OUT_DIR/build-receipt.txt" fi -if is_truthy "$STAGE_SERVER_DB"; then +if [[ "$BUILD_EVIDENCE_VERSION" == "1" ]] && is_truthy "$STAGE_SERVER_DB"; then stage_server_db "$OUT_DIR" "$SERVER_DB_DIR" printf 'server_db_dir=%s\n' "$SERVER_DB_DIR" | tee -a "$SUMMARY" printf 'server_db_manifest=%s\n' "$SERVER_DB_DIR/MANIFEST.toml" | tee -a "$SUMMARY" @@ -683,7 +782,7 @@ diff_manifest_if_requested \ if is_truthy "$WRITE_BUILD_EVIDENCE"; then run_step 15-write-build-evidence \ - "$BIN" write-build-evidence \ + "$BIN" "$EVIDENCE_COMMAND" \ "$OUT_DIR" \ "$SNAPSHOT" \ "$CORE_VERSION" \ diff --git a/scripts/local-regtest-e2e.sh b/scripts/local-regtest-e2e.sh index b4bb02b..a4cc850 100755 --- a/scripts/local-regtest-e2e.sh +++ b/scripts/local-regtest-e2e.sh @@ -189,6 +189,8 @@ run_once() { cd "$REPO_ROOT" cargo build -q -p pir-attested-builder BIN=${BIN:-"$REPO_ROOT/target/debug/pir-attested-builder"} +cargo build -q -p onionffi --features ffi +ONIONFFI_BIN=${ONIONFFI_BIN:-"$REPO_ROOT/target/debug/onionffi"} mkdir -p "$WORK" "$LOG_DIR" printf '# deterministic local-regtest-e2e test key\nsecret_seed_hex=%s\n' "$TEST_BUILDER_SEED_HEX" > "$KEY_FILE" @@ -219,6 +221,87 @@ assert_eq "bundle_sha256" "$EXPECTED_BUNDLE_SHA256" "$bundle_sha256" assert_eq "receipt_sha256" "$EXPECTED_RECEIPT_SHA256" "$receipt_sha256" assert_eq "manifest_sha256" "$EXPECTED_MANIFEST_SHA256" "$manifest_sha256" +# Native full-build v2: exercise the snapshot wrapper rather than +# reattest-existing. A host fixture has no SEV device, so this checks the +# complete staging layout and evidence binding up to (but not including) the +# platform quote generated in a measured builder. +V2_DIR="$WORK/full-build-v2" +SNAPSHOT="$FIXTURE" \ +EXPECTED_MUHASH="$EXPECTED_MUHASH" \ +NETWORK_MAGIC="$NETWORK_MAGIC" \ +ANCHOR_HEIGHT="$ANCHOR_HEIGHT" \ +CORE_VERSION="$CORE_VERSION" \ +ONION_ENTRY_SIZE="$ONION_ENTRY_SIZE" \ +ISSUED_AT="$ISSUED_AT" \ +PARTITIONS="$PARTITIONS" \ +OUT_DIR="$V2_DIR" \ +BIN="$BIN" \ +SKIP_CARGO_BUILD=1 \ +RUN_ONION_FFI=1 \ +ONIONFFI_BIN="$ONIONFFI_BIN" \ +WRITE_RECEIPT=0 \ +EMIT_SEV_SNP_QUOTE=0 \ +BUILD_EVIDENCE_VERSION=2 \ +"$REPO_ROOT/scripts/build-snapshot-database.sh" > "$LOG_DIR/full-build-v2.out" + +for rel in \ + root-bundle-payload.bin \ + build-evidence.bin \ + build-evidence.report-data \ + database.manifest.sha256 \ + all-artifacts.manifest.sha256 \ + server-db/MANIFEST.toml \ + oram-direct-inputs/utxo_chunks_index_nodust.bin \ + oram-direct-inputs/utxo_chunks_nodust.bin \ + oram-direct-inputs/direct-inputs.sha256; do + [[ -s "$V2_DIR/$rel" ]] || { + printf 'error: native full-build v2 missing required staging artifact: %s\n' "$rel" >&2 + exit 1 + } +done + +V2_EVIDENCE_LOG="$V2_DIR/logs/15-write-build-evidence.out" +grep -qx 'evidence_version=2' "$V2_EVIDENCE_LOG" +grep -qx 'evidence_mode=full_build' "$V2_EVIDENCE_LOG" +grep -qx 'predecessor_evidence_sha256=none' "$V2_EVIDENCE_LOG" +grep -qx 'predecessor_report_sha256=none' "$V2_EVIDENCE_LOG" + +index_hash=$(hash_one "$V2_DIR/oram-direct-inputs/utxo_chunks_index_nodust.bin") +chunk_hash=$(hash_one "$V2_DIR/oram-direct-inputs/utxo_chunks_nodust.bin") +grep -qx "$index_hash utxo_chunks_index_nodust.bin" "$V2_DIR/oram-direct-inputs/direct-inputs.sha256" +grep -qx "$chunk_hash utxo_chunks_nodust.bin" "$V2_DIR/oram-direct-inputs/direct-inputs.sha256" +grep -qx "index_sha256 = \"$index_hash\"" "$V2_DIR/server-db/MANIFEST.toml" +grep -qx "chunk_sha256 = \"$chunk_hash\"" "$V2_DIR/server-db/MANIFEST.toml" +index_records=$(($(wc -c < "$V2_DIR/oram-direct-inputs/utxo_chunks_index_nodust.bin") / 25)) +chunk_records=$(($(wc -c < "$V2_DIR/oram-direct-inputs/utxo_chunks_nodust.bin") / 40)) +grep -qx "index_records = $index_records" "$V2_DIR/server-db/MANIFEST.toml" +grep -qx "chunk_records = $chunk_records" "$V2_DIR/server-db/MANIFEST.toml" + +run_builder verify-build-evidence \ + "$V2_DIR/build-evidence.bin" \ + --snapshot "$FIXTURE" \ + --builder-bin "$BIN" \ + --payload "$V2_DIR/root-bundle-payload.bin" \ + --database-manifest "$V2_DIR/database.manifest.sha256" \ + --all-artifacts-manifest "$V2_DIR/all-artifacts.manifest.sha256" \ + --server-db-manifest "$V2_DIR/server-db/MANIFEST.toml" \ + > "$LOG_DIR/full-build-v2.verify-evidence.out" + +if SNAPSHOT="$FIXTURE" \ + EXPECTED_MUHASH="$EXPECTED_MUHASH" \ + NETWORK_MAGIC="$NETWORK_MAGIC" \ + ANCHOR_HEIGHT="$ANCHOR_HEIGHT" \ + OUT_DIR="$WORK/rejected-roots-only-v2" \ + BIN="$BIN" \ + SKIP_CARGO_BUILD=1 \ + ROOTS_ONLY=1 \ + BUILD_EVIDENCE_VERSION=2 \ + "$REPO_ROOT/scripts/build-snapshot-database.sh" > "$LOG_DIR/rejected-roots-only-v2.out" 2>&1; then + printf 'error: roots-only mode must not claim native full-build v2 evidence\n' >&2 + exit 1 +fi +grep -q 'BUILD_EVIDENCE_VERSION=2 requires a full snapshot build' "$LOG_DIR/rejected-roots-only-v2.out" + printf 'status=ok\n' printf 'fixture=%s\n' "$FIXTURE" printf 'muhash=%s\n' "$EXPECTED_MUHASH" @@ -230,3 +313,4 @@ printf 'payload_sha256=%s\n' "$payload_sha256" printf 'bundle_sha256=%s\n' "$bundle_sha256" printf 'receipt_sha256=%s\n' "$receipt_sha256" printf 'manifest_sha256=%s\n' "$manifest_sha256" +printf 'native_full_build_v2=%s\n' "$V2_DIR"