forked from WICG/trust-token-api
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathspec.bs
864 lines (652 loc) · 40.3 KB
/
spec.bs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
<pre class='metadata'>
Title: Private State Token API
H1: Private State Token API
Shortname: private-state-token-api
Level: 1
Status: CG-DRAFT
Group: WICG
Repository: WICG/trust-token-api
URL: https://wicg.github.io/trust-token-api/
Editor: Aykut Bulut, Google https://www.google.com/, [email protected]
Editor: Steven Valdez, Google https://www.google.com/, [email protected]
Abstract: Private State Token API is a web platform API that allows propagating a limited amount of anti-fraud signals across sites, using the Privacy Pass protocol as an underlying primitive.
!Participate: <a href="https://github.com/WICG/trust-token-api">GitHub WICG/trust-token-api</a> (<a href="https://github.com/WICG/trust-token-api/issues/new">new issue</a>, <a href="https://github.com/WICG/trust-token-api/issues?state=open">open issues</a>)
!Commits: <a href="https://github.com/WICG/trust-token-api/commits/main/spec.bs">GitHub spec.bs commits</a>
Markup Shorthands: css no, markdown yes
Ignored Terms: h1, h2, h3, h4, h5, h6, xmp
</pre>
<pre class="anchors">
urlPrefix: https://fetch.spec.whatwg.org/; spec: Fetch
text: http-network-or-cache fetch; url: #concept-http-network-or-cache-fetch; type: dfn
</pre>
<pre class='biblio'>
{
"PRIVACY-PASS-ARCHITECTURE": {
"authors": ["A. Davidson", "J. Iyengar", "C. A. Wood"],
"href": "https://www.ietf.org/archive/id/draft-ietf-privacypass-architecture-06.html",
"publisher": "IETF",
"title": "Privacy Pass Architectural Framework"
},
"PRIVACY-PASS-AUTH-SCHEME": {
"authors": ["T. Pauly", "S. Valdez", "C. A. Wood"],
"href" : "https://www.ietf.org/archive/id/draft-ietf-privacypass-auth-scheme-05.html",
"publisher": "IETF",
"title": "The Privacy Pass HTTP Authentication Scheme"
},
"PRIVACY-PASS-ISSUANCE-PROTOCOL": {
"authors": ["S. Celi", "A. Davidson", "A. Faz-Hernandez", "S. Valdez", "C. A. Wood"],
"href": "https://www.ietf.org/archive/id/draft-ietf-privacypass-protocol-06.html",
"publisher": "IETF",
"title": "Privacy Pass Issuance Protocol"
},
"PRIVACY-PASS-WG": {
"href": "https://datatracker.ietf.org/wg/privacypass/about/"
},
"PMB": {
"authors": ["Ben Kreuter", "Tancrede Lepoint", "Michele Orru", "Mariana Raykova"],
"href": "https://eprint.iacr.org/2020/072",
"publisher": "Cryptology ePrint Archive",
"title": "Anonymous Tokens with Private Metadata Bit"
},
"VOPRF": {
"authors": ["A. Davidson", "A. Faz-Hernandez", "N. Sullivan", "C. A. Wood"],
"href": "https://www.ietf.org/archive/id/draft-irtf-cfrg-voprf-13.html",
"publisher": "IETF",
"title": "Oblivious Pseudorandom Functions (OPRFs) using Prime-Order Groups"
},
"ISSUER-PROTOCOL": {
"authors": ["S. Valdez", "A. Bulut", "S. Schlesinger"],
"href": "https://github.com/WICG/trust-token-api/blob/main/ISSUER_PROTOCOL.md",
"publisher": "Google",
"title": "ISSUER_PROTOCOL"
}
}
</pre>
<pre class="anchors">
urlPrefix: https://tools.ietf.org/html/rfc8941; spec: rfc8941
type: dfn
text: structured header; url: #
for: structured header
type: dfn
text: integer; url: #section-3.3.1
text: string; url: #section-3.3.3
</pre>
**This is a working version and is subject to change.**
Goals {#goals}
==============
The goal of the Private State Token API is to transfer a limited amount of signals across
sites through time in a privacy preserving manner. It achieves this using
privacy pass protocol [[!PRIVACY-PASS-ISSUANCE-PROTOCOL]] specified in working
documents of privacy pass working group of IETF [[PRIVACY-PASS-WG]]. Private
State Tokens can be considered as a web platform implementation of Privacy
Pass.
<!--
In a real-world
system relying on anonymous tokens without private metadata bit, if the issuer stops providing
malicious users with tokens, the attacker will know that they have been detected as malicious.
In fact, this information could serve as an incentive to corrupt more users, or to train machine
learning models that detect which malicious behavior goes un-noticed.
https://eprint.iacr.org/2020/072.pdf
-->
Background {#background}
========================
The Private State Token API provides a mechanism for anonymous authentication. The
API provided by the browser does not authenticate clients, instead it facilitates
transfer of authentication information.
Authentication of the clients and token signing are both carried by the same
entity referred to as the **issuer**. This is the joint attester and issuer
architecture described in [[!PRIVACY-PASS-ARCHITECTURE]],
[[!PRIVACY-PASS-AUTH-SCHEME]].
Browsers store tokens in persistent storage. Navigated origins might fetch/spend
tokens in first party contexts or include third party code that fetch/spend
tokens. Spending tokens is called **redeeming**.
Origins may ask browser to fetch tokens from the issuers of their
choice. Tokens can be redeemed from a different origin than the fetching one.
Private State Tokens API performs cross site anonymous authentication without
using linkable state carrying cookies [[RFC6265]]. Cookies do provide cross
site authentication, however, they fail to provide anonymity.
Cookies store large amounts of information. [[RFC6265]] requires at least 4096
bytes per cookie and 50 cookies per domain. This means an origin has
50 x 4096 x 2^8 unique identifiers at its disposal. When backed with back end
databases, a server can store arbitrary data for that many unique
users/sessions.
Compared to a cookie, the amount of data stored in a Private State Token is very
limited. A token stores a value from a set of six values (think of a value of
an enum type of six possible values). Hence a token stores data between 2 and 3
bits (4 < 6 < 8). This is very small compared to 4096 bytes a cookie can store.
Moreover, Private State Tokens API use cryptographic protocols that prevents
origins from tracking which tokens they issue to which user. When presented with
their tokens, issuers can verify they issued them but cannot link the
tokens to the context of their issuance. Cookies do not have this property.
Unlike cookies, storing multiple tokens from an issuer does not deteriorate
privacy of the user due to the unlinkability of the tokens. The Private
State Token API allows at most 2 different issuers in a top level origin. This
is to limit the information stored for a user when the issuers are
collaborating.
Private State Token operations rely on [[!FETCH]]. A fetch request corresponding to a
specific Private State Token operation can be created and used as a parameter to the
fetch function.
Note: This specification uses the terms "masked" and "unmasked" where [[!PMB]]
uses "blinded" and "unblinded", respectively.
<!--
* how this is not as powerful like cookies, privacy guarantees?
* Between first and second para there is some gap. We should fill in.
* Level of details in privacy is good and important. A high level approach of this before API details.
* Start with use case and scenarios. This would help with people confused with API.
* how do we refer to unsigned/signed bind/clear tokens?
-->
Issuer Public Keys {#issuer-public-keys}
========================================
This section describes the public interfaces that an issuer is required to
support to provide public keys to be used by Private State Token protocols.
An issuer needs to maintain a set of keys and implement the **Issue** and
**Redeem** cryptographic functions to sign and validate tokens. Issuers are
required to serve a **key commitment** endpoint. Key commitments are
collections of cryptographic keys and associated metadata necessary for
executing the issuance and redemption operations. Issuers make these available
through secure HTTP [[!RFC8446]] endpoints. Browsers should fetch the key
commitments periodically.
Requests to key commitment endpoints should result in a JSON response
[[!RFC8259]] of the following format.
```javascript
{
<cryptographic protocol_version>: {
"protocol_version": <cryptographic protocol version>,
"id": <key commitment identifier>
"batchsize": <batch size>,
"keys": {
<keyID>: { "Y": <base64-encoded public key>,
"expiry": <key expirion data>},
<keyID>: { "Y": <base64-encoded public key>,
"expiry": <key expirion data}, ...
}
},
...
}
```
* `<cryptographic protocol version>` is a string identifier for the Private State Token
protocol version used. The same string is used as a value of the inner
`"protocol_version"` field. Protocol version string identifier is either
`"PrivateStateTokenV3PMB"` or `"PrivateStateTokenV3VOPRF"`. Both protocols have similar
properties in terms of privacy implications.
* Protocol version `“PrivateStateTokenV3PMB”` implements [[!PMB]] cryptographic
protocol. In this protocol, each token contains a private
metadata bit.
* Protocol version `“PrivateStateTokenV2VOPRF”` implements [[!VOPRF]] cryptographic
protocol. Contrary to [[!PMB]], tokens do not contain private
metadata bits. However, issuers can use twice as many
concurrently valid token signing keys (six compared to three of
[[!PMB]]).
* `"id"` field provides the identifier of the key commitment. It is a string
representation of a non-negative integer that is within the range of
an unsigned 32 bit integer type. Values should be montonically
increasing.
* `"batchsize"` specifies the maximum number of masked tokens that the issuer
supports for each token issuance operation. Its value is a
string representation of a positive integer. The user agent might send
fewer tokens in a single operation, but will generally default to
sending `batchsize` many tokens per operation.
* `"keys"` field is a dictionary of public keys listed by their identifiers.
* `<keyID>` is a string representation of a non-negative integer that
is within the range of an unsigned 32 bit integer type.
* Each key has a `"Y"` field which is a string representation of a
big-endian base64 encoding [[!RFC4648]] of the byte string of
the key.
* `"expiry"` field specifies how long the underlying key is valid. It
is a string representation of a nonnegative integer that
is within the range of an unsigned 64 bit integer type.
Underlying key expires if this amount many or more
microseconds are elapsed since the POSIX epoch
[[!RFC8536]].
All field names and their values are strings. When new key commitments are
fetched for an issuer, previous commitments are discarded.
Algorithms {#algorithms}
====================================
An user agent has <dfn>issuerAssociations</dfn>, which is a [=map=] where the keys are [=/origin=] |topLevel|, and the values are a [=list=] of [=origins=].
To <dfn>determine whether associating an issuer would exceed the top-level limit</dfn> given an [=/origin=] |issuer| and an [=/origin=] |topLevel|, run the following steps:
1. If [=issuerAssociations=][|topLevel|] does not [=map/exist=], return false.
1. If [=issuerAssociations=][|topLevel|] [=list/contains=] |issuer|, return false.
1. If the [=issuerAssociations=][|topLevel|] [=list/size=] is less than 2, return false.
1. Return true.
To <dfn>associate the issuer</dfn> |issuer| (an [=/origin=]) with the [=/origin=] |topLevel|, run the following steps:
1. If [=issuerAssociations=][|topLevel|] does not [=map/exist=], [=map/set=] [=issuerAssociations=][|topLevel|] to an empty [=list=].
1. [=list/Append=] |issuer| to [=issuerAssociations=][|topLevel|].
To determine whether an [=/origin=] |issuer| <dfn>is associated with</dfn> a given [=/origin=] |topLevel|, run the following steps:
1. If [=issuerAssociations=][|topLevel|] does not [=map/exist=], return false.
1. If [=issuerAssociations=][|topLevel|] [=list/contains=] |issuer|, return true.
1. Return false.
An user agent has <dfn>redemptionTimes</dfn>, a [=map=] where the keys are a [=tuple=] (|issuer|, |topLevel|), and the values are a [=tuple=] (|lastRedemption|, |penultimateRedemption|).
To <dfn>record redemption timestamp</dfn> given an [=/origin=] |issuer| and an [=/origin=] |topLevel|, run the following steps:
1. Let |currentTime| be the current date and time.
1. Let |previousRedemption| be the earliest representable date and time.
1. If [=redemptionTimes=][(|issuer|,|topLevel|)] [=map/exists=], let |previousRedemption| be the |lastRedemption| field of the tuple [=redemptionTimes=][(|issuer|,|topLevel|)].
1. [=map/set=] [=redemptionStore=][(|issuer|,|topLevel|)] to the tuple (|currentTime|, |previousRedemption|).
To <dfn>look up penultimate redemption</dfn> given an [=/origin=] |issuer| and an [=/origin=] |topLevel|, run the following steps:
1. Let |penultimateRedemption| be the earliest representable date and time.
1. If [=redemptionTimes=][(|issuer|,|topLevel|)] [=map/exists=], let |penultimateRedemption| be the |penultimateRedemption| field of the tuple [=redemptionTimes=][(|issuer|,|topLevel|)].
1. Return |penultimateRedemption|.
To <dfn>look up the key commitments</dfn> for a given [=/origin=] |issuer| and an [=/origin=] |topLevel|, run the following steps:
1. TODO
To <dfn>retrieve a token</dfn> for an [=/origin=] |issuer|, run the following steps:
1. TODO
To <dfn>discard tokens</dfn> from an [=/origin=] |issuer|, run the following steps:
1. TODO
To get the <dfn>number of tokens</dfn> from an [=/origin=] |issuer|, run the following steps:
1. TODO
To get the <dfn>max batch size</dfn> for an [=/origin=] |issuer|, run the following steps:
1. TODO
To <dfn>generate masked tokens</dfn> given a number |numTokens|, run the following steps:
1. TODO
Integration with Fetch {#fetch-integration}
====================================
Definitions {#definitions}
---------------------------------------------------------------
The {{RefreshPolicy}} is attached to a redemption request, determining whether or not the
redemption should result in a previously returned, unexpired [=redemption record=] or a
new one.
<pre class=idl>
enum RefreshPolicy { "none", "refresh" };
</pre>
The {{TokenType}} is currently set to "private-state-token", as this is the only token
type that the specification supports at this time.
<pre class=idl>
enum TokenType { "private-state-token" };
</pre>
The {{OperationType}} refers to which operation the user agent is attempting to complete.
<pre class=idl>
enum OperationType { "token-request", "send-redemption-record", "token-redemption" };
</pre>
The {{PrivateStateToken}} contains the information required to make a fetch request.
<pre class=idl>
dictionary PrivateStateToken {
required OperationType operation;
RefreshPolicy refreshPolicy = "none";
sequence<USVString> issuers;
};
</pre>
Issue(151): Need to specify how `operation` and `issuers` are used.
This specification adds a new property to the {{RequestInit}} dictionary:
<pre class=idl>
partial dictionary RequestInit {
PrivateStateToken privateStateToken;
};
</pre>
Modifications to request {#fetch-request}
---------------------------------------------------------------
A [=/request=] has an associated <dfn for=request>token refresh policy</dfn>, which is <code>"none"</code> or <code>"refresh"</code>. Unless stated otherwise it is <code>"none"</code>.
Add the following steps to the <code><a constructor lt="Request()">new Request (<var ignore>input</var>, |init|])</a></code> constructor, before step 28 ("<code>Set [=this=]'s [=Request/request=] to |request|</code>"):
1. If |init|["{{RequestInit/privateStateToken}}"] [=map/exists=], then set <var ignore>request</var>'s [=request/token refresh policy=]</a> to |init|["{{RequestInit/privateStateToken}}"]["{{PrivateStateToken/refreshPolicy}}"].
Modifications to http-network-or-cache fetch {#http-network-or-cache-fetch}
---------------------------------------------------------------
This specification adds the following steps to the [=http-network-or-cache fetch=] algorithm, before modifying the header list:
1. [=Append private state token issue request headers=] on |httpRequest|.
1. [=Append private state token redemption request headers=] on |httpRequest|.
Issuing Protocol {#issuing-protocol}
====================================
This section explains the issuing protocol. It has two sections that explains
the issuing protocol steps happenning in browsers and issuers.
Browser Steps For Creating Issue Request {#browser-issue-steps}
---------------------------------------------------------------
<div class=example>
An issue request is created and fetched as demonstrated in the following snippet.
```javascript
let issueRequest = new Request("https://example.issuer:1234/issuer_path?public=0&private=0", {
privateStateToken: {
type: "token-request",
issuer: "https://example.issuer"
}
});
fetch(issueRequest);
```
</div>
To <dfn>append private state token issue request headers</dfn> given a [=/request=] |request|, run the following steps:
1. If |request|'s [=request/client=] is not a [=secure context=], return.
1. Let |issuer| be |request|'s [=request/URL=]'s [=url/origin=].
1. Let |topLevel| be |request|'s [=request/client=]'s [=environment/top-level origin=].
1. If associating |issuer| with |topLevel| [=determine whether associating an issuer would exceed the top-level limit|would exceed the top level’s number-of-issuers limit=], return.
1. [=Associate the issuer=] |issuer| with |topLevel|.
1. If the [=number of tokens=] for |issuer| is at least 500, return.
1. Let |commitments| be the result of [=look up the key commitments|looking up the key commitments=] for |issuer| and |topLevel|.
1. If |commitments| is [=list/empty=], return.
1. Pass issuer public keys to cryptographic procedures. Reject if keys are
malformed.
Issue(187): How do we get the issuer public keys here?
1. [=Discard tokens=] from |issuer| that are signed with keys other than those from
the issuer's most recent commitments.
Issue(189): This probably needs to be its own algorithm, how is "most recent" defined here?
1. Let |numTokens| be |issuer|'s [=max batch size=] or an [=implementation-defined=] limit on the number of tokens (which is recommended to be 100), whichever is smaller.
1. Let |tokens| be the result of [=generate masked tokens|generating masked tokens=] with |numTokens|.
1. Set a load flag to bypass the HTTP cache.
1. Let |base64-encoded-tokens| be the base64-encoded version of |tokens|.
1. Let |version| be the version of the cryptographic protocol used.
1. [=header list/Set a structured field value=] given (<a http-header>`Sec-Private-State-Token`</a>, |base64-encoded-tokens|)
in |request|'s header list.
1. [=header list/Set a structured field value=] given (<a http-header>`Sec-Private-State-Token-Version`</a>, |version|)
in |request|'s header list.
<div class=example>
Private State Token HTTP request headers created for a typical fetch is as follows.
```
Sec-Private-State-Token: <masked tokens encoded as base64 string>
Sec-Private-State-Token-Version: <cryptographic protocol version, VOPRF or PMB>
```
</div>
Issuer Signing Tokens {#issuer-signing-tokens}
----------------------------------------------
This section explains the signing of tokens that happens in the issuer
servers. Information to be encoded in the tokens are passed in the URL `public`
and `private` parameters. VOPRF can only encode a value from set {0,1,2,3,4,5}
passed in the URL `public` parameter. [[!PMB]] can encode a value from set {0,1,2}
in `public` and a value from set {0,1} in `private` parameter.
Using its private keys, issuer signs the masked tokens obtained in the
<a http-header>Sec-Private-State-Token</a> request header value. Issuer uses the cryptographic protocol
specified in the request <a http-header>Sec-Private-State-Token-Version</a> header. Encoding the values
passed in URL `private` and `public` parameters happens in this signing
step. Issuer returns the signed tokens in the <a http-header>Sec-Private-State-Token</a> response header
value encoded as a base64 byte string.
<div class=example>
The following snippet displays a typical response demonstrating the Private State Token
header.
```
Sec-Private-State-Token: <token encoded as base64 string>
```
</div>
The details for servers implementing this protocol can be found in [[!ISSUER-PROTOCOL]].
Browser Steps For Issue Response {#browser-issue-response}
----------------------------------------------------------
To process a response to an issue request, browser carries out the following steps.
1. Let |p| be [=a new promise=].
1. If the response has no <a http-header>Sec-Private-State-Token</a> header, [=/reject=] |p| with an "{{InvalidStateError}}" {{DOMException}} and return |p|.
1. If the response has an empty <a http-header>Sec-Private-State-Token</a> header, [=/resolve=] |p| with {{undefined}} and return |p|. (This is a `Success` response bearing 0 tokens.)
1. Remove the <a http-header>Sec-Private-State-Token</a> header from the response and carry out the cryptographic procedures to obtain a list of unmasked tokens.
Issue(190): Define "cryptographic procedures to obtain a list of unmasked tokens"
1. If the cryptographic procedure succeeds, associate the tokens with the issuing key's label and store the tokens. [=/Resolve=] |p| with {{undefined}} and return |p|.
1. Else, [=/reject=] |p| with a "{{DataError}}" {{DOMException}} and return |p|.
Issue: Define this in terms of fetch
The details for servers implementing this protocol can be found in [[!ISSUER-PROTOCOL]].
Redeeming Tokens {#redeeming-tokens}
====================================
When browser navigates to an origin, top level origin or a third party site
embedded on the top level origin may redeem tokens stored in browser from a
specific issuer to learn `public` and/or `private` data encoded in the
tokens.
<div class=example>
Redemption is carried through fetch as demonstrated in the following
snippet. The default value for refreshPolicy is `'none'`.
```javascript
let redemptionRequest = new Request('https://example.issuer:1234/redemption_path', {
privateStateToken: {
type: 'token-redemption',
issuer: 'https://example.issuer',
refreshPolicy: {'none', 'refresh'}
}
});
```
</div>
<!--
checking fetch syntax, malformed input etc?
When `refreshPolicy` is `'none'`,
browser uses the previously cached [=redemption record=] instead of redeeming a new
token.
-->
To <dfn>set redemption headers</dfn> with [=/request=] |request| and a [=Redemption Record=] |record|:
1. Let |redemption-result| be the redemption procedure result.
1. Let |version| be the version of the cryptographic protocol used.
1. Let |token-lifetime| be the expiration time of the [=redemption record=] in seconds.
1. [=header list/Set a structured field value=] given (<a http-header>`Sec-Private-State-Token`</a>, |redemption-result|)
in |request|'s header list.
1. [=header list/Set a structured field value=] given (<a http-header>`Sec-Private-State-Token-Version`</a>, |version|)
in |request|'s header list.
1. Optionally, [=header list/set a structured field value=] given (<a http-header>`Sec-Private-State-Token-Lifetime`</a>, |token-lifetime|)
in |request|'s header list.
1. Configure the HTTP request. Set a load flag to bypass the HTTP cache.
Issue: This algorithm should append structured headers using [=header list/set a structured field value=]
To <dfn>append private state token redemption request headers</dfn> given a [=/request=] |request|, run the following steps:
1. Let |issuer| be |request|'s [=request/URL=]'s [=url/origin=].
1. Let |topLevel| be |request|'s [=request/client=]'s [=environment/top-level origin=].
1. If |request|'s [=request/client=] is not a [=secure context=], return.
1. If associating |issuer| with |topLevel| [=determine whether associating an issuer would exceed the top-level limit|would exceed the top level’s number-of-issuers limit=], return.
1. [=Associate the issuer=] |issuer| with |topLevel|.
1. If |request|'s [=request/token refresh policy=] is `"none"`:
1. Let |record| be the result of [=get an unexpired redemption record|getting an unexpired redemption record=].
1. If |record| is not null, [=set redemption headers=] with |request| and |record| and return.
1. Let |penultimate_redemption| be the result of [=look up penultimate redemption =] with |issuer| and |topLevel|, if the value is less than an [=implementation-defined=] time period (which is recommended to be 48 hours), return error.
1. Let |commitments| be the result of [=look up the key commitments|looking up the key commitments=] for |issuer| and |topLevel|.
1. If |commitments| is [=list/empty=], return.
1. [=Discard tokens=] from |issuer| that are signed with keys other than those from
the issuer's most recent commitments.
Issue(189): This probably needs to be its own algorithm, how is "most recent" defined here?
1. Let |token| be the result of [=retrieve a token|retrieving a token=] for |issuer|.
1. If |token| is null, return.
1. Let |record| be the result of running a cryptographic redemption procedure with |token|. If the procedure fails, return.
Issue: This needs to be specified more clearly
1. [=Set redemption headers=] with |request| and |record|.
ISSUE(165): Add redemption response handling, including calling [= record redemption timestamp =].
<a http-header>Sec-Private-State-Token-Lifetime</a> response header indicates how long (in seconds) the
[=redemption record=] should be cached for. When <a http-header>Sec-Private-State-Token-Lifetime</a> response header value
is invalid (too large, a negative number or non-numeric), UA should ignore the
<a http-header>Sec-Private-State-Token-Lifetime</a> header. When <a http-header>Sec-Private-State-Token-Lifetime</a> header value
is zero, UA should treat the record as expired. In case of multiple
<a http-header>Sec-Private-State-Token-Lifetime</a> headers, UA uses the last one. If
<a http-header>Sec-Private-State-Token-Lifetime</a> header is omitted, the lifetime of the [=redemption record=] will be
tied to the lifetime of the Private State Token verification key that confirmed the
redeemed token's issuance. The [=redemption record=] is HTTP-only and JavaScript is only able to
access/send the [=redemption record=] via Private State Token Fetch APIs. The [=redemption record=] is treated as an
arbitrary blob of bytes from the issuer, that may have semantic meaning to
downstream consumers.
Redemption Records {#redemption-records}
----------------------------------------
To reduce communication overhead, the browser might cache blobs returned in
<a http-header>Sec-Private-State-Token</a> header value in redemption responses. These blobs are
referred as [=Redemption Records=]. Browsers might choose to store these records
to include them in subsequent requests to the origins that can verify its
validity. Issuer might choose to include optional <a http-header>Sec-Private-State-Token-Lifetime</a>
header in the redemption response. The value of this header indicates the
expiration time for the [=redemption record=] provided. This expiration is
specified as number of seconds in the <a http-header>Sec-Private-State-Token-Lifetime</a> HTTP response
header value.
A <dfn>Redemption Record</dfn> is a [=byte sequence=].
To <dfn>get an unexpired [=Redemption Record=]</dfn> with an [=/origin=] |issuer| and an [=/origin=] |topLevel|:
1. TODO
<h3 id="the-document-object">Changes to {{Document}}</h3>
<pre class="idl">
partial interface Document {
Promise<boolean> hasPrivateTokens(USVString issuer, USVString type);
Promise<boolean> hasRedemptionRecord(USVString issuer, USVString type);
};
</pre>
Query APIs {#query-apis}
========================
Token Query {#token-query}
--------------------------
When invoked on {{Document}} |doc| with {{USVString}} |issuer| and {{USVString}} |type|, the <dfn export method for=Document><code>hasPrivateTokens(issuer, type)</code></dfn> method must run these steps:
1. Let |p| be [=a new promise=].
1. If |doc| is not [=Document/fully active=], then [=/reject=] |p| with an "{{InvalidStateError}}" {{DOMException}} and return |p|.
1. Let |global| be |doc|'s [=relevant global object=].
1. If |global| is not a [=secure context=], then [=/reject=] |p| with a "{{NotAllowedError}}" {{DOMException}} and return |p|.
1. If |type| is not `"private-state-token"`, [=reject=] |p| with a "{{TypeError}}" {{DOMException}} and return |p|.
1. Let |parsedURL| be the the result of running the [=URL parser=] on |issuer|.
1. If |parsedURL| is failure, [=reject=] |p| with a "{{TypeError}}" {{DOMException}} and return |p|.
1. Let |origin| be |parsedURL|'s [=/origin=].
1. Let |topLevel| be the [=top-level origin=] of |doc|'s [=relevant settings object=].
1. Run the following steps [=in parallel=]:
1. If associating |issuer| with |topLevel| [=determine whether associating an issuer would exceed the top-level limit|would exceed the top level’s number-of-issuers limit=], [=queue a global task=] on the [=networking task source=] given |global| to [=/reject=] |p| with a "{{NotAllowedError}}" {{DOMException}} and return.
1. [=Associate the issuer=] |origin| with |topLevel|.
1. [=Look up the key commitments=] for |origin| and |topLevel|. If there are key commitments,
[=discard tokens=] from |origin| that are signed with keys other than those
from the issuer's most recent commitments.
1. [=Queue a global task=] on the [=networking task source=] given |global| to [=/resolve=] |p| with true if there are tokens stored for the given issuer, with false otherwise.
1. Return |p|.
Note: This query modifies the browser state. It associates the issuer
argument with the current origin. Browser allows at most 2 issuers associated
with an origin. This is to prevent leaking information through the issers a
user has tokens from. Note that token query triggers removal of stale tokens
at Step 4.
Redemption Record Query {#redemption-record-query}
--------------------------------------------------
When invoked on {{Document}} |doc| with {{USVString}} |issuer| and {{USVString}} |type|, the <dfn export method for=Document><code>hasRedemptionRecord(issuer, type)</code></dfn> method must run these steps:
1. Let |p| be [=a new promise=].
1. If |doc| is not [=Document/fully active=], then [=/reject=] |p| with an "{{InvalidStateError}}" {{DOMException}} and return |p|.
1. Let |global| be |doc|'s [=relevant global object=].
1. If |global| is not a [=secure context=], then [=/reject=] |p| with a "{{NotAllowedError}}" {{DOMException}} and return |p|.
1. If |type| is not `"private-state-token"`, [=reject=] |p| with a "{{TypeError}}" {{DOMException}} and return |p|.
1. Let |parsedURL| be the the result of running the [=URL parser=] on |issuer|.
1. If |parsedURL| is failure, [=reject=] |p| with a "{{TypeError}}" {{DOMException}} and return |p|.
1. Let |origin| be |parsedURL|'s [=/origin=].
1. Let |topLevel| be the [=top-level origin=] of |doc|'s [=relevant settings object=].
1. Run the following steps [=in parallel=]:
1. If |origin| [=is associated with|is not associated with=] |topLevel|, [=queue a global task=] on the [=networking task source=] given |global| to [=/resolve=] |p| with false and return.
1. [=Look up the key commitments=] for |origin| and |topLevel|. If there are key commitments,
[=discard tokens=] from |origin| that are signed with keys other than those
from the issuer's most recent commitments.
1. [=Queue a global task=] on the [=networking task source=] given |global| to [=/resolve=] |p| with true if there is a [=redemption record=] for the issuer and top level pair, with false otherwise.
1. Return |p|.
Note: Similar to token query, redemption query might modify the browser state. Unlike
token query, redemption query does not associate issuer with the top level
origin. There is no need to associate the issuer queried with the top level
origin, because, answer to the redemption query does not leak information about
the issuers of the currently stored tokens. Similar to token query, redemption
query clears stale tokens.
Private State Token HTTP Header Fields {#pst-http-header-fields}
=================================
The 'Sec-Private-State-Token' Header Field {#sec-private-state-token}
----------------------------
The <dfn http-header>Sec-Private-State-Token</dfn> *request* header field sends
a collection of unsigned, masked tokens during issuance. During redemption, it
sends a singled signed, unmasked token along with associated redemption
metadata.
Issue: Define and link issuance, redemption, masked (formerly blinded), unmasked,
signed, unsigned
The <a http-header>Sec-Private-State-Token</a> *response* header field sends
a collection of signed, masked tokens. During redemption it sends the
just-created signed [=redemption record=].
Issue: Link stuff here too.
It is a [=Structured Header=] whose value MUST be an [=structured header/string=]
[[!RFC8941]].
The header’s ABNF is:
``` abnf
Sec-Private-State-Token = sf-string
```
The 'Sec-Private-State-Token-Lifetime' Header Field {#sec-private-state-token-lifetime}
----------------------------
The <dfn http-header>`Sec-Private-State-Token-Lifetime`</dfn> response header
field gives the expiration for the [=redemption record=]
given in the associated <a http-header>`Sec-Private-State-Token`</a>
response header. The expiration is given in seconds.
It is a [=Structured Header=] whose value MUST be an [=structured header/integer=]
[[!RFC8941]].
The header’s ABNF is:
``` abnf
Sec-Private-State-Token-Lifetime = sf-integer
```
The 'Sec-Private-State-Token-Version' Header Field {#sec-private-state-token-version}
----------------------------
The <dfn http-header>`Sec-Private-State-Token-Version`</dfn> header field gives
the "major version" of Private State Token protocol.
ISSUE(188): Define what "major version" actually means.
It is a [=Structured Header=] whose value MUST be an [=structured header/string=]
[[!RFC8941]].
The header’s ABNF is:
``` abnf
Sec-Private-State-Token-Version = sf-string
```
Privacy Considerations {#privacy}
=================================
Unlinkability {#unlinkability}
------------------------------
Cryptographic protocols [[!VOPRF]] and [[!PMB]] provide masked signatures. At
redemption time, issuers can recognize their signature on the provided token,
however they can not determine at what time or in which context they signed the token.
This prevents issuers from correlating their issuances on an origin with
redemptions on another origin. Issuers learn only the aggregate information
about the origins users visit.
Limiting Encoded Information {#limit-encoded-info}
--------------------------------------------------
User agents should enforce limits on the number of unique keys an issuer can have
at any point in time, to preserve client privacy. Without limits, an issuer could
de-anonymize clients by simply using a unique key for each client. For [[!VOPRF]], the
number of keys is limited to six, and for [[!PMB]], the number is limited to
three keys.
Issuers can utilize different keys to represent different "labels", which correspond
to an arbritrary client state, such as client trust level or some other useful
anti-fraud signal. Issuers are responsible for understanding this designation and
sharing the key "labels" with token redeemers so they know how to interpret the
significance of each token. This helps reduce reverse engineering from malicious
actors and preserves client privacy over human-readable labels. When using [[!VOPRF]],
the issuer's 6 keys can effectively represent six labels. Similarly, when using [[!PMB]],
even though there are only up to three keys, the issuer can still utilize six labels
by having each key correspond to the the private bit value (3 keys x 2 values = 6 labels).
Both [[!VOPRF]] and [[!PMB]] encode the same amount of information in a token, however the
difference is [[!PMB]] utilizing a private bit.
### Potential Attack: Side Channel Fingerprinting {#side-channel-fingerprinting}
Unlinkability is lost if the issuer is able to use network-level fingerprinting
or any other side-channel and can associate the browser at redemption time with
the browser at token issuance time, even though the Private State Token API
itself has only stored and revealed limited amount of information about the
browser.
Cross-site Information Transfer {#cross-site-info}
--------------------------------------------------
Private State Tokens transfer limited information between first-party contexts.
Underlying cryptographic protocols guarantee that each token only contains a
small amount of information. Still, if we allow many token redemptions on a
single page, the first-party cookie for user U on domain A can be encoded in
the Private State Token information channel and decoded on domain B, allowing
domain B to learn the user's domain A cookie until either 1p cookie is cleared.
Separate from the concern of channels allowing arbitrary communication between
domains, some identification attacks---for instance, a malicious redeemer
attempting to learn the exact set of issuers that have granted tokens to a
particular user, which could be identifying---have similar mitigations.
### Mitigation: Dynamic Issuance/Redemption Limits {#api-usage-limits}
To mitigate this attack, browser places limits on both issuance and redemption.
User activation with the issuing site is required in the issuing operation. The
browser does not allow a third redemption in a 48 hour window.
### Mitigation: Per-Site Issuer Limits {#per-issuer-limits}
The rate of identity leakage from one origin to another increases with the
number of issuers allowed in an origin. To avoid abuse, the browser allows
association of at most two issers per top level origin. Issuers are associated
with top level origins for token query API as well, see [[#token-query]].
Security Considerations {#security}
===================================
Preventing Token Exhaustion {#token-exhaustion}
-----------------------------------------------
Malicious origins might attempt to exhaust all tokens stored in the browser by
redeeming them all. To prevent this, the browser limits number of redemption
operations. In an origin first two redemptions are allowed, however, the third
redemption is not allowed in a 48 hour window. The third redemption is allowed
once more than 48 hours have elapsed since the first redemption.
Preventing Double Spending {#preventing-double-spend}
-----------------------------------------------------
Issuers can verify that each token is seen only once, because every redemption
is sent to the same token issuer. This means that even if a malicious piece of
malware exfiltrates all of a user's tokens, the tokens will run out over time.
Issuers can sign fewer tokens at a time to mitigate the risk.
IANA Considerations {#iana-considerations}
==========================================
This document intends to define the <a http-header>Sec-Private-State-Token</a>,
<a http-header>Sec-Private-State-Token-Lifetime</a>, <a http-header>Sec-Private-State-Token-Version</a>,
HTTP request header fields, and register them in the permanent message
header field registry ([[!RFC9110]]).
'Sec-Private-State-Token' Header Field {#iana-sec-private-state-token}
------------------------
Header field name: Sec-Private-State-Token
Applicable protocol: http
Status: standard
Author/Change controller: IETF
Specification document: this specification ([[#sec-private-state-token]])
'Sec-Private-State-Token-Lifetime' Header Field {#iana-sec-private-state-token-lifetime}
------------------------
Header field name: Sec-Private-State-Token-Lifetime
Applicable protocol: http
Status: standard
Author/Change controller: IETF
Specification document: this specification ([[#sec-private-state-token-lifetime]])
'Sec-Private-State-Token-Version' Header Field {#iana-sec-private-state-token-version}
------------------------
Header field name: Sec-Private-State-Token-Version
Applicable protocol: http
Status: standard
Author/Change controller: IETF
Specification document: this specification ([[#sec-private-state-token-version]])
<h2 id=acknowledgments class=no-num>Acknowledgments</h2>
Thanks to Alex Kallam, Charlie Harrison, Chris Fredrickson, David Van Cleve, Dylan Cutler,
Eric Trouton, Johann Hofmann, Kaustubha Govind, Mike Taylor, Ryan Kalla, and Sam
Schlesinger for their contributions. Thanks to Chris Wilson for reviewing and mentoring this spec.