Identity/AttachedServices/KeyServerProtocol: Difference between revisions

From MozillaWiki
Jump to navigation Jump to search
(lots of updates)
(move test vectors to their own section)
Line 13: Line 13:
* confirm this is actually implementable inside Firefox (especially w.r.t. NSS)
* confirm this is actually implementable inside Firefox (especially w.r.t. NSS)


The test vectors included on this page were produced by the python code in https://github.com/warner/picl-spec-crypto . The diagrams may lag behind the latest version of that code.


= Creating The Account =
= Creating The Account =
Line 83: Line 81:
09503006915442163037721228467470
09503006915442163037721228467470
35652616593381637186118123578112
35652616593381637186118123578112
The following examples use a non-ASCII email address of "andré@example.org" (with an accented "e", UTF8 encoding is 616e6472c3a9406578616d706c652e6f7267) and a non-ascii password of "pässwörd" (with accents on "a" and "o", UTF8 encoding is 70c3a4737377c3b67264). Given the password-stretching described earlier, this results in an srpPW of:
5b597db713ef1c05
67f8d053e9dde294
f917a0a838ddb661
a98a67a188bdf491


=== SRP Verifier Calculation ===
=== SRP Verifier Calculation ===
Line 98: Line 90:
[[File:PICL-IdPAuth-SRP-Verifier.png|client-side SRP Verifier calculation]]
[[File:PICL-IdPAuth-SRP-Verifier.png|client-side SRP Verifier calculation]]


Given the sample email and password above, the SRP Verifier calculation yields the following. (Note: the srpSalt is normally generated randomly, but for illustrative purposes, here we used fixed pre-calculated values).
Test vectors are provided at the end of this page. The protocol requires that several values ("a", "b", "srpSalt") are chosen randomly, but for illustrative purposes, the test vectors use carefully-crafted non-random values. For the user's sake, please ensure implementations use proper random values instead of simply copying the test vectors.
 
* srpSalt (hex string): 00f1000000000000 0000000000000000 0000000000000000 000000000000009b
* x (internal, as a hex string): ffd36e11f577d312892334810d55089cb96c39443c255a9d85874bb6df69a537
* x (internal, as decimal integer): 11571334079566
92128319718196619842967585739396
25477265918747447380376082294071
* v (internal, decimal integer): 710597
15947322363168818619231596014948
50266475387982152630667264830126
91363325468391002539838039127254
13731153916626297948231925131054
77620430120387238383382529286340
32606803605961340789655696705692
35971894130915251144385164054999
20023879039952438012163402227132
85297349371740668115032272229446
78351915275352511787735824142082
28132003206595132571178470786998
71417330468650192650539261877568
70781628009053137574167426864838
84981432162129791810924115157063
80745962226827721585324849766449
08876686423788254204401136102193
24427662561738518576134929894589
97367433462254526788221238212661
40913290180513540399852050747986
* srpVerifier (hex string):
00901a4e05a7986c fafe2c80993f6e21
847d38b8b9168065 149480722d008c9a
c5fe418d799d03c2 b1c26db2afcd4513
0a0601d310faa060 cc728888aba130a1
7d855773107ecc92 f31ea3a3838bc727
77fc26420ed59918 298583d15640b965
939dd6967e943bd6 ed846dbbb18885c7
4f6e9370e4eeecc4 c8e2a648850cf2ba
5baab18888b433c4 b0bd8891eeffe16c
c022a098284696bc 3a81e735a1a2a371
62f62b980879bbd4 03ae55548b9feeec
b18bf0740f0d078a 435fedb5324d630e
8a14fed435fbb5ea 4b6e94b8b129799d
2a0991671a67be34 149dc5e94a4a3d05
749fc3b9e1a53282 96b20a15348420be
d2f28d2558cb4099 f30be8a7240c9252
 


The client sends srpSalt and srpVerifier to the server when it creates the account. It will also re-compute the 'x' value (as an integer) during sign-in. The server will convert the srpVerifier string back into an integer ('v') for use during its own sign-in calculations.
The client sends srpSalt and srpVerifier to the server when it creates the account. It will also re-compute the 'x' value (as an integer) during sign-in. The server will convert the srpVerifier string back into an integer ('v') for use during its own sign-in calculations.
Line 151: Line 99:


[[File:PICL-IdPAuth-SRP-Server.png|server--side SRP]]
[[File:PICL-IdPAuth-SRP-Server.png|server--side SRP]]
* b (hex integer, normally random but pre-calculated for this example):
00f3000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000020
* b (as decimal integer):
1198277
66042000957856349411550091527197
08125226960544768993643095227800
29105361555030352774505625606029
71778328003253459733139844872578
33596964143417216389157582554932
02841499373672188315534280693274
23189198736863575142046053414928
39548731879043135718309539706489
29157321423483527294767988835942
53343430842313006332606344714480
99439808861069316482621424231409
08830704769167700098392968117727
43420990997238759832829219109897
32876428831985487823417312772399
92628295469389578458363237146486
38545526799188280210660508721582
00403102624831815596140094933216
29832845626116777080504444704039
04739431335617585333671378812960
* srpB (hex string):
00857f70b197a6f3 f79c4270a41c581d
62c7ec7fc554c797 481d4b4075b06be3
df7f4f189e71fbec 08d1bcff8c5e4f74
65256cba8a78b725 daa0b9bddcbbea43
d916067b12c59aaf 4a9cdad53e08e4a5
770ea72287987302 2c5f5f608eb94795
710a907e1b425080 688d9e7790ce0781
6e6b2cdb9ad2c18f 60a2a5feb91b6da3
92579c5eb1e36f42 5b85c34085b216b9
7c4a3f7ffeb887c8 78ce0152d8be66eb
9c7a51abbae3b3f6 56c6e56d95d3e148
a23af3e9aaa54c72 cde19b58bdcbfb34
b9eb7f6dcbcd86e2 7e6221f6d3da2517
255088f5e7c408b3 7d6765120134b719
86287225d781c49a e5436b89525e17eb
dcb8f3b7eb43163a cfb31c45a51a5267


Later, getToken2() will be called with the client's srpA string and its M1 key-confirmation string. srpA is combined with srpB to calculate the "u" integer. srpA is also turned into an integer and used to compute the shared-secret "S" integer. S is then used to compute the shared key "srpK", which is the output of the SRP process.
Later, getToken2() will be called with the client's srpA string and its M1 key-confirmation string. srpA is combined with srpB to calculate the "u" integer. srpA is also turned into an integer and used to compute the shared-secret "S" integer. S is then used to compute the shared key "srpK", which is the output of the SRP process.
Line 219: Line 111:


('''Again, it is critical that the client keep its "a" and "x" integers secret, both during and after the protocol run.''')
('''Again, it is critical that the client keep its "a" and "x" integers secret, both during and after the protocol run.''')
* a (hex integer, normally random but pre-calculated for this example):
00f2000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 000000000000115c
* a (as decimal integer):
1193346
47663227291363113405741243413916
43482736314616601220006703889414
28162541137108417166380088052095
43910927476491099816542561560345
50331133015255005622124012256352
06121987030570656676375703406470
63422988042473190059156975005813
46381864669664357382020200036915
26156674010218162984912976536206
14440782978764393137821956464627
16314542157937343986808167341567
89864323268060014089757606109012
50649711198896213496068605039486
22864591676298304745954690086093
75374681084741884719851454277570
80362211874088739962880012800917
05751238004976540634839106888223
63866455314898189520502368799907
19946264951520393624479315530076
* srpA (hex string)
00f2a357d7da7132 1be6c070fb3a5928
8cec951cb13e7645 1f8c466ab373626a
7272dc1484c79ea3 cd1ea32e57fa4665
2e6450aa61ac5ee7 eac7a8c06c28ab19
5ccbe57500062c50 1a15fbb23a7f71b2
35448326af5e51c0 63f167378c782137
93dbc54efb32f204 de753d7a6b3d826d
aaefc007d17862af 9b6a14e35f17f1eb
8b13c7b8ffa1f6f4 7b70d62bd0c351b4
7596b0b0abcba95c 2d731869ed6e4ec2
4ab90da8cb22e65d 256315ee84d8079b
4086d90c4e827b51 bb4e4d2d7b387da0
2e6b48904a3ba6d7 648a9bcdf3e9fc60
7cfba92f8eacae12 3ac45a79307cf3dd
281ed75a96c7de8f cd823f148dcc0634
9795f825fb029859 b963ab88320133de
* u (hex string)
610c6df1f495e429 8a2a59a0f5b00d47
ea2ed6ce2ccec8f7 ade158314a7bd794
* S (hex string)
009cc8da2f7a9501 5bc0091faa36d6ef
ff52c33b924353e1 1de1d8e738654d6f
6a481003acb17cae 2ba2d4ae3fea8431
4c940397640fce92 d9153dffb7f3bd29
cbdb49e4ff0d26c4 67061337fd370851
4e3039d24cb54dc4 6420426b0daf7724
63fe06eb1521c7b0 96c4eeb6e5f9f739
49dcc74bc91baab8 398aff6df6735da2
c9486a645a20f2d7 d8f455a2bd226f21
e127f23e202b21fd d4ef64dc1a6740b6
fcd2a6b032fcb393 a2b9d97506b6fb89
5585d29173cc0e89 c3b3077ffa31215d
b602b28364f81012 46ee9e8c47b63881
f3f867e67971825d f6a881d1142989ab
cd4abba9c27ae529 c31be53f69966ccb
81f7660f95d5f8fc 45d052df3bcbb761


