Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions sdk/go/ratls/go.mod
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions sdk/go/ratls/go.sum
Original file line number Diff line number Diff line change
@@ -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=
286 changes: 286 additions & 0 deletions sdk/go/ratls/ratls.go
Original file line number Diff line number Diff line change
@@ -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::<Sha384>(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")
Comment thread
Leechael marked this conversation as resolved.
}
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::<Sha384>()
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::<Sha384>()
// 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
}
Loading