Skip to content

Commit 7ab31cc

Browse files
author
Ubuntu
committed
remote attestation for openfl participants
1 parent 06d8335 commit 7ab31cc

File tree

7 files changed

+530
-2
lines changed

7 files changed

+530
-2
lines changed

openfl-docker/gramine_app/fx.manifest.template

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,6 @@ sgx.allowed_files = [
7171
"file:{{ workspace_root }}/plan/cols.yaml",
7272
"file:{{ workspace_root }}/plan/data.yaml",
7373
"file:{{ workspace_root }}/plan/plan.yaml",
74+
"file:{{ workspace_root }}/attestation",
75+
"file:{{ workspace_root }}/attestation/*",
7476
]

openfl-workspace/workspace/plan/defaults/network.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ settings:
77
client_reconnect_interval : 5
88
require_client_auth : True
99
cert_folder : cert
10-
enable_atomic_connections : False
10+
enable_atomic_connections : False
11+
enable_remote_attestation : False

openfl/interface/aggregator.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from openfl.utilities import click_types
3535
from openfl.utilities.path_check import is_directory_traversal
3636
from openfl.utilities.utils import getfqdn_env
37+
from openfl.utilities.attestation import attestation_utils as attestation_utils
3738

3839
logger = getLogger(__name__)
3940

@@ -90,6 +91,21 @@ def start_(plan, authorized_cols, task_group):
9091
parsed_plan.config["assigner"]["settings"] = {}
9192
parsed_plan.config["assigner"]["settings"]["selected_task_group"] = task_group
9293
logger.info(f"Setting aggregator to assign: {task_group} task_group")
94+
95+
# Check if remote attestation is enabled in the plan configuration
96+
if parsed_plan.config["network"]["settings"].get("enable_remote_attestation", False):
97+
# Fetch remote attestation environment variables
98+
env_vars = attestation_utils.fetch_attestation_env_vars()
99+
if env_vars is not None:
100+
attestation_mr = attestation_utils.AttestationManager("aggregator", env_vars["ATTESTATION_REPORT_PATH"], env_vars["ITA_API_KEY"], env_vars["AVS_URL"])
101+
# Generate and store the attestation report
102+
attestaion_report = attestation_mr.get_attestation_report()
103+
logger.info("Remote attestation report fetched successfully.")
104+
else:
105+
logger.error("Failed to fetch remote attestation environment variables.")
106+
else:
107+
logger.info("Remote attestation is not enabled. Skipping attestation report generation.")
108+
93109

94110
logger.info("🧿 Starting the Aggregator Service.")
95111

openfl/interface/collaborator.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from openfl.interface.cli_helper import CERT_DIR
2626
from openfl.utilities.path_check import is_directory_traversal
2727
from openfl.utilities.utils import rmtree
28-
28+
from openfl.utilities.attestation import attestation_utils as attestation_utils
2929
logger = getLogger(__name__)
3030

3131

@@ -78,6 +78,21 @@ def start_(plan, collaborator_name, data_config):
7878

7979
# TODO: Need to restructure data loader config file loader
8080

81+
# Check if remote attestation is enabled in the plan configuration
82+
if plan.config["network"]["settings"].get("enable_remote_attestation", False):
83+
# Fetch remote attestation environment variables
84+
env_vars = attestation_utils.fetch_attestation_env_vars()
85+
logger.info(f"Remote attestation environment variables fetched from env: {env_vars}")
86+
if env_vars is not None:
87+
attestation_mr = attestation_utils.AttestationManager(collaborator_name, env_vars["ATTESTATION_REPORT_PATH"], env_vars["ITA_API_KEY"], env_vars["AVS_URL"])
88+
# Generate and store the attestation report
89+
attestation_report = attestation_mr.get_attestation_report()
90+
logger.info("Remote attestation report stored successfully.")
91+
else:
92+
logger.error("Remote attestation environment variables not set.")
93+
else:
94+
logger.info("Remote attestation is not enabled in the plan configuration.")
95+
8196
echo(f"Data = {plan.cols_data_paths}")
8297
logger.info("🧿 Starting a Collaborator Service.")
8398

openfl/utilities/attestation/__init__.py

Whitespace-only changes.
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
2+
from base64 import b64encode
3+
import json
4+
import base64
5+
import os
6+
import http
7+
import requests
8+
import hashlib
9+
import logging
10+
11+
from openfl.utilities.attestation.signer_utils import ECDSASigner
12+
13+
14+
logger = logging.getLogger(__name__)
15+
16+
class AttestationManager:
17+
"""Class to manage attestation for enclaves.
18+
This class handles the generation of SGX quotes, fetching MRENCLAVE values,
19+
and obtaining attestation reports from AVS (Attestation Verification Service).
20+
"""
21+
22+
23+
def __init__(self, participant_name, attestation_report_path, ita_api_key, avs_url):
24+
"""Initializes the AttestationManager with the provided parameters.
25+
26+
Args:
27+
participant_name (str): Name of the enclave.
28+
attestation_report_path (str): Path to the store attestation reports.
29+
ita_api_key (str): API key for ITA.
30+
avs_url (str): URL for AVS.
31+
enable_remote_attestation (bool): Flag to enable remote attestation.
32+
"""
33+
self.participant_name = participant_name
34+
self.attestation_report_path = attestation_report_path
35+
self.ita_api_key = ita_api_key
36+
self.avs_url = avs_url
37+
38+
39+
def get_attestation_report(
40+
self
41+
):
42+
"""Generates the attestation report for the enclave.
43+
This function generates the SGX quote, fetches the MRENCLAVE value,
44+
and obtains the attestation report from AVS.
45+
It also generates an ECDSA-P 384 key pair and certificate for mTLS
46+
with the collaborator enclave.
47+
The generated report is saved in the specified attestation report path.
48+
The function also checks if the required environment variables are set
49+
and raises an exception if any of them are missing.
50+
The function returns a dictionary containing the MRENCLAVE value,
51+
participant name, and the AVS report token.
52+
The function raises a FileNotFoundError if the attestation report path
53+
does not exist, and a ValueError if the ITA API key or AVS URL is not set.
54+
Args:
55+
None
56+
Raises:
57+
FileNotFoundError: If the attestation report path does not exist.
58+
ValueError: If the ITA API key or AVS URL is not set.
59+
Returns:
60+
dict: A dictionary containing the MRENCLAVE value, participant name,
61+
and AVS report token.
62+
"""
63+
64+
if not os.path.exists(self.attestation_report_path):
65+
raise FileNotFoundError(f"attestation report path {self.attestation_report_path} does not exist")
66+
67+
if self.ita_api_key is None:
68+
raise ValueError("ITA API key is required for remote attestation")
69+
if self.avs_url is None:
70+
raise ValueError("AVS URL is required for remote attestation")
71+
72+
mrenclave = self.fetch_mrenclave_from_quote(None)
73+
logger.info(f"Enclave MRENCLAVE: {mrenclave}")
74+
75+
# Generate ECDSA-P 384 key pair and certificate
76+
# This is the enclave specific ECDSA-P 384 keypair for setting up mTLS
77+
# with collaborator enclave
78+
priv_key_path = os.path.join(self.attestation_report_path, f"{self.participant_name}_privkey.pem")
79+
ecdsa_p_384_signer = ECDSASigner.get_instance()
80+
cert = ecdsa_p_384_signer.cert("localhost",mrenclave).encode('utf-8')
81+
key = ecdsa_p_384_signer.serialize_private_key()
82+
pub_key = ecdsa_p_384_signer.get_pubkey()
83+
pubkey_path = os.path.join(self.attestation_report_path, f"{self.participant_name}_pubkey.pem")
84+
with open(pubkey_path, "wb") as fh:
85+
fh.write(pub_key.encode('utf-8'))
86+
avs_report = self.get_avs_report_ita(
87+
self.participant_name,
88+
self.attestation_report_path,
89+
cert,
90+
self.avs_url,
91+
self.ita_api_key
92+
)
93+
logger.info(f"AVS report: {avs_report}")
94+
return {'mrenclave': mrenclave,
95+
'name': self.participant_name,
96+
'token': avs_report["token"]}
97+
98+
99+
def gen_sgx_quote(
100+
self,
101+
user_data_bytes=None,
102+
quote_dump_path='/tmp/quote.json',
103+
orig_data=None,
104+
challenge="0000000000000"):
105+
"""Generates the SGX quote and saves it to the specified path.
106+
Args:
107+
user_data_bytes (bytes): User data to be included in the quote.
108+
quote_dump_path (str): Path to save the generated quote.
109+
orig_data (bytes): Original data to be included in the quote.
110+
challenge (str): Challenge string to be included in the quote.
111+
Returns:
112+
dict: A dictionary containing the generated quote.
113+
Raises:
114+
ValueError: If the user data is not a bytes object or exceeds 64 bytes.
115+
"""
116+
117+
# Set user data
118+
if user_data_bytes is not None:
119+
# Check that user data is a bytes like object
120+
if isinstance(user_data_bytes, bytes) is not True:
121+
raise ValueError("User data must be a bytes object")
122+
123+
# Check that user data is at most 64 bytes
124+
if len(list(user_data_bytes)) > 64:
125+
raise ValueError("User data can be at most 64 bytes")
126+
127+
# Set user report data
128+
with open("/dev/attestation/user_report_data", 'wb') as fh:
129+
fh.write(user_data_bytes)
130+
131+
# Generate attestation quote
132+
quote = None
133+
with open("/dev/attestation/quote", 'rb') as fh:
134+
quote = fh.read(8192)
135+
136+
# Create quote as expected by AVS for verification
137+
# a. base64 encode the quote
138+
# b. Set 'userData' to empty, we do not want AVS to verify user data
139+
quote_avs = {}
140+
quote_avs["quote"] = base64.b64encode(quote).decode('utf-8')
141+
142+
if orig_data is not None:
143+
quote_avs['runtime_data'] = base64.b64encode(orig_data).decode('utf-8')
144+
else:
145+
quote_avs['runtime_data'] = ''
146+
147+
# Save the quote in the specified location
148+
with open(quote_dump_path, 'w') as fh:
149+
json.dump(quote_avs, fh)
150+
151+
return quote_avs
152+
153+
def fetch_mrenclave_from_quote(self,quote):
154+
"""Fetches the MRENCLAVE value from the SGX quote.
155+
156+
Args:
157+
quote (str): The SGX quote in JSON format.
158+
159+
Returns:
160+
str: The MRENCLAVE value extracted from the quote.
161+
"""
162+
163+
if quote is None:
164+
# Generate attestation quote
165+
with open("/dev/attestation/quote", 'rb') as fh:
166+
quote = fh.read(8192)
167+
168+
# Create quote as expected by AVS for verification
169+
# a. base64 encode the quote
170+
# b. Set 'userData' to empty, we do not want AVS to verify user data
171+
quote_avs = {}
172+
quote_avs["quote"] = base64.b64encode(quote).decode('utf-8')
173+
174+
# Decode the base64 encoded quote
175+
decoded_quote = base64.b64decode(quote_avs['quote'])
176+
177+
# Extract the MRENCLAVE value from the decoded quote
178+
mrenclave_hex = decoded_quote[112:144].hex()
179+
logger.info(f"Extracted MRENCLAVE: {mrenclave_hex}")
180+
181+
return mrenclave_hex
182+
183+
184+
185+
def get_avs_report_ita(self,name, attestation_report_path,cert, avs_url, ita_api_key):
186+
"""Fetches the attestation report from AVS using the ITA API key.
187+
This function generates the SGX quote, sends it to AVS for attestation,
188+
and saves the attestation report in the specified path.
189+
Args:
190+
name (str): Name of the enclave.
191+
attestation_report_path (str): Path to save the attestation report.
192+
cert (bytes): Certificate for mTLS with the collaborator enclave.
193+
avs_url (str): URL for AVS.
194+
ita_api_key (str): API key for ITA.
195+
Returns:
196+
dict: A dictionary containing the attestation report from AVS.
197+
Raises:
198+
Exception: If the connection to AVS fails or if the response is not OK.
199+
"""
200+
# Set the paths for getting the enclave quote and ITA AVS response
201+
quote_dir = os.path.normpath("/tmp")
202+
quote_path = os.path.join(quote_dir, f"{name}_quote.json")
203+
avs_report_path = os.path.join(attestation_report_path, f"{name}_avs_report.json")
204+
205+
# AVS doesn't work with SHA384, so, moving to SHA256 in the meantime
206+
cert_sha256_digest = hashlib.sha256(cert).digest()
207+
self.gen_sgx_quote(cert_sha256_digest, quote_path, cert)
208+
209+
# ITA attestation
210+
avs_attest_endpoint = f"{avs_url}/appraisal/v1/attest"
211+
headers = {
212+
"Accept": "application/json",
213+
"Content-Type": "application/json",
214+
"x-api-key": ita_api_key
215+
}
216+
217+
# Read the quote and send it to AVS for getting the report
218+
with open(quote_path) as fh:
219+
quote_avs = fh.read()
220+
221+
# AVS has a self-signed certificate, so, disabling verification
222+
res = requests.post(
223+
avs_attest_endpoint,
224+
headers=headers,
225+
data=quote_avs)
226+
if res.status_code != http.HTTPStatus.OK:
227+
raise Exception(
228+
f"Failed to connect with {avs_url}, err: {res.status_code}")
229+
230+
avs_report = res.content
231+
with open(avs_report_path, "wb") as fh:
232+
fh.write(avs_report)
233+
234+
# Register enclave with governor
235+
avs_report = avs_report.decode('utf-8')
236+
avs_report = json.loads(avs_report)
237+
238+
return avs_report
239+
240+
241+
def fetch_attestation_env_vars():
242+
"""Fetches attestation environment variables from the system.
243+
This function retrieves the ITA API key, AVS URL, and attestation report path
244+
from the environment variables. If the attestation report path is not set,
245+
it defaults to None.
246+
Args:
247+
None
248+
Raises:
249+
None
250+
251+
Returns:
252+
dict: A dictionary containing the attestation environment variables.
253+
"""
254+
env_vars = {
255+
"ITA_API_KEY": os.getenv("ITA_API_KEY"),
256+
"AVS_URL": os.getenv("AVS_URL"),
257+
"ATTESTATION_REPORT_PATH": os.getenv("ATTESTATION_REPORT_PATH", None)
258+
}
259+
return env_vars

0 commit comments

Comments
 (0)