[[File:PICL-IdPAuth-SRP-Client2.png|client-side SRP 2]]
[[File:PICL-IdPAuth-SRP-Client2.png|client-side SRP 2]]
Line 301: Line 117:


Both client and server also hash "S" into "srpK". This is the shared session key, from which specific message encryption and MAC keys are derived (as described below).
Both client and server also hash "S" into "srpK". This is the shared session key, from which specific message encryption and MAC keys are derived (as described below).
* M1 (hex string):
182ff26523922c52
559cab3cdfc89a74
c986b1d7504ea53d
11d9a204fc54449d
* srpK (hex string):
78a36d3e0df089e7
29a98dee3290fc49
64cd6ec96b771d6a
bb6efe9181be868b


=== SRP Notes ===
=== SRP Notes ===
Line 382: Line 187:
resetAccount() needs request confidentiality, since the arguments include the newly wrapped kB value and the new SRP verifier, both of which enable a brute-force attack against the password. HAWK provides request integrity. The response is a single "ok" or "fail", conveyed by the HTTP headers, so we do not require response confidentiality, and can live without response integrity.
resetAccount() needs request confidentiality, since the arguments include the newly wrapped kB value and the new SRP verifier, both of which enable a brute-force attack against the password. HAWK provides request integrity. The response is a single "ok" or "fail", conveyed by the HTTP headers, so we do not require response confidentiality, and can live without response integrity.


So the single-use resetToken is used to derive three values (shown with their sample values):
So the single-use resetToken is used to derive three values:


* tokenID: 52437066aae511d3 3709bf25dc6a682a 7e943d49d94c84b3 4e1e6b11c9913159
* tokenID
* request HMAC key: 7de6c9b102dac62f 81d3a09baa00523d e7170ff17238b3af 8491e4cfb23e1a88
* request HMAC key
* request XOR key: 82d447f095aa8023 3eb5cb5d6c4eea25 5857809b6326b6bd 55fab2d3498b1cf8 a31bb0e319d7c0dc 2792740a480c1a98 99c1a6328bc2066e 3ecc9e8079ae8af6 046f15f3a586bfb3 b9908de7cd60b504 44fdfacc3cf32e2b efc72fca9063e28d a815989f86223394 b89db34bffdc94bb 68c05a49d1f1f63a 2c463d335a06c007
* request XOR key




[[File:PICL-IdPAuth-resetAccount.png|Deriving the resetAccount Keys]]
[[File:PICL-IdPAuth-resetAccount.png|Deriving the resetAccount Keys]]


The request data will contain wrap(kB), a new (randomly-generated) SRP salt, and the new SRP verifier, all concatenated together. Since we always pad the SRP verifier to the full (256-byte) group length, all four pieces are fixed-length. We generate enough reqXORkey bytes to cover all four values. In our example, the concatenated plaintext is as follows:
The request data will contain wrap(kB), a new (randomly-generated) SRP salt, and the new SRP verifier, all concatenated together. Since we always pad the SRP verifier to the full (256-byte) group length, all four pieces are fixed-length. We generate enough reqXORkey bytes to cover all four values.
 
4041424344454647
48494a4b4c4d4e4f
5051525354555657
58595a5b5c5d5e5f
a0a1a2a3a4a5a6a7
a8a9aaabacadaeaf
b0b1b2b3b4b5b6b7
b8b9babbbcbdbebf
c0c1c2c3c4c5c6c7
c8c9cacbcccdcecf
d0d1d2d3d4d5d6d7
d8d9dadbdcdddedf
e0e1e2e3e4e5e6e7
e8e9eaebecedeeef
f0f1f2f3f4f5f6f7
f8f9fafbfcfdfeff


