chore(test-utils): regenerate attestation fixtures with a retained signer key - #4109
Conversation
|
@claude review |
This comment was marked as outdated.
This comment was marked as outdated.
|
@claude review |
This comment was marked as outdated.
This comment was marked as outdated.
ca0eac4 to
6d48d11
Compare
…tion The MPC node generates its NEAR signer key inside the enclave and writes it only to the CVM's encrypted disk. Collecting test assets needs that key alongside the quote it is bound to, and an app-compose pre-launch script is the only way in: the dev image ships no sshd and the guest agent serves only node info and container logs. vmm-cli already supports --prelaunch-script; this just plumbs an optional PRELAUNCH_SCRIPT through. Attestation verification rejects any app-compose carrying a script, so a CVM deployed this way cannot join a network - the deploy guide, default.env and the script itself now say so loudly.
Collecting the attestation fixtures requires a pre-launch script to export the node's in-enclave signer key, which makes the resulting app-compose one that production verification rejects as arbitrary root code. The new allow-pre-launch-script feature relaxes exactly that field for test builds; init_script and bash_script stay rejected, and the production wasm never enables it. The check takes the policy as an argument so the strict behaviour keeps its unit test in test builds too, and a further test pins the compiled-in policy at the call site.
…ner key Regenerated from a fresh localnet CVM on a TDX host, keeping the node's NEAR signer key this time so tests can sign submit_participant_info as the fixture node. The quote's report_data binds that key and the contract reads it from the transaction signer, so without it the Verified store path is unreachable (#3787). create-assets.sh had been producing collateral our own parser cannot read since the tee-verifier-interface refactor: nodes serialize the DER and signature fields as JSON byte arrays while collateral_from_json expects hex strings. It now hex-encodes those fields, and strips the NUL that terminates the PEM chains so the fixture stays valid PEM. The regeneration reminder is unconditional, since the committed key is normally present but stale. .editorconfig no longer asserts the borsh fixture's final byte: it is whatever the encoded collateral ends with.
… released artifacts Implying allow-pre-launch-script from test-utils meant anything wanting only the collateral_from_json parser also disabled the arbitrary-root-code check. The feature is now named on the dependency edges that build tests, so every enablement is greppable, and a new check fails the build if it reaches the feature graph of the contract wasm, the node, attestation-cli (which verifies attestations locally) or the verifier wasm. Two tests replace one that could not fail: the committed keypair is checked against near_account_public_key.pub, since create-assets.sh rewrites the public key but cannot rewrite the secret; and the fixture app-compose is pinned as satisfying the production policy once its export hook is removed, which is the only place real attestation data meets the strict check. Also drops doc claims about sandbox tests and a contract feature that live in the follow-up, and records why the signer key has to be exported rather than supplied to the node.
Replaces the helper that took the compile-time policy as a runtime argument with `PRE_LAUNCH_SCRIPT_ALLOWED`, so every app-compose invariant reads inline in `validate_app_compose_config`. Drops the relaxation from this crate's own dev-dependency edge, so its unit tests compile the production check. The two tests that only existed to pass both values of the former argument go with it; CI builds `--all-features`, so the strict branch was never compiled there anyway, and the feature-leak guard is what keeps the relaxation out of released artifacts.
Multi-path updates replace the eight per-field pipeline stages, the hex alphabet is bound once, and splitting on the NUL replaces the trim loop. Records why the two committed fixtures encode the same bytes differently: `public_data.json` is the endpoint response verbatim, so its byte fields are JSON arrays, while the fixture parser reads hex.
Restores the Given/When/Then split in the fixture test, and stops the localnet runbook from restating the collateral's `nextUpdate` date, which goes stale on regeneration.
Removes `scripts/check-attestation-feature-leak.sh` and its `check-all-fast` task. The comments that named it as the control now state the invariant it checked: the relaxation is enabled only on dependency edges that no released artifact builds. Also corrects the `local-verify` comment, which claimed `mpc-contract` enables it; the off-chain callers are the node, the attestation CLI and the TEE authority.
Adds `export-signer-key-prelaunch.sh`, the `PRELAUNCH_SCRIPT` that exports the node's in-enclave signer key, so the extraction is reproducible instead of recoverable only from the fixture's measured app-compose. `single-node.sh` now resolves a relative `PRELAUNCH_SCRIPT` against the repo root: `deploy-launcher.sh` reads it from its own directory, so the documented path was looked up two directories away and the deploy failed. The runbook drops the steps duplicated from the asset README and gains the paths the exported key actually lands in.
|
#[cfg(all(target_arch = "wasm32", feature = "allow-pre-launch-script"))]
compile_error!("never enable in a wasm build");Same for |
gilcu3
left a comment
There was a problem hiding this comment.
Added some minor comments and suggestions. Main concern, already expressed on slack, is that it is tricky to avoid including this escape hatch in prod images in the future. There is apparently an alternative by modifying the launcher docker compose that could achieve the same (extracting the near key) without having to change anything in the verifier. If that's correct, it makes sense to try that solution as it would avoid the foot gun here.
| jq -r 'def tohex: | ||
| if type == "array" then "0123456789abcdef" as $h | ||
| | map($h[(. / 16 | floor):(. / 16 | floor) + 1] + $h[(. % 16):(. % 16) + 1]) | join("") | ||
| else . end; | ||
| def strip_nul: if type == "string" then split("\u0000")[0] else . end; |
There was a problem hiding this comment.
I would try to avoid this jq scripting if possible. Can we do something about this?
There was a problem hiding this comment.
In this file we lost a couple of hex values and now have arrays. Should we try and get the opposite? Was this a change in dstack?
There was a problem hiding this comment.
(...) Was this a change in dstack?
Yeah, good question. I was trying to figure that out before asking for a review, but I couldn’t find any specific commit or PR where it changed. :(
| [crates/tee-verifier/tests/fixtures/*.borsh] | ||
| end_of_line = unset | ||
| insert_final_newline = false | ||
| insert_final_newline = unset |
There was a problem hiding this comment.
If you revert it, you'll get this error:
rates/tee-verifier/tests/fixtures/verify_quote_args.borsh:
No final newline expected
1 errors found
Explained by Claude:
The reason is a one-byte coincidence:
main's borsh fixture happens to end with00, soinsert_final_newline = falsewas satisfiable there. Today's regenerated fixture ends with0a, and the checker reads that trailing byte as a final newline. Since it's encoded payload, "fixing" it would corrupt the fixture.
Verification hashes the launcher compose and never reads it, so a service that prints the node's `secrets.json` needs nothing relaxed. That replaces the app-compose `pre_launch_script`, which production rejects as arbitrary root code, so `allow-pre-launch-script` and its plumbing are gone from all five crates and the check is unconditional again — with the rejection test the feature had made impossible. Fixtures recaptured on a TDX host with the collection compose. The app-compose now carries no scripts at all, which a new test asserts. The contract derives the one legal compose hash from a template compiled into it, so the fixture's compose is not derivable and five contract tests register its hash directly. The accommodation is test data rather than a policy switch in shared code, so nothing can leak into a released artifact. Also drops three test names from the asset runbook that no longer exist, in favour of the crates that consume the fixtures.
|
@claude review |
This comment was marked as outdated.
This comment was marked as outdated.
Comparing the second half of the secret key only read back the public key it already carries, so a malformed seed passed. Deriving it proves the pair. Also notes in the asset runbook that the committed key is test-only, since scanners flagging it will otherwise keep raising the question.
The rust-launcher README pointed readers at step 3 of the asset regeneration steps, whose command is relative to the directory step 1 changes into, and the curl snippet used $AGENT_PORT as if single-node.sh exported it. The test command also omitted tee-authority, which consumes the fixture.
|
@gilcu3 @haiyuechen-nearone @barakeinav1 could you please re-review? Thanks! |
The test command runs tests in those crates rather than the crates themselves, and "the collection variant" named a concept the single-node readme never introduces.
| /// Test-only: allows one more compose hash for an already-allowed launcher. The attestation | ||
| /// fixture is captured from a CVM whose launcher compose carries a key-export service, so | ||
| /// [`get_docker_compose_hash`] cannot derive its hash. | ||
| #[cfg(test)] |
Needed by #4110, stacked on top: its sandbox tests submit a real attestation and must sign as the fixture node, which is impossible without the node's signer secret key.
The attestation fixtures in
crates/test-utils/assets/shipped the fixture node's public keys but not that key: it was never committed when they were captured, and the node it came from is long gone. Since the quote'sreport_databinds it, no test could sign as that node. The whole set is regenerated from a fresh capture, withnear_account_secret_keycommitted alongside it and the verifier's expected values updated to match.Exporting the key needs a
pre_launch_script, which production verification rejects, soattestation/allow-pre-launch-scriptrelaxes that one field where test code is built. The hook is committed asexport-signer-key-prelaunch.shso the capture can be repeated.The rest is collection tooling and docs.
Closes #4111