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. 发行者公钥
本节描述发行者需要支持的用于私有状态令牌协议的公钥相关公共接口。
发行者需管理一组密钥,并实现用于签发和验证令牌的 Issue 和 Redeem 加密函数。发行者需提供一个 密钥承诺(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- encodedpublic key> , "expiry" : < key expiration date> }, < keyID>: { "Y" : < base64- encodedpublic key> , "expiry" : < key expiration date> }, ... } }, ... }
-
<cryptographic protocol version>是所用私有状态令牌协议版本的字符串标识,与内部"protocol_version"字段的值一致。协议版本字符串标识为"PrivateStateTokenV1VOPRF"。-
协议版本
“PrivateStateTokenV1VOPRF”实现了 [VOPRF] 密码协议。发行者最多可使用六个有效的令牌签名密钥。
-
-
"id"字段为密钥承诺标识符,为非负整数,范围为无符号 32 位整数。该值应单调递增。 -
"batchsize"表示发行者支持的每次批量令牌签发的最大掩码令牌数。其值为正整数。用户代理每次操作通常会发送batchsize数量的令牌,也可能更少。 -
"keys"字段为公钥字典,按标识符索引。
所有字段名及其值均为字符串。为某发行者拉取新的密钥承诺后,会丢弃之前的承诺。
3.1. 发行者密钥获取/注册
为保护本API隐私并避免用户专用密钥,发行者应为所有发放与兑换令牌的客户端提供相同的密钥。
为确保此特性,建议用户代理采用与用户无关的方式拉取密钥承诺,比如通过代理或中心化方案,获取密钥后再分发给各个客户端。
如果采用中心化机制获取密钥,用户代理应有注册流程,以允许发行者注册,定期获取并将其密钥承诺发送给客户端。注册的需求和机制是实现自定。
采用注册流程时,建议用户代理为注册请求设置失效日期,便于移除废弃或停用发行者。
4. VOPRF 方法
本文件将协议消息编码为 TLS 表示语言,见 [RFC8446] 第 3 节。
要 序列化协议消息 和 反序列化协议消息,协议消息按 [RFC8446] 第 3 节描述进行编码和解释。
对于私有状态令牌,VOPRF 协议初始化时曲线(下文函数中的 G)选用 P-384,nonce_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 issuer 与origin topLevel,执行以下步骤:
-
如果 issuerAssociations[topLevel] 不存在,返回 false。
-
如果 issuerAssociations[topLevel] 包含 issuer,返回 false。
-
如果 issuerAssociations[topLevel] 长度小于 2,返回 false。
-
返回 true。
要关联发行者 issuer(一个origin)与origin topLevel,按如下步骤:
-
如果 issuerAssociations[topLevel] 不存在,设置 issuerAssociations[topLevel] 为一个空列表。
-
将 issuer 添加到 issuerAssociations[topLevel]。
要判断是否已关联 origin issuer 与指定origin topLevel,按以下步骤:
-
如果 issuerAssociations[topLevel] 不存在,返回 false。
-
如果 issuerAssociations[topLevel] 包含 issuer,返回 true。
-
返回 false。
用户代理拥有 redemptionTimes,是一个映射,键为元组 (issuer, topLevel),值为元组 (lastRedemption, penultimateRedemption)。
要记录兑换时间戳,给定origin issuer 和 origin topLevel,执行如下步骤:
-
设 currentTime 为当前日期和时间。
-
设 previousRedemption 为最早可表示的日期和时间。
-
如果 redemptionTimes[(issuer,topLevel)] 存在,则 previousRedemption 设为元组 redemptionTimes[(issuer,topLevel)] 的 lastRedemption 字段。
-
设置 redemptionTimes[(issuer,topLevel)] 为元组 (currentTime, previousRedemption)。
要查找倒数第二次兑换,给定origin issuer 和 origin topLevel,执行如下步骤:
-
设 penultimateRedemption 为最早可表示的日期和时间。
-
如果 redemptionTimes[(issuer,topLevel)] 存在,则设 penultimateRedemption 为元组 redemptionTimes[(issuer,topLevel)] 的 penultimateRedemption 字段。
-
返回 penultimateRedemption。
用户代理拥有 redemptionRecords,是一个映射,键为元组 (issuer, topLevel),值为元组 (record, expiration, signingKeys)。
要记录兑换记录,给定origin issuer,origin topLevel,字节序列 header,以及持续时间 lifetime,按如下步骤:
-
如果 lifetime 为零,返回。
-
设 currentTime 为自 1970 年 1 月 1 日 UTC 起当前日期和时间的毫秒数。
-
设 expiration 为 currentTime 与 lifetime 之和。
-
设 signingKeys 为查找最新密钥 issuer 的结果。
-
设置 redemptionRecords[(issuer, topLevel)] 为元组 (header, expiration, signingKeys)。
要获取兑换记录,给定origin issuer 和 origin topLevel,按如下步骤:
-
设 currentTime 为自 1970 年 1 月 1 日 UTC 起当前日期和时间的毫秒数。
-
如果 redemptionRecords[(issuer,topLevel)] 不存在,返回 null。
-
设 (record, expiration, signingKeys) 为 redemptionRecords[(issuer,topLevel)]。
-
如果 expiration 小于 currentTime,返回 null。
-
设 currentSigningKeys 为查找最新密钥 issuer 的结果。
-
如果 currentSigningKeys 不等于 signingKeys,返回 null。
-
返回 record。
用户代理拥有 pstKeyCommitments,是一个映射,键为origin,值为密钥承诺。
注意: 建议每个用户代理定期通过可信基础设施从发行者获取密钥承诺,然后将发行者和密钥承诺组成的合并映射发送给客户端,以确保不同用户代理实例间所用密钥一致。
要查找密钥承诺,给定origin issuer,按如下步骤:
-
如果 pstKeyCommitments[issuer] 不存在,返回 null。
-
设 issuerKeys 为 pstKeyCommitments[issuer]。
-
针对用户代理为此 API 支持的每个 cryptoProtocolVersion,按实现自定顺序,执行:
-
如 issuerKeys[cryptoProtocolVersion] 存在,则返回之。
-
-
返回 null。
注意: cryptoProtocolVersion 是表示可与此API配合使用的不同令牌加密版本的字符串标识。用户代理应仅选择自身支持的密钥,并按性能和用户偏好决定优先顺序。
要查找最新密钥,给定origin issuer,按以下步骤:
-
设 commitment 为查找密钥承诺 issuer 的结果。
-
如果 commitment 为 null,返回 null。
-
设 chosenKey 为 null。
-
设 currentTime 为当前日期和时间。
-
遍历 commitment["keys"] 中的 key,按如下步骤:
-
如果 key["expiry"] 小于 currentTime,跳过。
-
若 chosenKey 为 null,则设为 key。
-
如果 key["expiry"] 小于 chosenKey["expiry"],则 chosenKey 变为 key。
-
-
返回 chosenKey。
用户代理拥有 tokenStore,是映射,键为origin,值为列表,其中内容为storedToken。storedToken是一个元组 (字节序列,字节序列)。
要插入令牌,给定origin issuer,字节序列 token,以及字节序列 signingKey,按以下步骤:
-
创建新的元组 storedToken,内容为 (token, signingKey)。
-
如果 tokenStore[issuer] 不存在,则 tokenStore[issuer] 设为空列表。
-
将 storedToken 添加至 tokenStore[issuer]。
要获取令牌,给定origin issuer,执行如下步骤:
-
如果 tokenStore[issuer] 不存在,返回 null。
-
如果 长度 tokenStore[issuer] 为零,返回 null。
-
移除 tokenStore[issuer] 的随机一项并返回被移除项。
要丢弃令牌,给定origin issuer 以及字节序列 signingKey,执行如下步骤:
-
如果 tokenStore[issuer] 不存在,返回。
-
移除 tokenStore[issuer] 中所有第二项不等于 signingKey 的元素。
要获取令牌数量,从指定origin issuer,执行如下步骤:
-
如果 tokenStore[issuer] 不存在,返回 0。
-
返回 tokenStore[issuer] 的长度。
要获取最大批量数量,给定origin issuer,执行如下操作:
-
设 issuerKey 为 查找密钥承诺 issuer 的结果。
-
如果 issuerKey 为 null,返回 0。
-
返回 issuerKey["batchsize"]。
要生成掩码令牌,给定密钥承诺 issuerKeys 和数字 numTokens,执行如下步骤。返回 元组 (字节序列, 字节序列)。
-
设 issueRequest 为一个空的IssueRequest。
-
将 issueRequest["count"] 设为 numTokens。
-
设 blinds 为一个空的字节序列。
-
重复以下步骤 numTokens 次:
-
设 issueHeader 为 序列化协议消息 issueRequest 的结果。
-
返回 元组 (issueHeader, blinds)。
要去掩码令牌,给定密钥承诺 issuerKeys、字节串 blinds、以及字节序列 response,执行如下步骤,返回列表 (字节序列):
-
设 input 为一个空的字节序列。
-
设 evaluatedElements 为一个空列表。
-
设 blindedElements 为一个空列表。
-
设 issueResponse 为反序列化协议消息 response 为 IssueResponse 的结果。
-
遍历 issueResponse["signed"] 中的 nonce:
-
设 pkS 为 issuerKeys["keys"][issueResponse["key_id"]]["Y"]。
-
设 proof 为 issueResponse["proof"]。
-
设 blindsList 为一个空列表。
-
当 blinds 的长度>0 时:
-
设 blind 为 blinds 的前 N 个元素(N 为 X9.62 非压缩点长度),blinds 变为剩余部分。
-
将 blind 添加至 blindsList。
-
-
设 result(字节串列表)为在 PSTFinalize 上用 input, blindsList, evaluatedElements, blindedElements, pkS, proof 运行得到。
-
返回 result。
要根据 private token 为请求设置属性,给定 PrivateToken
privateToken 和request request,按如下步骤:
-
设 request 的private token operation 为 privateToken["
operation"]。 -
若 privateToken["
operation"] 为"token-request":-
若Should request be allowed to use feature? 作用于 "
private-state-token-issuance" 且 request 返回false,则抛出 "NotAllowedError"DOMException。 -
终止后续步骤。
-
-
断言:privateToken["
operation"] 是"token-redemption"或"send-redemption-record"。 -
若Should request be allowed to use feature? 作用于 "
private-state-token-redemption" 且 request 返回false,则抛出 "NotAllowedError"DOMException。 -
若 privateToken["
operation"] 等于"token-redemption":-
将 request 的private token refresh policy 设为 privateToken["
refreshPolicy"]。 -
终止后续步骤。
-
-
遍历 privateToken["
issuers"] 中的 issuer:-
设 issuerURL 为 URL 解析器处理 issuer 的结果。
-
若 issuerURL 为失败,则抛出
TypeError。 -
若 issuerURL 的scheme 不是HTTP(S) scheme,抛出
TypeError。 -
设 issuerOrigin 为 issuerURL 的origin。
-
将 issuerURL 添加到 request 的private 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 = "none";refreshPolicy 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
init 和
Request
request
,执行如下步骤:
-
如果 init["
privateToken"] 存在:-
设 privateToken 为 init["
privateToken"]。 -
对 privateToken 和 request 运行 set private token properties for request from private token。
-
6.3. 对 http-network-or-cache fetch 的补充
本规范在 http-network-or-cache fetch 算法中,在修改 header list 之前增加如下步骤:
-
如果 request 的 private token operation 为 null,则终止后续步骤。
-
如果 request 的 private token operation 为
"token-request":-
对 httpRequest 运行 append private state token issue request headers。
-
终止后续步骤。
-
-
如果 request 的 private token operation 为
"token-redemption":-
对 httpRequest 运行 append private state token redemption request headers。
-
终止后续步骤。
-
-
断言: request 的 private token operation 为
"send-redemption-record"。 -
对 httpRequest 运行 append private state token redemption record headers。
6.4. 对 HTTP fetch 步骤的补充
本规范在 HTTP fetch 算法中,在检查重定向状态之前(即“7. 如果 actualResponse 的状态是重定向状态,...”)增加如下步骤:
-
令 issue response result 为对 handling an issue response 的执行结果,输入为 request request 和 response actualResponse。
-
如果 issue response result 是 网络错误,则返回 issue response result。
-
令 redeem response result 为对 handling a redeem response 的执行结果,输入为 request request 和 response actualResponse。
-
如果 redeem response result 是 网络错误,则返回 issue response result。
7. 与 iframe 的集成
7.1. HTMLIframeElement 的 privateToken 内容属性
iframe
元素包含一个 privateToken
内容属性。IDL 属性 privateToken
反射
privateToken
内容属性。
partial interface HTMLIFrameElement { [SecureContext ]attribute DOMString ; };privateToken
7.2. 对“通过 fetch 创建导航参数”步骤的补充
在 通过 fetch 创建导航参数 步骤中的第 25 步“返回新的 navigation params,带 ...”之前,增加如下步骤:
-
如果 navigable 的 container 是
iframe元素,并且具有privateToken内容属性,则对 navigable 的 privateToken 和 request 调用 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)
方法的步骤如下:
-
如果 this 的 state 不是 "opened",则抛出 "
InvalidStateError"DOMException。 - 如果 this 的
send()标志 已设置, 则 抛出 "InvalidStateError"DOMException。 - 将 this 的 private state token 设为 privateToken。
8.2. send() monkeypatch
修改send(body)
如下:
在如下步骤之后:
令 req 为一个新的 request,初始化如下...
增加如下步骤:
9. 发行协议
本节说明发行协议。分为面向用户代理和发行者两部分分别说明协议步骤。
9.1. 创建发行请求
let issueRequest= new Request( "https://example.issuer:1234/issuer_path" , { privateToken: { version: 1 , operation: "token-request" , } }); fetch( issueRequest);
要添加私有状态令牌发行请求头,给定 request request,执行如下步骤:
-
如果将 issuer 与 topLevel 关联将超出顶级发行者数量限制,返回。
-
关联 issuer 与 topLevel。
-
如果 issuer 的 令牌数量 至少为 500,返回。
-
令 issuerKeys 为 查找密钥承诺 issuer 的结果。
-
如果 issuerKeys 为 null,返回。
-
令 signingKey 为 查找最新密钥 issuer 的结果。
-
用 issuer 和 signingKey 运行 丢弃令牌。
-
令 numTokens 为 issuer 的 最大批量数量 或实现自定义的令牌数上限(建议为 100),取小者。
-
令 (issueHeader, pretokens) 为 生成掩码令牌后的结果,参数为 issuerKeys 和 numTokens。
-
将 request 的 缓存模式 设为
"no-store"。 -
将 request 的 pstPretokens 设为 pretokens。
-
令 base64EncodedTokens 为 issueHeader 的 base64 编码 [RFC4648]。
-
令 cryptoProtocolVersion 为所用的加密协议版本。
-
在 request 的 header list 中 设置结构化字段值 (
Sec-Private-State-Token, base64EncodedTokens)。 -
在 request 的 header list 中 设置结构化字段值 (
Sec-Private-State-Token-Crypto-Version, cryptoProtocolVersion)。
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] 字节字符串。
9.3. 处理发行响应
要处理发行响应,给定 request request 和 response response,执行以下步骤:
-
如果 request 的 header list 不包含 Sec-Private-State-Token,则返回 null。
-
如果 response 的 header list 不包含 Sec-Private-State-Token,则返回网络错误。
-
令 header 为在 response 的 header list 中 获取 Sec-Private-State-Token 的结果。
-
如果 header 为空,则返回。
-
删除 Sec-Private-State-Token,从 response 的 header list 中。
-
令 issuerKeys 为 查找密钥承诺 issuer 的结果。
-
如果 issuerKeys 为 null,则返回。
-
令 pretokens 为 request 的 pstPretokens。
-
如果 pretokens 为 null,则返回。
-
令 rawResponse 为 header 的 base64 解码结果 [RFC4648]。
-
令 unmasked tokens 为对 issuerKeys、pretokens 和 rawResponse 执行 去掩码响应令牌 的结果。
-
如果 unmasked tokens 为空,则返回网络错误。
-
令 signingKey 为 查找最新密钥 issuer 的结果。
-
遍历 unmasked tokens 中的每个 token,执行:
-
插入令牌,参数为 issuer、token 和 signingKey。
-
-
返回。
10. 令牌兑换
当用户代理导航到顶级起源时,该顶级起源或嵌入在其中的第三方站点可以从指定发行者兑换用户代理存储的令牌,以获知令牌中编码的数据。
'none'。
let redemptionRequest= new Request( 'https://example.issuer:1234/redemption_path' , { privateToken: { version: 1 , operation: 'token-redemption' , refreshPolicy: { 'none' , 'refresh' } } });
要设置兑换头,参数 request request 和 RedeemRequest record:
-
令 redemptionRequest 为 序列化协议消息 record 的结果。
-
令 cryptoProtocolVersion 为所用加密协议的版本。
-
令 token-lifetime 为 兑换记录 的过期时间(秒)。
-
设置结构化字段值 (
Sec-Private-State-Token, redemptionRequest) 到 request 的 header list。 -
设置结构化字段值 (
Sec-Private-State-Token-Crypto-Version, cryptoProtocolVersion) 到 request 的 header list。 -
也可选地 设置结构化字段值 (
Sec-Private-State-Token-Lifetime, token-lifetime) 到 request 的 header list。 -
将 request 的 缓存模式 设为
"no-store"。
要添加私有状态令牌兑换请求头,给定 request request,执行以下步骤:
-
如果将 issuer 与 topLevel 关联将超出顶级发行者数量限制,则返回。
-
关联 issuer 与 topLevel。
-
如果 request 的 private token refresh policy 为
"none": -
令 penultimateRedemption 为调用 查找倒数第二次兑换,参数为 issuer 和 topLevel 的结果。
-
如果 penultimateRedemption 早于实现自定时间段(建议为 48 小时),返回错误。
-
令 commitments 为 查找密钥承诺 issuer 的结果。
-
如果 commitments 为 null,则返回。
-
使用 丢弃令牌,移除 issuer 已签名密钥非最新承诺的令牌。
-
令 token 为 获取令牌 issuer 的结果。
-
如果 token 为空,则返回。
-
令 redeemRequest 为一个空的 RedeemRequest。
-
将 redeemRequest["token"] 设为 token。
-
对 request 和 record 调用 设置兑换头。
10.1. 处理兑换响应
要处理兑换响应,给定 request request 和 response response,执行以下步骤:
-
如果 request 的 header list 不包含 Sec-Private-State-Token,则返回 null。
-
如果 response 的 header list 不包含 Sec-Private-State-Token,则返回网络错误。
-
令 rawHeader 为 response 的 header list 中 获取 Sec-Private-State-Token 的结果。
-
如果 rawHeader 为空,则返回 null。
-
令 rawResponse 为 rawHeader 的 base64 解码结果 [RFC4648]。
-
令 header 为 反序列化协议消息 rawHeader 为 RedeemResponse 的结果。
-
删除 Sec-Private-State-Token,从 response 的 header list 中。
-
将 lifetime 设为最大可表示时长。
-
如果 response 的 header list 包含 Sec-Private-State-Token-Lifetime 响应头,则将 lifetime 设为该值。
-
删除 Sec-Private-State-Token-Lifetime 从 response 的 header list 中。
-
使用 记录兑换时间戳,参数为 issuer 和 topLevel。
-
使用 记录兑换记录,参数为 issuer、topLevel、header 和 lifetime。
注意: 兑换记录 仅限 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,执行如下步骤:
-
遍历 private token issuers,对每个 issuer 执行:
-
遍历 private token issuers,对每个 issuer 执行:
-
若 records_per_issuer 为空,终止本步骤。
-
令 headerItems 为结构化头部列表 [RFC8941]。
-
遍历 records_per_issuer 映射 issuer → record:
-
令 serializedIssuer 为序列化 issuer 后的结果。
-
令 serializedRecord 为序列化 record 后的结果。
-
添加 serializedIssuer 与 serializedRecord 对至 headerItems。
-
-
令 serializedHeaderItems 为序列化 headerItems 的结果。
-
如果 serializedHeaderItems 为空,终止本步骤。
-
设置
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) 方法时,需按以下步骤执行:
-
令 p 为一个新的 promise。
-
如果 doc 不是完全激活,则拒绝 p,错误为 "
InvalidStateError"DOMException,并返回 p。 -
令 global 为 doc 的关联全局对象。
-
如果 global 不是安全上下文,则拒绝 p,错误为 "
NotAllowedError"DOMException,并返回 p。 -
令 parsedURL 为对 issuer 运行 URL 解析器 的结果。
-
如果 parsedURL 解析失败,则拒绝 p,错误为 "
TypeError"DOMException,并返回 p。 -
令 origin 为 parsedURL 的origin。
-
令 topLevel 为 doc 的关联 settings 对象的顶级起源。
-
并行运行以下步骤:
-
如果将 issuer 与 topLevel 关联会超出顶级关联发行者数量限制,则插入一个全局任务到 networking task source(使用 global),使其拒绝 p,错误为 "
NotAllowedError"DOMException,并返回。 -
关联发行者 origin 与 topLevel。
-
插入一个全局任务到 networking task source(使用 global),如果为该发行者存储了令牌,则 resolve p 为 true,否则为 false。
-
-
返回 p。
注意: 该查询会修改用户代理状态,会将发行者参数与当前源关联。规范最多允许一个源关联 2 个发行者,这样避免通过发行者泄漏信息。注意,令牌查询会触发过期令牌的清理。
11.2. 兑换记录查询
当在 Document
doc 上使用 USVString
issuer 调用 hasRedemptionRecord(issuer)
方法时,需按以下步骤执行:
-
令 p 为新的 promise。
-
如果 doc 不是完全激活,则拒绝 p 并抛出 "
InvalidStateError"DOMException并返回 p。 -
令 global 为 doc 的关联全局对象。
-
如果 global 不是安全上下文,则拒绝 p 并抛出 "
NotAllowedError"DOMException并返回 p。 -
令 parsedURL 为对 issuer 执行 URL 解析器 的结果。
-
如果 parsedURL 解析失败,则拒绝 p 并抛出 "
TypeError"DOMException并返回 p。 -
令 origin 为 parsedURL 的origin。
-
并行执行以下步骤:
-
如果 origin 未与 topLevel 关联, 分派全局任务到 networking task source 目标为 global, resolve p 为 false 并返回。
-
分派全局任务到 networking task source 目标为 global, 如果存在发行者和顶级起源对的 兑换记录 则 resolve p 为 true,否则为 false。
-
-
返回 p。
注意: 类似于令牌查询,兑换记录查询也可能修改用户代理状态。但与令牌查询不同,兑换记录查询不会将发行者与顶级源关联。无需关联合发行者,因为兑换记录查询的答案不会泄漏已存令牌的发行者信息。和令牌查询一样,兑换查询也会清除过期令牌。
11.3. 清除 PST 数据
应遵循 storage 标准的用户界面指南。用户代理应提供界面从存储中清除 PST 数据。
12. 私有状态令牌 HTTP 头字段
12.1. 'Sec-Private-State-Token' 头字段
Sec-Private-State-Token 请求头字段在发行时发送一组未签名、经过掩码的令牌。在兑换时,发送单个已签名、去掩码的令牌和相关兑换元数据。
Sec-Private-State-Token 响应头字段发送一组已签名、掩码令牌。兑换时会发送新创建的已签名兑换记录。
该头的 ABNF 格式:
Sec-Private-State-Token = sf-string
12.2. 'Sec-Private-State-Token-Lifetime' 头字段
Sec-Private-State-Token-Lifetime
响应头字段指明所附兑换记录的过期时间。单位为秒。
该头的 ABNF 格式:
Sec-Private-State-Token-Lifetime = sf-integer
12.3. 'Sec-Private-State-Token-Crypto-Version' 头字段
Sec-Private-State-Token-Crypto-Version
头字段指明私有状态令牌的加密协议版本。
该头的 ABNF 格式:
Sec-Private-State-Token-Crypto-Version = sf-string
12.4. 'Sec-Redemption-Record' 头字段
Sec-Redemption-Record
请求头字段发送先前兑换操作缓存的兑换记录。
该头的 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-Token、Sec-Private-State-Token-Lifetime、Sec-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 审阅并指导本规范。