Skip to content

refactor: mint self-issued RA-TLS client certificates for KMS calls - #1108

Draft
kvinwang wants to merge 3 commits into
feat/ratls-self-signed-client-certsfrom
feat/ratls-guest-self-signed
Draft

refactor: mint self-issued RA-TLS client certificates for KMS calls#1108
kvinwang wants to merge 3 commits into
feat/ratls-self-signed-client-certsfrom
feat/ratls-guest-self-signed

Conversation

@kvinwang

@kvinwang kvinwang commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

refactor: mint self-issued RA-TLS client certificates for KMS calls

Client-side half of #1106, for both callers: guests at boot, and KMS↔KMS onboarding.
Stacked on #1106 — review that first. Draft: targets 0.6.1 or later, not 0.6.0 (see
Version requirement).

Problem

A client cannot present an RA-TLS certificate to the KMS without first asking the KMS for
a CA private key:

  • guests: request_app_keys_from_kms_url calls GetTempCaCert and mints from it;
  • onboarding: onboard_service.rs does the same before GetKmsKey.

Both do it purely so rustls' pinned-CA check passes. #1106 removed that pin — the KMS
authenticates the attestation inside the certificate and ignores the issuer — so the fetch
is now pure ceremony. It is not free:

  • a CA private key that authenticates nobody is fetched at boot, persisted into
    .appkeys.json (system_setup.rs:2557-2558), and reused for the CVM's lifetime
    (cert-client/src/lib.rs:79);
  • it looks like a secret and is not one, which is why it keeps getting reported
    (#561,
    #619);
  • generate_ra_cert takes a fresh TDX quote on every CertRequestClient::create, to
    produce a certificate whose issuer nobody checks.

Fix

generate_self_signed_ra_cert mints the same certificate without a CA. The quote's
report_data binds the certificate's own SPKI, which is the whole identity.

Onboarding gets its own self-signing path rather than reusing that helper, because the KMS
takes its quote through the guest agent (app_attest) rather than ra_tls's direct quote
path — its attestation has to carry the agent's app info.

The KMS root CA now comes from GetMeta instead of GetTempCaCert. Same trust posture as
before, not a new one: learned over the same unauthenticated connection, pinned
immediately afterwards by verify_key_provider_id (system_setup.rs:2202-2215) against
the measured app_compose.key_provider_id, before setup_fs writes keys or mounts the
data disk. GetMetaResponse is byte-identical across every 0.5.x release and next.

Removing onboarding's GetTempCaCert call also removed the round trip that its
cert_validator needed in order to capture the source KMS's attestation — the validator
fires on a response, not on client construction, and ensure_kms_allowed ran before the
key fetch. Rather than re-add a call purely for that side effect, onboarding now runs over
a single client that carries both the client certificate and the validator, and
re-checks the source against local policy after GetKmsKey instead of before it.

That is a net improvement over what is there today, not just a wash. RaClient::new_mtls
— onboarding's only user, now removed — set no cert_validator, and
try_validate_attestation returns Ok(()) immediately when there is none
(client.rs:132-134). So the mTLS connection that actually carries the root CA private
key performed no server attestation verification at all; the source was verified only
on a separate earlier connection, with nothing binding the two. Now the handshake that
carries the key is the one that is verified.

ensure_kms_allowed moves after the fetch. The source decides whether to release keys to
us, and the validator has already verified its attestation on this connection, so the
check is a defence-in-depth one against our own policy rather than the thing standing
between a peer and the keys.

KeyProvider::Kms loses tmp_ca_key/tmp_ca_cert. .appkeys.json is regenerated from
the KMS on every boot and AppKeys is not deny_unknown_fields, so a file written by an
older image still parses. docs/encrypted-env-spec.md is updated to match.

GetTempCaCert now has no caller in this tree. It is retained, and the temp CA is
still generated and served, so guest images built before this change keep booting. The
proto and handler notes from #1106 are updated to say so.

Version requirement

A self-issued certificate is refused at the handshake by any KMS that still pins the temp
CA — every release up to and including 0.5.11. So:

  • a guest built from this change needs a KMS at 0.6.0 or later;
  • onboarding needs the source KMS at 0.6.0 or later.

This is why it targets 0.6.1+ rather than 0.6.0: #1106 ships in 0.6.0, so by the time this
lands every KMS in the field already accepts self-issued certificates.

The residual constraint is that a cluster still on 0.5.x upgrades through 0.6.0 as a
bridge hop rather than jumping straight to 0.6.1+. That is the same two-hop shape 0.5.4
already requires today (TC-KMS-UPGRADE-001). Direct 0.5.8/0.5.11 → 0.6.0 onboarding
(TC-KMS-UPGRADE-003/004) is unaffected, because 0.6.0 does not contain this change.

For reference, what changes for a client built from this PR, measured against PR #841's
upgrade matrix:

KMS version before this PR after
0.5.4 nora-tls/src/oids.rs has no PHALA_RATLS_ATTESTATION (.8), so it cannot read the versioned attestation no
0.5.7 / 0.5.8 / 0.5.11 yes no — pins tmp-ca.crt, so the handshake is refused
0.6.0 (#1106) and later yes yes

The 0.5.4 row already matches TC-KMS-UPGRADE-005 ("The 0.5.4 source rejects the versioned
attestation certificate").

Verification

A KMS built from this branch, probed with curl (client certificates carry a
syntactically valid but deliberately non-binding attestation, so invalid quote: report data mismatch is the app layer proving it still verifies quotes after the handshake):

client certificate result
self-issued, with attestation handshake OK → invalid quote: report data mismatch
minted from that KMS's temp CA handshake OK → invalid quote: report data mismatch
self-issued, no attestation handshake REJECTED
none → GetMeta returns ca_cert
none → GetTempCaCert still served

Row 2 and row 5 are the compatibility guarantee for guest images built before this change.

cargo fmt --check, the CI clippy invocation, and the touched crates' suites are clean.

Still to do before this leaves draft: boot a real CVM against a KMS carrying #1106 and
confirm GetMeta → self-issued cert → GetAppKeyverify_key_provider_id end to end,
plus a cert-client SignCert round trip and one real KMS↔KMS onboard. None of that is
runnable without TDX hardware.

@kvinwang kvinwang changed the title refactor(guest): mint self-issued RA-TLS client certificates for KMS calls refactor: mint self-issued RA-TLS client certificates for KMS calls Aug 23, 2026
@kvinwang
kvinwang force-pushed the feat/ratls-guest-self-signed branch 2 times, most recently from fec32d7 to c1cfe16 Compare August 23, 2026 16:52
@kvinwang
kvinwang force-pushed the feat/ratls-guest-self-signed branch from c1cfe16 to 2f6a1f9 Compare August 23, 2026 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant