私有状态令牌 API

社区组草案报告

此版本:
https://wicg.github.io/trust-token-api/
问题追踪:
GitHub
编辑:
谷歌
谷歌
参与:
GitHub WICG/trust-token-api新建议题开放议题
提交记录:
GitHub spec.bs 提交记录

摘要

私有状态令牌 API 是一个 Web 平台 API,允许基于 Privacy Pass 协议这一底层机制,在不同站点间传递有限数量的信号。

本文档状态

本规范由 Web 平台孵化社区组 发布。 它不是 W3C 标准,也不在 W3C 标准制定路径上。 请注意,根据 W3C 社区贡献者许可协议(CLA),有一定的选择退出权利和其他条件适用。 了解更多关于 W3C 社区和商务组 的信息。

1. 目标

私有状态令牌的目标是在保护隐私的前提下,在站点之间随时间传递有限的信号。它通过采用 IETF Privacy Pass 工作组工作文档中规定的 privacy pass 协议变体 [PRIVACY-PASS-ISSUANCE-PROTOCOL] [PRIVACY-PASS-WG] 实现。私有状态令牌可被认为是 Privacy Pass 协议变体的 Web 平台实现。

本规范在请求字典中引入了一个新字段,以支持令牌操作。文档描述了如何通过这个新字典使用私有状态令牌。

2. 背景

私有状态令牌 API 提供了一种匿名认证机制。用户代理所提供的 API 并不用于直接认证客户端,而是用于传递认证信息。

客户端的认证与令牌签名都由同一个实体完成,这个实体被称为 发行者(issuer)。这就是 [PRIVACY-PASS-ARCHITECTURE], [PRIVACY-PASS-AUTH-SCHEME] 所描述的“联合证明者和发行者”架构。

用户代理会将令牌存储在持久化存储中。被访问的起源可以在一方上下文下获取/消费令牌,或者包含第三方代码获取/消费令牌。消费令牌即“兑换”。

起源可要求用户代理从其指定的发行者处获取令牌。令牌可以在与获取源不同的起源进行兑换。

私有状态令牌 API 实现了无需使用可关联状态 Cookie [RFC6265] 的跨站匿名认证。Cookie 可以实现跨站认证,但无法保证匿名性。

Cookie 可存储大量信息。[RFC6265] 要求每个 Cookie 至少 4096 字节、每个域名最多 50 个 Cookie。这意味着单个起源可用 50 x 4096 x 2^8 个唯一标识。配合后端数据库,服务器可以为如此多的唯一用户/会话存储任意数据。

相比之下,私有状态令牌存储的数据极为有限。一个令牌仅能存储六个值(可理解为枚举类型的六种可能),也就是 2 到 3 位(二进制位)(4<6<8)。这远小于每个 Cookie 可存储的 4096 字节。

此外,私有状态令牌 API 使用了加密协议,防止起源追踪它们发放给哪些用户哪些令牌。当发行者收到令牌时可以确认它们曾经发放过,但无法将令牌与其发放时的上下文关联。Cookie 并不具备这个属性。

与 Cookie 不同,存储同一发行者的多个令牌不会因为令牌不可关联性影响用户隐私。私有状态令牌 API 在顶级起源下最多允许 2 个不同发行者,这样当发行者协作时能限制为用户存储的信息量。

私有状态令牌相关操作依赖 [FETCH]。可专门构造一个与特定私有状态令牌操作对应的 fetch 请求,并作为参数传递给 fetch 函数。

3. 发行者公钥

本节描述发行者需要支持的用于私有状态令牌协议的公钥相关公共接口。

发行者需管理一组密钥,并实现用于签发和验证令牌的 IssueRedeem 加密函数。发行者需提供一个 密钥承诺(key commitment)端点。密钥承诺是执行发行和兑换操作所需加密密钥及相关元数据的集合。发行者应通过安全的 HTTP [RFC8446] 端点进行提供。用户代理应定期获取密钥承诺。 密钥承诺 是一个映射,用于表示执行发行和兑换操作所需的加密密钥和相关元数据的集合。

对密钥承诺端点的请求应返回如下格式、媒体类型为 "application/pst-issuer-directory" 的 JSON 响应 [RFC8259]

{
  <cryptographic protocol_version>: {
    "protocol_version": <cryptographic protocol version>,
    "id": <key commitment identifier>
    "batchsize": <batch size>,
    "keys": {
      <keyID>: { "Y": <base64-encoded public key>,
                 "expiry": <key expiration date>},
      <keyID>: { "Y": <base64-encoded public key>,
                 "expiry": <key expiration date>}, ...
    }
  },
  ...
}

所有字段名及其值均为字符串。为某发行者拉取新的密钥承诺后,会丢弃之前的承诺。

3.1. 发行者密钥获取/注册

为保护本API隐私并避免用户专用密钥,发行者应为所有发放与兑换令牌的客户端提供相同的密钥。

为确保此特性,建议用户代理采用与用户无关的方式拉取密钥承诺,比如通过代理或中心化方案,获取密钥后再分发给各个客户端。

如果采用中心化机制获取密钥,用户代理应有注册流程,以允许发行者注册,定期获取并将其密钥承诺发送给客户端。注册的需求和机制是实现自定

采用注册流程时,建议用户代理为注册请求设置失效日期,便于移除废弃或停用发行者。

4. VOPRF 方法

本文件将协议消息编码为 TLS 表示语言,见 [RFC8446] 第 3 节。

序列化协议消息反序列化协议消息,协议消息按 [RFC8446] 第 3 节描述进行编码和解释。

对于私有状态令牌,VOPRF 协议初始化时曲线(下文函数中的 G)选用 P-384nonce_size 定义为 64。

注意: 当前 PST 版本输入/输出使用 X9.62 非压缩点表示,是与 [VOPRF] 规范的历史分歧。nonce_size 的选择也与 [BatchedTokens] 的当前草案不一致。

服务器发起发行时,执行 BlindEvaluateBatch 函数(协议来自 [BatchedTokens]),输入 IssueRequest 与输出 IssueResponse 的序列化格式如下:

