|
| 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