Skip to content

Commit 75259fb

Browse files
committed
feat(attestation): add vtpm attestation module
1 parent efa1166 commit 75259fb

File tree

6 files changed

+800
-12
lines changed

6 files changed

+800
-12
lines changed

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ description = "Flare AI Kit template for Retrieval-Augmented Generation (RAG)."
55
readme = "README.md"
66
requires-python = ">=3.12"
77
dependencies = [
8+
"cryptography>=44.0.1",
89
"httpx>=0.28.1",
910
"openrouter>=1.0",
1011
"pandas>=2.2.3",
11-
"python-dotenv>=1.0.1",
12+
"pyjwt>=2.10.1",
13+
"pyopenssl>=25.0.0",
1214
"qdrant-client>=1.13.2",
1315
"sentence-transformers>=3.4.1",
1416
"structlog>=25.1.0",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from .vtpm_attestation import (
2+
Vtpm,
3+
VtpmAttestationError,
4+
)
5+
from .vtpm_validation import (
6+
CertificateParsingError,
7+
InvalidCertificateChainError,
8+
SignatureValidationError,
9+
VtpmValidation,
10+
VtpmValidationError,
11+
)
12+
13+
__all__ = [
14+
"CertificateParsingError",
15+
"InvalidCertificateChainError",
16+
"SignatureValidationError",
17+
"Vtpm",
18+
"VtpmAttestationError",
19+
"VtpmValidation",
20+
"VtpmValidationError",
21+
]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
eyJhbGciOiJSUzI1NiIsImtpZCI6IjQ2NzZjNDkwZGM0MzgyOTYzNjU5NTQ0MmU5M2NkYzVkMjdhYWEyNzkiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL3N0cy5nb29nbGUuY29tIiwiZXhwIjoxNzMwNjgxNjEyLCJpYXQiOjE3MzA2NzgwMTIsImlzcyI6Imh0dHBzOi8vY29uZmlkZW50aWFsY29tcHV0aW5nLmdvb2dsZWFwaXMuY29tIiwibmJmIjoxNzMwNjc4MDEyLCJzdWIiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9jb21wdXRlL3YxL3Byb2plY3RzL2ZsYXJlLW5ldHdvcmstc2FuZGJveC96b25lcy91cy1jZW50cmFsMS1iL2luc3RhbmNlcy90ZXN0LWNvbmZpZGVudGlhbCIsImVhdF9ub25jZSI6IjB4MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwZEVhRCIsImVhdF9wcm9maWxlIjoiaHR0cHM6Ly9jbG91ZC5nb29nbGUuY29tL2NvbmZpZGVudGlhbC1jb21wdXRpbmcvY29uZmlkZW50aWFsLXNwYWNlL2RvY3MvcmVmZXJlbmNlL3Rva2VuLWNsYWltcyIsInNlY2Jvb3QiOnRydWUsIm9lbWlkIjoxMTEyOSwiaHdtb2RlbCI6IkdDUF9BTURfU0VWIiwic3duYW1lIjoiQ09ORklERU5USUFMX1NQQUNFIiwic3d2ZXJzaW9uIjpbIjI0MDkwMCJdLCJkYmdzdGF0IjoiZW5hYmxlZCIsInN1Ym1vZHMiOnsiY29uZmlkZW50aWFsX3NwYWNlIjp7Im1vbml0b3JpbmdfZW5hYmxlZCI6eyJtZW1vcnkiOmZhbHNlfX0sImNvbnRhaW5lciI6eyJpbWFnZV9yZWZlcmVuY2UiOiJnaGNyLmlvL2RpbmVzaHBpbnRvL3Rlc3QtY29uZmlkZW50aWFsOm1haW4iLCJpbWFnZV9kaWdlc3QiOiJzaGEyNTY6YWY3MzhmZGRkMzFlYmU0OGVkNGQ4ZWM5MzZmMjQyMTI3ODUwZDZiMDFhYTY4YTFmYzliZGViOWMwNjNmZWI3YyIsInJlc3RhcnRfcG9saWN5IjoiTmV2ZXIiLCJpbWFnZV9pZCI6InNoYTI1Njo3YmNiMGU5OWUwOGMzNGM3NTkxOWE2NTQwYzJhMDdjMWRkODQ0Y2QzN2Y5MjdjZjBmNTg4ZjBkMzVmYzZmMWViIiwiZW52X292ZXJyaWRlIjp7IkFVRElFTkNFIjoiaHR0cHM6Ly9zdHMuZ29vZ2xlLmNvbSIsIk5PTkNFIjoiMHgwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDBkRWFEIn0sImVudiI6eyJBVURJRU5DRSI6Imh0dHBzOi8vc3RzLmdvb2dsZS5jb20iLCJHUEdfS0VZIjoiNzE2OTYwNUY2MkM3NTEzNTZEMDU0QTI2QTgyMUU2ODBFNUZBNjMwNSIsIkhPU1ROQU1FIjoidGVzdC1jb25maWRlbnRpYWwiLCJMQU5HIjoiQy5VVEYtOCIsIk5PTkNFIjoiMHgwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDBkRWFEIiwiUEFUSCI6Ii91c3IvbG9jYWwvYmluOi91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiIsIlBZVEhPTl9TSEEyNTYiOiIyNDg4N2I5MmUyYWZkNGEyYWM2MDI0MTlhZDRiNTk2MzcyZjY3YWM5YjA3NzE5MGY0NTlhYmEzOTBmYWY1NTUwIiwiUFlUSE9OX1ZFUlNJT04iOiIzLjEyLjcifSwiYXJncyI6WyJ1diIsInJ1biIsImF0dGVzdGF0aW9uLnB5Il19LCJnY2UiOnsiem9uZSI6InVzLWNlbnRyYWwxLWIiLCJwcm9qZWN0X2lkIjoiZmxhcmUtbmV0d29yay1zYW5kYm94IiwicHJvamVjdF9udW1iZXIiOiI4MzY3NDUxNzg3NiIsImluc3RhbmNlX25hbWUiOiJ0ZXN0LWNvbmZpZGVudGlhbCIsImluc3RhbmNlX2lkIjoiMjMwOTAyNDU0MzcxMDQ5MzQ4NyJ9fSwiZ29vZ2xlX3NlcnZpY2VfYWNjb3VudHMiOlsiODM2NzQ1MTc4NzYtY29tcHV0ZUBkZXZlbG9wZXIuZ3NlcnZpY2VhY2NvdW50LmNvbSJdfQ.f2VAbbNl1N9CvL69HJzNKzqdxo4xVK9IVBaO7Wwp0gD8L8IKrvqSUzzXE_guw3hpX2enEnTUEzL6PqLj0bvCB8lMcwogKvhnV2q-WgOSHn3kPMZthrnTXtNarIOqZFTFty3HkFNjCRoE2isosS4rf9QLgASA5C4ASFGUUuFZhODC68sAWTB8mGkd4qTORF8yy5-2i_JgOCZVQhKKJLaEXwvUZmJXYO5i2OkkcFSoYnS1YvfobFi8zuiRIpqx-cv5aDGI6i91iXjk42Ljc4-7BYV_gLsf-p3lBvcEq9es-dGFUTUHLeUmhBXdpRaSgRgWkOgF6XNoLl4movIBZwLgvA
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""
2+
Client for communicating with the Confidential Space vTPM attestation service.
3+
4+
This module provides a client to request attestation tokens from a local Unix domain
5+
socket endpoint. It extends HTTPConnection to handle Unix socket communication and
6+
implements token request functionality with nonce validation.
7+
8+
Classes:
9+
VtpmAttestationError: Exception for attestation service communication errors
10+
VtpmAttestation: Client for requesting attestation tokens
11+
"""
12+
13+
import json
14+
import socket
15+
from http.client import HTTPConnection
16+
from pathlib import Path
17+
18+
import structlog
19+
20+
logger = structlog.get_logger(__name__)
21+
22+
23+
def get_simulated_token() -> str:
24+
"""Reads the first line from a given file path."""
25+
with (Path(__file__).parent / "simulated_token.txt").open("r") as f:
26+
return f.readline().strip()
27+
28+
29+
SIM_TOKEN = get_simulated_token()
30+
31+
32+
class VtpmAttestationError(Exception):
33+
"""
34+
Exception raised for attestation service communication errors.
35+
36+
This includes invalid nonce values, connection failures, and
37+
unexpected responses from the attestation service.
38+
"""
39+
40+
41+
class Vtpm:
42+
"""
43+
Client for requesting attestation tokens via Unix domain socket."""
44+
45+
def __init__(
46+
self,
47+
url: str = "http://localhost/v1/token",
48+
unix_socket_path: str = "/run/container_launcher/teeserver.sock",
49+
simulate: bool = False, # noqa: FBT001, FBT002
50+
) -> None:
51+
self.url = url
52+
self.unix_socket_path = unix_socket_path
53+
self.simulate = simulate
54+
self.attestation_requested: bool = False
55+
self.logger = logger.bind(router="vtpm")
56+
self.logger.debug(
57+
"vtpm", simulate=simulate, url=url, unix_socket_path=self.unix_socket_path
58+
)
59+
60+
def _check_nonce_length(self, nonces: list[str]) -> None:
61+
"""
62+
Validate the byte length of provided nonces.
63+
64+
Each nonce must be between 10 and 74 bytes when UTF-8 encoded.
65+
66+
Args:
67+
nonces: List of nonce strings to validate
68+
69+
Raises:
70+
VtpmAttestationError: If any nonce is outside the valid length range
71+
"""
72+
min_byte_len = 10
73+
max_byte_len = 74
74+
for nonce in nonces:
75+
byte_len = len(nonce.encode("utf-8"))
76+
self.logger.debug("nonce_length", byte_len=byte_len)
77+
if byte_len < min_byte_len or byte_len > max_byte_len:
78+
msg = f"Nonce '{nonce}' must be between {min_byte_len} bytes"
79+
f" and {max_byte_len} bytes"
80+
raise VtpmAttestationError(msg)
81+
82+
def get_token(
83+
self,
84+
nonces: list[str],
85+
audience: str = "https://sts.google.com",
86+
token_type: str = "OIDC", # noqa: S107
87+
) -> str:
88+
"""
89+
Request an attestation token from the service.
90+
91+
Requests a token with specified nonces for replay protection,
92+
targeted at the specified audience. Supports both OIDC and PKI
93+
token types.
94+
95+
Args:
96+
nonces: List of random nonce strings for replay protection
97+
audience: Intended audience for the token (default: "https://sts.google.com")
98+
token_type: Type of token, either "OIDC" or "PKI" (default: "OIDC")
99+
100+
Returns:
101+
str: The attestation token in JWT format
102+
103+
Raises:
104+
VtpmAttestationError: If token request fails for any reason
105+
(invalid nonces, service unavailable, etc.)
106+
107+
Example:
108+
client = VtpmAttestation()
109+
token = client.get_token(
110+
nonces=["random_nonce"],
111+
audience="https://my-service.example.com",
112+
token_type="OIDC"
113+
)
114+
"""
115+
self._check_nonce_length(nonces)
116+
if self.simulate:
117+
self.logger.debug("sim_token", token=SIM_TOKEN)
118+
return SIM_TOKEN
119+
120+
# Connect to the socket
121+
client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
122+
client_socket.connect(self.unix_socket_path)
123+
124+
# Create an HTTP connection object
125+
conn = HTTPConnection("localhost", timeout=10)
126+
conn.sock = client_socket
127+
128+
# Send a POST request
129+
headers = {"Content-Type": "application/json"}
130+
body = json.dumps(
131+
{"audience": audience, "token_type": token_type, "nonces": nonces}
132+
)
133+
conn.request("POST", self.url, body=body, headers=headers)
134+
135+
# Get and decode the response
136+
res = conn.getresponse()
137+
success_status = 200
138+
if res.status != success_status:
139+
msg = f"Failed to get attestation response: {res.status} {res.reason}"
140+
raise VtpmAttestationError(msg)
141+
token = res.read().decode()
142+
self.logger.debug("token", token_type=token_type, token=token)
143+
144+
# Close the connection
145+
conn.close()
146+
return token

0 commit comments

Comments
 (0)