// Scalars 为长度 Ns(由曲线定,P-384 为 48)的椭圆曲线标量。
// ECPoints 为用 X9.62 非压缩点表示编码的椭圆曲线点(P-384 为 97)。

struct {
  uint16 count;
  ECPoint nonces[count]; // 与 blindedElements 对应
} IssueRequest;

struct {
  ECPoint evaluated; // 对应 evaluatedElements
} SignedNonce;

struct {
  Scalar c;
  Scalar s;
} DLEQProof;

struct {
  uint16 issued;
  uint32 key_id;
  SignedNonce signed[issued];
  opaque proof<1..2^16-1>; // 保存序列化 DLEQProof 结构体的字节串。
} IssueResponse;

服务器兑换时,对 Token 执行 PSTEvaluate,输入 RedeemRequest 和输出 RedeemResponse 的序列化如下:

struct {
  uint32 key_id;
  opaque nonce[nonce_size];
  ECPoint W;
} Token;

struct {
  opaque token<1..2^16-1>; // 包含序列化 Token 结构体的字节串。
  opaque client_data<1..2^16-1>;
} RedeemRequest;

struct {
  opaque rr<1..2^16-1>;
} RedeemResponse;

私有状态令牌定义了 PSTFinalize,它是 FinalizeBatch (协议见 [BatchedTokens])的变体:

def PSTFinalize(input, blinds, evaluatedElements,
               blindedElements, pkS, proof):
  if VerifyProof(G.Generator(), pkS, blindedElements,
                 evaluatedElements, proof) == false:
    raise VerifyError

  // PST: 使用批量结构
  unblindedElements = []
  for index in range(evaluatedElements.length):
    N = G.ScalarInverse(blinds[index]) * evaluatedElements[index]
    unblindedElements.append(G.SerializeElement(N))

  // PST: 返回 unblindedElements 而不是 hash 输出
  return unblindedElements

私有状态令牌定义了 PSTEvaluate,它是 Evaluate(协议见 [VOPRF])的变体:

// skS 由服务器用 key_id 查找对应私钥获得

def PSTEvaluate(skS, nonce, W, client_data):
  inputElement = G.HashToGroup(nonce)
  if inputElement == G.Identity():
    raise InvalidInputError
  evaluatedElement = skS * inputElement
  issuedElement = G.SerializeElement(evaluatedElement)

  // PST: 校验 issuedElement 而非 hash 输出
  if issuedElement != W:
    raise InvalidInputError

  // PST: 服务器可使用 client_data 等信息构造 redemptionRecord 返回给客户端
  return redemptionRecord

5. 算法

用户代理拥有 issuerAssociations,它是一个映射,以来源(origin) topLevel为键,值为列表,列表内容为来源(origin)

判断关联一个发行者是否会超出顶级限制,给定origin issuerorigin topLevel,执行以下步骤:

  1. 如果 issuerAssociations[topLevel] 不存在,返回 false。

  2. 如果 issuerAssociations[topLevel] 包含 issuer,返回 false。

  3. 如果 issuerAssociations[topLevel] 长度小于 2,返回 false。

  4. 返回 true。

关联发行者 issuer(一个origin)与origin topLevel,按如下步骤:

  1. 如果 issuerAssociations[topLevel] 不存在设置 issuerAssociations[topLevel] 为一个空列表

  2. issuer 添加到 issuerAssociations[topLevel]。

要判断是否已关联 origin issuer 与指定origin topLevel,按以下步骤:

  1. 如果 issuerAssociations[topLevel] 不存在,返回 false。

  2. 如果 issuerAssociations[topLevel] 包含 issuer,返回 true。

  3. 返回 false。

用户代理拥有 redemptionTimes,是一个映射,键为元组 (issuer, topLevel),值为元组 (lastRedemption, penultimateRedemption)。

记录兑换时间戳,给定origin issuerorigin topLevel,执行如下步骤:

  1. currentTime 为当前日期和时间。

  2. previousRedemption 为最早可表示的日期和时间。

  3. 如果 redemptionTimes[(issuer,topLevel)] 存在,则 previousRedemption 设为元组 redemptionTimes[(issuer,topLevel)] 的 lastRedemption 字段。

  4. 设置 redemptionTimes[(issuer,topLevel)] 为元组 (currentTime, previousRedemption)。

查找倒数第二次兑换,给定origin issuerorigin topLevel,执行如下步骤:

  1. penultimateRedemption 为最早可表示的日期和时间。

  2. 如果 redemptionTimes[(issuer,topLevel)] 存在,则设 penultimateRedemption 为元组 redemptionTimes[(issuer,topLevel)] 的 penultimateRedemption 字段。

  3. 返回 penultimateRedemption

用户代理拥有 redemptionRecords,是一个映射,键为元组 (issuer, topLevel),值为元组 (record, expiration, signingKeys)。

记录兑换记录,给定origin issuerorigin topLevel字节序列 header,以及持续时间 lifetime,按如下步骤:

  1. 如果 lifetime 为零,返回。

  2. currentTime 为自 1970 年 1 月 1 日 UTC 起当前日期和时间的毫秒数。

  3. expirationcurrentTimelifetime 之和。

  4. signingKeys查找最新密钥 issuer 的结果。

  5. 设置 redemptionRecords[(issuer, topLevel)] 为元组 (header, expiration, signingKeys)。

获取兑换记录,给定origin issuerorigin topLevel,按如下步骤:

  1. currentTime 为自 1970 年 1 月 1 日 UTC 起当前日期和时间的毫秒数。

  2. 如果 redemptionRecords[(issuer,topLevel)] 不存在,返回 null。

  3. 设 (record, expiration, signingKeys) 为 redemptionRecords[(issuer,topLevel)]。

  4. 如果 expiration 小于 currentTime,返回 null。

  5. currentSigningKeys查找最新密钥 issuer 的结果。

  6. 如果 currentSigningKeys 不等于 signingKeys,返回 null。

  7. 返回 record

用户代理拥有 pstKeyCommitments,是一个映射,键为origin,值为密钥承诺

