Skip to content

[WIP][3/3] feat(memory): 审计链式 HMAC 完整性保护 - #61

Open
openjiuwen-sync-bot[bot] wants to merge 31 commits into
openJiuwen-ai:mem2.0from
openjiuwenai:sync/pr-167
Open

[WIP][3/3] feat(memory): 审计链式 HMAC 完整性保护#61
openjiuwen-sync-bot[bot] wants to merge 31 commits into
openJiuwen-ai:mem2.0from
openjiuwenai:sync/pr-167

Conversation

@openjiuwen-sync-bot

@openjiuwen-sync-bot openjiuwen-sync-bot Bot commented Jul 31, 2026

Copy link
Copy Markdown

Paired: GitHub #61GitCode !167

/kind feature

变更概述

这是安全模块三阶段交付的第 3 个 PR,建立在认证和角色感知授权能力之上。

主要增加:

  • 为审计事件增加链式 HMAC 完整性保护。
  • 审计记录安全元数据:acting_userrolekey_fpauth_mode
  • 增加前序摘要、链头和序号一致性校验。
  • 使用 CAS capability 防止并发写入覆盖审计链。
  • SQLite 当前 schema 异常时 fail closed。
  • 仅对合法旧 user_version 执行迁移和 last_seq 回填。
  • 增加篡改、删除、重排、重放、并发竞争和 schema 降级测试。
  • 同步审计接口、公共安全接口和完整性设计文档。

安全属性

  • TRUSTED、DEV、API_KEY 均记录正确 auth_mode
  • 审计元数据仅作为安全事实记录,不参与权限提升。
  • 不支持链式 CAS 的持久化 AuditLogger 不能启用 HMAC wrapper。
  • 当前版本数据库缺少严格结构时拒绝启动,不进行宽松修复。
  • 审计篡改、断链和序号异常能够被检测。

验证

  • PR 独有历史为 11 个自身提交,不含旧 merge 或不合规 style 提交。
  • 重建前后代码 tree hash 完全一致。
  • PR 变更范围 ruff check 通过。
  • PR 变更范围 ruff format --check 通过。
  • git diff --check 及冲突标记扫描通过。
  • PR③ 变更测试:91 passed。
  • 安全相关测试:292 passed。
  • 全量测试:1002 passed、60 skipped、3 个已知环境/上游基线失败,无新增失败。

Self-checklist

  • 设计:审计完整性方案已归档,并完成多轮安全红队审查。
  • 测试:新增 UT/集成测试已随 PR 提交。
  • 验证:预期目标与实际验证结果已在本描述中列明。
  • 接口:相关公共接口、注释、spec 与 AGENTS.md 已同步。
  • 文档:相关 feature/spec 文档已随 PR 提交。

合并信息

Merge-Order: 3/3
Depends-On: #​166(#59
Do-Not-Merge-Before: #​166
Merge-Method: merge commit(禁止 Squash)

本 PR 保持 Draft/WIP。PR② 合入后需刷新 diff、重跑最终集成门禁,再取消 Draft。

HankDUMPLINGZhong and others added 21 commits July 30, 2026 15:29
身份此前由请求体的 actor_* 字段提供(handler._actor_scope 的 docstring 自称
"Claimed"),调用方填什么就是什么,13 个动词全部可冒充任意主体。授权层本身是
对的——诚实身份读别人的 scope 正确返回 403——洞在于认证层根本不存在。

新增 src/security/:
- Authenticator 三实现(DEV / TRUSTED / API_KEY),模式在装配期选定而非每请求
  分流;错误消息一律笼统,避免主体枚举侧信道
- PrincipalKeyStore(Argon2id,OWASP 2024+ 参数),三条 resolve 路径各恰好一次
  verify 以消除 timing 侧信道;缺 argon2-cffi 时装配期抛错,不回退明文
- RateLimiter 令牌桶,挂在认证之前——Argon2 的 128MiB×4 单次成本在无限制调用下
  是放大器;桶按 peer 分并 LRU 有界,否则这个防耗尽的组件自己成为耗尽入口
- check_dev_binding 纯函数,DEV 模式绑非 loopback 时由进程入口 exit(1)

common/type_def/auth.py 提供 AuthContext(frozen,actor 与 role 均无默认值——
漏传会得到空 Scope() 即 platform-admin 全局权限)与 ContextVar 传播。

