Media and Blob
This content is not available in your language yet.
0. 规范语言
本文中的规范关键字(MUST / SHOULD / MAY 等)按 conformance/normative-language.md 解释;仅大写形式具规范约束力。
1. 目标
Blob service 提供内容寻址存储。Media profile 在 Blob 之上定义 MIME、缩略图、认证下载、加密附件和保留策略。
2. Blob Metadata
{ "blob_id": "ak:blob:019a7360-0000-7000-8000-000000000004", "schema": "ak.schema.blob.v1", "realm_id": "ak:realm:Ac1aCK8aQdnkYImvdH3DFjq4jDCP198pXYWCGzGuVyj5", "content_digest": "sha256:...", "size_bytes": 1234, "media_type": "image/png", "created_by": {"kind":"account","account_id":{"principal_id":"ak:did_core:webvh:z2gNJAM6eKtNKMnbxHuqHCnaw","station_id":"ak:did_core:webvh:z6mkfixturestationexample"}}, "created_at": "2026-04-26T00:00:00Z", "encryption": null}字段规则:
| 字段 | 类型 | 必填 | 说明与约束 |
|---|---|---|---|
blob_id | string | required | Blob metadata 资源的 producer-allocated UUIDv7 身份;不承诺内容字节。 |
schema | ak.schema.blob.v1 | required | Blob metadata schema discriminator。 |
realm_id | id:realm | conditional | Owning Realm。普通用户/组织上传 MUST 设置,用于授权、asset privacy policy enforcement、retention 与 GC。仅当 deployment policy 显式声明的全局/跨 Realm 服务 blob(例如 avatar 公共预览)才可省略。 |
content_digest | digest | required | 服务端计算的内容 digest,wire 形态为 <algo>:<lowercase_hex>。 |
size_bytes | int | required | 字节大小。 |
media_type | string | optional | 上传声明或服务端校正后的 MIME。缺省为 application/octet-stream。 |
created_by | ActorId | required | 上传 Actor 或 service 的完整 Arkret 身份。 |
created_at | datetime | required | 服务端接收时间。 |
updated_by | ActorId | optional | 最近更新 Blob metadata 的 Actor 或 service 的完整 Arkret 身份。 |
updated_at | datetime | optional | 最近更新时间。 |
filename | string | optional | 用户提供或服务生成的文件名;不得用于路径拼接。 |
encryption | object/null | required | 最小存储加密分类:plaintext 为 null;ciphertext 为 closed {scheme},scheme 只取 ak.blob.whole_file_aead.v1 / ak.blob.stream_aead.v1。不含密钥、group refs、明文 MIME / size 或完整 Content Block descriptor。 |
命名说明:blob_id=ak:blob:<uuidv7> 只标识 Blob metadata 资源,blob_ref=ak:blob:<suite>:<hex> 只标识并承诺 exact content bytes;两者的 schema 形态互斥,consumer 不得按值猜测一种字段的含义。Blob metadata、Media metadata 和 Content Block descriptor 中的字节数统一使用 size_bytes;不得使用裸 size 表示字节数(见 models/common-fields.md §3.0.1)。Blob metadata 以 content_digest 承诺其 metadata id 对应的内容;任何携带 content-addressed blob_ref 的 carrier 不得再携同原像 content_digest / ciphertext_digest 镜像。
上传请求 schema 见 blob-operations.schema.json#/$defs/blob_upload_request_body,响应 schema 见 blob-operations.schema.json#/$defs/blob_upload_outcome。
上传规则:
-
encryption缺失不得猜测为明文;非 null 为密文,服务端 MUST 持久化分类且 MUST 拒绝 presign。完整附件 descriptor 只留在获准 Content Block 中,不上传给 Blob storage。密文 transportmedia_typeMUST 为application/octet-stream,不得嗅探或公开明文 MIME。 -
media_type表单字段存在时是声明 MIME;content partContent-Type同时存在且不同则以schema_violation拒绝并零写入。无表单字段时使用 content part 类型;均无则 octet-stream。只有encryption=null时允许下述内容校正。 -
上传请求不承载自由
purpose;用途权限来自 owning Realm policy 与已验证 source refs,不得以客户端自报 purpose 扩大 scope / privacy / presign 权限。 -
Canonical HTTP upload body MUST be
multipart/form-datawith a requiredcontentbinary part and requiredsize_bytesmetadata field and requiredencryptionUTF-8 JSON metadata part (nullor the closed storage classification above). ThecontentpartContent-Typeis optional; its default value isapplication/octet-stream. -
客户端 SHOULD 提供准确
Content-Type,但服务端 MUST 把上传声明的 MIME 和文件名视为不可信 metadata。 -
如果服务端发现声明 MIME 与内容明显冲突,MAY 把
media_type降级为application/octet-stream,并记录安全标记。 -
文件名 MUST 做控制字符、路径分隔符和过长字段清理;不得影响
blob_ref或存储路径。 -
upload_receipt若返回,MUST 绑定 content-addressedblob_ref、size_bytes、received_at与issuer_id,并由 Blob Service DID 对 receipt canonical bytes 签名;digest 从blob_ref唯一恢复,不得作为 sibling 字段重复;receipt 不得作为开放实现私有对象返回。 -
大文件 / 弱网场景 MAY 通过 §2.1 的可续传上传 binding 完成同一次上传;该 binding 是可选扩展,是否支持以及如何发现见 §2.1 与
sync/service-surface.md§3。
2.1 可续传上传(Resumable Upload binding,optional extension)
可续传上传是 ak.self.blob.upload.create.v1 操作的可选替代传输 binding,面向大文件与弱网下的断点续传。它不是新的 operation_id,也不改变 Blob 的内容寻址与 receipt 语义;§2 的上传规则(声明 metadata 不可信、文件名清理、receipt 签名)对该 binding 同样适用。普通实现 MAY 暴露该 binding;当服务端未在 /_arkret/describe 声明该能力时,客户端 MUST 回退到 §2 的 canonical multipart/form-data 上传,不得对猜测的 endpoint 直接发起续传。
TUS 创建请求的 POST MUST 携带
Arkret-Operation: ak.self.blob.upload.create.v1;即使当前 advertised candidate 只有该版本也不得省略。
服务端 MUST 在读取 upload metadata/body 之前验证 selector。缺失返回 operation_selector_required;未知、
未广告或与 route family 不匹配时返回 unsupported_operation_version。后续 PATCH / HEAD / DELETE
同样 MUST 携带创建资源时绑定的 exact Arkret-Operation,不得另行选择或从 payload 推导其他 schema 版本。
协议绑定(normative)
- 该 binding MUST 采用 tus resumable upload 协议,当前 baseline 为
tus 1.0.0。服务端 MUST 支持creation扩展;SHOULD 支持creation-with-upload、checksum、expiration与termination。 - 创建续传资源(tus
POST)所需的认证、capability(ak.self.blob.upload.create.v1)与 quota(blob_max_bytes)与 canonical 上传完全一致。后续PATCH(写入 segment)、HEAD(查询Upload-Offset续传点)、DELETE(终止)作用于服务端返回的 upload URL,是 tus 原生 verb,不注册为独立 arkret operation。
内容寻址不变式(normative)
- 续传只是字节传输方式。所有 segment 组装完成后,服务端 MUST 对完整字节计算 Realm-selected digest 并编码为最终
blob_ref;同一字节序列走 §2 canonical multipart 上传必须得到 byte-identicalblob_ref,并按 §2 规则签发不含 sibling digest 的upload_receipt。独立 Blob metadata 资源仍以blob_id+content_digest表达其身份与内容承诺。 - tus 的 offset 分块(
Upload-Offset/ 每个PATCHchunk)是传输层切分,与加密附件ak.blob.stream_aead.v1(§3.3)的 AEAD segment 是两个独立维度:实现 MUST NOT 把 tus chunk 边界与 AEAD segment 边界互相约束或混为一谈。续传承载的始终是(可能已在客户端 AEAD 加密的)Blob 字节,加密形态由客户端在上传前决定。
能力发现(normative)
- 该 binding 按
sync/transport-bindings.md§6.1 分类为 per-operation HTTP 伴生 binding:本节即其 normative binding 文档,不需要独立ak.profile.binding.*profile。 - 支持该 binding 的服务端 MUST 在
/_arkret/describe同时声明:supported_features含ak.feature.blob.resumable_upload.tus.v1;transport_bindings含一条kind="tus"的 binding,携带 tus endpoint 的base_url、operations: ["ak.self.blob.upload.create.v1"]、extension_profile_required: null、tus_version(支持的协议版本列表)与tus_extensions(支持的扩展列表);limits携带下文的续传相关上限。
- 该 binding 只伴随 authenticated
ak.self.blob.upload.create.v1,目标固定为当前账号会话已经绑定的自己 Station;v1 不允许客户端从任意blob_nodedescribe 选择一个写入目标。客户端首次接受 binding、binding / policy 变化或会话/route freshness 失效时,MUST 核对当前会话的 exact Station/HTTPS origin 与 describe 中该 operation 的声明;不得为此下载 DID 历史或信任陌生 endpoint 自报。/_arkret/describe是服务级能力声明面;tusOPTIONS响应(Tus-Resumable、Tus-Version、Tus-Extension、Tus-Max-Size)是 endpoint 级的线上确认。二者 MUST 一致;冲突时客户端以已绑定自己 Station 的 describe 与服务端实际拒绝为准,不得仅凭对猜测 endpoint 的裸OPTIONS探测作为发现手段。跨 Station/独立 Blob 服务若未来需要该 binding,必须先登记具体代理/结果合同;本节不提供该能力。
隐私(normative)
- tus
Upload-Metadataheader MUST NOT 携带私有或 E2EE Blob 的明文文件名、MIME 或任何可枚举本地路径;此类 Blob 的Upload-Metadata除字节数和必填encryption分类外 SHOULD 省略。创建时encryptionmetadata 是 Base64 编码的 UTF-8 RFC 8785 JCS(null或上述 closed{scheme}),服务端冻结至 finalize,后续 PATCH 不得变更;不得携带完整附件 descriptor、解密材料或明文 MIME / group refs。原始文件名 / MIME 的归属与models/file-transfer.md§2 的 file-transfer 规则一致。 - upload URL MUST 按 authenticated 资源处理:每个 tus 请求(
POST/PATCH/HEAD/DELETE)MUST 独立认证;upload URL MUST NOT 作为可转发 bearer 凭证对待——这与 §5.4 presign 的只读 bearer URL 边界相反:presign 仅授权只读get/head,续传是写路径。
过期与 GC
- 未完成的续传资源 MUST 有有限生命周期。服务端 SHOULD 支持 tus
expiration扩展并通过Upload-Expires暴露过期时间。未完成上传 MUST NOT 产生可被 Event / account-data 引用的blob_ref;到期后服务端 MAY 直接回收已接收的部分字节。
limits 键
resumable_upload_incomplete_ttl_seconds:未完成续传 part 的最长保留秒数。resumable_upload_max_bytes(optional):单次续传上传上限;省略时回退到 canonicalblob_max_bytes/max_body_bytes语义。resumable_upload_min_chunk_bytes(optional):服务端要求的最小非末段PATCHchunk 字节数。
版本与前向兼容
- 当前 baseline 是 tus 1.0.0。IETF httpbis 的 Resumable Uploads(
draft-ietf-httpbis-resumable-upload)标准化后,其 binding MUST 通过单独登记的 feature id 与新的transport_bindings.kind增量声明,不改写本节 tus 1.0.0 语义;客户端按 describe 声明的 feature / binding 选择具体协议,对两者均可同时声明的服务端 SHOULD 优先选用其支持的最新标准化形态。
3. Encrypted Attachment
加密附件的 key_ref MUST 是 closed MLS group-binding 对象 {algorithm:"MLS",group_state_ref};current v1
不存在 {algorithm,key_id} 或其他 profile 分支。MLS 附件 epoch 不在 wire 上重复声明;其唯一派生式为
attachment_epoch = resolve_exact_winning_group_state(key_ref.group_state_ref).epoch。Event ref 必须解析到
accepted genesis 或 winning commit,proof hash 必须解析到等价的 exact winning group state;无法解析、非
winning state、hash/ref 歧义或缺少该 state 时 MUST 在密钥派生和解密前 fail closed。
{ "blob_ref": "ak:blob:sha256:...", "encrypted": true, "key_ref": { "algorithm": "MLS", "group_state_ref": "ak:event:Af-qizSfVETcKiliXG093VVneO4nQF194ZXGkMWJijix" }, "content_key_salt": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8", "nonce": "base64url...", "size_bytes": 1234, "media_type": "image/png", "encryption_algorithm": "xchacha20_poly1305"}3.0 MLS per-object 内容密钥(normative)
closed encrypted_attachment 在 key_ref 后增加 required content_key_salt,值为
base64url_no_pad(CSPRNG(32 octets)),canonical wire 恰为 43 chars。salt 是公开派生输入,不是
content key、key envelope、Blob 上传 metadata 或新对象 ID。每个新附件 object(包括缩略图)独立生成,
先与 immutable descriptor intent 耐久冻结;exact retry、上传恢复与同一密文再次引用 MUST 复用原
salt/descriptor/密文,改变明文或加密参数的重新加密 MUST 创建新 salt。不同 object MUST NOT 复用 salt。
Producer 使用冻结的 authoring scope 与已验证 group provenance;接收端先验证承载 descriptor 的 ContentBlock 与 enclosing Event。双方均按 §3 解析 exact winning MLS state、其 epoch、immutable genesis Event ref 和 effective scope,恢复获授权的该 epoch MLS checkpoint。effective scope 使用 encryption-and-audit §5.1 的 closed 形态,必须与 enclosing Event 及该 group provenance 逐字段匹配,不从下载 URL 或当前 UI 推断。historical epoch 不得替换为当前 epoch; 拥有 ref、salt 或密文字节不授予历史读取或密钥恢复权。缺 state/secret、非 winning state、scope 不符、 非 canonical salt 或未授权时 MUST 在派生/解密前 fail closed,不使用其它 epoch、随机 key 或其它 label fallback。
Context = JCS({effective_scope,genesis_event_ref,epoch,scheme,encryption_algorithm,content_key_salt})content_key = MLS-Exporter("ak.blob-content-key-v1", Context, 32)primitive 是 RFC 9420 §8.5 的 MLS-Exporter,使用该 exact epoch 的 exporter_secret 和 MLS
cipher suite 的 Hash/KDF;不是 Realm ID 的 digest suite,也不是对 secret 直接 HKDF。label 的唯一真源是
exporter-label-registry。Context 的逻辑字段顺序如上,实际 UTF-8 字节严格由 RFC 8785 JCS 排序,
不加换行、BOM、null、未知成员或字符串化的数字;epoch 为解析出的整数。whole-file descriptor 省略
scheme 时唯一补为 ak.blob.whole_file_aead.v1;algorithm 保持 descriptor 的 exact registered string。
genesis_event_ref 是已验证 group 的 immutable genesis EventId,不是当前 Commit ref;Event ref 与等价
proof-hash key_ref 解析到同一 group state 时 MUST 得到同一 Context 与 key,wire key_ref 不直接进入 KDF。
32 bytes 对应本 descriptor 已登记的 AES-256-GCM/XChaCha20-Poly1305 内容密钥长度。
blob_ref、密文 digest、enclosing Event ID 和接受后的 Commit 不得进入 KDF:它们依赖密文,会造成循环。
content_key_salt MUST 同时进入 §3.1/§3.3.3 的 immutable AAD。原 canonical key_ref 继续进入 AAD,
所以等价 ref 的 key 相同不允许在加密后替换 descriptor 的 ref。完整 descriptor(包括 salt、ref、scheme、
algorithm、nonce/prefix、分段参数、明文尺寸与 MIME)必须由已认证 ContentBlock/Event 绑定,
Blob metadata 的最小 encryption 分类不携带这些字段,也不能作为派生权威。thumbnail 使用独立 salt
与 nonce,仍是本合同的附件;不另登记 thumbnail 密钥载体或 label。
字节级 KAT、Context 分离、ref 等价与失败关闭向量见
ak.vector.blob.content_key_derivation.v1;此合同不修改非 MLS file-transfer 的独立 key_delivery。
3.1 AEAD nonce uniqueness(normative)
AEAD nonce 在同一 §3.0 派生 key 下复用会使相关密文同时失去机密性与完整性;不同 salt 的不同 key 不共享 replay 域。整文件形态使用 full-width counter 编码:nonce = I2OSP(durable_sender_counter, AEAD.Nn);nonce 长度、counter 持久化、replay 防护与 AAD binding 以本节为准。Blob / attachment envelope 的 purpose 取值固定为 "blob-attachment"。
-
Wire encoding:
nonce字段 base64url 编码 N_AEAD 字节(XChaCha20-Poly1305 → 24 bytes;AES-GCM → 12 bytes);接收方 MUST 在解密前校验 nonce 长度匹配 AEAD algorithm 声明。 -
AAD binding(normative): Blob / attachment envelope 的 AEAD AAD MUST 是 本节 pre-encryption immutable header 的 RFC 8785 JCS UTF-8 bytes。该 header 对整文件形态 (
ak.blob.whole_file_aead.v1) 至少绑定:scheme、alg、key_ref(canonical 形态)、按本节唯一规则派生的epoch;content_key_salt、nonce;purpose = "blob-attachment"、aead_profile;media_type;size_bytes——在encrypted_attachmentdescriptor 中它是明文字节数(§3.3.1 的段数只从它与segment_bytes派生),在 authority commit 之前即已确定,因此进入 AAD 不产生任何循环。它与 Blob metadata 顶层的size_bytes(存储的密文字节数)是两个不同对象上的不同量,实现 MUST NOT 互相替代;descriptor 中 MUST NOT 再增加第二个明文尺寸字段;- 任何 profile 声明的 content policy digest(该 digest 必须在加密前已确定)。
分块形态的逐段 AAD 见 §3.3.3。content-addressed
blob_refMUST NOT 进入 AAD:其内嵌 digest 覆盖含 AEAD tag 的完整密文,进入生成同一 tag 的 AAD 会形成不可构造循环(本节 immutable header 规则)。它是 post-encryption commitment,MUST 由引用该附件的已签名 Event / encrypted descriptor / upload receipt 覆盖;若某条 Blob 路径没有任何外层认证,MUST 补齐该认证,MUST NOT 把 ref 或其 digest 塞回 AEAD AAD。 -
禁止形态: 实现 MUST NOT 使用以下 nonce 来源:
- 纯随机 96-bit nonce(birthday bound 不够);
- 全局共享 counter(协调成本 / 同步攻击面);
- 用户输入派生(可控 = 可碰撞);
- HMAC/Hash 输出截断后直接作为完整 nonce 的形态;
- 任何不是
I2OSP(durable_sender_counter, AEAD.Nn)的整文件 nonce 形态。
-
整文件形态(
ak.blob.whole_file_aead.v1)接收方校验(normative):整文件 envelope 只携单nonce字段。Producer MUST 以原子 CAS 耐久预留严格递增的durable_sender_counter,并把整个AEAD.Nn字节 nonce 编码为I2OSP(counter, AEAD.Nn);不得为 sender domain、device 或其它信息保留高位 prefix。Receiver MUST 使用 §3.0 的 exact canonical Context 作为 key identity(不是 rawkey_ref,等价 ref 不分裂 replay 域),解码恰好AEAD.Nn字节的 nonce,以OS2IP(nonce)取得 counter,并把(Context, purpose, aead_profile, counter)绑定到 exact ciphertext digest。相同 tuple 的相同密文折叠;相同 tuple 的不同密文 MUST 以failed_precondition(aead_nonce_counter_replay)fail closed。高位字节不是任意 padding:任何不等于该 counter 的 canonical full-widthI2OSP编码、counter 回退/复用、counter 耗尽后的继续发送或 random fallback 都 MUST 拒绝(reasonaead_nonce_derivation_invalid)。
E2EE 附件 metadata MUST 使用 blob.schema.json#/$defs/encrypted_attachment 的 wire 形态。Producer 必须提供由 stored ciphertext bytes 派生的 content-addressed blob_ref,不得再提供 sibling ciphertext_digest 或明文 hash;若 deployment 出于审计需要保留 plaintext commitment,必须使用每事件随机 salt 的 commitment 或服务持有的 HMAC / pepper commitment,边界见 encryption-and-audit.md §2.3。
3.2 形态选择:整文件 AEAD 与分块流式 AEAD
加密附件 envelope 通过 scheme 字段声明密文构造形态,v1 定义两种并存的合法形态:
scheme | 形态 | 适用 |
|---|---|---|
ak.blob.whole_file_aead.v1 | 整文件单次 AEAD:单 nonce;整体密文 digest 由 content-addressed blob_ref 内嵌承诺,校验通过后才释放明文。 | 默认形态;小文件、缩略图。 |
ak.blob.stream_aead.v1 | 分块流式 AEAD(STREAM / OAE2):明文切成固定大小 segment,每段独立 AEAD 加密,可边下边验、内存有界。 | 大文件、流式播放、跨设备传输。见 §3.3。 |
- envelope MUST 携带
scheme字段。未携带scheme字段时的 missing-field default 为ak.blob.whole_file_aead.v1(整文件形态)。 - 发送方 MAY 对任意附件选用
ak.blob.stream_aead.v1;大文件 SHOULD 选用分块形态(见models/file-transfer.md)。 - 接收方 MUST 按 envelope 的
scheme字段分派解密路径;遇到未知schemeMUST fail closed(unsupported_attachment_scheme),不得回退到任何其它形态尝试解密。 ak.content.long_text例外(normative):../models/content-types.md§4.1.1 的 E2EE 长文本正文 MUST 使用ak.blob.stream_aead.v1与对应的_stream算法,MUST NOT 使用整文件形态;其blob_refMUST 是 content-addressed(ak:blob:(sha256|blake3):<64 hex>),且内嵌 digest MUST 等于完整 stored ciphertext bytes 的 digest。attachment 不再携 siblingciphertext_digest;独立 Blob metadata 的content_digest必须与 ref 内嵌值一致。plaintext 长文本的blob_ref同样承诺完整规范化正文字节;blob_id不得用于该 Content Block。强制 streaming 是为了让超过 256 KiB 的正文能边下边验且内存有界,不是同一语义的第二种可选形态。- 两种形态的 envelope 都 MUST 满足 §3.1 的 AEAD nonce 纪律;分块形态的 nonce 兼容关系见 §3.3.2。
3.3 Streaming Chunked AEAD(ak.blob.stream_aead.v1,normative)
本节的 key_ref、MLS epoch 派生与 §3.3.3 AAD 字段表属于 encrypted_attachment 合同。holder-private
file-transfer 复用本节的段长、段数、nonce、末段与密文拼接算法;它不携 MLS key_ref,fresh content key 与
逐段 AAD 的唯一合同在 file-transfer.md §4。两种上下文不得
混用 AAD,也不得为了套用 attachment 模型给 file-transfer record 添加未登记字段。
本节定义分块流式 AEAD scheme。其构造采用业界已生产化的 STREAM(OAE2,online authenticated encryption)形态,使接收方能在收到每个 segment 时增量验证并安全释放对应明文段,同时保留顶层单值整体完整性语义。
3.3.1 Segment 切分
- 明文按
segment_bytes字节切成有序 segment 序列;除最后一段外每段长度 MUST 恰为segment_bytes,最后一段长度 MUST 在1 .. segment_bytes区间(空明文按单个长度为 0 的末段处理,且该段仍携带末段 flag)。 segment_bytesMUST 在 envelope 中显式声明,单位为字节。v1 默认值为262144(256 KiB)。取值范围与派生段数上限见conformance/scalability-constraints.md§6。- verifier MUST 令
N = max(1, ceil(size_bytes / segment_bytes))。N是本地派生值,envelope MUST NOT 携带segment_count镜像字段。 - segment_index 从
0开始连续单调递增,无空洞;第N - 1段是末段。
3.3.2 Segment nonce 构造与 §3.1 兼容关系(normative)
每个 segment 独立用同一 content key 做 AEAD 加密,但使用各自的 nonce。segment nonce 构造为:
nonce = nonce_prefix || u32_be(segment_index) || last_segment_flag
其中: nonce_prefix 长度 = N_AEAD - 5 字节(per-object 随机,单调性由后缀保证) u32_be(segment_index) 4 字节 unsigned big-endian segment 序号 last_segment_flag 1 字节:0x00 普通段 / 0x01 末段N_AEAD 取值见 §3.1(XChaCha20-Poly1305 → 24,因此 nonce_prefix 为 19 字节;AES-GCM → 12,nonce_prefix 为 7 字节)。
nonce_prefixMUST per-object 随机生成(至少N_AEAD - 5字节 CSPRNG 输出),并在 envelope 中以 base64url 编码携带(字段nonce_prefix)。同一 content key 下不同 object MUST 使用不同nonce_prefix。- 与 §3.1 整文件形态的兼容关系:§3.1 的 nonce 是整个
AEAD.Nn字节宽度上的I2OSP(durable_sender_counter, AEAD.Nn),没有 sender prefix。本 scheme 使用 per-object 随机nonce_prefix加单调 segment 后缀;每个 transfer 又使用 fresh content key(content key MUST NOT 跨 transfer 复用,见 §3.3.4),因此(content key, nonce)对在全局唯一。两种 scheme 的 nonce 构造由scheme封闭分派,接收方不得把 stream prefix 规则套用到 whole-file nonce,也不得把 whole-file full-width counter 规则套用到 stream segment nonce。 - 因
segment_index为u32,单 object 的 segment 数硬上界为2^32;v1 实际派生段数上限远低于此(见 scalability-constraints §6),二者 MUST 同时满足。
3.3.3 AAD binding(normative)
每个 segment 的 AEAD AAD MUST 绑定以下 canonical 形态,使任一 segment 密文不能被挪用到其它 object、其它位置或另一末段判定:
scheme = "ak.blob.stream_aead.v1"key_ref(canonical 形态,见 §3)content_key_salt(canonical 形态,见 §3.0)nonce_prefix(本 object 的随机前缀)segment_indexlast_segment_flagsegment_count = N(从已认证的size_bytes与segment_bytes重算,只存在于 AAD,不是 envelope 字段)- §3.1 第 2 条要求的 envelope 绑定项:
media_type、size_bytes与任何 profile 声明的 content policy digest
segment_index、last_segment_flag 已进入 nonce,本节要求其同时进入 AAD,使重排、截断与末段伪造在 AEAD 层即被拒绝(tag 校验失败)。
上述字段全部在 AEAD authority commit 前确定,构成本节的 pre-encryption immutable header,其 canonical bytes 使用 RFC 8785 JCS。逐段 AAD MUST NOT 包含 blob_ref 或其内嵌 digest(§3.3.5 的整体 digest 覆盖每段的 tag,进入 AAD 会形成循环)、也 MUST NOT 包含任何其它 post-encryption 值。整体 commitment 的认证归属见 §3.1 第 2 条与 §3.3.5。
3.3.4 Content key 与 thumbnail
- 每个附件 object MUST 使用 §3.0 的 fresh per-object content key;content key MUST NOT 跨 object / 跨 transfer 复用,不能把 PrivateMessage 的 application ratchet 当附件 KDF。
- thumbnail 在分块形态下仍走 §4 / §5.3 的整文件形态:其独立 salt、key 与 nonce 按 §3.0/§3.1 产生,
purpose仍为blob-attachment。正文密文使用ak.blob.stream_aead.v1时,其缩略图附件 envelope 仍 SHOULD 使用ak.blob.whole_file_aead.v1。
3.3.5 blob_ref 内嵌 digest(分块形态语义,normative)
为保持与整文件形态一致的单值整体完整性语义:
blob_ref=ak:blob:<suite>:<hex>的<suite>:<hex>= 对全部 segment 密文(每段含其各自 AEAD tag)按 segment_index 升序拼接后的完整字节流,用 Realm digest suite(见../conformance/encoding.md§3.2)求得。- 拼接顺序 MUST 严格按 segment_index 升序,且覆盖恰好派生的
N段、不含其它字节。 - 语义分层:per-segment AEAD tag 提供增量校验(边下边验),content-addressed
blob_ref提供整体完整性(防止整体替换 / 段集合层面的攻击)。两者都 MUST 校验通过。 - 认证归属(normative):
blob_ref是 post-encryption commitment,MUST NOT 出现在任何 segment 的 AEAD AAD 中(本节 immutable header 规则)。它自身的真实性由引用该附件的已签名 Event / encrypted descriptor(例如ak.content.file/ak.content.long_text的 attachment、file-transfer record)或 upload receipt 承担;接收方 MUST 以外层已认证值为准,MUST NOT 采信仅由传输层提供的 digest。
3.3.6 解密 MUST(normative)
接收方解密 ak.blob.stream_aead.v1 附件时:
- 按序处理:MUST 按 segment_index 从
0起严格升序处理 segment;segment_index 跳变、乱序、出现空洞 MUST 拒绝(segment_sequence_invalid),并丢弃已缓冲明文。 - 逐段 AEAD 校验:每段 MUST 用 §3.3.2 的 nonce 与 §3.3.3 的 AAD 做 AEAD 解密;tag 校验失败 MUST 拒绝(
segment_aead_failed)。在第 N 段 AEAD 校验通过前,MUST NOT 释放第 N 段明文。 - 末段判定:MUST 仅在见到
last_segment_flag = 0x01的合法末段(且其segment_index == N - 1)并通过 AEAD 校验后,才认为附件完整。在见到合法末段前,接收方 MUST NOT 把附件视为已完整接收。 - 缺末段拒绝:流在未出现合法末段时即终止(连接中断,或派生的
N段已耗尽但末段 flag 仍为0x00)MUST 拒绝(segment_stream_truncated),并丢弃已释放/缓冲明文,不得把已得明文当作完整文件。 - 重复拒绝:同一 segment_index 出现多次 MUST 拒绝(
segment_replay)。 - 越界拒绝:
segment_index >= N、或非末段长度不等于segment_bytes、或末段长度超出1 .. segment_bytes(空明文 0 例外)MUST 拒绝(segment_bounds_invalid)。 - 整体 digest 校验:全部 segment 接收完毕后,MUST 按 §3.3.5 重算拼接密文 digest 并与
blob_ref内嵌值比对;不匹配 MUST 拒绝、丢弃全部明文、不得渲染或写入持久缓存(与 §5 digest mismatch 规则一致)。Range / 分段流式播放场景下允许在整体 digest 完成前消费已通过 per-segment 校验的明文段,但最终持久化或标记完整前 MUST 完成整体 digest 校验。
任一上述校验失败 MUST fail closed,按 §5 规则丢弃已下载字节、不得渲染、不得写入持久缓存,并 SHOULD 记录安全审计事件。
3.3.7 JSON 示例
{ "blob_ref": "ak:blob:sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "encrypted": true, "scheme": "ak.blob.stream_aead.v1", "key_ref": { "algorithm": "MLS", "group_state_ref": "ak:event:Af-qizSfVETcKiliXG093VVneO4nQF194ZXGkMWJijix" }, "content_key_salt": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8", "nonce_prefix": "base64url-N_AEAD-minus-5-bytes", "segment_bytes": 262144, "size_bytes": 3211264, "media_type": "video/mp4", "encryption_algorithm": "mls_exporter_aead_xchacha20poly1305_stream"}整文件形态(ak.blob.whole_file_aead.v1)的 envelope 仍按 §3 示例(携带单 nonce 而非 nonce_prefix / segment_*)。
4. Thumbnail
Thumbnail descriptor:
{ "source_blob_ref": "ak:blob:sha256:...", "thumbnail_blob_ref": "ak:blob:sha256:...", "width": 320, "height": 180, "media_type": "image/webp"}5. Authenticated Download
GET /_arkret/self/blob/get?blob_ref=<ref>请求字段:
| 字段 | 位置 | 类型 | 必填 | 说明与约束 |
|---|---|---|---|---|
blob_ref | query | string | required | 内容寻址 blob 引用。 |
Authorization | header | bearer token 或 device proof | 私有 blob required | 调用者认证。 |
Range | header | string | optional | Range 下载范围。 |
X-Arkret-Wait-For | header | cursor | optional | barrier cursor(purpose=barrier);服务端在 checkpoint 覆盖该 cursor 描述的 target event 前阻塞响应。 |
响应字段 / header:
| 字段 | 位置 | 类型 | 必填 | 说明与约束 |
|---|---|---|---|---|
| body | response | bytes | GET required | blob 字节内容。 |
Content-Length | header | int | optional | 可见时返回内容长度。 |
Digest | header | string | optional | 内容摘要。 |
Cache-Control | header | string | required | 缓存策略;私有内容必须保守。 |
Content-Type | header | string | 可见时 required | MIME 类型;不得泄露不可见资源。 |
Content-Disposition | header | string | 可见时 required | inline 或 attachment,并可携带安全清理后的 filename。 |
Content-Range | header | string | Range 响应 required | 实际返回字节范围。 |
Accept-Ranges | header | string | optional | 服务支持 Range 时 MAY 返回 bytes。 |
Location | header | url | redirect 时 required | 短期下载 URL 或对象存储 URL。 |
服务 MUST check:
- actor authorization
- Realm / Circle scope visibility
- retention / legal hold
- unsafe media policy
Retention 与 erasure 规则:
- Blob 删除 MUST 由 Realm policy、对象所有权、account lifecycle、retention expiry 或签名 compliance decision 授权。
- 处于 legal hold 的 blob MUST NOT 被物理删除;服务可以通过 redaction 或 policy 在普通视图中隐藏。
- Blob 被擦除后,服务 SHOULD 只保留最小 receipt:content-addressed
blob_ref(包含唯一 digest commitment)、策略允许时的 size class、erasure reason、执行服务 DID、执行时间和签名;不得再携同原像 sibling digest。 - 缩略图、preview、转码、搜索文本、embedding 和通知摘要等派生内容 MUST 在源 blob 或源 event 被 redacted / erased 后删除或重新最小化。
- E2EE 附件密钥销毁只能阻止后续访问,不能撤回已被授权接收方下载或解密的明文。
受保护内容默认必须走 authenticated download。公开 blob MAY 允许匿名读取,但私有 Realm、受控组织、E2EE 附件和任何带访问策略的媒体 MUST 要求认证。
下载请求 SHOULD 支持:
Authorization: DPoP <ak.session.grant>X-Arkret-Wait-For: <cursor>Range: bytes=<start>-<end>规则:
X-Arkret-Wait-For接受cursor.schema.json的purpose=barriercursor,用于避免客户端刚收到引用但 Blob 服务尚未完成授权物化。Blob 服务 SHOULD 等待本地授权 checkpoint 覆盖该 cursor 描述的 target event,超时返回revision_stale或temporarily_unavailable。- 下载授权 MUST 绑定 actor DID、device/session、Realm id、blob ref、purpose 和过期时间。服务端不得只凭 URL 随机串放行私有媒体。
- 受保护下载 MUST NOT 接受 query string 中的 session credential 或长期 capability。浏览器客户端应通过
Authorizationheader、service worker 代理或 device-bound proof 获取媒体。 - Blob 服务 MAY 返回短期 signed download URL 或
307/308redirect 到对象存储,但 redirect token MUST 短时效、单 blob、单 purpose、可撤销,并不得扩大可见性。 Location值不得被服务端或客户端长期缓存;未立即下载时 SHOULD 重新请求/_arkret/self/blob/get获取新的授权上下文。- 客户端跟随 redirect 后仍 MUST 重新计算内容 digest,并与 content-addressed
blob_ref内嵌值比对;若同时读取独立 Blob metadata,其content_digest也必须相等。 - 如果内容 hash、
Digestheader 或blob_ref不匹配,客户端 MUST 拒绝该响应、丢弃已下载字节、不得渲染、不得写入持久缓存,并 SHOULD 记录安全审计事件。服务端在上传、镜像或代理时发现 digest mismatch MUST 返回digest_mismatch,并不得生成可用 blob metadata。 - Range / HEAD download MUST 绑定同一授权上下文;服务端不得让 Range probe 或 HEAD response 泄露不可见 blob 的大小、MIME、文件名或存在性。
- 对采用
ak.blob.stream_aead.v1(§3.3)的加密附件,Range 下载的语义从”密文任意字节分片”升级为”可独立验证的明文分段”:客户端 SHOULD 按 segment 边界(segment_bytes的整数倍偏移)请求 Range,使每个取回的 segment 能立即用 per-segment AEAD tag 增量校验并安全释放对应明文,无需先下完整文件。客户端仍 MUST 按 §3.3.6 完成按序、末段与blob_ref内嵌整体 digest 校验后才认为附件完整;服务端对密文字节本身的 Range 语义不变(仍以Content-Range描述密文字节区间)。 - 对不可见 blob,服务端 SHOULD 返回与不存在资源一致的
not_found,并避免返回Content-Length、Content-Type、Content-Disposition、Accept-Ranges等可枚举 header。
5.1 Content-Type 与 Content-Disposition
下载或 HEAD 响应在资源对调用方可见时 MUST 返回 Content-Type。返回值 SHOULD 是上传 metadata 中的 media_type,但允许以下安全校正:
- 对
text/*添加 charset。 - 对未知或未提供 MIME 返回
application/octet-stream。 - 当服务端判定声明 MIME 明显错误或危险时,返回
application/octet-stream。
下载或缩略图响应在资源可见时 MUST 返回 Content-Disposition:
- 原始下载 MUST 使用
inline或attachment。 - 缩略图 SHOULD 使用
inline,并 MAY 使用服务生成文件名。 - 如果存在安全清理后的
filename,Content-DispositionSHOULD 携带它;否则不携带文件名。 - 服务端 SHOULD 仅对安全类型使用
inline,其他类型使用attachment。
建议允许 inline 的类型:
text/csstext/plaintext/csvapplication/jsonapplication/ld+jsonimage/jpegimage/gifimage/pngimage/apngimage/webpimage/avifvideo/mp4video/webmvideo/oggvideo/quicktimeaudio/mp4audio/webmaudio/aacaudio/mpegaudio/oggaudio/waveaudio/wavaudio/x-wavaudio/x-pn-wavaudio/flacaudio/x-flactext/html、text/javascript、image/svg+xml 和未知脚本型内容不得默认 inline。
5.2 缓存
私有或受控 blob response MUST 设置保守缓存头:
Cache-Control: private, no-store如果服务明确允许客户端缓存,MAY 使用 private, max-age=<n>,但 MUST 绑定用户 / device 授权,不得被共享代理缓存。
公开不可变 blob MAY 使用长缓存:
Cache-Control: public, immutable, max-age=31536000前提是内容地址包含强 hash,且 metadata 不泄露私有 Realm 信息。
5.3 缩略图与预览
缩略图、OCR 文本、转码副本、媒体探测 metadata 都是派生内容:
- E2EE 附件的缩略图 SHOULD 由客户端生成并加密上传,或只在本地生成。
- 服务端生成私有明文缩略图前,该服务 MUST 列入
plaintext_visible_services,且data_classes[]覆盖thumbnail/attachment_preview。 - 预览 URL、尺寸、MIME、文件名和 unsafe 标记都必须服从 Realm policy 与 capability,不能绕过正文授权。
- 缩略图必须重新绑定源 blob、生成参数、生成服务 DID 和可见性;删除、撤回、保留策略或 legal hold 改变时,派生内容必须随源内容重新判定。
- 缩略图 descriptor MUST 至少绑定 content-addressed
source_blob_ref、thumbnail_blob_ref、width、height、media_type、generated_by?、visibility和derivation_profile;两项 ref 的内嵌 digest 分别是 exact stored bytes 的唯一承诺,descriptor 不携source_ciphertext_digest/thumbnail_ciphertext_digest。若源附件是 E2EE,缩略图必须使用独立 AEAD key / nonce context,密钥按 §3.0 的独立 salt 派生,AAD 使用 §3.1 的purpose="blob-attachment";两项 ref、尺寸和生成参数由外层已认证 thumbnail descriptor 绑定,thumbnail_blob_ref不进入 KDF/AAD;不得复用原附件正文 key+nonce,也不得把明文缩略图 hash 暴露给未获授权服务。 - producer SHOULD 使用
thumbnails[]数组表达上述绑定。Consumer 收到只含media-metadata.schema.json的preview_blob_ref、缺少thumbnails[]descriptor 的 metadata 时,必须按源 blob 的最严格可见性处理,不得因缺少 descriptor 而放宽访问或缓存。
media-metadata.visibility 的标准取值是 public / realm_bound / actor_private / device_bound。realm_bound 表示访问受 owning Realm、Circle scope 与 capability 共同约束;它不是 Space 边界。actor_private 表示仅 issuing actor 的授权会话可通过 header auth 获取,MUST NOT 被转换为 bearer presign URL。presign 是 §5.4 定义的下载通道机制(发放短 TTL bearer URL),不是 visibility 维度上的取值;blob 的 visibility 仍按上述四值之一判定,是否允许 presign 由 §5.4.4.1 的 fail-closed 规则按 visibility 与 Realm policy 决定(例如 actor_private MUST NOT 走 presign)。presigned 不是合法 visibility 枚举值。
5.4 Pre-Signed URL(浏览器原生标签认证例外)
§5 与 server-threat-model.md §2.1 #21 URL 凭证泄露 规定受保护下载 MUST NOT 接受 query string 中的认证材料。该规则的存在原因是 query string 会被 HTTP access log、代理、CDN、浏览器历史、复制链接和 Referer 头无差别记录,长期 capability 一旦落入 URL 即等价于失控。
然而浏览器原生媒体标签(<img src>、<video src>、<audio src>、<link href>、CSS background-image: url(...) 等)无法附加 Authorization header——浏览器在解析这些属性时直接对目标 URL 发起未带认证 header 的 GET。fetch() API 本身可以附加 Authorization,但其返回的 Response 仅能转换为 Blob URL 后由 JS 注入 DOM 才能让原生标签消费,无法直接代替原生 src 的同源加载语义。若严格执行”无 URL 认证”规则,受保护媒体只能通过 service worker 代理或 JS Blob URL 间接渲染——这在很多原生体验、邮件预览、跨页面共享场景中是死路。
为此 v1 定义 ak.self.blob.command.presign.v1 作为该规则的狭窄定制例外:发出短 TTL、单对象、只读、可撤销的 pre-signed URL,让浏览器原生标签直接使用,同时通过严格 access_scope 限制把 URL 泄露的最大损失收敛在一个具体 blob 的短时间访问。
5.4.1 流程
1. 客户端 → POST /_arkret/self/blob/presign body: { blob_ref, max_age_seconds?, purpose? } auth: Authorization (standard bearer / service signature)
2. 服务端 (ak.self.blob.command.presign.v1 capability 通过后): - 生成 presign envelope (见 §5.4.2) - 用 blob service DID 签名 - 返回 url、expires_at
3. 浏览器 / 客户端: <img src="<url with embedded presign=...>" /> → GET /_arkret/self/blob/get?blob_ref=...&presign=... → 服务端验证 presign envelope 后吐 bytes5.4.2 Presign envelope wire 形态
presign query 参数的值是 base64url 编码的 detached-JWS envelope,覆盖 canonical JSON:
{ "scheme": "ak.blob.presign.v1", "blob_ref": "ak:blob:sha256:0123456789abcdef...", "realm_id": "ak:realm:Ac1aCK8aQdnkYImvdH3DFjq4jDCP198pXYWCGzGuVyj5", "issuer_id": "ak:did_core:webvh:z9L9sKcFqigzdgN2ucF1V6ztq", "issued_at": "2026-05-18T10:00:00Z", "expires_at": "2026-05-18T10:05:00Z", "purpose": "media_inline", "audience_hint": "did:webvh:z2dmj...:alice.example", "nonce": "base64url:random_16_bytes", "access_scope": { "method": ["GET", "HEAD"], "byte_range": null }}字段约束:
| 字段 | 必填 | 说明 |
|---|---|---|
scheme | yes | 固定 ak.blob.presign.v1;未来版本 MUST 用新 scheme id(不接受 in-space 升级) |
blob_ref | yes | 单一 blob 引用;与请求 ?blob_ref= 必须完全匹配 |
realm_id | conditional | 该 presign 授权的 Realm。普通 Realm-owned blob MUST 设置,且必须与 blob metadata 的 realm_id、签发时 capability scope 和响应时可见性检查一致。仅 deployment policy 明确声明的 public/global blob MAY 省略。 |
issuer_id | yes | 签发该 presign 的 blob service DID;MUST 是被部署 trust 的 service DID |
issued_at / expires_at | yes | TTL 硬上限 1h;deployment SHOULD 默认 ≤ 5 min |
purpose | yes | media_inline / thumbnail / download;服务端按 purpose 决定 Content-Disposition、限流强度等 |
audience_hint | optional | 期望使用者 DID(仅诊断 hint,不构成访问控制;浏览器原生标签无法证明调用者身份,详见 §5.4.4.1)。(注意:audience_hint 仅为路由/下载体验提示,不构成授权,也不是密码学意义上的 audience 绑定。) |
nonce | yes | 16+ bytes 随机;作为 presign envelope id、audit key 与撤销列表 key。除非显式声明 single-use profile,服务端不得因同一 nonce 被浏览器重复 GET / HEAD / Range 拉取而拒绝正常媒体加载。 |
access_scope.method | yes | 仅允许 GET / HEAD 中的子集;MUST NOT 包含写方法 |
access_scope.byte_range | optional | 可限制可访问字节区间(如 [0, 65536] 仅头部) |
5.4.3 接收方校验
GET /_arkret/self/blob/get?blob_ref=X&presign=<envelope> 处理时:
-
互斥检查:
Authorizationheader 与?presign=同时出现 MUST 拒绝param_invalid,避免混合 auth 模式 -
签名校验:用 envelope 内
issuer_id当前 verification method 验证签名 -
scheme 校验:仅识别注册 scheme id(v1 =
ak.blob.presign.v1);未知 scheme MUST 拒绝 -
blob_ref 一致性:envelope
blob_ref与 queryblob_ref必须完全相同 -
Realm 绑定校验:若 blob metadata 有
realm_id,enveloperealm_idMUST 存在且完全相同;若 envelope 省略realm_id,该 blob MUST 是 deployment public/global 白名单中显式登记的 blob。服务端 MUST NOT 仅因 blob metadata 缺realm_id即推断其为 public/global——“缺失realm_id”与”已授权公开”必须解耦:未显式登记在白名单的 realm-less blob(例如因上传 bug 漏设realm_id的 Realm-owned blob)MUST fail closed(not_found),不得为其签发无 Realm 绑定的 bearer URL。为 Realm A 签发的 presign 不能作为 Realm B 的授权使用。 -
method 校验:本次请求方法在 envelope
access_scope.method列表内 -
TTL 校验:接收方 MUST 按
contract-registry.json#protocol_time_tolerance_registry的ak.time_tolerance.blob_presign_ttl.v1校验issued_at与expires_at:未来issued_at和过去expires_at各自只获得该场景登记的短窗口宽限,边界包含在可接受区间内。实现 MUST NOT 换用 approval/temporal 场景的容差或方向,也 MUST NOT 自定义更宽容差使过期 presign 在无界时间内被接受。 -
nonce / 撤销校验:
nonce未在撤销列表内;普通media_inline/thumbnailpresign 是短 TTL 多次可用 bearer URL,服务端 MUST NOT 把浏览器正常的重复 GET / HEAD / Range 请求当作 replay 拒绝。只有 profile 显式声明 single-use presign 时,才可维护 consumed set。 -
撤销 / 状态实时回查(normative):服务端在每次 presign GET / HEAD / Range 响应阶段 MUST 同步回查该
blob_ref的当前 redaction / erasure / ban / legal-hold 状态,不得仅凭 envelope 自校验(签名 + TTL + scope)就放行。只要当前状态命中 redaction / erasure / ban / legal hold,即便 envelope 仍在 TTL 内且签名有效,也 MUST 拒绝(not_found,auditblob_redacted/legal_hold_active)。该回查 MUST 在响应 bytes 之前完成,以闭合”签发后被 redact 的内容在 TTL 窗口内仍被已泄露 URL 拉取”的竞态(见 §5.4.4 撤销索引保留下界与 §5.4.4.1 fail-closed 规则)。一致性边界(normative):本回查不要求每次都向远端撤销权威发起网络往返。参考实现 MAY 命中 §5.4.4 保留到
expires_at的本地撤销 / 失效索引做 O(1) 查询,因此本步骤的常态成本是本地命中而非每次远程查。允许的 staleness 上界按状态类别分级:- 普通 redaction / erasure / ban 命中:MAY 采用有界 staleness——本地撤销索引相对撤销权威的滞后 MUST ≤ 部署显式声明的
revocation_index_propagation_max_ms;该部署参数不是协议时钟容差,不得从ak.time_tolerance.blob_presign_ttl.v1或其它场景隐式继承。在该窗口内本地索引尚未收到的撤销可短暂未命中,但 presign TTL(≤ 1h)与撤销索引保留下界共同把暴露面收敛在有界窗口内。 - legal-hold / erasure(强一致 MUST):legal hold 与硬擦除命中 MUST 强一致——服务端 MUST NOT 用可能滞后的缓存放行:本地索引未能确认”无 legal hold / 未 erased”时 MUST fail closed(
not_found/legal_hold_active),不得在 staleness 窗口内放行受 legal hold 约束的内容。
实现不得以”性能”为由把 legal-hold / erasure 降级为有界 staleness,也不得对普通 redaction 引入无界 staleness 使已 redact 内容在 presign 整个 TTL 内持续可取。
- 普通 redaction / erasure / ban 命中:MAY 采用有界 staleness——本地撤销索引相对撤销权威的滞后 MUST ≤ 部署显式声明的
-
blob 状态与签发者校验:签发时服务端 MUST 已确认请求方有权为该
blob_refmint presign;响应时只能重新确认 envelope 绑定的 Realm / blob 仍一致、blob 未被 redacted / erased / banned / legal hold、issuer service DID 仍被部署信任。v1 bearer presign 无法在响应阶段证明当前请求者属于某个 audience。 -
Realm presign 资格实时重判(normative):服务端在响应阶段 MUST 重判该
blob_ref所属 Realm 当前是否已收紧为 routing-unlinkable(Realm asset policy 声明routing_unlinkability_required=true),并重判ak.realm.asset_privacy_policy(§6)是否仍显式声明direct_download_allowed=true。routing unlinkability 命中,或 asset policy 缺失、不可验证、未进入 effective policy bundle 的components、字段缺失、字段为 false 时,MUST 拒绝(not_found,auditrouting_unlinkability_presign_forbidden/direct_download_disallowed_presign_forbidden),不得仅凭签发时该 Realm 尚未收紧或曾允许 direct download 就放行。这闭合”签发时允许、签发后 policy 收紧/移除、已发 presign 仍在 TTL 内被拉取”的竞态,与 §5.4.4.1 签发侧的同名禁令构成两侧闭合。
任何校验失败 MUST 返回 not_found(不区分 envelope 无效 vs blob 不可见,避免暴露存在性);服务端 MAY 在 audit log 中记录具体 reason_code 如 presign_invalid / presign_expired / presign_scope_mismatch。
5.4.4 安全约束
MUST:
- TTL ≤ 1h(硬上限);deployment policy MAY 收紧到更短
- 单 blob,不接受通配
- 仅读(GET / HEAD),不接受 PUT / POST / DELETE
- envelope 不得包含可重用 credential(refresh token、session token、long-lived capability 等)
- envelope 由 blob service DID 签发,不能由 user device key 签发——这是 server-issued capability,不是 user delegation
- 已 revoked / redacted / erased blob 即使 envelope 仍有效 MUST 拒绝
- presign nonce 撤销 / 失效索引 MUST 至少保留到该 envelope 的
expires_at。普通media_inline/thumbnailpresign 在短 TTL 内多次可用(§5.4.3 步骤 8),其撤销条目若在expires_at前被清理,§5.4.4.1 对已签发但 TTL 未到的 presign 的即时拒绝(如 redacted blob)就会失效;因此撤销索引的保留下界 MUST 覆盖 envelope 的整个有效期。 - presign URL 不得通过普通 redirect / 反向代理透传到第三方 origin
MUST NOT:
- 用于 E2EE 附件 ciphertext fetch — E2EE 附件的
blob_ref+ decryption key 都 MUST NOT 出现在服务端可记录的 URL;E2EE 客户端坚持 header auth 路径,由 client-sidefetch()配合Authorization完成 - 用于
ak.self.blob.upload.create.v1、删除、mutation 或任何写/副作用操作;presign 只对 envelope 明确授权的ak.self.blob.resource.get.v1/ak.self.blob.resource.head.v1只读路径有效 - 由 user device 凭 capability 自签自用(必须经过
ak.self.blob.command.presign.v1operation 走一次服务端签发,进 audit log 与 capability check)
SHOULD:
- deployment 对 presign 签发频率限流(按 issuer service DID + 请求方 actor),防止滥用作为隐蔽 oracle
- audit log 记录每次签发(actor、blob_ref、purpose、TTL、issuer)以便事后追溯
- 客户端 SHOULD 优先用 header auth;仅在浏览器原生标签场景使用 presign
5.4.4.1 audience_hint 与 bearer-token 边界(normative)
audience_hint 是 诊断 hint,不是访问控制。原因:浏览器原生标签(<img> / <video>)在加载 presign URL 时不会附加任何调用者凭据;服务端无法在响应阶段验证当前请求确实来自 audience_hint 指向的 actor。任何把 audience_hint 当作 audience 强制的实现都会形成假阳性安全感。本节固定如下规则:
- 实现 MUST NOT 把
audience_hint当成访问控制 — 它只能进入 audit log 用于事后排查”presign 给谁发的”。 - presign 是一个短 TTL bearer URL:任何持有该 URL 的人在 TTL 内都可拉取
blob_ref对应字节。最大损失窗口由 (a) TTL、(b)access_scope.method/access_scope.byte_range、(c) revocation(redaction / erasure 触发即时拒绝)三者共同收敛。 nonce不提供普通媒体 presign 的单次消费语义。浏览器原生标签可能对同一 URL 执行HEAD+GET、Range、retry 或解码器重复拉取;v1media_inline/thumbnailpresign MUST 允许这些重复请求。需要单次下载时必须声明独立 profile(例如single_use=true或专用 purpose),且不得用于原生媒体标签。- 下列 blob 类别 MUST 走 fail-closed 规则,不得发 presign:
- E2EE ciphertext — 已经在 §5.4.4 MUST NOT 列出。E2EE 附件 fetch 走 client-side
fetch()+Authorizationheader 路径。 - legal hold blob — 处于 legal hold 状态的 blob MUST 拒绝
ak.self.blob.command.presign.v1(legal_hold_active),即便申请方持有ak.self.blob.command.presign.v1capability。原因:legal hold 要求 access 留痕可追溯,bearer URL 让第三方无凭据拉取破坏审计链。 - redacted blob —
ak.redaction已生效 /ak.audit.erasure_receipt已发布的 blob MUST 立即拒绝 presign 请求与已签发但 TTL 未到的 presign 请求(blob_redacted)。 - private attachment 私有附件(
visibility=actor_private或附ak.actor_privatepolicy 标签)— MUST NOT 走 presign 路径(private_attachment)。该类 blob 只允许 issuing actor 本人通过 header auth fetch。 - routing-unlinkable Realm-owned blob — blob metadata 绑定的 Realm 的 asset policy 声明
routing_unlinkability_required=true时,服务端 MUST NOT 签发 bearer presign URL(routing_unlinkability_presign_forbidden)。该类 Realm 的下载必须走 header auth、provider_proxy、ohttp_relay或等价的不把blob_ref/ bearer envelope 暴露到可转发 URL 的路径。只有 deployment-public/global blob(无 Realm 绑定,且 policy 明确允许 public direct download)可继续使用 presign。 - 未显式允许 direct download 的 Realm-owned blob — blob metadata 绑定的 Realm 只有在当前 effective
ak.realm.asset_privacy_policy(§6)中显式声明direct_download_allowed=true时,服务端才 MAY 继续评估 bearer presign;policy 缺失、不可验证、未进入 effective policy bundle 的components、字段缺失或字段为 false,一律按direct_download_allowed=falsefail closed,MUST NOT 签发 bearer presign URL(direct_download_disallowed_presign_forbidden),即便该 Realm 未声明routing_unlinkability_required、即便申请方持有ak.self.blob.command.presign.v1capability。presign 产出的就是一个可转发的 bearer download URL;缺省放行会把成员限定读取权降格为“持链接即可读取”。该类 Realm 的媒体必须走 authenticated header fetch 或download_mode声明的provider_proxy/ohttp_relay/client_mirror路径。只有 effective policy 显式direct_download_allowed=true的 Realm-owned blob,以及 deployment-public/global 白名单中的 realm-less blob,才可继续评估 presign 的其余 gate。
- E2EE ciphertext — 已经在 §5.4.4 MUST NOT 列出。E2EE 附件 fetch 走 client-side
- 最小 conformance matrix(normative):实现 MUST 覆盖四个 case:(a) Realm-owned blob 无 asset policy → 拒绝;(b)
direct_download_allowed=false→ 拒绝;(c) policy 已进入 effective policy bundle 的components且显式为 true → 仅在本节其它 gate 全部通过时允许;(d) URL 签发后 policy 被移除、失去 effective 引用或改为 false → 后续 GET / HEAD / Range 立即按步骤 11 拒绝。测试不得把 deployment-public/global realm-less 白名单分支当作 Realm-owned blob 的缺省回退。 - future audience-bound 机制(未来评估方向,不属于 v1):若未来需要真正绑定 audience,方案有 (a) 把 presign 升级为 cookie-bound URL(依赖
__Host-cookie + SameSite=Strict + presign 校验 cookie binding),(b) 通过 session-bound token 把 presign 换给 client 后只在该 session 内可用。两条都需要客户端配合,不属于 v1 范围。
5.4.4.2 Bearer URL 泄漏面控制(normative)
为限制 bearer URL 泄漏后的最大损失:
- 服务端 MUST 在响应 header 中加:
Cache-Control: private, no-store, max-age=0Referrer-Policy: no-referrerX-Content-Type-Options: nosniff
- 服务端 MUST NOT 在 access log / metrics / tracing 中记录
?presign=query 参数原文;记录 audit log 时 SHOULD 把 envelope hash 而不是原文写入。 - 服务端 MUST 限制 presign URL 在反向代理 / CDN 层不被缓存(
Cache-Control: private不够时还要 setVary: Authorization或显式 surrogate-control)。 - 客户端 MUST NOT 把 presign URL 持久化到 app state、route state、analytics、crash report、local storage、普通日志或浏览器历史中;SHOULD 仅在最终消费节点(
<img src=...>注入或 fetch)处构造 presign URL。
5.4.5 与 capability 的衔接
ak.self.blob.command.presign.v1 capability action(risk_tier=medium)控制谁可以为某 blob 签发 presign。grant 上的两个 constraint 收紧使用范围:
blob_presign_max_ttl_seconds:grant 允许的最大 TTL 上限(硬上限不超过 3600)blob_presign_scope:grant 允许的 purpose 集合(media_inline/thumbnail/download)与可选 blob_ref pattern(按 Realm / purpose 细分)
服务端在 ak.self.blob.command.presign.v1 调用时按 grant constraint 收窄请求的 max_age_seconds / purpose;超过 constraint 返回 capability_denied。
5.4.6 与 §5.2 / §5.3 的关系
- §5 仍是 认证下载默认路径;pre-signed 仅作为 §5 的 narrow 例外
- §5.2 redirect /
Location头的”短期 signed download URL”语义可以由ak.self.blob.command.presign.v1实现,但 redirect URL MUST 同样满足 §5.4 全部约束 - §5.3 缩略图通常通过
purpose=thumbnailpresign 让<img>直接渲染;服务端 SHOULD 设更短 TTL(默认 ≤ 60s)
6. Asset Privacy Policy
私有附件下载本身会暴露元数据,例如调用方 IP、在线时间、服务域名关系、blob 大小和下载频率。Realm SHOULD 使用 ak.realm.asset_privacy_policy 声明媒体上传、下载和代理隐私要求;无论 Realm 的 discoverability、join rule、history visibility 或 encryption profile 如何,Realm-owned blob 的 bearer presign 资格都采用 fail-closed 缺省:只有 effective policy 显式 direct_download_allowed=true 才可签发,缺失 policy 不等于允许。
{ "kind": "ak.realm.asset_privacy_policy", "payload": { "value": { "download_mode": "provider_proxy", "allowed_modes": [ "provider_proxy", "ohttp_relay" ], "direct_download_allowed": false, "upload_services": [ "ak:did_core:webvh:z9L9sKcFqigzdgN2ucF1V6ztq" ], "download_proxy_services": [ "ak:did_core:webvh:zGKqLZm5euMAfKUjGMsFkXR9E" ], "ohttp_gateway_services": [ "ak:did_core:webvh:zAQ5daUZibU8q3K1aaxD9vMRg" ], "max_plaintext_metadata": [ "size_bucket", "media_type_family" ], "client_digest_check_required": true } }}该 Event 使用 closed realm_asset_privacy_policy_payload;策略字段只能位于 whole-value payload.value,未知字段以 schema_violation 拒绝。
download_mode 取值:
| 值 | 含义 |
|---|---|
direct | 客户端直接从 Blob / 对象存储下载。只适合公开内容、同一信任域或明确接受 IP 暴露的 Realm。 |
provider_proxy | 通过调用方或 Realm policy 指定的受信 media proxy 下载,隐藏源 Blob 服务或对象存储细节。 |
ohttp_relay | 通过 OHTTP 或等价 oblivious relay 取回内容,降低 Blob 服务同时观察调用方身份和目标 blob 的能力。 |
client_mirror | 客户端从多个 authorized mirror 选择,按内容 hash 验证,适合高可用或隔离网络。 |
规则:
- 私有 Realm、E2EE 附件和高隐私 Realm 默认 SHOULD 使用
provider_proxy或ohttp_relay,不得默认 direct download。对声明routing_unlinkability_required=true的 Realm-owned blob,§5.4.4.1 的 presign hard reject 优先于本节的download_mode=direct;deployment 不得用 direct download policy 绕过该 bearer URL 禁令。 direct_download_allowed的 presign 缺省值是 false:只有本 policy 的realm_asset_privacy_policy安全 typed current result 在当前已确认状态中已设置且字段逐字为 true,才允许继续评估 presign。policy 缺失、未确认、不可验证或字段省略都 MUST 按 false 处理;deployment-wide “允许 direct”不得覆盖 Realm-owned blob 的该缺省。direct_download_allowed=false时,客户端 MUST NOT 绕过代理直接访问Location或外部 URL;服务端也不得返回强制 direct 的 redirect。该约束同样禁止 bearer presign:服务端 MUST NOT 为direct_download_allowed=falseRealm-owned blob 签发ak.self.blob.command.presign.v1URL(§5.4.4.1direct_download_disallowed_presign_forbidden),因为 presign 就是一个可转发的 direct bearer URL。- Proxy 服务不因参与下载而获得正文解密权。E2EE 附件必须保持密文,proxy 只能处理密文字节、size bucket、content hash 和授权 envelope。
max_plaintext_metadata控制服务可见 metadata。高隐私 Realm SHOULD 使用 bucketed size、MIME family,而不是精确文件名、精确字节数或完整 MIME。- 无论采用哪种下载路径,客户端 MUST 校验内容 hash、ciphertext digest 和 E2EE attachment metadata;proxy 成功不等于内容可信。
ak.realm.asset_privacy_policy由它自己的 Event kind 写入realm_asset_privacy_policytyped current result,不在ak.realm.policy_bundlepayload 内重复声明。该 metadata/下载策略由普通 Event/authority-commit/RealmCommit admission 保护;它本身不改变 MLS key 持有人,必须排除在key_access_revision外。
7. Safety
Blob service SHOULD:
- validate declared
size_bytes - compute digest server-side
- reject digest mismatch
- store MIME metadata as untrusted
- support malware scanning metadata
- support unsafe flag
- support GC grace period
8. Lifecycle
Blob MAY be GC’ed if:
- no live object references it
- grace period elapsed
- not under legal hold
- policy permits deletion