diff --git a/python/tappd_client/.gitignore b/python/tappd_client/.gitignore new file mode 100644 index 000000000..178135c2b --- /dev/null +++ b/python/tappd_client/.gitignore @@ -0,0 +1 @@ +/dist/ diff --git a/python/tappd_client/README.md b/python/tappd_client/README.md new file mode 100644 index 000000000..e6775f915 --- /dev/null +++ b/python/tappd_client/README.md @@ -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. diff --git a/python/tappd_client/pyproject.toml b/python/tappd_client/pyproject.toml new file mode 100644 index 000000000..f534a4ee0 --- /dev/null +++ b/python/tappd_client/pyproject.toml @@ -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"] diff --git a/python/tappd_client/tappd_client.py b/python/tappd_client/tappd_client.py new file mode 100644 index 000000000..4678c4fa9 --- /dev/null +++ b/python/tappd_client/tappd_client.py @@ -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']) diff --git a/ra-tls/src/cert.rs b/ra-tls/src/cert.rs index 1596dc582..fc7bf4bc2 100644 --- a/ra-tls/src/cert.rs +++ b/ra-tls/src/cert.rs @@ -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, quote: Option<&'a [u8]>, event_log: Option<&'a [u8]>, @@ -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| { diff --git a/tappd/rpc/proto/tappd_rpc.proto b/tappd/rpc/proto/tappd_rpc.proto index 23cf448ff..159a6b155 100644 --- a/tappd/rpc/proto/tappd_rpc.proto +++ b/tappd/rpc/proto/tappd_rpc.proto @@ -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 diff --git a/tappd/src/rpc_service.rs b/tappd/src/rpc_service.rs index 49c1154c1..495e91d37 100644 --- a/tappd/src/rpc_service.rs +++ b/tappd/src/rpc_service.rs @@ -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