The request data is XORed with requestXORkey, then delivered in the body of a HAWK request that uses tokenID as credentials.id and requestHMACkey as credentials.key . Note: it is critical to include the request body in the HAWK integrity check (options.payload=true, on both client and server), otherwise a man-in-the-middle could substitute their own SRP verifier, giving them control over the account (access to the user's class-A data, and a brute-force attack on their password).
The request data is XORed with requestXORkey, then delivered in the body of a HAWK request that uses tokenID as credentials.id and requestHMACkey as credentials.key . Note: it is critical to include the request body in the HAWK integrity check (options.payload=true, on both client and server), otherwise a man-in-the-middle could substitute their own SRP verifier, giving them control over the account (access to the user's class-A data, and a brute-force attack on their password).


[[File:PICL-IdPAuth-encryptResetAccount.png|Encrypting the resetAccount Request]]
[[File:PICL-IdPAuth-encryptResetAccount.png|Encrypting the resetAccount Request]]
In our example, the XORed ciphertext is:
c29505b3d1efc664
76fc81162003a46a
0806d2c83773e0ea
0da3e88815d642a7
03ba1240bd72667b
8f3bdea1e4a1b437
297014813f77b0d9
8675243bc5133449
c4aed73061437974
7159472c01ad7bcb
942c281fe826f8fc
371ef5114cbe3c52
48f47a7c62c7d573
507459a013317a54
9831a8ba250400cd
d4bfc7c8a6fb3ef8


Clients might use resetAccount for two reasons:
Clients might use resetAccount for two reasons:
Line 512: Line 280:
* kB: the master key for data stored as "class-B", a 32-byte binary string.
* kB: the master key for data stored as "class-B", a 32-byte binary string.
* wrap(kB): an encrypted copy of kB. The keyserver stores wrap(kB) and never sees kB itself. The client (browser) uses a key derived from the user's password to decrypt wrap(kB), obtaining the real kB.
* wrap(kB): an encrypted copy of kB. The keyserver stores wrap(kB) and never sees kB itself. The client (browser) uses a key derived from the user's password to decrypt wrap(kB), obtaining the real kB.
= Test Vectors =
The following example uses a non-ASCII email address of "andré@example.org" (with an accented "e", UTF8 encoding is 616e6472c3a9406578616d706c652e6f7267) and a non-ascii password of "pässwörd" (with accents on "a" and "o", UTF8 encoding is 70c3a4737377c3b67264).
These test vectors were produced by the python code in https://github.com/warner/picl-spec-crypto . The diagrams may lag behind the latest version of that code.
== stretch-KDF ==
email:
616e6472c3a94065
78616d706c652e6f
7267
password:
70c3a4737377c3b6
7264
K1:
f84913e3d8e6d624
689d0a3e9678ac8d
cc79d2c2f3d96414
88cd9d6ef6cd83dd
K2:
5b82f146a6412692
3e4167a0350bb181
feba61f63cb17140
12b19cb0be0119c5
stretchedPW:
c16d46c31bee242c
b31f916e9e38d60b
76431d3f5304549c
c75ae4bc20c7108c
== main-KDF ==
mainSalt (normally random):
0001020304050607
08090a0b0c0d0e0f
1011121314151617
18191a1b1c1d1e1f
srpPW:
1ea1cdd3ba3bb3a8
b6c46331123f48a6
746f143014f5a389
24e6fea4dc1c1289
unwrapBKey:
94995fc5423827df
42d598076eccd996
656183a309e9fbaf
e5026431d338b115
internal x (base 10):
137598577746
20950182695987769526622297924214
84665105182182372602062190994896
internal x (hex):
030ac7c51717e1d5
35d59725cb7c49fb
4936b7db7fcd0f10
17d3f1ffef50e5d0
v (verifier as number) (base 10):
914205
81735470597826353382993806239740
11269014547415710721123623779303
40577934966702279822019429237166
85777414275311785862734740227831
07992726500267571929376592901913
18064022616547283873724124275736
15772870036501836242138681573039
79134196479839195060563726666145
91886023021879280485567507056439
54248700318708010771386921629212
79420638327471362434416724355461
72934544873168338790005382928033
88728242355654245060876357499918
14028809240099280481407481230320
68269674162426430261934197103097
99968937656431136067890224663385
48685839318699687268537410286424
14469699859895598445302042986990
86071206600454059861610331587789
== SRP Verifier ==
k (base 10):
259003859907
09503006915442163037721228467470
35652616593381637186118123578112
srpSalt (normally random):
00f1000000000000
0000000000000000
0000000000000000
0000000000000070
srpVerifier:
00b9648c840be3e4 5ae305640dc24c64
ee3a1fb083bcafe4 0e10b37d04ea5a55
05c7538f6a72a6b9 748c97b2fd6d4dc4
89a2cbee5ae2ea9d cb7f2dbe1ad99518
75029ebb7e2f2bd5 bce1a619038092f4
2ea4ccba99665bc4 fd6c3c393d961b1c
a2b8f61da5a81c2c cfdf89d28bc256fb
b201b79908f64613 4a41fd1ae451f62f
ccb00809b5ef8b05 7f198296e5aad231
baf321487d6abfed 2070556097720d5e
f48d45724749c7a8 73768238bbd01123
0d004d5d487cc6ac ee40e6ab13a33f64
bd702d5c754167f2 230bb1d15151c070
7ef25d2787727424 32ea0537e95c1a04
d3006d10d99a1c7d 3318d284dc92460d
84dae38b4b698433 61008de94bd744cd
== SRP B ==
private b (normally random) (base 10):
1198277
66042000957856349411550091527197
08125226960544768993643095227800
29105361555030352774505625606029
71778328003253459733139844872578
33596964143417216389157582554932
02841499373672188315534280693274
23189198736863575142046053414928
39548731879043135718309539706489
29157321423483527294767988835942
53343430842313006332606344714480
99439808861069316482621424231409
08830704769167700098392968117727
43420990997238759832829219109897
32876428831985487823417312772399
92628295469389578458363237146486
38545526799188280210660508721582
00403102624831815596140094933216
29832845626116777080504444704039
04739431335617585333671378812933
private b (hex):
00f3000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000005
transmitted srpB:
00de219f6b48de47 bb8a20e450d50cef
10c9c9fbc80482c2 9792f89f9bbfd27f
a0e082209f919128 e29a8ffadfdcb48b
e0000fc447d05afd 59b6032581de5596
8ff5f39034fc1aea d033b246660e4257
44fadcb824e7a14e fa6d2fc57502b9a7
b9cef2935a54c2c8 9d24589f1aa9091b
5f2981096e936592 dbbe0adfcb9b97f2
e677f5cb2112d90b 802af7df98eb29c5
31556f62d473e84e 50f70ac6d89e0503
228fb27eaca19f40 03f28516fb8b46c4
122510a9557c6d24 65bb13579e8ddbe5
7aa842d8ccd956f3 5643f43a4da35920
2485e21a6fede4a9 b3d55ee48eab9572
56f75283aed2c06a 9eb03f3feb29cb3b
6dbf644bcd8088cf 777072eb8b6b870e
== SRP A ==
private a (normally random) (base 10):
1193346
47663227291363113405741243413916
43482736314616601220006703889414
28162541137108417166380088052095
43910927476491099816542561560345
50331133015255005622124012256352
06121987030570656676375703406470
63422988042473190059156975005813
46381864669664357382020200036915
26156674010218162984912976536206
14440782978764393137821956464627
16314542157937343986808167341567
89864323268060014089757606109012
50649711198896213496068605039486
22864591676298304745954690086093
75374681084741884719851454277570
80362211874088739962880012800917
05751238004976540634839106888223
63866455314898189520502368799907
19946264951520393624479315528231
private a (hex):
00f2000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000a27
transmitted srpA:
00e1cad4005422e9 f1bf68ba186c75ed
73f199ec71c2cdf0 e74baa258f9e594a
ab48c2d74f064e45 57cfbf05fa0a1c17
491ae8a0568f8f40 04a8529414c6d124
7bf4fe98d0596792 3997eb362fc0cb47
39fde90abe8623d2 38d80003bb6a66e0
987748a329596d54 a2514d50d426e2ac
00eb4c7939977bc1 535625c19e34db08
0a8742af30f3f975 bf668097d0c303c5
ad6408729dad779e ca0ebcb8688bff01
e7781510504fdd4a 5f3b7806353378b2
e98fa90ab9224d7a 9973becafd8b428a
9d8e64bc79cd505c 2f976adacc9947f3
0b1e819f3b958b18 be8f3d2c84726faa
23bdae8e74ecda07 d279f75b25cb9c33
274f42fe038c3d5b 058b2877eb60841c
== SRP key-agreement ==
u:
802ae3bc2a4f3117 77e24280b08ceda0
99d46ba99a65a750 e771229efca2aea3
S:
00516c2f83d5602c 17547fba0c6f3171
bc2cb6462670ec08 f8a0d7bc46eb015b
f40dfc06c5be9492 40628605bf2bf598
3ae15679cc7968eb a15249e96f561d47
f6b7d8b43e9ebaeb 127daa2536d52c05
d412c10ea2485fd0 bb90d716a9d36163
83fd3fbcc7981c3b 769bfb5c42244e83
8cfb9fbaf0f37e6b 3336e2af42c6e615
90a7be399352a2a8 b47d85437ffb0859
9f821993ef05328f d07470ba1db050bc
d588cc10a6e8dd68 e61d787b5e4a0634
5d5e90ee60c5ab95 329de9526c5684bf
5dad4c75450885a3 249709d37e0ba85e
67ee644ca01ee1de 9ebe3b730a6dd188
6300abae80ebeb68 f0e141cb71a861c7
d42f50d1723fbe0b 78b46305dffb3b90
M1:
f8c82e57d1771a24
229c05858cc03bee
ea3a7b73d39939d5
a3a8dbbc9dc474f2
srpK:
94ad3e71e29ceb1f
2ed2b80996314344
6cfa5d8640c271dd
b632f094f7eda7c7
== getSignToken request ==
srpK:
94ad3e71e29ceb1f
2ed2b80996314344
6cfa5d8640c271dd
b632f094f7eda7c7
respHMACkey:
001d14a524e7e7f0
1de527ad01dddce0
e64f915dca46242a
7795397d98cbbb16
respXORkey:
e51958994bf03d02
f0651338ea18a186
7f2bb49089000a88
d367770bd9696b86
99c25804ff3ade0d
08622bd66b5b1332
4ce14f315a6dca6c
20a8b49e3743db31
8fb2670ba4b2d10f
416f61dd4eb7bf53
6a233cae88636a19
078213557e583622
plaintext:
2021222324252627
28292a2b2c2d2e2f
3031323334353637
38393a3b3c3d3e3f
4041424344454647
48494a4b4c4d4e4f
5051525354555657
58595a5b5c5d5e5f
6061626364656667
68696a6b6c6d6e6f
7071727374757677
78797a7b7c7d7e7f
ciphertext:
c5387aba6fd51b25
d84c3913c6358fa9
4f1a86a3bd353cbf
eb5e4d30e55455b9
d9831a47bb7f984a
402b619d27165d7d
1cb01d620e389c3b
78f1eec56b1e856e
efd30568c0d7b768
29060bb622dad13c
1a524eddfc161c6e
7ffb692e0225485d
MAC:
40e3dd0d0b299033
a31222ceb3504ad4
7e55fc05f8b94402
2ed9e2be5c4be3e3
response:
c5387aba6fd51b25
d84c3913c6358fa9
4f1a86a3bd353cbf
eb5e4d30e55455b9
d9831a47bb7f984a
402b619d27165d7d
1cb01d620e389c3b
78f1eec56b1e856e
efd30568c0d7b768
29060bb622dad13c
1a524eddfc161c6e
7ffb692e0225485d
40e3dd0d0b299033
a31222ceb3504ad4
7e55fc05f8b94402
2ed9e2be5c4be3e3
== signCertificate ==
signToken:
6061626364656667
68696a6b6c6d6e6f
7071727374757677
78797a7b7c7d7e7f
tokenID:
8b5ff98850a2c98a
8059ee891c15b9a6
1af08356f54d865c
39f95f048d185195
reqHMACkey:
85f9aa22e9b35557
3504cd0e934a6c2b
1837fe6ca70d4932
627ba1c02b9aebc2
== resetAccount ==
resetToken:
8081828384858687
88898a8b8c8d8e8f
9091929394959697
98999a9b9c9d9e9f
tokenID:
52437066aae511d3
3709bf25dc6a682a
7e943d49d94c84b3
4e1e6b11c9913159
reqHMACkey:
7de6c9b102dac62f
81d3a09baa00523d
e7170ff17238b3af
8491e4cfb23e1a88
reqXORkey:
82d447f095aa8023
3eb5cb5d6c4eea25
5857809b6326b6bd
55fab2d3498b1cf8
a31bb0e319d7c0dc
2792740a480c1a98
99c1a6328bc2066e
3ecc9e8079ae8af6
046f15f3a586bfb3
b9908de7cd60b504
44fdfacc3cf32e2b
efc72fca9063e28d
a815989f86223394
b89db34bffdc94bb
68c05a49d1f1f63a
2c463d335a06c007
plaintext:
4041424344454647
48494a4b4c4d4e4f
5051525354555657
58595a5b5c5d5e5f
a0a1a2a3a4a5a6a7
a8a9aaabacadaeaf
b0b1b2b3b4b5b6b7
b8b9babbbcbdbebf
c0c1c2c3c4c5c6c7
c8c9cacbcccdcecf
d0d1d2d3d4d5d6d7
d8d9dadbdcdddedf
e0e1e2e3e4e5e6e7
e8e9eaebecedeeef
f0f1f2f3f4f5f6f7
f8f9fafbfcfdfeff
ciphertext:
c29505b3d1efc664
76fc81162003a46a
0806d2c83773e0ea
0da3e88815d642a7
03ba1240bd72667b
8f3bdea1e4a1b437
297014813f77b0d9
8675243bc5133449
c4aed73061437974
7159472c01ad7bcb
942c281fe826f8fc
371ef5114cbe3c52
48f47a7c62c7d573
507459a013317a54
9831a8ba250400cd
d4bfc7c8a6fb3ef8

Revision as of 07:31, 10 July 2013

PiCL Key Server / IdP Protocol

NOTE: This specification is under active development (27-Jun-2013). Several pieces are not yet complete. If you write any code based on this design, keep a close eye on this page and/or contact me (warner) on the #picl IRC channel to learn about changes. Eventually this will be nailed down and should serve as a stable spec for the PICL keyserver/IdP protocol.

Remaining TODO items:

  • define client-side key-stretching (PBKDF2+scrypt+PBKDF2)
  • finalize SRP questions (definition of M1, generation of a/b)
  • finalize how getToken2() declares whether a signToken or a resetToken is desired
  • provide test vectors for decrypting a resetToken
  • finalize proof-of-work/DoS-prevention details
  • define how wrap(kB) is unwrapped to get kB
  • confirm this is actually implementable inside Firefox (especially w.r.t. NSS)


Creating The Account

The first act performed by a user is to create the account. They enter email+password into their browser, which then does the following steps:

  • decide upon stretching parameters (perhaps consulting the keyserver for recommendations, but imposing a minimum strength requirement). For now we used a fixed {firstPBKDF:20000, scrypt: {N:65536, r:8, p:1}, secondPBKDF:20000}.
  • randomly choose a 32-byte mainSalt (this should be unique, but is not secret)
  • choose the SRP group parameters (fixed: use the 2048-bit group described below)
  • perform key-stretching (as described below), derive stretchedPW and srpPW
  • randomly choose a 32-byte srpSalt (unique, but not secret)
  • create srpVerifier from srpPW and srpSalt (as described below)
  • deliver (email, stretchParams, mainSalt, srpParams, srpSalt) to the keyserver's createAccount() API

To limit abuse, the createAccount() should also require a fresh "createToken". This should be created by some other API, outside the scope of this document, that perhaps requires a CAPTCHA or something. createAccount() might also require a proof-of-work token, as described below.

The server, when creating a new account, creates both kA and wrap(kB) as randomly-generated 256-bit (32-byte) strings. It stores these, along with all the remaining values, indexed by email, in the account table where they can be retrieved by getToken later.

After creating the account, the client immediately runs getToken("sign"), as described below, to fetch kA and wrap(kB). It then unwraps wrap(kB) by XORing it with wrapKey to obtain kB.


Email+Password -> SignToken/ResetToken

To connect a browser to an existing account, we use the following "getSignToken" protocol to transform an email+password pair into (kA, kB, signToken). "kA" and "kB" enable the browser to encrypt/decrypt synchronized data records, while "signToken" is used to convince the storage server to accept read and write requests for those records.

This protocol starts by using key-stretching to transform the email+password into a "stretchedPW", then feeds this into an SRP protocol to get a session key. It uses this session key to decrypt a bundle of encrypted data from the keyserver, resulting in three values: kA, wrap(kB), and the signToken. The stretchedPW is also used to derive the key that will decrypt wrap(kB) into the actual kB value.

IdP Auth Big Picture

This same protocol is used, with slightly different methods and constants, to obtain the "resetToken". This token allows a client to safely reset the account password.

The protocol is optimized to minimize round-trips and to enable parallelism, to reduce the time it takes to connect a browser to the account to just a few seconds. As a result, the two messages it sends (getToken1 and getToken2) each perform multiple jobs.

getToken1

As soon as the user finishes typing in the email address, the client should send it in the "getToken1" message to the keyserver. The response will include a set of parameters that are needed for key-stretching (described below), and the common parameters used by both sides of the SRP protocol to follow. These are simply looked up in a database entry for the client, along with an account-id. It must also include an allocated session-id that is used to associate this request with the subsequent getToken2 request. Finally, the response also includes the server's contribution to the SRP protocol ("srpB"), which is calculated on the server based upon a random value that it remembers in the session.

To mitigate DoS abuse, getToken1() may also require a proof-of-work string, described below.

Client-Side Key Stretching

"Key Stretching" is the practice of running a password through a computationally-expensive one-way function before using it for encryption or authentication. The goal is to make brute-force dictionary attacks more expensive, by raising the cost of testing each guess.

To protect the user's class-B data against compromises of our keyserver, we perform this key stretching on the client, rather than on the server. We use the memory-hard "scrypt" function (pronounced "ess-crypt") for this purpose, as motivated by the attacker-cost studies in Identity/CryptoIdeas/01-PBKDF-scrypt. Slower (low-RAM) devices can still participate by outsourcing the scrypt step to an "scrypt helper". To provide at least minimal protection against this helper, we sandwich the scrypt step between two PBKDF2 steps. The complete protocol looks like this:

Stretching KDF

Our initial parameter choices for this stretching are to use 20000 iterations of PBKDF2 on each side (40k in all), and to run scrypt with N/r/p set to 64k/8/1. This requires roughly 60MB of RAM for about 1.3 seconds on a 1.8GHz Intel Atom linux box (it will run faster, but use the same memory, on more modern CPUs). We are still studying performance on various platforms to decide what parameters to use in V1: lowering them speeds up the login process, but reduces security (by reducing the cost of an dictionary attack).

Since the stretching is expected to take a second or two, the client can optimistically start this process (using default parameters) before receiving the getToken1() response, and then check that it used the right parameters afterwards (repeating the operation if not). (We'll want to build the stretching function with periodic checkpoints so that we don't have to lose all progress if the parameters turn out to be wrong). The "mainSalt is added *after* the stretching, to enable this parallelism (at a tiny cost in security).

After "stretchedPW" is derived, a second HKDF call is used to derive "srpPW" and "unwrapBKey" which will be used later.

masterKey KDF

SRP Protocol Details

The PiCL client uses the SRP protocol (http://srp.stanford.edu/) to prove that it knows the (stretched) account password without revealing the actual password (or information enabling a brute-force attack) to the server or any eavesdroppers.

SRP is somewhat underspecified. We use SRP-6a, with SHA256 as the hash, and the 2048-bit modulus defined in RFC 5053 Appendix A. We consistently zero-pad all string values to 256 bytes (2048 bits), and use H(A+B+S) as the key-confirmation message "M1". These details, plus the SRP design papers and RFCs 2945 and 5054, should be enough to build a compatible implementation. The diagrams below are annotated with test vectors to verify compatibility.

The server should use Jed's SRP module from https://github.com/jedp/node-srp . The client might use SJCL (http://crypto.stanford.edu/sjcl/) or native code (NSS).

The basic idea is that we're using the main-KDF output "srpPW" as a password for the SRP calculation. We use the email address for "identity", and a server-provided string for "salt". (We could safely leave them blank, since equivalent values are already folded into the password-stretching process, but it's less confusing to follow the SRP spec and fill them in with something sensible).

Note that SRP-6a uses a "k" value which basically encodes the group being used ("N" and "g"). Since all PICL accounts use the same 2048-bit group, they will all use the same "k" value (not to be confused with the per-session shared-secret "K" key that emerges from the protocol). This group's "k" integer is (as a base-10 number): 259003859907 09503006915442163037721228467470 35652616593381637186118123578112

SRP Verifier Calculation

When the client first creates the account, it must combine the account email address, the stretched password (srpPW), and a randomly-generated srpSalt, to compute the srpVerifier. The server will use this verifier later, to check whether or not the client really knows the password.

If the server is compromised and an attacker learns the srpVerifier for a given account, they cannot use this to directly log in (the verifier is not "password-equivalent"), but it does allow them to perform an offline brute-force attack against the user's password. In this respect, it is similar to a traditional hashed password. We make these attacks somewhat more expensive by performing the client-side stretching described above, instead of using the raw user password in the SRP calculation.

client-side SRP Verifier calculation

Test vectors are provided at the end of this page. The protocol requires that several values ("a", "b", "srpSalt") are chosen randomly, but for illustrative purposes, the test vectors use carefully-crafted non-random values. For the user's sake, please ensure implementations use proper random values instead of simply copying the test vectors.

The client sends srpSalt and srpVerifier to the server when it creates the account. It will also re-compute the 'x' value (as an integer) during sign-in. The server will convert the srpVerifier string back into an integer ('v') for use during its own sign-in calculations.

SRP Server-side Sign-In Flow

When the user connects a new device to their account, they use the getToken1() API to start the SRP protocol. This sends the account email address to the server. The server looks up the stored srpVerifier for this account, creates a random 'b' integer, performs some math to compute the "B" number, then converts B into a string known as "srpB". "srpB" is returned to the client, along with srpSalt and the key-stretching parameters. "b" and "srpB" are retained for the subsequent getToken2() call. Note that it is critical that the "b" integer remain secret on the server.

server--side SRP

Later, getToken2() will be called with the client's srpA string and its M1 key-confirmation string. srpA is combined with srpB to calculate the "u" integer. srpA is also turned into an integer and used to compute the shared-secret "S" integer. S is then used to compute the shared key "srpK", which is the output of the SRP process.

SRP Client Calculation

While the client is waiting for the response to getToken1(), it begins its key-stretching calculations. Everything else must wait until the response to getToken1() arrives, which includes the key-stretching parameters (which are retroactively confirmed), srpSalt, and the server's generated srpB value.

client-side SRP 1

Once the client knows srpSalt, it computes the same "x" integer as it did in the middle of the srpVerifier calculation. It also converts srpB into an integer named "B". Then it creates a random "a" integer, uses it to compute the string "srpA", then combines srpA with the server's srpB to compute the "u" integer. It then combines the static "k", the password-derived "x", the combined "u", and the server's "B", together with some magic math, to derive the "S" integer. If everything went well, the client will compute the same "S" value as the server did. If not (the password was wrong, or the client is talking to a fake server that doesn't really know srpVerifier), then the two "S" values will not match.

(Again, it is critical that the client keep its "a" and "x" integers secret, both during and after the protocol run.)

client-side SRP 2

To safely tell if the "S" values match, both client and server combine srpA, srpB, and their (independently) generated "S" strings to form a string named "M1". The client sends M1 (along with srpA) in the getToken2() message. The server compares the client's copy of M1 against its own. If they match, the client knew the password and the server can safely respond with the encrypted account data. If they do not match, the client (or a man-in-the-middle attacker) did not know the password, and the client should increment a counter that can trigger defenses against online guessing attacks.

Both client and server also hash "S" into "srpK". This is the shared session key, from which specific message encryption and MAC keys are derived (as described below).

SRP Notes

The SRP "g" (generator) and "N" (prime modulus) should use the 2048-bit value from RFC 5054 Appendix A. Clients should not accept arbitrary g/N values (to protect against small primes, non-primes, and non-generators). In the future we might allow alternate parameter sets, in which case the server's first response should indicate which parameter set to use.

There are several places in SRP where integers (usually in the range 1..N-1) are converted into bytestrings, either for transmission over a wire, or to be passed into a hash function. The SRP spec is somewhat ambiguous about padding here: if the integer happens to be less than 2^2040, the simplest toString() approach will yield a 255 byte string, not a 256 byte string. PiCL consistently uses padding, so compatible implementations must prepend one or more NUL bytes to these short strings before transmission or hashing. The examples above were brute-forced to ensure that "srpVerifier", "srpA", "srpB", and "S" all wind up with leading zeros, to exercise the padding code in compatible implementations. If you are having problems getting your code to match these results, add some assertions to test that the stringified integers being put into hashes are exactly 256 bytes long.

The client does its entire SRP calculation in a single step, after receiving the server's "B" value. It creates its "A" value, computes the shared secret S, and the proof-of-knowledge M1. It sends both "A" and "M1" in the same message (getToken2).

The server receives "A" in getToken2, computes the shared secret "S", computes M1, checks that the client's M1 is correct, then derives the shared session key K. It then allocates a token (of the requested type) and encrypts kA+wrap(kB)+token as described below, returning the encrypted/MACed bundle in the response to getToken2.

Outstanding crypto questions:

  • How exactly should the "a" and "b" integers be generated? The issue is of how much bias SRP can tolerate. Ideally these integers are uniformly distributed from 1 to N-1 (inclusive). The only way to obtain a purely uniform distribution from a source of random bytes is try-try-again: pick a (integral-number-of-bytes) number, compare it to the desired range, try again if it falls outside the range. If you get unlucky, this can take a lot of guesses, depending upon how close the range is to a power of two. ECDSA implementations tend to pick a number twice as long as the modulus and then modulo it down (rand(2^4096) % N), which yields a tiny fraction of a bit of bias. The cheaper approach is to do the same with a number of equal length (rand(2^2048) % N), which imposes more bias. Some SRP implementations appear to be satisfied by rand(2^256). We need more review here.
  • The original SRP papers defined M1=H(A+B+S) as we use here, but other implementation (in particular RFC2945) uses a curious construct that includes the username, the salt, and an odd XOR combination of N and g. We need to decide what to use for M1.

getToken2

This method has two flavors, one for obtaining "signing tokens", the other for getting "reset tokens". TBD: either we'll have two different method names / API endpoints (getToken2Sign and getToken2Reset), or we'll pass an argument to a single "getToken2" method that indicates either "sign" or "reset". (using different endpoints would make it easier to monitor server load).

The client-side SRP calculation results in two values that are sent to the server in the "getToken2()" message: "srpA" and "srpM1". "A" is the client's contribution to the SRP protocol. "M1" is an output of this protocol, and proves (to the server) that this client knew the right password.

The server feeds "A" into its own SRP calculation and derives (hopefully) the same "S" value as the client did. It can then compute its own copy of M1 and see if it matches. If not, the client (or a man-in-the-middle) did not get the right password, and the server will return an error and increment it's "somebody is trying to guess passwords" counter (which will be used to trigger defenses against online guessing attacks). If it does match, then both sides can derive the same "K" session key.

The server then allocates a token for this device, and encrypts kA/wrap(kB)/token with the session key. The server returns a success message with the encrypted bundle.

Future variants (e.g. to fetch a third kind of token) might put additional values in the response to getToken2.

Decrypting the getToken2 Response

The SRP session key ("srpK") is used to derive two other keys: respHMACkey and respXORkey.

Decrypting the signToken 1

The respXORkey is used to encrypt the concatenated kA/wrap(kB)/token string, by simply XORing the two. This ciphertext is then protected by a MAC, using HMAC-SHA256, keyed by respHMACkey. The MAC is appended to the ciphertext, and the whole bundle is returned to the client.

Decrypting the signToken 2

The client recomputes the MAC, compares it (throwing an error if it doesn't match), extracts the ciphertext, XORs it with the derived respXORkey, then splits it into the separate kA/wrap(kB)/token values.

Since the kA/wrap(kB)/signToken response is so similar to the kA/wrap(kB)/resetToken response, the same code can be used to check+decrypt both. However remember that the respXORkey/respHMACkey is derived differently for each (using different "context" values).

Unwrapping kB

The server-provided wrap(kB) value is simply XORed with the password-derived wrapKey (both are 32-byte strings) to obtain kB. There is no MAC on wrap(kB).

Signing Certificates

The Sign Token is used to derive two values:

  • tokenID
  • request HMAC key


Deriving Keys to Sign the Certificate

The requestHMACkey is used in a HAWK (https://github.com/hueniverse/hawk/) request to provide integrity over the "signCertificate" request. It is used as credentials.key, while tokenID is used as credentials.id . HAWK includes the URL and the HTTP method ("POST") in the HMAC-protected data, and will optionally include the HTTP request body (payload) if requested.

For signCertificate(), it is critical to enable payload verification by setting options.payload=true (on both client and server). Otherwise a man-in-the-middle could submit their own public key, get it signed, and then delete the user's data on the storage servers.

Signing the signCertificate Request

HAWK provides one thing: integrity/authentication for the request contents (URL, method, and optionally the body). It does not provide confidentiality of the request, or integrity of the response, or confidentiality of the response.

For signCertificate(), we do not need request confidentiality or response confidentiality, since the client's pubkey and the resulting certificate will both be exposed over a similar SSL connection to the storage server later. And it is sufficient to rely on the response integrity provided by SSL, since the client can verify the returned certificate for itself.

Resetting the Account

resetAccount() needs request confidentiality, since the arguments include the newly wrapped kB value and the new SRP verifier, both of which enable a brute-force attack against the password. HAWK provides request integrity. The response is a single "ok" or "fail", conveyed by the HTTP headers, so we do not require response confidentiality, and can live without response integrity.

So the single-use resetToken is used to derive three values:

  • tokenID
  • request HMAC key
  • request XOR key


Deriving the resetAccount Keys

The request data will contain wrap(kB), a new (randomly-generated) SRP salt, and the new SRP verifier, all concatenated together. Since we always pad the SRP verifier to the full (256-byte) group length, all four pieces are fixed-length. We generate enough reqXORkey bytes to cover all four values.

The request data is XORed with requestXORkey, then delivered in the body of a HAWK request that uses tokenID as credentials.id and requestHMACkey as credentials.key . Note: it is critical to include the request body in the HAWK integrity check (options.payload=true, on both client and server), otherwise a man-in-the-middle could substitute their own SRP verifier, giving them control over the account (access to the user's class-A data, and a brute-force attack on their password).

Encrypting the resetAccount Request

Clients might use resetAccount for two reasons:

  • 1: Changing their password (i.e. they know the old one). In this case, the resetToken is acquired from getToken("reset"), and they know both kA and kB.
  • 2: resetting an account (i.e. they forgot the old password). Here, resetToken was acquired by proving control over the account email address (through a mechanism not described in this protocol). The client does not know kA or kB.


After using resetAccount, clients should immediately perform the getToken(sign) protocol. If the old password was forgotten, this is necessary to fetch kA. In either case, a new signToken is required, since old signTokens are revoked by resetAccount. Clients should retain the srpPassword value during this process to avoid needing to run the lengthy key-stretching routine a second time.

Crypto Notes

Strong entropy is needed in the following places:

  • (client) creation of private "a" value inside SRP
  • (server) initial creation of kA and wrap(kB)
  • (server) creation of private "B" value inside SRP
  • (server) creation of signToken and resetToken


On the server, code should get entropy from /dev/urandom via a function that uses it, like "crypto.randomBytes()" in node.js or "os.urandom()" in python. On the client, code should combine local entropy with some fetched from the keyserver via getEntropy(), to guard against failures in the local entropy pool. Something like HKDF(SKM=localEntropy+remoteEntropy, salt="", context=KW("mergeEntropy")).

An HKDF-based stream cipher is used to protect the response for getToken2(), and the request for resetAccount(). HKDF is used to create a number of random bytes equal to the length of the message, then these are XORed with the plaintext to produce the ciphertext. An HMAC is then computed from the ciphertext, to protect the integrity of the message.

HKDF, like any KDF, is defined to produce output that is indistinguishable from random data ("The HKDF Scheme", http://eprint.iacr.org/2010/264.pdf , by Hugo Krawczyk, section 3). XORing a plaintext with a random keystream to produce ciphertext is a simple and secure approach to data encryption, epitomized by AES-CTR or a stream cipher (http://cr.yp.to/snuffle/design.pdf). HKDF is not the fastest way to generate such a keystream, but it is safe, easy to specify, and easy to implement (just HMAC and XOR).

Each keystream must be unique. SRP is defined to produce a random session key for each run (as long as at least one of the sides provides a random ephemeral key). We define resetToken to be a single-use randomly-generated value. Hence our two HKDF-XOR keystreams will be unique.

A slightly more-traditional alternative would be to use AES-CTR (with the same HMAC-SHA256 used here), with a randomly-generated IV. This is equally secure, but requires implementors to obtain an AES library (with CTR mode, which does not seem to be universal). An even more traditional technique would be AES-CBC, which introduces the need for padding and a way to specify the length of the plaintext. The additional specification complexity, plus the library load, leads me to prefer HKDF+XOR.

kB is equal to the XOR of wrapKey (which is a deterministic function of the user's email address, password, mainSalt, and the stretching parameters) and the server's randomly-generated wrap(kB) value, making kB a random value too. Using XOR as a wrapping function allows us to avoid sending kB or wrap(kB) in the initial createAccount arguments, which are not protected by SRP, and thus would enable an eavesdropper to mount a dictionary attack on the password (using wrap(kB) as their oracle).

Likewise, allowing the server to generate kA avoids exposure during createAccount. The only point of vulnerability is a forgotten-password resetAccount() message, if an eavesdropper can learn the resetToken. In this case, the attacker might either use the resetToken themselves (then use signToken to learn kA and wrap(kB)), or passively decrypt the resetAccount() arguments to retrieve just wrap(kB). Given wrap(kB) and some encrypted browser data, the attacker can guess passwords (and derive kB, etc) until the data decrypts properly.

To make this technique safe, any time kB or the password is changed, the mainSalt should be changed too. Otherwise knowledge of both wrap(old-kB) and old-kB would reveal wrapKey, making it easy to deduce the new kB. Changing mainSalt causes wrapKey to change too, preventing this.

mainSalt is incorporated at the end of the stretching process to allow it to proceed in parallel with the getToken1() call that retrieves the salt. The inputs to the lengthy stretch come entirely from the user (email and password) or are optimistically (stretching parameters). This speedup seems more important than the minor security benefit of including the salt at the beginning of the stretch.

There is no MAC on wrap(kB). If the keyserver chooses to deliver a bogus wrap(kB) or kA, the client will discover the problem a moment later when it talks to a storage server and attempts to retrieve data from an unrecognized collection-ID (since we intend to derive collection-IDs from the key used to encrypt their data, which will be derived from kA or kB as appropriate). It might be useful to add a checksum to kA and wrap(kB) to detect accidental corruption (e.g. store and deliver kA+SHA256(kA)), but this doesn't protect against intentional changes. We omit this checksum for now, assuming that disks will be reliable enough to let us never experience such failures.

Proof-Of-Work

To protect the server's session table memory and CPU usage for the initial SRP calculation, the server might require clients to perform busy-work before calling getToken1(). The server can control how much work is required.

The getToken1() call looks for a "X-PiCL-PoW:" HTTP header. Most of the time, clients don't supply this header. But if the server responds to the getToken1() call with an error that indicates PoW is required, clients must create a valid PoW string and include it as the value of an "X-PiCL-PoW:" header in their next call to getToken1().

The server's error message includes two parameters. The first is a "prefix string": the client's PoW string is required to begin with this prefix. The second is a "threshold hash". SHA256(PoWString) is required to be lexicographically earlier than the thresholdHashString (i.e. the numerical value of its hash must be closer to zero than the threshold). The client is expected to concatenate the prefix with a counter, then repeatedly increment the counter and hash the result until they meet the threshold, then re-submit their getToken1() request with the combined prefix+counter string in the header. If the client has spent more than e.g. 10 seconds doing this, the client should probably help the user cancel the operation and try again.

When a server is under a DoS attack (either via some manual configuration tool or sensed automatically), it should start requiring valid unique X-PiCL-PoW headers. The server should initially require very little work, by using a threshold hash with just a few leading zero bits. If this is insufficient to reduce the attack volume, the threshold should be lowered, requiring even more work (from both the attacker and legitimate clients).

The server should create a prefix string that contains a parseable timestamp and a random nonce (e.g. "%d-%d-" % (int(time.time()), b32encode(os.urandom(8)))). The server should also decide on a cutoff time (perhaps ten minutes ago). Each server must then maintain a table of "old PoW strings" to prevent replay attacks (these do not need to be shared among all servers: an in-RAM cache is fine).

When the server receives a proposed PoW string, it first splits off the leading timestamp, and if the timestamp is older than the cutoff time, it rejects the string (either by dropping the connection, or returning a new "PoW required" error if it's feeling nice). Then it hashes the whole string and compares it against the threshold, rejecting those which fail to meet the threshold. Finally, for strings that pass the hash threshold, it checks the "old strings" table, and rejects any that appear on that list.

If the PoW string makes it past all these checks, the server should add the string to the "old strings" table, then accept the request (i.e. compute an srpB value and add a session-id table entry for the request).

The old-strings table check should be optimized to reject present strings quickly (i.e. if we are under attack, we should expect to see lots of duplicates of the same string, and must minimize the work we do when this occurs).

The server can remove values from the old-strings table that have timestamps older than the cutoff time. The server can also discard values at other times (to avoid consuming too much memory), without losing anything but protection against resource consumption.

Other notes:

The server-side code for this can be deferred until we care to have a response to a DoS attack. However the client-side code for this must be present from day one, otherwise we won't be able to turn on the defense later without fear of disabling legitimate old clients.

The server should perform as little work as possible before rejecting a token. Every extra CPU cycle it spends in this path is increasing the DoS attack amplification factor.

The nonce in the prefix string exists to make sure that two successive clients get different prefixes, and thus do not come up with the same counter value (and inadvertently create identical strings, looking like a replay attack). If this proved annoying or expensive, we could instead obligate clients to produce their own nonce.

TBD: Is this worth it? Should the PoW string go into an HTTP header? (I want it to be cheap to extract, and not clutter logs). Should the error response be a distinctive HTTP error code so our monitoring tools can easily count them? We can also use this feature to slow down online guessing attacks (i.e. trigger it either when getToken1 is called too much or when getToken2 produces too many errors). Since getToken1() includes an email address, we could also requires PoWs for some addresses (e.g. those we know to be under attack) but not others.

Glossary

This defines some of the jargon we've developed for this protocol.

  • data classes: each type of browser data (bookmarks, passwords, history, etc) can be assigned, by the user, to either class-A or class-B
  • class-A: data assigned to this class can be recovered, even if the user forgets their password, by proving control over an email address and resetting the account. It can also be read by Mozilla (since it runs the keyserver and knows kA), or by the user's IdP (by resetting the account without the user's permission).
  • class-B: data in this class cannot be recovered if the password is forgotten. It cannot be read by the IdP. Mozilla (via the keyserver) cannot read this data, but can attempt a brute-force dictionary attack against the password.
  • kA: the master key for data stored as "class-A", a 32-byte binary string. Individual encryption keys for different datatypes are derived from kA.
  • kB: the master key for data stored as "class-B", a 32-byte binary string.
  • wrap(kB): an encrypted copy of kB. The keyserver stores wrap(kB) and never sees kB itself. The client (browser) uses a key derived from the user's password to decrypt wrap(kB), obtaining the real kB.

Test Vectors

The following example uses a non-ASCII email address of "andré@example.org" (with an accented "e", UTF8 encoding is 616e6472c3a9406578616d706c652e6f7267) and a non-ascii password of "pässwörd" (with accents on "a" and "o", UTF8 encoding is 70c3a4737377c3b67264).

These test vectors were produced by the python code in https://github.com/warner/picl-spec-crypto . The diagrams may lag behind the latest version of that code.

stretch-KDF

email: 616e6472c3a94065 78616d706c652e6f 7267

password: 70c3a4737377c3b6 7264

K1: f84913e3d8e6d624 689d0a3e9678ac8d cc79d2c2f3d96414 88cd9d6ef6cd83dd

K2: 5b82f146a6412692 3e4167a0350bb181 feba61f63cb17140 12b19cb0be0119c5

stretchedPW: c16d46c31bee242c b31f916e9e38d60b 76431d3f5304549c c75ae4bc20c7108c

main-KDF

mainSalt (normally random): 0001020304050607 08090a0b0c0d0e0f 1011121314151617 18191a1b1c1d1e1f

srpPW: 1ea1cdd3ba3bb3a8 b6c46331123f48a6 746f143014f5a389 24e6fea4dc1c1289

unwrapBKey: 94995fc5423827df 42d598076eccd996 656183a309e9fbaf e5026431d338b115

internal x (base 10): 137598577746 20950182695987769526622297924214 84665105182182372602062190994896

internal x (hex): 030ac7c51717e1d5 35d59725cb7c49fb 4936b7db7fcd0f10 17d3f1ffef50e5d0

v (verifier as number) (base 10): 914205 81735470597826353382993806239740 11269014547415710721123623779303 40577934966702279822019429237166 85777414275311785862734740227831 07992726500267571929376592901913 18064022616547283873724124275736 15772870036501836242138681573039 79134196479839195060563726666145 91886023021879280485567507056439 54248700318708010771386921629212 79420638327471362434416724355461 72934544873168338790005382928033 88728242355654245060876357499918 14028809240099280481407481230320 68269674162426430261934197103097 99968937656431136067890224663385 48685839318699687268537410286424 14469699859895598445302042986990 86071206600454059861610331587789

SRP Verifier

k (base 10): 259003859907 09503006915442163037721228467470 35652616593381637186118123578112

srpSalt (normally random): 00f1000000000000 0000000000000000 0000000000000000 0000000000000070

srpVerifier: 00b9648c840be3e4 5ae305640dc24c64 ee3a1fb083bcafe4 0e10b37d04ea5a55 05c7538f6a72a6b9 748c97b2fd6d4dc4 89a2cbee5ae2ea9d cb7f2dbe1ad99518 75029ebb7e2f2bd5 bce1a619038092f4 2ea4ccba99665bc4 fd6c3c393d961b1c a2b8f61da5a81c2c cfdf89d28bc256fb b201b79908f64613 4a41fd1ae451f62f ccb00809b5ef8b05 7f198296e5aad231 baf321487d6abfed 2070556097720d5e f48d45724749c7a8 73768238bbd01123 0d004d5d487cc6ac ee40e6ab13a33f64 bd702d5c754167f2 230bb1d15151c070 7ef25d2787727424 32ea0537e95c1a04 d3006d10d99a1c7d 3318d284dc92460d 84dae38b4b698433 61008de94bd744cd

SRP B

private b (normally random) (base 10): 1198277 66042000957856349411550091527197 08125226960544768993643095227800 29105361555030352774505625606029 71778328003253459733139844872578 33596964143417216389157582554932 02841499373672188315534280693274 23189198736863575142046053414928 39548731879043135718309539706489 29157321423483527294767988835942 53343430842313006332606344714480 99439808861069316482621424231409 08830704769167700098392968117727 43420990997238759832829219109897 32876428831985487823417312772399 92628295469389578458363237146486 38545526799188280210660508721582 00403102624831815596140094933216 29832845626116777080504444704039 04739431335617585333671378812933

private b (hex): 00f3000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000005

transmitted srpB: 00de219f6b48de47 bb8a20e450d50cef 10c9c9fbc80482c2 9792f89f9bbfd27f a0e082209f919128 e29a8ffadfdcb48b e0000fc447d05afd 59b6032581de5596 8ff5f39034fc1aea d033b246660e4257 44fadcb824e7a14e fa6d2fc57502b9a7 b9cef2935a54c2c8 9d24589f1aa9091b 5f2981096e936592 dbbe0adfcb9b97f2 e677f5cb2112d90b 802af7df98eb29c5 31556f62d473e84e 50f70ac6d89e0503 228fb27eaca19f40 03f28516fb8b46c4 122510a9557c6d24 65bb13579e8ddbe5 7aa842d8ccd956f3 5643f43a4da35920 2485e21a6fede4a9 b3d55ee48eab9572 56f75283aed2c06a 9eb03f3feb29cb3b 6dbf644bcd8088cf 777072eb8b6b870e

SRP A

private a (normally random) (base 10): 1193346 47663227291363113405741243413916 43482736314616601220006703889414 28162541137108417166380088052095 43910927476491099816542561560345 50331133015255005622124012256352 06121987030570656676375703406470 63422988042473190059156975005813 46381864669664357382020200036915 26156674010218162984912976536206 14440782978764393137821956464627 16314542157937343986808167341567 89864323268060014089757606109012 50649711198896213496068605039486 22864591676298304745954690086093 75374681084741884719851454277570 80362211874088739962880012800917 05751238004976540634839106888223 63866455314898189520502368799907 19946264951520393624479315528231

private a (hex): 00f2000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000000 0000000000000a27

transmitted srpA: 00e1cad4005422e9 f1bf68ba186c75ed 73f199ec71c2cdf0 e74baa258f9e594a ab48c2d74f064e45 57cfbf05fa0a1c17 491ae8a0568f8f40 04a8529414c6d124 7bf4fe98d0596792 3997eb362fc0cb47 39fde90abe8623d2 38d80003bb6a66e0 987748a329596d54 a2514d50d426e2ac 00eb4c7939977bc1 535625c19e34db08 0a8742af30f3f975 bf668097d0c303c5 ad6408729dad779e ca0ebcb8688bff01 e7781510504fdd4a 5f3b7806353378b2 e98fa90ab9224d7a 9973becafd8b428a 9d8e64bc79cd505c 2f976adacc9947f3 0b1e819f3b958b18 be8f3d2c84726faa 23bdae8e74ecda07 d279f75b25cb9c33 274f42fe038c3d5b 058b2877eb60841c

SRP key-agreement

u: 802ae3bc2a4f3117 77e24280b08ceda0 99d46ba99a65a750 e771229efca2aea3

S: 00516c2f83d5602c 17547fba0c6f3171 bc2cb6462670ec08 f8a0d7bc46eb015b f40dfc06c5be9492 40628605bf2bf598 3ae15679cc7968eb a15249e96f561d47 f6b7d8b43e9ebaeb 127daa2536d52c05 d412c10ea2485fd0 bb90d716a9d36163 83fd3fbcc7981c3b 769bfb5c42244e83 8cfb9fbaf0f37e6b 3336e2af42c6e615 90a7be399352a2a8 b47d85437ffb0859 9f821993ef05328f d07470ba1db050bc d588cc10a6e8dd68 e61d787b5e4a0634 5d5e90ee60c5ab95 329de9526c5684bf 5dad4c75450885a3 249709d37e0ba85e 67ee644ca01ee1de 9ebe3b730a6dd188 6300abae80ebeb68 f0e141cb71a861c7 d42f50d1723fbe0b 78b46305dffb3b90

M1: f8c82e57d1771a24 229c05858cc03bee ea3a7b73d39939d5 a3a8dbbc9dc474f2

srpK: 94ad3e71e29ceb1f 2ed2b80996314344 6cfa5d8640c271dd b632f094f7eda7c7

getSignToken request

srpK: 94ad3e71e29ceb1f 2ed2b80996314344 6cfa5d8640c271dd b632f094f7eda7c7

respHMACkey: 001d14a524e7e7f0 1de527ad01dddce0 e64f915dca46242a 7795397d98cbbb16

respXORkey: e51958994bf03d02 f0651338ea18a186 7f2bb49089000a88 d367770bd9696b86 99c25804ff3ade0d 08622bd66b5b1332 4ce14f315a6dca6c 20a8b49e3743db31 8fb2670ba4b2d10f 416f61dd4eb7bf53 6a233cae88636a19 078213557e583622

plaintext: 2021222324252627 28292a2b2c2d2e2f 3031323334353637 38393a3b3c3d3e3f 4041424344454647 48494a4b4c4d4e4f 5051525354555657 58595a5b5c5d5e5f 6061626364656667 68696a6b6c6d6e6f 7071727374757677 78797a7b7c7d7e7f

ciphertext: c5387aba6fd51b25 d84c3913c6358fa9 4f1a86a3bd353cbf eb5e4d30e55455b9 d9831a47bb7f984a 402b619d27165d7d 1cb01d620e389c3b 78f1eec56b1e856e efd30568c0d7b768 29060bb622dad13c 1a524eddfc161c6e 7ffb692e0225485d

MAC: 40e3dd0d0b299033 a31222ceb3504ad4 7e55fc05f8b94402 2ed9e2be5c4be3e3

response: c5387aba6fd51b25 d84c3913c6358fa9 4f1a86a3bd353cbf eb5e4d30e55455b9 d9831a47bb7f984a 402b619d27165d7d 1cb01d620e389c3b 78f1eec56b1e856e efd30568c0d7b768 29060bb622dad13c 1a524eddfc161c6e 7ffb692e0225485d 40e3dd0d0b299033 a31222ceb3504ad4 7e55fc05f8b94402 2ed9e2be5c4be3e3

signCertificate

signToken: 6061626364656667 68696a6b6c6d6e6f 7071727374757677 78797a7b7c7d7e7f

tokenID: 8b5ff98850a2c98a 8059ee891c15b9a6 1af08356f54d865c 39f95f048d185195

reqHMACkey: 85f9aa22e9b35557 3504cd0e934a6c2b 1837fe6ca70d4932 627ba1c02b9aebc2

resetAccount

resetToken: 8081828384858687 88898a8b8c8d8e8f 9091929394959697 98999a9b9c9d9e9f

tokenID: 52437066aae511d3 3709bf25dc6a682a 7e943d49d94c84b3 4e1e6b11c9913159

reqHMACkey: 7de6c9b102dac62f 81d3a09baa00523d e7170ff17238b3af 8491e4cfb23e1a88

reqXORkey: 82d447f095aa8023 3eb5cb5d6c4eea25 5857809b6326b6bd 55fab2d3498b1cf8 a31bb0e319d7c0dc 2792740a480c1a98 99c1a6328bc2066e 3ecc9e8079ae8af6 046f15f3a586bfb3 b9908de7cd60b504 44fdfacc3cf32e2b efc72fca9063e28d a815989f86223394 b89db34bffdc94bb 68c05a49d1f1f63a 2c463d335a06c007

plaintext: 4041424344454647 48494a4b4c4d4e4f 5051525354555657 58595a5b5c5d5e5f a0a1a2a3a4a5a6a7 a8a9aaabacadaeaf b0b1b2b3b4b5b6b7 b8b9babbbcbdbebf c0c1c2c3c4c5c6c7 c8c9cacbcccdcecf d0d1d2d3d4d5d6d7 d8d9dadbdcdddedf e0e1e2e3e4e5e6e7 e8e9eaebecedeeef f0f1f2f3f4f5f6f7 f8f9fafbfcfdfeff

ciphertext: c29505b3d1efc664 76fc81162003a46a 0806d2c83773e0ea 0da3e88815d642a7 03ba1240bd72667b 8f3bdea1e4a1b437 297014813f77b0d9 8675243bc5133449 c4aed73061437974 7159472c01ad7bcb 942c281fe826f8fc 371ef5114cbe3c52 48f47a7c62c7d573 507459a013317a54 9831a8ba250400cd d4bfc7c8a6fb3ef8