注意: 建议每个用户代理定期通过可信基础设施从发行者获取密钥承诺,然后将发行者和密钥承诺组成的合并映射发送给客户端,以确保不同用户代理实例间所用密钥一致。

查找密钥承诺,给定origin issuer,按如下步骤:

  1. 如果 pstKeyCommitments[issuer] 不存在,返回 null。

  2. issuerKeyspstKeyCommitments[issuer]。

  3. 针对用户代理为此 API 支持的每个 cryptoProtocolVersion,按实现自定顺序,执行:

    1. issuerKeys[cryptoProtocolVersion] 存在,则返回之。

  4. 返回 null。

注意: cryptoProtocolVersion 是表示可与此API配合使用的不同令牌加密版本的字符串标识。用户代理应仅选择自身支持的密钥,并按性能和用户偏好决定优先顺序。

查找最新密钥,给定origin issuer,按以下步骤:

  1. commitment查找密钥承诺 issuer 的结果。

  2. 如果 commitment 为 null,返回 null。

  3. chosenKey 为 null。

  4. currentTime 为当前日期和时间。

  5. 遍历 commitment["keys"] 中的 key,按如下步骤:

    1. 如果 key["expiry"] 小于 currentTime,跳过。

    2. chosenKey 为 null,则设为 key

    3. 如果 key["expiry"] 小于 chosenKey["expiry"],则 chosenKey 变为 key

  6. 返回 chosenKey

用户代理拥有 tokenStore,是映射,键为origin,值为列表,其中内容为storedTokenstoredToken是一个元组 (字节序列字节序列)。

插入令牌,给定origin issuer字节序列 token,以及字节序列 signingKey,按以下步骤:

  1. 创建新的元组 storedToken,内容为 (token, signingKey)。

  2. 如果 tokenStore[issuer] 不存在,则 tokenStore[issuer] 设为空列表

  3. storedToken 添加至 tokenStore[issuer]。

获取令牌,给定origin issuer,执行如下步骤:

  1. 如果 tokenStore[issuer] 不存在,返回 null。

  2. 如果 长度 tokenStore[issuer] 为零,返回 null。

  3. 移除 tokenStore[issuer] 的随机一项并返回被移除项。

丢弃令牌,给定origin issuer 以及字节序列 signingKey,执行如下步骤:

  1. 如果 tokenStore[issuer] 不存在,返回。

  2. 移除 tokenStore[issuer] 中所有第二项不等于 signingKey 的元素。

获取令牌数量,从指定origin issuer,执行如下步骤:

  1. 如果 tokenStore[issuer] 不存在,返回 0。

  2. 返回 tokenStore[issuer] 的长度。

获取最大批量数量,给定origin issuer,执行如下操作:

  1. issuerKey查找密钥承诺 issuer 的结果。

  2. 如果 issuerKey 为 null,返回 0。

  3. 返回 issuerKey["batchsize"]。

生成掩码令牌,给定密钥承诺 issuerKeys 和数字 numTokens,执行如下步骤。返回 元组 (字节序列, 字节序列)。

  1. issueRequest 为一个空的IssueRequest

  2. issueRequest["count"] 设为 numTokens

  3. blinds 为一个空的字节序列

  4. 重复以下步骤 numTokens 次:

    1. input 为一个随机的字节序列

    2. 设 (blind, blindedElement) (元组 (字节序列, 字节序列))为在 Blind 函数([VOPRF] 协议)上运行 input 得到的结果,其中 blindedElement 按 X9.62 非压缩点编码。

    3. blind 添加至 blinds

    4. blindedElement 添加至 issueRequest["nonces"]。

  5. issueHeader序列化协议消息 issueRequest 的结果。

  6. 返回 元组 (issueHeader, blinds)。

去掩码令牌,给定密钥承诺 issuerKeys、字节串 blinds、以及字节序列 response,执行如下步骤,返回列表 (字节序列):

  1. input 为一个空的字节序列

  2. evaluatedElements 为一个空列表

  3. blindedElements 为一个空列表

  4. issueResponse反序列化协议消息 responseIssueResponse 的结果。

  5. 遍历 issueResponse["signed"] 中的 nonce

    1. nonce["blinded"] 添加至 blindedElements

    2. nonce["evaluated"] 添加至 evaluatedElements

  6. pkSissuerKeys["keys"][issueResponse["key_id"]]["Y"]。

  7. proofissueResponse["proof"]。

  8. blindsList 为一个空列表

  9. blinds长度>0 时:

    1. blindblinds 的前 N 个元素(N 为 X9.62 非压缩点长度),blinds 变为剩余部分。

    2. blind 添加至 blindsList

  10. result字节串列表)为在 PSTFinalize 上用 input, blindsList, evaluatedElements, blindedElements, pkS, proof 运行得到。

  11. 返回 result

根据 private token 为请求设置属性,给定 PrivateToken privateTokenrequest request,按如下步骤:

  1. requestprivate token operationprivateToken["operation"]。

  2. privateToken["operation"] 为 "token-request"

    1. Should request be allowed to use feature? 作用于 "private-state-token-issuance" 且 request 返回 false,则抛出 "NotAllowedError" DOMException

    2. 终止后续步骤。

  3. 断言:privateToken["operation"] 是 "token-redemption""send-redemption-record"

  4. Should request be allowed to use feature? 作用于 "private-state-token-redemption" 且 request 返回 false,则抛出 "NotAllowedError" DOMException

  5. privateToken["operation"] 等于 "token-redemption"

    1. requestprivate token refresh policy 设为 privateToken["refreshPolicy"]。

    2. 终止后续步骤。

  6. privateToken["issuers"] 不存在,则抛出 TypeError

  7. privateToken["issuers"] 为空,则抛出 TypeError

  8. 遍历 privateToken["issuers"] 中的 issuer

    1. issuerURLURL 解析器处理 issuer 的结果。

    2. issuerURL 为失败,则抛出 TypeError

    3. issuerURLscheme 不是HTTP(S) scheme,抛出 TypeError

    4. issuerOriginissuerURLorigin

    5. issuerOrigin 不是可能可信的来源,抛出 TypeError

    6. issuerURL 添加到 requestprivate token issuers

