-
Notifications
You must be signed in to change notification settings - Fork 84
/
spec.bs
1220 lines (932 loc) · 62 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
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<pre class='metadata'>
Title: Private State Token API
H1: Private State Token API
Shortname: private-state-token-api
Level: None
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: The Private State Token API is a web platform API that allows propagating a limited amount of 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
Complain About: missing-example-ids yes
</pre>
<pre class="link-defaults">
spec:fetch; type:dfn; for:/; text:response
spec:fetch; type:dfn; for:/; text:request
spec:infra; type:dfn; text:list
</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
urlPrefix: https://www.ietf.org/archive/id/draft-irtf-cfrg-voprf-21.html; spec: VOPRF
text: Blind; url: #section-3.3.1-2; type: dfn
text: BlindEvaluate; url: #section-3.3.2-2; type: dfn
text: Finalize; url: #section-3.3.2-5; type: dfn
text: Evaluate; url: #section-3.3.1-9; type: dfn
text: P-384; url: #name-oprfp-384-sha-384; type: dfn
urlPrefix: https://www.ietf.org/archive/id/draft-robert-privacypass-batched-tokens-01.html; spec: BatchedToken
text: BlindEvaluateBatch; url: #section-4-5; type: dfn
text: FinalizeBatch; url: #section-5-4; 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-10.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-10.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-10.html",
"publisher": "IETF",
"title": "Privacy Pass Issuance Protocol"
},
"PRIVACY-PASS-WG": {
"href": "https://datatracker.ietf.org/wg/privacypass/about/"
},
"VOPRF": {
"authors": ["A. Davidson", "A. Faz-Hernandez", "N. Sullivan", "C. A. Wood"],
"href": "https://www.ietf.org/archive/id/draft-irtf-cfrg-voprf-21.html",
"publisher": "IETF",
"title": "Oblivious Pseudorandom Functions (OPRFs) using Prime-Order Groups"
},
"BatchedTokens": {
"authors": ["R. Robert", "C. A. Wood"],
"href": "https://www.ietf.org/archive/id/draft-robert-privacypass-batched-tokens-01.html",
"publisher": "IETF",
"title": "Batched Token Issuance Protocol"
},
"RFC4648": {
"href": "https://www.rfc-editor.org/rfc/rfc4648"
}
}
</pre>
<pre class="anchors">
urlPrefix: https://tools.ietf.org/html/rfc8941; spec: rfc8941
type: dfn
text: structured header; url: #name-introduction
for: structured header
type: dfn
text: integer; url: #section-3.3.1
text: string; url: #section-3.3.3
</pre>
Goals {#goals}
==============
The goal of the Private State Tokens is to transfer a limited amount of signals across
sites through time in a privacy preserving manner. It achieves this using a variant of the
privacy pass protocol [[!PRIVACY-PASS-ISSUANCE-PROTOCOL]] specified in the working
documents of the IETF Privacy Pass Working Group [[PRIVACY-PASS-WG]]. Private
State Tokens can be considered to be a web platform implementation of a variant of
Privacy Pass.
The spec introduces a new field in request dictionary to support token
operations. It describes how Private State Tokens are utilized through this new
dictionary.
<!--
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 user agent 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]].
User agents 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 the user agent 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.
<!--
* 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. User agents should fetch the key
commitments periodically. A <dfn>key commitment</dfn> is a [=map=] representing
a collection of cryptographic keys and associated metadata necessary for executing
the issuance and redemption operations.
Requests to key commitment endpoints should result in a JSON response
[[!RFC8259]] of the following format with a media type of
`"application/pst-issuer-directory"`:
```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 expiration date>},
<keyID>: { "Y": <base64-encoded public key>,
"expiry": <key expiration date>}, ...
}
},
...
}
```
* `<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
`"PrivateStateTokenV1VOPRF"`.
* Protocol version `“PrivateStateTokenV1VOPRF”` implements [[!VOPRF]] cryptographic
protocol. Issuers can use up to six valid token signing keys.
* `"id"` field provides the identifier of the key commitment. It is 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
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.
Issuer Key Fetching/Registration {#issuer-registration}
----------------------------
To maintain the privacy of this API and avoid user-specific keys, issuers should present the same keys to all clients that issue and redeem tokens against them.
To ensure this property, it is recommended that user agents fetch the key commitments in an user-agnostic manner, through some sort of proxied mechanism or centralized mechanism for fetching the keys and distributing them to individual clients.
If using a centralized mechanism for fetching keys, user agents should have a registration process to allow for issuers to register to have their key commitments fetched and sent to clients at a regular client. The requirements and mechanisms for registering are [=implementation-defined=].
When using a registration process, it is recommended that user agents apply an expiration date to registration requests, to allow for the removal of deprecated or no longer active issuers.
VOPRF Methods {#voprf-methods}
====================================
This document encodes protocol messages in the TLS presentation language from section 3 of [[!RFC8446]].
To <dfn>serialize protocol messages</dfn> and <dfn>deserialize protocol messages</dfn>, protocol messages are encoded and interpreted as described in section 3 of [[!RFC8446]].
For Private State Tokens, the VOPRF protocol is initialized using [=P-384=] for the curve (`G` in the functions described below) and `nonce_size` is defined as 64.
Note: The use of X9.62 uncompressed points for the inputs/outputs in the current version of PST is a historical divergence from the existing [[!VOPRF]] specification. The selection of nonce_size is a historical divergence from the current draft of [[!BatchedTokens]].
When the server is performing an Issuance, the server performs the [=BlindEvaluateBatch=] function of the [[!BatchedTokens]] protocol, where the input <dfn>IssueRequest</dfn> and output <dfn>IssueResponse</dfn> are serialized as:
```
// Scalars are elliptic curve scalars of length Ns (determined by the curve, 48 for P-384).
// ECPoints are elliptic curve points encoded using the X9.62 Uncompressed point representation (determined by the curve, 97 for P-384).
struct {
uint16 count;
ECPoint nonces[count]; // Corresponding to blindedElements
} IssueRequest;
struct {
ECPoint evaluated; // Corresponding to evaluatedElements
} SignedNonce;
struct {
Scalar c;
Scalar s;
} DLEQProof;
struct {
uint16 issued;
uint32 key_id;
SignedNonce signed[issued];
opaque proof<1..2^16-1>; // Bytestring containing a serialized DLEQProof struct.
} IssueResponse;
```
When the server is performing a Redemption, the server performs [=PSTEvaluate=] on the `Token`, the input <dfn>RedeemRequest</dfn> and output <dfn>RedeemResponse</dfn> are serialized as:
```
struct {
uint32 key_id;
opaque nonce[nonce_size];
ECPoint W;
} Token;
struct {
opaque token<1..2^16-1>; // Bytestring containing a serialized Token struct.
opaque client_data<1..2^16-1>;
} RedeemRequest;
struct {
opaque rr<1..2^16-1>;
} RedeemResponse;
```
Private State Tokens defines <dfn>PSTFinalize</dfn> which is a variation of the [=FinalizeBatch=] function of the [[!BatchedTokens]] protocol as follows:
```
def PSTFinalize(input, blinds, evaluatedElements,
blindedElements, pkS, proof):
if VerifyProof(G.Generator(), pkS, blindedElements,
evaluatedElements, proof) == false:
raise VerifyError
// PST: Use batched construction.
unblindedElements = []
for index in range(evaluatedElements.length):
N = G.ScalarInverse(blinds[index]) * evaluatedElements[index]
unblindedElements.append(G.SerializeElement(N))
// PST: Return unblindedElements rather than hash output.
return unblindedElements
```
Private State Tokens defines <dfn>PSTEvaluate</dfn> which is a variation of the [=Evaluate=] function of the [[!VOPRF]] protocol as follows:
```
// skS is determined by the server by using key_id to lookup the corresponding private key.
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: Checks issuedElement rather than hash output.
if issuedElement != W:
raise InvalidInputError
// PST: The server may use client_data and other information to construct a redemptionRecord to return to the client.
return redemptionRecord
```
Algorithms {#algorithms}
====================================
A 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.
A 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=] [=redemptionTimes=][(|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|.
A user agent has <dfn>redemptionRecords</dfn>, a [=map=] where the keys are a [=tuple=] (|issuer|, |topLevel|), and the values are a [=tuple=] (|record|, |expiration|, |signingKeys|).
To <dfn>record a redemption record</dfn> given an [=/origin=] |issuer|, an [=/origin=] |topLevel|, a [=byte sequence=] |header|, and a [=duration=] |lifetime|, run the following steps:
1. If |lifetime| is zero, return.
1. Let |currentTime| be the current date and time in milliseconds since 01 January, 1970 UTC.
1. Let |expiration| be the sum of |currentTime| and |lifetime|.
1. Let |signingKeys| be the result of [=look up the latest keys|looking up of the latest keys=] for |issuer|.
1. Set [=redemptionRecords=][(|issuer|, |topLevel|))] to the tuple (|header|, |expiration|, |signingKeys|).
To <dfn>retrieve a redemption record</dfn> given an [=/origin=] |issuer| and an [=/origin=] |topLevel|, run the following steps:
1. Let |currentTime| be the current date and time in milliseconds since 01 January, 1970 UTC.
1. If [=redemptionRecords=][(|issuer|,|topLevel|)] [=map/exists|does not exist=], return null.
1. Let (|record|, |expiration|, |signingKeys|) be [=redemptionRecords=][(|issuer|,|topLevel|)].
1. If |expiration| is less than |currentTime|, return null.
1. Let |currentSigningKeys| be the result of [=look up the latest keys|looking up of the latest keys=] for |issuer|.
1. If |currentSigningKeys| does not equal |signingKeys|, return null.
1. Return |record|.
A user agent has <dfn>pstKeyCommitments</dfn>, a [=map=] where the keys are an [=/origin=], and the values are [=key commitments=].
Note: It is recommended that each user agent fetches the key commitments from issuers at a regular cadence and through trusted infrastructure, and then sends the concatenated map of issuers and key commitments to the client to ensure consistency between the keys different user agent instances use.
To <dfn>look up the key commitments</dfn> for a given [=/origin=] |issuer|, run the following steps:
1. If [=pstKeyCommitments=][|issuer|] does not [=map/exist=], return null.
1. Let |issuerKeys| be [=pstKeyCommitments=][|issuer|].
1. For each |cryptoProtocolVersion| that the user agent supports for this API in an [=implementation-defined=] order, run the following steps:
1. Return |issuerKeys|[|cryptoProtocolVersion|], if it [=map/exists=].
1. Return null.
Note: |cryptoProtocolVersion| is a string identifier representing different cryptographic versions of tokens that can be used with this API. User agents should only select keys for versions they support, ordered by which versions they prefer based on performance and any user defined preferences.
To <dfn>look up the latest keys</dfn> for a given [=/origin=] |issuer|, run the following steps:
1. Let |commitment| be the result of [=look up the key commitments|looking up the key commitments=] for |issuer|.
1. If |commitment| is null, return null.
1. Let |chosenKey| be null.
1. Let |currentTime| be the current date and time.
1. [=list/For each=] |key| of |commitment|["keys"], run the following steps:
1. If |key|["expiry"] is less than |currentTime|, continue.
1. If |chosenKey| is null, set |chosenKey| to |key|.
1. If |key|["expiry"] is less than |chosenKey|["expiry"], set |chosenKey| to |key|.
1. Return |chosenKey|.
A user agent has a <dfn>tokenStore</dfn>, a [=map=] where the keys are [=/origins=] and the values are [=lists=] of [=storedTokens=]. A <dfn>storedToken</dfn> is a [=tuple=] of ([=byte sequence=], [=byte sequence=]).
To <dfn>insert a token</dfn> for an [=/origin=] |issuer|, [=byte sequence=] |token|, and [=byte sequence=] |signingKey|, run the following steps:
1. Create a new [=tuple=] |storedToken| consisting of (|token|, |signingKey|).
1. If [=tokenStore=][|issuer|] does not [=map/exist=], set [=tokenStore=][|issuer|] to an empty [=list=].
1. [=list/Append=] |storedToken| to [=tokenStore=][|issuer|].
To <dfn>retrieve a token</dfn> for an [=/origin=] |issuer|, run the following steps:
1. If [=tokenStore=][|issuer|] does not [=map/exist=], return null.
1. If the [=list/size=] of [=tokenStore=][|issuer|] is zero, return null.
1. [=list/Remove=] a random element of [=tokenStore=][|issuer|] and return the removed element.
To <dfn>discard tokens</dfn> from an [=/origin=] |issuer| and [=byte sequence=] |signingKey|, run the following steps:
1. If [=tokenStore=][|issuer|] does not [=map/exist=], return.
1. [=list/Remove=] all elements of [=tokenStore=][|issuer|] whose second element is not equal to |signingKey|.
To get the <dfn>number of tokens</dfn> from an [=/origin=] |issuer|, run the following steps:
1. If [=tokenStore=][|issuer|] does not [=map/exist=], return 0.
1. Return the [=list/size=] of [=tokenStore=][|issuer|].
To get the <dfn>max batch size</dfn> for an [=/origin=] |issuer|, run the following steps:
1. Let |issuerKey| be the result of running [=look up the key commitments=] on |issuer|.
1. If |issuerKey| is null, return 0.
1. Return |issuerKey|["batchsize"].
To <dfn>generate masked tokens</dfn> given [=key commitments=] |issuerKeys| and number |numTokens|, run the following steps. They return a [=tuple=] ([=byte sequence=], [=byte sequence=]).
1. Let |issueRequest| be an empty [=IssueRequest=].
1. Set |issueRequest|["count"] to |numTokens|.
1. Let |blinds| be an empty [=byte sequence=].
1. Repeat the following steps, |numTokens| times:
1. Let |input| be a random [=byte sequence=].
1. Let (|blind|, |blindedElement|) ([=tuple=] ([=byte sequence=], [=byte sequence=])) be the result of running the [=Blind=] function of the [[!VOPRF]] protocol on |input|, where |blindedElement| is encoded as a X9.62 uncompressed point.
1. Append |blind| to |blinds|.
1. Append |blindedElement| to |issueRequest|["nonces"].
1. Let |issueHeader| be the result of [=serialize protocol message|serializing protocol message=] |issueRequest|.
1. Return [=tuple=] (|issueHeader|, |blinds|).
To <dfn>unmask tokens</dfn> given [=key commitments=] |issuerKeys|, byte string |blinds|, and a [=byte sequence=] |response|, run the following steps. They return a [=list=] of [=byte sequence=].
1. Let |input| be an empty [=byte sequence=].
1. Let |evaluatedElements| be an empty [=list=].
1. Let |blindedElements| be an empty [=list=].
1. Let |issueResponse| be the result of [=deserialize protocol message|deserializing protocol message=] |response| as an [=IssueResponse=].
1. [=list/For each=] |nonce| of |issueResponse|["signed"]:
1. [=list/Append=] |nonce|["blinded"] to |blindedElements|.
1. [=list/Append=] |nonce|["evaluated"] to |evaluatedElements|.
1. Let |pkS| be |issuerKeys|["keys"][|issueResponse|["key_id"]]["Y"].
1. Let |proof| be |issueResponse|["proof"].
1. Let |blindsList| be an empty [=list=].
1. While the [=byte sequence/length=] of |blinds| is greater than 0:
1. Let |blind| be the first N elements of |blinds| and |blinds| be the remainder, where N is the length of a X9.62 uncompressed point.
1. [=list/Append=] |blind| to |blindsList|.
1. Let |result| ([=list=] of byte strings) be the result of running [=PSTFinalize=] on |input|, |blindsList|, |evaluatedElements|, |blindedElements|, |pkS|, and |proof|.
1. Return |result|.
To <dfn>set private token properties for request from private token</dfn>, given a {{PrivateToken}} |privateToken| and a [=request=] |request|, run the following steps:
1. Set |request|'s [=request/private token operation=]</a> to |privateToken|["{{PrivateToken/operation}}"].
1. If |privateToken|["{{PrivateToken/operation}}"] is {{OperationType/"token-request"}}:
1. If [$Should request be allowed to use feature?$] on "<code>[=policy-controlled feature/private-state-token-issuance=]</code>" and |request| returns <code>false</code>, then throw a "{{NotAllowedError}}" {{DOMException}}.
1. Abort the remaining steps.
1. Assert: |privateToken|["{{PrivateToken/operation}}"] is {{OperationType/"token-redemption"}} or {{OperationType/"send-redemption-record"}}.
1. If [$Should request be allowed to use feature?$] on "<code>[=policy-controlled feature/private-state-token-redemption=]</code>" and |request| returns <code>false</code>, then throw a "{{NotAllowedError}}" {{DOMException}}.
1. If |privateToken|["{{PrivateToken/operation}}"] is <code>"token-redemption"</code>:
1. Set |request|'s [=request/private token refresh policy=]</a> to |privateToken|["{{PrivateToken/refreshPolicy}}"].
1. Abort the remaining steps.
1. If |privateToken|["{{PrivateToken/issuers}}"] does not [=map/exists|exist=], then throw {{TypeError}}.
1. If |privateToken|["{{PrivateToken/issuers}}"] is [=list/empty=], then throw {{TypeError}}.
1. [=list/For each=] |issuer| of |privateToken|["{{PrivateToken/issuers}}"]:
1. Let |issuerURL| be the the result of running the [=URL parser=] on |issuer|.
1. If |issuerURL| is failure, then throw {{TypeError}}.
1. If |issuerURL|'s [=url/scheme=] is not an [=HTTP(S) scheme=], then throw {{TypeError}}.
1. Let |issuerOrigin| be |issuerURL|'s [=/origin=].
1. If |issuerOrigin| is not a [=potentially trustworthy origin=], then throw {{TypeError}}.
1. [=list/Append=] |issuerURL| to <var ignore>request</var>'s [=request/private token issuers=].
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 {{TokenVersion}} is currently set to `1`, as this is the only version that the
specification supports at this time.
<pre class=idl>
enum TokenVersion { "1" };
</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 {{PrivateToken}} contains the information required to make a fetch request.
<pre class=idl>
dictionary PrivateToken {
required TokenVersion version;
required OperationType operation;
RefreshPolicy refreshPolicy = "none";
sequence<USVString> issuers;
};
</pre>
This specification adds a new property to the {{RequestInit}} dictionary:
<pre class=idl>
partial dictionary RequestInit {
PrivateToken privateToken;
};
</pre>
Modifications to request {#fetch-request}
---------------------------------------------------------------
A [=/request=] has an associated <dfn for=request>private token refresh policy</dfn>, which is of type {{RefreshPolicy}} with default value of <code>"none"</code>.
A [=/request=] has an associated <dfn for=request>private token operation</dfn>, which is of type {{OperationType}}.
A [=/request=] has an associated <dfn for=request>private token issuers</dfn>, which is a list of strings.
Note: [=request/Private token refresh policy=] is ignored unless [=request/private token operation=] is <code>"token-redemption"</code>. [=request/Private token issuers=] is ignored unless [=request/private token operation=] is <code>"send-redemption-record"</code>. [=request/Private token issuers=] must be specified and non-empty when [=request/private token operation=] is <code>"send-redemption-record"</code>.
This specification defines two new [=policy-controlled features=]. Exactly one of these policy features applies for a given Private State Token operation.
The [=policy-controlled feature=] identified by "<dfn data-dfn-for="policy-controlled feature"><code>private-state-token-issuance</code></dfn>" applies for the <code>"token-request"</code> operation. The [=default allowlist=] for this feature is <code>["self"]</code>.
The [=policy-controlled feature=] identified by "<dfn data-dfn-for="policy-controlled feature"><code>private-state-token-redemption</code></dfn>" applies for the <code>"send-redemption-record"</code> and <code>"token-redemption"</code> operations. The [=default allowlist=] for this feature is <code>["self"]</code>.
A [=request=] has an associated <dfn for="request">pstPretokens</dfn>, which is null or a [=byte sequence=].
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>"):
Given a {{RequestInit}} |init| and a {{Request}} |request| run the following steps:
1. If |init|["{{RequestInit/privateToken}}"] [=map/exists=]:
1. Let |privateToken| be |init|["{{RequestInit/privateToken}}"].
1. Run [=set private token properties for request from private token=] on |privateToken| and |request|.
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. If |request|'s [=request/private token operation=] is null, abort remaining steps.
1. If |request|'s [=request/private token operation=] is <code>"token-request"</code>:
1. [=Append private state token issue request headers=] on |httpRequest|.
1. Abort the remaining steps.
1. If |request|'s [=request/private token operation=] is <code>"token-redemption"</code>:
1. [=Append private state token redemption request headers=] on |httpRequest|.
1. Abort the remaining steps.
1. [=Assert=]: |request|'s [=request/private token operation=] is <code>"send-redemption-record"</code>.
1. [=Append private state token redemption record headers=] on |httpRequest|.
Modifications to HTTP fetch steps {#http-fetch}
---------------------------------------------------------------
The specification adds the following steps to the [=HTTP fetch=] algorithm, before checking the redirect status (i.e. "7. If |actualResponse|'s status is a redirect status, ..."):
1. Let |issue response result| be the result of [=handle an issue response|handling an issue response=], given [=request=] |request| and [=response=] |actualResponse| as input.
1. If |issue response result| is a [=network error=], return |issue response result|.
1. Let |redeem response result| be the result of [=handle a redeem response|handling a redeem response=], given [=request=] |request| and [=response=] |actualResponse| as input.
1. If |redeem response result| is a [=network error=], return |issue response result|.
Integration with iframe {#iframe-integration}
====================================
privateToken content attribute for HTMLIframeElement {#iframe-private-token}
---------------------------------------------------------------
The <{iframe}> element contains a <dfn element-attr for="iframe">privateToken</dfn> <a spec=html>content attribute</a>. The IDL attribute {{HTMLIFrameElement/privateToken}} <a spec=html>reflects</a> the <{iframe/privateToken}> <a spec=html>content attribute</a>.
<pre class=idl>
partial interface HTMLIFrameElement {
[SecureContext] attribute DOMString privateToken;
};
</pre>
Modification to "create navigation params by fetching" steps {#navigation-params-modifications}
---------------------------------------------------------------
The following step is added to the <a href="https://html.spec.whatwg.org/multipage/browsing-the-web.html#create-navigation-params-by-fetching">create navigation params by fetching</a>, before step "25. Return a new <a href="https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigation-params">navigation params</a>, with ...":
1. If <var ignore=''>navigable</var>'s [=container=] is an <{iframe}> element, and if it has a <{iframe/privateToken}> <a spec=html>content attribute</a>, then run [=set private token properties for request from private token=] on <var ignore=''>navigable</var>'s <var ignore=''>privateToken</var> and |request|.
Integration with XMLHttpRequest {#xhr}
====================================
Attach PrivateToken {#xhr-set-private-token}
---------------------------------------------------------------
An {{XMLHttpRequest}} has an associated <dfn for="XMLHttpRequest">private state token</dfn>,
a {{PrivateToken}} object that specifies the {{OperationType}} to execute against the request.
<xmp class="idl">
partial interface XMLHttpRequest {
undefined setPrivateToken(PrivateToken privateToken);
};
</xmp>
The <dfn method for="XMLHttpRequest">setPrivateToken(PrivateToken privateToken)</dfn> method steps are:
<ol>
<li><p>If [=this=]'s <a spec="xhr">state</a> is not "opened", then throw an
"{{InvalidStateError}}" {{DOMException}}.
<li> If <a>this</a>'s
<a spec="xhr">`send()` flag</a> is set,
then [=throw=] an "{{InvalidStateError}}" {{DOMException}}.
<li> Set [=this=]'s [=XMLHttpRequest/private state token=] to |privateToken|.
</ol>
send() monkeypatch {#xhr-send-monkeypatch}
---------------------------------------------------------------
Modify {{XMLHttpRequest/send(body)}} as follows:
After the step:
> Let |req| be a new [=request=], initialized as follows...
Add the step:
1. Run [=set private token properties for request from private token=] with <a>this</a>'s [=XMLHttpRequest/private state token=] and |req|.
Issuing Protocol {#issuing-protocol}
====================================
This section explains the issuing protocol. It has two sections that explains
the issuing protocol steps for user agents and issuers.
Creating An Issue Request {#issue-request}
---------------------------------------------------------------
<div class=example id="example-issue-request">
An issue request is created and fetched as demonstrated in the following snippet.
```javascript
let issueRequest = new Request("https://example.issuer:1234/issuer_path", {
privateToken: {
version: 1,
operation: "token-request",
}
});
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 |issuerKeys| be the result of [=look up the key commitments|looking up the key commitments=] for |issuer|.
1. If |issuerKeys| is null, return.
1. Let |signingKey| be the result of [=look up the latest keys|looking up of the latest keys=] for |issuer|.
1. Run [=discard tokens=] with |issuer| and |signingKey|.
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 (|issueHeader|, |pretokens|) be the result of [=generate masked tokens|generating masked tokens=] with |issuerKeys| and |numTokens|.
1. Set |request|'s [=cache mode=] to `"no-store"`.
1. Set |request|'s [=pstPretokens=] to |pretokens|.
1. Let |base64EncodedTokens| be the base64-encoded [[!RFC4648]] version of |issueHeader|.
1. Let |cryptoProtocolVersion| 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>, |base64EncodedTokens|)
in |request|'s header list.
1. [=header list/Set a structured field value=] given (<a http-header>`Sec-Private-State-Token-Crypto-Version`</a>, |cryptoProtocolVersion|)
in |request|'s header list.
<div class=example id="example-pst-request-headers">
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-Crypto-Version: <cryptographic protocol version, VOPRF>
```
</div>
Issuer Signing Tokens {#issuer-signing-tokens}
----------------------------------------------
This section explains the signing of tokens that happens in the issuer
servers. VOPRF can only encode one of six values by the selection of which
key to use.
Using its private keys, issuer signs the masked tokens obtained in the
<a http-header>Sec-Private-State-Token</a> request header value with a value
dependent on other information passed as part of the issuance request. Issuer uses the cryptographic protocol
specified in the request <a http-header>Sec-Private-State-Token-Crypto-Version</a> header. Issuer returns
the signed tokens in the <a http-header>Sec-Private-State-Token</a> response header
value encoded as a base64 [[!RFC4648]] byte string.
<div class=example id="example-pst-response">
The following snippet displays a typical response demonstrating the Private State Token
header.
```
Sec-Private-State-Token: <token encoded as base64 string>
```
</div>
Handling Issue Responses {#issue-response}
----------------------------------------------------------
To <dfn>handle an issue response</dfn>, given [=request=] |request| and [=response=] |response|, run the following steps:
1. If |request|'s [=request/header list=] [=header list/contains|does not contain=] <a http-header>Sec-Private-State-Token</a>, return null.
1. If |response|'s [=response/header list=] [=header list/contains|does not contain=] <a http-header>Sec-Private-State-Token</a>, return a [=network error=].
1. Let |header| be the result of [=header list/get|getting=] <a http-header>Sec-Private-State-Token</a> from |response|'s [=response/header list=].
1. If |header| is empty, return.
1. [=header list/delete|Delete=] <a http-header>Sec-Private-State-Token</a> from |response|'s [=response/header list=].
1. Let |issuer| be |request|'s [=request/URL=]'s [=url/origin=].
1. Let |issuerKeys| be the result of [=look up the key commitments|looking up the key commitments=] for |issuer|.
1. If |issuerKeys| is null, return.
1. Let |pretokens| be |request|'s [=pstPretokens=].
1. If |pretokens| is null, return.
1. Let |rawResponse| be the base64-decoded [[!RFC4648]] version of |header|.
1. Let |unmasked tokens| be the result of [=unmask tokens|unmasking response tokens=] given |issuerKeys|, |pretokens|, and |rawResponse|.
1. If |unmasked tokens| is null, return a [=network error=].
1. Let |signingKey| be the result [=look up the latest keys|looking up the latest keys=] for |issuer|.
1. For each |token| in |unmasked tokens|, run the following steps:
1. [=Insert a token=] for |issuer|, |token|, and |signingKey|.
1. Return.
Redeeming Tokens {#redeeming-tokens}
====================================
When the user agent navigates to a top-level origin, this top-level origin or a third party site
embedded on the top level origin may redeem tokens stored in the user agent from a
specific issuer to learn the data encoded in the tokens.
<div class=example id="example-redemption-request">
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', {
privateToken: {
version: 1,
operation: 'token-redemption',
refreshPolicy: {'none', 'refresh'}
}
});
```
</div>
To <dfn>set redemption headers</dfn> with [=/request=] |request| and a [=RedeemRequest=] |record|:
1. Let |redemptionRequest| be the result of [=serialize protocol message|serializing protocol message=] |record|.
1. Let |cryptoProtocolVersion| 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>, |redemptionRequest|)
in |request|'s header list.
1. [=header list/Set a structured field value=] given (<a http-header>`Sec-Private-State-Token-Crypto-Version`</a>, |cryptoProtocolVersion|)
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. Set |request|'s [=cache mode=] to `"no-store"`.
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/private token refresh policy=] is `"none"`:
1. Let |record| be the result of performing [=retrieve a redemption record=] with |issuer| and |topLevel|.
1. If |record| is not null, [=set redemption headers=] with |request| and |record| and return.
1. Let |penultimateRedemption| be the result of [=look up penultimate redemption =] with |issuer| and |topLevel|
1. If |penultimateRedemption| 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|.
1. If |commitments| is null, return.
1. [=Discard tokens=] from |issuer| that are signed with keys other than those from
the issuer's most recent commitments.
1. Let |token| be the result of [=retrieve a token|retrieving a token=] for |issuer|.
1. If |token| is null, return.
1. Let |redeemRequest| be an empty [=RedeemRequest=].
1. Set |redeemRequest|["token"] to |token|.
1. [=Set redemption headers=] with |request| and |record|.
Handling Redeem Responses {#redeem-response}
----------------------------------------------------------
To <dfn>handle a redeem response</dfn>, given [=request=] |request| and [=response=] |response|, run the following steps:
1. If |request|'s [=request/header list=] [=header list/contains|does not contain=] <a http-header>Sec-Private-State-Token</a>, return null.
1. If |response|'s [=response/header list=] [=header list/contains|does not contain=] <a http-header>Sec-Private-State-Token</a>, return a [=network error=].
1. Let |rawHeader| be the result of [=header list/get|getting=] <a http-header>Sec-Private-State-Token</a> from |response|'s [=response/header list=].
1. If |rawHeader| is empty, return null.
1. Let |rawResponse| be the base64-decoded [[!RFC4648]] version of |rawHeader|.
1. Let |header| be the result of [=deserialize protocol message|deserializing protocol message=] |rawHeader| as a [=RedeemResponse=].
1. [=header list/delete|Delete=] <a http-header>Sec-Private-State-Token</a> from |response|'s [=response/header list=].
1. Set |lifetime| to be the largest representable duration.
1. If |response|'s [=response/header list=] [=header list/contains=] <a http-header>Sec-Private-State-Token-Lifetime</a> response header, set |lifetime| to that value.
1. [=header list/delete|Delete=] <a http-header>Sec-Private-State-Token-Lifetime</a> from |response|'s [=response/header list=].
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. Perform [=record redemption timestamp=] with |issuer| and |topLevel|.
1. Perform [=record a redemption record=] with |issuer|, |topLevel|, |header|, and |lifetime|.
Note: 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 user agent 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=]. User agents might choose to store these records
to include them in subsequent requests to the origins that can verify its
validity. An issuer might choose to include an 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=].
The Private State Tokens API provides a `'send-redemption-record'` operation to [=append private state token redemption record headers=]. This operation attaches a previously [=record a redemption record|recorded redemption record=] from [=handle a redeem response=].
To <dfn>append private state token redemption record headers</dfn> given a [=/request=] |request|, run the following steps:
1. If |request|'s [=request/client=] is not a [=secure context=], then abort these steps.
1. Let |topLevel| be |request|'s [=request/client=]'s [=environment/top-level origin=].
1. [=list/For each=] |issuer| of [=request/private token issuers=]:
1. Let |issuerURL| be the the result of running the [=URL parser=] on |issuer|.
1. If |issuerURL| is failure, then abort these steps.
1. If |issuerURL|'s [=url/scheme=] is not an [=HTTP(S) scheme=], then abort these steps.
1. Let |issuerOrigin| be |issuerURL|'s [=/origin=].
1. If |issuerOrigin| is not a [=potentially trustworthy origin=], then abort these steps.
1. Let |records_per_issuer| be a [=map=] where keys are USVString and values are [=redemption records=].
1. [=list/For each=] |issuer| of [=request/private token issuers=]:
1. Let |record| be the result of performing [=retrieve a redemption record=] with |issuer| and |topLevel|.
1. If |record| is null, then [=iteration/continue=].
1. [=map/Set=] |records_per_issuer|[|issuer|] to |record|.
1. If |records_per_issuer| is empty, then abort these steps.
1. Let |headerItems| be a structured headers list [[RFC8941]].
1. [=map/For each=] |issuer| -> |record| of |records_per_issuer|:
1. Let |serializedIssuer| be result of serializing |issuer|.
1. Let |serializedRecord| be result of serializing |record|.
1. [=list/Append=] |serializedIssuer| and |serializedRecord| pairs to |headerItems|.
1. Let |serializedHeaderItems| be result of serializing |headerItems|.
1. If |serializedHeaderItems| is null, then abort these steps.
1. Set <a http-header>`Sec-Redemption-Record`</a> to |serializedHeaderItems|.
<h3 id="the-document-object">Changes to {{Document}}</h3>
<pre class="idl">
partial interface Document {
Promise<boolean> hasPrivateToken(USVString issuer);
Promise<boolean> hasRedemptionRecord(USVString issuer);
};
</pre>
Query APIs {#query-apis}
========================
Token Query {#token-query}
--------------------------
When invoked on {{Document}} |doc| with {{USVString}} |issuer|, the <dfn export method for=Document><code>hasPrivateToken(issuer)</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. 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|. 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 user agent state. It associates the issuer
argument with the current origin. The specification allows at most 2 issuers associated
with an origin. This is to prevent leaking information through the issuers a
user has tokens from. Note that querying tokens triggers removal of stale tokens.
Redemption Record Query {#redemption-record-query}
--------------------------------------------------
When invoked on {{Document}} |doc| with {{USVString}} |issuer|, the <dfn export method for=Document><code>hasRedemptionRecord(issuer)</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. 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|. 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 user agent 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 the 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.
Clearing PST Data {#data-clear}
--------------------------
<a href="https://storage.spec.whatwg.org/#ui-guidelines">User interface guidance</a>
from <a href="https://storage.spec.whatwg.org">storage</a> standard should be
followed. User agents should provide interfaces to clear PST data from
storage.
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.
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=].
It is a [=Structured Header=] whose value MUST be an [=structured header/string=]
[[!RFC8941]].