sign_csr stamps an app-info certificate extension whose device_id and os_image_hash the issuer never verified

Abierto
#1,289 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
55/100
Tipo de issue
Error
Claridad
Bastante claro
Estado de actividad
Activo
Stack tecnológico
rust

Línea de trabajo

Start in ra-tls/src/cert.rs at CaCert::sign_csr and compare its callers with kms/src/main_service.rs and the guest-agent call site. Trace decode_app_info_ex and the verified AppInfo path in dstack-attest/src/attestation.rs, then add a regression test showing that rewriting attestation.config cannot change the KMS-stamped extension; done means the KMS uses verified AppInfo while the local CA retains current behavior.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

Label: BUG (demonstrated) — mechanism reproduced by running it. End-to-end exploitation through a live KMS is argued from source, not demonstrated (no TDX hardware here).

Base: origin/next @ 030fbb2183. All cited files are byte-identical to origin/next.

What the chain claims

ra-tls/src/attestation.rs:12-18:

Application identity asserted by a KMS-issued certificate is an issuer claim and must be consumed only after normal certificate-chain verification.

The reader's contract is: once the chain verifies, the fields in the PHALA_RATLS_APP_INFO extension (OID 1.3.6.1.4.1.62397.1.9) are what the issuer verified.

What it enforces

ra-tls/src/cert.rs:99-103:

let app_info = if cfg.ext_app_info {
    Some(csr.attestation.clone().into_v1().decode_app_info(false)?)
} else {
    None
};

decode_app_info here is AttestationV1::decode_app_info_ex (dstack-attest/src/attestation.rs:917-1026) — the decoder that runs before verification. Two of its fields are not the ones the issuer checked:

  • device_iddstack-attest/src/attestation.rs:945 is literally device_id: sha256(Vec::<u8>::new()).to_vec(), i.e. the constant e3b0c442…, identical for every guest on every platform. The verified decoder at :2003 uses sha256(PPID) recovered from the DCAP-validated PCK chain.
  • os_image_hash:990-993 reads it out of self.stack.config(), the free-form config string the attestation carries. Nothing in verify_with_time (:2336-2407) inspects config. The KMS separately verifies request.vm_config, which is a different field (kms/src/main_service.rs:363-369 and :323-324).

cfg.ext_app_info lives inside the CSR and is chosen by the requester (ra-tls/src/cert.rs:156). It is set in production by dstack-util/src/system_setup.rs:613 and :632 (the gateway registration certificate) and by guest-agent/src/rpc_service.rs:512 (ext_app_info: request.with_app_info).

The right pattern is in the same statement — kms/src/main_service.rs:564-566:

let cert = app_ca.sign_csr(&csr, Some(&app_info.boot_info.app_id), "app:custom")?;

PHALA_RATLS_APP_ID gets the verified app_id. PHALA_RATLS_APP_INFO gets a fresh unverified re-decode.

Demonstrated

Scratch crate outside the repo, path-depending on ra-tls and cc-eventlog, driving the checked-in sdk/simulator/attestation.bin through CaCert::sign_csr(..., ext_app_info: true) and reading the extension back with CertExt::get_app_info() — the same call ra-rpc/src/rocket_helper.rs:524 makes. Only attestation.stack.config was rewritten; the quote and the runtime events (and therefore the RTMR3 replay and the report_data binding) were left untouched.

--- unmodified simulator attestation ---
  app_id        = 5bb4ff9a3837357f19dc176407a5709c62eb6c56
  compose_hash  = c143116fef3940e7f776bfc0919c256232ae5338a6e948c96b711e0b2383483a
  os_image_hash = e61be43dd1cc6a6f5edefd7c51b608983780839b0000ce60dd99737dcffe09b9
  device_id     = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
--- attestation.config rewritten ---
  app_id        = 5bb4ff9a3837357f19dc176407a5709c62eb6c56
  compose_hash  = c143116fef3940e7f776bfc0919c256232ae5338a6e948c96b711e0b2383483a
  os_image_hash = deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef
  device_id     = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

sha256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Not demonstrated: that a production KMS signs such a CSR. That needs a TDX quote over sha512("ratls-cert:" || SPKI) for a key the requester holds, and there is no TDX hardware on this machine. The argument is that sign_cert reads cfg.ext_app_info straight off the attacker's CSR (cert.rs:98-99) and that verify_with_ra_pubkey never looks at config.

Reachability

  • Who can trigger it: any tenant able to obtain a TDX quote bound to a key it holds — i.e. anyone with a deployed dstack app, via Tappd.TdxQuote / IssueCert on its own guest agent.
  • What credential it needs: a registered app and a whitelisted compose hash. No operator or KMS access.
  • Who controls frequency: the requester, unbounded.

What an attacker gains

A KMS-signed certificate asserting an os_image_hash the KMS never verified, plus a device_id that is a constant.

The reachable consumer is ra-rpc/src/rocket_helper.rs:523-524gateway/src/main_service.rs:2686-2694, which prefers the certificate extension over the live attestation, → gateway/src/main_service/auth_client.rs:23-33, which POSTs the whole AppInfo to a policy URL. Gateway auth ships enabled = false (gateway/gateway.toml:36-37), so in the default configuration there is no deployed decision behind those fields. That is why this is filed as a design issue rather than an advisory — but the extension is documented as an issuer claim and is exactly the thing a future policy backend would read.

(The gateway's preference for the extension over live attestation at main_service.rs:2686-2694 was introduced by 92f61ecf66 "gw: Support for non-quote RATLS". It is the same shape as GHSA-9fcv-r8wv-c65j, which is marked closed; the code still does it. Noted here as the reachability context, not as a separate ask.)

Steelman

sign_csr is a CaCert method shared by the KMS and by the guest agent's local CA. Only the KMS has a verified AppInfo; the local CA has none, and a CaCert signature cannot take a parameter only one caller can supply without changing the type. Re-decoding is the obvious way to make one function serve both, and cert-client/src/lib.rs:26-47 spells out that exact reasoning for the app_id half — correctly, because there the CSR and the CA are the same principal. The problem is generalising it to the KMS case, where they are not.

device_id has a second, independent justification: the PPID is not in the TD report, it is recovered from the PCK certificate during quote verification, so a guest genuinely cannot compute its own. Returning sha256("") keeps the struct total. Publishing it in a certificate is what turns a placeholder into an assertion.

Improvement directions

  1. Cheap, no wire change, matches the existing convention. Give sign_csr an optional verified &AppInfo and prefer it when present, mirroring the app_id: Option<&[u8]> parameter already in the signature. The KMS passes app_info.boot_info; the local CA passes None and keeps today's behaviour. One parameter, two call sites, plus a test asserting that rewriting attestation.config no longer changes the stamped extension.
  2. Cheap, narrower, composable with (1). Omit device_id from the extension when it is the unverified constant rather than publishing sha256("") as an identity. Consumers already tolerate absent fields — the msgpack-named encoding at cert.rs:385-392 exists for that.
  3. Deployment event — do not start here. Stop carrying AppInfo in a certificate and make consumers read the attestation extension and verify it. That is where ra-tls/src/attestation.rs:12-18 and ra-rpc/src/ratls_client_verifier.rs:14-22 already point, but it changes what every gateway and every peer reads, and needs a migration across gateway/, dstack-util/ and the SDKs.

(1) is the one that fits the tree's conventions and is testable without hardware.

Lenguaje dominante
Rust
Estrellas
546
Forks
96
Merge medio
19 h 22 min
PR fusionados (30 d)
109

Guía de contribución

Abrir la guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de Dstack-TEE/dstack

Todos los issues de Dstack-TEE/dstack

Issues similares

Más issues de Rust

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.