Skip to content

Commit 02a354f

Browse files
committed
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.
1 parent 4e4418b commit 02a354f

4 files changed

Lines changed: 401 additions & 0 deletions

File tree

sdk/go/ratls/go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module github.com/Dstack-TEE/dstack/sdk/go/ratls
2+
3+
go 1.24.0
4+
5+
require github.com/Phala-Network/dcap-qvl/golang-bindings v0.0.0-20260216131423-a30e3064ba35

sdk/go/ratls/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
github.com/Phala-Network/dcap-qvl/golang-bindings v0.0.0-20260216131423-a30e3064ba35 h1:7MbRUiUHwGHVs15Qi4wI++5eozhVvvo+lTE8ol72hlM=
2+
github.com/Phala-Network/dcap-qvl/golang-bindings v0.0.0-20260216131423-a30e3064ba35/go.mod h1:iVg1YOFXCHz9lYoVlSGgIbHFjT5HaWeLEWtL/tREJnM=

sdk/go/ratls/ratls.go

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
// Package ratls provides RA-TLS certificate verification for dstack TEE applications.
2+
//
3+
// RA-TLS embeds TDX attestation quotes into X.509 certificate extensions.
4+
// This package extracts and verifies those quotes, proving the certificate
5+
// holder is running inside a genuine TEE.
6+
package ratls
7+
8+
import (
9+
"bytes"
10+
"crypto/sha512"
11+
"crypto/tls"
12+
"crypto/x509"
13+
"encoding/asn1"
14+
"encoding/binary"
15+
"encoding/json"
16+
"fmt"
17+
18+
dcap "github.com/Phala-Network/dcap-qvl/golang-bindings"
19+
)
20+
21+
// Phala RA-TLS OIDs for certificate extensions.
22+
var (
23+
oidTdxQuote = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 62397, 1, 1}
24+
oidEventLog = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 62397, 1, 2}
25+
)
26+
27+
// DefaultPCCSURL is the default PCCS server for collateral fetching.
28+
const DefaultPCCSURL = "https://pccs.phala.network"
29+
30+
// dstackRuntimeEventType is the event type for dstack runtime events (0x08000001).
31+
// Matches Rust: cc_eventlog::runtime_events::DSTACK_RUNTIME_EVENT_TYPE
32+
const dstackRuntimeEventType uint32 = 0x08000001
33+
34+
// VerifyResult contains the result of a successful RA-TLS verification.
35+
type VerifyResult struct {
36+
// Report is the dcap-qvl verification report including TCB status and advisory IDs.
37+
Report *dcap.VerifiedReport
38+
// Quote is the parsed TDX quote structure with measurements and report data.
39+
Quote *dcap.Quote
40+
}
41+
42+
// Option configures RA-TLS verification.
43+
type Option func(*config)
44+
45+
type config struct {
46+
pccsURL string
47+
onVerified func(*VerifyResult)
48+
}
49+
50+
// WithPCCSURL sets the PCCS server URL for collateral fetching.
51+
func WithPCCSURL(url string) Option {
52+
return func(c *config) { c.pccsURL = url }
53+
}
54+
55+
// WithOnVerified sets a callback invoked after successful verification.
56+
// Use this with TLSConfig to inspect the VerifyResult.
57+
func WithOnVerified(fn func(*VerifyResult)) Option {
58+
return func(c *config) { c.onVerified = fn }
59+
}
60+
61+
func buildConfig(opts []Option) *config {
62+
cfg := &config{pccsURL: DefaultPCCSURL}
63+
for _, o := range opts {
64+
o(cfg)
65+
}
66+
return cfg
67+
}
68+
69+
// VerifyCert verifies that an X.509 certificate is a valid RA-TLS certificate.
70+
//
71+
// It extracts the embedded TDX quote, verifies it via dcap-qvl, checks that the
72+
// quote's report_data binds to the certificate's public key, validates TCB
73+
// attributes (debug mode, signer), and replays RTMR3 from the event log.
74+
func VerifyCert(cert *x509.Certificate, opts ...Option) (*VerifyResult, error) {
75+
cfg := buildConfig(opts)
76+
77+
// 1. Extract raw TDX quote from certificate extension (OID 1.1)
78+
rawQuote, err := getExtensionBytes(cert, oidTdxQuote)
79+
if err != nil {
80+
return nil, fmt.Errorf("ratls: failed to parse quote extension: %w", err)
81+
}
82+
if rawQuote == nil {
83+
return nil, fmt.Errorf("ratls: certificate has no TDX quote extension (OID %s)", oidTdxQuote)
84+
}
85+
86+
// 2. Verify quote via dcap-qvl (fetch collateral from PCCS + verify Intel signature)
87+
report, err := dcap.GetCollateralAndVerify(rawQuote, cfg.pccsURL)
88+
if err != nil {
89+
return nil, fmt.Errorf("ratls: quote verification failed: %w", err)
90+
}
91+
92+
// 3. Parse quote structure to access report fields
93+
quote, err := dcap.ParseQuote(rawQuote)
94+
if err != nil {
95+
return nil, fmt.Errorf("ratls: failed to parse quote structure: %w", err)
96+
}
97+
98+
// 4. Validate TCB attributes
99+
// Matches Rust: dstack_attest::attestation::validate_tcb()
100+
if err := validateTCB(quote); err != nil {
101+
return nil, fmt.Errorf("ratls: TCB validation failed: %w", err)
102+
}
103+
104+
// 5. Verify report_data binds to the certificate's public key
105+
// Format: SHA512("ratls-cert:" + SubjectPublicKeyInfo DER)
106+
// Matches Rust: QuoteContentType::RaTlsCert.to_report_data(cert.public_key().raw)
107+
h := sha512.New()
108+
h.Write([]byte("ratls-cert:"))
109+
h.Write(cert.RawSubjectPublicKeyInfo)
110+
expected := h.Sum(nil)
111+
112+
if !bytes.Equal(expected, []byte(quote.Report.ReportData)) {
113+
return nil, fmt.Errorf(
114+
"ratls: report_data mismatch: quote is not bound to this certificate's public key"+
115+
" (expected %x, got %x)", expected[:8], []byte(quote.Report.ReportData)[:8],
116+
)
117+
}
118+
119+
// 6. Replay RTMR3 from event log and compare with quote
120+
// Matches Rust: Attestation::replay_runtime_events::<Sha384>(None)
121+
if err := verifyRTMR3(cert, quote); err != nil {
122+
return nil, err
123+
}
124+
125+
return &VerifyResult{Report: report, Quote: quote}, nil
126+
}
127+
128+
// validateTCB checks TCB attributes to reject debug mode and invalid signers.
129+
// Matches Rust: dstack_attest::attestation::validate_tcb()
130+
func validateTCB(quote *dcap.Quote) error {
131+
switch quote.Report.Type {
132+
case "TD10":
133+
// td_attributes[0] bit 0 = debug
134+
if len(quote.Report.TdAttributes) > 0 && quote.Report.TdAttributes[0]&0x01 != 0 {
135+
return fmt.Errorf("debug mode is not allowed")
136+
}
137+
// mr_signer_seam must be all zeros
138+
if len(quote.Report.MrSignerSeam) > 0 && !isAllZeros(quote.Report.MrSignerSeam) {
139+
return fmt.Errorf("invalid mr_signer_seam")
140+
}
141+
case "TD15":
142+
// mr_service_td must be all zeros
143+
if len(quote.Report.MrServiceTD) > 0 && !isAllZeros(quote.Report.MrServiceTD) {
144+
return fmt.Errorf("invalid mr_service_td")
145+
}
146+
// TD15 includes TD10 checks
147+
if len(quote.Report.TdAttributes) > 0 && quote.Report.TdAttributes[0]&0x01 != 0 {
148+
return fmt.Errorf("debug mode is not allowed")
149+
}
150+
if len(quote.Report.MrSignerSeam) > 0 && !isAllZeros(quote.Report.MrSignerSeam) {
151+
return fmt.Errorf("invalid mr_signer_seam")
152+
}
153+
case "SGX":
154+
// attributes[0] bit 1 = debug
155+
if len(quote.Report.Attributes) > 0 && quote.Report.Attributes[0]&0x02 != 0 {
156+
return fmt.Errorf("debug mode is not allowed")
157+
}
158+
}
159+
return nil
160+
}
161+
162+
// tdxEvent matches the JSON format of cc_eventlog::tdx::TdxEvent.
163+
// Note: digest and event_payload are hex-encoded in JSON (Rust uses serde_human_bytes).
164+
type tdxEvent struct {
165+
IMR uint32 `json:"imr"`
166+
EventType uint32 `json:"event_type"`
167+
Digest dcap.HexBytes `json:"digest"`
168+
Event string `json:"event"`
169+
EventPayload dcap.HexBytes `json:"event_payload"`
170+
}
171+
172+
// verifyRTMR3 extracts the event log from the certificate, replays runtime events
173+
// using SHA384, and compares the result with the quote's RTMR3 value.
174+
// Matches Rust: Attestation::verify_tdx() RTMR3 replay
175+
func verifyRTMR3(cert *x509.Certificate, quote *dcap.Quote) error {
176+
if len(quote.Report.RTMR3) == 0 {
177+
return nil // Not a TDX quote, skip
178+
}
179+
180+
rawEventLog, err := getExtensionBytes(cert, oidEventLog)
181+
if err != nil {
182+
return fmt.Errorf("ratls: failed to parse event log extension: %w", err)
183+
}
184+
if rawEventLog == nil {
185+
return fmt.Errorf("ratls: certificate has TDX quote but no event log extension")
186+
}
187+
188+
var events []tdxEvent
189+
if err := json.Unmarshal(rawEventLog, &events); err != nil {
190+
return fmt.Errorf("ratls: failed to parse event log JSON: %w", err)
191+
}
192+
193+
// Replay: accumulate SHA384 over runtime events
194+
// Matches Rust: cc_eventlog::runtime_events::replay_events::<Sha384>()
195+
mr := make([]byte, 48) // starts at all zeros
196+
197+
for _, ev := range events {
198+
if ev.EventType != dstackRuntimeEventType {
199+
continue
200+
}
201+
202+
// Compute event digest: SHA384(event_type_ne_bytes || ":" || event || ":" || payload)
203+
// Matches Rust: RuntimeEvent::digest::<Sha384>()
204+
// TDX CVMs run on x86_64 (little-endian), so to_ne_bytes() is LE.
205+
eventTypeBytes := make([]byte, 4)
206+
binary.LittleEndian.PutUint32(eventTypeBytes, ev.EventType)
207+
208+
dh := sha512.New384()
209+
dh.Write(eventTypeBytes)
210+
dh.Write([]byte(":"))
211+
dh.Write([]byte(ev.Event))
212+
dh.Write([]byte(":"))
213+
dh.Write(ev.EventPayload)
214+
digest := dh.Sum(nil)
215+
216+
// Extend: mr = SHA384(mr || digest)
217+
eh := sha512.New384()
218+
eh.Write(mr)
219+
eh.Write(digest)
220+
mr = eh.Sum(nil)
221+
}
222+
223+
if !bytes.Equal(mr, []byte(quote.Report.RTMR3)) {
224+
return fmt.Errorf(
225+
"ratls: RTMR3 mismatch: replayed %x, quoted %x",
226+
mr[:8], []byte(quote.Report.RTMR3)[:8],
227+
)
228+
}
229+
return nil
230+
}
231+
232+
// TLSConfig returns a *tls.Config that verifies the server's RA-TLS certificate
233+
// during the TLS handshake.
234+
//
235+
// Standard CA chain verification is skipped because RA-TLS certificates are
236+
// self-signed; trust is established through hardware attestation instead.
237+
func TLSConfig(opts ...Option) *tls.Config {
238+
cfg := buildConfig(opts)
239+
return &tls.Config{
240+
InsecureSkipVerify: true,
241+
VerifyPeerCertificate: func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
242+
if len(rawCerts) == 0 {
243+
return fmt.Errorf("ratls: server presented no certificate")
244+
}
245+
cert, err := x509.ParseCertificate(rawCerts[0])
246+
if err != nil {
247+
return fmt.Errorf("ratls: failed to parse server certificate: %w", err)
248+
}
249+
result, err := VerifyCert(cert, opts...)
250+
if err != nil {
251+
return err
252+
}
253+
if cfg.onVerified != nil {
254+
cfg.onVerified(result)
255+
}
256+
return nil
257+
},
258+
}
259+
}
260+
261+
// getExtensionBytes finds a certificate extension by OID and unwraps
262+
// the DER OCTET STRING to return the raw content bytes.
263+
// Returns (nil, nil) if the extension is not present.
264+
// Matches Rust: CertExt::get_extension_bytes() which calls
265+
// yasna::parse_der(|reader| reader.read_bytes()) to unwrap OCTET STRING.
266+
func getExtensionBytes(cert *x509.Certificate, oid asn1.ObjectIdentifier) ([]byte, error) {
267+
for _, ext := range cert.Extensions {
268+
if ext.Id.Equal(oid) {
269+
var raw []byte
270+
if _, err := asn1.Unmarshal(ext.Value, &raw); err != nil {
271+
return nil, fmt.Errorf("failed to unmarshal extension value: %w", err)
272+
}
273+
return raw, nil
274+
}
275+
}
276+
return nil, nil
277+
}
278+
279+
func isAllZeros(b []byte) bool {
280+
for _, v := range b {
281+
if v != 0 {
282+
return false
283+
}
284+
}
285+
return true
286+
}

0 commit comments

Comments
 (0)