6. 与 Fetch 的集成

6.1. 定义

RefreshPolicy 附加在兑换请求上,用于确定兑换时返回的是之前返回的未过期兑换记录,还是一个新的兑换记录。

enum RefreshPolicy { "none", "refresh" };

TokenVersion 目前设置为 1,这是本规范当前唯一支持的版本。

enum TokenVersion { "1" };

OperationType 表示用户代理正在尝试完成的操作类型。

enum OperationType { "token-request", "send-redemption-record", "token-redemption" };

PrivateToken 包含发起 fetch 请求所需的信息。

dictionary PrivateToken {
  required TokenVersion version;
  required OperationType operation;
  RefreshPolicy refreshPolicy = "none";
  sequence<USVString> issuers;
};

本规范为 RequestInit 字典添加了一个新属性:

partial dictionary RequestInit {
  PrivateToken privateToken;
};

6.2. 对请求的补充说明

request 具有相关联的 private token refresh policy,类型为 RefreshPolicy, 默认值为 "none"

request 具有相关联的 private token operation,类型为 OperationType

request 具有相关联的 private token issuers,类型为字符串列表。

注意: private token refresh policy 仅在 private token operation"token-redemption" 时才生效。 private token issuers 仅在 private token operation"send-redemption-record" 时生效。当 private token operation"send-redemption-record" 时,private token issuers 必须指定且非空。

本规范定义了两个新受策略控制的特性。对于特定的私有状态令牌操作,始终仅有一个策略特性适用。

标识为 "private-state-token-issuance" 的 受策略控制特性适用于 "token-request" 操作。 该特性的 默认允许列表["self"]

标识为 "private-state-token-redemption" 的 受策略控制特性适用于 "send-redemption-record""token-redemption" 操作。该特性的 默认允许列表["self"]

request 具有相关联的 pstPretokens,类型为 null 或字节序列

new Request (input, init) 构造函数中,step 28 之前(即 "Set this's request to request") 增加如下步骤:

给定 RequestInit initRequest request ,执行如下步骤:

  1. 如果 init["privateToken"] 存在

    1. privateTokeninit["privateToken"]。

    2. privateTokenrequest 运行 set private token properties for request from private token

6.3. 对 http-network-or-cache fetch 的补充

本规范在 http-network-or-cache fetch 算法中,在修改 header list 之前增加如下步骤:

  1. 如果 requestprivate token operation 为 null,则终止后续步骤。

  2. 如果 requestprivate token operation"token-request"

    1. httpRequest 运行 append private state token issue request headers

    2. 终止后续步骤。

  3. 如果 requestprivate token operation"token-redemption"

    1. httpRequest 运行 append private state token redemption request headers

    2. 终止后续步骤。

  4. 断言requestprivate token operation"send-redemption-record"

  5. httpRequest 运行 append private state token redemption record headers

6.4. 对 HTTP fetch 步骤的补充

本规范在 HTTP fetch 算法中,在检查重定向状态之前(即“7. 如果 actualResponse 的状态是重定向状态,...”)增加如下步骤:

  1. issue response result 为对 handling an issue response 的执行结果,输入为 request requestresponse actualResponse

  2. 如果 issue response result网络错误,则返回 issue response result

  3. redeem response result 为对 handling a redeem response 的执行结果,输入为 request requestresponse actualResponse

  4. 如果 redeem response result网络错误,则返回 issue response result

7. 与 iframe 的集成

7.1. HTMLIframeElement 的 privateToken 内容属性

iframe 元素包含一个 privateToken 内容属性。IDL 属性 privateToken 反射 privateToken 内容属性

partial interface HTMLIFrameElement {
  [SecureContext] attribute DOMString privateToken;
};

通过 fetch 创建导航参数 步骤中的第 25 步“返回新的 navigation params,带 ...”之前,增加如下步骤:

  1. 如果 navigablecontaineriframe 元素,并且具有 privateToken 内容属性,则对 navigableprivateTokenrequest 调用 set private token properties for request from private token

8. 与 XMLHttpRequest 的集成

8.1. 附加 PrivateToken

XMLHttpRequest 具有一个相关联的 private state token, 一个 PrivateToken 对象,指定要在该请求上执行的 OperationType

partial interface XMLHttpRequest {
  undefined setPrivateToken(PrivateToken privateToken);
};

setPrivateToken(PrivateToken privateToken) 方法的步骤如下:

  1. 如果 thisstate 不是 "opened",则抛出 "InvalidStateError" DOMException

  2. 如果 thissend() 标志 已设置, 则 抛出 "InvalidStateError" DOMException
  3. thisprivate state token 设为 privateToken

8.2. send() monkeypatch

修改 send(body) 如下:

在如下步骤之后:

req 为一个新的 request,初始化如下...

增加如下步骤:

  1. thisprivate state tokenreq 运行 set private token properties for request from private token

9. 发行协议

本节说明发行协议。分为面向用户代理和发行者两部分分别说明协议步骤。

9.1. 创建发行请求

发行请求的创建与发起如下示例:
let issueRequest = new Request("https://example.issuer:1234/issuer_path", {
  privateToken: {
    version: 1,
    operation: "token-request",
  }
});
fetch(issueRequest);

添加私有状态令牌发行请求头,给定 request request,执行如下步骤:

  1. 如果 requestclient 不是 安全上下文,返回。

  2. issuerrequestURLorigin

  3. topLevelrequestclient顶级源

  4. 如果将 issuertopLevel 关联将超出顶级发行者数量限制,返回。

  5. 关联 issuertopLevel

  6. 如果 issuer令牌数量 至少为 500,返回。

  7. issuerKeys查找密钥承诺 issuer 的结果。

  8. 如果 issuerKeys 为 null,返回。

  9. signingKey查找最新密钥 issuer 的结果。

  10. issuersigningKey 运行 丢弃令牌

  11. numTokensissuer最大批量数量 或实现自定义的令牌数上限(建议为 100),取小者。

  12. 令 (issueHeader, pretokens) 为 生成掩码令牌后的结果,参数为 issuerKeysnumTokens

  13. request缓存模式 设为 "no-store"

  14. requestpstPretokens 设为 pretokens

  15. base64EncodedTokensissueHeader 的 base64 编码 [RFC4648]

  16. cryptoProtocolVersion 为所用的加密协议版本。

  17. request 的 header list 中 设置结构化字段值 (Sec-Private-State-Token, base64EncodedTokens)。

  18. request 的 header list 中 设置结构化字段值 (Sec-Private-State-Token-Crypto-Version, cryptoProtocolVersion)。

典型 fetch 所生成的私有状态令牌 HTTP 请求头如下所示:
Sec-Private-State-Token: <masked tokens encoded as base64 string>
Sec-Private-State-Token-Crypto-Version: <cryptographic protocol version, VOPRF>

9.2. 发行者签发令牌

本节说明在发行者服务器上对令牌的签名过程。VOPRF 只能通过选择不同密钥来编码六个值之一。

发行者使用其私钥,对在 Sec-Private-State-Token 请求头中获得的掩码令牌进行签名,签名的值取决于发行请求时提交的其他信息。发行者使用请求头 Sec-Private-State-Token-Crypto-Version 指定的加密协议。 发行者将签名后的令牌通过 Sec-Private-State-Token 响应头返回,值为 base64 [RFC4648] 字节字符串。

以下代码片段展示了带有 Private State Token 头的典型响应。
Sec-Private-State-Token: <token encoded as base64 string>

9.3. 处理发行响应

处理发行响应,给定 request requestresponse response,执行以下步骤:

  1. 如果 requestheader list包含 Sec-Private-State-Token,则返回 null。

  2. 如果 responseheader list包含 Sec-Private-State-Token,则返回网络错误

  3. header 为在 responseheader list获取 Sec-Private-State-Token 的结果。

  4. 如果 header 为空,则返回。

  5. 删除 Sec-Private-State-Token,从 responseheader list 中。

  6. issuerrequestURLorigin

  7. issuerKeys查找密钥承诺 issuer 的结果。

  8. 如果 issuerKeys 为 null,则返回。

  9. pretokensrequestpstPretokens

  10. 如果 pretokens 为 null,则返回。

  11. rawResponseheader 的 base64 解码结果 [RFC4648]

  12. unmasked tokens 为对 issuerKeyspretokensrawResponse 执行 去掩码响应令牌 的结果。

  13. 如果 unmasked tokens 为空,则返回网络错误

  14. signingKey查找最新密钥 issuer 的结果。

  15. 遍历 unmasked tokens 中的每个 token,执行:

    1. 插入令牌,参数为 issuertokensigningKey

  16. 返回。

10. 令牌兑换

当用户代理导航到顶级起源时,该顶级起源或嵌入在其中的第三方站点可以从指定发行者兑换用户代理存储的令牌,以获知令牌中编码的数据。

兑换通过 fetch 完成,方法如下示例。refreshPolicy 的默认值为 'none'
let redemptionRequest = new Request('https://example.issuer:1234/redemption_path', {
  privateToken: {
    version: 1,
    operation: 'token-redemption',
    refreshPolicy: {'none', 'refresh'}
  }
});

设置兑换头,参数 request requestRedeemRequest record

  1. redemptionRequest序列化协议消息 record 的结果。

  2. cryptoProtocolVersion 为所用加密协议的版本。

  3. token-lifetime兑换记录 的过期时间(秒)。

  4. 设置结构化字段值 (Sec-Private-State-Token, redemptionRequest) 到 request 的 header list。

  5. 设置结构化字段值 (Sec-Private-State-Token-Crypto-Version, cryptoProtocolVersion) 到 request 的 header list。

  6. 也可选地 设置结构化字段值 (Sec-Private-State-Token-Lifetime, token-lifetime) 到 request 的 header list。

  7. request缓存模式 设为 "no-store"

添加私有状态令牌兑换请求头,给定 request request,执行以下步骤:

  1. issuerrequestURLorigin

  2. topLevelrequestclient顶级源

  3. 如果 requestclient 不是安全上下文,则返回。

  4. 如果将 issuertopLevel 关联将超出顶级发行者数量限制,则返回。

  5. 关联 issuertopLevel

  6. 如果 requestprivate token refresh policy"none"

    1. record获取兑换记录,参数为 issuertopLevel

    2. 如果 record 非空,调用 设置兑换头,参数为 requestrecord,然后返回。

  7. penultimateRedemption 为调用 查找倒数第二次兑换,参数为 issuertopLevel 的结果。

  8. 如果 penultimateRedemption 早于实现自定时间段(建议为 48 小时),返回错误。

  9. commitments查找密钥承诺 issuer 的结果。

  10. 如果 commitments 为 null,则返回。

  11. 使用 丢弃令牌,移除 issuer 已签名密钥非最新承诺的令牌。

  12. token获取令牌 issuer 的结果。

  13. 如果 token 为空,则返回。

  14. redeemRequest 为一个空的 RedeemRequest

  15. redeemRequest["token"] 设为 token

  16. requestrecord 调用 设置兑换头

10.1. 处理兑换响应

处理兑换响应,给定 request requestresponse response,执行以下步骤:

  1. 如果 requestheader list包含 Sec-Private-State-Token,则返回 null。

  2. 如果 responseheader list包含 Sec-Private-State-Token,则返回网络错误

  3. rawHeaderresponseheader list获取 Sec-Private-State-Token 的结果。

  4. 如果 rawHeader 为空,则返回 null。

  5. rawResponserawHeader 的 base64 解码结果 [RFC4648]

  6. header反序列化协议消息 rawHeaderRedeemResponse 的结果。

  7. 删除 Sec-Private-State-Token,从 responseheader list 中。

  8. lifetime 设为最大可表示时长。

  9. 如果 responseheader list 包含 Sec-Private-State-Token-Lifetime 响应头,则将 lifetime 设为该值。

  10. 删除 Sec-Private-State-Token-Lifetimeresponseheader list 中。

  11. issuerrequestURLorigin

  12. topLevelrequestclient顶级源

  13. 使用 记录兑换时间戳,参数为 issuertopLevel

  14. 使用 记录兑换记录,参数为 issuertopLevelheaderlifetime