handler 改从认证上下文取身份,payload 里的 actor_* 一律 400 而非静默忽略:
静默忽略会让运维以为"我加了 actor_scope 限制"仍然生效。audit verb 的
actor_agent/actor_session 是查询谓词,语义不同,对该 verb 放行。

storage/fs_impl/encrypted_fs_store.py 补齐静态加密的 FS 侧(KV 侧上游已有):
与 EncryptedKVStore 逐条同构,不自带任何密码学,AAD 绑满五维 scope + ref。
明文兼容开关只在 provider 的 allow_plaintext 上,装饰器不重复提供。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
test_identity_forgery_rejected.py 按 CLAUDE.md §5 先写后修:在改动前的 HEAD 上
跑一遍看它全红(证明漏洞真实存在),再做改动看它全绿。其中
test_identity_comes_from_context_not_payload 比"伪造被拒"更重要——前者证明堵上
之后认证与授权确实串起来了,后者只证明洞堵上了。

关键断言:
- 限流跑在认证之前(认证器一次都没被调到),429 与 401 可分
- 审计 detail 恰好只有 mode 与 peer 两个键,用 set(detail) == {...} 精确断言:
  桶余量能用来反推限流参数,然后贴着阈值发请求
- resolve 的 timing pad 取 5 次中位数,比值须在 [0.5, 2.0]。这条实测抓到过真实
  缺陷:初版"前缀有候选但 key 错"跑两次 verify、"前缀无候选"跑一次,ratio=0.49;
  修的是实现(加 verified_any 标志)不是断言
- AuthContext 的 ContextVar 线程隔离与 reset 保证
- FS 加密:AAD 绑满五维 scope + ref;换 scope 搬密文解不开;明文兼容开着时写
  路径仍然加密

两个既有测试随实现改写:身份改由上下文提供后,每条用例都必须显式声明"谁在发这个
请求"——这正是要的效果,签名上就不给"不指定身份也能调"留位置。原先断言
actor_space 能覆盖 identity 的那条用例被删除,它断言的正是要堵的洞。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
新增 docs/features/security/F01-authentication-kernel.md:认证内核的 11 条决策、
8 条拒绝方案与 13 条已知遗留。速率限制写在同一份而不单开——它唯一的存在目的是
保护认证,且这个可用性风险是引入 Argon2 时一并带进来的,不是一件独立的事。把
"留了个洞"和"补上了"记在一起,比拆成两份让读者去两处对照要诚实。

F04(安全基线)就地加 9 条实现注记,标出主干与文档的偏离。这些不是文档笔误,
是照抄会出问题的地方:ROOT actor 用 Scope(org="*") 会先撞上"跨 org 一律拒绝"
反而寸步难行;compare_digest 传 str 在非 ASCII 输入下抛 TypeError,把 401 变成
500;异步 KeyProvider 在已有事件循环的进程里必炸;信封头实际 11 字节且没有
key_id——后者意味着轮换根密钥会让全部历史密文永久不可解,是个真实缺口。

storage/F02 从"加密 KV"扩为"加密存储",补入 FS 侧的 8 条决策与 13 条断言。两个
装饰器同构,合在一份里读者才看得出"明文兼容开关只在 provider 上"是一条贯穿两侧
的约束,而不是各自的实现选择。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
解决四个预期冲突:
- test_dispatch_management_compat.py: 合并身份上下文与新版list API
- F02-encrypted-storage.md: 合并KV和FS加密特性文档
- S07-common.md: 合并公共层接口与安全特性
- src/storage/AGENTS.md: 合并KV/FS加密装饰器规约

自动合并的关键文件已复核:
- handler.py: list返回MemoryListResult.count,身份从上下文来
- LocalMemoryAPI.list: 授权目标与filters一致,审计count/page_count
- KVSpaceManager: 原始枚举改用scan()

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
应用代码格式化到合并后的文件。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
修复审查报告指出的格式化问题。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR① 造出的 AuthContext 的 role 与 acting_user 两个字段此前没有任何消费方--
认证层算出来、进审计 detail、然后在授权边界被丢掉。本提交把它们接进 PDP。

PermissionManager.check 增加 keyword-only auth 参数(默认 None,向后兼容),
SQLitePermissionManager 据此加四道判定:auth.actor != actor 即拒(fail-closed);
管理面资源(resource_type 为 admin/audit,或 space 的写/删)要求 ROOT;ROOT 按
role 判定而非 actor 形状(空 actor 在有认证上下文时显式拒绝,堵住
_owner_scope_covers 顶部的通配分支);acting_user 委托覆盖同 org+space 的目标
user 分支(§4.3)。AllowAll 忽略 auth 恒 True,Routing 原样透传给 delegate。

LocalMemoryAPI._authorize 从 ContextVar 取 AuthContext 透传给 PDP(PDP 不自读
ContextVar,是其入参的纯函数);admin_*/audit 三处补 resource_type,使「这是
系统级操作」显式可读而非从 target 形状反推。TrustedAuthenticator 读 X-Acting-User
产出 acting_user:user 主体是它自己,agent 主体读该 header。

Co-Authored-By: Claude <noreply@anthropic.com>
test_permission_role_aware.py 覆盖 PDP 自身的 21 条判定:提升式与声明式 ROOT
等价、空 actor 非 ROOT 被拒、auth.actor != actor 被拒、agent+acting_user 委托
及其四条边界(不跨 org/space、不指向别的 user/agent)、管理面 resource_type
闸、grant 不被误伤、AllowAll/Routing 透传、auth=None 时旧规则逐字不变。

test_authorization_with_auth_context.py 覆盖 PEP 接线 7 条:认证上下文确实穿过
API 抵达 PDP,含提升式 ROOT 用管理面、agent 代 user 读写、identity 与
auth.actor 不一致被拒、无认证上下文时旧 platform-admin 形态保留。

test_authenticator_impl.py 补 acting_user 生产方 3 条;test_build_kernel_config
的 _DenyAllPermission 桩对齐新签名。

Co-Authored-By: Claude <noreply@anthropic.com>
新增 docs/features/security/F02-role-aware-authorization.md:三个缺口、六条决策、
两条诚实的范围排除(ADMIN 权限与 require_role 装饰器)。记下实现中对计划的三处
修正--grant 不进管理面(跨 org 已挡、对自有 scope 发 grant 是主用途)、空 actor
须显式拒绝(否则命中 _owner_scope_covers 通配分支)、set_current 已在 bootstrap
接好(非缺口)。

S03-control.md 的 check 签名与规则表同步:auth 参数、前置三道闸、ROOT 按 role、
委托覆盖。src/api/AGENTS.md 的 PEP 流程补「取 ContextVar 透传」与「管理面带
resource_type」两步。

Co-Authored-By: Claude <noreply@anthropic.com>
新增 src/security/audit_hmac.py 的 HmacAuditLogger 装饰器:包任意 AuditLogger,
record 时算链式 HMAC(每条含前一条 HMAC)塞进 event.detail,verify_integrity 全量
校验返回篡改行。不改 common/audit 的 ABC 与两个实现,与 encrypted 装饰器同构。
HMAC key 从 LocalKeyProvider 的 root key 经 HKDF 派生(context=audit),同源隔离。
@AuditProducer.register("hmac") 配置驱动 opt-in,register_security 触发注册。

AuthContext 加 auth_mode 字段,三个 authenticator 与 key_store.resolve 填上。
LocalMemoryAPI._record_audit 从 get_current() 取 acting_user/role/key_fp/auth_mode
塞进 detail(§7.2)。无认证上下文时不填--认证产物不存在比填空诚实。

Co-Authored-By: Claude <noreply@anthropic.com>
test_audit_hmac.py 11 条:链式链接顺序敏感、改行不重算检出、重算后后续链断、
伪造 prev_hmac 检出、干净链空、key 派生确定且隔离、错 key 全链判篡改、query 透明、
包 SqliteAuditLogger、factory 装配、factory 要求 inner。

test_audit_security_fields.py 3 条:有 AuthContext 填四字段、无 AuthContext 不填、
ROOT+DEV 模式。

Co-Authored-By: Claude <noreply@anthropic.com>
新增 docs/features/security/F03-audit-integrity.md:五条决策(装饰器不改
common/audit、只填 detail 不提升、Root Key 派生、AuthContext 加 auth_mode、
配置驱动 opt-in)、链式 HMAC 规则、key 派生与轮换、并发与重启、五条遗留。

src/security/AGENTS.md 模块地图加 audit_hmac.py;src/common/AGENTS.md 的
AuthContext 行补 auth_mode 字段。

Co-Authored-By: Claude <noreply@anthropic.com>
采用严格 user_version 规则区分旧库迁移和当前库损坏:
- user_version < 2:真正旧库,允许添加列后迁移
- user_version >= 2:当前版本,任何 schema 降级都拒绝

核心修复(审计 P1-1/P1-2):
1. _init_schema 只在 user_version < 2 时添加缺失列
2. get_chain_state 用 CTE 单 SQL 原子快照(head 缺失也适用)
3. get_chain_state 动态 SQL 处理列不存在场景,避免运行时错误
4. AuditIntegrityResult 保持 checked 在第三位(位置参数兼容)

防御效果:
- 当前库 DROP head 表 → 保持 version=2 → 拒绝
- 当前库重建为不完整表 → 保持 version=2 → 拒绝
- 攻击者无法通过表操作降级 user_version(数据库级元数据)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
新增测试(审计 P1-2):
- test_current_schema_recreated_as_legacy_shape_is_rejected
  验证 user_version=2 的当前库重建为旧结构会被拒绝

修正测试场景:
- test_previous_schema_last_seq_backfilled_on_migrate
  添加 PRAGMA user_version=0,真实模拟旧库(非当前库 DROP 表)
- test_legacy_signed_db_migrates_chain_head
  添加 PRAGMA user_version=0,区分旧库迁移和当前库损坏

测试覆盖:
- 旧库迁移(version 0 → 添加列 → version 2)成功
- 当前库 DROP 表后完整重建(version 2)拒绝
- 当前库 DROP 表后不完整重建(version 2)拒绝
- 空事件 + 悬空 head 拒绝
- last_seq 篡改拒绝
- head_hmac 篡改拒绝

35 passed

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- F03: 更新测试数量 34→35,补全 get_chain_state 原子快照说明与 AuditIntegrityResult 结构
- S07: AuditLogger 接口表新增 get_chain_state/init_chain_head/get_last_event 三方法
- AGENTS: 新增「安全不变量」章节,记录链头一致性、严格版本控制、并发保护、重启恢复、启动约束五大不变量

关联: 130f96f (feat), 258c84f (test)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 新增 AuditLogger.supports_chain_cas() capability 方法声明后端 CAS 能力
- HmacAuditLogger 构造时检查后端能力,不支持 CAS 则 fail closed(审计 P1-1)
- SqliteAuditLogger 声明支持事务级 CAS
- InMemoryAuditLogger 实现线程级 CAS 检查(_lock 保护 expected_head 比对)
- SqliteAuditLogger.get_chain_state() 当前版本缺核心列统一抛 ValidationError(审计 P2-3)
- 修复 InMemoryAuditLogger 并发安全(所有方法加锁保护)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- test_hmac_rejects_non_cas_backend: 非 CAS 后端构造时拒绝(审计 P1-1)
- test_in_memory_backend_supports_cas: InMemory 后端声明 CAS 支持
- test_current_version_missing_core_columns_rejected: 当前版本缺核心列统一 ValidationError(审计 P2-3)
- test_tampered_count_and_samples_truncated: 采样上限生效时记录真实总数与截断标志(审计 P2-1)
- test_cte_concurrent_write_during_chain_state_read: CTE 单 SQL 快照内部一致性(审计 P2-1)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- S07 更新修订日期为 2026-07-31(审计 P1-DOC)
- S07 移除 init_chain_head/get_last_event 公共接口声明,标注为 SQLite 私有扩展(审计 P1-DOC)
- S07 新增 supports_chain_cas() 方法说明(审计 P1-1)
- F03 修正元信息:移除 commit hash,恢复日期为 2026-07-30(审计 P1-DOC)
- F03 修正 user_version 描述:明确为 schema 迁移判别标记而非抗篡改安全锚点(审计 P2-2)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
**契约修正**(审计 P③-9):
- S07: 新增 supports_chain_cas() 到 AuditLogger 公开接口表
- S07: 明确 get_chain_state() 为公开接口(非 SQLite 私有)
- S07: 说明 record_chained 默认实现不提供 CAS capability
- S07: 移动 init_chain_head/get_last_event 到「SQLite 后端私有扩展」
- audit_hmac.py 模块/类文档:移除「包住任意」,改为「包装声明支持链式 CAS 的」
- F03: 同步 CAS capability 要求,明确构造时检查
- AGENTS.md: 新增「CAS capability 约束」章节,扩展 user_version 已知限制
- AGENTS.md: 澄清 InMemory 线程级 vs SQLite 事务级 CAS 保证差异

