Skip to content

Commit bad8c97

Browse files
authored
Merge pull request #512 from Dstack-TEE/feature-sdk-golang-ratls
feat(go-sdk): add RA-TLS certificate verification package
2 parents 2aeed83 + 4f177a6 commit bad8c97

4 files changed

Lines changed: 403 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: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
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+
default:
159+
return fmt.Errorf("unknown report type: %s", quote.Report.Type)
160+
}
161+
return nil
162+
}
163+
164+
// tdxEvent matches the JSON format of cc_eventlog::tdx::TdxEvent.
165+
// Note: digest and event_payload are hex-encoded in JSON (Rust uses serde_human_bytes).
166+
type tdxEvent struct {
167+
IMR uint32 `json:"imr"`
168+
EventType uint32 `json:"event_type"`
169+
Digest dcap.HexBytes `json:"digest"`
170+
Event string `json:"event"`
171+
EventPayload dcap.HexBytes `json:"event_payload"`
172+
}
173+
174+
// verifyRTMR3 extracts the event log from the certificate, replays runtime events
175+
// using SHA384, and compares the result with the quote's RTMR3 value.
176+
// Matches Rust: Attestation::verify_tdx() RTMR3 replay
177+
func verifyRTMR3(cert *x509.Certificate, quote *dcap.Quote) error {
178+
if len(quote.Report.RTMR3) == 0 {
179+
return nil // Not a TDX quote, skip
180+
}
181+
182+
rawEventLog, err := getExtensionBytes(cert, oidEventLog)
183+
if err != nil {
184+
return fmt.Errorf("ratls: failed to parse event log extension: %w", err)
185+
}
186+
if rawEventLog == nil {
187+
return fmt.Errorf("ratls: certificate has TDX quote but no event log extension")
188+
}
189+
190+
var events []tdxEvent
191+
if err := json.Unmarshal(rawEventLog, &events); err != nil {
192+
return fmt.Errorf("ratls: failed to parse event log JSON: %w", err)
193+
}
194+
195+
// Replay: accumulate SHA384 over runtime events
196+
// Matches Rust: cc_eventlog::runtime_events::replay_events::<Sha384>()
197+
mr := make([]byte, 48) // starts at all zeros
198+
199+
for _, ev := range events {
200+
if ev.EventType != dstackRuntimeEventType {
201+
continue
202+
}
203+
204+
// Compute event digest: SHA384(event_type_ne_bytes || ":" || event || ":" || payload)
205+
// Matches Rust: RuntimeEvent::digest::<Sha384>()
206+
// TDX CVMs run on x86_64 (little-endian), so to_ne_bytes() is LE.
207+
eventTypeBytes := make([]byte, 4)
208+
binary.LittleEndian.PutUint32(eventTypeBytes, ev.EventType)
209+
210+
dh := sha512.New384()
211+
dh.Write(eventTypeBytes)
212+
dh.Write([]byte(":"))
213+
dh.Write([]byte(ev.Event))
214+
dh.Write([]byte(":"))
215+
dh.Write(ev.EventPayload)
216+
digest := dh.Sum(nil)
217+
218+
// Extend: mr = SHA384(mr || digest)
219+
eh := sha512.New384()
220+
eh.Write(mr)
221+
eh.Write(digest)
222+
mr = eh.Sum(nil)
223+
}
224+
225+
if !bytes.Equal(mr, []byte(quote.Report.RTMR3)) {
226+
return fmt.Errorf(
227+
"ratls: RTMR3 mismatch: replayed %x, quoted %x",
228+
mr[:8], []byte(quote.Report.RTMR3)[:8],
229+
)
230+
}
231+
return nil
232+
}
233+
234+
// TLSConfig returns a *tls.Config that verifies the server's RA-TLS certificate
235+
// during the TLS handshake.
236+
//
237+
// Standard CA chain verification is skipped because RA-TLS certificates are
238+
// self-signed; trust is established through hardware attestation instead.
239+
func TLSConfig(opts ...Option) *tls.Config {
240+
cfg := buildConfig(opts)
241+
return &tls.Config{
242+
InsecureSkipVerify: true,
243+
VerifyPeerCertificate: func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
244+
if len(rawCerts) == 0 {
245+
return fmt.Errorf("ratls: server presented no certificate")
246+
}
247+
cert, err := x509.ParseCertificate(rawCerts[0])
248+
if err != nil {
249+
return fmt.Errorf("ratls: failed to parse server certificate: %w", err)
250+
}
251+
result, err := VerifyCert(cert, opts...)
252+
if err != nil {
253+
return err
254+
}
255+
if cfg.onVerified != nil {
256+
cfg.onVerified(result)
257+
}
258+
return nil
259+
},
260+
}
261+
}
262+
263+
// getExtensionBytes finds a certificate extension by OID and unwraps
264+
// the DER OCTET STRING to return the raw content bytes.
265+
// Returns (nil, nil) if the extension is not present.
266+
// Matches Rust: CertExt::get_extension_bytes() which calls
267+
// yasna::parse_der(|reader| reader.read_bytes()) to unwrap OCTET STRING.
268+
func getExtensionBytes(cert *x509.Certificate, oid asn1.ObjectIdentifier) ([]byte, error) {
269+
for _, ext := range cert.Extensions {
270+
if ext.Id.Equal(oid) {
271+
var raw []byte
272+
if _, err := asn1.Unmarshal(ext.Value, &raw); err != nil {
273+
return nil, fmt.Errorf("failed to unmarshal extension value: %w", err)
274+
}
275+
return raw, nil
276+
}
277+
}
278+
return nil, nil
279+
}
280+
281+
func isAllZeros(b []byte) bool {
282+
for _, v := range b {
283+
if v != 0 {
284+
return false
285+
}
286+
}
287+
return true
288+
}

0 commit comments

Comments
 (0)