注意: 兑换记录 仅限 HTTP 读写,JavaScript 仅能通过 Private State Token Fetch API 访问/发送 兑换记录。兑换记录被视为发行者提供的任意二进制数据,具体含义由下游应用解释。

10.2. 兑换记录

为减少通信开销,用户代理可以缓存兑换响应 Sec-Private-State-Token 头中的字节块。这些块被称为兑换记录。用户代理可选择将这些记录加入后续对可验证其有效性的起源的请求。发行者可选择在兑换响应中包含可选的 Sec-Private-State-Token-Lifetime 头,该头值指明 兑换记录 的过期时间(以 HTTP 头 Sec-Private-State-Token-Lifetime 指定的秒数)。

兑换记录字节序列

私有状态令牌 API 提供 'send-redemption-record' 操作以添加私有状态令牌兑换记录头。该操作附加自 处理兑换响应记录 的兑换记录。

添加私有状态令牌兑换记录头,给定 request request,执行如下步骤:

  1. 如果 requestclient 不是安全上下文,终止本步骤。

  2. topLevelrequestclient顶级源

  3. 遍历 private token issuers,对每个 issuer 执行:

    1. issuerURLURL 解析器 处理 issuer 的结果。

    2. issuerURL 失败,则终止本步骤。

    3. issuerURLscheme 不是HTTP(S),终止本步骤。

    4. issuerOriginissuerURLorigin

    5. issuerOrigin 不是可信来源,终止本步骤。

  4. records_per_issuer 为一个映射,键为 USVString,值为兑换记录

  5. 遍历 private token issuers,对每个 issuer 执行:

    1. record获取兑换记录,参数为 issuertopLevel

    2. 如果 record 为空,继续

    3. 设置 records_per_issuer[issuer] = record

  6. records_per_issuer 为空,终止本步骤。

  7. headerItems 为结构化头部列表 [RFC8941]

  8. 遍历 records_per_issuer 映射 issuerrecord

    1. serializedIssuer 为序列化 issuer 后的结果。

    2. serializedRecord 为序列化 record 后的结果。

    3. 添加 serializedIssuerserializedRecord 对至 headerItems

  9. serializedHeaderItems 为序列化 headerItems 的结果。

  10. 如果 serializedHeaderItems 为空,终止本步骤。

  11. 设置 Sec-Redemption-Record 头值为 serializedHeaderItems

10.3. Document 的更改

partial interface Document {
  Promise<boolean> hasPrivateToken(USVString issuer);
  Promise<boolean> hasRedemptionRecord(USVString issuer);
};

11. 查询 API

11.1. 令牌查询

当在 Document doc 上使用 USVString issuer 调用 hasPrivateToken(issuer) 方法时,需按以下步骤执行:

  1. p一个新的 promise

  2. 如果 doc 不是完全激活,则拒绝 p,错误为 "InvalidStateError" DOMException,并返回 p

  3. globaldoc关联全局对象

  4. 如果 global 不是安全上下文,则拒绝 p,错误为 "NotAllowedError" DOMException,并返回 p

  5. parsedURL 为对 issuer 运行 URL 解析器 的结果。

  6. 如果 parsedURL 解析失败,则拒绝 p,错误为 "TypeError" DOMException,并返回 p

  7. originparsedURLorigin

  8. topLeveldoc关联 settings 对象顶级起源

  9. 并行运行以下步骤:

    1. 如果将 issuertopLevel 关联会超出顶级关联发行者数量限制,则插入一个全局任务networking task source(使用 global),使其拒绝 p,错误为 "NotAllowedError" DOMException,并返回。

    2. 关联发行者 origintopLevel

    3. 查找密钥承诺 origin。如果存在密钥承诺,则丢弃所有由非最新承诺密钥签名的 origin 令牌。

    4. 插入一个全局任务networking task source(使用 global),如果为该发行者存储了令牌,则 resolve p 为 true,否则为 false。

  10. 返回 p

注意: 该查询会修改用户代理状态,会将发行者参数与当前源关联。规范最多允许一个源关联 2 个发行者,这样避免通过发行者泄漏信息。注意,令牌查询会触发过期令牌的清理。

11.2. 兑换记录查询

当在 Document doc 上使用 USVString issuer 调用 hasRedemptionRecord(issuer) 方法时,需按以下步骤执行:

  1. p新的 promise

  2. 如果 doc 不是完全激活,则拒绝 p 并抛出 "InvalidStateError" DOMException 并返回 p

  3. globaldoc关联全局对象

  4. 如果 global 不是安全上下文,则拒绝 p 并抛出 "NotAllowedError" DOMException 并返回 p

  5. parsedURL 为对 issuer 执行 URL 解析器 的结果。

  6. 如果 parsedURL 解析失败,则拒绝 p 并抛出 "TypeError" DOMException 并返回 p

  7. originparsedURLorigin

  8. topLeveldoc关联设置对象顶级起源

  9. 并行执行以下步骤:

    1. 如果 origin 未与 topLevel 关联, 分派全局任务networking task source 目标为 globalresolve p 为 false 并返回。

    2. 查找密钥承诺 origin。如存在密钥承诺,丢弃所有由非最新承诺密钥签名的 origin 令牌。

    3. 分派全局任务networking task source 目标为 global, 如果存在发行者和顶级起源对的 兑换记录resolve p 为 true,否则为 false。

  10. 返回 p

注意: 类似于令牌查询,兑换记录查询也可能修改用户代理状态。但与令牌查询不同,兑换记录查询不会将发行者与顶级源关联。无需关联合发行者,因为兑换记录查询的答案不会泄漏已存令牌的发行者信息。和令牌查询一样,兑换查询也会清除过期令牌。

11.3. 清除 PST 数据

应遵循 storage 标准的用户界面指南。用户代理应提供界面从存储中清除 PST 数据。

12. 私有状态令牌 HTTP 头字段

12.1. 'Sec-Private-State-Token' 头字段

Sec-Private-State-Token 请求头字段在发行时发送一组未签名、经过掩码的令牌。在兑换时,发送单个已签名、去掩码的令牌和相关兑换元数据。

Sec-Private-State-Token 响应头字段发送一组已签名、掩码令牌。兑换时会发送新创建的已签名兑换记录

这是一个结构化头,其值必须为字符串 [RFC8941]

该头的 ABNF 格式:

Sec-Private-State-Token = sf-string

12.2. 'Sec-Private-State-Token-Lifetime' 头字段

Sec-Private-State-Token-Lifetime 响应头字段指明所附兑换记录的过期时间。单位为秒。

这是一个结构化头,其值必须为整数 [RFC8941]

该头的 ABNF 格式:

Sec-Private-State-Token-Lifetime = sf-integer

12.3. 'Sec-Private-State-Token-Crypto-Version' 头字段

Sec-Private-State-Token-Crypto-Version 头字段指明私有状态令牌的加密协议版本。

这是一个结构化头,其值必须为字符串 [RFC8941]

该头的 ABNF 格式:

Sec-Private-State-Token-Crypto-Version = sf-string

12.4. 'Sec-Redemption-Record' 头字段

Sec-Redemption-Record 请求头字段发送先前兑换操作缓存的兑换记录

这是一个结构化头,其值必须为字符串 [RFC8941]

该头的 ABNF 格式:

Sec-Redemption-Record = sf-string

13. 隐私注意事项

13.1. 不可关联性

加密协议 [VOPRF] 提供掩码签名。兑换时,发行者能识别自己签名的令牌,但无法确定签名时间或上下文。这防止发行者将某个来源上的发行与另一个来源上的兑换相关联。发行者只能获知用户访问起源的汇总信息。

13.2. 限制编码信息

用户代理应限制发行者任意时刻持有的唯一密钥数量,以保护客户端隐私。若无限制,发行者可通过为每个客户端分配唯一密钥实现去匿名。对于 [VOPRF],密钥数限制为 6 个。

发行者可用不同密钥表示不同的“标签”,对应于任意客户端状态,比如信任等级或其他反欺诈信号。发行者负责理解各标签及与令牌兑换方共享密钥的“标签”意义,以便解读每个令牌所代表的含义。这有助于降低恶意方反向推断能力,同时保护人可读标签下的隐私。使用 [VOPRF] 时,发行者 6 条密钥就可代表六个标签。

13.2.1. 潜在攻击:侧信道指纹识别

如果发行者可用网络级指纹等任何侧信道,在兑换时将 user agent 与发行时的 agent 关联,则不可关联性会丧失。即使私有状态令牌 API 只存储和暴露极少量信息。

13.3. 跨站信息传递

私有状态令牌仅在第一方上下文之间传递有限的信息。底层加密协议保障每个令牌只包含极少量数据。即便如此,如果在同一页面允许大量令牌兑换,用户 U 在域名 A 下的第一方 cookie 可能会被编码到私有状态令牌信息通道中并在域名 B 下解码,从而使域名 B 获知用户在域名 A 的 cookie,直到 1p cookie 被清除为止。除了关注通道被用作完全跨域通信,一些身份识别攻击(例如恶意兑换者试图获知发过令牌给某用户的所有发行者集合,这可能造成身份识别)也存在类似的缓解手段。

13.3.1. 缓解措施:动态发行/兑换限制

为缓解此类攻击,规范对发行与兑换均作出限制。发行期间,需要用户在发行站点激活。规范禁止在 实现自定 的时间窗口内进行第三次兑换,通常该窗口为 48 小时。

13.3.2. 缓解措施:每站点发行者数量限制

从一个起源向另一个起源泄漏身份的速率,会随着该起源允许的发行者数量增加而上升。为避免滥用,用户代理仅允许每个顶级起源最多关联 2 个发行者。对于令牌查询 API,也以同样方式将发行者与顶级起源相关联,详见 § 11.1 令牌查询

14. 安全注意事项

14.1. 防止令牌耗尽

恶意起源可能试图一次性兑尽用户代理中存储的所有令牌。为避免此种情况,规范限制了兑换操作的次数。在给定起源上下文中,初始只允许两次兑换,第三次兑换只能在距离第一次兑换已过去超过 实现自定的时间(通常为 48 小时)后才允许。

14.2. 防止发行者争抢名额

多个脚本可能争抢调用 hasPrivateToken(issuer),以在每个顶级起源只允许两个发行者的前提下抢占 issuerAssociations map 的名额,以便在其它脚本的 issuer 之前插入自己的 issuer。为防止此问题,顶级起源可在引入任何其它 JavaScript 之前优先调用 hasPrivateToken(issuer) 最多两次,确保自己偏好的 issuer 可用。

14.3. 防止重复消费

发行者可验证每个令牌只能被看到一次,因为每次兑换都发送给同一个发行者。这意味着即便恶意程序盗走用户的所有令牌,这些令牌最终也会被消耗光。为降低风险,发行者可每次签发更少的令牌。

15. IANA 考虑事项

本文档旨在定义 Sec-Private-State-TokenSec-Private-State-Token-LifetimeSec-Private-State-Token-Crypto-Version 这几个 HTTP 请求头字段,并将其注册到永久消息头字段注册表中([RFC9110])。

15.1. 'Sec-Private-State-Token' 头字段

头字段名: Sec-Private-State-Token

适用协议: http

状态: standard

作者/变更控制方: IETF

规范文档: 本规范 (§ 12.1 'Sec-Private-State-Token' 头字段)

15.2. 'Sec-Private-State-Token-Lifetime' 头字段

头字段名: Sec-Private-State-Token-Lifetime

适用协议: http

状态: standard

作者/变更控制方: IETF

规范文档: 本规范 (§ 12.2 'Sec-Private-State-Token-Lifetime' 头字段)

15.3. 'Sec-Private-State-Token-Crypto-Version' 头字段

头字段名: Sec-Private-State-Token-Crypto-Version

适用协议: http

状态: standard

作者/变更控制方: IETF

规范文档: 本规范 (§ 12.3 'Sec-Private-State-Token-Crypto-Version' 头字段)

15.4. 'Sec-Redemption-Record' 头字段

头字段名: Sec-Redemption-Record

适用协议: http

状态: standard

作者/变更控制方: IETF

规范文档: 本规范 (§ 12.4 'Sec-Redemption-Record' 头字段)

致谢

感谢 Alex Kallam、Charlie Harrison、Chris Fredrickson、David Van Cleve、Dylan Cutler、Eric Trouton、Johann Hofmann、Kaustubha Govind、Mike Taylor、Ryan Kalla 和 Sam Schlesinger 的贡献。感谢 Chris Wilson 审阅并指导本规范。

一致性

文档约定

一致性要求通过描述性断言和 RFC 2119 术语的组合表达。 在本规范的规范性部分中,关键字“MUST”(必须)、“MUST NOT”(禁止)、“REQUIRED”(要求)、“SHALL”(应当)、“SHALL NOT”(不应当)、“SHOULD”(应该)、“SHOULD NOT”(不应该)、“RECOMMENDED”(推荐)、“MAY”(可以)和“OPTIONAL”(可选)应按照 RFC 2119 的说明进行解释。 但为提高可读性,本规范中这些词不会全部大写。

本规范的全部文本均为规范性内容,明确标注为非规范性的章节、示例和注释除外。[RFC2119]

本规范中的示例以“for example”为引导,或通过 class="example" 与规范性文本区分,如下所示:

这是一个信息性示例。

信息性注释以“Note”开头,并通过 class="note" 与规范性文本区分,格式如下:

注意,这是一个信息性注释。

索引

本规范定义的术语

引用文献中定义的术语

参考文献

规范性引用

[BatchedTokens]
R. Robert; C. A. Wood. Batched Token Issuance Protocol. URL: https://www.ietf.org/archive/id/draft-robert-privacypass-batched-tokens-01.html
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[FETCH]
Anne van Kesteren. Fetch Standard. Living Standard. URL: https://fetch.spec.whatwg.org/
[HR-TIME-3]
Yoav Weiss. High Resolution Time. URL: https://w3c.github.io/hr-time/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[PERMISSIONS-POLICY-1]
Ian Clelland. Permissions Policy. URL: https://w3c.github.io/webappsec-permissions-policy/
[PRIVACY-PASS-ARCHITECTURE]
A. Davidson; J. Iyengar; C. A. Wood. Privacy Pass Architectural Framework. URL: https://www.ietf.org/archive/id/draft-ietf-privacypass-architecture-10.html
[PRIVACY-PASS-AUTH-SCHEME]
T. Pauly; S. Valdez; C. A. Wood. The Privacy Pass HTTP Authentication Scheme. URL: https://www.ietf.org/archive/id/draft-ietf-privacypass-auth-scheme-10.html
[PRIVACY-PASS-ISSUANCE-PROTOCOL]
S. Celi; et al. Privacy Pass Issuance Protocol. URL: https://www.ietf.org/archive/id/draft-ietf-privacypass-protocol-10.html
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[RFC4648]
. URL: https://www.rfc-editor.org/rfc/rfc4648
[RFC8259]
T. Bray, Ed.. The JavaScript Object Notation (JSON) Data Interchange Format. December 2017. Internet Standard. URL: https://www.rfc-editor.org/rfc/rfc8259
[RFC8446]
E. Rescorla. The Transport Layer Security (TLS) Protocol Version 1.3. August 2018. Proposed Standard. URL: https://www.rfc-editor.org/rfc/rfc8446
[RFC8536]
A. Olson; P. Eggert; K. Murchison. The Time Zone Information Format (TZif). February 2019. Proposed Standard. URL: https://www.rfc-editor.org/rfc/rfc8536
[RFC8941]
M. Nottingham; P-H. Kamp. Structured Field Values for HTTP. February 2021. Proposed Standard. URL: https://httpwg.org/specs/rfc8941.html
[RFC9110]
R. Fielding, Ed.; M. Nottingham, Ed.; J. Reschke, Ed.. HTTP Semantics. June 2022. Internet Standard. URL: https://httpwg.org/specs/rfc9110.html
[SECURE-CONTEXTS]
Mike West. Secure Contexts. URL: https://w3c.github.io/webappsec-secure-contexts/
[URL]
Anne van Kesteren. URL Standard. Living Standard. URL: https://url.spec.whatwg.org/
[VOPRF]
A. Davidson; et al. Oblivious Pseudorandom Functions (OPRFs) using Prime-Order Groups. URL: https://www.ietf.org/archive/id/draft-irtf-cfrg-voprf-21.html
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/
[XHR]
Anne van Kesteren. XMLHttpRequest Standard. Living Standard. URL: https://xhr.spec.whatwg.org/

补充性引用

[PRIVACY-PASS-WG]
. URL: https://datatracker.ietf.org/wg/privacypass/about/
[RFC6265]
A. Barth. HTTP State Management Mechanism. April 2011. Proposed Standard. URL: https://httpwg.org/specs/rfc6265.html

IDL 索引

enum RefreshPolicy { "none", "refresh" };

enum TokenVersion { "1" };

enum OperationType { "token-request", "send-redemption-record", "token-redemption" };

dictionary PrivateToken {
  required TokenVersion version;
  required OperationType operation;
  RefreshPolicy refreshPolicy = "none";
  sequence<USVString> issuers;
};

partial dictionary RequestInit {
  PrivateToken privateToken;
};

partial interface HTMLIFrameElement {
  [SecureContext] attribute DOMString privateToken;
};

partial interface XMLHttpRequest {
  undefined setPrivateToken(PrivateToken privateToken);
};

partial interface Document {
  Promise<boolean> hasPrivateToken(USVString issuer);
  Promise<boolean> hasRedemptionRecord(USVString issuer);
};