This repository has been archived by the owner on Oct 18, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 17
/
scenarios.test.ts
324 lines (267 loc) · 10.3 KB
/
scenarios.test.ts
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
import * as crypto from 'crypto'
import {
hashAndInc,
newRandomPointEl,
randomBN
} from '@coil/privacypass-elliptic'
import * as elliptic from 'elliptic'
import BN from 'bn.js'
const p256 = new elliptic.ec('p256')
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const p256Order = p256.n!
const p256G = p256.g as Point
const randSecret = () => randomBN(p256Order)
const randomPoint = () => {
const random = newRandomPointEl()
return random.point
}
const hashPoints = (...pts: Point[]) => {
const h = crypto.createHash('sha256')
pts.forEach(pt => h.update(Buffer.from(pt.encodeCompressed())))
return h.digest() //
}
type Point = elliptic.curve.base.BasePoint
const divPt = (pt: Point, divisor: BN) => {
const divisorInverse = (divisor as any)._invmp(p256Order)
return pt.mul(divisorInverse)
}
const HMAC = (key: Point, message: Buffer) => {
const keyBuffer = Buffer.from(key.encodeCompressed())
const mac = crypto.createHmac('sha256', keyBuffer)
mac.update(message)
return mac.digest()
}
describe('PrivacyPass Scenarios as code scribbles', () => {
it('should describe scenario 1 - linkability', () => {
// ### client issue request
const T = randomPoint()
const creds = 'trackMe'
// The client takes a point on an elliptic curve T and sends it to the server.
const issueRequest = { T, creds }
// ### server issue response
const s = randSecret()
// The server applies a secret transformation
// (multiplication by a secret number s)
const sT = issueRequest.T.mul(s)
// and sends it back.
const issueResponse = { sT }
// Cheekily we keep track of theses issues
const trackerDB = new Map([[issueRequest.T, issueRequest.creds]])
// ### client redeem request
const redeemRequest = { sT: issueResponse.sT, T }
// ### server redeem response
expect(redeemRequest.T.mul(s).eq(redeemRequest.sT)).toBe(true)
// ## Problem: Linkability
// In this situation, the server knows T because it has seen it already.
// This lets the server connect the two requests, something we’re trying to avoid.
// This is where we introduce the blinding factor.
// Unfortunately we *can* track them
expect(trackerDB.get(redeemRequest.T)).toEqual('trackMe')
})
it('should describe scenario 2 - malleability', () => {
const creds = 'trackMe'
// ### client issue request
const T = randomPoint()
// Rather than sending T, the client generates its own secret number b.
const b = randSecret()
// The client multiplies the point T by b
const bT = T.mul(b)
// before sending it to the server
const issueRequest = { bT, creds }
// ### server issue response
const s = randSecret()
// The server does the same thing as in scenario 1
// (multiplies the point it receives by s).
// s(bT)
const sbT = issueRequest.bT.mul(s)
const issueResponse = { sbT }
// The client knows b.
// noinspection UnnecessaryLocalVariableJS
const knownB = b
// s(bT) is equal to b(sT) because multiplication is
// commutative.
{
const sT = T.mul(s)
// b(sT)
const bsT = sT.mul(knownB)
expect(sbT.eq(bsT)).toBe(true)
}
// The client can compute sT from b(sT) by dividing by b.
const bInverse = (knownB as any)._invmp(p256Order)
// TODO: why does the above work ??
// we can divide by b by multiplying by its inverse
const sT = issueResponse.sbT.mul(bInverse)
// works both ways
expect(sT.mul(b).eq(sbT)).toBe(true)
const redeemRequest = { T, sT }
// Since only the server knows s, it can confirm that sT
// is T multiplied by s and will verify the redemption.
expect(redeemRequest.T.mul(s).eq(redeemRequest.sT)).toBe(true)
// Problem: Malleability
{
const a = randSecret()
const aT = redeemRequest.T.mul(a)
const aST = redeemRequest.sT.mul(a)
expect(aT.mul(s).eq(aST)).toBe(true)
}
})
it('should describe scenario 3 - redemption hijacking', () => {
const creds = 'trackMe'
// Instead of picking an arbitrary point T, the client can pick a number t.
const t = randSecret()
const b = randSecret()
// The point T can be derived by hashing t to a point
// on the curve using a one-way hash.
const T = hashAndInc(t.toBuffer())
// ### Client Issue Request
const bT = T.mul(b)
const issueRequest = { bT, creds }
// ### Server Issue Response
const s = randSecret()
const sbT = issueRequest.bT.mul(s)
const issueResponse = { sbT }
// ### Client Redeem Request
const sT = divPt(issueResponse.sbT, b)
const redeemRequest = { t, sT }
// ### Server Redeem Response
{
const recomputedT = hashAndInc(t.toBuffer())
expect(recomputedT.mul(s).eq(redeemRequest.sT)).toBe(true)
}
// The hash guarantees that it’s hard to find another
// number that hashes to aT for an arbitrary a.
{
//
}
// ## Problem: Redemption hijacking
// If the values t and sT are sent across an unsecured network,
// an adversary could take them and use them for their own redemption.
// Sending sT is what lets attackers hijack a redemption.
// Since the server can calculate sT from t on it’s own,
// the client doesn’t actually need to send it.
// All the client needs to do is prove that it knows sT.
// A trick for doing this is to use t and sT to derive a HMAC key
// and use it to sign a message that relates to the redemption.
// Without seeing sT, the attacker will not be able to take this redemption
// and use it for a different message because it won’t be able to compute
// the HMAC key
})
it('should describe scenario 4 - tagging', () => {
// Instead of sending t and sT the client can send t and
// HMAC(sT, M) for a message M.
// When the server receives this, it calculates T = Hash(t), then uses
// its secret value to compute sT
/// With t and sT it can generate the HMAC key and check the signature.
// If the signature matches, that means the client knew sT.
const creds = 'trackMe'
const b = randSecret()
const t = randSecret()
const T = hashAndInc(t.toBuffer())
const bT = T.mul(b)
// ### Client Issue Request
const issueRequest = { bT, creds }
// ### Server Issue Response
const s = randSecret()
const sbT = issueRequest.bT.mul(s)
const issueResponse = { sbT }
// ### Client Redeem Request
const msg = Buffer.from('in a bottle')
const sT = divPt(issueResponse.sbT, b)
const redeemRequest = { t, M: msg, mac: HMAC(sT, msg) }
// ### Server Redeem Validation
{
const T = hashAndInc(redeemRequest.t.toBuffer())
const sT = T.mul(s)
const mac = HMAC(sT, redeemRequest.M)
expect(mac).toEqual(redeemRequest.mac)
}
// Problem: Tagging
// The server can use a different s for each client, say s1 for client 1
// and s2 for client 2. Then the server can identify the client by comparing
// s_1(H(t)) and s_2(H(t)) against the sT submitted by the client and seeing
// which one matches.
// This is where we introduce a zero-knowledge proof. We'll go into more
// detail about how these work in a later blog post. The specific proof
// we're using called a discrete logarithm equivalence proof (DLEQ)
// Those lucky enough to take the SAT before 2005 may remember the analogy
// section. You can think of a DLEQ proof in terms of an SAT analogy.
// It proves that two pairs of items are related to each other in in a
// similar way
// For example: puppies are to dogs as kittens are to cats. A kitten is a
// young cat and a puppy is a young dog. You can represent this with the
// following notation: puppy:dog == kitten:cat
// A DLEQ proves that two elliptic curve points are related by the same
// multiplicative factor without revealing that factor. Say you have a number
// s and two points P and Q. Someone with knowledge of s can construct a proof
// DLEQ(P:sP === Q:sQ). A third party with access to P, sP, Q, sQ can use
// DLEQ(P:sP === Q:sQ) to verify that the same value s was used without knowing
// what s is.
})
it('should describe scenario 5 - only one redemption per issuance', () => {
// The server picks a generator point G and publishes sG somewhere
// where every client knows it.
// Server side
const x = randSecret()
const G = randomPoint() // commitment
const sG = G.mul(x) // publically signed commitment, referred to as `H`
// ### Client Issue Request
const b = randSecret()
const t = crypto.randomBytes(32)
const T = hashAndInc(t)
const bT = T.mul(b)
const issueRequest = { bT }
/// Server Issue Response
const sBT = issueRequest.bT.mul(x)
const k = randSecret() // nonce
const A = G.mul(k)
const B = issueRequest.bT.mul(k)
const c = hashPoints(G, sG, issueRequest.bT, sBT, A, B)
const cn = new BN(c)
const s = k.sub(cn.mul(x)).umod(p256Order)
const DLEQ = { c, s }
const issueResponse = { sBT, DLEQ }
// Client verify
{
const {
sBT,
DLEQ: { c, s }
} = issueResponse
const cn = new BN(c)
const A2 = G.mul(s).add(sG.mul(cn))
const B2 = bT.mul(s).add(sBT.mul(cn))
const c2 = hashPoints(G, sG, bT, sBT, A2, B2)
expect(A.eq(A2)).toBe(true)
expect(B.eq(B2)).toBe(true)
expect(c.equals(c2)).toBe(true)
}
})
it('should describe scenario 6', () => {})
it('should describe scenario 7', () => {})
})
describe('DELQ proofs', () => {
it('should describe DLEQ ', () => {
// https://blog.cloudflare.com/privacy-pass-the-math/
// See DLEQ proofs
// Servers secret
const x = randSecret()
// Servers Commitment
const G = randomPoint()
const Y = G.mul(x)
// The blinded token point as submitted by the client
const M = randomPoint()
// The Server signed token point
const Z = M.mul(x)
// random Nonce
const k = randSecret()
const A = G.mul(k) // nonce signed commitment point
const B = M.mul(k) // nonce signed blinded token point
// challenge
const c = randSecret() // normally computed via hashing elements
const s = k.sub(c.mul(x).umod(p256Order)).umod(p256Order)
// S sends (c,s) to the user C
const Ac = G.mul(s).add(Y.mul(c))
const Bc = M.mul(s).add(Z.mul(c))
expect(Ac.eq(A)).toBe(true)
expect(Bc.eq(B)).toBe(true)
})
})