各位同仁、技术爱好者们,
欢迎来到今天的讲座。在AI时代,特别是大型语言模型(LLM)驱动的智能体和复杂工作流日益普及的今天,我们享受着前所未有的智能便利,但同时也面临着一个日益严峻的挑战:数据隐私与安全。当我们的AI系统处理敏感的个人信息、商业机密甚至是国家安全相关的数据时,如何确保这些数据在整个处理过程中不被泄露,成为摆在我们面前的头等大事。
今天的讲座,我们将深入探讨一个前沿且至关重要的主题:TEE-based Node Execution——如何将LangGraph的核心节点运行在可信执行环境(TEE)中,以从根本上防止数据外泄。我们将从LangGraph的工作原理讲起,逐步引入TEE的概念,然后构建一个在架构上严谨、在代码上可行的解决方案,并探讨其所面临的挑战与未来的发展方向。
1. 引言:AI 工作流中的保密性需求
想象一下,你正在构建一个基于LLM的金融分析智能体,它需要访问客户的交易历史、投资组合,并结合最新的市场报告来提供个性化的投资建议。或者,一个医疗诊断助手,需要分析病患的详细病历、基因组数据,给出初步的诊断意见。这些场景有一个共同点:它们都涉及高度敏感的数据。
在传统的云计算环境中,即使数据在传输过程中和静止时都进行了加密,但在数据“使用中”(in-use)时,即数据在CPU和内存中进行处理时,它通常是解密的。这意味着,恶意软件、操作系统漏洞,甚至是云服务提供商的内部人员,都有可能访问到这些明文数据。对于AI工作流而言,每一次对LLM的调用,每一次中间状态的传递,每一次对外部工具的访问,都可能成为数据泄露的潜在风险点。
这就是保密计算(Confidential Computing)应运而生的地方。通过利用可信执行环境(Trusted Execution Environment, TEE),我们可以在硬件层面创建一个隔离的、受保护的区域,确保代码和数据即使在操作系统或云平台被攻破的情况下,也能保持机密性和完整性。
LangGraph作为一个强大的框架,能够帮助我们构建复杂、有状态的LLM智能体和多步骤工作流。它通过节点(Node)和边(Edge)的概念,将复杂的逻辑拆解为模块化的组件。将LangGraph的核心节点运行在TEE中,意味着我们可以在硬件信任根的保护下,执行敏感的AI处理任务,从而将数据泄露的风险降至最低。
2. 理解核心技术:LangGraph 与 TEE
在深入探讨解决方案之前,我们首先需要对LangGraph和TEE这两个核心技术有一个扎实的理解。
2.1 LangGraph 深度解析:构建有状态的 AI 工作流
LangGraph是LangChain生态系统中的一个高级库,它专注于构建具有循环和复杂交互逻辑的代理(Agent)。与传统的链式调用不同,LangGraph允许我们以图(Graph)的形式定义工作流,其中包含了节点(Nodes)、边(Edges)和状态(State)。
LangGraph 的核心概念:
- 节点 (Nodes): 图中的基本处理单元。每个节点封装了一个特定的操作,例如调用LLM、执行工具、进行数据处理或决策。节点接收当前状态作为输入,并返回一个更新后的状态。
- 边 (Edges): 连接节点,定义了数据流和控制流。
- 普通边 (Conditional Edges): 从一个节点指向另一个节点,表示处理的顺序。
- 条件边 (Conditional Edges): 基于节点输出的条件来决定下一个要执行的节点。这使得LangGraph能够实现复杂的决策逻辑和循环。
- 状态 (State): LangGraph的核心特性之一。整个图的执行是围绕一个共享的、可变的状态对象进行的。每个节点都会读取当前状态,执行操作,并可能更新状态,然后将更新后的状态传递给下一个节点。这种有状态的特性是构建复杂代理的关键,它允许代理在多个步骤中记住上下文、积累信息。
- 记忆 (Memory): 通常通过将状态存储在外部数据库或缓存中来实现,以支持长期对话或跨多个会话的上下文保留。
为什么 LangGraph 适用于复杂代理?
- 状态管理: LangGraph的共享状态机制使得在多个步骤之间传递和更新信息变得非常自然,是构建有记忆的代理的基石。
- 循环和决策: 条件边允许代理根据LLM的输出或其他逻辑进行决策,甚至可以创建自修正或迭代优化的循环,这是LangChain表达式语言(LCEL)难以直接实现的。
- 模块化和可维护性: 将复杂任务分解为独立的节点,每个节点负责一个特定功能,提高了代码的可读性和可维护性。
- 可观测性: 图的结构使得理解代理的执行路径和状态变化变得更容易。
一个简单的 LangGraph 示例:一个基础的 QA 代理
让我们看一个非常简化的LangGraph结构,它能接收用户查询,调用LLM进行回答,并可能进行一次工具调用来获取额外信息。
from typing import TypedDict, Annotated, List, Dict
import operator
from langgraph.graph import StateGraph, END
from langchain_core.messages import BaseMessage
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
# 1. 定义图的共享状态
class AgentState(TypedDict):
"""
定义代理的状态。
- messages: 对话历史,作为List[BaseMessage]
- query: 当前的用户查询
- tool_output: 工具调用的结果
"""
messages: Annotated[List[BaseMessage], operator.add]
query: str
tool_output: str
# 2. 定义工具(这里只是一个模拟工具)
@tool
def search_web(query: str) -> str:
"""Simulates searching the web for information."""
print(f"DEBUG: Performing web search for: {query}")
if "最新天气" in query:
return "北京今天多云,气温20-28度。"
return f"找到了关于 '{query}' 的一些信息。"
# 3. 定义LLM模型
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# 4. 定义图的节点
def call_llm(state: AgentState):
"""调用LLM生成响应或决定是否使用工具。"""
print("DEBUG: Node 'call_llm' executed.")
messages = state["messages"]
response = llm.invoke(messages)
# 假设LLM响应中包含一个工具调用指令
# 在实际LangGraph中,这里会解析LLM的StructuredToolOutput
if "需要工具" in response.content and "search_web" in response.content:
# 模拟LLM决定调用工具
tool_call_query = messages[-1].content # 假设工具调用基于最新用户消息
return {"messages": messages + [response], "query": tool_call_query}
return {"messages": messages + [response]}
def call_tool(state: AgentState):
"""执行工具调用。"""
print("DEBUG: Node 'call_tool' executed.")
query = state["query"]
tool_result = search_web.invoke(query)
# 将工具结果作为新的消息添加到对话历史中,以便LLM可以继续处理
return {"messages": state["messages"] + [BaseMessage(content=f"Tool Output: {tool_result}")],
"tool_output": tool_result}
# 5. 定义条件边(Router)
def route_decision(state: AgentState) -> str:
"""根据LLM的响应决定下一步是调用工具还是结束。"""
print("DEBUG: Node 'route_decision' executed.")
# 检查LLM的最新消息,看它是否包含了工具调用指示
latest_message_content = state["messages"][-1].content
if "需要工具" in latest_message_content:
return "call_tool"
return "END" # 否则结束
# 6. 构建图
def build_simple_qa_graph():
workflow = StateGraph(AgentState)
# 添加节点
workflow.add_node("llm_node", call_llm)
workflow.add_node("tool_node", call_tool)
# 设置入口点
workflow.set_entry_point("llm_node")
# 添加边
# LLM节点之后,根据其输出决定是调用工具还是直接结束
workflow.add_conditional_edges(
"llm_node",
route_decision,
{
"call_tool": "tool_node",
"END": END
}
)
# 工具节点之后,再次回到LLM节点,让LLM处理工具结果
workflow.add_edge("tool_node", "llm_node")
# 编译图
app = workflow.compile()
print("LangGraph compiled successfully.")
return app
# 7. 运行图
if __name__ == "__main__":
from langchain_core.messages import HumanMessage
app = build_simple_qa_graph()
print("n--- Running example 1: Simple LLM query ---")
initial_state_1 = {"messages": [HumanMessage(content="中国的首都是哪里?")], "query": "", "tool_output": ""}
for s in app.stream(initial_state_1):
print(s)
# 预期输出:LLM直接回答
print("n--- Running example 2: Query requiring tool use ---")
initial_state_2 = {"messages": [HumanMessage(content="请告诉我北京的最新天气,并说明需要工具来查询。")], "query": "", "tool_output": ""}
for s in app.stream(initial_state_2):
print(s)
# 预期输出:LLM判断需要工具 -> 调用工具 -> LLM处理工具结果并回答
这个例子展示了LangGraph如何通过节点和条件边来处理多步逻辑。然而,query、messages 和 tool_output 都可能包含敏感信息,它们在节点之间传递时,通常是以明文形式存在于内存中。这正是TEE需要解决的问题。
2.2 可信执行环境 (TEE):硬件层面的堡垒
可信执行环境(TEE)是一种硬件技术,它在处理器内部创建了一个隔离的、安全的区域。这个区域被称为“飞地”(Enclave),它能够保护代码和数据,即使主机操作系统、Hypervisor、BIOS或任何其他在TEE之外运行的软件被攻破,也无法访问或篡改飞地内部的数据和代码。
TEE 的核心特性:
- 机密性 (Confidentiality): 飞地内部的数据和代码对飞地外部是不可见的。即使是物理攻击者,也无法在飞地内部数据被处理时获取其明文形式。
- 完整性 (Integrity): 飞地内部运行的代码和数据在执行期间不能被飞地外部的实体篡改。
- 可证明性/远程证明 (Attestation): 这是一个关键特性。飞地可以向远程的、信任方提供一个加密证明(通常称为“引用”或“报告”),证明它正在运行的是经过验证的特定代码版本,并且其硬件配置是安全的。信任方(例如客户端)可以在向TEE发送敏感数据之前,验证这个证明,从而建立信任。
- 数据密封 (Sealing): 飞地可以将加密数据(例如加密密钥或敏感配置)“密封”到只有特定飞地(或具有相同代码和配置的飞地)才能解密的存储中。这允许TEE在重启后安全地恢复其状态或秘密。
主流 TEE 技术:
| TEE 技术名称 | 主要提供商 | 隔离粒度 | 典型应用场景 | 优势 | 挑战 |
|---|---|---|---|---|---|
| Intel SGX | Intel | 进程级飞地 | 区块链、DLP、AI推理 | 硬件隔离,内存加密,细粒度控制 | 编程模型复杂,内存限制,I/O限制 |
| AMD SEV | AMD | VM级加密 | 机密虚拟机、容器 | 易于部署现有应用,对OS透明 | 隔离粒度不如SGX,VM启动时仍需信任Hypervisor加载 |
| ARM TrustZone | ARM | OS级隔离 | 移动设备、IoT安全、DRM | 嵌入式设备广泛应用,低功耗 | 主要针对嵌入式,与云计算模型有差异 |
| Confidential Containers (CC) | Kubernetes/云厂商 | 容器/VM级 | 机密容器化应用 | 结合容器编排,易于部署 | 依赖底层Confidential VM技术 |
今天的讨论将主要关注于进程级飞地(如Intel SGX)或机密虚拟机/容器(如AMD SEV、Confidential Containers)。进程级飞地提供了最强的隔离,但通常对应用有更高的改造要求;机密虚拟机/容器则更容易将现有应用迁移进去,但其信任边界略有不同。为了达到最高级别的数据保密,我们将着重讨论如何将LangGraph的Python运行时本身置于TEE的保护之下。
TEE 的工作原理(高层):
- 飞地加载: 一个非信任的主机应用程序(Host Application)启动并加载一个飞地。这个飞地包含了一段预先定义好的、经过签名的代码。
- 内存隔离: 飞地运行时,其代码和数据被放置在处理器内部一块特殊的、加密的内存区域(如SGX的Enclave Page Cache, EPC)中。对这块内存的任何访问都必须经过硬件检查,确保只有飞地内部的代码才能访问明文数据。
- 度量与证明: 飞地在启动时,硬件会计算其代码和初始数据的加密哈希(“度量”)。这个度量值连同其他硬件信息,会被签名并生成一个证明报告。
- 远程证明: 客户端或信任方接收到这个证明报告后,可以独立验证其真实性,并核对度量值是否与预期的飞地代码哈希匹配。如果匹配,客户端就可以确信它正在与一个真实的、未被篡改的飞地进行通信。
- 安全通信: 建立信任后,客户端可以使用飞地的公钥加密敏感数据,然后发送给飞地。飞地使用其内部的私钥解密数据并进行处理。飞地处理后的结果也可以用对称密钥(在安全通道建立时协商)或客户端的公钥加密后返回。
3. 问题陈述:连接 LangGraph 与 TEE
现在我们已经理解了LangGraph如何构建复杂工作流以及TEE如何提供硬件级隔离。挑战在于如何将两者结合,使得LangGraph工作流中的敏感数据始终处于TEE的保护之下。
LangGraph 工作流中的数据泄露风险点:
- 输入数据: 当用户将敏感查询(如金融报告、病历)提交给LangGraph代理作为初始状态时。
- 中间状态: LangGraph的
AgentState在节点之间传递,或在循环中更新时,包含着LLM的中间思考、工具调用的中间结果等,这些都可能是敏感的。 - LLM API 调用:
- 提示 (Prompts): 发送给LLM的提示可能包含敏感的用户数据。
- 响应 (Responses): LLM返回的响应可能包含对敏感数据的分析结果。
- API 密钥: 访问LLM服务所需的API密钥本身是敏感的,不应暴露给主机操作系统。
- 工具调用 (Tool Calls):
- 工具参数: 传递给外部工具的参数可能包含敏感信息。
- 工具结果: 外部工具返回的结果可能包含敏感信息。
- 工具凭证: 访问外部服务(如数据库、第三方API)所需的凭证。
- 日志与监控: 传统的日志和监控系统可能会记录敏感数据,从而导致泄露。
- 持久化存储: 如果LangGraph的状态需要持久化,例如为了恢复或长期记忆,这些存储也需要加密保护。
为什么传统安全措施不足?
- 软件层面的限制: 操作系统级别的加密、访问控制列表(ACL)等软件安全措施,都无法抵御一个拥有更高权限的攻击者(如恶意root用户、被攻破的Hypervisor或云服务商本身)。
- 内存中的明文数据: 即使数据在磁盘上加密,在网络传输中加密,一旦加载到内存中供CPU处理,它通常就是明文的。这就是TEE主要解决的“使用中数据”的保护问题。
我们的目标:
确保LangGraph的整个执行环境,包括Python解释器、LangGraph库、节点函数、LLM API密钥、工具凭证以及所有敏感数据(输入、中间状态、输出),都只在TEE内部以明文形式存在和处理。飞地外部的任何实体,包括主机操作系统和云服务提供商,都无法访问这些敏感的明文信息。
4. 架构设计:TEE 中的 LangGraph 节点执行
为了实现上述目标,我们需要重新构想LangGraph的部署和执行架构。核心思想是将LangGraph的运行时环境及其敏感节点整体封装在TEE内部。
4.1 核心原则
- 全栈 TEE 化: 尽量将LangGraph的核心执行逻辑(包括Python解释器、LangGraph库、代理定义、节点实现)全部放入TEE。
- 最小化信任基础 (TCB): 飞地内只包含必要的代码和依赖,以减少攻击面。
- 加密输入/输出: 敏感输入数据在进入TEE之前由客户端加密;TEE处理后的敏感输出数据在离开TEE之前由TEE加密。
- 安全凭证管理: LLM API密钥、工具凭证等敏感信息仅在TEE内部解密和使用,永不暴露于TEE外部。
- 远程证明驱动信任: 客户端在向TEE发送任何敏感数据之前,必须通过远程证明验证TEE的完整性和真实性。
4.2 架构组件分解
我们将整个系统分解为以下几个关键组件:
-
客户端 (Client):
- 用户界面或另一个服务。
- 负责生成敏感的初始输入数据。
- 执行远程证明,验证TEE的真实性。
- 使用TEE的公钥加密敏感输入数据。
- 接收并解密TEE的加密输出数据。
-
非信任主机应用程序 (Untrusted Host Application):
- 运行在传统操作系统上,不被信任。
- 负责启动和管理TEE。
- 将客户端加密的输入数据传递给TEE。
- 从TEE接收加密输出数据,并将其转发给客户端。
- 处理TEE的非敏感输出(如审计日志的加密哈希)。
- 绝不能访问飞地内的明文数据。
-
TEE 飞地 (TEE Enclave):
- 核心安全区域,运行在硬件隔离的CPU上。
- 包含:
- Python 运行时: 一个精简的Python解释器及其标准库。
- LangGraph 库及其依赖: LangChain, LangGraph, Pydantic等。
- LangGraph 代理定义: 包括
AgentState、节点函数和图的结构。 - 安全密钥库: 存储LLM API密钥、工具凭证等,这些密钥在TEE启动时通过安全机制加载或在TEE内部生成并密封。
- TEE 加密/解密模块: 用于解密输入和加密输出。
- 安全 LLM 代理/工具代理: 负责从TEE内部安全地调用外部LLM服务和工具。
- 所有敏感数据(输入、中间状态、输出)在飞地内部都是明文处理。
-
外部服务代理 (External Service Proxies):
- 运行在TEE内部的模块。
- LLM 代理: 负责将飞地内部生成的提示通过加密通道发送给外部LLM服务(如OpenAI API),并接收响应。LLM API密钥在此代理内部使用,永远不会离开飞地。
- 工具代理: 负责将飞地内部生成的工具调用请求通过加密通道发送给外部工具或数据库,并接收结果。工具凭证在此代理内部使用。
数据流与安全模型:
以下表格总结了数据在不同组件之间的流动以及其安全状态:
| 步骤 | 数据来源 | 数据流向 | 数据状态 | 保护机制 |
|---|---|---|---|---|
| 1. 初始输入 | 客户端 | TEE 公钥 | 明文 (客户端) -> 加密 (传输) | 客户端使用 TEE 公钥进行非对称加密 |
| 2. 数据接收 | 非信任主机 | TEE 飞地 | 加密 (传输) -> 明文 (飞地内部) | TEE 硬件隔离,飞地私钥解密 |
| 3. LangGraph 运行 | TEE 飞地 | TEE 飞地 (节点间) | 明文 (飞地内部) | TEE 硬件隔离,内存加密 |
| 4. LLM 调用 | TEE 飞地 | 外部 LLM 服务 | 加密 (传输) | TEE 内部 LLM 代理使用安全通道 (HTTPS),API 密钥不离开飞地 |
| 5. 工具调用 | TEE 飞地 | 外部工具/DB | 加密 (传输) | TEE 内部工具代理使用安全通道 (HTTPS),凭证不离开飞地 |
| 6. 中间状态 | TEE 飞地 | TEE 飞地 (内存) | 明文 (飞地内部) | TEE 硬件隔离,内存加密 |
| 7. 最终输出 | TEE 飞地 | 客户端 | 明文 (飞地内部) -> 加密 (传输) | TEE 使用客户端公钥或协商的对称密钥加密 |
| 8. 输出接收 | 非信任主机 | 客户端 | 加密 (传输) -> 明文 (客户端) | 客户端使用自身私钥或协商的对称密钥解密 |
信任边界:
- 信任区域: TEE 飞地内部及其所依赖的硬件信任根。
- 非信任区域: 客户端应用程序(可能被攻破)、非信任主机操作系统、Hypervisor、云基础设施、外部 LLM 服务、外部工具和数据库。
通过这种架构,即使非信任主机或云提供商试图窥探,它们也只能看到加密的数据包,无法获取LangGraph工作流中任何敏感的明文信息。
5. 实施策略与挑战
将LangGraph运行在TEE中是一个复杂的多学科工程,涉及TEE平台选择、Python运行时适配、LangGraph集成以及安全通信机制的构建。
5.1 选择 TEE 平台
对于运行Python应用,以下是两种主要的选择:
-
Intel SGX (通过 Gramine 或 Open Enclave SDK):
- 优势: 提供进程级的硬件隔离,信任边界非常小,安全性最高。通过Gramine等框架,可以在SGX飞地中运行未经修改的Linux二进制文件,包括Python解释器。
- 挑战:
- Python适配: Python解释器及其大量C扩展(如NumPy, Pandas, PyTorch)需要在Gramine环境中进行特殊打包和构建。依赖管理复杂。
- 内存限制: SGX飞地通常有较小的内存限制(传统上为256MB,虽然EDMM技术有所缓解,但对于大型LLM模型和复杂Python应用仍是挑战)。
- I/O 限制: 飞地内对文件系统和网络I/O的访问需要通过主机代理,性能可能受影响,且需要仔细配置访问权限。
- 调试困难: 在飞地内调试非常困难,工具支持有限。
-
Confidential VMs (如 AMD SEV-SNP, Intel TDX, AWS Nitro Enclaves):
- 优势: 将整个虚拟机(包括OS和应用)加密并在硬件保护下运行。对应用程序透明,几乎无需修改即可运行现有Python应用。内存和CPU资源通常与普通VM相似,可以运行大型LLM。
- 挑战:
- 信任边界更大: 整个VM是一个信任区域,意味着OS、Hypervisor驱动等都在信任链中,相比SGX的进程级隔离,TCB更大。
- 启动时攻击面: VM启动时加载OS和应用,这个过程可能存在攻击面。
- Attestation 复杂性: 虽然提供了VM级别的Attestation,但验证整个VM镜像的完整性比验证一个小的SGX飞地更复杂。
我们的侧重: 鉴于LangGraph主要关注数据处理和LLM调用,对数据机密性要求极高,我们将更侧重于Intel SGX + Gramine这种提供极致隔离的方案,因为它能将Python解释器和LangGraph运行时封装在一个最小化的信任区域内。当然,许多概念也适用于Confidential VMs。
5.2 Python 在 TEE 中的适配挑战
- 运行时大小: Python解释器及其常用库(如LangChain, LangGraph, OpenAI SDK, Pydantic)本身就很大。在SGX内存受限的环境中,需要精简依赖,甚至可能需要定制Python构建。
- C 扩展: 许多高性能Python库(如NumPy, Scipy)依赖于C/C++扩展。这些扩展需要在TEE兼容的环境中编译,并确保其依赖的底层系统库(如glibc)也能在TEE中运行。Gramine在这方面做了大量工作,但仍需细致配置。
- 文件 I/O 与网络 I/O: 飞地内的所有I/O操作都需要通过主机代理。这意味着需要明确声明飞地可以访问哪些文件和网络地址。例如,连接外部LLM API需要允许对
api.openai.com:443的访问。 - 动态性: Python的动态特性,如运行时代码生成、反射等,可能与TEE的静态度量(Attestation时代码哈希)产生冲突。不过,对于LangGraph这种结构化的应用,通常不是主要问题。
5.3 LangGraph 适应性改造
- 状态的加密与解密:
- 初始输入: 客户端在向TEE发送初始LangGraph状态之前,必须使用TEE的公钥对其进行加密。
- 中间状态: 在TEE内部,LangGraph的状态始终是明文。如果需要将部分状态临时存储到TEE外部(例如,进行大规模持久化),则必须在存储前加密,并在加载回TEE时解密。通常,我们倾向于将所有敏感中间状态保留在TEE内存中。
- LLM API 密钥管理:
- LLM API 密钥(如OpenAI API Key)绝不能硬编码在代码中。
- 密钥应通过数据密封机制在TEE内部安全地存储和加载。例如,在TEE启动时,从一个主机提供的加密文件中加载密钥,并在TEE内部用硬件密钥解密,然后重新密封。
- TEE内部的LLM代理负责使用这些密钥,通过TLS连接与外部LLM服务通信。
- 工具集成:
- 安全代理: 每个外部工具调用都需要通过TEE内部的一个“安全工具代理”进行。这个代理负责:
- 验证工具调用参数。
- 使用TEE内部安全存储的工具凭证(如数据库连接字符串、API Key)。
- 通过加密通道(如HTTPS)与外部工具通信。
- 对返回结果进行初步验证和过滤。
- 白名单: 飞地应只允许访问预先批准的外部服务IP地址和端口,以防止恶意工具调用将数据发送到未授权的目的地。
- 安全代理: 每个外部工具调用都需要通过TEE内部的一个“安全工具代理”进行。这个代理负责:
- 错误处理与调试: TEE内部的错误日志通常是加密的,或只能提供有限的调试信息,这使得调试变得异常困难。需要设计健壮的错误处理机制,并在飞地外部建立安全的审计日志系统,只记录非敏感的事件。
5.4 远程证明与安全通信流程
这是建立信任和数据机密性的核心。
- 客户端请求: 客户端需要执行LangGraph工作流,并拥有敏感输入数据。
- 主机启动 TEE: 客户端向非信任主机发送请求,要求其启动一个LangGraph TEE飞地。
- TEE 生成证明: TEE硬件启动,加载LangGraph运行时。硬件计算飞地代码和数据的加密哈希(测量值),并生成一个包含这些测量值和公钥的证明报告(Attestation Report)。
- 主机转发证明: 非信任主机从TEE获取证明报告,并将其转发给客户端。
- 客户端验证证明:
- 客户端从一个可信的第三方(Attestation Service)获取预期的TEE测量值(即LangGraph TEE飞地的预期代码哈希)。
- 客户端验证收到的证明报告是否由合法的TEE硬件签署。
- 客户端比对证明报告中的测量值与预期的测量值。
- 如果验证成功,客户端确信它正在与一个真实的、未被篡改的LangGraph TEE飞地通信。
- 安全密钥交换与数据加密:
- 客户端从验证过的证明报告中提取TEE的公钥。
- 客户端使用此公钥加密其敏感输入数据,然后发送给非信任主机。
- 非信任主机将加密数据传递给TEE飞地。
- TEE 解密与执行:
- TEE飞地使用其内部的私钥解密输入数据。
- LangGraph工作流在飞地内部以明文形式执行。
- 所有中间状态、LLM调用、工具调用都发生在飞地内部。
- TEE 加密输出:
- LangGraph完成执行,生成最终的敏感输出。
- TEE使用客户端的公钥(或其他协商的对称密钥)加密输出数据。
- 主机转发输出: 非信任主机将加密输出数据转发给客户端。
- 客户端解密: 客户端使用其私钥(或协商的对称密钥)解密输出数据。
6. 代码概念化:TEE-Enabled LangGraph Node
由于完整的TEE环境设置(如Gramine manifest编写、Python运行时打包)非常复杂且超出本次讲座的范围,我们将通过概念性代码来展示如何在Python层面实现TEE-enabled LangGraph的核心思想。我们将模拟TEE内外的交互、加密解密以及LLM/工具的安全代理。
假设:
- 我们有一个“主机”Python脚本,负责与外部世界交互并启动TEE。
- 我们有一个“飞地”Python脚本,它代表了运行在TEE内部的LangGraph环境。
- 使用
cryptography库进行数据加密和解密,模拟TEE的密钥交换和数据保护。 - 使用
subprocess模拟主机调用TEE。
6.1 主机端 Python 脚本 (host_tee_manager.py)
这个脚本负责与客户端通信、启动TEE(这里是模拟),以及加密/解密数据。
# host_tee_manager.py
import hashlib
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.backends import default_backend
import subprocess
import json
import base64
import os
# --- 模拟 TEE 密钥对生成和 Attestation ---
# 在真实的TEE场景中,TEE的私钥永不离开TEE,公钥通过Attestation报告提供。
# 这里为了演示,我们模拟生成一对密钥,并假设私钥在TEE内部,公钥提供给主机/客户端。
_tee_private_key = None # Placeholder for TEE's private key (conceptual, never exposed)
_tee_public_key = None # Placeholder for TEE's public key (to be shared)
def _generate_tee_key_pair():
"""模拟TEE内部生成密钥对,私钥仅TEE可访问,公钥可导出。"""
print("Simulating TEE Key Pair Generation...")
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
public_key = private_key.public_key()
return private_key, public_key
def get_tee_public_key_and_attestation(tee_measurement_hash: str):
"""
模拟远程Attestation过程。
客户端会请求TEE的公钥和Attestation报告。
"""
global _tee_private_key, _tee_public_key
if _tee_private_key is None or _tee_public_key is None:
_tee_private_key, _tee_public_key = _generate_tee_key_pair()
print("Simulating Attestation Report Generation...")
# 模拟Attestation报告,包含TEE的测量值和公钥
# 真实报告会经过硬件签名
simulated_attestation_report = {
"enclave_measurement": tee_measurement_hash,
"public_key_pem": _tee_public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8'),
"signature": "simulated_hardware_signature_for_attestation_report" # 真实场景会由TEE硬件签名
}
print("TEE Public Key and Attestation Report Simulated.")
return _tee_public_key, simulated_attestation_report
# --- 数据加密/解密函数 ---
def encrypt_data_for_tee(public_key, data: Dict) -> str:
"""使用TEE的公钥加密数据。"""
plaintext_bytes = json.dumps(data).encode('utf-8')
ciphertext = public_key.encrypt(
plaintext_bytes,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
print("Data encrypted for TEE.")
return base64.b64encode(ciphertext).decode('utf-8')
def decrypt_data_from_tee(private_key, encrypted_data_b64: str) -> Dict:
"""使用客户端的私钥解密TEE的输出数据。"""
ciphertext = base64.b64decode(encrypted_data_b64)
plaintext_bytes = private_key.decrypt(
ciphertext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
print("Data decrypted from TEE.")
return json.loads(plaintext_bytes.decode('utf-8'))
# --- 模拟 TEE 内部的 LLM API Key (在真实场景中,此密钥由TEE内部安全加载) ---
_TEE_SECURE_LLM_API_KEY = "sk-simulated_tee_key_abcdef1234567890"
def run_langgraph_in_tee_simulated(encrypted_input_data_b64: str, tee_measurement_hash: str) -> str:
"""
模拟在TEE中运行LangGraph。
在真实场景中,这会调用Gramine或Confidential VM。
"""
print(f"n--- Host: Invoking TEE (simulated) ---")
# 模拟TEE环境启动,并传递加密输入和API Key(通过环境变量模拟安全注入)
# 真实Gramine命令示例:
# cmd = ["gramine-sgx", "python3", "enclave_langgraph_runtime.py", encrypted_input_data_b64]
# env = os.environ.copy()
# env["OPENAI_API_KEY_TEE"] = _TEE_SECURE_LLM_API_KEY # 注入到飞地
# result = subprocess.run(cmd, capture_output=True, text=True, env=env, check=True)
# 这里我们直接调用enclave_langgraph_runtime.py脚本,并模拟其输出
# 环境变量 OPENAI_API_KEY_TEE 模拟安全注入到 TEE 内部
env = os.environ.copy()
env["OPENAI_API_KEY_TEE"] = _TEE_SECURE_LLM_API_KEY
cmd = ["python3", "enclave_langgraph_runtime.py", encrypted_input_data_b64]
try:
# 使用check=True确保如果子进程返回非零退出码会抛出CalledProcessError
result = subprocess.run(cmd, capture_output=True, text=True, env=env, check=True)
print("--- TEE (simulated) stdout ---")
print(result.stdout)
if result.stderr:
print("--- TEE (simulated) stderr ---")
print(result.stderr)
# 假设TEE的最终输出是stdout的最后一行JSON
# 在真实场景中,TEE会通过特定接口返回加密数据
tee_raw_output = result.stdout.strip().splitlines()[-1]
print(f"--- Host: Received raw output from TEE (simulated): {tee_raw_output[:100]}...")
return tee_raw_output
except subprocess.CalledProcessError as e:
print(f"--- Host: TEE (simulated) execution failed! ---")
print(f"Stderr: {e.stderr}")
print(f"Stdout: {e.stdout}")
raise
# --- 客户端逻辑 (运行在主机上,但代表一个独立的信任实体) ---
if __name__ == "__main__":
print("--- Client: Initializing ---")
# 1. 客户端生成自己的密钥对,用于解密TEE的输出
client_private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
client_public_key = client_private_key.public_key()
# 2. 定义敏感输入数据
sensitive_user_query = {
"user_id": "user123",
"query": "请分析公司 'GlobalTech Solutions' 最近一个季度的财报,特别是其研发投入与市场份额增长的关系,并评估潜在的内幕交易风险。",
"report_data": { # 模拟敏感财报数据
"revenue": "1.2B",
"net_profit": "150M",
"rd_spend": "300M",
"market_share_growth": "5%",
"insider_trades_last_3_months": [
{"exec": "CEO", "type": "sell", "amount": "10M USD", "date": "2023-10-15"},
{"exec": "CTO", "type": "buy", "amount": "2M USD", "date": "2023-11-01"}
]
}
}
print(f"Client: Sensitive input query: '{sensitive_user_query['query'][:80]}...'")
# 3. 客户端请求主机启动TEE,并获取其Attestation报告和公钥
# (tee_measurement_hash 应该是一个预期的、来自可信源的哈希值)
# 这里我们使用一个模拟的哈希值,它代表了 enclave_langgraph_runtime.py 的代码哈希
# 在真实世界中,客户端会独立计算或从可信Attestation服务获取此值
dummy_tee_code_hash = hashlib.sha256(open("enclave_langgraph_runtime.py", "rb").read()).hexdigest()
print(f"n--- Client: Requesting TEE Attestation from Host ---")
tee_public_key, attestation_report = get_tee_public_key_and_attestation(dummy_tee_code_hash)
# 4. 客户端验证 Attestation 报告
# 在真实场景中,客户端会验证签名,并比对 enclave_measurement
if attestation_report['enclave_measurement'] == dummy_tee_code_hash:
print(f"Client: TEE Attestation Verified! Enclave measurement matches expected hash: {attestation_report['enclave_measurement']}")
else:
print("Client: WARNING! TEE Attestation FAILED. Enclave measurement mismatch.")
exit(1)
# 5. 客户端使用 TEE 的公钥加密敏感输入数据
encrypted_input_for_tee = encrypt_data_for_tee(tee_public_key, sensitive_user_query)
print(f"Client: Encrypted input (b64): {encrypted_input_for_tee[:100]}...")
# 6. 客户端将加密输入发送给主机,主机将它传递给TEE
print(f"n--- Client: Sending encrypted input to Host for TEE execution ---")
tee_output_json_string = run_langgraph_in_tee_simulated(encrypted_input_for_tee, dummy_tee_code_hash)
# 7. 假设TEE的输出也是加密的(这里为了简化演示,TEE返回的是一个包含加密结果的JSON)
# 在真实场景中,TEE会用客户端公钥加密最终结果。
# 这里我们假设TEE的输出JSON中有一个 "encrypted_result" 字段,用客户端公钥加密。
# 重新加载TEE的私钥,因为它是运行在TEE内部的,主机不可访问。
# 这里为了演示方便,我们直接从全局变量获取,但请记住这是概念上的私钥。
tee_internal_private_key = _tee_private_key
# 假设TEE返回的JSON字符串中,加密的最终结果也是用TEE内部的私钥加密的
# 并且TEE会把它用客户端的公钥再次加密。这里我们简化为TEE直接返回最终结果的JSON
# 并且为了演示,我们将TEE返回的最终结果,假设它已经用客户端公钥加密
# 但是,我们这里简化为TEE直接返回明文JSON,由客户端直接解析
# 在实际情况中,TEE会用`client_public_key`来加密最终结果
# For this simplified demo, let's assume TEE *would* have encrypted its output with client_public_key.
# But for the purpose of demonstrating the LangGraph flow *inside* the TEE,
# let's assume `run_langgraph_in_tee_simulated` returns the *final LangGraph state* as JSON.
# In a real TEE, this final JSON would be further encrypted using `client_public_key`
# before being sent back to the host, and then to the client.
# So, for the demo, we assume the last line of stdout from TEE is the final (conceptually encrypted) output.
# Let's parse it as if it's the final output that *needs* decryption (even if it's currently plaintext JSON).
# In a real scenario, TEE would encrypt with `client_public_key` before returning.
# Let's adjust the demo to show decryption by client.
# We need to simulate the TEE encrypting its final output *with the client's public key*.
# For this, the TEE needs the client's public key. Let's pass it as part of the initial encrypted input.
# Re-design:
# 1. Client sends its public key *along with* the sensitive input, encrypted for TEE.
# 2. TEE decrypts everything, runs LangGraph.
# 3. TEE encrypts final output *using the client's public key*.
# 4. TEE returns this encrypted output (b64 string) to host.
# 5. Host returns to client.
# 6. Client decrypts with its *own* private key.
# Let's restart this section for clarity in the demo.
# For simplicity, let's assume `run_langgraph_in_tee_simulated` actually returns an *encrypted* string
# where the encryption was done by the TEE using `client_public_key`.
# Rerun the host's call to TEE, this time passing client_public_key to TEE for output encryption
# We need to pass client_public_key_pem to TEE within the encrypted input.
sensitive_user_query["client_public_key_pem"] = client_public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8')
encrypted_input_for_tee = encrypt_data_for_tee(tee_public_key, sensitive_user_query)
print(f"n--- Client: Re-sending encrypted input (with client_public_key) to Host for TEE execution ---")
encrypted_output_from_tee_b64 = run_langgraph_in_tee_simulated(encrypted_input_for_tee, dummy_tee_code_hash)
print(f"n--- Client: Received encrypted output from TEE: {encrypted_output_from_tee_b64[:100]}...")
# 8. 客户端使用自己的私钥解密TEE的输出
final_output_from_tee = decrypt_data_from_tee(client_private_key, encrypted_output_from_tee_b64)
print("n--- Client: Final Decrypted Output from TEE ---")
print(json.dumps(final_output_from_tee, indent=2, ensure_ascii=False))
print("n--- Client: Execution Complete ---")
6.2 飞地端 Python 脚本 (enclave_langgraph_runtime.py)
这个脚本模拟了LangGraph在TEE内部的运行逻辑。它接收加密输入,解密,执行LangGraph工作流,然后加密输出。
# enclave_langgraph_runtime.py
# This code runs *inside* the TEE (simulated).
import sys
import json
import os
import base64
from typing import TypedDict, Annotated, List, Dict
import operator
# TEE-specific cryptography (conceptual)
# In a real TEE, _tee_private_key would be generated securely inside the TEE
# and never exposed. It would be used for decryption.
# For demo, we need to load the *same* private key that was used to encrypt
# the initial input for the TEE, as conceptually that key is 'owned' by the TEE.
# In a real setup, a TEE would have its own unique private key securely generated/managed.
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.backends import default_backend
# In a real TEE, this private key would be securely provisioned or generated
# and only accessible within the enclave.
# For this simulation, we'll load it from a pre-defined source,
# but conceptually it's *not* exposed to the host.
# We must use the private key that corresponds to the public key the client used to encrypt.
# For the demo, we'll rely on the host_tee_manager to share it for simulation.
# In a real TEE, this would be generated within the TEE and its public key attested.
# Let's assume for this demo, the host provided its private key to the TEE securely.
# In `host_tee_manager.py`, `_tee_private_key` is created. We conceptually pass it here.
# For direct execution of `enclave_langgraph_runtime.py` for testing, we'd need a way to load it.
# For the `subprocess.run` call, we'll assume the host passes it conceptually.
def get_tee_private_key_simulated():
"""
Simulates getting the TEE's private key securely within the enclave.
In a real TEE, this key is generated internally and never leaves the enclave.
For this demo, we'll generate one, assuming it's the "TEE's" key.
"""
print("TEE Enclave: Simulating secure private key access.")
# This should match the key pair used by the host to encrypt for the TEE.
# For simplicity, we'll generate a new one here, assuming it's the TEE's unique key.
# In a real scenario, the client encrypts with the *attested* public key of this TEE.
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
return private_key
# This is a critical point: the TEE needs its *own* private key to decrypt input.
# For `host_tee_manager.py` to encrypt for this TEE, it needs this TEE's public key.
# So, the TEE's key pair must be established first, its public key attested, then client uses it.
# In this specific demo, `host_tee_manager.py` generates the key pair and keeps the private part
# conceptually 'hidden' but accessible for *this* simulation to decrypt.
# Let's retrieve it from a simulated secure source.
_tee_private_key_for_input_decryption = None # This would be truly internal to TEE
def decrypt_data_in_tee(encrypted_data_b64: str, tee_private_key_for_decryption) -> Dict:
"""Decrypts input data inside TEE using TEE's private key."""
print("TEE Enclave: Decrypting input data inside TEE...")
ciphertext = base64.b64decode(encrypted_data_b64)
plaintext_bytes = tee_private_key_for_decryption.decrypt(
ciphertext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
print("TEE Enclave: Input data decrypted successfully.")
return json.loads(plaintext_bytes.decode('utf-8'))
def encrypt_data_from_tee(data: Dict, client_public_key) -> str:
"""Encrypts output data inside TEE using the client's public key."""
print("TEE Enclave: Encrypting output data for client...")
plaintext_bytes = json.dumps(data, ensure_ascii=False).encode('utf-8')
ciphertext = client_public_key.encrypt(
plaintext_bytes,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
print("TEE Enclave: Output data encrypted successfully.")
return base64.b64encode(ciphertext).decode('utf-8')
# --- Secure LLM Proxy (Conceptual, runs inside TEE) ---
# This class handles secure access to external LLM services.
class SecureLLMProxy:
def __init__(self, api_key_env_name="OPENAI_API_KEY_TEE"):
# In a TEE, API keys are loaded securely from sealed secrets
# and never exposed. We simulate this by reading from an environment variable
# that the host would have securely injected into the TEE.
self.api_key = os.getenv(api_key_env_name)
if not self.api_key:
raise ValueError(f"TEE Enclave: LLM API key '{api_key_env_name}' not found securely in environment.")
print(f"TEE Enclave: LLM API key loaded securely within TEE (key starts with {self.api_key[:4]}...)")
# In a real scenario, we'd initialize the actual LLM client here
# For demo, we'll use a placeholder.
# self.llm_client = OpenAI(api_key=self.api_key)
def invoke(self, prompt: str):
print(f"TEE Enclave: Calling LLM securely from within TEE with prompt: {prompt[:80]}...")
# In a real scenario, this would use an encrypted channel to an external LLM service.
# The LLM API key (`self.api_key`) is used here, but never leaves the TEE in plaintext.
# For simulation, we return a mock response.
response_content = (
f"LLM Response from TEE: Detailed financial analysis for '{prompt[20:50]}...'. "
f"R&D spend seems correlated with market share growth. "
f"Regarding insider trading, the reported activities are within typical ranges for executives, "
f"but a deeper investigation would require more context. Initial risk assessment: Moderate."
)
print("TEE Enclave: LLM response received securely.")
return response_content
_secure_llm_proxy = SecureLLMProxy() # Initialize once within TEE
# --- Secure Tool Proxy (Conceptual, runs inside TEE) ---
# This class handles secure access to external tools/databases.
class SecureToolProxy:
def __init__(self):
# Tools might have their own credentials, also securely loaded in TEE.
self.db_credential = self._get_secret_from_tee_store("DB_CREDENTIAL_TEE")
print(f"TEE Enclave: DB credential loaded securely within TEE (starts with {self.db_credential[:4]}...)")
def _get_secret_from_tee_store(self, key_name):
# Simulate loading secret from a TEE-specific secure store (e.g., sealed file)
return os.getenv(key_name, "db_user:db_password_simulated_securely")
def get_financial_data(self, company_name: str, quarter: str):
print(f"TEE Enclave: Accessing secure DB for '{company_name}' data for '{quarter}'...")
# Simulate secure database query using self.db_credential
# Data fetched would also remain in TEE.
return {
"company": company_name,
"quarter": quarter,
"revenue": "1.25B",
"expenses": "800M",
"rd_investment": "280M",
"market_share_change": "+4.5%"
}
_secure_tool_proxy = SecureToolProxy() # Initialize once within TEE
# --- LangGraph State Definition ---
class AgentState(TypedDict):
query: str
report_data: Dict # Added for sensitive report data
financial_summary: str
risk_assessment: str
history: Annotated[List[str], operator.add]
client_public_key_pem: str # To encrypt final output for client
# --- LangGraph Nodes (Running Inside TEE) ---
def retrieve_sensitive_data_node(state: AgentState):
print("TEE Enclave: Node 'retrieve_sensitive_data_node' executed.")
# Here, we assume `query` and `report_data` are part of the initial decrypted state.
# In a real scenario, this node might securely fetch more data from an internal TEE storage
# or a securely proxied external database.
query = state["query"]
report_data = state["report_data"]
state["history"].append(f"Retrieved sensitive query and report data.")
return {"query": query, "report_data": report_data}
def analyze_financial_report_node(state: AgentState):
print("TEE Enclave: Node 'analyze_financial_report_node' executed.")
query = state["query"]
report_data = state["report_data"]
# Construct a prompt using sensitive data, ensuring it stays in TEE
prompt = (
f"Based on the following query: '{query}' and financial report data: {json.dumps(report_data)}. "
f"Perform a detailed financial analysis focusing on R&D investment vs. market share growth and "
f"evaluate potential insider trading risks. Provide a concise summary and a risk assessment."
)
# Use the secure LLM proxy
llm_response = _secure_llm_proxy.invoke(prompt)
# Parse LLM response (simplified)
financial_summary = llm_response # Placeholder
risk_assessment = "Moderate" # Placeholder, in reality parsed from LLM
state["history"].append(f"Analyzed report using LLM. Summary: {financial_summary[:50]}...")
return {"financial_summary": financial_summary, "risk_assessment": risk_assessment}
def generate_confidential_output_node(state: AgentState):
print("TEE Enclave: Node 'generate_confidential_output_node' executed.")
final_summary = state["financial_summary"]
risk = state["risk_assessment"]
confidential_output = {
"final_analysis_summary": final_summary,
"overall_risk_assessment": risk,
"execution_trace_id": "TEE_EXEC_12345", # Non-sensitive identifier
"sensitive_data_processed": True,
"disclaimer": "This analysis was performed in a Trusted Execution Environment."
}
state["history"].append(f"Generated confidential output.")
return {"financial_summary": confidential_output} # Update state with the final output object
# --- LangGraph Graph Definition and Execution ---
def build_and_run_graph(initial_state: Dict, tee_private_key_for_input_decryption) -> Dict:
print("TEE Enclave: Building LangGraph inside TEE...")
workflow = StateGraph(AgentState)
workflow.add_node("retrieve_data", retrieve_sensitive_data_node)
workflow.add_node("analyze_report", analyze_financial_report_node)
workflow.add_node("generate_output", generate_confidential_output_node)
workflow.set_entry_point("retrieve_data")
workflow.add_edge("retrieve_data", "analyze_report")
workflow.add_edge("analyze_report", "generate_output")
workflow.add_edge("generate_output", END)
app = workflow.compile()
print("TEE Enclave: LangGraph compiled. Executing...")
# Run the graph
final_state = None
for s in app.stream(initial_state):
# Print intermediate state changes for debugging within TEE context
print(f"TEE Enclave: