IronClaw Web Push 域实现解析基于 RFC 8030/8291/8292 的浏览器通知通道【免费下载链接】ironclawIronClaw is an Agent OS focused on privacy, security and extensibility项目地址: https://gitcode.com/gh_mirrors/iro/ironclawIronClaw 的ironclaw_web_app是一个纯领域 crate承载 Web App 浏览器通知通道的记录语法与纯机制订阅记录的校验与持久化、RFC 8291aes128gcm载荷加密、RFC 8292 VAPID 密钥材料生成以及无传输层的推送请求规划。读完本文你将掌握该通道从浏览器PushManager.subscribe()产出订阅、到加密载荷、再到受控 egress 发送的完整数据流理解它为何刻意不发送请求、不保管密钥、不做路由并能直接使用本文给出的验证命令与配置说明。Web Push 通道在 IronClaw 中的定位与职责边界ironclaw_web_app的定位在 crates/domains/ironclaw_web_app/README.md 中写得很明确它是Web PushRFC 8030/8291/8292领域 crate负责浏览器通知通道的记录语法和纯机制——不包含任何传输层实现。从 crates/domains/ironclaw_web_app/src/lib.rs 顶部注释可以看到它声明的三条边界无传输层本 crate 只规划请求WebAppRequestPlan从不真正发送发送由通道包在受控 egress 上驱动。不保管秘密VAPID 材料在这里生成由组合层经通道凭据路径存储只有 host 的 egress 注入器能读回。存储走 scoped filesystem 平面通过共享的有界 CAS 路径读写后端由组合层选择。模块划分为crypto、error、grammar、message、store、subscription、vapid七个模块各自承担一条清晰的责任线。Cargo.tomlcrates/domains/ironclaw_web_app/Cargo.toml中的依赖也印证了这一点aws-lc-rsES256 密钥生成 ECDH P-256 / HKDF-SHA256 / AES-128-GCM、base64、ironclaw_filesystemscoped 存储、ironclaw_host_api凭据与 HTTP 契约、serde、sha2、url、uuid——全部是纯计算与契约依赖没有任何 HTTP 客户端。与其他 crate 的分工README 给出了清晰的何时该用别的 crate清单这本质上是通道的调用边界关注点归属说明发送已规划的请求crates/extensions/packages/web-app通道包经 restricted egress 执行 POSTAuthorization: vapid头ironclaw_host_runtime在 egress 凭据边界 host 侧计算本 crate 永不计算通知路由/策略ironclaw_outbound通知的发散与策略判定订阅/退订产品表面ironclaw_assistant WebUI面向用户的操作入口订阅记录语法PushSubscriptionRecord 家族浏览器侧PushManager.subscribe()返回三样东西一个指向推送服务的 endpoint 能力 URL以及客户端 P-256 公钥p256dh和 16 字节 auth 密钥auth。ironclaw_web_app用三个类型把它们封装成严格的、经过校验的语法见 crates/domains/ironclaw_web_app/src/subscription.rs。PushEndpoint形状校验 部署白名单PushEndpoint在构造时PushEndpoint::validatesubscription.rs 第 41-76 行强制校验长度不超过MAX_ENDPOINT_BYTES 1024真实推送服务 endpoint 约 150-300 字节1 KiB 留足余量且不接受无界输入必须是合法 URL 且 scheme 为https不得携带 userinfo用户名/密码、不得带 fragment、不得显式指定端口只允许默认 https 端口、必须有 host。validate_against_push_servicessubscription.rs 第 84-96 行是入网时刻的闸门endpoint 的推送服务 host 必须命中部署声明的白名单。白名单的来源正是 Web App manifest 的[[channel.egress]]条目——与发送时刻 restricted egress 强制执行的同一份名单实现单一事实来源。测试endpoint_allowlist_gate_admits_declared_hosts_only验证了关键语义空名单会 fail-closedvalidate_against_push_services([])必然拒绝host 比较大小写不敏感。值得注意的是形状校验在构造时完成使已存记录在反序列化时无需白名单即可恢复白名单检查只在接受新入网时运行。这与PushEndpoint的#[serde(try_from String)]设计一致——任何来源的字符串反序列化都会先过形状校验。digest()subscription.rs 第 121-129 行给出 endpoint 的 SHA-256 小写十六进制摘要。endpoint 是 bearer 能力谁拿到谁就能尝试投递绝不能回显到设置界面但浏览器可以对本地订阅 endpoint 计算同样的摘要并与已入网集合比对从而在共享浏览器配置下区分为本账号入网与为其他账号入网而后端永不暴露 URL。PushSubscriptionKeys密钥形状校验PushSubscriptionKeyssubscription.rs 第 154-195 行对应PushSubscription.getKey()的输出base64url无填充编码p256dh解码后必须是65 字节非压缩 P-256 点0x04 || x || yauth解码后必须是16 字节。测试keys_validate_decoded_shapes覆盖了非法 base64、错误点长度、错误 auth 长度的拒绝路径。decode_b64url_field会先剔除尾部容忍带填充的输入。PushSubscriptionRecord一条入网记录PushSubscriptionRecordsubscription.rs 第 214-242 行包含subscription_iduuid v4、endpoint、keys、可选的user_agent供设置界面展示构造时经sanitize_user_agent去除控制字符并截断到 256 字节见测试user_agent_is_bounded_and_control_stripped、以及 RFC 3339 格式的created_at。每个用户可入网的浏览器数量被MAX_SUBSCRIPTIONS_PER_USER 16封顶subscription.rs 第 22 行注释真实用户通常持有少量浏览器配置该上限约束了扇出与存储。订阅存储WebAppSubscriptionStore 与 scoped filesystem CAS存储层crates/domains/ironclaw_web_app/src/store.rs把订阅持久化抽象为一个异步 traitWebAppSubscriptionStore包含三个操作upsert_subscription按 endpoint 插入或刷新返回Enrolled/Refreshedremove_subscription按 endpoint 移除返回是否存在list_subscriptions列出该 scope 用户当前全部入网最新在前。每 (tenant, user) 一个 JSON 文档FilesystemWebAppSubscriptionStore通过ScopedFilesystem实现scoped 挂载视图已前缀/tenants/tenant/users/user因此别名相对路径是常量SUBSCRIPTIONS_DOCUMENT /web-push/subscriptions.jsonstore.rs 第 25 行。文档带schema_version 1并记录tenant_id/user_id两个字段。SubscriptionDocument::validate_owner提供了超出路径作用域的纵深防御文档内记录的所有者与请求 scope 不一致即视为损坏或错路由直接拒绝绝不把另一用户的入网集合交出去。CAS 语义与并发正确性所有变更都走ironclaw_filesystem::cas_update共享有界 CAS 路径读取 → 应用 → 写回冲突时重读重放。upsert的具体语义同 endpoint 且 keys/user_agent 未变 →no_opRefreshed同 endpoint 但 keys 轮换 → 更新记录 Refreshed新 endpoint 且已达上限 →WebAppError::SubscriptionLimitReachedfail-closed新 endpoint → 插入到列表头部最新在前Enrolled。并发正确性由测试concurrent_enrollments_from_two_browsers_both_persist显式证明两个浏览器同时对同一空文档入网CAS 冲突时输家重读胜者的文档再应用两条记录都必须落地而不是互相覆盖。测试the_per_user_cap_fails_closed验证第 17 条入网必然失败scopes_are_isolated_per_user验证跨用户隔离。通道身份语法web-app 扩展、目标 id 与 binding-ref身份语法集中在 crates/domains/ironclaw_web_app/src/grammar.rs编码与解码两半都放在这里使通道包的 codec、target provider 与 adapter 三者不会漂移。常量值含义WEB_APP_EXTENSION_IDweb-app扩展身份命名产品表面Web App而非推送协议WEB_APP_CHANNEL_NAMEweb-app选择器中展示的通道标签WEB_APP_TARGET_IDweb-push常量、owner 作用域的 catalog target idWEB_APP_VAPID_CREDENTIAL_HANDLEweb_push_vapid部署级 VAPID 密钥材料的凭据句柄WEB_APP_TARGET_ID和凭据句柄刻意保留重命名前的拼写target id 作为持久化的每用户身份存入通信偏好重命名会让每个已存选择解析为Missing并在通知扇出中静默消失凭据句柄是持久化的 secret-store 键重命名会孤立所有部署已播种的密钥对而轮换 VAPID 身份会在密码学层面破坏全部已有浏览器订阅。Binding-ref 格式为web-app/v1/tenant/userREF_PREFIX web-app/v1/配套encode_web_app_target_ref/decode_web_app_target_ref/is_web_app_target_ref。解码端还接受旧前缀web-push/v1/LEGACY_REF_PREFIX——2026-08-10 重命名前持久化的旧 ref 永远可解码但只解码、不再铸造。测试legacy_prefix_refs_still_decode与foreign_refs_do_not_decode分别验证了历史兼容与外来 ref如slack/v1/...、builtin:web_app的拒绝。RFC 8291 载荷加密aes128gcm 逐字节拆解加密实现在 crates/domains/ironclaw_web_app/src/crypto.rs完整实现 RFC 8291 的aes128gcm内容加密。每条消息一个记录ECDH(P-256)对订阅的p256dh密钥HKDF-SHA256通过 RFC 8291 规定的 info 字符串AES-128-GCM加密plaintext || 0x02按 RFC 8188 帧格式输出。输出帧格式salt(16) || rs(4) || idlen(1) || as_public(65) || ciphertextsalt16 字节每次加密由系统随机数生成器新鲜产生rs声明的记录大小RECORD_SIZE 4096——推送服务普遍把整个 body 封顶在 4096 字节RFC 8030 §7.2 建议至少支持单记录恒够用idlen固定65非压缩 P-256 点长度as_public本次临时密钥对的 65 字节公钥ciphertextplaintext || 0x02末记录填充分隔符经 AES-128-GCM 加标签后的密文。由此推出明文预算MAX_PLAINTEXT_BYTES 4096 - 86 - 16 - 1 3993头部 86 字节、GCM 标签 16 字节、填充分隔符 1 字节。密钥派生协议固定的 info 字符串derive_cek_and_noncecrypto.rs 第 142-164 行执行 RFC 8291 §3.3-3.4 的两阶段 HKDFIKM HKDF(saltauth_secret, ikmecdh_secret, infoWebPush: info\0 || ua_public || as_public, 32)CEK HKDF(salt, IKM, Content-Encoding: aes128gcm\0, 16)NONCE HKDF(salt, IKM, Content-Encoding: nonce\0, 12)源码注释强调WebPush: info是PROTOCOL-FIXED字面量无论通道叫什么名字都不能改改动会破坏所有推送服务的解密——这是被 Appendix A 测试向量钉死、并被词汇退役门禁放行的常量。安全属性与 Appendix A 钉死加密只用接收方的公开材料加一次性临时密钥对与新鲜 salt不涉及任何存储密钥——这正是它能住在领域 crate 而 VAPID 签名必须留在 host egress 边界的原因。测试文件中有三个关键用例rfc8291_appendix_a_intermediate_values_match用 RFC 8291 Appendix A 的固定输入断言as_public、ecdh_secret、cek、nonce四个中间值与附录完全一致rfc8291_appendix_a_body_round_trips_and_frames_correctly用 UA 私钥对加密结果解密验证帧头salt、rs4096、idlen65、as_public、末记录分隔符0x02与原文完整还原random_path_produces_unique_bodies_that_fit_the_budget同一明文两次加密产生不同 body每次新 salt 新临时密钥且不超 4096 字节预算。超长明文与畸形接收方材料分别由oversized_plaintext_fails_closed、malformed_recipient_material_fails_closed保证 fail-closed。RFC 8292 VAPID 密钥材料生成VAPID 材料生成在 crates/domains/ironclaw_web_app/src/vapid.rs。generate_vapid_key_material(subject)生成 P-256 ES256 密钥对并产出两个东西material_json序列化的VapidCredentialMaterialV1作为通道凭据存储包含私钥public_key_b64url65 字节非压缩 P-256 公钥的 base64url即浏览器侧applicationServerKey。VapidCredentialMaterialV1的 schema 定义在 crates/contracts/ironclaw_host_api/src/http.rs第 194-202 行三个字段es256_private_key_pkcs8_b64urlPKCS#8 P-256 私钥、public_key_b64url、subjectRFC 8292sub声明。该类型同样手工实现Debug以脱敏私钥且#[serde(deny_unknown_fields)]拒绝结构外字段validate_shape在组合层就拒绝结构损坏的持久化凭据避免每次投递都被推送服务拒收后才暴露问题。subject按 RFC 8292 §2.1 校验为mailto:或https:URIvalidate_vapid_subjectvapid.rs 第 87-107 行长度上限 256 且不含控制字符——畸形 URI 在生成时刻就被拒绝而不是让每次投递都被推送服务退回。测试subjects_are_validated覆盖了mailto:opsexample.com、https://ironclaw.example的通过路径与空串、mailto:、http://x等的拒绝路径。需要强调边界本模块只生成从不签名也从不读回已存材料。每条请求的Authorization: vapid头由 host egress 凭据边界ironclaw_host_runtime计算其aud声明绑定到目标请求 URL 的 origin令牌只对本次发送的目标推送服务有效。请求规划build_push_request 与 WebAppRequestPlan通知载荷与请求规划在 crates/domains/ironclaw_web_app/src/message.rs。载荷语法WebAppNotificationPayload服务端与 Web App service worker 之间的契约是WebAppNotificationPayloadfrontend/public/sw.js恰好解析这些字段pub struct WebAppNotificationPayload { pub title: String, // 标题≤120 字符 pub body: String, // 正文≤1500 字符 pub url: String, // 应用相对深链如 /automations pub tag: OptionString, // 合并标签同 tag 互相替换≤64 字符 }WebAppNotificationPayload::new执行一整套强制语法title/body 消毒并字符封顶url强制为应用相对路径单个前导/、无 scheme、无协议相对//、无控制字节否则回退/测试non_app_relative_urls_collapse_to_root验证https://evil.example.com/x、javascript:alert(1)等全部塌缩到根路径tag消毒并封顶按序列化 JSON 字节数裁剪 bodyfit_body_to_byte_budget在超过MAX_PAYLOAD_JSON_BYTES 3800时按字符边界收缩并追加省略号。这一点很关键——测试multibyte_body_is_trimmed_to_the_serialized_byte_budget证明1500 个四字节 emoji 能通过字符上限却序列化出约 6 KiB纯字符截断会让每次发送都失败按字节裁剪才能保证落进单记录明文预算3993 字节之内。请求计划WebAppRequestPlanbuild_push_request(subscription, payload, ttl_seconds, urgency)message.rs 第 185-206 行把订阅 载荷转成一个无传输层的请求计划pub struct WebAppRequestPlan { pub host: String, // 推送服务 host入网时已白名单校验 pub path_and_query: String, // endpoint 的 origin-form 路径 查询 pub headers: Vec(static str, String), // ttl / urgency / content-encoding / content-type pub body: Vecu8, // 完整 aes128gcm 加密体 }四条协议头在计划中备齐ttl默认DEFAULT_TTL_SECONDS 86400一天——够撑过合上笔记本的离线又不至于几天后重放陈旧自动化噪音、urgencyRFC 8030 §5.3 的very-low/low/normal/high、content-encoding: aes128gcm、content-type: application/octet-stream。测试plan_carries_protocol_headers_and_origin_form_path验证了 header 值与 origin-form 路径如/wpush/v2/token?x1的组装。错误模型与信息脱敏WebAppErrorcrates/domains/ironclaw_web_app/src/error.rs是全部失败的类型化出口且严格遵循原因只带消毒摘要原则endpoint URL、密钥材料、后端错误内部细节永不过界endpoint 是 capability URL按敏感对待。具体到UnsupportedPushService只携带 hostStore变体根本不携带消息——根源通过tracing在 store 边界记录后即被丢弃。完整闭环从 manifest 声明到浏览器弹窗通道包的 manifest单一事实来源通道包的 crates/extensions/packages/web-app/manifest.toml 声明了整个通道表面id web-app、trust first_party_requested运行时为 first_party[channel.reply] transport stream回复流经持久化投影管道SSE/WebSocket直接推到浏览器adapter 无需回复半部[channel.delivery] transport push、requires_enrollment true投递轴与回复轴正交——一次运行既可以向打开标签页流式输出答案也可以在用户没看屏幕时触发推送但推送必须等到用户至少入网一个浏览器[channel.ingress]authenticated_session验证无 webhook 挂载浏览器请求永远到不了 webhook 命名空间[[channel.egress]]三个推送服务 host 的精确白名单——fcm.googleapis.com、updates.push.services.mozilla.com、web.push.apple.com每个都带credential_handle web_push_vapid与injection { type vapid_authorization }由tests/manifest_lockstep.rs与ironclaw_web_app的白名单保持锁步。admin_configuration中web_push_vapid字段标注host_managed true、required false密钥对由组合层在启动时生成并播种进 scoped secret store操作员不可手写——覆盖材料即轮换签名密钥在完整的再生成并重发布轮换流程落地前旧applicationServerKey下入网的浏览器都需要重新入网。启动初始化与 delivery 半部启动路径在 crates/app/ironclaw_cli/src/runtime/native_extensions.rsWebAppChannelInitializer默认 subject 为mailto:webpushironclaw.invalid可经环境变量覆盖调用generate_vapid_key_material后经组合层的中性凭据上下文以store_credential_if_absent存储已存在则跳过保证重启不轮换密钥随后读回、解析、validate_shape()最后只发布公钥作为不透明 bootstrap 文档。投递侧 crates/extensions/packages/web-app/src/channel.rs 实现了ChannelDelivery的唯一半部把 envelope 各部分合并渲染为一条通知NOTIFICATION_URL /automations按每个注册记录解析PushSubscriptionRecordparse_registration——畸形文档只废掉该注册不废整个投递并走与过期 endpoint 相同的 prune 上报路径逐条加密并 POST 到受控 egress推送服务回报已不存在的注册 id 由FanOutTally.pruned汇总回传给 hosthost 拥有记录此半部从不写存储。验证与质量门禁README 给出两条标准验证命令cargo test -p ironclaw_web_app cargo clippy -p ironclaw_web_app --all-targets --all-features -- -D warnings单元测试覆盖了本文章讨论的全部语义endpoint 白名单闸门与形状拒绝subscription.rs、CAS 并发双入网与每用户上限store.rs、binding-ref 往返与旧前缀解码grammar.rs、RFC 8291 Appendix A 中间值与帧级解密crypto.rs、VAPID 材料解析与 subject 校验vapid.rs、载荷字节预算与 URL 塌缩message.rs。跨 crate 侧crates/extensions/packages/web-app/tests/manifest_lockstep.rs 钉住 manifest 与领域常量的锁步关系crates/extensions/packages/web-app/tests/registration_parsing_contract.rs 校验注册文档解析契约。从源码结构可以推断该通道的设计哲学是分而治之各守边界领域 crate 只算不送、只生成不签名、只规划不执行——发送交给通道包与 restricted egress签名交给 host egress 凭据边界路由交给ironclaw_outbound而订阅管理的用户界面则位于ironclaw_assistant与 WebUI。这种边界划分使得密码学正确性RFC 8291 附录向量钉死、存储并发正确性CAS 重放测试证明与部署配置manifest 单一事实来源都能在各自最小的范围内被独立验证。【免费下载链接】ironclawIronClaw is an Agent OS focused on privacy, security and extensibility项目地址: https://gitcode.com/gh_mirrors/iro/ironclaw创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
企业数字化 ERP 产品动态
相关推荐
5个实战技巧让数据库插入快3倍新手避坑指南 5个实战技巧让数据库插入快3倍新手避坑指南 版本升级后 API 全变了,很多老代码直接报错,新手更是两眼一抹黑,这就是典型的 新手避坑 盲区。别慌,今天我们不聊虚的,直接切入 数据库插入 的性能瓶颈。你写的那条 INSERT INTO… · 2026/9/23 12:19:49
iOS图书商城源码解析:从环境搭建到二次开发避坑指南 简介:这是一套面向iOS开发初学者与进阶学习者的图书商城系统完整源码,采用Swift语言编写,适合用于课程设计、毕业项目参考或商城类App开发练手。资源包共58个文件,以25个swift源码文件为核心,配合6个json与5个plist配置… · 2026/9/23 12:19:42
契约式代码安全审计:让漏洞在提交前自检 1. 这不是“安全扫描”,而是让代码自己开口说漏洞“security-audit-skill”——这个标题乍看像一个技术标签,实则藏着一套正在悄然重构开发工作流的底层能力。它不指向某款商业扫描工具,也不等同于跑一遍Snyk或SonarQube;它描述的… · 2026/9/23 14:32:42
知识变现全流程指南:从经验到知识产品的六步落地法 我见过太多想做知识付费的人,一上来就对着镜头录课、对着电脑写专栏,结果忙了几个月,卖出去不到十份。也见过有人把一段自己总结的工作流整理成文档,挂到一个细分社群里,当天就有人付款。差别从来不在“谁更专业”&… · 2026/9/23 14:32:42
密码存储安全:哈希与加密的核心区别及最佳实践 1. 密码存储的本质:为什么哈希优于加密在网站开发中,用户密码的安全存储是每个开发者必须面对的基础问题。我见过太多项目因为错误地使用加密(Encryption)而非哈希(Hashing)来存储密码,最终导致… · 2026/9/23 14:32:42
ZCode静默上传Git历史的技术真相与风险解析 1. 项目概述:一场被代码提交记录戳穿的信任裂痕“智谱 ZCode 静默上传 Git 历史”——这十个字不是技术文档的标题,而是一份事故通报的导语。它背后没有炫酷的AI模型演示,没有流畅的IDE插件动画,只有一行被开发者反复翻查、最终在… · 2026/9/23 14:32:42
CPABE Java 实现详解:从双线性对到策略解密 简介:本资源是一套基于CPABE(密文策略属性基加密)的Java完整实现源码,面向密码学初学者、信息安全专业学生及Java开发者,用于理解与实践属性加密的核心机制。资源包含85个文件,主体为23个Java源码文件与19个… · 2026/9/23 14:32:33
明日之洗礼 春华:从报错到精通的市政公用后端实战 明日之洗礼 春华:从报错到精通的市政公用后端实战 盯着满屏红色的 StackTrace 报错,是不是脑子瞬间一片空白?那种“明明逻辑没问题,代码却跑不通”的无力感,是每一个后端新手在踏入市政公用工程数字化领域时绕不开的坎。很多兄弟觉得这些市… · 2026/9/23 14:32:33
3招搞定手机怎么下载微信面试难题实战项目解析 3招搞定手机怎么下载微信面试难题实战项目解析 面试被问“手机怎么下载微信”背后的原理,90%的人答不上来。别笑,这看似弱智的问题,实则是考察你对移动应用分发机制、安全校验及网络协议理解的试金石。我带过不少校招新人,他们背了八股文,却连一个A… · 2026/9/23 0:00:03
你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 面试被问“高并发下如何保证消息不丢失”,你张口就是“用Redis”,结果面试官追问“如果Redis宕机了怎么办”,你瞬间卡壳。这种场景太常见了,很多新手在背八股文时,只记住了技术名词… · 2026/9/23 0:00:29