From 02a354f3e40f4e6c6734e2e80b805dd42b56e7af Mon Sep 17 00:00:00 2001 From: Leechael Yim Date: Mon, 16 Feb 2026 21:40:22 +0800 Subject: [PATCH 1/2] feat(go-sdk): add RA-TLS certificate verification package Add sdk/go/ratls as a separate Go module providing RA-TLS certificate verification for dstack TEE applications. The implementation matches the Rust ra-tls/dstack-attest verification chain: - Extract TDX quote from X.509 certificate extension (OID 1.3.6.1.4.1.62397.1.1) - Verify quote via dcap-qvl (fetch collateral from PCCS + verify Intel signature) - Validate TCB attributes (reject debug mode, check mr_signer_seam/mr_service_td) - Verify report_data binds to certificate public key (SHA512 of "ratls-cert:" prefix) - Replay RTMR3 from event log and compare with quote Exposed API: - VerifyCert(cert, opts...) - verify a single X.509 certificate - TLSConfig(opts...) - return *tls.Config for RA-TLS handshake verification - WithPCCSURL / WithOnVerified options Uses github.com/Phala-Network/dcap-qvl/golang-bindings for quote parsing and verification. Separate go.mod keeps the dcap-qvl CGO dependency isolated from the base SDK module. --- sdk/go/ratls/go.mod | 5 + sdk/go/ratls/go.sum | 2 + sdk/go/ratls/ratls.go | 286 +++++++++++++++++++++++++++++++++++++ sdk/go/ratls/ratls_test.go | 108 ++++++++++++++ 4 files changed, 401 insertions(+) create mode 100644 sdk/go/ratls/go.mod create mode 100644 sdk/go/ratls/go.sum create mode 100644 sdk/go/ratls/ratls.go create mode 100644 sdk/go/ratls/ratls_test.go diff --git a/sdk/go/ratls/go.mod b/sdk/go/ratls/go.mod new file mode 100644 index 00000000..f9ea90b3 --- /dev/null +++ b/sdk/go/ratls/go.mod @@ -0,0 +1,5 @@ +module github.com/Dstack-TEE/dstack/sdk/go/ratls + +go 1.24.0 + +require github.com/Phala-Network/dcap-qvl/golang-bindings v0.0.0-20260216131423-a30e3064ba35 diff --git a/sdk/go/ratls/go.sum b/sdk/go/ratls/go.sum new file mode 100644 index 00000000..66a46aab --- /dev/null +++ b/sdk/go/ratls/go.sum @@ -0,0 +1,2 @@ +github.com/Phala-Network/dcap-qvl/golang-bindings v0.0.0-20260216131423-a30e3064ba35 h1:7MbRUiUHwGHVs15Qi4wI++5eozhVvvo+lTE8ol72hlM= +github.com/Phala-Network/dcap-qvl/golang-bindings v0.0.0-20260216131423-a30e3064ba35/go.mod h1:iVg1YOFXCHz9lYoVlSGgIbHFjT5HaWeLEWtL/tREJnM= diff --git a/sdk/go/ratls/ratls.go b/sdk/go/ratls/ratls.go new file mode 100644 index 00000000..0ae057f3 --- /dev/null +++ b/sdk/go/ratls/ratls.go @@ -0,0 +1,286 @@ +// Package ratls provides RA-TLS certificate verification for dstack TEE applications. +// +// RA-TLS embeds TDX attestation quotes into X.509 certificate extensions. +// This package extracts and verifies those quotes, proving the certificate +// holder is running inside a genuine TEE. +package ratls + +import ( + "bytes" + "crypto/sha512" + "crypto/tls" + "crypto/x509" + "encoding/asn1" + "encoding/binary" + "encoding/json" + "fmt" + + dcap "github.com/Phala-Network/dcap-qvl/golang-bindings" +) + +// Phala RA-TLS OIDs for certificate extensions. +var ( + oidTdxQuote = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 62397, 1, 1} + oidEventLog = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 62397, 1, 2} +) + +// DefaultPCCSURL is the default PCCS server for collateral fetching. +const DefaultPCCSURL = "https://pccs.phala.network" + +// dstackRuntimeEventType is the event type for dstack runtime events (0x08000001). +// Matches Rust: cc_eventlog::runtime_events::DSTACK_RUNTIME_EVENT_TYPE +const dstackRuntimeEventType uint32 = 0x08000001 + +// VerifyResult contains the result of a successful RA-TLS verification. +type VerifyResult struct { + // Report is the dcap-qvl verification report including TCB status and advisory IDs. + Report *dcap.VerifiedReport + // Quote is the parsed TDX quote structure with measurements and report data. + Quote *dcap.Quote +} + +// Option configures RA-TLS verification. +type Option func(*config) + +type config struct { + pccsURL string + onVerified func(*VerifyResult) +} + +// WithPCCSURL sets the PCCS server URL for collateral fetching. +func WithPCCSURL(url string) Option { + return func(c *config) { c.pccsURL = url } +} + +// WithOnVerified sets a callback invoked after successful verification. +// Use this with TLSConfig to inspect the VerifyResult. +func WithOnVerified(fn func(*VerifyResult)) Option { + return func(c *config) { c.onVerified = fn } +} + +func buildConfig(opts []Option) *config { + cfg := &config{pccsURL: DefaultPCCSURL} + for _, o := range opts { + o(cfg) + } + return cfg +} + +// VerifyCert verifies that an X.509 certificate is a valid RA-TLS certificate. +// +// It extracts the embedded TDX quote, verifies it via dcap-qvl, checks that the +// quote's report_data binds to the certificate's public key, validates TCB +// attributes (debug mode, signer), and replays RTMR3 from the event log. +func VerifyCert(cert *x509.Certificate, opts ...Option) (*VerifyResult, error) { + cfg := buildConfig(opts) + + // 1. Extract raw TDX quote from certificate extension (OID 1.1) + rawQuote, err := getExtensionBytes(cert, oidTdxQuote) + if err != nil { + return nil, fmt.Errorf("ratls: failed to parse quote extension: %w", err) + } + if rawQuote == nil { + return nil, fmt.Errorf("ratls: certificate has no TDX quote extension (OID %s)", oidTdxQuote) + } + + // 2. Verify quote via dcap-qvl (fetch collateral from PCCS + verify Intel signature) + report, err := dcap.GetCollateralAndVerify(rawQuote, cfg.pccsURL) + if err != nil { + return nil, fmt.Errorf("ratls: quote verification failed: %w", err) + } + + // 3. Parse quote structure to access report fields + quote, err := dcap.ParseQuote(rawQuote) + if err != nil { + return nil, fmt.Errorf("ratls: failed to parse quote structure: %w", err) + } + + // 4. Validate TCB attributes + // Matches Rust: dstack_attest::attestation::validate_tcb() + if err := validateTCB(quote); err != nil { + return nil, fmt.Errorf("ratls: TCB validation failed: %w", err) + } + + // 5. Verify report_data binds to the certificate's public key + // Format: SHA512("ratls-cert:" + SubjectPublicKeyInfo DER) + // Matches Rust: QuoteContentType::RaTlsCert.to_report_data(cert.public_key().raw) + h := sha512.New() + h.Write([]byte("ratls-cert:")) + h.Write(cert.RawSubjectPublicKeyInfo) + expected := h.Sum(nil) + + if !bytes.Equal(expected, []byte(quote.Report.ReportData)) { + return nil, fmt.Errorf( + "ratls: report_data mismatch: quote is not bound to this certificate's public key"+ + " (expected %x, got %x)", expected[:8], []byte(quote.Report.ReportData)[:8], + ) + } + + // 6. Replay RTMR3 from event log and compare with quote + // Matches Rust: Attestation::replay_runtime_events::(None) + if err := verifyRTMR3(cert, quote); err != nil { + return nil, err + } + + return &VerifyResult{Report: report, Quote: quote}, nil +} + +// validateTCB checks TCB attributes to reject debug mode and invalid signers. +// Matches Rust: dstack_attest::attestation::validate_tcb() +func validateTCB(quote *dcap.Quote) error { + switch quote.Report.Type { + case "TD10": + // td_attributes[0] bit 0 = debug + if len(quote.Report.TdAttributes) > 0 && quote.Report.TdAttributes[0]&0x01 != 0 { + return fmt.Errorf("debug mode is not allowed") + } + // mr_signer_seam must be all zeros + if len(quote.Report.MrSignerSeam) > 0 && !isAllZeros(quote.Report.MrSignerSeam) { + return fmt.Errorf("invalid mr_signer_seam") + } + case "TD15": + // mr_service_td must be all zeros + if len(quote.Report.MrServiceTD) > 0 && !isAllZeros(quote.Report.MrServiceTD) { + return fmt.Errorf("invalid mr_service_td") + } + // TD15 includes TD10 checks + if len(quote.Report.TdAttributes) > 0 && quote.Report.TdAttributes[0]&0x01 != 0 { + return fmt.Errorf("debug mode is not allowed") + } + if len(quote.Report.MrSignerSeam) > 0 && !isAllZeros(quote.Report.MrSignerSeam) { + return fmt.Errorf("invalid mr_signer_seam") + } + case "SGX": + // attributes[0] bit 1 = debug + if len(quote.Report.Attributes) > 0 && quote.Report.Attributes[0]&0x02 != 0 { + return fmt.Errorf("debug mode is not allowed") + } + } + return nil +} + +// tdxEvent matches the JSON format of cc_eventlog::tdx::TdxEvent. +// Note: digest and event_payload are hex-encoded in JSON (Rust uses serde_human_bytes). +type tdxEvent struct { + IMR uint32 `json:"imr"` + EventType uint32 `json:"event_type"` + Digest dcap.HexBytes `json:"digest"` + Event string `json:"event"` + EventPayload dcap.HexBytes `json:"event_payload"` +} + +// verifyRTMR3 extracts the event log from the certificate, replays runtime events +// using SHA384, and compares the result with the quote's RTMR3 value. +// Matches Rust: Attestation::verify_tdx() RTMR3 replay +func verifyRTMR3(cert *x509.Certificate, quote *dcap.Quote) error { + if len(quote.Report.RTMR3) == 0 { + return nil // Not a TDX quote, skip + } + + rawEventLog, err := getExtensionBytes(cert, oidEventLog) + if err != nil { + return fmt.Errorf("ratls: failed to parse event log extension: %w", err) + } + if rawEventLog == nil { + return fmt.Errorf("ratls: certificate has TDX quote but no event log extension") + } + + var events []tdxEvent + if err := json.Unmarshal(rawEventLog, &events); err != nil { + return fmt.Errorf("ratls: failed to parse event log JSON: %w", err) + } + + // Replay: accumulate SHA384 over runtime events + // Matches Rust: cc_eventlog::runtime_events::replay_events::() + mr := make([]byte, 48) // starts at all zeros + + for _, ev := range events { + if ev.EventType != dstackRuntimeEventType { + continue + } + + // Compute event digest: SHA384(event_type_ne_bytes || ":" || event || ":" || payload) + // Matches Rust: RuntimeEvent::digest::() + // TDX CVMs run on x86_64 (little-endian), so to_ne_bytes() is LE. + eventTypeBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(eventTypeBytes, ev.EventType) + + dh := sha512.New384() + dh.Write(eventTypeBytes) + dh.Write([]byte(":")) + dh.Write([]byte(ev.Event)) + dh.Write([]byte(":")) + dh.Write(ev.EventPayload) + digest := dh.Sum(nil) + + // Extend: mr = SHA384(mr || digest) + eh := sha512.New384() + eh.Write(mr) + eh.Write(digest) + mr = eh.Sum(nil) + } + + if !bytes.Equal(mr, []byte(quote.Report.RTMR3)) { + return fmt.Errorf( + "ratls: RTMR3 mismatch: replayed %x, quoted %x", + mr[:8], []byte(quote.Report.RTMR3)[:8], + ) + } + return nil +} + +// TLSConfig returns a *tls.Config that verifies the server's RA-TLS certificate +// during the TLS handshake. +// +// Standard CA chain verification is skipped because RA-TLS certificates are +// self-signed; trust is established through hardware attestation instead. +func TLSConfig(opts ...Option) *tls.Config { + cfg := buildConfig(opts) + return &tls.Config{ + InsecureSkipVerify: true, + VerifyPeerCertificate: func(rawCerts [][]byte, _ [][]*x509.Certificate) error { + if len(rawCerts) == 0 { + return fmt.Errorf("ratls: server presented no certificate") + } + cert, err := x509.ParseCertificate(rawCerts[0]) + if err != nil { + return fmt.Errorf("ratls: failed to parse server certificate: %w", err) + } + result, err := VerifyCert(cert, opts...) + if err != nil { + return err + } + if cfg.onVerified != nil { + cfg.onVerified(result) + } + return nil + }, + } +} + +// getExtensionBytes finds a certificate extension by OID and unwraps +// the DER OCTET STRING to return the raw content bytes. +// Returns (nil, nil) if the extension is not present. +// Matches Rust: CertExt::get_extension_bytes() which calls +// yasna::parse_der(|reader| reader.read_bytes()) to unwrap OCTET STRING. +func getExtensionBytes(cert *x509.Certificate, oid asn1.ObjectIdentifier) ([]byte, error) { + for _, ext := range cert.Extensions { + if ext.Id.Equal(oid) { + var raw []byte + if _, err := asn1.Unmarshal(ext.Value, &raw); err != nil { + return nil, fmt.Errorf("failed to unmarshal extension value: %w", err) + } + return raw, nil + } + } + return nil, nil +} + +func isAllZeros(b []byte) bool { + for _, v := range b { + if v != 0 { + return false + } + } + return true +} diff --git a/sdk/go/ratls/ratls_test.go b/sdk/go/ratls/ratls_test.go new file mode 100644 index 00000000..e4cc059b --- /dev/null +++ b/sdk/go/ratls/ratls_test.go @@ -0,0 +1,108 @@ +package ratls_test + +import ( + "crypto/tls" + "fmt" + "os" + "testing" + + "github.com/Dstack-TEE/dstack/sdk/go/ratls" +) + +// TestVerifyEndpoint connects to a real RA-TLS endpoint and verifies its certificate. +// +// Set RATLS_TEST_ENDPOINT to run this test, e.g.: +// +// RATLS_TEST_ENDPOINT=myapp-8443s.phala.network:443 go test -v -run TestVerifyEndpoint +func TestVerifyEndpoint(t *testing.T) { + endpoint := os.Getenv("RATLS_TEST_ENDPOINT") + if endpoint == "" { + t.Skip("RATLS_TEST_ENDPOINT not set") + } + + pccsURL := os.Getenv("RATLS_PCCS_URL") + if pccsURL == "" { + pccsURL = ratls.DefaultPCCSURL + } + + // Connect with standard TLS (skip CA verification), then verify RA-TLS ourselves + conn, err := tls.Dial("tcp", endpoint, &tls.Config{InsecureSkipVerify: true}) + if err != nil { + t.Fatalf("TLS dial failed: %v", err) + } + defer conn.Close() + + state := conn.ConnectionState() + if len(state.PeerCertificates) == 0 { + t.Fatal("server presented no certificate") + } + + cert := state.PeerCertificates[0] + result, err := ratls.VerifyCert(cert, ratls.WithPCCSURL(pccsURL)) + if err != nil { + t.Fatalf("RA-TLS verification failed: %v", err) + } + + t.Logf("verification succeeded") + t.Logf(" tcb status: %s", result.Report.Status) + t.Logf(" advisories: %v", result.Report.AdvisoryIDs) + t.Logf(" quote type: %s", result.Quote.QuoteType) + t.Logf(" report type: %s", result.Quote.Report.Type) + if len(result.Quote.Report.RTMR0) > 0 { + t.Logf(" RTMR0: %x", []byte(result.Quote.Report.RTMR0)) + t.Logf(" RTMR1: %x", []byte(result.Quote.Report.RTMR1)) + t.Logf(" RTMR2: %x", []byte(result.Quote.Report.RTMR2)) + t.Logf(" RTMR3: %x", []byte(result.Quote.Report.RTMR3)) + } +} + +// TestTLSConfigEndpoint tests the TLSConfig convenience function against a real endpoint. +func TestTLSConfigEndpoint(t *testing.T) { + endpoint := os.Getenv("RATLS_TEST_ENDPOINT") + if endpoint == "" { + t.Skip("RATLS_TEST_ENDPOINT not set") + } + + pccsURL := os.Getenv("RATLS_PCCS_URL") + if pccsURL == "" { + pccsURL = ratls.DefaultPCCSURL + } + + var result *ratls.VerifyResult + tlsCfg := ratls.TLSConfig( + ratls.WithPCCSURL(pccsURL), + ratls.WithOnVerified(func(r *ratls.VerifyResult) { + result = r + }), + ) + + conn, err := tls.Dial("tcp", endpoint, tlsCfg) + if err != nil { + t.Fatalf("RA-TLS dial failed: %v", err) + } + conn.Close() + + if result == nil { + t.Fatal("OnVerified callback was not called") + } + t.Logf("TLSConfig verification succeeded: status=%s", result.Report.Status) +} + +// ExampleVerifyCert demonstrates verifying an RA-TLS certificate from a TLS connection. +func ExampleVerifyCert() { + conn, err := tls.Dial("tcp", "myapp-8443s.phala.network:443", &tls.Config{ + InsecureSkipVerify: true, + }) + if err != nil { + panic(err) + } + defer conn.Close() + + cert := conn.ConnectionState().PeerCertificates[0] + result, err := ratls.VerifyCert(cert) + if err != nil { + fmt.Printf("verification failed: %v\n", err) + return + } + fmt.Printf("verified: status=%s\n", result.Report.Status) +} From 4f177a6410e6c9f44e69b6a6a6fc5c27c659c5b3 Mon Sep 17 00:00:00 2001 From: Leechael Yim Date: Tue, 10 Mar 2026 01:22:21 +0800 Subject: [PATCH 2/2] fix(go-sdk): reject unknown report types in TCB validation Add default case to validateTCB switch to reject unknown report types instead of silently passing. Unlike Rust's exhaustive match on the Report enum, Go's string-based switch would skip all checks for any new or unexpected report type. --- sdk/go/ratls/ratls.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk/go/ratls/ratls.go b/sdk/go/ratls/ratls.go index 0ae057f3..6ba7d386 100644 --- a/sdk/go/ratls/ratls.go +++ b/sdk/go/ratls/ratls.go @@ -155,6 +155,8 @@ func validateTCB(quote *dcap.Quote) error { if len(quote.Report.Attributes) > 0 && quote.Report.Attributes[0]&0x02 != 0 { return fmt.Errorf("debug mode is not allowed") } + default: + return fmt.Errorf("unknown report type: %s", quote.Report.Type) } return nil }