Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ insert_final_newline = false
# localnet verify_quote call; the text rules do not apply.
[crates/tee-verifier/tests/fixtures/*.borsh]
end_of_line = unset
insert_final_newline = false
# `unset`, not `false`: the last byte is whatever the encoded collateral ends
# with, so asserting either way breaks on the next fixture regeneration.
insert_final_newline = unset
trim_trailing_whitespace = false
charset = unset
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,9 @@ jobs:
if: github.ref == 'refs/heads/main'
run: nix develop --command cargo near build reproducible-wasm --manifest-path crates/contract/Cargo.toml

- name: Build tee-verifier
run: nix develop --command cargo make build-tee-verifier-optimized

- name: Build test-parallel-contract
run: nix develop --command cargo make build-test-parallel-contract-optimized

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions Makefile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ args = ["scripts/check-sandbox-image-version.sh"]

# These build tasks are the single source of truth for both local and CI builds.
# CI's `mpc-e2e-tests` job invokes them via `cargo make`.
# All three are skipped when `E2E_SKIP_BUILD` is set (used by `e2e-tests-skip-build`).
# Each is skipped when `E2E_SKIP_BUILD` is set (used by `e2e-tests-skip-build`).

[tasks.build-mpc-node-network-hardship-simulation]
description = "Build the mpc-node binary used by the E2E tests"
Expand Down Expand Up @@ -202,7 +202,11 @@ args = [
]

[tasks.build-tee-verifier-optimized]
description = "Build the tee-verifier WASM for localnet"
description = "Build the tee-verifier WASM for localnet and the E2E tests"
# `scripts/launch-localnet.sh` tells operators to run this task by hand; with
# `E2E_SKIP_BUILD` exported it no-ops (cargo-make logs "Skipping Task"), and the E2E
# loader's panic names this task so a missing WASM points back here.
condition = { env_not_set = ["E2E_SKIP_BUILD"] }
command = "cargo"
args = [
"near",
Expand Down Expand Up @@ -252,6 +256,7 @@ private = true
dependencies = [
"build-mpc-node-network-hardship-simulation",
"build-mpc-contract-optimized",
"build-tee-verifier-optimized",
"build-test-parallel-contract-optimized",
"build-backup-cli",
]
Expand All @@ -272,6 +277,7 @@ args = [
[tasks._run-e2e-logic.env]
MPC_CONTRACT_WASM = "${CARGO_MAKE_WORKING_DIRECTORY}/target/near/mpc_contract/mpc_contract.wasm"
MPC_PARALLEL_CONTRACT_WASM = "${CARGO_MAKE_WORKING_DIRECTORY}/target/near/test_parallel_contract/test_parallel_contract.wasm"
MPC_TEE_VERIFIER_WASM = "${CARGO_MAKE_WORKING_DIRECTORY}/target/near/tee_verifier/tee_verifier.wasm"

# Build the mpc-node binary and both contract WASMs, then run the E2E tests.
[tasks.e2e-tests]
Expand Down
6 changes: 5 additions & 1 deletion crates/attestation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ edition = { workspace = true }
[features]
borsh-schema = ["borsh/unstable__schema", "tee-verifier-interface/borsh-schema"]
dstack-conversions = ["dep:dstack-sdk-types"]
test-utils = []
test-utils = ["allow-pre-launch-script"]
# Accepts an app-compose carrying a `pre_launch_script`, which production rejects as
# arbitrary root code. The test fixture needs one: it is how the node's in-enclave signer
# key is exported during collection. `init_script` and `bash_script` stay rejected.
allow-pre-launch-script = []
# Pulls in `dcap-qvl` for full local DCAP + post-DCAP verification. Meant for
# off-chain callers; `mpc-contract` enables it today.
# TODO(#3264): contract drops this once DCAP moves to the verifier contract.
Expand Down
18 changes: 9 additions & 9 deletions crates/attestation/assets/tcb_info.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
"rtmr0": "e673be2f70beefb70b48a6109eed4715d7270d4683b3bf356fa25fafbf1aa76e39e9127e6e688ccda98bdab1d4d47f46",
"rtmr1": "b598fde9491427341bc4683b75d10d3e36770af3a36a6954d8b6b7b22aa66358f13e1f172e51b7d6e6710d99a8d8532f",
"rtmr2": "c812d42bfff1c75382e91a37c867ab117b97eb5e8d6797488928ea38e5fd38b5ed2f87d9613d392507f1c3af94657c93",
"rtmr3": "b7662ac19c27af648a939be042684bbdb43bb3dddf4cd17bb21f4d455ab1926c6ee57038152fc46ddea392c47eb2af27",
"rtmr3": "8050d4769527c84cb066078854ef8eb50ea7b150e072eeec7007240aca01b556041273ea3d20a1b1d48736f650ce13f0",
"os_image_hash": "7d47512fda31dc5a7318f72ae1869a3c76323981eea21fc30cafd0f79668642c",
"compose_hash": "cb9b2d6204f5e44238b75f69e3a3069550734c0d99ebdd3be507c238a261d8fa",
"compose_hash": "c6775c4d9bb7273ca21cd54be6b6a1879328dfafbeb550f6957b821e212f0d4a",
"device_id": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"app_compose": "{\n \"manifest_version\": 2,\n \"name\": \"mpc-localnet-one-node-1774945636\",\n \"runner\": \"docker-compose\",\n \"docker_compose_file\": \"version: '3.8'\\n\\nservices:\\n launcher:\\n image: nearone/mpc-launcher@sha256:1f54b55bad22c45067228a9262bc6377e393ca1a07edb64e691e80704f49b74e\\n\\n container_name: launcher\\n\\n environment:\\n - PLATFORM=TEE\\n - DOCKER_CONTENT_TRUST=1\\n - DEFAULT_IMAGE_DIGEST=sha256:6a5700fccbb3facddd1f3934f4976c4dcefc176c4aac28cd2fd035984b368980\\n\\n volumes:\\n - /var/run/docker.sock:/var/run/docker.sock\\n - /var/run/dstack.sock:/var/run/dstack.sock\\n - /tapp:/tapp:ro\\n - shared-volume:/mnt/shared:rw\\n\\n security_opt:\\n - no-new-privileges:true\\n\\n read_only: true\\n\\n tmpfs:\\n - /tmp\\n\\nvolumes:\\n shared-volume:\\n name: shared-volume\\n\",\n \"kms_enabled\": false,\n \"gateway_enabled\": false,\n \"local_key_provider_enabled\": true,\n \"key_provider_id\": \"\",\n \"public_logs\": true,\n \"public_sysinfo\": true,\n \"allowed_envs\": [],\n \"no_instance_id\": true,\n \"secure_time\": false\n}",
"app_compose": "{\n \"manifest_version\": 2,\n \"name\": \"mpc-localnet-one-node-1786102851\",\n \"runner\": \"docker-compose\",\n \"docker_compose_file\": \"version: '3.8'\\n\\nservices:\\n launcher:\\n image: nearone/mpc-launcher@sha256:5618a93a78c9ac9173e7ebf7c8af173bd675be6832a2f8c2a9a7149ac2678f54\\n\\n container_name: launcher\\n\\n environment:\\n - PLATFORM=TEE\\n - DOCKER_CONTENT_TRUST=1\\n - DEFAULT_IMAGE_DIGEST=sha256:564a5aebc33495d5610626d23cb9e3da8e9d531d0edff9e38aa1ccaf059c15d3\\n\\n volumes:\\n - /var/run/docker.sock:/var/run/docker.sock\\n - /var/run/dstack.sock:/var/run/dstack.sock\\n - /tapp:/tapp:ro\\n - shared-volume:/mnt/shared:rw\\n\\n security_opt:\\n - no-new-privileges:true\\n\\n read_only: true\\n\\n tmpfs:\\n - /tmp\\n\\nvolumes:\\n shared-volume:\\n name: shared-volume\\n\",\n \"kms_enabled\": false,\n \"gateway_enabled\": false,\n \"local_key_provider_enabled\": true,\n \"key_provider_id\": \"\",\n \"public_logs\": true,\n \"public_sysinfo\": true,\n \"allowed_envs\": [],\n \"no_instance_id\": true,\n \"secure_time\": false,\n \"pre_launch_script\": \"#!/bin/sh\\n# Test-asset collection hook, sourced by the CVM's app-compose.sh before the\\n# node starts. The node generates its NEAR signer key in-enclave, so exporting\\n# that key is the only way to check the fixture keypair into the repo next to\\n# the quote it is bound to (near/mpc #3787).\\n#\\n# The key appears only after the node starts, i.e. after this hook returns, so\\n# the wait runs as its own systemd unit: a plain background process would be\\n# reaped with app-compose.service's cgroup. The copy target is the CVM's\\n# host-shared dir, which the host sees as run/vm/<id>/shared/.\\n#\\n# The guest userland is BusyBox, so the wait loop sticks to shell built-ins and\\n# globs (no find/head, whose GNU options BusyBox rejects).\\n#\\n# Only ever use this on a throwaway localnet CVM: it exports a private key.\\n\\nCONSOLE=/dev/console\\nSHARED=/dstack/.host-shared\\nEXFIL=/etc/fixture-exfil.sh\\n\\ncat > \\\"$EXFIL\\\" <<'EOF'\\n#!/bin/sh\\nCONSOLE=/dev/console\\nSHARED=/dstack/.host-shared\\ni=0\\nwhile [ \\\"$i\\\" -lt 900 ]; do\\n for f in /var/lib/docker/volumes/*/_data/secrets.json; do\\n if [ -f \\\"$f\\\" ]; then\\n cp \\\"$f\\\" \\\"$SHARED/fixture-secrets.json\\\" 2>/dev/null\\n {\\n echo \\\"FIXTURE-SECRETS-BEGIN $f\\\"\\n cat \\\"$f\\\"\\n echo \\\"\\\"\\n echo \\\"FIXTURE-SECRETS-END\\\"\\n } > \\\"$CONSOLE\\\" 2>&1\\n exit 0\\n fi\\n done\\n i=$((i + 1))\\n sleep 2\\ndone\\necho \\\"FIXTURE-SECRETS-TIMEOUT\\\" > \\\"$CONSOLE\\\" 2>&1\\nEOF\\nchmod +x \\\"$EXFIL\\\" 2>/dev/null\\n\\ncat > /etc/systemd/system/fixture-exfil.service <<'EOF'\\n[Unit]\\nDescription=Export the MPC node signer key for test-asset collection\\n[Service]\\nType=oneshot\\nExecStart=/bin/sh /etc/fixture-exfil.sh\\nStandardOutput=journal+console\\nStandardError=journal+console\\nEOF\\n\\nsystemctl daemon-reload >/dev/null 2>&1\\nsystemctl start --no-block fixture-exfil.service >/dev/null 2>&1\\n\\n{\\n echo \\\"FIXTURE-DIAG-BEGIN\\\"\\n echo \\\"exfil_script=$([ -f \\\"$EXFIL\\\" ] && echo present || echo missing)\\\"\\n echo \\\"unit_state=$(systemctl is-active fixture-exfil.service 2>&1)\\\"\\n echo \\\"shared_writable=$([ -w \\\"$SHARED\\\" ] && echo yes || echo no)\\\"\\n echo \\\"FIXTURE-DIAG-END\\\"\\n} > \\\"$CONSOLE\\\" 2>&1\\n\"\n}",
"event_log": [
{
"imr": 0,
Expand Down Expand Up @@ -159,16 +159,16 @@
{
"imr": 3,
"event_type": 134217729,
"digest": "8f4219433b1fbb548e8e3c9c0a308a09888871da6b0472f8b2057d14846b33f66d674ebaa5829f26e9ffb3d2745eb5ce",
"digest": "970c96ea5f1ea46650645363f1716fd4e4260c92f614a6af308d4ca2e352b4dbd3d8f326e5a513c20b3029163284257e",
"event": "app-id",
"event_payload": "cb9b2d6204f5e44238b75f69e3a3069550734c0d"
"event_payload": "c6775c4d9bb7273ca21cd54be6b6a1879328dfaf"
},
{
"imr": 3,
"event_type": 134217729,
"digest": "f53085ad730605df556bbe4617e942a89186705897eac8974656be41ddcc516a08c4d2b9135fbf8a0f25836c26e1c0d3",
"digest": "6d4e926546694d6e8acc53d46b216cde12d3a60cd3b60550ab7db7f4920fc42c304f8af924e08f96c108a7f7076cf02a",
"event": "compose-hash",
"event_payload": "cb9b2d6204f5e44238b75f69e3a3069550734c0d99ebdd3be507c238a261d8fa"
"event_payload": "c6775c4d9bb7273ca21cd54be6b6a1879328dfafbeb550f6957b821e212f0d4a"
},
{
"imr": 3,
Expand Down Expand Up @@ -208,9 +208,9 @@
{
"imr": 3,
"event_type": 134217729,
"digest": "9aed81f5b1af85f768ef6873ed6f997f55f37de951cca18f5daa35890ab9e5573314d2e0cd188a6913dd4ab6f5455678",
"digest": "0405a5706797115cdcccb66841205179fa5c2d989ad42fee2e2fab4c5b52bb6d9aa9bf8a86755d37e8566fdaabebc468",
"event": "mpc-image-digest",
"event_payload": "6a5700fccbb3facddd1f3934f4976c4dcefc176c4aac28cd2fd035984b368980"
"event_payload": "564a5aebc33495d5610626d23cb9e3da8e9d531d0edff9e38aa1ccaf059c15d3"
}
]
}
79 changes: 72 additions & 7 deletions crates/attestation/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,11 +409,19 @@ impl DstackAttestation {
&& app_compose.local_key_provider_enabled
&& app_compose.allowed_envs.is_empty()
&& app_compose.no_instance_id
// Reject all three arbitrary-root-code fields. `pre_launch_script` and `init_script` run
// unconditionally; `bash_script` only runs when `runner == "bash"` (so the runner pin
// above already neutralizes it), but we reject it explicitly so the guarantee does not
// silently depend on that pin.
&& app_compose.pre_launch_script.is_none()
&& Self::scripts_absent(app_compose)
}

/// Rejects the arbitrary-root-code fields. `bash_script` only runs when
/// `runner == "bash"`, but is rejected explicitly so the guarantee does not depend on
/// the runner pin above.
fn scripts_absent(app_compose: &AppCompose) -> bool {
Self::scripts_absent_with(app_compose, cfg!(feature = "allow-pre-launch-script"))
}

/// Takes the policy as an argument so tests can assert both, whatever features are on.
fn scripts_absent_with(app_compose: &AppCompose, allow_pre_launch_script: bool) -> bool {
(allow_pre_launch_script || app_compose.pre_launch_script.is_none())
&& app_compose.init_script.is_none()
&& app_compose.bash_script.is_none()
}
Expand Down Expand Up @@ -620,19 +628,76 @@ mod tests {
}

#[test]
fn validate_app_compose_config__rejects_present_pre_launch_script() {
fn scripts_absent_with__should_reject_pre_launch_script_when_disallowed() {
// Asserts the production policy, which test builds relax for the fixture.

// Given
let app_compose = AppCompose {
pre_launch_script: Some("echo pwn".to_string()),
..valid_app_compose()
};
// When
let result = DstackAttestation::validate_app_compose_config(&app_compose);
let result = DstackAttestation::scripts_absent_with(&app_compose, false);

// Then
assert!(!result)
}

#[test]
fn validate_app_compose_config__should_follow_the_compiled_in_pre_launch_policy() {
// Covers the wired-in policy at the call site, which the tests below reach
// only through the helper.

// Given
let app_compose = AppCompose {
pre_launch_script: Some("echo collecting fixtures".to_string()),
..valid_app_compose()
};

// When
let result = DstackAttestation::validate_app_compose_config(&app_compose);

// Then
assert_eq!(result, cfg!(feature = "allow-pre-launch-script"));
}

#[test]
fn scripts_absent_with__should_accept_pre_launch_script_when_allowed() {
// Given
let app_compose = AppCompose {
pre_launch_script: Some("echo collecting fixtures".to_string()),
..valid_app_compose()
};
// When
let result = DstackAttestation::scripts_absent_with(&app_compose, true);

// Then
assert!(result)
}

#[test]
fn scripts_absent_with__should_reject_other_scripts_when_pre_launch_is_allowed() {
// The relaxation must stay scoped to `pre_launch_script`.

// Given
let with_init = AppCompose {
init_script: Some("echo pwn".to_string()),
..valid_app_compose()
};
let with_bash = AppCompose {
bash_script: Some("echo pwn".to_string()),
..valid_app_compose()
};

// When
let init_result = DstackAttestation::scripts_absent_with(&with_init, true);
let bash_result = DstackAttestation::scripts_absent_with(&with_bash, true);

// Then
assert!(!init_result);
assert!(!bash_result);
}

#[test]
fn validate_app_compose_config__rejects_present_init_script() {
// `init_script` is arbitrary root code run before dockerd. It is
Expand Down
3 changes: 3 additions & 0 deletions crates/contract/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ bench-contract-methods = []
# length of a fan-out queue). Distinct from `bench-contract-methods` because these are
# behavioral hooks, not gas-measurement hooks.
sandbox-test-methods = []
# For the sandbox wasm of tests that submit the Dstack fixture and expect it to verify;
# see `attestation/allow-pre-launch-script`.
sandbox-test-attestation = ["mpc-attestation/allow-pre-launch-script"]
dev-utils = ["rand", "threshold-signatures", "near-mpc-contract-interface/blstrs"]
abi = [
"borsh/unstable__schema",
Expand Down
16 changes: 16 additions & 0 deletions crates/contract/tests/sandbox/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ impl SandboxTestSetup {
number_of_participants: PARTICIPANT_LEN,
init_config: None,
with_sandbox_test_methods: false,
with_sandbox_test_attestation: false,
}
}

Expand All @@ -203,6 +204,7 @@ pub struct SandboxTestSetupBuilder {
number_of_participants: usize,
init_config: Option<dtos::InitConfig>,
with_sandbox_test_methods: bool,
with_sandbox_test_attestation: bool,
}

impl SandboxTestSetupBuilder {
Expand Down Expand Up @@ -234,9 +236,23 @@ impl SandboxTestSetupBuilder {
self
}

/// Deploys the wasm built with `--features sandbox-test-attestation`, which accepts the
/// attestation fixture's app-compose. Required by tests that submit the Dstack fixture
/// and expect it to verify.
pub fn with_sandbox_test_attestation(mut self) -> Self {
self.with_sandbox_test_attestation = true;
self
}

pub async fn build(self) -> SandboxTestSetup {
assert!(
!(self.with_sandbox_test_methods && self.with_sandbox_test_attestation),
"no wasm is built with both test feature sets; add one if a test needs it"
);
let (worker, contract) = if self.with_sandbox_test_methods {
init_with_wasm(contract_build::current_contract_with_sandbox_test_methods()).await
} else if self.with_sandbox_test_attestation {
init_with_wasm(contract_build::current_contract_with_sandbox_test_attestation()).await
} else {
init().await
};
Expand Down
Loading