关于 gpg-mcp
gpg-mcp 是面向 AI Agent 的 GnuPG MCP 服务,用于签名、验证、加密、解密和密钥管理。它通过固定类型的操作、审批与密钥白名单限制风险,私钥留在 gpg-agent 中。
主要功能
- 01签名与签名验证
- 02加密与解密
- 03OpenPGP 密钥管理
- 04高风险操作需审批
- 05审计高风险操作
适用场景
- 01在不向 Agent 暴露私钥时签署内容
- 02仅用白名单密钥解密消息
- 03验证签名并检查策略可接受性
MCP 相关
向 MCP 客户端提供固定类型的 GnuPG 操作工具;私钥材料与口令不会通过 MCP 边界传递。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
gpg-mcp
一个专注于安全的 GnuPG Model Context Protocol 服务。
gpg-mcp 允许 AI Agent 签名、验证、加密、解密和管理 OpenPGP 密钥,且绝不会接触私钥材料。私钥保留在 gpg-agent 中;口令只会传给 pinentry;高风险操作需要明确的人类批准;破坏性操作则要求输入指纹后缀。
**一句话概括威胁模型:**MCP 服务是策略边界,而不是操作系统沙箱。它可以防止 Agent 接触私钥,也可以防止 Agent 在不知情的情况下使用私钥。若要防范已通过同一操作系统用户取得 Shell 访问权限的 Agent,请使用单独的用户运行签名程序,或使用硬件令牌(参见更强的隔离)。
为什么需要它
大多数“GPG MCP”包装器都只是薄薄一层 subprocess Shell:任何带有 command: str 参数的工具,或任何能请求 --export-secret-keys 的 Agent,都能让整个设计失去意义。gpg-mcp 则提供经过精心取舍的固定类型接口:
- **不支持执行任意命令。**没有
run_gpg(args)工具。 - **不支持导出或备份私钥。**无法调用
--export-secret-keys、--export-secret-subkeys和--export-secret-ssh-key。 - **不接受口令参数。**不支持
--passphrase、--passphrase-fd、--passphrase-file、loopback pinentry,也不会通过环境变量或标准输入传递口令。 - **不允许覆盖
--homedir。**密钥环由配置固定。 - **不接受短密钥 ID。**操作必须使用完整的 40 位或 64 位十六进制指纹。
- **不自动获取密钥。**验证时不会悄悄从网络获取密钥。
- **不允许隐藏接收者。**解密前会检查密文,并拒绝未加密给允许使用的密钥的消息。
- **提供响应防护。**如果 gpg 输出任何看起来像私钥材料的内容,调用就会失败,并丢弃输出。
安全模型
服务的保障
- 私钥绝不会跨越 MCP 边界。 列出秘密密钥时仅返回元数据。
签名和解密由
gpg-agent执行。 - 服务永远不会接触口令。 强制使用
--pinentry-mode ask;pinentry 会直接与用户交互。服务无法从 argv、环境变量或 stdin 读取口令。 - 签名和解密仅使用允许列表中的密钥。 共享
~/.gnupg时,空允许列表会同时拒绝这两种操作。使用专用密钥环时,密钥环本身就是允许列表边界。 - 解密前会检查收件人。 解密前会使用
gpg --list-packets确认密文是发给允许的密钥。默认拒绝隐藏收件人,即使同一条消息也有可见的允许收件人。 - 嵌入的签名无效时会中止解密。 如果
verify=true(默认值),且解密后的消息带有无效、过期或已吊销的签名,则不会返回明文。valid是策略判定结果;加密学验证结果会单独报告。多个签名按每个NEWSIG序列聚合;在不支持--proc-all-sigs的 GnuPG 上,则按数据包聚合。只要有任意一个签名不符合策略,就会拒绝操作,与事件顺序无关。 - 授权采用默认拒绝的注册表机制。 每个高风险工具都会指定一项已注册操作。未知操作必须经过审批。复合操作(
sign_and_encrypt,以及带有sign_with的encrypt)会继承其所有组成操作的审批要求,因此仅对签名或仅对加密设置审批门槛都无法绕过。 - 导入前会验证吊销证书。 证书必须包含吊销签名,必须拒绝秘密密钥数据包,必须精确指向所请求的指纹,并且必须能在临时密钥环中确实吊销该密钥。
apply=false是真正的试运行,不会触及真实密钥环。 - 破坏性操作需要输入指定内容确认。 删除、吊销或应用吊销证书时,操作员必须输入指纹的最后 8 个字符。此要求由
destructive_confirmation控制,且独立于require_approval。 - 每项高风险操作都会记录审计日志。 日志记录操作名称、指纹、负载的 SHA-256、大小和决策,绝不记录明文、负载预览或口令。
- 不使用 shell。 命令使用
execve风格的 argv 数组。不进行 shell 插值,也不会因用户字符串导致选项注入。 - 使用最精简的环境。 不会向 gpg 传递
LD_*、GPG_AGENT_INFO、GPG_TTY、SSH_AUTH_SOCK等变量。只保留 pinentry 所需的显示和会话变量。 - 二进制文件和路径经过安全检查。
gpg、gpgconf和zenity会解析为绝对路径,必须是常规文件,且不得允许组或其他用户写入。不安全的zenity会导致本地审批默认拒绝,而不是信任被替换的对话框。 - 资源使用有上限。 限制输入和输出大小,设置每项操作的超时;超时后会终止专用进程组;使用权限为 0700 的临时目录和权限为 0600 的文件。状态输出超出有效状态上限时会默认拒绝,而不会对部分内容进行解析。
- 通用导入器无法执行吊销。
import_public_key会拒绝秘密密钥数据包和任何吊销签名,包括嵌入密钥材料中的吊销签名。吊销必须使用import_revocation,该操作会验证目标并要求输入指定内容确认。 - 会检查实际用于解密的密钥。 解密后,
DECRYPTION_KEY状态必须指明一个允许的主密钥或子密钥;否则会丢弃明文。即使密文含有可见的允许收件人,也不能在不知情的情况下使用另一把秘密密钥解密。 - GnuPG 配置具有确定性。 执行器使用
--no-options和--pinentry-mode ask,因此恶意的gpg.conf无法启用 loopback pinentry 或更改其他与安全相关的默认设置。 - 检查文件系统路径时不会跟随符号链接。 配置路径通过目录文件描述符和
O_NOFOLLOW逐级遍历,打开的文件会通过fstat验证,因此验证与使用之间不会发生竞态,也不会被路径替换重定向。密钥环路径保持未解析状态(不会解析GNUPGHOME的符号链接);serve、doctor和init会验证其完整的祖先路径,并拒绝符号链接形式的密钥环或祖先目录。祖先目录必须由用户或 root 所有;除非设置了 sticky bit,否则不得允许组或其他用户写入。拒绝未知密钥。对现有审计文件执行fstat;拒绝非常规文件或由其他用户所有的文件,并将组和其他用户的访问权限收紧为0600。当audit_required = true时,审计失败会导致操作失败。init使用O_EXCL|O_NOFOLLOW安全创建配置文件;如果gpg-agent.conf是符号链接,则会拒绝操作,而不会通过该链接写入或修改其目标文件权限。缺失的配置、密钥环和审计目录会相对于已验证的目录描述符逐级创建,因此在拒绝符号链接祖先目录之前,不会被其重定向或创建任何内容。审计路径也使用相同的目录描述符遍历方式,因此不安全的祖先目录无法重定向日志。 - 解密路由明确无歧义。
symmetric_decrypt会拒绝同时带有公钥收件人的混合密文。decrypt会拒绝仅使用对称加密的密文,并且除非 GnuPG 报告实际使用了允许列表中的公钥,否则不会返回明文;它绝不会回退到对称解密路由。因此,仅对symmetric_decrypt设置门槛的自定义策略无法通过decrypt绕过。对于混合输入,只容许 GnuPG 报告的、可恢复的对称口令失败,而且仅限于同时有公钥报告DECRYPTION_KEY和DECRYPTION_OKAY的情况;通用ERROR/FAILURE事件和无法解释的非零退出码都会被拒绝。 - 多重签名处理采用默认拒绝机制。 对于不支持
--proc-all-sigs的 GnuPG 版本,分离签名会按数据包逐个处理。对于明文、内联和嵌入式签名,在支持时会使用--proc-all-sigs。在较旧的 GnuPG 上,若消息中第一个签名无效,枚举可能会在第一个无效签名处停止,但仍会拒绝该消息;若所有签名均有效,则会枚举每个签名。 - 对称解密同样严格。 必须出现
DECRYPTION_OKAY;拒绝DECRYPTION_FAILED、BADMDC、ERROR和FAILURE;无法解释的非零退出码会视为致命错误。使用verify=false时,仅当明确识别出嵌入式签名结果,才会容许非零退出码;绝不会忽略无关的 GnuPG 失败。
验证结果语义
verify(以及 decrypt 返回的内嵌签名结果)会区分四个概念:
| 字段 | 含义 |
|---|---|
cryptographically_valid | gpg 输出了 VALIDSIG;签名的数学验证通过 |
policy_acceptable | 此外,还需有 GOODSIG,且签名未过期、密钥未过期、未被吊销,也不存在缺少公钥的情况 |
valid | policy_acceptable 的别名,为了兼容性而保留 |
identity_trusted | 签名者具有完全信任或终极信任;单独报告,不是接受签名的必要条件 |
只有当每个内嵌签名都符合 policy_acceptable 时,解密才会释放明文。数学上有效但已过期或已被吊销的签名绝不够用,坏签名也不能被后续的好签名掩盖。结果包含一个 signatures 列表,每个 NEWSIG 序列对应一项。
它不保证什么
- 同一用户下的隔离。 任何以你的操作系统用户身份运行的进程都可以读取
~/.gnupg、连接gpg-agent套接字,或编辑~/.config/gpg-mcp/config.toml。如果 Agent 可以访问 shell,MCP 策略就不是操作系统边界。请使用更强的隔离。 - 恶意 MCP 客户端。 在
approval = "client"模式下,客户端负责呈现审批对话框。恶意客户端可以伪造该对话框。请使用approval = "local"(与客户端无关的 zenity),或单独的用户/硬件令牌。 - 遭到入侵的 gpg 或 gpg-agent。 遭到入侵的系统二进制文件可以为所欲为。二进制文件检查可以减少 PATH 劫持风险,但无法防范供应链入侵。
- 语义安全。 服务可以展示要签名的内容(哈希值和预览),但无法判断内容是否适合签名。这个判断应由人来做。
威胁表
| 威胁 | 控制措施 |
|---|---|
| Agent 请求获取私钥 | 没有任何工具会返回私钥;响应防护会阻止意外输出 |
Agent 尝试使用 --export-secret-keys | 不会构造此选项;不执行原始命令 |
| Agent 将选项作为 UID/指纹提供 | 验证完整指纹;UID 拒绝以 - 开头或包含控制字符的内容;创建密钥时使用参数文件 |
| Agent 悄悄签署恶意标签/发行版 | 允许列表 + 审批,展示密钥、大小、SHA-256 和预览 |
| Agent 解密被盗的密文 | 允许列表 + 审批 + 接收方预检 |
| Agent 将机密加密给攻击者的密钥 | 审批中列出命名接收方 + 可选的 allowed_recipients |
| Agent 导入私钥 | 检测并拒绝私钥数据包 |
| Agent 删除密钥 | destructiveHint + 手动输入指纹后缀 |
| Agent 撤销密钥 | destructiveHint + 手动输入指纹后缀 |
| Agent 提交其他密钥的撤销证书 | 在临时密钥环中验证证书,并将其绑定到请求的指纹 |
| Agent 请求执行撤销试运行以修改状态 | apply=false 只会在临时密钥环中验证 |
| Agent 利用只针对某个复合操作组成部分设置门控的自定义策略 | 复合操作会继承所有组成操作的审批要求 |
| Agent 从审计账本读取载荷 | 审批预览仅显示在界面中;审计只存储哈希/元数据 |
| Agent 在可见接收方中隐藏一个接收方 | 除非明确启用,否则会拒绝任何隐藏接收方 |
| Agent 替换为伪造的本地审批对话框 | 对 zenity 执行与 gpg 相同的可信二进制检查 |
| Agent 从有问题的签名中读取明文 | 有问题/已过期/已撤销的嵌入式签名不符合策略要求,并会中止操作 |
| Agent 用后续的有效签名掩盖有问题的签名 | 按 NEWSIG 汇总;只要有任何签名不符合要求,就会安全地失败 |
| Agent 未经审批获取密钥以修改密钥环 | fetch_key 是已注册的写入操作,须经审批,并会报告导入的指纹 |
| Agent 通过 UID 向审批对话框注入换行符或双向文本 | 单行转义,并在审批前进行验证 |
| 预先存在的审计文件可被所有用户读取 | 使用 fstat + fchmod 设置为 0600;拒绝非普通文件或非当前用户所有的文件 |
从 require_approval 中移除破坏性操作 | 默认情况下,destructive_confirmation 仍要求手动输入后缀 |
| 通过通用导入器提交撤销证书 | 在数据包层面拒绝撤销签名;请使用 import_revocation |
| 解密时悄悄使用不在允许列表中的私钥 | 检查 DECRYPTION_KEY 状态;不匹配时丢弃明文 |
| 巨大/不完整的状态文件被视为成功 | 强制执行有效状态上限;状态不完整时安全地失败 |
| 混合消息绕过密钥允许列表进行对称解密 | symmetric_decrypt 拒绝公钥接收方;decrypt 要求报告一个位于允许列表中的公钥 |
仅含对称加密的输入使用限制较少的 decrypt 审批 | decrypt 在执行前拒绝仅含对称加密的密文 |
| 较旧的 GnuPG 在遇到第一个无效签名时停止 | 逐一验证分离的数据包,并在汇总时安全失败 |
| 配置在验证与打开之间被替换 | 使用目录文件描述符遍历路径 + O_NOFOLLOW + 对打开的文件描述符执行 fstat |
| 路径祖先目录被其他用户替换 | 使用目录文件描述符遍历祖先目录;拒绝非粘滞且可写的祖先目录 |
init 跟随被植入的 gpg-agent.conf 符号链接 | 使用 lstat/O_NOFOLLOW,并检查文件类型和所有者 |
符号链接形式的密钥环祖先目录重定向 GNUPGHOME | 在服务启动/doctor/init 时验证未解析路径及其完整祖先链 |
| 多字节文本的分配量远超输入上限 | 在接受输入前以有界数据块进行 UTF-8 编码 |
| 不可逆操作后记录结果审计失败 | 记录意图和结果阶段;结果记录失败时会说明操作可能已完成 |
恶意 gpg.conf 改变执行行为 | 使用 --no-options 和显式的 --pinentry-mode ask |
| 组/所有者以外的用户可写配置改变策略 | 配置完整性检查会拒绝不安全文件和未知密钥 |
| 审计写入悄悄失败 | audit_required = true 时操作会失败 |
| 压缩炸弹/输出过大 | 限制流式读取;终止进程组 |
| PATH 劫持 | 使用绝对二进制路径,并检查所有权/权限模式 |
| 通过 argv/env/stdin 泄露密码短语 | 仅使用 Pinentry;不存在密码短语处理代码路径 |
| 无人值守提示 | 超时后终止进程组;无法绕过审批 |
架构
MCP client (agent)
│ stdio JSON-RPC
▼
gpg-mcp (Python, mcp SDK v2)
│ fixed argv, stdin/stdout, --status-file
▼
gpg 2.2+ (2.4 shown) ──► gpg-agent ──► pinentry (human passphrase)
│
▼
GNUPGHOME (encrypted private keys + public keyring)
服务端从不打开私钥文件,而是请求 gpg 执行操作;只有在 pinentry 成功后,gpg-agent 才会解锁密钥。
安装
PyPI:gpg-mcp。MCP Registry:io.github.emiliano-go/gpg-mcp。
在客户端配置中固定版本,避免重启后服务端构建版本发生变化。
# run without a global install
uvx [email protected] serve
# or install a pinned CLI
uv tool install gpg-mcp==0.1.0
# or
pipx install gpg-mcp==0.1.0
# or
pip install gpg-mcp==0.1.0
首次运行。如果你没有安装命令行工具,请在这些命令前加上 uvx [email protected]:
gpg-mcp init
gpg-mcp doctor
无需全局安装的 MCP 客户端配置:
{
"mcpServers": {
"gpg": {
"command": "uvx",
"args": ["[email protected]", "serve"]
}
}
}
如果你使用 uv tool 或 pipx 安装:
{
"mcpServers": {
"gpg": {
"command": "gpg-mcp",
"args": ["serve"]
}
}
}
要求:Python 3.11+、GnuPG 2.2+(推荐 2.4)、pinentry;使用 approval = "local" 时还需要 zenity。支持 Linux 和其他类 Unix 系统;不支持 Windows。
可在以下地址搜索 Registry 条目:
https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.emiliano-go/gpg-mcp。
快速开始(从源代码安装)
要求:Python 3.11+、GnuPG 2.2+(推荐 2.4)以及 pinentry 程序。
git clone https://github.com/emiliano-go/gpg-mcp.git
cd gpg-mcp
uv sync
uv run gpg-mcp init
init 会在 ~/.local/share/gpg-mcp/gnupg 中创建一个隔离的密钥环(模式为 0700),并在 ~/.config/gpg-mcp/config.toml 中创建配置文件(模式为 0600)。它会写入一份保守的 gpg-agent.conf:
default-cache-ttl 60
max-cache-ttl 300
ignore-cache-for-signing
no-allow-external-cache
no-allow-mark-trusted
pinentry-timeout 120
ignore-cache-for-signing 表示每次签名都会再次请求 pinentry。只有在了解其中的权衡后,才将其移除。
检查安装情况:
uv run gpg-mcp doctor
将其添加到 MCP 客户端(以下为 JSON mcpServers 映射示例):
{
"mcpServers": {
"gpg": {
"command": "gpg-mcp",
"args": ["serve", "--config", "/home/you/.config/gpg-mcp/config.toml"]
}
}
}
然后,在 Agent 中使用 create_key 创建密钥(在 pinentry 中输入密码短语),再使用返回的指纹调用 sign、encrypt 或 decrypt。
**客户端超时:**审批加上 pinentry 可能需要比客户端默认 MCP 工具超时时间更长的时间。请将超时设置为至少 120 秒;生成密钥和更改密码短语时,300 秒是较为合适的默认值。对于 opencode:
"experimental": { "mcp_timeout": 300000 }如果客户端在对话框打开时超时,请取消对话框。服务端不会自动批准,而是会拒绝该操作。
专用密钥环初始为空。
gpg-mcp不会复制或导出现有的私钥。若要复用现有密钥,请自行通过带外方式迁移,或继续使用~/.gnupg,并显式配置allowed_sign_keys/allowed_decrypt_keys允许列表。
配置
配置路径:~/.config/gpg-mcp/config.toml,也可使用 --config 或 GPG_MCP_CONFIG。
| 键 | 默认值 | 含义 |
|---|---|---|
gnupg_home | ~/.local/share/gpg-mcp/gnupg | 隔离的密钥环。权限必须为 0700。 |
approval | client | client(MCP 请求确认)、local(zenity;需要显示环境)、none(仅用于测试) |
require_approval | 所有高风险操作 | 必须经过批准的操作 |
destructive_confirmation | true | 对破坏性操作输入指纹后缀进行确认,独立于 require_approval |
allow_test_mode | false | 在服务运行时使用 approval = "none" 的必要条件 |
audit_required | true | 如果无法写入审计账本,则拒绝高风险操作 |
allowed_sign_keys | [] | 允许用于签名的完整指纹。为空时,专用密钥环中允许任意密钥;~/.gnupg 中不允许任何密钥 |
allowed_decrypt_keys | [] | 解密规则相同 |
allowed_recipients | [] | 加密时可选的收件人允许列表 |
enable_network | false | 搜索/获取/发送至密钥服务器 |
enable_symmetric | false | 对称加密/解密 |
allow_unprotected_keys | false | 允许创建 protect=false 的密钥(仅用于测试密钥环) |
allow_hidden_recipients | false | 允许解密隐藏收件人的消息 |
max_input_bytes | 10485760 | 输入上限 |
max_output_bytes | 20971520 | 输出上限;状态文件上限为 max(1 MiB, this value) |
timeout_s | 120 | 每项操作的超时时间 |
keygen_timeout_s | 300 | 密钥生成/口令变更的超时时间 |
keyserver | hkps://keys.openpgp.org | 仅可通过配置指定;只允许 hkps:// 或 https:// |
audit_log | ~/.local/share/gpg-mcp/audit.jsonl | 仅追加的 JSONL 文件,权限为 0600 |
环境变量覆盖项:GPG_MCP_CONFIG、GPG_MCP_HOME、GPG_MCP_APPROVAL、
GPG_MCP_ENABLE_NETWORK、GPG_MCP_ENABLE_SYMMETRIC、GPG_MCP_AUDIT、
GPG_MCP_ALLOW_TEST_MODE、GPG_MCP_AUDIT_REQUIRED。
工具
只读:
| 工具 | 用途 |
|---|---|
list_keys | 列出公钥或密钥元数据(绝不包含密钥材料) |
get_key | 获取单个指纹对应的完整公钥元数据 |
export_public_key | 导出 ASCII 装甲格式/二进制公钥 |
export_ssh_key | 导出用于身份验证子密钥的 OpenSSH 公钥 |
inspect | 检查 OpenPGP 数据包/密钥而不导入;拒绝秘密密钥 |
inspect_recipients | 显示密文的接收者而不解密 |
verify | 验证分离式、明文或内嵌签名 |
card_status | OpenPGP 智能卡状态 |
agent_info | gpg 版本、套接字和 Agent 缓存选项 |
doctor | 二进制文件、密钥环和策略健康检查 |
加密:
| 工具 | 用途 |
|---|---|
sign | 分离式、明文或内嵌签名 |
encrypt | 加密给完整接收者指纹 |
sign_and_encrypt | 签名并加密 |
decrypt | 通过允许列表中的密钥解密公钥或混合加密密文;仅对称加密的输入必须使用 symmetric_decrypt |
symmetric_encrypt / symmetric_decrypt | 使用口令进行对称加密(需选择启用) |
密钥管理:
| 工具 | 用途 |
|---|---|
create_key | 创建密钥;通过 pinentry 输入口令 |
add_subkey、add_uid、set_expiry | 修改密钥 |
change_passphrase | 通过 pinentry 输入旧/新口令 |
import_public_key | 导入公钥材料;拒绝秘密数据包和撤销签名 |
delete_key | 删除公钥或秘密密钥与公钥;需输入指定确认内容 |
set_ownertrust | never/marginal/full;ultimate 仅可通过 CLI 设置 |
generate_revocation | 验证并返回已存储的撤销证书 |
import_revocation | 在临时密钥环中验证;apply=false 可执行真正的试运行 |
revoke_key | 验证并应用已存储的证书;需输入指定确认内容 |
网络(需选择启用):
| 工具 | 用途 |
|---|---|
search_keyserver | 搜索已配置的密钥服务器 |
fetch_key | 获取并导入公钥;需审批,并报告已导入的指纹 |
send_key | 发布公钥 |
刻意不提供
export_secret_key/秘密密钥备份。run_gpg/原始 argv 透传。- 任何口令参数。
- 工具中的
--homedir。 - 每次调用时指定任意密钥服务器 URL。
--edit-key交互式菜单。- 来自用户输入的
--trust-model。
审批模式
client(默认)
服务会要求 MCP 客户端显示审批表单。表单包含操作、完整密钥指纹和身份信息、载荷大小、SHA-256 以及简短预览。该功能通过 MCP resolver 实现,因此同时适用于旧版和当前协议版本。
如果客户端未声明支持 elicitation(例如某些 CLI 客户端),服务会回退到本地 zenity 对话框。这仍然需要人工确认,且独立于 MCP 客户端。如果 elicitation 和安全的本地对话框都不可用,调用会以 approval_unavailable 错误安全失败;绝不会自动批准。
如果组合操作中的任一组成操作需要审批,则该组合操作也需要审批。sign_and_encrypt 继承 sign 和 encrypt 的要求;encrypt(..., sign_with=...) 也会继承 sign 的要求。因此,自定义设置 require_approval = ["sign"] 仍会对带签名加密进行审批,require_approval = ["encrypt"] 仍会对 sign_and_encrypt 进行审批。
当 destructive_confirmation = true(默认值)时,破坏性操作必须输入指定的指纹后缀,即使该操作已从 require_approval 中移除也一样。将其设为 false 只会取消输入指定内容的确认要求;如果该操作仍在 require_approval 中,仍需审批。对于一次性测试密钥环,approval = "none" 会绕过这两项要求。
对于撤销导入,审批表单会显示从证书本身推导出的身份信息,而不只是作为参数传入的指纹。如果证书对应的是其他密钥,则会在请求审批前拒绝导入。
local
服务器使用 zenity(如果没有可用的显示器,则默认拒绝)。此设置独立于 MCP 客户端;如果你不完全信任客户端,这是更好的选择。破坏性操作会使用第二个 zenity --entry 提示来输入指纹后缀。
none
仅用于一次性测试密钥环。除非已配置 allow_test_mode = true(或 GPG_MCP_ALLOW_TEST_MODE=1)并且配置了专用的 GNUPGHOME,否则 serve 会拒绝此模式。审计账本会记录 decision: skipped。切勿对真实密钥启用此模式。
密钥环隔离
gpg-mcp init 会使用专用密钥环。这样你就能:
- 将
gpg-agent缓存与日常 GnuPG 使用分开; - 设置策略边界,让空的允许列表意味着“仅限此密钥环”;
- 为测试密钥和 Agent 专用密钥提供安全位置。
专用密钥环无法防范同一 OS 用户。有关这一点,请参见下文。
更强的隔离
单独的 OS 用户(软件)
使用拥有密钥环的另一个用户运行签名器,并仅向 MCP 服务开放范围有限的 Unix 套接字。Agent 用户无法读取私钥,也无法连接到 Agent 套接字。这是最强的软件隔离设计,也是自然的下一步;当前版本为求简便,仍采用同一用户模式。
硬件令牌(适用于高价值密钥)
将私钥存放在 OpenPGP 智能卡或 YubiKey 上。密钥不会存在于磁盘上,每次签名都可以要求输入 PIN 并进行实体触碰。gpg-mcp 无需更改即可使用:card_status 会报告卡及其密钥,list_keys 会返回私钥元数据。在卡上生成密钥需通过 gpg --edit-card 在带外完成。
systemd 加固
如果将服务器作为服务运行,至少要:
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=%h/.local/share/gpg-mcp
RestrictAddressFamilies=AF_UNIX
并在单独用户设计中以专用用户身份运行。
审计账本
每次高风险操作都会向 audit.jsonl 追加一个 JSON 对象:
{"ts":"2026-10-24T20:00:00Z","event":"operation","phase":"outcome","tool":"sign",
"fingerprint":"F759...E793","mode":"detached",
"payload_sha256":"...","size":42,"result":"ok"}
账本不包含明文、负载预览、口令或任何私钥材料。审批摘要会呈现给用户,但绝不会写入账本。文件采用仅所有者可读写的 0600 权限,以追加方式写入。每次写入时,都会对现有审计文件执行 fstat:拒绝非普通文件和由其他用户拥有的文件,并使用 fchmod 将已有的组/其他用户可访问文件收紧至 0600。gpg-mcp 仅会收紧其自行创建的审计目录权限;绝不会对 /tmp 这类现有的任意父目录执行 chmod。
启用 audit_required = true(默认值)时,如果无法写入审计条目,高风险操作就会失败;仅对一次性测试密钥环设置 audit_required = false。记录包含 phase:执行前写入 approval/intent,执行后写入 outcome。如果追加结果记录失败,错误会明确说明操作可能已经完成;不会重试操作。账本无法防范同一 OS 用户篡改。
测试
uv run pytest # never prompts; uses an ephemeral keyring
uv run ruff check .
# Optional: the two operations that intentionally need a human at pinentry
GPG_MCP_INTERACTIVE=1 uv run pytest tests/test_interactive.py -s
默认测试套件会在临时密钥环中使用 pinentry-program /bin/false,因此任何意外提示都会导致测试失败,而不是阻塞测试。它会创建一个临时密钥环并运行真实的 gpg 操作。测试会验证以下内容:
- 没有响应包含
PRIVATE KEY BLOCK; - 没有命令行包含被禁止的选项或口令;
- 拒绝导入私钥和导入私钥撤销证书;
- 默认策略要求对
import_revocation、revoke_key和delete_key进行审批并输入确认文本; - 复合操作会继承其组成操作的审批要求;
- 撤销预演不会修改真实密钥环,且会拒绝针对其他密钥的证书;
- 过期签名、过期密钥、已撤销密钥和缺少公钥的情况,策略一律不予接受;
- 多个嵌入式签名无论顺序是好是坏,都会失败关闭;
fetch_key会注册为写操作并要求审批;- 审批对话框会转义多行/双向文本 UID,并在审批前进行验证;
- 已有的
0644审计文件会收紧为0600,同时拒绝符号链接和 FIFO; - 即使从
require_approval中移除该操作,破坏性确认仍是必需的; - 无法通过
import_public_key使用撤销证书撤销密钥; - 实际的
DECRYPTION_KEY必须列入允许列表; - 超出配置上限的状态输出会失败关闭;
- 忽略恶意的
gpg.conf; decrypt会拒绝仅使用对称加密的输入,而混合输入可通过允许列表中的公钥成功解密;- 只对
symmetric_decrypt设置门控的自定义策略无法通过decrypt绕过; - 会安全地遍历配置目录的所有祖先目录,并拒绝非粘滞且可写的祖先目录;
init会拒绝符号链接形式的gpg-agent.conf,且悬空配置符号链接无法创建其目标;- 审计路径会拒绝非粘滞且可写的祖先目录;
- 会正确枚举并汇总多签名明文、内联和嵌入式消息;
- 混合解密只容忍可恢复的对称口令失败模式;
- 对称解密会拒绝无法解释的非零退出码;仅当
verify=false时,才会忽略已识别的签名失败; - 创建配置、密钥环和审计目录时,无法通过符号链接祖先目录写入;
- 在 serve/doctor/init 阶段会拒绝符号链接密钥环路径和可写的密钥环祖先目录;
- 无效的安全遍历路径不会泄露目录文件描述符;
- 无论是否使用
--proc-all-sigs,都会枚举多个签名并在失败时关闭; symmetric_decrypt会拒绝混合对称/公钥密文;- 会处理审计短写、意图/结果阶段以及结果失败;
- 会拒绝悬空配置符号链接和不安全的配置父目录;
- 超大分配前,多字节 UTF-8 内容会先触及字节数上限;
- 会拒绝不安全的配置文件、未知配置键和隐式的
approval="none"; audit_required=true时,不安全的审计文件会触发错误;- 会验证密钥生成参数组合并对部分导入进行分类;
- 工具注册表与每个已公开工具的读/写注解相符;
- 解密会拒绝发给其他密钥的密文以及混合隐藏/可见收件人的密文;
- 审计账本不包含载荷预览,也不会更改现有目录的权限模式;
- 会拒绝不安全的
zenity/二进制文件权限; - 选项注入尝试会在验证阶段失败。
CI 会在安装 GnuPG 的 Python 3.11、3.12 和 3.13 环境中运行 ruff 和 pytest(.github/workflows/ci.yml)。
main 分支受保护,必须通过全部三个 CI 任务;直接推送会被拒绝。
限制
- 文本和 base64 输入有大小上限;大型文件应通过带外方式处理。
- 密钥服务器的搜索输出是供人阅读的文本,而非结构化列表。
- 撤销操作使用创建密钥时生成的证书。如果证书缺失,请通过带外方式使用
gpg --gen-revoke生成。目标密钥必须已存在于配置的密钥环中;未知密钥的撤销证书会被拒绝。import_revocation处理的是完整主密钥撤销,不处理选择性子密钥或 UID 撤销。 - 智能卡密钥生成和 PIN 管理不在本项目范围内。
approval = "client"依赖客户端如实呈现征询表单。如果客户端无法发起征询,则会回退到经过验证的本地zenity对话框。- 仅支持 Linux/Unix:此实现依赖 Unix 权限、
os.getuid、POSIX 进程组和zenity;不支持 Windows。 generate_revocation会向调用方返回可用于撤销的证书。请将结果视为敏感信息并离线保存。审批提示会说明相关后果。- 在不支持
--proc-all-sigs的 GnuPG 版本中,明文、内联和嵌入式签名枚举可能会在遇到第一个无效签名时停止;但仍会拒绝该结果。分离签名使用逐数据包回退处理。
许可证
MIT。请参阅 LICENSE。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。