Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions python/tappd_client/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/dist/
46 changes: 46 additions & 0 deletions python/tappd_client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Tappd Client

A Python client for interacting with the Tappd daemon via Unix socket communication.

## Installation

```bash
pip install tappd-client
```

## Usage

```python
from tappd_client import TappdClient

# Initialize the client
client = TappdClient() # Default socket path: /var/run/tappd.sock
# Or specify custom socket path
client = TappdClient(socket_path='/custom/path/tappd.sock')

# Get TDX quote
# Using binary data
binary_data = b'Hello World'
response = client.tdx_quote(binary_data)

# Using string data
string_data = "Hello World"
response = client.tdx_quote(string_data)

# Get the quote
quote = response.quote

# Get event log
event_log = response.event_log

# Replay RTMRS from the event log
rtmrs = response.replay_rtmrs()
RTMR0 = rtmrs[0]
RTMR1 = rtmrs[1]
RTMR2 = rtmrs[2]
RTMR3 = rtmrs[3]
```

## License

This project is licensed under the MIT License. See the LICENSE file for more details.
28 changes: 28 additions & 0 deletions python/tappd_client/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "tappd-client"
version = "0.1.2"
description = "Python client for Tappd in DStack CVM"
requires-python = ">=3.7"
license = "MIT"
authors = [
{name = "Kevin Wang", email = "wy721@qq.com"}
]
dependencies = [
"requests-unixsocket2>=0.4.2",
]
readme = "README.md"
keywords = ["dstack", "tappd"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
]

[tool.hatch.build]
include = ["tappd_client.py"]
133 changes: 133 additions & 0 deletions python/tappd_client/tappd_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import requests_unixsocket
import json
import binascii
import hashlib
from urllib.parse import quote


def replay_rtmr(history: list[str]):
"""
Replay the RTMR history to calculate the final RTMR value.
"""
INIT_MR= "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
if len(history) == 0:
return INIT_MR
mr = bytes.fromhex(INIT_MR)
for content in history:
# mr = sha384(concat(mr, content))
# if content is shorter than 48 bytes, pad it with zeros
content = bytes.fromhex(content)
if len(content) < 48:
content = content.ljust(48, b'\0')
mr = hashlib.sha384(mr + content).digest()
return mr.hex()


class QuoteResponse:
def __init__(self, quote, event_log):
"""Initialize a QuoteResponse object with quote data and event log.

Args:
quote: The quote response from the TDX service
event_log: List of events containing RTMR measurements
"""
self.quote = quote
self.event_log = json.loads(event_log)

def replay_rtmrs(self) -> dict:
"""Replay the RTMR history from the event log to calculate final RTMR values.

Returns:
dict: Dictionary mapping RTMR indices to their calculated values
"""
rtmrs = {}
for idx in range(4):
history = []
for event in self.event_log:
if event.get('imr') == idx:
history.append(event['digest'])
rtmrs[idx] = replay_rtmr(history)
return rtmrs


class DerivedKey:
"""
A class to hold the derived key and certificate.
"""
key: str
"""
The derived key in pem format.
"""
certs: list[str]
"""
The derived certificate chain in pem format.
"""
def __init__(self, key, certs):
self.key = key
self.certs = certs


class TappdClient:
def __init__(self, socket_path='/var/run/tappd.sock'):
self.socket_path = socket_path

def _unix_socket_post(self, endpoint, data):
"""Make a POST request to a Unix socket with the given path and data."""
session = requests_unixsocket.Session()
try:
encoded_path = quote(self.socket_path, safe='')
socket_url = f"http+unix://{encoded_path}/{endpoint}?json"

response = session.post(socket_url, json=data)
response.raise_for_status()
return response.json()
finally:
session.close()

def _rpc_call(self, data, method):
"""Make an RPC call to the tappd service."""
endpoint = 'prpc/' + method
try:
return self._unix_socket_post(endpoint, data)
except Exception as e:
print(f"Error: {e}")
return None

def tdx_quote(self, report_data: bytes | str) -> QuoteResponse:
"""
Get TDX quote for the given report data.

Args:
report_data: Can be either:
- bytes object
- regular string

Returns:
QuoteResponse: An object containing the quote and event log
"""
if report_data is None:
raise ValueError("report_data cannot be None")
if not report_data: # Check for empty string/bytes
raise ValueError("report_data cannot be empty")

if isinstance(report_data, bytes):
hex_data = binascii.hexlify(report_data).decode('ascii')
elif isinstance(report_data, str):
hex_data = report_data.encode('utf-8').hex()
else:
raise TypeError("report_data must be either bytes or str")

data = {"report_data": hex_data}
response = self._rpc_call(data, 'Tappd.TdxQuote')
return QuoteResponse(response['quote'], response['event_log'])

def derive_key(self, path: str, subject: str | None = None, alt_names: list[str] | None = None) -> DerivedKey:
"""
Derive the key and certificate from the given path.
"""
data = {"path": path}
data["subject"] = subject or path
if alt_names:
data["alt_names"] = alt_names
response = self._rpc_call(data, 'Tappd.DeriveKey')
return DerivedKey(response['key'], response['certificate_chain'])
12 changes: 7 additions & 5 deletions ra-tls/src/cert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ pub struct CertRequest<'a> {
key: &'a KeyPair,
org_name: Option<&'a str>,
subject: &'a str,
alt_subject: Option<&'a str>,
alt_names: Option<&'a [String]>,
ca_level: Option<u8>,
quote: Option<&'a [u8]>,
event_log: Option<&'a [u8]>,
Expand All @@ -98,10 +98,12 @@ impl<'a> CertRequest<'a> {
}
dn.push(DnType::CommonName, self.subject);
params.distinguished_name = dn;
if let Some(alt_subject) = self.alt_subject {
params
.subject_alt_names
.push(SanType::DnsName(alt_subject.try_into()?));
if let Some(alt_names) = self.alt_names {
for alt_name in alt_names {
params
.subject_alt_names
.push(SanType::DnsName(alt_name.clone().try_into()?));
}
}
if let Some(quote) = self.quote {
let content = yasna::construct_der(|writer| {
Expand Down
2 changes: 2 additions & 0 deletions tappd/rpc/proto/tappd_rpc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ message DeriveKeyArgs {
string path = 1;
// Subject of the certificate to request
string subject = 2;
// DNS alternative names for the certificate
repeated string alt_names = 3;
}

// The response to a DeriveKey request
Expand Down
1 change: 1 addition & 0 deletions tappd/src/rpc_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ impl TappdRpc for InternalRpcHandler {
.context("Failed to derive key")?;
let req = CertRequest::builder()
.subject(&request.subject)
.alt_names(&request.alt_names)
.key(&derived_key)
.build();
let cert = self
Expand Down