**测试调整**:
- 重命名 test_cte_concurrent_write_during_chain_state_read
  → test_get_chain_state_snapshot_consistency(精准反映测试意图)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
**文档契约同步**(审计 P③-10):
- base.py record_chained: 移除「适用于单实例内存后端」,改为「默认实现仅提供
  接口兼容,不检查 expected_head,不具备 CAS capability,不能被 HmacAuditLogger
  安全包装」(DOC-1)
- F03 方案章节: 拆分 AuditLogger 公共方法与 SQLite 私有迁移 hook,明确
  init_chain_head/get_last_event 不属于基类接口(DOC-2)
- F03 schema 版本控制: 标题改为「schema 迁移判别标记」,与后文已知限制一致(DOC-3)
- F03 范围降级: 统一改为「已批准后移」,明确负责人决策,移除「待确认」(DOC-4)
- audit_hmac.py verify_integrity: 同步为「已批准后移」(DOC-4)
- test_audit_hmac.py 文件头: 改为「包装 CAS-capable AuditLogger」(DOC-5)
- Common AGENTS: InMemory CAS 边界澄清为「同一后端对象可由多个 wrapper/线程
  安全共享;不同对象相互独立」(DOC-5)

**验收门槛**:
- ruff check: All checks passed
- ruff format --check: 3 files already formatted
- git diff --check: 通过
- pytest security: 40 passed

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@openjiuwen-collaboration-bot

Copy link
Copy Markdown

head_sha: 73c1f1d465be9483789f2d2f978427a357eb672b

任务名称 结果 日志操作
静态检查 ❌FAILED 点此跳转
防投毒检查 ✅SUCCESS 点此跳转
开源合规检查 ✅SUCCESS 点此跳转
UT测试 ✅SUCCESS 点此跳转
ST测试 N/A N/A
build 编译包 N/A N/A
ruff codecheck ✅SUCCESS N/A

HankDUMPLINGZhong and others added 3 commits July 31, 2026 16:07
- G.LOG.02: __main__.py 和 mcp_server 使用 logging.error 替代 print
- G.FMT.08: Protocol 方法定义使用多行格式
- G.FMT.07: test_http_slow_upload.py import 顺序调整(importlib 放到标准库区)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- G.CTL.03: audit_hmac.py _recover_chain_head 方法中 if 语句拆分,将4个布尔表达式提取为 is_empty_db 变量

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@openjiuwen-collaboration-bot

Copy link
Copy Markdown

head_sha: f2daf23090fef89719c032d066e26934006e9516

任务名称 结果 日志操作
静态检查 ❌FAILED 点此跳转
防投毒检查 ✅SUCCESS 点此跳转
开源合规检查 ✅SUCCESS 点此跳转
UT测试 ✅SUCCESS 点此跳转
ST测试 N/A N/A
build 编译包 N/A N/A
ruff codecheck ✅SUCCESS N/A

@openjiuwen-collaboration-bot

Copy link
Copy Markdown

head_sha: 24ec0d2307ed657b06564e693e184dbc3e788f19

任务名称 结果 日志操作
静态检查 ❌FAILED 点此跳转
防投毒检查 ✅SUCCESS 点此跳转
开源合规检查 ✅SUCCESS 点此跳转
UT测试 ✅SUCCESS 点此跳转
ST测试 N/A N/A
build 编译包 N/A N/A
ruff codecheck ✅SUCCESS N/A

@openjiuwen-collaboration-bot

Copy link
Copy Markdown

head_sha: cca74c286ff80f5ac16aa2f76735a7a33f0c2bb0

任务名称 结果 日志操作
静态检查 ✅SUCCESS 点此跳转
防投毒检查 ✅SUCCESS 点此跳转
开源合规检查 ✅SUCCESS 点此跳转
UT测试 ✅SUCCESS 点此跳转
ST测试 N/A N/A
build 编译包 N/A N/A
ruff codecheck ✅SUCCESS N/A

@openjiuwen-collaboration-bot

Copy link
Copy Markdown

head_sha: cca74c286ff80f5ac16aa2f76735a7a33f0c2bb0

一次pr最好一个commit id

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants